本次,讀了兩本書,一本是《Beginning Java 8 Language Features》,一本是《Java 8 實戰》,有感。感受平時咱們都是使用了個Java8相關特性的皮毛。加上之前面試被人問:你知道一個列表我想同時根據不一樣字段,一次進行分組,怎麼作。我以爲有必要開一個深刻使用Java8的系列文章,來總結總結。本次主要是對流的一次性深刻。其中涉及了咱們不少沒有使用過的點。針對經常使用的,我在此就不在總結java
這裏我先舉個咱們平常使用流的一個例子,而後根據這個例子進行幾幅圖的解說,可以徹底把流內部的原理理解清楚,不只僅侷限於表現上面的使用程序員
請看代碼:面試
import java.util.ArrayList; import java.util.List; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; public class StramMain { private class Dash{ private int calories; private String name; public int getCalories() { return calories; } public void setCalories(int calories) { this.calories = calories; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public static void main(String[] args) { List<Dash> menus = new ArrayList<>(); //List<String> lowColoricDishesName = menus.parallelStream() 並行流 List<String> lowColoricDishesName = menus.stream() .filter(d -> d.getCalories() < 400)//過濾低於400卡路里 .sorted(comparing(Dash::getCalories))//根據過濾以後進行排序 .map(Dash::getName)//對象映射成String類型 .collect(toList());//結束操做,轉成list輸出 System.out.println(lowColoricDishesName.toString()); } }
對於上面代碼中使用的流的過程,大體總結成下面的流程:數據庫
相似於一個流水線,每經歷一個節點,都會對原有的集合數據進行一個」加工「的過程,最後加工完了再集中輸出成成品。這裏有幾個感念要理解下:json
整個流的過程就像一個流水線,collect就像是這個流水線的開關:咱們首先要把這個流水線要作的工序,都安排好,而後最後,咱們一開開關(collect),集合中的每個數據,挨個的一個接一個從流水線上面流過,通過一箇中間操做的節點,就會進行一個加工,最後流入一個新的集合裏面。這就是整個過程。下面是一個更細化的圖:數組
這樣作的優勢是:數據結構
除開咱們經常使用的一些中間操做:app
找一些平時想不太到的中間操做講講dom
傳統操做將一個字符串數組中的全部字符以一個List輸出,不能有重複,例如:ide
String[] words = {"jicheng","gufali"}
變成:List<String> = {"j","i","c","h","e","n","g","u","f","a","l"}
《Java 8 實戰》裏面嘗試了兩種方式,我以爲,頗有助於咱們思考這個拍扁操做的原理
public class StramMain { public static void main(String[] args) { String[] words = {"jicheng", "gufali"}; List<String[]> list = Arrays.stream(words) .map(value -> value.split("")) .distinct() .collect(toList()); System.out.println(list); } }
結果以下圖:
其實map中間操做裏面把源變成了兩個Stream<String[]>
這種類型,最後輸出成list的時候,就成了List<String[]>
,顯然和咱們想要的十萬八千里
代碼以下:
import java.util.Arrays; import java.util.List; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; public class StramMain { public static void main(String[] args) { List<String> words = Arrays.asList("jicheng", "gufali"); List<Stream<String>> collect = words.stream() .map(value -> value.split("")) .map(Arrays::stream) .collect(toList()); System.out.println(collect); } }
Stream<String[]>
類型Stream<String>
這樣一個Stream流List<Stream<String>>
類型顯然,也不是咱們要的
代碼以下,利用了上面的合併數組爲一個流的操做public static <T> Stream<T> stream(T[] array)
:
import static java.util.stream.Collectors.toList; public class StramMain { public static void main(String[] args) { List<String> words = Arrays.asList("jicheng", "gufali"); List<String> collect = words.stream() .map(value -> value.split("")) .flatMap(Arrays::stream) .distinct() .collect(toList()); System.out.println(collect); // result:[j, i, c, h, e, n, g, u, f, a, l] } }
下面是過程的流程圖:
使用代碼:
Optional<Dish> dish =menu.stream() .filter(Dish::isVegetarian) .findAny(); boolean isPresent = dish.isPresent();
查到一頓流操做以後的其中一個或者任意一個。幾點值得注意:
ifPresent(Consumer<T> block)
,若是值存在的話,會執行block這東西,相似於把集合裏面的全部元素進行一個大彙總(求和、最大最小值、平均值等),下面是源碼中的reduce方法:
Optional<T> reduce(BinaryOperator<T> accumulator);//① <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);//② T reduce(T identity, BinaryOperator<T> accumulator);//③
代碼以下:
public class StramMain { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Integer sum = numbers.stream().reduce(0, (a, b) -> a + b); System.out.println(sum); // result:55 } }
解說:首先,0做爲Lambda(a)的 第一個參數,從流中得到1做爲第二個參數(b)。0 + 1獲得1,它成了新的累積值。而後再用累 積值和流中下一個元素2調用Lambda,產生新的累積值3。接下來,再用累積值和下一個元素3 調用Lambda,獲得6。以此類推,獲得最終結果21。
public class StramMain { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Optional<Integer> reduce = numbers.stream().reduce((a, b) -> a + b); System.out.println(reduce.get()); // result:55 } }
結果是同樣的,表示:若是沒有初始值,流操做會取第一個數組的值,做爲初始值,因爲不肯定列表是否是有值的,若是沒值,第一個數值就去不到,那求和就不成功,就沒有值。因此返回一個Optional的對象
Optional<Integer> max = numbers.stream().reduce(Integer::max); Optional<Integer> min = numbers.stream().reduce(Integer::min);
一樣的,這個一樣也能夠有個初試的值
public class StramMain { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); Integer max = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max); Integer min = numbers.stream().reduce(Integer.MAX_VALUE, Integer::min); System.out.println("max number:"+max+",min number:"+min); // result:max number:10,min number:1 } }
Java 8引入了三個原始類型: IntStream 、 DoubleStream 和LongStream,分別將流中的元素特化爲int、long和double,從而避免了暗含的裝箱成本。每 個接口都帶來了進行經常使用數值歸約的新方法,好比對數值流求和的sum,找到最大元素的max。 此外還有在必要時再把它們轉換回對象流的方法。要記住的是,這些特化的緣由並不在於流的複雜性,而是裝箱形成的複雜性——即相似int和Integer之間的效率差別。
public class StramMain { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1,2,3,4,5); int sum = numbers.stream() .mapToInt(value -> value) .sum(); System.out.println(sum); // result:15 } }
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); Stream<Integer> stream = intStream.boxed();
public class StramMain { public static void main(String[] args) { List<Integer> numbers = Arrays.asList(1,2,3,4,5); OptionalInt max = numbers.stream() .mapToInt(value -> value) .max(); OptionalInt min = numbers.stream() .mapToInt(value -> value) .min(); int maxValue = max.orElse(Integer.MAX_VALUE);//若是沒有最大值默認給一個最大值 int minValue = min.orElse(Integer.MIN_VALUE);//若是沒有最小值默認給一個最小值 } }
Java 8引入了兩個能夠用於IntStream和LongStream的靜態方法,幫助生成這種範圍: range和rangeClosed。這兩個方法都是第一個參數接受起始值,第二個參數接受結束值。但 range是不包含結束值的,而rangeClosed則包含結束值:
IntStream evenNumbers = IntStream.rangeClosed(1, 100) .filter(n -> n % 2 == 0);//偶數流 System.out.println(evenNumbers.count());
c、一個風騷的操做:求勾股數
public class StramMain { public static void main(String[] args) { Stream<double[]> pythagoreanTriples = IntStream.rangeClosed(1, 100) .boxed() .flatMap(a -> IntStream.rangeClosed(a, 100) .mapToObj(b -> new double[]{a, b, Math.sqrt(a*a + b*b)}) .filter(t -> t[2] % 1 == 0)); pythagoreanTriples.limit(3).forEach(value->{ System.out.println(value[0]+","+value[1]+","+value[2]); }); /** * 結果: * 3.0,4.0,5.0 * 5.0,12.0,13.0 * 6.0,8.0,10.0 */ } }
Stream API提供了兩個靜態方法來從函數生成流:Stream.iterate和Stream.generate。 這兩個操做能夠建立所謂的無限流:不像從固定集合建立的流那樣有固定大小的流。
public class StramMain { public static void main(String[] args) { Stream.iterate(0, n -> n + 2) .limit(10)//註釋掉這一行,就會無限循環的生成下去 .forEach(System.out::println); } }
解釋:流的第一個元素是初始值0。而後加 上2來生成新的值2,再加上2來獲得新的值4,以此類推。這種iterate操做基本上是順序的, 由於結果取決於前一次應用。請注意,此操做將生成一個無限流——這個流沒有結尾,由於值是 按需計算的,能夠永遠計算下去。
下面的是generate的無限流:
public class StramMain { public static void main(String[] args) { Stream.generate(Math::random) .limit(5) .forEach(System.out::println); } }
細細讀了《Java8 實戰》,發現其實終端操做纔是真正的大殺器!哪怕是一些中間操做的功能,再終端操做也是能夠完成的。包括裏面的不少設計理念,更是錯中複雜。我這回集中講講下面的幾個點:
List<Object>
變換成Map<Object,Object>
,經常使用操做其實在中間操做中,同樣能夠完成此操做
下面是一系列歸約彙總的代碼示例片斷,其實不難:
import com.alibaba.fastjson.JSON; import java.util.Arrays; import java.util.Comparator; import java.util.IntSummaryStatistics; import java.util.List; import java.util.function.Function; import static java.util.stream.Collectors.*; public class StramMain { public static void main(String[] args) { List<Integer> intList = Arrays.asList(12, 23, 34, 54); //計算一共有多少個值 Long collect = intList.stream().collect(counting()); //同上 Long sameWithCollect = intList.stream().count(); System.out.println("一共有多少個數字:" + collect); //查找最大值 intList.stream() .collect(maxBy(Comparator.comparing(Function.identity()))) .ifPresent(integer -> { System.out.println("數字中的最大值:" + integer); }); Integer integer = intList.stream() .collect(summingInt(value -> value.intValue())); System.out.println("全部數字的和是:"+integer); Double averageNumber = intList.stream() .collect(averagingInt(value -> value.intValue())); System.out.println("平均數是:"+averageNumber); IntSummaryStatistics intSummaryStatistics = intList.stream() .collect(summarizingInt(value -> value.intValue())); System.out.println("全部的歸約彙總的結果對象是:" + JSON.toJSONString(intSummaryStatistics)); /** * result: * 一共有多少個數字:4 * 數字中的最大值:54 * 全部數字的和是:123 * 平均數是:30.75 * 全部的歸約彙總的結果對象是:{"average":30.75,"count":4,"max":54,"min":12,"sum":123} */ } }
joining()
工廠方法返回的收集器會把對流中每個對象應用toString方法獲得的全部字符 串鏈接成一個字符串。另外,joining在內部使用了StringBuilder來把生成的字符串逐個追加起來。
import static java.util.stream.Collectors.*; public class StramMain { public static void main(String[] args) { List<Integer> intList = Arrays.asList(12, 23, 34, 54); String stringJoin = intList.stream() .map(value -> value.toString()) .collect(joining(",")); System.out.println(stringJoin); // result: 12,23,34,54 } }
上面兩小節的歸約操做,其實都是基於一個底層的操做進行的,這個底層的歸約操做就是:reducing()
,能夠說上面全部的歸約操做都是當前reducing操做的特殊化,僅僅是方便程序員罷了。固然,方便程序員但是頭等大事兒。說白了,特殊化的歸約,是便於閱讀與書寫的一種模板。
import java.util.Arrays; import java.util.List; import static java.util.stream.Collectors.reducing; public class StramMain { public static void main(String[] args) { List<Integer> intList = Arrays.asList(12, 23, 34, 54); Integer sumNumber = intList.stream() //注意這裏的reducing方法 .collect(reducing(0, value -> value.intValue(), Integer::sum)); System.out.println("求和:"+sumNumber); // result: 123 } }
三個參數的意義:
這個話題,是我曾經的一次面試中經歷過的問題:咱們如何實現首先經過一個字段分組以後,在經過另一個字段進行再次的分組呢?當時本身只常常操做一個字段分組的樣子,並無繼續的研究如何經過一個以上字段進行連續分組。因此最終答得也不是很好。其實就是想用流這東西作到相似於數據庫裏面:group by col1,col2,這種操做。最終的結果數據結構,大致上是:Map<K,Map<T,List<O>>>
。要實現很簡單,咱們從的源碼中進行分析:
public final class Collectors { ... //① public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) { ... } //② public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) { ... } ... }
Map<K,List<O>>
類型Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream() .collect(groupingBy(Dish::getType, groupingBy(dish -> { // 這裏進行二次分組的實現函數 if (dish.getCalories() <= 400) return CaloricLevel.DIET; else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL; else return CaloricLevel.FAT; })));
這個操做也是很經常使用的,而且常常被咱們忽視的方法。若是一個List是咱們從數據庫裏面查出來的對象,裏面有id和其餘的值,咱們每每想快速經過id定位到一個具體的對象,那就須要將這個List裝換成一個以id爲key的map。以往咱們居然本身手寫map,有了toMap操做,簡直不能再簡單了!咱們來看看源碼中的toMap的幾種重載:
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new); }//① public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) { return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); }//② public static <T, K, U, M extends Map<K, U>> Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier) { BiConsumer<M, T> accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); }//③
咱們發現全部方法其實底層都死調用了③這個方法的。先解說下如何使用:
Map<Integer,Person> idToPerson = persons.stream() .collect(Collectors.toMap(Person::getId,Funtion.identity())); Map<Integer,Person> idToPerson = persons.stream() .collect(Collectors.toMap(Person::getId ,Funtion.identity() ,(existValue,newValue->existValue))); TreeMap<Integer,Person> idToPerson = persons.stream() .collect(Collectors.toMap(Person::getId ,Funtion.identity() ,(existValue,newValue->existValue) ,TreeMap::new));