深入理解协变与逆变:从 Java 到 Swift

本文最后更新于 2026年7月24日 早上

一篇搞懂泛型变体(Variance)在两门语言中的不同哲学

前言

协变(Covariance)和逆变(Contravariance)是类型系统中最容易让人困惑的概念之一。很多开发者即使写过几年泛型代码,面对 ? extends T? super T 时依然要犹豫半天。

这篇文章会用 Java 作为主力讲解语言(因为它的通配符机制是理解变体的最佳教材),然后再对比 Swift 的实现差异。读完你会明白:

  • 协变/逆变到底在解决什么问题
  • Java 的 PECS 原则为什么成立
  • Swift 为什么没有通配符,却依然存在变体
  • 两套设计各自的应用场景

一、预备知识:一个继承体系

为了后续讨论,我们先搭一个简单的类继承结构:

1
2
3
class Animal { }
class Dog extends Animal { }
class Cat extends Animal { }

DogCat 都是 Animal 的子类,这是所有讨论的起点。

二、什么是协变(Covariance)?

协变是指:保留类型的继承方向。

用大白话说:如果 DogAnimal 的子类,那么某种「容器类型」Container<Dog> 也应该被视为 Container<Animal> 的子类。

Java 中的协变:? extends T

1
2
List<Dog> dogs = new ArrayList<>();
List<? extends Animal> list = dogs; // ✅ 协变赋值

这里 List<? extends Animal> 可以接收 List<Dog>,因为继承方向被保留了。

协变的限制:只能读,不能写

1
2
3
4
Animal a = list.get(0);   // ✅ 读取没问题,拿出来的一定是 Animal
list.add(new Dog()); // ❌ 编译错误!
list.add(new Animal()); // ❌ 编译错误!
list.add(null); // ✅ 只有 null 是例外

为什么禁止写入?因为编译器不知道 list 实际是 List<Dog> 还是 List<Cat>。如果允许写入 Dog,万一实际类型是 List<Cat>,类型安全就被破坏了。

结论:协变适合「生产者」(Producer)—— 我从中读取数据。

三、什么是逆变(Contravariance)?

逆变是指:反转类型的继承方向。

如果 DogAnimal 的子类,那么 Container<Animal> 反而是 Container<? super Dog> 的子类。

Java 中的逆变:? super T

1
2
List<Animal> animals = new ArrayList<>();
List<? super Dog> list = animals; // ✅ 逆变赋值

List<Animal> 可以赋值给 List<? super Dog>,因为 AnimalDog 的父类,方向反转了。

逆变的限制:只能写,不能读(读出来全是 Object)

1
2
3
4
5
6
list.add(new Dog());      // ✅ 可以放入 Dog
list.add(new Poodle()); // ✅ 可以放入 Dog 的子类

list.add(new Animal()); // ❌ 编译错误!

Object obj = list.get(0); // ✅ 只能读成 Object

这里重点解释一下为什么 list.add(new Animal()) 会被禁止:

List<? super Dog> 的实际类型可能是 List<Dog>(注意,Dog 也是 Dog 的父类,因为自身是自身的父类)。如果实际类型是 List<Dog>,那么放入 Animal 就会破坏 List<Dog> 的类型约束。编译器为了绝对安全,只允许放入 Dog 及其子类,因为无论实际类型是 List<Dog>List<Animal> 还是 List<Object>Dog 总能安全地放进去。

而读取时,因为实际类型可能是 List<Object>,所以只能保证读出来的是 Object,类型信息被擦除了。

结论:逆变适合「消费者」(Consumer)—— 我往里放入数据。

四、Java 的 PECS 原则

Java 社区总结了一条经典口诀:

PECS:Producer Extends, Consumer Super

  • 如果你从容器中读取(生产数据),用 ? extends T(协变)
  • 如果你往容器中写入(消费数据),用 ? super T(逆变)

这条原则在 Java 集合框架中随处可见,比如 Collections.copy() 方法的签名:

1
public static <T> void copy(List<? super T> dest, List<? extends T> src)
  • src 是生产者,用 extends(从中读取)
  • dest 是消费者,用 super(向其中写入)

五、Swift 中的协变与逆变

Swift 的设计哲学和 Java 有很大不同。Swift 没有 Java 那样的通配符(? extends / ? super),泛型默认是不变的。但在三个特定场景下,变体规则依然存在。

