java 8


一、reduce方法

stream api的reduce方法用于对stream中元素进行聚合求值,最常见的用法就是将stream中一连串的值合成为单个值,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce,比如为一个包含一系列数值的数组求和。

reduce方法有三个重载的方法,方法签名如下

//重载方法一
Optional<T> reduce(BinaryOperator<T> accumulator);
//重载方法二
T reduce(T identity, BinaryOperator<T> accumulator);
//重载方法三
<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

第一个签名方法

接受一个BinaryOperator类型的lambada表达式, 常规应用方法如下

List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);

代码实现了对numList中的元素累加。lambada表达式的a参数是表达式的执行结果的缓存,也就是表达式这一次的执行结果会被作为下一次执行的参数,而第二个参数b则是依次为stream中每个元素。如果表达式是第一次被执行,a则是stream中的第一个元素。

int result = numList.stream().reduce((a,b) -> {
  System.out.println("a=" + a + ",b=" + b);
  return a + b;
} ).get();

在表达式中假如打印参数的代码,打印出来的内容如下

a=1,b=2
a=3,b=3
a=6,b=4
a=10,b=5

表达式被调用了4次, 第一次a和b分别为stream的第一和第二个元素,因为第一次没有中间结果可以传递, 所以 reduce方法实现为直接将第一个元素作为中间结果传递。

第二个签名

T reduce(T identity, BinaryOperator<T> accumulator);

与第一个签名的实现的唯一区别是它首次执行时表达式第一次参数并不是stream的第一个元素,而是通过签名的第一个参数identity来指定。我们来通过这个签名对之前的求和代码进行改进

List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce(0,(a,b) ->  a + b );
System.out.println(result);

示例:

String concat1 = Stream.of("A", "B", "C", "D").reduce("", String::concat);
log.info("拼接1的值为:{}",concat1);
拼接1的值为:ABCD

其实这两种实现几乎差别,第一种比第一种仅仅多了一个字定义初始值罢了。 此外,因为存在stream为空的情况,所以第一种实现并不直接方法计算的结果,而是将计算结果用Optional来包装,我们可以通过它的get方法获得一个Integer类型的结果,而Integer允许null。第二种实现因为允许指定初始值,因此即使stream为空,也不会出现返回结果为null的情况,当stream为空,reduce为直接把初始值返回。

累计运算的概念
以下面的这个方法为例解析
在这里插入图片描述

BinaryOperator 是BiFunction 的三参数特殊化形式,两个入参和返回结果都是类型T

计算1,2,3,4,5 的和,并且初始值为3
也就是计算3+1+2+3+4+5

1.使用Stream 两个参数的reduce方法进行归约运算
2.使用for循环迭代调用BinaryOperator 的apply进行运算

其实两种方式背后的思维方式是一样的
那就是
结果重新作为一个参数,不断地参与到运算之中,直到最后结束
理解reduce的含义重点就在于理解"累 加 器" 的概念
在这里插入图片描述
示例:

// 按行读取配置文件:
        List<String> props = Lists.newArrayList("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> map = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    Map<String, String> map1 = new HashMap<>();
                    map1.put(ss[0], ss[1]);
                    return map1;
                })
                // 把所有Map聚合到一个Map:
                .reduce(new HashMap<String, String>(), (m, kv) -> {
                    m.putAll(kv);
                    return m;
                });
        // 打印结果:
        map.forEach((k, v) -> {
            System.out.println(k + " = " + v);
        });


        List<Map<String, String>> collect = props.stream()
                // 把k=v转换为Map[k]=v:
                .map(kv -> {
                    String[] ss = kv.split("\\=", 2);
                    Map<String, String> map1 = new HashMap<>();
                    map1.put(ss[0], ss[1]);
                    return map1;
                }).collect(Collectors.toList());
        System.out.println("collect = " + collect);
结果:
logging = warn
interval = 500
debug = true
profile = native

collect = [{profile=native}, {debug=true}, {logging=warn}, {interval=500}]

第三个签名

三个参数的reduce

<U> U reduce(U identity, 
             BiFunction<U, ? super T, U> accumulator, 
             BinaryOperator<U> combiner); 

它的形式类似于
在这里插入图片描述

与两个参数的reduce不同的地方在于类型
双参数的返回类型为T Stream类型为T
三参数的返回类型为U Stream类型为T 有了更大的发挥空间 T可能为U 也可能不是U

如上面的代码示例,stream中的类型为int,那么计算结果也必须为int,这导致了灵活性的不足,甚至无法完成某些任务,
比入我们要对一个一系列int值求和,但是求和的结果用一个int类型已经放不下,必须升级为long类型,此实第三签名就能发挥价值了,它不将执行结果与stream中元素的类型绑死。

List<Integer> numList = Arrays.asList(Integer.MAX_VALUE,Integer.MAX_VALUE);
long result = numList.stream().reduce(0L,(a,b) ->  a + b, (a,b)-> 0L );
System.out.println(result);

如上代码所示,它能见int类型的列表合并成long类型的结果。
当然这只是其中一种应用罢了,犹豫拜托了类型的限制我们还可以通过他来灵活的完成许多任务,比入将一个int类型的ArrayList转换成一个String类型的ArrayList

List<Integer> numList = Arrays.asList(1, 2, 3, 4, 5, 6);
ArrayList<String> result = numList.stream().reduce(new ArrayList<String>(), (a, b) -> {
    a.add("element-" + Integer.toString(b));
    return a;
}, (a, b) -> null);
System.out.println(result);

