Tipo di cancellazione generici Java: quando e cosa succede?


238

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 la classe è caricata? Quando la classe viene istanziata?

Molti siti (incluso il tutorial ufficiale sopra menzionato) affermano che la cancellazione del tipo avviene al momento della compilazione. Se le informazioni sul tipo vengono completamente rimosse al momento della compilazione, in che modo JDK verifica la compatibilità del tipo quando viene richiamato un metodo che utilizza generics senza informazioni di tipo o informazioni di tipo errate?

Si consideri il seguente esempio: class Say Aha un metodo, empty(Box<? extends Number> b). Compiliamo A.javae otteniamo il file di classe A.class.

public class A {
    public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}

Ora creiamo un'altra classe B, che richiama il metodo emptycon un argomento senza parametri (tipo raw): empty(new Box()). Se si compila B.javacon A.classnel classpath, javac è abbastanza intelligente per sollevare un avvertimento. Quindi A.class contiene alcune informazioni sul tipo.

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

La mia ipotesi sarebbe che la cancellazione del tipo si verifichi quando la classe viene caricata, ma è solo un'ipotesi. Quindi quando succede?



@afryingpan: l'articolo menzionato nella mia risposta spiega in dettaglio come e quando avviene la cancellazione del tipo. Spiega anche quando vengono conservate le informazioni sul tipo. In altre parole: i generici reificati sono disponibili in Java, contrariamente alla credenza diffusa. Vedi: rgomes.info/using-typetokens-to-retrieve-generic-parameters
Richard Gomes

Risposte:


240

La cancellazione del tipo si applica all'uso di farmaci generici. Esistono sicuramente metadati nel file di classe 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 in fase di esecuzione. Quindi questo codice:

List<String> list = new ArrayList<String>();
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 è possibile scoprirlo T=Stringper l'oggetto elenco: tali informazioni sono sparite.

... ma l' List<T>interfaccia stessa si fa ancora pubblicità come generica.

EDIT: solo per chiarire, il compilatore conserva le informazioni sulla variabile essendo un List<String>- ma non è ancora possibile scoprirlo T=Stringper l'oggetto elenco stesso.


6
No, anche nell'uso di un tipo generico potrebbero essere disponibili metadati in fase di esecuzione. Una variabile locale non è accessibile tramite Reflection, ma per un parametro del metodo dichiarato come "Elenco <String> l", ci saranno metadati di tipo in fase di esecuzione, disponibili tramite l'API di Reflection. Sì, "la cancellazione del tipo" non è così semplice come molte persone pensano ...
Rogério,

4
@Rogerio: come ho risposto al tuo commento, credo che ti stai confondendo tra la possibilità di ottenere il tipo di una variabile e la possibilità di ottenere il tipo di un oggetto . L'oggetto stesso non conosce il suo argomento di tipo, anche se il campo lo fa.
Jon Skeet,

Certo, solo guardando l'oggetto stesso non puoi sapere che è un Elenco <String>. Ma gli oggetti non appaiono solo dal nulla. Vengono creati localmente, passati come argomento di invocazione del metodo, restituiti come valore di ritorno da una chiamata del metodo o letti da un campo di un oggetto ... In tutti questi casi è possibile sapere in fase di esecuzione qual è il tipo generico implicitamente o utilizzando l'API Java Reflection.
Rogério,

13
@Rogerio: come fai a sapere da dove proviene l'oggetto? Se hai un parametro di tipo List<? extends InputStream>come puoi sapere che tipo era quando è stato creato? Anche se riesci a scoprire il tipo di campo in cui è stato memorizzato il riferimento, perché dovresti farlo? Perché dovresti essere in grado di ottenere tutte le altre informazioni sull'oggetto in fase di esecuzione, ma non i suoi argomenti di tipo generico? Sembra che tu stia provando a cancellare il tipo come questa piccola cosa che non influisce davvero sugli sviluppatori, mentre in alcuni casi trovo che sia un problema molto significativo .
Jon Skeet,

Ma la cancellazione del tipo È una cosa minuscola che non ha effetti sugli sviluppatori! Certo, non posso parlare per gli altri, ma nella mia esperienza non è mai stato un grosso problema. In realtà utilizzo le informazioni sul tipo di runtime nella progettazione della mia API di simulazione Java (JMockit); ironicamente, le API di simulazione di .NET sembrano trarre meno vantaggio dal sistema di tipo generico disponibile in C #.
Rogério,

