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

pea*_*kit 680 java generics super pecs bounded-wildcard

我在阅读泛型时遇到了PECS(制片extends人和消费者的super简称).

能否给我一个人解释如何使用佩奇之间解决困惑extendssuper

Mic*_*ers 792

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保证.

  • 我总是试图以这种方式思考它:允许_producer_产生更具体的东西,因此_extends_,_consumer_被允许接受更通用的东西,因此_super_. (130认同)
  • 记住生产者/消费者区别的另一种方法是考虑方法签名.如果你有一个方法`doSomethingWithList(List list)`,你就*消耗*列表,因此需要协方差/扩展(或不变的List).另一方面,如果你的方法是`List doSomethingProvidingList`,那么你*生成*List并且需要逆变/超级(或不变的List). (8认同)
  • @Raman,我想你只是把它弄糊涂了.在doSthWithList中(你可以拥有List <?super Thing>),因为你是一个消费者,你可以使用super(记住,CS).但是,它的列表<?扩展Thing> getList(),因为在生成(PE)时允许返回更具体的内容. (7认同)
  • @MichaelMyers:为什么我们不能简单地在这两种情况下使用参数化类型?这里使用通配符有什么特别的优势,或者它只是提高可读性的一种方法,比如说,使用对`const`的引用作为C++中的方法参数来表示该方法不会修改参数? (3认同)
  • @Chatterjee:使用通配符的原因是灵活性。通过声明参数的类型为Collection &lt;?super Thing&gt;为您提供了更大的灵活性,因为调用者不仅可以使用Collection &lt;Thing&gt;作为参数来调用您的方法,还可以使用Collection &lt;SomeSupertypeOfThing&gt;作为参数来调用您的方法。 (3认同)
  • @EricZhang:当您添加到集合时,集合是否可迭代无关紧要. (2认同)
  • PECS 是违反直觉的,如果我从清单中取出某些东西,那么我是在消费物品还是在生产物品? (2认同)
  • @AZ_我分享您的观点。如果方法从列表中获取get(),则该方法将被视为Consumer &lt;T&gt;,并且列表将被视为提供者;否则,列表将被视为提供者。但是PECS的规则是“从列表的角度来看”,因此需要“扩展”。应该是GEPS:扩展;放超级。 (2认同)

ano*_*ias 524

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

  • 协方差:? extends MyClass,
  • 逆变:? super MyClass
  • 不变性/非方差: MyClass

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

图片提供:Andrey Tyukin

协方差与逆差

  • 嘿大家.我是Andrey Tyukin,我只想确认anoopelias和DaoWen联系我并获得我的许可使用草图,它是在(CC)-BY-SA下获得许可的.Thx @ Anoop给了它第二次生命^^ @Brian Agnew :(关于"少数投票"):那是因为它是Scala的草图,它使用Scala语法并假设声明站点差异,这与Java奇怪的调用完全不同-site variance ...也许我应该写一个更详细的答案,清楚地说明这个草图如何应用于Java ...... (133认同)
  • 这是我所发现的关于协方差和相反方差的最简单,最清晰的解释之一! (2认同)
  • 我最后添加了[解释Scala关于协方差和逆变的概念,详细解释和可编码的代码](/sf/ask/3416857811/ -an-元件内式功能/ 48858344#48858344). (2认同)

Pre*_*raj 40

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

}
Run Code Online (Sandbox Code Playgroud)

Liskov替换原则: 数组是协变的(不安全的)但是泛型不是那么(不安全).即替换原则不适用于参数化类型,这意味着它是非法的.
协变只是意味着如果?是子类型? extends Object那么? extends T也将是子类型T.

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  
Run Code Online (Sandbox Code Playgroud)

更多例子

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

  • 方差/非方差:Textends- 无界通配符.它代表所有类型的家庭.当你得到和放置时使用.
  • 协方差:( ? super T所有类型的子类型 T) - 具有上限的通配符.T是继承层次结构中最上面的类.super仅在从结构中获取值时使用通配符.
  • Contra-variance :( ?所有类型的超类型族 T) - 具有下限的通配符.extends较低的继承层次结构中最末端的类.super仅在值放入结构时使用通配符.

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

在此输入图像描述

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.
        */  
    }
}
Run Code Online (Sandbox Code Playgroud)

泛型例子

  • @Premraj,`不变/非变:?或者 ?扩展对象 - 无界通配符。它代表所有类型的家庭。当您同时使用 get 和 put.` 时,我无法将元素添加到 List&lt;?&gt; 或 List&lt;? extends Object&gt;,所以我不明白为什么它可以是“当你得到和放置时使用”。 (2认同)

Gab*_*ica 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
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @SaurabhPatil不,`?扩展B`表示B和任何扩展B的东西. (2认同)

Col*_*inD 23

正如我在解释我的回答另一个问题时,佩奇是乔希布洛赫为了帮助记住一个记忆装置P roducer extends,Ç onsumer super.

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

