Java泛型-PECS原则

什么是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类型,因为我们不知道具体的类型
}

总结

  1. List list:表示泛型的类型是 Fruit 或 Fruit 的子类,一般用于只获取元素。
  2. List list:表示泛型的类型是 Fruit 或 Fruit 的父类,一般用于只添加元素(获取出来的元素是 Object 类型,泛型意义不大)。
  3. 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

参考文章

泛型-PECS原则 – iiiorz – 博客园

[简单粗暴]一文彻底搞懂Java泛型中的PECS原则(在坑里躺了多年终于爬出来了)-CSDN博客

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