为什么一个组合器需要减少方法转换类型在java 8

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

有人能解释一下吗?

63899 次浏览

你试图使用的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处理T流并返回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接受一个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();

伊兰的回答描述了双参数和三参数版本reduce的区别,前者将Stream<T>简化为T,而后者将Stream<T>简化为U。然而,它实际上并没有解释在将Stream<T>减为U时需要额外的组合器函数。

Streams API的设计原则之一是API不应该区分顺序流和并行流,或者换句话说,特定的API不应该阻止流正确地运行,无论是顺序流还是并行流。如果你的lambdas有正确的属性(关联的,非干扰的,等等),一个流按顺序或并行运行应该会得到相同的结果。

让我们先考虑一下还原的双参数版本:

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

顺序实现很简单。标识值I将与第0个流元素“累积”以给出结果。该结果与第一个流元素一起累积以得到另一个结果,该结果又与第二个流元素一起累积,依此类推。对最后一个元素进行累加后,返回最终结果。

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

现在让我们考虑一个假设的双参数约简操作,将Stream<T>简化为U。在其他语言中,这被称为“折”或“左折叠”操作,所以我在这里这样称呼它。注意这在Java中不存在。

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

(注意,标识值I的类型是u)

foldLeft的顺序版本就像reduce的顺序版本一样,除了中间值是U类型而不是t类型,但在其他方面是相同的。(一个假设的foldRight操作将类似,除了操作将从右到左而不是从左到右执行。)

现在考虑foldLeft的并行版本。让我们从将流分割成段开始。然后我们可以让N个线程中的每个线程将其段中的T值减少为N个类型为u的中间值,现在呢?我们如何从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没有提供foldLeftfoldRight操作,因为它们意味着操作的特定顺序是固有的顺序。这与上面所述的设计原则相冲突,即提供同样支持顺序操作和并行操作的api。

不存在没有合路器的接受两种不同类型的减少版本,因为它不能并行执行(不确定为什么这是一个要求)。蓄电池必须是关联的这一事实使得这个接口几乎毫无用处,因为:

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

产生的结果如下:

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

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

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

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

你可以用

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

这可以帮助你想象正在发生的事情:

enter image description here

累加器函数一步一步地将流(红色)中的元素转换为最终的减少值(绿色)。accumulator函数简单地将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

这就是正在发生的事情

enter image description here

这里的累加函数(BiFunction)允许你将你的String数据转换为int数据。由于流是平行的,它被分为两个部分(红色),每个部分都是相互独立的,并产生同样多的部分(橙色)结果。需要定义一个组合器来提供一个规则,将部分int结果合并到最终的(绿色)int结果中。

从字符串到int(顺序流)

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