1. Wprowadzenie
Kiedy pracujesz z kolekcjami przez Stream API, często trzeba nie tylko pogrupować elementy, ale od razu coś z nimi zrobić: przekształcić, odfiltrować, zebrać do innego typu kolekcji. Do tego w Javie są kolektory downstream — zagnieżdżone kolektory, które stosuje się do każdej grupy lub części danych.
Co to jest kolektor downstream?
To kolektor stosowany do wyniku grupowania lub podziału. Na przykład możesz pogrupować studentów według kursu za pomocą groupingBy, a wewnątrz każdej grupy zebrać tylko imiona albo tylko prymusów (np. z progiem 4,5 GPA).
Przykłady
mapping
Pozwala przekształcić elementy grupy przed zebraniem.
Map<Integer, List<String>> namesByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.mapping(Student::getName, Collectors.toList())
));
- Grupujemy studentów według kursu.
- Dla każdej grupy zbieramy tylko imiona (a nie całe obiekty).
filtering
Pozwala filtrować elementy wewnątrz grupy (np. pozostawić GPA >= 4,5).
Map<Integer, List<Student>> honorsByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
));
W każdej grupie pozostawiamy tylko najlepszych.
flatMapping
Pozwala „rozwinąć” zagnieżdżone kolekcje wewnątrz grup.
Map<String, Set<String>> tagsByAuthor = books.stream()
.collect(Collectors.groupingBy(
Book::getAuthor,
Collectors.flatMapping(
book -> book.getTags().stream(),
Collectors.toSet()
)
));
Dla każdego autora zbieramy unikalne tagi wszystkich jego książek.
partitioningBy z downstream
Działa podobnie do groupingBy, ale dzieli na dwie grupy według warunku boolowskiego.
Map<Boolean, List<String>> namesByPassed = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getGpa() >= 3.0,
Collectors.mapping(Student::getName, Collectors.toList())
));
Dzielimy studentów na zdających i niezdających, wewnątrz każdej grupy — tylko imiona.
2. teeing: jednoczesna agregacja dwoma kolektorami
Czasem trzeba jednocześnie policzyć kilka agregatów dla strumienia: na przykład i sumę, i średnią, albo minimum i maksimum. Do tego w Javie 12+ pojawił się kolektor teeing.
Jak działa teeing?
Przekazujesz dwa kolektory i funkcję, która łączy ich wyniki.
Składnia:
Collectors.teeing(collector1, collector2, (result1, result2) -> ...)
Przykłady
Minimum + maksimum
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
));
Jednocześnie znajdujemy minimum i maksimum.
Suma + średnia
Result result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.averagingInt(Integer::intValue),
(sum, avg) -> new Result(sum, avg)
));
Otrzymujemy obiekt z sumą i średnią wartością.
Przykład: raport płac
SalaryStats stats = employees.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Employee::getSalary),
Collectors.averagingInt(Employee::getSalary),
SalaryStats::new
));
W SalaryStats przechowujemy i sumę, i średnią.
3. toUnmodifiableList/Set/Map i collectingAndThen do „zamrożenia”
W nowoczesnych wersjach Javy pojawiły się kolekcje, których nie można zmienić po utworzeniu — niezmienne (unmodifiable). To wygodne dla API, gdzie ważne jest, by wynik nie mógł zostać przypadkowo zmodyfikowany.
toUnmodifiableList/Set/Map
- Zwracają niezmienną kolekcję.
- Każda próba dodania/usunięcia elementu wywoła UnsupportedOperationException.
Przykłady:
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
Pozwala zastosować funkcję do wyniku kolektora — na przykład „zamrozić” kolekcję.
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
Najpierw zbieramy do zwykłej listy, potem czynimy ją niezmienną.
Przykład z Set:
Set<String> tags = books.stream()
.flatMap(book -> book.getTags().stream())
.collect(Collectors.collectingAndThen(
Collectors.toSet(),
Set::copyOf // Java 10+
));
4. Przypadki pipeline’owe
Raporty i statystyki po przekrojach
Za pomocą zaawansowanych kolektorów można budować złożone raporty i statystyki „w jednej linijce”.
Przykład: średnia pensja według działów
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Przykład: top 3 najdroższych produktów w kategoriach
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()
)
));
Niezmienny wynik jako kontrakt API
Jeśli twoja metoda zwraca kolekcję, której nie można zmieniać, chroni to przed przypadkowymi błędami i czyni API bardziej niezawodnym.
public List<String> getTags() {
return tags.stream()
.collect(Collectors.toUnmodifiableList());
}
Użytkownik nie będzie mógł zrobić getTags().add("nowy tag") — zostanie rzucony wyjątek.
Przykład: raport z kilkoma agregatami (teeing)
public SalaryReport getSalaryReport(List<Employee> employees) {
return employees.stream()
.collect(Collectors.teeing(
Collectors.averagingInt(Employee::getSalary),
Collectors.summingInt(Employee::getSalary),
SalaryReport::new
));
}
W SalaryReport przechowujemy i średnią, i sumę wynagrodzeń.
5. Typowe błędy i niuanse
Błąd nr 1: Zapomniano o niezmienności. Jeśli zwracasz zwykłą listę, ktoś może ją zmienić. Używaj toUnmodifiableList/Set/Map lub collectingAndThen do „zamrożenia” wyniku.
Błąd nr 2: Niewłaściwy kolektor downstream. Jeśli trzeba przekształcić elementy wewnątrz grupy — użyj mapping; jeśli filtrować — filtering; jeśli „rozwinąć” zagnieżdżone kolekcje — flatMapping.
Błąd nr 3: UnsupportedOperationException. Pojawia się przy próbie zmiany kolekcji zebranej przez toUnmodifiableList/Set/Map lub „zamrożonej” przez collectingAndThen.
Błąd nr 4: Utrata unikalności/kolizje kluczy. toUnmodifiableSet wymaga unikalnych elementów, a toUnmodifiableMap — unikalnych kluczy; w przeciwnym razie dostaniesz wyjątek podczas zbierania.
GO TO FULL VERSION