Method Handles in Java

Gestes de méthode en Java

1. introduction

Dans cet article, nous allons explorer une API importante qui a été introduite dans Java 7 et améliorée dans les versions suivantes, lesjava.lang.invoke.MethodHandles.

En particulier, nous allons découvrir ce que sont les descripteurs de méthode, comment les créer et comment les utiliser.

2. Que sont les poignées de méthode?

Venir à sa définition, comme indiqué dans la documentation de l'API:

Un handle de méthode est une référence typée, directement exécutable, à une méthode, un constructeur, un champ ou une opération de niveau inférieur sous-jacente, avec éventuellement des transformations d'arguments ou de valeurs de retour.

De manière plus simple,method handles are a low-level mechanism for finding, adapting and invoking methods.

Les descripteurs de méthode sont immuables et n'ont pas d'état visible.

Pour créer et utiliser unMethodHandle, 4 étapes sont nécessaires:

  • Créer la recherche

  • Création du type de méthode

  • Trouver le handle de méthode

  • Invoquer le descripteur de méthode

2.1. Poignées de méthode vs réflexion

Les descripteurs de méthode ont été introduits afin de fonctionner parallèlement à l'APIjava.lang.reflect existante, car ils servent des objectifs différents et ont des caractéristiques différentes.

Du point de vue des performances, lesMethodHandles API can be much faster than the Reflection API since the access checks are made at creation time rather than at execution time. Cette différence est amplifiée si un responsable de la sécurité est présent, car les recherches de membres et de classes sont soumises à des vérifications supplémentaires.

Cependant, étant donné que les performances ne sont pas la seule mesure d'adéquation pour une tâche, nous devons également considérer que l'APIMethodHandles est plus difficile à utiliser en raison du manque de mécanismes tels que l'énumération des classes de membres, l'inspection des indicateurs d'accessibilité, etc. .

Même ainsi, l'APIMethodHandles offre la possibilité de curry des méthodes, de changer les types de paramètres et de changer leur ordre.

Ayant une définition claire et les objectifs de l'APIMethodHandles, nous pouvons maintenant commencer à travailler avec eux, en commençant par la recherche.

3. Création desLookup

La première chose à faire lorsque nous voulons créer un handle de méthode consiste à extraire la recherche, l'objet fabrique responsable de la création des handles de méthodes pour les méthodes, les constructeurs et les champs visibles de la classe de recherche.

Grâce à l'APIMethodHandles, il est possible de créer l'objet de recherche, avec différents modes d'accès.

Créons la recherche qui donne accès aux méthodespublic:

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Cependant, dans le cas où nous voudrions également avoir accès aux méthodesprivate etprotected, nous pouvons utiliser, à la place, la méthodelookup():

MethodHandles.Lookup lookup = MethodHandles.lookup();

4. Créer unMethodType

Afin de pouvoir créer lesMethodHandle, l'objet de recherche nécessite une définition de son type et ceci est réalisé via la classeMethodType.

En particulier,a MethodType represents the arguments and return type accepted and returned by a method handle or passed and expected by a method handle caller.

La structure d'unMethodType est simple et elle est formée par un type de retour avec un nombre approprié de types de paramètres qui doivent être correctement mis en correspondance entre un handle de méthode et tous ses appelants.

De la même manière queMethodHandle, même les instances de aMethodType sont immuables.

Voyons comment il est possible de définir unMethodType qui spécifie une classejava.util.List comme type de retour et un tableauObject comme type d'entrée:

MethodType mt = MethodType.methodType(List.class, Object[].class);

Dans le cas où la méthode retourne un type primitif ouvoid comme type de retour, nous utiliserons la classe représentant ces types (void.class, int.class…).

Définissons unMethodType qui renvoie une valeur int et accepte unObject:

MethodType mt = MethodType.methodType(int.class, Object.class);

Nous pouvons maintenant procéder à la création deMethodHandle.

5. Recherche d'unMethodHandle

