1. Verschachteltes groupingBy: Syntax und Funktionsprinzip
Im echten Leben reicht es selten, Daten nur nach einem Merkmal zu gruppieren. Wenn Sie zum Beispiel eine Liste von Mitarbeitenden eines Unternehmens haben, möchten Sie oft nicht nur wissen, wie viele Personen es in jeder Abteilung gibt, sondern wie viele in jeder Abteilung auf jeder Position arbeiten. Oder, wenn Sie eine Studierenden-Datenbank haben — wie viele Studierende es in jedem Studienjahr pro Fachrichtung gibt.
Verschachtelte Gruppierungen ermöglichen solche „Karten in Karten“ — eine Struktur „Abteilung → Position → Liste der Mitarbeitenden“ oder „Kurs → Fachrichtung → Liste der Studierenden“.
Ohne Stream API würden solche Aufgaben mit mehreren verschachtelten Schleifen und dem manuellen Aufbau einer Map innerhalb einer Map gelöst. Mit Streams und Collectors geht das in ein bis zwei Zeilen.
Eine verschachtelte Gruppierung bedeutet, dass Sie als zweiten Parameter der Methode Collectors.groupingBy einen weiteren Collector übergeben, zum Beispiel ein weiteres groupingBy. Das Ergebnis ist eine Map, in der der Wert für jeden Schlüssel wiederum eine Map ist.
Allgemeines Muster
Map<Key1, Map<Key2, List<T>>> result =
stream.collect(Collectors.groupingBy(
object -> key1,
Collectors.groupingBy(object -> key2)
));
Beispiel: Mitarbeitende nach Abteilung und Position
Angenommen, wir haben eine Klasse:
class Employee {
private String name;
private String department;
private String position;
private int salary;
// ... Konstruktoren, Getter, toString
}
Liste der Mitarbeitenden:
List<Employee> employees = List.of(
new Employee("Ivan", "IT", "Entwickler", 120_000),
new Employee("Mariya", "IT", "Tester", 90_000),
new Employee("Pyotr", "HR", "Manager", 80_000),
new Employee("Ol'ga", "IT", "Entwickler", 130_000),
new Employee("Svetlana", "HR", "Recruiter", 70_000)
);
Gruppieren nach Abteilung und Position:
Map<String, Map<String, List<Employee>>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
Was haben wir erhalten?
Eine Map, in der der Schlüssel die Abteilung ist und der Wert eine Map (Schlüssel — Position, Wert — Liste der Mitarbeitenden).
Struktur visualisieren
IT:
Entwickler: [Ivan, Ol'ga]
Tester: [Mariya]
HR:
Manager: [Pyotr]
Recruiter: [Svetlana]
2. Gruppierung mit Aggregation: groupingBy und Aggregatoren kombinieren
Verschachtelte Gruppierungen beschränken sich nicht auf das Einsammeln von Listen! Man kann die Daten innerhalb jeder Untergruppe direkt aggregieren.
Beispiel: maximales Gehalt pro Abteilung
Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
));
Hier erhalten wir für jede Abteilung die Person mit dem höchsten Gehalt (das Ergebnis ist in ein Optional verpackt, weil eine Abteilung leer sein kann).
Verschachtelte Gruppierung + Aggregation
Angenommen, wir möchten das maximale Gehalt für jede Position in jeder Abteilung wissen:
Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
)
));
Was bedeutet das?
- Für jede Abteilung — eine Map der Positionen.
- Für jede Position — die Person mit dem höchsten Gehalt (oder ein leeres Optional, wenn niemand vorhanden ist).
3. Gruppierung mit Transformation: mapping innerhalb von groupingBy
Manchmal muss man nicht nur Objekte gruppieren, sondern innerhalb der Gruppen nur bestimmte Felder erhalten.
Beispiel: Namen der Mitarbeitenden nach Abteilungen
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
Ergebnis:
IT: [Ivan, Mariya, Ol'ga]
HR: [Pyotr, Svetlana]
Verschachteltes mapping
Man kann mapping mit einem verschachtelten groupingBy kombinieren:
Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.mapping(Employee::getName, Collectors.toList())
)
));
Ergebnis:
IT:
Entwickler: [Ivan, Ol'ga]
Tester: [Mariya]
HR:
Manager: [Pyotr]
Recruiter: [Svetlana]
4. Gruppierung + Aggregation numerischer Daten
Häufig möchte man nicht nur gruppieren, sondern pro Gruppe Summe, Durchschnitt oder Anzahl berechnen.
Beispiel: durchschnittliches Gehalt pro Abteilung
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Verschachtelte Aggregation
Durchschnittsgehalt pro Position in jeder Abteilung:
Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.averagingInt(Employee::getSalary)
)
));
5. partitioningBy + Aggregation
Manchmal ist es praktisch, die Sammlung nach einem booleschen Kriterium in zwei Gruppen zu teilen und innerhalb dieser Gruppen ebenfalls zu aggregieren.
Beispiel: Wie viele Mitarbeitende mit einem Gehalt über 100_000 pro Abteilung
Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.partitioningBy(
e -> e.getSalary() > 100_000,
Collectors.counting()
)
));
Ergebnis:
Für jede Abteilung — eine Map: true/false → Anzahl der Mitarbeitenden.
6. Praktische Aufgaben: verschachtelte Gruppierungen anwenden
Aufgabe 1. Studierende nach Kurs und Fachrichtung
class Student {
private String name;
private int course;
private String speciality;
private double grade;
// ... Getter, Konstruktor
}
List<Student> students = ... // nehmen wir an, schon vorhanden
Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(Student::getSpeciality)
));
Aufgabe 2. Durchschnittsnote pro Kurs
Map<Integer, Double> avgGradeByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.averagingDouble(Student::getGrade)
));
Aufgabe 3. Nur Namen nach Gruppen
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. Nützliche Nuancen
Wie man verschachtelte Maps liest und Daten daraus extrahiert
Mit verschachtelten Maps zu arbeiten, kann anfangs ungewohnt sein. Ein einfaches Beispiel:
for (var deptEntry : grouped.entrySet()) {
String dept = deptEntry.getKey();
Map<String, List<Employee>> byPosition = deptEntry.getValue();
System.out.println("Abteilung: " + dept);
for (var posEntry : byPosition.entrySet()) {
String pos = posEntry.getKey();
List<Employee> emps = posEntry.getValue();
System.out.println(" Position: " + pos + " -> " + emps);
}
}
Schematische Darstellung der verschachtelten Gruppierung
Map<Abteilung, Map<Position, List<Employee>>>
│
├── "IT"
│ ├── "Entwickler" → [Ivan, Ol'ga]
│ └── "Tester" → [Mariya]
└── "HR"
├── "Manager" → [Pyotr]
└── "Recruiter" → [Svetlana]
Tabelle: Was bei verschiedenen Kombinationen herauskommt
| Collector | Ergebnis |
|---|---|
|
|
|
|
|
|
|
|
|
|
8. Typische Fehler bei der Arbeit mit verschachtelten Gruppierungen
Fehler Nr. 1: Falsches Verständnis der Struktur verschachtelter Maps.
Nach verschachtelten Gruppierungen kommt man leicht durcheinander, was genau im Wert jeder Map liegt. Schauen Sie immer auf die Signatur des Ergebnisses — die IDE zeigt den Typ an. Wenn Sie unsicher sind, geben Sie das Ergebnis mit System.out.println(grouped) auf dem Bildschirm aus oder verwenden Sie den Debugger.
Fehler Nr. 2: NullPointerException beim Auslesen von Daten.
Wenn es den Schlüssel nicht gibt (zum Beispiel gibt es in der Abteilung keine Mitarbeitenden einer bestimmten Position), liefert Map.get(key) null. Prüfen Sie die Existenz des Schlüssels mit containsKey oder — seit Java 8 — verwenden Sie Map.getOrDefault, und seit Java 9 — Map.ofNullable und die Methoden von Optional.
Fehler Nr. 3: Zu tiefe Verschachtelung.
Wenn die Gruppierung zu tief wird (3–4 Ebenen), sollte man eventuell die Datenstruktur überdenken oder die Aufgabe in kleinere Schritte aufteilen.
Fehler Nr. 4: Aggregation auf der falschen Ebene.
Manchmal wird fälschlicherweise ein aggregierender Collector (averagingInt, counting) nicht in den benötigten groupingBy gesetzt, sondern außen — und man erhält ein unerwartetes Ergebnis. Achten Sie immer sorgfältig auf die Klammern!
Fehler Nr. 5: Versuch, Elemente innerhalb von collect zu ändern.
Ändern Sie weder die Ausgangskollektionen noch die Objekte während der Gruppierung — das kann zu schwer zu findenden Bugs führen.
GO TO FULL VERSION