1. Introduzione alle closure
Una closure è una funzione (o un oggetto‑funzione) che non solo usa i propri parametri, ma «memorizza» anche variabili dal contesto circostante in cui è stata creata. In parole povere, se un’espressione lambda o una classe anonima all’interno di un metodo usa variabili di quel metodo, allora diventa una closure.
Esempio intuitivo
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Ciao, ";
Runnable sayHello = () -> System.out.println(greeting + "mondo!");
sayHello.run(); // Stamperà: Ciao, mondo!
}
}
Qui la lambda ha «catturato» la variabile greeting dal metodo esterno e la usa al proprio interno. Questa è una closure!
2. Variabili effettivamente final: che cosa sono e perché?
In Java le espressioni lambda (e le classi anonime) possono usare solo quelle variabili del metodo esterno che sono dichiarate final oppure non vengono modificate dopo l’inizializzazione. Tali variabili si chiamano effettivamente final.
Perché?
Questa limitazione è dovuta al fatto che una lambda può essere invocata dopo la fine dell’esecuzione del metodo in cui è stata creata. Se la variabile potesse cambiare, ci sarebbe confusione: quale versione della variabile usare? Per evitare sorprese, Java richiede che la variabile sia immutabile (o almeno appaia immutabile).
Esempio: uso corretto
public static void main(String[] args) {
int number = 42; // number — effettivamente final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Esempio: tentativo di modificare la variabile dopo l’uso
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // ERRORE: la variabile number deve essere final o effettivamente final
r.run();
}
Il compilatore segnalerà un errore: Variable used in lambda expression should be final or effectively final.
Effettivamente final — significa... una variabile che viene assegnata esattamente una volta e non cambia più. Non è necessario scrivere final, il compilatore lo dedurrà da solo.
3. Come le espressioni lambda catturano le variabili?
Quando scrivete una lambda che usa una variabile esterna, Java «impacchetta» quella variabile insieme alla lambda. Anche se il metodo in cui la lambda è stata creata è già terminato, la variabile non scompare — vive all’interno della closure.
Illustrazione: la lambda «si ricorda» la variabile
public static Runnable createGreeter(String name) {
// name — è un parametro del metodo, verrà catturato dalla lambda
return () -> System.out.println("Ciao, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("Vasya");
greeter.run(); // Ciao, Vasya!
}
Qui la variabile name non esiste più nello stack del metodo main, ma greeter «ricorda» comunque il suo valore.
Come è implementato internamente?
Il compilatore Java crea un oggetto di supporto speciale (chiamato anche «capture/display class») che memorizza tutte le variabili catturate. L’espressione lambda diventa un oggetto che ha un riferimento a questo «contenitore» con le variabili.
4. Esempio di closure: restituiamo una funzione che usa una variabile
Scriviamo una funzione che restituisce una lambda che usa una variabile del proprio contesto:
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — viene catturato dalla lambda
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Qui la variabile x è già «uscita» dallo stack del metodo, ma la lambda può comunque usarla.
5. Perché non si possono modificare le variabili catturate?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // ERRORE!
printer.run();
}
Il compilatore non lo permette. Se potessimo cambiare base, non sarebbe chiaro quale versione della variabile usare nella lambda: quella vecchia o quella nuova? Per questo Java vieta di modificare le variabili locali catturate da una lambda.
Che cosa si può usare in una lambda?
- Variabili locali che non cambiano dopo l’inizializzazione (effettivamente final).
- I campi della classe (sia static sia non statici) — possono essere modificati, ma questo è un meccanismo diverso (accesso allo stato dell’oggetto), non la cattura di una variabile locale.
6. Confronto con le classi anonime
Prima dell’introduzione delle espressioni lambda in Java, si potevano creare closure usando le classi anonime:
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Le regole sono le stesse: la variabile word deve essere final o effettivamente final.
Differenza: ambito di this
- Nella classe anonima, this si riferisce all’istanza della classe anonima.
- Nell’espressione lambda, this si riferisce all’oggetto esterno (per esempio, l’istanza corrente della classe).
7. Closure e campi di classe
Se una lambda usa un campo della classe, non si tratta di «cattura di una variabile locale» in senso stretto — il campo è sempre accessibile e può essere modificato.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Contatore: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Contatore: 1
r.run(); // Contatore: 2
}
}
8. Errori tipici e particolarità delle closure in Java
Errore n. 1: tentare di modificare una variabile catturata dalla lambda. L’errore più comune è provare a modificare una variabile locale dopo che è stata usata in una lambda. Il compilatore segnalerà: Variable used in lambda expression should be final or effectively final.
Errore n. 2: aspettarsi che la variabile sia «congelata». In Java una variabile catturata non è una copia del valore, ma un riferimento all’originale quando si tratta di un campo della classe. Se il campo della classe cambia, la lambda vedrà il nuovo valore. Ma le variabili locali in una lambda devono essere solo effettivamente final.
Errore n. 3: aspettarsi che la lambda crei un nuovo ambito per this. Nella lambda this è l’oggetto esterno (la classe contenitore). Nella classe anonima this è la classe anonima stessa.
Errore n. 4: uso di oggetti mutabili. Se catturate un riferimento a un oggetto mutabile (per esempio, una lista), potete modificarne il contenuto all’interno della lambda, anche se la variabile è effettivamente final:
public static void main(String[] args) {
java.util.List<String> list = new java.util.ArrayList<>();
Runnable r = () -> list.add("Hello");
r.run();
System.out.println(list); // [Hello]
}
Qui la variabile list non cambia (non facciamo list = ...), ma l’oggetto a cui punta sì.
GO TO FULL VERSION