为啥在java 8中转换类型的reduce方法需要一个组合器
Posted
技术标签:
【中文标题】为啥在java 8中转换类型的reduce方法需要一个组合器【英文标题】:Why is a combiner needed for reduce method that converts type in java 8为什么在java 8中转换类型的reduce方法需要一个组合器 【发布时间】:2014-08-10 01:59:12 【问题描述】:我无法完全理解 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 相加。
有人能解释一下吗?
【问题讨论】:
相关问题:***.com/questions/24202473/… 啊哈,它是用于并行流...我称之为泄漏抽象! 我遇到了类似的问题。我想做一个map-reduce。我希望 Stream 的“reduce”方法有一个重载版本,允许映射到与输入类型不同的类型,但不强迫我编写组合器。据我所知,Java 没有这样的方法。因为有些人,比如我,希望找到它,但它不存在,这会造成混乱。注意:我不想编写组合器,因为输出是一个复杂的对象,组合器对于它来说是不现实的。 【参考方案1】:Eran's answer 描述了reduce
的两个参数和三个参数版本之间的区别,前者将Stream<T>
减少为T
,而后者将Stream<T>
减少为U
。但是,它实际上并没有解释在将 Stream<T>
减少到 U
时需要额外的组合器功能。
Streams API 的设计原则之一是 API 不应在顺序流和并行流之间有所不同,或者换句话说,特定 API 不应阻止流按顺序或并行正确运行。如果您的 lambda 具有正确的属性(关联性、非干扰性等),则按顺序或并行运行的流应该会产生相同的结果。
让我们首先考虑两个 arg 版本的归约:
T reduce(I, (T, T) -> T)
顺序实现很简单。标识值I
与第零个流元素“累加”以给出结果。该结果与第一个流元素累加以给出另一个结果,该结果又与第二个流元素累加,依此类推。最后一个元素累加后,返回最终结果。
并行实现首先将流分成多个段。每个段都由它自己的线程以我上面描述的顺序方式处理。现在,如果我们有 N 个线程,我们就有 N 个中间结果。这些需要减少到一个结果。由于每个中间结果都是 T 类型,而且我们有多个,因此我们可以使用相同的累加器函数将这 N 个中间结果减少为单个结果。
现在让我们考虑一个假设的双参数归约操作,将Stream<T>
归约为U
。在其他语言中,这称为"fold" 或“fold-left”操作,所以我在这里将其称为。请注意,这在 Java 中不存在。
U foldLeft(I, (U, T) -> U)
(请注意,标识值I
的类型为U。)
foldLeft
的顺序版本与reduce
的顺序版本一样,只是中间值是 U 类型而不是 T 类型。但在其他方面是相同的。 (假设的foldRight
操作与此类似,只是操作是从右到左而不是从左到右。)
现在考虑foldLeft
的并行版本。让我们从将流拆分为段开始。然后,我们可以让 N 个线程中的每一个将其段中的 T 值减少为 U 类型的 N 个中间值。现在怎么办?我们如何从 N 个 U 类型的值得到一个 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 的设计原则相冲突。
【讨论】:
那么如果你需要一个foldLeft
,因为计算依赖于之前的结果并且不能并行化,你该怎么办?
@amoebe 您可以使用forEachOrdered
实现自己的 foldLeft。不过,中间状态必须保存在捕获的变量中。
@StuartMarks 谢谢,我最终使用了 jOOλ。他们有一个整洁的implementation of foldLeft
。
喜欢这个答案!如果我错了,请纠正我:这解释了为什么 OP 的运行示例(第二个)在运行时永远不会调用组合器,因为它是流顺序的。
它解释了几乎所有事情......除了:为什么这要排除基于顺序的减少。在我的情况下,并行执行它是不可能的,因为我的减少通过调用其前身结果的中间结果的每个函数将函数列表减少到 U 中。这根本无法并行完成,也无法描述组合器。我可以使用什么方法来完成此操作?【参考方案2】:
因为我喜欢用涂鸦和箭头来阐明概念……让我们开始吧!
从字符串到字符串(顺序流)
假设有 4 个字符串:您的目标是将这些字符串连接成一个。你基本上从一个类型开始,然后以相同的类型结束。
你可以通过
String res = Arrays.asList("one", "two","three","four")
.stream()
.reduce("",
(accumulatedStr, str) -> accumulatedStr + str); //accumulator
这可以帮助您想象正在发生的事情:
累加器函数将(红色)流中的元素逐步转换为最终的缩减(绿色)值。累加器函数只是将一个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
这是正在发生的事情的计划
这里的累加器函数(BiFunction
)允许您将String
数据转换为int
数据。作为并行的流,它被分成两个(红色)部分,每个部分都相互独立地详细说明,并产生同样多的部分(橙色)结果。需要定义一个组合器以提供将部分int
结果合并到最终(绿色)int
的规则。
从 String 到 int(顺序流)
如果您不想并行化流怎么办?好吧,无论如何都需要提供一个组合器,但它永远不会被调用,因为不会产生部分结果。
【讨论】:
谢谢。我什至不需要阅读。我真希望他们能添加一个该死的折叠功能。 @LodewijkBogaards 很高兴它有帮助! JavaDoc 这里确实很神秘 @LuigiCortese 在并行流中是否总是将元素分成对? 感谢您清晰而有用的回答。我想重复一下您所说的内容:“好吧,无论如何都需要提供组合器,但永远不会调用它。”这是 Java 函数式编程的美丽新世界的一部分,我无数次向我保证,“使您的代码更简洁、更易于阅读”。让我们希望像这样的(手指引用)简洁明了的例子仍然很少。 这是最好的答案。放下手。【参考方案3】:您尝试使用的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流reduced到一个int。
reduce
的三个参数是defined as:
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BinaryOperator<U> combiner)
在你的情况下,U 是 Integer,T 是 String,所以这个方法会将 String 流减少为 Integer。
对于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 需要非组合器变体。【参考方案4】:
没有 reduce 版本在没有 combiner 的情况下采用两种不同的类型,因为它不能并行执行(不知道为什么这是一个要求)。 accumulator 必须是关联的这一事实使得这个接口几乎毫无用处,因为:
list.stream().reduce(identity,
accumulator,
combiner);
产生与以下相同的结果:
list.stream().map(i -> accumulator(identity, i))
.reduce(identity,
combiner);
【讨论】:
这样的map
技巧取决于特定的accumulator
和combiner
可能会减慢速度。
或者,显着加快速度,因为您现在可以通过删除第一个参数来简化 accumulator
。
并行缩减是可能的,这取决于你的计算。在您的情况下,您必须了解组合器的复杂性,但也要了解身份与其他实例的累加器。以上是关于为啥在java 8中转换类型的reduce方法需要一个组合器的主要内容,如果未能解决你的问题,请参考以下文章
为啥我的 hadoop map reduce 程序中出现类转换异常?
执行 map-reduce 操作的通用方法。 (Java-8)