Переменная вывода имеет несовместимые границы. Регрессия компилятора Java 8?
Следующая программа компилируется в Java 7 и в Eclipse Mars RC2 для Java 8:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
b(newList(type));
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Используя компилятор javac 1.8.0_45, сообщается следующая ошибка компиляции:
Test.java:6: error: method b in class Test cannot be applied to given types;
b(newList(type));
^
required: List<T>
found: CAP#1
reason: inference variable L has incompatible bounds
equality constraints: CAP#2
upper bounds: List<CAP#3>,List<?>
where T,L are type-variables:
T extends Object declared in method <T>b(List<T>)
L extends List<?> declared in method <L>newList(Class<L>)
where CAP#1,CAP#2,CAP#3 are fresh type-variables:
CAP#1 extends List<?> from capture of ? extends List<?>
CAP#2 extends List<?> from capture of ? extends List<?>
CAP#3 extends Object from capture of ?
Обходной путь заключается в локальном назначении переменной:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
// Workaround here
List<?> variable = newList(type);
b(variable);
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Я знаю, что вывод типа сильно изменился в Java 8 (например, из-за JEP 101 "обобщенный вывод целевого типа" ). Итак, это ошибка или новая "особенность" языка?
EDIT. Я также сообщил об этом Oracle как JI-9021550, но на всякий случай это "функция" в Java 8, я также сообщил об ошибке Eclipse:
Ответы
Ответ 1
Спасибо за отчет об ошибках, и спасибо, Хольгер, за пример в вашем ответе. Эти и некоторые другие, наконец, заставили меня задать одно небольшое изменение, сделанное в компиляторе Eclipse 11 лет назад. Дело в том, что Eclipse незаконно расширила алгоритм захвата, чтобы применить рекурсивно к границам подстановочных знаков.
Был один пример, когда это незаконное изменение полностью выровняло поведение Eclipse с javac. Поколения разработчиков Eclipse доверяли этому старому решению больше, чем то, что мы могли видеть в JLS. Сегодня я считаю, что предыдущее отклонение должно было иметь другую причину.
Сегодня я взял на себя смелость выровнять ecj с JLS в этом отношении, и вуаля 5 ошибок, которые, как оказалось, чрезвычайно трудно взломать, по существу были решены именно так (плюс небольшая настройка здесь и там в качестве компенсации).
Ergo: Да, у Eclipse была ошибка, но эта ошибка была исправлена с 4.7 вехой 2:)
Здесь какой ecj будет отчитываться отныне:
The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)
Это подстановочный знак внутри привязки, который не находит правила для обнаружения совместимости. Точнее, некоторое время во время вывода (точнее, включение) мы сталкиваемся со следующим ограничением (T # 0, представляющим переменную вывода):
⟨T#0 = ?⟩
Наивно, мы могли бы просто разрешить переменную типа для подстановочного знака, но - предположительно, потому что подстановочные знаки не считаются типами - правила сокращения определяют выше, как сокращение до FALSE, что позволяет сделать вывод недействительным.
Ответ 2
Отказ от ответственности - я не знаю достаточно о предмете, и следующее - неофициальное рассуждение, чтобы попытаться оправдать поведение javac.
Мы можем свести задачу к
<X extends List<?>> void a(Class<X> type) throws Exception
{
X instance = type.newInstance();
b(instance); // error
}
<T> List<T> b(List<T> list) { ... }
Чтобы сделать вывод T
, мы имеем ограничения
X <: List<?>
X <: List<T>
По существу, это неразрешимо. Например, no T
существует, если X=List<?>
.
Не уверен, как Java7 это может случиться. Но javac8 (и IntelliJ) ведет себя "разумно", я бы сказал.
Теперь, как это работает?
List<?> instance = type.newInstance();
b(instance); // ok!
Он работает благодаря подстановочному захвату, который вводит больше информации о типе, "сужая" тип instance
instance is List<?> => exist W, where instance is List<W> => T=W
К сожалению, это не делается, когда instance
есть X
, таким образом, существует меньше информации о типе, с которой можно работать.
Предположительно, язык может быть "улучшен", чтобы делать подстановочный знак для X тоже:
instance is X, X is List<?> => exist W, where instance is List<W>
Ответ 3
Благодаря ответу bayou.ios мы можем сузить проблему до того, что
<X extends List<?>> void a(X instance) {
b(instance); // error
}
static final <T> List<T> b(List<T> list) {
return list;
}
вызывает ошибку, когда
<X extends List<?>> void a(X instance) {
List<?> instance2=instance;
b(instance2);
}
static final <T> List<T> b(List<T> list) {
return list;
}
может быть скомпилирован без проблем. Назначение instance2=instance
- это расширяющееся преобразование, которое также должно происходить для аргументов вызова метода. Таким образом, разница в шаблоне этого ответа является дополнительным отношением подтипа.
Обратите внимание, что пока я не уверен, соответствует ли этот конкретный случай спецификации языка Java, некоторые тесты показали, что Eclipse, принимающий код, вероятно, связано с тем, что он более неряшлив в отношении общих типов в целом, поскольку следующее, определенно некорректный, код может быть скомпилирован без какой-либо ошибки или предупреждения:
public static void main(String... arg) {
List<Integer> l1=Arrays.asList(0, 1, 2);
List<String> l2=Arrays.asList("0", "1", "2");
a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
L l1=type.get(0), l2=type.get(1);
l2.set(0, l1.get(0));
}