Stream
La libreria Stream di Java (introdotta con Java 8) introduce elementi di programmazione funzionale in Java. Le caratteristiche principali prese dalla programmazione funzionale sono le seguenti:
- i dati sono elaborati da funzioni deterministiche: la funzione riceve un certo input e determina un certo output, non memorizza internamente uno stato;
- quando una funzione elabora un dato, il dato originale non viene modificato, ma ne viene creata una copia con la modifica richiesta;
- quando si elabora un insieme di dati, anziché svolgere un ciclo esplicitamente sui singoli elementi, si applica una funzione su ogni elemento;
- le funzioni possono essere concatenate: l’output di una funzione diventa input di un’altra.
Alla base degli Stream c’è il concetto di pipeline. Una pipeline è un insieme di elaborazioni in sequenza su una lista di elementi, ovvero è una catena di singole elaborazioni dove l’output di una elaborazione è l’input della successiva.
Una pipeline ha quindi:
- una prima funzione di generazione
- un elenco di funzioni di trasformazione in sequenza
- una funzione terminale che genera il risultato
La pipeline, senza la sua funzione terminale, può essere salvata in una variabile (di tipo Stream<T>) e poi attivata in un secondo momento con una funzione terminale. Questo punto è fondamentale: creare uno Stream non lo attiva automaticamente, ma solo con l’operazione terminale viene eseguito.
Stream<T>
Generare uno stream
Uno Stream viene generato da una funzione di generazione. Creiamo prima di tutto delle liste di oggetti:
String[] arrayOfStrings = new String[]{"a","b","c"};
List<String> list = List.of("Rosso", "Blu", "Verde");A questo punto possiamo creare degli Stream<T> corrispondenti a queste liste:
Stream<String> streamFromArray = Arrays.stream(arrayOfStrings); // usa il metodo statico di Arrays
Stream<String> streamFromList = list.stream(); // genera da list
Stream<String> streamFromOf = Stream.of("Giallo", "Nero", "Bianco"); // genera direttamente dai valori
E’ quindi possibile generare Stream da array, liste, direttamente con of, o da stringhe.
E’ possibile genrare una lista dallo split di una stringa:
Stream<String> stringStream = Arrays.stream("bianco,rosso,verdone".split(","));E’ possibile generare una lista anche da un ciclo:
Stream<Integer> evenStream = Stream.iterate(2, n -> n+2); // 2,4,6,8,10,...iterate accetta due argomenti: il valore iniziale, e una funzione lambda che prende come argomento un parametro, calcola il valore da inserire, e poi reitera il processo col valore appena inserito come parametro della chiamata successiva.
Stream<Double> randomStream = Stream.generate(Math::random);Con questa istruzione generiamo uno stream di numeri casuali. Notare che Math::random indica come metodo di generazione il metodo statico Math.random().
Intermediate operation
Le operazioni intermedie generano un nuovo stream dal precedente. Di particolare importanza filter, map e sorted.
streamFromArray = streamFromArray.distinct(); // elimina duplicati
streamFromArray = streamFromArray.sorted(); // ordina in modo genericoE’ possibile limitare la generazione ad un certo numero di elementi:
randomStream = randomStream.limit(10); // limita la generazione a 10 elementi
evenStream = evenStream.limit(5); // solo 5 elementi pariMap esegue l’operazione di mapping: per ogni elemento della lista, esegue la trasformazione indicata nella funzione lambda ed inserisce il risultato in una nuova lista:
Stream<Integer> intRandomStream = randomStream.map(n -> (int)(n*10)+1); // converte in interi da 1 a 10Filter applica una funzione di filtro (che applica una regola e restituisce quindi True o False) e gli elementi che la soddisfano finiscono in una nuova lista:
intEventRandomStream = intRandomStream.filter(e -> e % 2 == 0); // prende solo interi pari tra 1 e 10Terminal operation
L’operazione terminale genera un output dallo stream.
Vediamo le più importanti.
AnyMatch restituisce true se la lista non è vuota, false altrimenti
Boolean found = evenStream.anyMatch(e -> e % 2 == 0);Count indica quanti elementi ci sono:
Long count = stringStream.count();toList() trasforma il risultato in una List<T>
List<String> arrayList = streamFromArray.toList();Reduce trasforma una lista di oggetti T in un singolo oggetto T. La funzione riceve due argomenti:
- un accumulatore (valore iniziale)
- una funzione di accumulazione (che inserisce nell’oggetto finale il valore del singolo elemento)
String joinString = streamFromList.reduce("", (a,e) -> a+e);
Integer sum = evenStream.reduce(0, (a,e) -> a+e);Min, max e findFirst restituiscono il minimo, massimo ed il primo elemento
Optional<Double> max = randomStream.max(Double::compareTo);
Optional<Double> min = randomStream.min(Double::compareTo);
Optional<Double> first = randomStream.findFirst()Notare Double::compareTo. E’ una notazione che consente di estrarre il nome di un metodo statico dalla classe, da usare come predicato del confronto. In questo caso è il comparatore di default.
Come si vede viene introdotto un nuovo tipo, Optional<T>.
Optional<T>
I contenitori generici di tipo Optional servono per gestire il caso in cui l’istanza di T sia inesistente. Ad esempio se uno Stream è vuoto e vogliamo estrarre il primo valore da una lista vuota, otterremo un valore inesistente. Questo potrebbe causare potenziali errori nella successiva elaborazione.
Optional consente di ottenere il valore se presente nel contenitore, o gestire la situazione in cui invece non sia presente.
Ad esempio:
Stream<Integer> randomStream = Stream.generate(Math::random)
.limit(10)
.map(n -> (int)(n*10)+1)
.filter(e -> e % 2 == 0); // prende solo interi pari tra 1 e 10
Optional<Integer> value = randomStream.findFirst();E’ opzionale perché non viene garantito che esista un valore pari.
A questo punto possiamo estrarre il valore con get(), ma gestire anche il caso di valore inesistente:
boolean present = value.isPresent(); // true se presente
boolean empty = value.isEmpty(); // true se non presente
Integer v = value.get(); // estrae il contenuto dell'Optional
oppure
Integer v = value.orElse(0); // mette 0 se inesistente
oppure
Integer v = value.orElseThrow(); // solleva una eccezione se inesistente
oppure
Integer v = value.orElseGet(() => (Integer)Math.random()*10+1); // genera un nuovo valore
Pipeline
Gli stream come visto sopra sono molto comodi perché eliminano la necessità di eseguire cicli for su ogni elemento della lista, ma applicano automaticamente la funzione passata come argomento a tutti gli elementi, evitando molto codice.
Vediamo qualche esempio:
private static Integer sum(List<Integer> list) {
return list.stream().reduce(0, (acc, value) -> acc+value);
}
Vediamo un esempio più strutturato:
public record Persona(String nome, String cognome, int eta, String sesso) {}
public static void main(String[] args) {
List<Persona> persone = List.of(
new Persona("Luca", "Rossi", 28, "M"),
new Persona("Maria", "Bianchi", 34, "F"),
new Persona("Giovanni", "Verdi", 45, "M"),
new Persona("Anna", "Russo", 22, "F"),
new Persona("Paolo", "Ferrari", 51, "M"),
new Persona("Giulia", "Esposito", 29, "F"),
new Persona("Marco", "Romano", 38, "M"),
new Persona("Chiara", "Gallo", 26, "F"),
new Persona("Stefano", "Costa", 42, "M"),
new Persona("Valentina", "Conti", 31, "F")
);
List<Persona> fU30 = persone.stream()
.filter(p -> p.sesso=="F")
.filter(p -> p.eta < 30)
.toList();
int etamedia = persone.stream()
.map(Persona::eta)
.reduce(0, (a,eta) -> a + eta) / persone.size();
}