Skript 2011 - Technische Fakultät

Werbung
Universität
Bielefeld
Interner Bericht der
Technischen Fakultät
Abteilung Informationstechnik
Algorithmen und Datenstrukturen II
Skript zur Vorlesung im Sommersemester 2010
Universität Bielefeld • Postfach 10 01 31 • 33501 Bielefeld • Germany
Impressum: Herausgeber:
Robert Giegerich, Ralf Hofestädt, Peter Ladkin, Ralf Möller,
Helge Ritter, Gerhard Sagerer, Jens Stoye, Ipke Wachsmuth
Technische Fakultät der Universität Bielefeld,
Abteilung Informationstechnik, Postfach 10 01 31,
33501 Bielefeld, Germany
Vorwort
Vorwort zur ersten Auflage:
Dieses Skript ist aus der Vorlesung „Algorithmen und Datenstrukturen II“ hervorgegangen, die ich im Sommersemester 1997 an der Universität Bielefeld gehalten
habe. Seine Entstehung ist einzig und allein der Initiative der Studierenden Marcel Holtmann und Tanja Becker zu verdanken. Sie haben sich bereit erklärt, das
handschriftliche Manuskript nach LATEX zu übertragen. Jeder, der so etwas schon
einmal gemacht hat, weiß, wieviel Arbeit und Mühe in diesem Skript steckt. Ich
danke den beiden ganz herzlich.
Das letzte Kapitel wurde von Christian Büschking gestaltet, der mich in der letzten
Vorlesungswoche wegen einer Konferenzteilnahme vertreten hat. Auch ihm gilt
mein Dank.
Schließlich möchte ich mich bei Nicole Schwerdt bedanken, die mich während
der letzten Überarbeitung des Skriptes bei Schreibarbeiten unterstützt hat.
Das vorliegende Skript enthält mit an Sicherheit grenzender Wahrscheinlichkeit
Fehler. Wenn jemand einen Fehler findet, oder meint, ein Sachverhalt sei unklar formuliert, kann er/sie eine Nachricht an [email protected]
schicken.
Bielefeld, im Oktober 1997
Enno Ohlebusch
Vorwort zur Auflage 2005:
Mittlerweile wurde das Skript durch verschiedene Autoren aktualisiert und ergänzt. Im Sommersemester 2001 hat Robert Giegerich Änderungen in Kapitel 1
eingefügt.
Für das Sommersemester 2002 habe ich kleinere Umstellungen in den Kapiteln 2
und 7 vorgenommen, um die objektorientierten Anteile der Programmiersprache
Java etwas stärker zu betonen. Zum Sommersemester 2004 hat es einige weitere
kleine Ergänzungen gegeben, insbesondere ausführliche einleitende Beispiele in
den Kapiteln 4.6 und 8, für die ich Peter Menke und Sebastian Kespohl herzlich
danke.
Bielefeld, im August 2005
Jens Stoye
Vorwort zur Auflage 2006:
Für das Sommersemester 2006 habe ich nur wenige kleine Korrekturen vorgenommen und die Grafiken verfeinert.
Bielefeld, im Mai 2006
Tim Nattkemper
Vorwort zur Auflage 2008:
Im Sommersemester 2008 wurde das Skript um drei Kapitel erweitert: das Kapitel
Graphen gibt eine kurze Einführung in Graphrepräsentationen und einige wenige
beispielhafte Algorithmen auf Graphen. Nils Hoffmann hat eine kurze Einführung
in Threads verfasst, Jan Krüger eine Einführung in GUI-Programmierung in Java
mittels Swing.
Bielefeld, im Mai 2008
Alexander Sczyrba
ii
Inhaltsverzeichnis
1 Syntax und Semantik
1.1 Einführung . . . . .
1.2 EBNF-Definitionen
1.3 Syntaxdiagramme .
1.4 Aufgaben . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
. 1
. 9
. 13
. 16
2 Java: Der Einstieg
2.1 Grundlegendes zu Java . . . . . . . . . . . . .
2.1.1 Historisches . . . . . . . . . . . . . . .
2.1.2 Eigenschaften von Java . . . . . . . . .
2.1.3 Sicherheit . . . . . . . . . . . . . . . .
2.1.4 Erstellen eines Java-Programms . . . .
2.2 Grundzüge imperativer Sprachen . . . . . . .
2.2.1 Das Behältermodell der Variablen . . .
2.2.2 Konsequenzen . . . . . . . . . . . . .
2.3 Klassen, Objekte und Methoden im Überblick
2.3.1 Klassen . . . . . . . . . . . . . . . . .
2.3.2 Das Erzeugen von Objekten . . . . . .
2.3.3 Klassenvariablen . . . . . . . . . . . .
2.3.4 Methoden . . . . . . . . . . . . . . . .
2.3.5 Klassenbezogene Methoden . . . . . .
2.4 Vererbung, Pakete und Gültigkeitsbereiche . .
2.4.1 Vererbung . . . . . . . . . . . . . . . .
2.4.2 Pakete . . . . . . . . . . . . . . . . . .
2.4.3 Gültigkeitsbereiche . . . . . . . . . . .
2.5 Aufgaben . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
19
19
19
19
21
21
22
22
22
22
23
23
24
24
25
25
25
26
26
27
3 Imperative Programmierung in Java
3.1 Mini-Java . . . . . . . . . . . . .
3.2 Von Mini-Java zu Java . . . . . .
3.2.1 Elementare Datentypen . .
3.2.2 Kommentare . . . . . . .
3.2.3 Bool’sche Operatoren . . .
3.2.4 Bitoperatoren . . . . . . .
3.2.5 Inkrement und Dekrement
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
29
29
32
32
33
34
34
35
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
iii
Inhaltsverzeichnis
3.2.6 Zuweisungsoperatoren . . . .
3.2.7 Die nichtabweisende Schleife .
3.2.8 for-Schleife . . . . . . . . . .
3.2.9 if-then-else Anweisung . . . .
3.2.10 Mehrdimensionale Felder . . .
3.2.11 switch-Anweisung . . . . . .
3.3 Aufgaben . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Algorithmen zur exakten Suche in Texten
4.1 Die Klasse String . . . . . . . . . . . . . . . . . . . .
4.2 Grundlegende Definitionen . . . . . . . . . . . . . . .
4.3 Das Problem der exakten Suche . . . . . . . . . . . .
4.4 Der Boyer-Moore-Algorithmus . . . . . . . . . . . . .
4.4.1 Die bad-character Heuristik . . . . . . . . . .
4.4.2 Die good-suffix Heuristik . . . . . . . . . . . .
4.5 Der Boyer-Moore-Horspool-Algorithmus . . . . . . .
4.6 Der Knuth-Morris-Pratt-Algorithmus . . . . . . . . .
4.6.1 Einführendes Beispiel . . . . . . . . . . . . . .
4.6.2 Funktionsweise . . . . . . . . . . . . . . . . .
4.6.3 Korrektheit der Berechnung der Präfixfunktion
4.7 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 Objektorientierte Programmierung in Java
5.1 Traditionelle Konzepte der Softwaretechnik . . . . . . . . . . . .
5.1.1 Beispiel: Der ADT Stack . . . . . . . . . . . . . . . . . . .
5.2 Konzepte der objektorientierten Programmierung . . . . . . . .
5.3 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . .
5.4 Kommunikation mit Nachrichten . . . . . . . . . . . . . . . . . .
5.5 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.6 Konstruktoren und Initialisierungsblöcke . . . . . . . . . . . . . .
5.7 Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8 Methoden in Java . . . . . . . . . . . . . . . . . . . . . . . . . .
5.9 Unterklassen und Vererbung in Java . . . . . . . . . . . . . . . .
5.10 Überschreiben von Methoden und Verdecken von Datenfeldern .
5.11 Konstruktoren in Unterklassen . . . . . . . . . . . . . . . . . . .
5.12 Reihenfolgeabhängigkeit von Konstruktoren . . . . . . . . . . .
5.13 Abstrakte Klassen und Methoden . . . . . . . . . . . . . . . . .
5.14 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
35
36
36
37
39
40
42
.
.
.
.
.
.
.
.
.
.
.
.
43
43
45
46
48
48
48
49
51
51
52
56
59
.
.
.
.
.
.
.
61
61
62
62
64
64
65
66
.
.
.
.
.
.
.
.
69
71
73
75
77
78
79
81
6 Übergang von funktionaler zu OOP
85
6.1 Imperative vs. funktionale Programmierung . . . . . . . . . . . . . 85
6.2 Listen in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
iv
Inhaltsverzeichnis
6.3 Vergleich zwischen Haskell und Java . . . . . . . . . . . . . . . . . 92
6.4 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
7 Programmieren im Großen
7.1 Schnittstellen . . . . . . . . . . . . . . . . . . . . . . . . .
7.1.1 Beispiel: Die vordefinierte Schnittstelle Enumeration
7.2 Pakete . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2.1 Paketinhalte . . . . . . . . . . . . . . . . . . . . . .
7.2.2 Paketbenennung . . . . . . . . . . . . . . . . . . .
7.3 Ausnahmen (Exceptions) . . . . . . . . . . . . . . . . . . .
7.3.1 throw und throws . . . . . . . . . . . . . . . . . .
7.3.2 try, catch und finally . . . . . . . . . . . . . . .
7.4 Aufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
101
. 101
. 104
. 105
. 106
. 106
. 107
. 109
. 110
. 114
8 Graphen
8.1 Anwendungen von Graphen . . . . .
8.2 Terminologie . . . . . . . . . . . . . .
8.3 Repräsentation von Graphen . . . . .
8.4 Ein Abstrakter Datentyp (ADT) Graph
8.5 Breitensuche . . . . . . . . . . . . . .
8.6 Tiefensuche . . . . . . . . . . . . . .
8.7 Topologisches Sortieren . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
127
. 127
. 129
. 131
. 131
. 132
. 134
. 137
.
.
.
.
.
.
139
. 139
. 140
. 141
. 142
. 143
. 144
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9 Hashing
9.1 Einführendes Beispiel . . . . . . . . . . . .
9.2 Allgemeine Definitionen . . . . . . . . . .
9.3 Strategien zur Behandlung von Kollisionen
9.3.1 Direkte Verkettung . . . . . . . . .
9.3.2 Open Hashing . . . . . . . . . . . .
9.4 Die Klasse Hashtable in Java . . . . . . . .
9.5 Aufgaben . . . . . . . . . . . . . . . . . .
10 Ein- und Ausgabe
10.1 Ströme . . . . . . . . . . . . . .
10.1.1 Die Klasse InputStream
10.1.2 File-Ströme . . . . . . .
10.1.3 Gepufferte Ströme . . .
10.1.4 Datenströme . . . . . .
10.2 Stream Tokenizer . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
115
115
116
118
119
121
123
125
11 Graphische Benutzeroberflächen mit Swing
147
11.1 Historisches . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
11.2 Ein Fenster zur Welt . . . . . . . . . . . . . . . . . . . . . . . . . . 147
v
Inhaltsverzeichnis
11.3 Swing Komponenten . . . . . . . . . . . . . . . .
11.3.1 Eine einfache Schaltfläche - JButton . . . .
11.3.2 Ein Texteingabefeld - JTextField . . . . . .
11.4 Container . . . . . . . . . . . . . . . . . . . . . .
11.5 LayoutManager . . . . . . . . . . . . . . . . . . .
11.5.1 FlowLayout . . . . . . . . . . . . . . . . .
11.5.2 BorderLayout . . . . . . . . . . . . . . . .
11.5.3 GridLayout . . . . . . . . . . . . . . . . .
11.5.4 BoxLayout . . . . . . . . . . . . . . . . . .
11.6 Ereignisse und deren Behandlung . . . . . . . . .
11.6.1 ActionListener . . . . . . . . . . . . . . . .
11.6.2 Events und Listener . . . . . . . . . . . . .
11.7 Java Applet vs. Java WebStart vs. Java Application
11.7.1 Java (Webstart) Application . . . . . . . .
11.7.2 Java Applets . . . . . . . . . . . . . . . . .
11.7.3 Java Sicherheitskonzepte . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
12 Parallele Ausführung - Nebenläufigkeit - Threads
12.1 Das Threadmodell von Java . . . . . . . . . . . . . . .
12.2 Thread Pools . . . . . . . . . . . . . . . . . . . . . . .
12.3 Besonderheiten bei Swing - Der Event Dispatch Thread
12.4 Weiterführende Konzepte . . . . . . . . . . . . . . . .
12.5 Java Beans . . . . . . . . . . . . . . . . . . . . . . . . .
12.5.1 Konventionen . . . . . . . . . . . . . . . . . . .
12.5.2 Speichern und Laden . . . . . . . . . . . . . . .
vi
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
148
148
150
151
151
152
153
154
155
156
156
157
158
159
159
159
.
.
.
.
.
.
.
161
. 161
. 164
. 165
. 168
. 168
. 168
. 169
1 Syntax und Semantik
Bevor wir richtig in das Thema dieses zweiten Teiles der Vorlesung
Algorithmen und Datenstrukturen einsteigen, werden in diesem Einführungskapitel zunächst einige der notwendigen Definitionen behandelt, um Programmiersprachen überhaupt formal exakt beschreiben und die Bedeutung ihrer Programme festlegen zu können: Formale Sprachen, Grammatiken, Syntax und Semantik. Dies wird anhand von drei Beispielen geschehen: der Steuerung eines Bohrkopfes, einer Sprache zur Beschreibung von RNA-Sekundärstrukturen
und an Mini-Java, einer Teilsprache von Java.
1.1 Einführung
Programmiersprachen sind formale Sprachen. Es ist präzise festgelegt,
• welche Zeichenreihen überhaupt Programme einer Sprache L sind (Syntax),
• welche Ein-/Ausgabefunktion ein Programm berechnet (Semantik).
konkrete Syntax: genaue textuelle Aufschreibung für Programme.
abstrakte Syntax: Bindeglied zur Semantik; gibt an, wie ein Programm(-stück)
aufgebaut ist.
Beispiel 1.1.1
konkret
abstrakt
assign
x := y + 5
x=y+5
LET x = y + 5
ADD 5 TO y GIVING x
STORE y + 5 TO x
@
R
@
x
add
var
@
R
@
Pascal
C, Fortran, Java
Basic (anno 1963)
COBOL
dBase
const
@
y
R
@
5
1
1 Syntax und Semantik
In der abstrakten Syntax tauchen die primitiven Konstrukte einer Programmiersprache auf, sowie Operatoren, die diese zu neuen Konstrukten kombinieren.
primitiv:
Bezeichner, Zahl
Kombination: var:
Bezeichner
const: Zahl
add:
Ausdruck × Ausdruck
assign: Bezeichner × Ausdruck
→ Ausdruck
→ Ausdruck
→ Ausdruck
→ Anweisung
Für einen Sprachdesigner und den Übersetzer ist die abstrakte Syntax die wesentliche Programmdarstellung. Zum Programmieren dagegen braucht man die
konkrete Syntax.
Definition 1.1.2
• Ein Alphabet A ist ein endlicher Zeichenvorrat (eine endliche Menge).
• Die Mengen aller endlichen Zeichenreihen über einem Alphabet A bezeichnen wir mit A∗ .
• Das leere Wort der Länge 0 bezeichnen wir mit ε.
Definition 1.1.3 Eine Menge L ⊆ A∗ heißt formale Sprache über dem Alphabet
A.
Einen abstrakteren Sprachbegriff kann man kaum definieren. Die einzige Frage,
die man sich über w ∈ A∗ stellen kann, ist: Gilt w ∈ L oder w 6∈ L? Diese Frage
nennt man das Wortproblem von L.
Definition 1.1.4 Eine Programmiersprache ist ein Paar (L, L), wobei L ⊆ A∗ eine
formale Sprache und L : L → (A∗ → A∗ ) die Semantik von L ist.
Damit ordnet L jedem L-Programm l ∈ L als seine Bedeutung die Ein-/Ausgabefunktion L(l) zu, wobei Ein- und Ausgabe ebenfalls Zeichenreihen über A sind.
Für L(l) schreiben wir auch kurz Ll.
Definition 1.1.5 Eine kontextfreie Grammatik ist ein 4-Tupel G = (N, A, P, S),
wobei
1. N ein Alphabet von sogenannten Nonterminalsymbolen,
2. A ein Alphabet von sogenannten Terminalsymbolen mit N ∩ A = ∅,
3. P eine endliche Menge von Produktionen (Regeln) der Form V → α mit
V ∈ N und α ∈ (V ∪ A)∗ und
4. S ∈ N ein Startsymbol ist.
2
1.1 Einführung
Die Menge S(G) der Satzformen von G ist die kleinste Teilmenge von (N ∪ A)∗
mit den folgenden Eigenschaften:
1. S ∈ S(G).
2. Wenn αV β ∈ S(G) für ein Nonterminalsymbol V ∈ N und Zeichenfolgen
α, β ∈ (N ∪ A)∗ und wenn V → γ ∈ P eine Regel ist, so gilt auch αγβ ∈
S(G) („Ableitungsschritt“).
def
Die durch G definierte Sprache ist L(G) = S(G)∩A∗ . Den Test, ob ein gegebenes
Wort w durch eine Grammatik G erzeugt werden kann, also ob w ∈ L(G) gilt,
nennt man das Wortproblem von G.
Beispiel 1.1.6 (Syntax und Semantik von Sprachen: Bohrkopfsteuerung)
Wir betrachten sehr einfache Programme zur Steuerung eines Bohrkopfes. Das
Gerät kennt die folgenden Befehle:
N
W
E
O
S
Bewegung um 1 Gitterpunkt (north, east,
west, south)
O: Einstellen auf Nullpunkt (origin)
D: Senken des Bohrkopfes mit Bohrung (drill)
U : Heben des Bohrkopfes (up)
N, E, W, S:
Programme zur Maschinensteuerung sind z.B.
“ON N N EEDO”
Bohrung am Punkt (2,3) mit Rückkehr zum Nullpunkt
“ODU DU DU ”
Dreifach-Bohrung am Nullpunkt
“DU N DU W DU SDU ” bohrt Gitterquadrat, wo der Kopf gerade steht
In der ersten Variante unserer Steuersprache lassen wir beliebige Befehlsfolgen
zu. Außerdem kann jedes Programm als Eingabe ein Paar von Start-Koordinaten
erhalten.
L1 = {N, E, W, S, U, D, O}∗
Eingabe: (Int, Int)
3
1 Syntax und Semantik
Formal heißt dies, dass zu unserem Alphabet auch Koordinatenpaare gehören,
die aber in den Programmen selbst nicht auftreten.
Intuitiv scheint die Semantik der Sprache klar – der Bohrkopf wird bewegt und
bohrt mit jedem D-Befehl am aktuellen Gitterpunkt. Allerdings gibt es da ein
paar Feinheiten, auf die wir später zurückkommen. Zunächst definieren wird die
Semantik der Sprache L1 . Was soll die Ausgabe sein? Real ist es das gebohrte
Werkstück. Mathematisch gesehen entspricht dem eine Liste der Koordinaten
aller erfolgten Bohrungen.
L1 : L1 → (Int, Int) → [(Int, Int)]
Wir beschreiben die Semantik einzelner Befehle als Transformation des Maschinenzustands. Dieser enthält vier Komponenten:
x
y
l
cs
aktuelle x-Koordinate
aktuelle y-Koordinate
aktueller Hebezustand des Bohrkopfes: 0 gesenkt, 1 oben
Liste bisheriger Bohr-Koordinaten
Den Effekt einzelner Befehle beschreibt die Funktion
bef :: Befehl → Zustand → Zustand
bef b (x, y, l, cs) = case b of
O → (0, 0, 1, cs)
N → (x, y + 1, l, cs)
W → (x − 1, y, l, cs)
S → (x, y − 1, l, cs)
E → (x + 1, y, l, cs)
D → (x, y, 0, (x, y) : cs)
U → (x, y, 1, cs)
Die Abarbeitung einer Befehlsfolge beschreibt die Funktion
beff :: Befehl∗ → Zustand → [(Int, Int)]
beff [ ](x, y, l, cs) = reverse cs
beff (b : bs)(x, y, l, cs) = beff bs(bef b (x, y, l, cs))
Am Ende drehen wir die Liste der Bohrkoordinaten um, damit sie in der getätigten
Reihenfolge erscheint. Mathematisch gesehen ist das nicht nötig. Die Semantik
L1 wird beschrieben durch die Funktion
prog :: Befehl∗ → (Int, Int) → [(Int, Int)]
prog bs (i, j) = beff bs (i, j, 1, [])
Programmiersprache L1 und ihre Semantik L1 sind sehr naiv. Man sollte zum Beispiel bedenken, dass das Werkstück und vielleicht auch die Maschine beschädigt
4
1.1 Einführung
werden, wenn mit abgesenktem Bohrkopf Bewegungen ausgelöst werden. Freilich – solange niemand solche Steuerprogramme erzeugt, geht alles gut. Jedoch
wollen wir uns nicht darauf verlassen . . .
Generell gibt es zwei Möglichkeiten, eine Sprache (L, L) zu verfeinern: syntaktisch oder semantisch.
Semantisch heisst: das “Unglück” kann programmiert werden, tritt aber nicht
ein.
Syntaktisch heisst: das “Unglück” wird bereits als syntaktisch fehlerhaftes Programm abgewiesen.
Aufgabe 1.1.7 Modifiziere die Semantik L1 (bei unverändertem L1 ) in zwei verschiedenen Weisen. Tritt eine Bewegung mit abgesenktem Bohrkopf auf, so wird
a) die Ausführung des Programms abgebrochen und nur die Koordinaten der
bisherigen Bohrungen ausgegeben,
b) die Ausführung des Programms abgebrochen und keine Koordinaten angegeben.
Was bedeuten diese semantischen Unterschiede in der Bohrpraxis?
Wir entscheiden uns nun für die syntaktische Verfeinerung und stellen Forderungen an die Steuersprache L:
1. Auf Befehl D muss stets U oder O folgen.
2. Befehl O ist immer möglich, U nur unmittelbar nach D.
3. Alle Programme enden mit dem Bohrkopf am Nullpunkt.
4. Auf dem Weg von einer Bohrung zur nächsten sind gegenläufige Richtungswechsel unerwünscht, zum Beispiel . . . N SN S . . . oder . . . N ES . . . ,
weil sie die Maschine in Schwingung versetzen können.
Alle diese Forderungen lassen sich durch Einschränkungen von L1 erfüllen, erfordern aber verfeinerte Methoden zur syntaktischen Sprachbeschreibung. Solche
Mittel sind Grammatiken.
Grammatik G1 (zur Beschreibung von L1 )
A
N
S
P
={
={
=
={
N, E, W, S, U, D, O}
moves, move}
moves
moves → ε| move moves
move → N |E|W |S|U |D|O}
5
1 Syntax und Semantik
Hier gilt L(G1 ) = A∗ . Es ist w ∈ L(G1 ) mit
w = “W EN DEDEN U DOSU ED00
Übung: Leite w mit dieser Grammatik ab.
Verfeinerte Grammatik G2 (berücksichtigt Forderungen (1) - (3), aber nicht (4)).
A, N, S wie G1
P = { moves → O|DO| move moves
move → N |E|W |S|O|DU |DO}
Frage: Warum brauchen wir die Regel moves → DO überhaupt?
Antwort: Sonst ist “DO” ∈
/ S(G2 ).
Warum ist nun w = “W EN DEDEN U DOSU ED” ∈
/ S(G2 )?
Versuch einer Ableitung:
moves →
→
→
→
→3
→
move moves
W
moves
W
move moves
W
E
moves
W
E
N
move moves
W
E
N
?
Hier kann nur DU oder DO erzeugt werden, aber nicht D allein oder DE.
Verfeinerte Grammatik G3 (berücksichtigt Forderungen (1) und (4), (2) nur teilweise und (3) gar nicht):
A, S wie G1
N = { moves, ne, nw, se, sw, drill }
P = { moves → ε | ne moves | nw moves | se moves | sw moves
ne → N ne | E ne | drill
se → S se | E se | drill
nw → N nw | W nw | drill
sw → S sw | W sw | drill
drill → DU |DO }
Durch die Nichtterminale ne, nw, se, sw entscheidet die Grammatik zunächst
über die grobe Lage des nächsten Bohrloches, das dann ohne gegenläufigen
Richtungswechsel angesteuert wird.
Aufgabe 1.1.8 Schreibe eine Grammatik, die alle Forderungen (1) - (4) berücksichtigt.
6
1.1 Einführung
Aufgabe 1.1.9 Schreibe eine Grammatik, deren Programme den Befehl O nicht
benutzen und den Bohrkopf nach der letzten Bohrung in Umkehrung des gefahrenen Weges zum Startpunkt zurückführen. Ignoriere Forderungen (1) - (4).
Hinweis: Verwende Produktionen der Form moves → N moves N . . .
Beispiel 1.1.10 (RNA-Sekundärstrukturen)
Ein RNA-Molekül besteht aus Basen A,C,G,U. Durch Wasserstoff-Brücken zwischen A–U, G–C, G–U bilden sich Basenpaarungen, die zu einer Sekundärstruktur
führen.
Primärsequenz
C A C C U A A G G U C C
Sekundärstruktur
C A C C U A A G G U C C
C
C
A
C
C
U
← GHelix-Bildung schafft Stabilität
C
G
U
A
A
Grammatik zur Beschreibung der Sekundärstruktur (Ausschnitt):
A = { A, C, G, U }
N = { struct, any, stack, loop }
S=
struct
P={
struct → any | any struct | struct any | stack | ε
any
→ A | C | G | U
stack →
A stack U | U stack A |
G stack C | C stack G |
G stack U | U stack G | loop
loop → any loop | any any any }
Allein mit den ersten beiden Produktionen kann man alle RNA-Sequenzen ableiten:
struct → any struct → A struct → A any struct → AC struct . . .
Damit ist L(G) = A∗ . Der Witz der Grammatik ist, dass manche Ableitungen das
Vorliegen möglicher Sekundärstrukturen anzeigen – dann nämlich, wenn sie die
Produktionen für stack benutzen.
struct →2 C struct →4 C struct CC → C stack CC
→ CA stack UCC →2 CACC stack GGUCC → CACC loop GGUCC
→4 CACCUAAGGUCC
7
1 Syntax und Semantik
Noch deutlicher wird das Erkennen der Sekundärstruktur, wenn wir diese Ableitung als sogenannten Syntaxbaum ausschreiben:
struct
C
any struct
H
structHanyHH
H
C
struct anyHH
C
stack
H
H
A stack U
H
C
H
H
G
stackH
H
C stack G
U
loopH
H
any
any anyHH
A
A
Wer also mit dieser Grammatik und einer gegebenen RNA-Sequenz w das Wortproblem w ∈ L(G) löst, kann Aussagen über mögliche Sekundärstrukturen machen.
Die Chomsky-Hierarchie
In den 50er Jahren wurden von Noam Chomsky vier Typen von Grammatiken
vorgestellt, die unterschiedlich komplexe Klassen von Produktionen erlauben. Wir
geben jeweils ein Beispiel für den erlaubten Regeltyp an. Es ist X, Y, Z ∈ N ,
a, b, c ∈ A.
Typ 0:
Typ 1:
Typ 2:
Typ 3:
XaY b
aXb
X
X
→
→
→
→
cXZ
aZY b
aY bZc
aY
(allgemein)
(kontextsensitiv)
(kontextfrei)
(regulär)
Die Sprachklassen vom Typ 0 – 3 bilden eine echte Hierarchie, ihr Wortproblem
liegt in unterschiedlichen Komplexitätsklassen:
Typ 0:
Typ 1:
Typ 2:
unentscheidbar
exponentiell
polynomiell (Θ(n3 ) oder besser;
bei Grammatiken für Programmiersprachen in der Regel Θ(n))
Typ 3: linear
8
1.2 EBNF-Definitionen
1.2 EBNF-Definitionen
Historisches:
• Syntaxbeschreibung von FORTRAN und COBOL (am Anfang) durch Beispiele und Gegenbeispiele.
• 1958 formale Beschreibung der Syntax von ALGOL durch John Backus;
Backus-Normalform (BNF).
• Kleine Verbesserungen in der Notation durch Peter Naur, daher spricht man
heute von der Backus-Naur-Form (BNF).
• Niklaus Wirth hat die Backus-Naur-Form noch einmal überarbeitet und erweitert (EBNF – Extended BNF).
Definition 1.2.1 Die Metazeichen der EBNF (vgl. Klaeren [7], S. 104) sind:
das Definitionszeichen
=
das Alternativzeichen
|
die Anführungszeichen
" "
die Wiederholungsklammern { }
die Optionsklammern
[ ]
die Gruppenklammern
( )
der Punkt
.
Die Menge ET der EBNF-Terme ist gegeben durch:
1. Ist V eine Folge von Buchstaben und Ziffern, die mit einem Buchstaben
beginnt, so gilt V ∈ ET und gilt als Nonterminalsymbol.
2. Ist w eine Folge von beliebigen Symbolen, so ist “w”∈ ET und gilt als ein
(!) Terminalsymbol.
3. Für α ∈ ET sind auch
a) (α) ∈ ET ,
b) [α] ∈ ET und
c) {α} ∈ ET .
4. Für α1 , . . . , αn ∈ ET sind auch
a) α1 | . . . |αn ∈ ET und
b) α1 α2 . . . αn ∈ ET .
9
1 Syntax und Semantik
Eine EBNF-Definition besteht aus einer endlichen Menge von EBNF-Regeln der
Form
V = α.
wobei V ein Nonterminalsymbol entsprechend obiger Konvention und α ein
EBNF-Term ist. Das Nonterminalsymbol auf der linken Seite der ersten Regel ist
das Startsymbol.
Beispiel 1.2.2 EBNF-Definition für Mini-Java, einer Teilsprache von Java.
program
= “class” ident “{” mainMethod “}”.
mainMethod = “public” “static” “void” “main” “(” “String” “[” “]” argsIdent “)” block.
statement
= “int” ident “=” expression “;”
| ident “=” expression “;”
| “if” “(” condition “)” statement
| “while” “(” condition “)” statement
| block
| “System” “.” “out” “.” “println” “(” expression “)” “;”
| “;”
| “int” “[” “]” arrayIdent “=” “new” “int” “[” expression “]” “;”
| arrayIdent “[” expression “]” “=” expression “;”.
block
= “{” { statement } “}”.
condition
= expression ( “==” | “!=” | “<” | “<=” | “>” | “>=” ) expression.
expression = [ ( “+” | “-” ) ] term { ( “+” | “-” ) term }.
term
= factor { ( “*” | “/” ) factor }.
factor
= ident
| number
| “(” expression “)”
| “Integer” “.” “parseInt” “(” argsIdent “[” expression “]” “)”
| argsIdent “.” “length”
| arrayIdent “.” “length”
| arrayIdent “[” expression “]”.
ident
= ( letter | “_” | “$” ) { letter | digit }.
number
= ( “0” | digit { digit | “0” } ).
digit
= “1” | “2” | . . . | “9”.
letter
= “A” | . . . | “Z” | “a” | . . . | “ z”.
argsIdent
= ident.
arrayIdent
= ident.
10
1.2 EBNF-Definitionen
Um die Semantik einer EBNF-Definition zu definieren, benötigen wir folgende
Operationen auf Sprachen:
Definition 1.2.3 Seien L, L1 und L2 beliebige Sprachen (Wortmengen) über einem gemeinsamen Alphabet. Dann definieren wir:
def
1. Komplex-Produkt: L1 L2 = {w1 w2 | w1 ∈ L1 , w2 ∈ L2 }
(also L∅ = ∅L = ∅; L{ε} = {ε}L = L)
def
2. n-fache Iteration: L0 = {ε}, Ln+1 := LLn
def
3. Stern-Operation: L∗ =
S
n∈N
Ln
Beispiel 1.2.4
1. {aa, ab} {aa, ε} = {aaaa, abaa, aa, ab}
2. {a, b, c}2 = {a, b, c} {a, b, c} = {aa, ab, ac, ba, bb, bc, ca, cb, cc}
3. {a, b}∗ = {ε, a, b, aa, ab, ba, bb, . . . }
Definition 1.2.5 Die Semantik der EBNF definieren wir durch Rekursion über die
EBNF-Terme. Sei E eine EBNF-Definition (wobei S das Startsymbol, N die Menge
der Nonterminals und A die Menge der Terminals sei) und ET die Menge der
EBNF-Terme. Dann ist die von E erzeugte Sprache L(E) definiert als JSKE , wobei
J KE : ET ; P (A∗ )
wie folgt definiert ist (vgl. Klaeren [7], S. 107):
JαKE
falls V = α. eine Regel in E ist
def
1. Für V ∈ N ist JV KE =
∅
sonst
def
2. J“w”KE = {w}
3.
q y def
(α) E = JαKE
4.
q y def
[α] E = {ε} ∪ JαKE
5.
q
y def
{α} E = JαK∗E
def
6. Jα1 . . . αn KE = Jα1 KE . . . Jαn KE
def
7. Jα1 | · · · | αn KE = Jα1 KE ∪ · · · ∪ Jαn KE
11
1 Syntax und Semantik
Beispiel 1.2.6 Gegeben sei die EBNF-Definition E mit dem Startsymbol Rna sowie den beiden Regeln
und
Rna = Any [“A”]
Any = (“A” | “C” | “G” | “U”).
Durch wiederholte Anwendung der verschiedenen Gleichungen aus der Semantikdefinition ergibt sich die von dieser EBNF definierte Sprache folgendermaßen:
(1)
JRnaKE
=
(6)
=
(1),(4)
=
(3),(2)
=
(7)
=
=
=
=
q
y
Any [“A”] E
q
y
JAnyKE [“A”] E
q
y
(“A” | “C” | “G” | “U”) E {ε} ∪ J“A”KE
J“A” | “C” | “G” | “U”KE {ε} ∪ {A}
J“A”KE ∪ J“C”KE ∪ J“G”KE ∪ J“U”KE {ε, A}
{A} ∪ {C} ∪ {G} ∪ {U} {ε, A}
{A, C, G, U}{ε, A}
{A, C, G, U, AA, CA, GA, UA}.
Beispiel 1.2.7 Folgende Zeichenkette ist ein syntaktisch korrektes Mini-Java Programm (gemäß der EBNF-Definition aus Beispiel 1.2.2).
class BubbleSort {
public static void main(String[] args) {
int[] array = new int[args.length];
int i = 0;
while (i < args.length) {
array[i] = Integer.parseInt(args[i]);
i = i+1;
}
i = 1;
while (i < array.length) {
int j = array.length - 1;
while (j >= i) {
if (array[j] < array[j-1]) {
int tmp = array[j];
array[j] = array[j-1];
array[j-1] = tmp;
}
j = j-1;
12
1.3 Syntaxdiagramme
}
i = i+1;
}
i = 0;
while (i < array.length) {
System.out.println(array[i]);
i = i+1;
}
}
}
1.3 Syntaxdiagramme
Eine EBNF-Definition kann man folgendermaßen in Syntaxdiagramme überführen:
“w” :
-
V:
w
-
für alle w ∈ A.
-
V
-
für alle V ∈ N .
-
α
[α] :
? -
α {α} :
α1 . . . αn :
α1 | · · · | αn :
?
-
α1
-
-
α1
..
.
- αn
···
-
-
αn
-
6
13
1 Syntax und Semantik
Beispiel 1.3.1 Syntaxdiagramme für Mini-Java
program
-
class
ident
- {n- mainMethod - }n -
mainMethod
- public - static - void - main
- (m- String - [m- ]m- argsIdent - )m- block
-
statement
-expression - ;m
int - ident - =m
- ident - =m- expression - ;m
- if - (m- condition - )m- statement
- while - (m- condition - )m- statement
- block
- System - .m- out - .m- println - (m-expression - )m- ;m
- ;m
- int - [m- ]m-arrayIdent - =m
- new - int - [m-expression - ]m- ;m
-arrayIdent - [m-expression - ]m- =m-expression - ;m
-
block
-
{
6
condition
statement
}
-
- expression
expression
6
14
+
-
6
-
-
term
6
term
+ 6 - -
6
6
6
6
6
6
6
6
- !=
?- expression
- ==
6
- <
6
- <=
6
- >=
6
- >
-
1.3 Syntaxdiagramme
term
-
factor
6
factor
factor
* / -
- ident
6
- number
6
- (n- expression - )n
6
- Integer - .n- parseInt - (n- argsIdent - [n- expression - ]n- )n
6
- argsIdent - .n- length
6
- arrayIdent - .n- length
6
- arrayIdent - [n- expression - ]n
number
ident
- letter
- _
- $
digit
6 6
6
6
6
- 1
6
- ...
6
- 9
argsIdent
- ident
letter
- digit
6
digit
- 0
digit
6
0 letter - A
6
- ...
6
- Z
6
- a
6
- ...
6
- z
arrayIdent
-
- ident
-
15
1 Syntax und Semantik
1.4 Aufgaben
Aufgabe 1.4.1 Der Linguist N OAM C HOMSKY war sehr enttäuscht, als er feststellte, dass auch die von ihm klassifizierte Typ-0 Sprache sich nicht zur Beschreibung der englischen Sprache eignete. Erst recht hat die kontexfreie Grammatik
(Typ-2 Sprache) ihre Grenzen in der Beschreibungsfähigkeit von Sprachen.
(a) Die Sprache (an bn cn ) lässt sich nicht durch eine kontextfreie Grammatik beschreiben. Geben Sie dafür einen plausiblen Grund an (keinen Beweis).
(b) Geben Sie eine kontextfreie Grammatik an, die die Sprache (an bm cn ) beschreibt.
Aufgabe 1.4.2 Palindrome sind Worte, Sätze oder Verse, die von rechts nach
links gelesen gleich lauten wie von links nach rechts1 (abgeleitet vom griechischen palindromos für „wieder zurücklaufend“).
Geben Sie eine EBNF-Definition an, welche die Sprache der Palindrome über dem
Alphabet A = {a, b, c} beschreibt.
Aufgabe 1.4.3 Ribosomen übernehmen in biologischen Zellen eine wichtige Aufgabe bei der Proteinbiosynthese: Sie setzen aus einzelnen Aminosäuren ein bestimmtes Protein zusammen. Der Syntheseweg wird durch eine Nukleotidensequenz bestimmt, der mRNA. Jeweils drei Nukleotide werden zu einem sogenannten Triplett zusammengefasst; fast alle der möglichen Tripletts (Anzahl 43 = 64)
codieren für Proteine, diese Tripletts werden Codons genannt.
Im folgenden soll ein Mini-Ribosom besprochen werden. Unser Ribosom verarbeitet als mRNA eine Sequenz über dem Alphabet {A, C, G, U } zu den Proteinen
Asparagin, Glutamin, Arginin und Lysin. Das Ribosom erkennt eine korrekte mRNA dann, wenn sie aus einem Codon besteht, welches links von dem Startcodon
Methionin und rechts von einem Stopcodon flankiert wird. Das Ribosom basiert
auf der Grammatik G = (N, A, P, Protein), deren Komponenten unten definiert
sind.
(a) Erstellen Sie die zur Grammatik gehörende EBNF-Definition und die entsprechenden Syntaxdiagramme.
(b) Bestimmen Sie die von der EBNF-Definition erzeugte Sprache (die Semantik
der EBNF-Definition).
(c) In welchem gravierenden, unrealistischen Punkt weicht die Grammatik von
der biologischen Realität ab?
1
16
Radar
Madam I’m Adam
Roma tibi subito motibus ibit amor
Ein Neger mit Gazelle zagt im Regen nie
Able was I ere I saw Elba
1.4 Aufgaben
N = {Protein, Aminosäure, Met, Glu, Asp, Arg, Lys, Start, Stop}
A = {A, C, G, U}
P = { Protein
→ Start Aminosäure Stop
Start
→ Met
Aminosäure → Asp
Aminosäure → Glu
Aminosäure → Lys
Aminosäure → Arg
Stop
→ UAA
Met
→ AUG
Glu
→ GAA
Glu
→ GAG
Asp
→ GAC
Asp
→ GAU
Arg
→ CGC
Arg
→ CGG
Arg
→ CGA
Arg
→ CGU
Arg
→ AGA
Arg
→ AGG
Lys
→ AAG
Lys
→ AAA
Stop
→ U A A}
17
1 Syntax und Semantik
18
2 Java: Der Einstieg
Im vorigen Kapitel haben wir die Syntax der Sprache Mini-Java kennengelernt. Bevor wir in Kapitel 3 ausführlich auf Syntax und Semantik der Programmiersprache Java eingehen werden, soll dieses
Kapitel zunächst einige grundlegende Vorbemerkungen zu Java machen, sowie zu den beiden zugrundeliegenden Sprachkonzepten, der
imperativen und der objektorientierten Programmierung.
2.1 Grundlegendes zu Java
2.1.1 Historisches
• 1990-1991: Entwicklung der Programmiersprache OAK durch James Gosling von Sun Microsystems (zunächst für Toaster, Mikrowellen etc.; unabhängig vom Chip, extrem zuverlässig)
• Umbenennung in Java
• 1995: α und β Release
• von IBM, SGI, Oracle und Microsoft lizensiert
2.1.2 Eigenschaften von Java
• durch den Bytecode (Zwischensprachencode) unabhängig von der Plattform
• Syntax an C und C++ angelehnt
• objektorientiert
• streng typisiert
• unterstützt parallele Abläufe (Nebenläufigkeit / Threads)
• Graphical User Interface (GUI)
• netzwerkfähig
19
2 Java: Der Einstieg
• modularer Aufbau
• Nachteil: Effizienz leidet (ca. 5–10mal langsamer als C und C++)
Selbstständig
laufende
Anwendung
Quellprogramm
Java-Compiler
?
Java-Bytecode
Java-Interpreter
?
Ablauf des Programms
Applet
Quellprogramm
Java-Compiler
?
Java-Bytecode auf dem Server
Übertragung per Internet
?
Bytecodes auf dem Rechner
des Benutzers (Client)
Java-Interpreter im Browser
oder Applet-Viewer
?
Ablauf des Programms
A BBILDUNG 2.1: Vom Quellprogramm zum Ablauf
Abbildung 2.1 zeigt die notwendigen Schritte, um ein Java-Programm (selbstständig laufende Anwendung) bzw. ein Applet ablaufen zu lassen. Ein Applet
ist eine in ein HTML-Dokument eingebettete Anwendung, die auf dem Rechner
des Anwenders abläuft. Wir beschränken uns in diesem Skript auf selbstständig
laufende Anwendungen.
20
2.1 Grundlegendes zu Java
2.1.3 Sicherheit
• keine Pointerarithmetik wie in C
• Garbage Collection1
• Überprüfungen zur Laufzeit (Datentypen, Indizes, etc.) durch Mechanismen zur Verifizierung von Java-Bytecode bei der Übertragung
• dennoch ist die (Netz-)Sicherheit umstritten
2.1.4 Erstellen eines Java-Programms
1. Quellprogramm erstellen:
class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Dieses Programm besteht aus der Klasse Hello. Der Klassenname muss
mit dem Dateinamen übereinstimmen, d.h. das oben gezeigte Programm
muss in der Datei Hello.java abgelegt sein. Die Klasse Hello enthält eine Methode namens main. Ein Java-Programm muss eine main-Methode
enthalten, denn die main-Methode wird bei der Interpretation aufgerufen.
2. Übersetzen eines Programms:
> javac Hello.java
liefert eine Datei Hello.class, die das Programm in Bytecode enthält.
3. Interpretation des Bytecodes:
> java Hello
1
Ein Garbage Collector entfernt automatisch Objekte, Felder und Variablen, auf die keine Referenz mehr vorhanden ist, aus dem Speicher (siehe Arnold & Gosling [1], S. 12, Kapitel 1.6).
21
2 Java: Der Einstieg
2.2 Grundzüge imperativer Sprachen
2.2.1 Das Behältermodell der Variablen
Imperative Programmierung geht aus vom Modell eines Speichers, aufgegliedert
in einzelne Variablen, in denen Werte abgelegt werden können. Der Speicher
bzw. die Variablen werden verändert durch Befehle bzw. Anweisungen, die selbst
vom aktuellen Speicherinhalt abhängen. Ein typisches Beispiel ist die Anweisung
x = y + z.
Sie bedeutet: Addiere die Variableninhalte von y und z und lege die Summe in
der Variablen x ab. Während diese Semantikbeschreibung nun suggeriert, dass
der einzige Effekt der Anweisung eine Änderung des Variableninhalts von x ist,
ist in der Tat diese Vorstellung zu einfach:
• nicht nur der Variableninhalt von x kann sich ändern, sondern auch der von
y und z sowie aller möglicher anderer Variablen;
• falls x ≡ y oder y ≡ z, dann ist dies unmittelbar einsichtig.
Also ist “x = y + z” keine Gleichheit, die zwischen den Werten (Inhalten) von
x, y und z gilt, und mittels der wir über Programme nachdenken und Beweise
führen können. Das Prinzip der „referential transparency“ (Werttreue), das in
der funktionalen Programmierung gilt, ist in der imperativen verletzt.
2.2.2 Konsequenzen
1. Der Nachweis von Programmeigenschaften wird viel schwieriger, ebenso
das Verstehen von Programmen.
2. Die Semantik eines Programms hängt von einem strikten Nacheinander der
Ausführung der einzelnen Anweisungen ab.
3. Wiederverwendung von Programmteilen in anderem Kontext bedarf besonderer Vorsicht.
2.3 Klassen, Objekte und Methoden im Überblick
Java-Programme werden aus Klassen aufgebaut. Aus einer Klassendefinition lassen sich beliebig viele Objekte erzeugen, die auch Instanzen genannt werden
(vgl. Arnold & Gosling [1], Kapitel 1.6 und 1.7).
22
2.3 Klassen, Objekte und Methoden im Überblick
Eine Klasse enthält folgende Bestandteile:
• Objektvariablen (objektbezogene Datenfelder)
• objektbezogene Methoden
• Klassenvariablen (klassenbezogene Datenfelder)
• klassenbezogene Methoden
Datenfelder (Synonym: Attribute) enthalten den Zustand des Objektes oder der
Klasse. Methoden sind Sammlungen von imperativ formulierten Anweisungen,
die auf den Datenfeldern operieren, um deren Zustand zu ändern.
2.3.1 Klassen
Beispiel der Deklaration einer einfachen Klasse:
class Point {
double x, y;
}
In der Klasse Point sind zwei Objektvariablen deklariert, die die x- und y-Koordinate
eines Punktes repräsentieren. double bedeutet, dass die Variablen vom Typ double2
sind. Bis jetzt gibt es noch kein Objekt vom Typ Point, nur einen “Plan”, wie ein
solches Objekt aussehen wird.
2.3.2 Das Erzeugen von Objekten
Objekte werden mit dem Schlüsselwort new erzeugt. Neu geschaffene Objekte
bekommen innerhalb eines Bereiches des Speichers (welcher Heap genannt wird)
einen Speicherplatz zugewiesen und werden dort abgelegt. Auf alle Objekte in
Java wird über Objektreferenzen zugegriffen – jede Variable, die ein Objekt zu
enthalten scheint, enthält tatsächlich eine Referenz auf dieses Objekt (bzw. auf
deren Speicherplatz). Objektreferenzen haben den Wert null, wenn sie sich auf
kein Objekt beziehen. Wir werden im folgenden Objekte und Objektreferenzen
synonym verwenden, es sei denn, die Unterscheidung ist wichtig.
Erzeugung und Initialisierung
Point lowerLeft = new Point();
Point upperRight = new Point();
2
Gleitkommazahlen
23
2 Java: Der Einstieg
Wertzuweisung
lowerLeft.x = 0.0;
lowerLeft.y = 0.0;
upperRight.x = 1280.0;
upperRight.y = 1024.0;
2.3.3 Klassenvariablen
class Point {
double x, y;
static Point origin = new Point();
}
Wird eine Variable als static deklariert, so handelt es sich um eine Klassenvariable. Durch obige Deklaration gibt es genau eine Klassenvariable names Point.origin,
egal ob und wie viele Point-Objekte erzeugt werden. Point.origin hat den
Wert (0,0), weil dies die Voreinstellung für numerische Datenfelder ist, die nicht
explizit auf einen anderen Wert initialisiert werden (das wird später noch genauer
besprochen). Allerdings kann der Wert von Point.origin geändert werden. Ist
dies nicht erwünscht, soll Point.origin also eine Konstante sein, so muss man
den Modifizierer final benutzen.
static final Point origin = new Point();
2.3.4 Methoden
Eine Methode ist eine Funktion bzw. Prozedur. Sie kann parameterlos sein oder
Parameter haben. Sie kann einen Rückgabewert liefern oder als void deklariert
sein, wenn sie keinen Wert zurückliefert. Methoden dürfen nicht geschachtelt
werden. Innerhalb von Methoden dürfen lokale Variablen deklariert werden.
class Point {
double x, y;
void clear() {
x = 0.0;
y = 0.0;
}
}
Um eine Methode aufzurufen, gibt man ein Objekt und den Methodennamen,
getrennt durch einen Punkt, an.
24
2.4 Vererbung, Pakete und Gültigkeitsbereiche
lowerLeft.clear();
upperRight.clear();
Nun definieren wir eine Methode, die die Distanz zwischen dem Punkt, auf den
sie angewendet wird und einem übergebenen Punkt p zurückgibt. (Math.sqrt()
ist vordefiniert und liefert die Wurzel einer Zahl.)
double distance(Point p) {
double xdiff, ydiff; // Beispiel fuer lokale Variablen
xdiff = x - p.x;
ydiff = y - p.y;
return Math.sqrt(xdiff*xdiff + ydiff*ydiff);
}
Aufruf: double d = lowerLeft.distance(upperRight);
2.3.5 Klassenbezogene Methoden
Klassenbezogene Methoden werden durch das Schlüsselwort static deklariert,
z.B. ist Math.sqrt() eine Klassenmethode der vordefinierten Klasse Math.
“distance” als Klassenmethode
static double distance(Point p1, Point p2) {
double xdiff = p1.x - p2.x;
double ydiff = p1.y - p2.y;
return Math.sqrt(xdiff*xdiff + ydiff*ydiff);
}
Aufruf: double d = Point.distance(lowerLeft, upperRight);
In der Klasse Point selbst kann man Point. weglassen, d.h. es reicht ein Aufruf
der Form: double d = distance(lowerLeft, upperRight);
2.4 Vererbung, Pakete und Gültigkeitsbereiche
2.4.1 Vererbung
Klassen in Java können um zusätzliche Variablen und Methoden erweitert werden. Dies wird durch das Schlüsselwort extends angezeigt. Die entstehende Unterklasse besitzt dann alle Eigenschaften der Oberklasse und zusätzlich die in der
jeweiligen Erweiterung angegebenen Eigenschaften. Dieses Konzept wird auch
als Vererbung bezeichnet, weil die Unterklasse alle Eigenschaften der Oberklasse
erbt.
25
2 Java: Der Einstieg
Zum Beispiel ist ein farbiger Punkt eine Erweiterung eines Punktes:
class ColoredPoint extends Point {
String color;
}
2.4.2 Pakete
Bei größeren Softwareprojekten ist es häufig ratsam, diese in verschiedene, unabhängige Teile aufzuteilen. Solche Teile werden als Module oder Pakete bezeichnet. Java besitzt einige Eigenschaften, die es erlauben, Software modular
aufzubauen: Verschiedene (i.d.R. logisch zusammengehörige) Klassen können in
einem Paket zusammengefasst werden. Die Klassendefinitionen können in verschiedenen Dateien enthalten sein.
Der Paketname muss im Header jeder Datei angegeben sein:
A.java
package abc;
public class A {
...
}
C.java
package abc;
class C {
...
}
class B {
...
}
Um Definitionen aus anderen Paketen sichtbar zu machen, müssen sie mit dem
Schlüsselwort import importiert werden:
import abc.A;
class test {
A a;
}
Weitere Details zu Paketen folgen in Kapitel 7.2.
2.4.3 Gültigkeitsbereiche
Bei dem Zugriff auf Definitionen anderer Klassen oder Pakete gelten Beschränkungen, die beachtet werden müssen. Die Zugriffsrechte werden durch Gültigkeitsmodifizierer geregelt, die sich folgendermaßen auf Datenfelder, Methoden
und Klassen auswirken (für Klassen gibt es nur public und default):
26
2.5 Aufgaben
zugreifbar für
Nicht-Unterklassen im
selben Paket
zugreifbar für
Unterklassen im selben
Paket
zugreifbar für
Nicht-Unterklassen in
einem anderen Paket
zugreifbar für
Unterklassen in einem
anderen Paket
public
ja
default (package)
ja
protected
ja
private
nein
ja
ja
ja
nein
ja
nein
nein
nein
ja
nein
ja
nein
2.5 Aufgaben
Aufgabe 2.5.1 Erstellen Sie eine Klasse Circle, deren Instanzen Kreise im zweidimensionalen Raum repräsentieren. Ein Kreis ist z.B. bestimmt durch seinen Radius r und durch die x- und y-Koordinate seines Mittelpunktes in der Ebene.
Implementieren Sie für diese Klasse folgende Methoden:
(i) circumference berechnet den Umfang des Kreises.
(ii) area berechnet den Inhalt des Kreises.
Wie können die Methoden in einem Programm aufgerufen werden?
27
2 Java: Der Einstieg
28
3 Imperative Programmierung in Java
Im vorigen Kapitel haben wir generelle Eigenschaften der imperativen wie der objektorientierten Programmierung kennengelernt. Auch
Teile der Syntax der objektorientierten Anteile von Java wurden vorgestellt.
Dieses Kapitel wird sich nun mit der Syntax und Semantik der imperativen Anteile von Java beschäftigen, d.h. im wesentlichen mit
den Rümpfen von Methoden. Weder Syntax noch Semantik werden
allerdings formal eingeführt, wie wir es in Kapitel 1 kennengelernt
haben, sondern im wesentlichen anhand von Beispielen. Zunächst
wird die Semantik von Mini-Java erläutert, dessen Syntax wir bereits in Kapitel 1 definiert haben. Danach werden die wichtigsten der
noch fehlenden Java-Konstrukte vorgestellt. Eine komplette Syntaxbeschreibung von Java findet sich auf der folgenden SUN-Webseite:
http://java.sun.com/docs/books/jls/second_edition/html/syntax.doc.html
3.1 Mini-Java
Ein Mini-Java Programm besteht aus genau einer Klasse. In dieser Klasse gibt es
genau eine main-Methode. Folgende Konstrukte sind Anweisungen (statements
gemäß Mini-Java-Syntax, vgl. Beispiel 1.2.2):
1. Die Deklaration einer Variablen vom Typ int mit sofortiger Initialisierung:
int ident = expression;
Jeder Bezeichner (ident) darf in höchstens einer Variablendeklaration vorkommen. Diese kontextsensitive Bedingung lässt sich nicht in der EBNFDefinition formulieren.
2. Die Zuweisung eines Wertes an eine Variable:
ident = expression;
Diese Variable muss vorher deklariert worden sein und den gleichen Typ
wie der Ausdruck haben. Diese Nebenbedingung ist ebenfalls nicht in der
EBNF-Definition ausgedrückt.
29
3 Imperative Programmierung in Java
3. Eine bedingte Anweisung (if-then Anweisung):
if(condition) statement
Der Bool’sche Ausdruck (condition) wird ausgewertet; ist er true, so wird
die Anweisung (statement) ausgeführt. Ist er false, so wird die Anweisung
nicht ausgeführt und die Programmausführung mit der nächsten Anweisung hinter der if-then Anweisung fortgesetzt.
4. Eine abweisende Schleife (while-Schleife):
while(condition) statement
Der Bool’sche Ausdruck wird ausgewertet; ist er true, so wird die Anweisung so lange ausgeführt, bis der Bool’sche Ausdruck false wird.
5. Ein Block.
{ statement1; statement2; . . . }
Die Statements in der geschweiften Klammer werden von links nach rechts
nacheinander abgearbeitet.
6. Eine Anweisung zum Schreiben auf der Standardausgabe:
System.out.println(...);
System ist eine Klasse, die klassenbezogene Methoden zur Darstellung des
Zustandes des Systems bereitstellt. out ist eine Klassenvariable der Klasse System, ihr Inhalt ist der Standardausgabestrom. Die Methode println
wird also auf das klassenbezogene Datenfeld out angewendet – es wird
ein String mit abschließendem Zeilenvorschub auf dem Standardausgabestrom ausgegeben.
7. Die leere Anweisung.
;
Es geschieht nichts.
8. Die Deklaration eines eindimensionalen Feldes (Arrays) mit sofortiger Initialisierung:
int[] array = new int[3];
deklariert ein Feld namens array, erzeugt ein Feld mit drei int-Komponenten
und weist dieses der Feldvariablen array zu. Beachte, dass die Dimension
(3) einer Feldvariablen nicht bei der Deklaration (int[] array) angegeben
wird, sondern nur bei der Erzeugung (new int[3]). Die erste Komponente eines Feldes hat den Index 0. Die Länge des Feldes kann aus dessen
Datenfeld length ausgelesen werden (array.length).
30
3.1 Mini-Java
9. Die Zuweisung eines Wertes an die i-te Komponente eines Feldes, wobei
0 ≤ i ≤ array.length−1:
array[i] = expression;
Alle weiteren Konstrukte haben eine offensichtliche Bedeutung, bis auf
Integer.parseInt(argsIdent[expression])
Betrachten wir hierfür noch einmal die main-Methode: Sie hat als Parameter ein
Feld von Zeichenketten. Diese Zeichenketten sind die Programmargumente und
werden normalerweise vom Anwender beim Programmaufruf eingegeben.
class Echo {
public static void main(String[] args) {
int i = 0;
while(i < args.length) {
System.out.println(args[i]);
i = i+1;
}
}
}
> java Echo 10 2
10
2
>
Um eine Zeichenkette in eine ganze Zahl zu konvertieren, wird die Klassenmethode parseInt der Klasse Integer mit dieser Zeichenkette als Argument aufgerufen. Sie liefert die entsprechende ganze Zahl als Ergebnis zurück bzw. meldet
einen Fehler, falls die Zeichenkette keine ganze Zahl dargestellt hat.
class BadAddOne {
public static void main(String[] args) {
int i = 0;
while(i < args.length) {
int wert = args[i];
wert = wert+1;
System.out.println(wert);
i = i+1;
}
}
}
31
3 Imperative Programmierung in Java
> javac BadAddOne.java
BadAddOne.java:6: Incompatible type for declaration.
Can’t convert java.lang.String to int.
int wert = args[i];
>
Stattdessen muss eine explizite Typkonvertierung stattfinden:
class AddOne {
public static void main(String[] args) {
int i = 0;
while(i < args.length) {
int wert = Integer.parseInt(args[i]);
wert = wert+1;
System.out.println(wert);
i = i+1;
}
}
}
> java AddOne 6 3 20
7
4
21
>
3.2 Von Mini-Java zu Java
Jedes Mini-Java Programm ist ein Java Programm. In diesem Abschnitt werden
die Datentypen und imperativen Konstrukte von Java erläutert, die nicht bereits
in Mini-Java vorhanden sind.
3.2.1 Elementare Datentypen
Unicode: Java, als Sprache für das World Wide Web, benutzt einen 16-Bit Zeichensatz, genannt Unicode. Die ersten 256 Zeichen von Unicode sind identisch mit dem 8-Bit Zeichensatz Latin-1, wobei wiederum die ersten 128
Zeichen von Latin-1 mit dem 7-Bit ASCII Zeichensatz übereinstimmen.
32
3.2 Von Mini-Java zu Java
Elementare Datentypen und deren Literale:
Typ boolean true und false
Typ int
29 (Dezimalzahl) oder
035 (Oktaldarstellung wegen führender 0) oder
0x1D (Hexadezimaldarstellung wegen führendem 0x) oder
0X1d (Hexadezimaldarstellung wegen führendem 0X)
Typ long
29L (wegen angehängtem l oder L)
Typ short
short i = 29; (Zuweisung, es gibt kein short-Literal)
Typ byte
byte i = 29; (Zuweisung, es gibt kein byte-Literal)
Typ double
18.0 oder 18. oder 1.8e1 oder .18E2
Typ float
18.0f (wegen angehängtem f oder F)
Typ char
’Q’, ’\u0022’, ’\u0b87’
Typ String
"Hallo" (String ist kein elementarer Datentyp; s. Kapitel 4)
Initialbelegungen: Während ihrer Deklaration kann eine Variable wie in MiniJava initialisiert werden.
final double PI = 3.141592654;
float radius = 1.0f;
Sind für Datenfelder einer Klasse keine Anfangswerte angegeben, so belegt
Java sie mit voreingestellten Anfangswerten. Der Anfangswert hängt vom
Typ des Datenfeldes ab:
Feld-Typ
Anfangswert
boolean
false
char
’\u0000’
Ganzzahl (byte, short, int, long) 0
Gleitkommazahl
+0.0f oder +0.0d
andere Referenzen
null
Lokale Variablen in einer Methode (oder einem Konstruktor oder einem
klassenbezogenen Initialisierungsblock) werden von Java nicht mit einem
Anfangswert initialisiert. Vor ihrer ersten Benutzung muss einer lokalen Variablen ein Wert zugewiesen werden (ein fehlender Anfangswert ist ein
Fehler).
3.2.2 Kommentare
// Kommentar bis zum Ende der Zeile
/* Kommentar
zwischen */
33
3 Imperative Programmierung in Java
Achtung: /* */ können nicht geschachtelt werden!
/* falsch
/* geschachtelter Kommentar */
*/
3.2.3 Bool’sche Operatoren
&& logisches und
| | logisches oder
! logisches nicht
Die Auswertung eines Bool’schen Ausdrucks erfolgt von links nach rechts, bis der
Wert eindeutig feststeht. Folgender Ausdruck ist deshalb robust:
if(index>=0 && index<array.length && array[index]!=0) ...
3.2.4 Bitoperatoren
Die Bitoperatoren & (und) und | (oder) sind definiert durch:
& 0 1
0 0 0
1 0 1
| 0 1
0 0 1
1 1 1
int-Zahlen werden durch diese Operatoren bitweise behandelt.
Beispiel 3.2.1 Es seien x und y folgendermaßen gewählt: x = 60 (in Binärdarstellung 00111100) und y = 15 (binär: 00001111). In diesem Fall ist x&y = 12 und
x|y = 63:
x & y
00111100 (60)
& 00001111 (15)
00001100 (12)
x | y
00111100 (60)
| 00001111 (15)
00111111 (63)
Wenn man 0 als false und 1 als true interpretiert, so entspricht & dem logischen
und (&&) und | dem logischen oder (||).
34
3.2 Von Mini-Java zu Java
3.2.5 Inkrement und Dekrement
Man kann den Wert einer Variablen x (nicht den eines Ausdrucks) durch den
Operator ++ um 1 erhöhen bzw. durch -- um 1 erniedrigen. Es gibt Präfixund Postfixschreibweisen, die unterschiedliche Wirkungen haben: Bei der Präfixschreibweise wird der Wert zuerst modifiziert und danach der veränderte Wert
zurückgeliefert. Bei der Postfixschreibweise wird zuerst der Wert der Variablen
zurückgeliefert, dann wird sie modifiziert.
int i = 10;
int j = i++;
System.out.println(j);
int i = 10;
int j = ++i;
System.out.println(j);
>
10
>
11
Der Ausdruck i++ ist gleichbedeutend mit i = i+1, jedoch wird i nur einmal
ausgewertet!
Beispiel 3.2.2
(A)
arr[where()]++;
Die Methode where() wird einmal aufgerufen.
(B)
arr[where()] = arr[where()]+1;
Hierbei wird die Methode where() jedoch zweimal aufgerufen.
Seiteneffekte können hier sogar das Ergebnis beeinflussen: In dem Kontext arr[0]
= 0; arr[1] = 1; arr[2] = 2; und
private static int zaehler = 0;
private static int where() {
zaehler = zaehler+1;
return zaehler;
}
liefert (A) arr[1] = 2 bzw. (B) arr[1] = 3.
3.2.6 Zuweisungsoperatoren
i += 2; ist gleichbedeutend mit i = i+2; außer, dass der Ausdruck auf der linken Seite von i += 2; nur einmal ausgewertet wird (vgl. Inkrement und Dekrement).
Entsprechend sind -=, &= und |= definiert.
35
3 Imperative Programmierung in Java
3.2.7 Die nichtabweisende Schleife
Zusätzlich zur abweisenden Schleife gibt es eine nichtabweisende Schleife in Java:
do
statement
while(condition);
Die condition wird erst nach der Ausführung von statement ausgewertet. Solange sie true ist, wird statement wiederholt.
3.2.8 for-Schleife
for(init-statement; condition; increment-statement)
statement
ist gleichbedeutend mit (mit Ausnahme vom Verhalten bei continue):
{
init-statement
while(condition) {
statement
increment-statement
}
}
Übliche Verwendung der for-Schleife:
for(int i=0; i<=10; i++) {
System.out.println(i);
}
Der Gültigkeitsbereich der (Lauf-)Variablen i beschränkt sich auf die for-Schleife!
int i = 0;
for(int i=0; i<=10; i++) {
System.out.println(i);
}
ist jedoch nicht möglich, da die Variable i vorher schon deklariert wurde.
Die Initialisierungs- bzw. Inkrementanweisung einer for-Schleife kann eine durch
Kommata getrennte Liste von Ausdrücken sein. Diese werden von links nach
rechts ausgewertet.
36
3.2 Von Mini-Java zu Java
Beispiel 3.2.3 (Arnold & Gosling [1], S. 144)
public static int zehnerPotenz(int wert) {
int exp, v;
for(exp=0,v=wert; v>0; exp++, v=v/10)
; // leere Anweisung
return exp;
}
Alle Ausdrücke dürfen auch leer sein; dies ergibt eine Endlosschleife:
for(;;) {
System.out.println("Hallo");
}
3.2.9 if-then-else Anweisung
if(condition)
statement1
else
statement2
Die condition wird ausgewertet; ist sie true, so wird statement1 ausgeführt. Ist
sie false, so wird statement2 ausgeführt. Der else-Zweig darf entfallen; dies
ergibt dann die if-then Anweisung aus Mini-Java. Ein else bezieht sich immer
auf das letzte if, das ohne zugehöriges else im Programm vorkam. Was passiert,
wenn mehr als ein if ohne ein else vorangeht? Das folgende Beispiel zeigt eine
falsche (d.h. nicht intendierte) und eine richtige Verwendung (Schachtelung) von
if-then-else Anweisungen.
Beispiel 3.2.4 (Arnold & Gosling [1], S. 139)
public double positiveSumme(double[] werte) {
double sum = 0.0;
if(werte.length > 1)
for(int i=0; i<werte.length; i++)
if(werte[i] > 0)
sum += werte[i];
else // hoppla!
sum = werte[0];
return sum;
}
37
3 Imperative Programmierung in Java
Der else-Teil sieht so aus, als ob er an die Feldlängenprüfung gebunden wäre,
aber das ist eine durch die Einrückung erweckte Illusion, und Java ignoriert Einrückungen. Statt dessen ist ein else-Teil an das letzte if gebunden, das keinen
else-Teil hat. So ist der vorangehende Block äquivalent zu:
public double positiveSumme(double[] werte) {
double sum = 0.0;
if(werte.length > 1)
for(int i=0; i<werte.length; i++)
if(werte[i] > 0)
sum += werte[i];
else // hoppla!
sum = werte[0];
return sum;
}
Das war vielleicht nicht beabsichtigt. Um den else-Teil an das erste if zu binden,
müssen Klammern zur Erzeugung eines Blocks verwendet werden:
public double positiveSumme(double[] werte) {
double sum = 0.0;
if(werte.length > 1) {
for(int i=0; i<werte.length; i++)
if(werte[i] > 0)
sum += werte[i];
}
else {
sum = werte[0];
}
return sum;
}
38
3.2 Von Mini-Java zu Java
3.2.10 Mehrdimensionale Felder
Mehrdimensionale Felder werden in Java durch Felder von Feldern realisiert.
Beispiel 3.2.5 (Jobst [6], S. 37)
public class Array2Dim {
public static void main(String[] args) {
int[][] feld = new int[3][3];
//Weise feld[i][j] den Wert (i+1)*10+j zu
for(int i=0; i<feld.length; i++) {
for(int j=0; j<feld[i].length; j++) {
feld[i][j] = (i+1)*10+j;
System.out.print(feld[i][j]+" ");
}
System.out.println();
}
}
}
> java Array2Dim
10 11 12
20 21 22
30 31 32
Da Felder in Java dynamisch sind, kann bei mehrdimensionalen Feldern jedes
verschachtelte Feld eine andere Größe aufweisen.
Beispiel 3.2.6 (Jobst [6], S. 38)
public class DemoArray {
public static void main(String[] args) {
int[][] feld = new int[3][];
for(int i=0; i<feld.length; i++) {
feld[i] = new int[i+1];
for(int j=0; j<feld[i].length; j++) {
feld[i][j] = (i+1)*10+j;
System.out.print(feld[i][j]+" ");
}
39
3 Imperative Programmierung in Java
System.out.println();
}
}
}
> java DemoArray
10
20 21
30 31 32
Felder können bei ihrer Deklaration sofort initialisiert werden:
Beispiel 3.2.7 (Jobst [6] S. 39)
public class DemoFeldInitial {
public static void main(String[] args) {
int[][] feld = {{1,2,3},{4,5},{7,8,9,10}};
//Ausgabe des Feldes
for(int i=0; i<feld.length; i++) {
for(int j=0; j<feld[i].length; j++)
System.out.print(feld[i][j]+" ");
System.out.println();
}
}
}
>
1
4
7
java DemoFeldInitial
2 3
5
8 9 10
3.2.11 switch-Anweisung
switch(expression) {
case const1: statement1 break;
case const2: statement2 break;
...
default: statement
}
40
3.2 Von Mini-Java zu Java
Der Ausdruck (expression) muss ganzzahlig sein. Nach case müssen Konstanten stehen, die bei der Übersetzung des Programms berechnet werden können.
Der Ausdruck wird berechnet. Danach wird das Programm an derjenigen caseAnweisung fortgesetzt, deren Konstante dem Wert des Ausdrucks entspricht. Mit
break kann man switch verlassen. Nach case darf nur jeweils eine Konstante
stehen. Wenn es keine passende Konstante gibt, so wird das Programm bei der
default Anweisung fortgesetzt (falls vorhanden).
Achtung: Das nächste case erzwingt nicht das Verlassen der switch-Anweisung.
Es impliziert auch nicht das Ende der Anweisungsausführung.
Beispiel 3.2.8 (Jobst [6], S. 15)
public class DemoFuerSwitch {
public static void main (String[] args) {
for(int i=0; i<=10; i++)
switch(i) {
case 1:
case 2:
System.out.println(i+" Fall 1,2");
case 3:
System.out.println(i+" Fall 3");
case 7:
System.out.println(i+" Fall 7");
break;
default:
System.out.println(i+" sonst");
}
}
}
>
0
1
1
1
2
2
2
3
3
4
5
6
// Weiter bei Fall 3
// Weiter bei Fall 7
java DemoFuerSwitch
sonst
Fall 1,2
Fall 3
Fall 7
Fall 1,2
Fall 3
Fall 7
Fall 3
Fall 7
sonst
sonst
sonst
41
3 Imperative Programmierung in Java
7 Fall 7
8 sonst
9 sonst
10 sonst
>
3.3 Aufgaben
Aufgabe 3.3.1 Schreiben Sie ein Programm Primes in Mini-Java, das eine Zahl
als Argument erhält und überprüft, ob diese Zahl eine Primzahl ist. Wenn sie eine
ist, dann soll die Zahl wieder ausgegeben werden; wenn nicht, dann sollen die
Zahlen, durch die sie teilbar ist, ausgegeben werden.
Aufgabe 3.3.2 Implementieren Sie eine Klasse Factorial, in der die Fakultät einer Zahl durch die Methoden fwhile und ffor berechnet werden kann. Dabei
soll die Methode fwhile die Fakultät einer Zahl mit einer while-Schleife berechnen, während ffor dazu eine for-Schleife benutzt.
Aufgabe 3.3.3 Schreiben Sie ein Programm, das das Pascalsche Dreieck bis zu einer Tiefe von zwölf berechnet, dabei jede Reihe des Dreiecks in einem Array mit
entsprechender Länge speichert und alle zwölf Arrays in einem Array von intArrays hält. Entwerfen Sie Ihre Lösung so, dass die Ergebnisse durch eine Methode ausgegeben werden, die die Länge der einzelnen Arrays des Hauptarrays
berücksichtigt und nicht von einer konstanten Länge 12 ausgeht. Anschließend
sollten Sie Ihren Code bezüglich der Konstante 12 modifizieren können, ohne
dabei die Ausgabemethode ändern zu müssen.
Aufgabe 3.3.4 Schreiben Sie unter Verwendung von if/else bzw. unter Verwendung von switch je eine Methode, die einen Stringparameter erhält und
einen String zurückliefert, in dem alle Sonderzeichen des Ursprungsstrings durch
ihre Java-Äquivalente ersetzt wurden. Ein String, der beispielsweise ein Anführungszeichen (") enthält, sollte einen String zurückliefern, in dem das " durch \"
ersetzt worden ist. Bitte ersetzen Sie alle Zeichen, die in der folgenden Tabelle
aufgeführt werden durch ihre Äquivalente:
Sonderzeichen Java-Äquivalent
"
\"
´
\´
\
\\
42
4 Algorithmen zur exakten Suche in
Texten
In diesem Kapitel wird ein Problem aus der Sequenzanalyse näher
betrachtet, die exakte Textsuche: Gegeben ein Text und ein Muster,
finde alle Vorkommen von dem Muster in dem Text. Man wird sehen, dass zur Lösung dieses einfachen Problem einige nicht-triviale,
aber sehr effiziente Algorithmen entwickelt wurden. Zunächst soll
jedoch ein kurzer Blick auf die Art und Weise geworfen werden, wie
Sequenzen üblicherweise in Java repräsentiert sind.
4.1 Die Klasse String
Zeichenketten sind in Java Objekte. Sie können mit dem +-Operator zu neuen
Objekten verknüpft werden.
String str1 = "hello";
String str2 = " world";
String str3 = str1 + str2;
Grundlegende objektbezogene Methoden der Klasse String:
public int length()
liefert die Anzahl der Zeichen in der Zeichenkette.
public int charAt(int index)
liefert das Zeichen an der Position index der Zeichenkette.
public int indexOf(char ch, int fromIndex)
liefert die erste Position ≥ fromIndex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. −1, falls das Zeichen nicht vorkommt.
public int lastIndexOf(char ch, int fromIndex)
liefert die letzte Position ≤ fromIndex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. −1, falls das Zeichen nicht vorkommt (lastIndexOf
liest also im Gegensatz zu indexOf die Zeichenkette von rechts nach links).
43
4 Algorithmen zur exakten Suche in Texten
public boolean startsWith(String prefix)
liefert true gdw. die Zeichenkette mit dem angegebenen prefix beginnt.
public boolean endsWith(String suffix)
liefert true gdw. die Zeichenkette mit dem angegebenen suffix endet.
public String substring(int beginIndex, int endIndex)
liefert das Teilwort der Zeichenkette zwischen den Positionen beginIndex
und endIndex−1.
public char[] toCharArray()
wandelt die Zeichenkette in ein Feld von Zeichen um.
public boolean regionMatches (int start, String other, int ostart,
int len)
liefert true gdw. der Bereich der Zeichenkette mit dem Bereich der Zeichenkette String other übereinstimmt (s.u.). Der Vergleich beginnt bei der
Position start bzw. ostart. Es werden nur die ersten len Zeichen verglichen.
public boolean equals(Object anObject)
liefert true gdw. die übergebene Objektreferenz auf ein Objekt vom Typ
String mit demselben Inhalt zeigt. (Dies steht im Gegensatz zu ==, was die
Objekt-(referenz-)gleichheit testet.)
Man beachte den Rückgabetyp int von charAt bzw. den Parametertyp int von
ch in indexOf und lastIndexOf. Dennoch können die Methoden auf (Unicode-)
Zeichen angewendet werden: Ein char-Wert in einem Ausdruck wird automatisch in einen int-Wert umgewandelt, wobei die oberen 16 Bit auf Null gesetzt
werden.
Beispiel 4.1.1
public class DemoStringClass {
public static void main(String[] args) {
String str = "bielefeld";
System.out.println(str.length());
System.out.println(str.charAt(0));
System.out.println(str.indexOf(’e’,3));
System.out.println(str.lastIndexOf(’e’,3));
System.out.println(str.startsWith("biele"));
System.out.println(str.endsWith("feld"));
System.out.println(str.substring(0,5));
System.out.println(str.substring(5,9));
System.out.println(str.regionMatches(0,"obiele",1,5));
if(str == args[0])
44
4.2 Grundlegende Definitionen
System.out.println("str == args[0]");
if(str.equals(args[0]))
System.out.println("str.equals(args[0])");
}
}
> java DemoStringClass bielefeld
9
b
4
2
true
true
biele
feld
true
str.equals(args[0])
4.2 Grundlegende Definitionen
Die Suche nach allen (exakten) Vorkommen eines Musters in einem Text ist ein
Problem, das häufig auftritt (z.B. in Editoren, Datenbanken etc.). Wir vereinbaren
folgende Konventionen (wobei Σ ein Alphabet ist):
Muster P = P [0 . . . m − 1] ∈ Σm
Text T = T [0 . . . n − 1] ∈ Σn
P [j] = Zeichen an der Position j
P [b . . . e] = Teilwort von P zwischen den Positionen b und e
Beispiel 4.2.1 P = abcde, P [0] = a, P [3] = d, P [2 . . . 4] = cde.
Notation:
• leeres Wort: ε
• Länge eines Wortes x: |x|
• Konkatenation zweier Worte x und y: xy
Definition 4.2.2 Ein Wort w ist ein Präfix eines Wortes x (w < x), falls x = wy
gilt für ein y ∈ Σ∗ . w ist ein Suffix von x (w = x), falls x = yw gilt für ein y ∈ Σ∗ .
x ist also auch ein Präfix bzw. Suffix von sich selbst.
45
4 Algorithmen zur exakten Suche in Texten
Lemma 4.2.3 Es seien x, y und z Zeichenketten, so dass x = z und y = z gilt.
Wenn |x| ≤ |y| ist, dann gilt x = y. Wenn |x| ≥ |y| ist, dann gilt y = x. Wenn
|x| = |y| ist, dann gilt x = y.
Beweis: Übung (Aufgabe 4.7.1).
4.3 Das Problem der exakten Suche
Definition 4.3.1 Ein Muster P kommt mit der Verschiebung s im Text T vor, falls
0 ≤ s ≤ n − m und T [s . . . s + m − 1] = P [0 . . . m − 1] gilt. In dem Fall nennen
wir s eine gültige Verschiebung.
Das Problem der exakten Textsuche ist nun, alle gültigen Verschiebungen zu finden. Mit den Bezeichnungen S0 = ε und Sk = S[0 . . . k − 1] (d.h. Sk ist das
k-lange Präfix eines Wortes S ∈ Σ` für 0 ≤ k ≤ `) können wir das Problem der
exakten Textsuche folgendermaßen formulieren: Finde alle Verschiebungen s, so
dass P = Ts+m .
Beispiel 4.3.2
T = bielefeld oh bielefeld
P = feld
Gültige Verschiebungen: s = 5 und s = 18.
Die naive Lösung des Problems sieht in Pseudo-Code wie folgt aus:
Naive-StringMatcher(T, P )
1 n ← length[T ]
2 m ← length[P ]
3 for s ← 0 to n − m do
4
if P [0 . . . m − 1] = T [s . . . s + m − 1] then
5
print “Pattern occurs with shift” s
Am Beispiel T = abrakadabraxasa und P = abraxas erkennt man das ineffiziente Vorgehen, insbesondere wenn Text und Muster immer komplett verglichen
werden:
46
4.3 Das Problem der exakten Suche
a b r a k a d a b r a x a s a
|
|
|
|
o
|
o
a b r a x a s
o
-
o
o
o
o
o
o
a b r a x a s
o
-
o
o
|
o
|
o
a b r a x a s
|
-
o
o
o
o
o
o
a b r a x a s
o
-
o
o
|
o
o
o
a b r a x a s
|
o
o
o
o
|
o
a b r a x a s
-
o
-
o
o
o
o
o
o
a b r a x a s
|
-
|
|
|
|
|
|
a b r a x a s
o
-
o
o
o
o
o
o
a b r a x a s
Eine Implementierung in Java verläuft analog:
public static void naiveMatcher(String text, String pattern) {
int m = pattern.length();
int n = text.length();
for(int s=0; s<=n-m; s++) {
if(text.regionMatches(s,pattern,0,m))
System.out.println("naiveMatcher: Pattern occurs with shift "+s);
}
}
Aber selbst wenn Text und Muster zeichenweise von links nach rechts und nur
bis zum ersten Mismatch1 verglichen
werden, ist die worst-case Zeiteffizienz des
naiven Algorithmus O n · m , z.B. wird für T = an , P = am die for-Schleife n −
m + 1 mal ausgeführt und in jedem Test in Zeile 4 werden m Zeichen verglichen.
Wie kann man den naiven Algorithmus verbessern?
Idee 1: Überspringe ein Teilwort w von T , falls klar ist, dass w 6= P (BM-Algorithmus
1977).
Idee 2: Merke Informationen über bisherige Vergleiche und nutze diese, um neue,
unnötige Vergleiche zu vermeiden (KMP-Algorithmus 1977).
1
Die zwei Zeichen stimmen nicht überein.
47
4 Algorithmen zur exakten Suche in Texten
4.4 Der Boyer-Moore-Algorithmus
Der BM-Algorithmus legt wie der naive Algorithmus das Muster zunächst linksbündig an den Text, vergleicht die Zeichen des Musters dann aber von rechts
nach links mit den entsprechenden Zeichen des Textes. Beim ersten Mismatch
benutzt er zwei Heuristiken, um eine Verschiebung des Musters nach rechts zu
bestimmen.
4.4.1 Die bad-character Heuristik
Falls beim Vergleich von P [0 . . . m − 1] und T [s . . . s + m − 1] (von rechts nach
links) ein Mismatch
P [j] 6= T [s + j] für ein j mit 0 ≤ j ≤ m − 1
festgestellt wird, so schlägt die bad-character Heuristik eine Verschiebung des
Musters um j − k Positionen vor, wobei k der größte Index (0 ≤ k ≤ m − 1) ist
mit T [s + j] = P [k]. Wenn kein k mit T [s + j] = P [k] existiert, so sei k = −1.
Man beachte, dass j − k negativ sein kann.
4.4.2 Die good-suffix Heuristik
Falls beim Vergleich von P [0 . . . m − 1] und T [s . . . s + m − 1] (von rechts nach
links) ein Mismatch
P [j] 6= T [s + j] für ein j mit 0 ≤ j ≤ m − 1
festgestellt wird, so wird das Muster so weit nach rechts geschoben, bis das bekannte Suffix T [s + j + 1 . . . s + m − 1] wieder auf ein Teilwort des Musters passt.
Hierfür ist eine Vorverarbeitung des Musters notwendig, auf die wir hier aber
nicht näher eingehen wollen.
Im Boyer-Moore Algorithmus wird das Muster um das Maximum von beiden
vorgeschlagenen Verschiebungen verschoben. Es kann gezeigt werden, dass die
Laufzeitkomplexität dann im worst case O(n + m) ist.
Beispiel 4.4.1 Die Ausgangssituation ist in Abbildung 4.1(a) dargestellt. Die badcharacter Heuristik schlägt eine Verschiebung von j − k = (m − 3) − 5 = (12 −
3) − 5 = 4 Positionen vor; dies ist im Fall (b) illustriert. Aus der Darstellung (c)
wird ersichtlich, dass die good-suffix Heuristik eine Verschiebung von 3 Positionen vorschlägt. Also wird das Muster um max{4, 3} = 4 Positionen nach rechts
verschoben.
48
4.5 Der Boyer-Moore-Horspool-Algorithmus
bad character
good suffix
?
?z}|{
... w r i t t e n _ n o t i c e _ t h a t ...
o
s- r e m i n i s c e n c e
(a)
... w r i t t e n _ n o t i c e _ t h a t ...
s+4
-
reminiscence
(b)
... w r i t t e n _ n o t i c e _ t h a t ...
s+3 - r e m i n i s c e n c e
(c)
A BBILDUNG 4.1: Verhalten des Boyer-Moore Algorithmus
4.5 Der Boyer-Moore-Horspool-Algorithmus
Der BM-Algorithmus verdankt seine Schnelligkeit vor allem der bad-charakter
Heuristik. Daher wurde 1980 von Horspool eine Vereinfachung des BM-Algorithmus
vorgeschlagen: Die bad-charakter Heuristik wird derart modifiziert, dass sie immer eine positive Verschiebung vorschlägt. Damit wird die good-suffix Heuristik
überflüssig (und auch die Vorverarbeitung einfacher).
Der BMH-Algorithmus geht in den meisten Fällen analog zur bisherigen badcharacter Heuristik vor.
Aber: Falls P [j] 6= T [s + j] für ein j (0 ≤ j ≤ m − 1) gilt, so wird s um m − 1 − k
erhöht, wobei k der größte Index zwischen 0 und m−2 ist mit T [s+m−1] = P [k].
Wenn kein k (0 ≤ k ≤ m − 2) mit T [s + m − 1] = P [k] existiert, so wird s um m
erhöht. Das Muster wird also um
λ T [s+m−1] = min {m}∪ m−1−k | 0 ≤ k ≤ m−2 und T [s+m−1] = P [k]
verschoben.
Wenn
ein
Match
gefunden
wurde,
dann
wird
ebenfalls
um
λ
T [s +
m − 1] verschoben.
Die Funktion λ lässt sich wie folgt berechnen:
computeLastOccurrenceFunction(P, Σ)
1 m ← length[P ]
2 for each character a ∈ Σ do
3
λ[a] = m
4 for j ← 0 to m − 2 do
49
4 Algorithmen zur exakten Suche in Texten
... g o l d e n _ f l e e c e _ o f ...
o
s- r e m i n i s c e n c e
⇓
... g o l d e n _ f l e e c e _ o f ...
s+3 - r e m i n i s c e n c e
A BBILDUNG 4.2: Verhalten des BMH-Algorithmus bei einem Mismatch
... g o l d e n _ f l e e c e _ o f ...
s
-
fleece
⇓
... g o l d e n _ f l e e c e _ o f ...
s+2
-
fleece
A BBILDUNG 4.3: Verhalten des BMH-Algorithmus bei einem Treffer
5
λ P [j] ← m − 1 − j
6 return λ
Die
worst-case-Komplexität von computeLastOccurrenceFunction ist O |Σ| +
m .
Der BMH-Algorithmus sieht in Pseudo-Code folgendermaßen aus:
BMH-Matcher(T, P, Σ)
1 n ← length[T ]
2 m ← length[P ]
3 λ ← computeLastOccurrenceFunction(P, Σ)
4 s←0
5 while s ≤ n − m do
6
j ←m−1
7
while j ≥ 0 and P [j] = T [s + j] do
8
j ←j−1
9
if j = −1 then
10
print “Pattern
occurs with shift” s
11
s ← s + λ T [s + m − 1]
Zur Verifikation, ob eine Verschiebung s gültig ist, wird O(m) Zeit benötigt. Ins-
50
4.6 Der Knuth-Morris-Pratt-Algorithmus
gesamt hat der BMH-Algorithmus die worst-case Zeitkomplexität O n · m + |Σ|
(z.B. für T = an , P = am ).
4.6 Der Knuth-Morris-Pratt-Algorithmus
4.6.1 Einführendes Beispiel
Der naive Algorithmus ist ineffizient, da unabhängig von bereits stattgefundenen
erfolgreichen Vergleichen das Muster immer um ein Zeichen verschoben wird.
Im folgenden Beispiel ist dies noch einmal anhand der Suche nach dem Wort
abraxas im Text abrakadabra dargestellt.
a b r a
| | | |
a b r a
o
- a b r
o
- a b
|
- a
k a d a b r a
o
x a s
a x a s
r a x a s
o
b r a x a s
Man erkennt, dass im zweiten und dritten Schritt das Zeichen a an Position 1 im
Muster mit den Zeichen b und r an der zweiten und dritten Position des Textes
verglichen wird, obwohl bereits nach dem positiven Vergleich von abra klar sein
musste, dass hier keine Übereinstimmung existieren kann. Verwendet man dagegen Information aus vorangegangenen Vergleichen, so muss man nach einer Verschiebung mit den zeichenweisen Vergleichen zwischen Muster und Text nicht
wieder am Anfang des Musters anfangen. Der Knuth-Morris-Pratt-Algorithmus
geht nach folgendem Schema vor: Wenn ein Teilwort (Präfix) des Musters bereits
erkannt wurde, aber dann ein mismatch auftritt, so ermittelt der Algorithmus
das längste Präfix dieses Teilwortes, das gleichzeitig echtes Suffix davon ist, und
schiebt das Muster dann so weit nach rechts, dass dieses Präfix an der bisherigen
Position des Suffixes liegt.
Beispiel 4.6.1 Im obigen Beispiel ist das bereits erkannte Teilwort abra. Das
längste Präfix von abra, das gleichzeitig echtes Suffix davon ist, ist a. Also schiebt
der Algorithmus das Muster so, dass das erste a von abraxas an der Position
steht, an der sich vorher das zweite a befunden hat. Der nächste stattfindende Vergleich ist der von k mit b, da die Verschiebung ja gerade so durchgeführt
wurde, dass die beiden as übereinander liegen und somit nicht mehr verglichen
werden müssen.
51
4 Algorithmen zur exakten Suche in Texten
a b r a k a d a b r a
| | | | o
a b r a x a s
o
- a b r a x a s
Man erkennt, dass hier drei unnötige Vergleiche vermieden wurden.
4.6.2 Funktionsweise
Beim KMP-Algorithmus wird das Muster P zeichenweise von links nach rechts
mit dem Text T verglichen. Es werden Informationen über bereits stattgefundene Vergleiche ausgenutzt, um weitere unnötige Vergleiche zu vermeiden. Als
Ausgangssituation sei Pq ein Präfix von P , das an Position s im Text vorkommt.
...
Pq
s-
Pq
...
Dann gehen wir gemäß der folgenden Fälle vor:
(A) q = m, d.h. s ist gültige Verschiebung:
1.
Bestimme k ∗ = π[m] = max{k : k < m und Pk = Pm }.
2.a) Falls k ∗ > 0, verschiebe das Muster nach rechts, so dass das Präfix Pk∗
von P unter dem Suffix Pk∗ von T [s . . . s + |P | − 1] liegt.
...
Pk ∗
...
Pk ∗
...
Pk ∗
⇓
...
Pk ∗
2.b) Falls k ∗ = 0, verschiebe das Muster P um die Länge |P |.
...
52
...
4.6 Der Knuth-Morris-Pratt-Algorithmus
(B) q < m:
a
Pk
...
|
{z
}
{z
}
Pq
Pk
...
b
|
Pq
a) a 6= b und q 6= 0:
• bestimme π[q] = max{k : k < q und Pk = Pq }, d.h. die Länge des
längsten Präfixes von P , das auch Suffix von Pq ist;
• verschiebe P nach rechts, so dass das Suffix Pk von Pq über dem
Präfix Pk von P liegt.
a
Pk
...
Pk
...
b
b) a 6= b und q = 0:
• verschiebe P um 1 nach rechts.
... a
...
b
c) a = b:
• mache weiter mit q + 1.
...
Pq
a
Pq
a
...
⇓
...
Pq+1
s-
Pq+1
...
Beobachtung: Für jeden der vier Fälle gilt:
• das Muster wird nie nach links geschoben
• entweder wird ein neues Zeichen des Textes gelesen (B.c), oder das
Muster wird um mindestens eine Position nach rechts geschoben (A),
(B.a), (B.b).
53
4 Algorithmen zur exakten Suche in Texten
Insgesamt können die Fälle also höchstens 2n-mal vorkommen.
Wir bestimmen die worst-case Zeitkomplexität des KMP-Algorithmus:
• Fälle (B.b) und (B.c) erfordern jeweils konstanten Zeitaufwand.
• In (A) und (B.a) wird die Funktion π benötigt mit π[q] = max{k : k < q
und Pk = Pq }, d.h. π[q] = Länge des längsten Präfixes von P , das ein
echtes Suffix von Pq ist. π hängt nur von P ab und kann daher in einem
Vorverarbeitungsschritt (V V ) berechnet und als Tabelle abgespeichert werden. Gegeben diese Tabelle, erfordern auch (A) und (B.a) nur konstanten
Zeitaufwand.
Damit ergeben sich folgende Komplexitäten: O(V V )+O(n) Zeit und O(m) Speicherplatz im worst-case!
Beispiel 4.6.2 Die Präfixfunktion π : {1, 2, . . . , m} → {0, 1, . . . , m − 1} für ein
Muster P [0 . . . m − 1] ist definiert durch π[q] = max{k : k < q und Pk = Pq }, also
die Länge des längsten Präfixes von P , das ein echtes Suffix von Pq ist. Sie lautet
für unser Beispielwort P = abrakadabra:
0 1 2 3 4 5 6 7 8 9 10 11
q
P [q] a b r a k a d a b r a
⊥
π[q] ⊥ 0 0 0 1 0 1 0 1 2 3
4
Für q = 9 wäre das entsprechende Teilmuster also abrakadab. Das längste Präfix,
das gleichzeitig echtes Suffix davon ist, ist ab. Es hat die Länge 2, darum findet
sich in der Tabelle an der Position q = 9 der Eintrag π[q] = 2.
Die Präfixfunktion π kann naiv folgendermaßen berechnet werden (die Laufzeit
beträgt O(m3 ), aber wir werden später sehen, wie es auch effizienter geht):
computePrefixFunction(P )
1 m ← length[P ]
2 π[1] ← 0
3 for q ← 2 to m do
4
k ←q−1
5
while k > 0 and P [0 . . . k − 1] 6= P [0 . . . q − 1] do
6
k ←k−1
7
π[q] ← k
8 return π
Damit können wir den KMP-Algorithmus in Pseudo-Code formulieren:
KMP-Matcher(T, P )
1 n ← length[T ]
2 m ← length[P ]
54
4.6 Der Knuth-Morris-Pratt-Algorithmus
3 π ← computePrefixFunction(P )
4 q←0
5 for i ← 0 to n − 1 do
6
while q > 0 and P [q] 6= T [i] do
7
q ← π[q]
8
if P [q] = T [i] then
9
q ←q+1
10
if q = m then
11
print “Pattern occurs with shift” i − m + 1
12
q ← π[q]
Wir gehen nun der Frage nach, wie die Funktion π effizienter berechnet werden
kann. Unter der Annahme, dass die Werte von π für 1, . . . , q − 1 schon berechnet
sind, wollen wir π[q] berechnen. Gesucht:
π[q] = max{k : k < q und Pk = Pq }
Welche Pk kommen in Frage? Alle Pk = P [0 . . . k − 1] für die gilt:
Pk−1 = Pq−1
|{z}
|{z}
P [0...k−2]
und P [k − 1] = P [q − 1],
P [0...q−2]
also alle echten Suffixe von Pq−1 , die sich zu einem echten Suffix von Pq erweitern
lassen. Die Menge der Längen aller echten Suffixe von Pq−1 , die auch Präfixe von
P sind, ist:
{k − 1 : k − 1 < q − 1 : Pk−1 = Pq−1 } = {k 0 : k 0 < q − 1 : Pk0 = Pq−1 }.
Damit muss gelten:
π[q] = max{k 0 + 1 : k 0 < q − 1, Pk0 = Pq−1 und P [k 0 ] = P [q − 1]}.
Nun kann man folgende Gleichheit zeigen (Lemma 4.6.4):
{k 0 : k 0 < q − 1 : Pk0 = Pq−1 } = {π[q − 1], π π[q − 1] , π 3 [q − 1], . . . , π t [q − 1]},
| {z }
=π 2 [q−1]
wobei π t [q − 1] = 0 gilt. Schließlich erhalten wir:
π[q] = max{k 0 + 1 : k ∈ {π[q − 1], . . . , π t [q − 1]} und P [k 0 ] = P [q − 1]}.
Damit erhalten wir folgendes Verfahren, um π[q] zu berechnen: Wir schauen
π[q − 1] in der Tabelle von π nach und prüfen, ob sich das echte Suffix Pπ[q−1]
von Pq−1 zu einem echten Suffix von Pq erweitern lässt. Wenn ja, so ist π[q] =
55
4 Algorithmen zur exakten Suche in Texten
π[q − 1] + 1. Wenn nein, so iterieren wir diesen Prozess, d.h. wir machen das Gleiche mit π(π[q − 1]) usw. Diese Überlegungen sollten die folgende Berechnungsmethode der Funktion π motivieren. Wir werden deren Korrektheit im nächsten
Unterabschnitt beweisen.
computePrefixFunction2(P )
1 m ← length[P ]
2 π[1] ← 0
3 k←0
4 for q ← 2 to m do
5
while k > 0 and P [k] 6= P [q − 1] do
6
k ← π[k]
7
if P [k] = P [q − 1] then
8
k ←k+1
9
π[q] ← k
10 return π
Die Laufzeit von computePrefixFunction2 beträgt O(m). Dies kann man folgendermaßen sehen:
1. Die for-Schleife in Zeile 4 wird m − 1-mal durchlaufen, demzufolge auch
die Zeilen 7-9.
2. Der Wert von k ist anfangs 0 und wird insgesamt höchstens m − 1-mal in
Zeile 8 um jeweils 1 erhöht.
3. Bei jedem Durchlauf der while-Schleife in den Zeilen 5 und 6 wird der Wert
von k um mindestens 1 erniedrigt. Da k nicht negativ wird (Zeile 5), kann
die while-Schleife insgesamt also höchstens k-mal durchlaufen werden.
4.6.3 Korrektheit der Berechnung der Präfixfunktion
Das wesentliche Lemma im Korrektheitsbeweis besagt, dass man alle Präfixe Pk ,
die Suffix eines gegebenen Präfixes Pq sind, berechnen kann, indem man die
Präfixfunktion π iteriert.
Definition 4.6.3 Es sei
π ∗ [q] = q, π[q], π 2 [q], π 3 [q], . . . , π t [q]
wobei π 0 [q] = q und π i+1 [q] = π π i [q] für i ≥ 0; dabei sei vereinbart, dass die
Folge in π ∗ [q] abbricht, wenn π t [q] = 0 erreicht wird.
Lemma 4.6.4 Es sei P [0 . . . m − 1] ein Muster und π die dazu gehörige Präfixfunktion.
Dann gilt π ∗ [q] = {k : Pk = Pq } für q = 1, . . . , m.
56
4.6 Der Knuth-Morris-Pratt-Algorithmus
Beweis: Wir beweisen das Lemma, indem wir (1) π ∗ [q] ⊆ {k : Pk = Pq } und (2)
{k : Pk = Pq } ⊆ π ∗ [q] zeigen.
1. Wir zeigen: i ∈ π ∗ [q] impliziert Pi = Pq . Sei also i ∈ π ∗ [q]. Für i gibt es ein
u ∈ N mit i = π u [q]. Wir beweisen Pπu [q] = Pq durch Induktion über u.
Induktionsanfang: u = 0. Dann ist i = π 0 [q] = q und die Behauptung gilt.
Induktionshypothese: Für i0 = π u0 [q] gilt Pi0 = Pq .
Induktionsschritt:
Zu zeigen ist: für i = π u0 +1 [q] gilt Pi = Pq . Es gilt i =
u 0
π π [q] = π[i0 ]. Mit der Induktionshypothese folgt Pi0 = Pq . Weiterhin
gilt
i = π[i0 ] = max{k : k < i0 und Pk = Pi0 },
also gilt Pi = Pi0 = Pq . Da = transitiv ist, folgt Pi = Pq .
2. Wir beweisen {k : Pk = Pq } ⊆ π ∗ [q] durch Widerspruch. Wir nehmen an
∃j ∈ {k : Pk = Pq }\π ∗ [q].
Ohne Einschränkung der Allgemeinheit sei j die größte Zahl mit dieser Eigenschaft. Da
j ≤ q und q ∈ {k : Pk = Pq } ∩ π ∗ [q]
gilt, folgt j < q. Es sei j 0 die kleinste Zahl in π ∗ [q], die größer als j ist (da
q ∈ π ∗ [q] gilt, existiert solch ein j 0 immer). Wegen j ∈ {k : Pk = Pq } gilt
Pj = Pq . Weiterhin impliziert j 0 ∈ π ∗ [q] wg. (1) Pj 0 = Pq . Mit j < j 0 folgt
damit Pj = Pj 0 . Es gibt kein
j 00 , j < j 00 < j 0 , mit Pj = Pj 00 = Pj 0
Gäbe es solch ein j 00 , so müsste j 00 ∈ π ∗ [q] gelten, denn j ist die größte Zahl
mit
j ∈ {k : Pk = Pq }\π ∗ [q].
j 00 ∈ π ∗ [q] kann aber nicht gelten, denn j 0 ist die kleinste Zahl in π ∗ [q], die
größer als j ist. Also muss j = max{k : k < j 0 und Pk = Pj 0 } = π[j 0 ] und
damit j ∈ π ∗ [q] gelten. Dieser Widerspruch beweist {k : Pk = Pq } ⊆ π ∗ [q].
2
Definition 4.6.5 Für q = 2, 3, . . . , m sei
Eq−1 = k : k < q − 1, k ∈ π ∗ [q − 1], P [k] = P [q − 1] .
Die Menge Eq−1 ⊆ π ∗ [q − 1] enthält alle
k ∈ π ∗ [q − 1] = {k : Pk = Pq−1 }
mit k < q − 1 und Pk+1 = Pq ; denn aus
Pk = P [0 . . . k − 1] = Pq−1 = P [0 . . . q − 2] und P [k] = P [q − 1]
folgt Pk+1 = P [0 . . . k] = Pq = P [0 . . . q − 1]).
57
4 Algorithmen zur exakten Suche in Texten
Lemma 4.6.6 Sei P [0 . . . m − 1] ein Muster und π die zugehörige Präfixfunktion.
Für q = 2, 3, . . . , m gilt:
(
0
falls Eq−1 = ∅
π[q] =
1 + max{k ∈ Eq−1 } falls Eq−1 6= ∅
Beweis: Mit der Vereinbarung max{} = 0 gilt:
π[q]
=
max k : k < q und Pk = Pq
=
max k : k < q, Pk−1 = Pq−1 und P [k − 1] = P [q − 1]
(4.6.4)
=
max k : k − 1 < q − 1, k − 1 ∈ π ∗ [q − 1] und P [k − 1] = P [q − 1]
k0 =k−1
0
∗
0
=
max k 0 + 1 : k 0 < q − 1,
k
∈
π
[q
−
1]
und
P
[k
]
=
P
[q
−
1]
=
max k 0 + 1 : k 0 ∈ Eq−1
Fall 1: Eq−1 = ∅: Dann gilt π[q] = max{} = 0.
Fall 2: Eq−1 6= ∅: Dann gilt π[q] = max{1 + k 0 : k 0 ∈ Eq−1 } = 1 + max{k 0 : k 0 ∈
Eq−1 }.
2
Satz 4.6.7 computePrefixFunction2(P) berechnet die Funktion π korrekt.
Beweis: Am Anfang jeder Iteration der for-Schleife gilt k = π[q − 1], denn beim
ersten Betreten der for-Schleife gilt k = 0, q = 2 und π[q − 1] = 0 = k und diese
Eigenschaft bleibt wegen Zeile 9 bei jeder Iteration der for-Schleife erhalten. Die
Eigenschaft k = π[q − 1] wird daher auch Invariante der Schleife genannt. Die
while-Schleife untersucht alle Werte
k ∈ π ∗ [q − 1]\{q − 1},
bis einer mit P [k] = P [q − 1] gefunden wird; an diesem Punkt gilt
k = max{k ∈ Eq−1 },
so dass π[q] den Wert k + 1 = 1 + max{k ∈ Eq−1 } erhält. Wird kein solcher Wert
k gefunden, so wird π[q] der Wert 0 zugewiesen, denn es gilt k = 0 in den Zeilen
7-9.
Das in Zeile 10 zurückgegebene Feld π hat also folgende Werte:
π[1] = 0 und für q = 2, 3, . . . , m
0
π[q] =
1 + max{k ∈ Eq−1 }
falls Eq−1 = ∅
falls Eq−1 6= ∅
2
58
4.7 Aufgaben
4.7 Aufgaben
Aufgabe 4.7.1 Beweisen Sie Lemma 4.2.3.
Aufgabe 4.7.2 Implementieren Sie den Boyer-Moore-Horspool-Algorithmus in
Java.
Aufgabe 4.7.3 Bestimmen Sie die worst-case Zeiteffizienz der naiven Implementierung von computePrefixFunction.
Aufgabe 4.7.4 Implementieren Sie den Knuth-Morris-Pratt-Algorithmus in Java.
Aufgabe 4.7.5 Geben Sie konkrete Beispiele an, in denen
(a) der Knuth-Morris-Pratt-Algorithmus
(b) der Boyer-Moore-Horspool-Algorithmus
(c) der Boyer-Moore-Algorithmus
sich besser verhält als die beiden anderen (unter Vernachlässigung der Vorverarbeitung). Das Kriterium ist hierbei die Anzahl der Vergleiche von Zeichen, die
jeweils durchgeführt werden müssen.
Aufgabe 4.7.6 Man erreicht eine verbesserte Version des Knuth-Morris-PrattMatchers, indem man π in der Zeile 7 (nicht aber in Zeile 12) durch π 0 ersetzt. π 0
ist rekursiv für q = 1, 2, . . . , m − 1 definiert:

wenn π[q] = 0,
 0
π 0 [π[q]]
wenn π[q] 6= 0 und P [π[q]] = P [q],
π 0 [q] =

π[q]
wenn π[q] 6= 0 und P [π[q]] 6= P [q].
Erklären Sie, warum der modifizierte Algorithmus korrekt ist und inwiefern diese Modifikation eine Verbesserung darstellt. Stellen Sie die Funktion π 0 für das
Muster P = ababababca tabellarisch dar.
Aufgabe 4.7.7 Geben Sie einen Algorithmus an, der herausfindet, ob ein Text T
eine zyklische Verschiebung eines Textes T 0 ist. Zum Beispiel ist der Text reif eine
zyklische Verschiebung von frei. Der Algorithmus soll linear im Zeitaufwand
sein. Implementieren Sie Ihren Algorithmus in Java.
Aufgabe 4.7.8 Erklären Sie, wie man alle Vorkommen des Musters P im Text
T unter Zuhilfenahme der π Funktion für die Zeichenkette P T bestimmen kann.
P T ist die Konkatenation von P und T (also m+n Zeichen lang). Implementieren
Sie Ihren Vorschlag in Java.
59
4 Algorithmen zur exakten Suche in Texten
60
5 Objektorientierte Programmierung
in Java
Grundzüge der objektorientierten Programmierung haben wir bereits in Kapitel 2 kennengelernt, auch Teile der entsprechenden JavaSyntax. Dieses Kapitel soll nun etwas systematischer und ausführlicher noch einmal die entsprechenden Java-Konstrukte zur Umsetzung objektorientierter Programmierung sowie Ausnahmen und Spezialfälle behandeln. Beginnen wollen wir jedoch mit einem Blick
in die Historie, denn die Objektorientierung ist nur der (derzeitige)
Schlusspunkt einer längeren Entwicklung.
5.1 Traditionelle Konzepte der Softwaretechnik
Folgende traditionelle Konzepte des Software-Engineering werden u.a. im objektorientierten Ansatz verwendet:
Datenabstraktion (bzw. Datenkapselung) und Information Hiding
Die zentrale Idee der Datenkapselung ist, dass auf eine Datenstruktur nicht
direkt zugegriffen wird, indem etwa einzelne Komponenten gelesen oder
geändert werden, sondern, dass dieser Zugriff ausschließlich über Zugriffsoperatoren erfolgt. Es werden also die Implementierungen der Operationen
und die Datenstrukturen selbst versteckt.
Vorteil: Implementierungdetails können beliebig geändert werden, ohne
Auswirkung auf den Rest des Programmes zu haben.
abstrakte Datentypen (ADT)
Realisiert wird die Datenabstraktion duch den Einsatz abstrakter Datentypen, die Liskov & Zilles (1974) folgendermaßen definierten:
“An abstract data type defines a class of abstract objects which
is completely characterized by the operations available on those
objects. This means that an abstract data type can be defined by
defining the characterizing operations for that type.”
61
5 Objektorientierte Programmierung in Java
Oder etwas prägnanter:
Datentyp = Menge(n) von Werten + Operationen darauf
abstrakter Datentyp = Operationen auf Werten, deren Repräsentation nicht bekannt ist. Der Zugriff erfolgt ausschließlich über Operatoren.
Datenabstraktion fördert die Wiederverwendbarkeit von Programmteilen und die
Wartbarkeit großer Programme.
5.1.1 Beispiel: Der ADT Stack
Stack: Eine Datenstruktur über einem Datentyp T bezeichnet man als Stack1 ,
wenn die Einträge der Datenstruktur als Folge organisiert sind und es die
Operationen push, pop und peek gibt:
push fügt ein Element von T stets an das Ende der Folge.
pop entfernt stets das letzte Element der Folge.
peek liefert das letzte Element der Folge, ohne sie zu verändern.
Prinzip: last in first out (LIFO)
Typen der Operationen: initStack:
push:
pop:
peek:
empty:
T × Stack
Stack
Stack
Stack
−→ Stack
−→ Stack
−→ Stack
−→ T
−→ boolean
Spezifikation der Operationen durch Gleichungen. Sei x eine Variable vom Typ T,
stack eine Variable vom Typ Stack:
empty (initStack)
empty (push (x, stack))
peek (push (x, stack))
pop (push (x, stack))
= true
= false
=x
= stack
initStack und push sind Konstruktoren (sie konstruieren Terme), daher gibt es
keine Gleichungen für sie.
5.2 Konzepte der objektorientierten Programmierung
Ziel jeglicher Programmierung ist:
• Modellierung von Ausschnitten der Realität
1
bedeutet soviel wie Keller oder Stapel
62
5.2 Konzepte der objektorientierten Programmierung
• sachgerechte Abstraktion
• realitätsnahes Verhalten
• Nachbildung von Ähnlichkeit im Verhalten
• Klassifikation von Problemen
Je nach Problem können verschiedene Klassifikationen sachgerecht sein, dies ist
anhand eines Beispiels aus der Biologie in der Abbildung 5.1 dargestellt.
Tiere
HH
?
Insekten
@
@
R
@
?
HH
j
Säugetiere
Fische
@
HH
H
@
@
R
@
?
@
R
@
?
Tiere
HH
?
Zuchttiere
?
HH
j
H
Störtiere
Wild
@
HH
@
@
R
@
?
@
@
R
@
?
@
R
@
A BBILDUNG 5.1: Phylogenetische (oben) und ökonomische Klassifizierung (unten).
Es werden immer bestimmte Funktionen auf bestimmte Daten angewendet. Soll
nun die Architektur eines Systems (Modells) auf den Daten oder auf den Funktionen aufbauen?
Grundsätzlich gibt es drei Vorgehensweisen:
1. die funktionsorientierte
2. die datenorientierte
3. die objektorientierte
63
5 Objektorientierte Programmierung in Java
Der Kerngedanke des objektorientierten Ansatzes besteht darin, Daten und Funktionen zu verschmelzen. Im ersten Schritt werden die Daten abgeleitet, im zweiten Schritt werden den Daten die Funktionen zugeordnet, die sie manipulieren.
Die entstehenden Einheiten aus Daten und Funktionen werden Objekte genannt.
Wir schränken den Begriff Objektorientierung gemäß folgender Gleichung von
Coad & Yourdon weiter ein:
Objektorientierung = Klassen und Objekte
+ Kommunikation mit Nachrichten
+ Vererbung
Im folgenden erläutern wir diese Konzepte kurz.
5.3 Klassen und Objekte
Eine Klasse besteht konzeptionell aus einer Schnittstelle und einem Rumpf. In
der Schnittstelle sind die nach außen zur Verfügung gestellten Methoden (und
manchmal auch öffentlich zugängliche Daten), sowie deren Semantik aufgelistet. Diese Auflistung wird oft als Vertrag oder Nutzungsvorschrift zwischen dem
Entwerfer der Klasse und dem sie verwendenen Programmierer gedeutet. Der
Klassenrumpf enthält alle von außen unsichtbaren Implementierungdetails.
Historisch gesehen ist der Klassenbegriff älter als der Begriff des abstrakten Datentypen (ADT). In der Programmiersprache Simula 67 gab es bereits Klassen als
Mechanismus zur Datenkapselung (Abstakte Datentypen wurden erstmals 1974
von Liskov & Zilles definiert). Der Kerngedanke der Objektorientierung, Daten
und Funktionen konsequent als Objekte zusammenzufassen, wird jedoch auf die
Programmiersprache Smalltalk zurückgeführt (entwickelt seit Beginn der 70er
Jahre).
5.4 Kommunikation mit Nachrichten
Objekte besitzen die Möglichkeit, mit Hilfe ihrer Methoden Aktionen auszuführen. Das Senden einer Nachricht stößt die Ausführung einer Methode an. Eine
Nachricht besteht aus einem Empfänger (das Objekt, das die Aktionen ausführen soll), einem Selektor (die Methode, deren Aktionen auszuführen sind) und
gegebenenfalls aus Argumenten (Werte, auf die während der Ausführung der
Aktion zugegriffen wird).
64
5.5 Vererbung
5.5 Vererbung
Gleichartige Objekte werden zu Klassen zusammengefasst. Häufig besitzen Objekte zwar bestimmte Gemeinsamkeiten, sind aber nicht völlig gleichartig. Um
solche Ähnlichkeiten auszudrücken, ist es möglich, zwischen Klassen Vererbungsbeziehungen festzulegen. Dazu wird das Verhalten einer existierenden Klasse erweitert. Die Erweiterung erzeugt eine von ihr alle Attribute und Methoden erbende neue Klasse, die um weitere Attribute und Methoden ergänzt wird. Die
neue Klasse wird Unterklasse, die ursprüngliche Klasse Oberklasse genannt.
Gemeinsamkeiten: in der Oberklasse
Unterschiede: in der Unterklasse
Eine Unterklasse kann auch von der Oberklasse ererbte Methoden redefinieren
(überschreiben). Wir sprechen von Einfachvererbung, wenn jede neue Klasse genau eine Oberklasse erweitert (Abbildung 5.2).
Object
@
?
System
Math
@
R
@
Point
@
@
R
@
...
A BBILDUNG 5.2: Einfachvererbung (Java)
...
@
@
R
@
Tiere
Pflanzen
@
@
R
@
Fleischfresser
@
@
R
@
...
A BBILDUNG 5.3: Mehrfachvererbung
65
5 Objektorientierte Programmierung in Java
Wenn eine Klasse mehrere Oberklassen besitzen kann, sprechen wir von Mehrfachvererbung (Abbildung 5.3). In Java gibt es nur Einfachvererbung (aus gutem
Grund). Die einzige Klasse, die keine Oberklasse erweitert, ist die vordefinierte
Klasse Object. Klassen, die nicht explizit andere Klassen erweitern, erweitern implizit die Klasse Object. Alle Objektreferenzen sind in polymorpher Weise von
der Klasse Object, so dass Object die generische Klasse für Referenzen ist, die
sich auf Objekte jeder beliebigen Klasse beziehen können. Das nächste Beispiel
verdeutlicht dies.
Object oref = new Point();
oref = "eine Zeichenkette";
5.6 Konstruktoren und Initialisierungsblöcke
Einem neu erzeugten Objekt wird ein Anfangszustand zugewiesen. Datenfelder
können bei ihrer Deklaration mit einem Wert initialisiert werden, was manchmal
ausreicht, um einen sinnvollen Anfangszustand sicherzustellen. Oft ist aber mehr
als nur einfache Dateninitialisierung zur Erzeugung eines Anfangszustands nötig; der erzeugende Code muss vielleicht Anfangsdaten liefern oder Operationen
ausführen, die nicht als einfache Zuweisungen ausgedrückt werden können. Um
mehr als einfache Initialisierungen bewerkstelligen zu können, können Klassen
Konstruktoren enthalten. Konstruktoren sind keine Methoden, aber methodenähnlich: Sie haben denselben Namen wie die von ihnen initialisierte Klasse, haben keine oder mehrere Parameter und keinen Rückgabetyp. Bei der Erzeugung
eines Objekts mit new werden eventuelle Parameterwerte nach dem Klassennamen in einem Klammernpaar angegeben. Bei der Objekterzeugung werden zuerst den Instanzvariablen ihre voreingestellten Anfangswerte zugewiesen, dann
ihre Initialisierungsausdrücke berechnet und zugewiesen und dann der Konstruktor aufgerufen.
Im folgenden benutzen wir die Klasse Circle als Standardbeispiel. Ein Kreis
besteht aus einer x-Koordinate, einer y-Koordinate sowie dem Radius r. Desweiteren wird die Anzahl der erzeugten Kreise gezählt durch die Anweisung
numCircles++;, die bei jedem Aufruf des parameterlosen Konstruktors ausgeführt wird.
public class Circle {
int x=0, y=0, r=1;
static int numCircles=0;
public Circle() {
numCircles++;
}
66
5.6 Konstruktoren und Initialisierungsblöcke
public double circumference() {
return 2*Math.PI*r;
}
public double area() {
return Math.PI*r*r;
}
public static void main(String[] args) {
Circle c = new Circle();
System.out.println(c.r);
System.out.println(c.circumference());
System.out.println(c.area());
System.out.println(numCircles);
}
}
Statt des parameterlosen Konstruktors hätten wir in der Klasse auch einen Konstruktor mit drei Parametern definieren können, der nicht nur Einheitskreise erzeugen kann:
public Circle(int xCoord, int yCoord, int radius) {
numCircles++;
x = xCoord;
y = yCoord;
r = radius;
}
Standardmäßig benennt man die Parametervariablen im Konstruktor genauso
wie die Variablen in der Klasse. Da aber hierbei Namenskonflikte entstehen, muss
man die Variable des Objektes mit this.Variable referenzieren.
public Circle(int x, int y, int r) {
numCircles++;
this.x = x;
this.y = y;
this.r = r;
}
Für eine Klasse kann es in Java auch mehrere Konstruktoren geben. Diese müssen sich allerdings in der Anzahl der Attribute bzw. deren Typen unterscheiden.
Dies nennt man Überladen von Konstruktoren. In der folgenden Klasse gibt es
drei Konstruktoren namens Circle. Die Konstruktoren mit Parametern rufen den
67
5 Objektorientierte Programmierung in Java
parameterlosen Konstruktor mittels this() auf. Dies hat den Vorteil, dass Änderungen an den Konstruktoren nicht an drei Stellen gemacht werden müssen (was
fehleranfällig ist), sondern nur im parameterlosen Konstruktor. Um die Anzahl
der erzeugten Kreise zu zählen, muss man die Programmzeile numCircles++;
nur dem parameterlosen Konstruktor hinzufügen.
public class Circle {
int x = 0, y = 0, r = 1;
static int numCircles;
public Circle() {
numCircles++;
}
public Circle(int x, int y, int r) {
this();
this.x = x;
this.y = y;
this.r = r;
}
public Circle(int r) {
this(0,0,r);
}
public static void main(String[] args) {
Circle c1 = new Circle();
Circle c2 = new Circle(1,1,2);
Circle c3 = new Circle(3);
System.out.println(numCircles);
}
}
Klassenvariablen werden initialisiert, wenn die Klasse das erste Mal geladen wird.
Das Analogon zu Konstruktoren, um komplexe Initialisierungen von Klassenvariablen durchzuführen, sind die sogenannten Initialisierungsblöcke. Diese Blöcke
werden durch static {. . . } umschlossen, wie folgendes Beispiel demonstriert.
Beispiel 5.6.1 (Flanagan [5], S. 59)
public class Circle {
public static double[] sines = new double[1000];
public static double[] cosines = new double[1000];
68
5.7 Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen
static {
double x, delta_x;
int i;
delta_x = (Math.PI/2)/(1000-1);
for(i=0,x=0; i<1000; i++,x+=delta_x) {
sines[i] = Math.sin(x);
cosines[i] = Math.cos(x);
}
}
}
Es können mehrere klassenbezogene Initialisierungsblöcke in einer Klasse enthalten sein. Die Klasseninitialisierung erfolgt von links nach rechts und von oben
nach unten.
5.7 Java-Klassen als Realisierung und
Implementierung von abstrakten Datentypen
Durch den Modifizierer private können wir Implementierungsdetails verstecken,
denn als private deklarierte Attribute und Methoden sind nur in der Klasse selbst
zugreifbar2 . Folgende Klasse implementiert einen ADT Stack mittels eines Feldes:
public class Stack {
private Object[] stack;
private int top = -1;
private static final int CAPACITY = 10000;
/** liefert einen leeren Keller. */
public Stack() {
stack = new Object[CAPACITY];
}
/** legt ein Objekt im Keller ab und liefert dieses Objekt
zusaetzlich zurueck. */
public Object push(Object item) {
stack[++top] = item;
return item;
}
2
Synonyme für Zugreifbarkeit sind: Gültigkeit bzw. Sichtbarkeit.
69
5 Objektorientierte Programmierung in Java
/** entfernt das oberste Objekt vom Keller und liefert es zurueck.
Bei leerem Keller wird eine Fehlermeldung ausgegeben und
null zurueckgeliefert. */
public Object pop() {
if (empty()) {
System.out.println("Method pop: empty stack");
return null;
}
else
return stack[top--];
}
/** liefert das oberste Objekt des Kellers, ohne ihn zu veraendern.
Bei leerem Keller wird eine Fehlermeldung ausgegeben und
null zurueckgeliefert. */
public Object peek() {
if (empty()) {
System.out.println("Method peek: empty stack");
return null;
}
else
return stack[top];
}
/** liefert true genau dann, wenn der Keller leer ist. */
public boolean empty() {
return (top == -1);
}
/** liefert die Anzahl der Elemente des Kellers. */
public int size() {
return top+1;
}
}
Der Dokumentationskommentar /** ... */ wird zur automatischen Dokumentierung der Attribute und Methoden einer Klasse benutzt. Das Programm javadoc
generiert ein HTML-File, in dem alle sichtbaren Attribute und Methoden mit de-
70
5.8 Methoden in Java
ren Parameterlisten aufgezeigt und dokumentiert sind.
> javadoc Stack.java
Dieses HTML-File ist der Vertrag (die Schnittstelle) der Klasse und entspricht
dem ADT Stack, wobei die Operationen bzw. Methoden allerdings nur natürlichsprachlich spezifiziert wurden. Die obige verbale Spezifikation entspricht weitgehend der der vordefinierten Java-Klasse Stack (genauer java.util.Stack). Man
beachte, dass (aus diesem Grund) die obige Spezifikation von der Gleichungsspezifikation aus dem Unterabschnitt 5.1.1 abweicht.
5.8 Methoden in Java
Methoden können wie Konstruktoren überladen werden. In Java besitzt jede Methode eine Signatur, die ihren Namen sowie die Anzahl und Typen der Parameter
definiert. Zwei Methoden können denselben Namen haben, wenn ihre Signaturen unterschiedliche Anzahlen oder Typen von Parametern aufweisen; dies wird
als Überladen von Methoden bezeichnet. Wird eine Methode aufgerufen, vergleicht der Übersetzer die Anzahl und die Typen der Parameter mit den verfügbaren Signaturen, um die passende Methode zu finden.
Die Parameterübergabe zu Methoden erfolgt in Java durch Wertübergabe (call
by value). D.h., dass Werte von Parametervariablen in einer Methode Kopien der
vom Aufrufer angegebenen Werte sind. Das nächste Beispiel verdeutlicht dies.
public class CallByValue {
public static int sqr(int i) {
i = i*i;
return(i);
}
public static void main(String[] args) {
int i = 3;
System.out.println(sqr(i));
System.out.println(i);
}
}
> java CallByValue
9
3
71
5 Objektorientierte Programmierung in Java
Allerdings ist zu beachten, dass nicht Objekte, sondern Objektreferenzen übergeben werden. Wir betrachten unser Standardbeispiel Circle in folgender abgespeckter Form (gemäß der Devise, Implementierungsdetails zu verbergen, werden die Datenfelder als private deklariert).
public class Circle {
private int x,y,r;
public Circle(int x, int y, int r) {
this.x = x;
this.y = y;
this.r = r;
}
public double circumference() {
return 2 * Math.PI * r;
}
public double area() {
return Math.PI * r * r;
}
public static void setToZero (Circle arg) {
arg.r = 0;
arg = null;
}
public static void main(String[] args) {
Circle kreis = new Circle(10,10,1);
System.out.println("vorher : r = "+kreis.r);
setToZero(kreis);
System.out.println("nachher: r = "+kreis.r);
}
}
> java Circle
vorher : r = 1
nachher: r = 0
Dieses Verhalten entspricht jedoch nicht der Parameterübergabe call by reference, denn bei der Wertübergabe wird eine Kopie der Referenz erzeugt und die
72
5.9 Unterklassen und Vererbung in Java
ursprüngliche Referenz bleibt erhalten. Bei call by reference würde die übergebene Referenz eben nicht kopiert und daher in der Methode setToZero auf null
gesetzt.
5.9 Unterklassen und Vererbung in Java
Wir wollen die Klasse Circle so erweitern, dass wir deren Instanzen auch graphisch darstellen können. Da ein solcher “graphischer Kreis” ein Kreis ist (es
herrscht eine “ist-ein” Beziehung), erweitern wir die Klasse Circle zu der neuen
Klasse GraphicCircle3 . Durch das Schlüsselwort extends wird GraphicCircle
eine Unterklasse von Circle. Wir sagen auch GraphicCircle erweitert die (Ober)Klasse Circle. Damit erbt die Klasse GraphicCircle alle Attribute und Methoden von Circle, nur die als private deklarierten sind nicht über ihren Namen
zugreifbar. Damit ist unsere Entscheidung, die Attribute x, y und r privat zu halten, nicht mehr sinnvoll. Um diese Attribute dennoch vor unerwünschten Zugriffen zu schützen, werden sie als protected deklariert. Damit sind sie zugreifbar
für Unterklassen und werden an diese vererbt, in anderen Klassen sind sie nicht
zugreifbar4 .
import java.awt.Color;
import java.awt.Graphics;
public class GraphicCircle extends Circle {
protected Color outline;
// Farbe der Umrandung
protected Color fill;
// Farbe des Inneren
public GraphicCircle(int x,int y,int r,Color outline) {
super(x,y,r);
this.outline = outline;
this.fill = Color.lightGray;
}
public GraphicCircle(int x,int y,int r,Color outline,Color fill) {
this(x,y,r,outline);
this.fill = fill;
}
public void draw(Graphics g) {
3
Nur wenn eine solche “ist-ein” Beziehung herrscht, ist eine Erweiterung sinnvoll. Beispielsweise
wäre eine Erweiterung der Klasse Circle zu einer Klasse Ellipse ein Design-Fehler, da eine
Ellipse kein Kreis ist. Umgekehrt wäre dieses sinniger, da ein Kreis eine Ellipse ist.
4
Es sei denn, die Klasse befindet sich im selben Paket (siehe Abschnitt 2.4.2)!
73
5 Objektorientierte Programmierung in Java
g.setColor(outline);
g.drawOval(x-r, y-r, 2*r, 2*r);
g.setColor(fill);
g.fillOval(x-r, y-r, 2*r, 2*r);
}
public static void main(String[] args) {
GraphicCircle gc = new GraphicCircle(0,0,100,Color.red,Color.blue);
double area = gc.area();
System.out.println(area);
Circle c = gc;
double circumference = c.circumference();
System.out.println(circumference);
GraphicCircle gc1 = (GraphicCircle) c;
Color color = gc1.fill;
System.out.println(color);
}
}
Color und Graphics sind vordefinierte Klassen, die durch import zugreifbar gemacht werden (vgl. Abschnitt 2.4.2). Diese Klassen werden z.B. in [5] beschrieben. Zum Verständnis reicht es hier zu wissen, dass der erste Konstruktor den
Konstruktor seiner Oberklasse aufruft (vgl. Abschnitt 5.11) und das Kreisinnere die Farbe hellgrau erhält, sowie, dass die Methode draw einen farbigen Kreis
zeichnet.
Da GraphicCircle alle Methoden von Circle erbt, können wir z.B. den Flächeninhalt eines Objektes gc vom Typ GraphicCircle berechnen durch:
double area = gc.area();
Jedes Objekt gc vom Typ GraphicCircle ist ebenfalls ein Objekt vom Typ Circle
bzw. vom Typ Object. Deshalb sind folgende Zuweisungen korrekt.
Circle c = gc;
double area = c.area();
Man kann c durch casting5 in ein Objekt vom Typ GraphicCircle zurückverwandeln.
GraphicCircle gc1 = (GraphicCircle)c;
Color color = gc1.fill;
Die oben gezeigte Typumwandlung funktioniert nur, weil c tatsächlich ein Objekt
vom Typ GraphicCircle ist.
5
explizite Typumwandlung
74
5.10 Überschreiben von Methoden und Verdecken von Datenfeldern
5.10 Überschreiben von Methoden und Verdecken
von Datenfeldern
Wir betrachten folgendes Java-Programm (Arnold & Gosling [1], S. 66):
public class SuperShow {
public String str = "SuperStr";
public void show() {
System.out.println("Super.show: "+str);
}
}
public class ExtendShow extends SuperShow {
public String str = "ExtendStr";
public void show() {
System.out.println("Extend.show: "+str);
}
public static void main(String[] args) {
ExtendShow ext = new ExtendShow();
SuperShow sup = ext;
sup.show();
ext.show();
System.out.println("sup.str = "+sup.str);
System.out.println("ext.str = "+ext.str);
}
}
Verdecken von Datenfeldern
Jedes ExtendShow-Objekt hat zwei String-Variablen, die beide str heißen und
von denen eine ererbt wurde. Die neue Variable str verdeckt die ererbte; wir
sagen auch die ererbte ist verborgen. Sie existiert zwar, man kann aber nicht
mehr durch Angabe ihres Namens auf sie zugreifen.
Überschreiben von Methoden
Die Methode show() der Klasse ExtendShow überschreibt die gleichnamige Methode der Oberklasse. Dies bedeutet, dass die Implementierung der Methode der
Oberklasse durch eine neue Implementierung der Unterklasse ersetzt wird. Dabei
75
5 Objektorientierte Programmierung in Java
müssen Signatur und Rückgabetyp dieselben sein. Überschreibende Methoden
besitzen ihre eigenen Zugriffsangaben. Eine in der Oberklasse als protected deklarierte Methode kann wieder als protected redeklariert werden6 , oder sie wird
mit dem Modifizierer public erweitert. Der Gültigkeitsbereich kann aber nicht
z.B durch private eingeschränkt werden. (Eine Begründung dafür findet man in
Arnold & Gosling [1], S. 66.)
Wenn eine Methode von einem Objekt aufgerufen wird, dann bestimmt immer
der tatsächliche Typ des Objektes, welche Implementierung benutzt wird. Bei
einem Zugriff auf ein Datenfeld wird jedoch der deklarierte Typ der Referenz verwendet. Daher erhalten wir folgende Ausgabe beim Aufruf der main-Methode:
> java ExtendShow
Extend.show: ExtendStr
Extend.show: ExtendStr
sup.str = SuperStr
ext.str = ExtendStr
Die Objektreferenz super
Das Schlüsselwort super kann in allen objektbezogenen Methoden und Konstruktoren verwendet werden. In Datenfeldzugriffen und Methodenaufrufen stellt
es eine Referenz zum aktuellen Objekt als eine Instanz seiner Oberklasse dar.
Wenn super verwendet wird, so bestimmt der Typ der Referenz über die Auswahl der zu verwendenden Methodenimplementierung. Wir illustrieren dies wieder an einem Beispielprogramm.
public class T1 {
protected int x = 1;
protected String s() {
return "T1";
}
}
public class T2 extends T1 {
protected int x = 2;
protected String s() {
return "T2";
}
protected void test() {
System.out.println("x= "+x);
6
Dies ist die übliche Vorgehensweise.
76
5.11 Konstruktoren in Unterklassen
System.out.println("super.x= "+super.x);
System.out.println("((T1)this).x= "+((T1)this).x);
System.out.println("s(): "+s());
System.out.println("super.s(): "+super.s());
System.out.println("((T1)this).s(): "+((T1)this).s());
}
public static void main(String[] args) {
new T2().test();
}
}
> java T2
x= 2
super.x= 1
((T1)this).x= 1
s(): T2
super.s(): T1
((T1)this).s(): T2
5.11 Konstruktoren in Unterklassen
In Konstruktoren der Unterklasse kann direkt einer der Oberklassenkonstruktoren
mittels des super() Konstruktes aufgerufen werden.
Achtung: Der super-Aufruf muss die erste Anweisung des Konstruktors sein!
Wird kein Oberklassenkonstruktor explizit aufgerufen, so wird der parameterlose
Konstruktor der Oberklasse automatisch aufgerufen, bevor die Anweisungen des
neuen Konstruktors ausgeführt werden. Verfügt die Oberklasse nicht über einen
parameterlosen Konstruktor, so muss ein Konstruktor der Oberklasse explizit mit
Parametern aufgerufen werden, da es sonst einen Fehler bei der Übersetzung
gibt.
Ausnahme: Wird in der ersten Anweisung eines Konstruktors ein anderer Konstruktor derselben Klasse mittels this aufgerufen, so wird nicht automatisch der parameterlose Oberklassenkonstruktor aufgerufen.
Java liefert einen voreingestellten parameterlosen Konstruktor für eine erweiternde Klasse, die keinen Konstruktor enthält. Dieser ist äquivalent zu:
77
5 Objektorientierte Programmierung in Java
public class ExtendedClass extends SimpleClass {
public ExtendedClass () {
super();
}
}
Der voreingestellte Konstruktor hat dieselbe Sichtbarkeit wie seine Klasse.
Ausnahme: Enthält die Oberklasse keinen parameterlosen Konstruktor, so muss
die Unterklasse mindestens einen Konstruktor bereitstellen.
5.12 Reihenfolgeabhängigkeit von Konstruktoren
Wird ein Objekt erzeugt, so werden zuerst alle seine Datenfelder auf voreingestellte Werte initialisiert. Jeder Konstruktor durchläuft dann drei Phasen:
• Aufruf des Konstruktors der Oberklasse.
• Initialisierung der Datenfelder mittels der Initialisierungsausdrücke.
• Ausführung des Rumpfes des Konstruktors.
Beispiel 5.12.1
public class X {
protected String infix = "fel";
protected String suffix;
protected String alles;
public X() {
suffix = infix;
alles = verbinde("Ap");
}
public String verbinde(String original) {
return (original+suffix);
}
}
public class Y extends X {
protected String extra = "d";
public Y() {
suffix = suffix+extra;
78
5.13 Abstrakte Klassen und Methoden
alles = verbinde("Biele");
}
public static void main(String[] args) {
new Y();
}
}
Die Reihenfolge der Phasen ist ein wichtiger Punkt, wenn während des Aufbaus
Methoden aufgerufen werden (wie im obigen Beispiel). Wenn man eine Methode aufruft, erhält man immer die Implementierung dieser Methode für den derzeitigen Objekttyp. Verwendet die Methode Datenfelder des derzeitigen Typs,
dann sind diese vielleicht noch nicht initialisiert worden. Die folgende Tabelle
zeigt die Inhalte der Datenfelder beim Aufruf der main-Methode (d.h. des YKonstruktors).
Schritt
0
1
2
3
4
5
6
Aktion
infix extra
Datenfelder auf Voreinstellungen
Y-Konstruktor aufgerufen
X-Konstruktor aufgerufen
X-Datenfeld initialisiert
fel
X-Konstruktor ausgeführt
fel
Y-Datenfeld initialisiert
fel
d
Y-Konstruktor ausgeführt
fel
d
suffix alles
fel
fel
feld
Apfel
Apfel
Bielefeld
Die während des Objektaufbaus aufgerufenen Methoden sollten unter Beachtung dieser Faktoren entworfen werden. Auch sollte man alle vom Konstruktor aufgerufenen Methoden sorgfältig dokumentieren, um diejenigen, die den
Konstruktor überschreiben möchten, von den potentiellen Einschränkungen in
Kenntnis zu setzen.
5.13 Abstrakte Klassen und Methoden
Ein sehr nützliches Merkmal der objektorientierten Programmierung ist das der
abstrakten Klasse. Mittels abstrakter Klassen können Klassen deklariert werden,
die nur einen Teil der Implementierung definieren und erweiternden Klassen die
spezifische Implementierung einiger oder aller Methoden überlassen. Abstraktion
ist hilfreich, wenn Teile des Verhaltens für alle oder die meisten Objekte eines
gegebenen Typs richtig sind, es aber auch Verhalten gibt, das nur für bestimmte
Objekte sinnvoll ist und nicht für alle. Es gilt:
• eine abstrakte Methode hat keinen Rumpf;
79
5 Objektorientierte Programmierung in Java
• jede Klasse, die eine abstrakte Methode enthält, ist selbst abstrakt und muss
als solche gekennzeichnet werden;
• jede abstrakte Klasse muss mindestens eine abstrakte Methode besitzen;
• man kann von einer abstrakten Klasse keine Objekte erzeugen;
• von einer Unterklasse einer abstrakten Klasse kann man Objekte erzeugen
– vorausgesetzt sie überschreibt alle abstrakten Methoden der Oberklasse
und implementiert diese;
• eine Unterklasse, die nicht alle abstrakten Methoden der Oberklasse implementiert ist selbst wieder abstrakt.
Beispiel 5.13.1 (vgl. Arnold & Gosling [1], S. 72 ff.)
Wir wollen ein Programm zur Bewertung von Programm(teilen) schreiben. Unsere Implementierung weiß, wie eine Bewertung gefahren und gemessen wird,
aber sie kann nicht im voraus wissen, welches andere Programm bewertet werden soll. Die meisten abstrakten Klassen entsprechen diesem Muster: eine Klasse
ist zwar Experte in einem Bereich, doch ein fehlendes Stück kommt aus einer anderen Klasse. In unserem Beispiel ist das fehlende Stück ein Code, der bewertet
werden muss. Eine solche Klasse könnte wie folgt aussehen:
public abstract class Benchmark {
public abstract void benchmark();
public long repeat(int count) {
long start = System.currentTimeMillis();
for(int i=0; i<count; i++)
benchmark();
return (System.currentTimeMillis()-start);
}
}
Die Klasse ist als abstract deklariert, weil eine Klasse mit abstrakten Methoden
selbst als abstract deklariert werden muss. Diese Redundanz hilft dem Leser,
schnell zu erfassen, dass die Klasse abstrakt ist, ohne alle Methoden der Klasse
durchzusehen, ob zumindest eine von ihnen abstrakt ist.
Die Methode repeat stellt das Sachwissen zur Bewertung bereit. Sie weiß, wie
der Zeitbedarf für die Ausführung von count Aufrufen des zu bewertenden Codes zu messen ist. Wird die Messung komplizierter (vielleicht durch Messung der
Zeiten jeder Ausführung und Berechnung der Varianz als statistisches Maß darüber), so kann diese Methode verbessert werden, ohne die Implementierung des
speziellen zu bewertenden Codes in einer erweiternden Klasse zu beeinflussen.
80
5.14 Aufgaben
Die abstrakte Methode benchmark muss von jeder selbst nicht wieder abstrakten
Unterklasse implementiert werden. Deshalb gibt es in dieser Klasse keine Implementierung, sondern nur eine Deklaration. Hier nun ein Beispiel einer einfachen
Erweiterung von Benchmark:
public class MethodBenchmark extends Benchmark {
public void benchmark() { }
public static void main(String[] args) {
int count = Integer.parseInt(args[0]);
long time = new MethodBenchmark().repeat(count);
System.out.println(count+" Methodenaufrufe in "+time+
" Millisekunden");
}
}
Die Implementierung von benchmark ist denkbar einfach: die Methode hat einen
leeren Rumpf. Man kann daher den Zeitbedarf von n Methodenaufrufen feststellen, indem man die main-Methode der Klasse MethodBenchmark mit der Angabe
n der gewünschten Testwiederholungen laufen lässt.
5.14 Aufgaben
Aufgabe 5.14.1 Eine Folge heißt Schlange (engl. queue), wenn Elemente eines
gegebenen Datentyps T nur am Ende eingefügt und am Anfang entfernt werden
dürfen (FIFO-Prinzip: first in first out).
In Analogie zum abstrakten Datentypen Stack sollen Sie hier einen abstrakten
Datentypen Queue spezifizieren, der folgende Operationen enthält:
initQueue: Erzeugen einer leeren Schlange.
enqueue: Einfügeoperation.
dequeue: Entfernt das vorderste Element.
peek: Liefert das vorderste Element der Schlange, ohne die Schlange zu verändern.
empty: Liefert true gdw. die Schlange leer ist.
Die Operationen sind durch Gleichungen zu spezifizieren.
Hinweis: Fallunterscheidungen über Schlangen mit nur einem Element und Schlangen mit mindestens zwei Elementen sind hilfreich.
Aufgabe 5.14.2 Implementieren Sie eine Klasse Rent in Java, die die Klasse Stack
benutzt. Nehmen Sie an, Herr Meier ist Besitzer eines Buches. Herr Meier, der Eigentümer, wird im ersten Eintrag des Stapels beschrieben. Leiht jemand das Buch
aus, vielleicht Herr Schmidt, wird dessen Name auf dem Stapel abgelegt. Verleiht
81
5 Objektorientierte Programmierung in Java
Herr Schmidt es wiederum weiter, z.B. an Herrn Müller, erscheint dessen Name
an der Spitze des Stapels, usw. Wird das Buch an seinen Vorgänger zurückgegeben, wird der Name des Entleihers vom Stapel entfernt. Z.B. wird der Name
Müller vom Stapel entfernt, wenn er das Buch Herrn Schmidt zurückgibt. Der
letzte Name wird nie aus dem Stapel entfernt, denn sonst ginge die Information
über den Bucheigentümer verloren.
Hinweis: Die Klassen Stack und Rent müssen sich im selben Verzeichnis befinden.
Aufgabe 5.14.3 (a) Harry Hacker hat wieder einmal programmiert, ohne genau
nachzudenken. Er wollte mit der folgenden Methode einen Stack kopieren (d.h.
einen neuen Stack mit den gleichen Werten kreieren):
public static Stack copy(Stack stack) {
Stack cpStack = stack;
return cpStack;
}
Was hat Harry nicht bedacht? Und wie kann man Harry helfen? Schreiben Sie
in Java eine Methode betterCopy, die den ursprünglichen Gedanken von Harry erfüllt. Ergänzen Sie ebenfalls eine main-Methode, in der die beiden copyMethoden aufgerufen werden, so dass der Unterschied deutlich wird.
(b) Implementieren Sie statt der klassenbezogenen Methode betterCopy eine
objektbezogene Methode gleichen Namens, die dasselbe leistet.
Aufgabe 5.14.4 Objektorientierte Programmierung ermöglicht eine relativ einfache Modellierung von Ausschnitten der realen Welt. In dieser Aufgabe sollen Sie
eine Klasse Vehicle implementieren, die zwei Unterklassen enthält: (i) motorgetriebene Fahrzeuge (Motorrad, Auto, Bus, LKW, ...) und (ii) personengetriebene
Fahrzeuge (Fahrrad, Tretroller, Inliner, ...). Diese Klassen sollen wiederum Unterklassen besitzen. Z.B. kann man die motorgetriebenen Fahrzeuge in zweirädrige,
vierrädrige und mehr-als-vierrädrige Fahrzeuge unterteilen. Modellieren Sie Fahrzeuge in sinnvoller Klassenhierarchie. Obligatorisch sind folgende Attribute und
Methoden:
(a) Die Klasse Vehicle sollte mindestens Datenfelder für die aktuelle Geschwindigkeit, die aktuelle Richtung in Grad, den Preis und den Besitzernamen enthalten.
(b) Eine Klasse EnginePoweredVehicle soll mindestens Datenfelder über die Leistung in kW, Front- oder Heckantrieb und Höchstgeschwindigkeit besitzen.
(c) Die Klasse PersonPoweredVehicle soll mindestens ein Datenfeld besitzen,
das Auskunft über die Anzahl der Personen gibt, die das Fahrzeug antreiben.
(d) Es sollen Klassen Car, Bus, Truck, Bike, Motorbike, Inliner und Scooter
geben. Alle besitzen ein Datenfeld für eine eindeutige Identifikationsnummer.
82
5.14 Aufgaben
(e) Schreiben Sie Methoden, die die einzelnen Eigenschaften verändern könnnen. Z.B. sollte die Klasse EnginePoweredVehicle eine Methode besitzen, die
es ermöglicht, die Höchstgeschwindigkeit zu setzen (verändern). Jede Klasse soll
mindestens zwei Methoden enthalten!
(f) Schreiben Sie schließlich eine Klasse SomeVehicles mit einer main-Methode,
die sechs Fahrzeuge konstruiert. Darauf sollen jeweils mindestens zwei Methoden angewendet werden.
(g) Wenn diese Aufgabe Sie unterfordert, brechen Sie die Übung ab. Hauptsache,
Sie haben das Prinzip verstanden.
Aufgabe 5.14.5 Schreiben Sie eine Klasse LinkedList, die ein Datenfeld vom
Typ Object und eine Referenz zum nächsten LinkedList-Element in der Liste
enthält. Schreiben Sie zusätzlich für Ihre Klasse LinkedList eine main-Methode,
die einige Objekte vom Typ Vehicle erzeugt und sie aufeinanderfolgend in die
Liste einfügt. Können Sie mit Ihrer Implementierung eine leere Liste erzeugen?
Aufgabe 5.14.6 Sie haben schon die Klasse Circle kennengelernt, die drei Datenfelder besaß: die Koordinaten x und y, die den Mittelpunkt eines Kreises angeben, und eine Variable r, die den Radius enthält.
(a) Schreiben Sie analog dazu ein Klasse Rectangle, die vier Datenfelder besitzt.
Je zwei Koordinaten x1 und y1, sowie x2 und y2, beschreiben die Endpunkte der
Diagonalen eines Rechtecks, das damit vollständig beschrieben ist.
(b) Schreiben Sie eine abstrakte Klasse Shape, die die beiden abstrakten Methoden area (berechnet den Inhalt eines geometrischen Objekts) und circumference
(berechnet den Umfang eines geometrischen Objekts) beinhaltet.
(c) Die Klassen Circle und Rectangle sollen als erweiternde Klassen von Shape
implementiert werden.
(d) Schreiben Sie dann noch eine main Methode, in der ein Array von ShapeObjekten der Länge 5 konstruiert wird, das Circle- und/oder Rectangle-Objekte
enthalten kann. Dann soll das Array mit 5 entsprechenden Objekten gefüllt werden und der Gesamtumfang bzw. der Gesamtflächeninhalt aller Objekte ausgegeben werden.
83
5 Objektorientierte Programmierung in Java
84
6 Übergang von funktionaler zu OOP
Nachdem wir nun mit Java und seinen objektorientierten und imperativen Anteilen einigermaßen vertraut sind, ist es an der Zeit
für einen Vergleich zwischen diesen Programmierkonzepten und der
funktionalen Programmierung, wie wir sie bei Haskell kennengelernt
haben. Zunächst wollen wir allgemein funktionale und imperative
Programmierung anhand einiger Beispiele gegenüberstellen. Dann
gehen wir konkreter auf die Unterschiede zwischen den beiden Sprachen Haskell und Java ein. Diese Gegenüberstellung beginnt mit
einem ausführlichen Vergleich einer Java-Implementierung des abstrakten Datentyps „Liste“ mit den vordefinierten Listen in Haskell.
Danach werden weitere Punkte diskutiert, in denen sich die beiden
Sprachen unterscheiden, und es werden, wo dies möglich ist, Techniken angegeben, wie die Konzepte der einen Sprache in der anderen
nachempfunden werden können.
6.1 Imperative vs. funktionale Programmierung
Plakativ lassen sich folgende Aussagen treffen:
funktional: Berechnung von Werten von Ausdrücken
imperativ: Berechnung des Kontrollflusses
Beispiele zur Unterscheidung: Java vs. Haskell
1. Berechnung des Betrages einer ganzen Zahl n:
public static int abs(int n) {
if (n >= 0)
return n;
else
return -n;
}
> abs’ :: (Ord a, Num a) => a -> a
> abs’ n | n >= 0
= n
>
| otherwise = -n
85
6 Übergang von funktionaler zu OOP
2. Berechnung von sum(n) =
n
X
i2
i=1
public static int sum(int n) {
int s = 0;
for(int i=1; i<=n; i++)
s = s + i * i;
return s;
}
> sum’ :: (Enum a, Num a) => a -> a
> sum’ n = foldl g 0 [1..n]
>
where g x y = x + y * y
3. Berechnung von nextSquare(n) = min{q | q > n, q = s2 , s ∈ N}
public static int nextSquare(int n) {
int i = 1;
int q = 1;
while (q <= n) {
i++;
q = i*i;
}
return q;
}
> nextSquare’ :: (Num a, Enum a) => a -> a
> nextSquare’ n = (head.dropWhile (<=n).map (^2)) [1..]
86
6.2 Listen in Java
Es gibt folgende Entsprechungen zwischen beiden Welten:
funktional
imperativ
• Liste von Zwischenergebnissen
(anonym)
• Wertabfolgen in Behältern (benannt)
• Listentyp [t]
• Behältertyp t
• Rekursionsschemata (foldl, map)
• spezielle Anweisungen (while,
for)
• abstrahiert von Zwischenergebnissen
• Behälter werden weiterbenutzt
• Speicheraufwändig
• Speicherökonomisch
• Listen spielen eine zentrale Rolle
• Listen sind ein Datentyp wie viele
andere
6.2 Listen in Java
Wir geben beispielhaft eine (der vielen möglichen) Implementierungen von (Haskell) Listen in Java an. Der Typ der Listenelemente ist int. Unsere Klasse IntList
benutzt die Klasse Node.
public class Node {
protected int element;
protected Node next;
public Node(int val, Node node) {
element = val;
next = node;
}
public int element() {
return element;
}
public Node next() {
return next;
}
}
87
6 Übergang von funktionaler zu OOP
Knoten werden nun folgendermaßen zu Listen verknüpft:
[1,2] =
ˆ 1r 2r Die Konstruktoren von IntList
public class IntList {
private Node first = null;
public IntList() { }
private IntList(Node first) {
this.first = first;
}
}
IntList() liefert also eine leere Liste, während IntList(Node first) einen
Knoten (dessen next-Datenfeld wiederum auf einen anderen Knoten zeigen kann)
in eine Liste verwandelt (es handelt sich also um eine Typkonversion).
public IntList ()
first
private IntList (Node first)
first
2 ··· n 1 r r r r Die Methode empty
/** Tests whether a list is empty. */
public boolean empty() {
return first == null;
}
Die Methode cons
/** cons builds up lists. Returns a new reference to the list
cons(val,xs), does not change xs. In order to emphasize that cons
doesn’t change the list it acts upon, it is declared static. */
88
6.2 Listen in Java
public static IntList cons(int val, IntList xs) {
Node n = new Node(val, xs.first);
return new IntList(n);
}
Beispiel 6.2.1 cons(x,xs) wobei in (a) xs leer ist und in (b) xs der Liste [2,3]
entspricht.
(a)
return value
xs.first
(b)
xs.first
3 2 r r xs.first
x r xs.first
return value 2 3 r
r x
r Die Methoden head und tail
Die Funktionen head und tail sind in Haskell folgendermaßen definiert:
head (x:xs) = x
tail (x:xs) = xs
Da die Funktionen in Haskell partiell definiert sind, haben wir freie Wahl in der
Implementierung von head [] und tail []. In unserer Implementierung in Java
wird eine Fehlermeldung ausgegeben und der Wert −1 bzw. null zurückgegeben. (Das Auslösen einer Ausnahme wäre besser; vgl. Abschnitt 7.3).
/** Returns the first element of a list. Returns -1 and prints an
error message if the list is empty; does not change the list. */
public int head() {
if (empty()) {
System.out.println("Error at method head(): Empty List");
return -1;
// an exception would be better
}
return first.element();
}
89
6 Übergang von funktionaler zu OOP
/** Returns a new reference to the list obtained by removing the
first element. Returns null and prints an error message if the
list is empty; does not change the list. */
public IntList tail() {
if (empty()) {
System.out.println("Error at method tail(): Empty List");
return null;
// an exception would be better
}
return new IntList(first.next());
}
Die Methode append
Die Definition von append lautet in Haskell:
append [] ys
= ys
append (x:xs) ys = x:append xs ys
Wir betrachten zwei Versionen von append. Eine ist rekursiv und eine ist nicht
rekursiv definiert.
/** Returns a new reference to the list obtained by concatenation
of xs and ys; does not change the lists. In order to
emphasize this, it is declared static. */
public static IntList append(IntList xs,IntList ys) {
if (xs.empty())
return ys;
else
return cons(xs.head(),append(xs.tail(),ys));
}
private static IntList append2(IntList xs,IntList ys) {
Node tmp;
if (xs.empty())
return ys;
else {
for(tmp=xs.first; tmp.next!=null; tmp=tmp.next)
; // Find last node
tmp.next = ys.first;
90
6.2 Listen in Java
return xs;
}
}
Beispiel 6.2.2
ys.first
xs.first
2 1 r r 4 5 3 r r r zs = append(xs,ys) bzw. zs = append2(xs,ys)
append (rekursiv)
append2 (nicht rekursiv)
@
@
(nicht desktruktive Variante)
(desktruktive Variante)
@
@
R
@
xs.first
2 1 r r ys.first
zs.first
3 4 5 r
r
r 1
2
r r ys.first
xs.first
3 4 5 1 2 r r r r r zs.first
Das Verhalten von append2 ist destruktiv, denn beim Verketten von xs und ys
wird xs zerstört (oder milder ausgedrückt, verändert). Diesen Seiteneffekt muss
man bei jeder Anwendung von append2 berücksichtigen! Aber auch die erste
Version birgt eine Gefahr: Wenn nach zs = append(xs,ys) die Liste ys verändert wird, verändert sich damit auch zs.
91
6 Übergang von funktionaler zu OOP
Beispiel 6.2.3 Sonderfall xs = ys
xs.first
2 1 r r zs = append(xs,xs) bzw. zs = append2(xs,xs)
append
append2
@
@
@
R
@
xs.first
zs.first
1 2 r
r 1
2
r r xs.first
1 2
r r
6
zs.first Im Fall von append2 erhält man also eine „endlose“ Liste ohne terminierende
null-Referenz.
6.3 Vergleich zwischen Haskell und Java
1) Parametrischer Typpolymorphismus
Die Haskell-Funktion swap ist definiert durch:
swap (x,y) = (y,x) (der Typ ist (a,b) -> (b,a))
Beispielsweise ist (1,"a") das Ergebnis von swap("a",1).
Um diese Funktion in Java zu implementieren, benutzen wir den Typ Object.
public class Pair {
protected Object x;
protected Object y;
public Pair(Object x, Object y) {
this.x = x;
this.y = y;
}
92
6.3 Vergleich zwischen Haskell und Java
public void swap() {
Object tmp = x;
x = y;
y = tmp;
}
public static void main(String[] args) {
Pair p = new Pair("a",new Integer(1));
p.swap();
System.out.println(((Integer)(p.x)).intValue()+(String)p.y);
}
}
Da elementare Daten keine Objekte sind, müssen sie in Objekte verwandelt
werden. Dies geschieht mit Hilfe der Hüllenklassen – elementare Daten
werden durch einen Hüllenklassenkonstruktor “eingehüllt” und damit zu
Objekten.
Object
H
HH
?
Boolean
Char
HH
j
Number
)
Float
Double
PP
PP
@
PP
@
R
@
q
P
Integer
Long
A BBILDUNG 6.1: Klassenhierarchie der Hüllenklassen
Die Klassenhierarchie bezüglich der Hüllenklassen ist in Abbildung 6.1 dargestellt. Nachteil dieser Implementierung sind die vielen notwendigen expliziten Typkonversionen. Wenn man die Methode swap z.B. nur auf intZahlen benötigt, kann man folgende Implementierung ohne Typkonversionen wählen.
public class IntPair {
protected int x;
protected int y;
public IntPair(int x, int y) {
this.x = x;
this.y = y;
93
6 Übergang von funktionaler zu OOP
}
public void swap() {
int tmp = x;
x = y;
y = tmp;
}
}
Man muss unter Umständen aber für jeden elementaren Datentyp eine
Version der Klasse Pair erstellen. Dies widerspricht natürlich dem Wiederverwendungsgedanken, denn es wird viel Code dupliziert.
Seit Java 5 können Klassen mit Typparametern definiert werden. Solche
Klassen werden Generische Klassen oder Parametrisierte Klassen genannt.
Der Typparameter wird in spitzen Klammern ’<>’ nach dem Klassennamen
angegeben. Generische Klassen werden wie andere Klassen auch benutzt,
jedoch muss bei der Instanziierung der Typ angegeben werden
Beispiel 6.3.1 Die Klasse Pair mit Generic Types:
class Pair<E> {
protected E x;
protected E y;
public Pair(E x, E y) {
this.x = x;
this.y = y;
}
public void swap() {
E tmp = x;
x = y;
y = tmp;
}
public static void main(String[] args) {
Pair<Integer> intPair = new Pair<Integer>(1,2);
Pair<String> charPair = new Pair<String>("a","b");
intPair.swap();
charPair.swap();
System.out.println(intPair.x + "," + intPair.y);
System.out.println(charPair.x + "," + charPair.y);
}
}
94
6.3 Vergleich zwischen Haskell und Java
2) Funktionen höherer Ordnung
In Haskell gibt es keinen Unterschied zwischen Funktionen und Daten:
Funktionen können Argumente anderer Funktionen sein, Funktionen können Funktionen als Wert liefern etc. In Java sind Funktionen (Methoden)
Bestandteil von Objekten. Da Funktionen Objekte als Argumente oder Rückgabewert haben können, unterstützt Java insofern Funktionen höherer Ordnung.
3) lokale Änderung großer Datenstrukturen
• ist in Java ohne weiteres möglich;
• in Haskell simulierbar durch Monaden1 .
4) Klassen, Objekte und Vererbung
• in Java kein Problem (objektorientierte Programmiersprache);
• Haskell-Klassen definieren abstrakte Methoden, jedoch keine Objekte. Eine Instanz einer Klasse muss diese abstrakten Methoden implementieren (d.h. eine eigene Definition angeben). Insofern entspricht
eine Haskell-Klasse grob gesprochen einer abstrakten Klasse in Java,
die nur abstrakte Methoden enthält (genauer einer Schnittstelle).
5) Keine Entsprechung gibt es z.B.
• in Java für die unendlichen Datenstrukturen in Haskell
• in Haskell für die von Java unterstützte Nebenläufigkeit
6) Algebraische Datentypen und Pattern Matching
Pattern Matching kann nach einer Idee von Odersky & Wadler (1997) auf
sehr elegante Art und Weise simuliert werden. Wir demonstrieren dies an
Hand der append-Funktion auf Listen.
public class List {
protected static final int NIL_TAG = 0;
protected static final int CONS_TAG = 1;
protected int tag;
public List append(List ys) {
switch (this.tag) {
case NIL_TAG:
return ys;
case CONS_TAG:
char x = ((Cons)this).head;
1
Monaden stellen in Haskell eine Möglichkeit dar, imperativ zu programmieren, d.h. es wird ein
Kontrollfluss simuliert.
95
6 Übergang von funktionaler zu OOP
List xs = ((Cons)this).tail;
return new Cons(x, xs.append(ys));
default:
return new Nil(); //an exception would be better
}
}
}
public class Cons extends List {
protected char head;
protected List tail;
public Cons(char head, List tail) {
this.tag = CONS_TAG;
this.head = head;
this.tail = tail;
}
}
public class Nil extends List {
public Nil() {
this.tag = NIL_TAG;
}
}
6.4 Aufgaben
Aufgabe 6.4.1 In der Vorlesung Algorithmen und Datenstrukturen I haben Sie
das folgende Haskell-Programm für Insertion-Sort kennengelernt:
isort :: [Integer] -> [Integer]
isort []
= []
isort (a:x) = insert a (isort x)
insert :: Integer -> [Integer] -> [Integer]
insert a []
= [a]
insert a (b:x) = if a<=b then a:b:x
else b:insert a x
(a) Schreiben Sie ein Java-Programm InsertionSort, das eine Folge von intZahlen einliest, diese gemäß obiger Spezifikation sortiert und dann wieder ausgibt.
(b) Implementieren Sie ein nicht rekursives Programm, das dasselbe leistet.
(c) Bestimmen Sie die Komplexität Ihrer Implementierungen.
96
6.4 Aufgaben
Aufgabe 6.4.2 Das Haskell-Programm reverse
reverse :: [a] -> [a]
reverse
[] = []
reverse (x:xs) = reverse xs ++ [x]
ist Ihnen ebenfalls aus Algorithmen und Datenstrukturen I bekannt.
(a) Implementieren Sie reverse in Java. Welche Komplexität hat Ihr Programm?
(b) Implementieren Sie ein nicht rekursives Programm der Komplexität O(n), das
dasselbe leistet.
Aufgabe 6.4.3 Schreiben Sie eine Methode length, die die Länge einer Liste
vom Typ IntList in konstanter Zeit liefert. Dabei ist es erlaubt, Objekte vom
Typ IntList um weitere Datenfelder zu erweitern.
Aufgabe 6.4.4 Eine einfach verkettete Liste haben Sie bereits kennengelernt. In
dieser Aufgabe geht es um doppelt verkettete Listen. Eine doppelt verkettete Liste ist ein Liste, deren Knoten nicht nur auf den nächsten Knoten zeigen, sondern
auch auf den vorherigen (vgl. Cormen et al. [2], S. 204 ff.).
Die abstrakte Klasse Dictionary (genauer java.util.Dictionary) enthält abstrakte Methoden zur Speicherung und Ermittlung von Elementen, die über einen
Schlüssel indiziert werden (vgl. auch Kapitel 9). Die Methoden von Dictionary
sind:
public abstract Object put(Object key, Object element);
Legt element im Dictonary unter key ab. Das alte unter dem Schlüssel
gespeicherte Element wird zurückgegeben. Falls es keins gab, wird null
zurückgegeben.
public abstract Object get(Object key);
Es wird das mit dem Schlüssel key assoziierte Objekt aus dem Dictionary
zurückgegeben. Wenn der Schlüssel nicht definiert ist, wird null zurückgegeben.
public abstract Object remove(Object key);
Der zu key passende Eintrag wird aus dem Dictionary entfernt, und das
Element zum Schlüssel key wird zurückgegeben. Wenn es key nicht gibt,
wird null zurückgegeben.
public abstract int size();
Es wird die Anzahl der im Dictionary definierten Einträge zurückgegeben.
public abstract boolean isEmpty();
Liefert true gdw. das Dictionary keine Einträge enthält.
97
6 Übergang von funktionaler zu OOP
public abstract Enumeration keys();
Es wird eine Aufzählung der Schlüssel im Dictionary zurückgegeben.
public abstract Enumeration elements();
Es wird eine Aufzählung der Elemente aus dem Dictionary zurückgegeben.
Schreiben Sie eine Klasse DoLiList, die die ersten fünf abstrakten Methoden von
Dictionary mit Hilfe von doppelt verketteten Listen implementiert.
Aufgabe 6.4.5 Implementieren Sie unter Zuhilfenahme verketteter Listen eine
Klasse
(a) Stack, die einen Stack implementiert und
(b) Queue, die eine Queue implementiert.
Aufgabe 6.4.6 Harry Hacker hat ebenfalls Listen in Java implementiert. Seine
Klasse IntList sieht wie folgt aus:
public class IntList {
private Node first = null;
public IntList() { }
public void cons(int val) {
first = new Node(val,first);
}
public int head() {
if (first == null) {
System.out.println("Error at method head(): Empty List");
return -1;
}
return first.element();
}
public void tail() {
if (first == null)
System.out.println("Error at method tail(): Empty List");
else
first = first.next();
}
public void append(IntList ys) {
if (first == null)
first = ys.first;
98
6.4 Aufgaben
else {
int hd = head();
tail();
append(ys);
cons(hd);
}
}
}
Lisa Lista kennt sich jedoch mit Listen bestens aus und sieht sofort, dass zumindest die Implementierung einer Methode die entsprechende funktionale Spezifikation nicht erfüllt. Was geht schief?
99
6 Übergang von funktionaler zu OOP
100
7 Programmieren im Großen
Die sinnvolle Strukturierung des zu entwickelnden Programmcodes
ist eine der Grundvoraussetzungen für die erfolgreiche Durchführung größerer Softwareprojekte. Dieses Kapitel soll zeigen, wie in
Java über Klassen und Vererbung hinaus die strukturierte Programmierung mit Hilfe von Schnittstellen und Paketen unterstüzt wird.
Zur einheitlichen Fehlerbehandlung, ohne den Programmcode mit
bedingten Abfragen zu überfrachten, gibt es eine spezielle Ausnahmebehandlung in Java, die wir ebenfalls kennenlernen werden.
7.1 Schnittstellen
Es sei die in Abbildung 7.1 dargestellte Klassenhierarchie gegeben (vgl. 5.14.6).
Shape
Circle
?
GraphicCircle
Object
H
HH
HH
j
Drawable
@
@
R
@
Rectangle
?
GraphicRec
A BBILDUNG 7.1: Eine Klassenhierarchie
Bei Instanzen der Klassen GraphicCircle und GraphicRec handelt es sich um
Objekte, die man zeichnen kann. Um diese “zeichenbaren” Objekte einheitlich behandeln zu können, wäre es wünschenswert, eine abstrakte Oberklasse
Drawable der beiden Klassen zu haben.
Problem: Es gibt nur Einfachvererbung in Java.
Lösung: Schnittstellen (engl. Interfaces).
101
7 Programmieren im Großen
Eine Schnittstelle kann man sich als abstrakte Klasse vorstellen, die nur abstrakte
objektbezogene Methoden enthält. Während eine abstrakte Klasse auch nichtabstrakte Methoden definieren darf, sind in einer Schnittstelle alle Methoden
implizit abstrakte Methoden. Neben abstrakten Methoden darf eine Schnittstelle
nur Konstanten enthalten.
Beispiel 7.1.1 Eine Schnittstellendeklaration:
import java.awt.Graphics;
public interface Drawable {
public void draw(Graphics g);
}
Eine Klasse darf gleichzeitig eine andere Klasse erweitern und (evtl. mehrere)
Schnittstellen implementieren, wie folgendes Beispiel (vgl. Abschnitt 5.9) zeigt.
Beispiel 7.1.2
import java.awt.Color;
import java.awt.Graphics;
public class GraphicCircle extends Circle implements Drawable {
protected Color outline;
// Farbe der Umrandung
protected Color fill;
// Farbe des Inneren
public GraphicCircle(int x,int y,int r,Color outline,Color fill) {
super(x,y,r);
this.outline = outline;
this.fill = fill;
}
public void draw(Graphics g) {
g.setColor(fill);
g.fillOval(x-r, y-r, 2*r, 2*r);
g.setColor(outline);
g.drawOval(x-r, y-r, 2*r, 2*r);
}
}
Eine Schnittstelle ist ein Ausdruck reinen Entwurfs, wohingegen eine (abstrakte) Klasse eine Mischung aus Entwurf und Implementierung ist. Schnittstellen
können auch mit Hilfe von extends erweitert werden. Im Gegensatz zu Klassen können sie mehr als eine Schnittstelle erweitern. Die Menge der Obertypen
einer Klasse besteht aus der von ihr erweiterten Klasse und den von ihr implementierten Schnittstellen einschließlich der Obertypen dieser Klasse und dieser
102
7.1 Schnittstellen
Schnittstellen. Der Typ eines Objektes ist also nicht nur seine Klasse, sondern
auch jeder seiner Obertypen einschließlich der Schnittstellen. Das folgende Beispiel demonstriert dies.
Beispiel 7.1.3
import java.applet.Applet;
import java.awt.Color;
import java.awt.Graphics;
public class DemoShape extends Applet {
public void paint(Graphics g) {
Shape[] shapes = new Shape[3];
Drawable[] drawables = new Drawable[3];
Drawable gc = new GraphicCircle(300,200,200,Color.red,
Color.blue);
Drawable gr1 = new GraphicRec(450,200,100,300,Color.green,
Color.yellow);
Drawable gr2 = new GraphicRec(50,400,300,100,Color.black,
Color.magenta);
shapes[0] = (Shape) gc;
shapes[1] = (Shape) gr1;
shapes[2] = (Shape) gr2;
drawables[0] = gc;
drawables[1] = gr1;
drawables[2] = gr2;
double totalArea = 0;
for(int i=0; i<shapes.length; i++) {
totalArea = totalArea+shapes[i].area();
drawables[i].draw(g);
}
Double total = new Double(totalArea);
String str = "Total area = "+total.toString();
g.setColor(Color.black);
g.drawString(str,100,550);
}
}
103
7 Programmieren im Großen
7.1.1 Beispiel: Die vordefinierte Schnittstelle Enumeration
Die Schnittstelle Enumeration dient zur Aufzählung aller Elemente eines Datentyps. Sie deklariert zwei Methoden:
public abstract boolean hasMoreElements()
liefert true zurück, wenn die Aufzählung noch mehr Elemente (als bisher
aufgezählt) enthält. Sie darf auch mehr als einmal zwischen aufeinanderfolgenden Aufrufen von nextElement() aufgerufen werden.
public abstract Object nextElement()
gibt das nächste Element der Aufzählung zurück. Aufrufe dieser Methode
zählen aufeinanderfolgende Elemente auf. Wenn keine weiteren Elemente
existieren, wird NoSuchElementException ausgelöst (vgl. Abschnitt 7.3).
Beispiel 7.1.4 Wir wollen alle Elemente eines Stacks aufzählen. Die Klasse Stack
(vgl. Abschnitt 5.7) benutzt dazu die Klasse StackEnum.
import java.util.Enumeration;
class StackEnum implements Enumeration {
private Stack st;
private int pos;
protected StackEnum(Stack stack) {
st = stack;
pos = stack.top;
}
public boolean hasMoreElements() {
return (pos != -1);
}
public Object nextElement() {
if(pos != -1)
return st.stack[pos--];
else
return null; // an exception would be better
}
}
Man beachte, dass die Deklaration der Klasse StackEnum keinen Gültigkeitsmodifizierer enthält. Damit erhält diese Klasse automatisch (default) den Gültigkeitsbereich package. Desweiteren ist zu beachten, dass die Datenfelder stack und
104
7.2 Pakete
top der Klasse Stack im Abschnitt 5.7 als private deklariert wurden. D.h. sie
sind in der Klasse StackEnum nicht zugreifbar. Aus diesem Grund müssen diese
Datenfelder z.B. als protected deklariert werden.
Die Klasse Stack muss nun um die Methode elements erweitert werden.
public Enumeration elements() {
return new StackEnum(this);
}
Die Anwendung der Methode erfolgt dann durch das Erstellen eines neuen Objektes vom Typ Enumeration.
Enumeration e = stack.elements();
while(e.hasMoreElements())
System.out.println(e.nextElement());
Man beachte, dass die Implementierung von elements() völlig verborgen ist und
der Typ StackEnum in der Klasse Stack überhaupt nicht auftaucht (stattdessen
wird der Typ Enumeration der implementierten Schnittstelle benutzt).
Achtung: Die Schnittstelle Enumeration hat keine Schnappschussgarantie. Wird
der Inhalt der Sammlung während der Aufzählung verändert, kann das die von
den Methoden zurückgegebenen Werte beeinflussen. Ein Schnappschuss würde die Elemente so zurückgeben, wie sie waren, als das Enumeration-Objekt
erzeugt wurde.
7.2 Pakete
Pakete und Gültigkeitsbereiche wurden schon kurz in Abschnitt 2.4 behandelt.
Hier folgt nun eine ausführlichere Beschreibung.
Pakete enthalten inhaltlich zusammenhängende Klassen und Schnittstellen. Die
Klassen und Schnittstellen können gebräuchliche öffentliche Namen (wie get und
put) verwenden, denn durch Voranstellen des Paketnamens können eventuelle
Namenskonflikte vermieden werden. Beispielsweise macht es Sinn, die Klassen
Stack und StackEnum in ein Paket stack zu stecken.1 Am Anfang jeder Quelltextdatei, deren Klassen zu dem stack-Paket gehören sollen, muss die Paketdeklaration package stack; stehen.
package stack;
package stack;
public class Stack { ... }
class StackEnum { ... }
1
Paketnamen werden nach Konvention klein geschrieben.
105
7 Programmieren im Großen
Der Paketname ist implizit jedem im Paket enthaltenen Typnamen vorangestellt.
Benötigt außerhalb des Paketes definierter Code innerhalb des Paketes deklarierte Typen, kann er sich auf zwei Arten auf diese beziehen:
1. Voranstellen des Paketnames, z.B. stack.Stack.
2. Importieren (von Teilen) des Paketes durch
import stack.Stack;
oder auch durch Importieren aller zugreifbaren Klassen eines Paketes mit *:
import stack.*;
Der Paket- und Importmechanismus ermöglicht die Kontrolle über möglicherweise in Konflikt geratene Namen. Es gibt z.B. schon eine vordefinierte Java-Klasse
Stack. Diese befindet sich im Paket java.util. Sollen beide Klassen im selben
Quelltext verwendet werden, so kann dies geschehen durch:
• Angabe der voll qualifizierten Namen (stack.Stack und java.util.Stack).
• Importieren von z.B. java.util.Stack oder java.util.* und verwenden
von Stack für java.util.Stack sowie des vollen Namens von stack.Stack
bzw. umgekehrt.
7.2.1 Paketinhalte
Enthält eine Quelltextdatei keine Paketdeklarationen, gehen die in ihr deklarierten Typen in ein unbenanntes Paket ein. Pakete sollen sorgfältig entworfen
werden, damit sie nur von der Funktionalität zusammengehörige Klassen und
Schnittstellen enthalten, denn Klassen in einem Paket können auf die nichtprivaten Attribute und Methoden anderer Klassen frei zugreifen. Pakete können
ineinander geschachtelt werden (z.B. java.util). Die Schachtelung ermöglicht
ein hierarchisches Benennungssystem, stellt aber keinen speziellen Zugriff zwischen Paketen zur Verfügung.
7.2.2 Paketbenennung
Paketnamen sollen einmalig sein. Folgende Konvention soll dies sicherstellen:
package DE.Uni-Bielefeld.TechFak.juser.stack;
Der Code für ein Paket muss sich in einem diesen Namen widerspiegelnden Verzeichnis befinden (auf Details wollen wir hier nicht eingehen).
106
7.3 Ausnahmen (Exceptions)
7.3 Ausnahmen (Exceptions)
Java stellt einen umfangreichen Mechanismus zur Behandlung sog. Ausnahmen
(z.B. Fehlermeldungen) bereit. Wir stellen die Vorteile von Ausnahmen gegenüber der traditionellen Fehlerbehandlung schlagwortartig dar.
Ausnahmen
• sind eine saubere Art, Fehlerprüfungen vorzunehmen, ohne den Quelltext
zu überfrachten;
• vermeiden eine Überflutung des grundlegenden Ablaufs des Programms
mit vielen Fehlerprüfungen;
• zeigen Fehler direkt an, statt Variablen oder Seiteneffekte auf Datenfelder
zu nutzen, die anschließend zu prüfen sind;
• machen Fehlerbedingungen zum expliziten Bestandteil der Spezifikation einer Methode;
• sind daher für Programmierer sichtbar und bei einer Analyse überprüfbar.
Eine Ausnahme ist ein Signal, das auf eine unerwartete Fehlerbedingung hinweist. Eine Ausnahme wird ausgelöst durch das throw-Konstrukt. Die Ausnahme
wird durch ein umgebendes Konstrukt irgendwo entlang der aktuell ablaufenden
Methodenaufrufe abgefangen2 . Wird die Ausnahme nicht abgefangen, tritt die
standardmäßige Ausnahmebehandlung in Kraft.
Die Klassenhierarchie für Ausnahmen ist in Abbildung 7.2 dargestellt. Man unterscheidet zwischen geprüften und ungeprüften Ausnahmen. Es gilt:
• ungeprüfte Ausnahmen erweitern die Klasse Error und RuntimeException
und werden nicht abgefangen;
• geprüfte Ausnahmen erweitern die Klasse Exception (nach allgemeiner
Übereinkunft nicht die Klasse Throwable): der Übersetzer überprüft, dass
Methoden nur die von ihnen deklarierten Ausnahmen auslösen.
Jedes Objekt vom Typ Throwable hat ein Datenfeld vom Typ String, welches
mit der Methode getMessage() gelesen werden kann; dieser String enthält
eine Fehlermeldung, die die Ausnahme beschreibt. Das Datenfeld wird bei der
Erzeugung eines Ausnahmeobjekts mit Hilfe eines Konstruktors gesetzt.
2
Dies bezeichnet man auch als catch.
107
7 Programmieren im Großen
Object
?
Throwable
HH
HH
j
H
Exception
Error
HH
@
H
@
HH
R
@
j
AbstractMethodError
...
...
RuntimeException
ArithmeticException
@
@
R
@
...
A BBILDUNG 7.2: Klassenhierarchie für Ausnahmen
Beispiel 7.3.1 Der folgende Programmcode implementiert die hier dargestellte
Hierarchie von Ausnahmeklassen:
Exception
HH
H
HH
j
MyException
MyOtherException
MySubException
class MyException extends Exception {
public MyException() {
super();
}
public MyException(String s) {
super(s);
}
}
class MyOtherException extends Exception {
public MyOtherException() {
super();
}
108
7.3 Ausnahmen (Exceptions)
public MyOtherException(String s) {
super(s);
}
}
class MySubException extends MyException {
public MySubException() {
super();
}
public MySubException(String s) {
super(s);
}
}
Es gibt zwei wesentliche Gründe für die Einführung neuer Ausnahmetypen:
• Hinzufügen nützlicher Informationen;
• der Typ selbst ist eine wichtige Information über die Ausnahmebedingung,
da Ausnahmen aufgrund ihres Typs abgefangen werden.
7.3.1 throw und throws
Ausnahmen werden ausgelöst durch die ein Objekt als Parameter erhaltene throwAnweisung.
Beispiel 7.3.2
import java.util.EmptyStackException;
public class Stack {
...
public Object pop() {
if(empty())
throw new EmptyStackException();
else
return stack[top--];
}
public Object peek() {
if(empty())
throw new EmptyStackException();
else
109
7 Programmieren im Großen
return stack[top];
}
}
Alle geprüften Ausnahmen, die in einer Methode ausgelöst und nicht in dieser
abgefangen und behandelt werden, müssen deklariert werden durch throws. Das
Fehlen von throws bedeutet, dass die Methode keine geprüfte Ausnahme auslöst. In der Tat ist EmptyStackException eine Unterklasse von RuntimeException.
Daher musste EmptyStackException in Beispiel 7.3.2 nicht deklariert werden. Im
folgenden Beispiel ist dies anders.
Beispiel 7.3.3 MyException und MySubException seien wie in Beispiel 7.3.1 definiert.
public static int c(int i) throws MyException {
switch(i) {
case 0: throw new MyException("input too low");
case 1: throw new MySubException("input still too low");
default: return i*i;
}
}
Die Methode c kann die beiden Ausnahmen MyException und MySubException
auslösen. Diese müssen daher deklariert werden. Da allerdings MySubException
eine Unterklasse von MyException ist, reicht es, MyException zu deklarieren.
Ruft man eine Methode auf, die nach throws eine zu berücksichtigende Ausnahme aufführt, hat man die drei Möglichkeiten:
(a) die Ausnahme abzufangen und zu behandeln,
(b) die Ausnahme abzufangen und auf eine eigene Ausnahme abzubilden,
die man dann selbst auslösen kann, und deren Typ im eigenen throwsKonstrukt deklariert ist,
(c) den Ausnahmetyp im eigenen throws-Konstrukt zu deklarieren und die
Ausnahme ohne Behandlung die Methode passieren zu lassen (dabei kann
ein eigenes finally-Konstrukt vorher noch zum Aufräumen aktiv werden).
7.3.2 try, catch und finally
Das allgemeine Muster zum Umgang mit Ausnahmen in Java ist die try-catchfinally-Sequenz: Man versucht etwas; wenn dieses eine Ausnahme auslöst, fängt
man die Ausnahme ab; und schließlich räumt man nach Ende des normalen Ablaufs oder des Ausnahmeablaufs auf, was immer auch passiert ist. Die Syntax der
try-catch-finally-Sequenz ist folgendermaßen:
110
7.3 Ausnahmen (Exceptions)
try
block1
catch(exception_type identifier)
block2
catch(exception_type identifier)
block3
...
finally
blockL
Der Rumpf (block1) der try-Anweisung wird ausgeführt, bis entweder eine Ausnahme ausgelöst oder der Rumpf erfolgreich abgeschlossen wird. Wenn eine
Ausnahme ausgelöst wurde, werden die catch-Konstrukte betrachtet, um darin die Klasse der Ausnahmen oder eine ihrer Oberklassen zu finden. Wird kein
solches catch gefunden, so wird die Ausnahme über diese try-Anweisung hinaus an einen umgebenden try-Block zur Behandlung weitergeleitet. Die Zahl der
catch-Konstrukte ist beliebig, sogar kein catch ist möglich. Wenn kein catch in
einer Methode zur Behandlung einer Ausnahme in Frage kommt, wird die Ausnahme dem Aufrufer der Methode zur Behandlung weitergereicht.
Wenn ein finally-Konstrukt in einem try vorhanden ist, so wird dessen Block
nach allen anderen Anweisungen in dem try ausgeführt. Dies erfolgt unabhängig davon, wie die vorherigen Anweisungen abgeschlossen wurden; sei es regulär, durch Ausnahmen oder durch den Ablauf beeinflussende Anweisungen wie
return3 .
Die catch-Konstrukte werden der Reihe nach untersucht. Deshalb ist es ein Fehler, wenn in den catch-Konstrukten ein Ausnahmetyp vor einer Erweiterung desselben abgefangen wird (denn das catch mit dem Untertyp würde niemals erreicht werden). Dies wird in folgendem Beispiel deutlich.
Beispiel 7.3.4 (vgl. Arnold & Gosling [1], S. 155 ff.)
MyException und MySubException seien wie in Beispiel 7.3.1 definiert.
class BadCatch {
public void goodTry() {
try {
throw new MySubException();
}
catch(MyException myRef) {
// Abfangen von sowohl MyException als auch MySubException
}
catch(MySubException mySubRef) {
// Dies wird nie erreicht
3
Auch break ist so eine Anweisung, wir werden sie aber nicht genauer behandeln.
111
7 Programmieren im Großen
}
}
}
Nur eine Ausnahme wird in einem try-Konstrukt behandelt. Wenn eine weitere
Ausnahme ausgelöst wird, werden die catch-Konstrukte des try nicht ein weiteres Mal aktiviert.
Das finally-Konstrukt in der try-Anweisung ermöglicht es – unabhängig davon, ob eine Ausnahme ausgelöst wurde – abschließend noch ein Programmstück auszuführen (z.B. das Schließen von offenen Dateien; so kann sparsam mit
dieser begrenzten Ressource umgegangen werden).
Wenn der finally-Block durch return oder Auslösen einer Ausnahme verlassen
wird, kann ein ursprünglicher Rückgabewert in Vergessenheit geraten.
Beispiel 7.3.5 (Arnold & Gosling [1], S. 157)
try {
// ... irgendein Code ...
return 1;
}
finally {
return 2;
}
Es würde immer der Wert 2 zurückgegeben werden.
Beispiel 7.3.6 (Flanagan [5], S. 43 ff.)
MyException, MyOtherException und MySubException seien wie in Beispiel 7.3.1
definiert. Um das nachfolgende Programm verstehen zu können, muss man wissen, dass instanceof feststellt, ob ein Objekt von einem gegebenen Typ ist. Die
null-Referenz ist keine Instanz irgendeines Typs, daher ist false der Rückgabewert von null instanceof Typ.
public class ThrowTest {
public static void main(String[] args) {
int i;
try {
i = Integer.parseInt(args[0]);
}
catch(ArrayIndexOutOfBoundsException e) {
System.out.println("Must specify an argument");
return;
}
112
7.3 Ausnahmen (Exceptions)
catch(NumberFormatException e) {
System.out.println("Must specify an integer argument");
return;
}
a(i);
}
public static void a(int i) {
try {
b(i);
}
catch(MyException e) {
if(e instanceof MySubException)
System.out.print("MySubException:");
else
System.out.print("MyException:");
System.out.println(e.getMessage());
System.out.println("Handled at point 1");
}
}
public static void b(int i) throws MyException {
int result;
try {
System.out.println("i= "+i);
result = c(i);
System.out.println("c(i)= "+result);
}
catch(MyOtherException e) {
System.out.println("MyOtherException: "+e.getMessage());
System.out.println("Handled at point 2");
}
finally {
System.out.println();
}
}
public static int c(int i) throws MyException,MyOtherException {
switch(i) {
case 0: throw new MyException("input too low");
case 1: throw new MySubException("input still too low");
case 99: throw new MyOtherException("input too high");
default: return i*i;
113
7 Programmieren im Großen
}
}
}
> java ThrowTest hello
Must specify an integer argument
> java ThrowTest 0
i= 0
MyException:
input too low
Handled at point 1
> java ThrowTest 1
i= 1
MySubException:
input still too low
Handled at point 1
> java ThrowTest 2
i= 2
c(i)= 4
> java ThrowTest 99
i= 99
MyOtherException: input too high
Handled at point 2
7.4 Aufgaben
Aufgabe 7.4.1 Vervollständigen Sie die Klasse DoLiList aus Aufgabe 6.4.4, so
dass diese als nicht-abstrakte Unterklasse der abstrakten Klasse Dictionary deklariert werden kann.
Aufgabe 7.4.2 Reimplementieren Sie schon erstellte Programme derart, dass diese auf dem Ausnahmekonzept beruhen.
114
8 Graphen
Graphen und Graphalgorithmen sind allgegenwärtig in der Informatik. Hunderte von interessanten Problemen lassen sich mit Hilfe von
Graphen formulieren. Dieses Kapitel beschreibt zunächst Methoden
zur Repräsentation von Graphen. Danach wird eine kleine Auswahl
der wichtigsten Algorithmen bei ihrer Verarbeitung vorgestellt. Literatur: [3, 4]
8.1 Anwendungen von Graphen
Um einen Eindruck von der Vielfalt der Einsatzmöglichkeiten von Graphen und
Graphalgorithmen zu bekommen, wollen wir einige Beispiele betrachten:
Karten Wenn wir eine Reise planen, wollen wir Fragen beantworten wie: Was
ist der kürzeste Weg von Bielefeld nach München? Was der schnellste Weg?
Um diese Fragen beantworten zu können, benötigen wir Informationen über
Verbindungen (Reiserouten) zwischen Objekten (Städten).
Hypertexts Das ganze Web ist ein Graph: Dokumente enthalten Referenzen
(Links) auf andere Dokumente, durch die wir navigieren. Graphalgorithmen sind
essentielle Komponenten der Suchmaschinen, die uns Informationen im Web finden lassen.
Schaltkreise Für elektrische Schaltungen sind wir an kreuzungsfreien Chip-Layouts interessiert und müssen Kurzschlüsse vermeiden.
Zeitpläne Die Erledigung einiger Aufgaben hängt evtl. von der Erledigung anderer ab. Diese Abhängigkeiten können als Verbindungen von Aufgaben modelliert werden. Ein klassisches scheduling Problem wäre dann: Wie arbeiten wir die
Aufgaben unter den gegeben Voraussetzungen am schnellsten ab?
Netzwerke Computernetzwerke bestehen aus untereinander verbundenen Einheiten, die Nachrichten senden, empfangen und weiterleiten. Wir sind nicht nur
115
8 Graphen
daran interessiert, welchen Weg eine Nachricht von einem Ort zum anderen nehmen muss. Genauso will man sicherstellen, dass die Konnektivität aller Orte auch
dann gewähleistet ist, wenn sich das Netzwerk ändert (Ausfallsicherheit). Genauso muss der Datenfluss sichergestellt sein, so dass das Netzwerk nicht ’verstopft’.
8.2 Terminologie
Graphen werden als eine durch Kanten verbundene Menge von Knoten definiert.
Definition 8.2.1 Ein Graph G = (V, E) besteht aus einer Menge von Knoten V
(auch vertices oder nodes) und einer Menge von Kanten E (edges). Jede Kante
ist ein Paar (v, w) ∈ V , das Paare von Knoten verbindet.
Wir beschränken uns im Folgenden auf Graphen, die keine doppelten (oder parallele) Kanten besitzen. (Graphen, die doppelte Kanten enthalten, nennt man
Multigraphen.)
Definition 8.2.2 Ein gewichteter Graph (weighted graph) ist ein Graph, in dem
Kanten mit Gewichten versehen sind.
Gewichtete Graphen enthalten somit zusätzliche Attribute, in denen z.B. die Länge einer Kante (bei der Modellierung eines Straßennetzes) repräsentiert werden
können.
Beispiel 8.2.3 Abbildung 8.1 zeigt einen gewichteten Graphen, der Bahnverbindungen zwischen einigen Großstädten Deutschlands repräsentiert. Die Städte
sind als Knoten, direkte Verbindungen als Kanten zwischen den jeweiligen Städten dargestellt. Die Entfernungen zwischen zwei Städten ist als Gewicht an der
jeweiligen Kante vermerkt. Wir können z.B. folgende Fragen stellen, die mit Hilfe
des Graphen beantwortet werden können:
• Gibt es eine direkte Verbindung zwischen Stadt BI und M?
• Welches ist der kürzeste Weg von BI nach S?
• Welches ist der kürzeste Weg, der in Stadt F startet und alle Städte einmal
besucht?
Definition 8.2.4 Ein gerichteter Graph (directed graph, digraph) ist ein Graph, in
dem jede Kante eine Richtung hat. Für u, v ∈ V ist dann (u, v) 6= (v, u).
In gerichteten Graphen können zwei Knoten durch zwei Kanten verbunden sein,
allerdings nur durch je eine Kante in jede Richtung.
116
8.2 Terminologie
HH
160
H
110
115
285
B
293
BI
DO
350
220
585
F
630
210
S
230
M
A BBILDUNG 8.1: Ein Graph zur Repräsentation von Bahnverbindungen zwischen verschiedenen Städten.
Definition 8.2.5 Ein Teilgraph (subgraph) von (V, E) ist ein Paar (V 0 , E 0 ), mit
V 0 ⊂ V und E 0 = {(u, v)|(u, v) ∈ E : u ∈ V 0 , v ∈ V 0 }.
Definition 8.2.6 Ein Graph heißt verbunden (connected), wenn jeder Knoten
von jedem anderen Knoten aus erreicht werden kann. Ein Graph, der nicht verbunden ist, besteht aus einer Menge von Zusammenhangskomponenten (connected components), die maximal verbundene Teilgraphen sind.
Definition 8.2.7 Zwei Knoten u, v ∈ V mit u 6= v heißen benachbart (adjacent),
wenn (u, v) ∈ E oder (v, u) ∈ E.
Definition 8.2.8 Bei der Bestimmung des Grads (degree) eines Knotens muss
man zwischen ungerichteten und gerichteten Graphen unterscheiden:
• ungerichtete Graphen:
Der Grad eines Knotens ist die Zahl seiner Nachbarn.
• gerichtete Graphen:
Der Eingangsgrad (in-degree) eines Knotens v ∈ V ist die Zahl der Kanten
(u, v) ∈ E.
117
8 Graphen
Der Ausgangsgrad (out-degree) eines Knotens v ∈ V ist die Zahl der Kanten
(v, u) ∈ E.
Der Grad ist die Summe von Eingangs- und Ausgangsgrad.
Definition 8.2.9 Ein Pfad (path) von u nach v ist einen Folge von Knoten u1 , u2 , . . . , uk ,
so daß u1 = u und uk = v und (ui , ui+1 ) ∈ E für alle 1 ≤ i < k.
Definition 8.2.10 Ein Zyklus (cycle) ist ein Pfad, in dem Start- und Endknoten
identisch sind.
Definition 8.2.11 Auch Bäume sind spezielle Graphen: Ein Baum (tree) ist ein ungerichteter, verbundener Graph ohne Zyklen (genauer: ohne Kreise, also Zyklen,
in denen nur Anfangs- und Endpunkt identisch sind). Eine Menge von Bäumen
heißt Wald (forest). Ein Spannbaum (spanning tree) eines verbundenen Graphen
(V, E) ist ein Teilgraph, der alle Knoten V enthält und ein Baum ist.
8.3 Repräsentation von Graphen
Um Graphen darzustellen gibt es zwei Standardmöglichkeiten: Adjazenzlisten
und Adjazenzmatrizen, die beide sowohl für gerichtete als auch ungerichtete
Graphen verwendet werden können. Bei dünn besetzten (sparse) Graphen, bei
denen die Anzahl der Kanten |E| viel kleiner als |V |2 ist, liefern Adjazenzlisten eine kompakte Darstellung. Die Repräsentation durch Adjazenzmatrizen wird vorgezogen, wenn der Graph dicht besetzt (dense) ist (d.h. wenn |E| nahe an |V |2
liegt, oder wenn ein Algorithmus möglichst schnell hearusfinden muss, ob zwei
Knoten verbunden sind.
Adjazenzlisten
Die Abbildungen 8.2(b) und 8.3(b) zeigen ein Beispiele für die Repräsentation
eines Graphen als Adjazenzliste: ein Array Adj enthält für jeden Knoten aus V
eine Liste. Für jeden Knoten u ∈ V enthält die Liste Adj[u] alle Knoten v, so dass
(u, v) ∈ E. Die Knoten werden dabei üblicherweise in beliebiger Reihenfolge
gespeichert. Wenn die Kanten Gewichte haben, werden diese ebenfalls in der
Liste gespeichert. Diese Darstellung braucht Θ(V + E) Platz. Alle Knoten, die
adjazent zu u sind, können in Θ(degree(u)) bestimmt werden. Um zu überprüfen,
ob (u, v) ∈ E gilt, wird O(degree(u)) Zeit benötigt.
Adjazenzmatrizen
Die Abbildungen 8.2(c) und 8.3(c) zeigen ein Beispiele für die Repräsentation
eines Graphen als Adjazenzmatrix: die Knoten des Graphen seien in beliebiger
118
8.4 Ein Abstrakter Datentyp (ADT) Graph
1
1
2
5
2
1
5
3
2
4
4
2
5
5
4
1
1
2
3
4
5
1
0
1
0
0
1
2
1
0
1
1
1
3
0
1
0
1
0
3
4
0
1
1
0
1
2
5
1
1
0
1
0
2
3
5
3
4
4
(a)
(b)
(c)
A BBILDUNG 8.2: Zwei Repräsentationen eines ungerichteten Grapen mit 5 Knoten und
7 Kanten (a). (b) zeigt die Adjazenzlisten-Repräsentation des Graphen, (c) die entsprechende Adjazenzmatrix. (Nach [3], S. 528)
1
2
4
5
4
1
2
3
4
5
6
1
0
1
0
1
0
0
2
0
0
0
0
1
0
1
2
2
5
3
6
3
0
0
0
0
1
1
4
2
4
0
1
0
0
0
0
5
4
5
0
0
0
1
0
0
6
6
6
0
0
0
0
0
1
3
6
(a)
5
(b)
(c)
A BBILDUNG 8.3: Zwei Repräsentationen eines gerichteten Grapen mit 6 Knoten und
8 Kanten (a). (b) zeigt die Adjazenzlisten-Repräsentation des Graphen, (c) die entsprechende Adjazenzmatrix. (Nach [3], S. 528)
Reihenfolge nummeriert. Dann kann der Graph durch eine |V | × |V |-Matrix A =
(aij ) beschrieben werden, mit den Elementen
(
1
aij =
0
falls (i, j) ∈ E,
sonst.
Die Adjazenzmatrix-Darstellung benötigt O(V 2 ) Platz. Alle Knoten, die adjazent
zu u sind, können in Θ(V ) bestimmt werden. Um zu überprüfen, ob (u, v) ∈
E gilt, wird O(1) Zeit benötigt. Gewichtete Graphen können dargstellt werden,
indem man in der Matrix Gewichte statt Bits speichert.
8.4 Ein Abstrakter Datentyp (ADT) Graph
Wir wollen nun einen abstrakten Datentypen für Graphen beschreiben, der als
Java-Interface definiert wird. Dieses sehr einfach gehaltene Interface genügt, um
die in den nächsten Abschnitten beschriebenen Algorithmen zu implementieren.
119
8 Graphen
Der Graph Konstruktor bekommt zwei Parameter: einen Integer-Wert für die Anzahl der Knoten im Graphen und einen Boolean, der angibt, ob der Graph gerichtet ist oder nicht. Die Operationen beschränken sich zunächst auf:
numOfV() gibt die Anzahl der Knoten zurück
numOfE() gibt die Anzahl der Kanten zurück
directed() gibt an, ob der Graph gerichtet ist
insert(e) fügt eine Kante in den Graphen ein
remove(e) löscht einen Kante aus dem Graphen
edge(v,w) überprüft, ob es eine Kante zwischen Knoten v und w gibt
getAdjList(v) stellt einen Iterator zur Verfügung, der alle benachbarten Knoten von v aufzählt
Das Java-Interface ist folgendermaßen definiert:
public interface Graph {
int numOfV();
int numOfE();
boolean directed();
void insert(Edge e);
void remove(Edge e);
boolean edge(int v, int w);
AdjList getAdjList(int v);
}
public interface AdjList {
int begin();
int next();
boolean end();
}
public class Edge {
int v;
int w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
}
120
8.5 Breitensuche
Das oben angegebene Interface ist ein möglichst einfach gehaltenes Beispiel für
einen Graph-ADT. Bestimmte Algorithmen erfordern eventuell eine Anpassung
dieses Interfaces, z.B. durch Einführen einer speziellen Klasse Vertex oder Erweiterung der Klasse Edge, um zusätzliche Informationen in den Knoten oder Kanten (z.B. Gewichte) speichern zu können. Auch können in unserem Beispiel noch
keine Knoten hinzugefügt oder gelöscht werden. Ebenso finden keine Überprüfungen statt, ob z.B. parallele Kanten zwischen Knoten eingefügt werden. Dies
wiederum ist abhängig von der jeweiligen Anwendung des Graphen und müßte
entsprechend behandelt werden.
8.5 Breitensuche
Die Breitensuche (breadth-first search) ist eine der einfachsten Algorithmen zur
Suche in Graphen. Gegeben einen Graphen G und einen Startknoten s, durchsucht die Breitensuche systematisch jede Kante von G, um alle Knoten zu finden,
die von s erreichbar sind. Dabei wird die Distanz (kleinste Anzahl von Kanten)
von s zu jedem erreichbaren Knoten berechnet. Bei der Suche wird ein breadthfirst tree mit Wurzel s erzeugt, der alle erreichbaren Knoten enthält. Der kürzeste
Pfad von s nach v in dem Baum entspricht dem kürzesten Pfad von s nach v im
Graphen G.
Der Name ”Breitensuche” läßt sich dadurch erklären, dass die Suche die Grenze
zwischen besuchten und unbesuchten Knoten gleichmäßig über die Breite der
Grenze ausdehnt. D.h. der Algorithmus besucht zunächst alle Knoten mit Distanz
k von s, bevor Knoten mit Distanz k + 1 besucht werden.
Während der Breitensuche werden Knoten weiß, grau oder schwarz eingefärbt.
Wenn ein Knoten während der Suche entdeckt wird, ändert sich seine Farbe.
Graue und schwarze Knoten sind bereits entdeckt worden, und diese Information wird ausgenutzt, um die Breitensuche voranzutreiben. Wenn es eine Kante
(u, v) ∈ E gibt, und u ist schwarz, dann ist v entweder grau oder schwarz. Das
bedeutet, dass alle benachbarten Knoten eines schwarzen Knotens bereits entdeckt wurden. Graue Knoten können noch weiße Nachbarn haben; sie repräsentieren die Grenze zwischen entdeckten und unentdeckten Knoten.
Die Breitensuche konstruiert einen Breitensuchbaum, dessen Wurzel der Startknoten s ist. Mithilfe dieses Baumes lassen sich Vorgänger- und Nachfolgerrelationen (relativ zu s) aufstellen. Immer dann, wenn ein Knoten entdeckt wird (und
auch nur dann), wird dem Baum eine Kante hinzugefügt.
Der folgende Algorithmus führt eine Breitensuche in einem Graphen aus. Abbildung 8.4 zeigt die Suche an einem Beispiel. Die Farbe eines Knotens u ∈ V wird
in color[u] gespeichert, der Vorgänger von u in π[u]. Wenn u keinen Vorgänger
hat (z.B. s oder wenn u noch nicht entdeckt wurde), ist π[u] = N IL. Der Algorithmus berechnet auch die Distanz von s zu jedem Knoten u und speichert sie in
d[u] (initial ist die Distanz von s zu allen Knoten unendlich groß). Außerdem wird
121
Breadth-first Search
8 Graphen
Breadth-first Search
r
s
t
u
r
s
t
u
∞
0
∞
∞
∞
1
0
∞
∞
(a) Breadth-first Search
Q
∞
∞
∞
∞
v
w
x
y
(b)Breadth-first Search
s
0
Q
∞
∞
1
∞
∞
v
w
x
y
r
s
t
u
r
s
t
u
∞
1
0
∞
2
∞
∞
1
0
∞
2
∞
(c) Breadth-first Search
Q
∞
∞
1
∞
2
∞
v
w
x
y
r
t
x
1
2
2
(d)Breadth-first Search
Q
∞
2
∞
1
∞
2
∞
v
w
x
y
r
s
t
u
r
s
t
u
∞
1
0
∞
2
∞
3
∞
1
0
∞
2
∞
3
(e) Breadth-first Search
Q
∞
2
∞
1
∞
2
∞
v
w
x
y
x
v
u
2
2
3
(f) Breadth-first Search
Q
∞
2
∞
1
∞
2
∞
3
v
w
x
y
r
s
t
u
r
s
t
u
∞
1
0
∞
2
∞
3
∞
1
0
∞
2
∞
3
(g)Breadth-first Search
Q
∞
2
∞
1
∞
2
∞
3
v
w
x
y
r
s
t
u
∞
1
0
∞
2
∞
3
(i)
u
y
3
3
(h)
Q
∞
2
∞
1
∞
2
∞
3
v
w
x
y
w
r
1
1
t
x
v
2
2
2
v
u
y
2
3
3
y
3
Q
∞
2
∞
1
∞
2
∞
3
v
w
x
y
A BBILDUNG 8.4: Operationen der Breitensuche auf einem ungerichteten Graphen. Die
Kanten des bei der Suche entstehenden Baumes sind schattiert dargestellt. Jeder Knoten
u enthält den Wert d[u]. Die Queue Q ist jeweils zu Beginn jeder Iteration der whileSchleife gezeigt. Knotenabstände sind jeweils unter der Queue dargestellt. (Nach [3], S.
533)
noch eine Queue Q verwendet, um die grauen Knoten zwischenzuspeichern. Die
Laufzeit des Algorithmus zur Breitensuche ist O(V + E).
BFS(G, s)
1 for each vertex u ∈ V [G] − {s}
2
do color[u] ← W HIT E
3
d[u] ← ∞
4
π[u] ← N IL
5 color[s] ← GRAY
6 d[s] ← 0
7 π[s] ← N IL
8 Q←∅
9 ENQUEUE(Q, s)
122
8.6 Tiefensuche
10
11
12
13
14
15
16
17
18
while Q 6= ∅
do u ← DEQUEUE(Q)
for each v ∈ Adj[u]
do if color[v] = W HIT E
then color[v] ← GRAY
d[v] ← d[u] + 1
π[v] ← u
ENQUEUE(Q, v)
color[u] ← BLACK
8.6 Tiefensuche
Die Strategie der Tiefensuche besteht darin, immer zuerst ”tiefer” im Graphen
zu suchen. Dabei werden zunächst die Kanten durchsucht, die von dem zuletzt
entdeckten Knoten v ausgehen. Erst wenn alle Kanten von v untersucht wurden,
geht die Suche zurück zu dem Knoten, von dem aus v entdeckt wurde, um dort
wieder unentdeckte Knoten zu finden. Dieser Prozess läuft solange, bis alle Knoten entdeckt wurden, die vom Startknoten aus erreichbar sind. Sollte es dann
noch unentdeckte Knoten geben, wird einer von diesen als neuer Startknoten
ausgewählt und die Suche von dort neu gestartet. Dies passiert so oft, bis alle
Knoten gefunden wurden.
Entsprechend zur Breitensuche wird, wenn ein Knoten v während der Suche in
der Adjazenzliste eines bereits entdeckten Knotens u gefunden wird, dieses Ereignis notiert. Dazu wird der Vorgänger von v in π gespeichert: π[v] = u.
Knoten werden wieder entsprechend ihres Status eingefärbt. Anfangs sind alle
knoten weiß, sie werden grau, wenn sie entdeckt werden und schwarz, wenn die
Suche abgeschlossen ist (d.h. die Adjazenzliste des Knotens abgearbeitet wurde).
Außerdem werden alle Knoten während der Suche mit Zeitstempeln (timestamps)
versehen. Jeder Knoten v hat zwei solche Zeitstempel: der erste d[v] notiert, wann
v erstmals entdeckt (und grau eingefärbt) wurde, der zweite f [v], wann v abgearbeitet (und schwarz eingefärbt) wurde. Diese Zeitstempel werden in vielen
Graphalgorithmen verwendet.
Der folgende Pseudocode DFS zeigt den grundlegenden Algorithmus zur Tiefensuche. Der Eingabegraph G kann gerichtet oder ungerichtet sein, die globale Variable time wird zum Zeitstempeln genutzt. Abbildung 8.5 illustriert den Prozess
der Tiefensuche an einem Beispiel.
123
8 Graphen
Depth-first Search
u
v
Depth-first Search
w
1/
Depth-first Search
Depth-first Search
u
v
1/
2/
Depth-first Search
x
y
z
x
y
(a)
u
v
1/
2/
x
w
u
v
1/
2/
Depth-first Search
4/
3/
z
y
w
u
v
1/
2/
y
z
u
v
1/
1/8
2/7
2/
F
y
z
w
(i)
B
z
3/6
3/
x
y
z
(h)
u
v
w
u
v
w
2/7
2/
9/
1/
1/8
2/7
2/
9/
z
y
(j)
B
Depth-first Search
4/5
4/
3/6
3/
x
C
F
B
Depth-first Search
4/5
4/
y
w
1/
1/8
F
3/6
3/
x
v
2/7
2/
(g)
B
Depth-first Search
4/5
4/
3/6
3/
u
1/
Depth-first Search
4/5
4/
y
(f)
B
w
3/6
3/
x
z
(d)
B
z
w
x
(c)
y
v
w
y
Depth-first Search
4/5
4/
2/7
2/
v
2/
3/
x
3/
u
u
1/
z
B
x
w
Depth-first Search
Depth-first Search
4/5
4/
1/
x
v
2/
3/
(e)
Depth-first Search
4/5
4/
u
1/
(b)
B
Depth-first Search
4/
F
w
Depth-first Search
z
3/6
3/
x
y
(k)
z
(l)
u
v
w
u
v
w
u
v
w
u
v
w
1/
1/8
2/7
2/
9/
1/
1/8
2/7
2/
9/
1/
1/8
2/7
2/
9/
1/
1/8
2/7
2/
9/12
9/
C
F
C
F
B
4/5
4/
3/6
3/
10/
4/5
4/
3/6
3/
10/
x
y
z
x
y
z
(m)
C
F
B
(n)
B
4/5
4/
3/6
3/
10/11
10/
x
y
z
(o)
C
F
B
B
B
4/5
4/
3/6
3/
10/11
10/
x
y
z
B
(p)
A BBILDUNG 8.5: Tiefensuche auf einem gerichteten Graphen. Die Kanten, die gerade
von dem Algorithmus verarbeitet werden, sind entweder schattiert (Kanten des Suchsbaumes) oder gestrichelt dargestellt. Kanten, die nicht Teil des Suchbaumes sind, sind
mit B,C oder F bezeichnet, je nachdem ob sie back, cross oder forward Kanten sind. Die
Knoten sind mit den Zeitpunkten ihrer Entdeckung und Abarbeitung (Endzeit) benannt.
(Nach [3], S. 542)
DFS(G)
1 for each vertex u ∈ V [G]
2
do color[u] ← W HIT E
3
π[u] ← N IL
4 time ← 0
5 for each vertex u ∈ V [G]
6
do if color[u] = W HIT E
7
then DFS-VISIT(u)
124
8.7 Topologisches Sortieren
DFS-VISIT(u)
1 color[u] ← GRAY
White vertex u has just been discovered.
2 time ← time + 1
3 d[u] ← time
4 for each v ∈ Adj[u]
Explore edge (u, v).
5
do if color[v] = W HIT E
6
then π[v] ← u
7
DFS-VISIT(v)
8 color[u] ← BLACK
Blacken u; it is finished.
9 f [u] ← time ← time + 1
8.7 Topologisches Sortieren
Als nächstes wollen wir und ein Beispiel ansehen, wie die Tiefensuche verwendet werden kann, um topologisches Sortieren auf einem gerichteten Graphen
durchzuführen. Eine topologisches Sortierung eines gerichteten Graphen ist eine
lineare Anordnung aller seiner Knoten, so dass für jede Kante (u, v), der Knoten
u vor v einsortiert wird. (Dazu muss der Graph azyklich sein, sonst ist eine lineare
Anordnung nicht möglich.) Man kann diese Sortierung als Sortierung entlang einer horizontalen Linie ansehen, so dass alle gerichteten Kanten immer von links
nach rechts zeigen.
Gerichtete azyklische Graphen werden häufig verwendet, um Prioritäten zwischen Ereignissen zu beschreiben. Abbildung 8.6 zeigt ein Beispiel: Wenn Harry
Hacker sich morgens anzieht, muss er bestimmte Kleidungsstücke vor anderen
anlegen (z.B. die Socken vor den Schuhen). Andere sind unabhängig voneinander, wie z.B. Socken und Gürtel. Eine gerichtete Kante (u, v) gibt dann an,
dass Kleidungsstück u vor v angelegt werden muss (Abb.8.6a). Mithilfe einer topologische Sortierung kann Harry Hacker sich einen Plan erstellen, wie er sich
ankleiden muss (Abb. 8.6b).
Der folgende einfache Algorithmus sortiert den Graphen G topologisch:
TOPOLOGICAL-SORT(G)
1 call DFS(G) to compute finishing times f [v] for each vertex v
2 as each vertex is finished, insert it into the front of a linked list
3 return the linked list of vertices
125
8 Graphen
11/16
socks
undershorts
17/18
watch
pants
12/15
shoes
shirt
1/8
tie
2/5
9/10
13/14
(a)
6/7
belt
jacket
(b)
3/4
socks
undershorts
pants
shoes
watch
shirt
belt
tie
jacket
17/18
11/16
12/15
13/14
9/10
1/8
6/7
2/5
3/4
A BBILDUNG 8.6: (a) Wenn Harry Hacker sich morgens anzieht, sortiert er seine
Kleider zunächst topologisch. Jede gerichtete Kante (u, v) bedeutet, dass Kleidungsstück u vor v angelegt werden muss. Die Entdeckungs- und Endzeiten einer Tiefensuche sind neben den Knoten dargestellt. (b) zeigt denselben Graphen
wie (a), diesmal topologisch sortiert.Die Knoten sind von links nach rechts anhand der fallenden Endzeiten sortiert. Beachte, dass alle gerichteten Kanten von
links nach rechts verlaufen. (Nach [3], S. 550)
126
9 Hashing
Dieses Kapitel beschäftigt sich mit einem wichtigen Speicherungsund Suchverfahren, bei dem die Adressen von Daten aus zugehörigen Schlüsseln errechnet werden, dem Hashing. Dabei stehen die
Algorithmen und verschiedene Heuristiken der Kollisionsbehandlung
im Vordergrund. Es wird aber auch auf die in Java vordefinierte Klasse Hashtable eingegangen.
9.1 Einführendes Beispiel
Ein Pizza-Lieferservice in Bielefeld speichert die Daten seiner Kunden: Name, Vorname, Adresse und Telefonnummer. Wenn ein Kunde seine Bestellung telefonisch aufgibt, um dann mit der Pizza beliefert zu werden, dann muss er seine
127
9 Hashing
Telefonnummer angeben, da er über diese Nummer eindeutig identifiziert werden kann.
Natürlich existiert in Bielefeld jede Telefonnummer nur einmal, während es mehrere Menschen mit gleichem Vornamen, Nachnamen oder Adresse gibt. Das bedeutet: Wenn der Telefonist in der Pizzeria die Telefonnummer des Kunden erfragt (oder von dem Display seines Telefons abliest) und diese in seinen Computer
eingibt, dann bekommt er genau einen Kunden mit Name, Vorname und Adresse angezeigt, vorausgesetzt, der Kunde wurde schon einmal in die Datenbank
eingetragen. Die Telefonnummer ist also eine Art Schlüssel für die Suche nach
einem Datensatz, in diesem Fall die Kundendaten. Abstrakter bedeutet dies, dass
wir über einen Schlüssel (key) Zugriff auf einen Wert (value) erhalten.
Stellen wir uns die Repräsentation der Daten in dem Programm, das die Pizzeria
benutzt, so vor:
Telefonnummer
Name
00000000
Müller
00000001
Schmidt
00000002
Schultz
...
...
99999997
Meier
99999998
Neumann
99999999
Schröder
Vorname
Heinz
Werner
Hans
...
Franz
Herbert
Georg
PLZ
Straße
33615 Unistraße 15
33615
Grünweg 1
33602 Arndtstraße 12
...
...
33609
Kirchweg 4
33612 Jägerallee 15
33647
Mühlweg 2
Die Daten werden also in einer Tabelle gespeichert. Dabei entsprechen die Zeilen
den Telefonnummern, die es in Bielefeld möglicherweise geben könnte, nämlich
den achtstelligen Zahlen von 00000000 bis 99999999 – wobei natürlich einige
Zahlen, wie etwa die 00000000 keine gültigen Telefonnummern sind und wir
vereinfachend annehmen wollen, dass Telefonnummern, die weniger als acht
Stellen haben, mit führenden Nullen aufgefüllt werden können.
Bis hierher würde die Datentabelle also so aussehen wie in der obigen Abbildung
mit 108 – also 100 Millionen – verschiedene Nummern, die jeweils einer Zeile
in der Tabelle entsprechen. Hierbei fällt schon auf, dass diese Zahl natürlich viel
zu groß ist, denn es gibt sicherlich nicht annähernd so viele Telefonanschlüsse
in Bielefeld. Die Menge der möglichen Schlüssel, also die Zahlen 00000000 bis
99999999 ist gegenüber der Zahl der tatsächlich informativen Einträge viel zu
groß, so dass eine große Menge Speicher für Telefonnummern verbraucht wird,
die nie zugeordnet werden. Weiterhin ist zu bedenken, dass nicht jeder Einwohner Bielefelds eine Telefonnummer hat und dass nicht alle, die eine Nummer haben, beim Pizza-Service bestellen, und wenn sie doch eine Pizza bestellen, nicht
unbedingt bei dieser Pizzeria.
Gehen wir also davon aus, dass von der Menge aller Schlüssel nur ein kleiner
Teil wirklichen Datenbankeinträgen entspricht. Bielefeld hat ca. 300.000 Einwoh-
128
9.2 Allgemeine Definitionen
ner, dann gibt es vielleicht 200.000 Telefonnummern. Davon bestellt jeder fünfte eine Pizza – bleiben 40.000 potentielle Einträge, verteilt auf mehrere PizzaLieferservices. Optimistisch geschätzt wird unsere Pizzeria also ca. 10.000 Kunden haben.
Damit bleiben von den 100 Millionen Schlüsseln nur 10.000 tatsächlich benutzte
übrig, das sind 0,01 Prozent. Dies bedeutet für die Tabelle der Kundendaten,
dass sie erheblich verkleinert werden kann – nämlich auf 10.000 Zeilen, denn mit
mehr Kunden braucht die Pizzeria nicht zu rechnen.
Da stellt sich folgende Frage: Wir wissen doch gar nicht, welche Telefonnummern
bestellen werden – wie sollen denn dann die Zeilen benannt werden?
Unsere Aufgabe ist es, alle 100 Millionen Telefonnummern (denn jede einzelne
könnte ja theoretisch bestellen) so abzubilden, dass sie in eine 10.000 Zeilen
große Tabelle passen. Hierzu machen wir uns jetzt eine mathematische Operation
zunutze, die Modulo-Operation:
x mod y
liefert als Ergebnis den Rest der ganzzahligen Division x/y.
Beispielsweise ergibt 117 mod 20 = 17, da 117 = 5 · 20 + 17.
Wenn wir jetzt jede angegebene Telefonnummer modulo 10.000 nehmen, bekommen wir ein Ergebnis zwischen 0 und 9999. Somit können wir alle theoretischen Telefonnummern und damit Pizza-Besteller in einer Tabelle abbilden, die
gerade mal 10.000 Zeilen hat. Die Funktion, die wir dazu benutzen, lautet:
h(Telefonnummer) = Telefonnummer mod Tabellenlänge,
oder allgemein:
h(k) = k mod m
mit h für Hashfunktion, k für key und m für Tabellenlänge.
Wir benutzen also diese Hashfunktion, um jedem Schlüssel einen Index (hier eine Zahl zwischen 0 und 9999) in einer verkleinerten Tabelle (der sogenannten
Hashtabelle) zuzuordnen und damit eine Menge Platz zu sparen. Leider kann es
allerdings passieren, dass in ungünstigen Fällen zwei oder mehr Schlüssel (Telefonnummern) auf denselben Index in der Hashtabelle abgebildet werden, z.B. ist
01063852 mod 10000 = 08153852 mod 10000 = 3852. Wie solche Kollisionen
behandelt werden können, wird im übernächsten Abschnitt diskutiert.
9.2 Allgemeine Definitionen
Formal gesehen ist Hashing ein abstrakter Datentyp, der die Operationen insert,
delete und search auf (dynamischen) Mengen effizient unterstützt.
Hashing ist im Durchschnitt sehr effizient – unter vernünftigen Bedingungen werden obige Operationen in O(1) Zeit ausgeführt (im worst-case kann search O(n)
129
9 Hashing
q
q
q
q q q q
q q q
q q
q q
q
q
q
q
U
q
q
q
q
(Universum der Schlüssel)
q
9
v
0
v
q
v
q
q
ppppppppp
pppp pp p
p
p
p
q
p
pp pppp
q
7 v
p pp ppq
p
v
p
p
p
4
p
q
q
pppppppppp
p p p p pp
pp pppp
v
pppppppp p
p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p ppp p p p p p p p p p p
p
p
p
q
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
q
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p pp p p p p
pppp pp p p
pppppppp
p p p ppp p p p
p
q
pppp pppp q
p p p p p p pp p p p
pppp p p p p
p
1
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
ppp p pppppp
q
p
q
ppp
K
pp
pp ppp
p pp p
pppp ppppppp
q
pppp pp
p
p
p
p
p
q
p
p
p
p
ppp p pp (Aktuelle Schlüssel)
p
p
pp
ppp
q ppppppppppp
q
ppppp ppp ppp
pppp pp
p
p
p
p
p
p
p
pppppppp p
p
p
p
p
p
p
pp pp
p
p
p p pppppppppppp pppppppppppp qp
pppppp
v
q
ppp ppp
p
p
p
p
p
p
p
pp
p
v
p
p
p
p
p
p
p
p
q
2
3pppppppp pppppppppppp pppppppppppp ppp
ppp
q
ppp
p
q
p
p
p
p
p
p
p
p
p
p
p
q
p
p
p
p
p
p
p
ppp
pp
pppppppppppp
q
pp p pp p p
q
pppppppppppppppp
v
p p p p ppp
pp
5
p
q
p p pp p p
p
q
p
ppppp
pp p p p
q
ppppp
q
v
p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p ppp p p
p
p
q
p
p
p ppp p
p
p
p
p
p
p
p
p
p
q
p
p
p
p
p
p
p
p
p
p
p
p
ppppppppppppppppqpppppppppppp
p p ppp
ppp p
8
p p p p p p p p p p p p p p p p p p p p p p p p p pp
q
p ppp
p p pp p p
pppp p
p
p
q
p
p
p
p
p
q
p p pp
p
p
p
q
p
p
p
p
q
pp p p p p p p
q
p p p p p p p p p p p p p p p p p p p p p p p p p pp p p
q
p p p p p p ppp
q
q
q
q
q
q q
q q
q q
q
q q q q q
q
q
6
...
.............
.............
.............
.....................................
.... .................
.............
.............
.............
....
0
....
.............
.............
.............
...................................
..... .................
.............
.............
.............
....
1
2
q
3
....
.............
.............
.............
.....................................
. .
............. ..........................
.
.
.
.
.
.
.
.
.
.
.
.
.......
....
4
5
....
.............
.............
.............
...............................
............. ..........................
.
.
.
.
.
.
.
.
.
.
.
.
..........
.......
6
....
.............
.............
.............
..............................
............ .........................
.
.
.
.
.
.
.
.
.
.
.
.
............
.........
7
8
.............
.....
.............
.............
..................................
..... .................
.............
.............
.............
....
9
T
A BBILDUNG 9.1: Direkte Adressierung
Zeit benötigen). In unserer Darstellung folgen wir Cormen et al. [2]. Zur Vereinfachung nehmen wir an, dass die Daten keine weiteren Komponenten enthalten
(d.h. mit den Schlüsseln übereinstimmen). Wenn die Menge U aller Schlüssel relativ klein ist, können wir sie injektiv auf ein Feld abbilden; dies nennt man direkte
Adressierung (Abbildung 9.1).
Ist die Menge U aller Schlüssel aber sehr groß (wie im obigen Beispiel des PizzaServices), so können wir nicht mehr direkt adressieren. Unter der Voraussetzung,
dass die Menge K aller Schlüssel, die tatsächlich gespeichert werden, relativ klein
ist gegenüber U, kann man die Schüssel effizient in einer Hashtabelle abspeichern. Dazu verwendet man allgemein eine Hashfunktion
h : U → {0, 1, . . . , m − 1},
die Schlüssel abbildet auf Werte zwischen 0 und m − 1 (dabei ist m die Größe der
Hashtabelle). Dies illustriert Abbildung 9.2.
Beispiel 9.2.1 Eine typische Hashfunktion h für U = N ist
h(k) = k mod m.
130
9.3 Strategien zur Behandlung von Kollisionen
....
.............
.............
.............
.....................................
... ................
.............
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
......
..
q
q q q q q
q q q
q q
q q
q
q
q
U
....
.............
.............
.............
....................................
.. ..............
.............
.............
.
.
.
.
.
.
.
.
.
.
.
.
.......
...
q
p p pp p p
ppppp p pp
p
p
p
p
p
p
p
(Universum der Schlüssel)
q
ppppp
q
q pp pppppp pppppp p
p
p
p
p
q
p
ppp ppppp q q
q
pppppppppp
p p pp p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p p
p
p
p
p
p
p
p
p
p
ppppp pppp p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
q
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
q
p
p
p p pp
p p p p p pp p p p p p p p p p p p p
pppp pppppp
p p p p p p p p p p p p p pp p
pp p p p pp p p
q
pppppp ppp q
pppppp pp
p
ppppppp p
p
v
p
p
p
p
p
p
p
p
p
p
p
p
p
k1
q
q
K
p ppp
ppp pppppppp
pp pp
pppppp pppp ppp
p
p
q
p
p
p
q
p
p
ppp ppp (Aktuelle Schlüssel)
p
p
p
p
p
p
p
p
p
p
pppppp
q
p
p
p
p
q ppppppppppppppp
p
p
p
p
ppp p
p
p
ppppp
ppppppppp pppppp pppppppppppppppp
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
q
p
p
p
p
p
p
pp
p
p
p
q p
pppp
k4 v
pppqppppppp
ppppppp pppppppp
pppp
q
pppppppppp q
pppppppppp ppppp pppp
p
p
p
p
p
p
p
p
p
p
p
p
ppp
p
p
p
p
p
p
p
p
p
p
p
p
ppppppppp
q
ppp
pp ppppppppppp
pppppppppppp ppp
q
p p p pp
ppppppppp pppp
p
p
p
ppp pppppppppppp
p
p
p
p
p
p
p
q
p
v
p
pp p p p p pp
p
k
q
ppppppp p p
p
2
p
p
p
p
p
p
p
p
p
p
v
p
p p pp p p
q
q ppppppppppppppppppp
p p p p pp
ppppp
k5
q
p ppp pp ppppppppppppppppppppppppppqppppppppp ppppp
ppppp
p
p
p
p
ppp
p ppp p
q
ppppppppppppppppppppppppp
p p ppp
q
p ppppppppppppppppppppppppp
p ppp
p
p
p
p
p
q
p
p
v
p p pp
q
pp p
p
q
p
k
p pp p
p
3
q
pp
p pp p p p p
q
q
p pp
ppp p p p p p p p p
q
p p p p p p p p p p p p p p p p p p p p p p p p p pp
q
q
q
q
q
q q
q q
q q
q q q q q q q
q
q
0
h(k1 )
q
h(k4 )
....
.............
.............
.............
..............................
............ .........................
.
.
.
.
.
.
.
.
.
.
.
.
............
.........
h(k2 ) = h(k5 )
....
.............
.............
.............
....................................
.... .................
.............
.............
.............
....
h(k3 )
...
.............
.............
.............
....................................
..... .................
.............
.............
.............
....
....
.............
.............
.............
...................................
..... .................
.............
.............
.............
....
m−1
T
A BBILDUNG 9.2: Eine Hashfunktion h weist Schlüsseln ihren Platz in der Hashtabelle T zu.
Dabei sollte m eine Primzahl sein, die nicht (zu) nahe an Potenzen von 2 liegt.
Der Grund dafür wird in Cormen et al. [2], S. 228, erläutert.
Da Hashfunktionen nicht injektiv sind, tritt das Problem der Kollision auf: zwei
Schlüsseln wird der gleiche Platz in der Hashtabelle zugewiesen. Kollisionen sind
natürlich unvermeidbar, jedoch wird eine “gute” Hashfunktion h die Anzahl der
Kollisionen gering halten. D.h. h muss die Schlüssel gleichmäßig auf die Hashtabelle verteilen. Außerdem sollte h einfach zu berechnen sein. In Cormen et al. [2]
findet man weitere Erläuterungen zum Thema Hashfunktionen.
9.3 Strategien zur Behandlung von Kollisionen
9.3.1 Direkte Verkettung
Man kann Kollisionen auflösen, indem jeder Tabellenplatz einen Zeiger auf eine
verkettete Liste enthält. Schlüssel mit demselben Hashwert werden in dieselbe
Liste eingetragen (Abbildung 9.3).
131
9 Hashing
qqqqqqqqqqqqqqqqqqqqqqq
qqqq ppp ppppppppppppppppppppppp
qqqqqqqqqqqqqq qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq qqqqqqqqqqqqqqqqqqqqq
q
q
q
q
q
qq
ppp qpqpqpqpqppppppp
p
p
p
p
p
p
p
p
p
p
qq qq
p
pppppppppppp
qqq
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
qqq
p
qqq
p pppppppppppppp
p
p
p
p
p
p
p
v
p
qq
k4
pv
ppppppppppppppppppppppppppppqqqqqppppp
qqq pppppppppppppp pppppppppp
k2
q
pp
ppqqqppppppppppppppppppp ppppppp
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
p
v
pppppppppppppppp
qqqq
k5
q
qq
q
q
q
q
q
q
q
q
q
q
q
q
q
q
qq
qqqq
qqqqqqq
qqqq
qqqqqqq
q
q
q
q
q
qqqqqq
qqqqq
qqqqqqq
qqqqqqqqqqqqqqqqqqqqqqqqq
u
..........
.......
k4
..........
.......
k2
....
.............
.............
.............
...................................
..... .................
.............
.............
.............
....
u
..........
.......
k5
....
.............
.............
.............
...................................
.... ................
.............
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
......
..
A BBILDUNG 9.3: Direkte Verkettung
Damit ergeben sich folgende worst-case Laufzeiten:
insert: O(1) (neues Element vor die Liste hängen)
search: O(n) (n = Länge der Liste)
delete: O(1) (vorausgesetzt, man verwendet doppelt verkettete Listen und hat das
Element schon gefunden)
9.3.2 Open Hashing
Beim Open Hashing werden alle Einträge in der Hashtabelle gehalten. Ist eine
Komponente der Tabelle schon belegt, so wird ein freier Platz für einen weiteren
Eintrag gesucht.
qqqqqqqqqqqqqqqqqqqq
qqqqq
ppppppp
qqqqqqqqqqqqqq qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq qqqqqqqqqqqqqqqqqqqqqqq
q
q
q
q
q
q
q
qpqpqppppppppppppppppppp ppppppppp ppp
qqq
p
p
p
q
p
p
p
p
qq qq
p
pp qqq
pppppp pppppppppppp
q
qqq
ppppppppppppppppppp
p
p
qq
p
p
p
p
p
p
p
p
p
qqq
p
p
p
p
p
p
p
k4 v
q
pppppppppppppppppppppppppppppqqqqqpppp
v
qqq pppppppppppppppppppppppppp
k2
q
pppp pp
p p p qqq p p pppppppp ppppppppp
p
p
p
p
p
p
q
v
p
p
qq pp ppppp
k5
qq ppppppp pppp p
q
q
p p p p p p p p p p p p pp
qqqqqqqqqqqqqqq
qqqqqqq
qqqq
q
q
q
q
q
q
qqqq
qqqqqq
qqqqqq
qqqqq
qqqqqqq
q
qqqqqqqqqqqqqqqqqqqqqqq
....
.............
.............
.............
................................
............ .........................
.
.
.
.
.
.
.
.
.
.
.
.
...........
.......
A BBILDUNG 9.4: Open Hashing mit linearer Verschiebung
Es gibt u.a. folgende Strategien zur Suche eines freien Platzes:
1. Lineare Verschiebung
132
9.3 Strategien zur Behandlung von Kollisionen
Falls h(k) bereits durch einen anderen Schlüssel besetzt ist, wird versucht,
k in den Adressen h(k) + 1, h(k) + 2, . . . unterzubringen (Abbildung 9.4).
Präziser gesagt, wird folgende Hashfunktion verwendet:
h0 (k, i) = h(k) + i mod m mit i = 0, 1, 2, . . . , m − 1.
Unter der Annahme, dass jeder Tabellenplatz entweder einen Schlüssel oder
NIL enthält, wenn der Tabellenplatz leer ist1 , können die Operationen insert
und search wie folgt implementiert werden:
Hash-Insert(T, k)
1 i←0
2 repeat
3
j ← h0 (k, i)
4
if T [j] = NIL then
5
T [j] ← k
6
return j
7
i←i+1
8 until i = m
9 error “hash table overflow”
Hash-Search(T, k)
1 i←0
2 repeat
3
j ← h0 (k, i)
4
if T [j] = k then
5
return j
6
i←i+1
7 until T [j] = NIL or i = m
8 error NIL
2. Quadratische Verschiebung
Es wird die Hashfunktion
h0 (k, i) = (h(k) + c1 i + c2 i2 ) mod m
mit i = 0, 1, 2, . . . , m − 1
verwendet. Dabei sind c1 , c2 ∈ N und c2 6= 0 geeignete Konstanten (s.
Cormen et al. [2]).
3. Double Hashing
Die Hashfunktion h0 wird definiert durch
h0 (k, i) = (h1 (k) + i · h2 (k)) mod m mit i = 0, 1, 2, . . . , m − 1,
1
Wenn die Schlüssel natürliche Zahlen sind, dann können wir z.B. den Wert −1 zur Markierung
eines leeren Tabellenplatzes benutzen.
133
9 Hashing
wobei h1 und h2 Hashfunktionen sind. Die Verschiebung wird dabei durch
eine zweite Hashfunktion realisiert. D.h. es wird zweimal, also doppelt, gehasht.
Beispiel 9.3.1 Wir illustrieren Open Hashing mit linearer Verschiebung an einem
Beispiel. Unsere Hashtabelle hat nur fünf Plätze und die Hashfunktion sei h(k) =
k mod 5. Die Werte 0 und 5 seien bereits eingetragen.
0
1
2
0
1
0
insert 11
1
-
2
0
1
11
0
insert 10
1
-
2
3
3
3
4
4
4
0
1
11
10
0
delete 1
-
1
2
3
0
d
11
10
search 10
-
4
Man erkennt: Gelöschte Felder müssen markiert werden, so dass ein Suchalgorithmus nicht abbricht, obwohl das Element doch in der Liste gewesen wäre.
Natürlich kann in die gelöschten Felder wieder etwas eingefügt werden. Dieses
Problem muss in der obigen Implementierung zusätzlich berücksichtigt werden.
n
, wobei n die Anzahl
Der Ladefaktor α für eine Hashtabelle T ist definiert als m
der gespeicherten Schlüssel und m die Kapazität der Tabelle sind. Theoretische
Untersuchungen und praktische Messungen haben ergeben, dass der Ladefaktor
einer Hashtabelle den Wert 0.8 nicht überschreiten sollte (d.h. die Hashtabelle
darf höchstens zu 80% gefüllt werden). Ist der Ladefaktor ≤ 0.8, so treten beim
Suchen im Durchschnitt ≤ 3 Kollisionen auf. Bei einem höheren Ladefaktor steigt
die Zahl der Kollisionen rasch an.
9.4 Die Klasse Hashtable in Java
Die Klasse java.util.Hashtable implementiert alle Methoden der abstrakten
Klasse java.util.Dictionary (vgl. Aufgabe 6.4.4). Außerdem enthält Hashtable
noch folgende Methoden2 :
public synchronized boolean containsKey(Object key)
Es wird true zurückgegeben gdw. die Hashtabelle ein Element unter key
verzeichnet hat.
public synchronized boolean contains(Object element)
Gdw. das angegebene element ein Element der Hashtabelle ist, wird true
2
Das Voranstellen des Schlüsselworts synchronized bewirkt, dass eine Methode nicht in mehreren nebenläufigen Prozessen (threads) gleichzeitig mehrfach ausgeführt wird. Ansonsten
könnte es zu Inkonsistenzen in der Hashtabelle kommen.
134
9.4 Die Klasse Hashtable in Java
zurückgegeben. Diese Operation ist teurer als die containsKey-Methode,
da Hashtabellen nur beim Suchen nach Schlüsseln effizient sind.
public synchronized void clear()
Alle Schlüssel in der Hashtabelle werden gelöscht. Wenn es keine Referenzen mehr auf die Elemente gibt, werden sie vom Garbage-Collector aus
dem Speicher entfernt.
public synchronized Object clone()
Es wird ein Klon der Hashtabelle erzeugt. Die Elemente und Schlüssel selbst
werden aber nicht geklont.
Ein Hashtabellenobjekt wächst automatisch, wenn es zu stark belegt wird. Es ist
zu stark belegt, wenn der Ladefaktor der Tabelle überschritten wird. Wenn eine
Hashtabelle wächst, wählt sie eine neue Kapazität, die ungefähr das doppelte
der aktuellen beträgt. Das Wählen einer Primzahl als Kapazität ist wesentlich für
die Leistung, daher wird das Hashtabellenobjekt eine Kapazitätsangabe auf eine
nahegelegene Primzahl anpassen. Die anfängliche Kapazität und der Ladefaktor
kann von den Konstruktoren der Klasse Hashtable gesetzt werden:
public Hashtable()
Es wird eine neue, leere Hashtabelle mit einer voreingestellten Anfangskapazität von 11 und einem Ladefaktor von 0.75 erzeugt.
public Hashtable(int initialCapacity)
Eine neue, leere Hashtabelle mit der Anfangskapazität initialCapacity
und dem Ladefaktor 0.75 wird generiert.
public Hashtable(int initialCapacity, float loadFactor)
Es wird eine neue, leere Hastabelle erzeugt, die eine Anfangskapazität der
Größe initialCapacity und einen Ladefaktor von loadFactor besitzt.
Der Ladefaktor ist eine Zahl zwischen 0.0 und 1.0 und definiert den Beginn
eines rehashings der Tabelle in eine größere.
Beispiel 9.4.1 Um die Benutzung der Klasse Hashtable als Wörterbuch (Dictionary) zu demonstrieren, entwerfen wir zunächst eine Klasse Pair zur Speicherung von Name-Wert Paaren. Namen sind vom Typ String. Werte können
beliebigen Typ haben, deshalb wird der Wert in einer Variable vom Typ Object
abgelegt.
class Pair {
private String name;
private Object value;
public Pair(String name, Object value) {
135
9 Hashing
this.name = name;
this.value = value;
}
public String name() {
return name;
}
public Object value() {
return value;
}
public Object value(Object newValue) {
Object oldValue = value;
value = newValue;
return oldValue;
}
}
Der Name ist nur lesbar, denn er soll als Schlüssel in einer Hashtabelle benutzt
werden. Könnte das Datenfeld für den Namen von außerhalb der Klasse modifiziert werden, so könnte der zugehörige Wert verlorengehen: Er würde immer
noch unter dem alten Namen eingeordnet sein und nicht unter dem modifizierten
Namen. Der Wert hingegen kann jederzeit verändert werden.
Folgende Schnittstelle deklariert drei Methoden: eine, um einem Dic-Objekt ein
neues Paar hinzuzufügen; eine, um herauszufinden, ob in einem Dic-Objekt bereits ein Paar mit gegebenem Namen enthalten ist; und eine, um ein Paar aus
einem Dic-Objekt zu löschen.
interface Dic {
void add(Pair newPair);
Pair find(String pairName);
Pair delete(String pairName);
}
Hier nun das Beispiel einer einfachen Implementierung von Dic, die die Hilfsklasse java.util.Hashtable verwendet:
import java.util.Hashtable;
class DicImpl implements Dic {
protected Hashtable pairTable = new Hashtable();
136
9.5 Aufgaben
public void add(Pair newPair) {
pairTable.put(newPair.name(), newPair);
}
public Pair find(String name) {
return (Pair) pairTable.get(name);
}
public Pair delete(String name) {
return (Pair) pairTable.remove(name);
}
}
Der Initialisierer für pairTable erzeugt ein Hashtable-Objekt, um Paare zu speichern. Die Klasse Hashtable erledigt die meiste anfallende Arbeit. Sie verwendet die Methode hashCode des Objekts, um jedes ihr als Schlüssel übergebene
Objekt einzuordnen. Wir brauchen keine explizite Hashfunktion bereitzustellen,
denn String enthält schon eine geeignete hashCode-Implementierung.
Kommt ein neues Paar hinzu, wird das Pair-Objekt in einer Hashtabelle unter
seinem Namen gespeichert. Wir können dann einfach die Hashtabelle benutzen,
um Paare über deren Namen zu finden und zu entfernen.
9.5 Aufgaben
Aufgabe 9.5.1 Implementieren Sie in Java das Verfahren zum Open-Hashing.
Beschränken Sie sich dabei auf eine Methode hashinsert zum Einfügen in die
Hashtabelle. Verwenden Sie die primäre Hashfunktion h(k) = k mod m und die
folgenden drei Verfahren zur Kollisionsbehandlung:
(1) lineare Verschiebung;
(2) quadratische Verschiebung mit c1 = 1 und c2 = 3;
(3) double hashing mit h1 (k) = k mod m und h2 (k) = 1 + (k mod (m − 1)).
Verwenden Sie nun Ihre Implementierung, um die Schlüssel 10, 22, 31, 4, 15, 28,
17, 88, 59 in eine Hashtabelle der Größe m = 11 einzutragen. Geben Sie die
Hashtabelle nach dem Einfügen jedes Schlüssels an.
Aufgabe 9.5.2 Sei m die Größe einer Hashtabelle. Wir definieren die Hashfunktion hstring, die für einen String s einen ganzzahligen Wert hstring(m, s) liefert,
wie folgt:
hstring(m, s) = hstring0 (0, s) mod m.
137
9 Hashing
Dabei ist hstring0 in Haskell-ähnlicher Notation wie folgt definiert:
und
hstring(i, []) = i
hstring0 (i, a : w) = hstring0 (i · 31 + ord(a), w)
wobei ord eine Abbildung ist, die für ein Zeichen aus dem ASCII-Alphabet eine
eindeutige ganze Zahl zwischen 0 und 255 liefert, nämlich ihren ASCII-Wert.
1. Implementieren Sie die Hashfunktion hstring in Java. hstring0 sollte dabei
iterativ implementiert werden. Die Summen zur Berechnung des Hashwertes wachsen sehr schnell. Verwenden Sie daher long-Werte zur Summation, um einen Überlauf zu vermeiden.
2. Erstellen Sie sich eine Datei strings mit 1000 ASCII-Zufallssequenzen der
Länge 8. Wenden Sie hstring auf jede dieser Sequenzen an. Wählen Sie dabei nacheinander in verschiedenen Programmläufen m ∈ {67, 101, 113, 197}.
3. Eine gute Hashfunktion wird die 1000 Strings gleichmäßig auf die m Einträge in der Hashtabelle abbilden, d.h. im Idealfall werden etwa 1000/m
Strings auf jeden Eintrag abgebildet. Bestimmen Sie die Güte der Hashfunktion hstring, indem Sie
m−1
1 X
|1000/m − occ(i)|
g(m, hstring) =
m i=1
berechnen. Dabei ist occ(i) die Anzahl der Strings w mit hstring(m, w) = i.
Welcher Wert ergibt sich für die Güte g(m, hstring) für m ∈ {67, 101, 113, 197}?
138
10 Ein- und Ausgabe
In den bisherigen Programmbeispielen wurden die Benutzereingaben
immer über die Kommandozeile und Ausgaben immer durch Aufruf der Methode System.out.println realisiert. Tatsächlich sind die
Ein- und Ausgabemöglichkeiten von Java weit mächtiger. Dieses Kapitel behandelt die Grundlagen hiervon, Ströme und einige Beispiele
für deren Verwendung.
10.1 Ströme
Input und Output (kurz I/O) in Java basieren auf Strömen (engl. streams). Sie
sind geordnete Folgen von Daten, die einen Ursprung (input stream) oder ein
Ziel (output stream) haben.
Vorteil: Der Programmierer wird von den spezifischen Details des zugrundeliegenden Betriebssystems befreit, indem Zugriffe auf Systemressourcen einheitlich mittels Strömen möglich sind.
Die Standard-Ströme eines Java-Programms sind in Abbildung 10.1 dargestellt.
Standard−Eingabestrom
System.in
Java−
Programm
Standard−Ausgabestrom
System.out
Standard−Fehlerstrom
System.err
A BBILDUNG 10.1: Standardströme
Das Paket java.io definiert abstrakte Klassen für grundlegende Ein- und Ausgabeströme. Diese abstrakten Klassen werden dann erweitert und liefern mehrere
nützliche Stromtypen. Derzeit enthält das I/O-Paket 13 Unterklassen, die teilweise wiederum Unterklassen mit weiteren Unterklassen besitzen (Abbildung 10.2).
139
10 Ein- und Ausgabe
Object
HH
H
HH
j
InputStream
OutputStream
HH
@
FileInputStream
@
R
@
...
?
FilterInputStream
@
@
R
@
...
DataInputStream
?
BufferedInputStream
...
HH
j
H
FileOutputStream
?
FilterOutputStream
H XXX
XXX
HH
XXX
HH
z
X
j
...
DataOutputStream
PrintStream
?
BufferedOutputStream
A BBILDUNG 10.2: I/O-Klassenhierarchie
10.1.1 Die Klasse InputStream
Alle InputStream erweiternden Klassen können Bytes aus verschiedenen Quellen
einlesen. Das Einlesen kann byteweise oder in Blöcken von Bytes beliebiger Größe
durchgeführt werden. Wir listen hier nur die Methoden von InputStream auf
und gehen nicht detailliert auf deren Unterklassen ein.
public abstract int read() throws IOException;
Es wird das nächste Byte aus dem Strom gelesen und (als Integerzahl im
Bereich von 0 bis 255) zurückgegeben. Bei Erreichen des Stromendes wird
−1 zurückgegeben.
public int read(byte b[]) throws IOException;
Es werden b.length Bytes aus dem Strom gelesen und in dem Feld b abgelegt. Der Rückgabewert ist die tatsächliche Anzahl der gelesenen Bytes
oder bei Erreichen des Stromendes −1.
public int read(byte b[], int off, int len) throws IOException;
Es werden len Bytes gelesen und ab der Position off im Feld b abgelegt.
Die Anzahl der gelesenen Bytes wird wieder zurückgegeben.
public long skip(long n) throws IOException;
Die nächsten n Bytes werden überlesen.
140
10.1 Ströme
public int available() throws IOException;
Es wird die Anzahl der Bytes zurückgegeben, die von dem Strom noch gelesen werden können.
public void close() throws IOException;
Der Strom wird geschlossen und die benutzten Ressourcen sofort wieder
freigegeben.
public boolean markSupported();
Es wird geprüft, ob der Strom die Methoden mark und reset unterstützt.
public synchronized void mark(int readLimit);
Die aktuelle Position im Strom wird markiert.
public synchronized void reset() throws IOException;
Es wird zu der Position zurückgesprungen, die mit mark markiert wurde.
Analog zu InputStream und seinen Unterklassen gibt es die Klasse OutputStream
und deren Erweiterungen, die zum Schreiben von Zeichen benötigt werden.
10.1.2 File-Ströme
Um Dateien einlesen zu können, reicht es, ein FileInputStream-Objekt anzulegen.
Beispiel 10.1.1
import java.io.*;
class ReadDemo {
public static void main(String[] args) throws FileNotFoundException,
IOException {
FileInputStream fileInputStream = new FileInputStream(args[0]);
int ch, count=0;
while((ch = fileInputStream.read()) != -1) {
count++;
}
System.out.println("Die Datei enthält "+count+" Bytes.");
}
}
Ausgaben werden durch die Klasse FileOutputStream ermöglicht, z.B.
FileOutStream fos = new FileOuputStream("out.txt");
fos.write(’a’);
141
10 Ein- und Ausgabe
10.1.3 Gepufferte Ströme
Die Klassen BufferedInputStream und BufferedOutputStream unterstützen Objekte, die ihre Daten puffern. Damit verhindern diese, dass jedes Lesen oder
Schreiben sofort an den nächsten Strom weitergegeben wird. Diese Klassen werden oft in Verbindung mit File-Strömen verwendet, denn es ist relativ ineffizient,
auf eine Datei zuzugreifen, die auf der Festplatte gespeichert ist. Das Puffern
hilft, diesen Aufwand erheblich zu reduzieren.
Ein BufferedInputStream oder BufferedOutputStream entspricht in seinen Methoden dem InputStream bzw. OutputStream. Die Konstruktoren nehmen einen
entsprechenden Strom als Parameter. Bei einem Methodenaufruf werden die Daten gepuffert und bei einem vollen Puffer mit den entsprechenden Methoden des
Stroms geschrieben bzw. gelesen. Manuell lässt sich ein Puffer auch immer mit
der Methode public void flush() leeren.
Beispiel 10.1.2
import java.io.*;
import java.util.*;
class WriteDemo {
private static float time(OutputStream os, long j) throws IOException {
Date start = new Date();
for(int i=0; i<j; i++) {
os.write(1);
}
os.close();
Date end = new Date();
return (float)(end.getTime()-start.getTime());
}
public static void main(String[] args) throws IOException {
FileOutputStream unbufStream;
BufferedOutputStream bufStream;
long iterate;
iterate = Long.parseLong(args[0]);
unbufStream = new FileOutputStream("f.unbuffered");
bufStream = new BufferedOutputStream(
new FileOutputStream("f.buffered"));
float t1 = time(unbufStream,iterate);
142
10.1 Ströme
System.out.println("Write file unbuffered: "+t1+" ms");
float t2 = time(bufStream,iterate);
System.out.println("Write file
buffered: "+t2+" ms");
System.out.println("Thus, the version with buffered streams is "+
(t1/t2)+" times faster.");
}
}
> java WriteDemo 10000
Write file unbuffered: 102.0 ms
Write file
buffered: 4.0 ms
Thus, the version with buffered streams is 25.5 times faster.
> java WriteDemo 100000
Write file unbuffered: 964.0 ms
Write file
buffered: 14.0 ms
Thus, the version with buffered streams is 68.85714 times faster.
10.1.4 Datenströme
Man stellt bei häufigem Gebrauch von Strömen schnell fest, dass Byteströme
allein kein Format bieten, in das alle Daten eingezwängt werden können. Vor
allem die elementaren Datentypen von Java können in den bisher behandelten
Strömen weder gelesen noch geschrieben werden. Die Klassen DataInputStream
und DataOutputStream definieren Methoden zum Lesen und Schreiben, die komplexe Datenströme unterstützen.
FileInputStream
fis = new FileInputStream(args[0]);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream
dis = new DataInputStream(bis);
oder als ein Befehl
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(args[0])));
Beispiel 10.1.3
import java.io.*;
class DataIODemo {
public static void main(String[] args) throws FileNotFoundException,
143
10 Ein- und Ausgabe
IOException
{
DataOutputStream dataOutputStream
= new DataOutputStream(new FileOutputStream(args[0]));
double[] doubles = {16.57,15.44,9.99};
for(int i=0; i<doubles.length; i++) {
dataOutputStream.writeDouble(doubles[i]);
}
dataOutputStream.close();
DataInputStream dis
= new DataInputStream(new FileInputStream(args[0]));
double sum = 0.0;
try {
while(true) {
//Read next double until EOF is reached and an EOFException
//is thrown; add the double to sum
sum += dis.readDouble();
}
}
catch(EOFException e) {
System.out.println("Sum of doubles: "+sum);
}
finally {
dis.close();
}
}
}
> java DataIODemo test.dat
Sum of doubles : 42.0
10.2 Stream Tokenizer
Das Zerlegen von Eingabedaten in Token ist ein häufiges Problem. Java stellt eine
Klasse StreamTokenizer für solche einfachen lexikalischen Analysen zur Verfügung. Dabei wird gegenwärtig nur mit den untersten 8 Bits von Unicode, den
Zeichen aus dem Latin-1 Zeichensatz, gearbeitet, da das interne, die Zeichentypen markierende, Feld nur 256 Elemente hat.
144
10.2 Stream Tokenizer
Erkennt nextToken() ein Token, gibt es den Tokentyp als seinen Wert zurück
und setzt das Datenfeld ttype auf denselben Wert. Es gibt vier Tokentypen:
TT_WORD: Ein Wort wurde eingescannt. Das Datenfeld sval vom Typ String
enthält das gefundene Wort.
TT_NUMBER: Eine Zahl wurde eingescannt. Das Datenfeld nval vom Typ double
enthält den Wert der Zahl. Nur dezimale Gleitkommazahlen (mit oder ohne Dezimalpunkt) werden erkannt. Die Analyse versteht weder 3.4e79 als
Gleitkommazahl noch 0xffff als Hexadezimalzahl.
TT_EOL: Ein Zeilenende wurde gefunden (nur wenn eolIsSignificant true ist).
TT_EOF: Das Eingabeende wurde erreicht.
Beispiel 10.2.1
import java.io.*;
class TokenDemo {
public static void main(String[] args) throws IOException {
FileReader fileIn = new FileReader(args[0]);
StreamTokenizer st = new StreamTokenizer(fileIn);
int
int
int
int
words
numbers
eols
others
=
=
=
=
0;
0;
0;
0;
//
//
//
//
total
total
total
total
word number.
int number.
eols.
others.
st.eolIsSignificant(true); // to treat eols as a character
while(st.nextToken() != StreamTokenizer.TT_EOF) {
// to read next Token and check EOF
if(st.ttype == StreamTokenizer.TT_WORD)
words++;
// if word then total words++.
else if(st.ttype == StreamTokenizer.TT_NUMBER)
numbers++; // if number then total numbers++.
else if(st.ttype == StreamTokenizer.TT_EOL)
eols++;
// if eols total eols++.
else
others++; // else others++.
}
System.out.println("File "+args[0]+" contains\n\t"+
words+" words,\n\t"+
145
10 Ein- und Ausgabe
numbers+" numbers,\n\t"+
eols+" end-of-lines, and\n\t"+
others+" other characters.");
}
}
> java TokenDemo TokenDemo.java
File TokenDemo.java contains
61 words,
6 numbers,
31 end-of-lines, and
88 other characters.
146
11 Graphische Benutzeroberflächen
mit Swing
Anmerkung: Dieses Kapitel des Skripts soll einen kleinen Einblick in
die Programmierung von graphischen Benutzeroberflächen mit Java
geben. Es erhebt keinen Anspruch auf Vollständigkeit. Literatur: [9,
10].
11.1 Historisches
Die erste API zur Generierung von grafischen Benutzeroberflächen für Java war
das Abstract Window Toolkit (AWT). AWT bietet einen Satz von GUI Elementen, Ereignisbehandlung und einfache Funktionen. Jede grafische Komponente
des AWT wird auf eine Komponente der darunter liegenden Plattform abgebildet. Auf Grund der Plattformunabhängigkeit Javas und der Verschiedenheit der
unterstützten Plattformen beschränkt sich das AWT auf den kleinsten gemeinsamen Nenner an GUI Elementen, die in allen System vorhanden sind. Da jede
AWT Komponente Resourcen vom nativen System bezieht und diese nicht von
der Java Virtual Machine (JVM) verwaltet werden, spricht man bei AWT Komponenten von sogenannten ”Schwergewichtigen Komponenten” (heavyweight
components). Die später entwickelten und mit JDK 1.1 erstmals als Add-On
verfügbaren Java Found Classes (JFC), auch Swing genannt, sind sogenannte
”Leichtgewichtige Komponenten” (lightweight components). Swing Komponenten haben kein direktes Gegenstück im nativen System, sondern werden von Java verwaltet und gezeichnet. Der größte Vorteil von JFC Swing sind die Vielzahl
von GUI Komponenten, die mit jedem Java Release anwachsen und einfach zu
erweitern sind. (JFC/Swing ist im Gegensatz zu dem AWT vollständig OO implementiert).
11.2 Ein Fenster zur Welt
Den Einstieg in jedes ”graphische” Programm ist ein Fenster, der sogenannte
Top-Level Container. Ein Fenster kann mit der Swing Klasse JFrame erzeugt werden.
147
11 Graphische Benutzeroberflächen mit Swing
Beispiel 11.2.1
01: import javax.swing.JFrame;
02: public class HelloSwingFrame {
03:
public static void main(String[] args) {
04:
JFrame f = new JFrame("Das Fenster zur Welt!");
05:
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
06:
f.setSize(300,200);
07:
f.setVisible(true);
08:
}
09: }
A BBILDUNG 11.1: Screenshot: Ein Fenster zur Welt!
11.3 Swing Komponenten
Die Klasse JComponent bildet die Basisklasse für alle Swing Komponenten und
stellt viele grundlegende Funktionen zur Verfügung, wie z.B. die austauschbare
Darstellung (Look&Feel), Tooltips, Rahmen. Die Klassen JWindow (Repräsentation
eines Fenster) und JFrame sind von ihren jeweiligen AWT Penedants abgeleitet.
Neben den im folgenden vorgestellten Komponenten JButton und JTextField
gibt es eine (mit jedem Java Release wachsende) Anzahl von GUI Kompenten.
Komponenten, die keine Container sind, also keine weiteren Komponenten enthalten können, heissen atomare Komponenten.
11.3.1 Eine einfache Schaltfläche - JButton
Eine Schaltfläche ermöglicht es dem Anwender, eine Aktion auszulösen. Schaltflächen sind in der Regel beschriftet und/oder haben eine Grafik(Icon). Wie
148
11.3 Swing Komponenten
A BBILDUNG 11.2: Klassenhierachie der grundlegendesten Swing Komponenten
man der Klassenhierachie (siehe Abbildung 11.3) entnehmen kann, basieren alle Schaltflächen von Swing auf der gemeinsamen Oberklasse AbstractButton,
welche die grundlegendsten Eigenschaften, die allen Schaltflächen gemein sind,
implementiert.
A BBILDUNG 11.3: Klassenhierachie: Schaltflächen
Beispiel 11.3.1
01:import javax.swing.JButton;
02:import javax.swing.JFrame;
03:
04:public class Beispiel_JButton {
05:
06:
public static void main(String [] args){
07:
JFrame f = new JFrame("Das Fenster zur Welt!");
08:
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
09:
f.add(new JButton("Ich bin ein JButton!"));
10:
f.setSize(300,200);
11:
f.setVisible(true);
12:
}
13:}
149
11 Graphische Benutzeroberflächen mit Swing
A BBILDUNG 11.4: Screenshot: Eine einfache Schaltfläche - JButton
11.3.2 Ein Texteingabefeld - JTextField
Das Texteingabefeld (JTextField) von JFC/Swing ist die einfachste Möglichkeit
kleinere (einzeilige) Mengen Text von Seiten des Benutzers (weiter-)zuverarbeiten.
Wenn die Texteingabe abgeschlossen ist (durch Return/Enter) wird ein ActionEvent ausgelöst. Weitere Möglichkeiten der Textverarbeitung sind u.a. Labels (JLabel, für statische, durch den Benutzer unveränderbare Textinformationen) und
Textflächen (JTextArea- mehrzeilige Benutzereingaben).
A BBILDUNG 11.5: Klassenhierachie: Textkomponenten
Beispiel 11.3.2
01:import javax.swing.JFrame;
02:import javax.swing.JTextField;
03:
04:public class Beispiel_JTextField {
05:
06:
public static void main(String[] args) {
150
11.4 Container
07:
08:
09:
10:
11:
12:
13:
14:}
JFrame f = new JFrame("Das Fenster zur Welt!");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new JTextField("Ich bin ein JTexfield!",60));
f.setSize(300,200);
f.setVisible(true);
}
A BBILDUNG 11.6: Screenshot: Ein Texteingabefeld - JTextfield
11.4 Container
Alle Swing Komponenten müssen in einem Container plaziert werden. Container
sind Komponenten, die dazu dienen, andere Komponenten aufzunehmen und zu
verwalten. Der wichtigste und bekannteste Container neben dem grundlegenden
JFrame ist das JPanel, das die ihm zugeordeneten JComponents nach einem
zugewiesenen Layoutverfahren anordnet. Daneben gibt es noch JScrollPane,
JTabbedPane, JSplitPane, JToolBar und viele mehr.
11.5 LayoutManager
Ein LayoutManager ist dafür zuständig, die Komponenten eines Containers anzuordnen. Je nach Art des verwendeten LayoutManagers ist die Strategie der
Anordung verschieden. Im folgenden werden die wichtigsten LayoutManager
vorgestellt. Die Methode void setLayout(LayoutManager) weist einem Container einen LayoutManager zu. Ausser den hier kurz vorgestellten LayoutManagern FlowLayout, BorderLayout, GridLayout und BoxLayout gibt es noch die
151
11 Graphische Benutzeroberflächen mit Swing
GridBagLayout, CardLayout, SpringLayout und GroupLayout Manager.
Komplexe grafische Oberflächen lassen sich i.d.R. durch geschicktes Kombinieren
der hier vorgestellten LayoutManager erreichen.
11.5.1 FlowLayout
Der FlowLayout Manager ordnet die Komponenten von links nach rechts an, ohne die Grösse der Komponenten anzupassen und ist der StandardLayoutManager
eines Containers.
Beispiel 11.5.1
01:import javax.swing.JButton;
02:import javax.swing.JFrame;
03:import javax.swing.JPanel;
04:
05:public class Beispiel_FlowLayout extends JPanel{
06:
07:
public Beispiel_FlowLayout(){
08:
for(int i = 1; i <= 5; ++i){
09:
add(new JButton("Button "+(Math.pow(10, i))));
10:
}
11:
}
12:
13:
public static void main(String[] args) {
14:
JFrame f = new JFrame("FlowLayout");
15:
f.add(new Beispiel_FlowLayout());
16:
f.pack();
17:
f.setVisible(true);
18:
}
19:
20:}
A BBILDUNG 11.7: Screenshot: FlowLayout
152
11.5 LayoutManager
11.5.2 BorderLayout
Der BorderLayout Manager ordnet die Komponenten nach den Himmelsrichtungen an (Norden, Osten, Süden, Westen, Mitte).
Beispiel 11.5.2
01:import java.awt.BorderLayout;
02:import javax.swing.JButton;
03:import javax.swing.JFrame;
04:import javax.swing.JPanel;
05:
06:public class Beispiel_BorderLayout extends JPanel{
07:
08:
public Beispiel_BorderLayout(){
09:
setLayout(new BorderLayout());
10:
add(new JButton("Norden"),BorderLayout.NORTH);
11:
add(new JButton("Westen"),BorderLayout.WEST);
12:
add(new JButton("Osten"),BorderLayout.EAST);
13:
add(new JButton("S\"uden"),BorderLayout.SOUTH);
14:
add(new JButton("Mitte"),BorderLayout.CENTER);
15:
}
16:
17:
public static void main(String[] args) {
18:
JFrame f = new JFrame("BorderLayout");
19:
f.add(new Beispiel_BorderLayout());
20:
f.pack();
21:
f.setVisible(true);
22:
}
23:}
A BBILDUNG 11.8: Screenshot: BorderLayout
153
11 Graphische Benutzeroberflächen mit Swing
11.5.3 GridLayout
Der GridLayout Manager ordnet die Komponenten in einem Raster (Gitter) gegebener Grösse an, die Ausmasse aller Komponenten sind gleich.
Beispiel 11.5.3
01:import java.awt.GridLayout;
02:import javax.swing.JButton;
03:import javax.swing.JFrame;
04:import javax.swing.JPanel;
05:
06:public class Beispiel_GridLayout extends JPanel {
07:
08:
public Beispiel_GridLayout(){
09:
setLayout(new GridLayout(3,3));
10:
for (int i = 9; i >= 1; --i){
11:
add(new JButton(new Integer(i).toString()));
12:
}
13:
}
14:
15:
public static void main(String[] args) {
16:
JFrame f = new JFrame("GridLayout");
17:
f.add(new Beispiel_GridLayout());
18:
f.pack();
19:
f.setVisible(true);
20:
}
21:}
A BBILDUNG 11.9: Screenshot: GridLayout
154
11.5 LayoutManager
11.5.4 BoxLayout
Der BoxLayout Manager ordnet die Komponenten in X bzw. Y Richtung an, die
Aussmasse aller Komponenten sind gleich.
Beispiel 11.5.4
01:import javax.swing.BoxLayout;
02:import javax.swing.JButton;
03:import javax.swing.JFrame;
04:import javax.swing.JPanel;
05:
06:public class Beispiel_BoxLayout extends JPanel {
07:
08:
public Beispiel_BoxLayout(){
09:
this(BoxLayout.X_AXIS);
10:
}
11:
12:
public Beispiel_BoxLayout(int direction){
13:
setLayout(new BoxLayout(this, direction));
14:
for(int i = 1; i <=5; ++i){
15:
add(new JButton(new Integer(i).toString()));
16:
}
17:
}
18:
19:
public static void main(String[] args) {
20:
JFrame f = new JFrame("BoxLayout");
21:
f.add(new Beispiel_BoxLayout(BoxLayout.Y_AXIS));
22:
f.pack();
23:
f.setVisible(true);
24:
}
25:}
A BBILDUNG 11.10: Screenshot: BoxLayout mit Stil BoxLayout.Y_AXIS
155
11 Graphische Benutzeroberflächen mit Swing
11.6 Ereignisse und deren Behandlung
Eine Komponente kann ein Ereignis(Event) auslösen, z.B. alle AbstractButton
Klassen ein ActionEvent. Jedes Event wird durch eine Klasse repräsentiert, die das
entsprechende ListenerInterface implementiert (z.B. ActionListener um ActionEvents zu verarbeiten). Dadurch ist eine strikte Trennung zwischen der Quelle und
der Verarbeitung des Ereignis möglich (und meistens auch sinnvoll, vgl. 11.6.1).
11.6.1 ActionListener
Klickt man auf die Schaltfläche des Beispiels (siehe 11.3.1), so sollte dies eine
(Re-)Aktion des Programms zur Folge haben. Die Aktion wird in Form eines
ActionEvent-Objekts an einen möglichen Zuhörer (in diesem Fall einen ActionListener) gesandt. Ein ActionListener wird mit der Methode addActionListener()
an die Komponenten gebunden, die Aktionen auslösen können. Der ActionListener ist eine Schnittstelle mit der Methode actionPerformed(), die von der
implementierenden Klasse bereitgestellt werden muss. Die verschiedenen Möglichkeiten der Implementierung sind : als externe Klasse, in der Klasse selbst, als
interne Klasse oder als anonyme Klasse. Die erste beiden Möglichkeiten sollen an
dieser Stelle kurz vorgestellt werden.
Externe Klasse
Die beste - leider aber oft auch die aufwendigste - Art einen (Action-)Listener
zu implementieren ist als externe Klasse. Der grös̈te Vorteil neben der Übersichtlichkeit besteht in der Wiederverwendbarkeit des Listener für mehr als nur eine
auslösende Komponente.
Beispiel 11.6.1
01: java.awt.event.ActionEvent;
02:
03: public class MyActionListener implements ActionListener {
04:
public void actionPerformed(ActionEvent e) {
05:
System.out.println("Der JButton wurde gedr\"uckt!");
06:
}
07: }
01:import javax.swing.JButton;
02:import javax.swing.JFrame;
03:
04:public class Beispiel_JButton {
05:
06:
public static void main(String [] args){
156
11.6 Ereignisse und deren Behandlung
07:
08:
09:
10:
11:
12:
13:
14:
15:}
JButton j = new JButton("Ich bin ein JButton!");
j.addActionListener(new MyActionListener);
JFrame f = new JFrame("Das Fenster zur Welt!");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(j);
f.setSize(300,200);
f.setVisible(true);
}
Interne Klasse
Den (Action-)Listener in der Klasse zu implementieren ist eine Methode, die gerade von Einsteigern bevorzugt angewant wird, da man direkt auf die Komponenten der Klasse zugreifen (und diese manipulieren) kann. Nachteil, neben der
Übersichtlichkeit, wenn die Klasse mehr als nur ein (paar wenige) Komponenten
enthält, ist die schlechte Wiederverwendbarkeit.
Beispiel 11.6.2
01:public class MyButton extends JFrame implements ActionListener{
02:
03:
JButton button;
04:
05:
public MyButton() {
06:
button = new JButton("Button 1");
07:
button.addActionListener(this);
08:
add(button);
09:
}
10:
11:
public void actionPerformed(ActionEvent e) {
12:
System.out.println("Der Button wurde gedr\"uckt!");
13:
}
14:
15:
public static void main(String [] args) {
16:
MyButton mybutton = new MyButton();
17:
mybutton.pack();
18:
mybutton.setVisible(true);
19:
}
20:}
11.6.2 Events und Listener
Die Tabelle (11.6.2) ist eine (unvollständige) Übersicht über von Java/Swing unterstützten Events, den entsprechenden Listener und den Komponenten, die sie
157
11 Graphische Benutzeroberflächen mit Swing
unterstützen.
ActionEvent
ActionListener
addActionListener,
removeActionListener
ItemEvent
ItemListener
MouseEvent
MouseListener
TextEvent
TextListener
addItemListener,
removeItemListener
addMouseListener
removeMouseListener
addTextListener,
removeTextListener
WindowEvent
WindowListener
addWindowListener,
removeWindowListener
AbstractButton und alle Abkömmlinge (JButton, JRadioButton, JCheckboxButton, JMenuItem, . . . )
JCheckBox,
JComboBox,
JList, . . .
Components
und
Abkömmlinge
alle Abkömmlinge von
JTextComponent (JTextArea, JTextField . . . )
Window und Abkömmlinge (JFrame, JDialog, JFileDialog)
11.7 Java Applet vs. Java WebStart vs. Java
Application
Java Programme können auf drei verschiedene Arten und Weise gestartet werden, als Java Applikation, als Java Applet im Browser eingebettet, auf einer HTML
Seite, oder moderner als Java Webstart Applikation.
A BBILDUNG 11.11: Java Applet vs. Java Webstart vs. Java Application
Applikationen (Java Webstart und Java Application) verfügen über eine main()
Methode und Applets nicht. Applets erweitern stattdessen die Klasse JApplet
bzw. Applet. Programme, die sowohl Application als auch Applet sind, erweitern
sowohl die Klasse JApplet und implementieren eine main() Funktion.
158
11.7 Java Applet vs. Java WebStart vs. Java Application
11.7.1 Java (Webstart) Application
Java Applikationen werden von der Kommandozeile (oder der GUI) mittels einer
Java Laufzeitumgebung (JVM) gestartet. In der Regel unterliegen Java Applikationen keinerlei Einschränkungen hinsichtlich ihres Zugriff auf Systemresourcen.
Java Webstart Applikation werden ähnlich wie Applets in einer gesicherten Umgebung(Sandbox) gestart (siehe 11.7.3).
11.7.2 Java Applets
Ein Programm wird leicht zu einem Applet, wenn die Klasse JApplet erweitert
wird. Applets werden in der Regel in Webseiten eingebettet und vom Browser
mittels eines Java Plugins ausgeführt. Java Applets sind eine weitere Möglichkeit,
komplexe Programme weitestgehend Plattformunabhängig ohne eine lokale Installation auszuführen.
Beispiel 11.7.1
01:
02:
03:
04:
05:
06:
07:
08:
import javax.swing.JApplet;
import javax.swing.JLabel;
public class HelloWorld extends JApplet {
public void init(){
add(new JLabel("Hello World!");
}
}
11.7.3 Java Sicherheitskonzepte
Java-Programme können dynamisch Klassen aus einer Vielzahl von vertrauenswürdigen und nicht vertrauenswürdigen Quellen nachladen. Am problematischsten ist diese Eigenschaft bei Java-Programmen, die direkt über das ”Netz” als
Java-Applet oder Java Webstart Applikation gestartet werden. Ohne ein Sicherheitskonzept könnte ein böswilliges Programm Dateien löschen/verändern, persönliche Daten verschicken oder ähnlichen Unfug treiben. Das Java-Sicherheitskonzept
sieht vor, dass Java-Programme aus nicht vertrauenswürdigen Quellen in einem
vom Rest des System abgeschotteten Bereich gestartet werden, der sogenannten
Sandbox. Die Sandbox schränkt zum Beispiel den Zugriff auf das lokale Dateisystem ein bzw. unterbindet diesen vollständig.
159
11 Graphische Benutzeroberflächen mit Swing
160
12 Parallele Ausführung Nebenläufigkeit - Threads
Unter nebenläufiger Programmierung1 versteht man die Fähigkeit einer Programmiersprache, Programmteile quasi zeitgleich ausführbar
zu machen. Dies wird meist unter dem Konzept der Threads (engl.
Fäden) behandelt. Ein Thread wird dabei als eine in sich abgeschlossene Aufgabe definiert, die, bis auf Ein- und Ausgabe, unabhängig
von anderen Programmteilen laufen soll. Literatur: [11, 12].
12.1 Das Threadmodell von Java
Threads in Java laufen als eigene Programmteile innerhalb der Virtuellen Maschine (VM) ab und teilen sich dabei die für die VM allokierten Systemresourcen, wie
den Arbeitsspeicherbereich und offene Dateien. Auf einigen Betriebssystemen
korrespondieren diese User-Threads mit echten Threads des Betriebssystems, wie
zum Beispiel im Betriebssystem Solaris. Unterstützt das Betriebssystem von sich
aus keine Threads, so ändert sich aus Sicht des Java-Programmierers nichts, da
die VM die Threadverwaltung im Hintergrund übernimmt. Dies erlaubt es von
vorneherein Programme mit Threads umzusetzen, ohne damit die Portabilität
des Programms über Betriebssystemgrenzen hinaus einzuschränken.
Der einfachste Thread in Java wird automatisch erzeugt, wenn man ein Programm mit einer main-Methode aufruft. Dieser initiale main-Thread führt die
Methode aus. Andere Threads, die ständig im Hintergrund aktiv sind, sind zum
Beispiel der Garbage Collector und der Just-in-time-Compiler.
Eigene Threads kann man erstellen, indem man entweder direkt aus der Klasse
java.lang.Thread eine eigene Klasse ableitet, wie in Beispiel 12.4, oder man
implementiert das Interface java.lang.Runnable, wie in Beispiel 12.3, das lediglich die Methode public void run() enthält. Die Klasse Thread bietet viele
nützliche Methoden zum Starten und Stoppen eines Threads. Implementierungen von Runnable benötigen eine Instanz eines Thread, damit die Methode run
ausgeführt werden kann.
1
concurrency bzw. concurrent programming
161
12 Parallele Ausführung - Nebenläufigkeit - Threads
GarbageCollector
VM-Thread
erzeugt
User-Thread1
run()
Aufruf von
MeineKlasse.main()
gibt Resourcen frei
X
Ausführung
beendet
Virtuelle Maschine
A BBILDUNG 12.1: Schematische Darstellung der Ausführung eines eigenen Programms mit main-Methode in der Klasse MeineKlasse.java in der Java Virtual
Machine
VM-Thread
erzeugt
GarbageCollector
User-Thread1
run()
Aufruf von
MeineKlasse.main()
erzeugt
MeinThread
run()
X
Ausführung
beendet
gibt Resourcen frei
gibt Resourcen frei
X
Ausführung
beendet
Virtuelle Maschine
A BBILDUNG 12.2: Schematische Darstellung der Ausführung eines eigenen Programms mit main-Methode in der Klasse MeineKlasse.java und eines zusätzlichen
Threads MeinThread.java in der Java Virtual Machine
162
12.1 Das Threadmodell von Java
01: public class RunnableDemo implements Runnable {
02:
//Diese Methode stammt aus Runnable und muss
03:
//implementiert werden
04:
public void run() {
05:
//Zaehlen in einer Schleife von 0 bis 9
06:
for(int i=0;i<10;i++) {
07:
//Ausgabe jeder Zahl auf der Konsole
08:
System.out.println("Zahl "+i);
09:
}
10:
//fertig!
11:
System.out.println("Fertig!");
12:
}
13:
14:
public static void main(String[] args) {
15:
//Erzeugen einer neuen Instanz
16:
RunnableDemo rd = new RunnableDemo();
17:
//Erzeugen eines Thread, um das Runnable auszufuehren
18:
Thread t = new Thread(rd);
19:
//Thread starten
20:
t.start();
21:
}
22:}
A BBILDUNG 12.3: Eine Implementierung des Interfaces Runnable
01: public class ThreadDemo extends Thread {
02:
//Diese Methode stammt aus Runnable, ist aber in Thread
03:
//leer implementiert und muss daher ueberschrieben werden
04:
public void run() {
05:
//Zaehlen in einer Schleife von 0 bis 9
06:
for(int i=0;i<10;i++) {
07:
//Ausgabe jeder Zahl auf der Konsole
08:
System.out.println("Zahl "+i);
09:
}
10:
//fertig!
11:
System.out.println("Fertig!");
12:
}
13:
14:
public static void main(String[] args) {
15:
//Erzeugen einer neuen Instanz, inklusive Thread
16:
ThreadDemo td = new ThreadDemo();
17:
//Thread starten
18:
td.start();
19:
}
20:}
A BBILDUNG 12.4: Statt Implementierung des Interfaces Runnable nun Erweiterung der Klasse Thread
163
12 Parallele Ausführung - Nebenläufigkeit - Threads
12.2 Thread Pools
Ein Thread ist also ein Hilfsmittel, um ein Runnable ausführen zu können. Bei
der Erstellung vieler Threads kann dies zu einem hohen Aufwand für die Initialisierung neuer Objekte führen. Diesen kann man reduzieren, indem man sogenannte Thread Pools benutzt. Diese halten einen Vorrat an Threads bereit, an die
man Runnable-Objekte übergeben kann. Abbildung 12.5 zeigt schematisch die
Arbeitsweise eines CachedThreadPool, der eine bestimmte Anzahl Threads bereit
hält, um damit Runnables ausführen zu können. Der CachedThreadPool kann bei
Bedarf auch neue Threads erzeugen. Bei Thread Pools mit fester Anzahl Threads
werden überzählige Runnables in eine Warteschlange aufgenommen, um dann
von den nächsten freiwerdenden Threads ausgeführt zu werden. Meist gibt es
weniger Threads als Runnables, was dazu führt, dass überzählige Runnables in
einer Warteschlange für die Ausführung vorgemerkt werden.
>>ThreadPool<<
Executor
Thread2
erzeugt
Runnable
Thread1
finde nächsten freien Thread
run()
Aufruf von
Runnable
bin wieder frei
A BBILDUNG 12.5: Schematische Darstellung der Arbeitsweise eines Cached
Thread Pool
Im Paket java.util.concurrent gibt es sogenannte ExecutorService Klassen,
die die Funktionalität eines Thread Pools erfüllen. Ein Thread Pool mit der oben
beschriebenen Eigenschaft lässt sich durch den Aufruf der statischen Klassenmethode
Executors.newCachedThreadPool()
164
12.3 Besonderheiten bei Swing - Der Event Dispatch Thread
erzeugen. Ein Thread Pool mit einer fest vorgegebenen maximalen Anzahl an
Threads kann mit
Executors.newFixedThreadPool(int numberOfThreads)
erzeugt werden.2
12.3 Besonderheiten bei Swing - Der Event Dispatch
Thread
Insbesondere bei Swing werden häufig Fehler gemacht, die direkt mit Threads zu
tun haben. Swing Klassen und deren Methoden sind nur in wenigen Fällen synchronisiert, dass heisst, verschiedene Threads die zur gleichen Zeit versuchen Daten über set Methoden zu verindern, können zu einem inkonsistenten Zustand
des Objektes und zum Beispiel zu ArrayIndexOutOfBoundsExceptions führen.
Da Instanzen von Swing Klassen üblicherweise etwas anzeigen, kann es so dazu
kommen, dass Anzeige und Daten nicht konsistent sind. Daher sollten Änderungen am Zustand eines Swing-Objekts immer auf dem Event Dispatch Thread
ausgeführt werden. Dabei ist darauf zu achten, dass wirklich nur das Update auf
dem Event Dispatch Thread ausgeführt wird, und nicht etwa die Berechnungen,
die zu dem Ergebnis führen sollen. Solange ein Task im Event Dispatch Thread aktiv ist, wird zum Beispiel die GUI nicht aktualisiert, da diese Aktualisierung logisch
gesehen nach dem momentanen Task ausgeführt werden würde.
Der Event-Dispatch Thread stellt also die Synchronisierung aller Objekte der GUI
sicher, neue Tasks werden in eine Warteschlange aufgenommen, in der Reihenfolge ihres Eintreffens.3
Um festzustellen, ob der eigene Code auf dem Event Dispatch Thread ausgeführt
wird, kann man
boolean b = javax.swing.SwingUtilities.isEventDispatchThread();
aufrufen.
Für aufwendige Berechnungen, die im Hintergrund einer grafischen Benutzeroberfläche ausgeführt werden sollen, kann man seit der Version 6 der Java API
Implementierungen der Klasse javax.swing.SwingWorker<T,V> verwenden. Eine Beispielimplementierung ist in Beispiel 12.6 gegeben. SwingWorker führt die
Methode doInBackground() innerhalb eines eigenen Threads, unabhängig vom
Event Dispatch Thread aus. Das Ergebnis der Berechnung kann mit der blockierenden Methode get() abgefragt werden, dies sollte aber erst nach Beendigung
der Aufgabe des Workers geschehen, um ein Blockieren der GUI zu verhindern.4
2
http://java.sun.com/javase/6/docs/api/java/util/concurrent/Executors.html
http://java.sun.com/docs/books/tutorial/uiswing/concurrency/
4
http://java.sun.com/products/jfc/tsc/articles/threads/threads2.html
3
165
12 Parallele Ausführung - Nebenläufigkeit - Threads
01: import java.beans.PropertyChangeEvent;
02: import java.beans.PropertyChangeListener;
03: import java.util.ArrayList;
04: import java.util.List;
05: import java.util.Random;
06: import java.util.concurrent.ExecutionException;
07: import javax.swing.JFrame;
08: import javax.swing.JProgressBar;
09: import javax.swing.SwingUtilities;
10: import javax.swing.SwingWorker;
11:
12: public class MyWorker extends SwingWorker<ArrayList<Double>, Integer> {
13:
private ArrayList<Double> numbers = null;
14:
private int lastIndex = 0;
15:
private int nelements = 0;
16:
private boolean done = false;
17:
18:
public MyWorker(int elementsInList) {
19:
this.nelements = elementsInList;
20:
numbers = new ArrayList<Double>(this.nelements);
21:
}
22:
//Diese Methode wird im Hintergrund in einem Thread ausgefuehrt
23:
@Override
24:
public ArrayList<Double> doInBackground() {
25:
System.out.println("DoInBackground called!");
26:
//Initialisiere einen Zufallszahlengenerator mit
27:
//der aktuellen Systemzeit in Millisekunden
28:
Random r = new Random(System.currentTimeMillis());
29:
//Solange nicht n Zahlen berechnet sind und wir nicht
30:
//von ausserhalb gestoppt werden
31:
while (! (done) && ! isCancelled()) {
32:
//fuege neue Zahlen zu numbers hinzu
33:
addNumber(numbers,this.nelements,r);
34:
}
35:
return numbers;
36:
}
37:
//Hiermit fuegen wir eine neue Zahl hinzu
38:
protected void addNumber(List<Double> numbers, int nelements, Random r) {
39:
//Wenn wir schon genuegend Zahlen haben, setze done um abzubrechen
40:
if(lastIndex==nelements) {
41:
done = true;
42:
}
43:
else {
//sonst erzeuge eine neue Pseudozufallszahl
44:
Double d = new Double(r.nextDouble());
45:
numbers.add(d);
46:
//Prozentualen Fortschritt berechnen
47:
int prog = (int)Math.floor(100.0d*((double)(lastIndex+1)/(double)nelements));
48:
//Fortschritt setzen, benachrichtigt alle
49:
//registrierten PropertyChangeListener
50:
setProgress(prog);
51:
lastIndex++;
52:
}
53:
}
54:}
A BBILDUNG 12.6: Ein Beispiel für die Verwendung des SwingWorker
166
12.3 Besonderheiten bei Swing - Der Event Dispatch Thread
00: public class SwingWorkerExecutor {
01:
//Main Methode
02:
public static void main(String[] args) {
03:
//Erzeugen einer Fortschrittsanzeige
04:
final JProgressBar progressBar = new JProgressBar(0, 100);
05:
//Zeige Prozentwert an
06:
progressBar.setStringPainted(true);
07:
//Erzeuge neues Runnable fuer die grafische Oberflaeche
08:
Runnable r = new Runnable() {
09:
10:
@Override
11:
public void run() {
12:
JFrame jf = new JFrame("SwingWorkerDemo");
13:
jf.add(progressBar);
14:
jf.setVisible(true);
15:
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
16:
jf.pack();
17:
}
18:
19:
};
20:
//Erzeuge die grafische Oberflaeche auf dem Event-Dispatch-Thread
21:
SwingUtilities.invokeLater(r);
22:
//Starte unseren Task mit 5.000.000 zu erstellenden Zahlen
23:
MyWorker task = new MyWorker(5000000);
24:
System.out.println("Setting up task!");
25:
//Fuege die Fortschrittsanzeige als anonymen Listener unserem Task hinzu
26:
task.addPropertyChangeListener(
27:
new PropertyChangeListener() {
28:
public void propertyChange(PropertyChangeEvent evt) {
29:
//Wenn die Aenderung der Eigenschaft progress entspricht,
30:
//setze die Anzeige auf den mitgelieferten Wert
31:
if ("progress".equals(evt.getPropertyName())) {
32:
progressBar.setValue((Integer)evt.getNewValue());
33:
}
34:
}
35:
});
36:
37:
task.execute();
38:
39:
try {
40:
//task.get() blockiert, bis der Task fertig gerechnet hat
41:
final ArrayList<Double> v = task.get();
42:
//Ausgabe der Ergebnisse, wird ausgelassen
43:
//for(int i=0;i<v.size();i++) {
44:
// System.out.println("Zahl "+i+" = "+v.get(i));
45:
//}
46:
} catch (InterruptedException e) {
47:
e.printStackTrace();
48:
} catch (ExecutionException e) {
49:
e.printStackTrace();
50:
}
51:
}
52: }
A BBILDUNG 12.7: Eine Klasse, um den SwingWorker auszuführen
167
12 Parallele Ausführung - Nebenläufigkeit - Threads
12.4 Weiterführende Konzepte
Im Paket java.util.concurrent findet man noch viele weitere Klassen, um den
Umgang mit Threads und parallelen Programmteilen zu erleichtern.5
12.5 Java Beans
Java Beans realisieren plattformunabhängige, austauschbare und in sich abgeschlossene Komponenten in der Programmiersprache Java. Dieses modulare Konzept ermöglicht ein komponentenorientiertes Programmieren, wobei Abhängigkeiten zwischen den Komponenten minimiert werden. Dadurch sind einzelne Teile eines grös̈eren Projekts, zum Beispiel bei veränderten Anforderungen, leichter
austausch- und wartbar.6
12.5.1 Konventionen
Beans können durch zwei verschiedene Methoden zugänglich gemacht werden.
Entweder werden die Eigenschaften (Properties), öffentlichen (public) Methoden und Events per Reflection zur Laufzeit ausgelesen, wenn deren Benennung
bestimmten Konventionen folgt (siehe Beispiel 12.8), oder der Entwickler liefert
eine Klasse mit, die das Interface BeanInfo implementiert.
Es gilt weiter, dass eine Bean einen öffentlichen Konstruktor ohne Parameter anbieten muss, so dass Sie jederzeit instantiiert werden kann, ohne die Parameter
eines speziellen Konstruktors zu kennen. In Beispiel 12.8 wird dies durch
00: public SimpleBean(){
01:
setText("Hello world!");
02:
setOpaque(true);
03:
setBackground(Color.RED);
04:
setForeground(Color.YELLOW);
05:
setVerticalAlignment(CENTER);
06:
setHorizontalAlignment(CENTER);
07: }
abgedeckt.
Die am weitesten verbreiteten Beans sind die AWT- und Swing-Klassen, es gibt
allerdings auch nicht sichtbare Beans, die aber den gleichen SoftwaredesignRichtlinien folgen.7 Die Eigenschaften von Beans sind über sogenannte set oder
get Methoden verfügbar. Dabei setzt man mit
5
http://java.sun.com/javase/6/docs/api/java/util/concurrent/package-summary.
html
6
http://java.sun.com/javase/6/docs/technotes/guides/beans/index.html
7
http://java.sun.com/docs/books/tutorial/javabeans/index.html
168
12.5 Java Beans
setNameDerEigenschaft(ObjektVomTypDerEigenschaft argument)
zum Beispiel mit setText( "Hello world!" ), in Listing 12.8 die Eigenschaft
text. Die entsprechende Methode in der Oberklasse von SimpleBean lautet
public void setText(String text);
in der Klasse JLabel, die mit
public class SimpleBean extends JLabel
erweitert wird.
Es wird zwischen verschiedenen Typen von Eigenschaften unterschieden:
• Einfache Eigenschaften (simple properties), wie zum Beispiel
String name = "Max Mustermann";
• Eigenschaften, die aus mehreren Werten in einem Datenbereich (Array) bestehen (indexed properties), wie zum Beispiel
String[] vornamen = new String[]{"Heike","Max","Moritz"};
• Gebundene Eigenschaften (bound properties), zum Beispiel wurde diese
Komponente mit der Maus angeklickt? => Informiere für dieses Ereignis
registrierte Listener
• Abhängige Eigenschaften (constrained properties), zum Beispiel ist die Änderung der Eigenschaft gültig? Listener kann Veto einlegen, um dies zu
verhindern.
Die Eigenschaften können lesbar und/oder schreibbar sein.
12.5.2 Speichern und Laden
Die sogenannte Persistenz8 wird im Beispiel durch die Implementierung der Schnittstelle (Interface) java.io.Serializable erreicht. Persistenz bedeutet, dass der
Zustand eines Objekts, in diesem Fall einer Java Bean, dauerhaft gespeichert (serialisiert) werden kann. Das Objekt wird dazu mit allen Eigenschaften seriell in
eine von Sun festgelegte Binärdarstellung geschrieben.9 Diese kann zu einem
späteren Zeitpunkt wieder deserialisiert werden, um den ursprünglichen Zustand
des Objekts wiederherzustellen. Der Prozess der Serialisierung und Deserialsierung wird dabei vollständig von Java übernommen, das Interface Serializable
ist lediglich ein sogenanntes Marker-Interface ohne definierte Methoden.
Eine weitere Möglichkeit, die die zugrundeliegende Serialisierungs-API von Java
nutzt, ist die Implementierung der folgenden beiden Methoden:
8
9
Im Deutschen am ehesten als Dauerhaftigkeit zu übersetzen
http://java.sun.com/docs/books/tutorial/javabeans/persistence/index.html
169
12 Parallele Ausführung - Nebenläufigkeit - Threads
00:
01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
import
import
import
import
java.awt.Color;
java.beans.XMLDecoder;
javax.swing.JLabel;
java.io.Serializable;
public class SimpleBean extends JLabel
implements Serializable {
public SimpleBean() {
setText( "Hello world!" );
setOpaque( true );
setBackground( Color.RED );
setForeground( Color.YELLOW );
setVerticalAlignment( CENTER );
setHorizontalAlignment( CENTER );
}
}
A BBILDUNG 12.8: Eine einfache Java Bean aus dem Java Beans Tutorial der Firma
SUN
private void writeObject(java.io.ObjectOutputStream out)
throws IOException;
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
Hiermit sollte man arbeiten, wenn man zum Beispiel bestimmte zusätzliche Informationen über assoziierte Objekte mitspeichern will.
Wird mehr Kontrolle über das Speicherformat benötigt, so bietet Java das Interface java.io.Externalizable an, um in ein eigenes Format schreiben und von
diesem zurück lesend, ein Objekt im selben Zustand erzeugen zu können.
Die Klassen java.beans.XMLEncoder und java.beans.XMLDecoder erlauben es,
Objekte über die Serialisierungs-API in XML anstatt in einem Binärformat zu speichern und zu einem späteren Zeitpunkt wieder einzulesen.
Beispiel 12.10 zeigt, wie serialisierbare Objekte geschrieben und gelesen werden
können.
170
12.5 Java Beans
00:
01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
import java.awt.Color;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.Serializable;
public class DemoSimpleBean implements Serializable {
private String text = null;
private Color foreground = null;
private final PropertyChangeSupport pcs =
new PropertyChangeSupport(this);
//Initialisiere die Variablen im Konstruktor
public DemoSimpleBean() {
setText("Initialized!");
setForeground(Color.BLUE);
}
//Auslesen des Textes
public String getText() {
return text;
}
//Setzen des Textes
public void setText(String text) {
//Ablage des alten Textes in lokaler Variable
String old = this.text;
//Setzen des neuen Textes
this.text = text;
//Benachrichtigung der registrierten Listener
this.pcs.firePropertyChange("text", old, this.text);
}
//Auslesen der Vordergrundfarbe
public Color getForeground() {
return foreground;
}
//Setzen der Vordergrundfarbe
public void setForeground(Color c) {
Color old = foreground;
foreground = c;
this.pcs.firePropertyChange("foreground", old, foreground);
}
//Hinzufuegen von Listenern fuer Aenderung der Eigenschaften
public void addPropertyChangeListener(PropertyChangeListener listener)
{
this.pcs.addPropertyChangeListener(listener);
}
//Entfernen von Listenern fuer Aenderung der Eigenschaften
public void removePropertyChangeListener(PropertyChangeListener listener)
{
this.pcs.removePropertyChangeListener(listener);
}
}
A BBILDUNG 12.9: Eine eigene Java Bean mit den gebundenen Eigenschaften text
und foreground
171
12 Parallele Ausführung - Nebenläufigkeit - Threads
00:
01:
02:
03:
04:
05:
06:
07:
08:
09:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
//importiert alle Klassen im Paket java.io
import java.io.*;
public class ObjectOutputAndInputDemo{
public static void main(String[] args){
//Oeffnen eines Streams zur Datei "t.tmp", diese wird,
//falls Sie nicht existiert, neu angelegt
FileOutputStream fos = new FileOutputStream("t.tmp");
//Ein ObjectOutputStream kapselt den FileOutputStream
//ObjectOutputStream liest den Typ eines Objekts und
//schreibt ihn gemeinsam mit den Objekteigenschaften
//auf den FileOutputStream
ObjectOutputStream oos = new ObjectOutputStream(fos);
//Schreiben eines Integer-Objekts
oos.writeInt(12345);
//Schreiben eines String-Objekts
oos.writeObject("Today");
//Schreiben eines Date-Objekts
oos.writeObject(new Date());
//Schliessen der Datei
oos.close();
}
}
A BBILDUNG 12.10: Ein einfaches Beispiel für die Serialisierung
172
Literaturverzeichnis
[1] K. Arnold, J. Gosling: JavaT M - Die Programmiersprache. Addison-Wesley,
1996.
[2] T.H. Cormen, C.E. Leierson, R.L. Rivest: Introduction to Algorithms. MIT
Press, 1990.
[3] T.H. Cormen, C.E. Leierson, R.L. Rivest, C. Stein: Introduction to Algorithms. MIT Press, 2003.
[4] R. Sedgewick: Algorithms in Java, Part 5: Graph Algorithms. AddisonWesley, 2004, 3rd edition.
[5] D. Flanagan: Java in a Nutshell. O’Reilly & Associates Inc., 1996.
[6] F. Jobst: Programmieren in Java. Hanser Verlag, 1996.
[7] H. Klaeren: Vom Problem zum Programm. 2.Auflage, B.G. Teubner Verlag,
1991.
[8] K. Echtle, M. Goedicke: Lehrbuch der Programmierung mit Java. dpunktVerlag, 2000.
[9] Christian Ullenboom: Java ist auch eine
galileocomputing.de/openbook/javainsel
Insel.
http://www.
[10] Sun Microsystems, Inc.: Java Tutorial - Swing Trail, http://java.sun.com/
docs/books/tutorial/ui/index.html
[11] S. Oaks, H. Wong: Java Threads. O’Reilly, 2004, 3rd Edition.
[12] J. Gosling, B. Joy, G. Steele, G. Bracha: The Java Language Specification.
Addison Wesley, 2005, 3rd Edition
173
Literaturverzeichnis
174
Index
abstract, 80
boolean, 33
byte, 33
case, 40
catch, 110
char, 33
class, 23
default, 40
double, 23, 33
do, 36
else, 37
extends, 25, 73
false, 30
finally, 110
final, 24
float, 33
for, 36
if, 30, 37
implements, 102
import, 26, 106
instanceof, 112
int, 29, 33
length, 30
long, 33
new, 23
package, 26, 105
private, 27, 69
protected, 27
public, 27
short, 33
static, 24, 25, 68
String, 33, 43
super, 76
switch, 40
this, 67
throws, 110
throw, 109
true, 30
try, 110
void, 24
while, 30, 36
abstrakte
Klassen, 79
Methoden, 79
abstrakte Datentypen, 61
Algorithmen
Boyer-Moore (BM), 48
Boyer-Moore-Horspool (BMH), 49
Knuth-Morris-Pratt (KMP), 51
Textsuchalgorithmen, 43
Array, 30
mehrdimensional, 39
Ausgabe, 139
Ausnahmen, 107
Backus-Naur-Form, 9
Bitoperatoren, 34
BNF, 9
Bool’sche Operatoren, 34
Boyer-Moore (BM), 48
Boyer-Moore-Horspool (BMH), 49
call by reference, 72
call by value, 71
casting, 74
concurrent programming, 161
connected components, 117
Datenabstraktion, 61
175
Index
Datenkapselung, 61
Datenströme, 143
Datentyp
abstrakt, 61
Stack, 62
Dekrement, 35
direkte Verkettung, 131
Dokumentierung, 70
Double Hashing, 133
EBNF, 9
Einfachvererbung, 65
Eingabe, 139
Exceptions, 107
Feld, 30
mehrdimensional, 39
File-Ströme, 141
Generics, 94
Graphen, 115, 116
Adjazenzliste, 118
Adjazenzmatrix, 118
gerichtete, 116
gewichtete, 116
Kante, 116
Knoten, 116
benachbarte, 117
Grad eines, 117
Pfad, 118
Teilgraph, 117
verbundene, 117
Zyklus, 118
Instanzen, 22
Interfaces, 101
Java Beans, 168
Konventionen, 168
Speichern und Laden, 169
Klassen, 22, 23, 64
abstrakt, 79
generische, 94
String, 43
Klassenvariablen, 24
Knuth-Morris-Pratt (KMP), 51
Kollisionen, 131
Kommentare, 70
Konstruktoren, 66
in Unterklassen, 77
Reihenfolgeabhängigkeit, 78
Ladefaktor, 134
lineare Verschiebung, 133
Listen, 87
Literale, 32
Mehrfachvererbung, 66
Methoden, 24, 64
abstrakt, 79
in Java, 71
klassenbezogene Methoden, 25
Signatur, 71
überschreiben, 65
Methodenaufruf, 24
Mini-Java, 29
Hüllenklassen, 93
Hashing, 127
Haskell, 92
Nebenläufigkeit, 161
Event Dispatch Thread, 165
Threadmodell, 161
ThreadPools, 164
Importieren, 106
Initialbelegungen, 33
Initialisierungsblöcke, 68
Inkrement, 35
Input/Output (IO), 139
InputStream, 140
Oberklasse, 65
Obertypen, 102
Objekte, 22, 23, 64
Objektreferenzen, 23
OOP, 61
Open Hashing, 132
176
Index
Operatoren
Bitoperatoren, 34
Bool’sche Operatoren, 34
Zuweisungsoperatoren, 35
Pakete, 26, 105
Zugriff, 26
Pattern Matching, 95
Postfixschreibweise, 35
Präfix, 45
Präfixschreibweise, 35
Programm
interpretieren, 21
übersetzen, 21
Programmierung
Imperativ, 29
imperativ, 22
objektorientiert, 61
quadratische Verschiebung, 133
Reihenfolgeabhängigkeit, 78
Schnittstellen, 101
Semantik, 1
Stack, 62
Ströme, 139
Datenströme, 143
gepuffert, 142
Streams, 139
Suffix, 45
Swing, 147
BorderLayout, 153
BoxLayout, 155
FlowLayout, 152
GridLayout, 154
HelloWorld, 148
JButton, 149
JTextField, 150
Listener als externe Klasse,
156
Listener als interne Klasse,
157
Container, 151
Ereignisse, 156
Java Applet vs. Java WebStart vs.
Java Application, 158
Komponenten, 148
LayoutManager, 151
Sicherheitskonzepte, 159
Syntax, 1
Syntaxdiagramme, 13
Textsuchalgorithmen, 43
Thread, 161
Tokenizer, 144
Typumwandlung, 74
Überladen
von Konstruktoren, 67
von Methoden, 71
Überschreiben von Methoden, 75
Unicode, 32
Unterklasse, 65
Beispiel: GraphicCircle, 73
Variablen
Klassenvariablen, 24
Verdecken von Datenfeldern, 75
Vererbung, 65
Beispiel: GraphicCircle, 73
Einfachvererbung, 65
Mehrfachvererbung, 66
Verschiebung
lineare, 133
quadratische, 133
Zeichenketten, 43
Zugriff
Pakete, 26
Zusammenhangskomponenten, 117
Zuweisungsoperatoren, 35
177
Herunterladen