Las razones de esto se basan en cómo Java implementa los genéricos.
Un matrices Ejemplo
Con los arreglos que pueden hacer esto (arrays son covariantes)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Pero, ¿qué pasaría si intenta hacer esto?
myNumber[0] = 3.14; //attempt of heap pollution
Esta última línea se compilará bien, pero si se ejecuta este código, se podría obtener una ArrayStoreException
. Porque intenta poner un doble en una matriz de enteros (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 del tipo de tiempo de ejecución. Y esto es así porque las matrices son lo que llamamos tipos confirmables. Esto significa que, en tiempo de ejecución, Java sabe que esta matriz se instancia realmente como una matriz de enteros a los que simplemente se accede mediante una referencia de tipo Number[]
.
Entonces, como puede ver, una cosa es el tipo real del objeto, y otra cosa es el tipo de referencia que utiliza para acceder a ella, ¿no?
El problema con Java Generics
Ahora, el problema con los tipos genéricos de Java es que la información de tipo se descarta por el compilador y no está disponible en tiempo de ejecución. Este proceso se llama type erasure. Hay buenas razones para implementar genéricos como este en Java, pero esa es una historia larga, y tiene que ver con la compatibilidad binaria con el código preexistente.
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 acumulativa.
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 Java no le impide hacer esto, el sistema de tipos en tiempo de ejecución no se puede parar o bien, porque no hay manera, en tiempo de ejecución, para determinar que esta lista era se supone que es una lista de enteros solamente. El tiempo de ejecución de Java le permite colocar 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 se puede 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 son no no reificables.
Evidentemente, esto obstaculizaría el polimorfismo. Consideremos el siguiente ejemplo:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Ahora se puede utilizar de esta manera:
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 se intenta poner en práctica el mismo código con colecciones genéricas, que no tendrá éxito:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Obtendrá errores de compilación si intenta ...
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 poderosas características de los genéricos de Java conocidos como covarianza y contravarianza.
covarianza
Con covarianza se puede leer los artículos en una estructura, pero no se puede escribir nada en él. 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 se puede leer desde myNums
:
Number n = myNums.get(0);
Debido a que usted puede estar seguro de que cualquiera que sea la lista real contiene, se puede upcasted a un número (después de todo, cualquier cosa que se extiende número es un número , ¿no?)
Sin embargo, no está permitido poner nada en una estructura covariante.
myNumst.add(45L); //compiler error
Esto no se permitiría, 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. Para que pueda leer, pero no escribir.
contravarianza
Con contravarianza puede hacer lo contrario. Puede poner las cosas en una estructura genérica, pero no puede leer desde allí.
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 contravarianza, puede poner los números en ella, básicamente porque todos los números tienen como objeto 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 se puede ver, si el compilador permitió a escribir esta línea, se llega a un ClassCastException en tiempo de ejecución.
Obtener/Poner Principio
Como tal, el uso de covarianza cuando sólo se van a tomar valores genéricos de una estructura, utilice contravarianza cuando sólo se van a poner los valores genéricos en una estructura y el uso de los genéricos exacta escriba 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. Solo obtiene elementos de la fuente, y solo pone elementos en el destino.
public static void copy(List<? extends Number> source, List<? super Number> destiny) {
for(Number number : source) {
destiny.add(number);
}
}
Gracias a los poderes de la 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);
+1. Esa es realmente la diferencia práctica. se extiende a buscar, super para insertar. – Yishai
Muchas gracias. Gran explicación! –
@Jon ¿a qué te refieres con (en ambos casos, E mismo está bien) en el primer párrafo? – Geek