Utilizzo degli stream in Java.
Sebbene nasca come un linguaggio orientato agli oggetti, nel corso degli anni anche Java ha cercato di integrare paradigmi di programmazione diversi, al fine di dare allo sviluppatore tool più comodi per svolgere il suo lavoro.
Un aspetto fondamentale della programmazione funzionale è il trattare le funzioni come oggetti da poter passare come parametri ad altre funzioni, che vengono considerate funzioni di ordine superiore.
Quando le funzioni sono anonime, si parla di funzioni o espressioni lambda.
In Java le funzioni lambda sono definite come segue:
// Funzione che prende un intero e ritorna il triplo
Function<Integer, Integer> triple = (x) -> x * 3;
Dove 'x
' è il parametro, '->
' è l'operatore lambda e 'x * 3
' è il corpo della funzione.
Alcune varianti sono:
// Posso usare le parentesi graffe se ho necessità di più righe di codice
// In questo caso devo esplicitare il return
BiFunction<Float, Float, Float> sum = (x, y) -> { float z = x + y; return z; };
// Se non ho parametri, devo usare le parentesi tonde vuote
Runnable hello = () -> System.out.println("Hello World");
// Se ho un solo parametro, posso omettere le parentesi tonde
Predicate<String> startsWithF = s -> s.startsWith("F") || s.startsWith("f");
Le funzioni lambda sono associate a interfacce funzionali, ovvero interfacce che hanno un solo metodo astratto. Alcune delle interfacce più comuni sono:
Consumer<T>
: parametro di tipo T
e non ritorna nullaSupplier<T>
: non ha parametri e ritorna un valore di tipo T
Function<T, R>
: parametro di tipo T
e ritorna un valore di tipo R
Predicate<T>
: parametro di tipo T
e ritorna true o falseComparator<T>
: due parametri di tipo T
e ritorna un valore intero per indicare l'ordineRunnable
: non ha parametri e non ritorna nullaIn Java è possibile passare come parametro un metodo, che verrà trattato come una funzione lambda.
List<String> list = List.of("a1", "a2", "b1", "c2", "c1");
list.stream()
.map(String::toUpperCase) // Equivalente a s -> s.toUpperCase()
.forEach(System.out::println); // Equivalente a s -> System.out.println(s)
I comparatori sono funzioni che prendono in input due parametri e ritornano un valore intero, che indica l'ordine fra i due parametri.
Comparator<Integer> intComparator = (i1, i2) -> i1 - i2;
Comparator<String> stringComparator = (s1, s2) -> s1.compareTo(s2);
Per comodità, Java mette a disposizione la classe Comparator
, che sfrutta il metodo compareTo
dell'oggetto sul quale vogliamo effettuare il confronto.
// Ordina in maniera naturale (dal più piccolo al più grande)
Comparator<Double> doubleComparator = Comparator.naturalOrder();
// Ordina in maniera inversa (dal più grande al più piccolo)
Comparator<Float> floatComparator = Comparator.reverseOrder();
// Ordina gli utenti per età
Comparator<User> userComparator = Comparator.comparing(User::getAge);
Gli stream sono una nuova struttura dati introdotta in Java 8, che permette di eseguire operazioni su una sequenza di elementi, sfruttando al massimo le potenzialità della programmazione funzionale.
Una collezione può essere trasformata in uno stream tramite il metodo stream()
:
List<String> list = List.of("a1", "a2", "b1", "c2", "c1");
Stream<String> stream = list.stream();
Gli stream permettono di eseguire operazioni di trasformazione e filtraggio, che vengono eseguite in modo lazy, quindi non verranno lanciate fino a quando non verrà chiamata un'operazione terminale.
Altre operazioni, cioè quelle terminali, sono invece eager, ovvero vengono eseguite subito e forzano l'esecuzione di tutte le operazioni precedenti.
Alcune operazioni, come la map e la filter lavorano sul singolo elemento, quindi non hanno bisogno di contenere uno stato interno per funzionare.
Questo tipo di operazioni è detta stateless.
Altre operazioni, come la sorted o la max, invece hanno bisogno di contenere uno stato interno, per fare confronti fra gli elementi.
Questo tipo di operazioni è detta stateful.
Il metodo filter è un'operazione lazy stateless che prende in input un predicato e restituisce un nuovo stream contenente solo gli elementi che soddisfano il predicato.
List<String> list = List.of("a1", "a2", "b1", "c2", "c1");
Stream<String> stream = list.stream()
.filter(s -> s.startsWith("c"));
// stream contiene solo "c2" e "c1"
streamifilter()streamfa1↓Xa2↓Xb1↓Xc2↓✓c2c1↓✓c1
Il metodo map è un'operazione lazy stateless che prende in input una funzione e restituisce un nuovo stream contenente gli elementi trasformati dalla funzione.
List<String> list = List.of("a1", "a2", "b1", "c2", "c1");
Stream<String> stream = list.stream()
.map(s -> s.toUpperCase());
// stream contiene "A1", "A2", "B1", "C2", "C1"
streamimap()streamfa1↓toUpperA1a2↓toUpperA2b1↓toUpperB1c2↓toUpperC2c1↓toUpperC1
Il metodo flatMap è un'operazione lazy stateless che prende in input una funzione e restituisce un nuovo stream contenente gli elementi trasformati dalla funzione, che devono essere a loro volta stream.
List<List<String>> list = List.of(List.of("a1", "a2"), List.of("b1"),
List.of("c2", "c1"));
Stream<String> stream = list.stream()
.flatMap(l -> l.stream());
// stream contiene "a1", "a2", "b1", "c2", "c1"
streamiflatMap()streamf[a1,a2]↓streama1↘a2b1↓streamb1[c2,c1]↓streamc2↘c1
Il metodo sorted è un'operazione lazy stateful che prende in input un comparatore e restituisce un nuovo stream contenente gli elementi ordinati secondo il comparatore.
List<Integer> list = List.of(5, 2, 6, 3, 1);
Stream<Integer> stream = list.stream()
.sorted((s1, s2) -> s1 - s2);
// stream contiene 1, 2, 3, 5, 6
Il metodo count è un'operazione eager che restituisce il numero di elementi presenti nello stream.
List<String> list = List.of("a1", "a2", "b1", "c2", "c1");
long count = list.stream()
.filter(s -> s.equals("c2"))
.count();
// count contiene 1
Il metodo collect è un'operazione eager che prende in input un collector e restituisce un nuovo stream contenente gli elementi trasformati dal collector.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> newList = list.stream()
.filter(i -> i % 2 == 0)
.collect(Collectors.toList());
// .toList(); // Versione semplificata
// newList contiene 2, 4, 6, 8
Il metodo reduce è un'operazione eager che prende in input una funzione che, a partire da un valore accumulatore, continua ad applicare un'operazione fra l'accumulatore e l'elemento corrente e restituisce il risultato dell'accumulazione.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7);
int mult = list.stream()
.reduce(1, (acc, i) -> acc * i);
// mult contiene 5040
streamireduce()acc1↓acc=112↓acc=123↓acc=264↓acc=6245↓acc=241206↓acc=1207207↓acc=7205040→mult
Il metodo max (min) è un'operazione eager che prende in input un comparatore e restituisce il massimo (minimo) elemento dello stream secondo il comparatore come un Optional.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7);
Optional<Integer> max = list.stream()
.max(Comparator.naturalOrder());
// max contiene 7
Il metodo iterate è un'operazione lazy stateless che prende in input un valore iniziale che fa da accumulatore e una funzione che, applicando la funzione al valore corrente dell'accumulatore, restituisce il valore successivo. Poiché di default proseguirebbe all'infinito, è necessario specificare un limite con il metodo limit.
Stream<Integer> stream = Stream.iterate(0, i -> i + 1)
.limit(10);
// stream contiene 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Il metodo generate è un'operazione lazy stateless che prende in input una funzione che restituisce il valore successivo. Poiché di default proseguirebbe all'infinito, è necessario specificare un limite con il metodo limit.
Stream<Integer> stream = Stream.generate(() -> Math.round(Math.random()*10))
.limit(10);
// stream contiene 10 numeri casuali
Il metodo distinct è un'operazione lazy stateful che restituisce un nuovo stream contenente gli elementi dello stream originale senza duplicati.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3);
Stream<Integer> stream = list.stream()
.distinct();
// stream contiene 1, 2, 3, 4, 5, 6, 7, 8
Il metodo forEach è un'operazione eager che prende in input una funzione e applica la funzione a tutti gli elementi dello stream.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
list.stream()
.filter(i -> i % 2 == 0)
.forEach(System.out::println);
// stampa 2, 4, 6, 8
Il metodo peek è un'operazione lazy stateless che prende in input una funzione e applica la funzione a tutti gli elementi dello stream. In altre parole, è come un forEach, ma non è un'operazione terminale.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
int count = list.stream()
.filter(i -> i % 2 == 0)
.peek(System.out::println)
.count();
// stampa 2, 4, 6, 8
// restituisce 4
I metodi anyMatch, allMatch e noneMatch sono operazioni eager che prendono in input un predicato e restituiscono true rispettivamente se:
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
boolean any = list.stream()
.anyMatch(i -> i % 2 == 0); // true
boolean all = list.stream()
.allMatch(i -> i % 2 == 0); // false
boolean none = list.stream()
.noneMatch(i -> i % 2 == 0); // false
I metodi findFirst e findAny sono operazioni eager che restituiscono il primo elemento dello stream che soddisfa il predicato. Per poi ottenere il valore dell'elemento, è necessario usare il metodo get. Se non esiste alcun elemento che soddisfa il predicato, viene restituito un Optional vuoto, che può essere gestito con il metodo orElse.
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> first = list.stream()
.filter(i -> i % 2 == 0)
.findFirst();
// first contiene 2
// Se l'optional è vuoto, viene lanciata un'eccezione
Integer firstValue = first.get();
// Se l'optional è vuoto, firstValue contiene 0
Integer firstValueDefault = first.orElse(0);
Finora abbiamo visto solo gli Stream di tipo Stream<T>
, ma esistono anche dei tipi di Stream specializzati per i tipi primitivi.
IntStream è uno stream di interi.
IntStream stream = IntStream.of(1, 2, 3, 4, 5, 6, 7, 8);
Lo si può ottenere anche da uno stream di oggetti con il metodo mapToInt.
Stream<String> stream = Stream.of("a", "bc", "def", "ghij");
IntStream intStream = stream.mapToInt(s -> s.length());
Usare IntStream invece di Stream<Integer>
ha diversi vantaggi:
sum
, average
, min
, max
.
Questi metodi non necessitano di un comparatore, perché lo stream sa già come confrontare gli elementi