天天看點

java8新特性-Collect收集Stream的方法集合接口分類收集器的作用

java8新特性-Collect收集Stream的方法

  • 集合接口分類
  • 收集器的作用
    • 預定義的收集器
    • 最大值,最小值,平均值
    • 連接配接收集器
      • toList
      • toSet
      • toMap
    • 自定義歸約reducing
    • reducing
    • 分組

集合接口分類

Collection

Collection是Java集合的祖先接口。

Collections

Collections是java.util包下的一個工具類,内涵各種處理集合的靜态方法。

collect

java.util.stream.Stream#collect(java.util.stream.Collector<? super T,A,R>)是Stream的一個函數,負責收集流。

Collector

java.util.stream.Collector 是一個收集函數的接口, 聲明了一個收集器的功能。

Collectos

java.util.Comparators則是一個收集器的工具類,内置了一系列收集器實作。

收集器的作用

  • 你可以把Java8的流看做花哨又懶惰的資料集疊代器。他們支援兩種類型的操作:中間操作(

    e.g

    .

    filter

    ,

    map

    )和終端操作(如

    count

    ,

    findFirst

    ,

    forEach

    ,

    reduce

    ). 中間操作可以連接配接起來,将一個流轉換為另一個流。這些操作不會消耗流,其目的是建立一個流水線。與此相反,終端操作會消耗類,産生一個最終結果。
  • collect

    就是一個歸約操作,就像

    reduce

    一樣可以接受各種做法作為參數,将流中的元素累積成一個彙總結果。具體的做法是通過定義新的

    Collector

    接口來定義的。

預定義的收集器

下面簡單示範基本的内置收集器。模拟資料源如下:

final ArrayList<Dish> dishes = Lists.newArrayList(
    new Dish("pork", false, 800, Type.MEAT),
    new Dish("beef", false, 700, Type.MEAT),
    new Dish("chicken", false, 400, Type.MEAT),
    new Dish("french fries", true, 530, Type.OTHER),
    new Dish("rice", true, 350, Type.OTHER),
    new Dish("season fruit", true, 120, Type.OTHER),
    new Dish("pizza", true, 550, Type.OTHER),
    new Dish("prawns", false, 300, Type.FISH),
    new Dish("salmon", false, 450, Type.FISH)
);
           

最大值,最小值,平均值

// 為啥傳回Optional? 如果stream為null怎麼辦, 這時候Optinal就很有意義了
Optional<Dish> mostCalorieDish = dishes.stream().max(Comparator.comparingInt(Dish::getCalories));
Optional<Dish> minCalorieDish = dishes.stream().min(Comparator.comparingInt(Dish::getCalories));
Double avgCalories = dishes.stream().collect(Collectors.averagingInt(Dish::getCalories));
IntSummaryStatistics summaryStatistics = dishes.stream().collect(Collectors.summarizingInt(Dish::getCalories));
double average = summaryStatistics.getAverage();
long count = summaryStatistics.getCount();
int max = summaryStatistics.getMax();
int min = summaryStatistics.getMin();
long sum = summaryStatistics.getSum();
           
這幾個簡單的統計名額都有Collectors内置的收集器函數,尤其是針對數字類型拆箱函數,将會比直接操作包裝類型開銷小很多。

連接配接收集器

想要把Stream的元素拼起來?

//直接連接配接
String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining());
//逗号
String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));
           

toList

List<String> names = dishes.stream().map(Dish::getName).collect(toList());

将原來的Stream映射為一個單元素流,然後收集為List。
           

toSet

Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());

将Type收集為一個set,可以去重複。
           

toMap

有時候可能需要将一個數組轉為map,做緩存,友善多次計算擷取。toMap提供的方法k和v的生成函數。(注意,上述demo是一個坑,不可以這樣用!!!, 請使用toMap(Function, Function, BinaryOperator))

上面幾個幾乎是最常用的收集器了,也基本夠用了。但作為初學者來說,了解需要時間。想要真正明白為什麼這樣可以做到收集,就必須檢視内部實作,可以看到,這幾個收集器都是基于java.util.stream.Collectors.CollectorImpl,也就是開頭提到過了Collector的一個實作類。後面自定義收集器會學習具體用法。

自定義歸約reducing

  • 前面幾個都是reducing工廠方法定義的歸約過程的特殊情況,其實可以用Collectors.reducing建立收集器。比如,求和
