JavaRush /Java-Blog /Random-DE /Lambdas und Methodenreferenzen in ArrayList.forEach – wie...

Lambdas und Methodenreferenzen in ArrayList.forEach – wie es funktioniert

Veröffentlicht in der Gruppe Random-DE
Die Einführung in Lambda-Ausdrücke in der Java Syntax Zero-Quest beginnt mit einem ganz konkreten Beispiel:
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hallo", "Wie", "дела?");

list.forEach( (s) -> System.out.println(s) );
Die Autoren der Vorlesung analysieren Lambdas und Methodenreferenzen mithilfe der Standardfunktion forEach der ArrayList-Klasse. Persönlich fiel es mir schwer, die Bedeutung des Geschehens zu verstehen, da die Implementierung dieser Funktion sowie die damit verbundene Schnittstelle „unter der Haube“ bleiben. Woher die Argumente kommen , wo die println()- Funktion übergeben wird , sind Fragen, die wir selbst beantworten müssen. Glücklicherweise können wir mit IntelliJ IDEA problemlos einen Blick in die Interna der ArrayList-Klasse werfen und dieses Problem von Anfang an lösen. Wenn du auch nichts verstehst und es herausfinden willst, werde ich versuchen, dir dabei zumindest ein wenig zu helfen. Lambda-Ausdruck und ArrayList.forEach – wie es funktioniert Aus der Vorlesung wissen wir bereits, dass ein Lambda-Ausdruck eine Implementierung einer funktionalen Schnittstelle ist . Das heißt, wir deklarieren eine Schnittstelle mit einer einzelnen Funktion und verwenden ein Lambda, um zu beschreiben, was diese Funktion tut. Dazu benötigen Sie: 1. Erstellen Sie eine funktionale Schnittstelle; 2. Erstellen Sie eine Variable, deren Typ der Funktionsschnittstelle entspricht. 3. Weisen Sie dieser Variablen einen Lambda-Ausdruck zu, der die Implementierung der Funktion beschreibt. 4. Rufen Sie eine Funktion auf, indem Sie auf eine Variable zugreifen (vielleicht bin ich in der Terminologie grob, aber das ist der klarste Weg). Ich werde ein einfaches Beispiel von Google geben und es mit detaillierten Kommentaren versehen (danke an die Autoren der Website metanit.com):
interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс,
    // который можно реализовать с помощью лямбды
}

public class LambdaApp {

    public static void main(String[] args) {

        // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
        Operationable operation;
        // Прописываем реализацию функции calculate с помощью лямбды, на вход подаём x и y, на выходе возвращаем их сумму
        operation = (x,y)->x+y;

        // Теперь мы можем обратиться к функции calculate через переменную operation
        int result = operation.calculate(10, 20);
        System.out.println(result); //30
    }
}
Kehren wir nun zum Beispiel aus der Vorlesung zurück. Der Listensammlung werden mehrere Elemente vom Typ String hinzugefügt . Die Elemente werden dann mit der Standardfunktion forEach abgerufen , die für das Listenobjekt aufgerufen wird . Ein Lambda-Ausdruck mit einigen seltsamen Parametern wird als Argument an die Funktion übergeben .
ArrayList<string> list = new ArrayList<>();
Collections.addAll(list, "Hallo", "Wie", "дела?");

list.forEach( (s) -> System.out.println(s) );
Wenn Sie nicht sofort verstanden haben, was hier passiert ist, dann sind Sie nicht allein. Glücklicherweise verfügt IntelliJ IDEA über eine tolle Tastenkombination: Strg+Linke_Maustaste . Wenn wir mit der Maus über forEach fahren und auf diese Kombination klicken, öffnet sich der Quellcode der Standardklasse ArrayList, in dem wir die Implementierung der forEach -Methode sehen :
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i));
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
Wir sehen, dass das Eingabeargument eine Aktion vom Typ Consumer ist . Bewegen wir den Cursor über das Wort Consumer und drücken erneut die Zauberkombination Strg+LMB . Eine Beschreibung der Consumer- Schnittstelle wird geöffnet . Wenn wir die Standardimplementierung daraus entfernen (dies ist für uns jetzt nicht wichtig), sehen wir den folgenden Code:
public interface Consumer<t> {
   void accept(T t);
}
Also. Wir haben eine Consumer- Schnittstelle mit einer einzelnen Accept- Funktion , die ein Argument eines beliebigen Typs akzeptiert. Da es nur eine Funktion gibt, ist die Schnittstelle funktionsfähig und ihre Implementierung kann über einen Lambda-Ausdruck geschrieben werden. Wir haben bereits gesehen, dass ArrayList über eine forEach -Funktion verfügt , die eine Implementierung der Consumer- Schnittstelle als Aktionsargument verwendet . Darüber hinaus finden wir in der forEach-Funktion den folgenden Code:
for (int i = 0; modCount == expectedModCount && i < size; i++)
    action.accept(elementAt(es, i));
Die for-Schleife durchläuft im Wesentlichen alle Elemente einer ArrayList. Innerhalb der Schleife sehen wir einen Aufruf der Accept- Funktion des Aktionsobjekts – erinnern Sie sich , wie wir operation.calculate aufgerufen haben? Das aktuelle Element der Sammlung wird an die Accept- Funktion übergeben . Jetzt können wir endlich zum ursprünglichen Lambda-Ausdruck zurückkehren und verstehen, was er bewirkt. Sammeln wir den gesamten Code auf einem Stapel:
public interface Consumer<t> {
   void accept(T t); // Функция, которую мы реализуем лямбда-выражением
}

