Java 8 Stream API详解( 二)——Stream Collectors

831 阅读7分钟

Stream.collect()是Java 8的Stream API的终端方法之一。 它使我们能够对Stream实例中保存的数据元素执行可变的折叠操作(将元素重新打包到某些数据结构中,并且应用一些额外的逻辑,串接数据等)。 该操作的具体策略是通过Collector接口的实现来提供。

Collectors

所有预定义的实现都可以在_Collectors_类中找到。 通常的做法是将以下静态导入与这些方法结合使用,以提高可读性:

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

或者单个导入您需要的收集器:

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

本文后面的例子中都使用下面的列表进行演示:

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

Collectors.toList()

toList收集器可用于将所有Stream元素收集到List实例中。需要注意的是,我们使用此方法时不能假设任何特定的List实现。如果要对此进行控制,请使用toCollection方法。 我们先创建包含一系列元素的流实例,并将其中元素收集到一个List实例中:

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

Java 10引入了一种方便的方法可以将Stream中元素收集到一个unmodifiable List中:

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

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

Collectors.toSet()

toSet收集器可用于将所有Stream元素收集到Set实例中。同样的,我们使用此方法时也不能假设任何特定的Set实现。如果要对此进行控制,请使用toCollection方法。 我们来将流中元素收集到一个Set实例中:

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

Set中不包含重复的元素,如果集合中包含相等的元素,最终的Set中只会出现一次:

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

Java 10中也提供了_toUnmodifiableSet()_ 方法来创建unmodifiable Set

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

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

Collectors.toCollection()

前面提到了,在使用toSet和toList收集器时,您无法对其实现进行任何假设。 如果要指定实现类型,则需要使用toCollection收集器。 我们来将流中元素收集到一个LinkedList实例中:

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

请注意,这不适用于任何不可变的集合。 对于这种情况,您将需要编写自定义的Collector实现或使用collectionAndThen。

Collectors.toMap()

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

  • keyMapper
  • valueMapper

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

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

Function.identity()只是用于定义接受参数和返回值相同的函数的快捷方式。 如果我们的集合包含重复元素,会发生什么? 与toSet不同,toMap操作不会默默过滤重复项。 这是可以理解的——程序应该如何确定为该key选择哪个值?

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

请注意,toMap甚至不会评估值是否也相等。 如果看到重复的键,则会立即引发IllegalStateException。 在发生键冲突的情况下,我们应该使用toMap的另一种形式:

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

这里的第三个参数是BinaryOperator,我们可以在其中指定如何处理冲突。 在上面描述的场景下,我们可以选择这两个冲突值中的任何一个,因为我们知道相同的字符串也将始终具有相同的长度。 与List和Set类似,Java 10引入了一种简单的方法来将Stream元素收集到unmodifiable Map中:

Map<String, Integer> result = givenList.stream()
  .collect(toMap(Function.identity(), String::length));
assertThatThrownBy(() -> result.put("foo", 3))
  .isInstanceOf(UnsupportedOperationException.class);

Collectors.c_ollectingAndThen()_

collingandthen是一个特殊的收集器,它允许在收集结束后直接对结果执行另一个操作。 让我们将流元素收集到一个List实例中,然后将结果转换为一个ImmutableList实例:

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

Collectors.j_oining()_

Joining收集器可用于联接Stream 元素。 我们可以通过以下方式将他们加入一起:

String result = givenList.stream()
  .collect(joining());
// 结果为   "abbcccdd"

还可以指定自定义分隔符,前缀,后缀:

String result = givenList.stream()
  .collect(joining(" ", "PRE-", "-POST"));
// 结果为   "PRE-a bb ccc dd-POST"

数学类收集器

Counting收集器用于对流中的元素进行计数;

Long result = givenList.stream()
  .collect(counting());

SummarizingDouble/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);

AveragingDouble/Long/Int是用于返回元素属性的平均值的收集器。如计算字符串平均长度:

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

SummingDouble/Long/Int是返回所提取元素之和的收集器。计算字符串长度之和:

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

比较类收集器

MaxBy/MinBy收集器根据提供的Comparator实例返回Stream的最大/最小元素。 可以通过以下方式筛选最大元素:

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

要注意,返回值被封装为Optional类型,这也迫使用户重新思考空集合这类边界情况。

GroupingBy收集器用于根据某些属性对对象进行分组,并将结果存储在Map实例中。 下面的代码根据字符串长度对流元素进行分组,并将分组结果存入Set中:

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

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

PartitioningBy是groupingBy的一种特殊情况,它接受一个谓词实例并将Stream元素收集到一个Map实例中,该Map将布尔值作为键,将分组集合作为值。 在“ true”键下,您可以找到与给定谓词匹配的元素集合,在“ false”键下,您可以找到与给定谓词不匹配的元素集合。

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

结果Map中内容为:

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

自定义收集器

如果你要自定义收集器实现,则需要实现Collector接口并指定其三个泛型参数:

public interface Collector<T, A, R> {...}
  1. T –可用于收集的对象类型,
  2. A –可变累加器对象的类型,
  3. R –最终结果的类型。

我们可以实现一个收集器,将元素收集到_ImmutableSet_中,首先声明泛型类型:

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

由于我们需要一个可变的集合来进行处理收集器内部的集合操作,因此我们不能使用ImmutableSet。 我们需要使用其他可变集合或任何可以临时存储对象的类。 这里,我们选择使用ImmutableSet.Builder,现在我们需要实现5个方法:

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

各方法的作用和要求为: supplier()方法要返回一个supplier实例,该实例会生成一个空的accumulator实例,因此,在本例中,我们可以简单地返回一个builder对象。 accumulator()方法会返回一个函数,该函数用于将新元素添加到现有的accumulator对象中,因此我们可以使用Builder的add方法。 combiner()方法返回一个函数,该函数用于将两个accumulator中的元素合并在一起。 finisher()方法返回一个用于将累加器accumulator转换为最终结果类型的函数,因此在本例中,我们将使用构建器的build()方法。 characteristic()方法用于为流提供一些额外的信息,这些信息将用于内部优化。在这种情况下,我们不关注Set中的元素顺序,因此我们可以使用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<>();
    }
}

可以测试该自定义收集器:

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

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