Ereignisse in Java
Schnittstellen enthalten in Java nur Anwendungstypen (also Signaturen und Ergebnistypen), aber keine Semantik. Die kann zwar durch Dokumentationskommentare angegeben werden, aber der Inhalt dieser Dokumentationskommentare ist nicht für den Java -Übersetzer sondern für Menschen (Programmierer) gedacht und wird daher vom Java -Übersetzer nicht überprüft oder ausgewertet.
Semantische Schärfe
Eine semantische Spezifikation kann mehr oder weniger scharf sein. Die Spezifikation der Operation "compareTo( Object )" in der Schnittstelle "Comparable" sagt beispielsweise, daß diese Operation ein positives Ergebnis zurückgibt, wenn das Empfängerobjekt "e" der Versendung "e.compareTo( o )" größer als das Argumentobjekt "o" ist. Die Spezifikation könnte auch schärfer (genauer) sein, also weniger Möglichkeiten der Implementation offenlassen, indem sie verlangt, daß in diesem Fall genau der Wert "+1" und nicht irgendein positiver Wert zurückgegeben wird. Andererseits könnte die Spezifikation auch unschärfer sein und nur verlangen, daß die Versendung in diesem Fall einen beliebigen numerischen Wert zurückgibt (was aber nicht mehr so nützlich wäre).
Höchstmögliche Schärfe ist nicht immer wünschenswert, die Schärfe soll vielmehr genau dosiert sein, um ein bestimmtes Ziel zu erreichen, so wie auch ein gutes Photo manchmal absichtlich unscharfe Stellen enthält oder ein Schriftsteller bestimmte Details offenläßt. Im Falle der Operation "compareTo( Object )" erlaubt die Unschärfe es, daß als Ergebnis für den erwähnten Fall als positive Zahl der Wert "+1" zurückgegeben wird, aber auch jede andere positive Zahl ist erlaubt und kann verwendet werden, um durch den Betrag des Ergebnisses noch eine zusätzliche Information über das Vergleichsergebnis zurückzugeben, die durch die Spezifikation der Schnittstelle "Comparable" nicht weiter eingeschränkt wird.
Ereignisse
Ein Ereignis ist ein Anwendungstyp zusammen mit einer Bedeutung, die aber etwas anderes festlegt, als die Bedeutung einer Operation. Das Ereignis ist weniger durch sein Verhalten spezifiziert als dadurch, unter welchen Umständen es verschickt wird. Dadurch ist es einem Ereignis-Implementierer weitgehend freigestellt, irgend etwas zu machen, wenn er eine Ereignis-Nachricht erhält, und dadurch die Wirkung des Ereignisses weitgehend selber zu definieren. Insofern ist ein Ereignis als Operation abstrakt, aber hinsichtlich der Umstände, unter denen es auftritt, konkret.
- Ereignisse und Operationen
Ereignis: Sender ----------------> Empfänger Festgelegt, unter Nicht festgelegt,
welchen Umständen welches Verhalten Operation: Sender ----------------> Empfänger Nicht festgelegt, Festgelegt,
unter welchen welches Verhalten
Umständen
Ein Ereignis kann verglichen werden mit einem Klangsignal eines Fernsprechapparates. Dieses hat keine allgemein festgelegte Wirkung. Der Besitzer eines Fernsprechapparates kann darauf reagieren, wie er will. Der Absender des Klangsignals (Anrufer) kann dadurch nicht eine bestimmte Wirkung hervorrufen, sondern muß akzeptieren, was immer der Besitzer des Apparates entscheidet.
Im Falle eines Ereignisses ist also die Versendung von einer bestimmten Wirkung entkoppelt. Das Ereignis kann versendet werden, und welche Wirkung es hat, bestimmt der Empfänger. Wenn der Absender weiß, wie der Empfänger reagiert, dann kann er natürlich mit einer bestimmten Wirkung rechnen. Während man im allgemeinen nicht weiß, ob ein Anruf entgegengenommen wird, kann man bei einem Notrufdienst der Polizei damit rechnen.
Für Ereignisse ist es relativ scharf spezifiziert, unter welchen Umständen sie versendet werden, insofern liegt die Pflicht zur Erfüllung der Spezifikation zunächst beim Absender des Ereignisses, der dafür verantwortlich ist, das Ereignis wie spezifiziert zu versenden. So könnte es beispielsweise spezifiziert sein, daß ein bestimmtes Ereignis immer dann verschickt wird, wenn eine Taste angetippt wurde. Dann ist der Ereignissender dadurch in der Pflicht, diese Spezifikation zu erfüllen, während es dem Ereignisempfänger weitgehend freigestellt ist, wie er auf diese Nachricht reagiert.
Für ein Ereignis ist spezifiziert, unter welchen Umständen der Sender dieses versenden muß, während es dem Empfänger weitgehend freigestellt ist, wie er darauf reagiert.
Ein Ereignis modelliert den Eintritt eines Sachverhaltes, also daß irgend etwas geschieht, während es weitgehend offenläßt, wie darauf reagiert wird.
Damit ist ein Ereignis insofern das Gegenstück einer Operation : Der Empfänger (Implementierer) einer Operation muß deren Spezifikation genau erfüllen, während der Absender diese nach Belieben aktiveren kann. Der Empfänger eines Ereignisses kann dieses interpretieren wie er will, während der Absender das Ereignis genau nach dessen Spezifikation aussenden muß.
Ereignis-Schnittstellen
Eine Ereignis-Schnittstelle enthält verschiedene zusammengehörige Ereignisse.
Beispielsweise könnte eine einfache Tastatur die Taste "0", die Taste "1", die Taste "R", die Taste "E", die Taste "Q", die Taste "P", die Taste "J" und die Taste "K" besitzen.
Die Tastatur könnte entsprechende Ereignisse versenden, die Empfänger dann interpretieren können, wie sie wollen.
Damit Sender und Empfänger sich aufeinander abstimmen können, muß jedoch zunächst eine gemeinsame Schnittstelle aufgebaut werden.
KeyReceiver.java
/* Tastenschnittstelle */
interface KeyReceiver
{ void key0(); void key1(); void keyR(); void keyE();
void keyQ(); void keyP(); void keyJ(); void keyK(); }
Nun kann der Empfänger die Tasten implementieren, wie gewünscht. Hier nehmen wir an, der Empfänger solle einen Editor implementieren. Die Taste "0" und die Taste "1" bewirken das Anhängen des Zeichens "0" bzw. des Zeichens "1" an einen Textspeicher, die Taste "R" bewirkt das Löschen des letzten Zeichens des Textspeichers und die Taste "E" schließt die Bearbeitung ab. Die anderen Tasten sollen ignoriert werden.
In dem Quelltext "Editor.java" implementiert die Klasse "Editor" die Schnittstelle "KeyReceiver" und ist damit der Empfänger von Tastenereignissen. Sie verwendet wiederum die Klasse "EditAction" und die Klasse "EditBuffer" als „Hilfsklassen“, die aber keine Rolle im Rahmen der hier behandelten Ereignis-Schnittstelle spielen.
Editor.java
/* Tastenempfaenger */
class Editor implements KeyReceiver
{ EditAction a; Editor(){ a = new EditAction(); }
public void key0(){ a.insert0(); }
public void key1(){ a.insert1(); }
public void keyR(){ a.backspace(); }
public void keyE(){ a.done(); }
public void keyQ(){} public void keyP(){}
public void keyJ(){} public void keyK(){}}
class EditAction
{ EditBuffer b; EditAction(){ b = new EditBuffer(); }
void insert0 (){ b.append( '0' ); }
void insert1 (){ b.append( '1' ); }
void backspace(){ b.removeLast(); }
void done (){ b.print(); }}
class EditBuffer
{ StringBuffer s; EditBuffer(){ s = new StringBuffer(); }
void append( final char c ){ s.append( c ); }
void removeLast(){ s.deleteCharAt( s.length() - 1 ); }
void print(){ System.out.println( s ); }}
Die Klasse "Editor" schickt Nachrichten an die Klasse "EditAction", die Operationen aufrufen, die nun eine bestimmte Semantik haben und übersetzt damit die Nachrichten in bestimmte Aktionen. Die Klasse "EditAction" verwirklicht diese Operationen durch ein Objekt der Klasse "EditBuffer", welche die Bearbeitungsvorgänge auf dem Textspeicher wirklich umsetzt, wobei sie sich wiederum der Standardklasse "StringBuffer" bedient.
Die Art und Weise, wie hier die Klasse "EditBuffer" die Implementation der Klasse "StringBuffer" verwendet, ohne daß sie als Erweiterung dieser Klasse gekennzeichnet ist, wird auch als Delegation bezeichnet. Die Klasse "EditBuffer" delegiert bestimmte Aufgaben an die Klasse StringBuffer. Dazu verwendet sie ein Objekt dieser Klasse, das im Konstruktor beschafft wird. Entsprechendes gilt auch für die Klasse "EditAction" und die Klasse "Editor".
Da die Vererbung oft eine unnötig starke Bindung zwischen zwei Klassen festlegt, wird heute Delegation gegenüber der Vererbung in vielen Fällen bevorzugt.
So verwendet das ältere AWT zur Weitergabe von Nachrichten auch noch mehr Vererbung, während das neuere Swing dafür Delegation einsetzt. Manche (ältere) Lehrbücher betonen die Vererbung in Zusammenhang mit objektorientierter Programmierung auch zu sehr, so daß manchmal sogar der falsche Eindruck entsteht, daß objektorientierte Programmierung die Nutzung von Vererbung bedeuten muß.
Damit ein vollständiges Programm entsteht, wird nun noch eine Tastatur benötigt, welche die Tastenereignisse verschickt. Diese wird in der statischen Methode »sendKeysTo(KeyReceiver)« realisiert. Es handelt sich hierbei nicht um eine reale Tastatur, sondern nur um ein Software-Modell einer Tastatur, da in dieser Lektion die Anbindung an reale Tastaturen noch nicht behandelt werden soll. Auf dieser Modell-Tastatur werden dann die Taste "1", die Taste "Q", die Taste "0", die Taste "0", die Taste "R" und die Taste "E" angetippt.
Keyboard.java
public class Keyboard
{ private static void sendKeysTo( final KeyReceiver k )
{ k.key1(); k.keyQ(); k.key0(); k.key0(); k.keyR(); k.keyE(); }
public static void main( final java.lang.String[] args )
{ final KeyReceiver keyReceiver = new Editor();
sendKeysTo( keyReceiver ); }}System.out
10
Ereignis-Adapter
Die Implementation eines Tastenempfängers kann noch etwas erleichtert werden. Der Tastenempfänger muß ja für jedes mögliche Ereignis eine Methode definieren, auch wenn das Ereignis gar keine Aktion bewirken soll. Daher mußten in der Deklaration der Klasse "Editor" einige Methoden mit leeren Rümpfen für vier Tasten definiert werden, die eigentlich nicht interessieren.
Falls eine bestimmte Schnittstelle mit mehreren Ereignissen voraussichtlich öfter implementiert wird, können alle Ereignisse einmal in einer abstrakten Basisklasse mit leeren Methodenrümpfen vordeklariert werden. Solch eine abstrakte Basisklasse wird auch als eine Adapterklasse bezeichnet. Eine Empfängerklasse muß dann nur noch die Methoden überschreiben, deren Rumpf nicht leer sein sollen.
Editor1.java
abstract class KeyAdapter1
{ public void key0(){} public void key1(){}
public void keyR(){} public void keyE(){}
public void keyQ(){} public void keyP(){}
public void keyJ(){} public void keyK(){}}
/* Tastenempfaenger */
class Editor1 extends KeyAdapter1 implements KeyReceiver
{ final EditAction a;
Editor1(){ a = new EditAction(); }
public void key0(){ a.insert0(); }
public void key1(){ a.insert1(); }
public void keyR(){ a.backspace(); }
public void keyE(){ a.done(); }}
Diese Kombination aus einem Adapter und einem Empfänger ist zunächst aufwendiger als ein einzelner Empfänger, der Adapter lohnt sich aber dann, wenn später andere Empfänger geschrieben werden sollen, für die er wiederverwendet werden kann. Der Rumpf der Empfängerklasse ist jetzt kleiner geworden, weil sie nur noch die Implementation von Methoden mit nichtleeren Rümpfen enthält und alle anderen Implementation aus dem Adapter übernimmt.
Empfänger-Registrierung
In vielen Fällen soll ein Ereignis-Sender nicht fest auf einen Ereignis-Empfänger eingestellt sein, sondern seine Ereignisse an jeden interessierten Ereignis-Empfänger senden. Dazu muß ein Ereignis-Empfänger zunächst beim Ereignis-Sender eingetragen (registriert) werden, daraufhin kann der Ereignis-Sender seine Ereignisse an den Ereignis-Empfänger schicken.
Registrierung und Ereignis-Lieferung
1.) Registrierung
.---------------------------------------.
| 1 |
V |
.---------------------. .---------------------.
| Ereignis-Sender | | Ereignis-Empfaenger |
| | | |
| | | |
'---------------------' '---------------------'
^ | ^ |
| 1 | * | |
| '---------------------------------------' |
| 2.) Ereignisse |
| |
| 3.) Deregistrierung |
'---------------------------------------------------------'
Wenn die Ereignisse nicht mehr benötigt werden, sollte sich der Ereignisempfänger wieder deregistrieren (die Registrierung rückgängig machen).
Das Programm "Keyboard1" enthält einen Ereignissender, der einen Empfänger akzeptieren kann und diesem dann Ereignisse schicken kann. In der Hauptmethode der Klasse "Keyboard1" wird zunächst ein Empfänger und ein Sender erzeugt. Der Empfänger "receiver" wird alsdann beim Sender "sender" eingetragen und anschließend wird die Steuerung mit der Operation »send()« an den Sender übergeben, der nun seine Ereignisse an den Empfänger schicken kann.
Die Methode "register" ergibt einen Wahrheitswert, der angibt, ob der Registrierungsversuch erfolgreich war. Dieser Wert ist hier immer der Wert "true", aber ein Registrierungsversuch kann grundsätzlich auch scheitern, etwa wenn kein Speicherplatz mehr zur Verfügung steht.
Keyboard1.java
class KeySender { KeyReceiver k;
boolean register( final KeyReceiver k ){ this.k = k; return true; }
void unregister( final KeyReceiver k ){ this.k = null; }
void send()
{ k.key1(); k.keyQ(); k.key0(); k.key0(); k.keyR(); k.keyE(); }}
public class Keyboard1
{ public static void main( final String[] args )
{ final KeyReceiver receiver = new Editor();
final KeySender sender = new KeySender();
if( sender.register( receiver ))
{ sender.send();
sender.unregister( receiver ); }}}System.out
10
Das Ereignisdelegationsmodell (delegation event model )
Eine Ereignisverarbeitung der hier beschriebenen Art, die darauf beruht, daß eingetragenen Empfängern Nachrichten geschickt werden, wird auch als Ereignisdelegationsmodell (delegation event model ) bezeichnet.
Der Begriff „Ereignisdelegationsmodell“ ergibt sich daher, daß der Sender bei der Registrierung des Empfängers eine Referenz auf den Empfänger erhält und die Ereignisse dann an den Empfänger „delegiert“, indem er entsprechende Nachrichten an den Empfänger schickt. (Weiter oben delegierte die Klasse "Editor" die empfangenen Ereignisse dann wiederum an die Klasse "EditAction" weiter, was zufälligerweise auch eine Delegation ist, die aber nicht mehr zum Delegationsmodell gehört, sondern unabhängig davon erfolgt.) Das „Ereignisdelegationsmodell“ ist von einem „Erweiterungsmodell“ zu unterscheiden, bei dem eine Empfängerklasse eine Basisklasse erweitern muß, um Ereignisse zu erhalten. Dieses Erweiterungsmodell wird hier aber nicht behandelt.
Im Ereignisdelegationsmodell wird die Behandlung von Ereignissen an bestimmte Empfänger delegiert, die eine passende Schnittstelle implementieren.
In der Anwendung des Ereignisdelegationsmodells werden oft auch noch Ereignisobjekte verwendet, die einem Ereignisempfänger mit dem Ereignis zusammen als Argument übergeben werden, und weitere Informationen (Details) über das Ereignis enthalten.