目录
协变与逆变是一个宽泛的概念,并不只存在于Java。
以经典的Animal、Cat为例
class Animal{
@Override
public String toString() {
return "animal";
}
}
class Cat extends Animal{
@Override
public String toString() {
return "cat";
}
}
Cat继承于Animal,Cat是Animal的子类型。Cat是对Animal功能的拓展,也就是说需要传入Animal的地方,也同样能够传入Cat。(里氏替换原则)
Cat与Animal如果单独拿出来用,继承关系非常明确。
但如果换一个环境,Cat[]与Animal[]是何关系? List<Cat>与List<Animal>是何关系?
协变和逆变就是用来描述这种本来满足继承关系的两个实体换一种更复杂的环境是否还满足继承关系。
如果依然满足原有继承关系,则称之为协变。
如果继承关系(准确点说是兼容方向)发生了反转,则称之为逆变。
如果转换后的两种结构没有任何关系,比如List<Animal>与List<Cat>互不兼容,则称之为不变。
协变数组
Java中的数组是协变的
Object数组类型的引用可以指向Animal数组,Animal类型的数组可以存放Cat。
public class ArrTest {
public static void main(String[] args) {
Object[]arr = new Animal[2];
arr[0] = new Animal();
arr[1] = new Cat();
for (int i = 0; i < arr.length; i++) {
System.out.println(i + " : " + arr[i]);
}
arr = new Cat[0];
}
}
但如果尝试用Cat[ ] 类型的数组接收Animal对象,则会抛出异常ArrayStoreException。
public class ArrTest {
public static void main(String[] args) {
Object[]arr = new Cat[2];
arr[0] = new Animal();
arr[1] = new Cat();
for (int i = 0; i < arr.length; i++) {
System.out.println(i + " : " + arr[i]);
}
}
}
Java泛型
普通泛型
普通泛型是不可变的
Java中的泛型信息在编译期间会被擦除,上述的写法无法通过语法检查。
通配符
泛型看似好像将协变与逆变挡在了门外,但其实并不是一刀切,我们依然可以通过上下边界来拓展继承关系。
上边界extends:协变
如果我们需要保持继承关系,即List<Animal>可以接收List<Cat>之类的子类型集合。
可以看到,我们尝试向集合里添加数据的时候,都失败了,哪怕是最基本的Animal对象也不可以。这是为啥?
Animal的子类可能有Cat、Dog。Cat可以兼容Animal,Dog可以兼容Animal,但是问题在于Dog与Cat之间互不兼容,一个集合中可能出现两种甚至更多种类型的对象,是不安全的。所以干脆都不让添加元素了。
那这个上边界还有意义吗?
有,只要你能证明你传递的集合都是同一种类型的对象就可以了。看上图下方的两个例子,一个是单纯的赋值,另一个是方法传参。共同点在于我传递的是Cat类型的集合,确保集合里是同一种类型的对象,且Cat继承于Animal。
? extends Animal 表达的是,能从这个集合里面获取到的,都是Animal的子类。
下边界super:逆变
super有一个地方不是很好理解,网上很多的博客在这个地方都写的有点矛盾。
首先,<?super Cat >作为下边界, 表示的是只能接收Cat以及Cat父类的拓展数据类型的引用。
特地强调是拓展数据类型的引用,就拿集合来说
我向集合里添加了Cat的父类Animal,以及子类SmallCat,结果添加父类的时候报语法错误,子类却没事,说好的下边界呢?说好的只能添加Cat及Cat父类呢?
其实协变和逆变都只是针对于List<Animal>、List<Cat>这些拓展类型来说的,Animal与Cat本身的继承关系不受影响,依然是Cat继承于Animal。
看后面的引用、参数传递,就知道super的具体作用了。
List<? super Cat> 类型的引用,只能指向 List<? super Animal>以及 List<? super Cat>类型的数据结构,无法指向 List<? super SmallCat>类型。
这才所谓的下边界。
SmallCat类型是Cat的子类型,所以Cat类型的引用可以指向SmallCat对象。
但是使用泛型后,List<? super Cat>类型的引用,就不能指向List<SmallCat>类型的对象,但是可以指向List<Animal>集合,这就是继承关系的倒置,也就是“逆变”。
另外,使用super时,尝试获取容器内部元素时,默认的类型是Object,因为Cat最保险最顶层的基类就是Object。所以说使用super时,放便添加,但是不利于读取。
使用场合
总结一下上边界与下边界的优缺点
使用上边界(extends)时,无法添加元素,但是可以轻易的获取元素。(适合 读取)
使用下边界(super)时,可以很容易的添加元素,但是获取元素的时候只能用Object类型的引用接收。(适合写入)
其实也就是PECS原则(Producer Extends Consumer Super),生产者适合用extends,消费者适合用super。
Jdk源码中找一找也有类似的应用,例如Collection的一个集合拷贝方法
src 起始集合,也就是输入。
dest 目标集合,也就是输出。
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());
}
}
}
如有错误,欢迎批评指正。