美文网首页
Kotlin 泛型协变与逆变的理解

Kotlin 泛型协变与逆变的理解

作者: 会上网的井底之蛙 | 来源:发表于2022-11-06 10:17 被阅读0次

协变与逆变定义

逆变与协变用来描述类型转换后的继承关系

协变:如果 A 是 B 的子类型,并且Generic<A> 也是 Generic<B> 的子类型,那么 Generic<T> 可以称之为一个协变类。

逆变:如果 A 是 B 的子类型,并且 Generic<B> 是 Generic<A> 的子类型,那么 Generic<T> 可以称之为一个逆变类。

从上面的定义我们很好理解,协变与逆变关键在于类的父子关系在当类作为泛型参数时,泛型的父子关系是否有改变,父子关系保持协同一致的,叫协变,父子关系逆反了,叫逆变。

协变与逆变表示

kotlin 中提供了两个修饰符:out:声明协变;in:声明逆变

有两种使用方式:1.在类或接口的定义处声明、2.使用处声明

在类或接口的定义处声明,比如:

//A是父类,B为A的子类
open class A
class B: A(){}

//out声明协变
interface Production<out T> {
    //类的参数类型使用了out之后,该参数只能出现在方法的返回类型
    fun produce(): T
}

//in声明逆变
interface Consumer<in T> {
    //类的参数类型使用了in之后,该参数只能出现在方法的入参
    fun consume(item: T)
}

class ProductionA:Production<A> {
    override fun produce(): A {
        return A()
    }
}

class ProductionB:Production<B> {
    override fun produce(): B {
        return B()
    }
}

class ConsumerA:Consumer<A> {
    override fun consume(item: A) {
    }
}

class ConsumerB:Consumer<B> {
    override fun consume(item: B) {
        
    }
}

fun main() = runBlocking {
    //Production<out T>是一协变类,因为B是A的子类,则Production<B>相当于是Production<A>子类
    var productionA:Production<A> = ProductionB()
    
    //下面的赋值则是错误的
    //var productionB:Production<B> = ProductionA() //error 报 Type mismatch.
    
    //Consumer<T>是一逆变类,因为B是A的子类,则Consumer<A>相当于Consumer<B>的子类
    var consumerB:Consumer<B> = ConsumerA() //相当于子类对象赋给父类变量
    
    //下面的赋值则是错误的
    //var consumerA:Consumer<A> = ConsumerB() //error 报Type mismatch.
}

在使用处声明,比如:

//A是父类,B为A的子类
open class A
class B: A(){}

fun main() = runBlocking {
    val listB = mutableListOf(B())
    
    //error 报Type mismatch. Required:MutableList<A> Found:kotlin.collections.ArrayList<B>
    //val listA:MutableList<A> = listB
    
    //加入out后,表示协变,MutableList<B>作为MutableList<A>子类,但只取出元素,无法插入元素
    val listA:MutableList<out A> = listB  //ok
    listB.add(B())
    
    //listA实际对应的对象是ArrayList<B>,只能取出
    val a:A = listA[0]
    
    //下面两行都是错误
    //listA.add(A())  //error 报:Type mismatch. Required:Nothing Found:A
    //listA.add(B())  //error 报:Type mismatch. Required:Nothing Found:B
    
    
    
    
        
    val listA2:MutableList<A> = mutableListOf(A())
    
    //val listB2:MutableList<B> = listA2  //报错
    //加入out后,表示逆变,MutableList<A>作为MutableList<B>子类,可插入元素,但取出时类型为 Any?
    val listB2:MutableList<in B> = listA2 //ok

    listB2.add(B())
    
    //下面报错
    //val b:B = listB2[0] //error 报:Type mismatch. Required: B Found: Any?
   
    val b = listB2[0] //将类型去掉,是可以的
}

使用out/in时的规则

当一个类 C 的类型参数 T 被声明为 out 时,那么就意味着类 C 在参数 T 上是协变的;参数 T 只能出现在类 C 的输出位置,不能出现在类 C 的输入位置。

当一个类 C 的类型参数 T 被声明为 in 时,那么就意味着类 C 在参数 T 上是逆变的;参数 T 只能出现在类 C 的输如位置,不能出现在类 C 的输出位置。

关键字 功能 使用时声明 类(接口)定义时声明
out 协变 只能读取不能写入泛型对象 泛型参数只能出现在输出位置
in 逆变 只能写入,不能按照泛型类型读取泛型对象 泛型参数只能出现在输入位置

协变与逆变理解

out与in分别表示协变与逆变,已经从字面上说明了其规则,即协变时只能输出(out),逆变时只能输入(in)。之所以要有这样的规定,主要原因是:子类对象可以当作父类对象使用,反之不行。

普通的类,我们很好理解:

//A是父类,B为A的子类
open class A
class B: A(){}

fun main() = runBlocking {
    val a:A = B() //a的类型是父类,实际上是子类对象
}

换成泛型:

//A是父类,B为A的子类
open class A
class B: A(){}

//out声明协变
interface Production<out T> {
    //类的参数类型使用了out之后,该参数只能出现在方法的返回类型
    fun produce(): T
}

class ProductionA:Production<A> {
    override fun produce(): A {
        return A()
    }
}

class ProductionB:Production<B> {
    override fun produce(): B {
        return B()
    }
}

fun main() = runBlocking {
    //泛型会存在两对父子关系:1.泛型类的父子关系  2.泛型参数类的父子关系
    val productionA:Production<A> = ProductionB()
}

最重要的一点,操作productionA对象时,因为实际是Production<B>类型的对象,所以:

1.从productionA取出来的对象是B,而productionA表示A类型,B可以作为A来使用,符合子类对象可以当作父类对象使用的原则,没有问题

2.假设可以从productionA写入对象,因为productionAProduction<A>类型,所以写入的是A对象,这样会将A对象写入Production<B>中,即会出现将父类对象当成子类对象使用,就会出问题

从上面的两点可以知道,为了维持泛型参数类的父子关系满足子类对象可以当作父类对象使用的原则,只能生成子类对象,所以协变时只能输出元素,不能输入元素(输入元素意味着会产生父类对象)

逆变的情况:

//Consumer<T>是一逆变类,因为B是A的子类,则Consumer<A>相当于Consumer<B>的子类
var consumerB:Consumer<B> = ConsumerA() //相当于子类对象赋给父类变量

上面如果要满足类对象可以当作父类对象使用的原则,只能生成B类型的对象,所以只能输入元素,不能输出元素

生成对象的类型

out与in其实表示了生成泛型参数对象的出处,以泛型为主体,out就是从泛型取出对象,in就是从外面生成新的对象,放入泛型之中。

从泛型取出对象的类型比较好理解,取出来的肯定的实际的泛型参数,那从外面生成新的对象是什么类型呢,应该是定义变量时所声明的类型,举个例子:

val list:MutableList<A> = ArrayList()
list.add(A())

因为list定义时的泛型参数类型是A,所以从外面in对象时,插入应该是A的对象,假设list的实际所指向的对象可以为MutableList<B>类型,list.add的类型也应该为A,因为list所插入的对象类型是以它定义时的类型为依据,而不是实际所指向的对象。

再与之前说的只能生成子类对象的原则结合起来,不难得出:

泛型参数是子类的泛型对象可以赋值给泛型参数是父类的泛型变量的条件是泛型之中只能取出对象,所以用out约束,泛型的父子关系与泛型参数的父子关系一致,所以叫协变

泛型参数是父类的泛型对象可以赋值给泛型参数是子类的泛型变量的条件是泛型之中只能传入对象,所以用in约束,泛型的父子关系与泛型参数的父子关系不一致,所以叫逆变

总结

1.只能生成子类对象

2.生成子类对象有两个途径,从泛型取出,从泛型外部new产生

3.从泛型取出的是实际的对象,从外部new的的定义时的类型变量

4.out表示取出子类对象,只能为实际对象,与泛型本身的父子关系一致,所以叫协变

5.in表示从外部new子类对象,只能为定义时的类型变量,与泛型本身的父子关系相反,所以叫逆变

相关文章

  • Kotlin泛型与协变及逆变剖析

    Kotlin泛型与协变及逆变剖析 关于泛型的使用其实很简单,但是!!如文章开头所说,一直理解不了在Java框架中很...

  • Kotlin学习笔记 - 泛型

    1. 基本用法 2. 型变 型变包括 协变、逆变、不变 三种: 协变:泛型类型与实参的继承关系相同 逆变:泛型类型...

  • 泛型编程中的型变

    在泛型编程中,经常会提到型变。型变分为两种:协变与逆变。协变covariant表示与泛型参数T的变化相同,而逆变c...

  • Java协变和逆变

    泛型的协变与逆变 协变与逆变用来描述类型转换(type transformation)后的继承关系,其定义如下:如...

  • Scala 通俗易懂 ---- 协变、逆变、不变

    协变、逆变、不变 Scala 语言中协变、逆变、不变是指拥有泛型的类型,在声明和赋值时的对应关系 协变:声明时泛型...

  • JAVA泛型与类型安全

    1. 基础泛型 2. 协变与逆变与不变 协变 简单来说即: Java中的数组是协变的 逆变与协变相对,逆转了类型关...

  • Scala 泛型协变与泛型边界

    代码准备 泛型协变 泛型协变、逆变、不变是指拥有泛型的类在声明和赋值时的对应关系。 协变:声明时泛型是父类,赋值时...

  • Kotlin 泛型协变与逆变的理解

    协变与逆变定义 逆变与协变用来描述类型转换后的继承关系 协变:如果 A 是 B 的子类型,并且Generic 也...

  • Java 泛型与通配符

    参考地址:《Java 泛型,你了解类型擦除吗?》 《Java中的逆变与协变》 《java 泛型中 T、E .....

  • Kotlin 泛型:协变、逆变

    1、Why?为什么需要泛型? 根本目的是在保证泛型类 类型安全的基础上,提高API的灵活性 2、How?如何保证类...

网友评论

      本文标题:Kotlin 泛型协变与逆变的理解

      本文链接:https://www.haomeiwen.com/subject/hqybtdtx.html