一起来学Java8(七)——Stream(下)

一起来学Java8(七)——Stream(中)我们学习了Stream.collect的用法,今天我们来学习下Stream.reduce的用法。

reduce操作可以理解成对Stream中元素累计处理,它有三个重载方法。

  • 重载1:Optional<T> reduce(BinaryOperator<T> accumulator);
  • 重载2:T reduce(T identity, BinaryOperator<T> accumulator);
  • 重载3:<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

reduce(accumulator)

先来看下重载1方法,这个方法需要我们传入一个参数,参数名字定义为收集器,顾名思义是需要我们对元素进行收集。

下面通过一个数值累加的例子来说明reduce的基本用法。

Optional<Integer> opt = Stream.of(1, 2, 3, 4)
        .reduce((n1, n2) -> {
            int ret = n1 + n2;
            System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
            return ret;
        });
int sum = opt.orElse(0);
System.out.println("sum:" + sum);

打印:

1(n1) + 2(n2) = 3
3(n1) + 3(n2) = 6
6(n1) + 4(n2) = 10
sum:10

这个例子中对Stream例子中的三个数字进行相加,得到总和。最后返回一个Optional<Integer>对象是因为考虑到Stream中没有元素的情况,因此返回结果是未知的,应该由开发者来确定返回值。

在Lambda表达式中提供了两个参数n1,n2。从打印结果中可以看出,n1,n2最开始分别是Stream中第一,第二两个元素,把这两个数进行相加后返回,然后带着这个结果再次进入到Lambda表达式中,n1是前一次相加后的值,n2是下一个元素值。

这段代码效果等同于:

int[] arr = { 1, 2, 3, 4};
int sum = 0;
for (int i : arr) {
    sum += i;
}

reduce(identity, accumulator, combiner)

再来看下重载3,这个方法有三个参数,每个参数说明如下:

  • identity:给定一个初始值
  • accumulator:基于初始值,对元素进行收集归纳
  • combiner:对每个accumulator返回的结果进行合并,此参数只有在并行模式中生效。

如果Stream对象是串行的,那么只有accumulator生效,combiner是不生效的。

使用Stream.parallel()的方法开启并行模式,使用Stream.sequential()开启串行模式,默认开启的是串行模式。

并行模式可以简单理解为在多线程中执行,每个线程中单独执行它的任务。串行则是在单一线程中顺序执行。

下面来看下重载3的例子:

int sum = Stream.of(1, 2, 3, 4)
        .reduce(0, (n1, n2) -> {
            int ret = n1 + n2;
            System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
            return ret;
        }, (s1, s2) -> {
            int ret = s1 + s2;
            System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
            return ret;
        });
System.out.println("sum:" + sum);

打印:

0(n1) + 1(n2) = 1
1(n1) + 2(n2) = 3
3(n1) + 3(n2) = 6
6(n1) + 4(n2) = 10
sum:10

可以看到,在串行模式下并没运行combiner参数,只运行了accumulator参数,从给定的初始值0开始累加。

这里已经指定了初始值(identity),因此返回类型就是初始值的类型。

我们把例子改成并行模式,然后看下执行结果。

int sum = Stream.of(1, 2, 3, 4)
        .parallel() // 并行模式
        .reduce(0, (n1, n2) -> {
            int ret = n1 + n2;
            System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
            return ret;
        }, (s1, s2) -> {
            int ret = s1 + s2;
            System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
            return ret;
        });
System.out.println("sum:" + sum);

打印:

0(n1) + 3(n2) = 3
0(n1) + 1(n2) = 1
0(n1) + 2(n2) = 2
1(s1) + 2(s2) = 3
0(n1) + 4(n2) = 4
3(s1) + 4(s2) = 7
3(s1) + 7(s2) = 10
sum:10

从打印的结果中我们可以看到几个现象:

  1. combiner参数被执行了
  2. 打印的内容是无序的,说明它们在多线程环境下执行的
  3. n1参数始终是0

因为是并行模式,前2个现象很好理解,那为什么n1参数始终是0?

