【Java 8系列】Stream詳解

熱門系列:


目錄

1.前言

1.1 爲什麼要用Stream

1.2 什麼是聚合操作

2.正文

2.1 Stream操作分類

2.2 Stream API使用

2.2.1 Stream 構成與創建

2.2.2 無狀態(Stateless)操作

2.2.3 有狀態(Stateful)操作

2.2.4 短路(Short-circuiting)操作

2.2.5 非短路(Unshort-circuiting)操作

3.總結


1.前言

Java 8的另一大亮點Stream,它與 java.io 包裏的 InputStream 和 OutputStream 是完全不同的概念。

Java 8 中的 Stream 是對集合(Collection)對象功能的增強,它專注於對集合對象進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量數據操作 (bulk data operation)。

Stream API 藉助於同樣新出現的 Lambda 表達式,極大的提高編程效率和程序可讀性。同時它提供串行和並行兩種模式進行匯聚操作,併發模式能夠充分利用多核處理器的優勢,使用 fork/join 並行方式來拆分任務和加速處理過程。

1.1 爲什麼要用Stream

我個人總結有如下幾個特點:

  • 有高效率的並行操作
  • 有多中功能性的聚合操作
  • 函數式編程,使代碼更加簡潔,提高編程效率

1.2 什麼是聚合操作

舉個例子,例如我們現在有一個模塊的列表需要做如下處理:

  • 客戶每月平均消費金額
  • 最昂貴的在售商品
  • 本週完成的有效訂單(排除了無效的)
  • 取十個數據樣本作爲首頁推薦

以上這些操作,你可以理解爲就是對一個列表集合的聚合操作啦,類似於SQL裏面的(count()、sum()、avg()....)!

有一些操作,有人可能會說,可以在SQL語句中完成過濾分類!首先不說SQL不能實現的功能,即使SQL能夠實現,但是數據庫畢竟是用來讀寫數據的,主要功能是用於數據落地存儲的。並不是用來做大量的邏輯處理的,所以不能爲了圖方便,而忽略了性能方面的損耗!所以,相比之下,有一些列表操作我們必須在程序中做邏輯處理!那如果我們用之前的java處理方式,得像如下操作一樣:


  
  
    
  
  
  
  1. for( int i= 0;i< 10;i++){
  2. if(....){
  3. //內部做一系列的邏輯判斷處理
  4. //也
  5. //許
  6. //有
  7. //這
  8. //麼
  9. //多
  10. //行
  11. //還
  12. //不
  13. //止
  14. } else{
  15. //吧啦吧啦吧啦.......
  16. }
  17. }

那如果用Stream來處理的話,可能就只有如下簡單幾行:

list.stream().filter().limit(10).foreach();
  
  
    
  
  
  

所以,代碼不僅簡潔了,閱讀起來也會很是方便!


2.正文

2.1 Stream操作分類

Stream的操作可以分爲兩大類:中間操作、終結操作

中間操作可分爲:

  • 無狀態(Stateless)操作:指元素的處理不受之前元素的影響
  • 有狀態(Stateful)操作:指該操作只有拿到所有元素之後才能繼續下去

終結操作可分爲:

  • 短路(Short-circuiting)操作:指遇到某些符合條件的元素就可以得到最終結果
  • 非短路(Unshort-circuiting)操作:指必須處理完所有元素才能得到最終結果

 

Stream結合具體操作,大致可分爲如下圖所示:

 

2.2 Stream API使用

接下來,我們將按各種類型的操作,對一些常用的功能API進行一一講解:

2.2.1 Stream 構成與創建

2.2.1.1 流的構成

當我們使用一個流的時候,通常包括三個基本步驟:

獲取一個數據源(source)→ 數據轉換 → 執行操作獲取想要的結果,每次轉換原有 Stream 對象不改變,返回一個新的 Stream 對象(可以有多次轉換),這就允許對其操作可以像鏈條一樣排列,變成一個管道。

如下圖所示:

圖 1. 流管道 (Stream Pipeline) 的構成

