什么是PECS(制作人扩展消费者超级)?

时间:2010-04-27 17:16:40

标签: java generics super bounded-wildcard pecs

我在阅读泛型时遇到了PECS(制作人extends和消费者super 的缩写)。

有人可以向我解释如何使用PECS来解决extendssuper之间的混淆吗?

15 个答案:

答案 0 :(得分:749)

tl; dr:“PECS”来自集合的观点。如果您从通用集合中提取项目,则它是生产者,您应该使用extends;如果您只是 填充项目,则它是消费者,您应该使用super。如果您同时使用同一个集合,则不应使用extendssuper


假设您有一个方法,它将事物的集合作为参数,但您希望它比仅接受Collection<Thing>更灵活。

案例1:您希望浏览集合并对每个项目执行操作。
然后列表是生成器,因此您应该使用Collection<? extends Thing>

原因是Collection<? extends Thing>可以保存Thing的任何子类型,因此当您执行操作时,每个元素都会表现为Thing。 (实际上您无法向Collection<? extends Thing>添加任何内容,因为您无法在运行时知道该集合所拥有的特定子类型Thing。)

案例2:您想要向集合中添加内容。
然后列表是消费者,因此您应该使用Collection<? super Thing>

这里的推理是,与Collection<? extends Thing>不同,Collection<? super Thing>无论实际的参数化类型是什么,都可以始终保持Thing。这里你不关心列表中已有的内容,只要它允许添加Thing;这是? super Thing保证的。

答案 1 :(得分:500)

计算机科学背后的原理被称为

  • 协方差:? extends MyClass
  • Contravariance:? super MyClass
  • 不变/非差异:MyClass

下面的图片应该解释这个概念。

图片提供:Andrey Tyukin

Covariance vs Contravariance

答案 2 :(得分:31)

PECS(制作人extends和消费者super

助记符→获取和放置原则。

这个原则表明:

  • 如果只从结构中获取值,请使用扩展通配符。
  • 仅在将值放入结构时使用超级通配符。
  • 当你同时放弃时,不要使用通配符。

Java中的示例:

class Super {

    Object testCoVariance(){ return null;} //Covariance of return types in the subtype.
    void testContraVariance(Object parameter){} // Contravariance of method arguments in the subtype.
}

class Sub extends Super {

    @Override
    String testCoVariance(){ return null;} //compiles successfully i.e. return type is don't care(String is subtype of Object) 
    @Override
    void testContraVariance(String parameter){} //doesn't support even though String is subtype of Object

}

Liskov substitution principle:如果S是T的子类型,那么T类型的对象可以用S类型的对象替换。

在编程语言的类型系统中,输入规则

  • 协变,如果它保留了类型(≤)的排序,它将类型从更具体的顺序排列到更通用的类型;
  • 逆变如果它反转此顺序;
  • 如果这些都不适用,则
  • 不变或非变数。

Covariance and contravariance

  • 只读数据类型(来源)可以是 covariant ;
  • 只写数据类型(接收器)可以是逆变
  • 作为源和接收器的可变数据类型应不变

为了说明这种一般现象,请考虑数组类型。对于Animal类型,我们可以创建Animal []

类型
  • 协变:Cat []是动物[];
  • 逆变:Animal []是Cat [];
  • 不变:Animal []不是Cat []而Cat []不是Animal []。

Java示例:

Object name= new String("prem"); //works
List<Number> numbers = new ArrayList<Integer>();//gets compile time error

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //attempt of heap pollution i.e. at runtime gets java.lang.ArrayStoreException: java.lang.Double(we can fool compiler but not run-time)

List<String> list=new ArrayList<>();
list.add("prem");
List<Object> listObject=list; //Type mismatch: cannot convert from List<String> to List<Object> at Compiletime  

more examples

有界(即前往某处)通配符:有3种不同的通配符:

  • 方差/非方差:?? extends Object - 无界通配符。它代表所有类型的家庭。当你得到并放好时使用。
  • 协方差:? extends T(属于T的所有类型的系列) - 带有上限的通配符。 T是继承层次结构中的 upper 最重要的类。只有获取值的结构时,才使用extends通配符。
  • Contra-variance:? super T(属于T的超类型的所有类型的系列) - 带下限的通配符。 T是继承层次结构中 lower 最重要的类。仅当值添加到结构中时,请使用super通配符。

注意:通配符?表示零或一次,表示未知类型。通配符可以用作参数的类型,从不用作泛型方法调用的类型参数,通用类实例创建。(即使用通配符时,引用未在程序中的其他地方使用,就像我们使用{{1} })

enter image description here

T

genericsexamples

答案 3 :(得分:28)

public class Test {

    public class A {}

    public class B extends A {}

    public class C extends B {}

    public void testCoVariance(List<? extends B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b); // does not compile
        myBlist.add(c); // does not compile
        A a = myBlist.get(0); 
    }

    public void testContraVariance(List<? super B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b);
        myBlist.add(c);
        A a = myBlist.get(0); // does not compile
    }
}

