Вибір підпису методу для вираження лямбда з кількома типовими цільовими типами


11

Я відповідав на запитання і наткнувся на сценарій, який я не можу пояснити. Розглянемо цей код:

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

Я не розумію, чому явно набравши параметр лямбда (A a) -> aList.add(a)робить компіляцію коду. Крім того, чому це пов'язане з перевантаженням, Iterableа не тим, що відбувається в CustomIterable?
Чи є якесь пояснення до цього чи посилання на відповідний розділ специфікації?

Примітка: iterable.forEach((A a) -> aList.add(a));компілюється лише при CustomIterable<T>розширенні Iterable<T>(рівно перевантаження методів CustomIterableпризводить до неоднозначної помилки)


Отримання цього на обох:

  • openjdk версія "13.0.2" 2020-01-14
    Компілятор Eclipse
  • openjdk версія "1.8.0_232"
    компілятор Eclipse

Редагувати : Код, наведений вище, не вдається компілювати на будівництві з Maven, тоді як Eclipse успішно збирає останній рядок коду.


3
Жодна з трьох компіляцій на Java 8. Зараз я не впевнений, що це помилка, яку виправили в новій версії, або помилка / функція, яка була введена ... Ви, мабуть, повинні вказати версію Java
Sweeper

@Sweeper Я спочатку отримав це за допомогою jdk-13. Подальше тестування в java 8 (jdk8u232) показує ті самі помилки. Не впевнений, чому останній не збирається ні на вашій машині.
ernest_k

Неможливо відтворити і на двох онлайн-компіляторах ( 1 , 2 ). Я використовую 1.8.0_221 на своїй машині. Це стає все дивнішим і дивнішим ...
Підмітати

1
@ernest_k Eclipse має власну реалізацію компілятора. Це може бути вирішальною інформацією для питання. Також, на мою думку, питання має бути висвітлено і те, що помилки в чистому складі Maven також є останнім рядком . З іншого боку, щодо пов'язаного питання, припущення, що ОП також використовує Eclipse, може бути уточнено, оскільки код не відтворюється.
Наман

2
Хоча я розумію цю інтелектуальну цінність питання, я можу лише порадити не створювати перевантажених методів, які відрізняються лише функціональними інтерфейсами і очікуючи, що це можна сміливо назвати передачею лямбда. Я не вірю, що поєднання ламбда-виводу і перевантаження - це те, що пересічний програміст коли-небудь наблизиться до розуміння. Це рівняння з дуже багатьма змінними, якими користувачі не керують. Ухиліться від цього, будь ласка :)
Стефан Геррманн

Відповіді:


8

TL; DR, це помилка компілятора.

Не існує правила, яке б надало перевагу конкретному застосовному методу, коли він передається у спадок або методу за замовчуванням. Цікаво, що коли я змінюю код на

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));оператор видає помилку в Eclipse.

Оскільки жодна властивість forEach(Consumer<? super T) c)методу з Iterable<T>інтерфейсу не змінювалася при оголошенні чергової перевантаження, рішення Eclipse про вибір цього методу не може (послідовно) базуватися на будь-якій властивості методу. Це все ще єдиний успадкований метод, все ще єдиний defaultметод, все ще єдиний метод JDK тощо. Жодне з цих властивостей не повинно впливати на вибір методу.

Зауважте, що зміна декларації на

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

також створює "неоднозначну" помилку, тому кількість застосованих перевантажених методів також не має значення, навіть коли є лише два кандидати, немає загальної переваги defaultметодам.

Поки що проблема, здається, з'являється, коли є два застосовні методи, і defaultметод і відносини спадкування пов'язані, але це не правильне місце для подальшого копання.


Але зрозуміло, що конструкції вашого прикладу можуть оброблятися різним кодом реалізації в компіляторі, один виявляє помилку, а інший - ні.
a -> aList.add(a)- це неявно набраний лямбда-вираз, який не можна використовувати для роздільної здатності перевантаження. Навпаки, (A a) -> aList.add(a)це явно набраний лямбда-вираз, який можна використовувати для вибору методу узгодження із перевантаженими методами, але він тут не допомагає (не повинен тут допомагати), оскільки всі методи мають типи параметрів з точно однаковою функціональною підписом .

