JavaRush /Курси /JAVA 25 SELF /Просунуті колектори

Просунуті колектори

JAVA 25 SELF
Рівень 31 , Лекція 4
Відкрита

1. Вступ

Коли ви працюєте з колекціями через Stream API, часто потрібно не лише згрупувати елементи, а й одразу щось із ними зробити: перетворити, відфільтрувати, зібрати в інший тип колекції. Для цього у Java є downstream‑колектори — вкладені колектори, які застосовуються до кожної групи чи частини даних.

Що таке downstream‑колектор?

Це колектор, який застосовується до результату групування або поділу. Наприклад, ви можете згрупувати студентів за курсом за допомогою groupingBy, а всередині кожної групи зібрати лише імена або лише відмінників (скажімо, з порогом 4,5 за GPA).

Приклади

mapping

Дозволяє перетворити елементи групи перед збиранням.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • Групуємо студентів за курсом.
  • Для кожної групи збираємо лише імена (а не цілі об’єкти).

filtering

Дозволяє фільтрувати елементи всередині групи (наприклад, залишити тих, у кого GPA >= 4,5).

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

У кожній групі залишаємо лише відмінників.

flatMapping

Дозволяє «розгортати» вкладені колекції всередині груп.

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

Для кожного автора збираємо унікальні теги всіх його книжок.

partitioningBy з downstream

Працює подібно до groupingBy, але ділить на дві групи за булевою умовою.

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

Ділимо студентів на тих, хто склав, і тих, хто не склав; у кожній групі — лише імена.

2. teeing: одночасна агрегація двома колекторами

Іноді потрібно одночасно порахувати кілька агрегатів для потоку: наприклад, і суму, і середнє, або мінімум і максимум. Для цього у Java 12+ зʼявився колектор teeing.

Як працює teeing?

Ви передаєте два колектори та функцію, яка обʼєднує їхні результати.

Синтаксис:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

Приклади

Мінімум + максимум

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

Одночасно знаходимо мінімум і максимум.

Сума + середнє

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

Отримуємо обʼєкт із сумою та середнім значенням.

Приклад: звіт щодо зарплат

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

У SalaryStats зберігаємо і суму, і середнє.

3. toUnmodifiableList/Set/Map і collectingAndThen для «заморожування»

У сучасних версіях Java зʼявилися колекції, які не можна змінити після створення — незмінні (unmodifiable). Це зручно для API, де важливо, щоб результат не можна було випадково змінити.

toUnmodifiableList/Set/Map

  • Повертають незмінну колекцію.
  • Будь‑яка спроба додати/видалити елемент призведе до UnsupportedOperationException.

Приклади:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

Дозволяє застосувати функцію до результату колектора — наприклад, «заморозити» колекцію.

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

Спершу збираємо у звичайний список, потім робимо його незмінним.

Приклад із Set:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. Конвеєрні кейси

Звіти і статистика за розрізами

За допомогою просунутих колекторів можна будувати складні звіти і статистику «в один рядок».

Приклад: середня зарплата за відділами

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

Приклад: топ‑3 найдорожчих товарів за категоріями

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

Незмінний результат як контракт API

Якщо ваш метод повертає колекцію, яку не можна змінювати, це захищає від випадкових помилок і робить API надійнішим.

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

Користувач не зможе зробити getTags().add("новий тег") — буде виняток.

Приклад: звіт із кількома агрегатами (teeing)

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

У SalaryReport зберігаємо і середню, і сумарну зарплату.

5. Типові помилки та нюанси

Помилка № 1: Забули про незмінність. Якщо повертаєте звичайний список, його можуть змінити. Використовуйте toUnmodifiableList/Set/Map або collectingAndThen для «заморожування» результату.

Помилка № 2: Неправильний downstream‑колектор. Якщо потрібно перетворити елементи всередині групи — використовуйте mapping; якщо фільтрувати — filtering; якщо «розгорнути» вкладені колекції — flatMapping.

Помилка № 3: UnsupportedOperationException. Виникає під час спроби змінити колекцію, зібрану через toUnmodifiableList/Set/Map або «заморожену» через collectingAndThen.

Помилка № 4: Втрата унікальності або колізії ключів. toUnmodifiableSet вимагає унікальні елементи, а toUnmodifiableMap — унікальні ключі; інакше отримаєте виняток під час збирання.

1
Опитування
Групування та агрегація, рівень 31, лекція 4
Недоступний
Групування та агрегація
Stream API: групування та агрегація
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