Java8流顺序和并行执行产生不同的结果?

Udo*_*Udo 48 java lambda java-8 java-stream

在Java8中运行以下流示例:

    System.out.println(Stream
        .of("a", "b", "c", "d", "e", "f")
        .reduce("", (s1, s2) -> s1 + "/" + s2)
    );
Run Code Online (Sandbox Code Playgroud)

收益率:

/a/b/c/d/e/f
Run Code Online (Sandbox Code Playgroud)

这是 - 当然 - 毫不奇怪.由于http://docs.oracle.com/javase/8/docs/api/index.html?overview-summary.html,流顺序执行还是并行执行无关紧要:

除了标识为显式非确定性的操作(例如findAny())之外,流是顺序执行还是并行执行不应更改计算结果.

AFAIK reduce()是确定性的并且(s1, s2) -> s1 + "/" + s2是关联的,因此添加parallel()应该产生相同的结果:

    System.out.println(Stream
            .of("a", "b", "c", "d", "e", "f")
            .parallel()
            .reduce("", (s1, s2) -> s1 + "/" + s2)
    );
Run Code Online (Sandbox Code Playgroud)

但是我机器上的结果是:

/a//b//c//d//e//f
Run Code Online (Sandbox Code Playgroud)

这有什么不对?

BTW:使用(首选).collect(Collectors.joining("/"))而不是为顺序和并行执行reduce(...)产生相同的结果a/b/c/d/e/f.

JVM详细信息:

java.specification.version: 1.8
java.version: 1.8.0_31
java.vm.version: 25.31-b07
java.runtime.version: 1.8.0_31-b13
Run Code Online (Sandbox Code Playgroud)

Jar*_*lak 63

来自reduce的文档:

标识值必须是累加器函数的标识.这意味着对于所有t,accumulator.apply(identity,t)等于t.

在你的情况下不是这样 - ""和"a"创建"/ a".

我已经提取了累加器函数并添加了一个打印输出来显示发生了什么:

BinaryOperator<String> accumulator = (s1, s2) -> {
    System.out.println("joining \"" + s1 + "\" and \"" + s2 + "\"");
    return s1 + "/" + s2;
};
System.out.println(Stream
                .of("a", "b", "c", "d", "e", "f")
                .parallel()
                .reduce("", accumulator)
);
Run Code Online (Sandbox Code Playgroud)

这是示例输出(运行之间不同):

joining "" and "d"
joining "" and "f"
joining "" and "b"
joining "" and "a"
joining "" and "c"
joining "" and "e"
joining "/b" and "/c"
joining "/e" and "/f"
joining "/a" and "/b//c"
joining "/d" and "/e//f"
joining "/a//b//c" and "/d//e//f"
/a//b//c//d//e//f
Run Code Online (Sandbox Code Playgroud)

您可以向函数添加if语句以分别处理空字符串:

System.out.println(Stream
        .of("a", "b", "c", "d", "e", "f")
        .parallel()
        .reduce((s1, s2) -> s1.isEmpty()? s2 : s1 + "/" + s2)
);
Run Code Online (Sandbox Code Playgroud)

正如Marko Topolnik所注意到的那样,s2不需要检查,因为累加器不必是交换功能.

  • 或者他可以使用`stream.collect(Collectors.joining("/","/",""))` (4认同)
  • 此外,由于累加器函数不需要是可交换的,因此您不需要检查`s2`是否为空.那么,`(s1,s2) - > s1.isEmpty()?s2:s1 +"/"+ s2`就足够了. (3认同)
  • 首选条件表达式,恕我直言:`(s1,s2) - > s1.isEmpty()?s2:s2.isEmpty()?s1:s1 +"/"+ s2` (2认同)
  • 所以最大的问题是,为什么实现不会将`reduce(identity,accumulator)`委托给`reduce(accumulator).orElse(identity)`作为"accumulator.apply(identity,t)"的要求.等于`t`"意味着在每个评估线程中执行`whatever = accumulator.apply(identity,whatever)`都是无意义的. (2认同)

Jea*_*ard 6

要添加到其他答案,

您可能希望使用Mutable reduction,doc指定执行类似的操作

String concatenated = strings.reduce("", String::concat)
Run Code Online (Sandbox Code Playgroud)

会给出糟糕的表现结果.

我们会得到理想的结果,甚至可以并行工作.但是,我们可能对性能不满意!这样的实现将进行大量的字符串复制,并且运行时间将是字符数的O(n ^ 2).更高效的方法是将结果累积到StringBuilder中,StringBuilder是用于累积字符串的可变容器.我们可以使用相同的技术来并行化可变缩减,就像我们使用普通缩减一样.

所以你应该使用StringBuilder.