Vollskript

Werbung
Programmierpraktikum
Modulare Textverarbeitung in Java (MTJ)
Skript zum Praktikum
Wintersemester 2005
Institut für Informatik
Inhaltsverzeichnis
1 Grundlagen
1.1
1.2
1.3
1.4
1.5
1.6
1.7
1
Das System MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.1.1
Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.1.2
Stationsarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.3
Stationsvernetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
1.2.1
Programmstruktur und Klassen . . . . . . . . . . . . . . . . . . . . . . .
3
1.2.2
Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.2.3
Klassennamen und Packages . . . . . . . . . . . . . . . . . . . . . . . . .
8
Programmstruktur in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.1
Verzeichnisstruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.3.2
Dateistruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.3
Hauptprogramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.3.4
MTJ - Stufe 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.4.1
Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4.2
Variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.4.3
Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.4.4
Zugriffsbeschränkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Variable und Methoden in MTJ Stufe 1 . . . . . . . . . . . . . . . . . . . . . . 18
1.5.1
Vernetzung der Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.5.2
Ereignisfluß . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
1.5.3
Stationsnamen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.6.1
Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.6.2
Umgang mit statischen Klassenteilen . . . . . . . . . . . . . . . . . . . . 26
1.6.3
Umgang mit Objekten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Objekte in MTJ Stufe 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.7.1
Variable und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
i
INHALTSVERZEICHNIS
1.8
1.9
1.7.2
Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.7.3
Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.8.1
Wo kommen Anweisungen vor? . . . . . . . . . . . . . . . . . . . . . . . 37
1.8.2
Zuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.8.3
Methodenaufruf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.8.4
return-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.8.5
Bedingte Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Zur Implementierung einiger Methoden in MTJ Stufe 1 . . . . . . . . . . . . . . 39
1.9.1
Methodenergebnisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.9.2
Bedingte Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.9.3
Ausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2 Vererbung
45
2.1
MTJ - Stufe 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2
Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.3
2.4
2.2.1
Abgeleitete Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2.2
Konstruktoren und Vererbung . . . . . . . . . . . . . . . . . . . . . . . . 49
2.2.3
Zugriffsbeschränkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.2.4
Mehrere Datentypen für das gleiche Objekt . . . . . . . . . . . . . . . . 51
2.2.5
Abstrakte Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.2.6
Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Vererbungshierarchie für die MTJ-Klassen . . . . . . . . . . . . . . . . . . . . . 55
2.3.1
Klassen für Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.3.2
Klassen für Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.4.1
2.5
2.6
2.7
2.8
Methoden für Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Anwendung von Vektoren in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.5.1
Stationsverkettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.5.2
Ereignisfolge in Generatoren . . . . . . . . . . . . . . . . . . . . . . . . . 64
Blöcke und Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.6.1
Lokale Variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.6.2
Blöcke als Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.6.3
for-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Zur Implementierung einiger Methoden in MTJ . . . . . . . . . . . . . . . . . . 66
2.7.1
Bedingte Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.7.2
for-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
Strings und Character . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.8.1
Die Klasse Character . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
ii
INHALTSVERZEICHNIS
2.8.2
2.9
Die Klasse String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
Die Zeichenzähler-Anwendung in MTJ . . . . . . . . . . . . . . . . . . . . . . . 70
2.9.1
Character-Items . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.9.2
Der Character-Generator . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.9.3
Die Filterstationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.9.4
Die Zählerstation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3 Stationen
75
3.1
MTJ - Stufe 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.2
Reihungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.3
3.4
3.5
3.2.1
Unterschiede zu Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3.2.2
Reihungs-Datentypen
3.2.3
Erzeugung von Reihungen . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.2.4
Zugriff auf Reihungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
String-Items in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.3.1
Die Klasse StringItem . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.3.2
Die Klasse StringGenerator . . . . . . . . . . . . . . . . . . . . . . . . . 78
Ausnahmebehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
3.4.1
Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.4.2
Melden von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.4.3
Abfangen von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . 81
3.4.4
Deklaration von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . 82
Item-kompatible Stationsverbindungen in MTJ . . . . . . . . . . . . . . . . . . 83
3.5.1
Die Klasse MtjException . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
3.5.2
Verwaltung der Item-Arten . . . . . . . . . . . . . . . . . . . . . . . . . 83
3.5.3
Prüfung der Item-Arten . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.6
Die Klasse Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
3.7
Darstellung für Item-Arten bei MTJ . . . . . . . . . . . . . . . . . . . . . . . . 85
3.8
Verschattung und Überschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.9
3.8.1
Verschattung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
3.8.2
Überschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
Neutrale Stationen in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.10 Interfaces als Konstantensammlungen . . . . . . . . . . . . . . . . . . . . . . . . 90
3.10.1 Realisierung von Aufzählungstypen . . . . . . . . . . . . . . . . . . . . . 90
3.11 Statische Klassen als Klassenbestandteile . . . . . . . . . . . . . . . . . . . . . . 92
3.12 Signale in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.12.1 Die Klasse Signal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.12.2 Signalarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.13 Behandlung von Items durch Arbeitsstationen in MTJ . . . . . . . . . . . . . . 95
iii
INHALTSVERZEICHNIS
3.13.1 Gruppierung der Arbeitsstationsarten . . . . . . . . . . . . . . . . . . . 95
3.13.2 Behandlung von Signalen . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.13.3 Transparente Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
3.13.4 Filternde Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.13.5 Zerlegende Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.13.6 Stationen zum Zerlegen von Strings . . . . . . . . . . . . . . . . . . . . . 100
3.13.7 Sammelnde Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.13.8 Die Klasse StringConcatStation . . . . . . . . . . . . . . . . . . . . . . . 102
4 Benutzungsoberfläche
105
4.1
MTJ - Stufe 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
4.2
Model-View-Controller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
4.3
4.4
4.5
4.6
4.7
4.8
4.2.1
Aufgaben Model-View-Controller . . . . . . . . . . . . . . . . . . . . . . 106
4.2.2
Delegate-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
Aufbau der Benutzungsoberfläche aus Komponenten . . . . . . . . . . . . . . . 107
4.3.1
Komponenten und Container . . . . . . . . . . . . . . . . . . . . . . . . 108
4.3.2
Layout von Komponenten in einem Container . . . . . . . . . . . . . . . 111
4.3.3
Beispiel für eine einfache graphische Anwendung in Java . . . . . . . . . 113
Komponenten der MTJ-Benutzungsoberfläche . . . . . . . . . . . . . . . . . . . 114
4.4.1
Beschreibung der Komponenten . . . . . . . . . . . . . . . . . . . . . . . 114
4.4.2
Struktur der Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . 115
4.4.3
Die Klasse MtjFrame . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
Das Zeichnen von Graphiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
4.5.1
Zeichenumgebung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
4.5.2
Wiederherstellen von Zeichnungen . . . . . . . . . . . . . . . . . . . . . 119
4.5.3
Objektorientiertes Zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . 120
4.5.4
Koordinatensystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
4.5.5
Fonts
4.5.6
Farben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
Zeichnen einer Rechteck-Menge in MTJ . . . . . . . . . . . . . . . . . . . . . . 123
4.6.1
Die Klasse Rectangle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
4.6.2
Die Klasse RectangleSet . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
Klassen als nichtstatische Klassenbestandteile . . . . . . . . . . . . . . . . . . . 125
4.7.1
Zugriff auf Bestandteile der zugeordneten Instanz . . . . . . . . . . . . . 126
4.7.2
Erzeugung von Instanzen einer inner class . . . . . . . . . . . . . . . . . 127
4.7.3
Zugriff auf Instanzen einer inner class . . . . . . . . . . . . . . . . . . . 127
Behandlung von Benutzereingaben . . . . . . . . . . . . . . . . . . . . . . . . . 129
4.8.1
Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4.8.2
Ereignisbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
iv
INHALTSVERZEICHNIS
4.8.3
4.9
Verwendung von inner classes für Listener . . . . . . . . . . . . . . . . . 133
Benutzereingaben in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.9.1
Hinweis: Vergleich mit MTJ-Ereignissen . . . . . . . . . . . . . . . . . . 134
4.9.2
Ereignisbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.10 Der Konfigurations-Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.10.1 Darstellung der Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . 136
4.10.2 Interaktionsmöglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.10.3 Ausgaben von Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
4.10.4 Behandlung von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . 138
4.10.5 Realisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
4.11 Animation des Ereignisflusses . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
4.11.1 Übertragung von Signalen und Items . . . . . . . . . . . . . . . . . . . . 139
4.11.2 Realisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
5 Multithreading
145
5.1
MTJ - Stufe 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
5.2
Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
5.3
5.4
5.5
5.2.1
Threads in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
5.2.2
Erzeugen und Starten von Threads . . . . . . . . . . . . . . . . . . . . . 146
Jeder MTJ-Station ihr eigener Thread . . . . . . . . . . . . . . . . . . . . . . . 150
5.3.1
Bisherige Situation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150
5.3.2
Umstellung auf mehrere Threads . . . . . . . . . . . . . . . . . . . . . . 153
5.3.3
Erzeugung der Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
5.3.4
Kontrollfluss zwischen Stationen . . . . . . . . . . . . . . . . . . . . . . 154
Synchronisation zwischen Threads . . . . . . . . . . . . . . . . . . . . . . . . . 155
5.4.1
Monitore als Synchronisationsmittel . . . . . . . . . . . . . . . . . . . . 155
5.4.2
Warten auf Änderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
5.4.3
Deadlock - Beidseitiger Ausschluß . . . . . . . . . . . . . . . . . . . . . . 157
Ereignis-Weitergabe zwischen MTJ-Stationen . . . . . . . . . . . . . . . . . . . 158
5.5.1
Ereignispuffer für Arbeitsstationen . . . . . . . . . . . . . . . . . . . . . 158
5.5.2
Synchronisation des Zugriffs . . . . . . . . . . . . . . . . . . . . . . . . . 158
5.5.3
Ereignis-Übergabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
v
INHALTSVERZEICHNIS
vi
Abbildungsverzeichnis
1.1
Eine einfache Konfiguration zur Lösung der Zeilenumbruchsaufgabe . . . . . . .
2
1.2
Umsetzung von Quelldateien durch den Java-Compiler . . . . . . . . . . . . . .
4
1.3
Abhängigkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
1.4
Die statischen Beziehungen zwischen Stationen . . . . . . . . . . . . . . . . . . 18
1.5
Der Ablauf von addEventListener . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.6
Der Ablauf von sendEvent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.7
Klassen in UML-Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
1.8
Statischer und Schablonenteil einer Klasse . . . . . . . . . . . . . . . . . . . . . 25
1.9
Ein Objekt und seine zugehörige Klasse . . . . . . . . . . . . . . . . . . . . . . 28
1.10 Eine Referenz auf ein Objekt . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.11 Arbeitsweise des Vergleichsoperators . . . . . . . . . . . . . . . . . . . . . . . . 33
1.12 Statische Verkettung der Stationen durch Referenzen . . . . . . . . . . . . . . . 35
1.13 Konfiguration aus einer Generator- und zwei Arbeitsstationen . . . . . . . . . . 37
1.14 Stationsverkettung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
1.15 Die connected-Relation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.1
Abgeleitete Klasse und Basisklasse . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.2
java.lang.Object als Wurzel der Vererbungshierarchie . . . . . . . . . . . . . . . 47
2.3
Beispiel für eine abstrakte Basisklasse . . . . . . . . . . . . . . . . . . . . . . . 48
2.4
Beispiel für eine finale Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.5
Referenzen auf Objekte unterschiedlicher Klassen in einer Variablen . . . . . . . 51
2.6
Eine Klasse, die ein Interface implementiert. . . . . . . . . . . . . . . . . . . . . 55
2.7
Die Interfaces für Stationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.8
Die Klasse Station . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.9
Die Klassen für Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.10 Die statischen Beziehungen zwischen Stationen auf Stufe 2 . . . . . . . . . . . . 62
2.11 Statische Verkettung der Stationen durch Referenzen . . . . . . . . . . . . . . . 63
2.12 Der modifizierte Ablauf von sendEvent . . . . . . . . . . . . . . . . . . . . . . . 64
2.13 Konfiguration für Zeichenzähler (Aufgabe) . . . . . . . . . . . . . . . . . . . . . 72
vii
ABBILDUNGSVERZEICHNIS
3.1
Klassen für Ausnahmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.2
Schema zum Abfangen von Ausnahmen . . . . . . . . . . . . . . . . . . . . . . 82
3.3
Die vier Arten von Arbeitsstationen . . . . . . . . . . . . . . . . . . . . . . . . 96
3.4
zwei Möglichkeiten zur Signalbehandlung bei sammelnden Stationen . . . . . . 97
3.5
Stringverarbeitung (Aufgabe) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
4.1
Darstellung des Model-View-Controller-Musters . . . . . . . . . . . . . . . . . . 106
4.2
Darstellung des Delegate-Modells . . . . . . . . . . . . . . . . . . . . . . . . . . 107
4.3
Klassendiagramm der AWT- und SWING-Komponenten . . . . . . . . . . . . . 109
4.4
BorderLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.5
GridLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
4.6
Ausschnitt aus der AWT-Klassen-Hierarchie . . . . . . . . . . . . . . . . . . . . 113
4.7
einfache graphische Anwendung in Java . . . . . . . . . . . . . . . . . . . . . . 113
4.8
Benutzungsoberfläche in MTJ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
4.9
Die Schachtelungs-Hierarchie der Komponenten . . . . . . . . . . . . . . . . . . 116
4.10 Aufbau des Fensters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
4.11 Graphikausgabe in ein Canvas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
4.12 Beispiel für objektorientiertes Zeichnen . . . . . . . . . . . . . . . . . . . . . . . 121
4.13 Instanz einer inner class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
4.14 Instanzen einer inner class mit gemeinsamem Bezugsobjekt . . . . . . . . . . . 128
4.15 Ereignisbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
4.16 Information eines ActionListeners über ein Ereignis . . . . . . . . . . . . . . . . 133
4.17 Vergleich mit MTJ-Ereignissen . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
4.18 Darstellung einer Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
4.19 eine einfache Benutzungsoberfläche (Aufgabe) . . . . . . . . . . . . . . . . . . . 141
5.1
Klassen zum Umgang mit Threads . . . . . . . . . . . . . . . . . . . . . . . . . 148
5.2
Zustandübergangsdiagramm für Threads . . . . . . . . . . . . . . . . . . . . . . 149
5.3
Beispiel-Ablauf mit einem Thread . . . . . . . . . . . . . . . . . . . . . . . . . . 151
5.4
möglicher Ablauf in der Konfiguration . . . . . . . . . . . . . . . . . . . . . . . 152
5.5
Deadlock-Situation im Straßenverkehr . . . . . . . . . . . . . . . . . . . . . . . 158
5.6
Mögliche Abläufe bei der Ereignis-Weitergabe . . . . . . . . . . . . . . . . . . . 160
5.7
Beispiel für Verzögerung beim Betreten eines Monitors . . . . . . . . . . . . . . 161
viii
Kapitel 1
Grundlagen
Im ersten Abschnitt des Praktikums werden die Grundlagen von Java, Programmierung und
insbesondere objektorientierter Programmierung behandelt.
1.1
Das System MTJ
Im Rahmen des Praktikums soll ein System entwickelt werden, mit dem sich eine Reihe von
Aufgaben im Umfeld von Textverarbeitung durchführen lassen.
Die wesentliche Idee dabei ist es, einzelne Komponenten zu entwickeln, die in ihrem
Zusammenwirken bestimmte Aufgaben lösen. Inhalt des Praktikums ist sowohl die
Entwicklung spezifischer Komponenten, als auch der Infrastruktur zur Integration und
Konfigurierung der Komponenten.
1.1.1
Begriffe
Definition: Um eine bestimmte Aufgabe zu erledigen, beispielsweise den Zeilenumbruch eines
Dokuments, verwenden wir ein Programm, das aus einer Konfiguration von Stationen
besteht.
Jede Station nimmt einen bestimmten Bearbeitungsschritt vor. Die Stationen sind über
„Leitungen“ fest verbunden. Entlang jeder Leitung fließt ein Strom von bestimmten
Dateneinheiten, beispielsweise von Einzelzeichen, Worten oder Zeilen.
Definition: Diese Dateneinheiten wollen wir als Items bezeichnen. Neben den Items können
über eine Leitung noch zusätzliche Signale geschickt werden, beispielsweise als Trennzeichen
zwischen Gruppen von Items. Items und Signale werden zusammen als Ereignisse (Events)
bezeichnet.
1
KAPITEL 1. GRUNDLAGEN
Abbildung 1.1: Eine einfache Konfiguration zur Lösung der Zeilenumbruchsaufgabe
Sie besteht aus 5 numerierten Stationen, die linear durch Leitungen verbunden sind. Die
erste Station liefert eine Folge von Dateinamen. In den zugehörigen Dateien sind die
umzubrechenden Texte enthalten. Die zweite Station liest jeweils den Dateiinhalt und leitet
ihn als Folge von Zeilen weiter. Beim Übergang zwischen Dateien fügt sie ein Trennsignal in
den Zeilenstrom ein. Die dritte Station zerlegt die eintreffenden Zeilen in Wörter und reicht die
Trennsignale weiter. Die vierte Station sammelt die Wörter wieder zu Zeilen einer bestimmten
Länge auf, erledigt also die eigentliche Umbruchaufgabe. Bei einem Trennsignal wird eine Zeile
ggf. frühzeitig beendet. Die fünfte Station zeigt die bei ihr eintreffenden Zeilen an.
1.1.2
Stationsarten
Die Stationen in der Konfiguration lassen sich generell danach unterscheiden, ob sie einen
Eingabestrom verarbeiten, oder ob sie von sich aus einen Strom von Items generieren.
Definition: Die Stationen der ersten Art nennen wir Arbeitsstationen (processing
stations). Sie bilden die inneren Knoten der Konfiguration. Die Stationen der zweiten
Art nennen wir Generatorstationen. Sie bilden die Start- oder Eingangsknoten der
Konfiguration.
Im Zeilenumbruchsbeispiel ist Station 1 eine Generatorstation, alle übrigen Stationen sind
Arbeitsstationen.
1.1.3
Stationsvernetzung
Im Gegensatz zu der linearen Verkettung der Stationen im Beispiel sollen Stationen im
allgemeinen beliebig viele Vorgänger und Nachfolger haben können. Eine Station kann also
mehrere eintreffende Itemströme verarbeiten. Die Ströme werden dazu am Eingang der
Station gemischt. Die Station arbeitet unabhängig von der Anzahl der Vorgängerstationen,
sie erhält den fertig gemischten Eingangsstrom. Eine Station kann auch ihren Ausgabestrom
an mehrere Stationen weiterleiten. In diesem Fall erhält jede Nachfolgestation eine eigene
Kopie des kompletten Stroms, die Station arbeitet also unabhängig von der Anzahl ihrer
Nachfolgerstationen. Auf diese Weise ist es möglich, die Stationen unabhängig von der
Konfiguration zu programmieren, in der sie später eingesetzt werden sollen.
2
1.2. PROGRAMME
1.2
Programme
Bei der Programmierung entsteht eine als Programm bezeichnete Einheit. In klassischen
Programmiersprachen ist ein Programm relativ klar abgegrenzt, in Java besteht ein Programm
aus einer Sammlung von Klassen.
1.2.1
Programmstruktur und Klassen
Die Struktur von Programmen unterscheidet sich je nach Art der Programmiersprache.
Programm als syntaktische Einheit
In klassischen Programmiersprachen, beispielsweise Pascal, erstellt der Programmierer ein
„Programm“. Dieses bildet eine eigene syntaktische Einheit in der Programmiersprache und
ist durch entsprechende Schlüsselworte gekennzeichnet. Der Programmtext befindet sich in
einer Datei und wird im ganzen übersetzt.
Modularisierung
Programmiersprachen wie C erlauben eine stärkere Modularisierung. Hier besteht ein
Programm aus einer Sammlung von globalen Variablen und Prozeduren. Eine der Prozeduren
ist durch ihren Namen („main“) ausgezeichnet und entspricht dem Hauptprogramm, d.h.,
diese Prozedur wird bearbeitet, wenn das Programm gestartet wird. Die Variablen und
Prozeduren können auf mehrere Dateien aufgeteilt sein, die separat übersetzt werden. Durch
den Bindevorgang werden die so entstandenen Teile zu einem kompletten ausführbaren
Programm in einer Datei (unter Windows „.exe“) kombiniert.
Klassen
In Java findet sich eine noch konsequentere Modularisierung. Es existiert kein Programm mehr,
sondern nur noch eine Menge von „Klassen“.
• Dateien
Mehrere Klassen können in der gleichen Quelldatei (Endung „.java“) enthalten sein. Die
Definitionen werden im einfachsten Fall in der Form
1
2
3
4
5
6
class <Identifikator1> {
...
}
class <Identifikator2> {
...
}
hintereinander geschrieben. Der Übersetzer (Kommando: javac) übersetzt die als
Text vorliegenden Klassen in Bytecode. Dabei wird jede Klasse (unabhängig von der
Gruppierung in Quellfiles) in einer eigenen Datei (Endung „.class“) abgelegt.
3
KAPITEL 1. GRUNDLAGEN
Beispiel für eine Klasse
Folgendes ist der Quelltext für eine Klasse mit einer enthaltenen Methode.
1
2
3
4
5
6
class A {
public static void main(String[] argv)
{
System.out.println("Die Methode main von Klasse A");
}
}
Abbildung 1.2: Umsetzung von Quelldateien durch den Java-Compiler
• Dynamisches Binden
Der Bindevorgang entfällt. Damit existiert bis zum Start eines Programms dieses nicht
als Einheit. Erst beim Ablauf werden die benötigten Klassen identifiziert und nach und
nach hinzugeladen („dynamisches Binden“).
• Programmstart
Als Hauptprogramm kann in Java jede Klasse verwendet werden. Dazu muß in ihr eine
spezielle Methode (entspricht hier einer Prozedur) mit dem Namen „main“ definiert sein.
Um ein Java-Programm zu starten wird der Name einer Klasse angegeben (Kommando:
java <Klassenname>). Sofern diese Klasse eine Methode „main“ besitzt, wird diese
bearbeitet. Werden dabei weitere Klassen benutzt, so werden sie dynamisch geladen.
Sobald das Ende der Methode „main“ erreicht ist, ist der Programmlauf beendet.
4
1.2. PROGRAMME
1.2.2
Werkzeuge
Die minimal erforderlichen Werkzeuge bei der Erstellung von Java-Programmen ist ein Editor
zum editieren der .java-Dateien, der Übersetzer javac und ein Debugger zur Unterstützung
von Testläufen.
Zum Übersetzungsvorgang
Im einfachsten Fall lautet das Kommando zum Starten des Übersetzers
javac <Quelldatei>
Die angegebene Quelldatei wird übersetzt und die entsprechende Bytecode-Datei wird erzeugt.
Im Fall der Klasse A, die in der Datei A.java gespeichert sei, sieht der Aufruf beispielsweise
wie folgt aus:
javac A.java
Es wird die Bytecode-Datei A.class erzeugt.
• Abhängigkeiten zwischen Klassen
Sobald mehrere Klassen beteiligt sind, kommt es normalerweise zu Abhängigkeiten, die
bei der Übersetzung berücksichtigt werden müssen.
Für weitere Eklärungen führen wir folgendes Beispiel mit den zwei Klassen A und B ein,
die in den Quelldateien A.java und B.java gespeichert seien.
1
2
3
4
5
6
7
8
9
10
11
12
13
class A {
public static void main(String[] argv)
{
System.out.println("Die Methode main von Klasse A");
B.printIt();
}
}
class B {
public static void printIt()
{
System.out.println("Die Methode printIt von Klasse B");
}
}
– Übersetzung
Um einen Programmlauf zu starten müssen zunächst beide Klassen in BytecodeDateien übersetzt werden. Im Anschluß daran kann man einen Programmlauf
starten, indem man java A aufruft. Dieser startet dann, indem nach Konvention
die main Methode der Klasse A abgearbeitet wird.
– Abhängigkeit
Man sieht, daß in der Klasse A die Methode printIt der Klasse B aufgerufen wird.
Somit ergibt sich folgender Abhängigkeitsgraph:
5
KAPITEL 1. GRUNDLAGEN
Abbildung 1.3: Abhängigkeit
– Änderungen der Quelldateien
Weiterhin ist es natürlich selbstverständlich, daß nach der Änderung einer
Quelldatei vor einem erneuten Programmlauf diese Quelldatei übersetzt werden
muß. Technisch erkennt man dies daran, daß der Zeitstempel der Quelldatei jünger
ist als der Zeitstempel der dazugehörigen Bytecode-Datei.
– Inkonsistenzen
Nehmen wir nun folgendes Szenario an: Nachdem wir bereits mehrmals die
Quelldateien der Klassen A und B geändert, gespeichert und übersetzt haben,
ändern wir erneut beide Dateien und speichern sie. Wenn wir nun nur A erneut
übersetzen, kommt es im Folgenden zu einem inkonsistenten Programmlauf. Dies
erkennen wir sofort aus dem Abhängigkeitsgraph: A ist von B abhängig (A
verwendet B), deshalb muß B für einen konsistenten Programmlauf ebenfalls
übersetzt werden.
– Behandlung durch javac
Um bei derartigen Fällen eine unnötige Fehlersuche zu ersparen überwacht der
Java Übersetzer javac derartige Datei-Abhängigkeiten und erzeugt bei dem
Aufruf javac A.java zugleich die ebenfalls benötigte Bytecode-Datei B.class,
alle zum Programmlauf benötigen .class Dateien liegen in einer zum Quellcode
konsistenten Form vor.
– Besonders schön sieht man dies, wenn noch keine Bytecode-Dateien vorhanden sind.
Mit obigem javac Aufruf übersetzt der Übersetzer beide Quelldateien und erzeugt
somit beide .class Dateien.
– Diese bequeme Eigenschaft des Java Übersetzers muß in anderen Programmiersprachen mit Hilfe separater Werkzeuge, z.B. mit dem UNIX make-Werkzeug durch die
Programmierer selbst gelöst werden.
– An dieser Stelle sei darauf hingewiesen, daß es noch weitere andersartige
Abhängigkeiten in Java Quelldateien gibt, die nicht standardmäßig durch den Java
Übersetzer überwacht werden. Um auch diese Abhängigkeiten zu überwachen muß
der Übersetzer mit der Option -depend aufgerufen werden:
javac -depend <Quelldatei>
Das Testen von Java Programmen
Bei der Programmierung geraten fast zwangsläufig Fehler in den Quelltext. Diese können zum
einen von syntaktischer Natur sein und vom Java-Compiler angezeigt werden. Zum anderen
können semantische Fehler auftreten, das Programm tut nicht das, was der Programmierer
erwartet.
6
1.2. PROGRAMME
• Semantische Fehler
Fehler dieser Art können vom Compiler nicht erkannt werden, es liegt am Programmierer,
diese Fehler zu lokalisieren. Dazu bieten sich mehrere Vorgehensweisen an.
– Tritt ein Fehler in einem kleinen, übersichtlichen Modul auf, so kann man dessen
Ursache oft schon durch kurzes Durchdenken des betroffenen Moduls erkennen.
– Bei komplexeren Programmen oder „absolut undurchsichtigen“ Fehlern bietet sich
obige Vorgehensweise nicht mehr an, evtl. ist die Fehlerquelle im Programm ja
noch nicht einmal annähernd lokalisierbar. In diesem Fall muß die Problemstelle
systematisch gesucht werden. Dazu kann man entweder konsequent Hilfsausgaben
in ein Programm einbauen (sogenannter printf-debugger) oder einen richtigen
Debugger verwenden, mit dessen Hilfe man ein Programm schrittweise ablaufen
lassen und jederzeit alle Variablen und Aufrufbeziehungen von Methoden
inspizieren kann.
• jdb Ein leicht verfügbarer Java-Debugger ist der jdb.
– Aufruf
Ein Debug-Ablauf der Klasse A mit Hilfe des jdb wird durch
jdb A
gestartet. Dadurch wird der Java-Interpreter gestartet, die Klasse A geladen, aber
noch nicht ausgeführt; dazu muß noch das jdb-Kommando run eingegeben werden
- das Programm läuft dann ab, als ob man es normal mit dem Java Interpreter
aufgerufen hätte.
– Debugger-Kommandos
Zur Beobachtung des Programmablaufs gibt es mehrere jdb-Kommandos, die
verwendet werden können. Die wichtigsten sind hier im Folgenden erklärt (ist der
jdb-Debugger gestartet, kann man sich durch Eingabe von help eine Liste aller
Kommandos mit kurzer Erklärung anzeigen lassen).
∗ Beeinflussung des Programm-Ablaufs
Die folgenden Kommandos beeinflussen den Ablauf des Programms.
7
KAPITEL 1. GRUNDLAGEN
Befehl
stop at <class>:<line>
Auswirkung
Setzt einen sogenannten Breakpoint (markierte Stelle im Programm, an der die Programmausführung im Debugger angehalten
wird, falls sie ausgeführt werden soll; man
kann sich dann beispielsweise Variablenwerte anzeigen oder zeilenweise das Programm
abarbeiten lassen) in der Klasse <class>
an Zeile <line>.
stop in <method>
Setzt einen Breakpoint am Beginn der
angegeben Methode <method>.
clear <class>:<line>
Löscht einen gesetzten Breakpoint.
run <class>
Startet die Programmausführung. Es können optional eine Klasse und Parameter angegeben werden.
next
Arbeitet eine Programmzeile ab.
step
Arbeitet eine Programmzeile ab. Falls in
der aktuellen Zeile ein Methodenaufruf
enhalten ist, wird diese Methode wiederum
zeilenweise abgearbeitet.
cont
Setzt Programmausführung nach einem
Breakpoint fort.
quit
Beendet den Debugger und die Ausführung
des Programmes.
∗ Anzeige von Programmwerten
Die folgenden Kommandos erlauben es, programminterne Daten zu inspizieren.
Befehl
Auswirkung
dump <id>
Zeigt alle Information über ein Objekt an.
print <id>
Zeigt eine Variable an.
locals
Zeigt alle lokal verfügbaren Variablen an.
where
Zeigt
die
aktuelle
MethodenaufrufHierarchie an.
list
Zeigt die aktuelle Quellprogrammzeile plus
„etwas Umgebung“ an.
1.2.3
Klassennamen und Packages
Das Java-Laufzeitsystem muß Klassen (d.h. ihre Bytecode-Dateien) anhand des Klassennamens zur Laufzeit finden und laden können. Dies gilt für selbst programmierte Klassen ebenso
wie für Standard-Klassen, die bestimmte Grundfunktionen wie z.B. Datei-Ein/Ausgabe bereitstellen. Da die Klassen nicht auf ein bestimmtes Programm beschränkt sind, müssen ihre
Namen auch über Anwendungsgrenzen hinweg eindeutig sein.
Dies erfordert eine geeignete Strukturierung von Klassennamen, einfache Identifikatoren
reichen in der Praxis nicht mehr aus. Daher gruppiert Java alle Klassen in sogenannte
„Packages“.
8
1.2. PROGRAMME
Packages
Jede Klasse gehört grundsätzlich einem Package an. Diese Zugehörigkeit wird zu Beginn jeder
Java-Quelldatei durch eine package-Anweisung festgelegt:
package <Package-Name>;
Die Anweisung darf nur einmal je Quelldatei auftreten und gilt immer für alle Klassen, die in
der Datei definiert werden.
• Package-Namen
Package-Namen bestehen i.a. aus mehreren Identifikatoren, die durch Punkte „.“ getrennt
sind.
Beispiele für die Deklaration von Package-Namen
package mtj;
package mtj.level1;
• Struktur
Der Aufbau der Package-Namen bedeutet nicht, daß die Packages hierarchisch organisiert
sind. Das Package java.lang.reflect ist also nicht Teil des Packages java.lang.
• Default-Package
Enthält eine Quelldatei keine package-Anweisung, so gehören alle darin definierten
Klassen einem speziellen Default-Package mit leerem Package-Namen an.
Vollständige Klassennamen
Jeder vollständige („qualified“) Klassenname besteht aus einem Package-Namen und einem
durch einen Punkt abgetrennten Identifikator.
Beispiele für vollständige Klassennamen
java.lang.String
java.lang.Object
java.lang.reflect.Method
mtj.Station
• Schreibweise
Groß/Kleinschreibung macht einen Unterschied, die Namen mtj.Station und
mtj.station bezeichnen also unterschiedliche Klassen. Eine weitere Bedeutung hat die
Groß/Kleinschreibung nicht. Es ist allerdings üblich, die Teile des Package-Namens alle
klein zu schreiben und den Klassen-Identifikator mit einem Großbuchstaben zu beginnen.
• Default-Package
Der Klassenname einer Klasse im Default-Package (s.o.) besteht nur aus dem
Klassenidentifikator.
9
KAPITEL 1. GRUNDLAGEN
Vom Klassennamen zur Bytecode-Datei
Wie findet nun das Java-Laufzeitsystem die Bytecode-Datei, wenn ihm nur der vollständige
Klassenname (aufgeteilt in Package-Name und Rest) bekannt ist? Der Klassenname wird in
einen relativen Dateinamen übersetzt nach dem Schema (unter Unix):
pack1.pack2.pack3.Klasse
->
pack1/pack2/pack3/Klasse.class
• Pfadnamen
Die Teile des Package-Namens werden also als Verzeichnisnamen interpretiert und aus
dem Klassen-Identifikator wird der Dateiname konstruiert.
Beispiele
java.lang.String
java.lang.Object
java.lang.reflect.Method
mtj.Station
->
->
->
->
java/lang/String.class
java/lang/Object.class
java/lang/reflect/Method.class
mtj/Station.class
Bei Klassen im Default-Package entfällt der Verzeichnisname, da der Package-Name leer
ist.
• CLASSPATH
Der so ermittelte relative Dateiname wird dann in allen Verzeichnissen gesucht, die in
der Umgebungsvariablen CLASSPATH angegeben sind.
– Angabe von Verzeichnissen
CLASSPATH muß dazu eine Folge von Verzeichnisnamen enthalten, die durch
Doppelpunkt (unter Unix) bzw. Semikolon (unter Windows) getrennt sind. Die
Verzeichnisse werden in der Reihenfolge, in der sie in CLASSPATH stehen, durchsucht,
und die erste gefundene Datei wird verwendet. Für Klassen im Default-Package
muß die Bytecode-Datei also direkt in einem der in CLASSPATH angegebenen
Verzeichnisse liegen. Sie können Ihr jeweils aktuelles Arbeitsverzeichnis zum
Verzeichnis für das Default-Package machen, indem Sie den Verzeichnisnamen „.“
als erstes in CLASSPATH einfügen.
– Archivdateien
Anstelle eines Verzeichnisses kann in CLASSPATH auch eine Archivdatei angegeben
sein, die einen Verzeichnisbaum enthält. Zulässig sind hier zip-Archive und die
jar-Archive. Jar-Archive sind ebenfalls zip-Archive mit einem Unterverzeichnis
"META-INF".
– Standardverzeichnisse
Üblicherweise kennt das Java-System zusätzlich zu den in CLASSPATH angegebenen
Verzeichnissen noch gewisse Verzeichnisse, in denen Standard-Packages wie
java.lang liegen. Diese Verzeichnisse müssen also nicht mit in CLASSPATH
aufgenommen werden.
10
1.2. PROGRAMME
– Compiler-Aufruf
Auch bei einem Übersetzer-Aufruf ist es i.a. erforderlich, daß der CLASSPATH
definiert ist. Die zu übersetzende Datei wird zwar explizit im javac-Aufruf
angegeben. Darin verwendete Klassen aus anderen Dateien werden jedoch mit Hilfe
des CLASSPATH gesucht und es wird geprüft, ob sie existieren und ob sie in korrekter
Weise verwendet werden.
Unvollständige Klassennamen
Unter gewissen Umständen kann bei der Angabe einer Klasse der Package-Name weggelassen
werden.
• Fälle
Dies ist in folgenden Fällen zulässig:
– für alle Klassen im Package java.lang,
– wenn die Angabe in einer Quelldatei steht, die zum gleichen Package gehört,
– wenn die angegebene Klasse mit einer import-Anweisung in der Quelldatei
importiert wurde.
• import-Anweisung
import-Anweisungen können am Anfang einer Quelldatei (nach der package-Anweisung)
stehen. Sie haben die Form
import <vollstaendiger Klassenname>;
oder
import <Package-Name>.∗;
– Im zweiten Fall werden alle Klassen des angegebenen Packages importiert. Weitere
Packages, deren Name mit Package-Name beginnt, sind nicht betroffen. Die
Anweisung import java.lang.*; importiert also nur die Klassen aus java.lang,
nicht die Klassen aus java.lang.reflect.
– Eindeutigkeit der Klassennamen
Importiert man Klassen mit dem gleichen Identifikator aus verschiedenen Packages,
so ist ihr Identifikator in der Quelldatei nicht mehr eindeutig. In diesem Fall muß
man weiterhin den vollständigen Klassennamen verwenden.
Beispiele für import-Anweisungen
import java.util.Vector;
import java.util.∗
11
KAPITEL 1. GRUNDLAGEN
Zugriffsbeschränkungen
Unabhängig von der Schreibweise des Klassennamens darf nicht jede Klasse an jeder Stelle
verwendet werden. Normalerweise ist die Verwendung einer Klasse nur in dem Package möglich,
dem sie angehört.
• Öffentliche Klassen
Um eine Klasse von einem anderen Package aus zu verwenden, muß sie als öffentlich
(„public“) erklärt werden. Dazu wird das Schlüsselwort public der Klassendefinition
vorangestellt:
1
2
3
public class <Identifikator> {
...
}
– Bezug zur Quelldatei
Für öffentliche Klassen gilt die Einschränkung, daß ihr Identifikator mit dem Namen
der Quelldatei übereinstimmt, in der sie definiert wird. Ist die Klasse Station
also öffentlich, so muß sie in der Quelldatei Station.java definiert werden. Als
Konsequenz kann in jeder Quelldatei höchstens eine öffentliche Klasse definiert
werden.
– main-Methode
Der Aufruf der main-Methode einer Klasse als Programmlauf mit dem javaKommando ist immer möglich, unabhängig davon ob die Klasse öffentlich ist oder
nicht (die main-Methode muß allerdings öffentlich sein, s.u.).
1.3
Programmstruktur in MTJ
Wir wollen für das im Praktikum entwickelte System mehrere Packages verwenden, die
verschiedenen Entwicklungsstufen des Systems entsprechen. Die Packages heißen mtj.level1,
mtj.level2, etc.
1.3.1
Verzeichnisstruktur
Legen Sie sich dazu ein Verzeichnis $HOME/java an, speichern sie alle Quelldateien und
Bytecode-Dateien im Unterverzeichnis
$HOME/java/mtj/leveli
(wobei i die jeweilige Stufennummer ist) und setzen sie die CLASSPATH-Umgebungsvariable
auf $HOME/java. Sie können dann jede selbstgeschriebene Klasse Xyz über den Namen
mtj.leveli.Xyz ansprechen. Insbesondere können Sie einen Programmlauf der main-Methode
der Klasse Xyz auf Stufe 1 mit dem folgenden Kommando starten (unabhängig vom aktuellen
Verzeichnis, in dem Sie das Kommando aufrufen).
java mtj.level1.Xyz
12
1.4. KLASSEN
1.3.2
Dateistruktur
Für jede Klasse auf Stufe 1 (und entsprechend den weiteren Stufen) soll eine eigene Quelldatei
angelegt werden. Damit die Quelldatei der Stufe 1 angehört, muß sie im entsprechenden
Verzeichnis angelegt werden und mit der folgenden Anweisung beginnen.
package mtj.level1;
1.3.3
Hauptprogramme
Zusätzlich zu den jeweils benötigten Klassen soll für jede Aufgabe, die ein eigenes
Hauptprogramm erfordert, eine Klasse AufgNNx (wobei NNx für die Aufgabennummer steht)
verwenden, die die main-Methode enthält.
1.3.4
MTJ - Stufe 1
In Stufe 1 wird eine erste einfache Implementierung der Stationen entwickelt. Dabei geht es
nur um den Aufbau von Konfigurationen und um die Übertragung von Ereignissen zwischen
Stationen. Spezielle Aufgaben der Stationen werden noch nicht betrachtet.
Klassen in MTJ Stufe 1
Wir verwenden in Stufe 1 drei Klassen
• GenStation
für Generatorstationen
• ProcStation
für Arbeitsstationen
• Event
für Ereignisse
Vollständige Klassennamen wären also beispielsweise mtj.level1.Aufg02 und
mtj.level2.Aufg03b. Das Hauptprogramm zu Aufgabe 2 kann mit dem Kommando
java mtj.level1.Aufg02
gestartet werden.
1.4
Klassen
Betrachten wir nun eine einzelne Klasse und ihre Bestandteile („member“).
Eine Klasse kann drei Arten von Bestandteilen haben: Variable („variable“, auch: „field“),
Methoden und Klassen. Wir betrachten hier vorerst nur die beiden ersten Fälle.
13
KAPITEL 1. GRUNDLAGEN
1.4.1
Datentypen
Datentypen werden verwendet, um Klassenbestandteile näher zu beschreiben.
Arten
Java läßt nur drei Arten von Datentypen zu.
• einen der „primitiven“ Typen boolean, char, short, int, long, float, double
• eine Klasse (angegeben durch ihren Klassennamen)
• eine Reihung („array“) von Elementen eines Datentyps (angegeben durch die
Typbezeichnung der Elemente, gefolgt von „[] “)
Beispiele für Typbezeichnungen
Da String und Integer vordefinierte Klassen (im Package java.lang) sind, sind folgende
Angaben zulässige Typbezeichnungen:
String
int
boolean[]
Integer[]
String[][]
Datenwerte
Jeder Datentyp beschreibt eine Menge von möglichen Datenwerten, der Typ int beispielsweise
die Menge aller ganzen Zahlen, die sich mit 32 Bit darstellen lassen. Für gewisse Datentypen
können Werte durch „Literale“ angegeben werden.
Beispiele für Literale
Für die Typen short, int und long sind dies beispielsweise Zahlangaben in Dezimalschreibweise (z.B. 8748), für den Typ boolean sind es die Identifikatoren true und false, für den
Typ char sind es Zeichen in einfachen Anführungszeichen (z.B. ’a’). Für alle Klassen und
Reihungstypen ist das Literal null zulässig und für die Klasse String sind Zeichenreihen in
doppelten Anführungszeichen (z.B. "Dies ist ein String") als Literale zulässig.
1.4.2
Variable
Eine Variable entspricht einem Platz für einen Datenwert eines bestimmten Typs.
Definition
Variable werden im einfachsten Fall in der Form
<Typbezeichnung> <Identifikator>, ... , <Identifikator>;
definiert.
Beispiele für Variablendefinitionen
14
1.4. KLASSEN
int i,j;
Integer[] zahlfeld1;
Initialisierung
Bei der Definition einer Variablen kann optional eine Initialisierung angegeben werden. Damit
wird der Wert festgelegt, der zu Beginn in der Variablen abgelegt ist. Die Definition hat dann
die Form
<Typbezeichnung> <Identifikator> = <Ausdruck>, ...;
Variablen mit und ohne Initialisierung können in der gleichen Definition beliebig gemischt
werden.
Der Ausdruck ist im einfachsten Fall ein Literal, also beispielsweise definiert
int i = 5;
eine Variable i, die mit dem Wert 5 vorbelegt ist.
Konstante
Man kann weiter festlegen, daß der Wert einer Variablen nach der Initialisierung nicht mehr
geändert werden darf. Dies geschieht durch Voransetzen des Schlüsselworts final vor die
Definition, wie in
final int antwort = 42;
Solche „Variable“ sind also eigentlich (benannte) Konstante.
1.4.3
Methoden
Eine Methode entspricht einem Programmstück (Prozedur, Funktion). Eine Methode hat i.a.
Eingabeparameter und kann ein Ergebnis liefern. Eine Klasse kann Definitionen mehrerer
Methoden enthalten. Es gibt drei unterschiedliche Arten von Methoden, die im folgenden
beschrieben werden. Methoden können nicht ineinander geschachtelt werden, sondern sind
immer direkt in einer umgebenden Klasse definiert.
Zugriffsmethoden
Die allgemeinste Art von Methoden sind Zugriffsmethoden („access methods“, auch einfach
als „Methode“ bezeichnet). Sie werden in der Form
<Ergebnistyp> <Identifikator>(<Typ> <Parametername>, <Typ> <Parametername>, ...)
{ ... }
definiert. Falls die Methode kein Ergebnis liefern soll, muß als Ergebnistyp der Pseudotyp
void angegeben werden.
Beispiel für eine Methodendefinition
int addiere(int i1, int i2)
{ return i1 + i2; }
15
KAPITEL 1. GRUNDLAGEN
Beispiel für eine Methode ohne Parameter
int uhrzeit()
{ ... }
• main-Methode
Die in Abschnitt → (siehe Seite 3) beschriebene Methode main hat kein Ergebnis und
als Parameter eine Reihung von Strings. Ihre Definition muß also die Form
void main(String[] args)
{ ... }
haben. 1 Nur eine solche main-Methode kann als Hauptprogramm aufgerufen werden.
Bei anderen Ergebnis- oder Parametertypen ist dies nicht möglich. Die String-Reihung
von main dient zur Übergabe der Kommandozeilenparameter im java-Kommando.
Initialisierer
Eine spezielle Art von Methoden sind Initialisierer („initializer“). Sie haben keine Parameter,
liefern kein Ergebnis und können auch nicht explizit aufgerufen werden. Sie werden stattdessen
in bestimmten Situationen automatisch abgearbeitet.
• Definition
Entsprechend hat die Definition eines Initialisierers einfach die Form
{ ... }
• Initialisierung von Variablen
Die bei Variablendefinitionen angegebenen Initialisierungen können als spezielle Form
von Initialisierern aufgefaßt werden. Initialisierer dienen allgemein dazu, Variable
mit Werten vorzubelegen. Mit einem Initialisierer können jedoch mehrere Variable
gleichzeitig belegt werden und es sind kompliziertere Berechnungen möglich, als in einem
einzelnen Ausdruck.
Initialisierung zweier Variablen mit einem Initialisierer
1
2
3
4
5
6
int i1;
boolean b1;
{
i1 = 5;
b1 = (i1 > 10);
}
1
Zusätzlich muß sie als public (vgl. Abschnitt → (siehe Seite 17)) und static (vgl. Abschnitt → (siehe
Seite 24)) definiert sein!
16
1.4. KLASSEN
Konstruktoren
Konstruktoren („constructors“) sind ebenfalls eine spezielle Form von Methoden. 2 Sie liefern
kein Ergebnis, aber sie können wie Zugriffsmethoden Parameter haben. Außerdem muß der
Name mit dem Namen der umgebenden Klasse übereinstimmen.
• Definition
Entsprechend werden Konstruktoren in der Form
<Identifikator der Klasse>(<Typ> <Parametername>, <Typ> <Parametername>, ...)
{ ... }
definiert.
Overloading
Es kann in einer Klasse mehrere Methoden mit dem gleichen Identifikator geben, falls sie
sich durch ihre Parameterlisten unterscheiden. Dazu müssen die Methoden entweder eine
unterschiedliche Anzahl von Parametern haben, oder mindestens eine Parameterposition muß
sich im Datentyp unterscheiden. Dies wird als „overloading“ bezeichnet. Es ist sowohl für
Zugriffsmethoden als auch für Konstruktoren erlaubt.
Ein Beispiel für overloading sind die unterschiedlichen Methoden String.valueOf. Es gibt
einparametrige Methoden dieses Namens u.a. für Parameter der Datentypen int, char, char[]
und Object. Alle liefern eine Darstellung ihres Parameters als String.
1.4.4
Zugriffsbeschränkungen
Wie im Fall von Klassen dürfen auch Klassenbestandteile nicht von beliebigen Stellen aus
benutzt werden.
Im Normalfall darf eine Variable oder Methode von beliebigen Stellen im gleichen Package aus
benutzt werden.
• Öffentliche Bestandteile
Um Klassenbestandteile von anderen Packages aus benutzen zu können, müssen sie als
öffentlich erklärt werden, indem public vor die Definition geschrieben wird. Zusätzlich
muß die enthaltende Klasse öffentlich sein.
• Private Bestandteile
Der Zugriff kann umgekehrt auch weiter eingeschränkt werden. Wird ein Klassenbestandteil als privat erklärt (indem private vor die Definition geschrieben wird), so kann
er nur noch innerhalb der gleichen Klasse benutzt werden, also im Bereich zwischen den
Klammern { ... } der Klassendefinition.
• Initialisierer
Initialisierer können nicht explizit benutzt (aufgerufen) werden, daher sind für sie
Zugriffsbeschränkungen irrelevant und können nicht angegeben werden.
2
Ihre Verwendung wird später erklärt.
17
KAPITEL 1. GRUNDLAGEN
Beispiele für Definitionen von Klassenbestandteilen mit speziellen Angaben zur Zugriffsbeschränkung sind
public String Name;
private int zaehle(int i)
{ ... }
public void arbeite(String eingabe)
{ ... }
1.5
Variable und Methoden in MTJ Stufe 1
Betrachten wir nun, welche Variablen und Methoden wir in den MTJ-Klassen GenStation
und ProcStation auf der Stufe 1 benötigen. Hier sind zwei Aspekte zu implementieren:
die statische Vernetzung der Stationen (die „Leitungen“) und der dynamische Ereignisfluß
zwischen den Stationen.
1.5.1
Vernetzung der Stationen
Die statischen Beziehungen zwischen Stationen in einer Konfiguration lassen sich grafisch
veranschaulichen.
Abbildung 1.4: Die statischen Beziehungen zwischen Stationen
• UML
In dieser Abbildung und den weiteren im Praktikum verwenden wir UML-Notation.
UML (Unified Modeling Language) ist eine graphische Darstellungssprache für die
Modellierung objektorientierter Systeme. Wir werden nicht im einzelnen auf die
Elemente von UML eingehen, sondern nur die wichtigsten beim Auftreten jeweils kurz
erläutern.
Die connected-Relation
In der UML-Darstellung ist die Verbindungsbeziehung zwischen Generatorstationen und
Arbeitsstationen als connected-Relation dargestellt. Generatorstationen treten dabei nur als
Ereignisquellen („sender“) auf, Arbeitsstationen können sowohl Ereignisse senden als auch
erwarten („listener“).
18
1.5. VARIABLE UND METHODEN IN MTJ STUFE 1
Vereinfachte Verbindung
In dieser ersten Version wollen wir vereinfachte Verbindungen realisieren, in denen jede Station
höchstens eine Nachfolgerstation besitzt. Entsprechend sind als Anzahlen beim Relationsende
sender „1“ angegeben und beim Relationsende listener „0..1“ (eine Station muß keinen
Nachfolger haben). Mit dieser Festlegung lassen sich natürlich nur lineare Ketten von Stationen
realisieren. Wir werden dies auf der nächsten Stufe verallgemeinern.
Implementierung durch doppelte Verkettung
Zur Implementierung der statischen Vernetzung wollen wir eine doppelte Verkettung
verwenden. Es soll also Zugang bestehen von jeder Station zum Nachfolger und umgekehrt
von jeder Station zum Vorgänger. Dies wird in der Abbildung durch die beiden Pfeilspitzen
an den Relationen ausgedrückt.
Variable für die Verkettung
Für die Verkettung benutzen wir jeweils eine Variable, die eine Station aufnehmen kann.
• Nachfolgestation
Nachfolgestationen sind immer Arbeitsstationen, also hat die entsprechende Variable
den Datentyp ProcStation:
private ProcStation eventListener;
• Vorgängerstation
Die Vorgängerstation ist entweder eine Arbeits- oder eine Generatorstation. Wir
verwenden für die Verkettung zwei Variable, eine vom Typ ProcStation und eine vom
Typ GenStation:
private GenStation eventSenderGen;
private ProcStation eventSenderProc;
Von diesen soll immer nur höchstens eine mit einer Station besetzt sein. Außerdem haben
Generatorstationen grundsätzlich keinen Vorgänger, die beiden Variablen sind also nur
in der Klasse ProcStation enthalten.
• Zugriffsbeschränkung
Die Variablen sind als private deklariert, sie sind also nur von Methoden in der gleichen
Klasse aus zugänglich. Auf diese Weise lassen sich die externen Zugriffe auf die Variablen
auf bestimmte Operationen einschränken, die in der Form von Methoden implementiert
werden.
Methoden für die Verkettung
Wir wollen drei Operationen zum Auf- und Umbau der statischen Konfiguration anbieten.
Dazu definieren wir jeweils eine Methode.
19
KAPITEL 1. GRUNDLAGEN
• addEventListener(ProcStation listener)
baut eine Verbindung auf zu einer als Parameter angegebenen ProcStation als neuer
Nachfolger der Station.
• releaseFromListener()
löst die Verbindung zur Nachfolgestation (falls vorhanden).
• releaseFromSender()
löst die Verbindung zur Vorgängerstation (falls vorhanden).
Hilfsoperationen
Wegen der doppelten Verkettung muß in addEventListener nicht nur der neue Nachfolger in
die Variable eventListener der Station eingetragen werden, sondern die Station muß auch in
einer der eventSender-Variablen der Nachfolgestation vermerkt werden.
Um einen direkten Zugriff auf die Variable der Nachfolgestation zu vermeiden, verwenden wir
hierzu eine Hilfsmethode die nur die „Rückverkettung“ behandelt.
Der Ablauf von addEventListener
Die Abbildung zeigt den Ablauf von addEventListener am Beispiel der Verknüpfung zweier
Arbeitsstationen.
Abbildung 1.5: Der Ablauf von addEventListener
Der Aufrufer (hier das Hauptprogramm main) gibt die anzuhängende Station l1 als Parameter
mit. Die ausführende Station s1 ruft die Hilfsmethode setSender bei l1 auf und gibt sich selbst
als Parameter mit.
• Hilfsmethoden
Entsprechende Hilfsmethoden brauchen wir für alle drei Operationen. Im Fall, daß die
Vorgängerstation zu setzen ist, brauchen wir zwei Varianten, je nachdem ob es sich
beim Vorgänger um eine Arbeits- oder Generatorstation handelt. Dabei verwenden wir
overloading und definieren die Methode setSender einmal für einen Parameter vom Typ
ProcStation und einmal für einen Parameter vom Typ GenStation.
20
1.5. VARIABLE UND METHODEN IN MTJ STUFE 1
– setSender(ProcStation sender)
trägt die als Parameter angegebene Arbeitsstation als neuen Vorgänger ein
(aufgerufen von addEventListener).
– setSender(GenStation sender)
trägt die als Parameter angegebene Generatorstation als neuen Vorgänger ein
(aufgerufen von addEventListener).
– detachSender()
entfernt die Vorgängerstation aus der Variablen eventSenderProc bzw.
eventSenderGen (aufgerufen von releaseFromListener).
– detachListener()
entfernt die Nachfolgerstation aus der Variablen eventListener (aufgerufen von
releaseFromSender).
Generatorstationen
Zu beachten ist, daß der vollständige Satz von Operationen nur für Arbeitsstationen
erforderlich ist. Generatorstationen benötigen nur die Operationen addEventListener,
releaseFromListener und detachListener.
1.5.2
Ereignisfluß
In einer aufgebauten Konfiguration müssen Ereignisse erzeugt, weitergeleitet und behandelt
werden.
Weiterleitung
Die Weiterleitung zwischen den Stationen soll durch die Methode sendEvent realisiert werden.
• sendEvent(Event e)
leitet ein als Parameter angegebenes Ereignis an den Nachfolger der Station weiter (falls
existent) und veranlaßt dort ihre Bearbeitung, indem die Methode handleEvent beim
Nachfolger aufgerufen wird.
Behandlung
Die Ereignisbehandlung geschieht durch eine Methode, die bei jeder Arbeitsstation definiert
ist.
• handleEvent(Event e) verarbeitet das als Parameter übergebene Ereignis in
spezifischer Weise je nach Art der Arbeitsstation. Die Verarbeitung beinhaltet ggf. auch
das Senden weiterer Ereignisse an Nachfolgestationen.
• Vereinfachte Behandlung
Wie bei der Verkettung implementieren wir hier vorerst eine vereinfachte Version. Die
Methode handleEvent soll für alle Stationen und alle Ereignisse das gleiche tun: melden,
21
KAPITEL 1. GRUNDLAGEN
daß ein Ereignis angekommen ist und es mittels sendEvent weiterschicken. Auf der
nächsten Stufe werden wir zu einer spezifischen Ereignisbehandlung übergehen.
Abbildung 1.6: Der Ablauf von sendEvent
Die Abbildung zeigt den Ablauf. Die Methode sendEvent wird typischerweise von der
sendenden Station selbst aufgerufen und resultiert in einem handleEvent-Aufruf beim
Nachfolger mit Übergabe des Ereignisses.
Ereigniserzeugung
Generatorstationen erzeugen eine feste endliche Folge von Ereignissen. In der vereinfachten
Version auf Stufe 1 soll es sich nur um ein einziges Ereignis handeln.
• Variable
Zu diesem Zweck wird eine Variable
private Event event;
verwendet, die das Ereignis speichert.
• generateEvent()
ist eine Methode bei allen Generatortstationen, die bei Aufruf das Ereignis aus event
durch einen Aufruf von sendEvent verschickt. Daneben soll sie auf Stufe 1 eine
entsprechende Meldung ausgeben.
1.5.3
Stationsnamen
Eine Konfiguration aus Stationen kann mehrere gleichartige Stationen enthalten.
Um Stationen individuell unterscheiden zu können, wollen wir jeder Station noch einen String
als Identifikator („Stationsname“) zuordnen.
• Variable
Dies geschieht mit Hilfe der Variablen
private String name;
22
1.6. OBJEKTE
in jeder Station.
3
• String getName()
ist eine Methode, die den Wert der Variablen name als Ergebnis liefert.
• Keine Eindeutigkeit
Wir wollen nicht erzwingen, daß Stationsnamen in einer Konfiguration eindeutig sein
müssen. Die Namen dienen nur als Bezeichnung in Ausgabe-Meldungen. Falls eine
Station keine Ausgabe-Meldungen erzeugt ist der Name irrelevant.
Zusammenfassung
Zusammenfassend können wir die drei Klassen in UML-Notation mit ihren Bestandteilen wie
in der folgenden Abbildung darstellen.
Abbildung 1.7: Klassen in UML-Notation
Jeder Kasten stellt eine Klasse dar. Im ersten Abschnitt unter dem Namen sind die Variablen
aufgeführt, im nächsten die Methoden. Man kann Klassen auch ohne ihre Bestandteile
darstellen, wie beispielsweise in der → Abbildung zur connected-Relation (siehe Abbildung
1.4).
1.6
Objekte
In objektorientierten Programmiersprachen ist ein Objekt eine Kombination aus Speicher
und zugehörigen Programmteilen, es faßt also Daten und Operationen zu ihrer Bearbeitung
zusammen. In Java entspricht dem Speicher eines Objekts eine Menge von Variablen und den
Operationen eine Menge von Methoden.
3
Die Verwendung der Variablen wird später noch näher beschrieben.
23
KAPITEL 1. GRUNDLAGEN
1.6.1
Klassen und Objekte
Häufig benötigt man mehrere Objekte mit dem gleichen Aufbau. Dann ist es nützlich, nicht für
jedes einzelne Objekt die Variablen und Methoden erneut zu definieren, sondern nur einmal
für alle gleichartigen Objekte.
Diese Möglichkeit bieten die Klassen. Eine Klasse ist gewissermaßen eine Schablone, nach deren
Vorbild Objekte erzeugt werden können. In Java spielen die Klassen jedoch eine Doppelrolle.
Zum einen sind sie Schablonen für Objekte, zum anderen können sie auch selbst Variable und
Methoden enthalten. In der zweiten Rolle ähneln sie also den Objekten.
Statische und nichtstatische Klassenbestandteile
Die beiden Rollen einer Klasse werden dadurch organisiert, daß für jeden Klassenbestandteil
angegeben werden kann, ob er zur Klasse selbst gehört oder ob er eine Schablone für einen
entsprechenden Bestandteil bei den Objekten sein soll.
• Angabe
Normalerweise sind die Bestandteile Schablonen und werden auch als Instanzbestandteile
(„instance members“) bezeichnet. Bestandteile, die zu der Klasse selbst gehören, werden
als statische Bestandteile („static members“) bezeichnet. Um einen Bestandteil als
statisch zu deklarieren wird einfach das Schlüsselwort static vor die Definition
geschrieben.
Beispiele
static String helpstring = "Dies ist die Erlaeuterung";
private static int maxelem = 1000;
• Klassenaufbau
Man kann sich also jede Klasse aus zwei Teilen aufgebaut denken: aus dem statischen
Teil, der alle statischen Bestandteile umfaßt und aus dem Schablonenteil, der die übrigen
Bestandteile enthält (vgl. Abbildung 1.8).
24
1.6. OBJEKTE
Abbildung 1.8: Statischer und Schablonenteil einer Klasse
• Methodenarten
Konstruktoren gehören grundsätzlich zum Schablonenteil, dürfen also nicht als statisch
deklariert werden. Alle übrigen Bestandteile (auch Initialisierer) können entweder
statisch sein oder zur Schablone gehören.
Statischer Klassenteil als Objekt
Der statische Teil einer Klasse hat den gleichen Aufbau wie ein Objekt.
• Unterschiede zwischen Klassen und Objekten
Es bestehen die folgenden wesentlichen Unterschiede zwischen statischen Klassenteilen
und Objekten:
– Der statische Klassenteil existiert in jedem Programmlauf genau einmal (sofern die
Klasse überhaupt verwendet wird), Objekte gemäß der Schablone kann es beliebig
viele geben oder keines
– Der statische Klassenteil ist über den Klassennamen ansprechbar, Objekte haben
keinen festen Namen
– Ein statischer Klassenteil existiert ab dem Zeitpunkt, zu dem die Klasse vom JavaLaufzeitsystem geladen wurde. Es reicht also, die Klasse zu definieren. Objekte
müssen dagegen explizit durch eine Anweisung im Programm erzeugt werden
25
KAPITEL 1. GRUNDLAGEN
• Bereitstellung zur Laufzeit
Speziell bedeutet das, daß die statischen Bestandteile einer Klasse jederzeit benutzbar
sind, sofern die Klasse definiert und übersetzt wurde. Falls im Programmlauf bisher keine
andere Benutzung der Klasse stattfand lädt das Laufzeitsystem die Klasse. Danach ist
für jede statische Variable Speicherplatz reserviert und jede statische Methode kann
aufgerufen werden.
• main-Methode
Ein spezielles Beispiel für einen statischen Bestandteil ist die → Methode main (siehe
Seite 3). Sie wird beim Start eines Programmlaufs aufgerufen und muß daher bereits mit
der Definition ihrer Klasse benutzbar sein. Die korrekte Definition einer main-Methode
muß also immer lauten
public static void main (String[] args)
{ ... }
Echte Objekte
Schablonenbestandteile können erst benutzt werden, wenn ein Objekt erzeugt wurde. Da es
mehrere Objekte zur gleichen Klasse geben kann, muß darüberhinaus auch jeweils angegeben
werden, bei welchem Objekt der Bestandteil benutzt werden soll. Für eine Instanzvariable
wird bei jedem erzeugten Objekt ein eigener Speicherplatz reserviert.
Definition: Die zu einer Klasse erzeugten Objekte werden auch als Instanzen der Klasse
bezeichnet, der Vorgang der Erzeugung eines Objekts als Instanzierung der Klasse.
Abstrakte Klassen
Manchmal macht es Sinn, Klassen zu definieren, die nie instanziert werden sollen,
beispielsweise weil nur der statische Teil relevant ist.
Definition: Solche Klassen werden als abstrakte Klassen bezeichnet.
In Java besteht die Möglichkeit, für eine Klasse explizit festzulegen, daß sie nicht instanziert
werden darf. Dies geschieht durch Voranstellen des Schlüsselworts abstract vor die
Klassendefinition. Ein Beispiel ist
abstract class Xyz
{ ... }
Beim Versuch, die Klasse Xyz zu instanzieren (s.u.) meldet der Java-Compiler einen Fehler.
1.6.2
Umgang mit statischen Klassenteilen
Jeder statische Klassenteil wird beim Laden der Klasse angelegt (es wird Speicher für die
statischen Variablen reserviert und der Code der statischen Methoden im Speicher abgelegt)
und initialisiert (die Initialisierer werden abgearbeitet). Bei der Initialisierung werden alle
in der Klasse definierten (statischen) Initialisierer (einschließlich der in Variablendefinitionen
integrierten) in der Reihenfolge bearbeitet, in der sie in der Klasse aufgeschrieben sind.
26
1.6. OBJEKTE
Statische Klassenbestandteile werden angesprochen, indem ihr Identifikator durch einen Punkt
getrennt hinter den (vollständigen oder unvollständigen) Klassennamen gesetzt wird. Findet
die Verwendung innerhalb der Klasse statt (also zwischen den Klammern { ... } der
Klassendefinition), so kann der Klassenname entfallen und es reicht der Identifikator des
Bestandteils.
Variable
Variable werden benutzt, indem ihnen entweder ein (neuer) Wert zugewiesen wird in der Form
<Variablenname> = <Ausdruck>;
oder indem ihr Wert in einem Ausdruck verwendet wird (durch Angabe des Variablennamens).
Zu der Klassendefinition
1
2
3
4
5
package bsp.pack;
class Bsp {
static int zahl;
static int addiere(int i1, int i2) { ... }
}
kann die Variable zahl innerhalb der Klasse Bsp beispielsweise durch zahl = 17; oder zahl
+ 24 benutzt werden, außerhalb der Klasse durch Bsp.zahl = 17; oder Bsp.zahl + 24 bzw.
durch bsp.pack.Bsp.zahl = 17; oder bsp.pack.Bsp.zahl + 24.
Ein weiteres Beispiel für die Benutzung einer statischen Variablen ist die Verwendung der in der
Klasse java.lang.System definierten Variablen out vom Typ PrintStream durch Verwendung
des Namens System.out oder java.lang.System.out in einem Ausdruck. (Eine Zuweisung
System.out = ... ist nicht erlaubt, da die Variable als final definiert ist.)
Methoden
Methoden werden benutzt, indem sie mit Parametern aufgerufen werden:
<Methodenname>(<Ausdruck>, <Ausdruck>, ...)
Zu obiger Klassendefinition kann ein Aufruf innerhalb der Klasse beispielsweise
addiere(17,4) lauten, außerhalb der Klasse beispielsweise Bsp.addiere(17,4) bzw.
bsp.pack.Bsp.addiere(17,4).
Ein weiteres Beispiel ist der Aufruf der in der Klasse java.lang.String definierten statischen
Methode valueOf, die zu einem Wert vom Typ int eine Stringdarstellung als Ergebnis liefert:
String.valueOf(15)
String.valueOf(17 + 4)
java.lang.String.valueOf(bsp.pack.Bsp.addiere(99,1))
Zusammenfassend entsprechen also statische Variable in Java den globalen Variablen in
anderen Programmiersprachen, mit dem Unterschied, daß sie in Klassen gruppiert sind
und ihr Name nur zusammen mit dem vollständigen Klassennamen eindeutig sein muß.
Statische Methoden entsprechen den (nichtlokalen) Prozeduren und Funktionen in anderen
Programmiersprachen, wobei ebenfalls die Gruppierung in die Klassen hinzukommt. In Bezug
auf die statischen Bestandteile spielen die Klassen in Java die Rolle von Modulen: Sie fassen
27
KAPITEL 1. GRUNDLAGEN
jeweils globale Variable und Prozeduren und Funktionen zu einer eigenständigen Gruppe
zusammen.
1.6.3
Umgang mit Objekten
Objekte werden explizit erzeugt und über Referenzen angesprochen.
UML-Darstellung für Objekte
Die UML-Darstellung zeigt ein Objekt und seine zugehörige Klasse. Objekte werden in UML
wie Klassen dargestellt, mit dem Unterschied, daß ihr Name unterstrichen ist. Der Name
besteht aus einem individuellem Bezeichner für das Objekt, dem mit einem Doppelpunkt der
Name der Klasse angefügt ist. Der individuelle Bezeichner kann wie in der Abbildung entfallen,
wenn er nicht relevant ist.
Abbildung 1.9: Ein Objekt und seine zugehörige Klasse
Objekterzeugung
Objekte werden erzeugt durch Auswertung eines Konstruktorausdrucks der Form
new <Klassenname>(<Ausdruck>, <Ausdruck>, ...)
• Initialisierung
Dabei wird für die Instanzvariablen des Objekts Speicherplatz angelegt und sie werden
initialisiert. (Der Code für die Instanzmethoden ist für alle Objekte einer Klasse gleich
und wird daher nur einmal im Schablonenteil der Klasse abgelegt, nicht bei jedem
Objekt.) Die Initialisierung geschieht zuerst analog zur Initialisierung eines statischen
Klassenteils durch Abarbeitung aller (nichtstatischen) Initialisierer. Dieser Vorgang ist
für alle Objekte einer Klasse identisch.
• Konstruktoren
Um bereits bei der Erzeugung von Objekten individuelle Unterschiede in Variablenwerten zu ermöglichen, können die → Konstruktoren (siehe Seite 17) verwendet werden. Am
Ende der Initialisierung wird immer ein Konstruktor ausgeführt. Ihm werden die im Konstruktorausdruck angegebenen Ausdrücke als Parameter übergeben. Auf der Grundlage
28
1.6. OBJEKTE
dieser Parameter kann der Konstruktor dann eine individuelle Besetzung der Instanzvariablen vornehmen.
Beispiel
1
2
3
4
5
6
7
8
class Bsp2 {
int i1 = 0;
int i2 = 17 + 4;
String s;
{ s = String.valueOf(i1 + i2); }
Bsp2 (int i) { i1 = i; }
void aendere (String x) { s = x; }
}
Die Klasse Bsp2 definiert drei Variable, zwei davon mit Initialisierungen, einen
nichtstatischen Initialisierer, einen Konstruktor und eine Zugriffsmethode. Bei jedem
Objekt, das zu der Klasse erzeugt wird, werden zuerst die Variablen i1 und i2 auf den
Wert 0 bzw. 21 gesetzt, dann wird s mit dem String "21" besetzt. Zum Schluß wird der
Wert von i1 auf den im jeweiligen Konstruktorausdruck angegebenen Wert umgesetzt.
Es werden also alle neu erzeugten Objekte der Klasse Bsp2 in den Werten ihrer Variablen
i2 und s übereinstimmen, die Werte der Variablen i1 unterscheiden sich dagegen je nach
verwendetem Konstruktorausdruck. Beispiele für Konstruktorausdrücke sind
new Bsp2 (7)
new Bsp2 (117 ∗ (18 − 3))
– Klassen mit mehreren Konstruktoren
Eine Klasse kann mehrere Konstruktoren haben („overloading“). Welcher jeweils
verwendet wird, richtet sich nach der Anzahl und nach den Datentypen der
im Konstruktorausdruck angegebenen Parameterausdrücke. Ist für eine Klasse
kein Konstruktor definiert, so ergänzt Java automatisch einen Konstruktor ohne
Parameter.
∗ Mitverwendung zwischen Konstruktoren
Ein Konstruktor kann einen anderen Konstruktor der gleichen Klasse
verwenden, um die vorgenommenen Initialisierungen mitzubenutzen. Dazu muß
der Konstruktor-Rumpf mit einer Anweisung der Form
this(<Ausdruck>, <Ausdruck>, ...);
beginnen. Die Anweisung entspricht einem Konstruktorausdruck, in dem
anstelle von new <Klassenname> das Schlüsselwort this steht.
Um in der Klasse Bsp2 einen Konstruktor zu definieren, der i1 und i2
mit Werten aus dem Konstruktorausdruck besetzt, kann der ursprüngliche
Konstruktor wie folgt mitbenutzt werden:
Bsp2(int i, int j) { this(i); i2 = j; }
Objektreferenzen
Im Unterschied zu Klassen haben Objekte keine Namen. Dies liegt daran, daß sie nicht bei
der Aufschreibung des Programms definiert und benannt werden, sondern erst während einem
Programmlauf durch die Abarbeitung von Konstruktorausdrücken erzeugt werden.
29
KAPITEL 1. GRUNDLAGEN
Definition: Objekte werden durch Referenzen angesprochen.
Referenzen sind Werte, die in Variablen gespeichert, als Parameter an Methoden übergeben
und als Ergebniswert von Methoden geliefert werden können. Jedes Objekt in einem
Programmlauf hat genau eine eindeutige Referenz, die die Lage des Objekts im Speicher
angibt (Referenzen sind einfach Speicheradressen).
• UML-Darstellung
In leichter Adaption der UML-Notation stellen wir Referenzen wie in der folgenden
Abbildung dar. Die Zusatzangabe <<value>> soll anzeigen, daß es sich nicht um ein
Objekt, sondern um einen Wert handelt. Wir nehmen in der Abbildung ferner an, daß
Object eine Klasse für beliebige Objekte ist.
Abbildung 1.10: Eine Referenz auf ein Objekt
• Erzeugen von Referenzen
Die Auswertung eines Konstruktorausdrucks liefert die Referenz zum neu erzeugten
Objekt. Wird der selbe Konstruktorausdruck mehrmals ausgewertet, so wird jedesmal
ein neues Objekt mit einer individuellen Referenz erzeugt. Die vom Konstruktorausdruck
gelieferte Referenz stellt die einzige Möglichkeit dar, das erzeugte Objekt zu benutzen.
Üblicherweise wird sie in einer Variablen gespeichert, sie kann aber auch als Parameter
in einem Methodenaufruf verwendet werden.
String s = new String();
i = new Integer (17+4)
String.valueOf(new Integer(125))
• Zugriff auf Objekte mittels Referenz
Um einen Bestandteil eines Objekts anzusprechen, schreibt man im Programm einen
Ausdruck, der die Referenz liefert, gefolgt von dem durch einen Punkt abgetrennten
Identifikator des Bestandteils (der Schablone). Dies ist analog zur Angabe eines
30
1.6. OBJEKTE
statischen Klassenbestandteils, nur tritt der Ausdruck für die Objektreferenz an die
Stelle des Klassennamens.
Der Ausdruck kann beispielsweise der Name einer Variablen sein, in der die Referenz
gespeichert ist, der Aufruf einer Methode, die eine Referenz als Ergebniswert liefert, oder
sogar ein Konstruktorausdruck.
• Umgang mit Objektbestandteilen
Objektbestandteile können in der gleichen Weise benutzt werden wie statische
Klassenbestandteile: Variable können umbesetzt werden oder ihr Wert kann in
Ausdrücken verwendet werden und Methoden können aufgerufen werden.
Setzen wir die obige Definition der Klasse Bsp2 voraus, bezeichnet bvar eine Variable
vom Typ Bsp2 und bezeichnet getbsp eine Methode mit Ergebnistyp Bsp2, so sind
folgendes Beispiele für die Benutzung von Objektbestandteilen:
bvar.i1 = 15;
getbsp("name 1").i2 = 1 + bvar.i1;
(new Bsp2(20)).s = "Unsinn";
bvar.aendere("Sinn");
Das dritte Beispiel macht wenig Sinn, da die Referenz auf das neue Objekt nicht
gespeichert wird und damit nie mehr auf das Objekt zugegriffen werden kann. Im
vierten Beispiel wird die Methode aendere des Objekts aufgerufen, dessen Referenz
in der Variablen bvar gespeichert ist. Da die Methode Bestandteil des Objekts ist, wirkt
sie sich auch speziell auf dieses Objekt aus. Sie ändert nur die Variable s dieses Objekts,
nicht die gleichnamigen Variablen in den übrigen Instanzen von Bsp2.
Ein weiteres Beispiel für die Verwendung eines Objektbestandteils ist
System.out.println("Hello World");
Hier wird die Methode println als Bestandteil des in der Variablen System.out
gespeicherten Objekts aufgerufen. Dieses Objekt ist der Standard-Ausgabestrom des
Programmlaufs. Die Methode gibt den als Parameter übergebenen String auf den Strom
aus, dessen Bestandteil sie ist.
• Methoden als Objektbestandteile
Diese Möglichkeit, Methoden zu einem Bestandteil von Objekten zu machen und
bei einem Aufruf speziell mit diesem Objekt arbeiten zu lassen findet sich nur in
objektorientierten Programmiersprachen.
• Die Referenz this
Bisher haben wir in der Methode aendere nur den Bestandteil s des zugehörigen
Objekts benutzt. Wollen wir dagegen das gesamte Objekt benutzen, so benötigen wir
eine Referenz, um es ansprechen zu können.
Eine solche Referenz liefert der Ausdruck
this
Er ist nur zulässig in nichtstatischen Methoden (auch in Konstruktoren) und liefert
jeweils die Referenz auf das Objekt, dessen Bestandteil die Methode ist.
31
KAPITEL 1. GRUNDLAGEN
– Zugriff auf Bestandteile mittels this
Wie alle Ausdrücke, die eine Referenz liefern, kann this auch dazu verwendet
werden, um Bestandteile des Objekts zu benutzen. Dies ist i.a. nicht notwendig, da
in einer nichtstatischen Methode der Name des Bestandteils bereits ausreicht, um
ihn zu benutzen. Wurde der Name des Bestandteils in der Methode jedoch noch
für einen anderen Zweck verwendet, z.B. als Name für einen Methodenparameter,
so ist die Verwendung von this erforderlich.
Im folgenden Beispiel speichert die Methode ablage die Referenz auf das Objekt, in dem
sie enthalten ist, in die (statische) Variable aktuelles_bsp. Der Konstruktor verwendet
den Namen des Objektbestandteils s gleichzeitig als Parameternamen und muß deshalb
bei Benutzung des Objektbestandteils this verwenden.
1
2
3
4
5
6
class Bsp3 {
static Bsp3 aktuelles_bsp;
String s;
Bsp3 (String s) { this.s = s; }
void ablage () { aktuelles_bsp = this; }
}
• Das Literal null
Schließlich gibt es noch das Literal null. Es bezeichnet die undefinierte Referenz. Sie
kann in Variablen für Objekte gespeichert werden, zeigt aber nicht auf ein Objekt.
Der Versuch, über diese Referenz einen Objektbestandteil anzusprechen führt zu einem
Fehler.
Objektvergleich
Der normale Vergleichsoperator in Java ist ==. Er dient sowohl zum Vergleich von Werten
primitiver Datentypen als auch zum Vergleich von Objekten.
• Vergleich der Referenzen
Im Fall von Objekten prüft der Operator, ob zwei Referenzen gleich sind, also ob es
sich in beiden Fällen um das selbe (identische) Objekt handelt. Dies ist eine stärkere
Eigenschaft, als zu prüfen, ob zwei Objekte in ihren Inhalten übereinstimmen. Die
Abbildung stellt die Wirkung des Operators == dar.
32
1.6. OBJEKTE
Abbildung 1.11: Arbeitsweise des Vergleichsoperators
Der Operator == liefert nur den Ergebniswert true, wenn beide Operanden Referenzen
sind, die mit dem gleichen Konstruktoraufruf erzeugt wurden. Daher liefert beispielsweise
der Ausdruck
(new Bsp()) == (new Bsp())
immer false.
• Der Operator !=
Der Operator != prüft analog Objekte auf Verschiedenheit.
• Vergleich mit null
Auch die Referenz null kann in Vergleichen benutzt werden. Sie ist verschieden von
jeder Referenz auf ein gültiges Objekt.
Objektvernichtung
Objekte können nicht explizit wieder vernichtet werden. Dies würde die Referenz ungültig
machen und bei nachfolgender Benutzung zu einem Fehler führen. Umgekehrt ermittelt
das Java-Laufzeitsystem automatisch alle Objekte, zu denen die Referenz nirgends mehr
gespeichert ist. Diese Objekte können nicht mehr benutzt werden und werden daher
automatisch vernichtet, d.h. der von ihnen belegte Speicherplatz wird wieder freigegeben.
Definition: Dieser Mechanismus wird als garbage collection bezeichnet.
Im obigen Beispiel
(new Bsp2(20)).s = "Unsinn";
würde das neu erzeugte Objekt also nach der Umbesetzung der Variablen s vom Laufzeitsystem
vernichtet, da dann keine Referenz mehr zu ihm existiert.
33
KAPITEL 1. GRUNDLAGEN
1.7
Objekte in MTJ Stufe 1
In einer Konfiguration aus Stationen sind die einzelnen Stationen Objekte. Die inneren
Stationen sind Instanzen der Klasse ProcStation, die Eingangsstationen sind Instanzen der
Klasse GenStation.
1.7.1
Variable und Methoden
Die im → Abschnitt zu Variablen und Methoden (siehe Seite 18) eingeführten Klassenbestandteile sollen alle in jeder Station enthalten sein, es handelt sich also um Instanzvariable
und Instanzmethoden im Schablonenteil der jeweiligen Klasse.
• In jeder ProcStation existiert je ein Exemplar der Variablen eventListeners,
eventSenderProc und eventSenderGen. Der Inhalt ist jeweils der spezifische Nachfolger
bzw. Vorgänger der einzelnen Station. Unterschiedliche Stationen haben auch
unterschiedliche Nachfolger und Vorgänger.
• In entsprechender Weise hat jede Generatorstation in der Konfiguration ihr eigenes
Ereignis in der Variablen event, das sie bei Aufruf von generateEvent in die
Konfiguration einspeist.
• Auch der in der Variablen name gespeicherte Stationsname ist natürlich spezifisch für
jede Station.
• Die Methoden sind ebenfalls bei den einzelnen Instanzen enthalten. Ein Methodenaufruf
bezieht sich also immer auf eine ganz bestimmte Station. Im Fall der Methode
addEventListener wird die Methode bei der Station aufgerufen, die einen neuen
Nachfolger erhalten soll. Der Nachfolger wird dagegen als Parameter im Methodenaufruf
angegeben.
Sind also beispielsweise w1 und w2 Variable vom Typ ProcStation, so richtet der Aufruf
w1.addEventListener(w2);
eine Verbindung von w1 nach w2 ein.
• Die Methoden releaseFromListener und releaseFromSender haben keine Parameter,
sie lösen die bestehende Verbindung zum Nachfolger bzw. Vorgänger des Objekts, in
dem sie enthalten sind (falls eine solche Verbindung existiert).
Entsprechend bewirkt der Aufruf
g1.generateEvent();
das Senden des Ereignisses einer Generatorstation an ihren Nachfolger, falls g1 eine
Variable vom Typ GenStation ist.
1.7.2
Referenzen
Wir können jetzt auch näher festlegen, auf welche Weise die Stationen verkettet sind.
Um einen Zugriff von einer Station auf Vorgänger und Nachfolger zu ermöglichen, ist es am
einfachsten, Referenzen der Vorgänger und Nachfolger bereitzuhalten.
34
1.7. OBJEKTE IN MTJ STUFE 1
• Entsprechend speichern die Variablen eventListener und eventSenderGen bzw.
eventSenderProc jeweils eine Referenz auf eine Station.
Die Abbildung zeigt die Situation für zwei verkettete Arbeitsstationen.
Abbildung 1.12: Statische Verkettung der Stationen durch Referenzen
• Auch beim Aufruf von addEventListener wird als Parameter eine Referenz auf die
einzutragende Nachfolgerstation übergeben. Diese Referenz kann direkt in die Variable
eventListener bei der Station eingetragen werden. Sie kann außerdem verwendet
werden, um bei der Nachfolgerstation die Methode setSender aufzurufen und damit
den Rückverweis in der Verbindung zu erzeugen.
Falls listener eine Variable ist, in der die Referenz auf den neuen Nachfolger liegt,
lautet der Aufruf in addEventListener bei einer Arbeitsstation:
listener.setSender(this);
Als Parameter wird die Referenz auf die Station übergeben, auf die der Rückverweis
zeigen soll. Dies ist natürlich die Station, bei der die Methode addEventListener
aufgerufen wurde, also erhält man die Referenz durch den Ausdruck this.
1.7.3
Konstruktoren
Nachdem die Stationen Instanzen sind, müssen alle in einer Konfiguration verwendeten
Stationen explizit durch Konstruktorausdrücke erzeugt werden.
Aufbau einer Konfiguration
Ein Programm, das eine Konfiguration aus Stationen verwenden möchte, muß die
Konfiguration in zwei prinzipiellen Schritten aufbauen.
• Schritt 1
Erzeugung der Stationen durch Instanzierung der Klassen ProcStation und GenStation
• Schritt 2
Verkettung der erzeugten Stationen durch Aufruf der Methode addEventListener für
einzelne Stationen.
35
KAPITEL 1. GRUNDLAGEN
Initialisierung der Verkettung
Wir legen also fest, daß eine neu erzeugte Station keine Vorgänger oder Nachfolger
haben soll. Entsprechend werden die Variablen eventListener und eventSenderProc bzw.
eventSenderGen mit der null-Referenz initialisiert. Da dies für alle Stationen in gleicher
Weise geschehen soll, bietet es sich an, die Initialisierung direkt in der Variablendefinition
vorzunehmen:
1
2
3
private ProcStation eventListener = null;
private ProcStation eventSenderProc = null;
private GenStation eventSenderGen = null;
Initialisierung des Stationsnamens
Der → Stationsname (siehe Seite 22) soll dagegen bei der Erzeugung der Station
gesetzt werden. Hier wollen wir zwei Varianten zulassen: Verwendung des Standardnamens
„Arbeitsstation“ bzw. „Generatorstation“, falls der Name nicht relevant ist, und die explizite
Angabe eines beliebig gewählten Namens.
• Konstruktoren
Entsprechend benötigen wir zwei Konstruktoren in den Klassen ProcStation und
GenStation.
– Der eine erhält den Stationsnamen als Parameter und besetzt die Variable name
entsprechend:
1
2
public ProcStation(String name)
{ this.name = name; }
– Der zweite hat keinen Parameter und verwendet den ersten Konstruktor, um den
Standardnamen einzutragen:
1
2
public ProcStation()
{ this("Arbeitsstation"); }
Initialisierung für Ereignisse
Die Konstruktoren in der Klasse GenStation haben schließlich noch die Aufgabe, die Variable
event zu besetzen. Dazu muß ein neues Ereignis-Objekt erzeugt werden.
• Die Klasse für Ereignisse
Da auf Stufe 1 Ereignisse noch keine innere Struktur besitzen, sind im Konstruktor
für Ereignisse auch keine Instanzvariable zu initialisieren. Als Klassendefinition für
Ereignisse reicht die minimal mögliche Klassendefinition aus:
public class Event { }
Java ergänzt automatisch einen Konstruktor ohne Parameter.
36
1.8. ANWEISUNGEN
• Ereigniserzeugung
Neue Ereignisse können entsprechend mit dem Konstruktorausdruck
new Event()
erzeugt werden. Alle Ereignisse sind zwar vom Aufbau her gleich, es handelt sich aber
um individuelle Java-Objekte, die mit dem Operator == unterschieden werden können.
Beispiel zum Aufbau einer Konfiguration
Der Aufbau einer einfachen Konfiguration aus einer Generator- und zwei Arbeitsstationen, die
linear verkettet sind, könnte nun folgendermaßen realisiert werden:
1
2
3
GenStation g = new GenStation();
ProcStation w1 = new ProcStation("Station 1");
ProcStation w2 = new ProcStation("Station 2");
4
5
6
g.addEventListener(w1);
w1.addEventListener(w2);
Die resultierende Konfiguration hat dann die Form
Abbildung 1.13: Konfiguration aus einer Generator- und zwei Arbeitsstationen
Aktivierung der Konfiguration
Nun ist die Konfiguration bereit und kann mittels
g.generateEvent();
aktiviert werden.
1.8
Anweisungen
Die bisher beschriebenen Sprachbestandteile von Java (Packages, Klassen, Klassenbestandteile) dienen alle zur Strukturierung von Programmen oder Datenobjekten.
Definition: Die für die „eigentliche“ Programmierung benötigten Einheiten werden in Java
(und vielen anderen Programmiersprachen) als Anweisungen („statements “) bezeichnet.
Eine Anweisung kann zur Laufzeit „abgearbeitet“ werden und bewirkt dann gewisse
Änderungen an Datenobjekten oder Interaktionen mit der Programmumgebung.
1.8.1
Wo kommen Anweisungen vor?
Anweisungen kommen in Java in den Rümpfen von Methoden vor (einschließlich
Konstruktoren und Initialisierern). Jeder Rumpf kann mehrere Anweisungen hintereinander
enthalten. Der Rumpf ist durch geschweifte Klammern { ... } zusammengefaßt.
37
KAPITEL 1. GRUNDLAGEN
1.8.2
Zuweisungen
Eine der einfachsten Anweisungen ist die Zuweisung. Sie dient dazu, den Wert einer einzelnen
Variablen zu ändern. Sie hat die Form
<Variable> = <Ausdruck>;
Links steht der Name einer Variable. Rechts steht ein Ausdruck, dessen Auswertung den
neuen Wert ergibt, der dann in der Variablen abgelegt wird. Das abschließende Semikolon ist
in Java (wie in C) Teil der Zuweisung und dient nicht als Trenner zwischen Anweisungen (wie
beispielsweise in Pascal).
Kurzformen
Im Zusammenhang mit bestimmten Operatoren im Ausdruck gibt es Kurzformen für häufig
auftretende Zuweisungsformen.
• Es steht
<Variable> += <Ausdruck>;
für
<Variable> = <Variable> + <Ausdruck>;
und entsprechend für *=, -=, /= und andere Operatoren.
• Eine weitere Verkürzung ist die Form
<Variable>++;
für
<Variable> += 1;
und entsprechend für −−.
1.8.3
Methodenaufruf
Ein Methodenaufruf wird zu einer Anweisung, wenn er durch ein Semikolon abgeschlossen
wird:
Bsp.addiere(17,4);
Die Abarbeitung entspricht dem Aufruf der Methode und der Abarbeitung ihres Rumpfs.
Liefert die Methode einen Ergebniswert, so wird er ignoriert.
1.8.4
return-Anweisung
Methoden, die ein Ergebnis liefern, müssen den Ergebniswert im Rumpf festlegen. Dies
geschieht durch eine oder mehrere Anweisungen der Form
return <Ausdruck>;
38
1.9. ZUR IMPLEMENTIERUNG EINIGER METHODEN IN MTJ STUFE 1
Der Ausdruck wird ausgewertet und der so erhaltene Wert wird als Ergebnis der Methode
verwendet. Anschließend wird die Bearbeitung der Methode sofort abgebrochen und zur
Aufrufstelle zurückgekehrt.
• Liefert eine Methode kein Ergebnis, so kann die return-Anweisung verwendet werden,
um die Bearbeitung vorzeitig zu beenden.
return;
1.8.5
Bedingte Anweisung
Die bedingte Anweisung hat in Java eine der beiden Formen
if (<Ausdruck>) <Anweisung>
if (<Ausdruck>) <Anweisung> else <Anweisung>
Der als Bedingung verwendete Ausdruck muß bei der Auswertung einen Wert vom Typ
boolean liefern. Ist dieser Wert true, wird die erste Anweisung ausgeführt, ist er false,
wird ggf. die zweite Anweisung ausgeführt.
Beispiele für bedingte Anweisungen sind (v sei eine Variable vom Typ int):
if (v > 5) v = 0;
if (v > 0 && v < 100) if (v < 50) v = -1; else v = -2;
if (v > 0 && v < 100) { if (v < 50) v = -1; } else v = -2;
Im zweiten Fall wird der else-Teil automatisch zur inneren bedingten Anweisung gezählt. Ist
dies nicht beabsichtigt, so muß die erste Anweisung der äußeren if-Anweisung mit geschweiften
Klammern begrenzt werden, wie im dritten Fall. Bedingte Anweisungen werden nie durch ein
Semikolon abgeschlossen, im Beispiel gehören die Semikolons alle zu den inneren Zuweisungen.
1.9
Zur Implementierung einiger Methoden in MTJ Stufe 1
Für einige der besprochenen Methoden geben wir hier Hinweise zu ihrer Implementierung.
1.9.1
Methodenergebnisse
Die einzige bisher verwendete Methode mit Ergebnis ist die Methode getName. Alle übrigen
Methoden haben kein Ergebnis. In ihren Methodenrümpfen ist daher keine return-Anweisung
notwendig.
1.9.2
Bedingte Anweisung
Eine bedingte Anweisung benötigen wir in Methoden, deren Arbeitsweise davon abhängt, ob
eine Verkettung zu einer Vorgänger- bzw. Nachfolgerstation existiert oder nicht. Dies ist für
die Methoden releaseFromListener, releaseFromSender und sendEvent der Fall.
Bei der Methode sendEvent soll das Ereignis e beispielsweise nur dann an den Nachfolger
übermittelt werden, wenn ein Nachfolger existiert. Dazu ist zu prüfen, ob die Referenzvariable
eventListener auf eine existierende Station verweist:
39
KAPITEL 1. GRUNDLAGEN
if (eventListener != null) eventListener.handleEvent(e);
Einen Hinweis auf die Notwendigkeit der bedingten Anweisung liefert der beabsichtigte Aufruf
der Methode handleEvent beim durch eventListener referenzierten Objekt. Dies ist nur
möglich, wenn eventListener auf ein Objekt zeigt, ansonsten wird der Programmlauf mit einer
Fehlermeldung abgebrochen. Die bedingte Anweisung verhindert, daß dieser Fehler auftritt,
indem sie dann den Methodenaufruf vermeidet.
1.9.3
Ausgaben
In den Methoden generateEvent und handleEvent soll eine Meldung ausgegeben werden.
Dies ist mit Hilfe der vordefinierten Methode println des vordefinierten Objekts System.out
möglich (vgl. das→ Beispiel zum Zugriff auf Objektbestandteile (siehe Seite 31)). Als
Parameter erhält die Methode den auszugebenden String. Dieser kann mit dem Operator
+ aus einzelnen Teilen zusammengesetzt werden.
Eine Meldung in handleEvent, die den Stationsnamen mit einschließt, könnte also
beispielsweise mit der Anweisung
System.out.println("Station " + getName() + ": Ereignis");
ausgegeben werden. Dabei handelt es sich bei getName um die Methode beim gleichen
Objekt, zu dem die umgebende handleEvent-Methode gehört (vgl. → die Beschreibung zu
Objektreferenzen (siehe Seite 29)). Zur Verdeutlichung könnte man hier auch this.getName()
schreiben, innerhalb von Instanzmethoden ist aber immer auch die direkte Angabe von
Objektbestandteilen erlaubt.
Aufgaben
1. Aufgabe 1 Umgang mit dem Java Compiler
Diese Aufgabe soll Sie in den Umgang mit dem Java Compiler einführen. Grundlage
ist die → Beschreibung zu Programmen in Java (siehe Seite 3). Dazu erhalten Sie drei
vollständige Java-Programme, die alle das Selbe machen: sie geben den Text „Hello
World“ aus.
• Programmarten
Drei verschiedene Programme für ein und dasselbe sind deshalb in Java möglich,
da man hier zwischen drei verschiedenen Programmarten unterscheiden kann:
– Java Programme
werden in einem Terminal gestartet, sämtliche Ein- und Ausgaben finden
in diesem diesem Terminal statt. Diese Art von Java Programmen laufen
ausschließlich „zeichenorientiert“ ab.
– Java Applets
können z.B. in einem Web-Browser oder einem Appletviewer gestartet werden.
In ihnen hat man die Möglichkeit graphische Ausgaben (z.B. Linien zeichnen)
vorzunehmen oder GUI-Komponenten zu erzeugen.
40
1.9. ZUR IMPLEMENTIERUNG EINIGER METHODEN IN MTJ STUFE 1
– Java Programme mit graphischer Oberfläche
schließlich entsprechen normalen Programmen wie wir sie von modernen Systemen her kennen. Sie werden üblicherweise in einem Terminal gestartet. Weiterhin kann die Applikation beliebig viele Fenster mit Buttons, Zeichenflächen,
u.s.w. öffnen.
a) Ausführung der vorgegebenen Programme
Zuerst sollen Sie die vorgegebenen Programme übersetzen und ausführen.
• Verzeichnis für Quellen
Legen Sie zunächst ein Verzeichnis java in ihrem HOME Verzeichnis an 4 .
• Quellen kopieren
Kopieren sie sich dann von /usr/proj/ppmtj/sourcen/auf01 das gesamte
Verzeichnis auf01 mit allen seinen Untervereichnissen 5 in das Verzeichnis java.
• Übersetzung und Ausführung
In auf01 finden Sie die drei Verzeichnisse A, B und C in denen sich analog zu
obiger Reihenfolge drei verschiedene Arten von Java Programmen befinden.
Gehen Sie in jedes der drei Verzeichnisse und schauen Sie sich die darin zur
Verfügung gestellten Programme an. übersetzen und führen Sie dann diese
Programme aus. Befolgen Sie dazu die jeweils in der Datei ablauf vorgegebenen
Befehle.
• Behandlung von Abhängigkeiten
Interessant an der Lösung in Verzeichnis C ist, daß hier das Java Programm
aus drei .java-Dateien besteht. Vergleichen Sie hierzu die → Anmerkungen
zu Abhängigkeiten zwischen Klassen (siehe Seite 5). Prüfen sie, daß
MyButton.java tatsächlich bei Aufruf von javac HelloWorld.java nur dann
kompiliert wird, wenn es jünger ist als MyButton.class.
b) Modifikation der Programme
Schreiben Sie das Java Programm in Verzeichnis A derart um, daß der Text nun
fünfmal untereinander angezeigt wird. Selbstverständlich gibt es hierzu mehrere
Lösungsmöglichkeiten. Bei Interesse können Sie auch die Programme in den
Verzeichnissen B und C umprogrammieren.
c) Umstellung auf Packages
Definieren Sie die Lösung in Verzeichnis A und C als Package mtj.auf01.A bzw.
mtj.auf01.C und rufen Sie HelloWorld bzw. HelloWorldAWT von Ihrem HOMEVerzeichnis aus auf. Wie sehen die Aufrufe dazu aus ? Auf welchen Wert müssen
Sie die Umgebungsvariable CLASSPATH setzen ?
2. Aufgabe 2 Stationsverkettung
Das in Kapitel 1 beschriebene System ist zu implementieren. In diesem System soll die
folgende Konfiguration aufgebaut und durchlaufen werden:
4
5
Siehe man mkdir
Kommando cp -r siehe man cp
41
KAPITEL 1. GRUNDLAGEN
Abbildung 1.14: Stationsverkettung
Um die drei Arbeitsstationen voneinander unterscheiden zu können, geben Sie ihnen bei
der Erzeugung (Konstruktorausdruck) individuelle Namen.
• Kurzform der connected-Relation
Anmerkung: Auf diesem und den folgenden Aufgabenblättern verwenden wir
in Darstellungen für Konfigurationen abkürzend einen einfachen Pfeil für die
connected-Relation. Die Pfeilspitze ist auf der listener-Seite, deutet also die
Richtung des Ereignisflusses an.
Abbildung 1.15: Die connected-Relation
• Package
Alle zu implementierenden Klassen sollen dem Package mtj.level1 angehören.
Jede Klasse soll public sein und in einer eigenen Quelldatei definiert werden.
a) Implementierung: Klassen
Implementieren Sie die drei Klassen GenStation, ProcStation und Event.
b) Implementierung: Rahmen
Definieren Sie eine Klasse mtj.level1.Aufg02, die die statische main-Methode enthält. Implementieren Sie in dieser Methode den Aufbau der oben dargestellten
Konfiguration und die Aktivierung eines Durchlaufs. Anschließend soll die Konfiguration umgebaut werden (beispielsweise durch Auftrennen einer Verbindung)
und ein weiterer Durchlauf soll aktiviert werden.
• Für die Speicherung der in der main-Methode erzeugten Stationen definieren
Sie geeignete statische Variable in der Klasse mtj.level1.Aufg02.
c) Test
Lassen Sie die Methode mtj.level1.Aufg02.main im Debugger in Einzelschritten
ablaufen. Kontrollieren Sie den Fortschritt im Aufbau der Konfiguration, indem Sie
die statischen Variablen, in denen die Stationen gespeichert werden, mittels print
und dump inspizieren.
• Die mit dem Debugger-Kommando print ausgegebene Information zu einer
Station ist nicht sehr aussagekräftig, sie enthält nur die Klasse und eine
Identifikationsnummer. Dies läßt sich verbessern, indem in jeder Klasse eine
geeignete Anzeige-Methode implementiert wird.
42
1.9. ZUR IMPLEMENTIERUNG EINIGER METHODEN IN MTJ STUFE 1
d) Anzeige der Stationen beim Test
Ergänzen Sie in jeder Klasse eine Anzeige-Methode und wiederholen Sie den Ablauf
im Debugger.
• Hinweis zur Anzeige-Methode Die Anzeige-Methode muß die Form
public String toString() { ... }
haben und als Ergebnis einen String liefern. Dieser wird zur Anzeige der Station
im Debugger verwendet. In unserem Fall sollte er den Stationsnamen enthalten
und ggf. die Namen der Vorgänger- und Nachfolgerstation.
– Warnung Der String darf nur die Namen von Vorgänger und Nachfolger
enthalten, also beispielsweise
... + eventListener.getName() + ...
es darf nicht die ganze entsprechende Station in der Form
... + eventListener + ... <---- Fehler !!
eingefügt werden. Der zweiten Fall wird von Java in der Form
... + eventListener.toString() + ...
realisiert und dies führt wegen der doppelten Verkettung der Stationen zu
einer Unendlichschleife bei der Stations-Anzeige.
43
KAPITEL 1. GRUNDLAGEN
44
Kapitel 2
Vererbung
In diesem zweiten Abschnitt des Praktikums wird gezeigt, wie sich das Konzept der Vererbung
einsetzen lässt.
2.1
MTJ - Stufe 2
In Stufe 2 des Praktikums wird das bisherige rudimentäre Konzept der Stationen ausgebaut
und eine erste funktionsfähige Anwendung entwickelt. Für die Implementierung wird ein neues
Package mtj.level2 angelegt und die Klassen für Stationen und Ereignisse neu entwickelt.
2.2
Vererbung
Objekte der gleichen Klasse stimmen in allen Bestandteilen überein. Oft kommt es aber
vor, daß man ähnliche Objekte verwenden will, die nur in einem Teil der Bestandteile
übereinstimmen. In diesem Fall kann das Prinzip der Vererbung verwendet werden.
2.2.1
Abgeleitete Klassen
Mittels Vererbung kann eine Klasse definiert werden, die von einer anderen Klasse alle
Bestandteile übernimmt und dann um zusätzliche Bestandteile erweitert.
Definition einer abgeleiteten Klasse
Die Klassendefinition hat die Form
class <Identifikator> extends <Klassenname>
45
KAPITEL 2. VERERBUNG
Ein Beispiel ist
1
2
3
4
5
6
7
8
9
10
11
12
class Bsp4 {
static int si = 5;
int i = 10;
Bsp4(int i) { this.i = i; }
Bsp4() { }
int addiere (int i1) { return i + i1; }
}
class Bspx extends Bsp4 {
String s2 = String.valueOf(i);
static int multipliziere (int i1, int i2) { ... }
Bspx(String s) { s2 = s; }
}
• Definition: Eine so definierte Klasse Bspx wird als abgeleitete Klasse oder Subklasse
bezeichnet, die Klasse Bsp4 als Basisklasse oder Superklasse von Bspx.
Abgeleitete Klasse und ihre Basisklasse
Die folgende Abbildung zeigt eine abgeleitete Klasse und ihre Basisklasse. Die UMLNotation entspricht der Instanz-Klassen-Beziehung, in diesem Fall sind jedoch zwei
Klassen beteiligt.
Abbildung 2.1: Abgeleitete Klasse und Basisklasse
Die Klasse Object
In Java gibt es die vordefinierte Klasse java.lang.Object. Diese ist als einzige Klasse
nicht von einer anderen Klasse abgeleitet. Alle übrigen Klassen in Java haben genau eine
andere Klasse als Basisklasse. Alle Klassen bilden also eine baumförmige Hierarchie mit
java.lang.Object als Wurzel. Ist in der Definition einer Klasse keine Basisklasse angegeben,
so wird automatisch java.lang.Object als Basisklasse verwendet.
46
2.2. VERERBUNG
Abbildung 2.2: java.lang.Object als Wurzel der Vererbungshierarchie
Die Definitionen
class Bsp { ... }
class Bsp extends java.lang.Object { ... }
sind also völlig gleichwertig.
Vererbte Bestandteile
Die oben definierte Klasse Bspx hat die statischen Bestandteile si und multipliziere, wobei
der erste von der Klasse Bsp4 ererbt ist. Der Schablonenteil besteht entsprechend aus i,
addiere und s2. Hinzu kommen die Bestandteile der Klasse Object. Dabei handelt es sich
um 11 nichtstatische Methoden, Variable werden in Object nicht definiert.
Statische Bestandteile
Die ererbten statischen Bestandteile einer abgeleiteten Klasse sind identisch mit denen der
Basisklasse, es handelt sich nicht um Kopien. Die Variable si ist also als Bestandteil der
Klassen Bsp4 und Bspx nur insgesamt einmal vorhanden. Wird sie mittels Bsp4.si = 10
besetzt, so liefert auch der Zugriff Bspx.si den Wert 10 und umgekehrt.
Abstrakte Basisklassen
Ist die Basisklasse einer Klasse → als abstrakt definiert (siehe Seite 26), so kann die abgeleitete
Klasse trotzdem instanziert werden, solange sie nicht selbst abstrakt ist. Dies ist eine
typische Anwendung abstrakter Klassen: Gewisse gemeinsame Bestandteile mehrerer Klassen
werden aus organisatorischen Gründen zu einer separaten Basisklasse zusammengefaßt.
Da die Bestandteile jedoch als Schablone für sinnvoll verwendbare Objekte noch nicht
ausreichen, wird die Basisklasse als abstrakt definiert. Erst die einzelnen abgeleiteten Klassen
vervollständigen jeweils den Schablonenteil und sind instanzierbar.
47
KAPITEL 2. VERERBUNG
Ein Beispiel bilden die Klassen
public abstract class Steuerpflichtiger
{ int steuernummer; }
public class Person extends Steuerpflichtiger
{ String geburtstag; ... }
public class Firma extends Steuerpflichtiger
{ int gruendungsjahr; ... }
Abbildung 2.3: Beispiel für eine abstrakte Basisklasse
Finale Klassen
Abstrakte Basisklassen werden nur zu dem Zweck definiert, weitere Klassen von ihnen
abzuleiten. Umgekehrt kann es erwünscht sein festzulegen, daß von einer bestimmten Klasse
keine Klasse mehr abgeleitet werden darf. Dies kann durch das Voranstellen des Schlüsselworts
final vor die Klassendefinition geschehen:
final class Yorkshire extends Terrier { ... }
48
2.2. VERERBUNG
Abbildung 2.4: Beispiel für eine finale Klasse
• Der Java-Compiler meldet bei jedem Versuch, eine von einer finalen Klasse abgeleitete
Klasse zu definieren einen Fehler.
• Finale Klassen definiert man normalerweise aus technischen Gründen. Der Fall, daß sich
eine Klasse aus inhaltlichen Gründen nicht mehr erweitern läßt, ist eher ungewöhnlich
(selbst von Yorkshire-Terriern gibt es noch eine Mini- und eine Normalversion). Für
finale Klassen kann der Compiler aber gewisse Optimierungen vornehmen. Aus diesem
Grund sind beispielsweise die häufig verwendeten Klassen String, Integer, Boolean
etc. aus dem Package java.lang als final definiert und können nicht weiter abgeleitet
werden.
2.2.2
Konstruktoren und Vererbung
Konstruktoren und Initialisierer werden nicht mit vererbt, aber mit benutzt.
Statischer Klassenteil
Im Fall von statischen Initialisierern geschieht dies automatisch. Statische Initialisierer wirken
nur auf den statischen Teil einer Klasse. Der ererbte statische Teil einer abgeleiteten Klasse
ist aber identisch mit dem statischen Teil der Basisklasse, also wurde er beim Laden der
Basisklasse bereits durch Abarbeitung der dort definierten statischen Initialisierer initialisiert.
Beim Laden der abgeleiteten Klasse werden zusätzlich noch die in dieser Klasse definierten
statischen Initialisierer ausgeführt.
49
KAPITEL 2. VERERBUNG
Nichtstatischer Klassenteil
Im Fall von nichtstatischen Initialisierern und Konstruktoren organisiert Java explizit, daß
bei jeder Objekterzeugung auch die Initialisierer und mindestens ein Konstruktor aller
Superklassen ausgeführt werden.
• Zu diesem Zweck muß jeder Konstruktor in einer abgeleiteten Klasse mit einer Anweisung
der Form
super (<Ausdruck>, <Ausdruck>, ... );
beginnen. Entsprechend dem im Abschnitt → zur Objekterzeugung (siehe Seite 28)
beschriebenen Aufruf eines anderen Konstruktors mittels this(...) wird dadurch ein
Konstruktor der Superklasse aufgerufen. Welcher Konstruktor dies ist wird wie üblich
durch Anzahl und Typ der Ausdrücke bestimmt. Existiert ein solcher Konstruktor in
der Superklasse nicht, so meldet der Java-Compiler einen Fehler.
• Beginnt ein Konstruktor in einer abgeleiteten Klasse nicht mit super(...), so fügt der
Compiler automatisch die Anweisung super() ein, es wird also der Konstruktor ohne
Parameter der Superklasse verwendet. Existiert ein solcher nicht, wird wieder ein Fehler
gemeldet.
Im Fall der oben definierten Klasse Bspx wird im Konstruktor also super(); vor s2 =
s; eingefügt und damit der parameterlose Konstruktor von Bsp4 aufgerufen. Weitere
zulässige Konstruktoren für Bspx wären
Bspx(String s, int i) { super(i); s2 = s; }
Bspx() { super(15); }
• Eine Ausnahme bilden nur Konstruktoren, die mittels this(...) einen anderen
Konstruktor der gleichen Klasse aufrufen. Da bereits bei dessen Abarbeitung die
Superklassen-Konstruktoren mitausgeführt werden, muß dies nicht erneut geschehen und
der Compiler fügt auch keine super()-Anweisung ein.
• Da die super(...)-Anweisung immer am Anfang des Konstruktor-Rumpfs stehen muß
werden die Konstruktoren der Superklassen insgesamt streng sequentiell bearbeitet,
beginnend mit dem Konstruktor der obersten Superklasse Object.
• Wie üblich werden zusätzlich unmittelbar vor dem jeweils ersten Konstruktor einer Klasse noch alle (nichtstatischen) Initialisierer der Klasse in ihrer Aufschreibungsreihenfolge
abgearbeitet. Im Beispiel der Klasse Bspx wird s2 also nach Bearbeitung des Konstruktors von Bsp4, aber vor Bearbeitung des Konstruktors Bspx initialisiert.
2.2.3
Zugriffsbeschränkungen
Sind Klassenbestandeile als privat definiert worden, so werden diese zwar auch an abgeleitete
Klassen vererbt, können aber innerhalb der abgeleiteten Klasse nicht benutzt werden
(beispielsweise in Methoden, die in der abgeleiteten Klasse definiert sind).
Klassenbestandteile, die weder privat noch public sind, können nur innerhalb des gleichen
Packages benutzt werden. Im Zusammenhang mit abgeleiteten Klassen gibt es noch eine
50
2.2. VERERBUNG
vierte Art von Zugriffsbeschränkungen für Klassenbestandteile: auch außerhalb des gleichen
Packages, aber dort nur in abgeleiteten Klassen. Dies wird durch das Schlüsselwort protected
festgelegt.
Beispiel
−−−−− Datei Bsp.java −−−−−−−−−
package bsp.pack1;
public class Bsp { protected int i; }
−−−−− Datei Bspx.java −−−−−−−−
package bsp.pack2;
class Bspx1 extends bsp.pack1.Bsp { int j = i+2; ... }
class Bspx2 { ... }
In Bspx1 ist der Zugriff auf i möglich, in Bspx2 dagegen nicht.
2.2.4
Mehrere Datentypen für das gleiche Objekt
Nehmen wir an, von einer Basisklasse B sind mehrere Subklassen B1, B2, ... abgeleitet. Dann
kommt es vor, daß man Instanzen der verschiedenen Subklassen bei bestimmten Bearbeitungen
nicht unterscheiden möchte, sondern gemäß der Gemeinsamkeiten aus der Klasse B einheitlich
behandeln möchte.
Zu diesem Zweck ist es in Java möglich, Referenzen auf Objekte der abgeleiteten Klassen in
einer Variablen vb vom Typ B zu speichern (vgl. die UML-Darstellung). In diesem Fall ist der
Typ der Variablen also nicht identisch mit dem Typ des Objekts, sondern eine Superklasse
davon.
Abbildung 2.5: Referenzen auf Objekte unterschiedlicher Klassen in einer Variablen
51
KAPITEL 2. VERERBUNG
• Aus diesem Grund ist es auch möglich, eine abstrakte Klasse als Typ einer Variablen
zu verwenden. Es kann zwar keine Instanzen der Klasse geben, in der Variablen können
aber Instanzen beliebiger nichtabstrakter Subklassen gespeichert werden.
• Laufzeittyp und syntaktischer Typ
Allgemeiner unterscheidet man zwischen dem Laufzeittyp eines Objekts und dem
syntaktischen Typ.
Definition: Der Laufzeittyp eines Objekts ist über seine gesamte Lebensdauer fest
und ergibt sich aus dem Konstruktorausdruck, mit dem das Objekt erzeugt wurde. Der
syntaktische Typ ist der Typ, der sich aufgrund der syntaktischen Aufschreibung für
eine Objektreferenz an der Stelle ihrer Verwendung ermitteln läßt.
Der syntaktische Typ eines Objekts hängt also von der Verwendungsstelle im Programm
ab. Er wird durch die Art bestimmt, in der die Referenz angegeben ist.
– Generell gilt, daß der syntaktische Typ eines Objekts immer entweder identisch mit
dem Laufzeittyp ist, oder eine Superklasse davon.
Ist die Referenz in einer Variablen gespeichert und durch den Variablennamen angegeben,
so ist der syntaktische Typ des Objekts der deklarierte Variablentyp. Ist die Referenz das
Ergebnis eines Methodenaufrufs, so ist der syntaktische Typ der deklarierte Ergebnistyp
der Methode. Wird die Referenz durch this erhalten, so ist der syntaktische Typ die
umgebende Klasse.
• Cast
Der syntaktische Typ kann aber auch explizit durch einen cast angegeben werden. Dazu
wird der als syntaktischer Typ zu verwendende Klassenname in Klammern vor den
Ausdruck für die Referenz gesetzt.
Beispiel
Ist v eine Variable vom Typ Bspx, so liefert der Ausdruck
(Bsp4) v
eine Referenz auf das enthaltene Objekt mit syntaktischem Typ Bsp4 anstelle von Bspx.
• Der syntaktische Typ entscheidet darüber, welche Bestandteile eines Objekts
angesprochen werden können.
Wurde eine Referenz auf ein Objekt mit Laufzeittyp Bspx in einer Variablen v4
vom Typ Bsp4 abgelegt, so ist der Zugriff v4.s2 nicht mehr möglich und führt zu
einer Fehlermeldung durch den Compiler. Der Zugriff kann aber mit Hilfe eines casts
durchgeführt werden:
((Bspx) v4).s2
Die zusätzliche Klammerung des Ausdrucks für die Referenz ist notwendig, da der
Zugriffsoperator „.“ Vorrang vor dem ast hat.
Enthält die Variable v4 in diesem Beispiel ein Objekt, das als Laufzeittyp nicht Bspx
(oder eine davon abgeleitete Klasse) hat, so ist Bspx als syntaktischer Typ unzulässig.
Dies wird zur Laufzeit des Programms festgestellt und als Fehler gemeldet.
52
2.2. VERERBUNG
• Prüfen des Laufzeittyps mit intanceof
Fehler beim Cast lassen sich vermeiden, wenn vorher geprüft wird, ob der cast zulässig
ist oder nicht. Dies ist mit einem Ausdruck der Form
<Ausdruck fuer Referenz> instanceof <Klassenname>
möglich. Er prüft ob der Laufzeittyp des durch die Referenz angegebenen Objekts eine
Subklasse des angegebenen Klassennamens (oder identisch damit) ist. Als Ergebnis
liefert er den Wert true oder false vom Typ boolean.
Im obigen Beispiel könnte man also prüfen, ob
v4 instanceof Bspx
den Wert true liefert. Nur dann ist der cast zulässig.
• Typanpassung bei Zuweisungen
Der syntaktische Typ einer Referenz ist auch entscheidend bei der Zuweisung an eine
Variable oder bei Übergabe als Parameter an eine Methode. Dies ist wieder nur möglich,
wenn der deklarierte Typ der Variablen oder des Parameters eine Superklasse des
syntaktischen Typs der Referenz ist. Ansonsten meldet der Compiler einen Fehler. Auch
hier kann der syntaktische Typ der Referenz ggf. durch einen cast angepaßt werden.
Im Beispiel
Bsp4 v4 = new Bspx("xxx");
Bspx vx = (Bspx) v4;
ist im ersten Fall kein cast nötig, da der Variablentyp Bsp4 eine Superklasse des
syntaktischen Typs der Referenz (hier identisch mit dem Laufzeittyp) ist. Im zweiten
Fall dagegen ist der syntaktische Typ der Referenz in v4 die Klasse Bsp4 und muß durch
den cast angepaßt werden.
2.2.5
Abstrakte Methoden
Es kommt vor, daß man in einer Klasse für eine Instanzmethode nur den Namen und
Ergebnis- und Parametertypen festlegen möchte, während der Rumpf erst in abgeleiteten
Klassen definiert werden soll. In Java ist dies möglich, wenn die Instanzmethode als abstrakt
definiert wird. Eine abstrakte Methode ist ähnlich wie eine abstrakte Klasse unvollständig und
kann erst benutzt werden, wenn sie in einer abgeleiteten Klasse durch Angabe eines Rumpfs
„implementiert“ worden ist. Statische Methoden können nicht abstrakt sein.
• Definition
Eine abstrakte Methode wird definiert indem der Methodendefinition das Schlüsselwort
abstract vorangestellt wird. Anstelle des Methodenrumpfs (einschließlich der geschweiften Klammern { ... }) muß dann ein Semikolon geschrieben werden:
1
2
3
4
abstract class Figur {
abstract float flaeche();
abstract void skaliere(float faktor);
}
53
KAPITEL 2. VERERBUNG
• Abstrakte Methoden dürfen nur in abstrakten Klassen definiert werden. Abstrakte
Methoden werden an alle abgeleiteten Klassen vererbt. Damit muß die abgeleitete Klasse
i.a. ebenfalls abstrakt sein. Um in einer abgeleiteten Klasse eine abstrakte Methode mit
einem Rumpf zu versehen (sie zu implementieren), muß die Methode erneut vollständig
definiert werden:
1
2
3
4
5
6
class Quadrat extends Figur {
float seite;
float flaeche() { return seite ∗ seite; }
void skaliere(float faktor) { seite = seite ∗ faktor; }
Quadrat (float seite) { this.seite = seite; }
}
• Eine abgeleitete Klasse, die abstrakte Methoden erbt, darf nur dann nicht-abstrakt sein,
wenn sie alle ererbten abstrakten Methoden implementiert.
• Anwendung
Eine typische Anwendung für abstrakte Methoden ist der Fall, daß man für ähnliche
Objekte die gleiche Operation anbieten möchte (gleicher Name, gleiche Parameter),
diese Operation aber unterschiedlich implementieren möchte. Dazu definiert man eine
abstrakte Methode in einer gemeinsamen Basisklasse und versieht sie in verschiedenen
abgeleiteten Klassen mit unterschiedlichen Rümpfen.
2.2.6
Interfaces
Enthält eine abstrakte Klasse ausschließlich abstrakte Methoden, so beschreibt sie nur die
Schnittstelle zur Benutzung der Instanzen, aber keine Inhalte oder Implementierungen.
Definition: Eine solche Klasse kann in Java speziell als Interface definiert werden.
Dazu wird in der Klassendefinition das Schlüsselwort interface anstelle von class verwendet.
1
2
3
4
interface Figur2 {
abstract float flaeche();
abstract void skaliere(float faktor);
}
• Da ein Interface immer abstrakt ist, ebenso wie sämtliche enthaltenen Methoden, ist
das Schlüsselwort abstract hier optional und kann bei der Interfacedefinition und den
einzelnen Methodendefinitionen entfallen.
• Mehrfachvererbung für Interfaces
Interfaces unterscheiden sich von (abstrakten) Klassen dadurch, daß eine Klasse von
mehreren Interfaces abgeleitet sein kann. Im Fall von Interfaces spricht man anstelle
von „ableiten“ von „implementieren“. Eine Klasse, die Interfaces implementiert, wird in
folgender Form definiert:
class <Identifikator> implements <Interfacename>, <Interfacename>, ...
{ ... }
54
2.3. VERERBUNGSHIERARCHIE FÜR DIE MTJ-KLASSEN
Beispiel
1
2
class Quadrat2 implements Figur2
{ ... }
In der graphischen Darstellung werden Interfaces durch den Zusatz <<interface>>
markiert:
Abbildung 2.6: Eine Klasse, die ein Interface implementiert.
• Die Klasse kann zusätzlich noch von einer (einzelnen) Klasse abgeleitet sein. In diesem
Fall steht die implements-Angabe nach der extends-Angabe. Eine Klasse, die ein
Interface implementiert, aber nicht alle im Interface definierten (abstrakten) Methoden,
muß abstrakt sein. Nur wenn die Klasse alle Methoden des Interfaces implementiert kann
sie nicht-abstrakt sein und instanziert werden.
2.3
Vererbungshierarchie für die MTJ-Klassen
Wir wollen nun Vererbung und Interfaces verwenden, um die MTJ-Klassen inhaltlich besser
zu gliedern. Gleichzeitig verfeinern wir die Klasse für Ereignisse.
2.3.1
Klassen für Stationen
Für die Stationen fassen wir die Gemeinsamkeiten von Arbeits- und Generatorstationen in
einer separaten Klasse zusammen und leiten davon die beiden Varianten ab.
Aufspaltung in Teilfunktionalitäten
Als erstes wollen wir die bisher für Stationen beschriebene Funktionalität in drei
Teilfunktionalitäten aufspalten und jeweils durch ein Interface spezifizieren.
• Teilfunktionalitäten
Die Teilfunktionalitäten sollen sein:
1. Versenden von Ereignissen an Nachfolger,
2. Empfangen von Ereignissen von Vorgängern,
55
KAPITEL 2. VERERBUNG
3. Generieren einer Ereignisfolge.
• Arbeitsstationen besitzen die Funktionalitäten 1 und 2, Generatorstationen dagegen die
Funktionalitäten 1 und 3.
• Erweiterung
Gleichzeitig wollen wir zur vollen Funktionalität der Vernetzbarkeit übergehen, wie
sie → ursprünglich für das MTJ-System (siehe Seite 1) beschrieben wurde, also
mit beliebig vielen Nachfolgern und Vorgängern bei einer einzelnen Station. Ebenso
sollen Generatorstationen jetzt nicht nur einzelne Ereignisse, sondern beliebig lange
Ereignisfolgen erzeugen können. Dazu ist die Wirkung der Methoden geeignet zu
erweitern.
– releaseFromListeners, releaseFromSenders:
Diese Methoden lösen jetzt die Verbindung zu allen Nachfolgestationen bzw.
Vorgängerstationen. Entsprechend wurden auch die Namen angepaßt.
– detachListener, detachSender:
Diese Methoden lösen weiterhin nur die Rückverkettung zu einer Nachfolgerstation
bzw. Vorgängerstation. Da es jetzt mehrere mögliche Kandidaten gibt, muß der
jeweils auszutragende als Parameter an die Methode übergeben werden.
– sendEvent:
Diese Methode sendet nun das übergebene Ereignis an alle Nachfolgerstationen.
– generateEvents:
Diese Methode veranlaßt, daß die Generatorstation die komplette Folge aller ihrer
Ereignisse versendet. Entsprechend wurde der Name angepaßt.
Interfaces
Wir teilen nun die Methoden der Klassen für die Stationen auf die einzelnen Funktionalitäten
auf. Zu jeder Funktionalität definieren wir ein Interface. Diese Interfaces sind gleichzeitig als
Datentypen verwendbar und wir setzen sie ein zur Angabe der Parametertypen. Das Resultat
ist eine komplette Definition der Schnittstelle zur Funktionalität der Stationen, unabhängig
von einer konkreten Implementierung.
• Definitionen der Interfaces
1
2
3
4
5
6
public interface EventSender {
public void addEventListener(EventListener listener);
public void releaseFromListeners();
public void detachListener(EventListener listener);
public void sendEvent(Event e);
}
56
2.3. VERERBUNGSHIERARCHIE FÜR DIE MTJ-KLASSEN
7
8
9
10
11
12
13
14
15
public interface EventListener {
public void setSender(EventSender s);
public void detachSender(EventSender s);
public void releaseFromSenders();
public void handleEvent(Event e);
}
public interface EventGenerator {
public void generateEvents();
}
• Datentyp für beide Stationsarten
Der Datentyp EventSender umfaßt alle Objekte, die die entsprechende Funktionalität
haben, also sowohl Arbeits- als auch Generatorstationen. Damit reicht im Gegensatz
zu Stufe 1 eine einzelne Methode setSender aus, es muß kein overloading verwendet
werden.
Die Interfaces für Stationen
Die folgende Abbildung stellt die drei Interfaces in UML dar. Abstrakte Methoden werden in
UML durch kursive Schrift angezeigt.
Abbildung 2.7: Die Interfaces für Stationen
Die Klasse Station
Da die Funktionalität des EventSender sowohl in ProcStation als auch in GenStation
enthalten ist, bietet es sich an, eine gemeinsame Superklasse Station zu verwenden. Damit
läßt sich verhindern, daß das Interface EventSender doppelt implementiert werden muß.
• Die Klasse Station implementiert alle Methoden von EventSender. Dazu definiert sie
die private Variable eventListeners, die der Variablen eventListener aus Stufe 1
entspricht.
• Station ist trotzdem als abstrakte Klasse definiert, da es keine direkten Instanzen
gibt (also Objekte mit Laufzeittyp Station). Alle konkret als Objekte vorkommenden
Stationen sind entweder Generator- oder Arbeitsstationen.
• Stationsnamen
Als weitere Gemeinsamkeit von Generator- und Arbeitsstationen definiert Station die
→ bereits beschriebene (siehe Seite 22) Methode getName() und die Variable name
57
KAPITEL 2. VERERBUNG
für den Stationsnamen. Da auf diese Variable nur über die Zugriffsmethode getName()
zugegriffen werden soll, kann man sie als private definieren.
• Konstruktoren
Zur Benutzung aus Konstruktoren in abgeleiteten Klassen ist schließlich noch ein
Konstruktor mit einem Parameter vom Typ String enthalten. Er erwartet die Übergabe
des Stationsnamens und legt ihn in der Variablen name ab.
Abbildung 2.8: Die Klasse Station
Abgeleitete Klassen für Stationen
Die Klassen ProcStation und GenStation können nun von Station abgeleitet werden und
übernehmen damit die dort definierte Implementierung des Stationsnamens und des Sendens
von Ereignissen.
• EventListener
Die Klasse ProcStation als Ereignisempfänger implementiert zusätzlich das Interface
EventListener. Dazu definiert sie die private Variable eventSenders. Diese entspricht
den beiden Variablen eventSenderProc und eventSenderGen aus Stufe 1. Hier
erlaubt die Verwendung des gemeinsamen Interfaces EventSender als Datentyp die
Zusammenfassung der beiden Variablen.
• handleEvent
Es fällt dabei auf, daß ProcStation auf Stufe 2 nicht das komplette Interface
implementieren kann: Es ist bisher nicht bekannt, was die Methode handleEvent
eigentlich tun soll. Dies liegt daran, daß ProcStation ebenso wie Station nur ein
Sammelbegriff für verschiedene Arten konkreter Arbeitsstationen ist. Entsprechend ist
ProcStation ebenfalls eine abstrakte Klasse. Von ihr werden erst die Klassen für
spezifische Arbeitsstationen abgeleitet, die einen Strom bestimmter Items auf bestimmte
Weise verarbeiten. Erst in diesen abgeleiteten Klassen wird die Methode handleEvent
implementiert.
58
2.3. VERERBUNGSHIERARCHIE FÜR DIE MTJ-KLASSEN
• Standard-Stationsnamen
Ähnliches gilt für die Festlegung eines Standard-Stationsnamens im Konstruktor.
Um artspezifische Standardnamen zu erhalten verlagern wir die Festlegung von
Standardnamen in die Klassen für konkrete Stationen.
• ProcStation-Konstruktor
Die → beschriebene (siehe Seite 49) Verwendung von Superklassenkonstruktoren in
Konstruktoren für abgeleitete Klassen funktioniert nur für die direkt darüberliegende
Superklasse. Damit in den Klassen für die spezifischen Arbeitsstationen der Konstruktor
der Klasse Station benutzt werden kann, muß der Aufruf über einen Konstruktor in
ProcStation „weitergereicht“ werden. Aus diesem Grund enthält ProcStation einen
Konstruktor mit einem String-Parameter, der nichts weiter tut als den Konstruktor in
der Superklasse aufzurufen und den Parameter an ihn weiterzureichen.
• GenStation
Die Klasse GenStation implementiert das Interface EventGenerator und definiert dazu
die Variable events analog zur Variable event in Stufe 1. Wie im Fall von ProcStation
wird auch GenStation nicht direkt instanziert, sondern von ihr werden wiederum
Klassen für bestimmte Arten von Generatorstationen abgeleitet. In diesen Klassen wird
insbesondere die Art der generierten Items festgelegt und die Erzeugung der generierten
Ereignisfolge in events wird implementiert (in einem entsprechenden Konstruktor). Aus
diesem Grund müssen abgeleitete Klassen auf events zugreifen können und die Variable
muß als protected definiert sein.
• GenStation-Konstruktor
Schließlich hat auch GenStation einen Konstruktor mit dem Stationsnamen als
Parameter, der nur den übergeordneten Konstruktor bei Station aufruft.
2.3.2
Klassen für Ereignisse
Wir wollen nun die Ereignisse verfeinern. Wie → beschrieben (siehe Seite 1) unterscheiden wir
zwei Arten von Ereignissen: solche, die ein Item transportieren und solche für Signale. Items
werden wie Stationen und Ereignisse als Java-Objekte implementiert, entsprechend führen wir
eine Klasse Item ein. Für Signale wird keine gesonderte Klasse verwendet, Signale werden als
spezielle Form von Ereignissen realisiert.
Abstrakte Klasse
Die Klasse Item ist, ähnlich wie ProcStation und GenStation, ein Sammelbegriff für alle
vorkommenden Items wie Zeichen oder Zeichenreihen. Entsprechend handelt es sich um eine
abstrakte Klasse, von der die Klassen für konkrete Items abgeleitet werden.
Gemeinsamer Datentyp
Anders als ProcStation enthält Item jedoch keine gemeinsame Funktionalität oder
Implementierung für alle Items. Die Klasse hat also keine Bestandteile. Es ist trotzdem
59
KAPITEL 2. VERERBUNG
sinnvoll, sie zu definieren, da sie als syntaktischer Typ für alle Item-Objekte dienen kann.
Es lassen sich also Variable vom Typ Item definieren, die beliebige Items speichern können.
ItemEvent, SignalEvent
Die Klasse Event ist ebenfalls ein Sammelbegriff. Es gibt Ereignisse, die ein einzelnes Item
übertragen und es gibt Ereignisse, die ein Signal übertragen. Aus diesem Grund leiten wir die
beiden Klassen ItemEvent und SignalEvent von der abstrakten Klasse Event ab. Wie im Fall
von Item hat Event keine Bestandteile und dient nur als syntaktischer Typ
Abbildung 2.9: Die Klassen für Ereignisse
Items in Ereignissen
Jede Instanz von ItemEvent muß ein einzelnes Item enthalten. Dazu definiert ItemEvent
eine Instanzvariable item vom Typ Item. Diese Variable kann also Items beliebiger Arten
enthalten1 .
Zugriff auf Item in Ereignis
In objektorientierter Programmierung ist es üblich, nicht die Variablen eines Objekts anderen
Objekten zugänglich zu machen, sondern nur die Methoden. Aus diesem Grund ist die Variable
item in der Klasse ItemEvent privat definiert und es gibt eine public Methode getItem (ohne
Parameter), die den Wert der Variablen liefert, und einen Konstruktor mit einem Parameter
vom Typ Item, der die Variable besetzt.
Signalereignisse
Die Klasse SignalEvent definiert keine Bestandteile. Sie dient nur als Datentyp.
Signalereignisse können durch Test mit instanceof SignalEvent erkannt werden.
1
Bei dieser Realisierung haben also alle Item-Ereignisse den gleichen Laufzeittyp ItemEvent, auch wenn sie
Items unterschiedlicher Laufzeittypen enthalten. Alternativ könnte man für jede Itemart eine entsprechende
Ereignisart einführen.
60
2.4. VEKTOREN
2.4
Vektoren
Die vordefinierte Klasse Vector im Standardpackage java.util definiert Objekte für Folgen
von Objekten. Ein Vektorobjekt enthält also eine beliebige Anzahl von Objekten. Die
enthaltenen Objekte haben zusätzlich eine Reihenfolge. Die enthaltenen Objekte können über
ihre Nummer in der Reihenfolge angesprochen werden (beginnend mit Nummer 0). Man kann
einen Vektor jederzeit um zusätzliche Objekte erweitern oder Objekte herauslöschen.
2.4.1
Methoden für Vektoren
Die wichtigsten Methoden für den Umgang mit Vektoren werden hier kurz vorgestellt. Nähere
Informationen sind in der Java API-Beschreibung zu finden.
Konstruktor
Der wichtigste Konstruktor hat keine Parameter und erzeugt einen leeren Vektor ohne
enthaltene Objekte.
Vector()
Zugriffsmethoden
boolean isEmpty() ...
int size() ...
Object elementAt(int index) ...
boolean contains(Object obj) ...
void addElement(Object obj) ...
boolean removeElement(Object obj) ...
• Die Methode isEmpty liefert true, falls der Vektor keine Objekte enthält, die Methode
size liefert die aktuelle Zahl von Objekten im Vektor. Die Methode elementAt liefert
das enthaltene Objekt an der Position index in der Reihenfolge. Existiert aktuell kein
solches Objekt, so wird ein Laufzeitfehler gemeldet. Die Methode contains prüft, ob
das Objekt obj aktuell im Vektor enthalten ist.
• Die Methode addElement hängt obj hinten an den Vektor an. Die Methode
removeElement entfernt obj aus dem Vektor, falls es enthalten ist und rückt alle dahinter
enthaltenen Objekte um eine Position nach vorne. Ist obj mehrmals im Vektor enthalten,
so wird nur das erste Auftreten entfernt. In beiden Fällen liefert die Methode das
Ergebnis true. Ist obj nicht im Vektor enthalten, so bleibt er unverändert und die
Methode liefert das Ergebnis false.
• Der Parametertyp von addElement ist Object, also die Wurzelklasse der JavaKlassenhierarchie. Wie in Abschnitt → zur Typanpassung (siehe Seite 51) für Variable
beschrieben, kann der Parameter beim Aufruf der Methode mit jedem Objekt besetzt
werden, dessen Laufzeittyp von Object abgeleitet ist, also mit jedem beliebigen
Objekt. Ein Vektor kann daher Objekte mit beliebigen Laufzeittypen (auch gleichzeitig)
enthalten.
61
KAPITEL 2. VERERBUNG
Beispiel
1
2
3
Vector v = new Vector();
v.addElement(new Bsp());
v.addElement(new Bsp2( 5 ));
• Der Ergebnistyp von elementAt ist ebenfalls Object. Deshalb ist der syntaktische
Typ für jedes mittels elementAt aus einem Vektor geholte Objekt Object. Soll dieses
Objekt einer Variablen von einem speziellerem Typ zugewiesen oder als Parameter eines
spezielleren Typs an eine Methode übergeben werden, so ist ein cast notwendig.
Bsp b = (Bsp) v.elementAt(0);
Bsp2 b2 = (Bsp2) v.elementAt(1);
2.5
Anwendung von Vektoren in MTJ
Wir verwenden Vektoren zu zwei Zwecken: zum Speichern von Vorgänger- und Nachfolgermengen bei der Stationsverkettung und zur Speicherung der Ereignisfolge in Generatorstationen.
2.5.1
Stationsverkettung
Für die Stationsverkettung benötigen wir in jeder Station eine Nachfolgermenge und in
jeder Arbeitsstation eine Vorgängermenge. Entsprechend verwenden wir als Datentyp für die
Variablen eventListeners und eventSenders die Klasse Vector. Diese Vektoren enthalten
Referenzen der Nachfolge- bzw. Vorgängerstationen. Die Reihenfolge im Vektor ist dabei
irrelevant, die Vektoren werden wie Referenzmengen verwendet. Das Eintragen einer neuen
Referenz in den Methoden addEventListener und setSender kann daher immer am Ende,
also mittels addElement geschehen.
• Bei der Erzeugung einer Station sind die Mengen leer, entsprechend müssen die Variablen
mit der leeren Menge initialisiert werden. Da dies für alle Stationen in gleicher Weise
geschehen soll, bietet es sich an, die Initialisierung direkt in der Variablendefinition
vorzunehmen. Die leere Menge liefert hier der Ausdruck new Vector(), vgl. den
Abschnitt → über Vektoren (siehe Seite 61):
1
2
private Vector eventListeners = new Vector();
private Vector eventSenders = new Vector();
• Die statischen Beziehungen zwischen Stationen auf Stufe 2
Abbildung 2.10: Die statischen Beziehungen zwischen Stationen auf Stufe 2
62
2.5. ANWENDUNG VON VEKTOREN IN MTJ
In der Abbildung ist die neue Verbindungsbeziehung zwischen Generatorstationen und
Arbeitsstationen als connected-Relation dargestellt. Im Unterschied zur → Darstellung
bei Stufe 1 (siehe Abbildung Seite 18) deuten die Sterne anstelle von genaueren
Zahlenangaben an, daß mit einem sender beliebig viele listener verbunden sein können
und umgekehrt.
• Statische Verkettung der Stationen durch Referenzen
Die Verkettung zwischen den Stationen geschieht wie auf Stufe 1 mit Hilfe von Referenzen
auf Stationen. Die Referenzen sind aber jetzt in dem Vektor enthalten, nicht mehr direkt
in der Variablen. Die Situation für eine Arbeitsstation mit zwei Nachfolgern ist in der
folgenden Abbildung dargestellt.
Abbildung 2.11: Statische Verkettung der Stationen durch Referenzen
63
KAPITEL 2. VERERBUNG
• Der Ablauf von sendEvent
Abbildung 2.12: Der modifizierte Ablauf von sendEvent
Auch der Ereignisfluß muß entsprechend verallgemeinert werden. Die Abbildung zeigt
den modifizierten Ablauf. Die Methode sendEvent wird wie bisher von der sendenden
Station selbst aufgerufen, resultiert jetzt aber in handleEvent-Aufrufen bei allen statisch
verbundenen Nachfolgern mit Übergabe des Ereignisses.
2.5.2
Ereignisfolge in Generatoren
Die zweite Anwendung von Vektoren betrifft die Ereignisfolge in Generatorstationen. Um die
Folge zu speichern verwenden wir die Klasse Vector als Datentyp für die Variable events.
Hier ist die Reihenfolge relevant. Die Methode generateEvents sendet die Ereignisse in der
Reihenfolge, in der sie im Vektor liegen. Entsprechend muß der jeweilige Konstruktor in einem
von GenStation abgeleiteten konkreten Generator dafür sorgen, daß die Ereignisse in der
gewünschten Reihenfolge in dem Vektor zu liegen kommen.
2.6
Blöcke und Anweisungen
Neben den statischen Variablen und den Instanzvariablen gibt es in Java auch die aus
klassischen prozeduralen Programmiersprachen bekannten lokalen Variablen.
Definition: Wie in vielen anderen Programmiersprachen wird in Java die Umgebung, die
lokale Variable und Anweisungen enthalten kann als Block bezeichnet.
In Java ist der Rumpf jeder Methode (einschließlich Konstruktoren und Initialisierern) ein
Block.
Definition: Jeder Block ist eine gemischte Folge von Anweisungen („statements“) und
Deklarationen lokaler Variablen.
Die Folge ist durch geschweifte Klammern { ... } zusammengefaßt.
2.6.1
Lokale Variable
Lokale Variable dienen wie statische und Instanzvariable zum Speichern von Werten, haben
aber eine andere Lebensdauer. Eine in einem Block definierte lokale Variable existiert nur
64
2.6. BLÖCKE UND ANWEISUNGEN
während der Block abgearbeitet wird. Beim Verlassen des Blocks wird der Speicherplatz wieder
freigegeben.
• Statische Variable existieren nur einmal je Programmlauf, für Instanzvariable existiert
in jeder Instanz ein Exemplar. Entsprechend existiert auch für lokale Variable zu jeder
Abarbeitung des enthaltenden Blocks ein eigenes Exemplar. Der gleiche Block kann
auch zur gleichen Zeit mehrfach abgearbeitet werden, beispielsweise wenn es sich um
den Rumpf einer rekursiv aufgerufenen Methode handelt.
• Lokale Variable werden in der gleichen Form definiert wie statische und Instanzvariable, insbesondere kann die Initialisierung in die Definition integriert werden. Zugriffsbeschränkungen (private, public, protected) machen keinen Sinn, da auf lokale Variable
nur innerhalb des gleichen Blocks zugegriffen werden kann, und sind daher nicht erlaubt.
Eine lokale Variable kann aber als final definiert werden, ihr Wert kann dann nach der
Initialisierung nicht mehr geändert werden.
• Definitionen lokaler Variabler können in einem Block beliebig mit Anweisungen vermischt
werden. Es ist aber oft übersichtlicher, alle lokalen Variablen am Anfang des Blocks zu
definieren.
2.6.2
Blöcke als Anweisungen
Jeder Block ist eine Anweisung, Blöcke können also ineinander geschachtelt werden. Auch
dabei gilt, daß eine in einem Block definierte Variable nur innerhalb des Blocks zugänglich
ist. Allerdings bilden ineinander geschachtelte Blöcke einen einzigen Namensraum für lokale
Variable. Es ist also nicht möglich, in einem inneren Block eine Variable mit dem gleichen
Namen zu definieren, wie im umgebenden Block.
Beispiel
1
2
3
4
5
6
7
8
{
int i;
...
{
int i;
...
}
}
führt zu einer Fehlermeldung des Compilers, daß i mehrfach definiert ist.
2.6.3
for-Anweisung
Mit der for-Anweisung kann eine Anweisung wiederholt ausgeführt werden. Die forAnweisung hat die Form
for (<Init-Teil> ; <Ausdruck> ; <Inkrement-Teil>)
<Anweisung>
• Zuerst wird der Init-Teil abgearbeitet. Dann wird der Ausdruck ausgewertet. Er muß
einen Wert vom Typ boolean liefern. Solange er true liefert werden die Anweisung und
65
KAPITEL 2. VERERBUNG
der Inkrement-Teil abgearbeitet und dann der Ausdruck erneut ausgewertet. Liefert der
Ausdruck den Wert false, so ist die Bearbeitung der for-Anweisung beendet.
• Der Init-Teil ist entweder eine Variablendefinition oder eine Folge von durch Kommas
getrennten Zuweisungen. In beiden Fällen entfallen dabei die abschließenden Semikolons.
Der Inkrement-Teil ist eine Folge von durch Kommas getrennten Zuweisungen, bei denen
ebenfalls die abschließenden Semikolons entfallen.
Typische Beispiele für for-Anweisungen sind
for (int i = 0 ; i < 10 ; i++) System.out.println(i);
for (i = 0, j = 5 ; i < 100 ; i++, j = j-i) verarbeite(i+j);
Im zweiten Fall ist vorausgesetzt, daß die Variablen i und j und die Methode verarbeite
bereits definiert sind.
• Eine im Init-Teil definierte Variable ist nur innerhalb der for-Anweisung zugänglich,
ähnlich wie im Fall eines Blocks.
• Anmerkung
Die Aufnahme des Inkrement-Teils und des Init-Teils in die for-Anweisung sind nur eine
Schreibvariante. Der Inkrement-Teil könnte auch an die Rumpf-Anweisung angehängt
und der Init-Teil vor die for-Anweisung geschrieben werden.
Entsprechend kann man das zweite Beispiel oben auch schreiben als
1
2
3
4
5
{
i = 0;
j = 5;
for (; i < 100 ;) { verarbeite(i+j); i++; j = j-i; }
}
Leere Init- und Inkrement-Teile sind erlaubt.
2.7
2.7.1
Zur Implementierung einiger Methoden in MTJ
Bedingte Anweisung
Eine bedingte Anweisung benötigen wir in den Implementierungen der Methode handleEvent
in konkreten Arbeitsstationen. Hier ist jeweils zu entscheiden, ob das eingetroffene Ereignis
ein Signal- oder ein Item-Ereignis ist. Nur im Falle eines Item-Ereignisses kann beispielsweise
das Item aus dem Ereignis extrahiert und verarbeitet werden.
• Der Parameter der Methode handleEvent hat den Typ Event. Dies ist also auch der
syntaktische Typ des Ereignisses, wenn es im Rumpf verwendet wird. Um zwischen
Signal- und Item-Ereignissen zu unterscheiden muß der Laufzeittyp des Ereignisses
mittels instanceof (vgl. → den Abschnitt zur Typanpassung (siehe Seite 51)) geprüft
werden. Die zugehörige bedingte Anweisung hat also typischerweise die folgende Form.
Dabei ist e der Parameter von handleEvent.
66
2.8. STRINGS UND CHARACTER
1
2
3
4
5
6
if (e instanceof SignalEvent) ...
else
{
Item i = ((ItemEvent) e).getItem();
...
}
• Zu beachten ist, daß auch nach der Unterscheidung der syntaktische Typ von e immer
noch Event ist und daher für den Aufruf von getItem ein cast notwendig ist.
2.7.2
for-Anweisungen
Wir benötigen for-Anweisungen zum Bearbeiten aller Elemente eines Vektors. Dies ist
notwendig in der Methode sendEvent, in der das Ereignis an alle Nachfolger geschickt
werden soll, in den Methoden releaseFromListeners und releaseFromSenders, in denen die
Verbindungen zu allen Nachfolgern bzw. Vorgängern gelöst werden soll und in der Methode
generateEvents, in der alle in events gespeicherten Ereignisse geschickt werden sollen.
• Im Fall der Bearbeitung aller im Vektor eventListeners gespeicherten Nachfolger hat
die zugehörige for-Anweisung typischerweise die Form
1
2
3
4
5
for (int i = 0; i < eventListeners.size(); i++)
{
EventListener l = (EventListener) eventListeners.elementAt(i);
...
}
Die Anweisung benutzt eine lokale Variable i für die laufende Position im Vektor. Die
Position beginnt bei 0 und endet daher eine Stelle vor der Anzahl der Elemente im
Vektor. Nach jedem Schleifendurchlauf wird i mittels i++ um 1 erhöht. Im Rumpf
der Anweisung wird auf das jeweils i-te Element zugegriffen und es wird zur weiteren
Verarbeitung in einer lokalen Variablen l abgelegt. Da die Methode elementAt als
Ergebnistyp Object hat, ist ein cast notwendig. Zu beachten ist, daß im Gegensatz zum
Beispiel bei der bedingten Anweisung der cast hier das Ergebnis des Methodenaufrufs
betrifft, nicht das Objekt, bei dem die Methode aufgerufen wird (daher die fehlende
Klammerung).
2.8
Strings und Character
Die Klassen Character und String im Standardpackage java.lang unterstützen den Umgang
mit Zeichen und Zeichenreihen.
2.8.1
Die Klasse Character
Character ist eine sogenannte „wrapper“-Klasse. Jede Instanz entspricht einem Wert vom
Datentyp char. Im Unterschied zu einer Character-Instanz kann ein char-Wert nicht erzeugt
werden und er enthält keine Instanzmethoden, die aufgerufen werden können. char-Werte sind
ebenso wie die Werte der übrigen primitiven Datentypen keine Objekte.
67
KAPITEL 2. VERERBUNG
• Konstruktor
Der wichtigste Konstruktor für den Umgang mit Character-Instanzen erzeugt eine
Character-Instanz zu einem als Parameter angegebenen char-Wert.
Character(char value)
• Zugriffsmethode
public char charValue() ...
liefert den zur Character-Instanz gehörigen char-Wert.
• Statische Methoden
Daneben definiert die Klasse Character einige statische Methoden zum Umgang mit
char-Werten, u.a. folgende Methoden zum Prüfen, ob ein char-Wert eine Ziffer, ein
Buchstabe, ein Zwischenraum, ein Kleinbuchstabe bzw. ein Großbuchstabe ist:
public
public
public
public
public
2.8.2
static
static
static
static
static
boolean
boolean
boolean
boolean
boolean
isDigit(char ch) ...
isLetter(char ch) ...
isSpaceChar(char ch) ...
isLowerCase(char ch) ...
isUpperCase(char ch) ...
Die Klasse String
Jede Instanz der Klasse String ist eine Zeichenreihe aus Werten vom Typ char.
• Die Klasse String nimmt in Java eine Sonderstellung ein. Es ist eine von zwei Klassen,
für die Literale existieren (außer dem Literal null). Ferner ist es die einzige Klasse, für
deren Instanzen ein Operator existiert.
– String-Literale
String-Literale (siehe den → Abschnitt über Datentypen (siehe Seite 14))
sind keine spezielle Form von Konstruktorausdrücken. Ein String-Literal im
Programmcode bezeichnet auch wenn der Code mehrfach durchlaufen wird jedesmal
die gleiche Instanz (ein Konstruktorausdruck würde bei jedem Durchlauf ein
neues Objekt erzeugen). An unterschiedlicher Stelle im Programmcode auftretende
String-Literale bezeichnen jedoch verschiedene Instanzen, auch wenn sie in der
Zeichenfolge übereinstimmen.
– Der String-Operator +
Der Operator + kann auf String-Instanzen angewendet werden. Er konkateniert
zwei Strings zu einem neuen String.
Beispiel
String s = "abc" + "def" + "ghi";
• Instanzmethoden
Wegen der String-Literale und dem +-Operator benötigt man meist keine der
(existierenden) Konstruktoren der Klasse String. Die wichtigsten Instanzmethoden für
den Umgang mit Strings sind:
68
2.8. STRINGS UND CHARACTER
public int length() ...
public char charAt(int index) ...
public String substring(int beginIndex, int endIndex) ...
– Die Methode length liefert die Anzahl der Zeichen im String, die Methode charAt
liefert den char-Wert an der Position index. Die beiden Methoden entsprechen also
den Methoden size und elementAt bei → Vektoren (siehe Seite 61). Im Unterschied
zu Vektoren können in einer String-Instanz jedoch keine char-Werte zugefügt oder
entfernt werden.
– Die Methode substring liefert einen (jeweils neu erzeugten) String, dessen
Inhalt die Zeichenfolge aus dem ursprünglichen String ab Position beginIndex
(einschließlich) bis zur Position endIndex (ausschließlich) ist.
• Stringdarstellung anderer Datentypen
In Java gibt es automatisch für jeden Datentyp eine Darstellung als String. Dies wird
durch die statische Methode valueOf der Klasse String realisiert. Mittels overloading
ist die Methode für Parameter aller primitiver Datentypen2 und für Parameter des Typs
Object definiert.
– Da die Version für Object mit Objekten eines beliebigen Laufzeittyps aufgerufen
werden kann (vgl. den → Abschnitt über Typanpassung (siehe Seite 51)), ist die
Methode String.valueOf tatsächlich auf jeden Datenwert in Java anwendbar
und liefert eine Stringdarstellung des Werts. In selbstdefinierten Klassen kann
die Darstellung über die Methode toString definiert werden: String.valueOf
verwendet diese Methode für die Darstellung von Instanzen, falls sie definiert ist.
Dies wird beispielsweise auch im Debugger bei der Ausgabe von Werten verwendet.
– In zwei wichtigen Kontexten wird die durch String.valueOf vorgenommene Konvertierung automatisch durchgeführt: Bei Argumenten der Methode
PrintStream.println und des Operators +.
∗ println
Die Methode PrintStream.println ist ebenso wie String.valueOf mittels
overloading für alle primitiven Datentypen und für Object definiert und
konvertiert ihren Parameter falls notwendig mittels String.valueOf vor der
Ausgabe. Daher ist die folgende Ausgabe einer int-Variablen counter zulässig:
System.out.println(counter);
∗ Operator +
Falls nur ein Operand von + vom Typ String ist, wird auf der andere zuerst
in einen String konvertiert, anschließend wird die Konkatenation der Strings
ausgeführt. Daher ist auch die folgende Zuweisung mit einer int-Variablen
counter zulässig:
String meldung = "Der Wert ist: " + counter;
2
Für die Datentypen byte und short existiert keine explizit definierte Variante von valueOf. Diese
Datentypen können jedoch immer an allen Stellen verwendet werden, an denen ein int erwartet wird. Sie
werden dann automatisch angepaßt.
69
KAPITEL 2. VERERBUNG
2.9
Die Zeichenzähler-Anwendung in MTJ
Wir definieren nun eine erste einfache Anwendung für eine Konfiguration aus Stationen.
Dabei sind alle Items einzelne Zeichen. Wir benutzen eine Generatorstation, die eine bei der
Erzeugung als String übergebene Zeichenfolge in die Konfiguration schickt. Die Konfiguration
enthält Filterstationen, die nur bestimmte Zeichen durchlassen und Zählerstationen, die die
bei ihnen ankommenden Zeichen zählen. Am Schluß sendet die Generatorstation ein Signal,
das die Zähler veranlaßt, die Anzahl der gezählten Zeichen auszugeben.
2.9.1
Character-Items
Als spezielle Art von Items benötigen wir Items für Einzelzeichen. Dazu leiten wir die Klasse
CharItem von Item ab. CharItem hat eine private Variable c vom Typ char, einen Konstruktor
mit char-Parameter, der c besetzt und eine Zugrifsmethode charValue, die den Wert von c
liefert. Damit ist CharItem ähnlich definiert wie die Klasse Character.
• Alternative
Eine Alternative wäre gewesen, Item als Interface zu definieren und dann CharItem
von Character abzuleiten und zusätzlich das Interface Item zu implementieren. Dies
entspräche noch mehr der Idee, daß Character-Items besondere Character sind, mit der
zusätzlichen Eigenschaft, als Item verwendet zu werden. Diese Alternative ist jedoch
nicht realisierbar, da Character als final definiert ist und man daher keine Klassen
davon ableiten kann.
2.9.2
Der Character-Generator
Für den Character-Generator leiten wir die Klasse CharGenerator von der Klasse GenStation
ab.
• Bestandteile
Als einzige neue Bestandteile definiert die Klasse die beiden Konstruktoren. Der eine
Konstruktor erhält neben dem Stationsnamen einen weiteren String-Parameter, der die
zu erzeugende Character-Itemfolge vorgibt. Der Konstruktor benutzt den Konstruktor
der Superklasse, um den Stationsnamen einzutragen. Anschließend durchläuft er den
zweiten String mittels einer for-Anweisung und erzeugt zu jedem char-Wert im String
ein Item-Ereignis, das im Vektor events abgelegt wird. Als letztes wird ein SignalEreignis erzeugt und in events abgelegt.
• Das Durchlaufen des Strings erfolgt analog zum Durchlaufen eines Vektors unter
Verwendung der Methoden length und charAt.
• Der zweite Konstruktor hat nur den String für die Itemfolge als Parameter. Er benutzt
den ersten Konstruktor und gibt dabei den Standardnamen "character generator
station" vor.
70
2.9. DIE ZEICHENZÄHLER-ANWENDUNG IN MTJ
2.9.3
Die Filterstationen
Wir definieren drei verschiedene Filterstationen.
• UCFilterStation
Läßt nur Großbuchstaben durch.
• LCFilterStation
Läßt nur Kleinbuchstaben durch.
• DGFilterStation
Läßt nur Ziffern durch.
• Alle drei Klassen sind von ProcStation abgeleitet. Als neue Bestandteile definieren sie
die zwei Konstruktoren und implementieren die Methode handleEvent.
• Konstruktoren
Bei der Erzeugung einer Filterstation ist höchstens der Stationsname relevant.
Entsprechend hat der eine Konstruktor den Stationsnamen als Parameter und benutzt
den Konstruktor der Superklasse, um ihn abzulegen. Der andere Konstruktor hat keinen
Parameter. Er benutzt den ersten Konstruktor und gibt ihm den Standardnamen vor:
"upper case filter", "lower case filter", bzw. "digit filter".
• handleEvent
Unterscheiden zwischen Item- und Signalereignissen.
– Item-Ereignisse
Die Methode handleEvent prüft bei jedem Item-Ereignis das enthaltene Zeichen
mit einer bedingten Anweisung und einer der im → Abschnitt zur Klasse Character
(siehe Seite 67) beschriebenen Methode, ob es durchgelassen werden soll. Nur in
diesem Fall wird das Ereignis mittels sendEvent weitergeschickt.
– Signalereignisse
Signalereignisse sollen dagegen in jedem Fall weitergeschickt werden.
2.9.4
Die Zählerstation
Eine Zählerstation soll einfach die eintreffenden Itemereignisse zählen. Dabei ist es irrelevant,
ob es sich um Character-Items oder andere Items handelt.
• Dazu leiten wir die Klasse ItemCounterStation von ProcStation ab und definieren als
neuen Bestandteil eine private int-Variable counter, die mit 0 initialisiert wird und
zum Zählen der Items dient.
• handleEvent
Als weitere neue Bestandteile definieren wir wie bei den Filterstationen die beiden
Konstruktoren und die Methode handleEvent. In handleEvent muß zwischen Signalen
71
KAPITEL 2. VERERBUNG
und Items unterschieden werden. Im Fall eines Items wird counter hochgezählt. Im
Fall eines Signals wird eine Meldung mit der println-Methode der Standardausgabe
System.out ausgegeben. Die Meldung soll den Stationsnamen getName() und den
aktuellen Stand von counter enthalten.
– Die Zählerstation läßt alle Ereignisse durch, am Ende von handleEvent wird also
jedes Ereignis mit sendEvent weitergeschickt.
Aufgaben
1. Aufgabe 3 Zeichenzähler
Das in Kapitel 2 beschriebene System ist zu implementieren. In diesem System soll die
folgende Konfiguration aufgebaut und durchlaufen werden:
Abbildung 2.13: Konfiguration für Zeichenzähler (Aufgabe)
Die Konfiguration leitet den vom Generator erzeugten Zeichenstrom parallel durch die
drei Filter und zählt danach jeweils die durchgelassenen Zeichen. Um die Ausgaben der
Zähler unterscheiden zu können hat jeder Zähler einen spezifischen Stationsnamen. Die
übrigen Stationen verwenden ihren Standardnamen.
• Package
Alle zu implementierenden Klassen sollen dem Package mtj.level2 angehören.
Jede Klasse soll public sein und in einer eigenen Quelldatei definiert werden.
a) Entwurf
Veranschaulichen Sie sich die Vererbungshierarchie der Klassen und Interfaces in
einem UML-Klassendiagramm. Es handelt sich um insgesamt 13 Klassen und 3
Interfaces.
b) Interfaces
Schreiben Sie die in der Systembeschreibung vorgegebenen 3 Interface-Definitionen
in entsprechende Quelldateien.
72
2.9. DIE ZEICHENZÄHLER-ANWENDUNG IN MTJ
c) Implementierung: Klassen
Implementieren Sie die 13 Klassen wie in der Systembeschreibung informell
beschrieben.
d) Test: Klassen
Testen Sie jede der Klassen für konkrete Stationen einzeln. Dabei können Sie wie
folgt vorgehen.
• Test der Generatorstation
Definieren Sie eine Klasse mtj.level2.Aufg03d, in deren main-Methode
eine Instanz der Character-Generatorstation erzeugt wird und für die
generateEvents aufgerufen wird. Führen Sie damit einen Programmlauf im
Debugger durch und überzeugen Sie sich, daß alle Items gesendet werden.
• Test der Arbeitsstationen
Modifizieren Sie dann für jede Art von Arbeitsstation die main-Methode so, daß
zusätzlich eine Instanz der Arbeitsstation an die Generatorstation angehängt
wird und überzeugen Sie sich in einem Debugger-Lauf, daß die Arbeitsstation
die Items in der gewünschten Weise verarbeitet.
e) Implementierung und Test: Rahmen
Definieren Sie die Klasse mtj.level2.Aufg03e, die in der main-Methode die oben
dargestellte Konfiguration aufbaut und einen Durchlauf aktiviert. Verwenden
Sie für den Generator eine beliebige Zeichenfolge. Machen Sie Probeläufe des
entsprechenden Programms.
73
KAPITEL 2. VERERBUNG
74
Kapitel 3
Stationen
Im dritten Abschnitt des Praktikums wird eine Reihe von Konzepten vorgestellt und für die
Verfeinerung der Stationsklassen verwendet.
3.1
MTJ - Stufe 3
In Stufe 3 des Praktikums werden die Stations-Klassen weiter verfeinert. Die wesentlichen
Themen sind Fehlerbehandlung beim Aufbau von Konfigurationen und eine Klassifizierung
der Arbeitsstationen anhand der Art, wie sie mit Items umgehen. Für die Implementierung
wird wiederum ein neues Package mtj.level3 angelegt und die Klassenhierarchie überarbeitet.
3.2
Reihungen
Im → Abschnitt zu Datentypen (siehe Seite 14) wurden Reihungen, auch Arrays genannt,
als dritte Art von Datentypen in Java genannt. Reihungen sind vergleichbar mit → Vektoren
(siehe Seite 61). Jede Reihung besteht aus einer Folge von Werten.
3.2.1
Unterschiede zu Vektoren
Wesentliche Unterschiede zu Vektoren sind:
• Vektoren können nur Objekte enthalten. Reihungen können dagegen Werte beliebiger
(auch primitiver) Java-Datentypen enthalten.
• Vektoren haben immer den Datentyp Vector. Bei Reihungen existiert dagegen für jeden
möglichen Element-Datentyp ein eigener Reihungsdatentyp.
• Für die Erzeugung von Reihungen und den Zugriff auf Reihungen existieren spezielle
Sprachmittel in Java.
• Die Anzahl von Elementen in einem Vektor kann sich dynamisch ändern, wenn Elemente
zugefügt oder entfernt werden. Bei einer Reihung ist die Anzahl der Elemente dagegen
fest und muß bereits bei der Erzeugung der Reihung spezifiziert werden. Es ist dann nur
noch möglich, enthaltene Elemente zu ersetzen.
75
KAPITEL 3. STATIONEN
3.2.2
Reihungs-Datentypen
Jeder Datentyp für eine Reihung ergibt sich aus dem Datentyp für die Elemente durch
Anhängen von []. Die Elemente können insbesondere selbst wieder Reihungen sein, in diesem
Fall ergibt sich ein Datentyp für eine mehrdimensionale Reihung.
• Reihungen als Objekte
In Java sind alle Reihungen spezielle Objekte. Insbesondere gilt jeder Reihungs-Datentyp
als von Object abgeleitete Klasse. Im Unterschied zu anderen Klassen existiert jedoch
keine zugehörige Klassendefinition. Alle Reihungs-Klassen werden implizit durch den
Java-Compiler definiert, sobald sie verwendet werden.
• Reihungs-Typen als Subklassen von Object
Als Subklassen von Object erben Reihungen alle Bestandteile von Object. Weiterhin
ist Object ein zulässiger syntaktischer Typ für alle Reihungen. Jede Reihung kann also
beispielsweise einer Variablen vom Datentyp Object zugewiesen werden.
3.2.3
Erzeugung von Reihungen
Reihungen werden wie alle übrigen Objekte mit Hilfe von → Konstruktorausdrücken (siehe
Seite 28) erzeugt.
Konstruktorausdrücke für Reihungen
Die Konstruktorausdrücke für Reihungen haben eine spezielle Form1 :
new <Element-Datentyp> [ <Ausdruck> ]
new <Element-Datentyp> [] { <Ausdruck>, ..., <Ausdruck> }
In der ersten Form wird nur die Anzahl der Elemente durch den Ausdruck angegeben. Der
Ausdruck muß einen Wert vom Typ int liefern. In der zweiten Form werden dagegen alle
Elemente explizit angegeben. Jeder der Ausdrücke muß einen Wert vom Element-Datentyp
der Reihung liefern. Die Elemente sind in der erzeugten Reihung in der Reihenfolge der
Aufschreibung enthalten.
Beispiele für Reihungs-Konstruktorausdrücke sind
new
new
new
new
int [5]
Object [ Bsp.addiere(14,7) ]
String[] { "Modulare", "Textverarbeitung", "in", "Java" }
Station[] { new CounterStation(), new DGFilterStation() }
• Angabe beliebig langer Wertefolgen
Reihungs-Konstruktorausdrücke der zweiten Form haben eine wichtige Anwendung: Sie
sind in Java die einzige Möglichkeit, eine Folge beliebig vieler Werte direkt in einem
Ausdruck anzugeben. Während es beispielsweise in C in der Prozedur println erlaubt
ist, eine beliebige Anzahl von Parametern anzugeben, hat in Java jede Methode eine feste
1
die hier angegebenen Formen gelten strenggenommen nur für eindimensionale Reihungen
76
3.2. REIHUNGEN
Parameterzahl. Aus diesem Grund gibt es auch keinen Konstruktor für Vektoren, dem
die Elemente des Vektors als Parameter übergeben werden können. Vektoren müssen
immer separat erzeugt und dann mit Elementen besetzt werden.
3.2.4
Zugriff auf Reihungen
Zugriffe auf Reihungen erlauben die Abfrage der Anzahl der enthaltenen Elemente und das
Lesen und Ersetzen einzelner Elemente.
Anzahl der Elemente
Jede Reihung hat als Bestandteil die Variable length vom Typ int. Diese Variable enthält
die (feste) Anzahl der Elemente in der Reihung. Zu einer beliebigen Reihung läßt sich also die
Anzahl der Elemente mit folgendem Ausdruck ermitteln:
<Ausdruck fuer Reihung>.length
Der Ausdruck r.length mit einer Reihung r entspricht also dem Ausdruck v.size() mit
einem Vektor v. Eine typische Anwendung ist in einer for-Anweisung zum Durchlaufen einer
Reihung r:
for (int i = 0; i < r.length; i++) ...
Elemente
Auf die Elemente einer Reihung kann wie bei Vektoren durch die Angabe der Position
(beginnend bei 0) zugegriffen werden. Dem Zugriff
v.elementAt(<Ausdruck>)
bei einem Vektor v entspricht die spezielle Schreibweise
r [<Ausdruck>]
bei einer Reihung r. Zusätzlich kann diese Schreibweise bei Reihungen jedoch auch verwendet
werden, um ein Element zu ersetzen. Dazu wird das Reihungselement wie eine Variable in
einer Zuweisung verwendet:
r [<Ausdruck>] = <Ausdruck fuer neues Element>
Beispiel für den Umgang mit Reihungen
1
2
3
4
int[] zahlen = new int[] {1, 2, 3, 4};
zahlen[0] = 15;
for (int i = 0; i < zahlen.length; i++)
System.out.println(zahlen[i]);
77
KAPITEL 3. STATIONEN
3.3
String-Items in MTJ
Wir wollen nun eine zweite Art von Items neben den CharItems einführen: Items für Strings.
3.3.1
Die Klasse StringItem
Dazu leiten wir die neue Klasse StringItem von Item ab. Dies geschieht analog zur Definition
von CharItem: StringItem enthält eine private String-Variable, einen Konstruktor mit
einem String-Parameter und die parameterlose Methode getString, die den im StringItem
enthaltenen String liefert.
3.3.2
Die Klasse StringGenerator
Um String-Items in Stationskonfigurationen verwenden zu können, brauchen wir eine
Generatorstation, die String-Items erzeugt. Analog zu CharGenerator leiten wir dazu die
Klasse StringGenerator von der Klasse GenStation ab.
• Das einzige Problem ergibt sich bei der Spezifikation der gewünschten Itemfolge bei
der Konstruktion der Generatorstation. Im Fall von CharGenerator haben wir einen
String benutzt, um eine Folge von Zeichen an die Konstruktoren zu übergeben. Im Fall
von StringGenerator müssen wir dagegen eine Folge von Strings übergeben. Dies ist
entweder mit einem Vektor oder mit einer Reihung möglich.
• Wir entscheiden uns für die Reihung, weil es dann möglich ist, die Stringfolge direkt in
Konstruktorausdrücken für StringGenerator anzugeben. Die Konstruktoren verwenden
also Parameter vom Datentyp String[]. Dann kann eine Generatorstation, die eine
Folge von StringItems mit den Strings "erster", "zweiter", "dritter" generiert mit
dem folgenden Konstruktorausdruck erzeugt werden:
new StringGenerator(new String [] { "erster", "zweiter", "dritter" })
3.4
Ausnahmebehandlung
Eine typische Situation in Programmen ist die, daß bei der Abarbeitung einer Funktion
ein Fehler erkannt wird (beispielsweise ein fehlerhafter Parameterwert oder ein fehlerhafte
Benutzereingabe).
Beispiel: Division
Das Standardbeispiel hierfür ist eine Funktion für die Division, die in Java als statische
Methode dividiere in einer Klasse Bsp definiert werden könnte:
static float dividiere(float dividend, float divisor)
{ return dividend / divisor; }
Die Fehlersituation in dividiere tritt auf, wenn der divisor den Wert 0 hat. Die einfachste
Behandlung dieser Situation ist ein kompletter Programmabbruch.
78
3.4. AUSNAHMEBEHANDLUNG
• Alternativen zum Abbruch
Soll das Programm nicht komplett abgebrochen werden, so muß unabhängig von der
Fehlerbehandlung innerhalb der Methode (beispielsweise Ausgabe einer Fehlermeldung
auf die Standardausgabe) die Methode irgendwann verlassen werden und dabei ein
Ergebniswert geliefert werden. Das Problem liegt dabei darin, einen geeigneten Wert
zu finden. Günstig wäre ein Wert, an dem abzulesen ist, daß ein Fehler auftrat (im
Fall von natürlichen Zahlen, die mit Hilfe des Datentyps int dargestellt verwendet man
häufig -1 für diesen Zweck). Im Fall von dividiere ist das jedoch nicht möglich. Ein
weiterer Nachteil ist, daß nun alle Ergebnisse von Methoden einzeln darauf untersucht
werden müssen, ob sie eine Fehlersituation anzeigen.
3.4.1
Ausnahmen
Aus den beschriebenen Gründen gibt es in Java und einigen anderen Programmiersprachen
einen speziellen Mechanismus für die Behandlung von Laufzeitfehlern. Er erlaubt die Meldung
von „Ausnahmen“ und deren separate Behandlung.
Ausnahme melden
Definition: Erkennt man in einer Methode eine Fehlersituation, so kann man eine Ausnahme
melden. Dies bewirkt, daß die Abarbeitung der Methode sofort abgebrochen wird und ohne
ein Ergebnis zu liefern in die aufrufende Methode zurückgekehrt wird.
Wird die Ausnahme dort nicht behandelt, so wird auch diese Methode abgebrochen usw.
Auf diese Weise erreicht man irgendwann die als Hauptprogramm aufgerufene Methode main.
Wird auch hier die Ausnahme nicht behandelt, so wird sie ebenfalls verlassen (das entspricht
also einem Programmabbruch) und die Ausnahme wird vom Java-Laufzeitsystem dadurch
behandelt, daß eine Beschreibung der Ausnahme auf die Standardausgabe geschrieben wird.
Ausnahme abfangen
Definition: Man kann Ausnahmen jedoch an beliebiger Stelle abfangen und Anweisungen
schreiben, die sie dort geeignet behandeln.
Ein Vorteil ist, daß auf diese Weise an einer Stelle (z.B. im Hauptprogramm) Ausnahmen aus
vielen verschiedenen Methodenaufrufen behandelt werden können.
Ausnahme-Objekte
Ausnahmen sind in Java Objekte. Alle Ausnahmen sind Instanzen der Klasse
java.lang.Throwable oder einer davon abgeleiteten Klasse. Die Klasse Throwable ist direkt
von Object abgeleitet. Als zusätzliche Bestandteile definiert sie eine Fehlermeldung vom Typ
String und eine Beschreibung der Stelle, an der die Ausnahme gemeldet wurde (einen sogenannten „Stack Trace“). Die Fehlermeldung kann dem Konstruktor als Parameter mitgegeben
werden, der Stack Trace wird automatisch vom Java-Laufzeitsystem eingetragen.
79
KAPITEL 3. STATIONEN
• Abgeleitete Ausnahme-Klassen
Weil Ausnahmen Objekte sind, können sie beliebig viel zusätzliche Information
über die aufgetretene Fehlersituation enthalten. Außerdem lassen sich Klassen für
unterschiedliche Fehlersituationen von Throwable ableiten. Vordefiniert sind die Klassen
java.lang.Error und java.lang.Exception, die beide direkt von Throwable abgeleitet
sind.
– Error
Die Klasse Error umfaßt alle Ausnahmen, die nicht sinnvoll innerhalb des
Programms behandelt werden können (beispielsweise Hauptspeichermangel) und
daher normalerweise nicht abgefangen werden sollen.
– Exception
Alle übrigen Ausnahmen gehören zu Exception. Von der Klasse Exception ist
beispielsweise wiederum die Klasse java.lang.RuntimeException abgeleitet.
Abbildung 3.1: Klassen für Ausnahmen
3.4.2
Melden von Ausnahmen
Eine Ausnahme wird gemeldet durch eine Anweisung der Form
throw <Ausdruck>;
Der Ausdruck muß einen Wert vom Typ Throwable liefern. Typischerweise wird die
Ausnahme bei der Meldung neu erzeugt, dann handelt es sich bei dem Ausdruck um einen
Konstruktorausdruck.
throw new Exception("ein Fehler ist aufgetreten");
80
3.4. AUSNAHMEBEHANDLUNG
Ausnahme-Meldung bei Division
Im Fall der oben beschriebenen Methode dividiere könnte die vordefinierte Klasse
java.lang.ArithmeticException (abgeleitet von RuntimeException) verwendet werden.
Dann läßt sich der Fehler wie folgt erkennen und melden:
1
2
3
4
if (divisor == 0)
throw new ArithmeticException("Division durch 0");
else
return dividend / divisor;
3.4.3
Abfangen von Ausnahmen
Für jede beliebige Anweisungsfolge kann definiert werden, daß bei der Abarbeitung der Folge
auftretende Ausnahmen abgefangen werden sollen.
• try-Anweisung
Dazu wird die Folge in einer try-Anweisung geklammert:
try { <Anweisungsfolge> }
• catch-Anweisung
Unmittelbar danach muß die Behandlung der abgefangenen Ausnahmen spezifiziert
werden. Dies geschieht mittels einer catch-Anweisung der Form
catch(<Klassenname> <Identifikator>) { <Anweisungsfolge> }
Der Klassenname muß eine von Throwable abgeleitete Klasse bezeichnen. Der Identifikator spielt die Rolle einer lokalen Variable, die die abgefangene Ausnahme enthält. Die
Behandlung der Ausnahme geschieht durch Abarbeitung der Anweisungsfolge.
• Die catch-Anweisung behandelt nur Ausnahmen, die der angegebenen Klasse angehören
oder einer davon abgeleiteten Klasse. Es lassen sich jedoch zu einer try-Anweisung
mehrere catch-Anweisungen hintereinander spezifizieren. Auf diese Weise können
unterschiedliche Ausnahmen unterschiedlich behandelt werden.
Beispiel für try/catch Konstrukt
1
2
3
4
try { ... }
catch(ArithmeticException e) { ... }
catch(ClassCastException e) { ... }
catch(IndexOutOfBoundsException e) { ... }
• Eine try-Anweisung fängt nur die Ausnahmen ab, die durch nachfolgende catchAnweisungen behandelt werden.
81
KAPITEL 3. STATIONEN
Abbildung 3.2: Schema zum Abfangen von Ausnahmen
Die catch-Anweisung wirkt wie ein Filter, der die zugehörige try-Anweisung „überspannt“.
Alle Ausnahmen, die während der Abarbeitung der try-Anweisung gemeldet werden und
nicht weiter „innen“ abgefangen werden, gelangen an das Filter. Dort werden sie entweder
durchgelassen oder in die catch-Anweisung umgelenkt.
3.4.4
Deklaration von Ausnahmen
Man kann die Ausnahmen, die in einer Methode gemeldet werden können, als zusätzliche
spezielle Ergebnisse der Methode ansehen. Entsprechend müssen in Java die Klassen aller
Ausnahmen, die eine Methode melden kann, bei der Methodendefinition angegeben werden.
Dies betrifft alle Klassen außer den von Error und von RuntimeException abgeleiteten.
• Die Ausnahme-Klassen werden in der Methodendefinition unmittelbar vor dem Rumpf
(oder bei abstrakten Methoden vor dem Semikolon) in der folgenden Form angegeben.
throws <Klassenname>, ..., <Klassenname>
Definition der Methode dividiere
1
2
3
4
5
6
7
static float dividiere(float dividend, float divisor) throws ArithmeticException
{
if (divisor == 0)
throw new ArithmeticException("Division durch 0");
else
return dividend / divisor;
}
82
3.5. ITEM-KOMPATIBLE STATIONSVERBINDUNGEN IN MTJ
(Da ArithmeticException von RuntimeException abgeleitet ist, ist die Angabe in
diesem Fall nicht notwendig.)
• Die in der Deklaration angegebene Klasse muß nicht der Laufzeittyp der gemeldeten
Ausnahme sein, sondern kann auch eine Basisklasse davon sein. Auf diese Weise lassen
sich ggf. mehrere verschiedene Ausnahmeklassen mittels einer gemeinsamen Basisklasse
deklarieren. Im Extremfall kann man beliebig viele verschiedene Ausnahmen mit der
folgenden Deklaration angeben:
throws Exception
• Wird in einer Methode eine andere Methode aufgerufen, die eine Ausnahme melden kann,
so muß die Ausnahme auch für die aufrufende Methode deklariert werden, außer wenn
die Ausnahme in der aufrufenden Methode abgefangen wird. Der Compiler überprüft
dies und meldet ggf. einen Fehler.
3.5
Item-kompatible Stationsverbindungen in MTJ
Mit den Klassen CharItem und StringItem existieren inzwischen zwei unterschiedliche ItemArten. Stationen wie die Filterstationen können jedoch nur eine bestimmte Art verarbeiten.
Bei dem Konfigurationsaufbau ist daher darauf zu achten, daß nur Stationsausgänge mit
Stationseingängen verbunden werden, die die gleiche Item-Art verarbeiten können. Der
Versuch, eine andere Verbindung herzustellen ist eine Fehlersituation.
Wir wollen diese Fehlersituationen erkennen und als Ausnahme melden. Auf diese Weise läßt
sich garantieren, daß nur korrekte Konfigurationen aufgebaut werden können.
3.5.1
Die Klasse MtjException
Wir verwenden für alle Ausnahmen eine neue Klasse MtjException. Die Klasse ist abgeleitet
von Exception. Methoden, die MtjException-Ausnahmen melden, müssen sie deklarieren.
• MtjException erbt von Throwable den Fehlermeldungs-String. Diesen benutzen wir, um
die genauere Fehlerursache zu beschreiben. Er wird im Konstruktorausdruck angegeben.
3.5.2
Verwaltung der Item-Arten
Jede Arbeitsstation hat einen Eingang und einen Ausgang. Die Art der Items am Eingang kann
sich von der am Ausgang unterscheiden. Daher verwalten wir für jede Station die zulässige
Item-Art separat für Eingang und Ausgang. Generatorstationen haben nur einen Ausgang und
verwalten entsprechend nur eine Item-Art.
Abfrage der Item-Arten bei Stationen
Wir erweitern dazu das Interface EventSender um eine parameterlose Methode sendItemKind,
die die Art der gesendeten Items liefert und das Interface EventListener um eine
entsprechende Methode recItemKind für die Art der am Eingang erwarteten Items.
83
KAPITEL 3. STATIONEN
Implementierung
In der Klasse Station wird sendItemKind mit Hilfe einer Variablen sendItemKind
implementiert. Die Methode liefert einfach den Wert der Variablen.
2
• Die Variable kann in Station noch nicht besetzt werden, da erst in der abgeleiteten
Klasse für eine konkrete Station bekannt ist, welche Art von Items sie sendet. Daher
muß die Variable als protected deklariert werden, damit abgeleitete Klassen auf sie
zugreifen können. In einer Klasse für eine konkrete Station wird die Variable dann im
Konstruktor auf die jeweils passende Art gesetzt.
• Analog wird die Methode recItemKind in ProcStation mit Hilfe der Variable
recItemKind implementiert.
3.5.3
Prüfung der Item-Arten
Die Prüfung findet beim Verknüpfen zweier Stationen statt, betroffen sind also die Methoden
addEventListener und setSender. Wir führen die Prüfung in addEventListener durch.
Zu prüfen ist, ob die eigene sendItemKind() übereinstimmt mit der recItemKind() des
listener. Ansonsten wird eine MtjException gemeldet mit einem Fehlertext, der die Namen
beider Stationen enthält.
• Die Methode addEventListener muß nun noch um die Deklaration der Ausnahmen
vom Typ MtjException erweitert werden. Das gleiche gilt für alle Methoden, in denen
addEventListener aufgerufen wird ohne die MtjException abzufangen.
• Auf Stufe 3 des Systems wird addEventListener immer nur im Hauptprogramm main
aufgerufen, wenn die Konfiguration zusammengesetzt wird. Eine sinnvolle Behandlung
ist hier nicht möglich, da die Stationsverkettung fest im Programm spezifiziert ist.
Daher wird die MtjException im Hauptprogramm nicht abgefangen und statt dessen
als gemeldete Ausnahme der Methode main deklariert.
3.6
Die Klasse Class
In Sprachen wie C und Pascal ist die Überprüfung von Datentypen nur durch den Compiler
oder (eingeschränkt) durch das Laufzeitsystem möglich. Der Programmierer kann selbst keine
Typprüfungen und Reaktionen darauf implementieren.
In Java ist dies anders. Eine einfache Form der Typprüfung haben wir schon → mit
dem instanceof-Operator (siehe Seite 51) kennengelernt. Er erlaubt es zu prüfen, ob der
Laufzeittyp eines Objekts von einer fest angegebenen Klasse abgeleitet ist. Damit ist es jedoch
beispielsweise nicht möglich zu prüfen, ob zwei Objekte den gleichen Laufzeittyp haben.
• Objekte für Datentypen
Um einen flexiblen Umgang mit Datentypen in Programmen zu erlauben, stellt Java zu
jedem Datentyp ein repräsentierendes Objekt bereit. Alle diese Objekte sind Instanzen
der von Object abgeleiteten Klasse java.lang.Class.
2
Hinweis: gleicher Name wie die Methode ist zulässig wegen overloading
84
3.7. DARSTELLUNG FÜR ITEM-ARTEN BEI MTJ
• Abfrage des Datentyp-Objekts
Die Class-Instanz zum Laufzeittyp eines Objekts läßt sich mit der parameterlosen
Methode getClass abfragen. Die Methode ist in der Klasse Object definiert und daher
auf alle Objekte anwendbar (auch auf Reihungen).
• Class-Literale
Für Werte primitiver Datentypen läßt sich die zum Datentyp gehörige Class-Instanz
nicht direkt ermitteln. Es ist jedoch in jedem Fall möglich, ausgehend vom Datentyp die
Class-Instanz zu ermitteln. Dies geschieht mit speziellen Literalen („class literals“) der
Form
<Datentyp>.class
– Es handelt sich dabei im Falle, daß der Datentyp eine Klasse ist, nicht um einen
statischen Bestandteil der Klasse, wie die Schreibweise vermuten ließe, sondern um
eine Schreibweise für beliebige Datentypen. Die Klasse Class ist neben der Klasse
String (vgl. Abschnitt → (siehe Seite 68)) die einzige Klasse für deren Instanzen
die Sprache Java Literale anbietet.
Beispiele für Class-Literale
Object.class
int.class
String[][].class
• Zu jedem Datentyp, der in einem Programmlauf benutzt wird, existiert genau eine ClassInstanz. Auch wenn das gleiche Literal an verschiedenen Stellen im Programm steht,
liefert es immer die selbe Instanz (im Unterschied zu String-Literalen). Aus diesem
Grund lassen sich Class-Instanzen in Verbindung mit den Vergleichsoperatoren == und
!= dazu verwenden, die Gleichheit bzw. Verschiedenheit von Datentypen zu prüfen. Zwei
Datentypen sind genau dann gleich, wenn zu ihnen die selbe Class-Instanz gehört.
3.7
Darstellung für Item-Arten bei MTJ
Wir verwenden Class-Instanzen für die Repräsentation der Item-Arten bei Stationen. Eine
Item-Art wird durch die Class-Instanz zum Datentyp des Items dargestellt, also beispielsweise
durch CharItem.class oder StringItem.class.
• Entsprechend lautet die Definition der Methode sendItemKind im Interface
EventSender bzw. die der Variablen in der Klasse Station:
public Class sendItemKind();
protected Class sendItemKind;
• Alternativ könnte man Item-Arten durch Zahlen-Codes darstellen. Das hat jedoch
den Nachteil, daß man eine (willkürliche) Zuordnung zwischen Item-Arten und Zahlen
vornehmen muß (z.B. 0 für CharItem und 1 für StringItem) und diese Zuordnung bei der
Einführung neuer Item-Arten erweitern muß. Die Class-Instanzen stellt Java dagegen
automatisch zur Verfügung.
85
KAPITEL 3. STATIONEN
3.8
Verschattung und Überschreibung
Eine abgeleitete Klasse erbt alle Bestandteile ihrer Basisklasse. Sie kann selbst weitere neue
Bestandteile definieren. Dabei ist es insbesondere möglich, daß ein neuer Bestandteil den
gleichen Namen erhält wie ein ererbter Bestandteil. Dies wird für Variable und Methoden
unterschiedlich behandelt.
Definition: Im Fall von Variablen spricht man von Verschattung, im Fall von Methoden
von Überschreibung („overriding“).
3.8.1
Verschattung
Falls eine von der Klasse A abgeleitete Klasse B eine Variable mit dem Identifikator einer in A
enthaltenen und damit an B vererbten Variable definiert, so „verschattet“ die neue Variable die
ererbte. Dies bedeutet, daß der Identifikator in der abgeleiteten Klasse immer die neue Variable
bezeichnet. Dabei spielt es keine Rolle, ob die beiden Variablen den gleichen Datentyp haben
oder nicht.
Statische Variable
Im Falle einer statischen Variable sv kann man sich damit behelfen, daß man den
Klassennamen voranstellt. Damit sind beide Variable als A.sv und B.sv problemlos zugänglich.
Instanzvariable
Im Fall einer Instanzvariable iv ist dieses Vorgehen nicht möglich, da der Zugriff immer
über eine Objektreferenz geschieht und die Klasse des Objekts entscheidet, welche Variable
verwendet wird. Bei einem Objekt ob mit Laufzeittyp B bezeichnet ob.iv immer die in B
definierte Variable, obwohl die in A enthaltene gleichnamige Variable ebenfalls Bestandteil
von ob ist.
• In Java ist hier jedoch der syntaktische Typ (vgl. Abschnitt → (siehe Seite 51)) des
Objekts entscheidend. Ist beim Zugriff der syntaktische Typ von ob die Klasse A, so
wird auf die ererbte Variable zugegriffen. Der syntaktische Typ läßt sich beispielsweise
durch einen cast ändern: Der Zugriff ((A)ob).iv liefert die in A enthaltene iv-Variable.
• Auf diese Weise läßt sich auch innerhalb der Klasse B in Instanzmethoden auf die in A
enthaltene Variable zugreifen. Dazu verwendet man this als Objektreferenz und castet
sie nach A: während der Zugriff iv die in B definierte Variable anspricht, greift der
folgende Zugriff auf die in A enthaltene Variable zu:
((A)this).iv
• Ist A die direkte Superklasse von B, so kann anstelle von ((A)this).iv die folgende
spezielle Schreibweise verwendet werden:
super.iv
86
3.8. VERSCHATTUNG UND ÜBERSCHREIBUNG
3.8.2
Überschreibung
Falls die von A abgeleitete Klasse B eine Methode mit dem Identifikator einer ererbten Methode
definiert, so überschreibt die neue Methode die ererbte nur dann, falls sie zusätzlich im
Datentyp des Ergebnisses und aller Parameter übereinstimmt. Ansonsten liegt → overloading
(siehe Seite 17) vor und die Methoden werden wie üblich durch die Verwendung unterschieden.
Bei overriding bezeichnet der Identifikator in der abgeleiteten Klasse dagegen immer die neue
Methode.
Statische Methode
Im Falle einer statischen Methode kann man die Methoden wie im Fall der statischen Variablen
dadurch eindeutig bezeichnen, daß man den Klassennamen voranstellt.
Instanzmethode
Im Fall einer Instanzmethode im ist dieses Vorgehen nicht möglich. Wieder entscheidet die
Klasse des Objekts, welche Methode verwendet wird. Im Unterschied zu Variablen ist hier
jedoch immer der Laufzeittyp des Objekts entscheidend. Ein cast der Objektreferenz beim
Zugriff auf die Methode ist hier also wirkungslos.
In
1
2
3
4
A oa = new A();
oa.im(...);
oa = new B();
oa.im(...);
wird im zweiten Fall die in B definierte im-Methode aufgerufen, im ersten Fall die in A definierte.
• Diese Art der Methodenzuordnung über den Laufzeittyp bedeutet, daß zur Übersetzungszeit im allgemeinen nicht feststeht, welche Methode bei einem Aufruf verwendet
wird.
Definition: Die Zuordnung über den Laufzeittyp wird daher als dynamische Bindung
bezeichnet.
• Dynamische Bindung nutzt man in objektorientierter Programmierung häufig systematisch, um objektspezifisches Verhalten unter einem einheitlichen Namen auszulösen.
Ein beliebtes Beispiel ist eine draw-Methode, die ein Objekt an der Benutzeroberfläche
darstellen soll. Jede abgeleitete Klasse überschreibt diese Methode und definiert ihre
spezifische Darstellungsweise. Trotzdem kann jedes Objekt einheitlich durch einen Aufruf
von draw dargestellt werden.
Aufruf einer überschriebenen Methode
Der Aufruf einer überschriebenen Methode für ein Objekt ist nur mit Hilfe der superSchreibweise möglich. In Instanzmethoden einer Klasse bewirkt der folgende Ausdruck den
Aufruf der in der direkten Superklasse enthaltenen Methode, auch wenn diese in der aktuellen
Klasse überschrieben wurde:
87
KAPITEL 3. STATIONEN
super.<Methodenname>(<Ausdruck>, <Ausdruck>, ...)
• Dieser Mechanismus wird üblicherweise in der neuen Methodendefinition selbst
verwendet, wenn die überschriebene Methode einen Teil der neuen Definition bilden
soll.
Beispiel
Soll beispielsweise in einer abgeleiteten Klasse die Darstellung mittels draw nur durch
zusätzliche Anweisungen ergänzt werden, so überschreibt man draw, ruft aber im Rumpf
die überschriebene Version mittels super auf:
1
2
3
4
5
6
void draw()
{
...
super.draw();
...
}
• Im Gegensatz zum Konstruktoraufruf mittels super kann der Aufruf einer überschriebenen Methode an beliebiger Stelle im Rumpf vorkommen und kann auch mehrfach
auftreten.
Abstrakte Methoden
Auch die Implementierung einer → abstrakten Methode (siehe Seite 53) ist ein Spezialfall von
Überschreibung: Die ererbte abstrakte Methode ohne Rumpf wird durch eine Definition einer
gleichnamigen Methode mit Rumpf überschrieben. Weil der Laufzeittyp die zu verwendende
Methode bestimmt und eine (abstrakte) Klasse mit abstrakten Methoden nie Laufzeittyp eines
Objekts sein kann, wird nie eine abstrakte Methode aufgerufen, sondern immer nur eine ihrer
Implementierungen.
Beispiel für Überschreibung: toString
In Java ist die Methode toString ein gutes Beispiel für die Anwendung von Überschreibung.
Die Methode ist in der Klasse Object definiert und liefert als Ergebnis einen String mit
einer lesbaren Darstellung des Objekts. Jede Klasse sollte diese Methode überschreiben
und ihre spezifische Darstellung definieren. Die Methode toString wird in der Methode
String.valueOf verwendet um die Stringdarstellung beliebiger Objekte zu erhalten. Obwohl
der Parameter von String.valueOf den syntaktischen Typ Object hat, wird beim Aufruf von
toString immer die spezifische Darstellung gemäß dem Laufzeittyp des Objekts berechnet.
3.9
Neutrale Stationen in MTJ
Die Klasse ItemCounterStation ist ein Beispiel für eine Stationsart, die nicht auf eine
bestimmte Item-Art festgelegt ist.
Definition: Wir wollen solche Stationen neutral nennen.
• Bei dem für → item-kompatible Verbindungen (siehe Seite 83) beschriebenen Vorgehen
muß für jede Station eine bestimmte Item-Art festgelegt werden. Dazu wäre es nötig,
88
3.9. NEUTRALE STATIONEN IN MTJ
für neutrale Stationen je eine Klasse für jede existierende Item-Art zu schreiben. Um
dies zu vermeiden erweitern wir das Konzept um eine Sonderbehandlung für neutrale
Stationen.
– Wir legen fest, daß neutrale Stationen am Ausgang immer die gleiche Art von Items
senden, die am Eingang ankommen.
– Es reicht nicht aus, neutrale Stationen von der Item-Art-Prüfung komplett
auszunehmen. Hängen wir beispielsweise eine neutrale Station an einen StringGenerator an, so wird sie auch String-Items aussenden. Wird nun eine Station
angehängt, die nur Character-Items verarbeiten kann, so soll dies als Fehlersituation
erkannt werden.
• Dynamische Festlegung der Item-Art
Wir wollen die Item-Art für neutrale Stationen festlegen, sobald die Station mit einer
anderen verkettet wird.
– Einschränkende Regeln
Dies setzt allerdings voraus, daß bei der anderen Station die Item-Art bereits
feststeht. Wir legen dazu folgendes fest:
∗ Es ist ein Fehler, zwei isolierte neutrale Stationen miteinander zu verbinden. In
diesem Fall ist nämlich bei keiner von beiden die Item-Art festgelegt. Ließe man
eine solche Verbindung zu, so müßte man ggf. beim Einrichten einer Verbindung
die Item-Art für beliebig viele untereinander verbundene neutrale Stationen
festlegen und nicht nur für eine einzelne. Dies wäre möglich, aber aufwendiger.
∗ Für eine neutrale Station kann die Item-Art sowohl durch Verbindung mit
einem Nachfolger als auch mit einem Vorgänger festgelegt werden. In beiden
Fällen werden die Item-Arten für Ein- und Ausgang der neutralen Station
festgelegt.
∗ Ist die Item-Art einer neutralen Station festgelegt, so kann sie nicht mehr
geändert werden. Insbesondere verliert die Station die Item-Art auch dann
nicht, wenn sie von ihren Vorgängern und Nachfolgern wieder getrennt wird.
– Fälle bei Verkettung
Es gibt also nun vier Fälle beim Verketten zweier Stationen mittels
addEventListener:
1. Bei beiden Stationen ist eine Item-Art festgelegt. Dies ist die bisher für →
itemkompatible Verkettung (siehe Seite 83) beschriebene Situation. Es wird
geprüft, ob die Arten übereinstimmen und ggf. eine Ausnahme gemeldet.
2. Bei keiner der beiden Stationen ist eine Item-Art festgelegt. Dies widerspricht
der ersten Regel und wird durch eine MtjException-Ausnahme mit einem
entsprechenden Fehlertext gemeldet.
3. Nur beim Nachfolger ist die Item-Art festgelegt. In diesem Fall ist die
Verkettung immer zulässig. Zusätzlich muß für den Vorgänger die Item-Art
festgelegt werden.
4. Nur beim Vorgänger ist die Item-Art festgelegt. In diesem Fall ist die
Verkettung immer zulässig. Zusätzlich muß für den Nachfolger die Item-Art
festgelegt werden.
89
KAPITEL 3. STATIONEN
– Behandlung der Fälle
Die ersten beiden Fälle werden in addEventListener behandelt. Der dritte Fall
setzt die Item-Art des Vorgängers und wird daher lokal beim Vorgänger, also
ebenfalls in addEventListener behandelt. Der vierte Fall setzt die Item-Art des
Nachfolgers und wird daher lokal beim Nachfolger, also in setSender behandelt.
– Darstellung der Item-Art in neutralen Stationen
Eine nicht festgelegte Item-Art in einer neutralen Station wird dadurch dargestellt,
daß die Variablen sendItemKind und recItemKind den Wert → null (siehe
Seite 29) enthalten. Durch einen Vergleich mit null läßt sich also ermitteln, ob
die Item-Art einer Station bereits festgelegt ist.
– Unterschied zwischen Generator- und Arbeitsstation
Im dritten der beschriebenen vier Fälle wird die Item-Art der Station festgelegt,
bei der addEventListener aufgerufen wurde. Dabei kann es sich entweder um
eine Generatorstation oder eine Arbeitsstation handeln. Im ersten Fall ist nur
die Variable sendItemKind zu besetzen, im zweiten Fall auch die Variable
recItemKind. Da addEventListener jedoch in Station definiert wird, ist hier noch
nicht bekannt, welcher Fall vorliegt.
∗ Realisierung
Dies ist ein typischer Fall für den Einsatz von Überschreibung. Wir möchten
bei Station eine bestimmte Funktion (das Festsetzen der Item-Art) auslösen,
die genaue Implementierung dieser Funktion unterscheidet sich aber in den von
Station abgeleiteten Klassen.
Wir definieren dazu in der Klasse Station die Methode
1
2
3
4
void setItemKind(Class itemKind)
{
sendItemKind = itemKind;
}
die nur die Art der Ausgangs-Items besetzt. Diese Methode wird an GenStation
unverändert vererbt. In ProcStation wird sie dagegen überschrieben durch
eine Version, die sowohl die Art der Ausgangs- als auch der Eingangs-Items
setzt. Diese Version verwendet die überschriebene Version mittels super um die
Art der Ausgangs-Items zu setzen und erweitert die so übernommene Funktion
um das Setzen der Art der Eingangs-Items.
3.10
Interfaces als Konstantensammlungen
Interfaces können neben abstrakten Methoden auch Konstante enthalten (also als final
deklarierte Variable). Neben ihrer Verwendung als Schnittstellenspezifikationen werden
Interfaces daher auch benutzt, um Konstantensammlungen zu realisieren.
3.10.1
Realisierung von Aufzählungstypen
Eine spezielle Art von Konstantensammlungen sind die Werte von Datentypen mit nur endlich
vielen benannten Werten (in vielen Programmiersprachen als „Aufzählungstypen“ bezeichnet).
90
3.10. INTERFACES ALS KONSTANTENSAMMLUNGEN
In Java gibt es keine Aufzählungstypen, als Interfaces realisierte Konstantensammlungen
können diese Aufgabe übernehmen.
Objekte als Werte
Die Konstanten in einem Interface können beliebige Datentypen besitzen, es können auch
Objekte sein. Objektkonstanten werden wie üblich mit Hilfe von Konstruktorausdrücken
initialisiert. Da jede Auswertung eines Konstruktorausdrucks ein neues, individuelles Objekt
erzeugt, reichen für die Realisierung von Aufzählungstypen bereits Konstanten vom Typ
Object.
interface ColorValues
{
public static final Object
BLUE = new Object(),
RED = new Object(),
GREEN = new Object(),
YELLOW = new Object();
}
1
2
3
4
5
6
7
8
• Wie alle Objekte können die Werte mit den Operatoren == und != verglichen werden.
Hier wird eine wichtige typische Eigenschaft von objektorientierten Sprachen ausgenutzt,
nämlich die „Objektidentität“. Jedes Objekt ist ein „Individuum“ und läßt sich von allen
anderen Objekten unterscheiden, auch wenn es bezüglich des Inhalts (der Werte seiner
Variablen) mit anderen Objekten übereinstimmt.
Objekte mit eigenem Datentyp als Werte
Möchte man, daß die Werte einen eigenen Datentyp haben, so kann man dafür
eine entsprechende Klasse von Object ableiten. In dieser Klasse kann man zusätzlich
eine String-Darstellung für jeden Wert realisieren, indem man eine String-Variable als
Schablonenbestandteil einführt, die im Konstruktor besetzt wird, und die toString-Methode
geeignet → überschreibt (siehe Seite 87).
class Color
{
String name;
Color(String name) { this.name = name }
public String toString() { return name; }
}
interface ColorValues
{
public Color Blue = new Color("BLUE"),
RED = new Color("RED"),
...
}
1
2
3
4
5
6
7
8
9
10
11
12
3
3
In einem Interface deklarierte Variable sind automatisch static und final, die entsprechenden
Schlüsselworte können weggelassen werden.
91
KAPITEL 3. STATIONEN
Zahlen als Werte
Anstelle von Objekten kann man natürlich auch Zahlen als Werte für die Konstanten
verwenden, wenn man einen Aufzählungstyp realisieren möchte. Dann muß man aber explizit
dafür sorgen, daß jede Konstante mit einem anderen Wert initialisiert ist und es ist nicht
möglich, einen eigenen Datentyp zu verwenden und eine Stringdarstellung zu definieren.
Ansprechen der Werte
In einem Interface definierte Konstante werden wie alle statischen Klassenbestandteile
angesprochen, indem der Interface-Name vor den Identifikator der Konstante geschrieben wird,
also beispielsweise
ColorValues.BLUE
• Mittels Vererbung
Eine Klasse, die das Interface implementiert, erbt alle enthaltenen Konstanten. Damit
können sie (falls sie nicht verschattet sind) direkt durch ihren Identifikator angesprochen
werden. Dies gilt genauso für alle weiter abgeleiteten Klassen.
3.11
Statische Klassen als Klassenbestandteile
Wie → bereits erwähnt (siehe Seite 13), kann eine Klasse als Bestandteile neben Variablen und
Methoden auch weitere Klassen enthalten. Auch enthaltene Klassen können zum statischen
Teil oder zum Instanzenteil gehören. Im Fall einer enthaltenen statischen Klasse ist die Klasse
wie jede andere zu benutzen. Der Mechanismus dient nur zu einer feineren Gruppierung von
Klassendefinitionen, als dies durch die Packages möglich ist.
1
2
3
4
5
6
class A
{
static class B
{...}
...
}
• Ansprechen der Klassen
Enthaltene statische Klassen werden wie alle statischen Bestandteile angesprochen,
indem der Name der enthaltenden Klasse vor ihren Identifikator gesetzt wird, z.B. A.B.
Innerhalb der enthaltenden Klasse reicht der Identifikator aus.
• Bytecode-Datei
Enthaltene Klassen werden wie andere Klassen durch den Compiler in eine eigene
Klassendatei umgesetzt. Der Name der Klassendatei ergibt sich aus dem vollen
Pfadnamen ab der äußersten Klasse, wobei die trennenden Punkte durch Dollarzeichen
ersetzt werden. Die oben definierte Klasse B würde also in einer Klassendatei A$B.class
abgelegt.
92
3.12. SIGNALE IN MTJ
Anwendungsbeispiel: Aufzählungstypen
Ein Anwendungsbeispiel von enthaltenen Klassen sind wieder die bereits behandelten
Aufzählungstypen. Bei dem → dort beschriebenen (siehe Seite 90) Vorgehen bietet es sich
an, das Interface für die Konstantensammlung als Bestandteil der Klasse für den Datentyp zu
definieren:
1
2
3
4
5
6
7
8
9
10
11
12
class Color
{
String name;
private Color(String name) { this.name = name; }
public String toString() { return name; }
static interface Values
{
public Color BLUE = new Color("BLUE"),
RED = new Color("RED"),
...
}
}
• Durch die Unterordnung des Interface ist es hier möglich, den Konstruktor als privaten
Bestandteil zu deklarieren. Damit ist es nur innerhalb der Klasse möglich, Instanzen
von ihr zu erzeugen. Es kann also außer den Konstanten im Interface keine weiteren
Instanzen von Color geben.
3.12
Signale in MTJ
Wir wollen nun die Implementierung der Signale erweitern. Auf Stufe 2 waren alle Signale
gleich und die Klasse SignalEvent hatte keine Bestandteile. Wir wollen nun unterschiedliche
Signalarten einführen.
3.12.1
Die Klasse Signal
Die neue Klasse Signal entspricht einem Aufzählungstyp, der so wie → beschrieben (siehe
Seite 92) realisiert ist. Jede Instanz von Signal entspricht einer Art von Signalen. Jede
Signalart hat einen Namen, der dem Konstruktor als String übergeben wird. Die von Object
ererbte Methode toString wird überschrieben und stellt die Signalart durch den Namen dar,
dem „SIGNAL:“ vorangestellt wird.
• Die Klasse Signal hat als weiteren Bestandteil das Interface Values, in dem die
verfügbaren Signalarten als Konstante definiert sind.
• Die Klasse Station wird gegenüber Stufe 2 dadurch modifiziert, daß sie nun zusätzlich
das Interface Signal.Values implementiert. Damit erbt sie alle in diesem Interface
definierten Konstanten und vererbt sie weiter an alle abgeleiteten Klassen für Stationen.
Die Bezeichnungen der Signalarten sind damit in allen Klassen für Stationen verfügbar,
ohne daß Signal.Values davorgesetzt werden müßte.
93
KAPITEL 3. STATIONEN
• Die Klasse SignalEvent wird in Stufe 3 des Systems so erweitert, daß sie eine Variable
signal vom Datentyp Signal enthält. Der Wert wird dem Konstruktor als Parameter
übergeben und kann mit der neuen Methode getSignal abgefragt werden (damit hat
die Klasse SignalEvent einen analogen Aufbau wie ItemEvent).
3.12.2
Signalarten
Wir wollen Signale verwenden, um Item-Gruppen zu „klammern“. Entsprechend definieren wir
von jeder Signalart eine Start- und eine Ende-Varaiante. Auf Stufe 3 definieren wir folgende
Signalarten:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Signal
{
public static interface Values
{
public static final Signal
START = new Signal("START"),
END = new Signal("END"),
FILE_START = new Signal("FILE_START"),
FILE_END = new Signal("FILE_END"),
WORDGROUP_START = new Signal("WORDGROUP_START"),
WORDGROUP_END = new Signal("WORDGROUP_END"),
STRING_START = new Signal("STRING_START"),
STRING_END = new Signal("STRING_END");
}
...
}
Die Signale START und END
Wir legen fest, daß jede Generatorstation ihre erzeugten Items mit den Signalen START und END
klammert. Dazu müssen diese Signale in den Konstruktoren der konkreten Generatorstationen
in dem Vektor events vor bzw. nach allen übrigen Ereignissen eingetragen werden.
• Wir wollen weiterhin annehmen, daß auch bei jeder Arbeitsstation alle während
eines Konfigurationsdurchlaufs ankommenden Ereignisse durch diese beiden Signale
geklammert sind. Damit lassen sich jetzt mehrere Konfigurationsdurchläufe, also
mehrere Aktivierungen der Methode generateEvents des oder der Generatorstationen
unterscheiden.
• Anwendung für ItemCounterStation
Wir wollen die Klasse ItemCounterStation in Stufe 3 entsprechend abändern. Sie soll
jeweils nur die Items zwischen einem START- und dem nächsten END-Signal zählen und
die Summe ausgeben.
– Zu diesem Zweck muß die handleEvent-Methode geändert werden. Im Falle eines
Signalereignis muß die Signalart mittels getSignal besorgt werden und muß geprüft
werden, ob es sich bei ihr um START oder END handelt. Im ersten Fall muß der Zähler
94
3.13. BEHANDLUNG VON ITEMS DURCH ARBEITSSTATIONEN IN MTJ
auf 0 gesetzt werden, im zweiten Fall wird wie bisher die Summe ausgegeben. Alle
übrigen Signalarten werden ignoriert und einfach durchgelassen.
• In entsprechender Weise sollen alle übrigen Stationen funktionieren. Initialisierungen,
die für mehrere Konfigurationsdurchläufe gelten, werden im Konstruktor vorgenommen.
Initialisierungen, die für jeden Konfigurationsdurchlauf wiederholt werden müssen
werden beim Eintreffen eines START-Signals durchgeführt.
3.13
Behandlung von Items durch Arbeitsstationen in MTJ
Wir wollen nun das Verhalten der Arbeitsstationen etwas stärker systematisieren. Wir kennen
bisher zwei verschiedene Arten von Arbeitsstationen: Die Zählerstationen, die alle Items
durchlassen und die verschiedenen Filterstationen, die gewisse Items durchlassen und andere
nicht. Denkbar sind weiterhin Arbeitsstationen, die zu einem empfangenen Item mehrere neue
Items senden oder umgekehrt mehrere empfangene Items zu einem Item zusammenfassen.
3.13.1
Gruppierung der Arbeitsstationsarten
Wir bilden entsprechende Gruppen von Arbeitsstationsarten und definieren zugehörige
Klassen. Diese Klassen bilden eine Zwischenschicht zwischen ProcStation und den Klassen
für die konkreten Arbeitsstationen.
• Wir verwenden die folgenden vier Gruppen:
1. Transparente Stationen
Klasse TransparentStation: Alle Items und Signale werden unverändert
durchgelassen.
2. Filternde Stationen
Klasse ItemFilterStation: Jedes Item wird entweder unverändert durchgelassen
oder komplett verschluckt. Damit stimmt die Art der gesendeten Items immer mit
der Art der empfangenen Items überein.
3. Zerlegende Stationen
Klasse ItemSplitStation: Für jedes empfangene Item wird eine Folge beliebig
vieler Items gesendet (die Folge kann auch leer sein).
4. Sammelnde Stationen
Klasse ItemCollectStation: Empfangene Items werden gesammelt und gruppenweise zusammengefaßt. Für jede Gruppe wird genau ein Sammel-Item gesendet.
Die Art der Gruppenbildung für empfangene Items bleibt der jeweiligen konkreten
Stationsart überlassen.
95
KAPITEL 3. STATIONEN
Abbildung 3.3: Die vier Arten von Arbeitsstationen
• Implementierung gemeinsamer Anteile
Die konkreten Arbeitsstationen innerhalb der gleichen Gruppe haben nun eine Reihe
von Gemeinsamkeiten. Ziel der neuen Klassenstruktur ist es, diese gemeinsamen
Anteile bereits in den Klassen der Zwischenschicht zu implementieren. Da die
gemeinsamen Anteile die Verarbeitung der Ereignisse betreffen, bedeutet das, daß die
Klassen der Zwischenschicht eine Definition der Methode handleEvent enthalten. Die
Klassen für konkrete Arbeitsstationen implementieren dann nur noch ihre spezifischen
Erweiterungen.
– Für diesen Zweck ist Überschreibung nicht verwendbar, da bei Überschreibung
ja gerade die ererbte Implementierung von handleEvent ersetzt wird, anstatt
sie weiterzuverwenden und zu erweitern. Eine Ausnahme wäre der Aufruf der
überschriebenen Methode mittels super. Dabei ist es aber nur möglich, zusätzliche
Anweisungen vor oder hinter der ererbten Methode anzufügen. Dies reicht in
unserem Fall nicht aus, wir brauchen „Löcher“ in handleEvent, die dann in der
Klasse für eine konkrete Arbeitsstation „gefüllt“ werden.
– Diese Löcher lassen sich mit Hilfe abstrakter Methoden realisieren. Jede
Zwischenschicht-Klasse definiert geeignete abstrakte Methoden, die in ihrer
Implementierung von handleEvent aufgerufen werden. Die Klassen für konkrete
Arbeitsstationen implementieren dann nur noch diese abstrakten Methoden.
96
3.13. BEHANDLUNG VON ITEMS DURCH ARBEITSSTATIONEN IN MTJ
3.13.2
Behandlung von Signalen
Wir legen nun noch fest, wie in den vier Gruppen mit Signalen umgegangen wird.
Transparente und filternde Stationen
Im Fall der transparenten und filternden Stationen ist dies einfach: Signale werden immer
durchgelassen. 4
Zerlegende Stationen
Im Fall von zerlegenden Stationen kann es interessant sein, die Zusammengehörigkeit der Items
in der zu einem empfangenen Item gesendeten Folge dadurch zu markieren, daß man die Folge
durch Signale klammert. Welche Signalarten verwendet werden bleibt dabei der konkreten
Arbeitsstation überlassen. Ferner soll es möglich sein, diese Signale auch abzuschalten.
Sammelnde Stationen
Im Fall von sammelnden Stationen sind Signale problematisch, die innerhalb einer Gruppe
von zusammenzufassenden Items ankommen. Werden sie durchgelassen, so haben sie im
Ausgangsstrom eine andere Position als im Eingangsstrom, nämlich vor dem Sammel-Item
anstelle innerhalb. Da Signale verwendet werden um bestimmte Positionen im Strom zu
markieren, macht dies keinen Sinn.
Abbildung 3.4: zwei Möglichkeiten zur Signalbehandlung bei sammelnden Stationen
• Wir legen daher fest, daß eine sammelnde Station empfangene Signale je nach ihrer Art
entweder verschluckt oder durchläßt, beim Durchlassen aber vorher ein Sammel-Item zu
den bisher angesammelten Items versendet. Damit bleibt die Position für durchgelassene
Signale erhalten. Welche Signale durchgelassen werden soll bei der Erzeugung von
sammelnden Stationen festgelegt werden können.
3.13.3
Transparente Stationen
Eine transparente Station läßt alle Ereignisse durch, führt aber ggf. bestimmte zusätzliche
Aktionen durch. Wir unterscheiden dabei zwischen Aktionen für Signale und Items und
4
Falls wir Signale filtern wollen, realisieren wir das durch andere Arbeitsstationen.
97
KAPITEL 3. STATIONEN
definieren in der Klasse TransparentStation die zwei abstrakten Methoden
protected abstract void processSignal(Signal signal);
protected abstract void processItem(Item item);
• Die Methoden sind als protected definiert, da sie nur innerhalb der Klasse und in
abgeleiteten Klassen benutzt werden sollen. 5 Durch geeignete Implementierung in
abgeleiteten Klassen können dort die durchzuführenden Aktionen festgelegt werden.
• Implementierung von handleEvent
Die Implementierung der Methode handleEvent in TransparentStation prüft, ob
es sich um ein Signal- oder Item-Ereignis handelt und ruft processSignal oder
processItem mit dem enthaltenen Signal bzw. Item auf. Anschließend sendet sie
das Ereignis weiter. Damit die Station also wirklich transparent ist, dürfen in
Implementierungen der beiden abstrakten Methoden keine Ereignisse mehr gesendet
werden.
• ItemCounterStation
Die Klasse ItemCounterStation wird nun von TransparentStation abgeleitet.
In processSignal müssen die Signalarten START und END behandelt werden, in
processItem wird der Zähler erhöht.
• Konstruktor
Schließlich muß in TransparentStation noch ein Konstruktor mit String-Parameter
definiert werden, der wie in ProcStation nur den entsprechenden Konstruktor der
Superklasse aufruft. Da Konstruktoren nicht vererbt werden ist dies notwendig um
den ProcStation-Konstruktor für die von TransparentStation abgeleiteten Klassen
benutzbar zu machen.
3.13.4
Filternde Stationen
Das spezifische an filternden Stationen liegt darin, welche Items durchgelassen werden. Um
dies in abgeleiteten Klassen festlegen zu können, definiert die Klasse ItemFilterStation die
abstrakte Methode
protected abstract boolean passItem(Item item);
• Implementierung von handleEvent
Die Methode entspricht einem Prädikat (Testfunktion) für Items. Alle Items, die das
Prädikat erfüllen, sollen durchgelassen werden. Entsprechend ist die Implementierung
von handleEvent bei ItemFilterStation aufgebaut. Zuerst wird geprüft, ob es sich
um ein Signal-Ereignis handelt. Diese werden immer weitergesendet. Im Fall eines ItemEvents wird die passItem-Methode mit dem enthaltenen Item aufgerufen. Liefert sie als
Ergebnis true, so wird das Ereignis weitergeschickt.
5
Auch Überschreibung/Implementierung ist eine Art von Benutzung.
98
3.13. BEHANDLUNG VON ITEMS DURCH ARBEITSSTATIONEN IN MTJ
• Subklassen
Die drei Klassen für filternde Stationen aus Stufe 2 DGFilterStation, LCFilterStation
und UCFilterStation werden nun von der Klasse ItemFilterStation abgeleitet. Sie
brauchen nur noch den entsprechenden Test für Character-Items durch die Methode
passItem zu implementieren.
• Item-Art
Die Klasse ItemFilterStation ist übrigens unabhängig von der Item-Art. Wie bisher
wird die Item-Art erst im Konstruktor der konkreten Arbeitsstation festgelegt.
• Konstruktor
Auch hier ist wie bei TransparentStation ein Konstruktor mit String-Parameter
notwendig um den ProcStation-Konstruktor abgeleiteten Klassen zugänglich zu
machen.
3.13.5
Zerlegende Stationen
Spezifisch für zerlegende Stationen ist es, wie einzelne Signale und Items verarbeitet werden
und welche Signale zum Klammern der erzeugten Itemfolgen verwendet werden. Das Ein- oder
Ausschalten der Signalerzeugung soll bei der Stationserzeugung festgelegt werden.
Abstrakte Methoden in ItemSplitStation
abstract void processSignal(Signal signal);
abstract void processItem(Item item);
Variable in ItemSplitStation
protected SignalEvent startSignal, endSignal;
private boolean doSignals;
Die Variablen startSignal und endSignal sollen die klammernden Signale enthalten und
werden im Konstruktor der abgeleiteten Klassen besetzt. Die Variable doSignals gibt an, ob
die klammernden Signale auch tatsächlich erzeugt werden sollen.
Implementierung von handleEvent
Die Implementierung von handleEvent leistet folgendes.
• Signal-Ereignis
Im Falle eines Signal-Ereignisses wird processSignal mit der enthaltenen Signalart
aufgerufen und anschließend das Ereignis weitergeschickt.
• Item-Ereignis
Im Falle eines Item-Ereignisses wird erst das startSignal gesendet, falls doSignals
den Wert true enthält. Dann wird processItem mit dem im Ereignis enthaltenen Item
aufgerufen. Im Gegensatz zu processSignal hat processItem hier auch die Aufgabe, die
erzeugten Item-Ereignisse zu versenden. Abschließend wird abhängig vom doSignalsWert noch das endSignal gesendet.
99
KAPITEL 3. STATIONEN
Konstruktor
Daß der Wert von doSignals im Konstruktor der Stationen festgelegt werden kann, wird
ebenfalls bereits in ItemSplitStation implementiert. Der Konstruktor erhält sowohl den
Stationsnamen als auch den doSignals-Wert als Parameter. Den Stationsnamen setzt er durch
Aufruf des Superklassen-Konstruktors, den doSignals-Wert setzt er anschließend direkt.
Damit ist es nirgends anders nötig, auf die Variable doSignals zuzugreifen. Entsprechend
ist sie als private deklariert.
3.13.6
Stationen zum Zerlegen von Strings
Als Beispiel für zerlegende Stationen definieren wir zwei Stationsarten, die String-Items in
Bestandteile zerlegen. Die erste Stationsart zerlegt Strings in ihre Einzelzeichen, die zweite
in einzelne Wörter. In beiden Fällen wird die Stationsart durch eine von ItemSplitStation
abgeleitete Klasse definiert.
Die Klasse StringToCharStation
Die Klasse definiert einen Konstruktor mit allen Parametern, die der Konstruktor von
ItemSplitStation hat, also den Stationsnamen und die Angabe, ob die klammernden Signale
erzeugt werden sollen.
• In diesem Konstruktor muß zusätzlich erledigt werden:
– Die klammernden Signale müssen festgelegt werden. Wir verwenden die Signalarten
STRING_START und STRING_END (vgl. den Abschnitt zu → Signalarten (siehe
Seite 94)).
– Die Itemarten für Eingang und Ausgang müssen festgelegt werden (vgl. den
Abschnitt zu→ item-kompatiblen Verkettungen (siehe Seite 83)). Am Eingang
handelt es sich um String-Items, am Ausgang um Character-Items.
• Die Klasse definiert zwei weitere Konstruktoren, die einen bzw. beide Parameter mit
einem festen Wert vorbesetzen. Der eine setzt einen Standardnamen, wie bei allen
Stationen, der andere legt zusätzlich fest daß keine klammernde Signale erzeugt werden
sollen.
• Im Rest der Klasse werden nur noch die beiden von ItemSplitStation ererbten
abstrakten Methoden implementiert. Signale benötigen keine besondere Behandlung,
daher wird processSignal mit leerem Rumpf definiert. Die Methode processItem
implementiert die eigentliche Aufgabe der StringToCharStation, nämlich das Senden
aller Zeichen im String als einzelne Character-Item-Ereignisse. Dazu wird der String in
einer for-Schleife durchlaufen (vgl. den Konstruktor von CharGenerator).
Die Klasse StringToWordStation
Die Klasse ist analog zu StringToCharStation definiert. Als klammernde Signale verwendet
sie WORDGROUP_START und WORDGROUP_END, die Itemart ist String-Item am Ein- und Ausgang.
100
3.13. BEHANDLUNG VON ITEMS DURCH ARBEITSSTATIONEN IN MTJ
• Beim Zerlegen des Strings in processItem werden Zwischenräume als Trennzeichen zum
Separieren von Worten verwendet. Dies bedeutet, daß in den gesendeten Worten keine
Zwischenräume mehr enthalten sind.
• Der String wird in einer for-Schleife durchlaufen. Jedes erkannte Wort wird mittels
der substring-Methode (vgl. den Abschnitt über → Strings (siehe Seite 68)) aus dem
String kopiert und als Item-Ereignis verschickt. Zu beachten ist dabei, daß auch mehrere
Zwischenräume hintereinander auftreten können und am Anfang und Ende des Strings
Zwischenräume ignoriert werden müssen.
3.13.7
Sammelnde Stationen
Spezifisch bei sammelnden Stationen ist die Speicherung gesammelter Items, die Entscheidung,
wann ein Sammel-Item gesendet werden soll, die Realisierung eines Sammel-Items und das
Zufügen eines Items zur Sammlung. Außerdem lassen wir noch eine spezifische Verarbeitung
empfangener Signale zu.
Abstrakte Methoden
Entsprechend definiert die Klasse ItemCollectStation die abstrakten Methoden
abstract
abstract
abstract
abstract
void processSignal(Signal signal);
boolean doFlush(Item item);
void collectItem(Item item);
void flushCollectedItems();
Durchzulassende Signale
Allen sammelnden Stationen gemeinsam ist eine Menge von Signalarten, die durchgelassen
werden sollen. Wir verwenden einen Vektor passOnSignals zur Speicherung dieser Menge in
einer sammelnden Station. Mit der Methode contains (vgl. → den Abschnitt zu Vektoren
(siehe Seite 61)) läßt sich leicht prüfen, ob eine gegebene Signalart in der Menge enthalten ist.
Implementierung von handleEvent
Die Methode handleEvent ist dann wie folgt implementiert.
• Signal-Ereignis
Im Fall eines Signal-Ereignis wird zuerst processSignal mit der enthaltenen Signalart
aufgerufen. Anschließend wird geprüft, ob die Signalart in passOnSignals enthalten ist.
Nur in diesem Fall wird das Signal weitergeschickt und vorher mit flushCollectedItems
ein Sammel-Item zu den bisher angesammelten Items erzeugt.
• Item-Ereignis
Im Fall eines Item-Ereignis wird die Methode doFlush mit dem enthaltenen Item
aufgerufen, um zu prüfen, ob vor dem Zufügen des Items ein Sammel-Item erzeugt
werden soll. Ist das Ergebnis true, wird das Sammel-Ereignis mit flushCollectedItems
101
KAPITEL 3. STATIONEN
erzeugt. In jedem Fall wird schließlich das empfangene Item mittels collectItem der
Sammlung zugefügt.
Angabe durchzulassender Signale
Durchzulassende Signalarten sollen sowohl in Konstruktoren abgeleiteter Klassen spezifiziert
werden können, als auch als Parameter an solche Konstruktoren übergeben werden können.
• Wir wählen für diesen Zweck den Datentyp Signal[] zur Darstellung entsprechender
Mengen, da sich dann Mengen direkt durch Ausdrücke angeben lassen (vgl. → den
Abschnitt zu Reihungen (siehe Seite 76)).
• Der Konstruktor von ItemCollectStation erhält zwei solche Reihungen als Parameter
zusätzlich zum Stationsnamen. Die eine Reihung enthält im Konstruktor einer
abgeleiteten Klasse spezifizierte Signalarten, die andere zusätzlich als Parameter an
den Konstruktor übergebene Signalarten. Der Konstruktor in ItemCollectStation
durchläuft beide Reihungen jeweils mittels einer for-Anweisung und trägt alle
enthaltenen Signalarten in den Vektor passOnSignals ein.
3.13.8
Die Klasse StringConcatStation
Die Station wird durch die Klasse StringConcatStation definiert, die von
ItemCollectStation abgeleitet ist.
• Der Konstruktor erhält neben den beiden Parametern, die an den ItemCollectStationKonstruktor weitergegeben werden, noch einen String-Parameter für den Separator und
einen int-Parameter für die Maximallänge der erzeugten Items:
1
2
3
public StringConcatStation(String name, int maxLength, String separator,
Signal[] passOnSignals)
...
– Als zweite Signalmenge sollen an den Superklassenkonstruktor die Signale
START und END übergeben werden. Damit werden sie grundsätzlich von allen
StringConcatStation-Instanzen durchgelassen.
• Zwei weitere Konstruktoren legen einen Standardnamen fest bzw. legen zusätzlich den
Separator auf " " und die Menge der durchzulassenden Signale auf FILE_START und
FILE_END fest:
4
5
6
7
8
public StringConcatStation(int maxLength, String separator,
Signal[] passOnSignals)
...
public StringConcatStation(int maxLength)
...
• Im Rest der Klassendefinition werden die vier ererbten abstrakten Methoden
implementiert. Dabei wird eine private StringBuffer-Variable verwendet um die Strings
zu sammeln.
102
3.13. BEHANDLUNG VON ITEMS DURCH ARBEITSSTATIONEN IN MTJ
– Beim Sammeln werden Items mit leeren Strings "" einfach ignoriert, es wird also
auch kein Separator zugefügt. Auch am Beginn und am Ende der Sammlung wird
kein Separator zugefügt.
– Die Sammlung wird genau dann abgebrochen, wenn die Länge des bisher
angesammelten Strings (inklusive der Separatoren) noch kleiner oder gleich der
Maximallänge ist, sie aber nach dem Anhängen des nächsten empfangenen Strings
(und dem Separator) echt größer würde. In diesem Fall wird der bisher gesammelte
String als Item gesendet und mit dem nächsten empfangenen String eine neue
Sammlung begonnen. Daneben wird eine Sammlung natürlich wie bei jeder
von ItemCollectStation abgeleiteten Klasse durch ein durchzulassendes Signal
abgebrochen.
Aufgaben
1. Aufgabe 4 Stringverarbeitung
Das bisherige System soll wie in Kapitel 3 beschrieben erweitert werden. Zusätzlich sind
die neuen Stationen zum Zerlegen von Strings zu implementieren. Mit diesem System
soll die folgende Konfiguration aufgebaut und durchlaufen werden:
Abbildung 3.5: Stringverarbeitung (Aufgabe)
Die Konfiguration leitet den vom Generator erzeugten Stringstrom in die Lesestation.
Die gelesenen Zeilengruppen werden parallel in Worte und Zeichen zerteilt. Zeilen, Worte
und Zeichen werden jeweils gezählt und die Summe (über alle gelesenen Dateien) wird
ausgegeben.
• Package
Alle zu implementierenden Klassen sollen dem Package mtj.level3 angehören.
Kopieren Sie dazu alle Quelldateien aus .../mtj/level2 in ein neues Verzeichnis
.../mtj/level3 und ändern Sie sie dort ab.
a) Entwurf Erweitern Sie das Klassendiagramm aus Aufgabe 3a um die benötigten
neuen Klassen. Es handelt sich um insgesamt 12 zusätzliche Klassen.
103
KAPITEL 3. STATIONEN
b) Umstellung auf item-kompatible Verbindungen
Implementieren Sie im Teilsystem aus Aufgabe 3 die item-kompatiblen Stationsverbindungen und die neue Darstellung von Signalarten. Definieren Sie die
Klassen ItemFilterStation und TransparentStation und verwenden Sie sie
um die 4 konkreten Stationsarten davon abzuleiten. Definieren Sie eine Klasse
mtj.level3.Aufg04b, die die Konfiguration aus Aufgabe 3 im neuen System realisiert.
c) Implementierung: String-Verarbeitung
Implementieren und testen Sie String-Items, die Klasse ItemSplitStation und die
Klassen für die Stationen zum Zerteilen von Strings.
d) Test der item-kompatiblen Verbindungen
Definieren Sie eine Klasse mtj.level3.Aufg04d, die eine Konfiguration mit
Verbindungen konstruiert, die nicht item-kompatibel sind. Testen Sie damit, ob
die entsprechenden Ausnahmen gemeldet werden.
104
Kapitel 4
Benutzungsoberfläche
Im vierten Abschnitt des Praktikums wird eine interaktive graphische Benutzungsoberfläche
für das System aus Stationen entwickelt
4.1
MTJ - Stufe 4
In Stufe 4 des Praktikums wird für das System eine interaktive graphische Oberfläche
entwickelt. Die Oberfläche erlaubt den Auf- und Umbau von Konfigurationen, das manuelle
Layout der Stationen in der Darstellung und die Interaktion mit einzelnen Stationen. Ferner
stellt sie das Geschehen beim Ablauf einer Konfigurations-Aktivierung in animierter Form dar.
Die Implementierung geschieht in Form eines neues Packages mtj.level4. Dabei werden die
meisten Klassen auf Level 3 unverändert übernommen.
4.2
Model-View-Controller
Das Model-View-Controller-Muster dient zum Entwurf von interaktiven Programmen
(Dialogprogrammen), die Eingaben von Eingabegeräten wie der Tastatur entgegennehmen,
Verarbeitungen durchführen und Ausgaben auf dem Bildschirm erzeugen.
105
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Abbildung 4.1: Darstellung des Model-View-Controller-Musters
4.2.1
Aufgaben Model-View-Controller
Aufgaben Model
• Bereitstellen der Verarbeitungsfunktionen und von Funktionen zur Datenspeicherung
• Anmeldungen von Views und Controllern zur Benachrichtigung bei Datenänderungen
annehmen
• Views und Controller über erfolgte Änderungen im Model informieren
Aufgaben View
• Darstellung von Daten für den Benutzer
• Daten beim Modell nach Änderungsnachricht abfragen und Auffrischen der Darstellung
mit den aktualisierten Daten
Aufgaben Controller
• Benutzereingaben annehmen
• Eingabedaten an das Modell weiterleiten
• Daten beim Modell nach einer Änderungsnachricht abfragen.
• Views abhängig vom Modell und Bedienereingaben steuern
106
4.3. AUFBAU DER BENUTZUNGSOBERFLÄCHE AUS KOMPONENTEN
4.2.2
Delegate-Modell
In Java wird das Model-View-Controller-Muster in einer abgewandelten Form in den Klassen
der Klassenbibliothek durchgängig verwendet. Betrachtet man Programme auf verschiedenen
Betriebssystemen, so stellt man fest, daß das Erscheinungsbild der Komponenten und deren
spezifische Bedieneigenschaften fast immer zusammengehören. Dies wir auch als Look and
Feel bezeichnet. Dabei steht „Look“ für das Aussehen einer Komponente und „Feel“ für die
Reaktion der Komponente auf Benutzereingaben. Der View und der Controller des ModelView-Controller-Musters wird zu Delegate zusammengefaßt und man muß sich nun nicht mehr
um die Kommunikation zwischen View und Controller kümmern.
Abbildung 4.2: Darstellung des Delegate-Modells
4.3
Aufbau der Benutzungsoberfläche aus Komponenten
Im Vergleich zu anderen Programmierumgebungen ist die Realisierung von graphischen
Benutzungsoberflächen in Java stark vereinfacht worden. Die in Java verwendeten Konzepte
sind „objektorientiert“ konzipiert und damit stark an die reale Welt angelehnt. Unter
objektorieniert ist hierbei weniger die Ableitung von Klassen gemeint als vielmehr die Art
des Zusammenspiels von verschiedenen Objektinstanzen, um eine gemeinsame Aufgabe zu
lösen.
Ein grundlegender Aspekt der Benutzungsoberfläche ist die äußere Form, in der sich ein
Programm den Benutzern graphisch auf dem Bildschirm präsentiert. Diese Form wird als
Teil des Programms vom Programmierer festgelegt. Beim objektorientierten Ansatz werden
die sichtbaren Teile durch Objekte realisiert, die im Programm erzeugt und in der gewünschten
Weise zusammengesetzt werden.
107
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
4.3.1
Komponenten und Container
Definition: Die Objekte werden als Komponenten bezeichnet.
Die Struktur der Oberfläche entsteht durch eine hierarchische Schachtelung der Komponenten.
Jeder Komponente entspricht eine Teilfläche an der Oberfläche. Untergeordnete Komponenten
sind in dieser Fläche enthalten.
Definition: Komponenten, die andere Komponenten enthalten können, werden als Container
bezeichnet.
AWT vs. SWING
Eine Bedienoberfläche enthält graphische Elemente, die vom Benutzer mit der Maus geklickt
werden. Diese können entweder AWT- oder SWING-Komponenten sein. AWT bedeutet
Abstract Windowing Toolkit, das seit JDK 1.1.x verfügbar ist. Der AWT besteht aus dem
Package java.awt und weiteren Packages, deren Namen i.a. mit java.awt beginnen. SWINGKomponenten sind seit JDK 1.2 verfügbar und befinden sich im Package javax.swing.
• AWT - Heavyweight Components
AWT-Komponenten sind sogenannte Heavyweight Components. Die Komponenten werden vom Betriebssystem gezeichnet. Die Anbindung dieser Komponenten erfolgt mittels
sogenannter Peer-Objekte. Da Java eine betriebssystemunabhängige Programmiersprache ist, sind diese Peer-Objekte für jedes Betriebssystem unterschiedlich. Als Konsequenz
hieraus ergibt sich, daß nur eine kleinste gemeinsame Schnittmenge der Funktionalitäten
für verschiedene Betriebssysteme verfügbar ist.
• SWING - Lightweight Components
SWING-Komponenten sind sogenannte Lightweight Components. Das Zeichnen der
Komponenten wird durch Java direkt vorgenommen. Es werden also keine PeerObjekte benötigt. SWING-Komponenten sind sogenannte Container-Klassen, die von
AWT übernommen und durch Ableitung erweitert wurden. Alle SWING-Komponenten
beginnen mit einem großen „J“, wie z.B. JWindow oder JDialog. Neue Komponenten
werden von JComponent abgeleitet.
• Klassen für Komponenten
Die Klasse für Komponenten ist java.awt.Component, direkt abgeleitet von
Object. Container sind spezielle Komponenten, entsprechend ist ihre Klasse
java.awt.Container von java.awt.Component abgeleitet.
• Klassendiagramm der AWT- und SWING-Komponenten
Die folgende Abbildung zeigt die Klassenhierarchie der AWT- und SWINGKomponenten.
108
4.3. AUFBAU DER BENUTZUNGSOBERFLÄCHE AUS KOMPONENTEN
Abbildung 4.3: Klassendiagramm der AWT- und SWING-Komponenten
Äußerste Container: Windows und Frames
Eine spezielle Rolle spielen die äußersten Container. Sie entsprechen einem Fenster der
graphischen Oberfläche. Mit ihnen beginnt der Aufbau der Oberfläche, alle Komponenten
sind direkt oder indirekt in ihnen enthalten. Im einfachsten Fall verwendet ein Programm nur
ein Fenster.
• Klasse Window
Die Klasse java.awt.Window implementiert beliebige Fenster. Wichtige Methoden sind
pack, mit der alle enthaltenen Komponenten korrekt positioniert werden und show, mit
der das Fenster angezeigt wird.
– Position
Die Position, an dem ein Fenster bei Aufruf von show erscheint, kann
mit der Methode setLocation festgelegt werden. Die Angabe geschieht im
Koordinatensystem des Bildschirms. Ansonsten wird das Fenster per default
positioniert.
• Klasse Frame
Die Klasse java.awt.Frame ist abgeleitet von Window und implementiert typische
Hauptfenster für Programme mit Titel, Rahmen und optionalem Menübalken. Für
Frames verwendet man üblicherweise die Default-Positionierung.
• Klasse Dialog
Die Klasse java.awt.Dialog ist abgeleitet von Window und implementiert zusätzliche
Fenster mit beliebigem Inhalt für Programme.
109
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
– Bezugsframe
Bei der Erzeugung einer Dialog-Instanz muß immer ein Frame als Bezugsfenster
angegeben werden. Es bietet sich an, das Dialogfenster in die Nähe des
Bezugsframes zu positionieren, dies geschieht jedoch nicht automatisch.
– Modale Dialoge
Ein Dialogfenster kann als „modal“ eingestellt sein. Dies bedeutet, daß während es
angezeigt wird, keine Interaktion mit anderen Fenstern des gleichen Programms
möglich sind. Modale Dialoge verwendet man typischerweise zur Abfrage von
Informationen vom Benutzer, ohne die das Programm nicht sinnvoll weiterarbeiten
kann.
– Entfernen von Dialogfenstern
Soll ein Dialogfenster nur vorübergehend angezeigt werden, so muß es explizit durch
das Programm wieder vom Bildschirm entfernt werden. Dazu dient die Methode
setVisible mit Parameter false. Dies ist besonders wichtig für modale Dialoge.
Einsetzen von Komponenten
Jede Komponente wird im Programm als isolierte Instanz erzeugt. Zum Aufbau der Oberfläche
müssen die Komponenten ineinander eingesetzt werden. Nur Komponenten, die direkt oder
indirekt in einem Fenster eingesetzt sind können an der Oberfläche auch dargestellt werden.
• Die Methode add
Zum Einsetzen dient die in der Klasse Container definierte Methode add. Parameter ist
die einzusetzende Komponente und ggf. weitere Angaben zur Einsetzungs-Weise.
Einsetzen einer Button-Komponente
1
2
Frame f = new Frame();
f.add(new Button("press me"));
• Menüs
Menü-Komponenten verhalten sich anders als die immer sichtbaren normalen
Komponenten. Sie bilden daher eine eigene Klassenhierarchie ausgehend von der Klasse
java.awt.MenuComponent. Menüs sind entweder dynamisch angezeigte Popup-Menüs
(java.awt.PopupMenu) oder in einem Menübalken (java.awt.MenuBar) in einem Frame
enthalten.
– Aufbau
Menükomponenten werden untereinander ebenfalls mit add zusammengesetzt.
Popup-Menüs werden mit add bei Komponenten zugeordnet. Der Einbau des
Menübalkens in den Frame geschieht dagegen mit der Methode setMenuBar.
∗ Im Fall eines Popup-Menüs bedeutet die Unterordnung unter eine Komponente
nicht, daß das Popup-Menü immer auf der Komponentenfläche angezeigt wird.
Es übernimmt aber gewisse Einstellungen von der Komponente, beispielsweise
die Hintergrundfarbe. Meist ist es sinnvoll, Popup-Menüs ebenso wie den
Menübalken direkt dem Frame zuzuordnen.
110
4.3. AUFBAU DER BENUTZUNGSOBERFLÄCHE AUS KOMPONENTEN
– Anzeige
Der Menübalken wird automatisch mit dem übrigen Inhalt des Frames angezeigt,
die Funktionalität zur Interaktion mit den Einzelmenüs und ihren Einträgen
ist ebenfalls automatisch vorhanden. Popup-Menüs müssen dagegen für jeden
Gebrauch explizit mittels der Methode show in der Klasse PopupMenu sichtbar
gemacht werden. Dabei wird die Position und die Komponente, in deren
Bezugssystem die Position angegeben ist, als Parameter übergeben. Die weitere
Interaktionsfunktionalität ist dann wieder automatisch vorhanden.
Menübalken für Frame
1
2
3
4
5
Frame f = new Frame();
MenuBar mb = new MenuBar();
mb.add(new Menu("File"));
...
f.setMenuBar(mb);
Weitere Komponenten
Im AWT gibt es einige nützliche vordefinierte Klassen für häufig benötigte Komponenten.
Diese Klassen sind von Component abgeleitet. Zur Erläuterung und zu weiteren KomponentenKlassen siehe die API-Beschreibung.
• Label
Einzeilige Beschriftung.
• Button
Beschrifteter anklickbarer Knopf.
• TextField
Einzeiliges editierbares Textfeld.
• TextArea
Mehrzeiliges scrollbares Textfeld, ggf. editierbar durch den Benutzer.
• Canvas
Leere rechteckige Zeichenfläche.
• Panel
Einfachster Container, nur Komponenten sichtbar.
4.3.2
Layout von Komponenten in einem Container
Die Kontrolle über die Anordnung der enthaltenen Komponenten ist nicht bei den Containern
selbst implementiert, sondern durch separate Objekte, sogenannte „Layout Manager“. Auf
diese Weise kann jede Art von Layout in jeder Art von Container verwendet werden.
111
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
BorderLayout
Die Klasse java.awt.BorderLayout ordnet maximal 5 der im Container enthaltenen
Komponenten in der folgenden Weise an:
Abbildung 4.4: BorderLayout
• Positionsangaben
Zur Angabe der Positionen dienen die 5 Konstanten NORTH, SOUTH, EAST, WEST,
CENTER in der Klasse BorderLayout (statische Bestandteile).
GridLayout
Die Klasse java.awt.GridLayout ordnet die im Container enthaltenen Komponenten in einem
rechteckigen Gitter an. Die Anzahl der Zeilen und Spalten wird als Konstruktorparameter für
GridLayout-Instanzen angegeben.
Die Positionierung der Komponenten erfolgt zeilenweise von links oben nach rechts unten.
Abbildung 4.5: GridLayout
Zuordnung eines Layout-Managers
Der Layout-Manager muß dem Container zugeordnet werden, bevor die Komponenten
eingesetzt werden. Die Zuordnung geschieht mit der Methode setLayoutManager beim
Container, der die Layout-Manager-Instanz als Parameter übergeben wird.
112
4.3. AUFBAU DER BENUTZUNGSOBERFLÄCHE AUS KOMPONENTEN
Anwendung des Layout-Managers
Der Layout-Manager wird normalerweise automatisch angewendet, wenn die Komponenten
in den Container eingesetzt werden. Über die Position entscheidet entweder die Reihenfolge,
in der die Komponenten eingesetzt werden (z.B. beim GridLayout) oder sie wird explizit als
zusätzlicher Parameter der add-Methode spezifiziert (z.B. beim BorderLayout).
Abbildung 4.6: Ausschnitt aus der AWT-Klassen-Hierarchie
4.3.3
Beispiel für eine einfache graphische Anwendung in Java
Folgender Code dient als Beispiel für eine einfache graphische Anwendung in Java mit einem
Frame in dem ein Menü, ein Textfeld und ein Button enthalten sind. Zudem zeigt das Beispiel
die Anwendung verschiedener Layout-Manager.
Abbildung 4.7: einfache graphische Anwendung in Java
113
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyFrame extends Frame {
public MyFrame(String title) {
super(title);
Button button = new Button("Button");
Panel panel = new Panel();
TextField text = new TextField("Beispieltext");
MenuBar menuBar = new MenuBar();
Menu menu = new Menu("üMen");
BorderLayout borderLayout = new BorderLayout();
GridLayout gridLayout = new GridLayout();
setMenuBar(menuBar);
setLayout(borderLayout);
menuBar.add(menu);
add(panel, borderLayout.NORTH);
add(button, borderLayout.SOUTH);
panel.setLayout(gridLayout);
panel.add(text, null);
}
public static void main(String[] args) {
MyFrame myFrame = new MyFrame("Ein Beispiel-Frame");
myFrame.setSize(250, 150);
myFrame.setVisible(true);
}
}
4.4
Komponenten der MTJ-Benutzungsoberfläche
Die Benutzungsoberfläche in MTJ besteht im wesentlichen aus einem Hauptfenster mit einer
festen Struktur aus Komponenten:
Abbildung 4.8: Benutzungsoberfläche in MTJ
4.4.1
Beschreibung der Komponenten
Der Frame für das Hauptfenster enthält zunächst einen Menübalken mit nur einem „File“Menü. Darunter kommt ein horizontaler Bereich, der vier jeweils paarweise gruppierte Labels
114
4.4. KOMPONENTEN DER MTJ-BENUTZUNGSOBERFLÄCHE
enthält, welche die aktuellen Koordianten der Maus anzeigen, falls diese sich in dem direkt
darunter liegenden Zeichenbereich befindet. Das erste Label eines Paares zeigt dabei jeweils
einen konstanten Text an, in den hinteren werden jeweils die aktuelle x- bzw. y- Koordinate
der Maus angezeigt. Unter diesem Bereich befindet sich ein Zeichenbereich (Canvas) in dem
später die Konfiguration der Stationen dargestellt wird. Am unteren Ende des Fensters findet
sich noch ein exit-Button.
• File-Menü
Das File-Menü enthält nur einen mit „exit“ beschrifteten Eintrag. Dieser ist
alternativ über den Tastatur-Shortcut „Control-E“ zu erreichen (siehe Klasse
java.awt.MenuShortcut).
4.4.2
Struktur der Komponenten
Die Grobstruktur wird durch ein BorderLayout im Frame hergestellt, bei dem die West- und
East-Komponenten fehlen.
• Anordnung der Label
Bei dem BorderLayout Layoutmanager ist es nicht möglich pro Bereich mehr als eine
Komponente zuzuordnen. Aus diesem Grund muß in der Hierarchie über den Labels
noch ein Panel als Container der Label eingefügt werden. Dieses Panel ist dann einzige
Komponente im North-Bereich. Das Panel verwendet einen GridLayout Layoutmanager
mit einer Zeile und vier Spalten.
115
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Abbildung 4.9: Die Schachtelungs-Hierarchie der Komponenten
Aufbau des Fensters
Auf der linken Seite dieser Graphik ist das definierte Fenster, in der Mitte sind die benutzten
Objekte und auf der rechten Seite die verwendeten Layoutmanager dargestellt.
116
4.4. KOMPONENTEN DER MTJ-BENUTZUNGSOBERFLÄCHE
Abbildung 4.10: Aufbau des Fensters
4.4.3
Die Klasse MtjFrame
Das Fenster mit seinen Komponenten wird durch die Klasse MtjFrame implementiert, die von
java.awt.Frame abgeleitet ist.
Konstruktor
Der Aufbau der Fensterstruktur geschieht im Konstruktor von MtjFrame. Dort werden alle
Komponenten und Layout-Manager erzeugt und korrekt zusammengesetzt. Abschließend wird
die pack-Methode aufgerufen, die die genaue Geometrie aller Fensterinhalte berechnet.
• Fenstername
Der Konstruktor hat einen Parameter für den Fensternamen, den er an den Konstruktor
der Superklasse Frame weiterreicht.
• Zeichenfläche
Um unterschiedliche Zeichenflächen verwenden zu können, wird auch die ZeichenflächenKomponente als Konstruktor-Parameter vom Datentyp Canvas übergeben.
Anwendung
Die Klasse MtjFrame wird im Hauptprogramm verwendet um die Oberfläche zu erzeugen. Dazu
wird zuerst eine Zeichenflächen-Instanz erzeugt. Dann wird eine MtjFrame-Instanz erzeugt und
die Zeichenfläche als Parameter mitgegeben. Um das Fenster anzuzeigen muß dann die showMethode des Fensters aufgerufen werden.
• Programmende
Mit dem Aufbau und der Anzeige der Oberfläche sind die Aufgaben des Hauptprogramms
erfüllt. Alle weiteren Programm-Aktivitäten werden durch die Oberfläche gesteuert. Bei
117
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Verwendung des AWT ist das Erreichen des Hauptprogramm-Endes also nicht mehr
gleichbedeutend mit dem Ende des Programmlaufs.
4.5
Das Zeichnen von Graphiken
Graphik-Objekte wie Kreise, Linien etc. sind im AWT nicht als Komponenten realisiert,
sondern werden anders behandelt. Insbesondere wird ihre Anordnung nicht durch LayoutManager bestimmt.
4.5.1
Zeichenumgebung
Zuerst gilt es zu klären, wie in Java eine Graphik prinzipiell erzeugt und auf dem
Bildschirm ausgegeben werden kann. Als einfache Möglichkeit dazu verwendet Java die
abstrakte java.awt.Graphics Klasse. Jede Instanz ist eine Zeichenumgebung, die es erlaubt,
Zeichnungen zu erstellen, unabhängig davon, wo die Zeichnung erscheint. Diese Klasse stellt
auf der einen Seite Methoden wie drawLine, drawRect und drawString zur Ausgabe zur
Verfügung. Auf der anderen Seite stellt sie aber auch Verwaltungsmethoden zum Setzen von
Parametern wie z.B der Farbe (setColor) bei Zeichenaktionen oder des zu verwendenden
Fonts bei der Ausgabe von Text (setFont) zur Verfügung.
Unabhängigkeit vom Ziel der Zeichnung
Besondere Bedeutung erhält diese Graphics Klasse durch die Idee, keinerlei Hardware- oder
Betriebssystemabhängigkeiten an die Programmierer weiterzugeben. Erst dies ermöglicht
die problemlose Ausführung von Graphikprogrammen, die in Java geschrieben sind, auf
verschiedensten Plattformen. Es ist jedoch klar, daß man sich somit bei der Schnittstelle
der Graphics Klasse auf den kleinsten gemeinsamen Nenner über alle unterstützten
Betriebssysteme einigen mußte. Aus diesem Grund gibt es unter anderem in der Graphics
Klasse keine Methode, mit deren Hilfe man die Dicke beim Zeichnen von Linien einstellen
kann. Linien werden deshalb immer mit der Dicke 1 Pixel gezeichnet.
Zuordnung zu Komponenten
Jede Graphics-Instanz ist einem bestimmten Zeichnungs-Ziel zugeordnet. Ein typisches Ziel
für eine Zeichnung ist die sichtbaren Fläche einer Komponente. Entsprechende GraphicsInstanzen erhält man durch die Methode getGraphics der Klasse Component. Normalerweise
zeichnet man nur in leeren Komponenten. Speziell zu diesem Zweck existiert die vordefinierte
Komponentenklasse Canvas. Alle in der Graphics-Instanz vorgenommene Ausgaben werden
automatisch in das Canvas geleitet, von dem man sich das Graphics-Objekt geholt hat.
118
4.5. DAS ZEICHNEN VON GRAPHIKEN
Abbildung 4.11: Graphikausgabe in ein Canvas
4.5.2
Wiederherstellen von Zeichnungen
Es reicht meist nicht, eine Zeichnung nur einmal zu erstellen, auch wenn sie statisch ist. An
einer Benutzungsoberfläche kann sie durch andere Fenster verdeckt oder mit ihrem Fenster
verschoben werden. In beiden Fällen muß sie wiederhergestellt werden. Komponenten wie
Button-Objekte stellen sich in der Benutzungsoberfläche immer von selbst dar. Beispielsweise
werden Teile, die zuvor von anderen Fenstern verdeckt waren und nun durch eine beliebige
Interaktion sichtbar werden, automatisch von Button-Objekten selbst wiederhergestellt. Dies
gilt jedoch nicht für über Graphics erstellte Zeichnungen. Graphiken (oder Teile davon),
die über ein Graphics Objekt in ein Canvas Objekt gemalt wurden, während es (teilweise)
verdeckt war, werden bei Sichtbarwerden der betroffenen Fläche auf dem Bildschirm nicht
aktiv durch das Canvas dargestellt.
Die paint-Methode
Eine naheliegende und die einfachste Lösung, die auch in Java verwendet wird, ist die
Verwendung einer Callback-Methode, die vom Fenstersystem bei einer Komponente immer
dann aufgerufen wird, wenn Teile oder die ganze Fläche neu dargestellt werden müssen.
Die Methode trägt den Namen paint, ist in der Klasse Component definiert und erhält als
Parameter ein Graphics-Objekt für die Komponente. Ihre Aufgabe ist es, die Zeichnung mit
Hilfe des Graphics-Objekts wiederherzustellen.
Die update-Methode
Die paint-Methode wird über die update-Methode in Component aufgerufen. Dabei wird
vorher die Zeichnung gelöscht, indem die gesamte Fläche der Komponente mit der
Hintergrundfarbe übermalt wird. Daher muß in paint jedesmal die komplette Zeichnung
neu erstellt werden. Um sicherzustellen, daß durch das häufige Übermalen der Fläche die
Prozessorlast nicht zu hoch wird, ist es sinnvoll die Methode sleep(int ms) in paint()
aufzurufen. Hierdurch wird die Fläche nur in bestimmten Zeitintervallen neu gezeichnet.
119
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
• Die update-Methode wird auch aufgerufen, wenn die Komponente erstmalig an der
Oberfläche sichtbar wird. Für statische Zeichnungen reicht es daher aus, sie nur in der
paint-Methode der entsprechenden Komponente zu erstellen.
Beispiel
1
2
3
4
5
class MyCanvas extends Canvas {
public void paint(Graphics g) {
g.drawString("Hello World",10,20);
}
}
Dynamische Zeichnungen
Zusätzlich kann eine Zeichnung durch das Programm gesteuert modifiziert werden,
beispielsweise bei der Implementierung einer Uhr. In diesem Fall muß das Programm einen
Aufruf von paint veranlassen. Dies geschieht normalerweise nicht durch direkten Aufruf von
update sondern durch Aufruf der Methode repaint in der Klasse Component.
• Der Aufruf von repaint führt nicht selbst update durch, sondern merkt nur vor, daß der
Aufruf baldmöglichst durchzuführen ist. Nach dem repaint-Aufruf hat die Erneuerung
der Zeichnung daher normalerweise noch nicht stattgefunden. Will man sicher sein, daß
update fertig ausgeführt ist, muß man es direkt aufrufen.
4.5.3
Objektorientiertes Zeichnen
Das objektorientierte Zeichnen stellt eine effizientere Möglichkeit dar einen bestimmten
Bereich einer Zeichenfläche wiederherzustellen. In der Sichtweise der objektorientierten
Programmierung ist die Darstellung als Zeichnung eine Fähigkeit bestimmter Objekte und
ist als Methode dieser Objekte implementiert, d.h. daß jedes Objekt weiß, wie es sich selbst
zeichnen kann. Zusätzlich wird nur der Bereich einer Zeichenfläche neu gezeichnet, der sich
verändert hat. Nicht mehr der ganze Zeichenbereich wird mit der Hintergrundfarbe übermalt,
sondern nur noch der veränderte Bereich. Der veränderte Bereich muß hierfür jedoch bekannt
sein. Dazu definiert man in Java in der jeweiligen Klasse eine Methode draw, die ein GraphicsObjekt als Parameter erhält. In der Implementierung der Methode stellt sich das Objekt mit
Hilfe der Zeichenmethoden des Graphics-Objekts dar.
120
4.5. DAS ZEICHNEN VON GRAPHIKEN
Abbildung 4.12: Beispiel für objektorientiertes Zeichnen
Objekte in Komponenten zeichnen
Zur korrekten Darstellung in einer Komponente wird die draw-Methode des darzustellenden
Objekts in der paint-Methode der Komponente aufgerufen. Dazu muß das Objekt von der
Komponente aus zugänglich sein, z.B. über eine Referenz, die in einer Variable der Komponente
gespeichert ist.
Kreis in einer Canvas-Komponente
1
2
3
4
5
6
class CircleCanvas extends Canvas {
private Circle c = new Circle();
public void paint(Graphics g) {
c.draw(g);
}
}
Dynamische Objekte
Im Fall eines Objekts, das sich dynamisch ändert, muß das Objekt auch selber die
Aktualisierung der Zeichnung veranlassen können. Dazu braucht es Zugriff auf die
Komponente, in der es dargestellt ist, um dort repaint oder update aufzurufen. Beispielsweise
kann das Objekt eine Methode anbieten, über deren Aufruf ihm mitgeteilt wird, in welcher
Komponente es dargestellt ist.
121
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Uhr in einer Canvas-Komponente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Clock {
private Component presenter = null;
public void setPresenter(Component c) {
presenter = c;
}
public void draw(Graphics g) {
...
}
public void run() {
...
presenter.repaint();
...
}
}
class ClockCanvas extends Canvas {
private Clock c;
public ClockCanvas(Clock c) {
this.c = c;
c.setPresenter(this);
}
public void paint(Graphics g) {
c.draw(g);
}
}
4.5.4
Koordinatensystem
Zum Koordinatensystem in einem Graphics Objekt ist zu bemerken, daß der Ursprung in der
linken oberen Ecke liegt, die X-Achse nach rechts und die Y-Achse nach unten positive Werte
hat. Die verwendeten Längeneinheiten sind Pixel. Um ein Gefühl für verwendete Längen zu
bekommen sollte man wissen, daß moderne Bildschirme meist 1024 oder 1280 Pixel breit sind
und deshalb eine Linie mit 1024 bzw. 1280 Punkten Länge quer über den Bildschirm reicht.
4.5.5
Fonts
Eine weitere erwähnenswerte Klasse ist die Klasse java.awt.Font. Mit ihrer Hilfe können
mehrere Font-Objekte erstellt und damit Text in mehreren verschiedenen Schriftarten
ausgegeben werden. Um einen Text in einem gewünschten Font auszugeben, genügt es, diesen
Font vor der Zeichenaktion an das Graphics-Objekt mittels setFont zu übergeben.
Beispiel zur Font-Verwendung
1
2
3
Font f = new Font("TimesRoman",Font.PLAIN,24);
g.setFont(f);
g.drawString("Hello World",10,20);
• Natürlich ist zu beachten, daß der Font nicht willkürlich gewählt werden kann, er muß
auf dem ausführenden Rechner vorhanden sein. Ist dies nicht der Fall, so wird er durch
einen Default Font ersetzt. Dieser kann allerdings je nach System unterschiedlich gesetzt
sein.
122
4.6. ZEICHNEN EINER RECHTECK-MENGE IN MTJ
4.5.6
Farben
Um eine Zeichenaktion mit einer bestimmten Farbe auszuführen, muß analog zu der Erzeugung
eines neuen Fonts bei den Schriften eine neue Farbe erzeugt werden. Ermöglicht wird dies
durch die Klasse java.awt.Color. Im Anschluß an die Erzeugung einer Farbe muß auch hier
wiederum dem Graphics-Objekt mitgeteilt werden, daß es eine neue Farbe verwenden soll.
Dies geschieht mit Hilfe der Methode setColor.
Beispiel für Verwendung von Farben
1
2
3
Color c = new Color(255,0,0);
g.setColor(c);
g.drawString("Hello World",10,20);
• Die verwendeten Zahlen bei dem Konstruktoraufruf für das Color-Objekt stehen für
den Anteil von Rot, Grün und Blau an der neu erzeugten Farbe (hier angegebene Werte
können einen Wert zwischen 0 und 255 annehmen).
• Häufig verwendete Farben sind als statische Bestandteile der Klasse Color vordefiniert,
z.B. Color.black.
4.6
Zeichnen einer Rechteck-Menge in MTJ
Für die MTJ-Benutzungsoberfläche benötigen wir die Funktionalität zum Zeichnen einer
Menge von Rechtecken und zum Verwalten dieser Zeichnung.
4.6.1
Die Klasse Rectangle
Die Klasse mtj.level4.Rectangle definiert die Funktionalität eines einzelnen Rechtecks, das
einen String enthält und in drei Regionen aufgeteilt ist.
Verwendung von java.awt.Rectangle
Die Verwaltung der Position und Größe eines Rechtecks ist bereits in der Klasse
java.awt.Rectangle implementiert. Von dieser Klasse ist mtj.level4.Rectangle abgeleitet.
Es wird nur der parameterlose Konstruktor von java.awt.Rectangle verwendet, d.h. bei der
Erzeugung der Rechtecke wird weder die Position noch die Größe festgelegt.
Verwaltung des Strings
Statt Position und Größe erhält jedes Rechteck bei der Erzeugung einen String als Label. Mit
der Methode setString kann der String jederzeit durch einen neuen String ersetzt werden.
Die Größe soll immer so gesetzt sein, daß der aktuelle String bei der Darstellung im gewählten
Font gut in das Rechteck passt. Der Konstruktor der Klasse mtj.level4.Rectangle hat damit
nur diesen String als Parameter.
123
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
• Font für die String-Darstellung
Der für die Darstellung des Strings verwendete Font ist für alle Rechtecke gleich und ist
als statische Konstante mit geeigneter Initialisierung in der Klasse enthalten.
• Fläche für die String-Darstellung
Die für die Darstellung des Strings benötigte Fläche ist abhängig vom Inhalt des Strings,
und vom verwendeten Font. Zur Berechnung benötigt man ein FontMetrics-Objekt. Bei
ihm kann man die Angaben mittels stringWidth und getHeight abfragen. Zusätzlich
sollten einige Pixel Abstand zwischen String und Umrandung einkalkuliert werden. Das
FontMetrics-Objekt erhält man mittels getFontMetrics bei der Komponente oder dem
Graphics-Objekt. Daher kann die Berechnung erst unmittelbar beim Zeichnen in der
draw-Methode vorgenommen werden.
Regionen
Die Rechteck-Fläche ist horizontal in drei gleichgroße Regionen unterteilt. Entsprechende
Konstante LEFT, CENTER, RIGHT zu ihrer Bezeichnung sind als statische Konstante in
mtj.level4.Rectangle definiert. Die Methode getRegion liefert zu x/y-Koordinaten
entweder die Bezeichnung der Region oder null, wenn die Koordinaten außerhalb
der Rechteckfläche liegen. Zur Implementierung werden geeignete Methoden der Klasse
java.awt.Rectangle verwendet.
Darstellung
Die Methode draw übernimmt die Darstellung des Rechtecks in einem Graphics-Objekt.
Darzustellen sind der String und der Rand des Rechtecks in schwarzer Farbe.
Zugriff auf die darstellende Komponente
Um auch Eigendynamik realisieren zu können hat jedes Rechteck Zugriff auf die darstellende
Komponente. Dazu enthält Rectangle die Methode setPresenter in der die übergebene
Komponente lokal in einer Variablen abgelegt wird.
4.6.2
Die Klasse RectangleSet
Die Klasse RectangleSet implementiert eine Menge von positionierten Rechtecken der Klasse
Rectangle. Rechtecke können zugefügt und entfernt werden.
Zufügen und Entfernen von Rechtecken
Bei der Instanzierung ist kein Rechteck enthalten. Die Methode addRect fügt ein als Parameter
übergebenes Rechteck zu der Menge zu und positioniert es gemäß der zusätzlich übergebenen
x/y-Koordinaten. Die Methode removeRect entfernt das als Parameter übergebene Rechteck
aus der Menge, falls es enthalten war.
124
4.7. KLASSEN ALS NICHTSTATISCHE KLASSENBESTANDTEILE
Finden von Rechtecken
Die Methode getRect erhält als Parameter eine x- und eine y-Koordinate. Falls der so
spezifizierte Punkt in einem der Rechtecke in der Menge liegt, wird das Rechteck als Ergebnis
geliefert, ansonsten ist das Ergebnis null. Liegt der Punkt in mehreren überlappenden
Rechtecken, wird ein beliebiges davon geliefert.
• Zur Implementierung von getRect kann die Methode contains der Klasse
java.awt.Rectangle verwendet werden.
Darstellung
Die Methode draw stellt alle momentan in der Menge enthaltenen Rechtecke in dem als
Parameter übergebenen Graphics-Objekt dar. Dazu ruft sie bei jedem Rechteck die drawMethode auf.
Zugriff auf die darstellende Komponente
Jede Instanz von RectangleSet ist ab ihrer Erzeugung mit einer darstellenden Komponente
verbunden. Die Komponente wird als Konstruktorparameter übergeben und lokal in einer
Variablen gespeichert. Für jedes Rechteck wird bei Eintritt in die Menge die darstellende
Komponente mittels setPresenter übergeben. Bei Verlassen der Menge wird die Komponente
mittels setPresenter wieder auf null gesetzt.
4.7
Klassen als nichtstatische Klassenbestandteile
Klassen als Bestandteile anderer Klassen können zum statischen Teil oder zum InstanzenTeil gehören. Die Zugehörigkeit einer Klasse zum statischen Teil einer anderen Klasse hat
keine Auswirkung auf die Instanzen. Die Instanzen beider Klassen sind völlig unabhängig
voneinander.
Im Gegensatz dazu bewirkt die Zugehörigkeit einer Klasse zum Instanzenteil einer anderen
Klasse, daß jeder Instanz der inneren Klasse automatisch eine Instanz der äußeren Klasse
zugeordnet ist.
Definition: Klassen im Instanzenteil anderer Klassen werden auch als inner classes
bezeichnet.
125
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Abbildung 4.13: Instanz einer inner class
4.7.1
Zugriff auf Bestandteile der zugeordneten Instanz
Das Besondere an Instanzen einer „inner class“ ist, daß sie direkt auf alle Bestandteile der
zugeordneten Instanz der umgebenden Klasse zugreifen können. Dies gilt auch für private
Bestandteile, denn der Zugriff in der „inner class“ befindet sich ja innerhalb der umgebenden
Klasse.
• Angabe von Bestandteilen
Um in einer „inner class“ einen Instanzen-Bestandteil der umgebenden Klasse
anzusprechen, wird er wie in der umgebenden Klasse durch seinen Identifikator
angegeben, ohne explizit eine Referenz auf ein Objekt anzugeben. Es wird implizit immer
die Referenz des Bezugsobjekts verwendet.
• Angabe des Bezugsobjekts
Wie im Fall der Angabe des enthaltenden Objekts mit dem Ausdruck this ist es
manchmal nötig, das Bezugsobjekt als ganzes anzusprechen. Dies geschieht durch einen
Ausdruck folgender Form:
<Identifikator der umgebenden Klasse>.this
• Instanzen von inner classes als komplexe Bestandteile
Man kann Instanzen einer „inner class“ als Bestandteile der Instanzen der umgebenden
Klasse ansehen. Im Unterschied zu normalen Bestandteilen (Werten) haben sie eine
eigene komplexe Struktur und ein durch ihre Methoden definiertes Verhalten.
126
4.7. KLASSEN ALS NICHTSTATISCHE KLASSENBESTANDTEILE
Beispiel für Zugriffe auf das Bezugsobjekt
1
2
3
4
5
6
7
8
9
10
11
class Outer {
private int i = 5;
private int getVal() { return i; }
class Inner {
void someMethod() {
int j = i + 2; // Zugriff auf Variable
j = getVal() + 4; // Zugriff auf Methode
Outer h = Outer.this; // Zugriff auf Bezugsobjekt
}
}
}
4.7.2
Erzeugung von Instanzen einer inner class
Generell muß bei der Instanzierung einer „inner class“ immer das zu verwendende Bezugsobjekt
angegeben werden. Dies kann implizit oder explizit geschehen.
Explizite Angabe
Die explizite Angabe des Bezugsobjekts ist mit einer speziellen Schreibweise für Konstruktorausdrücke möglich:
<Ausdruck üfr Bezugs-Referenz>.<Konstruktorausdruck>
Outer x = new Outer();
Inner y = x.new Inner();
Implizite Angabe
Läßt man die Bezugsreferenz im Konstruktorausdruck für eine „inner class“ weg, so ergänzt
Java implizit den Ausdruck this.
new <inner class>(...)
ist also gleichbedeutend mit
this.new <inner class>(...)
Dies bedeutet, daß sich Instanzen der „inner class“ in allen Instanz-Methoden der umgebenden
Klasse mit dem normalen Konstruktorausdruck erzeugen lassen, wobei jeweils die aktuelle
Instanz der umgebenden Klasse als Bezugsobjekt verwendet wird.
4.7.3
Zugriff auf Instanzen einer inner class
Im Gegensatz zum Zugriff auf das Bezugsobjekt bietet Java keinerlei Unterstützung für den
umgekehrten Zugriff vom Bezugsobjekt auf die Instanz einer „inner class“. Diese Zuordnung
ist auch nicht eindeutig, mehrere Instanzen einer „inner class“ können das selbe Bezugsobjekt
haben.
127
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Abbildung 4.14: Instanzen einer inner class mit gemeinsamem Bezugsobjekt
• Will man eine Instanz einer „inner class“ vom Bezugsobjekt aus zugänglich machen,
muß man beim Bezugsobjekt eine entsprechende Variable definieren und die Instanz
nach ihrer Erzeugung explizit darin ablegen. Im Fall mehrerer Instanzen benutzt man
eine Reihung oder einen Vektor.
1
2
3
4
5
6
7
8
9
class Outer {
Inner myinner = new Inner();
void useInner() { myinner.someMethod(); }
class Inner {
void someMethod() {
...
}
}
}
128
4.8. BEHANDLUNG VON BENUTZEREINGABEN
4.8
Behandlung von Benutzereingaben
Neben der Darstellung von Komponenten ist die zweite wesentliche Aufgabe einer
Benutzungsoberfläche die Weiterleitung von Benutzereingaben und Benutzerinteraktionen zu
den Stellen des Programms, an denen sie verarbeitet werden.
4.8.1
Ereignisse
Wie in vielen anderen Benutzungsoberflächen werden die Eingaben und Interaktionen in Java
in Form von „Ereignissen“ weitergeleitet. Ereignisse sind Objekte unterschiedlicher Klassen,
die java.awt.AWTEvent als gemeinsame Basisklasse besitzen.
Beispiele für Ereignisse
Ereignisse sind beispielsweise das Drücken oder Loslassen einer Taste auf der Tastatur oder
Maus oder das Bewegen des Mauszeigers in einen bestimmten Bereich hinein oder aus ihm
heraus.
• Komponentenbezogene Ereignisse
Die wichtigsten Ereignisse im AWT sind jeweils mit einer Komponente assoziiert. Es
handelt sich um die innerste Komponente, auf deren Fläche sich der Mauszeiger befand,
als das Ereignis entstanden ist. Die assoziierte Komponente wird auch als „Quelle“ des
Ereignis bezeichnet.
4.8.2
Ereignisbehandlung
Möchte man über Interaktionen, die Benutzer mit den einzelnen Komponenten einer
Anwendung haben, informiert werden, so meldet man im Programm das Interesse an den
entsprechenden Ereignissen bei den gewünschten Quell-Komponenten an. Diese Komponenten
informieren dann im Falle einer Interaktion alle Interessenten durch einen Methodenaufruf
(Callback-Prinzip), bei dem das Ereignis als Parameter übergeben wird.
Abbildung 4.15: Ereignisbehandlung
129
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Listener
Die Interessenten, denen in Java die Ereignisse per Methodenaufruf übergeben werden, sind
ebenfalls Objekte. Sie werden als „listener“ bezeichnet.
• Interfaces für Listener
Die Ereignisse sind in Gruppen aufgeteilt und zu jeder Gruppe gibt es eine Art von
Listener-Objekten. Die beim Listener aufgerufene Methode unterscheidet sich nochmals
gemäß der Ereignis-Art. Entsprechend sind die Listener-Arten durch die aufrufbaren
Methoden charakterisiert und durch Interfaces im Package java.awt definiert.
– Das Interface KeyListener
Das Interface KeyListener enthält Methoden zum Ausliefern von TastaturEreignissen: keyPressed, keyReleased, keyTyped. Das übergebene Ereignis ist
jeweils vom Typ KeyEvent.
– Das Interface MouseListener
Das Interface MouseListener enthält Methoden zum Ausliefern von MausEreignissen: mousePressed, mouseReleased, mouseClicked, mouseEntered,
mouseExited. Das übergebene Ereignis ist jeweils vom Typ MouseEvent.
∗ Position
Bei Ereignissen vom Typ MouseEvent lassen sich u.a. die Koordinaten
der Mauszeiger-Position bei Entstehung des Ereignisses abfragen. Die
Koordinaten werden im Koordinatensystem der Quellkomponente angegeben.
Die Quellkomponente ist die Komponente über der das Mouse-Ereignis
ausgelöst wurde.
∗ Tasten
Die bei der Entstehung des Maus-Ereignisses gedrückte(n) Tasten(n) werden
als sogenannte „Modifier“ in Form einer Bitfolge (Typ int) mitgeliefert
und sind mittels getModifiers() abzufragen. Entsprechende Bitmasken sind
als statische Konstante BUTTON1_MASK, BUTTON2_MASK etc. in der Klasse
InputEvent definiert.
Um die Modifier auf eine bestimmte Taste zu prüfen, wird die entsprechende
Maske mit einer logischen Und-Operation auf sie angewendet und das Ergebnis
auf 0 geprüft:
int modif = e.getModifiers();
if ((modif & InputEvent.BUTTON2_MASK) != 0)
...
– Das Interface MouseMotionListener
Das Interface MouseMotionListener enthält Methoden zum Ausliefern von
Ereignissen, die kontinuierliche Mausbewegungen repräsentieren: mouseMoved,
mouseDragged. Das übergebene Ereignis ist wie im Fall des MouseListener vom
Typ MouseEvent.
∗ Interessant ist die Frage, wann und wie oft der mouseMoved Callback vom
System während der Mausbewegung gerufen wird. Eine einfache Lösung wäre,
130
4.8. BEHANDLUNG VON BENUTZEREINGABEN
diese
zeigt
einer
einer
Methode erst am Ende einer Mausbewegung aufzurufen. In der Praxis
sich aber, daß dies nicht ausreichend ist. Aus diesem Grund ist man zu
zeitbasierten Lösung übergegangen. Die Methode wird deshalb während
Mausbewegung in sehr kleinen Zeitabständen periodisch aufgerufen.
– Das Interface ActionListener
Das Interface ActionListener enthält nur eine Methode zum Ausliefern
von Ereignissen, die das Ergebnis der Interaktion mit einer komplexeren
Oberflächenkomponente enthalten: actionPerformed. Das übergebene Ereignis ist
vom Typ ActionEvent.
Beispiele für Quellkomponenten von ActionEvents sind Buttons und Menüeinträge.
Bei der erfolgreichen Betätigung des Buttons oder Anwahl des Menüeintrags wird
ein entsprechendes Ereignis erzeugt.
∗ Um verschiedene Interaktionen unterscheiden zu können enthält jedes
ActionEvent einen String, der als „Kommando“ bezeichnet wird und mittels
getActionCommand abgefragt werden kann. Er muß bei der Quellkomponente
mittels setActionCommand gesetzt werden und kann beliebig gewählt werden.
Will man die gleiche Aktion über verschiedene Komponenten auslösen können,
ordnet man diesen Komponenten den gleichen Kommando-String zu.
• Implementierung von Listenern
Um ein Objekt zu erhalten, das Ereignisse behandelt, muß man eine Klasse instanzieren,
die eines oder mehrere der Interfaces implementiert. Die Rümpfe der Interface-Methoden
enthalten den Programmcode, der beim Eintreten des entsprechenden Ereignisses
abgearbeitet werden soll.
– Die Adapter-Klassen
Häufig möchte man nur einen Teil der Ereignisse behandeln, die in einem der
Interfaces zusammengefaßt sind, z.B. nur mouseClicked-Ereignisse. Um die Klasse
instanzieren zu können, muß man aber alle (abstrakten) Methoden aus dem
Interface implementieren.
Als Vereinfachung für diesen Fall gibt es im AWT zu jedem Listener-Interface
eine Klasse, die alle Methoden implementiert, aber mit leerem Rumpf (d.h. die
entsprechenden Ereignisse werden ignoriert). Die Klasse zum Interface XXXListener
heißt jeweils XXXAdapter. Leitet man seine Klasse von der Adapter-Klasse ab, muß
man nur die Methoden überschreiben, deren Ereignisse man nicht ignorieren will.
• Anmeldung von Listenern bei einer Komponente
Nachdem man ein Listener-Objekt instanziert hat, muß man es noch bei der QuellKomponente anmelden, deren Ereignisse es behandeln soll.
– Anmelde-Methoden
Dies geschieht mit Hilfe der Methoden addMouseListener, addKeyListener,
addMouseMotionListener, der Klasse Component bzw. addActionListener bei
allen Klassen für Komponenten mit entsprechenden Interaktionsmöglichkeiten. Als
Parameter wird die Listener-Instanz übergeben.
131
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
– Mehrere Listener für ein Ereignis
Bei einer Komponente können sich beliebig viele Listener für die gleiche
Ereignisgruppe anmelden. Tritt ein Ereignis auf, wird jeder von ihnen durch Aufruf
der entsprechenden Methode informiert und erhält eine Kopie des Ereignisses.
Die Gesamtreaktion auf das Ereignis ergibt sich als Summe der Reaktionen aller
angemeldeten Listener.
• Automatisch vorhandene Listener
Im AWT muß der Programmierer nicht für alle Ereignisse entsprechende Listener selbst
programmieren. Typisches Verhalten von Komponenten, beispielsweise die Bewegung
eines Buttons beim Anklicken, wird durch Listener implementiert, die automatisch mit
der Komponente miterzeugt werden. Es ist aber immer möglich, zusätzliches Verhalten
durch zusätzliche Listener zu implementieren.
Implementierung von Listenern
class ActionHandler implements ActionListener {
public void actionPerformed(ActionEvent e) {
...
}
}
class MouseHandler extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
...
}
}
class MyFrame extends Frame {
public MyFrame() {
Button b = new Button();
add(b);
b.addActionListener(new ActionHandler());
Canvas c = new Canvas();
add(c);
c.addMouseListener(new MouseHandler());
}
}
132
4.8. BEHANDLUNG VON BENUTZEREINGABEN
Abbildung 4.16: Information eines ActionListeners über ein Ereignis
4.8.3
Verwendung von inner classes für Listener
Häufig benötigt ein Listener zur Behandlung eines Ereignisses Zugriff auf die Bestandteile der
Quell-Komponente oder einer anderen Bezugskomponente.
In diesem Fall bietet es sich an, die Listener-Klasse als „inner class“ der entsprechenden
Komponente zu implementieren.
Listener-Implementierung als inner class
Im folgenden Programmstück soll mit dem Anklicken des Buttons die Zeichenfläche gelöscht
werden. Dazu muß der ActionListener Zugriff auf die Zeichenfläche haben.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MouseHandler extends MouseAdapter {
public void mouseClicked(MouseEvent e) {
...
}
}
class MyFrame extends Frame {
Canvas c; // Zeichenflaeche als Bestandteil public
MyFrame() {
Button b = new Button();
add(b);
b.addActionListener(new ActionHandler());
c = new Canvas();
add(c);
c.addMouseListener(new MouseHandler());
}
class ActionHandler implements ActionListener {
public void actionPerformed(ActionEvent e) {
erase(c); // Zugriff auf äZeichenflche
}
private void erase(Canvas x) {
...
}
}
}
133
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
4.9
Benutzereingaben in MTJ
Die Funktionalität der MTJ-Benutzungsoberfläche ist erst komplett implementiert, wenn auch
die Eingaben verarbeitet werden.
4.9.1
Hinweis: Vergleich mit MTJ-Ereignissen
Insgesamt ist zu beachten, daß das im MTJ System realisierte Konzept zur Informationsverteilung zwischen den einzelnen Stationen und das hier vorgestellte Konzept zur Behandlung von
Benutzerinteraktionen auf der selben Idee basieren: Ein Objekt informiert andere Objekte,
indem es bei ihnen eine vorgegebene Methode aufruft und ein „Ereignis“-Objekt übergibt.
Abbildung 4.17: Vergleich mit MTJ-Ereignissen
4.9.2
Ereignisbehandlung
Die Ereignisbehandlung kann unterschiedlich auf Objekte verteilt werden. In der MTJOberfläche verwenden wir für jede Ereignisgruppe ein separates Objekt.
Ereignisgruppe zu ActionListener
Die mit dem ActionListener-Interface erfaßte Gruppe von Ereignissen betrifft alle Buttons
und Menüeinträge. Im Fall der MTJ-Oberfläche gibt es nur eine zugehörige Funktion, nämlich
die „Exit“-Funktion zum Beenden des Programmlaufs.
• Beenden des Programmlaufs
Bei der Verwendung des AWT ist das Erreichen des Endes des Hauptprogramms nicht
gleichbedeutend mit dem Ende des Programmlaufs. Stattdessen muß der Programmlauf
durch Aufruf der statischen Methode exit in der Klasse java.lang.System beendet
werden. Als Parameter wird ein „Programm-Ergebnis-Code“ mitgegeben, der im
Normalfall 0 sein sollte:
System.exit(0);
134
4.9. BENUTZEREINGABEN IN MTJ
• Die Klasse ExitHandler
Die Klasse mtj.level4.ExitHandler enthält die Realisierung der Funktion und
implementiert das Interface ActionListener. Zum Beenden des Programms benötigt die
Ereignis-Behandlungs-Instanz keinen Zugriff auf Komponenten der Oberfläche. Daher ist
die zugehörige Klasse ExitHandler als normale Klasse implementiert, nicht als „inner
class“ einer Komponente.
• Kommando-String
Die Klasse ExitHandler verarbeitet nur ActionEvent-Ereignisse mit dem KommandoString "Exit". Alle übrigen ActionEvents werden ignoriert.
• Die gemeinsame ExitHandler-Instanz
Es wird nur eine Instanz von ExitHandler benötigt, allerdings bei mehreren
Komponenten, über die die „Exit“-Funktion ausgelöst werden kann. Daher wird in der
Klasse ExitHandler eine Instanz erzeugt und in einer statischen Variable abgelegt. Auf
diese Weise ist sie zur Zuordnung zu beliebigen Komponenten verfügbar.
• Zuordnung zu Komponenten
Die ExitHandler-Instanz wird generell für zwei Komponenten verwendet: Für den
„Exit“-Eintrag im File-Menü und für den „Exit“-Button. Die Zuordnung wird beim
Aufbau der Oberfläche im Konstruktor von MtjFrame vorgenommen.
Ereignisgruppe zu MouseMotionListener
Die mit dem MouseMotionListener-Interface erfaßte Gruppe von Ereignissen betrifft alle
Mausbewegungen. Im Fall der MTJ-Oberfläche ist die zugehörige Funktion die Anzeige der
Mauspositions-Koordinaten wenn sich die Maus auf der Canvas-Komponente befindet.
• Anzeige mittels Label-Komponenten
Die Anzeige der Koordinaten geschieht, indem der Zahlenwert mittels der Methode
setText in die Label-Komponenten im MtjFrame geschrieben wird.
• Zugriff auf die Label-Komponenten
Zur Realisierung der Anzeige muß die Ereignis-Behandlungs-Instanz auf die LabelKomponenten zugreifen können. Zu diesem Zweck werden sie in der Klasse MtjFrame
in (nichtstatischen) Variablen abgelegt und die Klasse für die Ereignisbehandlung als
„inner class“ in MtjFrame definiert.
• Die Klasse MouseMotionHandler
Die Behandlungsklasse heißt MouseMotionHandler und implementiert das Interface
MouseMotionListener. Bei jedem eintreffenden Ereignis wird die Anzeige der
Mauskoordinaten aktualisiert.
• Zuordnung zu Komponenten
Die MouseMotionHandler-Instanz wird nur einmal benötigt, und zwar für die CanvasKomponente, die im Zentrum der MTJ-Oberfläche angeordnet ist. Entsprechend wird
die Instanz im Konstruktor von MtjFrame erzeugt und der als Parameter übergebenen
Komponente zugeordnet.
135
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
4.10
Der Konfigurations-Manager
In die Mtj-Benutzungsoberfläche eingebettet ist der Konfigurations-Manager. Dabei handelt
es sich um eine Zeichenfläche, die eine Konfiguration aus Stationen darstellt und
Interaktionsmöglichkeiten anbietet.
4.10.1
Darstellung der Konfiguration
Jede Station wird als Rechteck dargestellt. Innerhalb der Fläche wird der Stationsname
angezeigt. Außerhalb der Fläche wird in der Nähe der beiden Enden die dem jeweiligen Ende
zugeordnete Item-Art durch einen Kennbuchstaben angezeigt. Die Verbindungen zwischen
Stationen werden durch gerade Linien dargestellt, die von der Mitte der rechten Seite der
Vorgängerstation zur Mitte der linken Seite der Nachfolgerstation führen.
Abbildung 4.18: Darstellung einer Konfiguration
4.10.2
Interaktionsmöglichkeiten
Die Interaktionsmöglichkeiten lassen sich in verschiedene Gruppen aufteilen.
Layout
Eine Gruppe betrifft die Anordnung der Stationen auf der Zeichenfläche. Dazu gibt es keine
entsprechende Funktionalität im bisherigen System, es handelt sich um einen reinen Aspekt
der graphischen Oberfläche.
• Realisierung
Die Anordnung geschieht durch direktes „Ziehen“ der einzelnen als Rechtecke
dargestellten Stationen mit der Maus. Dabei muß mit der linken Maustaste in den
Mittelbereich des Rechtecks geklickt werden.
136
4.10. DER KONFIGURATIONS-MANAGER
Auf- und Umbau der Konfiguration
Hier muß die Oberfläche folgende Funktionen anbieten: Erzeugen von Stationen, Verbinden
von Stationen, Lösen von Verbindungen, Löschen von Stationen.
• Erzeugen von Stationen
Die Erzeugung geschieht über ein Popup-Menü, das durch einen Mausklick mit der
rechten oder mittleren Taste in die freie Zeichenfläche aktiviert wird. In zwei Untermenüs
für Generatorstationen und Arbeitsstationen enthält es jeweils einen Eintrag für jede im
System existierende nicht-abstrakte Stationsklasse. Bei Anwahl eines Eintrags wird eine
entsprechende Station erzeugt und an die Mauszeigerposition positioniert, an der das
Menü aktiviert wurde.
– Konstruktorparameter
Es wird jeweils der Konstruktor mit der minimalen Parameterzahl verwendet
und Parameter werden mit einem sinnvoll gewählten festen Wert besetzt. Bei
der Erzeugung kann also nur die Stationsart gewählt werden, keine weiteren
Eigenschaften.
• Verbinden von Stationen
Die Verbindung geschieht interaktiv. Dazu drückt man die linke Maustaste im rechten
Drittel des Rechtecks, das die Vorgängerstation darstellt, zieht die Maus in das Rechteck,
das die Nachfolgerstation darstellt und läßt sie dort los. Während dem Ziehen verbindet
eine gerade Linie die Mausposition mit der Mitte der rechten Seite der Vorgängerstation.
• Lösen von Verbindungen
Zum Abbau von Verbindungen wird ein Popup-Menü verwendet, das aktiviert wird,
indem man mit der rechten oder mittleren Maustaste in das Rechteck klickt, das
eine Station darstellt. Das Menü enthält einen Eintrag „release from listeners“ und für
Arbeitsstationen zusätzlich einen Eintrag „release from senders“. Bei Anwahl wird die
entsprechende Methode bei der Station aufgerufen.
• Löschen von Stationen
Das Stations-Popup-Menü enthält den zusätzlichen Eintrag „remove“. Bei Anwahl
werden sämtliche Verbindungen zu anderen Stationen abgebaut und die Station aus
der Zeichenfläche entfernt.
Stationseigenschaften festlegen
Da bei der Erzeugung der Stationen keine weiteren Eigenschaften festgelegt werden können,
wird dies als eigene Funktion realisiert. Das Stations-Popup-Menü enthält einen Eintrag „set
properties“. Bei seiner Anwahl erscheint ein modales Dialogfenster mit Eingabefeldern und
einem „OK“-Button zum Beenden der Eingabe.
137
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
• Stationsname
Das Dialogfenster soll in jedem Fall ein Eingabefenster zum Ändern des Stationsnamens
enthalten. Nach Ändern des Namens wird die Anzeige der Station entsprechend
aktualisiert, wobei sich das Rechteck der Länge des Namens anpaßt.
• Weitere Angaben
Je nach Art der Station lassen sich weitere Eigenschaften setzen. Die zu setzenden
Eigenschaften entsprechen dabei im wesentlichen den Konstruktor-Parametern der
jeweiligen Stations-Klasse. Für Generatorstationen soll es insbesondere möglich sein,
die gesendete Item-Folge festzulegen.
Konfiguration aktivieren
Zur Aktivierung der Konfiguration enthält das Stations-Popup-Menü für Generatorstationen
einen Eintrag „generate events“. Bei Anwahl wird die entsprechende Methode bei der Station
aufgerufen.
4.10.3
Ausgaben von Stationen
Alle Ausgaben, die Stationen im bisherigen System auf Standardausgabe machen, werden
umgestellt auf Ausgaben in einem Fenster der graphischen Oberfläche.
• Ausgabefenster für ItemCounterStation
Die Ausgaben erscheinen in einem nichtmodalen Dialogfenster in geeigneter Darstellung.
Das Fenster wird ggf. automatisch geöffnet, sobald eine Ausgabe erfolgt. Es wird in
die Nähe der entsprechenden Station positioniert und enthält einen „close“-Button zum
Schließen.
4.10.4
Behandlung von Ausnahmen
Ausnahmen der Klasse MtjException werden durch den Konfigurations-Manager abgefangen
und angezeigt. Danach ist ein normales Weiterarbeiten möglich.
• Anzeige der Ausnahmen
Die Anzeige erfolgt in einem modalen Dialogfenster. Das Dialogfenster stellt die mit der
Ausnahme verbundene Meldung dar und enthält einen „OK“-Button, der das Fenster
wieder schließt. Das Dialogfenster wird in die Nähe des Hauptframes der Mtj-Oberfläche
positioniert.
4.10.5
Realisierung
Zur Realisierung wird die Funktionalität des Konfigurations-Managers aufgeteilt auf
allgemeine und stationsspezifische Funktionen.
138
4.11. ANIMATION DES EREIGNISFLUSSES
Die Klasse ConfigurationArea
Die allgemeinen Funktionen werden durch die Klasse mtj.level4.ConfigurationArea
implementiert. Sie ist von Canvas abgeleitet und enthält geeignete Ereignis-Behandlungen
für alle relevanten Eingabeereignisse.
Die Klasse Station
Stationsspezifische Funktionen werden implementiert, indem in Stufe 4 die Klasse Station
von der Klasse mtj.level4.Rectangle abgeleitet wird. Damit erbt sie die Fähigkeit, sich als
Rechteck darzustellen.
• Erweiterung
Weitere Funktionen, wie das Darstellen der Verbindungen zu Nachfolgestationen und
der Sende-Item-Art, werden implementiert, indem die Methode draw in Station
überschrieben und geeignet erweitert wird. Die Anzeige der Empfangs-Item-Art erfolgt
entsprechend in der Klasse ProcStation.
• Eigenschaften
Weiterhin wird die neue Methode setProperties eingeführt, die die gesamte Eingabe
der Eigenschaften implementiert. Dabei ist die Grundfunktion des Dialogfensters mit
Namensänderung in der Klasse Station implementiert, weitere stationsartspezifische
Eingabefelder werden in den abgeleiteten Klassen durch Überschreibung zugefügt. Als
Klasse für Eingabefelder kann java.awt.TextField verwendet werden. Die Methode
wird bei Anwahl des entsprechenden Eintrags im Stations-Popup-Menü aufgerufen.
4.11
Animation des Ereignisflusses
Als Ergänzung der Funktionalität des Konfigurations-Managers wird der dynamische Ablauf
einer Konfigurations-Aktivierung dargestellt. Dabei wird die Übertragung von Ereignissen
über Verbindungen durch einen Farbwechsel der Verbindungslinie gezeigt.
4.11.1
Übertragung von Signalen und Items
Je nach Art des Ereignisses werden unterschiedliche Farben verwendet: Die Übertragung eines
Signals wird durch Wechsel auf Rot angezeigt, die eines Items durch Wechsel auf Grün.
Zwischen zwei Ereignissen wird wieder auf die normale schwarze Darstellung zurückgeschaltet.
4.11.2
Realisierung
Die Realisierung der Animation einer einzelnen Ereignisübertragung erfolgt in der Methode
sendEvent in der Klasse Station.
139
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
Geschwindigkeit der Animation
Im Programm erfordert die Übertragung des Ereignisses keine Zeit, es wird direkt die
Verarbeitung des übertragenen Ereignisses bei der Nachfolgestation angestoßen.
Für die Animation muß daher der Ablauf künstlich verlangsamt werden. Dazu schaltet das
Programm die Farbe um, wartet eine gewisse Zeit ab, schaltet wieder zurück auf Schwarz und
wartet nochmals eine gewisse Zeit.
• Zeitspanne abwarten
Zum Warten wird die folgende Java-Funktion verwendet:
Thread.currentThread().sleep(1000);
– Zeitangabe
Der Parameter gibt die Wartezeit in Millisekunden an. Statt einer festen
Zeitvorgabe kann auch eine an der Oberfläche im Rahmen der Stationseigenschaften
einstellbare Wartezeit implementiert werden.
– Ausnahmebehandlung
Die Methode sleep kann die Ausnahme InterruptedException melden, wenn der
Wartezustand frühzeitig verlassen werden muß. Diese Ausnahme wird abgefangen
und ignoriert.
Umschalten der Farbe
Um die Farbumschaltung zu realisieren, wird wie bei allen übrigen Änderungen der Darstellung
die gesamte Konfiguration neu in die Zeichenfläche gezeichnet. Die Aktualisierung der
Darstellung wird durch das Programm veranlaßt.
• Zugriff auf die Komponente
Um die Aktualisierung auszulösen benötigt das Programm Zugriff auf die darstellende
ConfigurationArea. Diese Möglichkeit wurde bereits in der Klasse Rectangle
bereitgestellt, eine Variable, in der die Komponente abgelegt ist wird an Station vererbt.
• Auslösen der Aktualisierung
Direkt anschließend an das Auslösen der Aktualisierung erfolgt das Warten mittels
sleep. Dazu muß sichergestellt sein, daß die Aktualisierung bereits ausgeführt wurde.
Aus diesem Grund wird sie mit der Methode update anstelle mit repaint veranlaßt.
• Realisierung des Farbwechsels
Der eigentliche Farbwechsel muß durch Erweiterung der Methode draw realisiert werden.
Dazu ist eine oder mehrere neue Variable in Station notwendig, in der die Information,
daß gewisse Verbindungen farbig darzustellen sind, abgelegt werden kann. Gemäß dieser
Information stellt draw dann die Verbindung dar. Die Information muß jeweils vor Aufruf
von update abgelegt werden.
140
4.11. ANIMATION DES EREIGNISFLUSSES
Aufgaben
1. Aufgabe 5 Benutzungsoberfläche Als Einstieg soll unabhängig vom restlichen MTJSystem eine einfache Benutzungsoberfläche zur direkten Manipulation einer Menge von
Rechtecken implementiert werden. Die Darstellung der Rechteckmenge wird in den
allgemeinen Rahmen für die MTJ-Oberfläche integriert.
Abbildung 4.19: eine einfache Benutzungsoberfläche (Aufgabe)
a) Implementierung und Test: Hauptfenster
Implementieren Sie die Klasse MtjFrame mit der zugehörigen Ereignisbehandlung.
Schreiben Sie ein Hauptprogramm mtj.level4.Aufg05a, in dem sie eine MtjFrameInstanz erzeugen und anzeigen. Verwenden Sie als Konstruktor-Parameter eine
Canvas-Instanz, deren Größe Sie beispielsweise auf 100x100 Pixel setzen. Testen
Sie damit die Funktionalität von MtjFrame (insbesondere die Anzeige der MausKoordinaten).
b) Implementierung: Rechtecke
Implementieren Sie die Klassen Rectangle und RectangleSet.
c) Interaktive Rechtecke
Leiten Sie von Canvas eine neue Klasse TestDrawArea ab und implementieren
Sie über geeignete, im Konstruktor zugeordnete Ereignisbehandlungs-Instanzen die
folgende Funktionalität für TestDrawArea-Instanzen:
• Jeder Instanz ist eine RectangleSet-Instanz zugeordnet, die zu Beginn leer ist.
Die Rechteck-Menge wird auf der Fläche der Instanz dargestellt.
• Popup-Menü
Jeder Instanz ist ein Popup-Menü zugeordnet.
– Aktivierung
Das Popup-Menü wird über die zweite oder dritte Maustaste aktiviert.
141
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
∗ Hinweis
Die Methode isPopupTrigger und die Masken zum Abfragen der zweiten
und dritten Maustaste sind auf verschiedenen Systemplattformen fehlerhaft. Aktivieren Sie daher das Popup-Menü im Fall eines mousePressedEreignisses, bei dem nicht die linke Maustaste (BUTTON1_MASK) gedrückt
ist!
– Exit
Implementieren Sie einen Menüeintrag „Exit“, der mit Hilfe des
ExitHandlers den Programmlauf beendet (ebenso wie der Eintrag im FileMenü etc.).
– Dialog
Implementieren Sie einen Menüeintrag „Dialog“, der ein modales Dialogfenster in der Nähe des MtjFrame öffnet. Das Dialogfenster soll einen festen
Text enthalten und einen mit „OK“ beschrifteten Button. Beim Anklicken
des Buttons wird der Dialog wieder entfernt.
– Rechteck erzeugen
Implementieren Sie einen Menüeintrag „Create Rectangle“, der ein neues
Rechteck (mit einem festen String) erzeugt, an der bei Aktivierung des
Menüs aktuellen Mausposition in die Recheckmenge zufügt und anzeigt.
– Rechteck entfernen
Implementieren Sie einen Menüeintrag „Remove Rectangle“, der, falls die
bei Aktivierung des Menüs aktuelle Mausposition im Bereich (mindestens)
eines Rechtecks liegt, dieses (eines davon) aus der Rechteckmenge (und der
Anzeige) entfernt.
• Kontrollausgaben
Bei jedem Mausklick innerhalb der Fläche der Instanz wird eine Kontrollausgabe auf Standardausgabe erzeugt. Diese gibt an, ob der Mausklick im Bereich
eines Rechtecks lag und falls ja, an welcher Position das (bzw. eines der) Rechteck(e) liegt und welcher Bereich (links/mitte/rechts) des Rechtecks angeklickt
wurde.
• Rechteck bewegen
Wird die linke Maustaste im Mittelbereich eines Rechtecks gedrückt und die
Maus dann bewegt, wird das Rechteck entsprechend mitverschoben, bis die
Maustaste wieder losgelassen wird. Dabei wird die gesamte Darstellung der
Rechteckmenge während der Bewegung ständig aktualisiert.
d) Rahmen
Schreiben Sie ein Hauptprogramm mtj.level4.Aufg05d, in dem sie eine MtjFrameInstanz erzeugen und anzeigen. Verwenden Sie als Konstruktor-Parameter eine
TestDrawArea-Instanz. Testen Sie damit die Funktionalität von TestDrawArea.
2. Aufgabe 6 Benutzungsoberfläche: Konfigurations-Manager
In dieser Aufgabe soll der erste Teil der Benutzungsoberfläche für den interaktiven
Umgang mit Konfigurationen („Konfigurations-Manager“) entwickelt werden.
142
4.11. ANIMATION DES EREIGNISFLUSSES
a) Implementierung: interaktive Konfigurationen
Implementieren Sie die Klasse ConfigurationArea mit der zugehörigen Ereignisbehandlung. Ändern Sie die Klasse Station so ab, dass sie von
mtj.level4.Rectangle abgeleitet ist und die draw-Methode geeignet überschreibt.
Ändern Sie ebenfalls die Klasse ProcStation, indem dort die draw-Methode erweitert wird. Insgesamt soll damit die folgende Funktionalität realisiert werden.
• Darstellung der Stationen mit Item-Arten und Verbindungen.
• Interaktives Layout durch Ziehen der Stationen und interaktives Verbinden der
Stationen mit der Maus.
• Popup-Menü zur Stationserzeugung und Stations-Popup-Menü mit allen
beschriebenen Einträgen außer „set properties“.
• Dialogfenster zur Anzeige von MtjException-Ausnahmen.
b) Rahmen
Schreiben Sie ein Hauptprogramm mtj.level4.Aufg06b, in dem sie eine MtjFrameInstanz mit einer ConfigurationArea-Instanz erzeugen und anzeigen. Testen Sie
damit alle implementierte Funktionalität des Konfigurations-Managers.
3. Aufgabe 7 Benutzungsoberfläche: Animation des Ablaufs In dieser Aufgabe
wird der Konfigurations-Manager vervollständigt und um die Animation des Ablaufs
von Aktivierungen ergänzt.
a) Erweiterung Konfigurationsmanager
Vervollständigen Sie den Konfigurations-Manager aus Aufgabe 6a um die folgende
Funktionalität.
• Implementierung des „set properties“ Eintrags im Stations-Popup-Menü und
implementierung der setProperties-Methode bei den Stationsklassen.
• Implementierung
des
Ausgabe-Dialogfenster
für
die
Klasse
ItemCounterStation.
b) Test: Konfigurationsmanager
Testen Sie mit dem Hauptprogramm mtj.level4.Aufg06b aus der letzten Aufgabe
den vervollständigten Konfigurations-Manager.
c) Implementierung: Ereignis-Animation
Erweitern Sie die Klasse Station um die Implementierung der Animation der
Ereignisweiterleitung.
d) Test
Testen Sie mit dem Hauptprogramm mtj.level4.Aufg06b den KonfigurationsManager mit Ablaufanimation.
143
KAPITEL 4. BENUTZUNGSOBERFLÄCHE
144
Kapitel 5
Multithreading
Im fünften Abschnitt des Praktikums wird der Einsatz von Multithreading für das System der
MTJ-Stationen behandelt.
5.1
MTJ - Stufe 5
Thema in Stufe 5 des Praktikums ist die Umstellung der Systemarchitektur auf (quasi)
parallele Abläufe. Im bisherigen System hat ein einziger Ablauf die Vorgänge in allen Stationen
bearbeitet. Nun soll jede Station einen eigenen Ablauf erhalten und weitgehend unabhängig
von den übrigen Stationen arbeiten können. Die Implementierung geschieht in Form des
neues Packages mtj.level5. Dabei werden die meisten Klassen auf Level 4 unverändert
übernommen.
5.2
Threads
Definition: In Java werden Abläufe als Threads bezeichnet.
Es handelt sich dabei um „Leichtgewichtsprozesse“, also Prozesse, die keinen eigenen
Adreßraum haben, sondern mit anderen Threads im gleichen Adreßraum laufen.
5.2.1
Threads in Java
Jedes Programm in Java verwendet Threads, ggf. nur implizit. Das minimal nötige ThreadManagement wird durch das Laufzeitsystem automatisch organisiert.
• Thread-Lebenszyklus
Jeder Thread, der in einem Programmlauf benutzt werden soll, muß erzeugt werden. Bei
der Erzeugung wird ihm eine Methode zugeordnet, die er abarbeiten soll. Der Thread
beginnt seine Arbeit, sobald er gestartet wird und beendet sie normalerweise, wenn die
Methode fertig bearbeitet ist.
145
KAPITEL 5. MULTITHREADING
• Fall: Nur ein Thread
Im einfachsten Fall verwendet ein Java-Programmlauf i.w. nur einen Thread. Dieser wird
vom Laufzeitsystem beim Programmstart erzeugt und erhält als zugeordnete Methode
die main-Methode der beim Start angegebenen Klasse. Er arbeitet die main-Methode ab
und beendet sich dann. Damit ist auch der Programmlauf beendet.
• Grafische Benutzungsoberfläche
Verwendet ein Programm eine grafische Benutzungsoberfläche, z.B. auf der Basis
von AWT, so kommen normalerweise weitere Threads hinzu. Diese Threads werden
innerhalb der Benutzungsoberflächenteile des Programms erzeugt und behandeln die
Benutzerereignisse. Das bedeutet, diese(r) Thread(s) arbeiten die Callback-Methoden
ab, die als Ereignisbehandler bei Oberflächenkomponenten angemeldet wurden. Beispiele
für solche Methoden sind actionPerformed, mousePressed, mouseMoved, etc. Weil die
Benutzerereignisse durch eigene Threads bearbeitet werden, kann ein Java-Programm
jederzeit auf Benutzereingaben reagieren, auch wenn es im Hauptprogramm gerade aktiv
ist.
• Thread-Realisierung
Die Organisation mehrerer Threads in einem Programmlauf übernimmt das JavaLaufzeitsystem. Unterstützt das darunterliegende Betriebssystem Threads, so ist es möglich, die Java-Threads durch Betriebssystem-Threads zu realisieren (dies ist abhängig
von der jeweils verwendeten Java-Implementierung). Auf einer Mehrprozessormaschine
werden dann Java-Threads u.U. von verschiedenen Prozessoren abgearbeitet. In allen
übrigen Fällen werden Threads quasi-parallel organisiert, der Prozessor wird also dynamisch zwischen den Threads hin und her geschaltet.
5.2.2
Erzeugen und Starten von Threads
Möchte der Programmierer in Java zusätzlich zu den automatisch erzeugten Threads weitere
benutzen, so muß er sie explizit erzeugen.
• Ableiten von der Klasse Thread
Ein Thread wird in Java durch ein Objekt der Klasse java.lang.Thread (abgeleitet von
java.lang.Object, siehe → Abbildung (siehe Abbildung Seite 148)) repräsentiert. Die
Erzeugung eines Threads geschieht wie im Falle aller anderen Objekte durch Abarbeitung
eines Konstruktorausdrucks. Möchte man also eine eigene Thread-Klasse schreiben, muß
man diese von der Klasse java.lang.Thread ableiten. Dabei ist die Methode run() der
Klasse java.lang.Thread zu überschreiben.
146
5.2. THREADS
Beispielcode zum Ableiten von der Klasse Thread
1
2
3
4
5
6
7
8
9
10
11
12
13
public class BspClass extends Thread {
public void run()
{
while (true) { // Endlosschleife
...do something...
}
}
}
public static void main(String[] args)
{
BspClass b = new BspClass();
b.start();
}
• Implementieren des Interfaces Runnable
Bei der Erzeugung wird dem Thread die zu bearbeitende Methode zugeordnet. Zu
diesem Zweck dient das Interface java.lang.Runnable. Es definiert als einzige abstrakte
Methode die Methode run. Ein Thread kann diese Methode entweder selbst enthalten
oder von einem beliebigen anderen Objekt mitbenutzen.
– run-Methode im Thread
Für den ersten Fall implementiert die Klasse Thread selber das Interface Runnable
und implementiert die Methode run mit einem leeren Rumpf. Um auf diese Weise
nichttriviale Threads zu erzeugen muß man eine eigene Klasse von Thread ableiten
und dort die Methode run geeignet überschreiben (vgl. die Klasse BspThread in
der → Abbildung (siehe Abbildung Seite 148)).
– run-Methode in separatem Objekt
Im zweiten Fall benötigt man eine beliebige Klasse, die das Interface Runnable
implementiert und eine geeignete Methode run definiert. Von dieser Klasse erzeugt
man eine Instanz und übergibt sie als Parameter bei der Erzeugung des Threads.
Der Thread verwendet dann nicht seine eigene run-Methode, sondern die des
übergebenen Objekts.
147
KAPITEL 5. MULTITHREADING
Beispiel zu separater run-Methode
Ein Beispiel für dieses Vorgehen zeigen die folgenden Programmfragmente (siehe
auch → Abbildung (siehe Abbildung Seite 148)):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BspRun implements Runnable {
public void run()
{ // Hier steht das vom Thread abzuarbeitende Teilprogramm
...
}
}
class MainProg {
public static void main(String[] argv)
{
Runnable r = new BspRun(); // r enthaelt die run−Methode
Thread t = new Thread(r); // t benutzt run−Methode von r
t.start(); // ab hier arbeitet t parallel zu main
...
}
}
Abbildung 5.1: Klassen zum Umgang mit Threads
• Thread-Steuerung
Die Klasse Thread definiert eine Reihe von Methoden, mit deren Hilfe sich ein Thread
steuern läßt. Folgende Darstellung zeigt alle möglichen Zustände, die ein Thread durch
Aufruf dieser Methoden annehmen kann. Zusätzlich zu Übergängen gesteuert durch die
API, sind auch Übergänge dargestellt, die durch die Java Virtual Machine gesteuert
werden.
148
5.2. THREADS
Abbildung 5.2: Zustandübergangsdiagramm für Threads
– Die Methode start()
Die wichtigste ist die Methode start(), die aufgerufen werden muß um den Thread
zu starten. Der Zustand des Threads geht von „new“ nach „ready-to-run“ über. Im
Normalfall, also wenn der Thread bis zum Ende seiner run-Methode laufen soll,
werden keine der weiteren Methoden benötigt.
– Die Methode sleep()
Die Methode sleep(long ms)kann aufgerufen werden, um einen Thread für eine
bestimmte Zeit (in Millisekunden als Parameter übergeben) anzuhalten. Der
Thread wird für diese Zeit in den Zustand „blocked“ überführt. Die Methode wirft
eine Interrupted Exception, wenn die Methode interrupt() aufgerufen wird und
es erfolgt ein Zustandswechsel von „blocked“ nach „ready-to-run“.
– Die Methode yield()
Die Methode yield()kann aufgerufen werden um die Verarbeitung zu unterbrechen. Hierfür wird einem Thread die Rechenzeit entzogen. Der Thread geht in den
Zustand „ready-to-run“ über. Dies ermöglicht anderen Threads, Rechenzeit zugewiesen zu bekommen.
– Die Methode join()
Ruft Thread1 die Methode join() von Thread2 auf, so bewirkt dies, daß Thread1
in den Zustand „blocked“ übergeht und erst dann wieder in den Zustand „readyto-run“ übergeht, wenn Thread2 beendet ist. Ist Thread2 bereits beendet wenn
join() aufgerufen wird, so geht Thread1 nicht in den Zustand „blocked“ über.
Wird die Methode „interrupt()“ aufgerufen während Thread1 im Zustand „blocked“
149
KAPITEL 5. MULTITHREADING
ist, wird eine Interrupted Exception geworfen. Die Ausführung wird nach Durchlauf
der Methode, die Thread1 in den „blocked“-Zustand überführt hat, fortgesetzt.
– Die Methode interrupt()
Wird die Methode interrupt() aufgerufen, wenn ein Thread im Zustand „blocked“
ist, wird ein Zustandswechsel nach „ready-to-run“ durchgeführt. Die Ausführung
des Threads wird fortgesetzt, wenn die Methode, die den Thread in den „blocked“Zustand überführt hat, durchlaufen ist.
– Auch wenn der gesamte Programmlauf durch Aufruf der Methode System.exit(0)
beendet wird, werden automatisch alle Threads beendet. Es reicht dagegen nicht
aus, wenn der Programmlauf am Ende der main-Methode ankommt, da dann die
übrigen Threads weiterexistieren.
• Durch die mögliche Aufteilung zwischen dem Thread und dem Objekt, das die runMethode enthält, besteht in Java nicht notwendigerweise eine 1:1-Zuordnung zwischen
dem Thread und der run-Methode. Verschiedene Threads können die gleiche runMethode abarbeiten, indem ihnen bei der Erzeugung das gleiche Runnable-Objekt
übergeben wird. Umgekehrt kann ein Thread Methoden mehrerer Objekte ausführen,
wenn von der zugeordneten run-Methode aus Methoden bei anderen Objekten aufgerufen
werden.
• Schließlich ist noch zu beachten, daß der Konstruktorausdruck für einen Thread
natürlich wiederum von einem (anderen) Thread bearbeitet werden muß. Als
Ausgangspunkt ist also immer mindestens ein existierender Thread notwendig, der vom
Laufzeitsystem automatisch erzeugt und gestartet wird. Von diesem aus können dann
im Hauptprogramm oder in davon aufgerufenen Methoden weitere Threads erzeugt und
gestartet werden.
5.3
Jeder MTJ-Station ihr eigener Thread
Wir führen für jede Arbeitsstation einen eigenen Thread ein, der unabhängig von den übrigen
Threads ablaufen kann.
5.3.1
Bisherige Situation
Im bisher implementierten MTJ-System in den Stufen 1 bis 4 ist ein einziger Ablauf für die
Arbeit aller Stationen in einer Konfiguration zuständig. Wird ein Ereignis von einer Station an
eine Nachfolgestation gesendet, so übernimmt der sendende Ablauf auch sofort die Bearbeitung
des Ereignisses bei der Nachfolgestation.
• Der Wechsel des Ablaufs zur Nachfolgestation geschieht durch den Aufruf der Methode
handleEvent in der Methode sendEvent. Da es sich hierbei um einen normalen
Unterprogrammaufruf handelt, kehrt der Ablauf erst dann zur sendenden Station zurück,
wenn das Ereignis bei der Nachfolgestation komplett verarbeitet wurde. Wird das
Ereignis von dort an weitere Stationen gesendet, so ist auch die dortige Bearbeitung
eingeschlossen.
150
5.3. JEDER MTJ-STATION IHR EIGENER THREAD
Beispiel-Ablauf mit einem Thread
Wir betrachten einen beispielhaften Ablauf in der folgenden Konfiguration aus vier
Stationen:
Abbildung 5.3: Beispiel-Ablauf mit einem Thread
151
KAPITEL 5. MULTITHREADING
• Die folgende Abbildung zeigt einen möglichen Ablauf in der Konfiguration:
Abbildung 5.4: möglicher Ablauf in der Konfiguration
Die Zeitachse verläuft von oben nach unten. Unter jedem Objekt sind als verbreiterter
Balken die Phasen eingetragen, in denen eine Methode bei dem Objekt aktiv ist. In den
weißen Abschnitten ist von der aktiven Methode aus eine Methode bei einem anderen
Objekt als Unterprogramm aufgerufen worden, in den grauen Abschnitten werden direkt
Anweisungen in der aktiven Methode bearbeitet. Damit geben alle grauen Abschnitte
zusammen den eigentlichen Ablauf wieder.
• Im Beispiel sendet die Generatorstation g drei Ereignisse an ihre Nachfolgerstation
p1. Das erste Ereignis wird an die Stationen p2 und p3 weitergesendet, das zweite
152
5.3. JEDER MTJ-STATION IHR EIGENER THREAD
Ereignis wird verschluckt und für das dritte Ereignis sendet p1 zwei Ereignisse an jede
Nachfolgestation aus. Es ist zu erkennen, wie der Ablauf jeweils ein Ereignis mit allen
Folgeereignissen komplett abarbeitet, bevor das nächste Ereignis in Angriff genommen
wird.
• Die Abbildung zeigt die Situation in Stufe 4, in der die Methode generateEvents
vom ActionHandler aus aufgerufen wird (in der Methode actionPerformed). Damit
übernimmt der Thread, der die actionPerformed-Methode abarbeitet auch automatisch
die Arbeit bei allen Stationen und die Methode actionPerformed ist erst dann beendet,
wenn alle erzeugten Ereignisse auch verarbeitet wurden.
• Unter AWT existiert ein Thread, der fast alle vom Benutzer ausgelösten Oberflächenereignisse bearbeitet. In Stufe 4 bearbeitet dieser Thread, wie in der Abbildung erkennbar,
nach Auswahl von „generateEvents“ im Popup-Menü auch den gesamten Konfigurationsdurchlauf mit ab. Dies ist daran erkennbar, daß das Programm bis zum Ende des
Durchlaufs auf die meisten weiteren Oberflächenereignisse nicht mehr reagiert.
5.3.2
Umstellung auf mehrere Threads
Die Art des Ablaufs in Stufe 4 entspricht sicher nicht der informellen Beschreibung unserer
Stationskonfigurationen, in der jede Station weitgehend selbständig Ereignisse entgegennimmt,
verarbeitet und weiterreicht.
Um dieser Vorstellung näherzukommen führen wir in Stufe 5 für jede Arbeitsstation einen
eigenen Thread ein, der genau die Bearbeitung von Ereignissen bei seiner Station übernimmt.
Arbeitsstationen
Bei einer Arbeitsstation hat der Thread die Aufgabe, folgende Schritte endlos zu wiederholen:
• Warten auf das nächste eintreffende Ereignis von einer Vorgängerstation,
• Aufruf der Methode handleEvent mit dem empfangenen Ereignis.
Generatorstationen
Eine Generatorstation dagegen ist nur kurzzeitig aktiv, während sie ihre vorbereiteten
Ereignisse sendet. Diese Aufgabe übernimmt der Einfachheit halber wie bisher der Thread, der
die Methode actionPerformed abarbeitet. Generatorstationen erhalten also keinen eigenen
Thread. 1
5.3.3
Erzeugung der Threads
Zu jeder Arbeitsstation muß in der bisher beschriebenen Lösung ein Thread erzeugt werden
und es muß ihm eine run-Methode zugeordnet werden.
1
Besser, aber auch aufwendiger wäre es, wenn in actionPerformed die Generatorstation nur informiert
würde und dann ein eigener Thread der Generatorstation die Methode generateEvents aufruft.
153
KAPITEL 5. MULTITHREADING
• Am einfachsten ist es, den Thread bei der Erzeugung der Arbeitsstation mitzuerzeugen,
also im Konstruktor von ProcStation. Da ferner die run-Methode für alle
Arbeitsstationen gleich ist, liegt es nahe, sie ebenfalls als Bestandteil der Klasse
ProcStation zu implementieren.
• Zu diesem Zweck muß ProcStation in Stufe 5 also das Interface Runnable
implementieren und die Definition der run-Methode enthalten. Im Konstruktor wird ein
zugehöriger Thread erzeugt und die Arbeitsstation selbst (d.h. this) wird als Parameter
übergeben, damit der Thread die run-Methode der Arbeitsstation ausführt. Ferner sollte
der Thread sofort gestartet werden (mit der Methode start, siehe den → Abschnitt zur
Thread-Steuerung (siehe Seite 146)).
5.3.4
Kontrollfluss zwischen Stationen
Als zweiter Punkt muß die Weitergabe der Ereignisse anders als bisher implementiert werden.
Würde wie bisher in sendEvent direkt die Methode handleEvent der Nachfolgestation
aufgerufen, würde der Thread der sendenden Station wie bisher die Verarbeitung bei der
Nachfolgestation mitübernehmen und der Thread der Nachfolgestation käme nie dazu, etwas
zu tun.
Statt dessen soll in sendEvent das Ereignis nur bei der Nachfolgestation deponiert werden
und der Thread soll sofort zurückkehren. Der Thread der Nachfolgestation holt sich dann das
deponierte Ereignis und ruft für es handleEvent auf.
• Zu diesem Zweck verwenden wir zwei neue Methoden:
public void deliverEvent(Event e) { ... }
private Event nextEvent() { ... }
• Beide Methoden sind Bestandteil der Nachfolgestation, sind also in der Klasse
ProcStation definiert.
– Die erste Methode wird von der Vorgängerstation aufgerufen um das Ereignis zu
deponieren, daher ist sie public deklariert.
– Die zweite Methode wird von der Arbeitsstation selbst aufgerufen, und zwar in der
run-Methode, um auf das nächste Ereignis zu warten und es, sobald es deponiert
wurde, als Ergebnis zu liefern. Entsprechend ist die Methode private deklariert.
• Zusätzlich sind folgende Änderungen vorzunehmen: Die Methode handleEvent braucht
nun nicht mehr im Interface EventListener definiert zu werden, sie wird dort durch die
Methode deliverEvent ersetzt.
– Die Methode handleEvent sollte statt dessen in der Klasse ProcStation als
abstrakte Methode definiert werden (sinnvollerweise protected, da sie nur in
Subklassen von ProcStation implementiert wird).
– Schließlich ist in der Klasse Station in der Methode sendEvent anstelle von
handleEvent nun deliverEvent aufzurufen. Alle weiteren Änderungen betreffen
dann nur noch die Implementierung der beiden neuen Methoden (siehe den →
154
5.4. SYNCHRONISATION ZWISCHEN THREADS
Abschnitt zur Ereignis-Übergabe (siehe Seite 158)). Insbesondere sind an keiner
Subklasse von ProcStation Änderungen erforderlich (außer ggf. die Änderung von
handleEvent von public auf protected).
– Im so entstehenden System sind das Eintreffen und die Bearbeitung von Ereignissen
bei den Arbeitsstationen nicht mehr zeitlich gekoppelt.
5.4
Synchronisation zwischen Threads
Sobald in einem Programm mehrere Threads verwendet werden, entsteht das Problem die
Threads synchronisieren zu müssen.
5.4.1
Monitore als Synchronisationsmittel
In Java wird die Synchronisation zwischen Threads nach dem „Monitor“-Konzept unterstützt.
Definition: Ein kritischer Abschnitt ist eine Folge von Befehlen, die ein Thread abarbeiten
muß, auch wenn er zwischenzeitlich Rechenzeit entzogen bekommt. Kein anderer Thread kann
den kritischen Abschnitt betreten, bevor die Abarbeitung vollständig erfolgt ist.
Definition: Die Daten auf denen ein kritischer Abschnitt arbeitet und der kritische Abschnitt
selbst sind in einem zentralen Konstrukt zusammengefaßt, dem Monitor. Der Monitor ist ein
Bereich im Programm, in dem nie mehr als ein Thread gleichzeitig aktiv sein kann.
Bezug zwischen Monitoren und Objekten
In Java sind die Monitore eng mit den Objekten verknüpft: jedes Objekt kann einen
Monitor haben. Der Monitor besteht im einfachsten Fall aus einer Menge speziell markierter
Instanzmethoden.
Methoden mit synchronized-Angabe
Eine Instanzmethode gehört zum Monitor, wenn sie mit dem Schlüsselwort synchronized
definiert wird.
Monitor-Beispiel
1
2
3
4
5
6
class BspMon {
public synchronized void A(int i) { ... }
private synchronized String B() { ... }
public int C() { ... }
public synchronized char D(String s) { ... }
}
In diesem Beispiel bilden die Methoden A, B und D zusammen den Monitor. Dies bedeutet,
daß höchstens ein Thread gleichzeitig in einer der Methoden aktiv sein kann. Ist beispielsweise
ein Thread in A aktiv, so kann kein weiterer Thread in A, B oder D aktiv sein. Es können aber
beliebig viele Threads in der Methode C aktiv sein, die nicht dem Monitor angehört.
155
KAPITEL 5. MULTITHREADING
• Der Monitor umfaßt jeweils nur eine Instanz. Werden mit
BspMon b1 = new BspMon();
BspMon b2 = new BspMon();
zwei Instanzen der Klasse BspMon erzeugt, so hat jede Instanz ihren eigenen Monitor, der
von denen der anderen Instanzen unabhängig ist. Es kann also beispielsweise gleichzeitig
ein Thread in b1.A aktiv sein und ein anderer in b2.A.
Monitor-Realisierung
Der Monitor wird dadurch realisiert, daß ein Thread, der versucht eine zum Monitor gehörige
Methode aufzurufen, vom Laufzeitsystem angehalten wird, falls bereits ein anderer Thread
in einer zum Monitor gehörenden Methode aktiv ist. Erst wenn der Monitor wieder „frei“ ist,
darf der aufrufende Thread seinen Ablauf fortsetzen.
Indem man alle Zugriffe auf Variable, die ggf. von mehreren Threads benutzt werden, in
Methoden im Monitor durchführt, kann man sicher sein, daß nie überlappende Zugriffe
vorkommen.
5.4.2
Warten auf Änderungen
Ein zweites Problem im Zusammenhang mit der Verwendung mehrerer Threads liegt darin, daß
manchmal ein Thread warten muß, bis ein anderer Thread ein bestimmtes Ergebnis produziert
hat.
• Warten in einem Monitor
In Java ist der entsprechende Mechanismus eng mit den Monitoren gekoppelt. Ein
Thread kann innerhalb eines Monitors warten (durch Aufruf der Methode wait) und ein
Thread kann innerhalb eines Monitors einen der dort wartenden Threads „aufwecken“
(durch Aufruf der Methode notify).
• Ein in einem Monitor wartender Thread gilt nicht als aktiv. Ein weiterer Thread kann
den Monitor betreten, auch wenn bereits mehrere Threads im Monitor warten.
• Auch nachdem ein Thread einen wartenden anderen Thread aufgeweckt hat, ist
nur ein Thread aktiv im Monitor: entweder der aufweckende oder der aufgeweckte
(dies entscheidet das Laufzeitsystem). Der zweite wird solange vom Laufzeitsystem
angehalten, bis der erste den Monitor entweder verlassen hat oder durch Aufruf von
wait passiv wird.
• Wichtig ist, daß mit notify nicht ein bestimmter Thread angesprochen wird, sondern
ein Monitor. Falls in dem Monitor Threads warten, wird dann einer von ihnen durch das
Laufzeitsystem ausgewählt und aufgeweckt. Wartet kein Thread im Monitor so passiert
nichts.
• Da jeder Monitor zu einem Objekt gehört, sind die Methoden notify und wait als
Instanzmethoden in der Klasse Object definiert und damit Bestandteil von jedem
beliebigen Objekt. Um in einem Monitor zu warten ruft ein Thread einfach die waitMethode beim zum Monitor gehörenden Objekt auf. Dazu muß er sich allerdings bereits
156
5.4. SYNCHRONISATION ZWISCHEN THREADS
im Monitor des Objekts (also in einer als synchronized definierten Methode des
Objekts) befinden. Ein Aufruf von wait oder notify außerhalb eines Monitors führt
zu einem Laufzeitfehler.
5.4.3
Deadlock - Beidseitiger Ausschluß
Bei falscher Anwendung eines Monitors besteht die Gefahr eines „beidseitigen“ Ausschlusses,
eines Deadlocks.
Definition: Ein Deadlock ist ein Situation, in denen zwei Threads darauf warten, daß
der jeweils andere eine Sperre bzw. einen Monitor freigibt. Weil keiner der beiden Threads
weitermachen kann, kann auch keiner eine Sperre freigeben, und beide halten für immer an.
Zur Synchronisation eignen sich typischerweise Methoden oder Codeblöcke, die auf
gemeinsame Ressourcen zugreifen, oder Klassenmethoden (statische Methoden) und
Codeblöcke, die auf statische Variablen zugreifen.
Beispiel für einen Deadlock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final Object resource1 = new Object(); //die beiden zu sperrenden Objekte
final Object resource2 = new Object();
Thread t1 = new Thread(new Runnable() { //Sperrt resource1, dann resource2
public void run() {
synchronized(resource1) {
synchronized(resource2) { compute(); }
}
}
});
Thread t2 = new Thread(new Runnable() { //Sperrt resource2, dann resource1
public void run() {
synchronized(resource2) {
synchronized(resource1) { compute(); }
}
}
});
t1.start(); //Sperrt resource1
t2.start(); //Sperrt resource2, jetzt kann kein Thread weitermachen!
157
KAPITEL 5. MULTITHREADING
Deadlock-Situation im Straßenverkehr
Zur Veranschaulichung nachfolgend eine Darstellung einer Deadlock-Situation im Straßenverkehr.
Abbildung 5.5: Deadlock-Situation im Straßenverkehr
5.5
Ereignis-Weitergabe zwischen MTJ-Stationen
Mit Hilfe der Monitore und des Wartemechanismus können wir nun die Details der EreignisÜbergabe zwischen Stationen realisieren.
5.5.1
Ereignispuffer für Arbeitsstationen
Generell muß es möglich sein, ein Ereignis bei einer Nachfolgestation abzuliefern, auch
wenn deren Thread gerade beschäftigt ist (mit der Bearbeitung vorangegangener Ereignisse).
Zu diesem Zweck führen wir einen Ereignispuffer vom Datentyp Vector bei der Klasse
ProcStation ein (nur Arbeitsstationen erhalten Ereignisse). Umgekehrt muß der Thread einer
Arbeitsstation warten, wenn er alle erhaltenen Ereignisse bearbeitet hat und kein weiteres
Ereignis vorliegt. 2
5.5.2
Synchronisation des Zugriffs
Auf den Ereignispuffer einer Arbeitsstation greifen sowohl die Threads der Vorgängerstationen
(beim ablegen von Ereignissen) als auch der eigene Thread zu. Diese Zugriffe müssen
also in einen Monitor verlegt werden. Wir verwenden zu diesem Zweck den Monitor der
2
Es handelt sich hier also um ein typisches Erzeuger-Verbraucher-Problem mit unbeschränktem Puffer.
158
5.5. EREIGNIS-WEITERGABE ZWISCHEN MTJ-STATIONEN
Arbeitsstation. Die Zugriffe erfolgen in den Methoden deliverEvent und nextEvent, diese
bilden also den Monitor der Arbeitsstation und müssen als synchronized definiert werden!
5.5.3
Ereignis-Übergabe
Die Übergabe der Ereignisse geschieht immer über den Ereignispuffer.
• In der Methode deliverEvent wird das abzuliefernde Ereignis einfach mit addElement
an den Vektor angehängt.
• In der Methode nextEvent muß zuerst geprüft werden, ob der Vektor Ereignisse enthält.
Ist dies der Fall, so wird das Element mit Index 0 aus dem Vektor entfernt und als
Ergebnis geliefert. Ansonsten muß die Methode wait der Arbeitsstation aufgerufen
werden, um zu warten, bis wieder ein Ereignis eingetroffen ist. Zu beachten ist, daß
deshalb die Methode deliverEvent in bestimmten Situationen die notify-Methode der
empfangenden Station aufrufen muß um deren wartenden Thread aufzuwecken.
– Mögliche Abläufe bei der Ereignis-Weitergabe
Die folgende Abbildung zeigt die beiden wesentlichen Möglichkeiten des Ablaufs
der Ereignisübergabe im Detail. Die Monitore sind als hellgrau hinterlegte Bereiche
dargestellt.
159
KAPITEL 5. MULTITHREADING
Abbildung 5.6: Mögliche Abläufe bei der Ereignis-Weitergabe
∗ Auf der linken Seite ruft p2 die Methode nextEvent bei leerem Puffer auf
und muß warten. Der nächste Aufruf von deliverEvent durch p1 kann den
Monitor von p2 betreten, legt ein Ereignis im Puffer ab und weckt den Thread
von p2 mittels notify auf. Sobald der Thread von p1 deliverEvent (und damit
den p2-Monitor) verlassen hat, setzt der Thread von p2 die Bearbeitung von
nextEvent fort, holt das abgelegte Ereignis aus dem Puffer und kehrt zurück
in die run-Methode.
∗ Auf der rechten Seite ruft p2 die Methode nextEvent bei nichtleerem Puffer auf
und kann sofort mit einem Ereignis zurückkehren. Das Ereignis wurde durch
einen vorangegangenen deliverEvent-Aufruf im Puffer abgelegt. In beiden
Fällen war der Monitor beim Aufruf der Methode frei.
– Beispiel für Verzögerung beim Betreten eines Monitors
Die folgende Abbildung zeigt eine Situation, in der der Monitor beim Betreten
bereits besetzt ist.
160
5.5. EREIGNIS-WEITERGABE ZWISCHEN MTJ-STATIONEN
Abbildung 5.7: Beispiel für Verzögerung beim Betreten eines Monitors
∗ Der p1-Thread versucht deliverEvent aufzurufen während der p2-Thread
in der Methode nextEvent aktiv ist. Daher wird der p1-Thread vom
Laufzeitsystem so lange angehalten, bis der p2-Thread den Monitor verlassen
hat.
Aufgaben
1. Aufgabe 8 Parallelarbeit der Stationen
In dieser Aufgabe wird das bisherige System auf „multithreading“ umgestellt.
a) Umstellung der Klassen
Ausgangspunkt ist das Ergebnis aus Aufgabe 7. Legen Sie ein neues Package
mtj.level5 an und nehmen Sie dort die erforderlichen Änderungen an den Klassen
EventListener, Station und ProcStation vor.
b) Test
Wiederholen Sie Testläufe wie aus Aufgabe 7 und erklären Sie die Unterschiede im
Ablauf.
161
Herunterladen