4.9 实现Collector接口
问题
由于 java.util.stream.Collectors 类提供的工厂方法无法满足需要,用户希望手动实现 java.util.stream.Collector 接口。
方案
为工厂方法 Collector.of 传入的 supplier、accumulator、combiner、finisher 函数提供 lambda 表达式或方法引用,以及其他所需的特性。
讨论
Collectors 工具类定义了多种便利的静态方法,它们的返回类型为 Collector。这些方法包括 toList、toSet、toMap 以及 toCollection,相关讨论请参见其他章节。实现 Collector 的类的实例作为 Stream.collect 方法的参数。如例 4-34 所示,evenLengthStrings 方法传入字符串参数,并返回仅包含偶数长度字符串的 List。
例 4-34 利用
collect方法返回List
- public List<String> evenLengthStrings(String... strings) {
- return Stream.of(strings)
- .filter(s -> s.length() % 2 == 0)
- .collect(Collectors.toList()); ➊
- }
➊ 将偶数长度的字符串收集到 List 中
编写自定义收集器的过程则略显复杂。收集器使用 5 个函数,它们的作用是将条目累加到可变容器,并有选择性地对结果进行转换。这 5 个函数是 supplier、accumulator、combiner、finisher 以及 characteristics。
我们首先讨论 characteristics 函数,表示 Collector.Characteristics 枚举的一个不可变的元素 Set。三个枚举常量为 CONCURRENT、IDENTITY_FINISH 与 UNORDERED。CONCURRENT 表示结果容器支持多个线程在结果容器上并发地调用累加器函数,UNORDERED 表示集合操作无须保留元素的出现顺序(encounter order),IDENTITY_FINISH 表示终止器函数返回其参数而不做任何修改。
请注意,如果默认值就是实际需要的,则不必提供任何特性。下面列出了每种参数的用途。
supplier()
使用 Supplier 创建累加器容器(accumulator container)。
accumulator()
使用 BiConsumer 为累加器容器添加一个新的数据元素。
combiner()
finisher()
characteristics()
从枚举值中选择的 Set。
如果读者熟悉 java.util.function 包定义的函数式接口,则不难理解各个参数的含义: Supplier 用于创建累加临时结果所用的容器;BiConsumer 用于将一个元素添加到累加器; BinaryOperator 表示输入类型和输出类型相同,因此可以将两个累加器合二为一;最后,Function 将累加器转换为所需的结果容器。
程序在收集过程中调用上述方法,它们由 Stream.collect 这样的方法触发。从概念上讲,集合过程相当于例 4-35 所示的(泛型)代码(取自 Javadoc)。
例 4-35 各种
Collector方法的用法
- R container = collector.supplier.get(); ➊
- for (T t : data) {
- collector.accumulator().accept(container, t); ➋
- }
- return collector.finisher().apply(container); ➌
❶ 创建累加器容器
❷ 将每个元素添加到累加器容器
❸ 通过 finisher 函数将累加器容器转换为结果容器
本例并未出现 combiner 函数,这或许令人感到困惑。如果处理的是顺序流,则不需要该函数,算法将既定方式执行。如果处理的是并行流,流将被分为多个子流,每个子流都会生成各自的累加器容器。接下来,在连接过程中使用 combiner 函数将多个累加器容器合并为一个,然后应用 finisher 函数。
例 4-36 显示了与例 4-34 类似的代码。
例 4-36 利用
collect方法返回不可修改的SortedSet
- public SortedSet<String> oddLengthStringSet(String... strings) {
- Collector<String, ?, SortedSet<String>> intoSet =
- Collector.of(TreeSet<String>::new, ➊
- SortedSet::add, ➋
- (left, right) -> { ➌
- left.addAll(right);
- return left;
- },
- Collections::unmodifiableSortedSet); ➍
- return Stream.of(strings)
- .filter(s -> s.length() % 2 != 0)
- .collect(intoSet);
- }
❶ Supplier:创建新的 TreeSet
❷ BiConsumer:将每个字符串添加到 TreeSet
❸ BinaryOperator:将两个 SortedSet 实例合二为一
❹ finisher:创建不可修改的 Set
程序将输出一个经过排序且不可修改的字符串集,它按字典序排序。
本例展示了如何通过 Collector.of 方法生成收集器。of 方法包括以下两种形式:
- static <T,A,R> Collector<T,A,R> of(Supplier<A> supplier,
- BiConsumer<A,T> accumulator,
- BinaryOperator<A> combiner,
- Function<A,R> finisher,
- Collector.Characteristics... characteristics)
- static <T,R> Collector<T,R,R> of(Supplier<R> supplier,
- BiConsumer<R,T> accumulator,
- BinaryOperator<R> combiner,
- Collector.Characteristics... characteristics)
Collectors 类提供了多种用于生成收集器的便利方法,用户几乎不需要创建自定义收集器,不过掌握相关的知识仍然很有必要。综合应用 java.util.function 包定义的函数式接口,可以创建各种有趣的对象。
另见
有关 finisher 函数(一种下游收集器)的详细讨论请参见范例 4.6,有关 Supplier、Function、BinaryOperator 等函数式接口的讨论请参见第 2 章,有关 Collectors 类定义的各种静态工具方法请参见范例 4.2。
