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 — уникальные ключи; иначе получите исключение во время сборки.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