Java8-自定義收集器的實現及源碼程式碼調用分析

  • 2020 年 2 月 18 日
  • 筆記

1.引子:

 Java8帶給Java的最大特點就是函數式編程,其中一個重要的更新就是對於流的支援,這使得開發者可以使用一行程式碼就能完成從一個數據結構轉化為另一個數據結構,編程效率大大提高。為了充分理解收集器Colleector介面,最好的方法就是自定義一個收集器,並分析其程式碼執行過程。

2.自定義收集器Collector:

CodeBlock-1:

import java.util.*;  import java.util.function.BiConsumer;  import java.util.function.BinaryOperator;  import java.util.function.Function;  import java.util.function.Supplier;  import java.util.stream.Collector;    /**   * @author Fisherman   */  public class MySetCollector<T> implements Collector<T, Set<T>, Set<T>> {          @Override      public Supplier<Set<T>> supplier() {          System.out.println("supplier is invoked.");          return HashSet::new;      }        @Override      public BiConsumer<Set<T>, T> accumulator() {            System.out.println("accumulator is invoked.");          return Set<T>::add;//相當於 return (set,item) -> set.add(item);          }        @Override      public BinaryOperator<Set<T>> combiner() {          System.out.println("combiner is invoked.");            return (set1, set2) -> {              set1.addAll(set2);              return set1;          };      }        @Override      /**       * 這是一個可選項,只有中間容器結果容器不一致才會被傳遞此介面實現       */      public Function<Set<T>, Set<T>> finisher() {          System.out.println("finisher is invoked.");          return Function.identity();//相當於 return t->t;        }        @Override      public Set<Characteristics> characteristics() {          System.out.println("characteristics is invoked.");          return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.UNORDERED));        }          public static void main(String[] args) {          List<String> list =  Arrays.asList("hello", "world", "fisherman","hello");            Set<String> set = list.stream().collect(new MySetCollector<>());            System.out.println(set);      }  }

控制台輸出:

supplier is invoked.  accumulator is invoked.  combiner is invoked.  characteristics is invoked.  characteristics is invoked.  [world, hello, fisherman]

3. 程式碼調用分析:

 分析說明:我們按照為了解釋控制台輸出順序的目的來進行程式碼調用的分析。由結果反推運行順序,也是一種不錯的學習方法。非常重要且需要辨析的一個知識點:控制台打出來的invoked執行順序,代表了收集器中的方法被調用,將返回值(函數式介面實現對象)作為參數傳入介面內部的執行順序。而對元素執行操作:添加到Set數據結構,只是關於accumulator()等方法的返回值(函數式介面的實現對象)的應用,此時System.out.println(「xxx is invoked.」);已經不會再被調用了。

 我們使用以下語句實現了從List數據結構到Set數據結構的轉變:Set<String> set = list.stream().collect(new MySetCollector<>());

stream().collect指向了Stream介面中的<R, A> R collect(Collector<? super T, A, R> collector);方法,由於多態的性質,實際上此方法是由ReferencePipeline抽象類中的 public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {...}實現的: CodeBlock-2:

    @Override      @SuppressWarnings("unchecked")      public final <R, A> R collect(Collector<? super P_OUT, A, R> collector) {          A container;          if (isParallel()                  && (collector.characteristics().contains(Collector.Characteristics.CONCURRENT))                  && (!isOrdered() || collector.characteristics().contains(Collector.Characteristics.UNORDERED))) {              container = collector.supplier().get();              BiConsumer<A, ? super P_OUT> accumulator = collector.accumulator();              forEach(u -> accumulator.accept(container, u));          }          else {              container = evaluate(ReduceOps.makeRef(collector));          }          return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)                 ? (R) container                 : collector.finisher().apply(container);

此方法中泛型說明: P_OUT:流中的單個元素類型 A:中間結果容器類型 R:返回結果容器類型

程式碼分析:

  1. 首先創造了一個中間結果容器,即:container
  2. 然後判斷是否為並行流,因為此例中不是並行流,所以直接跳至else{ }處運行;
  3. 運行至makeRef()方法,其作用是將Collector介面中的幾個抽象函數式介面進行賦值: CodeBlock-3:
 public static <T, I> TerminalOp<T, I>      makeRef(Collector<? super T, I, ?> collector) {          Supplier<I> supplier = Objects.requireNonNull(collector).supplier();          BiConsumer<I, ? super T> accumulator = collector.accumulator();          BinaryOperator<I> combiner = collector.combiner();          class ReducingSink extends Box<I>                  implements AccumulatingSink<T, I, ReducingSink> {              @Override              public void begin(long size) {                  state = supplier.get();              }                @Override              public void accept(T t) {                  accumulator.accept(state, t);              }                @Override              public void combine(ReducingSink other) {                  state = combiner.apply(state, other.state);              }          }          return new ReduceOp<T, I, ReducingSink>(StreamShape.REFERENCE) {              @Override              public ReducingSink makeSink() {                  return new ReducingSink();              }                @Override              public int getOpFlags() {                  return collector.characteristics().contains(Collector.Characteristics.UNORDERED)                         ? StreamOpFlag.NOT_ORDERED                         : 0;              }          };      }

 上述程式碼中的以下三個語句,就實現了對三個函數式介面的賦值。

        Supplier<I> supplier = Objects.requireNonNull(collector).supplier();          BiConsumer<I, ? super T> accumulator = collector.accumulator();          BinaryOperator<I> combiner = collector.combiner();

 所以,我們在控制台中可以看到,首先按序輸出的語句是:

    supplier is invoked.  	accumulator is invoked.  	combiner is invoked.

 注意:supplier.getr()方法並不會調用System.out.println("supplier is invoked.");,其餘兩個介面也有這樣的性質。

 接下來控制台輸出了兩個,為什麼不是一個?:

characteristics is invoked.  characteristics is invoked

 這是為什麼呢?請看以下分析:在 CodeBlock-3: 中,最終會運行到以下語句:

           public int getOpFlags() {                  return collector.characteristics().contains(Collector.Characteristics.UNORDERED)                         ? StreamOpFlag.NOT_ORDERED                         : 0;              }

 其功能是對於判斷傳入的collector介面是否含有Collector.Characteristics.UNORDERED特性,由則返回StreamOpFlag.NOT_ORDERED,無則返回0;這裡就是第一次造成了characteristics is invoked.的輸出。

 那麼第二次characteristics is invoked在控制台上的輸出是在哪造成的呢?可見我們依據將 CodeBlock-3: 中的程式碼執行完畢,所以接下來執行: CodeBlock-2: 中接下來的程式碼:

        return collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)                 ? (R) container                 : collector.finisher().apply(container);

 這裡的return語句:首先判斷傳入的collector介面是否含有Collector.Characteristics.IDENTITY_FINISH特性,如果有,那麼就將原本為A類型的中間容器 container強制轉為R類型返回,否則就調用finisher().apply方法進行中間容器到結果容器的轉變,並返回。

注意事項

  1. 強制類型轉換這裡根本就沒有進行A,R類型轉換是否合理的判斷,如果A不是繼承於R類型或者A≠R類型,那麼肯定為拋出異常。所以這就是 CodeBlock-3: 中的方法被修飾為@SuppressWarnings("unchecked")的原因,因為類型強制轉換沒有被檢查其合法性。
  2. 另一方面,為了防止拋出類型轉換異常,要求只要Collector介面的特性被修飾為Characteristics.IDENTITY_FINISH,那麼結果容器類型R和中間結果容器類型A一定是相同或者A extends R,避免拋出異常。

 分析到這裡我們已經把控制台輸出的結構都按序做了分析,但是你是否有個疑問:為何finisher介面實現中的System.out.println("finisher is invoked.");語句沒被執行,控制台上沒打出這句話?這是因為介面有Characteristics.IDENTITY_FINISH這個特性,所以直接返回(R) container,而finisher介面實現從來沒被讀取過!所以如果將 CodeBlock-1: 中的屬性Characteristics.IDENTITY_FINISH去掉,那麼就會讀取finisher介面實現。

最後的最後在說明一下數據結構上的改變:

  1. List 轉為 Set,原List中的重複元素只能加入到Set中一個,如」hello「被去重了,見控制台對於新數據結構的遍歷輸出。
  2. 由於收集器的特性被設置為 Characteristics.UNORDERED所以從List到Set的轉換過程中,元素的順序被打亂了:比如原來"hello", "world", "fisherman"的順序,現在為"world", "hello", "fisherman"
  3. 函數式編程的思想:這裡只是提供了一個收集器的介面實現,但是其具體如何進行數據結構的轉換是不需要我們進行編寫的。