「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」

你好,我是看山。

Java8 应该算是业界主版本了,版本中重要性很高的一个更新是Stream流处理。关于流处理内容比较多,本文主要是说一下Stream中的Collectors工具类的使用。

Collectorsjava.util.stream包下的一个工具类,其中各个方法的返回值可以作为java.util.stream.Stream#collect的入参,实现对队列的各种操作,包括:分组、聚合等。官方文档给出一些例子:

Implementations of {@link Collector} that implement various useful reduction operations, such as accumulating elements into collections, summarizing elements according to various criteria, etc.

The following are examples of using the predefined collectors to perform common mutable reduction tasks:

// Accumulate names into a List
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());// Accumulate names into a TreeSet
Set<String> set = people.stream().map(Person::getName).collect(Collectors.toCollection(TreeSet::new));// Convert elements to strings and concatenate them, separated by commas
String joined = things.stream().map(Object::toString).collect(Collectors.joining(", "));// Compute sum of salaries of employee
int total = employees.stream().collect(Collectors.summingInt(Employee::getSalary)));// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment));// Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment, Collectors.summingInt(Employee::getSalary)));// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream().collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));复制代码

定义示例数据

先定义待操作对象,一个万能的Student类(用到了 lombok):

@Data
@AllArgsConstructor
public class Student {private String id;private String name;private LocalDate birthday;private int age;private double score;
}
复制代码

然后定义一组测试数据:

final List<Student> students = Lists.newArrayList();
students.add(new Student("1", "张三", LocalDate.of(2009, Month.JANUARY, 1), 12, 12.123));
students.add(new Student("2", "李四", LocalDate.of(2010, Month.FEBRUARY, 2), 11, 22.123));
students.add(new Student("3", "王五", LocalDate.of(2011, Month.MARCH, 3), 10, 32.123));
复制代码

数据统计

元素数量:counting

这个比较简单,就是统计聚合结果的元素数量:

// 3
students.stream().collect(Collectors.counting())
复制代码

平均值:averagingDouble、averagingInt、averagingLong

这几个方法是计算聚合元素的平均值,区别是输入参数需要是对应的类型。

比如,求学生的分数平均值,因为分数是double类型,所以在不转类型的情况下,需要使用averagingDouble

// 22.123
students.stream().collect(Collectors.averagingDouble(Student::getScore))
复制代码

如果考虑转换精度,也是可以实现:

// 22.0
students.stream().collect(Collectors.averagingInt(s -> (int)s.getScore()))
// 22.0
students.stream().collect(Collectors.averagingLong(s -> (long)s.getScore()))
复制代码

如果是求学生的平均年龄,因为年龄是int类型,就可以随意使用任何一个函数了:

// 11.0
students.stream().collect(Collectors.averagingInt(Student::getAge))
// 11.0
students.stream().collect(Collectors.averagingDouble(Student::getAge))
// 11.0
students.stream().collect(Collectors.averagingLong(Student::getAge))
复制代码

注意:这三个方法的返回值都是Double类型。

和:summingDouble、summingInt、summingLong

这三个方法和上面的平均值方法类似,也是需要注意元素的类型,在需要类型转换时,需要强制转换:

// 66
students.stream().collect(Collectors.summingInt(s -> (int)s.getScore()))
// 66.369
students.stream().collect(Collectors.summingDouble(Student::getScore))
// 66
students.stream().collect(Collectors.summingLong(s -> (long)s.getScore()))
复制代码

但是对于不需要强制转换的类型,可以随意使用任何一个函数:

// 33
students.stream().collect(Collectors.summingInt(Student::getAge))
// 33.0
students.stream().collect(Collectors.summingDouble(Student::getAge))
// 33
students.stream().collect(Collectors.summingLong(Student::getAge))
复制代码

注意:这三个方法返回值和平均值的三个方法不一样,summingDouble返回的是Double类型、summingInt返回的是Integer类型,summingLong返回的是Long类型。

最大值/最小值元素:maxBy、minBy

顾名思义,这两个函数就是求聚合元素中指定比较器中的最大/最小元素。比如,求年龄最大/最小的Student对象:

// Optional[Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)],注意返回类型是Optional
students.stream().collect(Collectors.minBy(Comparator.comparing(Student::getAge)))
// Optional[Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)],注意返回类型是Optional
students.stream().collect(Collectors.maxBy(Comparator.comparing(Student::getAge)))
复制代码

从源码可以看出来,这两个方法算是作者给的福利,用于完善数据统计的结果。内部都是封装了reducing方法和BinaryOperator工具类,这些下面会讲到。

public static <T> Collector<T, ?, Optional<T>> maxBy(Comparator<? super T> comparator) {return reducing(BinaryOperator.maxBy(comparator));
}public static <T> Collector<T, ?, Optional<T>> minBy(Comparator<? super T> comparator) {return reducing(BinaryOperator.minBy(comparator));
}
复制代码

