泛型究竟是如何运作的?

时间:2014-12-22 16:21:14

标签: 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));

所以,基本上,我将一个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());

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


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


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

2 个答案:

答案 0 :(得分:14)

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传递给其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)方法。这是完全可以的,因此不会抛出异常。

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

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

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

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

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

答案 1 :(得分: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());
}

查看显式转换为Integer?现在为什么没有编译器在前一行中添加一个演员?方法println()的签名是:

public void println(Object x)

由于println期待Objecti.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
}