天天看點

詳解Java8 Stream流式程式設計

作者:Java海浪

目前市面上很多開源架構,如Mybatis- Plus、kafka Streams以及Flink流處理等都用到Stream流特性,寫出的代碼簡潔而易懂。當然,若是在不熟悉流特性的基礎上而貿然去使用Stream開發的話,難免會寫出一手bug。

在項目當中,很早就開始使用Java 8的流特性進行開發了,但是一直都沒有針對這塊進行開發總結。這次就對這一塊代碼知識做一次全面總結,在總結的過程中去發現自己的不足,同時友善日後開發查詢。

詳解Java8 Stream流式程式設計

一、流(Stream)的概念

1.1、什麼是流。

流(Stream)是對資料進行連續處理的抽象概念,可以看作數一種疊代器,按步驟處理資料元素。流的建立方式包括從集合、數組、檔案等資料源擷取輸入流或者輸出流,或者通過網絡連接配接擷取到網絡流,例如Kafka 的流處理。常見的使用場景包括從大型資料源讀取、過濾、資料轉換、聚合等操作。

1.2、流的特性:流的惰性求值、短路操作、可消費性等特性。

  • 惰性求值(Lazy Evaluation):流的元素隻在需要時才進行計算,不會提前計算整個流。
  • 短路操作(Short-Circuiting Operations):對于某些操作,如果前面的元素已經滿足條件,後面的元素就不再需要進行處理,類似Java裡的&&。
  • 可消費性:流隻能被消費一次,即每個元素隻能被處理一次。

1.3、流的類型:了解基本類型流、對象類型流和無限流等不同類型的流。

根據資料類型和元素個數的不同,可以将流(Stream)分為以下幾種類型:

  • 基本類型流(Primitive Stream):處理基本資料類型,如IntStream、Long Stream和DoubleStream。
  • 對象類型流(Object Stream):處理對象類型,如Stream,這裡的T表示任意對象類型。
  • 無限流(Infinite Stream):包含無限個元素的流,如Stream.iterate()和Stream.generate()方法生成的流。
  • 并行流(Parallel Stream):将流劃分成多個子流,充分利用多核處理器提高計算性能。
  • 裝飾流(Decorating Stream):通過對一個流進行裝飾模式,實作流的增強功能,如排序、過濾、映射等操作。

二、中間操作

​ Stream的中間操作是指在流鍊當中,可以對資料進行處理操作,包括filter過濾、map映射轉換、flatMap合并、distinct去重、sorted排序等操作。這些操作都會傳回一個新的Stream流對象,可以通過鍊式調用多個中間操作進行複雜的資料處理。需要注意的是,中間操作需要具有終止操作才會觸發。

​ 下面按類别講解Stream常見的中間操作。

2.1、filter:過濾出符合條件的元素。

​ filter()方法常用于實作資料過濾,即可以對集合、數組等資料源篩選出符合指定條件的元素,并傳回一個新的流。

//将數組轉換為一個字元串清單
List<String> numbers = Arrays.asList("13378520000","13278520000","13178520000","13358520000");
//通過stream()方法建立一個流,接着使用filter()方法過濾出字首為“133”的元素,最終通過collect() 方法将結果收集到一個新清單中
List<String> filterdNumbers = numbers.stream().filter(s -> s.startsWith("133")).collect(Collectors.toList());
System.out.println(filterdNumbers);

列印結果:[13378520000, 13358520000]           

2.2、map:映射轉換元素。

​ map()方法用于對流中的每個元素進行映射操作,将其轉換為另一個元素或者提取其中的資訊,并傳回一個新的流。

2.2.1、轉換元素

List<String> numbers = Arrays.asList("13378520000","13278520000","13178520000","13558520000");
//通過stream()方法建立一個流,使用map()方法将每個字元串轉換為截取前7位的字元,最後使用collect()方法将結果收集到一個新清單中
List<String> filterdNumbers = numbers.stream().map(s -> s.substring(0,7)).collect(Collectors.toList());
System.out.println(filterdNumbers);

列印結果:[1337852, 1327852, 1317852, 1355852]           

2.2.2、提取元素資訊

List<People> peopleList = Arrays.asList(
        new People("王二","13378520000"),
        new People("李二","13278520000"),
        new People("張四","13178520000")
);
//通過stream()方法建立一個流,使用map()方法提取每個使用者的手機号,最後使用collect()方法将結果收集到一個新清單中
List<String> tel = peopleList.stream().map(People::getTel).collect(Collectors.toList());
System.out.println(tel);

