1. 程式人生 > >Java 8系列之Stream的強大工具Collector

Java 8系列之Stream的強大工具Collector

Stream系列:

概述

前面我們使用過collect(toList()),在流中生成列表。實際開發過程中,List又是我們經常用到的資料結構,但是有時候我們也希望Stream能夠轉換生成其他的值,比如Map或者set,甚至希望定製生成想要的資料結構。

collect也就是收集器,是Stream一種通用的、從流生成複雜值的結構。只要將它傳給collect方法,也就是所謂的轉換方法,其就會生成想要的資料結構。這裡不得不提下,Collectors這個工具庫,在該庫中封裝了相應的轉換方法。當然,Collectors工具庫僅僅封裝了常用的一些情景,如果有特殊需求,那就要自定義了。

顯然,List是能想到的從流中生成的最自然的資料結構, 但是有時人們還希望從流生成其他值, 比如 Map 或 Set, 或者你希望定製一個類將你想要的東西抽象出來。

前面已經講過,僅憑流上方法的簽名,就能判斷出這是否是一個及早求值的操作。 reduce操作就是一個很好的例子, 但有時人們希望能做得更多。
這就是收集器,一種通用的、從流生成複雜值的結構。只要將它傳給collect 方法,所有的流就都可以使用它了。

<R, A> R collect(Collector<? super T, A, R> collector);

<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);

輔助介面

Supplier

Supplier<T>介面是一個函式介面,該介面聲明瞭一個get方法,主要用來建立返回一個指定資料型別的物件。

  • T:指定的資料型別

    @FunctionalInterface
    public interface Supplier {
    T get();
    }

BiConsumer

BiConsumer<T, U>介面是一個函式介面,該介面聲明瞭accept方法,並無返回值,該函式介面主要用來宣告一些預期操作。

同時,該介面定義了一個預設方法andThen,該方法接受一個BiConsumer,並返回一個組合的BiConsumer,其會按照順序執行操作。如果執行任一操作丟擲異常,則將其傳遞給組合操作的呼叫者。 如果執行此操作丟擲異常,將不執行後操作(after)。

@FunctionalInterface
public interface BiConsumer<T, U> {

    void accept(T t, U u);

    default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
        Objects.requireNonNull(after);

        return (l, r) -> {
            accept(l, r);
            after.accept(l, r);
        };
    }
}

BinaryOperator

BinaryOperator介面繼承於BiFunction介面,該介面指定了apply方法執行的引數型別及返回值型別均為T。

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

    public static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) <= 0 ? a : b;
    }

    public static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) {
        Objects.requireNonNull(comparator);
        return (a, b) -> comparator.compare(a, b) >= 0 ? a : b;
    }
}

@FunctionalInterface
public interface BiFunction<T, U, R> {


    R apply(T t, U u);

    default <V> BiFunction<T, U, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t, U u) -> after.apply(apply(t, u));
    }
}

Function

Funtion是一個函式介面,其內定義了一個轉換函式,將T轉換為R。比如Stream中的map方法便是接受該函式引數,將T轉換為R。

@FunctionalInterface
public interface Function<T, R> {

    /**
     * 轉換函式,將T轉換為R
     */
    R apply(T t);

    /**
     * 返回一個組合函式Function,首先執行before,然後再執行該Function
     *
     * 如果兩個函式的求值都丟擲異常,它將被中繼到組合函式的呼叫者。
     * 如果before為null,將會丟擲NullPointerException
     */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

     /**
     * 返回一個組合函式Function,首先執行Function,然後再執行after
     *
     * 如果兩個函式的求值都丟擲異常,它將被中繼到組合函式的呼叫者。
     * 如果after為null,將會丟擲NullPointerException
     */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    /**
     * 將輸入引數返回的函式
     */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

Collector

Collector是Stream的可變減少操作介面,可變減少操作包括:將元素累積到集合中,使用StringBuilder連線字串;計算元素相關的統計資訊,例如sum,min,max或average等。Collectors(類收集器)提供了許多常見的可變減少操作的實現。

Collector<T, A, R>接受三個泛型引數,對可變減少操作的資料型別作相應限制:

  • T:輸入元素型別
  • A:縮減操作的可變累積型別(通常隱藏為實現細節)
  • R:可變減少操作的結果型別

Collector介面聲明瞭4個函式,這四個函式一起協調執行以將元素目累積到可變結果容器中,並且可以選擇地對結果進行最終的變換.

  • Supplier<A> supplier(): 建立新的結果結
  • BiConsumer<A, T> accumulator(): 將元素新增到結果容器
  • BinaryOperator<A> combiner(): 將兩個結果容器合併為一個結果容器
  • Function<A, R> finisher(): 對結果容器作相應的變換

在Collector介面的characteristics方法內,可以對Collector宣告相關約束

  • Set<Characteristics> characteristics():

而Characteristics是Collector內的一個列舉類,聲明瞭CONCURRENT、UNORDERED、IDENTITY_FINISH等三個屬性,用來約束Collector的屬性。

  • CONCURRENT:表示此收集器支援併發,意味著允許在多個執行緒中,累加器可以呼叫結果容器
  • UNORDERED:表示收集器並不按照Stream中的元素輸入順序執行
  • IDENTITY_FINISH:表示finisher實現的是識別功能,可忽略。
    注:

    1. 如果一個容器僅宣告CONCURRENT屬性,而不是UNORDERED屬性,那麼該容器僅僅支援無序的Stream在多執行緒中執行。

身份約束和相關性約束

Stream可以順序執行,或者併發執行,或者順序併發執行,為了保證Stream可以產生相同的結果,收集器函式必須滿足身份約束和相關項約束。

身份約束說,對於任何部分累積的結果,將其與空結果容器組合必須產生等效結果。也就是說,對於作為任何系列的累加器和組合器呼叫的結果的部分累加結果a,a必須等於combiner.apply(a,supplier.get())。

相關性約束說,分裂計算必須產生等效的結果。也就是說,對於任何輸入元素t1和t2,以下計算中的結果r1和r2必須是等效的:

A a1 = supplier.get();
accumulator.accept(a1,t1);
accumulator.accept(a1,t2);
R r1 = finisher.apply(a1); // result without splitting

A a2 = supplier.get();
accumulator.accept(a2,t1);
A a3 = supplier.get();
accumulator.accept(a3,t2);
R r2 = finisher.apply(combiner.apply(a2,a3)); 

建立Collector

自定義Collector

基於Collector工具庫

在Collector工具庫中,聲明瞭許多常用的收集器,以供我們快速建立一個收集器。前面我們已經瞭解到,收集器函式必須滿足身份約束和相關項約束。而基於Collector實現簡化的庫(如Stream.collect(Collector))建立收集器時,必須遵守以下約束:

  1. 第一個引數傳遞給accumulator()函式,兩個引數都傳遞給combiner()函式,傳遞給finisher()函式的引數必須是上一次呼叫supplier(),accumulator()或combiner()函式的結果。
  2. 實現不應該對任何accumulator(),combiner()或finisher()函式的結果做任何事情,除非收集器將返回的結果返回給呼叫者
  3. 如果結果傳遞到combiner()或finisher()函式,而且返回物件與傳入的不相同,則不會再將物件傳遞給accumulator()函式呼叫。
  4. 一旦結果傳遞到combiner()或finisher()函式,它就不會再次傳遞到accumulator()函式。
  5. 對於序列收集器,supplier(),accumulator()或combiner()函式返回的任何結果必須是限制序列的。這使得收集器可以並行進行,而收集器不需要執行任何額外的同步。reduce操作實現必須管理Stream的元素被正確區別並分別處理,並且僅在累積完成之後,對累加器中的資料合併。
  6. 對於併發收集器,實現可以自由地(但不是必須)同時實現reduce操作。accumulator()可以在多個執行緒同時呼叫,而不是在累積期間保持結果的獨立性。僅當收集器具有Collector.Characteristics.UNORDERED特性或者原始資料是無序的時才應用併發還原。

轉換成其他集合

對於前面提到了很多Stream的鏈式操作,但是,我們總是要將Strea生成一個集合,比如:

