Accueil Nos publications Blog Java 8 – JSR-335 – 1/3 – Expressions Lambda

Java 8 – JSR-335 – 1/3 – Expressions Lambda

Neuf ans après les Generics et Collections de Java 5, les expressions Lambda seront la nouvelle révolution dans le monde de Java (version 8). Pour le meilleur ou pour le pire… à vous d’en juger !

Le projet Lambda [1] a pour objectif d’accueillir un prototype de l’implémentation de la JSR-335 (Lambda Expressions for the Java Programming Language [2]) qui sera incluse dans la JDK 8, l’implémentation de référence de Java SE 8.

Pour une explication généraliste des expressions Lambda (nommées aussi fonctions anonymes ou closures), je vous invite à consulter la page wikipedia consacrée au sujet [3].

Comme nous le verrons par la suite, la JSR-335 n’est pas limitée aux expressions Lambda, elle définit aussi l’aliasing de méthodes et de constructeurs par référence, ainsi que l’ajout de méthodes par défaut dans les interfaces.

Note importante : La JSR n’étant pas encore à l’état « final », il est possible qu’il y ait des différences entre les fonctionnalités décrites dans cet article et les fonctionnalités proposées dans la version finale.

Pour cet article la JDK 8 build b64 avec support des lambdas a été utilisée. Le dernier build peut être téléchargé depuis java.net.

L’intégralité des sources est disponible via Github.

Getting started

Commençons par un exemple simple, en implémentant une méthode buildMessage(), qui prend une chaîne de caractères en paramètres et qui retourne une chaîne de caractères.

Pour que l’article reste lisible, la javadoc, les noms de packages et les imports ont été omis dans tous les exemples.


        / **
          * org.isk.gettingstarted.FirstFunctionalInterface.java
          */
        public interface FirstFunctionalInterface {
String buildMessage(String msg);
        }

        / **
          * org.isk.gettingstarted.GettingStartedTest.java
          */
        public class GettingStartedTest {

@Test
public void oldWayWithAnonymousClass() {
final FirstFunctionalInterface firstInterface = new FirstFunctionalInterface() {
@Override
public String buildMessage(String msg) {
return "Hello " + msg;
}
};

final String string = firstInterface.buildMessage("Carl");

Assert.assertEquals("Hello Carl", string);
}

@Test
public void newWayWithLambdaExpression() {
final FirstFunctionalInterface firstInterface = msg ->
"Hello " + msg + " with a lambda expression";
final String string = firstInterface.buildMessage("John");

Assert.assertEquals("Hello John with a lambda expression", string);
}
        }
        

L’un des objectifs des expressions Lambda est d’éliminer la verbosité des classes anonymes. Comme nous pouvons le constater, c’est mission accomplie. L’utilisation d’une classe anonyme prend 5 lignes (6 avec l’annotation @Override ) alors que l’expression Lambda n’en prend qu’une seule.

A première vue, la structure d’une fonction Lambda est déconcertante. Le nouvel opérateur -> délimite les paramètres du corps de la méthode [paramètres -> corps de la méthode]. Notre méthode :


        public String buildMessage(String msg) {
return "Hello " + msg;
        }
        

est transformée en :


        msg -> "Hello " + msg + " with a lambda expression";
        

msg est le paramètre (le type est implicite, mais il peut être explicite si nécessaire), l’opérateur -> marque la fin de la déclaration des paramètres et le début du corps de la méthode. Le mot clé return est aussi absent lorsque le corps de la méthode est une expression.

Functional Interfaces

Une interface fonctionnelle est une interface n’ayant qu’une seule méthode abstraite, qui n’appartient pas à la classe Object. De plus, toutes les méthodes doivent être publiques.

Il est important de faire la distinction entre une méthode abstraite et une signature de méthode qui, selon la définition de la JLS [4], n’inclue que le nom de la méthode et ses paramètres. La valeur de retour, ainsi que les exceptions, n’en font pas partie.

A noter que rien de particulier ne doit être fait pour qu’une interface devienne une interface fonctionnelle, le compilateur a la charge de les identifier d’après leur structure.


        // Interface fonctionnelle
        // equals() appartient à la classe java.lang.Object et est publique
        public interface Comparator<T> {
boolean equals(Object obj);
int compare(T o1, T o2);
        }

        // N’est pas une interface fonctionnelle
        // Bien que clone() appartienne à la classe java.lang.Object, elle n’est pas publique
        public interface Foo {
int doSomething();
Object clone();
        }
        

La JDK contient déjà des interfaces répondant à cette définition, auparavant nommés SAM Types (Single Abstract Method). Elles sont parfaitement adaptées pour être utilisées avec les nouvelles fonctionnalités Lambda. Les suivantes étant le plus souvent utilisées :

Pour plus de détails sur les interfaces fonctionnelles – notamment sur la résolution de l’héritage d’interfaces -, vous pouvez consulter la JSR – Part A.

Expressions Lambda

Définition

En Java, une expression Lambda est une forme de méthode, plus compacte qu’une méthode standard, pour laquelle le nom est implicite (ce n’est jamais une fonction anonyme comme on peut le rencontrer dans d’autres langages puisque nous avons besoin d’une interface fonctionnelle). De plus, les paramètres peuvent être omis, tout comme leur type ou une valeur de retour.

Comme évoqué dans la section « Getting Started », le principal problème des classes anonymes est leur verbosité, ce que les expressions Lambda tentent de gommer.

Regardons quelques exemples d’expressions Lambda [5] :


        // 1. Prend un numérique et retourne sa valeur doublée
        x -> 2   * x

        // 2. Prend deux int et retourne leur somme
        (int x, int y) -> x + y

        // 3. Ne prend pas de paramètres et retourne 42
        () -> 42

        // 4. Prend une chaîne de caractères, affiche sa valeur et ne retourne rien
        (String s) -> System.out.println(s)

        // 5. Prend une collection, récupère sa taille, la vide
// et retourne la taille qu’elle avait au début du bloc
        c -> {
int s = c.size();
c.clear();
return s;
        }
        
  • Le type des paramètres peut être explicite (exemples 2 et 4) ou implicite (exemples 1 et 5). Cependant, il n’est pas possible de mixer les deux écritures dans une expression Lambda.
  • Les parenthèses entourant un, et un seul, paramètre implicite peuvent être omises (exemples 1 et 5).
  • Le corps de la méthode peut être un bloc (encadré par des accolades, exemple 5) ou une expression (exemples 1, 2, 3 et 4).
  • Si le corps de la méthode est un bloc, il peut retourner une valeur (exemple 5) ou non à l’aide du mot clé return , selon le cas.
  • Si le corps de la méthode est une expression, il peut retourner une valeur (exemples 1,2 et 3) ou non (exemple 4) selon le cas. Le mot clé return est inutile.

Attention, le problème de verticalité des classes anonymes ne doit pas être transformé en problème d’horizontalité, comme aurait pu le devenir l’exemple 5 qui pourrait être écrit sur une ligne.

Contexte

Le type d’une expression Lambda est celui de l’interface contenant la méthode implémentée par l’expression Lambda. Une expression Lambda peut avoir différents types dans différents contextes. Il n’existe pas comme dans certains langages un type générique Lambda.

Generics

Une interface fonctionnelle peut définir des Generics. En reprenant le tout premier exemple, voici ce qu’on peut malheureusement avoir si l’on en fait mauvais usage :


        / **
          * org.isk.generics.GenericsLambda.java
          */
        public interface GenericsLambda<T, R>  {
R doSomething(T t);
        }

        / **
          * org.isk.generics.GenericsLambdaTest.java
          */
        public class GenericsLambdaTest {
@Test
public void genericLambda() {
GenericsLambda<String, String> gl = msg ->
                                  "Hello " + msg + " with a lambda expression";
final String string = gl.doSomething("John");

Assert.assertEquals("Hello John with a lambda expression", string);
}
        }
        

Une méthode prenant un Type générique I et retournant un Type générique O doit être utilisée dans des situations bien précises, telles que nous le verrons dans le troisième article de cette série. Utiliser une telle méthode pour éviter d’avoir à définir une interface fonctionnelle, et avoir le sentiment d’utiliser une fonction anonyme, est considéré comme étant une mauvaise pratique.

