4.9 实现Collector接口

问题

由于 java.util.stream.Collectors 类提供的工厂方法无法满足需要,用户希望手动实现 java.util.stream.Collector 接口。

方案

为工厂方法 Collector.of 传入的 supplieraccumulatorcombinerfinisher 函数提供 lambda 表达式或方法引用,以及其他所需的特性。

讨论

Collectors 工具类定义了多种便利的静态方法,它们的返回类型为 Collector。这些方法包括 toListtoSettoMap 以及 toCollection,相关讨论请参见其他章节。实现 Collector 的类的实例作为 Stream.collect 方法的参数。如例 4-34 所示,evenLengthStrings 方法传入字符串参数,并返回仅包含偶数长度字符串的 List

例 4-34 利用 collect 方法返回 List

  1. public List<String> evenLengthStrings(String... strings) {
  2. return Stream.of(strings)
  3. .filter(s -> s.length() % 2 == 0)
  4. .collect(Collectors.toList());
  5. }

➊ 将偶数长度的字符串收集到 List

编写自定义收集器的过程则略显复杂。收集器使用 5 个函数,它们的作用是将条目累加到可变容器,并有选择性地对结果进行转换。这 5 个函数是 supplieraccumulatorcombinerfinisher 以及 characteristics

我们首先讨论 characteristics 函数,表示 Collector.Characteristics 枚举的一个不可变的元素 Set。三个枚举常量为 CONCURRENTIDENTITY_FINISHUNORDEREDCONCURRENT 表示结果容器支持多个线程在结果容器上并发地调用累加器函数,UNORDERED 表示集合操作无须保留元素的出现顺序(encounter order),IDENTITY_FINISH 表示终止器函数返回其参数而不做任何修改。

请注意,如果默认值就是实际需要的,则不必提供任何特性。下面列出了每种参数的用途。

supplier()

  使用 Supplier 创建累加器容器(accumulator container)。

accumulator()

  使用 BiConsumer 为累加器容器添加一个新的数据元素。

combiner()

  使用 BinaryOperator 合并两个累加器容器。

finisher()

  使用 Function 将累加器容器转换为结果容器。

characteristics()

  从枚举值中选择的 Set

如果读者熟悉 java.util.function 包定义的函数式接口,则不难理解各个参数的含义: Supplier 用于创建累加临时结果所用的容器;BiConsumer 用于将一个元素添加到累加器; BinaryOperator 表示输入类型和输出类型相同,因此可以将两个累加器合二为一;最后,Function 将累加器转换为所需的结果容器。

程序在收集过程中调用上述方法,它们由 Stream.collect 这样的方法触发。从概念上讲,集合过程相当于例 4-35 所示的(泛型)代码(取自 Javadoc)。

例 4-35 各种 Collector 方法的用法

  1. R container = collector.supplier.get();
  2. for (T t : data) {
  3. collector.accumulator().accept(container, t);
  4. }
  5. return collector.finisher().apply(container);

❶ 创建累加器容器

❷ 将每个元素添加到累加器容器

❸ 通过 finisher 函数将累加器容器转换为结果容器

本例并未出现 combiner 函数,这或许令人感到困惑。如果处理的是顺序流,则不需要该函数,算法将既定方式执行。如果处理的是并行流,流将被分为多个子流,每个子流都会生成各自的累加器容器。接下来,在连接过程中使用 combiner 函数将多个累加器容器合并为一个,然后应用 finisher 函数。

例 4-36 显示了与例 4-34 类似的代码。

例 4-36 利用 collect 方法返回不可修改的 SortedSet

  1. public SortedSet<String> oddLengthStringSet(String... strings) {
  2. Collector<String, ?, SortedSet<String>> intoSet =
  3. Collector.of(TreeSet<String>::new,
  4. SortedSet::add,
  5. (left, right) -> {
  6. left.addAll(right);
  7. return left;
  8. },
  9. Collections::unmodifiableSortedSet);
  10. return Stream.of(strings)
  11. .filter(s -> s.length() % 2 != 0)
  12. .collect(intoSet);
  13. }

Supplier:创建新的 TreeSet

BiConsumer:将每个字符串添加到 TreeSet

BinaryOperator:将两个 SortedSet 实例合二为一

finisher:创建不可修改的 Set

程序将输出一个经过排序且不可修改的字符串集,它按字典序排序。

本例展示了如何通过 Collector.of 方法生成收集器。of 方法包括以下两种形式:

  1. static <T,A,R> Collector<T,A,R> of(Supplier<A> supplier,
  2. BiConsumer<A,T> accumulator,
  3. BinaryOperator<A> combiner,
  4. Function<A,R> finisher,
  5. Collector.Characteristics... characteristics)
  6.  
  7. static <T,R> Collector<T,R,R> of(Supplier<R> supplier,
  8. BiConsumer<R,T> accumulator,
  9. BinaryOperator<R> combiner,
  10. Collector.Characteristics... characteristics)

Collectors 类提供了多种用于生成收集器的便利方法,用户几乎不需要创建自定义收集器,不过掌握相关的知识仍然很有必要。综合应用 java.util.function 包定义的函数式接口,可以创建各种有趣的对象。

另见

有关 finisher 函数(一种下游收集器)的详细讨论请参见范例 4.6,有关 SupplierFunctionBinaryOperator 等函数式接口的讨论请参见第 2 章,有关 Collectors 类定义的各种静态工具方法请参见范例 4.2。