1、泛型的类型擦除
Java的泛型是伪泛型,不同于C++的模板机制,这是因为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>泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型,在运行时ArrayList<String>和ArrayList<Integer>对应的class都是ArrayList.class。
既然泛型的类型在编译完成后就会被擦除,这样一来我们是不是就可以在运行时向ArrayList<Integer>
中添加字符串呢?当然可以,代码如下:
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "asd");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
在程序中定义了一个ArrayList泛型类型实例化为Integer对象,如果直接调用add()方法,那么只能存储整数数据,不过当我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
2、类型擦除后保留的原始类型
原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。如果泛型无限定,则会用Object替换,如果泛型有限定,则会用限定类型替换。
2.1、无限定的泛型擦除后替换成Object
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
上面泛型T是无限定的,所以在编译完成后就被替换成Object,替换后等同于下面的代码:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
2.1、有限定的泛型擦除后替换成第一个边界类型
class Pair<T extends Number & Comparable> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
上面代码中泛型T的上边界为Number和Comparable
,泛型擦除会被第一个边界类型替换,替换后的代码如下:
class Pair {
private Number value;
public Number getValue() {
return value;
}
public void setValue(Number value) {
this.value = value;
}
}
3、泛型擦除后引起的问题
3.1、泛型擦除后如何保证只能使用泛型限定的类型
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
ArrayList<String>
泛型擦除会导致String
被替换成Object
,为什么只能向list中添加字符串呢?
因为在泛型擦除前,编译器会先进行类型检查,然后再擦除,再进行编译。
3.2、泛型的类型擦除前,类型检查的原理
先看如下代码
ArrayList<String> list1 = new ArrayList();
list1.add("Hello");//编译成功
list1.add(1);//编译失败
ArrayList list2 = new ArrayList<String>();
list2.add("Hello");//编译成功
list2.add(1);//编译成功
从上面代码可以看出:ArrayList list2 = new ArrayList<String>()
的泛型的类型检查是不成功的,我们依然可以向list2中添加任意数据。这又是为什么呢?
new ArrayList()
只是开辟了一个内存空间,可以存储任何类型的对象,而类型检查是针对它的引用,引用list2
并没有使用泛型,所以并不能实现类型检查的功能。
例子:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
}
从上面代码可以看出类型检查是针对引用的,谁是一个引用,用这个引用调用泛型方法时,就会对这个引用调用的方法进行类型检查,而无关它真正引用的对象。
3.3、泛型的类型转换
泛型擦除会导致泛型类型被替换成Object或者上边界类型,那么为什么在获取值时并不需要进行强转呢?
看下面的例子:
public class Main2<T> {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aaa");
// list.get(0); //语句1
String str = list.get(0);
}
}
泛型擦除会将泛型String
替换成Object
,所以在调用list.get()
时返回的泛型擦除后的Object
,那为什么String str = list.get(0);
不需要进行强转,这是因为返回前内部已经进行了转换。
3.4、类型擦除和多态的冲突
先看个例子:定义一个泛型类Parent
class Parent<T>{
private T t;
public void setValue(T t){
this.t=t;
}
public T getValue(){
return t;
}
}
定义一个Child类实现Parent,并将泛型的具体类型指定为String类型。
class Child extends Parent<String> {
@Override
public void setValue(String first){
super.setValue(first);
}
@Override
public String getValue(){
return super.getValue();
}
}
泛型擦除会导致Parent的泛型被替换成Object,所以子类继承Parent时重写父类的方法应该为
class Child extends Parent<String> {
@Override
public void setValue(Object first){
super.setValue(first);
}
@Override
public Object getValue(){
return super.getValue();
}
}
可是子类中重写父类两个方法的具体实现确实下面这样的:
@Override
public void setValue(String first){
super.setValue(first);
}
@Override
public String getValue(){
return super.getValue();
}
可见类型擦除和多态产生了冲突,为了解决这个冲突Java编译器使用了桥方法。通过指令javap -c -s Child.class
查看字节码文件反编译的结果:
可以看到子类中新增了两个桥方法:
public void setValue(Object first) {
setFirst((String)first);
}
public Object getValue() {
//这里返回的 String 类型的 getFirst 方法
return getFirst();
}
这两个桥方法,相当于重写了父类的两个方法,最终还是会调用子类的方法,相当于实现了子类对父类的重写。
桥方法为子类和父类之间架起了一座连通的桥梁,真正实现了泛型继承中的动态绑定,也很好的解决了类型擦除与多态之间的冲突。