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


у меня возникли проблемы с полным пониманием роли, что combiner выполняет в ручьях reduce метод.

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

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

ошибка компиляции говорит : (несоответствие аргументов; int не может быть преобразован в Java.ленг.Строка)

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

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

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

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

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

4 89

4 ответа:

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

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

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

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

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

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

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

в вашем случае U-целое число, а T-строка, поэтому этот метод уменьшит поток строки до целого числа.

на BiFunction<U,? super T,U> аккумулятор вы можете передать параметры двух разных типы (U и ? super T), которые в вашем случае являются целыми и строковыми. Кроме того, значение идентификатора 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();

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

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

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

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

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

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

теперь давайте рассмотрим гипотетическую операцию сокращения двух arg, которая уменьшает Stream<T> до U. На других языках это называется "сложить" или "сложить влево" операция, так что это то, что я буду называть его здесь. Обратите внимание, что это не существует в 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 в один результат у. Если мы не меняем типы, это оказывается, что функция аккумулятора такая же, как и функция объединителя. Вот почему редукция к одному типу имеет только функцию аккумулятора, а редукция к другому типу требует отдельных функций аккумулятора и объединителя.

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

так как я люблю каракули и стрелки, чтобы уточнить понятия... давайте начнем!

от строки к строке (последовательный поток)

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

вы можете достичь этого с

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

и это помогает вам визуализировать то, что происходит:

enter image description here

аккумулятор функция преобразует, шаг за шагом, элементы в вашем (красном) потоке в конечное уменьшенное (зеленое) значение. Функция аккумулятора просто преобразует a 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

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

enter image description here

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

от String в int (последовательная трансляция)

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

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

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

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

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