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

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

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

150070 次浏览

正如我在我的答案中对另一个问题所解释的那样,PECS是Josh Bloch创建的一种助记符设备,用于帮助记住PCsuper

这意味着当传递给方法的参数化类型将生产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作为泛型返回类型的类型参数。

tl; dr:“PECS”是从集合的角度来看的。如果你是只有从一个通用集合中提取项目,它是一个生产者,你应该使用extends;如果你是只有填充项目,它是一个消费者,你应该使用super。如果你用同一个集合做这两件事,你不应该使用extendssuper


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

案例1:您想遍历集合并对每个项目进行处理。
然后列表是制片人,所以你应该使用Collection<? extends Thing>

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

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

这里的理由是,与Collection<? extends Thing>不同,无论实际参数化类型是什么,Collection<? super Thing>总是可以容纳Thing。这里你不在乎列表中已经有什么,只要它允许添加Thing;这是? super Thing保证的。

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 compilemyBlist.add(c); // does not compileA 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 MyClass
  • 反差:? super MyClass
  • 不变性/不变性:MyClass

下面的图片应该解释这个概念。图片提供:AndreyTyukin

协方差vs逆差

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

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

在处理集合时,在上限或下限通配符之间进行选择的常见规则是PECS。信贷

PECS(生产者extends和消费者super

助记符→Get(extend)和Put(Super)原理。

  • 原则表示:

    • 当您仅从结构中获取值时,请使用extends通配符。
    • 当您仅将值放入结构时,请使用super通配符。
    • 不要使用通配符,当你们都得到和放。

例如Java:

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

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

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

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

协方差和逆方差

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

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

  • 协变:猫[]是一种动物[];
  • 逆变:动物[]是猫[];
  • 不变量:动物[]不是猫[],猫[]也不是动物[]。

Java例子:

Object name= new String("prem"); //worksList<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

更多的例子

输入图片描述图片src

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

  • 方差/非方差:?? extends Object-无界通配符。它代表所有类型的家族。当您同时获取和放置时使用。
  • 协方差:? extends TT后代的统治)-具有上限的通配符。T是继承层次结构中上层最多的类。当结构中只有知道了个值时,请使用extends通配符。
  • 魂斗罗-Variance:? super TT祖先的统治)-具有下限的通配符。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 hierarchyList<? 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 hierarchyList<? 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());//ERRORlist.add(new Shape()); //ERRORlist.add(new Circle()); // ERRORlist.add(new Square()); // ERRORlist.add(new Rectangle()); // ERRORShape 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());//ERRORlist.add(new Shape());//OKlist.add(new Circle());//OKlist.add(new Square());//OKlist.add(new Rectangle());//OKShape shape= list.get(0); // ERROR. Type mismatch, so list acts only as consumerObject 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

  • 协方差:接受子类型(只读,即生产者)
  • 逆差:接受超类型(只写,即消费者)

(添加一个答案,因为使用泛型通配符的示例永远不够)

       // SourceList<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);
// DestinationList<Integer> intList2 = new ArrayList<>();List<Double> doublesList2 = new ArrayList<>();List<Number> numList2 = new ArrayList<>();
// WorkscopyElements1(intList,intList2);         // from int to intcopyElements1(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 supertypecopyElements1(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);}}

记住这个:

消费者吃晚餐(超级);生产者延伸他父母的工厂

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

class Creature{}// Xclass Animal extends Creature{}// Yclass Fish extends Animal{}// Zclass Shark extends Fish{}// Aclass HammerSkark extends Shark{}// Bclass DeadHammerShark extends HammerSkark{}// C

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

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

为什么不能在此列表中添加扩展“Shark”的对象?例如:

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

你可以添加上述类型的对象,因为任何低于鲨鱼(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、…编译器可以编译您的赋值语句(这似乎是正确的),但是,在运行时 s(动物)的类型在层次结构中可能低于列表的声明类型(可能是生物,或更高)。这是不允许的。

总结一下

我们使用#0将类型等于或低于#1的对象添加到#2。无法读取它。
我们使用<? extends T>从列表中读取类型等于或低于T的对象。我们不能向其添加元素。

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

  1. 想象一列有货车的货运列车,作为一个列表的类比。
  2. 你可以货物在货车如果货物有相同或更小的尺寸比货车=<? super FreightCarSize>
  3. 你可以卸载货物从货车如果你有足够的地方(超过货物的大小)在你的仓库=<? extends DepotSize>

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

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

这是最清晰,最简单的方式,我认为扩展与超级:

  • extends表示阅读

  • super表示写作

“PECS”是从数据采集本身的角度定义的——如果正在编写对象,集合将“消耗”它(它正在从调用代码中消耗对象),如果正在读取对象,集合将“生产”它(它正在为某个调用代码生产对象)。这与其他所有东西的命名方式相反。标准JavaAPI是从调用代码的角度命名的,而不是集合本身。例如,java.util.列表的以集合为中心的视图应该有一个名为“接收()”而不是“添加()”的方法——毕竟,调用代码增加是元素,但列表本身接收是元素。

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

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?通配符(未知类型)是producer0-赋值的“右手边”-<? extends T>确保无论?是什么,?“IS-A”T-producer1可以被赋值producer2,因为?是一个子类型(或至少与T相同的类型)。

PECS(生产者扩展和消费者超级)

[协方差和逆方差]

让我们来看看例子

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

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

//ListAList<A> listA = new ArrayList<A>();
//addlistA.add(new A());listA.add(new B());listA.add(new C());
//getA a0 = listA.get(0);A a1 = listA.get(1);A a2 = listA.get(2);
//ListBList<B> listB = new ArrayList<B>();
//addlistB.add(new B());
//getB b0 = listB.get(0);

问题

由于Java's Collection是引用类型,因此我们有下一个问题:

问题#1

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

*Swift的泛型没有这样的问题,因为Collection是Value type关于我们,因此创建了一个新集合

问题#2

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

解决方案-通用通配符

通配符是引用类型特性,不能直接实例化

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

List<? super A> listSuperA;listSuperA = listA;listSuperA = new ArrayList<Object>();
//addlistSuperA.add(new A());listSuperA.add(new B());
//getObject o0 = listSuperA.get(0);

解决方案#2

<? extends A>又名上界又名协方差又名生产者保证它由A和所有子类操作,这就是为什么收到和转换是安全的

List<? extends A> listExtendsA;listExtendsA = listA;listExtendsA = listB;
//getA a0 = listExtendsA.get(0);

让我们试着想象一下这个概念。

<? super SomeType>是一个“未定义(尚未)”的类型,但这种未定义的类型应该是一个超级class的“某些类型”类。

<? extends SomeType>也是如此。它是一个类型,应扩大是“某些类型”类(它应该是“某些类型”类的子类)。

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

在此处输入图片描述

哺乳动物类延伸动物类(动物类是哺乳动物类的超级类)。

猫/狗类延伸哺乳动物类(哺乳动物类是猫/狗类的超级类)。

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

在此处输入图片描述

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

你只能把一个小盒子放进一个大盒子里。

当你说<? super SomeType>时,你想描述一个与“某个类型”盒子大小相同或更大的“盒子”。

如果你说<? extends SomeType>,那么你要描述的是一个“盒子”,它的大小或较小与“某个类型”的盒子相同。

什么是PECS?

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

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

只要记住这一点:

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

  • 我们把自己的盒子“写”成“消费者”。

所以,我们需要从“生产者”把它放进我们的“盒子”里。中读取(获取)一些东西,这意味着从生产者那里获取的任何盒子都应该比我们的“盒子”大。这就是为什么“Produce erEx”。

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

另外,我们需要把我们自己的“盒子”变成写成一个“消费者”。这意味着消费者的盒子应该比我们自己的盒子小。这就是为什么“Consum erSuper”。

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

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

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的小盒子里。

如果有人读到这篇文章,我希望它能帮助你更好地理解

编码愉快!:)

PECS:生产者延伸和消费者超级

理解的先决条件:

  • 泛型和泛型通配符
  • 多态性、子类型和超类型

假设我们有一个接受泛型类型参数T的类型,例如List<T>。当我们编写代码时,还允许泛型类型参数T的子类型或超类型可能是有益的。这放松了API用户的限制,并可以使代码更加灵活。

让我们先看看我们得到了什么放松这些限制。假设我们有以下3个类:

class BaseAnimal{};
class Animal extends BaseAnimal{};
class Duck extends Animal{};

我们正在构建一个公共方法,它接受list<Animal>

  1. 如果我们使用超级List<? super Animal>而不是List<Animal>,我们现在可以传入更多的列表来满足我们方法的要求。我们现在可以传入List<Animal>List<BaseAnimal>甚至List<Object>
  2. 如果我们使用扩展List<? extends Animal>而不是List<Animal>,我们现在可以传入更多列表来满足我们方法的要求。我们现在可以传入List<Animal>List<Duck>

然而,这造成了以下2个限制:

  1. 如果我们使用像List<? super Animal>这样的超级类型,我们不知道List<T>的确切类型。它可能是List<Animal>List<BaseAnimal>List<Object>的列表。我们无法知道。这意味着我们永远无法从这个列表中获取值,因为我们不知道该类型是什么。然而,我们可以放入任何Animal的数据类型或将其扩展到List。因为我们只能将数据放入List,所以它被称为数据消费者
  2. 如果我们使用扩展List<? extends Animal>而不是List<Animal>。我们也不知道确切的类型是什么。它可以是List<Animal>List<Duck>。我们现在不能在List中添加一些东西,因为我们永远无法确定类型是什么。但是我们可以拉出一些东西,因为我们总是知道从列表中出来的任何东西都是Animal的子类型。因为我们只能从List中拉出数据,所以它被称为数据的制片人

下面是一个简单的程序来说明类型限制的放松:

import java.util.ArrayList;import java.util.List;
public class Generics {public static void main(String[] args) {
Generics generics = new Generics();
generics.producerExtends(new ArrayList<Duck>());generics.producerExtends(new ArrayList<Animal>());
generics.consumerSuper(new ArrayList<Object>());generics.consumerSuper(new ArrayList<Animal>());
}
//  ? extends T   is an upper boundpublic void producerExtends (List<? extends Animal> list) {
// Following are illegal since we never know exactly what type the list will be// list.add(new Duck());// list.add(new Animal());        
// We can read from it since we are always getting an Animal or subclass from it// However we can read them as an animal type, so this compiles fineif (list.size() > 0) {Animal animal = list.get(0);}}
// ? extends T   is a lower boundpublic void consumerSuper (List<? super Animal> list) {// It will be either a list of Animal or a superclass of it// Therefore we can add any type which extends animalslist.add(new Duck());list.add(new Animal());
// Compiler won't allow this it could potentially be a super type of Animal// Animal animal = list.get(0);}