CodeGym /Cours /JAVA 25 SELF /Traitement paresseux (lazy evaluation) dans l’API Stream

Traitement paresseux (lazy evaluation) dans l’API Stream

JAVA 25 SELF
Niveau 33 , Leçon 1
Disponible

1. Qu’est-ce que le traitement paresseux ?

Le traitement paresseux, ou « lazy evaluation », est le principe selon lequel les opérations sur les données sont différées jusqu’à ce que le résultat soit réellement nécessaire. Dans le contexte de l’API Stream, cela signifie : si vous avez écrit une chaîne de transformations sur une collection, Java ne les exécute pas immédiatement — elle attend qu’une opération terminale soit appelée. Ce n’est qu’à ce moment‑là que toute la chaîne est évaluée.

Pourquoi est‑ce utile ? D’une part, cela économise des ressources : les éléments finalement inutiles ne sont tout simplement pas traités. D’autre part, les performances s’améliorent — on peut construire de longues chaînes sans créer de nombreuses collections intermédiaires. Enfin, les « calculs courts » sont possibles : dès que le premier élément adéquat est trouvé, le traitement ultérieur s’arrête.

Une analogie : imaginez un serveur « paresseux ». Vous dites : « Apporte le menu, puis un café, puis un gâteau. » Il acquiesce, mais ne fait rien… tant que vous n’ajoutez pas : « Et maintenant, apporte‑le vraiment. » C’est alors seulement qu’il va exécuter la commande — et il peut n’apporter que le café si le gâteau n’est plus disponible. Le traitement paresseux dans les streams fonctionne à peu près de la même manière.

2. Opérations intermédiaires et terminales

Intermédiaires (intermediate) :

  • filter
  • map
  • sorted
  • distinct
  • peek (pour le débogage)
  • et autres

Les opérations intermédiaires renvoient un nouveau Stream, mais ne déclenchent pas les calculs. Elles ne font que « construire le plan » du traitement.

Terminales (terminal) :

  • collect
  • forEach
  • reduce
  • count
  • findFirst, findAny
  • anyMatch, allMatch, noneMatch
  • et autres

Seule une opération terminale déclenche l’exécution de toute la chaîne.

Exemple : rien ne se passe sans opération terminale

List<String> names = List.of("Alice", "Bob", "Charlie");

names.stream()
     .filter(name -> {
         System.out.println("Je filtre " + name);
         return name.startsWith("A");
     });
// Aucun message n’apparaîtra ! Le code ci‑dessus ne fait que "construire" la chaîne.

Ajoutons maintenant une opération terminale :

names.stream()
     .filter(name -> {
         System.out.println("Je filtre " + name);
         return name.startsWith("A");
     })
     .forEach(System.out::println);
// Cette fois, nous verrons une sortie dans la console !

Résultat :

Je filtre Alice
Je filtre Bob
Je filtre Charlie
Alice

3. Avantages du traitement paresseux

Économie de ressources

Le traitement paresseux évite de consacrer du temps et de la mémoire aux éléments dont vous n’avez pas besoin. Par exemple, si vous recherchez le premier objet adéquat, le traitement s’arrêtera à la première correspondance.

List<String> names = List.of("Alice", "Bob", "Charlie", "Anna");

String firstA = names.stream()
    .filter(name -> {
        System.out.println("Je vérifie : " + name);
        return name.startsWith("A");
    })
    .findFirst()
    .orElse("Introuvable");

System.out.println("Résultat : " + firstA);

Sortie :

Je vérifie : Alice
Résultat : Alice

À noter : les autres éléments ne sont même pas vérifiés !

Chaînes longues sans collections intermédiaires

On peut combiner de nombreuses opérations (filter, map, sorted, etc.) sans créer de collection à chaque étape.

List<String> names = List.of("Alice", "Bob", "Charlie", "Anna");

List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList(); // Java 16+, auparavant — .collect(Collectors.toList())

Calculs courts

S’il suffit de savoir qu’il existe au moins un élément adéquat, les autres ne seront pas vérifiés :

boolean hasLongName = names.stream()
    .anyMatch(name -> {
        System.out.println("Je vérifie : " + name);
        return name.length() > 10;
    });

// Si le premier élément est long, les autres ne seront pas vérifiés !

4. Exemples : comment fonctionne le traitement paresseux

Exemple 1 : rien ne se passe sans opération terminale

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

