Mic*_*ung 5 java generics casting
我希望这段代码抛出ClassCastException:
public class Generics {
    public static void main(String[] args) {
        method(Integer.class);
    }
    public static <T> T method(Class<T> t) {
        return (T) new String();
    }
}
但事实并非如此.将字符串转换为T不会失败,直到我以某种方式使用返回的对象,如:
public class Generics {
    public static void main(String[] args) {
        method(Integer.class).intValue();
    }
    public static <T> T method(Class<T> t) {
        return (T) new String();
    }
}
背景:我创建了一个使用JAXB来解组XML文件的类.它看起来像这样:
public static <T> T unmarshal(File file, Class<? extends T> clazz)
根据root-Element是否为匿名类型,返回T或JAXBElement.当然,JAXBElement不能转换为T.在我的单元测试中,我只调用了unmarshal()而没有对结果做任何事情,一切正常.在Code中,它失败了.
为什么不直接失败?这是一个错误吗?如果没有,我想了解原因.
基本上,由于类型擦除,每当您使用T特定的事实时,Java 都会在调用站点执行类型检查。
但不幸的是,事情并没有那么简单。
当其他答案说T存在Object是你没有得到 的原因时,他们是不正确的ClassCastException。
让我们测试一下理论并手动T选择Integer:
Generics.<Integer>method(Integer.class);
当我运行这个时,它仍然没有失败。
Java 确实推断T为Integer,method(Integer.class).intValue()这将是一个编译时错误,链式调用不会通知类型推断。
那么发生了什么?
请注意,当它确实失败时,它永远不会失败method,它总是会在内部失败main。
由于类型擦除,method编译后基本上没有任何泛型信息。返回类型最终为Object,参数类型是原始类型Class,并且方法内的强制转换被简单地删除,因为在没有任何泛型信息的情况下强制转换将是无操作。
在检查调用站点的字节码时您可以看到这一点:
   0: ldc           #7   // class java/lang/Integer
   2: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
                                                               ^^^^^^^^^^^^^^^^
                                                               return type
当调用字节码中的方法时,如果您愿意,返回类型最终会成为方法“名称”的一部分。
进一步探索编译器资源管理器,我们发现修改后的方法为前四行main生成以下字节码1 :
Main.<Integer>method(Integer.class);
   0: ldc           #7   // class java/lang/Integer
   2: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
   5: pop
Object o = Main.<Integer>method(Integer.class);
   6: ldc           #7   // class java/lang/Integer
   8: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
  11: astore_1
Main.<Integer>method(Integer.class).intValue();
  12: ldc           #7   // class java/lang/Integer
  14: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
  17: checkcast     #7   // class java/lang/Integer
  20: invokevirtual #15  // Method java/lang/Integer.intValue:()I
  23: pop
Integer i = Main.<Integer>method(Integer.class);
  24: ldc           #7   // class java/lang/Integer
  26: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
  29: checkcast     #7   // class java/lang/Integer
  32: astore_2
对于每一行,我都添加了相应的字节码,其中散布着 Java 代码。
比较不同行的字节码。注意 Java 如何checkcast在方法调用之后插入一条指令method,即在 之后invokevirtual。这对当前位于堆栈顶部的返回值执行类型检查。因为它是 aString并且被转换为Integer,所以你会得到一个ClassCastException。
对于不使用结果的前两行,它不会这样做。
这就是为什么只有当您像您一样实际使用结果时您的代码才会失败。
我会假设每当你利用这个事实来验证是否T确实返回了某种类型的东西时,Java 都会插入此强制转换,以便尽早失败。IntegermethodT
这是另一个例子:
Main.<Integer>method(Integer.class).toString();
  33: ldc           #7   // class java/lang/Integer
  35: invokestatic  #9   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
  38: checkcast     #7   // class java/lang/Integer
  41: invokevirtual #19  // Method java/lang/Integer.toString:()Ljava/lang/String;
  44: pop
编译器知道该.toString()调用是针对 类型的某些内容进行的,因此它直接对该方法的版本Integer进行虚拟调用。Integer当然,编译器需要插入一个检查以确保返回值(可以是运行时的任何值)符合Integer,因此它会插入另一checkcast条指令。
然而,即使使用不重写 Object 的类toString,Java 仍然会插入一个checkcast:
Main.<Main>method(Main.class).toString();
  45: ldc           #6   // class Main
  47: invokestatic  #3   // Method method:(Ljava/lang/Class;)Ljava/lang/Object;
  50: checkcast     #6   // class Main
  53: invokevirtual #7   // Method java/lang/Object.toString:()Ljava/lang/String;
  56: pop
尽管目标是所有对象都存在的方法,本质上是选择静态接收器类型Object,但 Java 仍然插入checkcast。
然而,当我们自己转换返回值时Object,Java 不会添加任何checkcast内容,并且调用可以通过。
让我们退一步思考一下我们一直在做什么。我们关注的不是 Java 本身,而是字节码。
Java 由 Java 语言规范定义。我希望找到某种规则来描述何时完成此类型检查以及何时未完成此类型检查。
不幸的是,我在规范中找不到有关这些插入的类型检查的任何内容。
在你偶然发现这一点的几年后,其他人也看过了。
如果确实未指定,那么每当我在上面说“Java 插入/不插入检查”时,我可能应该说“这个特定的编译器”而不是“Java”,而我们一直在关注的内容在技术上可能只是一个实施细节(到目前为止)。
1运行 JDK 17.0.0 的某些变体