Eran的回答描述了reduce
的两个参数和三个参数版本之间的差异,前者将Stream<T>
缩减为T
,而后者将Stream<T>
缩减为U
。然而,它并没有解释在将Stream<T>
缩减为U
时需要额外的组合器函数。
流API的设计原则之一是API不应在顺序流和并行流之间有所不同,或者换句话说,特定的API不应妨碍流以顺序或并行方式正确运行。如果您的lambda具有正确的属性(结合性、非干扰性等),则顺序或并行运行流应该给出相同的结果。
让我们首先考虑缩减的两个参数版本:
T reduce(I, (T, T) -> T)
顺序实现很直接。身份值I
与第零个数据流元素“累加”,得到一个结果。将此结果与第一个数据流元素相结合,得到另一个结果,然后这个结果再与第二个数据流元素相结合,以此类推。当最后一个元素被累加时,返回最终结果。
并行实现首先将流分成若干部分。每个部分都按照我上面描述的顺序,由自己的线程进行处理。现在,如果我们有N个线程,就会有N个中间结果。这些需要缩减为一个结果。由于每个中间结果都是类型T,而我们有多个结果,我们可以使用同样的累加器函数将这N个中间结果缩减为一个结果。
现在让我们考虑一个假想的两个参数的缩减操作,将Stream<T>
缩减为U
。在其他语言中,这称为"折叠"或"fold-left"操作,因此我将在此处称其为折叠操作。请注意,Java中不存在这个操作。
U foldLeft(I, (U, T) -> U)
(请注意,标识值I的类型为U。)
foldLeft
的顺序版本与
reduce
的顺序版本非常相似,只是中间值的类型为U而不是T。但它其余部分都是一样的。(假设
foldRight
操作将类似,只是操作将从右到左执行而不是从左到右。)
现在考虑
foldLeft
的并行版本。让我们首先将流分成段。然后,我们可以让每个N个线程将其段中的T值缩减为类型为U的N个中间值。现在怎么办?我们如何从类型为U的N个值转换为类型为U的单个结果?
缺少的是另一个函数,该函数将类型为U的多个中间结果合并为类型为U的单个结果。如果我们有一个将两个U值组合成一个值的函数,那么就足以将任意数量的值缩减为一个值-就像上面的原始缩减一样。因此,产生不同类型结果的缩减操作需要两个函数:
U reduce(I, (U, T) -> U, (U, U) -> U)
或者,使用Java语法:
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
总之,要对不同的结果类型进行并行归约,我们需要两个函数:一个将 T 元素累加到中间 U 值的函数,以及一个将中间 U 值组合成单个 U 结果的函数。如果我们没有切换类型,那么累加器函数就等同于组合函数。这就是为什么同一类型的归约只有累加器函数,而不同类型的归约需要单独的累加器和组合函数。
最后,Java 不提供 foldLeft 和 foldRight 操作,因为它们暗示了一种固有的顺序操作,这与上述提供支持顺序和并行操作的 API 的设计原则相冲突。