De même, l’utilisation d’une ellipse en tant que paramètre d’une méthode, et donc d’une expression Lambda, doit être limitée au passage d’un “pseudo” tableau dont tous les objets représentent la même chose (le principe restant que l’on passe des éléments séparés par une virgule, mais dont la représentation dans la méthode ou le corps de l’expression Lambda, est un tableau). La position des objets dans le tableau, ne doit pas avoir d’importance.


        / **
          * org.isk.generics.GenericsLambda.java
          */
        public interface GenericsLambda<T, R>  {
R doSomething(T t);
        }

        / **
          * org.isk.generics.GenericsLambdaWithEllipse.java
          */
        public interface GenericsLambdaWithEllipse<T, R> {
R doSomething(T ... t);
        }

        / **
          * org.isk.generics.GenericLambdaWithEllipseTest.java
          */
        public class GenericLambdaWithEllipseTest {
@Test
public void genericLambdaWithEllipse() {
// Construit un message avec le nom, l’âge et la ville
final GenericsLambdaWithEllipse<Object, String> gle = array -> {
// Retourne un message en fonction de l’âge
final GenericsLambda<Integer, String> gl = age -> {
if (age.intValue() < 40) {
return "You are young";
} else {
return "You are still young";
}
};

return "Hello " + array[0] + ", you live in " + array[2] + " and "
          + gl.doSomething((Integer)array[1]) + "!";
};

final String string = gle.doSomething("John", 20, "Paris");

Assert.assertEquals("Hello John, you live in Paris and You are young!", string);
}
        }
        

Outre la mauvaise utilisation de l’ellipse, l’imbrication des expressions Lambdas rend le code difficilement lisible, et par conséquent augmente le risque d’erreurs et donc de bugs.

Design first

Le fait d’avoir à créer une interface fonctionnelle – ce que certains pourraient considérer comme problématique -, impose d’avoir une réflexion sur la pertinence d’utiliser une expression Lambda.

Si par exemple nous souhaitons effectuer une curryfaction (une réduction de paramètres), utiliser les expressions Lambda est une très mauvaise idée, aussi bien en termes de conception que de lisibilité. Il n’est plus question de verbosité.


        / **
          * org.isk.currying.Division.java
          */
        public interface Division {
double divide(double x, double y);
        }

        / **
          * org.isk.currying.Reduction.java
          */
        public interface Reduction {
double divide(double x);
        }

        / **
          * org.isk.currying.CurryingTest.java
          */
        public class CurryingTest {
/ **
  * Currying without lambda expressions
  */
@Test
public void curryingWithoutLambdaExpression() {
Assert.assertEquals(1.5, this.divideByTwo(3), 0.00001);
Assert.assertEquals(1, this.divideByThree(3), 0.00001);
}

public double divideByTwo(int x) {
return this.divide(x, 2);
}

private double divideByThree(double x) {
return this.divide(x, 3);
}

private double divide(double x, double y) {
return x / y;
}

/ **
  * Currying with lambda expressions
  */
@Test
public void curryingWithLambdaExpression() {
final Division reduction = (x, y) -> x / y;

final Reduction divisionByTwo = x -> reduction.divide(x, 2);
final Reduction divisionByThree = x -> reduction.divide(x, 3);

Assert.assertEquals(1.5, divisionByTwo.divide(3), 0.00001);
Assert.assertEquals(1, divisionByThree.divide(3), 0.00001);
}
        }
        

Pipelining

Tout comme il est possible de chaîner l’appel de méthodes :


node.run().execute()

il est possible de chaîner la définition d’expressions Lambda :


Node<FinalNode> node = () -> () -> "ok";

qui auraient aussi pu être écrites de la façon suivante :


() -> {
 return () -> "ok";
}

Exemple complet :


        / **
          * org.isk.pipelining.Division.java
          */
        public interface Node<T> {
T run();
        }

        / **
          * org.isk.pipelining.Reduction.java
          */
        public interface FinalNode {
String execute();
        }

        / **
          * org.isk.pipelining.ChainedLambdaTest.java
          */
        public class ChainedLambdaTest {
@Test
public void chain() {
Node<FinalNode> node = () -> () -> "ok";

Assert.assertEquals("ok", node.run().execute());
}
        }
        

Scope des variables