Як зустрічний приклад, с

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

функціональні підписи відрізняються, і використання явно введеного лямбда-виразу може дійсно допомогти у виборі правильного методу, тоді як неявно введений лямбда-вираз не допомагає, тому forEach(s -> s.isEmpty())створює помилку компілятора. І всі компілятори Java згодні з цим.

Зауважте, що aList::addце неоднозначне посилання на метод, оскільки addметод теж перевантажений, тому він також не може допомогти у виборі методу, але посилання методу все одно можуть оброблятися іншим кодом. Перемикання на однозначне aList::containsабо зміни Listв Collection, щоб зробити addоднозначний, не змінити результат в моїй установці Eclipse , (я 2019-06).


1
@howlger ваш коментар не має сенсу. Метод за замовчуванням - це успадкований метод, і метод перевантажений. Іншого методу немає. Те, що успадкований метод - це defaultметод, є лише додатковим моментом. У моїй відповіді вже є приклад, коли Eclipse не дає переваги методу за замовчуванням.
Холгер

1
@howlger є принципова різниця в нашій поведінці. Ви лише виявили, що видалення defaultзмінює результат і відразу припускаєте, що знайшли причину спостережуваної поведінки. Ви настільки переконані в цьому, що називаєте інші відповіді неправильними, незважаючи на те, що вони навіть не суперечать. Оскільки ви проектуєте свою власну поведінку над іншими, я ніколи не претендував на спадщину . Я довів, що це не так. Я продемонстрував, що поведінка суперечлива, оскільки Eclipse вибирає конкретний метод в одному сценарії, а не в іншому, де існують три перевантаження.
Холгер

1
@howlger окрім цього, я вже назвав ще один сценарій в кінці цього коментаря , створити один інтерфейс, без успадкування, два методи, один defaultза іншим abstract, з двома споживачами, як аргументи, і спробувати його. Eclipse правильно говорить, що це неоднозначно, незважаючи на те, що це defaultметод. Мабуть, успадкування все ще має відношення до цієї помилки Eclipse, але на відміну від вас, я не збожеволів і називаю інші відповіді неправильно, лише тому, що вони не проаналізували помилку в повному обсязі. Це не наша робота тут.
Холгер

1
@howlger ні, справа в тому, що це помилка. Більшість читачів навіть не подбають про деталі. Eclipse може котити кістки щоразу, коли він вибирає метод, це не має значення. Eclipse не повинен вибирати метод, коли він неоднозначний, тому не має значення, чому він вибирає його. Ця відповідь доводить, що поведінка непослідовна, що вже достатньо, щоб сильно вказувати на те, що це помилка. Не обов’язково вказувати рядок у вихідному коді Eclipse, де справи йдуть не так. Це не мета Stackoverflow. Можливо, ви плутаєте Stackoverflow з трекером помилок Eclipse.
Холгер

1
@howlger Ви знову невірно стверджуєте, що я зробив (неправильне) твердження, чому Eclipse зробив той неправильний вибір. Знову ж таки, я цього не зробив, оскільки затемнення взагалі не повинно робити вибір. Метод неоднозначний. Точка. Причина, чому я використовував термін « успадкований », полягає в тому, що розрізнення однаково названих методів було необхідним. Я міг би сказати " метод за замовчуванням " замість цього, не змінюючи логіку. Більш правильно, я повинен був використовувати фразу " той самий метод Eclipse, неправильно вибраний з будь-якої причини ". Ви можете використовувати будь-яку з трьох фраз взаємозамінно, і логіка не змінюється.
Холгер

2

Компілятор Eclipse правильно вирішує defaultметод , оскільки це найбільш специфічний метод відповідно до специфікації мови Java 15.12.2.5 :

