类型擦除 知多滴!

一、前言

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

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

    ArrayList list = new ArrayList<>()

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

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

更不用说,每次遇到:

    class Child extend Parent {}

    ArrayList list = new ArrayList<>()
    list.add(new Child()) // 编译检查不通过。

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

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

二、 什么是类型擦除

先要来说一下泛型。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3、有限制的通配符

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

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

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

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

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

    public interface List extends Collection {
        addAll(Collection c)
    }

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

    addAll(Collection c)

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

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

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

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

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

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

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

List可以指向任意实际类型参数是Integer或者Integer的父类的List

        Listb = new ArrayList<>();
        List n= b;

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

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

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

再举个例子来说:

        Listb = new ArrayList<>();
        b.add(1d)
        List n= b; // 甚至可以是 n = new ArrayList()
        n.add(0)
 
 

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

4、稍总结下

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

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

看这个例子:

    //生产者:src,数据类型起码为T;传入消费者dest中,dest要求传入的数据类型起码为T
    public static  void copy(List dest, List 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 di=dest.listIterator();
            ListIterator si=src.listIterator();
            for (int i=0; i

四、参看文章

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

你可能感兴趣的:(类型擦除 知多滴!)