JavaRush /Java-Blog /Random-DE /Beliebt bei Lambda-Ausdrücken in Java. Mit Beispielen und...
Стас Пасинков
Level 26
Киев

Beliebt bei Lambda-Ausdrücken in Java. Mit Beispielen und Aufgaben. Teil 1

Veröffentlicht in der Gruppe Random-DE
Für wen ist dieser Artikel?
  • Für diejenigen, die glauben, Java Core bereits gut zu kennen, aber keine Ahnung von Lambda-Ausdrücken in Java haben. Oder vielleicht haben Sie schon etwas über Lambdas gehört, aber ohne Details.
  • Für diejenigen, die ein gewisses Verständnis für Lambda-Ausdrücke haben, aber immer noch Angst davor haben, sie zu verwenden.
Wenn Sie nicht in eine dieser Kategorien fallen, finden Sie diesen Artikel möglicherweise langweilig, falsch und im Allgemeinen „nicht cool“. In diesem Fall können Sie entweder gerne vorbeischauen oder, wenn Sie sich mit der Thematik gut auskennen, in den Kommentaren vorschlagen, wie ich den Artikel verbessern oder ergänzen könnte. Das Material erhebt keinen Anspruch auf akademischen Wert, geschweige denn auf Neuheit. Ganz im Gegenteil: Darin werde ich versuchen, (für manche) komplexe Dinge möglichst einfach zu beschreiben. Ich wurde zum Schreiben durch eine Bitte inspiriert, die Stream-API zu erklären. Ich habe darüber nachgedacht und bin zu dem Schluss gekommen, dass einige meiner Beispiele zu „Streams“ ohne Verständnis für Lambda-Ausdrücke unverständlich wären. Beginnen wir also mit Lambdas. Beliebt bei Lambda-Ausdrücken in Java.  Mit Beispielen und Aufgaben.  Teil 1 - 1Welche Kenntnisse sind erforderlich, um diesen Artikel zu verstehen:
  1. Verständnis der objektorientierten Programmierung (im Folgenden als OOP bezeichnet), nämlich:
    • Wissen darüber, was Klassen und Objekte sind und was der Unterschied zwischen ihnen ist;
    • Wissen darüber, was Schnittstellen sind, wie sie sich von Klassen unterscheiden, welche Verbindung zwischen ihnen besteht (Schnittstellen und Klassen);
    • Wissen darüber, was eine Methode ist, wie man sie aufruft, was eine abstrakte Methode ist (oder eine Methode ohne Implementierung), was die Parameter/Argumente einer Methode sind und wie man sie dort übergibt;
    • Zugriffsmodifikatoren, statische Methoden/Variablen, endgültige Methoden/Variablen;
    • Vererbung (Klassen, Schnittstellen, Mehrfachvererbung von Schnittstellen).
  2. Kenntnisse in Java Core: Generika, Sammlungen (Listen), Threads.
Nun, fangen wir an.

Eine kleine Geschichte

Lambda-Ausdrücke kamen aus der funktionalen Programmierung und dort aus der Mathematik nach Java. Mitte des 20. Jahrhunderts arbeitete in Amerika ein gewisser Alonzo Church an der Princeton University, der sich sehr für Mathematik und alle Arten von Abstraktionen interessierte. Es war Alonzo Church, der die Lambda-Kalküle erfand, die zunächst aus einigen abstrakten Ideen bestand und nichts mit Programmierung zu tun hatte. Zur gleichen Zeit arbeiteten Mathematiker wie Alan Turing und John von Neumann an derselben Princeton University. Alles passte zusammen: Church erfand das Lambda-Kalkülsystem, Turing entwickelte seine abstrakte Rechenmaschine, die heute als „Turing-Maschine“ bekannt ist. Nun, von Neumann schlug ein Diagramm der Computerarchitektur vor, das die Grundlage moderner Computer bildete (und heute „von Neumann-Architektur“ genannt wird). Zu dieser Zeit erlangten Alonzo Churchs Ideen nicht so viel Ruhm wie die Arbeiten seiner Kollegen (mit Ausnahme des Gebiets der „reinen“ Mathematik). Wenig später interessierte sich jedoch ein gewisser John McCarthy (zur Zeit der Geschichte ebenfalls Absolvent der Princeton University – ein Mitarbeiter des Massachusetts Institute of Technology) für Churchs Ideen. Darauf aufbauend entwickelte er 1958 die erste funktionale Programmiersprache, Lisp. Und 58 Jahre später drangen die Ideen der funktionalen Programmierung als Nummer 8 in Java ein. Es sind noch nicht einmal 70 Jahre vergangen ... Tatsächlich ist dies nicht der längste Zeitraum, um eine mathematische Idee in die Praxis umzusetzen.

