大家好,又见面了。
在我后面的文章《吃透JAVA的Stream流操作,多年实际总结》中呢,对Stream的整体状况进行了粗疏全面的解说,也大略介绍了下后果收集器Collectors
的常见用法 —— 但远不是全副。
本篇文章就来专门分析collect操作,一起解锁更多高级玩法,让Stream操作真正的成为咱们编码中的神兵利器。
初识Collector
先看一个简略的场景:
现有团体内所有人员列表,须要从中筛选出上海子公司的全副人员
假设人员信息数据如下:
姓名 | 子公司 | 部门 | 年龄 | 工资 |
---|---|---|---|---|
大壮 | 上海公司 | 研发一部 | 28 | 3000 |
二牛 | 上海公司 | 研发一部 | 24 | 2000 |
铁柱 | 上海公司 | 研发二部 | 34 | 5000 |
翠花 | 南京公司 | 测试一部 | 27 | 3000 |
玲玲 | 南京公司 | 测试二部 | 31 | 4000 |
如果你已经用过Stream流,或者你看过我后面对于Stream用法介绍的文章,那么借助Stream能够很轻松的实现上述诉求:
public void filterEmployeesByCompany() { List<Employee> employees = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.toList()); System.out.println(employees);}
上述代码中,先创立流,而后通过一系列两头流操作(filter
办法)进行业务层面的解决,而后经由终止操作(collect
办法)将解决后的后果输入为List对象。
但咱们理论面对的需要场景中,往往会有一些更简单的诉求,比如说:
现有团体内所有人员列表,须要从中筛选出上海子公司的全副人员,并依照部门进行分组
其实也就是加了个新的分组诉求,那就是先依照后面的代码实现逻辑根底上,再对后果进行分组解决就好咯:
public void filterEmployeesThenGroup() { // 先 筛选 List<Employee> employees = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.toList()); // 再 分组 Map<String, List<Employee>> resultMap = new HashMap<>(); for (Employee employee : employees) { List<Employee> groupList = resultMap .computeIfAbsent(employee.getDepartment(), k -> new ArrayList<>()); groupList.add(employee); } System.out.println(resultMap);}
仿佛也没啥故障,置信很多同学理论编码中也是这么解决的。但其实咱们也能够应用Stream操作间接实现:
public void filterEmployeesThenGroupByStream() { Map<String, List<Employee>> resultMap = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.groupingBy(Employee::getDepartment)); System.out.println(resultMap);}
两种写法都能够失去雷同的后果:
{ 研发二部=[Employee(subCompany=上海公司, department=研发二部, name=铁柱, age=34, salary=5000)], 研发一部=[Employee(subCompany=上海公司, department=研发一部, name=大壮, age=28, salary=3000), Employee(subCompany=上海公司, department=研发一部, name=二牛, age=24, salary=2000)]}
上述2种写法相比而言,第二种是不是代码上要简洁很多?而且是不是有种自正文的滋味了?
通过collect办法的正当失当利用,能够让Stream适应更多理论的应用场景,大大的晋升咱们的开发编码效率。上面就一起来全面意识下collect、解锁更多高级操作吧。
collect\Collector\Collectors区别与关联
刚接触Stream收集器的时候,很多同学都会被collect
,Collector
,Collectors
这几个概念搞的昏头昏脑,甚至还有很多人即便曾经应用Stream好多年,也只是晓得collect外面须要传入相似Collectors.toList()
这种简略的用法,对其背地的细节也不甚了解。
这里以一个collect收集器最简略的应用场景来分析阐明下其中的关系:
概括来说:
1️collect
是Stream流的一个终止办法,会应用传入的收集器(入参)对后果执行相干的操作,这个收集器必须是Collector接口
的某个具体实现类
2️Collector
是一个接口,collect办法的收集器是Collector接口的具体实现类
3️Collectors
是一个工具类,提供了很多的动态工厂办法,提供了很多Collector接口的具体实现类,是为了不便程序员应用而预置的一些较为通用的收集器(如果不应用Collectors类,而是本人去实现Collector接口,也能够)。
Collector应用与分析
到这里咱们能够看出,Stream后果收集操作的实质,其实就是将Stream中的元素通过收集器定义的函数解决逻辑进行加工,而后输入加工后的后果。
依据其执行的操作类型来划分,又可将收集器分为几种不同的大类:
上面别离论述下。
恒等解决Collector
所谓恒等解决,指的就是Stream的元素在通过Collector函数解决前后齐全不变,例如toList()
操作,只是最终将后果从Stream中取出放入到List对象中,并没有对元素自身做任何的更改解决:
恒等解决类型的Collector是理论编码中最常被应用的一种,比方:
list.stream().collect(Collectors.toList());list.stream().collect(Collectors.toSet());list.stream().collect(Collectors.toCollection());
归约汇总Collector
对于归约汇总类的操作,Stream流中的元素一一遍历,进入到Collector处理函数中,而后会与上一个元素的处理结果进行合并解决,并失去一个新的后果,以此类推,直到遍历实现后,输入最终的后果。比方Collectors.summingInt()
办法的解决逻辑如下:
比方本文结尾举的例子,如果须要计算上海子公司每个月须要领取的员工总工资,应用Collectors.summingInt()
能够这么实现:
public void calculateSum() { Integer salarySum = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.summingInt(Employee::getSalary)); System.out.println(salarySum);}
须要留神的是,这里的汇总计算
,不单单只数学层面的累加汇总,而是一个狭义上的汇总概念,行将多个元素进行解决操作,最终生成1个后果的操作,比方计算Stream
中最大值的操作,最终也是多个元素中,最终失去一个后果:
还是用之前举的例子,当初须要晓得上海子公司外面工资最高的员工信息,咱们能够这么实现:
public void findHighestSalaryEmployee() { Optional<Employee> highestSalaryEmployee = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))); System.out.println(highestSalaryEmployee.get());}
因为这里咱们要演示collect
的用法,所以用了上述的写法。理论的时候JDK为了方便使用,也提供了上述逻辑的简化封装,咱们能够间接应用max()
办法来简化,即上述代码与上面的写法等价:
public void findHighestSalaryEmployee2() { Optional<Employee> highestSalaryEmployee = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .max(Comparator.comparingInt(Employee::getSalary)); System.out.println(highestSalaryEmployee.get());}
分组分区Collector
Collectors工具类中提供了groupingBy
办法用来失去一个分组操作Collector,其外部解决逻辑能够参见下图的阐明:
groupingBy()
操作须要指定两个要害输出,即分组函数和值收集器:
- 分组函数:一个处理函数,用于基于指定的元素进行解决,返回一个用于分组的值(即分组后果HashMap的Key值),对于通过此函数解决后返回值雷同的元素,将被调配到同一个组里。
- 值收集器:对于分组后的数据元素的进一步解决转换逻辑,此处还是一个惯例的Collector收集器,和collect()办法中传入的收集器齐全等同(能够想想俄罗斯套娃,一个概念)。
对于groupingBy
分组操作而言,分组函数与值收集器二者必不可少。为了方便使用,在Collectors工具类中,提供了两个groupingBy
重载实现,其中有一个办法只须要传入一个分组函数即可,这是因为其默认应用了toList()作为值收集器:
例如:仅仅是做一个惯例的数据分组操作时,能够仅传入一个分组函数即可:
public void groupBySubCompany() { // 依照子公司维度将员工分组 Map<String, List<Employee>> resultMap = getAllEmployees().stream() .collect(Collectors.groupingBy(Employee::getSubCompany)); System.out.println(resultMap);}
这样collect返回的后果,就是一个HashMap
,其每一个HashValue的值为一个List类型
。
而如果不仅须要分组,还须要对分组后的数据进行解决的时候,则须要同时给定分组函数以及值收集器:
public void groupAndCaculate() { // 依照子公司分组,并统计每个子公司的员工数 Map<String, Long> resultMap = getAllEmployees().stream() .collect(Collectors.groupingBy(Employee::getSubCompany, Collectors.counting())); System.out.println(resultMap);}
这样就同时实现了分组与组内数据的解决操作:
{南京公司=2, 上海公司=3}
下面的代码中Collectors.groupingBy()
是一个分组Collector,而其内又传入了一个归约汇总Collector Collectors.counting()
,也就是一个收集器中嵌套了另一个收集器。
除了上述演示的场景外,还有一种非凡的分组操作,其分组的key类型仅为布尔值,这种状况,咱们也能够通过Collectors.partitioningBy()
提供的分区收集器来实现。
例如:
统计上海公司和非上海公司的员工总数, true示意是上海公司,false示意非上海公司
应用分区收集器的形式,能够这么实现:
public void partitionByCompanyAndDepartment() { Map<Boolean, Long> resultMap = getAllEmployees().stream() .collect(Collectors.partitioningBy(e -> "上海公司".equals(e.getSubCompany()), Collectors.counting())); System.out.println(resultMap);}
后果如下:
{false=2, true=3}
Collectors.partitioningBy()
分区收集器的应用形式与Collectors.groupingBy()
分组收集器的应用形式雷同。单纯从应用维度来看,分组收集器的分组函数
返回值为布尔值
,则成果等同于一个分区收集器。
Collector的叠加嵌套
有的时候,咱们须要依据先依据某个维度进行分组后,再依据第二维度进一步的分组,而后再对分组后的后果进一步的解决操作,这种场景外面,咱们就能够通过Collector收集器的叠加嵌套应用来实现。
例如上面的需要:
现有整个团体整体员工的列表,须要统计各子公司内各部门下的员工人数。
应用Stream的嵌套Collector,咱们能够这么实现:
public void groupByCompanyAndDepartment() { // 依照子公司+部门双层维度,统计各个部门内的人员数 Map<String, Map<String, Long>> resultMap = getAllEmployees().stream() .collect(Collectors.groupingBy(Employee::getSubCompany, Collectors.groupingBy(Employee::getDepartment, Collectors.counting()))); System.out.println(resultMap);}
能够看下输入后果,达到了需要预期的诉求:
{ 南京公司={ 测试二部=1, 测试一部=1}, 上海公司={ 研发二部=1, 研发一部=2}}
下面的代码中,就是一个典型的Collector嵌套解决的例子,同时也是一个典型的多级分组的实现逻辑。对代码的整体处理过程进行分析,大抵逻辑如下:
借助多个Collector嵌套应用,能够让咱们解锁很多简单场景解决能力。你能够将这个操作设想为一个套娃操作,如果违心,你能够有限嵌套上来(理论中不太可能会有如此荒诞的场景)。
Collectors提供的收集器
为了不便程序员应用呢,JDK中的Collectors工具类封装提供了很多现成的Collector实现类,可供编码时间接应用,对罕用的收集器介绍如下:
办法 | 含意阐明 |
---|---|
toList | 将流中的元素收集到一个List中 |
toSet | 将流中的元素收集到一个Set中 |
toCollection | 将流中的元素收集到一个Collection中 |
toMap | 将流中的元素映射收集到一个Map中 |
counting | 统计流中的元素个数 |
summingInt | 计算流中指定int字段的累加总和。针对不同类型的数字类型,有不同的办法,比方summingDouble等 |
averagingInt | 计算流中指定int字段的平均值。针对不同类型的数字类型,有不同的办法,比方averagingLong等 |
joining | 将流中所有元素(或者元素的指定字段)字符串值进行拼接,能够指定拼接连接符,或者首尾拼接字符 |
maxBy | 依据给定的比拟器,抉择出值最大的元素 |
minBy | 依据给定的比拟器,抉择出值最小的元素 |
groupingBy | 依据给定的分组函数的值进行分组,输入一个Map对象 |
partitioningBy | 依据给定的分区函数的值进行分区,输入一个Map对象,且key始终为布尔值类型 |
collectingAndThen | 包裹另一个收集器,对其后果进行二次加工转换 |
reducing | 从给定的初始值开始,将元素进行一一的解决,最终将所有元素计算为最终的1个值输入 |
上述的大部分办法,后面都有应用示例,这里对collectAndThen
补充介绍下。
collectAndThen
对应的收集器,必须传入一个真正用于后果收集解决的理论收集器downstream以及一个finisher办法,当downstream收集器计算出后果后,应用finisher办法对后果进行二次解决,并将处理结果作为最终后果返回。
还是拿之前的例子来举例:
给定团体所有员工列表,找出上海公司中工资最高的员工。
咱们能够写出如下代码:
public void findHighestSalaryEmployee() { Optional<Employee> highestSalaryEmployee = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect(Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))); System.out.println(highestSalaryEmployee.get());}
然而这个后果最终输入的是个Optional<Employee>
类型,应用的时候比拟麻烦,那能不能间接返回咱们须要的Employee
类型呢?这里就能够借助collectAndThen
来实现:
public void testCollectAndThen() { Employee employeeResult = getAllEmployees().stream() .filter(employee -> "上海公司".equals(employee.getSubCompany())) .collect( Collectors.collectingAndThen( Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)), Optional::get) ); System.out.println(employeeResult);}
这样就能够啦,是不是超简略的?
开发个自定义收集器
后面咱们演示了很多Collectors工具类中提供的收集器的用法,上一节中列出来的Collectors提供的罕用收集器,也能够笼罩大部分场景的开发诉求了。
但兴许在我的项目中,咱们会遇到一些定制化的场景,现有的收集器无奈满足咱们的诉求,这个时候,咱们也能够本人来实现定制化的收集器。
Collector接口介绍
咱们晓得,所谓的收集器,其实就是一个Collector接口的具体实现类。所以如果想要定制本人的收集器,首先要先理解Collector接口到底有哪些办法须要咱们去实现,以及各个办法的作用与用处。
当咱们新建一个MyCollector
类并申明实现Collector接口的时候,会发现须要咱们实现5个
接口:
这5个接口的含意阐明归纳如下:
接口名称 | 性能含意阐明 |
---|---|
supplier | 创立新的后果容器,能够是一个容器,也能够是一个累加器实例,总之是用来存储后果数据的 |
accumlator | 元素进入收集器中的具体解决操作 |
finisher | 当所有元素都解决实现后,在返回后果前的对后果的最终解决操作,当然也能够抉择不做任何解决,间接返回 |
combiner | 各个子流的处理结果最终如何合并到一起去,比方并行流解决场景,元素会被切分为好多个分片进行并行处理,最终各个分片的数据须要合并为一个整体后果,即通过此办法来指定子后果的合并逻辑 |
characteristics | 对此收集器解决行为的补充形容,比如此收集器是否容许并行流中解决,是否finisher办法必须要有等等,此处返回一个Set汇合,外面的候选值是固定的几个可选项。 |
对于characteristics
返回set汇合中的可选值,阐明如下:
取值 | 含意阐明 |
---|---|
UNORDERED | 申明此收集器的汇总归约后果与Stream流元素遍历程序无关,不受元素解决程序影响 |
CONCURRENT | 申明此收集器能够多个线程并行处理,容许并行流中进行解决 |
IDENTITY_FINISH | 申明此收集器的finisher办法是一个恒等操作,能够跳过 |
当初,咱们晓得了这5个接口办法各自的含意与用处了,那么作为一个Collector收集器,这几个接口之间是如何配合解决并将Stream数据收集为须要的输入后果的呢?上面这张图能够清晰的论述这一过程:
当然,如果咱们的Collector是反对在并行流中应用的,则其处理过程会稍有不同:
为了对上述办法有个直观的了解,咱们能够看下Collectors.toList()
这个收集器的实现源码:
static final Set<Collector.Characteristics> CH_ID = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));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);}
对上述代码拆解剖析如下:
- supplier办法:
ArrayList::new
,即new了个ArrayList
作为后果存储容器。 - accumulator办法:
List::add
,也就是对于stream中的每个元素,都调用list.add()
办法增加到后果容器追踪。 - combiner办法:
(left, right) -> { left.addAll(right); return left; }
,也就是对于并行操作生成的各个子ArrayList
后果,最终通过list.addAll()
办法合并为最终后果。 - finisher办法:没提供,应用的默认的,因为无需做任何解决,属于恒等操作。
- characteristics:返回的是
IDENTITY_FINISH
,也即最终后果间接返回,无需finisher办法去二次加工。留神这里没有申明CONCURRENT
,因为ArrayList是个非线程平安的容器,所以这个收集器是不反对在并发过程中应用。
通过下面的一一办法形容,再联想下Collectors.toList()
的具体表现,想必对各个接口办法的含意应该有了比拟直观的了解了吧?
实现Collector接口
既然曾经搞清楚Collector接口中的次要办法作用,那就能够开始入手写本人的收集器啦。新建一个class类,而后申明实现Collector接口,而后去实现具体的接口办法就行咯。
后面介绍过,Collectors.summingInt
收集器是用来计算每个元素中某个int类型字段的总和的,假如咱们须要一个新的累加性能:
计算流中每个元素的某个int字段值平方的总和
上面,咱们就一起来自定义一个收集器来实现此性能。
- supplier办法
supplier办法的职责,是创立一个后果存储累加的容器。既然咱们要计算多个值的累加后果,那首先就是要先申明一个int sum = 0
用来存储累加后果。然而为了让咱们的收集器能够反对在并发模式下应用,咱们这里能够采纳线程平安的AtomicInteger来实现。
所以咱们便能够确定supplier办法的实现逻辑了:
@Overridepublic Supplier<AtomicInteger> supplier() { // 指定用于最终后果的收集,此处返回new AtomicInteger(0),后续在此基础上累加 return () -> new AtomicInteger(0);}
- accumulator办法
accumulator
办法是实现具体的计算逻辑的,也是整个Collector的外围业务逻辑所在的办法。收集器解决的时候,Stream流中的元素会一一进入到Collector中,而后由accumulator
办法来进行一一计算:
@Overridepublic BiConsumer<AtomicInteger, T> accumulator() { // 每个元素进入的时候的遍历策略,以后元素值的平方与sum后果进行累加 return (sum, current) -> { int intValue = mapper.applyAsInt(current); sum.addAndGet(intValue * intValue); };}
这里也补充说下,收集器中的几个办法中,仅有accumulator
是须要反复执行的,有几个元素就会执行几次,其余的办法都不会间接与Stream中的元素打交道。
- combiner办法
因为咱们后面supplier办法中应用了线程平安的AtomicInteger作为后果容器,所以其反对在并行流中应用。依据下面介绍,并行流是将Stream切分为多个分片,而后别离对分片进行计算解决失去分片各自的后果,最初这些分片的后果须要合并为同一份总的后果,这个如何合并,就是此处咱们须要实现的:
@Overridepublic BinaryOperator<AtomicInteger> combiner() { // 多个分段后果解决的策略,间接相加 return (sum1, sum2) -> { sum1.addAndGet(sum2.get()); return sum1; };}
因为咱们这里是要做一个数字平方的总和,所以这里对于分片后的后果,咱们间接累加到一起即可。
- finisher办法
咱们的收集器指标后果是输入一个累加的Integer
后果值,然而为了保障并发流中的线程平安,咱们应用AtomicInteger作为了后果容器。也就是最终咱们须要将外部的AtomicInteger
对象转换为Integer对象,所以finisher
办法咱们的实现逻辑如下:
@Overridepublic Function<AtomicInteger, Integer> finisher() { // 后果解决实现之后对后果的二次解决 // 为了反对多线程并发解决,此处外部应用了AtomicInteger作为了后果累加器 // 然而收集器最终须要返回Integer类型值,此处进行对后果的转换 return AtomicInteger::get;}
- characteristics办法
这里呢,咱们申明下该Collector收集器的一些个性就行了:
- 因为咱们实现的收集器是容许并行流中应用的,所以咱们申明了
CONCURRENT
属性; - 作为一个数字累加算总和的操作,对元素的先后计算程序并没有关系,所以咱们也同时申明
UNORDERED
属性; - 因为咱们的finisher办法外面是做了个后果解决转换操作的,并非是一个恒等解决操作,所以这里就不能申明
IDENTITY_FINISH
属性。
基于此剖析,此办法的实现如下:
@Overridepublic Set<Characteristics> characteristics() { Set<Characteristics> characteristics = new HashSet<>(); // 指定该收集器反对并发解决(后面也发现咱们采纳了线程平安的AtomicInteger形式) characteristics.add(Characteristics.CONCURRENT); // 申明元素数据处理的先后顺序不影响最终收集的后果 characteristics.add(Characteristics.UNORDERED); // 留神:这里没有增加上面这句,因为finisher办法对后果进行了解决,非恒等转换 // characteristics.add(Characteristics.IDENTITY_FINISH); return characteristics;}
这样呢,咱们的自定义收集器就实现好了,如果须要残缺代码,能够到文末的github仓库地址上获取。
咱们应用下本人定义的收集器看看:
public void testMyCollector() { Integer result = Stream.of(new Score(1), new Score(2), new Score(3), new Score(4)) .collect(new MyCollector<>(Score::getScore)); System.out.println(result);}
输入后果:
30
完全符合咱们的预期,自定义收集器就实现好了。回头再看下,是不是挺简略的?
总结
好啦,对于Java中Stream的collect用法与Collector收集器的内容,这里就给大家分享到这里咯。看到这里,不晓得你是否把握了呢?是否还有什么疑难或者更好的见解呢?欢送多多留言切磋交换。
此外:
- 对于本文中波及的演示代码的残缺示例,我曾经整顿并提交到github中,如果您有须要,能够自取:https://github.com/veezean/JavaBasicSkills
我是悟道,聊技术、又不仅仅聊技术~
如果感觉有用,请点个关注,也能够关注下我的公众号【架构悟道】,获取更及时的更新。
期待与你一起探讨,一起成长为更好的本人。