Una delle nuove feature più importanti introdotte in Java 8 è senza dubbio il concetto di stream. Ne abbiamo già parlato in un , vediamo adesso come creare e utilizzare stream infiniti. Prima di questo vedremo con alcuni esempi il comportamento interno degli stream. Capire il comportamento interno è importante per sfruttare al massimo questa nuova feature che cambierà il modo di scrivere codice in Java.
Gli stream: modalità di esecuzione delle operazioni
Gli stream vengono di solito utilizzati concatenando più invocazioni di metodi una dopo l’altra. Alcuni metodi sono intermedi e quindi ritornano a loro volta uno stream per eseguire altre operazioni. Altri metodi sono terminali e quindi non ritornano niente o ritornano il risultato di un calcolo. Questa distinzione è esplicitata anche nel JavaDoc dei vari metodi. Nel seguente esempio utilizziamo il metodo map
(è un metodo intermedio, ritorna a sua volta uno stream) e forEach
(metodo terminale, non ritorna niente):
Stream.of("a", "b", "c") .map(String::toUpperCase) .forEach(System.out::print);
Ma come sono eseguite queste operazioni su uno stream? Vedendo il codice potrebbe sembrare che le operazioni siano eseguite una dopo l’altra così come avverrebbe usando una collection. Per approfondire questo concetto aggiungiamo nell’esempio appena vista una chiamata al metodo intermedio peek
:
Stream.of("a", "b", "c") .peek(System.out::print) .map(String::toUpperCase) .forEach(System.out::print);
Il metodo peek
è molto utile per il debug, la traduzione di peek è sbirciare e rende bene l’idea di cosa faccia questo metodo. In pratica ci permette di invocare una lambda expression sugli elementi dello stream senza modificarne il tipo. Eseguendo questo codice cosa vi aspettate sulla console? La stringa abcABC
?
Provate a eseguirlo e vedrete comparire la stringa aAbBcC
. In pratica le operazioni non sono eseguite in modo sequenziale ma sono ritardate fino all’ultimo momento possibile, è l’ultima operazione (quella terminale) che fa scaturire tutte le chiamate precedenti. Infatti se togliamo l’ultima chiamata a forEach
non vedremo niente stampato sulla console, senza l’operazione terminale non viene eseguito niente.
Modifichiamo leggermente l’esempio, aggiungiamo una chiamata al metodo limit
per far ritornare solo il primo elemento:
Stream.of("a", "b", "c") .peek(System.out::println) .map(String::toUpperCase) .limit(1) .forEach(System.out::println);
In questo caso vedremo sulla console la stringa aA
, gli elementi successivi dello stream non sono mai analizzati. Un discorso analogo può essere fatto per il metodo findFirst
, richiamandolo su uno stream viene ritornato un Optional
con l’eventuale risultato trovato (potete leggere il nostro per approfondire questo argomento). Al contrario di quello che ci potremmo aspettare ll metodo findFirst
non ha nessun parametro, non c’è una lambda expression da usare per scegliere l’elemento. Questa comunque non è una mancanza in quanto viene usato spesso dopo una chiamata a filter
. Vediamo un esempio:
Stream.of("a", "b", "c") .peek(System.out::print) .map(String::toUpperCase) .peek(System.out::print) .filter(s -> s.compareTo("B") >= 0) .findFirst() .ifPresent(System.out::print);
Eseguendo questo codice vedremo sulla console la stringa aAbBB
, il terzo elemento dello stream non viene mai analizzato in quanto il metodo findFirst termina la computazione al primo elemento trovato.
Stream generati dinamicamente e stream infiniti
Solitamente in Java siamo abituati a manipolare i dati in collection, i dati sono tutti in memoria e vengono analizzati in base a un algoritmo. Proprio perché i dati sono in memoria una collection può contenere solo un numero finito di oggetti. Invece parlando di stream, e avendo capito in che ordine vengono eseguite le varie operazioni, ha senso anche il concetto di stream infinito. Infatti i dati saranno logicamente disponibili ma non saranno prodotti fino a che non ce ne sarà bisogno.
Per esempio vediamo come creare uno stream dei numeri pari positivi. Per crearlo possiamo usare il metodo iterate
della classe IntStream
, questo metodo prende come parametri il primo valore dello stream e una lambda expression che genera l’i-esimo valore dello stream partendo dal precedente. Detto questo è banale creare uno stream dei numeri pari partendo dal valore zero:
IntStream.iterate(0, i -> i +2) .limit(100) .forEach(System.out::println);
Ovviamente togliendo la chiamata a limit
si avrebbe l’equivalente di un ciclo infinito. Un’altro esempio di stream infinito che può essere utile è quello di numeri casuali, per crearlo basta richiamare new Random().ints()
.
Un altro esempio interessante di stream infinito è quello riguardante i numeri primi. In questo caso possiamo generare uno stream di tutti i numeri interi positivi e poi eseguire un filtro:
LongStream.iterate(1, i -> i+1) .filter(Primes::isPrime) .limit(100) .forEach(System.out::println);
Il metodo per capire se un numero è primo utilizza a sua volta uno stream:
public static boolean isPrime(long n) { if (n < 2) { return false; } return LongStream .range(2, (long) (Math.sqrt(n) + 1)) .noneMatch(i -> n % i == 0); }
A parte il controllo iniziale, utilizziamo un stream di long creato con il metodo range
, con il metodo noneMatch
controlliamo che non ci siano divisori senza resto del numero passato.
Conclusioni
In questo post abbiamo visto alcune caratteristiche avanzate degli stream introdotti con Java 8. Ovviamente ci sono tante altre cose da dire sugli stream, i metodi sono tanti e solo con la pratica sarà possibile prenderne confidenza. Per chi vuole approfondire l’argomento un ottimo libro è quello di Venkat Subramaniam (coautore fra l’altro anche di ): .
Fino ad adesso abbiamo visto diversi aspetti dei nuovi concetti introdotti in Java 8, potete consultare la pagina della per consultare tutti i post che abbiamo pubblicato sull’argomento. Nei prossimi post vedremo un esempio pratico di utilizzo per capire come mettere insieme i vari pezzi. A presto!