1. Introduction
Lorsque vous travaillez avec des collections via le Stream API, il ne suffit pas souvent de simplement regrouper les éléments : vous voulez aussi agir immédiatement dessus : les transformer, les filtrer, les collecter dans un autre type de collection. Pour cela, Java propose des collecteurs downstream — des collecteurs imbriqués appliqués à chaque groupe ou partie de données.
Qu’est-ce qu’un collecteur downstream ?
C’est un collecteur qui s’applique au résultat d’un regroupement ou d’un partitionnement. Par exemple, vous pouvez regrouper des étudiants par cours avec groupingBy, et à l’intérieur de chaque groupe ne collecter que les noms ou seulement les étudiants d’excellence (par exemple, avec un seuil 4.5 de GPA).
Exemples
mapping
Permet de transformer les éléments du groupe avant la collecte.
Map<Integer, List<String>> namesByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.mapping(Student::getName, Collectors.toList())
));
- Nous regroupons les étudiants par cours.
- Pour chaque groupe, nous ne collectons que les noms (pas les objets entiers).
filtering
Permet de filtrer les éléments à l’intérieur du groupe (par exemple, ne garder que les GPA >= 4.5).
Map<Integer, List<Student>> honorsByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
));
Dans chaque groupe, nous ne gardons que les étudiants d’excellence.
flatMapping
Permet « d’aplatir » les collections imbriquées à l’intérieur des groupes.
Map<String, Set<String>> tagsByAuthor = books.stream()
.collect(Collectors.groupingBy(
Book::getAuthor,
Collectors.flatMapping(
book -> book.getTags().stream(),
Collectors.toSet()
)
));
Pour chaque auteur, nous collectons les tags uniques de tous ses livres.
partitioningBy с downstream
Fonctionne comme groupingBy, mais divise en deux groupes selon une condition booléenne.
Map<Boolean, List<String>> namesByPassed = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getGpa() >= 3.0,
Collectors.mapping(Student::getName, Collectors.toList())
));
Nous séparons les étudiants ayant validé et ceux n’ayant pas validé ; à l’intérieur de chaque groupe, seulement les noms.
2. teeing : agrégation simultanée par deux collecteurs
Il arrive que l’on doive calculer plusieurs agrégats en parallèle sur un flux : par exemple la somme et la moyenne, ou le minimum et le maximum. Pour cela, à partir de Java 12+ est apparu le collecteur teeing.
Comment fonctionne teeing ?
Vous passez deux collecteurs et une fonction qui fusionne leurs résultats.
Syntaxe :
Collectors.teeing(collector1, collector2, (result1, result2) -> ...)
Exemples
Minimum + maximum
Optional<MinMax> minMax = numbers.stream()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
));
Nous trouvons simultanément le minimum et le maximum.
Somme + moyenne
Result result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.averagingInt(Integer::intValue),
(sum, avg) -> new Result(sum, avg)
));
Nous obtenons un objet contenant la somme et la moyenne.
Exemple : rapport sur les salaires
SalaryStats stats = employees.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Employee::getSalary),
Collectors.averagingInt(Employee::getSalary),
SalaryStats::new
));
Dans SalaryStats, on stocke à la fois la somme et la moyenne.
3. toUnmodifiableList/Set/Map et collectingAndThen pour le « gel »
Dans les versions modernes de Java, il existe des collections qui ne peuvent pas être modifiées après leur création — immuables (unmodifiable). C’est pratique pour les API où il est important que le résultat ne puisse pas être modifié par inadvertance.
toUnmodifiableList/Set/Map
- Renvoient une collection immuable.
- Toute tentative d’ajouter/supprimer un élément lèvera une UnsupportedOperationException.
Exemples :
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
.collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));
collectingAndThen
Permet d’appliquer une fonction au résultat d’un collecteur — par exemple, « geler » la collection.
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
Nous collectons d’abord dans une liste ordinaire, puis nous la rendons immuable.
Exemple avec Set :
Set<String> tags = books.stream()
.flatMap(book -> book.getTags().stream())
.collect(Collectors.collectingAndThen(
Collectors.toSet(),
Set::copyOf // Java 10+
));
4. Cas de pipelines
Rapports et statistiques par segments
Avec des collecteurs avancés, on peut construire des rapports et des statistiques complexes « en une seule ligne ».
Exemple : salaire moyen par département
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
Exemple : top 3 des produits les plus chers par catégorie
Map<String, List<Product>> top3ByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.limit(3)
.toList()
)
));
Résultat immuable comme contrat d’API
Si votre méthode renvoie une collection non modifiable, cela évite des erreurs accidentelles et rend l’API plus robuste.
public List<String> getTags() {
return tags.stream()
.collect(Collectors.toUnmodifiableList());
}
L’utilisateur ne pourra pas faire getTags().add("nouveau tag") — une exception sera levée.
Exemple : rapport avec plusieurs agrégats (teeing)
public SalaryReport getSalaryReport(List<Employee> employees) {
return employees.stream()
.collect(Collectors.teeing(
Collectors.averagingInt(Employee::getSalary),
Collectors.summingInt(Employee::getSalary),
SalaryReport::new
));
}
Dans SalaryReport, on stocke à la fois le salaire moyen et le salaire total.
5. Erreurs courantes et subtilités
Erreur n° 1 : oubli de l’immuabilité. Si vous renvoyez une liste ordinaire, elle peut être modifiée. Utilisez toUnmodifiableList/Set/Map ou collectingAndThen pour « geler » le résultat.
Erreur n° 2 : mauvais collecteur downstream. S’il faut transformer les éléments à l’intérieur du groupe — utilisez mapping ; s’il faut filtrer — filtering ; s’il faut aplatir des collections imbriquées — flatMapping.
Erreur n° 3 : UnsupportedOperationException. Survient lors d’une tentative de modification d’une collection collectée via toUnmodifiableList/Set/Map ou « gelée » via collectingAndThen.
Erreur n° 4 : perte d’unicité/collisions de clés. toUnmodifiableSet exige des éléments uniques, et toUnmodifiableMap — des clés uniques ; sinon vous obtiendrez une exception lors de la collecte.
GO TO FULL VERSION