W3docs

Operazioni Intermedie degli Stream Java

Trasforma gli stream Java in modo lazy con filter, map, flatMap, sorted, distinct, peek, limit e skip.

Un'operazione intermedia riceve uno stream e restituisce un altro stream. Registra cosa dovrebbe accadere a ciascun elemento quando la pipeline viene eseguita; non esegue nulla da sola. Le operazioni si concatenano e la catena rimane inattiva finché un terminale non estrae il primo elemento. Questa pigrizia è ciò che rende una pipeline di 30 righe meno costosa delle sue parti, ciò che rende trattabili le sorgenti infinite e ciò che fa sì che la scelta dell'operazione riguardi più la chiarezza che l'evitare il lavoro — le operazioni intermedie adiacenti si fondono in un unico passaggio.

Questo capitolo è una panoramica di ogni operazione intermedia che scriverai. Ogni voce ha la stessa struttura: cosa fa, qual è il tipo del suo callback, se è stateless o stateful, e uno o due errori comuni che determinano se la pipeline è corretta.

filter — mantieni ciò che corrisponde

Elimina gli elementi che non soddisfano un Predicate<T>:

List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .toList();

Stateless, lazy, preserva l'ordine. Il predicato deve essere privo di effetti collaterali — se modifica qualcosa di visibile, le pipeline parallele ti sorprenderanno e anche quelle sequenziali diventano difficili da leggere.

filter non cambia il tipo dell'elemento. Per mantenere un sottoinsieme e cambiare il tipo, usa filter seguito da map, oppure mapMulti (Java 16+) per il raro caso in cui un input diventa zero o un output di tipo diverso.

map — trasforma ogni elemento

Applica una Function<T, R> a ogni elemento, producendo uno stream di R:

List<Integer> lengths = words.stream()
    .map(String::length)
    .toList();

Stateless, lazy, preserva l'ordine, uno-in uno-fuori. Usa le specializzazioni primitive quando il risultato è numerico:

  • mapToInt, mapToLong, mapToDouble → stream primitivo (nessun boxing, sum() disponibile).
  • mapToObj su uno stream primitivo → torna a Stream<R>.
int totalLength = words.stream().mapToInt(String::length).sum();

flatMap — sostituisce ogni elemento con uno stream di altri

Una Function<T, Stream<R>> che "spacchetta" ogni elemento in più output (o nessuno, o uno):

List<List<String>> grouped = List.of(List.of("a", "b"), List.of("c"));
List<String> flat = grouped.stream()
    .flatMap(List::stream)
    .toList();                       // [a, b, c]

Il modello mentale: "ogni elemento diventa un sotto-stream e flatMap li concatena." È il modo per passare da uno stream di contenitori (Stream<List<T>>) a uno stream di contenuti (Stream<T>), per espandere ogni testo nelle sue parole e per trasformare uno stream di Optional<T> in uno stream di valori presenti (tramite Optional::stream).

Esistono anche specializzazioni primitive — flatMapToInt, flatMapToLong, flatMapToDouble — per il fan-out in uno stream primitivo.

Una confusione comune: map(s -> s.split(" ")) produce Stream<String[]> — uno stream di array, non uno stream piatto di parole. Per appiattire, usa flatMap(s -> Arrays.stream(s.split(" "))).

mapMulti — emetti zero, uno o molti elementi per input

mapMulti (Java 16+) è una flatMap più efficiente per i casi in cui ogni input produce un numero piccolo e variabile di output e creare un Stream per elemento è eccessivo:

people.stream()
    .<String>mapMulti((p, downstream) -> {
        if (p.age() >= 18) downstream.accept(p.name());
        if (p.email() != null) downstream.accept(p.email());
    })
    .forEach(System.out::println);

Usa flatMap quando hai naturalmente uno stream/lista da emettere; usa mapMulti quando altrimenti costruiresti un minuscolo stream di uno o due elementi per input solo per soddisfare la firma di flatMap.

distinct — elimina i duplicati

Rimuove gli elementi uguali usando equals / hashCode:

List<String> unique = words.stream().distinct().toList();