2.2.1.2 流的創建

  • 通過 java.util.Collection.stream() 方法用集合創建流

  
  
     
  
  
  
  1. List<String> list = Arrays.asList( "hello", "world", "stream");
  2. //創建順序流
  3. Stream<String> stream = list.stream();
  4. //創建並行流
  5. Stream<String> parallelStream = list.parallelStream();
  • 使用java.util.Arrays.stream(T[] array)方法用數組創建流

  
  
     
  
  
  
  1. String[] array = { "h", "e", "l", "l", "o"};
  2. Stream<String> arrayStream = Arrays.stream(array);
  • Stream的靜態方法:of()、iterate()、generate()

  
  
     
  
  
  
  1. Stream<Integer> stream1 = Stream.of( 1, 2, 3, 4, 5, 6);
  2. Stream<Integer> stream2 = Stream.iterate( 0, (x) -> x + 2).limit( 3);
  3. stream2.forEach(System.out::println);
  4. Stream<Double> stream3 = Stream.generate(Math::random).limit( 3);
  5. stream3.forEach(System.out::println)
  6. //輸出結果如下:
  7. 0
  8. 2
  9. 4
  10. 0.9620319103852426
  11. 0.8303672905658537
  12. 0.09203215202737569
  • streamparallelStream的簡單區分

stream是順序流,由主線程按順序對流執行操作,而parallelStream是並行流,內部以多線程並行執行的方式對流進行操作,需要注意使用並行流的前提是流中的數據處理沒有順序要求(會亂序,即使用了forEachOrdered。例如篩選集合中的奇數,兩者的處理不同之處:

image-20201125155309486

當然,除了直接創建並行流,還可以通過parallel()把順序流轉換成並行流:

Optional<Integer> findFirst = list.stream().parallel().filter(x->x>4).findFirst();
  
  
     
  
  
  

 

2.2.2 無狀態(Stateless)操作

  •  filter篩選,是按照一定的規則校驗流中的元素,將符合條件的元素提取到新的流中的操作。
Stream<T> filter(Predicate<? super T> predicate);
  
  
     
  
  
  

流程解析圖如下:

20201109144706541

舉個栗子:


  
  
     
  
  
  
  1. public static void main(String[] args) {
  2. List<Integer> list = Arrays.asList( 6, 7, 3, 8, 1, 2);
  3. Stream<Integer> stream = list.stream();
  4. stream.filter(x -> x > 5).forEach(System.out::println);
  5. }
  6. //結果如下:
  7. 6
  8. 7
  9. 8

 

  • 映射(map、flatMap、peek)

①map:一個元素類型爲 T 的流轉換成元素類型爲 R 的流,這個方法傳入一個Function的函數式接口,接收一個泛型T,返回泛型R,map函數的定義,返回的流,表示的泛型是R對象;

簡言之:將集合中的元素A轉換成想要得到的B

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
  
  
     
  
  
  

流程解析圖如下:

Snipaste_2020-11-27_11-44-32

舉個栗子:


  
  
     
  
  
  
  1. //使用的People對象
  2. public class People {
  3. private String name;
  4. private int age;
  5. ...省略get,set方法
  6. }
  7. //將String轉化爲People對象
  8. Stream.of( "小王:18", "小楊:20").map( new Function<String, People>() {
  9. @Override
  10. public People apply(String s) {
  11. String[] str = s.split( ":");
  12. People people = new People(str[ 0],Integer.valueOf(str[ 1]));
  13. return people;
  14. }
  15. }).forEach(people-> System.out.println( "people = " + people));
  16. }

或如下(衆多姿勢,任君選擇):


  
  
     
  
  
  
  1. List<String> output = wordList.stream().
  2. map(String::toUpperCase).
  3. collect(Collectors.toList());

 

②flatMap:接收一個函數作爲參數,將流中的每個值都換成另一個流,然後把所有流連接成一個流。

簡言之:與Map功能類似,區別在於將結合A的流轉換成B流


  
  
     
  
  
  
  1. <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

流程解析圖如下:

2020110914550762

舉個栗子:


  
  
     
  
  
  
  1. public static void main(String[] args) {
  2. List<String> list1 = Arrays.asList( "m,k,l,a", "1,3,5,7");
  3. List<String> listNew = list1.stream().flatMap(s -> {
  4. // 將每個元素轉換成一個stream
  5. String[] split = s.split( ",");
  6. Stream<String> s2 = Arrays.stream(split);
  7. return s2;
  8. }).collect(Collectors.toList());
  9. System.out.println( "處理前的集合:" + list1);
  10. System.out.println( "處理後的集合:" + listNew);
  11. }
  12. //結果如下:
  13. //這個結果的引號是不存在的,爲了方便閱讀,我手動添加的
  14. 處理前的集合:[ "m,k,l,a", "1,3,5,7"]
  15. 處理後的集合:[ "m", "k", "l", "a", "1", "3", "5", "7"]

 

③peek:peek 操作接收的是一個 Consumer<T> 函數。顧名思義 peek 操作會按照 Consumer<T> 函數提供的邏輯去消費流中的每一個元素,同時有可能改變元素內部的一些屬性。

