Wir haben für euch ein kurzes Video hochgeladen, in dem die Themen Lambda-Ausdrücke, Methodenreferenzen und funktionale Interfaces eingeführt werden.
Lambda-Funktionen sind kleine Funktionen, die mit wenig Syntax definiert werden. Dabei sollten die Methoden entsprechend kurz und prägnant sein, sodass wir sie hoffentlich mit einem Blick verstehen können.
Ein Beispiel für die Verwendung von Lambda-Ausdrücken anstatt anonymer innerer Klassen ist das Starten eines Threads. Vor Java 8 war relativ viel Code notwendig:
// Vor Java 8:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Viel Code zu schreiben für einen Thread :-/");
}
}).start();Mit Lambdas kann es so aussehen:
new Thread(() -> System.out.println("Besser :-)")).start();Das funktioniert, weil Runnable ein
FunctionalInterface
ist, also ein Interface, welches genau eine abstrakte Methode beinhaltet.
Mit dem Lambda geben wir die Implementierung für diese abstrakte Methode an und führen sie aus.
Die Syntax für Lambdas ist:
( Lambda-Parameter ) -> { Anweisungen }Wenn der Block von Anweisungen nur aus einem return besteht, können wir sowohl die geschweiften Klammern, als auch das return weglassen.
Ähnliches gilt bei Parametern dea Lambda-Ausdrucks. Wenn wir nur einen Parameter haben, dürfen wir die runden Klammern auslassen.
Der Ausdruck e → e * e ist also ein gültiger Lambda-Ausdruck.
Gelegentlich stoßen wir auf Lambda-Ausdrücke der Form (a,b,c) → doSomething(a,b,c), wo die Parameter des Lambda-Ausdrucks an eine Methode weitergereicht werden.
In diesen Fällen können wir Methodenreferenzen verwenden.
Die Syntax für Methodenreferenzen ist:
KlasseOderInstanz::methodeDas Interface Greeter im Paket greet ist ein funktionales Interface.
Wir wollen zunächst ein wenig mit dem Interface experimentieren.
Wenn du lieber direkt mit der Verwendung weitermachen willst, kannst du diesen Teil einfach überspringen.
Aufgaben
-
Füge eine zweite Methode
void foo()hinzu und beobachte, was passiert. -
Wandele die zweite Methode in eine default Methode um.
default foo() {System.out.println("bar");}
-
Was passiert, wenn du alle Methoden entfernst?
-
Warum dürfen funktionale Interfaces eigentlich nur genau eine abstrakte Methode haben?
Die Klasse GreeterTest enthält einige Tests, bei denen du eigene Lambda-Ausdrücke schreiben sollst, die mit unserem Greeter Interface kompatibel sind.
|
ℹ️
|
Wir wollen uns hier nur die Verwendung von Lambda-Ausdrücken anschauen. Der Code ist nicht besonders sinnvoll ;-) |
Aufgaben
-
Entferne schrittweise die
@DisabledAnnotationen und bringe die Tests zum Laufen.
Für die Verwendung von Variablen, die außerhalb des Lambda-Ausdrucks deklariert sind, gelten die Regeln analog zu den Regeln für anonyme innerer Klassen.
Im Package closure findest du drei Klassen mit Beispielen.
Aufgaben:
-
Schau dir die Klasse
ClosureLocalsan, in der innerhalb der Lambda-Ausdrücke lokale Variablen verwendet werden. Entferne bei den markierten Zeilen die Kommentare und versuche zu verstehen was passiert. -
Schau dir die Klasse
ClosureStatican, in der innerhalb der Lambda-Ausdrücke eine statische Variable verwendet wird. Entferne bei der markierten Zeile die Kommentare und versuche zu verstehen was passiert. -
Schau dir die Klasse
ClosureInstancean, in der innerhalb der Lambda-Ausdrücke eine Instanzvariable verwendet wird. Entferne bei der markierten Zeile die Kommentare und versuche zu verstehen was passiert. -
Schreib einen der Lambda-Ausdrücke in
ClosureLocalsin eine anonyme innere Klasse um.ℹ️Wenn du schon länger mit Java zu tun hast, wunderst du dich vielleicht, dass du die Variablen nicht als finaldeklarieren musst. Seit Java 8 sind alle Variablen standardmäßig final und werden bei einer zweiten Zuweisung als nicht-final markiert. Eine Variable, die nicht mitfinalmarkiert ist, aber nur einmal zugewiesen wird, wird als effectively final bezeichnet.
-
Wir haben eine Liste von Zahlen und wollen diese Liste (warum auch immer) nach dem kleinsten Primfaktor der Zahlen sortieren. Der Code ist in der Klasse
PrimeComperatorschon implementiert und es gibt auch einemainMethode mit einem Beispiel.
Aufgaben:
-
Schreib den Ausdruck
(a, b) → compareByPrimeFactor(a, b)in eine Methodenreferenz um. -
Schreib den Ausdruck
s → { System.out.println(s);}in eine Methodenreferenz um. Du sollst aber keine neue Methode schreiben!
Schreib beide Lambda-Ausdrücke in Methodenreferenzen um.
Wenn in JUnit 5 getestet werden soll, ob eine bestimmte Exception geworfen wird, benutzen wir die Methode assertThrows, die die erwartete Exception-Klasse und einen Supplier übergeben bekommt.
Folgendes Beispiel aus der JUnit Dokumentation zeigt die Verwendung eines Lambda-Ausdrucks im Aufruf der Methode.
+
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}Aufgaben:
-
Warum muss hier unbedingt ein Supplier benutzt werden?
In Fällen, wo wir früher eine anonyme Klasse geschrieben haben, die ein funktionales Interface (z.B. Runnable, ActionListener aus AWT, …) implementiert, können wir heute einen Lambda-Ausdruck verwenden.
Es gibt allerdings Unterschiede zwischen den beiden Varianten.
In der Klasse AnonymousInnerClass sind zwei Varianten implementiert, wie wir Runnable Implementierungen nebenläufig ausführen können.
Aufgaben:
-
Finde mit etwas Debugging Code heraus, worauf sich
thisinnerhalb des Lambda-Ausdrucks bzw. der anonymen inneren Klasse bezieht. -
Ändere die Implementierung, die die anonyme innere Klasse verwendet so ab, dass du Zugriff auf dasselbe Objekt hast, das im Lambda-Ausdruck in
thisgespeichert ist. Du wirst vermutlich eine zusätzliche Variable verwenden müssen anstelle vonthis. -
Der Lambda-Ausdruck resultiert nicht in einer separaten Klasse. Wenn du Gradle verwendest, kannst du dir die Classfiles im Verzeichnis
build/classes/java/main/lambda_vs_innerclassanschauen. Kommentiere auch einmal die Methoderun1aus und schau dir an, welche Klassen dann erzeugt werden. -
Wenn du Lust hast, schau dir auch einmal den disassemblierten Bytecode der Klasse mit
javap -v AnonymousInnerClassan. Du stellst dann fest, dass an Byte 4 der beiden Methoden unterschiedlicher Code aufgerufen wird. Im Falle der anonymen inneren Klasse wird Speicher reserviert für die KlasseAnonymousInnerClass$1, im Falle des Lambda-Ausdrucks wirdinvokedynamicaufgerufen. Wasinvokedynamicgenau macht, geht etwas über den Inhalt dieses Workshops hinaus, aber wenn es dich interessiert, gibt es ein ca. einstündiges Video von Brian Goetz, das die Implementierungsdetails erklärt.