classifier
:键映射:该方法的返回值是键值对的 键mapFactory
:无参构造函数提供返回类型:提供一个容器初始化方法,用于创建新的 Map容器 (使用该容器存放值对)。容器类型只能设置为Map类型或者Map(M extends Mapdownstream
:值映射:通过聚合方法将同键下的结果聚合为指定类型,该方法返回的是键值对的 值。一般来说键值类型都可以根据需要自定义结果。Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)
Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier, Supplier<M> mapFactory, Collector<? super T, A, D> downstream)
以下为本次测试数据集合:
@Data
class Student {
private String name;
private Integer age;
private String clazz;
private String course;
private Integer score;
public Student(String name, Integer age, String clazz, String course, Integer score) {
this.name = name;
this.age = age;
this.clazz = clazz;
this.course = course;
this.score = score;
}
}
List<Student> students = Arrays.asList(
new Student("小张",16,"高一1班","历史",88),
new Student("小李",16,"高一3班","数学",12),
new Student("小王",17,"高二1班","地理",44),
new Student("小红",18,"高二1班","物理",67),
new Student("李华",15,"高二2班","数学",99),
new Student("小潘",19,"高三4班","英语",100),
new Student("小聂",20,"高三4班","物理",32)
);
// 将不同课程的学生进行分类
Map<String, List<Student>> groupByCourse = students.stream().collect(Collectors.groupingBy(Student::getCourse));
Map<String, List<Student>> groupByCourse1 = students.stream().collect(Collectors.groupingBy(Student::getCourse, Collectors.toList()));
// 上面的方法中容器类型和值类型都是默认指定的,容器类型为:HashMap,值类型为:ArrayList
// 可以通过下面的方法自定义返回结果、值的类型
Map<String, List<Student>> groupByCourse2 = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, HashMap::new, Collectors.toList()));
一般我们根据id就能实现分组,但有些时候用不上id,需要根据其他两个字段进行分组,这里就用到了自定义键分组。
在这之前,我们初始数据新加一条
new Student(“小狗”,16,“高一1班”,“历史”,88)
// 字段映射 分组显示每个课程的学生信息
Map<String, List<Student>> combineFiledKey = students.stream().collect(Collectors.groupingBy(student -> student.getClazz() + "&" + student.getCourse()));
System.out.println(combineFiledKey);
我们通过班级和课程进行分组,通过结果发现,高一1班&历史
有两条记录。
有时候除了根据指定字段外,我们还需要根据对不同区间内的数据设置不同的键,区别于字段,这种范围类型的键多数情况下都是通过比较来生成的,常用于统计指标。
- 对是否有某种属性、类型进行统计
- 统计多个区间内的人数、比例
// 根据两级范围 将学生划分及格不及格两类
Map<Boolean, List<Student>> customRangeKey = students.stream().collect(Collectors.groupingBy(student -> student.getScore() > 60));
// 根据多级范围 根据学生成绩来评分
Map<String, List<Student>> customMultiRangeKey = students.stream().collect(Collectors.groupingBy(student -> {
if (student.getScore() < 60) {
return "C";
} else if (student.getScore() < 80) {
return "B";
}
return "A";
}));
有些时候分组完成后,需要对同一分组内的元素进行计算:计数、平均值、求和、最大最小值、范围内数据统计。
计数语法:
Collectorcounting()
// 计数
Map<String, Long> groupCount = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.counting()));
求和语法:
CollectorsummingInt(ToIntFunction super T> mapper)
CollectorsummingLong(ToLongFunction super T> mapper)
CollectorsummingDouble(ToDoubleFunction super T> mapper)
// 求和
Map<String, Integer> groupSum = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.summingInt(Student::getScore)));
平均值语法:
CollectoraveragingInt(ToIntFunction super T> mapper)
CollectoraveragingLong(ToLongFunction super T> mapper)
CollectoraveragingDouble(ToDoubleFunction super T> mapper)
平均值计算关注点:
// 增加平均值计算
Map<String, Double> groupAverage = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.averagingInt(Student::getScore)));
最大最少值语法:
CollectorminBy(Comparator super T> comparator)
CollectormaxBy(Comparator super T> comparator)
Collectors.collectingAndThen语法:
CollectorcollectingAndThen(Collector downstream, Function finisher)
- Function
:提供参数类型为R,返回结果类型为RR。
Collectors.minBy方法返回的类型为Optional>,在取数据时还需要校验Optional是否为空。不过这一步可以通过Collectors.collectingAndThen方法实现,并返回校验结果。Collectors.collectingAndThen的作用便是在使用聚合函数之后,对聚合函数的结果进行再加工。
// 同组最小值
Map<String, Optional<Student>> groupMin = students.stream()
.collect(Collectors.groupingBy(Student::getCourse,Collectors.minBy(Comparator.comparing(Student::getCourse))));
// 使用Collectors.collectingAndThen方法,处理Optional类型的数据
Map<String, Student> groupMin2 = students.stream()
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.collectingAndThen(Collectors.minBy(Comparator.comparing(Student::getCourse)), op ->op.orElse(null))));
// 同组最大值
Map<String, Optional<Student>> groupMax = students.stream()
.collect(Collectors.groupingBy(Student::getCourse,Collectors.maxBy(Comparator.comparing(Student::getCourse))));
完整统计语法:
CollectorsummarizingInt(ToIntFunction super T> mapper)
CollectorsummarizingLong(ToLongFunction super T> mapper)
CollectorsummarizingDouble(ToDoubleFunction super T> mapper)
统计方法提供了三种计算方式:Int、Double、Long。它会将输入元素转为上述三种计算方式的基本类型,然后进行计算。Collectors.summarizingXXX方法可以计算一般统计所需的所有结果。无法向下转型,即Long无法转Int等。返回结果取决于用的哪种计算方式
。
// 统计方法同时统计同组的最大值、最小值、计数、求和、平均数信息
HashMap<String, IntSummaryStatistics> groupStat = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, HashMap::new,Collectors.summarizingInt(Student::getScore)));
groupStat.forEach((k, v) -> {
// 返回结果取决于用的哪种计算方式
v.getAverage();
v.getCount();
v.getMax();
v.getMin();
v.getSum();
});
Collectors.partitioningBy语法:
Collector> partitioningBy(Predicate super T> predicate)
Collector> partitioningBy(Predicate super T> predicate, Collector super T, A, D> downstream)
- predicate:条件参数,对分组的结果划分为两个范围。
上面的统计都是基于某个指标项的。如果我们需要统计范围,比如:得分大于、小于60分的人的信息,那么我们可以通过Collectors.partitioningBy方法对映射结果进一步切分。
// 切分结果,同时统计大于60和小于60分的人的信息
Map<String, Map<Boolean, List<Student>>> groupPartition = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.partitioningBy(s -> s.getScore() > 60)));
// 同样的,我们还可以对上面两个分组的人数数据进行统计
Map<String, Map<Boolean, Long>> groupPartitionCount = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.partitioningBy(s -> s.getScore() > 60, Collectors.counting())));
Collectors.partitioningBy仅支持将数据划分为两个范围进行统计,如果需要划分多个,可以嵌套Collectors.partitioningBy执行,不过需要在执行完后,手动处理不需要的数据。也可以在第一次Collectors.partitioningBy获取结果后,再分别对该结果进行范围统计。
Map<String, Map<Boolean, Map<Boolean, List<Student>>>> groupAngPartitionCount = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.partitioningBy(s -> s.getScore() > 60,
Collectors.partitioningBy(s -> s.getScore() > 90))));
将同一个键下的值,通过不同的方法最后合并为一条数据。
Collectors.reducing语法:
Collectorreducing(BinaryOperator op)
Collectorreducing(T identity, BinaryOperator op)
Collectorreducing(U identity, Function super T, ? extends U> mapper, BinaryOperator op)
identity
:合并标识值(因子),它将参与累加函数和合并函数的运算(即提供一个默认值,在流为空时返回该值,当流不为空时,该值作为起始值,参与每一次累加或合并计算)mapper
:映射流中的某个元素,并根据此元素进行合并。op
:合并函数,将mapper映射的元素,进行两两合并,最初的一个元素将于合并标识值进行合并。
// 合并结果,计算每科总分
Map<String, Integer> groupCalcSum = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.reducing(0, Student::getScore, Integer::sum)));
// 合并结果,获取每科最高分的学生信息
Map<String, Optional<Student>> groupCourseMax = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.reducing(BinaryOperator.maxBy(Comparator.comparing(Student::getScore)))));
Collectors.joining语法:
Collectorjoining()
Collectorjoining(CharSequence delimiter)
Collectorjoining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)
delimiter
:分隔符prefix
:每个字符的前缀suffix
:每个字符的后缀
Collectors.joining只能对字符进行操作,因此一般会与其它downstream方法组合使用。
// 统计各科的学生姓名
Map<String, String> groupCourseSelectSimpleStudent = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(Student::getName, Collectors.joining(","))));
实际上Collectors.groupingBy的第三个参数downstream,其实就是就是将元素映射为不同的值。而且上面的所有功能都是基于downstream的。这一节,主要介绍一些方法来设置自定义值。
将结果映射为ArrayList:
CollectortoList()
将结果映射为HashSet:
CollectortoSet()
Collectors.mapping的功能比较丰富,除了可以将分组结果映射为自己想要的值外,还能组合上面提到的所有downstream方法。
Collectors.mapping语法:
Collectormapping(Function super T, ? extends U> mapper, Collector super U, A, R> downstream)
将结果映射为指定字段:
Map<String, List<String>> groupMapping = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(Student::getName, Collectors.toList())));
转换bean对象:
Map<String, List<OutstandingStudent>> groupMapping2 = students.stream()
.filter(s -> s.getScore() > 60)
.collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(s -> BeanUtil.copyProperties(s, OutstandingStudent.class), Collectors.toList())));
组合joining:
// 组合joining
Map<String, String> groupMapperThenJoin= students.stream()
.collect(Collectors.groupingBy(Student::getCourse, Collectors.mapping(Student::getName, Collectors.joining(","))));
// 利用collectingAndThen处理joining后的结果
Map<String, String> groupMapperThenLink = students.stream()
.collect(Collectors.groupingBy(Student::getCourse,
Collectors.collectingAndThen(Collectors.mapping(Student::getName, Collectors.joining(",")), s -> "学生名单:" + s)));
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, 2、Function<? super T, ? extends U> valueMapper)
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction, Supplier<M> mapSupplier)
将结果映射为HashMap或其他map类:
keyMapper
:key映射valueMapper
:value映射mergeFunction
:当流中的key重复时,提供的合并方式,默认情况下,将会抛出IllegalStateException异常。mapSupplier
:提供Map容器的无参初始化方式,可以自定义返回的Map容器类型。
Collectors.toConcurrentMap的语法同Collectors.toMap,不过他们仍然有一些区别:
- 前者默认返回ConcurrentHashMap,后者返回HashMap
- 在处理并行流中存在差异:toMap会多次调用mapSupplier,产生多个map容器,最后在通过Map.merge()合并起来,而toConcurrentMap则只会调用一次,并且该容器将会不断接受其他线程的调用以添加键值对。在并发情况下,toMap容器合并的性能自然是不如toConcurrentMap优秀的。
下面代码我们旨在建立姓名和分数的映射关系:
Map<String, Integer> collect = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getScore);
下面我们在数据集(末尾
)额外添加一条数据:
new Student(“小聂”,20,“高三4班”,“物理”,100)
原来已存在数据:
new Student(“小聂”,20,“高三4班”,“物理”,32),
再次运行出现java.lang.IllegalStateException: Duplicate key 32
错误。原来在使用java.util.stream.Collectors
类的 toMap()
方法转为 Map 集合时,一定要使用含有参数类型为BinaryOperator,参数名为mergeFunction 的方法
,否则当出现相同key值时会抛出 IllegalStateException 异常。其中参数 mergeFunction 的作用
是当出现 key 重复时
,自定义对value 的处理策略
。(k1, k2) -> k2
重复策略表示取最新的值
。所以此时小聂的分数为100。
Map<String, Integer> collect = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getScore, (k1, k2) -> k2));
多次运行后发现,每一次的结果都不是固定的,如果我们要保证元素的属性按照最开始的数据顺序我们可以指定第四个参数Supplier
LinkedHashMap<String, Integer> collect = students.stream().collect(
Collectors.toMap(Student::getName, Student::getScore, (k1, k2) -> k2, LinkedHashMap::new));
当然toMap任然可以和groupingBy一起使用。
HashMap<String, LinkedHashMap<String, Integer>> collect = students.stream()
.collect(Collectors.groupingBy(Student::getCourse, HashMap::new,
Collectors.toMap(Student::getName, Student::getScore, (k1, k2) -> k2, LinkedHashMap::new)));
可以发现当我们同时指定
了groupingBy的返回类型
和toMap的返回类型
两者并不冲突
。
自定义downstream参考:
【Java8 Stream】:探秘Stream实现的核心:Collector,模拟Stream的实现
groupingBy参考:
Stream Collectors.groupingBy的四种用法 解决分组统计(计数、求和、平均数等)、范围统计、分组合并、分组结果自定义映射等问题