当前位置: 首页 > 知识库问答 >
问题:

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

松和璧
2023-03-14

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

<T extends AutoCloseable & Cloneable>
void printType(T... args) {
    System.out.println(args.getClass().getComponentType().getSimpleName());
}

// printType() prints "AutoCloseable"

我很清楚,没有类型

无论如何,如果切换了接口的顺序,结果仍然是一样的。

<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是否对此说了什么,我通过谷歌搜索找到的信息都没有涵盖这个特定的主题。


共有2个答案

公良鸿风
2023-03-14

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

如果所调用的方法是一个变量arity方法m,则它必须有n个变量

如果用k调用m≠ n个实际参数表达式,或者,如果使用k=n个实际参数表达式调用m,并且第k个参数表达式的类型与T[]的赋值不兼容,然后,论点列表(e e 1 >,,,, e<代码>e n-1 }),其中| T[]|表示对 T[]的擦除(§4.6)。

有趣的是,关于“someT”实际上是什么,它很模糊。最简单、最直接的解决方案是调用方法的声明参数类型;这将与分配兼容,并且使用不同类型没有实际优势。但是,正如我们所知,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决定在这里创建一个实际类型为Serializable[]的数组,尽管在应用类型擦除后,该方法的参数类型是AutoClosable[],这就是为什么可以在运行时分配字符串的原因。因此,它只会在最后一条语句中失败,即尝试使用

线程“main”java中出现异常。lang.CompatibleClassChangeError:类java。String不实现请求的java接口。lang.AutoCloseable

虽然我们可以将任何可序列化的对象放入数组中,但实际上的问题是,正式声明类型自动关闭[]静态字段引用的是实际类型可序列化的[]的对象。

尽管这是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();
}

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

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

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

楚乐逸
2023-03-14

通常,当编译器遇到对参数化方法的调用时,它可以推断类型(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

当然,从调用中获取AutoCloseable[]可能看起来很奇怪,因为Java代码无法做到这一点。但实际上,实际类型并不重要。在语言层面上,argsT[],其中T是一个“虚拟类型”,同时是a和B(JSL 4.9)。

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

换句话说,只要您只传入AutoCloseable

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

  • 由于类型擦除(JSL 4.6),T的接口顺序确实会影响(JSL 13.1)编译的方法签名和字节码。将使用第一个接口AutoClosable,例如在printType中调用AutoClosable.close()时不会进行类型检查。
  • 但这与问题的方法调用的类型干扰无关,即为什么要创建和传递AutoClosable[]。许多类型安全在擦除之前都会检查,因此顺序不会影响类型安全。我认为这是JSL所指的“类型的顺序......重要的只是擦除......是由第一种类型决定的”(JSL 4.4)。这意味着顺序在其他方面是无关紧要的。
  • 不管怎样,这个擦除规则确实会导致角落情况,例如添加printType(AutoCloseable[])触发编译错误,当添加printType(Runnable[])时不会。我相信这是一个意想不到的副作用,真的超出了范围。
  • P. S.挖得太深可能会导致精神错乱,考虑到我认为我是Ovis ary,查看源到汇编,并努力用英语而不是J S L回答。我的理智分数是b e yon d r e a l numb ers。T u r n̪͓͓̭̯̕ ̱̱̞̠̬ͅb a c k。b e f o̼͕̱͎̬̟̪r҉͏̛̣̼͙͍͍̠̫͙e

 类似资料:
  • 编译以下Java代码: 此外,编译以下代码: 以下代码将给我一个编译错误: 因此,看起来,如果其中一个边界类型本身是类型参数,就不可能有多个边界。但为什么呢? 据我所知,如果声明了多个边界,那么最多一个边界可能是一个类(其他所有边界都是接口),并且它必须是第一个边界。 但是,既然在我的示例中是一个接口,为什么编译器会抱怨呢?毕竟,T是接口还是类应该没有任何区别。 我试图在网上找到答案,但显然我的谷

  • 我知道在Java泛型中,当使用具有多个边界的类型参数时,编译器会将类型信息擦除到“最左边的边界”(即列表中的第一个类/枚举或接口)。那么为什么以下代码编译没有问题呢? 类型参数不应该被视为对象吗??(因此不允许我调用close()或append())??

  • 问题内容: 是否可以使用CSS3选择器选择具有给定类名称的第一个元素?我的考试没有成功,所以我认为不是吗? 问题答案: 不,仅使用一个选择器是不可能的。的伪类选择其的第一个元素类型(,等)。将类选择器(或类型选择器)与该伪类一起使用意味着,如果元素具有给定的类(或属于给定的类型) _并且_是其同级中的第一个元素,则选择该元素。 不幸的是,CSS没有提供仅选择类的首次出现的选择器。解决方法是,可以使

  • 我想创建一个rest来在服务器和客户端之间进行通信。 下面给出的构造函数: 对于普通类型,我可以执行以下操作: 如何传递泛型类型,如: 以下代码无效: 我遇到一个运行时错误:无法强制转换为ParameteredType。

  • 问题内容: 我有一个看起来像这样的方法: 因此,我期望传递的集合是实现Marshallable接口的Enum。如果我在运行时具有具体的Enum类型,则可以正常工作,但是我编写了一个测试方法,该方法从类对象动态创建一个Enum列表(实现Marshallable),并且无法将此列表传递给上面的方法。 标记的行将给出编译错误。我不知道如何在不更改方法签名的情况下传递列表。 问题答案: 您需要对泛型辅助方

  • 问题内容: 以下两个签名是否相同? 和 问题答案: 不,两个签名不相同。根据Java语言规范的第8章: 如果两个方法具有相同的名称和参数类型,则它们具有相同的签名。 如果满足以下所有条件,则两个方法或构造函数声明M和N具有相同的参数类型: 它们具有相同数量的形式参数(可能为零) 它们具有相同数量的类型参数 (可能为零) … 由于两种方法共享的类型参数数量不同,因此签名也不相同。 在实际情况下,使用