我无法完全理解 combiner
在 Streams reduce
方法中所扮演的角色。
例如,以下代码无法编译:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
编译错误说:(参数不匹配;int 无法转换为 java.lang.String)
但这段代码确实编译:
int length = asList("str1", "str2").stream()
.reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(),
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
我知道组合器方法用于并行流 - 所以在我的示例中,它将两个中间累积整数相加。
但是我不明白为什么第一个示例在没有组合器的情况下无法编译,或者组合器如何解决字符串到 int 的转换,因为它只是将两个 int 相加。
任何人都可以阐明这一点吗?
Eran's answer 描述了 reduce
的两个参数和三个参数版本之间的区别,前者将 Stream<T>
简化为 T
,而后者将 Stream<T>
简化为 U
。但是,它实际上并没有解释在将 Stream<T>
减少到 U
时需要额外的组合函数。
Streams API 的设计原则之一是 API 不应在顺序流和并行流之间有所不同,或者换句话说,特定 API 不应阻止流按顺序或并行正确运行。如果您的 lambda 具有正确的属性(关联性、非干扰性等),则顺序或并行运行的流应该会给出相同的结果。
让我们首先考虑减少的两个参数版本:
T reduce(I, (T, T) -> T)
顺序实现很简单。标识值 I
与第零个流元素“累加”以给出结果。该结果与第一个流元素累加以给出另一个结果,该结果又与第二个流元素累加,依此类推。最后一个元素累加后,返回最终结果。
并行实现首先将流拆分为段。每个段都由它自己的线程以我上面描述的顺序方式处理。现在,如果我们有 N 个线程,我们就有 N 个中间结果。这些需要减少到一个结果。由于每个中间结果都是 T 类型,并且我们有多个,因此我们可以使用相同的累加器函数将这 N 个中间结果减少为单个结果。
现在让我们考虑一个假设的双参数归约操作,将 Stream<T>
归约为 U
。在其他语言中,这称为 "fold" 或“向左折叠”操作,所以我将在这里称其为。请注意,这在 Java 中不存在。
U foldLeft(I, (U, T) -> U)
(请注意,标识值 I
的类型为 U。)
foldLeft
的顺序版本与 reduce
的顺序版本一样,只是中间值是 U 类型而不是 T 类型。但在其他方面是相同的。 (假设的 foldRight
操作类似,只是这些操作是从右到左而不是从左到右执行的。)
现在考虑 foldLeft
的并行版本。让我们从将流拆分为段开始。然后,我们可以让 N 个线程中的每一个将其段中的 T 值减少为 N 个 U 类型的中间值。现在怎么办?我们如何从 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 结果。如果我们不切换类型,那么 accumulator 函数与 combiner 函数是一样的。这就是为什么归约到相同类型只有累加器功能,而归约到不同类型需要单独的累加器和组合器功能。
最后,Java 不提供 foldLeft
和 foldRight
操作,因为它们暗示了一种特定的操作顺序,它本质上是顺序的。这与上述提供同样支持顺序和并行操作的 API 的设计原则相冲突。
因为我喜欢用涂鸦和箭头来阐明概念……让我们开始吧!
从字符串到字符串(顺序流)
假设有 4 个字符串:您的目标是将这些字符串连接成一个。您基本上从一个类型开始并以相同的类型结束。
你可以用
String res = Arrays.asList("one", "two","three","four")
.stream()
.reduce("",
(accumulatedStr, str) -> accumulatedStr + str); //accumulator
这可以帮助您想象正在发生的事情:
https://i.stack.imgur.com/JNUWm.png
累加器函数逐步将(红色)流中的元素转换为最终的缩减(绿色)值。累加器函数只是将一个 String
对象转换为另一个 String
。
从 String 到 int(并行流)
假设有相同的 4 个字符串:您的新目标是对它们的长度求和,并且您想要并行化您的流。
你需要的是这样的:
int length = Arrays.asList("one", "two","three","four")
.parallelStream()
.reduce(0,
(accumulatedInt, str) -> accumulatedInt + str.length(), //accumulator
(accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner
这是正在发生的事情的计划
https://i.stack.imgur.com/Sc0lD.png
此处的累加器函数(BiFunction
)允许您将 String
数据转换为 int
数据。由于流是并行的,它被分成两个(红色)部分,每个部分都相互独立地进行详细说明,并产生同样多的部分(橙色)结果。需要定义一个组合器以提供将部分 int
结果合并到最终(绿色)int
结果的规则。
从 String 到 int(顺序流)
如果你不想并行化你的流怎么办?好吧,无论如何都需要提供一个组合器,但它永远不会被调用,因为不会产生部分结果。
您尝试使用的 reduce
的两个和三个参数版本不接受 accumulator
的相同类型。
两个参数 reduce
是 defined as :
T reduce(T identity,
BinaryOperator<T> accumulator)
在您的情况下,T 是字符串,因此 BinaryOperator<T>
应该接受两个字符串参数并返回一个字符串。但是您将一个 int 和一个 String 传递给它,这会导致您得到编译错误 - argument mismatch; int cannot be converted to java.lang.String
。实际上,我认为在这里传递 0 作为标识值也是错误的,因为需要一个字符串 (T)。
另请注意,此版本的reduce 处理Ts 流并返回T,因此您不能使用它将String 流减少为int。
三个参数 reduce
是 defined as :
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
在您的情况下,U 是整数,T 是字符串,因此此方法会将字符串流减少为整数。
对于 BiFunction<U,? super T,U>
累加器,您可以传递两种不同类型的参数(U 和 ? super T),在您的情况下是整数和字符串。此外,身份值 U 在您的情况下接受 Integer ,因此将其传递 0 就可以了。
实现您想要的另一种方法:
int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
.reduce(0, (accumulatedInt, len) -> accumulatedInt + len);
这里流的类型与 reduce
的返回类型匹配,因此您可以使用 reduce
的两个参数版本。
当然,您根本不必使用 reduce
:
int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
.sum();
mapToInt(String::length)
而不是 mapToInt(s -> s.length())
,不确定一个是否会比另一个更好,但我更喜欢前者以提高可读性。
combiner
,为什么没有 accumulator
就足够了。在这种情况下:仅并行流需要组合器,以组合线程的“累积”结果。
没有一个 reduce 版本可以在没有组合器的情况下采用两种不同的类型,因为它不能并行执行(不知道为什么这是一个要求)。累加器必须是关联的这一事实使得这个接口几乎没有用,因为:
list.stream().reduce(identity,
accumulator,
combiner);
产生与以下相同的结果:
list.stream().map(i -> accumulator(identity, i))
.reduce(identity,
combiner);
map
技巧取决于特定的 accumulator
和 combiner
可能会减慢速度。
accumulator
。
不定期副业成功案例分享
foldLeft
,因为计算依赖于先前的结果并且不能并行化,您该怎么办?forEachOrdered
实现自己的 foldLeft。但是,中间状态必须保存在捕获的变量中。foldLeft
。