Java8之Stream-強大的collect操作

IT小生發表於2019-03-04

collect應該說是Stream中最強大的終端操作了,使用其幾乎能得到你想要的任意資料的聚合,下面好好分析該工具的用法.


在Stream介面中有如下兩個方法

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

 <R, A> R collect(Collector<? super T, A, R> collector);複製程式碼

很明顯第一種相當於簡易實現版本,第二種為高階用法.更多更復雜的操作都封裝到Collector介面中,並提供一些靜態方法供使用者呼叫.下面逐一分析.

簡易呼叫形式

簡易呼叫形式就是第一種介面,介面如下

  <R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);複製程式碼

呼叫方式如下,很明顯第一個引數supplier為結果存放容器,第二個引數accumulator為結果如何新增到容器的操作,第三個引數combiner則為多個容器的聚合策略.

String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,StringBuilder::append).toString();
//等價於上面,這樣看起來應該更加清晰
String concat = stringStream.collect(() -> new StringBuilder(),(l, x) -> l.append(x), (r1, r2) -> r1.append(r2)).toString();複製程式碼

那麼換一種,我想對一個List收集結果總和,按照Collect的要求,首先需要容器sum,然後新增操作 sum+x,聚合操作,sum1+sum2,那麼就很容易寫出來了,看完下面程式碼後好好體會下,然後再看高階用法.當然用sum方法收集是最佳解決方案,這裡只是提供一種示例應用.

// 由於基本型別都是不可變型別,所以這裡用陣列當做容器
final Integer[] integers = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream()
        .collect(() -> new Integer[]{0}, (a, x) -> a[0] += x, (a1, a2) -> a1[0] += a2[0]);複製程式碼

那麼再換一種,有一個Person類,其擁有type與name兩個屬性,那麼使用collect把他收集到Map集合中,其中鍵為type,值為person的集合.如下程式碼所示,看明白了相信就掌握了該方法.

   Lists.<Person>newArrayList().stream()
        .collect(() -> new HashMap<Integer,List<Person>>(),
            (h, x) -> {
              List<Person> value = h.getOrDefault(x.getType(), Lists.newArrayList());
              value.add(x);
              h.put(x.getType(), value);
            },
            HashMap::putAll
        );複製程式碼

Collector高階呼叫

Collector介面是使得collect操作強大的終極武器,對於絕大部分操作可以分解為旗下主要步驟,提供初始容器->加入元素到容器->併發下多容器聚合->對聚合後結果進行操作,同時Collector介面又提供了of靜態方法幫助你最大化的定製自己的操作,官方也提供了Collectors這個類封裝了大部分的常用收集操作.
另外CollectorImplCollector的實現類,因為介面不可例項化,這裡主要完成例項化操作.

    //初始容器
     Supplier<A> supplier();
    //加入到容器操作
    BiConsumer<A, T> accumulator();
    //多容器聚合操作
    BinaryOperator<A> combiner();
    //聚合後的結果操作
    Function<A, R> finisher();
    //操作中便於優化的狀態欄位
    Set<Characteristics> characteristics();複製程式碼

Collectors的方法封裝

Collectors作為官方提供的收集工具類,那麼其很多操作都具有參考性質,能幫助我們更加理解Collector介面,萬變不離其宗,最終只是上面五個函式介面的混合操作,下面來分析下官方是如何使用這幾個介面的.

toList()

容器: ArrayList::new
加入容器操作: List::add
多容器合併: left.addAll(right); return left;
聚合後的結果操作: 這裡直接返回,因此無該操作,預設為castingIdentity()
優化操作狀態欄位: CH_ID
這樣看起來很簡單,那麼對於Map,Set等操作都是類似的實現.

   public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }複製程式碼

joining()

