Java 8 - 自定义Collector
文章目錄
- Pre
- Collector接口聲明的方法
- 理解 Collector接口中聲明的方法
- 1.建立新的結果容器: supplier 方法
- 2.將元素添加到結果容器: accumulator 方法
- 3.對結果容器應用最終轉換: finisher 方法
- 4.合并兩個結果容器: combiner 方法
- 5. characteristics 方法
- 自定義Collector Demo
Pre
Collector 接口包含了一系列方法,為實現具體的歸約操作(即收集器)提供了范本。
我們已經看過了 Collector 接口中實現的許多收集器,例如 toList 或 groupingBy 。這也意味著可以為 Collector 接口提供自己的實現,從而自由地創建自定義歸約操作。
要開始使用 Collector 接口,我們先看看toList 工廠方法,它會把流中的所有元素收集成一個 List 。我們當時說在日常工作中經常會用到這個收集器,而且它也是寫起來比較直觀的一個,至少理論上如此。通過仔細研究這個收集器是怎么實現的。
我們可以很好地了解 Collector 接口是怎么定義的,以及它的方法所返回的函數在內部是如何為collect 方法所用的。
Collector接口聲明的方法
首先讓我們在下面的列表中看看 Collector 接口的定義,它列出了接口的簽名以及聲明的五個方法。
- T 是流中要收集的項目的泛型。
- A 是累加器的類型,累加器是在收集過程中用于累積部分結果的對象。
- R 是收集操作得到的對象(通常但并不一定是集合)的類型。
例如,你可以實現一個 ToListCollector<T> 類,將 Stream<T> 中的所有元素收集到一個List<T> 里,它的簽名如下
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>待會揭秘 這里用于累積的對象也將是收集過程的最終結果
理解 Collector接口中聲明的方法
現在我們可以一個個來分析Collector 接口聲明的五個方法了。通過分析,你會注意到,前四個方法都會返回一個會被 collect 方法調用的函數,而第五個方法 characteristics 則提供了一系列特征,也就是一個提示列表,告訴 collect 方法在執行歸約操作的時候可以應用哪些優化(比如并行化)。
1.建立新的結果容器: supplier 方法
supplier 方法必須返回一個結果為空的 Supplier ,也就是一個無參數函數,在調用時它會創建一個空的累加器實例,供數據收集過程使用。
很明顯,對于將累加器本身作為結果返回的收集器,比如我們的 ToListCollector ,在對空流執行操作的時候,這個空的累加器也代表了收集過程的結果。
在我們的 ToListCollector 中, supplier 返回一個空的 List ,如下所示:
public Supplier<List<T>> supplier() {return () -> new ArrayList<T>(); }請注意你也可以只傳遞一個構造函數引用:
public Supplier<List<T>> supplier() { return ArrayList::new; }2.將元素添加到結果容器: accumulator 方法
accumulator 方法會返回執行歸約操作的函數。當遍歷到流中第n個元素時,這個函數執行時會有兩個參數:保存歸約結果的累加器(已收集了流中的前 n-1 個項目),還有第n個元素本身。
該函數將返回 void ,因為累加器是原位更新,即函數的執行改變了它的內部狀態以體現遍歷的元素的效果。
對于 ToListCollector ,這個函數僅僅會把當前項目添加至已經遍歷過的項目的列表:
public BiConsumer<List<T>, T> accumulator() {return (list, item) -> list.add(item); }你也可以使用方法引用,這會更為簡潔:
public BiConsumer<List<T>, T> accumulator() {return List::add; }3.對結果容器應用最終轉換: finisher 方法
在遍歷完流后, finisher 方法必須返回在累積過程的最后要調用的一個函數,以便將累加器對象轉換為整個集合操作的最終結果。
通常,就像 ToListCollector 的情況一樣,累加器對象恰好符合預期的最終結果,因此無需進行轉換。所以 finisher 方法只需返回 identity 函數:
public Function<List<T>, List<T>> finisher() {return Function.identity(); }這三個方法已經足以對流進行順序歸約,至少從邏輯上看可以按下圖進行。
實踐中的實現細節可能還要復雜一點,一方面是因為流的延遲性質,可能在 collect 操作之前還需要完成其他中間操作的流水線,另一方面則是理論上可能要進行并行歸約。
4.合并兩個結果容器: combiner 方法
四個方法中的最后一個—— combiner 方法會返回一個供歸約操作使用的函數,它定義了對
流的各個子部分進行并行處理時,各個子部分歸約所得的累加器要如何合并。
對于 toList 而言,這個方法的實現非常簡單,只要把從流的第二個部分收集到的項目列表加到遍歷第一部分時得到的列表后面就行了:
public BinaryOperator<List<T>> combiner() {return (list1, list2) -> {list1.addAll(list2);return list1; } }有了這第四個方法,就可以對流進行并行歸約了。它會用到Java 7中引入的分支/合并框架和Spliterator 抽象, 如下圖所示
5. characteristics 方法
最后一個方法—— characteristics 會返回一個不可變的 Characteristics 集合,它定義了收集器的行為——尤其是關于流是否可以并行歸約,以及可以使用哪些優化的提示。
Characteristics 是一個包含三個項目的枚舉。
- UNORDERED ——歸約結果不受流中項目的遍歷和累積順序的影響
- CONCURRENT —— accumulator 函數可以從多個線程同時調用,且該收集器可以并行歸約流。如果收集器沒有標為 UNORDERED ,那它僅在用于無序數據源時才可以并行歸約。
- IDENTITY_FINISH ——這表明完成器方法返回的函數是一個恒等函數,可以跳過。這種情況下,累加器對象將會直接用作歸約過程的最終結果。這也意味著,將累加器 A 不加檢查地轉換為結果 R 是安全的。
我們迄今開發的 ToListCollector 是 IDENTITY_FINISH 的,因為用來累積流中元素的List 已經是我們要的最終結果,用不著進一步轉換了,但它并不是 UNORDERED ,因為用在有序流上的時候,我們還是希望順序能夠保留在得到的 List 中。
最后,它是CONCURRENT 的,但我們剛才說過了,僅僅在背后的數據源無序時才會并行處理。
自定義Collector Demo
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {private void log(final String log) {System.out.println(Thread.currentThread().getName() + "-" + log);}@Overridepublic Supplier<List<T>> supplier() {log("supplier");return ArrayList::new;}@Overridepublic BiConsumer<List<T>, T> accumulator() {log("accumulator");return List::add;}@Overridepublic BinaryOperator<List<T>> combiner() {log("combiner");return (list1, list2) -> {list1.addAll(list2);return list1;};}@Overridepublic Function<List<T>, List<T>> finisher() {log("finisher");return t -> t;}@Overridepublic Set<Characteristics> characteristics() {log("characteristics");return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));}
測試下
總結
以上是生活随笔為你收集整理的Java 8 - 自定义Collector的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入理解分布式技术 - 缓存过期策略手写
- 下一篇: Java开发规范01 - 集合篇_Arr