reduce() 方法在 Java 8 中是如何工作的?

Posted

技术标签:

【中文标题】reduce() 方法在 Java 8 中是如何工作的?【英文标题】:How does the reduce() method work in Java 8? 【发布时间】:2019-09-22 00:29:28 【问题描述】:

我试图了解reduce() 方法在java-8 中是如何工作的。

例如我有这个代码:

public class App 

    public static void main(String[] args) 
        String[] arr = "lorem", "ipsum", "sit", "amet";
        List<String> strs = Arrays.asList(arr);

        int ijk = strs.stream().reduce(0, 
            (a, b) ->  
                System.out.println("Accumulator, a = " + a + ", b = " + b);
                return a + b.length();
            ,
            (a, b) -> 
                System.out.println("Combiner");
                return a * b;
            );
        System.out.println(ijk); 
    

输出是这样的:

Accumulator, a = 0, b = lorem
Accumulator, a = 5, b = ipsum
Accumulator, a = 10, b = sit
Accumulator, a = 13, b = amet
17

它是这些字符串长度的总和。而且我看到没有访问组合器,所以它不会乘以数字,它只会添加数字。

但如果我将stream 替换为parallelStream

int ijk = strs.parallelStream().reduce(0, 
    (a, b) ->  
        System.out.println("Accumulator, a = " + a + ", b = " + b);
        return a + b.length();
    ,
    (a, b) -> 
        System.out.println("Combiner");
        return a * b;
    );

System.out.println(ijk); 

这是输出:

Accumulator, a = 0, b = ipsum
Accumulator, a = 0, b = lorem
Accumulator, a = 0, b = sit
Combiner
Accumulator, a = 0, b = amet
Combiner
Combiner
300

我看到累加器和组合器都被访问了,但只有乘法是返回的。那么总和会发生什么?

【问题讨论】:

combiner 仅用于并行流。还请仔细阅读reduce的文档,您不能在accumulator 中进行求和,在combiner 中进行乘法运算并期望得到有意义的结果 【参考方案1】:

您应该阅读reduce 的文档:

另外,combiner 函数必须与 accumulator 函数兼容;对于所有 u 和 t,必须满足以下条件:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

在您的情况下,您违反了该法律(在 accumulator 中进行 sum 并在 combiner 中进行 乘法),因此您看到的结果是一个操作实际上是未定义的,取决于底层源的 Spliterator 是如何实现的(不要那样做!)。

此外,combiner为并行流调用。

当然,您的整个方法可以简化为:

Arrays.asList("lorem", "ipsum", "sit", "amet")
      .stream()
      .mapToInt(String::length)
      .sum();

如果您这样做只是为了学习,正确的reduce 将是(获取sum):

strs.parallelStream()
    .reduce(0,
            (a, b) -> 
                  System.out.println("Accumulator, a = " + a + ", b = " + b);
                  return a + b.length();
            ,
            (a, b) -> 
                  System.out.println("Combiner");
                  return a + b;
            );

【讨论】:

【参考方案2】:

关键概念:标识、累加器和组合器

Stream.reduce() 操作:让我们将操作的参与者元素分解为单独的块。这样,我们将更容易理解每​​个人所扮演的角色

Identity – 一个元素,它是归约操作的初始值,如果流为空,则为默认结果 itemAccumulator – 一个接受两个参数的函数:归约操作的部分结果和流的下一个元素 Combiner – 一个接受两个参数的函数:归约操作的部分结果和流的下一个元素 组合器 - 当归约并行化或累加器参数的类型与累加器实现的类型不匹配时,用于组合归约操作的部分结果的函数

当流并行执行时,Java 运行时会将流拆分为多个子流。在这种情况下,我们需要使用一个函数将子流的结果合并为一个。这就是combiner的作用

案例 1:Combiner 与 parallelStream 一起使用,如您的示例所示

案例 2:具有不同类型参数的累加器示例

