© APSIS GmbH , Polling, 2008
Kapitel 6
Um das Programm durch das Schließen des Fensters zu beenden, muss ein Fensterlauscher programmiert werden: Die Methode windowClosing der Schnittstelle WindowListener (oder der Klasse WindowAdapter) dient dazu. Ein Aufruf der Methode exit der Klasse java.lang.System darin beendet die Anwendung:
class FensterLauscher extends java.awt.event.WindowAdapter { public void windowClosing (java.awt.event.WindowEvent ereignis) { System.exit (0); } }
Objekte der Klasse FensterLauscher sind so in der Lage, auf das Anklicken eines Schließen-Symbols zu reagieren. Damit das Programm durch das Schließen eines Fensters auf diese Weise beendet wird, muss ein FensterLauscher-Objekt beim Fenster registriert werden:
FensterLauscher schliesser = new FensterLauscher();
fenster.addWindowListener(schliesser);
Wenn von der Klasse FensterLauscher nur ein Objekt erzeugt werden soll, und zwar als Parameter von addWindowListener, dann kann die Lauscherklasse auch anonym implementiert und ausgeprägt werden:
fenster.addWindowListener (new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent ereignis) { System.exit(0); } } );
Durch die Registrierung werden die Lauscherobjekte im Oberflächenobjekt (hier im Fensterobjekt) durch eine hierfür vorgesehene Referenz vermerkt, und der Interpreter ruft die Methode im Ereignisfall polymorph zurück.
Um auf weitere Fensterereignisse (wie das Anklicken der Symbole in der Titelleiste, z.B. "Minimieren", oder Verschieben des Fensters usw.) zu reagieren zu können, müssen andere Methoden der Schnittstelle WindowListener implementiert werden. Die Klasse WindowAdapter implementiert diese Schnittstelle mit leeren Methoden, sodass bei der Erweiterung dieser Klasse nicht alle, nur einzelne Methoden überschrieben werden müssen.
Man kann auf die Idee kommen, vor der Auswahl der Aktion einen Eimer vorzuwählen, mit dem alle folgenden Aktionen durchgeführt werden. Dann muss (unabhängig von der Eimerzahl) für jede Aktion nur ein Menüpunkt (und dementsprechend nur eine Rückrufprozedur) definiert werden. Wir nennen diese Vorgehensweise Aktion nach Vorwahl.
Wenn wir versuchen, das Programm (6.17) auf diese Weise zu implementieren, brauchen wir eine Menüklasse mit den Menüpunkten: "Linker Eimer", "Rechter Eimer", "Füllen", "Entleeren" sowie "Ende". Für die ersten Vier vereinbaren wir entsprechende Rückrufprozeduren linksWaehlen, rechtsWaehlen, gewaehltenFuellen und gewaehltenEntleeren. Bei ihrer Definition stoßen wir jedoch auf das folgende Problem: Wie erfährt z.B. gewaehltenFuellen, ob zuvor der linke oder der rechte Eimer ausgewählt wurde? Sie muss einen der beiden füllen, sie muss entweder linkerEimer.fuellen() oder rechterEimer.fuellen() aufrufen.
Die Lösung ist, eine Methode mit unterschiedlichen Zielobjekten aufzurufen:
Abb.: Referenz als Gedächtnis
Mit Hilfe einer Referenz, die zwei verschiedene Eimerobjekte referieren kann, erinnert sich das Programm, welches zuvor gewählt wurde; sie ist sein Gedächtnis:
import lehrbuch.Eimer; public class AktionNachVorwahl extends MenueVorwahlAufg { // mit 4 Punkten private final Eimer linkerEimer = new Eimer(), rechterEimer = new Eimer(); private Eimer vorgewaehlterEimer = linkerEimer; // Referenz, vorbesetzt protected void linksWaehlen() { // linker Eimer wird vorgewählt vorgewaehlterEimer = linkerEimer; } protected void rechtsWaehlen() { // rechter Eimer wird vorgewählt vorgewaehlterEimer = rechterEimer; } protected void fuellen() { // Aktion für den vorgewählten Eimer try { vorgewaehlterEimer.fuellen(); } catch { ... } }
protected void entleeren() {
try {
vorgewaehlterEimer.entleeren();
} catch { ...
}
}
public void start() { ... // evtl. die zwei Eimer anzeigen, und dann: menue(); } }
Die Referenz vorgewaehlterEimer ist vom selben Typ wie linkerEimer und rechterEimer: Die linke Seite der Zuweisung passt somit zur rechten.
Übung: Schreiben Sie ein Programm für drei Kreise nach der Strategie „Aktion nach Vorwahl“: Rufen Sie menügesteuert die von Kreis exportierten Methoden bemalen, verstecken und wiederherstellen für den Kreis auf, der vorgewählt wurde. Die Farbe für bemalen lassen Sie über eine Auswahlliste angeben.
Im obigen Beispiel haben wir zwischen zwei Objekten derselben Klasse gewählt; somit konnte die Referenz vorgewaehlterEimer von derselben Klasse vereinbart werden. Wenn Objekte unterschiedlicher Klassen vorgewählt und für sie unterschiedliche Aktionen durchgeführt werden sollen, brauchen sie eine gemeinsame Oberklasse. Ist sie nicht vorhanden, müssen die Klassen in eine jeweils neue Klasse eingebettet werden, die aber dann eine gemeinsame Oberklasse haben. Die Oberklasse vereinbart die polymorphen (wahrscheinlich aufgeschobenen) Methoden, die in den einbettenden Unterklassen für die Polymorphie überschrieben werden müssen. Von hieraus können die Objekte der ursprünglichen, voneinander unabhängigen Klassen angesprochen werden:
Abb.: Polymorphe Klassen
Wir werden nun als Beispiel ein menügesteuertes Programm mit einem Kreis und einem Eimer anfertigen. In den ersten zwei Menüpunkten werden wir das Objekt (Kreis oder Eimer) auswählen, in den nächsten Menüpunkten die drei folgenden Operationen über sie ausführen können: "Rot", "Blau" oder "Weiß". Hierbei werden Rückrufprozeduren ausgeführt, die
Beide Objekte sind von Anfang an sichtbar. Um sie im Fenster nicht übereinander zu sehen, ist es zweckmäßig, zusätzlich z.B. zwei (an für sich überflüssige) Kreise anzuzeigen und dann verschwinden zu lassen: Dann erscheint der Kreis rechts neben dem Eimer. Am besten sorgen wir hierfür im Konstruktor der Kreisklasse.
Um das Kreisobjekt und das Eimerobjekt von derselben Referenz (z.B. gewaehltesObjekt) referieren zu können, muss diese von einer gemeinsamen Oberklasse (z.B. KreisOderEimer) sein. Diese braucht dann drei Methoden (z.B. rot, blau und weiss), die in den beiden Unterklassen überschrieben werden. Weil alle drei Methoden aufgeschoben (abstract) sind (in der Oberklasse gibt es noch keine Objekte, die gefärbt werden könnten), ist es sinnvoll, die gemeinsame Oberklasse nicht als class, sondern als interface zu vereinbaren.
In den Implementierungen der drei Methoden rot, blau und weiss werden dann für das enthaltene Eimer- bzw. das Kreisobjekt jeweils die Methoden fuellen (mit Parameter Eimer.WEIN und Eimer.WASSER) und entleeren bzw. bemalen (mit Parametern Kreis. ROT und Kreis.BLAU) sowie verstecken und zeichnen aufgerufen.
Für die Ausnahmebehandlung in der Eimerklasse bestehen zwei Möglichkeiten:
try { eimer.fuellen(Eimer.WEIN); } catch (VollException ausnahme) { try { eimer.entleeren(); eimer.fuellen(Eimer.WEIN); } catch (VollException a) { } catch (LeerException a) { } }
Die Struktur des Programms kann bildlich folgendermaßen dargestellt werden:
Abb.: Polymorphe Objektwahl
Die Menüklasse wird mit Menuegenerator generiert. Unsere Programmklasse erweitert das Generat und implementiert die fünf Rückrufprozeduren. Es enthält schon Applet und auch ihre start-Methode, die menue aufruft; weil wir keine zusätzlichen Arbeiten vorher oder nachher zu erledigen haben, können wir sie einfach erben.
In der Programmklasse brauchen wir jeweils ein Objekt der Kreis- bzw. Eimerklasse und eine Referenz auf die gemeinsame Oberklasse (auf die Schnittstelle). Die ersten zwei Rückrufprozeduren setzen die Referenz auf das richtige Objekt, die letzten drei rufen die ausgewählte Methode (rot, blau bzw. weiss) polymorph auf.
Das Programm kann nun entwickelt werden:
import lehrbuch.Eimer; import lehrbuch.Kreis; interface KreisOderEimer { void rot(); void blau(); void weiss(); }
class EimerObjekt implements KreisOderEimer { private Eimer eimer; EimerObjekt() { eimer = new Eimer(); eimer.anzeigen(); }
public void rot() { try { eimer.fuellen(Eimer.WEIN); } catch (lehrbuch.VollException ausnahme) { try { eimer.entleeren(); eimer.fuellen(Eimer.WEIN); } catch (Exception e) { System.err.println(ausnahme); // unerwartet } } }
public void blau() { ... } // ähnlich mit Eimer.WASSER
public void weiss() { ... }
} // ähnlich mit entleeren()
class KreisObjekt implements KreisOderEimer { private Kreis kreis; KreisObjekt() { kreis = new Kreis(); // zwei Attrappen (Dummys) kreis.zeichnen(); kreis.verstecken(); ... // zweiter Kreis ähnlich kreis = new Kreis(); // dritter (eigentlicher) Kreis erscheint rechts oben kreis.zeichnen(); }
public void rot() {
kreis.bemalen(Kreis.ROT);
}
public void blau() { ... } // ähnlich mit Kreis.BLAU
public void weiss() { ... } // ähnlich mit verstecken und zeichnen }
abstract class MenuePolymorph extends java.applet.Applet { // generiert public void start() { menue(); } public void menue() { ... } // Rückrufprozeduren werden aufgerufen abstract protected void eimerWaehlen(); abstract protected void kreisWaehlen(); abstract protected void rot(); abstract protected void blau(); abstract protected void weiss(); }
public class PolymorpheVorwahl extends MenuePolymorph { // enthält Applet; start ruft menue auf private final KreisObjekt kreis = new KreisObjekt(); private final EimerObjekt eimer = new EimerObjekt(); private KreisOderEimer gewaehltesObjekt = eimer; // Vorbesetzungswert protected void eimerWaehlen() { gewaehltesObjekt = eimer; } protected void kreisWaehlen() { gewaehltesObjekt = kreis; } protected void rot() { gewaehltesObjekt.rot(); } ... // blau() und weiss() ähnlich }
Übung: Die beiden Unterklassen KreisObjekt und EimerObjekt (die die Schnittstelle KreisOderEimer implementieren) enthalten jeweils eine Referenz auf ein Objekt der Klasse lehrbuch.Kreis bzw. lehrbuch.Eimer. Die oben dargestellte Vorgehensweise ist also Erwerben. Alternativ können sie von den Klassen lehrbuch.Kreis bzw. lehrbuch.Eimer auch erben – dank der Tatsache, dass KreisOderEimer eine Schnittstelle und keine Klasse ist. (Eine Klasse kann nur eine Klasse erweitern, daneben aber eine beliebige Anzahl von Schnittstellen implementieren.) Lösen Sie das Problem auf diesem Wege und überlegen Sie, welche Vor- und Nachteile diese Alternative hat.
© APSIS GmbH , Polling, 2008