java edición - ¿Qué es PECS (Producer Extends Consumer Super)?





manual de (10)


PECS (abreviatura de " Producer extends and Consumer super ") puede explicarse por: Principios de Get and Put

Principio de obtención y colocación (de Java Genéricos y colecciones)

Afirma,

  1. use un comodín extendido cuando solo obtiene valores de una estructura
  2. usa un super comodín cuando solo pones valores en una estructura
  3. y no use un comodín cuando ambos se ponen y se ponen .

Entendámoslo por ejemplo:

1. For Extends Wildcard (obtener valores, es decir, Producer se extends )

Aquí hay un método que toma una colección de números, convierte cada uno en un double y los resume.

public static double sum(Collection<? extends Number> nums) {
   double s = 0.0;
   for (Number num : nums) 
      s += num.doubleValue();
   return s;
}

Llamemos al método:

List<Integer>ints = Arrays.asList(1,2,3);
assert sum(ints) == 6.0;
List<Double>doubles = Arrays.asList(2.78,3.14);
assert sum(doubles) == 5.92;
List<Number>nums = Arrays.<Number>asList(1,2,2.78,3.14);
assert sum(nums) == 8.92;

Dado que, el uso del método sum() se extends , todas las siguientes llamadas son legales. Las primeras dos llamadas no serían legales si no se utilizara la extensión.

EXCEPCIÓN : no puede poner nada en un tipo declarado con un comodín de extends , excepto el valor null , que pertenece a cada tipo de referencia:

List<Integer> ints = new ArrayList<Integer>();
ints.add(1);
ints.add(2);
List<? extends Number> nums = ints;
nums.add(null);  // ok
assert nums.toString().equals("[1, 2, null]");

2. Para Super comodín (poner valores, es decir, super consumidor)

Aquí hay un método que toma una colección de números y una int n , y coloca los primeros n enteros, comenzando desde cero, en la colección:

public static void count(Collection<? super Integer> ints, int n) {
    for (int i = 0; i < n; i++) ints.add(i);
}

Llamemos al método:

List<Integer>ints = new ArrayList<Integer>();
count(ints, 5);
assert ints.toString().equals("[0, 1, 2, 3, 4]");
List<Number>nums = new ArrayList<Number>();
count(nums, 5); nums.add(5.0);
assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]");
List<Object>objs = new ArrayList<Object>();
count(objs, 5); objs.add("five");
assert objs.toString().equals("[0, 1, 2, 3, 4, five]");

Como el método count() usa super , todas las siguientes llamadas son legales: las dos últimas llamadas no serían legales si no se usara super.

EXCEPCIÓN : no puede sacar nada de un tipo declarado con un super comodín, a excepción de un valor de tipo Object , que es un supertipo de cada tipo de referencia:

List<Object> objs = Arrays.<Object>asList(1,"two");
List<? super Integer> ints = objs;
String str = "";
for (Object obj : ints) str += obj.toString();
assert str.equals("1two");

3. Cuando tanto Get como Put, no use comodines

Siempre que ambos pongan valores y obtengan valores de la misma estructura, no deben usar un comodín .

public static double sumCount(Collection<Number> nums, int n) {
   count(nums, n);
   return sum(nums);
}

Me encontré con PECS (abreviatura de Producer extends y Consumer super ) mientras leía sobre genéricos.

¿Puede alguien explicarme cómo usar PECS para resolver la confusión entre extends y super ?




Los principios detrás de esto en Ciencias de la Computación llevan el nombre de

  • Covarianza -? extiende MyClass,
  • Contravarianza -? super MyClass y
  • Invarianza / No-Varianza - MyClass

La imagen de abajo debe explicar el concepto.

Imagen cortesía de Andrey Tyukin.




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

Vamos a aclarar PE - Productor Extiende:

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

¿Por qué no puedes 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 en ella porque puede terminar con una combinación que no está permitida en Java.
En la práctica, el compilador puede ver en tiempo de compilación que agrega una B:

sharks.add(new HammerShark());

... pero no tiene forma de saber si en el 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 (tipo super) en una lista de DeadHammerShark, por ejemplo.