列印結果:[13378520000, 13278520000, 13178520000]           

2.3、flatMap:将多個流合并為一個流。

​ flatMap()方法可以實作多對多的映射,或者将多個清單合并成一個清單操作。

2.3.1、實作多對多的映射

​ 假設有兩組餘額清單A和B,需要将A組每個元素都與B組所有元素依次進行相加,可以使用flatMap實作該多對多的映射

List<Integer> listA = Arrays.asList(1, 2, 3);
List<Integer> listB = Arrays.asList(4, 5, 6);
List<Integer> list = listA.stream().flatMap(a -> listB.stream().map(b -> a +b)).collect(Collectors.toList());
System.out.println(list);

列印結果:  [5, 6, 7, 6, 7, 8, 7, 8, 9]	             

2.3.2、将多個清單合并成一個清單

​ 假設有一個包含多個手機号字元串清單的清單,現在需要合并所有手機号字元串成為一個清單,可以使用flatMap()方法實作:

List<List<String>> listOfLists = Arrays.asList(
        Arrays.asList("13378520000", "13278520000"),
        Arrays.asList("13178520000", "13558520000"),
        Arrays.asList("15138510000", "15228310000")
);
List<String> flatMapList = listOfLists.stream().flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(flatMapList);

列印結果:[13378520000, 13278520000, 13178520000, 13558520000, 15138510000, 15228310000]           

2.4、distinct:去除重複的元素。

List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000");
List<String> disNumbers = numbers.stream().distinct().collect(Collectors.toList());
System.out.println(disNumbers);		

列印結果:[13378520000, 15138510000, 13178520000]		           

​ 注意一點的是,distinct用于針對流作去重操作時,需要确定流中元素實作了equals()和hashCode()方法,因為這兩個方法是判斷兩個對象是否相等的标準。

2.5、sorted:排序元素。

​ 預設情況下是升序排序,可以使用reversed()降序排序

List<People> peopleList = Arrays.asList(
        new People("王二",20),
        new People("李二",30),
        new People("張四",31)
);
List<People> newpeopleList=peopleList.stream().sorted(Comparator.comparing(People::getAge)).collect(Collectors.toList());
//使用reversed()降序排序
//List<People> newpeopleList = peopleList.stream().sorted(Comparator.comparing(People::getAge).reversed()).collect(Collectors.toList());
newpeopleList.stream().forEach(System.out::println);

列印結果:
People{name='王二', age=20}
People{name='李二', age=30}
People{name='張四', age=31}           

2.6、peek:檢視每個元素的資訊,但不修改流中元素的狀态。

​ peek()方法可以在流中的任何階段使用,不會影響到流的操作,也不會終止流的操作。

List<String> telList = Arrays.asList("13378520000","13278520000","13178520000","13558520000");
telList.stream().peek(t -> System.out.println(t))
        .map(t -> t.substring(0,3))
        .peek(t -> System.out.println(t))
        .collect(Collectors.toList());

列印結果:
			13378520000
			133
			13278520000
			132           

peek()方法和forEach很類似,都是可以用于周遊流中的元素,但是,兩者之間存在較大的差別。主要一點是,forEach在流中是一個終止操作,一旦調用它,就意味着Stream流已經被處理完成,不能再進行任何操作。

2.7、limit 和 skip:截取流中的部分元素。

​ limit()和skip()都是用于截取Stream流中部分元素的方法,兩者差別在于,limit()傳回一個包含前n個元素的新流,skip()則傳回一個丢棄前n個元素後剩餘元素組成的新流。

int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.print("取數組前5個元素:");
Arrays.stream(arr).limit(5).forEach(n -> System.out.print(n + " ")); // 輸出結果為:1 2 3 4 5

System.out.print("跳過前3個元素,取剩餘數組元素:");
Arrays.stream(arr).skip(3).forEach(n -> System.out.print(n + " ")); // 輸出結果為:4 5 6 7 8 9 10           

三、終止操作

​ Stream的終止操作是指執行Stream流鍊中最後一個步驟,到這一步就會結束整個流處理。在Java8中,Stream終止操作包括forEach、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst和findAny等,這些終止操作都有傳回值。