numbers.stream()
    .filter(n -> {
        System.out.println("Je filtre " + n);
        return n % 2 == 0;
    });
// Pas de sortie !

Exemple 2 : chaîne avec opération terminale

numbers.stream()
    .filter(n -> {
        System.out.println("Je filtre " + n);
        return n % 2 == 0;
    })
    .map(n -> {
        System.out.println("Je multiplie " + n);
        return n * 10;
    })
    .forEach(System.out::println);

Sortie :

Je filtre 1
Je filtre 2
Je multiplie 2
20
Je filtre 3
Je filtre 4
Je multiplie 4
40
Je filtre 5

Remarque importante : les opérations sont exécutées élément par élément : d’abord filter, puis map, puis forEach — pour chaque élément à son tour. Il ne s’agit pas de deux parcours distincts « d’abord tout filtrer, puis tout transformer ».

Exemple 3 : utilisation de peek pour le débogage

numbers.stream()
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("Passé le filtre : " + n))
    .map(n -> n * 10)
    .peek(n -> System.out.println("Après map : " + n))
    .forEach(System.out::println);

5. Nuances utiles

N’utilisez pas les streams avec des effets de bord

La paresse peut jouer des tours si vous comptez sur une exécution immédiate. Les actions avec effets de bord au sein de map, filter ou peek (écriture dans un fichier, modification d’un état externe) peuvent s’exécuter dans un ordre inattendu, pas pour tous les éléments, ou ne pas s’exécuter du tout sans opération terminale.

Filtrez le plus tôt possible

Placez filter au début de la chaîne afin d’écarter plus tôt les éléments superflus et de réduire la charge du traitement ultérieur.

Besoin du premier résultat uniquement ? Utilisez les opérations terminales adaptées

Si vous avez besoin du premier élément adéquat, appelez findFirst ou findAny. Cela permet au stream de s’arrêter dès que le résultat est trouvé.

Les streams ne servent pas à modifier la collection d’origine

Les streams ne sont pas conçus pour ajouter/supprimer des éléments dans la collection d’origine. Pour modifier la structure d’une collection, utilisez d’autres mécanismes.

Visualisation du fonctionnement des streams paresseux

List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");

words.stream()
    .filter(w -> w.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

Comment cela se passe :

Étape cat dog elephant fox giraffe
filter
map
ELEPHANT GIRAFFE
forEach
affichage affichage

Tableau : comparaison des approches eager et lazy

Approche Quand le traitement est-il exécuté ? Utilisation de la mémoire Performances
Eager (avide) Immédiatement à l’appel Peut être élevée Parfois plus lent
Lazy (paresseux) Uniquement en cas de besoin Minimal Généralement plus rapide

Approche avide — par exemple, organiser manuellement plusieurs passages sur une collection en créant des listes intermédiaires.
Approche paresseuse — les streams : rien ne se fait tant que le résultat final n’est pas demandé.

6. Erreurs fréquentes avec les streams paresseux

Erreur n° 1 : attendre un résultat immédiat. Les débutants pensent que les appels à filter ou map s’exécutent tout de suite. Mais sans opération terminale (par exemple, collect, forEach) il ne se passera rien — d’où « le débogage ne marche pas », « rien ne s’affiche ».

Erreur n° 2 : effets de bord dans les opérations intermédiaires. Écrire dans un fichier, modifier des variables externes à l’intérieur de map/filter/peek — c’est une mauvaise pratique. À cause de la paresse et des optimisations, ces actions peuvent ne pas s’exécuter complètement, pas dans l’ordre attendu, ou ne pas s’exécuter du tout.

Erreur n° 3 : oubli d’appeler une opération terminale. Une chaîne de streams a été écrite, mais elle n’est pas terminée par collect, forEach, etc. Résultat : « silence ».

Erreur n° 4 : s’attendre à ce que tous les éléments soient traités. Des opérations comme findFirst ou anyMatch interrompent le pipeline dès le premier résultat. Les autres éléments ne sont pas traités — d’où la surprise « pourquoi mon println n’a‑t‑il pas été exécuté pour tous ? ».

Erreur n° 5 : utiliser des streams pour modifier la collection d’origine. Les streams ne sont pas faits pour modifier les collections d’origine (ajout/suppression d’éléments). Utilisez des méthodes spécialisées des collections ou des itérateurs.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION