Qual è il motivo per cui non riesco a creare tipi di array generici in Java?


273

Qual è il motivo per cui Java non ci consente di fare

private T[] elements = new T[initialCapacity];

Potrei capire .NET non ci ha permesso di farlo, poiché in .NET hai tipi di valore che in fase di runtime possono avere dimensioni diverse, ma in Java tutti i tipi di T saranno riferimenti a oggetti, quindi con le stesse dimensioni ( correggimi se sbaglio).

Qual è la ragione?


29
Di cosa stai parlando? Puoi assolutamente farlo in .NET. - Sto qui cercando di capire perché non riesco a farlo in Java.
BrainSlugs83,

@ BrainSlugs83 - aggiungi un link ad alcuni esempi di codice o tutorial che lo dimostrano.
MasterJoe2


1
@ MasterJoe2 il codice sopra nella domanda del PO è ciò a cui mi riferisco. Funziona bene in C #, ma non in Java. - La domanda afferma che non funziona in nessuno dei due, il che non è corretto. - Non sono sicuro che abbia valore nel discuterne ulteriormente.
BrainSlugs83

Risposte:


204

È perché gli array Java (a differenza dei generici) contengono, in fase di esecuzione, informazioni sul tipo di componente. Quindi è necessario conoscere il tipo di componente quando si crea l'array. Dato che non sai cosa Tè in fase di esecuzione, non puoi creare l'array.


29
Ma per quanto riguarda la cancellazione? Perché questo non vale?
Qix - MONICA È STATA MISTREATA il

14
Come fa ArrayList <SomeType>allora?
Thumbz

10
@Thumbz: vuoi dire new ArrayList<SomeType>()? I tipi generici non contengono il parametro type in fase di esecuzione. Il parametro type non viene utilizzato nella creazione. Non vi è alcuna differenza nel codice generato da new ArrayList<SomeType>()o new ArrayList<String>()o new ArrayList()affatto.
nuovo

8
Stavo chiedendo di più su come ArrayList<T>funziona con il suo ' private T[] myArray. Da qualche parte nel codice, deve avere una matrice di tipo T generico, quindi come?
Thumbz,

21
@Thumbz: non ha un array di tipo runtime T[]. Ha una matrice di tipo runtime Object[]e 1) il codice sorgente contiene una variabile di Object[](ecco come è nell'ultima sorgente Oracle Java); oppure 2) il codice sorgente contiene una variabile di tipo T[], che è una bugia, ma non causa problemi a causa della Tcancellazione all'interno dell'ambito della classe.
newacct

137

Citazione:

Le matrici di tipi generici non sono consentite perché non sono valide. Il problema è dovuto all'interazione di array Java, che non sono staticamente validi ma sono controllati dinamicamente, con generici, che sono staticamente validi e non controllati dinamicamente. Ecco come è possibile sfruttare la scappatoia:

class Box<T> {
    final T x;
    Box(T x) {
        this.x = x;
    }
}

class Loophole {
    public static void main(String[] args) {
        Box<String>[] bsa = new Box<String>[3];
        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3); // error not caught by array store check
        String s = bsa[0].x; // BOOM!
    }
}

Avevamo proposto di risolvere questo problema usando array staticamente sicuri (aka Variance) che era stato respinto per Tiger.

- Gafter

(Credo che sia Neal Gafter , ma non ne sono sicuro)

Guardalo nel contesto qui: http://forums.sun.com/thread.jspa?threadID=457033&forumID=316


3
Nota che l'ho trasformato in CW poiché la risposta non è mia.
Bart Kiers,

10
Questo spiega perché potrebbe non essere dilemma. Ma i problemi di sicurezza del tipo potrebbero essere avvertiti dal compilatore. Il fatto è che non è nemmeno possibile farlo, quasi per lo stesso motivo per cui non puoi farlo new T(). Ogni array in Java, in base alla progettazione, memorizza il tipo di componente (cioè T.class) al suo interno; pertanto è necessaria la classe T in fase di esecuzione per creare tale array.
nuovo

