收集器(Collector
)是为Stream.collect()
方法量身打造的工具接口(类)。考虑一下将一个Stream
转换成一个容器(或者Map
)需要做哪些工作?我们至少需要两样东西:
- 目标容器是什么?是
ArrayList
还是HashSet
,或者是个TreeMap
。 - 新元素如何添加到容器中?是
List.add()
还是Map.put()
。如果并行的进行规约,还需要告诉collect()
多个部分结果如何合并成一个?
结合以上分析,collect()
方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
,三个参数依次对应上述三条分析。
不过每次调用collect()
都要传入这三个参数太麻烦,收集器Collector
就是对这三个参数的简单封装,所以collect()
的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)
。
Collectors
工具类可通过静态方法生成各种常用的Collector
。
举例来说,如果要将Stream
规约成List
可以通过如下两种方式实现:
// 将Stream规约成List
Stream<String> stream = Stream.of("a", "bb", "ccc", "dddd");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
assertEquals(Arrays.asList("a", "bb", "ccc", "dddd"), list);
通常情况下我们不需要手动指定collect()
的三个参数,而是调用collect(Collector<? super T,A,R> collector)
方法,并且参数中的Collector
对象大都是直接通过Collectors
工具类获得。实际上传入的收集器的行为决定了collect()
的行为。
使用collect()生成Collection
将Stream
转换成List
或Set
是比较常见的操作,所以Collectors
工具已经为我们提供了对应的收集器,通过如下代码即可完成:
// 将Stream转换成List或Set
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)
// 使用toCollection()指定规约容器的类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)
使用collect()生成Map
Stream
背后依赖于某种数据源,数据源可以是数组、容器等,但不能是Map
。反过来从Stream
生成Map
是可以的,但我们要想清楚Map
的key
和value
分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下collect()
的结果会是Map:
- 使用
Collectors.toMap()
生成的收集器,用户需要指定如何生成Map
的key和value。 - 使用
Collectors.partitioningBy()
生成的收集器,对元素进行二分区操作时用到。 - 使用
Collectors.groupingBy()
生成的收集器,对元素做group
操作时用到。
生成map:使用toMap()
生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()
并列的方法。如下代码展示将学生列表转换成由<学生,GPA>组成的Map
。非常直观,无需多言。
// 使用toMap()统计学生GPA
Map<Student, Double> studentToGPA =
students.stream().collect(Collectors.toMap(Function.identity(),// 如何生成key
student -> computeGPA(student)));// 如何生成value
Function
是一个接口,那么Function.identity()
是什么意思呢?这要从两方面解释:
Java 8允许在接口中加入具体方法。接口中的具体方法有两种,default
方法和static
方法,identity()
就是Function
接口的一个静态方法。
Function.identity()
返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t
形式的Lambda表达式。
数据分块:使用partitioningBy()
生成的收集器,这种情况适用于将Stream
中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
数据分组:使用groupingBy()
生成的收集器,这是比较灵活的一种情况。跟SQL中的group by语句类似,这里的groupingBy()
也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map
的同一个key上。下列代码展示将员工按照部门进行分组:
// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其他查询,比如1. 先将员工按照部门分组,2. 然后统计每个部门员工的人数。Java类库设计者也考虑到了这种情况,增强版的groupingBy()
能够满足这种需求。增强版的groupingBy()
允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。
// 使用下游收集器统计每个部门的人数
Map<Department, Integer> totalByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.counting()));// 下游收集器
上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个Employee对象,可通过如下方式做到:
// 按照部门对员工分布组,并只保留员工的名字
Map<Department, List<String>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.mapping(Employee::getName,// 下游收集器
Collectors.toList())));// 更下游的收集器
使用collect()做字符串join
很多时候,收集流中的数据都是为了在最后生成一个字符串。字符串拼接时使用Collectors.joining()
生成的收集器,从此告别for循环。Collectors.joining()
方法有三种重写形式,分别对应三种不同的拼接方式。
// 使用Collectors.joining()拼接字符串
Stream<String> stream = Stream.of("a", "bb", "ccc");
//String joined = stream.collect(Collectors.joining());
//assertEquals("abbccc", joined);
//String joined = stream.collect(Collectors.joining(","));
//assertEquals("a,bb,ccc", joined);
String joined = stream.collect(Collectors.joining(",", "{", "}"));
assertEquals("{a,bb,ccc}", joined);