本文爲掘金社區首發簽約文章,未獲受權禁止轉載。java
思惟導圖鎮樓,先感謝你們對我上一篇文的積極點贊,助我完成KPI😄。web
上一篇中給你們講了Stream的前半部分知識——包括對Stream的總體概覽及Stream的建立和Stream的轉換流操做,並對Stream一些內部優化點作了簡明的說明。編程
雖遲但到,今天就來繼續給你們更Stream第二部分知識——終結操做,因爲這部分的API內容繁多且複雜,因此我單開一篇給你們細細講講,個人文章很長,請你們忍耐一下。安全
正式開始以前,咱們先來講說聚合方法自己的特性(接下來我將用聚合方法代指終結操做中的方法):markdown
聚合方法表明着整個流計算的最終結果,因此它的返回值都不是Stream。多線程
聚合方法返回值可能爲空,好比filter沒有匹配到的狀況,JDK8中用Optional來規避NPE。併發
聚合方法都會調用evaluate方法,這是一個內部方法,看源碼的過程當中能夠用它來斷定一個方法是否是聚合方法。app
ok,知曉了聚合方法的特性,我爲了便於理解,又將聚合方法分爲幾大類:ide
其中簡單聚合方法我會簡單講解,其它則會着重講解,尤爲是收集器,它能作的實在太多了。。。svg
Stream的聚合方法是咱們在使用Stream中的必用操做,認真學習本篇,不說立刻就能對Stream駕輕就熟,起碼也能夠行雲流水吧😄
第一節嘛,先來點簡單的。
Stream的聚合方法比上一篇講過的無狀態和有狀態方法都要多,可是其中也有一些是喵一眼就能學會的,第一節咱們先來講說這部分方法:
count()
:返回Stream中元素的size大小。
forEach()
:經過內部循環Stream中的全部元素,對每個元素進行消費,此方法沒有返回值。
forEachOrder()
:和上面方法的效果同樣,可是這個能夠保持消費順序,哪怕是在多線程環境下。
anyMatch(Predicate predicate)
:這是一個短路操做,經過傳入斷言參數判斷是否有元素可以匹配上斷言。
allMatch(Predicate predicate)
:這是一個短路操做,經過傳入斷言參數返回是否全部元素都能匹配上斷言。
noneMatch(Predicate predicate)
:這是一個短路操做,經過傳入斷言參數判斷是否全部元素都沒法匹配上斷言,若是是則返回true,反之則false。
findFirst()
:這是一個短路操做,返回Stream中的第一個元素,Stream可能爲空因此返回值用Optional處理。
findAny()
:這是一個短路操做,返回Stream中的任意一個元素,串型流中通常是第一個元素,Stream可能爲空因此返回值用Optional處理。
雖然以上都比較簡單,可是這裏面有五個涉及到短路操做的方法我仍是想提兩嘴:
首先是findFirst()
和findAny()
這兩個方法, 因爲它們只須要拿到一個元素就能方法就能結束,因此短路效果很好理解。
接着是anyMatch
方法,它只須要匹配到一個元素方法也能結束,因此它的短路效果也很好理解。
最後是allMatch
方法和noneMatch
,乍一看這兩個方法都是須要遍歷整個流中的全部元素的,其實否則,好比allMatch只要有一個元素不匹配斷言它就能夠返回false了,noneMatch只要有一個元素匹配上斷言它也能夠返回false了,因此它們都是具備短路效果的方法。
第二節咱們來講說歸約,因爲這個詞過於抽象,我不得不找了一句通俗易懂的解釋來翻譯這句話,下面是歸約的定義:
將一個Stream中的全部元素反覆結合起來,獲得一個結果,這樣的操做被稱爲歸約。
注:在函數式編程中,這叫作摺疊( fold )。
舉個很簡單的例子,我有一、二、3三個元素,我把它們倆倆相加,最後得出6這個數字,這個過程就是歸約。
再好比,我有一、二、3三個元素,我把它們倆倆比較,最後挑出最大的數字3或者挑出最小的數字1,這個過程也是歸約。
下面我舉一個求和的例子來演示歸約,歸約使用reduce方法:
Optional<Integer> reduce = List.of(1, 2, 3).stream()
.reduce((i1, i2) -> i1 + i2);
複製代碼
首先你可能注意到了,我在上文的小例子中一直在用倆倆這個詞,這表明歸約是倆倆的元素進行處理而後獲得一個最終值,因此reduce的方法的參數是一個二元表達式,它將兩個參數進行任意處理,最後獲得一個結果,其中它的參數和結果必須是同一類型。
好比代碼中的,i1和i2就是二元表達式的兩個參數,它們分別表明元素中的第一個元素和第二個元素,當第一次相加完成後,所得的結果會賦值到i1身上,i2則會繼續表明下一個元素,直至元素耗盡,獲得最終結果。
若是你以爲這麼寫不夠優雅,也可使用Integer中的默認方法:
Optional<Integer> reduce = List.of(1, 2, 3).stream()
.reduce(Integer::sum);
複製代碼
這也是一個以方法引用
表明lambda表達式的例子。
你可能還注意到了,它們的返回值是Optional的,這是預防Stream沒有元素的狀況。
你也能夠想辦法去掉這種狀況,那就是讓元素中至少要有一個值,這裏reduce提供一個重載方法給咱們:
Integer reduce = List.of(1, 2, 3).stream()
.reduce(0, (i1, i2) -> i1 + i2);
複製代碼
如上例,在二元表達式前面多加了一個參數,這個參數被稱爲初始值,這樣哪怕你的Stream沒有元素它最終也會返回一個0,這樣就不須要Optional了。
在實際方法運行中,初始值會在第一次執行中佔據i1的位置,i2則表明Stream中的第一個元素,而後所得的和再次佔據i1的位置,i2表明下一個元素。
不過使用初始值不是沒有成本的,它應該符合一個原則:accumulator.apply(identity, i1) == i1
,也就是說在第一次執行的時候,它的返回結果都應該是你Stream中的第一個元素。
好比我上面的例子是一個相加操做,則第一次相加時就是0 + 1 = 1
,符合上面的原則,做此原則是爲了保證並行流狀況下可以獲得正確的結果。
若是你的初始值是1,則在併發狀況下每一個線程的初始化都是1,那麼你的最終和就會比你預想的結果要大。
max方法也是一個歸約方法,它是直接調用了reduce方法。
先來看一個示例:
Optional<Integer> max = List.of(1, 2, 3).stream()
.max((a, b) -> {
if (a > b) {
return 1;
} else {
return -1;
}
});
複製代碼
沒錯,這就是max方法用法,這讓我以爲我不是在使用函數式接口,固然你也可使用Integer的方法進行簡化:
Optional<Integer> max = List.of(1, 2, 3).stream()
.max(Integer::compare);
複製代碼
哪怕如此,這個方法依舊讓我感受到很繁瑣,我雖然能夠理解在max方法裏面傳參數是爲了讓咱們本身自定義排序規則,但我不理解爲何沒有一個默認按照天然排序進行排序的方法,而是非要讓我傳參數。
直到後來我想到了基礎類型Stream,果真,它們裏面是能夠無需傳參直接拿到最大值:
OptionalLong max = LongStream.of(1, 2, 3).max();
複製代碼
果真,我能想到的,類庫設計者都想到了~
注 :OptionalLong是Optional對基礎類型long的封裝。
min仍是直接看例子吧:
Optional<Integer> max = List.of(1, 2, 3).stream()
.min(Integer::compare);
複製代碼
它和max區別就是底層把 >
換成了 <
,過於簡單,再也不贅述。
第三節咱們來看看收集器,它的做用是對Stream中的元素進行收集而造成一個新的集合。
雖然我在本篇開頭的時候已經給過一張思惟導圖了,可是因爲收集器的API比較多因此我又畫了一張,算是對開頭那張的補充:
收集器的方法名是collect,它的方法定義以下:
<R, A> R collect(Collector<? super T, A, R> collector);
複製代碼
顧名思義,收集器是用來收集Stream的元素的,最後收集成什麼咱們能夠自定義,可是咱們通常不須要本身寫,由於JDK內置了一個Collector的實現類——Collectors。
經過Collectors咱們能夠利用它的內置方法很方便的進行數據收集:
好比你想把元素收集成集合,那麼你可使用toCollection或者toList方法,不過咱們通常不使用toCollection,由於它須要傳參數,沒人喜歡傳參數。
你也可使用toUnmodifiableList,它和toList區別就是它返回的集合不能夠改變元素,好比刪除或者新增。
再好比你要把元素去重以後收集起來,那麼你可使用toSet或者toUnmodifiableSet。
接下來放一個比較簡單的例子:
// toList
List.of(1, 2, 3).stream().collect(Collectors.toList());
// toUnmodifiableList
List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableList());
// toSet
List.of(1, 2, 3).stream().collect(Collectors.toSet());
// toUnmodifiableSet
List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableSet());
複製代碼
以上這些方法都沒有參數,拿來即用,toList底層也是經典的ArrayList,toSet 底層則是經典的HashSet。
也許有時候你也許想要一個收集成一個Map,好比經過將訂單數據轉成一個訂單號對應一個訂單,那麼你可使用toMap():
List<Order> orders = List.of(new Order(), new Order());
Map<String, Order> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, order -> order));
複製代碼
toMap() 具備兩個參數:
第一個參數表明key,它表示你要設置一個Map的key,我這裏指定的是元素中的orderNo。
第二個參數表明value,它表示你要設置一個Map的value,我這裏直接把元素自己看成值,因此結果是一個Map<String, Order>。
你也能夠將元素的屬性看成值:
List<Order> orders = List.of(new Order(), new Order());
Map<String, List<Item>> map = orders.stream()
.collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));
複製代碼
這樣返回的就是一個訂單號+商品列表的Map了。
toMap() 還有兩個伴生方法:
toUnmodifiableMap():返回一個不可修改的Map。
toConcurrentMap():返回一個線程安全的Map。
這兩個方法和toMap() 的參數如出一轍,惟一不一樣的就是底層生成的Map特性不太同樣,咱們通常使用簡簡單單的toMap() 就夠了,它的底層是咱們最經常使用的HashMap() 實現。
toMap() 功能雖然強大也很經常使用,可是它卻有一個致命缺點。
咱們知道HahsMap遇到相同的key會進行覆蓋操做,可是toMap() 方法生成Map時若是你指定的key出現了重複,那麼它會直接拋出異常。
好比上面的訂單例子中,咱們假設兩個訂單的訂單號同樣,可是你又將訂單號指定了爲key,那麼該方法會直接拋出一個IllegalStateException,由於它不容許元素中的key是相同的。
若是你想對數據進行分類,可是你指定的key是能夠重複的,那麼你應該使用groupingBy 而不是toMap。
舉個簡單的例子,我想對一個訂單集合以訂單類型進行分組,那麼能夠這樣:
List<Order> orders = List.of(new Order(), new Order());
Map<Integer, List<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType));
複製代碼
直接指定用於分組的元素屬性,它就會自動按照此屬性進行分組,並將分組的結果收集爲一個List。
List<Order> orders = List.of(new Order(), new Order());
Map<Integer, Set<Order>> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType, toSet()));
複製代碼
groupingBy還提供了一個重載,讓你能夠自定義收集器類型,因此它的第二個參數是一個Collector收集器對象。
對於Collector類型,咱們通常仍是使用Collectors類,這裏因爲咱們前面已經使用了Collectors,因此這裏沒必要聲明直接傳入一個toSet()方法,表明咱們將分組後的元素收集爲Set。
groupingBy還有一個類似的方法叫作groupingByConcurrent(),這個方法能夠在並行時提升分組效率,可是它是不保證順序的,這裏就不展開講了。
接下來我將介紹分組的另外一種狀況——分區,名字有點繞,但意思很簡單:
將數據按照TRUE或者FALSE進行分組就叫作分區。
舉個例子,咱們將一個訂單集合按照是否支付進行分組,這就是分區:
List<Order> orders = List.of(new Order(), new Order());
Map<Boolean, List<Order>> collect = orders.stream()
.collect(Collectors.partitioningBy(Order::getIsPaid));
複製代碼
由於訂單是否支付只具備兩種狀態:已支付和未支付,這種分組方式咱們就叫作分區。
和groupingBy同樣,它還具備一個重載方法,用來自定義收集器類型:
List<Order> orders = List.of(new Order(), new Order());
Map<Boolean, Set<Order>> collect = orders.stream()
.collect(Collectors.partitioningBy(Order::getIsPaid, toSet()));
複製代碼
終於來到最後一節了,請原諒我給這部分的方法起了一個這麼土的名字,可是這些方法確實如我所說:經典復刻。
換言之,就是Collectors把Stream原先的方法又實現了一遍,包括:
map → mapping
filter → filtering
flatMap → flatMapping
count → counting
reduce → reducing
max → maxBy
**min ** → minBy
這些方法的功能我就不一一列舉了,以前的文章已經講的很詳盡了,惟一的不一樣是某些方法多了一個參數,這個參數就是咱們在分組和分區裏面講過的收集參數,你能夠指定收集到什麼容器內。
我把它們抽出來主要想說的爲何要復刻這麼多方法處理,這裏我說說我的看法,不表明官方意見。
我以爲主要是爲了功能的組合。
什麼意思呢?比方說我又有一個需求:使用訂單類型對訂單進行分組,並找出每組有多少個訂單。
訂單分組咱們已經講過了,找到其每組有多少訂單隻要拿到對應list的size就好了,可是咱們能夠不這麼麻煩,而是一步到位,在輸出結果的時候鍵值對就是訂單類型和訂單數量:
Map<Integer, Long> collect = orders.stream()
.collect(Collectors.groupingBy(Order::getOrderType, counting()));
複製代碼
就這樣,就這麼簡單,就行了,這裏等於說咱們對分組後的數據又進行了一次計數操做。
上面的這個例子可能不對明顯,當咱們須要對最後收集以後的數據在進行操做時,通常咱們須要從新將其轉換成Stream而後操做,可是使用Collectors的這些方法就可讓你很方便的在Collectors中進行數據的處理。
再舉個例子,仍是經過訂單類型對訂單進行分組,可是呢,咱們想要拿到每種類型訂單金額最大的那個,那麼咱們就能夠這樣:
List<Order> orders = List.of(new Order(), new Order());
Map<Integer, Optional<Order>> collect2 = orders.stream()
.collect(groupingBy(Order::getOrderType,
maxBy(Comparator.comparing(Order::getMoney))));
複製代碼
更簡潔,也更方便,不須要咱們分組完以後再去一一尋找最大值了,能夠一步到位。
再來一個分組以後,求各組訂單金額以後的:
List<Order> orders = List.of(new Order(), new Order());
Map<Integer, Long> collect = orders.stream()
.collect(groupingBy(Order::getOrderType, summingLong(Order::getMoney)));
複製代碼
不過summingLong這裏咱們沒有講,它就是一個內置的請和操做,支持Integer、Long和Double。
還有一個相似的方法叫作averagingLong看名字就知道,求平均的,都比較簡單,建議你們沒事的時候能夠掃兩眼。
該結束了,最後一個方法joining(),用來拼接字符串很實用:
List<Order> orders = List.of(new Order(), new Order());
String collect = orders.stream()
.map(Order::getOrderNo).collect(Collectors.joining(","));
複製代碼
這個方法的方法名看着有點眼熟,沒錯,String類在JDK8以後新加了一個join()
方法,也是用來拼接字符串的,Collectors的joining不過和它功能同樣,底層實現也同樣,都用了StringJoiner類。
終於寫完了。
在這篇Stream中終結操做中,我提了Stream中的全部聚合方法,能夠說你看完了這篇,Stream的全部聚合操做就掌握個七七八八了,不會用不要緊,就知道有這個東西了就好了,否則在你的知識體系中Stream根本作不了XX事,就有點貽笑大方了。
固然,我仍是建議你們在項目中多多用用這些簡練的API,提高代碼可讀性,也更加簡練,被review的時候也容易讓別人眼前一亮~
看到這的掘友,但願高擡貴手幫我點個贊,爲個人掘金KPI大業出一份力,大家的支持就是我創做的不竭動力,咱們下期見。
參考書籍:
推薦文章: