Perché le matrici sono covarianti ma i generici sono invarianti?


160

Da Effective Java 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 che se X è un sottotipo di Y, allora X [] sarà anche un sottotipo di Y []. Le matrici sono covarianti Poiché la stringa è il sottotipo di Object So

    String[] is subtype of Object[]

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

     List<X> will not be subType of List<Y>.

La mia domanda è: perché la decisione di rendere le matrici covarianti in Java? Esistono altri post SO come Why are Ararys invariant, ma Lists covariant? , ma sembrano concentrarsi sulla Scala e non sono in grado di seguirlo.


1
Non è perché i generici sono stati aggiunti in seguito?
Sotirios Delimanolis,

1
penso che il confronto tra array e collezioni sia ingiusto, le collezioni usano array in background !!
Ahmed Adel Ismail,

4
@ EL-conteDe-monteTereBentikh Non tutte le raccolte, ad esempio LinkedList.
Paul Bellora,

@PaulBellora so che le mappe sono diverse dagli implementatori di Collection, ma ho letto su SCPJ6 che le Collezioni in genere si basavano su array !!
Ahmed Adel Ismail,

Perché non esiste ArrayStoreException; quando si inserisce un elemento errato nella raccolta in cui è presente l'array. Quindi Collection può trovarlo solo al momento del recupero e anche a causa del casting. Quindi i generici assicureranno di risolvere questo problema.
Kanagavelu Sugumar,

Risposte:


151

Tramite Wikipedia :

Le prime versioni di Java e C # non includevano i generici (noti anche come polimorfismo parametrico).

In tale contesto, rendere invarianti le matrici esclude utili programmi polimorfici. Ad esempio, prendere in considerazione la possibilità di scrivere una funzione per mescolare un array o una funzione che verifica la parità di due array utilizzando il Object.equalsmetodo sugli elementi. L'implementazione non dipende dal tipo esatto di elemento archiviato nell'array, quindi dovrebbe essere possibile 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 trattati come invarianti, sarebbe possibile chiamare queste funzioni su un array esattamente del tipo Object[]. Ad esempio, non è possibile mescolare 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é le matrici sono covarianti?", O più precisamente, "Perché le matrici sono state rese covarianti in quel momento ?"

Quando furono introdotti i generici, non furono intenzionalmente resi covarianti per i motivi indicati in questa risposta da Jon Skeet :

No, a List<Dog>non è a List<Animal>. Considera cosa puoi fare con un List<Animal>- puoi aggiungere qualsiasi animale ad esso ... incluso un gatto. Ora, puoi logicamente aggiungere un gatto a una cucciolata di cuccioli? Assolutamente no.

// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new List<Dog>();
List<Animal> 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 rendere le matrici covarianti descritte nell'articolo di Wikipedia non si applicava ai generici perché i caratteri jolly hanno reso possibile l'espressione della covarianza (e della contraddizione), ad esempio:

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

3
sì, Arrays consente un comportamento polimorfico, tuttavia, introduce delle estensioni di runtime (diversamente dalle eccezioni in fase di compilazione con i generici). ad es .:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagerto

21
È corretto. Le matrici hanno tipi riutilizzabili e generano ArrayStoreExceptions secondo necessità. Chiaramente questo era considerato un degno compromesso in quel momento. In contrasto con oggi: molti considerano la covarianza dell'array come un errore, a posteriori.
Paul Bellora,

1
Perché "molti" lo considerano un errore? È molto più utile che non avere la covarianza dell'array. Quante volte hai visto un ArrayStoreException; sono abbastanza rari. L'ironia qui è imperdonabile imo ... tra i peggiori errori mai commessi in Java è la varianza del sito d'uso aka caratteri jolly.
Scott,

3
@ScottMcKinney: "Perché" molti "lo considerano un errore?" AIUI, questo perché la covarianza di array richiede test di tipo dinamici su tutte le operazioni di assegnazione di array (sebbene le ottimizzazioni del compilatore possano forse aiutare?) Che possono causare un notevole sovraccarico di runtime.
Dominique Devriese,

Grazie, Dominique, ma dalla mia osservazione sembra che la ragione per cui "molti" lo considerano un errore è più sulla falsariga di ciò che altri hanno detto. Ancora una volta, dando uno sguardo nuovo alla covarianza dell'array, è molto più utile che dannoso. Ancora una volta, l'errore BIG reale che Java ha commesso è stato la varianza generica del sito di utilizzo tramite caratteri jolly. Ciò ha causato più problemi di quanto ritenga che molti vogliano ammettere.
Scott,