2
Puoi ancora usare new Box<?>[n], che a volte potrebbe essere sufficiente, anche se non sarebbe di aiuto nel tuo esempio.
Bartosz Klimek il

1
@BartKiers Non capisco ... questo non si compila ancora (java-8): Box<String>[] bsa = new Box<String>[3];qualcosa è cambiato in java-8 e su suppongo?
Eugene,

1
@Eugene, le matrici di tipi generici specifici semplicemente non sono consentite perché possono portare a una perdita di sicurezza del tipo, come dimostrato nel campione. Non è consentito in nessuna versione di Java. La risposta inizia come "Le matrici di tipi generici non sono consentite perché non sono valide".
granato

47

Non riuscendo a fornire una soluzione decente, si finisce con qualcosa di peggio di IMHO.

Il lavoro comune è il seguente.

T[] ts = new T[n];

viene sostituito con (supponendo che T estende Object e non un'altra classe)

T[] ts = (T[]) new Object[n];

Preferisco il primo esempio, tuttavia più tipi accademici sembrano preferire il secondo, o semplicemente preferiscono non pensarci.

La maggior parte degli esempi del perché non si può semplicemente usare un Object [] si applicano ugualmente a List o Collection (che sono supportati), quindi li vedo come argomenti molto poveri.

Nota: questo è uno dei motivi per cui la libreria Collezioni non viene compilata senza avvisi. Se questo caso d'uso non può essere supportato senza avvertimenti, qualcosa è fondamentalmente rotto con il modello generico IMHO.


6
Devi stare attento con il secondo. Se restituisci l'array creato in modo tale a qualcuno che si aspetta, per esempio, un String[](o se lo memorizzi in un campo accessibile pubblicamente di tipo T[]e qualcuno lo recupera), otterrà una ClassCastException.
Newacct,

4
Ho votato a fondo questa risposta perché il tuo esempio preferito non è consentito in Java e il tuo secondo esempio potrebbe generare ClassCastException
José Roberto Araújo Júnior

5
@ JoséRobertoAraújoJúnior È abbastanza chiaro che il primo esempio deve essere sostituito con il secondo esempio. Sarebbe più utile per te spiegare perché il secondo esempio può generare ClassCastException in quanto non sarebbe ovvio per tutti.
Peter Lawrey,

3
@PeterLawrey Ho creato una domanda con risposta autonoma che mostra perché T[] ts = (T[]) new Object[n];è una cattiva idea: stackoverflow.com/questions/21577493/…
José Roberto Araújo Júnior,

1
@MarkoTopolnik Dovrei ricevere una medaglia per aver risposto a tutti i tuoi commenti per spiegare la stessa cosa che ho già detto, l'unica cosa che è cambiata rispetto alla mia ragione originale è che ho pensato che T[] ts = new T[n];fosse un valido esempio. Continuerò a votare perché la sua risposta può causare problemi e confusioni ad altri sviluppatori ed è anche fuori tema. Inoltre, smetterò di commentare questo.
José Roberto Araújo Júnior,

38

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 To 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 ArrayStoreExceptionperché 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


32

Il motivo per cui ciò è impossibile è che Java implementa i suoi generici esclusivamente a livello di compilatore e per ogni classe viene generato un solo file di classe. Questo si chiama Type Erasure .

In fase di runtime, la classe compilata deve gestire tutti i suoi usi con lo stesso bytecode. Quindi, new T[capacity]non avrei assolutamente idea di quale tipo debba essere istanziato.


17

La risposta è già stata fornita, ma se hai già un'istanza di T, puoi farlo:

T t; //Assuming you already have this object instantiated or given by parameter.
int length;
T[] ts = (T[]) Array.newInstance(t.getClass(), length);

Spero di poterti aiutare, Ferdi265


1
Questa è una bella soluzione. Ma questo riceverà avvisi non controllati (trasmessi da Object a T []). Un'altra soluzione, ma "più lento" "senza preavviso" potrebbe essere: T[] ts = t.clone(); for (int i=0; i<ts.length; i++) ts[i] = null;.
mezzanotte,

1
Inoltre, se ciò che abbiamo mantenuto è T[] t, lo sarebbe (T[]) Array.newInstance(t.getClass().getComponentType(), length);. ho trascorso alcune volte a capire getComponentType(). Spero che questo aiuti gli altri.
mezzogiorno,

1
@midnite t.clone()non tornerà T[]. Perché tnon è Array in questa risposta.
Xmen,

6

Il motivo principale è dovuto al fatto che gli array in Java sono covarianti.

C'è una buona panoramica qui .


Non vedo come si possa supportare "new T [5]" anche con array invarianti.
Dimitris Andreou,

2
@DimitrisAndreou Bene, l'intera faccenda è piuttosto una commedia di errori nel design Java. Tutto è iniziato con la covarianza dell'array. Poi, una volta che hai matrice di covarianza, si può lanciare String[]per Objecte memorizzare una Integerin esso. Quindi hanno dovuto aggiungere un controllo del tipo di runtime per i negozi di array ( ArrayStoreException) perché non è stato possibile rilevare il problema in fase di compilazione. (Altrimenti, un Integereffettivamente potrebbe essere bloccato in un String[], e si otterrebbe un errore quando si tenta di recuperarlo, il che sarebbe orribile.) ...
Radon Rosborough,

2
@DimitrisAndreou ... Quindi, una volta eseguito un controllo di runtime al posto di un controllo di compilazione più ecologico, si verifica la cancellazione del tipo (anche un difetto di progettazione sfortunato - incluso solo per compatibilità con le versioni precedenti). La cancellazione del tipo significa che non è possibile eseguire controlli del tipo di runtime per tipi generici. Pertanto, per evitare il problema del tipo di archiviazione dell'array, non è possibile disporre di array generici. Se in primo luogo avessero semplicemente reso invarianti le matrici, avremmo potuto semplicemente effettuare controlli di tipo in fase di compilazione senza eseguire una violazione della cancellazione.
Radon Rosborough,

... Ho appena scoperto il periodo di modifica di cinque minuti per i commenti. Objectavrebbe dovuto essere Object[]nel mio primo commento.
Radon Rosborough,

3

Mi piace la risposta indirettamente data da Gafter . Tuttavia, propongo che sia sbagliato. Ho cambiato un po 'il codice di Gafter. Si compila e funziona per un po ', poi bombe dove Gafter aveva previsto

class Box<T> {

    final T x;

    Box(T x) {
        this.x = x;
    }
}

class Loophole {

    public static <T> T[] array(final T... values) {
        return (values);
    }

    public static void main(String[] args) {

        Box<String> a = new Box("Hello");
        Box<String> b = new Box("World");
        Box<String> c = new Box("!!!!!!!!!!!");
        Box<String>[] bsa = array(a, b, c);
        System.out.println("I created an array of generics.");

        Object[] oa = bsa;
        oa[0] = new Box<Integer>(3);
        System.out.println("error not caught by array store check");

        try {
            String s = bsa[0].x;
        } catch (ClassCastException cause) {
            System.out.println("BOOM!");
            cause.printStackTrace();
        }
    }
}

L'output è

I created an array of generics.
error not caught by array store check
BOOM!
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
    at Loophole.main(Box.java:26)

Quindi mi sembra che tu possa creare tipi di array generici in Java. Ho frainteso la domanda?


Il tuo esempio è diverso da quello che ti ho chiesto. Quello che hai descritto sono i pericoli della covarianza dell'array. Dai un'occhiata (per .NET: blogs.msdn.com/b/ericlippert/archive/2007/10/17/… )
divorato elysium

Spero che tu riceva un avviso di sicurezza del tipo dal compilatore, sì?
Matt McHenry,

1
Sì, ricevo un avviso di sicurezza del tipo. Sì, vedo che il mio esempio non risponde alla domanda.
emory

In realtà ricevi più avvisi a causa dell'inizializzazione sciatta di a, b, c. Inoltre, questo è ben noto e influisce sulla libreria principale, ad esempio <T> java.util.Arrays.asList (T ...). Se passi qualsiasi tipo non riutilizzabile per T, ricevi un avviso (perché l'array creato ha un tipo meno preciso di quello che il codice pretende) ed è super brutto. Sarebbe meglio se l'autore di questo metodo ricevesse l'avvertimento, invece di emetterlo nel sito di utilizzo, dato che il metodo stesso è sicuro, non espone l'array all'utente.
Dimitris Andreou,

1
Non hai creato un array generico qui. Il compilatore ha creato un array (non generico) per te.
nuovo

2

So di essere un po 'in ritardo alla festa qui, ma ho pensato che potrei essere in grado di aiutare eventuali googler futuri poiché nessuna di queste risposte ha risolto il mio problema. La risposta di Ferdi265 ha aiutato immensamente però.

Sto cercando di creare il mio elenco collegato, quindi il seguente codice è quello che ha funzionato per me:

package myList;
import java.lang.reflect.Array;

public class MyList<TYPE>  {

    private Node<TYPE> header = null;

    public void clear() {   header = null;  }

    public void add(TYPE t) {   header = new Node<TYPE>(t,header);    }

    public TYPE get(int position) {  return getNode(position).getObject();  }

    @SuppressWarnings("unchecked")
    public TYPE[] toArray() {       
        TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());        
        for(int i=0 ; i<size() ; i++)   result[i] = get(i); 
        return result;
    }


    public int size(){
         int i = 0;   
         Node<TYPE> current = header;
         while(current != null) {   
           current = current.getNext();
           i++;
        }
        return i;
    }  

Nel metodo toArray () si trova il modo di creare una matrice di tipo generico per me:

TYPE[] result = (TYPE[])Array.newInstance(header.getObject().getClass(),size());    

