文章目录
- 一,分析
- 1,累加的概念
- 2,三个参数的reduce
一,分析
Stream中的三个归约方法如上所示,
java8 流相关的操作中,我们把它理解 “累加器”,之所以加引号是因为他并不仅仅是加法,他的运算可以是一个Lambda 表达式,所以更准确的说 reduce 是一个迭代运算器
一个reduce操作(也称为折叠)接受一系列的输入元素,并通过重复应用操作将它们组合成一个简单的结果
参照reduce方法文档给出的示例
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
1,累加的概念
以两个参数的reduce为例:
BinaryOperator 是BiFunction 的三参数特殊化形式,两个入参和返回结果都是类型T,即为同一种类型。
来看下面的例子:
计算1,2,3,4,5 的和,并且初始值为3
也就是计算3+1+2+3+4+5
⑴.使用Stream 两个参数的reduce方法进行归约运算
⑵.使用for循环迭代调用BinaryOperator 的apply进行运算:
@Test
public void test(){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//⑴ 归约方法----------------------------------------------------------------------------
Integer reduce = list.stream().reduce(3, (x, y) -> x + y);
System.out.println(reduce); // 18
//⑵ 循环累加----------------------------------------------------------------------------
//函数式接口实现 accumulator
BinaryOperator<Integer> accumulator = (x,y) -> x+y;
//初始值
int identity = 3;
//遍历
for(Integer ele : list){
identity = accumulator.apply(identity,ele);
}
System.out.println(identity); // 18
}
其实两种方式背后的思维方式是一样的,那就是:结果重新作为一个参数,不断地参与到运算之中,直到最后结束。
理解reduce的含义重点就在于理解"累 加 器" 的概念
只要能够理解了累计运算的概念就可以完全理解Stream 中reduce方法,他本质就是一个不断累计运算的过程
Stream的一个参数和两个参数的方法的基本逻辑都是如此:
- 对于两个参数的reduce方法来说绿框部分代表的是identity初始值
- 对于一个参数的reduce方法来说差别是少了绿框中初始值的部分,即result R = T1 ,然后再继续与剩下的元素参与运算。
2,三个参数的reduce
与两个参数的reduce不同的地方在于类型
- 双参数的返回类型为T Stream类型为T
- 三参数的返回类型为U Stream类型为T 有了更大的发挥空间 T可能为U 也可能不是U
类似于:
U result = identity;
for (T element : this stream)
result = accumulator.apply(result, element)
return result;
演示 T为String类型,U为Int类型:
// 10+1+3
Integer r = Arrays.asList("a", "bbb").stream()
.reduce(10, (x, y) -> x + y.length(), (x, y) -> x + y);
System.out.println(r); //14
很显然,三参数的reduce 方法的思维方式同双参数的并无二致
接下来重点看下第三个参数:
其实第三个参数用于在并行计算下 合并各个线程的计算结果
并行流运行时:内部使用了fork-join框架
多线程时,多个线程同时参与运算
多个线程执行任务,必然会产生多个结果
第三个参数的作用是将他们进行正确的合并大致处理流程:
来看下面的例子:
@Test
public void test(){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//非并行 5+1+2+3+4+5
Integer reduce1 = list.stream().reduce(5, (x, y) -> x + y, (x, y) -> x + y);
System.out.println(reduce1); //20
//并行 5+1 + 5+2 + 5+3 + 5+4 + 5+5
Integer reduce2 = list.parallelStream().reduce(5, (x, y) -> x + y, (x, y) -> x + y);
System.out.println(reduce2); //40
}
对于第一个和第二个结果不同,是由于计算方式不同
- 非并行流中,12345在一个fork中,初始值与其累加,也就是 5+1+2+3+4+5;
- 并行流中,将12345分成了5个不同的fork,每个fork分别于初始值进行计算,然后将结果求和,也就是(5+1) + (5+2) + (5+3) + (5+4) + (5+5)。
对于第二个并行流来说,我们想要的应该也是同第一个一样的结果,为什么并行和非并行的结果会有差别呢,是因为我们的写法的问题,我们来看该方法的注释部分:
这里是三参数reduce方法中的描述,简单来说就是
- 第一点:初始值
identity 的值对于合并运算combiner来说必须是一个恒等式,也就是说对于任意的u, combiner(identity,u) 和u是相同的。
这句话看起来怪怪的,对于任意的u 经过合并运算 竟然还是u,那还要这个干嘛??
从我们上面的并行处理流程可以看得出来,这个result 的初始identity 对于每一个分支都是参与运算的!!!
这也是为什么要求:任意的u, combiner(identity,u) 和u是相同的原因。
我们的combiner为 (a,b)->a+b;
那么如果分为两个分支进行运算,我们的初始值identity就参与了两次运算 也就是说多加了两个identity的值!!
怎么样才能保证u = combiner(identity,u)
除非identity=0 这才是对于 (a,b)->a+b 来说能够保障u = combiner(identity,u)
否则,你就不要用(a,b)->a+b 这个combiner
我们把Identity换成0之后如下:
//0+1 + 0+2 + 0+3 + 0+4 + 0+5
Integer reduce3 = list.parallelStream().reduce(0, (x, y) -> x + y, (x, y) -> x + y);
System.out.println(reduce3); //15
- 第二点:combiner 必须和accumulator要兼容
场景:
假设说4个元素 1,2,3,4 需要运算
此时假设已经 1,2,3 三组数据已经运算结束,马上要同第四组运算
如果是并行,我们假定1,2,3 在一个分支 4单独在另一分支
- 并行时
U为已经计算好的1,2,3后的结果 接下来要与另一组的4合并
T4则是identity与T参与运算,T4的fork中,4会与初始值identity单独进行一次计算
下面的图就是
combiner.apply(u, accumulator.apply(identity, t)) - 非并行运算
u 直接与下一个元素进行结合运算
显然这只是并行和非并行两种不同的处理运算方式,他们应该是相同的
也就是combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)