30

Il motivo è che ogni array conosce il suo tipo di elemento durante il runtime, mentre la raccolta generica non lo fa a causa della cancellazione del tipo.

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 ciò era consentito con raccolte generiche:

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

Ma ciò causerebbe problemi in seguito quando qualcuno tentasse di accedere all'elenco:

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

Penso che la risposta di Paul Bellora sia più appropriata poiché commenta il motivo per cui le matrici sono covarianti. Se le matrici sono state rese invarianti, allora va bene. avresti digitato la cancellazione con esso. Il motivo principale per la proprietà Tipo di cancellazione è la compatibilità con le versioni precedenti è corretta?
eagerto

@ user2708477, sì, la cancellazione del tipo è stata introdotta a causa della compatibilità con le versioni precedenti. E sì, la mia risposta cerca di rispondere alla domanda nel titolo, perché i generici sono invarianti.
Katona,

Il fatto che gli array conoscano il loro tipo significa che mentre la covarianza consente al codice di chiedere di archiviare qualcosa in un array in cui non si adatta, non significa che tale negozio sarà autorizzato. Di conseguenza, il livello di pericolo introdotto dall'avere array di covarianti è molto inferiore a quello che sarebbe se non conoscessero i loro tipi.
supercat,

@supercat, corretto, quello che volevo sottolineare è che per i generici con la cancellazione del tipo in atto, la covarianza non avrebbe potuto essere implementata con la minima sicurezza dei controlli di runtime
Katona,

1
Personalmente ritengo che questa risposta fornisca la giusta spiegazione del perché gli array sono covarianti quando le Collezioni non potrebbero esserlo. Grazie!
chiede il

23

Potrebbe essere questo aiuto: -

I generici non sono covarianti

Le matrici in linguaggio Java sono covarianti, il che significa che se Integer estende il numero (cosa che fa), allora non solo un numero intero è anche un numero, ma un numero intero [] è anche a Number[], e sei libero di passare o assegnare un Integer[]dove Number[]è richiesto a. (Più formalmente, se Number è un supertipo di Integer, allora Number[]è un supertipo di Integer[].) Potresti pensare che lo stesso valga anche per i tipi generici - che List<Number>è un supertipo di List<Integer>e che puoi passare un punto List<Integer>dove List<Number>è previsto. Sfortunatamente, non funziona in questo modo.

Si scopre che c'è una buona ragione per cui non funziona in questo modo: si spezzerebbe il tipo che i generici di sicurezza dovevano fornire. Immagina di poter assegnare List<Integer>a a List<Number>. Quindi il seguente codice ti permetterebbe di inserire qualcosa che non era un numero intero in un List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Perché ln è un List<Number>, l'aggiunta di un float ad esso sembra perfettamente legale. Ma se venissi aliasato li, allora spezzerebbe la promessa di sicurezza del tipo implicita nella definizione di li - che si tratta di un elenco di numeri interi, motivo per cui i tipi generici non possono essere covarianti.


3
Per gli array, ottieni un ArrayStoreExceptionruntime.
Sotirios Delimanolis,

4
la mia domanda è WHY: le matrici sono rese covarianti. come ha detto Sotirios, con gli array si otterrebbe ArrayStoreException in fase di esecuzione, se gli array fossero resi invarianti, allora potremmo rilevare questo errore al momento della compilazione stesso?
eagerto

@eagertoLearn: Una delle principali debolezze semantiche di Java è che nulla nel suo sistema di tipi può distinguere "Matrice che non contiene altro che derivati Animal, che non deve accettare alcun elemento ricevuto da altrove" da "Matrice che non deve contenere altro che Animal, e deve essere disposto ad accettare riferimenti forniti esternamente Animal. Il codice che necessita del primo dovrebbe accettare un array di Cat, ma il codice che necessita del secondo non dovrebbe. Se il compilatore potesse distinguere i due tipi, potrebbe fornire un controllo in fase di compilazione. Sfortunatamente, l'unica cosa che li distingue ...
supercat

... è se il codice tenta effettivamente di archiviare qualcosa al loro interno e non c'è modo di saperlo fino al runtime.
supercat

3

Le matrici sono covarianti per almeno due motivi:

  • È utile per le raccolte che contengono informazioni che non cambieranno mai per essere covarianti. Perché una collezione di T sia covariante, anche il suo negozio di supporto deve essere covariante. Mentre si potrebbe progettare una Tcollezione immutabile che non ha utilizzato un T[]archivio di backup (ad esempio utilizzando un albero o un elenco collegato), è improbabile che tale raccolta si comporti come pure quella supportata da un array. Si potrebbe sostenere che un modo migliore per fornire raccolte immutabili covarianti sarebbe stato quello di definire un tipo di "array immutabile covariante" che avrebbero potuto usare un archivio di supporto, ma probabilmente consentire semplicemente la covarianza dell'array era probabilmente più facile.

  • Le matrici saranno frequentemente mutate da un codice che non sa che tipo di cosa si troverà in esse, ma non inserirà nell'array tutto ciò che non è stato letto da quello stesso array. Un primo esempio di ciò è il codice di ordinamento. Concettualmente potrebbe essere stato possibile 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 oggetto "manipolatore di array" che contiene un riferimento a un array e una o più cose che era stato letto da esso e che poteva includere metodi per memorizzare elementi già letti nell'array da cui erano venuti. Se le matrici non fossero covarianti, il codice utente non sarebbe in grado di definire tale tipo, ma il runtime avrebbe potuto includere alcuni metodi specializzati.

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


1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- buon punto
eagerto

3

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

Con generics, questo viene fatto con i tipi di caratteri jolly:

<E extends Comparable<E>> 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. Nulla di tutto ciò era disponibile al momento dell'aggiunta di matrici a Java e matrici di tipo covariante di tipo di riferimento consentivano un modo molto più semplice per consentire algoritmi polimorfici:

void sort(Comparable[]);

Tuttavia, quella semplicità ha aperto una lacuna nel sistema di tipo statico:

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

che richiede un controllo di runtime di ogni accesso in scrittura a una matrice di tipo di riferimento.

In breve, l'approccio più nuovo incarnato dai generici rende il sistema di tipi più complesso, ma anche più staticamente sicuro, mentre l'approccio più vecchio era più semplice e meno staticamente sicuro. I progettisti del linguaggio hanno optato per un approccio più semplice, avendo cose più importanti da fare rispetto alla chiusura di una piccola lacuna nel sistema di tipi che raramente causa problemi. Più tardi, quando fu creato Java, e le esigenze urgenti prese in carico, avevano le risorse per farlo nel modo giusto per i generici (ma cambiarlo per array avrebbe rotto i programmi Java esistenti).


2

I generici sono invarianti : da JSL 4.10 :

... Il sottotipo non si estende attraverso tipi generici: T <: U non implica che C<T><: C<U>...

e qualche riga in più, JLS spiega anche che gli
array sono covarianti (primo proiettile):

4.10.3 Sottotipizzazione tra i tipi di array

inserisci qui la descrizione dell'immagine


2

Penso che abbiano preso una decisione sbagliata in primo luogo che ha reso l'array covariante. Si rompe la sicurezza del tipo come descritto qui e si sono bloccati a causa della compatibilità con le versioni precedenti e successivamente hanno cercato di non commettere lo stesso errore per i generici. E questo è uno dei motivi per cui Joshua Bloch preferisce gli elenchi agli array nell'articolo 25 del libro "Effective Java (seconda edizione)"


Josh Block è stato l'autore del framework delle collezioni Java (1.2) e l'autore dei generici di Java (1.5). Quindi il ragazzo che ha costruito i generici di cui tutti si lamentano è per coincidenza anche il ragazzo che ha scritto il libro dicendo che sono il modo migliore per andare? Non è una grande sorpresa!
cpurdy

1

La mia opinione: quando il codice si aspetta un array A [] e tu gli dai B [] dove B è una sottoclasse di A, ci sono solo due cose di cui preoccuparti: cosa succede quando leggi un elemento array e cosa succede se scrivi esso. Quindi non è difficile scrivere regole linguistiche per garantire che la sicurezza dei tipi sia preservata in tutti i casi (la regola principale è che si ArrayStoreExceptionpotrebbe lanciare un an se si tenta di inserire un A in un B []). Per un generico, tuttavia, quando dichiari una classe SomeClass<T>, ci possono essere molti modi in cui Tviene usato nel corpo della classe, e immagino che sia semplicemente troppo complicato elaborare tutte le possibili combinazioni su cui scrivere regole su quando le cose sono permesse e quando non lo sono.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.