Utilizzo degli stream in Java.
Approccio funzionale
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.
Funzioni 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");
Tipi delle funzioni lambda
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 tipoT
e non ritorna nullaSupplier<T>
: non ha parametri e ritorna un valore di tipoT
Function<T, R>
: parametro di tipoT
e ritorna un valore di tipoR
Predicate<T>
: parametro di tipoT
e ritorna true o falseComparator<T>
: due parametri di tipoT
e ritorna un valore intero per indicare l'ordineRunnable
: non ha parametri e non ritorna nulla
Metodi passati come funzioni
In 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)
Comparators
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);
Stream
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();
Lazy vs Eager
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.
Stateless vs stateful
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.
Filter
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"
Map
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"
FlatMap
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"
Sorted
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
Count
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
Collect
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
Reduce
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
Max (Min)
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
Iterate
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
Generate
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
Distinct
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
ForEach
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
Peek
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
AnyMatch, AllMatch, NoneMatch
I metodi anyMatch, allMatch e noneMatch sono operazioni eager che prendono in input un predicato e restituiscono true rispettivamente se:
- esiste almeno un elemento che soddisfa il predicato
- se tutti gli elementi soddisfano il predicato
- se nessun elemento soddisfa il predicato
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
FindFirst o FindAny
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);
Tipi di Stream
Finora abbiamo visto solo gli Stream di tipo Stream<T>
, ma esistono anche dei tipi di Stream specializzati per i tipi primitivi.
- IntStream
- LongStream
- DoubleStream
IntStream
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());
Vantaggi di IntStream
Usare IntStream invece di Stream<Integer>
ha diversi vantaggi:
- Maneggiare gli interi è più efficiente che maneggiare gli oggetti
- Esistono metodi specializzati per i tipi primitivi:
sum
,average
,min
,max
. Questi metodi non necessitano di un comparatore, perché lo stream sa già come confrontare gli elementi