深入介绍和使用Java8的Collector接口
- 一 .Collector
- 二. Collectors
- 2.1 toCollection 、toList、toSet
- 2.2 toMap
- 2.3 joining
- 2.4 mapping
- 2.5 collectingAndThen
- 2.6 counting、reducing、maxBy、minBy
- 2.7 summingInt,summingLong,summingDouble
- 2.8 averagingInt,averagingLong,averagingDouble
- 2.9 groupingBy groupingByConcurrent
- 2.10 summarizingInt,summarizingLong,summarizingDouble:汇总流中的元素,汇总后的类型分别是 IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics
- 三 举个例子
首先看这篇文章需要有一点Stream()的知识,lambda是最基本的使用规则,因为我们全文都在使用函数式接口
一 .Collector
不知道你看见这种代码眼熟吗,先通过将每个元素map处理后,直接一个collect方法就能拿到我们想要收集起来的类型,里面跟着一起写奇奇怪怪的方法,我们来看一下collect里面包含什么
一个Collector类型的对象,我们继续点进去看
Collector接口里面有五个具体的方法,其中四个为函数式接口对象
函数式接口可以被隐式转换为 lambda 表达式
添加了函数式接口注解表明BiConsumer为一个函数式接口,可以看到accept方法传入两个T,U类型的参数,没有返回值,可以说明这是一个消费形的接口,只是处理出入参数 类似于隐式(a,b)->{ System.out.printf(a,b);}
我们来看看这我一个方法具体做了哪些
- supplier参数用于生成结果容器,容器类型为A
- accumulator用于消费元素,也就是归纳元素,这里的T就是元素,它会将流中的元素一个一个与结果容器A发生操作
- combiner用于两个两个合并并行执行的线程的执行结果,将其合并为一个最终结果A
- finisher用于将之前整合完的结果R转换成为A
- characteristics表示当前Collector的特征值,这是个不可变Set
基础对象
@Data
@Builder
static class ObiectMap{
private String key;
private String value;
}
@Data
@Builder
static class NumObj{
private String key;
private Integer value;
}
我们直接实现一下这个接口,首先我们将list里面的数据收集起来作为对象,然后我们想把相同key的元素累加后收集起来起来Supplier我们提供一个HashMap,accumulator返回一个消费型接口,我们可以直接拿到第一步收集起来的容器,然后判断相同的key的时候累加起来value,combiner代表为并行收集后将并行数据处理,finisher将最后收集的元素类型转换
通过这些方法,我们可以拿到我们想要收集起来的任何类型,因为具体收集处理方法是你传入的,以前我们的接口只能传入参数对象,而引入函数式接口,我们可以直接将方法的实现传入,大大增加了我们方法的复用性
二. Collectors
很多场景下,我们并不需要像自己手写一些收集方法,很麻烦也不能复用,所以我们可以使用内置的Collectors工具类取实现Collector接口
返回类型都是 Collector 接口对象,各个方法用法如下
- toCollection,toList,toSet:处理并将结果返回成集合对象
- toMap: 处理并收集成Map
- joining:处理并将结果返回成字符串
- mapping:首先将流中的元素从 T 转成 U,然后再将含有 U 的流转给 downstream 继续处理
- collectingAndThen:首先将流传给 downstream 处理,然后将 downstream 中的 R 转成 RR,最后的结果为 RR
- counting:求流中元素的总数,类型为 Long
- minBy: 求流中最小的元素
- maxBy:求流中最大的元素
- summingInt,summingLong,summingDouble:求流中元素之和,和的类型分别是 int, long, double 类型
- averagingInt,averagingLong,averagingDouble:求流中元素的平均数,平均数的类型分别是 int, long, double 类型
- reducing:减少流中的元素
- groupingBy:将流中的元素分组处理
- groupingByConcurrent:以并行的方式将流中的元素分组处理
- partitioningBy:将流中的元素分区处理
- toMap, toConcurrentMap:处理并将结果返回成 Map 对象
- summarizingInt,summarizingLong,summarizingDouble:汇总流中的元素,汇总后的类型分别是 IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics
2.1 toCollection 、toList、toSet
传入一个Supplier容器,accumulator作用的是将Collection元素一个个加到容器中,combiner加入所有元素
这样看toList,toSet一下子就看的很明白了,List,Set也是继承了Collection接口,只不过是容器类型不一样,Supplier传入各自类型,accumulator传入各自累加方法,combiner传入并行处理方法
区分只是并行收集的SET类型传参不一样,具体如下
- CONCURRENT 如果使用 parallelStream,characteristics 方法返回的 set 集合中可以加上这个枚举
- UNORDERED 如果流中的数据是没有顺序的,也可以加上这个枚举
- IDENTITY_FINISH加上这个表示 finisher 方法中的参数和返回值完全一致,finisher 方法不会执行了
2.2 toMap
我们再来看一下toMap方法
传入两个Function对应Map的Key,Value,传入你自己想要的key和value即可,还传入一个mergeFunction,这个BinaryOperator接口对象继承了BiFunction,传入两个参数,返回一个参数,这个是为了防止冲突时的解决方案,我们进去merge方法去看
调用的是Map里的merge方法,处理冲突第一步,根据传入的key先拿到旧值,如果旧值为空就用新值替代,如果不为空就代表有冲突,这时候我们调用外部的merge方法解决冲突,返回一个解决后的值,如果此时value还为null,那就remove掉key,否则就put进map
此时继续调用底部方法,accumulator实现为上面解决冲突的方法,combiner也解决并行收集的冲突问题
2.3 joining
首先,joining方法返回可以按照我们想要的类型的字符串输出
每一个元素类型T CharSequence, 容器A StringBuilder, 返回R 类型String
首先创建一个StringBuilder容器,里面每个元素都append进去,并行收集时全部append,最终收集为String类型
2.4 mapping
类似于先拿到特定的值,与之传入的downstream发生关系
我们可以看到五个参数四个都是用的传入downstream的参数,只有中间accumulator将元素塞入容器的操作,用了特定的取值方法取值然后再作用于容器中
举个例子,我们想将根据key分组的valueList的值收集起来,不直接拿到对象list
2.5 collectingAndThen
将downstreamR元素拿到后转换成RR类型
finisher用的也是Function接口,我们直接调用andThen,可以看到先调用自己的apply方法后,调用传入finisher的apply方法
2.6 counting、reducing、maxBy、minBy
底层都是运用规约reducing接口,用于将一个一个元素相互作用后产生结果
identity 基准值
mapper 映射函数
op 操作函数
boxSupplier将基准值传入返回一个跟基准值类型的数组(并添加自己进入元素),accumulator对应元素与容器作用reduce会将所有值与容器中第一个元素作用,combiner并行会将所有并行数组第一个元素加到一起,finisher返回数组第一个元素
底层还是调用reducing接口
有可能传入空对象,所以会存在Optional类型的,BinaryOperator.maxBy(comparator)传入比较器,收集成OptionalBox类型的容器,调用外部比较器进行比较然后放到容器中,并行也进行比较,最终返回一个Optional类型的对象
minBy同理
counting
既然我们知道了底层reducing做的什么事情,那我们直接来看看counting,首先给了个初始基准值,防止出现Optional类型,也是我们容器的类型,然后将每个元素映射成1L,与数组中的基准值进行相加操作,返回一个最终相加的值,就是我们的统计数量
2.7 summingInt,summingLong,summingDouble
这几个接口就很容易理解了,初始容器值设置为int数组包含一个0,将数组中元素与applyAsInt元素相加,applyAsInt拿到对象的int值,最终返回相加后的元素,就是所有相加的值
2.8 averagingInt,averagingLong,averagingDouble
看起来更简单了,初始化long数组长度为2,第一个元素记录总值,第二元素个记录数量,最终返回一个总值/总数的平均值
2.9 groupingBy groupingByConcurrent
分组接口,我们最常用的应该就是这个了
如果你只传入分组的key,默认我们分组后的数据类型就是 Map<key,List(Object)>
搞清楚groupingBy对我们来说也是至关重要的
groupingByConcurrent用了ConcurrentHashMap解决并发收集线程问题
2.10 summarizingInt,summarizingLong,summarizingDouble:汇总流中的元素,汇总后的类型分别是 IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics
Collectors.summarizingInt 汇总所有信息包括数量、求和、平均值、最小值、最大值
三 举个例子
基础对象
@Data
@Builder
static class ObjectMap{
private String key;
private String value;
}
@Data
@Builder
static class NumObj{
private String key;
private Integer value;
}
一组数据
List<String> strings = Lists.newArrayList("name=kk", "sex=1", "tel=1111", "email=xx@qq.com","name=ww","name=yy");
3.1 通过ObjectMap对象的key进行分组,将value收集起来成list
System.out.println("map1: ");
Map<String, List<String>> collect1 = strings.stream().map(e -> { //封装成对象
String[] split = e.split("\\=", 2);
return ObjectMap.builder().key(split[0]).value(split[1]).build();
}).collect(Collectors.toMap(ObjectMap::getKey,v->Lists.newArrayList(v.getValue()), (List<String> newList, List<String> oldList) -> { // Collectors.toMap(a,b,(n1,n2)) -> a代表Map的key (这里直接用方法引用拿到key) b代表value (n1,n2)代表key相同时value的处理办法,直接合并List
oldList.addAll(newList);
return oldList;
}));
collect1.forEach((k,v)->{
System.out.printf(k+" { ");
String vList = v.stream().collect(Collectors.joining(",")); //加上逗号,最后一个不加
System.out.printf(vList);
System.out.printf(" }");
System.out.println();
});
System.out.println();
3.2 通过ObjectMap对象的key进行分组,将value收集起来成list 2.0版本
System.out.println("map2: ");
Map<String, List<String>> collect2 = strings.stream().map(e -> {
String[] split = e.split("\\=", 2);
return ObjectMap.builder().key(split[0]).value(split[1]).build();
}).collect(Collectors.groupingBy(ObjectMap::getKey, Collectors.mapping(ObjectMap::getValue, Collectors.toList()))); //Collectors.groupingBy(a,b) a还是通过key来分组 ,b将value收集起来做list value
collect2.forEach((k,v)->{
System.out.printf(k+" { ");
String vList = v.stream().collect(Collectors.joining(",")); //加上逗号,最后一个不加
System.out.printf(vList);
System.out.printf(" }");
System.out.println();
});
System.out.println();
3.3 通过ObjectMap对象的key进行分组,value为分组数量
System.out.println("map4: get name with count");
Map<String, Long> collect4 = strings.stream().map(e -> {
String[] split = e.split("\\=", 2);
return ObjectMap.builder().key(split[0]).value(split[1]).build();
}).collect(Collectors.groupingBy(ObjectMap::getKey, Collectors.counting())); // 拿到数量
collect4.forEach((k,v)->{
System.out.printf(k+" { ");
System.out.printf("%d",v);
System.out.printf(" }");
System.out.println();
});
System.out.println();
一组数据
List<String> specialString = Lists.newArrayList( "name=cc","sex=dd","name=bb","name=aa", "sex=aa","name=ww");
3.4 将相同key的value集合起来(对象list或者list),并按照指定字符串进行内部排序 例如aa > bb > cc (指定字符) 其他的排在后边
Map<String,Integer> keyMap=new HashMap(){{
put("aa",1);
put("bb",2);
put("cc",3);
}};
Map<String, List<String>> collect5 = specialString.stream().map(e -> { //封装成对象
String[] split = e.split("\\=", 2);
return ObjectMap.builder().key(split[0]).value(split[1]).build();
}).collect(Collectors.groupingBy(ObjectMap::getKey, Collectors.mapping(ObjectMap::getValue, toSortedList((o1, o2) -> { //groupingBy 可以有三个重载方法 三个参数分别是 1.分类器(通过什么方式分类) 2.封装类(默认hashmap 可以传一些其他Collector) 3.收集dowStream (默认tolist)
if (keyMap.get(o1) == null) { //Collectors.mapping 返回是一个Collector 里面可以指定通过特定值收集 toSortedList我们自定义一个收集器
return 1;
} else if (keyMap.get(o2) == null) {
return -1;
} else if (keyMap.get(o1) == null && keyMap.get(o2) == null) {
return 0;
}
return keyMap.get(o1).compareTo(keyMap.get(o2));
}))));
一组数据
List<String> numStr = Lists.newArrayList("aa=1", "bb=10", "cc=1111", "aa=66","cc=22","dd=101");
3.5 通过ObjectMap对象的key进行分组,获取每组最大的元素
System.out.println("map8: get sorted max element");
List<String> numStr = Lists.newArrayList("aa=1", "bb=10", "cc=1111", "aa=66","cc=22","dd=101");
Map<String, Integer> collect8 = numStr.stream().map(e -> {
String[] split = e.split("=");
return NumObj.builder().key(split[0]).value(Integer.parseInt(split[1])).build(); //老样子先转换成对象
}).collect(Collectors.groupingBy(NumObj::getKey // 然后通过groupingBy使用对象的KEY去分类 reducing(a,b,c)规约函数流由一个个元素组成,归约就是将一个个元素“折叠”成一个值,如求和、求最值、求平均值都是归约操作。
, Collectors.collectingAndThen(Collectors.reducing(0, NumObj::getValue, (c1, c2) -> { //collectingAndThen(a,b) 包装一层,不然外边拿到的是Optional类对象,
return c1>c2?c1:c2;
}), e -> e)));
collect8.forEach((k,v)->{
System.out.println(k+":"+v);
});