什么是类型擦除
Java 泛型依赖编译器实现,只存在于编译期,JVM 中没有泛型概念。具体来说,每定义一个泛型类,编译阶段都会将类型参数替换为具体类型,生成对应的原生类。到运行时,类型参数已不存在。
例如,下面的泛型类:
- public class GenericType<T> {
- private T v;
-
- public GenericType(T v) {
- this.v = v;
- }
-
- public T getV() {
- return v;
- }
-
- public void setV(T v) {
- this.v = v;
- }
-
- public static void main(String[] args) {
- GenericType<String> gt = new GenericType<>("");
- System.out.println(gt.getV());
- }
- }
在编译后生成的原生代码就像这样:
- public class GenericType {
- private Object v;
-
- public GenericType(Object v) {
- this.v = v;
- }
-
- public Object getV() {
- return v;
- }
-
- public void setV(Object v) {
- this.v = v;
- }
-
- public static void main(String[] args) {
- GenericType gt = new GenericType("");
- System.out.println((String)gt.getV());
- }
- }
为什么要擦除类型
- 在 Java 诞生 10 年后,才想实现类似 C++ 模板的概念,即
泛型
- Java 的类库是 Java 生态中非常宝贵的财富,必须保证
向后兼容
(即现有代码依旧合法)和迁移兼容
(泛化代码和非泛化代码可互相调用)
基于以上背景,Java 设计者采取了 类型擦除
这种折中的实现方式。
类型参数替换规则
编译后,类型参数会被擦除并替换为 最小上限
,如果没有指定,则上限为 Object,就像上面的代码。可通过 <T extends Father>
表达式指定 T 的最小上限,就像下面这样。
- class Father {}
- class Son extends Father {}
-
- public class GenericType<T extends Father> {
- private T v;
-
- public GenericType(T v) {
- this.v = v;
- }
-
- public T getV() {
- return v;
- }
-
- public void setV(T v) {
- this.v = v;
- }
-
- public static void main(String[] args) {
- GenericType<Son> gt = new GenericType<>(null);
- gt.setV(new Son());
- System.out.println(gt.getV());
- }
- }
则编译后 T 会被替换为最小上限 Father。
- class Father {}
- class Son extends Father {}
-
- public class GenericType {
- private Father v;
-
- public GenericType(Father v) {
- this.v = v;
- }
-
- public Father getV() {
- return v;
- }
-
- public void setV(Father v) {
- this.v = v;
- }
-
- public static void main(String[] args) {
- GenericType gt = new GenericType(null);
- gt.setV(new Son());
- System.out.println((Son)gt.getV());
- }
- }
编译器做了两件事,第一,保证 传入值
类型为 声明类型
,否则无法通过编译。如声明 GenericType<Son> gt
,则传入值必须为 Son 或其子类
。对应 GenericType 类,虽然其 构造器
和 setV
方法参数被擦除为 Father
,但因声明为 GenericType<Son> gt
,编译器只允许传入 Son 及其子类
。
第二,在所有调用 传出
值方法的地方插入 转型
字节码,同样转为 声明类型
。对应 GenericType 类,因声明为 GenericType<Son> gt
,每个调用 getV
方法处,都会插入转为 Son
类型的字节码。
什么是协变
简单来说,协变即 子类型 ≦ 基类型
,赋值时可以 自动向上转型
。Java 数组支持协变,如下代码中,Son[]
型数组可以赋值给 Father[]
型,因 Son 是 Father 的子类,但编译器仍允许该引用放入 Father 对象,这将导致运行时异常。
- class Father {}
- class Son extends Father {}
-
- public class GenericType {
- public static void main(String[] args) {
- Father[] fathers = new Son[2];
- fathers[0] = new Son();
- // 下行可以通过编译,但会抛 ArrayStoreException
- fathers[1] = new Father();
- }
- }
Java 非通配符泛型不支持协变
,这意味着以下语句无法通过编译。
- List<Father> fathers = new ArrayList<Son>();
而 上边界限定通配符泛型
支持,以下语句可以通过编译。
- List<? extends Father> fathers = new ArrayList<Son>();
但此时,编译器无法得知 List<? extends Father>
存储的究竟是 Father
还是它的某个 子类
对象,但一定是某一个,所以它不允许向其中添加任何对象,除了 null。可以理解为编译器不知道 ?
对应的特定类是谁,无法保证添加进去的对象可以转型为该特定类,所以不允许添加。
- // 以下 3 行无法通过编译
- fathers.add(new Son());
- fathers.add(new Father());
- fathers.add(new Object());
-
- // 只能向其中添加 null
- fathers.add(null);
与此同时,编译器允许从中取出元素赋值给 Father 引用。正如上面所说,List<? extends Father>
中存储的是 Father,或其某个子类的对象,所以取出后上转给 Father 是安全的,且编译时会自动生成转型字节码。
- // 但可取出 Father 或它的某个子类对象,自动上转为 Father 型
- Father father = fathers.get(0);
什么是逆变
与协变相反,逆变即父类可下转为子类。Java 数组和非通配符泛型都不支持逆变,比如下面的代码都无法通过编译。
- Son[] sons = new Father[100];
- List<Son> son = new ArrayList<Father>();
而 下边界限定通配符泛型
支持,以下语句可以通过编译。
- List<? super Son> sons = new ArrayList<Father>();
List<? super Son>
表示该 List 中持有某个未知具体类型,它可能是 Son,也可能是 Son 的父类。这意味着向其中放入 Son 或其子类
对象都是合法的。而放入 Father
对象不合法,因为那个具体父类不一定是 Father
或其子类。
- class Father {}
- class Son extends Father {}
- class Grandchild extends Son {}
-
- public class GenericType {
- public static void main(String[] args) {
- List<? super Son> sons = new ArrayList<Father>();
- // 下面两行能够通过编译
- sons.add(new Son());
- sons.add(new Grandchild());
-
- // 下面不行
- sons.add(new Father());
- }
- }
与此同时,从 List<? super Son>
中不一定可取出能下转为 Son
的对象,因为它的类型是 Son 或其某个父类
,父类转子类是不安全的。它也不一定能取出可转为 Father
的对象,因为那个父类不一定是 Father
。唯一能做的是将取出的对象上转为 Object
。
- // 下面两行不能通过编译
- Son son = sons.get(0);
- Father father = sons.get(1);
-
- // 下面可以
- Object object = sons.get(2);
无边界通配符泛型
<?>
表示某种特定类型,但编译器并不知道是哪种类型,这意味着向此种容器中放任何对象都是非法的,除了 null。与此同时,从中取出的对象只能上转为 Object,因为无法确定它的类型。
- List<?> list = new ArrayList<Son>();
- // 下面两行无法通过编译
- list.add(new Son());
- list.add(new Object());
-
- // 下行合法
- list.add(null);
-
- // 下行无法通过编译
- Son son = list.get(0);
-
- // 下行合法
- Object object = list.get(0);
如果不使用泛型,则容器使用 Object[]
存放元素,它可以接收任何类型对象,但取出时需要强转为具体类型,以下操作均合法。
- List list = new ArrayList();
- list.add(new Son());
- list.add(new Father());
-
- Son son = (Son) list.get(0);
List<?> list
和 List list
的另一个区别在于,前者中的所有元素类型相同,而后者并无此要求,因为任何对象均可上转为 Object。
参考资料
- Chapter 15, Generics —— Thinking in Java, Version 4
如有问题请在下方留言,文章转载请注明出处,详细交流请加下方群组!请大佬不要屏蔽文中广告,因为它将帮我分担服务器开支,如果能帮忙点击我将万分感谢。
强调几点:(该留言由系统自动生成!)
1. 请不要刷广告,本站没有流量!
2. 我不回复虚假邮箱,因为回复了你也看不到!
3. 存在必须回复的隐藏内容时,可以直接使用表情框里的阿鲁表情!