Java8 新特性之 Stream 使用指南

什么是 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)成如下圖所示:


流的構(gòu)成.png

中間操作:中間操作都是惰性化的,在執(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 的方式

  1. 從集合中獲取。如 Collection.stream()(串行流)榆芦、Collection.parallelStream()(并行流)柄粹。
  2. 從數(shù)組中獲取。如 Array.Stream(Object[]) 匆绣。
  3. 從靜態(tài)工廠方法中獲取驻右。如 Stream.of(Object[])、IntStream.range(int, int)崎淳、Stream.iterate(Object, UnaryOperator) 堪夭。
  4. 從文件中獲取流。如 BufferedReader.lines() 拣凹。
  5. 其他方式森爽,包括Random.ints()、BitSet.stream()嚣镜、Pattern.splitAsStream(java.lang.CharSequence)爬迟、JarFile.stream()。

Stream 和集合的區(qū)別

  1. 流不是數(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ù)源)獲取育韩,播放的音樂仍存放在原來的地方克蚂,這便是流。
  2. 功能性筋讨。流不會改變數(shù)據(jù)源埃叭。
  3. 惰性化。流的操作都是向后延遲的悉罕,當(dāng)調(diào)用中間操作時(shí)赤屋,它并不會真正執(zhí)行,而是等到終端操作被調(diào)用時(shí)壁袄,再合并一次性執(zhí)行类早。關(guān)于中間操作和終端操作,后面會詳細(xì)說明嗜逻。
  4. 流可以無限大涩僻。短回路操作(如 limit()、findFirst())允許一個(gè)無窮大的流在有限的時(shí)間內(nèi)返回計(jì)算結(jié)果栈顷。比如執(zhí)行 limit(10) 逆日,流在獲取前 10 個(gè)元素后即返回,不再對后面的元素執(zhí)行任何操作妨蛹。
  5. 只能迭代一次屏富。流和 Iterator 相似,都是只能迭代一次蛙卤,必須重新生成流才能再次訪問數(shù)據(jù)源狠半。

一些需要注意的問題

  1. 流的參數(shù)一般是 lambda 表達(dá)式或方法引用。
  2. 不應(yīng)在流的執(zhí)行期間修改流的數(shù)據(jù)源颤难。
  3. 盡量避免使用有狀態(tài)的 lambda 表達(dá)式神年。
  4. 如果數(shù)據(jù)源不是有序的,則流也不保證有序性行嗤∫讶眨可通過 unordered() 方法,聲時(shí)可以無序栅屏。
  5. 慎重使用并行流飘千。比如當(dāng)需要保證有有序性時(shí),使用并行流將可能破壞有序性栈雳。

如何使用 Stream

  1. 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)被過濾掉了霉旗。

  1. 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瘦材。

  1. 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í)行,元素的順序都有可能不同千埃。

  1. 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è)元素备韧。

  1. anyMatch:Stream 中有任意一個(gè)元素與給定的謂語相匹配劫樟,返回 true。
 boolean isMatch = pens.stream().anyMatch(pen -> pen.getWeight() == 10);
System.out.println(isMatch);

結(jié)果如下:

true
  1. 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
  1. 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
  1. 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)排序。

  1. 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
  1. 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è)流。

  1. 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
  1. 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ù))組合返回。

  1. findFrist:返回第一個(gè)元素
Optional firstEmelment = pens.stream().findFirst();
System.out.println(firstEmelment.get());

結(jié)果如下:

Pen{color='red', weight=10}
  1. 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
  1. max:返回流中的最大值
Optional maxWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(maxWeight.get());

結(jié)果如下:

Pen{color='yellow', weight=16}
  1. min:返回流中的最小值
 Optional minWeight = pens.stream().max(Comparator.comparing(Pen::getWeight));
System.out.println(minWeight.get());

結(jié)果如下:

Pen{color='red', weight=10}
  1. 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 官方文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市偷溺,隨后出現(xiàn)的幾起案子蹋辅,更是在濱河造成了極大的恐慌,老刑警劉巖亡蓉,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晕翠,死亡現(xiàn)場離奇詭異,居然都是意外死亡砍濒,警方通過查閱死者的電腦和手機(jī)淋肾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爸邢,“玉大人樊卓,你說我怎么就攤上這事「芎樱” “怎么了碌尔?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長券敌。 經(jīng)常有香客問我唾戚,道長,這世上最難降的妖魔是什么待诅? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任叹坦,我火速辦了婚禮,結(jié)果婚禮上卑雁,老公的妹妹穿的比我還像新娘募书。我一直安慰自己,他們只是感情好测蹲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布莹捡。 她就那樣靜靜地躺著,像睡著了一般扣甲。 火紅的嫁衣襯著肌膚如雪篮赢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天琉挖,我揣著相機(jī)與錄音荷逞,去河邊找鬼。 笑死粹排,一個(gè)胖子當(dāng)著我的面吹牛种远,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顽耳,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼坠敷,長吁一口氣:“原來是場噩夢啊……” “哼妙同!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起膝迎,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤粥帚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后限次,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芒涡,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年卖漫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了费尽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡羊始,死狀恐怖旱幼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情突委,我是刑警寧澤柏卤,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站匀油,受9級特大地震影響缘缚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜敌蚜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一忙灼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钝侠,春花似錦、人聲如沸酸舍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啃勉。三九已至忽舟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淮阐,已是汗流浹背叮阅。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留泣特,地道東北人浩姥。 一個(gè)月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像状您,于是被迫代替她去往敵國和親勒叠。 傳聞我的和親對象是個(gè)殘疾皇子兜挨,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評論 2 355

推薦閱讀更多精彩內(nèi)容