JavaRush /Курсы /JAVA 25 SELF /Методы reduce и collect: агрегирование данных

Методы reduce и collect: агрегирование данных

JAVA 25 SELF
31 уровень , 1 лекция
Открыта

1. Метод reduce: универсальная свёртка

В программировании часто требуется «свернуть» коллекцию в одно итоговое значение: посчитать сумму, найти произведение, склеить строки, вычислить агрегированную метрику или собрать элементы в новую структуру. Раньше это делалось вручную через циклы и переменную-аккумулятор. Сегодня Stream API предлагает элегантные способы — универсальные методы reduce и collect, позволяющие писать код компактно и декларативно.

  • reduce — сворачивает поток в одно итоговое значение (сумма, произведение, конкатенация и т. п.).
  • collect — преобразует поток в коллекцию, строку, карту или произвольную структуру.

Давайте разбираться по порядку.

Зачем нужен reduce?

reduce — терминальный метод, который «сворачивает» элементы потока в одно значение, используя функцию-аккумулятор. Мысленно это выглядит как проход по коллекции со шаговым накоплением результата.

Сигнатуры метода reduce

В Stream API есть три основных варианта reduce():

Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  • accumulator — функция, принимающая текущее накопленное значение и следующий элемент, и возвращающая новый результат.
  • identity — начальное значение аккумулятора (например, 0 для суммы, 1 для произведения).
  • combiner — используется в параллельных потоках для объединения промежуточных результатов.

Примеры использования reduce

Пример 1: Сумма чисел

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// reduce без identity — результат Optional
Optional<Integer> sum1 = numbers.stream()
    .reduce((a, b) -> a + b);

System.out.println(sum1.orElse(0)); // 15

// reduce с identity — результат всегда есть
int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

System.out.println(sum2); // 15

Пример 2: Произведение всех чисел

int product = numbers.stream()
    .reduce(1, (a, b) -> a * b);
System.out.println(product); // 120

Пример 3: Конкатенация строк

List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
    .reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API

Пример 4: Поиск максимального элемента

Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);
max.ifPresent(System.out::println); // 5

Пример 5: Сумма длин всех строк

List<String> texts = List.of("кот", "собака", "слон");
int totalLength = texts.stream()
    .map(String::length)
    .reduce(0, Integer::sum);
System.out.println(totalLength); // 14

Как работает reduce

Логика reduce эквивалентна следующему циклу:

T result = identity;
for (T element : collection) {
    result = accumulator.apply(result, element);
}
return result;

Если не задан identity, то стартовым значением берётся первый элемент потока, а метод возвращает Optional (может быть пустым, если поток пуст).

2. Метод collect: универсальное преобразование

collect — терминальный метод, который превращает поток в коллекцию, строку, карту или любую другую структуру. Для этого используются «коллекторы» (Collector), описывающие процесс сборки. Чаще всего берём готовые из класса Collectors.

Самые популярные коллекторы

  • Collectors.toList() — собирает элементы в List.
  • Collectors.toSet() — собирает элементы в Set.
  • Collectors.toMap() — собирает элементы в Map.
  • Collectors.joining() — склеивает строки в одну.
  • Collectors.groupingBy() — группирует элементы по признаку.
  • Collectors.counting() — считает количество элементов.
  • Collectors.summarizingInt() — собирает статистику по числам (сумма, среднее, мин/макс).

Примеры использования collect

Пример 1: Сбор в список

List<String> names = List.of("Аня", "Борис", "Вася", "Аня");
List<String> uniqueNames = names.stream()
    .distinct()
    .collect(Collectors.toList());
System.out.println(uniqueNames); // [Аня, Борис, Вася]

Пример 2: Сбор в множество (Set)

Set<String> nameSet = names.stream()
    .collect(Collectors.toSet());
System.out.println(nameSet); // [Аня, Борис, Вася] (порядок не гарантируется)

Пример 3: Сбор в строку

String csv = names.stream()
    .collect(Collectors.joining(", "));
System.out.println(csv); // Аня, Борис, Вася, Аня

Пример 4: Сбор в Map

Допустим, у нас есть класс:

public class Employee {
    private String name;
    private String department;
    public Employee(String name, String department) {
        this.name = name;
        this.department = department;
    }
    public String getName() { return name; }
    public String getDepartment() { return department; }
}

Соберём карту «имя → отдел»:

List<Employee> employees = List.of(
    new Employee("Аня", "IT"),
    new Employee("Борис", "HR"),
    new Employee("Вася", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // обработка дубликатов имён
    ));

