1. Schnittstellen

Um zu verstehen, was Lambda-Funktionen sind, müssen Sie zunächst verstehen, was Schnittstellen sind. Erinnern wir uns also an die wichtigsten Punkte.

Eine Schnittstelle ist eine Variation des Konzepts einer Klasse. Nehmen wir an, eine stark verkürzte Klasse. Im Gegensatz zu einer Klasse kann eine Schnittstelle keine eigenen Variablen haben (außer statischen). Sie können auch keine Objekte erstellen, deren Typ eine Schnittstelle ist:

  • Sie können keine Variablen der Klasse deklarieren
  • Sie können keine Objekte erstellen

Beispiel:

interface Runnable
{
   void run();
}
Beispiel einer Standardschnittstelle

Verwendung einer Schnittstelle

Warum wird also eine Schnittstelle benötigt? Schnittstellen werden nur zusammen mit Vererbung verwendet. Die gleiche Schnittstelle kann von verschiedenen Klassen geerbt werden, oder wie es auch heißt – Klassen implementieren die Schnittstelle .

Wenn eine Klasse eine Schnittstelle implementiert, muss sie die von der Schnittstelle deklarierten, aber nicht von ihr implementierten Methoden implementieren. Beispiel:

interface Runnable
{
   void run();
}

class Timer implements Runnable
{
   void run()
   {
      System.out.println(LocalTime.now());
   }
}

class Calendar implements Runnable
{
   void run()
   {
      var date = LocalDate.now();
      System.out.println("Today: " + date.getDayOfWeek());
   }
}

Die TimerKlasse implementiert die RunnableSchnittstelle, daher muss sie alle in der Schnittstelle enthaltenen Methoden in sich selbst deklarieren Runnableund implementieren, dh Code in einen Methodenkörper schreiben. Dasselbe gilt auch für die CalendarKlasse.

Aber jetzt Runnablekönnen Variablen Verweise auf Objekte speichern, die die RunnableSchnittstelle implementieren.

Beispiel:

Code Notiz
Timer timer = new Timer();
timer.run();

Runnable r1 = new Timer();
r1.run();

Runnable r2 = new Calendar();
r2.run();

Die run()Methode in der TimerKlasse wird aufgerufen.


Die run()Methode in der TimerKlasse wird aufgerufen.


Die run()Methode in der CalendarKlasse wird aufgerufen

Sie können einer Variablen eines beliebigen Typs jederzeit einen Objektverweis zuweisen, solange dieser Typ eine der Vorfahrenklassen des Objekts ist. Für die Timerund- CalendarKlassen gibt es zwei solcher Typen: Objectund Runnable.

Wenn Sie einer Variablen eine Objektreferenz zuweisen Object, können Sie nur die in der ObjectKlasse deklarierten Methoden aufrufen. Und wenn Sie einer Variablen eine Objektreferenz zuweisen Runnable, können Sie die Methoden des RunnableTyps aufrufen.

Beispiel 2:

ArrayList<Runnable> list = new ArrayList<Runnable>();
list.add (new Timer());
list.add (new Calendar());

for (Runnable element: list)
    element.run();

Dieser Code funktioniert, da die Timerund- CalendarObjekte über Ausführungsmethoden verfügen, die einwandfrei funktionieren. Ein Anruf ist also kein Problem. Wenn wir beiden Klassen nur eine run()-Methode hinzugefügt hätten, könnten wir sie nicht auf so einfache Weise aufrufen.

Grundsätzlich Runnablewird die Schnittstelle nur als Ort für die Ausführungsmethode verwendet.



2. Sortieren

Kommen wir zu etwas Praktischerem. Schauen wir uns zum Beispiel das Sortieren von Zeichenfolgen an.

Um eine Sammlung von Zeichenfolgen alphabetisch zu sortieren, verfügt Java über eine großartige Methode namensCollections.sort(collection);

Diese statische Methode sortiert die übergebene Sammlung. Und beim Sortieren führt es paarweise Vergleiche seiner Elemente durch, um zu verstehen, ob Elemente ausgetauscht werden sollten.

Beim Sortieren werden diese Vergleiche mit der compareToMethode () durchgeführt, die alle Standardklassen haben: Integer, String, ...

Die Methode „compareTo()“ der Klasse „Integer“ vergleicht die Werte zweier Zahlen, während die Methode „compareTo()“ der Klasse „String“ die alphabetische Reihenfolge von Zeichenfolgen untersucht.

Eine Sammlung von Zahlen wird also in aufsteigender Reihenfolge sortiert, während eine Sammlung von Zeichenfolgen alphabetisch sortiert wird.

Alternative Sortieralgorithmen

Was aber, wenn wir Strings nicht alphabetisch, sondern nach ihrer Länge sortieren wollen? Und was ist, wenn wir Zahlen in absteigender Reihenfolge sortieren möchten? Was machen Sie in diesem Fall?

Um mit solchen Situationen umzugehen, Collectionsverfügt die Klasse über eine weitere sort()Methode mit zwei Parametern:

Collections.sort(collection, comparator);

Dabei ist der Komparator ein spezielles Objekt, das weiß, wie Objekte in einer Sammlung während eines Sortiervorgangs verglichen werden . Der Begriff Komparator kommt vom englischen Wort comparator , das sich wiederum von „ compare “ ableitet , was „vergleichen“ bedeutet.

Was ist also dieses besondere Objekt?

ComparatorSchnittstelle

Nun, es ist alles ganz einfach. Der Typ des sort()zweiten Parameters der Methode istComparator<T>

Dabei ist T ein Typparameter, der den Typ der Elemente in der Sammlung angibt , und Comparatoreine Schnittstelle mit einer einzelnen Methodeint compare(T obj1, T obj2);

Mit anderen Worten, ein Komparatorobjekt ist ein beliebiges Objekt einer beliebigen Klasse, die die Comparator-Schnittstelle implementiert. Die Comparator-Schnittstelle sieht sehr einfach aus:

public interface Comparator<Type>
{
   public int compare(Type obj1, Type obj2);
}
Code für die Comparator-Schnittstelle

Die compare()Methode vergleicht die beiden Argumente, die ihr übergeben werden.

Wenn die Methode eine negative Zahl zurückgibt, bedeutet das obj1 < obj2. Wenn die Methode eine positive Zahl zurückgibt, bedeutet dies obj1 > obj2. Wenn die Methode 0 zurückgibt, bedeutet das obj1 == obj2.

Hier ist ein Beispiel für ein Komparatorobjekt, das Zeichenfolgen anhand ihrer Länge vergleicht:

public class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Code der StringLengthComparatorKlasse

Um Stringlängen zu vergleichen, subtrahieren Sie einfach eine Länge von der anderen.

Der vollständige Code für ein Programm, das Strings nach Länge sortiert, würde so aussehen:

public class Solution
{
   public static void main(String[] args)
   {
      ArrayList<String> list = new ArrayList<String>();
      Collections.addAll(list, "Hello", "how's", "life?");
      Collections.sort(list, new StringLengthComparator());
   }
}

class StringLengthComparator implements Comparator<String>
{
   public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
}
Strings nach Länge sortieren


3. Syntaktischer Zucker

Was denken Sie, kann dieser Code kompakter geschrieben werden? Im Grunde gibt es nur eine Zeile, die nützliche Informationen enthält – obj1.length() - obj2.length();.

Aber Code kann außerhalb einer Methode nicht existieren, also mussten wir eine compare()Methode hinzufügen und um die Methode zu speichern, mussten wir eine neue Klasse hinzufügen – StringLengthComparator. Und wir müssen auch die Typen der Variablen angeben ... Alles scheint korrekt zu sein.

Es gibt jedoch Möglichkeiten, diesen Code kürzer zu machen. Wir haben etwas syntaktischen Zucker für Sie. Zwei Kugeln!

Anonyme innere Klasse

Sie können den Komparatorcode direkt in die main()Methode schreiben und der Compiler erledigt den Rest. Beispiel:

public class Solution
{
    public static void main(String[] args)
    {
        ArrayList<String> list = new ArrayList<String>();
        Collections.addAll(list, "Hello", "how's", "life?");

        Comparator<String> comparator = new Comparator<String>()
        {
            public int compare (String obj1, String obj2)
            {
                return obj1.length() – obj2.length();
            }
        };

        Collections.sort(list, comparator);
    }
}
Sortieren Sie Zeichenfolgen nach Länge

Sie können ein Objekt erstellen, das die ComparatorSchnittstelle implementiert, ohne explizit eine Klasse zu erstellen! Der Compiler erstellt es automatisch und gibt ihm einen temporären Namen. Lass uns vergleichen:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Anonyme innere Klasse
Comparator<String> comparator = new StringLengthComparator();

class StringLengthComparator implements Comparator<String>
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
}
StringLengthComparatorKlasse

In den beiden unterschiedlichen Fällen wird dieselbe Farbe verwendet, um identische Codeblöcke anzuzeigen. In der Praxis sind die Unterschiede recht gering.

Wenn der Compiler auf den ersten Codeblock trifft, generiert er einfach einen entsprechenden zweiten Codeblock und gibt der Klasse einen zufälligen Namen.


4. Lambda-Ausdrücke in Java

