W3docs

Opérations terminales des Streams Java

Déclenchez l'évaluation d'un stream Java avec les opérations terminales : collect, forEach, reduce, count, min, max, anyMatch.

Une opération terminale est ce qui fait réellement s'exécuter un pipeline de stream. Les opérations intermédiaires (filter, map, sorted, …) ne font qu'enregistrer le travail et restent paresseuses ; une opération terminale tire les éléments au travers du pipeline, évalue l'ensemble et produit un résultat (ou un effet de bord). Chaque pipeline se termine par exactement une opération terminale — appelez-la, et le stream est consommé ; appelez une autre opération terminale sur le même stream et vous obtiendrez une IllegalStateException.

Ce chapitre couvre toutes les opérations terminales que vous utiliserez, les cas où chacune court-circuite, et les cas limites avec des streams vides qui font trébucher les gens discrètement.

Les opérations terminales se présentent sous trois formes. Les agrégateurs renvoient une valeur unique (count, sum, min, max, reduce). Les chercheurs recherchent un élément et s'arrêtent (findFirst, findAny, anyMatch, allMatch, noneMatch). Les constructeurs matérialisent le stream dans un conteneur (toList, toArray, collect, forEach pour les effets de bord). Ce chapitre parcourt toutes les opérations terminales que vous utiliserez en dehors de collect, qui est suffisamment importante pour nécessiter son propre chapitre.

forEach / forEachOrdered — effets de bord

L'opération terminale la plus simple. Exécute un Consumer<T> pour chaque élément, ne retourne rien :

names.stream().forEach(System.out::println);

L'ordre n'est pas garanti — sur un stream séquentiel, il l'est généralement ; sur un stream parallèle, il ne l'est pas. Si vous avez besoin de l'ordre source même en parallèle, utilisez forEachOrdered :

names.parallelStream().forEachOrdered(System.out::println);

forEach est réservé aux effets de bord que vous voulez réellement — journalisation, mutation d'un récepteur, appel d'une API non-stream. Ce n'est pas la bonne façon de construire une collection (c'est toList / collect) ou d'accumuler une valeur (c'est reduce). Un forEach qui mute une liste externe est un code smell même lorsqu'il fonctionne, car il abandonne tout ce qui rendait le pipeline déclaratif.

count — nombre d'éléments

Retourne un long :

long adults = people.stream().filter(p -> p.age() >= 18).count();

count court-circuite sur les sources de taille connue où la JVM peut calculer la réponse à partir de la taille de la source (ainsi IntStream.range(0, 1_000_000).count() retourne 1000000 sans itérer). Sur un stream avec un filter ou flatMap actif, il doit parcourir chaque élément.

Un piège courant : stream.count() sur une chaîne .peek(...) peut ne pas exécuter le peek si la JVM peut prouver le count à partir de la source, car il n'y a pas de différence de comportement observable. N'utilisez pas peek pour "voir combien ont été filtrés" — utilisez mapToInt(x -> 1).sum() ou restructurez.

min / max — éléments extrêmes

Les deux prennent un Comparator<T> et retournent Optional<T> (parce que le stream peut être vide) :

Optional<Person> oldest  = people.stream().max(Comparator.comparingInt(Person::age));
Optional<String> shortest = words.stream().min(Comparator.comparingInt(String::length));

Les spécialisations primitives sont plus simples — IntStream.max() retourne OptionalInt, sans comparateur :

OptionalInt highest = nums.stream().mapToInt(Integer::intValue).max();
int hi = highest.orElse(Integer.MIN_VALUE);

min/max ne court-circuitent que sur les sources bornées. Sur un stream infini, max ne termine jamais.

findFirst / findAny — obtenir un élément

Les deux retournent Optional<T> et court-circuitent. La différence porte sur quel élément ils promettent de retourner :

Optional<Person> first = people.stream().filter(p -> p.age() >= 30).findFirst();
Optional<Person> any   = people.stream().filter(p -> p.age() >= 30).findAny();
  • findFirst retourne le premier élément dans l'ordre de rencontre. Sur un stream séquentiel, c'est littéralement le premier. Sur un stream parallèle, cela coûte plus cher que findAny car la JVM doit se coordonner.
  • findAny retourne un élément correspondant — le premier que trouve n'importe quel worker. En parallèle, c'est moins coûteux. En séquentiel, les deux retournent la même chose.

