As razões para isso são baseadas em como o Java implementa genéricos.
Um exemplo de matrizes
Com matrizes, você pode fazer isso (matrizes são covariantes)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Mas, o que aconteceria se você tentar fazer isso?
myNumber[0] = 3.14; //attempt of heap pollution
Essa última linha seria compilada, mas se você executar esse código, poderá obter um ArrayStoreException
. Porque você está tentando colocar um duplo em uma matriz inteira (independentemente de ser acessado por meio de uma referência numérica).
Isso significa que você pode enganar o compilador, mas não pode enganar o sistema de tipo de tempo de execução. E é assim porque matrizes são o que chamamos de tipos reificáveis . 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 você pode ver, uma coisa é o tipo real do objeto e outra é o tipo de referência que você usa para acessá-lo, certo?
O problema com os genéricos Java
Agora, o problema com os tipos genéricos de Java é que as informações do tipo são descartadas pelo compilador e não estão disponíveis no tempo de execução. Esse processo é chamado de apagamento de tipo . Há boas razões para implementar genéricos como este em Java, mas isso é uma longa história, e tem a ver, entre outras coisas, com compatibilidade binária com código pré-existente (consulte Como obtemos os genéricos que temos ).
Mas o ponto importante aqui é que, já que, 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.
Por exemplo,
List<Integer> myInts = new ArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap pollution
Se o compilador Java não impedir que você faça isso, o sistema do tipo tempo de execução também não poderá impedi-lo, 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 Java permite colocar o que você deseja nesta lista, quando deve conter apenas números inteiros, porque quando foi criado, foi declarado como uma lista de números inteiros.
Como tal, os designers de Java se certificaram de que você não pode enganar o compilador. Se você não pode enganar o compilador (como podemos fazer com matrizes), também não pode enganar o sistema de tipo de tempo de execução.
Como tal, dizemos que tipos genéricos não são reificáveis .
Evidentemente, isso dificultaria o polimorfismo. Considere o seguinte exemplo:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Agora você pode usá-lo assim:
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));
Mas se você tentar implementar o mesmo código com coleções genéricas, não terá êxito:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Você obteria erros de compilador se tentar ...
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
A solução é aprender a usar dois recursos poderosos dos genéricos Java conhecidos como covariância e contravariância.
Covariância
Com a covariância, você pode ler itens de uma estrutura, mas não pode escrever nada nela. Todas estas são declarações válidas.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
E você pode ler em myNums
:
Number n = myNums.get(0);
Como você pode ter certeza de que, independentemente da lista real, ela pode ser convertida para um número (afinal, qualquer coisa que estenda o número é um número, certo?)
No entanto, você não tem permissão para colocar nada em uma estrutura covariante.
myNumst.add(45L); //compiler error
Isso não seria permitido, porque Java não pode garantir qual é o tipo real do objeto na estrutura genérica. Pode ser qualquer coisa que estenda Number, mas o compilador não pode ter certeza. Então você pode ler, mas não escrever.
Contravariância
Com contravariância, você pode fazer o oposto. Você pode colocar as coisas em uma estrutura genérica, mas não pode ler a partir dela.
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);
Nesse caso, a natureza real do objeto é uma Lista de objetos e, por contravariância, você pode colocar números nele, basicamente porque todos os números têm Objeto como seu ancestral comum. Como tal, todos os números são objetos e, portanto, isso é válido.
No entanto, você não pode ler com segurança nada dessa estrutura contravariante, assumindo que receberá um número.
Number myNum = myNums.get(0); //compiler-error
Como você pode ver, se o compilador permitir que você escreva essa linha, você obterá uma ClassCastException em tempo de execução.
Get / Put Princípio
Assim, use covariância quando pretender apenas retirar valores genéricos de uma estrutura, use contravariância quando pretender colocar valores genéricos em uma estrutura e use o tipo genérico exato quando pretender fazer as duas coisas.
O melhor exemplo que tenho é o seguinte, que copia qualquer tipo de número de uma lista para outra. Ele só obtém itens da fonte e apenas coloca itens no destino.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Graças aos poderes de covariância e contravariância, isso funciona para um 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);