9.5 Generics und Vererbung, Invarianz
Vererbung und Substitution ist für Java-Entwickler alltäglich, sodass diese Eigenschaft nicht weiter verwunderlich ist. Die toString()-Methode zum Beispiel wird ganz natürlich auf allen Objekten aufgerufen, und Entwicklern ist klar, dass der Aufruf dynamisch gebunden ist. Genauso lässt sich bei String.toString(Object o) jedes Objekt übergeben, und die statische Methode ruft die Objektmethode toString() auf.
9.5.1 Arrays sind invariant

Nehmen wir als folgendes Beispiel die Hierarchie der bekannten Wrapper-Klassen. Natürlich steht Object oben. Die numerischen Wrapper-Klassen implementieren alle Number. Darunter stehen dann etwa Integer, Double und die anderen numerischen Wrapper. Folgendes bereitet keine Kopfschmerzen:
Number number = Integer.valueOf( 10 );
number = Double.valueOf( 1.1 );
Einmal zeigt number auf ein Integer, dann auf ein Double-Objekt.
Wie verhält es sich nun mit Arrays? Da ist ein Number-Array der Basistyp eines Double-Arrays:
Number[] numbers = new Double[ 100 ];
numbers[ 0 ] = 1.1;
Dass ein Array vom Typ Double[] ein Untertyp von Number[] ist, nennt sich Invarianz. Doch lässt sich das auch auf Generics übertragen?
9.5.2 Generics sind kovariant

Es funktioniert, Folgendes zu schreiben:
Set<String> set = new HashSet<String>();
Ein HashSet mit Strings ist eine Art von Set mit Strings. Aber ein HashSet mit Strings ist kein HashSet mit Objects. Damit wäre Folgendes falsch:
HashSet<Object> set = new HashSet<String>(); //Compilerfehler!
Generics sind nicht invariant, sie sind kovariant. Diese Eigenschaft ist auf den ersten Blick gegen die Intuition, doch ein Beispiel rückt diesen Eindruck schnell gerade. Bleiben wir bei unserem Pocket und den Wrapper-Klassen. Auch wenn Number die Oberklasse von Integer ist, so gilt dennoch nicht, dass Pocket<Number> ein Obertyp von Pocket<Integer> ist. Wäre es das, wäre Folgendes möglich und zur Laufzeit ein Problem:
Pocket<Number> p;
p = new Pocket<Integer>(); // Ist das OK?
p.set( 2.2 );
Das Argument 2.2 ist über Autoboxing ein Double, und daher scheint es auf Number zu passen. Allerdings sollte Double aber gar nicht erlaubt sein, da wir mit Pocket<Integer> ja eine Tasche für Integer aufgebaut haben, und ein Double darf nicht in die Integer-Tasche. Daher folgt: Die Ableitungsbeziehung zwischen Typen überträgt sich nicht auf generische Klassen. Ein Pocket<Number> ist also keine Oberklasse, die alle erdenklichen numerischen Typen in der Tasche erlaubt. Der Compiler meckert bei diesem Versuch sofort:
Pocket<Number> p;
p = new Pocket<Integer>(); //Type mismatch: cannot convert from Pocket<Integer>
// to Pocket<Number>
Auch durch eine alternative Schreibweise lässt sich der Compiler nicht in die Irre führen:
Pocket<Integer> p = new Pocket<Integer>();
Pocket<Number> p2 = p; //Type mismatch: cannot convert
// from Pocket<Integer> to Pocket<Number>
Hinweis |
Im Fall von immutable Objekten mit Nur-lese-Zugriff bestünde eigentlich kein Grund für Kovarianz. Nehmen wir an, die folgende Deklaration wäre korrekt: Pocket<Number> p = new Pocket<Integer>( 1 ); |
9.5.3 Wildcards mit ?

