1. groupingBy annidato: sintassi e principio di funzionamento
Nella pratica è raro che basti raggruppare i dati per un solo criterio. Per esempio, se avete un elenco di dipendenti dell’azienda, spesso non basta sapere quanti sono in ogni reparto, ma quanti in ogni reparto per ciascuna posizione. Oppure, se avete un database di studenti – quanti studenti in ogni anno di corso per ciascuna specializzazione.
Le raggruppamenti annidati permettono di costruire queste “mappe dentro mappe” – la struttura “reparto → posizione → elenco di dipendenti”, oppure “anno di corso → specializzazione → elenco di studenti”.
Senza lo Stream API questi compiti richiederebbero più cicli annidati e la costruzione manuale di Map dentro Map. Con gli stream e i collector si fa in una o due righe.
Un raggruppamento annidato è quando, come secondo argomento del metodo Collectors.groupingBy, si passa un altro collector, ad esempio un altro groupingBy. Il risultato è una mappa in cui il valore per ogni chiave è un’altra mappa.
Schema generale
Map<Klyuch1, Map<Klyuch2, List<T>>> result =
stream.collect(Collectors.groupingBy(
obekt -> klyuch1,
Collectors.groupingBy(obekt -> klyuch2)
));
Esempio: dipendenti per reparto e posizione
Supponiamo di avere la classe:
class Employee {
private String name;
private String department;
private String position;
private int salary;
// ... costruttori, getter, toString
}
Elenco dei dipendenti:
List<Employee> employees = List.of(
new Employee("Ivan", "IT", "Sviluppatore", 120_000),
new Employee("Mariya", "IT", "Tester", 90_000),
new Employee("Pyotr", "HR", "Manager", 80_000),
new Employee("Olga", "IT", "Sviluppatore", 130_000),
new Employee("Svetlana", "HR", "Recruiter", 70_000)
);
Raggruppiamo per reparto e posizione:
Map<String, Map<String, List<Employee>>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
Che cosa abbiamo ottenuto?
Una mappa in cui la chiave è il reparto e il valore è una mappa (chiave – posizione, valore – elenco di dipendenti).
Visualizzazione della struttura
IT:
Sviluppatore: [Ivan, Olga]
Tester: [Mariya]
HR:
Manager: [Pyotr]
Recruiter: [Svetlana]
2. Raggruppamento con aggregazione: combiniamo groupingBy e aggregatori
I raggruppamenti annidati non si limitano a raccogliere liste! Si può aggregare subito i dati all’interno di ciascun sottogruppo.
Esempio: stipendio massimo per reparto
Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
));
Qui per ogni reparto otteniamo il dipendente con lo stipendio massimo (il risultato è incapsulato in Optional, perché un reparto potrebbe essere vuoto).
Raggruppamento annidato + aggregazione
Supponiamo di voler conoscere lo stipendio massimo per ciascuna posizione in ogni reparto:
Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
)
));
Che cosa significa?
- Per ogni reparto – una mappa delle posizioni.
- Per ogni posizione – il dipendente con lo stipendio massimo (oppure un Optional vuoto, se non c’è nessuno).
3. Raggruppamento con trasformazione: mapping dentro groupingBy
A volte serve non solo raggruppare gli oggetti, ma ottenere solo determinati campi all’interno dei gruppi.
Esempio: nomi dei dipendenti per reparto
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
Risultato:
IT: [Ivan, Mariya, Olga]
HR: [Pyotr, Svetlana]
mapping annidato
Si può combinare mapping con un groupingBy annidato:
Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.mapping(Employee::getName, Collectors.toList())
)
));
Risultato:
IT:
Sviluppatore: [Ivan, Olga]
Tester: [Mariya]
HR:
Manager: [Pyotr]
Recruiter: [Svetlana]
4. Raggruppamento + aggregazione di dati numerici
Spesso non basta raggruppare, ma bisogna calcolare somma, media o conteggio per gruppi.
Esempio: stipendio medio per reparto
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Aggregazione annidata
Stipendio medio per posizione in ogni reparto:
Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.averagingInt(Employee::getSalary)
)
));
5. partitioningBy + aggregazione
A volte è comodo dividere la collezione in due gruppi secondo un predicato booleano e, all’interno, aggregare.
Esempio: quanti dipendenti con stipendio superiore a 100_000 in ogni reparto
Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.partitioningBy(
e -> e.getSalary() > 100_000,
Collectors.counting()
)
));
Risultato:
Per ogni reparto – una mappa: true/false → numero di dipendenti.
6. Esercizi pratici: applichiamo i raggruppamenti annidati
Esercizio 1. Studenti per anno di corso e specializzazione
class Student {
private String name;
private int course;
private String speciality;
private double grade;
// ... getter, costruttore
}
List<Student> students = ... // supponiamo che esista già
Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(Student::getSpeciality)
));
Esercizio 2. Media dei voti per anno di corso
Map<Integer, Double> avgGradeByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.averagingDouble(Student::getGrade)
));
Esercizio 3. Solo i nomi per gruppi
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. Dettagli utili
Come leggere ed estrarre dati dalle Map annidate
Lavorare con mappe annidate può essere insolito all’inizio. Ecco un esempio di base:
for (var deptEntry : grouped.entrySet()) {
String dept = deptEntry.getKey();
Map<String, List<Employee>> byPosition = deptEntry.getValue();
System.out.println("Reparto: " + dept);
for (var posEntry : byPosition.entrySet()) {
String pos = posEntry.getKey();
List<Employee> emps = posEntry.getValue();
System.out.println(" Posizione: " + pos + " -> " + emps);
}
}
Schema del raggruppamento annidato
Map<Reparto, Map<Posizione, List<Employee>>>
│
├── "IT"
│ ├── "Sviluppatore" → [Ivan, Olga]
│ └── "Tester" → [Mariya]
└── "HR"
├── "Manager" → [Pyotr]
└── "Recruiter" → [Svetlana]
Tabella: cosa si ottiene dopo diverse combinazioni
| Collector | Risultato |
|---|---|
|
|
|
|
|
|
|
|
|
|
8. Errori tipici con i raggruppamenti annidati
Errore n. 1: comprensione errata della struttura delle Map annidate.
Dopo raggruppamenti annidati è facile confondersi su cosa ci sia nel valore di ogni mappa. Guarda sempre la firma del risultato – l’IDE suggerirà il tipo. Se non sei sicuro, stampa il risultato a schermo con System.out.println(grouped) oppure usa il debugger.
Errore n. 2: NullPointerException durante l’estrazione dei dati.
Se la chiave non esiste (ad esempio, in un reparto non ci sono dipendenti di una certa posizione), Map.get(key) restituirà null. Verifica la presenza della chiave con containsKey oppure, a partire da Java 8, puoi usare Map.getOrDefault, e da Java 9 – Map.ofNullable e i metodi di Optional.
Errore n. 3: annidamenti troppo complessi.
Se il raggruppamento diventa troppo profondo (3–4 livelli di annidamento), forse conviene rivedere la struttura dei dati o suddividere il compito in fasi più piccole.
Errore n. 4: aggregazione al livello sbagliato.
Talvolta si mette per errore un collector di aggregazione (averagingInt, counting) non dentro al groupingBy desiderato, ma all’esterno – e si ottiene un risultato inatteso. Fai sempre molta attenzione alle parentesi!
Errore n. 5: tentativo di modificare gli elementi durante collect.
Non modificare le collezioni o gli oggetti di origine durante il raggruppamento – può portare a bug difficili da individuare.
GO TO FULL VERSION