泛型究竟是如何工作的?

Posted

技术标签:

【中文标题】泛型究竟是如何工作的?【英文标题】:How exactly do Generics work? 【发布时间】:2015-02-20 18:34:58 【问题描述】:

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

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时不能返回一个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 错误。

【问题讨论】:

您没有向ArrayList&lt;Quod&gt; 添加任何内容,而是在重新分配test 时丢弃了对它的引用。 对我来说,在 java 1.6 中的 eclipse 中,前一个输出 java.lang.Object@1e63e3d 并且没有抛出任何错误 你为什么要删除你的答案?这是迄今为止最好的,我会在 5 分钟计时器结束后立即接受它 It's still not that simple.(使用 sun-jdk-7) @pbabcdefp,这是因为println 的特殊版本采用String 作为参数。所有其他类都使用采用Object 的版本。 【参考方案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));

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() 方法。因此,编译器指定要调用在类型Quod 中声明(或继承至)的toString 方法。显然,这个方法需要this 指向Quod 的一个实例,这就是为什么编译器在调用该方法之前将一个额外的转换为Quod 插入到字节码中——这个转换会抛出一个ClassCastException

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

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

【讨论】:

很好的解释。我发现尝试System.out.println(((Object) test.get(0)).toString()); 并看到它成功很有见地。 我不太明白这个。 toString() 不特定于 QuodtoString()Object 的一个方法。在运行时,toString() 方法是根据Quod 实例的运行时类型动态选择的。例如,如果类Rod扩展了Quod,则编译器无法知道将调用哪个toString()方法(Quod中的一个或Rod中的那个),因此编译器无法指定要调用哪个toString() 方法。那么首先投射到Quod 有什么意义呢? @pbabcdefp:Java 语言通过尽可能保持二进制兼容性来支持接口和类的演变。因此,在将源代码翻译成类文件时,它尽可能少地假设存在、子类型关系和其他类型的成员。具体来说,如果编译器要编码(如您所建议的)声明该方法的最不具体的超类的名称,则如果该类型不再是超类型或不再声明该方法(例如因为该方法已移至子类) 这将特别令人烦恼,因为不必更改源代码。 可以说,toString() 方法可能会出现异常,因为 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());

看到Integer 的显式转换了吗?现在为什么编译器没有在前一行中添加强制转换?方法println()的签名是:

public void println(Object x)

由于println 期待Object 并且i.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

【讨论】:

以上是关于泛型究竟是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章

c#的泛型,委托,反射是啥?

day15 java语言中的-------泛型

Python 打字:如何让 Type[C] 与 TypeVars 和泛型一起工作?

泛型在 C++/CX 中的工作原理

如何使用给定的 Type 对象调用泛型方法? [复制]

使用多种泛型类型在 Java 中实现抽象泛型方法