Wir wollen eine Methode isOnePocketEmpty() schreiben, die eine variable Anzahl von beliebigen Tascheninhalten bekommt und testet, ob eine davon leer ist. Ein Aufruf könnte so aussehen:
Pocket<String> p1 = new Pocket<String>( "Bad-Bank" );
Pocket<Integer> p2 = new Pocket<Integer>( 1500000 );
System.out.println( isOnePocketEmpty( p1, p2 ) ); // false
Die erste Idee für den Methodenkopf sieht so aus:
public static boolean isOnePocketEmpty( Pocket<Object>... pockets )
Doch halt! Da Pocket<Object> nicht Taschen mit allen Typen umfasst, sondern nur exakt eine Tasche trifft, die ein Object-Objekt enthält, ist das keine sinnvolle Parametrisierung für isOnePocketEmpty(). Das hatten wir im oberen Abschnitt schon festgestellt. Denn wäre das möglich, würde es die Typsicherheit gefährden. Denn wenn diese Methode tatsächlich Taschen mit allen Inhalten akzeptieren würde, so könnte einer Tasche leicht ein Wert mit falschem Typ untergeschoben werden. Denn wird wie in unserem Beispiel die Methode isOnePocketEmpty() mit einem Pocket<String> aufgerufen, so würde wegen isOnePocketEmpty(Pocket<Object>... pockets) dann auch der Aufruf von set(12) auf dem Pocket gültig sein, und dann stände plötzlich statt des gewünschten Inhalts vom Taschentyp String nun ein Integer in der Tasche. Das darf nicht gültig sein!
Ist der Typ egal, könnten wir an den Original-Typ (Raw-Type) denken. Doch die Raw-Types haben den Nachteil, dass bei ihnen der Compiler überhaupt nichts prüft, wir aber eine gewisse Prüfung möchten. So soll die Methode isOnePocketEmpty() beliebige Taschen entgegennehmen, aber gleichzeitig soll es der Methode auch verboten sein, falsche Dinge in die Taschen zu setzen. Ein isOnePocketEmpty(Pocket... pockets) ist also keine gute Idee und führt außerdem zu diversen Warnungen.
Die Lösung besteht im Einsatz des Wildcard-Typs ?. Er repräsentiert dann eine Familie von Typen. Wenn schon Pocket<Object> nicht der Basistyp aller Tascheninhalte ist, dann ist es Pocket<?>. Es ist wichtig zu verstehen, dass ? nicht für Object steht, sondern für einen unbekannten Typ! Damit lässt sich isOnePocketEmpty() realisieren:
Listing 9.15: com/tutego/insel/generic/PocketsEmpty.java
public static boolean isOnePocketEmpty( Pocket<?>... pockets )
{
for ( Pocket<?> pocket : pockets )
if ( pocket.isEmpty() )
return true;
return false;
}
public static void main( String[] args )
{
Pocket<String> p1 = new Pocket<String>( "Bad-Bank" );
Pocket<Integer> p2 = new Pocket<Integer>( 1500000 );
System.out.println( isOnePocketEmpty( p1, p2 ) ); // false
System.out.println( isOnePocketEmpty( p1, p2, new Pocket<Byte>() ) ); // true
}
Dass der Aufruf von isOnePocketEmpty(), also bei keiner übergebenen Tasche, zu false führt, soll an dieser Stelle als gegeben gelten.
Wir müssen Wildcards von Typvariablen gedanklich streng trennen. Instanziierungen mit Wildcards sind nicht erlaubt, da ein Wildcard ja eben nicht für einen konkreten Typ, sondern für eine ganze Reihe von möglichen Typen steht. Wildcards können auch nicht wie Typvariablen in Methoden genutzt werden, auch wenn der Typ beliebig ist.
Korrekt mit Typvariable | Falsch mit Wildcard (Compilerfehler!) |
Pocket<?> pocket = new Pocket<Byte>(); | Pocket<?> pocket = new Pocket<?>(); |
static <T> T random( T m, T n ) { ... } | static ? random( ? m, ? n ) { ... } |
Auswirkungen auf Lese-/Schreiboperationen
Ist der Wildcard-Typ bei Pocket<?> im Einsatz, wissen wir nichts über den Typ, und dem Compiler gehen alle Informationen verloren. Deklarieren wir etwa
Pocket<?> p1 = new Pocket<Integer>();
oder
Pocket<Integer> p2 = new Pocket<Integer>();
Pocket<?> p3 = p2;
dann ist über die wirklichen Typparameter bei p1 und p3 nichts bekannt. Das hat wichtige Auswirkungen auf die Methoden, die wir auf Pocket aufrufen können.
- Ein Aufruf von p1.get() ist legal, denn alles, was die Methode liefern wird, ist immer ein Object, auch wenn es null ist. Die Anweisung Object v = p1.get(); ist dementsprechend korrekt.
- Ein p1.set(value) kann nicht erlaubt sein, da über den Typ von value nichts bekannt ist. In p1 dürfen wir kein Double einsetzen, da Pocket nur Integer speichern soll. Die einzige Ausnahme ist null, da null jeden Typ hat. p1.set(null) ist also eine zulässige Anweisung. Das heißt ebenso, dass mit <?> aufgebaute Objekte nicht automatisch immutable sind.
9.5.4 Bounded Wildcards

