原文链接:https://zhuanlan.zhihu.com/p/481351073 在本文中,我们将介绍Java8的收集器 Collectors ,它们用于处理流的最后一步。

Stream.collect() 方法

Stream.collect()是Java 8的流API的终端方法之一。它++允许我们对流实例中保存的数据元素执行可变折叠操作(将元素重新打包到某些数据结构,并应用一些附加逻辑,将它们连接起来,等等)++。此操作的策略通过收集器接口实现提供。

Collectors

所有预定义的实现都可以在 Collectors 类中找到。通常使用以下静态导入来提高可读性:

import static java.util.stream.Collectors.*;

我们也可以使用我们选择的单一导入收集器collectors:

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

在以下示例中,我们将重用以下list:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");

1.Collectors.toList()

==toList 收集器==++可用于将所有流元素收集到列表实例中++。需要记住的重要一点是,我们不能用这种方法假设任何特定的列表实现。如果我们想对此有更多的控制,我们可以使用 toCollection 。

让我们创建一个表示元素序列的流实例,然后将它们收集到一个列表实例中:

List<String> result = givenList.stream().collect(toList());

2.Collectors.toUnmodifiableList()

Java 10引入了一种方便的方法,将流元素累积到一个不可修改的列表中:

List<String> result = givenList.stream().collect(toUnmodifiableList());

现在,如果我们试图修改结果列表,我们将得到一个UnsupportedOperationException

assertThatThrownBy(() -> result.add("foo"))
  .isInstanceOf(UnsupportedOperationException.class);

3.Collectors.toSet()

==toSet 收集器==可用于将所有流元素收集到集合实例中。需要记住的重要一点是,我们不能用这种方法假设任何特定的集合实现。如果我们想对此有更多的控制,我们可以使用 toCollection 。 让我们创建一个表示元素序列的流实例,然后将它们收集到一个集合实例中:

Set<String> result = givenList.stream().collect(toSet());

==集合不包含重复的元素==。如果我们的集合包含彼此相等的元素,则它们只在结果集中出现一次:

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set<String> result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);
Collectors.toUnmodifiableSet()

自Java 10以来,我们可以使用t oUnmodifiableSet() 收集器轻松创建一个不可修改的集:

Set<String> result = givenList.stream()
  .collect(toUnmodifiableSet());

任何修改结果集的尝试都将以不支持操作异常告终:

assertThatThrownBy(() -> result.add("foo")).isInstanceOf(UnsupportedOperationException.class);

4.Collectors.toCollection()

正如我们已经指出的,在使用 toSet 和 toList 收集器时,我们不能对它们的实现进行任何假设。如果我们想使用自定义实现,我们需要将 toCollection 收集器与我们选择的提供的集合一起使用。 让我们创建一个表示元素序列的流实例,然后将它们收集到 LinkedList 实例中:

List<String> result = givenList.stream().collect(toCollection(LinkedList::new))

请注意,==这不适用于任何不可变的集合==。在这种情况下,我们需要编写自定义收集器实现或使用 CollectionAndThen 。

5.Collectors.toMap()

toMap 收集器可用于将流元素收集到映射实例中。为此,我们需要提供两个功能:

keyMapper valueMapper

我们将使用keyMapper从流元素中提取映射键,使用valueMapper提取与给定键关联的值。 让我们将这些元素收集到一个映射中,该映射将字符串存储为键,长度存储为值:

Map<String, Integer> result = givenList.stream().collect(toMap(Function.identity(), String::length))

Function.identity() 只是定义接受并返回相同值的函数的快捷方式。

那么,如果我们的集合包含重复的元素,会发生什么呢?与 toSet 相反, toMap不会默默地过滤重复项,这是可以理解的,因为它如何确定为该键选择哪个值?

List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
    listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);

请注意, toMap 甚至不计算这些值是否相等。如果它看到重复的key,它会立即抛出一个非法状态异常。 在key冲突的情况下,我们应该使用另一个签名的toMap :

