Java 8中的reduce累加器允许修改其参数吗?

17
在Java 8中,Stream有一个reduce方法:
T reduce(T identity, BinaryOperator<T> accumulator);

累加器运算符是否允许修改其任一参数?我认为不允许,因为JavaDoc称累加器应该是非干扰性的,尽管所有示例都在谈论修改集合而不是修改集合中的元素。

因此,以一个具体的例子来说,如果我们有:

 integers.reduce(0, Integer::sum);

假设一下,如果 Integer 是可变的,那么 sum 能否通过将第二个参数的值加到第一个参数上(原地修改)来修改它的第一个参数呢?
我认为不行,但我也想要一个例子来说明这种干扰会引起什么问题。
2个回答

13
不。累加器不应修改其参数;它需要两个值并生成一个新值。如果您想在积累过程中使用变异(例如将字符串累积到StringBuffer中而不是连接),请使用为此设计的Stream.collect()
以下是一个代码示例,如果您尝试这样做,它将产生错误的答案。假设您想使用一个假设的MutableInteger类进行加法:
// Don't do this
MutableInteger result = stream.reduce(new MutableInteger(0), (a,b) -> a.add(b.get()));

这个做法得到错误结果的原因之一是,如果我们并行地将计算拆分,那么现在两个计算会共享同一个可变的起始值。请注意:

a + b + c + d
= 0 + a + b + 0 + c + d  // 0 denotes identity
= (0 + a + b) + (0 + c + d) // associativity

因此,我们可以自由地拆分流,计算部分和 0 + a + b0 + c + d,然后将结果相加。但是,如果它们共享同一个标识值,并且该值因一个计算的结果而被改变,那么另一个计算可能会从错误的值开始。

(进一步注意,即使对于顺序计算,如果实现认为这样做有价值,它也可以这样做。)


只要reduce方法没有提供identity,似乎一个可变的累加器就可以产生良好的结果,即使在并行运行时也是如此。我知道这不是一个好的实践,但它似乎“有效”。 - assylias
1
@assylias 即使“似乎工作”实际上意味着“工作”,你为什么要这样做?已经有一个专门为此目的设计的替代方案。鉴于此,你为什么还要考虑使用不为此设计的工具? - Brian Goetz
“collect” 对我来说似乎更加复杂,需要提供者、消费者和组合器。这就是使用“reduce”的原因之一。此外,“collect” 似乎还需要一个非干扰的累加器。这个非干扰是否只指源集合呢? - Graeme Moss
2
然而,这并不是使用错误的东西的借口,仅仅因为正确的东西更加复杂...不干扰意味着:它不能干扰同一流水线中的任何其他计算。这包括源,还包括愚蠢的事情,比如一个lambda修改了某些状态,而同一流水线中的另一个状态依赖于该状态来获取其答案。这并不意味着您无法更新结果容器。 - Brian Goetz
2
也许 collect 并不是那么复杂。我猜你可以像这样做:stream.collect(MutableInteger::new, MutableInteger::add, MutableInteger::add)。 - Graeme Moss
collectеҝ…йЎ»и°ғз”ЁдёҖдёӘе…·жңүдёҖдёӘе…ғзҙ зҡ„Streamзҡ„дҫӣеә”е•ҶгҖӮеӣ жӯӨпјҢеҰӮжһңе…Ғи®ёеҸҳејӮпјҢжҲ‘并дёҚи®Өдёәе®ғеҸҜд»Ҙжӣҝд»ЈдҪҝз”ЁеҚ•еҸӮж•°reduceж–№жі•жүҖиғҪеҒҡзҡ„дәӢжғ…гҖӮ - Ed Thomas

0

从语法上讲,这是允许的,但我认为它违反了设计模式并且是一个不好的想法。

  static void accumulatorTest() {
     ArrayList<Point> points = new ArrayList<>();
     points.add(new Point(5, 6));
     points.add(new Point(0, 6));
     points.add(new Point(1, 9));
     points.add(new Point(4, 16));
     BinaryOperator<Point> sumPoints = new BinaryOperator<Point>() {
        public Point apply(Point p1, Point p2) {
           p2.x += p1.x;
           p2.y += p1.y;
           return new Point(p2); //return p2 and the list is transformed into running total
        }
     };
     Point sum = points.stream().reduce(new Point(0, 0), sumPoints); 
     System.out.println(sum);
     System.out.println(points);
  }

答案是正确的;我们得到了所有x和y坐标的总和。原始列表已被修改,输出结果证实:

java.awt.Point[x=10,y=37] [java.awt.Point[x=5,y=6], java.awt.Point[x=5,y=12], java.awt.Point[x=6,y=21], java.awt.Point[x=10,y=37]]


我尝试了这个方法,它运行得很好。通过将列表大小增加到10,000个点(在具有2个内核的Intel i5上),应该会导致JVM实际并行运行命令。在修改列表时仍然返回正确的最终答案。 - Thorn
1
这个例子非常狭窄地被“偶然”制作出来。例如,如果您修改了列表的结构(添加或插入),或者您从中间操作而不是最后一个终端操作修改了状态,或者您修改了任何在计算期间将再次读取的状态,您会看到异常情况。因此,我会非常小心地得出“我尝试过了,我没有看到任何区别”的结论。 - Brian Goetz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接