Utilisez findAny quand quel résultat vous obtenez n'a vraiment pas d'importance (c'est une simple vérification d'existence qui nécessite la valeur, pas seulement un boolean). Utilisez findFirst quand vous voulez dire "le premier".

anyMatch / allMatch / noneMatch — quantificateurs d'existence

Prennent un Predicate<T> et retournent boolean. Les trois court-circuitent :

boolean hasAdult  = people.stream().anyMatch(p -> p.age() >= 18);
boolean allAdult  = people.stream().allMatch(p -> p.age() >= 18);
boolean noChildren = people.stream().noneMatch(p -> p.age() < 13);
  • anyMatch s'arrête dès qu'un élément passe.
  • allMatch s'arrête dès qu'un élément échoue.
  • noneMatch est !anyMatch(p) — s'arrête au premier succès et retourne false.

Sémantique des streams vides (la règle qui piège tout le monde une fois) : anyMatch sur un stream vide est false. allMatch et noneMatch sur un stream vide sont tous les deux true — par vacuité, car il n'y a pas de contre-exemples. Cela peut être exactement ce que vous voulez ou exactement ce que vous ne voulez pas, selon la question. Si "vide" est une possibilité à traiter, vérifiez d'abord isEmpty (ou count() == 0).

reduce — réduire à une valeur unique

L'agrégateur le plus général. Trois surcharges, chacune pour une forme légèrement différente :

reduce(identity, accumulator) à deux arguments — plie avec une valeur de départ, retourne T (pas d'Optional, car l'identité est la réponse pour un stream vide) :

int sum = nums.stream().reduce(0, Integer::sum);
String all = words.stream().reduce(\"\", String::concat);

reduce(accumulator) à un argument — pas d'identité ; retourne Optional<T> pour le cas du stream vide :

Optional<Integer> sum = nums.stream().reduce(Integer::sum);
Optional<String> longest = words.stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);

reduce(identity, accumulator, combiner) à trois arguments — utilisé quand l'accumulateur produit un type différent des éléments (et requis en parallèle). Le combiner fusionne deux résultats partiels :

int totalLength = words.stream()
    .reduce(0,
            (acc, w) -> acc + w.length(),     // BiFunction<Integer, String, Integer>
            Integer::sum);                     // BinaryOperator<Integer>

Trois règles pour reduce qui empêchent le pipeline de donner des résultats erronés :

  1. L'accumulateur doit être associatif : f(f(a, b), c) == f(a, f(b, c)). Les sommes et la concaténation de chaînes satisfont cela ; la soustraction ne le fait pas.
  2. L'identité doit être une vraie identité : f(id, x) == x pour tout x. 0 pour +, 1 pour *, \"\" pour concat.
  3. L'accumulateur et le combiner doivent être sans état et sans effets de bord.

Violez l'une de ces règles et un pipeline séquentiel donnera généralement la bonne réponse — un pipeline parallèle vous surprendra. (C'est le même contrat sur lequel s'appuient Collectors.reducing et le reduce parallèle.)

sum / average — agrégateurs primitifs

Uniquement sur les streams primitifs. sum retourne le primitif ; average retourne un OptionalDouble :

int total      = IntStream.rangeClosed(1, 100).sum();
OptionalDouble avg = nums.stream().mapToInt(Integer::intValue).average();
double mean = avg.orElse(0.0);

Pour des résumés numériques plus riches — count, sum, min, max, average en un seul passage — voir IntSummaryStatistics :

IntSummaryStatistics stats = nums.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println(stats);   // {count=N, sum=..., min=..., average=..., max=...}

C'est un seul passage, une seule allocation, et bien moins coûteux que de les calculer séparément.

toArray et toList — matérialiser

Deux opérations terminales raccourcies "donnez-moi tout" :

Object[] anyArr = stream.toArray();                     // Object[]
String[] strArr = stream.toArray(String[]::new);        // typed via constructor ref
List<String> immutable = stream.toList();               // Java 16+, unmodifiable

