Java 8 函数式编程中“减少”函数的第三个参数的目的

Posted

技术标签:

【中文标题】Java 8 函数式编程中“减少”函数的第三个参数的目的【英文标题】:Purpose of third argument to 'reduce' function in Java 8 functional programming 【发布时间】:2014-05-13 13:28:01 【问题描述】:

在 Java 8 流中调用 'reduce' 的第三个参数在什么情况下?

下面的代码尝试遍历字符串列表并将每个字符串的第一个字符的代码点值相加。最终 lambda 返回的值似乎从未被使用过,如果您插入 println,它似乎永远不会被调用。该文档将其描述为“组合器”,但我找不到更多详细信息...

int result =
  data.stream().reduce(0, (total,s) -> total + s.codePointAt(0), (a,b) -> 1000000); 

【问题讨论】:

【参考方案1】:

你说的是this function吗?

reduce <U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner) 

使用提供的标识、累积和 组合功能。这相当于:

 U result = identity;
 for (T element : this stream)
     result = accumulator.apply(result, element)
 return result;   

但不限于顺序执行。标识值必须是组合器函数的标识。这 意味着对于所有 u,combiner(identity, u) 等于 u。 此外,组合器功能必须与 累加器功能;对于所有 u 和 t,必须满足以下条件:

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

这是一个终端操作。

API 注意:使用这种形式的许多归约可以表示更多 简单地通过 map 和 reduce 操作的显式组合。这 累加器功能充当融合映射器和累加器,它可以 有时比单独的映射和归约更有效,例如 就像知道先前减少的值可以让您避免一些 计算。类型参数: U - 结果的类型参数: identity - 组合函数累加器的标识值 - 一个关联的、非干扰的、无状态的函数,用于合并 结果组合器中的附加元素 - 关联, 用于组合两个值的无干扰、无状态函数,其中 必须与累加器函数兼容返回:结果 另请参阅:reduce(BinaryOperator), reduce(Object, 二元运算符)

我假设它的目的是允许并行计算,所以我的猜测是它仅在并行执行缩减时使用。如果按顺序执行,则无需使用combiner。我不确定这一点——我只是根据文档注释“[...] 不限于按顺序执行”以及 cmets 中许多其他提到的“并行执行”进行猜测。

【讨论】:

功能确实如此。关于并行性的有趣想法 - 将尝试一下...... 看起来就是这样。插入“.parallel()”会导致返回 1000000。 我也不确定为什么您需要在顺序流中使用组合器。 Oracle 应该在他们的 JavaDocs 中明确说明这个主题。 简短的回答是并行案例需要它。 (注意,只有当累加器的输入和输出类型不同时才需要显式组合器;重载 reduce(BinaryOperator) 不需要组合器。)那么,为什么不有两个版本呢?因为我们不区分顺序流和并行流。只有一个流时间,流。最好将顺序执行视为并行执行的退化情况,为此需要组合器。【参考方案2】:

我认为java.util.stream包摘要中的Reduction operations段可以回答这个问题。让我在这里引用最重要的部分:


在更一般的形式中,对 &lt;T&gt; 类型的元素进行归约操作并产生 &lt;U&gt; 类型的结果需要三个参数:

<U> U reduce(U identity,
              BiFunction<U, ? super T, U> accumulator,
              BinaryOperator<U> combiner);

这里,标识元素既是归约的初始种子值,也是没有输入元素时的默认结果。累加器函数获取部分结果和下一个元素,并产生新的部分结果。 combiner 函数组合两个部分结果以产生新的部分结果。 (组合器在并行缩减中是必需的,其中输入被分区,为每个分区计算部分累积,然后将部分结果组合以产生最终结果。) 更正式地说,标识值必须是组合器函数的标识。这意味着对于所有ucombiner.apply(identity, u) 等于u。此外,combiner 函数必须是关联函数,并且必须与 accumulator 函数兼容:对于所有 utcombiner.apply(u, accumulator.apply(identity, t)) 必须是 equals()accumulator.apply(u, t)

