11.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.
11.5.1 Arrays sind kovariant
Nehmen wir als Beispiel die Hierarchie der bekannten Wrapper-Klassen. Natürlich steht Object oben. Die numerischen Wrapper-Klassen erweitern alle die abstrakte Klasse Number. Darunter stehen dann etwa Integer, Double und die anderen numerischen Wrapper. Folgendes bereitet keine Kopfschmerzen:
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 und Object[] über allen nichtprimitiven Felder liegt, nennt sich Kovarianz. Doch lässt sich das auf Generics übertragen?
11.5.2 Generics sind nicht kovariant, sondern invariant
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 kovariant, sie sind invariant. Diese Eigenschaft ist auf den ersten Blick nicht intuitiv, doch ein Beispiel rückt diesen Eindruck schnell gerade. Bleiben wir bei unserem Beispiel Rocket und den Wrapper-Klassen. Auch wenn Number die Oberklasse von Integer ist, so gilt dennoch nicht, dass Rocket<Number> ein Obertyp von Rocket<Integer> ist. Wäre es das, wäre Folgendes möglich und zur Laufzeit ein Problem:
Rocket<Number> r;
r = new Rocket<Integer>(); // Ist das OK?
r.set( 2.2 );
Das Argument 2.2 ist über Autoboxing ein Double, und daher scheint es auf Number zu passen. Allerdings sollte Double gar nicht erlaubt sein, da wir mit Rocket<Integer> ja eine Rakete für Integer aufgebaut haben, und ein Double darf nicht in die Integer-Rakete. Daher folgt: Die Ableitungsbeziehung zwischen Typen überträgt sich nicht auf generische Klassen. Ein Rocket<Number> ist also keine Oberklasse, die alle erdenklichen numerischen Typen in der Rakete erlaubt. Der Compiler meckert bei diesem Versuch sofort:
Rocket<Number> r;
r = new Rocket<Integer>(); // Type mismatch: cannot convert from Rocket<Integer>
// to Rocket<Number>
Auch durch eine alternative Schreibweise lässt sich der Compiler nicht in die Irre führen:
Rocket<Integer> r1 = new Rocket<>();
Rocket<Number> r2 = r1; // Type mismatch: cannot convert
// from Rocket<Integer> to Rocket<Number>
[»] Hinweis
Im Fall von immutablen Objekten mit Nur-Lese-Zugriff bestünde eigentlich kein Grund für Kovarianz. Nehmen wir an, die folgende Deklaration wäre korrekt:
Rocket<Number> r = new Rocket<Integer>( 1 );
Number n = r.get();
Dann haben wir gezeigt, dass p.set(2.2) zum Beispiel nicht in Ordnung ist, da Double nicht mit Integer kompatibel ist. Aber wenn das Objekt etwa über den Konstruktor initialisiert würde, spräche nichts dagegen, mit einem Basistyp, also hier Number, daraus zu lesen. Jedoch kann Java nicht erkennen, ob ein Typ immutable ist, und kann daher auch bei den Generics solche Ausnahmen nicht machen. Der Compiler nimmt immer an, Zugriffe wären lesend und schreibend.
11.5.3 Wildcards mit ?
Wir wollen eine Methode isOneRocketEmpty(…) schreiben, die eine variable Anzahl von Raketen bekommt – und dabei soll es egal sein, was die Rakete transportiert. Die Methode soll auch testen, ob eine Rakete leer ist. Ein Aufruf könnte so aussehen:
Rocket<String> r1 = new Rocket<>( "Bad-Bank" );
Rocket<Integer> r2 = new Rocket<>( 1500000 );
System.out.println( isOneRocketEmpty( r1, r2 ) ); // false
Die erste Idee für den Methodenkopf sieht so aus:
public static boolean isOneRocketEmpty( Rocket<Object>... rockets )
Doch halt! Da Rocket<Object> nicht Raketen mit allen Typen umfasst, sondern nur exakt eine Rakete trifft, die ein Object-Objekt enthält, ist das keine sinnvolle Parametrisierung für isOneRocketEmpty(…). Das hatten wir im oberen Abschnitt schon festgestellt. Denn wäre das möglich, würde es die Typsicherheit gefährden. Wenn diese Methode tatsächlich Raketen mit allen Inhalten akzeptieren würde, so könnte einer Rakete leicht ein Wert mit falschem Typ untergeschoben werden. Wird isOneRocketEmpty(…) mit einem Rocket<String> aufgerufen, so würde wegen isOneRocketEmpty(Rocket<Object>... rockets) auch der Aufruf von set(12) auf der Rocket gültig sein, und dann stünde plötzlich statt des gewünschten Inhalts der Rakete String nun ein Integer in der Rakete. Das darf nicht gültig sein!
Ist der Typ egal, könnten wir an den Originaltyp (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 isOneRocketEmpty(…) beliebige Raketeninhalte entgegennehmen, aber gleichzeitig soll es der Methode auch verboten sein, falsche Dinge in die Rakete zu setzen. Ein isOneRocketEmpty(Rocket... rockets) 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 Rocket<Object> nicht der Basistyp aller Raketeninhalte ist, dann ist es Rocket<?>. Es ist wichtig zu verstehen, dass ? nicht für Object steht, sondern für einen unbekannten Typ! Damit lässt sich isOneRocketEmpty(…) realisieren:
public static boolean isOneRocketEmpty( Rocket<?>... rockets ) {
for ( Rocket<?> rocket : rockets )
if ( rocket.isEmpty() )
return true;
return false;
}
public static void main( String[] args ) {
Rocket<String> r1 = new Rocket<>( "Bad-Bank" );
Rocket<Integer> r2 = new Rocket<>( 1500000 );
System.out.println( isOneRocketEmpty( r1, r2 ) ); // false
System.out.println( isOneRocketEmpty( r1, r2, new Rocket<Byte>() ) ); // true
}
Dass der Aufruf von isOneRocketEmpty() bei keiner übergebenen Rakete 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 eine 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!) |
---|---|
Rocket<?> r = new Rocket<Byte>(); | Rocket<?> r = new Rocket<?>(); |
static <T> T random( T m, T n ) { … } | static <?> ? random( ? m, ? n ) { … } |
Auswirkungen auf Lese-/Schreiboperationen
Ist der Wildcard-Typ bei Rocket<?> im Einsatz, wissen wir nichts über den Typ, und dem Compiler gehen alle Informationen verloren. Deklarieren wir etwa
Rocket<?> r1 = new Rocket<Integer>();
oder
Rocket<Integer> r2 = new Rocket<Integer>();
Rocket<?> r3 = r2;
dann ist über die wirklichen Typargumente bei r1 und r3 nichts bekannt. Das hat wichtige Auswirkungen auf die Methoden, die wir auf Rocket aufrufen können:
Ein Aufruf von r1.get() ist legal, denn alles, was die Methode liefern wird, ist immer ein Object, auch wenn es null ist. Die Anweisung Object v = r1.get(); ist dementsprechend korrekt.
Ein r1.set(value) ist nicht erlaubt, da dem Compiler die Typargumente von r1 fehlen und er keine Typen prüfen kann. In r1 dürfen wir kein Double einsetzen, da Rocket nur Integer speichern soll. Die einzige Ausnahme ist null, da null jeden Typ hat. r1.set(null) ist also eine zulässige Anweisung. Das heißt ebenso, dass mit <?> aufgebaute Objekte nicht automatisch immutable sind.
11.5.4 Bounded Wildcards
Die Angabe des Typarguments wie bei Rocket<Integer> und die Wildcard-Form Rocket<?> bilden Extreme. Die Rakete Rocket<Integer> nimmt nur Ganzzahlen auf, Rocket <?> auf der anderen Seite alles. Es muss aber auch etwas dazwischen geben, um zum Beispiel auszudrücken, dass die Rakete nur eine Zahl oder eine Zeichenkette enthalten soll.
Daher sind Typeinschränkungen mit extends und super möglich. Damit ergeben sich drei Arten von Wildcards:
Wildcard | Bezeichnung | Typargument |
---|---|---|
? | Wildcard-Typ | Ist beliebig. |
? extends Typ | alles, was Typ erweitert, also Untertypen, und Typ selbst | |
? super Typ | alle Obertypen von Typ und Typ selbst |
Eine Wildcard beschreibt also die Eigenschaft eines Typarguments. Wenn es
Rocket<? extends Number> r;
heißt, dann können in der Rakete p alle möglichen Number-Objekte sein. Machen wir extends und super noch an einem anderen 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-bounded Wildcard und der Lower-bounded Wildcard finden sich in den Sortiermethoden der Datenstrukturen und Algorithmen:
Beispiel | Bedeutung |
---|---|
Rocket<?> p; | Raketen mit beliebigem Inhalt |
Rocket<? extends Number> p; | Raketen nur mit Zahlen, also Unterklassen von Number, wie Integer, Double, BigDecimal … |
Comparator, der Objekte vom Typ String, Object oder CharSequence vergleicht, also Obertypen von String. Idee: Ein Comparator, der zum Beispiel CharSequence-Objekte vergleichen kann, kann auch Strings vergleichen, denn durch die Vererbung ist ein String eine Art von CharSequence. Alle Comparator-Typen <? super String> können (irgendwie) Strings vergleichen. |
Beispiel mit Upper-bounded-Wildcard-Typ
Die Upper-bounded Wildcard ist häufiger zu finden als die Lower-bounded-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 Rakete. Die Raketen 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:
interface Portable {
double getWeight();
void setWeight( double weight );
}
abstract class AbstractPortable implements Portable {
private 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(…) prüfen, 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 areLighterThan(…)-Methode mit einem Parametertyp List<Portable> überhaupt keinen Sinn ergibt. Eine vernünftige Schreibweise ist nur mit einem Upper-bounded-Wildcard-Typ möglich:
boolean areLighterThan( List<? extends Portable> list, double maxWeight )
Somit nimmt die Methode nur Listen mit Portable-Objekten an, und das ist auch nötig, denn Portable-Objekte haben ein Gewicht, und diese Eigenschaft brauchen wir.
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-bounded Wildcards deklarierten Datenstruktur List<? extends Portable> nur gelesen werden. Sie kann aber nicht verändert werden.
11.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 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 Typparameter (etwa T) nur in der Signatur auftaucht (die Signatur ergibt sich aus dem Methodennamen, der Parameterliste und den Ausnahmen) und es in der Methode selbst keinen Rückgriff auf den Typ T gibt, sollte man die Variante mit der Wildcard wählen.
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). Es soll das leichteste Objekt in einer Sammlung von Raketen zurückgeben:
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 – es sei denn, es greift die Best Practice. 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älfte.
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.
public static <T extends Portable> List<T> leftSublist( 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(Collection<T> c) wählten die Autoren boolean addAll(Collection<? extends E> c).
11.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[ 216 ](Im Englischen ist auch der Ausdruck PECS (producer – extends, consumer – super) in 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 Anweisung dest.add(portable). Wir erinnern uns: Mit einer Upper-bounded 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: Es kann jeder Typ sein, der über Portable liegt! Das sind Portable selbst und Object, also alle Obertypen. Dies ist aber der Lower-bounded-Wildcard-Typ, den wir mit super schreiben. Damit folgt:
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:
List<? extends Portable> src = Arrays.asList( pen, cup );
List<? super Portable> dest = new ArrayList<>();
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 Listenoperationen 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 <T> void 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 übrig, 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, mit einem Trick, denn reverse(List<?> list) kann auf eine interne Umdrehmethode reverse_(List<T>) weiterleiten. Zwar müssen die Methoden anders benannt werden, aber wegen des sogenannten Wildcard-Captures funktioniert die Abbildung von einer Wildcard auf eine Typvariable.
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).
11.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 dass wir genau auf die Methodenrümpfe 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 einem Aufzählungstyp entsteht also eine Klasse, die Enum erweitert und als parametrisierten Typ genau diese Klasse nennt: Page extends Enum<Page>. Vergleichen wir das mit der generischen Typdeklaration Enum<E extends Enum<E>>, so ist das Typargument Page eine Instanziierung der Typvariablen E. Und Page ist eine Unterklasse von Enum (Page extends Enum), genauso wie die Typvariable E das mit dem Typparameter-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:
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, zwei Aufzählungen zu vergleichen und zum Beispiel A4.compareTo(A3) zu schreiben. Java erlaubt dabei nur, zwei Aufzählungen vom gleichen Typ zu vergleichen: 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(String). 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 einen Unterschied im Bytecode, der sich durch die Typlö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.