在流约化方法中,对于总和,恒等式是否必须始终为 0,对于乘法,必须始终为 1?

2022-09-02 02:09:56

我继续学习java 8。

我发现了一个有趣的行为:

让我们看看代码示例:

// identity value and accumulator and combiner
Integer summaryAge = Person.getPersons().stream()
        //.parallel()  //will return surprising result
        .reduce(1,
                (intermediateResult, p) -> intermediateResult + p.age,
                (ir1, ir2) -> ir1 + ir2);
System.out.println(summaryAge);

和模型类:

public class Person {

    String name;

    Integer age;
    ///...

    public static Collection<Person> getPersons() {
        List<Person> persons = new ArrayList<>();
        persons.add(new Person("Vasya", 12));
        persons.add(new Person("Petya", 32));
        persons.add(new Person("Serj", 10));
        persons.add(new Person("Onotole", 18));
        return persons;
   }
}

12+32+10+18 = 72.对于顺序流,此代码始终返回,但对于并行,它始终返回哪个是(4 等于流元素计数)。7372 + 17672 + 4*1

当我看到这个结果时,我认为并行流和顺序流返回不同的结果很奇怪。

我是否在某个地方违反了合同?

附言

对我来说,73是预期结果,但76不是。


答案 1

标识值是一个值,使得 .这个概念并不是Java独有的,例如在维基百科上x op identity = xStream

它列出了一些标识元素的示例,其中一些可以直接用Java代码表示,例如

  • reduce("", String::concat)
  • reduce(true, (a,b) -> a&&b)
  • reduce(false, (a,b) -> a||b)
  • reduce(Collections.emptySet(), (a,b)->{ Set<X> s=new HashSet<>(a); s.addAll(b); return s; })
  • reduce(Double.POSITIVE_INFINITY, Math::min)
  • reduce(Double.NEGATIVE_INFINITY, Math::max)

应该清楚的是,任意的表达式只能在 时满足,因此是加法的单位元。同样,是乘法的标识元素。x + y == xxy==001

更复杂的示例是

  • 减少谓词流

    reduce(x->true, Predicate::and)
    reduce(x->false, Predicate::or)
    
  • 减少函数流

    reduce(Function.identity(), Function::andThen)
    

答案 2

@holger答案极大地解释了不同函数的恒等式,但没有解释为什么我们需要恒等式,以及为什么在并行流和顺序流之间有不同的结果。

您的问题可以简化为对知道如何求和2个元素的元素列表的总和

因此,让我们采用一个列表和一个求和函数L = {12,32,10,18}(a,b) -> a + b

就像你在学校学习的那样,你会做:

(12,32) -> 12 + 32 -> 44
(44,10) -> 44 + 10 -> 54
(54,18) -> 54 + 18 -> 72

现在想象一下我们的列表成为,如何总结这个列表?身份()来了。L = {12}x op identity = x

(0,12) -> 12

所以现在你可以理解为什么你得到你的总和,如果你放而不是,那是因为你用一个错误的值初始化。+110

(1,12) -> 1 + 12 -> 13
(13,32) -> 13 + 32 -> 45
(45,10) -> 45 + 10 -> 55
(55,18) -> 55 + 18 -> 73

那么现在,我们怎样才能提高速度呢?并行化事物

如果我们可以拆分列表并将拆分的列表提供给4个不同的线程(假设是4核cpu),然后将其合并,该怎么办?这将给我们 , , ,L1 = {12}L2 = {32}L3 = {10}L4 = {18}

所以同一性 = 1

  • 线程 1:(1,12) -> 1+12 -> 13
  • 线程 2:(1,32) -> 1+32 -> 33
  • 线程 3:(1,10) -> 1+10 -> 11
  • 线程 4:(1,18) -> 1+18 -> 19

然后组合,等于,这就解释了为什么误差传播了4次。13 + 33 + 11 +1976

在这种情况下,并行的效率可能较低。

但此结果取决于您的计算机和输入列表。Java 不会为 1000 个元素创建 1000 个线程,并且随着输入的增长,错误将传播得更慢。

尝试运行此代码求和一千s,结果非常接近10001

public class StreamReduce {

public static void main(String[] args) {
        int sum = IntStream.range(0, 1000).map(i -> 1).parallel().reduce(1, (r, e) -> r + e);
        System.out.println("sum: " + sum);
    }
}

现在,您应该了解为什么如果违反标识协定,并行和顺序之间会产生不同的结果。

请参阅 Oracle 文档,了解编写总和的正确方法


问题的身份是什么?