¿Cuál es la diferencia entre <? super E> y <? extiende E>?


147

¿Cuál es la diferencia entre <? super E>y <? extends E>?

Por ejemplo, cuando echas un vistazo a la clase, java.util.concurrent.LinkedBlockingQueuehay la siguiente firma para el constructor:

public LinkedBlockingQueue(Collection<? extends E> c)

y para uno por el método:

public int drainTo(Collection<? super E> c)

Respuestas:


182

El primero dice que es "algún tipo que es un antepasado de E"; el segundo dice que es "algún tipo que es una subclase de E". (En ambos casos, E está bien).

Por lo tanto, el constructor usa el ? extends Eformulario para garantizar que cuando obtenga valores de la colección, todos serán E o alguna subclase (es decir, es compatible). El drainTométodo está tratando de poner valores en la colección, por lo que la colección debe tener un tipo de elemento E o una superclase .

Como ejemplo, suponga que tiene una jerarquía de clases como esta:

Parent extends Object
Child extends Parent

y a LinkedBlockingQueue<Parent>. Puede construir este paso en un List<Child>que copiará todos los elementos de forma segura, porque cada uno Childes un padre. No se pudo pasar List<Object>porque algunos elementos podrían no ser compatibles Parent.

Del mismo modo, puede drenar esa cola en un List<Object>porque cada Parentes un Object... pero no podría drenarlo en un List<Child>porque List<Child>espera que todos sus elementos sean compatibles Child.


25
+1. Esa es realmente la diferencia práctica. se extiende para buscar, super para insertar.
Yishai

1
@Jon, ¿qué quieres decir con (en ambos casos, E está bien) en el primer párrafo?
Geek

2
@ Geek: Quiero decir que si tienes algo así ? extends InputStreamo ? super InputStreampuedes usar un InputStreamcomo argumento.
Jon Skeet

Realmente nunca obtuve la explicación PECS de Josh Block en Java efectivo. Sin embargo, @Yishai, esta es una forma útil de recordar. Quizás podamos proponer un nuevo mnemónico, SAGE: Super -> Agregar / Obtener -> Extender
dcompilado el

Entonces, si lo leo bien, "<? Extiende E>" requiere que "?" es una subclase de "E" y "<? super E>" requiere que "E" sea una subclase de "?", ¿verdad?
El Suscriptor Justiciero

130

Las razones para esto se basan en cómo Java implementa genéricos.

Un ejemplo de matrices

Con las matrices puede hacer esto (las matrices son covariantes)

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;

Pero, ¿qué pasaría si intentas hacer esto?

myNumber[0] = 3.14; //attempt of heap pollution

Esta última línea se compilaría bien, pero si ejecuta este código, podría obtener un ArrayStoreException. Porque está tratando de poner un doble en una matriz entera (independientemente de que se acceda a través de una referencia numérica).

Esto significa que puede engañar al compilador, pero no puede engañar al sistema de tipos de tiempo de ejecución. Y esto es así porque las matrices son lo que llamamos tipos reificables . Esto significa que, en tiempo de ejecución, Java sabe que esta matriz en realidad se instancia como una matriz de enteros a los que simplemente se accede a través de una referencia de tipo Number[].

Entonces, como puede ver, una cosa es el tipo real del objeto, y otra es el tipo de referencia que usa para acceder a él, ¿verdad?

El problema con los genéricos de Java

Ahora, el problema con los tipos genéricos de Java es que el compilador descarta la información de tipo y no está disponible en tiempo de ejecución. Este proceso se llama borrado de tipo . Hay buenas razones para implementar genéricos como este en Java, pero esa es una larga historia, y tiene que ver, entre otras cosas, con la compatibilidad binaria con el código preexistente (vea Cómo obtuvimos los genéricos que tenemos ).

Pero el punto importante aquí es que, dado que, en el tiempo de ejecución, no hay información de tipo, no hay forma de garantizar que no estamos cometiendo contaminación por montón.

Por ejemplo,

List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);

List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution

Si el compilador de Java no le impide hacer esto, el sistema de tipos de tiempo de ejecución tampoco puede detenerlo, porque no hay forma, en tiempo de ejecución, de determinar que se suponía que esta lista era solo una lista de enteros. El tiempo de ejecución de Java le permitirá poner lo que desee en esta lista, cuando solo debe contener enteros, porque cuando se creó, se declaró como una lista de enteros.

