1. Zagnieżdżone groupingBy: składnia i zasada działania
W praktyce rzadko wystarcza pogrupować dane tylko według jednego kryterium. Na przykład, jeśli masz listę pracowników firmy, to często chcesz wiedzieć nie tylko, ile osób jest w każdym dziale, ale ile jest w każdym dziale na każdym stanowisku. Albo, jeśli masz bazę studentów — ilu studentów jest na każdym roku w każdej specjalności.
Zagnieżdżone grupowania pozwalają budować takie „mapy w mapach” — strukturę „dział → stanowisko → lista pracowników” lub „rok → specjalność → lista studentów”.
Bez Stream API takie zadania byłyby rozwiązywane kilkoma zagnieżdżonymi pętlami i ręczną budową Map wewnątrz Map. Z strumieniami i kolektorami robi się to w jednym–dwóch wierszach.
Zagnieżdżone grupowanie polega na tym, że jako drugi argument metody Collectors.groupingBy przekazujesz kolejny kolektor, na przykład kolejne groupingBy. W rezultacie otrzymujesz mapę, w której wartością dla każdego klucza będzie kolejna mapa.
Ogólny wzorzec
Map<Klucz1, Map<Klucz2, List<T>>> result =
stream.collect(Collectors.groupingBy(
obiekt -> klucz1,
Collectors.groupingBy(obiekt -> klucz2)
));
Przykład: pracownicy według działu i stanowiska
Załóżmy, że mamy klasę:
class Employee {
private String name;
private String department;
private String position;
private int salary;
// ... konstruktory, gettery, toString
}
Lista pracowników:
List<Employee> employees = List.of(
new Employee("Ivan", "IT", "Programista", 120_000),
new Employee("Mariya", "IT", "Tester", 90_000),
new Employee("Pyotr", "HR", "Menedżer", 80_000),
new Employee("Olga", "IT", "Programista", 130_000),
new Employee("Svetlana", "HR", "Rekruter", 70_000)
);
Grupujemy według działu i stanowiska:
Map<String, Map<String, List<Employee>>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
Co otrzymaliśmy?
Mapa, w której kluczem jest dział, a wartością — mapa (kluczem jest stanowisko, wartością — lista pracowników).
Wizualizacja struktury
IT:
Programista: [Ivan, Olga]
Tester: [Mariya]
HR:
Menedżer: [Pyotr]
Rekruter: [Svetlana]
2. Grupowanie z agregacją: łączymy groupingBy i agregatory
Zagnieżdżone grupowania nie ograniczają się do zbierania list! Można od razu agregować dane wewnątrz każdej podgrupy.
Przykład: maksymalne wynagrodzenie w każdym dziale
Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
));
Tutaj dla każdego działu otrzymujemy pracownika z maksymalnym wynagrodzeniem (wynik jest opakowany w Optional, ponieważ dział może okazać się pusty).
Zagnieżdżone grupowanie + agregacja
Załóżmy, że chcemy znać maksymalne wynagrodzenie na każdym stanowisku w każdym dziale:
Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
)
));
Co to oznacza?
- Dla każdego działu — mapa stanowisk.
- Dla każdego stanowiska — pracownik z maksymalnym wynagrodzeniem (albo pusty Optional, jeśli nikogo nie ma).
3. Grupowanie z przekształceniem: mapping wewnątrz groupingBy
Czasem trzeba nie tylko pogrupować obiekty, ale uzyskać w grupach wyłącznie określone pola.
Przykład: imiona pracowników w działach
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
Wynik:
IT: [Ivan, Mariya, Olga]
HR: [Pyotr, Svetlana]
Zagnieżdżone mapping
Można łączyć mapping z zagnieżdżonym groupingBy:
Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.mapping(Employee::getName, Collectors.toList())
)
));
Wynik:
IT:
Programista: [Ivan, Olga]
Tester: [Mariya]
HR:
Menedżer: [Pyotr]
Rekruter: [Svetlana]
4. Grupowanie + agregacja danych liczbowych
Często potrzebne jest nie tylko grupowanie, ale też policzenie sumy, średniej lub liczności w grupach.
Przykład: średnie wynagrodzenie w dziale
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Zagnieżdżona agregacja
Średnie wynagrodzenie na stanowisku w każdym dziale:
Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.averagingInt(Employee::getSalary)
)
));
5. partitioningBy + agregacja
Czasem wygodnie jest podzielić kolekcję na dwie grupy według predykatu boolowskiego, a w środku — także agregować.
Przykład: ilu pracowników z wynagrodzeniem powyżej 100_000 w każdym dziale
Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.partitioningBy(
e -> e.getSalary() > 100_000,
Collectors.counting()
)
));
Wynik:
Dla każdego działu — mapa: true/false → liczba pracowników.
6. Zadania praktyczne: stosujemy zagnieżdżone grupowania
Zadanie 1. Studenci według roku i specjalności
class Student {
private String name;
private int course;
private String speciality;
private double grade;
// ... gettery, konstruktor
}
List<Student> students = ... // załóżmy, że już jest
Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(Student::getSpeciality)
));
Zadanie 2. Średnia ocena na roku
Map<Integer, Double> avgGradeByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.averagingDouble(Student::getGrade)
));
Zadanie 3. Tylko imiona w grupach
Map<Integer, Map<String, List<String>>> namesByCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(
Student::getSpeciality,
Collectors.mapping(Student::getName, Collectors.toList())
)
));
7. Przydatne niuanse
Jak czytać i wyciągać dane z zagnieżdżonych Map
Praca z zagnieżdżonymi mapami na początku może być nietypowa. Oto podstawowy przykład:
for (var deptEntry : grouped.entrySet()) {
String dept = deptEntry.getKey();
Map<String, List<Employee>> byPosition = deptEntry.getValue();
System.out.println("Dział: " + dept);
for (var posEntry : byPosition.entrySet()) {
String pos = posEntry.getKey();
List<Employee> emps = posEntry.getValue();
System.out.println(" Stanowisko: " + pos + " -> " + emps);
}
}
Schemat zagnieżdżonego grupowania
Map<Dział, Map<Stanowisko, List<Employee>>>
│
├── "IT"
│ ├── "Programista" → [Ivan, Olga]
│ └── "Tester" → [Mariya]
└── "HR"
├── "Menedżer" → [Pyotr]
└── "Rekruter" → [Svetlana]
Tabela: co otrzymamy po różnych kombinacjach
| Kolektor | Wynik |
|---|---|
|
|
|
|
|
|
|
|
|
|
8. Typowe błędy przy pracy z zagnieżdżonymi grupowaniami
Błąd nr 1: Nieprawidłowe rozumienie struktury zagnieżdżonych Map.
Po zagnieżdżonych grupowaniach łatwo się pogubić, co dokładnie leży w wartości każdej mapy. Zawsze patrz na sygnaturę wyniku — IDE podpowie typ. Jeśli nie masz pewności, wypisz wynik na ekran za pomocą System.out.println(grouped) lub użyj debuggera.
Błąd nr 2: NullPointerException przy pobieraniu danych.
Jeśli klucza nie ma (np. w dziale nie ma pracowników danego stanowiska), Map.get(key) zwróci null. Sprawdzaj obecność klucza przez containsKey lub, od Java 8, możesz użyć Map.getOrDefault, a od Java 9 — Map.ofNullable i metod Optional.
Błąd nr 3: Zbyt głębokie zagnieżdżenia.
Jeśli grupowanie staje się zbyt głębokie (3–4 poziomy zagnieżdżenia), być może warto przemyśleć strukturę danych lub podzielić zadanie na mniejsze etapy.
Błąd nr 4: Agregacja na niewłaściwym poziomie.
Czasem błędnie umieszcza się agregujący kolektor (averagingInt, counting) nie wewnątrz właściwego groupingBy, lecz na zewnątrz — i pojawia się nieoczekiwany wynik. Zawsze uważnie stawiaj nawiasy!
Błąd nr 5: Próba modyfikacji elementów podczas collect.
Nie modyfikuj źródłowych kolekcji ani obiektów w trakcie grupowania — może to prowadzić do trudnych do wykrycia błędów.
GO TO FULL VERSION