Die Angabe des konkreten Typparameters wie Pocket<Integer> und die Wildcard-Form Pocket<?> bilden Extreme. Die Tasche Pocket<Integer> nimmt nur Ganzzahlen auf, Pocket<?> auf der anderen Seiten alles. Es muss aber auch etwas dazwischen geben, um zum Beispiel auszudrücken, dass die Tasche nur eine Zahl oder eine Zeichenkette enthalten soll.
Daher sind Typ-Einschränkungen mit extends und super möglich. Damit ergeben sich drei Arten von Wildcards:
Die Wildcard beschreibt also die Eigenschaft eines Typparameters. Wenn es
Pocket<? extends Number> p;
heißt, dann können in der Tasche p alle möglichen Number-Objekte sein.
Machen wir extends und super noch an einem anderem Beispiel deutlich, das zeigt, welche Familie von Typen die Syntax beschreibt:
? extends CharSequence | ? super String |
CharSequence | String |
String | CharSequence |
StringBuffer | Object |
StringBuilder | |
... |
Die erste Tabellenzeile (nach dem Tabellenkopf) macht deutlich, dass extends und super den angegebenen Typ selbst mit einschließen. In <? extends CharSequence> ist CharSequence genau der Upper-Bound der Wildcard, und in <? super String> ist String der Lower-Bound der Wildcard. Während die Anzahl der Typen beim Lower-Bound beschränkt ist (die Anzahl der Oberklassen kann sich nicht erweitern), ist die Anzahl der Typen mit Upper-Bound im Prinzip unbekannt, da es immer wieder neue Unterklassen geben kann.
Einsatzgebiete
Jeder der drei Wildcard-Typen hat seine Einsatzgebiete. Weitere Anwendungen der Upper-bound Wildcard und der Lower-bound Wildcard zeigen die Sortiermethoden der Datenstrukturen und Algorithmen.
Beispiel | Bedeutung |
Pocket<?> p; | Taschen mit beliebigem Inhalt |
Pocket<? extends Number> p; | Taschen nur mit Zahlen |
Comparator<? super String> comp = String.CASE_INSENSITIVE_ORDER; |
Entweder String-, Object- oder CharSequence-Comparator. Idee: Ein Comparator, der allgemeine Object-Objekte vergleichen kann, kann (irgendwie) auch Strings vergleichen, denn durch die Vererbung ist ein String eine Art von Object. |
Beispiel mit Upper-bound-Wildcard-Typ
Die Upper-bound Wildcard ist häufiger zu finden als die Lower-bound-Variante. Daher wollen wir ein Beispiel aufführen, an dem gut der übliche Einsatz für den Upper-bound abzulesen ist. Unser Player hatte eine rechte und eine linke Tasche. Die Taschen sollen aber nun nicht alles Mögliche speichern können, sondern nur besondere Spielobjekte vom Typ Portable (engl. für tragbar). Portable ist eine Schnittstelle, die ein Gewicht für die tragbaren Objekte vorschreibt. Zwei Typen sollen tragbar sein: Pen und Cup. Die Implementierung sieht so aus:
Listing 9.16: com/tutego/insel/generic/PortableDemo.java, Ausschnitt
interface Portable
{
double getWeight();
void setWeight( double weight );
}
abstract class AbstractPortable implements Portable
{
double weight;
@Override public double getWeight() { return weight; }
@Override public void setWeight( double weight ) { this.weight = weight; }
@Override public String toString() { return getClass().getName() +
"[weight=" + weight + "]"; };
}
class Pen extends AbstractPortable { }
class Cup extends AbstractPortable { }
Um zu testen, ob der Spieler nicht zu viele Sachen trägt, soll eine Methode areLighterThan() testen, ob das Gewicht einer Liste von tragbaren Dingen unter einer gegebenen Grenze bleibt. Der erste Versuch könnte so aussehen:
boolean areLighterThan( List<Portable> collection, double maxWeight )
Moment! Das würde wieder ausschließlich Portable-Objekte akzeptieren, denn Kovarianz gilt ja nicht. Selbst wenn es gehen würde, könnte das bedeuten, dass in einer Methode dann vielleicht über collection.add() ein Pen hinzugefügt werden kann, auch wenn die übergebene Liste mit Cup deklariert wurde. Dann stände in der Liste plötzlich etwas Falsches. Außerdem ist Portable eine Schnittstelle, sodass die Methode wirklich überhaupt keinen Sinn ergibt. So ist die korrekte Schreibweise nur mit einem Upper-bound-Wildcard-Typ möglich:
boolean areLighterThan( List<? extends Portable> list, double maxWeight )
Somit nimmt die Methode nur Listen mit Portable-Objekte an, und das ist auch nötig, denn Portable-Objekte haben ein Gewicht, und diese Eigenschaft brauchen wir.
Listing 9.17: com/tutego/insel/generic/PortableDemo.java, Ausschnitt
class PortableUtils
{
public static boolean areLighterThan( List<? extends Portable> list, double maxWeight )
{
double accumulatedWeight = 0.0;
for ( Portable portable : list )
accumulatedWeight += portable.getWeight();
return accumulatedWeight < maxWeight;
}
}
public class PortableDemo
{
public static void main( String[] args )
{
Pen pen = new Pen();
pen.setWeight( 10 );
Cup cup = new Cup();
cup.setWeight( 100 );
System.out.println( PortableUtils.areLighterThan( Arrays.asList( pen, cup ),
10 ) ); //false
System.out.println( PortableUtils.areLighterThan( Arrays.asList( pen, cup ),
120 ) ); //true
}
}
Wie schon besprochen wurde, kann aus der mit den Upper-bound-Wildcards deklarierten Datenstruktur List<? extends Portable> nur gelesen, aber nicht verändert werden.
9.5.5 Bounded-Wildcard-Typen und Bounded-Typvariablen

