2014-02-20 6 views
36

Я играю с 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, который не является функциональным интерфейсом. Почему он не может решить правильный метод?

+6

Я бы предположил, что ответ таков, что подтип T of B может реализовать A. –

+0

@ user2580516 Да, это может быть проблемой, я не думал об этой возможности. – schenka7

ответ

43

Существует много сложности на пересечении разрешения перегрузки и вывода типа. В спецификации current draft спецификации лямбда есть все детали. Разделы 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.) Поэтому для конструкций, таких как неявные лямбды, которые требуют вывода, кажется разумным отказаться от чего-то в перегрузке, чтобы увеличить диапазон случаев, когда могут использоваться неявные лямбды ,

- Brian Goetz, Lambda Expert Group, 9 Aug 2013

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

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

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

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

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

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

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

+1

Благодарим вас за подробное объяснение. Могу ли я суммировать следующее: Оценки аргументов типа общих методов не учитываются при разрешении перегрузки при передаче неявного типизированного лямбда-выражения в качестве аргумента? – schenka7

+1

Я думаю, что это справедливо. Чтобы немного расширить его, имеется много информации, которая не учитывается при разрешении перегрузки. –

4

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

+0

Поскольку @StuartMarks прокомментировал подтип B, который реализует A, не является функциональным интерфейсом. Компилятор сообщает об ошибке при попытке передать лямбда методу, параметризованному таким типом. – schenka7

1

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

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

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

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

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

Я подал отчет об ошибке: JDK-8046045

Смежные вопросы