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

Java 8 неоднозначная ссылка метода для универсального класса

Код ниже компилируется и работает нормально в Java 7, но не скомпилирован в Java 1.8.0 u25:

public class GenericTest {

    public static class GenericClass<T> {
        T value;

        public GenericClass(T value) {
            this.value = value;
        }
    }

    public static class SecondGenericClass<T> {
        T value;

        public SecondGenericClass(T value) {
            this.value = value;
        }
    }


    public static<T >void verifyThat(SecondGenericClass<T> actual, GenericClass<T> matcher) {
    }

    public static<T >void verifyThat(T actual, GenericClass<T> matcher) {
    }

    @Test
    public void testName() throws Exception {
        verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));
    }

}

Сообщение об ошибке в Java 8 выглядит так:

Error:(33, 9) java: reference to verifyThat is ambiguous
  both method <T>verifyThat(com.sabre.ssse.core.dsl.GenericTest.SecondGenericClass<T>,com.sabre.ssse.core.dsl.GenericTest.GenericClass<T>) in com.sabre.ssse.core.dsl.GenericTest and method <T>verifyThat(T,com.sabre.ssse.core.dsl.GenericTest.GenericClass<T>) in com.sabre.ssse.core.dsl.GenericTest match

Я просмотрел все изменения между:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12.2
https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.12.2

Но я не заметил точную причину такого поведения.

Изменить:

Чтобы ответить на некоторые комментарии, совершенно ясно, что компилятор как в Java 7, так и 8 сможет обрабатывать такие вызовы (с сигнатурами, аналогичными тем, что осталось после стирания стилей времени компиляции:

public static void verifyThat(SecondGenericClass actual, GenericClass matcher) {
}

public static void verifyThat(Object actual, GenericClass matcher) {
}

@Test
public void testName() throws Exception {
    verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));
}

Байт-код, сгенерированный для обоих общих методов и стираемый, является таким же и выглядит следующим образом:

public static verifyThat(Lcom/sabre/ssse/core/dsl/GenericTest$SecondGenericClass;Lcom/sabre/ssse/core/dsl/GenericTest$GenericClass;)V
public static verifyThat(Ljava/lang/Object;Lcom/sabre/ssse/core/dsl/GenericTest$GenericClass;)V

Edit2:

Компиляция под javac 1.8.0_40 не выполняется с той же ошибкой

4b9b3361

Ответ 1

JLS, глава §15.12.2.5 Выбор наиболее конкретного метода - это трудное чтение, но содержит интересное резюме:

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

Мы можем легко опровергнуть это для вашего случая в следующем примере:

GenericTest.<String>verifyThat( // invokes the first method
    new SecondGenericClass<>(""), new GenericClass<>(""));
GenericTest.<SecondGenericClass<String>>verifyThat( // invokes the second
    new SecondGenericClass<>(""), new GenericClass<>(null));

поэтому здесь нет более конкретного метода, однако, как показывает пример, можно вызвать любой метод с использованием аргументов, которые делают другой метод неприменимым.

В Java 7 было проще сделать метод неприменимым из-за ограниченных попыток (компилятора) найти аргументы типа, чтобы применить больше методов (например, ограничение по типу). Выражение new SecondGenericClass<>("") имело тип SecondGenericClass<String>, выведенный из его аргумента "", и это оно. Таким образом, для вызова verifyThat(new SecondGenericClass<>(""), new GenericClass<>("")) аргументы имели тип SecondGenericClass<String> и GenericClass<String>, что делало метод <T> void verifyThat(T,GenericClass<T>) неприменимым.

Обратите внимание, что есть пример неоднозначного вызова, который проявляет двусмысленность в Java 7 (и даже Java 6): verifyThat(null, null); спровоцирует ошибку компилятора при использовании javac.

Но у Java 8 есть Вывод применимости Invocation (там есть разница с JLS 7, совершенно новая глава...), которая позволяет компилятору выбирать аргументы типа, которые делают применимый кандидат метода (который работает через вложенные вызовы). Вы можете найти такие аргументы типа для вашего частного случая, вы даже можете найти аргумент типа, который подходит для обоих,

GenericTest.<Object>verifyThat(new SecondGenericClass<>(""), new GenericClass<>(""));

однозначно неоднозначен (в Java 8), даже Eclipse соглашается на это. Напротив, вызов

verifyThat(new SecondGenericClass<>(""), new GenericClass<String>(""));

достаточно специфичен, чтобы сделать второй метод неприменимым и вызвать первый метод, который дает нам подсказку о том, что происходит в Java 7, где тип new GenericClass<>("") фиксируется как GenericClass<String>, как и при использовании new GenericClass<String>("").


Суть в том, что это не выбор наиболее конкретного метода, который изменился с Java 7 на Java 8 (значительно), но применимость из-за улучшенного вывода типа. Как только оба метода применимы, вызов является неоднозначным, поскольку ни один из методов не является более конкретным, чем другой.

Ответ 2

При разрешении того, какой метод использовать в случае применения нескольких методов, "... типы аргументов вызова не могут, в общем, быть входом в анализ." Java 7 Spec не хватает этой квалификации.

Если вы замените T во втором определении verifyThat для SecondGenericClass, подписи совпадут.

Другими словами, представьте себе попытку вызвать второе определение verifyThat следующим образом:

SecondGenericClass<String> t = new SecondGenericClass<String>("foo");
GenericTest.verifyThat(t, new GenericClass<String>("bar"));

Во время выполнения не было способа определить, какую версию verifyThat вызывать, поскольку тип переменной T является допустимой заменой для SecondGenericClass<T> и T.

Обратите внимание, что если Java проверила дженерики (и это будет когда-нибудь), в этом примере одна сигнатура метода не более конкретна, чем другая. Закрытие лазеек...