Comportamento da passare in giro
Una lambda expression è un blocco di codice compatto che puoi passare a un metodo, salvare in una variabile o restituire, proprio come faresti con un numero o una stringa. Prima delle lambda, passare comportamento significava scrivere un'intera classe (o una prolissa classe anonima) solo per racchiudere un metodo. Una lambda riduce tutto all'essenziale: i parametri e il corpo.
Una lambda implementa sempre un'interfaccia funzionale, cioè un'interfaccia con esattamente un metodo astratto (le hai incontrate alla fine della pagina sulle interfacce). Il compilatore capisce dal contesto a quale interfaccia ti riferisci e associa la tua lambda a quell'unico metodo.
x -> x * 2 è l'intera implementazione di apply. Niente new, niente corpo di classe, niente nome di metodo: a tutto questo pensa l'interfaccia.
La sintassi con la freccia
Ogni lambda ha la forma parametri -> corpo. I pezzi si adattano in base a quello che ti serve:
() -> 42 // nessun parametro
x -> x + 1 // un parametro, parentesi facoltative
(x, y) -> x + y // due o più parametri richiedono le parentesi
(int x, int y) -> x + y // i tipi sono facoltativi: di solito vengono dedotti
x -> { // un corpo a blocco richiede graffe e return
int doubled = x * 2;
return doubled + 1;
}
Un corpo formato da una sola espressione (x -> x + 1) restituisce implicitamente il suo valore, senza la parola chiave return. Appena usi le graffe, stai scrivendo un normale blocco e devi usare return in modo esplicito se il metodo restituisce qualcosa. Un errore comune è mescolare le due forme: x -> { x + 1 } non compila, perché un blocco richiede un'istruzione (return x + 1;).
Le lambda sostituiscono le classi anonime
Il modo più chiaro per capire cosa ti offre una lambda è il confronto prima/dopo. Ordinare con un Comparator personalizzato un tempo si scriveva così:
// prima: classe anonima
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
La stessa cosa con una lambda è una sola riga leggibile:
Comparator è un'interfaccia funzionale (il suo unico metodo astratto è compare), quindi la lambda si inserisce direttamente. Tutta la cerimonia (il new, la classe, la firma del metodo) sparisce e resta solo la logica di confronto.
La cassetta degli attrezzi di java.util.function
Raramente devi dichiarare una tua interfaccia funzionale. Il package java.util.function fornisce le forme più comuni, e quasi tutte le API della libreria Java le accettano:
Function<T, R>: riceve unT, restituisce unR(apply)Predicate<T>: riceve unT, restituisce unboolean(test)Supplier<T>: non riceve nulla, restituisce unT(get)Consumer<T>: riceve unT, non restituisce nulla (accept)
Sono interfacce generiche: Function<String, Integer> riutilizza i generics visti nella pagina precedente per mantenere la sicurezza dei tipi. Scegli quella la cui forma corrisponde a ciò che il tuo codice deve ricevere e produrre.
Method reference
Quando una lambda non fa altro che chiamare un metodo esistente, puoi sostituirla con un method reference usando ::. È lo stesso valore, scritto in modo più diretto:
I method reference esistono in diverse varianti: String::toUpperCase (un metodo di istanza chiamato su ogni argomento), Math::max (un metodo statico), System.out::println (un metodo di un oggetto specifico) e ArrayList::new (un costruttore). Usane uno solo quando si legge con chiarezza: se devi pensarci su per capire quale forma si applica, una semplice lambda va benissimo.
Catturare le variabili
Una lambda può usare le variabili locali del metodo che la circonda, ma solo se sono final o effectively final: assegnate una volta e mai modificate. Java cattura il valore nel momento in cui la lambda viene creata, quindi una variabile che potrebbe cambiare in seguito sarebbe ambigua.
Se riassegni factor in qualsiasi punto, il compilatore rifiuta la lambda con "variable used in lambda expression should be final or effectively final". Quando ti serve davvero uno stato mutabile condiviso, cattura invece un oggetto (un campo, un elemento di un array o un AtomicInteger), perché il riferimento resta fisso anche se il contenuto cambia. Nota che le lambda, a differenza delle classi anonime, non creano un proprio ambito: this dentro una lambda si riferisce all'istanza che la contiene, non alla lambda stessa.
Prossimo argomento: gli stream
Le lambda sono il mattone di base, non il punto d'arrivo. Il loro vero vantaggio arriva con la Streams API, dove concateni operazioni come filter, map e reduce (ognuna riceve una lambda) per esprimere le trasformazioni dei dati come una pipeline leggibile invece che come un groviglio di cicli. È l'argomento della pagina successiva.
Domande frequenti
Cos'è una lambda expression in Java?
Una lambda è un modo breve per scrivere un'istanza di un'interfaccia funzionale, cioè un'interfaccia con un solo metodo astratto. Invece di un'intera classe anonima, scrivi parametri -> corpo. Il compilatore associa la lambda all'unico metodo dell'interfaccia, quindi Runnable r = () -> System.out.println("hi"); è un Runnable completo. Le lambda ti permettono di passare comportamento da una parte all'altra come se fossero dati.
Qual è la differenza tra una lambda e un method reference in Java?
Entrambi producono un'istanza di un'interfaccia funzionale. Una lambda ha parametri espliciti e un corpo (s -> s.toUpperCase()), mentre un method reference è una forma abbreviata di una lambda che si limita a chiamare un metodo esistente (String::toUpperCase). Usa un method reference quando la lambda non farebbe altro che inoltrare i suoi argomenti a un singolo metodo con nome: è più corto e si legge meglio.
Perché le variabili usate in una lambda Java devono essere final o effectively final?
Una lambda può catturare le variabili locali del metodo che la contiene, ma solo se non cambiano mai dopo l'assegnazione: è questo il significato di "effectively final". Java cattura il valore, non un riferimento vivo alla variabile, quindi permettere la riassegnazione sarebbe ambiguo e poco sicuro tra thread diversi. Se ti serve uno stato mutabile condiviso, usa invece un campo oppure un contenitore come un array o un AtomicInteger.