类型擦除 知多滴!

一、前言

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

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

    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泛型(二) 协变与逆变

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    光剑书架上的书阅读 2,143评论 6 10
  • 参考地址:《Java 泛型,你了解类型擦除吗?》 《Java中的逆变与协变》 《java 泛型中 T、E .....
    琦小虾阅读 2,967评论 0 10
  • 泛型 泛型(Generic Type)简介 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么...
    Tenderness4阅读 1,412评论 4 2
  • 其实,这是一篇游记,虽然是打着赛记的名义开头,就好像我打着参加名古屋女子马拉松的名义跟着几个小伙伴去玩了一圈...
    鲛小爻阅读 520评论 0 1
  • 我想北戴河之美,几乎是人人都知道的。我也是早些年前就听闻了北戴河。 在我的印象中,北戴河在秦皇岛上,一个风景宜人,...
    我是空无阅读 375评论 3 5