Method Handles in Java

Methodenhandles in Java

1. Einführung

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 erfahren wir, was Methodenhandles sind, wie sie erstellt und wie sie verwendet werden.

2. Was sind Methodenhandles?

Kommen zu seiner Definition, 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 istmethod handles are a low-level mechanism for finding, adapting and invoking methods.

Methodenhandles sind unveränderlich und haben keinen sichtbaren Status.

Zum Erstellen und Verwenden vonMethodHandle sind 4 Schritte erforderlich:

  • Lookup erstellen

  • Methodentyp erstellen

  • Finden des Methodenhandles

  • Aufrufen des Methodenhandles

2.1. Methodengriffe gegen Reflexion

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

Vom Standpunkt der Leistung aus sind dieMethodHandles API can be much faster than the Reflection API since the access checks are made at creation time rather than at execution time. Dieser Unterschied wird verstärkt, wenn ein Sicherheitsmanager anwesend ist, da die Suche nach Mitgliedern und Klassen zusätzlichen Prü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 dieMethodHandles-API aufgrund fehlender Mechanismen wie Aufzählung der Mitgliederklassen, Überprüfung der Barrierefreiheitsflags und mehr schwieriger zu verwenden ist .

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

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

3. Lookup erstellen

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

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

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

MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

Wenn wir jedoch auch auf die Methodenprivate undprotected zugreifen möchten, können wir stattdessen die Methodelookup() verwenden:

MethodHandles.Lookup lookup = MethodHandles.lookup();

4. MethodType erstellen

UmMethodHandle erstellen zu können, benötigt das Suchobjekt eine Definition seines Typs, und dies wird durch die KlasseMethodType erreicht.

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

Die Struktur vonMethodType 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.

Auf die gleiche Weise wieMethodHandle sind auch die Instanzen vonMethodType unveränderlich.

Lassen Sie uns sehen, wie es möglich ist, einMethodType zu definieren, das einejava.util.List-Klasse als Rückgabetyp und einObject-Array als Eingabetyp angibt:

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

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

Definieren wir einMethodType, das einen int-Wert zurückgibt und einObject akzeptiert:

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

Wir können nun fortfahren,MethodHandle zu erstellen.

5. Finden einesMethodHandle

Nachdem wir unseren Methodentyp definiert haben, müssen wir ihn zum Erstellen vonMethodHandle, über das Objektlookup oderpublicLookup finden und dabei auch die Ursprungsklasse und den Methodennamen angeben.

Insbesondere bietet die Lookup-Factory einset of methods, mit dem 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 MethodefindVirtual()können wir ein MethodHandle für eine Objektmethode erstellen. Erstellen wir eine, basierend auf der Methodeconcat()der KlasseString:

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

5.2. Methodenhandle für statische Methoden

Wenn wir Zugriff auf eine statische Methode erhalten möchten, können wir stattdessen die MethodefindStatic()verwenden:

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

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

In diesem Fall haben wir ein Methodenhandle erstellt, das ein Array vonObjects inList konvertiert.

5.3. Methodenhandle für Konstruktoren

Der Zugriff auf einen Konstruktor kann mit der MethodefindConstructor() erfolgen.

Erstellen wir ein Methodenhandle, das sich als Konstruktor der KlasseIntegerverhält und ein AttributStringakzeptiert:

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

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

5.4. Methodenhandle für Felder

Mit einem Methodenhandle können Sie auch auf Felder zugreifen.

Beginnen wir mit der Definition der KlasseBook:

public class Book {

    String id;
    String title;

    // constructor

}

Wenn eine direkte Zugriffssichtbarkeit zwischen dem Methodenhandle und der deklarierten Eigenschaft vorausgesetzt wird, können wir ein Methodenhandle erstellen, das sich wie ein Getter verhält:

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

Weitere Informationen zum Umgang mit Variablen / Feldern finden Sie inJava 9 Variable Handles Demystified, wo wir die in Java 9 hinzugefügtejava.lang.invoke.VarHandle-API erläutern.

5.5. Methodenhandle für private Methoden

Das Erstellen eines Methodenhandles für eine private Methode kann mithilfe derjava.lang.reflect-API erfolgen.

Beginnen wir mit dem Hinzufügen einerprivate-Methode zurBook-Klasse:

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

Jetzt können wir ein Methodenhandle erstellen, das sich genau wie die MethodeformatBook()verhält:

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

