Auswahl der Methodensignatur für den Lambda-Ausdruck mit mehreren übereinstimmenden Zieltypen

11

Ich beantwortete eine Frage und stieß auf ein Szenario, das ich nicht erklären kann. Betrachten Sie diesen Code:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

Ich verstehe nicht, warum das explizite Eingeben des Parameters des Lambda (A a) -> aList.add(a)den Code kompilieren lässt. Warum ist es außerdem Iterableeher mit der Überlastung als mit der Überlastung verbunden CustomIterable?
Gibt es eine Erklärung dafür oder einen Link zu dem relevanten Abschnitt der Spezifikation?

Hinweis: iterable.forEach((A a) -> aList.add(a));Kompiliert nur, wenn es CustomIterable<T>erweitert wird Iterable<T>(eine flache Überladung der Methoden CustomIterableführt zu einem mehrdeutigen Fehler).


Bekomme das auf beiden:

  • openjdk Version "13.0.2" 2020-01-14
    Eclipse Compiler
  • openjdk version "1.8.0_232"
    Eclipse-Compiler

Bearbeiten : Der obige Code kann beim Erstellen mit maven nicht kompiliert werden, während Eclipse die letzte Codezeile erfolgreich kompiliert.

ernest_k
quelle
3
Keine der drei Kompilierungen unter Java 8. Jetzt bin ich mir nicht sicher, ob dies ein Fehler ist, der in einer neueren Version behoben wurde, oder ein Fehler / eine Funktion, die eingeführt wurde ... Sie sollten wahrscheinlich die Java-Version angeben
Sweeper
@Sweeper Ich habe dies zunächst mit jdk-13 bekommen. Nachfolgende Tests in Java 8 (jdk8u232) zeigen die gleichen Fehler. Ich bin mir nicht sicher, warum der letzte auch auf Ihrem Computer nicht kompiliert wird.
ernest_k
Kann auch nicht auf zwei Online-Compilern reproduziert werden ( 1 , 2 ). Ich verwende 1.8.0_221 auf meinem Computer. Das wird immer seltsamer ...
Kehrmaschine
1
@ernest_k Eclipse hat eine eigene Compiler-Implementierung. Das könnte eine wichtige Information für die Frage sein. Auch die Tatsache, dass ein sauberer Maven Build Fehler in der letzten Zeile macht, sollte meiner Meinung nach in der Frage hervorgehoben werden. Andererseits könnte in Bezug auf die verknüpfte Frage die Annahme geklärt werden, dass das OP auch Eclipse verwendet, da der Code nicht reproduzierbar ist.
Naman
2
Obwohl ich diesen intellektuellen Wert der Frage verstehe, kann ich nur davon abraten, jemals überladene Methoden zu erstellen, die sich nur in funktionalen Schnittstellen unterscheiden, und zu erwarten, dass sie sicher als Lambda-Passing bezeichnet werden können. Ich glaube nicht, dass die Kombination aus Inferenz vom Lambda-Typ und Überladung etwas ist, das der durchschnittliche Programmierer jemals annähernd verstehen wird. Es ist eine Gleichung mit sehr vielen Variablen, die Benutzer nicht kontrollieren. Vermeiden Sie dies bitte :)
Stephan Herrmann

Antworten:

8

TL; DR, dies ist ein Compiler-Fehler.

Es gibt keine Regel, die einer bestimmten anwendbaren Methode Vorrang einräumt, wenn sie vererbt wird, oder einer Standardmethode. Interessanterweise, wenn ich den Code auf ändere

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

Die iterable.forEach((A a) -> aList.add(a));Anweisung erzeugt einen Fehler in Eclipse.

Da sich beim Deklarieren einer weiteren Überladung keine Eigenschaft der forEach(Consumer<? super T) c)Methode über die Iterable<T>Schnittstelle geändert hat, kann die Entscheidung von Eclipse, diese Methode auszuwählen, nicht (konsistent) auf einer Eigenschaft der Methode basieren. Es ist immer noch die einzige geerbte Methode, immer noch die einzige defaultMethode, immer noch die einzige JDK-Methode und so weiter. Keine dieser Eigenschaften sollte sich ohnehin auf die Methodenauswahl auswirken.

Beachten Sie, dass Sie die Deklaration in ändern

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

erzeugt auch einen "mehrdeutigen" Fehler, so dass die Anzahl der anwendbaren überladenen Methoden ebenfalls keine Rolle spielt, selbst wenn es nur zwei Kandidaten gibt, gibt es keine allgemeine Präferenz für default Methoden.

Bisher scheint das Problem aufzutreten, wenn zwei anwendbare Methoden vorhanden sind und eine defaultMethode und eine Vererbungsbeziehung beteiligt sind, aber dies ist nicht der richtige Ort, um weiter zu graben.


Es ist jedoch verständlich, dass die Konstrukte Ihres Beispiels möglicherweise von unterschiedlichem Implementierungscode im Compiler verarbeitet werden, wobei einer einen Fehler aufweist, der andere jedoch nicht.
a -> aList.add(a)ist ein implizit typisierter Lambda-Ausdruck, der nicht für die Überlastungsauflösung verwendet werden kann. Im Gegensatz dazu (A a) -> aList.add(a)handelt es sich um einen explizit typisierten Lambda-Ausdruck, mit dem eine übereinstimmende Methode aus den überladenen Methoden ausgewählt werden kann. Dies hilft hier jedoch nicht (sollte hier nicht helfen), da alle Methoden Parametertypen mit genau derselben funktionalen Signatur haben .

Als Gegenbeispiel mit

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

Die funktionalen Signaturen unterscheiden sich, und die Verwendung eines explizit eingegebenen Lambda-Ausdrucks kann in der Tat bei der Auswahl der richtigen Methode hilfreich sein, während der implizit typisierte Lambda-Ausdruck nicht hilfreich ist und daher forEach(s -> s.isEmpty())einen Compilerfehler erzeugt. Und alle Java-Compiler sind sich einig.

Beachten Sie, dass dies aList::addeine mehrdeutige Methodenreferenz ist, da die addMethode ebenfalls überladen ist, sodass sie auch bei der Auswahl einer Methode helfen kann. Methodenreferenzen werden jedoch möglicherweise trotzdem von einem anderen Code verarbeitet. Das Wechseln zu einer eindeutigen aList::containsoder das Ändern Listzu Collection, um addeindeutig zu machen , hat das Ergebnis in meiner Eclipse-Installation (die ich verwendet habe 2019-06) nicht geändert .