Como tal, los diseñadores de Java se aseguraron de que no puedas engañar al compilador. Si no puede engañar al compilador (como podemos hacer con las matrices) tampoco puede engañar al sistema de tipo de tiempo de ejecución.

Como tal, decimos que los tipos genéricos no son reificables .

Evidentemente, esto obstaculizaría el polimorfismo. Considere el siguiente ejemplo:

static long sum(Number[] numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Ahora puedes usarlo así:

Integer[] myInts = {1,2,3,4,5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};

System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

Pero si intenta implementar el mismo código con colecciones genéricas, no tendrá éxito:

static long sum(List<Number> numbers) {
   long summation = 0;
   for(Number number : numbers) {
      summation += number.longValue();
   }
   return summation;
}

Obtendrías errores de compilación si intentas ...

List<Integer> myInts = asList(1,2,3,4,5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);

System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

La solución es aprender a usar dos potentes características de los genéricos de Java conocidos como covarianza y contravarianza.

Covarianza

Con la covarianza, puede leer elementos de una estructura, pero no puede escribir nada en ella. Todas estas son declaraciones válidas.

List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();

Y puedes leer de myNums:

Number n = myNums.get(0); 

Debido a que puede estar seguro de que, sea lo que sea lo que contenga la lista real, se puede convertir a un Número (después de todo, todo lo que se extiende Número es un Número, ¿verdad?)

Sin embargo, no está permitido poner nada en una estructura covariante.

myNumst.add(45L); //compiler error

Esto no estaría permitido, porque Java no puede garantizar cuál es el tipo real del objeto en la estructura genérica. Puede ser cualquier cosa que extienda Number, pero el compilador no puede estar seguro. Entonces puedes leer, pero no escribir.

Contravarianza

Con contravarianza puedes hacer lo contrario. Puede poner las cosas en una estructura genérica, pero no puede leerlas.

List<Object> myObjs = new List<Object>();
myObjs.add("Luke");
myObjs.add("Obi-wan");

List<? super Number> myNums = myObjs;
myNums.add(10);
myNums.add(3.14);

En este caso, la naturaleza real del objeto es una Lista de objetos y, a través de la contravarianza, puede colocar Números en él, básicamente porque todos los números tienen Objeto como su ancestro común. Como tal, todos los números son objetos y, por lo tanto, esto es válido.

Sin embargo, no puede leer con seguridad nada de esta estructura contravariante, suponiendo que obtendrá un número.

Number myNum = myNums.get(0); //compiler-error

Como puede ver, si el compilador le permitiera escribir esta línea, obtendría una ClassCastException en tiempo de ejecución.

Principio Get / Put

Como tal, use la covarianza cuando solo tenga la intención de tomar valores genéricos de una estructura, use la contravarianza cuando solo intente poner valores genéricos en una estructura y use el tipo genérico exacto cuando tenga la intención de hacer ambas cosas.

El mejor ejemplo que tengo es el siguiente que copia cualquier tipo de números de una lista a otra lista. Solo obtiene elementos de la fuente y solo coloca elementos en el destino.

public static void copy(List<? extends Number> source, List<? super Number> target) {
    for(Number number : source) {
        target(number);
    }
}

Gracias a los poderes de covarianza y contravarianza, esto funciona para un caso como este:

List<Integer> myInts = asList(1,2,3,4);
List<Double> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = new ArrayList<Object>();

copy(myInts, myObjs);
copy(myDoubles, myObjs);

27
Esta respuesta debería ir al principio. Buena explicación
Suresh Atta

1
@edwindalorzo, hay un pequeño error tipográfico que desea corregir en Contravarianza. Usted dice List<Object> myObjs = new List<Object();(que falta el cierre >para el segundo Object).

¡Ejemplos fantásticos, fáciles y claros de estos conceptos sutiles!
db1234

Algo que podría agregar para ayudar a otros a recordar cosas. Cuando quieres llamar a un método desde una superclase, lo usas super.methodName. Cuando se usa <? super E>, significa "algo en la superdirección" en lugar de algo en la extendsdirección. Ejemplo: Objectestá en la superdirección de Number(ya que es una superclase) y Integerestá en la extendsdirección (ya que se extiende Number).
BrainStorm.exe

59

<? extends E>se define Ecomo el límite superior: "Esto se puede enviar a E".

<? super E>se define Ecomo el límite inferior: " Ese puede lanzar a esto".


66
Este es uno de los mejores resúmenes simples / prácticos de la diferencia que he visto.
JAB

1
Durante décadas (con OOP) he estado luchando contra una inversión instintiva de las nociones "superior" e "inferior". ¡Agravante! Para mí, Objectes inherentemente una clase de nivel inferior, a pesar de su posición como la superclase final (y dibujada verticalmente en UML o árboles de herencia similares). Nunca he podido deshacer esto a pesar de los eones de intentarlo.

3
@ tgm1024 "superclase" y "subclase" deben causarle muchos problemas.
David Moles

@DavidMoles, ¿por qué? Claramente no estás siguiendo lo que digo. "superclase" es similar a "superconjunto"; La noción de especialización es una reducción en la aplicabilidad bajo la relación IS-A. Apple es una fruta. Fruit (superclase) es un superconjunto que incluye Apple (subclase) como un subconjunto. Esa relación verbal está bien. Lo que digo desglosa es la noción de que "superior" e "inferior" tienen asignaciones intrínsecas a "superconjunto" y "subconjunto". Superior e inferior deben evitarse como términos en OO.

1
@ tgm1024 "Super-" viene del latín super "arriba, en" y "sub-" del latín sub "debajo, debajo". Es decir, etimológicamente, super está arriba y sub está abajo.
David Moles

12

Voy a tratar de responder esto. Pero para obtener una respuesta realmente buena, debe consultar el libro de Joshua Bloch Effective Java (2nd Edition). Describe el PECS mnemotécnico, que significa "Producer Extends, Consumer Super".

La idea es que si el código consume los valores genéricos del objeto, entonces debería usar extend. pero si está produciendo nuevos valores para el tipo genérico, debe usar super.

Así por ejemplo:

public void pushAll(Iterable<? extends E> src) {
  for (E e: src) 
    push(e);
}

Y

public void popAll(Collection<? super E> dst) {
  while (!isEmpty())
    dst.add(pop())
}

Pero realmente deberías consultar este libro: http://java.sun.com/docs/books/effective/


12

<? super E> medio any object including E that is parent of E

<? extends E> medio any object including E that is child of E .


respuesta corta y dulce
chirag soni

7

Es posible que desee buscar en Google los términos contravarianza ( <? super E>) y covarianza ( <? extends E>). Descubrí que lo más útil al comprender los genéricos era que entendiera la firma del método de Collection.addAll:

public interface Collection<T> {
    public boolean addAll(Collection<? extends T> c);
}

Tal como le gustaría poder agregar un Stringa un List<Object>:

List<Object> lo = ...
lo.add("Hello")

También debería poder agregar una List<String>(o cualquier colección de Strings) a través del addAllmétodo:

List<String> ls = ...
lo.addAll(ls)

Sin embargo, debe darse cuenta de que a List<Object>y a List<String>no son equivalentes y tampoco es una subclase de la primera. Lo que se necesita es el concepto de un parámetro de tipo covariante , es decir, el <? extends T>bit.

Una vez que tenga esto, es simple pensar en escenarios donde también desea contravarianza (verifique la Comparableinterfaz).


4

Antes de la respuesta; Por favor sea claro que

  1. Los genéricos solo compilan la función de tiempo para garantizar TYPE_SAFETY, no estará disponible durante el tiempo de ejecución.
  2. Solo una referencia con Generics forzará el tipo de seguridad; si la referencia no se declara con genéricos, funcionará sin tipo de seguridad.

Ejemplo:

List stringList = new ArrayList<String>();
stringList.add(new Integer(10)); // will be successful.

Espero que esto te ayude a entender el comodín más claro.

//NOTE CE - Compilation Error
//      4 - For

class A {}

class B extends A {}

public class Test {

    public static void main(String args[]) {

        A aObj = new A();
        B bObj = new B();

        //We can add object of same type (A) or its subType is legal
        List<A> list_A = new ArrayList<A>();
        list_A.add(aObj);
        list_A.add(bObj); // A aObj = new B(); //Valid
        //list_A.add(new String()); Compilation error (CE);
        //can't add other type   A aObj != new String();


        //We can add object of same type (B) or its subType is legal
        List<B> list_B = new ArrayList<B>();
        //list_B.add(aObj); CE; can't add super type obj to subclass reference
        //Above is wrong similar like B bObj = new A(); which is wrong
        list_B.add(bObj);



        //Wild card (?) must only come for the reference (left side)
        //Both the below are wrong;   
        //List<? super A> wildCard_Wrongly_Used = new ArrayList<? super A>();
        //List<? extends A> wildCard_Wrongly_Used = new ArrayList<? extends A>();


        //Both <? extends A>; and <? super A> reference will accept = new ArrayList<A>
        List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<A>();
                        list_4__A_AND_SuperClass_A = new ArrayList<Object>();
                      //list_4_A_AND_SuperClass_A = new ArrayList<B>(); CE B is SubClass of A
                      //list_4_A_AND_SuperClass_A = new ArrayList<String>(); CE String is not super of A  
        List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<A>();
                          list_4__A_AND_SubClass_A = new ArrayList<B>();
                        //list_4__A_AND_SubClass_A = new ArrayList<Object>(); CE Object is SuperClass of A


        //CE; super reference, only accepts list of A or its super classes.
        //List<? super A> list_4__A_AND_SuperClass_A = new ArrayList<String>(); 

        //CE; extends reference, only accepts list of A or its sub classes.
        //List<? extends A> list_4__A_AND_SubClass_A = new ArrayList<Object>();

        //With super keyword we can use the same reference to add objects
        //Any sub class object can be assigned to super class reference (A)                  
        list_4__A_AND_SuperClass_A.add(aObj);
        list_4__A_AND_SuperClass_A.add(bObj); // A aObj = new B();
        //list_4__A_AND_SuperClass_A.add(new Object()); // A aObj != new Object(); 
        //list_4__A_AND_SuperClass_A.add(new String()); CE can't add other type

        //We can't put anything into "? extends" structure. 
        //list_4__A_AND_SubClass_A.add(aObj); compilation error
        //list_4__A_AND_SubClass_A.add(bObj); compilation error
        //list_4__A_AND_SubClass_A.add("");   compilation error

        //The Reason is below        
        //List<Apple> apples = new ArrayList<Apple>();
        //List<? extends Fruit> fruits = apples;
        //fruits.add(new Strawberry()); THIS IS WORNG :)

        //Use the ? extends wildcard if you need to retrieve object from a data structure.
        //Use the ? super wildcard if you need to put objects in a data structure.
        //If you need to do both things, don't use any wildcard.


        //Another Solution
        //We need a strong reference(without wild card) to add objects 
        list_A = (ArrayList<A>) list_4__A_AND_SubClass_A;
        list_A.add(aObj);
        list_A.add(bObj);

        list_B = (List<B>) list_4__A_AND_SubClass_A;
        //list_B.add(aObj); compilation error
        list_B.add(bObj);

        private Map<Class<? extends Animal>, List<? extends Animal>> animalListMap;

        public void registerAnimal(Class<? extends Animal> animalClass, Animal animalObject) {

            if (animalListMap.containsKey(animalClass)) {
                //Append to the existing List
                 /*    The ? extends Animal is a wildcard bounded by the Animal class. So animalListMap.get(animalObject);
                 could return a List<Donkey>, List<Mouse>, List<Pikachu>, assuming Donkey, Mouse, and Pikachu were all sub classes of Animal. 
                 However, with the wildcard, you are telling the compiler that you don't care what the actual type is as long as it is a sub type of Animal.      
                 */   
                //List<? extends Animal> animalList = animalListMap.get(animalObject);
                //animalList.add(animalObject);  //Compilation Error because of List<? extends Animal>
                List<Animal> animalList = animalListMap.get(animalObject);
                animalList.add(animalObject);      


            } 
    }

    }
}



1
Buena explicación con código. Pero si hubiera utilizado comentarios en el código fuera del bloque de código, sería mejor para ver y más legible.
Prabhu

3

Un comodín con un límite superior se parece a "? Extiende Tipo" y representa la familia de todos los tipos que son subtipos de Tipo, incluido el Tipo Tipo. El tipo se llama límite superior.

Un comodín con un límite inferior se parece a "? Super Type" y representa la familia de todos los tipos que son supertipos de Type, incluido Type type. El tipo se llama límite inferior.


1

Tiene una clase Parent y una clase Child heredada de la clase Parent. La clase Parent se hereda de otra clase llamada GrandParent Class. Por lo tanto, el orden de herencia es GrandParent> Parent> Child. Ahora, <? extiende Parent>: esto acepta la clase Parent o la clase Child <? super Parent>: acepta la clase Parent o la clase GrandParent

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.