当一个参数化类型被传递给方法会消耗的情况下,T(他们将被传递给它做一些事情),? super T应该使用,因为实例T可以合法地传递给一个接受的某些超类型的任何方法T.例如,A Comparator<Number>可用于a Collection<Integer>.? extends T不行,因为一个Comparator<Integer>不能操作Collection<Number>.

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

  • 这个原则只适用于集合吗?当人们试图将其与列表关联起来时,这是有道理的。如果您考虑 sort(List&lt;T&gt;,Comparator&lt;? super T&gt;) 的签名 ---&gt; 这里 Comparator 使用 super,因此这意味着它是 PECS 上下文中的消费者。当您查看以下实现时,例如: public int Compare(Person a, Person b) { return a.age &lt; b.age ? -1:a.年龄==b.年龄?0:1;我觉得人不会消耗任何东西,只会产生年龄。这让我很困惑。我的推理是否存在缺陷,或者 PECS 只适用于集合? (2认同)
  • @FatihArslan 不研究比较器的实现。这无关紧要。方法“sort(List&lt;T&gt;,Comparator&lt;? super T&gt;)”声明类型边界,并且在该“sort”方法中,比较器*消耗*“T”实例。 (2认同)

Pra*_*hal 21

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

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


kaa*_*aan 10

这是我想到的最清晰,最简单的方法扩展与超级:

  • extends是为了阅读

  • super是为了写作

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

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


And*_*ejs 8

(添加答案因为从来没有足够的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);
        }
    }
Run Code Online (Sandbox Code Playgroud)


Dan*_*iel 8

我们假设这个层次结构:

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
Run Code Online (Sandbox Code Playgroud)

让我们澄清一下PE - Producer扩展:

List<? extends Shark> sharks = new ArrayList<>();
Run Code Online (Sandbox Code Playgroud)

为什么你不能在这个列表中添加扩展"Shark"的对象?喜欢:

sharks.add(new HammerShark());//will result in compilation error
Run Code Online (Sandbox Code Playgroud)

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

sharks.add(new HammerShark());
Run Code Online (Sandbox Code Playgroud)

...但是无法判断在运行时,您的B将是列表类型的子类型还是超类型.在运行时,列表类型可以是A,B,C类型中的任何一种.因此,您最终无法在DeadHammerShark列表中添加HammerSkark(超类型).

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

让我们澄清一下CS - Consumer Super:

在同一层次结构中,我们可以尝试这样:

List<? super Shark> sharks = new ArrayList<>();
Run Code Online (Sandbox Code Playgroud)

可以添加到此列表的内容和原因?

sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());
Run Code Online (Sandbox Code Playgroud)

你可以添加上述类型的对象,因为鲨鱼(A,B,C)下面的任何东西都将是鲨鱼(X,Y,Z)之上的任何东西的子类型.容易明白.

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

但为什么你不能从这个列表中读取?(我的意思是你可以从中获取一个元素,但你不能将它分配给Object o以外的任何东西):

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

Animal s;
s = sharks.get(2);//doen't work
Run Code Online (Sandbox Code Playgroud)

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

总结一下

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


mou*_*abs 6

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);
}
Run Code Online (Sandbox Code Playgroud)

在分配比喻而言示出这一点,consumer?通配符(未知类型)为基准-的分配的“左手侧” -和<? super T>保证,无论?是,T“IS-A” ?-这T可以被分配给它,因为?是与T.

对于producer相同的问题,它只是颠倒了:producer?通配符(未知类型)是指代- 分配的“右侧” - 并<? extends T>确保无论??“IS-A” T-都可以分配T, 因为?T.


sta*_*iet 5

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

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

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

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

在此处输入图片说明

哺乳动物类扩展动物类(动物类是一个超级Mammal 类类)。

猫/狗类扩展了哺乳动物类(哺乳动物类是一个超级Cat/Dog 类类)。

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

在此处输入图片说明

你不能把一个更大的盒子放进一个更小的盒子里。

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

当您说 时<? super SomeType>,您想描述一个与“SomeType”框大小相同或更大的“框”。

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

那么什么是PECS呢?

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

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

请记住这一点:

  • 我们从“制作人”那里“阅读”,然后把这些东西放进我们自己的盒子里。

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

因此,我们需要从“生产者”那里读取(获取)一些东西并将其放入我们的“盒子”中。这意味着,从生产者采取的任何箱应大于我们的“盒子”更大。这就是为什么“ P roducer ê xtends。”

“扩展”意味着一个较小的盒子(上面维恩图较小的圆圈)。生产商的盒子应该比我们自己的盒子小,因为我们将从生产商那里拿走这些盒子,然后把它们放进我们自己的盒子里。我们不能放任何比我们的盒子大的东西!

此外,我们需要将我们自己的“盒子”写入(放入)“消费者”中。这意味着消费者的盒子应该比我们自己的盒子小。这就是为什么“ Ç onsumer小号UPER。”

“超级”意味着更大的盒子(上面维恩图更大的圆圈)。如果我们要把自己的盒子放到一个消费者里面,消费者的盒子应该比我们的盒子大!

现在我们可以很容易地理解这个例子:

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)); 
  } 
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,我们想要读取(获取)一些东西src并将它们写入(放入)到dest. 所以这src是一个“生产者”,它的“盒子”应该比某种类型更小(更具体)T

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

如果 的“盒子”src比 的大dest,我们就不能把那些大盒子放进小盒子里dest

如果有人读这一点,我希望它可以帮助您更好地理解“ P roducer ê xtends,Ç onsumer小号UPER。”

快乐编码!:)