Почему вызов типа Generic типа Java 8 выбирает эту перегрузку?
Рассмотрим следующую программу:
public class GenericTypeInference {
public static void main(String[] args) {
print(new SillyGenericWrapper().get());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(String string) {
System.out.println("String");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Он печатает "String" под Java 8 и "Object" в Java 7.
Я ожидал, что это будет двусмысленность в Java 8, потому что оба перегруженных метода совпадают. Почему компилятор выбирает print(String)
после JEP 101?
Обоснованный или нет, это нарушает обратную совместимость, и изменение не может быть обнаружено во время компиляции. Код просто скрытно ведет себя по-разному после перехода на Java 8.
ПРИМЕЧАНИЕ: SillyGenericWrapper
поистине назван "глупым". Я пытаюсь понять, почему компилятор ведет себя так, как он это делает, не говорите мне, что глупая обертка - это плохой дизайн в первую очередь.
UPDATE: я также попытался скомпилировать и запустить пример в Java 8, но с использованием уровня языка Java 7. Поведение соответствовало Java 7. Это ожидалось, но я все еще чувствовал необходимость проверки.
Ответы
Ответ 1
Правила вывода типа получили существенный пересмотр в Java 8; наиболее заметно, что вывод типа цели значительно улучшился. Итак, в то время как перед Java 8 сайт аргумента метода не получил никакого вывода, по умолчанию для Object, в Java 8 выводится наиболее специфический применимый тип, в данном случае String. В JLS для Java 8 была добавлена новая глава Глава 18. Типовой вывод, отсутствующий в JLS для Java 7.
В более ранних версиях JDK 1.8 (до 1.8.0_25) была ошибка, связанная с разрешением перегруженных методов, когда компилятор успешно скомпилировал код, который, согласно JLS, должен был вызвать ошибку неоднозначности Почему этот метод перегружает неоднозначно? Как отмечает Marco13 в комментариях
Эта часть JLS, вероятно, самая сложная
который объясняет ошибки в более ранних версиях JDK 1.8, а также проблему совместимости, которую вы видите.
Как показано в примере из Java Tutoral (Type Inference)
Рассмотрим следующий метод:
void processStringList(List<String> stringList) {
// process stringList
}
Предположим, вы хотите вызвать метод processStringList с пустым списком. В Java SE 7 следующий оператор не компилируется:
processStringList(Collections.emptyList());
Компилятор Java SE 7 генерирует сообщение об ошибке, подобное следующему:
List<Object> cannot be converted to List<String>
Для компилятора требуется значение для аргумента типа T, поэтому оно начинается со значения Object. Следовательно, вызов Collections.emptyList возвращает значение типа List, которое несовместимо с методом processStringList. Таким образом, в Java SE 7 вы должны указать значение значения аргумента типа следующим образом:
processStringList(Collections.<String>emptyList());
Это больше не требуется в Java SE 8. Понятие о том, что является типом назначения, было расширено, чтобы включить аргументы метода, такие как аргумент метода processStringList. В этом случае processStringList требует аргумент типа List
Collections.emptyList()
- общий метод, подобный методу get()
. В Java 7 метод print(String string)
даже не применим к вызову метода, поэтому он не участвует в процессе разрешения перегрузки. Если в Java 8 применяются оба метода.
Эта несовместимость стоит упомянуть в Руководстве по совместимости для JDK 8.
Вы можете проверить мой ответ на аналогичный вопрос, связанный с перегруженными методами. Метод перегружает неоднозначность с помощью трехмерных термальных условных и unboxed-примитивов Java 8
Согласно JLS 15.12.2.5 Выбор наиболее конкретного метода:
Если более чем один метод-член доступен и применим к необходимо вызвать один из них, чтобы обеспечить дескриптор для отправки времени выполнения. Программирование на Java язык использует правило, в котором выбран наиболее специфический метод.
Тогда:
Один применимый метод m1 более конкретный, чем другой применимый метод m2, для вызова с выражениями аргументов e1,..., ek, if любое из следующего верно:
-
m2 является общим, а m1 считается более конкретным, чем m2 для выражения аргументов e1,..., ek по §18.5.4.
-
m2 не является общим, а m1 и m2 применимы строгими или свободными вызов и где m1 имеет формальные типы параметров S1,..., Sn и m2 имеет формальные типы параметров T1,..., Tn, тип Si более специфичен чем Ti для аргумента ei для всех я (1 ≤ я ≤ n, n = k).
-
m2 не является общим, а m1 и m2 применимы переменной arity invocation и где первые k переменных параметров arity-типов m1 представляют собой S1,..., Sk и первые k переменных параметров параметров arty m2 T1,..., Tk, тип Si более специфичен, чем Ti для аргумента ei для всех я (1 ≤ я ≤ k). Кроме того, если m2 имеет k + 1 параметров, то k + 1-й тип параметра переменной arity m1 является подтипом k + 1'-тип переменной переменной arity m2.
Вышеуказанные условия являются единственными обстоятельствами, при которых один метод может быть более конкретным, чем другой.
Тип S более специфичен, чем тип T для любого выражения, если S <: T (§ 4.10).
Второй из трех вариантов соответствует нашему делу. Так как String
является подтипом Object
(String <: Object
), он более конкретный. Таким образом, сам метод более конкретный. После JLS этот метод также строго конкретнее и наиболее специфичен и выбран компилятором.
Ответ 2
В java7 выражения интерпретируются снизу вверх (за очень немногими исключениями); значение подвыражения является своего рода "контекстом свободным". Для вызова метода типы аргументов разрешаются кулаком; компилятор затем использует эту информацию для разрешения значения вызова, например, чтобы выбрать победителя среди применимых перегруженных методов.
В java8 эта философия больше не работает, потому что мы ожидаем использовать неявную лямбду (например, x->foo(x)
) всюду; типы параметров лямбда не указаны и должны быть выведены из контекста. Это означает, что для вызовов метода иногда типы параметров метода определяют типы аргументов.
Очевидно, существует дилемма, если метод перегружен. Поэтому в некоторых случаях перед компиляцией аргументов необходимо сначала разрешить перегрузку метода, чтобы выбрать один победитель.
Это серьезный сдвиг; и какой-то старый код, подобный вашему, станет жертвой несовместимости.
Обходной путь заключается в предоставлении "целевого ввода" аргумента с "контекстом каста"
print( (Object)new SillyGenericWrapper().get() );
или как предложение @Holger, укажите параметр типа <Object>get()
, чтобы избежать вывода всех вместе.
Перегрузка Java-метода чрезвычайно сложна; польза от сложности сомнительна. Помните, что перегрузка никогда не является необходимостью - если они разные методы, вы можете дать им разные имена.
Ответ 3
Прежде всего, это не имеет никакого отношения к переопределению, но ему приходится иметь дело с перегрузкой.
Jls,. Раздел 15 содержит много информации о том, как именно компилятор выбирает перегруженный метод
Наиболее специфический метод выбирается во время компиляции; его дескриптор определяет, какой метод фактически выполняется во время выполнения.
Итак, при вызове
print(new SillyGenericWrapper().get());
Компилятор выбирает String
версию поверх Object
, потому что метод print
, который принимает String
, более конкретный, чем тот, который принимает Object
. Если вместо String
было Integer
, то оно будет выбрано.
Кроме того, если вы хотите вызывать метод, который принимает Object
в качестве параметра, вы можете назначить возвращаемое значение параметру типа Object
E.g.
public class GenericTypeInference {
public static void main(String[] args) {
final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
final Object o = sillyGenericWrapper.get();
print(o);
print(sillyGenericWrapper.get());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(Integer integer) {
System.out.println("Integer");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Он выводит
Object
Integer
Ситуация начинает становиться интересной, если вы говорите, что у вас есть 2 действительных определения метода, которые могут быть перегружены. Например.
private static void print(Integer integer) {
System.out.println("Integer");
}
private static void print(String integer) {
System.out.println("String");
}
и теперь, если вы вызываете
print(sillyGenericWrapper.get());
У компилятора будет 2 допустимых метода определения, поэтому вы получите ошибку компиляции, потому что он не может отдать предпочтение одному методу над другим.
Ответ 4
Я запустил его с помощью Java 1.8.0_40 и получил "Object".
Если вы запустите следующий код:
public class GenericTypeInference {
private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {
print(new SillyGenericWrapper().get());
Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
for (Method m : allMethods) {
System.out.format("%s%n", m.toGenericString());
System.out.format(fmt, "ReturnType", m.getReturnType());
System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());
}
private static void print(Object object) {
System.out.println("Object");
}
private static void print(String string) {
System.out.println("String");
}
public static class SillyGenericWrapper {
public <T> T get() {
return null;
}
}
}
Вы увидите, что вы получаете:
Объект public T com.xxx.GenericTypeInference $SillyGenericWrapper.get() ReturnType: класс java.lang.Object GenericReturnType: T
Это объясняет, почему используется метод, перегруженный объектом, а не строковый.