Следующая тестовая программа выведена из более сложной программы, которая делает что-то полезное. Он успешно компилируется с компилятором Eclipse.
import java.util.ArrayList;
import java.util.List;
public class InferenceTest
{
public static void main(String[] args)
{
final List<Class<? extends Foo<?, ?>>> classes =
new ArrayList<Class<? extends Foo<?, ?>>>();
classes.add(Bar.class);
System.out.println(makeOne(classes));
}
private static Foo<?, ?> makeOne(Iterable<Class<? extends Foo<?, ?>>> classes)
{
for (final Class<? extends Foo<?, ?>> cls : classes)
{
final Foo<?, ?> foo = make(cls); // javac error here
if (foo != null)
return foo;
}
return null;
}
// helper used to capture wildcards as type variables
private static <A, B, C extends Foo<A, B>> Foo<A, B> make(Class<C> cls)
{
// assume that a real program actually references A and B
try
{
return cls.getConstructor().newInstance();
}
catch (final Exception e)
{
return null;
}
}
public static interface Foo<A, B> {}
public static class Bar implements Foo<Integer, Long> {}
}
Однако с Oracle JDK 1.7 javac он не справляется с этим:
InferenceTest.java:18: error: invalid inferred types for A,B; inferred type does not
conform to declared bound(s)
final Foo<?, ?> foo = make(cls);
^
inferred: CAP#1
bound(s): Foo<CAP#2,CAP#3>
where A,B,C are type-variables:
A extends Object declared in method <A,B,C>make(Class<C>)
B extends Object declared in method <A,B,C>make(Class<C>)
C extends Foo<A,B> declared in method <A,B,C>make(Class<C>)
where CAP#1,CAP#2,CAP#3 are fresh type-variables:
CAP#1 extends Foo<?,?> from capture of ? extends Foo<?,?>
CAP#2 extends Object from capture of ?
CAP#3 extends Object from capture of ?
1 error
Какой компилятор прав?
Одним из подозрительных аспектов вышеприведенного вывода является CAP#1 extends Foo<?,?>
. Я ожидал бы, что ограничения переменной типа будут CAP#1 extends Foo<CAP#2,CAP#3>
. Если бы это было так, то предполагаемая оценка CAP#1
соответствовала объявленным границам. Однако это может быть красная селедка, потому что C действительно следует понимать как CAP#1
, но сообщение об ошибке относится к A и B.
Обратите внимание: если я заменю строку 26 следующим образом, оба компилятора принимают программу:
private static <C extends Foo<?, ?>> Foo<?, ?> make(Class<C> cls)
Однако теперь я не могу ссылаться на захваченные типы параметров Foo
.
Обновление: Аналогично, принятое обоими компиляторами (но также и бесполезное):
private static <A, B, C extends Foo<? extends A, ? extends B>>
Foo<? extends A, ? extends B> make(Class<C> cls)
Это по существу приводит к тому, что A
и B
тривиально выводятся как Object
и поэтому явно не полезны ни в каком контексте. Тем не менее, это подтверждает мою теорию ниже, что javac
будет выполнять вывод только по границам подстановочных знаков, а не по границам захвата. Если у кого-то нет лучших идей, это может быть (неудачный) ответ. (End Update)
Я понимаю, что весь этот вопрос, скорее всего, TL; DR, но я продолжу, если кто-то еще столкнется с этой проблемой...
Основываясь на JLS 7, §15.12.2.7 Вывод аргументов типа на основе фактических аргументов, я сделал следующий анализ:
Учитывая ограничение формы
A << F
,A = F
илиA >> F
:
Изначально у нас есть одно ограничение формы A << F
, которое указывает, что тип A
можно преобразовать в тип F
путем преобразования вызова метода (§5.3). Здесь A
- Class<CAP#1 extends Foo<CAP#2, CAP#3>>
, а F
- Class<C extends Foo<A, B>>
. Обратите внимание, что другие формы ограничений (A = F
и A >> F
) возникают только при повторении алгоритма вывода.
Далее, C
следует определить как CAP#1
по следующим правилам:
(2.) В противном случае, если ограничение имеет вид
A << F
:
- Если
F
имеет видG<..., Yk-1, U, Yk+1, ...>
, гдеU
- выражение типа, которое включаетTj
, то еслиA
имеет супертип видаG<..., Xk-1, V, Xk+1, ...>
гдеV
- это выражение типа, этот алгоритм применяется рекурсивно к ограничениюV = U
.
Здесь G
- Class
, U
и Tj
- C
, а V
- CAP#1
. Рекурсивное приложение к CAP#1 = C
должно привести к ограничению C = CAP#1
:
(3.) В противном случае, если ограничение имеет вид
A = F
:
- Если
F = Tj
, то подразумевается ограничениеTj = A
.
До этого момента анализ, похоже, согласуется с выходом javac. Возможно, точка расхождения заключается в том, продолжать ли пытаться вывести A
и B
. Например, учитывая это правило
- Если
F
имеет видG<..., Yk-1, ? extends U, Yk+1, ...>
, гдеU
включаетTj
, то еслиA
имеет супертип, который является одним из:
G<..., Xk-1, V, Xk+1, ...>
, гдеV
является выражением типа.G<..., Xk-1, ? extends V, Xk+1, ...>
.Затем этот алгоритм применяется рекурсивно к ограничению
V << U
.
Если CAP#1
считается подстановочным знаком (который является захватом), то это правило применяется, и вывод продолжает рекурсивно с U
как Foo<A, B>
и V
как Foo<CAP#2, CAP#3>
. Как и выше, это даст A = CAP#2
и B = CAP#3
.
Однако, если CAP#1
является просто переменной типа, то ни одно из правил не рассматривает его границы. Возможно, эта концессия в конце раздела в спецификации относится к таким случаям:
Алгоритм вывода типа следует рассматривать как эвристику, предназначенную для выполнения на практике. Если он не сможет вывести желаемый результат, вместо него могут использоваться явные параметры типа.
Очевидно, подстановочные знаки нельзя использовать в качестве явных параметров типа.: - (