Java泛型:通配符上下界与PECS原则的深度应用
大家好,今天我们来深入探讨Java泛型中的一个重要且稍微复杂的部分:通配符的上下界以及与之密切相关的PECS原则。理解这些概念对于编写健壮、灵活且类型安全的代码至关重要。
1. 泛型基础回顾
在深入通配符之前,我们先简单回顾一下泛型的基本概念。泛型允许我们在定义类、接口和方法时使用类型参数,从而实现代码的重用,并在编译时提供类型检查。
例如,一个简单的泛型类 Box<T>:
class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
public class GenericExample {
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(10);
Integer value = integerBox.get(); // No cast needed!
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String text = stringBox.get();
}
}
在上面的例子中,T 是类型参数。我们可以用具体的类型(如 Integer 或 String)替换它,从而创建不同类型的 Box 对象。
2. 为什么需要通配符?
虽然泛型提供了类型安全和代码重用,但在某些情况下,直接使用类型参数会带来一些限制。考虑以下场景:
public class GenericProblem {
public static void printList(List<Object> list) {
for (Object element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
// printList(stringList); // Compilation error!
}
}
这段代码尝试将 List<String> 传递给接受 List<Object> 的 printList 方法。尽管 String 是 Object 的子类,但 List<String> 并不是 List<Object> 的子类型。这是因为泛型类型是不变的(invariant)。
不变性意味着,即使 A 是 B 的子类, GenericType<A> 也不是 GenericType<B> 的子类。 为了解决这个问题,我们需要通配符。
3. 通配符: ?
通配符 ? 表示未知类型。它允许我们编写可以处理多种类型参数的泛型方法。
public class WildcardExample {
public static void printList(List<?> list) {
for (Object element : list) {
System.out.println(element);
}
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
stringList.add("World");
printList(stringList); // Works now!
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
printList(integerList); // Also works!
}
}
现在 printList 方法可以接受任何类型的 List,因为 List<?> 是 List<String> 和 List<Integer> 的父类型。 但是,使用 ? 也有一个限制:你不能向使用 ? 的集合中添加元素,因为编译器无法确定集合中元素的具体类型。
public class WildcardProblem {
public static void addToList(List<?> list, Object element) {
// list.add(element); // Compilation error!
}
}
编译器会阻止向 List<?> 添加任何元素,除了 null。 这是因为编译器无法确定 list 中元素的具体类型,因此无法保证类型安全。 为了解决这个问题,我们需要通配符的上下界。
4. 上界通配符: ? extends T
上界通配符 ? extends T 表示未知类型,但它必须是 T 或 T 的子类。 这允许我们读取集合中的元素,并将其视为 T 类型。
public class UpperBoundWildcard {
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
System.out.println("Sum of integers: " + sumOfList(integerList));
List<Double> doubleList = new ArrayList<>();
doubleList.add(1.5);
doubleList.add(2.5);
System.out.println("Sum of doubles: " + sumOfList(doubleList));
}
}
在上面的例子中,sumOfList 方法接受一个 List,其元素类型是 Number 或 Number 的子类。 这意味着我们可以将 List<Integer>、List<Double> 等传递给该方法。
与 ? 类似,你通常不能向使用 ? extends T 的集合中添加元素,因为编译器无法确定集合中元素的具体类型。 例如,如果你有一个 List<? extends Number>,编译器无法确定它是 List<Integer> 还是 List<Double>。 如果你尝试添加一个 Integer,但集合实际上是 List<Double>,则会导致类型错误。 例外情况是添加 null, 因为 null 可以赋值给任何引用类型。
5. 下界通配符: ? super T
下界通配符 ? super T 表示未知类型,但它必须是 T 或 T 的父类。 这允许我们向集合中添加 T 或 T 的子类型的元素。
public class LowerBoundWildcard {
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
addIntegers(integerList);
System.out.println("Integer list: " + integerList);
List<Number> numberList = new ArrayList<>();
addIntegers(numberList);
System.out.println("Number list: " + numberList);
List<Object> objectList = new ArrayList<>();
addIntegers(objectList);
System.out.println("Object list: " + objectList);
}
}
在上面的例子中,addIntegers 方法接受一个 List,其元素类型是 Integer 或 Integer 的父类。 这意味着我们可以将 List<Integer>、List<Number>、List<Object> 等传递给该方法。
但是,你不能保证从使用 ? super T 的集合中读取的元素的类型。 你只能保证读取的元素是 Object 类型。 这是因为集合可能包含 T 的任何父类的实例。 例如,如果你有一个 List<? super Integer>,它可能是 List<Number> 或 List<Object>。 如果它是 List<Number>,则它可以包含 Double 类型的元素。
6. PECS原则:Producer Extends, Consumer Super
PECS原则是使用通配符上下界的一个重要指导原则。 PECS是 "Producer Extends, Consumer Super" 的缩写,它告诉我们什么时候使用 extends (上界通配符) 和什么时候使用 super (下界通配符)。
-
Producer Extends (生产者使用 extends): 如果你需要从集合中读取数据(即,集合是生产者),则使用
? extends T。 例如,List<? extends Number>允许你从列表中读取Number类型的元素。 -
Consumer Super (消费者使用 super): 如果你需要向集合中写入数据(即,集合是消费者),则使用
? super T。 例如,List<? super Integer>允许你向列表中添加Integer类型的元素。
如果既要读取数据,又要写入数据,则不要使用通配符。
PECS原则总结:
| 操作 | 集合角色 | 使用通配符 | 通配符类型 | 示例 |
|---|---|---|---|---|
| 读取数据 | 生产者 | 是 | ? extends T |
List<? extends Number> |
| 写入数据 | 消费者 | 是 | ? super T |
List<? super Integer> |
| 既读又写 | 否 | 无 | List<Integer> |
7. PECS原则的应用案例
7.1. 复制集合
考虑一个复制集合的场景。 我们需要从一个集合中读取元素,并将它们添加到另一个集合中。
public class PECSExample {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T element : src) {
dest.add(element);
}
}
public static void main(String[] args) {
List<Integer> integerList = new ArrayList<>();
integerList.add(1);
integerList.add(2);
List<Number> numberList = new ArrayList<>();
copy(numberList, integerList);
System.out.println("Number list: " + numberList);
List<Object> objectList = new ArrayList<>();
copy(objectList, integerList);
System.out.println("Object list: " + objectList);
}
}
在这个例子中,src 集合是生产者,我们从中读取元素,因此我们使用 ? extends T。 dest 集合是消费者,我们向其中写入元素,因此我们使用 ? super T。
7.2. 比较器
再考虑一个使用比较器的场景。
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Animal implements Comparable<Animal> {
private int age;
public Animal(int age) {
this.age = age;
}
public int getAge() {
return age;
}
@Override
public int compareTo(Animal other) {
return Integer.compare(this.age, other.age);
}
}
class Dog extends Animal {
public Dog(int age) {
super(age);
}
}
public class ComparatorExample {
public static <T> void sort(List<T> list, Comparator<? super T> comparator) {
Collections.sort(list, comparator);
}
public static void main(String[] args) {
List<Dog> dogList = new ArrayList<>();
dogList.add(new Dog(3));
dogList.add(new Dog(1));
dogList.add(new Dog(2));
// 使用 Animal 类型的 Comparator 对 Dog 列表进行排序
Comparator<Animal> animalComparator = Comparator.comparingInt(Animal::getAge);
sort(dogList, animalComparator);
for (Dog dog : dogList) {
System.out.println("Dog age: " + dog.getAge());
}
}
}
在这个例子中,Comparator 是一个消费者,它消费 T 类型的对象。 因此,我们使用 ? super T。 这允许我们使用比较 T 的父类的比较器来比较 T 类型的对象。
8. 类型擦除 (Type Erasure)
需要注意的是,Java泛型是通过类型擦除来实现的。 这意味着在编译时,泛型类型信息会被擦除,并替换为原始类型(raw type)。 例如,List<String> 和 List<Integer> 在运行时都会变成 List。
类型擦除会影响泛型的某些行为,例如:
- 不能使用基本类型作为类型参数(例如,
List<int>是不允许的,必须使用List<Integer>)。 - 在运行时无法获取泛型类型信息。
9. 总结
通配符上下界是Java泛型中一个强大而复杂的特性。 通过使用 ? extends T 和 ? super T,我们可以编写更灵活、更通用的代码,同时保持类型安全。 PECS原则是使用通配符的一个重要指导原则,它告诉我们什么时候使用 extends,什么时候使用 super。 深入理解通配符的上下界和PECS原则,能写出更优雅更健壮的Java泛型代码。