Risposte:
È 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.Object
ovunque 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:
Object
(in uno scenario debolmente tipizzato) è effettivamente un List<String>
) per esempio. In Java questo non è fattibile: puoi scoprire che è un ArrayList
tipo 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>
).
Class<T>
parametro a un costruttore (o metodo generico) semplicemente perché Java non conserva tali informazioni. Guarda ad EnumSet.allOf
esempio - 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)
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 -XD
parte è ciò che dice javac
di consegnarlo al vaso eseguibile che esegue effettivamente la compilazione piuttosto che solo javac
, ma sto divagando ...) -d output_dir
Ciò è 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 for
loop in stile foreach vengono espansi in for
loop regolari , ecc. È bello vedere le piccole cose che accadono automagicamente.
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();
}
}
jigawot
dire che funziona.
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:
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 checkCollection
metodo 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:
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.Iterator
classe 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();
}
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.
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.
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);
}
}
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:
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:
[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:
List<Integer>
, List<String>
ed List<List<String>>
è List
.List<Integer>[]
è List[]
.List
è essa stessa, allo stesso modo per qualsiasi tipo grezzo.Integer
è essa stessa, allo stesso modo per qualsiasi tipo senza parametri di tipo.T
nella definizione di asList
è Object
, perché T
non ha limiti.T
nella definizione di max
è Comparable
, perché T
ha legato Comparable<? super T>
.T
nella definizione finale di max
è Object
, perché
T
ha 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 :)