Java 泛型中的通配符:一篇文章彻底讲清楚 <? extends T> 和 <? super T>

举报
汪子熙 发表于 2025/09/05 13:39:51 2025/09/05
【摘要】 Java 语言作为一种静态强类型语言,在设计过程中必须平衡类型安全、灵活性以及代码的可复用性。泛型机制便是 Java 在 1.5 版本中引入的强大工具,旨在实现类型安全的集合操作及代码的通用化。## 引入通配符的动机在讨论通配符的动机之前,需要了解 Java 泛型的基本设计目标,即增强类型检查和减少强制类型转换。这一设计极大地提升了代码的类型安全性,但也带来了一个重要的问题:泛型的类型参数化...

Java 语言作为一种静态强类型语言,在设计过程中必须平衡类型安全、灵活性以及代码的可复用性。

泛型机制便是 Java 在 1.5 版本中引入的强大工具,旨在实现类型安全的集合操作及代码的通用化。

## 引入通配符的动机

在讨论通配符的动机之前,需要了解 Java 泛型的基本设计目标,即增强类型检查和减少强制类型转换。这一设计极大地提升了代码的类型安全性,但也带来了一个重要的问题:泛型的类型参数化一旦定义,就变得非常严格,从而限制了一些灵活性。在实际项目中,往往需要解决更具通用性的问题,这时就需要 Java 泛型的通配符来帮助处理这些类型的不确定性。

`<? extends T>` 和 `<? super T>` 的主要动机是为了应对协变与逆变的问题。

在编程中,尤其是在集合框架中,我们需要实现更加通用和灵活的代码,以支持不同类型之间的继承关系,而不损害类型安全性。

### 泛型协变和逆变的需求

为了理解泛型通配符的必要性,需要从两个简单的问题入手:

1. 能否将 `List<Cat>` 作为 `List<Animal>` 使用?
2. 在方法中能否传递 `List<Animal>` 来接收 `List<Cat>`?

这两个问题的答案是“不能直接这样做”,因为 Java 中 `List<Cat>` 并不是 `List<Animal>` 的子类型,尽管 `Cat` 是 `Animal` 的子类型。Java 泛型在继承关系方面并不支持这种协变关系。

通配符 `<? extends T>` 和 `<? super T>` 就是为了解决这类问题而设计的。

#### 现实生活中的例子

设想有一个场景,假设你经营着一个动物收容所,你手头有不同种类的动物,比如猫(`Cat`)和狗(`Dog`),而这些动物都是动物类(`Animal`)的子类。在 Java 中,如果你有一个方法用于给收容所内的所有动物添加一些相同的食物,你希望这个方法不仅适用于 `List<Cat>`,也能用于 `List<Dog>`,甚至是 `List<Animal>`。然而,普通泛型定义限制了这种操作的灵活性,因为 `List<Cat>` 不能被看作是 `List<Animal>`。

在这里,`<? extends T>` 和 `<? super T>` 正是为了这样的需求而诞生的,以在不同类型的继承体系中实现安全且灵活的类型处理。

## `<? extends T>` 和协变

`<? extends T>` 通配符表示某个类是 `T` 或 `T` 的子类,称为协变。协变的目的是允许我们`读取`该集合内的对象,但`不能安全地往集合中添加新对象`。


考虑下面的例子:

```java
class Animal {
    void eat() {
        System.out.println("Animal is eating");
    }
}

class Cat extends Animal {
    void meow() {
        System.out.println("Cat is meowing");
    }
}

public class CovariantExample {
    public static void feedAnimals(List<? extends Animal> animals) {
        for (Animal animal : animals) {
            animal.eat();
        }
        // animals.add(new Cat()); // 编译错误
    }

    public static void main(String[] args) {
        List<Cat> cats = new ArrayList<>();
        cats.add(new Cat());
        feedAnimals(cats);  // List<Cat> 被接受了,因为 Cat extends Animal
    }
}
```
在 `feedAnimals()` 方法中,参数是 `List<? extends Animal>`,意味着该列表可以是 `Animal` 或者 `Animal` 的任意子类。这样定义允许在方法中传递 `List<Cat>` 或 `List<Dog>`。但在方法中,你无法向 `animals` 集合中添加新元素。原因是 JVM 在编译期间不能确定 `animals` 具体是 `List<Cat>` 还是 `List<Dog>`,为了类型安全,编译器禁止往集合中插入元素。

在字节码层面,这种约束的本质是为了避免类型混淆。例如,如果在 `feedAnimals()` 中尝试添加 `Dog` 对象到 `List<Cat>` 中,这显然是不安全的。因此,在协变场景中,你可以安全地读取集合内的对象(知道它们至少是 `Animal`),但是不能往其中插入任何对象。

### 协变的应用场景
`<? extends T>` 适合用于只需要读取对象的场景。回到前面的动物收容所例子,当你只需要遍历集合并让所有动物进食时,使用协变是非常合理的,因为你并不关心集合中具体是哪种动物,只要它们都是 `Animal` 即可。

## `<? super T>` 和逆变

`<? super T>` 通配符表示某个类是 `T` 或 `T` 的父类,称为逆变。逆变的目的是允许我们往集合中添加对象,但读取对象时只能读取为 `Object` 类型,因为集合中的类型上界无法确定。

### 示例代码与解读
我们来看一个简单的例子:

```java
public class ContravariantExample {
    public static void addCats(List<? super Cat> animals) {
        animals.add(new Cat());
        // Animal animal = animals.get(0); // 编译错误,只能返回 Object
    }

    public static void main(String[] args) {
        List<Animal> animals = new ArrayList<>();
        addCats(animals);  // List<Animal> 被接受了,因为 Animal 是 Cat 的父类
    }
}
```
在 `addCats()` 方法中,参数是 `List<? super Cat>`,意味着该列表可以是 `Cat` 或 `Cat` 的任何父类(例如 `Animal`)。这样定义允许你向集合中添加 `Cat` 对象,因为我们确信集合中至少能够容纳 `Cat`。然而,当你试图读取集合中的对象时,只能将它们读取为 `Object`,因为编译器无法确定集合中元素的具体类型,只能确定它们至少是 `Object`。

在字节码层面,这种设计确保了集合中元素的类型一致性,避免了向集合中插入不兼容类型的对象。例如,假设在 `addCats()` 中传入了 `List<Animal>`,如果允许读取为 `Cat` 类型,当集合中包含其他 `Animal` 的实例时,将导致类型转换异常。因此,编译器限制只能读取为 `Object`,以保证类型安全性。

### 逆变的应用场景
`<? super T>` 适合用于需要往集合中添加对象的场景。例如,回到动物收容所的例子,如果你有一个方法专门用于向收容所中添加猫,你可以使用 `List<? super Cat>` 作为参数,这样无论收容所收容的是动物(`List<Animal>`)还是猫(`List<Cat>`),都可以安全地将猫添加进去。

## 为什么 Java 泛型不能直接支持继承关系?
在理解了 `<? extends T>` 和 `<? super T>` 之后,一个自然的问题是:为什么 Java 不能直接让 `List<Cat>` 成为 `List<Animal>` 的子类?要回答这个问题,需要从类型安全和字节码设计的角度分析。

Java 中的泛型是通过类型擦除(Type Erasure)实现的。类型擦除意味着在编译期间,泛型信息会被移除,并在字节码中被替换为原始类型。例如,`List<Cat>` 和 `List<Animal>` 在编译后都会变成 `List`,泛型信息只在编译时起作用,而不会进入运行时。因此,如果 `List<Cat>` 和 `List<Animal>` 直接有继承关系,将会在类型擦除后产生歧义和类型安全问题。

假设 `List<Cat>` 可以作为 `List<Animal>` 使用,考虑如下代码:

```java
List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats; // 假设这合法
animals.add(new Dog());      // 这样将导致 cats 中出现 Dog
Cat cat = cats.get(0);       // 类型不匹配,出现运行时错误
```
如果允许这样的操作,在类型擦除后,集合 `cats` 中会包含 `Dog` 对象,而 `cats` 被定义为 `List<Cat>`,这将导致不可预测的行为。因此,为了保持类型安全,Java 不允许 `List<Cat>` 和 `List<Animal>` 之间存在直接的继承关系。

## JVM 和字节码层面的设计考量
从 JVM 和字节码的角度来看,Java 泛型的设计是基于保持向后兼容性的考量。在 Java 泛型引入之前,Java 集合可以存储任何对象,而类型安全完全依赖于强制类型转换。为了保持向后兼容性,Java 采用了类型擦除的方式来实现泛型,这使得在运行时,泛型集合与普通集合并无区别。

正是因为泛型信息在编译后被擦除,JVM 在运行时并不知道集合中元素的具体类型,因此也无法保证类型的正确性。为了在这种类型擦除的实现机制下仍然保持类型安全,Java 需要在编译期对泛型类型进行严格检查。这也就是为什么需要通过 `<? extends T>` 和 `<? super T>` 来在一定程度上解决灵活性与类型安全之间的矛盾。

## 泛型 PECS 原则:Producer Extends, Consumer Super
在实际应用中,有一个广泛使用的原则可以帮助决定何时使用 `<? extends T>` 和 `<? super T>`,即 PECS 原则:`Producer Extends, Consumer Super`。

- 如果需要从集合中读取类型 `T` 或其子类的对象,应该使用 `<? extends T>`。
- 如果需要将类型 `T` 或其子类的对象写入集合,应该使用 `<? super T>`。

这个原则很容易用一个现实的例子来理解。假设你有一个水果盘(`Fruit`),里面可以包含不同种类的水果(如苹果 `Apple` 或香蕉 `Banana`)。

- 如果你只是想吃水果,并不在意具体是哪种水果,使用 `<? extends Fruit>` 是合适的,因为你只需要从盘中取出水果,且不用担心具体是哪种水果。
- 如果你想向水果盘中添加水果,则应该使用 `<? super Fruit>`,这样可以保证你放入的对象至少是一个水果。

## 省流版
Java 引入 `<? extends T>` 和 `<? super T>` 的主要原因是为了在类型安全的前提下解决泛型的灵活性问题。通过这两种通配符,Java 可以更好地处理协变和逆变的问题,使得代码在处理集合类时既能保持灵活性,又不损害类型安全性。无论是在只读操作还是写入操作中,`<? extends T>` 和 `<? super T>` 都提供了一种安全的方式来处理类型的不确定性。

JVM 在字节码层面采用类型擦除的方式实现泛型,这也导致了泛型不能直接支持继承关系的设计限制。通过使用泛型通配符,Java 提供了一种在编译期保证类型安全而在运行时保持灵活性的机制,从而解决了泛型在复杂继承关系中的实际应用问题。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。