2

Nel mio caso, volevo semplicemente una serie di pile, qualcosa del genere:

Stack<SomeType>[] stacks = new Stack<SomeType>[2];

Poiché ciò non è stato possibile, ho usato il seguente come soluzione alternativa:

  1. Creata una classe wrapper non generica attorno a Stack (ad esempio MyStack)
  2. MyStack [] stacks = new MyStack [2] ha funzionato perfettamente

Brutto, ma Java è felice.

Nota: come menzionato da BrainSlugs83 nel commento alla domanda, è totalmente possibile avere array di generici in .NET


2

Dal tutorial Oracle :

Non è possibile creare matrici di tipi con parametri. Ad esempio, il seguente codice non viene compilato:

List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error

Il codice seguente mostra cosa succede quando si inseriscono tipi diversi in un array:

Object[] strings = new String[2];
strings[0] = "hi";   // OK
strings[1] = 100;    // An ArrayStoreException is thrown.

Se provi la stessa cosa con un elenco generico, ci sarebbe un problema:

Object[] stringLists = new List<String>[];  // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>();   // OK
stringLists[1] = new ArrayList<Integer>();  // An ArrayStoreException should be thrown,
                                            // but the runtime can't detect it.

Se fossero consentite matrici di elenchi parametrizzati, il codice precedente non genererebbe ArrayStoreException desiderato.

A me sembra molto debole. Penso che chiunque abbia una comprensione sufficiente dei generici, starebbe benissimo, e persino si aspetterebbe, che ArrayStoredException non venga lanciato in questo caso.


0

Deve esserci sicuramente un buon modo per aggirarlo (forse usando la riflessione), perché mi sembra che sia esattamente quello che ArrayList.toArray(T[] a)fa. Quoto:

public <T> T[] toArray(T[] a)

Restituisce un array contenente tutti gli elementi in questo elenco nell'ordine corretto; il tipo di runtime dell'array restituito è quello dell'array specificato. Se l'elenco si adatta all'array specificato, viene restituito al suo interno. In caso contrario, viene assegnato un nuovo array con il tipo di runtime dell'array specificato e le dimensioni di questo elenco.

Quindi un modo per aggirarlo sarebbe usare questa funzione, cioè creare uno ArrayListdegli oggetti desiderati nell'array, quindi usare toArray(T[] a)per creare l'array reale. Non sarebbe veloce, ma non hai menzionato le tue esigenze.

