泛型究竟是如何工作的?
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<Quod>
添加任何内容,而是在重新分配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()
不特定于 Quod
。 toString()
是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
【讨论】:
以上是关于泛型究竟是如何工作的?的主要内容,如果未能解决你的问题,请参考以下文章