第 6 章 用流收集数据

本章内容

  • Collectors类创建和使用收集器
  • 将数据流归约为一个值
  • 汇总:归约的特殊情况
  • 数据分组和分区
  • 开发你的自定义收集器

我们在前一章中学到,流可以用类似于数据库的操作帮助你处理集合。你可以把Java 8的流看作花哨又懒惰的数据集迭代器。它们支持两种类型的操作:中间操作(如filtermap)和终端操作(如countfindFirstforEachreduce)。中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,例如返回流中的最大元素。它们通常可以通过优化流水线来缩短计算时间。

我们已经在第4章和第5章中用过collect终端操作了,当时主要是用来把Stream中所有的元素结合成一个List。在本章中,你会发现collect是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector接口来定义的,因此区分CollectionCollectorcollect是很重要的。

下面是一些查询的例子,看看你用collect和收集器能够做什么。

  • 对一个交易列表按货币分组,获得该货币的所有交易额总和(返回一个Map)。
  • 将交易列表分成两组:贵的和不贵的(返回一个Map>)。
  • 创建多级分组,比如按城市对交易分组,然后进一步按照贵或不贵分组(返回一个Map>>)。

激动吗?很好,先来看一个利用收集器的例子。想象一下,你有一个由Transaction构成的List,并且想按照名义货币进行分组。在Java 8之前,哪怕像这种简单的用例实现起来都很啰唆,就像下面这样。

代码清单 6-1 用指令式风格对交易按照货币分组

  1. Map<Currency, List<Transaction>> transactionsByCurrencies =
  2. new HashMap<>(); ←---- 建立累积交易分组的Map
  3. for (Transaction transaction : transactions) { ←---- 迭代TransactionList
  4. Currency currency = transaction.getCurrency(); ←---- 提取Transaction的货币
  5. List<Transaction> transactionsForCurrency =
  6. transactionsByCurrencies.get(currency);
  7. if (transactionsForCurrency == null) { ←---- 如果分组Map中没有这种货币的条目,就创建一个
  8. transactionsForCurrency = new ArrayList<>();
  9. transactionsByCurrencies
  10. .put(currency, transactionsForCurrency);
  11. }
  12. transactionsForCurrency.add(transaction); ←---- 将当前遍历的Transaction加入同一货币的TransactionList
  13. }

如果你是一位经验丰富的Java程序员,那写这种东西可能挺顺手的,不过你必须承认,做这么简单的一件事就得写很多代码。更糟糕的是,读起来比写起来更费劲!代码的目的并不容易看出来,尽管换作白话是很直截了当:“把列表中的交易按货币分组。”你在本章中会学到,用Stream中collect方法的一个更通用的Collector参数,就可以用一句话实现完全相同的结果,而用不着使用上一章中那个toList的特殊情况了:

  1. Map<Currency, List<Transaction>> transactionsByCurrencies =
  2. transactions.stream().collect(groupingBy(Transaction::getCurrency));

这一比差得还真多,对吧?