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