Poignées de méthode en Java

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, les java.lang.invoke.MethodHandles.

En particulier, nous allons apprendre ce que sont les method handles, comment les créer et comment les utiliser.

Que sont les Method Handles ?

Venons-en à sa définition, telle qu’elle figure dans la documentation de l’API :

Un gestionnaire 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 bas niveau similaire sous-jacente, avec des transformations facultatives des arguments ou des valeurs de retour.

D’une manière plus simple, les handles de méthode sont un mécanisme de bas niveau pour trouver, adapter et invoquer des méthodes.

Les handles de méthode sont immuables et n’ont pas d’état visible.

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

Création de la consultation
Création du type de méthode
Recherche de l’identifiant de la méthode
Appeler le handle de la méthode

Manchettes de méthode et Reflection

Les gestionnaires de méthodes ont été introduits afin de fonctionner parallèlement à l’API java.lang.reflect existante, car ils servent des objectifs différents et présentent 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.

Toutefois, si l’on considère que les performances ne sont pas la seule mesure d’adéquation d’une tâche, il faut également tenir compte du fait que l’API MethodHandles est plus difficile à utiliser en raison de l’absence de mécanismes tels que l’énumération des classes membres, l’inspection des indicateurs d’accessibilité, etc.

Malgré tout, l’API MethodHandles offre la possibilité d’incurver les méthodes, de changer les types de paramètres et de modifier leur ordre.

freestar
Ayant une définition et des objectifs clairs de l’API MethodHandles, nous pouvons maintenant commencer à travailler avec eux, en commençant par le lookup.

Création de la consultation

La première chose à faire lorsque nous voulons créer un handle de méthode est de récupérer la lookup, l’objet usine qui est responsable de la création des handles de méthode pour les méthodes, les constructeurs et les champs, qui sont visibles pour la classe lookup.

Grâce à l’API MethodHandles, il est possible de créer l’objet lookup, avec différents modes d’accès.

Créons le lookup 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() :

freestar
MethodHandles.Lookup lookup = MethodHandles.lookup() ;

Création d’un MethodType

Afin de pouvoir créer le MethodHandle, l’objet lookup a besoin d’une définition de son type et ceci est réalisé par la classe MethodType.

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

La structure d’un MethodType est simple et est formée d’un type de retour ainsi que d’un nombre approprié de types de paramètres qui doivent être correctement appariés entre un handle de méthode et tous ses appelants.

De la même manière que MethodHandle, 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 d’objets 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 retourne une valeur int et accepte un Object :

MethodType mt = MethodType.methodType(int.class, Object.class) ;
Nous pouvons maintenant procéder à la création de MethodHandle.

freestar

Recherche d’un MethodHandle

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

En particulier, la fabrique de lookup fournit un ensemble de méthodes qui nous permettent de trouver le MethodHandle d’une manière appropriée compte tenu de la portée de notre méthode. En commençant par le scénario le plus simple, nous allons explorer les principaux scénarios.

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

Gestion des méthodes statiques

Lorsque nous voulons accéder à une méthode statique, nous pouvons plutôt utiliser la méthode findStatic() :

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

MethodHandle asListMH = publicLookup.findStatic(Arrays.class, « asList », mt) ;
Dans ce cas, nous avons créé un gestionnaire de méthode qui convertit un tableau d’objets en une liste d’objets.

Gestionnaire de méthode pour les constructeurs

L’accès à un constructeur peut se faire à l’aide de la méthode findConstructor().

Créons un method handles qui se comporte comme le constructeur de la classe Integer, en acceptant un attribut String :

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

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

Manche de méthode pour les champs

En utilisant un Method Handle, il est possible d’accéder également aux champs.

freestar
Commençons par définir la classe Book :

public class Book {

String id ;
String title ;

// constructeur

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

MethodHandle getTitleMH = lookup.findGetter(Book.class, « title », String.class) ;
Pour plus d’informations sur la manipulation des variables/champs, consultez le document Java 9 Variable Handles Demystified, où nous abordons l’API java.lang.invoke.VarHandle, ajoutée à Java 9.

Icon for javascript ,programming

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

Il est possible de créer un identifiant de méthode pour une méthode privée à l’aide de l’API java.lang.reflect.

Commençons par ajouter une méthode privée à la classe Book :

private String formatBook() {
return id +  » >  » + title ;
}
Nous pouvons maintenant créer un gestionnaire de méthode qui se comporte exactement comme la méthode formatBook() :

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

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod) ;

Appel d’un Method Handle

Une fois que nous avons créé nos method handles, l’étape suivante consiste à les utiliser. En particulier, la classe MethodHandle fournit 3 façons différentes d’exécuter un method handle : invoke(), invokeWithArugments() et invokeExact().

Commençons par l’option invoke.

freestar

Invoquer un manche de méthode

Lors de l’utilisation de la méthode invoke(), nous imposons que le nombre d’arguments (arité) soit fixe mais nous autorisons l’exécution du casting et du boxing/unboxing des arguments et des types de retour.

Voyons comment il est possible d’utiliser la méthode invoke() avec un argument encadré :

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

Sortie String = (String) replaceMH.invoke(« jovo », Character.valueOf(‘o’), ‘a’) ;

assertEquals(« java », sortie) ;
Dans ce cas, le replaceMH requiert des arguments char, mais invoke() effectue un unboxing sur l’argument Character avant son exécution.

Invocation avec des arguments

L’invocation d’un handle de méthode à l’aide de la méthode invokeWithArguments, est la moins restrictive des trois options.

En effet, elle permet une invocation d’arité variable, en plus du casting et du boxing/unboxing des arguments et des types de retour.

En pratique, cela nous permet de créer une liste d’entiers à partir d’un tableau de valeurs int :

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

Invocation exacte

Dans le cas où nous voulons être plus restrictifs dans la façon dont nous exécutons un handle de méthode (nombre d’arguments et leur type), nous devons utiliser la méthode invokeExact().

En effet, elle ne fournit aucun casting à la classe fournie et requiert un nombre fixe d’arguments.

freestar
Voyons comment nous pouvons additionner deux valeurs int 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éthode invokeExact un nombre qui n’est pas un int, l’invocation conduira à une WrongMethodTypeException.

Travailler avec un tableau

Les MethodHandles ne sont pas conçus pour fonctionner uniquement avec des champs ou des objets, mais également avec des tableaux. En fait, avec l’API asSpreader(), il est possible de créer un descripteur de méthode d’étalement de tableau.

Dans ce cas, le gestionnaire de la méthode accepte un argument tableau, répartissant ses éléments comme arguments positionnels, et éventuellement la longueur du tableau.

Voyons comment nous pouvons étendre un descripteur 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 » })) ;

Amélioration d’un Method Handle

Une fois que nous avons défini un identifiant de méthode, il est possible de l’améliorer en liant l’identifiant de méthode à un argument sans l’invoquer réellement.

Par exemple, dans Java 9, ce type de comportement est utilisé pour optimiser la concaténation des chaînes de caractères.

Voyons comment nous pouvons effectuer une concaténation en liant un suffixe à notre concatMH :

freestar
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 ! »)) ;

Améliorations de Java 9

Avec Java 9, quelques améliorations ont été apportées à l’API MethodHandles dans le but de la rendre beaucoup plus facile à utiliser.

Ces améliorations concernent 3 sujets principaux :

Fonctions de recherche – permettant la recherche de classes à partir de différents contextes et la prise en charge des méthodes non abstraites dans les interfaces.
Gestion des arguments – amélioration des fonctionnalités de pliage, de collecte et de diffusion des arguments.
Combinaisons supplémentaires – ajout de boucles (loop, whileLoop, doWhileLoop…) et d’une meilleure prise en charge des exceptions avec le tryFinally.
Ces changements ont apporté quelques avantages supplémentaires :

Optimisations accrues du compilateur JVM
Réduction de l’instanciation
Précision de l’utilisation de l’API MethodHandles.
Les détails des améliorations apportées sont disponibles dans la Javadoc de l’API MethodHandles.

Conclusion

Dans cet article, nous avons abordé l’API MethodHandles, ce qu’elle représente et comment l’utiliser.

Nous avons également abordé son lien avec l’API Reflection. Étant donné que les method handles permettent d’effectuer des opérations de bas niveau, il est préférable d’éviter de les utiliser, à moins qu’ils ne correspondent parfaitement à la portée du travail.

Comme toujours, le code source complet de cet article est disponible sur Github.

%d blogueurs aiment cette page :