我有困难充分理解的角色,合成器在流减少方法中实现。
例如,下面的代码不能编译:
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相加。
有人能解释一下吗?
您尝试使用的两个和三个参数版本的reduce不接受累加器的相同类型。
两个参数reduce被定义为:
T reduce(T identity,
BinaryOperator<T> accumulator)
在你的例子中,T是String,所以BinaryOperator<T>应该接受两个String参数并返回一个String。但是你传递给它一个int和一个String,这将导致你得到的编译错误——参数不匹配;int不能转换为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。
对于biffunction <U,?超级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();
Eran的回答描述了2参数和3参数版本reduce的区别,前者将Stream<T>还原为T,而后者将Stream<T>还原为U。然而,它实际上并没有解释在将Stream<T>还原为U时需要额外的组合器函数。
Streams API的设计原则之一是API不应该区分顺序流和并行流,或者换句话说,特定的API不应该阻止流正确地运行,无论是顺序流还是并行流。如果你的lambdas有正确的属性(关联的,非干扰的,等等),一个流按顺序或并行运行应该会得到相同的结果。
让我们先考虑一下还原的双参数版本:
T reduce(I, (T, T) -> T)
顺序实现很简单。单位值I与第零个流元素“累积”得到一个结果。该结果与第一个流元素一起累积以得到另一个结果,该结果又与第二个流元素一起累积,依此类推。对最后一个元素进行累加后,返回最终结果。
并行实现从将流分割为段开始。每个段由它自己的线程以我上面描述的顺序方式处理。现在,如果我们有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没有提供foldLeft和foldRight操作,因为它们意味着操作的特定顺序是固有的顺序。这与上面所述的设计原则相冲突,即提供同样支持顺序操作和并行操作的api。
因为我喜欢用涂鸦和箭头来阐明概念……让我们开始!
从字符串到字符串(顺序流)
假设有4个字符串:您的目标是将这些字符串连接成一个字符串。基本上从一个类型开始,以相同的类型结束。
你可以用
String res = Arrays.asList("one", "two","three","four")
.stream()
.reduce("",
(accumulatedStr, str) -> accumulatedStr + str); //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
这就是正在发生的事情
这里的累加函数(bifuncfunction)允许您将String数据转换为int数据。由于流是平行的,它被分为两个部分(红色),每个部分都是相互独立的,并产生同样多的部分(橙色)结果。需要定义一个组合器来提供将部分整型结果合并到最终(绿色)整型结果的规则。
从字符串到int(顺序流)
如果你不想并行化你的流怎么办?好吧,无论如何都需要提供一个组合器,但它永远不会被调用,因为不会产生部分结果。