99

Il compilatore è responsabile della comprensione di Generics al momento della compilazione. Il compilatore è anche responsabile di buttare via questa "comprensione" delle classi generiche, in un processo che chiamiamo cancellazione del tipo . Tutto accade in fase di compilazione.

Nota: Contrariamente alle credenze della maggior parte degli sviluppatori Java, è possibile conservare le informazioni sul tipo di compilazione e recuperare queste informazioni in fase di esecuzione, nonostante in modo molto limitato. In altre parole: Java fornisce generici reificati in modo molto limitato .

Per quanto riguarda la cancellazione del tipo

Si noti che, al momento della compilazione, il compilatore ha a disposizione informazioni complete sul tipo, ma queste informazioni vengono eliminate intenzionalmente in generale quando viene generato il codice byte, in un processo noto come cancellazione del tipo . Questo viene fatto in questo modo a causa di problemi di compatibilità: l'intenzione dei progettisti del linguaggio era di fornire la piena compatibilità del codice sorgente e la piena compatibilità del codice byte tra le versioni della piattaforma. Se fosse stato implementato in modo diverso, sarebbe necessario ricompilare le applicazioni legacy durante la migrazione a versioni più recenti della piattaforma. In questo modo, tutte le firme dei metodi vengono conservate (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 particolare delle classi anonime, è possibile recuperare informazioni complete sul tipo di tempo di compilazione in fase di esecuzione che, in altre parole, significa: generici reificati. Ciò significa che il compilatore non elimina le informazioni sul tipo quando sono coinvolte classi anonime; queste informazioni sono conservate nel codice binario generato e il sistema di runtime consente di recuperare queste informazioni.

Ho scritto un articolo su questo argomento:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

Una nota sulla tecnica descritta nell'articolo sopra è che la tecnica è oscura per la maggior parte degli sviluppatori. Nonostante funzioni e funzioni bene, la maggior parte degli sviluppatori si sente confuso o a disagio con la tecnica. Se si dispone di una base di codice condivisa o si prevede di rilasciare il codice al pubblico, non consiglio la tecnica di cui sopra. D'altra parte, se sei l'unico utente del tuo codice, puoi sfruttare il potere che questa tecnica ti offre.

Codice di esempio

L'articolo sopra contiene collegamenti al codice di esempio.


1
@ will824: ho migliorato enormemente la risposta e ho aggiunto un link ad alcuni casi di test. Saluti :)
Richard Gomes il

1
In realtà, non hanno mantenuto la compatibilità sia binaria che sorgente: oracle.com/technetwork/java/javase/compatibility-137462.html Dove posso leggere di più sulla loro intenzione? I documenti dicono che utilizza la cancellazione del tipo, ma non dice il perché.
Dzmitry Lazerka,

@Richard Davvero, eccellente articolo! Potresti aggiungere che anche le classi locali funzionano e che, in entrambi i casi (classi anonime e locali), le informazioni sull'argomento di tipo desiderato vengono conservate solo in caso di accesso diretto new Box<String>() {};non in caso di accesso indiretto void foo(T) {...new Box<T>() {};...}perché il compilatore non conserva le informazioni di tipo per la dichiarazione del metodo allegato.
Yann-Gaël Guéhéneuc,

Ho corretto un link non funzionante al mio articolo. Sto lentamente deselezionando la mia vita e recuperando i miei dati. :-)
Richard Gomes,

33

Se si dispone di un campo di tipo generico, i suoi parametri di tipo vengono compilati nella classe.

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

Questa informazione è ciò che il compilatore utilizza per dirvi che non è possibile passare una Box<String>al empty(Box<T extends Number>)metodo.

L'API è complicato, ma si può controllare questo tipo di informazioni tramite l'API di riflessione con metodi come getGenericParameterTypes, getGenericReturnTypee, per i campi, getGenericType.

Se si dispone di codice che utilizza un tipo generico, il compilatore inserisce i cast secondo necessità (nel chiamante) per controllare i tipi. Gli oggetti generici stessi sono solo di tipo raw; il tipo con parametri è "cancellato". Pertanto, quando si crea un new Box<Integer>(), non ci sono informazioni sulla Integerclasse Boxnell'oggetto.