Zwischen Bounded-Wildcard-Typen und Bounded-Typvariablen gibt es natürlich einen Zusammenhang, und bei der Deklaration sind zwei Varianten wählbar. Warum das so ist, kann unsere Methode areLighterThan() demonstrieren. Statt
boolean areLighterThan( List<? extends Portable> list, double maxWeight )
hätten wir auch einen formalen Typparameter lokal für die Methode deklarieren können:
<T extends Portable> boolean areLighterThan( List<T> list, double maxWeight )
Beide Varianten erfüllen den gleichen Zweck. Doch ist die erste Variante der zweiten vorzuziehen.
Best Practice |
Immer dann, wenn der formale Typparameter (etwa T) nur in der Signatur auftaucht und es in der Methode selbst keinen Rückgriff auf den Typ T gibt, wähle die Variante mit der Wildcard. |
Mit Typparametern lassen sich gut Abhängigkeiten zwischen den einzelnen Argumenten oder dem Rückgabetyp herstellen. Das zeigt das folgende Beispiel (mit einigen Methoden, die bisher noch nicht vorgestellt wurden), das das leichteste Objekt in einer Sammlung von Taschen zurückgeben soll:
Listing 9.18: com/tutego/insel/generic/PortableDemo.java, PortableUtils
public static <T extends Portable> T lightest( Collection<T> collection )
{
Iterator<T> iterator = collection.iterator();
T lightest = iterator.next();
while ( iterator.hasNext() )
{
T next = iterator.next();
if ( next.getWeight() < lightest.getWeight() )
lightest = next;
}
return lightest;
}
Der Compiler achtet darauf, dass der Typ der Rückgabe mit dem Typ der Sammlung übereinstimmt.
Auf Bounded-Wildcard-Typen in Rückgaben verzichten
Wenn es möglich ist, Bounded-Wildcard-Typen oder Bounded-Typvariablen zu nutzen, sind Bounded-Typvariablen immer vorzuziehen. Wildcard-Typen liefern keine Typinformation, und es ist immer besser, sich vom Compiler über die Typ-Inferenz einen genaueren Typ geben zu lassen.
Nehmen wir eine statische Methode leftSublist() an, die von einer Liste eine Unterliste zurückgibt. Die Unterliste geht von der ersten Position bis zur Hälfe.
Versuch 1:
public static List<?> leftSublist( List<? extends Portable> list )
{
return list.subList( 0, list.size() / 2 );
}
Der Rückgabetyp List<?> ist so ziemlich der schlechteste, den wir wählen können, denn der Aufrufer der Methode kann mit der Rückgabe überhaupt nichts anfangen: Er weiß nichts über den Inhalt der Liste.
Versuch 2:
public static List<? extends Portable> leftSublist( List<? extends Portable> list )
Das ist schon ein wenig besser, denn hier bekommt der Empfänger wenigstens die Information zurück, dass die Liste irgendwelche tragbaren Dinge enthält.
Noch besser ist natürlich, auf die Typ-Inferenz des Compilers zu setzen und dem Aufrufer genau den Typ wieder zurückzugeben, mit dem er den Parametertyp spickte. Dazu müssen wir aber eine Typvariable einsetzen. Der Grund ist: Deklariert eine Methode Parameter oder eine Rückgabe mit mehreren Wildcard-Typen, so sind die wirklichen Typargumente völlig frei wählbar und ohne Zusammenhang.
Listing 9.19: com/tutego/insel/generic/PortableDemo.java, PortableUtils
public static <T extends Portable> List<T> rightSublist( List<T> list )
{
return list.subList( 0, list.size() / 2 );
}
Nun ist der Typ der Liste, die reinkommt, gleich dem Typ der Liste, die rauskommt. Mit extends ist die Liste zwar nur lesbar, aber das liegt in der Natur der Sache.
Hinweis |
Insbesondere in der Klasse Collections aus der Java-Standard-API könnten viele Methoden auch anders geschrieben werden.
Ein Beispiel: Statt <T extends E> boolean addAll |
9.5.6 Das LESS-Prinzip

