Ссылка неоднозначна с дженериками

У меня довольно сложный случай с дженериками и перегрузкой методов. Посмотрите этот примерный класс:

public class Test {
    public <T> void setValue(Parameter<T> parameter, T value) {
    }

    public <T> void setValue(Parameter<T> parameter, Field<T> value) {
    }

    public void test() {
        // This works perfectly. <T> is bound to String
        // ambiguity between setValue(.., String) and setValue(.., Field)
        // is impossible as String and Field are incompatible
        Parameter<String> p1 = getP1();
        Field<String> f1 = getF1();
        setValue(p1, f1);

        // This causes issues. <T> is bound to Object
        // ambiguity between setValue(.., Object) and setValue(.., Field)
        // is possible as Object and Field are compatible
        Parameter<Object> p2 = getP2();
        Field<Object> f2 = getF2();
        setValue(p2, f2);
    }

    private Parameter<String> getP1() {...}
    private Parameter<Object> getP2() {...}

    private Field<String> getF1() {...}
    private Field<Object> getF2() {...}
}

Приведенный выше пример отлично компилируется в Eclipse (Java 1.6), но не с помощью команды Ant javac (или с помощью команды javac JDK), где я получаю это сообщение об ошибке при втором вызове setValue:

ссылка на setValue неоднозначна, оба метода SetValue (org.jooq.Parameter, Т) в тесте и методе SetValue (org.jooq.Parameter, org.jooq.Field) в тестовом матче

В соответствии со спецификацией и моим пониманием того, как работает Java-компилятор, всегда следует выбирать наиболее специфический метод: http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#20448

В любом случае, даже если <T> привязан к Object, что делает приемлемыми кандидаты для вызова setValue приемлемыми кандидатами для вызова, параметр с параметром Field всегда кажется более конкретным. И он работает в Eclipse, только не с компилятором JDK.

UPDATE

Подобным образом, он будет работать как в Eclipse, так и с помощью JDK-компилятора (конечно, с предупреждениями типа rawtypes). Я понимаю, что правила, указанные в спецификации, весьма специфичны, когда задействованы дженерики. Но я нахожу это довольно запутанным:

    public <T> void setValue(Parameter<T> parameter, Object value) {
    }

    // Here, it easy to see that this method is more specific
    public <T> void setValue(Parameter<T> parameter, Field value) {
    }

ОБНОВЛЕНИЕ 2:

Даже с generics, я могу создать это обходное решение, где я избегаю привязки типа <T> к Object при времени setValue, добавив дополнительную однозначную косвенность, называемую setValue0. Это заставляет меня думать, что привязка T к Object действительно вызывает все проблемы здесь:

    public <T> void setValue(Parameter<T> parameter, T value) {
    }

    public <T> void setValue(Parameter<T> parameter, Field<T> value) {
    }

    public <T> void setValue0(Parameter<T> parameter, Field<T> value) {
        // This call wasn't ambiguous in Java 7
        // It is now ambiguous in Java 8!
        setValue(parameter, value);
    }

    public void test() {
        Parameter<Object> p2 = p2();
        Field<Object> f2 = f2();
        setValue0(p2, f2);
    }

Я что-то не понимаю? Есть ли известная ошибка компилятора, связанная с этим? Или есть способ обхода/компиляции, чтобы помочь мне?

Follow-Up:

Для тех, кто заинтересован, я подал отчет об ошибках как в Oracle, так и в Eclipse. Oracle приняла ошибку, пока Eclipse проанализировал ее и отклонил! Похоже, что моя интуиция правильная, и это ошибка в javac

Ответ 1

JDK прав. Второй метод не более конкретный, чем 1-й. Из JLS3 # 15.12.2.5

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

Это явно не так. Я подчеркнул любой вызов. Свойство одного метода более специфично, чем другое, чисто зависит от самих двух методов; он не изменяется для каждого вызова.

Формальный анализ по вашей проблеме: m2 более конкретный, чем m1?

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value) 

Во-первых, компилятор должен вывести R из начальных ограничений:

Parameter<V>   <<   Parameter<R>
Field<V>       <<   R

Результатом является R=V, за правила вывода в 15.12.2.7

Теперь подставим R и проверим отношения подтипа

Parameter<V>   <:   Parameter<V>
Field<V>       <:   V

Вторая строка не выполняется в соответствии с правилами подтипирования в 4.10.2. Таким образом, m2 не является более специфичным, чем m1.

V не является Object в этом анализе; анализ рассматривает все возможные значения V.

Я бы предложил использовать разные имена методов. Перегрузка никогда не нужна.


Это, по-видимому, значительная ошибка в Eclipse. Спектр довольно четко указывает, что переменные типа не заменяются на этом шаге. Eclipse, по-видимому, сначала вводит замену переменных, а затем проверяет соответствие метода.

Если такое поведение более "разумно" в некоторых примерах, это не в других примерах. Скажем,

