|
Wir wollen uns in diesem Abschnitt damit beschäftigen, die oben erwähnten Entwurfsmuster für die Abwicklung des Nachrichtenverkehrs in Java-Programmen vorzustellen. Wie bereits erwähnt, hat jedes dieser Verfahren seine ganz spezifischen Vor- und Nachteile und ist für verschiedene Programmieraufgaben unterschiedlich gut geeignet.
Als Basis für unsere Experimente wollen wir ein einfaches Programm schreiben, das die folgenden Anforderungen erfüllt:
Basis der Programme ist das folgende Listing:
|
![]() |
|
![]() |
Das Programm erfüllt die ersten der oben genannten Anforderungen, ist aber mangels Event-Handler noch nicht in der Lage, per [ESC] beendet zu werden. Um die Nachfolgeversionen vorzubereiten, haben wir bereits die Anweisung import java.awt.event.* eingefügt. |
![]() |
|
![]() |
Die Ausgabe des Programms ist:
Abbildung 18.3: Das Programm für den Nachrichtentransfer
Bei der ersten Variante gibt es nur eine einzige Klasse, Listing1802. Sie ist einerseits eine Ableitung der Klasse Frame, um ein Fenster auf dem Bildschirm darzustellen und zu beschriften. Andererseits implementiert sie das Interface KeyListener, das die Methoden keyPressed, keyReleased und keyTyped definiert. Der eigentliche Code zur Reaktion auf die Taste [ESC] steckt in der Methode keyPressed, die immer dann aufgerufen wird, wenn eine Taste gedrückt wurde. Mit der Methode getKeyCode der Klasse KeyEvent wird auf den Code der gedrückten Taste zugegriffen und dieser mit der symbolischen Konstante VK_ESCAPE verglichen. Stimmen beide überein, wurde [ESC] gedrückt, und das Programm kann beendet werden.
001 /* Listing1802.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing1802 007 extends Frame 008 implements KeyListener 009 { 010 public static void main(String[] args) 011 { 012 Listing1802 wnd = new Listing1802(); 013 } 014 015 public Listing1802() 016 { 017 super("Nachrichtentransfer"); 018 setBackground(Color.lightGray); 019 setSize(300,200); 020 setLocation(200,100); 021 setVisible(true); 022 addKeyListener(this); 023 } 024 025 public void paint(Graphics g) 026 { 027 g.setFont(new Font("Serif",Font.PLAIN,18)); 028 g.drawString("Zum Beenden bitte ESC drücken...",10,50); 029 } 030 031 public void keyPressed(KeyEvent event) 032 { 033 if (event.getKeyCode() == KeyEvent.VK_ESCAPE) { 034 setVisible(false); 035 dispose(); 036 System.exit(0); 037 } 038 } 039 040 public void keyReleased(KeyEvent event) 041 { 042 } 043 044 public void keyTyped(KeyEvent event) 045 { 046 } 047 } |
Listing1802.java |
Die Verbindung zwischen der Ereignisquelle (in diesem Fall der Fensterklasse Listing1802) und dem Ereignisempfänger (ebenfalls die Klasse Listing1802) erfolgt über den Aufruf der Methode addKeyListener der Klasse Frame. Alle Tastaturereignisse werden dadurch an die Fensterklasse selbst weitergeleitet und führen zum Aufruf der Methoden keyPressed, keyReleased oder keyTyped des Interfaces KeyListener. Diese Implementierung ist sehr naheliegend, denn sie ist einfach zu implementieren und erfordert keine weiteren Klassen. Nachteilig ist dabei allerdings:
Es bleibt festzuhalten, daß diese Technik bestenfalls für kleine Programme geeignet ist, die nur begrenzt erweitert werden müssen. Durch die Vielzahl leerer Methodenrümpfe können aber auch kleine Programme schnell unübersichtlich werden.
Die zweite Alternative ist eine bessere Lösung. Sie basiert auf der Verwendung lokaler bzw. anonymer Klassen und kommt ohne die Nachteile der vorigen Version aus. Sie ist das in der Dokumentation des JDK empfohlene Entwurfsmuster für das Event-Handling in kleinen Programmen oder bei Komponenten mit einfacher Nachrichtenstruktur. Vor ihrem Einsatz sollte man allerdings das Prinzip lokaler und anonymer Klassen kennenlernen, das mit dem JDK 1.1 in Java eingeführt wurde. Wir wollen es hier kurz vorstellen, uns dabei aber lediglich mit den Grundzügen dieser Technik beschäftigen, um den Einsatz für die Ereignisbehandlung aufzuzeigen. Ein weiteres Beispiel für die Verwendung lokaler Klassen findet sich in Abschnitt 27.4.
Bis zum JDK 1.0 wurden Klassen immer auf der Paketebene definiert, eine Schachtelung war nicht möglich. Seit JDK 1.1 gibt es die Möglichkeit, innerhalb einer bestehenden Klasse X eine neue Klasse Y zu definieren (im JDK wird das gesamte Konzept als Inner Classes bezeichnet). Diese Klasse unterliegt lexikalischen Sichtbarkeitsregeln und ist nur innerhalb von X sichtbar. Objektinstanzen von Y können damit auch nur aus X erzeugt werden. Anders herum (und das macht die lokalen Klassen für das Event-Handling interessant) kann Y auf alle Membervariablen von X zugreifen. Bei der Instanzierung wird (neben einem impliziten this-Zeiger) ein weiterer Verweis auf die erzeugende Instanz der umschließenden Klasse übergeben, der es ermöglicht, auf sie zuzugreifen.
Die Anwendung lokaler Klassen für die Ereignisbehandlung besteht darin, mit ihrer Hilfe die benötigten EventListener zu implementieren. Dazu wird in dem GUI-Objekt, das einen Event-Handler benötigt, eine lokale Klasse definiert und aus einer passenden Adapterklasse abgeleitet. Nun braucht nicht mehr das gesamte Interface implementiert zu werden (denn die Methodenrümpfe werden ja aus der Adapterklasse geerbt), sondern lediglich die tatsächlich benötigten Methoden. Da die lokale Klasse zudem auf die Membervariablen und Methoden der Klasse zugreifen kann, in der sie definiert wurde, lassen sich auf diese Weise sehr schnell die benötigten Ereignisempfänger zusammenbauen.
Das folgende Beispiel definiert eine lokale Klasse MyKeyListener, die aus KeyAdapter abgeleitet wurde und auf diese Weise das KeyListener-Interface implementiert. Sie überlagert lediglich die Methode keyPressed, um auf das Drücken einer Taste zu reagieren. Als lokale Klasse hat sie außerdem Zugriff auf die Methoden der umgebenden Klasse und kann somit durch Aufruf von setVisible und dispose das Fenster, in dem sie als Ereignisempfänger registriert wurde, schließen. Die Registrierung der lokalen Klasse erfolgt durch Aufruf von addKeyListener, bei dem gleichzeitig eine Instanz der lokalen Klasse erzeugt wird. Als lokale Klasse ist MyKeyListener überall innerhalb von Listing1803 sichtbar und kann an beliebiger Stelle instanziert werden.
|
![]() |
|
![]() |
Der Vorteil dieser Vorgehensweise ist ganz offensichtlich: es werden keine unnützen Methodenrümpfe erzeugt, aber trotzdem verbleibt der Ereignisempfängercode wie im vorigen Beispiel innerhalb der Ereignisquelle. Dieses Verfahren ist also immer dann gut geeignet, wenn es von der Architektur oder der Komplexität der Ereignisbehandlung her sinnvoll ist, Quelle und Empfänger zusammenzufassen.
Eine Variante der lokalen Klassen sind die anonymen Klassen. Sie werden ebenfalls lokal zu einer anderen Klasse erzeugt, kommen aber ohne Klassennamen aus. Dazu werden sie bei der Übergabe eines Objektes an eine Methode oder als Rückgabewert einer Methode innerhalb einer einzigen Anweisung definiert und instanziert. Damit eine anonyme Klasse überhaupt irgendeiner sinnvollen Aufgabe zugeführt werden kann, muß sie aus einer anderen Klasse abgeleitet sein oder ein bestehendes Interface implementieren.
Sowohl einfache lokale Klassen als auch anonyme lokale Klassen wurden realisiert, ohne daß eine Änderung der virtuellen Maschine erforderlich war. Statt dessen wurde die Implementierung dieses Konzepts vollständig dem Java-Compiler übertragen. Im Falle einer lokalen Klasse Y innerhalb einer Klasse X erzeugt der Compiler beim Übersetzen der Quelldatei Klassennamen der Art X$Y.class. Im Falle einer anonymen Klasse innerhalb von X erzeugt er Code der Art X$1.class, X$2.class usw. Das Laufzeitsystem interpretiert die lokale Klasse wie eine nicht-lokale. |
![]() |
|
![]() |
Das folgende Beispiel ist eine leichte Variation des vorigen Beispiels. Es zeigt die Verwendung einer anonymen Klasse, die aus KeyAdapter abgeleitet wurde, als Ereignisempfänger. Zum Instanzierungszeitpunkt erfolgt die Definition der überlagerten Methode keyPressed, in der der Code zur Reaktion auf das Drücken der Taste [ESC] untergebracht wird.
|
![]() |
|
![]() |
Vorteilhaft bei dieser Vorgehensweise ist der verringerte Aufwand, denn es muß keine separate Klassendefinition angelegt werden. Statt dessen werden die wenigen Codezeilen, die zur Anpassung der Adapterklasse erforderlich sind, dort eingefügt, wo die Klasse instanziert wird, nämlich beim Registrieren des Nachrichtenempfängers. Anonyme Klassen haben einen ähnlichen Einsatzbereich wie lokale, empfehlen sich aber vor allem, wenn sehr wenig Code für den Ereignisempfänger benötigt wird. Bei aufwendigeren Ereignisempfängern ist die explizite Definition einer benannten Klasse dagegen vorzuziehen.
Wir hatten am Anfang darauf hingewiesen, daß in größeren Programmen eine Trennung zwischen Programmcode, der für die Oberfläche zuständig ist, und solchem, der für die Anwendungslogik zuständig ist, wünschenswert wäre. Dadurch wird eine bessere Modularisierung des Programms erreicht, und der Austausch oder die Erweiterung von Teilen des Programms wird erleichtert.
Das Delegation Event Model wurde auch mit dem Designziel entworfen, eine solche Trennung zu ermöglichen bzw. zu erleichtern. Der Grundgedanke dabei war es, auch Nicht-Komponenten die Reaktion auf GUI-Events zu ermöglichen. Dies wurde dadurch erreicht, daß jede Art von Objekt als Ereignisempfänger registriert werden kann, solange es die erforderlichen Listener-Interfaces implementiert. Damit ist es möglich, die Anwendungslogik vollkommen von der grafischen Oberfläche abzulösen und in Klassen zu verlagern, die eigens für diesen Zweck entworfen wurden. |
![]() |
|
![]() |
Das nachfolgende Beispiel zeigt diese Vorgehensweise, indem es unser Beispielprogramm in die drei Klassen Listing1805, MainFrameCommand und MainFrameGUI aufteilt. Listing1805 enthält nur noch die main-Methode und dient lediglich dazu, die anderen beiden Klassen zu instanzieren. MainFrameGUI realisiert die GUI-Funktionalität und stellt das Fenster auf dem Bildschirm dar. MainFrameCommand spielt die Rolle des Kommandointerpreters, der immer dann aufgerufen wird, wenn im Fenster ein Tastaturereignis aufgetreten ist.
Die Verbindung zwischen beiden Klassen erfolgt durch Aufruf der Methode addKeyListener in MainFrameGUI, an die das an den Konstruktor übergebene MainFrameCommand-Objekt weitergereicht wird. Dazu ist es erforderlich, daß das Hauptprogramm den Ereignisempfänger cmd zuerst instanziert, um ihn bei der Instanzierung des GUI-Objekts gui übergeben zu können.
Umgekehrt benötigt natürlich auch das Kommando-Objekt Kenntnis über das GUI-Objekt, denn es soll ja das zugeordnete Fenster schließen und das Programm beenden. Der scheinbare Instanzierungskonflikt durch diese zirkuläre Beziehung ist aber in Wirklichkeit gar keiner, denn bei jedem Aufruf einer der Methoden von MainFrameCommand wird an das KeyEvent-Objekt der Auslöser der Nachricht übergeben, und das ist in diesem Fall stets das MainFrameGUI-Objekt gui. So kann innerhalb des Kommando-Objekts auf alle öffentlichen Methoden des GUI-Objekts zugegriffen werden. |
![]() |
|
![]() |
001 /* Listing1805.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing1805 007 { 008 public static void main(String[] args) 009 { 010 MainFrameCommand cmd = new MainFrameCommand(); 011 MainFrameGUI gui = new MainFrameGUI(cmd); 012 } 013 } 014 015 class MainFrameGUI 016 extends Frame 017 { 018 public MainFrameGUI(KeyListener cmd) 019 { 020 super("Nachrichtentransfer"); 021 setBackground(Color.lightGray); 022 setSize(300,200); 023 setLocation(200,100); 024 setVisible(true); 025 addKeyListener(cmd); 026 } 027 028 public void paint(Graphics g) 029 { 030 g.setFont(new Font("Serif",Font.PLAIN,18)); 031 g.drawString("Zum Beenden bitte ESC drücken...",10,50); 032 } 033 } 034 035 class MainFrameCommand 036 implements KeyListener 037 { 038 public void keyPressed(KeyEvent event) 039 { 040 Frame source = (Frame)event.getSource(); 041 if (event.getKeyCode() == KeyEvent.VK_ESCAPE) { 042 source.setVisible(false); 043 source.dispose(); 044 System.exit(0); 045 } 046 } 047 048 public void keyReleased(KeyEvent event) 049 { 050 } 051 052 public void keyTyped(KeyEvent event) 053 { 054 } 055 } |
Listing1805.java |
Diese Designvariante ist vorwiegend für größere Programme geeignet, bei denen eine Trennung von Programmlogik und Oberfläche sinnvoll ist. Für sehr kleine Programme oder solche, die wenig Ereigniscode haben, sollte eher eine der vorherigen Varianten angewendet werden, wenn diese zu aufwendig ist.
Natürlich erhebt das vorliegende Beispielprogramm nicht den Anspruch, unverändert in ein sehr großes Programm übernommen zu werden. Es soll lediglich die Möglichkeit der Trennung von Programmlogik und Oberfläche in einem großen Programm mit Hilfe der durch das Event-Handling des JDK 1.1 vorgegebenen Möglichkeiten aufzeigen. Eine sinnvolle Erweiterung dieses Konzepts könnte darin bestehen, weitere Modularisierungen vorzunehmen (z.B. analog dem MVC-Konzept von Smalltalk, bei dem GUI-Anwendungen in Model-, View- und Controller-Layer aufgesplittet werden, oder auch durch Abtrennen spezialisierter Kommandoklassen). Empfehlenswert ist in diesem Zusammenhang die Lektüre der JDK-Dokumentation, die ein ähnliches Beispiel in leicht veränderter Form enthält.
Als letzte Möglichkeit, auf Nachrichten zu reagieren, soll das Überlagern der Event-Handler in den Ereignisquellen selbst aufgezeigt werden. Jede Ereignisquelle besitzt eine Reihe von Methoden, die für das Aufbereiten und Verteilen der Nachrichten zuständig sind. Soll eine Nachricht weitergereicht werden, so wird dazu zunächst innerhalb der Nachrichtenquelle die Methode processEvent aufgerufen. Diese verteilt die Nachricht anhand ihres Typs an spezialisierte Methoden, deren Name sich nach dem Typ der zugehörigen Ereignisklasse richtet. So ist beispielsweise die Methode processActionEvent für das Handling von Action-Events und processMouseEvent für das Handling von Mouse-Events zuständig:
protected void processEvent(AWTEvent e) protected void processComponentEvent(ComponentEvent e) protected void processFocusEvent(FocusEvent e) ... |
java.awt.Component |
Beide Methodenarten können in einer abgeleiteten Klasse überlagert werden, um die zugehörigen Ereignisempfänger zu implementieren. Wichtig ist dabei, daß in der abgeleiteten Klasse die gleichnamige Methode der Basisklasse aufgerufen wird, um das Standardverhalten sicherzustellen. Wichtig ist weiterhin, daß sowohl processEvent als auch processActionEvent usw. nur aufgerufen werden, wenn der entsprechende Ereignistyp für diese Ereignisquelle aktiviert wurde. Dies passiert in folgenden Fällen:
|
![]() |
|
![]() |
Die Methode enableEvents erwartet als Argument eine Maske, die durch eine bitweise Oder-Verknüpfung der passenden Maskenkonstanten aus der Klasse AWTEvent zusammengesetzt werden kann:
protected final void enableEvents(long eventsToEnable) |
java.awt.Component |
Die verfügbaren Masken sind analog zu den Ereignistypen benannt und heißen ACTION_EVENT_MASK, ADJUSTMENT_EVENT_MASK, COMPONENT_EVENT_MASK usw.
Das folgende Beispiel überlagert die Methode processKeyEvent in der Klasse Frame (die sie aus Component geerbt hat). Durch Aufruf von enableEvents wird die Weiterleitung der Tastaturereignisse aktiviert, und das Programm zeigt dasselbe Verhalten wie die vorigen Programme.
|
![]() |
|
![]() |
Diese Art der Ereignisbehandlung ist nur sinnvoll, wenn Fensterklassen oder Dialogelemente überlagert werden und ihr Aussehen oder Verhalten signifikant verändert wird. Alternativ könnte natürlich auch in diesem Fall ein EventListener implementiert und die entsprechenden Methoden im Konstruktor der abgeleiteten Klasse registriert werden.
Das hier vorgestellte Verfahren umgeht das Delegation Event Model vollständig und hat damit die gleichen inhärenten Nachteile wie das Event-Handling des alten JDK. Die Dokumentation zum JDK empfiehlt daher ausdrücklich, für alle »normalen« Anwendungsfälle das Delegation Event Model zu verwenden und die Anwendungen nach einem der in den ersten drei Beispielen genannten Entwurfsmuster zu implementieren. |
![]() |
|
![]() |
Die hier vorgestellten Entwurfsmuster geben einen Überblick über die wichtigsten Designtechniken für das Event-Handling in Java-Programmen. Während die ersten beiden Beispiele für kleine bis mittelgroße Programme gut geeignet sind, kommen die Vorteile der in Variante 3 vorgestellten Trennung zwischen GUI-Code und Anwendungslogik vor allem bei größeren Programmen zum Tragen. Die vierte Variante ist vornehmlich für Spezialfälle geeignet und sollte entsprechend umsichtig eingesetzt werden.
Wir werden in den nachfolgenden Kapiteln vorwiegend die ersten beiden Varianten einsetzen. Wenn es darum geht, Ereignishandler für die Beispielprogramme zu implementieren, werden wir also entweder die erforderlichen Listener-Interfaces in der Fensterklasse selbst implementieren oder sie in lokalen oder anonymen Klassen unterbringen. Da das Event-Handling des JDK 1.1 eine Vielzahl von Designvarianten erlaubt, werden wir uns nicht immer sklavisch an die vorgestellten Entwurfsmuster halten, sondern teilweise leicht davon abweichen oder Mischformen verwenden. Dies ist beabsichtigt und soll den möglichen Formenreichtum demonstrieren. Wo nötig, werden wir auf spezielle Implementierungsdetails gesondert eingehen. |
![]() |
|
![]() |
Kapitel 19 widmet sich den wichtigsten Low-Level-Events und demonstriert den genauen Einsatz ihrer Listener- und Event-Methoden anhand vieler Beispiele. In späteren Kapiteln werden die meisten der High-Level-Events erläutert. Sie werden in der Regel dort eingeführt, wo ihr Einsatz durch das korrespondierende Dialogelement motiviert wird. So erläutert Kapitel 20 in Zusammenhang mit der Vorstellung von Menüs die Action-Ereignisse, und in Kapitel 22 werden Ereignisse erläutert, die von den dort vorgestellten Dialogelementen ausgelöst werden.
|
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com |