【发布时间】:2013-07-04 02:22:49
【问题描述】:
下面的测试程序源自一个更复杂的程序,它做了一些有用的事情。用 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 只会对通配符边界进行推断,而不是捕获边界。如果没有人有更好的想法,这可能是(不幸的)答案。 (结束更新)
我意识到这整个问题很可能是 TL;DR,但我会继续,以防其他人遇到这个问题......
基于JLS 7,§15.12.2.7 Inferring Type Arguments Based on Actual Arguments,我做了如下分析:
给定
A << F、A = F或A >> F形式的约束:
最初,我们有一个A << F 形式的约束,它表示A 类型可以通过方法调用转换(§5.3) 转换为F 类型。这里,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 只是一个类型变量,那么似乎没有任何规则考虑它的界限。也许规范部分末尾的这个让步是指这种情况:
类型推断算法应被视为一种启发式算法,旨在在实践中表现良好。如果它无法推断出所需的结果,则可以使用显式类型参数来代替。
显然,通配符不能用作显式类型参数。 :-(
【问题讨论】:
-
您使用的是哪个版本的
javac?javac -version -
@Jeffrey:比 1.7 更具体?
javac 1.7.0_25 -
@BevynQ:是的,我的问题是那个示例“解决方法”。但是,它没有用,因为
make的目的是捕获通配符,例如用于在进一步的方法调用中表示相同类型的多个实例或使用这些类型参数创建其他对象实例。该问题与将结果分配给Foo<?,?>无关;该部分适用于任一编译器。 -
FWIW,IntelliJ 也成功编译了您的示例。
-
@BevynQ:感谢您提供帮助。我只是澄清一下a)您提到的解决方法是逐字逐句地在原始问题中,所以我不明白它添加了什么信息,并且b)编译器错误出现在方法调用中,而不是它拒绝分配结果。如果您删除所有提及
foo(即没有结果变量、没有赋值、没有if、没有return),错误仍然会发生。