Cancellazione di tipo generico Java: quando e cosa succede?

Ho letto della cancellazione del tipo di Java sul sito Web di Oracle .

Quando si verifica la cancellazione del tipo? In fase di compilazione o runtime? Quando viene caricata la class? Quando la class viene istanziata?

Molti siti (incluso il tutorial ufficiale sopra menzionato) dicono che la cancellazione dei tipi avviene al momento della compilazione. Se le informazioni sul tipo vengono completamente rimosse in fase di compilazione, in che modo JDK controlla la compatibilità del tipo quando viene invocato un metodo che utilizza i generici senza informazioni di tipo o informazioni di tipo errate?

Considera il seguente esempio: Supponiamo che la class A abbia un metodo, empty(Box b) . Compiliamo A.java e otteniamo il file di class A.class .

 public class A { public static void empty(Box b) {} } 
 public class Box {} 

Ora creiamo un’altra class B che richiama il metodo empty con un argomento non parametrizzato (tipo non elaborato): empty(new Box()) . Se compiliamo B.java con A.class nel classpath, javac è abbastanza intelligente da generare un avviso. Quindi A.class ha alcune informazioni di tipo memorizzate in esso.

     public class B { public static void invoke() { // java: unchecked method invocation: // method empty in class A is applied to given types // required: Box // found: Box // java: unchecked conversion // required: Box // found: Box A.empty(new Box()); } } 

    La mia ipotesi sarebbe che la cancellazione del tipo si verifica quando la class viene caricata, ma è solo un’ipotesi. Quindi quando succede?

    La cancellazione di tipo si applica all’uso di farmaci generici. Esistono sicuramente metadati nel file di class per dire se un metodo / tipo è generico e quali sono i vincoli, ecc. Ma quando vengono utilizzati i generici, vengono convertiti in controlli in fase di compilazione e cast di esecuzione. Quindi questo codice:

     List list = new ArrayList(); list.add("Hi"); String x = list.get(0); 

    è compilato in

     List list = new ArrayList(); list.add("Hi"); String x = (String) list.get(0); 

    Al momento dell’esecuzione non c’è modo di scoprire che T=String per l’object lista – quell’informazione è andata.

    … ma l’interfaccia List stessa si pubblicizza ancora come generica.

    EDIT: Giusto per chiarire, il compilatore mantiene le informazioni sulla variabile come List – ma non è ancora ansible scoprire che T=String per l’object list stesso.

    Il compilatore è responsabile della comprensione di Generics in fase di compilazione. Il compilatore è anche responsabile di buttar via questa “comprensione” delle classi generiche, in un processo che chiamiamo cancellazione di tipo . Tutto accade in fase di compilazione.

    Nota: contrariamente alle credenze della maggioranza degli sviluppatori Java, è ansible mantenere le informazioni sul tipo in fase di compilazione e recuperare queste informazioni in fase di esecuzione, nonostante in un modo molto limitato. In altre parole: Java fornisce generici reificati in un modo molto limitato .

    Riguardo alla cancellazione del tipo

    Si noti che, in fase di compilazione, il compilatore dispone di informazioni complete sul tipo, ma questa informazione viene intenzionalmente eliminata in generale quando viene generato il codice byte, in un processo noto come cancellazione del tipo . Questo è stato fatto in questo modo a causa di problemi di compatibilità: l’intenzione dei progettisti linguistici era fornire compatibilità completa del codice sorgente e compatibilità completa del codice byte tra le versioni della piattaforma. Se è stato implementato in modo diverso, è necessario ricompilare le applicazioni legacy durante la migrazione a versioni più recenti della piattaforma. Il modo in cui è stato eseguito, tutte le firme dei metodi vengono mantenute (compatibilità del codice sorgente) e non è necessario ricompilare nulla (compatibilità binaria).

    Riguardo ai generici reificati in Java

    Se è necessario conservare le informazioni sul tipo in fase di compilazione, è necessario utilizzare classi anonime. Il punto è: nel caso molto speciale di classi anonime, è ansible recuperare informazioni di tipo full compile-time in fase di esecuzione che, in altre parole, significano: generici reificati. Ciò significa che il compilatore non cancella le informazioni sul tipo quando sono coinvolte classi anonime; questa informazione viene mantenuta nel codice binario generato e il sistema di runtime consente di recuperare queste informazioni.

    Ho scritto un articolo su questo argomento:

    http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

    Una nota sulla tecnica descritta nell’articolo precedente è che la tecnica è oscura per la maggior parte degli sviluppatori. Nonostante funzioni e funzioni bene, molti sviluppatori si sentono confusi o a disagio con la tecnica. Se hai un codice base condiviso o hai intenzione di rilasciare il tuo codice al pubblico, non consiglio la tecnica di cui sopra. D’altra parte, se sei l’unico utente del tuo codice, puoi sfruttare la potenza che questa tecnica ti offre.

    Codice d’esempio

    L’articolo sopra ha collegamenti al codice di esempio.

    Se si dispone di un campo di tipo generico, i relativi parametri di tipo vengono compilati nella class.

    Se si dispone di un metodo che accetta o restituisce un tipo generico, tali parametri di tipo vengono compilati nella class.

    Questa informazione è ciò che il compilatore usa per dirti che non puoi passare una Box al metodo empty(Box) .

    L’API è complicata, ma è ansible ispezionare questo tipo di informazioni tramite l’API di reflection con metodi come getGenericParameterTypes , getGenericReturnType e, per i campi, getGenericType .

    Se si dispone di codice che utilizza un tipo generico, il compilatore inserisce i cast come necessario (nel chiamante) per controllare i tipi. Gli oggetti generici stessi sono solo il tipo grezzo; il tipo parametrizzato è “cancellato”. Quindi, quando si crea una new Box() , non vi sono informazioni sulla class Integer nell’object Box .

    Le FAQ di Angelika Langer sono il miglior riferimento che ho visto per Java Generics.

    Generics in Java Language è un’ottima guida su questo argomento.

    I generici sono implementati dal compilatore Java come una conversione front-end chiamata cancellazione. Puoi (quasi) pensare ad esso come una traduzione da sorgente a sorgente, per cui la versione generica di loophole() viene convertita nella versione non generica.

    Quindi, è in fase di compilazione. La JVM non saprà mai quale ArrayList hai usato.

    Raccomando anche la risposta del signor Skeet su Qual è il concetto di cancellazione nei generici in Java?

    Il tipo di cancellazione si verifica in fase di compilazione. Che tipo di cancellazione significa è che si dimenticherà del tipo generico, non di ogni tipo. Inoltre, ci saranno ancora metadati sui tipi generici. Per esempio

     Box b = new Box(); String x = b.getDefault(); 

    è convertito in

     Box b = new Box(); String x = (String) b.getDefault(); 

    al momento della compilazione. Potresti ricevere avvertimenti non perché il compilatore sa di che tipo è generico, ma, al contrario, perché non ne sa abbastanza, quindi non può garantire la sicurezza del tipo.

    Inoltre, il compilatore conserva le informazioni sul tipo dei parametri su una chiamata al metodo, che è ansible recuperare tramite riflessione.

    Questa guida è la migliore che ho trovato sull’argomento.

    Ho incontrato con cancellazione di tipo in Android. Nella produzione usiamo gradle con l’opzione minify. Dopo la minificazione ho un’eccezione fatale. Ho reso semplice la funzione per mostrare la catena di ereditarietà del mio object:

     public static void printSuperclasss(Class clazz) { Type superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); while (superClass != null && clazz != null) { clazz = clazz.getSuperclass(); superClass = clazz.getGenericSuperclass(); Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName())); Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString())); } } 

    E ci sono due risultati di questa funzione:

    Codice non miniato:

     D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: android.support.v7.util.SortedList$Callback D/Reflection: this class: android.support.v7.util.SortedList$Callback D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

    Codice miniato:

     D/Reflection: this class: com.example.App.UsersList D/Reflection: superClass: class com.example.App.SortedListWrapper D/Reflection: this class: com.example.App.SortedListWrapper D/Reflection: superClass: class android.support.v7.ge D/Reflection: this class: android.support.v7.ge D/Reflection: superClass: class java.lang.Object D/Reflection: this class: java.lang.Object D/Reflection: superClass: null 

    Quindi, nel codice minificato le classi parametrizzate effettive vengono sostituite con tipi di classi raw senza alcuna informazione di tipo. Come soluzione per il mio progetto ho rimosso tutte le chiamate di riflessione e le ho sostituite con tipi params espliciti passati in argomenti di funzione.

    Il termine “cancellazione di tipo” non è in realtà la descrizione corretta del problema di Java con i generici. La cancellazione di tipo non è di per sé una cosa negativa, anzi è molto necessaria per le prestazioni ed è spesso utilizzata in diverse lingue come C ++, Haskell, D.

    Prima di provare disgusto, ricorda la corretta definizione di cancellazione di tipo da Wiki

    Cos’è la cancellazione del tipo?

    la cancellazione di tipo fa riferimento al processo di caricamento in base al quale le annotazioni di tipo esplicito vengono rimosse da un programma, prima che venga eseguito in fase di esecuzione

    Digitare cancellazione significa eliminare tag di tipo creati in fase di progettazione o tag di tipo inferito in fase di compilazione in modo tale che il programma compilato nel codice binario non contenga alcun tipo di tag. E questo è il caso per ogni linguaggio di programmazione che compila in codice binario, tranne in alcuni casi in cui è necessario tag di runtime. Queste eccezioni includono, ad esempio, tutti i tipi esistenziali (tipi di riferimento Java che sono sottotipo, Qualsiasi tipo in molte lingue, Tipi unione). Il motivo della cancellazione dei tipi è che i programmi vengono trasformati in un linguaggio che è in qualche modo univoco (linguaggio binario che consente solo bit) poiché i tipi sono solo astrazioni e asseriscono una struttura per i suoi valori e la semantica appropriata per gestirli.

    Quindi questo è in cambio, una cosa naturale normale.

    Il problema di Java è diverso e causato da come viene reificato.

    Anche le affermazioni spesso fatte su Java non hanno generici reificati sono sbagliate.

    Java si reifica, ma in modo errato a causa della compatibilità con le versioni precedenti.

    Cos’è la reificazione?

    Dal nostro Wiki

    La reificazione è il processo mediante il quale un’idea astratta su un programma per computer viene trasformata in un modello di dati esplicito o in un altro object creato in un linguaggio di programmazione.

    Reificazione significa convertire qualcosa di astratto (tipo parametrico) in qualcosa di concreto (tipo concreto) per specializzazione.

    Illustriamo questo con un semplice esempio:

    Un ArrayList con definizione:

     ArrayList { T[] elems; ...//methods } 

    è un’astrazione, in dettaglio un costruttore di tipi, che viene “reificato” quando specializzato con un tipo concreto, ad esempio Integer:

     ArrayList { Integer[] elems; } 

    dove ArrayList è davvero un tipo.

    Ma questo è esattamente ciò che Java non fa !!! invece reificano costantemente i tipi astratti con i loro limiti, cioè producendo lo stesso tipo di calcestruzzo indipendente dai parametri passati per la specializzazione:

     ArrayList { Object[] elems; } 

    che è qui reificato con l’object associato implicito ( ArrayList == ArrayList ).

    Nonostante ciò rende inutilizzabili gli array generici e causa alcuni strani errori per i tipi non elaborati:

     List l= List.of("h","s"); List lRaw=l l.add(new Object()) String s=l.get(2) //Cast Exception 

    causa molte ambiguità come

     void function(ArrayList list){} void function(ArrayList list){} void function(ArrayList list){} 

    fare riferimento alla stessa funzione:

     void function(ArrayList list) 

    e quindi l’overloading del metodo generico non può essere utilizzato in Java.