首先假设有如下几个类:
1 | open class GrandFather |
概念
协变
协变指的是如果有一个类型T,它可以被替换为它或者它的子类型。
比如只有年龄不大于七十岁的才能买保险,那么就可以说Insurance<Father>
是协变的,因为它可以被替换为Insurance<Son>
和Insurance<Baby>
。
逆变
协变指的是如果有一个类型T,它可以被替换为它或者它的父类型。
比如只有成年人才能开车,那么就可以说Driver<Son>
是逆变的,因为它可以被替换为Driver<Father>
和Driver<GrandFather>
。
不变
不变指的是如果有一个类型T,它既不能被替换为子类型,也不能被替换为父类型。
比如只有满足特定年龄段才能上学,那么就可以说Student<Son>
是不变的,它不能被替换。
协变的语言支持
赋值处协变
两种语言都默认支持。
在Java中:
1 | Father father = new Son(); |
在Kotlin中:
1 | val father: Father = Son() |
方法重写处协变
两种语言都默认支持,其中参数必须是不变的,返回值可以是协变的。
比如在Kotlin中:
1 |
|
MyInterfaceImplA
将返回值的类型从Father
变成了Son
,这是支持的,MyInterfaceImplB
将参数的类型同样从Father
变成了Son
,就会导致编译不过,因为参数是不支持协变的。
数组协变
Java支持,但是有缺陷,会导致运行时错误,比如这段代码:
1 | public class Test { |
运行后就会得到如下异常:
1 | Exception in thread "main" java.lang.ArrayStoreException: Son |
因为一开始把fathers
协变成了Baby[]
,然后下一行又尝试把一个Baby
的父类Son
放入里面,就需要将父类提升成子类,这样的操作是不支持的。
为了解决这个问题,Kotlin是不支持数组协变的。比如下面这段代码,是无法通过编译的:
1 | fun main(args: Array<String>) { |
泛型集合协变
Java支持,需要显式调用,用<? extends>
表示。
1 | public class Test { |
Kotlin支持,需要显式调用,用out
表示。
1 | fun main(args: Array<String>) { |
需要注意的是,协变后的泛型集合就变成了只读的,两种语言都不支持在协变后的集合上进行写操作,协变后的集合不能被作为参数,因此也就无法被修改。
下面这段代码是过不了编译的:
1 | public class Test { |
这是因为集合的协变可能会导致和Java中数组协变同样的问题,也就是将一个父类放入子类的集合中去,详细分析如下:
Type | Original Type | Possible List Type | Available Element Type |
---|---|---|---|
GrandFather | |||
Father | * | * | * |
Son | * | * | |
Baby | * | * |
集合一开始的类型是List<Father>
,它可能被协变为List<Son>
和List<Baby>
,在向集合中加入元素的时候,用的规则是赋值处协变的规则,即所有Father
的子类都可以被加入,这时候只要协变后的集合类型层级比加入的元素层级高就会出错,比如集合协变成了List<Baby>
,然后向其中加入Son
的元素,就会导致ArrayStoreException
的错误,因此两种语言都禁止了对协变后集合的写操作。
逆变的语言支持
赋值处逆变
两种语言都不支持。
在Java中:
1 | // NOT OK |
在Kotlin中:
1 | // NOT OK |
数组逆变
两种语言都不支持。
在Java中:
1 | public class Test { |
在Kotlin中:
1 | fun main(args: Array<String>) { |
方法重写处逆变
两种语言都不支持。无论是方法参数还是返回值都不行。
比如在Kotlin中:
1 | interface MyInterface { |
泛型集合逆变
Java支持,需要显式调用,用<? super>
表示。
1 | public class Test { |
Kotlin支持,需要显式调用,用in
表示。
1 | fun main(args: Array<String>) { |
需要注意的是,逆变后的泛型集合就变成了只写的,两种语言都不支持在协变后的集合上进行读操作,逆变后的集合不能被作为返回值,因此也就不能被读取。
比如下面这段代码是过不了编译的:
1 | public class Test { |
原因和协变禁止写操作的理由类似,原始集合的类型是List<Father>
,可以被逆变成List<GrandFather>
,如果集合逆变成了List<GrandFather>
,从中取出元素时就需要把元素提升转换为原始类型Father
,而这样的操作是不支持的。
Type | Original Type | Possible List Type | Available Element Type |
---|---|---|---|
GrandFather | * | ||
Father | * | * | * |
Son | * | ||
Baby | * |
常见用法
协变
一种常见的用法是在方法的返回值上协变,比如:
1 | public GrandFather doSometing() { |
逆变
协变常常用于比较器上,比如有如下两个比较器:
1 | public class SonComparator implements Comparator<Son> { |
1 | public class FatherComparator implements Comparator<Father> { |
假设有一个方法接收一个比较器来进行Son
之间的比较,这种情况下用它父类的比较器来比较也是可以理解的,因为Son
的所有属性在它的父类里都有,所以以下代码可以运行:
1 | public class Test { |
可以看到,虽然compare()
方法进行的是Son
之间的比较,但是传入一个类型为Comparator<Father>
的比较器仍然可以运行。
参考:
评论(需梯子)