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

Java Generics - Type Erasure
Java Generics - Type Erasure
 

JavaMagazin, Oktober 2004
Angelika Langer & Klaus Kreft


 

Umwälzungen im Java-Typsystem

Generische Typen,  Type Erasure und wie sie sich das Ganze auf das Java-Typsystem auswirkt

Java Generics sind ein Sprachmittel, das in J2SE 5.0 neu zur Programmiersprache Java hingekommen ist.  Das neue Sprachmittel erlaubt die Benutzung und Definition von Typen und Methoden, die mit Typvariablen parametrisiert sind (z.B. LinkedList<String>).   Die Integration der parametrisierten Typen in das Typsystem von Java hat interessante und zum Teil auch überraschende Auswirkungen auf das Java-Typsystem.  In diesem Artikel wollen wir uns einige dieser Effekte ansehen. Unter anderem wollen wir erklären, warum Arrays von parametrisierten Typen in Java nicht erlaubt sind und was die sogenannten "Checked Collections" sind.
 

Wenn man mit Java Generics programmiert, stellt man rasch fest, dass  die Semantik des Sprachmittels bisweilen überraschend ist.  Manche Dinge, von denen man intuitiv erwarten würde, dass sie problemlos möglich sind, sind in Java Generics  nicht erlaubt.  Beispielweise könnte man erwarten, dass man Instanziierungen eines parametrierten Typs, wie zum Beispiel LinkedList<String>, genauso verwenden kann wie einen regulären nicht-parametrisierten Typen, wie zum Beispiel String.  Das ist aber nicht so;  für parametrisierte Typen gibt es eine Reihe von Einschränkungen.  Diese Limitationen muss man kennen, zum einen, um die entsprechenden Compiler-Meldungen zu verstehen, zum anderen, um Fehler zu vermeiden. Beispielsweise führt ein Cast wie (LinkedList<String>)  ref zu folgender Warnung: warning: "unchecked cast". Der entsprechende instanceof-Ausdruck (ref instanceof LinkedList<String>)  ist gleich ganz verboten: error: "illegal generic type for instanceof".  Da stellt sich die Frage: Was ist mit dem instanceof-Ausdruck nicht in Ordnung?  Was ist ein "unchecked cast"?  Was will der Compiler mit dieser Warnung sagen?  Muß man diese Warnung ernst nehmen?  Diesen und anderen Fragen wollen wir in diesem Artikel nachgehen.

Um die Effekte erläutern zu können, müssen wir etwas tiefer in das Typsystem von Java einsteigen.  Wir haben in einem vorangegangenen Artikel (siehe / MAG1 /) bereits erklärt, dass der Java-Compiler generisches Java in Java-Bytecode übersetzt, indem er eine sogenannte "Type Erasure" durchführt. Bei der Übersetzung per Type Erasure werden die Typparameter eines Typs oder einer Methode entfernt, so dass zur Laufzeit parametrisierte Typen nicht mehr von regulären Typen unterschieden werden können.  Diese Übersetzungstechnik ist der Grund für die Effekte, denen wir uns in diesem Artikel widmen wollen.  Wir beginnen deshalb mit einer Gegenüberstellung der Typinformation, die für reguläre Typen einerseits und parametrisierte Typen andererseits zur Verfügung steht.  Anschließend gehen wir der Frage nach, welche Einschränkungen es für die Benutzung von parametrisierten Typen gibt und worauf man im Umgang mit parametrisierten Typen achten sollte.
 

1 Nicht-Exakte Laufzeit-Typinformation

Referenzvariablen haben in Java (wie auch in anderen Sprachen) einen statischen und einen dynamischen Typ.  Etwas vereinfacht kann man sagen, der statische Typ ist beim Übersetzen relevant und der dynamische Typ wird zur Laufzeit verwendet.  Traditionell ist es in Java so, dass der dynamische Type exakter ist als der statische Typ.  Das ist zum Beispiel bei einer Referenzvariablem vom Typ Object so.  Der statische Typ der Referenz ist Object und sagt nicht viel über den Typ des referenzierten Objekts.  Zur Laufzeit jedoch spiegelt der dynamische Typ der Referenz präzise den Typ des referenzierten Objekts wider.  In diesem Sinne ist in Java traditionell der dynamische Typ exakter als der statische Typ.  Java Generics brechen mit dieser Tradition – ein Bruch, der gewöhnungsbedürftig ist.  Sehen wir uns im Folgenden den Effekt genauer an.
 

1.1 Typprüfungen in Nicht-Generischem Java

Um den Unterschied zwischen der Typinformation über generische und nicht-generische Typen zu erläutern, betrachten wird zuerst einmal die nicht-generischen, regulären Typen.  Die nachfolgende Tabelle zeigt Beispiele, die den Unterschied zwischen dem statischen und dynamischen Typ einer Referenzvariable illustrieren.
 
statischer Typ dynamischer Typ
String stringRefToString = new String(); String String
Object objectRefToString = new String();  Object String
Object objectRefToObject = new Object();  Object Object

Der statische und der dynamische Typ einer Variable sind immer dann potentiell verschieden, wenn eine Referenzvariable auf ein Objekt eines Subtyps verweist. Der zweite Eintrag in der Tabelle zeigt ein Beispiel: eine Variable vom Typ Object verweist auf ein Objekt vom Typ String. Der statische Typ ist der Typ der Variablen, nämlich Object, und der dynamische Typ ist der Typ des referenzierten Objekts, nämlich String.

