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

Lou*_*ler 123 java java-8 java-stream

我无法完全理解combinerStreams reduce方法中实现的角色.

例如,以下代码不编译:

int length = asList("str1", "str2").stream()
            .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());
Run Code Online (Sandbox Code Playgroud)

编译错误说:( 参数不匹配; int无法转换为java.lang.String)

但是这段代码确实编译:

int length = asList("str1", "str2").stream()  
    .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), 
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);
Run Code Online (Sandbox Code Playgroud)

我知道组合器方法用于并行流 - 所以在我的例子中它将两个中间累积的int加在一起.

但是我不明白为什么第一个例子在没有组合器的情况下编译或者组合器如何解决字符串到int的转换,因为它只是将两个整数加在一起.

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

Stu*_*rks 193

Eran的答案描述了两个arg和三个arg版本之间的差异reduce,前者减少Stream<T>T后者,而后者减少Stream<T>U.但是,它实际上并没有解释的附加功能组合的需求减少时Stream<T>U.

Streams API的设计原则之一是API在顺序流和并行流之间不应该有所区别,换句话说,特定的API不应该阻止流顺序或并行地正确运行.如果你的lambdas具有正确的属性(关联,非干扰等),顺序或并行运行的流应该给出相同的结果.

让我们首先考虑减少的两个arg版本:

T reduce(I, (T, T) -> T)
Run Code Online (Sandbox Code Playgroud)

顺序实现很简单.I使用第0个流元素"累积" 标识值以给出结果.该结果与第一流元素一起累积以给出另一结果,该结果又与第二流元素一起累积,等等.累积最后一个元素后,返回最终结果.

并行实现通过将流拆分为段来开始.每个段由其自己的线程以上述顺序方式处理.现在,如果我们有N个线程,我们有N个中间结果.这些需要减少到一个结果.由于每个中间结果都是T类型,并且我们有几个,我们可以使用相同的累加器函数将这些N个中间结果减少到单个结果.

现在让我们考虑一个假设的两个arg减少操作,减少Stream<T>U.在其他语言中,这称为"折叠"或"向左折叠"操作,因此我将在此处称之为"折叠"或"向左折叠"操作.请注意,这在Java中不存在.

U foldLeft(I, (U, T) -> U)
Run Code Online (Sandbox Code Playgroud)

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

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

现在考虑并行版本foldLeft.让我们从将流分割成段开始.然后我们可以让N个线程中的每个线程将其段中的T值减少为U类型的N个中间值.现在是什么?我们如何从U类型的N值到U类型的单个结果?

缺少的是 U类型的多个中间结果组合成U类型的单个结果的另一个函数.如果我们有一个将两个U值组合成一个的函数,那就足以将任意数量的值减少到一个 - 就像原来的减少在上面.因此,给出不同类型结果的缩减操作需要两个函数:

U reduce(I, (U, T) -> U, (U, U) -> U)
Run Code Online (Sandbox Code Playgroud)

或者,使用Java语法:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
Run Code Online (Sandbox Code Playgroud)

总之,要对不同的结果类型进行并行缩减,我们需要两个函数:一个将T元素累积到中间U值,另一个中间U值组合成单个U结果.如果我们不切换类型,则结果是累加器函数与组合器函数相同.这就是为什么减少到相同类型只有累加器功能和减少到不同类型需要单独的累加器和组合器功能.

最后,Java不提供foldLeftfoldRight操作,因为它们暗示了本质上顺序的特定操作顺序.这与上述设计原则相冲突,即提供支持顺序和并行操作的API.

  • 那么如果你需要一个`foldLeft`你会怎么做,因为计算取决于之前的结果而无法并行化? (7认同)
  • @amoebe您可以使用`forEachOrdered`实现自己的foldLeft.但是,中间状态必须保存在捕获的变量中. (4认同)
  • 它解释了几乎所有内容...除了:为什么要排除基于顺序的归约。在我的情况下,并行执行是不可能的,因为我的简化操作是通过在其前任结果的中间结果上调用每个函数,从而将函数列表简化为U。这根本不能并行完成,也没有办法描述组合器。我可以使用哪种方法来完成此任务? (2认同)

Lui*_*ese 97

因为我喜欢涂鸦和箭头来澄清概念......让我们开始吧!

从String到String(顺序流)

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

你可以实现这一目标

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

这有助于您可视化正在发生的事情:

在此输入图像描述

累加器功能将您(红色)流中的元素逐步转换为最终的缩减(绿色)值.累加器函数只是将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
Run Code Online (Sandbox Code Playgroud)

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

在此输入图像描述

累加器函数(a BiFunction)允许您将String数据转换为int数据.作为流并行,它被分成两个(红色)部分,每个部分彼此独立地精心制作并产生尽可能多的部分(橙色)结果.需要定义组合器以提供将部分int结果合并到最终(绿色)结果的规则int.

从String到int(顺序流)

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

  • 谢谢你.我甚至不需要阅读.我希望他们会添加一个怪异的折叠功能. (6认同)
  • 这是最好的答案。把手放下。 (5认同)
  • 我感谢您的明确而有用的回答。我想重复一下你说的:“好吧,无论如何都需要提供一个组合器,但它永远不会被调用。” 这是 Java 函数式编程的美丽新世界的一部分,我已经无数次保证,“使您的代码更简洁,更易于阅读”。让我们希望像这样(用手指引用)简洁明了的例子仍然很少。 (2认同)

Era*_*ran 65

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

两个参数reduce定义为:

T reduce(T identity,
         BinaryOperator<T> accumulator)
Run Code Online (Sandbox Code Playgroud)

在你的情况下,T是String,所以BinaryOperator<T>应该接受两个String参数并返回一个String.但是你传递给它一个int和一个String,这会导致你得到的编译错误 - argument mismatch; int cannot be converted to java.lang.String.实际上,我认为传递0作为身份值在这里也是错误的,因为期望String(T).

另请注意,此版本的reduce处理Ts流并返回T,因此您无法使用它将String流减少为int.

这三个参数reduce定义为:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)
Run Code Online (Sandbox Code Playgroud)

在你的情况下,U是Integer,T是String,所以这个方法会将String流减少为Integer.

对于BiFunction<U,? super T,U>累加器,您可以传递两种不同类型的参数(U和?super T),在您的情况下是Integer和String.另外,标识值U在你的情况下接受一个I​​nteger,所以传递0就可以了.

实现您想要的另一种方式:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);
Run Code Online (Sandbox Code Playgroud)

这里流的类型与返回类型匹配reduce,因此您可以使用两个参数版本reduce.

当然你根本不需要使用reduce:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();
Run Code Online (Sandbox Code Playgroud)

  • 许多人会找到这个答案,因为他们不知道为什么需要`combiner`,为什么不用'accumulator`就足够了.在这种情况下:组合器仅用于并行流,以组合线程的"累积"结果. (13认同)
  • 我认为您的答案没有特别有用 - 因为您根本没有解释组合器应该做什么以及没有它我如何工作!在我的情况下,我想将类型 T 减少到 U,但根本无法并行完成。这是不可能的。您如何告诉系统我不需要/不需要并行性,从而忽略了组合器? (9认同)
  • 作为你上一个代码中的第二个选项,你也可以使用`mapToInt(String :: length)`而不是`mapToInt(s - > s.length())`,不确定一个会比另一个更好,但是我更喜欢前者的可读性. (7认同)
  • 这个答案根本没有解释组合器,只是解释为什么 OP 需要非组合器变体。 (2认同)