CodeGym /Kurse /JAVA 25 SELF /Fortgeschrittene Aggregation: verschachtelte Gruppierunge...

Fortgeschrittene Aggregation: verschachtelte Gruppierungen

JAVA 25 SELF
Level 31 , Lektion 3
Verfügbar

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
groupingBy(Employee::getDepartment)
Map<String, List<Employee>>
groupingBy(Employee::getDepartment, averagingInt(...))
Map<String, Double>
groupingBy(Employee::getDepartment, groupingBy(...))
Map<String, Map<String, List<Employee>>>
groupingBy(..., mapping(..., toList()))
Map<..., List<...>>
groupingBy(..., groupingBy(..., mapping(..., toList())))
Map<..., Map<..., List<...>>>

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.

Kommentare (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
10 April 2026
Fehler Nr. 2: NullPointerException beim Auslesen von Daten. Map.ofNullable ❌ Gibt es nicht solche Methode! Optional.ofNullable ✅ Ja, es ist vorhanden.