|
|||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||
|
Java Generics - A Generic Pair Class and its compareTo Method
|
||||||||||||||||||||||||||||||||
Wir haben uns im letzen Beitrag (siehe /
GEN6
/)
angesehen, wie man eine generische Pair-Klasse implementieren kann und
wie deren Konstruktoren aussehen würden. Dieses Mal wollen
wir uns weitere Methoden der Pair-Klasse ansehen.
RückblickDie Pair-Klasse haben wir so angelegt, dass sie zwei Objekte unterschiedlichen Typs enthält und alle benötigten Infrastruktur-Methoden zur Verfügung stellt, wie Konstruktoren, Getter und Setter, Equality und HashCode, Clonen, Sortierreihenfolge und anderes. Im Prinzip sieht die Pair-Klasse so aus:public final class Pair<X,Y> {Die Konstruktoren haben wir letztes Mal ausführlich besprochen. Jetzt wollen wir eine eine Sortierreihenfolge für Paare implementieren, damit wir die Paare beispielsweise in einem TreeSet ablegen können. ComparableZu diesem Zweck soll die Klasse Pair das Interface Comparable implementieren. Ein erster Versuch sieht in etwa so aus:public final class Pair<X,Y> implements Comparable<Pair<X,Y>> {Die Details der Implementierung der Methode compareTo vernachlässigen wir einmal. Egal wie die Implementierung im Detail aussieht, sie wird höchstwahrscheinlich die compareTo-Methoden der beiden enthaltenen Objekte aufrufen. Diese Aufrufe führen zu Fehlermeldungen. Das liegt daran, daß die beiden zu vergleichenden Felder von unbekannten Typen X und Y sind, über die der Compiler nichts weiß. Insbesondere weiß er nicht, ob die beiden unbekannten Typen überhaupt compareTo-Methoden haben. In dieser Situation helfen die Typparameter-Bounds (siehe / GEN1 /). Mithilfe der Bounds legen wir fest, daß die beiden unbekannten Typen X und Y Subtypen von Comparable<X> bzw. Comparable<Y> sein müssen, damit der Compiler weiß, dass X und Y jeweils compareTo-Methoden haben und er sie aufrufen kann. Das sieht dann so aus: public final class Pair<X extends Comparable<X>,Mit den Typparameter-Bounds haben wir nun die compareTo-Methode der Klasse Pair implementieren können, aber leider hat diese Lösung auch Nachteile. Es ist jetzt nicht mehr möglich, ein Pair<Number,Number> zu bilden, weil Number kein Subtyp von Comparable<Number> ist, wie in der Bounds-Klausel verlangt wird. Mit anderen Worten, durch das Hinzufügen der Typparameter-Bounds ist die Verwendbarkeit unserer Pair-Klasse spürbar reduziert worden. Das ist ein typischer Nebeneffekt von Typparameter-Bounds. Einerseits erlauben sie den Aufruf bestimmter Methoden der Typparameter und damit die Implementierung eigener Methoden des generischen Typs, andererseits ist die Menge der möglichen Typargumente durch die Typparameter-Bounds eingeschränkt. Im Falle unserer Pair-Klasse muß man nun entscheiden, ob die Einschränkung akzeptabel ist. Wohl eher nicht. Ein Pair<Number,Number> ist absolut sinnvoll und es gibt semantisch keinen Grund, warum es keine Paare von non-comparable Typen geben sollte. Wenn die enthaltenen Objekte nicht vergleichbar sind, dann kann man halt die Paare nicht vergleichen, aber alle anderen Methoden stehen uneingeschränkt zur Verfügung. Idealerweise möchte man also, dass Paare von vergleichbaren Typen vergleichbar sind und Paare, bei denen mindestens ein Objekt nicht vergleichbar ist, nicht vergleichbar sind. Jetzt wäre es naheliegend, dass man zwei Varianten der Pair-Klasse bereitstellt: eine, die Comparable<Pair<X,Y>> implementiert und nur für Comparable-Typen X und Y verwendet werden kann, und eine, die nicht Comparable ist und allgemein für alle Typen benutzt werden kann. Also: public final class Pair<X extends Comparable<X>,und public final class Pair<X,Y> {Das geht aber in Java nicht. Die beiden Klassen würden nach der Type Erasure beide Pair heißen und wären nicht mehr voneinander unterscheidbar. Deshalb läßt der Compiler eine solche Spezialisierung von generischen Klassen nicht zu. Es gibt zwei Möglichkeiten, dem Problem zu begegnen:
Mehrere Pair-Klassen mit unterschiedlichen NamenWenn wir mehrere Pair-Klassen mit unterschiedliche Namen implementieren, z.B. Pair und ComparablePair, dann liegt es nahe, dass ComparablePair von Pair abgeleitet ist.Also: public class Pair<X,Y> {und public class ComparablePair<X extends Comparable<X>,Diese Implementierungsstrategie hat den Nachteil, dass sie zu einer Inflation von Klassen führt, wenn weitere Operationen implementiert werden sollen, die ebenfalls Anforderungen an die im Paar enthaltenen Objekte stellen und zu weiteren Typparameter-Bounds führen. Wenn wir z.B. erreichen wollen, dass die Pair-Klasse Cloneable ist, dann brauchen wir auch noch ein CloneablePair. public class CloneablePair<X extends Cloneable,Wenn beides kombiniert wird, braucht man ein CloneableComparablePair: public class CloneableComparablePair<X extends Cloneable & Comparable<X>,Da es in Java keine Mehrfachvererbung gibt, ist es schwierig, Redundanz zu vermeiden. Wir haben zwar die Implementierung der compareTo-Methode vererben können, aber die Implementierung der clone-Methode wiederholt sich in CloneablePair und ClonableComparablePair.
Angesichts der denkbaren und fast unvermeidlichen Inflation von Klassen,
die bei diesem Lösungsansatz entsteht, gewinnt die alternative Lösungsstrategie
mit Verzicht auf Typparameter-Bounds an Attraktivität. Sehen
wir das einmal genauer an.
Die universell verwendbare Pair-KlasseBetrachten wir die Variante, bei der wir nur eine einzige Pair-Klasse implementieren und darin auf die Typparameter-Bounds verzichten, um eine breite Verwendungbarkeit der Pair-Klasse zu erreichen.Die universelle Klasse würde das Interface Comparable<Pair<X,Y>> implementieren und hätte eine entsprechende compareTo-Methode. Diese compareTo-Methode würde funktionieren, wenn die beiden enthaltenen Objekte ihrerseits vergleichbar sind (d.h. das entsprechende Comparable<X>- bzw. Comparable<Y>-Interface implementieren). Die compareTo-Methode würde mit einer Exception scheitern, wenn das Paar Objekte von non-Comparable Typen enthält. Das bedeutet, die compareTo-Methode funktioniert, wann immer es die enthaltenen Objekte zulassen, und scheitert andernfalls. Man könnte einwenden, dass es schlechtes Design sei, wenn eine Klasse (z.B. Pair<Number,Number> eine compareTo-Methode hat, die immer scheitert (weil Number das Interface Comparable<Number> nicht implementiert). Wenn die Methode immer scheitert, dann sollte sie besser gar nicht im API auftauchen. Ähnliche Einwände werden auch gegen Klassen im JDK vorgebracht. Man denke beispielsweise an die unmodifiable-Adaptoren im java.util-Package: die Methode Collections.unmodifiableCollection liefert eine adaptierte Collection zurück, bei der alle modifizierenden Methoden mit einer UnsupportedOperationException scheitern. Der Grund für dieses Design war im JDK ähnlich wie in unserem Beispiel. Es sollte eine Inflation von Klassen und Interfaces verhindert werden, weil es nämlich auch noch synchronized- und checked-Adaptoren gibt und man andernfalls sämtliche Kombinationen sämtlicher Adaptoren hätte berücksichtigen müssen. So gesehen erscheint es akzeptabel, dass nur Paare mit vergleichbarem Inhalt vergleichbar sind und bei Paaren mit nicht vergleichbarem Inhalt die compareTo-Methode mit einer Exception scheitert. Ob man das Scheitern durch eine UnsupportedOperationException wie im JDK zum Ausdruck bringt oder durch eine Exception von einem anderen Typ, ist Geschmacksache. Einfacher, und vielleicht auch informativer, wäre eine ClassCastException, wie die nähere Betrachtung der Implementierung der compareTo-Methode zeigt. Wenden wir uns der Implementierung zu. Um eine breite Verwendbarkeit der Pair-Klasse zu erreichen, haben wir bewußt auf die Typparameter-Bounds verzichtet. Der Verzicht auf die Typparameter-Bounds hat den Nebeneffekt, dass Casts erforderlich werden, die einerseits scheitern können (aus den eben geschilderten Gründen) und andererseits zu unvermeidlichen unchecked-Warnungen führen. Die Implementierung könnte in etwa so aussehen: public final class Pair<X,Y> implements Comparable<Pair<X,Y>> {Ohne die Typparameter-Bounds ist ein Cast nach Comparable<X> bzw. Comparable<Y> erforderlich, um überhaupt die compareTo-Method der beiden enthaltenen Objekte aufrufen zu können. Jeder Cast, dessen Zieltyp ein parametrisierter Typ ist, führt zu einer unchecked-Warnung, weil zur Laufzeit nur auf den Raw Type geprüft werden kann, nicht aber auf den parametrisierten Typ. Um die Warnung zu vermeiden, könnte man versuchen, einen Cast nach Comparable<?> zu machen. Der Cast auf einen unbounded-Wildcard-Typ wie Comparable<?> löst nämlich keine Warnung aus. Das sähe dann so aus: public final class Pair<X,Y> implements Comparable<Pair<X,Y>> {Leider bekommt man dann eine Fehlermeldung, weil in einem Wildcard-Typ nicht alle Methoden uneingeschränkt aufgerufen werden dürfen, und die compareTo-Methode gehört dummerweise zu den Methoden, die nicht aufgerufen werden dürfen (siehe / GEN2 /). Bleibt noch der Versuch, auf den Raw Type Comparable zu casten. Dann gibt es zwar zu dem Cast keine Warnung mehr, aber der Aufruf der compareTo-Methode auf einem Raw Type führt wiederum zu einer unchecked-Warnung (siehe / GEN3 /), diesmal nicht wegen dem Zieltyp des Casts, sondern wegen der Verwendung des Raw Types. public final class Pair<X,Y> implements Comparable<Pair<X,Y>> {Was man auch versucht, die unchecked-Warnungen sind nicht zu umgehen. Man kann sie lediglich durch die Verwendung einer @SuppressWarnings("unchecked")-Annotation unterdrücken. Zuvor sollte man sich aber fragen, ob das Ignorieren der Warnungen wirklich unproblematisch ist oder ob es zu üblen Fehlern kommen kann. Was kann schlimmstenfalls passieren? Zunächst einmal kann es sein, daß die Typen X oder Y keine Subtypen von Comparable sind. Dann scheitert der Cast sowieso. Das ist nicht überraschend. In diesem Fall sind die enthaltenen Objekte nicht vergleichbar und damit ist auch das Paar nicht vergleichbar. Das Scheitern der compareTo-Method mit einer ClassCastException wäre in diesem Fall ein erwartetes und richtiges Verhalten. Es kann aber auch passieren, daß die Typen X oder Y zwar Subtypen von Comparable sind, aber nicht vergleichbar zu sich selbst sind. Beispielsweise könnte der Typ X vergleichbar mit einem anderen Typ sein, z.B. vergleichbar zu String. Dann wäre der Typ X ein Subtyp von Comparable<String> und hätte eine compareTo-Methode, die einen String als Argument erwartet. Wir würden aber ein Objekt vom Typ X als argument an die compareTo-Methode übergeben. Das Ergebnis wäre wiederum eine ClassCastException, deren Ursprung diesmal allerdings schwer zu lokalisieren ist. Sie wird nämlich von einer synthetischen Methode ausgelöst wird, die der Compiler in den Subtyp X von Comparable<String> hinein generiert hat. Auf die Details wollen wir an dieser Stelle nicht eingehen. Nur soviel: die synthetische Methode ist eine sogenannte Bridge-Methode und ist im Sourcecode nicht sichtbar. (Details zu Bridge-Methoden findet man unter / BRIDGE /.) Der Stack-Trace der ausgelösten Exception ist deshalb nicht ein wenig verwirrend. Er wird auf Sourcezeilen verweisen, die gar nicht existieren oder mit dem Problem nichts zu tun haben.
Man bekommt also schlimmstenfalls eine unerwartete ClassCastException,
deren Ursache schwer zu finden ist. Das ist eigentlich fast immer
so, wenn man unchecked-Warnungen ignoriert. Wie wahrscheinlich ist
es denn nun, dass dieser schlimmste Fall eintritt? Erfahrungsgemäß
sind Typen, die nicht zu sich selbst aber zu einem anderen Typ vergleichbar
sind, ziemlich ungewöhnlich, so dass das Problem wohl in der Praxis
nur selten auftreten wird. Schließlich konnte man so etwas vor dem
JDK 5.0 überhaupt nicht ausdrücken und das Ableiten von Comparable
bedeutete immer die Vergleichbarkeit mit dem eigenen Typ bzw. typkompatiblen
Subtypen.
FazitWir haben gesehen, dass Typparameter-Bounds einerseits hilfreich, aber andererseits auch hinderlich sind. Sie vereinfachen die Implementierung von Methoden des generischen Typs, schränken aber die Verwendbarkeit des generischen Typs ein. Man steht daher bei der Implementierung manchmal vor der Entscheidung, ob man Typparameter-Bounds verwenden soll oder nicht.Mit dem Verzicht auf die Typparameter-Bounds erhält man einen universell verwendbaren Typ, der mit allen oder zumindest einer größeren Zahl von Typen parametrisiert werden kann. In unserem Beispiel ging es einen allgemeinen Pair-Typ. Die Typparameter-Bounds Comparable<X> und Comparable<Y> wurden nur für eine einzige Methode gebraucht, nämlich die compareTo-Methode. In solchen Fällen ist es überlegenswert, ob man wegen einer einzigen Methode die Benutzbarkeit des gesamten generischen Typs drastisch einschränken will oder ob es nicht akzeptabel wäre, wenn die betreffende Methode eben für gewisse Parametrisierungen mit einer Exception scheitert, der generische Typ an sich aber allgemein verwendbar ist.
Wir haben zwei Lösungen betrachtet: eine mit und eine ohne Typparameter-Bounds.
Unter Benutzung von Typparameter-Bounds ist die Implementierung ohne
Warnungen möglich. Dafür kann der generische Typ
nur mit einer eingeschränkten Menge von Typen parametrisiert werden,
so dass man mehrere Varianten der Klasse implementieren wird. Das
führt u.U. zu einer Inflation von Klassen und einem gewissen Maß
an Redundanz in der Implementierung.
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/36.GenericPairPart2/36.GenericPairPart2.html> last update: 5 Jun 2012 |