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

來源:互聯網
上載者:User

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

Stream系列:

  • Java 8系列之Stream的基本文法詳解
  • Java 8系列之Stream的強大工具Collector
  • Java 8系列之重構和定製收集器
  • Java 8系列之Stream中萬能的reduce
概述

前面我們使用過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)。

@FunctionalInterfacepublic 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。

@FunctionalInterfacepublic 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;    }}@FunctionalInterfacepublic 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。

@FunctionalInterfacepublic 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實現的是識別功能,可忽略。
    註:

身份約束和相關性約束

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 splittingA 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

Java 8系列之重構和定製收集器

基於Collector工具庫

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

轉換成其他集合

對於前面提到了很多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)

註:若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。可能舉的例子給你帶來了誤區,實際上他們兩個完全是不一樣的。

字串

有時候,我們將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上的方法是等價的,加上它們是為了將它們當作下遊收集器來使用的。

查看評論

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.