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