Java Stream: le basi

J

In questo articolo analizzaremo una della funzionalità più interessanti introdotte con l’arrivo di Java 8, gli Stream.

L’oggetto principale al quale ruota tutto è rappresentato dall’interfaccia Stream<T>  contenuta nel package java.util.stream che comprende una serie di classi per gestire delle sequenze di elementi.

Creazione

La creazione di uno Stream può avvenire in molti modi, da una collection, da un array, tramite un builder, con classi di utilità, ecc… vediamo quindi in questo capitolo quanti e quali sono i modi per fare tutto ciò.

Collection e array

Nel codice seguente verrà creato uno Stream da un array, da una serie di elementi e da una collection. In particolare in quest’ultimo caso ricordiamo che il metodo stream() è definito sull’interfaccia Collection quindi sarà fruibile da tutte le classi che la estendono ( ad esempio List o Set ) e da tutte quelle che la implementano ( ad esempio TreeSet, ArrayList, Vector, Stack ).

// Array -> Stream
String[] coloriArray = new String[]{"bianco", "rosso", "giallo", "blu", "verde"};
Stream<String> stream = java.util.Arrays.stream(coloriArray);
    
// Stream da una lista di elementi
stream = Stream.of("bianco", "rosso", "giallo", "blu", "verde");
    
// Collection -> Stream
Collection<String> coloriList = java.util.Arrays.asList(coloriArray);
stream = coloriList.stream();
Empty

Il metodo empty() di Stream ci permette di creare uno Stream vuoto, solitamente utilizzato nel return di un metodo come valore di ritorno al posto di null.

Stream<String> stream2 = Stream.empty();
Builder

L’utilizzo può essere in qualche modo paragonato a quello dello StringBuilder, si andranno quindi ad aggiungere i valori desiderati uno alla volta terminado il tutto con la chiamata al metodo build() che  come si può intuire si prenderà cura di generare lo Stream e restituirlo.

Una cosa molto importante da ricordare è che è necessario specificare la tipizzazione altrimenti verrà creato automaticamente uno Stream<Object>.

Stream<String> streamBuilder = Stream.<String>builder().add("bianco").add("rosso").add("giallo").add("blu").add("verde").build();
Generate

Il metodo generate() accetta un Supplier<T> per la generazione degli elementi. Va ricordato che verrà generato uno Stream di grandezza infinita quindi sarà compito del programmatore impostare un limite raggiunto il quale la generazione verrà interrota. Nell’esempio seguente utilizzeremo il metodo per ottenere uno Stream di 10 numeri random.

Stream<Double> streamRandom = Stream.generate(new java.util.Random()::nextDouble).limit(10);
Iterate
Stream<Integer> streamNumeriPari = Stream.iterate(2, n -> n + 2).limit(10);

In questo caso verrà creato uno Stream di 10 numeri pari che parte dal primo parametro ( 2 ) fino a 20.

Tipi primitivi

Non essendo possibile tipizzare lo Stream con tipi primitivi ( int, long e double ) Java 8 mette a disposizione 3 interfacce IntStream, LongStream e DoubleStream per la creazione di questi tipi dato.

IntStream intStream = IntStream.range(1, 10);
LongStream longStream = LongStream.range(1, 10);
DoubleStream longStream = intStream.asDoubleStream()

Il metodo range(int startInclusive, int endExclusive) include l’elemento di partenza ed esclude quello di arrivo mentre il metodo rangeClosed(int startInclusive, int endInclusive) include entrambi i parametri.

Java 8 non fornisce un’interfaccia CharStream per la gestione dei char che possono essere invece gestiti com IntStream. La stessa class String possiede in metodo chars() che restituisce uno Stream dei caratteri presenti

IntStream charsStream = "rosso".chars();

 

Stringhe

Per quanto riguarda le stringhe vale la pena ricordare che anche la classe java.util.regex.Pattern può essere molto utile per generare degli stream in quando è stato aggiunto il metodo splitAsStream().

Stream<String> stringStream = Pattern.compile(",").splitAsStream("bianco,rosso,giallo,blu,verde");

In questo caso tramite un’espressione regolare è stato possibile eseguire uno split sulla stringa di input per ottenere uno Stream con i vari token.

Anche Java NIO introduce delle novità per quanto riguarda gli Stream. In particolare il metodo lines() della classe Files ci consente di ottenere uno Stream di stringhe per ogni riga presente

