前言

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收集器原理的更多相关文章

  1. Java8中重要的收集器Collector

    Collector介绍 Java8的stream api能很方便我们对数据进行统计分类等工作,函数式编程的风格让我们方便并且直观地编写统计代码. 例如: Stream<Integer> s ...

  2. java8学习之Collector源码分析与收集器核心

    之前已经对流在使用上已经进行了大量应用了,也就是说对于它的应用是比较熟悉了,但是比较欠缺的是对于它底层的实现还不太了解,所以接下来准备大量通过阅读官方的javadoc反过来加深对咱们已经掌握这些知识更 ...

  3. Java8学习笔记(十)--自定义收集器

    前言 以前写过Java8中的自定义收集器,当时只是在文章末尾放了个例子,觉得基本用法挺简单,而且有些东西没搞懂(比如combiner方法到底做什么的),没有专门写,过了一段时间又忘了,所以,即使还是没 ...

  4. JVM中常见的垃圾收集器

    垃圾收集机制是 Java 的招牌能力,极大地提高了开发效率.如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展, Java 的垃圾收集机制仍然在不断的演进中,不同大小的设备.不同特征的应用 ...

  5. java8学习之自定义收集器实现

    在上次花了几个篇幅对Collector收集器的javadoc进行了详细的解读,其涉及到的文章有: http://www.cnblogs.com/webor2006/p/8311074.html htt ...

  6. JVM实用参数(七)CMS收集器

    HotSpot JVM的并发标记清理收集器(CMS收集器)的主要目标就是:低应用停顿时间.该目标对于大多数交互式应用很重要,比如web应用.在我们看一下有关JVM的参数之前,让我们简要回顾CMS收集器 ...

  7. 学习JVM--垃圾回收(二)GC收集器

    1. 前言 在上一篇文章中,介绍了JVM中垃圾回收的原理和算法.介绍了通过引用计数和对象可达性分析的算法来筛选出已经没有使用的对象,然后介绍了垃圾收集器中使用的三种收集算法:标记-清除.标记-整理.标 ...

  8. 学习JVM-GC收集器

    1. 前言 在上一篇文章中,介绍了JVM中垃圾回收的原理和算法.介绍了通过引用计数和对象可达性分析的算法来筛选出已经没有使用的对象,然后介绍了垃圾收集器中使用的三种收集算法:标记-清除.标记-整理.标 ...

  9. JDK1.7 Update14 HotSpot虚拟机GC收集器

    在测试服务器上使用如下命令可以查看当前使用的 GC收集器,当然不止这一个命令可以看到,还有其他一些方式 第三列”=”表示第四列是参数的默认值,而”:=” 表明了参数被用户或者JVM赋值了 [csii@ ...

随机推荐

  1. Python - 面向对象编程 - 新式类和旧式类

    object object 是 Python 为所有对象提供的父类,默认提供一些内置的属性.方法:可以使用 dir 方法查看 新式类 以 object 为父类的类,推荐使用 在 Python 3.x ...

  2. Spring Cloud Eureka 实践(一)

    Spring Cloud Eureka是Spring Cloud Netflix微服务套件中的一部分,主要在Spring Cloud架构中提供服务注册发现的功能.那么是不是可以尝试在本地搭一个单例Eu ...

  3. CGLib 简析

    背景 JDK 动态代理存在的一些问题: 调用效率低 JDK 通过反射实现动态代理调用,这意味着低下的调用效率: 每次调用 Method.invoke() 都会检查方法的可见性.校验参数是否匹配,过程涉 ...

  4. GIT:创建、查看分支命令(git branch -vv)

    在开发过程中一般会用到Git进行版本管理,创建查看分支并与远程仓库交互是非常常见的操作. branch分支 是指在开发主线中分离出来的,做进一步开发而不影响到原来的主线. Git存储的不是一系列的更改 ...

  5. weblogic从ssrf到redis获取shell

    一.环境搭建和知识储备 1.1.影响版本 漏洞编号:CVE-2014-4210 weblogic 10.0.2.0 weblogic 10.3.6.0 1.2.Docker搭建环境 1.进入vulhu ...

  6. Vue组件传值(一)之 父子之间如何传值

    Vue中组件之间是如何实现通信的? 1.父传子: 父传子父组件通过属性进行传值,子组件通过 props 进行接受: 1 父组件中: 2 3 <template> 4 <div id= ...

  7. C# .NET Core 3.1 中 AssemblyLoadContext 的基本使用

    C# .NET Core 3.1 中 AssemblyLoadContext 的基本使用 前言 之前使用 AppDomain 写过一个动态加载和释放程序的案例,基本实现了自己"兔死狗烹&qu ...

  8. PHP中的对象比较

    在之前的文章中,我们讲过PHP中比较数组的时候发生了什么?.这次,我们来讲讲在对象比较的时候PHP是怎样进行比较的. 首先,我们先根据PHP文档来定义对象比较的方式: 同一个类的实例,比较属性大小,根 ...

  9. Spring Boot中如何配置线程池拒绝策略,妥善处理好溢出的任务

    通过之前三篇关于Spring Boot异步任务实现的博文,我们分别学会了用@Async创建异步任务.为异步任务配置线程池.使用多个线程池隔离不同的异步任务.今天这篇,我们继续对上面的知识进行完善和优化 ...

  10. vue实现事件代理(通过事件冒泡实现)

    事件代理/事件委托以ul>li来模拟 使用冒泡的用法:使用冒泡的用法来实现事件代理 分离出来 动态根据索引添加类名: