Java泛型通过类型擦除来实现,即类型参数仅用于编译和链接,而被擦除以执行。即,编译时间类型和运行时类型之间没有1:1的对应关系。特别是,泛型类型的所有实例共享相同的运行时类:
new ArrayList<Quod>().getClass() == new ArrayList<String>().getClass();
在编译时类型系统中,存在类型参数,并用于类型检查。在运行时类型系统中,不存在类型参数,因此不进行检查。
除了类型转换和原始类型,这将没有问题。强制转换是类型正确性的断言,并将类型检查从编译时推迟到运行时。但是正如我们所看到的,编译时间和运行时类型之间没有1:1的对应关系;类型参数在编译期间被擦除。因此,运行时无法完全检查包含类型参数的强制转换的正确性,并且不正确的强制转换可以成功,这违反了编译时类型系统。Java语言规范将此称为
堆污染 。
结果,运行时不能依赖类型参数的正确性。但是,它必须强制运行时类型系统的完整性,以防止内存损坏。它通过延迟类型检查直到实际使用泛型引用来完成此操作,此时运行时知道它必须支持的方法或字段,并且可以检查它实际上是声明该字段或方法的类或接口的实例。
。
这样,回到您的代码示例,我对其进行了稍微简化(这不会改变行为):
ArrayList<Quod> test = new ArrayList<Quod>();ArrayList obj = test; obj.add(new Object());System.out.println(test.get(0));
的声明类型为
obj原始类型
ArrayList。原始类型会在编译时禁用类型参数检查。结果,
Object即使
ArrayList可能仅将
Quod实例保存在编译时类型系统中,我们也可以将其传递给其add方法。也就是说,我们已经成功地对编译器说谎并实现了堆污染。
剩下的是运行时类型系统。在运行时类型系统中,ArrayList与type的引用一起使用
Object,因此将an传递
Object给
add方法是完全可以的。调用
get()也是如此,它也会返回
Object。这是有分歧的:在您的第一个代码示例中,您有:
System.out.println(test.get(0));
的编译时间类型
test.get(0)为
Quod,唯一匹配的println方法为
println(Object),因此嵌入在类文件中的是该方法的签名。因此,在运行时,我们将传递
Object给
println(Object)方法。完全可以,因此不会引发异常。
在第二个代码示例中,您具有:
System.out.println(test.get(0).toString());
同样,的编译时间类型
test.get(0)为
Quod,但是现在我们正在调用其toString()方法。因此,编译器指定要调用
toString以(或继承为)类型声明的方法
Quod。显然,此方法需要
this指向的一个实例
Quod,这就是为什么编译器
Quod在调用该方法之前在字节码中插入一个额外的强制类型转换的原因-
该强制类型转换抛出一个
ClassCastException。
也就是说,运行时允许使用第一个代码示例,因为未以特定于
Quod引用的方式使用引用,但是由于引用被用于访问type方法而拒绝了第二个示例
Quod。
就是说,您不应该依赖编译器何时确切地插入此合成强制转换,而应该通过编写类型正确的代码来防止堆污染的发生。需要Java编译器通过在代码可能导致堆污染的情况下发出未经检查的原始警告来帮助您。摆脱警告,您将不必了解那些细节;-)。



