# Java 泛型中的协变与设计原则冲突

**协变逆变**（[Covariance and Contravariance: Conflict without a Cause](https://www.di.ens.fr/reports/1996/liens-94-18.A4.pdf)）

设：

* `A` 和 `B` 是类型（types）；
    
* `f` 是一个类型变换器（type constructor / type transformation），例如 `f(T) = List<T>`；
    
* `≤` 表示子类型关系（subtyping relation），即 `A ≤ B` 表示 `A` 是 `B` 的子类型（即 `A <: B`）。
    

那么：

* **协变（Covariant）**：如果 `A ≤ B`，则 `f(A) ≤ f(B)` 成立；
    
* **逆变（Contravariant）**：如果 `A ≤ B`，则 `f(B) ≤ f(A)` 成立；
    
* **不变（Invariant）**：如果以上两者都不成立。
    

在 [Java 泛型系统](https://en.wikipedia.org/wiki/Generics_in_Java)中，协变是一种允许子类型泛型在父类型泛型中使用的方式，典型形式是通过 `? extends T` 实现。例如：

```java
List<? extends Number> numbers = new ArrayList<Integer>();
```

协变使得泛型使用更为灵活，但它也带来了不少设计上的挑战，甚至违反了一些经典的面向对象设计原则。本文将分析协变如何与设计原则发生冲突，并解释背后的原因。

### **一、违反里氏替换原则（**[**Liskov Substitution Principle, LSP**](https://en.wikipedia.org/wiki/Liskov_substitution_principle)**）**

**LSP 定义**：

如果那么 `S` 是 `T` 的子类型，那么对每一个类型为 `T` 的对象 `o1`，都有类型为 `S` 的对象 `o2` 代替它，且程序的行为不变 。

形式化定义如下：

> 如果那么 `S` 是 `T` 的子类型，那么对每一个类型为 `T` 的对象 `o1`，都有类型为 `S` 的对象 `o2` 代替它，且程序的行为不变 。
> 
> 形式化定义如下：
> 
> ![https://wikimedia.org/api/rest_v1/media/math/render/svg/1dea439a2b789de7fee9d97867f57872eecaf759](https://wikimedia.org/api/rest_v1/media/math/render/svg/1dea439a2b789de7fee9d97867f57872eecaf759 align="left")

**问题**：

```java
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings;
objects.add(new Object()); // 编译错误！
```

虽然 `List<String>` 被赋值给 `List<? extends Object>`，看似满足协变，但由于 Java 泛型类型擦除的限制，我们无法保证添加的元素是合法的，所以编译器禁止添加操作。

**结论**：协变使得我们可以“看起来”用子类型泛型替代父类型泛型，但行为上的不一致（尤其写操作）导致**替代不彻底**，弱化了 LSP。

### **二、突破泛型不变性（Invariance）**

Java 泛型默认是不变的：

```java
List<Object> list = new ArrayList<String>(); // 编译错误！
 
协变（? extends T）打破了这种不变性：
 
List<? extends Number> numbers = new ArrayList<Integer>();
Number num = numbers.get(0); // OK
numbers.add(3.14); // 编译错误
```

这种设计虽然确保了类型安全，但需要开发者理解泛型 PECS（Producer Extends, Consumer Super）原理，否则很容易误用。

### **三、违反最小惊讶原则（**[**Principle of Least Astonishment**](https://en.wikipedia.org/wiki/Principle_of_least_astonishment)**）**

开发者很容易以为下面的代码是合法的：

```java
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings;
objects.add("abc"); // 不能添加！
```

这与我们对“String 是 Object 子类”的直觉相悖。

**结论**：虽然语法合法，但协变限制了写操作，这可能违反了最小惊讶原则，尤其对初学者而言更加困惑。（注意 ，这里是java泛型设计违反了**POLA**）

### **四、PECS 原则与协变**

**PECS 原则**：[Producer Extends, Consumer Super](https://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html)。

* `? extends T`：表示**生产者**，可以从中读取 T（或其子类型），但不能写入。
    
* `? super T`：表示**消费者**，可以写入 T（或其子类型），但读取只能是 Object。
    

举例：

```java
public void printAll(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
}
 
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}
```

PECS 是泛型中非常重要的经验法则，它揭示了协变与逆变的使用边界。

### **五、为什么 Java 仍引入协变？**

* **实用性**：多数情况下我们是读取数据而非写入，协变对读操作非常友好。
    
* **安全性**：只读语义让类型系统在保持安全性的同时提高了灵活性。
    
* **灵活性**：配合 PECS 原则，可以构建非常泛用的 API 接口。
    

### **总结**

| **设计原则** | **协变带来的问题** |
| --- | --- |
| **设计原则** | **协变带来的问题** |
| 里氏替换原则（LSP） | 替代不彻底，写操作不可行 |
| 泛型不变性 | 协变突破了不变性，但引入了额外复杂性 |
| 最小惊讶原则 | 使用体验不符合直觉，容易导致误用 |
| PECS 原则 | 协变用于 Producer，有效提升了类型灵活性 |

Java 泛型协变机制是一种**权衡设计**，在类型安全、灵活性与复杂度之间取得了平衡

### **六、相关参考链接（Java 官方规范）**

* [Java Language Specification - Generics](https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.5)
    
* [JLS §4.10 - Subtyping](https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.10)
    
* [JLS §4.5.1 - Type Arguments](https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.5.1)
    
* [JLS §4.6 - Type Erasure](https://docs.oracle.com/javase/specs/jls/se17/html/jls-4.html#jls-4.6)
