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: группировка и агрегация
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Роман Соловьев Уровень 58
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 Уровень 41
12 декабря 2025
это же не совсем одно и тоже. В концептуальной идее - да, но в реализации нет Различия: 1) toUnmodifiableList - не позволяет, чтобы в списке были NULL, иначе упадет с ошибкой. toList - корректно работает с NULL 2) toUnmodifiableList - в зависимости от размеров коллекции не гарантирует, что вернет неизменяемый список. toList - гарантирует результат Но в целом - да, стоило сказать, что это более новый (Java 16+) и надежный способ получить неизменяемый список