Jakob Jenkov 2014-06-23
Java 泛型中的通配符(wildcards)机制,旨在使某个类(例如 A)的集合能够被转换为 A 的子类或父类的集合。本文将对此进行详细解释。
基本泛型集合赋值问题
假设有如下类继承结构:
public class A { }
public class B extends A { }
public class C extends A { }
类 B 和 C 都继承自 A。
再看下面两个 List 变量:
List<A> listA = new ArrayList<A>();
List<B> listB = new ArrayList<B>();
你能否让 listA 指向 listB?或者让 listB 指向 listA?换句话说,以下赋值是否合法?
listA = listB;
listB = listA;
答案是:两种情况都不合法。原因如下:
在 listA 中,你可以插入 A 类或其子类(如 B、C)的实例。如果你可以执行以下代码:
List<B> listB = listA;
那么 listA 中可能包含非 B 类型的对象(比如 A 或 C 实例)。当你通过 listB 获取元素时,就有可能拿到非 B 类型的对象,这违反了 listB 的类型声明契约。
同样地,将 listB 赋值给 listA 也会带来问题:
listA = listB;
如果允许这种赋值,你就可以通过 listA(声明为 List<A>)向原本是 List<B> 的集合中插入 A 或 C 的实例,从而破坏了该列表只应包含 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> 的子类型。
泛型通配符
泛型通配符正是为解决上述问题而设计的。它主要满足两类需求:
- 从泛型集合中读取数据
- 向泛型集合中插入数据
定义泛型集合变量时,有三种通配符写法:
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 类或其子类(如 B、C)的实例。
由于你知道所有元素都是 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);
但是,你不能向该列表中插入任何元素(包括 A、B、C),因为你不知道列表的确切类型——它可能是 List<B>,此时插入 C 就不合法。
✅ 适用于“读取”场景(Producer - 生产者)
下界通配符(List<? super A>)
List<? super A> 表示一个列表,其类型是 A 或其父类(如 Object)。
既然列表能容纳 A 或其父类,那么插入 A 或其子类(如 B、C)就是安全的:
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 泛型中非常重要的高级特性。