Map<String, Integer> result = givenList.stream().collect(toMap(Function.identity(), String::length,(item, identicalItem) -> item));

这里的==第三个参数是 BinaryOperator ,我们可以在其中指定希望如何处理碰撞==。在本例中,我们只选择这两个冲突的值中的任何一个,因为我们知道相同的字符串也总是具有相同的长度。

6.Collectors.toUnmodifiableMap()

与列表和集合类似,Java 10引入了一种将流元素收集到不可修改映射中的简单方法:

Map<String, Integer> result = givenList.stream().collect(toMap(Function.identity(), String::length))

正如我们所见,如果我们试图在结果映射中添加一个新条目,我们将得到一个不支持的操作异常:

assertThatThrownBy(() -> result.put("foo", 3))
  .isInstanceOf(UnsupportedOperationException.class);

7.Collectors.collectingAndThen()

==CollectionAndThen== 是一个特殊的收集器,允许我们在收集结束后立即对结果执行另一个操作。 让我们将流元素收集到列表实例,然后将结果转换为 ImmutableList 实例:

List<String> result = givenList.stream().collect(collectingAndThen(toList(), ImmutableList::copyOf))

8.Collectors.joining()

Joining 收集器可用于 joining Stream<String> 元素。 我们可以通过以下方式将它们结合在一起:

String result = givenList.stream().collect(joining());

结果:

"abbcccdd"

我们还可以指定自定义分隔符、前缀和后缀:

String result = givenList.stream().collect(joining(" "));

结果:

"a bb ccc dd"

也可这样写:

String result = givenList.stream().collect(joining(" ", "PRE-", "-POST"));

结果:

"PRE-a bb ccc dd-POST"

9.Collectors.counting()

==Counting== 是一个简单的收集器,允许对所有流元素进行计数。 现在我们可以写:

Long result = givenList.stream().collect(counting());
Collectors.summarizingDouble/Long/Int()

==SummaringDouble/Long/Int==是一个收集器,它++返回一个特殊的类,该类包含有关提取元素流中数字数据的统计信息++。 我们可以通过以下操作获得有关字符串长度的信息:

DoubleSummaryStatistics result = givenList.stream().collect(summarizingDouble(String::length));

在这种情况下,以下是正确的:

assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);
Collectors.averagingDouble/Long/Int()

==AveragingDouble/Long/Int== 是一个收集器,它++只返回提取元素的平均值++。 我们可以通过以下操作获得平均字符串长度:

Double result = givenList.stream()
  .collect(averagingDouble(String::length));

10.Collectors.summingDouble/Long/Int()

==SummingDouble/Long/Int==是一个收集器,它只++返回提取元素的总和++。 我们可以通过以下操作得到所有字符串长度的总和:

Double result = givenList.stream()
  .collect(summingDouble(String::length));

11.Collectors.maxBy()/minBy()

==MaxBy/MinBy== 收集器根据提供的比较器实例++返回流的最大/最小元素++。 我们可以通过以下方式选择最大的元素:

Optional<String> result = givenList.stream().collect(maxBy(Comparator.naturalOrder()));

我们可以看到返回的值被包装在一个可选的实例中。这迫使用户重新考虑空的收集角落案例。

12.Collectors.groupingBy()

==GroupingBy== collector用于++按某些属性对对象进行分组,然后将结果存储在Map实例中++。 我们可以按字符串长度对它们进行分组,并将分组结果存储在集合实例中:

Map<Integer, Set<String>> result = givenList.stream().collect(groupingBy(String::length, toSet()));

结果是true:

assertThat(result)
  .containsEntry(1, newHashSet("a"))
  .containsEntry(2, newHashSet("bb", "dd"))
  .containsEntry(3, newHashSet("ccc"));

我们可以看到 groupingBy 方法的第二个参数是收集器。此外,我们可以自由使用我们选择的任何收集器。

13.Collectors.partitioningBy()

==PartitioningBy==是groupingBy的一种特殊情况,它接受谓词实例,然后将流元素收集到Map实例中,++Map实例将布尔值存储为键,将集合存储为值。在“true”键下,我们可以找到与给定谓词匹配的元素集合,在“false”键下,我们可以找到与给定谓词不匹配的元素集合++。 我们可以写:

Map<Boolean, List<String>> result = givenList.stream().collect(partitioningBy(s -> s.length() > 2))

在Map中的结果:

{false=["a", "bb", "dd"], true=["ccc"]}

14.Collectors.teeing()

让我们使用到目前为止所学的收集器,从给定流中找出最大和最小数:

List<Integer> numbers = Arrays.asList(42, 4, 2, 24);
Optional<Integer> min = numbers.stream().collect(minBy(Integer::compareTo));
Optional<Integer> max = numbers.stream().collect(maxBy(Integer::compareTo));

// do something useful with min and max 在这里,我们使用两个不同的收集器,然后将这两个收集器的结果结合起来,创造出一些有意义的东西。在Java12之前,为了涵盖此类用例,我们必须对给定流进行两次操作,将中间结果存储到临时变量中,然后将这些结果合并。 幸运的是,Java12提供了一个内置收集器,代表我们处理这些步骤;我们所要做的就是提供两个采集器和组合器功能。 由于这种新的收集器将给定的流转向两个不同的方向,因此称为T形:

numbers.stream().collect(teeing(
  minBy(Integer::compareTo), // The first collector
  maxBy(Integer::compareTo), // The second collector
  (min, max) -> // Receives the result from those collectors and combines them
));

15.Custom Collectors

如果我们想编写自己的收集器实现,我们需要实现收集器接口,并指定其三个通用参数: public interface Collector<T, A, R> {...} T–可供收集的对象类型 A–可变累加器对象的类型 R–最终结果的类型 让我们编写一个示例收集器,用于将元素收集到 ImmutableSet 实例中。我们首先指定正确的类型:

private class ImmutableSetCollector<T>
  implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {...}

因为我们需要一个可变集合来处理内部集合操作,所以不能使用 ImmutableSet 。相反,我们需要使用一些其他可变集合,或任何其他可以临时为我们积累对象的类。在这种情况下,我们将使用 ImmutableSet 。现在我们需要实现5种方法:

Supplier<ImmutableSet.Builder<T>> supplier()
BiConsumer<ImmutableSet.Builder<T>, T> accumulator()
BinaryOperator<ImmutableSet.Builder<T>> combiner()
Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher()
Set<Characteristics> characteristics()

==supplier()==方法返回一个生成空累加器实例的Supplier实例。所以在这种情况下,我们可以简单地写:

@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
    return ImmutableSet::builder;
}

acculator() 方法返回一个函数,该函数用于向现有 acculator 对象添加新元素。让我们使用生成器的 add 方法:

@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
    return ImmutableSet.Builder::add;
}

==combiner()== 方法返回一个用于将两个累加器合并在一起的函数:

@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
    return (left, right) -> left.addAll(right.build());
}

==finisher()== 方法返回一个函数,用于将累加器转换为最终结果类型。所以在这种情况下,我们只使用 Builder 的构建方法:

@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
    return ImmutableSet.Builder::build;
}

==characteristics()== 方法用于为Stream提供一些用于内部优化的附加信息。在这种情况下,我们不会注意元素在集合中的顺序。

@Override public Set<Characteristics> characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

以下是完整的实现和用法:

public class ImmutableSetCollector<T>
  implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {

@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
    return ImmutableSet::builder;
}

@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
    return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
    return (left, right) -> left.addAll(right.build());
}

@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
    return ImmutableSet.Builder::build;
}

@Override
public Set<Characteristics> characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static <T> ImmutableSetCollector<T> toImmutableSet() {
    return new ImmutableSetCollector<>();
}

最后在action中:

List<String> givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet<String> result = givenList.stream()
  .collect(toImmutableSet());