Stream<T> peek(Consumer<? super T> action);
  
  
     
  
  
  

這裏我們要提一下這個 Consumer<T> ,以理解什麼是消費。

Consumer<T> 是一個函數接口。一個抽象方法 void accept(T t) 意爲接受一個 T 類型的參數並將其消費掉。其實消費給我的感覺就是 “用掉” ,自然返回的就是 void 。通常“用掉” T 的方式爲兩種:

  • T 本身的 void 方法 比較典型的就是 setter 。

  • 把 T 交給其它接口(類)的 void 方法進行處理 比如我們經常用的打印一個對象 System.out.println(T)

操作流程解析圖如下:

下面我們來看個栗子:


  
  
     
  
  
  
  1. Stream<String> stream = Stream.of( "hello", "felord.cn");
  2. stream.peek(System.out::println);

執行之後,控制檯並沒有輸出任何字符串!納尼??

這是因爲流的生命週期有三個階段:

  • 起始生成階段。

  • 中間操作會逐一獲取元素並進行處理。可有可無。所有中間操作都是惰性的,因此,流在管道中流動之前,任何操作都不會產生任何影響。

  • 終端操作。通常分爲 最終的消費 (foreach 之類的)和 歸納 (collect)兩類。還有重要的一點就是終端操作啓動了流在管道中的流動。

所以,上面的代碼是因爲缺少了終端操作,因此,我們改成如下即可:


  
  
     
  
  
  
  1. Stream<String> stream = Stream.of( "hello", "felord.cn");
  2. stream.peek(System.out::println).collect(Collectors.toList());
  3. //控制檯打印內容如下:
  4. hello
  5. felord.cn

重點:peek VS map

他們最大的區別是:

peek 操作 一般用於不想改變流中元素本身的類型或者只想元素的內部狀態時;

而 map 則用於改變流中元素本身類型,即從元素中派生出另一種類型的操作。

 

④mapToInt、mapToLong、mapToDouble、flatMapToDouble、flatMapToInt、flatMapToLong

以上這些操作是map和flatMap的特例版,也就是針對特定的數據類型進行映射處理。其對應的方法接口如下:


  
  
     
  
  
  
  1. IntStream mapToInt(ToIntFunction<? super T> mapper);
  2. LongStream mapToLong(ToLongFunction<? super T> mapper);
  3. DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
  4. IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
  5. LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
  6. DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

此處就不全部單獨說明了,取一個操作舉例說明一下其用法:


  
  
     
  
  
  
  1. Stream<String> stream = Stream.of( "hello", "felord.cn");
  2. stream.mapToInt(s->s.length()).forEach(System.out::println);
  3. //輸出結果
  4. 5
  5. 9

並且這些指定類型的流,還有另外一些常用的方法,也是很好用的,可以參考:IntStreamLongStreamDoubleStream

 

  • 無序化(unordered)

unordered()操作不會執行任何操作來顯式地對流進行排序。它的作用是消除了流必須保持有序的約束,從而允許後續操作使用不必考慮排序的優化。

舉個栗子:


  
  
     
  
  
  
  1. public static void main(String[] args) {
  2. Stream.of( 5, 1, 2, 6, 3, 7, 4).unordered().forEach(System.out::println);
  3. Stream.of( 5, 1, 2, 6, 3, 7, 4).unordered().parallel().forEach(System.out::println);
  4. }
  5. //兩次輸出結果對比(方便比較,寫在一起)
  6. 第一遍: 第二遍:
  7. //第一行代碼輸出 //第一行代碼輸出
  8. 5 5
  9. 1 1
  10. 2 2
  11. 6 6
  12. 3 3
  13. 7 7
  14. 4 4
  15. //第二行代碼輸出 //第二行代碼輸出
  16. 3 3
  17. 6 6
  18. 4 7
  19. 7 5
  20. 2 4
  21. 1 1
  22. 5 2

以上結果,可以看到,雖然用了unordered(),但是第一個循環裏的數據順序並沒有被打亂;是不是很好奇?

您可以在Java 8文檔中有一下一段內容:

對於順序流,順序的存在與否不會影響性能,隻影響確定性。如果流是順序的,則在相同的源上重複執行相同的流管道將產生相同的結果;

如果是非順序流,重複執行可能會產生不同的結果。 對於並行流,放寬排序約束有時可以實現更高效的執行

在流有序時, 但用戶不特別關心該順序的情況下,使用 unordered 明確地對流進行去除有序約束可以改善某些有狀態或終端操作的並行性能。

 

