在Java编程中,泛型是一个强大的工具,能够提高代码的通用性和类型安全性。然而,许多开发者在使用泛型时会遇到困难,特别是关于PECS原则的理解。PECS,即 “Producer Extends, Consumer Super”,是使用泛型通配符的核心法则。本文将深入探讨PECS原则,通过详尽的代码示例和实战应用,帮助你彻底掌握泛型中的 <? extends T> 和 <? super T> 的用法。我们将解释为什么生产者(只读)使用 extends,消费者(只写)使用 super,并展示如何在实际开发中应用这一原则。通过本文,你将学会编写更灵活、安全和高效的Java代码。
彻底搞懂Java泛型中的PECS原则
在Java中,泛型(Generics)是一个强大的工具,它允许我们在编写代码时定义类、接口和方法时使用类型参数。泛型使得代码更具通用性、类型安全性和可读性。在泛型中,PECS原则是一个非常重要的概念。本文将详细讲解PECS原则,帮助大家更好地理解和应用。
什么是PECS原则?
PECS 是 “Producer Extends, Consumer Super” 的缩写。这句话的意思是:生产者(Producer)用 extends
,消费者(Consumer)用 super
。这个原则在使用泛型限定通配符时非常有用,可以帮助我们决定何时使用 extends
和 super
。
两种限定通配符
- 类型的上界:
<? extends T>
表示类型必须为T
类型或者T
的子类。 - 类型的下界:
<? super T>
表示类型必须为T
类型或者T
的父类。
上代码
为了更好地解释这些概念,我们先定义一些类:
public class Fruit {
}
public class Apple extends Fruit {
}
public class Banana extends Fruit {
}
List<? extends Fruit> 的理解
正如字面意思,<? extends Fruit>
表示泛型的类型是 Fruit
或者 Fruit
的子类。也就是说,我们给 list
赋值时,泛型可以是 Fruit
或者 Fruit
的子类,比如 new ArrayList<Fruit>()
、new ArrayList<Apple>()
或者 new ArrayList<Banana>()
。
private static List<? extends Fruit> getExtendsList() {
List<? extends Fruit> list;
list = new ArrayList<Fruit>();
list = new ArrayList<Apple>();
list = new ArrayList<Banana>();
return list;
}
具体来说,List<? extends Fruit>
允许我们将 List<Fruit>
、List<Apple>
和 List<Banana>
都赋值给它。然而,虽然我们可以从这个列表中获取元素(因为我们知道它们至少是 Fruit
类型),但我们不能向其中添加元素(除了 null
),因为编译器无法确定我们实际的列表类型。
private static void m1(List<? extends Fruit> list) {
Fruit fruit = list.get(0); // 安全的,因为我们知道列表中至少是Fruit类型
// list.add(new Apple()); // 错误,编译器不允许,因为list可能是new ArrayList<Banana>()
}
List<? super Fruit> 的理解
<? super Fruit>
表示泛型的类型是 Fruit
或者 Fruit
的父类。也就是说,我们给 list
赋值时,泛型可以是 Fruit
或者 Fruit
的父类,比如 new ArrayList<Fruit>()
或者 new ArrayList<Object>()
。
private static List<? super Fruit> getSuperList() {
List<? super Fruit> list;
list = new ArrayList<Fruit>();
list = new ArrayList<Object>();
return list;
}
具体来说,List<? super Fruit>
允许我们向列表中添加 Fruit
或者 Fruit
的子类,但我们不能安全地获取特定类型的元素,只能获取 Object
类型的元素。
private static void m2(List<? super Fruit> list) {
list.add(new Apple()); // 可以,因为Apple是Fruit的子类
list.add(new Banana()); // 可以,因为Banana是Fruit的子类
Object object = list.get(0); // 只能是Object类型,因为我们不知道具体的类型
}
总结
- List<? extends Fruit> list:表示泛型的类型是
Fruit
或Fruit
的子类,一般用于只获取元素。 - List<? super Fruit> list:表示泛型的类型是
Fruit
或Fruit
的父类,一般用于只添加元素(获取出来的元素是Object
类型,泛型意义不大)。 - List list:明确的泛型,可以获取元素,也可以添加元素,是最常用的泛型。
JDK中的PECS
在JDK中,PECS原则被广泛应用。例如,java.util.Collections
类中的 copy
方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i = 0; i < srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di = dest.listIterator();
ListIterator<? extends T> si = src.listIterator();
for (int i = 0; i < srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
在这个方法中,dest
是一个消费者,只接受元素,所以使用 super
;而 src
是一个生产者,只提供元素,所以使用 extends
。
深入理解泛型通配符
为了更深入理解,我们可以考虑以下几个方面:
-
协变(Covariance)和逆变(Contravariance):
<? extends T>
是协变的,这意味着如果Apple
是Fruit
的子类,那么List<Apple>
也是List<? extends Fruit>
的子类型。<? super T>
是逆变的,这意味着如果Fruit
是Apple
的父类,那么List<Fruit>
也是List<? super Apple>
的父类型。
-
泛型方法中的PECS:
- 在定义泛型方法时,PECS原则同样适用。通过明确指定泛型参数的上下界,可以更好地控制方法的输入和输出。
public static <T> void addToList(List<? super T> list, T item) {
list.add(item);
}
public static <T> void copyList(List<? extends T> source, List<? super T> destination) {
for (T item : source) {
destination.add(item);
}
}
- 实战应用:
- 在实际开发中,我们常常需要根据需求选择合适的泛型通配符。例如,在处理只读取数据的列表时,可以使用
<? extends T>
;而在处理需要写入数据的列表时,可以使用<? super T>
。
- 在实际开发中,我们常常需要根据需求选择合适的泛型通配符。例如,在处理只读取数据的列表时,可以使用
结语
理解和应用PECS原则是掌握Java泛型的重要一步。通过正确使用泛型通配符,我们可以编写出更为通用、灵活和类型安全的代码。希望这篇文章能够帮助你彻底搞懂Java泛型中的PECS原则,让你在编程的道路上少踩坑,多收获。