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.