Java: Стримы
Теория: Свертка (reduce)
Операция reduce() (свертка) - это терминальная операция, которая обрабатывает элементы стрима и возвращает одно значение. Это может быть сумма всех чисел, объединение строк или любая другая операция, сводящая все элементы к одному результату. Фактически свертка используется во всех операциях агрегации данных. Рассмотрим пример нахождения суммы всех чисел без использования стримов:
Свертка отличается от остальных операций преобразования списка тремя аспектами:
-
У свертки есть начальное значение. В случае суммы это ноль, в случае умножения — единица и так далее.
-
Следующее значение в свертке базируется на результате, который был получен на предыдущей итерации.
-
Свертка — терминальная операция, то есть она возвращает какое-то значение, которому не нужно выполнять преобразование в список с помощью
toList()
Все это нужно для реализации reduce(), поэтому его использование отличается, например, от фильтрации.
reduce() принимает на вход два параметра, первый это начальное значение, второй лямбда выполняющая операцию свертки. В эту лямбду приходит два параметра: результат предыдущего вычисления и текущий элемент. Накапливаемый результат в процессе вычисления принято называть аккумулятором, так как он аккумулирует значение. Новое значение аккумулятора, всегда равно тому, что было возвращено из предыдущего вызова лямбды. Ниже пример поиска максимального числа, чтобы продемонстрировать это.
Здесь аккумулятор используется только для сравнения, если он меньше, то возвращается текущее значение, которое само становится аккумулятором.
Посмотрим на еще один пример для закрепления. В этом примере на вход подается список букв, которые собираются в строку через конкатенацию.
В этом случае начальным значением будет пустая строка. Если мы захотим соединить слова в обратном порядке, то единственное, что мы поменяем это порядок конкатенации. Вместо result + element будет element + result.
Отличающиеся типы
В примерах выше, тип данных в коллекции и тип аккумулятора совпадали. Так бывает не всегда, в любой нетривиальной ситуации типы скорее всего будут отличаться. Возьмем для примера ситуацию, в которой у нас есть список товаров с указанием цены, представленные строками. Нашей задачей будет извлечь из цены стоимость товара и найти общую сумму товаров в этом списке. Решим нашу задачу сначала без использования reduce.
Этот же код с помощью reduce() выглядит так:
Несмотря на то, что код выглядит правильно, он не компилируется с такой ошибкой:
Если не вдаваться в подробности, то связано это с тем, что типы выводятся таким образом:
ArrayList<T>(T, T) -> T
Выйти из этой ситуации можно с помощью комбайнера, третьего параметра в reduce(), который обычно используется для параллельного запуска вычисления. В таком случае reduce() работает так:
- Коллекция разбивается на части
- Каждая часть обрабатывается в своем потоке выполнения, в результате которого образуется несколько аккумуляторов
- С помощью функции комбайнера эти аккумуляторы собираются в один аккумулятор. В случае числовых аккумуляторов, обычно комбайнер это функция их суммирования, в случае списков это слияние списков и тому подобное.
Комбайнер помогает компилятору увидеть тип аккумулятора, поэтому код с комбайнером компилируется.
При этом параллельного выполнения происходить не будет, так как для этого нужно работать не с обычным стримом stream(), а с параллельным parallelStream(). Это выглядит немного странно, но так устроен reduce().
Рекомендуемые программы
Завершено
0 / 10

