java 8中的 reduce 规约使用以及详解

java 8中的reduce

简介

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

reduce 三个重载的方法

一、有一个参数

java 复制代码
Optional<T> reduce(BinaryOperator<T> accumulator);
1、涉及参数
复制代码
 ```text

BinaryOperator accumulator

BinaryOperator 继承于 BiFunction, 这里实现 BiFunction.apply(param1,param2) 接口即可。支持lambda表达式,形如:(result,item)->{...}

```

我们是否在疑问,为什么返回的是一个optional对象呢,我们看看源码是怎么样的,可以看到 result 结果是否为空的话,就用构建一个空对象,因为我们传过来的stream 可能是一个空的,在java 8中我们看到到处都有 optional的身影,就是为了避免空指针带来的一系列问题 ;

2、源码
markdown 复制代码
boolean foundAny = false;
     T result = null;
    for (T element : this stream) {
       if (!foundAny) {
           foundAny = true;
           result = element;
         }
             else
                  result = accumulator.apply(result, element);
         }
     return foundAny ? Optional.of(result) : Optional.empty();
3、代码实操
java 复制代码
 List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5, 6);

  Integer sum = integerList.stream().reduce((result, item) -> {
    return result + item;
 }).get();// 由于该地方返回的是一个 optional对象,所以需要单独 get 获取

### 得到结果
  总数:21  
4、参数详解

我们是否在好奇,形参中的 result,item 两个元素到底是什么呢,我们为了印证 将计算的结果都打印出来。 可以发现Lambda表达式中的两个参数(result,item)的含义

第一个参数 result :初始值为集合中的第一个元素,后面为每次的累加计算结果 ;
第二个参数 item :遍历的集合中的每一个元素(从第二个元素开始,第一个被result使用了)。

java 复制代码
integerList.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
result:15,item:6   

二、有二个参数

markdown 复制代码
T reduce(T identity, BinaryOperator<T> accumulator);
1、涉及参数
markdown 复制代码
**T** 初始值,  后续返回结果为该类型
**BinaryOperator** 跟上面相同第一个为元素第一个值,第二个参数为 每一个元素
对应的形参: (
   new Integer(0)
   ,
)
2、源码

我们看到跟一个参数的不同点在于,返回是一个明确的类型了,没有optional 对象了,原因是 第一个参数就是最终的返回类型了。

java 复制代码
T result = identity;
for (T element : this stream)
    result = accumulator.apply(result, element)
    return result;
}
3、代码实操
java 复制代码
 List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5, 6);

        Integer sum = integerList.stream().reduce(0,(result, item) -> {
            return result + item;
        });

## 运行结果
 总数:21   
4、参数详解
markdown 复制代码
**参数1**:T identity 为一个初始值(默认值) ,当集合为空时,就返回这个默认值,当集合不为空时,该值也会参与计算;
**参数2**:BinaryOperator<T> accumulator 这个与一个参数的reduce相同。
返回值:并非 Optional,由于有默认值 identity ,因此计算结果不存在空指针的情况。
java 复制代码
#当我们创建一个空的集合,统计后看返回的数据是否为第一个
integerList = new ArrayList<>();
Integer total = integerList.stream().reduce(0, (result, item) -> {
    return result + item;
});
# 结果 0
    
Integer total = integerList.stream().reduce(10, (result, item) -> {
    System.out.println("result:"+result + ",item:"+item);
    return result + item;
});
 # 结果 10  并且  result 没有打印
  

三、有三个参数

1、涉及参数
markdown 复制代码
<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);
第一个参数和第二个参数的定义同上,** 第三个参数比较特殊,后面看结果 **                 
2、源码

我们看到跟二个参数的并没有什么不同,那么可以肯定的是,第三个参数会有所影响

复制代码
*     U result = identity;
*     for (T element : this stream)
*         result = accumulator.apply(result, element)
*     return result;
* }
3、代码实操

我们新建一个复杂类型的学生对象,用来计算学校的分数信息

