在查找(测试)信息时,我遇到了一些问题,完全不知道为什么会发生。现在,我知道没有实际的理由要这样做,这绝对是可怕的代码,但是为什么行得通呢?

ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0));

因此,基本上,我将对象添加到Quods的ArrayList中。现在,我知道java如何无法有效地检查它,因为它必须浏览所有引用,而这些引用可能甚至都没有存储在任何地方。但是为什么get()起作用了。 get()是否不是要返回Quod的实例,就像在Eclipse中将鼠标放在Quod的实例上所说的那样?如果它答应返回Quod类型的对象时可以返回仅是一个对象的对象,那么当我说要返回int时为什么不能返回String?

事情变得更奇怪了。这会因为发生运行时错误(java.lang.ClassCastException错误)(!?!?)而崩溃:
ArrayList<Quod> test=new ArrayList<Quod>();
ArrayList obj=new ArrayList();
test=obj;
obj.add(new Object());

System.out.println(test.get(0).toString());

为什么不能在对象上调用toString?为什么println()方法调用它的toString可以,但是我不能直接调用它呢?

编辑:我知道我对我创建的ArrayList的第一个实例不做任何事情,因此,这实际上只是在浪费处理时间。

编辑:我在Java 1.6上使用Eclipse其他人说过,他们在运行Java 1.8的Eclipse中得到相同的结果。但是,在其他一些编译器上,两种情况均会引发CCE错误。

最佳答案

Java泛型是通过类型擦除实现的,即类型参数仅用于编译和链接,而被擦除以执行。即,编译时间类型和运行时类型之间没有1:1的对应关系。特别是,泛型类型的所有实例共享相同的运行时类:

new ArrayList<Quod>().getClass() == new ArrayList<String>().getClass();

在编译时类型系统中,存在类型参数,并用于类型检查。在运行时类型系统中,不存在类型参数,因此不进行检查。

除了类型转换和原始类型,这将没有问题。强制转换是类型正确性的断言,并将类型检查从编译时推迟到运行时。但是正如我们所看到的,编译时间和运行时类型之间没有1:1的对应关系;类型参数在编译期间被擦除。因此,运行时无法完全检查包含类型参数的强制转换的正确性,并且不正确的强制转换可以成功,从而违反了编译时类型系统。 Java语言规范将此称为堆污染。

结果,运行时不能依赖类型参数的正确性。但是,它必须强制运行时类型系统的完整性,以防止内存损坏。它通过延迟类型检查直到实际使用泛型引用来完成此操作,此时运行时知道它必须支持的方法或字段,并且可以检查它实际上是声明该字段或方法的类或接口(interface)的实例。 。

这样,回到您的代码示例,我对其进行了一些简化(这不会改变行为):
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可以使用Object类型的引用来工作,因此将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特定的方式使用引用,但是由于引用被用于访问Quod类型的方法,因此运行时拒绝了第二个示例。

就是说,您不应该依赖编译器何时确切地插入此合成强制转换,而应该通过编写类型正确的代码来防止堆污染的发生。需要Java编译器通过在代码可能导致堆污染的情况下发出未经检查的原始警告来帮助您。摆脱警告,您将不必了解那些细节;-)。

10-01 05:44