reduce的三种重载

reduce的三种重载

reduce的重载秘密 在之前看SIA5的过程中,比较头疼的就是lambda表达式还不是很熟练,反应式的流全部都要用函数式编程,这两天正好在补一补lambda的相关知识。 Java 8里对于lambda最多的应用就是集合加上一个.stream()变成Stream接口对象,然后用lambda表达式操作

reduce的重载秘密

在之前看SIA5的过程中,比较头疼的就是lambda表达式还不是很熟练,反应式的流全部都要用函数式编程,这两天正好在补一补lambda的相关知识。 Java 8里对于lambda最多的应用就是集合加上一个.stream()变成Stream接口对象,然后用lambda表达式操作。其中常用的map,filter,reduce方法的参数都是Java 8里新增的一批函数式接口@FunctionalInterface。 昨晚在看《Java 8 函数式编程》的时候,仿照书上的例子写了段代码:
ArrayList<String> s = stringList.stream().reduce(new ArrayList<String>(), (x, y) -> {
    if (y.length() > 3) {
        x.add(y);

    }
    return x;
}, (x, y) -> x);
但是发现如果把第三个参数去掉,就总是报错。一开始没有想明白,由于reduce的原理是不断的操作前两个数字(包含或者不包含初始值),得到一个结果继续和第三个数字计算,那我把初始值设置为一个新的集合,然后每次用集合和下一个元素操作,将下一个元素加到集合中来,再返回这个集合,逻辑上应该不存在任何问题。 后边反复看了reduce的三个重载方法,才发现奥秘所在。 reduce的三个重载分别是一个参数,二个参数和三个参数的重载,如下:
  1. Optional<T> reduce(BinaryOperator<T> accumulator)
  2. T reduce(T identity, BinaryOperator<T> accumulator);
  3. <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

单参数的reduce

从泛型方法可以看到,单参数的类型限定死了,一定就是Stream<T>中的类型T,而且由于没有初始值,是直接从流的前两个元素开始计算。 其中的参数BinaryOperator<T>就是我遇到的坑,这个参数和两参数的reduce重载方法的第二个参数是一样的。 BinaryOperator<T>是一个特殊的BiFunction,其定义是 public interface BinaryOperator<T> extends BiFunction<T,T,T>。 可以发现,这个参数强制要求两个传入的类型和返回类型都相同,所以单参数和两参数的reduce其实没有什么花头,能返回的东西都已经限定好了。 单参数的特点是返回一个Optional<T>对象,这也是Java 8新增的对象,用于包装可能是null的对象,在null的情况下可以通过.orElse()返回内容。

两参数的reduce

两参数的reduce相比单参数,只不过多了一个初始值,在计算的时候,是拿初始值和流内第一个元素进行计算,然后再依次与其他元素进行计算。 其类型限定依然已经定死,就是流的类型。而且不会返回Optional对象,这是因为有初始值,所以结果不可能是null。 我在最开始提到的问题就是,删掉第三个参数之后,类型已经限定死了,初始值的类型必须与流中的元素类型一致,所以总是不通过。 如果要实现自定义的逻辑,那就必须采用三参数的reduce了,因为三参数的reduce的第二个参数与前两个重载不同。

三参数的reduce

三参数的reduce的第二个参数是BiFunction<U, ? super T, U> accumulator,这是任意一个类型(初始值的类型)与流元素的超类(因为要和流元素做运算)做运算,返回初始值类型的BiFunction函数式接口,采用这个,就可以解决我开头的问题了。 那么第三个参数有什么用呢?第三个参数是两个相同的类型计算出一个相同的类型,而且都是最终返回类型。 第三个参数只有在并行计算即.stream().parallel()之后才会发生作用。 回想一下,如果不采用并行计算,reduce的计算方式肯定是单线程的,因为不可能直接计算出最终结果,而是每一个计算都依赖于上一个计算。 而并行计算的话,很显然与非并行计算的计算方式不同,如果还是按照原来reduce的方式进行计算,多线程也没有用,因为结果一步一步都互相依赖。 实际上,并行计算之后,计算方式不再是将结果依次与下一个元素计算,而是直接拿初始值与流中的每一个元素进行计算,这个过程被第二个参数控制。 计算出来的各个结果如何合并,则由第三个参数进行控制。 用一个简单的例子来说明:
Stream<Integer> integerStream = Stream.of(3, 4, 5, 6, 7);
Stream<Integer> integerStream2 = Stream.of(3, 4, 5, 6, 7);

int result = integerStream2.reduce(10, (x, y) -> x + y);

int parellelResult = integerStream.parallel().reduce(10, (x, y) -> x + y, (x, y) -> (x * y));

System.out.println("单线程操作的结果是:"+result);

System.out.println("并行操作的结果是:"+parellelResult);
单线程操作的结果很明显,是10+3+4+5+6+7=35。 并行操作呢,结果实际上是(10+3)*(10+4)*(10+5)*(10+6)*(10+7),第二个参数中的(x,y)实际上变成了初始值和每一个元素的运算过程,最后一个参数则表示将所有的结果相乘。 最后还要提一句的是,标准的BiFunction函数式接口是:
public interface BiFunction<T, U, R> {
    R apply(T t, U u);
}
看仔细喽,reduce方法中的BiFunction参数全部都不是标准的BiFunction函数式接口。这就是昨晚让我一开始困惑的地方。

结论

所以reduce一共有三个重载方法,但是有四种应用场合,两种计算逻辑。应用reduce的情况可以总结如下:
  1. 不需要初始值且无需改变返回类型,使用单参数的reduce方法,注意返回的是Optional对象
  2. 需要初始值且无需改变返回类型,使用两参数的reduce方法,直接返回指定类型
  3. 单线程对所有元素依次操作,需要返回其他类型,使用三参数的reduce方法,第三个参数随便传个lambda表达式即可,没有影响
  4. 多线程同时操作所有元素然后进行联合,需要增加.parallel(),此时reduce的计算逻辑会发生变化。
如何理解单线程reduce的思想呢,就拿编程这事来说吧,经常面临的情况是:
  • 想要会A,发现需要懂B
  • 想懂B,发现需要掌握C
  • 想掌握C,发现需要了解D
  • 想了解D,发现需要深入理解E
  • 想深入了解E,发现需要精通F
然后就开始看F,看完以后得到了精通F的自己,然后再看E,得到了精通F和理解E的自己,一直到看完A。得到了懂ABCDEF的自己。 链式调用,也是一种函数式的改造,比如myself.learn(F).learn(E)....
LICENSED UNDER CC BY-NC-SA 4.0
Comment