我想更好地了解当 Java 编译器遇到对如下方法的调用时会发生什么。
<T extends AutoCloseable & Cloneable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
我很清楚没有类型 <T extends AutoCloseable & Cloneable>
在运行时,因此编译器会做它可以做的最少错误的事情,并使用两个边界接口(interface)之一的类型创建一个数组,丢弃另一个。
不管怎样,切换接口(interface)的顺序,结果还是一样的。
<T extends Cloneable & AutoCloseable>
void printType(T... args) {
System.out.println(args.getClass().getComponentType().getSimpleName());
}
// printType() prints "AutoCloseable"
这促使我进行了更多调查,看看当接口(interface)发生变化时会发生什么。 在我看来,编译器使用某种严格的顺序规则来决定哪个接口(interface)是最重要的,而接口(interface)在代码中出现的顺序没有任何作用。
<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 是否对此有任何说明,而且我通过谷歌搜索找到的信息均未涵盖此特定主题。
最佳答案
通常,当编译器遇到对参数化方法的调用时,它可以推断类型 (JSL 18.5.2) 并可以在调用者中创建正确类型的可变参数数组。
这些规则主要是表示“找到所有可能的输入类型并检查它们”的技术方式(例如 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
}
}
- JSL 18.2指示如何处理类型推断的约束,例如将
AutoCloseable & Channel
简化为仅Channel
。 但这些规则无助于回答这个问题。
当然,从调用中获取 AutoCloseable[]
可能看起来很奇怪,因为我们不能用 Java 代码来做到这一点。
但实际上,实际类型并不重要。
在语言级别,args
是 T[]
,其中 T
是“虚拟类型”,既是 A 又是 B (JSL 4.9 )。
编译器只需要确保它的用法满足所有约束,然后它就知道逻辑是正确的并且不会出现类型错误(这就是Java泛型的设计方式)。
当然,编译器仍然需要创建一个 real 数组,并为此创建一个“通用数组”。
因此警告 "unchecked generic array creation
" (JLS 15.12.4.2)。
也就是说,只要你只传入AutoCloseable&Runnable
,并且只调用Object
、AutoCloseable
、
方法,实际的数组类型无关紧要。
其实printType
中的 RunnableprintType
的字节码都是一样的,不管传入的是什么数组。
由于 printType
不关心可变参数数组类型,因此 getComponentType()
不重要也不应该重要。
如果要获取接口(interface),请尝试 getGenericInterfaces()
它返回一个数组。
- 由于类型删除( JSL 4.6 ),
T
的接口(interface)顺序确实会影响( JSL 13.1 )编译的方法签名和字节码。将使用第一个接口(interface)AutoClosable
,例如在printType
中调用AutoClosable.close()
时不会进行类型检查。 - 但这与问题的方法调用的类型干扰无关,即为什么创建和传递
AutoClosable[]
。在删除之前检查了许多类型安全,因此顺序不会影响类型安全。我认为这是 JSL 的一部分含义,“类型的顺序......仅在删除......由第一种类型决定时才有意义”
(JSL 4.4)。这意味着订单在其他方面无关紧要。 - 不管怎样,这个删除规则确实会导致一些极端情况,例如添加
printType(AutoCloseable[])
会触发编译错误,而添加printType(Runnable[])
则不会。我相信这是一个意想不到的副作用,并且真的超出了范围。 - 附言挖得太深可能会导致insanity ,考虑到我认为我是Ovis aries , 查看源代码 into assembly ,并且努力用英语而不是 J̶́S͡L̴̀ 回答。我的理智分数是 b҉ȩyon̨d͝ r̨̡͝e̛a̕l̵ numb͟ers͡ .回头。 ̠̝͕b̭̳͠͡ͅẹ̡̬̦̙f͓͉̼̻o̼͕̱͎̬̟̪r҉͏̛̣̼͙͍͍̠̫͙ȩ̵̮̟̱̫͚..t̷҉̛̫͔͉̥͎̬ò̢̱̪͉̲͎͜o̭͈̩̖̭̬ ..̮̘̯̗l̷̞͍͙̻̻͙̯̣͈̳͓͇a̸̢̢̰͓͓̪̳͉̯͉̼͝͝t̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̕͢ę̴̹̯̟͉̲͔͉̳̲̣͝͞..
关于java - Java 编译器如何为具有多个边界的参数化类型选择运行时类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50177313/