美文网首页javase+IO
Java的泛型类型擦除及类型擦除带来的问题

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

作者: code希必地 | 来源:发表于2020-10-15 13:39 被阅读0次

    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>();
    list1.add("Hello");//编译成功
    list1.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查看字节码文件反编译的结果:

    桥方法.png
    可以看到子类中新增了两个桥方法:
    public void setFirst(Object first) {
        setFirst((String)first);
    }
    
    public Object getFirst() {
        //这里返回的 String 类型的 getFirst 方法
        return getFirst();
    }
    

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

    相关文章

      网友评论

        本文标题:Java的泛型类型擦除及类型擦除带来的问题

        本文链接:https://www.haomeiwen.com/subject/fakwpktx.html