Lambdache?
Quindi, cosa sono le lambda expression? Definiamo la lambda expression come funzioni anonime, sì funzioni come quelle del linguaggio C, per capirsi e anonime: senza, cioè, una dichiarazione che le dia un nome. Abbiamo, quindi, un nuovo approccio al codice, possiamo cioè scrivere codice funzionale: passare funzioni a funzioni così come adesso passiamo oggetti ad oggetti, restituire funzioni da funzioni, come adesso restituiamo oggetti a partire da oggetti ma soprattutto possiamo usare oggetti e funzioni assieme semplificando il codice. Detta così non è ancora chiara la portata della novità, per cui codice, codice, codice! Partiamo da come scriviamo il codice oggi e arriviamo a come si può scrivere con Java 8.
Lambda expressions are cool
In questo post faremo un unico esempio che andremo via via a modificare introducendo le lambda expression: abbiamo una lista di stringhe e vogliamo stamparla su console. Scriviamo il primo codice che ci viene in mente:
package it.cosenonjaviste.lambda;
import java.util.*;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
for (int i = 0; i < strings.size(); i++) {
System.out.print(strings.get(i));
}
}
}
Quanto abbiamo scritto fa quello che ci aspettiamo, ma con l’uso dei generics possiamo fare di meglio:
package it.cosenonjaviste.lambda;
import java.util.*;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
for (String element : strings) {
System.out.print(element);
}
}
}
Questo codice è sicuramente familiare, perché lo usiamo da anni e a colpo d’occhio sappiamo cosa fa. Siamo sicuri però che sia il codice più semplice da scrivere e quello più performante? Prima di procedere riflettiamo per un attimo su quello che abbiamo di fronte, siamo davanti ad un caso di iterazione esterna, cioè stiamo dicendo alla collezione di stringhe: dammi una stringa, poi dammene un’altra e un’altra ancora, è il codice client che chiede alla lista un elemento alla volta e poi decide cosa farne, in pieno stile imperativo.
Iterazione interna
L’interazione interna invece, è tutta un’altra storia e mi permetto di prendere in prestito l’esempio di Mario Fusco al JavaOne per far capire la differenza tra i due. L’iterazione esterna è un po’ come far riordinare i giochi alla propria figlia piccolina:
“prendi quella palla laggiù”
“e adesso?”
“mettila nella cesta dei giochi”
“e adesso?”
“prendi quella bambola qui”
“e adesso?”
“mettila nella cesta dei giochi”
e adesso?
…
L’iterazione interna invece assomiglia di più a “prendi i tuoi giochi e mettili nella cesta” (sottointeso: scegli tu l’ordine con cui farlo e se hai l’altra mano libera e passi vicino ad un giocattolo, e ti va di prenderlo, prendi anche quello). Sicuramente anche chi non è genitore apprezza la compattezza e semplicità della seconda soluzione 😉 .
Cosa ci offre Java 8 per usare questo tipo di iterazione nel caso visto sopra? Il nuovo metodo forEach:
1
2
void forEach(Consumer super T> action)
//Performs an action for each element of this stream.
che prende in ingresso un Consumer, una nuova interfaccia di Java 8 (per la precisione è una interfaccia funzionale ma lasciamo questo argomento per un prossimo post). La cosa che ci interessa sapere ora è che questa interfaccia è come quelle che conosciamo e che ha un metodo da implementare:
1
void accept(T t) //Performs this operation on the given argument.
Scriviamo quindi una inner class anonima che implementa Consumer e la passiamo direttamente al forEach.
package it.cosenonjaviste.lambda;
import java.util.*;
import java.util.function.Consumer;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
strings.forEach(new Consumer() {
public void accept(String s) {
System.out.print(s);
}
});
}
}
Il risultato non cambia, ma quello che abbiamo fatto è un cambio di paradigma, non diciamo più alla lista come produrre il risultato, ma cosa voglia che venga fatto su ogni elemento della collezione, ci concentriamo meno sul modo per farlo ma su cosa vogliamo ottenere. Un altro vantaggio è che l’iterazione è nascosta nell’implementazione. Non solo non rischiamo più di commettere errori nella costruzione del ciclo, ma l’implementazione è polimorfica in base alla classe a cui si applica il metodo. Di conseguenza, l’iterazione interna può essere ottimizzata a seconda del tipo di struttura dati e, quando richiesto e dove applicabile, può essere svolta in parallelo.
Basta un poco di zucchero e..
Tuttavia, il codice che abbiamo adesso non sembra così invitante: abbiamo scritto molte più righe per ottenere lo stesso risultato di prima. Fortunatamente, possiamo sostituire la inner class con una lambda expression, liberandoci di tutto il codice in più e, di fatto, scrivendo tutto in un’unica riga.
import java.util.*;
import java.util.function.Consumer;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
strings.forEach((String s) -> System.out.print(s));
}
}
Adesso sì che abbiamo qualcosa di un po’ più “alieno” sotto gli occhi. Esaminiamo la nostra lambda (=funzione anonima) con attenzione. Una funzione ha un nome, una lista di parametri, un corpo e un valore ritornato. Abbiamo la lista di parametri (String s) separata dal corpo (System.out.print(s)) con la freccia ->. Il tipo di ritorno viene dedotto dal contesto, in questo caso è void. Il nome invece non c’è: era anonima la inner class prima, è anonima la funzione adesso. Ora la definizione di lambda che abbiamo dato all’inizio ha più senso.
Quindi, dove possiamo utilizzare le lambda? Dovunque ci sia una interfaccia che preveda tra i suoi metodi uno e un solo metodo astratto. Beh, ma le interfacce non hanno implementazioni, tutti i metodi di una interfaccia sono astratti per definizione, direte. No, da Java 8 è possibile avere interfacce che hanno dei metodi implementati (detti di default), anche questo argomento lo lasciamo per un prossimo post.
Meno verboso, meno noioso
Si può fare ancora di meglio, dal contesto il compilatore sa che stiamo applicando la nostra lambda ad una collezione di stringhe attraverso la type inference, quindi possiamo semplificare la funzione omettendo il tipo per s.
package it.cosenonjaviste.lambda;
import java.util.*;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
strings.forEach(s -> System.out.print(s));
}
}
Ora la nostra lambda è talmente semplice che.. si può semplificare ancora! Dato che l’unica cosa che facciamo al suo interno è chiamare un metodo, possiamo usare un method reference al posto della nostra espressione. Anche questa è una novità introdotta dalla versione 8. Possiamo pensare i method reference come delle lambda expression che non sono anonime, ma che si riferiscono ad un specifico metodo di una determinata classe (o di una istanza). Ad esempio, String::valueOf o Integer::compare sono esempi di method reference per metodi statici. Detto questo, non ci resta che usare questo nuovo costrutto nel nostro codice e modificare il nostro esempio per l’ultima volta.
package it.cosenonjaviste.lambda;
import java.util.*;
public class SevenMinutesLambda {
public static void main(String[] args) {
List strings = Arrays.asList(“Lambda “, “expressions “, “are “, “cool”);
strings.forEach(System.out::print);
}
}
Il passaggio alle lambda è completo ora, siamo passati da un ciclo for, classico esempio di iterazione esterna, a una iterazione interna gestita attraverso una funzione anonima o un method reference, sicuramente un bel po’ della verbosità di Java è stata eliminata a vantaggio della chiarezza e semplicità.