我想更好地理解当Java编译器遇到如下方法的调用时会发生什么。
<T extends AutoCloseable & Cloneable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
我很清楚在运行时没有类型<T extends AutoCloseable & Cloneable>
,所以编译器做了它可以做的最不对的事情,并创建一个类型为两个边界接口之一的数组,丢弃另一个之一。
无论如何,如果切换接口的顺序,结果仍然是相同的。
<T extends Cloneable & AutoCloseable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
这让我做了一些调查,看看接口发生变化时会发生什么。 在我看来,编译器使用某种严格的顺序规则来决定哪个接口是最重要的,并且接口在代码中出现的顺序不起作用。
<T extends AutoCloseable & Runnable> // "AutoCloseable"
<T extends Runnable & AutoCloseable> // "AutoCloseable"
<T extends AutoCloseable & Serializable> // "Serializable"
<T extends Serializable & AutoCloseable> // "Serializable"
<T extends SafeVarargs & Serializable> // "SafeVarargs"
<T extends Serializable & SafeVarargs> // "SafeVarargs"
<T extends Channel & SafeVarargs> // "Channel"
<T extends SafeVarargs & Channel> // "Channel"
<T extends AutoCloseable & Channel & Cloneable & SafeVarargs> // "Channel"
问题: 当有多个边界时,Java编译器如何确定参数化类型的varargs数组的组件类型?
我甚至不确定JLS是否对此有所说明,而且我通过Google搜索找到的所有信息都没有涵盖这一特定主题。
答案 0 :(得分:12)
通常,当编译器遇到对参数化方法的调用时,它可以推断出类型(JSL 18.5.2),并且可以在调用者中创建正确类型的vararg数组。
规则主要是说“找到所有可能的输入类型并检查它们”的技术方法(例如void,三元运算符或lambda)。 其余的是常识,例如使用最具体的公共基类(JSL 4.10.4)。 例如:
public class Test {
private static class A implements AutoCloseable, Runnable {
@Override public void close () throws Exception {}
@Override public void run () {} }
private static class B implements AutoCloseable, Runnable {
@Override public void close () throws Exception {}
@Override public void run () {} }
private static class C extends B {}
private static <T extends AutoCloseable & Runnable> void printType( T... args ) {
System.out.println( args.getClass().getComponentType().getSimpleName() );
}
public static void main( String[] args ) {
printType( new A() ); // A[] created here
printType( new B(), new B() ); // B[] created here
printType( new B(), new C() ); // B[] which is the common base class
printType( new A(), new B() ); // AutoCloseable[] - well...
printType(); // AutoCloseable[] - same as above
}
}
AutoCloseable & Channel
简化为Channel
。
但规则无助于回答这个问题。AutoCloseable[]
可能看起来很奇怪,因为我们无法使用Java代码执行此操作。
但实际上实际类型并不重要。
在语言级别,args
为T[]
,其中T
为“虚拟类型”,同时为A和B(JSL 4.9)。
编译器只需要确保它的用法满足所有约束,然后它就知道 logic 是合理的,并且不存在类型错误(这就是Java泛型的设计方式)。
当然编译器仍然需要创建一个真正的数组,并且为此目的它创建了一个“通用数组”。
因此警告"unchecked generic array creation
"(JLS 15.12.4.2)。
换句话说,只要您仅传入AutoCloseable & Runnable
,并且只调用Object
中的AutoCloseable
,Runnable
和printType
方法,实际的数组类型无关紧要。
事实上,printType
的字节码将是相同的,无论传入何种类型的数组。
由于printType
不关心vararg数组类型,getComponentType()
不会也不应该重要。
如果要获取接口,请尝试返回数组的getGenericInterfaces()
。
T
的接口顺序确实会影响(JSL 13.1)编译的方法签名和字节码。将使用第一个接口AutoClosable
,例如在AutoClosable.close()
。printType
时,不会进行类型检查
AutoClosable[]
被创建和传递。在擦除之前检查许多类型的安全装置,因此顺序不影响类型安全性。我认为这是"The order of types... is only significant in that the erasure ... is determined by the first type"
(JSL 4.4)对JSL意味着什么的一部分。这意味着订单无关紧要。printType(AutoCloseable[])
时,这种删除规则确实会导致诸如添加printType( Runnable[])
触发器编译错误等极端情况。我认为这是一个意想不到的副作用,真的超出了范围。答案 1 :(得分:3)
这是一个非常有趣的问题。规范的相关部分是§15.12.4.2. Evaluate Arguments:
如果被调用的方法是变量arity方法
m
,则它必须具有 n &gt; 0形式参数。对于某些m
,T[]
的最终形式参数必须具有T
类型,并且必须使用 k ≥0实际参数表达式调用m
如果使用 k ≠ n 实际参数表达式调用
m
,或者,如果使用 k调用m
= n 实际参数表达式和 k '参数表达式的类型与T[]
的赋值不兼容,然后是参数列表({对{1}},...,e1
,en-1
,...,en
)进行评估,就好像它被写为(ek
,...,e1
,en-1
|new
|T[]
{
,...,en
ek
),其中| {{1 }} |表示}
的擦除(第4.6节)。
“有些T[]
”究竟是什么,这很有趣。最简单和最直接的解决方案是被调用方法的声明参数类型;这将是赋值兼容的,并且使用不同类型没有实际优势。但是,正如我们所知,T[]
不会走那条路并使用所有参数的某种公共基类型,或者根据数组元素类型的某些未知规则选择一些边界。现在你甚至可以在野外依赖这种行为找到一些应用程序,假设通过检查数组类型在运行时获得有关实际T
的一些信息。
这会产生一些有趣的后果:
javac
T
决定在这里创建一个实际类型static AutoCloseable[] ARR1;
static Serializable[] ARR2;
static <T extends AutoCloseable & Serializable> void method(T... args) {
ARR1 = args;
ARR2 = args;
}
public static void main(String[] args) throws Exception {
method(null, null);
ARR2[0] = "foo";
ARR1[0].close();
}
的数组,尽管在应用类型擦除后方法的参数类型为javac
,这就是为什么赋值{ {1}}在运行时是可行的。因此,当尝试使用
Serializable[]
方法时,它只会在最后一个语句中失败
AutoClosable[]
它在这里归咎于类String
,尽管我们可以将任何close()
对象放入数组中,因为实际问题是形式声明类型{{1}的Exception in thread "main" java.lang.IncompatibleClassChangeError: Class java.lang.String does not implement the requested interface java.lang.AutoCloseable
字段指的是实际类型String
的对象。
虽然这是我们迄今为止得到的HotSpot JVM的特定行为,因为它的验证程序在涉及接口类型时(包括接口类型数组)不检查赋值,但是推迟检查实际类是否实现了接口在尝试实际调用接口方法时,到最后一刻。
有趣的是,类型转换是严格的, 时它们出现在类文件中:
Serializable
虽然static
在上面的示例中对AutoCloseable[]
的决定似乎是任意的,但应该很清楚,无论选择哪种类型,其中一个字段分配只能在JVM中使用宽松的类型检查。我们还可以强调问题的更基本性质:
Serializable[]
虽然这实际上没有回答实际规则static <T extends AutoCloseable & Serializable> void method(T... args) {
AutoCloseable[] a = (AutoCloseable[])args; // actually removed by the compiler
a = (AutoCloseable[])(Object)args; // fails at runtime
}
public static void main(String[] args) throws Exception {
method();
}
使用的问题(除了它使用“some javac
”),但它强调了处理为 varargs <创建的数组的重要性/ em>参数预期:您最好不关心的任意类型的临时存储(不分配给字段)。