统计结果:summarizingDouble、summarizingInt、summarizingLong

既然是数据操作,基本上逃不出计数、求平局、求和、最大、最小这几个,所以作者也是很贴心的实现了一组聚合的数据统计方法。

这组方法与求和、求平均的方法类似,都需要注意方法类型。比如,按照分数统计的话,需要进行类型转换:

// IntSummaryStatistics{count=3, sum=66, min=12, average=22.000000, max=32}
students.stream().collect(Collectors.summarizingInt(s -> (int) s.getScore()))
// DoubleSummaryStatistics{count=3, sum=66.369000, min=12.123000, average=22.123000, max=32.123000}
students.stream().collect(Collectors.summarizingDouble(Student::getScore))
// LongSummaryStatistics{count=3, sum=66, min=12, average=22.000000, max=32}
students.stream().collect(Collectors.summarizingLong(s -> (long) s.getScore()))
复制代码

如果是用年龄统计的话,三个方法通用:

// IntSummaryStatistics{count=3, sum=33, min=10, average=11.000000, max=12}
students.stream().collect(Collectors.summarizingInt(Student::getAge))
// DoubleSummaryStatistics{count=3, sum=33.000000, min=10.000000, average=11.000000, max=12.000000}
students.stream().collect(Collectors.summarizingDouble(Student::getAge))
// LongSummaryStatistics{count=3, sum=33, min=10, average=11.000000, max=12}
students.stream().collect(Collectors.summarizingLong(Student::getAge))
复制代码

注意:这三个方法返回值不一样,summarizingDouble返回DoubleSummaryStatistics类型,summarizingInt返回IntSummaryStatistics类型,summarizingLong返回LongSummaryStatistics类型。

聚合、分组

聚合元素:toList、toSet、toCollection

这几个函数比较简单,是将聚合之后的元素,重新封装到队列中,然后返回。比如,得到所有Student的 ID 列表,只需要根据需要的结果类型使用不同的方法即可:

// List: [1, 2, 3]
final List<String> idList = students.stream().map(Student::getId).collect(Collectors.toList());
// Set: [1, 2, 3]
final Set<String> idSet = students.stream().map(Student::getId).collect(Collectors.toSet());
// TreeSet: [1, 2, 3]
final Collection<String> idTreeSet = students.stream().map(Student::getId).collect(Collectors.toCollection(TreeSet::new));
复制代码

注意:toList方法返回的是List子类,toSet返回的是Set子类,toCollection返回的是Collection子类。我们都知道,Collection的子类包括ListSet等众多子类,所以toCollection更加灵活。

聚合元素:toMap、toConcurrentMap

这两个方法的作用是将聚合元素,重新组装为Map结构,也就是 k-v 结构。两者用法一样,区别是toMap返回的是MaptoConcurrentMap返回ConcurrentMap,也就是说,toConcurrentMap返回的是线程安全的 Map 结构。

比如,我们需要聚合Student的 id:

// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map11 = students.stream().collect(Collectors.toMap(Student::getId, Function.identity()));
复制代码

但是,如果 id 有重复的,会抛出java.lang.IllegalStateException: Duplicate key异常,所以,为了保险起见,我们需要借助toMap另一个重载方法:

// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map2 = students.stream().collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -> x));
复制代码

可以看到,toMap有不同的重载方法,可以实现比较复杂的逻辑。比如,我们需要得到根据 id 分组的Student的姓名:

// {1=张三, 2=李四, 3=王五}
final Map<String, String> map3 = students.stream().collect(Collectors.toMap(Student::getId, Student::getName, (x, y) -> x));
复制代码

比如,我们需要得到相同年龄得分最高的Student对象集合:

// {10=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123), 11=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 12=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)}
final Map<Integer, Student> map5 = students.stream().collect(Collectors.toMap(Student::getAge, Function.identity(), BinaryOperator.maxBy(Comparator.comparing(Student::getScore))));
复制代码

所以,toMap可玩性很高。

分组:groupingBy、groupingByConcurrent

groupingBytoMap都是将聚合元素进行分组,区别是,toMap结果是 1:1 的 k-v 结构,groupingBy的结果是 1:n 的 k-v 结构。

比如,我们对Student的年龄分组:

// List: {10=[Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)], 11=[Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123)], 12=[Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Integer, List<Student>> map1 = students.stream().collect(Collectors.groupingBy(Student::getAge));
// Set: {10=[Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)], 11=[Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123)], 12=[Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Integer, Set<Student>> map12 = students.stream().collect(Collectors.groupingBy(Student::getAge, Collectors.toSet()));
复制代码

