泛型通配符与上下界

面试官问:"List<? extends Number>List<? super Number> 有什么区别?"

候选人小韩答:"extends 是上限,super 是下限。"

面试官追问:"为什么读取元素时用 extends,写入元素时用 super?"

小韩说不上来。

面试官又写了一段代码:

List<Number> numbers = new ArrayList<>();
List<? extends Number> list = new ArrayList<Integer>();
list.add(3.14); // ❌ 为什么编译错误?

小韩彻底卡住了。

【面试官心理】 这道题是 Java 泛型中最容易混淆的知识点。能说出"PECS 法则"和"获取-写入原理"的候选人,说明真正理解了泛型通配符的设计意图。

一、两种通配符对比 🔴

通配符名称用途读取写入
? extends T上界通配符限制为 T 或 T 的子类只能读取 T 类型(生产者)不能写入
? super T下界通配符限制为 T 或 T 的父类只能读取 Object 类型(消费者)可以写入 T 类型
// extends:生产者 - 只能读
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Double>();

Number n = list1.get(0); // ✅ 能读取,类型是 Number
// list1.add(3.14); ❌ 编译错误,不能写入

// super:消费者 - 可以写
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();

list3.add(3.14); // ✅ 能写入 Integer(是 Number 的子类)
Object o = list3.get(0); // 能读取,但类型是 Object(最宽泛)

二、为什么 extends 不能写入?🔴

2.1 反证法理解

// 如果允许向 extends 集合写入:
List<Number> numbers = new ArrayList<>();
List<? extends Number> extendsList = new ArrayList<Double>(); // Double extends Number

extendsList.add(new Integer(42)); // 如果允许写入...
numbers = extendsList; // numbers 现在指向 Double 的 List
numbers.get(0); // 返回 Double
Number n = numbers.get(0); // Double 不是 Integer!类型安全问题!

// 因此:extends 集合禁止写入,只能读取

2.2 为什么 extends 能读取?

List<? extends Number> list = new ArrayList<Double>();
// list 可能指向 List<Double>, List<Integer>, List<Number>
// 无论哪种,里面的元素一定是 Number 的子类(可能是 Double, Integer, Number)
// 所以读取出来的类型安全,一定能当成 Number 处理
Number n = list.get(0); // ✅ 安全

三、为什么 super 能写入?🔴

3.1 写入原理

List<? super Number> list = new ArrayList<Object>();
// list 可能指向 List<Object>, List<Number>
// 无论是哪种,添加 Number 或其子类(Integer, Double)都是安全的
// 因为 Number 是两者都能接收的类型

list.add(new Integer(3));  // ✅ 安全:Integer 是 Number 的子类
list.add(new Double(3.14)); // ✅ 安全:Double 是 Number 的子类
list.add(new Object());     // ❌ 危险:Object 不一定是 Number

3.2 为什么 super 读取不安全?

List<? super Number> list = new ArrayList<Object>();
// 读取时只能确定是 Object 类型(最宽泛)
Object o = list.get(0); // ✅ 能读取,但类型是 Object
// list.get(0) 返回的不一定是 Number,可能是 Integer,也可能是 String!

四、PECS 法则 🔴

Producer Extends, Consumer Super(生产者用 extends,消费者用 super)

// 生产者(提供数据):用 extends
public double sum(List<? extends Number> numbers) {
    double sum = 0;
    for (Number n : numbers) { // 只能读取 Number
        sum += n.doubleValue();
    }
    return sum;
}

// 消费者(消费数据):用 super
public void addNumbers(List<? super Integer> list) {
    list.add(1);    // 可以写入 Integer
    list.add(2);
    // 读取只能当 Object,不关心
}

// 如果既要读又要写:用没有通配符的原生类型
public void copy(List<Object> src, List<Object> dest) {
    for (Object item : src) { // 读
        dest.add(item);       // 写
    }
}
💡

能说出 PECS 法则并用具体代码演示的候选人,面试官会认为有丰富的泛型使用经验。这是 P6 的标准要求。

五、无界通配符 ? 🔴

// ? 等同于 ? extends Object
List<?> list = new ArrayList<String>();
Object o = list.get(0); // 能读取,但类型是 Object

// 主要用途:处理类型未知或不重要的情况
public boolean isEmpty(List<?> list) {
    return list.size() == 0;
}

六、通配符与类型参数的区别 🔴

// T 是类型参数,? 是通配符
// T 在编译时是确定的
// ? 在编译时是"不确定的某个类型"

class Container<T> {
    void add(T item) { } // T 是确定的类型
    T get() { return null; }
}

class Utils {
    void process(List<?> list) { } // ? 表示某个未知类型
}
场景用类型参数 T用通配符 ?
类/方法有多个相关类型class Map<K, V>
单一泛型集合,需要读写void copy(List<T> src, List<T> dest)
只读(生产者)void read(List<? extends T> list)
只写(消费者)void write(List<? super T> list)
类型无关的操作boolean isEmpty(List<?> list)

七、追问升级

面试官:"List<?>List<Object> 有什么区别?"

List<Object> list1 = new ArrayList<String>(); // ❌ 编译错误
List<?> list2 = new ArrayList<String>(); // ✅ 可以

list1.add(123); // ✅ 可以
list2.add(123); // ❌ 不可以(? 是未知类型,不知道能否写入)

// List<?> 可以持有 List<String>,List<Integer> 等任何类型
// 但写入受限制(因为不知道具体是什么类型)
// List<Object> 可以持有任何 List
// 但读取时需要强制转换(不推荐)

【面试官心理】 能说出 List<Object>List<?> 差别的候选人,说明理解了通配符的"类型安全性"设计意图。这是泛型进阶的标志。

面试官:"Collections.copy 的签名是什么?为什么这样设计?"

// JDK 源码
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {      // src 是生产者,用 extends
        dest.add(item);       // dest 是消费者,用 super
    }
}

// 这正是 PECS 法则的完美应用:
// src 提供数据 → extends
// dest 消费数据 → super