1. Introduzione
Diciamolo chiaramente: scrivere classi anonime per una o due righe di codice — è come noleggiare un enorme camion per trasportare dal panificio al negozio un solo panino.
Per esempio, se volete ordinare un elenco di stringhe per lunghezza, prima di Java 8 bisognava scrivere così:
List<String> list = Arrays.asList("mela", "banana", "kiwi");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Il compito sembra semplice, ma il codice occupa mezza schermata. È particolarmente fastidioso quando di operazioni simili ce ne sono tante: il codice diventa “rumoroso” e si perde l’essenza del problema. I programmatori hanno pianto, sofferto e poi hanno inventato le espressioni lambda. Le abbiamo già studiate un po’, ora ripasseremo e approfondiremo le nostre conoscenze.
Che cos’è un’espressione lambda
Un’espressione lambda è una forma compatta per scrivere l’implementazione di un’interfaccia funzionale, cioè un’interfaccia con un unico metodo astratto (ad esempio, Comparator, Runnable, Consumer e molte altre).
In parole povere, un’espressione lambda vi permette di scrivere una funzione “al volo”, proprio dove serve, senza dichiarare una classe o un metodo separato.
Sintassi generale:
(parametri) -> { corpo }
Esempi:
- (a, b) -> a + b — funzione che somma due numeri
- x -> x * x — funzione che eleva un numero al quadrato
- () -> System.out.println("Hello!") — funzione senza parametri
Relazione con le interfacce funzionali:
Un’espressione lambda può sempre essere assegnata a una variabile di tipo interfaccia funzionale o passata come argomento a un metodo che si aspetta tale interfaccia.
2. Sintassi delle espressioni lambda
Senza parametri
Runnable r = () -> System.out.println("Ciao, mondo!");
r.run(); // Stamperà: Ciao, mondo!
Un parametro
Se il parametro è uno solo, le parentesi si possono omettere:
Consumer<String> print = s -> System.out.println(s);
print.accept("Java è fantastica!");
Più parametri
Le parentesi sono obbligatorie:
Comparator<String> cmp = (a, b) -> a.length() - b.length();
Corpo con una sola espressione
Se il corpo è composto da una sola espressione, non servono le parentesi graffe né return:
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Corpo a blocco
Se servono più istruzioni, usate le parentesi graffe e return (se c’è un valore di ritorno):
Function<Integer, Integer> abs = x -> {
if (x < 0) {
return -x;
}
return x;
};
System.out.println(abs.apply(-3)); // 3
Tipi dei parametri
Spesso i tipi dei parametri non è necessario specificarli — il compilatore li deduce dal contesto. Ma se volete, potete indicarli esplicitamente:
Comparator<String> cmp = (String a, String b) -> a.length() - b.length();
Lambda senza valore di ritorno
Se l’interfaccia restituisce void, scrivete semplicemente le istruzioni:
list.forEach(s -> System.out.println("Elemento: " + s));
Tabella: Varianti di sintassi delle espressioni lambda
| Cosa facciamo | Esempio | Commento |
|---|---|---|
| Senza parametri | |
Per esempio, per Runnable |
| Un parametro | |
Si può senza parentesi |
| Più parametri | |
Parentesi obbligatorie |
| Un’espressione | |
Senza return e senza parentesi graffe |
| Blocco di codice | |
Con return, se c’è un risultato |
3. Applicazioni: dove e come usare le espressioni lambda
Le espressioni lambda si usano soprattutto dove serve passare un “comportamento” — una funzione — come argomento. È stata una vera rivoluzione per collezioni, stream (Stream API), eventi e molto altro.
Ordinamento di una lista
Prima di Java 8:
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
Con un’espressione lambda:
list.sort((a, b) -> a.length() - b.length());
Creare un thread (Thread)
Thread t = new Thread(() -> System.out.println("Thread avviato!"));
t.start();
Elaborazione di collezioni
list.forEach(s -> System.out.println(s.toUpperCase()));
Filtrare una lista
List<String> longWords = list.stream()
.filter(s -> s.length() > 5)
.collect(Collectors.toList());
Esempio: sviluppiamo la nostra applicazione didattica
Supponiamo di avere un elenco di utenti:
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");
Stampare tutti gli utenti i cui nomi sono più lunghi di 4 caratteri:
users.stream()
.filter(name -> name.length() > 4)
.forEach(name -> System.out.println("Utente: " + name));
4. Ambito di visibilità delle variabili nelle espressioni lambda
Le espressioni lambda possono usare variabili dal metodo circostante, ma ci sono delle sfumature!
Variabili e lambda
Una lambda in Java può “catturare” solo variabili che, dopo l’inizializzazione, non cambiano più. Se la variabile è dichiarata con final — tutto chiaro. Ma anche se la parola final non è scritta, il compilatore verifica da sé se il valore cambia o no. Se non cambia, la considera «come se fosse final» e la ammette tranquillamente nella lambda.
Esempio:
int minLength = 4; // il valore non cambia da nessuna parte
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
Qui funziona tutto, perché minLength rimane lo stesso numero.
Ma se dopo l’uso nella lambda provate a riassegnare minLength, otterrete un errore di compilazione:
int minLength = 4;
users.forEach(name -> {
if (name.length() > minLength) {
System.out.println(name);
}
});
minLength = 10; // Errore! La lambda ha già "catturato" il valore
In sostanza la regola è molto semplice: la variabile che entra nella lambda deve essere immutabile.
Differenza rispetto alle classi anonime
Nelle classi anonime e nelle espressioni lambda le variabili del metodo esterno funzionano allo stesso modo: solo final/effettivamente final.
MA!
In un’espressione lambda this si riferisce all’oggetto esterno (l’istanza corrente della classe), mentre in una classe anonima — all’istanza della classe anonima. È importante se dentro la lambda accedete ai campi o ai metodi della classe corrente.
Esempio:
public class Example {
String name = "Classe esterna";
void demo() {
Runnable r1 = new Runnable() {
String name = "Classe anonima";
@Override
public void run() {
System.out.println(this.name); // "Classe anonima"
}
};
Runnable r2 = () -> System.out.println(this.name); // "Classe esterna"
r1.run();
r2.run();
}
}
5. Errori tipici nel lavoro con le espressioni lambda
Errore n. 1: uso di una variabile non final/non effettivamente final. Se decidete di modificare una variabile dopo averla usata nella lambda, il compilatore vi fermerà subito. È per sicurezza: altrimenti non sarebbe chiaro quale valore della variabile usare.
Errore n. 2: confusione con this. In un’espressione lambda this è la classe esterna, mentre in una classe anonima è la classe anonima stessa. Se cercate di chiamare un metodo della classe esterna da una lambda, tutto funzionerà; da una classe anonima — no (se contavate sul contesto della classe esterna).
Errore n. 3: lambda senza contesto. Un’espressione lambda non può essere usata da sola — va assegnata a una variabile di un’interfaccia funzionale oppure passata dove tale interfaccia è attesa. Il tentativo di scrivere semplicemente x -> x + 1 fuori contesto causerà un errore.
Errore n. 4: lambda troppo complessa. Se un’espressione lambda supera le 3–5 righe, diventa difficile da leggere. In questi casi è meglio estrarre la logica in un metodo separato.
GO TO FULL VERSION