Was ist das Konzept der Löschung in Generika in Java?
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 aList<String>
) ist. In Java ist das einfach nicht machbar - Sie können feststellen, dass es sich um einenArrayList
, 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 (asClass<T>
) übergeben.Class<T>
, einem Konstruktor (oder einer generischen Methode) einen Parameter hinzuzufügen, nur weil Java diese Informationen nicht speichert. Schauen Sie sichEnumSet.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:
Dies
-printflat
ist das Flag, das an den Compiler übergeben wird, der die Dateien generiert. (Der-XD
Teil sagtjavac
, dass es an die ausführbare JAR-Datei übergeben werden soll, die das Kompilieren tatsächlich durchführt und nicht nurjavac
, 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ärenfor
Schleifen erweitert usw. Es ist schön zu sehen, welche kleinen Dinge automatisch ablaufen.quelle
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.
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.
quelle
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:
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 einemClassCastException
.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.
quelle
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.
quelle
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.
quelle
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,
Dekompilierter Code aus seinem Bytecode,
quelle
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>
undList<List<String>>
istList
.List<Integer>[]
istList[]
.List
ist selbst, ähnlich für jeden rohen Typ.Integer
ist selbst, ähnlich für jeden Typ ohne Typparameter.T
in der Definition vonasList
istObject
, weilT
keine Grenze hat.T
in der Definition vonmax
istComparable
, weilT
gebunden hatComparable<? super T>
.T
in der endgültigen Definition vonmax
istObject
, weilT
hatObject
& gebunden hatComparable<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.
Wir beabsichtigen, dass dieser Code wie folgt funktioniert:
In diesem Fall sind die Löschungen der Signaturen beider Methoden jedoch identisch:
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 :)
quelle