Java 7 : petit état des lieux du projet Lambda…

En fin d’année dernière, le report de Java 7 laissait envisager l’intégration des closures. Cela a donné naissance au projet Lambda dont l’objectif était de regrouper les différents travaux afin d’en sortir une spécification claire et fonctionnelle quitte à se passer de certain « power-concept ».

Il en ressort une proposition d’expressions Lambda relativement allégée vis à vis des multiples et très complètes propositions de closures qui ont pu être proposées par le passé. Mais cela s’accompagne également de nouveaux concepts fort intéressants (Exception Transparency, Method References, Defender Methods).

Voyons voir de quoi il en retourne…

[edit] Note : Les expressions Lambdas sont finalement prévu pour Java 8 à la place de Java 7

Attention : je reporte ici des informations de la liste de diffusion du groupe de travail du projet Lambda. Les fonctionnalités précises ainsi que la syntaxe ne sont pas définitives et peuvent encore évoluer…

Type SAM

Il ne s’agit pas vraiment d’un nouveau type ni d’une réelle nouveauté du langage, mais plus précisément d’un nouveau concept. Un type SAM correspond tout simplement à un type Java (classe ou interface) possédant une seule et unique méthode abstraite.

On peut prendre comme exemple l’interface Runnable, qui ne définit qu’une seule et unique méthode run().

A première vue cela peut paraitre totalement insignifiant, mais cela revêt en fait une importance considérable.

Expressions Lambda

Les expressions lambda représentent un « morceau de code ». On pourrait considérer ceci comme une « méthode anonyme », ou comme une manière plus concise d’écrire une classe anonyme mono-méthode.

Son principal intérêt étant de proposer une syntaxe réduite au stricte minimum :

  • Afin de bien les différencier, elles commencent par le caractère #.
  • Vient ensuite la liste des paramètres de la méthode entre parenthèse.
  • Puis le code en lui-même, englobé entre des accolades.

Par exemple, l’expression lambda suivante permet de comparer deux variables de type String en ignorant la case :


#(String a, String b) { return a.compareToIgnoreCase(b); }

C’est là que rentrent en jeu les types SAM. En effet les expressions lambda peuvent être automatiquement converties vers un type SAM dont la signature de la méthode abstraite correspond à celle de l’expression lambda.

Dans notre cas on peut affecter cela à un Comparator :


Comparator<String> comparator = #(String a, String b) { return a.compareToIgnoreCase(b); };

Actuellement pour obtenir le même résultat, il faut utiliser une classe anonyme bien plus verbeuse :


Comparator<String> comparator = new Comparator<String>() {
  @Override
  public int compare(String a, String b) {
    return a.compareToIgnoreCase(b);
  }
};

Et encore la syntaxe peut encore être allégée. Il est en effet possible de ne pas spécifier le type des paramètres (ils seront déduits en fonction du type SAM correspondant). De même si le code se limite à une seule instruction, on peut se passer du return et du point-virgule. Ce qui nous donne alors ceci :


Comparator<String> comparator = #(a, b) { a.compareToIgnoreCase(b) };

Les types SAM étant assez nombreux dans l’API, ceci permettra d’utiliser massivement les expressions lambda y compris avec les APIs existantes qui n’avaient pas été conçus pour cela. Par exemple :


