什么是PECS原则?
PECS 是 “Producer Extends, Consumer Super” 的缩写。换句话说,如果参数化类型表示一个生产者(只读)就使用 <? extends T>,如果它表示一个消费者(只写)就使用 <? super T>。这个原则在使用泛型限定通配符时非常有用,可以帮助我们决定何时使用 <? extends T> 和 <? super T>。
<? extends T>和<? super T>是Java泛型中的“通配符(Wildcards)”和“边界(Bounds)”的概念。
<? extends T>:是指 “上界通配符(Upper Bounds Wildcards)”<? super T>:是指 “下界通配符(Lower Bounds Wildcards)”
为什么要有限定通配符?
假设有这样一些类型定义。
//Lev 1
class Food{}
//Lev 2
class Fruit extends Food{}
class Meat extends Food{}
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}
定义一个容器,Plate类。盘子里可以放一个泛型的“东西”:
class Plate<T>{
private T item;
public Plate(T t){item=t;}
public void set(T t){item=t;}
public T get(){return item;}
}
现在我定义一个“水果盘子”,逻辑上水果盘子当然可以装苹果。
Plate<Fruit> p=new Plate<Apple>(new Apple()); // 反例:泛型不协变
但实际上Java编译器不允许这个操作。会报错,“装苹果的盘子”无法转换成“装水果的盘子”。
其实很好解释:
苹果 IS-A 水果
装苹果的盘子 NOT-IS-A 装水果的盘子
所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的。于是有了<? extends T>和<? super T>。
上界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

下界通配符 Plate<? super Fruit> 覆盖下图中红色的区域。

接下来看一下PECS,再举个例子:
下面是一个简单的Stack的API接口:
// 使用 java.util 中的集合类型。
public class Stack<E>{
public Stack() {}
private final java.util.Deque<E> data = new java.util.ArrayDeque<>();
public void push(E e) { data.push(e); }
public E pop() { return data.pop(); }
public boolean isEmpty() { return data.isEmpty(); }
//按顺序将一系列元素全部放入Stack中,你可能想到的实现方式如下:
public void pushAll(Iterable<E> src){
for(E e : src)
push(e);
}
}
这时,有个Stack<Number>,想要灵活的处理Integer,Long等Number的子类型的集合:
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = java.util.List.of(1, 2);
numberStack.pushAll(integers);
此时代码编译无法通过,因为对于类型Number和Integer来说,虽然后者是Number的子类,但是对于任意Number集合(如List<Number>)不是Integer集合(如List<Integer>)的超类,因为泛型是不可变的。
幸好java提供了一种叫有限通配符的参数化类型,pushAll参数替换为“E的某个子类型的Iterable接口”:
public void pushAll(Iterable<? extends E> src){
for (E e: src)
push(e);
}
这样就可以正确编译了,这里的 <? extends E>就是所谓的 producer-extends。这里的 Iterable 就是生产者,要使用 <? extends E>。因为Iterable<? extends E> 可以容纳任何E的子类。在执行操作时,可迭代对象的每个元素都可以当作是E来操作。
与之对应的是:假设有一个方法popAll()方法,从Stack集合中弹出每个元素,添加到指定集合中去:
public void popAll(Collection<E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
假设有一个Stack<Number>和Collection<Object>对象:
Stack<Number> numberStack = new Stack<>();
Collection<Object> objects = new ArrayList<>();
numberStack.popAll(objects);
同样上面这段代码也无法通过,解决的办法就是使用 Collection<? super E>。这里的objects是消费者,因为是添加元素到objects集合中去。使用 Collection<? super E>后,无论objects是什么类型的集合,满足一点的是他是E的超类,所以不管这个参数化类型具体是什么类型都能将E装进objects集合中去。
PECS原则的运用
案例一
btw,我们反过来呢?以下两种均不能通过编译。
public void pushAll(Iterable<? super E> src){
for (E e: src)
push(e);
}
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是E的基类,那往里存粒度比E小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。Stack自然也无法存E的基类。
public void popAll(Collection<? extends E> dst){
while(!isEmpty()){
dst.add(pop());
}
}
原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。编译器在?标上一个占位符:CAP#1,来表示捕获一个E或E的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入A或者B或者C编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。
案例二
为了更好地解释这些概念,我们先定义一些类:
public class Fruit {
}
public class Apple extends Fruit {
}
public class Banana extends Fruit {
}
List<? 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()
}
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<Fruit> list:明确的泛型,可以获取元素,也可以添加元素,是最常用的泛型。
案例三
1. Producer Extends
定义: 当集合是生产者(提供数据)时,使用
<? extends T>。含义: 集合中的元素是
T或其子类,只能从中读取数据,不能写入(除null外)。示例:
List<? extends Number> numbers = new ArrayList<Integer>(java.util.List.of(1));
Number num = numbers.get(0); // 可以读取
// numbers.add(1); // 编译错误,不能写入
2. Consumer Super
定义: 当集合是消费者(接受数据)时,使用
<? super T>。含义: 集合中的元素是
T或其父类,只能写入T或其子类的数据,读取时只能得到Object类型。示例:
List<? super Integer> integers = new ArrayList<Number>();
integers.add(1); // 可以写入
// Integer i = integers.get(0); // 编译错误,读取时只能得到 Object
Object obj = integers.get(0); // 可以读取为 Object