Invokedynamic 101

La version Java 7 d'Oracle a introduit une nouvelle invokedynamicinstruction bytecode dans la machine virtuelle Java (JVM) et un nouveau java.lang.invokepackage API dans la bibliothèque de classes standard. Cet article vous présente cette instruction et cette API.

Le quoi et le comment de la dynamique invoquée

Q: Qu'est-ce que c'est invokedynamic?

R: invokedynamic est une instruction bytecode qui facilite l'implémentation de langages dynamiques (pour la JVM) via l'appel de méthode dynamique. Cette instruction est décrite dans l'édition Java SE 7 de la spécification JVM.

Langages dynamiques et statiques

Un langage dynamique (également connu sous le nom de langage à typage dynamique ) est un langage de programmation de haut niveau dont la vérification de type est généralement effectuée à l'exécution, une fonctionnalité connue sous le nom de typage dynamique . La vérification de type vérifie qu'un programme est de type sécurisé : tous les arguments d'opération ont le type correct. Groovy, Ruby et JavaScript sont des exemples de langages dynamiques. (L' @groovy.transform.TypeCheckedannotation oblige Groovy à taper check au moment de la compilation.)

En revanche, un langage statique (également appelé langage à typage statique ) effectue une vérification de type au moment de la compilation, une fonctionnalité connue sous le nom de typage statique . Le compilateur vérifie qu'un programme est de type correct, bien qu'il puisse différer une vérification de type au runtime (pensez aux casts et à l' checkcastinstruction). Java est un exemple de langage statique. Le compilateur Java utilise ces informations de type pour produire du bytecode fortement typé, qui peut être exécuté efficacement par la JVM.

Q: Comment invokedynamicfacilite la mise en œuvre dynamique d'un langage?

R: Dans un langage dynamique, la vérification de type se produit généralement au moment de l'exécution. Les développeurs doivent passer les types appropriés ou risquer des échecs d'exécution. C'est souvent le cas qui java.lang.Objectest le type le plus précis pour un argument de méthode. Cette situation complique la vérification de type, ce qui affecte les performances.

Un autre défi est que les langages dynamiques offrent généralement la possibilité d'ajouter des champs / méthodes et de les supprimer des classes existantes. Par conséquent, il est nécessaire de reporter la résolution de classe, de méthode et de champ à l'exécution. De plus, il est souvent nécessaire d'adapter un appel de méthode à une cible qui a une signature différente.

Ces défis exigeaient traditionnellement que le support d'exécution ad hoc soit construit au-dessus de la JVM. Cette prise en charge inclut les classes de type wrapper, l'utilisation de tables de hachage pour fournir une résolution dynamique des symboles, etc. Le bytecode est généré avec des points d'entrée au runtime sous la forme d'appels de méthode à l'aide de l'une des quatre instructions d'invocation de méthode:

  • invokestaticest utilisé pour appeler des staticméthodes.
  • invokevirtualest utilisé pour invoquer publicet protectednon- staticméthodes via une répartition dynamique.
  • invokeinterfaceest similaire à invokevirtualsauf pour la méthode de distribution basée sur un type d'interface.
  • invokespecialest utilisé pour appeler des méthodes d'initialisation d'instances (constructeurs) ainsi que des privateméthodes et des méthodes d'une superclasse de la classe courante.

Cette prise en charge de l'exécution affecte les performances. Le bytecode généré nécessite souvent plusieurs appels de méthode JVM pour un appel de méthode de langage dynamique. La réflexion est largement utilisée et contribue à la dégradation des performances. En outre, les nombreux chemins d'exécution différents rendent impossible pour le compilateur juste à temps (JIT) de la JVM d'appliquer des optimisations.

Pour remédier aux mauvaises performances, l' invokedynamicinstruction supprime le support d'exécution ad hoc. Au lieu de cela, le premier appel démarre en invoquant une logique d'exécution qui sélectionne efficacement une méthode cible, et les appels suivants invoquent généralement la méthode cible sans avoir à redémarrer.

invokedynamicprofite également aux implémenteurs de langage dynamique en prenant en charge des cibles de site d'appel qui changent dynamiquement - un site d'appel , plus spécifiquement, un site d'appel dynamique est une invokedynamicinstruction. De plus, comme la JVM prend en charge en interne invokedynamic, cette instruction peut être mieux optimisée par le compilateur JIT.

Poignées de méthode

Q: Je comprends que cela invokedynamicfonctionne avec des descripteurs de méthode pour faciliter l'appel de méthode dynamique. Qu'est-ce qu'un descripteur de méthode?

R: Un descripteur de méthode est «une référence typée, directement exécutable à une méthode sous-jacente, un constructeur, un champ ou une opération de bas niveau similaire, avec des transformations facultatives d'arguments ou de valeurs de retour». En d'autres termes, il est similaire à un pointeur de fonction de style C qui pointe vers un code exécutable - une cible - et qui peut être déréférencé pour appeler ce code. Les descripteurs de méthode sont décrits par la java.lang.invoke.MethodHandleclasse abstraite .

Q: Pouvez-vous fournir un exemple simple de création et d'appel de descripteur de méthode?

R: Consultez la liste 1.

Listing 1. MHD.java(version 1)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findStatic(MHD.class, "hello", MethodType.methodType(void.class)); mh.invokeExact(); } static void hello() { System.out.println("hello"); } }

Le listing 1 décrit un programme de démonstration de descripteur de méthode composé de méthodes de classe main()et hello(). Le but de ce programme est d'appeler hello()via un handle de méthode.

main()La première tâche de ce dernier est d'obtenir un java.lang.invoke.MethodHandles.Lookupobjet. Cet objet est une fabrique pour créer des descripteurs de méthode et est utilisé pour rechercher des cibles telles que des méthodes virtuelles, des méthodes statiques, des méthodes spéciales, des constructeurs et des accesseurs de champ. En outre, il dépend du contexte d'appel d'un site d'appel et applique les restrictions d'accès de descripteur de méthode chaque fois qu'un descripteur de méthode est créé. En d'autres termes, un site d'appel (tel que la main()méthode du Listing 1 agissant comme un site d'appel) qui obtient un objet de recherche ne peut accéder qu'aux cibles accessibles au site d'appel. L'objet de recherche est obtenu en appelant la méthode de la java.lang.invoke.MethodHandlesclasse MethodHandles.Lookup lookup().

publicLookup()

MethodHandlesdéclare également une MethodHandles.Lookup publicLookup()méthode. Contrairement lookup(), qui peut être utilisé pour obtenir un descripteur de méthode pour toute méthode / constructeur ou champ accessible, publicLookup()peut être utilisé pour obtenir un descripteur de méthode vers un champ accessible au public ou une méthode / constructeur accessible au public uniquement.

Après avoir obtenu l'objet de recherche, la MethodHandle findStatic(Class refc, String name, MethodType type)méthode de cet objet est appelée pour obtenir un descripteur de méthode pour la hello()méthode. Le premier argument passé à findStatic()est une référence à la classe ( MHD) à partir de laquelle la méthode ( hello()) est accédée, et le second argument est le nom de la méthode. Le troisième argument est un exemple de type de méthode , qui «représente les arguments et le type de retour acceptés et renvoyés par un descripteur de méthode, ou les arguments et le type de retour transmis et attendus par un appelant de descripteur de méthode». Il est représenté par une instance de la java.lang.invoke.MethodTypeclasse et obtenu (dans cet exemple) en appelant java.lang.invoke.MethodTypela MethodType methodType(Class rtype)méthode de. Cette méthode est appelée car elle hello()ne fournit qu'un type de retour, qui se trouve êtrevoid. Ce type de retour est rendu disponible methodType()en passant void.classà cette méthode.

Le handle de méthode retourné est affecté à mh. Cet objet est ensuite utilisé pour appeler MethodHandlela Object invokeExact(Object... args)méthode de, pour invoquer le handle de méthode. En d'autres termes, les invokeExact()résultats hello()sont appelés et helloécrits dans le flux de sortie standard. Parce qu'il invokeExact()est déclaré lancer Throwable, j'ai ajouté throws Throwableà l'en- main()tête de la méthode.

Q: Dans votre réponse précédente, vous avez mentionné que l'objet de recherche ne peut accéder qu'aux cibles accessibles au site d'appel. Pouvez-vous fournir un exemple illustrant la tentative d'obtention d'un descripteur de méthode sur une cible inaccessible?

R: Consultez la liste 2.

Listing 2. MHD.java(version 2)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class HW { public void hello1() { System.out.println("hello from hello1"); } private void hello2() { System.out.println("hello from hello2"); } } public class MHD { public static void main(String[] args) throws Throwable { HW hw = new HW(); MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(HW.class, "hello1", MethodType.methodType(void.class)); mh.invoke(hw); mh = lookup.findVirtual(HW.class, "hello2", MethodType.methodType(void.class)); } }

Le listing 2 déclare HW(Hello, World) et MHDclasses. HWdéclare une publichello1()méthode d'instance et une privatehello2()méthode d'instance. MHDdéclare une main()méthode qui tentera d'appeler ces méthodes.

main()La première tâche de l 'est d' instancier HWen vue de l 'invocation de hello1()et hello2(). Ensuite, il obtient un objet de recherche et utilise cet objet pour obtenir un descripteur de méthode à appeler hello1(). Cette fois, MethodHandles.Lookupla findVirtual()méthode de est appelée et le premier argument passé à cette méthode est un Classobjet décrivant la HWclasse.

Il s'avère que findVirtual()cela réussira et l' mh.invoke(hw);expression suivante sera appelée hello1(), ce qui entraînera la hello from hello1sortie.

Parce que hello1()c'est public, il est accessible au main()site d'appel de méthode. En revanche, hello2()n'est pas accessible. Par conséquent, la deuxième findVirtual()invocation échouera avec un IllegalAccessException.

Lorsque vous exécutez cette application, vous devez observer la sortie suivante:

hello from hello1 Exception in thread "main" java.lang.IllegalAccessException: member is private: HW.hello2()void, from MHD at java.lang.invoke.MemberName.makeAccessException(MemberName.java:507) at java.lang.invoke.MethodHandles$Lookup.checkAccess(MethodHandles.java:1172) at java.lang.invoke.MethodHandles$Lookup.checkMethod(MethodHandles.java:1152) at java.lang.invoke.MethodHandles$Lookup.accessVirtual(MethodHandles.java:648) at java.lang.invoke.MethodHandles$Lookup.findVirtual(MethodHandles.java:641) at MHD.main(MHD.java:27)

Q: Les listes 1 et 2 utilisent les méthodes invokeExact()et invoke()pour exécuter un descripteur de méthode. Quelle est la différence entre ces méthodes?

R: Bien que invokeExact()et invoke()soient conçus pour exécuter un handle de méthode (en fait, le code cible auquel le handle de méthode fait référence), ils diffèrent lorsqu'il s'agit d'effectuer des conversions de type sur les arguments et la valeur de retour. invokeExact()n'effectue pas de conversion automatique de type compatible sur les arguments. Ses arguments (ou expressions d'argument) doivent être un type exact correspondant à la signature de la méthode, chaque argument étant fourni séparément, ou tous les arguments fournis ensemble sous forme de tableau. invoke()exige que ses arguments (ou expressions d'argument) soient une correspondance de type compatible avec la signature de la méthode - des conversions de type automatiques sont effectuées, chaque argument étant fourni séparément, ou tous les arguments fournis ensemble sous forme de tableau.

Q: Pouvez-vous me fournir un exemple qui montre comment appeler le getter et le setter d'un champ d'instance?

R: Consultez la liste 3.

Listing 3. MHD.java(version 3)

import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; class Point { int x; int y; } public class MHD { public static void main(String[] args) throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); Point point = new Point(); // Set the x and y fields. MethodHandle mh = lookup.findSetter(Point.class, "x", int.class); mh.invoke(point, 15); mh = lookup.findSetter(Point.class, "y", int.class); mh.invoke(point, 30); mh = lookup.findGetter(Point.class, "x", int.class); int x = (int) mh.invoke(point); System.out.printf("x = %d%n", x); mh = lookup.findGetter(Point.class, "y", int.class); int y = (int) mh.invoke(point); System.out.printf("y = %d%n", y); } }

Le listing 3 présente une Pointclasse avec une paire de champs d'instance d'entiers 32 bits nommés xet y. Setter et getter de chaque champ est accessible en appelant MethodHandles.Lookup« s findSetter()et des findGetter()méthodes, et résultant MethodHandleest retourné. Chacun de findSetter()et findGetter()requiert un Classargument qui identifie la classe du champ, le nom du champ et un Classobjet qui identifie la signature du champ.

La invoke()méthode est utilisée pour exécuter un setter ou un getter - dans les coulisses, les champs d'instance sont accessibles via les JVM putfieldet les getfieldinstructions. Cette méthode nécessite qu'une référence à l'objet dont le champ est accédé soit passée comme argument initial. Pour les invocations de setter, un deuxième argument, constitué de la valeur assignée au champ, doit également être passé.

Lorsque vous exécutez cette application, vous devez observer la sortie suivante:

x = 15 y = 30

Q: Votre définition de descripteur de méthode inclut l'expression "avec des transformations facultatives d'arguments ou de valeurs de retour". Pouvez-vous donner un exemple de transformation d'argument?

R: J'ai créé un exemple basé sur la méthode de Mathclasse de la double pow(double a, double b)classe. Dans cet exemple, j'obtiens un descripteur de méthode pour la pow()méthode et je transforme ce descripteur de méthode afin que le deuxième argument passé à pow()soit toujours 10. Consultez la liste 4.