Выбор сигнатуры метода для лямбда-выражения с несколькими совпадающими типами целей

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 успешно компилирует последнюю строку кода.

ernest_k
источник
3
Ни одна из трех компиляций на Java 8. Теперь я не уверен, является ли это ошибкой, которая была исправлена ​​в более новой версии, или ошибкой / введенной функцией ... Вы, вероятно, должны указать версию Java
Sweeper,
@ Сначала я получил это, используя jdk-13. Последующее тестирование в Java 8 (jdk8u232) показывает те же ошибки. Не уверен, почему последний не компилируется на вашем компьютере.
ernest_k
Не может воспроизвести и на двух онлайн-компиляторах ( 1 , 2 ). Я использую 1.8.0_221 на моей машине. Это становится все страннее и страннее ...
Sweeper
1
@ernest_k Eclipse имеет собственную реализацию компилятора. Это может быть важной информацией для вопроса. Кроме того, тот факт, что чистые maven ошибки сборки последней строки тоже должны быть выделены в вопросе на мой взгляд. С другой стороны, что касается связанного вопроса, предположение, что OP также использует Eclipse, могло бы быть разъяснено, так как код не воспроизводим.
Наман
3
Хотя я понимаю эту интеллектуальную ценность вопроса, я могу только рекомендовать не создавать перегруженных методов, которые отличаются только функциональными интерфейсами, и ожидать, что их можно смело назвать прохождением лямбды. Я не верю, что сочетание вывода и перегрузки лямбда-типа - это то, что среднестатистический программист когда-либо приблизится к пониманию. Это уравнение с очень многими переменными, которые пользователи не контролируют. ИЗБЕГАЙТЕ ЭТОГО, пожалуйста :)
Стефан Херрманн

Ответы:

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

Holger
источник
1
@howlger ваш комментарий не имеет смысла. Метод по умолчанию является унаследованным, а метод перегружен. Другого метода нет. Тот факт, что унаследованный метод является defaultметодом, является лишь дополнительным моментом. Мой ответ уже показывает пример, в котором Eclipse не отдает приоритет методу по умолчанию.
Хольгер
1
@howlger есть принципиальная разница в нашем поведении. Вы только обнаружили, что удаление defaultменяет результат и сразу предполагаете, что нашли причину наблюдаемого поведения. Вы настолько самоуверенны в этом, что называете другие ответы неправильными, несмотря на то, что они даже не противоречат. Поскольку вы проецируете свое собственное поведение на других, я никогда не утверждал, что причиной было наследство . Я доказал, что это не так. Я продемонстрировал, что поведение противоречиво, поскольку Eclipse выбирает конкретный метод в одном сценарии, но не в другом, где существуют три перегрузки.
Хольгер
1
@howlger Кроме того, я уже назвал другой сценарий в конце этого комментария , создаю один интерфейс, без наследования, два метода, один defaultдругой abstract, с двумя аргументами, подобными потребителю, и пробую его. Затмение правильно говорит, что это неоднозначно, несмотря на то, что это defaultметод. Очевидно, наследование все еще имеет отношение к этой ошибке Eclipse, но в отличие от вас, я не схожу с ума и называю другие ответы неправильными, просто потому, что они не проанализировали ошибку полностью. Это не наша работа здесь.
Хольгер
1
@howlger нет, дело в том, что это ошибка. Большинство читателей даже не заботятся о деталях. Затмение может бросать кости каждый раз, когда выбирает метод, это не имеет значения. 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)
    }

}
howlger
источник
4
Вы пропустили предварительное условие прямо перед цитируемым предложением: « Если все максимально конкретные методы имеют сигнатуры, эквивалентные переопределению ». Конечно, два метода, аргументы которых являются полностью несвязанными интерфейсами, не имеют сигнатуры, эквивалентной переопределению. Кроме того, это не объясняет, почему 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 да, похоже, что выбор кандидата из переопределенных эквивалентных методов стал более сложным, и было бы интересно узнать обоснование этого, но это не имеет отношения к рассматриваемому вопросу. Должен быть другой документ, объясняющий изменения и мотивы. В прошлом была, но с этой политикой «новая версия каждые полтора года», сохранение качества кажется невозможным ...
Хольгер
2

Код, в котором Eclipse реализует JLS §15.12.2.5, не находит ни один метод более специфичным, чем другой, даже в случае явно лямбда с типизированным типом.

Так что в идеале «Затмение» остановится здесь и сообщит о двусмысленности. К сожалению, реализация разрешения перегрузки имеет нетривиальный код в дополнение к реализации JLS. Насколько я понимаю, этот код (который датируется временем, когда Java 5 была новой) должен быть сохранен, чтобы заполнить некоторые пробелы в JLS.

Я подал https://bugs.eclipse.org/562538, чтобы отследить это.

Независимо от этой конкретной ошибки, я могу только настоятельно рекомендовать против этого стиля кода. Перегрузка хороша для большого количества неожиданностей в Java, умноженная на вывод типа лямбда, сложность совершенно непропорциональна полученному усилению.

Стефан Херрманн
источник
Спасибо. Если бы вы уже зарегистрировали bugs.eclipse.org/bugs/show_bug.cgi?id=562507 , возможно, вы могли бы помочь связать их или закрыть как дубликат ...
ernest_k