Qual è il concetto di cancellazione in generici in Java?


141

Qual è il concetto di cancellazione in generici in Java?

Risposte:


200

È fondamentalmente il modo in cui i generici sono implementati in Java tramite l'inganno del compilatore. Il codice generico compilato in realtà utilizza solo java.lang.Objectovunque tu parli T(o qualche altro parametro di tipo) - e ci sono alcuni metadati per dire al compilatore che è davvero un tipo generico.

Quando compili del codice con un tipo o metodo generico, il compilatore capisce cosa intendi realmente (ovvero quale sia l'argomento type per T) e verifica in fase di compilazione che stai facendo la cosa giusta, ma il codice emesso parla ancora in termini di java.lang.Object- il compilatore genera cast extra se necessario. Al momento dell'esecuzione, a List<String> e a List<Date>sono esattamente gli stessi; le informazioni aggiuntive sul tipo sono state cancellate dal compilatore.

Confrontalo con, diciamo, C #, dove le informazioni vengono conservate al momento dell'esecuzione, consentendo al codice di contenere espressioni come le typeof(T)quali è equivalente a T.class- tranne che quest'ultima non è valida. (Ci sono ulteriori differenze tra generici .NET e generici Java, intendiamoci.) La cancellazione del tipo è la fonte di molti dei messaggi di errore / avviso "dispari" quando si tratta di generici Java.

Altre risorse:


6
@Rogerio: No, gli oggetti non avranno tipi generici diversi. I campi conoscono i tipi, ma gli oggetti no.
Jon Skeet,

8
@Rogerio: Assolutamente - è estremamente facile scoprire al momento dell'esecuzione se qualcosa che viene fornito come Object(in uno scenario debolmente tipizzato) è effettivamente un List<String>) per esempio. In Java questo non è fattibile: puoi scoprire che è un ArrayListtipo generico originale, ma non lo è. Questo genere di cose può emergere in situazioni di serializzazione / deserializzazione, per esempio. Un altro esempio è dove un contenitore deve essere in grado di costruire istanze del suo tipo generico - devi passare quel tipo separatamente in Java (as Class<T>).
Jon Skeet,

6
Non ho mai sostenuto che si trattasse sempre o quasi sempre di un problema, ma è almeno ragionevolmente frequente un problema nella mia esperienza. Ci sono vari posti in cui sono costretto ad aggiungere un Class<T>parametro a un costruttore (o metodo generico) semplicemente perché Java non conserva tali informazioni. Guarda ad EnumSet.allOfesempio - l'argomento di tipo generico al metodo dovrebbe essere sufficiente; perché devo specificare anche un argomento "normale"? Risposta: digitare la cancellazione. Questo genere di cose inquina un'API. Per interesse, hai usato molto .NET generics? (continua)
Jon Skeet,

5
Prima di usare i generici .NET, ho trovato i generici Java imbarazzanti in vari modi (e il jolly è ancora un mal di testa, sebbene la forma di varianza "specificata dal chiamante" abbia sicuramente dei vantaggi) - ma era solo dopo che avevo usato i generici .NET per un po 'ho visto quanti schemi sono diventati imbarazzanti o impossibili con i generici Java. È di nuovo il paradosso dei Blub. Non sto dicendo che i generici .NET non abbiano neanche degli svantaggi, tra l'altro - ci sono varie relazioni di tipo che non possono essere espresse, sfortunatamente - ma preferisco di gran lunga ai generici Java.
Jon Skeet,

5
@Rogerio: c'è molto che puoi fare con la riflessione, ma non tendo a scoprire che voglio fare quelle cose quasi tutte le volte che non posso fare con i generici Java. Non voglio scoprire l'argomento type per un campo quasi tutte le volte che voglio scoprire l'argomento type di un oggetto reale.
Jon Skeet,

41

Proprio come una nota a margine, è un esercizio interessante vedere effettivamente cosa sta facendo il compilatore quando esegue la cancellazione - rende l'intero concetto un po 'più facile da capire. C'è un flag speciale che puoi passare al compilatore per generare file java in cui i generici sono stati cancellati e i cast inseriti. Un esempio:

javac -XD-printflat -d output_dir SomeFile.java

Il -printflatè la bandiera che viene passata ad al compilatore che genera i file. (La -XDparte è ciò che dice javacdi consegnarlo al vaso eseguibile che esegue effettivamente la compilazione piuttosto che solo javac, ma sto divagando ...) -d output_dirCiò è necessario perché il compilatore ha bisogno di un posto dove mettere i nuovi file .java.

Questo, ovviamente, non si limita alla semplice cancellazione; tutte le cose automatiche eseguite dal compilatore vengono eseguite qui. Ad esempio, vengono anche inseriti costruttori predefiniti, i nuovi forloop in stile foreach vengono espansi in forloop regolari , ecc. È bello vedere le piccole cose che accadono automagicamente.


29

La cancellazione significa letteralmente che le informazioni sul tipo presenti nel codice sorgente vengono cancellate dal bytecode compilato. Cerchiamo di capirlo con un po 'di codice.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

Se compili questo codice e poi lo decompili con un decompilatore Java, otterrai qualcosa del genere. Si noti che il codice decompilato non contiene alcuna traccia delle informazioni sul tipo presenti nel codice sorgente originale.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 

Ho provato a usare il decompilatore java per vedere il codice dopo la cancellazione del tipo dal file .class, ma il file .class ha ancora informazioni sul tipo. Ho provato a jigawotdire che funziona.
franco

25

Per completare la risposta già molto completa di Jon Skeet, è necessario rendersi conto che il concetto di cancellazione del tipo deriva da un'esigenza di compatibilità con le versioni precedenti di Java .

Presentata inizialmente a EclipseCon 2007 (non più disponibile), la compatibilità includeva quei punti:

  • Compatibilità sorgente (Bello avere ...)
  • Compatibilità binaria (Must have!)
  • Compatibilità con la migrazione
    • I programmi esistenti devono continuare a funzionare
    • Le librerie esistenti devono essere in grado di utilizzare tipi generici
    • Deve avere!

Risposta originale:

Quindi:

new ArrayList<String>() => new ArrayList()

Vi sono proposte per una maggiore reificazione . Reify è "Considera un concetto astratto come reale", dove i costrutti del linguaggio dovrebbero essere concetti, non solo zucchero sintattico.

Dovrei anche menzionare il checkCollectionmetodo di Java 6, che restituisce una vista di tipo dinamico della raccolta specificata. Qualsiasi tentativo di inserire un elemento di tipo errato comporterà un'immediata ClassCastException.

Il meccanismo generics nella lingua fornisce il controllo del tipo in fase di compilazione (statico), ma è possibile sconfiggere questo meccanismo con cast non controllati .

Di solito questo non è un problema, poiché il compilatore emette avvisi su tutte queste operazioni non controllate.

Vi sono, tuttavia, momenti in cui il controllo statico del tipo da solo non è sufficiente, come:

  • quando una raccolta viene passata a una libreria di terze parti ed è indispensabile che il codice della libreria non danneggi la raccolta inserendo un elemento di tipo errato.
  • un programma ha esito negativo con a ClassCastException, a indicare che un elemento digitato in modo errato è stato inserito in una raccolta con parametri. Sfortunatamente, l'eccezione può verificarsi in qualsiasi momento dopo l'inserimento dell'elemento errato, quindi in genere fornisce poche o nessuna informazione sulla vera fonte del problema.

Aggiornamento luglio 2012, quasi quattro anni dopo:

Ora è (2012) dettagliato in " Regole di compatibilità della migrazione API (Test di firma) "

Il linguaggio di programmazione Java implementa i generici usando la cancellazione, il che assicura che versioni legacy e generiche generino di solito file di classe identici, ad eccezione di alcune informazioni ausiliarie sui tipi. La compatibilità binaria non viene interrotta perché è possibile sostituire un file di classe legacy con un file di classe generico senza modificare o ricompilare alcun codice client.

Per facilitare l'interfacciamento con codice legacy non generico, è anche possibile utilizzare la cancellazione di un tipo con parametri come tipo. Tale tipo è chiamato tipo grezzo ( Java Language Specification 3 / 4.8 ). Consentire il tipo raw garantisce anche la compatibilità con le versioni precedenti del codice sorgente.

In base a ciò, le seguenti versioni della java.util.Iteratorclasse sono sia binarie che compatibili con il codice sorgente:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

2
Si noti che la retrocompatibilità avrebbe potuto essere raggiunta senza la cancellazione del tipo, ma non senza che i programmatori Java apprendessero un nuovo set di raccolte. Questa è esattamente la strada percorsa da .NET. In altre parole, è questo terzo proiettile che è quello importante. (Continua.)
Jon Skeet,

15
Personalmente penso che questo sia stato un errore miope - ha dato un vantaggio a breve termine e uno svantaggio a lungo termine.
Jon Skeet,

8

Completando la risposta già integrata di Jon Skeet ...

È stato menzionato che l'implementazione dei farmaci generici attraverso la cancellazione porta ad alcune fastidiose limitazioni (es. No new T[42]). È stato anche menzionato che il motivo principale per fare le cose in questo modo era la retrocompatibilità nel bytecode. Questo è anche (principalmente) vero. Il bytecode generato -target 1.5 è in qualche modo diverso dal solo casting -target -target 1.4. Tecnicamente, è persino possibile (attraverso un inganno inganno) ottenere l'accesso a istanze di tipo generico in fase di esecuzione , dimostrando che c'è davvero qualcosa nel bytecode.

Il punto più interessante (che non è stato sollevato) è che l'implementazione di farmaci generici usando la cancellazione offre un po 'più di flessibilità in ciò che il sistema di tipo di alto livello può realizzare. Un buon esempio di ciò sarebbe l'implementazione JVM di Scala rispetto a CLR. Sulla JVM, è possibile implementare direttamente i tipi superiori a causa del fatto che la JVM stessa non impone restrizioni sui tipi generici (poiché questi "tipi" sono effettivamente assenti). Ciò contrasta con il CLR, che ha una conoscenza di runtime delle istanze dei parametri. Per questo motivo, lo stesso CLR deve avere un'idea di come usare i generici, annullando i tentativi di estendere il sistema con regole impreviste. Di conseguenza, i tipi superiori di Scala sul CLR sono implementati usando una strana forma di cancellazione emulata all'interno del compilatore stesso,

La cancellazione può essere scomoda quando vuoi fare cose cattive in fase di esecuzione, ma offre la massima flessibilità agli autori del compilatore. Immagino che sia parte del motivo per cui non andrà via presto.


6
L'inconveniente non è quando vuoi fare cose "cattive" al momento dell'esecuzione. È quando vuoi fare cose perfettamente ragionevoli al momento dell'esecuzione. In effetti, la cancellazione del tipo ti consente di fare cose molto più difficili - come lanciare un Elenco <String> in Elenco e quindi in Elenco <Data> con solo avvertenze.
Jon Skeet,

5

A quanto ho capito (essendo un tipo .NET ) la JVM non ha alcun concetto di generica, quindi il compilatore sostituisce i parametri di tipo con Object ed esegue tutto il casting per te.

Ciò significa che i generici Java non sono altro che zucchero di sintassi e non offrono alcun miglioramento delle prestazioni per i tipi di valore che richiedono box / unboxing quando passati per riferimento.


3
I generici Java non possono comunque rappresentare tipi di valore - non esiste un elenco <int>. Tuttavia, in Java non esiste alcun riferimento pass-by - è rigorosamente pass by value (dove quel valore può essere un riferimento).
Jon Skeet

2

Ci sono buone spiegazioni. Aggiungo solo un esempio per mostrare come funziona la cancellazione del tipo con un decompilatore.

Classe originale,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

Codice decompilato dal suo bytecode,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

2

Perché usare Generici

In breve, i generici consentono ai tipi (classi e interfacce) di essere parametri quando si definiscono classi, interfacce e metodi. Proprio come i parametri formali più familiari utilizzati nelle dichiarazioni dei metodi, i parametri di tipo forniscono un modo per riutilizzare lo stesso codice con input diversi. La differenza è che gli input per i parametri formali sono valori, mentre gli input per digitare i parametri sono tipi. ode che usa generics ha molti vantaggi rispetto al codice non generico:

  • Controlli di tipo più forti al momento della compilazione.
  • Eliminazione di calchi.
  • Consentire ai programmatori di implementare algoritmi generici.

Che cos'è la cancellazione del tipo

I generici sono stati introdotti nel linguaggio Java per fornire controlli di tipo più rigorosi in fase di compilazione e supportare la programmazione generica. Per implementare generics, il compilatore Java applica la cancellazione del tipo a:

  • Sostituisci tutti i parametri di tipo in tipi generici con i loro limiti o Oggetto se i parametri di tipo non sono associati. Il bytecode prodotto, quindi, contiene solo classi, interfacce e metodi ordinari.
  • Inserire cast di tipo se necessario per preservare la sicurezza del tipo.
  • Generare metodi bridge per preservare il polimorfismo in tipi generici estesi.

[NB] -Che cos'è il metodo bridge? In breve, nel caso di un'interfaccia parametrizzata come Comparable<T>, ciò può causare l'inserimento di metodi aggiuntivi da parte del compilatore; questi metodi aggiuntivi sono chiamati ponti.

Come funziona la cancellazione

La cancellazione di un tipo è definita come segue: elimina tutti i parametri di tipo dai tipi con parametri e sostituisce qualsiasi variabile di tipo con la cancellazione del suo limite, o con Oggetto se non ha alcun limite, o con la cancellazione del limite più a sinistra se ha limiti multipli. Ecco alcuni esempi:

  • La cancellazione di List<Integer>, List<String>ed List<List<String>>è List.
  • La cancellazione di List<Integer>[]è List[].
  • La cancellazione di Listè essa stessa, allo stesso modo per qualsiasi tipo grezzo.
  • La cancellazione di int è essa stessa, in modo simile per qualsiasi tipo primitivo.
  • La cancellazione di Integerè essa stessa, allo stesso modo per qualsiasi tipo senza parametri di tipo.
  • La cancellazione di Tnella definizione di asListè Object, perché T non ha limiti.
  • La cancellazione di Tnella definizione di maxè Comparable, perché T ha legato Comparable<? super T>.
  • La cancellazione di Tnella definizione finale di maxè Object, perché Tha legato Object& Comparable<T>e prendiamo la cancellazione del limite più a sinistra.

Bisogna fare attenzione quando si usano i generici

In Java, due metodi distinti non possono avere la stessa firma. Poiché i generici sono implementati con la cancellazione, ne consegue anche che due metodi distinti non possono avere firme con la stessa cancellazione. Una classe non può sovraccaricare due metodi le cui firme hanno la stessa cancellazione e una classe non può implementare due interfacce che hanno la stessa cancellazione.

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

Intendiamo che questo codice funzioni come segue:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

Tuttavia, in questo caso le cancellazioni delle firme di entrambi i metodi sono identiche:

boolean allZero(List)

Pertanto, al momento della compilazione viene segnalato un conflitto di nomi. Non è possibile assegnare a entrambi i metodi lo stesso nome e cercare di distinguerli sovraccaricandoli, poiché dopo la cancellazione è impossibile distinguere una chiamata di metodo dall'altra.

Speriamo che Reader si diverta :)

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.