lambda

Lambda-Ausdrücke haben die Programmierer-Welt in den letzten Jahren im Sturm erobert. Die meisten modernen Programmiersprachen haben sie als wichtigen Bestandteil der funktionalen Programmierung übernommen. JVM-basierte Sprachen wie Scala, Groovy und Clojure verwenden Lambda-Ausdrücke gar als zentrale Elemente. Und (endlich) macht auch Java 8 bei der Party mit.

Das Interessante an Lambda-Ausdrücken ist, dass sie von der Perspektive der JVM absolut unsichtbar sind. Die JVM hat keine Ahnung, was eine anonyme Funktion oder ein Lambda-Ausdruck ist. Sie kennt nur den Bytecode, der strikt objektorientiert (OO) ist. Die Ersteller dieser Sprache und der Compiler müssen mit diesen Einschränkungen arbeiten, um neuere, komplexere Sprachelemente zu schaffen.

Zum ersten Mal waren wir damit konfrontiert, als wir Scala-Unterstützung in Takipi einbauen wollten, wozu wir tief in den Scala-Compiler vordringen mussten. Aufgrund der Verfügbarkeit von Java 8 dachte ich, dass es interessant wäre, sich mal anzusehen, wie die Scala- und Java-Compiler Lambda-Ausdrücke umsetzen. Das Ergebnis war ziemlich überraschend.

Für den Anfang nahm ich mir einen einfachen Lambda-Ausdruck vor, der eine Liste von Zeichenketten in eine Liste mit deren Längen umwandelt.


List names = Arrays.asList("1", "2", "3");
Stream lengths = names.stream().map(name -> name.length());

In Scala –

val names = List("1", "2", "3")
val lengths = names.map(name => name.length)

Erstens: Scala

SCalaLam (1)

Der Code

Ich habe javap benutzt, um mir den Bytecode-Inhalt der vom Scala-Compiler erstellten .class anzusehen. Das resultierende Bytecode – also das, was die JVM ausführt – sieht so aus:

// Dies lädt die Namen-Variable in den Stapelspeicher (Stack)
// (die JVM betrachtet sie als Variable Nr. 2).
// Hier bleibt sie eine Weile, bis die
// .map-Funktion sie benutzt.
aload_2

Jetzt wird’s noch interessanter – eine neue Instanz einer synthetischen (vom Compiler erzeugten) class wird erstellt und initialisiert. Dieses Objekt enthält aus der Perspektive der JVM die Lambda-Methode. Das Kuriose ist, dass die Lambda als Bestandteil unserer Methode definiert wird, in Wirklichkeit aber vollständig außerhalb der class besteht.

new myLambdas/Lambda1$$anonfun$1 // Instanziiert das Lambda-Objekt
dup // Tut es wieder in den Stack
// Rufe schließe den Konstruktor auf. Noch einmal:
// Nur ein herkömmliches Objekt aus JVM-Perspektive.
invokespecial myLambdas/Lambda1$$anonfun$1/()V
// Diese beiden (langen) Zeilen laden die unveränderliche (immutable) List. CanBuildFrom-Factory
// erstellt die neue Liste. Diese Factory-Methode ist Teil von
// Scalas Collection-Architektur
getstatic scala/collection/immutable/List$/MODULE$
Lscala/collection/immutable/List$;
invokevirtual scala/collection/immutable/List$/canBuildFrom()
Lscala/collection/generic/CanBuildFrom;
// Im Stack haben wir jetzt das Lambda-Objekt und die Factory.
// In der nächsten Phase muss die .map()-Funktion aufgerufen werden.
// Wir haben ja die names-Variable am Anfang in den Stack geladen. Die
// wird jetzt als die this für den Aufruf der .map()-Funktion verwendet.
// Damit werden auch das Lambda-Objekt und die Factory akzeptiert, um
// die neue Längen-Liste zu erstellen.
invokevirtual scala/collection/immutable/List/map(Lscala/Function1;
Lscala/collection/generic/CanBuildFrom;)Ljava/lang/Object;

Moment mal – was geht in dem Lambda-Objekt vor sich?

Das Lambda-Objekt

Die Lambda class wird von scala.runtime.AbstractFunction1 abgeleitet. Dadurch kann die .map()-Funktion die überschriebene apply() polymorphisch aufrufen. Der Code der apply() steht unten:

// Dieser Code lädt this und das Zielobjekt, auf das sie wirken soll,
// prüft, dass es ein String ist, und ruft dann eine apply-Überladung auf,
// die die eigentliche Arbeit macht und den ausgegeben Wert speichert.
aload_0 // lädt this
aload_1 // lädt die String arg
checkcast java/lang/String // stellt sicher, dass es ein String ist/wir ein Objekt haben
// ruft eine weitere apply()-Methode in der synthetischen class auf
invokevirtual myLambdas/Lambda1$$anonfun$1/apply(Ljava/lang/String;)I
// Ergebnis wird gespeichert
invokestatic scala/runtime/BoxesRunTime/boxToInteger(I)Ljava/lang/Integer
areturn

Der eigentliche Code für die .length()-Operation ist in der weiteren apply-Methode eingebettet, die einfach die Länge des Strings ausgibt, was wir ja auch wollten.
Uff… das hat ziemlich lange gedauert!

aload_1
invokevirtual java/lang/String/length()I
ireturn

Für eine so einfache Zeile wie die obige wird ziemlich viel Bytecode generiert – eine zusätzliche class und einige neue Methoden. Das sollte uns jedoch nicht von der Verwendung von Lambdas abbringen (wir schreiben in Scala, nicht in C). Das Beispiel soll nur zeigen, wie komplex diese Elemente sind. Stellt euch mal vor, wie viel Code das insgesamt ist und wie komplex das aussieht, wenn ganze Ketten komplizierter Lambda-Ausdrücke erstellt werden!
Ich hätte gedacht, dass Java 8 das genauso umsetzen würde, aber überraschenderweise wählt Java 8 einen ganzen anderen Ansatz.

Java 8 – Ein neuer Ansatz

JavaLam (1)

Der Bytecode ist hier etwas kürzer, aber er macht etwas ziemlich Überraschendes. Er lädt einfach zuerst die names-Variable und ruft ihre .stream()-Methode auf, aber dann macht er etwas Gediegenes. Anstatt ein neues Objekt zu erstellen, das die Lambda-Funktion umhüllt, benutzt der Bytecode hier die neue invokeDynamic-Aufforderung, die mit Java 7 eingeführt würde, um diese Aufrufseite dynamisch mit der eigentlich Lambda-Funktion zu verknüpfen.

aload_1 // Lädt die names-Variable
// Ruft ihre stream()-Funktion auf
invokeinterface java/util/List.stream:()Ljava/util/stream/Stream;
//invokeDynamic Ta-daaa!
invokedynamic #0:apply:()Ljava/util/function/Function;
// Ruft die map()-Funktion auf
invokeinterface java/util/stream/Stream.map:
(Ljava/util/function/Function;)Ljava/util/stream/Stream;

InvokeDynamic magic. Diese JVM-Anweisung wurde mit Java 7 eingeführt, um die JVM flexibler zu gestalten. Sie ermöglicht es dynamischen Sprachen, Symbole in der Runtime zu binden, anstatt alles statisch zu verlinken, wenn der Code durch die JVM erstellt wird.
Dynamic Linking. Wenn ihr euch die eigentliche invokedynamic-Anweisung anseht, werden ihr sehen, dass es dort keinen Hinweis auf die eigentliche Lambda-Funktion (lambda$0) gibt. Dies liegt am Aufbau der invokedynamic (die ist wiederum ein eigenständiger Blog-Eintrag), aber die kurze Begründung sind der Name und die Signatur der Lambda, die in unserem Fall so aussieht:

// Eine Funktion namens lamda$0 die einen String einholt und einen Integer ausgibt.
lambdas/Lambda1.lambda$0:(Ljava/lang/String;)Ljava/lang/Integer;

Diese werden in einem Eintrag in einer separaten Tabelle in der class gespeichert, auf die der der Anweisung zugewiesene Parameter #0 verweist. Diese neue Tabelle hat die Struktur der Bytecode-Spezifizierung zum ersten Mal seit einigen Jahren verändert, weshalb wir auch Takipis Fehleranalyse-Engine daran anpassen mussten.

Der Lambda-Code

Dies ist der Code für den eigentlichen Lambda-Ausdruck. Der ist sehr pragmatisch – er lädt den String-Parameter, ruft die length() auf und speichert das Ergebnis. Er wurde als static-Funktion erstellt, damit ihm kein neues this-Objekt zugewiesen werden musste, wie es in Scala der Fall war.

aload_0
invokevirtual java/lang/String.length:()
invokestatic java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
areturn

Dies ist ein weiteres Beispiel für den Ansatz mit invokedynamic, denn er ermöglicht es uns, die Methode von der Perspektive der .map()-Funktion polymorphisch aufzurufen, ohne jedoch ein umgebendes Objekt (wrapper) oder eine virtuelle Überschreibmethode zuweisen zu müssen. Cool, oder?
Zusammenfassung. Es ist faszinierend zu sehen, wie Java – die “strengste” der modernen Programmiersprachen – jetzt eine dynamische Verknüpfung nutzt, um die neuen Lambda-Ausdrücke umzusetzen. Das ist auch sehr effizient, da keine neue class geladen und erstellt werden muss – die Lambda-Methode ist einfach eine weitere Methode in unserer class.
Java 8 hat auf sehr elegante Weise die mit Java 7 eingeführte neue Technologie genutzt, um Lambda-Ausdrücke sehr ergebnisorientiert umzusetzen. Ist doch schön, festzustellen, dass selbst eine alte ehrwürdige Lady wie Java uns noch ein paar neue Tricks zeigen kann 🙂

Duke-T-shirt

Java developers – Install Takipi now and get a free T-shirt

Java 8

Java 8 Exceptions have never been so beautiful – Try Takipi for Java 8

email
Tal is the CEO of OverOps. Tal has been designing scalable, real-time Java and C++ applications for the past 15 years. He still enjoys analyzing a good bug though, and instrumenting code. In his free time Tal plays Jazz drums.