2. Programmazione funzionale
Interfacce Funzionali
Partiamo con un esempio:
class Person{
private String name;
private int age;
...
int getAge(){
return age;
}
}
List<Person> list = ...
list.add(...);
Ho una lista di persone: come posso ordinarla in modo tale da avere le persone ordinate in base alla data di nascita?
Posso usare il metodo Collections.sort()
, che è definito come public static <T> void sort(List <T> list, Comparator <? super T> c)
.
A questo punto la lista ce l’ho, devo solo definire il comparator:
MyComparator c = new MyComparator();
Collections.sort(list, c);
class MyComparator implements Comparator<Person> {
@Override
public int compare(Person p1, Person p2) {
if (p1.getEta() < p2.getEta()) return -1;
else if (p1.getEta() > p2.getEta()) return 1;
else return 0;
}
}
La classe MyComparator
implementa una singola funzione che codifica il significato di fare il confronto tra due elementi: possiamo dire che il metodo sort()
è parametrico rispetto alla funzione di confronto e che MyComparator
rappresenta questa funzione.
Possiamo usare anche una classe anonima, invece di definire la classe MyComparator
.
Collections.sort(list, new Comparator<Person>()) {//Anonymous class:
@Override
public int compare(Person p1, Person p2) {
if (p1.getEta() < p2.getEta()) return -1;
else if (p1.getEta() > p2.getEta()) return 1;
return 0;
}
}
Le interfacce con un solo metodo, come Comparator, vengono dette interfacce funzionali.
Espressioni Lambda
In Java è anche possibile usare le espressioni lambda per definire il metodo di un’interfaccia funzionale.
Riprendendo l’esempio di prima, posso direttamente passare una funzione lambda al metodo Collections.sort()
.
Collections.sort(list, (p1, p2) -> {
if (p1.getEta() < p2.getEta()) return -1;
else if (p1.getEta() > p2.getEta()) return 1;
else return 0;
});
Sintassi delle espressioni lambda
Le espressioni lambda possono essere scritte specificando il tipo di dato dei parametri della funzione, ad esempio:
obj.method(String name -> name.doSomething());
// Oppure
obj.methos(final String name -> name.doSomething());
Oppure possono essere scritte senza il tipo, lasciando l’operazione di inferenza di tipo al compilatore:
obj.method(name -> name.doSomething());
obg.method(final name -> name.doSomething()); // NO!! Questo è illegale
Comunque venga scritta l’espressione lambda, l’input è immutabile.
I valori di ritorno possono essere impliciti nel caso in cui la funzione esegua solo un’operazione:
obj.method((par1, par2) -> par1.foo(par2)); // È come se ci fosse scritto return par1.foo(par2)
Nel caso in cui la funzione esegua più di un’operazione è necessario specificare il valore di ritorno.
obj.doSomething(final String name -> {
if (name.equals("Boo"))
return "Foo";
else
return name; });
Thread in programmazione funzionale
// Versione con la classe anonima
Runnable r = new Runnable(){
@Override
public void run(){
doSomething();
}
}
Thread t = new Thread(r);
t.start();
// Versione con l'espressione lambda
Runnable r = () -> doSomething();
Thread t = new Thread(r);
t.start();
// Oppure
Thread t = new Thread(() -> doSomething());
t.start();
Optional<T>
Il tipo Optional<T> permette di fare operazioni su dati, ignorando il fatto che alcuni dati potrebbero essere assenti (null).
Per creare un opzionale si può usare uno dei seguenti metodi:
Optional.of(<value>)
, crea un opzionale con il valore specificato da value.
Optional.empty()
, crea un opzionale vuoto
public class Address {
final String via;
final Optional<String> comments;
public Address(String street, String comments) {
this.via = street;
this.comments = Optional.of(comments); //creates an Optional with the value of comments
}
public Address(String via) {
this.via = via;
this.comments = Optional.empty(); //creates an empty Optional. i.e., replaces comments=null;
}
...
public Optional<String> getComments() {
return comments;
}
}
Come accedo al valore di un opzionale Optional<T> o
?
o.get()
o.ifPresent(<lambda_expression>)
: prende in input una funzione che accetta il tipo T. La funzione viene invocata solo se l’opzionale non è vuoto.
o.flatMap(<lambda_expression>)
: prende in input una funzione che accetta il tipo T e che ritorna un Optional<U>. Seo
è vuoto la funzione ritorna empty, sennò viene invocata la funzione.
o.orElse(T val)
: seo
non è vuoto ritorna il suo valore, sennò ritornaval
.
Stream<T>
Le stream permettono di concatenare funzioni che agiscono su collezioni.
A partire da una Collection<T>
, il metodo stream()
ritorna una Stream<T>
.
Metodo count(): conta gli elementi dello stream.
Metodo forEach(): prende in input una funzione che fa qualcosa con ogni elemento dello stream. Generalmente si usa per stampare le stream.
List<String> list = ...
list.stream()
.forEach(x -> System.out.println(x));
// Oppure
list.stream()
.forEach(System.out::println);
Metodo filter(): prende in input un predicato e ritorna una stream che contiene solo gli elementi per i quali il predicato è vero.
List<Integer> list = ...
list.stream()
.filter(x -> x%2 == 0)
.forEach(x -> System.out.println(x));
Metodo map(): prende in input una funzione che accetta il tipo T
e la applica ad ogni elemento, ritornando una Stream<U>
.
List<String> list = ...
list.stream()
.map(x -> x.size())
.forEach(x -> System.out.println(x));
Metodo distinct(): elimina i duplicati da uno stream
Metodo sort(): ordina gli elementi di uno stream; usa l’ordine naturale per variabili intere e stringhe, ha bisogno di un Comparator negli altri casi.
Metodo flatMap(): prende in input una funzione che accetta il tipo T
, la applica a tutti gli elementi dello stream, producendo una Stream<U>
per ogni elemento, e infine unisce tutte le Steam<U>
.
Metodo limit(): prende in input un numero e ritorna una stream formata da i primi x elementi della stream originale.
Metodo sum(): fa la somma di tutti gli elementi di una stream di interi
Metodo average(): fa la media di tutti gli elementi di una stream di interi
Metodo reduce(): permette di operare una riduzione, cioè un’operazione che raccoglie un valore unico da una stream. I metodi per ricavare la somma e la media sono esempi di riduzione. È possibile creare metodi di riduzione custom, fornendo alla funzione un punto di partenza e l’operazione da eseguire ad ogni step.
Integer sum = list
.stream()
.flatMap(x -> x.size())
.reduce(0, (a, b) -> a + b);
Metodo collect(): simile a reduce, prende in input tre funzioni: supplier, accumulatore e combinatore.
List< String> sentence = ...
List<String> wordsConA =
sentence.stream()
.filter(word -> word.startsWith("a"))
.collect(ArrayList::new,
ArrayList::add,
ArrayList::addAll);
Metodo parallel(): consente di far eseguire le operazioni sulle stream in parallelo.
java.utils.function
Function<T,R>
: funzione che ha come parametro il tipo T e come ritorno il tipo R
BiFunction<T, U, R>
: funzione che ha come parametri i tipi T ed U e come ritorno il tipo R
Consumer<T>
: funzione che ha come parametro il tipo T e non ha valori di ritorno
Predicate<T>
: funzione che ha come parametro il tipo T e ha un booleano come valore di ritorno
Chiusure
Supponiamo di voler usare un’espressione lambda più volte: è possibile usare un Predicate<T>
, ad esempio:
Predicate<String> pred = s -> s.startsWith("L");
...
list.stream()
.filter(pred)
.forEach(x -> System.out.println(x));
Ma se volessi generalizzare l’esempio in modo tale che il predicato ci dica quali sono le stringhe che iniziano con un generico prefisso?
Posso creare un metodo che ritorna un’espressione lambda parametrizzata:
public static Predicate<String> pred(String prefix){
return s -> s.startsWith(prefix);
}
...
list.stream()
.filter(pred("X"))
.forEach(x -> System.out.println(x));
L’espressione lambda in questo caso viene detta chiusura.
Esempi di codici con le stream
1) Codice per trovare il valore massimo di una lista di interi.
int max = prices.stream()
.reduce(0, Math:max)
2) Codice che ritorna una lista formata da tutti i nomi che non erano completamente in maiuscolo, in maiuscolo. (Focalizzarsi sulla collect
)
public static List<String> inUpperCase(List<String> people) {
return people.stream()
.filter(x -> !(x.equals(x.toUpperCase())))
.map(x -> x.toUpperCase())
.collect(Collectors.toList());
}