List< ? extends T > 和 List < ? super T > 之间有什么区别 ?
参考回答**
在Java的泛型中,List<? extends T>
和List<? super T>
是两种使用通配符的方式,它们的含义和使用场景有所不同。
List<? extends T>
:表示这个列表可以持有类型为T
或T
的子类的对象,但只允许读取,不允许添加(除了null
)。List<? super T>
:表示这个列表可以持有类型为T
或T
的父类的对象,允许添加T
及其子类的对象,但读取时只能保证返回Object
类型。
简而言之:
? extends T
:用来获取(读取),你可以安全地读取类型为T
或其子类的数据。? super T
:用来存储(写入),你可以安全地写入类型为T
或其子类的数据。
详细讲解与拓展
1. List<? extends T>
? extends T
表示列表的元素是某种类型T
或T
的子类,但在编译时不能确定具体的子类,因此有以下特点:
- 可以读取元素,编译器会认为元素是
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
表示列表的元素是某种类型T
或T
的父类,但在编译时不能确定具体的父类,因此有以下特点:
- 可以写入
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 |
允许添加T 或T 的子类对象 |
读取元素 | 返回类型为T 或其子类(上限是T ) |
返回类型为Object (下限是T ) |
使用场景 | 主要用来获取数据(读取) | 主要用来存储数据(写入) |
4. 使用场景
? extends T
的使用场景
- 当我们关心从列表中读取数据时,例如遍历集合、处理集合元素等。
- 例子:复制集合数据。
public static void copy(List<? extends Animal> source, List<Animal> destination) { for (Animal animal : source) { destination.add(animal); // 可以安全读取并写入 } }
? 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
,表示类型必须是T
或T
的子类。 - 下界(Lower Bound):
? super T
,表示类型必须是T
或T
的父类。
通过上下边界,Java的泛型机制可以更好地控制类型安全,增强代码的灵活性。
总结
? extends T
:读取为主,限制写入。适合只读数据的场景。? super T
:写入为主,限制读取。适合向集合写入数据的场景。- 牢记PECS原则:生产者用
extends
,消费者用super
。