吃透JAVA的Stream流操作,多年實(shí)踐總結(jié)
在JAVA中,涉及到對(duì)數(shù)組
、Collection
等集合類中的元素進(jìn)行操作的時(shí)候,通常會(huì)通過循環(huán)的方式進(jìn)行逐個(gè)處理,或者使用Stream的方式進(jìn)行處理。
例如,現(xiàn)在有這么一個(gè)需求:
從給定句子中返回單詞長(zhǎng)度大于5的單詞列表,按長(zhǎng)度倒序輸出,最多返回3個(gè)
在JAVA7及之前的代碼中,我們會(huì)可以照如下的方式進(jìn)行實(shí)現(xiàn):
java復(fù)制代碼public List<String> sortGetTop3LongWords(@NotNull String sentence) { ?? ?// 先切割句子,獲取具體的單詞信息 ?? ?String[] words = sentence.split(" "); ?? ?List<String> wordList = new ArrayList<>(); ?? ?// 循環(huán)判斷單詞的長(zhǎng)度,先過濾出符合長(zhǎng)度要求的單詞 ?? ?for (String word : words) { ?? ? ? ?if (word.length() > 5) { ?? ? ? ? ? ?wordList.add(word); ?? ? ? ?} ?? ?} ?? ?// 對(duì)符合條件的列表按照長(zhǎng)度進(jìn)行排序 ?? ?wordList.sort((o1, o2) -> o2.length() - o1.length()); ?? ?// 判斷l(xiāng)ist結(jié)果長(zhǎng)度,如果大于3則截取前三個(gè)數(shù)據(jù)的子list返回 ?? ?if (wordList.size() > 3) { ?? ? ? ?wordList = wordList.subList(0, 3); ?? ?} ?? ?return wordList; } ?
在JAVA8及之后的版本中,借助Stream流,我們可以更加優(yōu)雅的寫出如下代碼:
java復(fù)制代碼 public List<String> sortGetTop3LongWordsByStream(@NotNull String sentence) { ?? ?return Arrays.stream(sentence.split(" ")) ?? ? ? ? ? ?.filter(word -> word.length() > 5) ?? ? ? ? ? ?.sorted((o1, o2) -> o2.length() - o1.length()) ?? ? ? ? ? ?.limit(3) ?? ? ? ? ? ?.collect(Collectors.toList()); } ?
直觀感受上,Stream
的實(shí)現(xiàn)方式代碼更加簡(jiǎn)潔、一氣呵成。很多的同學(xué)在代碼中也經(jīng)常使用Stream流,但是對(duì)Stream流的認(rèn)知往往也是僅限于會(huì)一些簡(jiǎn)單的filter
、map
、collect
等操作,但JAVA的Stream可以適用的場(chǎng)景與能力遠(yuǎn)不止這些。
那么問題來了:Stream相較于傳統(tǒng)的foreach的方式處理,到底有啥優(yōu)勢(shì)?
這里我們可以先擱置這個(gè)問題,先整體全面的了解下Stream,然后再來討論下這個(gè)問題。
筆者結(jié)合在團(tuán)隊(duì)中多年的代碼檢視遇到的情況,結(jié)合平時(shí)項(xiàng)目編碼實(shí)踐經(jīng)驗(yàn),對(duì)Stream的核心要點(diǎn)與易混淆用法、典型使用場(chǎng)景等進(jìn)行了詳細(xì)的梳理總結(jié),希望可以幫助大家對(duì)Stream有個(gè)更全面的認(rèn)知,也可以更加高效的應(yīng)用到項(xiàng)目開發(fā)中去。
Stream初相識(shí)
概括講,可以將Stream流操作分為3種類型:
創(chuàng)建Stream
Stream中間處理
終止Steam
每個(gè)Stream管道操作類型都包含若干API方法,先列舉下各個(gè)API方法的功能介紹。
開始管道
主要負(fù)責(zé)新建一個(gè)Stream流,或者基于現(xiàn)有的數(shù)組、List、Set、Map等集合類型對(duì)象創(chuàng)建出新的Stream流。
API功能說明stream()創(chuàng)建出一個(gè)新的stream串行流對(duì)象parallelStream()創(chuàng)建出一個(gè)可并行執(zhí)行的stream流對(duì)象Stream.of()通過給定的一系列元素創(chuàng)建一個(gè)新的Stream串行流對(duì)象
中間管道
負(fù)責(zé)對(duì)Stream進(jìn)行處理操作,并返回一個(gè)新的Stream對(duì)象,中間管道操作可以進(jìn)行疊加。
API功能說明filter()按照條件過濾符合要求的元素, 返回新的stream流map()將已有元素轉(zhuǎn)換為另一個(gè)對(duì)象類型,一對(duì)一邏輯,返回新的stream流flatMap()將已有元素轉(zhuǎn)換為另一個(gè)對(duì)象類型,一對(duì)多邏輯,即原來一個(gè)元素對(duì)象可能會(huì)轉(zhuǎn)換為1個(gè)或者多個(gè)新類型的元素,返回新的stream流limit()僅保留集合前面指定個(gè)數(shù)的元素,返回新的stream流skip()跳過集合前面指定個(gè)數(shù)的元素,返回新的stream流concat()將兩個(gè)流的數(shù)據(jù)合并起來為1個(gè)新的流,返回新的stream流distinct()對(duì)Stream中所有元素進(jìn)行去重,返回新的stream流sorted()對(duì)stream中所有的元素按照指定規(guī)則進(jìn)行排序,返回新的stream流peek()對(duì)stream流中的每個(gè)元素進(jìn)行逐個(gè)遍歷處理,返回處理后的stream流
終止管道
顧名思義,通過終止管道操作之后,Stream流將會(huì)結(jié)束,最后可能會(huì)執(zhí)行某些邏輯處理,或者是按照要求返回某些執(zhí)行后的結(jié)果數(shù)據(jù)。
API功能說明count()返回stream處理后最終的元素個(gè)數(shù)max()返回stream處理后的元素最大值min()返回stream處理后的元素最小值findFirst()找到第一個(gè)符合條件的元素時(shí)則終止流處理findAny()找到任何一個(gè)符合條件的元素時(shí)則退出流處理,這個(gè)對(duì)于串行流時(shí)與findFirst相同,對(duì)于并行流時(shí)比較高效,任何分片中找到都會(huì)終止后續(xù)計(jì)算邏輯anyMatch()返回一個(gè)boolean值,類似于isContains(),用于判斷是否有符合條件的元素allMatch()返回一個(gè)boolean值,用于判斷是否所有元素都符合條件noneMatch()返回一個(gè)boolean值, 用于判斷是否所有元素都不符合條件collect()將流轉(zhuǎn)換為指定的類型,通過Collectors進(jìn)行指定toArray()將流轉(zhuǎn)換為數(shù)組iterator()將流轉(zhuǎn)換為Iterator對(duì)象foreach()無返回值,對(duì)元素進(jìn)行逐個(gè)遍歷,然后執(zhí)行給定的處理邏輯
Stream方法使用
map與flatMap
map
與flatMap
都是用于轉(zhuǎn)換已有的元素為其它元素,區(qū)別點(diǎn)在于:
map 必須是一對(duì)一的,即每個(gè)元素都只能轉(zhuǎn)換為1個(gè)新的元素
flatMap 可以是一對(duì)多的,即每個(gè)元素都可以轉(zhuǎn)換為1個(gè)或者多個(gè)新的元素
比如:有一個(gè)字符串ID列表,現(xiàn)在需要將其轉(zhuǎn)為User對(duì)象列表??梢允褂胢ap來實(shí)現(xiàn):
java復(fù)制代碼 /** ?* 演示map的用途:一對(duì)一轉(zhuǎn)換 ?*/ public void stringToIntMap() { ?? ?List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111"); ?? ?// 使用流操作 ?? ?List<User> results = ids.stream() ?? ? ? ? ? ?.map(id -> { ?? ? ? ? ? ? ? ?User user = new User(); ?? ? ? ? ? ? ? ?user.setId(id); ?? ? ? ? ? ? ? ?return user; ?? ? ? ? ? ?}) ?? ? ? ? ? ?.collect(Collectors.toList()); ?? ?System.out.println(results); } ?
執(zhí)行之后,會(huì)發(fā)現(xiàn)每一個(gè)元素都被轉(zhuǎn)換為對(duì)應(yīng)新的元素,但是前后總元素個(gè)數(shù)是一致的:
bash復(fù)制代碼 [User{id='205'}, ? User{id='105'}, ?User{id='308'}, ? User{id='469'}, ? User{id='627'}, ? User{id='193'}, ? User{id='111'}] ?
再比如:現(xiàn)有一個(gè)句子列表,需要將句子中每個(gè)單詞都提取出來得到一個(gè)所有單詞列表。這種情況用map就搞不定了,需要flatMap
上場(chǎng)了:
java復(fù)制代碼 public void stringToIntFlatmap() { ?? ?List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao"); ?? ?// 使用流操作 ?? ?List<String> results = sentences.stream() ?? ? ? ? ? ?.flatMap(sentence -> Arrays.stream(sentence.split(" "))) ?? ? ? ? ? ?.collect(Collectors.toList()); ?? ?System.out.println(results); } ?
執(zhí)行結(jié)果如下,可以看到結(jié)果列表中元素個(gè)數(shù)是比原始列表元素個(gè)數(shù)要多的:
csharp復(fù)制代碼 [hello, world, Jia, Gou, Wu, Dao] ?
這里需要補(bǔ)充一句,flatMap
操作的時(shí)候其實(shí)是先每個(gè)元素處理并返回一個(gè)新的Stream,然后將多個(gè)Stream展開合并為了一個(gè)完整的新的Stream,如下:
peek和foreach方法
peek
和foreach
,都可以用于對(duì)元素進(jìn)行遍歷然后逐個(gè)的進(jìn)行處理。
但根據(jù)前面的介紹,peek屬于中間方法,而foreach屬于終止方法。這也就意味著peek只能作為管道中途的一個(gè)處理步驟,而沒法直接執(zhí)行得到結(jié)果,其后面必須還要有其它終止操作的時(shí)候才會(huì)被執(zhí)行;而foreach作為無返回值的終止方法,則可以直接執(zhí)行相關(guān)操作。
java復(fù)制代碼 public void testPeekAndforeach() { ?? ?List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao"); ?? ?// 演示點(diǎn)1: 僅peek操作,最終不會(huì)執(zhí)行 ?? ?System.out.println("----before peek----"); ?? ?sentences.stream().peek(sentence -> System.out.println(sentence)); ?? ?System.out.println("----after peek----"); ?? ?// 演示點(diǎn)2: 僅foreach操作,最終會(huì)執(zhí)行 ?? ?System.out.println("----before foreach----"); ?? ?sentences.stream().forEach(sentence -> System.out.println(sentence)); ?? ?System.out.println("----after foreach----"); ?? ?// 演示點(diǎn)3: peek操作后面增加終止操作,peek會(huì)執(zhí)行 ?? ?System.out.println("----before peek and count----"); ?? ?sentences.stream().peek(sentence -> System.out.println(sentence)).count(); ?? ?System.out.println("----after peek and count----"); } ?
輸出結(jié)果可以看出,peek
獨(dú)自調(diào)用時(shí)并沒有被執(zhí)行、但peek后面加上終止操作之后便可以被執(zhí)行,而foreach
可以直接被執(zhí)行:
css復(fù)制代碼 ----before peek---- ----after peek---- ----before foreach---- hello world Jia Gou Wu Dao ----after foreach---- ----before peek and count---- hello world Jia Gou Wu Dao ----after peek and count---- ?
filter、sorted、distinct、limit
這幾個(gè)都是常用的Stream的中間操作方法,具體的方法的含義在上面的表格里面有說明。具體使用的時(shí)候,可以根據(jù)需要選擇一個(gè)或者多個(gè)進(jìn)行組合使用,或者同時(shí)使用多個(gè)相同方法的組合:
java復(fù)制代碼 public void testGetTargetUsers() { ?? ?List<String> ids = Arrays.asList("205","10","308","49","627","193","111", "193"); ?? ?// 使用流操作 ?? ?List<Dept> results = ids.stream() ?? ? ? ? ? ?.filter(s -> s.length() > 2) ?? ? ? ? ? ?.distinct() ?? ? ? ? ? ?.map(Integer::valueOf) ?? ? ? ? ? ?.sorted(Comparator.comparingInt(o -> o)) ?? ? ? ? ? ?.limit(3) ?? ? ? ? ? ?.map(id -> new Dept(id)) ?? ? ? ? ? ?.collect(Collectors.toList()); ?? ?System.out.println(results); } ?
上面的代碼片段的處理邏輯很清晰:
使用filter過濾掉不符合條件的數(shù)據(jù)
通過distinct對(duì)存量元素進(jìn)行去重操作
通過map操作將字符串轉(zhuǎn)成整數(shù)類型
借助sorted指定按照數(shù)字大小正序排列
使用limit截取排在前3位的元素
又一次使用map將id轉(zhuǎn)為Dept對(duì)象類型
使用collect終止操作將最終處理后的數(shù)據(jù)收集到list中
輸出結(jié)果:
bash復(fù)制代碼[Dept{id=111}, ?Dept{id=193}, ?Dept{id=205}] ?
簡(jiǎn)單結(jié)果終止方法
按照前面介紹的,終止方法里面像count
、max
、min
、findAny
、findFirst
、anyMatch
、allMatch
、nonneMatch
等方法,均屬于這里說的簡(jiǎn)單結(jié)果終止方法。所謂簡(jiǎn)單,指的是其結(jié)果形式是數(shù)字、布爾值或者Optional對(duì)象值等。
java復(fù)制代碼 public void testSimpleStopOptions() { ?? ?List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193"); ?? ?// 統(tǒng)計(jì)stream操作后剩余的元素個(gè)數(shù) ?? ?System.out.println(ids.stream().filter(s -> s.length() > 2).count()); ?? ?// 判斷是否有元素值等于205 ?? ?System.out.println(ids.stream().filter(s -> s.length() > 2).anyMatch("205"::equals)); ?? ?// findFirst操作 ?? ?ids.stream().filter(s -> s.length() > 2) ?? ? ? ? ? ?.findFirst() ?? ? ? ? ? ?.ifPresent(s -> System.out.println("findFirst:" + s)); } ?
執(zhí)行后結(jié)果為:
vbnet復(fù)制代碼 6 true findFirst:205 ?
避坑提醒
這里需要補(bǔ)充提醒下,一旦一個(gè)Stream被執(zhí)行了終止操作之后,后續(xù)便不可以再讀這個(gè)流執(zhí)行其他的操作了,否則會(huì)報(bào)錯(cuò),看下面示例:
java復(fù)制代碼 public void testHandleStreamAfterClosed() { ?? ?List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193"); ?? ?Stream<String> stream = ids.stream().filter(s -> s.length() > 2); ?? ?// 統(tǒng)計(jì)stream操作后剩余的元素個(gè)數(shù) ?? ?System.out.println(stream.count()); ?? ?System.out.println("-----下面會(huì)報(bào)錯(cuò)-----"); ?? ?// 判斷是否有元素值等于205 ?? ?try { ?? ? ? ?System.out.println(stream.anyMatch("205"::equals)); ?? ?} catch (Exception e) { ?? ? ? ?e.printStackTrace(); ?? ?} ?? ?System.out.println("-----上面會(huì)報(bào)錯(cuò)-----"); } ?
執(zhí)行的時(shí)候,結(jié)果如下:
css復(fù)制代碼 6 -----下面會(huì)報(bào)錯(cuò)----- java.lang.IllegalStateException: stream has already been operated upon or closed ?at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) ?at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449) ?at com.veezean.skills.stream.StreamService.testHandleStreamAfterClosed(StreamService.java:153) ?at com.veezean.skills.stream.StreamService.main(StreamService.java:176) -----上面會(huì)報(bào)錯(cuò)----- ?
因?yàn)閟tream已經(jīng)被執(zhí)行count()
終止方法了,所以對(duì)stream再執(zhí)行anyMatch
方法的時(shí)候,就會(huì)報(bào)錯(cuò)stream has already been operated upon or closed
,這一點(diǎn)在使用的時(shí)候需要特別注意。
結(jié)果收集終止方法
因?yàn)镾tream主要用于對(duì)集合數(shù)據(jù)的處理場(chǎng)景,所以除了上面幾種獲取簡(jiǎn)單結(jié)果的終止方法之外,更多的場(chǎng)景是獲取一個(gè)集合類的結(jié)果對(duì)象,比如List、Set或者HashMap等。
這里就需要collect
方法出場(chǎng)了,它可以支持生成如下類型的結(jié)果數(shù)據(jù):
一個(gè)
集合類
,比如List、Set或者HashMap等StringBuilder對(duì)象,支持將多個(gè)
字符串進(jìn)行拼接
處理并輸出拼接后結(jié)果一個(gè)可以記錄個(gè)數(shù)或者計(jì)算總和的對(duì)象(
數(shù)據(jù)批量運(yùn)算統(tǒng)計(jì)
)
生成集合
應(yīng)該算是collect最常被使用到的一個(gè)場(chǎng)景了:
java復(fù)制代碼 public void testCollectStopOptions() { ?? ?List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23)); ?? ?// collect成list ?? ?List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20) ?? ? ? ? ? ?.collect(Collectors.toList()); ?? ?System.out.println("collectList:" + collectList); ?? ?// collect成Set ?? ?Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20) ?? ? ? ? ? ?.collect(Collectors.toSet()); ?? ?System.out.println("collectSet:" + collectSet); ?? ?// collect成HashMap,key為id,value為Dept對(duì)象 ?? ?Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20) ?? ? ? ? ? ?.collect(Collectors.toMap(Dept::getId, dept -> dept)); ?? ?System.out.println("collectMap:" + collectMap); } ?
結(jié)果如下:
bash復(fù)制代碼 collectList:[Dept{id=22}, Dept{id=23}] collectSet:[Dept{id=23}, Dept{id=22}] collectMap:{22=Dept{id=22}, 23=Dept{id=23}} ?
生成拼接字符串
將一個(gè)List或者數(shù)組中的值拼接到一個(gè)字符串里并以逗號(hào)分隔開,這個(gè)場(chǎng)景相信大家都不陌生吧?
如果通過for
循環(huán)和StringBuilder
去循環(huán)拼接,還得考慮下最后一個(gè)逗號(hào)如何處理的問題,很繁瑣:
java復(fù)制代碼 public void testForJoinStrings() { ?? ?List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193"); ?? ?StringBuilder builder = new StringBuilder(); ?? ?for (String id : ids) { ?? ? ? ?builder.append(id).append(','); ?? ?} ?? ?// 去掉末尾多拼接的逗號(hào) ?? ?builder.deleteCharAt(builder.length() - 1); ?? ?System.out.println("拼接后:" + builder.toString()); } ?
但是現(xiàn)在有了Stream,使用collect
可以輕而易舉的實(shí)現(xiàn):
java復(fù)制代碼 public void testCollectJoinStrings() { ?? ?List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193"); ?? ?String joinResult = ids.stream().collect(Collectors.joining(",")); ?? ?System.out.println("拼接后:" + joinResult); } ?
兩種方式都可以得到完全相同的結(jié)果,但Stream的方式更優(yōu)雅:
復(fù)制代碼拼接后:205,10,308,49,627,193,111,193 ?
?? 敲黑板:
關(guān)于這里的說明,評(píng)論區(qū)中很多的小伙伴提出過疑問,就是這個(gè)場(chǎng)景其實(shí)使用 String.join()
就可以搞定了,并不需要上面使用 stream
的方式去實(shí)現(xiàn)。這里要聲明下,Stream的魅力之處就在于其可以結(jié)合到其它的業(yè)務(wù)邏輯中進(jìn)行處理,讓代碼邏輯更加的自然、一氣呵成。如果純粹是個(gè)String字符串拼接的訴求,確實(shí)沒有必要使用Stream來實(shí)現(xiàn),畢竟殺雞焉用牛刀嘛~ 但是可以看看下面給出的這個(gè)示例,便可以感受出使用Stream進(jìn)行字符串拼接的真正魅力所在。
數(shù)據(jù)批量數(shù)學(xué)運(yùn)算
還有一種場(chǎng)景,實(shí)際使用的時(shí)候可能會(huì)比較少,就是使用collect生成數(shù)字?jǐn)?shù)據(jù)的總和信息,也可以了解下實(shí)現(xiàn)方式:
java復(fù)制代碼 public void testNumberCalculate() { ?? ?List<Integer> ids = Arrays.asList(10, 20, 30, 40, 50); ?? ?// 計(jì)算平均值 ?? ?Double average = ids.stream().collect(Collectors.averagingInt(value -> value)); ?? ?System.out.println("平均值:" + average); ?? ?// 數(shù)據(jù)統(tǒng)計(jì)信息 ?? ?IntSummaryStatistics summary = ids.stream().collect(Collectors.summarizingInt(value -> value)); ?? ?System.out.println("數(shù)據(jù)統(tǒng)計(jì)信息: " + summary); } ?
上面的例子中,使用collect方法來對(duì)list中元素值進(jìn)行數(shù)學(xué)運(yùn)算,結(jié)果如下:
python復(fù)制代碼 平均值:30.0 總和: IntSummaryStatistics{count=5, sum=150, min=10, average=30.000000, max=50} ?
并行Stream
機(jī)制說明
使用并行流,可以有效利用計(jì)算機(jī)的多CPU硬件,提升邏輯的執(zhí)行速度。并行流通過將一整個(gè)stream劃分為多個(gè)片段
,然后對(duì)各個(gè)分片流并行執(zhí)行處理邏輯,最后將各個(gè)分片流的執(zhí)行結(jié)果匯總為一個(gè)整體流。
約束與限制
并行流類似于多線程在并行處理,所以與多線程場(chǎng)景相關(guān)的一些問題同樣會(huì)存在,比如死鎖等問題,所以在并行流終止執(zhí)行的函數(shù)邏輯,必須要保證線程安全。
回答最初的問題
到這里,關(guān)于JAVA Stream的相關(guān)概念與用法介紹,基本就講完了。我們?cè)侔呀裹c(diǎn)切回本文剛開始時(shí)提及的一個(gè)問題:
Stream相較于傳統(tǒng)的foreach的方式處理stream,到底有啥優(yōu)勢(shì)?
根據(jù)前面的介紹,我們應(yīng)該可以得出如下幾點(diǎn)答案:
代碼更簡(jiǎn)潔、偏聲明式的編碼風(fēng)格,更容易體現(xiàn)出代碼的邏輯意圖
邏輯間解耦,一個(gè)stream中間處理邏輯,無需關(guān)注上游與下游的內(nèi)容,只需要按約定實(shí)現(xiàn)自身邏輯即可
并行流場(chǎng)景效率會(huì)比迭代器逐個(gè)循環(huán)更高
函數(shù)式接口,延遲執(zhí)行的特性,中間管道操作不管有多少步驟都不會(huì)立即執(zhí)行,只有遇到終止操作的時(shí)候才會(huì)開始執(zhí)行,可以避免一些中間不必要的操作消耗
當(dāng)然了,Stream也不全是優(yōu)點(diǎn),在有些方面也有其弊端:
代碼調(diào)測(cè)debug不便
程序員從歷史寫法切換到Stream時(shí),需要一定的適應(yīng)時(shí)間
總結(jié)
好啦,關(guān)于JAVA Stream的理解要點(diǎn)與使用技能的闡述就先到這里啦。那通過上面的介紹,各位小伙伴們是否已經(jīng)躍躍欲試了呢?快去項(xiàng)目中使用體驗(yàn)下吧!當(dāng)然啦,如果有疑問,也歡迎找我一起探討探討咯。