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 |
|---|---|---|---|---|---|
|
✗ | ✗ | ✓ | ✗ | ✓ |
|
— | — | ELEPHANT | — | GIRAFFE |
|
— | — | 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.
GO TO FULL VERSION