这个例子对于Java 8流来说非常棘手。它们被设计用于顺序不重要的操作。
函数应用不是结合律的。为了解释,让我们举一个更简单的例子,假设我们想要取一个数字并将其除以一系列数字:
static List<Double> dividers = Arrays.asList( 3.5, 7.0, 0.5, 19.0 );
public double divideByList( double a ) {
for ( Double d : dividers ) {
a /= d;
}
return a;
}
所以,你得到的是
a ÷ 3.5 ÷ 7.0 ÷ 0.5 ÷ 19.0
这个算术很简单 - 除法是左结合的,意味着这等同于
a ÷ ( 3.5 × 7.0 × 0.5 × 19.0)
不是
a ÷ ( 3.5 ÷ 7.0 ÷ 0.5 ÷ 19.0 )
和不是
( a ÷ 3.5 ÷ 7.0 ) ÷ ( 0.5 ÷ 19.0 )
基于reduce/collectors的流操作要求“归约”操作是左结合的。这是因为它们想允许该操作并行化,使得一些线程执行一些操作,然后将结果组合起来。现在,如果您的运算符是乘法而不是除法,那就不会有问题,因为
a × 3.5 × 7.0 × 0.5 × 19.0
等同于
(a × 3.5 × 7.0 ) × (0.5 × 19)
这意味着一个线程可以执行 a × 3.5 × 7.0
,另一个线程可以执行 0.5 × 19.0
操作,然后你可以将结果相乘,得到与顺序计算相同的结果。但对于除法来说,这种方法行不通。
函数应用也是非关联的,就像除法一样。也就是说,如果你有函数 f
、g
和 h
,并且你运行顺序计算,你最终会得到:
result = val + f(val) + g(val + f(val)) + h(val + f(val) + g(val + f(val)))
现在,如果您有两个中间线程,一个应用f
和g
,另一个应用h
,并且您想要组合结果-没有办法将正确的值放入h
。
你可能会尝试使用像Stream.reduce
这样的方法,就像@Bohemian建议的那样。但是文档警告你不要这样做:
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
...
The identity value must be an identity for the combiner function. This means that for all u, combiner(identity, u) is equal to u. Additionally, the combiner function must be compatible with the accumulator function; for all u and t, the following must hold:
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
对于像+
这样的操作,其身份是0。对于*
,其身份是1。因此,将您的val
用作identity
是违反文档规定的。而第二个条件更加棘手。
尽管非并行流的当前实现不使用combiner部分,这使得两个条件都是不必要的,但这是未记录在文档中的,未来的实现或不同JRE的实现可能决定创建中间结果并使用combiner将它们连接起来,也许是为了提高性能或出于其他考虑。
因此,尽管很诱人,但不应该使用Stream.reduce
来尝试模仿原始的顺序处理。
有一种方法可以做到这一点,而不会真正破坏文档。它涉及保持一个可变对象来保存结果(必须是对象,以便在仍然可变的同时有效地最终),并使用{{link1:Stream.forEachOrdered
}},如果流是有序的,则保证操作将按照它们在流中出现的顺序执行。列表的流具有定义的顺序。即使您使用myList.stream().parallel()
,这也适用。
public static double streamedCalculate(double val) {
class MutableDouble {
double currVal;
MutableDouble(double initVal) {
currVal = initVal;
}
}
final MutableDouble accumulator = new MutableDouble(val);
myList.stream().forEachOrdered((x) -> accumulator.currVal += x.apply(accumulator.currVal));
return accumulator.currVal;
}
就我个人而言,我发现你原来的循环比这个更易读,所以在这里使用流并没有任何优势。
根据@Tagir Valeev的评论,未来Java版本中计划添加foldLeft操作。那时候它可能看起来更加优雅。