需要注意一點是,如果沒有執行終止操作的話,Stream流是不會觸發執行的。例如,一個沒有終止操作的peek()方法代碼是不會執行進而列印

list.stream().peek(t -> System.out.println("ddd"))

//當加上終止操作話,例如加上collect,就會列印出“ddd”
list.stream().peek(t -> System.out.println("ddd")).collect(Collectors.toList());            

下面按類别分别講解各個終止操作的使用。

3.1、forEach:周遊流中的每個元素。

3.2、count:統計流中元素的數量。

List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000");
long count = numbers.stream()
        .distinct()//去重
        .count();//統計去重後的手機号
System.out.println(count);

列印結果:3           

3.3、reduce:将流中的所有元素歸約成一個結果。

​ 常用文法格式如下:

Optional<T> result = stream.reduce(BinaryOperator<T> accumulator);           

reduce方法會傳回一個Optional類型的值,表示歸約後的結果,需要通過get()方法擷取Optional裡的值。

//一個包含多個手機号的List清單,去重之後将清單所有字元串拼按照逗号間隔接成一個字元串
List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15138510000");
Optional result = numbers.stream()
        .distinct() //去重
        .reduce((a ,b) -> a+","+b);//指定規則為,相臨兩個字元通過逗号“,”間隔
System.out.println(result.get());

列印結果:13378520000,15138510000,13178520000           

3.4、collect:将流中的元素收集到一個容器中,并傳回該容器。

​ 在Java8的collect方法中,除裡toList()之外,還提供了例如toSet,toMap等方法滿足不同的場景,根據名字就可以知道,toSet()傳回的是一個Set集合,toMap()傳回的是一個Map集合。

3.5、min 和 max:找出流中的最小值和最大值。

List<People> peopleList = Arrays.asList(
        new People("王二",20),
        new People("李二",30),
        new People("張四",31)
);
//查找年齡最小的使用者,若沒有則傳回一個null
People people = peopleList.stream().min(Comparator.comparing(People::getAge)).orElse(null);
System.out.println(people);

列印結果:People{name='王二', age=20}           

3.6、anyMatch、allMatch 和 noneMatch:判斷流中是否存在滿足指定條件的元素。

3.6.1、anyMatch

​ anyMatch用于判斷,如果流中至少有一個元素滿足給定條件,那麼傳回true,反之傳回false,即 true||false為true這類的判斷。

List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15338510000");
boolean hasNum = numbers.stream().anyMatch(n -> n.startsWith("153"));
System.out.println(hasNum);

列印結果:true           

3.6.2、allMatch

​ allMatch用于判斷,流中的所有元素是否都滿足給定條件,滿足傳回true,反之false,即true&&false為false這類判斷

List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "15338510000");
boolean hasNum = numbers.stream().allMatch(n -> n.startsWith("153"));
System.out.println(hasNum);

列印結果:false           

3.6.2、noneMatch

​ noneMatch用于判斷,如果流中沒有任何元素滿足給定的條件,傳回true,如果流中有任意一個條件滿足給定條件,傳回false,類似!true為false的判斷。

List<String> numbers = Arrays.asList("13378520000", "15138510000","13178520000", "1238510000");
//numbers裡沒有字首為“153”的手機号
boolean hasNum = numbers.stream().noneMatch(n -> n.startsWith("153"));
System.out.println(hasNum);

列印結果:true           

3.7、findFirst 和 findAny:傳回流中第一個或任意一個元素。

3.7.1、findFirst

​ findFirst用于傳回流中第一個元素,如果流為空話,則傳回一個空的Optional對象

List<People> peopleList = Arrays.asList(
        new People("王二","13178520000","20210409"),
        new People("李二","13178520000","20230401"),
        new People("張四","13178520000","20220509"),
        new People("趙六","13178520000","20220109")
);
/**
 * 先按照時間升序排序,排序後的結果如下:
 *   People{name='王二', tel='13178520000', time='20210409'}
 *   People{name='趙六', tel='13178520000', time='20220109'}
 *   People{name='張四', tel='13178520000', time='20220509'}
 *   People{name='李二', tel='13178520000', time='20230401'}
 *
 *排序後,People{name='王二', tel='13178520000', time='20210409'}成了流中的第一個元素
 */
People people = peopleList.stream().sorted(Comparator.comparing(People::getTime)).findFirst().orElse(null);
System.out.println(people);

列印結果:People{name='王二', tel='13178520000', time='20210409'}            

3.7.2、findAny

​ findAny傳回流中的任意一個元素,如果流為空,則通過Optional對象傳回一個null。

//blackList是已經存在的黑名單清單
List<String> blackList = Arrays.asList("13378520000", "15138510000");
//新來的手機号清單
List<String> phoneNumber = Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000");
String blackPhone = phoneNumber.stream()
        //過濾出phoneNumber有包含在blackList的手機号,這類手機号即為黑名單手機号。
        .filter(phone -> blackList.contains(phone))
        //擷取過濾确定為黑名單手機号的任意一個
        .findAny()
        //如果沒有則傳回一個null
        .orElse(null);
System.out.println(blackPhone);

列印結果:13378520000           

四、并行流

​ 前面的案例主要都是以順序流來講解,接下來,就是講解Stream的并行流。在大資料量處理場景下,使用并行流可以提高某些操作效率,但同樣存在一些需要考慮的問題,并非所有情況下都可以使用。

4.1、什麼是并行流:并行流的概念和原理。

​ 并行流是指通過将資料按照一定的方式劃分成多個片段分别在多個處理器上并行執行,這就意味着,可能處理完成的資料順序與原先排序好的資料情況是不一緻的。主要是用在比較大的資料量處理情況,若資料量太少,效率并不比順序流要高,因為底層其實就使用到了多線程的技術。

​ 并行流的流程原理如下:

​ **1、輸入資料:**并行流的初始資料一般是集合或者數組,例如Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000");

​ **2、劃分資料:**将初始資料平均分成若幹個子集,每個子集可以在不同的線程中獨立進行處理,這個過程通常叫“分支”(Forking),預設情況下,Java8并行流使用到了ForkJoinPool架構,會将Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000")劃分成更小的顆粒進行處理,可能會将該數組劃分成以下三個子集:

[13378520000, 13178520000]    [1238510000, 13338510000]    [13299920000]           

​ **3、處理資料:**針對劃分好的子集并行進行相同的操作,例如包括過濾(filter)、映射(map)、去重(distinct)等,這個過程通常叫“計算”(Computing),例如需要過濾為字首包括“133”的字元集合,那麼,各個子集,就會處理得到以下結果:

[13378520000]    [13338510000]    []           

**4、合并結果:**将所有子集處理完成的結果進行彙總,得到最終結果。這個過程通常叫“合并”(Merging),結果就會合并如下:

[13378520000,13338510000]             

**5、傳回結果:**傳回最終結果。

通俗而言,就是順序流中,隻有一個勞工在摘水果,并行流中,是多個勞工同時在摘水果。

4.2、建立并行流:通過 parallel() 方法将串行流轉換為并行流。

List<String> numbers = Arrays.asList("13378360000","13278240000","13178590000","13558120000");
//通過stream().parallel()方法建立一個并行流,使用map()方法将每個字元串轉換為截取前7位的字元,最後使用collect()方法将結果收集到一個新清單中
List<String> filNums = numbers.stream().parallel().map(s -> s.substring(0,7)).collect(Collectors.toList());
System.out.println(filNums);

列印結果:[1337836, 1327824, 1317859, 1355812]           

4.3、并行流的注意事項:并行流可能引發的線程安全,以及如何避免這些問題。

​ 在使用并發流的過程中,可能會引發以下線程安全問題:并行流中的每個子集都在不同線程運作,可能會導緻對共享狀态的競争和沖突。

​ 避免線程問題的方法如下:避免修改共享狀态,即在處理集合過程當中,避免被其他線程修改集合資料,可以使用鎖來保證線程安全。

​ 使用無狀态操作:在并行流處理過程盡量使用無狀态操作,例如filter、map之類的,可以盡量避免線程安全和同步問題。

五、擴充流處理

除裡以上常用的流處理之外,Java8還新增了一些專門用來處理基本類型的流,例如IntStream、LongStream、DoubleStream等,其對應的Api接口基本與前面案例相似,讀者可以自行研究。

最後,需要注意一點是,在流處理過程當中,盡量使用原始類型資料,避免裝箱操作,因為裝箱過程會有性能開銷、記憶體占用等問題,例如,當原始資料int類型被裝箱成Integer包裝類型時,這個過程會涉及到對象的建立、初始化、垃圾回收等過程,需要額外的性能開銷。