List<String> list = ...
Collections.sort(list, #(a, b){a.compareToIgnoreCase(b)});

Exception Transparency

Peut être la nouveauté la moins visible pour la majorité des développeurs. Ce n’est pas vraiment un concept spécifique pour les expressions lambda, mais plutôt une évolution des Generics afin de permettre de mieux gérer les exceptions.

On peut bien se rendre compte du problème en regardant de plus près les interfaces Runnable et Callable, qui correspondent grosso-modo à ceci :


  public interface Runnable {
    public void run();
  }
 
  public interface Callable<V> {
    public V call() throws Exception;
  }

Si on omet le type de retour, la seule différence vient de la gestion des exceptions.

  • Avec Runnable on ne peut pas remonter d’exception, ce qui peut s’avérer gênant.
  • Avec Callable on peut renvoyer n’importe quelle exception, mais cela implique alors de catcher toutes les exceptions, sans permettre de spécialiser le type d’exception.

Actuellement il est tout à fait possible d’utiliser les Generics pour paramétrer cela :


  public interface XCallable<V, E extends Exception> {
    public V call() throws E;
  }

Mais cela conserve une grosse limitation dû au caractère unique de chaque paramétrage.
En fait si l’on utilise un seul type d’exception cela fonctionne parfaitement :


XCallable<String, IOException> callable = ...
try {
  callable.call();
} catch (IOException e) {
  ...
}

Mais il est impossible de spécifier plusieurs types d’exceptions différents (IOException et SQLException par exemple), à moins de se résigner à continuer d’utiliser le type Exception bien trop générique…

La notion d’exception transparency consiste à faire évoluer les Generics afin de gérer plus spécifiquement le cas des exceptions. Pour cela le paramétrage des exceptions se distinguera par le mot-clef throws. Notre interface deviendrait alors :


  public interface XCallable<V, throws E> {
    public V call() throws E;
  }

A première vue pas de changement majeur. La différence survient principalement lors de l’utilisation, car on peut alors spécifier plusieurs types d’exceptions :


XCallable<String, IOException|SQLException> callable = ...
try {
  callable.call();
} catch (IOException e) {
  ...
} catch (SQLException e) {
  ...
}

A noter qu’il sera également possible de préciser qu’aucune exception ne sera remontée, ce qui permet de s’éviter un try/catch inutile :


XCallable<String, void> callable = ...
callable.call(); // pas d'exception

Bref cela permettra de paramétrer finement les exceptions d’une méthode.

Method References

Comme le titre l’indique si bien, les Method References permettront de référencer une méthode de manière sécurisée (vérifiée à la compilation).

La syntaxe devrait correspondre à celle utilisée dans la javadoc pour référencer une méthode, c’est à dire le nom de la classe séparée du nom de la méthode par le caractère #.

Quelques exemples :


System#getProperties()
String#compareToIgnoreCase(String)

Tout comme les expressions Lambda, les Method References peuvent être affectées à un type SAM correspondant. A noter que pour une méthode d’instance on obtiendra un paramètre initial supplémentaire qui devra contenir une instance du type de la classe.


Callable<Properties> properties = System#getProperties();
Comparator<String> compare = String#compareToIgnoreCase(String);

A noter qu’il devrait être possible d’appeler directement une méthode d’instance sur une instance précise en utilisant une variable précise en lieu et place du nom de la classe :


String text = "Hello World !";
Callable<String> upperCase = text#toUpperCase();
 
System.out.println( upperCase.call() ); // Affiche : HELLO WORLD !

Cela permettra de « passer » une méthode en paramètre d’autres méthodes. Pour reprendre l’exemple du sort() :


List<String> list = ...
Collections.sort(list, String#compareToIgnoreCase(String));

A noter que les Method References devraient également pouvoir être converties vers des objets MethodHandles de la JSR292 (dynamic language).

Defender Methods

J’en ai déjà parlé plus longuement il y a quelques temps, la dernière nouveauté poussée par le projet Lambda vise à permettre l’évolution des interfaces.

En effet tout ajout de méthode dans une interface de l’API est impossible sous peine d’engendrer une multitude d’incompatibilités dans toutes les classes qui l’implémentent, justement car elles n’implémenteront pas ces nouvelles méthodes.

Le rôle des Defender Methods consiste à permettre cela, en proposant une implémentation par défaut en cas d’absence d’implémentation

L’idée consiste donc à ajouter des méthodes d’extension qui préciseraient l’implémentation par défaut via une méthode statique dont les paramètres correspondent :


public interface List<E> {
 
  ...
 
  extension void sort() default Collections.sort;
  extension void sort(Comparator<? super E> c) default Collections.<E>sort;
}

S’en suit alors deux traitements spécifiques lorsque ces méthodes ne sont pas implémentées:

  • Au moment de la compilation, le compilateur génèrera automatiquement le code de ces méthodes en appelant la méthode statique. On obtient alors une classe qui implémente correctement toutes les méthodes de l’interface, y compris les méthodes d’extension rajoutées malgré leurs absences dans le code source.
  • Au moment de l’exécution, la JVM injectera automatiquement le code de ces méthodes de la même manière si elles sont absentes (ce qui peut se produire si les classes ont été compilées avec un compilateur plus ancien).

Dès lors il devient possible de faire évoluer les interfaces « sans tout casser », en proposant une implémentation par défaut mais en conservant la possibilité de redéfinir une implémentation spécifique dans les classes filles si besoin.

14 réflexions au sujet de « Java 7 : petit état des lieux du projet Lambda… »

  1. adiguba Auteur de l’article

    Malheureusement, je pense qu’il faudra attendre Java 8 pour avoir une API standard qui profite réellement des lambdas. Restera les librairies externes…

    Mais du fait de la conversion en type SAM, on pourra quand même les utiliser avec certaines APIs existantes !

    a++

  2. Atma_

    L’apport des closures dans Java7 est une avancé indéniable.

    En revanche, même si cet apport devait se faire dans les temps pour Java7, aura-t-on le temps d’en faire bénéficier l’API ?

    J’entends par là que l’apport des générics en java 5 a nécessité un refactoring assez conséquent de toute l’API (Collections, Class, etc…) pour les « généri-fier »

    L’apport des closures permettrait un nombre de sucres syntaxiques non négligeables (pour s’en convaincre, il suffit d’aller voir du coté de langages comme Groovy où ces dernières sont utilisées à tout bout de champ).
    Par exemple, on pourrait voir apparaître une méthode Collection.forEach().

    L’évolution de l’API suite à la mise en place des closures me paraît difficilement faisable dans un laps de temps si court.. (malheureusement :( )

  3. Avatar de UtherUther

    mcferson> Certes il y a quelques fautes d’accord, mais pas de quoi s’arracher les yeux non plus.

    adiGuba> Ok tu l’auras cherché ;)

    * le report de Java 7 laissait envisager
    * quitte à se passer de certains « power-concept ».
    * Il en ressort une proposition d’expressions Lambda relativement allégée vis à vis des multiples et très complètes propositions de closures qui ont pu être proposées par le passé.
    * ne sont pas définitives et peuvent encore évoluer…
    * C’est là que rentrent en jeu les types SAM.
    * Et encore la syntaxe peut encore être allégée.
    * ils seront déduits en fonction du type SAM correspondant
    * les APIs existantes qui n’avaient pas été conçues pour cela.
    * de manière sécurisée (vérifiée à la compilation).
    * La syntaxe devrait correspondre à celle utilisée dans la javadoc pour référencer une méthode, c’est à dire le nom de la classe séparée du nom de la méthode par le caractère #.
    * les Method References devraient également
    * des méthodes d’extension qui préciseraient l’implémentation
    * les méthodes d’extension rajoutées malgré leurs absences
    * avec un compilateur plus anciens

    C’est fou, je vois les fautes chez les autres alors que j’en fait moi même 2 à la ligne si je ne me relis pas 10 fois. C’est pour ça que je n’aime pas les commentaires de ce blog que l’on ne peut pas éditer si on fait des erreurs d’orthographe ou de mise en page.

  4. adiguba Auteur de l’article

    La syntaxe n’est pas définitive…

    En ce qui concerne la transparence et les structures de contrôles, les deux sont un peu lié.
    Je pense qu’il y a une volonté de ne pas trop compliquer les choses…

    Quand au futur framework Fork/Join, il nécessite de définir un grand nombre d’interface, ce qui aurait pu être éviter avec les « function types ». Enfin en quelque sorte puisque les « function types » généraient implicitement des interfaces…

    a++

  5. Avatar de UtherUther

    Je suis quelque peu les discutions sur la mailing list et j’ai des sentiment mitigés sur l’évolution de la chose.

    Type SAM:
    Il faut également noter que les types SAM deviennent la seule manière de stocker une lambda étant donné que les « fonction type » ont étés retirés.
    Dans les premières propositions, plutôt que d’utiliser une classe SAM pour

    Comparator comparator = #(String a, String b) { return a.compareToIgnoreCase(b); }; &nbsp;<br />

    on pouvait faire:

    #int(String,String) comparator = #(String a, String b)

    Au début, j’ai vu ça comme une grosse régression, mais en y réfléchissant davantage, il me semble que ça ne devrait pas être si gênant que ça pour moi et il est vrai que dans certains cas, ont perdait beaucoup en lisibilité.
    Par contre il semblerai que ça pose des problèmes pour le futur framework Fork/Join

    Expressions Lambda
    Je regrette que la proposition de « transparence » n’ait pas été retenue. Ca aurait permis que les lambdas apportent vraiment un plus par rapport à l’utilisation d’une classe anonyme.
    L’idée était que la lambda soit considérée comme un bloc de code et non comme une méthode. Donc les mots-clé break et continue utilisés à l’intérieur d’une lambda auraient permit de sortir d’un bloc de boucle de niveau supérieur à la lambda. De même return aurait terminé la méthode faisant appel à la lambda.

    Method reference
    A noter que Oracle réfléchit a la notation suivante:

    #System.getProperties()    //au lieu de System#getProperties()

    Personnellement, je préfère le # devant. C’est vrai que dans le cas d’un:

    #composant.getText().length()     //au lieu de composant.getText()#length()

    ça peux perturber, mais quand on est assez tordu pour faire ça, faut assumer.

    Je regrette par contre que les structures de contrôles ne soient plus envisagées. Je pense que c’est vraiment « le truc » qui manque à Java et qui pourrait rendre beaucoup plus naturel l’utilisation des concepts java qui font recours aux SAM comme l’EDT.

  6. CyberChouan

    J’approuve. Vivement la sortie (en espérant que cette fois, elle ne soit plus retardée).

    Ce qui est un peu frustrant, c’est qu’entre la sortie et le moment où les clients accepteront d’y passer, il risque de s’écouler un bout de temps pendant lequel on devra continuer de développer en Java6 (quand ce n’est pas 5 ou 1.4). Mais bon… j’admets volontiers que les réserves et le recul face au « dernier truc à la mode » sont légitimes. ;-)

Laisser un commentaire