Java8新特性Stream流詳解
一、Stream 流是如何工作的?
流表示包含著一系列元素的集合,我們可以對(duì)其做不同類(lèi)型的操作,用來(lái)對(duì)這些元素執(zhí)行計(jì)算。
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1"); myList .stream() // 創(chuàng)建流 .filter(s -> s.startsWith("c")) // 執(zhí)行過(guò)濾,過(guò)濾出以 c 為前綴的字符串 .map(String::toUpperCase) // 轉(zhuǎn)換成大寫(xiě) .sorted() // 排序 .forEach(System.out::println); // for 循環(huán)打印 12345678910
我們可以對(duì)流進(jìn)行中間操作或者終端操作。兄弟們可能會(huì)疑問(wèn)?什么是中間操作?什么又是終端操作?
中間操作:①:中間操作會(huì)再次返回一個(gè)流,所以,我們可以鏈接多個(gè)中間操作,注意這里是不用加分號(hào)的。上圖中的filter 過(guò)濾,map 對(duì)象轉(zhuǎn)換,sorted 排序,就屬于中間操作。
終端操作:②:終端操作是對(duì)流操作的一個(gè)結(jié)束動(dòng)作,一般返回 void 或者一個(gè)非流的結(jié)果。上圖中的 forEach循環(huán) 就是一個(gè)終止操作。
看完上面的操作,感覺(jué)是不是很像一個(gè)流水線(xiàn)式操作呢。實(shí)際上,大部分流操作都支持 lambda 表達(dá)式作為參數(shù),正確理解,應(yīng)該說(shuō)是接受一個(gè)函數(shù)式接口的實(shí)現(xiàn)作為參數(shù)。
二、不同類(lèi)型的 Stream 流
我們可以從各種數(shù)據(jù)源中創(chuàng)建 Stream 流,其中以 Collection 集合最為常見(jiàn)。如 List 和 Set 均支持 stream() 方法來(lái)創(chuàng)建順序流或者是并行流。
1.Arrays.asList()
并行流是通過(guò)多線(xiàn)程的方式來(lái)執(zhí)行的,它能夠充分發(fā)揮多核 CPU 的優(yōu)勢(shì)來(lái)提升性能。本文在最后再來(lái)介紹并行流,我們先討論順序流:
Arrays.asList("a1", "a2", "a3") .stream() // 創(chuàng)建流 .findFirst() // 找到第一個(gè)元素 .ifPresent(System.out::println); // 如果存在,即輸出 1234
2.Stream.of()
在集合上調(diào)用stream()方法會(huì)返回一個(gè)普通的 Stream 流。但是, 大可不必刻意地創(chuàng)建一個(gè)集合,再通過(guò)集合來(lái)獲取 Stream 流,還可以通過(guò)如下這種方式:
Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println); 123
例如上面這樣,我們可以通過(guò) Stream.of() 從一堆對(duì)象中創(chuàng)建 Stream 流。
注: 除了常規(guī)對(duì)象流之外,Java 8還附帶了一些特殊類(lèi)型的流,用于處理原始數(shù)據(jù)類(lèi)型int,long以及double。說(shuō)道這里,你可能已經(jīng)猜到了它們就是IntStream
,LongStream
還有DoubleStream
。
3.IntStream.range()
IntStreams.range()方法還可以被用來(lái)取代常規(guī)的 for 循環(huán), 如下所示:
IntStream.range(1, 4) .forEach(System.out::println); // 相當(dāng)于 for (int i = 1; i < 4; i++) {} 123
注: 上面這些原始類(lèi)型流的工作方式與常規(guī)對(duì)象流基本是一樣的,但還是略微存在一些區(qū)別:
原始類(lèi)型流使用其獨(dú)有的函數(shù)式接口,例如IntFunction
代替Function,IntPredicate
代替Predicate。
4.average()
原始類(lèi)型流支持額外的終端聚合操作,sum()以及average()
,如下所示:
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) // 對(duì)數(shù)值中的每個(gè)對(duì)象執(zhí)行 2*n + 1 操作 .average() // 求平均值 .ifPresent(System.out::println); // 如果值不為空,則輸出 1234
5.mapToInt(),mapToLong() ,mapToDouble()
但是,偶爾我們也有這種需求,需要將常規(guī)對(duì)象流轉(zhuǎn)換為原始類(lèi)型流,這個(gè)時(shí)候,中間操作 mapToInt()
,mapToLong()
以及mapToDouble
就派上用場(chǎng)了:
Stream.of("a1", "a2", "a3") .map(s -> s.substring(1)) // 對(duì)每個(gè)字符串元素從下標(biāo)1位置開(kāi)始截取 .mapToInt(Integer::parseInt) // 轉(zhuǎn)成 int 基礎(chǔ)類(lèi)型類(lèi)型流 .max() // 取最大值 .ifPresent(System.out::println); // 不為空則輸出 12345
6.mapToObj()
如果說(shuō),需要將原始類(lèi)型流裝換成對(duì)象流,您可以使用 mapToObj()
來(lái)達(dá)到目的:
IntStream.range(1, 4) .mapToObj(i -> "a" + i) // for 循環(huán) 1->4, 拼接前綴 a .forEach(System.out::println); // for 循環(huán)打印 123
下面是一個(gè)組合示例,我們將雙精度流首先轉(zhuǎn)換成 int 類(lèi)型流,然后再將其裝換成對(duì)象流:
Stream.of(1.0, 2.0, 3.0) .mapToInt(Double::intValue) // double 類(lèi)型轉(zhuǎn) int .mapToObj(i -> "a" + i) // 對(duì)值拼接前綴 a .forEach(System.out::println); // for 循環(huán)打印 1234
三、Stream 流的處理順序
上小節(jié)中,我們已經(jīng)學(xué)會(huì)了如何創(chuàng)建不同類(lèi)型的 Stream 流,接下來(lái)我們?cè)偕钊肓私庀聰?shù)據(jù)流的執(zhí)行順序。
在討論處理順序之前,您需要明確一點(diǎn),那就是中間操作的有個(gè)重要特性 —— 延遲性。觀察下面這個(gè)沒(méi)有終端操作的示例代碼:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }); 12345
注: 執(zhí)行此代碼段時(shí),您可能會(huì)認(rèn)為,將依次打印 “d2”, “a2”, “b1”, “b3”, “c” 元素。然而當(dāng)你實(shí)際去執(zhí)行的時(shí)候,它不會(huì)打印任何內(nèi)容。
出現(xiàn)這樣的原因是:當(dāng)且僅當(dāng)存在終端操作時(shí),中間操作操作才會(huì)被執(zhí)行。
接下來(lái),對(duì)上面的代碼添加 forEach終端操作:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return true; }) .forEach(s -> System.out.println("forEach: " + s)); 123456
再次執(zhí)行,我們會(huì)看到輸出如下:
filter: d2
forEach: d2
filter: a2
forEach: a2
filter: b1
forEach: b1
filter: b3
forEach: b3
filter: c
forEach: c12345678910
思考: 輸出的順序可能會(huì)讓你很驚訝!你腦海里肯定會(huì)想,應(yīng)該是先將所有 filter 前綴的字符串打印出來(lái),接著才會(huì)打印 forEach 前綴的字符串。
事實(shí)上,輸出的結(jié)果卻是隨著鏈條垂直移動(dòng)的。比如說(shuō),當(dāng) Stream 開(kāi)始處理 d2 元素時(shí),它實(shí)際上會(huì)在執(zhí)行完 filter 操作后,再執(zhí)行 forEach 操作,接著才會(huì)處理第二個(gè)元素。
是不是很神奇?為什么要設(shè)計(jì)成這樣呢?
原因是出于性能的考慮。這樣設(shè)計(jì)可以減少對(duì)每個(gè)元素的實(shí)際操作數(shù),看完下面代碼你就明白了:
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě) }) .anyMatch(s -> { System.out.println("anyMatch: " + s); return s.startsWith("A"); // 過(guò)濾出以 A 為前綴的元素 }); // map: d2 // anyMatch: D2 // map: a2 // anyMatch: A2 1234567891011121314
說(shuō)明: 終端操作 anyMatch()
表示任何一個(gè)元素以 A 為前綴,返回為 true,就停止循環(huán)。所以它會(huì)從 d2 開(kāi)始匹配,接著循環(huán)到 a2 的時(shí)候,返回為 true ,于是停止循環(huán)。
由于數(shù)據(jù)流的鏈?zhǔn)秸{(diào)用是垂直執(zhí)行的,map這里只需要執(zhí)行兩次。相對(duì)于水平執(zhí)行來(lái)說(shuō),map會(huì)執(zhí)行盡可能少的次數(shù),而不是把所有元素都 map 轉(zhuǎn)換一遍。
四、中間操作順序這么重要?
1.map和filter垂直執(zhí)行
下面的例子由兩個(gè)中間操作map
和filter
,以及一個(gè)終端操作forEach
組成。讓我們?cè)賮?lái)看看這些操作是如何執(zhí)行的:
Stream.of("d2", "a2", "b1", "b3", "c") .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě) }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); // 過(guò)濾出以 A 為前綴的元素 }) .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出 // map: d2 // filter: D2 // map: a2 // filter: A2 // forEach: A2 // map: b1 // filter: B1 // map: b3 // filter: B3 // map: c // filter: C 12345678910111213141516171819202122
注: 學(xué)習(xí)了上面一小節(jié),您應(yīng)該已經(jīng)知道了,map和filter會(huì)對(duì)集合中的每個(gè)字符串調(diào)用五次,而forEach卻只會(huì)調(diào)用一次,因?yàn)橹挥?“a2” 滿(mǎn)足過(guò)濾條件,滿(mǎn)足條件才會(huì)放行
如果我們改變中間操作的順序,將filter移動(dòng)到鏈頭的最開(kāi)始,就可以大大減少實(shí)際的執(zhí)行次數(shù):
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s) return s.startsWith("a"); // 過(guò)濾出以 a 為前綴的元素 }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě) }) .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出 // filter: d2 // filter: a2 // map: a2 // forEach: A2 // filter: b1 // filter: b3 // filter: c 123456789101112131415161718
現(xiàn)在,map僅僅只需調(diào)用一次,性能得到了提升,這種小技巧對(duì)于流中存在大量元素來(lái)說(shuō),是非常很有用的。
2.sorted水平執(zhí)行
接下來(lái),讓我們對(duì)上面的代碼再添加一個(gè)中間操作sorted
:
Stream.of("d2", "a2", "b1", "b3", "c") .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); // 排序 }) .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); // 過(guò)濾出以 a 為前綴的元素 }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); // 轉(zhuǎn)大寫(xiě) }) .forEach(s -> System.out.println("forEach: " + s)); // for 循環(huán)輸出 1234567891011121314
sorted
是一個(gè)有狀態(tài)的操作,因?yàn)樗枰谔幚淼倪^(guò)程中,保存狀態(tài)以對(duì)集合中的元素進(jìn)行排序。
執(zhí)行上面代碼,輸出如下:
sort: a2; d2
sort: b1; a2
sort: b1; d2
sort: b1; a2
sort: b3; b1
sort: b3; d2
sort: c; b3
sort: c; d2
filter: a2
map: a2
forEach: A2
filter: b1
filter: b3
filter: c
filter: d2123456789101112131415
sorted是水平執(zhí)行的。因此,在這種情況下,sorted會(huì)對(duì)集合中的元素組合調(diào)用八次。這里,我們也可以利用上面說(shuō)道的優(yōu)化技巧
將 filter 過(guò)濾中間操作移動(dòng)到開(kāi)頭部分:
Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }) .sorted((s1, s2) -> { System.out.printf("sort: %s; %s\n", s1, s2); return s1.compareTo(s2); }) .map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }) .forEach(s -> System.out.println("forEach: " + s)); // filter: d2 // filter: a2 // filter: b1 // filter: b3 // filter: c // map: a2 // forEach: A2 12345678910111213141516171819202122
從上面的輸出中,我們看到了 sorted從未被調(diào)用過(guò),因?yàn)榻?jīng)過(guò)filter過(guò)后的元素已經(jīng)減少到只有一個(gè),這種情況下,是不用執(zhí)行排序操作的。因此性能被大大提高了。
五、數(shù)據(jù)流復(fù)用問(wèn)題
Java8 Stream 流是不能被復(fù)用的,一旦你調(diào)用任何終端操作,流就會(huì)關(guān)閉:
Stream<String> stream = Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); stream.anyMatch(s -> true); // ok stream.noneMatch(s -> true); // exception 123456
當(dāng)我們對(duì) stream 調(diào)用了 anyMatch 終端操作以后,流即關(guān)閉了,再調(diào)用 noneMatch 就會(huì)拋出異常:
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.noneMatch(ReferencePipeline.java:459) at com.winterbe.java8.Streams5.test7(Streams5.java:38) at com.winterbe.java8.Streams5.main(Streams5.java:28) 12345
為了克服這個(gè)限制,我們必須為我們想要執(zhí)行的每個(gè)終端操作創(chuàng)建一個(gè)新的流鏈,例如,我們可以通過(guò) Supplier 來(lái)包裝一下流,通過(guò) get() 方法來(lái)構(gòu)建一個(gè)新的 Stream 流,如下所示:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok 123456
通過(guò)構(gòu)造一個(gè)新的流,來(lái)避開(kāi)流不能被復(fù)用的限制, 這也是取巧的一種方式。
六、高級(jí)操作
Streams 支持的操作很豐富,除了上面介紹的這些比較常用的中間操作,如filter或map(參見(jiàn)Stream Javadoc)外。還有一些更復(fù)雜的操作,如collect
,flatMap
以及reduce
。接下來(lái),就讓我們學(xué)習(xí)一下:
本小節(jié)中的大多數(shù)代碼示例均會(huì)使用以下 List進(jìn)行演示:
class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return name; } } 1234567891011121314
// 構(gòu)建一個(gè) Person 集合 List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); 1234567
1.Collect
collect 是一個(gè)非常有用的終端操作,它可以將流中的元素轉(zhuǎn)變成另外一個(gè)不同的對(duì)象,例如一個(gè)List,Set或Map。collect 接受入?yún)镃ollector(收集器),它由四個(gè)不同的操作組成:供應(yīng)器(supplier)、累加器(accumulator)、組合器(combiner)和終止器(finisher)。
感覺(jué)復(fù)雜其實(shí)很簡(jiǎn)單,其實(shí)并不需要自己去實(shí)現(xiàn)收集器。因?yàn)?Java 8通過(guò)Collectors類(lèi)內(nèi)置了各種常用的收集器,你直接拿來(lái)用就行了。
2.Collectors.toList()
讓我們先從一個(gè)非常常見(jiàn)的用例開(kāi)始:
List<Person> filtered = persons .stream() // 構(gòu)建流 .filter(p -> p.name.startsWith("P")) // 過(guò)濾出名字以 P 開(kāi)頭的 .collect(Collectors.toList()); // 生成一個(gè)新的 List System.out.println(filtered); // [Peter, Pamela] 1234567
你也看到了,從流中構(gòu)造一個(gè) List 異常簡(jiǎn)單。如果說(shuō)你需要構(gòu)造一個(gè) Set 集合,只需要使用Collectors.toSet()
就可以了。
3.Collectors.groupingBy
接下來(lái)這個(gè)示例,將會(huì)按年齡對(duì)所有人進(jìn)行分組:
Map<Integer, List<Person>> personsByAge = persons .stream() .collect(Collectors.groupingBy(p -> p.age)); // 以年齡為 key,進(jìn)行分組 personsByAge .forEach((age, p) -> System.out.format("age %s: %s\n", age, p)); // age 18: [Max] // age 23: [Peter, Pamela] // age 12: [David] 12345678910
5.Collectors.summarizingInt
如果您還想得到一個(gè)更全面的統(tǒng)計(jì)信息,摘要收集器可以返回一個(gè)特殊的內(nèi)置統(tǒng)計(jì)對(duì)象。通過(guò)它,我們可以簡(jiǎn)單地計(jì)算出最小年齡、最大年齡、平均年齡、總和以及總數(shù)量。
IntSummaryStatistics ageSummary = persons .stream() .collect(Collectors.summarizingInt(p -> p.age)); // 生成摘要統(tǒng)計(jì) System.out.println(ageSummary); // IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23} 1234567
6.Collectors.joining
下一個(gè)這個(gè)示例,可以將所有人名連接成一個(gè)字符串:
String phrase = persons .stream() .filter(p -> p.age >= 18) // 過(guò)濾出年齡大于等于18的 .map(p -> p.name) // 提取名字 .collect(Collectors.joining(" and ", "In Germany ", " are of legal age.")); // 以 In Germany 開(kāi)頭,and 連接各元素,再以 are of legal age. 結(jié)束 System.out.println(phrase); // In Germany Max and Peter and Pamela are of legal age. 12345678
連接收集器的入?yún)⒔邮芊指舴约翱蛇x的前綴以及后綴。
7.對(duì)于如何將流轉(zhuǎn)換為 Map集合
我們必須指定 Map 的鍵和值。這里需要注意,Map 的鍵必須是唯一的,否則會(huì)拋出IllegalStateException 異常。
你可以選擇傳遞一個(gè)合并函數(shù)作為額外的參數(shù)來(lái)避免發(fā)生這個(gè)異常:
Map<Integer, String> map = persons .stream() .collect(Collectors.toMap( p -> p.age, p -> p.name, (name1, name2) -> name1 + ";" + name2)); // 對(duì)于同樣 key 的,將值拼接 System.out.println(map); // {18=Max, 23=Peter;Pamela, 12=David} 123456789
8.構(gòu)建自定義收集器
既然我們已經(jīng)知道了這些強(qiáng)大的內(nèi)置收集器,接下來(lái)就讓我們嘗試構(gòu)建自定義收集器吧。
比如說(shuō),我們希望將流中的所有人轉(zhuǎn)換成一個(gè)字符串,包含所有大寫(xiě)的名稱(chēng),并以|分割。為了達(dá)到這種效果,我們需要通過(guò)Collector.of()創(chuàng)建一個(gè)新的收集器。同時(shí),我們還需要傳入收集器的四個(gè)組成部分:供應(yīng)器、累加器、組合器和終止器。
Collector<Person, StringJoiner, String> personNameCollector = Collector.of( () -> new StringJoiner(" | "), // supplier 供應(yīng)器 (j, p) -> j.add(p.name.toUpperCase()), // accumulator 累加器 (j1, j2) -> j1.merge(j2), // combiner 組合器 StringJoiner::toString); // finisher 終止器 String names = persons .stream() .collect(personNameCollector); // 傳入自定義的收集器 System.out.println(names); // MAX | PETER | PAMELA | DAVID 123456789101112
由于Java 中的字符串是 final 類(lèi)型的,我們需要借助輔助類(lèi)StringJoiner
,來(lái)幫我們構(gòu)造字符串。
最開(kāi)始供應(yīng)器使用分隔符構(gòu)造了一個(gè)StringJointer。
累加器用于將每個(gè)人的人名轉(zhuǎn)大寫(xiě),然后加到StringJointer中。
組合器將兩個(gè)StringJointer合并為一個(gè)。
最終,終結(jié)器從StringJointer構(gòu)造出預(yù)期的字符串。
9.FlatMap
上面我們已經(jīng)學(xué)會(huì)了如通過(guò)map操作, 將流中的對(duì)象轉(zhuǎn)換為另一種類(lèi)型。但是,Map只能將每個(gè)對(duì)象映射到另一個(gè)對(duì)象。
如果說(shuō),我們想要將一個(gè)對(duì)象轉(zhuǎn)換為多個(gè)其他對(duì)象或者根本不做轉(zhuǎn)換操作呢?這個(gè)時(shí)候,flatMap就派上用場(chǎng)了。
FlatMap
能夠?qū)⒘鞯拿總€(gè)元素, 轉(zhuǎn)換為其他對(duì)象的流。因此,每個(gè)對(duì)象可以被轉(zhuǎn)換為零個(gè),一個(gè)或多個(gè)其他對(duì)象,并以流的方式返回。之后,這些流的內(nèi)容會(huì)被放入flatMap返回的流中。
在學(xué)習(xí)如何實(shí)際操作flatMap之前,我們先新建兩個(gè)類(lèi),用來(lái)測(cè)試:
class Foo { String name; List<Bar> bars = new ArrayList<>(); Foo(String name) { this.name = name; } } class Bar { String name; Bar(String name) { this.name = name; } } 12345678910111213141516
接下來(lái),通過(guò)我們上面學(xué)習(xí)到的流知識(shí),來(lái)實(shí)例化一些對(duì)象:
List<Foo> foos = new ArrayList<>(); // 創(chuàng)建 foos 集合 IntStream .range(1, 4) .forEach(i -> foos.add(new Foo("Foo" + i))); // 創(chuàng)建 bars 集合 foos.forEach(f -> IntStream .range(1, 4) .forEach(i -> f.bars.add(new Bar("Bar" + i + " <- " + f.name)))); 123456789101112
我們創(chuàng)建了包含三個(gè)foo的集合,每個(gè)foo中又包含三個(gè) bar。
flatMap 的入?yún)⒔邮芤粋€(gè)返回對(duì)象流的函數(shù)。為了處理每個(gè)foo中的bar,我們需要傳入相應(yīng) stream 流:
foos.stream() .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); // Bar1 <- Foo1 // Bar2 <- Foo1 // Bar3 <- Foo1 // Bar1 <- Foo2 // Bar2 <- Foo2 // Bar3 <- Foo2 // Bar1 <- Foo3 // Bar2 <- Foo3 // Bar3 <- Foo3 12345678910111213
如上所示,我們已成功將三個(gè) foo對(duì)象的流轉(zhuǎn)換為九個(gè)bar對(duì)象的流。
最后,上面的這段代碼可以簡(jiǎn)化為單一的流式操作:
IntStream.range(1, 4) .mapToObj(i -> new Foo("Foo" + i)) .peek(f -> IntStream.range(1, 4) .mapToObj(i -> new Bar("Bar" + i + " <- " f.name)) .forEach(f.bars::add)) .flatMap(f -> f.bars.stream()) .forEach(b -> System.out.println(b.name)); 1234567
flatMap也可用于Java8引入的Optional類(lèi)。Optional的flatMap操作返回一個(gè)Optional或其他類(lèi)型的對(duì)象。所以它可以用于避免繁瑣的null檢查。
接下來(lái),讓我們創(chuàng)建層次更深的對(duì)象:
class Outer { Nested nested; } class Nested { Inner inner; } class Inner { String foo; } 1234567891011
我們還可以使用Optional的flatMap操作,來(lái)完成上述相同功能的判斷,且更加優(yōu)雅:
Optional.of(new Outer()) .flatMap(o -> Optional.ofNullable(o.nested)) .flatMap(n -> Optional.ofNullable(n.inner)) .flatMap(i -> Optional.ofNullable(i.foo)) .ifPresent(System.out::println); 12345
注: 如果不為空的話(huà),每個(gè)flatMap的調(diào)用都會(huì)返回預(yù)期對(duì)象的Optional包裝,否則返回為null的Optional包裝類(lèi)。
10.Reduce
規(guī)約操作可以將流的所有元素組合成一個(gè)結(jié)果。Java 8 支持三種不同的reduce
方法。第一種將流中的元素規(guī)約成流中的一個(gè)元素。
讓我們看看如何使用這種方法,來(lái)篩選出年齡最大的那個(gè)人:
persons .stream() .reduce((p1, p2) -> p1.age > p2.age ? p1 : p2) .ifPresent(System.out::println); // Pamela 1234
reduce
方法接受BinaryOperator
積累函數(shù)。該函數(shù)實(shí)際上是兩個(gè)操作數(shù)類(lèi)型相同的BiFunction
。BiFunction功能和Function一樣,但是它接受兩個(gè)參數(shù)。示例代碼中,我們比較兩個(gè)人的年齡,來(lái)返回年齡較大的人。
第二種reduce方法接受標(biāo)識(shí)值和BinaryOperator
累加器。此方法可用于構(gòu)造一個(gè)新的 Person,其中包含來(lái)自流中所有其他人的聚合名稱(chēng)和年齡:
Person result = persons .stream() .reduce(new Person("", 0), (p1, p2) -> { p1.age += p2.age; p1.name += p2.name; return p1; }); System.out.format("name=%s; age=%s", result.name, result.age); // name=MaxPeterPamelaDavid; age=76 1234567891011
第三種reduce方法接受三個(gè)參數(shù):標(biāo)識(shí)值,BiFunction累加器和類(lèi)型的組合器函數(shù)BinaryOperator。由于初始值的類(lèi)型不一定為Person,我們可以使用這個(gè)歸約函數(shù)來(lái)計(jì)算所有人的年齡總和:
Integer ageSum = persons .stream() .reduce(0, (sum, p) -> sum += p.age, (sum1, sum2) -> sum1 + sum2); System.out.println(ageSum); // 76 12345
結(jié)果為76,但是內(nèi)部究竟發(fā)生了什么呢?讓我們?cè)俅蛴∫恍┱{(diào)試日志:
Integer ageSum = persons .stream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Max // accumulator: sum=18; person=Peter // accumulator: sum=41; person=Pamela // accumulator: sum=64; person=David 12345678910111213141516
你可以看到,累加器函數(shù)完成了所有工作。它首先使用初始值0和第一個(gè)人年齡相加。接下來(lái)的三步中sum會(huì)持續(xù)增加,直到76。
我們以并行流的方式運(yùn)行上面的代碼,看看日志輸出:
Integer ageSum = persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s\n", sum, p); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s\n", sum1, sum2); return sum1 + sum2; }); // accumulator: sum=0; person=Pamela // accumulator: sum=0; person=David // accumulator: sum=0; person=Max // accumulator: sum=0; person=Peter // combiner: sum1=18; sum2=23 // combiner: sum1=23; sum2=12 // combiner: sum1=41; sum2=35 12345678910111213141516171819
注: 并行流的執(zhí)行方式完全不同。這里組合器被調(diào)用了。實(shí)際上,由于累加器被并行調(diào)用,組合器需要被用于計(jì)算部分累加值的總和。
七、并行流
流是可以并行執(zhí)行的,當(dāng)流中存在大量元素時(shí),可以顯著提升性能。并行流底層使用的ForkJoinPool
, 它由ForkJoinPool.commonPool()
方法提供。底層線(xiàn)程池的大小最多為五個(gè) - 具體取決于 CPU 可用核心數(shù):
ForkJoinPool commonPool = ForkJoinPool.commonPool(); System.out.println(commonPool.getParallelism()); // 3 12
在我的機(jī)器上,公共池初始化默認(rèn)值為 3。你也可以通過(guò)設(shè)置以下JVM參數(shù)可以減小或增加此值:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5 1
集合支持parallelStream()
方法來(lái)創(chuàng)建元素的并行流。或者你可以在已存在的數(shù)據(jù)流上調(diào)用中間方法parallel()
,將串行流轉(zhuǎn)換為并行流,這也是可以的。
為了詳細(xì)了解并行流的執(zhí)行行為,我們?cè)谙旅娴氖纠a中,打印當(dāng)前線(xiàn)程的信息:
Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 1234567891011121314
通過(guò)日志輸出,我們可以對(duì)哪個(gè)線(xiàn)程被用于執(zhí)行流式操作,有個(gè)更深入的理解:
filter: b1 [main]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: c2 [ForkJoinPool.commonPool-worker-3]
map: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: A2 [ForkJoinPool.commonPool-worker-1]
map: b1 [main]
forEach: B1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-3]
map: a1 [ForkJoinPool.commonPool-worker-3]
forEach: A1 [ForkJoinPool.commonPool-worker-3]
forEach: C1 [ForkJoinPool.commonPool-worker-2]123456789101112131415
注: 所見(jiàn),并行流使用了所有的ForkJoinPool中的可用線(xiàn)程來(lái)執(zhí)行流式操作。在持續(xù)的運(yùn)行中,輸出結(jié)果可能有所不同,因?yàn)樗褂玫奶囟ň€(xiàn)程是非特定的。
讓我們通過(guò)添加中間操作sort來(lái)擴(kuò)展上面示例:
Arrays.asList("a1", "a2", "b1", "c2", "c1") .parallelStream() .filter(s -> { System.out.format("filter: %s [%s]\n", s, Thread.currentThread().getName()); return true; }) .map(s -> { System.out.format("map: %s [%s]\n", s, Thread.currentThread().getName()); return s.toUpperCase(); }) .sorted((s1, s2) -> { System.out.format("sort: %s <> %s [%s]\n", s1, s2, Thread.currentThread().getName()); return s1.compareTo(s2); }) .forEach(s -> System.out.format("forEach: %s [%s]\n", s, Thread.currentThread().getName())); 12345678910111213141516171819
運(yùn)行代碼,輸出結(jié)果看上去有些奇怪:
filter: c2 [ForkJoinPool.commonPool-worker-3]
filter: c1 [ForkJoinPool.commonPool-worker-2]
map: c1 [ForkJoinPool.commonPool-worker-2]
filter: a2 [ForkJoinPool.commonPool-worker-1]
map: a2 [ForkJoinPool.commonPool-worker-1]
filter: b1 [main]
map: b1 [main]
filter: a1 [ForkJoinPool.commonPool-worker-2]
map: a1 [ForkJoinPool.commonPool-worker-2]
map: c2 [ForkJoinPool.commonPool-worker-3]
sort: A2 <> A1 [main]
sort: B1 <> A2 [main]
sort: C2 <> B1 [main]
sort: C1 <> C2 [main]
sort: C1 <> B1 [main]
sort: C1 <> C2 [main]
forEach: A1 [ForkJoinPool.commonPool-worker-1]
forEach: C2 [ForkJoinPool.commonPool-worker-3]
forEach: B1 [main]
forEach: A2 [ForkJoinPool.commonPool-worker-2]
forEach: C1 [ForkJoinPool.commonPool-worker-1]123456789101112131415161718192021
貌似sort只在主線(xiàn)程上串行執(zhí)行。但是實(shí)際上,并行流中的sort在底層使用了Java8中新的方法Arrays.parallelSort()。如 javadoc官方文檔解釋的,這個(gè)方法會(huì)按照數(shù)據(jù)長(zhǎng)度來(lái)決定以串行方式,或者以并行的方式來(lái)執(zhí)行。
如果指定數(shù)據(jù)的長(zhǎng)度小于最小數(shù)值,它則使用相應(yīng)的Arrays.sort方法來(lái)進(jìn)行排序。
回到上小節(jié) reduce的例子。我們已經(jīng)發(fā)現(xiàn)了組合器函數(shù)只在并行流中調(diào)用,而不不會(huì)在串行流中被調(diào)用。
讓我們來(lái)實(shí)際觀察一下涉及到哪個(gè)線(xiàn)程:
List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); persons .parallelStream() .reduce(0, (sum, p) -> { System.out.format("accumulator: sum=%s; person=%s [%s]\n", sum, p, Thread.currentThread().getName()); return sum += p.age; }, (sum1, sum2) -> { System.out.format("combiner: sum1=%s; sum2=%s [%s]\n", sum1, sum2, Thread.currentThread().getName()); return sum1 + sum2; }); 12345678910111213141516171819
通過(guò)控制臺(tái)日志輸出,累加器和組合器均在所有可用的線(xiàn)程上并行執(zhí)行:
accumulator: sum=0; person=Pamela; [main]
accumulator: sum=0; person=Max; [ForkJoinPool.commonPool-worker-3]
accumulator: sum=0; person=David; [ForkJoinPool.commonPool-worker-2]
accumulator: sum=0; person=Peter; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=18; sum2=23; [ForkJoinPool.commonPool-worker-1]
combiner: sum1=23; sum2=12; [ForkJoinPool.commonPool-worker-2]
combiner: sum1=41; sum2=35; [ForkJoinPool.commonPool-worker-2]1234567
總之,你需要記住的是,并行流對(duì)含有大量元素的數(shù)據(jù)流提升性能極大。但是你也需要記住并行流的一些操作,例如reduce和collect操作,需要額外的計(jì)算(如組合操作),這在串行執(zhí)行時(shí)是并不需要。
此外,我們也了解了,所有并行流操作都共享相同的 JVM 相關(guān)的公共ForkJoinPool。所以你可能需要避免寫(xiě)出一些又慢又卡的流式操作,這很有可能會(huì)拖慢你應(yīng)用中,嚴(yán)重依賴(lài)并行流的其它部分代碼的性能。
以上就是Java8新特性Stream流詳解的詳細(xì)內(nèi)容,更多關(guān)于Java8 Stream流的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java中SpringBoot的@Transactional原理
這篇文章主要介紹了Java中SpringBoot的@Transactional原理,面向元數(shù)據(jù)遍歷已經(jīng)成為越來(lái)越多開(kāi)發(fā)者的偏好,因此原理從Springboot的EnableTransactionManagement注解說(shuō)起,需要的朋友可以參考下2023-07-07SpringBoot如何讀取xml配置bean(@ImportResource)
這篇文章主要介紹了SpringBoot如何讀取xml配置bean(@ImportResource),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01@CacheEvict 清除多個(gè)key的實(shí)現(xiàn)方式
這篇文章主要介紹了@CacheEvict 清除多個(gè)key的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02SpringMVC之簡(jiǎn)單的增刪改查示例(SSM整合)
本篇文章主要介紹了SpringMVC之簡(jiǎn)單的增刪改查示例(SSM整合),這個(gè)例子是基于SpringMVC+Spring+Mybatis實(shí)現(xiàn)的。有興趣的可以了解一下。2017-03-03自定義的Troop<T>泛型類(lèi)( c++, java和c#)的實(shí)現(xiàn)代碼
這篇文章主要介紹了自定義的Troop<T>泛型類(lèi)( c++, java和c#)的實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-05-05Spring中的AutowireCandidateResolver的具體使用詳解
這篇文章主要介紹了Spring中的AutowireCandidateResolver的具體使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04springboot2.x整合redis知識(shí)點(diǎn)講解
在本篇文章中小編給大家分享的是一篇關(guān)于springboot2.x整合redis知識(shí)點(diǎn)內(nèi)容,有興趣的朋友們可以學(xué)習(xí)下。2020-01-01