2.2.3 有狀態(Stateful)操作

  • distinct:返回由該流的不同元素組成的流(根據 Object.equals(Object));distinct()使用hashCode()和equals()方法來獲取不同的元素。因此,我們的類必須實現hashCode()和equals()方法。
Stream<T> distinct();
  
  
     
  
  
  

簡言之:就是去重;下面看下流程解析圖:

20201109150012790

舉個栗子:


  
  
     
  
  
  
  1. Stream< String> stream = Stream.of( "1", "3", "4", "10", "4", "6", "23", "3");
  2. stream.distinct(). forEach(System.out::println);
  3. //輸出
  4. 1
  5. 3
  6. 4
  7. 10
  8. 6
  9. 23

可以發現,重複的數字會被剔除掉!那麼如果需要對自定義的對象進行過濾,則需要重寫對象的equals方法即可 !

另外有一個細節可以看到,去重之後還是按照原流中的排序順序輸出的,所以是有序的!

 

  • sorted:返回由該流的元素組成的流,並根據自然順序排序

該接口有兩種形式:無參和有參數,如:


  
  
     
  
  
  
  1. Stream<T> sorted();
  2. Stream<T> sorted(Comparator<? super T> comparator);

那區別其實就在於:傳入比較器的參數,可以自定義這個比較器,即自定義比較規則

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. stream.sorted().forEach(System.out::println);
  3. //輸出
  4. 1
  5. 3
  6. 4
  7. 8
  8. 9
  9. 10
  10. 16

 

  • limit:獲取流中n個元素返回的流

這個很好理解,和mysql的中的limit函數一樣的效果,返回指定個數的元素流。

Stream<T> limit(long maxSize);
  
  
     
  
  
  

流程解析圖如下:

20201109145946301

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. stream.limit( 3).forEach(System.out::println);
  3. //輸出
  4. 3
  5. 1
  6. 10

 

  • skip:在丟棄流的第一個n元素之後,返回由該流的其餘元素組成的流。

簡言之:跳過第n個元素,返回其後面的元素流;

Stream<T> skip(long n);
  
  
     
  
  
  

流程解析圖:

20201109150001270

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. stream.skip( 3).forEach(System.out::println);
  3. //輸出
  4. 16
  5. 8
  6. 4
  7. 9

 

2.2.4 短路(Short-circuiting)操作

  • anyMatch:Stream 中只要有一個元素符合傳入的 predicate,返回 true;
boolean anyMatch(Predicate<? super T> predicate);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. System.out.println( "result="+stream.anyMatch(s->s== 2));
  3. //輸出
  4. result= false

 

  • allMatch:Stream 中全部元素符合傳入的 predicate,返回 true;
boolean allMatch(Predicate<? super T> predicate);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. System.out.println( "result="+stream.allMatch(s->s>= 1));
  3. //輸出
  4. result= true

 

  • noneMatch:Stream 中沒有一個元素符合傳入的 predicate,返回 true.
boolean noneMatch(Predicate<? super T> predicate);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. System.out.println( "result="+stream.noneMatch(s -> s>= 17 ));
  3. //輸出
  4. result= true

 

  • findFirst:用於返回滿足條件的第一個元素(但是該元素是封裝在Optional類中)

關於Optional可以點這裏【Java 8系列】Java開發者的判空利器 -- Optional

Optional<T> findFirst();
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. Stream<Integer> stream = Stream.of( 3, 1, 10, 16, 8, 4, 9);
  2. System.out.println( "result="+stream.findFirst().get());
  3. //輸出
  4. result= 3
  5. //當然,我們還可以結合filter處理
  6. System.out.println( "result="+stream.filter(s-> s > 3).findFirst().get());
  7. //輸出
  8. result= 10

 

  • findAny:返回流中的任意元素(但是該元素也是封裝在Optional類中)
Optional<T> findAny();
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill", "Dany", "Julia", "Jenish", "Divya");
  2. String result = strAry.parallelStream().filter(s->s.startsWith( "J")).findFirst().get();
  3. System.out.println( "result = " + result);
  4. //輸出
  5. result = Jhonny

通過多次執行,我們會發現,其實findAny會每次按順序返回第一個元素。那這個時候,可能會認爲findAny與findFirst方法是一樣的效果。其實不然,findAny()操作,返回的元素是不確定的,對於同一個列表多次調用findAny()有可能會返回不同的值。使用findAny()是爲了更高效的性能。如果是數據較少,串行地情況下,一般會返回第一個結果,如果是並行的情況,那就不能確保是第一個。