容器: StringBuilder::new
加入容器操作: StringBuilder::append
多容器合併: r1.append(r2); return r1;
聚合後的結果操作: StringBuilder::toString
優化操作狀態欄位: CH_NOID

    public static Collector<CharSequence, ?, String> joining() {
        return new CollectorImpl<CharSequence, StringBuilder, String>(
                StringBuilder::new, StringBuilder::append,
                (r1, r2) -> { r1.append(r2); return r1; },
                StringBuilder::toString, CH_NOID);
    }複製程式碼

下面來個複雜的

groupingBy()

groupingBytoMap的一種高階方式,彌補了toMap對值無法提供多元化的收集操作,比如對於返回Map<T,List<E>>這樣的形式toMap就不是那麼順手,那麼groupingBy的重點就是對Key和Value值的處理封裝.分析如下程式碼,其中classifier是對key值的處理,mapFactory則是指定Map的容器具體型別,downstream為對Value的收集操作,具體程式碼這裡不做分析,無非是把值一個一個的put進指定容器.

   public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
       .......
    }複製程式碼

對於之前用原生collect方法做的收集操作那麼就可以很容易改寫為groupBy形式

//原生形式
   Lists.<Person>newArrayList().stream()
        .collect(() -> new HashMap<Integer,List<Person>>(),
            (h, x) -> {
              List<Person> value = h.getOrDefault(x.getType(), Lists.newArrayList());
              value.add(x);
              h.put(x.getType(), value);
            },
            HashMap::putAll
        );
//groupBy形式
Lists.<Person>newArrayList().stream()
        .collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.toList()));
//因為對值有了操作,因此我可以更加靈活的對值進行轉換
Lists.<Person>newArrayList().stream()
        .collect(Collectors.groupingBy(Person::getType, HashMap::new, Collectors.mapping(Person::getName,Collectors.toSet())));複製程式碼

reducing()

reducing是針對單個值的收集,其返回結果不是集合家族的型別,而是單一的實體類T
容器: boxSupplier(identity),這裡包裹用的是一個長度為1的Object[]陣列,至於原因自然是不可變型別的鍋
加入容器操作: a[0] = op.apply(a[0], t)
多容器合併: a[0] = op.apply(a[0], b[0]); return a;
聚合後的結果操作: 結果自然是Object[0]所包裹的資料a -> a[0]
優化操作狀態欄位: CH_NOID
那麼看到這裡困惑是不是有一種恍然大悟的感覺,反正我是有的.

  public static <T> Collector<T, ?, T>
    reducing(T identity, BinaryOperator<T> op) {
        return new CollectorImpl<>(
                boxSupplier(identity),
                (a, t) -> { a[0] = op.apply(a[0], t); },
                (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
                a -> a[0],
                CH_NOID);
    }複製程式碼

那麼接下來就是對之前Collect的一些操作的改造

//原生操作
final Integer[] integers = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream()
        .collect(() -> new Integer[]{0}, (a, x) -> a[0] += x, (a1, a2) -> a1[0] += a2[0]);
//reducing操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream()
        .collect(Collectors.reducing(0, Integer::sum));    
//當然Stream也提供了reduce操作
final Integer collect = Lists.newArrayList(1, 2, 3, 4, 5)
        .stream().reduce(0, Integer::sum)複製程式碼

可能遇到的問題

記錄下生產中使用該工具遇到的一些小錯誤

toMap所產生的異常

toMap的操作主要如下程式碼,異常來自兩個方面

  1. 操作呼叫的是map.merge方法,該方法遇到value為null的情況會報npe,即使你使用的是hashMap可以接受null值,也照樣報.搞不懂這裡為什麼這樣設計.
  2. 未指定衝突合併策略,也就是第三個引數BinaryOperator<U> mergeFunction時遇到重複的key會直接拋IllegalStateException,因此需要注意.

總結

到此對於collect的操作應該就很清晰了,希望通過這些例子能掌握核心,也就是Collector介面中那幾個函式的作用,希望對你有幫助.

個人部落格 mrdear.cn ,歡迎交流

相關文章