W3docs

Operazioni Terminali degli Stream Java

Attiva la valutazione degli stream Java con operazioni terminali: collect, forEach, reduce, count, min, max, anyMatch.

Un'operazione terminale è ciò che fa eseguire effettivamente una pipeline di stream. Le operazioni intermedie (filter, map, sorted, …) registrano solo il lavoro e rimangono lazy; una terminale tira gli elementi attraverso la pipeline, valuta l'intera catena e produce un risultato (o un effetto collaterale). Ogni pipeline termina con esattamente un'operazione terminale — invocala, e lo stream viene consumato; richiama un'altra terminale sullo stesso stream e otterrai IllegalStateException.

Questo capitolo copre ogni terminale che scriverai, quando ognuno cortocircuita, e i casi limite dello stream vuoto che silenziosamente mettono in difficoltà le persone.

Le terminali si presentano in tre forme. Gli aggregatori restituiscono un singolo valore (count, sum, min, max, reduce). I cercatori cercano un elemento e si fermano (findFirst, findAny, anyMatch, allMatch, noneMatch). I costruttori materializzano lo stream in un contenitore (toList, toArray, collect, forEach per gli effetti collaterali). Questo capitolo percorre ogni terminale che scriverai al di fuori di collect, che è abbastanza ampio da richiedere un capitolo a sé nel prossimo.

forEach / forEachOrdered — effetti collaterali

La terminale più semplice. Esegue un Consumer<T> per ogni elemento, non restituisce nulla:

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

L'ordine non è garantito — su uno stream sequenziale di solito lo è; su uno stream parallelo non lo è. Se hai bisogno dell'ordine della sorgente anche in parallelo, usa forEachOrdered:

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

forEach è per gli effetti collaterali che vuoi genuinamente — logging, mutare un sink, chiamare un'API non-stream. Non è il modo corretto per costruire una collezione (quello è toList / collect) o accumulare un valore (quello è reduce). Un forEach che muta una lista esterna è un code smell anche quando funziona, perché rinuncia a tutto ciò che rendeva la pipeline dichiarativa in primo luogo.

count — quanti elementi

Restituisce un long:

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

count cortocircuita su sorgenti di dimensione nota dove la JVM può calcolare la risposta dalla dimensione della sorgente (quindi IntStream.range(0, 1_000_000).count() restituisce 1000000 senza iterare). Su uno stream con un filter o flatMap attivo, deve percorrere ogni elemento.

Un trabocchetto comune: stream.count() su una catena .peek(...) potrebbe non eseguire il peek se la JVM può dedurre il conteggio dalla sorgente, perché non c'è differenza di comportamento osservabile. Non usare peek per "vedere quanti sono stati filtrati" — usa mapToInt(x -> 1).sum() o ristruttura.

min / max — elementi estremi

Entrambi prendono un Comparator<T> e restituiscono Optional<T> (perché lo stream potrebbe essere vuoto):

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

Le specializzazioni primitive sono più semplici — IntStream.max() restituisce OptionalInt, nessun comparator necessario:

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

min/max cortocircuitano solo su sorgenti delimitate. Su uno stream infinito, max non termina mai.

findFirst / findAny — ottieni un elemento

Entrambi restituiscono Optional<T>, entrambi cortocircuitano. La differenza sta in ciò che promettono su quale elemento ottieni:

Optional<Person> first = people.stream().filter(p -> p.age() >= 30).findFirst();
Optional<Person> any   = people.stream().filter(p -> p.age() >= 30).findAny();
  • findFirst restituisce il primo elemento nell'ordine di incontro. Su uno stream sequenziale è letteralmente il primo. Su uno stream parallelo costa di più rispetto a findAny perché la JVM deve coordinarsi.
  • findAny restituisce qualche elemento corrispondente — il primo che trova qualsiasi worker. In parallelo è più economico. In sequenziale, entrambi restituiscono la stessa cosa.

Usa findAny quando quale corrispondenza ottieni non ha davvero importanza (è un controllo di esistenza singolo che necessita del valore, non solo un boolean). Usa findFirst quando intendi "il primo".

anyMatch / allMatch / noneMatch — quantificatori di esistenza

Prendono un Predicate<T> e restituiscono boolean. Tutti e tre cortocircuitano:

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 si ferma non appena uno passa.
  • allMatch si ferma non appena uno fallisce.
  • noneMatch è !anyMatch(p) — si ferma al primo passaggio e restituisce false.

Semantica dello stream vuoto (la regola che inganna tutti almeno una volta): anyMatch su un insieme vuoto è false. allMatch e noneMatch su un insieme vuoto sono entrambi true — per vuota verità, perché non ci sono controprove. Può essere esattamente ciò che vuoi o esattamente ciò che non vuoi, a seconda della domanda. Se "vuoto" è una possibilità che vale la pena gestire, controlla prima isEmpty (o count() == 0).

reduce — riduzione a un singolo valore

L'aggregatore più generale. Tre overload, ciascuno per una forma leggermente diversa:

reduce(identity, accumulator) a due argomenti — riduzione con un valore iniziale, restituisce T (nessun Optional, perché l'identità è la risposta per uno stream vuoto):

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

reduce(accumulator) a un argomento — nessuna identità; restituisce Optional<T> per il caso dello stream vuoto:

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) a tre argomenti — usato quando l'accumulatore produce un tipo diverso dagli elementi (e richiesto in parallelo). Il combiner unisce due risultati parziali:

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

