本文地址


10 | 泛型:逆变or协变,傻傻分不清?

泛型基础

使用泛型可以复用程序代码的逻辑。

  • 类名 的后面加上 <T>,可以为类增加泛型支持。
class TV
class MiTV : TV

class Controller1<T>      // T 代表泛型的形参,【形参】的意思是:可以随便取一个名字作为类型,比如这里的 T
class Controller2<T : TV> // 指定泛型的上界:泛型实参必须是 TV 或其子类

fun main() {
    Controller1<String>() // String 代表泛型的实参,【实参】的意思是:一个具体的类型,比如这里的 String
    Controller1<MiTV>()
    Controller2<MiTV>()
}
  • 直接在 fun 关键字的后面加上 <T>,可以为函数增加泛型支持。
fun <T> turnOn(tv: T) = println(tv.toString())
fun <T : TV> turnOff(tv: T) = println(tv.toString())

泛型的不变性

如果 Cat、Dog 是 Animal 的子类,那么编译器会认为:

  • 对象 Home<Cat> 与对象 Home<Animal> 两者是 没有任何关系
  • 集合 MutableList<Cat> 与集合 MutableList<Animal> 两者是 没有任何关系

这就是 泛型的不变性

泛型类的不变性

open class Animal
class Cat : Animal()
class Home<T>

fun foo1(home: Home<Animal>) = println("foo1")
fun foo2(home: Home<Cat>) = println("foo2")

fun main() {
    foo1(Home<Animal>())
    foo2(Home<Cat>())

    //foo1(Home<Cat>())    // 需要父类泛型,传入子类泛型,会编译失败
    //foo2(Home<Animal>()) // 需要子类泛型,传入父类泛型,会编译失败
}
  • 需要携带泛型为 Animal 的对象时,如果对象携带泛型为 Cat,会编译失败
  • 需要携带泛型为 Cat 的对象时,如果对象携带泛型为 Animal,会编译失败

泛型集合的不变性

open class Animal
class Dog : Animal()
class Cat : Animal()

fun foo1(list: MutableList<Animal>) { // 需要父类集合
    list.add(Dog())                   // 假如可以传入 Cat 集合,因为 Cat 集合不能存 Dog,那么这里就会出问题
}

fun foo2(list: MutableList<Cat>) { // 需要子类集合
    val cat: Cat = list[0]         // 假如可以传入 Animal 集合,因为 Animal 集合中可能有 Dog,那么这里就会出问题
}

fun main() {
    val cats: MutableList<Cat> = mutableListOf(Cat())
    val animals: MutableList<Animal> = mutableListOf(Dog()) // 创建一个 Animal 集合,并且有一个 Dog

    foo1(animals)
    foo2(cats)

    //foo1(cats)    // 需要父类集合,传入子类集合,会编译失败
    //foo2(animals) // 需要子类集合,传入父类集合,会编译失败
}
  • 需要 Animal 集合时,如果传入的是 Cat 集合,会编译失败
  • 需要 Cat 集合时,如果传入的是 Animal 集合,会编译失败

泛型的型变 Variance

在某些场景下,泛型的不变性 会给我们带来麻烦,泛型的 型变,就是为了解决泛型的不变性问题。

  • 逆变in - 作为参数传入 - <? super T> - 可以写入,不可以读取(只能以 Any? 读取)
  • 协变out - 作为返回值传出 - <? extends T> - 可以读取,不可以写入(只能写入 Nothing)

总结

  • 逆变 Covariant:泛型 T 最终会以函数的参数的形式,被传入函数的里面,这往往是一种写入行为,这时候,使用关键字 in
  • 协变 Contravariant:泛型 T,最终会以返回值的形式,被传出函数的外面,这往往是一种读取行为,这时候,使用关键字 out
  • Consumer in, Producer out :消费者使用 in,生产者使用 out
  • 传入 in,传出 out
  • 泛型作为参数用 in,泛型作为返回值用 out
  • 函数传入参数的时候,并不一定就意味着写入
  • 某些情况下,valprivate var,可以用 out,因为其也满足 可以读取不可以写入 等特性
  • 正常情况下,同时作为参数和返回值的泛型参数,无法直接使用 in 或者 out 来修饰泛型
  • 特殊场景下,同时作为参数和返回值的泛型参数,可以用 @UnsafeVariance 解决型变冲突

声明处型变

声明处型变,就是修改泛型参数声明处的代码,即在泛型形参前加 in/out

  • 逆变和协变,都属于型变
  • Java 中没有声明处型变,只有使用处型变
  • 所谓的型变,对应到 Java 中,是指有 ? 的泛型
    • 类似 <? super Animal><? extends Animal> 这种属于型变
    • 类似 <T super Animal><T extends Animal> 这种不属于型变

声明处逆变

open class Animal
class Cat : Animal()
class Home<in T> // 声明处逆变,在泛型形参前加 in。Java 中没有声明处型变,因为不能在这里出现 <? super T>

