Fortgeschrittene Programmierung mit Java 5 - Nowak - Beck-Shop

Werbung
Fortgeschrittene Programmierung mit Java 5
Generics, Annotations, Concurrency und Reflection - inklusive Neuerungen der J2SE 5.0
von
Johannes Nowak
1. Auflage
Fortgeschrittene Programmierung mit Java 5 – Nowak
schnell und portofrei erhältlich bei beck-shop.de DIE FACHBUCHHANDLUNG
dpunkt.verlag 2004
Verlag C.H. Beck im Internet:
www.beck.de
ISBN 978 3 89864 306 1
Inhaltsverzeichnis: Fortgeschrittene Programmierung mit Java 5 – Nowak
21
3
Generics – eine Einführung
Das folgende Kapitel erläutert das neue Generics-Konzept anhand eines
größeren »intuitiven« Beispiels. Es geht hier noch nicht um die genauen
theoretischen Einzelheiten, sondern einfach nur darum, ein »Gefühl« für
die Verwendung von Generics zu entwickeln. Es wird in diesem Kapitel
bewusst darauf verzichtet, »fertige« Klassen der Klassenbibliothek zu
benutzen. Sofern generische Container-Klassen notwendig sind, werden sie
selbst geschrieben. In dem Beispiel geht es um Getränke, Getränkeflaschen
und Getränkeflaschenkästen.
3.1
Probleme mit Bier- und Weinflaschen
Es gibt vielerlei Getränke: Bier, Rotwein, Weißwein etc. Diesen Sachverhalt
könnte man etwa in folgender Klassenhierarchie repräsentieren:
abstract class Drink {
}
class Beer extends Drink {
private final String brewery;
public Beer (String brewery)
public String getBrewery ()
public String toString ()
}
abstract class Wine extends Drink {
private final String region;
public Wine (String region)
public String getRegion ()
public String toString ()
}
class WhiteWine extends Wine {
public WhiteWine (String region)
}
class RedWine extends Wine {
public RedWine (String region)
}
{ this.brewery = brewery; }
{ return this.brewery; }
{ ... }
{ this.region = region; }
{ return this.region; }
{ ... }
{ super (region); }
{ super (region); }
22
3 Generics – eine Einführung
Hier das Klassendiagramm:
Drink
Wine
Beer
WhiteWine
RedWine
Es gibt natürlich auch entsprechende Flaschen: Bierflaschen, Rotweinflaschen,
Weißweinflaschen etc. Flaschen kann man füllen und leeren. Auch hier könnte
man eine Klassenhierarchie aufbauen:
abstract class DrinkBottle {
}
class BeerBottle extends DrinkBottle {
private Beer content;
public void fill (Beer content) {
this.content = content;
}
public Beer empty () {
Beer content = this.content;
this.content = null;
return content;
}
}
abstract class WineBottle extends DrinkBottle {
}
class WhiteWineBottle extends WineBottle {
private WhiteWine content;
public void fill (WhiteWine content) {
this.content = content;
}
public WhiteWine empty () {
WhiteWine content = this.content;
this.content = null;
return content;
}
}
3.1 Probleme mit Bier- und Weinflaschen
23
class RedWineBottle extends WineBottle {
private RedWine content;
public void fill (RedWine content) {
this.content = content;
}
public RedWine empty () {
RedWine content = this.content;
this.content = null;
return content;
}
}
Hier das Klassendiagramm:
DrinkBottle
BeerBottle
WineBottle
WhiteWineBottle
RedWineBottle
Zwar mag es hier im ersten Moment den Anschein haben, als seien fill und
empty Methoden, die in den abgeleiteten Klassen überschrieben würden – dies ist
jedoch nicht der Fall. Die Parametertypen von fill und die Return-Typen von
empty sind nämlich jeweils verschieden:
Die Klasse BeerBottle z.B. enthält folgende Methoden:
public void fill (Beer content)
public Beer empty ()
Die Klasse RedWineBottle aber folgende Methoden:
public void fill (RedWine content)
public RedWine empty ()
In jeder instantiierbaren Bottle-Klasse müssen jeweils drei Elemente definiert
werden: das content-Attribut, die fill-Methode und die empty-Methode – vorausgesetzt, Typsicherheit soll gewährleistet werden. Vorausgesetzt also, dass eine
Bierflasche nur mit Bier und eine Weißweinflasche nur mit Weißwein zu füllen
sein soll; und weiterhin vorausgesetzt, dass man sicher sein möchte, aus einer
Bierflasche Bier zu trinken und aus einer Weißweinflasche Weißwein.
Es wäre schöner, wenn man statt einer ganzen Batterie von Flaschenklassen
nur eine einzige Flaschenklasse zu definieren brauchte – denn die eigentliche
24
3 Generics – eine Einführung
Funktionalität des Füllens und Leerens ist ja jeweils dieselbe. Der Unterschied der
verschiedenen Flaschentypen besteht also nur in der Art des Inhalts – und demnach auch in der Art der Parametertypen und der Return-Typen.
Man könnte also eine Alternative ausprobieren:
class Bottle {
private Drink content;
public boolean isEmpty () {
return this.content == null;
}
public void fill (Drink content) {
this.content = content;
}
public Drink empty () {
Drink content = this.content;
this.content = null;
return content;
}
}
Dann allerdings wäre man nicht sicher, dass eine Flasche, die für Bier vorgesehen
ist, tatsächlich auch Bier beinhalten würde:
Bottle beerBottle = new Bottle ();
beerBottle.fill (new WhiteWine ("Riesling"));
Beer beer = (Beer) beerBottle.empty ();// ClassCastException!
Man müsste stets vorher testen:
Bottle beerBottle = new Bottle ();
...
Drink drink = beerBottle.empty ();
if (drink instanceof Beer) {
Beer beer = (Beer) drink;
...
}
Beide oben skizzierten Lösungen – die Implementierung per Vererbung und die
auf dem Drink-content beruhende Lösung – sind diesem Problem also offensichtlich nicht gewachsen.
3.2
Ein generischer Flaschentyp
Definition einer generischen Klasse
Eine dem Problem angemessene Lösung bedient sich des Konzepts der Generics –
der Typ-Parametrisierung. Hier eine erste Variante einer solchen Lösung:
3.2 Ein generischer Flaschentyp
25
class Bottle<T> {
private T content;
public boolean isEmpty () {
return this.content == null;
}
public void fill (T content) {
if (this.content != null)
throw new IllegalStateException ();
this.content = content;
}
public T empty () {
if (this.content == null)
throw new IllegalStateException ();
T content = this.content;
this.content = null;
return content;
}
public String toString () {
return this.getClass ().getName ()
+ " [content=" + this.content + "]";
}
}
Es handelt sich hier um eine Typ-parametrisierte Klasse. In der Überschrift der
Klasse wird ein formaler Typ-Parameter namens T eingeführt (der auch als TypVariable bezeichet wird):
class Bottle<T> {
(Statt T hätte man auch den Namen SchallUndRauch wählen können – es wird aber
empfohlen, formale Typ-Parameter durch ein einziges großes Zeichen zu benennen.)
T ist ein »Platzhalter« – für Beer, WhiteWine oder RedWine (oder für igendwelche
andere Klassen). Überall dort, wo in der Vererbungs-Lösung die Namen Beer,
WhiteWine oder RedWine benutzt wurden (und wo in der Drink-content-Lösung
Drink verwendet wird), kann nun dieser Platzhalter benutzt werden. T steht für
irgendeine Klasse.
Zunächst wird das content-Attribut definiert. Es ist nun nicht mehr vom Typ
Beer etc., sondern vom Typ T:
private T content;
Die fill-Methode ist parametrisiert mit einem T-Parameter:
public void fill (T content) { ... }
Und empty liefert ein Resultat vom Typ T zurück:
public T empty () { ... }
(Und definiert noch eine weitere lokale Variable vom Typ T.)
26
3 Generics – eine Einführung
Hinweis: Die IllegalStateExceptions werden geworfen, wenn sich eine Flasche nicht in dem für die jeweilige Operation erforderlichen Zustand befindet.
Eine volle Flasche kann man nicht füllen und eine leere nicht austrinken.
Generierung neuer Typen
Wie besorgt man sich nun von dieser Klassendefinition eine Bierflasche (Weißweinflasche, Rotweinflasche)?
Zunächst benötigt man eine Referenzvariable, die auf eine Bierflasche zeigen
kann:
Bottle<Beer> beerBottle;
Der Name der hier definierten Referenzvariablen ist beerBottle; ihr Typ ist
Bottle<Beer>.
Dann muss eine Flasche erzeugt werden, auf die man mit einer Bottle<Beer>Referenz zeigen kann:
new Bottle<Beer> ()
Zusammengefasst:
Bottle<Beer> beerBottle = new Bottle<Beer> ();
Eine Flasche, auf die man mit einer Bottle<WhiteWine>-Referenz zeigen kann, wird
wie folgt erzeugt:
Bottle<WhiteWine> whiteWineBottle = new Bottle< WhiteWine > ();
Man kann sich die Sache so vorstellen, als habe man im ersten Fall eine Klasse
benutzt, in der das T der Bottle-Klasse durch den aktuellen Typ-Parameter Beer
ersetzt wurde; im zweiten eine Klasse, in der dieses T durch WhiteWine ersetzt
wurde.
Tatsächlich existiert aber nur eine einzige – parametrisierte – Bottle-Klasse.
(Dies wird im Kapitel 4 »Generics – systematisch betrachtet« wesentlich präziser
beleuchtet.)
Eine Bierflasche kann dann mit Bier gefüllt werden:
beerBottle.fill (new Beer ("Veltins"));
Einen Versuch, eine Bierflasche mit Weißwein zu füllen:
beerBottle.fill (new WhiteWine ("Riesling")); // illegal
weist der Compiler zurück. Denn die fill-Methode des Typs Bottle<Beer> verlangt einen Parameter, der kompatibel zu Beer ist. Weißwein aber ist kein Bier.
Und die Weißweinflasche kann man nur mit Weißwein füllen:
whiteWineBottle.fill (new WhiteWine ("Riesling"));
3.2 Ein generischer Flaschentyp
27
Wenn man eine Bierflasche hat, kann man versuchen, sie zu leeren – wenn sie
nicht bereits leer ist, ist garantiert, dass man Bier bekommt:
Beer beer = beerBottle.empty ();
Hier ist kein Downcast erforderlich: Aufgerufen über eine Referenz vom Typ
Bottle<Beer>, liefert die empty-Methode der Bottle-Klasse eben exakt Beer zurück.
Aufgerufen über eine Referenz vom Typ Bottle<WhiteWine>, liefert dieselbe
Methode WhiteWine zurück.
Bottle ist eine Typ-parametrisierte Klasse – eine Klasse, welche offensichtlich
die Grundlage ist für konkrete Typen. Bottle<Beer> ist ein solcher konkreter Typ,
Bottle<WhiteWine> ein anderer konkreter Typ. Aufgrund der Klasse können beliebige konkrete Typen generiert werden.
Hier wird es also wichtig, die Begriffe Klasse und Typ auseinander zu halten.
Eine »gewöhnliche«, nicht Typ-parametrisierte Klasse definiert genau einen einzigen Typ – eine parametrisierte Klasse ist die Grundlage für die Definition beliebig
vieler anderer Typen.
Im Folgenden muss es also darum gehen, die konkreten Typen, die aufgrund
einer generischen Klasse erzeugt werden können, genauer zu beleuchten – insbesondere, was das Problem der Kompatibilität solcher Typen angeht.
Kann ein Bottle-Typ generiert werden für Flaschen, die sowohl Rotwein als
auch Weißwein enthalten können?
Bottle<Wine> wineBottle = new Bottle<Wine> ();
Das funktioniert – obwohl es sich bei Wine um eine abstrakte Klasse handelt. Man
kann die wineBottle nun sowohl mit Weißwein als auch mit Rotwein füllen:
wineBottle.fill (new WhiteWine ("Riesling"));
Wine wine = wineBottle.empty ();
System.out.println (wine.getRegion ());
wineBottle.fill (new RedWine ("Chianti"));
wine = wineBottle.empty ();
System.out.println (wine.getRegion ());
Dann kann man natürlich einen Bottle-Typ für solche Flaschen generieren, die
sowohl Bier als auch Rot- und Weißwein enthalten können (ob das sinnvoll ist,
ist eine andere Frage):
Bottle<Drink> drinkBottle = new Bottle<Drink> ();
Die fill-Methode des Typs Bottle<Drink> gibt sich mit jedem zu Drink kompatiblen Parameter zufrieden; die empty-Methode dieses Typs liefert eine Referenz des
Typs Drink zurück (der dann u. U. auf Beer downgecastet werden muss):
drinkBottle.fill (new Beer ("Jever"));
Drink drink = drinkBottle.empty ();
Beer beer = (Beer) drink;
28
3 Generics – eine Einführung
Und man könnte einen noch allgemeineren Bottle-Typ generieren:
Bottle<Object> objectBottle = new Bottle<Object> ();
In diese hier erzeugte Flasche könnte man auch Benzin einfüllen:
objectBottle.fill (new Petrol (...));
Object object = objectBottle.emtpy ();
Petrol petrol = (Petrol) object;
Man beachte: Die weiter oben entwickelte, auf dem Drink-content basierende
Bottle-Klasse verlangte vom content, dass er mindestens Drink-kompatibel ist.
Benzin konnte man in Objekte dieser Klasse also nicht einfüllen. Diese – für das
Problem möglicherweise sinnvolle Einschränkung – ist bei der parametrisierten
Klasse nicht gegeben! Siehe hierzu aber weiter unten.
Kompatibilität
Beer ist kompatibel zu Drink, Drink ist kompatibel zu Object:
Beer beer = new Beer ("Jever");
Drink drink = beer;
Object object = drink;
Man könnte daher auf die nahe liegende Idee kommen, dass auch Bottle<Beer>
kompatibel zu Bottle<Drink> und Bottle<Drink> kompatibel zu Bottle<Object> ist:
Bottle<Beer> beerBottle = new Bottle<Beer> ();
Bottle<Drink> drinkBottle = beerBottle;
// illegal
Bottle<Object> objectBottle = drinkBottle;
// illegal
Die beiden letzten Zeilen werden aber vom Compiler als fehlerhaft zurückgewiesen. Die Typen sind also nicht kompatibel – obwohl die Typen der Parameter der
Bottle-Typen kompatibel sind!
Der Grund hierfür ist leicht einzusehen. Angenommen, es existiert eine Bierflasche:
Bottle<Beer> beerBottle = new Bottle<Beer> ();
Angenommen dann, folgende Zuweisung sei möglich (sie ist nicht möglich):
Bottle<Drink> drinkBottle = beerBottle;
// illegal, but let's assume...
Dann würde die Referenzvariable drinkBottle auf ein Objekt des Typs
Bottle<Beer> zeigen. Die Variable ist aber vom Typ Bottle<Drink>. Und der Typ
einer Referenzvariablen bestimmt, was man mit einer Referenz tun kann. Die
fill-Methode von Bottle<Drink> begnügt sich mit einem Parameter, der kompatibel zu Drink ist. Über die Variable drinkBottle könnte man die fill-Methode
wie folgt aufrufen:
drinkBottle.fill (new Whitewine ("Riesling"));
3.2 Ein generischer Flaschentyp
29
Mit dem Ergebnis, dass die Bierflasche nun plötzlich Wein beinhaltet. Eben deshalb darf es nicht zulässig sein, eine Referenz vom Typ Bottle<Beer> an eine vom
Typ Bottle<Drink> zuzuweisen. Man kann zwar Bier als Getränk betrachten,
nicht aber eine Bierflasche als Getränkeflasche.
Anders gesagt: Generizität definiert keine Vererbung. Generizität und Vererbung
sind orthogonal zueinander. Generizität hat nichts mit Generalisierung/Spezialisierung zu tun.
Das Generics-Konzept unterstützt typsicheres Programmieren. Es führt dazu,
dass die Verwendung von instanceof und die Verwendung des Downcasts bis auf
wenige Ausnahmefälle der Vergangenheit angehören sollten.
Rohe Typen
Aufgrund der obigen Bottle-Definition ist es auch möglich, einfach nur Bottle als
Typ zu verwenden (ohne <...>):
Bottle bottle = new Bottle ();
Ein solcher Typ wird als Raw-Type bezeichnet.
In die durch bottle referenzierte Flasche kann alles Mögliche gefüllt werden.
Die fill-Methode dieses Typs besitzt einen formalen Parameter vom Typ Object;
die empty-Methode liefert eine Object-Referenz zurück:
bottle.fill ("Hello World");
Object object = bottle.empty ();
String s = (String) object;
Der Typ Bottle scheint auf den ersten Blick identisch zu sein mit Bottle<Object>.
Beim genaueren Hinsehen aber existieren wesentliche Unterschiede:
Man kann einer Bottle-Referenz z.B. eine Bottle<Beer>-Referenz zuweisen:
Bottle bottle = new Bottle<Beer> ();
bottle.fill (new WhiteWine ("Riesling"));
WhiteWine whiteWine = (WhiteWine) bottle.empty ();
System.out.println (whiteWine);
Wäre bottle nicht vom rohen Typ Bottle, sondern vom Typ Bottle<Object>, so
wäre diese Zuweisung nicht möglich. Man erkennt am obigen Beispiel aber auch,
dass man mit rohen Typen einigen Unsinn programmieren kann!
Man befindet sich nur dann auf der typsicheren Seite, wenn man die Vermischung von rohen Typen und parametrisierten Typen vermeidet.
30
3 Generics – eine Einführung
Eingeschränkte Typ-Parameter
Die parametrisierte Klasse Bottle konnte bislang auch genutzt werden, um aus
ihr den Typ Bottle<Object> zu generieren. Das T in der Bottle-Definition konnte
durch jeden konkreten Typ ersetzt werden. Gesetzt aber den Fall, man möchte
eine Klasse für Getränkeflaschen definieren – dann muss es für die aktuellen
Typen, durch welche der formale Typ-Parameter der Klasse (T) ersetzt werden
kann, die Einschränkung geben, dass er nur durch Drink-kompatible Typen
»ersetzt« werden kann.
Diese Einschränkung kann wie folgt formuliert werden:
class Bottle<T extends Drink> {
...
}
Hier wird der Typ T eingeschränkt: Alle aktuellen Typen, durch die T ersetzt werden kann, müssen kompatibel zu Drink sein.
Aus dem neuen Bottle-Typ können daher nur noch folgende Typen generiert
werden:
Bottle<Drink>
Bottle<Beer>
Bottle<Wine>
Bottle<WhiteWine>
Bottle<RedWine>
Die folgenden Typen z.B. sind unzulässig (sofern Petrol nicht von Drink abgeleitet ist):
Bottle<Petrol>
Bottle<Object>
Man kann sich also vorstellen, dass die »alte« Definition der Bottle-Klasse:
class Bottle<T> { ... }
nur eine abkürzende Schreibweise für die folgende ausführliche Form ist:
class Bottle<T extends Object> { ... }
Genauso wie class C { ... } bekanntlich nur eine abkürzende Form für class C
extends Object { ... } ist.
3.2 Ein generischer Flaschentyp
31
Arrays mit parametrisierten Typen?
Angenommen, man möchte einen Kasten bauen, der sechs Bierflaschen enthalten
kann. Hier die nahe liegende Formulierung:
class BeerBottleBox {
private Bottle<Beer> [] bottles = new Bottle<Beer> [6];
...
}
Leider meldet der Compiler einen Fehler: »Arrays of generic type are not allowed«.
Man kann das Problem zunächst derart lösen, dass man einen Array mit
Object-Referenzen erzeugt und die Klasse dann mit typsicheren Methoden ausstattet:
class BeerBottleBox {
private Object [] bottles = new Object [6];
private int count = 0;
public boolean isFull () {
return this.bottles.length == this.count;
}
public int getBottleCount () {
return this.count;
}
public int getCapacity () {
return this.bottles.length;
}
public void add (Bottle<Beer> bottle) {
if (this.isFull ())
throw new IllegalStateException ();
this.bottles [this.count] = bottle;
this.count++;
}
public Bottle<Beer> getBottle (int index) {
return (Bottle<Beer>) this.bottles [index];
}
}
Die add-Methode verlangt eine Bierflasche und die getBottle-Methode liefert
eben eine solche zurück.
Nach demselben Schema könnte man einen Weißweinflaschenkasten bauen.
(Genaueres zum Thema Arrays und generische Typen findet sich im Kapitel 4
»Generics – systematisch betrachtet«.)
32
3 Generics – eine Einführung
Container mit Typ-parametrisiertem Inhalt
Natürlich wäre es wünschenswert, eine einzige Klasse BottleBox schreiben zu
können, deren Objekte entweder Bierflaschen, Rotweinflaschen oder Weißweinflaschen aufnehmen können.
Hier eine mögliche Lösung:
class BottleBox<T extends Drink> {
private Object [] bottles = new Object [6];
private int count = 0;
public boolean isFull () { ... }
public int getBottleCount () { ... }
public int getCapacity () { ... }
public void add (Bottle<T> bottle) {
if (this.isFull ())
throw new IllegalStateException ();
this.bottles [this.count] = bottle;
this.count++;
}
public Bottle<T> getBottle (int index) {
return (Bottle<T>) this.bottles [index];
}
}
Auf Grundlage dieser Klasse könnte man dann einen Bierflaschenkasten füllen
und leeren:
BottleBox<Beer> beerBottleBox = new BottleBox<Beer> ();
for (int i = 0; i < beerBottleBox.getCapacity (); i++) {
Bottle<Beer> beerBottle = new Bottle<Beer> ();
beerBottle.fill (new Beer ("Jever"));
beerBottleBox.add (beerBottle);
}
for (int i = 0; i < beerBottleBox.getBottleCount (); i++) {
Bottle<Beer> beerBottle = beerBottleBox.getBottle (i);
Beer beer = beerBottle.empty ();
System.out.println (beer);
}
Vom Compiler wird garantiert, dass in die beerBottleBox nur Bierflaschen eingefügt werden können.
3.3 Vergleichbarkeit von Flaschen
3.3
33
Vergleichbarkeit von Flaschen
Oben wurde gesagt, dass zwar Beer und Wine kompatibel sind zu Drink, nicht aber
Bottle<Beer> und Bottle<Wine> zu Bottle<Drink>.
Die folgende Zeile würde also der Compiler als fehlerhaft zurückweisen:
Bottle<Drink> drinkBottle = new Bottle<Beer> (); // illegal
Die Begründung für das Verhalten des Compilers wurde oben dargestellt.
Angenommen, man möchte nun einen Kasten bauen, der alle möglichen
Getränkeflaschen beinhalten kann. Dies scheint auf den ersten Blick nicht möglich zu sein. (Es sei denn, man geht auf den rohen Typ zurück – mit der damit verbundenen Typunsicherheit.)
Um dieses Problem – und damit verwandte Probleme – trotzdem typsicher
lösen zu können, hat Java eine so genannte Wildcard eingeführt.
Angenommen, man hat eine Bierflasche erzeugt und gefüllt:
Bottle<Beer> beerBottle = new Bottle<Beer> ();
beerBottle.fill (new Beer ("Veltins"));
Man kann nun folgende Referenzvariable definieren:
Bottle<? extends Drink> drinkBottle;
Das ? wird als Wildcard bezeichnet. Es steht für irgendeinen Typ (der in der obigen Definition aber eingeschränkt ist auf Drink – er muss also Drink-kompatibel
sein). ? extends Drink könnte man also übersetzen mit »any« Drink.
Dann kann dieser Variablen auf eine typsichere Art die Bierflasche (oder auch
eine Weißwein- oder Rotweinflasche) zugewiesen werden:
drinkBottle = beerBottle;
Man kann also den Typ Bottle<? extends Drink> als Supertyp der Klassen
Bottle<Beer>, Bottle<WhiteWine> etc. betrachten.
Interessant ist nun, was man mit drinkBottle tun kann. Betrachten wir
zunächst, was man mit beerBottle tun könnte (beerBottle ist vom Typ
Bottle<Beer>):
beerBottle.fill (new Beer ("Jever"));
Beer beer = beerBottle.empty ();
Über beerBottle kann man die Bierflasche also sowohl leeren als auch füllen (was
selbstverständlich ist).
Der Versuch, nun aber über drinkBottle die Bierflasche zu füllen, würde vom
Compiler als fehlerhaft zurückgewiesen:
drinkBottle.fill (new Beer ("Jever"));// illegal
Mit anderen Worten: Es gibt keinen einzigen Typ, der kompatibel wäre zu <?
extends Drink> – nicht einmal Drink selbst (und auch nicht Object). Die fill-
34
3 Generics – eine Einführung
Methode des Typs Bottle<? extends Drink> verlangt aber eben genau einen Parameter eines solchen Typs. Und daraus folgt: die fill-Methode kann über eine
Bottle<? extends Drink>-Referenz niemals aufgerufen werden! (Es sei denn, man
übergibt ihr null.) Und das ist auch richtig so.
Anders verhält es sich mit der empty-Methode. Aufgerufen über eine Referenz
vom Typ <? extends Drink> liefert sie eben auch <? extends Drink> zurück – also
eine Referenz, welche auf jeden Fall auf ein Drink-kompatibles Objekt zeigen
wird. Die folgende Zeile ist somit völlig korrekt:
Drink drink = drinkBottle.empty ();
Also: Über die drinkBottle-Referenz kann man zwar die Flasche nicht füllen, aber
durchaus leeren.
Dann ist auch klar, wie man eine Klasse für Kästen definieren kann, welche
beliebige Getränkeflaschen aufnehmen können:
class BottleBox {
private Object [] bottles = new Object [6];
private int count = 0;
public boolean isFull () { ... }
public int getCapacity () { ... }
public int getBottleCount () { ... }
public void add (Bottle<? extends Drink> bottle) {
if (this.isFull ())
throw new IllegalStateException ();
this.bottles [this.count] = bottle;
this.count++;
}
public Bottle<? extends Drink> getBottle (int index) {
return (Bottle<? extends Drink>) this.bottles [index];
}
}
Es handelt sich hier um eine »gewöhnliche« Klasse, die nicht Typ-parametrisiert
ist. Ihre add-Methode verlangt einen Parameter, dessen Typ kompatibel zu
Bottle<? extends Drink> ist – also z.B. eine Bierflasche oder eine Rotweinflasche
oder eine Weißweinflasche. Die getBottle-Methode liefert eine dementsprechende Referenz zurück.
Folgende Operationen wären damit möglich:
BottleBox box = new BottleBox ();
Bottle<Beer> beerBottle = new Bottle<Beer> ();
beerBottle.fill (new Beer ("Veltins"));
box.add (beerBottle);
Bottle<WhiteWine> whiteWineBottle = new Bottle<WhiteWine> ();
whiteWineBottle.fill (new WhiteWine ("Riesling"));
box.add (whiteWineBottle);
3.3 Vergleichbarkeit von Flaschen
35
for (int i = 0; i < box.getBottleCount (); i++) {
Bottle<? extends Drink> bottle = box.getBottle (i);
Drink drink = bottle.empty ();
System.out.println (drink);
}
Man beachte hier wieder, dass die fill-Operation nur über die Variablen beerBottle und whiteWineBottle aufgerufen werden kann. Über die drink-Referenz (in
der Schleife) können die Flaschen nur geleert werden.
Noch einmal: Container mit Typ-parametrisiertem Inhalt
Weiter oben wurde eine Klasse für Kästen definiert, welche jeweils nur Flaschen
eines bestimmten Typs aufnehmen konnten:
class BottleBox<T extends Drink> {
...
public void add (Bottle<T> bottle) { ... }
public Bottle<T> getBottle (int index) { ... }
}
Auf Grundlage dieser Definition konnte man z.B. einen Bierflaschenkasten erzeugen:
BottleBox<Beer> beerBox = new BottleBox<Beer> ();
Diese BottleBox-Klasse war völlig korrekt – sie hat allerdings eine kleine semantische Ungereimtheit. Sie ist parametrisiert mit einem Typ T, welcher kompatibel
sein muss mit Drink. Aber es soll doch kein Kasten für Getränke gebaut werden,
sondern ein Kasten für Getränkeflaschen. Das sollte auch in der Typ-Parametrisierung klarer ausgedrückt werden können.
Mit Wildcards kann dieses Problem angemessener gelöst werden:
class BottleBox <T extends Bottle <? extends Drink>> {
private Object [] bottles = new Object [6];
private int count = 0;
public boolean isFull () { ... }
public int getCapacity () { ... }
public int getBottleCount () { ... }
public void add (T bottle) {
if (this.isFull ()) throw new IllegalStateException ();
this.bottles [this.count] = bottle;
this.count++;
}
public T getBottle (int index) {
return (T) this.bottles [index];
}
}
36
3 Generics – eine Einführung
BottleBox ist hier wieder eine Typ-parametrisierte Klasse. Der Typ-Parameter
muss kompatibel sein zu Bottle<? extends Drink>. Also könnte z.B. ein Bierkasten
erzeugt werden:
BottleBox<Bottle<Beer>> box = new BottleBox<Bottle<Beer>> ();
Dieser Kasten kann nur Bierflaschen aufnehmen:
Bottle<Beer> bottleBeer = new Bottle<Beer> ();
bottleBeer.fill (new Beer ("Veltins"));
box.add (bottleBeer);
Und in den folgenden Zeilen werden alle Bierflaschen dieses Kastens geleert:
for (int i = 0; i < box.getBottleCount (); i++) {
Bottle<Beer> b = box.getBottle (i);
Beer beer = b.empty ();
System.out.println (beer);
}
3.4
Umfüllen von Flaschen
Angenommen, es existiere das (zugegebenermaßen fiktive) Problem, den Inhalt
einer Getränkeflasche jeweils in eine andere Flasche umzufüllen. Diese Aufgabe
könnte ein BottleTransfuser übernehmen. Natürlich muss sichergestellt sein, dass
der Inhalt einer Bierflasche nur in eine andere Bierflasche und der Inhalt einer
Rotweinflasche nur in eine andere Rotweinflasche umgefüllt werden kann.
Man könnte folgende Typ-parametrisierte Klasse schreiben:
class BottleTransfuser <T extends Bottle<? extends Drink>> {
public void transfuse (T fromBottle, T toBottle) {
Drink drink = fromBottle.empty ();
toBottle.fill (drink);
}
}
Leider wird diese Klasse vom Compiler nicht übersetzt. (Warum nicht? Leeren
geht, füllen nicht!)
Also könnte man sich folgender Lösung bedienen:
class BottleTransfuser <T extends Drink> {
public void transfuse (Bottle<T> fromBottle, Bottle<T> toBottle) {
T drink = fromBottle.empty ();
toBottle.fill (drink);
}
}
3.4 Umfüllen von Flaschen
37
Man könnte dann folgendes Programmfragment schreiben:
BottleTransfuser<Beer> beerTransfuser = new BottleTransfuser<Beer> ();
Bottle<Beer> b1 = new Bottle<Beer> ();
b1.fill (new Beer ("Jever"));
Bottle<Beer> b2 = new Bottle<Beer> ();
transfuser.beerTransfuser (b1, b2);
Der beerTansfuser macht seine Sache typsicher – aber genau in dieser Typsicherheit liegt auch seine Beschränktheit: Er kann nur Bierflaschen umfüllen. Um Rotweinflaschen umzufüllen, bräuchte man also einen zweiten Umfüller, einen, der
ausschließlich für Rotweine zuständig ist: einen BottleTransfuser<RedWine>.
Die Typsicherheit, die hier verlangt werden sollte, besteht aber nur darin,
dass ein Umfüller während seiner Tätigkeit des Umfüllens Bier nicht in Weinflaschen füllt oder umgekehrt. »Während der Tätigkeit des Umfüllens« – also während der Ausführung der transfuse-Methode. Zur Lösung dieses Problems kann
eine generische Methode verwendet werden:
class BottleTransfuser {
public <T extends Drink> void transfuse (
Bottle<T> fromBottle, Bottle<T> toBottle) {
T drink = fromBottle.empty ();
toBottle.fill (drink);
}
}
Man beachte den Typ-Parameter vor dem Typ der Methode (vor void). Genau
dieser wird in der Parameterliste der Methode verwendet. Nur die Methode
transfuse ist Typ-parametrisiert, nicht aber die Klasse. Die Methodendefinition
stellt sicher, dass beide ihr übergebenen Parameter vom selben Typ sind – nämlich
vom Typ Bottle<T>.
Ein BottleTransfuser kann dann sowohl Bier- in Bierflaschen als auch Weißwein- in Weißweinflaschen umfüllen:
BottleTransfuser transfuser = new BottleTransfuser ();
Bottle<Beer> b1 = new Bottle<Beer> ();
b1.fill (new Beer ("Jever"));
Bottle<Beer> b2 = new Bottle<Beer> ();
transfuser.transfuse (b1, b2);
Bottle<WhiteWine> b3 = new Bottle< WhiteWine > ();
b3.fill (new WhiteWine ("Riesling"));
Bottle< WhiteWine > b4 = new Bottle< WhiteWine > ();
transfuser.transfuse (b3, b4);
Fogende Zeile würde der Compiler nicht übersetzen:
transfuser.transfuse (b1, b4);
// illegal
(Man darf kein Bier in eine Weinflasche füllen dürfen.)
38
3 Generics – eine Einführung
Natürlich hätte die Methode transfuse auch static sein können:
class BottleTransfuser {
public static <T extends Drink> void transfuse (
Bottle<T> fromBottle, Bottle<T> toBottle) {
T drink = fromBottle.empty ();
toBottle.fill (drink);
}
}
Dann hätte man die Flaschen wie folgt umfüllen können:
BottleTransfuser.transfuse (b1, b2);
BottleTransfuser.transfuse (b3, b4);
Herunterladen