Poignées 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, java.lang.invoke.MethodHandles .

En particulier, nous apprendrons 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?

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

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 des arguments ou des valeurs de retour.

De manière plus simple, les descripteurs de méthode sont un mécanisme de bas niveau pour rechercher, adapter et appeler des méthodes .

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

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

  • Création de la recherche
  • Création du type de méthode
  • Recherche du handle de méthode
  • Appel du handle de méthode

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

Les descripteurs de méthode ont été introduits afin de fonctionner avec l' API java.lang.reflect existante , car ils ont des objectifs différents et ont des caractéristiques différentes.

Du point de vue des performances, l' API MethodHandles peut être beaucoup plus rapide que l'API Reflection puisque les contrôles d'accès sont effectués au moment de la création plutôt qu'au moment de l'exécution . Cette différence est amplifiée si un gestionnaire de sécurité est présent, car les recherches de membres et de classes sont soumises à des contrôles 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' API MethodHandles 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' API MethodHandles 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' API MethodHandles , nous pouvons maintenant commencer à travailler avec eux, en commençant par la recherche.

3. Création de la recherche

La première chose à faire lorsque nous voulons créer un descripteur de méthode est de récupérer la recherche, l'objet de fabrique qui est responsable de la création de descripteurs de méthode pour les méthodes, les constructeurs et les champs, visibles par la classe de recherche.

Grâce à l' API MethodHandles , 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éthodes publiques :

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Cependant, au cas où nous voudrions avoir accès également aux méthodes privées et protégées , nous pouvons utiliser à la place la méthode lookup () :

MethodHandles.Lookup lookup = MethodHandles.lookup();

4. Création d'un MethodType

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

En particulier, un MethodType représente les arguments et le type de retour acceptés et retournés par un handle de méthode ou passés et attendus par un appelant de handle de méthode .

La structure d'un MethodType 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 que MethodHandle , même les instances d'un MethodType sont immuables.

Voyons comment il est possible de définir un MethodType qui spécifie une classe java.util.List comme type de retour et un tableau Object comme type d'entrée:

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

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

Définissons un MethodType qui renvoie une valeur int et accepte un objet :

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

Nous pouvons maintenant procéder à la création de MethodHandle .

5. Recherche d'une méthode

Une fois que nous avons défini notre type de méthode, afin de créer un MethodHandle, nous devons le trouver via l' objet lookup ou publicLookup , en fournissant également la classe d'origine et le nom de la méthode.

En particulier, la fabrique de recherche fournit un ensemble de méthodes qui nous permettent 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éthode findVirtual () nous permet de créer un MethodHandle pour une méthode objet. Créons-en un, basé sur la méthode concat () de la classe String :

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

When we want to gain access to a static method, we can instead use the findStatic() method:

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

In this case, we created a method handle that converts an array of Objects to a List of them.

5.3. Method Handle for Constructors

Gaining access to a constructor can be done using the findConstructor() method.

Let's create a method handles that behaves as the constructor of the Integer class, accepting a String attribute:

MethodType mt = MethodType.methodType(void.class, String.class); MethodHandle newIntegerMH = publicLookup.findConstructor(Integer.class, mt);

5.4. Method Handle for Fields

Using a method handle it's possible to gain access also to fields.

Let's start defining the Book class:

public class Book { String id; String title; // constructor }

Having as precondition a direct access visibility between the method handle and the declared property, we can create a method handle that behaves as a getter:

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

For further information on handling variables/fields, give a look at the Java 9 Variable Handles Demystified, where we discuss the java.lang.invoke.VarHandle API, added in Java 9.

5.5. Method Handle for Private Methods

Creating a method handle for a private method can be done, with the help of the java.lang.reflect API.

Let's start adding a private method to the Book class:

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

Now we can create a method handle that behaves exactly as the formatBook() method:

Method formatBookMethod = Book.class.getDeclaredMethod("formatBook"); formatBookMethod.setAccessible(true); MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

6. Invoking a Method Handle

Once we've created our method handles, use them is the next step. In particular, the MethodHandle class provides 3 different way to execute a method handle: invoke(), invokeWithArugments() and invokeExact().

Let's start with the invoke option.

6.1. Invoking a Method Handle

When using the invoke() method, we enforce the number of the arguments (arity) to be fixed but we allow the performing of casting and boxing/unboxing of the arguments and return types.

Let's see how it's possible to use the invoke() with a boxed argument:

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);

In this case, the replaceMH requires char arguments, but the invoke() performs an unboxing on the Character argument before its execution.

6.2. Invoking With Arguments

Invoking a method handle using the invokeWithArguments method, is the least restrictive of the three options.

In fact, it allows a variable arity invocation, in addition to the casting and boxing/unboxing of the arguments and of the return types.

Coming to practice, this allows us to create a List of Integer starting from an array of int values:

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. Invoking Exact

In case we want to be more restrictive in the way we execute a method handle (number of arguments and their type), we have to use the invokeExact() method.

In fact, it doesn't provide any casting to the class provided and requires a fixed number of arguments.

Let's see how we can sum two int values using a method handle:

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);

If in this case, we decide to pass to the invokeExact method a number that isn't an int, the invocation will lead to WrongMethodTypeException.

7. Working With Array

MethodHandles aren't intended to work only with fields or objects, but also with arrays. As a matter of fact, with the asSpreader() API, it's possible to make an array-spreading method handle.

In this case, the method handle accepts an array argument, spreading its elements as positional arguments, and optionally the length of the array.

Let's see how we can spread a method handle to check if the elements within an array are equals:

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. Enhancing a Method Handle

Once we've defined a method handle, it's possible to enhance it by binding the method handle to an argument without actually invoking it.

For example, in Java 9, this kind of behaviour is used to optimize String concatenation.

Let's see how we can perform a concatenation, binding a suffix to our concatMH:

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. Java 9 Enhancements

With Java 9, few enhancements were made to the MethodHandles API with the aim to make it much easier to use.

The enhancements affected 3 main topics:

  • Lookup functions – allowing class lookups from different contexts and support non-abstract methods in interfaces
  • Argument handling – improving the argument folding, argument collecting and argument spreading functionalities
  • Additional combinations – adding loops (loop, whileLoop, doWhileLoop…) and a better exception handling support with the tryFinally

These changes resulted in few additional benefits:

  • Optimisations accrues du compilateur JVM
  • Réduction de l'instanciation
  • Précision activée dans l'utilisation de l' API MethodHandles

Les détails des améliorations apportées sont disponibles dans le Javadoc API MethodHandles .

10. Conclusion

Dans cet article, nous avons couvert l' API MethodHandles , ce qu'ils sont et comment nous pouvons les utiliser.

Nous avons également discuté de son lien avec l'API Reflection et comme les poignées de méthode permettent des opérations de bas niveau, il devrait être 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 disponible à l'adresse over sur Github.