为原始类型抛出不一致的 ClassCastException

Inconsistent ClassCastException thrown for Raw Types

执行下面的代码时,代码完美执行,没有任何错误,但是对于List<Integer>类型的变量,get()方法的return类型应该是Integer,但是在执行这段代码时,当我调用 x.get(0) 时,一个字符串被 returned,而这应该抛出异常。

public static void main(String[] args)
      {
            ArrayList xa = new ArrayList();
            xa.addAll(Arrays.asList("ASDASD", "B"));
            List<Integer> x = xa;
            System.out.println(x.get(0));
      }

但是在执行下面的代码时,只需将 returned 对象的 class 检索添加到前面的代码块中,就会抛出 class 转换异常。如果上面的代码完美执行,下面的代码也应该毫无例外地执行:

public static void main(String[] args)
      {
            ArrayList xa = new ArrayList();
            xa.addAll(Arrays.asList("ASDASD", "B"));
            List<Integer> x = xa;
            System.out.println(x.get(0).getClass());
      }

为什么java在获取对象的class类型时执行类型转换?

因为 PrintStream#println:

public void println(Object x) {
    String s = String.valueOf(x);
    ...

查看它如何将您提供的任何内容转换为字符串,但首先将其分配给一个 Object(之所以有效,是因为 Integer 是一个 Object)。将您的第一个代码更改为:

    ArrayList xa = new ArrayList();
    xa.addAll(Arrays.asList("ASDASD", "B"));
    List<Integer> x = xa;
    Integer i = x.get(0);
    System.out.println(i);

你会得到同样的失败。

编辑

是的,Didier 的评论是正确的;因此在考虑了一段时间后更新。

这甚至可以像这样简化以理解为什么编译器要插入额外的 checkcast #5 // class java/lang/Integer:

 ArrayList<Integer> l = new ArrayList<>();
 l.get(0).getClass();

在运行时没有 Integer 类型,只有 Object;除其他外,这将编译为:

  10: invokevirtual #4 // Method java/util/ArrayList.get:(I)Ljava/lang/Object;
  13: checkcast     #5 // class java/lang/Integer
  16: invokevirtual #6 // Method java/lang/Object.getClass:()Ljava/lang/Class;

注意 checkcast 检查我们从 List 得到的类型实际上是一个 IntegerList::get 是一个泛型方法,运行时的泛型参数是一个 Object;为了在运行时保持正确的 List<Integer>,需要 checkcast

编译器必须在必要时在字节码级别插入类型检查指令,因此在对 Object 进行赋值时,例如Object o = x.get(0);System.out.println(x.get(0));,可能不需要它,在表达式 x.get(0) 上调用方法 需要它。

原因就在于binary compatibility rules。简单的说,被调用的方法是否被继承或者被接收者类型显式声明是无关紧要的,表达式 x.get(0) 的形式类型是 Integer 而你调用的方法是 getClass()因此,调用将被编码为对名为 getClass 的方法的调用,在接收方 class java.lang.Integer 上具有签名 () → java.lang.Class。该方法已从 java.lang.Object 继承并在编译时声明为 final 的事实并未反映在已编译的 class.

因此理论上,在运行时,可以从 java.lang.Object 中删除该方法,并将新方法 java.lang.Class getClass() 添加到 java.lang.Integer,而不会破坏与该特定代码的兼容性。虽然我们知道这永远不会发生,但编译器只是遵循正式规则,不会将有关继承的假设注入代码。

由于调用将被编译为针对java.lang.Integer的调用,因此在调用指令之前需要进行类型转换,这将在堆污染场景中失败。

请注意,如果将代码更改为

System.out.println(((Object)x.get(0)).getClass());

您将明确假设该方法已在 java.lang.Object 中声明。扩大到 java.lang.Object 不会生成任何额外的字节码指令,所有这些代码都会将方法调用的接收器类型更改为 java.lang.Object,从而消除类型转换的需要。

这里有一个有趣的偏离规则的地方,如果方法是java.lang.Object 中声明的已知 final 方法之一。这可能是因为这些特定方法 are specified in the JLS 并以这种形式对它们进行编码允许 JVM 快速识别这些特殊方法。但是 checkcast 指令和 invokevirtual 指令的组合仍然表现出相同的兼容行为。