  • 已有程式碼是為集合編寫的, 因此需要將流轉換成集合傳入;
  • 在集合上進行一系列鏈式操作後, 最終希望生成一個值;
  • 寫單元測試時, 需要對某個具體的集合做斷言。

有些Stream可以轉成集合,比如前面提到toList,生成了java.util.List 類的例項。當然了,還有還有toSet和toCollection,分別生成 Set和Collection 類的例項。

toList

示例:

List<Integer> collectList = Stream.of(1, 2, 3, 4)
        .collect(Collectors.toList());
System.out.println("collectList: " + collectList);
// 列印結果
// collectList: [1, 2, 3, 4]

toSet

示例:

Set<Integer> collectSet = Stream.of(1, 2, 3, 4)
        .collect(Collectors.toSet());
System.out.println("collectSet: " + collectSet);
// 列印結果
// collectSet: [1, 2, 3, 4]

toCollection

通常情況下,建立集合時需要呼叫適當的建構函式指明集合的具體型別:

List<Artist> artists = new ArrayList<>();

但是呼叫toList或者toSet方法時,不需要指定具體的型別,Stream類庫會自動推斷並生成合適的型別。當然,有時候我們對轉換生成的集合有特定要求,比如,希望生成一個TreeSet,而不是由Stream類庫自動指定的一種型別。此時使用toCollection,它接受一個函式作為引數, 來建立集合。

值得我們注意的是,看Collectors的原始碼,因為其接受的函式引數必須繼承於Collection,也就是意味著Collection並不能轉換所有的繼承類,最明顯的就是不能通過toCollection轉換成Map

toMap

如果生成一個Map,我們需要呼叫toMap方法。由於Map中有Key和Value這兩個值,故該方法與toSet、toList等的處理方式是不一樣的。toMap最少應接受兩個引數,一個用來生成key,另外一個用來生成value。toMap方法有三種變形:

  • toMap(Function<? super T, ? extends K> keyMapper,Function<? super T, ? extends U> valueMapper)

    1. keyMapper: 該Funtion用來生成Key
    2. valueMapper:該Funtion用來生成value

注:若Stream中有重複的值,導致Map中key重複,在執行時會報異常java.lang.IllegalStateException: Duplicate key **