我們接着看下面這個例子:


  
  
     
  
  
  
  1. List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill", "Dany", "Julia", "Jenish", "Divya");
  2. String result = strAry.parallelStream().filter(s->s.startsWith( "J")).findAny().get();
  3. System.out.println( "result = " + result);
  4. //輸出
  5. result = Jill
  6. result = Julia

如此可見,在並行流裏,findAny可就不是隻返回第一個元素啦!

 

2.2.5 非短路(Unshort-circuiting)操作

  • forEach:該方法接收一個Lambda表達式,然後在Stream的每一個元素上執行該表達式

可以理解爲我們平時使用的for循環,但是較於for循環,又略有不同!咱們待會再講。

void forEach(Consumer<? super T> action);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. List<String> strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill", "Dany", "Julia", "Jenish", "Divya");
  2. strAry.stream().forEach(s-> {
  3. if( "Jack".equalsIgnoreCase(s)) System.out.println(s);
  4. });
  5. //輸出
  6. Jack

那如果我們把 "Jack"用在循環外部用一個變量接收,如下操作:


  
  
     
  
  
  
  1. String name = "Jack";
  2. strAry.stream().forEach(s-> {
  3. if(name.equalsIgnoreCase(s)) name = "Jackson";
  4. });

那麼此時編輯器則會爆紅,

因爲lambda中,使用的外部變量必須是最終的,不可變的,所以如果我們想要對其進行修改,那是不可能的!如果必須這麼使用,可以將外部變量,移至表達式之中使用纔行!

 

  • forEachOrdered:該方法接收一個Lambda表達式,然後按順序在Stream的每一個元素上執行該表達式
void forEachOrdered(Consumer<? super T> action);
  
  
     
  
  
  

該功能其實和forEach是很相似的,也是循環操作!那唯一的區別,就在於forEachOrdered是可以保證循環時元素是按原來的順序逐個循環的!

但是,也不盡其然!因爲有的時候,forEachOrdered也是不能百分百保證有序!例如下面這個例子:


  
  
     
  
  
  
  1. Stream.of( "AAA,", "BBB,", "CCC,", "DDD,").parallel().forEach(System.out::print);
  2. System.out.println( "\n______________________________________________");
  3. Stream.of( "AAA,", "BBB,", "CCC,", "DDD").parallel().forEachOrdered(System.out::print);
  4. System.out.println( "\n______________________________________________");
  5. Stream.of( "DDD,", "AAA,", "BBB,", "CCC").parallel().forEachOrdered(System.out::print);
  6. //輸出爲:
  7. CCC,DDD,BBB,AAA,
  8. ______________________________________________
  9. AAA,BBB,CCC,DDD
  10. ______________________________________________
  11. DDD,AAA,BBB,CCC

可以看到,在並行流時,由於是多線程處理,其實還是無法保證有序操作的!

 

  • toArray:返回包含此流元素的數組;當有參數時,則使用提供的generator函數分配返回的數組,以及分區執行或調整大小可能需要的任何其他數組

  
  
     
  
  
  
  1. Object [] toArray();
  2. <A> A[] toArray(IntFunction<A[]> generator);

舉個栗子:


  
  
     
  
  
  
  1. List<String> strList = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill", "Dany", "Julia", "Jenish", "Divya");
  2. Object [] strAryNoArg = strList.stream().toArray();
  3. String [] strAry = strList.stream().toArray(String[]:: new);

 

  • reduce:方法接收一個函數作爲累加器,數組中的每個值(從左到右)開始縮減,最終計算爲一個值

通過字面意思,可能比較難理解是個什麼意思?下面我們先看一個圖,熟悉一下這個接口的操作流程是怎樣的:

該接口含有3種調用方式:


  
  
     
  
  
  
  1. Optional<T> reduce(BinaryOperator<T> accumulator);
  2. T reduce(T identity, BinaryOperator<T> accumulator);
  3. <U> U reduce(U identity,
  4. BiFunction<U, ? super T, U> accumulator,
  5. BinaryOperator<U> combiner);
  6. //以及參數的定義結構
  7. @FunctionalInterface
  8. public interface BinaryOperator<T> extends BiFunction<T,T,T> {
  9. //兩個靜態方法,先進行忽略
  10. }
  11. @FunctionalInterface
  12. public interface BiFunction<T, U, R> {
  13. R apply(T t, U u);
  14. //一個默認方法,先進行忽略
  15. }

 

下面舉幾個栗子,看看具體效果:

(一).先以1個參數的接口爲例

