Зачем нужен комбайнер для метода reduce, преобразующего тип в java 8

Мне трудно полностью понять роль, которую combiner выполняет в методе Streams reduce.

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

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

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

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

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

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

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

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


person Louise Miller    schedule 19.06.2014    source источник
comment
Связанный вопрос: stackoverflow.com/questions/24202473/   -  person nosid    schedule 19.06.2014
comment
ага, это для параллельных потоков ... Я называю дырявую абстракцию!   -  person Andy    schedule 22.04.2017
comment
Я столкнулся с похожей проблемой. Я хотел сделать карту-сокращение. Я хотел, чтобы у метода сокращения Stream была перегруженная версия, которая позволяет отображать тип, отличный от типа ввода, но не заставляет меня писать объединитель. Насколько мне известно, в Java такого метода нет. Поскольку некоторые люди, такие как я, ожидают найти его, но его там нет, это создает путаницу. Примечание: я не хотел писать объединитель, потому что на выходе был сложный объект, для которого объединитель был нереалистичным.   -  person user2367418    schedule 17.05.2021


Ответы (4)


Версии 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).

Также обратите внимание, что эта версия reduce обрабатывает поток 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();
person Eran    schedule 19.06.2014
comment
В качестве второго варианта в вашем последнем коде вы также можете использовать mapToInt(String::length) вместо mapToInt(s -> s.length()), не уверен, что один из них лучше другого, но я предпочитаю первый для удобства чтения. - person skiwi; 19.06.2014
comment
Многие найдут этот ответ, так как не поймут, зачем нужен combiner, почему недостаточно иметь accumulator. В этом случае: комбайнер нужен только для параллельных потоков, чтобы объединить накопленные результаты потоков. - person ddekany; 24.11.2017
comment
Я не считаю ваш ответ особенно полезным - потому что вы вообще не объясняете, что должен делать комбайнер и как я могу работать без него! В моем случае я хочу уменьшить тип T до U, но это вообще невозможно сделать параллельно. Это просто невозможно. Как сообщить системе, что я не хочу / не нуждаюсь в параллелизме и, таким образом, не использовать комбайнер? - person Zordid; 28.08.2018
comment
@Zordid API Streams не включает возможность уменьшить тип T до U без передачи комбайнера. - person Eran; 28.08.2018
comment
Этот ответ вообще не объясняет комбайнер, а только то, почему OP нужны варианты без комбайнера. - person Benny Bottema; 25.09.2020

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

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

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

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

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

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

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

