本文通过一个水果篮子的例子,试图帮助读者理解泛型使用中的PECS原则。本文假设读者对泛型以及泛型通配符有基础性的了解。
一个水果篮子
笔者将以一个装水果的篮子(List集合)为例,示范泛型的使用。水果的继承关系如下:
public class Fruit {...}
public class Apple extends Fruit {...}
泛型
泛型是java1.5出现的语言特征。在没有泛型之前,从集合中读取出的每个对象都必须进行类型转化。这样导致一些类型的错误只有在运行时才能发现:
/**不使用泛型**/
List basket = new ArrayList();//水果篮子
basket.add("水果");
Fruit fruit = (Fruit)basket.get(0);//编译正确,运行错误
有了泛型,就不再需要运行时的类型转化,可以直接告诉编译器集合接受什么类型的对象,编译器在编译时可以做检查:
/**使用泛型,不再需要类型转化**/
Fruit get = basket.get(0);
将篮子里将水果都拿出来
我写一个方法,将水果篮子中所有水果拿出来(即取出集合所有元素并进行操作)
public static void getOutFruits(List<Fruit> basket){
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
}
接着在装水果的蓝子(List<Fruit>)和装苹果的篮子(List<Apple>)上执行这个方法:
List<Fruit> fruitBasket = new ArrayList<Fruit>();
fruitBasket(new Fruit());
getOutFruits(fruitBasket);//成功
List<Apple> appleBasket = new ArrayList<Apple>();
appleBasket(new Apple());
//getOutFruits(appleBasket);//编译错误
//getOutFruits((List<Fruit>) appleBasket);//强制类型转换,同样编译错误
//不兼容的类型: List<Apple>无法转换为List<Fruit>
结果出人意料:装苹果的篮子(List<Apple>)执行时编译出错了。错误显示无法转换。强制转换也没有用。
这个不科学呀! 在面向对象中,子类型对象是可以转成父类型的。
原来泛型是不可变。即对于任何2个不同类型的type1和type2,List<Type1>即不是List<Type2>的子类型,也不是List<Type2>的超类型。(《effective java》第25条 )
所以,Fruit和Apple虽是父子关系,但作为2个不同的类型,List<Apple>和List<Fruit>之间没有继承关系,所以2者之间无法转化。
使用<? extends T>进行改进
如果想解决上面的问题,即在装水果的蓝子(List<Fruit>)的地方,兼容装苹果的篮子(List<Apple>),则需要使用<? extends T>这种通配符泛型。
/**参数使用List<? extends Fruit>**/
public static void getOutFruits(List<? extends Fruit> basket){
for (Fruit fruit : basket) {
System.out.println(fruit);
//...do something other
}
}
public static void main(String[] args) {
List<Fruit> fruitBasket = new ArrayList<>();
fruitBasket.add(new Fruit());
getOutFruits(fruitBasket);
List<Apple> appleBasket = new ArrayList<>();
appleBasket.add(new Apple());
getOutFruits(appleBasket);//编译正确
}
问题解决了。说明List<? extends Fruit>,同时兼容了List<Fruit>和List<Apple>,我们可以理解为List<? extends Fruit>现在是List<Fruit>和List<Apple>的超类型(父类型)了
哎呦,原来使用<? extends T>就万事大吉,哈哈!
怎么可能?少年你还太年轻了!
再看这个例子
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? extends Fruit> basket = apples;//按上一个例子,这个是可行的
for (Fruit fruit : basket)
{
System.out.println(fruit);
}
//basket.add(new Apple()); //编译错误
//basket.add(new Fruit()); //编译错误
问题出现了,明明是就放水果的篮子(List<? extends Fruit>,可兼容List<Fruit>和List<Apple>),现在不仅不能放苹果到里面,连水果也不能放入了。不过从篮子取出水果是可以的,这又是怎么回事?
笔者试着用解释一下:用了<? extends Fruit>相当于告诉编译器,我们的篮子(集合)是用来处理水果以及水果的子类型。因为子类型有许多,我们并没有告诉编译器是哪个子类型。
编译器在这里遇到的问题是,如果add的是Apple类型时,则basket应该是List<Apple>,如果add是Fruit类型,则basket应该是List<Fruit>。而List<Apple>和List<Fruit>前面已经提过,是2个完全没有关系的类型,
所以编译器不知道是哪个子类型将加入集合,不知道到底是List<Apple>还是List<Fruit>,所以编译器只能报错。(注意,这里讨论的都是类型,而不是对象)
另一方面,编译器已经知道集合里全部都是水果的子类型,所以编译器可以保证取出的数据全部是水果。
所以,在上面的例子中,我们从篮子中拿水果,实际就是从集合里获取元素。简单的说,当只想从集合中获取元素,请把这个集合看成生产者,请使用<? extends T>,这就是Producer extends原则,PECS原则中的PE部分。
改用<? super T>试试
上一个例子里,我们不能往篮子里加水果。现在换一个角度,我们要实现如何往篮子里加水果,而且是不同的水果。这将用到<? super T>通配符泛型。
首先我们扩展一下水果的继承关系,增加苹果的子类型redApple:
public class Fruit {...}
public class Apple extends Fruit {...}
public class RedApple extends Apple {...}
下面使用<? super T>的例子:
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? super Apple> basket = apples;//这里使用了super
basket.add(new Apple());
basket.add(new RedApple());
//basket.add(new Fruit()); //编译错误
Object object = basket.get(0);//正确
//Fruit fruit =basket.get(0);//编译错误
//Apple apple = basket.get(0);//编译错误
//RedApple redApple = basket.get(0);//编译错误
显然,苹果和红萍果都能正确地放入篮子(List<? super Apple>)。但奇怪的是,水果对象却不能。另一个奇怪现象是,篮子中只能取出Object类型的对象。
笔者试图解释一下:用了<? super Apple>相当于告诉编译器,集合接受处理Apple以及Apple的超类型,即Object,Fruit,Apple三个类型。
但编译器并不知道到底是List<Object>,List<Fruit>还是List<Apple>?
编译器只知道,苹果和苹果子类型是可以放进去(也是Fruit的子类型,也是Object的子类型)。这意味着,我们总是可以将一个苹果的子类型放入苹果的超类型的list中。
而取出时的情况是,编译器不知道是按哪个类型取出, 到底是Object,Fruit,Apple中的哪个呢?但是编译器可以选择永远不会错的类型,也就是Object的类型,因为Object是所有类型的超类型。
因此,在上面的例子中的,我们将数据放进集合List<? super Apple> basket,所以这个篮子是实际上消费元素,例如Apple。简单的说,当你仅仅想增加元素到集合,把这个集合看成消费者,请使用<? super T>。这就是Consumer super原则,PECS原则中的CS部分。
总结PECS原则
- 如果你只需要从集合中获得类型T , 使用<? extends T>通配符
- 如果你只需要将类型T放到集合中, 使用<? super T>通配符
- 如果你既要获取又要放置元素,则不使用任何通配符。例如List<Apple>
- PECS即 Producer extends Consumer super, 为了便于记忆。(《effective java》第28条)
为何要PECS原则?
你还记得前面提到泛型是不可变吗?即List<Fruit>和List<Apple>之间没有任何继承关系。API的参数想要同时兼容2者,则只能使用PECS原则。这样做提升了API的灵活性。
在java集合API中,大量使用了PECS原则,例如java.util.Collections中的集合复制的方法:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
}
集合复制是最典型的用法:
- 复制源集合src,主要获得元素,所以用<? extends T>
- 复制目标集合dest,主要是设置元素,所以用<? super T>
当然,为了提升了灵活性,自然牺牲了部分功能。鱼和熊掌不能兼得。
补充说明
- 这里的错误全部是编译阶段不是运行阶段,编译阶段程序是没有运行。所以不能用运行程序的思维来思考。
- 使用泛型,就是要在编译阶段,就找出类型的错误来。
参考资料
《Effective Java》第2版