Lambda et le bytecode de la JVM
Vous pouvez vous demander comment le compilateur Java implémente les expressions lambda et comment la machine virtuelle Java (JVM) les gère. Si vous pensez que les expressions lambda peuvent simplement être traduites en classes anonymes, vous devriez continuer à lire. Cette annexe explique brièvement comment les expressions lambda sont compilées, en examinant les fichiers de classe générés.
D.1. Classes anonymes
Nous avons montré au chapitre 2 que des classes anonymes peuvent être utilisées pour déclarer et instancier une classe en même temps. Par conséquent, tout comme les expressions lambda, elles peuvent être utilisées pour fournir l’implémentation d’une interface fonctionnelle.
Comme une expression lambda fournit l’implémentation de la méthode abstraite d’une interface fonctionnelle, il semblerait facile de demander au compilateur Java de traduire une expression lambda dans une classe anonyme pendant le processus de compilation. Mais les classes anonymes ont des caractéristiques indésirables qui affectent la performance des applications:
- Le compilateur génère un nouveau fichier de classe pour chaque classe anonyme. Le nom de fichier ressemble généralement à ClassName$1, où ClassName est le nom de la classe dans laquelle la classe anonyme apparaît, suivi d’un signe dollar et d’un nombre. La génération de nombreux fichiers de classe n’est pas souhaitable car chaque fichier de classe doit être chargé et vérifié avant d’être utilisé, ce qui a un impact sur les performances de démarrage de l’application. Si les lambdas étaient traduits en classes anonymes, vous auriez un nouveau fichier de classe pour chaque lambda.
- Chaque nouvelle classe anonyme introduit un nouveau sous-type pour une classe ou une interface. Si vous aviez cent lambdas différents pour exprimer un Comparateur, cela signifierait une centaine de sous-types différents de Comparateur. Dans certaines situations, cela peut rendre plus difficile l’amélioration des performances d’exécution de la JVM.
D.2. Génération du bytecode
Un fichier source Java est compilé en Java bytecode par le compilateur Java. La machine virtuelle Java peut ensuite exécuter le bytecode généré et lancer l’application. Les classes anonymes et les expressions lambda utilisent des instructions de bytecode différentes lorsqu’elles sont compilées. Vous pouvez inspecter le bytecode et le pool constant de n’importe quel fichier de classe à l’aide de la commande
Essayons d’implémenter une instance de l’interface Function en utilisant l’ancienne syntaxe Java 7, en tant que classe interne anonyme, comme indiqué dans la liste suivante.
Figure D.1. Une fonction implémentée en tant que classe interne anonyme
Pour ce faire, le bytecode généré correspondant à Function et crée en tant que classe interne anonyme sera quelque chose comme ceci:
Ce code montre ce qui suit:
- Un objet de type InnerClass$1 est instancié à l’aide de l’opération bytecode new. Une référence à l’objet nouvellement créé est poussée sur la pile en même temps.
- L’opération dup duplique cette référence sur la pile.
- Cette valeur est ensuite consommée par l’instruction invokespecial, qui initialise l’objet.
- Le sommet de la pile contient maintenant une référence à l’objet, qui est stockée dans le champ f1 de la classe LambdaBytecode en utilisant l’instruction putfield.
InnerClass$1 est le nom généré par le compilateur pour la classe anonyme. Si vous voulez vous rassurer, vous pouvez aussi inspecter le fichier de classe InnerClass$1, et vous trouverez le code pour l’implémentation de l’interface Function:
D.3. InvokeDynamic à la rescousse
Essayons maintenant de faire la même chose en utilisant la nouvelle syntaxe Java 8 en tant qu’expression lambda. Inspectez le fichier de classe généré du code dans la figures suivante.
Figure D.2. Une fonction implémentée avec une expression lambda
Vous trouverez les instructions de bytecode suivantes:
Nous avons expliqué les inconvénients de la traduction d’une expression lambda dans une classe interne anonyme, et en effet vous pouvez voir que le résultat est très différent. La création d’une classe supplémentaire a été remplacée par une instruction dynamique invoquée.
L’instruction dynamique invoquée
L’instruction bytecode invokedynamic a été introduite dans JDK7 pour prendre en charge les langages typés dynamiquement sur la JVM. invokedynamic ajoute un niveau supplémentaire d’indirection lors de l’appel d’une méthode, pour laisser une certaine logique dépendant d’un langage dynamique spécifique, déterminer la cible de l’appel. L’utilisation typique pour cette instruction est quelque chose comme:
Ici les types de a et b ne sont pas connus au moment de la compilation et peuvent changer de temps en temps. Pour cette raison, lorsque la JVM exécute une invokedynamic pour la première fois, elle consulte une méthode de bootstrap, qui implémente la logique dépendante du langage et qui détermine ainsi la méthode à appeler. La méthode de bootstrap renvoie un site d’appel lié. Il y a de fortes chances que si la méthode add est appelée avec deux ints, l’appel suivant sera également avec deux ints. Par conséquent, il n’est pas nécessaire de redécouvrir la méthode à appeler à chaque invocation. Le site d’appel lui-même peut contenir la logique définissant dans quelles conditions il doit être réédité.
Dans la figure D.2, les caractéristiques de l’instruction dynamique invoquée ont été utilisées dans un but légèrement différent de celui pour lequel elles ont été introduites à l’origine. En fait, ici, elle est utiliseé pour retarder la stratégie utilisée pour traduire les expressions lambda en bytecode jusqu’à l’exécution. En d’autres termes, utiliser invokedynamic de cette manière permet de différer la génération de code pour implémenter l’expression lambda jusqu’à l’exécution. Ce choix de conception a des conséquences positives:
- La stratégie utilisée pour traduire le corps d’expression lambda en bytecode devient un détail d’implémentation pur. Elle pourrait également être modifiée dynamiquement, ou optimisé et modifié dans les futures implémentations JVM, en préservant la rétrocompatibilité du bytecode.
- Il n’y a pas de surcharge, comme des champs supplémentaires ou un initialiseur statique, si la lambda n’est jamais utilisée.
- Pour les lambdas sans état (non capturés), il est possible de créer une instance de l’objet lambda, de le mettre en cache et de toujours retourner la même chose. C’est un cas d’utilisation courant, et les gens étaient habitués à le faire explicitement avant Java 8; par exemple, déclarer une instance de comparateur spécifique dans une variable finale statique.
- Il n’y a pas de coût de performance supplémentaire car cette traduction doit être effectuée, et son résultat lié, uniquement lorsque le lambda est invoqué pour la première fois. Toutes les invocations suivantes peuvent ignorer ce chemin lent et appeler l’implémentation précédemment établie.
D.4. Stratégies de génération de code
Une expression lambda est traduite en bytecode en plaçant son corps dans l’une des méthodes statiques créées lors de l’exécution. Une lambda sans état, qui ne capture aucun état de sa portée englobante, comme celui que nous avons défini dans la figure D.2, est le type le plus simple de lambda à traduire. Dans ce cas, le compilateur peut générer une méthode ayant la même signature que l’expression lambda, de sorte que le résultat de ce processus de traduction puisse être vu comme suit:
Le cas d’une expression lambda capturant des variables ou champs locaux finaux (ou effectivement finaux), comme dans l’exemple suivant, est un peu plus complexe:
Dans ce cas, la signature de la méthode générée ne peut pas être la même que l’expression lambda, car il est nécessaire d’ajouter des arguments supplémentaires pour porter l’état supplémentaire du contexte inclus. La solution la plus simple pour y parvenir est de préfixer les arguments de l’expression lambda avec un argument supplémentaire pour chacune des variables capturées, du coup la méthode générée pour implémenter l’ancienne expression lambda sera quelque chose comme ceci:
Plus d’informations sur le processus de translation des expressions lambda peuvent être trouvées ici: http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html.