List< ? extends T > 和 List < ? super T > 之间有什么区别 ?

参考回答**

在Java的泛型中,List<? extends T>List<? super T>是两种使用通配符的方式,它们的含义和使用场景有所不同。

  1. List<? extends T>:表示这个列表可以持有类型为TT的子类的对象,但只允许读取,不允许添加(除了null)。
  2. List<? super T>:表示这个列表可以持有类型为TT的父类的对象,允许添加T及其子类的对象,但读取时只能保证返回Object类型。

简而言之:

  • ? extends T:用来获取(读取),你可以安全地读取类型为T或其子类的数据。
  • ? super T:用来存储(写入),你可以安全地写入类型为T或其子类的数据。

详细讲解与拓展

1. List<? extends T>

? extends T表示列表的元素是某种类型TT的子类,但在编译时不能确定具体的子类,因此有以下特点:

  • 可以读取元素,编译器会认为元素是T类型(或其子类)。
  • 不能安全地写入元素,除了写入null外。

例子:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

public class Main {
    public static void main(String[] args) {
        List<? extends Animal> animals = new ArrayList<Dog>();
        // 读取
        Animal animal = animals.get(0); // 可以安全读取为Animal类型
        // 写入
        animals.add(new Dog()); // 编译错误,因为无法确定列表具体持有的类型
        animals.add(new Animal()); // 编译错误
        animals.add(null); // 可以,因为null是所有类型的默认值
    }
}

为什么不允许添加元素?因为编译器无法确定通配符的具体类型,比如:

  • 如果animals实际是List<Dog>,添加Cat会不安全。
  • 如果animals实际是List<Cat>,添加Dog也会不安全。

因此,为了保证类型安全,Java禁止向? extends T的列表中添加非null值。


2. List<? super T>

? super T表示列表的元素是某种类型TT的父类,但在编译时不能确定具体的父类,因此有以下特点:

  • 可以写入T或其子类的对象
  • 读取时只能保证返回Object类型

例子:

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

public class Main {
    public static void main(String[] args) {
        List<? super Dog> animals = new ArrayList<Animal>();
        // 写入
        animals.add(new Dog()); // 可以,因为Dog是T本身
        animals.add(new Puppy()); // 可以,Puppy是Dog的子类
        animals.add(new Animal()); // 编译错误,因为Animal不是Dog及其子类
        // 读取
        Object obj = animals.get(0); // 可以读取,但只能作为Object类型处理
        Dog dog = animals.get(0);    // 编译错误,因为无法确定类型为Dog
    }
}

为什么读取时只能返回Object?因为列表可能持有Dog的父类对象,比如Animal,返回类型无法保证具体是Dog


3. 总结两者的核心区别

特性 List<? extends T> List<? super T>
添加元素 只能添加null 允许添加TT的子类对象
读取元素 返回类型为T或其子类(上限是T 返回类型为Object(下限是T
使用场景 主要用来获取数据(读取) 主要用来存储数据(写入)

4. 使用场景

  1. ? extends T的使用场景
  • 当我们关心从列表中读取数据时,例如遍历集合、处理集合元素等。
  • 例子:复制集合数据。
    public static void copy(List<? extends Animal> source, List<Animal> destination) {
       for (Animal animal : source) {
           destination.add(animal); // 可以安全读取并写入
       }
    }
    
  1. ? super T的使用场景
  • 当我们关心向列表中写入数据时,例如添加元素到集合。
  • 例子:将数据写入列表。
    public static void addAnimals(List<? super Dog> list) {
       list.add(new Dog()); // 可以添加Dog
       list.add(new Puppy()); // 可以添加Dog的子类
    }
    

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

一个常见的记忆规则是:

  • ? extends用于生产者(Producer):如果你只想从集合中读取数据(生产数据),使用? extends T
  • ? super用于消费者(Consumer):如果你只想向集合中写入数据(消费数据),使用? super T

例子:

  • 生产者(Producer):我们从List<? extends T>中获取数据,用于其他地方。
    public static void printAnimals(List<? extends Animal> list) {
      for (Animal animal : list) {
          System.out.println(animal);
      }
    }
    
  • 消费者(Consumer):我们向List<? super T>中添加数据。
    public static void fillDogs(List<? super Dog> list) {
      list.add(new Dog());
      list.add(new Puppy());
    }
    

6. 延伸:泛型的上下边界

? extends T? super T其实是Java泛型中上下边界(Upper Bound and Lower Bound)的具体应用:

  • 上界(Upper Bound)? extends T,表示类型必须是TT的子类。
  • 下界(Lower Bound)? super T,表示类型必须是TT的父类。

通过上下边界,Java的泛型机制可以更好地控制类型安全,增强代码的灵活性。


总结

  • ? extends T:读取为主,限制写入。适合只读数据的场景。
  • ? super T:写入为主,限制读取。适合向集合写入数据的场景。
  • 牢记PECS原则:生产者用extends,消费者用super

发表回复

后才能评论