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