CodeGym /Cours /JAVA 25 SELF /Introduction aux expressions lambda

Introduction aux expressions lambda

JAVA 25 SELF
Niveau 21 , Leçon 0
Disponible

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é!"));

() -> 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
Runnable
() -> System.out.println("Hi")
new Runnable() { public void run() { System.out.println("Hi"); } }
Consumer<String>
s -> System.out.println(s)
new Consumer<String>() { public void accept(String s) { System.out.println(s); } }
Comparator<Integer>
(a, b) -> a - b
new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }

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;
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION