深入理解 Java/Kotlin 协变与逆变
型变(Variance)是泛型编程中最容易让人困惑的概念之一。本文从问题出发,讲清原理,落到实战。
1. 引子:一个"理所当然"的错误
java
// Java
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // ❌ 编译错误!
直觉上 Cat 是 Animal 的子类,List<Cat> 应该也能赋值给 List<Animal>。但编译器拒绝了------为什么?
java
List<Animal> animals = cats;
animals.add(new Dog()); // 合法! Animal 列表可以加 Dog
Cat cat = cats.get(0); // 💥 取出一只 Dog,却以为是 Cat
如果允许这种赋值,类型安全就会被破坏。 这就是"型变"要解决的问题:如何在保持类型安全的前提下,让泛型也能享受继承带来的灵活性。
2. 三种型变关系
假设 Cat extends Animal,对于泛型 Box<Cat> 和 Box<Animal>:
| 型变 | 关系 | 含义 |
|---|---|---|
| 协变 Covariance | Box<Cat> 是 Box<Animal> 的子类型 |
继承方向保持一致 |
| 逆变 Contravariance | Box<Animal> 是 Box<Cat> 的子类型 |
继承方向发生反转 |
| 不变 Invariance | 两者没有任何继承关系 | 默认行为 |
swift
协变: Cat → Animal ⟹ Producer<Cat> → Producer<Animal> (方向一致 ↑↑)
逆变: Cat → Animal ⟹ Consumer<Animal> → Consumer<Cat> (方向反转 ↑↓)
不变: Cat → Animal ⟹ Box<Cat> ✗ Box<Animal> (毫无关系)
3. Java 的型变机制
3.1 数组协变:一个历史遗留的坑
Java 数组是协变的,这在编译期不会报错,但运行时可能崩溃:
java
Cat[] cats = {new Cat(), new Cat()};
Animal[] animals = cats; // ✅ 编译通过(数组协变)
animals[0] = new Dog(); // 💥 运行时 ArrayStoreException!
数组在运行时仍记得自己是 Cat[],存入 Dog 时抛出异常。这是 Java 早期为了兼容没有泛型的代码而做的妥协,后来被泛型的"不变"设计纠正。
3.2 使用处型变:通配符
Java 泛型默认不变 ,灵活性靠使用处型变(在使用泛型的地方声明):
java
// ? extends T ------ 协变:可以接受 T 及其子类
void feedAll(List<? extends Animal> animals) {
for (Animal a : animals) {
a.eat(); // ✅ 读取安全:一定读出 Animal
}
// animals.add(new Cat()); // ❌ 编译错误:无法确定具体类型
}
// ? super T ------ 逆变:可以接受 T 及其父类
void addCats(List<? super Cat> list) {
list.add(new Cat()); // ✅ 写入安全:Cat 可以放进任何父类容器
// Cat c = list.get(0); // ❌ 只能以 Object 接收
}
口诀:extends 只能读,super 只能写。
3.3 PECS 原则
Producer Extends, Consumer Super ------ 生产者用
extends,消费者用super。
JDK 经典案例 Collections.copy:
java
public static <T> void copy(List<? super T> dest, // dest 是消费者:写入
List<? extends T> src) // src 是生产者:读取
{
// src 生产数据(读) → dest 消费数据(写)
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
// 使用:
List<Animal> animals = new ArrayList<>();
List<Cat> cats = List.of(new Cat());
Collections.copy(animals, cats); // ✅ Cat 列表 → Animal 列表
4. Kotlin 的型变机制
Kotlin 认为 Java 的"每次使用都要写通配符"太啰嗦,于是把型变声明移到了类的定义处。
4.1 声明处型变:out 与 in
kotlin
// out T ------ 协变:该类只能"生产"T(作为返回值)
interface Producer<out T> {
fun produce(): T // ✅ T 出现在返回位置
// fun consume(t: T) // ❌ 编译错误:out 类型不能用作参数
}
// in T ------ 逆变:该类只能"消费"T(作为参数)
interface Consumer<in T> {
fun consume(t: T) // ✅ T 出现在参数位置
// fun produce(): T // ❌ 编译错误:in 类型不能用作返回值
}
使用时就无需再写通配符:
kotlin
val catProducer: Producer<Cat> = ...
val animalProducer: Producer<Animal> = catProducer // ✅ 协变
val animalConsumer: Consumer<Animal> = ...
val catConsumer: Consumer<Cat> = animalConsumer // ✅ 逆变
4.2 位置限制规则
| 声明 | 允许出现的位置 | 编译器的保证 |
|---|---|---|
out T(协变) |
仅返回值位置(out-position) | 只出不进,读取一定安全 |
in T(逆变) |
仅参数位置(in-position) | 只进不出,写入一定安全 |
对比记忆:
r
out T ≈ Java 的 ? extends T (生产者,往外给)
in T ≈ Java 的 ? super T (消费者,往里收)
4.3 使用处型变(类型投影)
当类本身没有声明型变时,Kotlin 也支持在使用处投影:
kotlin
class Box<T>(var value: T) // 默认不变
fun copy(from: Box<out Animal>, to: Box<in Animal>) {
to.value = from.value // ✅ 读 from(协变),写 to(逆变)
}
4.4 星号投影 <*>
kotlin
val list: MutableList<*> = mutableListOf("a", 1, true)
// 等价于 MutableList<out Any?>:只能读(读出来是 Any?),不能写
5. 原理:为什么"只出不进"是安全的
型变的本质,是编译器用位置限制换取子类型关系。
rust
协变 Box<out T> 能安全地当 Box<out Animal> 用,前提是:
你只能通过它"拿"东西 ------ 拿到的一定是 Animal ✓
禁止"放"东西 ------ 否则可能往 Box<Cat> 里放 Dog ✗
逆变 Box<in T> 能安全地当 Box<in Cat> 用,前提是:
你只能通过它"放"东西 ------ 放 Cat 进 Animal 容器没问题 ✓
禁止"拿"东西 ------ 拿出来的可能是任何 Animal,不保证是 Cat ✗
记忆技巧:把泛型容器想成仓库。
markdown
Producer<out T> = 供货仓库
只能出货 ──► 给出来的什么都是 Animal,安全 ✓
Consumer<in T> = 收货仓库
只能收货 ──► Cat 一定能放进 Animal 的仓库,安全 ✓
6. 实战应用
6.1 Kotlin 标准库的设计
kotlin
// List 只读 → 声明为协变
interface List<out E> : Collection<E>
// MutableList 可读写 → 必须不变
interface MutableList<E> : List<E>, MutableCollection<E>
// 因此:
val cats: List<Cat> = listOf(Cat())
val animals: List<Animal> = cats // ✅ 协变
val mCats: MutableList<Cat> = mutableListOf(Cat())
// val mAnimals: MutableList<Animal> = mCats // ❌ 不变,编译错误
设计启示:只读接口加 out,只写接口加 in,读写都有就保持不变。
6.2 Function 类型的型变签名
Kotlin 函数类型的定义完美体现了 PECS:
kotlin
// 参数用 in(消费参数),返回值用 out(生产结果)
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}
所以下面的赋值合法:
kotlin
val f1: (Animal) -> Cat = { animal -> Cat() }
val f2: (Cat) -> Animal = f1 // ✅
// 分析:
// 参数: (Animal) 可以当 (Cat) 用 → 逆变 (传 Cat 进去,f1 收 Animal 没问题)
// 返回值: Cat 可以当 Animal 用 → 协变 (返回 Cat 就是返回 Animal)
记忆:参数逆变,返回值协变(函数类型也是"消费参数、生产结果")。
6.3 Android 实战:通用事件回调
kotlin
// 事件监听器:只消费事件 → 用 in
interface OnEventListener<in T> {
fun onEvent(event: T)
}
open class BaseEvent
class ClickEvent : BaseEvent()
// 一个监听所有基础事件的监听器,可以直接用于 ClickEvent
val baseListener = object : OnEventListener<BaseEvent> {
override fun onEvent(event: BaseEvent) = println("收到: $event")
}
val clickListener: OnEventListener<ClickEvent> = baseListener // ✅ 逆变
clickListener.onEvent(ClickEvent()) // 输出: 收到: ClickEvent@...
6.4 通用数据生产者
kotlin
interface DataSource<out T> {
fun load(): List<T>
}
class CatDataSource : DataSource<Cat> {
override fun load() = listOf(Cat(), Cat())
}
// 可以统一交给处理 Animal 的函数
fun printAll(source: DataSource<Animal>) {
source.load().forEach { println(it) }
}
printAll(CatDataSource()) // ✅ 协变:DataSource<Cat> → DataSource<Animal>
7. 对比总结与面试速记
Java vs Kotlin 型变对比
| 维度 | Java | Kotlin |
|---|---|---|
| 型变位置 | 使用处(每次用都要写) | 声明处为主(定义一次,处处生效) |
| 协变写法 | ? extends T |
out T / 声明处 class Foo<out T> |
| 逆变写法 | ? super T |
in T / 声明处 class Foo<in T> |
| 通配任意类型 | <?> |
<*>(星号投影) |
| 数组 | 协变(有运行时风险) | Array<T> 不变(修正了 Java 缺陷) |
| 逃生舱 | 无(使用处声明) | @UnsafeVariance 注解 |
kotlin
interface List<out E> {
// contains 需要 E 作参数(in 位置),但 out E 不允许!
// 实际上 contains 只是"比较",不会真的写入,是安全的
fun contains(element: @UnsafeVariance E): Boolean // ✅ 逃生舱
}