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); // 13
Як працює 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); // 13
Завдання 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 і комплексної агрегації.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