Java8 应该算是业界主版本了,版本中重要性很高的一个更新是Stream
流处理。关于流处理内容比较多,本文主要是说一下Stream
中的Collectors
工具类的使用。
Collectors
是java.util.stream
包下的一个工具类,其中各个方法的返回值可以作为java.util.stream.Stream#collect
的入参,实现对队列的各种操作,包括:分组、聚合等。官方文档给出一些例子:
// 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):
然后定义一组测试数据:
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))
和: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))
最大值/最小值元素: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))
聚合、分组
聚合元素: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));
聚合元素:toMap、toConcurrentMap
这两个方法的作用是将聚合元素,重新组装为Map
结构,也就是 k-v 结构。两者用法一样,区别是toMap
返回的是Map
,toConcurrentMap
返回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
groupingBy
与toMap
都是将聚合元素进行分组,区别是,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
partitioningBy
与groupingBy
的区别在于,partitioningBy
借助Predicate
断言,可以将集合元素分为true
和false
两部分。比如,按照年龄是否大于 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));
同mapping
,reducing
的操作与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);
在上文说到maxBy
和minBy
时,提到这两个函数就是通过reducing
实现的。
文末总结
本文主要讲解了 Java8 Stream 中 Collectors 定义的 24 个方法,这种流式计算逻辑,依靠 Fork/Join 框架,性能方面有很大的优势。如果没有掌握这些用法,可能在后续阅读代码时,会很吃力,毕竟,Java8 基本上已经是业界标杆了。