[java]你应该知道的泛型(Generic)与PECS原则

本文通过一个水果篮子的例子,试图帮助读者理解泛型使用中的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版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容