¿Qué es PECS (Producer Extender Consumer Super)?

729

Me encontré con PECS (abreviatura de Productor extendsy Consumidorsuper ) mientras leía sobre genéricos.

¿Alguien puede explicarme cómo usar PECS para resolver la confusión entre extendsy super?

pico
fuente
3
Una muy buena explicación con un ejemplo @ youtube.com/watch?v=34oiEq9nD0M&feature=youtu.be&t=1630 que explica superparte pero da una idea de otra.
lupchiazoem

Respuestas:

844

tl; dr: "PECS" es desde el punto de vista de la colección. Si solo está sacando artículos de una colección genérica, es un productor y debe usarlo extends; si solo está cargando artículos, es un consumidor y debe usarlo super. Si haces ambas cosas con la misma colección, no deberías usar ninguna extendso super.


Supongamos que tiene un método que toma como parámetro una colección de cosas, pero desea que sea más flexible que simplemente aceptar un Collection<Thing> .

Caso 1: desea revisar la colección y hacer cosas con cada elemento.
Entonces la lista es un productor , por lo que debe usar a Collection<? extends Thing>.

El razonamiento es que a Collection<? extends Thing>podría contener cualquier subtipo de Thingy, por lo tanto, cada elemento se comportará como Thingcuando realice su operación. (En realidad, no puede agregar nada a un Collection<? extends Thing>, porque no puede saber en tiempo de ejecución qué subtipo específico de Thingla colección contiene).

Caso 2: desea agregar cosas a la colección.
Entonces la lista es un consumidor , por lo que debe usar unCollection<? super Thing> .

El razonamiento aquí es que Collection<? extends Thing>, a diferencia , Collection<? super Thing>siempre puede contener un Thingno importa cuál sea el tipo parametrizado real. Aquí no le importa lo que ya está en la lista, siempre y cuando permita Thingque se agregue un; Esto es lo que ? super Thinggarantiza.

Michael Myers
fuente
142
Siempre trato de pensarlo de esta manera: un productor puede producir algo más específico, por lo tanto , se extiende , un consumidor puede aceptar algo más general, por lo tanto, súper .
Feuermurmel
10
Otra forma de recordar la distinción productor / consumidor es pensar en la firma de un método. Si tiene un método doSomethingWithList(List list), está consumiendo la lista y, por lo tanto, necesitará covarianza / extensión (o una Lista invariante). Por otro lado, si su método es List doSomethingProvidingList, entonces está produciendo la Lista y necesitará contravarianza / super (o una Lista invariante).
Raman
3
@MichaelMyers: ¿Por qué no podemos simplemente usar un tipo parametrizado para ambos casos? ¿Existe alguna ventaja específica de usar comodines aquí, o es solo un medio de mejorar la legibilidad similar a, por ejemplo, usar referencias a constcomo parámetros del método en C ++ para indicar que el método no modifica los argumentos?
Chatterjee
77
@Raman, creo que lo confundiste. En doSthWithList (puede tener List <? Super Thing>), ya que es un consumidor, puede usar super (recuerde, CS). Sin embargo, es List <? extiende Thing> getList () ya que puede devolver algo más específico al producir (PE).
masterxilo
44
@AZ_ Comparto tu sentimiento. Si un método obtiene () de la lista, el método se consideraría un Consumidor <T> y la lista se consideraría un proveedor; pero la regla de PECS es "desde el punto de vista de la lista", por lo que se requiere 'extender'. Debería ser GEPS: get extend; poner super.
Treefish Zhang
561

Los principios detrás de esto en informática se llaman

  • Covarianza: ? extends MyClass,
  • Contravarianza: ? super MyClass y
  • Invarianza / no varianza: MyClass

La siguiente imagen debería explicar el concepto. Imagen cortesía: Andrey Tyukin

Covarianza vs Contravarianza

anoopelias
fuente
144
Hola a todos. Soy Andrey Tyukin, solo quería confirmar que anoopelias y DaoWen se pusieron en contacto conmigo y obtuvieron mi permiso para usar el boceto, está licenciado bajo (CC) -BY-SA. Thx @ Anoop por darle una segunda vida ^^ @Brian Agnew: (en "pocos votos"): Eso es porque es un boceto para Scala, usa la sintaxis de Scala y asume una variación del sitio de declaración, que es bastante diferente a la extraña llamada de Java -varianza del sitio ... Tal vez debería escribir una respuesta más detallada que muestre claramente cómo se aplica este boceto a Java ...
Andrey Tyukin
3
¡Esta es una de las explicaciones más simples y claras para la covarianza y contravarianza que he encontrado!
cs4r
@Andrey Tyukin Hola, también quiero usar esta imagen. ¿Cómo puedo contactarte?
slouc
Si tiene alguna pregunta sobre esta ilustración, podemos analizarla en la sala de chat: chat.stackoverflow.com/rooms/145734/…
Andrey Tyukin
49