三参数形式是二参数形式的推广,在累加步骤中加入了映射步骤。我们可以使用更一般的形式重新构建简单的权重总和示例,如下所示:

 int sumOfWeights = widgets.stream()
                           .reduce(0,
                                   (sum, b) -> sum + b.getWeight())
                                   Integer::sum);

虽然显式 map-reduce 形式更具可读性,因此通常应该是首选。广义形式适用于可以通过将映射和归约组合为单个函数来优化重要工作的情况。


换句话说,据我了解,三参数形式在两种情况下很有用:

    当并行执行很重要时。 当可以通过结合映射和累积步骤来实现显着的性能优化时。否则,可以使用更简单易读的显式 map-reduce 形式。

之前在同一个文档中提到了显式形式:

int sumOfWeights = widgets.parallelStream()
        .filter(b -> b.getColor() == RED)
        .mapToInt(b -> b.getWeight())
        .sum();

【讨论】:

【参考方案3】:

确认combiner使用的简单测试代码:

String[] strArray = "abc", "mno", "xyz";
List<String> strList = Arrays.asList(strArray);

System.out.println("stream test");
int streamResult = strList.stream().reduce(
        0, 
        (total,s) ->  System.out.println("accumulator: total[" + total + "] s[" + s + "] s.codePointAt(0)[" + s.codePointAt(0) + "]"); return total + s.codePointAt(0); , 
        (a,b) ->  System.out.println("combiner: a[" + a + "] b[" + b + "]"); return 1000000;
    );
System.out.println("streamResult: " + streamResult);

System.out.println("parallelStream test");
int parallelStreamResult = strList.parallelStream().reduce(
        0, 
        (total,s) ->  System.out.println("accumulator: total[" + total + "] s[" + s + "] s.codePointAt(0)[" + s.codePointAt(0) + "]"); return total + s.codePointAt(0); , 
        (a,b) ->  System.out.println("combiner: a[" + a + "] b[" + b + "]"); return 1000000;
    );
System.out.println("parallelStreamResult: " + parallelStreamResult);

System.out.println("parallelStream test2");
int parallelStreamResult2 = strList.parallelStream().reduce(
        0, 
        (total,s) ->  System.out.println("accumulator: total[" + total + "] s[" + s + "] s.codePointAt(0)[" + s.codePointAt(0) + "]"); return total + s.codePointAt(0); , 
        (a,b) ->  System.out.println("combiner: a[" + a + "] b[" + b + "] a+b[" + (a+b) + "]"); return a+b;
    );
System.out.println("parallelStreamResult2: " + parallelStreamResult2);

输出:

stream test
accumulator: total[0] s[abc] s.codePointAt(0)[97]
accumulator: total[97] s[mno] s.codePointAt(0)[109]
accumulator: total[206] s[xyz] s.codePointAt(0)[120]
streamResult: 326
parallelStream test
accumulator: total[0] s[mno] s.codePointAt(0)[109]
accumulator: total[0] s[abc] s.codePointAt(0)[97]
accumulator: total[0] s[xyz] s.codePointAt(0)[120]
combiner: a[109] b[120]
combiner: a[97] b[1000000]
parallelStreamResult: 1000000
parallelStream test2
accumulator: total[0] s[mno] s.codePointAt(0)[109]
accumulator: total[0] s[xyz] s.codePointAt(0)[120]
accumulator: total[0] s[abc] s.codePointAt(0)[97]
combiner: a[109] b[120] a+b[229]
combiner: a[97] b[229] a+b[326]
parallelStreamResult2: 326

【讨论】:

以上是关于Java 8 函数式编程中“减少”函数的第三个参数的目的的主要内容,如果未能解决你的问题,请参考以下文章

on java 8 第十三章 函数式编程

java_函数式编程

Java函数式编程原理以及应用

跟上 Java 8 : 函数式编程

Java 8 新语法习惯 (更轻松的函数式编程)

浅谈Java 8的函数式编程