问题

泛型中上下界限定符extends和super有什么区别?

答案

核心概念

  • 上界通配符(Upper Bounded Wildcard)<? extends T> - 限定类型为T或T的子类
  • 下界通配符(Lower Bounded Wildcard)<? super T> - 限定类型为T或T的父类

语法与含义

1. 上界通配符 - extends

// 定义:接受Number及其子类
public void process(List<? extends Number> list) {
    // ...
}

// 调用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5);
List<Number> numbers = Arrays.asList(1, 2.5);

process(ints);    // ✅ Integer extends Number
process(doubles); // ✅ Double extends Number
process(numbers); // ✅ Number本身

类型层次

Number (上界)
  ├─ Integer
  ├─ Double
  ├─ Long
  └─ Float

2. 下界通配符 - super

// 定义:接受Integer及其父类
public void addNumbers(List<? super Integer> list) {
    // ...
}

// 调用
List<Integer> ints = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addNumbers(ints);    // ✅ Integer本身
addNumbers(numbers); // ✅ Number是Integer的父类
addNumbers(objects); // ✅ Object是Integer的父类

类型层次

Object
  └─ Number
       └─ Integer (下界)

读写权限差异

上界通配符(extends):只读

public void readList(List<? extends Number> list) {
    // ✅ 读取:可以读,返回值为Number
    Number num = list.get(0);
    double value = num.doubleValue();

    // ❌ 写入:不能写(编译错误)
    // list.add(new Integer(1));  // 编译错误
    // list.add(new Double(2.5)); // 编译错误
    // list.add(new Number());    // 编译错误

    // 唯一可以添加的是null
    list.add(null); // ✅
}

原因:编译器不知道list的确切类型

List<Integer> ints = new ArrayList<>();
List<? extends Number> list = ints;

// 如果允许添加
// list.add(new Double(2.5)); // ❌ Double不能放入List<Integer>

下界通配符(super):只写

public void writeList(List<? super Integer> list) {
    // ✅ 写入:可以写Integer及其子类
    list.add(new Integer(1));
    list.add(100); // 自动装箱为Integer

    // ❌ 读取:只能读出Object(编译器不知道确切类型)
    Object obj = list.get(0); // ✅ 只能当作Object

    // Integer num = list.get(0); // ❌ 编译错误
}

原因:编译器不知道list的确切类型

List<Number> numbers = new ArrayList<>();
List<? super Integer> list = numbers;

// 写入安全
list.add(100); // ✅ Integer可以存入List<Number>

// 读取不安全
// Integer num = list.get(0); // ❌ 实际可能是Number或Object

PECS原则

PECS:Producer Extends, Consumer Super

  • 生产者(Producer):如果只需要读取数据,使用 <? extends T>
  • 消费者(Consumer):如果只需要写入数据,使用 <? super T>

示例1:复制列表

// Collections.copy() 源码
public static <T> void copy(List<? super T> dest,
                             List<? extends T> src) {
    for (T item : src) {      // src是生产者,读取数据
        dest.add(item);       // dest是消费者,写入数据
    }
}

// 使用
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();

Collections.copy(numbers, ints);
// src: List<? extends T> → 读取Integer
// dest: List<? super T> → 写入Number(Integer的父类)

示例2:查找最大值

// 生产者:只需要读取元素进行比较
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    if (list.isEmpty()) {
        throw new IllegalArgumentException("Empty list");
    }

    T maxElement = list.get(0);
    for (T element : list) {  // 只读取,不写入
        if (element.compareTo(maxElement) > 0) {
            maxElement = element;
        }
    }
    return maxElement;
}

// 使用
List<Integer> ints = Arrays.asList(1, 5, 3);
Integer max = max(ints); // 5

示例3:添加所有元素

// 消费者:只需要写入元素
public static <T> void addAll(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);  // 只写入,不读取
    }
}

// 使用
List<Integer> src = Arrays.asList(1, 2, 3);
List<Number> dest = new ArrayList<>();

addAll(dest, src);
// dest: List<? super Integer> → 写入
// src: List<? extends Integer> → 读取

多层继承关系示例

// 类层次
class Animal { }
class Dog extends Animal { }
class Puppy extends Dog { }

// 上界通配符
public void processDogs(List<? extends Dog> dogs) {
    // 可以接受
    List<Dog> dogList = Arrays.asList(new Dog());
    List<Puppy> puppyList = Arrays.asList(new Puppy());

    processDogs(dogList);   // ✅
    processDogs(puppyList); // ✅

    // 读取
    Dog dog = dogs.get(0); // ✅ 至少是Dog

    // 写入
    // dogs.add(new Dog());   // ❌ 编译错误
    // dogs.add(new Puppy()); // ❌ 编译错误
}

// 下界通配符
public void addDogs(List<? super Dog> list) {
    // 可以接受
    List<Dog> dogList = new ArrayList<>();
    List<Animal> animalList = new ArrayList<>();
    List<Object> objectList = new ArrayList<>();

    addDogs(dogList);     // ✅
    addDogs(animalList);  // ✅
    addDogs(objectList);  // ✅

    // 写入
    list.add(new Dog());   // ✅
    list.add(new Puppy()); // ✅ Puppy是Dog的子类

    // 读取
    Object obj = list.get(0); // ✅ 只能当作Object
    // Dog dog = list.get(0);   // ❌ 编译错误
}

对比表格

特性 <? extends T> <? super T>
类型范围 T及其子类 T及其父类
读取操作 ✅ 可以读,返回T ⚠️ 只能读出Object
写入操作 ❌ 不能写(除null) ✅ 可以写T及其子类
典型用途 数据提供者(Producer) 数据消费者(Consumer)
代表方法 Collections.max() Collections.copy()

实战应用

1. Stream API

// Stream.max() 使用上界通配符
public interface Stream<T> {
    Optional<T> max(Comparator<? super T> comparator);
    //                        ↑ 下界通配符
}

// 使用
Stream<Integer> stream = Stream.of(1, 5, 3);
Optional<Integer> max = stream.max(Comparator.naturalOrder());

2. 集合工具类

public class CollectionUtils {
    // 查找:上界通配符(读取)
    public static <T extends Comparable<? super T>> T findMax(
            List<? extends T> list) {
        // 实现
        return null;
    }

    // 添加:下界通配符(写入)
    public static <T> void addAll(
            List<? super T> dest,
            List<? extends T> src) {
        dest.addAll(src);
    }

    // 过滤:上界通配符(读取)
    public static <T> List<T> filter(
            List<? extends T> list,
            Predicate<? super T> predicate) {
        // 实现
        return null;
    }
}

3. DAO层设计

public interface BaseDao<T> {
    // 查询:返回T的子类列表(上界)
    <S extends T> List<S> findAll(Class<S> subClass);

    // 保存:接受T及其子类(上界)
    <S extends T> void save(S entity);

    // 批量保存:接受T及其子类的列表
    <S extends T> void saveAll(List<? extends S> entities);
}

常见陷阱

陷阱1:混淆上下界

// ❌ 错误:想要写入,却用了extends
public void addItems(List<? extends Number> list) {
    // list.add(100); // 编译错误
}

// ✅ 正确:使用super
public void addItems(List<? super Integer> list) {
    list.add(100); // ✅
}

陷阱2:不必要的通配符

// ❌ 过度使用通配符
public <T> void process(List<? extends T> list) {
    // 如果只需要T,不需要通配符
}

// ✅ 简化
public <T> void process(List<T> list) {
    // 足够了
}

陷阱3:不能同时读写

// ❌ 既想读又想写
public void processNumbers(List<? extends Number> list) {
    Number num = list.get(0);  // ✅ 可以读
    // list.add(100);           // ❌ 不能写
}

// ✅ 如果需要读写,使用具体类型
public void processNumbers(List<Number> list) {
    Number num = list.get(0);  // ✅
    list.add(100);              // ✅
}

高级用法

递归类型限定

// Comparable接口使用递归限定
public interface Comparable<T> {
    int compareTo(T other);
}

// 确保元素可以与自己比较
public static <T extends Comparable<T>> T max(List<T> list) {
    // ...
}

// 更灵活的版本(允许父类实现Comparable)
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    // ...
}

// 示例:子类继承父类的Comparable实现
class Parent implements Comparable<Parent> {
    @Override
    public int compareTo(Parent other) { return 0; }
}

class Child extends Parent {
    // 继承了Comparable<Parent>,而不是Comparable<Child>
}

List<Child> children = Arrays.asList(new Child());
Child max = max(children); // 使用 <? super T> 才能编译通过

答题总结

上界通配符 <? extends T>

  • 类型范围:T及其子类
  • 操作特点:只读不写(生产者)
  • 使用场景:从集合中读取数据进行处理

下界通配符 <? super T>

  • 类型范围:T及其父类
  • 操作特点:只写不读(消费者)
  • 使用场景:向集合中写入数据

PECS原则

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

典型应用:Collections.copy(List<? super T> dest, List<? extends T> src) - 源列表是生产者(读取),目标列表是消费者(写入)。