Le FAQ di Angelika Langer sono la migliore referenza che ho visto per Java Generics.


2
In realtà, è il tipo generico formale dei campi e dei metodi che vengono compilati nella classe, ovvero la tipica "T". Per ottenere il tipo reale di tipo generico, è necessario utilizzare il "trucco di classe anonima" .
Yann-Gaël Guéhéneuc,

13

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) pensarlo come una traduzione da fonte a fonte, per cui la versione generica di loophole()viene convertita in versione non generica.

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

Consiglierei anche la risposta di Mr. Skeet su Qual è il concetto di cancellazione in generici in Java?


6

La cancellazione del tipo si verifica al momento della 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<String> b = new Box<String>();
String x = b.getDefault();

viene convertito in

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

al momento della compilazione. Potresti ricevere avvisi non perché il compilatore è a conoscenza del tipo di generico, ma al contrario, perché non ne conosce abbastanza e non può garantire la sicurezza del tipo.

Inoltre, il compilatore conserva le informazioni sul tipo relative ai parametri in una chiamata di metodo, che è possibile recuperare tramite reflection.

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


6

Il termine "cancellazione del tipo" non è in realtà la descrizione corretta del problema di Java con i generici. La cancellazione dei tipi non è di per sé una cosa negativa, anzi è molto necessaria per le prestazioni ed è spesso usata in diversi linguaggi come C ++, Haskell, D.

Prima di disgustare, ricorda la corretta definizione di cancellazione del tipo da Wiki

Che cos'è la cancellazione del tipo?

la cancellazione del tipo si riferisce 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

La cancellazione del tipo significa eliminare tag di tipo creati in fase di progettazione o tag di tipo dedotto in fase di compilazione in modo tale che il programma compilato in codice binario non contenga tag di tipo. E questo è il caso di ogni linguaggio di programmazione che si compila in codice binario, tranne in alcuni casi in cui sono necessari tag di runtime. Queste eccezioni includono, ad esempio, tutti i tipi esistenziali (tipi di riferimento Java sottotipizzabili, qualsiasi tipo in molte lingue, tipi di unione). Il motivo della cancellazione del tipo è che i programmi vengono trasformati in un linguaggio che è in qualche modo tipizzato uni (il linguaggio binario consente solo i bit) poiché i tipi sono solo astrazioni e affermano una struttura per i suoi valori e la semantica appropriata per gestirli.

Quindi questo è in cambio, una cosa normale naturale.

Il problema di Java è diverso e causato dalla sua reificazione.

Anche le dichiarazioni spesso fatte su Java non hanno generici reificati è sbagliata.

Java reify, ma in modo sbagliato a causa della compatibilità con le versioni precedenti.

Che 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 altro oggetto creato in un linguaggio di programmazione.

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

Lo illustriamo con un semplice esempio:

Una lista di array con definizione:

ArrayList<T>
{
    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>
{
    Integer[] elems;
}

dove ArrayList<Integer>è davvero un tipo.

Ma questa è esattamente la cosa che Java non fa !!! , invece reificano i tipi costantemente astratti con i loro limiti, cioè producendo lo stesso tipo concreto indipendentemente dai parametri passati per la specializzazione:

ArrayList
{
    Object[] elems;
}

che è qui reificato con l'oggetto associato implicito ( ArrayList<T extends Object>== ArrayList<T>).

Nonostante ciò rende inutilizzabili array generici e causa strani errori per i tipi grezzi:

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

provoca molte ambiguità come

void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}

fare riferimento alla stessa funzione:

void function(ArrayList list)

e quindi il sovraccarico del metodo generico non può essere utilizzato in Java.


2

Ho riscontrato la cancellazione del tipo in Android. In produzione utilizziamo il grado con l'opzione di minimizzazione. Dopo la minificazione ho un'eccezione fatale. Ho creato una semplice funzione per mostrare la catena di eredità del mio oggetto:

public static void printSuperclasses(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 minimizzato:

D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>

D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>

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 minimizzato:

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.g.e

D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object

D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null

Pertanto, nel codice minimizzato le classi parametrizzate effettive vengono sostituite con tipi di classi non elaborate senza alcuna informazione sul tipo. Come soluzione per il mio progetto ho rimosso tutte le chiamate di riflessione e le ho sostituite con tipi di parametri espliciti passati in argomenti di funzione.

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.