实现 Collector 接口
为什么要实现 Collector 接口?
有三种方法可以创建您自己的 Collector。
在本教程中,我们研究了第一种方法。它包括使用 Collectors
工厂类提供的不同机制将现有的 Collector 组合起来,即,将 Collector 作为下游 Collector 传递给另一个 Collector,或者使用 collectingAndThen()
Collector 作为完成器。
您还可以调用 collect()
方法,该方法接受构建 Collector 的三个元素。这些方法在基本类型流和对象流上都可用。它们接受我们在上一节中介绍的三个参数。
- 供应商用于创建可变容器,用于累积流中的元素。
- 累加器,由双消费者建模。
- 组合器,也由双消费者建模,用于组合两个部分填充的容器,在并行流的情况下使用。
第三种方法是实现 Collector
接口,并将您的实现传递给我们已经介绍过的 collect()
方法。实现您自己的 Collector 可以为您提供最大的灵活性,但它也更技术性。
了解 Collector 的参数类型
让我们检查一下此接口的参数。
interface Collector<T, A, R> {
// content of the interface
}
让我们首先检查以下类型:T
和 R
。
第一个类型是 T
,它对应于此 Collector 处理的流元素的类型。
最后一个类型是 R
,它是此 Collector 生成的类型。
对于 toList()
Collector,在 Stream
实例上调用,类型 R
将是 List<T>
。对于 toSet()
Collector,它将是 Set<T>
。
groupingBy()
方法接受一个函数来计算返回的映射的键。如果您正在使用此 Collector 收集 Stream
,那么您需要传递一个可以映射 T
实例的函数。它可以将它们映射到任何类型为 K
的实例以创建映射的键。因此,结果映射的类型将是 Map<K, List<T>>
。因此,类型 R
只是这个:Map<K, List<T>>
。
类型 A
更难处理。您可能尝试过使用 IDE 来存储您在前面示例中创建的 Collector 之一。如果您这样做了,您可能已经意识到您的 IDE 没有为该类型提供明确的值。对于以下示例,情况也是如此。
Collection<String> strings =
List.of("two", "three", "four", "five", "six", "seven", "eight", "nine",
"ten", "eleven", "twelve");
Collector<String, ?, List<String>> listCollector = Collectors.toList();
List<String> list = strings.stream().collect(listCollector);
Collector<String, ?, Set<String>> setCollector = Collectors.toSet();
Set<String> set = strings.stream().collect(setCollector);
Collector<String, ?, Map<Integer, Long>> groupingBy =
Collectors.groupingBy(String::length, Collectors.counting());
Map<Integer, Long> map = strings.stream().collect(groupingBy);
对于所有这些 Collector,第二个参数类型只是 ?
。
如果您需要实现 Collector
接口,那么您将必须为 A
提供一个明确的值。类型 A
是此 Collector 使用的中间可变容器的类型。对于 toList()
Collector,它将是 ArrayList
,对于 toSet()
Collector,它将是 HashSet
。事实证明,此 A
类型被 toList()
工厂方法声明的返回类型隐藏了,这就是为什么您不能在前面的示例中将 ?
类型替换为 ArrayList<T>
的原因。
即使内部可变容器由实现直接返回,A
和 R
类型也可能不同。例如,在 toList()
Collector 的情况下,您可以通过为 A
固定 ArrayList<T>
以及为 R
固定 List<T>
来实现 Collector<T, A, R>
接口。
了解 Collector 的特性
Collector 定义了内部特性,流实现使用这些特性来优化此 Collector 的使用。
有三个特性。
IDENTITY_FINISH
特性表示此 Collector 的完成器是恒等函数。实现不会为具有此特性的 Collector 调用完成器。UNORDERED
特性表示此 Collector 不保留处理流元素的顺序。对于toSet()
Collector 来说,情况就是这样,它具有此特性。另一方面,toList()
Collector 没有此特性。CONCURRENT
特性表示累加器存储处理元素的容器支持并发访问。这一点对于并行流很重要。
这些特性在 Collector.Characteristics
枚举中定义,并由 characteristics()
方法以集合形式返回,该方法在 Collector
接口中定义。
实现 toList() 和 toSet() 收集器
有了这些元素,您现在可以重新创建类似于 toList()
收集器的收集器实现。
class ToList<T> implements Collector<T, List<T>, List<T>> {
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
public BiConsumer<List<T>, T> accumulator() {
return Collection::add;
}
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {list1.addAll(list2); return list1; };
}
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
public Set<Characteristics> characteristics() {
return Set.of(Characteristics.IDENTITY_FINISH);
}
}
您可以使用以下模式使用此收集器。
Collection<String> strings =
List.of("one", "two", "three", "four", "five") ;
List<String> result = strings.stream().collect(new ToList<>());
System.out.println("result = " + result);
此代码输出以下结果。
result = [one, two, three, four, five]
实现类似于 toSet()
收集器的收集器只需要两个修改。
- 该
supplier()
方法将返回HashSet::new
。 - 该
characteristics()
方法将添加Characteristics.UNORDERED
到返回的集合中。
实现 joining() 收集器
重新创建此收集器的实现很有趣,因为它只对字符串进行操作,并且它的终结器不是身份函数。
此收集器将它处理的字符串累积在一个 StringBuffer
实例中,然后调用该累积器的 toString()
方法来生成最终结果。
此收集器的特性集为空。它保留了元素处理的顺序(因此没有UNORDERED特性),它的终结器不是身份函数,并且它不能并发使用。
让我们看看如何实现此收集器。
class Joining implements Collector<String, StringBuffer, String> {
public Supplier<StringBuffer> supplier() {
return StringBuffer::new;
}
public BiConsumer<StringBuffer, String> accumulator() {
return StringBuffer::append;
}
public BinaryOperator<StringBuffer> combiner() {
return StringBuffer::append;
}
public Function<StringBuffer, String> finisher() {
return Object::toString;
}
public Set<Characteristics> characteristics() {
return Set.of();
}
}
您可以在以下示例中看到如何使用此收集器。
Collection<String> strings =
List.of("one", "two", "three", "four", "five") ;
String result = strings.stream().collect(new Joining());
System.out.println("result = " + result);
运行此代码会产生以下结果。
result = onetwothreefourfive
支持分隔符、前缀和后缀将使用 StringJoiner
而不是 StringBuilder
,它已经支持这些元素。
上次更新: 2021 年 9 月 14 日