Wie interagieren Lambda-Anrufe mit Schnittstellen?

8

Das unten gezeigte Code-Snippet funktioniert. Ich bin mir jedoch nicht sicher, warum es funktioniert. Ich folge nicht ganz der Logik, wie die Lambda-Funktion Informationen an die Schnittstelle weitergibt.

Wo wird die Kontrolle weitergegeben? Wie macht der Compiler Sinn für jeden nin der Schleife und jeden messageerstellt?

Dieser Code kompiliert und liefert die erwarteten Ergebnisse. Ich bin mir nur nicht sicher wie.

import java.util.ArrayList;
import java.util.List;

public class TesterClass {

    public static void main(String[] args) {

        List<String> names = new ArrayList<>();

        names.add("Akira");
        names.add("Jacky");
        names.add("Sarah");
        names.add("Wolf");

        names.forEach((n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        });
    }

    interface SayHello {
        void speak(String message);
    }
}
Brodiman
quelle

Antworten:

8

Dies SayHelloist eine Single Abstract Method-Schnittstelle mit einer Methode, die eine Zeichenfolge verwendet und void zurückgibt. Dies ist analog zu einem Verbraucher. Sie stellen lediglich eine Implementierung dieser Methode in Form eines Verbrauchers bereit, die der folgenden anonymen Implementierung der inneren Klasse ähnelt.

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

names.forEach(n -> sh.speak(n));

Glücklicherweise verfügt Ihre Schnittstelle über eine einzige Methode, sodass das Typsystem (formeller der Typauflösungsalgorithmus) auf seinen Typ als schließen kann SayHello. Aber wenn es zwei oder mehr Methoden geben würde, wäre dies nicht möglich.

Ein viel besserer Ansatz besteht jedoch darin, den Verbraucher vor der for-Schleife zu deklarieren und wie unten gezeigt zu verwenden. Das Deklarieren der Implementierung für jede Iteration erzeugt mehr Objekte als nötig und erscheint mir kontraintuitiv. Hier ist die erweiterte Version, die Methodenreferenzen anstelle von Lambda verwendet. Die hier verwendete beschränkte Methodenreferenz ruft die relevante Methode für die hellooben deklarierte Instanz auf.

SayHello hello = message -> System.out.println("Hello " + message);
names.forEach(hello::speak);

Aktualisieren

Angesichts der Tatsache, dass für staatenlose Lambdas, die nur einmal eine Instanz aus ihrem lexikalischen Geltungsbereich erfassen, niemals eine Instanz erstellt wird, erstellen beide Ansätze lediglich eine Instanz von SayHello, und es gibt keinen Gewinn, der dem vorgeschlagenen Ansatz folgt. Dies scheint jedoch ein Implementierungsdetail zu sein, und ich wusste es bis jetzt nicht. Ein viel besserer Ansatz ist es also, einen Verbraucher an Ihren forEach weiterzugeben, wie im Kommentar unten vorgeschlagen. Beachten Sie auch, dass alle diese Ansätze nur eine Instanz Ihrer SayHelloBenutzeroberfläche erstellen, während die letzte prägnanter ist. So sieht es aus.

names.forEach(message -> System.out.println("Hello " + message));

Diese Antwort gibt Ihnen mehr Einblick. Hier ist der relevante Abschnitt aus dem JLS §15.27.4 : Laufzeitauswertung von Lambda-Ausdrücken

Diese Regeln sollen Implementierungen der Java-Programmiersprache Flexibilität bieten, indem:

  • Ein neues Objekt muss nicht bei jeder Bewertung zugewiesen werden.

Tatsächlich dachte ich anfangs, dass jede Bewertung eine neue Instanz erstellt, was falsch ist. @ Holger danke für den Hinweis, guter Fang.

Ravindra Ranwala
quelle
Ok, ich glaube ich verstehe. Ich habe einfach eine Version der Consumer-Oberfläche erstellt. Da ich es jedoch in die Schleife eingefügt habe, habe ich in jeder Schleife ein separates Objekt erstellt, das Speicherplatz verbraucht. Während Ihre Implementierung dieses eine Objekt erstellt und "Nachricht" in jeder Schleife austauscht. Vielen Dank für die Erklärung!
Brodiman
1
Ja, es ist immer gut, wenn Sie eine vorhandene Funktionsschnittstelle verwenden können, anstatt Ihre eigene einzuführen. In diesem Fall können Sie Consumer<String>anstelle der sayHelloSchnittstelle verwenden.
Ravindra Ranwala
1
Es spielt keine Rolle, wo Sie die Zeile platzieren SayHello hello = message -> System.out.println("Hello " + message);, es wird nur eine SayHelloInstanz geben. Angesichts der Tatsache, dass auch dieses eine Objekt hier ohnehin veraltet ist, da dasselbe über erreichbar ist names.forEach(n -> System.out.println("Hello " + n)), gibt es keine Verbesserung in Ihrer „erweiterten Version“.
Holger
@ Holger Ich habe die Antwort mit Ihrem Vorschlag aktualisiert. Schätze es wirklich. Alle Implementierungen erstellen also eine einzige Instanz von SayHello, während die von Ihnen vorgeschlagene kompakter ist? Ist das nicht so?
Ravindra Ranwala
1

Die Signatur von foreach lautet wie folgt:

void forEach(Consumer<? super T> action)

Es nimmt ein Objekt des Verbrauchers auf. Die Consumer-Schnittstelle ist eine funktionale Schnittstelle (eine Schnittstelle mit einer einzelnen abstrakten Methode). Es akzeptiert eine Eingabe und gibt kein Ergebnis zurück.

Hier ist die Definition:

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

Daher kann jede Implementierung, die in Ihrem Fall, auf zwei Arten geschrieben werden:

Consumer<String> printConsumer = new Consumer<String>() {
    public void accept(String name) {
        SayHello hello = (message) -> System.out.println("Hello " + message);
        hello.speak(n);
    };
};

ODER

(n) -> {
            SayHello hello = (message) -> System.out.println("Hello " + message);
            hello.speak(n);
        }

Ebenso kann der Code für die SayHello-Klasse wie folgt geschrieben werden

SayHello sh = new SayHello() {
    @Override
    public void speak(String message) {
        System.out.println("Hello " + message);
    }
};

ODER

SayHello hello = message -> System.out.println("Hello " + message);

names.foreachRuft also intern zuerst die Methode consumer.accept auf und führt ihre Lambda / anonyme Implementierung aus, die die SayHello-Lambda-Implementierung erstellt und aufruft, und so weiter. Mit dem Lambda-Ausdruck ist der Code sehr kompakt und klar. Sie müssen nur verstehen, wie es funktioniert, dh in Ihrem Fall wurde die Consumer-Oberfläche verwendet.

Also endgültiger Ablauf: foreach -> consumer.accept -> sayhello.speak

Nishant Lakhara
quelle