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是一样的。最后用一张表格作总结:

 

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Kotlin中的泛型是一种类型变量的机制,它允许我们在不确定具体类型的情况下编写通用代码。Kotlin中的泛型支持协变逆变,以及in和out的关键字。 首先,我们来看一个简单的例子,实现一个泛型容器类: ```kotlin class Container<T>(var item: T) { fun getItem(): T { return item } } fun main() { val container = Container<String>("Hello") println(container.getItem()) } ``` 在这个例子中,我们定义了一个名为Container的泛型类,它有一个类型参数T。我们可以创建一个Container实例,并将其实例化为一个具体类型。我们使用getItem方法来获取这个容器中的item。 接下来,我们来介绍一下协变逆变。假设我们有两个类: ```kotlin open class Animal { fun makeSound() { println("Making animal sound") } } class Cat: Animal() { fun meow() { println("Meow") } } ``` 我们可以通过一个简单的示例来说明协变逆变: ```kotlin fun main() { val animals: List<Animal> = listOf(Cat(), Animal()) makeSounds(animals) } fun makeSounds(animals: List<Animal>) { for (animal in animals) { animal.makeSound() } } ``` 在这个例子中,我们定义了一个List<Animal>类型的变量animals,它包含了一个Cat和一个Animal实例。我们将这个变量传递给了makeSounds函数,该函数接受一个List<Animal>类型的参数。 在makeSounds函数中,我们使用for循环遍历animals列表,并对其中的每个Animal实例调用makeSound方法。由于Cat是Animal的子类,因此它也可以被视为Animal类型,因此我们可以将其添加到List<Animal>类型的变量中。 这里的List<Animal>类型就是协变的,因为我们可以将它的子类(如Cat)作为参数传递给makeSounds函数。 现在我们来看一下逆变。假设我们有一个接受Animal类型的参数的函数: ```kotlin fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 我们可以将这个函数传递给另一个函数,该函数期望一个Cat类型的参数。在这种情况下,我们可以使用in关键字来表示逆变: ```kotlin fun main() { val cat: Cat = Cat() takeCat(cat) } fun takeCat(cat: Cat) { takeAnimal(cat) } fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 在这个例子中,我们定义了一个takeCat函数,它接受一个Cat类型的参数。我们将这个函数传递给了takeAnimal函数,该函数期望一个Animal类型的参数。由于Cat是Animal的子类,因此我们可以将Cat类型的参数传递给takeAnimal函数。这里的takeAnimal函数是逆变的,因为它可以接受其超类型(如Animal)的参数。 最后,我们来看一下out和in关键字。我们可以在定义泛型类型参数时使用这些关键字来限制泛型类型参数的使用方式。out关键字用于声明泛型类型参数是协变的,in关键字用于声明泛型类型参数是逆变的。 例如,我们可以定义一个只允许读取的泛型接口: ```kotlin interface ReadOnlyContainer<out T> { fun getItem(): T } ``` 在这个例子中,我们使用out关键字来声明泛型类型参数T是协变的。这意味着我们只能从ReadOnlyContainer接口中获取T类型的值,而不能修改它。这样做的好处是可以使我们更加安全地使用泛型类型参数。 类似地,我们可以定义一个只允许写入的泛型接口: ```kotlin interface WriteOnlyContainer<in T> { fun setItem(item: T) } ``` 在这个例子中,我们使用in关键字来声明泛型类型参数T是逆变的。这意味着我们只能向WriteOnlyContainer接口中设置T类型的值,而不能获取它。这样做的好处是可以避免意外修改泛型类型参数的值。 总结一下,Kotlin中的泛型支持协变逆变,以及in和out关键字。使用协变逆变可以使我们更加灵活地使用泛型类型参数,而使用in和out关键字可以帮助我们更加安全地使用泛型类型参数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我们间的空白格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值