为什么在java 8中转换类型的reduce方法需要一个组合器从字符串到字符串(顺序流)从字符串到整型(并行流)从字符串到整型(顺序流)

2022-08-31 06:16:04

我无法完全理解 在 Streams 方法中履行的角色。combinerreduce

例如,以下代码无法编译:

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相加。

任何人都可以阐明这一点吗?


答案 1

Eran的回答描述了双 arg 和三 arg 版本之间的差异,因为前者简化为 ,而后者简化为 。但是,它实际上并没有解释在简化为时需要额外的组合器功能。reduceStream<T>TStream<T>UStream<T>U

Streams API 的设计原则之一是,API 不应该在顺序流和并行流之间有所不同,或者换句话说,特定的 API 不应该阻止流按顺序或并行正确运行。如果您的 lambda 具有正确的属性(关联、非干扰等),则按顺序或并行运行的流应提供相同的结果。

让我们首先考虑一下归约的双参数版本:

T reduce(I, (T, T) -> T)

顺序实现非常简单。标识值与第 0 个流元素“累加”,以提供结果。此结果与第一个流元素一起累积,以提供另一个结果,而该结果又与第二个流元素一起累积,依此类推。累积最后一个元素后,将返回最终结果。I

并行实现首先将流拆分为段。每个段都由其自己的线程以我上面描述的顺序方式处理。现在,如果我们有 N 个线程,我们就有 N 个中间结果。这些需要减少到一个结果。由于每个中间结果都是 T 类型,并且我们有多个,因此我们可以使用相同的累加器函数将这些 N 个中间结果减少到单个结果。

现在,让我们考虑一个假设的双 arg 约简操作,该运算简化为 。在其他语言中,这被称为“折叠”或“向左折叠”操作,这就是我在这里所说的。请注意,这在Java中不存在。Stream<T>U

U foldLeft(I, (U, T) -> U)

(请注意,标识值为 U 类型。I

的顺序版本与 的顺序版本类似,只是中间值的类型为 U 类型而不是类型 T。但除此之外是一样的。(假设的操作是相似的,只是操作将从右到左而不是从左到右执行。foldLeftreducefoldRight

现在考虑 并行版本 。让我们首先将流拆分为多个段。然后,我们可以让每个 N 个线程将其段中的 T 值减小为 U 类型的 N 个中间值。我们如何从 U 型的 N 个值减少到 U 型的单个结果?foldLeft

缺少的另一个函数将 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不提供和操作,因为它们意味着本质上是顺序的操作的特定顺序。这与上面所述的设计原则相冲突,即提供平等支持顺序和并行操作的 API。foldLeftfoldRight


答案 2

由于我喜欢涂鸦和箭头来澄清概念...让我们开始吧!

从字符串到字符串(顺序流)

假设有4个字符串:你的目标是将这些字符串连接成一个。你基本上从一个类型开始,以相同的类型结束。

您可以通过以下方式实现这一目标

String res = Arrays.asList("one", "two","three","four")
        .stream()
        .reduce("",
                (accumulatedStr, str) -> accumulatedStr + str);  //accumulator

这有助于您可视化正在发生的事情:

enter image description here

累加器函数逐步将(红色)流中的元素转换为最终的缩小(绿色)值。累加器函数只是将一个对象转换为另一个对象。StringString

从字符串到整型(并行流)

假设具有相同的 4 个字符串:您的新目标是对它们的长度求和,并且您希望并行化流。

你需要的是这样的东西:

int length = Arrays.asList("one", "two","three","four")
        .parallelStream()
        .reduce(0,
                (accumulatedInt, str) -> accumulatedInt + str.length(),                 //accumulator
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner

这是正在发生的事情的一个计划

enter image description here

这里的累加器函数(a)允许您将数据转换为数据。作为平行的流,它被分成两个(红色)部分,每个部分彼此独立地阐述,并产生同样多的部分(橙色)结果。需要定义一个合并器来提供将部分结果合并到最终(绿色)结果中的规则。BiFunctionStringintintint

从字符串到整型(顺序流)

如果您不想并行化流,该怎么办?好吧,无论如何都需要提供一个组合器,但是它永远不会被调用,因为不会产生部分结果。


推荐