# Java 8 Stream ## 1. Stream 初體驗 **官方定義** >A sequence of elements supporting sequential and parallel aggregate operations. > >支持順序和並行聚合操作的一系列元素。 我們來解讀一下上面的那句話: 1. Stream是元素的集合,這點讓Stream看起來用些類似Iterator; 2. 可以支持順序和並行的對原Stream進行匯聚的操作; 大家可以把Stream當成一個高級版本的Iterator。 原始版本的Iterator,用戶只能一個一個的遍歷元素並對其執行某些操作; 高級版本的Stream,用戶只要給出需要對其包含的元素執行什麽操作,比如“過濾掉長度大於10的字符串”、“獲取每個字符串的首字母”等,具體這些操作如何應用到每個元素上,就給Stream就好了! 別急,我們來看一段Stream範例程式吧~ ```java= List<Integer> nums = Arrays.asList(1, null, 3, 4, null, 6); nums.stream().filter(n -> n != null).count(); ``` 剖析Stream通用語法 ![](https://i.imgur.com/0KeayIy.png) + 可以很清楚的看見:原本一條語句被三種顏色的框分割成了三個部分。 - 紅色框中的語句是一個Stream的生命開始的地方,負責創建一個Stream實例; - 綠色框中的語句是賦予Stream靈魂的地方,把一個Stream轉換成另外一個Stream,紅框的語句生成的是一個包含所有nums變量的Stream,進過綠框的filter方法以後,重新生成了一個過濾掉原nums列表所有null以後的Stream; - 藍色框中的語句是豐收的地方,把Stream的裏面包含的內容按照某種算法來匯聚成一個值,例子中是獲取Stream中包含的元素個數。 如果這樣解析以後,還不理解,那就只能動用“核武器”–圖形化,一圖抵千言! ![](https://i.imgur.com/V8Ih84K.png) #### 總結一下使用Stream的基本步驟: 1. 創建Stream; 2. 轉換Stream,每次轉換原有Stream對象不改變,返回一個新的Stream對象(**可以有多次轉換**); 3. 對Stream進行聚合(Reduce)操作,獲取想要的結果; ## 2. 創建Stream 最常用的創建Stream有兩種途徑: 1. 通過Stream接口的靜態工廠方法(註意:Java8裏接口可以帶靜態方法); 2. 通過Collection接口的默認方法(默認方法:Default method,也是Java8中的一個新特性,就是接口中的一個帶有實現的方法,後續文章會有介紹)–stream(),把一個Collection對象轉換成Stream #### 2.1 使用Stream靜態方法來創建Stream 1. of方法:有兩個overload方法,一個接受變長參數,一個接口單一值 ```java= Stream<Integer> integerStream = Stream.of(1, 2, 3, 5); Stream<String> stringStream = Stream.of("taobao"); ``` 2. generator方法:生成一個無限長度的Stream,其元素的生成是通過給定的Supplier(這個接口可以看成一個對象的工廠,每次調用返回一個給定類型的對象) ```java= Stream.generate(new Supplier<Double>() { @Override public Double get() { return Math.random(); } }); Stream.generate(() -> Math.random()); Stream.generate(Math::random); ``` 三條語句的作用都是一樣的,只是使用了lambda表達式和方法引用的語法來簡化代碼。每條語句其實都是生成一個無限長度的Stream,其中值是隨機的。這個無限長度Stream是懶加載,一般這種無限長度的Stream都會配合Stream的limit()方法來用。 3. iterate方法:也是生成無限長度的Stream,和generator不同的是,其元素的生成是重覆對給定的種子值(seed)調用用戶指定函數來生成的。其中包含的元素可以認為是:seed,f(seed),f(f(seed))無限循環 ```java= Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println); ``` 這段代碼就是先獲取一個無限長度的正整數集合的Stream,然後取出前10個打印。千萬記住使用limit方法,不然會無限打印下去。 #### 2.2 通過Collection子類獲取Stream 這個在初體驗的例子中就展示了從List對象獲取其對應的Stream對象,如果查看Java doc就可以發現Collection接口有一個stream方法,所以其所有子類都都可以獲取對應的Stream對象。 ```java public interface Collection<E> extends Iterable<E> { //其他方法省略 default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); } } ``` ## 3. 轉換Stream 轉換Stream其實就是把一個Stream通過某些行為轉換成一個新的Stream。 Stream接口中定義了幾個常用的轉換方法,下面我們挑選幾個常用的轉換方法來解釋。 1. distinct: 對於Stream中包含的元素進行去重操作(去重邏輯依賴元素的equals方法),新生成的Stream中沒有重覆的元素; ![](https://i.imgur.com/NWZdUrX.png) 2. filter: 對於Stream中包含的元素使用給定的過濾函數進行過濾操作,新生成的Stream只包含符合條件的元素; ![](https://i.imgur.com/BxgqAaT.png) 3. map: 對於Stream中包含的元素使用給定的轉換函數進行轉換操作,新生成的Stream只包含轉換生成的元素。這個方法有三個對於原始類型的變種方法,分別是:mapToInt,mapToLong和mapToDouble。這三個方法也比較好理解,比如mapToInt就是把原始Stream轉換成一個新的Stream,這個新生成的Stream中的元素都是int類型。之所以會有這樣三個變種方法,可以免除自動裝箱/拆箱的額外消耗; ![](https://i.imgur.com/f1rnzV6.png) 4. flatMap:和map類似,不同的是其每個元素轉換得到的是Stream對象,會把子Stream中的元素壓縮到父集合中; ![](https://i.imgur.com/utjGXoQ.png) 5. peek: 生成一個包含原Stream的所有元素的新Stream,同時會提供一個消費函數(Consumer實例),新Stream每個元素被消費的時候都會執行給定的消費函數; ![](https://i.imgur.com/tpWTYaB.png) 6. limit: 對一個Stream進行截斷操作,獲取其前N個元素,如果原Stream中包含的元素個數小於N,那就獲取其所有的元素; ![](https://i.imgur.com/rVH4pHu.png) 7. skip: 返回一個丟棄原Stream的前N個元素後剩下元素組成的新Stream,如果原Stream中包含的元素個數小於N,那麽返回空Stream; ![](https://i.imgur.com/FUTmsYI.png) 8. 組合技 ```java= List<Integer> nums = Arrays.asList(1,1,null,2,3,4,null,5,6,7,8,9,10); System.out.println(“sum is:” + nums.stream() .filter(num -> num != null) .distinct() .mapToInt(num -> num * 2) .peek(System.out::println) .skip(2) .limit(4) .sum() ); ``` 9. 效能問題 在對於一個Stream進行多次轉換操作,每次都對Stream的每個元素進行轉換,而且是執行多次,這樣時間覆雜度就是一個for循環裏把所有操作都做掉的N(轉換的次數)倍啊。 其實不是這樣的,轉換操作都是lazy的,多個轉換操作只會在Reduce操作(見下節)的時候融合起來,一次循環完成。 我們可以這樣簡單的理解,Stream裏有個操作函數的集合,每次轉換操作就是把轉換函數放入這個集合中,在匯聚操作的時候循環Stream對應的集合,然後對每個元素執行所有的函數。 ## 4. Reduce Stream Java doc定義 >A reduction operation (also called a fold) takes a sequence of input elements and combines them into a single summary result by repeated application of a combining operation, such as finding the sum or maximum of a set of numbers, or accumulating elements into a list. The streams classes have multiple forms of general reduction operations, called reduce() and collect(), as well as multiple specialized reduction forms such as sum(), max(), or count(). > 簡單翻譯一下:Reduce操作接受一個元素序列為輸入,反覆使用某個合併操作,把序列中的元素合併成一個Reduce的結果。比如查找一個數字列表的總和或者最大值,或者把這些數字累積成一個List對象。 Stream接口有一些通用的Reduce操作,比如reduce()和collect();也有一些特定用途的Reduce操作,比如sum(),max()和count()。 注意:sum方法不是所有的Stream對象都有的,只有IntStream、LongStream和DoubleStream是實例才有。 #### 4.1 collect方法 ```java= <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner); ``` 直接看例子吧~ ```java= List<Integer> nums = Arrays.asList(1,1,null,2,3,4,null,5,6,7,8,9,10); List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null) .collect(() -> new ArrayList<Integer>(), (list, item) -> list.add(item), (list1, list2) -> list1.addAll(list2)); ``` - 第一個函數生成一個新的ArrayList實例; - 第二個函數接受兩個參數,第一個是前面生成的ArrayList對象,二個是stream中包含的元素,函數體就是把stream中的元素加入ArrayList對象中。第二個函數被反覆調用直到原stream的元素被消費完畢; - 第三個函數也是接受兩個參數,這兩個都是ArrayList類型的,函數體就是把第二個ArrayList全部加入到第一個中; 嗯... 還好Java 8 有一個 Collector的工具類 – [Collectors] ```java= List<Integer> numsWithoutNull = nums.stream().filter(num -> num != null).collect(Collectors.toList()); ``` 簡潔許多了,還有許多方法。可參閱 [官方文件](http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html) #### 4.2 reduce方法 reduce方法有三個override的方法 4.2.1 - 第一種 >Optional<T> reduce(BinaryOperator<T> accumulator) 接受一個BinaryOperator類型的參數,直接看範例吧~ **ReduceDemo1.java** ```java= int[] array = {23,43,56,97,32}; Arrays.stream(array).reduce((x,y) -> x+y).ifPresent(System.out::println); Arrays.stream(array).reduce(Integer::sum).ifPresent(System.out::println); Arrays.stream(array).reduce(StatisticsUtility::addIntData).ifPresent(System.out::println); ``` **StatisticsUtility.java** ```java= public static int addIntData(int num1, int num2) { return num1 + num2; } ``` 可以看到reduce方法接受一個函數,這個函數有兩個參數,第一個參數是上次函數執行的返回值(也稱為中間結果),第二個參數是stream中的元素,這個函數把這兩個值相加,得到的和會被賦值給下次執行這個函數的第一個參數。要注意的是:**第一次執行的時候第一個參數的值是Stream的第一個元素,第二個參數是Stream的第二個元素**。這個方法返回值類型是Optional,這是Java8防止出現NPE的一種可行方法。 - 示意圖 ![](http://img03.taobaocdn.com/imgextra/i3/90219132/T28rVAXJlaXXXXXXXX_!!90219132.jpg) 4.2.2 - 第二種 >T reduce(T identity, BinaryOperator<T> accumulator) 這個定義上上面已經介紹過的基本一致,不同的是:它允許用戶提供一個循環計算的初始值,如果Stream為空,就直接返回該值。而且這個方法不會返回Optional,因為其不會出現null值。 **ReduceDemo2** ```java= int[] array = {23,43,56,97,32}; //Set start value. Result will be startvalue + sum of array. int startValue = 100; int sum = Arrays.stream(array).reduce(startValue, (x,y) -> x+y); System.out.println(sum); sum = Arrays.stream(array).reduce(startValue, Integer::sum); System.out.println(sum); sum = Arrays.stream(array).reduce(startValue, StatisticsUtility::addIntData); System.out.println(sum); ``` 4.2.3 - 第三種 > \<U> U reduce(U identity, BiFunction<U,super T,U> accumulator, BinaryOperator\<U> combiner) > **ReduceDemo3** ```java= List<Integer> list2 = Arrays.asList(2, 3, 4); //Here result will be 2*2 + 2*3 + 2*4 that is 18. int res = list2.parallelStream().reduce(2, (s1, s2) -> s1 * s2, (p, q) -> p + q); System.out.println(res); List<String> list1 = Arrays.asList("Mohan", "Sohan", "Ramesh"); String result = list1.parallelStream().reduce("-", (s1, s2) -> s1 + s2, (p, q) -> p + q); System.out.println(result); ``` #### 其他的方法 - count : 獲取Stream中元素的個數 - allMatch:是不是Stream中的所有元素都滿足給定的匹配條件 - anyMatch:Stream中是否存在任何一個元素滿足匹配條件 - findFirst: 返回Stream中的第一個元素,如果Stream為空,返回空Optional - noneMatch:是不是Stream中的所有元素都不滿足給定的匹配條件 - max和min:使用給定的比較器(Operator),返回Stream中的最大|最小值