什么是PECS(生产者扩展了超级消费者)?


727

在阅读泛型时,我遇到了PECS(生产者extends和消费者的super缩写)。

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


3
@ youtube.com / watch?v = 34oiEq9nD0M&feature = youtu.be&t = 1630提供了一个很好的解释示例,该示例解释了super一部分,但又给出了另一种思路。
lupchiazoem

Answers:


840

tl; dr: “ PECS”是从集合的角度来看的。如果仅从通用集合中提取项目,则它是生产者,应使用extends; 如果您仅将物品塞入其中,则它是消费者,应该使用super。如果您都使用同一集合,则不应使用extendssuper


假设您有一个方法以事物的集合为参数,但是您希望它比只接受一个更加灵活Collection<Thing>

情况1:您想浏览集合并为每个项目做事。
然后列表是生产者,因此您应该使用Collection<? extends Thing>

原因是a Collection<? extends Thing>可以包含的任何子类型Thing,因此每个元素Thing在执行操作时都将表现为a 。(实际上,您无法将任何内容添加到中Collection<? extends Thing>,因为您无法在运行时知道该集合包含哪种特定的子类型Thing。)

情况2:您想向集合中添加内容。
那么列表是一个消费者,因此您应该使用Collection<? super Thing>

这里的理由是,与不同Collection<? extends Thing>,无论实际的参数化类型是什么,Collection<? super Thing>都可以始终保持Thing。在这里,您不必关心列表中已经存在的内容,只要允许Thing添加即可。这就是? super Thing保证。


142
我一直在尝试这样思考:允许生产者生产更具体的东西,从而扩展,允许消费者接受更一般的东西,从而允许super
Feuermurmel

10
记住生产者/消费者区别的另一种方法是考虑方法签名。如果您有一个method doSomethingWithList(List list),那么您正在使用列表,因此将需要协方差/扩展(或不变的List)。另一方面,如果您的方法是List doSomethingProvidingList,则您正在生成列表,并且将需要逆/超(或不变列表)。
拉曼2014年

3
@MichaelMyers:为什么我们不能在这两种情况下都简单地使用参数化类型?在这里使用通配符是否有任何特定的优势,或者仅仅是一种提高可读性的方法,例如类似于const在C ++中使用引用作为方法参数来表示该方法不会修改参数?
Chatterjee 2014年

7
@拉曼,我想你只是感到困惑。在doSthWithList(您可以拥有List <?super Thing>)中,因为您是消费者,所以可以使用super(记住,CS)。但是,它是List <?扩展了Thing> getList(),因为在生成(PE)时允许您返回更具体的内容。
masterxilo 2014年

4
@AZ_我分享您的观点。如果方法从列表中获取get(),则该方法将被视为Consumer <T>,并且列表将被视为提供者;否则,列表将被视为提供者。但是PECS的规则是“从列表的角度来看”,因此需要“扩展”。应该是GEPS:扩展;放超级。
树鱼张

561

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

  • 协方差:? extends MyClass
  • 矛盾:? super MyClass
  • 不变/不变: MyClass

下图应解释该概念。图片提供:Andrey Tyukin

协方差vs协方差


143
嘿大家。我是Andrey Tyukin,我只是想确认anoopelias&DaoWen已与我联系并获得了我的使用草图的许可,该草图是根据(CC)-BY-SA许可的。Thx @ Anoop赋予了第二次生命^^ @Brian Agnew :(基于“少量票数”):这是因为它是Scala的草图,它使用Scala语法并假定了声明位置的变化,这与Java的怪异调用大不相同。现场差异...也许我应该写一个更详细的答案,清楚地显示出该草图如何适用于Java ...
Andrey Tyukin 2014年

3
这是我所发现的关于协方差和相反方差的最简单,最清晰的解释之一!
cs4r

@Andrey Tyukin您好,我也想使用此图像。我该如何联系您?
slouc

如果您对本说明有任何疑问,我们可以在聊天室里讨论它们chat.stackoverflow.com/rooms/145734/...
安德烈Tyukin


48

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替代原则: 如果S是T的子类型,则可以用S类型的对象替换T类型的对象。

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

  • 如果它保留类型(≤)的顺序,则为协变,它将类型从更具体的类型排序为更通用的类型;
  • 逆变如果它反转这个排序;
  • 不变不变,如果这两个都不适用。

协方差和协方差

  • 只读数据类型(源)可以是协变的
  • 只写数据类型(接收器)可以是逆向的
  • 既充当源又充当宿的可变数据类型应该是不变的

为了说明这种普遍现象,请考虑数组类型。对于动物类型,我们可以将动物类型[]

  • 协变: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  

更多例子

有界(即朝某个地方去)通配符:有3种不同的通配符类型:

  • 不变/不变:?? extends Object- 无界通配符。它代表所有类型的家庭。当您得到和放置时使用。
  • 协方差:(? extends T属于的子类型的所有类型的族 T)-具有上限的通配符。T继承层次结构中最末端的类。extends仅当从结构中获取值时使用通配符。
  • 反方差:(? super T属于的所有超类型的所有类型的族 T)-具有下限的通配符。T是继承层次结构中最底层的类。super仅在值放入结构中时使用通配符。

注意:通配符?表示零或一次,表示未知类型。通配符可以被用作一个参数的类型,从未用作一个通用的方法调用,一个通用类实例创建一个类型参数。(即,当使用的通配符该引用并非在程序的其它地方使用像我们使用T

在此处输入图片说明

class Shape { void draw() {}}

class Circle extends Shape {void draw() {}}

class Square extends Shape {void draw() {}}

class Rectangle extends Shape {void draw() {}}

public class Test {
 /*
   * Example for an upper bound wildcard (Get values i.e Producer `extends`)
   * 
   * */  

    public void testCoVariance(List<? extends Shape> list) {
        list.add(new Shape()); // Error:  is not applicable for the arguments (Shape) i.e. inheritance is not supporting
        list.add(new Circle()); // Error:  is not applicable for the arguments (Circle) i.e. inheritance is not supporting
        list.add(new Square()); // Error:  is not applicable for the arguments (Square) i.e. inheritance is not supporting
        list.add(new Rectangle()); // Error:  is not applicable for the arguments (Rectangle) i.e. inheritance is not supporting
        Shape shape= list.get(0);//compiles so list act as produces only

        /*You can't add a Shape,Circle,Square,Rectangle to a List<? extends Shape> 
         * You can get an object and know that it will be an Shape
         */         
    }
      /* 
* Example for  a lower bound wildcard (Put values i.e Consumer`super`)
* */
    public void testContraVariance(List<? super Shape> list) {
        list.add(new Shape());//compiles i.e. inheritance is supporting
        list.add(new Circle());//compiles i.e. inheritance is  supporting
        list.add(new Square());//compiles i.e. inheritance is supporting
        list.add(new Rectangle());//compiles i.e. inheritance is supporting
        Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer
        Object object= list.get(0); // gets an object, but we don't know what kind of Object it is.

        /*You can add a Shape,Circle,Square,Rectangle to a List<? super Shape> 
        * You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
        */  
    }
}

泛型示例


嘿,我只是想知道您对最后一句话的意思:“如果您认为我的比喻是错误的,请更新”。您是说这是道德上的错误(主观上)还是在编程环境中是错误的(客观上:不,这没有错)?我想用一个更中立的例子代替它,它不受文化规范和道德信仰的影响,是普遍公认的;如果可以的话。
Neuron

最后我可以得到它。很好的解释。
奥列格·库茨

2
@Premraj,In-variance/Non-variance: ? or ? extends Object - Unbounded Wildcard. It stands for the family of all types. Use when you both get and put.我无法将元素添加到List <?>或List <?扩展Object>,所以我不明白为什么会这样Use when you both get and put
刘文斌

1
@LiuWenbin_NO。-那部分答案是误导的。?-“无界通配符”-与不变性完全相反。请参考以下文档:docs.oracle.com/javase/tutorial/java/generics/…其中指出:如果代码需要同时以“ in”和“ out”变量的形式访问变量,请执行不要使用通配符。(他们使用“输入”和“输出”作为“获取”和“输入”的同义词)。除了,null您无法将新增至参数化为的Collection ?
mouselabs

29
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
    }
}

因此,“?extends B”应解释为“?Bextends”。B进行了扩展,以包括B到对象的所有超类,但B本身除外。感谢您的代码!
索拉·帕蒂(Saurabh Patil)

3
@SaurabhPatil没有,? extends B方法B和扩展B.任何东西
ASGS

24

正如我在解释我的回答另一个问题时,佩奇是乔希布洛赫为了帮助助记符设备记住P roducer extendsÇ 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作通用返回类型的type参数。


1
此原则仅适用于收藏集吗?当人们尝试将其与列表相关时,这是有意义的。如果您考虑sort(List <T>,Comparator <?super T>)--->的签名,则在这里Comparator使用super,这意味着它是PECS上下文中的使用者。例如,当您查看实现时:public int compare(Person a,Person b){return a.age <b.age?-1:a.age == b.age?0:1; }我觉得人不消耗任何东西,而是会产生年龄。这让我感到困惑。我的推理是否有缺陷,或者PECS仅适用于Collections?
法提赫·阿尔斯兰

23

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

  1. <? extends T>如果您需要T从集合中检索类型的对象,请使用通配符。
  2. <? super T>如果需要放置类型的对象,请使用通配符T
  3. 如果您需要同时满足这两个条件,则不要使用任何通配符。就如此容易。

10

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

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 ,因此无法在其中添加任何类型为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,...编译器可以编译您的赋值语句(看似正确),但是在运行时,s(动物)的类型可以更低。层次结构而不是列表的声明类型(可以为Creature或更高)。这是不允许的。

总结一下

我们<? super T>通常将类型等于或小于此类型的对象添加TList我们无法阅读。
我们用来从列表中<? extends T>读取等于或低于类型的对象T我们无法在其中添加元素。


9

(添加答案,因为没有足够的带有泛型通配符的示例)

       // 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);
        }
    }

4

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

  • extends用于阅读

  • super用于写作

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

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


我遇到了同样的心理冲突,并且倾向于同意,除了PECS不指定代码的命名并且类型边界本身在Collection声明设置的。此外,就命名而言,您经常会有用于产生/使用Collection的名称,例如srcdst。因此,您同时处理代码和容器,而我最终沿着这些思路进行了思考-“消费代码”从生产容器中消费,而“生产代码”为消费容器生产。
mouselabs

4

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?通配符(未知类型)是对象(分配的“右侧”),并<? extends T>确保无论??“ IS-A” T- 可以将其分配T,因为它?是的子类型(或至少相同的类型)T


2

记住这一点:

消费者吃晚饭(超级);制片人扩大了父母的工厂


1

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

  1. 想象一下,一列货运火车与货车类似。
  2. 如果货物的尺寸等于或小于货车的大小,则可以货物放入货车中=<? super FreightCarSize>
  3. 您可以卸载从货车货物,如果你有足够的空间在你的仓库(超过货物的大小)=<? extends DepotSize>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.