- Es richtet sich an Leute, die den ersten Teil dieses Artikels lesen.
- Es ist für Leute gedacht, die glauben, Java Core bereits gut zu kennen, aber keine Ahnung von Lambda-Ausdrücken in Java haben. Oder vielleicht haben sie etwas über Lambda-Ausdrücke gehört, aber es fehlen die Details.
- Es ist für Leute gedacht, die ein gewisses Verständnis für Lambda-Ausdrücke haben, sich aber dennoch von ihnen einschüchtern lassen und nicht daran gewöhnt sind, sie zu verwenden.
Zugriff auf externe Variablen
Lässt sich dieser Code mit einer anonymen Klasse kompilieren?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
Nein. Die counter
Variable muss sein final
. Oder wenn nicht final
, dann kann es zumindest seinen Wert nicht ändern. Das gleiche Prinzip gilt für Lambda-Ausdrücke. Sie können von der Stelle aus, an der sie deklariert sind, auf alle Variablen zugreifen, die sie „sehen“ können. Aber ein Lambda darf sie nicht ändern (ihnen einen neuen Wert zuweisen). Es gibt jedoch eine Möglichkeit, diese Einschränkung in anonymen Klassen zu umgehen. Erstellen Sie einfach eine Referenzvariable und ändern Sie den internen Zustand des Objekts. Dabei ändert sich die Variable selbst nicht (zeigt auf dasselbe Objekt) und kann sicher als markiert werden final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
Hier counter
ist unsere Variable eine Referenz auf ein AtomicInteger
Objekt. Und die incrementAndGet()
Methode wird verwendet, um den Status dieses Objekts zu ändern. Der Wert der Variablen selbst ändert sich während der Ausführung des Programms nicht. Es zeigt immer auf dasselbe Objekt, sodass wir die Variable mit dem Schlüsselwort final deklarieren können. Hier sind die gleichen Beispiele, jedoch mit Lambda-Ausdrücken:
int counter = 0;
Runnable r = () -> counter++;
Dies lässt sich aus demselben Grund nicht kompilieren wie die Version mit einer anonymen Klasse: counter
Sie darf sich nicht ändern, während das Programm ausgeführt wird. Aber alles ist gut, wenn wir es so machen:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Dies gilt auch für aufrufende Methoden. Innerhalb von Lambda-Ausdrücken können Sie nicht nur auf alle „sichtbaren“ Variablen zugreifen, sondern auch alle zugänglichen Methoden aufrufen.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
Obwohl staticMethod()
es privat ist, kann innerhalb der main()
Methode darauf zugegriffen werden, sodass es auch aus einem in der main
Methode erstellten Lambda heraus aufgerufen werden kann.
Wann wird ein Lambda-Ausdruck ausgeführt?
Vielleicht finden Sie die folgende Frage zu einfach, aber Sie sollten sie trotzdem stellen: Wann wird der Code im Lambda-Ausdruck ausgeführt? Wann wird es erstellt? Oder wann es heißt (was noch nicht bekannt ist)? Dies ist relativ einfach zu überprüfen.
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
Bildschirmausgabe:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Sie können sehen, dass der Lambda-Ausdruck ganz am Ende ausgeführt wurde, nachdem der Thread erstellt wurde und erst, wenn die Programmausführung die run()
Methode erreicht. Sicherlich nicht, wenn es deklariert wird. Durch die Deklaration eines Lambda-Ausdrucks haben wir lediglich ein Runnable
Objekt erstellt und beschrieben, wie sich seine run()
Methode verhält. Die Methode selbst wird viel später ausgeführt.
Methodenreferenzen?
Methodenreferenzen stehen nicht in direktem Zusammenhang mit Lambdas, aber ich denke, es macht Sinn, in diesem Artikel ein paar Worte darüber zu sagen. Angenommen, wir haben einen Lambda-Ausdruck, der nichts Besonderes tut, sondern lediglich eine Methode aufruft.
x -> System.out.println(x)
Es empfängt einige x
und nur Anrufe System.out.println()
, die vorbeigehen x
. In diesem Fall können wir es durch einen Verweis auf die gewünschte Methode ersetzen. So was:
System.out::println
Das ist richtig – keine Klammern am Ende! Hier ist ein vollständigeres Beispiel:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
In der letzten Zeile verwenden wir die forEach()
Methode, die ein Objekt akzeptiert, das die Consumer
Schnittstelle implementiert. Auch hier handelt es sich um eine funktionale Schnittstelle mit nur einer void accept(T t)
Methode. Dementsprechend schreiben wir einen Lambda-Ausdruck, der einen Parameter hat (da er in der Schnittstelle selbst eingegeben wird, geben wir den Parametertyp nicht an; wir geben nur an, dass wir ihn nennen werden x
). Im Hauptteil des Lambda-Ausdrucks schreiben wir den Code, der beim accept()
Aufruf der Methode ausgeführt wird. Hier zeigen wir einfach an, was in der Variablen gelandet ist x
. Dieselbe forEach()
Methode durchläuft alle Elemente in der Sammlung und ruft die accept()
Methode bei der Implementierung aufConsumer
Schnittstelle (unser Lambda) und übergibt jedes Element in der Sammlung. Wie gesagt, wir können einen solchen Lambda-Ausdruck (einer, der einfach eine andere Methode klassifiziert) durch einen Verweis auf die gewünschte Methode ersetzen. Dann sieht unser Code so aus:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
Die Hauptsache ist, dass die Parameter der Methoden println()
und accept()
übereinstimmen. Da die println()
Methode alles akzeptieren kann (sie ist für alle Primitivtypen und alle Objekte überladen), können wir anstelle von Lambda-Ausdrücken einfach einen Verweis auf die println()
Methode an übergeben forEach()
. Dann forEach()
wird jedes Element in der Sammlung genommen und direkt an die Methode übergeben println()
. Für alle, die dies zum ersten Mal bemerken, beachten Sie bitte, dass wir nicht anrufen System.out.println()
(mit Punkten zwischen Wörtern und mit Klammern am Ende). Stattdessen übergeben wir einen Verweis auf diese Methode. Wenn wir das schreiben
strings.forEach(System.out.println());
wir werden einen Kompilierungsfehler haben. Vor dem Aufruf von forEach()
erkennt Java, dass System.out.println()
aufgerufen wird, erkennt also, dass der Rückgabewert lautet void
und versucht, void
an zu übergeben forEach()
, das stattdessen ein Consumer
Objekt erwartet.
Syntax für Methodenreferenzen
Es ist ganz einfach:-
Wir übergeben einen Verweis auf eine statische Methode wie folgt:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
Wir übergeben einen Verweis auf eine nicht statische Methode mithilfe eines vorhandenen Objekts, etwa so:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
Wir übergeben einen Verweis auf eine nicht statische Methode mithilfe der Klasse, die sie wie folgt implementiert:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
Wir übergeben eine Referenz wie folgt an einen Konstruktor:
ClassName::new
Methodenreferenzen sind sehr praktisch, wenn Sie bereits über eine Methode verfügen, die perfekt als Rückruf funktionieren würde. Anstatt einen Lambda-Ausdruck zu schreiben, der den Code der Methode enthält, oder einen Lambda-Ausdruck zu schreiben, der einfach die Methode aufruft, übergeben wir in diesem Fall einfach einen Verweis darauf. Und das ist es.
Eine interessante Unterscheidung zwischen anonymen Klassen und Lambda-Ausdrücken
In einer anonymen Klassethis
verweist das Schlüsselwort auf ein Objekt der anonymen Klasse. Wenn wir dies jedoch innerhalb eines Lambda verwenden, erhalten wir Zugriff auf das Objekt der enthaltenden Klasse. Der, in dem wir tatsächlich den Lambda-Ausdruck geschrieben haben. Dies geschieht, weil Lambda-Ausdrücke in eine private Methode der Klasse kompiliert werden, in der sie geschrieben sind. Ich würde die Verwendung dieser „Funktion“ nicht empfehlen, da sie einen Nebeneffekt hat und den Prinzipien der funktionalen Programmierung widerspricht. Allerdings steht dieser Ansatz völlig im Einklang mit OOP. ;)
Woher habe ich meine Informationen und was sollten Sie sonst noch lesen?
- Tutorial auf der offiziellen Website von Oracle. Viele detaillierte Informationen, einschließlich Beispielen.
- Kapitel über Methodenreferenzen im selben Oracle-Tutorial.
- Lassen Sie sich in Wikipedia hineinziehen, wenn Sie wirklich neugierig sind.
GO TO FULL VERSION