Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | 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 
NEWSLETTER 
CONTACT 
Java Generics - Type Erasure Pitfall

Java Generics - Type Erasure Pitfall
Java Generics: Type Erasure
Konsequenzen und Einschränkungen

JavaSPEKTRUM, September 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 der letzten Ausgabe dieser Kolumne haben wir uns angesehen, wie mit Hilfe der Type Erasure Technik generische Typen in Java übersetzt werden und welche Repräsentation sie dann zur Laufzeit haben. Die Type Erasure dient in erster Linie der Kompatiblität von altem Code, der keine parametrisierten Typen verwendet, und neuen Code, der von den parametrisierten Typen in Java 5.0 Gebrauch macht. Diesmal wollen wir uns einige Konsequenzen ansehen, die sich aus der Type Erasure ergeben.

Type Erasure

Fangen wir noch einmal mit einer kurzen Wiederholung der Type Erasure an. Als Type Erasure wird die Technik bezeichnet, mit der generische Typen in Java übersetzt werden. Die Regeln für die Übersetzung sind dabei:
  • Aus einem generischen Typ wird ein nicht-generischer Typ, indem die Typparameter entfernt und durch den ersten Typ in ihrer jeweiligen Bounds-Klausel ersetzt werden. Wenn es keine Bounds-Klausel gibt, wird der Typparameter durch Object ersetzt.
  • Aus einem parametrisierten Typ wird ein Raw Type, indem die Typargumente entfernt werden.
  • Zusätzlich werden, wo dies nötig ist, Casts und Brückenmethoden eingefügt.
Schauen wir uns dazu ein Beispiel an.  Hier ist die Definition und Verwendung einer generischen Klasse:
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) { ... }
  ...
}
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);
  }
}
Dieser Source-Code wird vom Java Compiler zu Byte-Code übersetzt, in dem nur noch normale (d.h. nicht-generische bzw. nicht-parametrisierte) Typen vorkommen:
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) { ... }
  ...
}
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);
  }
}
Zur Laufzeit ist der generische Typ LinkedList<A> ein ganz normaler Referenztyp, der nichts mehr von seiner Parametrisierung weiß. Dies gilt auch für ein Objekt eines parametrisierten Typs: die LinkedList<String> weiß zur Laufzeit nicht, dass sie mit String parametrisiert wurde. Das heißt, sämtliche Typprüfungen auf Basis der Parametrisierung mit String erfolgen nur durch den Compiler zum Übersetzungszeitpunkt; zur Laufzeit ist keinerlei Typinformation über den Parametertyp mehr vorhanden.

Typisierung und Cast

Soweit wir das im letzten Artikel betrachtet haben, hat die Art und Weise, wie die generischen Typen in Java implementiert sind, nur Vorteile:
  • 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 unverändert 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.
Gemessen an diesen Vorteilen sind die Einschränkungen als eher gering zu bewerten. Kern der Einschränkungen ist der Effekt, dass auf Grund der Type Erasure die vollständige Typinformation zu einem parametrisierten Typ zur Laufzeit nicht mehr bekannt ist. Was das heißt, wollen wir im Folgenden genauer diskutieren. Fangen wir dazu mit einigen elementaren Überlegungen zum Java Typsystem an:
 
              Anweisung
   statisch
dynamisch
String stringRefToString = new String();
String
String
Object objectRefToString = new String();
Object
String
Object objectRefToObject = new Object();
Object
Object

Die Tabelle enthält in der ersten Spalte drei Variablendefinitionen mit Konstruktion und Zuweisung von Objekten. Die zweite und dritte Spalte zeigen jeweils den statischen Typ der Variable bzw. den dynamischen Typ des Objekts aus der vorhergehenden Anweisung. Der statische Typ wird vom Compiler für Prüfung bei Zuweisungen sowie bei der Übergabe von Methodenparametern und Returnwerten verwendet. Daneben wird der statische Typ vom Compiler auch zur Overload Resolution verwendet. Der dynamische Typ hingegen wird zu Laufzeitprüfungen benutzt, etwa beim Cast oder bei der Benutzung des Operators  instanceof. Typisch für Java ist, dass der dynamische Typ mindestens so exakt, wenn nicht exakter ist, als der statische Typ.