fun foo2(home: Home<Cat>) = println("foo2")

fun main() {
    foo2(Home<Cat>())
    foo2(Home<Animal>())
}
  • 修改后,当需要 Home<Cat> 时,就可以传入 Home<Animal>
  • 此时,可以认为 Home<Cat>Home<Animal> 的父类,这种父子关系颠倒的现象,就叫做 泛型的逆变

声明处协变

open class Animal
class Cat : Animal()
class Home<out T> // 声明处协变,在泛型形参前加 out。Java 中没有声明处型变,因为不能在这里出现 <? extends T>

fun foo1(home: Home<Animal>) = println("foo1")

fun main() {
    foo1(Home<Animal>())
    foo1(Home<Cat>())
}
  • 修改后,当需要 Home<Animal> 时,就可以传入 Home<Cat>
  • 此时,仍可认为 Home<Animal>Home<Cat> 的父类,这种父子关系一致的现象,就叫做 泛型的协变

使用处型变

使用处型变,就是修改泛型参数使用处的代码,即在泛型实参声明前加 in/out

Java 中,在构造泛型对象时也支持使用处型变,例如 List<? extends Number> list=new ArrayList<>();

泛型类

open class Animal
class Cat : Animal()
class Home<T>

fun foo1(home: Home<out Animal>) = println("foo1") // 使用处协变,在泛型实参前加 out
fun foo2(home: Home<in Cat>) = println("foo2")     // 使用处逆变,在泛型实参前加 in

fun main() {
    foo1(Home<Animal>())
    foo2(Home<Cat>())

    foo1(Home<Cat>())
    foo2(Home<Animal>())
}

泛型集合 -- 更常见的案例

  • 逆变in - 作为参数传入 - <? super T> - 可以写入,不可以读取(只能以 Any? 读取)
  • 协变out - 作为返回值传出 - <? extends T> - 可以读取,不可以写入(只能写入 Nothing)
open class Animal
class Dog : Animal()
class Cat : Animal()

fun foo1(list: MutableList<out Animal>) { // 使用处协变,在泛型实参前加 out
    var animal: Animal = list[0] //协变,可以读取
    //list.add(Dog()) // Type mismatch. Required Nothing, Found Dog
    list.add(throw Exception()) // 协变,不可以写入(只能写入 Nothing)
}

fun foo2(list: MutableList<in Cat>) {     // 使用处逆变,在泛型实参前加 in
    //val cat: Cat = list[0] // Type mismatch. Required Cat, Found Any?
    list.add(Cat()) //逆变,可以写入
    val cat: Any? = list[0] // 逆变,不可以读取(只能以 Any? 读取)
}

fun main() {
    val cats: MutableList<Cat> = mutableListOf(Cat())
    val animals: MutableList<Animal> = mutableListOf(Dog())

    foo1(animals)
    foo2(cats)

    foo1(cats)
    foo2(animals)
}

星投影 Star-Projections

所谓的星投影就是,当我们不关心实参到底是什么的时候,可以用星号作为泛型的实参

使用案例

class Home<T> {
    fun getT(): T = // 返回一个 T 的实例,这里用到泛型实例的语法,暂时先不管他
}

fun getHome(): Home<*> = // 不关心实参到底是什么,就可以用星号 * 作为泛型的实参

fun main() {
    val home: Home<*> = getHome() // 返回值类型是 Home<*>,无法确定其中泛型的实参是什么类型
    val t: Any? = home.getT()     // 无法确定返回值的类型,因此,这里【只能】看作是【Any?】类型
}
  • 在 getHome() 中,我们没有传递任何具体的类型给 Home,而是使用了星号作为 Home 的泛型实参,因此,我们就无法知道 Home 到底是什么类型。
  • 相应的,当我们调用 home.getT() 的时候,就无法确定它的返回值到底是什么类型。这时候,变量 t 的实际类型可能是任意的,比如 String、Int、Animal、Cat,甚至可能是 null,因此,我们只能将其看作是 Any? 类型。

增加上界

上面的代码中,如果我们为 Home 的泛型类型加上边界的话,t 的类型就可以更精确一些。

open class Animal
class Home<T : Animal> { // 为 泛型增加了上界 Animal
    fun getT(): T =
}

fun getHome(): Home<*> =

fun main() {
    val home: Home<*> = getHome()
    val animal: Animal = home.getT() // 返回值是 Animal 或其子类
}

为 Home 泛型类型增加了上界 Animal 以后,即使使用了星投影,仍然可以通过调用 home.getT() 拿到 Animal 类型的变量。

小结

Kotlin 朱涛-10 泛型 型变 逆变 in 协变 out 星投影

2016-05-14

相关文章:

  • 2021-12-04
  • 2022-01-16
  • 2022-03-04
  • 2021-05-26
  • 2021-08-19
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-04-06
  • 2022-01-17
  • 2021-06-04
  • 2022-02-25
  • 2021-11-01
相关资源
相似解决方案