1. 程式人生 > >《Java 8 in Action》Chapter 6:用流收集資料

《Java 8 in Action》Chapter 6:用流收集資料

1. 收集器簡介

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

指令式程式碼和函式式對比:

要是做多級分組,指令式和函式式之間的區別就會更加明顯:由於需要好多層巢狀迴圈和條件,指令式程式碼很快就變得更難閱讀、更難維護、更難修改。相比之下,函式式版本只要再加上 一個收集器就可以輕鬆地增強

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

  • 將流元素歸約和彙總為一個值
  • 元素分組
  • 元素分割槽

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可以計算數值的平均數:

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操作你可以就數出選單中元素的個數,並得到熱量總和、平均值、最大值和最小值:

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方法生成的。這個方法接受兩個引數:一個函式對流中的元素做變換,另一個則將變換的結果物件收