Perché gli array sono covarianti ma i generici sono invarianti?

Da Java efficace di Joshua Bloch,

  1. Le matrici differiscono dal tipo generico in due modi importanti. I primi array sono covarianti. I generici sono invarianti.
  2. Covariant significa semplicemente se X è sottotipo di Y, quindi X [] sarà anche sottotipo di Y []. Le matrici sono covarianti Poiché la stringa è sottotipo di Oggetto Così

    String[] is subtype of Object[]

    Invariant significa semplicemente indipendentemente dal fatto che X sia sottotipo di Y o no,

      List will not be subType of List. 

La mia domanda è: perché la decisione di rendere gli array covarianti in Java? Ci sono altri post SO come Why are Array invarianti, ma Lists covariant? , ma sembrano concentrati su Scala e non sono in grado di seguirlo.

Via Wikipedia :

Le prime versioni di Java e C # non includevano i generici (ovvero il polimorfismo parametrico).

In tale impostazione, rendendo le matrici invarianti esclude programmi polimorfici utili. Ad esempio, si consideri di scrivere una funzione per mescolare un array, o una funzione che testa due array per l’uguaglianza usando il metodo Object.equals sugli elementi. L’implementazione non dipende dal tipo esatto di elemento memorizzato nell’array, quindi dovrebbe essere ansible scrivere una singola funzione che funzioni su tutti i tipi di array. È facile implementare funzioni di tipo

 boolean equalArrays (Object[] a1, Object[] a2); void shuffleArray(Object[] a); 

Tuttavia, se i tipi di array fossero considerati invarianti, sarebbe ansible chiamare queste funzioni solo su un array esattamente del tipo Object[] . Ad esempio, non è ansible mischiare una serie di stringhe.

Pertanto, sia Java che C # trattano i tipi di array in modo covariante. Ad esempio, in C # string[] è un sottotipo di object[] e in Java String[] è un sottotipo di Object[] .

Questo risponde alla domanda “Perché gli array sono covarianti?”, O più precisamente, “Perché gli array sono stati covarianti in quel momento ?”

Quando sono stati introdotti i farmaci generici, questi non sono stati volutamente covarianti per le ragioni evidenziate in questa risposta da Jon Skeet :

No, una List non è una List . Considera cosa puoi fare con una List – puoi aggiungere qualsiasi animale ad essa … incluso un gatto. Ora, puoi aggiungere logicamente un gatto a una cucciolata di cuccioli? Assolutamente no.

 // Illegal code - because otherwise life would be Bad List dogs = new List(); List animals = dogs; // Awooga awooga animals.add(new Cat()); Dog dog = dogs.get(0); // This should be safe, right? 

All’improvviso hai un gatto molto confuso.

La motivazione originale per fare gli array covarianti descritti nell’articolo di wikipedia non si applicava ai generici perché i caratteri jolly rendevano ansible l’espressione della covarianza (e della contravarianza), ad esempio:

 boolean equalLists(List l1, List l2); void shuffleList(List l); 

Il motivo è che ogni array conosce il suo tipo di elemento durante il runtime, mentre la raccolta generica non dipende dalla cancellazione dei tipi. Per esempio:

 String[] strings = new String[2]; Object[] objects = strings; // valid, String[] is Object[] objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime 

Se questo è stato permesso con le raccolte generiche:

 List strings = new ArrayList(); List objects = strings; // let's say it is valid objects.add(12); // invalid, Integer should not be put into List but there is no information during runtime to catch this 

Ma questo potrebbe causare problemi in seguito quando qualcuno cercherebbe di accedere alla lista:

 String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String 

Potrebbe essere questo aiuto: –

I generici non sono covarianti

Le matrici in linguaggio Java sono covarianti – il che significa che se Integer estende Number (che fa), quindi non solo è un numero intero anche un numero, ma un numero intero [] è anche un Number[] , e sei libero di passare oppure assegna un numero Integer[] dove viene richiesto un Number[] . (Più formalmente, se Number è un supertipo di Integer, quindi Number[] è un supertipo di Integer[] .) Si potrebbe pensare che lo stesso sia vero anche per i tipi generici – che List è un supertipo di List List List e che è ansible passare un List cui è previsto un List . Sfortunatamente, non funziona in questo modo.

Si scopre che c’è una buona ragione per cui non funziona in questo modo: romperebbe il tipo di sicurezza che i generici avrebbero dovuto fornire. Immagina di poter assegnare un List a un List . Quindi il seguente codice ti consentirebbe di inserire qualcosa che non fosse un intero in un List :

 List li = new ArrayList(); List ln = li; // illegal ln.add(new Float(3.1415)); 

Poiché ln è una List , aggiungere un Float sembra perfettamente legale. Ma se l fosse aliasato con li , allora romperebbe la promise di sicurezza del tipo implicita nella definizione di li – che è una lista di interi, ed è per questo che i tipi generici non possono essere covarianti.

