Matrizes são covariantes
Diz-se que matrizes são covariantes, o que basicamente significa que, dadas as regras de subtipagem de Java, uma matriz de tipo T[]
pode conter elementos do tipo T
ou qualquer subtipo de T
. Por exemplo
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Mas não é só isso, as regras de subtipagem de Java também afirmam que uma matriz S[]
é um subtipo da matriz T[]
se S
for um subtipo de T
, portanto, algo como isso também é válido:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Porque de acordo com as regras de subtipagem em Java, uma matriz Integer[]
é um subtipo de matriz Number[]
porque Integer é um subtipo de Number.
Mas essa regra de subtipagem pode levar a uma pergunta interessante: o que aconteceria se tentássemos fazer isso?
myNumber[0] = 3.14; //attempt of heap pollution
Essa última linha seria compilada muito bem, mas se executássemos esse código, obteríamos um ArrayStoreException
porque estamos tentando colocar um duplo em uma matriz inteira. O fato de estarmos acessando a matriz por meio de uma referência Number é irrelevante aqui, o que importa é que a matriz é uma matriz de números inteiros.
Isso significa que podemos enganar o compilador, mas não podemos enganar o sistema do tipo tempo de execução. E é assim porque matrizes são o que chamamos de tipo reificável. Isso significa que, em tempo de execução, Java sabe que essa matriz foi realmente instanciada como uma matriz de números inteiros que simplesmente é acessada por meio de uma referência do tipo Number[]
.
Então, como podemos ver, uma coisa é o tipo real do objeto, outra coisa é o tipo de referência que usamos para acessá-lo, certo?
O problema com Java Generics
Agora, o problema com tipos genéricos em Java é que as informações de tipo para os parâmetros de tipo são descartadas pelo compilador após a compilação do código; portanto, essas informações de tipo não estão disponíveis no tempo de execução. Esse processo é chamado de apagamento de tipo . Existem boas razões para implementar genéricos como este em Java, mas isso é uma longa história e tem a ver com compatibilidade binária com código pré-existente.
O ponto importante aqui é que, como em tempo de execução, não há informações de tipo, não há como garantir que não estamos cometendo poluição de pilha.
Vamos considerar agora o seguinte código não seguro:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Se o compilador Java não nos impedir de fazer isso, o sistema do tipo tempo de execução também não poderá nos impedir, porque não há como, em tempo de execução, determinar que essa lista deveria ser apenas uma lista de números inteiros. O tempo de execução do Java nos permite colocar o que queremos nesta lista, quando deve conter apenas números inteiros, porque quando foi criado, foi declarado como uma lista de números inteiros. É por isso que o compilador rejeita a linha número 4 porque é inseguro e, se permitido, pode quebrar as suposições do sistema de tipos.
Como tal, os designers de Java se certificaram de que não podemos enganar o compilador. Se não podemos enganar o compilador (como podemos fazer com matrizes), também não podemos enganar o sistema do tipo tempo de execução.
Como tal, dizemos que os tipos genéricos não são reificáveis, pois em tempo de execução não podemos determinar a verdadeira natureza do tipo genérico.
Eu pulei algumas partes destas respostas, você pode ler o artigo completo aqui:
https://dzone.com/articles/covariance-and-contravariance