泛型究竟是如何运作的?

WiE*_*rD0 15 java eclipse generics arraylist

在查找(测试)另一个问题的信息时,我遇到了一些问题,完全不知道为什么会这样.现在,我知道没有实际的理由这样做,这是绝对可怕的代码,但为什么这是有效的:

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

System.out.println(test.get(0));
Run Code Online (Sandbox Code Playgroud)

所以,基本上,我将一个Object添加到Quods的ArrayList中.现在,我看到java如何无法有效地检查这一点,因为它必须查看所有引用,这些引用可能甚至不存储在任何地方.但是为什么get()有效呢?是不是get()假设返回一个Quod实例,就像它在Eclipse中将鼠标放在它上面时所说的那样?如果它在承诺返回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());
Run Code Online (Sandbox Code Playgroud)

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


编辑:我知道我没有对我创建的第一个ArrayList实例做任何事情,所以它实际上只是浪费处理时间.


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

mer*_*ike 15

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

new ArrayList<Quod>().getClass() == new ArrayList<String>().getClass();
Run Code Online (Sandbox Code Playgroud)

在编译时类型系统中,存在类型参数,并用于类型检查.在运行时类型系统中,缺少类型参数,因此不会检查.

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

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

有了它,回到你的代码示例,我稍微简化了(这不会改变行为):

ArrayList<Quod> test = new ArrayList<Quod>();
ArrayList obj = test; 
obj.add(new Object());
System.out.println(test.get(0));
Run Code Online (Sandbox Code Playgroud)

声明的类型obj是原始类型ArrayList.原始类型在编译时禁用类型参数的检查.因此,我们可以传递Object给它的add方法,即使它ArrayList只能Quod在编译时类型系统中保存实例.也就是说,我们成功地欺骗了编译器并完成了堆污染.

这留下了运行时类型系统.在运行时类型系统中,ArrayList使用类型的引用Object,因此传递Objectadd方法是完全可以的.所以是调用get(),也返回Object.这里有不同的东西:在你的第一个代码示例中,你有:

System.out.println(test.get(0));
Run Code Online (Sandbox Code Playgroud)

编译时类型test.get(0)Quod唯一匹配的println方法println(Object),因此它是嵌入在类文件中的方法的签名.因此,在运行时,我们将传递Objectprintln(Object)方法.这是完全可以的,因此没有抛出任何异常.

在第二个代码示例中,您有:

System.out.println(test.get(0).toString());
Run Code Online (Sandbox Code Playgroud)

同样,编译时类型test.get(0)Quod,但现在我们调用它的toString()方法.因此,编译器指定要调用toString在(或继承到)类型中声明的方法Quod.显然,这个方法需要this指向一个实例Quod,这就是编译器Quod在调用方法之前在字节代码中插入一个额外的强制转换的原因 - 这个转换抛出了一个ClassCastException.

也就是说,运行时允许第一个代码示例,因为引用不是以特定的方式使用Quod,而是拒绝第二个,因为引用用于访问类型的方法Quod.

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

  • @pbabcdefp:Java语言通过尽可能保持二进制兼容性来支持接口和类的演化.在将源代码转换为类文件时,它因此尽可能少地假设存在,子类型关系和其他类型的成员.具体来说,如果编译器要编码(如您所建议的)声明该方法的最不具体的超类的名称,那么如果该类型不再是超类型,或者不再声明该方法(例如,因为该方法已被移动到子类) (3认同)
  • 我真的不明白这一点.`toString()`不是特定于`Quod`.`toString()`是`Object`的方法.在运行时,`toString()`方法是根据`Quod`实例的运行时类型动态选择的.例如,如果类`Rod`扩展`Quod`,编译器无法知道将调用哪个`toString()`方法(`Quod`中的那个或`Rod`中的那个),所以编译器无法指定要调用的`toString()`方法.那么首先投射到`Quod`有什么意义呢? (2认同)
  • 可以说,可以对toString()方法进行异常,因为它的存在是由Java语言规范强制实现的.不过,这种特殊情况可能不值得额外的复杂性. (2认同)