generics究竟是如何运作的?
在查找(测试)另一个问题的信息时,我遇到了一些事情,完全不知道为什么会发生这种情况。 现在,我知道没有实际的理由这样做,这是绝对可怕的代码,但为什么这是有效的:
ArrayList test=new ArrayList(); ArrayList obj=new ArrayList(); test=obj; obj.add(new Object()); System.out.println(test.get(0));
所以,基本上,我将一个Object添加到Quods的ArrayList中。 现在,我看到java如何无法有效地检查这一点,因为它必须查看所有引用,这些引用可能甚至不存储在任何地方。 但是为什么get()有效呢? 是不是get()假设返回一个Quod实例,就像它在Eclipse中将鼠标放在它上面时所说的那样? 如果它在承诺返回Quod类型的对象时可以返回一个只是对象的对象,那么当我说我将返回一个int时,为什么我不能返回一个String?
事情变得更加怪异。 这会因为运行时错误(java.lang.ClassCastException错误)(!?!?)而崩溃:
ArrayList test=new ArrayList(); 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错误。
Javagenerics是通过类型擦除实现的,即类型参数仅用于编译和链接,但是被擦除以便执行。 也就是说,编译时类型和运行时类型之间没有1:1的对应关系。 特别是,generics类型的所有实例共享相同的运行时类:
new ArrayList().getClass() == new ArrayList().getClass();
在编译时类型系统中,存在类型参数,并用于类型检查。 在运行时类型系统中,缺少类型参数,因此不会检查。
对于演员表和原始类型,这不是问题。 强制转换是类型正确性的断言,并将类型检查从编译时延迟到运行时。 但正如我们所看到的,编译时和运行时类型之间没有1:1的对应关系; 在编译期间会删除类型参数。 因此,运行时无法完全检查包含类型参数的强制转换的正确性,并且错误的强制转换可能会成功,从而违反了编译时类型系统。 Java语言规范称这种堆污染 。
因此,运行时不能依赖于类型参数的正确性。 但是,它必须强制运行时类型系统的完整性以防止内存损坏。 它通过延迟类型检查直到实际使用generics引用来完成此操作,此时运行时知道它必须支持的方法或字段,并且可以检查它实际上是声明该字段或方法的类或接口的实例。
有了它,回到你的代码示例,我稍微简化了(这不会改变行为):
ArrayList test = new ArrayList (); ArrayList obj = test; obj.add(new Object()); System.out.println(test.get(0));
声明的obj
类型是原始类型ArrayList
。 原始类型在编译时禁用类型参数的检查。 因此,我们可以将Object
传递给它的add方法,即使ArrayList
只能在编译时类型系统中保存Quod
实例。 也就是说,我们成功地欺骗了编译器并完成了堆污染。
这就离开了运行时类型系统。 在运行时类型系统中,ArrayList使用Object
类型的引用,因此将Object
传递给add
方法是完全可以的。 所以调用get()
,它也返回Object
。 事情就是分歧:在你的第一个代码示例中,你有:
System.out.println(test.get(0));
test.get(0)
的编译时类型是Quod
,唯一匹配的println方法是println(Object)
,因此它是嵌入在类文件中的方法的签名。 因此,在运行时,我们将Object
传递给println(Object)
方法。 这是完全可以的,因此没有抛出任何exception。
在第二个代码示例中,您有:
System.out.println(test.get(0).toString());
同样, test.get(0)
的编译时类型是Quod
,但现在我们调用它的toString()方法。 因此,编译器指定要调用在(或inheritance)类型Quod
声明的toString
方法。 显然,这个方法要求指向Quod
一个实例,这就是为什么编译器在调用方法之前将一个额外的Quod
转换插入到字节代码中的原因 – 这个转换抛出了ClassCastException
。
也就是说,运行时允许第一个代码示例,因为引用不是以特定于Quod
的方式使用,而是拒绝第二个,因为引用用于访问Quod
类型的方法。
也就是说,您不应该依赖编译器何时插入此合成强制转换,而是通过编写类型正确的代码来防止堆污染首先发生。 只要代码可能导致堆污染,Java编译器就会通过发出未经检查和原始类型的警告来帮助您。 摆脱警告,你将不必了解这些细节;-)。
问题的关键是:
为什么println()方法调用它的toString很好,但不是直接用于我?
由于调用toString()
而由于编译器添加了显式ClassCastException
因此未发生ClassCastException
exception。
一张图片胜过千言万语,让我们来看看一些反编译的代码。
考虑以下代码:
public static void main(String[] args) { List s = new ArrayList (); s.add("kshitiz"); List i = new ArrayList(s); System.out.println(i.get(0)); //This works System.out.println(i.get(0).toString()); // This blows up!!! }
现在看看反编译的代码:
public static void main(String[] args) { ArrayList s = new ArrayList(); s.add("kshitiz"); ArrayList i = new ArrayList(s); System.out.println(i.get(0)); System.out.println(((Integer)i.get(0)).toString()); }
看到显式转换为Integer
? 现在为什么没有编译器在前一行中添加一个强制转换? println()
方法的签名是:
public void println(Object x)
由于println
需要一个Object
,而i.get(0)
结果是Object
,因此不会添加任何i.get(0)
。
您也可以调用toString()
,授予您这样做,以便不生成强制转换:
public static void main(String[] args) { List s = new ArrayList (); s.add("kshitiz"); List i = new ArrayList(s); myprint(i.get(0)); } public static void myprint(Object arg) { System.out.println(arg.toString()); //Invoked toString but no exception }