O Java8 transmite execução sequencial e paralela produzindo resultados diferentes?

Executando o seguinte exemplo de stream no Java8:

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

rendimentos:

 /a/b/c/d/e/f 

Qual é – naturalmente – nenhuma surpresa. Devido a http://docs.oracle.com/javase/8/docs/api/index.html?overview-summary.html , não importa se o stream é executado sequencialmente ou paralelamente:

Exceto para operações identificadas como explicitamente não determinísticas, como findAny (), se um stream executado sequencialmente ou em paralelo não deve alterar o resultado do cálculo.

AFAIK reduce() é determinístico e (s1, s2) -> s1 + "/" + s2 é associativo, de modo que adicionar parallel() deve produzir o mesmo resultado:

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

No entanto, o resultado na minha máquina é:

 /a//b//c//d//e//f 

O que há de errado aqui?

BTW: usando (o preferido) .collect(Collectors.joining("/")) vez de reduce(...) produz o mesmo resultado a/b/c/d/e/f para execução sequencial e paralela.

Detalhes da 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 

Da documentação da redução:

O valor de identidade deve ser uma identidade para a function de acumulador. Isso significa que para todo t, accumulator.apply (identity, t) é igual a t.

O que não é verdade no seu caso – “” e “a” cria “/ a”.

Eu extraí a function de acumulador e adicionei uma impressão para mostrar o que acontece:

 BinaryOperator 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) ); 

Este é o exemplo de saída (difere entre as execuções):

 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 

Você pode adicionar uma declaração if à sua function para manipular uma string vazia separadamente:

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

Como notou Marko Topolnik, a verificação de s2 não é necessária, pois o acumulador não precisa ser comutativo.

Para adicionar a outra resposta,

Você pode querer usar a redução mutável , o doc especificar que fazer algo como

 String concatenated = strings.reduce("", String::concat) 

Vai dar resultado ruim.

Nós obteríamos o resultado desejado e ele funcionaria em paralelo. No entanto, podemos não estar felizes com o desempenho! Essa implementação faria uma grande quantidade de cópias de strings, e o tempo de execução seria O (n ^ 2) no número de caracteres. Uma abordagem mais eficiente seria acumular os resultados em um StringBuilder , que é um contêiner mutável para acumular strings. Podemos usar a mesma técnica para paralelizar a redução mutável como fazemos com a redução ordinária.

Então você deve usar um StringBuilder em seu lugar.

Para alguém que acabou de começar com lambdas e streams, levou algum tempo para chegar ao momento “AHA”, até que eu realmente entendi o que está acontecendo aqui. Reescreverei isso um pouco para facilitar um pouco (pelo menos, como eu gostaria que fosse realmente respondida) para um novato do stream como eu.

Está tudo sob a documentação de redução que afirma:

O valor de identidade DEVE ser uma identidade para a function de acumulador. Isso significa que para todo t, accumulator.apply (identity, t) é igual a t.

Nós podemos facilmente provar que o código do caminho é, a associatividade é quebrada:

 static private void isAssociative() { BinaryOperator operator = (s1, s2) -> s1 + "/" + s2; String result = operator.apply("", "a"); System.out.println(result); System.out.println(result.equals("a")); } 

Uma String vazia concatenada com outra String, deve realmente produzir a segunda String; o que não acontece, assim o acumulador (BinaryOperator) NÃO é associativo e assim o método reduce não pode garantir o mesmo resultado em caso de invocação paralela.