PECS (Productor extendsy Consumidor super)

mnemónico → Obtener y poner principio.

Este principio establece que:

  • Use un comodín extendido cuando solo obtenga valores de una estructura.
  • Use un súper comodín cuando solo ponga valores en una estructura.
  • Y no use un comodín cuando ambos se ponen y se ponen.

Ejemplo en 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

}

Principio de sustitución de Liskov: si S es un subtipo de T, entonces los objetos de tipo T pueden reemplazarse por objetos de tipo S.

Dentro del sistema de tipos de un lenguaje de programación, una regla de tipeo

  • covariante si conserva el orden de los tipos (≤), que ordena los tipos de más específicos a más genéricos;
  • contravariante si invierte este orden;
  • invariante o no variable si ninguno de estos aplica.

Covarianza y contravarianza

  • Los tipos de datos de solo lectura (orígenes) pueden ser covariantes ;
  • los tipos de datos de solo escritura (sumideros) pueden ser contravariantes .
  • Los tipos de datos mutables que actúan como fuentes y sumideros deben ser invariables .

Para ilustrar este fenómeno general, considere el tipo de matriz. Para el tipo Animal podemos hacer el tipo Animal []

  • covariante : un gato [] es un animal [];
  • contravariante : un animal [] es un gato [];
  • invariante : un animal [] no es un gato [] y un gato [] no es un animal [].

Ejemplos de 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  

más ejemplos

comodín delimitado (es decir, hacia un lugar) : hay 3 tipos diferentes de comodines:

  • En varianza / no varianza: ?o ? extends Object- Sin límites Comodín sin . Es sinónimo de la familia de todo tipo. Úselo cuando ambos consiguen y ponen.
  • Covarianza: ? extends T(la familia de todos los tipos que son subtipos de T): un comodín con un límite superior . Tes la clase más alta en la jerarquía de herencia. Use un extendscomodín cuando solo obtenga valores de una estructura.
  • Contra-varianza: ? super T(la familia de todos los tipos que son supertipos de T): un comodín con un límite inferior . Tes la clase más baja en la jerarquía de herencia. Use un supercomodín cuando solo ponga valores en una estructura.

Nota: comodín ?significa cero o una vez , representa un tipo desconocido. El comodín se puede usar como el tipo de un parámetro, nunca se usa como argumento de tipo para una invocación de método genérico, una creación de instancia de clase genérica (es decir, cuando se usa el comodín esa referencia que no se usa en otra parte del programa como la que usamos T)

ingrese la descripción de la imagen aquí

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.
        */  
    }
}

genéricos y ejemplos

Premraj
fuente
Oye, solo quería saber a qué te referías con la última frase: "Si crees que mi analogía es incorrecta, actualízala". ¿Quiere decir si es éticamente incorrecto (que es subjetivo) o si está equivocado en el contexto de la programación (que es objetivo: no, no está mal)? Me gustaría reemplazarlo con un ejemplo más neutral que sea universalmente aceptable independientemente de las normas culturales y las creencias éticas; Si eso está bien contigo.
Neurona
Por fin pude conseguirlo. Buena explicación
Oleg Kuts el
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.no puedo agregar elementos a List <?> O List <? extiende Object>, por lo que no entiendo por qué puede ser Use when you both get and put.
LiuWenbin_NO.
1
@LiuWenbin_NO. - Esa parte de la respuesta es engañosa. ?- el "comodín ilimitado" - se corresponde con el opuesto exacto de invariancia. Consulte la siguiente documentación: docs.oracle.com/javase/tutorial/java/generics/… que establece: En el caso en que el código necesite acceder a la variable como variable " entrante " y "saliente", haga No use un comodín. (Están usando "in" y "out" como sinónimo de "get" y "put"). Con la excepción de nullque no puede agregar a una Colección parametrizada con ?.
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
    }
}
Charla
fuente
Entonces "? Extiende B" debe interpretarse como "? Extiende B". Es algo que B extiende para incluir todas las superclases de B hasta Object, excluyendo B en sí. ¡Gracias por el código!
Saurabh Patil
3
@SaurabhPatil No, ? extends Bsignifica B y cualquier cosa que se extienda B.
pregunta el
24

Como explico en mi respuesta a otra pregunta, PECS es un dispositivo mnemónico creado por Josh Bloch para ayudar a recordar a P roducer extends, C consumidor super.

Esto significa que cuando un tipo parametrizado que se pasa a un método producirá instancias de T(se recuperarán de él de alguna manera), ? extends Tdebe usarse, ya que cualquier instancia de una subclase de Ttambién es a T.

Cuando un tipo parametrizado que se pasa a un método consumirá instancias de T(se pasará a él para hacer algo), ? super Tdebe usarse porque una instancia de Tpuede pasar legalmente a cualquier método que acepte algún supertipo de T. A Comparator<Number>podría usarse en un Collection<Integer>, por ejemplo. ? extends Tno funcionaría, porque a Comparator<Integer>no podría funcionar en a Collection<Number>.

Tenga en cuenta que, en general, solo debe usar ? extends Ty ? super Tpara los parámetros de algún método. Los métodos solo deben usarse Tcomo parámetro de tipo en un tipo de retorno genérico.

ColinD
fuente
1
¿Este principio solo se aplica a las colecciones? Tiene sentido cuando uno trata de correlacionarlo con una lista. Si piensa en la firma de clasificación (Lista <T>, Comparador <? Super T>) ---> aquí, el Comparador usa super, por lo que significa que es un consumidor en el contexto PECS. Cuando observa la implementación, por ejemplo, como: public int compare (Persona a, Persona b) {return a.age <b.age? -1: a.age == b.age? 0: 1; } Siento que la persona no consume nada sino que produce edad. Eso me confunde. ¿Hay algún defecto en mi razonamiento o PECS solo se aplica a las colecciones?
Fatih Arslan
24

En pocas palabras, tres reglas fáciles de recordar PECS:

  1. Use el <? extends T>comodín si necesita recuperar un objeto de tipo Tde una colección.
  2. Use el <? super T>comodín si necesita poner objetos de tipoT en una colección.
  3. Si necesita satisfacer ambas cosas, bueno, no use ningún comodín. Tan simple como eso.
Pradeep Kr Kaushal
fuente
10

asumamos esta jerarquía:

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

Aclaremos PE - El productor extiende:

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

¿Por qué no puede agregar objetos que extiendan "Tiburón" en esta lista? me gusta:

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

Dado que tiene una lista que puede ser de tipo A, B o C en tiempo de ejecución , no puede agregar ningún objeto de tipo A, B o C porque puede terminar con una combinación que no está permitida en Java.
En la práctica, el compilador puede ver en el momento de la compilación que agrega un B:

sharks.add(new HammerShark());

... pero no tiene forma de saber si en tiempo de ejecución, su B será un subtipo o supertipo del tipo de lista. En el tiempo de ejecución, el tipo de lista puede ser cualquiera de los tipos A, B, C. Por lo tanto, no puede terminar agregando HammerSkark (super tipo) en una lista de DeadHammerShark, por ejemplo.

* Dirás: "OK, pero ¿por qué no puedo agregar HammerSkark ya que es el tipo más pequeño?". Respuesta: es la más pequeña que conoces. Pero HammerSkark también puede ser extendido por otra persona y terminas en el mismo escenario.

Aclaremos CS - Consumer Super:

En la misma jerarquía podemos intentar esto:

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

¿Qué y por qué puedes agregar a esta lista?

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

Puede agregar los tipos de objetos anteriores porque cualquier cosa debajo de tiburón (A, B, C) siempre será subtipos de cualquier cosa por encima de tiburón (X, Y, Z). Fácil de comprender.

No puede agregar tipos por encima de Shark, porque en tiempo de ejecución el tipo de objeto agregado puede tener una jerarquía más alta que el tipo declarado de la lista (X, Y, Z). Esto no esta permitido.

¿Pero por qué no puedes leer de esta lista? (Quiero decir que puede obtener un elemento de él, pero no puede asignarlo a otra cosa que no sea el Objeto o):

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

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

En tiempo de ejecución, el tipo de lista puede ser de cualquier tipo por encima de A: X, Y, Z, ... El compilador puede compilar su declaración de asignación (que parece correcta) pero, en tiempo de ejecución, el tipo de s (Animal) puede ser menor en jerarquía que el tipo declarado de la lista (que podría ser Criatura o superior). Esto no esta permitido.

Para resumir

Usamos <? super T>para agregar objetos de tipos iguales o inferiores TalList . No podemos leerlo.
Usamos <? extends T>para leer objetos de tipos iguales o inferiores Tde la lista. No podemos agregarle elementos.

Daniel
fuente
9

(agregando una respuesta porque nunca hay suficientes ejemplos con comodines genéricos)

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

Esta es la forma más clara y sencilla para mí pensar en extendidos vs. super:

  • extendses para leer

  • superes para escribir

Considero que "PECS" es una forma no obvia de pensar sobre quién es el "productor" y quién es el "consumidor". "PECS" se define desde el punto de vista de la propia recopilación de datos - la colección "consume" si los objetos están siendo escritas a ella (que está consumiendo objetos de código de llamada), y que "produce" si los objetos están siendo leídos de él (ella está produciendo objetos para algún código de llamada). Sin embargo, esto es contrario a cómo se nombra todo lo demás. Las API Java estándar se nombran desde la perspectiva del código de llamada, no desde la colección en sí. Por ejemplo, una vista centrada en la colección de java.util.List debería tener un método llamado "recibir ()" en lugar de "agregar ()" - después de todo,el elemento, pero la lista mismarecibe el elemento

Creo que es más intuitivo, natural y coherente pensar en las cosas desde la perspectiva del código que interactúa con la colección: ¿el código "lee" o "escribe en" la colección? Después de eso, cualquier código escrito en la colección sería el "productor", y cualquier lectura de código de la colección sería el "consumidor".

kaan
fuente
Me he encontrado con esa misma colisión mental y tendería a estar de acuerdo, excepto que PECS no especifica el nombre del código y los límites de tipo en sí mismos se establecen en las declaraciones de la Colección. Además, en lo que respecta a la denominación, a menudo tiene nombres para producir / consumir colecciones como srcy dst. Entonces, se trata tanto de código como de contenedores al mismo tiempo y terminé pensando en eso: el "código consumidor" se consume de un contenedor productor y el "código productor" se produce para un contenedor consumidor.
mouselabs
4

La "regla" de PECS solo garantiza que lo siguiente sea legal:

  • Consumidor: sea lo que ?sea, legalmente puede referirse a T
  • Productor: sea lo que ?sea, legalmente puede ser referido por T

El emparejamiento típico en la línea de List<? extends T> producer, List<? super T> consumeres simplemente garantizar que el compilador pueda hacer cumplir las reglas de relación de herencia estándar "IS-A". Si pudiéramos hacerlo legalmente, podría ser más simple decirlo <T extends ?>, <? extends T>(o mejor aún en Scala, como puede ver arriba, es [-T], [+T]. Desafortunadamente, lo mejor que podemos hacer es <? super T>, <? extends T>.

Cuando encontré esto por primera vez y lo descompuse en mi cabeza, la mecánica tenía sentido, pero el código en sí seguía pareciéndome confuso: seguía pensando "parece que los límites no deberían invertirse así", aunque yo fue claro en lo anterior, que se trata simplemente de garantizar el cumplimiento de las normas estándar de referencia.

Lo que me ayudó fue mirarlo usando la asignación ordinaria como analogía.

Considere el siguiente código de juguete (no listo para producción):

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

Ilustrando esto en términos de la analogía de la asignación, para consumerel ?comodín (tipo desconocido) es la referencia, el "lado izquierdo" de la asignación, y <? super T>asegura que lo que sea ?, T"IS-A" ?, Tse le puede asignar, porque ?es un supertipo (o como mucho el mismo tipo) que T.

Porque producerla preocupación es la misma, solo se invierte: producerel ?comodín (tipo desconocido) es el referente , el "lado derecho" de la asignación, y <? extends T>garantiza que, sea lo que sea ?, ?"IS-A" T, se pueda asignar a unT , porque ?es un subtipo (o al menos el mismo tipo) que T.

mouselabs
fuente
2

Recuerda esto:

El consumidor come la cena (super); El productor extiende la fábrica de sus padres.

Jason
fuente
1

Usando el ejemplo de la vida real (con algunas simplificaciones):

  1. Imagine un tren de carga con vagones de carga como analogía de una lista.
  2. Puede poner una carga en un vagón de carga si la carga tiene el mismo tamaño o menor que el vagón de carga =<? super FreightCarSize>
  3. Puede descargar una carga de un vagón de carga si tiene suficiente lugar (más del tamaño de la carga) en su depósito =<? extends DepotSize>
contrapuesto
fuente
1

Covarianza : aceptar subtipos
Contravarianza : aceptar supertipos

Los tipos covariantes son de solo lectura, mientras que los tipos contravariantes son de solo escritura.

Farrukh Chishti
fuente
0

Veamos un ejemplo.

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

Generics le permite trabajar con Tipos dinámicamente de manera segura

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

Problema

Como la Colección de Java es un tipo de referencia como resultado, tenemos los siguientes problemas:

Problema # 1

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

* El genérico de Swift no tiene ese problema porque la Colección es Value type[Acerca de], por lo tanto, se crea una nueva colección

Problema # 2

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

La solución: comodines genéricos

El comodín es una característica de tipo de referencia y no se puede instanciar directamente

La solución # 1, <? super A> también conocida como límite inferior, también conocida como contravarianza, también conocida como consumidores, garantiza que sea operada por A y todas las superclases, por eso es seguro agregar

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

Solución n. ° 2

<? extends A>aka límite superior aka covarianza aka productores garantiza que es operado por A y todas las subclases, es por eso que es seguro obtener y lanzar

List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;

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

yoAlex5
fuente