Las matrices son covariantes
Se dice que las matrices son covariantes, lo que básicamente significa que, dadas las reglas de subtipo de Java, una matriz de tipo T[]
puede contener elementos de tipo T
o cualquier subtipo de T
. Por ejemplo
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Pero no solo eso, las reglas de subtipo de Java también establecen que una matriz S[]
es un subtipo de la matriz T[]
si S
es un subtipo de T
, por lo tanto, algo como esto también es válido:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Porque de acuerdo con las reglas de subtipo en Java, una matriz Integer[]
es un subtipo de una matriz Number[]
porque Integer es un subtipo de Número.
Pero esta regla de subtipo puede llevar a una pregunta interesante: ¿qué pasaría si intentamos hacer esto?
myNumber[0] = 3.14; //attempt of heap pollution
Esta última línea se compilaría bien, pero si ejecutamos este código, obtendríamos un ArrayStoreException
porque estamos tratando de poner un doble en una matriz entera. El hecho de que estemos accediendo a la matriz a través de una referencia de Número es irrelevante aquí, lo que importa es que la matriz es una matriz de enteros.
Esto significa que podemos engañar al compilador, pero no podemos engañar al sistema de tipo de tiempo de ejecución. Y esto es así porque las matrices son lo que llamamos un tipo reificable. 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 podemos ver, una cosa es el tipo real del objeto, otra cosa es el tipo de referencia que usamos para acceder a él, ¿verdad?
El problema con los genéricos de Java
Ahora, el problema con los tipos genéricos en Java es que la información de tipo para los parámetros de tipo es descartada por el compilador después de la compilación del código; por lo tanto, este tipo de información 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 con la compatibilidad binaria con el código preexistente.
El punto importante aquí es que, dado que en tiempo de ejecución no hay información de tipo, no hay forma de garantizar que no estamos cometiendo contaminación por montón.
Consideremos ahora el siguiente código inseguro:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Si el compilador de Java no nos impide hacer esto, el sistema de tipo de tiempo de ejecución tampoco puede detenernos, porque no hay forma, en tiempo de ejecución, de determinar que esta lista se suponía que era solo una lista de enteros. El tiempo de ejecución de Java nos permitiría incluir lo que queramos en esta lista, cuando solo debería contener enteros, porque cuando se creó, se declaró como una lista de enteros. Es por eso que el compilador rechaza la línea número 4 porque no es segura y, si se permite, podría romper los supuestos del sistema de tipos.
Como tal, los diseñadores de Java se aseguraron de que no podemos engañar al compilador. Si no podemos engañar al compilador (como podemos hacer con las matrices), tampoco podemos engañar al sistema de tipo de tiempo de ejecución.
Como tal, decimos que los tipos genéricos no son reificables, ya que en tiempo de ejecución no podemos determinar la verdadera naturaleza del tipo genérico.
Me salteé algunas partes de estas respuestas, puede leer el artículo completo aquí:
https://dzone.com/articles/covariance-and-contravariance