jav 复制代码
  public List<Student> initStudents() {
   List<Student> students = Arrays.asList(new Student("张三",90)
                ,new Student("李四",10),new Student("王五",60)
                ,new Student("赵六",1)
                ,new Student("王二麻子",1));
    return students;            
  }
  
  
  
  static class Student {
        private String name;

        private double score;

        public Student(String name, double score) {
            this.name = name;
            this.score = score;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public double getScore() {
            return score;
        }

        public void setScore(double score) {
            this.score = score;
        }
    }
结果1
java 复制代码
Double total = students.stream().reduce(new Double(0), (result, score) -> {
    return result + score.getScore();// 累加计算 accumulator
}, (in1, in2) -> {
    double result = in1 + in2;
    return result;
});// 第三个参数 combiner*/

System.out.println("分数:"+total);

## 打印结果:
分数:162.0
结果2
java 复制代码
 double sum = students.stream().mapToDouble(Student::getScore).sum();

我们得到结论,结果1 和结果2 走的是同一个操作

第三个参数 BinaryOperator++combiner++

这个参数的形参我写的是:(in1, in2) -> in1 + in2; 我们现在将结果打印出来看看

java 复制代码
Double total = students.stream().reduce(new Double(0), (result, score) -> {
    return result + score.getScore();// 累加计算 accumulator
}, (in1, in2) -> {
    double result = in1 + in2;
    System.out.println("in1="+in1+", in2="+in2);
    return result;
});
# 这里的参数根本没有打印,也就是说根本没有进来    

现在变更为并行流试试,并且打印线程id

java 复制代码
 Double total = students.stream().parallel().reduce(new Double(0), (result, score) -> {
     return result + score.getScore();// 累加计算 accumulator
 }, (in1, in2) -> {
     double result = in1 + in2;
     System.out.println("threadid:"+Thread.currentThread().getId()+",in1="+in1+", in2="+in2);
     return result;
 });
## 运行结果
threadid:1,in1=1.0, in2=1.0
threadid:1,in1=60.0, in2=2.0
threadid:12,in1=90.0, in2=10.0
threadid:12,in1=100.0, in2=62.0
计算结果----------:162.0
4、参数详解

把 stream 换成并行的 parallelStream,

可以看出,有两个线程在执行任务:线程13和线程1 ,

每个线程会分配几个元素做计算,

可以多跑几次,每次执行的结果不一定相同,如果看不出来规律,可以尝试增加集合中的元素个数,数据量大更有利于并行计算发挥作用。

5、总结

因此,第三个参数 BinaryOperator<U> combiner 的作用为:「汇总所有线程的计算结果得到最终结果」

并行计算会启动多个线程执行同一个计算任务,每个线程计算完后会有一个结果,最后要将这些结果汇总得到最终结果。

6、把第一个参数 identity 从0换成1
java 复制代码
// 汇总score 字段总数
Double total2 = students.stream().parallel().reduce(new Double(1), (result, score) -> {
    System.out.println("$$ threadId="+Thread.currentThread().getId()+", integer="+result+", score="+score.getScore());
    return result + score.getScore();// 累加计算 accumulator
}, (in1, in2) -> {
    double result = in1 + in2;
    System.out.println("threadid:"+Thread.currentThread().getId()+",in1="+in1+", in2="+in2);
    return result;
});/
            
## 运行结果
$$ threadId=1, integer=1.0, score=60.0
$$ threadId=12, integer=1.0, score=10.0
$$ threadId=11, integer=1.0, score=1.0
$$ threadId=13, integer=1.0, score=1.0
$$ threadId=14, integer=1.0, score=90.0
threadid:13,in1=2.0, in2=2.0
threadid:14,in1=91.0, in2=11.0
threadid:13,in1=61.0, in2=4.0
threadid:13,in1=102.0, in2=65.0
计算结果----------:167.0    

结论

预期结果应该是163(初始值1+原来的结果162),但实际结果为167,多加了4次1,猜测是多加了四次初始值,「从打印的结果可以发现:」

(1)并行计算时用了5个线程(线程id依次为:1, 12, 11, 13, 14),汇总合并时用了两个线程(线程id为13和14)

(2)并行计算的每一个线程都用了初始值参与计算,因此多加了4次初始值。

**「总结:」**使用 parallelStream 时,初始值 identity 应该设置一个不影响计算结果的值,比如本示例中设置为 0 就不会影响结果。

我觉得这个初始值 identity 有两个作用:确定泛型U的类型避免空指针

但是如果初始值本身就是一个复杂对象那该怎么办呢?

比如是初始值是一个数组,那么应该设定为一个空数组。如果是其他复杂对象那就得根据你reduce的具体含义来设定初始值了。

java 复制代码
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计算时就不会产生错误结果
相关推荐
李慕婉学姐1 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆2 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin3 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20053 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉3 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国3 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882484 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈4 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_994 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹4 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理