Während die mit extends eingeschränkten Familien Leseoperationen zulassen, gilt für super das Gegenteil. Hier ist Lesen nicht erlaubt, aber Schreiben. Als Merkhilfe lässt sich das als LESS-Prinzip[173](Im Englischen ist auch der Ausdruck PECS (producer-extends, consumer-super) im Umlauf.) festhalten:
Lesen = Extends, Schreiben = Super (LESS)
Ein Beispiel ist auch hier hilfreich. Eine statische Methode copyLighterThan() soll nur die Elemente aus einer Liste in eine andere kopieren, die leichter als eine bestimmte Obergrenze sind. Der erste Versuch:
public static void copyLighterThan( List<? extends Portable> src,
List<? extends Portable> dest, double maxWeight )
{
for ( Portable portable : src )
if ( portable.getWeight() < maxWeight )
dest.add( portable ); //Compilerfehler !!
}
Auf den ersten Blick sieht es gut aus, aber das Programm lässt sich nicht übersetzen. Das Problem ist die Zeile dest.add(portable);. Wir erinnern uns: Mit einer Upper-bound-Wildcard lässt sich nicht schreiben. Das ergibt Sinn, denn die Liste src kann ja zum Beispiel eine Liste von Cup-Objekten sein und dest eine Liste von Pen-Objekten. Beide sind Portable, aber dennoch inkompatibel, da Cups nicht in Pens kopiert werden können. Die Frage ist also, wie der Typ der Ergebnisliste aussehen soll. Beginnen wir bei der Quellliste. Hier ist List<? extends Portable> schon korrekt, denn die Liste kann ja alles enthalten, was tragbar ist. Doch welche Anforderungen gibt es an die Zielliste? Wie muss der Typ sein, sodass sich alles vom Typ Portable, wie Cup oder Pen, oder sogar noch Unterklassen speichern lassen? Die Antwort ist einfach: Jeder Typ, der über Portable liegt! Das sind Portable selbst und Object, also alle Obertypen. Dies ist aber der Lower-bound-Wildcard-Typ, den wir mit super schreiben. Damit folgt:
Listing 9.20: com/tutego/insel/generic/PortableDemo.java, PortableUtils
public static void copyLighterThan( List<? extends Portable> src,
List<? super Portable> dest, double maxWeight )
{
for ( Portable portable : src )
if ( portable.getWeight() < maxWeight )
dest.add( portable );
}
Ein Beispiel für den Aufruf:
Listing 9.21: com/tutego/insel/generic/PortableDemo.java, Ausschnitt main()
List<? extends Portable> src = Arrays.asList( pen, cup );
List<? super Portable> dest = new ArrayList<Object>();
PortableUtils.copyLighterThan( src, dest, 20 );
System.out.println( dest.size() ); // 1
Object result = dest.get( 0 );
System.out.println( result ); // com.tutego.insel.generic.Pen[weight=10.0]
Die Liste dest ist schreibbar, aber der lesbare Typ ist lediglich Object – der Compiler weiß nicht, was hier tatsächlich in der Liste steckt, er weiß nur, dass es beliebige Obertypen von Portable sein können. Und da bleibt als allgemeinster Typ eben nur Object.
Wildcard-Capture
Das LESS-Prinzip hat eine wichtige Konsequenz, die insbesondere bei Listen-Operationen auffällt. Eine mit einer Wildcard parametrisierte Liste kann nicht verändert werden. Doch wie lässt sich zum Beispiel eine Methode schreiben, die eine Liste umdreht? Vom API-Design her könnte eine Methode reverse() wie folgt aussehen:
public static void reverse( List<?> list );
oder so:
public static void <T> reverse( List<T> list );
Nach unserem Verständnis, dass wir bei völlig freien Typen die Wildcard-Schreibweise bevorzugen wollen, stehen wir vor einem Dilemma.
public static <T> void reverse( List<?> list )
{
for ( int i = 0; i < list.size() / 2; i++ )
{
int j = list.size() – i – 1;
? tmp = list.get( i ); //Compilerfehler
list.set( i, list.get( j ) );
list.set( j, tmp );
}
}
Es bleibt uns nichts anderes, als doch die Variante mit der Typvariablen zu wählen, sodass wir Zugriff auf den Typ T haben.
Da nun vom API-Design reverse(List<?> list) bevorzugt wird, aber reverse(List<T> list) in der Implementierung nötig ist, stellt sich die Frage, ob beides miteinander vereinbar ist. Die gute Nachricht: Ja, denn reverse(List<?> list) kann auf eine Umdrehmethode revese_(List<T>) weiterleiten. Zwar müssen die Methoden anders benannt werden, aber wegen des sogenannten Wildcard-Capture funktioniert die Abbildung von einer Wildcard auf eine Typvariable.
Listing 9.22: com/tutego/insel/generic/WildcardCapture, WildcardCapture
public class WildcardCapture
{
private static <T> void reverse_( List<T> list )
{
for ( int i = 0; i < list.size() / 2; i++ )
{
int j = list.size() – i – 1;
T tmp = list.get( i );
list.set( i, list.get( j ) );
list.set( j, tmp );
}
}
public static void reverse( List<?> list )
{
reverse_( list );
}
}
Der Compiler »fängt« bei reverse(list) den unbekannten Typ der Liste ein und »füllt« die Typvariable bei reverse_(list).
9.5.7 Enum<E extends Enum<E>> *
Die generische Deklaration der Klasse Enum besitzt eine Besonderheit, die wir uns kurz vornehmen wollen:
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable
Ein konkreterer parametrisierter Typ muss also die Typvariable E so wählen, dass sie einen Untertyp von Enum beschreibt.
Das Ganze lässt sich am besten an einem Beispiel erklären. Die Klasse Enum ist eine besondere Klasse, die der Compiler immer dann verwendet, wenn er eine enum-Aufzählung umsetzen soll. Angenommen, Page deklariert zwei Seitengrößen:
public enum Page
{
A4, A3
}
Ohne zu genau auf die Methodenrümpfe zu schauen, generiert der Compiler folgenden Programmcode:
public final class Page extends java.lang.Enum<Page>
{
public static final Page A4 = ...
public static final Page A3 = ...
public static Page[] values() { ... }
public static Page valueOf(String s) { ... }
...
}
Aus einer enum-Aufzählung entsteht also eine Klasse, die Enum erweitert und als parametrisierter Typ genau diese Klasse nennt: Page extends Enum<Page>. Vergleichen wir das mit der generischen Typdeklaration Enum<E extends Enum<E>>, so ist der Typparameter Page eine Instanziierung der Typvariable E. Und Page ist eine Unterklasse von Enum (Page extends Enum), genauso wie die Typvariable E das mit dem Typ-Parameter-Bound vorschreibt: E extends Enum.
Was wir bisher gesehen haben, zeigt, dass die Deklaration »passt«. Aber warum ist sie so gewählt? Die Typvariable E ist so deklariert, dass sie für Enum-Unterklassen steht, also für die konkrete Aufzählung selbst, wie es Page zeigt. Das ist wichtig für Vergleiche. Dazu schauen wir uns einen Ausschnitt aus der Deklaration der abstrakten Klasse Enum noch einmal an, und zwar genau die Teile, die etwas mit dem Typ E einfordern; das sind zwei Methoden:
Listing 9.23: java/lang/Enum.java, Ausschnitt
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable
{
public final int compareTo(E o) {
public final Class<E> getDeclaringClass() {
...
}
Bleiben wir bei der Vergleichsmethode: compareTo() ermöglicht es, dass wir zwei Aufzählungen vergleichen und zum Beispiel A4.compareTo(A3) schreiben können. Java erlaubt dabei nur, dass zwei Aufzählungen vom gleichen Typ verglichen werden können: Vergleiche der Art A4.compareTo(Thread.State.NEW) führen zu einem Compilerfehler. Damit sind wir der Lösung schon nah. Die Deklaration der compareTo()-Methode befindet sich in Enum und wird den Unterklassen vererbt – die Methode wird nicht vom Compiler magisch in die Unterklassen gesetzt, wie etwa values() oder valueOf(). Damit bei compareTo(E o) jetzt nur eine Unterklasse von Enum, nämlich die konkrete Aufzählung, erlaubt ist, fordert Enum eben E extends Enum<E>.
Die abschließende Frage ist, ob auch eine andere Deklaration für Enum möglich gewesen wäre, ohne dass es zu einem Nachteil kommen würde. Die Antwort ist: Ja, im Prinzip ist auch class Enum<E> möglich. Auf den ersten Blick scheint das aber falsch zu sein. Spielen wir diese Deklaration statt Enum<E extends Enum<E>> kurz durch. Dann könnte ein Entwickler schreiben: class Page extends Enum<Bunny> – die geerbte Vergleichsmethode von Page hieße dann compareTo(Bunny o), was falsch wäre. Mit der korrekten Deklaration Enum<E extends Enum<E>> ist nur ein class Page extends Enum<Page> möglich.
Jetzt kommt aber die große Einschränkung: Wir dürfen keine Unterklassen von Enum aufbauen, sondern nur der Compiler darf das tun. Ein eigenmächtiger Versuch wird vom Compiler abgestraft. Der unfehlbare Compiler könnte mit einer Deklaration class Enum<E> arbeiten, denn er würde für E den Aufzählungstyp einsetzen, also Programmcode für class Page extends Enum<Page> generieren. So stände in compareTo() der richtige Typ, denn E wäre mit Page instanziiert, was zu dem gewollten compareTo(Page o) führt. Und auch die in Enum deklarierte Methode getDeclaringClass() liefert Page. Einschränkungen der möglichen Typparameter helfen Entwicklern, Typfehler zu minimieren, aber der Compiler macht keine Fehler, für ihn ist die Präzisierung nicht nötig. Aber es gibt für die Java-API-Designer keinen Grund, Enum schwächer zu deklarieren als nötig. Außerdem gibt es noch einen Unterschied im Bytecode, der sich durch die Typ-Löschung ergibt. Bei Enum<E> ist die Umsetzung von E getDeclaringClass() im Bytecode nur Object getDeclaringClass(), doch mit Enum<E extends Enum<E>> ist sie immerhin Enum getDeclaringClass(), was besser ist.
Ihr Kommentar
Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.