Wie sieht das Ganze nun bei parametrisierten Typen aus?  Sehen wir uns einige Beispiel mit parametrisierten Typen an:
 

              Anweisung
              statisch
    dynamisch
LinkedList<Integer> refToInegerList = 
               new LinkedList<Integer>();
LinkedList<Integer>
LinkedList
LinkedList<String> refToStringList = 
               new LinkedList<String>();
LinkedList<String>
LinkedList
Object objectRefToStringList = 
               new LinkedList<String>();
Object
LinkedList

Auf Grund der Type Erasure sind statischer und dynamischer Typ nie gleich. Auch ist der dynamische Typ nicht mindestens so exakt wie der statische; das kann man an den ersten beiden Anweisungen sehen. Ganz offensichtlich werden einige Regeln des Java Typsystems bei der Benutzung parametrisierter Typen neu definiert. Schauen wir uns an, was es in der Praxis bedeuet.

LinkedList<String>  refToStringList  = new LinkedList<String>();
LinkedList<Integer> refToIntegerList = new LinkedList<Integer>();
refToStringList = refToIntegerList;   // Zeile 3: inkompatibel – lässt sich nicht übersetzen

LinkedList<String>  refToStringList  = new LinkedList<String>();
Object objectRefToStringList  = new LinkedList<String>();
refToStringList = objectRefToStringList;  // Zeile 6: inkompatibel – lässt sich nicht übersetzen

Der Compiler nutzt weiterhin den statischen Typ, um die Typverträglichkeit bei Zuweisungen zu prüfen. Damit ergibt sich, dass die Zeilen 3 und 6 nicht übersetzbar sind, da in beiden Fällen der Typ der Variable auf der rechten Seite der Zuweisung inkompatibel zum Typ auf der linken Seite ist. Soweit ist alles genauso wie bei nicht-parametrisierten Typen. Was ist, wenn man die Zeile 6 folgendermaßen ändert?
LinkedList<String>  refToStringList  = new LinkedList<String>();
Object objectRefToStringList  = new LinkedList<String>();
refToStringList = (LinkedList<String>)objectRefToStringList;  // Zeile 6: lässt sich nun übersetzen
Der zusätzliche Cast führt dazu, dass die Zeile sich nun kompilieren lässt und zur Laufzeit gibt es auch keine Probleme, da beide Seiten vom Typ LinkedList sind.

Was passiert aber, wenn die Object-Referenz in Zeile 5 gar nicht auf eine LinkedList<String> sondern auf eine LinkedList<Integer> verweist?

LinkedList<String>  refToStringList  = new LinkedList<String>();
Object objectRefToStringList  = new LinkedList<Integer>();
refToStringList = (LinkedList<String>)objectRefToStringList;  // Zeile 6: lässt sich immer noch übersetzen
Auch das lässt sich wegen des eingefügten Casts compilieren. Es entspricht auch unseren Erwartungen, denn wir haben den Cast ja explizit eingefügt, um den Compiler dazu zu bringen, die rechte Seite als LinkedList<String> zu interpretieren. Der Code bringt aber auch beim Ablauf keinen Fehler - und das entspricht nicht ganz unseren Vorstellungen. Eigentlich hätten wir eine ClassCastException erwartet: auf der linken Seite der Zuweisung steht eine LinkedList<String> und auf der rechten  Seite eine LinkedList<Integer>. Also sollte die Typprüfung auf Grund des Casts zur Laufzeit die Unverträglichkeit der Typen feststellen und per Exception melden.  Genau das passiert aber nicht, weil die beteiligten Objekte nur im Source Code von unterschiedlichen Typen sind. Nach dem Compilieren und der Type Erasure steht zur Laufzeit auf beiden Seiten der Zuweisung eine LinkedList und deshalb gibt es keinen Grund, warum die dynamische Typprüfung des Casts scheitern sollte.

Dieser Effekt ist wirklich eine Überraschung, denn zumindest für parametrisierte Typen gilt nicht mehr, dass man zur Laufzeit den exakten Typ eines Objekts ermitteln kann. Das liegt an der Type Erasure: das Typargument eines parametrisierten Typs ist zur Laufzeit nicht mehr vorhanden. Sehr pointiert kann man es mit dem folgenden Vergleich illustrieren, der immer true liefert:

(new LinkedList<Integer>()).getClass() == (new LinkedList<String>()).getClass()


Kommen wir noch einmal auf das Beispiel von oben zurück. Was passiert eigentlich, nachdem sich die Zuweisung der LinkedList<Integer> an die LinkedList<String> wegen des Casts übersetzen lässt und die dynamische Prüfung zur Laufzeit auch nicht fehlschlägt? Macht es sich irgendwie bemerkbar, dass eine Variable vom Typ LinkedList<String> auf ein Objekt vom Typ LinkedList<Integer> verweist, oder macht das gar nichts?  Stellen wir uns den weiteren Verlauf des Programms vor. Wahrscheinlich wird irgendwann auf refToStringList  zugegriffen:

LinkedList<String>  refToStringList  = new LinkedList<String>();
Object objectRefToStringList  = new LinkedList<Integer>();
// ... objectRefToStringList  mit Integer füllen ...
refToStringList = (LinkedList<String>)objectRefToStringList;
// ...
String tmpString = refToStringList.get(0);
Wie wir von unserer Diskussion oben wissen, erzeugt der Compiler bei der Übersetzung per Type Erasure folgenden Code:
LinkedList refToStringList  = new LinkedList();
Object objectRefToStringList  = new LinkedList();
// ... objectRefToStringList  mit Integer füllen ...
refToStringList = (LinkedList)objectRefToStringList;
// ...
String tmpString = (String)refToStringList.get(0);
Beim Zugriff auf die Liste von Integers über die Variable refToStringList geht dann der compiler-generierte Cast von Integer auf String schief, so dass es doch noch zu einem Fehler kommt. Ideal ist diese Situation natürlich nicht, weil man die eigentlich Fehlerursache (die inkompatible LinkedList Zuweisung) erst noch suchen muss – und das kann mühselig sein, weil das Fehlersymptom (die ClassCastException) vom der Fehlerursache (der fragwürdigen Zuweisung) sehr weit entfernt sein kann.

Allerdings ist man als Entwickler vorgewarnt, denn der Compiler meldet eine unchecked-Warnung, wenn der Zieltyp eines Casts ein parametrisierter Typ ist.  Das heißt, auf den Cast nach LinkedList<String> in der fragwürdigen Zuweisung in unserem Beispiel wird vom Compiler ausdrücklich hingewiesen, weil der Cast im Source-Code exakter aussieht, als er zur Laufzeit nach der Type Erasure tatsächlich ausgeführt wird.  Man sollte solche unchecked-Warnungen also durchaus ernst nehmen.

Weitere Einschränkungen und Regeln

Wie gerade ausführlich diskutiert, kann man den folgenden Cast hinschreiben:
refToStringList = (LinkedList<String>)objectRefToStringList;
obwohl die Prüfung zur Laufzeit den Typparameter String gar nicht berücksichtigt. Beim instanceof-Operator ist der Compiler strenger.  Auf der rechten Seite des instanceof-Operators darf nur der Raw Type stehen, nicht aber ein parametrisierter Typ. Damit ist unmissverständlich klar, dass Typparameter bei der instanceof-Prüfung keine Rolle spielen:
if (o instanceof LinkedList) …         // okay
if (o instanceof LinkedList<String>) … // Fehler beim Kompilieren
instanceof und Cast unterliegen also verschiedenen Regeln. Das liegt daran, dass der Cast, je nach Situation, auch Compilezeit-Effekte hat, instanceof aber immer nur zur Laufzeit relevant ist.   Ein Beispiel für die Compilezeit-Relevanz ist ein Cast von String nach Integer.  Der Compiler weiss, dass ein Cast von String nach Integer (im Gegensatz zu einem Cast von Object nach Integer) niemals erfolgreich sein kann, und meldet einen Fehler.

Aus ähnlichen Gründen wie oben gibt es keine Class-Literale für parametrisierte Typen, sondern nur für den Raw Type:

Class<? extends LinkedList> c1 = java.util.LinkedList.class;          // okay
Class<? extends LinkedList> c2 = java.util.LinkedList<String>.class;  // Fehler beim Kompilieren

Statische Felder und Methoden einer generischen Klasse