* Dirás: "Está bien, pero ¿por qué no puedo agregar HammerSkark ya que es el tipo más pequeño?". Respuesta: Es el más pequeño que conozcas. Comprar 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 probar 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 del tiburón (A, B, C) siempre será subtipos de cualquier cosa sobre el tiburón (X, Y, Z). Fácil de entender.

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 puedes sacar un elemento de él, pero no puedes 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 cualquier tipo por encima de A: X, Y, Z, ... El compilador puede compilar su declaración de asignación (lo que parece correcto) pero, en tiempo de ejecución, el tipo de s (Animal) puede ser inferior en jerarquía que el tipo declarado de la lista (que podría ser Criatura o superior). Esto no esta permitido.

Para resumir

Utilizamos <? super T> <? super T> para agregar objetos de tipos iguales o por debajo de T en la lista. No podemos leer de ello.
Utilizamos <? extends T> <? extends T> para leer objetos de tipos iguales o por debajo de T de la lista. No podemos añadirle elemento.




Los comodines se pueden utilizar de tres maneras:

              - Upper bound Wildcard  ( ? extends Type ).

              - Lower bound Wildcard  ( ? super Type ) .

              - Unbounded Wildcard    ( ? ) .

Para los fines de esta discusión, es útil pensar que las variables proporcionan una de dos funciones:

                      - In Variable

                              An "in" variable serves up data to the code. 
                              Imagine a copy method with two arguments: 
                                      copy(src, dest)
                              The src argument provides the data to be copied, so it is the "in" parameter.
                      - Out Variable

                              An "out" variable holds data for use elsewhere. In the copy example, 
                                      copy(src, dest)
                              the dest argument accepts data, so it is the "out" parameter.

              An "in" variable is defined with an upper bounded wildcard, using the extends keyword.
              An "out" variable is defined with a lower bounded wildcard, using the super keyword.
              In the case where the "in" variable can be accessed using methods defined in the Object class, use an unbounded wildcard.
              In the case where the code needs to access the variable as both an "in" and an "out" variable, do not use a wildcard.

                      class NaturalNumber 
                      {

                              private int i;

                              public NaturalNumber(int i)
                              { 
                                      this.i = i;
                              }
                      }

                          class EvenNumber extends NaturalNumber 
                          {

                              public EvenNumber(int i) 
                              { 
                                      super(i);
                              }
                          }

              Consider the following code:

                      List<EvenNumber> le = new ArrayList<>();
                      List<? extends NaturalNumber> ln = le;
                      ln.add(new NaturalNumber(35));  // compile-time error


                      You can add null.
                      You can invoke clear.
                      You can get the iterator and invoke remove.
                      You can capture the wildcard and write elements that you've read from the list.



Como explico en mi respuesta a otra pregunta, PECS es un dispositivo mnemotécnico creado por Josh Bloch para ayudar a recordar las tendencias de productos e xtends , más allá s uper .

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 T Se debe usar la ? extends T , ya que cualquier instancia de una subclase de T es también una T

Cuando un tipo parametrizado que se pasa a un método consumirá instancias de T (se le pasará para que haga algo) ? super T ? super T debería usar ? super T porque una instancia de T se puede pasar legalmente a cualquier método que acepte algún supertipo de T Un Comparator<Number> podría usarse en una Collection<Integer> , por ejemplo. ? extends T ? extends T no funcionaría, porque un Comparator<Integer> no podría operar en una Collection<Number> .

Tenga en cuenta que generalmente solo debe usar ? extends T ? extends T y ? super T ? super T para los parámetros de algún método. Los métodos deberían usar T como el parámetro de tipo en un tipo de retorno genérico.




En pocas palabras fácil de recordar PECS

  1. Usa el <? extends T> <? extends T> comodín si necesita recuperar un objeto de tipo T de una colección.
  2. Usa el <? super T> <? super T> comodín si necesita colocar objetos de tipo T en una colección.
  3. Si necesita satisfacer ambas cosas, bueno, no use ningún comodín. Tan simple como es.



(Añadiendo 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);
        }
    }



tl; dr: "PECS" es desde el punto de vista de la colección. Si solo está extrayendo artículos de una colección genérica, es un productor y debe usar extends ; Si solo está rellenando artículos, es un consumidor y debe usar super . Si haces ambas cosas con la misma colección, no deberías usar extends o 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 solo aceptar una Collection<Thing> .

Caso 1: Quieres ir a través de la colección y hacer cosas con cada elemento.
Entonces la lista es un productor , por lo que deberías usar una Collection<? extends Thing> Collection<? extends Thing> .

El razonamiento es que una Collection<? extends Thing> Collection<? extends Thing> podría contener cualquier subtipo de Thing , y así cada elemento se comportará como una Thing cuando realice su operación. (En realidad, no puede agregar nada a una Collection<? extends Thing> , porque no puede saber en tiempo de ejecución qué subtipo específico de Thing la colección).

Caso 2: Quieres añadir cosas a la colección.
Entonces la lista es un consumidor , por lo que debería usar una Collection<? super Thing> Collection<? super Thing> .

El razonamiento aquí es que a diferencia de Collection<? extends Thing> Collection<? extends Thing> , Collection<? super Thing> Collection<? super Thing> siempre puede mantener un Thing sin importar cuál sea el tipo real parametrizado. Aquí no le importa lo que ya está en la lista, siempre y cuando permita agregar una Thing ; esto es que ? super Thing ? super Thing garantiza.




PECS (Productor se extends y Consumo super )

mnemotécnica → principio de Get and Put.

Este principio establece que:

  • Utilice un comodín extendido cuando solo obtiene valores de una estructura.
  • Use un super comodín cuando solo ponga valores en una estructura.
  • Y no use un comodín cuando ambos se ponen y se ponen.

En Java, los parámetros y los parámetros de tipo genérico no admiten la herencia de la siguiente manera.

class Super {
    void testCoVariance(Object parameter){} // method Consumes the Object
    Object testContraVariance(){ return null;} //method Produces the Object
}

class Sub extends Super {
    @Override
    void testCoVariance(String parameter){} //doesn't support eventhough String is subtype of Object

    @Override
    String testContraVariance(){ return null;} //compiles successfully i.e. return type is don't care 
}

Principio de sustitución de Liskov: las matrices son covariantes (inseguras) pero los genéricos no son invariables (seguros). es decir, el principio de sustitución no funciona con los tipos parametrizados, lo que significa que es ilegal escribir.
Covariante simplemente significa que si X es un subtipo de Y entonces X[] también será un subtipo de Y[] .

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 acotado (es decir, en dirección a algún lugar): hay 3 sabores diferentes de comodines:

  • En varianza / No varianza ? o ? extends Object ? extends Object - comodín sin límite. Es sinónimo de la familia de todo tipo. Úsalo cuando ambos te pongas y pongas.
  • Covarianza ? extends T ? extends T (la familia de todos los tipos que son subtipos de T ) - un comodín con un límite superior . T es la clase más alta en la jerarquía de herencia. Use un comodín extends cuando solo obtenga valores de una estructura.
  • Contra-varianza ? super T ? super T (la familia de todos los tipos que son supertipos de T ): un comodín con un límite inferior . T es la clase más baja en la jerarquía de herencia. Use un super comodí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 un 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 un comodín que no se usa en ningún otro lugar en el programa como usamos 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 TestContraVariance {
 /*
   * 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<? extends Shape> 
        * You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
        */  
    }
}

generics y examples




La pregunta original ha pedido "por qué es importante" y "ejemplo" donde esta Serial Version ID sería útil. Bueno, he encontrado uno.

Supongamos que creas una clase de Car , la creas y la escribes en una secuencia de objetos. El objeto de coche aplanado permanece en el sistema de archivos durante algún tiempo. Mientras tanto, si la clase Car se modifica agregando un nuevo campo. Más adelante, cuando intenta leer (es decir, deserializar) el objeto Car aplanado, obtiene la java.io.InvalidClassException , ya que todas las clases serializables reciben automáticamente un identificador único. Esta excepción se produce cuando el identificador de la clase no es igual al identificador del objeto aplanado. Si realmente lo piensas, la excepción se produce debido a la adición del nuevo campo. Puede evitar que se produzca esta excepción controlando el control de versiones usted mismo declarando un serialVersionUID explícito. También hay un pequeño beneficio en el rendimiento al declarar explícitamente su serialVersionUID (porque no tiene que ser calculado). Por lo tanto, es una práctica recomendada agregar su propio serialVersionUID a sus clases de Serializable tan pronto como las cree como se muestra a continuación:

public class Car {
static final long serialVersionUID = 1L; //assign a long value
}




java generics super bounded-wildcard pecs