错误的泛型转换没有 ClassCastException [Java]
No ClassCastException on wrong generic cast [Java]
看看下面的主要方法class:
public class Outer {
static class A<T> {
public A<T> a;
public T t;
public A() { a = null; }
public A(T t) {
a = (A<T>) new B();
a.t = t;
}
}
static class B extends A<Integer> {}
public static void main(String[] args) {
// A<String> c = new B(); // CREATION 1: would not work
// A<String> b = (A<String>) new B(); // CREATION 2: would not work
A<String> a1 = new A<>("str"); // CREATION 3: works (???)
A<String> a2 = new A(0); // CREATION 4: works (???)
System.out.println(a1.a); // CALL 1 -> Poly$B@...
System.out.println(a1.t); // CALL 2 -> null
System.out.println(a1.a.a); // CALL 3 -> null
System.out.println(a1.a.t); // CALL 4 -> str (???)
System.out.println(a2.a.t); // CALL 5 -> ClassCastException (???)
}
}
显然,创建 1 和 2 在编译时会失败,因为无法进行强制转换。第三个对象创建是可能的,所以我没想到它会在编译时失败,但是:我预计 (A<T>) new B();
在运行时会失败,因为 B
扩展 A<Integer>
并且它不应该可以将 A<Integer>
转换为 A<String>
(与创建 2 相同)...但是这里没有 ClassCastException!?
不幸的是,创造 3 成功了。 a1.a.t
(这是 B
对象的 t
成员)现在将存储字符串 "str"!?所以一个 B
对象(意思是 t
是 Integer
类型)可以在 Integer t
!!?
中存储 String
s
另一件事:看a2
。尝试访问 a2.a.t
将产生 ClassCastException。这让我感到惊讶,因为在访问期间甚至没有执行转换 - 至少,这是我的想法...
所以我的问题简而言之是:
- 为什么创世3(/4)可以?
- 为什么 B 对象能够在其泛型整数变量 t 中存储字符串?
- 为什么调用 5 在运行时失败?
谢谢!! :)
为什么创作3(/4)可以?
Creation 3 是用于推断类型的 "diamond" 语法。所以 new A<>
和 new A<String>
.
是一样的
所以我们可以用 Java 1.7 之前的长语法替换它。
A<String> a1 = new A<String>("str");
这将通过以下构造函数,其中 T
是 java.lang.String
,t
是 java.lang.String
。请注意,此代码在运行时只有一个副本,T
的原始类型为 java.lang.Object
.
public A(T t) {
a = (A<T>) new B();
// ^^^^^^ NB: This will at least give a rawtype warning
// as it causes heap pollution.
a.t = t;
}
a
的类型在编译时是A<T>
,java.lang.Object
被擦除。 a.t
和 t
的类型在编译时都是 T
并且 java.lang.Object
被擦除。所以它在没有警告的情况下编译,并且在运行时没有强制检查。赋值成功
Creation 4 正在使用原始类型。你的编译器应该给出一个警告(使用 -Xlint
)。忽略这些警告可能会在以后导致 ClassCastException
。泛型是真实虚拟机上的编译器 "fiction",它基本上遵循 Java 1.0 语言的规则。
为什么 B 对象能够在其通用整数变量 t 中存储字符串?
A
(因此 B
)中 t
的擦除类型是 java.lang.Object
,因此如果没有检查或检查不正确,可以存储任何引用类型。
为什么调用 5 在运行时失败?
a2.a.t
处的对象是 java.lang.Integer
。 a2.a
有静态类型 A<java.lang.String>
所以 a2.a.t
的静态类型是 java.lang.String
。 javac
将插入一个转换以检查返回的原始类型是否符合静态类型。在这种特殊情况下,IIRC,旧版本的 javac
会不正确地省略强制转换并使用 println(java.lang.Object)
重载而不是 println(java.lang.String)
.
您可以使用 javap -c -private
查看 checkcast
指令的确切位置。
结论
确保您已打开警告。默认情况下没有启用它们的任何东西都有些可疑。不要忽略或禁止警告,尤其是关于原始类型的警告。
看看下面的主要方法class:
public class Outer {
static class A<T> {
public A<T> a;
public T t;
public A() { a = null; }
public A(T t) {
a = (A<T>) new B();
a.t = t;
}
}
static class B extends A<Integer> {}
public static void main(String[] args) {
// A<String> c = new B(); // CREATION 1: would not work
// A<String> b = (A<String>) new B(); // CREATION 2: would not work
A<String> a1 = new A<>("str"); // CREATION 3: works (???)
A<String> a2 = new A(0); // CREATION 4: works (???)
System.out.println(a1.a); // CALL 1 -> Poly$B@...
System.out.println(a1.t); // CALL 2 -> null
System.out.println(a1.a.a); // CALL 3 -> null
System.out.println(a1.a.t); // CALL 4 -> str (???)
System.out.println(a2.a.t); // CALL 5 -> ClassCastException (???)
}
}
显然,创建 1 和 2 在编译时会失败,因为无法进行强制转换。第三个对象创建是可能的,所以我没想到它会在编译时失败,但是:我预计 (A<T>) new B();
在运行时会失败,因为 B
扩展 A<Integer>
并且它不应该可以将 A<Integer>
转换为 A<String>
(与创建 2 相同)...但是这里没有 ClassCastException!?
不幸的是,创造 3 成功了。 a1.a.t
(这是 B
对象的 t
成员)现在将存储字符串 "str"!?所以一个 B
对象(意思是 t
是 Integer
类型)可以在 Integer t
!!?
String
s
另一件事:看a2
。尝试访问 a2.a.t
将产生 ClassCastException。这让我感到惊讶,因为在访问期间甚至没有执行转换 - 至少,这是我的想法...
所以我的问题简而言之是:
- 为什么创世3(/4)可以?
- 为什么 B 对象能够在其泛型整数变量 t 中存储字符串?
- 为什么调用 5 在运行时失败?
谢谢!! :)
为什么创作3(/4)可以?
Creation 3 是用于推断类型的 "diamond" 语法。所以 new A<>
和 new A<String>
.
所以我们可以用 Java 1.7 之前的长语法替换它。
A<String> a1 = new A<String>("str");
这将通过以下构造函数,其中 T
是 java.lang.String
,t
是 java.lang.String
。请注意,此代码在运行时只有一个副本,T
的原始类型为 java.lang.Object
.
public A(T t) {
a = (A<T>) new B();
// ^^^^^^ NB: This will at least give a rawtype warning
// as it causes heap pollution.
a.t = t;
}
a
的类型在编译时是A<T>
,java.lang.Object
被擦除。 a.t
和 t
的类型在编译时都是 T
并且 java.lang.Object
被擦除。所以它在没有警告的情况下编译,并且在运行时没有强制检查。赋值成功
Creation 4 正在使用原始类型。你的编译器应该给出一个警告(使用 -Xlint
)。忽略这些警告可能会在以后导致 ClassCastException
。泛型是真实虚拟机上的编译器 "fiction",它基本上遵循 Java 1.0 语言的规则。
为什么 B 对象能够在其通用整数变量 t 中存储字符串?
A
(因此 B
)中 t
的擦除类型是 java.lang.Object
,因此如果没有检查或检查不正确,可以存储任何引用类型。
为什么调用 5 在运行时失败?
a2.a.t
处的对象是 java.lang.Integer
。 a2.a
有静态类型 A<java.lang.String>
所以 a2.a.t
的静态类型是 java.lang.String
。 javac
将插入一个转换以检查返回的原始类型是否符合静态类型。在这种特殊情况下,IIRC,旧版本的 javac
会不正确地省略强制转换并使用 println(java.lang.Object)
重载而不是 println(java.lang.String)
.
您可以使用 javap -c -private
查看 checkcast
指令的确切位置。
结论
确保您已打开警告。默认情况下没有启用它们的任何东西都有些可疑。不要忽略或禁止警告,尤其是关于原始类型的警告。