Stateful — per sapere se un elemento è un duplicato, distinct deve ricordare quelli già emessi. Su uno stream ordinato mantiene la prima occorrenza. Su uno stream non ordinato la JVM può essere più intelligente nel lavoro parallelo. Su uno stream infinito quasi mai vorresti usare distinct senza un limit a monte.

sorted — ordina gli elementi

Due forme — ordine naturale e un Comparator<T>:

List<String> az  = words.stream().sorted().toList();
List<String> byLen = words.stream().sorted(Comparator.comparingInt(String::length)).toList();

Stateful e bloccante al terminale: sorted deve bufferizzare ogni elemento prima di poterne emettere uno. Questo lo rende l'operazione intermedia più costosa e da usare con attenzione. Inserirlo prima di un limit(n) non risparmia lavoro — la JVM deve comunque vedere ogni input per sapere quali n tenere. (Per una pipeline "top N", preferisci una PriorityQueue limitata o Collectors.toList() seguito da subList dopo un sorted, a seconda di N rispetto al totale.)

Inoltre: non chiamare mai sorted su uno stream da una sorgente infinita — non restituisce mai.

peek — osserva senza modificare

Un Consumer<T> che viene eseguito per ogni elemento estratto. Restituisce lo stream invariato:

words.stream()
    .peek(s -> System.out.println("seen: " + s))
    .filter(s -> s.length() > 3)
    .toList();

Solo per il debug. peek viene eseguito in modo lazy e esattamente una volta per ogni elemento estratto, quindi è una finestra utile sulla pigrizia e il cortocircuito:

Stream.iterate(1, n -> n + 1)
    .peek(n -> System.out.println("considered " + n))
    .filter(n -> n > 100)
    .findFirst();                        // pulls 1..101 -- peek fires 101 times, then stops

Non mettere logica reale in un peek. La JVM è autorizzata a fondere, riordinare o saltare le chiamate a peek in determinate condizioni su stream non modificati, e su stream paralleli l'ordine non è definito.

limit(n) — mantieni al massimo n elementi

Ferma la pipeline dopo che n elementi sono passati:

List<Integer> firstFive = Stream.iterate(1, i -> i + 1).limit(5).toList();

Stateful (conta) e short-circuiting (lo stream a valle si ferma una volta raggiunto n). Su uno stream ordinato mantiene i primi n. Su uno stream parallelo non ordinato mantiene qualsiasi n — l'ordine non è garantito e un limit parallelo su uno stream ordinato paga per l'ordinamento. Se non ti importa quali n ottieni, stream.unordered().limit(n) è più veloce in parallelo.

Il pattern standard per domare qualsiasi sorgente infinita: ogni Stream.iterate / Stream.generate termina con un limit, un iterate a 3 argomenti limitato o un terminale short-circuiting come findFirst.

skip(n) — salta i primi n

Il complemento di limit. Scarta i primi n elementi, poi emette il resto:

List<Integer> rest = nums.stream().skip(2).toList();   // drops nums[0], nums[1]

Stateful (conta alla rovescia). Su uno stream ordinato il significato è esatto; su uno stream parallelo ordinato paga un costo di ordinamento. Insieme a limit, offre accesso "paginato":

list.stream().skip(page * pageSize).limit(pageSize).toList();

Funziona, ma per skip grandi su una List è ancora O(skip + limit). Un list.subList(...) diretto è più economico se hai la List a portata di mano.

takeWhile / dropWhile — finestre basate sul prefisso

Due operazioni intermedie short-circuiting (Java 9+) che agiscono su un prefisso dello stream:

// take elements while predicate holds, stop at the first miss
List<Integer> small = Stream.of(1, 2, 3, 10, 4, 5)
    .takeWhile(n -> n < 5)
    .toList();                                // [1, 2, 3]

// drop elements while predicate holds, then emit the rest
List<Integer> rest = Stream.of(1, 2, 3, 10, 4, 5)
    .dropWhile(n -> n < 5)
    .toList();                                // [10, 4, 5]

Queste non sono filter. filter verifica ogni elemento. takeWhile si ferma al primo fallimento (inclusi quelli che supererebbero filter in seguito). Su uno stream ordinato sono il modo economico per esprimere "tutto fino alla soglia".

