|
||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | ||||||||||||||||||||||||||||||||
|
Java Generics - Wildcards
|
|||||||||||||||||||||||||||||||
In der letzten Ausgabe dieser Kolumne haben wir das in Java 5.0 neue
Sprachmittel der generischen Typen (Java Generics) in einem Überblick
vorgestellt (siehe /KREFT/). Heute wollen wir uns ein spezifisches
Element der generischen Typen, die Wildcards, genauer ansehen.
Was sind Wildcards?Fangen wir mit einer kurzen Wiederholung unserer letzten Kolumne an. Ein generischer Typ wie List<E> hat einen Typparameter E, den man bei der Parametrisierung durch einen konkreten 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 Objekte vom Typ String
in die Liste 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 immer ein Objekt von
Typ String zurück.
List<String> myList = new ArrayList<String>();Nun kann man einen generischen Typ aber nicht nur mit einem konkreten Referenztyp parametrisieren, sondern auch mit einem Wildcard. Wildcards sind zusammen mit den generischen Typen neu im JDK 5.0. Das einfachste Wildcard ist das unbeschränkte Wildcard, dargestellt durch das Fragezeichen "?". Es steht für "alle Referenztypen"; man kann es auch als: "irgendein beliebiger Referenztyp" interpretieren. Eine Variable von einem wildcard-parametrisierten Typ, wie z.B. List<?> newList = null;kann die Referenz auf eine Liste enthalten, die mit irgendeinem Referenztyp parametrisiert wurde. Hier sind einige Beispiele: newList = new ArrayList<String>();Wie und warum man einen solchen wildcard-parametrisierten Typ benutzt, werden wir weiter unter diskutieren. Hier geht es erst einmal darum zu verstehen, was wildcard-parametrisierte Typen überhaupt sind. Sie sind typartige Konstrukte, die aus generischen Typen entstehen, wenn man statt eines konkreten Typs ein Wildcard als Typargument einsetzt. Zu den Eigenschaften von wildcard-parametrisierten Typen gehört, dass es keine Objekte von einem solchen Typ gibt. Die folgende Anweisung ist unzulässig: newList = new ArrayList<?>(); // falschDie Anweisung läßt sich nicht kompilieren, weil wildcard-parametrisierte Typen zwar den Typ von Variablen oder Methodenparametern definieren können, aber nicht für die Erzeugung von Objekten verwendet werden dürfen. Wildcard-parametrisierte Typen sind, was diese Art der Benutzung angeht, Interfaces (im Gegensatz zu Klassen) sehr ähnlich: es gibt Interface-Variablen, aber keine Interface-Objekte. Neben dem unbeschränkten Wildcard gibt es noch zwei weitere Wildcards. Das eine ist das nach oben beschränkten Wildcard: ? extends supertypeEr steht für jeden beliebigen Typ, der von supertype abgeleitet ist. Zu dieser Menge der Subtypen gehört auch supertype selbst. Schauen wir uns dazu ein Beispiel an: "? extends Runnable" bezeichnet
List<? extends Runnable> myList = null;auf myList = new LinkedList<Runnable>();verweisen. Das dritte und letzte Wildcard ist das nach unten beschränkte: ? super subtypeEs bezeichnet einen Supertype von subtype. Bei "? super Thread" sind das die Typen: Thread, Runnable und Object. Bisher wirken die drei Wildcards recht abstrakt und es ist ziemlich unklar, wie man sie in der Praxis einsetzten kann. Aber jeder der Typen liefert in bestimmten Situationen Lösungen bei der Benutzung generischer Typen in Java. Im folgenden wollen wir diese Situationen diskutieren. Der unbeschränkte Wildcard TypFangen wir mit dem Beispiel einer Methode an, die die Elemente einer Collection ausdruckt:void oldPrintCollection(Collection c) {Wie man sieht, stammt die Methode noch aus der Zeit, als es in Java keine generischen Typen gab: die Collection ist nicht parametrisiert. Nun schauen wir einmal, wie eine Version der Methode aussehen könnte, die die Features des JDK 5.0 nutzt. Hier ist unser erster Ansatz: void newPrintCollection(Collection<Object> c) {Die Nutzung der neuen for-Schleife reduziert den Code schon ganz erheblich, da das explizite Hantieren mit dem Iterator entfällt. Aber ansonsten ist die Lösung eher eine Enttäuschung, denn der folgende Code läßt sich nicht übersetzten: List<String> myStrings = new ArrayList<String>();Eine List<String> kann nicht an die Methode übergeben werden. Nur Collections, die mit Object parametrisiert sind, können an die Methode übergeben werden: List<Object> myObjects = new ArrayList<Object>();Aber das ist nicht, was wir wollen. Mit der Nutzung von List<Object> geben wir die Typsicherheit auf, wegen der wir die generischen Typen überhaupt benutzen: In einer List<Object> können wieder Objekte von beliebigen Typen abgelegt werden und nicht nur Strings.
Das eigentliche Problem mit unserer newPrintCollection() Methode ist,
dass wir gehofft hatten, durch die Definition des Methodenparameters als
Collection<Object> würde auch Collection<String> akzeptiert.
Das ist aber nicht so. Beide Typen stehen in keinerlei Beziehung zueinander.
Sie sind zwar durch Parameterisierung aus demselben generischen Typ entstanden,
aber sie sind in keiner Weise kompatibel zueinander.
Wenn man ehrlich ist, ist der Wildcard-Typ nicht die einzig mögliche Lösung für unser Problem. Die andere Lösung sieht so aus: <T> void newPrintCollection(Collection<T> c) {Hierbei handelt es sich um eine generische Methode, bei der der Elementtyp der Collection der Typparameter ist. In der Benutzung sind beide Lösungen völlig gleichwertig, der benutzende Code ist sogar exakt der gleiche: List<String> myStrings = new ArrayList<String>();Wo ist eigentlich der Unterschied in den beiden Lösungen? Immerhin sieht Collection<?> ziemlich genauso aus wie Collection<T>. Gibt es überhaupt einen Unterschied? Der Unterschied liegt darin, dass die Methode mit Collection<?> eine normale, nicht-generische Methode ist, wohingegen die Methode mit Collection<T> eine generische Methode ist. Bei der generischen Methode muss der Compiler erst einmal deduzieren, welcher konkrete Typ für den Typparameter T eingesetzt werden muss. Das macht er beim Aufruf der Methode. Zum Beispiel folgert der Compiler beim Aufruf von newPrintCollection(myStrings), dass T durch String ersetzt werden muss. Eine solche Deduktion ist bei der Methode mit Collection<?> nicht nötig, weil die Methode nicht generisch ist. Es gibt noch einen kleinen Unterschied zwischen den beiden Ansätzen (nicht-generische Methode mit Wildcard-Typ vs. generische Methode ohne Wildcards), der aber erst bei komplexeren Konstellationen zum Tragen kommt. Vergleichen wir dazu folgende zwei Methoden: void print(Collection<Tuple<?>> tpls) {Wieder ist eine Methode generisch und die andere verwendet einen Wildcard-Typen. Hier gibt es, anders als in unserem vorhergehenden Beispiel, einen Unterschied bei der Benutzung der Methoden. Die erste Methode mit der Wildcard-Parametrisierung kann man auch auf heterogene Collections anwenden, in denen Tuple von verschiedenem Typ enthalten sind. Die generische Methode kann man nur auf homogene Collections anwenden, denn beim konkreten Aufruf muss der Aufrufparametertyp so etwas wie Collection<Tuple<String>> oder Collection<Tuple<Integer>> sein, sonst kann der Compiler das Typargument T nicht deduzieren. Wir haben oben schon erwähnt, dass es für die Verwendung von wildcard-parametrisierten Typen gewisse Einschränkungen gibt. Zum Beispiel kann man keine Objekte von wildcards-parametrisierten Typen erzeugen. Es gibt aber noch weitere Einschränkungen, die aber für die verschiedenen Arten von Wildcards jeweils anders sind. Für das unbeschränkte Wildcard gilt die Einschränkung, dass man keine Methode aufrufen darf, die den Typparameter als Methodenparameter haben. Das folgende ist also falsch und läßt sich nicht kompilieren: List<?> l = new LinkedList<String>();Wie ist das zu begründen? Das Beispiel sieht doch aus, als sei es in Ordnung. Schauen wir uns noch einen anderen Fall an, der das Problem deutlicher auf den Punkt bringt: void foo(List<?> l) {Hier sieht der Compiler, dass es sich bei l um eine Liste handelt, die Elemente eines beliebigen, unbekannten Typs enthalten kann. Wegen der Wildcard-Parametrisierung kann der Compiler nun nicht sicherstellen, dass die Liste typsicher bleibt, d.h. dass sie nur Elemente des gleichen Typs enthält. Er kann nicht entscheiden, ob es richtig ist, dass ein Objekt von Typ String in die Liste eingefügt wird, weil er nicht weiß, ob es sich um eine List<String> handelt oder nicht. Der Compiler kann beim Einfügen in eine List<?> überhaupt nichts sicherstellen, weil er gar keine Information über den Elementtyp der Liste hat. Deshalb darf die add()-Methode auf einer List<?> nicht aufgerufen werden. Diese Regel betrifft alle Methoden, die den Typparameter der Liste als Argumenttyp haben. Eine Ausnahme von der Regel gibt es natürlich doch noch. Das Folgende funktioniert: void foo(List<?> l) {Das liegt daran, dass null von einem eigenen speziellen Referenztyp ist, der kompatibel zu allen Referenztypen ist. Wie ist das nun, wenn der Typparameter nicht der Argumenttyp, sondern der Returntyp einer Methode ist? Gibt es da auch Einschränkungen? Ja, aber nur unwesentliche. Der Aufruf solcher Methoden ist erlaubt. Aber da der Elementtyp der Liste nicht bekannt ist, kann der Compiler nur sicherstellen, dass das Objekt, das von der Methode zurückgeliefert wird, vom Typ Object ist. Spezifischer Typinformation über den Returnwert ist nicht vorhanden, d.h. man ist in einer Situation wie vor der Benutzung generischer Typen und muss einen expliziten Cast einfügen, um den Typ wiederherzustellen: List<String> stringList = new LinkedList<String>(); Der nach oben beschränkte Wildcard TypHäufiger als unbeschränkte Wildcards werden in der Praxis die nach oben beschränkten Wildcards gebraucht. Fangen wir wieder mit dem Beispiel einer Methode an, die vor Erscheinen des JDK 5.0 implementiert wurde:void oldDrawAll(Collection c) {Nehmen wir an, dass wir eine Hierarchie von Grafikelementen haben mit dem Superinterface Shape haben. Shape enthält unter anderem eine Instanzmethode draw(), mit der sich das Grafikelement anzeigt lässt. Die Methode oldDrawAll() aus unserem Beispiel oben zeigt nun alle Grafikelemente an, die ihr in der Collection als Parameter übergeben werden. Da der Code vor Erscheinen des JDK 5.0 implementiert worden ist, kann die Methode nichts anderes machen, als blind den Downcast durchzuführen und zu hoffen, dass die aufrufende Seite nur Shapes in der Collection abgelegt hat. In der Realität würde man die Fehlerbehandlung natürlich etwas eleganter lösen, damit ein Objekt vom falschen Typ nicht gleich die gesamte Schleife abbricht, aber zusätzliche Fehlerbehandlungen lenken hier nur vom eigentlichen Beispiel ab. Jetzt geht es wieder darum, den Code von oldDrawAll() so zu ändern, dass die Features des JDK 5.0, speziell die Typsicherheit der generischen Typen, zum Einsatz kommen. Der erste Versuch ist: void newDrawAll(Collection<Shape> shapes) {Aus unserer Diskussion des unbeschränkten Wildcards wissen wir schon, dass wir damit zwar eine List<Shape> anzeigen können, aber keine List<Rectangle> oder List<Circle> (wobei Rectangle und Circle von Shape abgeleitet sind). Im vorhergehenden Beispiel haben wir als Lösung ein unbeschränktes Wildcard benutzt. Versuchen wir das doch hier noch einmal: void newDrawAll(Collection<?> shapes) {Das funktioniert hier leider nicht, weil sich Zeile 2 nicht übersetzen lässt: über den Typ eines Elements in einer Collection<?> ist nichts bekannt. Ein solches Element kann deshalb nur Object zugewiesen werden, nicht aber Shape. Jetzt kann man zwar in Anlehnung an den Originalcode von oldDrawAll() auf die Idee für folgende Implementierung zu kommen: void newDrawAll(Collection<?> shapes) {Das lässt sich zwar übersetzten und kann auch mit List<Rectangle> aufgerufen werden, trotzdem ist es nicht das, was wir wollen. Die Implementierung ist nicht typsicherer als das Original, obwohl sie sich generischer Typen bedient. Ganz offensichtlich kommen wir mit allen bisher bekannten Ansätzen nicht weiter. Wir müssen von dem Parameter für newDrawAll() verlangen, dass er eine Collection von Shape oder irgendeinem Subtyp von Shape ist. Wie wir bereits oben gesehen haben, läßt sich dies mit dem nach oben durch Shape beschränkte Wildcard "? extends Shape" ausdrücken. Die Implementierung von newDrawAll() sieht dann so aus: void newDrawAll(Collection<? extends Shape> shapes) {Für das nach oben beschränkten Wildcard gelten einige ähnliche Regeln wie für das unbeschränkten Wildcard. Deshalb wollen wir sie hier nur kurz wiederholen. Zum einen gibt es wieder als Alternative zum Wildcard eine Lösung mit einer generischen Methode, die die gleiche Typsicherheit und Funktionalität zur Verfügung stellt wie die mit dem Wildcard-Parameter. Die generische Methode sieht so aus: <T extends Shape> void newDrawAll(Collection<T> shapes) {Wiederum dürfen auf einem Typ, der mit einem nach oben beschränkten Wildcard parametrisiert ist, keine Methoden aufgerufen werden, die den Typparameter als Methodenparameter haben. Hier ein Beispiel: List<? extends Shape> myList = new ArrayList<Shape>();Zeile 3 läßt sich nicht übersetzen. Die Begründung ist die gleiche wie beim unbeschränkten Wildcard: der Compiler weiß nicht, ob die List<? extends Shape> nun Rectangles, Triangles, oder sonstwas enthält. Also kann er auch nicht beurteilen, ob das Einfügen eines Circle typsicher ist oder nicht. Deshalb darf die add()-Methode auf einer List<? extends Shape> nicht aufgerufen werden. Zeile 4 ist wieder okay, weil die typlose null-Referenz keine Typprobleme erzeugt. Der nach unten beschränkte Wildcard TypDie Benutzung des nach unten beschränkten Wildcards ist meist erst in komplexeren Situationen sinnvoll und nötig. Das Beispiel für die Verwendung des nach unten beschränkten Wildcards ist nicht ganz so einfach wie in den beiden vorhergehenden Fällen. Im Folgenden versuchen wir eine Methode zu implementieren, die das Maximum von zwei Objekten eines beliebigen Typs ermittelt. Es handelt sich um eine generische Methode, die als Parameter die beiden zu vergleichenden Objekte sowie einen Comparator nimmt. Der Comparator wird im Methodenbody zum Vergleich der beiden Objekte benutzt. Der erste Versuch ist dieser:T max(T t1, T t2, Comparator<T> cmp) {Schauen wir uns an, wie die Benutzung der Methode aussieht: max(new Date(), someOtherDate, ...); // Zeile 5Als dritter Parameter fehlt noch der Comparator. Dieser muss vom Typ Comparator<Date> oder einem Subtyp von Comperator<Date> sein. Was ist aber, wenn man folgenden universellen Comparator hat, mit dem sich beliebige Objekte vergleichen lassen: class UniversalComparator implements Comparator<Object> {Diesen Comparator kann man nicht als drittes Argument in der Codezeile 5 oben verwenden, weil er nicht vom richtigen Typ ist: er ist ein Comparator<Object> und nicht, wie gefordert, ein Comparator<Date>. Die bisherige Implementierung der generischen Methode max() ist nicht falsch. Sie ist aber auch nicht allgemein genug, um jeden sinnvoll möglichen Comparator zu akzeptieren. Was sind denn sinnvoll mögliche Comparatoren? In unserem Beispiel mit Date wären es Comparatoren, die entweder Objekte vom Typ Date oder Objekte vom Typ Object vergleichen können, also Comparator<Date> oder Comparator<Object>. Verallgemeinert heißt das: Comparatoren, die den betreffenden Typ oder einen seiner Supertypen vergleichen können. Das läßt sich mit Hilfe eines nach unten beschränkten Wildcards ausdrücken. Die korrigierte Lösung von max() müßte also folgendermaßen aussehen: T max(T t1, T t2, Comparator<? super T> cmp) {Es gibt natürlich für jedes Problem mehrere Lösungen. Um den UniversalComparator als drittes Argument in der Codezeile 5 zu nutzen, kann man statt eine Wildcard zu benutzen, eine Adapterklasser implementieren, die die Funktionalität des UniversalComparators in einer Klasse vom Typ Comparator<Date> zur Verfügung stellt: class MyDateComperator implements Comperator<Date> {Diese Lösung ist der Benutzung des nach unten beschränkten Wildcard Typs in der Implementierung von max() aber deutlich unterlegen, weil in jeder Situation, in der der Comparator-Typ nicht passt, eine neue Adapterklasse benötigt wird. Auch bei einem parametrisierten Typ mit einem nach unten beschränkten Wildcard gibt es Einschränkungen in der Benutzung. Diese Einschränkungen sind aber andere als die bei unbeschränkten und nach oben beschränkten Wildcards. Schauen wir uns dazu ein Beispiel an: List<? super Integer> d = new LinkedList<Number>();Die Codezeile 6 ist falsch und lässt sich nicht übersetzen. d ist vom Typ "List<? super Integer>", d.h. eine Liste mit Elementen vom Typ Integer oder mit Elementen eines Supertyps von Integer (Number, Serializable, Object, etc.). Wenn man ein Element aus der Liste nimmt, kann man nicht sicher sein, dass es vom Typ Integer oder Number ist. Allerdings kann man sicher sein, dass es vom höchsten Supertyp Object ist; deshalb ist Zeile 7 richtig. Zeile 8 ist richtig, denn einen Integer kann man problemlos ohne Verletzung der Typsicherheit in eine List<Integer>, List<Number>, List<Serializable> oder List<Object> tun. Für genau diese Supertypen von Integer steht ja "? super Integer". Damit ist auch klar, warum Zeile 9 wieder falsch ist: einen Long kann man nicht in einer List<Integer> ablegen. Der Compiler weiß aufgrund der Typinformation List<? super Integer> nicht, was für eine Liste es nun wirklich ist. Es könnte ja eine List<Integer> sein. Die Tatsache, dass d in unserem Beispiel eine List<Number> referenziert, in der man sehr wohl einen Long ablegen kann, ist nicht relevant, da der Compiler für d.add(n) den statischen Typ von d berücksichtigt, und der ist nun mal List<? super Integer>. ZusammenfassungWir haben in diesem Artikel eine Einführung in das Thema wildcard-parametrisierte Typen gegeben. Wildcards sind ein ziemlich schwieriges Thema, was im wesentlichen daran liegt, dass sie so neu und ungewohnt sind. Java ist die erste Sprache, die wildcard-parametrisierte Typen zulässt, so dass man bei diesem Thema nicht auf Erfahrungen aus anderen Sprachen zurückgreifen kann. Zu Anfang fehlt deshalb das intuitive Verständnis dafür, was zum Beispiel eine List<? super Integer> ist und was man damit machen kann. Nach einer Weile gewöhnt man sich aber an die Wildcard-Konstrukte und stellt recht rasch fest, dass sie relativ häufig in den Argumenttypen von Methoden vorkommen und recht nützlich sind.Wir haben in diesem Artikel bewusst die schwierigeren Aspekte wie Multilevel-Wildcards (Beispiel: Collection<? extends Pair<String,?>>) ausgeklammert. Wir haben auch nicht vor, sie in einem weiterführenden Artikel zu diskutieren. Wer sich dafür interessiert, findet Informationen zu dem Thema in unserem Generics-FAQ, siehe /MULTI/. Das nächste Mal befassen wir uns statt dessen mit der Type Erasure; das ist die Technik, mit der generischer Java-Source-Code in Java-Byte-Code übersetzt wird. Die Type Erasure hat vielfältige Auswirkungen und stellt wichtiges Hintergrundswissen für jeden Benutzer der Java Generics dar. Literaturverweise und weitere Informationsquellen
Die gesamte Serie über Java Generics:
|
||||||||||||||||||||||||||||||||
© Copyright 1995-2012 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/31.Wildcards/31.Wildcards.html> last update: 4 Nov 2012 |