Wybór sygnatury metody dla wyrażenia lambda z wieloma pasującymi typami docelowymi

11

Odpowiadałem na pytanie i wpadłem na scenariusz, którego nie potrafię wyjaśnić. Rozważ ten kod:

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
    }
}

Nie rozumiem, dlaczego wyraźne wpisanie parametru lambda (A a) -> aList.add(a)powoduje, że kod się kompiluje. Dodatkowo, dlaczego łączy się to z przeciążeniem Iterablezamiast w tym CustomIterable?
Czy jest jakieś wyjaśnienie tego lub link do odpowiedniej sekcji specyfikacji?

Uwaga: iterable.forEach((A a) -> aList.add(a));kompiluje się tylko po CustomIterable<T>rozszerzeniu Iterable<T>(płaskie przeciążenie metod CustomIterablepowoduje niejednoznaczny błąd)


Uzyskanie tego na obu:

  • wersja openjdk „13.0.2” 2020-01-14
    Kompilator Eclipse
  • wersja Openjdk „1.8.0_232”
    Kompilator Eclipse

Edycja : Powyższy kod nie kompiluje się przy budowaniu z maven, podczas gdy Eclipse pomyślnie kompiluje ostatni wiersz kodu.

ernest_k
źródło
3
Żadna z trzech nie kompiluje się w Javie 8. Teraz nie jestem pewien, czy jest to błąd, który został naprawiony w nowszej wersji, czy też błąd / funkcja, która została wprowadzona ... Prawdopodobnie należy podać wersję Java
Sweeper
@Sweeper Początkowo mam to za pomocą jdk-13. Kolejne testy w Javie 8 (jdk8u232) pokazują te same błędy. Nie jestem pewien, dlaczego ostatni nie kompiluje się na twoim komputerze.
ernest_k
Nie można również odtworzyć na dwóch kompilatorach internetowych ( 1 , 2 ). Używam 1.8.0_221 na moim komputerze. Robi się coraz dziwniej ...
Sweeper
1
@ernest_k Eclipse ma własną implementację kompilatora. To może być kluczowa informacja dla pytania. Ponadto, moim zdaniem, w pytaniu należy podkreślić fakt, że kompilacja czystego mavu zawiera błędy również w ostatnim wierszu . Z drugiej strony, jeśli chodzi o powiązane pytanie, założenie, że OP używa Eclipse, można wyjaśnić, ponieważ kod nie jest powtarzalny.
Naman
2
Chociaż rozumiem tę intelektualną wartość pytania, mogę jedynie odradzać tworzenie przeciążonych metod, które różnią się jedynie interfejsami funkcjonalnymi i oczekuję, że można je bezpiecznie nazwać przekazaniem lambda. Nie wierzę, że połączenie wnioskowania typu lambda i przeciążenia jest czymś, co przeciętny programiści kiedykolwiek będzie w stanie zrozumieć. Jest to równanie z bardzo wieloma zmiennymi, których użytkownicy nie kontrolują. UNIKAJ TEGO, proszę :)
Stephan Herrmann

Odpowiedzi:

8

TL; DR, to jest błąd kompilatora.

Nie ma reguły, która dawałaby pierwszeństwo konkretnej stosownej metodzie po jej odziedziczeniu lub metodzie domyślnej. Co ciekawe, kiedy zmieniam kod na

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
}

iterable.forEach((A a) -> aList.add(a));stwierdzenie wywołuje błąd w Eclipse.

Ponieważ żadna właściwość forEach(Consumer<? super T) c)metody z Iterable<T>interfejsu nie zmieniła się podczas deklarowania kolejnego przeciążenia, decyzja Eclipse o wybraniu tej metody nie może (konsekwentnie) opierać się na żadnej właściwości metody. Jest to wciąż jedyna metoda odziedziczona, wciąż jedyna defaultmetoda, wciąż jedyna metoda JDK i tak dalej. Żadna z tych właściwości i tak nie powinna wpływać na wybór metody.

Pamiętaj, że zmiana deklaracji na

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

powoduje również „dwuznaczny” błąd, więc liczba stosowanych przeciążonych metod również nie ma znaczenia, nawet jeśli są tylko dwaj kandydaci, nie ma ogólnej preferencji względem default metod.

Jak do tej pory wydaje się, że problem pojawia się, gdy istnieją dwie odpowiednie metody oraz defaultmetoda i relacja dziedziczenia, ale nie jest to właściwe miejsce na dalsze kopanie.


Ale zrozumiałe jest, że konstrukcje z twojego przykładu mogą być obsługiwane przez inny kod implementacyjny w kompilatorze, jeden wykazuje błąd, a drugi nie.
a -> aList.add(a)jest niejawnie wpisanym wyrażeniem lambda, którego nie można użyć do rozwiązania problemu przeciążenia. W przeciwieństwie,(A a) -> aList.add(a) jest wyraźnie wpisany wyrażeniem lambda, którego można użyć do wybrania pasującej metody spośród przeciążonych metod, ale to nie pomaga tutaj (nie powinno tutaj pomóc), ponieważ wszystkie metody mają typy parametrów z dokładnie taką samą sygnaturą funkcjonalną .

