什么是类型擦除
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
如有问题请在下方留言,文章转载请注明出处,详细交流请加下方群组!请大佬不要屏蔽文中广告,因为它将帮我分担服务器开支,如果能帮忙点击我将万分感谢。
大佬厉害了,啥都懂啊!@(滑稽)
说笑了,大佬