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

时间:2018-05-04 14:28:06

标签: java variadic-functions jls multiple-bounds

我想更好地理解当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搜索找到的所有信息都没有涵盖这一特定主题。

2 个答案:

答案 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
   }
}
  • JSL 18.2指示如何处理类型推断的约束,例如AutoCloseable & Channel简化为Channel。 但规则无助于回答这个问题。
当然,从调用中获取AutoCloseable[]可能看起来很奇怪,因为我们无法使用Java代码执行此操作。 但实际上实际类型并不重要。 在语言级别,argsT[],其中T为“虚拟类型”,同时为A和B(JSL 4.9)。

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

换句话说,只要您仅传入AutoCloseable & Runnable,并且只调用Object中的AutoCloseableRunnableprintType方法,实际的数组类型无关紧要。 事实上,printType的字节码将是相同的,无论传入何种类型的数组。

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

  • 由于类型擦除(JSL 4.6),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[])触发器编译错误等极端情况。我认为这是一个意想不到的副作用,真的超出了范围。
  • P.S。挖掘太深可能会导致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̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̕͢ę̴̹̯̟͉͔͉̳̣͝͞.̬͖͖͇͈̤̼͖͘͢.͏̪̝̠̯̬͍̘̣̩͉̯̹̼͟͟͠.̨͠҉̬̘̹

答案 1 :(得分:3)

这是一个非常有趣的问题。规范的相关部分是§15.12.4.2. Evaluate Arguments

  

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

     

如果使用 k n 实际参数表达式调用m,或者,如果使用 k调用m = n 实际参数表达式和 k '参数表达式的类型与T[]的赋值不兼容,然后是参数列表({对{1}},...,e1en-1,...,en)进行评估,就好像它被写为(ek,..., e1en-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>参数预期:您最好不关心的任意类型的临时存储(不分配给字段)。