Accueil / Articles PiApplications. / La plate-forme Java / Le langage

Introduction aux expressions lambda Java.

La publication du JDK 1.8 (ou JDK8) est une avancée majeure de la plate-forme Java. Contrairement aux évolutions antérieures qui portaient sur l'optimisation de la vitesse d'exécution et l'enrichissement de l'API, le JDK8 fait sérieusement évoluer le compilateur sans remettre en question les bases du langage (polymorphisme, langage fortement structuré, etc.). En revanche il enrichi sa capacité à modifier le comportement d'un objet via des règles externes à ce dernier. Là où le comportement d'un objet ne pouvait être modifié que par d'autres objets, il est désormais possible grâce aux expressions lambda de décrire ce comportement via des fonctions anonymes issues de l'implémentation d'interfaces fonctionnelles ; les expressions lambda.

Les interfaces fonctionnelles sont à la base des expressions lambda. Une interface fonctionnelle est une interface qui décrit une et une seule méthode abstraite. En plus de cette méthode abstraite et depuis le JDK8, une interface fonctionnelle peut également décrire :

Sa définition peut être précédée par l'annotation @FuntionalInterface.

Syntaxe et généralités.

Une expression lambda est une fonction anonyme. Ce n'est ni une classe, ni une méthode. Elle peut-être utilisée partout où une interface fonctionnelle est attendue en fournissant le code de la seule méthode abstraite de cette interface.

Une expression lambda peut être une fonction qui n'admet aucun paramètre. Dans ce cas elle commence par () ->. Nous pouvons par exemple écrire Callable cll = () -> process() ; (assignation à une variable) ou new Thread(() -> process()).start(); (assignation à une méthode).

Le package java.util.function définit un certain nombre d'interface fonctionnelles qui couvrent la majorité des cas d'emploi.

Il est possible de réutiliser une méthode en tant qu'implémentation d'une expression lambda : FileFilter ffl = File fil -> fil.canRead(); peut s'écrire FileFilter ffl = File::canRead;

La simplification répond au format général : référence_cible::nom_méthode.

Il existe 3 sortes de référence de méthode :

  1. les références sur méthode statique : (args) -> ClassName.staticMethod(args)
    Exemple : (String s) -> System.out.println(s) peut s'écrire System.out::println.
  2. les références sur une méthode d'un objet quelconque : (arg0, rest) -> arg0.instanceMethod(rest)
    Exemple : (String s, int i) -> s.substring(i) peut s'écrire String::substring.
  3. les références sur une méthode d'une instance d'objet existant : (args) -> expr.instanceMethod(args)
    Axis a -> getLength(a) peut s'écrire this::getLength.

Les constructeur n'étant qu'une forme particulière de méthode (liée à l'automatisation de son invocation), les références sur méthodes s'appliquent aussi aux constructeurs. Par exemple Factory<List<String>> f = () -> return new ArrayList<String>();peut s'écrire Factory<List<String>> f = ArrayList<String>::new;

Une expression lambda peut utiliser une variable située dans sa portée si cette dernière est effectivement de type final (non modifiée). Cela est vrai que la variable ait été explicitement déclarée final ou non. En voici un exemple de méthode qui affiche tous les fichiers dont la date de dernière modification est antérieure à une valeur donnée (GDH exprimé sous forme d'un entier long) :

void expire(File filRoot, long lBefore) {
 filRoot.listFiles(File fil -> fil.lastModified() < lBefore);
}

Les expressions lambda supportent la notion de this. Toutefois, comme les expressions lambda sont des fonctions anonymes et non des classes, this n'a pas ici le même sens. Observons cela sur un exemple :

  private int iCurrent = 10;
  private void test()
  {
    List<Integer> lstEx = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    lstEx.replaceAll(i -> ++iCurrent);
    lstEx.forEach(System.out::println);
  }

Si on déclare la variable iCurrent au sein de la méthode cela ne marche pas car le compilateur détecte que la variable est modifiée et donc non final. En revanche placée en attribut de la classe qui déclara la méthode test cela fonctionne. Etonnant non ?

En fait, pas tant que ça. Lorsque le compilateur rencontre la variable iCurrent, il référence implicitement un thissur l'objet dans la portée immédiate de l'expression lambda (objet qui définit l'attribut iCurrent). Vu de ce "this", la variable iCurrent est effectivement final car non encore modifiée par cet objet. On obtiendrait le même effet en référençant explicitement le this de la variable iCurrent : lstEx.replaceAll(i -> this.++iCurrent);.

En d'autres termes, this dans une expression lambda se réfère à l'objet dans la porté immédiate de cette expression. Si l'expression lambda référence des variables ou des attributs de cet objet le compilateur ajoutera implicitement ce this devant la variable.

Il faut cependant bien conserver à l'esprit que l'emploi de ce type d'astuce dans un contexte de fonctionnement multitâche est très risqué. Dans un tel contexte on évitera ce type d'utilisation.

Optimisation de code.

Les expressions lambda sont également extrêmement utiles pour améliorer les performances d'une application. Chaque fois qu'une méthode attend un paramètre créé à partir d'une fonction ou d'une méthode, elle exécute cette fonction ou cette méthode pour récupérer le paramètre avant de le transmettre ensuite à la méthode. Si la méthode est dans un cas où de toute façon elle ne produit rien, on aura perdu le temps d'exécution de la fonction ou de la méthode qui a calculé le paramètre.

C'est particulièrement vrai dans le cas d'un journal ou la méthode invoquée (finest par exemple) peut être systématiquement ignoré en fonction du niveau de filtrage. Si dans le code nous avons une instruction du type logger.finest(createComplexMessage()); et que le niveau de filtrage est interrompu après warning, la méthode createComplexMessage sera systématiquement exécutée alors que la valeur du paramètre qu'elle retourne sera ignorée.

Une expression lambda sur une interface de type Supplier<T> évite cette exécution : logger.finest(() -> createComplexMessage());. L'expression lambda n'est exécutée qu'au moment où l'on utilise effectivement le paramètre. Comme il n'est jamais utilisé dans ce cas de figure, elle n'est jamais exécutée d'où un gain proportionnel au temps d'exécution du calcul du paramètre et au nombre d'invocations de la méthode finest.

Méthodes par défaut utiles.

(c) PiApplications 2016