|
|||||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||||
|
Java Generics - Type Erasure
|
||||||||||||||||||||||||||||||||||||||||||
Java Generics - Type Erasure
JavaMagazin, Oktober 2004
Umwälzungen im Java-TypsystemGenerische Typen, Type Erasure und wie sie sich das Ganze auf das Java-Typsystem auswirktJava 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-TypinformationReferenzvariablen 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 JavaUm 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.
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:
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;
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 JavaBetrachten wir einige Beispiel, in denen parametrisierte Typen vorkommen:
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 ;
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 TypenParametrisierte 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:
2.1 Keine parametrisierten Typen als Array-ElementeArrays 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];
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> {
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
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:
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
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>[]
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 WarnungenSehen 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];
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];
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];
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.
2.3 Checked CollectionsIm 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
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).
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 {
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> {
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 ZusammenfassungIn 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
|
|||||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2007 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/JavaMagazin/Generics/GenericsPart2.html> last update: 10 Aug 2007 |