Antworten:
Es ist im Grunde die Art und Weise, wie Generika in Java über Compiler-Tricks implementiert werden. Der kompilierte generische Code wird eigentlich nur dort verwendet, java.lang.Object
wo Sie sprechen T
(oder in einem anderen Typparameter) - und es gibt einige Metadaten, die dem Compiler mitteilen, dass es sich wirklich um einen generischen Typ handelt.
Wenn Sie Code gegen einen generischen Typ oder eine generische Methode kompilieren, ermittelt der Compiler, was Sie wirklich meinen (dh wofür das Typargument T
lautet), und überprüft beim Kompilieren, ob Sie das Richtige tun, aber der ausgegebene Code spricht erneut nur in Bezug auf java.lang.Object
- der Compiler generiert bei Bedarf zusätzliche Casts. Zur Ausführungszeit sind a List<String>
und a List<Date>
genau gleich; Die zusätzlichen Typinformationen wurden vom Compiler gelöscht .
Vergleichen Sie dies beispielsweise mit C #, wo die Informationen zur Ausführungszeit beibehalten werden, sodass Code Ausdrücke enthalten kann, typeof(T)
die beispielsweise äquivalent zu T.class
- außer dass letzterer ungültig ist. (Es gibt wohlgemerkt weitere Unterschiede zwischen .NET-Generika und Java-Generika.) Das Löschen von Typen ist die Quelle vieler "ungerader" Warn- / Fehlermeldungen beim Umgang mit Java-Generika.
Andere Ressourcen:
Object
(in einem schwach typisierten Szenario) bereitgestellt wird, tatsächlich a List<String>
) ist. In Java ist das einfach nicht machbar - Sie können feststellen, dass es sich um einen ArrayList
, aber nicht um den ursprünglichen generischen Typ handelt. So etwas kann beispielsweise in Serialisierungs- / Deserialisierungssituationen auftreten. Ein anderes Beispiel ist, wenn ein Container in der Lage sein muss, Instanzen seines generischen Typs zu erstellen - Sie müssen diesen Typ separat in Java (as Class<T>
) übergeben.
Class<T>
, einem Konstruktor (oder einer generischen Methode) einen Parameter hinzuzufügen, nur weil Java diese Informationen nicht speichert. Schauen Sie sich EnumSet.allOf
zum Beispiel an - das generische Typargument für die Methode sollte ausreichen. Warum muss ich auch ein "normales" Argument angeben? Antwort: Geben Sie Löschen ein. Diese Art von Dingen verschmutzt eine API. Haben Sie aus Interesse häufig .NET-Generika verwendet? (Fortsetzung)
Nur als Randnotiz ist es eine interessante Übung, tatsächlich zu sehen, was der Compiler tut, wenn er das Löschen durchführt - was das gesamte Konzept ein wenig verständlicher macht. Es gibt ein spezielles Flag, mit dem Sie den Compiler übergeben können, um Java-Dateien auszugeben, bei denen die Generika gelöscht und Casts eingefügt wurden. Ein Beispiel:
javac -XD-printflat -d output_dir SomeFile.java
Dies -printflat
ist das Flag, das an den Compiler übergeben wird, der die Dateien generiert. (Der -XD
Teil sagt javac
, dass es an die ausführbare JAR-Datei übergeben werden soll, die das Kompilieren tatsächlich durchführt und nicht nur javac
, aber ich schweife ab ...) -d output_dir
Dies ist erforderlich, da der Compiler einen Platz zum Ablegen der neuen Java-Dateien benötigt.
Dies bedeutet natürlich mehr als nur Löschen. Alle automatischen Aufgaben des Compilers werden hier erledigt. Beispielsweise werden auch Standardkonstruktoren eingefügt, die neuen foreach- for
Schleifen werden zu regulären for
Schleifen erweitert usw. Es ist schön zu sehen, welche kleinen Dinge automatisch ablaufen.
Löschen bedeutet wörtlich, dass die im Quellcode vorhandenen Typinformationen aus dem kompilierten Bytecode gelöscht werden. Lassen Sie uns dies mit etwas Code verstehen.
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);
}
}
}
Wenn Sie diesen Code kompilieren und dann mit einem Java-Dekompiler dekompilieren, erhalten Sie so etwas. Beachten Sie, dass der dekompilierte Code keine Spur der Typinformationen enthält, die im ursprünglichen Quellcode vorhanden sind.
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
gesagt, es funktioniert.
Um die bereits sehr vollständige Antwort von Jon Skeet zu vervollständigen, müssen Sie das Konzept der Typlöschung erkennen, das sich aus der Notwendigkeit der Kompatibilität mit früheren Java-Versionen ergibt .
Die Kompatibilität wurde ursprünglich auf der EclipseCon 2007 vorgestellt (nicht mehr verfügbar) und umfasste folgende Punkte:
Ursprüngliche Antwort:
Daher:
new ArrayList<String>() => new ArrayList()
Es gibt Vorschläge für eine größere Verdinglichung . Reify ist "Betrachten Sie ein abstraktes Konzept als real", wo Sprachkonstrukte Konzepte sein sollten, nicht nur syntaktischer Zucker.
Ich sollte auch die checkCollection
Methode von Java 6 erwähnen , die eine dynamisch typsichere Ansicht der angegebenen Sammlung zurückgibt. Jeder Versuch, ein Element des falschen Typs einzufügen, führt sofort zu einem ClassCastException
.
Der generische Mechanismus in der Sprache bietet eine (statische) Typprüfung zur Kompilierungszeit. Es ist jedoch möglich, diesen Mechanismus mit ungeprüften Casts zu umgehen .
Normalerweise ist dies kein Problem, da der Compiler bei all diesen ungeprüften Vorgängen Warnungen ausgibt.
Es gibt jedoch Zeiten, in denen die statische Typprüfung allein nicht ausreicht, z.
ClassCastException
, was darauf hinweist, dass ein falsch eingegebenes Element in eine parametrisierte Sammlung aufgenommen wurde. Leider kann die Ausnahme jederzeit nach dem Einfügen des fehlerhaften Elements auftreten, sodass in der Regel nur wenige oder gar keine Informationen zur tatsächlichen Ursache des Problems vorliegen.Update Juli 2012, fast vier Jahre später:
Es ist jetzt (2012) in " API-Migrationskompatibilitätsregeln (Signaturtest) " beschrieben.
Die Programmiersprache Java implementiert Generika mithilfe von Erasure, wodurch sichergestellt wird, dass ältere und generische Versionen normalerweise identische Klassendateien generieren, mit Ausnahme einiger zusätzlicher Informationen zu Typen. Die Binärkompatibilität wird nicht beeinträchtigt, da eine ältere Klassendatei durch eine generische Klassendatei ersetzt werden kann, ohne dass Clientcode geändert oder neu kompiliert werden muss.
Um die Anbindung an nicht generischen Legacy-Code zu erleichtern, kann auch das Löschen eines parametrisierten Typs als Typ verwendet werden. Ein solcher Typ wird als Rohtyp bezeichnet ( Java Language Specification 3 / 4.8 ). Durch das Zulassen des Rohtyps wird auch die Abwärtskompatibilität für den Quellcode sichergestellt.
Demnach sind die folgenden Versionen der
java.util.Iterator
Klasse sowohl Binär- als auch Quellcode abwärtskompatibel:
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();
}
Ergänzung der bereits ergänzten Antwort von Jon Skeet ...
Es wurde erwähnt, dass die Implementierung von Generika durch Löschen zu einigen störenden Einschränkungen führt (z new T[42]
. B. nein ). Es wurde auch erwähnt, dass der Hauptgrund für diese Vorgehensweise die Abwärtskompatibilität im Bytecode war. Dies ist auch (meistens) wahr. Der generierte Bytecode -Ziel 1.5 unterscheidet sich etwas von dem einfach zuckerfreien Casting -Ziel 1.4. Technisch gesehen ist es sogar möglich (durch immense Tricks), zur Laufzeit auf generische Typinstanziierungen zuzugreifen , was beweist, dass der Bytecode wirklich etwas enthält.
Der interessantere Punkt (der nicht angesprochen wurde) ist, dass die Implementierung von Generika mithilfe der Löschung einiges an Flexibilität bietet, was das System auf hoher Ebene leisten kann. Ein gutes Beispiel hierfür wäre die JVM-Implementierung von Scala im Vergleich zu CLR. In der JVM ist es möglich, höhere Arten direkt zu implementieren, da die JVM selbst keine Einschränkungen für generische Typen auferlegt (da diese "Typen" effektiv fehlen). Dies steht im Gegensatz zur CLR, die über Laufzeitkenntnisse zu Parameterinstanziierungen verfügt. Aus diesem Grund muss die CLR selbst ein Konzept für die Verwendung von Generika haben, wodurch Versuche, das System mit unerwarteten Regeln zu erweitern, zunichte gemacht werden. Infolgedessen werden Scalas höhere Arten in der CLR mithilfe einer seltsamen Form der Löschung implementiert, die im Compiler selbst emuliert wird.
Das Löschen kann unpraktisch sein, wenn Sie zur Laufzeit ungezogene Dinge tun möchten, bietet jedoch den Compiler-Autoren die größte Flexibilität. Ich vermute, das ist ein Teil dessen, warum es nicht so schnell verschwindet.
Soweit ich weiß (als .NET- Typ), hat die JVM kein Konzept für Generika, daher ersetzt der Compiler die Typparameter durch Object und führt das gesamte Casting für Sie durch.
Dies bedeutet, dass Java-Generika nichts anderes als Syntaxzucker sind und keine Leistungsverbesserung für Werttypen bieten, die Boxing / Unboxing erfordern, wenn sie als Referenz übergeben werden.
Es gibt gute Erklärungen. Ich füge nur ein Beispiel hinzu, um zu zeigen, wie die Typlöschung mit einem Dekompiler funktioniert.
Ursprüngliche Klasse,
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);
}
}
Dekompilierter Code aus seinem 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);
}
}
Warum Generices verwenden?
Kurz gesagt, Generika ermöglichen es Typen (Klassen und Schnittstellen), Parameter bei der Definition von Klassen, Schnittstellen und Methoden zu sein. Ähnlich wie die bekannteren formalen Parameter, die in Methodendeklarationen verwendet werden, bieten Typparameter eine Möglichkeit, denselben Code mit unterschiedlichen Eingaben wiederzuverwenden. Der Unterschied besteht darin, dass die Eingaben für formale Parameter Werte sind, während die Eingaben für Typparameter Typen sind. Eine Ode, die Generika verwendet, hat viele Vorteile gegenüber nicht generischem Code:
Was ist Typ Löschung?
Generika wurden in die Java-Sprache eingeführt, um beim Kompilieren strengere Typprüfungen zu ermöglichen und die generische Programmierung zu unterstützen. Um Generika zu implementieren, wendet der Java-Compiler das Löschen des Typs an:
[NB] -Was ist die Brückenmethode? Bei einer parametrisierten Schnittstelle wie z. B. Comparable<T>
kann dies dazu führen, dass der Compiler zusätzliche Methoden einfügt. Diese zusätzlichen Methoden werden als Brücken bezeichnet.
So funktioniert das Löschen
Das Löschen eines Typs ist wie folgt definiert: Löschen Sie alle Typparameter aus parametrisierten Typen und ersetzen Sie jede Typvariable durch das Löschen ihrer Bindung oder durch Object, wenn es keine Bindung hat, oder durch das Löschen der Grenze ganz links, wenn es eine Grenze hat mehrere Grenzen. Hier sind einige Beispiele:
List<Integer>
, List<String>
und List<List<String>>
ist List
.List<Integer>[]
ist List[]
.List
ist selbst, ähnlich für jeden rohen Typ.Integer
ist selbst, ähnlich für jeden Typ ohne Typparameter.T
in der Definition von asList
ist Object
, weil T
keine Grenze hat.T
in der Definition von max
ist Comparable
, weil T
gebunden hat Comparable<? super T>
.T
in der endgültigen Definition von max
ist Object
, weil
T
hat Object
& gebunden hat Comparable<T>
und wir die Löschung der am weitesten links liegenden Grenze nehmen.Seien Sie vorsichtig, wenn Sie Generika verwenden
In Java können zwei unterschiedliche Methoden nicht dieselbe Signatur haben. Da Generika durch Löschen implementiert werden, folgt auch, dass zwei unterschiedliche Methoden keine Signaturen mit derselben Löschung haben können. Eine Klasse kann nicht zwei Methoden überladen, deren Signaturen dieselbe Löschung aufweisen, und eine Klasse kann keine zwei Schnittstellen implementieren, die dieselbe Löschung aufweisen.
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;
}
}
Wir beabsichtigen, dass dieser Code wie folgt funktioniert:
assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));
In diesem Fall sind die Löschungen der Signaturen beider Methoden jedoch identisch:
boolean allZero(List)
Daher wird beim Kompilieren ein Namenskonflikt gemeldet. Es ist nicht möglich, beide Methoden gleich zu benennen und durch Überladen zwischen ihnen zu unterscheiden, da es nach dem Löschen unmöglich ist, einen Methodenaufruf vom anderen zu unterscheiden.
Hoffentlich wird der Leser genießen :)