2012-05-26 7 views
16

supporre che Cane classe estende Animal classe: perché questa affermazione polimorfa non è consentito:perché il polimorfismo non tratta le raccolte generiche e gli array semplici allo stesso modo?

List<Animal> myList = new ArrayList<Dog>(); 

Tuttavia, è consentito con gli array semplici:

Animal[] x=new Dog[3]; 
+6

Ci sono quelli che sostengono che gli array che consentono di fare che è stata una cattiva idea. – Jeffrey

+3

Tipo di cancellazione e generici, non raccolte, sono la ragione. – duffymo

+1

La risposta breve è "i contenitori generici non sono matrici". La risposta più lunga, come suggerito da Duffymo alreyad, è "cancellazioni": http://code.stephenmorley.org/articles/java-generics-type-erasure/ – paulsm4

risposta

21

Le ragioni di questo sono basate su come Java implementa i generici.

Un Array Esempio

Con gli array si può fare questo (gli array sono covariante come altri hanno spiegato)

Integer[] myInts = {1,2,3,4}; 
Number[] myNumber = myInts; 

Ma, che cosa accadrebbe se si tenta di fare questo?

Number[0] = 3.14; //attempt of heap pollution 

Questa ultima riga sarebbe compilare bene, ma se si esegue questo codice, si potrebbe ottenere un ArrayStoreException. Perché stai cercando di mettere un doppio in un array intero (indipendentemente dall'accesso tramite un riferimento numerico).

Ciò significa che è possibile ingannare il compilatore, ma non è possibile ingannare il sistema di tipo runtime. E questo è così perché gli array sono quelli che noi chiamiamo tipi reificabili. Ciò significa che in fase di esecuzione Java sa che questa matrice è stata effettivamente istanziata come una matrice di numeri interi a cui semplicemente si accede tramite un riferimento di tipo Number[].

Quindi, come puoi vedere, una cosa è il tipo effettivo dell'oggetto, un'altra cosa è il tipo di riferimento che usi per accedervi, giusto?

Il problema con Java Generics

Ora, il problema con Java tipi generici è che le informazioni sul tipo viene scartato dal compilatore e non è disponibile in fase di esecuzione. Questo processo è chiamato type erasure. Ci sono buone ragioni per implementare generici come questo in Java, ma questa è una lunga storia e ha a che fare con la compatibilità binaria con il codice preesistente.

Ma il punto importante qui è che, in fase di esecuzione non ci sono informazioni sul tipo, non c'è modo di garantire che non stiamo commettendo inquinamento da cumulo.

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 polution 

Se il compilatore Java non ti impedisce di fare questo, il sistema di tipo runtime non può fermare neanche, perché non c'è modo, in fase di esecuzione, per determinare che questo elenco è stato dovrebbe essere solo una lista di interi. Il runtime Java ti consente di inserire tutto ciò che vuoi in questo elenco, quando deve contenere solo numeri interi, perché quando è stato creato, è stato dichiarato come un elenco di numeri interi.

Come tale, i progettisti di Java hanno fatto in modo che non si potesse ingannare il compilatore. Se non puoi ingannare il compilatore (come possiamo fare con gli array) non puoi ingannare neanche il sistema di tipo runtime.

Come tale, diciamo che i tipi generici sono non riscrivibili.

Evidentemente, questo ostacolerebbe il polimorfismo.Si consideri il seguente esempio:

static long sum(Number[] numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Ora si potrebbe usare 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 le collezioni generiche, non si avrà successo:

static long sum(List<Number> numbers) { 
    long summation = 0; 
    for(Number number : numbers) { 
     summation += number.longValue(); 
    } 
    return summation; 
} 

Si otterrebbero degli errori del compilatore se si tenta di ...

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 utilizzare due potenti funzionalità dei generici Java noti come covarianza e controvarianza.

covarianza

Con covarianza è possibile leggere gli elementi da una struttura, ma non si può scrivere nulla in esso. 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 si può leggere da myNums:

Number n = myNums.get(0); 

Perché si può essere sicuri che tutto ciò che ha l'elenco contiene, può essere upcasted ad un numero (dopo tutto tutto ciò che si estende numero è un numero , giusto?)

Tuttavia, non è consentito inserire nulla in una struttura covariante.

myNumst.add(45L); //compiler error 

questo non sarebbe consentito, perché Java non può garantire quello che è il tipo reale 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 controvarianza si può fare il contrario. Puoi mettere le cose in una struttura generica, ma non puoi leggerne.

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 reale dell'oggetto è una lista di oggetti, e attraverso controvarianza, è possibile mettere i numeri in esso, fondamentalmente perché tutti i numeri abbiano oggetto come il loro antenato comune. In quanto tale, tutti i numeri sono oggetti, e quindi questo è valido.

Tuttavia, non è possibile leggere in modo sicuro nulla da questa struttura controvariante presumendo che si otterrà un numero.

Number myNum = myNums.get(0); //compiler-error 

Come si può vedere, se il compilatore ha permesso di scrivere questa linea, si otterrebbe un ClassCastException in fase di esecuzione.

Get/Put Principio

Come tale, utilizzare covarianza quando hai solo intenzione di prendere valori generici di una struttura, utilizzare controvarianza quando si intende solo per mettere i valori generici in una struttura e utilizzare l'esatto generica scrivi quando intendi fare entrambe le cose.

L'esempio migliore che ho è il seguente che copia qualsiasi tipo di numero da una lista in un'altra lista. Solo riceve elementi dalla fonte e solo mette gli oggetti nel destino.

public static void copy(List<? extends Number> source, List<? super Number> destiny) { 
    for(Number number : source) { 
     destiny.add(number); 
    } 
} 

Grazie ai poteri di covarianza e controvarianza Questo 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); 
1

Questo è molto interessante. Non posso dire la risposta, ma questo funziona se si vuole mettere un elenco di cani nella lista degli animali:

List<Animal> myList = new ArrayList<Animal>(); 
myList.addAll(new ArrayList<Dog>()); 
+1

Questo funziona perché 'addAll()' scorre sulla raccolta che viene passata e chiama 'List.add()' su ogni elemento della collezione. E un 'Elenco ' può avere assolutamente elementi che siano una sottoclasse di 'Animal'. – QuantumMechanic

+0

@QuantumMechanic Sì, sapevo perché funziona, l'ho appena postato nel caso in cui fosse bloccato su una soluzione necessaria. –

1

Il modo per codificare la versione collezioni in modo che compila è:

List<? extends Animal> myList = new ArrayList<Dog>(); 

Il motivo per cui non è necessario questo con gli array è dovuto al tipo di cancellazione: gli array di non primitivi sono tutti solo Object[] e gli array java non sono una classe tipizzata (come le raccolte). Il linguaggio non è mai stato progettato per soddisfarlo.

Gli array e i generici non si mescolano.

+0

Questo suona per me, ma forse sto interpretando male ciò che stai dicendo. Dici che "gli array di non primitivi sono tutti solo Object [] e gli array java non sono una classe tipizzata (come le collezioni)", ma non è il contrario? Gli array Java mantengono il loro tipo in fase di runtime, ma le collezioni non sono dovute alla cancellazione dei tipi. –

5

Le matrici differiscono dai tipi generici in due modi importanti. Innanzitutto, gli array sono covarianti. Questa parola dal suono spaventoso significa semplicemente che se Sub è un sottotipo di Super, quindi il tipo di array Sub [] è un sottotipo di Super []. I generici, al contrario, sono invarianti: per due tipi distinti Tipo1 e Tipo2, Lista <Tipo1> non è né un sottotipo né un tipo Elenco <Tipo2>.

[..] La seconda differenza principale tra array e generici è che gli array sono reificato [JLS, 4.7]. Ciò significa che gli array conoscono e applicano i loro tipi di elementi al runtime .

[..] Generics, al contrario, sono implementati dalla cancellazione [JLS, 4.6]. Ciò significa che applicano i loro vincoli di tipo solo al momento della compilazione e scarta (o cancella) le informazioni sul tipo di elemento in fase di runtime. La cancellazione è ciò che consente ai tipi generici di interagire liberamente con codice legacy che non utilizza i generici (elemento 23). A causa di queste differenze fondamentali, array e generici non mescolano bene . Ad esempio, è illegale creare una matrice di un tipo generico, un tipo parametrizzato o un parametro di tipo. Nessuna di queste espressioni di creazione dell'array è legale: new Elenco <E> [], nuovo elenco <Stringa> [], nuova E []. Tutto si tradurrà in errori di creazione di array generici in fase di compilazione. [..]

Prentice Hall - Effective Java 2nd Edition

1
List<Animal> myList = new ArrayList<Dog>(); 

non è possibile perché in questo caso si potrebbe mettere i gatti in cani:

private void example() { 
    List<Animal> dogs = new ArrayList<Dog>(); 
    addCat(dogs); 
    // oops, cat in dogs here 
} 

private void addCat(List<Animal> animals) { 
    animals.add(new Cat()); 
} 

D'altra parte

List<? extends Animal> myList = new ArrayList<Dog>(); 

è possibile, ma in questo caso non è possibile utilizzare metodi con paramteres generici (solo nulla è accettata):

private void addCat(List<? extends Animal> animals) { 
    animals.add(null);  // it's ok 
    animals.add(new Cat()); // compilation error here 
} 
1

La risposta finale è che è così perché Java è stato specificato in quel modo. Più precisamente, perché questo è il modo in cui la specifica Java si è evoluta *.

Noi non si può dire ciò che il pensiero reale dei progettisti di Java era, ma considerare questo:

List<Animal> myList = new ArrayList<Dog>(); 
myList.add(new Cat()); // compilation error 

contro

Animal[] x = new Dog[3]; 
x[0] = new Cat();  // runtime error 

L'errore di runtime che verrà gettato qui è ArrayStoreException. Questo potrebbe potenzialmente essere gettato su qualsiasi incarico a qualsiasi array di non primitivi.

Uno potrebbe rendere un caso che la gestione di tipi di array di Java è errata ... a causa di esempi come quello sopra.

* Si noti che la digitazione degli array Java è stata specificata prima di Java 1.0, ma i tipi generici sono stati aggiunti solo in Java 1.5. Il linguaggio Java ha una metadata over-arching di retrocompatibilità; Ad esempio, le estensioni del linguaggio non dovrebbero rompere il vecchio codice. Tra le altre cose, ciò significa che non è possibile correggere errori storici, come il modo in cui funziona la digitazione della matrice. (Supponendo che si accetta che era un errore ...)


Sul lato tipo generico, digitare la cancellazione des non spiega l'errore di compilazione. L'errore di compilazione si verifica effettivamente a causa del controllo del tipo di compilazione che utilizza i tipi generici non cancellati.

E infatti, è possibile sovvertire l'errore di compilazione utilizzando un deseleziona typecast (ignora l'avviso) e finire in una situazione in cui lo ArrayList<Dog> contiene effettivamente oggetti Cat in fase di esecuzione. (Che è una conseguenza della cancellazione dei tipi!) Ma attenzione, che la sovversione degli errori di compilazione che utilizza una conversione non controllata può portare a errori di runtime in luoghi imprevisti ... se si sbaglia. Ecco perché è una cattiva idea.

0

Nei giorni precedenti i generici, scrivere una routine che potesse ordinare array di tipo arbitrario avrebbe richiesto di essere in grado di (1) creare array di sola lettura in modo covariante e scambiare o modificare gli elementi in modalità indipendente dal tipo, o (2) creare matrici di read-write in modo covariante che possono essere lette in modo sicuro, e possono essere scritte in modo sicuro con elementi letti in precedenza dallo stesso array, oppure (3) avere array che forniscono alcuni mezzi indipendenti dal tipo per confrontare elementi.Se le interfacce generiche covarianti e controvarianti erano state incluse nella lingua dall'inizio, il primo approccio avrebbe potuto essere il migliore, dal momento che avrebbe evitato la necessità di eseguire il controllo dei tipi in fase di esecuzione e la possibilità che tali controlli di tipo potrebbe fallire. Tuttavia, dal momento che tale supporto generico non esisteva, non esisteva nulla in cui un array di tipo derivato potesse essere gettato in modo sensato su un array diverso da un tipo di base.

Problemi correlati