《Java 8 in Action》Chapter 6:用流收集數據

1. 收集器簡介

collect() 接收一個類型爲 Collector 的參數,這個參數決定了如何把流中的元素聚合到其它數據結構中。Collectors 類包含了大量經常使用收集器的工廠方法,toList() 和 toSet() 就是其中最多見的兩個,除了它們還有不少收集器,用來對數據進行對複雜的轉換。算法

指令式代碼和函數式對比:安全

要是作多級分組,指令式和函數式之間的區別就會更加明顯:因爲須要好多層嵌套循環和條件,指令式代碼很快就變得更難閱讀、更難維護、更難修改。相比之下,函數式版本只要再加上 一個收集器就能夠輕鬆地加強數據結構

預約義收集器,也就是那些能夠從Collectors類提供的工廠方法(例如groupingBy)建立的收集器。它們主要提供了三大功能:app

  • 將流元素歸約和彙總爲一個值
  • 元素分組
  • 元素分區

2. 使用收集器

在須要將流項目重組成集合時,通常會使用收集器(Stream方法collect 的參數)。再寬泛一點來講,但凡要把流中全部的項目合併成一個結果時就能夠用。這個結果能夠是任何類型,能夠複雜如表明一棵樹的多級映射,或是簡單如一個整數。分佈式

3. 收集器實例

3.1 流中最大值和最小值

Collectors.maxBy和 Collectors.minBy,來計算流中的最大或最小值。這兩個收集器接收一個Comparator參數來比較流中的元素。你能夠建立一個Comparator來根據所含熱量對菜餚進行比較:函數

System.out.println("找出熱量最高的食物:");
Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
collect.ifPresent(System.out::println);
System.out.println("找出熱量最低的食物:");
Optional<Dish> collect1 = DataUtil.genMenu().stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
collect1.ifPresent(System.out::println);複製代碼

3.2 彙總求和

Collectors類專門爲彙總提供了一個工廠方法:Collectors.summingInt。它可接受一個把對象映射爲求和所需int的函數,並返回一個收集器;該收集器在傳遞給普通的collect方法後即執行咱們須要的彙總操做。舉個例子來講,你能夠這樣求出菜單列表的總熱量:優化

Integer collect = DataUtil.genMenu().stream().collect(Collectors.summingInt(Dish::getCalories));
System.out.println("總熱量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summingDouble(Double::doubleValue));
System.out.println("double和:" + collect1);
Long collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summingLong(Long::longValue));
System.out.println("long和:" + collect2);複製代碼

3.3 彙總求平均值

Collectors.averagingInt,averagingLong和averagingDouble能夠計算數值的平均數:ui

Double collect = DataUtil.genMenu().stream().collect(Collectors.averagingInt(Dish::getCalories));
System.out.println("平均熱量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.averagingDouble(Double::doubleValue));
System.out.println("double 平均值:" + collect1);
Double collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.averagingLong(Long::longValue));
System.out.println("long 平均值:" + collect2);複製代碼

3.4 彙總合集

你可能想要獲得兩個或更多這樣的結果,並且你但願只需一次操做就能夠完成。在這種狀況下,你可使用summarizingInt工廠方法返回的收集器。例如,經過一次summarizing操做你能夠就數出菜單中元素的個數,並獲得熱量總和、平均值、最大值和最小值:spa

IntSummaryStatistics collect = DataUtil.genMenu().stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println("int:" + collect);
DoubleSummaryStatistics collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summarizingDouble(Double::doubleValue));
System.out.println("double:" + collect1);
LongSummaryStatistics collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summarizingLong(Long::longValue));
System.out.println("long:" + collect2);複製代碼

3.5 鏈接字符串

joining工廠方法返回的收集器會把對流中每個對象應用toString方法獲得的全部字符串鏈接成一個字符串。線程

String collect = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining());複製代碼

請注意,joining在內部使用了StringBuilder來把生成的字符串逐個追加起來。幸虧,joining工廠方法有一個重載版本能夠接受元素之間的分界符,這樣你就能夠獲得一個都好分隔的名稱列表:

String collect1 = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining(","));複製代碼

4. 廣義的歸約彙總

全部收集器,都是一個能夠用reducing工廠方法定義的歸約過程的特殊狀況而已。Collectors.reducing工廠方法是全部這些特殊狀況的通常化。它須要三個參數:

  • 第一個參數是歸約操做的起始值,也是流中沒有元素時的返回值,因此很顯然對於數值和而言0是一個合適的值。
  • 第二個參數就是你在6.2.2節中使用的函數,將菜餚轉換成一個表示其所含熱量的int。
  • 第三個參數是一個BinaryOperator,將兩個項目累積成一個同類型的值。這裏它就是對兩個int求和。

下面兩個是相同的操做:

Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));複製代碼

5. 分組

用Collectors.groupingBy工廠方法返回的收集器就能夠輕鬆地完成任務:

Map<Dish.Type, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType));複製代碼

給groupingBy方法傳遞了一個Function(以方法引用的形式),它提取了流中每 一道Dish的Dish.Type。咱們把這個Function叫做分類函數,由於它用來把流中的元素分紅不一樣的組。分組操做的結果是一個Map,把分組函數返回的值做爲映射的鍵,把流中全部具備這個分類值的項目的列表做爲對應的映射值。

5.1 多級分組

要實現多級分組,咱們可使用一個由雙參數版本的Collectors.groupingBy工廠方法建立的收集器,它除了普通的分類函數以外,還能夠接受collector類型的第二個參數。那麼要進行二級分組的話,咱們能夠把一個內層groupingBy傳遞給外層groupingBy,並定義一個爲流中項目分類的二級標準:

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(
        Collectors.groupingBy(Dish::getType,
                Collectors.groupingBy(dish -> {
                    if (dish.getCalories() <= 400) {
                        return CaloricLevel.DIET;
                    } else if (dish.getCalories() <= 700) {
                        return CaloricLevel.NORMAL;
                    } else return CaloricLevel.FAT;
                }))
);複製代碼

5.2 按子組收集數據

傳遞給第一個groupingBy的第二個收集器能夠是任何類型,而不必定是另外一個groupingBy。例如,要數一數菜單中每類菜有多少個,能夠傳遞counting收集器做爲groupingBy收集器的第二個參數:

Map<Dish.Type, Long> collect2 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));複製代碼

還要注意,普通的單參數groupingBy(f)(其中f是分類函數)其實是groupingBy(f, toList())的簡便寫法。把收集器返回的結果轉換爲另外一種類型,你可使用 Collectors.collectingAndThen工廠方法返回的收集器,接受兩個參數:要轉換的收集器以及轉換函數,並返回另外一個收集器。

Map<Dish.Type, Dish> collect3 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType,
        Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
                Optional::get
        )));複製代碼

這個操做放在這裏是安全的,由於reducing收集器永遠都不會返回Optional.empty()。

經常和groupingBy聯合使用的另外一個收集器是mapping方法生成的。這個方法接受兩個參數:一個函數對流中的元素作變換,另外一個則將變換的結果對象收􏰁起來。其目的是在累加以前對每一個輸入元素應用一個映射函數,這樣就可讓接受特定類型元素的收􏰁器適應不一樣類型的對象。咱們來看一個使用這個收集器的實際例子。比方說你想要知道,對於每種類型的Dish, 菜單中都有哪些CaloricLevel。

Map<Dish.Type, Set<CaloricLevel>> collect4 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(
        Dish::getType, Collectors.mapping(
                dish -> {
                    if (dish.getCalories() <= 400) {
                        return CaloricLevel.DIET;
                    } else if (dish.getCalories() <= 700) {
                        return CaloricLevel.NORMAL;
                    } else return CaloricLevel.FAT;
                }, Collectors.toSet()
        )
));複製代碼

6. 分區

分區是分組的特殊狀況:由一個謂詞(返回一個布爾值的函數)做爲分類函數,它稱分類函數。分區函數返回一個布爾值,這意味着獲得的分組Map的鍵類型是Boolean,因而它最多能夠 分爲兩組——true是一組,false是一組。例如,若是想要把菜按照素食和非素食分開:

Map<Boolean, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println(collect.get(true));
partitioningBy 工廠方法有一個重載版本,能夠像下面這樣傳遞第二個收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(
        Dish::isVegetarian, Collectors.groupingBy(Dish::getType)
));複製代碼

分區看做分組一種特殊狀況。

7. Collectors類的靜態工廠方法

8. 收集器接口

public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
}複製代碼

本列表適用如下定義:

  • T是流中要收集的項目的泛型。
  • A是累加器的類型,累加器是在收集過程當中用於累積部分結果的對象。
  • R是手機操做獲得的對象(一般但並不必定是集合)的類型。

8.1 創建新的結果容器:supplier方法

supplier方法必須返回一個結果爲空的Supplier,也就是一個無參數函數,在調用時它會建立一個空的累加器實例,供數據收集過程使用。

8.2 將元素添加到結果容器:accumulator方法

accumulator方法會返回執行歸約操做的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約結果的累加器(已收集了流中的前n-1個項目),還有第n個元素自己。該函數將返回void,由於累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷的元素的效果。

8.3 對結果容器應用最終轉換:finisher方法

在遍歷完流後,finisher方法必須返回在累積過程的最後要調用的一個函數,以便將累加器對象轉換爲整個集合操做的最終結果。順序歸約過程的邏輯步驟:

8.4 合併兩個結果容器:combiner方法

四個方法中的最後一個——combiner方法會返回一個供歸約操做使用的函數,它定義了對流的各個子部分進行並行處理時,各個子部分歸約所得的累加器要如何合併:

  • 原始流會以遞歸方式拆分爲子流,直到定義流是否須要進一步拆分的一個條件爲非(若是分佈式工做單位過小,並行計算每每比順序計算要慢,並且要是生成的並行任務比處理器內核數多不少的話就毫無心義了)。
  • 如今,全部的子流均可以並行處理,即對每一個子流應用圖6-7所示的順序歸約算法。
  • 最後,使用收集器combiner方法返回的函數,將全部的部分結果兩兩合併。這時會把原始流每次拆分時獲得的子流對應的結果合併起來

8.5 characteristics方法

最後一個方法——characteristics會返回一個不可變的Characteristics集合,它定義了收集器的行爲——尤爲是關於流是否能夠並行歸約,以及可使用哪些優化的提示。Characteristics是一個包含三個項目的枚舉。

  • UNORDERED——歸約結果不受流中項目的遍歷和累積順序的影響。
  • CONCURRENT——accumulator函數能夠從多個線程同時調用,且該收集器能夠並行歸約流。若是收集器沒有標爲UNORDERED,那它僅在用於無序數據源時才能夠並行歸約。
  • IDENTITY_FINISH——這代表完成器方法返回的函數是一個恆等函數,能夠跳過。這種狀況下,累加器對象將會直接用做歸約過程的最終結果。這也意味着,將累加器A不加檢查地轉換爲結果R是安全的。

9. 小結

  • collect是一個終端操做,它接受的參數是將流中元素累積到彙總結果的各類方式(稱爲收集器)。
  • 預約義收集器包括將流元素歸約和彙總到一個值,例如計算最小值、最大值或平均值。這些收集器總結在表6-1中。
  • 預約義收集器能夠用groupingBy對流中元素進行分組,或用partitioningBy進行分區。
  • 收集器能夠高效地複合起來,進行多級分組、分區和歸約。
  • 你能夠實現Collector接口中定義的方法來開發你本身的收集器。

資源獲取

  • 公衆號回覆 : Java8 便可獲取《Java 8 in Action》中英文版!

Tips

  • 歡迎收藏和轉發,感謝你的支持!(๑•̀ㅂ•́)و✧
  • 歡迎關注個人公衆號:莊裏程序猿,讀書筆記教程資源第一時間得到!

相關文章
相關標籤/搜索