Java继承 | 泛型擦除和转换

一、泛型擦除

在编译期间,所有泛型信息都会被擦除掉,在生成的字节码中是不包括泛型中的类型信息的

直接举个例子

public class Main2<T> {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("aaa");

        List<Integer> list1 = new ArrayList<>();
        list1.add(111);

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

}

结果为 true

再来看一下反编译后的结果

看到第 11 行和第 31 行,在 add 的时候,已经将泛型信息擦除了,最后得到的 add 方法中的类型已经变为 Object 类了

为什么是 Object 呢?

这是因为 List 的参数类是

public interface List<E> extends Collection<E>

此时因为 T 是一个无上限的变量,因此就直接用最大的一个范围来代表,即 Object 类

我们举个例子来验证这个观点

public class Main2<T extends Number> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public static void main(String[] args) {
        Main2<Integer> main2 = new Main2<>();
        main2.setT(12);

        Main2<Double> main21 = new Main2<>();
        main21.setT(12.32);
    }

}

我们对类的参数类型做一个约束,再来看反编译后的结果

可以清晰的看到,在第 14 行和第 32 行,泛型擦除后的类型不再是 Object,而是我们规定的上限 Number 类了

二、擦除前的检查

还是举刚才的例子,既然说是泛型擦除,那么为什么下面那个语句会报错呢?不是说已经将类型变为 Object 了吗?

public class Main2<T> {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add(123);  //编译错误
    }

}

答案是:java编译器会在编译之前检查泛型的类型,只有通过之后才可以进行编译和擦除

三、擦除后进行类型转换

1.对泛型表达式进行转换

既然擦除了泛型类型,那么最终我们得到 String 类,Integer 类又是怎么获取的?还是刚才那个例子,最后我们用 get 方法进行获取

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);
    }

}

对于语句1,调用 get 方法获取的参数的类型就是擦除类型后的 Object 类型,并没有进行类型转换,可以通过反编译来验证

而对于语句 1 的下面一条语句,结果就不同了,其实他经历了两步

  • 调用 get 方法
  • 将返回的 Object 类强制转换为 String 类

通过反编译来验证一下吧

注意第 24 行的 checkcast,直接将 Object 类转换成了 String 类

2.对泛型方法进行转换

假设有这么一个类父类

public class Pair<T> {

    private T first;

    public Pair() {}

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }
    
}

我们使用一个子类来继承这个父类,然后使用多态调用父类的方法

public class Test2 extends Pair<String> {

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        test2.setFirst(new String());
        test2.getFirst();
    }
}

然后我们执行这段代码,然后通过反编译来看下运行的过程

通过第 11 行和第 15行可以看出,当子类调用父类的方法且在编译的时候,传入的父类的泛型方法的类型已经被擦除了,相当于是这样的,因为没有对类型设置上限,因此就是 Object

public class Pair {
    private Object first;

    public Pair() {}

    public Object getFirst() {
        return first;
    }

    public void setFirst(Object first) {
        this.first = first;
    }
}

此时调用父类的方法,得到的类型实则就是 Object 类。当然,你也可以像上面一样,对得到的数据进行类型转换

知道这一点之后,我们在来看一个例子,假如子类重写父类的方法会怎么样

public class Test2 extends Pair<String> {

    @Override
    public void setFirst(String first) {
        super.setFirst(first);
    }

    @Override
    public String getFirst() {
        return super.getFirst();
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        test2.setFirst("aaa");
        test2.getFirst();
    }
}

此时子类在调用的时候,其实由小到大进行动态搜索,包括

// 继承父类的已经类型擦除后的方法
public void setFirst(Object first){...}
// 子类自己新增的方法
public void setFirst(String first){...}
// 其他继承 Object 的方法

乍一看,这并不是多态啊,如果真是多态,重写的应该是 void setFirst(Object first) 方法,但是这里却多出来了 void setFirst(String first) 方法。可见类型擦除和多态之间发生了冲突!!!

那么java编译器又是怎么处理这个冲突的呢?使用桥方法。之间看反编译的结果

可以看到,编译器在 Test2 类中生成了两个桥方法

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

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

这两个桥方法,相当于重写了父类的两个方法,最终还是会调用子类的方法,相当于实现了子类对父类的重写

桥方法为子类和父类之间架起了一座连通的桥梁,真正实现了泛型继承中的动态绑定,也很好的解决了类型擦除与多态之间的冲突

四、其他问题

假如我们将一个定义了泛型参数的对象赋值给一个没有定义泛型参数的对象时

public class Test3 {

    public static void main(String[] args) {
        Pair<String> stringPair = new Pair<>();
        stringPair.setFirst("asv");
        // 1
        String first = stringPair.getFirst();
        // 2
        Pair pair = stringPair;
        // 此时的 pair 是 Object 类的,因此需要手动的进行类型转换
        String s = (String)pair.getFirst();
    }

}

结果是一样的,不同的是,第1步会先调用 getFirst 方法,然后会泛型擦除,之后就是进行类型转换;对于第2步,把 Pair 赋值给 Pair,这个时候原来的泛型类型(String)就会被扔掉,变为 Pair

另一个例子

public class Test3 {

    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("aaa");

        List list = stringList;
        list.add(123);
        list.add(123.123);

        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + " ");
        }
        
        String s = stringList.get(0);
        String s1 = stringList.get(1);  //运行时错误
    }

}

结果是:aaa 123 123.123

可以看到,当把 List 赋给 List 之后,小的范围变成了大的范围,此时 List 也变成了 Object 类型的了,但这只是代表能够容纳的参数的类型变多了,如下如所示

因为 stringList 对应的泛型类型还是 String 类型的,因此在泛型擦除后进行的类型转换,还是转为 String 类

因为里面有 Integer 类和 Double 类型,此时会报错

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

五、参考

https://blog.csdn.net/lonelyroamer/article/details/7868820
https://www.cnblogs.com/xz816111/p/7643323.html
https://www.cnblogs.com/LinkinPark/p/5232980.html

猜你喜欢

转载自blog.csdn.net/babycan5/article/details/82953563