1. Découverte
En bref : une expression lambda — c’est un moyen de créer rapidement, « à la volée », une implémentation d’une interface fonctionnelle, sans déclarer une classe séparée ni une classe anonyme. C’est comme une mini-méthode sans nom que l’on peut passer en argument ou stocker dans une variable.
Pendant longtemps, Java a été un langage POO « classique ». Si vous vouliez passer un morceau de comportement, par exemple quoi faire lors de l’appui sur un bouton, il fallait écrire des classes anonymes :
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("Bouton cliqué!");
}
});
À partir de Java 8, la magie des lambdas est arrivée :
button.setOnClickListener(() -> System.out.println("Bouton cliqué!"));
Où () -> System.out.println("Bouton cliqué!") — c’est justement une expression lambda.
Formellement : une expression lambda est une forme compacte d’écriture de l’implémentation de l’unique méthode abstraite d’une interface fonctionnelle.
Pourquoi est-ce important ?
- Passer un comportement comme une valeur (par exemple, quoi faire pour chaque élément d’une liste).
- Écrire un code concis et lisible.
- Utiliser les API modernes de Java : API Stream, gestion d’événements, tâches asynchrones et bien plus encore.
2. Syntaxe des expressions lambda
Forme générale
(paramètres) -> expression
// ou
(paramètres) -> { bloc de code }
Exemples pour différentes interfaces
1. Sans paramètres (par exemple, Runnable) :
Runnable r = () -> System.out.println("Bonjour, lambda!");
r.run();
2. Un paramètre (par exemple, Consumer) :
Consumer<String> printer = s -> System.out.println("Vous avez passé: " + s);
printer.accept("Java");
Si le paramètre est unique et que son type peut être déduit, on peut omettre les parenthèses.
Le type String — c’est le type de la variable (s) que nous y passons.
3. Plusieurs paramètres (par exemple, Comparator) :
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
Le type Integer — c’est le type des variables (a, b) que nous y passons.
4. Corps multi-ligne (accolades requises et return s’il y a un retour explicite) :
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
Le premier Integer — c’est le type du résultat de la fonction, le second Integer — c’est le type du paramètre.
Abréviations et concision
- Si le corps est une seule expression, on peut omettre les accolades et return.
- S’il n’y a pas de paramètres — on écrit des parenthèses vides : () -> ...
- S’il y a un seul paramètre — on peut omettre les parenthèses : x -> ...
- S’il y a plus d’un paramètre — les parenthèses sont nécessaires : (a, b) -> ...
Tableau récapitulatif
Voyons comment la même idée s’écrit avec une lambda et avec une classe anonyme :
| Interface | Expression lambda (exemple) | Équivalent en classe anonyme |
|---|---|---|
|
|
|
|
|
|
|
|
|
Exemple avec une interface personnalisée
Supposons que nous ayons une interface fonctionnelle :
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
Avant, on l’implémentait ainsi :
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
Maintenant — façon moderne :
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
Exemples pour les interfaces standard
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("pomme", "banane", "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. Portée et capture des variables
Variables du contexte externe (effectively final)
Les expressions lambda peuvent utiliser des variables de la méthode environnante. Mais il y a une règle : ces variables doivent être effectively final — c’est-à-dire soit explicitement déclarées final, soit simplement non modifiées après leur initialisation.
Exemple :
String prefix = "Résultat: ";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix est "gelé" ici — après cela on ne peut plus le modifier
System.out.println(f.apply(5)); // Résultat: 10
Si vous essayez de modifier prefix après qu’il a été utilisé dans la lambda, le compilateur signalera une erreur.
Pourquoi ?
L’expression lambda peut être appelée après la sortie de la méthode où la variable a été déclarée. Pour éviter des bogues « magiques », Java autorise l’utilisation de variables uniquement immuables.
Différence avec les classes anonymes
Dans les classes anonymes, la même règle s’applique : les variables de la méthode externe doivent être final/effectively final. Mais il y a des nuances de portée : à l’intérieur d’une classe anonyme, this fait référence à la classe anonyme elle-même, et dans une lambda — à la classe externe.
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // Affichera: Demo$1 (classe anonyme)
}
};
Runnable r2 = () -> System.out.println(this); // Affichera: Demo (classe externe)
r1.run();
r2.run();
}
}
Lambda et champs de classe
L’expression lambda peut accéder aux champs de la classe externe sans restrictions :
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));
}
}
Ici base — peut être modifié (c’est un champ de classe).
x — doit être effectively final.
4. Pratique : écrivons quelques expressions lambda
Exemple : filtrage d’une liste de nombres
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("Pair: " + n));
Vous en saurez davantage sur l’API Stream au niveau 30 :P
Exemple : fonction de transformation de chaîne
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
Exemple : votre propre interface fonctionnelle
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("Salut")); // Salut!
Exemple : utilisation de variables du contexte externe
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// factor ne peut pas être modifié après ça!
5. Erreurs courantes lors de l’utilisation des expressions lambda
Erreur n° 1 : tentative de modifier une variable capturée par une lambda.
Ce code ne compilera pas — la variable doit être effectively final :
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // Erreur: sum n'est pas final!
Si vous devez accumuler des valeurs — utilisez un tableau ou un objet enveloppe.
Erreur n° 2 : confusion avec la portée de this.
À l’intérieur d’une expression lambda, this fait référence à la classe externe, et non à la lambda (contrairement à une classe anonyme).
Erreur n° 3 : oubli des accolades et de return dans une expression lambda multi-ligne.
Si le corps de la lambda n’est pas une seule expression, il faut des accolades et return :
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
Erreur n° 4 : mauvaise définition du type d’une expression lambda.
Une expression lambda implémente toujours une interface fonctionnelle. On ne peut pas simplement écrire :
var f = x -> x + 1; // Erreur! Type d'interface inconnu.
Il faut indiquer explicitement le type :
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION