Quelle est la différence entre map() et flatMap() en Java 8 ?
Depuis Java 8, les Streams offrent deux méthodes de transformation très proches mais distinctes : map() et flatMap(). Savoir choisir la bonne est essentiel pour éviter les erreurs de typage et les Stream<Stream<T>> imbriqués.
La différence en une phrase
map(Function<T, R>)transforme chaque élément en un autre élément.flatMap(Function<T, Stream<R>>)transforme chaque élément en un Stream, puis aplatit tous ces Streams en un seul.
Exemple avec map()
Transformer une liste de chaînes en liste de longueurs :
List<String> mots = List.of("Java", "Stream", "API");
List<Integer> longueurs = mots.stream()
.map(String::length) // String → Integer
.collect(Collectors.toList());
// [4, 6, 3]
Chaque élément entre, un élément sort. La taille du Stream ne change pas.
Exemple avec flatMap()
Imaginons une liste de phrases et l'objectif d'obtenir tous les mots individuels :
List<String> phrases = List.of(
"Java est puissant",
"Stream API simplifie le code"
);
// ❌ Avec map : on obtient un Stream<Stream<String>> — pas ce qu'on veut
Stream<Stream<String>> nested = phrases.stream()
.map(p -> Arrays.stream(p.split(" ")));
// ✅ Avec flatMap : on aplatit en Stream<String>
List<String> mots = phrases.stream()
.flatMap(p -> Arrays.stream(p.split(" ")))
.collect(Collectors.toList());
// [Java, est, puissant, Stream, API, simplifie, le, code]
Quand utiliser lequel ?
Utilisez map() quand :
- Vous transformez un élément en un autre élément.
- La relation est 1 pour 1 :
T → R. - Exemples : mise en majuscules, parsing en entier, extraction d'une propriété d'objet.
Utilisez flatMap() quand :
- Chaque élément génère plusieurs éléments (ou zéro).
- La relation est 1 pour N :
T → Stream<R>. - Exemples : décomposer des phrases en mots, lire toutes les lignes de plusieurs fichiers, développer une hiérarchie.
Un cas concret : extraire les emails d'un groupe d'utilisateurs
class Utilisateur {
List<String> emails;
List<String> getEmails() { return emails; }
}
List<Utilisateur> utilisateurs = ...;
// Tous les emails de tous les utilisateurs, sans doublons
Set<String> tousLesEmails = utilisateurs.stream()
.flatMap(u -> u.getEmails().stream())
.collect(Collectors.toSet());
Avec map, on aurait obtenu un Stream<List<String>> et il aurait fallu itérer manuellement. flatMap règle tout en une ligne.
flatMap avec Optional
La même logique s'applique à Optional. Quand une opération renvoie elle-même un Optional, utilisez flatMap pour éviter un Optional<Optional<T>> :
Optional<Utilisateur> user = chercherUser(id);
// Si getEmailPrincipal() renvoie Optional<String>
Optional<String> email = user.flatMap(Utilisateur::getEmailPrincipal);
Résumé visuel
map : [a, b, c] → [f(a), f(b), f(c)]
flatMap: [a, b, c] → [g(a)1, g(a)2, g(b)1, g(c)1, g(c)2, g(c)3]
La règle à retenir : dès que votre fonction de transformation retourne un Stream, une Collection ou un Optional, pensez à flatMap.