歸約、分組與分區,深刻講解JavaStream終結操做

本文爲掘金社區首發簽約文章,未獲受權禁止轉載。java

思惟導圖鎮樓,先感謝你們對我上一篇文的積極點贊,助我完成KPI😄。web

上一篇中給你們講了Stream的前半部分知識——包括對Stream的總體概覽及Stream的建立和Stream的轉換流操做,並對Stream一些內部優化點作了簡明的說明。編程

雖遲但到,今天就來繼續給你們更Stream第二部分知識——終結操做,因爲這部分的API內容繁多且複雜,因此我單開一篇給你們細細講講,個人文章很長,請你們忍耐一下。安全

正式開始以前,咱們先來講說聚合方法自己的特性(接下來我將用聚合方法代指終結操做中的方法):markdown

  1. 聚合方法表明着整個流計算的最終結果,因此它的返回值都不是Stream。多線程

  2. 聚合方法返回值可能爲空,好比filter沒有匹配到的狀況,JDK8中用Optional來規避NPE。併發

  3. 聚合方法都會調用evaluate方法,這是一個內部方法,看源碼的過程當中能夠用它來斷定一個方法是否是聚合方法。app

ok,知曉了聚合方法的特性,我爲了便於理解,又將聚合方法分爲幾大類:ide

其中簡單聚合方法我會簡單講解,其它則會着重講解,尤爲是收集器,它能作的實在太多了。。。svg

Stream的聚合方法是咱們在使用Stream中的必用操做,認真學習本篇,不說立刻就能對Stream駕輕就熟,起碼也能夠行雲流水吧😄

1. 簡單聚合方法

第一節嘛,先來點簡單的。

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了,因此它們都是具備短路效果的方法。

2. 歸約

2.1 reduce:反覆求值

第二節咱們來講說歸約,因爲這個詞過於抽象,我不得不找了一句通俗易懂的解釋來翻譯這句話,下面是歸約的定義:

將一個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,那麼你的最終和就會比你預想的結果要大。

2.2 max:利用歸約求最大

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的封裝。

2.3 min:利用歸約求最小

min仍是直接看例子吧:

Optional<Integer> max = List.of(1, 2, 3).stream()
                .min(Integer::compare);
複製代碼

它和max區別就是底層把 > 換成了 <,過於簡單,再也不贅述。

3. 收集器

第三節咱們來看看收集器,它的做用是對Stream中的元素進行收集而造成一個新的集合。

雖然我在本篇開頭的時候已經給過一張思惟導圖了,可是因爲收集器的API比較多因此我又畫了一張,算是對開頭那張的補充:

收集器的方法名是collect,它的方法定義以下:

<R, A> R collect(Collector<? super T, A, R> collector);
複製代碼

顧名思義,收集器是用來收集Stream的元素的,最後收集成什麼咱們能夠自定義,可是咱們通常不須要本身寫,由於JDK內置了一個Collector的實現類——Collectors。

3.1 收集方法

經過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() 具備兩個參數:

  1. 第一個參數表明key,它表示你要設置一個Map的key,我這裏指定的是元素中的orderNo。

  2. 第二個參數表明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是相同的。

3.2 分組方法

若是你想對數據進行分類,可是你指定的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(),這個方法能夠在並行時提升分組效率,可是它是不保證順序的,這裏就不展開講了。

3.3 分區方法

接下來我將介紹分組的另外一種狀況——分區,名字有點繞,但意思很簡單:

將數據按照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()));
複製代碼

3.4 經典復刻方法

終於來到最後一節了,請原諒我給這部分的方法起了一個這麼土的名字,可是這些方法確實如我所說:經典復刻。

換言之,就是Collectors把Stream原先的方法又實現了一遍,包括:

  1. mapmapping

  2. filterfiltering

  3. flatMapflatMapping

  4. countcounting

  5. reducereducing

  6. maxmaxBy

  7. **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類。

4. 總結

終於寫完了。

在這篇Stream中終結操做中,我提了Stream中的全部聚合方法,能夠說你看完了這篇,Stream的全部聚合操做就掌握個七七八八了,不會用不要緊,就知道有這個東西了就好了,否則在你的知識體系中Stream根本作不了XX事,就有點貽笑大方了。

固然,我仍是建議你們在項目中多多用用這些簡練的API,提高代碼可讀性,也更加簡練,被review的時候也容易讓別人眼前一亮~

看到這的掘友,但願高擡貴手幫我點個贊,爲個人掘金KPI大業出一份力,大家的支持就是我創做的不竭動力,咱們下期見。


參考書籍:

推薦文章:

相關文章
相關標籤/搜索