Holger
quelle
1
@howlger dein Kommentar macht keinen Sinn. Die Standardmethode ist die geerbte Methode und die Methode ist überladen. Es gibt keine andere Methode. Die Tatsache, dass die geerbte Methode eine defaultMethode ist, ist nur ein zusätzlicher Punkt. Meine Antwort zeigt bereits ein Beispiel, in dem Eclipse der Standardmethode keinen Vorrang einräumt.
Holger
1
@howlger gibt es einen grundlegenden Unterschied in unserem Verhalten. Sie haben nur festgestellt, dass das Entfernen defaultdas Ergebnis ändert, und sofort davon ausgegangen, dass der Grund für das beobachtete Verhalten gefunden wurde. Sie sind so übermütig, dass Sie andere Antworten als falsch bezeichnen, obwohl sie nicht einmal widersprüchlich sind. Weil Sie Ihr eigenes Verhalten über andere projizieren, habe ich nie behauptet, dass Vererbung der Grund sei . Ich habe bewiesen, dass es nicht so ist. Ich habe gezeigt, dass das Verhalten inkonsistent ist, da Eclipse die bestimmte Methode in einem Szenario auswählt, jedoch nicht in einem anderen, in dem drei Überladungen vorhanden sind.
Holger
1
@howlger außerdem habe ich am Ende dieses Kommentars bereits ein anderes Szenario benannt , eine Schnittstelle, keine Vererbung, zwei Methoden, eine defaultandere abstract, mit zwei verbraucherähnlichen Argumenten erstellt und ausprobiert. Eclipse sagt zu Recht, dass es mehrdeutig ist, obwohl es sich um eine defaultMethode handelt. Anscheinend ist die Vererbung für diesen Eclipse-Fehler immer noch relevant, aber im Gegensatz zu Ihnen werde ich nicht verrückt und nenne andere Antworten falsch, nur weil sie den Fehler nicht vollständig analysiert haben. Das ist hier nicht unsere Aufgabe.
Holger
1
@howlger nein, der Punkt ist, dass dies ein Fehler ist. Die meisten Leser werden sich nicht einmal um die Details kümmern. Eclipse könnte jedes Mal würfeln, wenn es eine Methode auswählt, es spielt keine Rolle. Eclipse sollte keine Methode auswählen, wenn sie nicht eindeutig ist. Daher spielt es keine Rolle, warum sie eine auswählt. Diese Antwort beweist, dass das Verhalten inkonsistent ist, was bereits ausreicht, um stark darauf hinzuweisen, dass es sich um einen Fehler handelt. Es ist nicht notwendig, auf die Zeile im Eclipse-Quellcode zu zeigen, in der etwas schief geht. Das ist nicht der Zweck von Stackoverflow. Vielleicht verwechseln Sie Stackoverflow mit dem Bug-Tracker von Eclipse.
Holger
1
@howlger Sie behaupten wieder fälschlicherweise, dass ich eine (falsche) Aussage darüber gemacht habe, warum Eclipse diese falsche Wahl getroffen hat. Wieder habe ich nicht, da Eclipse überhaupt keine Wahl treffen sollte. Die Methode ist nicht eindeutig. Punkt. Der Grund, warum ich den Begriff „ geerbt “ verwendet habe, ist, dass die Unterscheidung identisch benannter Methoden erforderlich war. Ich hätte stattdessen " Standardmethode " sagen können , ohne die Logik zu ändern. Richtiger hätte ich den Ausdruck "genau die Methode Eclipse, die aus irgendeinem Grund falsch ausgewählt wurde " verwenden sollen. Sie können einen der drei Sätze austauschbar verwenden, und die Logik ändert sich nicht.
Holger
2

Der Eclipse-Compiler wird korrekt in die defaultMethode aufgelöst , da dies die spezifischste Methode gemäß der Java-Sprachspezifikation 15.12.2.5 ist :

Wenn genau eine der maximal spezifischen Methoden konkret ist (dh nicht abstractoder standardmäßig), ist dies die spezifischste Methode.

javac(wird standardmäßig von Maven und IntelliJ verwendet) teilt mit, dass der Methodenaufruf hier nicht eindeutig ist. Laut Java Language Specification ist dies jedoch nicht mehrdeutig, da eine der beiden Methoden hier die spezifischste ist.

Implizit typisierte Lambda-Ausdrücke werden anders behandelt als explizit typisierte Lambda-Ausdrücke in Java. Implizit typisiert im Gegensatz zu explizit typisierten Lambda-Ausdrücken durchlaufen die erste Phase, um die Methoden des strengen Aufrufs zu identifizieren (siehe Java-Sprachspezifikation jls-15.12.2.2 , erster Punkt). Daher ist der Methodenaufruf hier für implizit typisierte Lambda-Ausdrücke nicht eindeutig .

In Ihrem Fall besteht die Problemumgehung für diesen javacFehler darin, den Typ der Funktionsschnittstelle anzugeben , anstatt einen explizit typisierten Lambda-Ausdruck wie folgt zu verwenden:

iterable.forEach((ConsumerOne<A>) aList::add);

oder

iterable.forEach((Consumer<A>) aList::add);

