核心问题:型变解决的是"泛型类型的继承关系"问题。
想象一下你有一个盒子(泛型类):
Box<Apple>
:一个专门装苹果的盒子。Box<Fruit>
:一个可以装任何水果(苹果、香蕉、橙子...)的盒子。
直觉上你会觉得:既然 Apple
是 Fruit
的子类型,那 Box<Apple>
应该也是 Box<Fruit>
的子类型吧? 这样我就能把一盒苹果当成一盒水果来用(比如把苹果盒递给一个需要水果盒的地方)。
但默认情况下(不变 / Invariant),Kotlin (和 Java) 认为 Box<Apple>
和 Box<Fruit>
是完全没有关系的两种类型!
kotlin
class Box<T>(var item: T)
fun main() {
val appleBox: Box<Apple> = Box(Apple())
val fruitBox: Box<Fruit> = Box(Fruit())
// 编译错误!Type mismatch: required Box<Fruit>, found Box<Apple>
// fruitBox = appleBox // 不允许:不能把苹果盒赋值给水果盒变量
}
为什么不允许?安全!
假设允许 fruitBox = appleBox
,那么 fruitBox
这个变量现在指向的实际上是一个 Box<Apple>
。看起来没问题?危险在于写入操作!
kotlin
// 假设上面错误的赋值被允许了
fruitBox = appleBox // (错误赋值,假设编译器允许了)
// 现在 fruitBox 变量类型是 Box<Fruit>,但实际对象是 Box<Apple>
// 然后我往这个"水果盒"里放一个香蕉 (Banana 也是 Fruit)
fruitBox.item = Banana() // !!! 灾难 !!!
// 但实际上,appleBox 里的 item 必须是 Apple 类型!
// 现在 appleBox.item 被塞进了一个 Banana,程序在运行时会崩溃!
val apple: Apple = appleBox.item // 试图取出苹果,结果拿到香蕉 -> ClassCastException!
默认不变就是为了防止这种"把香蕉放进苹果盒"的错误。 编译器不知道你的 Box
是只用来读 (get
) 还是只用来写 (set
),所以它最安全的做法就是禁止 Box<Apple>
和 Box<Fruit>
之间的赋值。
型变的本质:告诉编译器更多信息,放宽限制(在保证安全的前提下)
我们需要一种方式告诉编译器:"我的这个泛型类,在某些场景下是安全的,你可以允许 Box<Apple>
当作 Box<Fruit>
来用,或者反过来"。这就是 out
和 in
的作用。
1. 协变 (Covariant) - out
(生产者,只取不出)
-
含义: 如果
A
是B
的子类型,那么Producer<A>
就是Producer<B>
的子类型。 -
关键词:
out
-
位置: 在声明泛型类/接口时使用(声明处型变)。
-
作用: 放宽输出(读取)的限制。
-
限制: 标记了
out
的类型参数T
只能出现在输出位置(方法的返回值类型) ,不能出现在输入位置(方法的参数类型)。编译器会严格检查这点。 -
为什么安全? 因为
T
只用于输出(你只能从里面拿东西出来),不能往里塞东西。既然不能往里塞,就不可能发生"把香蕉塞进苹果盒"的错误。 -
生活比喻: 生产者(Producer) 。想象一个水果榨汁机
Juicer<out T: Fruit>
。你给它水果(T
的子类型),它给你果汁。你放苹果进去,它产苹果汁;你放橙子进去,它产橙汁。重要的是:- 输出安全: 它承诺产出的果汁是
T
类型(或子类型)。如果它是Juicer<Apple>
,它保证产出苹果汁。你把它当作Juicer<Fruit>
使用时,你知道它产出的至少是果汁(可能是苹果汁、橙汁等),这完全没问题。 - 输入限制: 它本身不让你往果汁出口塞水果(
T
不能作为输入参数类型),所以安全。
- 输出安全: 它承诺产出的果汁是
-
Kotlin 例子:
List<out E>
-
List
在 Kotlin 标准库中声明为interface List<out E>
。 -
为什么?因为
List
在 Kotlin 默认是只读 的!你只能从List
里get
元素(输出),不能add
元素(输入 - 修改操作在MutableList
里)。 -
所以,它是安全的!
List<Apple>
可以被当作List<Fruit>
使用:kotlinval apples: List<Apple> = listOf(Apple(), Apple()) val fruits: List<Fruit> = apples // 成功!协变允许。因为只能读 val firstFruit: Fruit = fruits[0] // 安全,取出来的是 Apple (也是 Fruit) // fruits.add(Banana()) // 错误!Kotlin 的 List 没有 add 方法。安全!
-
2. 逆变 (Contravariant) - in
(消费者,只存不取)
-
含义: 如果
A
是B
的子类型,那么Consumer<B>
就是Consumer<A>
的子类型。注意方向反了! -
关键词:
in
-
位置: 在声明泛型类/接口时使用(声明处型变)。
-
作用: 放宽输入(写入)的限制。
-
限制: 标记了
in
的类型参数T
只能出现在输入位置(方法的参数类型) ,不能出现在输出位置(方法的返回值类型)。编译器会严格检查这点。 -
为什么安全? 因为
T
只用于输入(你只能往里面塞东西),不能从里面拿东西出来(除了像Any?
这种绝对安全的类型)。既然不能拿出来用,就不可能发生"期望拿到苹果却拿到香蕉"的错误。 -
生活比喻: 消费者(Consumer) 。想象一个垃圾桶
TrashCan<in T>
。你只能往里面扔垃圾(T
或其父类型)。如果它是TrashCan<Fruit>
:-
你可以扔水果(
Fruit
)、苹果(Apple
)、香蕉(Banana
)、甚至厨余垃圾(如果FoodWaste > Fruit
)------ 只要是T
(Fruit
) 或其父类型都行。 -
现在,把它当作
TrashCan<Apple>
使用(逆变允许TrashCan<Fruit>
是TrashCan<Apple>
的子类型!):cssval fruitTrashCan: TrashCan<Fruit> = TrashCan() val appleTrashCan: TrashCan<Apple> = fruitTrashCan // 逆变允许!TrashCan<Fruit> 可以赋值给 TrashCan<Apple> 变量 // 往"苹果垃圾桶"里扔一个苹果 (实际对象是 TrashCan<Fruit>) appleTrashCan.throwIn(Apple()) // 安全!Apple 是 Fruit // 往"苹果垃圾桶"里扔一个香蕉 (实际对象是 TrashCan<Fruit>) appleTrashCan.throwIn(Banana()) // 安全!Banana 也是 Fruit。苹果垃圾桶承诺能处理苹果,但实际它能处理所有水果,处理香蕉当然也没问题!
-
关键安全点:
appleTrashCan
变量要求你只能扔Apple
(或其子类型)。- 但实际指向的
fruitTrashCan
能处理所有Fruit
(包括Apple
和Banana
)。 - 所以,当你通过
appleTrashCan
扔Apple
时,fruitTrashCan
完全能处理。 - 当你通过
appleTrashCan
扔Banana
时,虽然appleTrashCan
变量类型只承诺处理Apple
,但扔Banana
在语法上符合appleTrashCan
的类型要求吗?不符合! 编译器会根据变量类型TrashCan<Apple>
检查,throwIn(Banana())
会报错:Banana
不是Apple
。所以你根本没法通过appleTrashCan
变量往里扔香蕉! 安全由调用方的类型检查保证了。 - 同时,
TrashCan
不能让你从里面拿出一个具体的T
(比如Apple
),因为T
只用于输入。你只能知道里面都是垃圾 (Any?
)。所以不可能发生类型错误。
-
-
Kotlin 例子:
Comparable<in T>
-
Comparable
接口声明:interface Comparable<in T> { fun compareTo(other: T): Int }
-
为什么是
in
?compareTo
只消费T
(作为参数输入) ,不生产T
(不返回T
)。 -
作用:允许更广泛的比较。
kotlinopen class Fruit class Apple : Fruit() val appleComparator: Comparable<Apple> = object : Comparable<Apple> { override fun compareTo(other: Apple): Int { ... } // 比较两个苹果 } // 逆变允许:Comparable<Apple> 是 Comparable<Fruit> 的子类型? // 不!记住逆变含义:如果 Apple 是 Fruit 的子类型,那么 Comparable<Fruit> 是 Comparable<Apple> 的子类型。 // 所以,一个能比较任意水果 (Fruit) 的比较器,可以被当作一个只能比较苹果 (Apple) 的比较器来使用: val fruitComparator: Comparable<Fruit> = ... // 能比较任何水果 val appleComp: Comparable<Apple> = fruitComparator // 逆变允许!安全 val apple1 = Apple() val apple2 = Apple() appleComp.compareTo(apple1, apple2) // 安全!fruitComparator 当然能比较两个苹果
- 一个要求比较
Fruit
的函数,可以传入一个Comparable<Fruit>
。 - 一个要求比较
Apple
的函数,根据逆变,也可以传入一个Comparable<Fruit>
(因为它能处理Fruit
,处理Apple
更没问题)。
- 一个要求比较
-
总结:关键区别与记忆口诀
特性 | 协变 (out ) |
逆变 (in ) |
---|---|---|
关键字 | out |
in |
含义 | Producer<A> 是 Producer<B> 的子类型 (当 A 是 B 的子类型) |
Consumer<B> 是 Consumer<A> 的子类型 (当 A 是 B 的子类型) |
方向 | 子类型关系与 T 一致 (A -> B) |
子类型关系与 T 相反 (B -> A) |
T 的位置 | 只能输出 (返回值) | 只能输入 (参数) |
角色 | 生产者 (Producer) - 你从 它那里获取 T |
消费者 (Consumer) - 你向 它提供 T |
安全保证 | 不能写入 T (防止污染) |
不能读取 T (除 Any? 外) (防止误用) |
常见例子 | Kotlin List<out E> , Source<out T> |
Kotlin Comparable<in T> , Consumer<in T> |
记忆口诀 | OUT = OUTPUT = 输出 = 只取不出 = 生产东西 | IN = INPUT = 输入 = 只存不取 = 消费东西 |
子类型赋值 | Producer<Apple> 可赋给 Producer<Fruit> |
Consumer<Fruit> 可赋给 Consumer<Apple> |
简单粗暴记忆法:
- 看到
out
:想着这个类像个只读的仓库 或者水果榨汁机 ,你只能从里面拿东西(T
) 出来用。它生产T
。所以Box<Apple>
可以安全地当作Box<Fruit>
用。 - 看到
in
:想着这个类像个垃圾桶 或者比较器 ,你只能往里面塞东西(T
) 。它消费T
。所以TrashCan<Fruit>
(能处理所有水果的桶) 可以安全地当作TrashCan<Apple>
(只能处理苹果的桶) 来用(因为大桶的容量完全覆盖小桶的需求)。
核心思想: out
和 in
是给编译器的承诺和安全约束 。通过限制类型参数 T
在类中出现的位置 (只能输出或只能输入),编译器就能在保证类型安全的前提下,允许更灵活的泛型类型赋值(协变或逆变),让你的代码更通用。记住 PECS (Producer-Extends, Consumer-Super) 原则(来自Java,对应Kotlin的 out
和 in
)也很有帮助:生产者用 out
(extends
), 消费者用 in
(super
)。理解了这个比喻和限制,型变就不再迷糊了!