CodeGym /Java-Blog /Random-DE /Eine Erklärung von Lambda-Ausdrücken in Java. Mit Beispie...
John Squirrels
Level 41
San Francisco

Eine Erklärung von Lambda-Ausdrücken in Java. Mit Beispielen und Aufgaben. Teil 2

Veröffentlicht in der Gruppe Random-DE
Für wen ist dieser Artikel?
  • 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.
Wenn Sie nicht in eine dieser Kategorien passen, ist dieser Artikel möglicherweise langweilig, fehlerhaft oder im Allgemeinen nicht Ihr Ding. In diesem Fall können Sie gerne auf andere Dinge eingehen oder, wenn Sie sich in der Materie gut auskennen, gerne in den Kommentaren Vorschläge machen, wie ich den Artikel verbessern oder ergänzen könnte. Eine Erklärung von Lambda-Ausdrücken in Java.  Mit Beispielen und Aufgaben.  Teil 2 - 1Das Material erhebt keinen Anspruch auf akademischen Wert, geschweige denn auf Neuheit. Ganz im Gegenteil: Ich werde versuchen, Dinge, die (für manche Menschen) komplex sind, so einfach wie möglich zu beschreiben. Eine Bitte, die Stream-API zu erklären, hat mich dazu inspiriert, dies zu schreiben. Ich habe darüber nachgedacht und bin zu dem Schluss gekommen, dass einige meiner Stream-Beispiele ohne Verständnis für Lambda-Ausdrücke unverständlich wären. Wir beginnen also mit Lambda-Ausdrücken.

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 counterist unsere Variable eine Referenz auf ein AtomicIntegerObjekt. 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:  counterSie 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 mainMethode 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 RunnableObjekt 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 xund 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 ConsumerSchnittstelle 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 aufConsumerSchnittstelle (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 voidund versucht, voidan zu übergeben forEach(), das stattdessen ein ConsumerObjekt erwartet.

Syntax für Methodenreferenzen

Es ist ganz einfach:
  1. 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 
        } 
    }
    
  2. 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 
        } 
    }
    
  3. 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); 
            } 
        } 
    }
    
  4. 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 Klasse thisverweist 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?

Und natürlich habe ich jede Menge Sachen bei Google gefunden :)
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION