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

Я отвечал на вопрос и столкнулся со сценарием, который я не могу объяснить. Рассмотрим этот код:

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? Есть ли какое-то объяснение этому или ссылка на соответствующий раздел spe c?

Примечание: 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 ]

8 голосов
/ 22 апреля 2020

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

3 голосов
/ 28 апреля 2020

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

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

Я подал https://bugs.eclipse.org/562538 в отслеживать это.

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

2 голосов
/ 26 апреля 2020

Компилятор Eclipse правильно разрешает метод default , поскольку это наиболее специфичный c метод согласно спецификации языка Java 15.12. 2.5 :

Если конкретно один из максимально определенных c методов является конкретным (т. Е. Не abstract или по умолчанию), это наиболее конкретный c метод.

javac (используется по умолчанию Maven и IntelliJ) говорит, что вызов метода здесь неоднозначен. Но в соответствии с Java спецификацией языка это не является двусмысленным, поскольку один из двух методов является наиболее конкретным c здесь.

Неявно типизированные лямбда-выражения обрабатываются по-разному чем явно набранные лямбда-выражения в 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)
    }

}
...