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


141

我无法完全理解combinerStreams 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的,因为它只是将两个int加在一起。

谁能阐明这一点?



2
啊哈,它是用于并行流...我称之为泄漏抽象!
安迪

Answers:


77

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

两个参数reduce定义为

T reduce(T identity,
         BinaryOperator<T> accumulator)

在您的情况下,T为String,因此BinaryOperator<T>应接受两个String参数并返回一个String。但是您将一个int和一个String传递给它,这会导致您得到-的编译错误argument mismatch; int cannot be converted to java.lang.String。实际上,我认为在此处传递0作为标识值也是错误的,因为期望使用字符串(T)。

另请注意,此版本的reduce处理Ts流并返回T,因此您不能使用它将String流简化为int。

这三个参数reduce定义为

<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),在您的情况下为Integer和String。另外,在您的情况下,标识值U接受整数,因此将其传递为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();

8
作为您上一个代码中的第二个选项,您还可以使用mapToInt(String::length)over mapToInt(s -> s.length()),不确定一个是否比另一个更好,但是我更喜欢前者以提高可读性。
skiwi 2014年

19
许多人会找到这个答案,因为他们不明白为什么combiner需要,为什么没有accumulator足够。在这种情况下:只有并行流才需要组合器,以组合线程的“累加”结果。
ddekany

1
我认为您的答案没有特别的用处-因为您根本不解释合成器应该做什么以及如果没有它我将如何工作!就我而言,我想将类型T简化为U,但是根本不可能并行完成。根本不可能。您如何告诉系统我不需要/不需要并行性,从而省去了组合器?
Zordid

@Zordid Streams API不包含在不通过组合器的情况下将类型T简化为U的选项。
伊兰(Eran)

216

Eran的答案描述了的两个参数和三个参数的区别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个中间结果减少为单个结果。

现在让我们考虑降低一个假设两ARG减少操作Stream<T>U。在其他语言中,这称为“折叠”或“向左折叠”操作,因此我在这里将其称为。请注意,这在Java中不存在。

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

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

的顺序版本foldLeft类似于的顺序版本,reduce不同之处在于中间值的类型为U而不是T。但是在其他方面相同。(假设的foldRight操作将类似,除了操作将从右到左执行,而不是从左到右执行。)

现在考虑的并行版本foldLeft。首先,将流分成多个部分。然后,我们可以让N个线程中的每一个将其段中的T值都减少为U型的N个中间值。现在呢?我们如何从N个类型的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不提供foldLeftfoldRight操作,因为它们暗含了固有顺序的特定操作顺序。这与上述提供相同支持顺序和并行操作的API的设计原则相冲突。


7
那么,如果需要a怎么办,foldLeft因为计算取决于先前的结果并且无法并行化?
变形虫2015年

5
@amoebe您可以使用实现自己的foldLeft forEachOrdered。但是,中间状态必须保留在捕获的变量中。
Stuart Marks 2015年

@StuartMarks谢谢,我最终使用了jOOλ。他们对的执行foldLeft很简洁。
变形虫2015年

1
喜欢这个答案!如果我错了,请纠正我:这解释了为什么OP的运行示例(第二个示例)在运行时永远不会调用合并器,因为它是流顺序的。
路易吉·柯蒂斯

2
它解释了几乎所有内容...除了:为什么要排除基于顺序的归约。在我的情况下,并行执行是不可能的,因为我的简化操作是通过在其前任结果的中间结果上调用每个函数,从而将函数列表简化为U。这根本不能并行完成,也没有办法描述组合器。我可以使用哪种方法来完成此任务?
Zordid

115

由于我喜欢用涂鸦和箭头来阐明概念,所以让我们开始吧!

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

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

您可以使用

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

这可以帮助您可视化正在发生的事情:

在此处输入图片说明

累加器功能逐步将(红色)流中的元素转换为最终的减小的(绿色)值。累加器功能只是将一个String对象转换为另一个对象String

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

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

您需要的是这样的:

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

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

在此处输入图片说明

在这里,累加器功能(a BiFunction)使您可以将String数据转换为int数据。由于流是并行的,所以将其分为两个(红色)部分,每个部分彼此独立设计,并产生同样多的部分(橙色)结果。需要定义组合器以提供将部分int结果合并到最终(绿色)结果中的规则int

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

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


7
谢谢你 我什至不需要阅读。我希望他们会添加一个令人惊讶的折叠功能。
Lodewijk Bogaards

1
@LodewijkBogaards很高兴它有所帮助!JavaDoc的确确实是个神秘人物
路易吉·柯蒂斯

@LuigiCortese在并行流中,是否总是将元素分成几对?
TheLogicGuy

1
感谢您提出的清晰有用的答案。我想重复一下您所说的话:“好吧,无论如何都需要提供组合器,但是永远不会调用它。” 这是Java函数编程的“勇往直前新世界”的一部分,我无数次向我保证,“它使您的代码更简洁,更易于阅读。” 希望这样的示例(手指引号)简洁明了,而且之间相距甚远。
燕窝

最好用8个字符串来说明减少...
Ekaterina Ivanova iceja.net

0

没有reduce的版本没有没有组合器的两种不同类型,因为它不能并行执行(不确定为什么要这样做)。累加器必须具有关联性这一事实使该接口几乎无用,因为:

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

产生与以下结果相同的结果:

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

这种map技巧取决于具体情况accumulatorcombiner可能会使事情变慢。
塔吉尔·瓦列夫2015年

或者,由于您现在可以简化操作,因此可以大大加快速度 accumulator通过删除第一个参数。
quiz123

并行缩减是可能的,这取决于您的计算。在您的情况下,您必须了解组合器的复杂性,而且还必须了解身份和其他实例的累加器。
LoganMzz
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.