1. 前言
java.util.stream.Collectors 实现各种有用的缩减操作的Collector的实现,例如将元素累积到集合中,根据各种标准汇总元素等。
以下以示例操作展示Collectors的详细用法,多数的操作是可以嵌套组合的,根据实际需要得到相应的结果。
2. 示例数据
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
/** 姓名 */
private String name;
/** 昵称 */
private String nickname;
/** 总分 */
private Double score;
/** 年龄 */
private int age;
/** 是否本地人 */
private boolean local;
/** 年级 */
private GradeType gradeType;
/**
* 年级类型
*/
public enum GradeType {ONE,TWO,THREE}
}
List<Student> students = Arrays.asList(
new Student("刘一", "xiaoliu", 88.8, 12, true, Student.GradeType.THREE),
new Student("陈二", "xiaochen", 66.6, 15, true, Student.GradeType.THREE),
new Student("张三", "xiaozhang", 55.5, 16, true, Student.GradeType.THREE),
new Student("李四", "xiaoli", 59.5, 19, true, Student.GradeType.TWO),
new Student("王五", "xiaowang", 59.5, 15, false, Student.GradeType.THREE),
new Student("赵六", "xiaozhao", 56.65, 14, true, Student.GradeType.THREE),
new Student("孙七", "xiaosun", 44.69, 17, false, Student.GradeType.ONE));
3. 常用方法
1.平均值: averagingDouble、averagingInt、averagingLong
| 修饰符和类型 | 方法和描述 |
|---|---|
| static Collector<T,?,Double> | averagingDouble(ToDoubleFunction<? super T> mapper)Returns a Collector that produces the arithmetic mean of a double-valued function applied to the input elements.返回一个 Collector ,它产生应用于输入元素的双值函数的算术平均值。 |
| static Collector<T,?,Double> | averagingInt(ToIntFunction<? super T> mapper)返回一个 Collector ,它产生应用于输入元素的整数值函数的算术平均值。 |
| static Collector<T,?,Double> | averagingLong(ToLongFunction<? super T> mapper)返回一个 Collector ,它产生应用于输入元素的长值函数的算术平均值。 |
求学生分数的平均数
@Test
public void averagingDouble() {
Double averagingDouble = students.stream().collect(Collectors.averagingDouble(Student::getScore));
Optional.ofNullable(averagingDouble).ifPresent(System.out::println);
Double age = students.stream().collect(Collectors.averagingDouble(Student::getAge));
System.out.println(age);
}
//61.605714285714285
//16.714285714285715
注意:这三个方法的返回值都是
Double类型。
2.操作链:collectingAndThen
它是先对集合进行一次聚合操作,然后通过Function定义的函数,对聚合后的结果再次处理。
@Test
public void testcollectingAndThen() {
Map<Integer, Student> collect = students.stream()
.collect(Collectors.groupingBy(Student::getAge, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
System.out.println(JSON.toJSONString(collect));
}
@Test
public void testcollectingAndThen2() {
String collect = students.stream().map(Student::getNickname).collect(Collectors.collectingAndThen(Collectors.joining("-"), String::toUpperCase));
System.out.println(collect);
}
3.counting
counting方法返回一个Collector收集器接受T类型的元素,用于计算输入元素的数量。如果没有元素,则结果为0。
@Test
public void testCounting() {
Optional.of(menu.stream().collect(Collectors.counting())).ifPresent(System.out::println);
}
// 7
4.groupingBy
4.1 groupingBy(Function)
groupingBy(Function)方法返回一个Collector收集器对T类型的输入元素执行"group by"操作,根据分类函数对元素进行分组,并将结果返回到Map。 (类似数据库的groupBy操作,也可以通过此方式减少数据库压力)
groupingBy与toMap都是将聚合元素进行分组,区别是,toMap结果是 1:1 的 k-v 结构,groupingBy的结果是 1:n 的 k-v 结构。
如果想要线程安全的Map,可以使用groupingByConcurrent。
对Student的年龄分组
@Test
public void testgroupBy(){
Map<Integer, List<Student>>map=students.stream().collect(Collectors.groupingBy(Student::getAge));
System.out.println(map);
}
//不想value为List类型
@Test
public void testgroupBy2() {
Map<Integer, Set<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.toSet()));
System.out.println(map);
}
//对比toMap
@Test
public void testmap() {
Map<String, Student> collect = students.stream().collect(Collectors.toMap(Student::getNickname, Function.identity(), (x, y) -> x));
System.out.println(collect);
}
4.2 groupingBy(Function, Collector)
groupingBy(Function, Collector)方法返回一个Collector收集器,对T类型的输入元素执行级联"group by"操作,根据分类函数对元素进行分组,然后使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。
@Test
public void testGroupingByFunctionAndCollector() {
Optional.of(students.stream().collect(Collectors.groupingBy(Student::getGradeType, Collectors.counting()))).ifPresent(System.out::println);
}
// {THREE=5, ONE=1, TWO=1}
4.3 groupingBy(Function, Supplier, Collector)
groupingBy(Function, Supplier, Collector)方法返回一个Collector收集器,对T类型的输入元素执行级联"group by"操作,根据分类函数对元素进行分组, 然后使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。收集器生成的Map是使用提供的工厂函数创建的。
分类函数将元素映射到某些键类型K。下游收集器对T类型的元素进行操作,并生成D类型的结果。产生收集器生成Map<K, D>。
//示例:统计各个年级的平均成绩,并有序输出
@Test
public void testGroupingByFunctionAndSupplierAndCollector() {
Map<Student.GradeType, Double> map = students.stream()
.collect(Collectors.groupingBy(
Student::getGradeType,
TreeMap::new,
Collectors.averagingDouble(Student::getScore)));
Optional.of(map).ifPresent(System.out::println);
}
//{ONE=44.69, TWO=59.5, THREE=65.41}
5.groupingByConcurrent
5.1 groupingByConcurrent(Function)
groupingByConcurrent(Function)方法返回一个并发Collector收集器对T类型的输入元素执行"group by"操作,根据分类函数对元素进行分组
5.2 groupingByConcurrent(Function, Collector)
groupingByConcurrent(Function, Collector)方法返回一个并发Collector收集器,对T类型的输入元素执行级联"group by"操作,根据分类函数对元素进行分组,然后使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。
5.3 groupingByConcurrent(Function, Supplier, Collector)
groupingByConcurrent(Function, Supplier, Collector)方法返回一个并行Collector收集器,对T类型的输入元素执行级联"group by"操作,根据分类函数对元素进行分组,然后使用指定的下游Collector收集器对与给定键关联的值执行缩减操作。收集器生成的ConcurrentMap是使用提供的工厂函数创建的。
6.joining
这个方法对String类型的元素进行聚合,拼接成一个字符串返回,作用与java.lang.String#join类似,提供了 3 个不同重载方法,可以实现不同的需要。比如:
@Test
public void testJoin() {
//javagosql
Optional.of(Stream.of("java", "go", "sql").collect(Collectors.joining())).ifPresent(System.out::println);
//java,go,sql
Optional.of(Stream.of("java","go","sql").collect(Collectors.joining(","))).ifPresent(System.out::println);
//【java,go,sql】
Optional.of( Stream.of("java","go","sql").collect(Collectors.joining(",","【","】"))).ifPresent(System.out::println);
}
7.mapping
该方法是先对元素使用 Function 进行再加工操作,然后用另一个Collector 归纳。
@Test
public void testMapping() {
//刘一,陈二,张三,李四,王五,赵六,孙七
Optional.of(students.stream().collect(Collectors.mapping(Student::getName, Collectors.joining(",")))).ifPresent(System.out::println);
//[刘一, 陈二, 张三, 李四, 王五, 赵六, 孙七]
Optional.of(students.stream().collect(Collectors.mapping(Student::getName, Collectors.toList()))).ifPresent(System.out::println);
//用map更为清晰些[推荐]
//[刘一, 陈二, 张三, 李四, 王五, 赵六, 孙七]
Optional.of(students.stream().map(Student::getName).collect(Collectors.toList())).ifPresent(System.out::println);
}
8.maxBy
maxBy方法返回一个Collector收集器,它根据给定的Comparator比较器生成最大元素,描述为Optional。
//获取分数最高的学生,注意:同分情况下默认第一个最高分
@Test
public void testMaxBy() {
Optional.of(students.stream().collect(Collectors.maxBy(Comparator.comparingDouble(Student::getScore)))).ifPresent(System.out::println);
}
//Optional[Student(name=刘一, nickname=xiaoliu, score=88.8, age=12, local=true, gradeType=THREE)]
9.minBy
minBy方法返回一个Collector收集器,它根据给定的Comparator比较器生成最小元素,描述为Optional。
10.partitioningBy
partitioningBy(Predicate)方法返回一个Collector收集器,它根据Predicate对输入元素进行分区,并将它们组织成Map<Boolean, List>。
partitioningBy与groupingBy的区别在于,partitioningBy借助Predicate断言进行分组,可以将集合元素分为true和false两部分。
//按学生成绩是否大于80进行分组
@Test
public void testpartitioningBy() {
Map<Boolean, List<Student>> map1 = students.stream().collect(Collectors.partitioningBy(s -> s.getScore() > 80));
Map<Boolean, Set<Student>> map2 = students.stream().collect(Collectors.partitioningBy(s -> s.getScore() > 80, Collectors.toSet()));
//列出本和外地学生的平均分
Map<Boolean, Double> collect = students.stream().collect(Collectors.partitioningBy(Student::isLocal, Collectors.averagingDouble(Student::getScore)));
}
11.reducing
返回一个Collector收集器,它在指定的BinaryOperator下执行其输入元素的缩减。结果被描述为Optional。
reducing()相关收集器在groupingBy或partitioningBy下游的多级缩减中使用时非常有用。要对流执行简单缩减,请使用Stream#reduce(BinaryOperator)。
这个方法非常有用!但是如果要了解这个就必须了解其参数 BinaryOperator 。 这是一个函数式接口,是给两个相同类型的量,返回一个跟这两个量相同类型的一个结果,伪表达式为 (T,T) -> T。默认给了两个实现 maxBy 和 minBy ,根据比较器来比较大小并分别返回最大值或者最小值。当然你可以灵活定制。然后 reducing 就很好理解了,元素两两之间进行比较根据策略淘汰一个,随着轮次的进行元素个数就是 reduce 的。那这个有什么用处呢? Java 官方给了一个例子:统计每个城市个子最高的人。
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Map<String, Optional<Person>> tallestByCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(BinaryOperator.maxBy(byHeight))));
上面这一层是根据 Height 属性找最高的 Person ,而且如果这个属性没有初始化值或者没有数据,很有可能拿不到结果所以给出的是 Optional。 如果我们给出了 identity 作一个基准值,那么我们首先会跟这个基准值进行 BinaryOperator 操作。 比如我们给出高于 2 米 的人作为 identity。 我们就可以统计每个城市不低于 2 米 而且最高的那个人,当然如果该城市没有人高于 2 米则返回基准值identity :
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Person identity= new Person();
identity.setHeight(2.);
identity.setName("identity");
Map<String, Person> collect = persons.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(identity, BinaryOperator.maxBy(byHeight))));
这时候就确定一定会返回一个 Person 了,最起码会是基准值identity 不再是 Optional 。
还有些情况,我们想在 reducing 的时候把 Person 的身高先四舍五入一下。这就需要我们做一个映射处理。定义一个 Function<? super T, ? extends U> mapper 来干这个活。那么上面的逻辑就可以变更为:
Comparator<Person> byHeight = Comparator.comparing(Person::getHeight);
Person identity = new Person();
identity.setHeight(2.);
identity.setName("identity");
// 定义映射 处理 四舍五入
Function<Person, Person> mapper = ps -> {
Double height = ps.getHeight();
BigDecimal decimal = new BigDecimal(height);
Double d = decimal.setScale(1, BigDecimal.ROUND_HALF_UP).doubleValue();
ps.setHeight(d);
return ps;
};
Map<String, Person> collect = persons.stream()
.collect(Collectors.groupingBy(Person::getCity, Collectors.reducing(identity, mapper, BinaryOperator.maxBy(byHeight))));
11.1reducing(BinaryOperator)
//列出所有学生中成绩最高的学生信息
@Test
public void testReducingBinaryOperator() {
students.stream().collect(Collectors
.reducing(BinaryOperator
.maxBy(Comparator
.comparingDouble(Student::getScore))))
.ifPresent(System.out::println);
//可直接使用reduce
students.stream().reduce(BinaryOperator
.maxBy(Comparator
.comparingDouble(Student::getScore)))
.ifPresent(System.out::println);
}
11.2reducing(Object, BinaryOperator)
//统计所有学生的总成绩
@Test
public void testReducingBinaryOperatorAndIdentiy() {
Double result = students.stream()
.map(Student::getScore).collect(Collectors.reducing(0.0,(d1, d2) -> d1 + d2));
System.out.println(result);
//可以直接用reduce
Double reduce = students.stream()
.map(Student::getScore).reduce(0.0, Double::sum);
System.out.println(reduce);
}
11.3reducing(Object, Function, BinaryOperator)
@Test
public void testReducingBinaryOperatorAndIdentiyAndFunction() {
Double result = students.stream().collect(Collectors.reducing(0.0, Student::getScore, (d1, d2) -> d1 + d2));
//可以直接用reduce
Double result2 = students.stream().map(Student::getScore).reduce(0.0, Double::sum);
}
12.summarizingDouble
summarizingDouble方法返回一个Collector收集器,它将double生成映射函数应用于每个输入元素,并返回结果值的摘要统计信息。
既然是数据操作,基本上逃不出总数,总和,最小值,最大值,平均值这几个,所以作者也是很贴心的实现了一组聚合的数据统计方法。
//统计所有学生的摘要信息(总人数,总成绩,最高成绩,最低成绩和平均成绩)
@Test
public void testSummarizingDouble() {
DoubleSummaryStatistics result = students.stream()
.collect(Collectors.summarizingDouble(Student::getScore));
Optional.of(result).ifPresent(System.out::println);
}
//统计所有学生的摘要信息(总人数,总成绩,最高成绩,最低成绩和平均成绩)
13.summingDouble、summingInt、summingLong
返回一个Collector收集器,它生成应用于输入元素函数的总和。如果没有元素,则结果为0。也是需要注意元素的类型,在需要类型转换时,需要强制转换:
@Test
public void testSummingDouble() {
students.stream().collect(Collectors.summarizingDouble(Student::getScore));
students.stream().collect(Collectors.summingLong(s -> (long) s.getAge()));
}
14.toCollection
返回一个Collector收集器,它按遇见顺序将输入元素累积到一个新的Collection收集器中。Collection收集器由提供的工厂创建。
//统计总分大于60的所有学生的信息放入LinkedList中
@Test
public void testToCollection() {
Optional.of(students.stream().filter(d -> d.getScore() > 60)
.collect(Collectors.toCollection(LinkedList::new)))
.ifPresent(v -> {
System.out.println(v.getClass());
System.out.println(v);
});
}
//class java.util.LinkedList
//[Student(name=刘一, nickname=xiaoliu, score=88.8, age=12, local=true, gradeType=THREE), Student(name=陈二, nickname=xiaochen, score=66.6, age=15, local=true, gradeType=THREE), Student(name=孙七, nickname=xiaosun, score=88.8, age=17, local=false, gradeType=ONE)]
15.toConcurrentMap
15.1 toConcurrentMap(Function, Function)
返回一个并发的Collector收集器,它将元素累积到ConcurrentMap中,其键和值是将提供的映射函数应用于输入元素的结果。
如果映射的键包含重复项(根据Object#equals(Object)),则在执行收集操作时会抛出IllegalStateException。如果映射的键可能有重复,请使用toConcurrentMap(Function, Function, BinaryOperator)。
注意: 键或值作为输入元素是常见的。在这种情况下,实用方法java.util.function.Function#identity()可能会有所帮助。 这是一个Collector.Characteristics#CONCURRENT并发和Collector.Characteristics#UNORDERED无序收集器。
//以学生姓名为键总分为值统计信息
@Test
public void testToConcurrentMap() {
Optional.of(students.stream()
.collect(Collectors.toConcurrentMap(Student::getName, Student::getScore)))
.ifPresent(v -> {
System.out.println(v);
System.out.println(v.getClass());
});
}
15.2 toConcurrentMap(Function, Function, BinaryOperator)
返回一个并发的Collector收集器,它将元素累积到ConcurrentMap中,其键和值是将提供的映射函数应用于输入元素的结果。
//以年级为键学生人数为值统计信息
@Test
public void testToConcurrentMapWithBinaryOperator() {
Optional.of(students.stream()
.collect(Collectors.toConcurrentMap(Student::getGradeType, v -> 1, (a, b) -> a + b)))
.ifPresent(v -> {
System.out.println(v);
System.out.println(v.getClass());
});
}
15.3toConcurrentMap(Function, Function, BinaryOperator, Supplier)
//以年级为键学生人数为值将统计的信息放入ConcurrentSkipListMap中
@Test
public void testToConcurrentMapWithBinaryOperatorAndSupplier() {
Optional.of(students.stream()
.collect(Collectors.toConcurrentMap(
Student::getGradeType,
v -> 1L,
(a, b) -> a + b,
ConcurrentSkipListMap::new)))
.ifPresent(v -> {
System.out.println(v);
System.out.println(v.getClass());
});
}
// {ONE=1, TWO=1, THREE=5}
// class java.util.concurrent.ConcurrentSkipListMap
16.toList
返回一个Collector收集器,它将输入元素累积到一个新的List中。返回的List的类型,可变性,可序列化或线程安全性无法保证;如果需要更多地控制返回的List,请使用toCollection(Supplier)。
@Test
public void testToList() {
Optional.of(students.stream().filter(Student::isLocal).collect(Collectors.toList()))
.ifPresent(r -> {
System.out.println(r.getClass());
System.out.println(r);
});
}
17.toMap
17.1toMap(Function, Function)
返回一个Collector ,它将元素累积到一个Map中,其键和值是将提供的映射函数应用于输入元素的结果。
如果映射的键包含重复项(根据Object.equals(Object) ),则在执行收集操作时会引发IllegalStateException 。如果映射的键可能有重复项,请改用toMap(Function, Function, BinaryOperator) 。
Api注释: 键或值作为输入元素是很常见的。在这种情况下,实用方法Function.identity()可能会有所帮助。例如,以下生成了一个Map学生到他们的平均绩点的地图:
Map<Student, Double> studentToGPA
students.stream().collect(toMap(Functions.identity(),
student -> computeGPA(student)));
下面会生成一个Map到学生的唯一标识符的 Map:
Map<String, Student> studentIdToStudent
students.stream().collect(toMap(Student::getId,
Functions.identity());
返回的Collector收集器不是并发的。对于并行流管道,combiner函数通过将键从一个映射合并到另一个映射来操作,这可能是一个昂贵的操作。
如果不需要将结果以遇见的顺序插入Map,则使用toConcurrentMap(Function, Function, BinaryOperator)可以提供更好的并行性能。
//此写法可以解决键重复问题,键重复时会抛出java.lang.IllegalStateException: Duplicate key异常
//(x,y) -> x ; x时以第一次匹配到的value,y表示最后一次匹配到的value
@Test
public void testToMap() {
Map<Integer, String> collect = students.stream().collect(Collectors.toMap(Student::getAge, Student::getName, (x, y) -> y));
System.out.println(collect);
}
17.2toMap(Function, Function, BinaryOperator)
17.3toMap(Function, Function, BinaryOperator, Supplier)
18.toSet
返回一个Collector收集器,它将输入元素累积到一个新的Set中。返回的Set的类型,可变性,可序列化或线程安全性无法保证;如果需要更多地控制返回的Set,请使用toCollection(Supplier)。