爲了方便理解,先看下內部的執行效果代碼:


  
  
     
  
  
  
  1. boolean foundAny = false;
  2. T result = null;
  3. for (T element : this stream) {
  4. if (!foundAny) {
  5. foundAny = true;
  6. result = element;
  7. }
  8. else
  9. result = accumulator.apply(result, element);
  10. }
  11. return foundAny ? Optional.of(result) : Optional.empty();

再看下具體栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 1, 2, 4, 5, 6, 7);
  2. *原接口一比一原汁原味寫法*
  3. Integer integer = num.stream().reduce( new BinaryOperator<Integer>() {
  4. @Override
  5. public Integer apply(Integer a, Integer b) {
  6. System.out.println( "x:"+a);
  7. return a + b;
  8. }
  9. }).get();
  10. System.out.println( "resutl:"+integer);
  11. *等效寫法一*
  12. Integer result = num.stream().reduce((x, y) -> {
  13. System.out.println( "x:"+x);
  14. return x + y;
  15. }).get();
  16. System.out.println( "resutl:"+result);
  17. *等效的普通寫法*
  18. boolean flag = false;
  19. int temp = 0;
  20. for (Integer integer : num) {
  21. if(!flag){
  22. temp = integer;
  23. flag = true;
  24. } else {
  25. System.out.println( "x:"+temp);
  26. temp += integer;
  27. }
  28. }
  29. System.out.println( "resutl:"+temp);

執行結果都是:


  
  
     
  
  
  
  1. x: 1
  2. x: 3
  3. x: 7
  4. x: 12
  5. x: 18
  6. resutl: 25

(二)再以2個參數的接口爲例

先看下內部的執行效果代碼:


  
  
     
  
  
  
  1. T result = identity;
  2. for (T element : this stream){
  3. result = accumulator.apply(result, element)
  4. }
  5. return result;

在看具體栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 1, 2, 4, 5, 6, 7);
  2. *一比一原汁原味寫法*
  3. Integer integer = num.stream().reduce( 1, new BinaryOperator<Integer>() {
  4. @Override
  5. public Integer apply(Integer a, Integer b) {
  6. System.out.println( "a="+a);
  7. return a + b;
  8. }
  9. });
  10. System.out.println( "resutl:"+integer);
  11. *普通 for循環寫法*
  12. int temp = 1;
  13. for (Integer integer : num) {
  14. System.out.println( "a="+temp);
  15. temp += integer;
  16. }
  17. System.out.println( "resutl:"+temp);

輸出結果都是:


  
  
     
  
  
  
  1. a= 1
  2. a= 2
  3. a= 4
  4. a= 8
  5. a= 13
  6. a= 19
  7. resutl: 26

(三)最後3個參數的接口爲例

這個接口的內部執行效果,其實和2個參數的幾乎一致。那麼第三個參數是啥呢?這是一個combiner組合器;

組合器需要和累加器的返回類型需要進行兼容,combiner組合器的方法主要用在並行操作中

在看具體栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 1, 2, 3, 4, 5, 6);
  2. List<Integer> other = new ArrayList<>();
  3. other.addAll(Arrays.asList( 7, 8, 9, 10));
  4. num.stream().reduce(other,
  5. (x, y) -> { //第二個參數
  6. System.out.println(JSON.toJSONString(x));
  7. x.add(y);
  8. return x;
  9. },
  10. (x, y) -> { //第三個參數
  11. System.out.println( "並行纔會出現:"+JSON.toJSONString(x));
  12. return x;
  13. });
  14. //輸出結果:
  15. [ 7, 8, 9, 10, 1]
  16. [ 7, 8, 9, 10, 1, 2]
  17. [ 7, 8, 9, 10, 1, 2, 3]
  18. [ 7, 8, 9, 10, 1, 2, 3, 4]
  19. [ 7, 8, 9, 10, 1, 2, 3, 4, 5]
  20. [ 7, 8, 9, 10, 1, 2, 3, 4, 5, 6]