Wenn man sich erst einmal daran gewöhnt hat, dass es für alle Parametrisierungen sowie den Raw Type eines generischen Typs nur genau eine Klassenrepräsentation zur Laufzeit gibt, dann kann man sich vorstellen, dass static members (also statische Felder und Methoden) eines generischen Typs ebenfalls besonderen Regeln unterliegen.

Schauen wir uns dazu das Beispiel eines generischen Typs mit einem statischen Feld an:

public class MyClass<T> {
  public static int cnt = 0;
   …
}
Im Folgenden nutzten wir nun zwei Parametrisierungen dieses generischen Typs:
MyClass<Integer> myi = new MyClass<Integer>();
MyClass<String>  mys = new MyClass<String>();
myi.cnt++;
mys.cnt++;
Die Frage ist nun, ob es zwei verschiedene cnt Felder (je eines pro Parametrisierung) oder überhaupt nur eines gibt. Die Antwort ist: es gibt nur ein Feld, das den Wert 2 enthält, nachdem der Code oben durchlaufen wurde. Dies ist naheliegend, wenn man bedenkt, dass es nach der Type Erasure nur noch eine Klasse gibt, die sowohl MyClass<Integer> als auch MyClass<String> repräsentiert.

Was wir hier am Beispiel des Feldes cnt besprochen haben, gilt grundsätzlich für alle statischen Felder und Methoden: das jeweilige statische Feld bzw. die jeweilige statische Methode existiert nur genau einmal für alle Parametrisierungen des generischen Typs und seinen Raw Type.

In Java ist es möglich, ein statisches Element über ein Objekt anzusprechen, zum Beispiel als: myi.cnt++. Die Anweisung führt zu einer Warnung: „The static field cnt should be accessed in a static way.” Das heißt, das Feld soll über eine Klasse statt über ein Objekt angesprochen werden. Wie macht man dass bei einer generischen Klasse? Spricht man statische Felder über den parametrisierten Typ an?  Es wäre zumindest denkbar, dass man das statische cnt-Feld des parametrisierten Typs MyClass<Integer> als MyClass<Integer>.cnt anspricht.  Das ist aber nicht zulässig. Wie beim Class-Literal und beim instanceof Operator ist nur der Raw Type zulässig:

MyClass.cnt++; // okay
MyClass<String>.cnt++;  // Fehler beim Kompilieren
MyClass<Integer>.cnt++; // Fehler beim Kompilieren
Der Zugriff über den parametrisierten Typ führt dagegen zu einem Fehler beim Kompilieren.

Es gibt noch  eine weitere gravierende Einschränkung bei generischen Typen und statischen Elementen: ein Typparameter des generischen Typs darf nicht in der Definition von statischen Feldern oder statischen Methoden verwendet werden. Das bedeutet, dass die folgenden drei Definitionen Fehler beim Kompilieren liefern:

public final class X<T> {
  private static T field;                           // Fehler beim Kompilieren
  public  static T getField() { return field; }     // Fehler beim Kompilieren
  public  static void setField(T t) { field = t; }  // Fehler beim Kompilieren
}
Das Problem ist: es ist unklar, wofür die Typvariable T in einem statischen Kontext steht. Welchen Returnwert hat zum Beispiel X.getField()? Es gibt nur ein statisches Feld field, unabhängig von der Parametrisierung von X. Naheliegend wäre also, dass das Feld field und der Returnwert von X.getField() vom Typ Object sind. Aber dann macht die Parametrisierung der Klasse keinen Sinn und letztlich wäre eine Implementierung der Klasse als nicht-generische Klasse unter Verwendung von Object statt T deutlich klarer. Und genau deshalb gibt es die Regel: ein Typparameter des generischen Typs darf nicht in der Definition von statischen Feldern oder statischen Methoden verwendet werden.

Das bedeutet aber nicht, dass statische Methoden nicht generisch sein können. Ganz im Gegenteil. Es gibt viele Beispiele für generische, statische Methoden. Zum Beispiel die Methoden der Klasse java.util.Collections:

