本文同步发表于我的微信公众号,在微信搜索
OpenCV or Android
即可关注。
协变、逆变
概念
许多程序设计语言的类型系统支持子类型。例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)
是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如Cat列表之于Animal列表,回传Cat的函数之于回传Animal的函数...等等)的子类型关系。当我们用类型构造出更复杂的类型,原本类型的子类型性质可能被保持、反转、或忽略───取决于类型构造器的变型性质
。
协变与逆变用来描述类型转换(type transformation)后的继承关系:A、B表示类型,f表示类型转换,A≦ B表示A为B的子类,那么则存在:
- 当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变;
- 当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变;
- 如果上面两种关系都不成立则叫做不可变。
具象化
定义Cat、Animal两个类型,且Cat是Animal的子类,类型构造器采用数组的形式:
- 协变(covariant):一个Cat[]也是一个Animal[]
- 逆变(contravariant):一个Animal[]也是一个Cat[]
- 不变(invariant):以上两种均不满足。
总结:假想程序设计语言中的类型为输入,数组、列表、泛型等类型构造器为函数,当函数值与输入正相关时为协变,当函数值与输入负相关时为逆变。父子类关系代表输入的大小。
语言场景
Java
Java语言中,数组支持协变,泛型类原生既不支持协变,也不支持逆变。
定义类Dog,类Animal,且Dog为Animal的子类
public static void main(String[] args) {
Dog[] dogs = new Dog[5];
Animal[] animals = dogs; // Java数组支持协变
ArrayList<Dog> dogList = new ArrayList<>();
ArrayList<Animal> animalList = dogList; // 编译报错,Java泛型直接使用不支持协变
}
显然,Dog ≦ Animal,Dog[] ≦ Animal[],Java数组支持协变,不支持逆变,也就是说子类数组对象可以赋值给父类数组申明,但是父类数组对象不能赋值给子类数组申明。而针对Java泛型,直接使用既不支持协变也不支持逆变。逆变不支持好理解,为啥Java泛型连协变也不支持呢?
因为类型擦除。泛型虽然是 Java 1.5 版本引进的概念,但是,泛型代码能够很好地和旧版本代码兼容。因为Java为了兼容老版本,将与泛型相关的类型信息进行了擦除。关于类型擦除,有一道经典面试题:
public static void typeEraseSample() {
ArrayList<Integer> intList = new ArrayList<>();
ArrayList<String> strList = new ArrayList<>();
boolean isSameClass = intList.getClass() == strList.getClass()
System.out.printf(String.valueOf(isSameClass));
}
如上代码最后输入为何?终端输出:true。何解?查看字节码,一目了然。
利用类型擦除,我们可以采用反射方式向Java列表对象中添加任何类型的对象。
public static void hackTypeErase() {
ArrayList<Integer> intList = new ArrayList<>();
intList.add(23);
try {
Method method = intList.getClass().getDeclaredMethod("add", Object.class);
method.invoke(intList, new Dog());
method.invoke(intList, "yidong");
} catch (Exception e) {
e.printStackTrace();
}
for (Object object : intList) {
System.out.println(object);
}
}
举例只是为了说明问题,并不是推荐大家这样操作。继续回到协变和逆变,Java泛型直接使用不支持协变和逆变,但是通过Java提供的泛型通配符,我们可以做到返回值协变和参数逆变。
泛型通配符
PECS原则:Producer extends,Consumer super。
上界通配符:? extends T
// 泛型协变
public static void covariantGeneric() {
List<? extends Animal> objList = new ArrayList<Dog>() {{
add(new Dog());
add(new Dog());
}};
Animal animal = objList.get(0);//编译通过
Dog dog = objList.get(0); //编译报错
objList.add(new Animal()); //编译报错
objList.add(new Dog()); //编译报错
}
通俗理解:? extends Animal,代表的是Animal及其子类,所以Animal是类型上界,由此来理解“上界通配符”这个名称。针对返回值是泛型的方法(示例中get方法),由于子类对象可以赋值给父类引用,所以必须用Animal或者其父类引用来接收,体现协变的转型一致性。针对参数是泛型的方法(示例中add方法),为了保持确定性,不允许执行该类操作,因为无法确定程序传入的子类对象类型,倘若允许此类操作,在获取列表元素时,就会存在明显的类型不安全。
总结:? extends T表示所存储类型都是T及其子类,但是获取元素所使用的引用类型只能是T或者其父类。使用上限通配符实现向上转型,但是会失去存储对象的能力,上限通配符为集合的协变表示。适用于只使用,不修改的场景,也就是生产者角色。
下界通配符:? super T
// 泛型逆变
public static void contravariantGeneric() {
List<? super Dog> objList = new ArrayList<Animal>() {{
add(new Animal());
add(new Animal());
}};
Animal animal = objList.get(0);//编译报错
Dog dog = objList.get(0); //编译报错
Object object = objList.get(0);//编译正常
objList.add(new Animal()); //编译报错
objList.add(new Dog()); //编译通过
}
通俗理解:? super Dog,代表的是Dog及其父类,所以Dog是类型下界,由此来理解“下界通配符”这个名称。针对返回值是泛型的方法,由于子类对象只能赋值给自己或者父类引用,但是我们并不能保证返回的对象的继承关系比引用类型低,所以除了用Object引用接受,其他的类型接受均是不被允许的。而针对参数是泛型的方法(示例中add方法),由于Dog是继承关系的最底层,所以传入Dog或者其子类对象,列表元素引用是必然可以接收的,所以该操作是被允许的。
总结:下限通配符 ? super T表示 所存储类型为T及其父类,但是添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object,因为Object为所有类的父类。下限通配符为集合的逆变表示。适用于只修改,不使用的场景,也就是消费者角色。
无界通配符:?
只使用类型无关的方法时可采用无界通配符。
public static int getLength(List<?> list) {
return list.size();
}
Kotlin
Kotlin泛型直接使用不支持协变,也不支持逆变。由于Kotlin数组也是采用泛型的形式实现的,所以也不支持协变和逆变。与Java语言对应,Kotlin也可以使用关键字in和out
来打开协变和逆变的限制,但是使用过程中同样存在和Java一样的限制。
in
关键字对应? super
out
关键字对应? extends
Kotlin和Java关键字名称很好的诠释了PECS法则:Producer exends, Consumer super。
为了方便记忆,可以合并一下:Producer (out) exends, Consumer (in) super。
完整示例如下:
// 类型擦除
fun typeEraseSample() {
val intList = ArrayList<Int>()
val strList = ArrayList<String>()
val isSameClass = intList.javaClass == strList.javaClass
System.out.printf(isSameClass.toString())
}
// 利用反射完成填充操作
fun hackTypeErase() {
val intList = ArrayList<Int>()
intList.add(23)
try {
val method = intList.javaClass.getDeclaredMethod("add", Any::class.java)
method.invoke(intList, Dog())
method.invoke(intList, "yidong")
} catch (e: Exception) {
e.printStackTrace()
}
for (obj in intList) {
println(obj)
}
}
// 泛型协变
fun covariantGeneric() {
val objList: MutableList<out Animal> = MutableList(5) { Dog() }
val animal = objList[0] //编译通过
val dog: Dog = objList[0] //编译报错
objList.add(Animal()) //编译报错
objList.add(Dog()) //编译报错
}
// 泛型逆变
fun contravariantGeneric() {
val objList: MutableList<in Dog> = MutableList(5) { Animal() }
val animal: Animal = objList[0] // 编译报错
val dog: Dog = objList[0] // 编译报错
val obj: Any? = objList[0] // 编译正常
objList.add(Animal()) //编译报错
objList.add(Dog()) //编译通过
}
// 获取列表长度
fun getLength(list: List<*>): Int {
return list.size
}
最后介绍一个Java里没有的内容,具体化类型参数【reified】。在Java语言中,类型参数并不是一个真正的类型,而只是一个代号,我们无法把它当成一个普通类型使用,比如无法调用instanceof
函数。但是在Kotlin中,我们可以通过reified
关键字来具体化类型参数,但是只能在内联方法中使用,因为 Kotlin 编译器会把内联函数的代码插入到调用者的地方,所以可以在编译期就确定泛型的类型。
下面这个简单的方法很好的体现了reified
的作用:
inline fun <reified R> isInstanceOf(t: Any) = t is R
总结
为了方便记忆,首尾呼应一下:
参考链接:
https://baike.baidu.com/item/%E5%8D%8F%E5%8F%98/10963814?fr=aladdin https://blog.csdn.net/zy_jibai/article/details/90082239 https://www.zybuluo.com/zhanjindong/note/34147 https://www.bilibili.com/video/BV1T441117u8