Stream<String> stringStream = Files.lines(Paths.get("/path/of/file.txt"));

Operazioni sugli stream

Ora che abbiamo capito in che modo possiamo creare o ricavare uno Stream possiamo vedere le principali operazioni che possiamo eseguire per capirne meglio tutte le potenzialità che ci offrono.

Tutte le operazioni disponibili si suddividono i 2 categorie:

  • Intermediate operation, cioè tutte quelle operazioni che danno come risultato uno Stream<T>.
    E’ molto importante ricordare che tutte queste operazioni sono lazy, vengono quindi eseguite solo se sono necessare per eseguire una terminal operation.
    Altro aspetto importante da tenere in considerazione è che, restituendo uno Stream<T>, sono concatenabili, potranno quindi a loro volta eseguire un’altra intermediate operation.
  • Terminal operation, ovvero tutte quelle operazioni che restituiscono un tipo definito.
Stream.of("verde", "bianco", "rosso", "verde", "giallo", "blu", "verde")
      .distinct()
      .sorted()
      .collect(Collectors.toList());

Ecco quello che avviene riga per riga nell’esempio riportato:

  1. il metodo of genera uno Stream con le stringhe passate come parametro
  2. distinct(), intermediate operation, restituisce lo Stream rimuovendo gli elementi uguali basandosi sul metodo equals()
  3. sorted(), intermediate operation, esegue il sort dei valori nel natural order
  4. collect(), terminal operation, restituisce tutti gli elementi in un oggetto di tipo List

A fine esecuzione il contenuto della lista ottenuta sarà: bianco,blu,giallo,rosso,verde

Giunti alla fine della teoria ecco che possiamo passare a quello che molti di voi si stanno chiedendo da inizio articolo ovvero, ma cosa possiamo fare con gli Stream? In questo primo articolo andremo a esaminare negli aspetti principali le operazioni di Iterating, Filtering, Mapping e Collecting. Vedremo poi nel prossimo articolo in modo più approfondito tutti gli aspetti di ogni operazione per capirne meglio il funzionamento e l’utilizzo pratico.

Iterating e Matching

L’aspetto forse più conosciuto degli Stream è appunto quello di poter semplificare il codice quando si tratta di eseguire cicli e filtri che a volte rendono il tutto pesante da vedere e molto prolisso. Nell’esempio seguente, data una lista di numeri viene restituito true se esiste un numero maggiore di 0, false altrimenti.

List<Integer> numbers = Arrays.asList(-2,0,10,15,3,-6);

Senza l’uso di Stream:

for (Integer integer : numbers) {
    if(integer > 0) {
        return true;
    }
}

Utilizzando uno Stream

return numbers.stream().anyMatch(numb -> numb > 0);

Come possiamo notare oltre che ad aver ridotto le linee di codice ne abbiamo migliorato di molto la leggibilità. Per eseguire la validazione sugli elementi dello Stream, vengono forniti i metodi anyMatch, allMatch e noneMatch. Sono tutte terminal operation che restituisco un valore boolean e il loro nome fa capire subito che tipo di match viene eseguito.

Filtering

Il metodo filter() consente di escludere tutti gli elementi che non soddisfano il Predicate. Essendo una intermediate operation può essere utilizzata più volte per eseguire filtri in serie.  Ecco un esempio in cui data una lista di Integer vogliamo avere solo quelli non nulli, maggiori di 0 e pari

List<Integer> numbers = Arrays.asList(-2, 0, null, 10, 15, 3, -6, 12);

Prima di Java 8 avremo dovuto scrivere ad esempio

ListIterator<Integer> iter = numbers.listIterator();
while(iter.hasNext()){
    Integer numb = iter.next();
    if(numb == null || numb <= 0 || numb % 2 != 0){
        iter.remove();
    }
}

con gli Stream

numbers.stream().filter(Objects::nonNull).filter(numb -> numb > 0).filter(numb -> numb % 2 == 0);
Mapping

I metodi di mapping consentono di poter applicare una funzione all’elemento dello Stream per trasformarlo in un altro. Nel prossimo esempio utilizzeremo uno Stream di Integer dove ogni elemento rappresenta il numero del mese, in java i mesi partono da 0 (gennaio) e arrivano a 11 (dicembre), e il metodo map eseguirà la conversione da numero a parola.

Stream<Integer> mesiIntStream = Stream.iterate(0, n -> n + 1).limit(12);
Stream<String> mesiStringStream = mesiIntStream.map(numb -> new DateFormatSymbols().getMonths()[numb]);

Molto spesso dimenticato ma dalla utilissima funzione è il metodo flatMap() che consente di ottenere uno Stream di una proprietà di un elemento che è una sequenza di elementi. Vediamo di chiarire con un esempio pratico questo concetto.

Partiamo dall’oggetto Marca definito in questo modo

public class Marca {
        
    private String nome;
    private List<String> modelli;

    public Marca(String nome, List<String> modelli) {
        super();
        this.nome = nome;
        this.modelli = modelli;
    }

    public String getNome() {
        return nome;
    }

    public List<String> getModelli() {
        return modelli;
    }
}

La proprietà modelli è una lista di stringhe e se il nostro scopo fosse quello di avere uno Stream di tutti i modelli di una lista di marche ecco che il metodo flatMap() si adatta perfettamente al nostro scopo.

List<String> marche = new ArrayList<>();
marche.add(new Marca("Fiat",Arrays.asList("Panda","Punto","500")));
marche.add(new Marca("Renault",Arrays.asList("Clio","Twingo")));
marche.add(new Marca("Ford",Arrays.asList("Focus","C-Max")));

Stream<String> stream = marche.stream().flatMap(marca -> marca.getModelli().stream());

Esistono molte varianti dei metodi map() e flatMap() che qui non andremo ad analizzare perchè di facile comprensione, tutti consultabili sulle API dell’interfaccia Stream.

Collecting

Arrivati a questo punto capisco che la domanda fondamentale sia: ma di questo Stream cosa me ne faccio? Domanda più che lecita a cui la terminal operation collect() dà una risposta.

Attraverso questa operazione potremo convertire uno Stream in un qualsiasi tipo dato che ci occorre. Per la conversione Java ha messo a disposizione la classe Collectors che ci offre una serie di metodi per la soluzione dei casi più utilizzati. Partendo dallo Stream

Stream<String> stream = Stream.of("bianco", "rosso", "giallo", "blu", "verde");

vediamo i principali tipi di collect:

List e Set

List<String> listColori = stream.filter(colore -> colore.contains("b")).collect(Collectors.toList());
Set<String> setColori = stream.filter(colore -> colore.contains("b")).collect(Collectors.toSet());

In questo caso abbiamo ottenuto rispettivamente una List ed un Set di tutti i colori che contengono la lettera “b”.

String

String colori = stream.filter(colore -> colore.contains("b")).collect(Collectors.joining(","));

Utilizzando il meotodo joining() abbiamo ottenuto una stringa di tutti i colori che contengono la lettera “b” separati dal carattere “,”.

Map

Map<Integer, List<String>> mapColori = stream.collect(Collectors.groupingBy(String::length));

Ecco che possiamo apprezzare in questo esempio l’estrema facilità con cui in una sola riga di codice abbiamo ottenuto una Map dove i colori sono stati raggruppati per il numero di lettere che li compongono. Il risulato sarà quindi una mappa con questi valori:

{3=[blu], 5=[rosso, verde], 6=[bianco, giallo]}

 

Conclusioni

In questo articolo abbiamo fatto un piccolo tour per quanto riguarda le Stream API introdotte da Java 8. Diciamo che si sentiva proprio l’esigenza di dare una “svecchiata” alla programmazione in Java con notivà come questa e altre ( vedi Optional, metodi su interfacce, nuove api per date-time, ecc… ).

Quello che abbiamo affrontato è stato solo un accenno basilare che serve per poter comprendere cosa sono gli Stream e come utilizzarli. Nel prossimo articolo cercheremo di approfondire e analizzare più in dettaglio l’uso per riuscire a comprenderne il loro utilizzo al 100%.

A proposito di me

Gianluca Fattarsi

Perito informatico, esperienza come programmatore nell'ambito lavorativo dal lontano 2002. La sua formazione in Java spazia dal Front-End al Back-End con esperienze in gestione database e realizzazione di App Android in Kotlin. Hobby preferiti: Linux, lettura e World of Warcraft.

I nostri Partner

Gli articoli più letti

Articoli recenti

Commenti recenti