Super通配符
约 1660 字大约 6 分钟
2025-07-17
6.1 super 通配符的使用
前面我们已经了解到,泛型类型之间不存在继承关系,例如 Pair<Integer> 不是 Pair<Number> 的子类。
现在考虑一个 set 方法,其作用是设置 Pair 对象的 first 和 last 属性:
void set(Pair<Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}上述方法只能接受 Pair<Integer> 类型的参数。如果我们想让该方法接受 Pair<Number>、Pair<Object> 等类型,因为 Number 和 Object 是 Integer 的父类,并且 setFirst(Number) 和 setFirst(Object) 实际上可以接受 Integer 类型,这时就需要使用 super 通配符。
使用 super 通配符改写后的方法如下:
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}Pair<? super Integer> 表示该方法接受所有泛型类型为 Integer 或 Integer 父类的 Pair 类型。例如,Pair<Integer>、Pair<Number> 和 Pair<Object> 都可以作为参数传递给这个 set 方法。
以下代码展示了 super 通配符的用法:
public class Main {
public static void main(String[] args) {
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println(p1.getFirst() + ", " + p1.getLast()); // 100, 100
System.out.println(p2.getFirst() + ", " + p2.getLast()); //200, 200
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}在这个例子中,setSame 方法接受 Pair<? super Integer> 类型的参数,这意味着可以传入 Pair<Integer>、Pair<Number> 或 Pair<Object> 类型的实例。
6.2 方法签名与返回值
对于 Pair<? super Integer> 的 setFirst() 方法,其方法签名实际上是:
void setFirst(? super Integer);因此,可以安全地传入 Integer 类型。
而对于 Pair<? super Integer> 的 getFirst() 方法,其方法签名实际上是:
? super Integer getFirst();? super Integer 表示 Integer 类型或 Integer 的任何父类。这意味着我们无法使用 Integer 类型来接收 getFirst() 的返回值,下面的语句将无法通过编译:
Integer x = p.getFirst(); // 编译错误因为如果传入的实际类型是 Pair<Number>,编译器无法将 Number 类型转型为 Integer。
唯一可以接收 getFirst() 方法返回值的是 Object 类型:
Object obj = p.getFirst();因此,使用 <? super Integer> 通配符表示:
- 允许调用
set(? super Integer)方法传入Integer的引用。 - 不允许调用
get()方法获得Integer的引用。
唯一例外是可以获取 Object 的引用:Object o = p.getFirst()。
换句话说,使用 <? super Integer> 通配符作为方法参数,表示方法内部代码对于参数只能写,不能读。
6.3 Extends 和 Super 通配符的对比
作为方法参数,<? extends T> 类型和 <? super T> 类型的区别在于:
| 类型参数 | 合法 | 非法 | 例外情况 |
|---|---|---|---|
<? extends T> | 调用读方法 T get() 获取 T 的引用 | 调用写方法 set(T) 传入 T 的引用 | 允许传入 null |
<? super T> | 调用写方法 set(T) 传入 T 的引用 | 调用读方法 T get() 获取 T 的引用 | 允许获取 Object 对象 |
一个是允许读不允许写,另一个是允许写不允许读。
6.4 Collections.copy() 方法示例
Java 标准库的 Collections 类定义的 copy() 方法很好地展示了 extends 和 super 的意图:
public class Collections {
// 把src的每个元素复制到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}它的作用是把一个 List 的每个元素依次添加到另一个 List 中。它的第一个参数是 List<? super T>,表示目标 List,第二个参数 List<? extends T>,表示要复制的 List。
在这个 copy() 方法中:
- 对于类型
<? extends T>的变量src,可以安全地获取类型T的引用。 - 对于类型
<? super T>的变量dest,可以安全地传入T的引用。
copy() 方法内部不会读取 dest,因为不能调用 dest.get() 来获取 T 的引用。copy() 方法内部也不会修改 src,因为不能调用 src.add(T)。这是由编译器检查来实现的。如果在方法代码中意外修改了 src,或者意外读取了 dest,就会导致一个编译错误。
这个 copy() 方法的另一个好处是可以安全地把一个 List<Integer> 添加到 List<Number>,但是无法反过来添加:
// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);而这些都是通过 super 和 extends 通配符,并由编译器强制检查来实现的。
6.5 PECS 原则
何时使用 extends,何时使用 super? 为了便于记忆,可以使用 PECS 原则:Producer Extends Consumer Super。
- 生产者 (Producer): 提供数据的一方。
- 消费者 (Consumer): 接收数据的一方。
即:如果需要返回 T,它是生产者(Producer),要使用 extends 通配符;如果需要写入 T,它是消费者(Consumer),要使用 super 通配符。
还是以 Collections 的 copy() 方法为例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}需要返回 T 的 src 是生产者,因此声明为 List<? extends T>,需要写入 T 的 dest 是消费者,因此声明为 List<? super T>。
6.6 无限定通配符
Java 的泛型还允许使用无限定通配符(Unbounded Wildcard Type),即只定义一个 ?:
void sample(Pair<?> p) {
}因为 <?> 通配符既没有 extends,也没有 super,因此:
- 不允许调用
set(T)方法并传入引用(null除外)。 - 不允许调用
T get()方法并获取T引用(只能获取Object引用)。
换句话说,既不能读,也不能写,那只能做一些 null 判断:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}虽然 <?> 允许传入任何类型的 Pair 作为参数,提供了一定的灵活性,但它牺牲了类型安全和可操作性。大多数情况下,可以引入泛型参数 <T> 消除 <?> 通配符,从而提供更强的能力和灵活性:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}<?> 通配符有一个独特的特点,就是:Pair<?> 是所有 Pair<T> 的超类:
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
Pair<?> p2 = p; // 安全地向上转型
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}上述代码是可以正常编译运行的,因为 Pair<Integer> 是 Pair<?> 的子类,可以安全地向上转型。