首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

使用流执行聚合(2)

使用流执行聚合(2)

可变缩减缩减获取一个值序列并将它缩减为单个值,比如数列的和或它的最大值。但是有时您不想要单个汇总值;您想将结果组织为类似 List                或 Map 的数据结构,或者将它缩减为多个汇总值。在这种情况下,您应该使用缩减                的可变类似方法,也称为收集。
考虑将元素累积到一个 List 中的简单情况。使用累加器反模式,您可以这样编写它:
1
2
3
ArrayList<String> list = new ArrayList<>();
for (Person p : people)
    list.add(p.toString());




当累加器变量是一个简单值时,缩减是累加的更好替代方法,与此类似,在累加器结果是更复杂的数据结构时,也有一种更好的替代方法。缩减的构建块是一个身份值和一种将两个值组合成新值的途径;可变缩减的类似方法包括:
  • 一种生成空结果容器的途径
  • 一种将新元素合并到结果容器中的途径
  • 一种合并两个结果容器的途径
这些构建块可以轻松地表达为函数。这些函数中的第 3                个支持并行执行可变缩减:您可以对数据集进行分区,为每一部分生成一个中间累加结果,然后合并中间结果。Streams 库有一个                collect() 方法,它接受以下 3 个函数:
1
2
3
<R> collect(Supplier<R> resultSupplier,
            BiConsumer<R, T> accumulator,
            BiConsumer<R, R> combiner)




在前一节中,您看到了一个使用缩减来计算字符串串联的示例。该代码会生成正确的结果,但是,因为 Java                中的字符串是不可变的,而且串联要求复制整个字符串,所以它还有 O(n2)                运行时(一些字符串将复制多次)。您可以通过将结果收集到 StringBuilder 中,更高效地表达字符串串联:
1
2
3
4
StringBuilder concat = strings.stream()
                              .collect(() -> new StringBuilder(),
                                       (sb, s) -> sb.append(s),
                                       (sb, sb2) -> sb.append(sb2));




此方法使用 StringBuilder 作为结果容器。传递给 collect() 的 3                个函数使用默认构造函数创建了一个空容器,append(String)                方法将一个元素添加到容器中,append(StringBuilder)                方法将一个容器合并到另一个容器中。使用方法引用可能可以比拉姆达表达式更好地表达此代码:
1
2
3
4
StringBuilder concat = strings.stream()
                              .collect(StringBuilder::new,
                                       StringBuilder::append,
                                       StringBuilder::append);




类似地,要将一个流收集到一个 HashSet 中,您可以这样做:
1
2
3
4
Set<String> uniqueStrings = strings.stream()
                                   .collect(HashSet::new,
                                            HashSet::add,
                                            HashSet::addAll);




在这个版本中,结果容器是一个 HashSet 而不是                StringBuilder,但方法是一样的:使用默认构造函数创建一个新的结果容器,使用                add() 方法将一个新元素合并到结果集中,使用 addAll()                方法合并两个结果集。很容易看到如何将此代码调整为其他任何类型的集合。
您可能会想,因为使用了可变结果容器(StringBuilder 或                HashSet),所以这也是累加器反模式的一个例子。但其实不然。累加器反模式在这种情况下采用的类似方法是:
1
2
Set<String> set = new HashSet<>();
strings.stream().forEach(s -> set.add(s));




“可将收集器组合到一起来形成更复杂的聚合。”

就像只要组合函数是结合性的,且没有相互干扰的副作用,就可以安全地并行化缩减一样,如果满足一些简单的一致性要求(在                collect() 的规范中列出),就可以安全地并行化使用了 Stream.collect()                的可变缩减。关键区别在于,对于 forEach() 版本,多个线程会同时尝试访问一个结果容器,而对于并行                collect(),每个线程拥有自己的本地结果容器,会在以后合并其中的结果。
收集器传递给 collect() 的 3 个函数(创建、填充和合并结果容器)之间的关系非常重要,所以有必要提供它自己的抽象                Collector 和 collect() 的相应简化版本。字符串串联示例可重写为:
1
String concat = strings.stream().collect(Collectors.joining());




收集到结果集的示例可重写为:
1
Set<String> uniqueStrings = strings.stream().collect(Collectors.toSet());