m1: <T extends Object> void check(List<T> list, T obj) { print("1"); }
m2: <T extends Number> void check(List<T> list, T num) { print("2"); }

void test()
    check( new ArrayList<Integer>(), new Integer(0) );

"Интуитивно" и формально для spec, m2 более специфично, чем m1, а тестовая печать "2". Однако, если первая замена T=Integer выполняется, оба метода становятся идентичными!


для обновления 2

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value) 

m3: <T> void setValue2(Parameter<T> parameter, Field<T> value)
s4:             setValue(parameter, value)

Здесь m1 не применимо для вызова метода s4, поэтому m2 является единственным выбором.

В соответствии с 15.12.2.2, чтобы убедиться, что m1 применимо для s4, сначала выполняется вывод типа, чтобы сделать вывод, что R = T; то мы проверим Ai :< Si, что приводит к Field<T> <: T, что неверно.

Это согласуется с предыдущим анализом - если m1 применимо к s4, то любое обращение, обрабатываемое m2 (по существу, такое же, как s4), может обрабатываться m1, что означает, что m2 будет более специфичным, чем m1, что является ложным.

в параметризованном типе

Рассмотрим следующий код

class PF<T>
{
    public void setValue(Parameter<T> parameter, T value) {
    }

    public void setValue(Parameter<T> parameter, Field<T> value) {
    }
}

void test()

    PF<Object> pf2 = null;

    Parameter<Object> p2 = getP2();
    Field<Object> f2 = getF2();

    pf2.setValue(p2,f2);

Это компилируется без проблем. На 4.5.2 типы методов из PF<Object> являются методами в PF<T> с подстановкой T=Object. То есть методы pf2 являются

    public void setValue(Parameter<Object> parameter, Object value) 

    public void setValue(Parameter<Object> parameter, Field<Object> value) 

Второй метод более конкретный, чем первый.

Ответ 2

Мое предположение заключается в том, что компилятор выполняет метод перегрузки разрешения по JLS, раздел 15.12.2.5.

В этом разделе компилятор использует сильный подтипирование (таким образом, не допускается какое-либо непроверенное преобразование), поэтому T value становится Object value, а Field<T> value становится Field<Object> value. Применяются следующие правила:

Метод m применим подтипирование тогда и только тогда, когда обе выполняются следующие условия:

* For 1in, either:
      o Ai is a subtype (§4.10) of Si (Ai <: Si) or
      o Ai is convertible to some type *Ci* by unchecked conversion

(§5.1.9) и Ci <: Si.     * Если m - общий метод, как описано выше, то Ul <: Bl [R1 = U1,..., Rp = Up], 1lp.

(См. bullet 2). Поскольку Field<Object> является подтипом Object, то найден наиболее конкретный метод. Поле f2 соответствует вашим методам (из-за вышеприведенной маркировки 2) и делает его неоднозначным.

Для String и Field<String> между ними не существует отношения подтипа.

PS. Это мое понимание вещей, не цитируйте его как кошерное.

Ответ 3

Изменить. Этот ответ неверен. Взгляните на принятый ответ.

Я думаю, что проблема сводится к следующему: компилятор не видит тип f2 (т.е. поле) и предполагаемый тип формального параметра (то есть Field → Field) как один и тот же тип.

Другими словами, похоже, что тип f2 (Field) считается подтипом типа формального параметра Field (Field). Поскольку поле того же типа является подтипом Object, компилятор не может выбрать один метод над другим.

Изменить. Позвольте мне немного расширить мой оператор.

Оба метода применимы, и это выглядит как Фаза 1: Идентифицировать Способы сопоставления Arity, применяемые подтипами, используются для определения того, какой метод вызывать, а не правила из Выбор наиболее конкретного метода, но не удалось по какой-то причине выбрать второй метод по сравнению с первым.

Раздел Phase 1 использует это обозначение: X <: S (X является подтипом S). Основываясь на моем понимании <:, X <: X является допустимым выражением, т.е. <: не является строгим и включает сам тип (X - подтип X) в этом контексте. Это объясняет результат фазы 1: оба метода выбраны в качестве кандидатов, так как Field<Object> <: Object и Field<Object> <: Field<Object>.

В разделе "Выбор наиболее специфического метода" используются те же обозначения, что один метод более конкретный, чем другой. Интересная часть абзаца, начинающаяся с "Один метод элемента фиксированной arty с именем m более конкретный, чем другой член...". Он имеет, помимо прочего:

Для всех j от 1 до n, Tj <: Sj.

Это заставляет меня думать, что в нашем случае второй метод должен быть выбран первым, потому что выполняется следующее:

  • Parameter<Object> <: Parameter<Object>
  • Field<Object> <: Object

в то время как другой путь не выполняется из-за того, что Object <: Field<Object> является ложным (объект не является подтипом поля).

Примечание. В случае примеров String Phase 1 просто выбирает единственный применимый метод: второй.

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