在这种情况下,我们有一个用户对象流,累加器参数的类型是整数和用户。但是,累加器的实现是整数的总和,所以编译器无法推断用户参数的类型。

List<User> users = Arrays.asList(new User("John", 30), new User("Julie", 35));
int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge());

编译错误

The method reduce(User, BinaryOperator<User>) in the type Stream<User> is not applicable for the arguments (int, (<no type> partialAgeResult, <no type> user) -> )

我们可以通过使用组合器来解决这个问题:这是方法引用Integer::sum 或使用 lambda 表达式(a,b)-&gt;a+b

int computedAges = users.stream().reduce(0, (partialAgeResult, user) -> partialAgeResult + user.getAge(),Integer::sum);

简单地说,如果我们使用顺序流并且累加器参数的类型和它的实现类型匹配,我们就不需要使用组合器。

【讨论】:

【参考方案3】:

使用java-stream 有 3 种减少方法。简而言之,Stream::reduce 从两个后续项(或一个标识值与第一个项)开始,并与它们执行操作以产生新的减少值。对于下一个项目,同样的情况会发生,并使用减少的值执行操作。

假设您有'a''b''c''d' 的流。归约执行以下操作序列:

    result = operationOn('a', 'b') - operationOn 可能是任何东西(输入长度​​的总和......) result = operationOn(result, 'c') result = operationOn(result, 'd') result is returned

方法有:

Optional&lt;T&gt; reduce(BinaryOperator&lt;T&gt; accumulator) 对元素进行缩减。从产生减少值的前两个项目开始,然后每个项目产生减少值。返回Optional&lt;T&gt;,因为不能保证输入流不为空。

T reduce(T identity, BinaryOperator&lt;T&gt; accumulator) 与上面的方法相同,除了标识值作为第一项给出。 T 被返回,因为由于T identity,始终保证至少有一项。

U reduce(U identity, BiFunction&lt;U,? super T, U&gt; accumulator, BinaryOperator&lt;U&gt; combiner) 和上面的方法一样,只是功能是合并的。 U 被返回,因为由于U identity,总是至少有一项保证。

【讨论】:

身份值称为“默认值”是一种误导,如果不是更糟的话。如果您需要默认值,请使用reduce(function).orElse(default)。此外,对第三次重载的解释,“加上功能被组合”并没有真正的帮助。第三个版本的要点是,它允许归约到与 Stream 的元素类型不同的结果类型。与直接替代方案stream.map(…).reduce(id, function) 的区别在于,您可以使用优化函数将源元素 (T) 与结果元素 (U) 组合在一起(如果有的话)。 “initial”可能比“default”更好,对吧?我仍然提高我的英语。感谢您的留言:) 嗯,“身份价值”这个词本身已经是一个词,没有更好的替代词。 “初始”和“默认”都不够。身份值的契约在文档中进行了解释,但是,正如 this answer 中所解释的,它是一个术语,而不仅仅是为归约操作而发明的。【参考方案4】:

我假设您选择做加法和乘法只是为了看看究竟发生了什么。

正如您已经注意到的那样,正如已经提到的,组合器仅在并行流上调用。

简而言之,在并行流上,流的一部分(分别是底层的 Spliterator)被截断并由不同的线程处理。在处理了几个部分后,它们的结果会被组合器组合起来。

在您的情况下,这四个元素都由不同的线程处理,然后按元素进行组合。这就是为什么你看不到任何加法(除了0 +),而只有乘法。

但是,为了获得有意义的结果,您应该从* 切换到+,而是进行更有意义的输出。

【讨论】:

以上是关于reduce() 方法在 Java 8 中是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

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

reduce_sum() 在张量流中是如何工作的?

如何在 Java 8 中引用 reduce() 操作的结果?

Kotlin reduce 有限制吗?

选项卡在 Java 中是如何工作的?

java 使用reduce和Integer.max查找最大值java 8