Java编译器如何为具有多个边界的参数化类型选择运行时类型?

我想更好地理解当Java编译器遇到如下方法的调用时会发生什么。

 void printType(T... args) { System.out.println(args.getClass().getComponentType().getSimpleName()); } // printType() prints "AutoCloseable" 

我很清楚,在运行时没有类型 ,所以编译器做了它可以做的最不对的事情,并创建一个类型为两个边界接口之一的数组,丢弃另一个。

无论如何,如果切换接口的顺序,结果仍然是相同的。

  void printType(T... args) { System.out.println(args.getClass().getComponentType().getSimpleName()); } // printType() prints "AutoCloseable" 

这导致我做了一些调查,看看接口发生变化时会发生什么。 在我看来,编译器使用某种严格的顺序规则来决定哪个接口是最重要的 ,并且接口在代码中出现的顺序不起作用。

  // "AutoCloseable" 
  // "AutoCloseable" 
  // "Serializable" 
  // "Serializable" 
  // "SafeVarargs" 
  // "SafeVarargs" 
  // "Channel" 
  // "Channel" 
  // "Channel" 

问题:当存在多个边界时,Java编译器如何确定参数化类型的varargs数组的组件类型?

我甚至不确定JLS是否对此有所说明,而且我通过谷歌搜索发现的任何信息都没有涵盖这一特定主题。

通常,当编译器遇到对参数化方法的调用时,它可以推断出类型( 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  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代码执行此操作。 但实际上实际类型并不重要。 在语言层面, argsT[] ,其中T是“虚拟类型”,既是A又是B( JSL 4.9 )。

编译器只需要确保它的用法满足所有约束,然后它就知道逻辑是合理的,并且不存在类型错误(这就是Javagenerics的设计方式)。 当然编译器仍然需要创建一个真正的数组,并且为此目的它创建了一个“通用数组”。 因此警告“ unchecked generic array creation ” ( JLS 15.12.4.2 )。

换句话说,只要传入AutoCloseable & Runnable ,并且只调用printType ObjectAutoCloseableRunnable方法,实际的数组类型就无关紧要了。 实际上,无论传入何种类型的数组, printType的字节码都是相同的。

由于printType不关心vararg数组类型,因此getComponentType()不会也不应该重要。 如果要获取接口,请尝试getGenericInterfaces() ,它返回一个数组。

  • 由于类型擦除( JSL 4.6 ), T的接口顺序确实会影响( JSL 13.1 )编译的方法签名和字节码。 将使用第一个接口AutoClosable ,例如,在printType调用AutoClosable.close()时,不会进行类型检查。
  • 但这与问题的方法调用的类型干扰无关,即创建和传递AutoClosable[]原因。 在擦除之前检查许多类型的安全装置,因此顺序不影响类型安全性。 我认为这是JSL所指的一部分"The order of types... is only significant in that the erasure ... is determined by the first type" ( JSL 4.4 )。 这意味着订单无关紧要。
  • 无论如何,当添加printType( Runnable[])时,这种擦除规则确实会导致诸如添加printType(AutoCloseable[])触发编译错误等printType( Runnable[])情况。 我认为这是一个意想不到的副作用, 实际上超出了范围。
  • PS挖掘太深可能导致精神错乱 ,考虑到我认为我是Ovis白羊座 ,观看汇编的来源,并努力用英语而不是J̶S͡L̴回答。 我的理智分数是b҉ȩyon̨d͝r̨̡͝e̛a̕l̵numb͟ers͡ 。 回头。 ̠̝͕b̭̳͠͡ẹ̡̬̦̙f͓͉̼̻o̼͕͎̬̟̪r҉͏̛̣̼͙͍͍̠̫͙ȩ̵̮̟̫͚҉͏̛̣̼͙͍͍̠̫͙ȩ̵̮̟̫͚..t̷҉̛̫͔͉̥͎̬ò̢̪͉͎͜o̭͈̩̖̭̬..̮̘̯̗l̷̞͍͙̻̻͙̯̣͈̳͓͇a̸̢̢̰͓͓̪̳͉̯͉̼͝͝t̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̕͢ę̴̹̯̟͉͔͉̳̣͝͞.̬͖͖͇͈̤̼͖͘͢.͏̪̝̠̯̬͍̘̣̩͉̯̹̼͟͟͠.̨͠҉̬̘̹

这是一个非常有趣的问题。 规范的相关部分是§15.12.4.2。 评估参数 :

如果被调用的方法是变量arity方法m ,则它必须具有n > 0个forms参数。 对于某些Tm的最终forms参数必然具有类型T[] ,并且必须使用k≥0个实际参数表达式来调用m

如果使用kn实际参数表达式调用m ,或者,如果使用k = n实际参数表达式调用m并且第k个参数表达式的类型与T[]不兼容,则参数列表( e 1 ,…, e n-1e n ,…, e k )被评估为好像被写为( e 1 ,…, e n-1new | T[] | { e n ,…, e k } ),其中| T[] | 表示T[]的擦除(第4.6节)。

对于“某些T ”究竟是什么,有趣的是模糊不清。 最简单和最直接的解决方案是被调用方法的声明参数类型; 这将是赋值兼容的,并且使用不同类型没有实际优势。 但是,正如我们所知, javac并没有走那条路并且使用所有参数的某种公共基类型,或者根据数组元素类型的某些未知规则选择一些边界。 现在你甚至可以在野外依赖这种行为找到一些应用程序,假设通过检查数组类型在运行时获得有关实际T一些信息。

这会导致一些有趣的后果:

 static AutoCloseable[] ARR1; static Serializable[] ARR2; static  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(); } 

尽管在应用类型擦除之后方法的参数类型是AutoClosable[] ,但javac决定在这里创建一个实际类型Serializable[]的数组,这就是为什么在运行时可以分配String的原因。 所以当它尝试用它调用close()方法时,它只会在最后一个语句中失败

Exception in thread "main" java.lang.IncompatibleClassChangeError: Class java.lang.String does not implement the requested interface java.lang.AutoCloseable

它在这里指责类String ,尽管我们可以将任何Serializable对象放入数组中,因为实际问题是forms声明类型AutoCloseable[]static字段指的是实际类型Serializable[]的对象。

虽然它是我们迄今为止所获得的HotSpot JVM的特定行为,因为它的validation程序在涉及接口类型时(包括接口类型的数组)不检查赋值,但是延迟检查实际类是否实现到最后一个的接口在尝试实际调用接口方法时可能的时刻。

有趣的是, 类型转换出现在类文件中时,它们是严格的:

 static  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(); } 

虽然javac在上面的例子中对Serializable[]的决定似乎是任意的,但应该很清楚,无论选择哪种类型,其中一个字段赋值只能在具有松散类型检查的JVM中实现。 我们还可以强调问题的更基本性质:

 // erased to method1(AutoCloseable[]) static  void method1(T... args) { method2(args); // valid according to generic types } // erased to method2(Serializable[]) static  void method2(T... args) { } public static void main(String[] args) throws Exception { // whatever array type the compiler picks, it would violate one of the erased types method1(); } 

虽然这实际上没有回答javac使用的实际规则(除了它使用“some T ”)之外的问题,但它强调了按预期处理为varargs参数创建的数组的重要性:临时存储(不分配给字段)任何类型的你最好不要在意。