泛型与型变
泛型是静态类型语言中复用代码的核心工具。虽然 Kotlin 的泛型基于 JVM(存在类型擦除),但它通过声明点型变(Declaration-site variance)简化了泛型系统的复杂性,优化了 Java 中通配符的使用体验。
不变性问题
在泛型系统中,虽然 String 是 Any 的子类,但 MutableList<String> 并不是 MutableList<Any> 的子类。
val strs: MutableList<String> = mutableListOf("A", "B")
// ❌ 编译错误
// val objs: MutableList<Any> = strs
// objs.add(1) // 避免将 Int 存入 String 列表默认情况下,泛型是不变的 (Invariant)。这保证了运行时的类型安全,但在某些只读或只写的场景下限制了灵活性。为了建立泛型容器之间的继承关系,需要引入型变。
协变
如果一个泛型类仅作为生产者,即只从成员中返回(生产)类型 T 的数据,而不接收(消费)它,则可以使用协变。
- 关键字:
out - 语义:子类容器可以赋值给父类容器引用。
- 示例:
List<String>是List<Any>的子类。
interface Source<out T> {
fun nextT(): T // 允许作为返回值
}
fun demo(strs: Source<String>) {
val objs: Source<Any> = strs // 协变赋值成功
}逆变
如果一个泛型类仅作为消费者,即只接收(消费)类型 T 的数据,而不返回(生产)它,则可以使用逆变。
- 关键字:
in - 语义:父类容器可以赋值给子类容器引用。
- 示例:
Comparable<Any>是Comparable<String>的子类。
interface Comparable<in T> {
operator fun compareTo(other: T): Int // 允许作为参数
}
fun demo(x: Comparable<Number>) {
val y: Comparable<Double> = x // 逆变赋值成功
}类型投影
当类本身同时具备生产和消费行为(如 MutableList)而无法在声明时指定型变时,可以在使用处通过类型投影进行局部限制。
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?。
fun <T : Comparable<T>> sort(list: List<T>) { /*...*/ }非空约束
如果指定上界为 Any,则泛型参数不可为可空类型。
class Box<T : Any> // T 必须是非空类型多重约束
当泛型参数需要满足多个约束条件时,使用 where 子句。
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 模式。
// 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+
当泛型参数可以被编译器自动推断时,可以使用下划线 _ 来省略具体的类型声明。
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 告知编译器绕过检查。
public interface Collection<out E> : Iterable<E> {
public operator fun contains(element: @UnsafeVariance E): Boolean
}总结
- 型变准则:生产者使用
out(协变),消费者使用in(逆变)。 - 默认行为:支持读写操作的泛型默认是不变的。
- 声明点型变:Kotlin 倾向于在接口定义时指定型变,以精简调用端代码。
- 运行时限制:受限于 JVM 机制,需通过内联具体化处理泛型擦除问题。