MethodHandle formatBookMH = lookup.unreflect(formatBookMethod);

6. Aufrufen eines Methodenhandles

Nachdem wir unsere Methodenhandles erstellt haben, ist ihre Verwendung der nächste Schritt. Insbesondere bietet die KlasseMethodHandle drei verschiedene Möglichkeiten, ein Methodenhandle auszuführen:invoke(),invokeWithArugments() undinvokeExact().

Beginnen wir mit der Optioninvoke.

6.1. Aufrufen eines Methodenhandles

Bei Verwendung derinvoke()-Methode erzwingen wir die Anzahl der zu fixierenden Argumente (arity), erlauben jedoch das Durchführen von Casting und Boxing / Unboxing der Argumente und Rückgabetypen.

Mal sehen, wie es möglich ist,invoke() mit einem Argument in Kästchen zu verwenden:

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 diesem Fall erfordertreplaceMHchar Argumente, aberinvoke() führt vor seiner Ausführung ein Unboxing für dasCharacter Argument durch.

6.2. Mit Argumenten aufrufen

Das Aufrufen eines Methodenhandles mit der MethodeinvokeWithArgumentsist die am wenigsten einschränkende der drei Optionen.

Tatsächlich erlaubt es einen Aufruf mit variabler Arität, zusätzlich zum Casting und Boxing / Unboxing der Argumente und der Rückgabetypen.

In der Praxis können wirList vonInteger ausgehend vonarray vonint Werten erstellen:

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. Genau aufrufen

Wenn wir die Art und Weise, wie wir ein Methodenhandle ausführen (Anzahl der Argumente und deren Typ), restriktiver gestalten möchten, müssen wir die MethodeinvokeExact()verwenden.

Tatsächlich bietet es kein Casting für die bereitgestellte Klasse und erfordert eine feste Anzahl von Argumenten.

Mal sehen, wie wirsum zweiint Werte mit einem Methodenhandle erreichen können:

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

Wenn wir in diesem Fall beschließen, an dieinvokeExact-Methode eine Zahl zu übergeben, die keineint ist, führt der Aufruf zuWrongMethodTypeException.

7. Arbeiten mit Array

MethodHandles sollen nicht nur mit Feldern oder Objekten, sondern auch mit Arrays funktionieren. Tatsächlich ist es mit derasSpreader()-API möglich, eine Array-Spreading-Methode zu erstellen.

In diesem Fall akzeptiert das Methodenhandle ein Arrayargument, wobei seine Elemente als Positionsargumente und optional die Länge des Arrays verteilt werden.

Lassen Sie uns sehen, wie wir ein Methodenhandle verteilen können, um zu überprüfen, ob die Elemente in einem Array gleich sind:

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. Verbessern eines Methodenhandles

Sobald wir ein Methodenhandle definiert haben, können Sie es erweitern, indem Sie das Methodenhandle an ein Argument binden, ohne es tatsächlich aufzurufen.

In Java 9 wird diese Art von Verhalten beispielsweise verwendet, um die Verkettung vonStringzu optimieren.

Mal sehen, wie wir eine Verkettung durchführen können, indem wir ein Suffix an unsereconcatMH binden:

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-Verbesserungen

Mit Java 9 wurden nur wenige Verbesserungen an derMethodHandles-API vorgenommen, um die Verwendung zu vereinfachen.

Die Verbesserungen betrafen drei Hauptthemen:

  • Lookup functions - Ermöglicht das Nachschlagen von Klassen aus verschiedenen Kontexten und unterstützt nicht abstrakte Methoden in Schnittstellen

  • Argument handling - Verbesserung der Funktionen zum Falten von Argumenten, zum Sammeln von Argumenten und zum Verbreiten von Argumenten

  • Additional combinations - Hinzufügen von Schleifen (loop,whileLoop,doWhileLoop…) und eine bessere Unterstützung bei der Ausnahmebehandlung mittryFinally

Diese Änderungen führten zu einigen zusätzlichen Vorteilen:

  • Erhöhte JVM-Compiler-Optimierungen

  • Instantiierungsreduktion

  • Aktivierte Genauigkeit bei der Verwendung derMethodHandles-API

Details zu den vorgenommenen Verbesserungen finden Sie unterMethodHandles API Javadoc.

10. Fazit

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

Wir haben auch erläutert, wie es mit der Reflection-API zusammenhängt, und 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 inover on Github verfügbar.