类型擦除 知多滴!

作者: 真心czx | 来源:发表于2020-04-12 23:55 被阅读0次

    一、前言

    毋庸置疑,大家肯定听说过类型擦除。但却不一定能知道一些细节方面的东西。
    这篇文章主要讲的是我对于类型擦除学习过程一直存在的一些加强。希望读者可以有所收获。

    以前,甚至可以说一个月之前,对于类型擦除,我就只有这么一个印象,那就使用泛型的时候。

        ArrayList<String> list = new ArrayList<>()
    

    在编译过后,String就会被擦除,而并不会生成ArrayList<String>.class。
    所有使用ArrayList<T> 对应的class文件,都是 ArrayList.class.
    了解到这里。戛然而止。
    连这个判断为什么会编译不过,我都解释不来:

        ArrayList<String> list = new ArrayList<>()
        if (list instanceOf ArrayList<Object>) {} // String 是Object的子类,为什么连检查都不通过?
    

    更不用说,每次遇到:

        class Child extend Parent {}
    
        ArrayList<? extend Parent> list = new ArrayList<>()
        list.add(new Child()) // 编译检查不通过。
    

    一遇到 <? extend Parent> 和 <? super Child> 我就要纠结半天。
    然后对于上次、上上次脑海中总结的关于上下限的概念,又来捋一遍,下次继续懵逼。

    后来看见公众号提到了,协变和逆变,WTF,what is it?
    终于,这次花了点时间做个总结。

    二、 什么是类型擦除

    先要来说一下泛型。

    声明中具有一个或者多个类型参数的类或者接口,就是泛型(generic)。 --- Effective Java

    而类型参数就我们声明class时候,使用的<T> 。比如,class List<T>, class Map<K, V>

    class List<T> T就是形式类型参数,而我们在使用List<String>时String则就是对应的实际类型参数

    然而泛型在Java 1.5版本才引入。以前的List 变成了List<T>,那么多陈旧代码,怎么玩?

    为了兼容旧版本,于是编译阶段把所有关于T的信息都给擦除了!对于List<T>生成的List.class里面涉及T的都用Object来代替。
    另外Java中还保留有直接使用List的用法, 称之为原生态类型(raw type)。

        List list = new ArrayList()
        list.add(0);
        list.add("string")
    

    这显然是不安全的,也不知道什么时候,使用list.get(int) 进行强转换的时候就出现ClassCaseExexption.

    相比较直接使用List的原生态类型,还是使用List<Object>比较稳妥。毕竟前者直接规避类型检查,后者则明确告诉编译器器持有任意类型的对象。最大区别在于:

        // 原生态可以指向任意List<T>
        List list = new ArrayList<String>()  
        list.add(0) // 可正常添加。并不受ArrayList<String>() 的String影响
        // 误以为list都是String,强转String的时候就会崩
        String s = (String)list.get(0)
        
        
        // List<Object> 只能指向List<Object>
        List<Object> list = new ArrayList<String>() // error
        
    

    三、通配符

    1、?无限制通配符

    考虑这段代码。判断一个集合是否另一个集合的子集

        boolean contains(Set s1, Set s2) {
            for(Object s: s1) {
                if(!s2.contains(s)) {
                    return false;
                }
            }
            return true;
        }
    

    s1、s2 也不在意究竟是什么类型,虽然所以代码正常运行。但是使用原生态类型本身就是一种错。
    如果确实并不在意是什么类型,向上述代码中无多余的操作,那么可以使用通配符来替代。

        boolean contains(Set<?> s1, Set<?> s2) {
            for(Object s: s1) {
                if(!s2.contains(s)) {
                    return false;
                }
            }
            return true;
        }
        
        // List<?> 也可以指向任意List<T>
        List<?> list = new ArrayList<>()
        list = new ArrayList<String>()
        list = new ArrayList<Integer>()
    

    由于可以指向任意参数类型。也就是会有原生态一样的安全隐患,所以编译器对其添加了约束,使其安全。

        List<?> list = new ArrayList<String>()
        list.add("string") // 编译失败,原生态是可以的。
        list.add(null) // ok,由于不清楚list最终指向谁,所以一刀切,只能添加null
        list.get(0) // 统一返回return Object(或者null)。List<String>返回String对象。
    

    具体的约束我们等下可看下面讲解的有限制的通配符

    2、协变和逆变

    讲到这里我们可以先引入协变和逆变了。
    从网上抄了这个公式。

    逆变与协变用来描述类型转换(type transformation)后的继承关系,
    其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
    f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
    f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
    f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
    

    刚开始真没看懂,看不懂也无所谓,。毕竟这协变这些个概念目前我就在数组和泛型中才有看到过。而且举个例子看下就大概差不多懂了。

    🌰1、先说数组:Number[] 和 Integer[]

        任意存在继承关系的两个类,如Integer 是 Number的子类。
        数组是协变的。
        则其对应的数组类型也存在继承关系,如 Integer[] 是 Number[] 的子类
        那么:
        Number[] number = new Integer[10]; // true
        boolean b = number instanceof Integer[]; // true!
    

    那么逆变呢?emmm...没啥例子好举,还是看上面的公式吧...

        逆变就是与协变相反
        先假设数组是逆变的
        任意存在继承关系的两个类,如Integer 是 Number的子类。
        那么会有 Integer[] integer = new Number[10];  // 虽然这显然不科学。
    

    🌰2、再看看集合,例如,List<Number> 和List<Integer>

        任意存在继承关系的两个类,如Integer 是 Number的子类。
        泛型是不变的。
        则其对应的泛型不存在继承关系,如List<Integer> 不是 List<Number> 的子类
        所以下面的:
        ArrayList<Number> number = new ArrayList<Integer>() // error!
        ArrayList<Integer> number = new ArrayList<Number>() // error!
    

    泛型的不变,从直觉上看,这很奇怪。但是这很有意义。毕竟List<Number>可以放进Double的数据,但是List<Integer>只能放进Integer的数据。如果ArrayList<Number> number = new ArrayList<Integer>()成立,也就是number.add(1d)也成立。那么读取数据进行操作的时候很就得崩。

    反观数组,下面代码编译时通过的,但是执行的时候,就得boom boom boom

        Number[] number = new Integer[10];
        number[0] = 1d; // ArrayStoreExecption;
    

    数组是协变,所以泛型是不变的。泛型把类型安全的检测提前到了编译期,而不是等到运行时,才去发现问题。

    3、有限制的通配符

    泛型是不变的。但是为了api的灵活性,JDK提供了使泛型支持协变和逆变的方法。

    1. extend ---使得泛型支持协变

        List<Integer> b = new ArrayList<>();
        List<Number> n= new ArrayList<Number>;
        n.addAll(b);
    

    上述代码是可以正常执行的,Number类型的添加一下Integer数据,正常不过的事情。

    但是addAll(..)的参数该如何定义呢?通用点就应该是

        public interface List<E> extends Collection<E> {
            addAll(Collection<E> c)
        }
    

    如果这样定义的话,n.addAll(b)的时候,由于List<Integer> 不是 Collection<Number>的子类型,那肯定编译不通过。所以JDK提供的方法是这样的:

        addAll(Collection<? extends E> c)
    

    Collection<? extends E> 使得n.addAll() 可以支持实际类型参数是Number或者Number的子类的Collection<E>。

    也就是可以支持协变了,即:

        Collection<Integer> b = new ArrayList<>()
        Collection<? extends Number> c = b;
        // c.add(0) // error
    

    当然这个玩意类似于?, 也使得其多了些限制。但相比较?,因为已经确定实际参类型参数的上限,也就是Number,所以get(int)的时候返回不再是Object,而是Numbe对象。
    但是由于,变量c依然可以随意指向Collection<Integer>,Collection<Double>等,编译器无法确定其实际参数类型,故而add()时依然也只能添加null。

    具体来说。Collection<? extends Number> 和Collection<?> ,基本都无法调用任何以类型参数作为参数类型的方法。除非参数传null。

        Collection.boolean add(E e); // 以类型参数作为参数,故c无法调用,编译器报错,除非参数传null。
    

    2. super ---使得泛型支持逆变

    List<? super Integer>可以指向任意实际类型参数是Integer或者Integer的父类的List<T>

            List<Number>b = new ArrayList<>();
            List<? super Integer> n= b;
    

    这段代码很是符合逆变的公式呀,也就是这样的泛型支持逆变的!

    当然同样存在限制。与extend相比也是反过来了。super确定其实际类型参数的下限,也就是Integer。也就是变量n可以随意指向List<Number>,List<Object>等。但这也导致也不能确定实际参数类型是哪一个(Object~Integer之间)。
    所以相比较?,n.add(Integer)或者n.add(Integer的子类)显然是没有问题的了。而在get(int)的时候只能返回Object类型。

    emmm...当然啦...class Integer 是final修饰的,没有子类。

    再举个例子来说:

            List<Number>b = new ArrayList<>();
            b.add(1d)
            List<? super Integer> n= b; // 甚至可以是 n = new ArrayList<Object>()
            n.add(0)
    

    那么显然我们处理
    n.get(0) 无法判断其具体类型,只能退化到Object.

    4、稍总结下

    总的来说,extend适合作为生产者。比如addAll(Collection<? extend Number> c) 限制所有c中所有数据都得起码是Number。适合作为一个生产者来提供数据。

    而super适合作为消费者,Collection<? super Number> c 则限制数据的流入,想要被c消费使用(c.add(Number))的数据起码为Number。

    看这个例子:

        //生产者:src,数据类型起码为T;传入消费者dest中,dest要求传入的数据类型起码为T
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            int srcSize = src.size();
            if (srcSize > dest.size())
                throw new IndexOutOfBoundsException("Source does not fit in dest");
    
            if (srcSize < COPY_THRESHOLD ||
                (src instanceof RandomAccess && dest instanceof RandomAccess)) {
                for (int i=0; i<srcSize; i++)
                    dest.set(i, src.get(i));
            } else {
                ListIterator<? super T> di=dest.listIterator();
                ListIterator<? extends T> si=src.listIterator();
                for (int i=0; i<srcSize; i++) {
                    di.next();
                    di.set(si.next());
                }
            }
        }
    

    四、参看文章

    这要参看这两篇,其他杂七杂八的也没注意了。
    Java泛型(一)类型擦除
    Java泛型(二) 协变与逆变

    相关文章

      网友评论

        本文标题:类型擦除 知多滴!

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