|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Java Generics - A Generics Pair Class and its Constructors
|
||||||||||||||||||||||||||||||||
Nachdem wir uns in den vorangegangenen Beiträgen dieser Kolumne verschiedene Aspekte der Java Generics ausführlich angesehen haben, wollen wir in diesem und dem nächsten Beitrag zwei Fallstudien betrachten, in denen Generics verwendet werden. Die Beispiele illustrieren, wie man selbst generische Typen entwirft und implementiert und worauf man dabei achten muss. Wir wollen eine einfache Abstraktion implementieren, die zwei Objekte unterschiedlichen Typs enthält. Sie soll Pair heißen und zwei Typparameter haben, die die Typen der beiden enthaltenen Objekte repräsentieren. Es soll später möglich sein, dem Typ eines Paars anzusehen, was es enthält, d.h. man möchte Typen wie Pair<String,String>, Pair<Date,Object>, usw. bilden. Dazu muß die Klasse generisch sein. Eine solche Pair-Abstraktion kann durchaus nützlich sein, beispielsweise als kombinierter Returnwert, wenn einer Methode zwei Objekte zurückliefern muss. Da eine Funktion nur einen einzigen Returnwert liefern kann, muß sie die Rückgabeinformationen in einem einzigen Objekt zusammenfassen. Sie könnte also ein Pair als Returnwert liefern.
Im Folgenden wollen wir schrittweise eine solche Pair-Abstraktion implementieren
und daran demonstrieren, wie man Java Generics in der Praxis verwenden
kann.
Generische vs. nicht-generische Pair-KlasseDie Pair-Klasse soll zwei Objekte unterschiedlichen Typs enthalten und alle benötigten Infrastruktur-Methoden zur Verfügung stellen, wie Konstruktoren, Getter und Setter, Equality und HashCode, Clonen, Sortierreihenfolge und anderes. Hier ist ein erster Ausschnitt der Pair-Klasse:public final class Pair<X,Y> {Das heißt, wir definieren die Klasse Pair als generische Klasse mit je einem Typparameter für jedes der Felder. Da stellt sich bereits die erste Frage: Ist es eigentlich zwingend erforderlich, daß eine Pair-Klasse generisch ist? Man könnte doch einfach eine nicht-generischen Klasse mit zwei Feldern vom Typ Object implementieren. Sie könnte ebenfalls zwei Objekte beliebigen Typs aufnehmen. Wo liegt der Vorteil der generischen Klasse? Wie oben schon angedeutet, bietet eine generische Klasse die Möglichkeit, informativere Typen zu bilden, also zum Beispiel Pair<String,String> anstelle von Pair. Beim Typ Pair<String,String> sieht man gleich, daß Strings enthalten sind; beim Typ Pair ist aus dem Typ allein nicht sichtbar, was das Paar enthält. Die generische Pair-Klasse ist aber auch semantisch anders als eine nicht-generische Pair-Klasse. Nicht nur, daß man einem nicht-generischen Pair-Objekt nicht ansehen kann, was es enthält, es kann sogar in Verlauf seines Lebens gänzlich unterschiedliche Objekte enthalten: erst Äpfel und Birnen und später Double und Float. Bei einer generischen Pair-Klasse ist das anders. Ein Pair<String,String> enthält zwei Strings; ein Pair<Number,CharSequence> enthält eine Zahl und einen Zeichenkette; ein Pair<Object,Object> kann alles enthalten, so wie es bei der nicht-generischen Pair-Klasse der Fall ist. Prinzipiell liefert jedoch eine generische Pair-Klasse klarere Aussagen über die Art der enthaltenen Objekte und bringt auch deutlich zum Ausdruck, wenn man mit gemischten und wechselnden Typen von enthaltenen Objekten rechnen muß, wie im Falle eines Pair<Object,Object>. Dieser auf Grund der Verwendung von Generics deutlich deskriptivere Code hilft besonders, wenn es darum geht, Schnittstellen zwischen Programmteilen zu beschreiben. Auch die spätere Wartung des Programms wird erleichtert. Da wir eine Pair-Klasse implementieren wollen, die möglichst aussagekräftige Typkonstrukte erlaubt, muss unsere Klasse also generisch sein. KonstruktorenSehen wir uns nun die eigentliche Implementierung an. Fangen wir mit den Konstruktoren der generischen Pair-Klasse an. Wir implementieren einen Default-Konstruktor, einen Konstrukor, der die Initialwerte für die beiden enthaltenen Objekte nimmt, und einen Copy-Konstruktor, der ein neues Paar als Kopie eines existierenden Paars konstruiert.public final class Pair<X,Y> {Beim Copy-Konstruktor haben wir einen typischen Fehler gemacht, der sich durch Fehlermeldungen des Compilers bemerkbar macht. Der Compiler bemängelt am Copy-Konstruktor, daß die Zuweisung der Felder des existierenden Paars an das neue Paar nicht möglich sei, weil die Typen inkompatibel seien. Der Typ von this.first sei X und der Typ von other.first sei Object und das seinen keine kompatiblen Typen. Damit hat der Compiler auch Recht. X ist ein unbekannter Typ, der möglicherweise deutlich spezialisierter ist als Object. X wäre z.B. in einem Pair<String,String> der Typ String und bekanntlich kann man ein Object keinem String zuweisen. Irgendwas haben wir also falsch gemacht ... Der Fehler liegt in der Deklaration des Argumenttypen des Copy-Konstruktors. Wir haben als Typ des Konstruktor-Arguments Pair deklariert (ohne Typargumente in spitzen Klammern); Pair ist der sogenannte Raw Type. In dem Raw Type Pair sind die beiden enthaltenen Objekte vom Typ Object, weil für die Typparameter X und Y nichts spezifiziert wurde (siehe / GEN3 /). Folglich sind die beiden Felder im Pair other vom Typ Object und damit inkompatibel zu den Feldern von this, die nämlich vom Typ X und Y sind. Das ist ein typischer Fehler, den insbesondere Programmierer machen, denen Java Generics neu sind. In der Regel geschieht es unabsichtlich. Deshalb sollte man es sich zur Gewohnheit machen, generische Typen stets mit Typargumenten zu versehen. Schauen wir uns an, wie wir unseren Fehler beheben können. Wem nicht klar ist, daß die Fehlermeldung von der Raw Type Verwendung herrührt, der könnte auf folgende Lösung kommen: man könnte den Compiler doch zum Schweigen bringen, indem ein Cast eingefügt wird. Die beiden fraglichen Anweisungen könnten wie folgt "repariert" werden:
first = (X)other.first;
// warning: unchecked cast
Jetzt ist zumindest der Fehler weg, aber wir bekommen statt dessen Warnungen. Solche „unchecked cast“-Warnungen bekommt man immer, wenn der Zieltyp des Casts ein Typparameter ist. Das liegt daran, daß Typparameter zur Laufzeit wegen der Type Erasure keine Typrepräsentation haben, so dass der Cast zur Laufzeit nicht so ausgeführt werden kann, wie es der Sourcecode glauben macht (siehe / GEN4 /). Nun kann man Warnungen einfach ignorieren - immerhin läßt sich der Sourcecode ja übersetzen. Was passiert, wenn wir die Warnungen ignorieren? Möglicherweise gar nichts, wahrscheinlich ist aber eine unerwartete ClassCastException an einer Stelle, die keinerlei Hinweis auf die Ursache der Exception gibt. Hier ist ein Beispiel für eine Situation, in der eine solche unerwartete ClassCastException ausgelöst wird: public static void main(String... args) {Wir erzeugen ein Paar p2 vom Typ Pair<String,Date> als Kopie des Paares p1 vom Typ Pair<String,Integer> mit Hilfe unseres Copy-Konstruktors, obwohl p1 einen String und einen Integer enthält und die Kopie einen String und ein Datum. Das ist offensichtlich nicht in Ordnung und man würde erwarten, daß der Compiler es bemerkt und eine Fehlermeldung ausgibt. Das ist aber nicht der Fall. Unser Copy-Konstruktor hat mit den ungleichen Paaren nicht das geringste Problem und weist tapfer dem Datum den Inhalt des Integers zu. Damit haben wir ein Paar vom Typ Pair<String,Date> erzeugt, das zwar so aussieht, als enthielte es einen String und ein Datum, aber in Wirklichkeit einen String und einen Integer enthält. Das macht u. U. lange keine Probleme, aber früher oder später wird man auf den zweiten Teil des Paares zugreifen, in der festen Überzeugung, daß es sich um ein Datum handelt. Und genau in diesem Moment stellt sich heraus, daß das vermeintliche Datum ein Integer ist und es gibt eine ClassCastException. Diese ClassCastException entsteht völlig unerwartet. Man erwartet ClassCastExceptions eigentlich nur als Ergebnis eines gescheiterten Casts, aber in der betreffenden Sourcecodezeile ist weit und breit kein Cast zu sehen. Die ClassCastException wird nämlich ausgelöst durch einen Cast, den der Compiler im Zuge der Type Erasure heimlich eingefügt hat; dieser Cast ist im Sourcecode nicht sichtbar. Wer unchecked-Warnungen ignoriert, muß also mit unerwarteten ClassCastExceptions rechnen. Das Unangenehme an diesen unerwarteten Exceptions ist, daß ihre Ursache in der Regel nur schwer zu identifizieren ist, weil sie in einem ganz anderen Teil der Applikation schlummert. In unserem Beispiel liegt das Problem beim Copy-Konstruktor der Pair-Klasse: der Argumenttyp ist schlicht falsch deklariert und die unchecked-Casts verschleiern dieses Problem, statt es zu lösen. Korrekt wäre folgende Lösung: Wie oben schon angedeutet, müssen wir als Argumenttyp des Copy-Konstrutors statt des Raw Types eine konkrete Parametrisierung der Pair-Klasse deklarieren, um das Problem zu lösen. In Frage käme die Parametrisierung Pair<X,Y>. Das würde bedeuten, daß ein neues Paar nur als Kopie eines Paars des exakt gleichen Typs erzeugt werden kann. Der Copy-Konstruktor sähe dann so aus: public Pair(Pair<X,Y> other) {Diese Lösung ist sicher nicht falsch, aber gänzlich befriedigend ist sie auch nicht. Sie schließt zwar aus, daß ein Pair<String,Date> aus einem Pair<String,Integer> erzeugt werden kann, wie es zuvor möglich war. Gleichzeitig schließt diese Deklaration aber auch Konstruktionen aus, die durchaus sinnvoll wären. Hier ist ein Beispiel: public static void main(String... args) {Warum sollte es verboten sein, ein Pair<String,Number> aus einem Pair<String,Integer> zu erzeugen? Immerhin kann der Integer der Number zugewiesen werden, weil Integer ein Subtyp von Number ist. Diese Copy-Konstruktion wäre absolut sinnvoll, wird aber vom Compiler abgewiesen, weil die beiden Paare von unterschiedlichem Typ sind. Wie bringt man es nun fertig, diese sinnvolle Copy-Konstruktion zuzulassen und alle gefährlichen Copy-Konstruktionen auszuschließen? Es gibt zwei Lösungen:
Generischer Konstruktor vs. Wildcard-ArgumenttypenBetrachten wir als erstes die Lösung mit dem generischen Copy-Konstruktor. Wenn wir den Copy-Konstruktor als generische Methode mit eigenen Typparametern deklarieren, können wir als Konstruktor-Argument kompatible Paare zulassen, d.h. Paare, deren Inhalte Subtypen der Inhalte des zu erzeugenden Paares sind. Das sähe so aus:public <A extends X, B extends Y>Dieser generische Konstruktor hat zwei eigene Typparameter A und B, die jeweils Subtypen von X bzw. Y sein müssen (wobei hier und im Folgenden „Subtyp“ bedeutet, dass A von X abgeleitet ist oder dass A gleich X ist). Man kann also ein Pair<X,Y> aus einem Pair<A,B> erzeugen, wenn A ein Subtyp von X und B ein Subtyp von Y ist. Damit läßt dieser Konstruktor zu, daß ein Pair<String,Number> aus einem Pair<String,Integer> erzeugt wird, weil Integer ein Subtyp von Number ist, und schließt aus, daß ein Pair<String,Date> aus einem Pair<String,Integer> erzeugt wird, weil Integer eben kein Subtyp von Date ist. Das ist genau das, was wir erreichen wollten, wie man im folgenden sieht: public static void main(String... args) {Alternativ kann der Copy.Konstruktor als nicht-generische Methode definiert werden. Dabei werden als Argumenttypen des Copy-Konstruktors Wildcard-Typen verwendet. Das sähe so aus: public Pair(Pair<? extends X,? extends Y> other) {Jetzt ist der Copy-Konstruktor keine generische Methode mehr, sondern eine reguläre Methode, dafür ist der Argumenttyp ein Wildcard-Typ. Der Effekt ist derselbe: es werden nur Paare als Konstruktor-Argumente akzeptiert, die Objekte von Subtypen enthalten; alle anderen Paare werden abgewiesen (siehe / GEN2 /).
Wie das Beispiel des Copy-Konstruktors illustriert, ist die Verwendung
von Wildcard-Typen oder generischen Methoden keineswegs exotisch, sondern
selbst bei einfachen generischen Klassen bereits sehr naheliegend.
Value-SemantikUnser Copy-Konstruktor hat eine Schwäche: er weist den Feldern des neuen Paars lediglich Referenzen auf die Felder des anderen Paares zu, so daß anschließend beide Paare gemeinsam auf diesselben Objekte verweisen. Diese Referenz-Zuweisung haben wir konsequent in allen Konstruktoren gemacht, nicht nur im Copy-Konstruktor. Die Gemeinsamverwendung ist aber möglicherweise nicht das, was man haben möchte. Oft ist es so, dass sich das neue Paar eine Kopie des Inhalts des anderen Paares speichern soll, damit beide Paare voneinander unabhängig sind. Um dies zu erreichen, müssten wir die Felder des anderen Paares klonen. Unser Copy-Konstruktor müsste dann in etwa so aussehen:public Pair(Pair<? extends X,? extends Y> other) {Das gefällt dem Compiler gar nicht. Er meldet hier gleich mehrere Probleme. Das erste Problem ist, dass die clone()-Methode in der Klasse Object protected ist und deshalb nicht aufgerufen werden darf. Der Compiler stellt nämlich fest, dass die clone()-Methode auf einem Objekt von unbekanntem Typ (genauer gesagt: unbekanntem Subtyp von X bzw. Y) aufgerufen wird. Da der Compiler über den unbekannten Typ nichts weiß, kommt also nur die clone()-Methode aus Object für den Aufruf in Frage, und die ist leider protected in Object und damit private in jedem Subtyp von Object. Das zweite Problem ist der Returntyp der clone()-Methode, aber darauf kommen wir gleich noch. Versuchen wir erst einmal das Problem mit Zugriff auf die clone()-Methode zu lösen. Es wäre naheliegend, nach Cloneable zu casten, um so die clone()-Methode im unbekannten Subtyp von X bzw. Y zugänglich zu machen. Das sähe etwa so aus: public Pair(Pair<? extends X,? extends Y> other) {Leider hilft es nicht. Erstens ist nicht jeder Typ Cloneable; der Cast könnte also mit einer ClassCastException scheitern. Aber selbst wenn other.first und other.second von Cloneable-Typen wären, dann würde der Cast nach Cloneable noch immer keinen keinen Zugriff auf die clone()-Methode geben, weil das Cloneable-Interface ein leeres Marker-Interface ist. Die einzige Möglichkeit, die clone()-Methode aufzurufen, ist die Verwendung von Reflection. Das sähe dann so aus: public Pair(Pair<? extends X,? extends Y> other) {Über die Fehlerbehandlung kann man streiten. Wir weisen einfach die Referenzen zu, wenn kein Klon erzeugt werden kann. Man könnte alternativ auch eine Exception werfen. Beide Lösungen wären sinnvoll. Es gibt aber hier noch ein anderes Problem: der Compiler liefert "unchecked"-Warnungen. Das liegt daran, dass Methoden, die über Reflection aufgerufen werden, immer Object zurück liefern. Genauer gesagt, um eine Methode über Reflection aufzurufen, wird die Methode invoke() der Klasse java.lang.reflect.Method benutzt. Das sieht man im obigen Beispiel: getMethod() liefert das Method-Objekt für die clone()-Methode und anschließend wird die clone()-Methode per invoke() aufgerufen. Die invoke()-Methode liefert den Returnwert der aufgerufenen clone()-Methode über eine Referenz vom Typ Object zurück. Das ist immer so, wenn Methoden, die über Reflection aufgerufen werden, denn über die invoke()-Methode können beliebige Methoden mit beliebigen Returntypen aufgerufen werden. Was auch immer der Returnwert ist, beim Aufruf über invoke() kommt er als Object-Referenz zurück. Wir verlieren beim reflektiven Aufruf also Typinformation. Selbst wenn in userem Beispiel die fraglichen clone()-Methode Objekte vom Typ X bzw. Y zurückgeben, bekommen wir immer nur eine Object-Referenz auf den Returnwert geliefert. Es ist also ein Cast auf den eigentlichen Returntyp nötig, in unserem Fall ein Cast mit dem Zieltyp X bzw. Y. Das Problem ist nun, dass X und Y die Typparameter der Pair-Klasse sind und zur Laufzeit keine Repräsentation haben wegen der Type Erasure. Solche Casts können zu Problemen führen, wie wir oben (in Abschnitt "Konstruktoren") bereits diskutiert haben. Der Compiler gibt also aus gutem Grund eine Warnung ab. Normalerweise kann man "unchecked"-Warnungen durch typkorrektes Programmieren vermeiden; auch das haben wir oben schon gesehen. Hier gibt es aber keine Möglichkeit, den Cast zu vermeiden. Man kann bestenfalls mit einer @SuppressWarning("unchecked")-Annotation die Warnungen unterdrücken. Ehe man das tut, sollte man sich aber fragen, ob man die Warnung wirklich getrost ignorieren kann. Kann hier irgendwas schief gehen? Zur Laufzeit werden die Casts nach X bzw. Y durch Casts nach Object, also durch "gar nichts" ersetzt. Es wird also nicht geprüft, ob das Ergebnis des Klonens vom erwarteten Typ X bzw. Y ist. Das muss aber auch gar nicht geprüft werden. Wenn die clone()-Methoden von other.first und other.second korrekt implementiert sind, dann liefern sie Objekte vom Typ X bzw. Y zurück. Der Cast ist hier nur erforderlich, weil die clone()-Methoden über Reflection aufgerufen werden müssen und dabei geht unvermeidlich die Information über den Returntyp verloren. Das heißt aber nicht, dass die clone()-Methoden nun plötzlich unerwartete Typen von Objekten zurückliefern. Es besteht also keine Gefahr und die "unchecked"-Warnungen können guten Gewissens unterdrückt werden.
Was wir hier am Beispiel des Copy-Konstruktors beschrieben haben, zieht
sich natürlich durch die gesamte Implementierung der Pair-Klasse.
Auch andere Konstruktoren und Methoden werden Klone erzeugen und dabei
Reflection verwenden und Warnungen unterdrücken müssen.
ZusammenfassungSelbstverständlich ist die Implementierung unserer Pair-Klasse rudimentär geblieben. Man würde sicherlich weitere Methoden wie equals(), hashcode(), clone(), compareTo(), etc. implementieren. Auch dabei macht man interessante Erfahrungen im Umgang mit Generics. Wir werden weitere Aspekte der Implementierung der Pair-Klasse im nächsten Beitrag betrachten. Aber selbst bei der Implementierung der Konstruktoren haben wir bereits einige interessante Beobachtung gemacht.
Wir haben gesehen, was passiert, wenn man unchecked-Warnungen ignoriert.
Wir haben festgestellt, daß generische Methoden und Wildcard-Typen
selbst in einfachen generischen Klassen bereits vorkommen. Wir sind
auf das uralte Problem mit dem leeren Cloneable-Interface gestoßen.
Und wir haben erlebt, daß man "unchecked"-Warnung manchmal gar nicht
vermeiden kann.
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/35.GenericPairPart1/35.GenericPairPart1.html> last update: 5 Jun 2012 |