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计算时就不会产生错误结果
相关推荐
Aliano2172 分钟前
Java的jackson库
java·开发语言
昙鱼24 分钟前
Maven的了解与使用
java·maven
北纬39°的风38 分钟前
从0开始搭建一个生产级SpringBoot2.0.X项目(十)SpringBoot 集成RabbitMQ
java·spring boot·redis
forestqq1 小时前
设置JAVA以适配华为2288HV2服务器的KVM控制台
java·运维·服务器
LUwantAC1 小时前
Java学习路线:Maven(一)认识Maven
java·学习·maven
勤匠1 小时前
使用 Stream 处理集合数据【Java 1.8 新特性】
java
OKkankan1 小时前
单链表的实现(数据结构)
java·c语言·数据结构·c++
威哥爱编程1 小时前
适合才最美:Shiro安全框架使用心得
java·shiro·javaee
Lyqfor1 小时前
Redis学习:BitMap/HyperLogLog/GEO案例 、布隆过滤器BloomFilter、缓存预热+缓存雪崩+缓存击穿+缓存穿透
java·数据库·redis·学习·算法·缓存·1024程序员节
小菜不菜_xc1 小时前
小菜家教平台(二):基于SpringBoot+Vue打造一站式学习管理系统
java·spring