天天看點

Java8新特性——StreamAPI(二)

1. 收集器簡介

收集器用來将經過篩選、映射的流進行最後的整理,可以使得最後的結果以不同的形式展現。

collect方法即為收集器,它接收Collector接口的實作作為具體收集器的收集方法。

Collector接口提供了很多預設實作的方法,我們可以直接使用它們格式化流的結果;也可以自定義。Collector接口的實作,進而定制自己的收集器。

這裡先介紹Collector常用預設靜态方法的使用,自定義收集器會在下一篇博文中介紹。

2. 收集器的使用

2.1 歸約

流由一個個元素組成,歸約就是将一個個元素“折疊”成一個值,如求和、求最值、求平均值都是歸約操作。

2.1.1 計數

long count = list.stream()
                    .collect(Collectors.counting());      

也可以不使用收集器的計數函數:

long count = list.stream().count();      

注意:計數的結果一定是long類型。

2.1.2 最值

例:找出所有人中年齡最大的人。
Optional<Person> oldPerson = list.stream()
                    .collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));      

計算最值需要使用Collector.maxBy和Collector.minBy,這兩個函數需要傳入一個比較器Comparator.comparingInt,這個比較器又要接收需要比較的字段。

這個收集器将會傳回一個Optional類型的值。

Optional類簡介請移步至:​​Java8新特性——StreamAPI(一)​​。

2.1.3 求和

例:計算所有人的年齡總和。
int summing = list.stream()
                    .collect(Collectors.summingInt(Person::getAge));      

當然,既然Java8提供了summingInt,那麼還提供了summingLong、summingDouble。

2.1.4 求平均值例:

計算所有人的年齡平均值。
double avg = list.stream()
                    .collect(Collectors.averagingInt(Person::getAge));      

注意:計算平均值時,不論計算對象是int、long、double,計算結果一定都是double。

2.1.5 一次性計算所有歸約操作

Collectors.summarizingInt函數能一次性将最值、均值、總和、元素個數全部計算出來,并存儲在對象IntSummaryStatisics中。

可以通過該對象的getXXX()函數擷取這些值。

2.1.6 連接配接字元串

例:将所有人的名字連接配接成一個字元串。
String names = list.stream()
                        .collect(Collectors.joining());      

每個字元串預設分隔符為空格,若需要指定分隔符,則在joining中加入參數即可:

String names = list.stream()
                        .collect(Collectors.joining(", "));      

此時字元串之間的分隔符為逗号。

2.1.7 一般性的歸約操作

若你需要自定義一個歸約操作,那麼需要使用Collectors.reducing函數,該函數接收三個參數:

  • 第一個參數為歸約的初始值 。
  • 第二個參數為歸約操作進行的字段 。
  • 第三個參數為歸約操作的過程。
例:計算所有人的年齡總和。
Optional<Integer> sumAge = list.stream()
                                    .collect(Collectors.reducing(0,Person::getAge,(i,j)->i+j));      

上面例子中,reducing函數一共接收了三個參數:

  • 第一個參數表示歸約的初始值。我們需要累加,是以初始值為0。
  • 第二個參數表示需要進行歸約操作的字段。這裡我們對Person對象的age字段進行累加。
  • 第三個參數表示歸約的過程。這個參數接收一個Lambda表達式,而且這個Lambda表達式一定擁有兩個參數,分别表示目前相鄰的兩個元素。由于我們需要累加,是以我們隻需将相鄰的兩個元素加起來即可。

Collectors.reducing方法還提供了一個單參數的重載形式。

你隻需傳一個歸約的操作過程給該方法即可(即第三個參數),其他兩個參數均使用預設值。

  • 第一個參數預設為流的第一個元素。
  • 第二個參數預設為流的元素 這就意味着,目前流的元素類型為數值類型,并且是你要進行歸約的對象。
例:采用單參數的reducing計算所有人的年齡總和。
Optional<Integer> sumAge = list.stream()
            .filter(Person::getAge)
            .collect(Collectors.reducing((i,j)->i+j));      

2.2 分組

分組就是将流中的元素按照指定類别進行劃分,類似于SQL語句中的GROUPBY。

2.2.1 一級分組

例:将所有人分為老年人、中年人、青年人。
Map<String,List<Person>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    }));      

groupingby函數接收一個Lambda表達式,該表達式傳回String類型的字元串,groupingby會将目前流中的元素按照Lambda傳回的字元串進行分組。

分組結果是一個Map< String,List< Person>>,Map的鍵就是組名,Map的值就是該組的Perosn集合。

2.2.2 多級分組

多級分組可以支援在完成一次分組後,分别對每個小組再進行分組。

使用具有兩個參數的groupingby重載方法即可實作多級分組。

  • 第一個參數:一級分組的條件。
  • 第二個參數:一個新的groupingby函數,該函數包含二級分組的條件。
例:将所有人分為老年人、中年人、青年人,并且将每個小組再分成:男女兩組。
Map<String,Map<String,List<Person>>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    },
                                    groupingby(Person::getSex)));      

此時會傳回一個非常複雜的結果:Map< String,Map< String,List< Person>>>。

2.2.3 對分組進行統計

擁有兩個參數的groupingby函數不僅僅能夠實作多幾分組,還能對分組的結果進行統計。

例:統計每一組的人數。
Map<String,Long> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    },
                                    counting()));      

此時會傳回一個Map< String,Long>類型的map,該map的鍵為組名,map的值為該組的元素個數。

2.2.4将收集器的結果轉換成另一種類型

當使用maxBy、minBy統計最值時,結果會封裝在Optional中,該類型是為了避免流為空時計算的結果也為空的情況。在單獨使用maxBy、minBy函數時确實需要傳回Optional類型,這樣能確定沒有空指針異常。然而當我們使用groupingBy進行分組時,若一個組為空,則該組将不會被添加到Map中,進而Map中的所有值都不會是一個空集合。既然這樣,使用maxBy、minBy方法計算每一組的最值時,将結果封裝在optional對象中就顯得有些多餘。

我們可以使用collectingAndThen函數包裹maxBy、minBy,進而将maxBy、minBy傳回的Optional對象進行轉換。

例:将所有人按性别劃分,并計算每組最大的年齡。
Map<String,Integer> map = list.stream()
                            .collect(groupingBy(Person::getSex,
                            collectingAndThen(
                            maxBy(comparingInt(Person::getAge)),
                            Optional::get
                            )));