Gibt es einen Leistungsvorteil bei der Verwendung der Methodenreferenzsyntax anstelle der Lambda-Syntax in Java 8?

56

Überspringen Methodenverweise den Overhead der Lambda-Hülle? Könnten sie in der Zukunft?

Gemäß dem Java-Tutorial zu Methodenreferenzen :

Manchmal ... ruft ein Lambda-Ausdruck nur eine vorhandene Methode auf. In diesen Fällen ist es oft klarer, die vorhandene Methode namentlich zu bezeichnen. Mit Methodenreferenzen können Sie dies tun. Sie sind kompakte, leicht zu lesende Lambda-Ausdrücke für Methoden, die bereits einen Namen haben.

Ich bevorzuge die Lambda-Syntax aus mehreren Gründen der Methodenreferenzsyntax:

Lambdas sind klarer

Trotz der Behauptungen von Oracle finde ich die Lambda-Syntax besser lesbar als die Objektmethodenreferenz, da die Methodenreferenzsyntax nicht eindeutig ist:

Bar::foo

Rufen Sie eine statische Methode mit einem Argument für die Klasse x auf und übergeben Sie sie x?

x -> Bar.foo(x)

Oder rufen Sie eine Instanzmethode mit null Argumenten für x auf?

x -> x.foo()

Die Methodenreferenzsyntax könnte für beide stehen. Es verbirgt, was Ihr Code tatsächlich tut.

Lambdas sind sicherer

Wenn Sie Bar :: foo als Klassenmethode referenzieren und Bar später eine gleichnamige Instanzmethode hinzufügt (oder umgekehrt), wird Ihr Code nicht mehr kompiliert.

Sie können Lambdas konsequent verwenden

Sie können jede Funktion in ein Lambda einschließen, sodass Sie überall dieselbe Syntax konsistent verwenden können. Die Methodenreferenzsyntax funktioniert nicht bei Methoden, die primitive Arrays annehmen oder zurückgeben, geprüfte Ausnahmen auslösen oder denselben Methodennamen haben, der als Instanz und statische Methode verwendet wird (da die Methodenreferenzsyntax nicht eindeutig ist, welche Methode aufgerufen wird). . Sie funktionieren nicht, wenn Sie Methoden mit der gleichen Anzahl von Argumenten überladen haben, aber Sie sollten das sowieso nicht tun (siehe Punkt 41 von Josh Bloch), daher können wir das nicht mit Methodenreferenzen vergleichen.

Fazit

Wenn es dafür keine Leistungseinbußen gibt, bin ich versucht, die Warnung in meiner IDE auszuschalten und die Lambda-Syntax konsistent zu verwenden, ohne den gelegentlichen Methodenverweis in meinen Code zu streuen.

PS

Weder hier noch dort, aber in meinen Träumen sehen Objektmethodenreferenzen eher so aus und wenden ohne Lambda-Wrapper eine Aufrufdynamik auf die Methode direkt auf das Objekt an:

_.foo()
GlenPeterson
quelle
2
Dies beantwortet Ihre Frage nicht, aber es gibt andere Gründe für die Verwendung von Verschlüssen. Meiner Meinung nach überwiegen viele von ihnen bei weitem den geringen Aufwand eines zusätzlichen Funktionsaufrufs.
9
Ich denke, Sie sorgen sich vorzeitig um die Leistung. Meiner Meinung nach sollte die Leistung bei der Verwendung einer Methodenreferenz eine untergeordnete Rolle spielen. Es geht nur darum, deine Absicht auszudrücken. Wenn Sie eine Funktion irgendwo übergeben müssen, warum übergeben Sie nicht einfach die Funktion anstelle einer anderen Funktion, die an sie delegiert? Es kommt mir seltsam vor; Wenn Sie nicht wirklich kaufen, dass Funktionen erstklassig sind, benötigen sie eine anonyme Aufsichtsperson, um sie zu bewegen. Um Ihre Frage jedoch direkter zu beantworten, würde es mich nicht wundern, wenn eine Methodenreferenz als statischer Aufruf implementiert werden kann.
Doval
7
.map(_.acceptValue()): Wenn ich mich nicht irre, sieht das der Scala-Syntax sehr ähnlich. Vielleicht versuchen Sie nur, die falsche Sprache zu verwenden.
Giorgio,
2
„Die Methode Referenz - Syntax wird nicht auf Methoden arbeiten , die void zurückgeben“ - Wie so? Ich bin vorbei System.out::printlnan die forEach()ganze Zeit ...?
Lukas Eder
3
Msgstr "Die Methodenreferenzsyntax funktioniert nicht bei Methoden, die primitive Arrays annehmen oder zurückgeben, geprüfte Ausnahmen auslösen ...." Das ist nicht korrekt. In solchen Fällen können Sie sowohl Methodenreferenzen als auch Lambda-Ausdrücke verwenden, sofern die betreffende Funktionsschnittstelle dies zulässt.
Stuart Marks

Antworten:

12

In vielen Szenarien halte ich Lambda und Methodenreferenz für äquivalent. Das Lambda umschließt das Aufrufziel jedoch mit dem deklarierenden Schnittstellentyp.

Zum Beispiel

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

Sie werden sehen, dass die Konsole das Stacktrace ausgibt.

In lambda()der aufrufenden Methode target()gibt lambda$lambda$0(InvokeTest.java:20)es nachvollziehbare Zeileninformationen. Das ist natürlich das Lambda, das Sie schreiben. Der Compiler generiert eine anonyme Methode für Sie. Und dann ist der Aufrufer der Lambda-Methode so etwas wie InvokeTest$$Lambda$2/1617791695.run(Unknown Source)der invokedynamicAufruf in JVM. Dies bedeutet, dass der Aufruf mit der generierten Methode verknüpft ist .

In methodReference()ist der Methodenaufruf target()direkt der InvokeTest$$Lambda$1/758529971.run(Unknown Source), dh der Aufruf ist direkt mit der InvokeTest::targetMethode verbunden .

Fazit

Vergleichen Sie vor allem mit der Methodenreferenz, da die Verwendung des Lambda-Ausdrucks nur noch einen Methodenaufruf für die Erzeugungsmethode von Lambda verursacht.

JasonMing
quelle
1
Die Antwort über die Metafactory ist etwas besser. Ich habe einige nützliche Kommentare hinzugefügt, die relevant sind (nämlich wie Implementierungen wie Oracle / OpenJDK ASM verwenden, um die Wrapper-Klassenobjekte zu generieren, die zum Erstellen von Lambda / Methoden-Handle-Instanzen erforderlich sind.
Ajax
29

Es geht nur um die Metafactory

Erstens müssen die meisten Methodenreferenzen nicht von der Lambda-Metafabrik entzuckert werden, sondern werden einfach als Referenzmethode verwendet. Im Abschnitt "Lambda-Körperzuckerung" des Artikels " Übersetzung von Lambda-Ausdrücken " ("TLE"):

Wenn alle Dinge gleich sind, sind private Methoden den nicht-privaten vorzuziehen, statische Methoden den Instanzmethoden vorzuziehen. Es ist am besten, wenn Lambda-Körper in die innerste Klasse eingeordnet werden, in der der Lambda-Ausdruck vorkommt. Signaturen sollten mit der Körpersignatur des Lambda übereinstimmen, extra Argumente sollten für erfasste Werte an der Vorderseite der Argumentliste vorangestellt werden und würden Methodenreferenzen überhaupt nicht desugar. Es gibt jedoch Ausnahmefälle, in denen wir möglicherweise von dieser Basisstrategie abweichen müssen.

Dies wird weiter unten in TLEs "The Lambda Metafactory" hervorgehoben:

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

Das implArgument gibt die Lambda-Methode an, entweder einen entzuckerten Lambda-Körper oder die in einer Methodenreferenz angegebene Methode.

Eine statische ( Integer::sum) oder unbeschränkte Instanzmethode ( Integer::intValue) ist die "einfachste" oder "bequemste" in dem Sinne, dass sie von einer "Fast-Path" -Metafactory-Variante ohne Desugaring optimal gehandhabt werden kann . Auf diesen Vorteil wird in den "Metafactory-Varianten" von TLE besonders hingewiesen:

Durch das Eliminieren von nicht benötigten Argumenten werden Klassendateien kleiner. Und die Fast-Path-Option senkt die Messlatte für die VM, um die Lambda-Konvertierungsoperation zu unterteilen, sodass sie als "Boxing" -Operation behandelt werden kann und Unbox-Optimierungen erleichtert werden.

Natürlich muss eine instanzerfassende Methodenreferenz ( obj::myMethod) die begrenzte Instanz als Argument für das Methodenhandle für den Aufruf bereitstellen, was bedeuten kann, dass die Verwendung von 'Brücken'-Methoden desugariert werden muss.

Fazit

Ich bin mir nicht ganz sicher, auf was für einen Lambda-Wrapper Sie hinweisen, aber obwohl das Endergebnis der Verwendung Ihrer benutzerdefinierten Lambdas oder Methodenreferenzen dasselbe ist, scheint die Art und Weise , in der sie erreicht werden, ganz anders zu sein kann in Zukunft anders sein, wenn das jetzt nicht der Fall ist. Daher ist es wahrscheinlich, dass Methodenreferenzen von der Metafactory optimaler gehandhabt werden können.

hjk
quelle
1
Dies ist die relevanteste Antwort, da sie tatsächlich die interne Maschinerie berührt, die zur Erzeugung eines Lambda erforderlich ist. Als jemand, der den Lambda-Erstellungsprozess tatsächlich debuggt hat (um herauszufinden, wie stabile IDs für eine bestimmte Lambda / Methoden-Referenz generiert werden, damit sie aus den Maps entfernt werden können), fand ich es ziemlich überraschend, wie viel Arbeit für die Erstellung eines Instanz eines Lambda. Oracle / OpenJDK verwendet ASM, um dynamisch Klassen zu generieren, die, falls erforderlich, über einer Variablen geschlossen werden, auf die in Ihrem Lambda verwiesen wird (oder dem Instanzqualifizierer einer Methodenreferenz) ...
Ajax
1
... Zusätzlich zum Schließen von Variablen erstellen Lambdas insbesondere auch eine statische Methode, die in die deklarierende Klasse eingefügt wird, sodass das erstellte Lambda einen Aufruf hat, um den Funktionen, die Sie aufrufen möchten, zuzuordnen. Eine Methodenreferenz mit einem Instanzqualifizierer nimmt diese Instanz als Parameter für diese Methode. Ich habe nicht getestet, ob eine this :: - Methodenreferenz in einer privaten Klasse diese Schließung überspringt, aber ich habe getestet, dass statische Methoden tatsächlich die Zwischenmethode überspringen (und nur ein Objekt erstellen, das dem Aufrufziel am entspricht Website anrufen).
Ajax
1
Vermutlich wird für eine private Methode auch kein virtueller Versand benötigt, wohingegen öffentlichere Methoden die Instanz (über eine generierte statische Methode) schließen müssen, damit sie für die tatsächliche Instanz aufgerufen werden kann, um sicherzustellen, dass sie Überschreibungen bemerkt. TL; DR: Methodenreferenzen schließen über weniger Argumente hinweg, sodass sie weniger auslaufen und weniger dynamische Codegenerierung erfordern.
Ajax
3
Letzter Spam-Kommentar ... Die dynamische Klassengeneration ist ziemlich schwergewichtig. Immer noch wahrscheinlich besser als das Laden einer anonymen Klasse in den Classloader (da die schwersten Teile nur einmal ausgeführt werden), aber Sie wären wirklich überrascht, wie viel Arbeit in die Erstellung einer Lambda-Instanz steckt. Es wäre interessant, echte Benchmarks von Lambdas im Vergleich zu Methodenreferenzen im Vergleich zu anonymen Klassen zu sehen. Beachten Sie, dass zwei Lambdas oder Methodenreferenzen, die auf die gleichen Dinge verweisen, aber an verschiedenen Stellen zur Laufzeit unterschiedliche Klassen erstellen (auch innerhalb derselben Methode, direkt nacheinander).
Ajax
0

Bei der Verwendung von Lambda-Ausdrücken, die die Leistung beeinträchtigen können, gibt es eine schwerwiegende Konsequenz.

Wenn Sie einen Lambda-Ausdruck deklarieren, erstellen Sie einen Abschluss für den lokalen Bereich.

Was bedeutet das und wie wirkt es sich auf die Leistung aus?

Nun, ich bin froh, dass du gefragt hast. Dies bedeutet, dass jeder dieser kleinen Lambda-Ausdrücke eine kleine anonyme innere Klasse ist und dass er einen Verweis auf alle Variablen enthält, die sich im selben Bereich des Lambda-Ausdrucks befinden.

Dies bedeutet auch eine thisReferenz der Objektinstanz und aller ihrer Felder. Abhängig vom Zeitpunkt des tatsächlichen Aufrufs des Lambdas kann dies zu einem erheblichen Ressourcenleck führen, da der Garbage Collector diese Referenzen nicht loslassen kann, solange das Objekt, das an einem Lambda anhält, noch lebt ...

Roland Tepp
quelle
Gleiches gilt für nicht statische Methodenreferenzen.
Ajax
12
Java-Lambdas sind keine reinen Verschlüsse. Sie sind auch keine anonymen inneren Klassen. Sie enthalten keinen Verweis auf alle Variablen im Gültigkeitsbereich, in denen sie deklariert sind. Sie führen NUR eine Referenz auf die Variablen, auf die sie tatsächlich verweisen. Dies gilt auch für das thisObjekt, auf das verwiesen werden könnte. Ein Lambda verliert also keine Ressourcen, indem es nur Spielraum für sie hat. es hält nur die Objekte fest, die es benötigt. Andererseits kann eine anonyme innere Klasse Ressourcen verlieren, tut dies aber nicht immer. Ein Beispiel finden Sie in diesem Code: a.blmq.us/2mmrL6v
squid314
Diese Antwort ist einfach falsch
Stefan Reich
Danke @StefanReich, das war sehr hilfreich!
Roland Tepp