Die Essenz

Ein Lambda-Ausdruck ist eine solche Funktion. Sie können sich dies als eine reguläre Methode in Java vorstellen. Der einzige Unterschied besteht darin, dass sie als Argument an andere Methoden übergeben werden kann. Ja, es ist möglich geworden, nicht nur Zahlen, Strings und Katzen an Methoden zu übergeben, sondern auch andere Methoden! Wann könnten wir das brauchen? Zum Beispiel, wenn wir einen Rückruf weitergeben möchten. Wir benötigen die von uns aufgerufene Methode, um eine andere Methode aufrufen zu können, die wir an sie übergeben. Das heißt, dass wir die Möglichkeit haben, in einigen Fällen einen Rückruf und in anderen einen anderen zu übermitteln. Und unsere Methode, die unsere Rückrufe akzeptieren würde, würde sie aufrufen. Ein einfaches Beispiel ist das Sortieren. Nehmen wir an, wir schreiben eine Art knifflige Sortierung, die etwa so aussieht:
public void mySuperSort() {
    // ... hier etwas tun
    if(compare(obj1, obj2) > 0)
    // ... und hier machen wir etwas
}
Wobei ifwir die Methode aufrufen compare(), dort zwei Objekte übergeben, die wir vergleichen, und wir wollen herausfinden, welches dieser Objekte „größer“ ist. Wir werden das „mehr“ vor das „kleinere“ stellen. Ich habe „mehr“ in Anführungszeichen geschrieben, weil wir eine universelle Methode schreiben, die nicht nur in aufsteigender, sondern auch in absteigender Reihenfolge sortieren kann (in diesem Fall ist „mehr“ das wesentlich kleinere Objekt und umgekehrt). . Um die Regel festzulegen, wie wir genau sortieren möchten, müssen wir sie irgendwie an unsere übergeben mySuperSort(). In diesem Fall können wir unsere Methode während des Aufrufs irgendwie „steuern“. Natürlich können Sie zwei separate Methoden mySuperSortAsc()zum mySuperSortDesc()Sortieren in aufsteigender und absteigender Reihenfolge schreiben. Oder übergeben Sie einen Parameter innerhalb der Methode (z. B. booleanif true, sortieren Sie in aufsteigender Reihenfolge und if falsein absteigender Reihenfolge). Was aber, wenn wir nicht eine einfache Struktur, sondern beispielsweise eine Liste von String-Arrays sortieren möchten? Woher mySuperSort()weiß unsere Methode, wie diese String-Arrays sortiert werden sollen? Messen? Nach Gesamtlänge der Wörter? Vielleicht alphabetisch, abhängig von der ersten Zeile im Array? Was aber, wenn wir in einigen Fällen eine Liste von Arrays nach der Größe des Arrays und in einem anderen Fall nach der Gesamtlänge der Wörter im Array sortieren müssen? Ich denke, Sie haben bereits von Komparatoren gehört und dass wir in solchen Fällen einfach ein Komparatorobjekt an unsere Sortiermethode übergeben, in dem wir die Regeln beschreiben, nach denen wir sortieren möchten. Da die Standardmethode sort()nach dem gleichen Prinzip implementiert ist wie , mySuperSort()werde ich in den Beispielen die Standardmethode verwenden sort().
String[] array1 = {"Mama", "Seife", "rahmen"};
String[] array2 = {"ICH", "Sehr", "Ich liebe", "java"};
String[] array3 = {"Welt", "arbeiten", "Dürfen"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Ergebnis:
  1. Mama hat den Rahmen gewaschen
  2. Friede, Arbeit, Mai
  3. Ich liebe Java wirklich
Hier werden die Arrays nach der Anzahl der Wörter in jedem Array sortiert. Ein Array mit weniger Wörtern wird als „kleiner“ betrachtet. Deshalb kommt es am Anfang. Das Wort mit mehr Wörtern gilt als „mehr“ und landet am Ende. Wenn sort()wir der Methode einen weiteren Komparator übergeben (sortByWordsLength), ist das Ergebnis anders:
  1. Friede, Arbeit, Mai
  2. Mama hat den Rahmen gewaschen
  3. Ich liebe Java wirklich
Nun werden die Arrays nach der Gesamtzahl der Buchstaben in den Wörtern eines solchen Arrays sortiert. Im ersten Fall sind es 10 Buchstaben, im zweiten 12 und im dritten 15. Wenn wir nur einen Komparator verwenden, können wir dafür keine separate Variable erstellen, sondern einfach direkt an der Stelle ein Objekt einer anonymen Klasse erstellen Zeitpunkt des Aufrufs der Methode sort(). Ungefähr so:
String[] array1 = {"Mama", "Seife", "rahmen"};
String[] array2 = {"ICH", "Sehr", "Ich liebe", "java"};
String[] array3 = {"Welt", "arbeiten", "Dürfen"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Das Ergebnis ist das gleiche wie im ersten Fall. Aufgabe 1 . Schreiben Sie dieses Beispiel so um, dass die Arrays nicht in aufsteigender Reihenfolge der Anzahl der Wörter im Array, sondern in absteigender Reihenfolge sortiert werden. Das alles wissen wir bereits. Wir wissen, wie man Objekte an Methoden übergibt, wir können dieses oder jenes Objekt an eine Methode übergeben, je nachdem, was wir gerade benötigen, und innerhalb der Methode, an die wir ein solches Objekt übergeben, wird die Methode aufgerufen, für die wir die Implementierung geschrieben haben . Es stellt sich die Frage: Was haben Lambda-Ausdrücke damit zu tun? Vorausgesetzt, ein Lambda ist ein Objekt, das genau eine Methode enthält. Es ist wie ein Methodenobjekt. Eine in ein Objekt eingeschlossene Methode. Sie haben lediglich eine etwas ungewöhnliche Syntax (dazu später mehr). Schauen wir uns diesen Eintrag noch einmal an
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Hier nehmen wir unsere Liste arraysund rufen ihre Methode auf sort(), wobei wir ein Komparatorobjekt mit einer einzigen Methode übergeben compare()(es spielt für uns keine Rolle, wie sie heißt, da sie die einzige in diesem Objekt ist, werden wir sie nicht übersehen). Diese Methode benötigt zwei Parameter, mit denen wir als nächstes arbeiten. Wenn Sie mit IntelliJ IDEA arbeiten , haben Sie wahrscheinlich gesehen, wie es Ihnen diesen Code bietet, um Folgendes deutlich zu verkürzen:
arrays.sort((o1, o2) -> o1.length - o2.length);
So wurde aus sechs Zeilen eine kurze. 6 Zeilen wurden in eine kurze umgeschrieben. Etwas ist verschwunden, aber ich garantiere, dass nichts Wichtiges verschwunden ist, und dieser Code funktioniert genauso wie mit einer anonymen Klasse. Aufgabe 2 . Finden Sie heraus, wie Sie die Lösung für Problem 1 mithilfe von Lambdas umschreiben können (als letzten Ausweg bitten Sie IntelliJ IDEA , Ihre anonyme Klasse in ein Lambda umzuwandeln).

Lassen Sie uns über Schnittstellen sprechen

Im Grunde ist eine Schnittstelle nur eine Liste abstrakter Methoden. Wenn wir eine Klasse erstellen und sagen, dass sie eine Art Schnittstelle implementieren wird, müssen wir in unsere Klasse eine Implementierung der in der Schnittstelle aufgeführten Methoden schreiben (oder als letzten Ausweg sie nicht schreiben, sondern die Klasse abstrakt machen). ). Es gibt Schnittstellen mit vielen verschiedenen Methoden (z. B. List), es gibt Schnittstellen mit nur einer Methode (z. B. demselben Comparator oder Runnable). Es gibt Schnittstellen ohne eine einzige Methode (sog. Marker-Schnittstellen, zum Beispiel Serializable). Solche Schnittstellen, die nur eine Methode haben, werden auch funktionale Schnittstellen genannt . In Java 8 sind sie sogar mit einer speziellen @FunctionalInterface- Annotation gekennzeichnet . Es sind Schnittstellen mit einer einzigen Methode, die für die Verwendung durch Lambda-Ausdrücke geeignet sind. Wie ich oben sagte, ist ein Lambda-Ausdruck eine in ein Objekt eingeschlossene Methode. Und wenn wir irgendwo ein solches Objekt übergeben, übergeben wir tatsächlich diese eine einzige Methode. Es stellt sich heraus, dass es uns egal ist, wie diese Methode heißt. Für uns sind nur die Parameter wichtig, die diese Methode benötigt, und zwar der Methodencode selbst. Ein Lambda-Ausdruck ist im Wesentlichen. Implementierung einer funktionalen Schnittstelle. Wenn wir eine Schnittstelle mit einer Methode sehen, bedeutet das, dass wir eine solche anonyme Klasse mithilfe eines Lambda umschreiben können. Wenn die Schnittstelle mehr/weniger als eine Methode hat, dann passt der Lambda-Ausdruck nicht zu uns und wir werden eine anonyme Klasse oder sogar eine reguläre Klasse verwenden. Es ist Zeit, sich mit den Lambdas auseinanderzusetzen. :) :)

Syntax

Die allgemeine Syntax sieht etwa so aus:
(параметры) -> {тело метода}
Das heißt, Klammern, in denen sich die Methodenparameter befinden, ein „Pfeil“ (das sind zwei Zeichen hintereinander: Minus und größer), danach steht der Körper der Methode wie immer in geschweiften Klammern. Die Parameter entsprechen denen, die in der Schnittstelle bei der Beschreibung der Methode angegeben wurden. Wenn der Typ der Variablen vom Compiler klar definiert werden kann (in unserem Fall ist sicher bekannt, dass wir mit Arrays aus Strings arbeiten, da er Listgenau durch Arrays aus Strings typisiert wird), dann muss der Typ der Variablen String[]nicht angegeben werden geschrieben sein.
Wenn Sie sich nicht sicher sind, geben Sie den Typ an. IDEA markiert ihn dann grau, wenn er nicht benötigt wird.
Weitere Informationen finden Sie beispielsweise im Oracle-Tutorial . Dies wird als „Zieltypisierung“ bezeichnet . Sie können den Variablen beliebige Namen geben, nicht unbedingt die in der Schnittstelle angegebenen. Wenn keine Parameter vorhanden sind, dann nur Klammern. Wenn nur ein Parameter vorhanden ist, nur der Variablenname ohne Klammern. Wir haben die Parameter geklärt, nun geht es um den Hauptteil des Lambda-Ausdrucks selbst. Schreiben Sie innerhalb der geschweiften Klammern den Code wie für eine reguläre Methode. Wenn Ihr gesamter Code nur aus einer Zeile besteht, müssen Sie überhaupt keine geschweiften Klammern schreiben (wie bei Ifs und Schleifen). Wenn Ihr Lambda etwas zurückgibt, sein Körper jedoch aus einer Zeile besteht, returnist es überhaupt nicht notwendig, etwas zu schreiben. Wenn Sie jedoch geschweifte Klammern haben, müssen Sie wie bei der üblichen Methode explizit schreiben return.

