Raw Types und Konsequenzen
Zur Laufzeit kennt Java keine der im Programmcode vorkommenden Ausprägungen der generischen Typen mehr. Stattdessen wird jede generische Klasse auf ihren sogeannten Raw Type zurückgeführt, und sämtliche generischen Objektreferenzen in normale Referenzen umgewandelt. Der Java Bytecode der resultierenden Klassendateien sieht also prinzipiell genau so wie in den vorherigen Java-Versionen aus (zumindest sind keine Veränderungen durch die Generics hinzugekommen).
Alle Vorkommen von Typparametern innerhalb einer Klasse werden durch den speziellsten (nicht den allgemeinsten; dies wird gleich erläutert) möglichen Typen ersetzt.
Hier noch einmal das Beispiel einer generischen verketteten Liste:
Compiliert man diese Klasse und decompiliert anschließend die entstandene .class-Datei mit einem Java Decompiler, sieht der übriggebliebene Code wie folgt aus:
Wurde der Typparameter bereits eingeschränkt, nutzt der Compiler dies, um statt
Object
einen spezielleren Typ zu nehmen (daher der speziellste mögliche):
Daraus wird nach Compilation und Decompilation:
Der Raw Type ist auch derjenige, der verwendet wird, wenn im Programmcode auf die generische Parametrisierung verzichtet wird. Der Vorteil ist ersichtlich: Älterer Java-Code ist komplett kompatibel zu den Generics; er ist nur natürlich weiterhin nicht typsicher (bzw. die Typsicherheit wird nicht garantiert).
Jedoch tun sich hierdurch auch eine ganze Reihe von Problemen auf. Einmal ist dies der Grund, warum Typparameter nicht im statischen Kontext einer Klasse benutzt werden können (außer bei unabhängigen generischen Methoden mit eigenem Typparameter). Zusätzlich resultieren aber auch eine Vielzahl zunächst verwirrender Einschränkungen aus diesem Verhalten. Die Beschränkungen bei der Verwendung von generischen Arrays, die am Ende des letzten Kapitels erwähnt wurden, sind ein Beispiel hierfür. Außerdem ist es z. B. nicht möglich, Methoden anhand von generischen Parametern zu unterscheiden, die nur im Typparameter differieren:
Zumindest hierfür kann man sich mit einem kleinen "Workaround" helfen, der jedoch auch keine optimale Lösung darstellt. Man kann sich Klassen definieren, die von der betroffenen generischen Klasse erben und bei der Vererbung bereits einen konkreten Typparameter angeben. So sind diese Klassen selbst nicht mehr generisch, und der Compiler kann sie unterscheiden. Sie dienen quasi als "Alias" für die konkreten Ausprägungen der generischen Klasse.
Um bei der Verwendung von generischen Containern die Methoden der Elemente korrekt aufrufen zu können, muss der Compiler wieder Typecasts einbauen. Würde man die oben gezeigte verkettete Liste mit
Integer
parametrisieren, würden die Elemente, besser gesagt, die Rückgabewerte der Methode
get()
ja u. a. die Methode
intValue()
bereitstellen. Da
get()
im Bytecode jedoch, wie oben ersichtlich, nur noch ein
Object
liefert, muss der Compiler zusätzlich einen Downcast einbauen:
Resultierender Code nach Compilation:
Da also die Downcasts, die durch die Generics im Programmcode nicht mehr nötig sind, noch immer vorhanden sind, ist hier durch die Einführung der Generizität keinerlei Performancegewinn erzielt worden.
Da jedoch auch keine zusätzlichen Klassen, wie z. B. in C++ bei der Verwendung von Templates, erzeugt werden, ist zumindest auch kein Performanceverlust durch die Verwendung von Generics zu beklagen.
instanceof
Dadurch, dass die generischen Objekte auf den entsprechenden Raw Type zurückgeführt werden (man spricht auch vom erased Type der Ausprägungen), sind natürlich auch keine generischen Abfragen per instanceof möglich:
Dies führt dazu, dass man unter einigen Umständen davon ausgehen muss, dass ein Objekt von einem bestimmten generischen Typ ist, und es explizit darauf casten muss. Damit ist ein Ausgangsproblem wieder vorhanden. In diesem Falle spricht man von unchecked casts.
Unchecked Casts
Angenommen, es werden generische Objekte zur Laufzeit serialisiert (das Wissen um die allgemeine Serialisierung von Objekten wird an dieser Stelle vorausgesetzt). Werden diese Objekte später wieder deserialisiert, ist es logischerweise nicht möglich, festzustellen, ob diese Objekte wirklich vom generischen Typ sind, der benötigt wird.
Die Methode
readObject
der Klasse
ObjectInputStream
liefert eine
Object
-Referenz zurück. Diese kann man natürlich per instanceof beispielsweise daraufhin überprüfen, ob es sich wirklich um eine Referenz auf ein
Vector
-Objekt handelt. Mit welchem Typparameter dieses
Vector
-Objekt parametrisiert wurde, muss man jedoch schlicht voraussetzen, was zu einer Warnung des Compilers führt:
Brückenmethoden
Folgendes Codebeispiel:
In Zeile 4 deklariert die Klasse X eine Methode
get()
, die als Parameter ein Objekt vom Typen des Typparameters der Klasse erwartet. Klasse Y erbt nun von der Klasse X, setzt dabei aber für den Typparameter
String
ein. Anschließend scheint die Klasse Y in Zeile 12 die Methode
get()
zu überschreiben, da die Methode im Rahmen der Vererbung die gleiche Signatur hat wie die
get()
-Methode der Basisklasse X.
Erinnert man sich nun jedoch daran, wie der Compiler die Klasse X übersetzt, folgt daraus, dass dort nur noch ein
Object
als Parameter der
get()
-Methode steht. Die Methode der Klasse Y hat also nach der Compilation eine andere Signatur.
Da der Programmierer hiervon aber prinzipiell nichts weiß, simuliert der Compiler die Überschreibung mit einer sogenannten Brückenmethode. Der compilierte und decompilierte Code sieht wie folgt aus:
Die Methode in Zeile 20 wurde vom Compiler eingefügt. Sie überschreibt die Methode der Klasse X und ruft lediglich die theoretisch überschreibende Methode der Klasse Y auf. Somit verhält sich die Klasse Y so, als wäre die Methode überschrieben worden.
Die Brückenmethoden werden sogar bei Methoden angewendet, die sich (bei Rückführung auf Raw Types) nur im Rückgabewert unterscheiden:
Das Interface
Iterator
definiert eine allgemeine Methode
next()
, die eine Objektreferenz vom Typ des Typparameters liefert. Der Compiler macht hieraus wieder einen
Object
-Rückgabewert.
Die Klasse
Alphabet
implementiert die Schnittstelle, jedoch nur für Zeichen. Die Methode
next()
, die folgerichtig ein Zeichen liefert, sollte also das
next()
aus der Schnittstelle
Iterator
korrekt implementieren. Selbst hier erzeugt der Compiler eine Brückenmethode:
Dieser Code wäre im Programmcode nicht erlaubt, da die Bedingung gilt, dass verschiedene Methoden innerhalb einer Klasse sich durch mehr als nur ihren Rückgabewert unterscheiden müssen. In Java Bytecode ist dies jedoch tatsächlich möglich.