person Stuart Marks    schedule 19.06.2014
comment
Итак, что вы можете сделать, если вам нужен foldLeft, потому что вычисление зависит от предыдущего результата и не может быть распараллелено? - person amoebe; 09.05.2015
comment
@amoebe Вы можете реализовать свой собственный foldLeft, используя forEachOrdered. Однако промежуточное состояние должно храниться в захваченной переменной. - person Stuart Marks; 10.05.2015
comment
@StuartMarks спасибо, в итоге я использовал jOOλ. У них есть аккуратный foldLeft. - person amoebe; 10.05.2015
comment
Люблю этот ответ! Поправьте меня, если я ошибаюсь: это объясняет, почему рабочий пример OP (второй) никогда не будет вызывать комбайнер при запуске, будучи последовательным потоком. - person Luigi Cortese; 25.11.2015
comment
Это объясняет почти все ... кроме того, почему это должно исключать последовательное сокращение. В моем случае НЕВОЗМОЖНО делать это параллельно, так как мое сокращение сокращает список функций до U, вызывая каждую функцию для промежуточного результата результата ее предшественника. Это вообще невозможно сделать параллельно, и нет способа описать комбайнер. Какой метод я могу использовать для этого? - person Zordid; 28.08.2018
comment
@Zordid у вас есть старый цикл sad for или вы можете использовать forEach с внешней переменной в качестве аккумулятора. Это грустно, и я в твоей ситуации, поэтому тебе сочувствую - person Rick77; 07.06.2019
comment
Превосходно. Отличное понимание потоков java уменьшает api - person sigirisetti; 13.07.2019
comment
@Zordid звучит так, будто вы пытаетесь использовать сокращение потока для чего-то, для чего он не предназначен. Сокращение должно быть без гражданства, похоже, у вас нет. Что означает результат предшественников? Похоже, это может быть вашей проблемой. - person Frans; 22.11.2019
comment
@Frans Проблема не в сохранении состояния. Проблема в том, что API потока Java требует, чтобы функции аккумуляторов были ассоциативными (для поддержки параллельного выполнения), а описываемый Zordid аккумулятор не ассоциативен. Разрешение параллельного выполнения невозможно одновременно с разрешением неассоциативных функций накопителя. - person Silwing; 03.09.2020
comment
@Silwing Я не совсем понимаю, что вы имеете в виду под ассоциативностью; Вы можете объяснить? Я думаю, мы говорим одно и то же, но разными словами. - person Frans; 13.09.2020
comment
Ассоциативность @Frans - это свойство, которое может иметь операция. Это означает, что если вы объединяете несколько таких операций вместе, не имеет значения, в каком порядке они выполняются: (a op b) op c = a op (b op c) Таким образом, все, что требует определенного порядка выполнения, не является ассоциативным. Я имею в виду, что он может быть без гражданства, не будучи ассоциативным. - person Silwing; 14.09.2020
comment
Я не понимаю, как этот API соответствует тому, что вы сказали о том, что параллельный и последовательный должны работать одинаково. Я реализую ту же логику (объединение накопленных результатов с текущим элементом против объединения накопленных результатов) в двух местах: аккумулятор и объединитель. Если вы допустили ошибку в одной из этих функций, результат будет отличаться в зависимости от того, как вы запускаете поток. - person endertunc; 13.10.2020
comment
Отличный ответ, но излишне теоретический. Если бы я прочитал / понял это раньше, я бы не публиковал это: stackoverflow.com/questions/64403813/ - person igobivo; 17.10.2020
comment
Отличное объяснение, некоторые из этих фактов должны быть в reduce Javadoc, особенно те, которые касаются последовательного / параллельного потока и комбайнера. - person Gerard Bosch; 25.10.2020

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

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

Предположим, у вас есть 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 (последовательный поток)

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

person Luigi Cortese    schedule 28.11.2015
comment
Спасибо за это. Мне даже не нужно было читать. Я действительно хотел бы, чтобы они просто добавили чертову функцию складывания. - person Lodewijk Bogaards; 04.03.2016
comment
@LodewijkBogaards рад, что это помогло! JavaDoc здесь действительно довольно загадочный - person Luigi Cortese; 04.03.2016
comment
@LuigiCortese Всегда ли в параллельном потоке элементы делятся на пары? - person TheLogicGuy; 18.05.2017
comment
Я ценю ваш ясный и полезный ответ. Я хочу повторить кое-что из того, что вы сказали: в любом случае, комбайнер должен быть предоставлен, но он никогда не будет вызван. Это часть «Дивного нового мира функционального программирования Java», который, как меня уверяли бесчисленное количество раз, делает ваш код более кратким и легким для чтения. Будем надеяться, что примеров такой лаконичной ясности (цитаты из пальцев), подобных этой, осталось немного. - person dnuttle; 23.05.2019
comment
Будет НАМНОГО лучше проиллюстрировать редукцию с восемью струнами ... - person Ekaterina Ivanova iceja.net; 02.06.2020
comment
Это лучший ответ. Руки вниз. - person Mingtao Sun; 15.12.2020

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

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

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

list.stream().map(i -> accumulator(identity, i))
             .reduce(identity,
                     combiner);
person quiz123    schedule 04.09.2015
comment
Такой map трюк в зависимости от конкретных accumulator и combiner может значительно замедлить работу. - person Tagir Valeev; 04.09.2015
comment
Или значительно ускорите его, так как теперь вы можете упростить accumulator, отбросив первый параметр. - person quiz123; 04.09.2015
comment
Возможна параллельная редукция, это зависит от ваших вычислений. В вашем случае вы должны осознавать сложность комбайнера, но также и аккумулятора для идентификации по сравнению с другими экземплярами. - person LoganMzz; 27.06.2017