Contrairement à une classe anonyme, une expression lambda peut être considérée comme un bloc de code (code encadré par des accolades). Par conséquent, toutes les règles de visibilité des variables applicables à un bloc de code le sont aussi pour une expression lambda, à une exception près comme nous allons le voir.


        / **
          * org.isk.scope.Executor.java
          */
        public interface Executor {
int modify(int v);
        }

        / **
          * org.isk.scope.Mutable.java
          */
        public class Mutable {
public int value;
        }

        / **
          * org.isk.scope.ScopeTest.java
          */
        public class ScopeTest {
private int classField = 1;
private final static int FINAL_STATIC_INT = 2;
private final int FINAL_INT = 3;

@Test
public void scope() {
final Mutable mutable = new Mutable();
mutable.value = 4;

int effectivelyFinalInt = 5;
final int finalInt = 6;

final Executor executor = arg -> {
int localInt = 7;
mutable.value++;

// effectivelyFinalInt++;

return ++this.classField
+ ScopeTest.FINAL_STATIC_INT
+ this.FINAL_INT
+ effectivelyFinalInt
+ finalInt
+ --localInt
+ arg;
};
// effectivelyFinalInt++;

Assert.assertEquals("1", 4, mutable.value);
Assert.assertEquals("2", 31, executor.modify(7));
Assert.assertEquals("3", 5, mutable.value);

// effectivelyFinalInt++;
}
        }
        

Dans le corps d’une expression Lambda, sont accessibles :

  • Les variables définies localement – ex : localInt
  • Les variables passées en paramètre – ex : arg
  • Les variables final ( static ou non) – ex : FINAL_STATIC_INT , FINAL_INT, mutable et finalInt
  • Les variables qui sont en réalité final (variables assignées une seule fois et jamais modifiées par la suite dans la méthode contenant l’expression Lambda) – ex : effectivelyFinalInt
  • Les variables de classe (de la classe contenant l’expression Lambda) – ex : classField

Seules les variables modifiées localement sont interdites. Si effectivelyFinalInt est modifiée dans la méthode définissant l’expression Lambda, elle appartient à cette catégorie.

this fait référence à l’instance de la classe contenant l’expression Lambda. De même, super fait référence au parent de cette classe.

A noter que le nom des paramètres de l’expression Lambda doit être différent des variables de classe et des variables locales de la méthode contenant l’expression Lambda.

Objets muables

Certains l’auront peut-être remarqué, mais une erreur difficilement identifiable peut être créée.

Si, à une variable locale final, est assignée l’instance d’une classe (muable ou non), elle ne peut plus pointer vers une autre référence. En revanche, si la classe est muable, les champs de l’instance peuvent être modifiés. En d’autres termes, nous avons une variable globale à la méthode, accessible et modifiable par les expressions Lambda définies dans cette méthode.


        / **
          * org.isk.mutables.Executor.java
          */
        public interface Executor {
int exec();
        }

        / **
          * org.isk.mutables.Mutable.java
          */
        public class Mutable {
public int value;
        }

        / **
          * org.isk.mutables.MutableTest.java
          */
        public class MutableTest {
@Test
public void mutable() {
final Mutable mutable = new Mutable();
mutable.value = 0;

Executor executor = () -> mutable.value++;

Assert.assertEquals("1", 0, mutable.value);

executor.exec();
Assert.assertEquals("2", 1, mutable.value);

executor.exec();
Assert.assertEquals("3", 2, mutable.value);

mutable.value++;
Assert.assertEquals("4", 3, mutable.value);
}
        }
        

Les utilisateurs d’expressions Lambda sont divisés en deux groupes : les partisans pour capturer uniquement des objets immuables dans le corps des expressions Lambda, et les autres pour lesquels ce n’est pas un soucis de capturer des objets muables. Néanmoins, si vous faites partie de la deuxième catégorie, il faut avoir conscience que prendre en compte toute notion de concurrence dans le corps d’une expression Lambda est difficile, et souvent, posera plus de problèmes qu’il n’en résoudra.

Ressources

[1] https://openjdk.java.net/projects/lambda/

[2] https://www.jcp.org/en/jsr/summary?id=335

[3] https://en.wikipedia.org/wiki/Anonymous_function

[4] https://docs.oracle.com/javase/specs/

[5] https://www.lambdafaq.org/what-is-a-lambda-expression/

[-] https://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-4.html

[-] https://www.lambdafaq.org