什么是形变(Type Variance)?

在类型系统中,协变(covariance) 和 逆变(contravariance) 统称为 型变(variance) 。它们描述的是在类型变换的作用下,子类型关系是否以及如何发生变化。
什么是型变(Variance)?
我们先看一个抽象模型:
- 设有两个类型
X和Y,若X ≤ Y表示X是Y的子类型。 - 设
f是一种 类型变换 ,即将类型映射为另一个复合类型,例如:f(T)可以是List<T>、T[]等。 - 那么根据
f(X)与f(Y)的子类型关系,我们可以将f分为以下四类:
| 名称 | 表达式关系 | 含义 |
|---|---|---|
| 协变 | X ≤ Y⇒f(X) ≤ f(Y) |
子类型关系保持不变 |
| 逆变 | X ≤ Y⇒f(Y) ≤ f(X) |
子类型关系被反转 |
| 双变 | X ≤ Y⇒f(X) ≤ f(Y)且f(Y) ≤ f(X) |
双向成立,类型变换前后两者相互兼容 |
| 不可变 | X ≤ Y但f(X)与f(Y)不可比较 |
类型变换后无子类型关系,类型之间完全独立 |
✅ 型变描述的是:子类型关系在类型变换之后是否还能“传递下去”。
示例说明:以 Animal 和 Cat 为例
假设:
Cat ≤ Animal
也就是说 Cat 是 Animal 的子类。那么不同类型变换下构造出来的新类型关系可能如下:
1. 协变(Covariant) :类型关系保持
比如 Java 的数组类型:
Animal[] animals = new Cat[10]; // 合法
Cat[] ≤ Animal[],数组类型保持了原始的子类型关系 —— 说明 Java 中的数组是 协变的 。
还有Java 的泛型通配符 <? extends T>:
List<? extends Animal> list = new ArrayList<Cat>();
Animal a = list.get(0); // ✅ 安全读取
list.add(new Dog()); // ❌ 编译错误,不能写入
此时,你不能往 list 中添加任何元素(除了 null),但可以安全地读取出 Animal 类型的对象。
这就是协变的典型特征:“只读” —— 你可以读取(out),但不能写入(in)。
2. 逆变(Contravariant) :类型关系反转
比如 Java 的泛型通配符 <? super T>:
List<? super Cat> list = new ArrayList<Animal>(); // 合法
list.add(new Cat()); // ✅ 合法:写入 Cat
list.add(new BlackCat()); // ✅ 合法:BlackCat extends Cat
Object o = list.get(0); // ✅ 合法,但只能是 Object 类型
Cat c = list.get(0); // ❌ 编译错误:无法确保读出来的就是 Cat
虽然 Cat ≤ Animal,但 List<Animal> 却能赋值给 List<? super Cat>,说明泛型的 super 位置是 逆变的 。此时,你可以往list写入,但是不能读取。
这就是协变的典型特征:“只写” —— 你可以写入(out),但不能读取(in)。
3. 不可变(Invariant) :关系不传递
Java 中普通泛型默认是不可变的:
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // ❌ 编译错误
即使 Cat ≤ Animal,但 List<Cat> 与 List<Animal> 没有任何子类型关系,说明泛型 List<T> 是默认 不可变的 。
4. 双变(Bivariant) :Java 不支持,但其他语言可能支持
在一些语言或场景下,某些类型可能既支持协变又支持逆变,比如某些函数类型或结构体字段类型,但 Java 中几乎不支持双变(由于类型安全问题)。
小结
类型变换f |
举例(以 Java 为例) | 所属变型类型 |
|---|---|---|
T[] |
数组类型 | 协变 |
List |
泛型的extends |
协变 |
List |
泛型的super |
逆变 |
List |
泛型默认 | 不可变 |
| — | 多数函数类型参数 & 返回值(其他语言) | 双变(视情况而定) |
标题:什么是形变(Type Variance)?
作者:Gakkiyomi
地址:https://gakkiyomi.blog/articles/2025/07/19/1752897315152.html
哇咔咔