W3docs

Expressions lambda en Java

Implémentations inline concises d'interfaces fonctionnelles en Java avec les expressions lambda : (params) -> corps.

Une expression lambda est la syntaxe concise introduite par Java 8 pour « une instance d'une interface ayant exactement une méthode abstraite ». Auparavant, cela s'écrivait sous la forme d'une classe anonyme. Désormais, on l'écrit comme une liste de paramètres, une flèche et un corps :

Runnable r = () -> System.out.println("hi");
Comparator<String> byLen = (a, b) -> a.length() - b.length();
Function<String, Integer> length = s -> s.length();

Il n'y a pas de nouveau type de valeur ici — r, byLen et length sont toujours des références d'objets, et à l'exécution chacun contient une instance d'une classe qui implémente l'interface à gauche. Ce qui est nouveau, c'est que le code qui dit « créez-m'en une » est suffisamment court pour tenir au point d'appel, ce qui débloque tous les autres idiomes fonctionnels de la partie : prédicats de filtre, constructeurs de comparateur, gestionnaires d'événements, pipelines de flux.

Les formes de syntaxe

Une lambda comporte trois éléments : liste de paramètres, flèche -> et corps. Chaque élément possède une forme abrégée :

// Zero parameters: empty parens are required
Runnable r = () -> System.out.println("tick");

// One parameter: parens optional (idiomatic to omit them)
Function<String, Integer> len = s -> s.length();
Function<String, Integer> len2 = (s) -> s.length();           // same thing

// Two or more: parens required
Comparator<String> cmp = (a, b) -> a.length() - b.length();

// Explicit types: rare but legal
BinaryOperator<Integer> add = (Integer a, Integer b) -> a + b;

// Expression body: the value of the expression is the return value
Predicate<Integer> positive = n -> n > 0;

// Block body: explicit `return` required if the interface method returns a value
Function<Integer, String> describe = n -> {
  if (n == 0) return "zero";
  if (n < 0)  return "negative";
  return "positive";
};

Trois règles relient tout cela :

  1. Les types de paramètres sont généralement inférés à partir du type cible (l'interface déclarée au point d'appel). Ne les écrivez que lorsque le compilateur ne peut pas en choisir un ou lorsqu'ils améliorent la lisibilité.
  2. Un corps d'expression retourne sa valeur implicitement. Pas de return, pas de point-virgule. L'expression est le résultat.
  3. Un corps de bloc nécessite return lorsque la méthode de l'interface a un type de retour. L'oublier est une erreur de compilation, pas un null silencieux.

Typage cible — où les lambdas peuvent apparaître

Une lambda n'a pas de type intrinsèque. Le compilateur détermine son type à partir de la cible — le contexte dans lequel elle est utilisée :

Runnable           r1 = () -> doWork();          // target: Runnable
Callable<Integer>  c1 = () -> 42;                // target: Callable<Integer>
Supplier<Integer>  s1 = () -> 42;                // target: Supplier<Integer>

() -> 42 est le même code source dans les trois cas, mais il se compile en trois instances d'interface différentes. C'est pourquoi une lambda ne peut pas être assignée directement à ObjectObject o = () -> 42; est ambigu et le compilateur refuse. Castez pour lever l'ambiguïté : Object o = (Supplier<Integer>) () -> 42;.

Les cibles les plus courantes :

  • Un paramètre de méthode de type interface fonctionnelle : list.removeIf(s -> s.isEmpty()).
  • Un champ ou une variable locale de type interface fonctionnelle : Predicate<String> empty = String::isEmpty;.
  • Un type de retour : public Supplier<Date> now() { return Date::new; }.

S'il n'y a pas de cible, il ne peut pas y avoir de lambda. var f = s -> s.length(); ne compile pasvar ne peut pas inférer un type cible.

Capture de variables : « effectivement final »

Une lambda peut lire des variables locales de la méthode englobante, mais seulement si ces variables sont effectivement finales — jamais réassignées après leur valeur initiale :

int multiplier = 3;
IntFunction<Integer> scale = n -> n * multiplier;     // OK — `multiplier` never reassigned
multiplier = 4;                                        // <-- this line would make the lambda not compile

La règle est la même que celle que les classes internes anonymes ont toujours appliquée, et la raison est la même : une lambda peut survivre à la méthode dans laquelle elle a été définie (vous pourriez la stocker dans un champ ou la passer à un autre thread), et Java ne dispose pas de fermetures capturant la variable — il capture la valeur au moment de la construction. Autoriser la réassignation créerait une illusion confuse.

Les champs sont une autre histoire. Une lambda peut lire et modifier librement les champs d'instance et les champs statiques :

class Counter {
  private int n = 0;
  Runnable inc = () -> n++;     // legal — `n` is a field, not a local
}

C'est une source fréquente de bogues dans le code de flux — une lambda qui modifie un champ partagé semble anodine mais entre en concurrence avec elle-même lorsque le flux est parallèle. Les lambdas pures sont plus sûres.

this, return et break dans une lambda

Une lambda n'est pas une nouvelle portée pour this. À l'intérieur d'une lambda, this fait référence à l'instance englobante — la même que celle du code environnant :

class Greeter {
  String prefix = "Hello, ";
  Function<String, String> greet = name -> this.prefix + name;   // `this` is the Greeter
}

C'est l'une des plus grandes différences pratiques avec les classes anonymes, où this faisait référence à l'instance anonyme elle-même.

return à l'intérieur d'une lambda retourne depuis la lambda, pas depuis la méthode englobante. break et continue ne fonctionnent pas dans une lambda — ils appartiennent à la boucle qu'ils ciblent, et le corps de la lambda ne fait pas partie de la boucle environnante.

Lambda vs classe anonyme — quand chacun convient

Pour les interfaces fonctionnelles, les lambdas sont presque toujours plus courtes et plus claires. Elles génèrent un bytecode légèrement différent (invokedynamic) et ne créent pas de nouveau fichier de classe par site d'utilisation, elles sont donc généralement plus légères à l'exécution également.

Utilisez une classe anonyme lorsque :

  • L'interface a plus d'une méthode abstraite (elle n'est pas fonctionnelle).
  • Vous avez besoin d'un champ local à la méthode (int seen = 0; accessible entre les appels).
  • Vous avez besoin que this fasse référence à l'instance que vous créez, et non à l'instance englobante.
  • Vous devez redéfinir une méthode par défaut pour spécialiser son comportement.

Dans tous les autres cas, la lambda l'emporte.

Un exemple concret : capture, typage cible, les quatre sites d'appel

Le programme ci-dessous illustre les quatre endroits les plus courants où une lambda apparaît — forEach de collection, removeIf, tri et filtre de flux — ainsi que les règles de capture et le typage cible.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • () -> \"hi\" a fonctionné à la fois comme Callable<String> et comme Supplier<String> — même code source, types cibles différents, instances d'interface différentes. C'est pourquoi une lambda n'a pas de type jusqu'à ce que le contexte en fournisse un.
  • times = n -> n * factor a capturé factor par valeur. Le compilateur l'a accepté parce que factor n'a jamais été réassigné. Décommenter factor = 11 transformerait factor en variable non effectivement finale et bloquerait la compilation de la lambda.
  • forEach, removeIf et sort prennent chacun une interface fonctionnelle différente (Consumer, Predicate, Comparator), et la forme de la lambda — nombre de paramètres, présence d'un retour — correspondait à la méthode abstraite unique de chaque interface. Le compilateur effectue la correspondance par typage cible.
  • La lambda describe à corps de bloc nécessitait des instructions return explicites parce que sa cible (Function<Integer, String>) a un type de retour non void. Les lambdas à corps d'expression ci-dessus retournaient leur expression implicitement.

Et ensuite

Vous connaissez la syntaxe et les règles de capture. La question suivante est : quelle interface, exactement, une lambda compile-t-elle ? Les interfaces fonctionnelles Java présente la règle SAM (méthode abstraite unique), l'annotation @FunctionalInterface, et comment écrire votre propre interface fonctionnelle pour les cas que la bibliothèque standard ne couvre pas.

Pratique

Pratique
Laquelle de ces expressions lambda le compilateur Java refusera-t-il ?
Laquelle de ces expressions lambda le compilateur Java refusera-t-il ?
Was this page helpful?