Stream,集合操作利器,讓你好用到飛起來(lái)

作者:xcbeyond
瘋狂源自夢(mèng)想,技術(shù)成就輝煌!微信公眾號(hào):《程序猿技術(shù)大咖》號(hào)主,專(zhuān)注后端開(kāi)發(fā)多年,擁有豐富的研發(fā)經(jīng)驗(yàn),樂(lè)于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項(xiàng)目的研發(fā)工作,涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對(duì)于Java、微服務(wù)、數(shù)據(jù)庫(kù)、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗(yàn)。


集合是Java中使用最多的API,幾乎每個(gè)程序員天天都會(huì)和它打招呼,它可以讓你把相同、相似、有關(guān)聯(lián)的數(shù)據(jù)整合在一起,便于使用、提取以及運(yùn)算等操作。在實(shí)際Java程序中,集合的使用往往隨著業(yè)務(wù)需求、復(fù)雜度而變得更加復(fù)雜,在這其中將可能會(huì)涉及到更多的運(yùn)算,如:求和、平均值、分組、過(guò)濾、排序等等。如何這些操作混合出現(xiàn),又該如何實(shí)現(xiàn)?難道遍歷、再遍歷、再運(yùn)算么?拋開(kāi)性能因素,這些操作已經(jīng)嚴(yán)重影響了代碼的整潔,這種代碼也沒(méi)有幾個(gè)人愿意來(lái)讀。

那么,有沒(méi)有什么好的辦法來(lái)解決這種現(xiàn)狀呢?畢竟集合最為最常用的操作,難道Java語(yǔ)言的設(shè)計(jì)者沒(méi)有意識(shí)到這一點(diǎn)嗎?如何能夠幫助你節(jié)約寶貴的時(shí)間,讓程序員活得更輕松一點(diǎn)呢?

你可能已經(jīng)猜到了,答案就是流—Stream。

本文將從JDK1.8中Stream API講起,讓你覺(jué)得集合操作原來(lái)可以這么輕松使用。

(在學(xué)習(xí)本節(jié)之前,必須先學(xué)習(xí)Lambda表達(dá)式相關(guān)知識(shí),不清楚的可以翻看前幾篇文章JDK1.8新特性(三):Lambda表達(dá)式,讓你愛(ài)不釋手、JDK1.8新特性(四):函數(shù)式接口)

一、Stream是什么
Stream是Java API中的新成員,它允許你以聲明的方式處理數(shù)據(jù)集合(通過(guò)查詢語(yǔ)句來(lái)表達(dá),而不是臨時(shí)編寫(xiě)一個(gè)實(shí)現(xiàn)),你可以把它看成是遍歷數(shù)據(jù)集的高級(jí)迭代器。此外,Stream還可以透明地并行處理,而無(wú)需寫(xiě)任何多線程代碼了。

我們先簡(jiǎn)單的對(duì)比使用下Stream的好處吧。下面兩段代碼都是實(shí)現(xiàn)篩選出名字中包含“xc”字符串的人,并按照其年齡進(jìn)行排序。

傳統(tǒng)方式(JDK1.8之前,非Stream流):

List<People> peoples = new ArrayList<>();
// 遍歷 + 判斷
for (People people : allPeoples) {
    if (people.getName().contains("xc")) {
        peoples.add(people);
    }
}
// 對(duì)年齡排序
Collections.sort(peoples, new Comparator<People>() {
    @Override
    public int compare(People p1, People p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
});

Stream方式:

List<People> peoples2 = allPeoples.stream()
                .filter(people -> people.getName().contains("xc"))
                .sorted(Comparator.comparing(People::getAge))
                .collect(Collectors.toList());

沒(méi)有對(duì)比就沒(méi)有傷害,效果顯而易見(jiàn)。從開(kāi)發(fā)角度來(lái)看,Stream方式有以下顯而易見(jiàn)的好處:

代碼以聲明方式寫(xiě)的:說(shuō)明想要完成什么(篩選出滿足條件的數(shù)據(jù))而不是說(shuō)明如何實(shí)現(xiàn)一個(gè)操作(利用循環(huán)和if條件等控制流語(yǔ)句)。

多個(gè)基本操作鏈接起來(lái):將多個(gè)基礎(chǔ)操作鏈接起來(lái),來(lái)表達(dá)復(fù)雜的數(shù)據(jù)處理流水線(如下圖),同時(shí)體現(xiàn)了代碼的清晰、可讀性。
 

Stream API功能非常強(qiáng)大,類(lèi)似上面Stream處理流水線方式應(yīng)用場(chǎng)景很多,理論上可以生成一個(gè)具有無(wú)窮長(zhǎng)的流水線的。更重要的是,在復(fù)雜業(yè)務(wù)中你用不著為了讓某些數(shù)據(jù)處理任務(wù)并行而去操心線程和鎖了,Stream API都替你做好了!

Stream,即:”流“,通過(guò)將集合轉(zhuǎn)換為一種叫做”流“的元素序列,通過(guò)聲明方式,對(duì)集合中的每個(gè)元素進(jìn)行一系列并行或串行的流水線操作。

換句話說(shuō),你只需要告訴流你的要求,流便會(huì)在背后自行根據(jù)要求對(duì)元素進(jìn)行處理,而你只需要 “坐享其成”。

二、Stream操作
整個(gè)流操作就是一條流水線,將元素放在流水線上一個(gè)個(gè)地進(jìn)行處理,如下圖所示。


 

其中,數(shù)據(jù)源是原始集合數(shù)據(jù),然后將如 List<T>的集合轉(zhuǎn)換為Stream<T>類(lèi)型的流,并對(duì)流進(jìn)行一系列的操作,比如過(guò)濾保留部分元素、對(duì)元素進(jìn)行排序、類(lèi)型轉(zhuǎn)換等,最后再進(jìn)行一個(gè)終止操作,可以把 Stream 轉(zhuǎn)換回集合類(lèi)型,也可以直接對(duì)其中的各個(gè)元素進(jìn)行處理,比如打印、比如計(jì)算總數(shù)、計(jì)算最大值等。

很多流操作本身就會(huì)返回一個(gè)流,所以多個(gè)操作可以直接連接起來(lái),就如上面舉例中Stream方式的代碼一樣。
 

如果是以前,進(jìn)行這么一系列操作,你需要做個(gè)迭代器或者 foreach 循環(huán),然后遍歷,一步步地親力親為地去完成這些操作。但是如果使用流,你便可以直接聲明式地下指令,流會(huì)幫你完成這些操作。

通過(guò)上面Stream操作流水線、實(shí)例,Stream操作大體上分為兩種:中間操作符和終止操作符。

1. 中間操作符
對(duì)于數(shù)據(jù)流來(lái)說(shuō),中間操作符在執(zhí)行指定處理邏輯后,數(shù)據(jù)流依然可以傳遞給下一級(jí)的操作符。

中間操作符包含8種:

map(mapToInt,mapToLong,mapToDouble) 轉(zhuǎn)換操作:把比如A->B,這里默認(rèn)提供了轉(zhuǎn)int,long,double的操作符。

flatmap(flatmapToInt,flatmapToLong,flatmapToDouble)拍平操作:比如把int[]{2,3,4}拍平變成 2,3,4,也就是從原來(lái)的一個(gè)數(shù)據(jù)變成了3個(gè)數(shù)據(jù),這里默認(rèn)提供了拍平成int,long,double的操作。

limit限流操作:比如數(shù)據(jù)流中有10個(gè),我只要前3個(gè)就可以使用。

distinct去重操作:重復(fù)元素去重。

filter過(guò)濾操作:對(duì)集合數(shù)據(jù)進(jìn)行過(guò)濾。

peek消費(fèi)操作:如果想對(duì)數(shù)據(jù)進(jìn)行某些操作,如:讀取、編輯修改等。

skip跳過(guò)操作:跳過(guò)某些元素。

sorted排序操作:對(duì)元素排序,前提是實(shí)現(xiàn)Comparable接口,當(dāng)然也可以自定義比較器。

(具體可參照源碼java.util.stream.Stream)

2. 終止操作符
數(shù)據(jù)經(jīng)過(guò)一系列的中間操作,就輪到終止操作符上場(chǎng)了。終止操作符就是用來(lái)對(duì)數(shù)據(jù)進(jìn)行收集或者消費(fèi)的,數(shù)據(jù)到了終止操作這里就不會(huì)向下流動(dòng)了,終止操作符只能使用一次。

collect收集操作:將所有數(shù)據(jù)收集起來(lái),這個(gè)操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以說(shuō)Stream的核心在于Collectors。

count統(tǒng)計(jì)操作:統(tǒng)計(jì)最終的數(shù)據(jù)個(gè)數(shù)。

findFirst、findAny查找操作:查找第一個(gè)、查找任何一個(gè),返回的類(lèi)型為Optional。

noneMatch、allMatch、anyMatch匹配操作:數(shù)據(jù)流中是否存在符合條件的元素,返回值為bool 值。

min、max最值操作:需要自定義比較器,返回?cái)?shù)據(jù)流中最大、最小的值。