既然groupingBy也是分组,是不是也能够实现与toMap类似的功能,比如,根据 id 分组的Student

// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map3 = students.stream().collect(Collectors.groupingBy(Student::getId, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
复制代码

为了对比,把toMap的写法放在这:

// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map2 = students.stream().collect(Collectors.toMap(Student::getId, Function.identity(), (x, y) -> x));
复制代码

如果想要线程安全的Map,可以使用groupingByConcurrent

分组:partitioningBy

partitioningBygroupingBy的区别在于,partitioningBy借助Predicate断言,可以将集合元素分为truefalse两部分。比如,按照年龄是否大于 11 分组:

// List: {false=[Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)], true=[Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Boolean, List<Student>> map6 = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11));
// Set: {false=[Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123), Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123)], true=[Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123)]}
final Map<Boolean, Set<Student>> map7 = students.stream().collect(Collectors.partitioningBy(s -> s.getAge() > 11, Collectors.toSet()));
复制代码

链接数据:joining

这个方法对String类型的元素进行聚合,拼接成一个字符串返回,作用与java.lang.String#join类似,提供了 3 个不同重载方法,可以实现不同的需要。比如:

// javagosql
Stream.of("java", "go", "sql").collect(Collectors.joining());
// java, go, sql
Stream.of("java", "go", "sql").collect(Collectors.joining(", "));
// 【java, go, sql】
Stream.of("java", "go", "sql").collect(Collectors.joining(", ", "【", "】"));
复制代码

操作链:collectingAndThen

这个方法在groupingBy的例子中出现过,它是先对集合进行一次聚合操作,然后通过Function定义的函数,对聚合后的结果再次处理。

比如groupingBy中的例子:

// {1=Student(id=1, name=张三, birthday=2009-01-01, age=12, score=12.123), 2=Student(id=2, name=李四, birthday=2010-02-02, age=11, score=22.123), 3=Student(id=3, name=王五, birthday=2011-03-03, age=10, score=32.123)}
final Map<String, Student> map3 = students.stream().collect(Collectors.groupingBy(Student::getId, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0))));
复制代码

显示将结果聚合成List列表,然后取列表的第 0 个元素返回,通过这种方式,实现 1:1 的 map 结构。

再来一个复杂一些的,找到聚合元素中年龄数据正确的Student列表:

// [],结果为空,是因为例子中所有人的年龄都是对的
students.stream().collect(Collectors.collectingAndThen(Collectors.toList(), (list -> list.stream().filter(s -> (LocalDate.now().getYear() - s.getBirthday().getYear()) != s.getAge()).collect(Collectors.toList()))));
复制代码

这个例子纯粹是为了使用collectingAndThen的用法,其实可以简化为:

students.stream().filter(s -> (LocalDate.now().getYear() - s.getBirthday().getYear()) != s.getAge()).collect(Collectors.toList());
复制代码

操作后聚合:mapping

mapping先通过Function函数处理数据,然后通过Collector方法聚合元素。比如,获取获取students的姓名列表:

// [张三, 李四, 王五]
students.stream().collect(Collectors.mapping(Student::getName, Collectors.toList()));
复制代码

这种计算与java.util.stream.Stream#map方式类似:

// [张三, 李四, 王五]
students.stream().map(Student::getName).collect(Collectors.toList());
复制代码

从这点上看,还是通过java.util.stream.Stream#map更清晰一些。

聚合后操作:reducing

reducing提供了 3 个重载方法:

  • public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op):直接通过BinaryOperator操作,返回值是Optional

  • public static <T> Collector<T, ?, T> reducing(T identity, BinaryOperator<T> op):预定默认值,然后通过BinaryOperator操作

  • public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper, BinaryOperator<U> op):预定默认值,通过Function操作元素,然后通过BinaryOperator操作

比如,计算所有students的得分总数:

// Optional[66.369],注意返回类型是Optional
students.stream().map(Student::getScore).collect(Collectors.reducing(Double::sum));
// 66.369
students.stream().map(Student::getScore).collect(Collectors.reducing(0.0, Double::sum));
// 66.369
students.stream().collect(Collectors.reducing(0.0, Student::getScore, Double::sum));
复制代码

mappingreducing的操作与java.util.stream.Stream#reduce方式类似:

// Optional[66.369],注意返回类型是Optional
students.stream().map(Student::getScore).reduce(Double::sum);
// 66.369
students.stream().map(Student::getScore).reduce(0.0, Double::sum);
复制代码

在上文说到maxByminBy时,提到这两个函数就是通过reducing实现的。

对于mappingreducing,可以参考函数式编程中 map-reduce 的概念。

文末总结

本文主要讲解了 Java8 Stream 中 Collectors 定义的 24 个方法,这种流式计算逻辑,依靠 Fork/Join 框架,性能方面有很大的优势。如果没有掌握这些用法,可能在后续阅读代码时,会很吃力,毕竟,Java8 基本上已经是业界标杆了。

