协变,逆变,不可变是各语言在支持泛型型变中通用的术语,所以有必要理解清楚其设计思想与作用,这样在语言过渡中才能有共性的认识,加快学习速度。之前没有彻底掌握,今天又重新梳理了一遍。越来越深的体会到学习+笔记+总结带来的记忆深度要超过'看懂会用'很多的。
实事求是的讲,截至本篇总结完,我依然无法100%确认我理解的是否完全正确。欢迎斧正~
一、什么是型变
Object a = new String("字符串")
复制代码
String作为Object的子类,可以直接将子类对象赋值给父类。这个操作即达到了型变
但是当使用泛型类型时,是无法型变的。
例:
List<String> strs = new ArrayList<String>();
List<Object> objs = strs;// !!!此处的编译器错误让我们避免了之后的运行时异常
//假设我们忽略这个错误继续操作,假设objs = strs 也支持型变
objs.add(1);// 这里我们把一个整数放入一个字符串列表
String s = strs.get(0);// !!! ClassCastException:无法将整数转换为字符串
复制代码
Java中为了保证运行时安全,所以在禁止了这样的操作
实际应用中,开发者是需要语言对泛型类型的型变的支持。所以引出了协变,逆变,不可变的实现思想。以此来支持泛型的型变
在重点开始前先做两个准备工作
1.准备几个类并明确继承关系,每向右递进一次,作为一次子类实现。
- Fruit
- Banana
- Apple
- RedApple
- GreenApple
- 准备两个支持泛型的类Orchard(果园模拟产出角色:输出),Shop(店铺模拟处理角色:输入)
public static class Orchard<T> {
T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
public static class Shop<T> {
T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
复制代码
二、什么是协变(Covariance)
概念:一个类型规则或者类型构造器保持了子继承父的类型关系,即:子类型≦父类型。
作用:能够接收比原始指定的派生类型的派生程度更大(更具体的)的类型
实际开发中,需要子泛型类型能够型变为父泛型类型。
直觉上的实现代码:
Orchard<Fruit> orchardF = new Orchard<Apple>();
复制代码
这当然是无法通过编译的,需要扩大左边类型的赋值范围,需使用通配符? extends X
来接受更大的类型范围
Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
复制代码
扩大了接受类型为Fruit 及其子类。依然维持读取类型为Fruit,不管协变类型是哪个Fruit子类,读取Fruit都是类型安全的。
Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
Fruit fruit = orchardF.get();
复制代码
不可以进行写入,以下代码将报错:
Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
orchardF.set(new Apple());
复制代码
从直觉上似乎这样的操作并不应该存在问题。
假设将读取与写入的边界都设置为Fruit及其子类,下面的例子会有什么问题吗?
Orchard<? extends Fruit> orchardF = new Orchard<Apple>();
orchardF.set(new Banana());//报错类型不匹配
复制代码
很明显从orchardF
对象的使用者来看它可以接受任何Fruit的子类,但是这实际上是给仅支持Apple类型的对象传入了一个Banana对象,这当然是不正确的。
即:在使用? extends E
时,并不知道什么对象符合那个未知的E
的子类型,所以无法支持写入。
java中的协变:在java中通常将通配符? extends E
限制上边界来完成协变(限定输出类型的上边界)。保证了接受类型协变,读取类型安全
三、什么是逆变(Contravariance)
概念:一个类型规则或者类型构造器逆转了子继承父的类型关系,即:以父类型接受最后输出子类型
作用:能够接收比原始指定的派生类型的派生程度更小(不太具体的)的类型
在进行消费操作时如果需要使用父类型的操作,这需要下面的实现
Shop<Apple> shopA = new Shop<Fruit>();
复制代码
这也是不被允许的,直觉上也不对呀。
需要使用? super X
通配符来扩大接收类型范围,来接收实际操作对象是泛型为父类型Fruit的具体对象。
Shop<? super Apple> shopA = new Shop<Fruit>();
复制代码
这样似乎看起来有些问题,扩大了类型接受范围,也扩大输入范围,因为声明类型默认是以实际泛型类型作为上边界的,像下面这样:
Shop<Fruit> shopF = new Shop<Fruit>();
shopF.set(new Apple());
shopF.set(new Banana());
复制代码
使用了父类型,同时又被迫扩大了输入范围(期望是只输入Apple及其子类),这显然不正确。
这便是? super X
通配符的作用所在,扩大赋值类型范围,限制输入类型范围,称之为限制下边界
输入示例:
Shop<? super Apple> shopA = new Shop<Fruit>();
shopA.set(new Apple());
shopA.set(new RedApple());
shopA.set(new Banana());//编译错误
复制代码
关于下边界的理解,在上面的例子中:? super Apple
将Apple指定为对象Shop<Fruit>()
的输入类型的下边界。
java中的逆变:在java中通常将通配符? super E
限制下边界,允许声明的泛型为子类接收泛型为父类的对象,完成逆变。限定输入类型的下边界,从输入处隔绝了类型不安全。
四、不可变性(Invariance)
概念:不满足协变的同时不满足逆变即为不可变。
不使用通配符的情况下都是不可变的。
Shop<Fruit> shopF = new Shop<Fruit>()
复制代码
总结
概念是别人思考的结果的总结,还是要基于独立的思考与实践分析才能理解透彻。
抛开概念,实际开发中可借助Joshua Bloch 的助记符:
PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)
参考:
本篇的由来其实就是因为在阅读文档时遇到了阻力。此篇文档重点不是介绍型变的本质,所以自己恶补了以下。为的是在理解 kotlin 泛型实现中能够正确无误。
本篇中对于相关概念的总结,更直观,更好理解。此篇的阐述中借助了相当一部分。
掘金的主题 好有趣,比如本篇使用的,看着就有一种轻松的感觉。