Потоки последовательного и параллельного выполнения streamов Java8 дают разные результаты?

Выполнение следующего примера streamа в Java8:

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

выходы:

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

Что, конечно, не удивительно. Из-за http://docs.oracle.com/javase/8/docs/api/index.html?overview-summary.html не должно иметь значения, выполняется ли stream последовательно или параллельно:

За исключением операций, определенных как явно недетерминированные, такие как findAny (), следует ли stream последовательно или параллельно не изменять результат вычисления.

AFAIK reduce() детерминирован и (s1, s2) -> s1 + "/" + s2 ассоциативен, так что добавление parallel() должно давать тот же результат:

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

Однако результат на моей машине:

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

Что здесь не так?

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 

Из документации сокращения:

Значение идентификатора должно быть идентификатором для функции аккумулятора. Это означает, что для всех t, accumulator.apply (identity, t) равен t.

Что не так в вашем случае – «» и «a» создает «/ a».

Я извлек функцию аккумулятора и добавил распечатку, чтобы показать, что происходит:

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

Это пример вывода (он отличается между прогонами):

 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 

Вы можете добавить оператор if в свою функцию для обработки пустой строки отдельно:

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

Как заметил Марко Топольник, проверка s2 не требуется, так как аккумулятор не должен быть коммутативной функцией.

Чтобы добавить к другому ответу,

Возможно, вы захотите использовать Mutable reduction , документ указывает, что делать что-то вроде

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

Дает плохую производительность.

Мы получим желаемый результат, и он будет работать параллельно. Однако мы, возможно, не очень довольны производительностью! Такая реализация будет выполнять большое количество копий строк, а время выполнения будет O (n ^ 2) в количестве символов. Более реалистичным подходом было бы накопление результатов в StringBuilder , который является изменяемым контейнером для накопления строк. Мы можем использовать ту же технику, чтобы распараллеливать изменчивое восстановление, как это происходит с обычной редукцией.

Поэтому вместо этого вы должны использовать StringBuilder.

Для тех, кто только начинал с lambda и ручейков, потребовалось довольно много времени, чтобы добраться до момента «AHA», пока я действительно не понял, что здесь происходит. Я немного перефразирую это, чтобы немного облегчить (по крайней мере, как мне хотелось бы, чтобы это было действительно ответили) для новичков streamа, подобных мне.

Все это под документацией сокращения, которая гласит:

Значение идентификатора ДОЛЖНО быть идентификатором для функции аккумулятора. Это означает, что для всех t, accumulator.apply (identity, t) равен t.

Мы можем легко доказать, что код пути, ассоциативность нарушена:

 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")); } 

Пустая строка, конкатенированная с другой строкой, должна действительно создать вторую строку; что не происходит, поэтому накопитель (BinaryOperator) НЕ ассоциативен, и поэтому метод уменьшения не может гарантировать тот же результат в случае параллельного вызова.

  • Как определить функцию lambda для копирования копии вместо ссылки на C #?
  • Lambda выражение для преобразования массива / Список строк в массив / Список целых чисел
  • Почему я не могу редактировать метод, содержащий анонимный метод в отладчике?
  • Expression.Lambda и генерация запросов во время выполнения, самый простой пример «Где»
  • Захват ссылки по ссылке в C ++ 11 lambda
  • Печать отладочной информации об ошибках с помощью java 8 lambda-выражений
  • Невозможно использовать параметр ref или out в lambda-выражениях
  • Как отличить выражение <Func к выражению <Func >
  • C # => оператор?
  • Справочник по методу экземпляра и lambda-параметры
  • Как работает общая lambda в C ++ 14?
  • Interesting Posts

    Доступ к event.target внутри обратного вызова в реакции

    Windows 7 Шифрует папку с паролем

    Как я могу получить значение переменной сеанса внутри статического метода?

    Сопоставлено ли целочисленное переполнение по-прежнему неопределенным поведением в C ++?

    Бесплатное программное обеспечение водяного знака для пакетного изображения

    Читайте url для строки в нескольких строках кода Java

    Unicode-эквиваленты для \ w и \ b в регулярных выражениях Java?

    Как найти ключ активации Windows 10

    Android – LinearLayout Горизонтальная с обертыванием детей

    Понимание блока и типа блока Magento

    Как разобрать текст справа налево в Блокноте?

    Отладка Logcat Eclipse

    Преобразовать строку из ASCII в EBCDIC в Java?

    Получение имен файлов всех файлов в папке

    Как запустить программу с повышенными разрешениями с учетной записи пользователя с ограниченным доступом без пароля администратора?

    Давайте будем гением компьютера.