Java中泛型的擦除概念是什么?
它基本上是通过编译器技巧在Java中实现泛型的方式.编译的通用代码实际上只是java.lang.Object
在你谈论的地方T
(或其他类型参数)使用 - 并且有一些元数据告诉编译器它确实是泛型类型.
当您针对泛型类型或方法编译某些代码时,编译器会找出您的真正含义(即类型参数T
是什么)并在编译时验证您正在做正确的事情,但是发出的代码再次只是会谈就...而言java.lang.Object
- 编译器会在必要时生成额外的强制转换.在执行时,a List
和a List
完全相同; 额外的类型信息已被编译器删除.
将此与C#进行比较,其中信息在执行时保留,允许代码包含typeof(T)
等效的表达式T.class
- 除了后者无效.(请注意,.NET泛型和Java泛型之间存在进一步的差异.)在处理Java泛型时,类型擦除是许多"奇怪"警告/错误消息的来源.
其他资源:
Oracle文档
维基百科
Gilad Bracha的Java泛型指南(PDF - 强烈推荐;链接可能需要定期更改)
Angelika Langer的Java Generics FAQ
就像旁注一样,实际看到编译器在执行擦除时正在做什么是一个有趣的练习 - 使整个概念更容易掌握.有一个特殊的标志,你可以通过编译器输出已经删除了泛型和插入的强制转换的java文件.一个例子:
javac -XD-printflat -d output_dir SomeFile.java
的-printflat
是,被移交到生成文件编译器的标志.(该-XD
部分是告诉javac
将其交给实际进行编译的可执行jar而不仅仅是javac
,但我离题了......)这-d output_dir
是必要的,因为编译器需要一些地方来放置新的.java文件.
当然,这不仅仅是擦除; 编译器完成的所有自动操作都在这里完成.例如,还插入了默认构造函数,新的foreach样式for
循环扩展为常规for
循环等.很高兴看到自动发生的小事情.
擦除,字面意思是源代码中存在的类型信息从编译的字节码中擦除.让我们用一些代码来理解这一点.
import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class GenericsErasure { public static void main(String args[]) { Listlist = new ArrayList (); list.add("Hello"); Iterator iter = list.iterator(); while(iter.hasNext()) { String s = iter.next(); System.out.println(s); } } }
如果您编译此代码然后使用Java反编译器对其进行反编译,您将得到类似的结果.请注意,反编译代码不包含原始源代码中存在的类型信息的跟踪.
import java.io.PrintStream; import java.util.*; public class GenericsErasure { public GenericsErasure() { } public static void main(String args[]) { List list = new ArrayList(); list.add("Hello"); String s; for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s)) s = (String)iter.next(); } }
要完成已经非常完整的Jon Skeet的答案,您必须意识到类型擦除的概念源于需要与以前版本的Java兼容.
最初在EclipseCon 2007上展示(不再可用),兼容性包括以下几点:
源兼容性(很高兴......)
二进制兼容性(必须!)
迁移兼容性
现有的计划必须继续发挥作用
现有库必须能够使用泛型类型
一定有!
原始答案:
因此:
new ArrayList() => new ArrayList()
有更大的具体化的命题.实现"将抽象概念视为真实",其中语言结构应该是概念,而不仅仅是语法糖.
我还应该提到checkCollection
Java 6 的方法,它返回指定集合的动态类型安全视图.任何插入错误类型元素的尝试都会立即导致ClassCastException
.
语言中的泛型机制提供了编译时(静态)类型检查,但是可以使用未经检查的强制转换来破坏此机制.
通常这不是问题,因为编译器会对所有此类未经检查的操作发出警告.
但是,有时单独进行静态类型检查是不够的,例如:
当集合传递给第三方库时,库代码必须通过插入错误类型的元素来破坏集合.
程序因a失败ClassCastException
,表示将错误输入的元素放入参数化集合中.不幸的是,异常可以在插入错误元素之后的任何时间发生,因此它通常提供关于问题的真实来源的很少或没有信息.
大约四年后的2012年7月更新:
现在(2012)详细介绍了" API迁移兼容性规则(签名测试) "
Java编程语言使用擦除来实现泛型,这确保了旧版本和通用版本通常生成相同的类文件,除了有关类型的一些辅助信息.二进制兼容性未被破坏,因为可以使用泛型类文件替换遗留类文件,而无需更改或重新编译任何客户端代码.
为了便于与非通用遗留代码接口,还可以使用参数化类型的擦除作为类型.这种类型称为原始类型(Java语言规范3/4.8).允许原始类型还可确保源代码的向后兼容性.
根据这个,
java.util.Iterator
该类的以下版本是二进制和源代码向后兼容:
Class java.util.Iterator as it is defined in Java SE version 1.4: public interface Iterator { boolean hasNext(); Object next(); void remove(); } Class java.util.Iterator as it is defined in Java SE version 5.0: public interface Iterator{ boolean hasNext(); E next(); void remove(); }
补充已经补充的Jon Skeet答案......
已经提到通过擦除来实现泛型导致一些恼人的限制(例如,否new T[42]
).还有人提到,以这种方式做事的主要原因是字节码的向后兼容性.这也是(大多数)真实的.生成的字节码-target 1.5与仅脱糖的cast -target 1.4略有不同.从技术上讲,甚至可以(通过巨大的技巧)在运行时获得对泛型类型实例的访问,证明字节码中确实存在某些东西.
更有趣的一点(尚未提出)是使用擦除实现泛型在高级类型系统可以实现的方面提供了更多的灵活性.一个很好的例子就是Scala的JVM实现与CLR.在JVM上,由于JVM本身不对泛型类型施加任何限制(因为这些"类型"实际上不存在),因此可以直接实现更高类型.这与CLR形成对比,CLR具有参数实例化的运行时知识.因此,CLR本身必须具有如何使用泛型的一些概念,从而使用未预料到的规则扩展系统的尝试无效.因此,Scala在CLR上的更高种类是使用在编译器本身内模拟的奇怪形式的擦除来实现的,
当你想在运行时做顽皮的东西时,擦除可能会很不方便,但它确实为编译器编写者提供了最大的灵活性.我猜这是为什么它不会很快消失的部分原因.
据我所知(作为.NET人员),JVM没有泛型的概念,因此编译器用Object替换类型参数并为您执行所有的转换.
这意味着Java泛型只是语法糖,并且不会为通过引用传递时需要装箱/取消装箱的值类型提供任何性能改进.