首先声明三个类:
open class Person(val name: String, val age: Int) {
}
class Man(val n: String, val a: Int, val male: String = "man")
: Person(n, a) {
}
class Woman(val n: String, val a: Int, val female: String = "woman")
: Person(n, a) {
}
Man和Woman都是Person的子类,继承了name和age并单独声明了male和female属性以表明性别。接着分别声明一个Person类型的引用和一个Person类型的list集合引用,并分别用Man类型的对象和Man类型的list集合对象来接收:
val p: Person = Man("jack", 15) //编译正常
val pl: ArrayList<Person> = ArrayList<Man>() //编译报错
//Type mismatch.
//Required:
//kotlin.collections.ArrayList<Person>
//Found:
//kotlin.collections.ArrayList<Man>
val pl: List<Person> = listOf<Man>() //编译正常
// 这是因为List接口在kotlin中它的泛型类型已经被协变了:
// public interface List<out E> : Collection<E> {...}
会发现p可以正常编译,因为Man是Person的子类,但pl编译报错并提示这里要求使用Person类型的List集合对象,因为ArrayList<Person>并不是ArrayList<Man>的父类,他们都是List接口的子类,相互之间并没有继承关系所以编译器并不能匹配正确的引用类型。java为了解决这个问题提供了<? extends T>通配符,而在kotlin中将这个通配符用out关键字来替代。out修饰Person表明pl这个集合对象中存的可以是Person及其子类的对象:
val pl: ArrayList<out Person> = ArrayList<Man>() //编译通过
Man是Person的子类同时ArrayList<Man>又是ArrayList<out Person>的子类,那么我们就可以说ArrayList在Person这个类型上是协变(covariance)的。
上述是简单地举了个协变的例子,只是表面浅浅的一层,下面对协变和逆变详细说一说。
说之前先声明两个概念:一个泛型类或者泛型接口中的方法 接收入参的位置称为in位置,返回值输出数据的位置称为out位置:

一.协变
先声明个泛型类,内部封装了一个私有的data属性并向外部提供set、get方法:
class MyClass<T> {
private var data: T? = null
fun set(t: T?) {
data = t
}
fun get(): T? = data
}
将一个Man类型的MyClass对象作为入参传给test方法,但这个test方法接收的是Person类型的MyClass对象,这就和最开始说的ArrayList<Person>并不是ArrayList<Man>的父类是同样的问题。如果说kotlin允许这样跨继承传参(即test方法调用时不编译报错),那么data.get()拿到的就是一个Woman类型的对象而data.get()需要返回的是一个Man类型的对象,这样就会发生类型转换异常。所以kotlin是不会允许这样去跨继承传参的,而换个角度想之所以这样写会出现类转换异常就是因为test方法中给set了一个Woman对象导致了问题,如果说MyClass在泛型T上是只读的即没有set方法那么就不会因为set一个Woman对象导致类型转换异常。
kotlin为了实现这个只能读不能写的功能而提供了out关键字,即这个泛型T只能出现在out位置不能出现在in位置:
class MyClass<out T>(val data: T?) {
fun get(): T? = data
}
特别要注意的是由于data只读不写所以在声明时需要val修饰,如果用var修饰则表明data是可写的,它仍然处在in位置上,除非用private var修饰,这样写就表明data不暴露给外部它仍然是不可写的即不处在in位置上:
class MyClass<out T>(private var data: T?) {
fun get(): T? = data
}
现在我们就可以说MyClass在泛型T上是协变的。
kotlin重新定义的一些原生接口中已经为我们集成了协变,就比如上述提到的List接口:
val pl: List<Person> = listOf<Man>() //编译正常
// 这是因为List接口在kotlin中它的泛型类型已经被协变了:
// public interface List<out E> : Collection<E> {
// override val size: Int
// override fun isEmpty(): Boolean
// override fun contains(element: @UnsafeVariance E): Boolean
// override fun iterator(): Iterator<E>
// }
可以看到List接口在泛型E上是协变的,E就只能出现在out位置上,但是contains方法中E出现在了in位置上,这样本身是不合规定的,但是contains方法只是去判断是否包含并不会去写数据,是绝对安全的,kotlin为了处理这种情况就提供了@UnsafeVariance注解来特殊处理这个情况,这样编译器就允许这里的in位置出现E。但是这个注解功能如果使用不当极易导致类转换异常,需要慎重使用:
class MyClass<out T>(private var data: T?) {
fun get(): T? = data
fun set(d: @UnsafeVariance T?) {
data = d
}
}
二.逆变
private val pl: ArrayList<Man> = arrayListOf<Person>() //编译报错
private val p: Man = Person("jack", 15) as Man //编译通过
逆变和协变是相反的,但其实道理是一样的,之所以第一句编译报错就是因为 ArrayList<Man>并不是 ArrayList<Person>的子类无法进行类型强转。同样的,java为了解决这个问题提供了<? super T>通配符,而在kotlin中将这个通配符用in关键字来替代,in修饰Man表明pl这个集合对象中存的可以是Man及其父类的对象:
private val pl: ArrayList<in Man> = arrayListOf<Person>() //编译通过
Person是Man的父类同时ArrayList<Person>又是ArrayList<in man>的父类,那么我们就可以说ArrayList在Man这个类型上是逆变(contravariant)的。
声明一个MyClass接口:
interface MyClass<T> {
fun show(d: T?): T?
}
test方法的形参d接收一个Man类型实现的Myclass对象,但是在调用时传给test方法一个Person类型实现的MyClass对象,可以看到调用时编译报错了,还是因为kotlin不支持直接跨继承传参。如果说编译不报错,那么继续走下去会看到形参d是Man类型的Myclass引用,result变量要求接收一个Man类型实现的Myclass对象,但是实际上实参data的show方法返回了一个Woman对象,由于它是Person的子类,所以data在实现show方法的时候并没有问题,但是result在接收的时候无法将Man强转为Woman类型,这样就会发生类型转换异常。和协变一样,我们换个角度想之所以这样写会出现类转换异常就是因为show方法要去返回一个Person对象导致了问题,如果说MyClass在泛型T上是只写的即不允许泛型T出现在out位置上,那么就不会因为show方法返回了一个Woman对象而导致的类型转换问题。
kotlin为了实现这个只能写不能读的功能而提供了in关键字,即这个泛型T只能出现在in位置不能出现在out位置:
interface MyClass<in T> {
fun show(d: T?)
}

逆变中也可以使用@UnsafeVariance注解来强行让泛型T作为输出位置:
interface MyClass<in T> {
fun show(d: T?): @UnsafeVariance T?
}
//下面的实现和调用逻辑没有变化,仅仅修改了上面的MyClass接口并用@UnsafeVariance注解out位置,
//编译是完全正常的没有报错,但是运行之后就会报类转换异常,这就是@UnsafeVariance注解使用不当而
//会导致的严重后果
fun test(d: MyClass<Man>) {
val result = d.show(Man("sim", 12))//运行时报类转换异常
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val data = object: MyClass<Person>{
override fun show(d: Person?): Person? {
Log.i("tag", "${d?.name}--${d?.age}")
return Woman(d?.name?: "", d?.age?: 0)
}
}
test(data)
}
总的来说协变和逆变是java为了处理泛型的类型擦除而带入的新规则,kotlin在java的基础上用了out和in两个关键字来实现,原理和java是一样的。最后用一张表格作总结:

————————————————
版权声明:本文为优快云博主「我们间的空白格」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/qq_37159335/article/details/122671974