概述
stream API中提供的reduce方法是经常被用到的,它的作用主要是对流中的数据按照指定的计算方式计算出一个结果(缩减/归并操作)
reduce的三个重载方法
reduce方法有三个override的方法,分别接受1个参数,2个参数,和3个参数,下面来依次介绍
一、2个参数
为了方便理解,我们先从2个参数的reduce方法开始介绍
1. 使用介绍
方法定义为:T reduce(T identity, BinaryOperator<T> accumulator);
可见:这个方法接收两个参数:identity和accumulator
我们先看下这段代码:
1 // 初始化值 2 int sum = 0; 3 List list={1,2,3,4,5}; 4 for (n : list) { 5 // 指定计算方式 6 sum = sum + n; 7 8 } 9 return sum;
与上面for循环的代码很相似,reduce()内部的计算方式如下:
1 T result = identity 2 for(T element: this stream) 3 result = accumulator.apply(result,element) 4 return result;
可见,reduce的作用是把stream中的元素给组合起来,我们可以传入一个初始值,它会按照我们的计算方式依次拿流中的元素和初始化值进行计算,计算结果再和后面的元素计算
举例:
1 public static void main(String[] args) { 2 3 /*Integer sum1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, new BinaryOperator<Integer>() { 4 @Override 5 public Integer apply(Integer integer, Integer integer2) { 6 return integer + integer2; 7 } 8 });*/ 9 10 // lambda表达式 11 int sum1 = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (n1, n2) -> n1 + n2); 12 System.out.println("累加和为:" + sum1); //累加和为:45 13 }
2. 并行和串行
使用Stream.parallel()或者parallelStream()的方法开启并行模式,使用Stream.sequential()开启串行模式,默认开启的是串行模式。
并行模式可以简单理解为在多线程中执行,每个线程中单独执行它的任务。串行则是在单一线程中顺序执行。
3. 并行模式下,identity的指定是有要求的
identity是reduce的初始化值,但是在并行模式下,这个值不能随意指定
我们看下这个例子:
1 public static void main(String[] args) { 2 3 Integer sum1 = Stream.of(1, 2, 3).reduce(100, (integer, integer2) -> integer + integer2); 4 System.out.println("累加和为:" + sum1); //累加和为:106 5 6 Integer sum2 = Stream.of(1, 2, 3).parallel().reduce(100, (integer, integer2) -> integer + integer2); 7 System.out.println("累加和为:" + sum2); //累加和为:306 8 }
非并行计算和并行计算的结果居然不一样,我们在代码里面打印试试:
1 public static void main(String[] args) { 2 Integer sum2 = Stream.of(1, 2, 3).parallel().reduce(100, new BinaryOperator<Integer>() { 3 @Override 4 public Integer apply(Integer n1, Integer n2) { 5 System.out.println(Thread.currentThread().getName()+">>>n1>>>"+n1+",n2>>>>"+n2); 6 return n1 + n2; 7 } 8 }); 9 System.out.println("累加和为:" + sum2); 10 }
打印结果如下:
1 main>>>n1>>>100,n2>>>>2 2 ForkJoinPool.commonPool-worker-9>>>n1>>>100,n2>>>>1 3 ForkJoinPool.commonPool-worker-2>>>n1>>>100,n2>>>>3 4 ForkJoinPool.commonPool-worker-2>>>n1>>>102,n2>>>>103 5 ForkJoinPool.commonPool-worker-2>>>n1>>>101,n2>>>>205 6 累加和为:306
由上面的打印结果可以看出:在并行计算的时候,每个线程的初始累加值都是100,最后3个线程加出来的结果就是306
官方文档说明:
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.
翻译过来大概意思就是:identity必须是accumulator函数的一个identity,也就是说必须满足:对于所有的 t, 都必须满足 accumulator.apply(identity, t) == t
所以这里我们传入100是不对的,因为sum(100+1)!= 1。并行计算中,这里sum方法的identity只能是0。
如果我们用 0 作为identity,则stream和parallelStream计算出的结果是一样的。这就是identity的真正意图。
注意:计算求积时,初始值必须设置为1
二、1个参数
方法定义为:Optional<T> reduce(BinaryOperator<T> accumulator);
解析:
1)该方法接受一个BinaryOperator参数,BinaryOperator是一个@FunctionalInterface,继承 BiFunction,需要实现方法:R apply(T t, U u);
2)accumulator告诉reduce方法怎么去累计stream中的数据
内部的计算方式如下:
1 boolean foundAny = false; 2 T result = null; 3 for(T element : this stream){ 4 if(!foundAny){ 5 foundAny = true; 6 result = element; 7 } 8 else 9 result= accumulator.apply(result,element); 10 } 11 12 return foundAny ? Optional.of(result) : Optional.empty();
说明:
1) 由上面代码可以看出:将流中的第一个元素作为初始化值
2)这里返回值类型为Optional,这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。
举例:
1 public static void main(String[] args) { 2 Optional<Integer> opt = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(new BinaryOperator<Integer>() { 3 @Override 4 public Integer apply(Integer acc, Integer n) { 5 return acc + n; 6 } 7 }); 8 9 10 //Optional<Integer> opt = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce((acc, n) -> acc + n); 11 if (opt.isPresent()) { 12 System.out.println("累加和为:" + opt.get()); 13 } 14 }
三、3个参数
方法定义为:<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
1. 每个参数的说明
identity:给定一个初始值
accumulator:基于初始值,对元素进行收集归纳
combiner:对每个accumulator返回的结果进行合并,此参数只有在并行模式中生效
说明:
1)和前面的方法不同的是,多了一个combiner,这个combiner用来合并多线程计算的结果
2)BinaryOperator<U> combiner 操作的对象是第二个参数BiFunction<U,? super T,U> accumulator的返回值
2. 内部的计算方式如下:
1 U result = identity 2 for(T element: this stream) 3 result = accumulator.apply(result,element) 4 5 return result;
3. 参数2和参数3的区别
大家可能注意到了为什么accumulator的类型是BiFunction而combiner的类型是BinaryOperator?
public interface BinaryOperator<T> extends BiFunction<T,T,T>
BinaryOperator是BiFunction的子接口。BiFunction中定义了要实现的apply方法。reduce底层方法的实现只用到了apply方法,并没有用到接口中其他的方法,所以猜测这里的不同只是为了简单的区分。
4. 举例
例一:
串行模式,初始化值identity为0
public static void main(String[] args) { 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); }
打印结果为:
1 0(n1) + 1(n2) = 1 2 1(n1) + 2(n2) = 3 3 3(n1) + 3(n2) = 6 4 6(n1) + 4(n2) = 10 5 sum:10
例二:
并行模式,初始化值identity为0
1 public static void main(String[] args) { 2 int sum = Stream.of(1, 2, 3, 4) 3 .parallel() 4 .reduce(0, (n1, n2) -> { 5 int ret = n1 + n2; 6 System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret); 7 return ret; 8 }, (s1, s2) -> { 9 int ret = s1 + s2; 10 System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret); 11 return ret; 12 }); 13 System.out.println("sum:" + sum); 14 15 }
打印结果为:
0(n1) + 1(n2) = 1 0(n1) + 2(n2) = 2 0(n1) + 4(n2) = 4 0(n1) + 3(n2) = 3 1(s1) + 2(s2) = 3 3(s1) + 4(s2) = 7 3(s1) + 7(s2) = 10 sum:10
例三:
并行模式,初始化值identity为1
1 public static void main(String[] args) { 2 int sum = Stream.of(1, 2, 3, 4) 3 .parallel() 4 .reduce(1, (n1, n2) -> { 5 int ret = n1 + n2; 6 System.out.println(n1 + "(n1) + " + n2 + "(n2) = " + ret); 7 return ret; 8 }, (s1, s2) -> { 9 int ret = s1 + s2; 10 System.out.println(s1 + "(s1) + " + s2 + "(s2) = " + ret); 11 return ret; 12 }); 13 System.out.println("sum:" + sum); 14 }
打印结果为:
1 1(n1) + 1(n2) = 2 2 1(n1) + 3(n2) = 4 3 1(n1) + 2(n2) = 3 4 1(n1) + 4(n2) = 5 5 2(s1) + 3(s2) = 5 6 4(s1) + 5(s2) = 9 7 5(s1) + 9(s2) = 14 8 sum:14
预期的结果应该是11才对,即1 + 1 + 2 + 3 + 4。可以看到在并行模式下对identity的值是有要求的。 必须满足公式:accumulator.apply(identity, t) == t
3. 分析过程
这里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
4. fork/join 分支合并框架
stream.reduce()方法在并行模式下,即用Fork/Join的方式把一堆数据聚合成一个数据。
reduce运行草图如下:
结合草图,要实现stream.reduce()方法,必须要告诉JDK:
1) 你有什么需求数据要汇聚?(Stream已经提供了数据源,对应上面草图的A元素)
2)最后要汇聚成怎样的一个数据类型(对应reduce方法的参数一,对应上面草图的B元素)
3)如何将需求数据处理或转化成一个汇聚数据(对应reduce方法的参数二,对应上面草图的汇聚方式1)
4)如何将多个汇聚数据进行合并(对应reduce方法的参数三,对应上面草图的汇聚方式2)
四、总结
1. 使用reduce的满足条件
使用 reduce(identity, accumulator, combiner) 方法时,必须同时满足下面两个公式:
公式1:针对 accumulator:accumulator.apply(identity, t) == t
公式2:针对 combiner:combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
2. 2个参数和3个参数重载方式的关系
reduce(identity, accumulator) 其实是 reduce(identity, accumulator, combiner)的一种特殊形式,只不过是把combiner部分用accumulator来代替了
3. 并行流
并行计算下,如果identity是ArrayList等对象而非基本数据类型或String,那么结果跟我们想的可能很不一样,因为ArrayList非线程安全。
4. BinaryOperator
BinaryOperator是供多线程使用的,如果不在Stream中声明使用多线程,就不会使用子任务,自然也不会调用到该方法。另外多线程下使用BinaryOperator的时候是需要考虑线程安全的问题