Java 泛型通配符

更新于 2025-12-28

Jakob Jenkov 2014-06-23

Java 泛型中的通配符(wildcards)机制,旨在使某个类(例如 A)的集合能够被转换为 A 的子类或父类的集合。本文将对此进行详细解释。

基本泛型集合赋值问题

假设有如下类继承结构:

public class A { }
public class B extends A { }
public class C extends A { }

BC 都继承自 A

再看下面两个 List 变量:

List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();

你能否让 listA 指向 listB?或者让 listB 指向 listA?换句话说,以下赋值是否合法?

listA = listB;
listB = listA;

答案是:两种情况都不合法。原因如下:

listA 中,你可以插入 A 类或其子类(如 BC)的实例。如果你可以执行以下代码:

List<B> listB = listA;

那么 listA 中可能包含非 B 类型的对象(比如 AC 实例)。当你通过 listB 获取元素时,就有可能拿到非 B 类型的对象,这违反了 listB 的类型声明契约。

同样地,将 listB 赋值给 listA 也会带来问题:

listA = listB;

如果允许这种赋值,你就可以通过 listA(声明为 List<A>)向原本是 List<B> 的集合中插入 AC 的实例,从而破坏了该列表只应包含 B 或其子类对象的约定。


何时需要此类赋值?

上述赋值需求通常出现在编写可复用的方法时,这些方法需要操作特定类型的集合。

例如,假设你有一个方法用于处理 List 中的元素,比如打印所有元素:

public void processElements(List<A> elements) {
    for (A o : elements) {
        System.out.println(o.getValue());
    }
}

该方法遍历一个 A 类型的列表,并调用每个元素的 getValue() 方法(假设 A 类有此方法)。

但正如前文所述,你不能List<B>List<C> 作为参数传给这个方法,因为 List<B> 并不是 List<A> 的子类型。


泛型通配符

泛型通配符正是为解决上述问题而设计的。它主要满足两类需求:

  1. 从泛型集合中读取数据
  2. 向泛型集合中插入数据

定义泛型集合变量时,有三种通配符写法:

List<?> listUnknown = new ArrayList<A>();
List<? extends A> listExtends = new ArrayList<A>();
List<? super A> listSuper = new ArrayList<A>();

下面分别解释它们的含义。


未知通配符(List<?>

List<?> 表示一个类型未知的列表。它可能是 List<A>List<B>List<String> 等任意类型。

由于你不知道列表的实际类型,因此只能从中读取元素,且读出的对象只能被视为 Object 类型。例如:

public void processElements(List<?> elements) {
    for (Object o : elements) {
        System.out.println(o);
    }
}

现在,processElements() 方法可以接受任何泛型 List 作为参数,例如:

List<A> listA = new ArrayList<A>();
processElements(listA);

List<String> listStr = new ArrayList<String>();
processElements(listStr);

但你不能向 List<?> 中添加任何元素(除了 null),因为你无法保证类型安全。


上界通配符(List<? extends A>

List<? extends A> 表示一个列表,其中的元素是 A 类或其子类(如 BC)的实例。

由于你知道所有元素都是 A 或其子类,因此可以安全地将它们当作 A 来读取:

public void processElements(List<? extends A> elements) {
    for (A a : elements) {
        System.out.println(a.getValue());
    }
}

现在,你可以传入 List<A>List<B>List<C>

List<A> listA = new ArrayList<A>();
processElements(listA);

List<B> listB = new ArrayList<B>();
processElements(listB);

List<C> listC = new ArrayList<C>();
processElements(listC);

但是,你不能向该列表中插入任何元素(包括 ABC),因为你不知道列表的确切类型——它可能是 List<B>,此时插入 C 就不合法。

适用于“读取”场景(Producer - 生产者)


下界通配符(List<? super A>

List<? super A> 表示一个列表,其类型是 A 或其父类(如 Object)。

既然列表能容纳 A 或其父类,那么插入 A 或其子类(如 BC)就是安全的:

public static void insertElements(List<? super A> list) {
    list.add(new A());
    list.add(new B());
    list.add(new C());
}

你可以这样调用:

List<A> listA = new ArrayList<A>();
insertElements(listA);

List<Object> listObject = new ArrayList<Object>();
insertElements(listObject);

不能安全地从中读取具体类型的元素。例如:

Object obj = list.get(0);   // ✅ 合法(所有对象都是 Object)
A a = list.get(0);          // ❌ 不合法!无法确定是不是 A

因为列表中可能存的是 Object,而不仅仅是 A

适用于“写入”场景(Consumer - 消费者)


总结记忆口诀

PECS 原则(Producer-Extends, Consumer-Super)

  • 如果你需要从列表中读取(作为生产者)→ 使用 ? extends T
  • 如果你需要向列表中写入(作为消费者)→ 使用 ? super T
  • 如果既要读又要写 → 不要使用通配符,直接用具体类型 List<T>

📌 提示:通配符让泛型在保持类型安全的同时,具备更强的灵活性,是 Java 泛型中非常重要的高级特性。