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); // 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

Завдання Що використовувати Приклад
Сума чисел
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); // 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 і комплексної агрегації.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