Java 8: gli stream infiniti

Una delle nuove feature più importanti introdotte in Java 8 è senza dubbio il concetto di stream. Ne abbiamo già parlato in un post introduttivo, 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 post sugli optional 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 Practices Of An Agile Developer): Functional Programming in Java: Harnessing the Power Of Java 8 Lambda Expressions.

Fino ad adesso abbiamo visto diversi aspetti dei nuovi concetti introdotti in Java 8, potete consultare la pagina della categoria Java8 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!

Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono in Nana Bianca dove mi occupo dello sviluppo di alcune app Android. Coautore della seconda edizione di Android Programmazione Avanzata e docente di corsi di sviluppo su piattaforma Android. Follow me on Twitter - LinkedIn profile

  • Giampaolo Trapasso

    Bravo Fabio, bel post!

  • uros

    Non mi sorprende piu che proprio in italiano trovo articoli su java molto profondi ma comunque scritti con leggerezza. Tante grazie!