1. groupingBy imbriqué : syntaxe et principe de fonctionnement
Dans la vie réelle, il est rarement suffisant de regrouper les données selon un seul critère. Par exemple, si vous avez une liste d’employés d’une entreprise, on souhaite souvent savoir non seulement combien de personnes sont dans chaque département, mais aussi combien dans chaque département pour chaque poste. Ou, si vous avez une base d’étudiants — combien d’étudiants à chaque année pour chaque spécialité.
Les groupements imbriqués permettent de construire de telles « cartes dans des cartes » — une structure « département → poste → liste d’employés », ou « année → spécialité → liste d’étudiants ».
Sans le Stream API, de telles tâches seraient résolues par plusieurs boucles imbriquées et une construction manuelle d’une Map à l’intérieur d’une autre Map. Avec les streams et les collecteurs, cela se fait en une ou deux lignes.
Un groupement imbriqué consiste à passer en second argument de Collectors.groupingBy un autre collecteur, par exemple un autre groupingBy. Le résultat est une carte où la valeur pour chaque clé est une autre carte.
Modèle général
Map<Key1, Map<Key2, List<T>>> result =
stream.collect(Collectors.groupingBy(
object -> key1,
Collectors.groupingBy(object -> key2)
));
Exemple : employés par département et poste
Supposons que nous ayons une classe :
class Employee {
private String name;
private String department;
private String position;
private int salary;
// ... constructeurs, getters, toString
}
La liste des employés :
List<Employee> employees = List.of(
new Employee("John", "IT", "Développeur", 120_000),
new Employee("Maria", "IT", "Testeur", 90_000),
new Employee("Peter", "HR", "Manager", 80_000),
new Employee("Olga", "IT", "Développeur", 130_000),
new Employee("Sophie", "HR", "Recruteur", 70_000)
);
Regroupons par département et par poste :
Map<String, Map<String, List<Employee>>> grouped = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(Employee::getPosition)
));
Qu’avons-nous obtenu ?
Une carte où la clé est le département, et la valeur est une carte (clé = poste, valeur = liste d’employés).
Visualisation de la structure
IT:
Développeur: [John, Olga]
Testeur: [Maria]
HR:
Manager: [Peter]
Recruteur: [Sophie]
2. Groupement avec agrégation : combiner groupingBy et des agrégateurs
Les groupements imbriqués ne se limitent pas à collecter des listes ! On peut agréger directement les données à l’intérieur de chaque sous-groupe.
Exemple : salaire maximal par département
Map<String, Optional<Employee>> maxSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
));
Ici, pour chaque département, on obtient l’employé au salaire maximal (le résultat est enveloppé dans un Optional, car un département peut être vide).
Groupement imbriqué + agrégation
Supposons que l’on veuille connaître le salaire maximal pour chaque poste dans chaque département :
Map<String, Map<String, Optional<Employee>>> maxSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))
)
));
Qu’est-ce que cela signifie ?
- Pour chaque département — une carte des postes.
- Pour chaque poste — l’employé au salaire maximal (ou un Optional vide s’il n’y a personne).
3. Groupement avec transformation : mapping à l’intérieur de groupingBy
Parfois, il ne faut pas seulement regrouper des objets, mais extraire uniquement certains champs à l’intérieur des groupes.
Exemple : prénoms des employés par département
Map<String, List<String>> namesByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
Résultat :
IT: [John, Maria, Olga]
HR: [Peter, Sophie]
mapping imbriqué
On peut combiner mapping avec un groupingBy imbriqué :
Map<String, Map<String, List<String>>> namesByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.mapping(Employee::getName, Collectors.toList())
)
));
Résultat :
IT:
Développeur: [John, Olga]
Testeur: [Maria]
HR:
Manager: [Peter]
Recruteur: [Sophie]
4. Groupement + agrégation de données numériques
Il est souvent nécessaire non seulement de regrouper, mais aussi de calculer une somme, une moyenne ou un effectif par groupe.
Exemple : salaire moyen par département
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Agrégation imbriquée
Salaire moyen par poste dans chaque département :
Map<String, Map<String, Double>> avgSalaryByDeptAndPos = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getPosition,
Collectors.averagingInt(Employee::getSalary)
)
));
5. partitioningBy + agrégation
Il est parfois pratique de diviser une collection en deux groupes selon un prédicat booléen et, à l’intérieur, d’agréger également.
Exemple : combien d’employés avec un salaire supérieur à 100_000 dans chaque département
Map<String, Map<Boolean, Long>> countByDeptAndSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.partitioningBy(
e -> e.getSalary() > 100_000,
Collectors.counting()
)
));
Résultat :
Pour chaque département — une carte : true/false → nombre d’employés.
6. Problèmes pratiques : appliquer les groupements imbriqués
Problème 1. Étudiants par année et spécialité
class Student {
private String name;
private int course;
private String speciality;
private double grade;
// ... getters, constructeur
}
List<Student> students = ... // supposons que la liste existe déjà
Map<Integer, Map<String, List<Student>>> byCourseAndSpec = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.groupingBy(Student::getSpeciality)
));
Problème 2. Moyenne des notes par année
Map<Integer, Double> avgGradeByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.averagingDouble(Student::getGrade)
));
Problème 3. Uniquement les prénoms par groupes
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. Points utiles
Comment lire et extraire des données des Map imbriquées
Travailler avec des cartes imbriquées peut être inhabituel au début. Voici un exemple de base :
for (var deptEntry : grouped.entrySet()) {
String dept = deptEntry.getKey();
Map<String, List<Employee>> byPosition = deptEntry.getValue();
System.out.println("Département : " + dept);
for (var posEntry : byPosition.entrySet()) {
String pos = posEntry.getKey();
List<Employee> emps = posEntry.getValue();
System.out.println(" Poste : " + pos + " -> " + emps);
}
}
Schéma de groupement imbriqué
Map<Departement, Map<Poste, List<Employee>>>
│
├── "IT"
│ ├── "Développeur" → [John, Olga]
│ └── "Testeur" → [Maria]
└── "HR"
├── "Manager" → [Peter]
└── "Recruteur" → [Sophie]
Tableau : ce que donnent différentes combinaisons
| Collecteur | Résultat |
|---|---|
|
|
|
|
|
|
|
|
|
|
8. Erreurs typiques avec les groupements imbriqués
Erreur n° 1 : mauvaise compréhension de la structure des Map imbriquées.
Après des groupements imbriqués, il est facile de se perdre sur ce qui se trouve exactement dans la valeur de chaque carte. Regardez toujours la signature du résultat — l’IDE indiquera le type. En cas de doute, affichez le résultat à l’écran avec System.out.println(grouped) ou utilisez le débogueur.
Erreur n° 2 : NullPointerException lors de l’extraction des données.
Si la clé est absente (par exemple, aucun employé d’un poste donné dans un département), Map.get(key) renverra null. Vérifiez la présence de la clé via containsKey ou, depuis Java 8, utilisez Map.getOrDefault, et depuis Java 9 — Map.ofNullable et les méthodes d’Optional.
Erreur n° 3 : imbrications trop complexes.
Si le groupement devient trop profond (3–4 niveaux d’imbrication), il peut être pertinent de repenser la structure des données ou de découper la tâche en étapes plus petites.
Erreur n° 4 : agrégation au mauvais niveau.
Il arrive que l’on place par erreur un collecteur d’agrégation (averagingInt, counting) non pas à l’intérieur du groupingBy approprié, mais à l’extérieur — et l’on obtient un résultat inattendu. Placez toujours les parenthèses avec soin !
Erreur n° 5 : tenter de modifier des éléments pendant le collect.
Ne modifiez pas les collections ou objets sources pendant le groupement — cela peut entraîner des bogues difficiles à détecter.
GO TO FULL VERSION