当前位置: 首页 > 工具软件 > Reduce > 使用案例 >

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

杨飞
2023-12-01

说明

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计算时就不会产生错误结果

 类似资料: