什么是 Stream
關(guān)于 Stream(流),官方文檔給出的描述是:Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections.
翻譯成中文也就是說:流是一個(gè)用于支持在元素流上進(jìn)行函數(shù)式操作的類祭芦,例如集合上的map-reduce轉(zhuǎn)換。它可以十分方便高效地實(shí)現(xiàn)聚合操作或大批量數(shù)據(jù)處理勇边,且代碼十分簡潔。比如在一個(gè)彩色筆的集合中,求出紅筆的重量總和络它,可以這么寫:
Pen redPen1 = new Pen("red", 10);
Pen redPen2 = new Pen("red", 15);
Pen redPen3 = new Pen("red", 13);
Pen yellowPen1 = new Pen("yellow", 10);
Pen yellowPen2 = new Pen("yellow", 16);
List<Pen> pens = Arrays.asList(redPen1, redPen2, redPen3, yellowPen1, yellowPen2);
int sum = pens.stream()
.filter(p -> "red".equals(p.getColor()))
.mapToInt(p -> p.getWeight())
.sum();
System.out.println("sum: " + sum);
輸出結(jié)果為:
sum: 38
可以看到咖城,我們只用一句代碼就實(shí)現(xiàn)了在一個(gè)集合中求取符合某個(gè)條件的數(shù)值總和茬腿。如果我們不使用 Stream 來實(shí)現(xiàn)呼奢,這將需要用大篇幅的代碼來編寫。
簡單來說切平,流就是一個(gè)來自數(shù)據(jù)源的元素隊(duì)列握础,能夠?qū)现械拿總€(gè)元素進(jìn)行一系列并行或串行的流水線操作。
- 數(shù)據(jù)源:即流的來源悴品, 如集合禀综、數(shù)組等。如上面的示例中的 pens 集合苔严。
- 元素隊(duì)列:元素是特定類型的對象定枷,形成一個(gè)隊(duì)列。 值得注意的是邦蜜,Java中的 Stream 不是數(shù)據(jù)結(jié)構(gòu)依鸥,不會存儲元素,它只與計(jì)算相關(guān)悼沈。如上面的示例中使用了 stream() 方法將 pens 轉(zhuǎn)換成一個(gè)串行流贱迟。
- 聚合操作:類似SQL語句一樣的操作, 如 filter絮供、map衣吠、reduce、find壤靶、match缚俏、sorted等。如上面的示例中使用了 filter() 過濾取出顏色為 red 的筆贮乳,而后使用 mapToInt() 映射取出紅筆的 weight忧换,最后使用 sum() 求出紅筆重量總和。
流的種類有:Stream向拆、LongStream亚茬、IntStream、DoubleStream浓恳。每種流都可以選擇串行或并行刹缝。默認(rèn)是串行。
Stream 的結(jié)構(gòu)組成
流操作分為中間操作(Intermediate)和終端操作(Terminal)颈将,并組合成流管道(stream pipelines)梢夯。其構(gòu)成如下圖所示:
中間操作:中間操作都是惰性化的,在執(zhí)行終端操作之前晴圾,調(diào)用的中間操作都不會真正執(zhí)行颂砸,而是返回一個(gè)新的流,一直到終端操作被調(diào)用雁歌。中間操作還可分為有狀態(tài)(如 distinct()葬毫、sort())和無狀態(tài)(如 map()、filter())闯捎。前者在執(zhí)行過程中會保留先前看到的元素狀態(tài)撒蟀,而后者不會,且每個(gè)元素都可以獨(dú)立于其他元素的操作進(jìn)行處理温鸽。
終端操作:終端操作會產(chǎn)生一個(gè)結(jié)果或副作用保屯。它總是饑餓的,會在返回之前涤垫,完成數(shù)據(jù)的遍歷和處理(只有 iterator() 和 spliterator() 不是)姑尺。終端操作完成之后,流即失效蝠猬,不能再使用切蟋。
獲取 Stream 的方式
- 從集合中獲取。如 Collection.stream()(串行流)榆芦、Collection.parallelStream()(并行流)柄粹。
- 從數(shù)組中獲取。如 Array.Stream(Object[]) 匆绣。
- 從靜態(tài)工廠方法中獲取驻右。如 Stream.of(Object[])、IntStream.range(int, int)崎淳、Stream.iterate(Object, UnaryOperator) 堪夭。
- 從文件中獲取流。如 BufferedReader.lines() 拣凹。
- 其他方式森爽,包括Random.ints()、BitSet.stream()嚣镜、Pattern.splitAsStream(java.lang.CharSequence)爬迟、JarFile.stream()。
Stream 和集合的區(qū)別
- 流不是數(shù)據(jù)結(jié)構(gòu)祈惶。它只與計(jì)算相關(guān)雕旨,且按需計(jì)算,不存儲任何數(shù)據(jù)捧请。什么叫只與計(jì)算相關(guān)凡涩?用聽音樂打個(gè)比方,音樂存儲在硬盤上疹蛉,需要時(shí)本地播放活箕,這是集合;音樂存放在網(wǎng)絡(luò)上可款,需要時(shí)從網(wǎng)絡(luò)(數(shù)據(jù)源)獲取育韩,播放的音樂仍存放在原來的地方克蚂,這便是流。
- 功能性筋讨。流不會改變數(shù)據(jù)源埃叭。
- 惰性化。流的操作都是向后延遲的悉罕,當(dāng)調(diào)用中間操作時(shí)赤屋,它并不會真正執(zhí)行,而是等到終端操作被調(diào)用時(shí)壁袄,再合并一次性執(zhí)行类早。關(guān)于中間操作和終端操作,后面會詳細(xì)說明嗜逻。
- 流可以無限大涩僻。短回路操作(如 limit()、findFirst())允許一個(gè)無窮大的流在有限的時(shí)間內(nèi)返回計(jì)算結(jié)果栈顷。比如執(zhí)行 limit(10) 逆日,流在獲取前 10 個(gè)元素后即返回,不再對后面的元素執(zhí)行任何操作妨蛹。
- 只能迭代一次屏富。流和 Iterator 相似,都是只能迭代一次蛙卤,必須重新生成流才能再次訪問數(shù)據(jù)源狠半。
一些需要注意的問題
- 流的參數(shù)一般是 lambda 表達(dá)式或方法引用。
- 不應(yīng)在流的執(zhí)行期間修改流的數(shù)據(jù)源颤难。
- 盡量避免使用有狀態(tài)的 lambda 表達(dá)式神年。
- 如果數(shù)據(jù)源不是有序的,則流也不保證有序性行嗤∫讶眨可通過 unordered() 方法,聲時(shí)可以無序栅屏。
- 慎重使用并行流飘千。比如當(dāng)需要保證有有序性時(shí),使用并行流將可能破壞有序性栈雳。
如何使用 Stream
- filter:返回與給定謂詞相匹配的元素
仍以開篇中的基礎(chǔ)數(shù)據(jù)做示例护奈,返回重量大于 10 的筆并打印。
pens.stream().filter(pen -> pen.getWeight() > 10).forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='yellow', weight=16}
可以看到哥纫,2 個(gè) weight = 10 的元素已經(jīng)被過濾掉了霉旗。
- distinct:去除重復(fù)元素
需要注意是,distinct() 方法并不支持傳遞參數(shù),因此使用時(shí)需要重寫 equals() 和 hasCode() 方法厌秒。為了方便測試读拆,這里我寫的 equals() 方法并不判斷顏色是否相同。即只要 weight 相等鸵闪,equals() 就會返回 true檐晕。
pens.stream().distinct().forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=10}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='yellow', weight=16}
可以看到,redPen1 和 yellowPen1 的 weight 都是 10蚌讼,因此有一個(gè)被過濾掉了棉姐。但是 Stream 是如何確定要保留哪個(gè)元素呢?事實(shí)上啦逆,在順序流中,distinct() 會保留重復(fù)元素中第一個(gè)出現(xiàn)的元素(parallelStream 也是如此)笛洛,但如果流是無序的(如使用 unordered() 指明流是無序的夏志,不需要保證穩(wěn)定性),則返回結(jié)果也是不穩(wěn)定的苛让。
pens.stream().unordered().distinct().forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='yellow', weight=16}
Pen{color='red', weight=13}
Pen{color='yellow', weight=10}
Pen{color='red', weight=15}
可以看到沟蔑,此時(shí)保留的 weight = 10 的元素是 yellowPen1。我試了幾次狱杰,有的時(shí)候也會返回 redPen1瘦材。
- skip:跳過前 n 個(gè)元素。
pens.stream().skip(2).forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=13}
Pen{color='yellow', weight=10}
Pen{color='yellow', weight=16}
可以看到仿畸,返回的結(jié)果是跳過了前 2 個(gè)元素食棕。此時(shí)如果是使用 parallelStream() 并行執(zhí)行,一樣會跳過前 2 個(gè)元素错沽,但不能保證返回的結(jié)果的不穩(wěn)定性簿晓,即每次執(zhí)行,元素的順序都有可能不同千埃。
- limit:返回前 n 個(gè)元素憔儿。
pens.stream().limit(3).forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=10}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
可以看到,返回的結(jié)果是 list 的前 3 個(gè)元素放可。當(dāng)然谒臼,limit 還可以和 skip 一起使用,返回從第 n 個(gè)元素開始耀里,取 m 個(gè)元素蜈缤。
pens.stream().skip(1).limit(2).forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=15}
Pen{color='red', weight=13}
可以看到,此時(shí)返回的恰好是第 2 個(gè)元素到第 3 個(gè)元素备韧。
- anyMatch:Stream 中有任意一個(gè)元素與給定的謂語相匹配劫樟,返回 true。
boolean isMatch = pens.stream().anyMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch);
結(jié)果如下:
true
- allMatch:Stream 中的所有元素都與給定的謂語相匹配,返回 true叠艳。
boolean isMatch1 = pens.stream().allMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch1);
boolean isMatch2 = pens.stream().allMatch(pen -> pen.getWeight() > 0);
System.out.println(isMatch2);
結(jié)果如下:
false
true
- noneMatch:Stream 中的所有元素都與給定的謂語不匹配奶陈,返回 true。
boolean isMatch1 = pens.stream().noneMatch(pen -> pen.getWeight() > 10);
boolean isMatch2 = pens.stream().noneMatch(pen -> pen.getWeight() > 20);
System.out.println(isMatch1);
System.out.println(isMatch2);
結(jié)果如下:
false
true
- sort:排序
pens.stream().sorted(Comparator.comparing(Pen::getWeight)).forEach(pen -> System.out.println(pen));
System.out.println("===================");
pens.stream().sorted(Comparator.comparing(Pen::getWeight).reversed()).forEach(pen -> System.out.println(pen));
結(jié)果如下:
Pen{color='red', weight=10}
Pen{color='yellow', weight=10}
Pen{color='red', weight=13}
Pen{color='red', weight=15}
Pen{color='yellow', weight=16}
===================
Pen{color='yellow', weight=16}
Pen{color='red', weight=15}
Pen{color='red', weight=13}
Pen{color='red', weight=10}
Pen{color='yellow', weight=10}
sorted() 方法默認(rèn)是自然排序附较,即從小到大吃粒。但是可以使用 reversed() 反轉(zhuǎn)排序。
- map:通過給定的函數(shù)拒课,將輸入流中的元素映射到輸出流并返回徐勃。
還是以 原來的 list 做為示例,利用 map() 將 list 中的 weight 映射成一個(gè)流早像,再將其轉(zhuǎn)換成 String 類型僻肖,并拼接成一個(gè)字符串。
String s = pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).collect(Collectors.joining(" "));
System.out.println(s);
結(jié)果如下:
10 15 13 10 16
- flatMap:通過映射函數(shù)卢鹦,作用到流中的每個(gè)元素臀脏,并組成返回成一個(gè)新的流。
示例中通過 map() 將 list 中的 weight 映射成一個(gè)輸出流后冀自,將 weight 轉(zhuǎn)換成 String 類型揉稚,切割字符串并打印。這里用了 2 種方法熬粗,一種是利用 map() 進(jìn)行字符串切割后即返回搀玖;另一種是在切割后使用 flatMap() 映射多一次再打印。我們來看看二者有什么區(qū)別驻呐。
pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).map(word -> word.split(" ")).forEach(System.out::println);
System.out.println("==============");
pens.stream().map(Pen::getWeight).map(item -> String.valueOf(item)).map(word -> word.split(" ")).flatMap(Arrays::stream).forEach(System.out::println);
結(jié)果如下:
[Ljava.lang.String;@4dd8dc3
[Ljava.lang.String;@6d03e736
[Ljava.lang.String;@568db2f2
[Ljava.lang.String;@378bf509
[Ljava.lang.String;@5fd0d5ae
==============
10
15
13
10
16
可以看到灌诅,沒有用 flatMap() 做多一次映射的,打印出只是一個(gè)地址暴氏。我們都知道延塑,split() 方法返回的是一個(gè) String[],我們直接去打印自然只能得到一個(gè)地址答渔。而 flatMap() 可以將流中的內(nèi)容返回关带,而不是返回一個(gè)流。
- collect:將流還原成集合沼撕。
// List -> Stream -> List
List<Integer> penWeightList = pens.stream().map(Pen::getWeight).collect(Collectors.toList());
// List -> Stream -> Set
HashSet<Integer> penWeightSet = pens.stream().map(Pen::getWeight).collect(toCollection(HashSet::new));
// List -> Stream -> Double(計(jì)算平均值)
Double averagWeight = pens.stream().collect(averagingInt(Pen::getWeight));
penWeightList.forEach(System.out::println);
System.out.println("===========================");
penWeightSet.forEach(System.out::println);
System.out.println("===========================");
System.out.println(averagWeight);
結(jié)果如下:
10
15
13
10
16
===========================
16
10
13
15
===========================
12.8
- reduce:匯聚操作宋雏,根據(jù)給定的累加器,將 Stream 中的元素一個(gè)個(gè)累加計(jì)算务豺。進(jìn)行什么操作與累加器相關(guān)磨总,如相乘、相加笼沥、比較大小等蚪燕。
// 一個(gè)參數(shù)(累加器)
Optional sum = pens.stream().map(Pen::getWeight).reduce((a, b) -> a + b);
System.out.println("sum: " + sum.get());
Optional max = pens.stream().map(Pen::getWeight).reduce((a, b) -> a > b ? a : b);
System.out.println("max: " + max.get());
// 兩個(gè)參數(shù)(初始值娶牌,累加器)
int sum2 = pens.stream().map(Pen::getWeight).reduce(10, (a, b) -> a + b);
System.out.println("sum2: " + sum2);
// 三個(gè)參數(shù)(初始值,累加器馆纳, 組合器)诗良,第三個(gè)參數(shù)只在并行時(shí)生效
int sum3 = pens.stream().map(Pen::getWeight).reduce(10, (a, b) -> a + b, (a, b) -> a + b);
System.out.println("sum3: " + sum3);
int sum4 = pens.parallelStream().map(Pen::getWeight).reduce(10, (a, b) -> a + b, (a, b) -> a + b);
System.out.println("sum4: " + sum4);
結(jié)果如下:
sum: 64
max: 16
sum2: 74
sum3: 74
sum4: 114
可以看到,sum 和 max 分別計(jì)算出了 weight 的總和以及 weight 中的最大值鲁驶。sum2 和 sum3 相等鉴裹。這是因?yàn)樵诖辛髦校瑀educe() 的第三個(gè)參數(shù)是不起作用的钥弯,而在并行流中径荔,reduce() 的第三個(gè)參數(shù)會將各線程的計(jì)算結(jié)果組合起來。
在開篇的代碼中脆霎,給定的 weight 有 10总处、15、13睛蛛、10辨泳、16。在 reduce() 方法中我又給了初始值 10玖院,因此在串行流中的計(jì)算應(yīng)是 10 + 10 = 20,20 + 15 = 35第岖,35 + 13 = 48难菌,48 + 10 = 58,58 + 16 = 74蔑滓。但是當(dāng)有第三個(gè)參數(shù)且流并行執(zhí)行時(shí)郊酒,它是這么計(jì)算執(zhí)行的:10 + 10 = 20,10 + 15 = 25键袱,10 + 13 = 23燎窘,10 + 10 = 20,10 + 16 = 26蹄咖;20 + 25 + 23 + 20 + 26 = 114褐健。
在前面我們已經(jīng)學(xué)習(xí)到,流的一個(gè)特性就是向后延遲澜汤,在執(zhí)行最終的操作之前都不會進(jìn)行真正的計(jì)算蚜迅,因此執(zhí)行地,線程間互不影響俊抵,都是拿初始值進(jìn)行加法運(yùn)算谁不,最后由組合器(第三個(gè)參數(shù))組合返回。
- findFrist:返回第一個(gè)元素
Optional firstEmelment = pens.stream().findFirst();
System.out.println(firstEmelment.get());
結(jié)果如下:
Pen{color='red', weight=10}
- findAny:返回任意一個(gè)元素(串行流返回第一個(gè)元素)徽诲,如果是空流刹帕,則返回empty Optional吵血。
Optional optional = pens.stream().map(pen -> pen.getWeight()).findAny();
System.out.println(optional.get());
Optional optional1 = pens.parallelStream().map(pen -> pen.getWeight()).findAny();
System.out.println(optional1.get());
結(jié)果如下:
10
13
- max:返回流中的最大值
Optional maxWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(maxWeight.get());
結(jié)果如下:
Pen{color='yellow', weight=16}
- min:返回流中的最小值
Optional minWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(minWeight.get());
結(jié)果如下:
Pen{color='red', weight=10}
- count:返回流中元素的個(gè)數(shù)
long count = pens.stream().distinct().count();
System.out.println(count);
結(jié)果如下:
4
參考文章
使用Java 8 Stream像操作SQL一樣處理數(shù)據(jù)
Java 8 中的 Streams API 詳解
Java 8新特性:全新的Stream API
Stream 官方文檔