840

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

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

4

15 回答 15

961

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


假设你有一个方法,它的参数是一个东西的集合,但你希望它比仅仅接受一个更灵活Collection<Thing>

案例 1:您想浏览集合并对每个项目执行操作。
那么列表是一个生产者,所以你应该使用一个Collection<? extends Thing>.

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

案例 2:您想将东西添加到集合中。
那么列表是一个消费者,所以你应该使用一个Collection<? super Thing>.

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

于 2010-04-27T17:37:26.150 回答
626

在计算机科学中,这背后的原理被称为

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

下图应该解释这个概念。图片提供:安德烈·秋金

协方差与逆变

于 2013-11-02T06:34:38.217 回答
77

PECS(生产者extends和消费者super

助记符 → Get t (ex t end) 和 P u t (S u per) 原则。

  • 原则指出:

    • extends仅从结构中获取值时使用通配符。
    • super仅将值放入结构时使用通配符。
    • 在 get 和 put 时不要使用通配符。

Java 中的示例:

class Super {
        Number testCoVariance() {
            return null;
        }
        void testContraVariance(Number parameter) {
        } 
    }
    
    class Sub extends Super {
        @Override
        Integer testCoVariance() {
            return null;
        } //compiles successfully i.e. return type is don't care(Integer is subtype of Number)
        @Override
        void testContraVariance(Integer parameter) {
        } //doesn't support even though Integer is subtype of Number
    }

Liskov 替换原则 (LSP) 指出“<strong>程序中的对象应该可以用其子类型的实例替换,而不会改变该程序的正确性”。

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

  • 协变,如果它保留类型的顺序 (≤),它将类型从更具体到更通用的顺序排列;
  • 如果它颠倒了这个顺序,它是逆变的;
  • 如果这些都不适用,则为不变或不变。

协变和逆变

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

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

  • 协变:一个 Cat[] 是一个 Animal[];
  • 逆变: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 种不同风格的通配符:

  • In-variance/Non-variance:?? extends Object-无界通配符。它代表所有类型的家庭。在获取和放置时使用。
  • 协方差:(后代? extends T统治T)- 具有上限的通配符。T是继承层次结构中最上层的类。当您只从结构中获取值时使用extends通配符。
  • 反方差:(祖先? super T统治T)- 具有下限的通配符。T是继承层次结构中最底层的类。仅将值放入结构super时使用通配符。

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

在此处输入图像描述

 import java.util.ArrayList;
import java.util.List;

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 {

    public static void main(String[] args) {
        //? extends Shape i.e. can use any sub type of Shape, here Shape is Upper Bound in inheritance hierarchy
        List<? extends Shape> intList5 = new ArrayList<Shape>();
        List<? extends Shape> intList6 = new ArrayList<Cricle>();
        List<? extends Shape> intList7 = new ArrayList<Rectangle>();
        List<? extends Shape> intList9 = new ArrayList<Object>();//ERROR.


        //? super Shape i.e. can use any super type of Shape, here Shape is Lower Bound in inheritance hierarchy
        List<? super Shape> inList5 = new ArrayList<Shape>();
        List<? super Shape> inList6 = new ArrayList<Object>();
        List<? super Shape> inList7 = new ArrayList<Circle>(); //ERROR.

        //-----------------------------------------------------------
        Circle circle = new Circle();
        Shape shape = circle; // OK. Circle IS-A Shape

        List<Circle> circles = new ArrayList<>();
        List<Shape> shapes = circles; // ERROR. List<Circle> is not subtype of List<Shape> even when Circle IS-A Shape

        List<? extends Circle> circles2 = new ArrayList<>();
        List<? extends Shape> shapes2 = circles2; // OK. List<? extends Circle> is subtype of List<? extends Shape>


        //-----------------------------------------------------------
        Shape shape2 = new Shape();
        Circle circle2= (Circle) shape2; // OK. with type casting

        List<Shape> shapes3 = new ArrayList<>();
        List<Circle> circles3 = shapes3; //ERROR. List<Circle> is not subtype of  List<Shape> even Circle is subetype of Shape

        List<? super Shape> shapes4 = new ArrayList<>();
        List<? super Circle> circles4 = shapes4; //OK.
    }

    
    
    /*
     * Example for an upper bound wildcard (Get values i.e Producer `extends`)
     *
     * */
    public void testCoVariance(List<? extends Shape> list) {
        list.add(new Object());//ERROR
        list.add(new Shape()); //ERROR
        list.add(new Circle()); // ERROR
        list.add(new Square()); // ERROR
        list.add(new Rectangle()); // ERROR
        Shape shape= list.get(0);//OK 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 Object());//ERROR
        list.add(new Shape());//OK
        list.add(new Circle());//OK
        list.add(new Square());//OK
        list.add(new Rectangle());//OK
        Shape shape= list.get(0); // ERROR. Type mismatch, so list acts only as consumer
        Object object= list.get(0); //OK 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.
         */
    }
}

泛型示例

协变和逆变根据类型确定兼容性。在任何一种情况下,方差都是有向关系。协变可以翻译为“在同一方向上不同”或“不同”,而逆变意味着“在相反方向上不同”或“反对-不同”。协变和逆变类型并不相同,但它们之间存在相关性。这些名称暗示了相关性的方向。

https://stackoverflow.com/a/54576828/1697099
https://stackoverflow.com/a/64888058/1697099

  • 协方差:接受子类型(只读,即生产者)
  • 逆变:接受超类型(只写即消费者)
于 2015-07-26T06:33:49.120 回答
33
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
    }
}
于 2013-10-07T18:15:13.510 回答
30

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

  1. <? extends T>如果您需要T从集合中检索类型的对象,请使用通配符。
  2. <? super T>如果您需要将类型的对象放入集合中,请使用通配符T
  3. 如果您需要同时满足这两件事,那么请不要使用任何通配符。就如此容易。