Tre regole per reduce che impediscono alla pipeline di andare silenziosamente storta:

  1. L'accumulatore deve essere associativo: f(f(a, b), c) == f(a, f(b, c)). Somme e concatenazione di stringhe soddisfano questo; la sottrazione no.
  2. L'identità deve essere una vera identità: f(id, x) == x per tutti gli x. 0 per +, 1 per *, \"\" per concat.
  3. L'accumulatore e il combiner devono essere senza stato e privi di effetti collaterali.

Viola una qualsiasi di queste e una pipeline sequenziale darà comunque la risposta corretta la maggior parte delle volte — una parallela ti sorprenderà. (Questo è lo stesso contratto su cui si basano Collectors.reducing e reduce parallelo.)

sum / average — aggregatori primitivi

Solo su stream primitivi. sum restituisce il primitivo; average restituisce un OptionalDouble:

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

Per riepiloghi numerici più ricchi — count, sum, min, max, average in un solo passaggio — vedi IntSummaryStatistics:

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

Questo è un passaggio solo, un'allocazione, e molto più economico che calcolarli separatamente.

toArray e toList — materializzazione

Due terminali abbreviate "dammi tutto":

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+) è il modo moderno per materializzare uno stream in una List ed è la scelta giusta nel 95% dei casi. È non modificabile e può contenere null; se hai bisogno di una lista mutabile, di un'implementazione specifica, o di un Set/Map, ricadi su collect(Collectors.toCollection(ArrayList::new)) o i suoi affini nel prossimo capitolo.

toArray(T[]::new) è l'unico modo per ottenere un array tipizzato da uno stream di oggetti — la forma IntFunction<T[]> fornisce al runtime il tipo componente dell'array.

iterator e spliterator — vie d'uscita

Uno stream può essere trasformato in un Iterator<T> o Spliterator<T> per passarlo a codice che ne aspetta uno:

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

Entrambi sono terminali — consumano lo stream. Esistono per l'interoperabilità, non per "voglio un ciclo for"; se vuoi un ciclo, usane uno senza creare prima uno stream.

Cortocircuito vs. consumo — la tabella di sicurezza

TerminaleCortocircuita su sorgente infinita?
findFirst / findAny
anyMatch / allMatch / noneMatch
limit(n) (intermedia) poi qualsiasi cosa
forEach / forEachOrderedno — consuma tutto
countno — consuma tutto
min / maxno — consuma tutto
reduceno — consuma tutto
sum / average / summaryStatisticsno — consuma tutto
toList / toArray / collectno — consuma tutto

Il pattern è chiaro: qualsiasi terminale che ha bisogno di considerare ogni elemento per produrre la sua risposta non cortocircuita, e abbinarla a una sorgente infinita senza un limit a monte blocca la JVM. Cercatori e quantificatori sono le uniche terminali "sicure su fonti infinite".

Un esempio concreto: ogni forma di terminale su una pipeline

Il programma seguente costruisce un piccolo stream, richiama ogni terminale che abbiamo trattato e mostra le risposte per stream vuoto per i tre matcher e per min / findFirst / reduce.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • Le terminali di "ricerca" — findFirst, findAny, anyMatch, allMatch, noneMatch — e le terminali di "consumo totale" — count, min/max, reduce, sum, toList — dividono il capitolo in modo netto. Le terminali di ricerca cortocircuitano; quelle di consumo totale no. Abbina il secondo gruppo a una sorgente infinita solo dietro un limit.
  • allMatch su uno stream vuoto ha restituito true. Lo ha fatto anche noneMatch. Questa è la verità vacua — è la risposta standard, ed è il motivo più comune per cui il codice di produzione "supera erroneamente" un caso limite con input vuoto. Se il vuoto è significativo, controllalo prima.
  • I tre overload di reduce coprono tre pattern. A due argomenti con una vera identità restituisce T. A un argomento restituisce Optional<T> perché non c'è identità. A tre argomenti consente al tipo dell'accumulatore di differire dal tipo degli elementi — ed è la forma che è effettivamente sicura in parallelo, perché il combiner dice alla JVM come unire i risultati parziali.
  • summaryStatistics() ha fatto in un passaggio ciò che chiamare min, max, sum, average e count separatamente avrebbe fatto in cinque. Su qualsiasi stream numerico non banale, preferiscilo.
  • toList() ha restituito una lista non modificabile. Questo è il valore predefinito di Java 16+ e quasi sempre quello che vuoi; il prossimo capitolo mostra la forma Collectors.toCollection(...) quando hai bisogno di una lista mutabile, di un'implementazione specifica, o di un Set / Map.

Cosa c'è dopo

collect è l'unica terminale che abbiamo rimandato — e il gateway verso metà dell'API. Il prossimo capitolo, Java Stream Collectors, percorre la cassetta degli strumenti Collectors: toList/toSet/toMap, groupingBy, partitioningBy, joining, counting, summingInt, averagingDouble, mapping, reducing, e il pattern downstream che li compone.

Pratica

Pratica
Uno stream contiene zero elementi. Quale di questi restituisce `true`?
Uno stream contiene zero elementi. Quale di questi restituisce `true`?
Was this page helpful?