1. Introduction
Imaginons que nous ayons une liste d’étudiants, et que chaque étudiant possède sa propre liste de loisirs. Par exemple :
List<List<String>> hobbies = List.of(
List.of("Natation", "Échecs"),
List.of("Football"),
List.of("Programmation", "Lecture", "Cinéma")
);
Votre tâche : obtenir une liste unifiée de tous les loisirs afin de savoir ce qui intéresse globalement les étudiants. Il est logique d’essayer d’utiliser map :
List<Stream<String>> streams = hobbies.stream()
.map(list -> list.stream())
.collect(Collectors.toList());
Qu’obtient-on ? Une liste de flux ! Autrement dit, un Stream<Stream<String>>. Ce n’est pas exactement ce qu’il nous faut — nous aimerions simplement obtenir un Stream<String>, pour traiter chaque loisir directement.
Imaginez que vous avez une boîte contenant d’autres boîtes de jouets. La méthode map se contente de sortir chaque boîte et de vous montrer la boîte (Stream<Stream<String>>). Mais vous voudriez voir immédiatement tous les jouets (Stream<String>) sans fouiller dans les boîtes.
2. flatMap : comment « déplier » des collections imbriquées
Pour « déballer les boîtes » et obtenir un flux plat, il faut la méthode flatMap.
Elle prend une fonction qui, pour chaque élément, renvoie un flux, et « aplatit » immédiatement tous les flux imbriqués en un seul.
Syntaxe de flatMap
Stream<T> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
En bref : flatMap s’attend à ce que vous renvoyiez un flux (Stream) pour chaque élément, et il « lisse » tout cela en un seul grand flux.
Exemple : réunir tous les loisirs des étudiants
List<String> allHobbies = hobbies.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Natation, Échecs, Football, Programmation, Lecture, Cinéma]
Pour chaque liste de loisirs, nous avons appelé list.stream() (nous avons obtenu le flux des loisirs d’un étudiant). Puis flatMap a « aplati » tous les flux en un seul grand flux — nous voyons désormais tous les loisirs, et non des flux de loisirs.
Schéma visuel
hobbies.stream()
|
|---> [Natation, Échecs] -> stream
|---> [Football] -> stream
|---> [Programmation, ...] -> stream
|
flatMap: regroupe tout en un seul Stream<String>
Pourquoi map ne résout-il pas le problème ?
Si nous utilisions map, nous obtiendrions un Stream<Stream<String>>, ce qui est peu pratique : par exemple, on ne peut pas itérer immédiatement sur toutes les chaînes, il faut effectuer des parcours supplémentaires.
3. Exemples pratiques d’utilisation de flatMap
Découper des chaînes en caractères (List<String> → Stream<Character>)
Supposons que vous ayez une liste de chaînes et que vous vouliez obtenir le flux de tous les caractères apparaissant dans toutes les chaînes :
List<String> words = List.of("Java", "Stream");
List<Character> characters = words.stream()
.flatMap(word -> word.chars().mapToObj(ch -> (char) ch))
.collect(Collectors.toList());
System.out.println(characters);
// [J, a, v, a, S, t, r, e, a, m]
Explication :
- word.chars() renvoie un IntStream (flux de codes de caractères).
- mapToObj convertit les codes en caractères.
- flatMap fusionne tous les flux de caractères obtenus en un seul.
Travailler avec Optional : Stream<Optional<T>> → Stream<T>
Supposons que vous ayez une liste d’objets Optional, et que vous vouliez obtenir un flux ne contenant que les valeurs effectivement présentes :
List<Optional<String>> optionals = List.of(
Optional.of("Java"),
Optional.empty(),
Optional.of("Stream")
);
List<String> present = optionals.stream()
.flatMap(opt -> opt.stream())
.collect(Collectors.toList());
System.out.println(present);
// [Java, Stream]
Astuce :
Optional<T> (depuis Java 9) possède une méthode stream(), qui renvoie soit un flux vide, soit un flux d’un seul élément. flatMap regroupe toutes les valeurs non vides en un seul flux.
4. La nouvelle méthode mapMulti : quand flatMap n’est pas nécessaire
Pourquoi mapMulti est-il apparu ?
Introduite dans Java 16, la méthode mapMulti fonctionne de manière similaire à flatMap, mais elle est un peu plus efficace et plus flexible.
flatMap est un outil puissant, mais il a un inconvénient : pour chaque élément, vous êtes obligé de créer un nouveau Stream, même si vous voulez retourner 0, 1 ou plusieurs éléments. Cela peut être inefficace, surtout si vous voulez simplement « déplier » des éléments sans créer de collections ou de flux intermédiaires.
mapMulti est une version améliorée de flatMap, qui permet d’ajouter directement les valeurs nécessaires au flux résultant via un Consumer, sans créer de structures intermédiaires. Au lieu de renvoyer un flux, vous indiquez directement quels éléments ajouter au flux résultant via le Consumer.
Syntaxe de mapMulti
<R> Stream<R> mapMulti(BiConsumer<? super T, ? super Consumer<R>> mapper)
Pour chaque élément, on appelle la fonction mapper, qui reçoit l’élément lui‑même et un Consumer dans lequel on peut « empiler » de nouvelles valeurs.
Exemple : filtrer et déplier des éléments en un seul passage
Supposons que vous ayez une liste de nombres, et que vous vouliez ajouter les nombres pairs au flux deux fois, et les impairs — pas du tout (c’est‑à‑dire, filtrage et « dépliage » simultanés) :
List<Integer> numbers = List.of(1, 2, 3, 4);
List<Integer> result = numbers.stream()
.mapMulti((number, consumer) -> {
if (number % 2 == 0) {
consumer.accept(number);
consumer.accept(number); // Ajouté deux fois
}
// Pour les impairs, on ne fait rien (filtrage)
})
.collect(Collectors.toList());
System.out.println(result);
// [2, 2, 4, 4]
Comparaison avec flatMap
La même tâche avec flatMap s’écrirait ainsi :
List<Integer> result = numbers.stream()
.flatMap(number -> number % 2 == 0
? Stream.of(number, number)
: Stream.empty())
.collect(Collectors.toList());
Ici, nous sommes tout de même obligés de créer Stream.of(...) ou Stream.empty() pour chaque élément, même si cela est inefficace.
5. Quand utiliser flatMap, et quand mapMulti ?
- map — lorsque vous transformez chaque élément en un seul élément.
- flatMap — lorsque vous transformez chaque élément en un flux d’éléments.
- mapMulti — lorsque vous voulez générer plusieurs éléments sans créer de flux intermédiaire (plus efficace, notamment dans les boucles chaudes).
Autre exemple : déplier Map<Integer, List<String>> en Stream<Pair(Integer, String)>
Supposons que nous ayons une map : id → liste de loisirs.
Map<Integer, List<String>> studentHobbies = Map.of(
1, List.of("Natation", "Échecs"),
2, List.of("Football"),
3, List.of("Programmation", "Lecture", "Cinéma")
);
List<String> allHobbies = studentHobbies.values().stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(allHobbies);
// [Natation, Échecs, Football, Programmation, Lecture, Cinéma]
Et si nous voulons obtenir des paires (id, loisir) :
List<String> pairs = studentHobbies.entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(hobby -> entry.getKey() + ": " + hobby))
.collect(Collectors.toList());
System.out.println(pairs);
// [1: Natation, 1: Échecs, 2: Football, 3: Programmation, 3: Lecture, 3: Cinéma]
Avec mapMulti (Java 16+) :
List<String> pairs = studentHobbies.entrySet().stream()
.mapMulti((entry, consumer) -> {
for (String hobby : entry.getValue()) {
consumer.accept(entry.getKey() + ": " + hobby);
}
})
.collect(Collectors.toList());
Avantage :
mapMulti ne crée pas de flux intermédiaires, il « pousse » simplement les valeurs directement dans le flux final.
Visualisation : flatMap et mapMulti
| Méthode | Ce que renvoie la fonction | Comment il combine | Collections intermédiaires ? |
|---|---|---|---|
|
Un seul élément | Simplement | Non |
|
Un flux (Stream) | Fusionne | Oui (Stream intermédiaires) |
|
Consumer (0..n fois) | Fusionne | Non (ajoute directement) |
6. Erreurs typiques avec flatMap et mapMulti
Erreur n° 1 : vous avez obtenu Stream<Stream<T>> au lieu de Stream<T>. Il arrive souvent que l’on utilise map au lieu de flatMap lorsqu’on manipule des collections de collections. On se retrouve alors à écrire des boucles supplémentaires.
Erreur n° 2 : type de retour incorrect. Pour flatMap, la fonction doit renvoyer un Stream, et non une List ou un tableau.
Erreur n° 3 : inefficacité. Dans les cas simples, flatMap fonctionne très bien, mais si vous devez créer Stream.of ou Stream.empty() pour chaque élément, cela peut être excessif. Pour ces tâches, il vaut mieux utiliser mapMulti.
Erreur n° 4 : mapMulti ne fonctionne pas dans les anciennes versions de Java. mapMulti n’a été introduit qu’à partir de Java 16. Si votre version du JDK est plus ancienne, cette méthode ne sera pas disponible.
Erreur n° 5 : problèmes avec null. Ne retournez pas null depuis flatMap — retournez toujours Stream.empty() pour les cas « vides ».
Erreur n° 6 : collections intermédiaires. N’ajoutez pas de listes ou de flux superflus si vous pouvez pousser les éléments directement via le consumer dans mapMulti.
GO TO FULL VERSION