CodeGym /Kursy /JAVA 25 SELF /Zaawansowane kolektory

Zaawansowane kolektory

JAVA 25 SELF
Poziom 31 , Lekcja 4
Dostępny

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.

1
Ankieta/quiz
Grupowanie i agregacja, poziom 31, lekcja 4
Niedostępny
Grupowanie i agregacja
Stream API: grupowanie i agregacja
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION