什么是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接口:
public class Stack<E>{
public Stack();
public void push(E e):
public E pop();
public boolean isEmpty();
//按顺序将一系列元素全部放入Stack中,你可能想到的实现方式如下:
public void pushAll(Iterable<E> src){
for(E e : src)
push(e)
}
}
这时,有个Stack<Number>,想要灵活的处理Integer,Long等Number的子类型的集合:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ....;
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){
if(!isEmpty()){
dst.add(pop());
}
}
假设有一个Stack<Number>和Collection<Object>对象:
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
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){
if(!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()、new ArrayList() 或者 new ArrayList()。
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 允许我们将 List、List 和 List 都赋值给它。然而,虽然我们可以从这个列表中获取元素(因为我们知道它们至少是 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 list:表示泛型的类型是 Fruit 或 Fruit 的子类,一般用于只获取元素。
- List list:表示泛型的类型是 Fruit 或 Fruit 的父类,一般用于只添加元素(获取出来的元素是 Object 类型,泛型意义不大)。
- List list:明确的泛型,可以获取元素,也可以添加元素,是最常用的泛型。
案例三
1. Producer Extends
- 定义: 当集合是生产者(提供数据)时,使用
<? extends T>
。 - 含义: 集合中的元素是
T
或其子类,只能从中读取数据,不能写入(除null
外)。 - 示例:
List<? extends Number> numbers = new ArrayList<Integer>();
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