Почему комбайнер необходим для метода уменьшения, который преобразует тип в java 8

У меня возникли проблемы с полным пониманием роли, которую выполняет combiner в методе Streams reduce.

Например, следующий код не компилируется:

int length = asList("str1", "str2").stream()
            .reduce(0, (accumulatedInt, str) -> accumulatedInt + str.length());

Ошибка компиляции говорит:      (аргумент mismatch; int не может быть преобразован в java.lang.String)

но этот код компилируется:

int length = asList("str1", "str2").stream()  
    .reduce(0, (accumulatedInt, str ) -> accumulatedInt + str.length(), 
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2);

Я понимаю, что метод объединителя используется в параллельных потоках, поэтому в моем примере он объединяет два промежуточных накопленных ints.

Но я не понимаю, почему первый пример не компилируется без комбайнера или как объединитель решает преобразование строки в int, поскольку он просто складывает два ints.

Может ли кто-нибудь пролить свет на это?

Ответ 1

Две и три версии аргументов reduce, которые вы пытались использовать, не принимают один и тот же тип для accumulator.

Два аргумента reduce определяемые как:

T reduce(T identity,
         BinaryOperator<T> accumulator)

В вашем случае T является String, поэтому BinaryOperator<T> должен принимать два аргумента String и возвращать String. Но вы передаете ему int и String, что приводит к ошибке компиляции - argument mismatch; int cannot be converted to java.lang.String. На самом деле, я думаю, что передача 0, поскольку значение идентичности здесь также неверно, поскольку ожидается String (T).

Также обратите внимание, что эта версия уменьшает процесс потока Ts и возвращает T, поэтому вы не можете использовать его для уменьшения потока String до int.

Три аргумента reduce определены как:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

В вашем случае U является Integer и T является String, поэтому этот метод уменьшит поток String до Integer.

Для аккумулятора BiFunction<U,? super T,U> вы можете передавать параметры двух разных типов (U и? super T), которые в вашем случае являются Integer и String. Кроме того, значение U идентичности принимает целое число в вашем случае, поэтому передача его 0 прекрасна.

Еще один способ добиться того, чего вы хотите:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .reduce(0, (accumulatedInt, len) -> accumulatedInt + len);

Здесь тип потока соответствует типу возврата reduce, поэтому вы можете использовать две версии параметров reduce.

Конечно, вам не нужно использовать reduce вообще:

int length = asList("str1", "str2").stream().mapToInt (s -> s.length())
            .sum();

Ответ 2

Ответ Эрана описал различия между версиями reduce двух-arg и трех-arg версий, в которых первая уменьшает Stream<T> до T, тогда как последний уменьшает Stream<T> до U. Однако на самом деле это не объясняло необходимость дополнительной функции объединителя при уменьшении Stream<T> до U.

Одним из принципов проектирования API Streams является то, что API не должен различаться между последовательными и параллельными потоками или иначе, конкретный API не должен препятствовать правильной работе потока последовательно или параллельно. Если ваши лямбды имеют правильные свойства (ассоциативные, неинтерферирующие и т.д.), Поток, выполняемый последовательно или параллельно, должен давать одинаковые результаты.

Пусть сначала рассмотрим версию сокращения с двумя аргументами:

T reduce(I, (T, T) -> T)

Последовательная реализация проста. Значение идентичности I "накапливается" с элементом нулевого потока, чтобы дать результат. Этот результат накапливается с первым элементом потока, чтобы дать другой результат, который, в свою очередь, накапливается со вторым элементом потока и т.д. После того, как последний элемент накоплен, возвращается окончательный результат.

Параллельная реализация начинается с разделения потока на сегменты. Каждый сегмент обрабатывается собственным потоком последовательным образом, описанным выше. Теперь, если у нас есть N потоков, мы имеем N промежуточных результатов. Их необходимо сократить до одного результата. Поскольку каждый промежуточный результат имеет тип T, и у нас есть несколько, мы можем использовать одну и ту же функцию аккумулятора, чтобы уменьшить промежуточные результаты N до одного результата.

Теперь рассмотрим гипотетическую операцию сокращения двух аргументов, которая уменьшает Stream<T> до U. В других языках это называется операцией "fold" или "fold-left", так что я назову ее здесь. Обратите внимание, что это не существует на Java.

