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