泛型是在开发中非常重要的一种特性,可以帮助我们在不同类型之间复用相似的逻辑代码。包括:泛型基础使用、型变、星投影三大部分,泛型是对程序的抽象

Kotlin 泛型本质 - 图1
泛型的基础很简单, 用来定义泛型, 定义泛型上界: : 表示了这个泛型类继承了TV

型变

泛型是不变的,而型变就是为了解决泛型的不变性

型变讨论的基础在于子类和父类:已知CatAnimal的子类,MutableList<Animal>MutableList<Cat>编译器会认为这两个类是没有任何关系的,也就是说泛型是不变的

  1. //父类
  2. open class Animal {}
  3. //子类
  4. class Cat : Animal()
  5. //子类
  6. class Dog : Animal()
  1. 父类集合,传入子类集合(实际情况编辑器会报错)

    假设使用MutableList来代替MutableList,编译器不会阻止报错,那么会发生什么呢? ```kotlin fun foo(list:MutableList){ //list实际上是 -> MutableList 不能存储Dog list.add(Dog()) //① 出错 val animal:Animal = list[0] //实际上取出来的是Cat }

fun main() { //需要传递MutableList 实际上传递 MutableList 假设编译器不阻止报错 foo(mutableListOf(Cat())) }

  1. 上述代码中,list是需要Animal的集合,如果传入Cat集合,往list存入Dog其他的动物这样显然是不正确的
  2. 2. 子类集合,传入父类集合
  3. 假设使用MutableList<Animal>来代替MutableList<Cat>,假如编译器不会阻止报错,那么会发生什么呢?
  4. ```kotlin
  5. fun foo2(list:MutableList<Cat>){
  6. list.add(Cat())
  7. //① 出错
  8. val car:Cat = list[0] //假设编辑器通过,那么第一条数据,实际上取出来的是Animal对象而不是Cat对象
  9. }
  10. fun main() {
  11. foo2(mutableListOf<Animal>(Animal())) //编辑器会提示报错,假设这段代码通过
  12. }

上述代码中,list是需要Cat集合,如果传入Animal集合,list内部正常的add存储是没有问题的,但是取出第一条数据不是Cat对象,而是Animal对象。

所以在默认情况下,编译器会任务MutableList和MutableList之间不存在继承关系,它们之间无法替换,所以如果写上面的两种代码,编译器会提示报错,这就是泛型的不变性

泛型的逆变和协变就可以登场了

逆变

父子关系发生了颠倒就是泛型的逆变(父类泛型,代替子类泛型,父子关系发生颠倒),又分为使用处逆变和声明处的逆变

  1. open class TV {
  2. }
  3. class xiaomiTV : TV() {
  4. }
  5. fun foo(tv: TV) {
  6. }
  7. //class Controller<in T> 声明处型变
  8. class Controller<in T> {
  9. fun turnOn(tv: T) {
  10. }
  11. }
  12. //controller: Controller<in xiaomiTV> 使用处型变
  13. fun buy(controller: Controller<in xiaomiTV>) {
  14. val xiaomiTV = xiaomiTV()
  15. //打开小米电视机
  16. controller.turnOn(xiaomiTV)
  17. }
  18. fun main() {
  19. //xiaomiTV 是TV的子类
  20. foo(xiaomiTV())
  21. //万能遥控器 TV 是所有电视机的父类
  22. val controller = Controller<TV>()
  23. // Controller<xiaomiTV> 和 Controller<TV>()
  24. //添加了in 关键字,使用TV 代替了 xiaomiTV,也就是说TV是xiaomiTV的子类 万能的遥控器成了小米遥控器的子类了
  25. //父子关系发生了颠倒,这就是泛型的逆变,逆变有分为使用处逆变和声明处逆变
  26. buy(controller)
  27. }

协变

父子关系一致的现象(泛型默认是不变的,也就是父类和子类的泛型认为是没有关系的,而协变可以让泛型具备父子关系),就是泛型的协变

  1. open class Food {}
  2. //KFC 是 Food的子类
  3. class KFC : Food() {}
  4. //饭店的角色
  5. class Restaurant<out T> {
  6. fun orderFood(): T? {
  7. return null
  8. }
  9. }
  10. //点外卖的方法,找到饭店,<Food> 泛型为Food 食物类型即可
  11. fun orderFood(restaurant: Restaurant<out Food>){
  12. val food = restaurant.orderFood()
  13. }
  14. fun main(){
  15. //点击KFC
  16. val kfc = Restaurant<KFC>()
  17. //如果要使用Restaurant<KFC> 来代替 Restaurant<Food> ,Restaurant<KFC>看作是 Restaurant<Food>的子类
  18. orderFood(kfc)//报错了 因为类型不匹配,Restaurant<Food> 和 Restaurant<KFC> 因为泛型是不变的 编译器认为这两个类没有任何关系
  19. }