Nehmen wir an, Sie entscheiden sich für die Verwendung einer anonymen inneren Klasse in Ihrem Code. In diesem Fall erhalten Sie einen Codeblock wie diesen:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
    {
        return obj1.length() – obj2.length();
    }
};
Anonyme innere Klasse

Hier kombinieren wir die Deklaration einer Variablen mit der Erstellung einer anonymen Klasse. Aber es gibt eine Möglichkeit, diesen Code kürzer zu machen. Zum Beispiel so:

Comparator<String> comparator = (String obj1, String obj2) ->
{
    return obj1.length() – obj2.length();
};

Das Semikolon wird benötigt, da es sich hier nicht nur um eine implizite Klassendeklaration, sondern auch um die Erstellung einer Variablen handelt.

Eine solche Notation wird als Lambda-Ausdruck bezeichnet.

Wenn der Compiler in Ihrem Code auf eine solche Notation stößt, generiert er einfach die ausführliche Version des Codes (mit einer anonymen inneren Klasse).

Beachten Sie, dass wir beim Schreiben des Lambda-Ausdrucks nicht nur den Namen der Klasse, sondern auch den Namen der Methode weggelassen haben.Comparator<String>int compare()

Bei der Kompilierung wird es keine Probleme geben, die Methode zu bestimmen , da ein Lambda-Ausdruck nur für Schnittstellen geschrieben werden kann, die über eine einzelne Methode verfügen . Übrigens gibt es eine Möglichkeit, diese Regel zu umgehen, aber Sie werden mehr darüber erfahren, wenn Sie beginnen, sich eingehender mit OOP zu befassen (wir sprechen hier von Standardmethoden).

Schauen wir uns noch einmal die ausführliche Version des Codes an, aber wir blenden den Teil aus, der beim Schreiben eines Lambda-Ausdrucks weggelassen werden kann:

Comparator<String> comparator = new Comparator<String>()
{
    public int compare (String obj1, String obj2)
   {
      return obj1.length() – obj2.length();
   }
};
Anonyme innere Klasse

Es scheint, dass nichts Wichtiges ausgelassen wurde. Wenn die ComparatorSchnittstelle tatsächlich nur über eine compare()Methode verfügt, kann der Compiler den ausgegrauten Code vollständig aus dem verbleibenden Code wiederherstellen.

Sortierung

Den Sortiercode können wir jetzt übrigens so schreiben:

Comparator<String> comparator = (String obj1, String obj2) ->
{
   return obj1.length() – obj2.length();
};
Collections.sort(list, comparator);

Oder sogar so:

Collections.sort(list, (String obj1, String obj2) ->
   {
      return obj1.length() – obj2.length();
   }
);

Wir haben die Variable einfach sofort comparatordurch den Wert ersetzt, der der comparatorVariablen zugewiesen wurde.

Typinferenz

Aber das ist nicht alles. Der Code in diesen Beispielen kann noch kompakter geschrieben werden. Erstens kann der Compiler selbst feststellen, dass die Variablen obj1und obj2sind Strings. Und zweitens können die geschweiften Klammern und die Return-Anweisung auch weggelassen werden, wenn Sie nur einen einzigen Befehl im Methodencode haben.

Die gekürzte Version würde so aussehen:

Comparator<String> comparator = (obj1, obj2) ->
   obj1.length() – obj2.length();

Collections.sort(list, comparator);

Und wenn wir statt der comparatorVariablen sofort ihren Wert verwenden, erhalten wir die folgende Version:

Collections.sort(list, (obj1, obj2) ->  obj1.length() — obj2.length() );

Na, was haltet Ihr davon? Nur eine Codezeile ohne überflüssige Informationen – nur Variablen und Code. Es gibt keine Möglichkeit, es kürzer zu machen! Oder gibt es das?



5. Wie es funktioniert

Tatsächlich kann der Code sogar noch kompakter geschrieben werden. Aber dazu später mehr.

Sie können einen Lambda-Ausdruck schreiben, bei dem Sie einen Schnittstellentyp mit einer einzelnen Methode verwenden würden.

Im Code können Sie beispielsweise einen Lambda-Ausdruck schreiben, da die Signatur der Methode wie folgt lautet:Collections.sort(list, (obj1, obj2) -> obj1.length() - obj2.length());sort()

sort(Collection<T> colls, Comparator<T> comp)

Als wir die ArrayList<String>Sammlung als erstes Argument an die Sortiermethode übergeben haben, konnte der Compiler feststellen, dass der Typ des zweiten Arguments ist . Daraus wurde der Schluss gezogen, dass diese Schnittstelle über eine einzige Methode verfügt. Alles andere ist eine Formsache.Comparator<String>int compare(String obj1, String obj2)