泛型究竟是如何工作的?

2022-09-02 21:34:12

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

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 类型的对象时,它可以返回一个仅是对象的对象,为什么当我说我将返回一个 int 时,我不能返回一个字符串?

事情变得更加奇怪。这崩溃,因为它假设与运行时错误(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 错误。


答案 1

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));

声明的类型是 原始类型 。原始类型禁止在编译时检查类型参数。因此,我们可以将一个 to its add 方法传递给它,即使 可能只在编译时类型系统中保存实例。也就是说,我们已经成功地欺骗了编译器,并完成了堆污染。objArrayListObjectArrayListQuod

这就离开了运行时类型系统。在运行时类型系统中,ArrayList 使用类型的引用,因此将 a 传递给该方法是完全可以的。调用也是如此,这也返回 。以下是不同的情况:在你的第一个代码示例中,你有:ObjectObjectaddget()Object

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

编译时类型为 ,唯一匹配的 println 方法是 ,因此嵌入到类文件中的是该方法的签名。因此,在运行时,我们将 一个传递给该方法。这是完全可以的,因此不会引发异常。test.get(0)Quodprintln(Object)Objectprintln(Object)

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

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

同样,编译时类型是 ,但现在我们正在调用它的 toString() 方法。因此,编译器指定要调用以类型声明(或继承到)类型的方法。显然,此方法需要指向 的实例,这就是为什么编译器在调用该方法之前将额外的强制转换为字节代码的原因 - 并且此强制转换会抛出一个 .test.get(0)QuodtoStringQuodthisQuodQuodClassCastException

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

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


答案 2

问题的症结在于:

为什么 println() 方法可以调用它的 toString,而我却不能直接调用它?

ClassCastException 异常不是由于调用 toString() 而发生的,而是由于编译器添加的显式强制转换。

一张图片胜过千言万语,所以让我们来看看一些反编译的代码。

请考虑以下代码:

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> 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());
}

请参阅显式转换为 ?那么为什么编译器没有在上一行中添加强制转换呢?该方法的签名是:Integerprintln()

public void println(Object x)

因为 是 期望 和 结果是 没有添加强制转换。printlnObjecti.get(0)Object

您也可以调用 ,前提是您这样做,因此不会生成强制转换:toString()

public static void main(String[] args) {
    List<String> s = new ArrayList<String>();
    s.add("kshitiz");
    List<Integer> i = new ArrayList(s);

    myprint(i.get(0));
}

public static void myprint(Object arg) {
    System.out.println(arg.toString()); //Invoked toString but no exception
}

推荐