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的三个重载分别是一个参数,二个参数和三个参数的重载,如下:
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);
单参数的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的情况可以总结如下:
- 不需要初始值且无需改变返回类型,使用单参数的reduce方法,注意返回的是Optional对象
- 需要初始值且无需改变返回类型,使用两参数的reduce方法,直接返回指定类型
- 单线程对所有元素依次操作,需要返回其他类型,使用三参数的reduce方法,第三个参数随便传个lambda表达式即可,没有影响
- 多线程同时操作所有元素然后进行联合,需要增加
.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)....