因为开了并行模式后,运行reduce方法的底层是使用了ForkJoinPool(分支/合并框架)。

分支/合并框架的原理是将一个大任务拆分成多个子任务,这些子任务并行处理自己的事情,然后框架将这些子任务的结果合并起来,生成一个最终结果。

每个子任务之间是没有关联的,它们的执行状态都是一样的,因此每个子任务给到的初始值(identity)都是一样的,在本例中是0

同时需要一个合并方法用来合并每个子任务的处理结果,然后最终返回,使用数学表达式即为:

(0+1) + (0+2) + (0+3) + (0+4) = 10

再来看下重载3这个方法签名,每个参数的分工都明确了。

reduce(identity, accumulator, combiner)

  • identity:初始值
  • accumulator:每个子任务执行的操作
  • combiner:合并每个子任务的结果

注意事项

查看reduce方法文档,发现有下面一段话:

Performs a reduction on the elements of this stream, 
using the provided identity value and an associative accumulation function, 
and returns the reduced value. This is equivalent to: 

T result = identity;
for (T element : this stream)
 result = accumulator.apply(result, element)
return result;

The identity value must be an identity for the accumulator function. 
This means that for all t, accumulator.apply(identity, t) is equal to t. 
The accumulator function must be an associative function. 

其中有一句重要的话:This means that for all t, accumulator.apply(identity, t) is equal to t.

简单来说,必须要满足下面这个公式:

accumulator.apply(identity, t) == t

如果不满足的话,在并行模式下执行accumulator会有问题。

我们把上一个例子中的初始值改成1,然后看看执行结果

int sum = Stream.of(1, 2, 3, 4)
        .parallel()
        // 这里改成了1
        .reduce(1, (n1, n2) -> {
            int ret = n1 + n2;
            System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret);
            return ret;
        }, (s1, s2) -> {
            int ret = s1 + s2;
            System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret);
            return ret;
        });
System.out.println("sum:" + sum);

打印:

1(n1) + 3(n2) = 4
1(n1) + 4(n2) = 5
4(s1) + 5(s2) = 9
1(n1) + 2(n2) = 3
1(n1) + 1(n2) = 2
2(s1) + 3(s2) = 5
5(s1) + 9(s2) = 14
sum:14

理想中的结果应该是11才对,即1 + 1 + 2 + 3 + 4。可以看到在并行模式下对identity的值是有要求的。
必须满足公式:accumulator.apply(identity, t) == t

这里accumulator.apply(identity, t) == t即为:accumulator.apply(1, 1) == 1,使用数学表达式表示:

1(identity) + 1 == 1

显然这个等式是不成立的,把identity改成0则公式成立:0 + 1 == 1

紧接着,对于combiner参数,需要满足另一个公式:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

  • t:表示第一个参数
  • u:表示第二个参数

在这个例子中,我们取第一次执行combiner情况: 4(s1) + 5(s2) = 9,套用公式即为:

combiner.apply(5, accumulator.apply(1, 4)) == accumulator.apply(5, 4)

在这里u=5,identity=1,t=4

转换成数学表达式为:5 + (1 + 4) == 5 + 4

显然这个等式是不成立的,把identity改成0,等式就成立了:5 + (0 + 4) == 5 + 4

总结一下

使用reduce(identity, accumulator, combiner)方法时,必须同时满足下面两个公式:

  • 公式1,针对accumulator:accumulator.apply(identity, t) == t
  • 公式2,针对combiner:combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

reduce(identity, accumulator)

这个方法其实是reduce(identity, accumulator, combiner)的一种特殊形式,只不过是把combiner部分用accumulator来代替了,即

reduce(identity, accumulator)等同于reduce(identity, accumulator, accumulator)

因此reduce(identity, accumulator)的使用方式和注意事项是跟reduce(identity, accumulator, combiner)一样的,这里不再赘述。

小节

本篇主要讲解了Stream.reduce的使用方法及注意事项,在并行模式下,reduce是使用分支/合并框架实现的,在下一篇文章中我们开始学习分支/合并框架

定期分享技术干货,一起学习,一起进步!微信公众号:猿敲月下码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容