型变的使用场景

那么什么时候使用协变和逆变呢:Consumer in, Producer out ! 消费者 in,生产者 out

传入 in,传出 out。或者也可以说:泛型作为参数的时候,用 in,泛型作为返回值的时候,用 out。

逆变

  1. // 逆变
  2. // ↓
  3. public interface Comparable<in T> {
  4. // 泛型作为参数
  5. // ↓
  6. public operator fun compareTo(other: T): Int
  7. }

协变

  1. // 协变
  2. // ↓
  3. public interface Iterator<out T> {
  4. // 泛型作为返回值
  5. // ↓
  6. public operator fun next(): T
  7. public operator fun hasNext(): Boolean
  8. }

需要注意的地方:Success() 方法中,泛型T作为入参,为什么还要使用协变out呢?这是因为val 只读的属性,只有getter没有setter

  1. sealed class Result<out R> {
  2. // 改为var后,编译器就会立马报错
  3. // ↓
  4. data class Success<out T>(val data: T, val message: String = "") : Result<T>()
  5. data class Error(val exception: Exception) : Result<Nothing>()
  6. data class Loading(val time: Long = System.currentTimeMillis()) : Result<Nothing>()
  7. }

也就是说,如果泛型的 T,既是函数的参数类型,又是函数的返回值类型,那么,我们就无法直接使用 in 或者 out 来修饰泛型 T。

函数传入参数的时候,并不一定就意味着写入,这时候,即使泛型 T 是作为参数类型,我们也仍然要想一些办法来用 out 修饰泛型
官方的实例代码:

  1. // 协变
  2. // ↓
  3. public interface List<out E> : Collection<E> {
  4. // 泛型作为返回值
  5. // ↓
  6. public operator fun get(index: Int): E
  7. // 泛型作为参数
  8. // ↓
  9. override fun contains(element: @UnsafeVariance E): Boolean
  10. // 泛型作为参数
  11. // ↓
  12. public fun indexOf(element: @UnsafeVariance E): Int
  13. }

Kotlin 官方源码当中的 List,也就是这里的泛型 E,它既作为了返回值类型,又作为了参数类型。在正常情况下,如果我们用 out 修饰 E,那编译器是会报错的。但我们其实很清楚,对于 contains、indexOf 这样的方法,它们虽然以 E 作为参数类型,但本质上并没有产生写入的行为。所以,我们用 out 修饰 E 并不会带来实际的问题。所以这个时候,我们就可以通过 @UnsafeVariance 这样的注解,来让编译器忽略这个型变冲突的问题。

如下代码,注意泛型P使用in修饰逆变,P作为参数入参,泛型T用out修饰协变作为返回值类型,但是注意instance变量使用var修饰,var 有getter和setter,而T为什么不会报错呢?这是因为instance使用的是private修饰的如果删除private就是报错

  1. //in P 逆变作为泛型参数入参,out T 协变作为泛型返回值类型
  2. abstract class BaseSingleton<in P, out T> {
  3. //var 修饰了 out T,var 有setter为什么不会报错呢?是因为它是private修饰的去掉private就会报错
  4. private var instance: T? = null
  5. protected abstract val creator: (P) -> T
  6. fun getInstance(param: P): T =
  7. instance ?: synchronized(this) {
  8. instance ?: creator(param).also { instance = it }
  9. }
  10. }
  • 泛型,是对程序的一种抽象。通过泛型,我们可以实现代码逻辑复用的目的,Kotlin 标准库当中很多源代码也都是借助泛型来实现的。
  • 从型变的位置来分类的话,分为使用处型变和声明处型变。
  • 从型变的父子关系来分类的话,分为逆变和协变。逆变表示父子关系颠倒了,而协变表示父子关系和原来一致。
  • 型变的口诀:泛型作为参数,用 in;泛型作为返回值,用 out。
  • 在特殊场景下,同时作为参数和返回值的泛型参数,我们可以用 @UnsafeVariance 来解决型变冲突。
  • 星投影,就是当我们对泛型的具体类型不感兴趣的时候,直接传入一个“星号”作为泛型的实参。