reduce規(guī)約操作:將整個(gè)數(shù)據(jù)流的值規(guī)約為一個(gè)值,count、min、max底層就是使用reduce。

forEach、forEachOrdered遍歷操作:這里就是對(duì)最終的數(shù)據(jù)進(jìn)行消費(fèi)了。

toArray數(shù)組操作:將數(shù)據(jù)流的元素轉(zhuǎn)換成數(shù)組。

說(shuō)了這么多,心動(dòng)不如行動(dòng),俗話說(shuō):實(shí)踐出真理。那么,一起來(lái)實(shí)戰(zhàn)吧。






三、實(shí)戰(zhàn)演練
Stream的一系列操作,必須要使用終止操作符,否則整個(gè)數(shù)據(jù)流是不會(huì)執(zhí)行起來(lái)的。

1. map
轉(zhuǎn)換、映射操作,將元素轉(zhuǎn)換成其他形式或提取一些信息。

比如,從People集合中獲取所有人的年齡:

allPeoples.stream()
    .map(People::getAge)
    .forEach(System.out::println);

2. flatmap
將元素拍平拍扁 ,將拍扁的元素重新組成Stream,并將這些Stream 串行合并成一條Stream。

比如,帶有-字符的字符串進(jìn)行拆分,并輸出:

Stream.of("x-c-b-e-y-o-n-d","a-b-c-d")
    .flatMap(m -> Stream.of(m.split("-")))
    .forEach(System.out::println);

輸出:

x
c
b
e
y
o
n
d
a
b
c
d

3. limit
限流操作,限制集合元素的個(gè)數(shù)。

比如,集合中有10個(gè)元素,我只要前4個(gè)就可以使用:

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .limit(4)
    .forEach(System.out::println);

輸出:

1
2
3
4

4. distinct
去重操作,重復(fù)元素去重,類(lèi)似數(shù)據(jù)庫(kù)中的關(guān)鍵字distinct。

比如,集合中可能存在一些重復(fù)的數(shù)據(jù),需要進(jìn)行去重操作:

Stream.of("xcbeyond","Niki","Liky","xcbeyond")
    .distinct()
    .forEach(System.out::println);

輸出:

xcbeyond
Niki
Liky

5. filter
過(guò)濾、篩選,對(duì)某些元素進(jìn)行過(guò)濾,不符合篩選條件的將無(wú)法進(jìn)入流的下游。

比如,篩選出一個(gè)數(shù)字集合中的所有偶數(shù):

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .filter(n -> 0 == n%2)
    .forEach(System.out::println);

輸出:

2
4
6
8
10

6. peek
消費(fèi)操作,如果想對(duì)數(shù)據(jù)進(jìn)行某些操作,如:讀取、編輯修改等。

比如,將People集合中name統(tǒng)一修改為name+age的形式:

allPeoples.stream()
    .peek(people -> people.setName(people.getName() + people.getAge()))
    .forEach(people -> System.out.println(people.getName()));

