一、前言
毋庸置疑,大家肯定听说过类型擦除。但却不一定能知道一些细节方面的东西。
这篇文章主要讲的是我对于类型擦除学习过程一直存在的一些加强。希望读者可以有所收获。
以前,甚至可以说一个月之前,对于类型擦除,我就只有这么一个印象,那就使用泛型的时候。
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泛型(二) 协变与逆变
网友评论