执行结果为

[element-1, element-2, element-3, element-4, element-5, element-6]

这个示例显得有点鸡肋,一点不实用,不过在这里我们的主要目的是说明代码能达到什么样的效果,因此代码示例也不必取自实际的应用场景。

从上面两个示例可以看出第三个reduce比前面两个强大的多,它的功能已经完全覆盖前面两个的实现,如果我们不考虑代码的简洁性,甚至可以抛弃前面两个。

另外,还需要注意的是这个reduce的签名还包含第三个参数,一个BinaryOperator类型的表达式。在常规情况下我们可以忽略这个参数,敷衍了事的随便指定一个表达式即可,目的是为了通过编译器的检查,因为在常规的stream中它并不会被执行到,然而, 虽然此表达式形同虚设,可是我们也不是把它设置为null,否者还是会报错。 在并行stream中,此表达式则会被执行到

并行操作示例

Integer parallelResult1 = Stream.of(1,2,3).parallel().reduce(10, (x, y) -> x + y, (x, y) -> x * y);
log.info("并行下打印结果:{}",parallelResult1);
log日志结果为
并行下打印结果:1716

上面并行的操作步骤大致如下:

先fork,在每个并行子线程中都执行:x+y
线程一:10+1
线程二:10+2
线程三:10+3

再join,对每个子线程计算出的结果进行累乘:x * y
result  = 线程一的结果
result *= 线程二的结果
result *= 线程三的结果
得到result = 1716

注意,重载方法三的第三个参数在非并行Stream下是不起作用的,因为没有combiner的过程(或者说join的过程),也就没有x*y的过程,那么上面的例子就和使用重载方法一得到的结果一致

Integer result1 = Stream.of(1,2,3).reduce(10, (x, y) -> x + y, (x, y) -> x * y);
log.info("非并行下打印结果:{}",result1);
Integer result2 = Stream.of(1,2,3).reduce(10, (x, y) -> x + y);
log.info("非并行下重载方法一打印结果:{}",result2);
log日志结果为
非并行下打印结果:16
非并行下重载方法一打印结果:16

注意,第三个参数的作用在于并行计算下 合并各个线程的计算结果
并行流运行时内部使用了fork-join框架
在这里插入图片描述

多线程时,多个线程同时参与运算
多个线程执行任务,必然会产生多个结果
那么如何将他们进行正确的合并
这就是第三个参数的作用
大致处理流程如下:
在这里插入图片描述

从流程上看的 结果R是一直参与运算的
所以,不合理使用重载方法三会出现下面的情况:

log.info("非并行累加结果:{}",Stream.of(1,2,3,4,5).reduce(6,(x,y) -> x + y,(x,y) -> x + y));
log.info("并行累加结果:{}",Stream.of(1,2,3,4,5).parallel().reduce(6,(x,y) -> x + y,(x,y) -> x + y));
log结果为:
非并行累加结果:21
并行累加结果:45

结果不同是因为

((((5+1)+2)+3)+4)+5(5+1)+ (5+2)+ (5+3)+ (5+4)+ (5+5)

运算结果不相同
那么这个方法不是有问题么?其实不然,有问题的是我们的写法,java api文档中进行了明确的说明要求:

第一点:identity 的值对于合并运算combiner来说必须是一个恒等式,也就是说对于任意的u, combiner(identity,u) 和u是相同的 —— for all u, combiner(identity, u) is equal to u.即重载方法三对identity取值有严格要求
这句话看起来怪怪的,对于任意的u 经过合并运算 竟然还是u,那还要这个干嘛??
从我们上面的并行处理流程可以看得出来,这个result 的初始identity 对于每一个分支都是参与运算的!
这也是为什么要求:
任意的u, combiner(identity,u) 和u是相同的
的原因
我们之所以会错,就是因为没有达到要求
我们的combiner为 (a,b)->a+b;
那么如果分为两个分支进行运算,我们的初始值identity就参与了两次运算 也就是说多加了两个identity的值!!
怎么样才能保证u = combiner(identity,u)
除非identity=0 这才是对于 (a,b)->a+b 来说能够保障u = combiner(identity,u)
否则,你就不要用(a,b)->a+b 这个combiner
我们把Identity换成0之后

log.info("非并行累加结果:{}",Stream.of(1,2,3,4,5).reduce(0,(x,y) -> x + y,(x,y) -> x + y));
log.info("并行累加结果:{}",Stream.of(1,2,3,4,5).parallel().reduce(0,(x,y) -> x + y,(x,y) -> x + y));
//这里得到的结果都是一致的,都为15
combiner必须和accumulator要兼容,对于任意的u 和 t
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

这到底是什么意思呢?现在有如下场景

假设说4个元素 1,2,3,4  需要运算
此时假设已经 1,2,3 三组数据已经运算结束,马上要同第四组运算 
如果是并行,我们假定1,2,3 在一个分支   4单独在另一分支

并行时
U为已经计算好的1,2,3后的结果,接下来要与另一组的4合并
T4则是identity与T参与运算
combiner.apply(u, accumulator.apply(identity, t))
在这里插入图片描述
非并行时
u直接与下一个元素进行结合运算在这里插入图片描述
显然这只是并行和非并行两种不同的处理运算方式,他们应该是相同的
也就是

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

注:图引自他人博客,非本人原创。

二、排序

排序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值