Collectors 类包含许多常见聚合操作的因素,比如累加到集合中、字符串串联、缩减和其他汇总计算,以及创建汇总表(通过                groupingBy())。表 1 包含部分内置收集器的列表,而且如果它们不够用,编写您自己的收集器也很容易(请参阅                    “” 部分)。
表 1. 内置收集器收集器行为toList() 将元素收集到一个 List                            中。toSet() 将元素收集到一个 Set                            中。toCollection(Supplier<Collection>)将元素收集到一个特定类型的                            Collection 中。toMap(Function<T, K>, Function<T,                            V>)将元素收集到一个                            Map 中,依据提供的映射函数将元素转换为键值。summingInt(ToIntFunction<T>)计算将提供的                            int 值映射函数应用于每个元素(以及 long 和                            double 版本)的结果的总和。summarizingInt(ToIntFunction<T>)计算将提供的                            int 值映射函数应用于每个元素(以及 long 和                            double 版本)的结果的                            sum、min、max、count                            和 average。reducing() 向元素应用缩减(通常用作下游收集器,比如用于                            groupingBy)(各种版本)。 partitioningBy(Predicate<T>)将元素分为两组:为其保留了提供的预期的组和未保留预期的组。partitioningBy(Predicate<T>,                            Collector)将元素分区,使用指定的下游收集器处理每个分区。groupingBy(Function<T,U>) 将元素分组到一个                            Map 中,其中的键是所提供的应用于流元素的函数,值是共享该键的元素列表。groupingBy(Function<T,U>,                            Collector)将元素分组,使用指定的下游收集器来处理与每个组有关联的值。minBy(BinaryOperator<T>)                            计算元素的最小值(与 maxBy() 相同)。mapping(Function<T,U>,                            Collector)将提供的映射函数应用于每个元素,并使用指定的下游收集器(通常用作下游收集器本身,比如用于                            groupingBy)进行处理。joining() 假设元素为 String                            类型,将这些元素联结到一个字符串中(或许使用分隔符、前缀和后缀)。counting() 计算元素数量。(通常用作下游收集器。)
将收集器函数分组到 Collector                抽象中在语法上更简单,但实际收益来自您开始将收集器组合在一起时,比如您想要创建复杂的汇总结果(比如                groupingBy() 收集器创建的摘要)的时候,该收集器依据来自元素的一个键将元素收集到                Map 中。例如,要创建超过 1000 美元的交易的 Map,可以使用卖家作为键:
1
2
3
4
Map<Seller, List<Txn>> bigTxnsBySeller =
    txns.stream()
        .filter(t -> t.getAmount() > 1000)
        .collect(groupingBy(Txn::getSeller));




但是,假设您不想要每个卖家的交易                List,而想要来自每个卖家的最大交易。您仍希望使用卖家作为结果的键,但您希望进一步处理与卖家关联的交易,以便将它缩减为最大的交易。可以使用                groupingBy()                    的替代版本,无需将每个键的元素收集到列表中,而是将它们提供给另一个收集器(downstream                收集器)。对于下游收集器,您可以选择 maxBy() 等缩减方法:
1
2
3
4
Map<Seller, Txn> biggestTxnBySeller =
    txns.stream()
        .collect(groupingBy(Txn::getSeller,
                            maxBy(comparing(Txn::getAmount))));




在这里,您将交易分组到以卖家作为键的映射中,但该映射的值是使用 maxBy()                收集器收集该卖家的所有销售的结果。如果您不想要该卖家的最大交易,而想要总和,可以使用 summingInt()                收集器:
1
2
3
4
Map<Seller, Integer> salesBySeller =
    txns.stream()
         .collect(groupingBy(Txn::getSeller,
                            summingInt(Txn::getAmount)));




要获得多级汇总结果,比如每个区域和卖家的销售,可以使用另一个 groupingBy 收集器作为下游收集器:
1
2
3
4
5
Map<Region, Map<Seller, Integer>> salesByRegionAndSeller =
    txns.stream()
        .collect(groupingBy(Txn::getRegion,
                            groupingBy(Txn::getSeller,
                                       summingInt(Txn::getAmount))));




举一个不同领域的例子:要计算一个文档中的词频直方图,可以使用 BufferedReader.lines()                将文档拆分为行,使用 Pattern.splitAsStream() 将它分解为一个单词流,然后使用                collect() 和 groupingBy() 创建一个                Map,后者的键是单词,值是这些单词的数量,如清单 3 所示。
清单 3. 使用 Streams                计算单词数量直方图
1
2
3
4
5
6
Pattern whitespace = Pattern.compile("\\s+");
Map<String, Integer> wordFrequencies =
    reader.lines()
          .flatMap(s -> whitespace.splitAsStream())
          .collect(groupingBy(String::toLowerCase),
                              Collectors.counting());

返回列表