|
|||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||
|
Java Generics - Type Erasure and Raw Types
|
||||||||||||||||||||
In den letzten zwei Ausgabe dieser Kolumne haben wir das in Java 5.0 neue Sprachmittel der generischen Typen (Java Generics) vorgestellt. Nach einer allgemeinen Einleitung im ersten Beitrag haben wir uns im zweiten Beitrag Wildcard-parametrisierte Typen angesehen. In diesem Beitrag wollen wir uns näher mit dem Raw Type beschäftigen sowie mit der Kompatibilität von neuem Java Code, der parametrisierte Typen verwendet, und Legacy Code, der keine parametrisierten Typen verwendet. Der Raw TypeSeit dem JDK 5.0 gibt es in Java generische Typen. Ein generischer Typ wie List<E> hat einen Typparameter E, den man zur konkreten Parametrisierung durch einen beliebigen Referenztyp ersetzt. Das Ergebnis ist ein parametrisierter Typ wie List<String>, den man ganz normal wie jeden anderen Referenztyp in Java verwenden kann. Das Schöne an einem parametrisierten Type wie List<String> ist, dass der Compiler den Elementtyp der Liste überprüfen kann. So achtet er darauf, dass mit der add() Methode nur Objekt vom Typ String eingefügt werden. Deshalb entfällt auch der lästige explizite Downcast, wenn ein Element mit get() aus der Liste herausgeholt wird. Die get() Methode von List<String> liefert nämlich ein Objekt von Typ String zurück, da mit der add() Methode nur Objekte vom Typ String eingefügt wurden.Da nun in der JavaDoc des JDK 5.0 die java.util.List<E> als generischer Typ mit Typparameter E beschrieben ist, muss man sich eigentlich fragen, wo das gute, alte nicht-generische Interface java.util.List geblieben ist. Die Frage stellt sich insbesondere dann, wenn man das Interface noch in vorhandenem Code verwendet, wie zum Beispiel hier: List myList = new ArrayList();Gleiches gilt natürlich in dem Beispiel auch für die ArrayList sowie für alle anderen Typen, die mit dem JDK 5.0 generische Typen geworden sind. Gibt es die nicht-generischen Typen noch? In der JavaDoc sind sie zumindest nicht zu finden. Trotzdem legt die Tatsache, dass man den Code aus dem Beispiel oben mit dem JDK 5.0 kompilieren kann, nahe, dass man die nicht-generischen Typen noch benutzen kann. Eine Menge Fragen - kommen wir zu den Antworten. In allen Fällen, in denen mit dem JDK 5.0 ein generischer Typ einen nicht-generischen ersetzt hat, z.B. List<E> statt List, gibt es heute nur noch den generischen Typ. Jetzt kann man den generischen Typ aber nicht nur parametrisieren, um ihn zu verwenden, z.B. als List<String> oder List<Integer>, vielmehr kann man auch den Raw Type des generischen Typs, nämlich List, verwenden. Das macht man ganz automatisch, wenn man Legacy Code wie in unserem Beispiel oben, mit dem JDK 5.0 übersetzt. Die Kompatibilität mit bestehendem Legacy Code ist der Hauptgrund, warum der Raw Type überhaupt als mögliche Verwendung eines generischen Typs zugelassen wurde. Oder anders ausgedrückt: mit Hilfe des Raw Types war es nicht nötig, mit dem JDK 5.0 einen vollständig neuen generischen Collection-Framework zu definieren. Stattdessen hat man bei Sun den alten Framework mit allen Interfaces und Klassen hergenommen und diese sinnvoll in generische Typen überführt. Dies gilt nicht nur für den Collection-Framework, sondern für alle Klassen des JDK. So ist zum Beispiel im JDK 5.0 sogar java.lang.Class<T> ein generischer Typ. Die Kompatibilität mit vorhandenem Legacy Code ist eine wichtige Voraussetzung für die Akzeptanz der generischen Typen in der Java Community. Jeder Entwickler kann parametrisierte Typen in dem Maße benutzen, wie er möchte: entweder er nutzt sie als parametrisierten Typ wie List<String> oder als Raw Type wie List. Mit dem Raw Type ist wieder alles so wie vor dem JDK 5.0, so dass jeder Entwickler im Grunde die Möglichkeit hat, Java Generics mehr oder weniger zu ignorieren. Dabei ist die Nutzung des Raw Types, d.h. der Verzicht auf parametrisierte Typen, in neuen Implementierungen keine gute Alternative: man verliert die Typprüfung durch den Compiler und damit den wesentlichen Vorteil von generischen Typen. Der Compiler weist auch darauf hin. Wenn man das Beispiel von oben übersetzt, wird zwar Code erzeugt, aber beim Compilieren meldet der Compiler für die Zeile 2 eine sogenannte unchecked warning. List myList = new ArrayList();Die Warnung bedeutet, dass der Compiler den Parameter der add() Methode nicht prüfen konnte, da myList vom Raw Type ist und deshalb jegliche Information über den Typ der Listenelemente fehlt. Die Alternative mit korrekt parametrisierten Typen und ohne Compiler-Warnungen sieht so aus: List<String> myList = new ArrayList<String>();Nicht alle Methodenaufrufe auf Raw-Type-Objekten führen zu Warnungen. Aber immer dann, wenn man eine Methode auf einem Objekt des Raw Types aufruft, bei der einer oder mehrere Methodenparameter vom Typ eines Typparameters sind, erfolgt eine unchecked-Warnung. Gleiches gilt auch, wenn man einen Konstruktor des Raw Types aufruft, bei dem einer oder mehrere Konstruktorparameter vom Typ eines Typparameters sind. Nur wenn der Typparameter als Returntyp oder überhaupt nicht in der Methodensignatur auftaucht, entfällt die Warnung. In Legacy Code verwendet man automatischen den Raw Type und in neuen Implementierungen soll man stattdessen parametrisierte Typen verwenden. Das wirft unweigerlich neue Fragen auf: Wie funktioniert das an der Schnittstelle zwischen neuem und altem Code? Kann man einen parametrisierten Typ an eine Methode übergeben, die einen Raw Type verlangt? Und umgekehrt? Wie funktioniert das bei Zuweisungen? Auch hier muss es ein hohes Maß an Kompatibilität geben, denn sonst könnten parametrisierte Typen erst einmal nur auf neu implementierten Inseln’ verwendet werden. Entsprechend hatte die Kompatibilität zwischen Raw Type und parametrisieren Typen bei der Entwicklung der generischen Typen durch Sun oberste Priorität. Im Folgenden wollen wir uns ansehen, wie diese Kompatibilität erreicht wird. Danach kommen wir noch einmal auf das Thema „Kompatibilität von Raw Type und parametrisiertem Typ“ zurück und diskutieren die Details ausführlich. Im nächsten Artikel beschäftigen wir uns mit den Konsequenzen und Einschränkungen dieser Ansatzes. Type ErasureSchauen wir uns zuerst an, wie generische Typen nach der Kompilierung aussehen. Fangen wir dazu mit der folgenden Definition eines generischen Typs an:class LinkedList<A> implements List<A> {Diese Definition wird vom Java Compiler zu Byte-Code übersetzt, der einer normalen (d.h. nicht-generischen) Klasse entspricht, die in unserem Beispiel fast genauso aussieht wie die alte, nicht-parametrisierte Version der LinkedList: class LinkedList implements List {Die Regeln für die Übersetzung sind dabei:
Schauen wir uns ein Beispiel an, bei dem ein Cast eingefügt werden muss: interface Bound1 { void f1(); }Wie beschrieben wird der Typparameter durch den ersten Typ aus der Bounds-Klausel, nämlich Bound1, ersetzt: class MyGenericClass {Zusätzlich ist ein Cast nötig, um m2.f2() aufzurufen. Es ist sichergestellt, dass der Cast nicht scheitert, da der Typparameter auf Grund seiner Bounds-Klausel sowohl das Interface Bound1 als auch das Interface Bound2 implementieren muss. Was es mit den Brückenmethoden auf sich hat und wann sie benötigt werden, wollen wir hier nicht weiter diskutieren. Wer sich dafür interessiert, kann es in unserem Generics FAQ nachlesen (siehe /BRIDGE/). Halten wir fest: ein generischer Typ entspricht im Byte-Code einem ganz normalen Referenztyp, bei dem der Typparameter durch seinen ersten Bounds-Typ oder - falls er keinen hat - durch Object ersetzt wird. Dieses Vorgehen nennt sich Type Erasure, da der Typparameter gelöscht wird und durch einen ganz normalen Referenztyp ersetzt wird. Da diese Transformation allein in einigen Situationen nicht ausreicht, werden wo nötig Casts und Brückenmethoden eingefügt. Das heißt, nach der Kompilierung ist ein generischer Typ ein ganz normales Interface oder eine ganz normale Klasse. Damit das funktioniert, muss der Compiler auch an der Stelle eingreifen, an der der generische Typ als parametrisierter Typ genutzt wird. Dazu ein Beispiel in dem die LinkedList<A> aus unserem Beispiel oben genutzt wird. Hier der Code vor der Compilierung: final class Test {Und hier der Java Code, der dem Ergebnis der Übersetzung entspricht: final class Test {Wie man sieht, muss der Compiler beim Herausnehmen eines Elements aus der LinkedList (Aufruf der get() Methode) einen Cast nach String einfügen. Das ist nötig, da in der übersetzten LinkedList (d.h. zur Laufzeit) die Elemente nur vom Typ Object sind. Trotzdem ist sichergestellt, dass der Cast niemals schietert, weil der Compiler beim Übersetzen prüft, ob die Elemente, die mit add() in die LinkedList<String> eingefügt werden, auch wirklich vom Typ String sind. Fassen wir das bisher Gesagte noch mal zusammen: zur Laufzeit ist der generische Typ ein ganz normale Referenztyp, der fast 2) nichts mehr von seiner Parametrisierung weiß. Dies gilt ganz besonders für ein Objekt eines parametrisierten Typs. Die LinkedList<String> weiß zur Laufzeit nicht, dass sie mit String parametrisiert wurde. Die Typprüfung auf Grund der Parametrisierung mit String erfolgt nur durch den Compiler zum Übersetzungszeitpunkt; zur Laufzeit ist keinerlei Typinformation über den Parametertyp mehr vorhanden. 2) Das einschränkende fast’ hat für die Diskussion über die Type Erasure keine Relevanz. Es gibt aber sehr wohl zur Laufzeit Unterschiede zwischen generischen Typen und normalen Referenztypen. Diese schauen wir uns im Detail im nächsten Artikel an. When Worlds CollideWie sieht es nun aus, wenn in einem Programm
Schauen wir uns dazu ein Beispiel an: class OldClass {Die Klasse OldClass ist vor dem JDK 5.0 implementiert worden und nutzt daher den Raw Type von List<E>. Sie hat unter anderem die Methoden foo() und bar(). Die Methode foo() nimmt eine List als Aufrufparameter; die Methode bar() gibt eine List zurück. In beiden Fällen handelt es sich um eine List von Strings, was man aber an den Signaturen nicht sehen kann. Um zu wissen, was der Elementtyp der List ist, muss man sich die Implementierung der betreffenden Methode ansehen. Jetzt wollen wir die beiden Methoden aus neu implementierendem Source Code, der parametrisierte Typen nutzt, aufrufen. Fangen wir mit foo() an: OldClass oldClassObject = new OldClass();Alles funktioniert problemlos. Der Code lässt sich übersetzen und zur Laufzeit beim Aufruf von foo() ist das übergebene List-Objekt wegen der Type Erasure genau vom erwarteten Typ. Natürlich geht der Cast auf String in der ersten Zeile von foo() auch gut, da wir ein Objekt vom Typ List<String> übergeben habe. Schauen wir nun, was beim Aufruf von bar() geschieht: List<String> ll = oldClassObject.bar(); // unchecked warningBeim Übersetzen gibt es in der ersten Zeile eine unchecked warning. Das ist eigentlich nicht verwunderlich, denn bar() liefert eine List zurück und ll ist vom Typ List<String>. Der Compiler kann bei der Zuweisung der List an die List<String> die Typkompatibilität nicht prüfen: woher soll er wissen, ob in der List wirklich nur Strings abgelegt sind oder nicht? Um die Kompatibilität zwischen neuem und altem Code zu ermöglichen, lässt der Compiler die Zuweisung aber trotzdem zu. Allerdings gibt er eine Warnung, weil er die Zuweisungsverträglichkeit nicht hat prüfen können. Beim Ablauf des Code geht alles gut, da in bar() nur Strings in der Liste abgelegt worden sind. Was wäre geschehen, wenn bar() Integers statt Strings in die Liste eingefügt hätte? Der Compiler hätte die Zuweisung einer solchen Raw-Type-Liste mit Integer-Elementen an die List<String> erlaubt – zwar mit Warnung, aber Warnungen kann man ja ignorieren. Dann hätte es beim Ablauf in der 2. Zeile eine ClassCastException gegeben: List<String> ll = oldClassObject.bar(); // unchecked warningDas liegt daran, dass der Compiler bei der Übersetzung im Zuge der Type Erasure einen Cast auf String einfügt hat. Hier der Code nach der Type Erasure: List ll = oldClassObject.bar(); // unchecked warningWenn in der Liste ll Integers anstelle von Strings enthalten sind, dann scheitert der hineingenerierte Cast auf String natürlich zur Laufzeit. Mit ähnlichen Argumenten wie oben kann man sich überlegen, dass nicht nur der Aufruf von alten APIs aus neuem Code, sondern auch das Umgekehrte, nämlich der Aufruf von neuen APIs aus altem Code, funktioniert. Zusammenfassend lässt sich sagen: wenn ein Objekt des Raw Types an eine Variable (oder einen Methodenparameter) von parametrisiertem Typ übergeben wird, dann meldet der Compiler eine unchecked warning. Zur Laufzeit kann es später zu einer ClassCastException kommen, wenn das Raw-Type-Objekt Elemente eines anderen als des erwarteten Typs enthält. Dieses Problem bestand vor dem JDK 5.0 schon in genau der gleichen Form. Allerdings war die ClassCastException früher weniger überraschend als heute in Java 5.0, weil der Cast nach String im Source-Code deutlich sichtbar war, wohingegen nun in Java 5.0 eine ClassCastException an einer Stelle im Source-Code ausgelöst wird, an der weit und breit kein Cast zu sehen ist. Es ist der vom Compiler „heimlich“ eingefügte Cast, der scheitert – und das erschwert die Fehlersuche in solchen Situation. 3) 3) Eine solche Situation, in der eine Variable von einem parametrisierten Typ auf ein Objekt von einem unpassenden Typ verweist, wird übrigens als Heap Pollution bezeichnet (Details siehe /POLL/). ZusammenfassungIn diesem Artikel haben wir uns angesehen wie generische Typen in Java mit Hilfe der Type Erasure Technik implementiert sind, um auf Basis des Raw Types ein hohes Maß an Kompatibilität mit den alten nicht-generischen Typen zu erhalten. Hier noch einmal die sich ergebenden Vorteile in der Übersicht:
Literaturverweise und weitere Informationsquellen
Die gesamte Serie über Java Generics:
|
|||||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.html> last update: 4 Nov 2012 |