Gli array sono covarianti per almeno due motivi:

  • È utile per le raccolte che contengono informazioni che non cambieranno mai per essere covarianti. Perché una raccolta di T sia covariante, il suo backing store deve anche essere covariante. Mentre si poteva progettare una collezione T immutabile che non usasse un T[] come backing store (ad es. Usando un albero o una lista collegata), sarebbe improbabile che una collezione di questo tipo potesse essere riprodotta come quella di un array. Si potrebbe obiettare che un modo migliore per fornire collezioni immutabili covarianti sarebbe stato quello di definire un tipo di “matrice immutabile covariante” che potesse usare un backing store, ma semplicemente consentire la covarianza dell’array era probabilmente più facile.

  • Gli array saranno spesso mutati da codice che non sa quale tipo di cosa sarà in loro, ma non metterà nell’array nulla che non sia stato letto dallo stesso array. Un primo esempio di questo è l’ordinamento del codice. Concettualmente potrebbe essere stato ansible per i tipi di array includere metodi per scambiare o permutare elementi (tali metodi potrebbero essere ugualmente applicabili a qualsiasi tipo di array) o definire un object “array manipulator” che contiene un riferimento a un array e uno o più oggetti che era stato letto da esso e poteva includere metodi per memorizzare elementi precedentemente letti nell’array da cui erano arrivati. Se gli array non erano covarianti, il codice utente non sarebbe in grado di definire un tale tipo, ma il runtime avrebbe potuto includere alcuni metodi specializzati.

Il fatto che gli array siano covarianti può essere visto come un brutto attacco, ma nella maggior parte dei casi facilita la creazione di codice funzionante.

Una caratteristica importante dei tipi parametrici è la capacità di scrivere algoritmi polimorfici, cioè algoritmi che operano su una struttura di dati indipendentemente dal valore del parametro, come Arrays.sort() .

Con i generici, questo è fatto con i tipi di caratteri jolly:

 > void sort(E[]); 

Per essere veramente utili, i tipi di caratteri jolly richiedono l’acquisizione di caratteri jolly e ciò richiede la nozione di un parametro di tipo. Nessuno di questi era disponibile al momento in cui gli array venivano aggiunti a Java, e gli array di mover di covarianti di tipo di riferimento consentivano un modo molto più semplice per consentire algoritmi polimorfici:

 void sort(Comparable[]); 

Tuttavia, questa semplicità ha aperto una scappatoia nel sistema di tipi statici:

 String[] strings = {"hello"}; Object[] objects = strings; objects[0] = 1; // throws ArrayStoreException 

richiede un controllo runtime di ogni accesso in scrittura a un array di tipo di riferimento.

In breve, l’approccio più recente incarnato dai generici rende il sistema di tipi più complesso, ma anche più staticamente il tipo sicuro, mentre l’approccio più vecchio era più semplice, e meno staticamente il tipo sicuro. I progettisti del linguaggio hanno optato per un approccio più semplice, avendo cose più importanti da fare che chiudere una piccola scappatoia nel sistema di tipi che raramente causa problemi. Più tardi, quando fu creato Java, e le pressanti necessità di cura, avevano le risorse per farlo bene per i generici (ma cambiarlo per gli array avrebbe rotto i programmi Java esistenti).

I generici sono invarianti : da JSL 4.10 :

… La sottotipizzazione non si estende ai tipi generici: T <: U non implica che C <: C

e qualche riga in più, JLS spiega anche questo
Gli array sono covarianti (primo punto):

4.10.3 Sottotitoli tra i tipi di matrice

inserisci la descrizione dell'immagine qui

La mia opinione: quando il codice si aspetta un array A [] e gli dai B [] dove B è una sottoclass di A, ci sono solo due cose di cui preoccuparsi: cosa succede quando leggi un elemento dell’array e cosa succede se scrivi esso. Quindi non è difficile scrivere regole linguistiche per garantire che la sicurezza del tipo sia preservata in tutti i casi (la regola principale è che una ArrayStoreException possa essere lanciata se si tenta di inserire una A in una B []). Per un generico, però, quando dichiari una class SomeClass , ci può essere un qualsiasi numero di modi in cui T è usato nel corpo della class, e sto supponendo che sia troppo complicato per elaborare tutte le combinazioni possibili scrivere regole su quando le cose sono permesse e quando non lo sono.

Penso che abbiano preso una decisione sbagliata al primo posto che ha fatto covariant di matrice. Rompe il tipo di sicurezza come descritto qui e sono rimasti bloccati con quello a causa della retrocompatibilità e in seguito hanno cercato di non commettere lo stesso errore per generico. E questo è uno dei motivi per cui Joshua Bloch preferisce le liste agli arrays nell’articolo 25 del libro “Effective Java (seconda edizione)”