Java泛型擦除简介
在 Java 中的 泛型 ,常常被称之为 伪泛型 ,究其原因是因为在实际代码的运行中,将实际类型参数的信息擦除掉了 (Type Erasure) 。
Java的泛型只在编译时有效,到了运行时这个泛型类型就会被擦除掉,即List和List在运行时其实都是List类型。
那是什么原因导致了 Java 泛型擦除呢?下面我就带着大家以 Java案例来详解。
Java泛型擦除原理
先看一个简单的例子,看看Java泛型擦除的案例,代码如下:
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); } }
我们定义了两个ArrayList数组,一个是ArrayList<String>泛型类型的,只能存储字符串,一个是ArrayList<Integer>泛型类型的,只能存储整数。
上面代码最终结果输出的是什么?不了解泛型的和很熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。
正确答案是 true,难道不是false?为什么是这样?
这是因为判断
System.out.println(list1.getClass() == list2.getClass());
泛型类型String和Integer都被擦除掉了,只剩下原始类型。
比如ArrayList<Integer>和ArrayList<String>等类型,在编译后都会变成ArrayList,JVM看到的只是ArrayList,泛型附加的类型信息对JVM来说是不可见的。
那怎么擦除为原始类型呢,我们接着聊。
泛型原始类型详解
原始类型顾名思义就是没有泛型的最初类型,即擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。
没有限定类型时,类型变量被擦除,原始类型为Object,看一个案例:
class Colleage<T>{ private T student; public Colleage(T student){ this.student=student; } public T getStudent() { return student; } public void setStudent(T student) { this.student = student; } }
上例中Colleage类在经过编译之后,就成为原始的Colleage类了,因为泛型形参T是一个无限定的类型变量,所以它的原始类型为Object。
有限定类型时,类型变量被擦除,原始类型是其限定类型。对于有多个限定的类型变量,那么原始类型就用第一个边界的类型变量来替换。
class Colleage <T extends Comparable & Serializable>{
此时Colleage的原始类型就是Comparable
class Colleage <T extends Serializable & Comparable>{
Java泛型擦除问题与解决方案
1.泛型不能用于显式地引用运行时类型的操作之中
例如instanceof、new
class Erased{
public void f(T t,String a){
//T t = new T(); //error
//T[] ts = new T[100]; //error
//boolean k = a instanceof T; //error
}
}
我们可以把具体类型的Class传进来解决部分问题
class Erased{
Class kind;
public Erased(Class kind){
this.kind = kind;
}
public void f(String a) throws Exception{
T t = kind.newInstance(); //这个类需要有默认构造器,反射知识
T[] ts = (T[]) Array.newInstance(kind,10);
boolean k = kind.isInstance(a);
}
}
2.基本类型不能作为参数类型
由于擦除的原因,类型参数会被擦除到上一边界,边界是一个类,不兼容基本类型,所以不能声明List。
Java1.5后有自动包装机制,可以用List实现基本类型的泛化,但是要注意int[]与Integer[]是转化的,在一些需要传Integer[]的地方不能传int[].
3.数组
可以声明带泛型的数组引用,但是不能直接创建带泛型的数组对象
List[] stringLists; //可以声明带泛型的数组引用
List[] stringLists2 = new ArrayList[1];//不能直接创建带泛型的数组对象
List[] stringLists3 = new ArrayList[1];
可以通过Array.newInstance(Class,int)来创建T[]
4.重载
class Holder<t,e>{
void f(List list){}
void f(List list){}
}
</t,e>
上面的代码将会编译不通过,由于擦除的原因,这两个方法的类型签名是一样的。
5.基类劫持了接口
interface Run{
void with(T t);
}
class Animal implements Run{
@Override
public void with(Integer integer) {}
}
public class Dog extends Animal implements Run{}//报错
子类与父类继承用一个泛型接口,如果两个泛型类型不同,将会被父类类型劫持
mikechen睿哥
mikechen睿哥,十余年BAT架构经验,资深技术专家,就职于阿里、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》