答案 4 :(得分:22)

正如我在my answer中向另一个问题解释的那样,PECS是由Josh Bloch创建的助记符设备,用于帮助记住 P roducer extends C onsumer super

  

这意味着当传递给方法的参数化类型将生成 T的实例(它们将以某种方式从中检索)时,? extends T应该是使用,因为T的子类的任何实例也是T

     

当传递给方法的参数化类型将消耗 T的实例(它们将被传递给它以执行某些操作)时,应使用? super T,因为T的实例可以合法地传递给任何接受某些超类型T的方法。例如,可以在Comparator<Number>上使用Collection<Integer>? extends T无效,因为Comparator<Integer>无法对Collection<Number>进行操作。

请注意,通常您应该只使用? extends T? super T作为某些方法的参数。方法应该只使用T作为泛型返回类型的类型参数。

答案 5 :(得分:19)

简而言之,要记住PECS有三个简单的规则:

  1. 如果需要检索对象,请使用<? extends T>通配符 从集合中输入T
  2. 如果需要放置<? super T>类型的对象,请使用T通配符 一个集合。
  3. 如果你需要满足这两个要求,那就不要使用任何通配符。如 就这么简单。

答案 6 :(得分:8)

对于我来说,这是扩展与超级的最清晰,最简单的方法:

  • extends用于阅读

  • super用于写作

我发现“ PECS”是一种关于谁是“生产者”和谁是“消费者”的思考方式。 “ PECS”是从数据集合本身的角度定义的-集合“消费”是否将对象写入 (它从调用代码中消耗对象),如果从 中读取对象,它就会“产生”(它正在为某些调用代码产生对象)。这与其他所有内容的命名方式相反。标准Java API是从调用代码的角度命名的,而不是集合本身的名称。例如,java.util.List的以集合为中心的视图应具有一个名为“ receive()”而不是“ add()”的方法–毕竟,调用代码 add ,但是列表本身接收该元素。

我认为从与集合交互的代码的角度来思考事物是更直观,自然和一致的–代码是“读取”还是“写入”集合?之后,写入该集合的任何代码将成为“生产者”,而读取该集合的任何代码将成为“消费者”。

答案 7 :(得分:6)

(添加答案,因为Generics通配符中没有足够的例子)

       // Source 
       List<Integer> intList = Arrays.asList(1,2,3);
       List<Double> doubleList = Arrays.asList(2.78,3.14);
       List<Number> numList = Arrays.asList(1,2,2.78,3.14,5);

       // Destination
       List<Integer> intList2 = new ArrayList<>();
       List<Double> doublesList2 = new ArrayList<>();
       List<Number> numList2 = new ArrayList<>();

        // Works
        copyElements1(intList,intList2);         // from int to int
        copyElements1(doubleList,doublesList2);  // from double to double


     static <T> void copyElements1(Collection<T> src, Collection<T> dest) {
        for(T n : src){
            dest.add(n);
         }
      }


     // Let's try to copy intList to its supertype
     copyElements1(intList,numList2); // error, method signature just says "T"
                                      // and here the compiler is given 
                                      // two types: Integer and Number, 
                                      // so which one shall it be?

     // PECS to the rescue!
     copyElements2(intList,numList2);  // possible



    // copy Integer (? extends T) to its supertype (Number is super of Integer)
    private static <T> void copyElements2(Collection<? extends T> src, 
                                          Collection<? super T> dest) {
        for(T n : src){
            dest.add(n);
        }
    }

答案 8 :(得分:5)

让我们假设这个层次结构:

class Creature{}// X
class Animal extends Creature{}// Y
class Fish extends Animal{}// Z
class Shark extends Fish{}// A
class HammerSkark extends Shark{}// B
class DeadHammerShark extends HammerSkark{}// C