我們再講串行流改成並行流,看下會出現什麼結果:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 4, 5, 6);
  2. List<Integer> other = new ArrayList<>();
  3. other.addAll(Arrays.asList( 1, 2, 3));
  4. num.parallelStream().reduce(other,
  5. (x, y) -> { //第二個參數
  6. x.add(y);
  7. System.out.println(JSON.toJSONString(x));
  8. return x;
  9. },
  10. (x, y) -> { //第三個參數
  11. x.addAll(y);
  12. System.out.println( "結合:"+JSON.toJSONString(x));
  13. return x;
  14. });
  15. //輸出結果
  16. //第一遍
  17. [ 1, 2, 3, 4, 5, 6]
  18. [ 1, 2, 3, 4, 5, 6]
  19. [ 1, 2, 3, 4, 5, 6]
  20. 結合:[ 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
  21. 結合:[ 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
  22. //第二遍
  23. [ 1, 2, 3, 4, 6]
  24. [ 1, 2, 3, 4, 6]
  25. [ 1, 2, 3, 4, 6]
  26. 結合:[ 1, 2, 3, 4, 6, 1, 2, 3, 4, 6]
  27. 結合:[ 1, 2, 3, 4, 6, 1, 2, 3, 4, 6, 1, 2, 3, 4, 6, 1, 2, 3, 4, 6]
  28. //第三遍
  29. [ 1, 2, 3, 5, 4, 6]
  30. [ 1, 2, 3, 5, 4, 6]
  31. [ 1, 2, 3, 5, 4, 6]
  32. 結合:[ 1, 2, 3, 5, 4, 6, 1, 2, 3, 5, 4, 6]
  33. 結合:[ 1, 2, 3, 5, 4, 6, 1, 2, 3, 5, 4, 6, 1, 2, 3, 5, 4, 6, 1, 2, 3, 5, 4, 6]

我們會發現每個結果都是亂序的,並且多執行幾次,都會出現不同的結果。並且第三個參數組合器內的代碼也得到了執行!!

這就是因爲並行時,使用多線程時順序性沒有保障所產生的結果。通過實踐,可以看到:組合器的作用,其實是對參數2中的各個線程,產生的結果進行了再一遍的歸約操作!

並且仔細看第二遍的執行結果:每一組都少了一1個值!!!

所以,對於並行流parallelStream操作,必須慎用!!

 

  • collect:稱爲收集器,是一個終端操作,它接收的參數是將流中的元素累積到彙總結果的各種方式

  
  
     
  
  
  
  1. <R, A> R collect(Collector<? super T, A, R> collector);
  2. <R> R collect(Supplier<R> supplier,
  3. BiConsumer<R, ? super T> accumulator,
  4. BiConsumer<R, R> combiner);

第一種方式會比較經常使用到,也比較方便使用,現在先看一看裏面常用的一些方法:

工廠方法

返回類型

用於

toList

List<T>

把流中所有元素收集到List中

示例:List<Menu> menus=Menu.getMenus.stream().collect(Collectors.toList());

 

toSet

Set<T>

把流中所有元素收集到Set中,刪除重複項

示例:Set<Menu> menus=Menu.getMenus.stream().collect(Collectors.toSet());

 

toCollection

Collection<T>

把流中所有元素收集到給定的供應源創建的集合中

示例:ArrayList<Menu> menus=Menu.getMenus.stream().collect(Collectors.toCollection(ArrayList::new));

 

Counting

Long

計算流中元素個數

示例:Long count=Menu.getMenus.stream().collect(counting);

 

SummingInt

Integer

對流中元素的一個整數屬性求和

示例:Integer count=Menu.getMenus.stream().collect(summingInt(Menu::getCalories));

 

averagingInt

Double

計算流中元素integer屬性的平均值

示例:Double averaging=Menu.getMenus.stream().collect(averagingInt(Menu::getCalories));

 

Joining

String

連接流中每個元素的toString方法生成的字符串

示例:String name=Menu.getMenus.stream().map(Menu::getName).collect(joining(“, ”));

 

maxBy

Optional<T>

一個包裹了流中按照給定比較器選出的最大元素的optional
如果爲空返回的是Optional.empty()

示例:Optional<Menu> fattest=Menu.getMenus.stream().collect(maxBy(Menu::getCalories));

 

minBy

Optional<T>

一個包裹了流中按照給定比較器選出的最大元素的optional
如果爲空返回的是Optional.empty()

示例: Optional<Menu> lessest=Menu.getMenus.stream().collect(minBy(Menu::getCalories));

 

Reducing

歸約操作產生的類型

從一個作爲累加器的初始值開始,利用binaryOperator與流中的元素逐個結合,從而將流歸約爲單個值

示例:int count=Menu.getMenus.stream().collect(reducing(0,Menu::getCalories,Integer::sum));

 

collectingAndThen

轉換函數返回的類型

包裹另一個轉換器,對其結果應用轉換函數

示例:Int count=Menu.getMenus.stream().collect(collectingAndThen(toList(),List::size));

 

groupingBy

Map<K,List<T>>

根據流中元素的某個值對流中的元素進行分組,並將屬性值做爲結果map的鍵

示例:Map<Type,List<Menu>> menuType=Menu.getMenus.stream().collect(groupingby(Menu::getType));

 

partitioningBy

Map<Boolean,List<T>>

根據流中每個元素應用謂語的結果來對項目進行分區

示例:Map<Boolean,List<Menu>> menuType=Menu.getMenus.stream().collect(partitioningBy(Menu::isType)

第二種方式看起來跟reduce的三個入參的方法有點類似,也可以用來實現filter、map等操作!

流程解析圖如下:

avatar

舉個栗子:


  
  
     
  
  
  
  1. List<Integer> numList = Arrays.asList( 1, 2, 3);
  2. numList.stream()
  3. .collect(()->{ //第一個參數
  4. //構造器
  5. System.out.println( "構造器,返回一個你想用到的任意起始對象!此處返回一個空List爲例");
  6. System.out.println();
  7. return new ArrayList();
  8. }, (a, b) -> { //第二個參數
  9. synchronized (Java8DemoServiceImpl.class) { //加鎖是爲了並行流下方便查看打印結果
  10. System.out.println("累加器");
  11. a.forEach(item -> System.out.println("a:" + item));
  12. System.out.println("b:" + b);
  13. a.add(b);
  14. //換行
  15. System.out.println();
  16. }
  17. }, (a, b) -> { //第三個參數
  18. synchronized (Java8DemoServiceImpl.class) { //加鎖是爲了並行流下方便查看打印結果
  19. System.out.println("合併器");
  20. System.out.println("a:" + JSON.toJSONString(a) + " , " + "b:" + JSON.toJSONString(b));
  21. a.addAll(b);
  22. System.out.println(); //爲了換行方便查看打印
  23. }
  24. })
  25. .forEach(item -> System.out.println("最終結果項:" + item));

運行結果:


  
  
     
  
  
  
  1. 構造器,返回一個你想用到的任意起始對象!此處返回一個空List爲例
  2. 累加器
  3. b: 1
  4. 累加器
  5. a: 1
  6. b: 2
  7. 累加器
  8. a: 1
  9. a: 2
  10. b: 3
  11. 最終結果項: 1
  12. 最終結果項: 2
  13. 最終結果項: 3

如果把上述流換成並行流,會得到如下一種結果:


  
  
     
  
  
  
  1. 構造器,返回一個你想用到的任意起始對象!此處返回一個空List爲例
  2. 構造器,返回一個你想用到的任意起始對象!此處返回一個空List爲例
  3. 構造器,返回一個你想用到的任意起始對象!此處返回一個空List爲例
  4. 累加器
  5. b: 2
  6. 累加器
  7. b: 1
  8. 累加器
  9. b: 3
  10. 合併器
  11. a:[ 2] , b:[ 3]
  12. 合併器
  13. a:[ 1] , b:[ 2, 3]
  14. 最終結果項: 1
  15. 最終結果項: 2
  16. 最終結果項: 3

可以看到,根據流內的元素個數n,起了n個線程,同時分別執行了構造器、累加器、合併器內代碼!與reduce的行爲方式基本一致!

 

  • max:根據提供的Comparator返回此流的最大元素
Optional<T> max(Comparator<? super T> comparator);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 4, 5, 6);
  2. num.stream().max(Integer::compareTo).ifPresent(System.out::println);
  3. //輸出
  4. 6

 

  • min:根據提供的Comparator返回此流的最小元素
Optional<T> min(Comparator<? super T> comparator);
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 4, 5, 6);
  2. num.stream().min(Integer::compareTo).ifPresent(System.out::println);
  3. //輸出
  4. 4

 

  • count:返回此流中的元素計數
long count();
  
  
     
  
  
  

舉個栗子:


  
  
     
  
  
  
  1. List<Integer> num = Arrays.asList( 4, 5, 6);
  2. System.out.println(num.stream().count());
  3. //輸出
  4. 3

 


3.總結

此處給正在學習的朋友兩個小提示:

1、對於流的各種操作所屬分類,還不夠熟悉的,可以直接進入方法的源碼接口內,如下,是可以查看到類型說明的:

2、對於並行流stream().parallel()、parallelStream()的使用,須慎重使用!使用前須考慮其不確定因素和無序性,考慮多線程所帶來的複雜性!!

2020年已近年末,再過幾天就要步入新年啦!工作之餘,耗時幾天,終於寫完了這篇博文!分享不易,希望感興趣的朋友,可以留言討論,點贊收藏!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章