泛型是 Java 中强大的特性之一,它提供了类型安全的集合操作。然而,泛型的类型关系(如逆变与协变)常常让人感到困惑。
本文将深入探讨 Java 泛型中的逆变与协变,帮助你更好地理解其原理和应用场景。
一、什么是协变与逆变?
1. 协变(Covariance)
协变
是指子类型关系在泛型中得以保留。
- 例如,如果 Cat 是 Animal 的子类,那么 List 可以被视为 List 的子类型。
2. 逆变(Contravariance)
逆变
是指子类型关系在泛型中反转。
- 例如,如果 Cat 是 Animal 的子类,那么 List 可以被视为 List 的父类型。
二、Java 中的协变与逆变
1. 数组的协变
Java 中的数组是协变的。例如:
Animal[] animals = new Cat[10]; // 合法
然而,这种协变可能会导致运行时异常:
animals[0] = new Dog(); // 编译通过,但运行时抛出 ArrayStoreException
2. 泛型的不变性
Java 的泛型是不变的。例如:
List<Animal> animals = new ArrayList<Cat>(); // 编译错误
这种设计是为了保证类型安全。
三、使用通配符实现协变与逆变
1. 协变通配符(<? extends T>)
协变通配符允许泛型类型接受 T 或其子类型。
示例:
List<? extends Animal> animals = new ArrayList<Cat>(); // 合法
限制:
- 只能从集合中读取数据,不能写入数据:
Animal animal = animals.get(0); // 合法
animals.add(new Cat()); // 编译错误
2. 逆变通配符(<? super T>)
逆变通配符允许泛型类型接受 T 或其父类型。
示例:
List<? super Cat> cats = new ArrayList<Animal>(); // 合法
限制:
- 只能向集合中写入数据,读取的数据类型不确定:
cats.add(new Cat()); // 合法
Object obj = cats.get(0); // 合法,但类型为 Object
四、协变与逆变的应用场景
1. 协变的应用
协变通配符常用于只读操作,
例如遍历集合:
public void printAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
System.out.println(animal);
}
}
2. 逆变的应用
逆变通配符常用于写入操作,
例如向集合中添加元素:
public void addCat(List<? super Cat> cats) {
cats.add(new Cat());
}
五、PECS
原则
PECS(Producer Extends, Consumer Super)
原则是使用协变与逆变的重要指导:
- Producer Extends:如果泛型类型是生产者(提供数据),使用 <? extends T>。
- Consumer Super:如果泛型类型是消费者(接收数据),使用 <? super T>。
示例:
public void copy(List<? extends Animal> src, List<? super Animal> dest) {
for (Animal animal : src) {
dest.add(animal);
}
}
六、总结
协变
:<? extends T>,用于只读操作,保证类型安全。逆变
:<? super T>,用于写入操作,提供灵活性。PECS
原则:生产者使用 extends,消费者使用 super。
通过理解协变与逆变,你可以更好地设计泛型方法,提升代码的灵活性和安全性。希望本文能帮助你掌握 Java 泛型中的这一重要概念!