U foldLeft(I, (U, T) -> U)

(Обратите внимание, что значение идентичности I имеет тип U.)

Последовательная версия foldLeft аналогична последовательной версии reduce, за исключением того, что промежуточные значения имеют тип U вместо типа T. Но в остальном это одно и то же. (Гипотетическая операция foldRight была бы аналогичной, за исключением того, что операции будут выполняться справа налево, а не слева направо.)

Теперь рассмотрим параллельную версию foldLeft. Пусть начнется разделение потока на сегменты. Затем мы можем иметь, что каждый из N потоков уменьшает значения T в своем сегменте на N промежуточных значений типа U. Теперь что? Как мы получаем от N значений типа U до одного результата типа U?

Отсутствует другая функция, которая объединяет несколько промежуточных результатов типа U в единичный результат типа U. Если у нас есть функция, которая объединяет два значения U в один, которые достаточны для уменьшения любого количество значений до одного - точно так же, как и первоначальное сокращение выше. Таким образом, операция сокращения, которая дает результат другого типа, нуждается в двух функциях:

U reduce(I, (U, T) -> U, (U, U) -> U)

Или, используя синтаксис Java:

<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

Итак, для параллельного сокращения для другого типа результата нам нужны две функции: одна, которая накапливает элементы T до промежуточных значений U, а вторая, которая объединяет промежуточные значения U в единый результат U. Если мы не переключаем типы, получается, что функция аккумулятора такая же, как функция объединителя. То, что сокращение до того же типа имеет только функцию аккумулятора, а уменьшение к другому типу требует отдельных функций аккумулятора и объединителя.

Наконец, Java не предоставляет операции foldLeft и foldRight, потому что они подразумевают конкретный порядок операций, который по своей сути является последовательным. Это противоречит описанному выше принципу проектирования предоставления API-интерфейсов, которые одинаково поддерживают последовательную и параллельную работу.

Ответ 3

Так как мне нравятся каракули и стрелки, чтобы прояснить понятия... let start!

От String to String (последовательный поток)

Предположим, что у вас 4 строки: ваша цель - объединить такие строки в одну. В основном вы начинаете с типа и заканчиваете с тем же типом.

Вы можете достичь этого с помощью

String res = Arrays.asList("one", "two","three","four")
        .stream()
        .reduce("",
                (accumulatedStr, str) -> accumulatedStr + str);  //accumulator

и это поможет вам понять, что происходит:

введите описание изображения здесь

Функция аккумулятора преобразует, шаг за шагом, элементы в вашем (красном) потоке до конечного уменьшенного (зеленого) значения. Функция аккумулятора просто преобразует объект String в другой String.

От String до int (параллельный поток)

Предположим, что у вас есть те же 4 строки: ваша новая цель состоит в том, чтобы суммировать их длину и вы хотите распараллелить свой поток.

Что вам нужно, так это:

int length = Arrays.asList("one", "two","three","four")
        .parallelStream()
        .reduce(0,
                (accumulatedInt, str) -> accumulatedInt + str.length(),                 //accumulator
                (accumulatedInt, accumulatedInt2) -> accumulatedInt + accumulatedInt2); //combiner

и это схема того, что происходит

введите описание изображения здесь

Здесь функция аккумулятора (a BiFunction) позволяет преобразовать ваши данные String в данные int. Будучи параллельным потоком, он разделяется на две (красные) части, каждая из которых разработана независимо друг от друга и производит столько же частичных (оранжевых) результатов. Определение объединителя необходимо для обеспечения правила объединения частичных результатов int в окончательный (зеленый) int один.

От String до int (последовательный поток)

Что делать, если вы не хотите распараллелить свой поток? Ну, в любом случае, комбайнер должен быть предоставлен, но он никогда не будет вызван, если не будет получено никаких частичных результатов.

Ответ 4

Нет версии сокращения, которая принимает два разных типа без комбайнера, поскольку он не может выполняться параллельно (не уверен, почему это требование). Тот факт, что аккумулятор должен быть ассоциативным, делает этот интерфейс практически бесполезным, поскольку:

list.stream().reduce(identity,
                     accumulator,
                     combiner);

Производит те же результаты, что и:

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);