7. skip
跳過(guò)操作,跳過(guò)某些元素。

比如,一個(gè)數(shù)字集合,跳過(guò)前4個(gè)元素:

Stream.of(1,2,3,4,5,6,7,8,9,10)
    .skip(4)
    .forEach(System.out::println);

輸出:

5
6
7
8
9
10

8. sorted
排序操作,對(duì)元素排序,前提是實(shí)現(xiàn)Comparable接口,當(dāng)然也可以自定義比較器。

比如,將People集合按照年齡排序:

allPeoples.stream()
    .sorted(Comparator.comparing(People::getAge))
    .forEach(System.out::println);

9. collect
收集操作,終止操作符,用于將最終的數(shù)據(jù)收集到新的集合中,如,List、Set、Map等集合。

比如,將People集合按照年齡排序,并存放在一個(gè)新的集合中,供后續(xù)使用:

List<People> sortedPeople = allPeoples.stream()
    .sorted(Comparator.comparing(People::getAge))
    .collect(Collectors.toList());

10. count
統(tǒng)計(jì)操作,用于對(duì)集合元素個(gè)數(shù)的統(tǒng)計(jì),返回類(lèi)型是long。

比如,計(jì)算People集合中年齡大于30的人數(shù):

long count = allPeoples.stream()
    .filter(people -> people.getAge() > 30)
    .count();

11. findFirst、findAny
查找操作,查找第一個(gè)、任何一個(gè),返回的類(lèi)型為Optional。常用于查詢集中符合條件的元素,并結(jié)合Optional.isPresent()進(jìn)行判斷,防止出現(xiàn)未找到而強(qiáng)制獲取數(shù)據(jù)元素的異常情況。

比如,查找People集合中名字為xcbeyond的人:

People xcbeyondPeople = null;
Optional<People> optional = allPeoples.stream()
    .filter(people -> "xcbeyond".equals(people.getName()))
    .findFirst();
if (optional.isPresent()) {
    xcbeyondPeople = optional.get();
}

12. noneMatch、allMatch、anyMatch
匹配操作,判斷數(shù)據(jù)流中是否存在符合條件的元素,返回值為boolean值。

noneMatch:沒(méi)有匹配條件的元素

allMatch、anyMatch:全匹配

比如,判斷People集合中是否有名字為xcbeyond的人:

boolean bool = allPeoples.stream()
    .allMatch(people -> "xcbeyond".equals(people.getName()));

13. min、max
最值操作,根據(jù)自定義比較器,返回?cái)?shù)據(jù)流中最大、最小的元素。

比如,找到People集合中最大年齡的人:

People maxAgePeople = null;
Optional<People> maxAgeOptional = allPeoples.stream()
    .max(Comparator.comparing(People::getAge));
if (maxAgeOptional.isPresent()) {    // 可能沒(méi)有,則需要進(jìn)行判斷
    maxAgePeople = maxAgeOptional.get();
}

14. reduce
規(guī)約操作:將整個(gè)數(shù)據(jù)流的值規(guī)約為一個(gè)值,其中count、min、max底層就是使用reduce。

比如,對(duì)一個(gè)整數(shù)集合進(jìn)行求和:

int sum = Stream.of(1,9,8,4,5,6,-1)
    .reduce(0,(e1,e2)->e1+e2);
System.out.println(sum);

輸出:

32

四、總結(jié)
本文就Stream的基礎(chǔ)使用層面進(jìn)行了全面的介紹、實(shí)戰(zhàn),告訴你該怎么用每種操作符,只有掌握了這些基本的操作,在面對(duì)實(shí)際復(fù)雜處理邏輯時(shí),需要進(jìn)一步配合使用,就會(huì)知道它的妙處了。這也讓你對(duì)集合的操作更上一步,為你省去了不少麻煩。關(guān)于Stream更深入的說(shuō)明,如:并行處理、是否高效等,將會(huì)在之后的章節(jié)進(jìn)行詳盡的闡述、驗(yàn)證,以消除使用中的疑惑與擔(dān)憂。