让我们澄清一下PE-生产者扩展:

List<? extends Shark> sharks = new ArrayList<>();

为什么不能在此列表中添加扩展“鲨鱼”的对象?喜欢:

sharks.add(new HammerShark());//will result in compilation error

由于您的列表在运行时可能是A,B或C类型,因此无法在其中添加任何类型的对象,因为最终可能会组合使用在Java中是不允许的。
实际上,编译器确实可以在编译时看到您添加了B:

sharks.add(new HammerShark());

...但是它无法告诉您B在运行时是列表类型的子类型还是超类型。在运行时,列表类型可以是A,B,C中的任何一种。因此,例如,您最终不能在DeadHammerShark列表中添加HammerSkark(超级类型)。

*您将说:“好的,但是为什么我不能在其中添加HammerSkark,因为它是最小的类型?”。 答:这是您所知道的最小的。购买HammerSkark也可以被其他人扩展,您最终会遇到相同的情况。

让我们澄清一下CS-超级消费者:

在同一层次结构中,我们可以尝试以下方法:

List<? super Shark> sharks = new ArrayList<>();

什么以及为什么可以添加到此列表中?

sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());

您可以添加以上对象的类型,因为在shark(A,B,C)以下的任何内容始终都是在shark(X,Y,Z)上方的所有内容的子类型。容易明白。

不能在Shark上方添加类型,因为在运行时,所添加对象的类型在层次结构上可能高于列表的声明类型(X,Y,Z )。这是不允许的。

但是为什么您不能从此列表中读取? (我的意思是可以从中获取一个元素,但不能将其分配给对象o以外的其他对象):

Object o;
o = sharks.get(2);// only assignment that works

Animal s;
s = sharks.get(2);//doen't work

在运行时,列表的类型可以是A之上的任何类型:X,Y,Z,... 编译器可以编译您的赋值语句(似乎正确),但是在运行时的类型(动物)可以在层次结构上低于列表的声明类型(可以为Creature或更高)。 )。这是不允许的。

总结

我们使用<? super T>在列表中添加类型等于或小于T的对象。我们无法读取 它。
我们使用<? extends T>从列表中读取类型等于T或小于T的对象。我们无法向其中添加元素。

答案 9 :(得分:5)

PECS的“规则”仅确保以下内容合法:

  • 消费者:无论?是什么,它都可以合法地引用 T
  • 生产者:无论?是什么, T

沿List<? extends T> producer, List<? super T> consumer行的典型配对只是确保编译器可以执行标准的“ IS-A”继承关系规则。如果我们可以合法地这样做,那么说<T extends ?>, <? extends T>可能会更简单(或者更好的是在Scala中,就像您在上面看到的那样,它是[-T], [+T]。不幸的是,我们可以做的最好的事情是<? super T>, <? extends T>

当我第一次遇到这个问题并将其分解为头脑时,机制是有意义的,但是代码本身仍然让我感到困惑-我一直在想“似乎不需要像那样颠倒界限了”-即使我在上述内容上很明确-只是为了保证遵守标准的参考规则。

使用普通作业作为类比的方法来帮助我解决问题。

请考虑以下(尚未投入生产)玩具代码:

// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
   for(T t : producer)
       consumer.add(t);
}

根据分配类比进行说明,对于consumer?通配符(未知类型)是引用-分配的“左侧”,而<? super T>确保?T“ IS-A” ?-可以将T分配给它,因为?是超类型(或在类型与T最为相似。

对于producer,关注点是一样的,只是被反转了:producer的{​​{1}}通配符(未知类型)是引用对象-“右手”,然后?确保<? extends T>?“ IS-A” ?-可以分配 T ,因为T?的子类型(或至少是同一类型)。

答案 10 :(得分:2)

记住这一点:

  

消费者吃晚餐(超级);制片人延伸他父母的工厂

答案 11 :(得分:2)

[Covariance and contravariance]

让我们看一个例子

public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }

泛型使您可以安全地动态使用Types

//ListA
List<A> listA = new ArrayList<A>();

//add
listA.add(new A());
listA.add(new B());
listA.add(new C());

//get
A a0 = listA.get(0);
A a1 = listA.get(1);
A a2 = listA.get(2);
//ListB
List<B> listB = new ArrayList<B>();

//add
listB.add(new B());

//get
B b0 = listB.get(0);

问题

由于Java的Collection是一种引用类型,因此我们遇到了下一个问题:

问题1

//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;

* Swift的泛型不存在此类问题,因为Collection是Value type [About],因此创建了一个新集合

问题2

//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;

解决方案-通用通配符

通配符是参考类型的功能,不能直接实例化

解决方案#1 <? super A>又名下界又名协变,也就是消费者保证它是由A和所有超类操作的,这就是为什么添加

是安全的
List<? super A> listSuperA;
listSuperA = listA;
listSuperA = new ArrayList<Object>();

//add
listSuperA.add(new A());
listSuperA.add(new B());

//get
Object o0 = listSuperA.get(0);

解决方案2

<? extends A>(又名上限,也称为协方差),即生产者保证它是由A和所有子类操作的,因此,获取并进行强制转换

是安全的
List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;

//get
A a0 = listExtendsA.get(0);

答案 12 :(得分:1)

协方差:接受子类型
相反性:接受超类型

协变类型为只读,而协变类型为只写。

答案 13 :(得分:1)

让我们尝试形象化这个概念。

<? super SomeType>是“未定义(尚未)”类型,但是该未定义类型应该是“ SomeType”类的超级类。

<? extends SomeType>也是如此。 应该扩展“ SomeType”类的类型(它应该是“ SomeType”类的子类)。

如果我们在维恩图中考虑“类继承”的概念,则示例将如下所示:

enter image description here

哺乳动物类扩展动物类(动物类是哺乳动物类的 super 类)。

猫/狗类扩展哺乳动物类(哺乳动物类是猫/狗类的超级类)。

然后,让我们将上图中的“圆圈”视为具有物理体积的“盒子”。

enter image description here

您不能将较大的盒子放入较小的盒子。

您只能将较小的盒子放入较大的盒子中。

您说<? super SomeType>时要描述的是与“ SomeType”框大小相同或更大的“框”。

如果您说<? extends SomeType>,则要描述一个与“ SomeType”框大小相同或的“框”。

那PECS到底是什么?

“生产者”的一个示例是我们仅从中读取的列表。

“消费者”的一个示例是我们仅写入的列表。

请记住这一点:

  • 我们从“生产者”那里“读”书,然后将它们放入我们自己的盒子中。

  • 然后我们将自己的盒子“写入”“消费者”。

因此,我们需要从“生产者”中读取(获取)某些东西,然后将其放入“盒子”中。这意味着从生产者那里获取的任何盒子都不应该大于我们的“盒子”。这就是为什么“ P 引辊 E 延伸”的原因。

“扩展”是指较小的框(在上方的维恩图中较小的圆圈)。生产者的盒子应该比我们自己的盒子小,因为我们要从生产者那里拿走那些盒子,然后放入我们自己的盒子中。我们不能放比盒子更大的东西!

此外,我们需要将自己的“盒子”写(放入)“消费者”。这意味着消费者的包装盒应该小于我们自己的包装盒。这就是为什么“ C 消费者 S 较高”。

“超级”是指一个更大的盒子(上面的维恩图中较大的圆圈)。如果我们想将自己的盒子放到消费者中,那么消费者的盒子应该比我们的盒子大!

现在,我们可以轻松理解此示例:

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src) {
      for (int i = 0; i < src.size(); i++) 
        dest.set(i, src.get(i)); 
  } 
}

在上面的示例中,我们希望从src中读取(获取)某些内容并将其写入(放入)dest中。因此,src是一个“生产者”,它的“盒子”应该比某些类型的T小(更具体)。

反之亦然,dest是一个“消费者”,它的“盒子”应该比某些类型的T大(更一般)。

如果src的“盒子”大于dest的“盒子”,我们不能将那些大盒子放到dest的较小盒子中。

如果有人读过这篇文章,希望它能帮助您更好地理解 “ P 引纸器 E xtends, C 在消费者 S 上方。”

祝您编程愉快! :)

答案 14 :(得分:0)

使用现实生活中的示例(有一些简化):

  1. 想象一下,一辆货运火车像货车一样,是一个清单。
  2. 如果货物大小等于或小于货运车= <? super FreightCarSize>
  3. ,则可以放入货运车中的货物
  4. 如果您的仓库中有足够的地方(大于货物的大小),您可以从货运车中卸下货物<{1} / li>