Une fois que nous avons défini notre type de méthode, pour créer unMethodHandle,, nous devons le trouver via l’objetlookup oupublicLookup, en fournissant également la classe d’origine et le nom de la méthode.

En particulier, la fabrique de recherche fournit unset of methods qui nous permet de trouver le handle de méthode de manière appropriée compte tenu de la portée de notre méthode. En commençant par le scénario le plus simple, explorons les principaux.

5.1. Handle de méthode pour les méthodes

L'utilisation de la méthodefindVirtual() nous permet de créer un MethodHandle pour une méthode objet. Créons-en un, basé sur la méthodeconcat() de la classeString:

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

5.2. Handle de méthode pour les méthodes statiques

Lorsque nous voulons accéder à une méthode statique, nous pouvons à la place utiliser la méthodefindStatic():

MethodType mt = MethodType.methodType(List.class, Object[].class);

MethodHandle asListMH = publicLookup.findStatic(Arrays.class, "asList", mt);

Dans ce cas, nous avons créé un descripteur de méthode qui convertit un tableau deObjects enList d'entre eux.

5.3. Handle de méthode pour les constructeurs

L'accès à un constructeur peut être fait en utilisant la méthodefindConstructor().

Créons un descripteur de méthode qui se comporte comme le constructeur de la classeInteger, en acceptant un attributString:

MethodType mt = MethodType.methodType(void.class, String.class);

MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);

5.4. Handle de méthode pour les champs

En utilisant un descripteur de méthode, il est possible d’accéder également aux champs.

Commençons par définir la classeBook:

public class Book {

    String id;
    String title;

    // constructor

}

Ayant comme condition préalable une visibilité d'accès direct entre le handle de méthode et la propriété déclarée, nous pouvons créer un handle de méthode qui se comporte comme un getter:

MethodHandle getTitleMH = lookup.findGetter(Book.class, "title", String.class);

Pour plus d'informations sur la gestion des variables / champs, jetez un œil àJava 9 Variable Handles Demystified, où nous discutons de l'APIjava.lang.invoke.VarHandle, ajoutée dans Java 9.

5.5. Handle de méthode pour les méthodes privées

La création d'un descripteur de méthode pour une méthode privée peut être effectuée à l'aide de l'APIjava.lang.reflect.

Commençons par ajouter une méthodeprivate à la classeBook:

private String formatBook() {
    return id + " > " + title;
}

Nous pouvons maintenant créer un handle de méthode qui se comporte exactement comme la méthodeformatBook():

Method formatBookMethod = Book.class.getDeclaredMethod("formatBook");
formatBookMethod.setAccessible(true);

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

6. Appel d'un handle de méthode

Une fois que nous avons créé nos poignées de méthode, utilisez-les est l'étape suivante. En particulier, la classeMethodHandle fournit 3 façons différentes d'exécuter un handle de méthode:invoke(),invokeWithArugments() etinvokeExact().

Commençons par l'optioninvoke.

6.1. Appel d'un handle de méthode

Lors de l'utilisation de la méthodeinvoke(), nous imposons le nombre d'arguments (arité) à fixer mais nous autorisons la réalisation du casting et du boxing / unboxing des arguments et des types de retour.

Voyons comment il est possible d’utiliser lesinvoke() avec un argument encadré:

MethodType mt = MethodType.methodType(String.class, char.class, char.class);
MethodHandle replaceMH = publicLookup.findVirtual(String.class, "replace", mt);

String output = (String) replaceMH.invoke("jovo", Character.valueOf('o'), 'a');

assertEquals("java", output);

Dans ce cas, lereplaceMH requiert les argumentschar, mais leinvoke() effectue un déballage sur l'argumentCharacter avant son exécution.

6.2. Appel avec des arguments

L'appel d'un descripteur de méthode à l'aide de la méthodeinvokeWithArguments est la moins restrictive des trois options.

En fait, il permet une invocation d'arity variable, en plus du transtypage et de la mise en forme / décoffrage des arguments et des types de retour.

En pratique, cela nous permet de créer unList deInteger à partir d'unarray de valeursint:

