1. Introduction aux fermetures
Une fermeture — c’est une fonction (ou un objet-fonction) qui non seulement utilise ses propres paramètres, mais « mémorise » aussi des variables du contexte environnant dans lequel elle a été créée. En termes simples, si une expression lambda ou une classe anonyme à l’intérieur d’une méthode utilise des variables de cette méthode — elle devient une fermeture.
Exemple simple
public class ClosureDemo {
public static void main(String[] args) {
String greeting = "Bonjour, ";
Runnable sayHello = () -> System.out.println(greeting + "monde!");
sayHello.run(); // Affichera: Bonjour, monde!
}
}
Ici, la lambda a « capturé » la variable greeting depuis la méthode externe et l’utilise en son sein. C’est exactement une fermeture !
2. Variables final effectives : qu’est-ce que c’est et pourquoi ?
En Java, les expressions lambda (et les classes anonymes) ne peuvent utiliser que des variables de la méthode externe qui sont déclarées final ou qui ne sont pas modifiées après leur initialisation. De telles variables sont dites effectivement final.
Pourquoi ?
Cette contrainte est liée au fait qu’une expression lambda peut être appelée après la fin de la méthode dans laquelle elle a été créée. Si la variable pouvait changer, ce serait confus : quelle version de la variable utiliser ? Pour éviter les surprises, Java exige que la variable soit immuable (ou du moins apparaisse comme immuable).
Exemple : utilisation correcte
public static void main(String[] args) {
int number = 42; // number — effectivement final
Runnable r = () -> System.out.println(number);
r.run(); // 42
}
Exemple : tentative de modifier la variable après utilisation
public static void main(String[] args) {
int number = 42;
Runnable r = () -> System.out.println(number);
number++; // ERREUR: la variable number doit être final ou effectivement final
r.run();
}
Le compilateur affichera l’erreur : Variable used in lambda expression should be final or effectively final.
Effectivement final — c’est… une variable à laquelle on assigne exactement une fois et qui ne change plus. Il n’est pas nécessaire d’écrire final, le compilateur le déduira.
3. Comment les expressions lambda capturent-elles des variables ?
Quand vous écrivez une lambda qui utilise une variable externe, Java « emballe » cette variable avec la lambda. Même si la méthode où la lambda a été créée est déjà terminée, la variable ne disparaît pas — elle vit à l’intérieur de la fermeture.
Illustration : la lambda « mémorise » la variable
public static Runnable createGreeter(String name) {
// name — paramètre de la méthode, il sera capturé par la lambda
return () -> System.out.println("Bonjour, " + name + "!");
}
public static void main(String[] args) {
Runnable greeter = createGreeter("John");
greeter.run(); // Bonjour, John!
}
Ici, la variable name n’existe déjà plus dans la pile de la méthode main, mais greeter « se souvient » toujours de sa valeur.
Comment est-ce implémenté en interne ?
Le compilateur Java crée un objet auxiliaire spécial (on l’appelle aussi « capture/display class ») qui stocke toutes les variables capturées. L’expression lambda devient un objet qui possède une référence vers ce « conteneur » de variables.
4. Exemple de fermeture : retourner une fonction qui utilise une variable
Écrivons une fonction qui retourne une lambda utilisant une variable de son contexte :
import java.util.function.IntSupplier;
public class ClosureFactory {
public static IntSupplier makeAdder(int x) {
// x — capturé par la lambda
return () -> x + 10;
}
public static void main(String[] args) {
IntSupplier adder = makeAdder(5);
System.out.println(adder.getAsInt()); // 15
}
}
Ici, la variable x a déjà « quitté » la pile de la méthode, mais la lambda peut toujours l’utiliser.
5. Pourquoi ne peut-on pas modifier les variables capturées ?
public static void main(String[] args) {
int base = 100;
Runnable printer = () -> System.out.println(base);
base = 200; // ERREUR!
printer.run();
}
Le compilateur ne vous laissera pas faire. Si nous pouvions modifier base, il deviendrait ambigu de savoir quelle version de la variable utiliser dans la lambda : l’ancienne ou la nouvelle ? C’est pourquoi Java interdit de modifier les variables locales capturées par une lambda.
Que peut-on utiliser dans une lambda ?
- Des variables locales qui ne changent pas après l’initialisation (effectivement final).
- Des champs de classe (aussi bien static que non statiques) — on peut les modifier, mais c’est un autre mécanisme (accès à l’état de l’objet), et non une capture de variable locale.
6. Comparaison avec les classes anonymes
Avant l’arrivée des expressions lambda en Java, on pouvait réaliser des fermetures à l’aide de classes anonymes :
public static void main(String[] args) {
String word = "Java";
Runnable r = new Runnable() {
public void run() {
System.out.println(word);
}
};
r.run(); // Java
}
Les mêmes règles s’appliquent : la variable word doit être final ou effectivement final.
Différence : portée de this
- Dans une classe anonyme, this fait référence à l’instance de la classe anonyme.
- Dans une expression lambda, this fait référence à l’objet externe (par exemple, l’instance courante de la classe).
7. Fermeture et champs de classe
Si une lambda utilise un champ de classe, ce n’est pas une « capture de variable locale » au sens strict — le champ est toujours accessible et peut être modifié.
public class Counter {
private int count = 0;
public Runnable makeCounter() {
return () -> {
count++;
System.out.println("Compteur: " + count);
};
}
public static void main(String[] args) {
Counter c = new Counter();
Runnable r = c.makeCounter();
r.run(); // Compteur: 1
r.run(); // Compteur: 2
}
}
8. Erreurs courantes et particularités des fermetures en Java
Erreur n° 1 : tentative de modifier une variable capturée par une lambda. L’erreur la plus fréquente est d’essayer de modifier une variable locale après qu’elle a été utilisée dans une lambda. Le compilateur indiquera : Variable used in lambda expression should be final or effectively final.
Erreur n° 2 : s’attendre à ce que la variable soit « figée ». En Java, une variable capturée n’est pas une copie de la valeur, mais une référence vers l’original lorsqu’il s’agit d’un champ de classe. Si le champ de classe change, la lambda verra la nouvelle valeur. Mais les variables locales utilisées par une lambda doivent être uniquement effectivement final.
Erreur n° 3 : penser qu’une lambda crée une nouvelle portée pour this. Dans une lambda, this — c’est l’objet externe (la classe englobante). Dans une classe anonyme, this — c’est la classe anonyme elle-même.
Erreur n° 4 : utilisation d’objets mutables. Si vous capturez une référence vers un objet mutable (par exemple, une liste), vous pouvez modifier son contenu à l’intérieur de la lambda, même si la variable elle-même est effectivement 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]
}
Ici, la variable list ne change pas (nous ne faisons pas list = ...), mais l’objet auquel elle fait référence, lui, change.
GO TO FULL VERSION