Якщо саме один із максимально конкретних методів конкретний (тобто не- abstractабо за замовчуванням), це найбільш конкретний метод.

javac(використовується за замовчуванням Maven та IntelliJ) повідомляє, що виклик методу тут неоднозначний. Але згідно із специфікацією мови Java це неоднозначно, оскільки один із двох методів є найбільш конкретним методом.

Неявно введені лямбда-вирази обробляються інакше, ніж явно введені лямбда-вирази на Java. Неявно введені на відміну від явно набраних лямбда-виразів проходять через першу фазу для виявлення методів суворого виклику (див. Специфікацію мови Java jls-15.12.2.2 , перша точка). Отже, виклик методу тут неоднозначний для неявно введених лямбда-виразів.

У вашому випадку вирішенням цієї javacпомилки є визначення типу функціонального інтерфейсу, а не використання явно набраного лямбда-виразу таким чином:

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

або

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

Ось ваш приклад, який мінімізується для тестування:

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

}

3
Ви пропустили попередню умову перед цитованим реченням: " Якщо всі максимально конкретні методи мають переозначення еквівалентних підписів " Звичайно, два методи, аргументи яких повністю пов'язані між собою інтерфейсами, не мають еквівалентних підписів. Крім того, це не пояснює, чому Eclipse зупиняє вибір defaultметоду, коли є три методи-кандидати або коли обидва способи оголошені в одному інтерфейсі.
Холгер

1
@Holger Ваша відповідь стверджує: "Не існує жодного правила, яке б надало перевагу конкретному застосовному методу, коли він передається у спадок або методу за замовчуванням". Я правильно вас розумію, що ви говорите про те, що умова цього неіснуючого правила тут не поширюється? Зверніть увагу, параметр тут є функціональним інтерфейсом (див. JLS 9.8).
виття

1
Ви вирвали речення з контексту. У реченні описується вибір методів, що еквівалентно переосмислюванню , іншими словами, вибір між деклараціями, які б усі викликали один і той же метод під час виконання, оскільки у конкретному класі буде лише один конкретний метод. Це не має значення для випадків різних методів, таких як forEach(Consumer)і forEach(Consumer2)які ніколи не можуть закінчитися тим самим методом реалізації.
Холгер

2
@StephanHerrmann Я не знаю JEP чи JSR, але зміна виглядає як виправлення, яке відповідає значенню "конкретного", тобто порівняти з JLS§9.4 : " Методи за замовчуванням відрізняються від конкретних методів (§8.4. 3.1), які декларуються в класах. ”, Який ніколи не змінювався.
Холгер

2
@StephanHerrmann так, схоже, вибір кандидата з методів, що еквівалентно переосмисленню, ускладнився, і було б цікаво дізнатися обґрунтування, але це не стосується питання, що знаходиться в цій галузі. Повинно бути ще один документ, що пояснює зміни та мотиви. У минулому існувала, але з цією політикою "нова версія кожні півтора року" зберігати якість здається неможливим ...
Holger

2

Код, де Eclipse реалізує JLS §15.12.2.5, не знаходить жодного методу як більш конкретного, ніж інший, навіть у випадку явно набраної лямбда.

Тож в ідеалі Затьмарення зупинилося б тут і повідомило б про неоднозначність. На жаль, реалізація роздільної здатності перевантаження має нетривіальний код на додаток до реалізації JLS. Наскільки я розумію, цей код (який датується часом, коли Java 5 був новим) повинен зберігатися, щоб заповнити деякі прогалини в JLS.

Я подав https://bugs.eclipse.org/562538 для відстеження цього.

Незалежно від цієї конкретної помилки, я можу лише настійно порадити цей стиль коду. Перевантаження хороша для великої кількості сюрпризів на Java, помножених на умови лямбда-виводу, складність є досить непропорційною WR від сприйнятого посилення.


Дякую. Якщо ви вже зареєструвались bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , можливо, ви могли б допомогти пов’язати їх або закрити один як дублікат ...
ernest_k
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.