Integer totalCalories = dishes.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
//使用内置函數代替箭頭函數
Integer totalCalories2 = dishes.stream().collect(reducing(0, Dish::getCalories, Intege
           
  • 當然也可以直接使用reduce
  • 雖然都可以,但考量效率的話,還是要選擇下面這種

上面的demo說明,函數式程式設計通常提供了多種方法來執行同一個操作,使用收集器collect比直接使用stream的api用起來更加複雜,好處是collect能提供更高水準的抽象和概括,也更容易重用和自定義。

我們的建議是,盡可能為手頭的問題探索不同的解決方案,始終選擇最專業的一個,無論從可讀性還是性能來看,這一般都是最好的決定。

  • reducing除了接收一個初始值,還可以把第一項當作初始值
Optional<Dish> mostCalorieDish = dishes.stream()
        .collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2))
           

reducing

  • 關于reducing的用法比較複雜,目标在于把兩個值合并成一個值。
public static <T, U>
  Collector<T, ?, U> reducing(U identity,
                Function<? super T, ? extends U> mapper,
                BinaryOperator<U> op)
           
  • 首先看到3個泛型
    • U是傳回值的類型,比如上述demo中計算熱量的,U就是Integer。
    • T,T是Stream裡的元素類型。由Function的函數可以知道,mapper的作用就是接收一個參數T,然後傳回一個結果U。對應demo中Dish。
    • ?在傳回值Collector的泛型清單的中間,這個表示容器類型,一個收集器當然需要一個容器來存放資料。這裡的?則表示容器類型不确定。事實上,在這裡的容器就是U[]。
  • 關于參數:
    • identity是傳回值類型的初始值,可以了解為累加器的起點。
    • mapper則是map的作用,意義在于将Stream流轉換成你想要的類型流。
    • op則是核心函數,作用是如何處理兩個變量。其中,第一個變量是累積值,可以了解為sum,第二個變量則是下一個要計算的元素。進而實作了累加。
  • reducing還有一個重載的方法,可以省略第一個參數,意義在于把Stream裡的第一個參數當做初始值
public static <T> Collector<T, ?, Optional<T>>
  reducing(BinaryOperator<T> op)
           

先看傳回值的差別,T表示輸入值和傳回值類型,即輸入值類型和輸出值類型相同。還有不同的就是Optional了。這是因為沒有初始值,而第一個參數有可能是null,當Stream的元素是null的時候,傳回Optional就很意義了。

再看參數清單,隻剩下BinaryOperator。BinaryOperator是一個三元組函數接口,目标是将兩個同類型參數做計算後傳回同類型的值。可以按照1>2?

1:2來了解,即求兩個數的最大值。求最大值是比較好了解的一種說法,你可以自定義lambda表達式來選擇傳回值。那麼,在這裡,就是接收兩個Stream的元素類型T,傳回T類型的傳回值。用sum累加來了解也可以。

上述的demo中發現reduce和collect的作用幾乎一樣,都是傳回一個最終的結果,比如,我們可以使用reduce實作toList效果:

//手動實作toListCollector --- 濫用reduce, 不可變的規約---不可以并行
List<Integer> calories = dishes.stream().map(Dish::getCalories)
    .reduce(new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
          l.add(e);
          return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
          l1.addAll(l2);
          return l1;
        }
    );
           

關于上述做法解釋一下。

<U> U reduce(U identity,
         BiFunction<U, ? super T, U> accumulator,
         BinaryOperator<U> combiner);
           

U是傳回值類型,這裡就是List

BiFunction<U, ? super T, U>

accumulator是是累加器,目标在于累加值和單個元素的計算規則。這裡就是List和元素做運算,最終傳回List。即,添加一個元素到list。

BinaryOperator< U > combiner是組合器,目标在于把兩個傳回值類型的變量合并成一個。這裡就是兩個list合并。

這個解決方案有兩個問題:一個是語義問題,一個是實際問題。語義問題在于,reduce方法旨在把兩個值結合起來生成一個新值,它是一個不可變歸約。相反,collect方法的設計就是要改變容器,進而累積要輸出的結果。這意味着,上面的代碼片段是在濫用reduce方法,因為它在原地改變了作為累加器的List。錯誤的語義來使用reduce方法還會造成一個實際問題:這個歸約不能并行工作,因為由多個線程并發修改同一個資料結構可能會破壞List本身。在這種情況下,如果你想要線程安全,就需要每次配置設定一個新的List,而對象配置設定又會影響性能。這就是collect适合表達可變容器上的歸約的原因,更關鍵的是它适合并行操作。

總結:reduce适合不可變容器歸約,collect适合可變容器歸約。collect适合并行。

分組

  • 資料庫中經常遇到分組求和的需求,提供了group by原語。在Java裡, 如果按照指令式風格(手動寫循環)的方式,将會非常繁瑣,容易出錯。而Java8則提供了函數式解法。
  • 比如,将dish按照type分組。和前面的toMap類似,但分組的value卻不是一個dish,而是一個List。
https://www.jb51.net/article/138519.htm