青山不改,绿水长流,我们下次见。

推荐阅读

一文掌握 Java8 Stream 中 Collectors 的 24 个操作相关推荐

  1. js 中转换成list集合_java stream中Collectors的用法

    简介 在java stream中,我们通常需要将处理后的stream转换成集合类,这个时候就需要用到stream.collect方法.collect方法需要传入一个Collector类型,要实现Col ...

  2. java stream中Collectors的用法

    文章目录 简介 Collectors.toList() Collectors.toSet() Collectors.toCollection() Collectors.toMap() Collecto ...

  3. java双层list扁平化,浅谈java8 stream flatMap流的扁平化操作

    概念: Steam 是Java8 提出的一个新概念,不是输入输出的 Stream 流,而是一种用函数式编程方式在集合类上进行复杂操作的工具.简而言之,是以内部迭代的方式处理集合数据的操作,内部迭代可以 ...

  4. java reduce 分组_使用JAVA8 stream中三个参数的reduce方法对List进行分组统计

    背景 平时在编写前端代码时,习惯使用lodash来编写'野生'的JavaScript; lodash提供来一套完整的API对JS对象(Array,Object,Collection等)进行操作,这其中 ...

  5. Java8 Stream 中的 reduce() 方法,执行聚合操作

    初识 Stream 中的 reduce() 方法,是在最近一次刷算法题的过程中(下面会讲到),简洁干练的写法,让我眼前一亮. 所以,我简单学习了,总结了一下写法: 正文 Java 8 API添加了一个 ...

  6. Java8 Stream之Collectors

    文章目录 toList.toSet toMap toConcurrentMap toCollection joining collectingAndThen groupingBy groupingBy ...

  7. java8 stream中的惰性求值

    https://blog.csdn.net/qq_32961235/article/details/80080980

  8. java8 stream to map_Java 8 Stream Api 中的 map和 flatMap 操作

    1.前言 Java 8 提供了非常好用的 Stream API ,可以很方便的操作集合.今天我们来探讨两个 Stream 中间操作 map 和 flatMap 2. map 操作 map 操作是将流中 ...

  9. Java 8系列之Stream中万能的reduce

    Stream系列: Java 8系列之Stream的基本语法详解 Java 8系列之Stream的强大工具Collector Java 8系列之重构和定制收集器 Java 8系列之Stream中万能的 ...

  10. java 8 Stream中操作类型和peek的使用

    文章目录 简介 中间操作和终止操作 peek 结论 java 8 Stream中操作类型和peek的使用 简介 java 8 stream作为流式操作有两种操作类型,中间操作和终止操作.这两种有什么区 ...

最新文章

  1. 重温java web过滤器filter
  2. LDC1000循迹小车
  3. Xilinx发布实时视频编码服务器
  4. 系统管理员必须知道的PHP安全实践
  5. SIP中第三方呼叫控制(3PCC)建立流程
  6. nginx+tomcat+redis负载均衡及session共享
  7. php 根目录怎么写,php – 如何重写根目录中的目录
  8. 二分搜索 POJ 1064 Cable master
  9. 软件工程中逻辑覆盖的例题_干货丨一文读懂:飞算全自动软件工程平台如何提升软件开发效率...
  10. 荣耀路由2 虚拟服务器,荣耀路由器2恢复出厂设置的两种方法
  11. Oracle 日历表详解(含节假日)
  12. 5G到底厉害在什么地方?和4G有什么不同?
  13. Android 运行程序报错:Unable to execute dex: Multiple dex files define Lcom/baidu/android/pushservice/Push
  14. http://baiy.cn/doc/cpp/index.htm#代码风格与版式_函数
  15. IOS 学习笔记14 内存管理(3)有效的内存管理 后半部分
  16. 知识图谱入门一:知识图谱介绍
  17. java 面试题合集_撩课-Java面试题合辑1-50题
  18. python解常微分方程龙格库_数值常微分方程-欧拉法与龙格-库塔法
  19. 基于JSP的酒店登记预定系统的设计与实现
  20. 系统封装Win10专业版1803

热门文章

  1. 成分句法分析与依存句法分析
  2. div 中img 居中
  3. 洛谷-P1957 口算练习题
  4. 取消参考文献自动编号_毕业论文给尾注加[ ]及删除自动编号
  5. vue使用Mars3D实现3d卫星轨道和3d地球图
  6. XYOJ1234出租车费
  7. mysql 查询生日_MySQL,怎么查询一段时间内过生日的员工
  8. win10装机之天涯若比邻长时间卡死
  9. android 统计图表MPAndroidChart
  10. html修改页面图片大小,css怎么改变图片大小?