于 2014-11-13T06:51:24.617 回答
25

正如我在对另一个问题的回答解释的那样,PECS 是 Josh Bloch 创建的一种助记设备,用于帮助记住P生产者extends、消费者。super

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

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

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

于 2010-04-27T17:32:52.383 回答
16

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

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 - Producer Extends:

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 - Consumer Super:

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

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

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

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

您可以添加上述类型的对象,因为 Shark(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

在运行时,列表的类型可以是 A 以上的任何类型:X、Y、Z、...层次结构比列表的声明类型(可以是 Creature 或更高)。这是不允许的。

总结一下

我们用于<? super T>将类型等于或低于的对象添加TList. 我们无法从中读取。
我们用来从列表<? extends T>中读取类型等于或低于的对象。T我们不能向它添加元素。

于 2018-09-23T17:42:34.220 回答
12

这是我认为extends vs. super最清晰、最简单的方法:

  • extends是为了阅读

  • super是为了写作

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

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

于 2020-01-22T20:57:02.613 回答
10

让我们尝试可视化这个概念。

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

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

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

在此处输入图像描述

Mammal 类扩展了 Animal 类(Animal 类是 Mammal 类的类)。

Cat/Dog 类扩展了 Mammal 类(Mammal 类是 Cat/Dog 类的类)。

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

在此处输入图像描述

你不能把一个大盒子装进一个小盒子。

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

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

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

那么究竟什么是PECS?

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

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

请记住这一点:

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

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

因此,我们需要从“生产者”那里读取(获取)一些东西,然后将其放入我们的“盒子”中。这意味着从生产者那里取出的任何盒子都不应该我们的“盒子”大。这就是“<strong>Producer Extends ”的原因。</p>

“延伸”是指一个更小的盒子(上面维恩图中的小圆圈)。生产者的盒子应该比我们自己的盒子小,因为我们要从生产者那里拿走那些盒子,然后把它们放进我们自己的盒子里。我们不能放任何比我们的盒子更大的东西!

此外,我们需要将自己的“盒子”写入(放入) “消费者”。这意味着消费者的盒子不应小于我们自己的盒子。这就是为什么“<strong>消费者超级。”</p>

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

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

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

在上面的例子中,我们想要srcdest. 所以这src是一个“生产者”,它的“盒子”应该比某种类型更小(更具体)T

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

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

如果有人读到这篇文章,我希望它能帮助您更好地理解“<strong>Producer Extends, C onsumer S uper </p>

快乐编码!:)

于 2020-11-18T06:21:30.850 回答
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);
        }
    }
于 2017-05-17T18:12:22.877 回答
7

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

于 2020-04-18T18:30:58.110 回答
4

协方差:接受子类型
逆变:接受超类型

协变类型是只读的,而逆变类型是只写的。

于 2019-02-07T15:33:37.840 回答
3

记住这一点:

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

于 2018-02-19T09:50:08.363 回答
2

[协变和逆变]

让我们看一个例子

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

泛型允许您以安全的方式动态地使用类型

//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> aka 下限 aka 逆变 aka 消费者保证它由 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);
于 2020-06-03T10:55:37.457 回答
1

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

  1. 想象一列载有货车的货运列车,类似于一个列表。
  2. 如果货物的尺寸与货车相同或更小,您可以货物放入货车=<? super FreightCarSize>
  3. 如果您的仓库中有足够的空间(超过货物的大小),您可以从货车上卸下货物 =<? extends DepotSize>
于 2018-11-20T13:47:41.107 回答