stream.toList() (Java 16+) est la façon moderne de matérialiser un stream en List et constitue le bon choix dans 95% des cas. Elle est non modifiable et peut contenir des null ; si vous avez besoin d'une liste mutable, d'une implémentation spécifique, ou d'un Set/Map, utilisez collect(Collectors.toCollection(ArrayList::new)) ou ses équivalents dans le prochain chapitre.

toArray(T[]::new) est le seul moyen d'obtenir un tableau typé à partir d'un stream d'objets — la forme IntFunction<T[]> donne au runtime le type de composant du tableau.

iterator et spliterator — portes de sortie

Un stream peut être converti en Iterator<T> ou Spliterator<T> pour être transmis à du code qui en attend un :

for (Iterator<String> it = stream.iterator(); it.hasNext(); ) {
    use(it.next());
}

Ce sont toutes deux des opérations terminales — elles consomment le stream. Elles existent pour l'interopérabilité, pas pour "je veux une boucle for" ; si vous voulez une boucle, utilisez-en une sans créer de stream au préalable.

Court-circuitage vs consommation — table de sécurité

Opération terminaleCourt-circuite sur source infinie ?
findFirst / findAnyoui
anyMatch / allMatch / noneMatchoui
limit(n) (intermédiaire) puis quoi que ce soitoui
forEach / forEachOrderednon — consomme tout
countnon — consomme tout
min / maxnon — consomme tout
reducenon — consomme tout
sum / average / summaryStatisticsnon — consomme tout
toList / toArray / collectnon — consomme tout

Le schéma est clair : toute opération terminale qui doit considérer chaque élément pour produire sa réponse ne court-circuite pas, et l'associer à une source infinie sans un limit en amont bloque la JVM. Les chercheurs et les quantificateurs sont les seules opérations terminales "sûres sur les sources infinies".

Exemple concret : toutes les formes d'opérations terminales sur un même pipeline

Le programme ci-dessous construit un petit stream, appelle chaque opération terminale que nous avons couverte, et montre les réponses pour un stream vide avec les trois matchers ainsi qu'avec min / findFirst / reduce.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Les opérations terminales de "recherche" — findFirst, findAny, anyMatch, allMatch, noneMatch — et les opérations terminales de "consommation totale" — count, min/max, reduce, sum, toList — divisent clairement le chapitre. Les opérations de recherche court-circuitent ; les opérations de consommation totale ne le font pas. N'associez le second groupe à une source infinie qu'après un limit.
  • allMatch sur un stream vide a retourné true. Tout comme noneMatch. C'est la vérité vacuouse — c'est la réponse standard, et c'est la raison la plus courante pour laquelle le code de production "passe incorrectement" un cas limite avec une entrée vide. Si vide est significatif, vérifiez-le d'abord.
  • Les trois surcharges de reduce couvrent trois schémas. Deux arguments avec une vraie identité retournent T. Un argument retourne Optional<T> car il n'y a pas d'identité. Trois arguments permettent au type de l'accumulateur de différer du type de l'élément — et c'est la forme qui est réellement sûre en parallèle, car le combiner indique à la JVM comment fusionner les résultats partiels.
  • summaryStatistics() a accompli en un seul passage ce qu'auraient fait séparément min, max, sum, average et count en cinq. Sur tout stream numérique non trivial, préférez-le.
  • toList() a retourné une liste non modifiable. C'est le comportement par défaut de Java 16+ et c'est presque toujours ce que vous souhaitez ; le prochain chapitre présente la forme Collectors.toCollection(...) pour les cas où vous avez besoin d'une liste mutable, d'une implémentation spécifique, ou d'un Set / Map.

Et ensuite

collect est la seule opération terminale que nous avons différée — et la porte d'entrée vers la moitié de l'API. Le prochain chapitre, Java Stream Collectors, parcourt la boîte à outils Collectors : toList/toSet/toMap, groupingBy, partitioningBy, joining, counting, summingInt, averagingDouble, mapping, reducing, et le schéma downstream qui les compose.

Pratique

Pratique
Un stream contient zéro élément. Lequel de ces exemples retourne `true` ?
Un stream contient zéro élément. Lequel de ces exemples retourne `true` ?
Was this page helpful?