1. 一句话直观记忆
-
协变(out) :读 出来安全(只读生产者)。如果 Cat <: Animal,那 Box <: Box 。
-
逆变(in) :写 进去安全(只写消费者)。如果 Cat <: Animal,那 Box <: Box 。
-
不变 :两边都不放过;Box 与 Box 没有子类型关系。
换成口号:生产者用 extends/out;消费者用 super/in;默认不变。
2. 形式化定义(看"子类型关系如何传递")
设 S <: T(S 是 T 的子类型),则:
- 协变: 方向保持
- 逆变: 方向相反
- 不变: 无子类型关系
3. Java 的三种型变(用通配符实现)
Java 的泛型默认不变;通过通配符 ? extends / ? super 在**使用处(use-site)**表达协/逆变。
3.1 协变:? extends T(只读生产者)
ini
List<? extends Number> src = List.of(1, 2, 3);
// src.add(4); // ❌ 编译错误:不能安全写入
Number n = src.get(0); // ✅ 读取安全(最小上界是 Number)
场景 :把 List 当作 List<? extends Number> 来读取元素。
代价:编译器禁止 add,因为你可能传入 Double 等破坏具体子类型。
3.2 逆变:? super T(只写消费者)
ini
List<? super Integer> dst = new ArrayList<Number>();
dst.add(123); // ✅ 可以写入 Integer(以及其子类)
Object x = dst.get(0); // 只能当 Object 读出(类型不精确)
场景 :把 List 当作 List<? super Integer> 来接收整数写入(消费者)。
代价:读出的精确类型丢失(只能是 Object)。
3.3 不变(默认)
ini
List<Integer> a = new ArrayList<>();
List<Number> b = a; // ❌ 不成立(不变)
结论:默认不变可以在编译期阻止很多潜在类型错配。
3.4 经典法则(PECS)
Producer Extends, Consumer Super****
-
生产者(只读来源)→ ? extends T
-
消费者(只写目标)→ ? super T
实战例:复制函数
javascript
static <T> void copy(List<? extends T> from, List<? super T> to) {
for (T e : from) to.add(e);
}
4. Kotlin 的型变(声明处 + 使用处)
Kotlin 在**声明处(declaration-site)**就能标注型变,更易用。
4.1 声明处协变out
kotlin
interface Source<out T> { fun get(): T }
// 因为只"产出 T",声明为 out T → Source<Cat> 可当作 Source<Animal>
- 约束:标了 out 的位置不能当参数使用(除非在类型投影等安全场合)。
4.2 声明处逆变in
kotlin
interface Sink<in T> { fun put(x: T) }
// 因为只"消费 T",声明为 in T → Sink<Animal> 可当作 Sink<Cat>
- 约束:标了 in 的位置不能作为返回类型(否则返回类型不安全)。
4.3 使用处投影(与 Java 通配符类似)
-
协变投影:List 等价 Java List<? extends Number>
-
逆变投影:MutableList 等价 Java List<? super Integer>
4.4 Kotlin 函数类型的型变(非常重要)
函数类型是参数逆变,返回协变:
kotlin
// (P) -> R 等价于 Function1<in P, out R>
val f: (Animal) -> Cat
val g: (Cat) -> Animal
// g 可以赋给 f 吗?参数逆变 → 需要 (super Cat);返回协变 → 需要 (sub Cat)
// 正确结论: (Animal)->Cat 可以赋给 (Cat)->Animal ? 反之更常见:
// 接口 Comparator<in T> / Producer<out T> 源于这一原则
更实用的例子:Comparator(只消费待比较对象,逆变);Iterable(只生产元素,协变)。
4.5 星投影
当你"不关心具体类型参数"时使用:
kotlin
val anyList: List<*> = listOf(1, 2, 3) // 只能以 Any? 读取,不可写
5. 数组的特例(高频面试点)
- Java 数组 是协变 的:Cat[] 可赋给 Animal[]。但这不类型安全,会在运行时抛出 ArrayStoreException:
ini
Animal[] a = new Cat[10];
a[0] = new Dog(); // 运行期 ArrayStoreException
- Kotlin Array 默认不变 ;需要型变时用 Array / Array 的投影。
6. 什么时候用哪种型变?
需求 | Java 写法 | Kotlin 写法 | 说明 |
---|---|---|---|
只读数据源(生产者) | List<? extends T> | List / 声明处 out | 可读不可写 |
只写接收端(消费者) | List<? super T> | MutableList / 声明处 in | 可写不可精确读 |
同时读写(容器、缓冲) | 具体 List(不变) | MutableList(不变) | 默认不变更稳 |
API 抽象的只读接口 | Iterable<? extends T> | Iterable | 设计层面协变 |
比较/回调(只消费) | Comparator<? super T> | Comparator | 逆变更宽松 |
函数类型 | N/A | (in P) -> out R | 记住"参逆返协" |
7. 实战模式与坑点
7.1 只读接口与可变实现分离(Kotlin/Android 推荐)
kotlin
interface Repo {
val items: List<Item> // 只读暴露(协变接口)
}
class RepoImpl : Repo {
private val _items = mutableListOf<Item>()
override val items: List<Item> get() = _items // 外部看不到可变方法
}
- 对外协变只读 ,对内持有可变实现,既安全又易维护。
7.2 复制/转换 API:PECS 套路
javascript
// Java:from 生产(extends),to 消费(super)
static <T> void copy(List<? extends T> from, List<? super T> to) {
for (T e : from) to.add(e);
}
7.3 比较器/回调要用逆变,兼容更广
ini
Comparator<? super Cat> cmp = Comparator.comparing(Animal::getAge);
// 可拿比较 Animal 的比较器来排 Cat,更灵活
7.4 避免"错误协变"导致的写入失效
- List<? extends Number> 看似"广",却不能 add ;若你既要读又要写,就不要用 extends/out,而是用不变 List 或把写入端单独抽出去。
8. 面试/复盘小抄(TL;DR)
-
协变 :读安全、写受限------生产者(Java ? extends / Kotlin out)。
-
逆变 :写安全、读不精确------消费者(Java ? super / Kotlin in)。
-
不变:默认、稳妥------读写都要时用它。
-
PECS:Producer Extends,Consumer Super。
-
函数类型 :参数逆变、返回协变。
-
数组:Java 协变但不安全(会 ArrayStoreException);Kotlin Array 不变。
-
API 设计:只读接口协变、消费者接口逆变;实现细节保持不变以便读写。