Le ragioni sono basate su come Java implementa i generici.
Un esempio di array
Con gli array puoi farlo (gli array sono covarianti)
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Ma cosa succederebbe se provassi a farlo?
myNumber[0] = 3.14; //attempt of heap pollution
Quest'ultima riga verrà compilata correttamente, ma se si esegue questo codice, è possibile ottenere un ArrayStoreException
. Perché stai cercando di inserire un doppio in un array intero (indipendentemente dall'accesso tramite un riferimento numerico).
Ciò significa che puoi ingannare il compilatore, ma non puoi ingannare il sistema di tipi di runtime. E questo perché le matrici sono quelli che chiamiamo tipi riutilizzabili . Ciò significa che in fase di esecuzione Java sa che questo array è stato effettivamente istanziato come un array di numeri interi a cui si accede semplicemente tramite un riferimento di tipo Number[]
.
Quindi, come puoi vedere, una cosa è il tipo reale dell'oggetto, e un'altra cosa è il tipo di riferimento che usi per accedervi, giusto?
Il problema con Java Generics
Ora, il problema con i tipi generici Java è che le informazioni sul tipo vengono scartate dal compilatore e non sono disponibili in fase di esecuzione. Questo processo si chiama cancellazione del tipo . Ci sono buone ragioni per implementare generici come questo in Java, ma questa è una lunga storia, e deve fare, tra l'altro, con la compatibilità binaria con codice preesistente (vedi Come abbiamo i generici che abbiamo ).
Ma il punto importante qui è che dal momento che, in fase di esecuzione, non ci sono informazioni sul tipo, non c'è modo di garantire che non stiamo commettendo l'inquinamento del mucchio.
Per esempio,
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 il compilatore Java non ti impedisce di farlo, neanche il sistema di tipi di runtime può impedirti, poiché al momento dell'esecuzione non è possibile determinare che questo elenco dovrebbe essere solo un elenco di numeri interi. Il runtime Java ti consente di inserire tutto ciò che desideri in questo elenco, quando dovrebbe contenere solo numeri interi, poiché quando è stato creato, è stato dichiarato come un elenco di numeri interi.
Pertanto, i progettisti di Java si sono assicurati di non poter ingannare il compilatore. Se non puoi ingannare il compilatore (come possiamo fare con gli array), non puoi nemmeno ingannare il sistema di tipi di runtime.
Pertanto, diciamo che i tipi generici non sono riutilizzabili .
Evidentemente, ciò ostacolerebbe il polimorfismo. Considera il seguente esempio:
static long sum(Number[] numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Ora puoi usarlo in questo modo:
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));
Ma se si tenta di implementare lo stesso codice con raccolte generiche, non si riuscirà:
static long sum(List<Number> numbers) {
long summation = 0;
for(Number number : numbers) {
summation += number.longValue();
}
return summation;
}
Otterresti errori del compilatore se provi a ...
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 soluzione è imparare a usare due potenti funzionalità dei generici Java noti come covarianza e contravarianza.
covarianza
Con la covarianza puoi leggere elementi da una struttura, ma non puoi scrivere nulla al suo interno. Tutte queste sono dichiarazioni valide.
List<? extends Number> myNums = new ArrayList<Integer>();
List<? extends Number> myNums = new ArrayList<Float>();
List<? extends Number> myNums = new ArrayList<Double>();
E puoi leggere da myNums
:
Number n = myNums.get(0);
Perché puoi essere sicuro che qualunque sia l'elenco reale, può essere convertito in un numero (dopo tutto ciò che estende Number è un numero, giusto?)
Tuttavia, non ti è permesso inserire nulla in una struttura covariante.
myNumst.add(45L); //compiler error
Ciò non sarebbe consentito, poiché Java non può garantire quale sia il tipo effettivo dell'oggetto nella struttura generica. Può essere qualsiasi cosa che estende Numero, ma il compilatore non può essere sicuro. Quindi puoi leggere, ma non scrivere.
controvarianza
Con la contraddizione puoi fare il contrario. Puoi mettere le cose in una struttura generica, ma non puoi leggerle.
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);
In questo caso, la natura effettiva dell'oggetto è un Elenco di oggetti e, attraverso la contraddizione, è possibile inserire i Numeri in esso, fondamentalmente perché tutti i numeri hanno Oggetto come loro antenato comune. Come tale, tutti i numeri sono oggetti e quindi questo è valido.
Tuttavia, non puoi leggere nulla di sicuro da questa struttura contraddittoria supponendo che otterrai un numero.
Number myNum = myNums.get(0); //compiler-error
Come puoi vedere, se il compilatore ti consentisse di scrivere questa riga, otterrai una ClassCastException in fase di esecuzione.
Principio Get / Put
Come tale, utilizzare la covarianza quando si intende estrarre valori generici da una struttura, utilizzare la contraddizione quando si intende solo inserire valori generici in una struttura e utilizzare il tipo generico esatto quando si intende fare entrambe le cose.
Il miglior esempio che ho è il seguente che copia qualsiasi tipo di numero da un elenco a un altro elenco. Riceve solo oggetti dalla fonte e mette solo oggetti nel bersaglio.
public static void copy(List<? extends Number> source, List<? super Number> target) {
for(Number number : source) {
target(number);
}
}
Grazie ai poteri della covarianza e della contraddizione funziona per un caso come questo:
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);