ChatGPT解决这个技术问题 Extra ChatGPT

为什么在java 8中转换类型的reduce方法需要一个组合器

我无法完全理解 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 相加。

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

啊哈,它用于并行流......我称之为泄漏抽象!
我遇到了类似的问题。我想做一个map-reduce。我希望 Stream 的“reduce”方法有一个重载版本,允许映射到与输入类型不同的类型,但不强迫我编写组合器。据我所知,Java没有这样的方法。因为有些人,比如我,希望找到它,但它不存在,这会造成混乱。注意:我不想编写组合器,因为输出是一个复杂的对象,组合器对于它来说是不现实的。

N
Naman

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 不提供 foldLeftfoldRight 操作,因为它们暗示了一种特定的操作顺序,它本质上是顺序的。这与上述提供同样支持顺序和并行操作的 API 的设计原则相冲突。


那么,如果您需要一个 foldLeft,因为计算依赖于先前的结果并且不能并行化,您该怎么办?
@amoebe 您可以使用 forEachOrdered 实现自己的 foldLeft。但是,中间状态必须保存在捕获的变量中。
@StuartMarks 谢谢,我最终使用了 jOOλ。他们有一个整洁的 implementation of foldLeft
喜欢这个答案!如果我错了,请纠正我:这解释了为什么 OP 的运行示例(第二个)在运行时永远不会调用组合器,因为它是流顺序的。
它解释了几乎所有东西......除了:为什么这应该排除基于顺序的减少。在我的情况下,并行执行它是不可能的,因为我的缩减通过调用其前身结果的中间结果的每个函数来将函数列表减少到 U 中。这根本无法并行完成,也无法描述组合器。我可以用什么方法来完成这个?
C
Community

因为我喜欢用涂鸦和箭头来阐明概念……让我们开始吧!

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

假设有 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(顺序流)

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


谢谢你。我什至不需要阅读。我真希望他们刚刚添加了一个该死的折叠功能。
@LodewijkBogaards 很高兴它有帮助! JavaDoc 这里确实很神秘
@LuigiCortese 在并行流中它总是将元素分成对吗?
我很欣赏你清晰而有用的回答。我想重复一下您所说的内容:“好吧,无论如何都需要提供组合器,但永远不会调用它。”这是 Java 函数式编程的美丽新世界的一部分,我无数次向我保证,“使您的代码更简洁,更易于阅读”。让我们希望(手指引号)这样简洁明了的例子仍然很少见。
这是最好的答案。把手放下。
E
Eran

您尝试使用的 reduce 的两个和三个参数版本不接受 accumulator 的相同类型。

两个参数 reducedefined 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。

三个参数 reducedefined 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 就足够了。在这种情况下:仅并行流需要组合器,以组合线程的“累积”结果。
我觉得您的回答没有什么特别有用的——因为您根本没有解释组合器应该做什么以及没有它我该如何工作!就我而言,我想将类型 T 减少为 U 但根本不可能并行完成。这根本不可能。你如何告诉系统我不想要/不需要并行性,从而省略了组合器?
@Zordid Streams API 不包含在不传递组合器的情况下将类型 T 减少为 U 的选项。
这个答案根本没有解释组合器,只是为什么 OP 需要非组合器变体。
q
quiz123

没有一个 reduce 版本可以在没有组合器的情况下采用两种不同的类型,因为它不能并行执行(不知道为什么这是一个要求)。累加器必须是关联的这一事实使得这个接口几乎没有用,因为:

list.stream().reduce(identity,
                     accumulator,
                     combiner);

产生与以下相同的结果:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);

这种 map 技巧取决于特定的 accumulatorcombiner 可能会减慢速度。
或者,显着加快速度,因为您现在可以通过删除第一个参数来简化 accumulator
并行减少是可能的,这取决于您的计算。在您的情况下,您必须了解组合器的复杂性,但还要了解身份与其他实例的累加器。