Beispiele

Beispiel 1.
() -> {}
Die einfachste Möglichkeit. Und das bedeutungsloseste :). Weil es nichts bewirkt. Beispiel 2.
() -> ""
Auch eine interessante Option. Es akzeptiert nichts und gibt eine leere Zeichenfolge zurück ( returnwird weggelassen, da unnötig). Das Gleiche, aber mit return:
() -> {
    return "";
}
Beispiel 3. Hallo Welt mit Lambdas
() -> System.out.println("Hello world!")
Empfängt nichts, gibt nichts zurück (wir können nicht returnvor dem Aufruf stehen System.out.println(), da der Rückgabetyp in der Methode println() — void)einfach eine Inschrift auf dem Bildschirm anzeigt. Ideal für die Implementierung einer Schnittstelle Runnable. Das gleiche Beispiel ist vollständiger:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
Oder so:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
Oder wir können den Lambda-Ausdruck sogar als Objekt vom Typ speichern Runnableund ihn dann an den Konstruktor übergeben thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Schauen wir uns den Moment des Speicherns eines Lambda-Ausdrucks in einer Variablen genauer an. Die Schnittstelle Runnablesagt uns, dass ihre Objekte eine Methode haben müssen public void run(). Laut Schnittstelle akzeptiert die run-Methode nichts als Parameter. Und es gibt nichts zurück (void). Daher wird beim Schreiben auf diese Weise ein Objekt mit einer Methode erstellt, die nichts akzeptiert oder zurückgibt. Das stimmt ziemlich gut mit der Methode run()in der Schnittstelle überein Runnable. Deshalb konnten wir diesen Lambda-Ausdruck in eine Variable wie einfügen Runnable. Beispiel 4
() -> 42
Auch hier akzeptiert es nichts, sondern gibt die Zahl 42 zurück. Dieser Lambda-Ausdruck kann in einer Variablen vom Typ platziert werden Callable, da diese Schnittstelle nur eine Methode definiert, die in etwa so aussieht:
V call(),
Dabei Vist der Typ des Rückgabewerts (in unserem Fall int). Dementsprechend können wir einen solchen Lambda-Ausdruck wie folgt speichern:
Callable<Integer> c = () -> 42;
Beispiel 5. Lambda in mehreren Zeilen
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Auch dies ist ein Lambda-Ausdruck ohne Parameter und seinen Rückgabetyp void(da es keinen gibt return). Beispiel 6
x -> x
Hier nehmen wir etwas in eine Variable хund geben es zurück. Bitte beachten Sie, dass, wenn nur ein Parameter akzeptiert wird, die Klammern um diesen nicht geschrieben werden müssen. Das Gleiche, aber mit Klammern:
(x) -> x
Und hier ist die Option mit einer expliziten return:
x -> {
    return x;
}
Oder so, mit Klammern und return:
(x) -> {
    return x;
}
Oder mit expliziter Angabe des Typs (und entsprechend mit Klammern):
(int x) -> x
Beispiel 7
x -> ++x
Wir nehmen es an хund geben es zurück, aber für 1mehr. Sie können es auch so umschreiben:
x -> x + 1
In beiden Fällen geben wir keine Klammern um den Parameter, den Methodenkörper und das Wort an return, da dies nicht erforderlich ist. Optionen mit Klammern und Return werden in Beispiel 6 beschrieben. Beispiel 8
(x, y) -> x % y
Wir akzeptieren einige хund уgeben den Rest der Division xdurch zurück y. Hier sind bereits Klammern um Parameter erforderlich. Sie sind nur dann optional, wenn nur ein Parameter vorhanden ist. So mit expliziter Angabe der Typen:
(double x, int y) -> x % y
Beispiel 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Wir akzeptieren ein Cat-Objekt, eine Zeichenfolge mit einem Namen und einem ganzzahligen Alter. In der Methode selbst setzen wir den übergebenen Namen und das Alter auf die Katze. catDa unsere Variable ein Referenztyp ist, ändert sich das Cat-Objekt außerhalb des Lambda-Ausdrucks (es erhält den darin übergebenen Namen und das Alter). Eine etwas kompliziertere Version, die ein ähnliches Lambda verwendet:
public class Main {
    public static void main(String[] args) {
        // eine Katze erstellen und auf dem Bildschirm ausgeben, um sicherzustellen, dass sie „leer“ ist
        Cat myCat = new Cat();
        System.out.println(myCat);

        // Lambda erstellen
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // Rufen Sie die Methode auf, an die wir die Katze und das Lambda übergeben
        changeEntity(myCat, s);
        // auf dem Bildschirm anzeigen und sehen, dass sich der Zustand der Katze geändert hat (hat einen Namen und ein Alter)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, „Mürzik“, 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Ergebnis: Cat{name='null', age=0} Cat{name='Murzik', age=3} Wie Sie sehen können, hatte das Cat-Objekt zunächst einen Status, aber nach Verwendung des Lambda-Ausdrucks änderte sich der Status . Lambda-Ausdrücke funktionieren gut mit Generika. DogUnd wenn wir beispielsweise eine Klasse erstellen müssen , die auch implementiert WithNameAndAge, können wir in der Methode main()dieselben Operationen mit Dog ausführen, ohne den Lambda-Ausdruck selbst überhaupt zu ändern. Aufgabe 3 . Schreiben Sie eine funktionale Schnittstelle mit einer Methode, die eine Zahl annimmt und einen booleschen Wert zurückgibt. Schreiben Sie eine Implementierung einer solchen Schnittstelle in Form eines Lambda-Ausdrucks, der zurückgibt, truewenn die übergebene Zahl ohne Rest durch 13 teilbar ist . Aufgabe 4 . Schreiben Sie eine funktionale Schnittstelle mit einer Methode, die zwei Zeichenfolgen akzeptiert und dieselbe Zeichenfolge zurückgibt. Schreiben Sie eine Implementierung einer solchen Schnittstelle in Form eines Lambda, das die längste Zeichenfolge zurückgibt. Aufgabe 5 . Schreiben Sie eine funktionale Schnittstelle mit einer Methode, die drei Bruchzahlen akzeptiert: a, bund cund dieselbe Bruchzahl zurückgibt. Schreiben Sie eine Implementierung einer solchen Schnittstelle in Form eines Lambda-Ausdrucks, der eine Diskriminante zurückgibt. Wer hat es vergessen, D = b^2 - 4ac . Aufgabe 6 . Schreiben Sie mithilfe der Funktionsschnittstelle aus Aufgabe 5 einen Lambda-Ausdruck, der das Ergebnis der Operation zurückgibt a * b^c. Beliebt bei Lambda-Ausdrücken in Java. Mit Beispielen und Aufgaben. Teil 2.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION