Подтвердить что ты не робот

Java8: двусмысленность с lambdas и перегруженные методы

Я играю с java8 lambdas, и я наткнулся на ошибку компилятора, которую я не ожидал.

Скажем, у меня есть функциональные interface A, abstract class B и class C с перегруженными методами, которые принимают либо A, либо B в качестве аргументов:

public interface A { 
  void invoke(String arg); 
}

public abstract class B { 
  public abstract void invoke(String arg); 
}

public class C {
  public void apply(A x) { }    
  public B apply(B x) { return x; }
}

Затем я могу передать лямбда в c.apply и правильно разрешен c.apply(A).

C c = new C();
c.apply(x -> System.out.println(x));

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

public class C {
  public void apply(A x) { }    
  public <T extends B> T apply(T x) { return x; }
}

Я думал, что компилятор увидит, что T должен быть подклассом B, который не является функциональным интерфейсом. Почему он не может решить правильный метод?

4b9b3361

Ответ 1

На пересечении разрешения перегрузки и вывода типа существует много сложностей. текущий проект спецификации лямбда имеет все детали. Разделы F и G разрешают перегрузку и вывод типа соответственно. Я не притворяюсь, что все это понимаю. Сводные разделы во введении достаточно понятны, и я рекомендую людям читать их, особенно резюме разделов F и G, чтобы получить представление о том, что происходит в этой области.

Чтобы кратко рассмотреть проблемы, рассмотрите вызов метода с некоторыми аргументами при наличии перегруженных методов. Разрешение перегрузки должно выбрать правильный метод для вызова. "Форма" метода (арность или количество аргументов) наиболее значительна; очевидно, вызов метода с одним аргументом не может решить метод, который принимает два параметра. Но перегруженные методы часто имеют одинаковое количество параметров разных типов. В этом случае типы начинают иметь значение.

Предположим, что есть два перегруженных метода:

    void foo(int i);
    void foo(String s);

и некоторый код имеет следующий вызов метода:

    foo("hello");

Очевидно, что это разрешает второй метод, основанный на типе передаваемого аргумента. Но что, если мы делаем разрешение перегрузки, а аргумент - лямбда? (Особенно тот, чьи типы неявны, который полагается на вывод типа для установления типов.) Напомним, что тип выражения лямбда выведен из целевого типа, то есть типа, ожидаемого в этом контексте. К сожалению, если у нас есть перегруженные методы, у нас нет целевого типа, пока мы не разрешим перегруженный метод, который мы будем называть. Но поскольку у нас еще нет типа для выражения лямбда, мы не можем использовать его тип, чтобы помочь нам во время разрешения перегрузки.

Посмотрим на пример здесь. Рассмотрим интерфейс A и абстрактный класс B, как определено в примере. У нас есть класс C, который содержит две перегрузки, а затем некоторый код вызывает метод apply и передает ему лямбда:

    public void apply(A a)    
    public B apply(B b)

    c.apply(x -> System.out.println(x));

Обе перегрузки apply имеют одинаковое количество параметров. Аргументом является лямбда, которая должна соответствовать функциональному интерфейсу. A и B являются действительными типами, поэтому он показывает, что A является функциональным интерфейсом, тогда как B не является, поэтому результатом разрешения перегрузки является apply(A). В этот момент мы теперь имеем целевой тип A для лямбда, а вывод типа для x продолжается.

Теперь вариация:

    public void apply(A a)    
    public <T extends B> T apply(T t)

    c.apply(x -> System.out.println(x));

Вместо фактического типа вторая перегрузка apply является переменной общего типа T. Мы не сделали вывода типа, поэтому не учитываем T, по крайней мере, до тех пор, пока не будет завершено разрешение перегрузки. Таким образом, обе перегрузки по-прежнему применимы, и не являются наиболее конкретными, а компилятор испускает ошибку, вызывающую неоднозначность.

Вы можете утверждать, что, поскольку мы знаем, что T имеет ограничение типа B, которое является классом, а не функциональным интерфейсом, лямбда не может применяться к этой перегрузке, поэтому она должна быть правильной во время разрешения перегрузки, устраняя двусмысленность. Я не из тех, у кого есть этот аргумент.:-) Это может быть ошибка в компиляторе или, возможно, даже в спецификации.

Я знаю, что эта область прошла через кучу изменений во время разработки Java 8. Ранее варианты пытались принести больше информации о проверке типов и выводах в фазу разрешения перегрузки, но их было сложнее реализовать, указать и Понимаю. (Да, еще труднее понять, чем сейчас). К сожалению, проблемы продолжали возникать. Было решено упростить ситуацию, уменьшив диапазон вещей, которые могут быть перегружены.

Тип вывода и перегрузки всегда находятся в оппозиции; многие языки с типом вывода с первого дня запрещают перегрузку (за исключением, может быть, arity.) Поэтому для конструкций, таких как неявные лямбды, которые требуют вывода, кажется разумным отказаться от чего-то в перегрузке, чтобы увеличить диапазон случаев, когда могут использоваться неявные лямбды.

- Брайан Гетц, экспертная группа Lambda, 9 августа 2013 г.

(Это было довольно спорное решение. Обратите внимание, что было 116 сообщений в этой теме, и есть несколько других потоков, которые обсуждают этот вопрос.)

Одним из последствий этого решения было изменение некоторых API, чтобы избежать перегрузки, например API компаратора. Ранее метод Comparator.comparing имел четыре перегрузки:

    comparing(Function)
    comparing(ToDoubleFunction)
    comparing(ToIntFunction)
    comparing(ToLongFunction)

Проблема заключалась в том, что эти перегрузки дифференцируются только по типу возврата лямбда, и мы на самом деле никогда не получали вывод типа для работы здесь с неявным типом lambdas. Чтобы использовать их, всегда нужно было бы указать или предоставить аргумент явного типа для лямбда. Эти API впоследствии были изменены на:

    comparing(Function)
    comparingDouble(ToDoubleFunction)
    comparingInt(ToIntFunction)
    comparingLong(ToLongFunction)

который несколько неуклюжий, но он абсолютно недвусмыслен. Аналогичная ситуация наблюдается с Stream.map, mapToDouble, mapToInt и mapToLong, а также в нескольких других местах вокруг API.

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

Ответ 2

Я считаю, что ответ заключается в том, что подтип T of B может реализовать A, тем самым делая его неоднозначным, для которого функция отправляет аргумент такого типа T.

Ответ 3

Я думаю, что этот тестовый пример раскрывает ситуацию, в которой компилятор javac 8 мог бы сделать больше, чтобы попытаться отказаться от неприменимого кандидата перегрузки, второй метод в:

public class C {
    public void apply(A x) { }    
    public <T extends B> T apply(T x) { return x; }
}

Исходя из того факта, что T никогда не может быть создан для функционального интерфейса. Этот случай очень интересный. @schenka7 спасибо за это. Я буду исследовать плюсы и минусы такого предложения.

В настоящее время основным аргументом против реализации этого может быть то, насколько часто этот код может быть. Я думаю, что когда люди начнут конвертировать текущий Java-код в Java 8, возможности найти этот шаблон могут быть выше.

Еще одно соображение состоит в том, что, если мы начнем добавлять специальные случаи в spec/compiler, это может стать более сложным для понимания, объяснения и поддержки.

Я зарегистрировал этот отчет об ошибке: JDK-8046045