java8集合Stream之reduce聚合函数——看这篇就够了

说明

java8集合中Stream()相关函数都支持lambda表达式,reduce()就是其中之一,
reduce是一种聚合操作,聚合的含义就是将多个值经过特定计算之后得到单个值,
常见的 count 、sum 、avg 、max 、min 等函数就是一种聚合操作。

本文使用reduce函数做求和计算来说明它的用法:

reduce有三个重载方法

1.一个参数的reduce

Optional<T> reduce(BinaryOperator<T> accumulator);

参数: BinaryOperator<T> accumulator , 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<Integer> 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<Integer> 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, BinaryOperator<T> accumulator);

参数1:T identity 为一个初始值(默认值) ,当集合为空时,就返回这个默认值,当集合不为空时,该值也会参与计算;
参数2:BinaryOperator<T> accumulator 这个与一个参数的reduce相同。
返回值:并非 Optional,由于有默认值 identity ,因此计算结果不存在空指针的情况。

List<Integer> 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> U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

第一个参数和第二个参数的定义同上,第三个参数比较特殊,后面慢慢讲。

可以看到该方法有两个泛型 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<ScoreBean> 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  

第三个参数 BinaryOperator<U> combiner 是个什么鬼?
这个参数的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的入参进行汇总计算。
可以多跑几次,每次执行的结果不一定相同,如果看不出来规律,可以尝试增加集合中的元素个数,数据量大更有利于并行计算发挥作用。

因此,第三个参数 BinaryOperator<U> combiner 的作用为:汇总所有线程的计算结果得到最终结果
并行计算会启动多个线程执行同一个计算任务,每个线程计算完后会有一个结果,最后要将这些结果汇总得到最终结果。

我们再来看一个有意思的结果,把第一个参数 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计算时就不会产生错误结果










發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章