Qualcuno sa come toArray(T[] a)viene implementato?


3
List.toArray (T []) funziona perché in sostanza gli stai assegnando il tipo di componente T in fase di esecuzione (gli stai fornendo un'istanza del tipo di array desiderato, da cui può ottenere la classe di array e quindi la classe di componenti T ). Con il tipo di componente effettivo in fase di runtime, è sempre possibile creare una matrice di quel tipo di runtime utilizzando Array.newInstance(). Troverai quello menzionato in molte domande che chiedono come creare un array con un tipo sconosciuto al momento della compilazione. Ma l'OP stava chiedendo specificamente perché non è possibile utilizzare la new T[]sintassi, che è una domanda diversa
newacct

0

È perché i generici sono stati aggiunti a java dopo averlo creato, quindi è un po 'goffo perché i produttori originali di java hanno pensato che quando si creava un array il tipo sarebbe stato specificato nella sua realizzazione. In modo che non funzioni con generici quindi devi fare E [] array = (E []) new Object [15]; Questo compila ma dà un avvertimento.


0

Se non possiamo istanziare array generici, perché la lingua ha tipi di array generici? Qual è il punto di avere un tipo senza oggetti?

L'unica ragione che mi viene in mente è varargs - foo(T...). Altrimenti avrebbero potuto cancellare completamente i tipi di array generici. (Beh, non avevano davvero bisogno di usare l'array per varargs, dato che varargs non esisteva prima della 1.5 . Questo è probabilmente un altro errore.)

Quindi è una bugia, puoi istanziare array generici, attraverso varargs!

Naturalmente, i problemi con le matrici generiche sono ancora reali, ad es

static <T> T[] foo(T... args){
    return args;
}
static <T> T[] foo2(T a1, T a2){
    return foo(a1, a2);
}

public static void main(String[] args){
    String[] x2 = foo2("a", "b"); // heap pollution!
}

Possiamo usare questo esempio per dimostrare effettivamente il pericolo dell'array generico .

D'altra parte, usiamo vararg generici da un decennio e il cielo non sta ancora calando. Quindi possiamo sostenere che i problemi vengono esagerati; non è un grosso problema. Se è consentita la creazione esplicita di array generici, avremo bug qua e là; ma siamo stati abituati ai problemi di cancellazione e possiamo conviverci.

E possiamo indicare di foo2confutare l'affermazione secondo cui le specifiche ci impediscono di risolvere i problemi da cui sostengono. Se Sun avesse avuto più tempo e risorse per 1,5 , credo che avrebbero potuto raggiungere una risoluzione più soddisfacente.


0

Come altri hanno già detto, puoi ovviamente creare alcuni trucchi .

Ma non è raccomandato

Poiché la cancellazione del tipo e, soprattutto, l' covariancearray in che consente solo un array di sottotipi può essere assegnato a un array di supertipi, il che ti costringe a utilizzare il cast di tipi espliciti quando tenti di ottenere il valore causando il runtime ClassCastExceptionche è uno degli obiettivi principali che i generici cercano di eliminare: controlli di tipo più forti al momento della compilazione .

Object[] stringArray = { "hi", "me" };
stringArray[1] = 1;
String aString = (String) stringArray[1]; // boom! the TypeCastException

Un esempio più diretto può essere trovato in Effective Java: Item 25 .


covarianza : una matrice di tipo S [] è un sottotipo di T [] se S è un sottotipo di T


0

Se la classe utilizza come tipo con parametri, può dichiarare una matrice di tipo T [], ma non può creare un'istanza diretta di tale matrice. Invece, un approccio comune è quello di creare un'istanza di una matrice di tipo Object [], quindi creare un cast restrittivo per digitare T [], come mostrato di seguito:

  public class Portfolio<T> {
  T[] data;
 public Portfolio(int capacity) {
   data = new T[capacity];                 // illegal; compiler error
   data = (T[]) new Object[capacity];      // legal, but compiler warning
 }
 public T get(int index) { return data[index]; }
 public void set(int index, T element) { data[index] = element; }
}

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.