MethodType mt = MethodType.methodType(List.class, Object[].class);
MethodHandle asList = publicLookup.findStatic(Arrays.class, "asList", mt);

List list = (List) asList.invokeWithArguments(1,2);

assertThat(Arrays.asList(1,2), is(list));

6.3. Invoquer Exact

Dans le cas où nous voulons être plus restrictifs dans la manière dont nous exécutons un handle de méthode (nombre d'arguments et leur type), nous devons utiliser la méthodeinvokeExact().

En fait, il ne fournit aucun casting à la classe fournie et nécessite un nombre fixe d'arguments.

Voyons comment nous pouvonssum deux valeursint en utilisant un handle de méthode:

MethodType mt = MethodType.methodType(int.class, int.class, int.class);
MethodHandle sumMH = lookup.findStatic(Integer.class, "sum", mt);

int sum = (int) sumMH.invokeExact(1, 11);

assertEquals(12, sum);

Si dans ce cas, on décide de passer à la méthodeinvokeExact un nombre qui n’est pas unint, l’appel conduira àWrongMethodTypeException.

7. Travailler avec Array

LesMethodHandles ne sont pas destinés à fonctionner uniquement avec des champs ou des objets, mais également avec des tableaux. En fait, avec l’APIasSpreader(), il est possible de créer un handle de méthode de répartition des tableaux.

Dans ce cas, le handle de méthode accepte un argument de tableau, étalant ses éléments sous forme d'arguments de position et éventuellement la longueur du tableau.

Voyons comment nous pouvons diffuser un handle de méthode pour vérifier si les éléments d'un tableau sont égaux:

MethodType mt = MethodType.methodType(boolean.class, Object.class);
MethodHandle equals = publicLookup.findVirtual(String.class, "equals", mt);

MethodHandle methodHandle = equals.asSpreader(Object[].class, 2);

assertTrue((boolean) methodHandle.invoke(new Object[] { "java", "java" }));

8. Amélioration d'une poignée de méthode

Une fois que nous avons défini un descripteur de méthode, il est possible de l’améliorer en liant le descripteur de méthode à un argument sans l’appeler.

Par exemple, dans Java 9, ce type de comportement est utilisé pour optimiser la concaténation deString.

Voyons comment nous pouvons effectuer une concaténation, en liant un suffixe à nosconcatMH:

MethodType mt = MethodType.methodType(String.class, String.class);
MethodHandle concatMH = publicLookup.findVirtual(String.class, "concat", mt);

MethodHandle bindedConcatMH = concatMH.bindTo("Hello ");

assertEquals("Hello World!", bindedConcatMH.invoke("World!"));

9. Améliorations de Java 9

Avec Java 9, peu d'améliorations ont été apportées à l'APIMethodHandles dans le but de la rendre beaucoup plus facile à utiliser.

Les améliorations concernaient 3 sujets principaux:

  • Lookup functions - autorise les recherches de classe à partir de différents contextes et prend en charge les méthodes non abstraites dans les interfaces

  • Argument handling - amélioration des fonctionnalités de pliage d'arguments, de collecte d'arguments et de diffusion d'arguments

  • Additional combinations - ajout de boucles (loop,whileLoop,doWhileLoop…) et une meilleure prise en charge de la gestion des exceptions avec lestryFinally

Ces changements ont eu peu d’avantages supplémentaires:

  • Optimisation accrue du compilateur JVM

  • Réduction d'instanciation

  • Précision activée dans l'utilisation de l'APIMethodHandles

Les détails des améliorations apportées sont disponibles auMethodHandles API Javadoc.

10. Conclusion

Dans cet article, nous avons abordé l'APIMethodHandles, ce qu'elles sont et comment nous pouvons les utiliser.

Nous avons également expliqué son rapport avec l'API Reflection et, puisque les descripteurs de méthode permettent des opérations de bas niveau, il est préférable d'éviter de les utiliser, à moins qu'elles ne correspondent parfaitement à la portée du travail.

Comme toujours, le code source complet de cet article est disponibleover on Github.