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 - A Generic Pair Class and its compareTo Method

Java Generics - A Generic Pair Class and its compareTo Method
Java Generics: Eine generische Klasse - Fallstudie
Eine Pair-Klasse und ihre compareTo()-Methode

JavaSPEKTRUM, März 2008
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

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ückblick

Die 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> {
  private X first;
  private Y second;
  ...
}
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.
 

Comparable

Zu 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>> {
  private X first;
  private Y second;

  public int compareTo(Pair<X,Y> other) {
  ... first.compareTo(other.first) ...        // error: cannot find compareTo method
  ... second.compareTo(other.second) ...      // error: cannot find compareTo method
  }
}
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>,
                        Y extends Comparable<Y>>
 implements Comparable<Pair<X,Y>> {

 public int compareTo(Pair<X,Y> other) {
  ... first.compareTo(other.first) ...     // now fine
  ... second.compareTo(other.second) ...   // now fine
 }
}
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>,
                        Y extends Comparable<Y>>
 implements Comparable<Pair<X,Y>> {

}
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:

  • Man verwendet Typparameter-Bounds und implementiert mehrere Klassen mit unterschiedliche, Namen, z.B. Pair und ComparablePair.
  • Man verzichtet auf die Typparameter-Bounds und verwendet nur eine einzige Pair-Klasse, die dann uneingeschränkt verwendbar ist.

Mehrere Pair-Klassen mit unterschiedlichen Namen

Wenn 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>,
                            Y extends Comparable<Y>>
  extends Pair<X,Y>
  implements Comparable<ComparablePairPair<X,Y>> {
  …
  public int compareTo(ComparablePair<X,Y> other) { ... }
}
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,
                           Y extends Cloneable>
  extends Pair<X,Y>
  implements Cloneable {
  …
  public CloneablePair<X,Y> clone() { ... }
}
Wenn beides kombiniert wird, braucht man ein CloneableComparablePair:
public class CloneableComparablePair<X extends Cloneable & Comparable<X>,
                                     Y extends Cloneable & Comparable<Y>>
  extends ComparablePair<X,Y>
  implements Cloneable {
  …
  public CloneableComparablePair<X,Y> clone() { ... }
}
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-Klasse

Betrachten 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>> {
  public int compareTo(Pair<X,Y> other) {
    ... ((Comparable<X>)first).compareTo(other.first) ...      // warning: unchecked cast
    ... ((Comparable<Y>)second).compareTo(other.second) ...    // warning: unchecked cast
  }
}
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>> {
  public int compareTo(Pair<X,Y> other) {
    ... ((Comparable<?>)first).compareTo(other.first) ...      // error: method cannot be called
    ... ((Comparable<?>)second).compareTo(other.second) ...    // error: method cannot be called
  }
}
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>> {
  public int compareTo(Pair<X,Y> other) {
    ... ((Comparable)first).compareTo(other.first) ...    // warning: method invocation on raw type
    ... ((Comparable)second).compareTo(other.second) ...  // warning: method invocation on raw type
  }
}
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.
 

Fazit

Wir 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.
Die Lösung ohne Typparameter-Bounds hat den Schönheitsfehler, dass sie unchecked-Warnungen verursacht und schlimmstenfalls unerwartete ClassCastExceptions auslöst.  In unserem Beispiel mit der compareTo-Methode sind unerwartete ClassCastExceptions sehr unwahrscheinlich, so dass man die unchecked-Warnungen relativ guten Gewissens unterdrücken kann.  Das ist aber gewiss nicht immer so und muss in jedem Einzelfall erneut entschieden werden.

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

/FAQ/ Java Generics FAQ
Angelika Langer
URL: http://www.AngelikaLanger.com/GenericsFAQ/JavaGenericsFAQ.html
/BRIDGE/ Java Generics FAQ - Technical Details FAQ #102: What is a bridge method?
Angelika Langer
URL: http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ102

Die gesamte Serie über Java Generics:

/GEN1/  Java Generics - Einführung
Klaus Kreft & Angelika Langer
Java Spektrum, März 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/30.GenericsIntro/30.GenericsIntro.html
/GEN2/  Java Generics - Wildcards
Klaus Kreft & Angelika Langer
Java Spektrum, Mai 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/31.Wildcards/31.Wildcards.html
/GEN3/ Java Generics - Raw Types & Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, Juli 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/32.TypeErasure/32.TypeErasure.html
/GEN4/ Java Generics - Schattenseiten der Type Erasure
Klaus Kreft & Angelika Langer
Java Spektrum, September 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/33.TypeErasurePitfall/33.TypeErasurePitfall.html
/GEN5/ Java Generics - Generische Objekterzeugung
Klaus Kreft & Angelika Langer
Java Spektrum, November 2007
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/34.GenericCreation/34.GenericCreation.html
/GEN6/ Java Generics - Eine generische Klasse - Teil 1: Konstruktoren
Klaus Kreft & Angelika Langer
Java Spektrum, Januar 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/35.GenericPairPart1/35.GenericPairPart1.html
/GEN7/ Java Generics - Eine generische Klasse - Teil 2: compareTo()-Methode
Klaus Kreft & Angelika Langer
Java Spektrum, März 2008
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/36.GenericPairPart2/36.GenericPairPart2.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 Language Features - An overview of all language features added to Java in release 5.0; includes generics, concurrency utilities, enumeration types, auto-boxing, etc.
2 day seminar (on-site)
Java Generics - Java Generics Explained: Everything You Always Wanted To Know About Generics
2 day seminar ( open enrollment and on-site)
Effective Java - Java best practice programming techniques, common pitfalls, and off-the-beaten-path language features
4 day seminar ( open enrollment and on-site)
 

 
  © 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