Kotlin-泛型out,in:泛型协变和逆变——泛型高级功能2(第一行代码Kotlin学习笔记12)


Kotlin中泛型还有一个高级功能,就是协变和逆变。假如我们有类A继承B,那么我们就可以让MyClass< A>作为MyClass< B>的子类使用,这叫做泛型的协变,我们在泛型前用out 关键字修饰就可以了,好比:class MyClass< out T>(val data: T)。相反的,如果我们想让MyClass< B>作为MyClass< A>的子类使用,就叫做泛型的逆变,需要在泛型前用in 关键字修饰。

A继承B
协变
逆变
B
A
MyClass泛型B
MyClass泛型A
MyClass泛A
MyClass泛B

其实协变和逆变我们还可以这么理解:我们把一个泛型接口或者泛型类中的方法的参数部分叫做in,把返回值部分叫做out,那么如果用out修饰泛型,则表示该泛型只能用于返回值,不能在方法接收参数的时候使用。如果用in修饰,则表示方法只能在接收参数的时候使用泛型,而不能用于返回某个泛型类型的值。

接下来我们看下为什么要这么规定:

1. 泛型的协变

现在我们有如下代码,Student和Teacher继承Person

open class Person(val name:String,val age:Int)
class Student (name:String,age:Int) :Person(name,age)
class Teacher (name:String,age:Int) :Person(name,age)

然后我们定义一个包装类WrapData

class WrapData<out T>(private var data:T?){
    fun set(t:@UnsafeVariance T?){
        if (t != null) {
            data = t
        }
    }
    fun get():T?{
        return data
    }
}

此处我们用out修饰了泛型T,那么意味着WrapData在泛型T上是协变的,那set方法中@UnsafeVariance又是设么呢?我们前面说过用out修饰泛型,则表示该泛型只能用于返回值,如果我们想要在参数中使用该泛型,则必须用@UnsafeVariance来说明我们接下来的操作是安全的。但是既然我们要特殊说明安全问题,那不安全因素又在哪?看下面代码:

fun main() {
	//1
    val student = Student("张三",17)
    //2
    val data = WrapData<Student>(student)
    //3
    handleWrapData(data)
    //5
    val handleStudent = data.get()
}
fun handleWrapData(data:WrapData<Person>){
	//4
    val teacher = Teacher("王老师", 33)
    data.set(teacher)
}

这段代码在编译时完全不回报错,因为我们使用了泛型的协变,所以WrapData< Student>是WrapData< Person>的子类,所以这样传参没有任何问题,但是在运行时,就会报错:
在这里插入图片描述
接下来我们看下为什么会类型转换异常:

  1. 定义student对象
  2. 定义包装类,并且传入我们定义好的student对象
  3. 处理包装类handleWrapData(data:WrapData< Person>),传入的是WrapData< Student>类型数据,此时实参是形参的子类,没有任何问题
  4. 我们在handleWrapData(data:WrapData< Person>)方法中,将WrapData< Person>类型参数中的数据设置为teacher对象,Teacher是Person的子类,这样设置也没有问题
  5. 我们调用了data.get()方法,因为data的类型是WrapData< Student>()类型,所以我们在调用get()方法时得到的应该是Student类型数据,但是此时我们data中的实际数据在第4步的时候,就变成了Teacher类型对象,而Teacher类型是无法转化成Student类型的,所以报出ClassCastException。

由此看来,我们的代码并不安全,但是如果我们禁止在WrapData中用set()方法为data参数赋值,那么,我们就不再会又类型转换错误的隐患了,没错,使用out修饰泛型确实在语法上不允许我们在参数位置使用泛型作为参数类型。前面讲过,我们使用@UnsafeVariance来说明我们接下来的操作是安全的,才可以在参数位置,也就是in位置使用泛型类型,如果我们去掉@UnsafeVariance注解,则会报如下错误:
在这里插入图片描述
这就是泛型的协变,接下来我们再看泛型的逆变。

2. 泛型的逆变

现在我们还有Student和Teacher继承Person,我们现在定义一个接口,用于数据和对象之间转化:

interface ConvertPerson<in T>{
    
    fun person2message(data: T):String
    
    fun message2person(name:String,age:Int) : @UnsafeVariance T
}

没错,我们又看到了熟悉的@UnsafeVariance,因为前面我们说过,逆变不允许泛型类型用于返回值,因为这样用依然可能会导致ClassCastException,所以此处我们使用注解,让编译通过。

我们结合具体场景看下为什么会有ClassCastException的隐患呢:

fun main() {
    //---1------
    val convent = object :ConvertPerson<Person>{
        override fun person2message(data: Person):String {
            return "${data.name} 今年 ${data.age} 岁了"
        }
		//---4---
        override fun message2person(name: String, age: Int): Person {
            return Teacher(name,age)
        }
    }
    //----2----
    handleStudent(convent)
    //----3----
    handleMessage(convent)
}
fun handleStudent(convert: ConvertPerson<Student>){
    val student = Student("李四",13)
    val person2message = convert.person2message(student)
    println(person2message)
    //李四 今年 13 岁了
}
fun handleMessage(convert: ConvertPerson<Student>){
    val student = convert.message2person("王武",8)
}

因为我们在接口ConvertPerson中使用了in修饰泛型T,表示我们该泛型接口可以实现逆变,因此ConvertPerson< Person>是ConvertPerson< Student>类型的子类,所以handleStudent(convent)(2处代码)这行代码调用没有任何问题,我们也可以正常打印出(李四 今年 13 岁了)。

但是当我们调用 handleMessage(convent)(3处)代码时,我们最终执行了 val student = convert.message2person(“王武”,8),此处由于我们的形参convert类型中泛型T为Student类型,所以我们message2person()方法的返回值期待类型也是Student,但是我们实际调用中返回的却是Teacher类型,所以代码运行到这里同样会报出ClassCastException。所以,如果当我们支持逆变的泛型也就是用in修饰的泛型的泛型类或接口中的方法不允许在返回值(out位)使用泛型时,就没有这样的隐患了。

如果我们把ConvertPerson接口中message2person方法返回值中的@UnsafeVariance注解去掉的话,同样会在编译时就报错,告诉我们用in修饰的泛型,不可以在out位使用。
在这里插入图片描述

3. 举栗

3.1 协变

Kotlin内置api中,Collection接口,List接口,都是支持协变的:

//Collection部分源码
public interface Collection<out E> : Iterable<E> {
    public val size: Int
    public fun isEmpty(): Boolean
    public operator fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
}
//List部分源码
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>
}

我们可以看到这两个集合接口都是支持协变的,但是contains(element: @UnsafeVariance E): Boolean方法则使用了@UnsafeVariance来修饰泛型,因为contains并没有对集合做任何实际的操作,只是判断来是否包含某个元素,所以不可能有类型转化异常,所以这么写实际上也是安全的。

3.2 逆变

在Kotlin内置API中,逆变的典型接口就是Comparable接口,这也是我们在日常开发中使用频率较高的接口之一。

public interface Comparable<in T> {
    /**
     * Compares this object with the specified object for order. Returns zero if this object is equal
     * to the specified [other] object, a negative number if it's less than [other], or a positive number
     * if it's greater than [other].
     */
    public operator fun compareTo(other: T): Int
}

如果我们实现有Comparable< Person>类型的实现类来比较两个人的大小,具体可能是比较年龄,身高什么的,那么这个实现类在比较Teacher或Student时也同样适用,那么Comparable< Person>如果是Comparable< Student>的子类的话,我们在代码中实现一些逻辑就会方便很多了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值