最大值和最小值

   原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用学到的reduce来计算流中最大或最小元素。正如你前面看到的,reduce接收两个参数:

  1. 一个初始值
  2. 一个Lambda来把两个流元素结合起来并产生一个新值。

Lambda是一步步用加法运算符应用到流中每个元素的,如下图所示。因此,你需要一个给定元素能够返回最大值Lambda。reduce操作会考虑新值和流中下一个元素,并产生一个新的最大值,直到整个流消耗完!你可以像下面这样使用reduce来计算流中的最大值,如图所示。

Java8 归约 reduce 第二讲

Optional<Integer> max = numbers.stream().reduce(Integer::max);

要计算最小值,你需要把Integer.min传给reduce来替换Integer.max;

Optional<Integer> min = numbers.stream().reduce(Integer::min);

你当然也可以写成Lambda(x,y) -> x < y ? x : y而不是Integer::min,不过后者比较易读。

测验归约:

怎样用map和reduce方法数一数流中有多少个菜呢?

答案:要解决整个问题,你可以把流中每个元素都映射成数字1,然后用reduce求和。这相当于按照顺序数流中的元素个数。

int count = menus.stream().map(menu -> 1)

.reduce(0, (a,b) -> a+b);

map和reduce的连接通常称为map-reduce模式,因google用它来进行网络搜索而出名,因为它很容易并行化。请注意,我们会在后面的内容中可以考到内置count方法用来计算流中元素的个数:

long count = menus.stream().count();

归约方法的优势与并行化

相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代求和例子要封信共享变量sum,这不是那么容易并行化的。如果你加入了同步,很有可能发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。在后面的章节看到使用分支/合并框架来做是什么样子。但现在重要的是要认识到,可变的累加器模式对于并行化来说是死路一条。你需要一种新的模式,这正是reduce所提供的。在后面章节,使用流来对所有的元素并行求和时,你的代码几乎不用修改:stream()换成了parallelStream().reduce(0, Integer::sum);

但是并行执行这段代码也要付出一定代价,我们稍后会想你解释:传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按照任意顺序执行。

流操作:无状态和有状态

你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,并且只要在从集合生成流的时候把Stream换成parallelStream就可以实现并行。

当然,对于很多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用filter选出某一类的菜肴,然后对得到的流做map来对卡路里求和,最后reduce得到菜单的总热量。这个流计算是甚至可以并行进行。但是这些操作的特性并不相同。它们需要操作的内部状态还是有一些问题的。

但诸如reduce,sum,max等操作需要内部状态来累计结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的。

相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接收一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要先前的历史。例如,排序要求所有元素都放入缓冲区后次啊能给输入流加入一个项目,这一操作的存储要求时无界的。要是流比较大或者无限流,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们这些操作叫作有状态操作。

其实什么叫作有状态的操作,就是需要知道上一个流操作的状态或者历史。

参考书籍:Java8实战

相关文章: