首页 > 其他 > 详细

Stream中的Collector收集器原理

时间:2020-07-29 01:57:57      阅读:78      评论:0      收藏:0      [点我收藏+]

前言

Stream的基本操作因为平时工作中用得非常多(也能看到一些同事把Stream操作写得很丑陋),所以基本用法就不写文章记录了。
之所以能把Stream的操作写得很丑陋,完全是因为Stream底层的一些东西不太明白。自己也需要注意。
本文就是介绍Collector的基本原理。以便加深自己的记忆。

自己对这个接口的定义(很强势):收集器、收集器,就是按照一定的规则(你可以任意实现它),把一个流里面的数据收集到一个容器里。

Collector接口源码与定义

我就不挨着挨着翻译源码里的注释了,太多。记一下自己的理解即可。

package java.util.stream;

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();

    enum Characteristics {
        CONCURRENT,
        UNORDERED,
        IDENTITY_FINISH
    }
}

Collector接口中有四个方法定义(每个方法的返回值都是一个JDK8默认的函数式接口)和一个枚举。

四大方法

  • Supplier<A> supplier()
    中文翻译:提供者
    在Collector中,这个方法的意义是返回一个可变的结果容器(result container),而是这个可变的结果容器(result container)的获取行为(别人毕竟是返回的Supplier对象),这里解释两点
    1)为什么叫结果容器?因为根据前言中自己的定义,需要一个容器来装东西。
    2)为什么是可变的?我们知道,流是由流源/中间操作/终止操作组合而构成的一个双向链表,只有终止操作执行的时候,才会把流源里的集合的数据执行中间操作和终止操作(忘记了,可以回忆下我的这篇Stream中的Pipeline理解)。所以当触发流的执行的时候,是一个一个从原始容器(流源中的)往新的结果容器中添加的,所以是可变的。

  • BiConsumer<A, T> accumulator()
    中文翻译:累加器
    回顾一下JDK 8中重要的函数式接口(必知必会)中的双参消费者,明白了这个累加器代表接收两个参数,返回void的行为(为什么老是强调行为行为,是因为函数式接口就代表一个行为,而不是简单意义的对象,虽然本质上是,但函数式编程的精髓不能这么理解。这个行为可以被传递,也可以被客户端实现)。
    这里简单说下返回值BiConsumer<A, T>,在收集器收集的过程中执行的是:【a1是结果容器,t1是流源的遍历对象】

A a1 = supplier.get();
accumulator.accept(a1, t1);
  • BinaryOperator<A> combiner()
    中文翻译:组合器
    回顾一下JDK 8中重要的函数式接口(必知必会)中的同类型双参操作,代表接收两个同类型参数,返回一个同类型参数
    如果一个流是并行流,它做收集的时候是基于JDK7的ForkJoin框架来分解与合并任务的,这个组合器就是对于并行流起作用的。
    后续再分析,目前我对ForkJoin也懂得不全面。

  • Function<A, R> finisher()
    中文翻译:终结者
    返回一个函数f(x)=y,输入一个参数,返回一个参数。
    对串行流来说,在所有的累加完成之后,把结果容器a1转换为r1的行为。
    对并行流来说,在组合完成之后,把组合结果x1转换为r1的行为。

talk is cheap,show me the code 不过这不是code,是JDK源码中的注释。我觉得这说明了一切。

      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));  // result with splitting

三大泛型

下面几行来自于JDK源码中的注释,从上面几行的代码也可以:

  • @param <T> the type of input elements to the reduction operation,输入过来的元素的类型
  • @param <A> the mutable accumulation type of the reduction operation (often hidden as an implementation detail) 结果容器的类型
  • @param <R> the result type of the reduction operation 执行最后终结操作的返回值

例子:
User类中有几个属性:id、username
如果流源是List,结果容器是ArrayList,收集器目的是收集所有用户的用户名,那么终结者的返回值就也是ArrayList.
那么:
<T> 为User
<A> 为ArrayList
<R> 为ArrayList

Characteristics枚举类

    enum Characteristics {
        CONCURRENT,
        UNORDERED,
        IDENTITY_FINISH
    }

