Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Exceptions
7 Äußere.innere Klassen
8 Besondere Klassen der Java SE
9 Generics<T>
10 Architektur, Design und angewandte Objektorientierung
11 Die Klassenbibliothek
12 Einführung in die nebenläufige Programmierung
13 Einführung in Datenstrukturen und Algorithmen
14 Einführung in grafische Oberflächen
15 Einführung in Dateien und Datenströme
16 Einführung in die <XML>-Verarbeitung mit Java
17 Einführung ins Datenbankmanagement mit JDBC
18 Bits und Bytes und Mathematisches
19 Die Werkzeuge des JDK
A Die Klassenbibliothek
Stichwort

Download:
- Aufgaben, ca. 1,1 MB
- Programme, ca. 12,8 MB

Buch bestellen
Ihre Meinung?

Spacer
Java ist auch eine Insel von Christian Ullenboom
Das umfassende Handbuch
Buch: Java ist auch eine Insel

Java ist auch eine Insel
Galileo Computing
1308 S., 10., aktualisierte Auflage, geb., mit DVD
ca. 49,90 Euro, ISBN 978-3-8362-1802-3
Pfeil 9 Generics<T>
Pfeil 9.1 Einführung in Java Generics
Pfeil 9.1.1 Mensch versus Maschine: Typprüfung des Compilers und der Laufzeitumgebung
Pfeil 9.1.2 Taschen
Pfeil 9.1.3 Generische Typen deklarieren
Pfeil 9.1.4 Generics nutzen
Pfeil 9.1.5 Diamonds are forever
Pfeil 9.1.6 Generische Schnittstellen
Pfeil 9.1.7 Generische Methoden/Konstruktoren und Typ-Inferenz
Pfeil 9.2 Umsetzen der Generics, Typlöschung und Raw-Types
Pfeil 9.2.1 Realisierungsmöglichkeiten
Pfeil 9.2.2 Typlöschung (Type Erasure)
Pfeil 9.2.3 Probleme aus der Typlöschung
Pfeil 9.2.4 Raw-Type
Pfeil 9.3 Einschränken der Typen über Bounds
Pfeil 9.3.1 Einfache Einschränkungen mit extends
Pfeil 9.3.2 Weitere Obertypen mit &
Pfeil 9.4 Typparameter in der throws-Klausel *
Pfeil 9.4.1 Deklaration einer Klasse mit Typvariable <E extends Exception>
Pfeil 9.4.2 Parametrisierter Typ bei Typvariable <E extends Exception>
Pfeil 9.5 Generics und Vererbung, Invarianz
Pfeil 9.5.1 Arrays sind invariant
Pfeil 9.5.2 Generics sind kovariant
Pfeil 9.5.3 Wildcards mit ?
Pfeil 9.5.4 Bounded Wildcards
Pfeil 9.5.5 Bounded-Wildcard-Typen und Bounded-Typvariablen
Pfeil 9.5.6 Das LESS-Prinzip
Pfeil 9.5.7 Enum<E extends Enum<E>> *
Pfeil 9.6 Konsequenzen der Typlöschung: Typ-Token, Arrays und Brücken *
Pfeil 9.6.1 Typ-Token
Pfeil 9.6.2 Super-Type-Token
Pfeil 9.6.3 Generics und Arrays
Pfeil 9.6.4 Brückenmethoden

Rheinwerk Computing - Zum Seitenanfang

9.5 Generics und Vererbung, InvarianzZur nächsten Überschrift

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.


Rheinwerk Computing - Zum Seitenanfang

9.5.1 Arrays sind invariantZur nächsten ÜberschriftZur vorigen Überschrift

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?


Rheinwerk Computing - Zum Seitenanfang

9.5.2 Generics sind kovariantZur nächsten ÜberschriftZur vorigen Überschrift

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>();        // Fehler 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>(); // Fehler 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; // Fehler 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 );
Number n = p.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 kleineren Typ, also hier Number, daraus zu lesen. Jedoch kann Java nicht erkennen, ob ein Typ immutable ist, und kann daher auch solche Ausnahmen bei den Generics nicht machen. Der Compiler nimmt immer an, Zugriffe wären lesend und schreibend.


Rheinwerk Computing - Zum Seitenanfang

9.5.3 Wildcards mit ?Zur nächsten ÜberschriftZur vorigen Überschrift

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.

Tabelle 9.8: Möglicher und unmöglicher Einsatz von Wildcard

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.

Rheinwerk Computing - Zum Seitenanfang

9.5.4 Bounded WildcardsZur nächsten ÜberschriftZur vorigen Überschrift

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:

Tabelle 9.9: Die drei Wildcard-Typen

Wildcard Bezeichnung Typparameter
? Wildcard-Typ ist beliebig
? extends Typ Upper-bound Wildcard-Typ muss Typ erweitern
? super Typ Lower-bound Wildcard-Typ muss über Typ stehen

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:

Tabelle 9.10: Einige eingeschlossene Typen bei extends und super

? 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.

Tabelle 9.11: Beispiel für alle drei Wildcard-Typen

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.


Rheinwerk Computing - Zum Seitenanfang

9.5.5 Bounded-Wildcard-Typen und Bounded-TypvariablenZur nächsten ÜberschriftZur vorigen Überschrift

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> 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).


Rheinwerk Computing - Zum Seitenanfang

9.5.6 Das LESS-PrinzipZur nächsten ÜberschriftZur vorigen Überschrift

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 ); // Fehler 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<>();
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 <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 ); // Fehler 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 interne 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).


Rheinwerk Computing - Zum Seitenanfang

9.5.7 Enum<E extends Enum<E>> *Zur vorigen Überschrift

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.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Java ist auch eine Insel





Java ist auch eine Insel
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Java SE Bibliotheken






 Java SE Bibliotheken


Zum Katalog: Professionell entwickeln mit Java EE 7






 Professionell
 entwickeln mit
 Java EE 7


Zum Katalog: Einstieg in Eclipse






 Einstieg in
 Eclipse


Zum Katalog: Einstieg in Java






 Einstieg in
 Java


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2011
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de