Java的泛型类型擦除及类型擦除带来的问题

1、泛型的类型擦除

Java的泛型是伪泛型,不同于C++的模板机制,这是因为Java的泛型只存在编译期间,在编译完成后泛型就会被擦除。引入泛型是为了将类型检查提前到编译期间,将类型转换交由编译器处理,那么为什么还要进行泛型的擦除呢?泛型擦除的目的是为了向下兼容老的Java版本,老的Java版本是没有泛型概念的。
下面通过一个例子证明泛型的擦除

public class Test {

    public static void main(String[] args) {

        ArrayList list1 = new ArrayList();
        list1.add("abc");

        ArrayList list2 = new ArrayList();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}

在这个例子中,我们定义了两个ArrayList数组,一个是ArrayList泛型类型的,只能存储字符串;一个是ArrayList泛型类型的,只能存储整数,最后,我们通过list1对象和list2对象的getClass()方法获取他们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下原始类型,在运行时ArrayList和ArrayList对应的class都是ArrayList.class。
既然泛型的类型在编译完成后就会被擦除,这样一来我们是不是就可以在运行时向ArrayList中添加字符串呢?当然可以,代码如下:

public class Test {

    public static void main(String[] args) throws Exception {

        ArrayList list = new ArrayList();

        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 {  
    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 {  
    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 list = new ArrayList();  
list.add("123");  
list.add(123);//编译错误  

ArrayList泛型擦除会导致String被替换成Object,为什么只能向list中添加字符串呢?
因为在泛型擦除前,编译器会先进行类型检查,然后再擦除,再进行编译。

3.2、泛型的类型擦除前,类型检查的原理

先看如下代码

ArrayList list1 = new ArrayList(); 
list1.add("Hello");//编译成功
list1.add(1);//编译失败
ArrayList list2 = new ArrayList();
list2.add("Hello");//编译成功
list2.add(1);//编译成功

从上面代码可以看出:ArrayList list2 = new ArrayList()的泛型的类型检查是不成功的,我们依然可以向list2中添加任意数据。这又是为什么呢?
new ArrayList()只是开辟了一个内存空间,可以存储任何类型的对象,而类型检查是针对它的引用,引用list2并没有使用泛型,所以并不能实现类型检查的功能。
例子:

public class Test {  

    public static void main(String[] args) {  

        ArrayList list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList().add("11"); //编译通过  
        new ArrayList().add(22); //编译错误  

        String str2 = new ArrayList().get(0); //返回类型就是String  
    }  

} 

从上面代码可以看出类型检查是针对引用的,谁是一个引用,用这个引用调用泛型方法时,就会对这个引用调用的方法进行类型检查,而无关它真正引用的对象。

3.3、泛型的类型转换

泛型擦除会导致泛型类型被替换成Object或者上边界类型,那么为什么在获取值时并不需要进行强转呢?
看下面的例子:

public class Main2 {
    public static void main(String[] args) {
        List 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{
    private T t;
    
    public void setValue(T t){
        this.t=t;
    } 
    
    public T getValue(){
        return t;
    }
}

定义一个Child类实现Parent,并将泛型的具体类型指定为String类型。

class Child extends Parent {

     @Override
    public void setValue(String first){
        super.setValue(first);
    } 
     @Override
    public String getValue(){
        return super.getValue();
    }
}

泛型擦除会导致Parent的泛型被替换成Object,所以子类继承Parent时重写父类的方法应该为

class Child extends Parent {

    @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查看字节码文件反编译的结果:

桥方法.png

可以看到子类中新增了两个桥方法:

public void setValue(Object first) {
    setFirst((String)first);
}

public Object getValue() {
    //这里返回的 String 类型的 getFirst 方法
    return getFirst();
}

这两个桥方法,相当于重写了父类的两个方法,最终还是会调用子类的方法,相当于实现了子类对父类的重写。
桥方法为子类和父类之间架起了一座连通的桥梁,真正实现了泛型继承中的动态绑定,也很好的解决了类型擦除与多态之间的冲突。

你可能感兴趣的:(Java的泛型类型擦除及类型擦除带来的问题)