下面几句话是我自己翻译的JDK源码的注释,如有错误请指正

  • Characteristics代表着可以在优化规约操作的一些特性。收集器可以指明这些特性。
  • CONCURRENT特性:代表这个收集器支持并发,这里并发指可以支持并发(多线程)的对同一个result container进行“累加操作”
  • 如果一个收集器具有CONCURRENT特性,但是没有UNORDERED特性,就意味着:这个收集器只会在应用到一个无序数据源(如Set)时,才会被并发的执行收集过程
  • UNORDERED特性:代表这个收集器在收集时不会保留它遇到的元素的顺序,如果一个收集器的结果容器(result container)是一个Set这样的无序容器,那么应该给你的结果容器设置UNORDERED这个特性。
  • IDENTITY_FINISH特性:代表这个终结者返回的Function对象是identity function.也就代表着如果设置了这个特性,将三大泛型中的泛型A转换为R不需要检查,必须被执行成功

Collector的同一性(Identity)与结合性(Associativity)

查看Collector的JDK源码文档可以看到

To ensure that sequential and parallel executions produce equivalent results, the collector functions must satisfy an identity and an associativity constraints.
为了确保串行和并行的执行得到相同的结果,收集器必须满足2个条件:同一性(identity)和结合性(associativity)

The identity constraint says that for any partially accumulated result, combining it with an empty result container must produce an equivalent result. That is, for a partially accumulated result a that is the result of any series of accumulator and combiner invocations, a must be equivalent to combiner.apply(a, supplier.get()).
同一性:在任何累加过程中,中间结果容器和一个空的结果容器合并,应该等于当前这个中间结果容器。即:a = combiner.apply(a, supplier.get())

The associativity constraint says that splitting the computation must produce an equivalent result. That is, for any input elements t1 and t2, the results r1 and r2 in the computation below must be equivalent:
结合性:指对源数据的分割的计算结果等价于不分割的计算结果。即:如下面代码的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));  // result with splitting

然后JDK官方文档链接了结合性的说明:JDK官方的结合性说明

Associativity
An operator or function op is associative if the following holds:

     (a op b) op c == a op (b op c)
 
The importance of this to parallel evaluation can be seen if we expand this to four terms:

     a op b op c op d == (a op b) op (c op d)
 
So we can evaluate (a op b) in parallel with (c op d), and then invoke op on the results.
Examples of associative operations include numeric addition, min, and max, and string concatenation.

Collector的复合(Composed)

Collectors are designed to be composed; many of the methods in Collectors are functions that take a collector and produce a new collector. For example, given the following collector that computes the sum of the salaries of a stream of employees :
Collector设计出来是支持复合的,在工具类java.util.stream.Collectors类中有很多方法,可以传入一个Collector,得到一个新的Collector。

先创建一个收集员工的工资总额的收集器。
Collector<Employee, ?, Integer> summingSalaries = Collectors.summingInt(Employee::getSalary))

然后,创建一个新的收集器,按照部门分组,计算所有员工的工资总额。就复用了summingSalaries收集器。
If we wanted to create a collector to tabulate the sum of salaries by department, we could reuse the "sum of salaries" logic using Collectors.groupingBy(Function, Collector):

     Collector<Employee, ?, Map<Department, Integer>> summingSalariesByDept
         = Collectors.groupingBy(Employee::getDepartment, summingSalaries);

自定义收集器实现

举一个自己工作中的例子,当时也是为了使用自定义的收集器而使用,后来发现,把收集逻辑独立出来,创建一个自定义收集器,在项目的深入进行,这些逻辑也是可以复用的。
所以以后有自定义收集的过程的时候,一定要单独把逻辑独立出来。

public class YourLogicCollector
        implements Collector<你自己的流源输入对象, Set<String>, Set<String>> {

    static final Set<Collector.Characteristics> CH_UNORDERED_ID
            = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED,
                                                     Collector.Characteristics.IDENTITY_FINISH));

    @Override
    public Supplier<Set<String>> supplier() {
        return HashSet::new;
    }

    @Override
    public BiConsumer<Set<String>, 你自己的流源输入对象> accumulator() {
        return (set, data) -> set.addAll(你自己的逻辑(data));
    }

    @Override
    public BinaryOperator<Set<String>> combiner() {
        return (left, right) -> {
            left.addAll(right);
            return left;
        };
    }

    @Override
    public Function<Set<String>, Set<String>> finisher() {
        return t -> t;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return CH_UNORDERED_ID;
    }

    private Set<String> 你自己的逻辑(YourInputEntity entity) {
        return 你自己的收集逻辑;
    }
}

Stream中的Collector收集器原理

原文:https://www.cnblogs.com/1626ace/p/13326519.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!