Skip to content

泛型与型变

源:Generics: in, out, where

泛型是静态类型语言中复用代码的核心工具。虽然 Kotlin 的泛型基于 JVM(存在类型擦除),但它通过声明点型变(Declaration-site variance)简化了泛型系统的复杂性,优化了 Java 中通配符的使用体验。

不变性问题

在泛型系统中,虽然 StringAny 的子类,但 MutableList<String> 并不是 MutableList<Any> 的子类。

kotlin
val strs: MutableList<String> = mutableListOf("A", "B")
// ❌ 编译错误
// val objs: MutableList<Any> = strs 
// objs.add(1) // 避免将 Int 存入 String 列表

默认情况下,泛型是不变的 (Invariant)。这保证了运行时的类型安全,但在某些只读或只写的场景下限制了灵活性。为了建立泛型容器之间的继承关系,需要引入型变。

协变

如果一个泛型类仅作为生产者,即只从成员中返回(生产)类型 T 的数据,而不接收(消费)它,则可以使用协变。

  • 关键字out
  • 语义:子类容器可以赋值给父类容器引用。
  • 示例List<String>List<Any> 的子类。
kotlin
interface Source<out T> {
    fun nextT(): T // 允许作为返回值
}

fun demo(strs: Source<String>) {
    val objs: Source<Any> = strs // 协变赋值成功
}

逆变

如果一个泛型类仅作为消费者,即只接收(消费)类型 T 的数据,而不返回(生产)它,则可以使用逆变。

  • 关键字in
  • 语义:父类容器可以赋值给子类容器引用。
  • 示例Comparable<Any>Comparable<String> 的子类。
kotlin
interface Comparable<in T> {
    operator fun compareTo(other: T): Int // 允许作为参数
}

fun demo(x: Comparable<Number>) {
    val y: Comparable<Double> = x // 逆变赋值成功
}

类型投影

当类本身同时具备生产和消费行为(如 MutableList)而无法在声明时指定型变时,可以在使用处通过类型投影进行局部限制。

kotlin
fun copy(from: MutableList<out Any>, to: MutableList<Any>) {
    val item = from[0] // 允许读取
    // from.add(item)  // 禁止写入,from 被投影为只读视图
}

星投影

星投影 * 用于表示对具体类型参数一无所知,但需要以安全方式操作的场景。

  • 对于 Foo<out T>Foo<*> 等价于 Foo<out Any?>
  • 对于 Foo<in T>Foo<*> 等价于 Foo<in Nothing>

泛型约束

上界约束

使用冒号指定泛型参数必须继承的基类或实现的接口,默认上界为 Any?

kotlin
fun <T : Comparable<T>> sort(list: List<T>) { /*...*/ }

非空约束

如果指定上界为 Any,则泛型参数不可为可空类型。

kotlin
class Box<T : Any> // T 必须是非空类型

多重约束

当泛型参数需要满足多个约束条件时,使用 where 子句。

kotlin
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return list.filter { it > threshold }.map { it.toString() }
}

递归泛型

有时泛型参数需要引用其自身。最典型的例子是 Comparable 接口,或者是实现链式调用的 Builder 模式。

kotlin
// T 必须实现 Comparable,且比较的对象也是 T 类型
fun <T : Comparable<T>> sort(list: List<T>) { ... }

// Builder 模式中的自引用
abstract class Builder<T : Builder<T>> {
    fun setProp(value: String): T {
        // ...
        @Suppress("UNCHECKED_CAST")
        return this as T // 强制转换为子类类型,实现链式调用
    }
}

下划线操作符 Kotlin 1.7+

当泛型参数可以被编译器自动推断时,可以使用下划线 _ 来省略具体的类型声明。

kotlin
abstract class SomeClass<K, V> {
    abstract fun execute()
}

// 编译器能推断出 V 是 Int,但需要显式指定 String
val runner = object : SomeClass<String, _>() {
    override fun execute() { ... }
}

类型擦除与 Reified

在 JVM 环境下,泛型信息在编译后会被擦除。在运行时,List<String>List<Int> 的类型相同。

如需在运行时获取泛型类型信息,必须配合内联函数的 reified 关键字。

详细信息参考:内联函数与 Reified

UnsafeVariance 注解

在某些特殊场景下,协变类(out)需要在参数位置接收泛型类型。例如 Collection<out E> 接口中的 contains(element: E) 方法。

由于该操作仅用于读取比较,不涉及写入导致的类型污染,可以使用 @UnsafeVariance 告知编译器绕过检查。

kotlin
public interface Collection<out E> : Iterable<E> {
    public operator fun contains(element: @UnsafeVariance E): Boolean
}

总结

  • 型变准则:生产者使用 out(协变),消费者使用 in(逆变)。
  • 默认行为:支持读写操作的泛型默认是不变的。
  • 声明点型变:Kotlin 倾向于在接口定义时指定型变,以精简调用端代码。
  • 运行时限制:受限于 JVM 机制,需通过内联具体化处理泛型擦除问题。