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

Продвинутые коллекторы

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

1. Введение

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

Что такое downstream collector?

Это коллектор, который применяется к результату группировки или разбиения. Например, вы можете сгруппировать студентов по курсу с 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. Конвейерные кейсы

Отчёты и статистика по срезам

С помощью advanced-коллекторов можно строить сложные отчёты и статистику «в одну строчку».

Пример: средняя зарплата по отделам

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
Задача
JAVA 25 SELF, 31 уровень, 4 лекция
Недоступна
Сортировка абитуриентов в Академии Великих Достижений 📜
Сортировка абитуриентов в Академии Великих Достижений 📜
1
Задача
JAVA 25 SELF, 31 уровень, 4 лекция
Недоступна
Экспресс-анализ финансового портфеля 💰
Экспресс-анализ финансового портфеля 💰
1
Опрос
Группировка и агрегация, 31 уровень, 4 лекция
Недоступен
Группировка и агрегация
Stream API: группировка и агрегация
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Timofey Novitsky Уровень 36
22 мая 2026
Мне нравится, что в задачах предыдущей была тема из этой.
Сержик Бурчо Уровень 56
16 мая 2026
Жесть просто, мозги поплыли от этого уровня
Iskander Уровень 45
19 марта 2026
Мне уже снятся эти коллекторы))))
Grrbrr7 Уровень 44
24 марта 2026
Главное чтоб не звонили в дверь)
Роман Соловьев Уровень 1
7 декабря 2025
Интересно почему в задачах предыдущих лекциях ожидали, что уже знаем то, что дают в текущей🤨
I'll kick them all Уровень 5
4 октября 2025
А зачем?

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}
когда


public List<String> getTags() {
    return tags.stream().toList();
}
Ну хотя б сказали б что одно и то же =)
Sergey Lunev Уровень 1
12 декабря 2025
это же не совсем одно и тоже. В концептуальной идее - да, но в реализации нет Различия: 1) toUnmodifiableList - не позволяет, чтобы в списке были NULL, иначе упадет с ошибкой. toList - корректно работает с NULL 2) toUnmodifiableList - в зависимости от размеров коллекции не гарантирует, что вернет неизменяемый список. toList - гарантирует результат Но в целом - да, стоило сказать, что это более новый (Java 16+) и надежный способ получить неизменяемый список