为原始类型抛出不一致的 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
得到的类型实际上是一个 Integer
。 List::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
指令的组合仍然表现出相同的兼容行为。
执行下面的代码时,代码完美执行,没有任何错误,但是对于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
得到的类型实际上是一个 Integer
。 List::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
指令的组合仍然表现出相同的兼容行为。