it-swarm.com.ru

Зачем нужен объединитель для метода lower, который преобразует тип в 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, поскольку он просто складывает два целых числа.

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

114
Louise Miller

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

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

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

В вашем случае T - это строка, поэтому BinaryOperator<T> должен принять два аргумента строки и вернуть строку. Но вы передаете ему 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 - целое число, а T - строка, поэтому этот метод уменьшит поток строки до целого числа.

Для аккумулятора 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();
59
Eran

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

179
Stuart Marks

Так как мне нравятся каракули и стрелки для уточнения понятий ... начнем!

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

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

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

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

и это поможет вам визуализировать происходящее:

enter image description here

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

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

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

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

91
Luigi Cortese

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

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

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

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