Ответ 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 избегают перегруженных методов, в которых предполагается использовать неявно типизированные лямбда.