为什么要实现Collector接口?
有三种方法可以创建您自己的收集器。
我们在本教程中研究了第一个。它包括将现有收集器与collector工厂类提供的不同机制相结合,即将一个收集器作为下游收集器传递给另一个收集器,或者使用collectingAndThen()收集器的完成器。
您还可以调用collect()方法,该方法接受构建收集器所需要的三个元素。这些方法在基本类型流和对象流中都可用。它们采用了我们在前几节中介绍的三个参数。
- supplier :用于创建在其中积累流元素的可变容器
- accumulator:累加器,由biconsumer建模
- combiner:组合器,也由双向使用者建模,用于组合两个部分填充的容器,用于并行流。
第三种方法是自己实现Collector接口,并将实现传递给我们已经介绍过的collect()方法。实现您自己的收集器给您提供了最大的灵活性,但它也更具有技术性。
理解Collector的参数类型
让我们检查一下这个接口的参数。
interface Collector<T, A, R> {
// content of the interface
}
让我们首先检查以下类型:T和R。
第一种类型是T,它对应于收集器正在处理的流的元素类型。
最后一种类型是R,它是这个收集器产生的类型。
对于在Stream实例上调用的toList()收集器,类型R将是List。对于toSet()收集器,它将是Set。
groupingBy()方法接受一个函数来计算返回映射的键。如果您使用这个收集器收集Stream,那么您需要传递一个函数,该函数可以映射T的实例。它可以将它们映射到任何类型K的实例,以创建映射的键。因此生成的映射的类型将是map >。所以类型R就是这个:Map>。
类型A比较复杂。您可能已经尝试使用IDE来存储在前面示例中创建的一个收集器。如果您这样做了,您可能会意识到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接口,那么您必须给A一个显式的值。类型A是该收集器使用的中间可变容器的类型。对于toList()收集器,它将是ArrayList,对于toSet()收集器,它将是HashSet。结果是,这个A类型被toList()工厂方法声明的返回类型隐藏了,这就是为什么前面的例子中?不能替换为ArrayList类型。
即使内部可变容器由实现直接返回,类型A和R也可能不同。例如,在toList()收集器的情况下,您可以通过固定ArrayList的A和List的R来实现Collector<T, A, R> 接口。
了解Collector的特性
收集器定义的被流实现使用的内部特征用于优化该收集器的使用。
有三个特点。
- IDENTITY_FINISH特征表明此收集器的结束器是标识函数。该实现不会为具有此特性的收集器调用完成器。
- 无序特征表明该收集器不保留处理流元素的顺序。这就是toSet()收集器的情况,它具有这个特性。另一方面,toList()收集器没有它。
- CONCURRENT特征表明累加器存储已处理元素的容器支持并发访问。这一点对于平行流很重要。
这些特征在Collector.Characteristics枚举中定义,并由Collector接口上定义的Characteristics()方法在集合中返回。
实现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返回集合。
实现join()Collector
重新创建这个收集器的实现很有趣,因为它只对字符串进行操作,而它的完成器不是标识函数。
这个收集器在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,前者已经支持这些元素。