public void forEach(Consumer<? super E> action) // В action хранится ein Objekt Consumer, в котором функция accept реализована нашей лямбдой {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    final Object[] es = elementData;
    final int size = this.size;
    for (int i = 0; modCount == expectedModCount && i < size; i++)
        action.accept(elementAt(es, i)); // Вызываем нашу реализацию функции accept интерфейса Consumer для каждого Element коллекции
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

//...

list.forEach( (s) -> System.out.println(s) );
Unser Lambda-Ausdruck ist eine Implementierung der in der Consumer- Schnittstelle beschriebenen Accept- Funktion . Mithilfe eines Lambda haben wir angegeben, dass die Funktion „accept“ ein Argument „ s“ entgegennimmt und es auf dem Bildschirm anzeigt. Der Lambda-Ausdruck wurde als Aktionsargument an die Funktion forEach übergeben , die die Implementierung der Consumer- Schnittstelle speichert . Jetzt kann die forEach-Funktion unsere Implementierung der Consumer-Schnittstelle mit einer Zeile wie dieser aufrufen:
action.accept(elementAt(es, i));
Somit ist das Eingabeargument s im Lambda-Ausdruck ein weiteres Element der ArrayList-Sammlung , das an unsere Implementierung der Consumer-Schnittstelle übergeben wird . Das ist alles: Wir haben die Logik des Lambda-Ausdrucks in ArrayList.forEach analysiert. Verweis auf eine Methode in ArrayList.forEach – wie funktioniert das? Der nächste Schritt in der Vorlesung besteht darin, sich Methodenreferenzen anzusehen. Es stimmt, sie verstehen es auf eine sehr seltsame Weise – nachdem ich die Vorlesung gelesen hatte, hatte ich keine Chance zu verstehen, was dieser Code macht:
list.forEach( System.out::println );
Zunächst noch einmal ein wenig Theorie. Eine Methodenreferenz ist grob gesagt eine Implementierung einer funktionalen Schnittstelle, die durch eine andere Funktion beschrieben wird . Auch hier beginne ich mit einem einfachen Beispiel:
public interface Operationable {
    int calculate(int x, int y);
    // Единственная функция в интерфейсе — значит, это функциональный интерфейс
}

public static class Calculator {
    // Создадим статический класс Calculator и пропишем в нём метод methodReference.
    // Именно он будет реализовывать функцию calculate из интерфейса Operationable.
    public static int methodReference(int x, int y) {
        return x+y;
    }
}

public static void main(String[] args) {
    // Создаём переменную operation типа Operationable (так называется наш функциональный интерфейс)
    Operationable operation;
    // Теперь реализацией интерфейса будет не лямбда-выражение, а метод methodReference из нашего класса Calculator
    operation = Calculator::methodReference;

    // Теперь мы можем обратиться к функции интерфейса через переменную operation
    int result = operation.calculate(10, 20);
    System.out.println(result); //30
}
Kehren wir zum Beispiel aus der Vorlesung zurück:
list.forEach( System.out::println );
Ich möchte Sie daran erinnern, dass System.out ein Objekt vom Typ PrintStream ist, das über eine println- Funktion verfügt . Bewegen wir den Mauszeiger über println und klicken Sie auf Strg+LMB :
public void println(String x) {
    if (getClass() == PrintStream.class) {
        writeln(String.valueOf(x));
    } else {
        synchronized (this) {
            print(x);
            newLine();
        }
    }
}
Beachten wir zwei Hauptmerkmale: 1. Die println- Funktion gibt nichts zurück (void). 2. Die println- Funktion erhält ein Argument als Eingabe. Erinnert Sie an nichts?
public interface Consumer<t> {
   void accept(T t);
}
Das ist richtig – die Signatur der Funktion „akzeptieren“ ist ein allgemeinerer Fall der Signatur der Methode „println “ ! Dies bedeutet, dass letzteres erfolgreich als Referenz auf eine Methode verwendet werden kann – das heißt, println wird zu einer spezifischen Implementierung der Accept-Funktion :
list.forEach( System.out::println );
Wir haben die println- Funktion des System.out- Objekts als Argument an die forEach -Funktion übergeben . Das Prinzip ist das gleiche wie beim Lambda: Jetzt kann forEach über einen action.accept(elementAt(es, i))- Aufruf ein Collection-Element an die println-Funktion übergeben . Tatsächlich kann dies jetzt als System.out.println(elementAt(es, i)) gelesen werden .
public void forEach(Consumer<? super E> action) // В action хранится ein Objekt Consumer, в котором функция accept реализована методом println {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        final Object[] es = elementData;
        final int size = this.size;
        for (int i = 0; modCount == expectedModCount && i < size; i++)
            action.accept(elementAt(es, i)); // Функция accept теперь реализована методом System.out.println!
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
Ich hoffe, dass ich die Situation für diejenigen, die mit Lambdas und Methodenreferenzen noch nicht vertraut sind, zumindest ein wenig geklärt habe. Abschließend empfehle ich das berühmte Buch „Java: A Beginner's Guide“ von Robert Schildt – darin sind meiner Meinung nach Lambdas und Funktionsreferenzen recht sinnvoll beschrieben.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION