Kotlin协变和逆变

首先声明三个类:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值