1. Introduzione
In breve: un’espressione lambda — è un modo per creare rapidamente l’implementazione di un’interfaccia funzionale “al volo”, senza dichiarare una classe separata o una classe anonima. È come un mini-metodo senza nome che si può passare come argomento o salvare in una variabile.
Per molto tempo Java è stato un linguaggio OOP “classico”. Se volevi passare un pezzetto di comportamento, per esempio cosa fare alla pressione di un pulsante, bisognava scrivere classi anonime:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("Pulsante premuto!");
}
});
Con Java 8 è arrivata la magia delle lambda:
button.setOnClickListener(() -> System.out.println("Pulsante premuto!"));
Dove () -> System.out.println("Pulsante premuto!") — questa è un’espressione lambda.
Formalmente: un’espressione lambda è una forma compatta per scrivere l’implementazione dell’unico metodo astratto di un’interfaccia funzionale.
Perché è importante?
- Passare il comportamento come valore (per esempio, cosa fare con ogni elemento di una lista).
- Scrivere codice compatto e leggibile.
- Usare le API moderne di Java: Stream API, gestione degli eventi, attività asincrone e molto altro.
2. Sintassi delle espressioni lambda
Forma generale
(parametri) -> espressione
// oppure
(parametri) -> { blocco di codice }
Esempi per diverse interfacce
1. Senza parametri (per esempio, Runnable):
Runnable r = () -> System.out.println("Ciao, lambda!");
r.run();
2. Un parametro (per esempio, Consumer):
Consumer<String> printer = s -> System.out.println("Hai passato: " + s);
printer.accept("Java");
Se c’è un solo parametro e il suo tipo può essere dedotto, si possono omettere le parentesi.
Il tipo String è il tipo della variabile (s) che passiamo lì.
3. Più parametri (per esempio, Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Il tipo Integer è il tipo delle variabili (a, b) che passiamo lì.
4. Corpo su più righe (servono le parentesi graffe e return, se c’è un ritorno esplicito):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
Il primo Integer è il tipo del risultato della funzione, il secondo Integer è il tipo del parametro.
Abbreviazioni e concisione
- Se il corpo è una sola espressione, si possono omettere le parentesi e return.
- Se non ci sono parametri — si scrivono le parentesi vuote: () -> ...
- Se c’è un solo parametro — si possono omettere le parentesi: x -> ...
- Se ci sono più parametri — servono le parentesi: (a, b) -> ...
Tabella riassuntiva
Vediamo come la stessa idea si scrive con una lambda e con una classe anonima:
| Interfaccia | Espressione lambda (esempio) | Equivalente di classe anonima |
|---|---|---|
|
|
|
|
|
|
|
|
|
Esempio con un’interfaccia personalizzata
Supponiamo di avere un’interfaccia funzionale:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Prima si implementava così:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
Ora — in modo moderno:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Esempi per le interfacce standard
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("mela", "banana", "kiwi");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. Ambito e cattura delle variabili
Variabili dal contesto esterno (effectively final)
Le espressioni lambda possono usare variabili dal metodo circostante. Ma c’è una regola: tali variabili devono essere effectively final — cioè o dichiarate esplicitamente come final, oppure semplicemente non modificate dopo l’inizializzazione.
Esempio:
String prefix = "Risultato: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix qui è "congelato" - dopo di ciò non si può modificare
System.out.println(f.apply(5)); // Risultato: 10
Se provi a modificare prefix dopo che è stato usato nella lambda, il compilatore segnalerà un errore.
Perché?
Un’espressione lambda può essere invocata dopo l’uscita dal metodo in cui è stata dichiarata la variabile. Per evitare bug “magici”, Java consente di usare solo variabili immutabili.
Differenza rispetto alle classi anonime
Nelle classi anonime vale la stessa regola: le variabili dal metodo esterno devono essere final/effectively final. Ma ci sono differenze di ambito: dentro una classe anonima this si riferisce alla classe anonima stessa, mentre nella lambda — alla classe esterna.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Stamperà: Demo$1 (classe anonima)
}
};
Runnable r2 = () -> System.out.println(this); // Stamperà: Demo (classe esterna)
r1.run();
r2.run();
}
}
Lambda e campi della classe
Un’espressione lambda può accedere liberamente ai campi della classe esterna:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
Qui base — si può modificare (è un campo della classe).
x — deve essere effectively final.
4. Pratica: scriviamo alcune espressioni lambda
Esempio: filtrare una lista di numeri
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Pari: " + n));
Scoprirai di più su Stream API al livello 30 :P
Esempio: funzione di trasformazione di una stringa
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Esempio: una propria interfaccia funzionale
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Ciao")); // Ciao!
Esempio: uso di variabili del contesto esterno
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor non può essere modificato dopo questo!
5. Errori tipici nell'uso delle espressioni lambda
Errore n. 1: tentativo di modificare una variabile catturata dalla lambda.
Questo codice non verrà compilato — la variabile deve essere effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Errore: sum non è final!
Se devi accumulare valori, usa un array o un oggetto wrapper.
Errore n. 2: confusione con l’ambito di this.
All’interno di un’espressione lambda this si riferisce alla classe esterna, non alla lambda (a differenza di una classe anonima).
Errore n. 3: dimenticate le parentesi graffe e return in una lambda su più righe.
Se il corpo della lambda non è una sola espressione, servono le parentesi graffe e return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Errore n. 4: definizione errata del tipo dell’espressione lambda.
Un’espressione lambda implementa sempre un’interfaccia funzionale. Non puoi semplicemente scrivere:
var f = x -> x + 1; // Errore! Non si sa quale tipo di interfaccia.
Bisogna specificare esplicitamente il tipo:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION