介绍 Java 8 Collectors 类
本文我们探讨Java 8 Collectors 类,其一般用于流处理中的最后一步。
Stream.collect() 方法
Stream.collect() 是java 8 stream api中的终止方法。它实现对流实例中保存的数据元素执行可变的折叠操作(将元素重新包装至特定的数据结构中,并应用一些额外逻辑将它们连接起来,等等)。此操作的策略由Collector接口实现提供。
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<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");
Collectors.toList()
ToList 方法可以收集所有流中元素至List实例。需要注意的是,使用该方法不能假设任何List的特定实现,如果你想指定List实现,需要使用toCollection 代替.下面示例创建一个流实例表示元素序列,然后收集至List实例对象中。
List<String> result = givenList.stream().collect(toList());
Collectors.toSet()
ToSet 方法可以收集所有流中元素至Set 实例.需要注意的是,使用该方法不能假设任何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);
Collectors.toCollection()
你可能已经注意到,使用toSet 和 toList收集器,不能指定其实现。如果想使用特定实现,需要使用toCollection收集器并提供特定集合实现。下面示例创建一个流实例表示元素序列,然后收集至LinkedList例对象中
List<String> result = givenList.stream().collect(toCollection(LinkedList::new))
注意这里不能使用不可变集合实现。通过自定义Collector实现或使用collectingAndThen。
Collectors.toMap()
ToMap 收集器用于收集流元素至 Map 实例,实现该功能需要提供两个函数:
- keyMapper
- valueMapper
keyMapper 用于从流元素中抽取map 的key,valueMapper抽取与可以关联的value。
下面示例收集流元素至Map中,存储字符串作为key,其长度作为value:
Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length))
Function.identity() 是一个预定义的返回接收参数的快捷函数。
如果我们集合中包含重复元素会怎么样?与toSet相反,toMap不能过滤重复元素。这个比较好理解————其如何确定key关联那个value?
List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);
我们看到,toMap甚至不判断值是否相等,如果key重复,立刻抛出IllegalStateException异常。当key冲突时,我们应该使用toMap的另一个重载方法:
Map<String, Integer> result = givenList.stream()
.collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
第三个参数是二元操作,我们可以指定如何处理key冲突。本例中我们可以任意选择一个,因为两者的长度始终一样。
Collectors.collectingAndThen()
CollectingAndThen 是一个特殊收集器,其可以收集完成后再结果上执行另外动作。下面收集流元素至List示例,然后转换结果为ImmutableList实例:
List<String> result = givenList.stream()
.collect(collectingAndThen(toList(), ImmutableList::copyOf))
Collectors.joining()
Joining 收集器可以用于连接字符串流Stream中的元素。示例如下:
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"
Collectors.counting()
Counting 是一个简单收集器,返回元素数量:
Long result = givenList.stream()
.collect(counting());
Collectors.summarizingDouble/Long/Int()
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);
Collectors.averagingDouble/Long/Int()
AveragingDouble/Long/Int 是返回流中抽取元素的平均值,下面示例计算字符串长度平均值:
Double result = givenList.stream()
.collect(averagingDouble(String::length));
Collectors.summingDouble/Long/Int()
SummingDouble/Long/Int 返回抽取元素之和,下面计算所有字符串长度之和:
Double result = givenList.stream()
.collect(summingDouble(String::length));
Collectors.maxBy()/minBy()
MaxBy/MinBy 收集器根据提供的 Comparator 实例,返回流中最大和最小元素,下面计算最大元素:
Optional<String> result = givenList.stream()
.collect(maxBy(Comparator.naturalOrder()));
需要注意的是,其结果为Optional类型,其强制用户考虑空集合情况
Collectors.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"));
第二个参数是一个集合,我们开使用任何集合实现。
Collectors.partitioningBy()
PartitioningBy 是一个特殊分组收集器,依据 Predicate 实例收集流元素至Map中,存储 Boolean 值作为key,值为集合。 “true” 键对应值为已匹配元素的集合,“false” 键为非匹配元素集合。示例代码如下:
Map<Boolean, List<String>> result = givenList.stream()
.collect(partitioningBy(s -> s.length() > 2))
结果Map如下:
{false=["a", "bb", "dd"], true=["ccc"]}
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
这里,我们使用两个不用的收集器,然后合并集合去实现相应业务。在Java 12之前,为了实现这样功能必须对流执行两次操作,把中间结果存储至临时对象中,最后合并返回。
幸运的是,java 12 提供了内置收集器帮助我们处理这些步骤:我们仅需提供两个收集器和合并函数。
因为新的收集器tee对流执行两个不用方向的操作,故称为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
));
总结
本文我们学习了Collectors类及其内置的各种收集器。