Jako kontrprzykład z

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

podpisy funkcjonalne różnią się, a użycie jawnie typu wyrażenia lambda może rzeczywiście pomóc w wyborze właściwej metody, podczas gdy niejawnie wpisane wyrażenie lambda nie pomaga, więc forEach(s -> s.isEmpty()) powoduje błąd kompilatora. I wszystkie kompilatory Java się z tym zgadzają.

Zauważ, że aList::addjest to niejednoznaczne odwołanie do metody, ponieważ addmetoda jest również przeciążona, więc nie może również pomóc w wyborze metody, ale odwołania do metod mogą być przetwarzane przez inny kod. Przełączenie na jednoznacznym aList::containslub zmienia Listsię Collection, aby addjednoznaczna, nie zmienił wynik w mojej instalacji Eclipse (kiedyś 2019-06).

Holger
źródło
1
@howlger Twój komentarz nie ma sensu. Metodą domyślną jest metoda dziedziczona, a metoda jest przeciążona. Nie ma innej metody. Fakt, że odziedziczona metoda jest defaultmetodą, to tylko dodatkowy punkt. Moja odpowiedź pokazuje już przykład, w którym Eclipse nie daje pierwszeństwa domyślnej metodzie.
Holger
1
@ wycie jest zasadnicza różnica w naszych zachowaniach. Odkryłeś tylko, że usunięcie defaultzmienia wynik i od razu zakładasz, że znalazłeś przyczynę zaobserwowanego zachowania. Jesteś tak zbyt pewny tego, że błędnie nazywasz inne odpowiedzi, mimo że nie są nawet sprzeczne. Ponieważ projektujesz swoje własne zachowanie w stosunku do innych , nigdy nie twierdziłem, że powodem jest dziedziczenie . Udowodniłem, że tak nie jest. Wykazałem, że zachowanie jest niespójne, ponieważ Eclipse wybiera konkretną metodę w jednym scenariuszu, ale nie w innym, w którym występują trzy przeciążenia.
Holger
1
@howlger poza tym, na końcu tego komentarza nazwałem już inny scenariusz , utwórz jeden interfejs, bez dziedziczenia, dwie metody, jedną defaultdrugą abstract, z dwoma argumentami podobnymi do konsumentów i spróbuj. Zaćmienie słusznie mówi, że jest dwuznaczna, mimo że jedna jest defaultmetodą. Najwyraźniej dziedziczenie wciąż dotyczy tego błędu Eclipse, ale w przeciwieństwie do ciebie, nie oszaleję i nie nazywam innych odpowiedzi błędnie, tylko dlatego, że nie przeanalizowali błędu w całości. To nie nasza praca tutaj.
Holger
1
@ wyjący nie, chodzi o to, że to błąd. Większość czytelników nie dba nawet o szczegóły. Zaćmienie może rzucać kostką za każdym razem, gdy wybiera metodę, to nie ma znaczenia. Zaćmienie nie powinno wybierać metody, gdy jest niejednoznaczna, więc nie ma znaczenia, dlaczego ją wybiera. Ta odpowiedź dowodzi, że zachowanie jest niespójne, co już wystarczy, aby zdecydowanie wskazać, że jest to błąd. Nie jest konieczne wskazywanie linii w kodzie źródłowym Eclipse, gdzie coś idzie nie tak. To nie jest celem Stackoverflow. Być może mylisz Stackoverflow ze śledzeniem błędów Eclipse.
Holger
1
@ Howlger, znowu niepoprawnie twierdzisz, że złożyłem (niewłaściwe) stwierdzenie, dlaczego Eclipse dokonało złego wyboru. Znów tego nie zrobiłem, ponieważ zaćmienie wcale nie powinno być wyborem. Metoda jest niejednoznaczna. Punkt. Powodem, dla którego użyłem terminu „ odziedziczony ”, jest to, że konieczne było rozróżnienie metod o identycznych nazwach. Mógłbym zamiast tego powiedzieć „ domyślna metoda ”, bez zmiany logiki. Bardziej poprawnie, powinienem był użyć wyrażenia „ sama metoda Eclipse niepoprawnie wybrana z jakiegokolwiek powodu ”. Możesz użyć jednej z trzech fraz zamiennie, a logika się nie zmienia.
Holger
2

Kompilator Eclipse poprawnie rozwiązuje tę defaultmetodę , ponieważ jest to najbardziej specyficzna metoda zgodnie ze specyfikacją języka Java 15.12.2.5 :