Wann wirkt sich dieser Unterschied aus?  Unter anderem bei Typprüfungen.  Der statische und der dynamische Typ werden für unterschiedliche Typprüfungen berücksichtigt:
 

  • Den statischen Typ einer Variablen berücksichtigt der Compiler bei Typprüfungen bezüglich Typgleichheit und Typkompatibilität.  Das ist zum Beispiel der Fall bei Zuweisungen, wenn Argumente an Methoden übergeben werden oder wenn der Compiler aus einer Menge von überladenen Methoden die passende (den sogenannten „best match“) heraussucht.
  • Der dynamische Typ wird bei Typprüfungen verwendet, die zur Laufzeit durchgeführt werden. Das ist bei den meisten Casts der Fall, aber auch bei der Auswertung von instanceof-Ausdrücken.  Außerdem basieren alle Mechanismen, die über Reflection zur Verfügung stehen, auf den dynamischen Typen. Auch der Typ, den getClass() liefert, ist immer der dynamische Typ.
Zur Illustration sehen wir uns ein paar Beispiele an, in denen Zuweisungen und Casts eine Rolle spielen.

Was passiert, wenn wir einer String-Referenz eine Object-Referenz zuweisen?

stringRefToString = objectRefToString;  // compile-time failure

Der Compiler muss hier prüfen, ob die beiden Variablen zuweisungsverträglich sind.  Dazu verwendet er die statischen Typen.  Zuweisungsverträglich wären sie, wenn der statische Typ der linken Seite ein Supertyp der rechten Seite wäre.  In diesem Beispiel ist das nicht der Fall; String ist kein Supertyp von Object.  Also meldet der Compiler einen Fehler.

Probieren wir es noch einmal, diesmal mit einem Cast.

stringRefToString = (String)objectRefToString;  // fine

Der Cast hat zur Folge, dass sich der statische Typ der rechten Seite ändert. Nach dem Cast ist die rechte Seite der Zuweisung vom statischen Typ String, genau wie die linke Seite, und damit akzeptiert der Compiler nun die Zuweisung.

Der Compiler lässt selbstverständlich nicht jeden beliebigen Cast zu.  Casts, die unmöglich sinnvoll sein können, weist er als Fehler zurück. Ein Beispiel eines solchen unsinnigen Casts wäre der Cast von String nach Integer; die Konvertierung von String nach Integer ist in Java nicht möglich und wird deshalb vom Compiler zu Recht abgewiesen.  Casts, die zur Laufzeit durchaus sinnvoll sein könnten, akzeptiert der Compiler hingegen.  In unserem obigen Beispiel könnte es ja durchaus sein, dass die Object-Referenz zur Laufzeit tatsächlich auf ein String-Objekt verweist, und deshalb wird der Cast vom Compiler zugelassen.  Man sieht hier, dass Casts sowohl einen statischen als auch einen dynamischen Anteil haben. Der statische Anteil ist der, der die "unsinnigen" Casts aussortiert; der dynamische Anteil ist der, der zur Laufzeit unter Umständen eine ClassCastException auslöst.

Das passiert beispielsweise im folgenden Fall:

String stringRefToString;
Object objectRefToObject = new Object();
stringRefToString = (String)objectRefToObject; // runtime failure

Der Cast wird vom Compiler zugelassen und führt dazu, dass auch die Zuweisung akzeptiert wird.  Zur Laufzeit führt die virtuelle Maschine dann den dynamischen Teil des Casts durch und prüft, ob die Referenz auf der rechten Seite tatsächlich auf ein String-Objekt verweist, wie es im Cast verlangt wird.  In diesem Beispiel ist das nicht der Fall; deshalb wird eine ClassCastException ausgelöst.

Das ist das traditionelle Verhalten von Typprüfungen in nicht-generischem Java.  Was ist nun anders im Zusammenhang mit generischen Typen?
 

1.2 Typprüfungen in Generischem Java

Betrachten wir einige Beispiel, in denen parametrisierte Typen vorkommen:
 
Statischer Typ Dynamischer Typ
LinkedList<Integer> refToIntegerList = new LinkedList<Integer>();  LinkedList<Integer> LinkedList
LinkedList<String> refToStringList = new LinkedList<String>();  LinkedList<String>  LinkedList
Object objectRefToStringList = new LinkedList<String>(); Object LinkedList

Anders als bei nicht-parametrisierten Typen sind bei parametrisierten Typen der statische und der dynamische Typ immer verschieden.  Das liegt daran, dass der statische Typ der exakte Typ inklusive Typparameter ist; der dynamische Typ ist aber immer der Typ, der nach der Type Erasure übrig bleibt, nämlich der Typ ohne Typparameter (der sogenannte "Raw Type").  Ansonsten beobachtet man den üblichen Unterschied zwischen statischen und dynamischem Typ, nämlich wenn Supertyp-Referenzen auf Subtyp-Objekte verweisen.

Schauen wir uns nun an, wie sich die parametrisierten Typen im Zusammenhang mit Zuweisungen und Casts verhalten.  Als erstes weisen wir eine Referenz auf eine Integer-Liste einer Referenz auf eine String-Liste zu:

refToStringList = refToIntegerList;  // compile-time failure

Das ist offensichtlicher Unfug und, in der Tat, der Compiler weist es mit einer Fehlermeldung zurück. Der Compiler verwendet hier für die Prüfung der Zuweisungsverträglichkeit die statischen Typen, und die sind verschieden, nämlich LinkedList<String> und LinkedList<Integer>. Selbst ein Cast würde hier nichts nützen, weil der Compiler weiß, dass eine Konvertierung von LinkedList<Integer> und LinkedList<String> nicht möglich ist.

Betrachten wir also ein weiteres Beispiel:

refToStringList = objectRefToStringList;  // compile-time failure

Auch hier weigert sich der Compiler, die Zuweisung zu akzeptieren, weil die statischen Typen LinkedList<String> und Object verschieden und nicht zuweisungsverträglich sind. Hier können wir mit einem Cast versuchen, den Compiler zu überreden, die Zuweisung zu akzeptieren:

refToStringList = (LinkedList<String>)objectRefToStringList;  // fine

Das ist erfolgreich: eine Object-Referenz kann potentiell auf ein Objekt vom Type LinkedList<String> verweisen.  Nach dem Cast sind linke und rechte Seite der Zuweisung vom gleichen statischen Typ und der Compiler lässt die Zuweisung zu. Nun würde man erwarten, dass ein solcher Cast zur Laufzeit geprüft wird und mit einer ClassCastException  scheitert, falls die Referenz auf der rechten Seite der Zuweisung nicht auf den im Cast spezifizierten Typ verweist.  Diese Erwartung wird aber leider enttäuscht.  Hier ist ein Beispiel:

LinkedList<Integer> refToIntegerList ;
Object objectRefToStringList = new LinkedList<String>();
RefToIntegerList = (LinkedList<Integer>)objectRefToStringList;  // should fail,
                                                                // but is fine

Wir haben hier das Beispiel einer Object-Referenz, die auf eine String-Liste verweist und nach Integer-Liste gecastet wird.  Das sollte zur Laufzeit eigentlich scheitern ...  man beobachtet aber, das der Code zur Laufzeit klaglos, ohne eine ClassCastException  auszulösen, ausgeführt wird. Warum scheitert der Cast zur Laufzeit nicht?

Der Grund liegt in der Implementierungstechnik, die die Designer der Java Generics gewählt haben, nämlich Übersetzung per Type Erasure.  Nach der Type Erasure ist zur Laufzeit kein Unterschied mehr zwischen einer String-Liste und einer Integer-Liste zu erkennen.  Beide haben denselben Laufzeittyp, nämlich LinkedList in unserem Beispiel.  Deshalb scheitert der Cast zur Laufzeit natürlich nicht.  Zwar sieht der Cast zum Zieltyp LinkedList<Integer> im Sourcecode so aus, als würde dort nach LinkedList<Integer> gecastet.  Das stimmt aber nur für den statischen Teil des Casts.  Der dynamische Teil des Cast ist ein Cast nach LinkedList.  Im Gegensatz zum Compiler kann die virtuelle Maschine keinen Typunterschied mehr erkennen zwischen einer LinkedList<Integer>  und einer LinkedList<String>.

Alles, was zur Laufzeit passiert, geschieht auf Basis der dynamischen Typen, und die sind nach der Type Erasure nicht mehr exakt und damit nicht mehr so aussagekräftig wie im Sourcecode zur Compilezeit.  Das heißt, Java Generics Sourcecode darf man nicht "wörtlich" nehmen. Man muss sich stets vor Augen halten, dass die Typparameter nur im Sourcecode vorkommen und nur für die Übersetzung relevant sind.  Zur Laufzeit sind sie komplett verschwunden und spielen keine Rolle mehr. Der Java-Entwickler muß sich außerdem im Klaren darüber sein, welcher Teil seines Sourcecodes für den Compiler bestimmt ist und welcher Teil von der virtuellen Maschine verarbeitet wird.  Bei manchen Sprachmitteln, wie zum Beispiel beim Cast und beim instanceof-Operator, sind beide Aspekte vermischt, was das Verständnis nicht gerade erleichtert.

Weil im Zusammenhang mit generischen Typen einerseits und Casts und instanceof-Ausdrücken andererseits durchaus ein Fehlerpotential vorhanden ist, sind instanceof-Ausdrücke mit einem parametrisierten Zieltyp verboten und werden mit einer Fehlermeldung vom Compiler zurückgewiesen. Casts, deren Zieltyp ein parametrisierter Typ ist, läßt der Compiler zwar zu, aber mit einer sogenannten "unchecked" Warnung.  Wir sehen uns später in diesem Artikel noch genauer an, wie es sich auswirkt, wenn man eine "unchecked"-Warnung ignoriert.

Neben den interessanten Warnungen, die der Compiler zu einem Cast mit einem parametrisierten Zieltyp meldet, und dem Verbot der instanceof-Ausdrücke mit parametrisiertem Zieltyp gibt es noch eine Reihe zusätzlicher Benutzungseinschränkungen für die parametrisierten Typen.  Diese Einschränkungen wollen wir uns im Folgenden ansehen.
 

2 Einschränkungen für Parametrisierte Typen

Parametrisierte Typen kann man nicht ganz so uneingeschränkt verwenden wie reguläre Typen.  Neben dem Verbot von instanceof-Ausdrücken mit parametrisiertem Zieltyp gibt es 3 weitere Einschränkungen:
 
  • Class-Literal. Man kann von einem parametrisierten Typ kein Class-Literal bilden.  Ein Ausdruck wie LinkedList<String>.class ist unzulässig und wird vom Compiler mit einer Fehlermeldung abgewiesen. Analog wird man beobachten, dass der Aufruf Class.forName(“LinkedList<String>“) immer mit einer ClassNotFoundException scheitert. Dieses Verhalten ist eigentlich nicht überraschend, wenn man bedenkt, dass es in Java Generics keine exakte Laufzeitrepräsentation eines parametrisierten Typen gibt.  Der Laufzeittyp ist immer nur der unparametrisierte Typ LinkedList. Deshalb sind LinkedList.class und Class.forName(“LinkedList“) in Ordnung, wohingegen LinkedList<String>.class und Class.forName(“LinkedList<String>“) unzulässig sind.
  • Exceptions. Es ist nicht erlaubt, einen parametrisierten Typen direkt oder indirekt von Throwable abzuleiten. Mit anderen Worten, parametrisierte Exception-Typen sind nicht erlaubt.  Das erklärt sich dadurch, dass der Exception-Handling-Mechanismus ein Laufzeit-Mechanismus ist und das Laufzeitsystem, d.h. die JVM, weiß nichts von Java Generics. Deshalb machen parametrisierte Exception-Typen keinen Sinn.
  • Arrays. Ein parametrisierter Typ ist als Elementtyp eines Arrays nicht zugelassen.  Das heißt, es darf zum Beispiel kein Array des Typs Comparable<String>[] deklariert oder erzeugt werden. Die einzige Ausnahme ist die Wildcard-Instanziierung des parametrisierten Types: eine Deklaration wie Comparable<?>[] ist erlaubt. Das Verbot parametrisierter Typen als Elementtyp eines Arrays ist zunächst einmal eine recht überraschende Einschränkung, die sich der Praxis dann auch als reichlich lästig erweist.  Sehen wir uns die Hintergründe genauer an.  Die Ursache der Einschränkung liegt wieder in der Type Erasure.

2.1 Keine parametrisierten Typen als Array-Elemente

Arrays mit einem Elementtyp, der ein parametrisierter Typ ist, sind nicht typsicher.  Dabei bedeutet "Typsicherheit" (engl. type-safety)  folgende Garantie:  wenn sich ein Programm fehler- und warnungsfrei übersetzen lässt, dann ist es ausgeschlossen, dass zur Laufzeit eine unerwartete ClassCastException ausgelöst wird.  Eine "unerwartete" ClassCastException wäre eine, die ohne einen entsprechenden Cast im Sourcecode entsteht.  Bei Verwendung von parametrisierten Typen als Elementtyp eines Arrays kann keine Typsicherheit gewährleistet werden, da der Compiler außerstande ist, alle Typverletzungen im Zusammenhang mit Arrays mit parametrisiertem Elementtyp zu entdecken.  Deshalb sind solche Arrays unzulässig.

Woran liegt es, dass der Compiler nicht alle Typverletzungen im Zusammenhang mit Arrays von parametrisiertem Typ erkennen kann?  Das hängt mit dem sogenannten "Array-Store-Check" zusammen.  In Java beinhaltet die Typinformation eines Arrays Information über den Elementtyp des Arrays.  Diese Information wird benutzt, wenn zur Laufzeit ein Element an eine Position im Array zugewiesen wird.  Bei dieser Zuweisung führt die virtuelle Maschine den Array-Store-Check aus: sie prüft, ob das zuzuweisende Element vom erwarteten Elementtyp ist.  Ziel dieser Prüfung ist es, die Homogenität des Arrays sicherzustellen, also dass z.B. ein String-Array nur Strings enthält. Der Versuch, einen Integer in einem String-Array einzutragen, würde mit einer ArrayStoreException abgewiesen.  Hier ist ein Beispiel:

1 Object[] objArr = new String[10];
2 objArr[0] = new Integer(); // compiles;
3                            // fails at runtime with ArrayStoreException

Nehmen wir nun einmal an, parametrisierte Typen wären erlaubt als Elementtyp eines Arrays.  Dann funktioniert der Array-Store-Check nicht mehr.  Betrachten wir als Beispiel einen parametrisierten Typ Pair<X,Y> (siehe Listing 1).

Listing 1: Auszug aus einer Implementierung eines Pair Typs

public final class Pair<X,Y> {
 private X fst;
 private Y snd;
 public Pair(X fst, Y snd) {this.fst=fst; this.snd=snd;}
 public X getFirst() { return fst; }
 public Y getSecond() { return snd; }
 ...
}

Wenn wir ein Array von Integer-Paaren erzeugen dürften, ganz analog zu unserem Array von Strings, dann könnten wir versuchen, in das Array ein Element von einem anderen Typ einzutragen.  Normalerweise sollte das vom Compiler durch statische Typprüfungen oder spätestens von der virtuellen Maschine durch den Array-Store-Check verhindert werden.  Im nachfolgenden Beispiel versuchen wir, ein String-Paar in ein Array von Integer-Paaren einzutragen.

1 Object[] arr = new Pair<Integer,Integer>[10];  // compile-time error
2 arr[0] = new Pair<String,String>("","");

Das Beispiel läßt sich nicht übersetzen, weil der Compiler die Verwendung von Pair<Integer,Integer>[10] mit der Fehlermeldung "arrays of generic types are not allowed" abweist.  Aber wenn der Compiler es zuließe, dann wären weder der Compiler noch die virtuelle Maschine in der Lage zu verhindern, dass das String-Paar in  dem Array von Integer-Paaren abgelegt wird.

Der Compiler kann es nicht verhindern, weil wir auf das Array von Integer-Paaren über eine Variable objArr vom Typ Object-Array zugreifen. In Java sind Arrays kovariant, d.h. es ist erlaubt, dass eine Variable vom Typ Supertyp-Array auf ein Subtyp-Array verweist. Eine solche Situation haben wir in unserem Beispiel (in Zeile 1) hergestellt: die Object-Array-Variable verweist auf ein Array von  Integer-Paaren.  In der Zuweisung des Array-Elements (in Zeile 2) ist die linke Seite eine Position im Array.  Da wir über eine Variable vom Typ ein Object-Array zugreifen, ist die linke Seite der Zuweisung vom statischen Typ Object. Die rechte Seite ist vom statischen Typ Pair<String,String>.  Damit liegt Zuweisungsverträglichkeit vor und deshalb lässt der Compiler die Zuweisung des String-Paars an eine Position im Array zunächst einmal zu.  Nun ist das Object-Array in Wirklichkeit ein Array von Integer-Paaren  und deshalb wird die virtuelle Maschine zur Laufzeit später den Array-Store-Check ausführen, um die Zuweisung des fremden Elements vom Typ String-Paar zu verhindern.

Der Array-Store-Check funktioniert aber nicht bei parametrisierten Elementtypen, weil er auf den dynamischen Typen der beteiligten Variablen basiert. In unserem Beispiel sind beide Seiten der Zuweisung (in Zeile 2) vom dynamischen Typ Pair.  Wegen der Type Erasure ist aus dem Array von Integer-Paaren ein Array von einfachen Paaren geworden und das String-Paar auf der rechten Seite der Zuweisung ist ebenfalls zu einem einfachen Paar mutiert.  Die virtuelle Maschine hat daher zur Laufzeit gar keine Chance mehr, im Array-Store-Check die Typabweichung zwischen einem Pair<Integer,Integer> und einem Pair<String,String> zu erkennen.  Das Ablegen des String-Paares in dem Array von Integer-Paaren wäre also ohne Fehler oder Warnung möglich, wenn Arrays mit parametrisiertem Elementtyp erlaubt wären. Trotz fehler- und warnungsfreier Übersetzung würde es später bei Herausholen von Elementen aus dem Array eine unerwartete ClassCastException geben, weil sich im Integer-Array unerwartet ein String befinden würde.  Ein solches Verhalten entspräche nicht dem Ziel, Java als typsichere Sprache zu erhalten.  Deshalb haben sich die Designer der Java Generics dazu entschlossen, Arrays mit Elementen von einem parametrisierten Typ grundsätzlich zu verbieten.

Für die praktische Arbeit ist das Verbot von Arrays mit Elementen von einem parametrisierten Typ eine heftige Einschränkung.  Wenn man ernsthaft mit generischen Typen programmiert, dann ist es völlig natürlich, dass man auch Arrays mit parametrisiertem Elementtyp anlegen will. Das geht aber nicht. Was bedeutet das in der Praxis? Der Entwickler hat zwei Alternativen:

  • Entweder er verzichtet auf die Verwendung von Arrays und verwendet statt dessen Collections; also statt eines Pair<String,String>[] wird eine Collection<Pair<String,String>> verwendet.
  • Oder er deklariert als Elementtyp des Arrays keine konkrete Instanziierung eines parametrisierten Typs, sondern verwendet als Elementtyp eine Wildcard-Instanziierung ; also statt  Pair<String,String>[] wird ein Pair<?,?>[] verwendet.
Wildcard-Instanziierungen sind - im Gegensatz zu den konkreten Instanziierungen - als Elementtyp von Arrays  erlaubt.  Dabei muß das Wildcard das sogenannte "Unbounded Wildcard" sein (syntaktisch bezeichnet durch ein Fragezeichen).

Eine Wildcard-Instanziierungen wie Pair<?,?> ist als Elementtyp zulässig, weil die Type Erasure sich auf diese Art der Wildcard-Instanziierungen nicht auswirkt.  Ein "Unbounded Wildcard" macht keinerlei Aussagen über einen Typparameter. Der Typ Pair<?,?> zum Beispiel enthält keine Information über die Typargumente der Instanziierung und ist damit genauso unexakt wie der "Raw Type" Pair, der nach der Übersetzung per Type Erasure übrig bleibt. Ein Array mit Elementtyp Pair<?,?>  ist semantisch gesehen ein Array von Paaren beliebigen Inhalts.  Von einem Array-Store-Check wird man daher auch nur erwarten, dass er sicherstellt, dass ausschließlich Paare (beliebigen Inhalts) im Array abgelegt werden.  Und das genau leistet ein Array-Store-Check auf Basis der nicht-exakten dynamischen Typinformation. Bei Verwendung von Wildcard-Instanziierungen wie Pair<?,?> als Elementtyp eines Arrays ergeben sich daher keine Überraschungen, und deshalb sind sie – im Gegensatz zu den Arrays mit konkret instanziiertem parametrisiertem Typ - zugelassen.
 

Sehen wir uns die beiden Alternativen zum Array im Beispiel an:

Bei Verwendung einer Collection anstelle eines Arrays sähe das Beispiel so aus:

1 ArrayList<Pair<Integer,Integer>> arr
2    = new ArrayList<Pair<Integer,Integer>>(10);
3 arr.set(0,new Pair<String,String>("",""));  // compile-time error message

Der Versuch, ein Paar vom falschen Typ in der Collection abzulegen, wird bereits vom Compiler mit einer Fehlermeldung abgefangen. Eine Prüfung à la Array-Store-Check zur Laufzeit ist gar nicht nötig.  Die Prüfung erfolgt schon zur Compile-Zeit auf Basis der Signatur der set()-Methode. Die set()-Methode einer ArrayList<Pair<Integer,Integer>> akzeptiert nämlich nur Argumente vom Typ Pair<Integer,Integer>.  Das Argument vom Typ Pair<String,String> ist daher vom falschen Typ und wird vom Compiler abgelehnt.
 

Bei Verwendung einer Wildcard-Instanziierung anstelle einer konkreten Instanziierung als Elementtype des Arrays sähe das Beispiel so aus:

1 Object[] arr = new Pair<?,?>[10];   // supposed to be Pair<Integer,Integer>[]
2 arr[0] = new Pair<String,String>("","");

Jetzt ist das Array ein Pair<?,?>-Array und es ist aus dem Sourcecode bereits klar ersichtlich, dass im Prinzip jeder beliebige Typ von Paar in diesem Array enthalten sein kann. Das Ablegen des String-Paares ist daher zulässig.

Die beiden Alternativen zu Arrays mit parametrisiertem Elementtyp – Collections oder Wildcard-Arrays – sind beide semantisch verschieden von einem  Array mit parametrisiertem Elementtyp.  Die Collection bringt den üblichen Overhead einer Collection mit sich und ist natürlich nicht so effizient wie ein Array.  Das Wildcard-Array ist zwar ein Array und entsprechend effizient, aber es ist keine Sequenz von Elementen desselben Typs, so wie es die Collection ist oder es ein Array mit parametrisiertem Elementtyp wäre. Ein Pair<?,?>-Array ist ein gemischtes Array von Paaren beliebigen Typs.  Möglicherweise ist es nicht das, was wir haben wollten, aber wir haben in Java Generics nun mal keine Möglichkeit auszudrücken, dass wir mit einem Array von Paaren eines bestimmten Typs arbeiten wollen.
 

2.2 Die Folgen ignorierter Warnungen

Sehen wir uns an, was passiert, wenn man die "unchecked"-Warnungen, die der Compiler bisweilen ausgibt, ignoriert.  Für die Diskussion nehmen wir unser Array-Beispiel von oben her. Wir verwenden ein Pair<?,?>- Array und füllen Integer-Paare hinein.  Wenn wir die Integer-Paare aus dem Array herausholen, sind sie vom statischen Type her keine Integer-Paare, sondern nur Paare unbestimmten Inhalts vom Typ Pair<?,?>.  Das führt zu einer Fehlermeldung im folgenden Programmabschnitt:

1 Pair<?,?>[] arr = new Pair<?,?>[10];
2
3 for (int i=0; i<arr.length; i++)
4     arr[i] = new Pair<Integer,Integer>(i,i);
5
6 for (int i=0; i<arr.length; i++) {
7     Pair<Integer,Integer> p = arr[i];  // error !!!
8     ...
9 }

Der Compiler meldet einen Fehler in  Zeile 7, weil ein Pair<?,?> nicht einem Pair<Integer,Integer> zugewiesen werden kann.  Nehmen wir einmal an, wir wüßten aufgrund der Semantik des Programms, dass alle Paare im Array Integer-Paare sind.  Also könnten wir auf den Gedanken kommen, den Compiler mit geschickten Casts austricksen.   Folgender Cast täuscht den Compiler, so dass er die Zuweisung – allerdings mit "unchecked warning" - akzeptiert:

1 Pair<?,?>[] arr = new Pair<?,?>[10];
2
3 for (int i=0; i<arr.length; i++)
4     arr[i] = new Pair<Integer,Integer>(i,i);
5
6 for (int i=0; i<arr.length; i++) {
7     Pair<Integer,Integer> p = (Pair<Integer,Integer) arr[i];  // unchecked warning
8     ...
9 }

So gelingt es uns, Integer-Paare im Pair<?,?>-Array abzulegen und die Array-Elemente später auch so zu verwenden, als seien sie Integer-Paare.  Wir brauchen zwar einen Cast und der Compiler gibt eine Warnung aus, aber diese Warnung haben wir beschlossen zu ignorieren.

Nun könnte es aber auch sein sein, dass sich im Pair<?,?>-Array entgegen unseren Erwartungen ein String-Paar eingeschlichen hat.  Diese Situation haben wir im folgenden Beispiel hergestellt; in Zeile 6 schmuggeln wir ein String -Paar ein:

1 Pair<?,?>[] arr = new Pair<?,?>[10];
2
3 for (int i=0; i<arr.length; i++)
4     arr[i] = new Pair<Integer,Integer>(i,i);
5
6 arr[0] = new Pair<String,String>("...","...");  // add alien pair
7
8 for (int i=0; i<arr.length; i++) {
9 Pair<Integer,Integer> p = (Pair<Integer,Integer>)arr[i];  // unchecked warning
10
11 Integer first  = p.getFirst();         // ClassCastException
12 Integer second = p.getSecond();        // ClassCastException
12     ...
13 }

Das "Einschmuggeln" funktioniert problemlos, weil ein Array von Pair<?,?>-Elementen eben einfach keine Sequenz von Paaren eines bestimmten Typs ist. Selbstverständlich können wir jede Art von Paar im Array ablegen. Der Compiler kann und soll hier gar nichts melden.  Erst beim Herausholen der Elemente kommt wegen des unvermeidlichen Casts die "unchecked"-Warnung, diesmal zu Zeile 9. Was passiert, wenn wir die Warnung ignorieren? Wie wirkt es sich aus, wenn eine Referenzvariable vom statischen Typ Pair<Integer,Integer> auf ein Objekt vom dynamischen Typ Pair<String,String> verweist?