5.1 数组(Array)—— 协变

Swift 的数组支持协变:

1
2
3
4
5
class Animal {}
class Dog: Animal {}

let dogs: [Dog] = [Dog()]
let animals: [Animal] = dogs // ✅ 数组协变

这其实是不安全的协变(和 Java 数组一样),但 Swift 通过值语义写时复制(COW)降低了风险。在实际使用中,由于数组是 struct(值类型),拷贝时会独立复制,不容易出现 Java 数组那样的 ArrayStoreException

5.2 闭包(Closure)—— 返回值协变 + 参数逆变

这是 Swift 中最符合类型理论的地方。函数类型的子类型规则是:

  • 返回值类型:支持协变(可以返回更具体的子类型)
  • 参数类型:支持逆变(可以接受更宽泛的父类型)
1
2
3
4
5
6
7
8
9
typealias Handler = (Dog) -> Animal

// 返回值协变:返回 Dog 可以替代返回 Animal
let covariant: (Dog) -> Dog = { _ in Dog() }
let handler1: Handler = covariant // ✅

// 参数逆变:参数用 Animal 可以替代参数用 Dog
let contravariant: (Animal) -> Animal = { _ in Animal() }
let handler2: Handler = contravariant // ✅

为什么参数要逆变?因为调用 Handler 时,外界会传入一个 Dog。如果内部函数接受的是 Animal,传入 Dog 完全合法(DogAnimal 的子类)。

5.3 泛型(Generic)—— 默认不变

Swift 的泛型类和结构体默认是不变的:

1
2
3
class Box<T> {}
let dogBox = Box<Dog>()
// let animalBox: Box<Animal> = dogBox // ❌ 编译错误!

Box<Dog>Box<Animal> 没有任何类型关系。如果需要变体行为,必须通过关联类型(Associated Type)配合 where 约束,或者使用类型擦除(如 AnyPublisher)。

六、Java vs Swift 全面对比

维度 Java Swift
泛型变体支持 通过 ? extends(协变)和 ? super(逆变)显式声明 默认不变,需通过协议/关联类型间接实现
数组 协变(不安全,运行时会抛出 ArrayStoreException 协变(通过值语义和写时复制降低风险)
函数/闭包 方法重写时返回值可协变,参数类型必须不变 返回值协变 + 参数逆变(完全遵循函数子类型理论)
设计目的 集合读写灵活性与安全性平衡(PECS) 类型安全优先,函数式编程友好
语法标识 extends(上界)和 super(下界)关键字 无专用关键字,靠类型推断和编译器检查
类型安全 编译时强检查(通配符边界) 编译时更强检查(泛型系统更严格)

七、两套设计背后的哲学

Java 的思路:「我声明时就要说清楚我是用来读还是用来写」

Java 的泛型通配符是在使用端(use-site)进行变体声明。开发者需要在声明变量时就明确告诉编译器:这个容器是只读的(extends)还是只写的(super)。这是一种显式的 API 设计约束

优点:灵活、精细控制。
缺点:语法复杂,学习曲线陡峭(PECS 口诀就是明证)。

Swift 的思路:「除了数组和闭包,泛型就是两个不同的类型」

Swift 的变体规则是声明端(declaration-site)定义的,语言内置了少数几个变体场景(数组、闭包),其余场景默认不变。这种设计更倾向于简单明了:大多数情况下,不同类型的泛型容器就是没有关系,不需要额外思考。

优点:代码更直观,不易出错。
缺点:某些场景需要手动类型擦除,增加样板代码。

八、总结

语言 核心理念 适合场景
Java 通过 PECS 原则在集合读写中灵活使用协变/逆变 大型企业应用、集合框架设计
Swift 默认不变,仅在数组和闭包中内置变体规则 iOS/macOS 开发、函数式编程

一句话概括:

Java 的协变/逆变是「显式的集合访问权限控制」,而 Swift 的协变/逆变是「隐式的语言底层类型法则」——前者服务于 API 设计的灵活性,后者服务于类型系统的纯净性。


深入理解协变与逆变:从 Java 到 Swift
https://wttch96.github.io/post/swift/深入理解协变与逆变.html
作者
Wttch
发布于
2026年7月22日
更新于
2026年7月24日
许可协议