Vous aimez ce que vous lisez sur ce blog ?
Envie d'aller plus loin avec véritable formation d'expertise en Java ?
Venez suivre ma formation Masterclasse Expertise Java !

"Même un développeur experimenté a besoin de continuer à apprendre. Et dans cette formation... j'ai appris beaucoup !" - A.G., Java Champion

Prochaines sessions inter-entreprises : 28-31 mars 2017 / 13-16 juin 2017
Sessions intra-entreprises sur demande.
Inscrivez-vous vite !

Enigmatiques lambdas

Tout a commencé sur Twitter, lorsque Jean-Christophe Sirot (@jcsirot / Coding Stories) m'apostrophe à propos d'un étrange problème de compilation d'une lambda.

Pourquoi ce code compile-t-il...

public class Test {
    private final String s;
 
    public Test(String s) {
        this.s = s;
    }
 
    public Consumer<String> c = 
      (x) -> System.out.println(x + "/" + this.s);
}

...alors que celui-là refuse ?

public class Test {
    private final String s;
 
    public Test(String s) {
        this.s = s;
    }
 
    public Consumer<String> c = 
      (x) -> System.out.println(x + "/" + s);
}

La différence semble subtile : l'un référence directement le champ "s", quand l'autre explicite l'accès via this.

Premiers tests

Rapidement, José Paumard (@JosePaumard) signale que le compilateur d'Eclipse réussit à compiler les deux classes. Mais Eclipse utilise un compilateur maison, qui n'a pas forcément le même comportement que celui d'OpenJDK.

De mon côté, je teste donc sur un JDK Oracle 8u45, et j'ai la surprise de constater... qu'aucune des deux classes ne compile. Avec ou sans this, même combat : le compilateur se plaint que la "Variable s might not have been initialized".

Pourtant, le champ est final, et bien initialisé dans le constructeur...
Et si on retire le modificateur "final" ? Là, les classes compilent. Toutes les deux.

Le problème viendrait-il d'une règle particulière de gestion des variables "final" par le compilateur ?
Et ce problème est-il lié particulièrement à l'utilisation des lambdas ?

Pour avoir une explication fiable, je pose la question à Rémi Forax, contributeur majeur aux spécifications et à l'implémentation d'OpenJDK (notamment sur les lambdas).
Bien m'en a pris, car la réponse était loin d'être simple...

L'explication

Voici donc l'explication de Rémi, que je vous retranscris (librement) avec son aimable permission.

Premièrement, le compilateur s'efforce toujours de vérifier que l'on n'accède pas à une référence marquée "final" avant qu'elle ne soit complètement initialisée. Il analyse donc les chemins d'accès à cette variable.
Mais le compilateur n'est pas tout-puissant ; il y a certaines expressions qu'il peut analyser, d'autres non.

Un accès direct à un champ (ici, s) ne lui pose aucun problème. En revanche, avant le JDK 8u20, un bug 8039026 l'empêchait d'analyser une expression déréférencée (ici, this.s).
Passer par this était donc une "astuce" connue permettant de ne pas s'attirer les foudres du compilateur.
C'est sans doute ce qui explique la différence de comportement entre Jean-Christophe et moi.

Maintenant, pourquoi le compilateur estime-t-il que la variable n'est pas initialisée ?
Le problème est subtil, et est bien lié aux lambdas.

Une lambda étant compilée sous la forme d'une méthode résolue par invokedynamic, il est impossible de savoir quand elle sera initialisée. Le compilateur adopte donc une approche radicale (pour ne pas dire paranoïaque), et applique aux lambdas les mêmes règles d'analyse qu'aux constructeurs[1].
En l'état actuel des choses, il est donc interdit donc d'accéder directement à un champ "final" (avec ou sans this).

Toutefois, le compilateur arrêtant de pister l'accès à un champ au moindre appel de méthode, il est toujours possible de passer (discrètement) par un accesseur privé :

public class Test1 {
    private final String s;
 
    public Test1(String s) {
        this.s = s;
   }
 
    private String access_s() {
        // Embrouillage de compilo +9000
        return s;
    }
 
    public Consumer<String> c = 
      (x) -> System.out.println(x + "/" + access_s());
}

Alors oui, ça sent un peu la bidouille, mais d'un autre côté... C'est à vous de voir !

Conclusion

Ca ressemble à un quiz, ça aurait pu en devenir un, mais la réponse était tellement improbable que j'ai préféré vous épargner une calvitie précoce. En tout cas, merci à Jean-Christophe pour ce problème intéressant, et à Rémi pour l'explication !


! NEW : FORMATIONS !

Vous lancez un projet en Java 8 et vous souhaitez vous former proprement à ses fonctionnalités révolutionnaires que sont les Lambdas et les Streams ?
Demandez ma formation "Java 8 : Lambdas et Streams" (1 jour) !
Au programme : syntaxe, design patterns, bonnes pratiques et performance, et de nombreux exercices pratiques.

Envie de plus Je propose également une formation "Masterclass Expertise Java" sur 4 jours.
Une véritable formation d'expertise, pour des développeurs confirmés souhaitant accéder à un niveau supérieur de maîtrise et challenger leurs pratiques de développement.
(Attention, formation exigeante !)

Ces formations sont certifiantes, sérieuses et pragmatiques.
Entièrement rédigées et présentées par votre serviteur, elles sont accompagnées d'un support de cours extrêmement complet en français.
Pour toute information : contact at mokatech point net

Note

[1] Point sur lequel Rémi est en désaccord avec l'expert group - il estime que les lambdas devaient plutôt être analysées comme de simples méthodes


Commentaires

1. Le jeudi 3 septembre 2015, 11:38 par JC Sirot

Pour la petite histoire je suis tombé sur ce problème en convertissant du code "style Java 7" (en l’occurrence des classes anonymes de type Function<String, String>) en lambda afin de rendre mon code plus lisible.

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.