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

我想更好地了解当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是否对此有任何看法,而且我通过谷歌搜索找到的信息都没有涵盖这个特定主题。


答案 1

通常,当编译器遇到对参数化方法的调用时,它可以推断类型(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 规定了如何处理类型推断的约束,例如 is 简化为 。但这些规则无助于回答这个问题。AutoCloseable & ChannelChannel

当然,从调用中获取可能看起来很奇怪,因为我们无法用Java代码做到这一点。但实际上,实际类型并不重要。在语言级别,is 是 ,其中“虚拟类型”既是 A 又是 B (JSL 4.9)。AutoCloseable[]argsT[]T

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

换句话说,只要您只传入 、并且只调用 、 和 中的方法,实际的数组类型就无关紧要。事实上,的字节码是相同的,无论传入哪种数组。AutoCloseable & RunnableObjectAutoCloseableRunnableprintTypeprintType

由于不关心 vararg 数组类型,因此无关紧要。如果你想获取接口,请尝试 getGenericInterfaces() 它返回一个数组。printTypegetComponentType()

  • 由于类型擦除 (JSL 4.6),接口的顺序会影响 (JSL 13.1) 编译的方法签名和字节码。将使用第一个接口,例如,在 中调用时不会执行类型检查。TAutoClosableAutoClosable.close()printType
  • 但这与问题的方法调用的类型干扰无关,即为什么被创建和传递。许多类型安全在擦除之前都会进行检查,因此顺序不会影响类型安全。我认为这是JSL所指的(JSL 4.4)的一部分。这意味着订单在其他方面是微不足道的。AutoClosable[]"The order of types... is only significant in that the erasure ... is determined by the first type"
  • 无论如何,此擦除规则确实会导致一些极端情况,例如添加触发器编译错误,而添加则不会。我认为这是一个意想不到的副作用,真的超出了范围。printType(AutoCloseable[])printType( Runnable[])
  • 附言:挖掘得太深可能会导致精神错乱,考虑到我认为我是绵羊,将源代码视为汇编,并且很难用英语而不是J̶'SʡL̴̀来回答。我的理智分数是 b҉ȩyon̨d͝ r̨̡͝e̛a̕l̵ 麻木者١。T͉͎̫͠u͍r̟̦͝n̪͓͓̭̯̕ ̱̱̞̠̬ͅb̯̠̞̩͎a̘̜̯c̠̮k.̠̝͕b̭̳͠¤¤ͅẹ̡̬̦̙f͓͉̼̻o̼͕̱͎̬̟̪r҉̛̣̼͙͍͍̠̫͙͊ȩ̵̮̟̱̫͚ ̢͚̭̹̳̣̩̱͠..t̷҉̛̫͔͉̥͎̬ò̢̱̪͉̲͎͜o̭͈̩̖̭̬..̮̘̯̗l̷̞͍͙̻̻͙̯̣͈̳͓͇a̸̢̢̢̰͓͓̪̳̳̯͉̼͝͝t̛̥̪̣̹̬͔̖͙̬̩̝̰͕̖̮̰̗͓̔͢ę̴̹̯̟͉̲͔͉̳̲̣͝͞.̬͖͖͇͈̤̼͖́͘͢.͖ ͏̪̱̝̠̯̬͍̘̣̩͉̯̹̼͟͟͠.̨͠҉̬̘̹ͅ

答案 2

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

如果被调用的方法是一个变量 arity 方法,它必须有 n 个> 0 个形式参数。最后一个形式参数必然具有某些类型的类型,并且必须用 k 个≥ 0 个实际的参数表达式来调用。mmT[]Tm

如果 是使用 kn 个实际参数表达式调用的,或者,如果用 k = n 个实际参数表达式调用,并且第 k 个参数表达式的类型与 不兼容,则参数列表 (, ..., , , ..., ) 的计算方式就像它被写成 (, ..., , || , ..., ),其中 ||表示的擦除 (§4.6) 。mmT[]e1en-1eneke1en-1newT[]{enek}T[]T[]

有趣的是,关于“一些”到底是什么,它含糊不清。最简单,最直接的解决方案是调用方法的声明参数类型;这将是赋值兼容的,并且使用不同类型的没有实际优势。但是,正如我们所知,不会走这条路,而是使用所有参数的某种通用基类型,或者根据数组元素类型的一些未知规则选择一些边界。如今,您甚至可能会发现一些应用程序依赖于此行为,假设通过检查数组类型在运行时获取有关实际情况的一些信息。TjavacT

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

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决定在此处创建实际类型的数组,尽管该方法的参数类型是在应用类型擦除之后,这就是可以在运行时分配的原因。因此,它只会在最后一个语句处失败,当尝试调用其上的方法时Serializable[]AutoClosable[]Stringclose()

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

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

虽然这是HotSpot JVM的一个特定行为,但正如我们已经做到了这一点,因为它的验证器在涉及接口类型(包括接口类型数组)时不会检查赋值,而是将检查实际类是否实现接口推迟到最后一刻,当尝试实际调用其上的接口方法时。

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

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

虽然在上面的例子中,对 的决策似乎是武断的,但应该清楚的是,无论它选择哪种类型,其中一个字段分配只能在类型检查松弛的 JVM 中进行。我们还可以强调问题的更基本性质:javacSerializable[]

// erased to method1(AutoCloseable[])
static <T extends AutoCloseable & Serializable> void method1(T... args) {
    method2(args); // valid according to generic types
}
// erased to method2(Serializable[])
static <T extends Serializable & AutoCloseable> 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();
}

虽然这实际上并没有回答实际规则使用的问题(除了它使用“一些”),但它强调了按预期对待为varargs参数创建的数组的重要性:一个你最好不关心的任意类型的临时存储(不分配给字段)。javacT