说明
java8集合中Stream()相关函数都支持lambda表达式,reduce()就是其中之一,
reduce是一种聚合操作,聚合的含义就是将多个值经过特定计算之后得到单个值,
常见的 count 、sum 、avg 、max 、min 等函数就是一种聚合操作。
本文使用reduce函数做求和计算来说明它的用法:
reduce有三个重载方法
1.一个参数的reduce
Optionalreduce(BinaryOperatoraccumulator);
参数: BinaryOperatoraccumulator , BinaryOperator 继承于 BiFunction, 这里实现 BiFunction.apply(param1,param2) 接口即可。支持lambda表达式,形如:(result,item)->{...} 。
返回值:返回Optional对象,由于结果存在空指针的情况(当集合为空时)因此需要使用Optional。
如下代码通过reduce 求整数集合中的元素之和:
import com.google.common.collect.Lists;
import java.util.List;
public class LambdaTest {
public static void main(String[] args) {
List list=Lists.newArrayList(1,2,3,4,5);
//将数组进行累加求和
//由于返回的是 Optional ,因此需要get()取出值。
Integer total=list.stream().reduce((result,item)->result+item).get();
System.out.println(total);
}
}
//结果为: 15
将累加的每一步打印,可以发现Lambda表达式中的两个参数(result,item)的含义:
第一个参数 result :初始值为集合中的第一个元素,后面为每次的累加计算结果 ;
第二个参数 item :遍历的集合中的每一个元素(从第二个元素开始,第一个被result使用了)。
List list=Lists.newArrayList(1,2,3,4,5);
list.stream().reduce((result,item)->{
System.out.println("result="+result+", item="+item);
return result+item;
});
/* 结果如下:
result=1, item=2
result=3, item=3
result=6, item=4
result=10, item=5
*/
2.两个参数的reduce
T reduce(T identity, BinaryOperatoraccumulator);
参数1:T identity 为一个初始值(默认值) ,当集合为空时,就返回这个默认值,当集合不为空时,该值也会参与计算;
参数2:BinaryOperatoraccumulator 这个与一个参数的reduce相同。
返回值:并非 Optional,由于有默认值 identity ,因此计算结果不存在空指针的情况。
List list=Lists.newArrayList(1,2,3,4,5);
Integer total=list.stream().reduce(0,(result,item)->result+item);
System.out.println(total);//结果为:15
list=new ArrayList<>();
total=list.stream().reduce(0,(result,item)->result+item);
System.out.println(total);//数组为空时,结果返回默认值0
3.三个参数的reduce
U reduce(U identity, BiFunctionaccumulator,BinaryOperatorcombiner);
第一个参数和第二个参数的定义同上,第三个参数比较特殊,后面慢慢讲。
可以看到该方法有两个泛型 T 和 U :
(1)泛型T是集合中元素的类型,
(2)泛型U是计算之后返回结果的类型,U的类型由第一个参数 identity 决定。
也就是说,三个参数的reduce()可以返回与集合中的元素不同类型的值,方便我们对复杂对象做计算式和转换。
而一个参数和两个参数的reduce()只能返回与集合中的元素同类型的值。
现在我们在集合中存放 ScoreBean 对象,模拟学生分数统计:
static class ScoreBean {
private String name; //学生姓名
private int score; //分数,需要汇总该字段
public ScoreBean(String name, int score) {
this.name = name;
this.score = score;
}
//get 和 set 方法省略
}
我们对 ScoreBean 中 score 字段汇总:(后面示例代码中的list定义省略,都用这个)
List list= Lists.newArrayList(
new ScoreBean("张三",1)
,new ScoreBean("李四",2)
,new ScoreBean("王五",3)
,new ScoreBean("小明",4)
,new ScoreBean("小红",5));
Integer total=list.stream()
.reduce(
Integer.valueOf(0) /*初始值 identity*/
,(integer,scoreBean)->integer+scoreBean.getScore() /*累加计算 accumulator*/
,(integer1,integer2)->integer1+integer2 /*第三个参数 combiner*/
);
System.out.println(total);//结果:15
其实这个相当于:
Integer total=list.stream().mapToInt(ScoreBean::getScore).sum();
System.out.println(total);//结果也是:15
第三个参数 BinaryOperatorcombiner 是个什么鬼?
这个参数的lambda表达式我是这么写的:(integer1,integer2)->integer1+integer2)
现在我将其打印出来:
Integer total=list.stream()
.reduce(
Integer.valueOf(0)
,(integer,scoreBean)->integer+scoreBean.getScore()
,(integer1,integer2)->{
//这个println居然没有执行!!!
System.out.println("integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
发现这个参数的lambda表达式根本就没有执行?!
我换了一种方式,换成 parallelStream ,然后把线程id打印出来:
//Integer total=list.stream()
Integer total=list.parallelStream()
.reduce(
Integer.valueOf(0)
,(integer,scoreBean)->integer+scoreBean.getScore()
,(integer1,integer2)->{
//由于用的 parallelStream ,可发生并行计算,所以我增加线程id的打印:
System.out.println("threadId="+Thread.currentThread().getId()+", integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
/*结果如下:
threadId=13, integer1=1, integer2=2
threadId=1, integer1=4, integer2=5
threadId=1, integer1=3, integer2=9
threadId=1, integer1=3, integer2=12
*/
把 stream 换成并行的 parallelStream,
可以看出,有两个线程在执行任务:线程13和线程1 ,
每个线程会分配几个元素做计算,
如上面的线程13分配了元素1和2,线程1分配了3、4、5。
至于线程1为什么会有两个3,是由于线程13执行完后得到的结果为3(1+2),而这个3又会作为后续线程1的入参进行汇总计算。
可以多跑几次,每次执行的结果不一定相同,如果看不出来规律,可以尝试增加集合中的元素个数,数据量大更有利于并行计算发挥作用。
因此,第三个参数 BinaryOperatorcombiner 的作用为:汇总所有线程的计算结果得到最终结果。
并行计算会启动多个线程执行同一个计算任务,每个线程计算完后会有一个结果,最后要将这些结果汇总得到最终结果。
我们再来看一个有意思的结果,把第一个参数 identity 从0换成1:
//Integer total=list.stream()
Integer total=list.parallelStream()
.reduce(
Integer.valueOf(1)
,(integer,scoreBean)->{
System.out.println("$ threadId="+Thread.currentThread().getId()+", integer="+integer+", scoreBean.getScore()="+scoreBean.getScore());
return integer+scoreBean.getScore();
}
,(integer1,integer2)->{
System.out.println("threadId="+Thread.currentThread().getId()+", integer1="+integer1+", integer2="+integer2);
return integer1+integer2;
}
);
System.out.println("result="+total);
/* 运行结果如下:
$ threadId=12, integer=1, scoreBean.getScore()=2
$ threadId=1, integer=1, scoreBean.getScore()=3
$ threadId=14, integer=1, scoreBean.getScore()=5
$ threadId=13, integer=1, scoreBean.getScore()=1
$ threadId=15, integer=1, scoreBean.getScore()=4
threadId=13, integer1=2, integer2=3
threadId=15, integer1=5, integer2=6
threadId=15, integer1=4, integer2=11
threadId=15, integer1=5, integer2=15
result=20
*/
预期结果应该是16(初始值1+原来的结果15),但实际结果为20,多加了4次1,猜测是多加了四次初始值,
从打印的结果可以发现:
(1)并行计算时用了5个线程(线程id依次为:12, 1, 14, 13, 15),汇总合并时用了两个线程(线程id为13和15)
(2)并行计算的每一个线程都用了初始值参与计算,因此多加了4次初始值。
总结:
使用 parallelStream 时,初始值 identity 应该设置一个不影响计算结果的值,比如本示例中设置为 0 就不会影响结果。
我觉得这个初始值 identity 有两个作用:确定泛型U的类型 和 避免空指针。
但是如果初始值本身就是一个复杂对象那该怎么办呢?
比如是初始值是一个数组,那么应该设定为一个空数组。如果是其他复杂对象那就得根据你reduce的具体含义来设定初始值了。
用表达式来解释就是初始值identity应该满足以下等式:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
//combiner.apply(u1,u2) 接收两个相同类型U的参数
//accumulator.apply(u, t) 接收两个不同类型的参数U和T,U是返回值的类型,T是集合中元素的类型
//这个等式恒等,parallelStream计算时就不会产生错误结果