
Von einem Übersetzer: LambdaMetafactory ist wahrscheinlich einer der am meisten unterschätzten Java 8-Mechanismen. Wir haben ihn kürzlich entdeckt, aber seine Fähigkeiten bereits geschätzt. Version 7.0 des CUBA- Frameworks verbessert die Leistung, indem reflektierende Aufrufe zugunsten der Erzeugung von Lambda-Ausdrücken vermieden werden. Eine der Anwendungen dieses Mechanismus in unserem Framework ist das Binden von Anwendungsereignishandlern durch Anmerkungen, eine häufige Aufgabe, ein Analogon von EventListener aus Spring. Wir glauben, dass die Kenntnis der Prinzipien von LambdaFactory in vielen Java-Anwendungen nützlich sein kann, und wir beeilen uns, diese Übersetzung mit Ihnen zu teilen.
In diesem Artikel werden einige wenig bekannte Tricks beim Arbeiten mit Lambda-Ausdrücken in Java 8 und die Einschränkungen dieser Ausdrücke gezeigt. Die Zielgruppe des Artikels sind hochrangige Java-Entwickler, Forscher und Toolkit-Entwickler. Ohne com.sun.*
wird nur die öffentliche Java-API verwendet com.sun.*
Und andere interne Klassen, sodass der Code zwischen verschiedenen JVM-Implementierungen portierbar ist.
Kurzes Vorwort
Lambda-Ausdrücke wurden in Java 8 verwendet, um anonyme Methoden zu implementieren.
in einigen Fällen als Alternative zu anonymen Klassen. Auf Bytecode-Ebene wird der Lambda-Ausdruck durch die invokedynamic
. Diese Anweisung wird verwendet, um eine funktionale Schnittstellenimplementierung zu erstellen, und ihre einzige Methode delegiert den Aufruf an die tatsächliche Methode, die den im Hauptteil des Lambda-Ausdrucks definierten Code enthält.
Zum Beispiel haben wir den folgenden Code:
void printElements(List<String> strings){ strings.forEach(item -> System.out.println("Item = %s", item)); }
Dieser Code wird vom Java-Compiler in Folgendes konvertiert:
private static void lambda_forEach(String item) {
Die invokedynamic
Anweisung kann grob als solcher Java-Code dargestellt werden:
private static CallSite cs; void printElements(List<String> strings) { Consumer<String> lambda;
Wie Sie sehen können, wird LambdaMetafactory
verwendet, um eine CallSite zu erstellen, die eine Factory-Methode bereitstellt , die einen Handler für die Zielmethode zurückgibt. Diese Methode gibt die Implementierung der Funktionsschnittstelle mit invokeExact
. Wenn der Lambda-Ausdruck erfasste Variablen invokeExact
akzeptiert invokeExact
diese Variablen als tatsächliche Parameter.
In Oracle JRE 8 generiert die Metafactory mithilfe von ObjectWeb Asm dynamisch eine Java-Klasse, die eine Klasse erstellt, die eine funktionale Schnittstelle implementiert. Der erstellten Klasse können zusätzliche Felder hinzugefügt werden, wenn der Lambda-Ausdruck externe Variablen erfasst. Dieser sieht aus wie anonyme Java-Klassen, es gibt jedoch die folgenden Unterschiede:
- Eine anonyme Klasse wird vom Java-Compiler generiert.
- Die Klasse zum Implementieren des Lambda-Ausdrucks wird zur Laufzeit von der JVM erstellt.
Die Implementierung der Metafactory hängt vom JVM-Hersteller und der Version ab
Natürlich wird die invokedynamic
nicht nur für Lambda-Ausdrücke in Java verwendet. Es wird hauptsächlich verwendet, wenn dynamische Sprachen in der JVM-Umgebung ausgeführt werden. Die in Java integrierte Nashorn- JavaScript- Engine verwendet diese Anweisung in großem Umfang.
Als nächstes konzentrieren wir uns auf die LambdaMetafactory
Klasse und ihre Funktionen. Weiter
In diesem Abschnitt wird davon ausgegangen, dass Sie sehr gut verstehen, wie metafabrische Methoden funktionieren und was MethodHandle
Tricks mit Lambda-Ausdrücken
In diesem Abschnitt zeigen wir, wie dynamische Lambdas für alltägliche Aufgaben erstellt werden.
Überprüfte Ausnahmen und Lambdas
Es ist kein Geheimnis, dass alle in Java vorhandenen Funktionsschnittstellen keine geprüften Ausnahmen unterstützen. Die Vorteile geprüfter Ausnahmen gegenüber regulären Ausnahmen sind eine sehr langjährige (und immer noch heiße) Debatte.
Was aber, wenn Sie Code mit aktivierten Ausnahmen in Lambda-Ausdrücken in Kombination mit Java Streams verwenden müssen? Beispielsweise müssen Sie eine Liste von Zeichenfolgen in eine Liste von URLs wie folgt konvertieren:
Arrays.asList("http://localhost/", "https://github.com").stream() .map(URL::new) .collect(Collectors.toList())
Eine auslösbare Ausnahme wird im Konstruktor der URL (String) deklariert, sodass sie nicht direkt als Methodenreferenz in der Functiion- Klasse verwendet werden kann.
Sie werden sagen: "Nein, vielleicht, wenn Sie diesen Trick hier anwenden":
public static <T> T uncheckCall(Callable<T> callable) { try { return callable.call(); } catch (Exception e) { return sneakyThrow(e); } } private static <E extends Throwable, T> T sneakyThrow0(Throwable t) throws E { throw (E)t; } public static <T> T sneakyThrow(Throwable e) { return Util.<RuntimeException, T>sneakyThrow0(e); }
Dies ist ein schmutziger Hack. Und hier ist warum:
- Der Try-Catch-Block wird verwendet.
- Die Ausnahme wird erneut ausgelöst.
- Die schmutzige Verwendung der Typlöschung in Java.
Das Problem kann auf "legalere" Weise gelöst werden, indem die folgenden Fakten bekannt sind:
- Überprüfte Ausnahmen werden nur auf Java-Compilerebene erkannt.
- Der
throws
Abschnitt besteht nur aus Metadaten für eine Methode ohne semantischen Wert auf JVM-Ebene. - Überprüfte und normale Ausnahmen sind auf Bytecode-Ebene in der JVM nicht zu unterscheiden.
Die Lösung besteht darin, die Callable.call
Methode in eine Methode ohne throws
Abschnitt zu verpacken:
static <V> V callUnchecked(Callable<V> callable){ return callable.call(); }
Dieser Code wird nicht kompiliert, da die Callable.call
Methode im Abschnitt " Callable.call
" geprüfte Ausnahmen deklariert Callable.call
. Wir können diesen Abschnitt jedoch mit einem dynamisch aufgebauten Lambda-Ausdruck entfernen.
Zuerst müssen wir eine funktionale Schnittstelle deklarieren, die keinen throws
.
aber wer kann den Anruf an Callable.call
:
@FunctionalInterface interface SilentInvoker { MethodType SIGNATURE = MethodType.methodType(Object.class, Callable.class);
Der zweite Schritt besteht darin, eine Implementierung dieser Schnittstelle mit LambdaMetafactory
zu erstellen und den Aufruf der SilentInvoker.invoke
Methode an die Callable.call
Methode zu Callable.call
. Wie bereits erwähnt, wird der SilentInvoker.invoke
auf Bytecode-Ebene ignoriert, sodass die SilentInvoker.invoke
Methode die SilentInvoker.invoke
Methode aufrufen Callable.call
, ohne Ausnahmen zu deklarieren:
private static final SilentInvoker SILENT_INVOKER; final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "invoke", MethodType.methodType(SilentInvoker.class), SilentInvoker.SIGNATURE, lookup.findVirtual(Callable.class, "call", MethodType.methodType(Object.class)), SilentInvoker.SIGNATURE); SILENT_INVOKER = (SilentInvoker) site.getTarget().invokeExact();
Drittens schreiben wir eine Callable.call
, die Callable.call
ohne Ausnahmen zu deklarieren:
public static <V> V callUnchecked(final Callable<V> callable) { return SILENT_INVOKER.invoke(callable); }
Jetzt können Sie Streams ohne Probleme mit aktivierten Ausnahmen neu schreiben:
Arrays.asList("http://localhost/", "https://dzone.com").stream() .map(url -> callUnchecked(() -> new URL(url))) .collect(Collectors.toList());
Dieser Code wird problemlos kompiliert, da callUnchecked
keine geprüften Ausnahmen deklariert. Darüber hinaus kann der Aufruf dieser Methode mithilfe von monomorphem Inline-Caching integriert werden , da nur eine Klasse in der gesamten JVM die SilentOnvoker
Schnittstelle implementiert
Wenn die Implementierung von Callable.call
zur Laufzeit eine Ausnahme Callable.call
, wird sie von der aufrufenden Funktion ohne Probleme Callable.call
:
try{ callUnchecked(() -> new URL("Invalid URL")); } catch (final Exception e){ System.out.println(e); }
Trotz der Möglichkeiten dieser Methode sollten Sie immer die folgende Empfehlung beachten:
Versteckte Ausnahmen mit callUnchecked nur ausblenden, wenn Sie sicher sind, dass der aufgerufene Code keine Ausnahmen auslöst
Das folgende Beispiel zeigt ein Beispiel für diesen Ansatz:
callUnchecked(() -> new URL("https://dzone.com"));
Die vollständige Implementierung dieser Methode ist hier , sie ist Teil des SNAMP Open Source- Projekts .
Arbeiten mit Gettern und Setzern
Dieser Abschnitt ist nützlich für diejenigen, die Serialisierung / Deserialisierung für verschiedene Datenformate wie JSON, Thrift usw. schreiben. Darüber hinaus kann es sehr nützlich sein, wenn Ihr Code stark von der Reflexion für Getters und Setter in JavaBeans abhängt.
Ein in JavaBean deklarierter Getter ist eine Methode namens getXXX
ohne Parameter und mit einem anderen Rückgabedatentyp als void
. Ein in JavaBean deklarierter Setter ist eine Methode namens setXXX
mit einem Parameter und der Rückgabe von void
. Diese beiden Notationen können als funktionale Schnittstellen dargestellt werden:
- Getter kann durch die Function- Klasse dargestellt werden, in der das Argument der Wert
this
. - Setter kann durch die BiConsumer- Klasse dargestellt werden, in der das erste Argument
this
ist und das zweite der Wert, der an Setter übergeben wird.
Jetzt werden wir zwei Methoden erstellen, die jeden Getter oder Setter in diese konvertieren können
funktionale Schnittstellen. Dabei spielt es keine Rolle, dass beide Schnittstellen generisch sind. Nach dem Löschen von Typen
Der reale Datentyp ist Object
. Das automatische LambdaMetafactory
von Rückgabetyp und Argumenten kann mit LambdaMetafactory
. Darüber hinaus hilft die Guava-Bibliothek beim Zwischenspeichern von Lambda-Ausdrücken für dieselben Getter und Setter.
Erster Schritt: Erstellen Sie einen Cache für Getter und Setter. Die Method- Klasse der Reflection-API stellt einen echten Getter oder Setter dar und wird als Schlüssel verwendet.
Der Cache-Wert ist eine dynamisch aufgebaute Funktionsschnittstelle für einen bestimmten Getter oder Setter.
private static final Cache<Method, Function> GETTERS = CacheBuilder.newBuilder().weakValues().build(); private static final Cache<Method, BiConsumer> SETTERS = CacheBuilder.newBuilder().weakValues().build();
Zweitens werden wir Factory-Methoden erstellen, die eine Instanz der Funktionsschnittstelle basierend auf Verweisen auf Getter oder Setter erstellen.
private static Function createGetter(final MethodHandles.Lookup lookup, final MethodHandle getter) throws Exception{ final CallSite site = LambdaMetafactory.metafactory(lookup, "apply", MethodType.methodType(Function.class), MethodType.methodType(Object.class, Object.class),
Die automatische Typkonvertierung zwischen Argumenten vom Typ Object
in funktionalen Schnittstellen (nach dem Löschen des Typs) und realen Argumenttypen und dem Rückgabewert wird unter Verwendung der Differenz zwischen samMethodType
und instantiatedMethodType
(dem dritten bzw. fünften Argument der metafactory-Methode) erreicht. Der Typ der erstellten Instanz der Methode - dies ist die Spezialisierung der Methode, die die Implementierung des Lambda-Ausdrucks bereitstellt.
Drittens werden wir eine Fassade für diese Fabriken mit Unterstützung für das Caching erstellen:
public static Function reflectGetter(final MethodHandles.Lookup lookup, final Method getter) throws ReflectiveOperationException { try { return GETTERS.get(getter, () -> createGetter(lookup, lookup.unreflect(getter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } } public static BiConsumer reflectSetter(final MethodHandles.Lookup lookup, final Method setter) throws ReflectiveOperationException { try { return SETTERS.get(setter, () -> createSetter(lookup, lookup.unreflect(setter))); } catch (final ExecutionException e) { throw new ReflectiveOperationException(e.getCause()); } }
Methodeninformationen, die von einer Instanz der Method
Klasse mithilfe der Java Reflection-API abgerufen wurden, können problemlos in ein MethodHandle
konvertiert werden. Beachten Sie, dass Klasseninstanzmethoden immer ein verstecktes erstes Argument haben, mit dem this
an diese Methode übergeben wird. Statische Methoden haben keinen solchen Parameter. Beispielsweise sieht die tatsächliche Signatur der Methode Integer.intValue()
wie int intValue(Integer this)
. Dieser Trick wird bei der Implementierung von funktionalen Wrappern für Getter und Setter verwendet.
Und jetzt ist es Zeit, den Code zu testen:
final Date d = new Date(); final BiConsumer<Date, Long> timeSetter = reflectSetter(MethodHandles.lookup(), Date.class.getDeclaredMethod("setTime", long.class)); timeSetter.accept(d, 42L);
Dieser Ansatz mit zwischengespeicherten Gettern und Setzern kann effektiv in Serialisierungs- / Deserialisierungsbibliotheken (wie Jackson) verwendet werden, die Getter und Setter während der Serialisierung und Deserialisierung verwenden.
Das Aufrufen von Funktionsschnittstellen mit dynamisch generierten Implementierungen mit LambdaMetaFactory
erheblich schneller als das Aufrufen über die Java Reflection-API
Die Vollversion des Codes finden Sie hier , sie ist Teil der SNAMP- Bibliothek.
Einschränkungen und Fehler
In diesem Abschnitt werden einige Fehler und Einschränkungen im Zusammenhang mit Lambda-Ausdrücken im Java-Compiler und in der JVM erläutert. Alle diese Einschränkungen können in OpenJDK und Oracle JDK mit javac
Version 1.8.0_131 für Windows und Linux reproduziert werden.
Erstellen von Lambda-Ausdrücken aus Methodenhandlern
Wie Sie wissen, kann ein Lambda-Ausdruck mithilfe von LambdaMetaFactory
dynamisch LambdaMetaFactory
. Dazu müssen Sie einen Handler definieren - die MethodHandle
Klasse, die die Implementierung der einzigen Methode angibt, die in der Funktionsschnittstelle definiert ist. Schauen wir uns dieses einfache Beispiel an:
final class TestClass { String value = ""; public String getValue() { return value; } public void setValue(final String value) { this.value = value; } } final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final MethodHandles.Lookup lookup = MethodHandles.lookup(); final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findVirtual(TestClass.class, "getValue", MethodType.methodType(String.class)), MethodType.methodType(String.class)); final Supplier<String> getter = (Supplier<String>) site.getTarget().invokeExact(obj); System.out.println(getter.get());
Dieser Code entspricht:
final TestClass obj = new TestClass(); obj.setValue("Hello, world!"); final Supplier<String> elementGetter = () -> obj.getValue(); System.out.println(elementGetter.get());
Was aber, wenn wir den Methodenhandler, der auf getValue
durch den Handler ersetzen, den die Getterfelder darstellen:
final CallSite site = LambdaMetafactory.metafactory(lookup, "get", MethodType.methodType(Supplier.class, TestClass.class), MethodType.methodType(Object.class), lookup.findGetter(TestClass.class, "value", String.class),
Dieser Code sollte wie erwartet funktionieren, da findGetter
einen Handler zurückgibt, der auf die findGetter
verweist und die richtige Signatur hat. Wenn Sie diesen Code ausführen, wird jedoch die folgende Ausnahme angezeigt:
java.lang.invoke.LambdaConversionException: Unsupported MethodHandle kind: getField
Interessanterweise funktioniert der Getter für das Feld einwandfrei, wenn wir MethodHandleProxies verwenden :
final Supplier<String> getter = MethodHandleProxies .asInterfaceInstance(Supplier.class, lookup.findGetter(TestClass.class, "value", String.class) .bindTo(obj));
Es ist zu beachten, dass MethodHandleProxies
keine gute Möglichkeit ist, Lambda-Ausdrücke dynamisch zu erstellen, da diese Klasse MethodHandle
einfach in eine Proxy-Klasse einschließt und invocationHandler.invoke an MethodHandle.invokeWithArguments delegiert . Dieser Ansatz verwendet Java Reflection und ist sehr langsam.
Wie bereits gezeigt, können nicht alle Methodenhandler zur Laufzeit zum Erstellen von Lambda-Ausdrücken verwendet werden.
Es können nur wenige Arten von Methodenhandlern verwendet werden, um Lambda-Ausdrücke dynamisch zu erstellen.
Hier sind sie:
- REF_invokeInterface: Kann mit Lookup.findVirtual für Schnittstellenmethoden erstellt werden
- REF_invokeVirtual: Kann mit Lookup.findVirtual für virtuelle Methoden der Klasse erstellt werden
- REF_invokeStatic: Erstellt mit Lookup.findStatic für statische Methoden
- REF_newInvokeSpecial: Kann mit Lookup.findConstructor für Konstruktoren erstellt werden
- REF_invokeSpecial: Kann mit Lookup.findSpecial erstellt werden
für private Methoden und frühzeitiges Binden mit virtuellen Methoden der Klasse
Andere Arten von LambdaConversionException
Fehler aus.
Allgemeine Ausnahmen
Dieser Fehler hängt mit dem Java-Compiler und der Möglichkeit zusammen, generische Ausnahmen im Abschnitt " throws
zu deklarieren. Das folgende Codebeispiel veranschaulicht dieses Verhalten:
interface ExtendedCallable<V, E extends Exception> extends Callable<V>{ @Override V call() throws E; } final ExtendedCallable<URL, MalformedURLException> urlFactory = () -> new URL("http://localhost"); urlFactory.call();
Dieser Code muss kompiliert werden, da der Konstruktor der URL
Klasse eine MalformedURLException
. Aber es wird nicht kompiliert. Die folgende Fehlermeldung wird angezeigt:
Error:(46, 73) java: call() in <anonymous Test$CODEgt; cannot implement call() in ExtendedCallable overridden method does not throw java.lang.Exception
Wenn wir jedoch den Lambda-Ausdruck durch eine anonyme Klasse ersetzen, wird der Code kompiliert:
final ExtendedCallable<URL, MalformedURLException> urlFactory = new ExtendedCallable<URL, MalformedURLException>() { @Override public URL call() throws MalformedURLException { return new URL("http://localhost"); } }; urlFactory.call();
Daraus folgt:
Die Typinferenz für generische Ausnahmen funktioniert in Kombination mit Lambda-Ausdrücken nicht richtig
Einschränkungen des Parametrisierungstyps
Mit dem Zeichen &
: <T extends A & B & C & ... Z>
können Sie ein generisches Objekt mit verschiedenen <T extends A & B & C & ... Z>
erstellen.
Diese Methode zur Bestimmung generischer Parameter wird selten verwendet, wirkt sich jedoch aufgrund einiger Einschränkungen in gewisser Weise auf Lambda-Ausdrücke in Java aus:
- Jede Typeinschränkung mit Ausnahme der ersten muss eine Schnittstelle sein.
- Eine reine Version einer Klasse mit einem solchen generischen Wert berücksichtigt nur die erste Typbeschränkung aus der Liste.
Die zweite Einschränkung führt zu unterschiedlichen Verhaltensweisen des Codes zur Kompilierungszeit und zur Laufzeit, wenn eine Bindung an den Lambda-Ausdruck auftritt. Dieser Unterschied kann mit dem folgenden Code demonstriert werden:
final class MutableInteger extends Number implements IntSupplier, IntConsumer {
Dieser Code ist absolut korrekt und wird erfolgreich kompiliert. Die MutableInteger
Klasse erfüllt die Einschränkungen des generischen Typs T:
MutableInteger
erbt von Number
.MutableInteger
implementiert IntSupplier
.
Der Code stürzt jedoch mit einer Ausnahme zur Laufzeit ab:
java.lang.BootstrapMethodError: call site initialization exception at java.lang.invoke.CallSite.makeSite(CallSite.java:341) at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307) at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297) at Test.minValue(Test.java:77) Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class java.lang.Number; not a subtype of implementation type interface java.util.function.IntSupplier at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233) at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303) at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
Dies liegt daran, dass die JavaStream-Pipeline nur einen reinen Typ erfasst, in unserem Fall die Number
Klasse, und die IntSupplier
Schnittstelle nicht implementiert. Dieses Problem kann behoben werden, indem der Parametertyp explizit in einer separaten Methode deklariert wird, die als Referenz auf die Methode verwendet wird:
private static int getInt(final IntSupplier i){ return i.getAsInt(); } private static <T extends Number & IntSupplier> OptionalInt findMinValue(final Collection<T> values){ return values.stream().mapToInt(UtilsTest::getInt).min(); }
Dieses Beispiel zeigt eine falsche Typinferenz im Compiler und zur Laufzeit.
Die Behandlung mehrerer generischer Einschränkungen für Parametertypen in Verbindung mit der Verwendung von Lambda-Ausdrücken zur Kompilierungszeit und zur Laufzeit ist nicht konsistent