Jeśli dokładnie jedna z maksymalnie określonych metod jest konkretna (to znaczy nie abstractlub domyślna), jest to metoda najbardziej szczegółowa.

javac(domyślnie używane przez Maven i IntelliJ) informuje, że wywołanie metody jest tutaj niejednoznaczne. Jednak zgodnie ze specyfikacją języka Java nie jest to niejednoznaczne, ponieważ jedna z dwóch metod jest tutaj najbardziej specyficzna.

Wyrażenia lambda niejawnie wpisane są obsługiwane w Javie inaczej niż wyrażenia lambda typowo wpisane . W domniemanym typowaniu, w przeciwieństwie do jawnie wpisanych wyrażeń lambda, przechodzi się przez pierwszą fazę w celu identyfikacji metod ścisłego wywoływania (patrz specyfikacja języka Java jls-15.12.2.2 , pierwszy punkt). Dlatego wywołanie metody tutaj jest dwuznaczne dla niejawnie wpisanego wyrażeń lambda.

W twoim przypadku obejściem tego javacbłędu jest określenie rodzaju interfejsu funkcjonalnego zamiast używania jawnie wpisanego wyrażenia lambda w następujący sposób:

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

lub

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

Oto Twój przykład dodatkowo zminimalizowany do testowania:

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)
    }

}
wycie
źródło
3
Przegapiłeś warunek poprzedzający cytowane zdanie: „ Jeśli wszystkie maksymalnie określone metody mają podpisy równoważne z nadpisaniem ” Oczywiście dwie metody, których argumenty są całkowicie niepowiązanymi interfejsami, nie mają podpisów równoważnych z nadpisaniem. Poza tym nie wyjaśniałoby to, dlaczego Eclipse przestaje wybierać defaultmetodę, gdy istnieją trzy metody kandydujące lub gdy obie metody są zadeklarowane w tym samym interfejsie.
Holger
1
@Holger Twoja odpowiedź twierdzi: „Nie ma reguły, która dawałaby pierwszeństwo konkretnej stosownej metodzie, gdy jest ona dziedziczona lub metodzie domyślnej”. Czy rozumiem cię poprawnie, że mówisz, że warunek wstępny dla tej nieistniejącej reguły nie ma tutaj zastosowania? Uwaga: tutaj parametr jest interfejsem funkcjonalnym (patrz JLS 9.8).
wycie
1
Wyrwałeś zdanie z kontekstu. To zdanie opisuje wybór metod równoważenia zastępowania , innymi słowy, wybór między deklaracjami, które wszystkie wywoływałyby tę samą metodę w czasie wykonywania, ponieważ w konkretnej klasie będzie tylko jedna konkretna metoda. Jest to nieistotne w przypadku odrębnych metod, takich jak forEach(Consumer)i forEach(Consumer2)które nigdy nie kończą się tą samą metodą implementacji.
Holger
2
@StephanHerrmann Nie znam JEP ani JSR, ale zmiana wygląda jak poprawka zgodna ze znaczeniem „konkret”, tj. Porównaj z JLS§9.4 : „ Metody domyślne różnią się od konkretnych metod (§ 8.4. 3.1), które są zadeklarowane w klasach. ”, Który nigdy się nie zmienił.
Holger
2
@StephanHerrmann tak, wygląda na to, że wybranie kandydata spośród metod zastępujących stało się bardziej skomplikowane i byłoby interesujące poznać uzasadnienie, ale nie ma to związku z omawianym pytaniem. Powinien istnieć inny dokument wyjaśniający zmiany i motywacje. W przeszłości istniała, ale przy tej „nowej wersji co pół roku” utrzymanie jakości wydaje się niemożliwe ...
Holger
2

Kod, w którym Eclipse implementuje JLS § 15.12.2.5, nie uznaje żadnej metody za bardziej szczegółową od drugiej, nawet w przypadku jawnie wpisanej lambda.

Idealnie więc Eclipse zatrzyma się tutaj i zgłosi dwuznaczność. Niestety, implementacja rozwiązania problemu przeciążenia zawiera nietuzinkowy kod oprócz implementacji JLS. Z mojego zrozumienia tego kodu (który pochodzi z czasu, gdy Java 5 była nowa) należy zachować, aby wypełnić pewne luki w JLS.

Złożyłem https://bugs.eclipse.org/562538 aby to śledzić.

Niezależnie od tego konkretnego błędu, mogę tylko zdecydowanie odradzać ten styl kodu. Przeciążenie jest dobre dla wielu niespodzianek w Javie, pomnożone przez wnioskowanie typu lambda, złożoność jest dość nieproporcjonalna do postrzeganego zysku.

Stephan Herrmann
źródło
Dziękuję Ci. Zarejestrowałeś już bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , być może możesz pomóc połączyć je lub zamknąć jako duplikat ...
ernest_k