Le matrici sono covarianti
Si dice che le matrici siano covarianti, il che significa sostanzialmente che, date le regole del sottotipo di Java, una matrice di tipo T[]
può contenere elementi di tipo T
o qualsiasi sottotipo di T
. Per esempio
Number[] numbers = new Number[3];
numbers[0] = newInteger(10);
numbers[1] = newDouble(3.14);
numbers[2] = newByte(0);
Ma non solo, le regole del sottotipo di Java affermano anche che un array S[]
è un sottotipo dell'array T[]
se S
è un sottotipo di T
, quindi qualcosa di simile è anche valido:
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
Perché secondo le regole del sottotipo in Java, un array Integer[]
è un sottotipo di un array Number[]
perché Integer è un sottotipo di Number.
Ma questa regola di sottotitolo può portare a una domanda interessante: cosa accadrebbe se provassimo a farlo?
myNumber[0] = 3.14; //attempt of heap pollution
Quest'ultima riga verrà compilata correttamente, ma se eseguiamo questo codice, otterremmo un ArrayStoreException
perché stiamo cercando di inserire un doppio in un array intero. Il fatto che stiamo accedendo all'array tramite un riferimento numerico è irrilevante qui, ciò che conta è che l'array sia un array di numeri interi.
Ciò significa che possiamo ingannare il compilatore, ma non possiamo ingannare il sistema di tipi di runtime. E questo perché le matrici sono ciò che chiamiamo un tipo riutilizzabile. Ciò significa che al runtime 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 possiamo vedere, una cosa è il tipo reale dell'oggetto, un'altra cosa è il tipo di riferimento che usiamo per accedervi, giusto?
Il problema con Java Generics
Ora, il problema con i tipi generici in Java è che le informazioni sul tipo per i parametri del tipo vengono scartate dal compilatore dopo aver completato la compilazione del codice; pertanto queste informazioni sul tipo non sono disponibili in fase di esecuzione. Questo processo è chiamato cancellazione del tipo . Ci sono buoni motivi per implementare generici come questo in Java, ma questa è una lunga storia, e ha a che fare con la compatibilità binaria con codice preesistente.
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 da ammasso.
Consideriamo ora il seguente codice non sicuro:
List<Integer> myInts = newArrayList<Integer>();
myInts.add(1);
myInts.add(2);
List<Number> myNums = myInts; //compiler error
myNums.add(3.14); //heap polution
Se il compilatore Java non ci impedisce di farlo, il sistema dei tipi di runtime non può fermarci neanche, perché al momento dell'esecuzione non è possibile determinare che questo elenco dovrebbe essere un elenco di soli numeri interi. Il runtime Java ci consentirebbe di inserire ciò che vogliamo in questo elenco, quando dovrebbe contenere solo numeri interi, poiché quando è stato creato, è stato dichiarato come un elenco di numeri interi. Ecco perché il compilatore rifiuta la riga numero 4 perché non è sicuro e, se consentito, potrebbe infrangere i presupposti del sistema di tipi.
Pertanto, i progettisti di Java si sono assicurati di non poter ingannare il compilatore. Se non riusciamo a ingannare il compilatore (come possiamo fare con le matrici), non possiamo nemmeno ingannare il sistema di tipi di runtime.
Pertanto, diciamo che i tipi generici non sono riutilizzabili, poiché in fase di esecuzione non possiamo determinare la vera natura del tipo generico.
Ho saltato alcune parti di queste risposte, puoi leggere l'articolo completo qui:
https://dzone.com/articles/covariance-and-contravariance