  • toMap(Function

轉成值

使用collect可以將Stream轉換成值。maxBy和minBy允許使用者按照某個特定的順序生成一個值。

  • averagingDouble:求平均值,Stream的元素型別為double
  • averagingInt:求平均值,Stream的元素型別為int
  • averagingLong:求平均值,Stream的元素型別為long
  • counting:Stream的元素個數
  • maxBy:在指定條件下的,Stream的最大元素
  • minBy:在指定條件下的,Stream的最小元素
  • reducing: reduce操作
  • summarizingDouble:統計Stream的資料(double)狀態,其中包括count,min,max,sum和平均。
  • summarizingInt:統計Stream的資料(int)狀態,其中包括count,min,max,sum和平均。
  • summarizingLong:統計Stream的資料(long)狀態,其中包括count,min,max,sum和平均。
  • summingDouble:求和,Stream的元素型別為double
  • summingInt:求和,Stream的元素型別為int
  • summingLong:求和,Stream的元素型別為long

示例:

Optional<Integer> collectMaxBy = Stream.of(1, 2, 3, 4)
            .collect(Collectors.maxBy(Comparator.comparingInt(o -> o)));
System.out.println("collectMaxBy:" + collectMaxBy.get());
// 列印結果
// collectMaxBy:4

分割資料塊

collect的一個常用操作將Stream分解成兩個集合。假如一個數字的Stream,我們可能希望將其分割成兩個集合,一個是偶數集合,另外一個是奇數集合。我們首先想到的就是過濾操作,通過兩次過濾操作,很簡單的就完成了我們的需求。

但是這樣操作起來有問題。首先,為了執行兩次過濾操作,需要有兩個流。其次,如果過濾操作複雜,每個流上都要執行這樣的操作, 程式碼也會變得冗餘。

這裡我們就不得不說Collectors庫中的partitioningBy方法,它接受一個流,並將其分成兩部分:使用Predicate物件,指定條件並判斷一個元素應該屬於哪個部分,並根據布林值返回一個Map到列表。因此對於key為true所對應的List中的元素,滿足Predicate物件中指定的條件;同樣,key為false所對應的List中的元素,不滿足Predicate物件中指定的條件

這裡寫圖片描述

這樣,使用partitioningBy,我們就可以將數字的Stream分解成奇數集合和偶數集合了。

 Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
            .collect(Collectors.partitioningBy(it -> it % 2 == 0));
System.out.println("collectParti : " + collectParti);
// 列印結果
// collectParti : {false=[1, 3], true=[2, 4]}

資料分組

資料分組是一種更自然的分割資料操作, 與將資料分成true和false兩部分不同,可以使用任意值對資料分組。

呼叫Stream的collect方法,傳入一個收集器,groupingBy接受一個分類函式,用來對資料分組,就像partitioningBy一樣,接受一個
Predicate物件將資料分成true和false兩部分。我們使用的分類器是一個Function物件,和map操作用到的一樣。

這裡寫圖片描述

示例:

Map<Boolean, List<Integer>> collectGroup= Stream.of(1, 2, 3, 4)
            .collect(Collectors.groupingBy(it -> it > 3));
System.out.println("collectGroup : " + collectGroup);
// 列印結果
// collectGroup : {false=[1, 2, 3], true=[4]}


注:

看groupingBy和partitioningBy的例子,他們的效果都是一樣的,都是將Stream的資料進行了分割處理並返回一個Map。可能舉的例子給你帶來了誤區,實際上他們兩個完全是不一樣的。

  1. partitioningBy是根據指定條件,將Stream分割,返回的Map為Map

字串

有時候,我們將Stream的元素(String型別)最後生成一組字串。比如在Stream.of(“1”, “2”, “3”, “4”)中,將Stream格式化成“1,2,3,4”。

如果不使用Stream,我們可以通過for迴圈迭代實現。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);

StringBuilder sb = new StringBuilder();

for (Integer it : list) {
    if (sb.length() > 0) {
        sb.append(",");
    }
    sb.append(it);

}
System.out.println(sb.toString());
// 列印結果
// 1,2,3,4

在Java 1.8中,我們可以使用Stream來實現。這裡我們將使用 Collectors.joining 收集Stream中的值,該方法可以方便地將Stream得到一個字串。joining函式接受三個引數,分別表示允(用以分隔元素)、字首和字尾。

示例:

String strJoin = Stream.of("1", "2", "3", "4")
        .collect(Collectors.joining(",", "[", "]"));
System.out.println("strJoin: " + strJoin);
// 列印結果
// strJoin: [1,2,3,4]

組合Collector

前面,我們已經瞭解到Collector的強大,而且非常的使用。如果將他們組合起來,是不是更厲害呢?看前面舉過的例子,在資料分組時,我們是得到的分組後的資料列表 collectGroup : {false=[1, 2, 3], true=[4]}。如果我們的要求更高點,我們不需要分組後的列表,只要得到分組後列表的個數就好了。

這時候,很多人下意識的都會想到,便利Map就好了,然後使用list.size(),就可以輕鬆的得到各個分組的列表個數。

// 分割資料塊
Map<Boolean, List<Integer>> collectParti = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it % 2 == 0));

Map<Boolean, Integer> mapSize = new HashMap<>();
collectParti.entrySet()
        .forEach(entry -> mapSize.put(entry.getKey(), entry.getValue().size()));

System.out.println("mapSize : " + mapSize);
// 列印結果
// mapSize : {false=2, true=2}

在partitioningBy方法中,有這麼一個變形:

Map<Boolean, Long> partiCount = Stream.of(1, 2, 3, 4)
        .collect(Collectors.partitioningBy(it -> it.intValue() % 2 == 0,
                Collectors.counting()));
System.out.println("partiCount: " + partiCount);
// 列印結果
// partiCount: {false=2, true=2}

在partitioningBy方法中,我們不僅傳遞了條件函式,同時傳入了第二個收集器,用以收集最終結果的一個子集,這些收集器叫作下游收集器。收集器是生成最終結果的一劑配方,下游收集器則是生成部分結果的配方,主收集器中會用到下游收集器。這種組合使用收集器的方式, 使得它們在 Stream 類庫中的作用更加強大。

那些為基本型別特殊定製的函式,如averagingInt、summarizingLong等,事實上和呼叫特殊Stream上的方法是等價的,加上它們是為了將它們當作下游收集器來使用的。