boxed / asLongStream / asDoubleStream — muoversi tra i mondi primitivi

Gli stream primitivi hanno alcune operazioni intermedie proprie per tornare nel mondo degli oggetti:

IntStream.range(0, 5).boxed().toList();           // Stream<Integer> [0, 1, 2, 3, 4]
IntStream.range(0, 3).asLongStream().sum();        // 0L + 1L + 2L
IntStream.range(0, 3).asDoubleStream().average();

boxed è il ponte dal primitivo a Stream<Integer/Long/Double>. Il contrario è mapToInt/mapToLong/mapToDouble.

Stateless vs. stateful — perché è importante

StatelessStateful
filterdistinct
map / mapToXsorted
flatMap / mapMultilimit
peekskip
boxed / asLongStream / asDoubleStreamtakeWhile / dropWhile

Le operazioni intermedie stateful devono ricordare qualcosa tra gli elementi. sorted deve bufferizzare tutto. distinct deve ricordare ogni elemento già emesso. limit e skip necessitano di un contatore. Ciò le rende più costose (specialmente in parallelo) e da usare con attenzione.

L'ordine conta — fondi, filtra prima, trasforma dopo

Poiché le operazioni intermedie adiacenti si fondono in un unico passaggio elemento per elemento, l'ordine in cui le scrivi determina quanto lavoro fa la pipeline:

// Good: filter first, then the expensive map runs only on survivors.
people.stream()
    .filter(p -> p.age() >= 18)
    .map(this::expensiveLookup)
    .toList();

// Bad: every element pays for the map, then most are thrown away.
people.stream()
    .map(this::expensiveLookup)
    .filter(r -> r.score() > 0.5)
    .toList();

La regola generale: filtra prima, trasforma dopo, ordina una volta, distinct una volta. La JVM non riordina le tue operazioni intermedie — lo fai tu.

Un esempio pratico: l'intero vocabolario in una sola pipeline

Il programma seguente costruisce uno stream da una piccola lista, percorre ogni operazione trattata, stampa il risultato di ciascuna e dimostra la laziness/short-circuiting con peek e un iterate infinito.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • filter e map sono i cavalli di battaglia; le altre operazioni intermedie uno-in-uno-fuori (mapToInt, mapToObj, boxed) sono le economiche conversioni di valuta tra stream oggetto e stream primitivi.
  • flatMap e mapMulti sono il modo in cui un input diventa più output. La struttura Stream.of("a b") -> Arrays.stream(split(...)) è il pattern canonico di "tokenizzazione"; mapMulti è la scelta più economica quando altrimenti si costruirebbe un minuscolo stream per ogni elemento.
  • distinct e sorted sono statefuldistinct ha dovuto ricordare ogni Person precedentemente emessa per eliminare la "Alice" duplicata, e sorted ha dovuto bufferizzare l'intero input. Ecco perché entrambi vengono usati deliberatamente, di solito una volta sola e di solito alla fine.
  • peek si è eseguito una volta per ogni elemento estratto sull'iterate infinito — c'erano esattamente tante righe "considered N" quanti erano gli elementi che findFirst ha dovuto esaminare. Senza il cortocircuito quella pipeline non terminerebbe mai.
  • I due blocchi di conteggio lookup alla fine hanno reso concreta la regola dell'ordine. Filtrare prima ha eseguito la trasformazione costosa su molti meno elementi rispetto a mappare prima. Questo compromesso è tuo da impostare.

Cosa viene dopo

Le operazioni intermedie registrano la forma del lavoro; nulla viene eseguito finché un terminale non estrae. Il prossimo capitolo, Operazioni Terminali degli Stream Java, è il vocabolario completo dei terminali — forEach, count, min/max, findFirst/findAny, anyMatch/allMatch/noneMatch, reduce, toArray, toList e il gateway al capitolo successivo — collect.

Pratica

Pratica
In quale pipeline `sorted` deve bufferizzare *ogni* elemento di input prima di poterne emettere anche solo uno di output?
In quale pipeline `sorted` deve bufferizzare *ogni* elemento di input prima di poterne emettere anche solo uno di output?
Was this page helpful?