class Collections {
   public static <T> void sort(List<T> list, Comparator<? super T> comp);
   // ... und so weiter ...
}
Die Klasse Collections ist selbst nicht generisch, sondern bildet lediglich die Hülle um eine Ansammlung von jeweils generischen Methoden. Die statische Methode sort() ist generisch und sortiert eine Liste list mit Hilfe der durch den Comperator comp definierten Ordnung. Die Methode hat einen eigenen Typparameter T, der den Elementtyp der Liste repräsentiert.

Unterschiede zwischen generischen Typen und normalen Referenztypen zur Laufzeit

Wenn wir Vorträgen zu diesem Thema halten, kommen manchmal Einwände bei der Aussage, dass nach der Type Erasure die Typparameter nicht mehr bekannt sind. Unterstrichen werden die Einwände meist mit dem Verweis auf neue Interfaces im Package java.lang.reflect. Diese sind speziell mit dem JDK 5.0 eingeführt worden, um über Reflection Information über generische Typen zu erhalten. Beispiele für neue Interfaces sind GnericDeclaration, sowie Type mit seinen Sub-Interfaces GenericArrayType, ParametrizedType, TypVariable<D> und WildcardType.

Schaut man sich diese Interfaces einmal genauer an, so stellt man fest, dass sich die Information, die man über diese Interfaces bekommt, nicht auf den Laufzeittyp der Typparameter bezieht. Was man erfragen kann, sind vielmehr die Details des formalen Typparameters eines generischen Typs. So liefert zum Beispiel TypeVariable<D>.getBounds() die Typen der Boundsklausel und TypeVariable<D>.getName() den Namen des Typparameters im Sourcecode. Schauen wir uns dazu ein Beispiel an:

LinkedList<String> l = new LinkedList<String>();

TypeVariable tp = (l.getClass().getTypeParameters())[0];

System.out.println("name: " + tp.getName());

Type[] types = tp.getBounds();
if (types[0] instanceof Class)
  System.out.println("classname: " + ((Class)types[0]).getName());

Wir initialisiern l mit einem LinkedList<String>-Objekt und holen uns in tp ein Objekt, das den ersten Typparameter von LinkedList repräsentiert. Dann geben wir den Namen dieses Typparameters und den Klassennamen seines ersten Bounds aus. Die Ausgabe des Programms ist:
name: E
classname: java.lang.Object
Dies stimmt genau mit der Beschreibung von java.util.LinkedList<E> in der JavaDoc und dem Sourcecode der Implementierung der LinkedList überein. Der Name des Typparameters im Sourcecode ist E und der Typparameter E hat keine expliziten Bounds; deshalb kommt als erstes (und einziges) Bound Object heraus.

Wie man sieht, ist diese Information nicht dazu geeignet, etwas über den aktuellen Typparameter unserer LinkedList<String> zu erfahren.  Was man über Reflection erhält, ist ausschließlich statische Typinformation.

Zusammenfassung

In diesem Artikel haben wir uns die Auswirkungen der Type Erasure Technik auf die Java Programmierung angesehen. Die zwei wichtigsten Erkenntnisse sind dabei:
  • Bei parametrisierten Typen ist der aktuelle Typparameter zur Laufzeit nicht mehr vorhanden. Das heißt, dass man die typische Java-Programmiertechnik, den vollständigen Typ zur Laufzeit durch Cast, instanceof, usw. zu ermitteln, nicht problemlos einsetzen kann. Siehe dazu unser Beispiel aus dem Absatz „Typisierung und Cast“.
  • Um deutlich zu machen, dass es zur Laufzeit nur eine Repräsentation für einen generischen Typ gibt, die für alle Parametrisierungen und den Raw Type relevant ist, darf man an vielen Stellen, an denen auf die Repräsentation verwiesen wird, nur den Raw Type und keinen parametrisierten Typ benutzten. Diese Stellen sind Class-Literale, instanceof-Operator und der Zugriff auf statischen Felder und Methoden.

Literaturverweise und weitere Informationsquellen

/FAQ/ Java Generics FAQ
Angelika Langer
URL: http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html

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, Juli 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.html
/GEN4/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, September 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/33.TypeErasurePitfall/33.TypeErasurePitfall.html
/GEN5/ Java Generics - Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, November 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/34.GenericCreation/34.GenericCreation.html

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/33.TypeErasurePitfall/33.TypeErasurePitfall.html  last update: 4 Nov 2012