Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
CONTACT 
Java Generics - Type Erasure and Raw Types

Java Generics - Type Erasure and Raw Types
Java Generics: Raw Type und Type Erasure

JavaSPEKTRUM, Juli 2007
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

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 Type

Seit 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();
myList.add("Hello World!"); // Zeile 2
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();
myList.add("Hello World!"); // unchecked warning
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>();
myList.add("Hello World!");  // fine
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 Erasure

Schauen 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> {
  protected class Node {
    A elt;
    Node next = null;
    Node (A elt) { this.elt = elt; }
  }
  public void add (A elt) { ...  }
  public A get(int i) { ... }
  ...
}
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 {
  protected class Node {
    Object elt; Node next = null;
    Node (Object elt) { this.elt = elt; }
  }
  public void add (Object elt) { ...  }
  public Object get(int i) { ... }
  ...
}
Die Regeln für die Übersetzung sind dabei:
  • Die Typparameter werden entfernt und durch den ersten Typ in ihrer Bounds-Klausel 1)   ersetzt. Wenn es wie in unserem Fall keine Bounds-Klausel gibt, wird der Typparameter durch Object ersetzt.
  • Zusätzlich werden wo nötig Casts und Brückenmethoden (bridge method) eingefügt.
1)    Wir hatten die Bounds-Klausel in unserem ersten Artikel über generische Typen bereits diskutiert (siehe /KREFT1/). Deshalb hier nur eine kurze Wiederholung. In dem Beispiel hat die Klasse public class TreeMap<Key extends Comparable<Key>,Data> einen Typparameter Key mit der Bounds-Klausel extends Comparable<Key>.  Die Bounds-Klausel beschreibt die Anforderungen, die der generische Typ an seine Typparameter hat. So auch in unserem Beispiel: um eine auf einem Binär-Baum basierende Map zu implementieren, muss der Key-Typ eine Ordnung haben, also das Comparable Interface implementieren. Detaillierte Informationen zur Bounds-Klausel finden sich auch in unserem Generics FAQ (siehe /BOUND/).

Schauen wir uns ein Beispiel an, bei dem ein Cast eingefügt werden muss:

interface Bound1 { void f1(); }
interface Bound2 { void f2(); }

class MyGenericClass<T extends Bound1 & Bound2> {
  private T m1, m2;
  ...
  public void f() {
    m1.f1();
    m2.f2();
  }
}

Wie beschrieben wird der Typparameter durch den ersten Typ aus der Bounds-Klausel, nämlich Bound1, ersetzt:
class MyGenericClass {
  private Bound1 m1, m2;
  ...
  public void f() {
    m1.f1();
    ((Bound2)m2).f2();
  }
}
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 {
  public static void main (String[ ] args) {
    LinkedList<String> ys = new LinkedList<String>();
    ys.add("zero"); ys.add("one");
    String y = ys.get(0);
  }
}
Und hier der Java Code, der dem Ergebnis der Übersetzung entspricht:
final class Test {
  public static void main (String[ ] args) {
    LinkedList ys = new LinkedList();
    ys.add("zero"); ys.add("one");
    String y = (String) ys.get(0);
  }
}
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 Collide

Wie sieht es nun aus, wenn in einem Programm
  • alter Source Code, in dem ein generischer Tyl als Raw Type benutzt wird, und
  • neuer Source Code, in dem eine Parametrisierung desselben generischen Typs vorkommt,
zusammenkommen?

Schauen wir uns dazu ein Beispiel an:

class OldClass {
  ...
  public void foo(List l) {
    String s = (String) l.get(0);
    ...
  }

  public List bar() {
    List l = new ArrayList();
    // ... fill List l with Strings ...
    l.add("Hello World");
    return l;
  }
  ...
}

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();
List<String> l = new ArrayList<String>();
// ... fill List l with Strings ...
oldClassObject.foo(l);
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 warning
String s = ll.get(0);
Beim Ü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 warning
String s = ll.get(0);                    // ClassCastException
Das 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 warning
String s = (String) ll.get(0);   // ClassCastException
Wenn 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/).

Zusammenfassung

In 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:
  • Als Java Entwickler muss man sich trotz der Einführung generischer Typen im JDK 5.0 nicht in neue Typen einarbeiten. Die neuen generischen Typen im JDK 5.0 sind die alten Typen, aber jetzt mit Typparametern, und damit ist ihre Semantik und Funktionalität gleich geblieben.
  • Man kann einen generischen Typ alternativ als Raw Type oder parametrisierten Typ nutzen. Dabei dient die Nutzung des Raw Types der Kompatibilität zu vorhandenem Legacy Code.
  • Zur Laufzeit sind Raw Type und parametrisierter Typ kompatibel, so dass das Mischen von Legacy Code und neuem Code, der parametrisierte Typen nutzt, kein Problem ist.
Wie man sich denken kann, hat die Übersetzungstechnik auf Basis der Type Erasure nicht nur Vorteile. Auf Grund der Type Erasure gibt es einige Überraschungen bei der Benutzung von generischen Typen, die wir uns im nächsten Artikel ansehen wollen.
 

Literaturverweise und weitere Informationsquellen

/FAQ/ Java Generics FAQ
Angelika Langer
URL: http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html
/BOUND/ Java Generics FAQ: What is a type parameter bound?
URL: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeParameters.html#FAQ101
/BRIDGE/ Java Generics FAQ: What is a bridge method?
URL: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102
/POLL/ Java Generics FAQ: What is heap pollution?
URL: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ050

Die gesamte Serie über Java Generics:

/GEN1/  Java Generics - Einführung
Klaus Kreft & Angelika Langer
Java Spektrum, März 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/30.GenericsIntro/30.GenericsIntro.html
/GEN2/  Java Generics - Wildcards
Klaus Kreft & Angelika Langer
Java Spektrum, Mai 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/31.Wildcards/31.Wildcards.html
/GEN3/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, Junli 007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.htm

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Java best practice programming techniques, common pitfalls, and off-the-beaten-path language features
4 day seminar ( open enrollment and on-site)
 

 
  © 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