Зарегистрируйтесь, чтобы продолжить обучение

Свертка (reduce) Java: Стримы

Операция reduce() (свертка) - это терминальная операция, которая обрабатывает элементы стрима и возвращает одно значение. Это может быть сумма всех чисел, объединение строк или любая другая операция, сводящая все элементы к одному результату. Фактически свертка используется во всех операциях агрегации данных. Рассмотрим пример нахождения суммы всех чисел без использования стримов:

var numbers = List.of(1, 2, 3, 4, 5);

// Переменная для хранения суммы
var sum = 0;

// Цикл для суммирования чисел
for (var number : numbers) {
    sum += number;
}

// Вывод суммы чисел
System.out.println("Сумма чисел: " + sum); // => Сумма чисел: 15

Свертка отличается от остальных операций преобразования списка тремя аспектами:

  • У свертки есть начальное значение. В случае суммы это ноль, в случае умножения — единица и так далее.

    var sum = 0;
    
  • Следующее значение в свертке базируется на результате, который был получен на предыдущей итерации.

    sum += number;
    
  • Свертка — терминальная операция, то есть она возвращает какое-то значение, которому не нужно выполнять преобразование в список с помощью toList()

Все это нужно для реализации reduce(), поэтому его использование отличается, например, от фильтрации.

var numbers = List.of(1, 2, 3, 4, 5);

var sum = numbers.stream()
                 .reduce(0, (subtotal, element) -> subtotal + element);
                 // или используя Integer.sum(subtotal, element)
                 // .reduce(0, Integer::sum);

System.out.println(sum); // => Вывод чисел: 15

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

var numbers = List.of(1, 2, 3, 4, 5);

// Начальное значение - первый элемент списка
var maxNumber = numbers.stream()
                       .reduce(numbers.get(0), (a, b) -> a > b ? a : b);

Здесь аккумулятор используется только для сравнения, если он меньше, то возвращается текущее значение, которое само становится аккумулятором.

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

var letters = List.of("h", "e", "x", "l", "e", "t");
var result = letters.stream() // hexlet
                    .reduce("", (result, element) -> result + element);
                    // .reduce("", String::concat);

В этом случае начальным значением будет пустая строка. Если мы захотим соединить слова в обратном порядке, то единственное, что мы поменяем это порядок конкатенации. Вместо result + element будет element + result.

Отличающиеся типы

В примерах выше, тип данных в коллекции и тип аккумулятора совпадали. Так бывает не всегда, в любой нетривиальной ситуации типы скорее всего будут отличаться. Возьмем для примера ситуацию, в которой у нас есть список товаров с указанием цены представленные строками. Нашей задачей будет извлечь из цены стоимость товара и найти общую сумму товаров в этом списке. Решим нашу задачу сначала без использования reduce.

var products = List.of("Laptop: 800", "Headphones: 50", "Smartphone: 500", "Mouse: 20");

var totalCost = 0;
for (var product : products) {
    // Отделяем название от цены
    var parts = product.split(": "); // Splitting the string into product name and price
    // Преобразуем цену в Integer
    var price = Integer.parseInt(parts[1].trim()); // Extracting the price part and converting
    // Находим общую сумму
    totalCost += price;
}

System.out.println(totalCost); // => 1370

Этот же код с помощью reduce() выглядит так:

var products = List.of("Laptop: 800", "Headphones: 50", "Smartphone: 500", "Mouse: 20");

var totalPrice = products.stream()
                         .reduce(0,
                                 (sum, product) -> {
                                     var parts = product.split(": ");
                                     var price = Integer.parseInt(parts[1].trim());
                                     return sum + price;
                                 });

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

error: no suitable method found for reduce(int,(sum,produ[...]ce; })
                .reduce(0,
                ^
    method Stream.reduce(String,BinaryOperator<String>) is not applicable
      (argument mismatch; int cannot be converted to String)

Если не вдаваться в подробности, то связано это с тем, что типы выводятся таким образом:

  • ArrayList<T>
  • (T, T) -> T

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

  • Коллекция разбивается на части
  • Каждая часть обрабатывается в своем потоке выполнения, в результате которого образуется несколько аккумуляторов
  • С помощью функции комбайнера эти аккумуляторы собираются в один аккумулятор. В случае числовых аккумуляторов, обычно комбайнер это функция их суммирования, в случае списков это слияние списков и тому подобное.

Комбайнер помогает компилятору увидеть тип аккумулятора, поэтому код с комбайнером компилируется.

var products = List.of("Laptop: 800", "Headphones: 50", "Smartphone: 500", "Mouse: 20");

var totalPrice = products.stream()
                         .reduce(0,
                                 (sum, product) -> {
                                     var parts = product.split(": ");
                                     var price = Integer.parseInt(parts[1].trim());
                                     return sum + price;
                                 },
                                 Integer::sum);

При этом параллельного выполнения происходить не будет, так как для этого нужно работать не с обычным стримом stream(), а с параллельным parallelStream(). Это выглядит немного странно, но так устроен reduce().


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка приложений на языке Java
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»