Hier ist Ihr Beispiel zum Testen weiter minimiert:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}
heulend
quelle
3
Sie haben die Vorbedingung direkt vor dem zitierten Satz übersehen: „ Wenn alle maximal spezifischen Methoden überschreibungsäquivalente Signaturen haben “ Natürlich haben zwei Methoden, deren Argumente völlig unabhängige Schnittstellen sind, keine überschreibungsäquivalenten Signaturen. Außerdem würde dies nicht erklären, warum Eclipse die Auswahl der defaultMethode beendet, wenn drei Kandidatenmethoden vorhanden sind oder wenn beide Methoden in derselben Schnittstelle deklariert sind.
Holger
1
@Holger In Ihrer Antwort heißt es: "Es gibt keine Regel, die einer bestimmten anwendbaren Methode Vorrang einräumt, wenn sie vererbt wird, oder einer Standardmethode." Verstehe ich Sie richtig, dass Sie sagen, dass die Voraussetzung für diese nicht existierende Regel hier nicht gilt? Bitte beachten Sie, dass der Parameter hier eine Funktionsschnittstelle ist (siehe JLS 9.8).
Heulen
1
Sie haben einen Satz aus dem Zusammenhang gerissen. Der Satz beschreibt die Auswahl von überschreibungsäquivalenten Methoden, dh eine Auswahl zwischen Deklarationen, die alle zur Laufzeit dieselbe Methode aufrufen würden, da es in der konkreten Klasse nur eine konkrete Methode gibt. Dies ist irrelevant für unterschiedliche Methoden wie forEach(Consumer)und forEach(Consumer2)die niemals zur gleichen Implementierungsmethode führen können.
Holger
2
@StephanHerrmann Ich kenne kein JEP oder JSR, aber die Änderung scheint ein Fix zu sein, der der Bedeutung von „konkret“ entspricht, dh vergleiche mit JLS §9.4 : „ Standardmethoden unterscheiden sich von konkreten Methoden (§8.4. 3.1), die in Klassen deklariert sind. ”, Was sich nie geändert hat.
Holger
2
@StephanHerrmann Ja, es sieht so aus, als wäre die Auswahl eines Kandidaten aus überschreibungsäquivalenten Methoden komplizierter geworden, und es wäre interessant, die Gründe dafür zu kennen, aber es ist für die vorliegende Frage nicht relevant. Es sollte ein weiteres Dokument geben, in dem Änderungen und Motivationen erläutert werden. In der Vergangenheit gab es, aber mit dieser Richtlinie "Alle ½ Jahre eine neue Version" scheint es unmöglich, die Qualität beizubehalten ...
Holger vor
2

Der Code, in dem Eclipse JLS §15.12.2.5 implementiert, findet keine der beiden Methoden spezifischer als die andere, selbst für den Fall des explizit typisierten Lambda.

Im Idealfall würde Eclipse hier anhalten und Unklarheiten melden. Leider enthält die Implementierung der Überlastungsauflösung zusätzlich zur Implementierung von JLS nicht trivialen Code. Nach meinem Verständnis muss dieser Code (der aus der Zeit stammt, als Java 5 neu war) beibehalten werden, um einige Lücken in JLS zu schließen.

Ich habe eingereicht https://bugs.eclipse.org/562538 , um dies zu verfolgen.

Unabhängig von diesem speziellen Fehler kann ich nur dringend von diesem Codestil abraten. Überladung ist gut für eine gute Anzahl von Überraschungen in Java, multipliziert mit Lambda-Typ-Inferenz. Die Komplexität ist im Verhältnis zum wahrgenommenen Gewinn ziemlich unverhältnismäßig.

Stephan Herrmann
quelle
Vielen Dank. Hatte bereits bugs.eclipse.org/bugs/show_bug.cgi?id=562507 angemeldet , könnten Sie vielleicht helfen, sie zu verlinken oder als Duplikat zu schließen ...
ernest_k