System.out.println(nameToDept); // {Аня=IT, Борис=HR, Вася=IT}

Пример 5: Собрать уникальные элементы в Set

Set<String> unique = names.stream()
    .collect(Collectors.toSet());
System.out.println(unique);

Пример 6: Собрать статистику по числам

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
    .collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum());     // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax());     // 5
System.out.println(stats.getMin());     // 1

3. Сравнение: когда использовать reduce, а когда — collect

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

collect — когда требуется собрать элементы в коллекцию/карту/строку или выполнить более сложную агрегацию с Collector. Для таких задач collect обычно мощнее и эффективнее.

Таблица: reduce vs collect

Задача Что использовать Пример
Сумма чисел
reduce
reduce(0, Integer::sum)
Произведение
reduce
reduce(1, (a, b) -> a * b)
Сбор в List
collect
collect(Collectors.toList())
Сбор в Map
collect
collect(Collectors.toMap(...))
Группировка
collect
collect(Collectors.groupingBy(...))
Конкатенация строк reduce / collect reduce("", String::concat) или Collectors.joining()

4. Практические задачи

Задача 1: Найти сумму длин всех строк в списке

List<String> words = List.of("кот", "собака", "слон");

int totalLength = words.stream()
    .mapToInt(String::length)
    .sum(); // или через reduce: .reduce(0, Integer::sum)

System.out.println(totalLength); // 14

Задача 2: Собрать уникальные элементы в Set

List<String> fruits = List.of("яблоко", "груша", "яблоко", "апельсин");

Set<String> uniqueFruits = fruits.stream()
    .collect(Collectors.toSet());

System.out.println(uniqueFruits); // [яблоко, груша, апельсин]

Задача 3: Построить Map из списка объектов

List<Employee> employees = List.of(
    new Employee("Аня", "IT"),
    new Employee("Борис", "HR"),
    new Employee("Вася", "IT")
);

Map<String, String> nameToDept = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getDepartment,
        (oldValue, newValue) -> newValue // если имена совпадают
    ));

System.out.println(nameToDept);

Задача 4: Собрать все имена через запятую

String allNames = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", "));

System.out.println(allNames); // Аня, Борис, Вася

5. Особенности реализации и нюансы

Optional и reduce

Если использовать reduce без identity, результат — Optional. Это безопасно: если поток пуст, результат тоже пустой. Не забывайте корректно его обрабатывать: ifPresent(...), orElse(...), orElseThrow(...).

Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);

Собственные коллекторы: если хочется экстрима

Вы можете написать свой Collector, если стандартных не хватает. Но для 99% задач — достаточно готовых из Collectors.

Коллекторы и параллельные стримы

Готовые коллекторы из Collectors спроектированы для корректной работы с parallelStream(). Не добавляйте элементы вручную в общую изменяемую коллекцию внутри forEach на параллельном потоке — словите гонки данных.

6. Типичные ошибки при работе с reduce и collect

Ошибка №1: Не проверяете Optional после reduce. Если поток пуст, reduce без identity возвращает пустой Optional. Вызов get() приведёт к NoSuchElementException. Используйте ifPresent, orElse или orElseThrow.

Ошибка №2: Пытаетесь собрать коллекцию через reduce. Это можно сделать, но collect для этого предназначен лучше и быстрее:

// Неэффективно!
List<String> list = stream.reduce(
    new ArrayList<>(),
    (acc, elem) -> { acc.add(elem); return acc; },
    (acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Лучше так:
List<String> list2 = stream.collect(Collectors.toList());

Ошибка №3: Не обрабатываете дубликаты ключей в toMap. Если ключи совпадают, будет исключение. Добавляйте третий аргумент в toMap для разрешения конфликтов.

Ошибка №4: Используете изменяемые коллекции в параллельных стримах без синхронизации. В collect используйте стандартные коллекторы — они корректно работают в параллельном режиме. Не делайте list.add() в forEach на parallelStream().

Ошибка №5: Путаете reduce и collect для сложных задач. reduce — для простых агрегаций (сумма, максимум). collect — для сбора в коллекции, группировки, построения Map и комплексной агрегации.

1
Задача
JAVA 25 SELF, 31 уровень, 1 лекция
Недоступна
Поиск пиковой температуры за неделю 🌡️
Поиск пиковой температуры за неделю 🌡️
1
Задача
JAVA 25 SELF, 31 уровень, 1 лекция
Недоступна
Создание магического заклинания 📜
Создание магического заклинания 📜
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