Methodenhandles in Java

1. Einleitung

In diesem Artikel werden wir eine wichtige API untersuchen, die in Java 7 eingeführt und in den folgenden Versionen erweitert wurde: java.lang.invoke.MethodHandles .

Insbesondere lernen wir, was Methodenhandles sind, wie man sie erstellt und wie man sie verwendet.

2. Was sind Methodenhandles?

Zur Definition kommen, wie in der API-Dokumentation angegeben:

Ein Methodenhandle ist eine typisierte, direkt ausführbare Referenz auf eine zugrunde liegende Methode, einen Konstruktor, ein Feld oder eine ähnliche Operation auf niedriger Ebene mit optionalen Transformationen von Argumenten oder Rückgabewerten.

Auf einfachere Weise sind Methodenhandles ein Mechanismus auf niedriger Ebene zum Suchen, Anpassen und Aufrufen von Methoden .

Methodenhandles sind unveränderlich und haben keinen sichtbaren Status.

Zum Erstellen und Verwenden eines MethodHandle sind 4 Schritte erforderlich:

  • Lookup erstellen
  • Erstellen des Methodentyps
  • Finden des Methodenhandles
  • Aufrufen des Methodenhandles

2.1. Methodengriffe gegen Reflexion

Methodenhandles wurden eingeführt, um mit der vorhandenen java.lang.reflect- API zusammenzuarbeiten, da sie unterschiedlichen Zwecken dienen und unterschiedliche Eigenschaften aufweisen.

Unter Leistungsgesichtspunkten kann die MethodHandles- API viel schneller als die Reflection-API sein, da die Zugriffsprüfungen eher zur Erstellungszeit als zur Ausführungszeit durchgeführt werden . Dieser Unterschied wird verstärkt, wenn ein Sicherheitsmanager anwesend ist, da die Suche nach Mitgliedern und Klassen zusätzlichen Überprüfungen unterzogen wird.

Angesichts der Tatsache , dass die Leistung nicht das einzige Eignungsmaß für eine Aufgabe ist, müssen wir auch berücksichtigen, dass die MethodHandles- API aufgrund fehlender Mechanismen wie Aufzählung der Mitgliedsklassen, Überprüfung der Barrierefreiheitsflags und mehr schwieriger zu verwenden ist.

Trotzdem bietet die MethodHandles- API die Möglichkeit, Methoden zu curryen , die Parametertypen zu ändern und ihre Reihenfolge zu ändern.

Nachdem wir eine klare Definition und Ziele der MethodHandles- API haben, können wir nun mit der Arbeit beginnen, beginnend mit der Suche.

3. Erstellen der Suche

Wenn Sie ein Methodenhandle erstellen möchten, müssen Sie zunächst die Suche abrufen, das Factory-Objekt, das für die Erstellung von Methodenhandles für Methoden, Konstruktoren und Felder verantwortlich ist, die für die Suchklasse sichtbar sind.

Über die MethodHandles- API ist es möglich, das Suchobjekt mit verschiedenen Zugriffsmodi zu erstellen.

Erstellen wir die Suche, die den Zugriff auf öffentliche Methoden ermöglicht:

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Wenn wir jedoch auch auf private und geschützte Methoden zugreifen möchten , können wir stattdessen die lookup () -Methode verwenden:

MethodHandles.Lookup lookup = MethodHandles.lookup();

4. Erstellen eines MethodType

Um das MethodHandle erstellen zu können, benötigt das Lookup-Objekt eine Definition seines Typs. Dies wird durch die MethodType- Klasse erreicht.

Insbesondere stellt ein MethodType die Argumente und den Rückgabetyp dar, die von einem Methodenhandle akzeptiert und zurückgegeben oder von einem Aufrufer eines Methodenhandles übergeben und erwartet werden .

Die Struktur eines MethodType ist einfach und besteht aus einem Rückgabetyp zusammen mit einer geeigneten Anzahl von Parametertypen, die zwischen einem Methodenhandle und allen seinen Aufrufern ordnungsgemäß übereinstimmen müssen.

Wie bei MethodHandle sind auch die Instanzen eines MethodType unveränderlich.

Lassen Sie uns sehen, wie es möglich ist, einen MethodType zu definieren , der eine java.util.List- Klasse als Rückgabetyp und ein Object- Array als Eingabetyp angibt:

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

Falls die Methode einen primitiven Typ oder void als Rückgabetyp zurückgibt , verwenden wir die Klasse, die diese Typen darstellt (void.class, int.class…).

Definieren wir einen MethodType , der einen int-Wert zurückgibt und ein Objekt akzeptiert :

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

Wir können nun mit der Erstellung von MethodHandle fortfahren .

5. Suchen eines MethodHandle

Sobald wir unsere Methodentyp definiert haben, um eine erstellen MethodHandle, haben wir es durch die finden Lookup oder publicLookup Objekt und bietet auch den Ursprung Klasse und den Namen der Methode.

Insbesondere bietet die Lookup-Factory eine Reihe von Methoden, mit denen wir das Methodenhandle unter Berücksichtigung des Umfangs unserer Methode auf geeignete Weise finden können. Beginnen wir mit dem einfachsten Szenario und untersuchen die wichtigsten.

5.1. Methodenhandle für Methoden

Mit der findVirtual () -Methode können wir ein MethodHandle für eine Objektmethode erstellen. Erstellen wir eine, basierend auf der concat () -Methode der String- Klasse:

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

5.2. Methodenhandle für statische Methoden

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:

  • Verbesserte JVM-Compiler-Optimierungen
  • Instanziierungsreduzierung
  • Aktivierte Genauigkeit bei der Verwendung der MethodHandles- API

Details zu den vorgenommenen Verbesserungen finden Sie in der MethodHandles- API Javadoc.

10. Schlussfolgerung

In diesem Artikel haben wir die MethodHandles- API behandelt, was sie sind und wie wir sie verwenden können.

Wir haben auch besprochen, wie es sich auf die Reflection-API bezieht. Da die Methodenhandles Operationen auf niedriger Ebene ermöglichen, sollte es besser sein, sie nicht zu verwenden, es sei denn, sie passen perfekt zum Aufgabenbereich.

Wie immer ist der vollständige Quellcode für diesen Artikel auf Github verfügbar.