Java8关于Stream的收集器和Collector接口
从Stream类中的collect作为本文的开始。现在有一个装有Frog对象的List集合,从这个list中将frog对象的size值提取成一个新的集合,可以通过如下方法实现:
List<Double> sizeList = frogList.stream()
.map(Frog::getSize)
.collect(Collectors.toList());
Forg类
public class Frog {
private String name;
private Integer age;
private String color;
private Double size;
// 省略getter和setter等方法
}
上面代码大致意思为:stream方法将集合转化为流,map方法将frog对象的size值提取出来,collect用来将size的值再收集起来。前面两个方法都比较好理解,重点关注collect。collect方法的参数是一个Collector接口,表示接收一个收集器(一个收集的行为或者说按什么规则收集流中数据),按照这个行为方式将流中数据收集起来(行为参数化)。问题是这个行为该怎么定义,收集器执行流程是怎样?(一定要看到文末)
收集器伪流程图
图中c框中就是收集器要做的事情,分别对应Collector接口中几个方法:
1.supplier 构建容器
2.accumulator 收集元素
3.combiner 合并结果
4.finisher 转化为最终值
Collector接口
在分析Collector接口之前先看一下java8几个常用的函数式接口:
接口 | 参数 | 返回类型 | 说明 |
---|---|---|---|
Predicate | T | boolean | 输入某个值,输出boolean值,用于对某值进行判定 |
Consumer | T | void | 输入某值,无输出。用于消费某值 |
Function<T,R> | T | R | 输入某类型值,输出另种类型值,用于类型转化等 |
Supplier | T | 无输入,输出某值,用于生产某值 | |
UnaryOperator | T | T | 输入某类型值,输出同类型值,用于值的同类型转化,如对值进行四则运算等 |
BinaryOperator | (T,T) | T | 输入两个某类型值,输出一个同类型值,用于两值合并等 |
Collector接口的泛型为Collector<T, A, R>,其中第一个泛型T表示输入的类型,第二个A表示累计时的类型,第三个R表示最终的结果类型。
下面看一下接口中的几个方法:
supplier (容器)
该方法需要返回一个Supplier<A>接口,Supplier不需要输入,用于输出值,类似一个工厂,用来构建收集数据的容器。
accumulator (累加器)
该方法需要返回一个BiConsumer<A, T>接口,与表中Consumer功能一样,表示消费类型为A和T的值,在这里应该让输入的T类型的值累积到A类型的容器中。
combiner (合并)
该方法需要返回一个BinaryOperator<A>接口,这里表示将多个容器合并为一个容器。
finisher (修整)
该方法需要返回一个Function<A, R>接口,完成中间值到最终值得转换。
characteristics (特征)
该方法表示收集器应该有的特征,枚举中分别表示为CONCURRENT(并发)、UNORDERED(无序)、IDENTITY_FINISH(一致性完成,恒等函数功能)
toList()的实现
解释这么多还是难以理解,所以还是找个例子看看该如何构建一个收集器。
Collectors的内部类CollectorImpl实现了Collector接口,而Collectors的静态方法toList又使用了CollectorImpl的构造方法返回一个Collector,下面就通过toList来分析收集器如何诞生(其实应该理解为收集流中数据的这种行为如何定义或者是收集流中数据的步骤如何制定)。
1 首先构建实际类型为ArrayList的集合(前面所说的容器),用来存放流中元素。结合文章开头此时输入的元素是Frog的的size属性值,类型为Double,即泛型T应该为Double类型。
2 通过add方法将元素收集到List集合中,完成累加功能。
3 List中addAll方法是一个集合将另外一个集合中所有元素添加到自己内部,在这里代表完成了容器的合并。
4 指定收集特征为IDENTITY_FINISH,一致性完成,如恒等函数,输入即输出不做任何处理,那么输出为List<Double>类型。
5 中间少了修整这一步,默认为castingIdentity方法,即无修整。
自定义收集器
通过分析toList实现逻辑发现之前那些概念清晰了很多,可以着手实现一个自定义的收集器了。不需要在添加一个Collector接口的实现类,使用接口中的of方法可以便捷的返回一个Collector。
同时将业务场景升级一下,假设要得到一个这样的集合,将frogList先按name分组,分组后需要将每组的size收集成一个字符串集合,即需要一个<Map<String,List<String>>类型。
DecimalFormat df = new DecimalFormat("0.000000"); // 格式化
Map<String, ArrayList<Object>> frogMap = frogList.stream()
.collect(Collectors.groupingBy(
Frog::getName,
Collector.of(
ArrayList::new,
(list, frog) -> list.add(df.format(frog.getSize())),
(listA, listB) -> {
listA.addAll(listB);
return listA;
},
Collector.Characteristics.IDENTITY_FINISH
)));
这个自定义的收集器其实只是在double转string格式化的时候做了一个处理,还有一个更方便的处理方式:
DecimalFormat df = new DecimalFormat("0.000000");
Map<String, List<String>> collect = frogList.stream()
.collect(Collectors.groupingBy(
Frog::getName,
Collectors.mapping(
frog -> df.format(frog.getSize()),
Collectors.toList()))
);
相比之下后一种清晰了许多。所以除非特殊情况出现,大多数情况还是用不到自定义收集器的,只要善于挖掘这些已经有的方法就好。
(END)