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
| Задача | Что использовать | Пример |
|---|---|---|
| Сумма чисел | |
|
| Произведение | |
|
| Сбор в List | |
|
| Сбор в Map | |
|
| Группировка | |
|
| Конкатенация строк | 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 и комплексной агрегации.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