Das Integer-Paar p, das eigentlich ein String-Paar ist, wird im Programm weitergereicht und verwendet.  Das funktioniert auch - solange, bis auf das Paar zugegriffen wird, in der Erwartung, es enthalte Integers.  Dann erst gibt es eine ClassCastException. In unserem Beispiel passiert das in Zeile 11 bereits.  In einem realistischen Programm kann das aber an einer ganz anderen Stelle im Programm sein. Das heißt, lange nachdem der eigentliche Fehler, nämlich das Zuweisen eines String-Paares an eine Variable vom Typ Pair<Integer,Integer>, passiert ist, wirkt sich der Fehler erst aus.  Die Ursache eines solchen Fehlers dann noch zu identifizieren, ist in der Praxis meistens recht mühselig.  Es empfiehlt sich also, die "unchecked"-Warnungen nicht grundsätzlich zu ignorieren.

Nun gibt es leider einige Situationen, in denen die "unchecked"-Warnungen gar nicht verhindert werden können.
Dazu gehört die gemischte Verwendung von generischen und nicht-generischen Programmteilen.  Das dürfte in der Praxis ein recht häufiger Fall sein, weil man alten "Legacy"-Code sicher nicht komplett generifizieren wird, sondern alten und neuen Code nebeneinander und gemischt verwenden wird. Daneben sind "unchecked"-Warnungen unvermeidlich bei der Implementierung von Methoden wie clone() und equals(), die aus historischen Gründen nicht parametrisiert sind und noch immer mit Object funktionieren.  Eine detaillierte Diskussion dieser Situationen würde den Rahmen des Artikels sprengen.  Es bleibt lediglich festzuhalten, dass sich "unchecked"-Warnungen nicht vollständig vermeiden lassen. Viele dieser Warnungen sind unberechtigt und die Kunst besteht darin, die berechtigten Warnungen unter den unberechtigten zu identifizieren.
 

2.3 Checked Collections

Im Zusammenhang mit der Diskussion über Arrays mit parametrisiertem Elementtyp haben wird die Collections mit parametrisiertem Elementtyp als Alternative erwähnt.  Bei der Collection ist im Gegensatz zum Wildcard-Array gewährleistet, dass es nur Elemente desselben Typs enthält, weil der Compiler Prüfungen auf Basis der exakten statischen Typeinformation, wie z.B. List<String>, macht. Da man den Compiler aber mit Casts leicht überlisten kann, ist natürlich keineswegs gewährleistet., dass eine List<String> tatsächlich nur Strings enthält.

Wenn man wirklich sicher gehen will, dass eine List<String> tatsächlich nur Strings enthält, dann kann man einen sogenannten "checked"-Adapter verwenden.  Die Klasse Collections bietet einen Adapter, der ähnlich wie der synchronized- oder der unmodifiable-Adapter eine Sicht auf eine existierende Collection bietet.  Der Unterschied zur Original-Collection besteht darin, dass eine checked-Collection jedes Mal, wenn ein Element eingefügt wird, zur Laufzeit prüft, ob das Element vom richtigen Typ ist.

Hier wird zu den statischen Typprüfungen, die der Compiler auf Basis der Typparameter macht  - und die man mit geschickten Casts sabotieren kann - noch zusätzlich eine Typprüfung zur Laufzeit gemacht. Das ist natürlich "doppelt-gemoppelt" und dient allein dazu, die sabotierbaren statischen Typprüfungen durch zusätzliche dynamische Typprüfungen abzusichern.  Hier ist das Beispiel einer checked-Collection:

1  Collection<String> cc
2  = Collections.checkedCollection(new LinkedList<String>(), String.class);
3  cc.add(new Integer(5));  // compile-time error: wrong argument type
4
5  Collection<Integer> fake
6  =(Collection<Integer>)(Collection<?>)cc;  // warning: unchecked cast
7  fake.add(new Integer(5)); // exception: attempt to insert element of wrong type

Der plumpe Versuch, einen Integer in eine String-Liste einzufügen, scheitert natürlich an den Typprüfungen des Compilers: die Methode add() akzeptiert nur String-Argumente und das meldet der Compiler dann auch als Fehler (in Zeile 3).
Also greifen wir in die Trickkiste (in Zeile 5 und 6) und lassen die String-Collection so aussehen, als sei sie eine Integer-Collection.  Dazu verwenden wir einen sogenannten "double-fisted cast", also ein Cast über zwei Stufen, wobei die erste Stufe ein völlig generischer Typ wie Object oder ein Wildcard-Typ ist.  Das klappt immer und legt jede wohlgemeinte Typprüfung des Compilers lahm.  Der Compiler gibt eine "unchecked"-Warnung aus und läßt das Einfügen des Integer-Elements in die nun vermeintliche Integer-Collection zu (siehe Zeile 7). Zur Laufzeit schlägt dann die dynamische Prüfung der checked-Collection zu und löst eine ClassCastException aus.

In obigem Beispiel haben wir mit äußerst brutalen Mitteln den Fremdling eingeschmuggelt; so etwas macht in der Praxis natürlich nicht. Aber ähnliche Probleme können auch versehentlich hervorgerufen werden, beispielsweise wenn nicht-generische Legacy-Methoden aufgerufen werden. Hier ein Beispiel:

1  class Legacy {
2   private List elements;
3   public Legacy(List l) { elements = l; }
4   ...
5     public List getElements() { return elements; }
6   }
7
8  Collection<String> cc = new LinkedList<String>();
9  Legacy leg = new Legacy(cc);
10 ...
11 cc = leg.get();  // unchecked warning

Nun mag es zwar sein, dass wir aufgrund der Dokumentation wissen (oder glauben zu wissen), dass die Liste, die von der get()-Methode geliefert wird, nur Strings enthält, aber das kann der Compiler nicht prüfen und es ist durch nichts in der Sprache abgesichert. Der Compiler läßt die Zuweisung der Raw-Type-Liste an die String-Collection-Variable aus Kompatibilitätsgründen dennoch zu – allerdings mit einer uncheckd-Warnung. Hier könnte nun wegen eines Mißverständnisses eine Liste mit ganz anderen Elementen fälschlicherweise als eine Liste von Strings betrachtet und verwendet werden.  Wenn man sicher gehen will, dass die String-Liste auch wirklich nur Strings enthält, dann kann man eine checked-Collection verwenden, die das Einfügen von unerwünschten Elementen zur Laufzeit abfängt.

Intern sieht die checked-Collection übrigens in etwa so aus:

class CheckedCollection<E> implements Collection<E> {
  private final Collection<E> c;
  private final Class<E> type;
  ...
  public CheckedCollection(Collection<E> c, Class<E> type) {
     this.c = c;
     this.type = type;
  }
  public boolean add(E o){
    if (!type.isInstance(o)) throw new ClassCastException();
    return c.add(o);
  }
  ...
}

Den Zusatzaufwand für die Extra-Typprüfung zur Laufzeit wird man natürlich nur dann in Kauf nehmen, wenn es wirklich wichtig ist, dass die Collection homogen ist.  Zu Debugging-Zwecken etwa kann eine checked-Exception sehr nützlich sein, beispielsweise wenn man eine ClassCastException beim Herausholen eines Elements aus einer Collection bekommt und wissen will, wann und wie das störende Element in die Collection gelangt ist.  Dann kann die fragliche Collection zu Testzwecken durch eine checked-Collection ersetzen, so dass die ClassCastException bereits beim Einfügen eines Elements in die Collection ausgelöst wird.

Dem aufmerksamen Leser ist nun möglicherweise aufgefallen, dass die vermeintliche Sicherheit der checked-Collection trotz ihrer doppelten (statischen und dynamischen) Typprüfung bei Elementtypen von parametrisiertem Type dann doch ihre Grenzen findet.  Wenn der Elementtyp zum Beispiel Pair<String,String> ist, dann werden die statischen Prüfungen zwar auf den exakten Typ Pair<String,String> prüfen, aber die zusätzliche Prüfung zur Laufzeit verwendet natürlich nur den nicht-exakten Typ Pair.  Das ist dann dasselbe Problem wie beim Array-Store-Check. Allerdings treten Probleme mit Collections vom parametrisiertem Elementtyp nur in Programmen auf, die an irgendeiner Stelle eine "unchecked"-Warnung hervorgerufen haben. Bei warnungsfreier Übersetzung ist vollständige Typsicherheit gewährleistet und es treten keine überraschenden ClassCastExceptions zutage, anders als das bei der Verwendung von Arrays mit parametrisiertem Elementtyp der Fall wäre.  Deshalb sind die Arrays verboten, die Collections hingegen zulässig.

3 Zusammenfassung

In diesem Artikel haben wir erläutert, dass der Java–Compiler für parametrisierte Typen  nicht-exakte Laufzeit-Typinformation generiert.  Wir haben gesehen, dass Casts mit parametrisiertem Zieltyp fragwürdig sind und zu "unchecked"-Warnungen führen.  "Unchecked"-Warnungen sollten nicht ignoriert werden, denn sie können später zu unerwarteten ClassClassExceptions führen. Das Fehlen von exakter Typinformation zur Laufzeit bringt einige Einschränkungen mit sich, was die Benutzung von parametrisierten Typen angeht. Die wohl gravierendste dieser Einschränkungen ist das Verbot, Arrays zu verwenden, deren Elementtyp ein parametrisierter Typ ist. Diese Einschränkung ist nötig, um die Typsicherheit zu gewährleisten.

4 Weitere Informationen

 
/JDK15/  Java TM 2 SDK, Standard Edition 1.5.0
Update 1
http://java.sun.com/developer/earlyAccess/j2sdk150_alpha/
/TUT/  Java Generics Tutorial
Gilad Bracha
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf
/FAQ/ Java Generics FAQ
Angelika Langer
http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html
/LAN/  Links related to Java Generics
Further references to articles, tutorials, conversations and other information related to Java Generics can be found on this website at http://www.AngelikaLanger.com/Resources/Links/JavaGenerics.htm .
/MAG1/ Java Generics - Parametrisierte Typen und Methoden
Klaus Kreft & Angelika Langer
JavaMagazin, April 2004
http://www.AngelikaLanger.com/Articles/JavaMagazin/Generics/GenericsPart1.html
/MAG2/ Wildcard Instantiations of Parameterized Types
Klaus Kreft & Angelika Langer
JavaMagazin, Oktober 2004
http://www.AngelikaLanger.com/Articles/JavaMagazin/Generics/GenericsPart2.html /

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Java 5.0 - New Features in J2SE 5.0
1 day seminar (open enrollment and on-site)
Effective Java - Advanced Java Programming Idioms 
5 day seminar (open enrollment and on-site)
 
 
  © Copyright 1995-2007 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/JavaMagazin/Generics/GenericsPart2.html  last update: 10 Aug 2007