Einführung in die Programmierung in Java

Werbung
Einführung in die Programmierung in Java
Stephan Euler
FH-Giessen–Friedberg
Fachbereich MND
Version 0.95 14. Januar 2009
Wintersemester 2008
ii
Dieses Skript wurde mit LATEX 2ε und TEX(Version 3.141592) erstellt. Eingesetzt
wurde die integrierte Benutzeroberfläche WinEdt 5.3 zusammen mit MiKTeX
Version 2.2.
Inhaltsverzeichnis
1 Allererstes Java-Programm
1.1 Klasse für Beispielprogramm
1.2 Übersetzen und Ausführen .
1.3 Hinweise zur Formatierung .
1.4 Erste Rechnung . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Ganzzahlige Datentypen und erste Programme
2.1 Einleitung . . . . . . . . . . . . . . . . . . . . .
2.2 Ganzzahlige Datentypen . . . . . . . . . . . . .
2.3 Variablen . . . . . . . . . . . . . . . . . . . . .
2.4 Ausdrücke und Wertzuweisungen . . . . . . . .
2.5 Rechnen mit Integerwerten . . . . . . . . . . . .
2.5.1 Bit-Operatoren . . . . . . . . . . . . . .
2.5.2 Inkrement und Dekrement Operator . .
2.5.3 Vereinfachte Zuweisung . . . . . . . . . .
2.5.4 Integer-Quiz . . . . . . . . . . . . . . . .
2.6 Übungen . . . . . . . . . . . . . . . . . . . . . .
3 Abläufe
3.1 Logische Ausdrücke . . . . . . .
3.2 Rangfolge der Operatoren . . .
3.3 if Abfrage . . . . . . . . . . . .
3.4 Else-If . . . . . . . . . . . . . .
3.5 Fragezeichen-Operator . . . . .
3.6 Die switch-Anweisung . . . . .
3.7 Schleifen . . . . . . . . . . . . .
3.7.1 Vorzeitiges Verlassen von
3.7.2 Sprünge . . . . . . . . .
3.8 Beispiele . . . . . . . . . . . . .
3.9 Übungen . . . . . . . . . . . . .
iii
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
Schleifen
. . . . . .
. . . . . .
. . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
1
3
4
5
.
.
.
.
.
.
.
.
.
.
7
7
7
8
10
11
12
13
14
14
14
.
.
.
.
.
.
.
.
.
.
.
17
17
19
20
21
21
22
23
25
26
27
28
iv
INHALTSVERZEICHNIS
4 Verwendung von Gleitkomma-Zahlen
4.1 Gleitkomma-Zahlen . . . . . . . . . . . . .
4.1.1 Gleitkomma- Darstellung . . . . . .
4.1.2 Verwendung von Gleitkommazahlen
4.1.3 Vergleich der Zahlenformate . . . .
4.2 Gleitkommazahlen in Java . . . . . . . . .
4.2.1 Mathematisch Funktionen . . . . .
4.3 Umwandlung zwischen Datentypen . . . .
4.4 Übungen . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
31
31
31
36
37
37
39
40
42
5 Felder
5.1 Zugriff auf Elemente . . . . . . . . . . . . . . . . . . . . . . . . .
5.2 Mehrdimensionale Felder . . . . . . . . . . . . . . . . . . . . . .
5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
45
47
49
50
6 Methoden
6.1 Einleitung . . . . . . . .
6.2 Definition . . . . . . . .
6.3 Überladen von Methoden
6.4 Übergabe von Feldern .
6.5 Rekursion . . . . . . . .
6.6 Anmerkungen . . . . . .
6.7 Übungen . . . . . . . . .
.
.
.
.
.
.
.
53
53
54
56
57
58
59
59
.
.
.
.
.
61
61
65
66
69
69
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7 Algorithmen – vom Problem zum Programm
7.1 Algorithmen . . . . . . . . . . . . . . . . . . .
7.2 Flussdiagramm . . . . . . . . . . . . . . . . .
7.3 Struktogramme . . . . . . . . . . . . . . . . .
7.4 Aktivitätsdiagramm . . . . . . . . . . . . . . .
7.5 Übungen . . . . . . . . . . . . . . . . . . . . .
8 Objektorientierte Programmierung
8.1 Einleitung . . . . . . . . . . . . . . . . . .
8.2 Objektorientierte Programmierung (OOP)
8.3 Objektorientierte Programmiersprachen . .
8.4 Übungen . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
73
73
74
76
77
9 Klassen
9.1 Einleitung . . . . . . . . . . . . . . . . . . . .
9.2 Klassendefinition . . . . . . . . . . . . . . . .
9.2.1 Zugriff auf Attribute . . . . . . . . . .
9.3 Instanz- und Klassenvariablen und Methoden
9.4 Konstruktoren . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
79
79
79
81
82
84
.
.
.
.
INHALTSVERZEICHNIS
9.5
9.6
9.7
v
Destruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Namenskonventionen . . . . . . . . . . . . . . . . . . . . . . . . .
Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
10 Objekte und Klassen
10.1 Einleitung . . . . . . . . . . . . . . . . . .
10.2 Basisklassen . . . . . . . . . . . . . . . . .
10.3 Klasse Fahrzeug . . . . . . . . . . . . . . .
10.4 static Elemente . . . . . . . . . . . . . . .
10.5 Konstruktoren und die Klasse Auto . . . .
10.5.1 Die Klasse Auto . . . . . . . . . . .
10.5.2 Verkettung von Konstruktoren . . .
10.6 Die Klasse Verleih . . . . . . . . . . . . . .
10.7 Überlagerung von Methoden . . . . . . . .
10.7.1 Dynamische Suche . . . . . . . . .
10.8 Attribute . . . . . . . . . . . . . . . . . .
10.9 Mehrfachvererbung und Interfaces . . . . .
10.9.1 Einleitung . . . . . . . . . . . . . .
10.10Beispiel FH-Verwaltung . . . . . . . . . .
10.11Beispiel Sortieren . . . . . . . . . . . . . .
10.11.1 Interface in eigener Klasse . . . . .
10.11.2 Interface integriert in andere Klasse
10.12Anonyme Klassen . . . . . . . . . . . . . .
10.13Lokale Klassen . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
11 Zeichenketten
11.1 Einleitung . . . . . . . . . . . . . . . . . . . .
11.2 Datentyp char . . . . . . . . . . . . . . . . . .
11.3 Konstruktoren für String . . . . . . . . . . . .
11.4 Länge von Zeichenketten und einzelne Zeichen
11.5 Arbeiten mit Zeichenketten . . . . . . . . . .
11.6 Teilketten . . . . . . . . . . . . . . . . . . . .
11.7 Vergleichen und Suchen . . . . . . . . . . . . .
11.7.1 Vergleichen . . . . . . . . . . . . . . .
11.8 Verändern von Zeichenketten . . . . . . . . . .
11.8.1 Suchen . . . . . . . . . . . . . . . . . .
11.9 Tokenizer . . . . . . . . . . . . . . . . . . . .
11.10Konvertierungen . . . . . . . . . . . . . . . . .
11.11Die Klasse String ist endgültig . . . . . . . . .
11.12Übungen . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
85
86
86
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
89
89
89
91
92
94
94
96
97
98
99
100
102
102
103
105
106
107
108
109
.
.
.
.
.
.
.
.
.
.
.
.
.
.
111
111
111
112
113
113
115
115
115
117
118
119
120
120
121
vi
INHALTSVERZEICHNIS
12 Reguläre Ausdrücke
123
12.0.1 Gefundene Teile . . . . . . . . . . . . . . . . . . . . . . . . 126
12.1 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
13 Tools
13.1 Einleitung . . . . . . .
13.2 jar . . . . . . . . . . .
13.3 java und Klassennamen
13.4 Klassennamen . . . . .
13.5 javadoc . . . . . . . . .
13.6 jdb . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
14 Ein- und Ausgabe und Dateien
14.1 Einleitung . . . . . . . . . . . . . . . .
14.2 Standardeingabe und Standardausgabe
14.2.1 Ausgabe . . . . . . . . . . . . .
14.2.2 Formatierte Ausgabe mit printf
14.2.3 Eingabe . . . . . . . . . . . . .
14.2.4 Einlesen mit Scanner . . . . . .
14.3 Streams, Reader und Writer . . . . . .
14.4 Reader . . . . . . . . . . . . . . . . . .
14.4.1 Schachtelung von Readern . . .
14.4.2 Übersicht Reader . . . . . . . .
14.5 Writer . . . . . . . . . . . . . . . . . .
14.6 PrintWriter . . . . . . . . . . . . . . .
14.7 Streams . . . . . . . . . . . . . . . . .
14.8 Random Access File . . . . . . . . . .
14.9 Die Klasse File . . . . . . . . . . . . .
14.10Übungen . . . . . . . . . . . . . . . . .
15 Exception
15.1 Einleitung . . . . . . . . . . . . . . .
15.2 Beispiel . . . . . . . . . . . . . . . .
15.3 try - catch Anweisung . . . . . . . .
15.4 Hierarchie von Ausnahmefehlern . . .
15.4.1 Die Klasse Error . . . . . . .
15.4.2 Die Klasse Exception . . . . .
15.4.3 Die Klasse RuntimeException
15.5 Eigene Exceptions . . . . . . . . . . .
15.6 finally-Block . . . . . . . . . . . . . .
15.7 Übungen . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
129
129
129
131
132
133
134
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
137
137
137
138
139
139
141
141
142
143
144
145
147
148
148
149
151
.
.
.
.
.
.
.
.
.
.
153
153
154
155
156
157
157
158
159
159
160
INHALTSVERZEICHNIS
16 Dynamische Datenstrukturen
16.1 Vector . . . . . . . . . . . . . . . . .
16.1.1 Konstruktor und Einfügen von
16.1.2 Zugriff auf Elemente . . . . .
16.1.3 Iterator . . . . . . . . . . . .
16.1.4 Wrapper Klassen . . . . . . .
16.1.5 Stack . . . . . . . . . . . . . .
16.2 Assoziativspeicher . . . . . . . . . . .
16.2.1 Hashtable . . . . . . . . . . .
16.2.2 Properties . . . . . . . . . . .
16.2.3 Bäume . . . . . . . . . . . . .
16.3 Methoden in der Klasse Collections .
16.3.1 Beispiel Kartenspiel . . . . . .
vii
. . . . . . .
Elementen
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
163
163
163
164
165
166
167
168
168
171
172
173
173
17 Erweiterungen WS08
177
17.1 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
17.2 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
17.3 close . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Literaturverzeichnis
185
viii
INHALTSVERZEICHNIS
Kapitel 1
Allererstes Java-Programm
1.1
Klasse für Beispielprogramm
Java ist eine moderne und leistungsfähige Programmiersprache. Die Sprache wurde etwa ab 1991 bei der Firma Sun entwickelt. Java übernimmt wesentliche Elemente aus C und C++, verzichtet aber auf einige der komplizierten, fehlerträchtigen Möglichkeiten. Die Sprache ist stärker in Richtung Klarheit und Einfachheit
ausgerichtet. Zur Unterstützung des Benutzers enthält Java ein automatisches
Speichermanagement sowie eine umfangreiche Bibliothek u.a. mit Graphikmöglichkeiten. In der Form von Applets können Java Programme in Web-Seiten eingebunden werden.
Java ist eine objektorientierte Sprache und basiert auf dem Konzept von Klassen. Was das bedeutet werden wir im Laufe der Vorlesung sehen. Zunächst geht
es darum, die Grundelemente der Sprache zu erlernen. Wir werden dazu zunächst
einfache Programme untersuchen, bei denen die Klassen noch keine Rolle spielen.
Die Grundelemente sind bis auf wenige Ausnahmen sehr ähnlich zu C und C++.
Daher können die Programme leicht in diese oder weitere, verwandte Sprachen
übertragen werden.
Um Programme ausführen zu können, benötigen wir allerdings einen entsprechenden Rahmen mit einer Klasse. Das bedeutet, wir müssen in jedem Fall eine
Klasse anlegen, in die wir das Programm einbetten – selbst wenn in der Anwendung selbst keinerlei objektorientierte Techniken Verwendung finden. Betrachten
wir einen konkreten Fall. Dazu generieren wir mit aus einer Entwicklungsumgebung heraus eine Klasse Ue1. Die Entwicklungsumgebung – in diesem Fall BlueJ
– erspart uns einige Tipparbeit und generiert automatisch ein Standardmuster
für Klassen. Man kann aber genau so gut mit irgend einem Editor arbeiten und
den Text selbst eingeben. Wichtig ist dabei, dass die Datei den Namen der Klasse
mit der Endung .java bekommt. Das Muster hat folgende Form:
/**
* Beschreiben Sie hier die Klasse Ue1.
1
2
KAPITEL 1. ALLERERSTES JAVA-PROGRAMM
*
* @author (Ihr Name)
* @version (eine Versionsnummer oder ein Datum)
*/
public class Ue1
{
// Definieren Sie ab hier die Klassenvariablen für Ue1
// Definieren Sie ab hier die Objektvariablen für Ue1
// Definieren Sie ab hier die KOnstruktoren für Ue1
/**
* Konstruktor für Objekte der Klasse Ue1
*/
public Ue1()
{
// Objektvariable initialisieren
}
// Definieren Sie ab hier die Methoden für Ue1
/**
* Diese Methode leistet....
*
* @param
Parameter...
* @return Rückgabewert...
*/
void sageHallo() {
System.out.println( "Hallo" );
}
}
Im Kopf steht zunächst ein Kommentar. Kommentare beginnen mit einem /* und
enden mit einen */. Sie können mehrere Zeilen umfassen. Kommentare können
nicht geschachtelt werden. Kommentare haben keinerlei Einfluss auf die Ausführung des Programms. Sie dienen lediglich der Dokumentation. Der erste Kommentar in dem Beispiel enthält den Namen der Klasse sowie das Datum der Erstellung.
Klassennamen beginnen üblicherweise mit einem Großbuchstaben. Allerdings ist
dies lediglich eine allgemein anerkannte Konvention, keine Pflicht.
Der zweite Kommentar hat eine spezielle Form beginnend mit /** und dient
als Eingabe für eine automatischen Erstellung der Dokumentation. Wir werden
diesen Kommentar fürs erste ignorieren. Mit der Zeile
public class Ue1 {
1.2. ÜBERSETZEN UND AUSFÜHREN
3
wird eine Klasse Ue1 angelegt. Der zu dieser Klasse gehörende Code steht in einem
Block begrenzt durch die geschweiften Klammern. Die Entwicklungsumgebung
hat in der Klasse Platz für Konstruktoren und Methoden angelegt. Konstruktoren
dienen zur Erzeugung von Objekten. Sie tragen den gleichen Namen wie die
Klasse. Die für uns relevante Stelle ist die Methodendefinition
void sageHallo() {
System.out.println( "Hallo" );
}
Damit wird eine Methode mit dem Namen sageHallo definiert. Methoden sind
in sich abgeschlossene Blöcke, die als Einheit ausgeführt werden. Beim Aufruf
kann man gegebenenfalls Parameter übergeben und am Ende (bei der Rückkehr)
können Methoden ein Resultat – den Rückgabewert – liefern. In anderen Programmiersprachen spricht man von Funktionen, Prozeduren oder Modulen und
die Parameter werden oft als Argumente bezeichnet. Zur Ausgabe steht in Java
die Methode System.out.println zur Verfügung. Ein Text kann direkt in der
Form
System.out.println( "Hallo" );
ausgegeben werden. Der Text wird durch zwei Anführungszeichen begrenzt und
wird als Parameter an die Methode übergeben. Das Ende einer Anweisung wird
durch das Semikolon markiert. Die Methode übernimmt dann die Aufgabe, diesen
Text am Bildschirm anzuzeigen. Es ist wie wir noch sehen werden eine recht
universelle Methode, die alle Arten von Parameter in jeweils sinnvoller Weise
ausgeben kann. Die Endung ln steht für einen Zeilenumbruch, d. h. nach der
Ausgabe springt der Cursor in eine neue Zeile. Ist dies nicht gewünscht, so kann
man auf eine Variante System.out.print zurückgreifen.
1.2
Übersetzen und Ausführen
Die Klasse ist jetzt fertig und soll ausprobiert werden. Verwendet man eine Entwicklungsumgebung, so genügt in der Regel das Klicken auf einen entsprechenden
Knopf.
Mehr Einblick in den Vorgang erhält man, wenn man die notwendigen Schritte
einzeln ausführt. Dazu benötigt man ein entsprechendes Fenster (Eingabeaufforderung unter WinXX, shell unter Linux). Am einfachsten geht man dann zu dem
Verzeichnis, in dem die Java-Datei steht. Dort führt man den Befehl
javac Ue1.java
aus. Damit wird der Java-Compiler javac gestartet. Der Compiler überprüft
das Programm auf formale Korrektheit. Findet er einen oder mehrere Fehler, so
werden diese mit einer mehr oder weniger hilfreichen Meldung angezeigt. Vergisst
man beispielsweise das abschließende Semikolon, so erhält man die Fehlermeldung
4
KAPITEL 1. ALLERERSTES JAVA-PROGRAMM
Ue1.java:23: ’;’ expected
System.out.println( "Hallo" )
^
1 error
inklusive Angabe der Stelle (Zeile 23), an der der Fehler gefunden wurde. Ist das
Programm fehlerfrei, so erzeugt der Compiler eine Datei mit dem gleichen Namen
und der Endung .class in dem selben Verzeichnis. Mit dieser Datei wird durch
den Aufruf
java Ue1
schließlich das Programm ausgeführt. (Wichtig: die Endung .class darf bei diesem Aufruf nicht angegeben werden.) An dieser Stelle unterscheidet sich Java
von vielen anderen Programmiersprachen. Es wird keine für sich alleine lauffähige
Anwendung erzeugt, sondern der Code wird durch die Java-Maschine ausgeführt.
Zur Ausführung wird der Code in den im Allgemeinen mehreren .class–Dateien
interpretiert und in ausführbare Befehle umgesetzt. Dieser Ansatz hat mehrere
Vorteile:
• Unabhängigkeit vom Betriebssystem
• Kleine Dateien, d. h. schnelle Übertragung, geringer Platzbedarf
• Höhere Flexibilität
Dem stehen zwei Nachteile gegenüber:
• Zusätzlicher Aufwand für die Interpretation bedeutet langsamere Ausführung
• Auf Zielsystem muss eine Java-Maschine installiert sein (heute in aller Regel
gegeben)
1.3
Hinweise zur Formatierung
Java erlaubt eine freie Gestaltung des Programmcodes. So akzeptiert der Compiler durchaus auch folgende, wenig empfehlenswerte Form eines Beispielprogramms
public class Ue1{public static
void main(String args[]){System.out.println(
"Hallo");}}
Gerade wegen dieser großen Freiheit ist eine Strukturierung im Sinne einer besseren Lesbarkeit und Wartbarkeit essentiell. Einige Grundsätze:
• Beginn eines Programms in der ersten Spalte
1.4. ERSTE RECHNUNG
5
• In der Regel nur eine Anweisung pro Zeile
• Blöcke einrücken
• Leerzeilen und Leerzeichen zur Gliederung einsetzen
• Auch mal einen Kommentar einfügen (aber mit nützlicher Information,
nicht Selbstverständlichkeiten)
1.4
Erste Rechnung
Als erstes Beispiel für eine Berechnung betrachten wir folgende Methode etwas
genauer:
void rechneStunden() {
int stunden = 6;
int wochen = 14;
int gesamt = stunden * wochen;
System.out.println( "Gesamtstunden: " + gesamt );
}
Als Name wurde rechneStunden gewählt. Die Methode braucht nichts zurückzugeben. Dies wird durch das Attribut void (engl. für leer, nichtig) angezeigt.
In den runden Klammern nach dem Methodenname können Parameter stehen.
Bis auf weiteres werden wir dies nicht benötigen. Aber die Klammern müssen
trotzdem an dieser Stelle bleiben, damit dies als gültiger Methodenname erkannt
wird. Schließlich folgt der Inhalt der Methode in dem durch geschweifte Klammern markierten Block. Der Block beginnt mit den Variablenvereinbarungen.
Jede Variable muss vor ihrer Verwendung in dieser Form angelegt werden. Die
Variablenvereinbarungen müssen allerdings nicht am Anfang des Blocks stehen.
Eine Variablenvereinbarung besteht aus
• Typbezeichnung
• Name
• optionaler Anfangswert (nach einem = Zeichen)
Anweisungen werden in Java durch ein Semikolon beendet. In dem Beispiel handelt es sich um den Typ int für Integer. Darunter versteht man ganze Zahlen.
Die so deklarierten Variablen können nur ganze Zahlen (2, 22, -3376, etc.) enthalten. Die ersten beiden Variablen werden mit Anfangswerten belegt. Dazu folgt
nach dem Namen ein =-Zeichen und dahinter der Wert. Der Inhalt der Variable
gesamt wird auf das Ergebnis der Multiplikation gesetzt. Die beiden beteiligten
Variablen werden nicht verändert. Schließlich wird das Ergebnis mit dem Aufruf
println ausgegeben.
6
KAPITEL 1. ALLERERSTES JAVA-PROGRAMM
Kapitel 2
Ganzzahlige Datentypen und erste
Programme
2.1
Einleitung
Eine Speicherzelle in einem Computer enthält nur ein Muster mit Werten von
0 und 1. Erst durch die Festlegung, wie dieses Bitmuster zu interpretieren ist,
wird der tatsächliche Wert festgelegt. Man spricht dann von einem Datentyp.
Der Datentyp legt darüber hinaus fest, welche Operationen mit Elementen dieses
Typs erlaubt sind. Einige Datentypen sind ganze Zahlen, Zahlen mit Dezimalpunkt an fester Stelle, logische Ausdrücke, Zeichen, Gleitkommazahlen oder etwa
komplexe Zahlen. Verschiedene Programmiersprachen unterstützen in der Regel
unterschiedliche Typen. Dabei kann es sein, dass eine Sprache einen Typ gar nicht
enthält (z. B. gibt es in Java keine komplexen Zahlen) oder dass sich die Datentypen in Realisierungsdetails unterscheiden. Im folgenden wird zunächst für ganze
Zahlen die Realisierung in Java beschrieben.
2.2
Ganzzahlige Datentypen
Für die Darstellung von (positiven und negativen) ganzen Zahlen verwendet man
den Datentyp Integer (engl. any positive or negative whole number or zero, Webster´s Dictionary). Der Grundtyp int ist 4 Byte groß und enthält Zahlen in
2er-Komplement Darstellung. Der Wertebereich ist dann von -2147483648 bis
+2147483647
Konstanten für int können sowohl als Dezimalwerte als auch in den beiden
anderen Zahlensystemen angegeben werden. Für Oktalzahlen fügt man eine führende 0 ein (Beispiel 0156), bei Hexadezimalzahlen schreibt man 0x. . . (Beispiel
0xFF). Das Vorzeichen wird durch - oder ein optionales + gekennzeichnet. Neben
dem Basistyp int gibt es einige weitere Typen:
7
8KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME
Typ
byte
short
int
long
Speicherbedarf
1 Byte
2 Byte
4 Byte
8 Byte
Wertebereich
-128 . . . 127
-32768 . . . +32767
-2147483648 . . . +2147483647
-9223372036854775808 ... 9223372036854775807
Es stellt sich die Frage, was passiert bei Bereichsüberschreitungen? Betrachten
wir ein Beispiel:
int zahl = 2147483647;
System.out.println( "i: " + zahl );
++zahl;
// Erhoehe zahl um 1 (Kurzschreibweise)
System.out.println( "i: " + zahl );
Die Ausgabe lautet:
i: 2147483647
i+1: -2147483648
Indem wir eine 1 zu der größten positiven Zahl addiert haben, sind wir zu der
kleinsten negativen Zahl gelangt. Wie in Bild 2.1 dargestellt, kann man sich die
Zahlen im Kreis angeordnet vorstellen.
-1 0 1
+n
?
-2147483648
2147483647
Abbildung 2.1: Anordnung von Integerzahlen
2.3
Variablen
In der Regel will man mehrfach auf eine Speicherzelle zugreifen und beispielsweise
• einen Wert zuweisen
2.3. VARIABLEN
9
• den Inhalt verändern
• den Inhalt abfragen und ausgeben
Dazu gibt man der Speicherzelle einen Namen (Bezeichner), unter dem man immer wieder auf sie zugreifen kann und spricht dann von einer Variablen. Dabei
gilt
• eine Variable hat einen bestimmten Typ
• eine Variable muss vor der ersten Benutzung festgelegt (vereinbart) werden
• eine Variable kann den Typ nicht ändern
Insgesamt kann man sagen, der Name gibt an, wo die Werte gespeichert sind und
der Datentyp legt die Größe des Speicherbereichs sowie die Interpretation der
Bitwerte fest. In Java wird eine Variable vereinbart (definiert), indem man den
Typ und den Variablennamen zusammen angibt. Die Anweisung wird durch ein
Semikolon ; abgeschlossen. Beispiel:
int i;
long anzahlDerStudenten;
Mehrere Variablen gleichen Typs können gemeinsam definiert werden. Beispiel:
int anzahlHoerer, anzahlHoererinnen;
Der Name kann nach folgenden Regeln gebildet werden:
• Der Name beginnt mit einem Buchstaben.
• Anschließend folgt eine beliebige Folge von alphanumerischen Zeichen.
• Der Unterstrich (underscore) kann wie ein Buchstabe eingesetzt werden.
• Groß- und Kleinschreibung wird unterschieden.
• Java Konvention: Namen von Variablen beginnen mit einem Kleinbuchstaben.
Neben den festen Vorgaben sollte man folgende allgemeine Hinweise beachten:
• Namen sollten passend und weitgehend selbsterklärend (aussagekräftig)
sein (nicht int eineintvariable;).
• Namen von Integer Variablen fangen oft mit Buchstaben von i bis n an.
• Für kurzlebige Variablen können kurze Bezeichnungen wie i, j, k verwendet
werden.
10KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME
• Konsequent sein: wenn sich die Bedeutung einer Variable im Programm
ändert, auch ihren Namen anpassen.
• Die Struktur innerhalb eines Namens wird mit Groß- / Kleinschreibung
markiert:
– anzahlStudenten
– strahlungsDauer
• Einheitliche Sprache (Englisch oder Deutsch).
• Konsistenz! Ein einmal eingeführter Stil für Namen, Einrückungen, etc.
sollte beibehalten werden.
Bei großen Software-Projekten werden oft Namenskonventionen verbindlich vorgegeben.
2.4
Ausdrücke und Wertzuweisungen
Aus im allgemeinen mehreren Variablen und Konstanten kann man mit entsprechenden Operatoren Ausdrücke bilden. Zum Beispiel ergibt i + 10 + 123 die
Summe der drei Werte. Ausdrücke können Variablen zugewiesen werden. Die
Syntax in Java ist
Variable = Ausdruck
Das Zeichen = bezeichnet man als Zuweisungsoperator. Der Ausdruck auf der
rechten Seite wird berechnet und dann in die Variable auf der linken Seite gespeichert. Dies ist nicht zu verwechseln mit der Bedeutung als Gleichheitszeichen.
In der Sprache Pascal wird dies deutlicher durch das Zeichen := zum Ausdruck
gebracht.
Beispiel 2.1 Zuweisungen
int i;
i = 5;
i = 5 * 6;
Hier werden nacheinander die Werte 5 und 30 in die Variable i geschrieben. Mit
jeder Zuweisung wird der alte Wert überschrieben. Die Variable darf auch in dem
Ausdruck auf der rechten Seite vorkommen. Bei der Auswertung des Ausdrucks
werden die Variablen nicht verändert, sondern es wird nur mit den Werten gerechnet. Erst durch die Zuweisung wird ein neuer Wert in den Speicher geschrieben.
So gesehen wird eine Variable auf der rechten Seite genauso behandelt wie eine
2.5. RECHNEN MIT INTEGERWERTEN
11
Konstante. Der prinzipielle Unterschied zwischen Variablen und Konstanten liegt
in der Adressierbarkeit. Beide belegen Speicher und sind mit einem Datentyp verknüpft, aber bei Variablen kann man auch den Speicherplatz mit neuen Werten
füllen. In diesem Sinn hat die Variable m in
m = m + 67;
zwei verschiedene Bedeutungen. Auf der rechten Seite der Anweisung ist der
Inhalt der Speicherzelle gemeint. Dieser Wert wird zu 67 addiert. Das Ergebnis
wird dann in die mit m bezeichnete Speicherzelle geschrieben. Man spricht auch
von R-Wert und L-Wert (rvalue, lvalue) wenn man die Bedeutung auf der rechten
bzw. linken Seite meint. Mit den englischen Begriffen kann man sich unter den
Namen auch read value und location value vorstellen. Eine Konstante kann nur
als R-Wert benutzt werden. Eine Anweisung in der Art 7 = 3 + 4; ist weder
sinnvoll noch erlaubt.
In Java kann (und sollte) bereits bei der Definition einer Variablen ein Wert
zugewiesen werden ( int anzahlHoerer = 100;). Eine Variable ohne zugewiesenen Wert wird mit dem Wert 0 initialisiert.
2.5
Rechnen mit Integerwerten
Java unterstützt die Grundrechenarten mit den Operatoren +, -, * und /. Daneben gibt es noch den Modulo-Operator %, der den Rest bei der ganzzahligen
Division ergibt. Für die Operatoren gilt die übliche Hierarchie. Bei gleichberechtigten Operatoren wird der Ausdruck von links nach rechts abgearbeitet. Bei der
Division muss man berücksichtigen, dass das Ergebnis wieder ein Integer Wert
ist und damit ein eventueller Rest verloren geht. Dadurch spielt bei komplexeren Ausdrücken u. U. die Reihenfolge eine Rolle. Bei ungeschickter Reihenfolge
kann es passieren, dass Zwischenergebnisse nur mit eingeschränkter Genauigkeit
berechnet werden. In Folge des damit verbundene Fehlers führt die Auswertung
nicht zu dem gewünschten Ergebnis.
Beispiel 2.2 Auswertung von Integerausdrücken:
Ausdruck
32 / 5 * 5
32 * 5 / 5
21 % 6
28 % 7
21 / 6
23 / 6
Ergebnis
30
32
3 (21 - 3 * 6 )
0
3
3
Man kann die Reihenfolge durch Klammern () verändern. Bei der Auswertung
werden zunächst die Ausdrücke in Klammern berechnet. Klammern können geschachtelt werden. Die Berechnung beginnt dann bei der innersten Klammer.
12KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME
Dann wird die Klammer durch das Resultat ersetzt und die Berechnung fortgesetzt.
Übung 2.1 Welchen Wert ergeben die folgenden Ausdrücke?
2 * 5 + 6 * 2
2 * ( 5 + 6 * 2)
2 * ( ( 5 + 6 ) * 2)
2.5.1
Bit-Operatoren
Ein Bit-Operator betrachtet den Operanden als Folge von Bits. Diese Bitfolge
kann manipuliert werden, indem beispielsweise alle Werte um eine gegebene Anzahl von Positionen geschoben werden. Bei der Verknüpfung zweier Operanden
werden alle Bits Position für Position miteinander bearbeitet. Die Operanden
für Bit-Operatoren müssen ganzzahlig sein. Im Einzelnen bietet Java folgende
Bit-Operatoren:
Operator
~
&
|
^
<<
>>
>>>
Funktion
bitweises NICHT
bitweises UND
bitweises ODER
bitweises EXOR
schieben nach links (shift)
schieben nach rechts (shift)
rechts shift, 0 nachschieben
Anwendung
~ausdruck
ausdruck1 & ausdruck2
ausdruck1 | ausdruck2
ausdruck1 ^ ausdruck2
ausdruck1 << ausdruck2
ausdruck1 >> ausdruck2
ausdruck1 >>> ausdruck2
Das bitweise NICHT invertiert jedes Bit des Operanden. Die drei Operationen
UND, ODER und EXOR arbeiten entsprechend auf jedem einzelnen Bit. UND
wird oft benutzt um Bits auszuschneiden, während mit ODER Bits gezielt gesetzt
werden können. Beispiel:
i = i & 0xF;
i = i | 0xF0;
// löscht alle außer den letzten vier Bits
// setzt 4 Bits
Die Shift Operatoren verschieben den linken Ausdruck um die im rechten Ausdruck angegebene Anzahl von Stellen. Dabei wird beim Schieben nach links stets
mit 0 aufgefüllt. Schiebt man nach rechts, so wird bei der Variante >> bei negativen Werten das höchstwertige Bit auf 1 gesetzt. Bei der Variante >>> wird
demgegenüber das höchstwertige Bit immer auf 0 gesetzt.
Beispiel 2.3 Bit-Operatoren
2.5. RECHNEN MIT INTEGERWERTEN
int i=15;
//
13
Bitmuster 0000 1111
System.out.println("i: " + Integer.toBinaryString( i ));
System.out.println("i << 1: " + Integer.toBinaryString( i << 1 ));
System.out.println("i >> 2: " + Integer.toBinaryString( i >> 2 ));
System.out.println("i | 0x70: " + Integer.toBinaryString( i | 0x70 ));
System.out.println("i & 0xc: " + Integer.toBinaryString( i & 0xc ));
System.out.println("~i: " + Integer.toBinaryString( ~i ) );
ergibt:
i: 1111
i << 1: 11110
i >> 2: 11
i | 0x70: 1111111
i & 0xc: 1100
~i: 11111111111111111111111111110000
2.5.2
Inkrement und Dekrement Operator
In Java gibt es zwei spezielle Operatoren ++ und --, die eine Variable um 1 erhöhen oder erniedrigen. Diese Operatoren verändern den Operanden und erfordern
daher einen lvalue. Sie können beispielsweise nicht auf Konstanten angewandt
werden (++5 ist nicht erlaubt). Der Ausdruck mit dem Operator ist selbst wieder
ein rvalue ( ++i = ...; geht nicht). Ungewöhnlich ist, dass die Operatoren vor
(prefix) oder nach (postfix) dem Operanden stehen können. Im ersten Fall wird
die Variable verändert, bevor sie weiter verwendet wird, im zweiten Fall wird erst
der Wert benutzt, dann verändert.
Mit n = 7 wird durch i = ++n; die Variable i auf 8 gesetzt, bei i = n++;
auf 7. In beiden Fällen hat n anschließend den Wert 8. Man kann die Operatoren anwenden, ohne die Variable weiter zu benutzen, d. h. ++n; oder n--; sind
mögliche Anweisungen und gebräuchliche Abkürzungen für n = n + 1; bzw. n
+= 1; (siehe nächster Abschnitt) oder n = n - 1; In diesem Fall spielt die Unterscheidung zwischen präfix und postfix keine Rolle. Ansonsten muss man bei
komplizierteren Ausdrücken mit unbeabsichtigten Nebeneffekten rechnen. In jedem Fall leidet die Lesbarkeit des Programms. Gefährlich sind Querbezüge in der
Art
i = 4;
i = i++ * 5;
In solchen Fällen ist es besser, eine Zeile mehr zu schreiben und damit klar darzustellen, was gemeint ist. Selbst wenn das Programm tatsächlich das ausführt, was
die Programmiererin oder der Programmierer wollte, ist die kompakte Schreibweise schwerer zu verstehen. Es ist auch nicht zu erwarten, dass das entstehende
14KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME
Programm schneller läuft. In aller Regel wird der Compiler selbst durch Optimierung den effizientesten Code generieren.
2.5.3
Vereinfachte Zuweisung
Häufig hat man Zuweisungen in der Art
aktienImDepot = aktienImDepot + kauf;
d. h. die Variable auf der linken Seite wird auch als erster Operand auf der rechten
Seite benutzt:
expr1 = expr1 op expr2
Java bietet dafür bei den meisten Operatoren mit zwei Argumenten die kompakte
Schreibweise
expr1 op= expr2
(ohne Leerzeichen zwischen op und =) an. Die wahrscheinlich am häufigsten in der
Praxis auftretende Form ist die Verbindung mit der Addition oder Subtraktion:
i += 5; Hauptvorteil ist die bessere Übersichtlichkeit. Insbesondere wenn der
Ausdruck auf der rechten Seite komplex wird, ist in dieser Schreibweise sofort
die Bedeutung ersichtlich. Die Schreibweise entspricht der Intention: i soll um 5
erhöht werden.
2.5.4
Integer-Quiz
Die Variablen i und j seien vom Typ int. Welche Werte haben sie nach folgenden
Anweisungen :
i
i
i
i
i
i
i
2.6
=
=
=
=
=
=
=
32 / 6 + 7 % 2;
1 << 3;
0xf; i |= 0xf0;
4; j = i++ * 5;
1 & 2;
2; j = 3; i *= j + 1;
0777; i &= ~077;
Übungen
Übung 2.2 Welche Namen sind zulässige Bezeichner?
• beta
• ws01/02
2.6. ÜBUNGEN
15
• Gamma
• ___test___
• dritte_loesung
• ws2001_okt_08
• ss2001-07-08
• 3eck
• monDay
Legen Sie für die folgenden Übungen eine Übungsklasse (z. B. UebungenInteger)
an. Die Lösungen zu den einzelnen Übungen können Sie dann als Methoden in
dieser Klasse realisieren.
Übung 2.3 Die Methode Integer.toBinaryString( int i ) erzeugt eine Zeichenkette mit der Darstellung des Wertes von i als Binärzahl. Verwenden Sie
diese Methode um das Bitmuster folgender Ausdrücke auszugeben:
• 185
• 185 & 0xf0
• 185 | 07
Welche anderen Methode stellt die Klasse Integer zur Verfügung, um Werte in
verschiedenen Zahlensystemen darzustellen?
Übung 2.4 Lassen Sie von einer 4-stelligen Dezimalzahl die Quersumme berechnen.
Übung 2.5 Erstellen Sie in Ihrer Übungsklasse eine Methode testen(), in der
folgende Fehlerfälle beim Rechnen mit Integer-Zahlen auftreten:
• Bereichsüberschreitungen
• Division durch 0
• Shift um mehr Stellen als vorhanden
Welches Ergebnis erhält man jeweils?
16KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME
Kapitel 3
Abläufe
Bisher hatten wir nur einfache Programme betrachtet, bei denen die Anweisungen
in einer linearen Reihenfolge abgearbeitet wurden. Damit lassen sich allerdings
nur einfache Probleme lösen. Oft ist es notwendig, Anweisungen mehrfach auszuführen oder in Abhängigkeit von Zwischenresultaten oder etwa Benutzereingaben
verschiedene Anweisungen auszuführen. Mit logischen Ausdrücken und entsprechenden Konstruktionen für Schleifen und Verzweigungen wird der Ablauf eines
Programms gesteuert. Abhängig von dem Resultat des logischen Ausdrucks werden alternative Programmzweige durchlaufen, Schleifen beendet, Rückfragen an
den Benutzer gestellt, und so weiter.
3.1
Logische Ausdrücke
Der weitere Ablauf eines Programms soll in Abhängigkeit von einer Bedingung
gewählt werden. Beispielsweise könnte man eine Fallunterscheidung zwischen positiven und negativen Werte benötigen. Dazu stellt Java eine Reihe von Vergleichsoperatoren bereit. Um zu testen, ob der Inhalt einer Variablen test positiv
ist, kann man einen Ausdruck test > 0 verwenden. Das Result des Vergleichs
ist entweder wahr (true) oder falsch (false).
Wird das Ergebnis erst später benötigt, kann es in einer logischen Variablen
abgelegt werden. Dazu steht der Datentyp boolean1 zur Verfügung. Variablen
vom Typ boolean haben entweder den Wert true oder false.
Beispiel 3.1 Verwendung von boolschen Variablen.
int i = 6;
boolean b1 = i < 5;
boolean b2 = true;
System.out.println("b1 = " + b1);
1
benannt nach George Boole, engl. Mathematiker 1815-1864, gilt als Begründer der mathematischen Logik
17
18
KAPITEL 3. ABLÄUFE
Tabelle 3.1: Vergleichsoperatoren in Java
== gleich
!= nicht gleich
>
größer
>= größer gleich
<
kleiner
<= kleiner gleich
Tabelle 3.2: Logische Operatoren in Java
!
Nicht
&
Und
|
Oder
^
exklusives Oder
&& Und mit Short-Circuit-Evaluation
|| Oder mit Short-Circuit-Evaluation
System.out.println("b2 = " + b2);
ergibt
b1 = false
b2 = true
Tabelle 3.1 enthält eine vollständige Liste der Vergleichsoperatoren. Mehrere logische Ausdrücke können durch logische Operatoren verknüpft werden. So bezeichnet & die Und-Verknüpfung zweier Ausdrücke. Die logischen Operatoren sind in
Tabelle 3.2 zusammen gestellt. Es werden wieder die gleichen Zeichen verwendet
wie bei den entsprechenden Bit-Operatoren. Eine Verwechslungsgefahr besteht
nicht, da Java anhand des Typs der Operanden erkennt, ob eine logische oder
bitweise Verknüpfung gemeint ist.
Beispiel 3.2 Logische Operatoren.
n > 5 & n < 10
i == 2 | i == 4 | i == 6
Eine Besonderheit in Java ist die Unterscheidung zwischen Operatoren mit
und ohne so genannter Short-Circuit-Evaluation. Bei der Short-Circuit-Evaluation
nutzt man die Eigenschaften der Operatoren zu einer eventuell verkürzten Auswertung aus. Bei einer Und-Verknüpfung müssen beide Operanden wahr sein,
damit der Ausdruck selbst auch wahr ist. Stellt man nun fest, dass der erste Operand den Wert falsch hat, so steht bereits fest, dass auch der gesamte Ausdruck
den Wert falsch haben wird. Auf die Auswertung des zweiten Ausdrucks kann
3.2. RANGFOLGE DER OPERATOREN
19
dann verzichtet werden. Dadurch kann Rechenzeit gespart werden. Kompliziert
wird das Verhalten, wenn bei der Auswertung des zweiten Ausdrucks Nebenwirkungen auftreten. Betrachten wir das Beispiel
int n = 4;
System.out.println(
System.out.println(
System.out.println(
System.out.println(
n > 5
"n = "
n > 5
"n = "
&& n++ < 10 );
+ n);
& n++ < 10 );
+ n);
Die Bedingung n > 5 ist nicht erfüllt. Mit Short-Circuit-Evaluation wird der
zweite Ausdruck nicht ausgewertet. Dementsprechenden behält die Variable n
ihren alten Wert. Ohne Short-Circuit-Evaluation wird auch der zweite Ausdruck
berechnet und der Wert von n erhöht. Das Programm liefert demnach die Ausgabe
false
n = 4
false
n = 5
Man kann dieses Verhalten gezielt einsetzen und damit eine Art interne Ablaufsteuerung realisieren. Diesen Stil findet man in anderen Programmiersprachen wie
C oder Perl recht häufig. Allerdings sollte man bei der Verwendung vorsichtig sein
und sich auf wenige, klar verständliche Fälle beschränken.
Beispiel 3.3 Typische Short-Circuit-Evaluation in der Programmiersprache Perl
open(IN, ’<’ .
$datei)
|| die "$datei nicht gefunden";
In der Variablen $datei steht der Name einer Datei. Mit open wird versucht,
diese Datei zu öffnen. Falls dies nicht funktioniert, gibt open den Wert 0 zurück.
Dann und nur dann wird der zweite Teil des Oder-Ausdrucks ausgeführt, in dem
mit einer Fehlermeldung abgebrochen wird.
Übung 3.1 Warum gibt es keine Form des exklusiven Oder mit Short-CircuitEvaluation?
3.2
Rangfolge der Operatoren
In Integer-Ausdrücken können arithmetische und logische Operatoren, Vergleichsoperatoren, u. s.ẇ. auftreten. Die Reihenfolge der bisher behandelten Operatoren
ist in Tabelle 3.3 zusammengestellt. Je weiter oben ein Operator in der Tabelle
steht desto höher ist sein Rang oder man sagt, er bindet stärker. Am stärksten
binden die Operatoren, die nur einen Operanden haben.
20
KAPITEL 3. ABLÄUFE
Tabelle 3.3: Reihenfolge der Operatoren
++, -Inkrement, Dekrement
~
bitweise NICHT
!
logisches NICHT
+, Vorzeichen
*, /, %
multiplikative Operatoren
+, Addition und Subtraktion
<<, >>
Bit shift
<, <=, >=, > Vergleichsoperatoren
==, !=
Gleichheit, Ungleichheit
&
bitweises UND
^
bitweises XOR
|
bitweises ODER
&&
logisches UND
||
logisches ODER
3.3
if Abfrage
Die einfache Fallunterscheidung ist durch die if-else Anweisung realisiert. Die
allgemeine Form ist:
if( ausdruck ) {
anweisung1
} else {
anweisung2
}
In der runden Klammer steht ein logischer Ausdruck. Abhängig von seinem Wert
wird bei Resultat Wahr der erste Block ausgeführt, ansonsten der zweite. Der
durch else eingeleitete Block ist optional und kann entfallen. Wenn ein Block
nur aus einer einzigen Anweisung besteht, kann man die geschweiften Klammern
weg lassen.
Beispiel 3.4 if Abfrage
if(
i < 10 ) {
System.out.println("Einstellige Zahl");
} else {
System.out.println("Mehrstellige Zahl");
}
kann auch als
if( i < 10 )
else
System.out.println("Einstellige Zahl");
System.out.println("Mehrstellige Zahl");
3.4. ELSE-IF
21
geschrieben werden. Die Form ohne Klammern sollte nur für einfache und übersichtliche Fälle benutzt werden.
3.4
Else-If
Häufig möchte man mehr als zwei Fälle unterscheiden. Dazu kann man die erweiterte Form mit else if benutzen:
if( ausdruck1 ) {
anweisung1
} else if( ausdruck2 ) {
anweisung2
} else if( ausdruck3 ) {
anweisung3
} else {
anweisung4
}
In dieser Form werden mehrere Überprüfungen geschachtelt nacheinander ausgeführt. Sobald eine Bedingung erfüllt ist, wird der zugehörige Block ausgeführt und
die ganze Kette verlassen (selbst wenn weitere Bedingungen erfüllt wären). Der
letzte (optionale) else Block behandelt dann alle Fälle, die von keiner Bedingung
abgedeckt sind,
Beispiel 3.5 Else-If Konstruktion
if(
i < 10 ) {
System.out.println("Einstellige Zahl");
} else if( i < 100 ) {
System.out.println("Zweistellige Zahl");
} else {
System.out.println("Mehrstellige Zahl");
}
3.5
Fragezeichen-Operator
Die Auswahl zwischen zwei Alternativen kann kompakt mit dem ?-Operator geschrieben werden. Die allgemeine Syntax ist
ausdruck ? alternative_ja : alternative_nein
Ist der Ausdruck wahr, so wird die erste Alternative, ansonsten die zweite ausgeführt. Als Beispiel ist
22
KAPITEL 3. ABLÄUFE
p = ( x > 0 ) ? x : -x;
eine kompakte Alternative zu
if(
x > 0 ) {
p = x;
} else {
p = -x;
}
um den Absolutbetrag zu von x berechnen. Die Verwendung des ?-Operators
ist Geschmackssache. Man kann damit kompakten und eleganten Code schreiben, aber zumindest für Anfänger ist eine ausführliche Formulierung über if-else
übersichtlicher.
3.6
Die switch-Anweisung
Wenn man eine Auswahl aus vielen einander sich gegenseitig ausschließenden
Alternativen treffen will, werden if ... else if Strukturen recht unübersichtlich. Als Vereinfachung stellt Java die Möglichkeit der switch (engl. Schalter)
Anweisung zur Verfügung. Sie hat die Form
switch( ausdruck ) {
case konst1:
anweisung1
case konst2:
anweisung2
default:
anweisung3
}
In der Klammer steht ein Ausdruck, der einen ganzzahligen Wert liefert. Dieser
wird dann mit den Konstanten an den case (engl. Fall) Marken verglichen. Bei
Gleichheit wird das Programm an dieser Stelle fortgesetzt. Etwas gewöhnungsbedürftig ist die Tatsache, dass die Ausführung nicht automatisch durch das nächste
case beendet wird. Soll – wie es in der Regel der Fall ist – die Bearbeitung der
switch Anweisung nach einer Übereinstimmung verlassen werden, muss dies explizit durch ein break festgelegt werden. Das optionale default „sammelt“ alle
Fälle, in denen keine Übereinstimmung gefunden wurde.
Beispiel 3.6 Ausgabe des Wochentages.
int wochentag;
switch( wochentag ) {
case 1: System.out.println("Montag" ); break;
3.7. SCHLEIFEN
23
case 2: System.out.println("Dienstag" ); break;
case 3: System.out.println("Mittwoch" ); break;
case 4: System.out.println("Donnerstag" ); break;
case 5: System.out.println("Freitag" ); break;
case 6: System.out.println("Samstag" ); break;
case 7: System.out.println("Sonntag" ); break;
default: System.out.println("Kein gültiger Wochentag");
}
Möchte man mehrere Fälle zusammenfassen, kann man die entsprechenden case
Marken direkt aufeinander folgen lassen. Man könnte etwa im obigen Beispiel
schrieben
case 6: case 7:
System.out.println("Wochenende" ); break;
3.7
Schleifen
Die logischen Ausdrücke werden auch benutzt, um einen Block von Anweisungen
(Schleifenkörper) mehrfach auszuführen bis eine Abbruchsbedingung erfüllt ist.
Die einfachste Schleife wird mit while eingeleitet:
while( ausdruck ) {
anweisung
}
Der Ausdruck in der while Anweisung wird zunächst ausgewertet. Ergibt er
Wahr, so werden die Anweisungen im Block ausgeführt (vorausgehende Bedingungsprüfung). Anschließend wird die Bedingung wieder überprüft und gegebenenfalls der Block erneut ausgeführt. Beispiel:
Beispiel 3.7 While-Schleife
i = 0;
while( i < 10 ) {
System.out.println("i = " + i++);
}
Die Schleife wird durchlaufen, bis der Wert von i größer als 10 wird. Es gibt keinen
automatischen Schutz, dass die Schleife irgendwann beendet wird. Lässt man den
++ Operator weg, bleibt das Programm ewig in der Schleife. Falls die Bedingung
am Anfang nicht erfüllt ist, wird der Block überhaupt nicht ausgeführt. Man
spricht daher auch von einer abweisenden Schleife.
Die Wiederholung mit einem Schleifenzähler, die so genannte Zählschleife,
kommt so oft vor, dass es dafür ein eigenes Konstrukt gibt – die for Schleife.
24
KAPITEL 3. ABLÄUFE
for( ausdruck1; ausdruck2; ausdruck3 ) {
anweisungen
}
dies ist äquivalent mit
ausdruck1
while( ausdruck2) {
anweisungen
ausdruck3
}
Der erste Ausdruck wird vor Beginn der eigentlichen Schleife (Initialisierung)
ausgeführt. Die zweite Komponente ist die Abbruchsbedingung. Der dritte Ausdruck (Inkrementierung) schließlich wird am Ende jedes Durchgangs angehängt.
Aus dem obigen Beispiel wird dann
for(i = 0; i < 10; i++ ) {
System.out.println("i = " + i);
}
Dies ist eine Standardform solche Schleifen zu schrieben. Allerdings können in
den drei Komponenten von for beliebige Anweisungen stehen. Man könnte auch
das Beispiel als
for(i = 0; i < 10; System.out.println("i = " + i++) );
schreiben. Derartige Konstruktionen sind aber schwerer zu lesen und sollten daher
nicht genutzt werden. Einzelne Komponenten können leer bleiben. Eine leere
Kontrollabfrage gilt als immer wahr. Die Schreibweise for( ;; ) ist eine gängige
Konstruktion für Endlosschleifen.
Übung 3.2 Berechnen Sie in einer for-Schleife die Summe aller ungeraden Zahlen von 1 bis 999.
Es gibt noch eine dritte, relativ selten benutzte Form bei der die Bedingung
nach der Ausführung des Blocks erfolgt (nachfolgende Bedingungsprüfung):
do {
anweisungen
} while( ausdruck );
In diesem Fall wird die Schleife immer mindestens einmal durchlaufen (nicht abweisende Schleife). Erst am Ende der ersten Schleife wird die Bedingung zum
ersten Mal überprüft. Dieser Art von Abfrage ist dann sinnvoll, wenn die Bedingung erst nach der Ausführung ausgewertet werden kann.
3.7. SCHLEIFEN
Tabelle 3.4: Übersicht Schleifen
Typ
Wiederholung mit vorausgehender abweisend
Prüfung (Kopfprüfung)
Wiederholung mit nachfolgender nicht abweisend
Prüfung (Fußprüfung)
Zählschleife
abweisend
25
Java-Syntax
while(){}
do{}while()
for(;;){}
In Tabelle 3.4 sind die drei Grundtypen von Schleifen zusammen mit der JavaSyntax zusammen gestellt. Grundsätzlich kann man noch die Wiederholung ohne
Prüfung als eigenen Typ betrachten. Derartige Schleifen werden endlos wiederholt. Einsatzgebiet sind Prozesse, die ständig auf Ereignisse warten. Für Endlosschleifen gibt es in Java keine gesonderte Konstruktion. Üblich sind die Formen
for(;;) und while( true ).
3.7.1
Vorzeitiges Verlassen von Schleifen
In manchen Fällen ist es erforderlich, eine Schleife entweder ganz zu verlassen
oder zumindest den aktuellen Durchgang abzubrechen. In Java gibt es dazu die
Anweisungen break und continue. Break beendet die Schleife komplett, der
Programmablauf wird mit der ersten Anweisung hinter der Schleife fortgesetzt.
Demgegenüber bleibt bei einem continue das Programm in der Schleife, nur der
aktuelle Durchgang wird nicht zu Ende geführt. Statt dessen wird bei einer while
oder do Schleife die Abbruchbedingung ausgewertet. Bei einer for Schleife wird
das Programm mit der Ausführung des dritten Ausdrucks fortgesetzt.
Beispiel 3.8 break-Anweisung
int i, summe = 0;
for( i=0; ; i++ ) {
if( ( summe += i ) > 1000 ) break;
}
System.out.println("Summe " + summe + " bei i=" + i + " erreicht");
In diesem Programm wird in der Abfrage ausgenutzt, dass auch eine Zuweisung
einen Ausdruck darstellt. Der Wert der Zuweisung ist genau der zugewiesene
Wert. Beispielsweise hat die Zuweisung
i = 4 * 5;
den Wert (rvalue) 20.
26
3.7.2
KAPITEL 3. ABLÄUFE
Sprünge
Wesentlich für die flexible Bearbeitung von Befehlsfolgen ist die Möglichkeit,
die Ausführung an einer Stelle abzubrechen und an einer anderen fortzusetzen.
Die vorgestellten Elemente für Schleifen und Verzweigungen beruhen implizit auf
derartigen Sprüngen in der Befehlsfolge. Aufgrund der großen Bedeutung von
Sprüngen stellen Prozessoren in der Regel auch eine reiche Auswahl an Befehlen
für absolute und relative, bedingte und unbedingte Sprünge zur Verfügung.
Entgegen dieser großen Bedeutung von Sprungbefehlen auf unterer Ebene,
sollten sie in höheren Programmiersprachen weitgehend vermieden werden. Programme mit expliziten Sprungbefehlen werden schnell unübersichtlich und der
Ablauf ist schwer nachvollziehbar. Im Prinzip können mit den bereits vorgestellten Elementen alle Abläufe ohne Sprünge realisiert werden Es gibt allerdings
einige wenige Fälle, in denen Sprünge zu klareren Programmen führen. Insbesondere ist hier der Rücksprung aus mehrfach geschachtelten Schleifen zu nennen:
for( ...) {
for( ...) {
for( ...) {
if( katastrophe ) goto fehlerBehandlung;
...
fehlerBehandlung:
...
Diese Art von Fehlerbehandlung kann sinnvoll sein, um verschiedene Fehlerfälle
gemeinsam zu behandeln.
Die Entwickler von Java haben auf allgemeine Sprunganweisung wie etwa das
goto in C verzichtet und lediglich den Rücksprung aus geschachtelten Schleifen
in die Sprache eingebaut. Dazu können die Befehle break und continue mit einer
Marke versehen werden. Der Name der Zielmarke oder Sprungmarke (label, engl.
Etikett) wird nach den Regeln für Variablen gebildet. Das Label wiederum muss
eine der umgebenden Kontrollstrukturen markieren. Dazu wird der Name gefolgt
von einem Doppelpunkt vor die entsprechende Schleife gesetzt. Für das obige
Beispiel könnte man schreiben:
f1: for( ...) {
for( ...) {
for( ...) {
if( katastrophe ) break f1;
...
In diesem Fall beendet das break die mit f1 markierte umgebende Schleife. Allerdings enthält Java wesentlich leistungsfähigere Konzepte zur Fehlerbehandlung.
Die Bedeutung von gelabelten break und continue Anweisungen ist daher nicht
sehr groß.
3.8. BEISPIELE
3.8
27
Beispiele
Die folgenden kurze Programme illustrieren die Verwendung von Schleifen und
Verzweigungen.
Beispiel 3.9 Berechnung aller Primzahlen bis 30.
int teiler, zahl;
boolean istPrimzahl;
for( zahl=2; zahl<30; zahl++ ) {
istPrimzahl = true;
for( teiler=2; teiler<zahl; teiler++ ) {
if( zahl % teiler == 0 ) {
istPrimzahl = false;
break;
}
}
if( istPrimzahl ) {
System.out.println( zahl + " ist eine Primzahl");
} else {
System.out.println( zahl + " enthält " + teiler);
}
}
Beispiel 3.10 Berechnung der Quersumme einer gegebenen Zahl. Die Variable
verbose wird eingesetzt, um den Umfang der Ausgaben zu regulieren.
int zahl = 1235601;
boolean verbose = true;
int querSumme = 0;
int stelle;
System.out.println( "zahl = " + zahl );
while( zahl > 0 ) {
stelle = zahl % 10;
querSumme += stelle;
if( verbose ) System.out.println(stelle);
zahl /= 10;
}
System.out.println("Quersumme = " + querSumme );
Beispiel 3.11 Wahrheitstabelle: Das Programm gibt für eine Verknüpfung von
vier logischen Variablen die Wahrheitstabelle aus.
28
KAPITEL 3. ABLÄUFE
int a, b, c, d;
System.out.println(" a b c d : ab + c + bd");
for( a=0; a<2; a++ ) {
for( b=0; b<2; b++ ) {
for( c=0; c<2; c++ ) {
for( d=0; d<2; d++ ) {
System.out.print(" "+a+" "+b+" "+c+" "+d+" : ");
if( (a & b | c | b & d) == 1 ) {
System.out.print( "1" );
} else {
System.out.print( "0" );
}
System.out.println();
}
}
}
}
3.9
Übungen
Übung 3.3 Schreiben Sie Schleifen, um die folgenden Sequenzen auszugeben:
1. −10, −8, −6, . . . , 10
2. 1, −2, 3, −4, 5, −6, . . . − 50
3. 1, 2, 4, 7, 11, 16, ... bis Wert > 300
4. 1, 1.1, 1.2, . . . , 1.9, 2
5. 1, 2, 3, 11, 12, 13, 21, 22, 23, . . . , 91, 92, 93
Übung 3.4 Berechnen Sie - soweit definiert - für die ganzen Zahlen von -10 bis
+10 die Werte
1. i3
2. 2 ∗ i2 − 5 ∗ i
3. i!
Übung 3.5 Fibonacci-Zahlen
Die Fibonacci2 -Zahlen sind durch die Startbedingung i1 = 1, i2 = 1 und die
2
Leonardo Pisano genannt Fibonacci, italienischer Mathematiker, ca. 1170-1250
3.9. ÜBUNGEN
29
Rekursion in = in−2 + in−1 definiert. Damit gilt
i3
i4
i5
i6
=
=
=
=
i1 + i2
i2 + i3
i3 + i4
i4 + i5
=1+1=2
=1+2=3
=2+3=5
=3+5=8
und so weiter. Geben Sie alle Fibonacci Zahlen kleiner als 30000 aus. Welchem
Wert nähert sich der Quotient in+1 /in mit wachsendem n an?
Übung 3.6 Sie betreiben einen Internet-Handel. Bei jedem Kunden zählen Sie
die Anzahl der Einkäufe. Abhängig von dieser Zahl gelten Kunden als
Neuling
bis 5 Einkäufe
Kunde
bis 50 Einkäufe
Stammkunde bis 500 Einkäufe
Gold Kunde ab 501 Einkäufe
Wie sieht eine if else if Abfrage aus, um in Abhängigkeit von der Anzahl der
Käufe eines konkreten Kunden seinen Status auszugeben?
Übung 3.7 Berechnen Sie alle Primzahlen bis 1000. Geben Sie dabei alle PrimzahlPaare (d. h. Primzahlen die den minimalen Abstand von 2 haben wie z. B. 3 und
5) aus. Wie viele Paare finden Sie? Wie groß ist der maximale Abstand zwischen
zwei aufeinanderfolgenden Primzahlen?
Übung 3.8 Geben Sie untenstehende Tabelle für das Einmaleins aus.
∗ Geben Sie doppelte Werte (z. B. 6*7 und 7*6) nur einmal aus, so dass aus dem
Rechteck ein Dreieck wird.
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9 10
--------------------------------------: 1
2
3
4
5
6
7
8
9 10
: 2
4
6
8 10 12 14 16 18 20
: 3
6
9 12 15 18 21 24 27 30
: 4
8 12 16 20 24 28 32 36 40
: 5 10 15 20 25 30 35 40 45 50
: 6 12 18 24 30 36 42 48 54 60
: 7 14 21 28 35 42 49 56 63 70
: 8 16 24 32 40 48 56 64 72 80
: 9 18 27 36 45 54 63 72 81 90
: 10 20 30 40 50 60 70 80 90 100
30
KAPITEL 3. ABLÄUFE
Übung 3.9 Die Prüfziffer der Internationale Standard Buchnummer ISBN wird
nach folgendem Algorithmus berechnet: Zunächst wird aus den 9 Ziffern der eigentlichen Kennung eine gewichtete Summe bestimmt. Dazu wird die erste Ziffer der ISBN mit 10 multipliziert, die zweite mit 9, die dritte mit 8 usw. Die
Produkte werden addiert. Von der resultierenden Summe wird die Differenz zur
nächstgrößeren durch 11 teilbaren Zahl berechnet. Dieser Wert wird als Prüfziffer
angehängt. Der Sonderfall 10 wird durch ein X als Prüfziffer markiert. Implementieren Sie diesen Prüfalgorithmus. Testen die Korrektheit an Hand einiger
Beispiele.
Kapitel 4
Verwendung von
Gleitkomma-Zahlen
4.1
4.1.1
Gleitkomma-Zahlen
Gleitkomma- Darstellung
Bisher hatten wir Zahlen in der Integerdarstellung betrachtet. Diese Darstellung
von ganzen Zahlen und das Rechnen damit ist exakt solange man
• im darstellbaren Zahlenbereich bleibt
• keine Nachkommstellen betrachtet (z.B. nach Division)
In vielen praktischen Anwendungen benötigt man aber eine flexiblere Repräsentation von Zahlen. Oft ist das Rechnen mit Nachkommastellen notwendig oder
zumindest natürlich. Viele Angaben enthalten Nachkommastellen (Zinssatz 3,5%,
4,7 Liter auf 100 Km). Die erste Erweiterung ist die Einführung von Nachkommastellen. Bei der Festkommadarstellung gibt man die Anzahl der Vor- und Nachkommastellen fest vor. Als Nachteil bleibt dabei die eingeschränkte Dynamik des
Zahlenbereichs. Daher wird diese Darstellung nur in wenigen Spezialanwendungen
verwendet.
Auch im Alltag und noch mehr in der Technik haben wir das Problem der
unterschiedlichen Bereiche. Bei Längen beispielsweise kann je nach Anwendung
eine Angabe in mm (Schrauben im Baumarkt) oder in km (Urlaubsreise) sinnvoll sein. Der Trick dabei ist, dass die Länge mit einer Anzahl von signifikanten
Stellen und der Größenordnung angegeben wird: 3,5mm oder 650km. Vollständige Genauigkeit 650.245.789mm ist weder sinnvoll noch notwendig. Allgemein
schreibt man eine Größe z als Produkt der Mantisse M und einer ganzzahligen
Potenz p von 10:
z = M · 10p
31
32
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
etwa 3,5 10−3 m. Für Maßangaben gibt es Namen bzw. Vorsilben für die entsprechenden Potenzen von 10 (Kilo, Giga, Mega, Nano, etc). Zum Rechnen muss man
Zahlen auf eine einheitliche Darstellung normieren:
3,5mm + 650km = 0,003.5m + 650.000m = 650.000,003.5m
Die Beispiele zeigen, wie man durch Verschieben des Kommas in der Mantisse den Exponenten verändert. Es gibt für eine gegebene Zahl (unendlich) viele
gleichwertige Darstellungen:
123 = 12, 3 · 10 = 1, 23 · 102 = 1230 · 10−1 = . . .
Eine einheitliche Darstellung erreicht man mit der Vereinbarung, dass die Mantisse nur genau eine Vorkommastelle hat. Damit hat man eine eindeutige Abbildung
zwischen der Zahl z und ihrer Darstellung durch Mantisse m und Exponent p:
z ⇔ (m, p)
Die Position des Kommas wird je nach Bedarf verschoben, man nennt daher dieses Format Gleitkommadarstellung (floating point) oder auch halblogarithmische
Darstellung. Für die Verwendung in Computern geht man zum Dualsystem über,
so dass p dann der Exponent zur Basis 2 ist. In der normalisierten Darstellung
wird das Komma so gesetzt, dass nur eine Vorkommastelle bleibt. Die Mantisse
dann hat im Dualsystem immer die Form
m = 1, . . .
Da die führende 1 bei jeder Zahl steht, braucht man sie in der Realisierung nicht
abzuspeichern. In der Praxis sind für m und p nur endlich viele Bits verfügbar.
Die Größe von m bestimmt die Genauigkeit der Zahlendarstellung und die Größe
von p legt den insgesamt abgedeckten Zahlenbereich fest. Aufgrund der endlichen
Größe von m und p kann es zu folgenden Fehlern in der Repräsentation kommen:
• Die Zahl ist zu groß oder zu klein (z ≥ 2pmax+1 , z ≤ −2pmax+1 ).
• Die Zahl ist betragsmäßig zu klein (|z| < 2−pmax bei symmetrischem Zahlenbereich des Exponenten).
• Die Mantisse ist nicht groß genug, um die erforderliche Anzahl von Stellen
zu repräsentieren (Rundungsfehler).
Beispiel 4.1 Geleitkommadarstellung
Betrachten wir folgende Zahlendarstellung im Dezimalsystem: ±x.xxx · 10±ee .
Geben Sie dazu folgende Wert an:
• Kleinste Zahl
4.1. GLEITKOMMA-ZAHLEN
33
• Größte Zahl
• Betragsmäßig kleinste Zahl 6= 0
• Abstand zwischen den beiden größten Zahlen
• Abstand zwischen den beiden betragsmäßig kleinsten Zahlen
Insbesondere die Rundungsfehler bedingen, dass im allgemeinen eine reelle Zahl
nicht genau dargestellt werden kann. Berechnet man etwa 1/3 so ist das Resultat 0,33333. . . prinzipiell nicht exakt darstellbar. Für die Repräsentierung einer
Gleitkommazahl benötigt man im Detail:
• Mantisse
• Vorzeichen der Mantisse
• Exponent
• Vorzeichen des Exponents
Weiterhin muss festgelegt werden, wie die insgesamt verfügbaren Bits auf Mantisse und Exponent aufgeteilt werden. Lange Zeit waren die Details der Implementierung von Gleitkommazahlen herstellerabhängig. Da dies zu Problemen beim
Datenaustausch und auch bei der Kompatibilität von Programmen führen kann,
wurde vor einigen Jahren ein Standard von Normierungsgremien des IEEE (Institute for Electrical and Electronics Engineers, www.ieee.org) verabschiedet. Dieser
Standard IEEE 754 definiert 3 Formate:
short real
long real
temporary real
32 Bit
64 Bit
80 Bit
einfache Genauigkeit
doppelte Genauigkeit
erweiterte Genauigkeit
Als Beispiel betrachten wir das Format short real näher:
Bit 31
Vz
1 Bit
Bit 0
Characteristik c
8 Bit
Mantisse m
23 Bit
mit
Vz
Vorzeichen der Mantisse (0 positiv, 1 negativ)
Characteristik Exponent + 127
Mantisse
Nachkommastellen
Im Gegensatz zu der bei Integer üblichen Darstellung mit dem 2er-Komplement
wird die Mantisse mit Betrag und getrenntem Vorzeichen abgelegt (VorzeichenBetrag-Darstellung). In der Charakteristik wird der um 127 verschobene Exponent (biased exponent) eingetragen.
34
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
Beispiel 4.2 Konvertierung in den Standard IEEE 754
Die Zahl −37, 12510 soll in das Format short real gebracht werden.
1. Konvertierung in Binärdarstellung: 100101, 001 (32 + 4 + 1 + 1/8)
2. Normalisierung: 1, 00101001 · 25
3. Mantisse: 00101001
4. Charakteristik: 5 + 127 = 13210 = 100001002
5. Vorzeichen: 1 für negative Zahl
Bit 31
1
1 Bit
1000010 0
8 Bit
Bit 0
0010100 10000000 00000000
23 Bit
Die Werte 0 und 255 sind reserviert, um den Wert Null sowie einige Spezialfälle
darstellen zu können. Die Null ist ein Sonderfall, da für sie keine normalisierte
Darstellung mit einer Eins vor dem Komma möglich ist. Daher wurde vereinbart, dass die Null durch ein Löschen aller Bit in Charakteristik und Mantisse
dargestellt wird. Im Detail gilt:
Nicht normalisiert
Null
Unendlich (Inf)
Keine Zahl (NaN)
Vz Charakteristik
±
0
±
0
±
255
±
255
Mantisse
6= 0
0
0
6= 0
Mit den beiden Werten Inf und NaN können Fehlerfälle abgefangen werden. Bei
Bereichsüberschreitung wird das Result auf Inf gesetzt, während NaN durch „unerlaubte“ Operationen wie Division von 0 durch 0 oder Wurzel aus einer negativen
Zahl entsteht. Damit besteht einerseits die Möglichkeit, solche Fälle zu erkennen.
So steht beispielsweise in C die Funktion _isnan zur Verfügung, um auf NaN zu
testen. Andererseits kann man auch mit den Werten weiter rechnen. Der Standard spezifiziert das Ergebnis von Operationen wie Inf + 10 = Inf. In manchen
Algorithmen kann man damit alle Abfragen auf Sonderfälle vermeiden, die sonst
den linearen Programmablauf stören würden. Eine ausführliche Darstellung der
Thematik enthält der Artikel von Goldberg [Gol91].
Beispiel 4.3 Inf und NaN
Der C-Code
printf( "log( 0.) = %15g\n", log( 0.));
printf( "log(-1.) = %15g\n", log(-1.));
liefert das Resultat
4.1. GLEITKOMMA-ZAHLEN
log( 0.) =
log(-1.) =
35
-1.#INF
-1.#IND
Bei den beiden größeren Typen long real und temporary real wird sowohl
die Genauigkeit erhöht als auch der Wertebereich erweitert. Rechnen mit Gleitkommazahlen bedeutet einen wesentlich größeren Aufwand als das Rechnen mit
Integerzahlen. Speziell bei der Addition müssen zunächst die Mantissen und Exponenten verschoben werden, bevor die Mantissen addiert werden können. Das
Ergebnis muss anschließend gegebenenfalls wieder in die Normalform gebracht
werden.
Schnelles Rechen in Gleitkommadarstellung erfordert entsprechenden zusätzlichen Schaltungsaufwand. Früher wurden dafür spezielle Baustein als Co-Prozessoren
eingesetzt. Heute ist eine entsprechende Einheit bei den leistungsfähigen Prozessoren bereits integriert. Die Angabe der möglichen Gleitkommaoperationen pro
Sekunde (FLOPS: floating point operations per second) ist eine wichtige Kenngröße für die Leistungsfähigkeit eines Computers.
Übung 4.1 Gleitkommadarstellung
Betrachten Sie folgendes einfaches Format für Gleitkommazahlen:
• Bit 15: Vorzeichen des Exponenten (0 für positiv)
• Bit 14: Vorzeichen der Mantisse (0 für positiv)
• Bit 6-13 Betrag der Mantisse
• Bit 0-5 Betrag des Exponenten
Welchen Wert haben die folgenden Bitmuster:
0
1
0
0
0110 0000
1010 0000
00 0011
00 0100
Übung 4.2 Wertebereich im Standard IEEE 754
Welches ist jeweils die
• kleinste
• größte
• betragsmäßig kleinste
darstellbare Zahl im short real Format?
Übung 4.3 Konvertierung in den Standard IEEE 754
Wie wird die Zahl 14, 62510 als Gleitkommazahl im Format real short dargestellt?
Geben Sie das Resultat in Binär- und Hexadezimaldarstellung an.
36
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
Übung 4.4 Addition und Multiplikation
Berechnen Sie für die beiden Zahlen 7, 5 · 104 und 6, 34 · 102 Summe und Produkt.
Geben Sie das Ergebnis jeweils in normierter Form mit einer Vorkommastelle an.
Welche einzelnen Rechenschritte sind erforderlich?
Übung 4.5 Darstellung der Zahl 0
In IEEE 754 gibt es zwei Bitmuster für die Zahl 0, einmal mit positiven und
einmal mit negativem Vorzeichen. Diese Werte kann man als +0 und -0 interpretieren. Wo kann man diese Unterscheidung sinnvoll einsetzen? Welche Probleme
können sich aus der doppelten Darstellung ergeben?
4.1.2
Verwendung von Gleitkommazahlen
Der große Vorzug von Gleitkommazahlen ist der große Wertebereich mit gleichbleibender relativer Genauigkeit. Andererseits ist die Abdeckung – im Gegensatz
zu den Integerzahlen – nicht vollständig. Zwischen benachbarten Gleitkommazahlen ist eine Lücke und die Breite der Lücke hängt von der Größe der Zahlen
ab. Dies kann zu Effekten führen, die der Mathematik wiedersprechen. Das folgende Fragment C-Code dient zur Veranschaulichung dieses Verhaltens. In dem
Programm wird zu einer Zahl der Wert 1 addiert, wobei die Zahl schrittweise um
den Faktor 10 erhöht wird.
double test = 1.;
double testp1;
do{
test *= 10.;
testp1 = test + 1.;
printf( "%10g %10g %5g \n", test, testp1, testp1-test );
} while( testp1 > test );
Die Ausführung liefert die Ausgabe:
10
100
1000
10000
100000
1e+006
1e+007
1e+008
1e+009
1e+010
1e+011
11
101
1001
10001
100001
1e+006
1e+007
1e+008
1e+009
1e+010
1e+011
1
1
1
1
1
1
1
1
1
1
1
4.2. GLEITKOMMAZAHLEN IN JAVA
1e+012
1e+013
1e+014
1e+015
1e+016
1e+012
1e+013
1e+014
1e+015
1e+016
37
1
1
1
1
0
Zunächst zeigt das Programm das erwartete Verhalten und berechnet die Differenz zu 1. Aber wenn der Wert 1016 erreicht ist, führt die Addition nicht mehr
zu einer anderen Zahl und die Differenz zwischen 1016 und 1016 + 1 liefert den
Wert 0. Dieser Einfluss der Rundungsfehler ist bei der Programmentwicklung zu
berücksichtigen.
Beispiel 4.4 Rundungsfehler
Die folgende Anweisung in C
printf( "5. - sqrt(5)*sqrt(5) = %15g\n", 5. - sqrt(5.)*sqrt(5.));
ergibt
5. - sqrt(5)*sqrt(5) =
-8.88178e-016
Übung 4.6 Reihenfolge der Auswertung eines Ausdrucks
Sei x = 1030 , y = −1030 und z = 1. Welches Resultat ergibt sich bei dem Rechnen
in Gleitkomma-Arithmetik für die beiden Ausdrücke
• (x + y) + z
• x + (y + z)
4.1.3
Vergleich der Zahlenformate
Abschließend sind die wesentlichen Unterschiede in den Zahlendarstellungen in
Tabelle 4.1 zusammen gestellt. Abhängig von der Anwendung sind die einzelnen
Kriterien unterschiedlich zu gewichten. Beispielsweise spielt bei einer low-costAnwendung der Preis eine entscheidende Rolle, so dass auf eine eigene Einheit
für Gleitkommaoperationen verzichtet wird. Dann ist es oft notwendig Berechnungen, für die Gleitkommazahlen besser geeignet wären, aus Performanzgründen
trotzdem mit Integerzahlen durchzuführen.
4.2
Gleitkommazahlen in Java
Java unterstützt die beiden Gleitkomma-Typen float und double gemäß IEEE754. Zwischen Gleitkomma-Werten gelten die Grundrechenarten mit den Operatoren +, -, * und / mit den schon behandelten Vorrangsregeln. Auch die Vergleichsoperatoren sind für Gleitkomma-Werte gültig. Der Modulo Operator %
38
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
Tabelle 4.1: Vergleich zwischen Integer- und Gleitkommazahlen
Integer
Gleitkomma
Nachkommastellen
Nein
Ja
Genauigkeit
Ergebnisse sind exakt
Rundungsfehler
Bereich
eingeschränkt
groß
Überlauf
wird nicht gemeldet
Inf
Rechenaufwand
niedrig
groß
Speicherbedarf
8-32 Bit
32-80 Bit
Bit-Operatoren, modulo
allerdings ist in diesem Fall sinnlos und kann daher nicht auf Gleitkomma-Werte
angewandt werden. Ebenso sind die Bitoperationen für Gleitkomma-Werte nicht
definiert.
Die Genauigkeit beträgt etwa 7 Stellen bei float und 15 Stellen bei double.
Bei der Eingabe von Konstanten benutzt man den Punkt anstelle des Kommas.
Optional kann man einen Zehner-Exponenten angeben.
Beispiel 4.5 Gültige Gleitkommazahlen:
0.456
-177.999
99.988e17
-1e-10
.1e-10
Die Werte werden intern normalisiert, bei der Eingabe ist man frei in der Anzahl
der Stellen vor dem Dezimalpunkt. Der Dezimalpunkt direkt vor dem Exponenten kann weggelassen werden. Standardmäßig interpretiert der Compiler diese
Konstanten als double. Durch Anhängen der Endung f (F) werden sie zu float
Werten.
Im Gegensatz zu Integerwerten stellt für übliche Anwendungen der Wertebereich kein Problem dar. Über- oder Unterschreitungen sind eher selten. Allerdings
muss man stets mit Rundungseffekten rechnen. Problematisch sind Vergleiche in
der Art
if( x == 5 )
Wenn x das Ergebnis einer Rechnung ist, kann es durchaus sein, dass x aufgrund
der Rundungsfehler den Wert 4.99999 hat. Die Abfrage würde dann nicht erfüllt
sein. Besser ist, in einem solchen Fall
if( Math.abs( x - 5 ) < epsilon )
4.2. GLEITKOMMAZAHLEN IN JAVA
39
zu schrieben, wobei epsilon die gewünschte oder geforderte Genauigkeit angibt.
Mit der gleichen Vorsicht muss man for-Schleifen mit Gleitkommawerten betrachten. In dem Beispiel
for( x=0.; x<=1.; x+=0.01 )
ist nicht gewährleistet, dass der Wert 1 exakt getroffen wird, so dass eventuell
(z. B. bei x=1.0000001) die Schleife eine Iteration zu früh abgebrochen wird.
4.2.1
Mathematisch Funktionen
Eine ganze Reihe von mathematische Funktionen sind in Java standardmäßig
verfügbar. Die Funktionen sind als Methoden einer Klasse Math aufrufbar. Wir
werden in einem späteren Kapitel auf die Verwendung von Methoden genauer
eingehen. An dieser Stelle verwenden wir Methoden als Implementierung von
mathematischen Funktionen. Die Syntax in Java für den Aufruf einer Methode
ist
Klasse.methode( Parameterliste )
Die Methode erhält eine Anzahl von Parameter, berechnet aus diesen Werten ein
Ergebnis und gibt dieses Ergebnis zurück. Das Beispiel
y = Math.sin( t );
berechnet den Sinus des Wertes in der Variablen t und speichert das Resultat in
y. Unter anderem gibt es:
• trigonometrische Funktionen: sin(x), cos(x), tan(x), Argument jeweils
im Bogenmaß
• inverse trigonometrische Funktionen: asin(x), acos(x), atan(x)
• Potenzen und Logarithmen: exp(x), log(x), sqrt(x), pow(x,y)
• Runden:
– ceil(x) kleinste ganze Zahl größer x
– floor(x) größte ganze Zahl kleiner x
• Betrag: abs(x)
• Zufallszahlen: random() liefert eine Zufallszahl aus dem Intervall [0, 1]
Probleme entstehen, wenn
• das Argument nicht in dem Definitionsbereich liegt (log(-1)) (domain error)
40
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
• das Ergebnis nicht mehr darstellbar ist (pow(1000,1000)) (range error)
Die Funktionen geben in diesen Fällen definierte Sonderwerte zurück, die auch
bei der Ausgabe als solche dargestellt werden.
Beispiel 4.6 Sonderfälle in mathematischen Funktionen.
public static void main(String[] args) {
/* range error with exp() */
for( double x=0; x<1000; x+=300. ) {
System.out.println("x = "+ x +", exp(x) = "+ Math.exp(x) );
}
/* domain error with log() */
System.out.println( "log( 0.) = " + Math.log( 0.));
System.out.println( "log(-1.) = " + Math.log(-1.));
}
ergibt
x = 0.0, exp(x) = 1.0
x = 300.0, exp(x) = 1.9424263952412558E130
x = 600.0, exp(x) = 3.7730203009299397E260
x = 900.0, exp(x) = Infinity
log( 0.) = -Infinity
log(-1.) = NaN
4.3
Umwandlung zwischen Datentypen
In einem Ausdruck können die Operanden verschiedenen Datentyp haben. Der
Typ des Ergebnisses hängt dann von den beteiligten Datentypen ab. Auch bei
einer Zuweisung hat der Ausdruck auf der rechten Seite manchmal einen anderen
Ergebnistyp als die Variable auf der linken Seite. In solchen Fällen erfolgt eine
Umwandlung des Typs vor der Rechnung bzw. der Zuweisung. Die Umwandlung
erfolgt dabei stets vom „kleineren“ zum „größeren“ Typ (erweiternde Konvertierung). Beispielsweise wird bei der Addition eines int und eines long Wertes
zunächst der int Wert in einen long Wert gewandelt. Die Umwandlung erfolgt
allerdings „Schritt für Schritt“. Selbst wenn etwa auf der linken Seite einer Anweisung eine double Variable steht, werden nicht notwendigerweise alle Rechnung
in double ausgeführt. Das folgende Beispiel demonstriert diesen Effekt:
Beispiel 4.7 Umwandlung.
double test;
test = 10 / 3;
System.out.println( "Konvertierung 1, test = " + test);
4.3. UMWANDLUNG ZWISCHEN DATENTYPEN
41
test = 10. / 3;
System.out.println( "Konvertierung 2, test = " + test);
Im ersten Fall wird die rechte Seite noch als Integer-Rechnung ausgeführt. Erst
das Ergebnis wird umgewandelt. Im zweiten Fall ist der erste Operand 10. bereits
ein double, so dass vor der Division die 3 umgewandelt wird. Entsprechend liefert
das Beispiel
Konvertierung 1, test = 3.0
Konvertierung 2, test = 3.3333333333333335
Bei der erweiternde Konvertierung bleibt die Information erhalten. Sie werden
daher als sicher angesehen und wenn notwendig vom Compiler automatisch eingefügt. Demgegenüber sind Konvertierungen in die andere Richtung (einschränkende Konvertierung) unsicher. Eventuell verliert man Genauigkeit oder der Wert
passt nicht mehr in den geringeren Darstellungsbereich. Daher werden einschränkende Konvertierungen vom Compiler als Fehler behandelt. Die Anweisung
int i = 1.5;
führt zu der Fehlermeldung
FloatTest.java [35:1] possible loss of precision
found
: double
required: int
int i = 1.5;
^
1 error
Errors compiling main.
Man kann explizit eine Konvertierungen mittels des so genannten Type-CastOperators anfordern. Die allgemeine Form, um einen Ausdruck a in einen anderen
Datentyp zu wandeln, ist
(Datentyp) a
Sofern möglich, wird der Ausdruck in den in Klammern angegebenen Typ konvertiert. Damit werden auch einschränkende Konvertierungen in der Art
int i = (int) 1.5;
vom Compiler akzeptiert. Allerdings gilt dies nur für legale Konvertierungen. Der
Versuch, einen float-Wert in einen boolean zu wandeln,
boolean b = (boolean) 1.5;
scheitert bereits beim Kompilieren mit der Fehlermeldung
42
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
FloatTest.java [36:1] inconvertible types
found
: double
required: boolean
boolean b = (boolean) 1.5;
^
1 error
Errors compiling main.
4.4
Übungen
Übung 4.7 Nach einer alten Legende wünschte sich der Erfinder des Schachspiels vom König:
• 1 Reiskorn auf dem ersten Feld eines Schachbrettes
• 2 Reiskörner auf dem 2. Feld
• 4 Reiskörner auf dem 3. Feld
• u.s.w.
Geben Sie die Anzahl der Reiskörner mit wachsender Anzahl von Feldern aus.
Berechnen Sie zusätzlich, wie viele LKWs (je 7,5 Tonnen Ladung) man für den
Transport benötigt, wenn jedes Reiskorn 30 mg wiegt. Wenn weiterhin ein Reiskorn ein Volumen von etwa 3.5 · 10−8 m3 hat, wie hoch wird dann ein Fußballfeld
(70 auf 105 m) bedeckt?
Übung 4.8 Schreiben Sie ein Programm, das die Lösungen der quadratischen
Gleichung x2 + p · x + q = 0 berechnet. Dabei soll q den festen Wert 0,1 haben und
p in Schritten von 0,1 von 0 bis 2 laufen. Prüfen Sie jeweils, ob eine reelle Lösung
existiert. Falls ja, berechnen Sie die beiden Lösungen. Zur Kontrolle setzten Sie
die gefundenen Werte in die Gleichung ein und geben das Ergebnis ebenfalls aus.
Im Fall von komplexen Lösungen geben Sie einen entsprechenden Hinweis aus.
Hinweis: zum Berechnen der Wurzel gibt es die Methode Math.sqrt();
Übung 4.9 Sie nehmen ein Darlehen über 100000 Euro auf. Der jährliche Zinssatz beträgt 4,5%. Jeden Monate zahlen Sie eine Rate von 500 Euro. Verfolgen Sie
per Programm die Entwicklung des Darlehens. Berechnen Sie dazu jeden Monat
die verbliebene Restschuld und geben diesen Wert jeweils am Ende eines Jahres
aus. Wie lange dauert es, das Darlehen zu tilgen? Wie viele Zinsen werden bis
zum Ende insgesamt gezahlt worden sein?
4.4. ÜBUNGEN
43
Übung 4.10 Für die Masse m eines Körpers mit der Ruhmasse m0 gilt bei einer
Geschwindigkeit v
m0
m= s
µ ¶2
v
1−
c
Berechnen Sie für einen Astronauten mit m0 = 80kg die Masse bei Annäherung
an die Lichtgeschwindigkeit c = 300000km/s für die Geschwindigkeiten v = 0.01 ·
c, 0.02 · c, . . . , 0.99 · c.
Übung 4.11 Bestimmen Sie durch Monte-Carlo Simulation eine Näherung für
die Zahl π. Betrachten Sie dazu ein Quadrat der Seitenlänge 1, in dem ein Viertelkreis mit Radius 1 liegt. Die Fläche FQ des Quadrats ist 1, die des Viertelkreises
FV = π/4. Damit gilt die Beziehung π = 4 · FV /FQ . Zur experimentellen Bestimmung des Verhältnis FV /FQ erzeugt man zufällige Punkte im Intervall [0, 1; 0, 1]
und zählt, wie viele davon in den Viertelkreis fallen.
Übung 4.12 Die Methode Float.floatToRawIntBits( float f ) wandelt einen
Wert vom Typ float in einen int Wert mit dem gleichen Bitmuster um. Dieses
Bitmuster kann dann ausgegeben werden (Siehe Übung 2.3). Schreiben Sie ein
Programm, um das Bitmuster von float Werten auszugeben. Verwenden Sie die
Bitoperatoren, um die einzelnen Komponenten Vorzeichen, Charakteristik und
Mantisse getrennt auszugeben.
44
KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN
Kapitel 5
Felder
Häufig benötigt man zur Darstellung eines Sachverhaltes nicht nur eine einzelne Variable eines bestimmten Datentyps, sondern eine Reihe oder Anzahl von
gleichartigen Variablen. Beispiele sind:
• die Matrikelnummern aller Hörer und Hörerinnen dieser Vorlesung
• alle Primzahlen kleiner 1000
Charakteristisch ist, dass ein fester Datentyp mehrfach benötigt wird. In dem
Beispiel der Matrikelnummern braucht man eine Struktur, die nacheinander alle
Nummern (jeweils in einer int-Variablen) enthält. Im Speicher hat man folgende
Darstellung:
1. Matrikelnummer
2. Matrikelnummer
...
N-te Matrikelnummer
Die N aufeinander folgende Speicherzellen enthalten die Matrikelnummern der
Studenten und Studentinnen. Die entsprechende Datenstruktur ist das Feld (engl.
array). Da ein Feld aus den einfachen Datentypen aufgebaut ist, spricht man von
einem strukturiertem Datentyp. Ein Feld ist charakterisiert durch:
• einen Namen (Bezeichner)
• einen Datentyp
• der vorgegebenen, festen Anzahl der Elemente, d. h. Variablen des Datentyps
• Indizes für die Elemente
Die Deklaration eines Feldes entspricht der Deklaration einer Variablen ergänzt
um ein Paar eckiger Klammern. Die Klammern können nach dem Datentyp oder
nach dem Variablennamen stehen:
45
46
KAPITEL 5. FELDER
int ifeld[];
byte[] bfeld;
// meine bevorzugte Form
Nach der Deklaration muss noch Speicherplatz reserviert werden. Dies erfolgt mit
dem Operator new. Um für das oben deklarierte Integerfeld eine Größe von 10
Werten anzulegen, schreibt man:
ifeld = new int[10];
Deklaration und Speicherreservierung können auch gleichzeitig erfolgen:
float[] feld = new float[100];
Der neu reservierte Speicherbereich wird standardmäßig mit Nullen gefüllt. Bei
Zahlentypen ist dies der Wert 0 während Felder von boolean mit false belegt
werden.
Beispiel 5.1 Anlegen eines Feldes
Nach der Anweisung double[] messwerte = new double[30] gilt:
• Es gibt ein Feld mit dem Namen messwerte.
• Das Feld besteht aus 30 Speicherzellen.
• Jede Speicherzelle hat Platz für einen Wert vom Typ double.
• Alle Speicherzellen enthalten als Anfangswert 0.
Eine zweite Form der Initialisierung erlaubt es, direkt die Werte (Literale) anzugeben. Die Größe des Feldes wird dann automatisch bestimmt.
Beispiel 5.2 Anlegen eines Feldes mit 4 Werten
short[] sfeld = { 1, 3, 5, 7};
Diese Form kann nur zusammen mit der Deklaration verwendet werden. Die Felder in Java sind semidynamisch. Ihre Größe wird zur Laufzeit festgelegt, ist danach aber fest. Es ist nicht möglich, ein bestehendes Feld zu erweitern. Allerdings
kann jederzeit ein neuer Speicherbereich mit unterschiedlicher Größe angefordert
werden.
ifeld = new int[10];
...
ifeld = new int[20];
Eine zweite oder weitere Reservierung legt einen vollständig neuen Speicherbereich an. Die alten Werte gehen verloren.
5.1. ZUGRIFF AUF ELEMENTE
5.1
47
Zugriff auf Elemente
In Java bieten die Felder einen Namen für eine Reihe gleichartiger Elemente. Man
kann nicht direkt mit Feldern rechnen. Der Versuch in der Art
int[] a = new int[3], b = new int[3];
a += b;
führt zu der Fehlermeldung
Feldtest.java [22:1] operator + cannot be applied to int[],int[]
a += b;
^
1 error
Zum Rechnen muss man auf die einzelnen Elemente zugreifen. Der Zugriff erfolgt
über den Index, d. h. der Position im Feld. Die Zählung der Elemente beginnt
in Java immer mit dem Index 0. Bei einem Feld mit 10 Elementen haben die
einzelnen Element die Indizes 0, 1, 2, . . ., 9. Ansprechen kann man ein Element
über den Namen des Feldes und den Index in eckigen Klammern:
a[2]
ist das 3. Element (das Element mit Index 2) des Feldes a. Mit einem solchen Element kann man umgehen wie mit einer Variablen. Es kann sowohl in Ausdrücken
eingesetzt werden als auch Ziel einer Zuweisung sein.
Beispiel 5.3 Verwendung von Elementen aus Feldern
int[] a = new int[3], b = new int[10];
...
a[0] = 50 * b[2] + 30 * b[3];
Um alle Elemente eines Feldes zu bearbeiten, kann man beispielsweise for Schleifen benutzen.
Beispiel 5.4 Füllen eines Feldes mit Quadratzahlen
int size = 10;
int[] feld = new int[size];
int i;
for( i=0; i<size; i++ ) {
feld[i] = i * i;
}
48
KAPITEL 5. FELDER
Der Zugriff auf ein Element außerhalb des definierten Bereichs führt zu einem
Laufzeitfehler.
Beispiel 5.5 Zugriff auf Elemente außerhalb eines Feldes
int size = 10;
int[] feld = new int[size];
feld[11] = 111;
führt zu der Fehlermeldung
java.lang.ArrayIndexOutOfBoundsException
at Feldtest.main(Feldtest.java:31)
Exception in thread "main"
Damit werden eine Vielzahl von Programmierfehler abgefangen. In anderen Sprachen wie C ohne Bereichsprüfung sind Fehler durch falsch berechnete Zugriffe oft
nur sehr schwer zu finden. Der Preis dafür ist die langsamere Ausführung durch
die internen Prüfabfragen.
Intern wird ein Feld als Objekt behandelt. In einem Feld-Objekt ist auch die
Information über die Länge abgelegt. Diese Information kann über eine Variable
namens length abgefragt werden. Die genaue Syntax ist feldname.length. Dann
kann ein Feld in der Form
for(int j=0; j<feld.length; j++ ) {
System.out.println( feld[j] );
}
ausgegeben werden. Es es ist auf diese Art und Weise jederzeit möglich, die aktuelle Länge eines Feldes abzufragen.
Seit Version 1.5 bietet Java für solche Fälle eine erweiterte Form der forSchleife an. Anstelle eines expliziten Zählers werden nacheinander alle Elemente
eines Feldes angesprochen. Diese Art von Schleifen werden allgemein als foreachSchleifen bezeichnet. Die Ausgabe aller Elemente wird dann in der Form
for(int w : feld ) {
System.out.println( w );
}
realisiert. In der Schleife werden nacheinander der Variablen w alle Elemente des
Feldes feld zugewiesen.
Beispiel 5.6 Sieb des Eratosthenes
Bestimmung von Primzahlen nach der Methode des Eratosthenes1 .
1
griechischer Mathematiker, 276-195 v. Chr.
5.2. MEHRDIMENSIONALE FELDER
49
public static void main(String[] args) {
int max = 1000;
boolean[] istPrimzahl = new boolean[max+1];
int i;
// Hypothese: alles sind Primzahlen
for( i=1; i<=max; i++ ) istPrimzahl[i] = true;
int test = 2;
while( test < max / 2 ) {
// naechste Primzahl suchen
while( ! istPrimzahl[test] ) test++;
// ganzzahlige Vielfache dieser Primzahl streichen
for( i=test+test; i<=max; i+=test ) istPrimzahl[i] = false;
++test;
}
// verbliebene Primzahlen ausgeben
for( i=2; i<=max; i++ ) {
if( istPrimzahl[i] ) System.out.println( "Primzahl " + i );
}
}
5.2
Mehrdimensionale Felder
Mehrdimensionale Felder werden durch mehrere Klammerpaare spezifiziert:
Beispiel 5.7 Zweidimensionales Feld
// Zweidimensionales Feld mit 10 x 20 Elementen
int[][] zifeld = new int[10][20];
Zweidimensionale Felder sind intern ineinander geschachtelte Felder. Damit kann
man auch nicht-rechteckige Felder erzeugen. Die Anweisung
zifeld[0] = new int[30];
führt dazu, dass das erste Unterfeld jetzt 30 Elemente hat.
Beispiel 5.8 Ineinander geschachtelte Felder
Im folgenden Code wird ein Feld für alle Tage eines Jahres geordnet nach Monaten
angelegt. Für jeden Tag wird über den logischen Wert dargestellt, ob es sich um
einen Urlaubstag handelt.
50
KAPITEL 5. FELDER
boolean[][] urlaub = new boolean[12][];
urlaub[0] = new boolean[31];
urlaub[1] = new boolean[28];
...
urlaub[7][0] = true;
// 1. August ist ein Urlaubstags
urlaub[11][24] = true; // 25. Dezember ist ein Urlaubstags
5.3
Übungen
Übung 5.1 Fußball
In den letzten 5 Jahren haben Professoren und Studenten gegeneinander Fußball
gespielt. Die jeweils erzielten Tore sind in zwei Feldern
int[] stud = { 3, 2, 5, 7, 1};
int[] prof = { 0, 4, 2, 1, 1};
gespeichert. Im ersten Jahr war das Ergebnis 3:0, dann 2:4 und so weiter. Schreiben Sie eine Auswertung um
• die Gesamt-Punkte für die Studenten (Sieg 3 Punkte, Remis 1 Punkt)
• das Gesamt-Torverhältnis
zu ermitteln. Die Ausgabe könnte wie folgt aussehen (willkürliche Zahlenwerte):
Studenten gegen Professoren
Punkte: 33
Tore: 25:23
Übung 5.2 In der folgenden Methode wird ein Feld mit zufälligen Werten gefüllt:
public void auswerten() {
int anzahl = 100;
double[] werte = new double[anzahl];
for( int i=0; i<werte.length; i++) {
werte[i] = Math.random();
}
}
Lassen Sie aus diesem Feld folgende Größen berechnen:
• Mittelwert
• kleinster Wert
5.3. ÜBUNGEN
51
• größter Wert
Übung 5.3 Definieren Sie ein int Feld, das für jeden Monat (0-11) die Anzahl
der Tage enthält (kein Schaltjahr). Prüfen Sie die Eingabe, indem Sie alle Werte
addieren. Wie kann man unter Verwendung dieses Feldes ausrechnen, in den
wievielten Monat der Tage M (0-364) eines Jahres fällt?
Übung 5.4 Verwenden Sie das Feld aus Übung 5.3 für eine elegantere Lösung
zum Aufbau des zweidimensionalen Urlaub-Feldes in Beispiel 5.8. Tragen Sie einige Urlaubstage ein und lassen Sie das Ganze in geeigneter Form ausgeben.
Beispiel für eine Ausgabe (verkürzte Darstellung):
*
1
1
1
1
1
1
*
1
1
1
1
*
2
2
2
2
2
2
*
2
2
2
2
3
3
3
3
3
3
3
*
3
3
3
3
4
4
4
4
4
4
4
*
4
4
4
4
5
5
5
5
5
5
5
*
5
5
5
5
6
6
6
6
6
6
6
6
6
6
6
6
7
7
7
7
7
7
7
7
7
7
7
7
8
8
8
8
8
8
8
8
8
8
8
8
9
9
9
9
9
9
9
9
9
9
9
9
10
10
10
10
10
10
10
10
10
10
10
10
11
11
11
11
11
11
11
11
11
11
11
11
12
12
12
12
12
12
12
12
12
12
12
12
18
18
18
18
18
18
18
18
18
18
18
18
19
19
19
19
19
19
19
19
19
19
19
19
20
20
20
20
20
20
20
20
20
20
20
20
21
21
21
21
21
21
21
21
21
21
21
21
22
22
22
22
22
22
22
22
22
22
22
22
23
23
23
23
23
23
23
23
23
23
23
23
24
24
24
24
24
24
24
24
24
24
24
24
25
25
25
25
25
25
25
25
25
25
25
*
26 27 28 29 30 31
26 27 28
26 27 28 29 30 31
26 27 28 29 30
26 27 28 29 30 31
26 27 28 29 30
26 27 28 29 30 31
26 27 28 29 30 31
26 27 28 29 30
26 27 28 29 30 31
26 27 28 29 30
* 27 28 29 30 31
Übung 5.5 Felder als Zähler
Implementieren Sie auf der Basis der Methode Math.random() eine WürfelSimulation, die Zahlen zwischen 1 und 6 erzeugt. Zählen Sie in einem Feld entsprechender Größe, wie oft jede Zahl bei 10000 Würfen vorkommt.
Übung 5.6 Game of Life
Ausgedacht hat sich dieses „Spiel“ der amerikanische Mathematiker Conway. Bekannt wurde das Game of Life, als es im Jahr 1970 im Wissenschaftsmagazin
Scientific American vorgestellt wurde. Die Regeln sind einfach: Gespielt wird auf
einem rechteckigen Feld, das wie ein Schachbrett in lauter quadratische Zellen
eingeteilt ist. Eine Zelle ist entweder besetzt oder unbesetzt. Eine Zelle hat bis
zu 8 Nachbarzellen, d. h. Zellen die in gerader oder schräger Richtung direkt neben der Zelle liegen. Eine Konfiguration von besetzten und unbesetzten Zellen
kann man sich als Generation von Lebewesen vorstellen, aus der sich die nächste
Generation nach folgenden Regeln entwickelt:
1. Eine leere Zelle wird in der nächsten Generation besetzt, wenn sie genau
drei besetzte Nachbarzellen hat.
2. Eine besetzte Zelle bleibt auch in der nächsten Generation besetzt, wenn sie
zwei oder drei besetzte Nachbarzellen hat.
52
KAPITEL 5. FELDER
3. Alle Zellen, bei denen die Voraussetzungen der Regeln 1 und 2 nicht zutreffen, sind in der nächsten Generation unbesetzt.
Realisieren Sie eine einfache Version des Spiels. Benutzen Sie zwei 2-dimensionale
Felder oder ein 3-dimensionales Feld für die aufeinander folgenden Generationen
um eine Version von Life zu realisieren. Wählen Sie die Feldgröße passend zu der
Größe des Ausgabefensters.
Hinweise:
Die Behandlung der Zellen am Rand wird wesentlich vereinfacht, wenn man das
Brett um zusätzliche, leere Zeilen und Spalten an den Rändern erweitert.
Bei Aufruf der Methode Thread.sleep(long m) wartet das Programm m Millisekunden.
Kapitel 6
Methoden
6.1
Einleitung
Ein wesentlicher Gedanke der strukturierten Software-Entwicklung ist die Aufteilung einer komplexen Aufgabe in einzelne Teilaufgaben. Programmtechnisch
entspricht dies der Aufteilung des Programms in mehrere Untereinheiten oder
Module. In Java ist die entsprechende Untereinheit eine Methode. Eine Methode
ist ein eigenständiger Verarbeitungsblock mit definierten Eingangs- und Ausgangswerten. Vorteile von Methoden sind:
• Berechnungen, die mehrfach benötigt werden, brauchen nur einmal programmiert zu werden.
• Kapselung von Funktionalitäten (black box Prinzip, information hiding)
• Verbesserte Testmöglichkeiten
• Wiederverwendbarkeit in anderen Projekten
In verschiedenen Programmiersprachen gibt es unterschiedliche Bezeichnungen
und Konzepte für Module. Man kann grob unterscheiden:
• Funktionen verfügen über Argumente und Rückgabewerte
• Prozeduren oder Unterprogramme (subroutines) haben keinen Rückgabewert
• Methoden sind in objektorientierten Sprachen Funktionen, die zu einer
Klasse gehören
• Makros sind Quellcode-Bausteine, bei denen vor dem Kompilieren ein gemeinsames, in der Regel längeres Stück Code an die entsprechend markierten Stellen eingefügt werden (Makro-Ersetzung).
53
54
6.2
KAPITEL 6. METHODEN
Definition
Eine Methode ist ein Block von Definitionen und Anweisungen mit einem Namen
sowie Eingangswerten und einem Ausgangswert. Methoden stehen auf der ersten
Ebene, d. h. die Definitionen können nicht ineinander geschachtelt werden. Allerdings ist es sehr wohl möglich, dass eine Methode eine weiter Methode aufruft.
Die allgemeine Form ist
attribute rückgabe-typ methodenName( parameter ) . . .
Zunächst wird spezifiziert, welchen Typ von Variable die Methode zurück gibt.
Anschließend folgt der Name der Methode und dann in runden Klammern die
Liste der Eingangswerte (Parameter). Jeder Parameter besteht aus dem Typ und
einem Namen. Mehrere Parameter werden durch Komma getrennt. Die verkürzte
Schreibweise wie etwa (int i, j) ist nicht erlaubt, jeder Parameter benötigt
eine eigene Typangabe: (int i, int j). Die eigentliche Methode folgt dann als
Block begrenzt durch geschweifte Klammern. Für die Rückgabe steht der Befehl
return ausdruck;
zur Verfügung. Der Ausdruck muss dabei dem bei der Definition der Methode
angegeben Datentyp entsprechen. Bei Erreichen eines return Befehls wird die
Bearbeitung der Methode beendet. Der Ausdruck wird ausgewertet und an das
aufrufende Programm zurück gegeben. Innerhalb einer Methode können mehrere
return Anweisungen stehen, um alternative Rücksprünge zu realisieren.
Beispiel 6.1 Berechnung der Fakultät:
int fakultaet( int wert ) {
int result = 1;
while( wert > 1 ) {
result *= wert;
--wert;
}
return result;
}
Das Beispiel berechnet für den Eingangswertswert die Fakultät und gibt das
Ergebnis zurück. Um eine Methode aufzurufen, wird sie über den Namen und mit
entsprechenden Argumenten angesprochen. Eine Methode liefert in den meisten
Fällen einen Wert zurück. Das aufrufende Programm (Hauptprogramm) kann
diesen Wert ignorieren oder in einem Ausdruck als rvalue weiter verwenden.
Beispiel 6.2 Verwendung der Methode Fakultät:
6.2. DEFINITION
55
/* gibt den Wert z! aus */
System.out.println( fakultaet(z) );
/* legal aber wenig wirksam */
fakultaet( 2 * 3 );
Bei dem Aufruf wird der Ausdruck in der Klammer ausgewertet und an die Methode übergeben. Selbst wenn man direkt eine Variable als Argument angibt,
wird nur der Wert bzw. Inhalt der Variablen übergeben (call by value). Daher
erfolgen alle Änderungen in der Methode an einem Argument nur an der lokalen
Kopie. Im obigen Beispiel bleibt etwa der Wert der Variablen z unverändert.
Grundsätzlich hat einen Methode keinen Zugriff auf die Variablen im aufrufenden Teil. Aufgrund der strikten Trennung gibt es auch keine Konflikte bei
gleichen Namen für Variablen. Variablen innerhalb einer Methode haben nur die
Lebensdauer eines Methodesaufrufs. Sie werden beim Aufruf angelegt und nach
Ende der Methode wieder gelöscht (automatic variable).
Beispiel 6.3 Methode ohne Parameter und Rückgabewert:
void ausgeben() {
System.out.println( "Methode ausgeben" );
}
In dem Beispiel sind zwei neue Elemente enthalten:
• Die Parameterliste kann leer sein
• Eine Methode ohne Rückgabewert hat den Typ void (engl. leer, nichtig).
Eine solche Methode endet entweder mit einem return ohne Wert oder an der
schließenden Blockklammer.
Beispiel 6.4 Berechnung von ab :
/* Methode zur Berechnung der Potenz a hoch b
* Einschränkung: b >= 0
*/
int power( int base, int exponent ) {
int i;
int result = 1;
for( i=0; i<exponent; i++ ) result *= base;
return result;
}
56
6.3
KAPITEL 6. METHODEN
Überladen von Methoden
Es ist in Java möglich, unter einem Namen mehrere Methoden mit unterschiedlichen Parameterlisten zu definieren. Die Methoden werden damit überladen.
Selbstverständlich sollten die Methoden einen engen Bezug zueinander haben.
Zur Unterscheidung der verschiedenen Varianten dient die Signatur einer Methode. Die Signatur als Kennung besteht aus dem Namen der Methode, Anzahl,
Reihenfolge und Typen ihrer Parameter und – im allgemeinen aber nicht in Java –
dem Typ des Rückgabewertes. Zwei oder mehrere Methoden dürfen den gleichen
Namen haben, sofern sie sich in ihrer Signatur unterscheiden.
Man kann dieses Konzept einsetzen, um die gleiche Funktionalität für unterschiedliche Datentypen bereitzustellen. Beispielsweise kann man eine Methode
power zur Berechnung von Potenzen sowohl für ganzzahlige als auch GleitkommaWerte implementieren:
double power( double x ) { ... }
int power( int i ) { ... }
Abhängig vom Argument wählt der Compiler die passende Methode aus. Gebräuchlich ist dieser Mechanismus auch, um zusätzliche Parameter zu übergeben.
Wir können eine weiter Methode zum Ausdrucken definieren, die einen zusätzlichen String als Überschrift erhält:
Beispiel 6.5 Überladen der Methode ausgeben:
void ausgeben(String titel) {
System.out.println(titel);
ausgeben();
}
Je nach Aufruf – mit oder ohne String-Parameter – wählt der Compiler die richtige Version aus. Die verschiedenen Definitionen müssen nur eindeutig sein, d. h. sie
müssen sich im Typ oder in der Anzahl der Parameter unterscheiden. Eine Version kann eine andere Version aufrufen. Eine andere häufig angewandte Technik
simuliert Standardbelegungen (Default-Werte) für Parameter. Betrachtet man als
Beispiel eine Methode
int beispiel( int i, int j ).
Wenn man für den zweiten Parameter eine Standardbelegung (z. B. den Wert 0)
bereit stellen möchte, kann dies durch eine Methode mit nur einem Parameter
implementiert werden:
int beispiel( int i ) {
// Default Wert 0 für zweiten Parameter
return beispiel( i, 0 );
}
6.4. ÜBERGABE VON FELDERN
57
Dann kann man je nach Bedarf entweder die einfache Version der Methode mit
nur einem Parameter und einer Standardbelegeung der zweiten Größe oder die
allgemeinere Form mit zwei Parametern verwenden.
Mit dieser Technik kann man oft die eigentliche Berechnung oder Bearbeitung
an einer Stelle konzentrieren. Anstatt den gleichen Code in mehreren Varianten
zu kopieren, wird er nur an einer Stelle eingetragen. Verschiedene Varianten rufen
dann immer die gleiche Kernmethode auf. Dies ist sehr viel besser, als den gleichen
Code mehrfach einzutragen. Korrekturen und Erweiterungen brauchen dann nur
an einer Stelle angebracht zu werden.
Übung 6.1 Methode print
Wie viele überladene Versionen von print und println gibt es?
6.4
Übergabe von Feldern
Der Übergabemechanismus call by value, bei dem die Methode mit lokalen Kopien
arbeitet, beschränkt sich auf die einfachen Datentypen. Bei komplexeren Objekten wie Feldern wäre dies nicht sinnvoll. Felder stets vollständig zu kopieren würde
einen hohen Aufwand bedeuten. Außerdem ist es oft gerade gewünscht, per Methodenaufruf den Inhalt eines Feldes zu verändern. So ist es bei einer Methode
zum Sortieren effizienter, das Feld direkt zu bearbeitet. Daher wird bei Feldern
statt der Inhalte ein Verweis (Referenz) an die Methode übergeben. Dementsprechend nennt man dieses Verfahren call by reference. Über diese Referenz können
die einzelnen Zellen angesprochen werden. Formal verwendet man die gleiche
Schreibweise. Der Ablauf ist im folgenden Beispiel dargestellt.
Beispiel 6.6 Methode zum Vertauschen zweier Zellen eines Feldes:
void tausche( int[] feld, int i, int j ) {
int tmp = feld[i];
feld[i] = feld[j];
feld[j] = tmp;
}
...
void testeTausche() {
int[] test = {1,2,3,4,5,6,7,8,9,10};
tausche( test, 0, 1 );
}
Nach der Ausführung der Methode sind die beiden ersten Werte in test vertauscht. Die Methode greift über die Referenz auf das in der aufrufenden Methode
testeTausche eingeführte Feld zu.
Ähnlich ist die Situation bei der Rückgabe eines Feldes. Wird ein Feld in
einer Methode angelegt und dann als Rückgabewert an die aufrufende Methode
übergeben, so bleibt der Speicherplatz mit den Werten erhalten.
58
KAPITEL 6. METHODEN
Beispiel 6.7 Rückgabe eines Feldes.
int[] legeQuadrateAn( int l ) {
int[] feld = new int[l];
for( int i=0; i<l; i++ ) feld[i] = i*i;
return feld;
}
public void testeLegeQuadrateAn() {
int[] q = legeQuadrateAn( 5 );
for( int i=0; i<q.length; i++ ) {
System.out.println( "q["+i+"]: "+ q[i] );
}
}
liefert die Ausgabe
q[0]:
q[1]:
q[2]:
q[3]:
q[4]:
0
1
4
9
16
Der Datentyp Feld wurde hier stellvertretend für alle komplexere Datentypen
verwendet. Die vorgestellten Mechanismen gelten in gleicher Weise für allgemeine Objekte, die wir später behandeln werden. Wichtig ist die Unterscheidung
zwischen den beiden Übergabeverfahren. Einfache Werte werden kopiert und Änderungen an den lokalen Kopien haben keine weiteren Auswirkungen. Bei komplexen Werten wird eine Referenz übergeben, mittels derer die Methode auf die
Inhalt zugreifen und sie dauerhaft verändern kann.
6.5
Rekursion
Wir haben gesehen, dass eine Methode mit lokalen Kopien der Argumenten arbeitet und alle Variablen temporärer Natur sind. Daher ist es möglich, dass eine
Methode sich selbst — oder besser gesagt eine neue Instanz von sich selbst –
wieder aufruft. Das klassische Beispiel für diese Rekursion ist das Berechnen der
Fakultät:
int fakultaetRekursiv( int wert ) {
if( wert == 0 || wert == 1) return 1;
else return fakultaetRekursiv(wert - 1) * wert;
}
6.6. ANMERKUNGEN
59
Wenn das Argument 0 oder 1 ist, wird wegen 0! = 1! = 1 direkt der Wert 1 zurück
gegeben. Ansonsten wird der aktuelle Wert mit der Fakultät des um 1 kleineren
Wertes gemäß n! = n ∗ (n − 1)! berechnet. Beginnend mit einem positiven Wert
wird diese Rekursion wiederholt, bis schließlich der immer wieder verminderte
Wert auf 1 gefallen ist. Rekursive Lösungen sind nicht unbedingt effizienter in
Bezug auf Speicherbedarf oder Rechenzeit. Aber die resultierenden Programm
sind sehr kompakt und oft leichter zu schreiben und zu verstehen.
6.6
Anmerkungen
In den Beispielen und Übungen hatten wir bereits einige Methoden wie etwa
println eingesetzt. In jeder Klasse kann es eine Methode main geben. Diese Methode dient beim direkten Ausführen einer Klasse Startpunkt. Wenn eine Klasse
Beispiel eine Methode main enthält, so wird durch den Befehl
java Beispiel
genau diese Methode aufgerufen. Die vollständige Definition ist
public
void main(String[] args)
d. h. main gibt keinen Wert zurück und hat einen Parameter, dessen Bedeutung
wir noch nicht kennen. Nach der Rückkehr aus main werden eventuell belegte
Ressourcen freigegeben und die Anwendung beendet.
6.7
Übungen
Übung 6.2 Klausurnoten
In den Allgemeine Bestimmungen für Bachelorprüfungsordnungen1 ist in § 6 die
Bewertung der Leistungen geregelt. Verwenden Sie die Formel
N = 4 − 3 ∗ (P − 50)/45 P ≥ 50
zur Berechnung den Note N bei P erzielten Prozent. Implementieren Sie auf
dieser Basis Methoden, um die Note zu berechnen. Die Methoden sollen folgende
Parameter haben
1. ein int-Wert mit den Prozenten als Zahl zwischen 0 und 100
2. zwei int-Werte, die erzielten Punkte und die maximal möglichen Punkte
3. ein double-Wert mit den Prozenten als Zahl zwischen 0 und 1
1
Zu finden über die Webseite der FH und dann unter FH Download » Studium » Modulhandbücher, Studien- und Prüfungsordnungen, Studienganginfo
60
KAPITEL 6. METHODEN
4. ein Feld von double-Werten mit den Prozenten für mehrere Studierenden,
jeweils als Zahl zwischen 0 und 1
Rückgabewert soll jeweils ein double beziehungsweise im letzten Fall ein Feld von
double Werten sein. Die Rundung auf eine Nachkommastelle ist nicht notwendig.
Schreiben Sie die eigentliche Berechnung nur einmal.
Übung 6.3 Suchen Sie alle natürliche Zahlen < 100000, die gleich der Summe
der Fakultäten ihrer einzelnen Ziffern sind:
abc = a! + b! + c!
Beachten Sie die Vereinbarung 0! = 1. Eine Lösung ist
145 = 1! + 4! + 5! = 1 + 24 + 120
Verwenden Sie dazu eine Methode zur Berechnung der Fakultät. Die Ermittlung
der einzelnen Stellen ist in Beispiel 3.10 beschrieben.
Übung 6.4 Implementieren Sie eine rekursive Lösung zur Berechnung der FibonacciZahlen. Wie ist die Rechenzeit bei größer werdenden Zahlen? Wie erklären Sie
diesen Effekt?
Übung 6.5 Türme von Hanoi
Die folgende Klasse löst die Aufgabe rekursiv.
• Wie funktioniert das Programm?
• Wie viele Schritte benötigt man für einen Turm der Höhe n?
public class Hanoi {
int anzahlSchritte;
void lege(int n, String von, String nach, String zwischen) {
if (n>0) {
lege(n-1, von, zwischen, nach);
System.out.println(n + ". Scheibe von " + von + " nach " + nach);
lege(n-1,zwischen, nach, von);
anzahlSchritte++;
}
}
public void bewegen(int maxzahl) {
lege(maxzahl, "Turm 1", "Turm 2", "Turm 3");
System.out.println("-----------------------------------------");
System.out.println(anzahlSchritte + " Schritte");
}
}
Kapitel 7
Algorithmen – vom Problem zum
Programm
7.1
Algorithmen
Mit den bisher besprochenen Elementen der Sprache Java lassen sich bereits eine
Vielzahl von Problemen lösen. Ein solches Beispiel war die Berechnung von Primzahlen. Dem Programm zugrunde liegt ein Verfahren zur Berechnung – „suche
systematisch echte Teiler von n“. In einfachen, überschaubaren Fällen reicht eine
solche Verfahrensidee bereits als Grundlage für das Programm. Einfach bezieht
sich dabei sowohl auf das Problem als auch auf den Lösungsplan. In komplexeren
Fällen ist demgegenüber die Ausarbeitung des Lösungsplans selbst eine aufwendige und schwierige Tätigkeit. Wäre das Problem z. B. „suche alle Primzahlen mit
bis zu 30 Stellen“, dann würde der einfache Ansatz an der mangelnden Rechengenauigkeit und der benötigten Rechenzeit scheitern und wir müssten zunächst
ein geeignetes Verfahren suchen, um solche großen Zahlen zu behandeln. In den
Anwendungsbereichen der Informatik fallen viele komplexe Probleme:
• effiziente Suche nach Schlüsselwörtern im Internet
• Steuerung von Fertigungsabläufen
• Verwaltung eines Internet Shops
• Verschlüsselung zur Datensicherheit
• Vernetzen von sehr vielen Rechnern
• eine email mit Anhängen von Australien nach Grönland bringen
• Grammatikprüfung eines Textes
Auf der anderen Seite verstehen Computer nur sehr einfache Befehle:
61
62
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
• addiere Speicherzelle N zu Akkumulator
• erhöhe Indexregister um 1
• springe zur Programmzeile 3678
Verschiedene Schritte auf dem Weg zu einem Programm, das die gestellte Aufgabe
löst, sind in der folgenden Tabelle zusammen gestellt.
Problem
Problemanalyse
Spezifikation
Design/Entwurf
Programmierung
ausführbares Programm
Test
Programm
Die höheren Programmiersprachen stellen eine erste Abstraktionsebene zur
Verfügung. Der Programmierer braucht sich nicht (kaum) um konkrete Details
des Rechners zu kümmern, sondern kann das Lösungsverfahren mit den Sprachelementen formulieren. Ein solches Programm ist dann im Idealfall auch auf
anderen Rechnern einsetzbar. Auf der anderen Seite gilt es, das Problem zunächst richtig zu erfassen und dann einen oder mehrere alternative Lösungspläne
zu entwerfen. Dabei kann die Problemanalyse bereits ein aufwändiger Prozess
sein. Es gilt Randbedingungen zu klären hinsichtlich der Leistungsfähigkeit (bis
zu welcher Primzahl soll das Programm funktionieren?) als auch allgemeiner u.U.
auch nicht technischer Vorgaben (kompatibel mit Version 1.0, bis Ende nächsten
Monats fertig). Elementare Fragen in der Problemanalyse sind
• Welches Problem ist zu lösen?
• Habe ich die Problemstellung richtig verstanden?
• Ist die Aufgabenstellung vollständig beschrieben?
• Kenne ich alle Randbedingungen?
• Welche Informationen benötige ich gegebenenfalls noch?
Die Problemanalyse sollte so genau wie möglich sein, so dass nachträgliche Änderungen, die sehr kosten- und zeitaufwendig sein können, vermieden werden.
Änderungen an der Problemanalyse während oder nach der Programmierung
sollten unbedingt vermieden werden. Das Ergebnis der Problemanalyse ist eine
Spezifikation. Sie enthält Vorgaben für die Funktionalität des zu entwickelnden
Programms. Man unterscheidet zwei Stufen:
7.1. ALGORITHMEN
63
1. das Lastenheft, beschreibt allgemein was das Programm (System) leiste
sollte
2. das Pflichtenheft, enthält „ausführliche Beschreibung der Leistungen, die
erforderlich sind oder gefordert werden, damit die Ziele des Projekts erreicht
werden“(DIN 69901),
Ausgehend von der Problemanalyse werden in der Regel verschiedene Lösungspläne entworfen und miteinander verglichen. Ein Lösungsplan enthält eine Vorschrift
zur schrittweise Bearbeitung der Aufgabe. Eine solche Vorschrift nennt man Algorithmus.
Jeder Algorithmus besteht somit aus (vielen) einzelnen Schritten. Größere
Aufgaben müssen in der Regel strukturiert und in handhabbare Teilaufgaben
zerlegt werden. Daher kann es notwendig werden, einzelne Schritte durch eine
feinere Betrachtung wiederum als Algorithmus aufzufassen, der selbst auch wieder
aus einzelnen Schritten besteht (Verfeinerung). Dies wird so oft wiederholt, bis
aus dem ursprünglich „groben Algorithmus“ ein für die Lösung des Problems
hinreichend „feiner Algorithmus“ entwickelt worden ist. Diese so genannte topdown Vorgehensweise ist insbesondere bei komplexen Problemen oft erforderlich.
Der Algorithmus stellt jedoch noch kein Programm dar, sondern lediglich eine
präzise Verfahrensvorschrift in „umgangssprachlicher Form“ oder wie wir noch
sehen werden in graphischer Form.
Algorithmen sind nicht auf die Informatik beschränkt sondern begegnen uns
auch im täglichen Leben. Typische Beispiele sind
• Gebrauchsanleitungen
• Anleitungen zum Aufbauen von Möbeln
• Kochrezepte
Betrachten wir folgenden Algorithmus:
1. Wasser einfüllen
2. Filtertüte einlegen
3. Kaffeepulver einfüllen
4. Gerät einschalten
Dies ist offensichtlich ein Algorithmus (untere mehreren) zur Zubereitung von
Kaffee. Charakteristisch ist die Abfolge von einzelnen klar abgegrenzten Schritten.
Allerdings ist für die Ausführung z. B. Anweisung 3 noch nicht detailliert genug.
Eine Verfeinerung könnte sein:
1. Kaffeepulver einfüllen:
64
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
(a) Schrank links unten öffnen
(b) Kaffeedose heraus nehmen
(c) falls Kaffeedose nicht leer
(d) Kaffeedose öffnen
(e) Kaffeelöffel aus Dose nehmen
(f) einen Löffel Kaffee pro Tasse nehmen . . .
Selbst für die einfache Aufgabe ergibt sich bereits ein recht umfangreicher Algorithmus, insbesondere wenn man mögliche Fehlerfälle berücksichtigt (Kaffeedose
leer am Sonntag). Aus dem Algorithmus wird allerdings noch kein Kaffee. Dazu
muss der Algorithmus noch in ein ausführbares Programm umgesetzt werden. So
wird aus dem Punkt „Gerät einschalten“ ein Programmteil „bewege Zeigefinger
mit einer Geschwindigkeit von . . . zur Position x,y,z“, der selbst eine Fülle von
wohl koordinierten Muskelbewegungen bedingt. Ein Beispiel aus unseren Übungsaufgaben ist die Berechnung der Fakultät einer Zahl:
setze ergebnis = 1
setze zähler = 2
solange zähler <= zahl
ergebnis = ergebnis * zähler
zähler = zähler + 1
Mit diesen Überlegungen kommen wir zu der Definition:
Ein Algorithmus ist ein präzise formulierter Plan,
wie man durch Ausführen von einzelnen Arbeitsschritten (Aktionen) schrittweise zur Lösung eines Problems
kommt.
Oft ist ein Algorithmus auf eine ganze Klasse von gleichartigen Problemen anwendbar. Führt der Algorithmus für jede zulässige Eingabe in endlich vielen
Schritten zur Lösung heißt er terminierend. Wenn weiterhin der Ablauf eindeutig bestimmt ist, d. h. es gibt zu jeder Aktion genau eine Folgeaktion, so spricht
man von deterministischen Algorithmen. Dies impliziert, dass für einen festen
Eingangswert stets das gleiche Ergebnis geliefert wird. Nicht-deterministische Algorithmen findet man u.a. im Bereich von Netzwerken, wo der Übertragungsweg
von Datenpaketen mit gleichem Start- und Zielort variieren kann.
Algorithmen bieten eine übersichtliche und leicht nachvollziehbare Darstellung des Lösungsplans unabhängig von den konkreten Realisierungsdetails. Damit sind sie das geeignete Mittel während des Entwurfs des Lösungsplans und
zur Dokumentation oder Erklärung. Es ist sehr viel leichter, einen Lösungsplan
anhand des Algorithmus zu erklären als etwa mit dem fertigen Programm. Hat
man einen klar spezifizierten Algorithmus als Ausgangsbasis, ist die Umsetzung
7.2. FLUSSDIAGRAMM
65
in eine konkrete Programmiersprache stark vereinfacht. Umgekehrt gibt es auch
den „analytische Einsatz“. Man bekommt ein schlecht dokumentiertes Programm
und möchte den Ablauf nachvollziehen. In solchen Fällen ist es hilfreich, sich aus
dem Code wieder die höhere Darstellung zu rekonstruieren.
Zur graphischen Darstellung von Algorithmen gibt es zwei weit verbreitete
Formen: Flussdiagramme und Struktogramme nach Nassi-Shneidermann. Im folgenden werden beide Methoden kurz vorgestellt.
7.2
Flussdiagramm
Flussdiagramme (engl. flowchart) – auch als Programmablaufpläne bezeichnet –
sind, wie der Name sagt, eine graphische Darstellung von Abläufen. Sie werden
auch außerhalb der Programmierung zur Veranschaulichung von Abläufen und
Vorgängen verwendet. Die einzelnen Symbole sind in DIN 66001 standardisiert.
Das grundlegende Element bei Flussdiagrammen ist die Verarbeitung, dargestellt
durch ein Rechteck. Der Fluss der Verarbeitung, d. h. die Abfolge der einzelnen
Aktionen ist durch Pfeile dargestellt.
Anweisung 1
?
Anweisung 2
Bedingungen werden durch eine Raute symbolisiert.
Bedingung
N-
Zweig 2
Y
?
Zweig 1
Mit diesen Grundelementen können Schleifen dargestellt werden. Als Beispiel sei
eine abweisende Schleife angegeben.
66
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
-
N
6
-
Bedingung
J Schleifenkörper
Beispiel 7.1 Die Berechnung der Fakultät einer positiven Zahl.
- Fertig
N
zähler = 2
ergebnis = 1
7.3
- zähler <= zahl
6
J - ergebnis = ergebnis * zähler
zähler = zähler + 1
Struktogramme
Struktogramme entstanden aus dem Bemühen den Prozess der Programmentwicklung zu systematisieren und dadurch übersichtliche und wartbare Programme zu erhalten. Dabei wird das gesamte Programm in einer top-down Vorgehensweise in möglichst voneinander unabhängige Bausteine - die Strukturblöcke - zu
zerlegen. Grundbausteinen von Struktogramme sind Strukturblöcke. Entwickelt
wurde diese Darstellungsform 1972/73 von Isaac Nassi und Ben Shneiderman und
später als DIN 66261 genormt. Für die Darstellung der Blöcke gilt:
• jeder Block ist rechteckig
• jeder Block hat oben einen Eingang und unten einen Ausgang
• Blöcke stehen untereinander oder sind vollständig ineinander geschachtelt
• die Grundtypen sind
– Sequenzblock
– Selektionsblock
7.3. STRUKTOGRAMME
67
– Iterationsblock
Ein Sequenzblock ist ein Rechteck, in dem eine Folge von Schritten (mindestens
einem) steht. Die Schritte werden nacheinander ausgeführt. Mehrere Schritte können zur besseren Übersicht in mehrere aufeinander folgende Sequenzblöcke aufgeteilt werden.
Nassi-Shneiderman — Sequenz-Symbol
Anweisung 1
Anweisung 2
Ein Selektionsblock besteht aus der Abfrage und im einfachsten Fall zwei Alternativen.
Nassi-Shneiderman — If-Verzweigung
J
logische Bedingung
Then-Block
N
Else-Block
Bei dem Iterationsblock unterscheidet man zwei Fälle
• Prüfung am Anfang (Kopfprüfung, abweisende Schleife)
• Prüfung am Ende (Fußprüfung, nicht abweisende Schleife)
Dargestellt werden die Iterationsblöcke durch zwei ineinander geschachtelte Rechtecke. Die logische Bedingung wird dabei vor oder nach dem Schleifenkern eingetragen.
Nassi-Shneiderman — abweisende Schleife
logische Bedingung
Schleifenkern
68
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
Nassi-Shneiderman — nicht abweisende Schleife
Schleifenkern
logische Bedingung
Eine sinnvolle Vorgabe ist, dass Struktogramme nicht länger als eine Seite
werden sollen. Bei einem umfangreichen Algorithmus kann man dies einhalten,
indem man kleinere, in sich abgeschlossene Teile in Modulblöcke zusammen fasst.
In dem übergeordneten Struktogramm schreibt man den Modulnamen sowie eine
kurze Funktionsbeschreibung. Auf diese Weise kann man zunächst den Algorithmus grob entwickeln und wie oben beschrieben dann weiter verfeinern. Graphisch
wird ein Modul wie folgt dargestellt:
Nassi-Shneiderman — Modul
Beschreibung der Modulfunktion (Modulname)
Aus Sicht des übergeordneten Blocks ist bei einem Modul nur die Ein- und Ausgabe sowie die Funktionalität relevant. Die internen Abläufe sind verborgen und
sollen keine Rückwirkungen auf die höhere Ebene haben (black box).
Beispiel 7.2 Als Beispiel wieder die Berechnung der Fakultät einer positiven
Zahl.
Nassi-Shneiderman — Fakultät
Zähler = 2
Ergebnis = 1
Zähler < = Zahl
Ergebnis = Ergebnis * Zähler
Zähler = Zähler + 1
7.4. AKTIVITÄTSDIAGRAMM
69
Abbildung 7.1: Aktivitätsdiagramm zur Berechnung der Fakultät
7.4
Aktivitätsdiagramm
Im Bereich der objektorientierten Programmierung spielen beide Darstellungsformen keine große Rolle mehr. Stattdessen werden die Aktivitätsdiagramm (engl.
activity diagram) aus der Unified Modeling Language (UML) verwendet. Bild 7.1
zeigt die entsprechende Darstellung für die Berechnung der Fakultät.
7.5
Übungen
Übung 7.1 Analysieren Sie folgende Strukturen mit Flussdiagrammen oder Struktogrammen. Gibt es einfachere Lösungen?
a)
if( a > 0 ) {
b = a;
} else {
70
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
if( a == 0 ) {
b = 0;
} else {
b = -a;
}
}
b)
if( a > 0 ) {
if ( a < 0 ) {
a = 1;
} else {
a = 2;
}
}
Übung 7.2 Das folgende Struktogramm beschreibt den Euklidischen1 Algorithmus zur Bestimmung des größten gemeinsamen Teilers (ggT) zweier Zahlen a
und b. Der Einfachheit halber soll die Ungleichung a > b gelten. Setzen Sie das
Struktogramm in ein Java-Programm um.
Nassi-Shneiderman — Euklidischer Algorithmus
Solange b größer als 0
Bestimme den Rest bei Division von a durch b
Kopiere b nach a
Ersetze b durch Rest
Der ggT steht jetzt in a
Übung 7.3 Entwickeln Sie ein Programm, um Messwerte eingeben zu können.
Das Programm erwartet positive Zahlen, die der Anwender nacheinander eingibt.
Nach jeder Eingabe wird der bis jetzt kleinste und größte Wert sowie die Anzahl
der Eingaben ausgegeben. Das Programm wird durch Eingabe einer negativen Zahl
oder 0 beendet. Wie sieht ein entsprechendes Struktogramm aus?
Hinweis: Um die Zahlen einlesen zu können, muss zunächst ein BufferedReader
angelegt werden. Dann kann mit dem unten angegebenen Aufruf ein int-Wert gelesen werden.
1
Euklid, griechischer Mathematiker ca. 325-ca. 270 v. Chr.
7.5. ÜBUNGEN
71
BufferedReader br = new BufferedReader(
new InputStreamReader( System.in )
);
...
int iwert = Integer.parseInt( br.readLine() );
Zusätzlich muss die Methode main mit throws Exception deklariert werden.
Weiterhin wird ganz am Anfang der Datei die Anweisung import java.io.*;
benötigt.
Übung 7.4 Zwei Zahlenspielereien:
• Gegeben sei eine 4-stellige Zahl abcd. Nach Multiplikation mit einer 1stelligen Zahl n größer 1 sollen die Stellen im Produkt in umgekehrter Reihenfolge stehen: abcd ∗ n = dcba Welche Zahlen erfüllen diese Bedingung?
• Suchen Sie natürliche Zahlen (< 100000), die gleich der Summe der Fakultäten ihrer Ziffern sind: abc = a! + b! + c!. Beachten Sie die Vereinbarung
0! = 1.
Benutzen Sie zur Entwicklung der Lösungswege Flussdiagramme oder Struktogramme. Beide Aufgaben sind dem Buch M. Gardner, Mathematische Hexereien,
Ullstein Verlag 1977, entnommen.
72
KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM
Kapitel 8
Objektorientierte Programmierung
8.1
Einleitung
In den 80er Jahren erreichten die immer umfangreicher werdenden Programme
eine Größe, die mit den bekannten Programmiertechniken kaum noch bewältigt werden konnte (Programmierung im Großen). Ausgehend von den Ansätzen
der strukturierten Programmierung suchte man Möglichkeiten, die Komplexität
durch Modularisierung und Kapselung zu reduzieren. Wenn es gelingt, ein komplexes System durch kleinere, zusammenwirkende Teilsysteme zu realisieren, kann
jedes Teilsystem für sich entworfen, implementiert, getestet und optimiert werden. Im Idealfall können außerdem solche Teilsysteme - wenn sie ausreichend
universell ausgelegt sind - in anderen Projekten wieder verwendet werden.
Mit prozeduralen Programmiersprachen wie Fortran, C oder Pascal kann man
bereits eine weitgehende Modularisierung erreichen. Dabei übernehmen Module
(Funktionen) kleinere, in sich abgeschlossene Aufgaben. Funktionen haben klar
definierte Schnittstellen: die Argumentliste und den Rückgabewert. Der interne
Ablauf hat keinen Einfluss auf den Aufruf und die Ausführung hat keine weiteren
äußeren Auswirkungen (black box, Information hiding). Die prozeduralen Programmiersprachen „erzwingen“ allerdings nicht die modulare Programmierung.
Durch den Einsatz globaler Variablen kann der Entwickler modulübergreifende
Abhängigkeiten einbauen.
Neben der funktionalen Gliederung ist auch eine angemessene Darstellung
der Daten wichtig. Mit dem in vielen Sprachen eingeführten Datentyp Verbund
(Struktur) lassen sich zusammen gehörende Daten als Einheit modellieren. Die
einzelnen Komponenten können dabei von unterschiedlichem Typ sein. Aus Verbundvariablen lassen sich größere Datenstrukturen wie Listen oder Bäume aufbauen.
Aufbauend auf diese Ansätze - man kann von objektbasierte Programmierung
sprechen - wurde eine weitergehende Modularisierung entwickelt, die sowohl Abläufe als auch Daten umfasste. Grundlegend ist die durchgängige Anwendung des
73
74
KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG
Konzepts von Objekten in den drei Phasen einer Software-Entwicklung Analyse, Design und Programmierung. Die drei Phasen nennt man dann entsprechend
OOA, OOD und OOP.
Im folgenden wird eine erste kurze Übersicht zur objektorientierte Programmierung gegeben. Anschließend folgt der Einstieg in die Konzepte von JAVA. Wir
werden später zu dem Thema objektorientierte Software-Entwicklung zurück kehren und anhand von Beispielen diese Methodik im Detail studieren.
8.2
Objektorientierte Programmierung (OOP)
„Objektorientierte Programmierung ist eine Implementierungsmethode, bei der
Programme als kooperierende Ansammlung von Objekten angeordnet sind. Jedes
dieser Objekte stellt eine Instanz einer Klasse dar, und alle Klassen sind Elemente
einer Klassenhierarchie, die durch Vererbungsbeziehungen gekennzeichnet ist.“
Aus Grady Booch: Objektorientierte Analyse und Design [Boo94]
Aus diesem Zitat ergeben sich die Kernpunkte:
• Programme basieren auf Objekten
• Objekte sind Instanzen einer Klasse
• Zwischen den Klassen besteht eine Vererbungshierarchie
Klassen bestehen im wesentlichen aus Variablen und Methoden. Die Variablen
definieren, welche Daten in der Klasse oder in einem Klassen-Objekt gespeichert
werden können - die Eigenschaften oder Attribute einer Klasse. Durch die Methoden (Funktionen) ist das Verhalten festgelegt. Das folgende Beispiel zeigt an
Hand einer Klasse Auto, wie durch Variablen und Methoden Eigenschaften und
Verhalten spezifiziert werden.
Beispiel 8.1 Klasse Auto
Daten:
String
int
int
int
int
name;
zulassungsJahr;
kaufPreis;
kilometerStand;
AnzahlUnfaelle;
Methoden:
int
float
restWert();
kilometerProJahr();
8.2. OBJEKTORIENTIERTE PROGRAMMIERUNG (OOP)
void
void
void
75
neuerUnfall();
setzeRestWert( int wert );
erhoeheKilometerStand();
Eine Klasse definiert einen Datentyp. Dann können Variablen dieses Datentyps –
Objekte oder Instanzen – angelegt werden. Jede Instanz füllt die Variablen mit
individuellen Werten. Eine Instanz der Klasse Auto könnte sein:
Auto schumisRennwagen;
mit den Eigenschaften
schumisRennwagen.name
schumisRennwagen.zulassungsJahr
schumisRennwagen.kaufPreis
schumisRennwagen.kilometerStand
schumisRennwagen.AnzahlUnfaelle
=
=
=
=
=
"Ferrari";
0;
300000;
2453;
3;
Die Methoden „hängen“ an dem Objekt. Der Aufruf erfolgt ähnlich wie der Zugriff
auf die Variable mit Objektname - Punktoperator -Methodenname. Ein Unfall
mit Totalschaden als Beispiel hätte die Aufrufe
schumisRennwagen.neuerUnfall();
schumisRennwagen.setzeRestWert( 0 );
zur Folge. Klassen in OOP sind aber nicht einfach nur Datenstrukturen mit angehängten Funktionen. Eine solche Art der Programmierung lässt sich bereits recht
gut mit prozeduralen Sprachen wie C realisieren. Zur Unterscheidung spricht
man in diesem Fall von objektbasierter Entwicklung. Wesentlich für OOP ist der
Charakter der Klassen als Teil einer Hierarchie von Klassen.
Die Klassen-Hierarchie bildet einen Baum vom Allgemeinen zum Speziellen.
Klassen übernehmen Eigenschaften und Verhalten von Oberklassen, spezialisieren
oder ergänzen sie und geben sie an Unterklassen weiter. Ein Beispiel für eine
Klassen-Hierarchie ausgehend von der Klasse Fahrzeug ist:
Fahrzeug
Motorrad
Fahrrad
MountainBike Rennrad
Auto
PKW
Cabrio
LKW
76
KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG
Eine Klasse übernimmt die Eigenschaften und das Verhalten der übergeordneten Klasse und ergänzt sie um spezifische Details. Sprachlich kann man die
Beziehung mit „ist ein“ (engl. „is a“) ausdrücken:
• ein Cabrio ist ein Pkw
• ein Pkw ist ein Auto
• ein Auto ist ein Fahrzeug.
Die „ist ein“ Beziehung gilt nicht nur für die direkt übergeordnete Klasse, sondern
auch für alle anderen übergeordneten Klassen im entsprechenden Zweig:
• ein Cabrio ist ein Auto
In dieser Hierarchie werden Eigenschaften und Verhalten entlang eines Zweiges
vererbt. Im Beispiel kann Cabrio alle Attribute von Pkw übernehmen und um seine speziellen Eigenschaften ergänzen. Beispielsweise kann die Klasse Cabrio eine
zusätzliche Komponente zur Angabe des Dachtyps einführen. Diese Eigenschaft
spielt bei den anderen Fahrzeug-Klassen keine Rolle.
Abhängig von der Sichtweise kann eine Klasse durchaus in verschiedenen Klassenhierarchien eingeordnet werden. So könnte eine Klasse Rennrad sowohl in den
obigen Baum für Fahrzeuge als auch in einen Baum für Sportgeräte eingeordnet
werden. Je nach Anwendung - Verkehrsplanung oder Sportartikelversand - kann
die eine oder die andere Sichtweise dem Problem angemessen sein. Wenn eine
Klasse Attribute und Verhalten aus zwei (oder mehr) Oberklassen erbt, spricht
man von mehrfacher Vererbung (Ein Rennrad ist ein Fahrrad und ist ein Sportgerät).
8.3
Objektorientierte Programmiersprachen
Es gibt eine große Anzahl (>100) von verschiedenen objektbasierten und objektorientierten Programmiersprachen. Weit verbreitet und wohl mit Abstand die
Sprachen mit der derzeit größten praktischen Bedeutung sind JAVA und C++.
Beide sind mit C verwandt und übernehmen elementare Datentypen, Operatoren,
Kontrollstrukturen, u. s. w. von C.
JAVA ist die jüngere von beiden Sprachen. In vieler Hinsicht ist JAVA einfacher als C++. Einige komplexe Sprachelemente aus C++ fehlen, sei es weil die
Designer von JAVA sie nicht für notwendig oder empfehlenswert erachteten oder
weil sie aufgrund des konsequenteren Designs nicht benötigt werden. Gleichzeitig
entfernt sich JAVA stärker von C. Die Sprachelemente von C++ sind eine Obermenge der Sprachelemente von C. Ein C++ Compiler akzeptiert auch C Kode.
In der Tat benutzen viele Programmierer nur einige wenige C++ Merkmale wie
etwa die vereinfache Ausgabe oder Speicherallokation und schreiben Programme
8.4. ÜBUNGEN
Tabelle 8.1: Vergleich JAVA und C++
Funktionalität
JAVA
einfache Vererbung
+
Mehrfachvererbung
(+)
automatische Speicherverwaltung
+
Überladen von Methoden
+
Überladen von Operatoren
Zeiger
Prä-Prozessor
Strukturen
String und boolean als Datentypen
+
feste Größe der elementaren Datentypen
+
77
C++
+
+
+
+
+
+
+
-
ansonsten im alten C Stil. Demgegenüber ist JAVA nicht abwärtskompatibel. Ein
JAVA Übersetzer wird C Kode nicht akzeptieren. Dies bedeutet allerdings nicht
notwendigerweise, dass immer ein sehr hoher Portierungsaufwand von C nach
JAVA besteht. Die Tabelle 8.1 zeigt wichtige Gemeinsamkeiten und Unterschiede
der beiden Programmiersprachen.
Eine neuere Entwicklung ist die Sprache C# (C sharp ausgesprochen) der
Firma Microsoft. Sie enthält Elemente beider Sprache. Inwieweit sie sich mittelund langfristig gegen die anderen Sprachen durchsetzen wird, ist derzeit noch
nicht abzusehen.
8.4
Übungen
Übung 8.1 Ihre Firma entwickelt eine Verwaltungs-Software für Garten- und
Baumärkte. Betrachten Sie beispielhaft die Artikel:
Hammer
Erdbeerpflanzen Zange
Spaten
Torf
Batterien
Holzleim Schrauben
Glühbirnen Elektrokabel
Stichsäge
1. Zeichnen Sie einen Klassenbaum mit mindestens drei Ebenen, d. h. der Ausgangsklasse Artikel, mindestens einer Zwischenebene und dann den Klassen für die einzelnen Artikel. Wählen Sie für die Zwischenebene(n) eine
sinnvolle Einteilung.
2. Gibt es Artikel, für die mehrere Zuordnungen sinnvoll wären?
3. Nennen Sie ein Beispiel für eine Eigenschaft, die sinnvoll bereits in der
Basisklasse Artikel definiert und von dort an alle anderen Klassen vererbt
werden kann.
78
KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG
4. Definieren Sie für einen der Artikel eine Java-Klasse mit mindestens 4
Eigenschaften (Variablen passenden Typs) und einem Konstruktor mit entsprechenden 4 Parameter.
Kapitel 9
Klassen
9.1
Einleitung
Klassen sind das wesentliche Element von Java. Selbst einfache Programme wie
unser einführendes Beispiel benötigen eine Klasse – auch wenn bei der Programmierung Objektorientierung keinerlei Rolle spielt. In diesem Kapitel wird das
Konzept von Klassen im Detail besprochen. Zunächst noch zur Terminologie:
Klassen beschreiben die Eigenschaften und das Verhalten von Objekten. So
definierten wir im Beispiel der Klasse Auto Eigenschaften wie Kaufpreis oder Kilometerstand sowie Methoden um beispielsweise diese Eigenschaften abzufragen
oder zu verändern. Ein konkretes Auto ist dann ein Objekt oder eine Instanz dieser Klasse. Verschiedene Autos unterscheiden sich in ihren Eigenschaften, stellen
aber alle die gleichen Methoden zur Verfügung.
9.2
Klassendefinition
In unserer ersten Anwendung verwendeten wir die Konstruktion
public class Ue1 {
...
}
um eine Klasse zu definieren. Im einfachsten Fall besteht die Definition aus dem
Schlüsselwort class und dem Namen der Klasse. Häufig steht jede Klasse in einer eigenen Datei, die wiederum den gleichen Namen und die Erweiterung .java
haben muss. Wir werden später auch Fälle untersuchen, in denen mehrere Klassen in einer Quelldatei stehen. Aber spätestens beim Kompilieren werden die
Klassen in individuelle Dateien – dann mit der Endung .class – aufgeteilt. Die
Definition der Klasse kann um Attribute (z. B. public zur Sichtbarkeit) erweitert werden. Weiterhin können nach dem Klassennamen Angaben zur Vererbung
folgen (public class HalloFbApplet extends JApplet). In dem Block nach
79
80
KAPITEL 9. KLASSEN
der Klassendefinition folgen die Daten und Methoden zu dieser Klasse. Für das
Beispiel Auto kann man schreiben:
public class Auto {
String name;
int
zulassungsJahr;
int
kaufPreis;
int
kilometerStand = 0;
int
anzahlUnfaelle = 0;
public void erhoeheKilometerStand( int neueKm ) {
kilometerStand += neueKm;
}
public void print() {
System.out.println(name + ":");
System.out.println("Kilometerstand: "
+ kilometerStand );
}
}
Mit diesem Code sind 5 Variablen sowie 2 Methoden definiert. Wir haben damit
einige Eigenschaften der Auto-Objekte festgelegt. Die Variablen können explizit
mit Werten initialisiert werden. Standardmäßig initialisiert Java die Elemente
mit dem Wert 0 beziehungsweise dem Äquivalent des jeweiligen Datentyps. Mit
der Methode erhoeheKilometerStand() kann eine der Eigenschaften verändert
werden. Die Methode print() gibt die Daten eines Autos aus. Die Methoden
einer Klasse können deren Variablen ohne weiter Angabe verwenden. Eine Variable ohne explizite Zuordnung wird automatisch der eigenen Klasse zugeordnet.
Objekte dieser Klasse können unter Angabe des Klassennamens angelegt werden.
Zunächst wird das Objekt deklariert:
Auto meinRenault;
Mit dieser Anweisung wird die Referenz auf ein Objekt der Klasse Auto angelegt. Es wird noch kein Speicher reserviert. Um tatsächlich auch ein Objekt zu
erzeugen, muss der new-Operator angewendet werden:
meinRenault = new Auto();
Jetzt ist Speicher reserviert worden und die Referenz deutet auf diesen neuen
Speicherblock. Man kann Deklaration und Initialisierung zusammen fassen und
verkürzt schreiben:
Auto meinRenault = new Auto();
9.2. KLASSENDEFINITION
81
In beiden Fällen kann danach meinRenault als ein Objekt der Klasse Auto eingesetzt werden. Zugriff auf die Variablen und die Methoden erfolgt in der Syntax
meinRenault Punktoperator Variable oder Methode
Für Variablen erlaubt diese Syntax sowohl lesenden als auch schreibenden Zugriff.
Die Verwendung illustriert folgendes Beispiel:
public void testAuto() {
Auto meinRenault = new Auto();
meinRenault.kaufPreis = 10000;
meinRenault.print();
}
Die Klasse Auto kann wie ein Variablentyp benutzt werden. Man kann beliebig
viele Objekte dieser Klasse erzeugen. Jedes Objekt hat seinen eigenen Speicherbereich. Die Objekte können zu Feldern zusammen gefasst werden, an Methoden
übergeben werden, etc.
9.2.1
Zugriff auf Attribute
Die Möglichkeit des direkten Zugriff auf die Variable kaufPreis über die Referenz
auf das Objekt in der Art meinRenault.kaufPreis sollte nur in einfachen und
sehr überschaubaren Fällen verwendet werden. Besser ist es, in jeder Klasse zu
überlegen, welche Eigenschaften von außen sichtbar oder veränderbar sein sollen.
Die Klasse wird damit gegen unbeabsichtigte Eingriffe abgesichert. Gleichzeitig
wird die Verwendung vereinfacht, da nur noch die dafür notwendigen Informationen sichtbar sind. Dementsprechend wird dieses Prinzip als Kapselung oder
Information hiding bezeichnet. In Java kann man Variablen durch den Modifizierer private schützen. Damit ist die Variable außerhalb der Klasse nicht mehr
sichtbar. Sofern man Zugriff gewähren will, kann dies über entsprechende Zugriffsmethoden geschehen. In Java ist es üblich, die Methoden mit get oder set
beginnen zu lassen und dann den Namen der Variablen anzufügen. Dementsprechende würde man für den Kaufpreis das Methodenpaar
public void setKaufPreis( int kaufPreis ) {
this.kaufPreis = kaufPreis;
}
public int getKaufPreis() {
return kaufPreis;
}
einführen. Viele Tools unterstützen diese Namenskonvention. Die Methoden werden dann als Getter und Setter bezeichnet.
82
KAPITEL 9. KLASSEN
Das Beispiel zeigt weiterhin eine allgemein üblich Form der Namensgebung
für Parameter. Der Parameter kilometerStand trägt den gleichen Namen wie
die Instanzvariable. Die lokale Variable in der Methode überdeckt dann die Instanzvariable. Über die Referenz this, die auf das Objekt selbst verweist, kann
wiederum auf die Instanzvariable zugegriffen werden. Diese Schreibweise zeigt
deutlich, dass eine Initialisierung ausgeführt wird und man braucht sich keine
zusätzlichen Namen für die Parameter auszudenken.
9.3
Instanz- und Klassenvariablen und Methoden
Wenn man Methoden wie oben beschrieben definiert, dann können sie nur über
ein Objekt der Klasse angesprochen werden. Es gibt keine Möglichkeit, die Methode print() der Klasse Auto aufzurufen, ohne vorher ein Objekt erzeugt zu
haben. Erst dann ist die Methode über die Referenz auf das Objekt zugänglich
(und sinnvoll). In vielen Fällen haben Methoden jedoch keinen Bezug zu einer
Instanz. Dies gilt insbesondere für die funktionale Verwendung. Ein typisches Beispiel sind die mathematischen Funktionen wie sin() oder cos(), wo die Form
sin(x) die Bedeutung gut verdeutlicht. Java ermöglicht es daher auch, Methoden
für eine Klasse unabhängig von einer konkreten Instanz zu definieren. Entsprechend nennt man derartige Methoden Klassenmethoden im Gegensatz zu den
Instanzmethoden. Klassenmethoden werden durch das Attribut static gekennzeichnet. Für die Klasse Auto führen wir eine Klassenmethode printVersion ein,
die die aktuelle Versionsnummer der Klasse ausgibt:
class Auto {
static void printVersion() {
System.out.println("Klasse Auto, Version 1.0");
}
...
public static void main(String[] args) {
Auto.printVersion();
Die Klassenmethoden werden in Verbindung mit dem Klassennamen aufgerufen.
Es ist nicht notwendig, vorher eine Instanz der Klasse anzulegen. Entsprechend
werden die Methoden der Klasse Math in der Form Math.sin(x) benutzt. Neben
Klassenmethoden gibt es auch Klassenvariablen, d. h. Variablen die unabhängig
von Instanzen existieren. Sie werden nur einmal angelegt und alle Instanzen haben
nur einen gemeinsamen Satz von Klassenvariablen. Sie bestehen während der
gesamten Laufzeit eines Programms. In ihrer Anwendung sind sie mit globalen
Variablen in anderen Programmiersprachen vergleichbar. Klassenvariablen sind
sinnvoll, um übergeordnete Eigenschaften einer Klasse darzustellen. In der Auto
Klasse kann man so z. B. einen Zähler für die Anzahl der Fahrzeuge anlegen:
9.3. INSTANZ- UND KLASSENVARIABLEN UND METHODEN
83
Klasse
Klassenvariablen
Klassenmethoden
Objekt
Objekt
Variablen
Objekt
Variablen
Methoden
Variablen
Methoden
Methoden
Abbildung 9.1: Objekt- und Klassenvariablen und Methoden
class Auto {
...
static int anzahlAutos = 0;
...
public static void main(String[] args) {
Auto meinRenault = new Auto();
++Auto.anzahlAutos;
Eine andere Anwendung sind Konstanten. Eine nützliche Konstante für die Auto
Klasse könnte die TÜV-Periode sein. Eine entsprechende Konstante wird wie eine
normale (Klassen-) Variable definiert und erhält zusätzlich das Attribut final
um zu zeigen, dass der Wert nicht mehr geändert werden kann.
static final int TÜV_PERIODE = 2;
Die beiden Konstanten in der Klasse Math sind Math.PI und Math.E. Der Zusammenhang zwischen Methoden und Variablen einer Klasse und ihrer Instanzen
ist in Bild 9.1 graphisch dargestellt.
Hinweis: Klassenmethoden und Klassenvariablen können auch über den Namen einer Instanz angesprochen werden. Da dies aber nicht dem logischen Zusammenhang entspricht, sollte diese Form nicht ohne gute Gründe verwendet
werden.
84
KAPITEL 9. KLASSEN
9.4
Konstruktoren
Der Erzeugungsoperator new legt ein neues Objekt der entsprechenden Klasse
an. Damit wird Speicher reserviert und die Elemente werden initialisiert. Außerdem wird der Konstruktor der übergeordneten Klasse aufgerufen. Weitergehende
Initialisierungen kann man mit speziellen Methoden - den Konstruktoren - ausführen. Konstruktoren haben die Form von normalen Methoden, wobei der Klassenname als Methodenname übernommen wird. Wenn kein eigener Konstruktor
angegeben ist, führt Java einen parameterlosen Standard-Konstruktor aus. Wir
erweitern die Klasse Auto um einen Konstruktor, der den Namen des Autos als
Parameter erhält:
Auto( String text) {
name = text;
}
Der Konstruktor hat keinen Rückgabewert - auch nicht void. Mit dem neuen
Konstruktor können Auto-Objekte mit Angabe des Namens in der Form
Auto meinBMW = new Auto( "BMW");
erzeugt werden. Eine Klasse kann mehrere Konstruktoren haben, die sich dann
in der Parameterliste unterscheiden müssen. Sehr gebräuchlich sind verschiedene Varianten um mehr oder weniger viele der Elemente zu initialisieren. In der
Beispielklasse wären dies Konstruktoren in der Art
Auto( String text) { ... }
Auto( String text, int kaufPreis) { ... }
Auto( String text, int kaufPreis, int kilometerStand) { ... }
Um nicht den gleichen Code mehrfach schreiben zu müssen, ist es sinnvoll die
Konstruktoren zu verketten. Konstruktoren können sich gegenseitig aufrufen. Der
Aufruf muss dabei jeweils am Anfang der Methode stehen. Der Aufruf erfolgt über
den speziellen Namen this:
Auto( String text, int kaufPreis, int kilometerStand) {
// anderen Konstruktor aufrufen
this(text, kaufPreis);
// Instanzvariable initialisieren
this.kilometerStand = kilometerStand;
}
Hier ruft der speziellere Konstruktor zunächst einen einfacheren Konstruktor auf
und führt dann seine eigene Initialisierung aus. In dieser Art und Weise kann man
speziellere Konstruktoren auf einfacheren aufbauen und vermeidet ein mehrfaches
Schreiben (und später mehrfaches Ändern) von identischem Code.
9.5. DESTRUKTOREN
85
Sobald in einer Klasse irgendein eigener Konstruktor definiert wird, muss man
auch den parameterlosen Konstruktor implementieren. Im einfachsten Fall kann
dieser Konstruktor leer sein:
Auto() { }
Man kann allerdings dort auch Anweisungen eintragen, die immer ausgeführt
werden sollen:
Auto() {
++anzahlAutos;
}
Wenn dann jeder andere Konstruktor diesen parameterlosen Konstruktor entweder direkt als this() oder indirekt über einen anderen Konstruktor aufruft, stellt
anzahlAutos einen zuverlässigen Zähler für die Anzahl der angelegten Objekte
dar.
9.5
Destruktoren
Das Gegenstück zu den Konstruktoren sind die Destruktoren. Sie werden aufgerufen, wenn ein Objekt nicht mehr benötigt wird. Beispielsweise werden lokale
Objekte am Ende der Methode wieder gelöscht, sofern nicht die Methode eine
Referenz auf dieses Objekt zurück gibt.
Am Ende der Lebensdauer eines Objektes sollten die gebundenen Ressourcen
- in erster Linie der Speicher - wieder frei gegeben werden. In C und C++ dient
dazu die Funktion free. Bei großen Anwendungen ist es sehr wichtig, dass der
Speicher wieder vollständig freigegeben wird. Ansonsten sammelt sich im Laufe
der Zeit unbenutzter und unbrauchbarer Speicher an. Handelt es sich um größere
Speichermengen oder läuft das Programm sehr lange, kann daraus eine Belastung
für das gesamte System werden.
In Java spielen Destruktoren keine große Rolle. In Java ist eine automatische
Speicherverwaltung integriert. Sobald Java feststellt, dass ein Objekt nicht mehr
benutzt wird, wird dieses Objekt zum Löschen markiert. Ein spezieller Mechanismus – garbage collection (engl. Müllsammlung) – sorgt dann dafür, dass der
Speicher wieder frei gegeben wird. Dabei wird auch ein Destruktor – die Methode void finalize( void ) – aufgerufen. Dadurch braucht sich der Anwender
in den allermeisten Fällen nicht um die Speicherverwaltung zu kümmern. Nicht
mehr benötigte Objekte werden durch den im Hintergrund tätigen garbage collector weggeräumt. Allerdings gibt es keine Garantien, wie schnell der garbage
collector arbeitet oder ob er überhaupt aktiv ist. Man sollte sich daher bei speicherkritischen Anwendungen nicht auf den Automatismus verlassen, sondern den
Speicher gezielt entfernen lassen.
86
KAPITEL 9. KLASSEN
Klassen
Methoden
Variablen
Konstanten
9.6
Tabelle 9.1: Namenskonventionen in Java
Regeln
Beispiele
Klassennamen sollten aus Hauptwörtern Auto,
Student,
gebildet werden, der erste Buchstabe Image, ImageView
ist groß geschrieben. Namensbestandteile
sind durch Großschreibung getrennt (mixed case).
Um die Aktivität zu verdeutlichen soll- print, erhöheKm,
ten Methodennamen aus Verben beste- Ausnahmen: sin,
hen. Der erste Buchstabe ist klein ge- cos
schrieben, ansonsten gilt wieder „mixed
case“.
Variablennamen sollten kurz und präg- kaufPreis,
nant sein und mit Kleinbuchstaben begin- restWert
nen. Für Variablen mit sehr kurzer Lebensdauer können die einfachen Namen
wie i, j, x benutzt werden.
Nur Großbuchstaben, getrennt durch Un- TÜV_PERIODE
terstreichestrich
Namenskonventionen
Im Prinzip können die Namen für Klassen, Methoden, Variablen, etc. frei gewählt
werden. Die Lesbarkeit kann allerdings durch Verwendung einer festen Konvention stark verbessert werden. Beispielsweise kann man für jeden Typ von Bezeichner eine eindeutige Schreibweise vereinbaren. In Tabelle 9.1 sind die wichtigsten
Regeln gemäß den Vorschlägen von Sun zusammen gestellt.
9.7
Übungen
Übung 9.1 Klasse Auto:
Entwickeln Sie nach den Vorlagen im Skript eine Klasse Auto. Welche Variablen
und Methoden benötigen Sie? Implementieren Sie die Klasse und testen Sie verschiedene Konstruktoren und andere Methoden. Einige Vorschläge für Methoden:
• berechneKmProJahr
• berechneZeitwert
• zeigeZeitwertÜberJahre
Übung 9.2 FH-Verwaltung:
Sie wollen ein Programm zur Verwaltung der FH entwickeln. Entwerfen Sie eine
9.7. ÜBUNGEN
87
Klassenhierarchie für die verschiedenen Angehörigen:
• Studenten
• Professoren
• Mitarbeiter
Implementieren Sie zwei Klassen:
1. Student
2. FH
In der Klasse Student sollen passende Variablen für Eigenschaften wie Name,
Studienfach und Semsterzahl enthalten sein. Weiterhin soll es mindestens eine
Methode print() geben, um die Eigenschaften einer Instanz der Klasse auszugeben. Verwenden Sie dann die Klasse FH um einige Objekte der Klasse Student zu
erzeugen, mit Werten zu belegen und dann über die Methode print() auszugeben.
88
KAPITEL 9. KLASSEN
Kapitel 10
Objekte und Klassen
10.1
Einleitung
In diesem Kapitel wird die Verwendung von Objekten und Klassen anhand von
konkreten Beispielen näher untersucht. Insbesondere wird dabei auf Fragen der
Vererbung eingegangen. Als Beispiel dient dabei eine Autovermietung oder allgemeiner eine Vermietung von Fahrzeugen. Der Schwerpunkt liegt allerdings auf
der Darstellung der Sprachelemente, weniger auf dem tatsächlichen Nutzen der
Klassen. Als Entwicklungsumgebung dient BlueJ.
10.2
Basisklassen
Wir beginnen die Entwicklung mit zwei Klassen:
• Fahrzeug – die Basisklasse für verschiedene Typen von Fahrzeugen
• Vermietung – die Klasse für die Modellierung der Vermietung
Nach dem Anlegen hat die Klasse Fahrzeug folgendes Aussehen:
/**
* Klasse fuer Fahrzeuge
*
* @author Euler
* @version 0.1 Nov. 2008
*/
public class Fahrzeug
{
// Definieren Sie ab hier die Klassenvariablen für Fahrzeug
// Definieren Sie ab hier die Objektvariablen für Fahrzeug
89
90
KAPITEL 10. OBJEKTE UND KLASSEN
// Definieren Sie ab hier die KOnstruktoren für Fahrzeug
/**
* Konstruktor für Objekte der Klasse Fahrzeug
*/
public Fahrzeug()
{
// Objektvariable initialisieren
}
}
Die Klasse wird direkt von der allgemeinen Basisklasse Object abgeleitet. Explizit
könnte man schreiben
public class Fahrzeug extends Object
Mit dem Schlüsselwort extends wird die Klasse angegeben, die erweitert wird.
Fehlt die Angabe, wird automatisch Object als Oberklasse genommen. Die neue
Klasse erbt damit alle Variablen und Methoden von Object. Ein Blick in die
Dokumentation zeigt, dass Object keine Variablen aber einige Methoden hat.
So gibt die Methode toString() einen beschreibenden Text zu dem jeweiligen
Objekt zurück.
Bereits jetzt können wir ein Fahrzeug-Objekt erzeugen und diese Methoden
aufrufen. Ohne BlueJ würde man für solche einfachen Tests die Methode main
verwenden. Eigentlich benötigen wir diese Methode nicht, da die Verwaltung später in der Klasse Vermietung implementiert wird. Aber in Java kann jede Klasse
eine eigene Methode main haben. Durch den Aufruf
java Klassenname
wird die main Methode der genannten Klasse aufgerufen. Es stört nicht, wenn
andere Klassen in dem Projekt ebenfalls eine main Methode enthalten. Wir verwenden in BlueJ statt main die Methode testeFahrzeug() als Startpunkt. In
dem folgende Code wird
• ein Fahrzeug-Objekt angelegt
• die Methode toString benutzt, um die Textrepräsentation auszugeben
• mit der Methode getClass ein zugehöriges Klassenobjekt erzeugt und dessen Methode getName zur Ausgabe des Klassennamens aufgerufen
public void testeFahrzeug() {
Fahrzeug test = new Fahrzeug();
System.out.println("test: " + test.toString() );
System.out.println("test ist ein Objekt der Klasse <"
+ test.getClass().getName() + ">");
}
10.3. KLASSE FAHRZEUG
91
Der Aufruf der Methode liefert folgende Ausgabe:
test: Fahrzeug@162e295
test ist ein Objekt der Klasse <Fahrzeug>
Der Wert hinter dem @-Zeichen ist eine eindeutige Kennung für das Objekt, der so
genannte Hash-Code. Erzeugt man ein weiteres Objekt, wird dieses einen anderen
Wert haben.
10.3
Klasse Fahrzeug
Ausgehend von dem Grundgerüst können erste Erweiterungen an der Klasse
Fahrzeug vorgenommen werden. Sinnvoll ist es, übergreifende Variablen und Methoden – d. h. solche die für alle Fahrzeuge und die daraus abgeleiteten Objekte
gelten – hier zu definieren. Ohne spezielle Attribute erbt jede abgeleitete Klasse
alle diese Variablen und Methoden. Im Beispiel sind dies
• drei Variablen name, kaufPreis, kaufJahr
• ein Konstruktor bei dem diese drei Variablen durch Parameter gesetzt werden
• eine Methode print zur Ausgabe
Die Erweiterungen werden in teste() ausprobiert. Dort werden zwei Objekte
erzeugt: eines mit dem parameterlosen Konstruktor und das zweite mit dem neuen
Konstruktor. Anschließend werden beide Objekte ausgegeben.
public class Fahrzeug extends Object
{
// Definieren Sie ab hier die Klassenvariablen für Fahrzeug
// Definieren Sie ab hier die Objektvariablen für Fahrzeug
String name;
int
kaufPreis;
int
kaufJahr;
// Definieren Sie ab
public Fahrzeug()
public Fahrzeug(
String name,
this.name
=
this.kaufPreis =
this.kaufJahr =
}
hier die KOnstruktoren für Fahrzeug
{
}
int kaufPreis, int kaufJahr ) {
name;
kaufPreis;
kaufJahr;
92
KAPITEL 10. OBJEKTE UND KLASSEN
void print() {
System.out.println(name + ":");
System.out.print("gekauft im Jahr " + kaufJahr );
System.out.print(" für " + kaufPreis + " Euro" );
System.out.println();
}
public void testeFahrzeug() {
Fahrzeug test = new Fahrzeug();
System.out.println("test: " + test.toString() );
System.out.println("test ist ein Objekt der Klasse <"
+ test.getClass().getName() + ">");
}
public void teste() {
Fahrzeug test1, test2;
test1 = new Fahrzeug( );
test2 = new Fahrzeug( "Alpha", 24999, 2000);
System.out.println("test1: ");
test1.print();
System.out.println("test2: ");
test2.print();
}
}
Ausgabe:
test1:
null:
gekauft im Jahr 0 für 0 Euro
test2:
Alpha:
gekauft im Jahr 2000 für 24999 Euro
In dem parameterlosen Konstruktor werden die Variablen auf den Wert 0 bzw.
null gesetzt.
10.4
static Elemente
Neben den Instanzvariablen und –methoden können auch entsprechende Elemente
für die Klasse definiert wird. Dem Beispiel aus dem Kapitel Klassen folgend, fügen
wir einen Zähler für die Anzahl der Fahrzeug-Objekte ein:
10.4. STATIC ELEMENTE
93
/**
* Klasse fuer Fahrzeuge
*
* @author Euler
* @version 0.1 Nov. 2008
*/
public class Fahrzeug extends Object
{
// Definieren Sie ab hier die Klassenvariablen für Fahrzeug
static int anzahl;
// Definieren Sie ab hier die Objektvariablen für Fahrzeug
String name;
int
kaufPreis;
int
kaufJahr;
// Definieren Sie ab
public Fahrzeug()
++anzahl;
}
public Fahrzeug(
String name,
this();
this.name
=
this.kaufPreis =
this.kaufJahr =
}
hier die KOnstruktoren für Fahrzeug
{
int kaufPreis, int kaufJahr ) {
name;
kaufPreis;
kaufJahr;
public static void printAnzahl() {
System.out.println("Anzahl Fahrzeuge: " + anzahl);
}
void print() {
System.out.println(name + ":");
System.out.print("gekauft im Jahr " + kaufJahr );
System.out.print(" für " + kaufPreis + " Euro" );
System.out.println();
}
public void testeFahrzeug() {
Fahrzeug test = new Fahrzeug();
System.out.println("test: " + test.toString() );
System.out.println("test ist ein Objekt der Klasse <"
94
KAPITEL 10. OBJEKTE UND KLASSEN
+ test.getClass().getName() + ">");
}
public void teste() {
Fahrzeug test1, test2;
test1 = new Fahrzeug( );
test2 = new Fahrzeug( "Alpha", 24999, 2000);
System.out.println("test1: ");
test1.print();
System.out.println("test2: ");
test2.print();
printAnzahl();
}
}
Dazu werden folgende Erweiterungen eingebaut:
• eine static Variable für den Zähler
• eine static Methode um den Zähler auszugeben
• im parameterlosen Konstruktor die Anweisung, um den Zähler zu erhöhen
• im zweiten Konstruktor der Aufruf des ersten Konstruktors
Man könnte auch im zweiten Konstruktor direkt den Zähler inkrementieren. Statt
dessen wird der erste Konstruktor aufgerufen, in dem der Zähler erhöht wird. Die
gewählte Lösung hat den Vorteil, dass nur an einer Stelle Kode für die Erhöhung
des Zähler eingetragen wird. Bei eventuellen späteren Änderungen braucht daher
nur eine einzige Stelle bearbeitet zu werden.
10.5
10.5.1
Konstruktoren und die Klasse Auto
Die Klasse Auto
Als erste abgeleitete Klasse betrachten wir eine Klasse Auto. Die Klasse wird aus
Fahrzeug abgeleitet (Auto ist ein Fahrzeug). Sie erbt damit Eigenschaften und
Verhalten. Spezifische Eigenschaften eines Autos, die nicht jedes Fahrzeug hat,
werden dann in diese Klasse eingebaut.
In BlueJ wird eine neue Klasse Auto angelegt. Dann kann man durch einen
durchgezogenen Pfeil → die Vererbungsbeziehung einfügen. Dann wird die Klassendefinition zu
public class Auto extends Fahrzeug { ...
10.5. KONSTRUKTOREN UND DIE KLASSE AUTO
Abbildung 10.1: Klasse Auto als Spezialisierung von Fahrzeug
95
96
KAPITEL 10. OBJEKTE UND KLASSEN
Die Klasse selbst steht in einer eigenen Datei Auto.java. Bild 10.1 zeigt die Darstellung in BlueJ. Als zusätzliche Eigenschaft wird der Kilometerstand eingeführt:
public class Auto extends Fahrzeug {
int kilometer;
...
10.5.2
Verkettung von Konstruktoren
Die abgeleitete Klasse erbt alle Methoden außer den Konstruktoren. In Java werden Konstruktoren nicht vererbt. Vielmehr wird durch Verkettung der Konstruktoren garantiert, dass auch die Konstruktoren der Elternklasse aufgerufen werden.
Dies kann entweder explizit durch eine entsprechende Anweisung erfolgen oder
wird implizit durch den Compiler eingefügt. Ein expliziter Aufruf erfolgt über die
Methode super als erste Anweisung in einem Konstruktor. Dieser Aufruf führt
den Konstruktor der Elternklasse aus. Für den Konstruktor mit drei Parametern
kann man schreiben
public Auto(String name, int kaufPreis, int kaufJahr) {
super( name, kaufPreis, kaufJahr );
}
Der Konstruktor enthält vorerst nur den Aufruf des Konstruktors der Klasse
Fahrzeug. Fehlt ein solcher Aufruf, so setzt der Compiler automatisch den Aufruf
des parameterlosen Konstruktors super() ein. Damit lässt sich schreiben:
public class Auto extends Fahrzeug {
int kilometer;
/** Creates new Auto */
public Auto() {
}
public Auto(String name, int kaufPreis, int kaufJahr) {
super( name, kaufPreis, kaufJahr );
}
}
In dieser Form werden bei beiden Konstruktoren die entsprechenden Konstruktoren der Elternklasse aufgerufen -– einmal implizit und einmal explizit. Diese
Verkettung setzt sich weiter fort. Auch die Konstruktoren in Fahrzeug rufen die
entsprechenden Konstruktoren der Elternklasse – in diesem Fall Object – auf.
Zusätzlich können wie gesehen auch Konstruktoren untereinander verkettet sein.
Im Beispiel läuft beim Aufruf
10.6. DIE KLASSE VERLEIH
97
Auto auto2 = new Auto( "Golf", 19999, 2002);
folgende Kette von Konstruktoren ab:
super
this
super
Auto( String, int, int)
Fahrzeug( String, int, int)
Fahrzeug()
Object()
Die Kette wird von „oben nach unten“ abgearbeitet. Der als letztes aufgerufene
Konstruktor Object() wird als erstes ausgeführt. Danach folgt Fahrzeug() und
so weiter. Ausgehend vom allgemeinen erfolgt eine zunehmende Verfeinerung oder
Spezialisierung. Nach dem gleichen Muster kann man einen weiteren Konstruktor,
der auch die zusätzliche Variable kilometer beinhaltet, einführen:
public Auto(String name, int kaufPreis, int kaufJahr, int kilometer) {
super( name, kaufPreis, kaufJahr );
this.kilometer = kilometer;
}
Übung 10.1 Ist der folgende Code korrekt und wenn ja, welche Ausgabe resultiert?
Fahrzeug f = new Fahrzeug( "Golf", 19999, 2002);
Auto
a = new Auto( "Sharan", 30000, 1999);
Auto.printAnzahl();
10.6
Die Klasse Verleih
Im nächsten Schritt werden in der Klasse Verleih einige Basisfunktionalitäten
eingebaut. Dazu werden
• ein Feld von Autos
• eine Methode erzeugeAutos(), um eine Liste von Autos zu erzeugen
• eine Methode print() zur Anzeige der Autos
eingefügt.
public class Vermietung
{
// Definieren Sie ab hier die Objektvariablen für Vermietung
Auto[] autos;
// Definieren Sie ab hier die Konstruktoren für Vermietung
98
KAPITEL 10. OBJEKTE UND KLASSEN
public Vermietung()
{
erzeugeAutos();
}
void erzeugeAutos(){
autos = new Auto[3];
autos[0] = new Auto("Sharan",
autos[1] = new Auto("Golf",
autos[2] = new Auto("Jaguar",
}
29999, 2000, 45000);
19999, 2001, 36777);
55000, 1998, 15666);
public void print() {
for( Auto auto : autos ) {
auto.print();
}
}
}
Erzeugt man ein neues Vermietung-Objekt und führt dessen Methode print()
aus, so erhält man die Ausgabe
Sharan:
gekauft im Jahr 2000 für 29999 Euro
Golf:
gekauft im Jahr 2001 für 19999 Euro
Jaguar:
gekauft im Jahr 1998 für 55000 Euro
10.7
Überlagerung von Methoden
Da bisher nur die Methode print aus Fahrzeug benutzt wird, fehlt noch die
Ausgabe des Kilometerstands. Daher ist es notwendig, in Auto die Methode print
zu überlagern. Dies wird durch die Definition einer Methode des gleichen Namens
in Auto erreicht:
void print() {
System.out.println(name + ":");
System.out.print("gekauft im Jahr " + kaufJahr );
System.out.print(" für " + kaufPreis + " Euro" );
System.out.println();
System.out.println("Kilometerstand: " + kilometer);
}
10.7. ÜBERLAGERUNG VON METHODEN
99
Wenn dann bei einem Auto-Objekt die Methode print aufgerufen wird, wählt
der Compiler die Methode aus der eigenen Klasse. Nur wenn in dieser Klasse die
Methode nicht implementiert ist, sucht der Compiler in den Elternklassen nach
einer Methode mit diesem Namen. Die Suche verläuft von unten nach oben und
die zuerst gefundene Methode wird verwendet. Die allgemeineren Methoden sind
dabei durch die spezielleren Methoden verdeckt. Im vorliegenden Beispiel wird
viel Code aus der übergeordneten Methode dupliziert. Besser ist es – ähnlich wie
bei den Konstruktoren – vorhandenen Code in die neue Methode einzubinden.
Dazu kann über die Konstruktion super.methode() auf die verdeckte Methode
zugegriffen werden. Aus dem Beispiel wird dann
void print() {
super.print(); // Methode print aus Elternklasse ausführen
System.out.println("Kilometerstand: " + kilometer);
}
10.7.1
Dynamische Suche
Zwischen den abgeleiteten Klassen und den Elternklassen besteht eine ist ein
Beziehung. In unserem Beispiel gilt ein Auto ist ein Fahrzeug. Daher kann ein
Objekt einer abgeleiteten Klasse -– das ja alle Eigenschaften der Elternklasse
aufweist -– die Rolle eines Objektes der Elternklasse annehmen. Somit kann ein
Auto-Objekt einem Fahrzeug-Ojekt zugewiesen werden:
Auto a = new Auto( "Golf", 19999, 2002);
Fahrzeug f2 = a;
Die Variablen einer abgeleiteten Klasse sind zuweisungskompatibel zu den Variablen der Elternklasse. Die umgekehrte Richtung ist nicht möglich. Der Versuch
ergibt die Fehlermeldung:
Verleih.java [55:1] Incompatible type for =. Can’t convert Fahrzeug to Auto.
a = f2;
^
Ein Fahrzeug kann nicht die Rolle eines Autos übernehmen. Wenn nun eine Variable für Fahrzeuge auch ein Auto oder irgendein anderes Objekt einer abgeleiten
Klasse enthalten kann, stellt sich die Frage, welche Methode ausgewählt wird.
Wird bei
f2.print();
die Methode der Variablen (Klasse Fahrzeug) oder die gleichnamige Methode des
Objekts (Klasse Auto) verwendet? Die Java-Maschine interpretiert die Methodenaufruf zur Laufzeit. Bei dem Aufruf untersucht sie das Objekt und wählt die am
100
KAPITEL 10. OBJEKTE UND KLASSEN
besten passende Methode aus. Man spricht dann von dynamischem oder spätem
Binden. In dem Beispiel stellt der Interpreter fest, dass es sich um ein AutoObjekt handelt und ruft die Methode aus der Klasse Auto auf. Dieses durchaus
wünschenswerte Verhalten bedeutet allerdings einen gewissen Mehraufwand während der Ausführung. Bei jeder Variablen muss geprüft werden, welches Objekt
aus dem entsprechenden Vererbungsbaum aktuell referenziert wird. Aus diesem
Grund wurde die Klasse String als final deklariert. Damit ist klar, dass es keine
abgeleiteten Klassen gibt und die Suche entfällt.
Bei Programmiersprachen ohne Interpreter ist diese Methodenauswahl ein
Problem. In der Regel wird die Methode dort bereits zum Zeitpunkt des Compilerens festgelegt (statisches oder frühes Binden). Das dynamische Binden wird
dann durch spezielle Konstruktionen angefordert. In C++ dient dazu das Schlüsselwort virtual.
Die Tatsache, dass eine Variable Objekte unterschiedlicher Klassen enthalten
kann, wird als Polymorphismus bezeichnet. Polymorphismus ist ein wesentliches
Gestaltungselement der objekt-orientierten Entwicklung. Der große Vorteil liegt
in der gemeinsamen Behandlung zusammen gehörender Objekte verschiedenen
Typs. So kann in unserem Beispiel eine Variable vom Typ Fahrzeug Objekte aller
abgeleiteten Klassen wie Auto, Fahrrad, Flugzeug, etc. aufnehmen. Ruft man
eine Methode der Klassen wie z.B print auf, so wird automatisch die zu dem
speziellen Objekt passende Variante ausgewählt. Im folgenden Code-Abschnitt
wird diese Technik verwendet:
Fahrzeug[] fahrzeuge = new Fahrzeug[3];
fahrzeuge[0] = new Auto( "BMW" );
fahrzeuge[1] = new Fahrrad( "Bianci" );
fahrzeuge[2] = new Flugzeug( "Airbus" );
for( int i=0; i<fahrzeuge.length; i++ ) {
fahrzeuge[i].print();
}
In der Schleife wird automatisch für jedes Objekt die passende Methode print()
ausgewählt. Wenn in der jeweiligen Klasse eine
10.8
Attribute
Die Sichtbarkeit, Lebensdauer und Ableitbarkeit von Klassen, Methoden und
Variablen kann durch verschiedene Attribute bei der Definition festlegen. Die
wichtigsten sind:
10.8. ATTRIBUTE
public
private
protected
static
final
abstract
101
Die weitestgehende Festlegung. Variablen, Methoden und
Klassen sind überall sichtbar.
Nur die aktuelle Klasse sieht die Variablen oder Methoden. Abgeleitete Klassen erben sie nicht.
Abgeleitete Klassen und Klassen im gleichen Paket sehen
die Variablen oder Methoden.
Definition von Klassenvariablen und -methoden
Klassen mit dem Attribut final können nicht abgeleitet,
Methoden nicht überlagert und Variablen nicht verändert
werden.
Abstrakte Methoden enthalten keinen Rumpf und können nicht selbst aufgerufen werden (Syntaxbeispiel: abstract void test();). Sie dienen lediglich als Vorlagen für
die Methoden in den abgeleiteten Klassen. Eine Klasse mit
abstrakten Methoden ist selbst abstrakt und kann nicht
instanziert werden.
Verschiedene Attribute können miteinander kombiniert werden. Abstrakte Klassen dienen als Muster. Sie können wie andere Klassen abgeleitet werden. Wir
könnten beispielsweise die Klasse Fahrzeug als abstract deklarieren. Damit wäre sichergestellt, dass keine Objekte aus dieser Klasse erzeugt werden. Bei dem
Versuch resultiert die Fehlermeldung
Verleih.java [44:1] Cannot use operator new for this type
Fahrzeug f = new Fahrzeug( "Golf", 19999, 2002);
^
Die Objekte aus den abgeleiteten, konkreten Klassen könnten wie gehabt verwendet werden. Abstrakte Klassen können sowohl konkrete als auch abstrakte
Elemente enthalten. Um aus einer abstrakten Klasse eine konkrete Klasse abzuleiten, müssen alle abstrakten Elemente konkretisiert werden. Es ist möglich, bei
der Ableitung nur einen Teil der Elemente zu konkretisieren. Die resultierende
Klasse ist dann aber auch wieder abstrakt.
Übung 10.2 Wo ist aus Sicht einer abgeleiteten Klasse der Unterschied zwischen
final und private Methoden in der Elternklasse?
Übung 10.3 Welche Konstruktionen sind legal:
• public static final int TÜV_PERIODE = 2;
• abstract private void eingeben();
• abstract protected void ausgeben();
102
KAPITEL 10. OBJEKTE UND KLASSEN
Übung 10.4 Leiten Sie aus Fahrzeug eine Klasse Fahrrad ab. Neue Eigenschaften sollen sein:
• Rahmenhöhe
• Herren– oder Damenrad
10.9
10.9.1
Mehrfachvererbung und Interfaces
Einleitung
Nicht immer lassen sich die Klasse in einen einzigen Vererbungsbaum einordnen. In unserem Beispiel basiert der Vererbungsbaum auf der Interpretation der
Klassen als Fahrzeuge. Aus einem anderen Blickwinkel könnte aber beispielsweise
ein Mountainbike auch als Sportgerät gesehen werden. Ein Mountainbike könnte daher sowohl in dem Baum Fahrzeug als auch in dem Baum Sportgerät sein.
Man spricht dann von Mehrfachvererbung. Der Nutzen der Mehrfachvererbung
ist nicht unumstritten. Sicherlich gibt es Anwendungsfälle, die durch eine hierarchische Baumstruktur nicht vollständig beschrieben werden können. Andererseits
wirft die Mehrfachvererbung eine Reihe von Problemen auf. Beispielsweise gilt es
dann, Namenskonflikt in den beiden (oder mehreren) Oberklassen zu vermeiden.
Die Entwickler von Java haben sich für einen Mittelweg entschieden. Es gibt
nur eine einfache Vererbung, aber über so genannte Interfaces (Schnittstellen)
existiert ergänzend zu dem Vererbungsbaum eine zweite Vererbungsmöglichkeit.
Interfaces definieren ein bestimmtes Verhalten oder bestimmte Eigenschaften
von Klassen. Man kann sie auch als eine Art von Protokoll interpretieren. Die Vererbungshierarchie spielt dabei eine weniger große Rolle. So werden wir in einem
der Beispiele sehen, wie durch ein Interface die Eigenschaft Vergleichbar implementiert wird. Diese Eigenschaft kann von ganz unterschiedliche Klassen erfüllt
werden. Auch wenn mehrere Klassen diese Eigenschaft haben, begründet dies
nicht notwendigerweise eine Verwandtschaftsbeziehung untereinander. Während
die Ableitung eine ist ein Beziehung begründet, kann man bei implementierten
Interfaces von einer verhält sich wie oder hat Fähigkeiten von Beziehung sprechen.
Methoden und Konstanten werden in einem Interface wie in einer normalen
Klasse definiert. Ein Interface ist allerdings stets abstrakt. Es können demnach
keine Instanzen erzeugt werden. Eine Klasse kann ein Interface implementieren,
indem sie alle abstrakten Methoden realisiert. Im Unterschied zur Ableitung bei
Klassen spricht man bei Interfaces von Implementierung. Eine Klasse kann mehrere Interfaces implementieren, in dem sie die Methoden konkretisiert. Mit jeder
Implementierung verpflichtet sich die Klasse zu dem durch das jeweilige Interface
festgelegte Verhalten.
10.10. BEISPIEL FH-VERWALTUNG
10.10
103
Beispiel FH-Verwaltung
Für die Verwaltung unserer FH können wir folgenden Klassenbaum verwenden:
Person
Student
Professor Mitarbeiter
Tutor
Ausgangspunkt ist die allgemeine Klasse Person. Eine einfache Realisierung ist:
public class Person extends java.lang.Object {
String name;
/** Creates new Person */
public Person() {
}
public Person(String name) {
this.name = name;
}
}
Als einzige Eigenschaft ist ein Name realisiert. Davon abgeleitet ist die Klasse
Student mit einer zusätzlichen Variablen für die Matrikelnummer.
public class Student extends Person {
int matrikel;
public Student() {
}
public Student( String name ) {
super( name );
}
public Student( String name, int matrikel ) {
super( name );
this.matrikel = matrikel;
}
}
Interessant in Bezug auf Mehrfachvererbung sind die beiden Klassen Tutor und
Professor. Die beiden Klassen befinden sich in unterschiedlichen Zweigen. Trotzdem haben sie eine Gemeinsamkeit: sie können Veranstaltungen übernehmen. Beide können in diesem Sinn als Lehrkraft angesehen werden. Dazu gehört, dass sie
Vorlesungen übernehmen. Dieser Zusammenhang kann durch ein entsprechendes
Interface realisiert werden. Dazu definieren wir ein Interface wie folgt:
104
KAPITEL 10. OBJEKTE UND KLASSEN
// Lehrkraft.java
public interface Lehrkraft {
public void übernehmeVorlesung(String name, int stunden);
}
Die einzige Methode dient dazu, eine Vorlesung mit gegebenem Namen und einer
Anzahl von Stunden zu übernehmen. Jede Klasse, die die Rolle einer Lehrkraft
übernehmen will, verpflichtet sich dann, diese Methode zu implementieren. Betrachten wir zunächst die Klasse Professor:
public class Professor extends Person implements Lehrkraft {
private int SWS = 0;
public Professor() {}
public Professor(String name) { super( name ); }
public void übernehmeVorlesung(String vorlesung, int stunden) {
System.out.println( "Prof. " + name + " übernimmt " + vorlesung);
SWS += stunden;
System.out.println( "SWS: " + SWS);
}
}
Die Klasse wird von Person abgeleitet und implementiert das Verhalten Lehrkraft.
Neu ist die Variable SWS für die Anzahl der Semesterwochenstunden. Die im Interface vorgegebene Methode wird passend realisiert. Die Klasse Tutor hat folgende
Form:
public class Tutor extends Student implements Lehrkraft {
float vergütung = 0;
public Tutor() { }
public Tutor( String name ) { super( name ); }
public void übernehmeVorlesung(String vorlesung, int stunden) {
System.out.println( "Tutor " + name + " übernimmt " + vorlesung);
vergütung += stunden * 10;
System.out.println( "Vergütung: " + vergütung + " Euro");
}
}
In diesem Fall dient die Methode übernehmeVorlesung auch dazu, das Gehalt
festzulegen. Welchen Gewinn bietet die Verwendung des Interfaces? Der Vorteil
ist, dass jetzt sowohl Professor als auch Tutor als Lehrkraft verwendet werden
können. Beide Klassen erfüllen die Bedingung „ist eine Lehrkraft“. Wir untersuchen diese Möglichkeit an Hand einer weiteren Klasse Vorlesung.
10.11. BEISPIEL SORTIEREN
public class Vorlesung
String name;
int stunden = 2;
105
{
public Vorlesung() { }
public Vorlesung(String n, Lehrkraft lk) {
name = n;
lk.übernehmeVorlesung( name, stunden);
}
}
Der zweite Konstruktor erwartet als Parameter eine Referenz auf eine Lehrkraft.
Als Interface ist Lehrkraft abstrakt und es können keine Objekte erzeugt werden.
Aber statt dessen kann ein Objekt einer Klasse, die dieses Interface implementiert,
eingesetzt werden. Ein entsprechendes Beispiel ist:
public static void main(String args[]) {
Professor p = new Professor( "Claudia Maier" );
Tutor t
= new Tutor( "Jens Schmidt");
Vorlesung RN
= new Vorlesung( "Rechnernetze", p);
Vorlesung RNL1 = new Vorlesung( "Analysis 1", t);
}
Die Ausgabe ist:
Prof. Claudia Maier übernimmt Rechnernetze
SWS: 2
Tutor Jens Schmidt übernimmt Analysis 1
Vergütung: 20.0 Euro
Durch die Einführung des Interfaces können beide Objekte – obwohl sie Instanzen
unterschiedlicher Klassen sind – gleichermaßen als zweiter Parameter übergeben
werden. Im Konstruktor wird die Methode übernehmeVorlesung ausgeführt. Automatisch wird die jeweils passende Methode ausgewählt und damit die entsprechende Aktion ausgeführt: bei der Professorin wird die Zahl der SWS erhöht und
bei dem Tutor die Vergütung
10.11
Beispiel Sortieren
Betrachten wir als weiteres Beispiel das Sortieren des Feldes mit Objekten der
Klasse Auto. In der Klasse Arrays finden wir die Methode
public static void sort(Object[] a, Comparator c)
106
KAPITEL 10. OBJEKTE UND KLASSEN
zum Sortieren von Feldern. Im zweiten Argument wird ein Objekt zum Vergleichen der Feldinhalte übergeben. Der Vergleicher wird durch Implementieren des
Interfaces Comparator realisiert. Der Dokumentation entnimmt man:
public interface Comparator
Method Summary
int
compare(Object o1, Object o2)
boolean
equals(Object obj)
Compares its two arguments for order.
Indicates whether some other object is "
Der Quellcode ist
public interface Comparator {
int compare(Object o1, Object o2);
boolean equals(Object obj);
}
Das Schlüsselwort interface unterscheidet die Definition von einer Klasse. Ein
Interface ist per Definition abstrakt. Nach dem Namen könnte noch die Ableitung eines oder mehrerer anderer Interfaces folgen. Im Block steht die Definition
der beiden Methoden. Ansonsten darf ein Interface nur noch die Definition von
Konstanten enthalten.
10.11.1
Interface in eigener Klasse
Der Comparator wird in einer Klasse Vergleich wie folgt implementiert:
public class Vergleich
extends java.lang.Object
implements java.util.Comparator {
public int compare(
java.lang.Object obj, java.lang.Object obj1) {
Auto a1, a2;
a1 = (Auto) obj;
a2 = (Auto) obj1;
// Sortieren nach Preis
return a1.kaufPreis - a2.kaufPreis;
}
}
Die Klasse wird von Object abgeleitet und implementiert Comparator. Es fällt
auf, dass nur die Methode compare implementiert wird, nicht aber die Methode
equals. Dies scheint zunächst der Forderung zu widersprechen, alle Methoden
des Interfaces zu implementieren, um zu einer konkreten Klasse zu kommen. Die
10.11. BEISPIEL SORTIEREN
107
Klasse Vergleich braucht allerdings equals nicht selbst zu implementieren, da
sie diese Methode von Object erbt. Die Methoden des Interfaces müssen also
nicht unbedingt selbst implementiert zu werden, sondern können von irgendeiner
der Oberklasse geerbt werden. Damit lässt sich das Feld wie folgt sortieren
Vergleich vrgl = new Vergleich();
Arrays.sort( v.autos, vrgl );
oder
Arrays.sort( v.autos, new Vergleich() );
10.11.2
Interface integriert in andere Klasse
Ein Interface kann auch von einer normalen Klasse implementiert werden. Wir
betrachten eine Erweiterung der Klasse Auto:
public class Auto extends Fahrzeug implements Comparable {
...
public int compareTo(java.lang.Object p1) {
return this.kaufJahr - ((Auto) p1).kaufJahr;
}
...
Die Klasse implementiert jetzt das Interface Comparable. Dieses Interface hat nur
eine Methode compareTo, mit der das aktuelle Objekt mit einem zweiten Objekt
verglichen wird. Der Aufruf zum Sortieren des Feldes ist dann
Arrays.sort( v.autos );
Es wird kein expliziter Vergleicher angegeben sondern die natural comparison method – die den Objekten eigene Vergleichsmethode – wird verwendet. Da abgeleitete Klassen zuweisungskompatibel zu den Oberklassen sind, kann Auto sowohl
als Fahrzeug als auch als Comparable betrachtet werden. Das folgende Beispiel
illustriert diese Doppelnatur:
Auto a
= new Auto("porsche", 40000, 2001);
Comparable c = new Auto("golf",
15000, 1999);
System.out.println("Comparable c: " + c);
System.out.println("Vegleich " + c.compareTo( a ) );
System.out.println("Vegleich " + a.compareTo( c ) );
Ausgabe:
Comparable c: Auto@1fcc69
Vegleich -2
Vegleich 2
108
KAPITEL 10. OBJEKTE UND KLASSEN
Ein Auto-Objekt hat Zugang zu den Methoden des Interfaces. Umgekehrt kann
ein Objekt der Oberklasse Comparable nicht auf die Methoden der Klasse Auto
zugreifen:
c.print();
// geht nicht
Allerdings ist eine explizite Typumwandlung möglich:
( (Auto) c).print();
10.12
// okay
Anonyme Klassen
Klassen, die nur an einer Stelle benötigt werden, können dort direkt als so genannte anonyme Klassen eingefügt werden. Aus dem Beispiel mit dem Comparator
wird dann
Arrays.sort( v.autos, new Comparator() {
public int compare(
java.lang.Object obj, java.lang.Object obj1) {
Auto a1, a2;
a1 = (Auto) obj;
a2 = (Auto) obj1;
return a1.kaufPreis - a2.kaufPreis;
}
} );
Hier wird sofort der Code für die Methode eingefügt. Vorteilhaft an diesem Vorgehen ist eine gewisse Übersichtlichkeit:
• weniger explizite Klassen
• Code direkt dort wo er benötigt wird
Das Verfahren ist aber auf solche kleinen, einmaligen Klassen beschränkt. Sobald
der Code umfangreicher wird, sollte eine eigene Klasse angelegt werden. Häufig
findet man anonyme Klassen bei Programmen mit graphischer Oberfläche, um
verschiedenste Arten von Ereignissen zu behandeln. Das folgende Beispiel zeigt
einen WindowAdapter, in dem das Verhalten bei Schließen des Fensters definiert
wird:
addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent event) {
dispose();
// alle Ressourcen freigeben
System.exit(0);
// Anwendung beenden
}
} );
10.13. LOKALE KLASSEN
10.13
109
Lokale Klassen
Der Vollständigkeit halber sollen an dieser Stelle kurz lokale Klassen eingeführt
werden. Lokale Klasse werden innerhalb einer anderen Klasse definiert (innerhalb
des äußersten {}-Blocks). Sie sind nur innerhalb der äußeren Klasse sichtbar
haben aber umgekehrt Zugriff auf alle Merkmale dieser Klasse. Der Compiler
erzeugt aus anonymen und lokalen Klassen eigene .class Dateien. Der Namen
setzt sich aus dem Namen der äußeren Klasse, einem Dollarzeichen und dem
Namen der inneren Klasse zusammen. Anonyme Klassen werden nummeriert.
Beispiel 10.1 Innere und anonyme Klassen:
Verleih$Innen.class
Verleih$1.class
110
KAPITEL 10. OBJEKTE UND KLASSEN
Kapitel 11
Zeichenketten
11.1
Einleitung
Die Basis für die Verarbeitung von Zeichen ist der Datentyp char (Kurzform für
character ). Natürlich ist es möglich, Felder von einzelnen Zeichen anzulegen. In
Java gibt es darüber hinaus eine eigene Klasse String für Zeichenketten. Die Verwendung von String Instanzen erleichtert wesentlich die Programmierung. Java
stellt eine ganze Reihe von Methoden unter anderem zum Suchen in Zeichenketten sowie zum Vergleichen und Verändern von Zeichenketten zur Verfügung.
Aufgrund der großen praktischen Bedeutung der String-Verarbeitung wird die
Klasse String bevorzugt behandelt. Der Compiler kennt den internen Aufbau
und nutzt diese Kenntnis, um den Code zu optimieren. Weiterhin gibt es für
diese Klasse einen speziellen Verknüpfungsoperator.
11.2
Datentyp char
Einzelne Zeichen werden in Java durch den Datentyp char dargestellt. Die Konstanten werden in einfache Anführungsstriche gesetzt:
char zeichen = ’x’;
Dabei wird für die Zeichenkodierung Unicode verwendet, so dass auch nationale
Sonderzeichen erfasst sind. Steuerzeichen wie der Zeilenumbruch werden durch
ein vorgestelltes \-Zeichen notiert:
\n
\t
\’
\\
\uXXXX
newline Zeilenumbruch
Horizontaler Tabulator
Einfaches Anführungszeichen
Backslash
Unicode-Zeichen mit den vier Hexadezimalziffern XXXX
111
112
KAPITEL 11. ZEICHENKETTEN
11.3
Konstruktoren für String
Eine Referenz der Klasse String wird durch die Anweisung in der Form
String text;
angelegt. Nach dieser Anweisung ist text eine Referenz auf ein String-Objekt.
Entweder direkt bei der Definition oder später an beliebiger Stelle kann die Instanz dann mit einer Zeichenketten-Konstanten (Literal) durch einfache Zuweisung belegt werden:
text = "Frankfurt";
Dadurch wird ein String-Objekt mit dem entsprechenden Text als Inhalt erzeugt
und die Referenz auf dieses Objekt gesetzt. Das folgende Programm zeigt ein
einfaches Beispiel mit einem String, der nacheinander mit zwei verschiedenen
Inhalten gefüllt wird.
public class TestString {
String text;
public void teste( ) {
System.out.println( "text: " + text );
text = "München";
System.out.println( "text: " + text );
text = "Frankfurt";
System.out.println( "text: " + text );
}
}
Die Ausgabe lautet:
text: null
text: München
text: Frankfurt
Bei der ersten Ausgabe ist der String noch leer -– angezeigt durch den speziellen Wert null. Bei der Zuweisung wird automatisch jeweils ein ausreichender Speicherbereich reserviert. Diese Speicherverwaltung übernimmt das JavaLaufzeitsystem. So ist es kein Problem, dass bei der zweiten Zuweisung der Text
länger ist.
Die Klasse String enthält weiterhin noch eine Reihe von Konstruktoren. Typisch ist der Konstruktor
public String(char[] zeichen)
11.4. LÄNGE VON ZEICHENKETTEN UND EINZELNE ZEICHEN
113
Der Konstruktor erhält ein Feld von Zeichen. Aus diesen Zeichen bildet er ein
String-Objekt mit der entsprechenden Zeichenkette. Ein Beispiel dazu ist:
char[] zeichen = { ’a’, ’b’, ’c’ };
text = new String( zeichen );
Dann enthält text die Zeichenkette „abc“. Dieses Vorgehen kann nützlich sein, um
Texte automatisch zu erzeugen. Dabei ist zu beachten, dass die Zeichen kopiert
werden. Spätere Änderungen im Feld wirken sich nicht mehr auf das StringObjekt aus Andere Konstruktoren erlauben es, nur einen Ausschnitt des Feldes
mit Zeichen zu verwenden oder anstelle der Zeichen byte-Werte zu übergeben.
11.4
Länge von Zeichenketten und einzelne Zeichen
Die erzeugten String-Objekte können wie andere Objekte benutzt werden. Insbesondere gibt es eine ganze Reihe von Methoden, um mit Strings zu arbeiten.
Die Länge der Zeichenkette erhält man mit der Methode length(). Der Aufruf
text.length() gibt die Länge – d. h. die Anzahl der enthaltenen Zeichen – zurück. Einzelne Zeichen aus der Zeichenkette kann man mit charAt( int index
) abfragen. Wie bei Felder beginnt die Zählung mit dem Index 0. Die folgenden
Programmzeilen bilden eine Schleife zur zeichenweise Ausgabe:
for( int i=0; i<text.length(); i++ ) {
System.out.println( i + ": " + text.charAt(i) );
}
11.5
Arbeiten mit Zeichenketten
Die Zeichenketten in String-Objekten sind konstant. Nach der Initialisierung in
einer der oben beschriebenen Formen liegt der Text fest und kann nicht mehr
verändert werden. Auf den ersten Blick erscheint dies als eine große und wenig
einleuchtende Einschränkung. Schließlich ist die Veränderung von Zeichenketten
eine wesentliche Aufgabe vieler Anwendungen.
Der Widerspruch löst sich auf, wenn man sich klar macht, dass man eigentlich
stets mit den Referenzen auf die String-Objekte arbeitet. Ein Text wird dann
verändert, indem ein neues Objekt angelegt wird und anschließend die Referenz
auf diese neue Objekt gesetzt wird. Das alte Objekt bleibt unverändert bestehen.
Betrachten wir als Beispiel die Anwendung der Methode trim() in der Klasse
String. Diese Methode hat die Aufgabe, Leerzeichen am Anfang und Ende einer
Zeichenkette zu entfernen. Zunächst wird durch die Zuweisung
String
text = "
München
";
114
KAPITEL 11. ZEICHENKETTEN
ein Text mit Leerzeichen erzeugt. Der Aufruf text.trim() erzeugt ein neues
String-Objekt, füllt es mit dem Text ohne die Leerzeichen und gibt es zurück.
Mit der Zuweisung
text = text.trim();
wird die Referenz auf das neue Objekt gesetzt. Auf diese Art und Weise wird das
Ziel erreicht: text enthält jetzt den gewünschten Text ohne Leerzeichen. Intern
gibt es jetzt zwei Objekte: der ursprüngliche String mit den Leerzeichen und
ein neuer String ohne Leerzeichen. Falls das erste Objekt – mit den Leerzeichen
– nicht mehr von anderen Referenzen angesprochen wird, ist es Aufgabe des
Garbage Collectors dieses Objekt zu löschen und den Speicherplatz wieder frei
zu geben. Der Ablauf sieht zusammen gefasst wie folgt aus:
Ref.
1.) text
2.) text
3.)
text
Objekte
→
" München
→
" München
trim() → "München"
" München
→
"München"
"
"
"
Ist keine Veränderung notwendig, weil keine Leerzeichen an den Ränder stehen, so
wird auch kein neues Objekt benötigt. Der Aufruf gibt dann einfach die Referenz
auf das bestehende Objekt zurück. Der Programmierer braucht sich um diese
Details nicht zu kümmern. Aus seiner Sicht ist mit der obigen Anweisung der Text
verändert worden. Die Verwaltung der Objekte kann er Java überlassen. Wichtig
ist nur, dass er die „Falle“ vermeidet, lediglich die Methode aufzurufen ohne das
neue Objekt bzw. die neue Referenz anschließend einer Variablen zuzuweisen. Der
Aufruf der Methode text.trim() alleine verändert nicht den Inhalt von text.
Die intuitive Anwendung der String-Objekte zeigt folgendes Beispiel:
text = "Frankfurt";
text += " am Main";
System.out.println( "text: " + text );
Mittels des Operators zur Verkettung von Strings wird scheinbar die Angabe
" am Main" direkt angehängt. Intern ist der Vorgang komplexer. Ausführlich
geschrieben lautet die Anweisung
text = text + " am Main";
Aus den beiden Strings – der Variablen text und dem Literal " am Main" – wird
ein neues String-Objekt erzeugt. Die Referenz auf das neue Objekt wird dann
wieder text zugewiesen.
Diese Verarbeitung – Erzeugung eines neuen String-Objektes für jede Veränderung – bedeutet einen gewissen Mehraufwand. In den meisten Anwendungen
11.6. TEILKETTEN
115
ist dies jedoch nicht relevant. Ansonsten besteht noch die Möglichkeit, die Klasse
StringBuffer zu verwenden, die dynamisch veränderbare Zeichenketten realisiert.
11.6
Teilketten
Die Methode zum Ausschneiden von Teilen aus Zeichenketten (substrings) ist
String substring( int beginIndex, int endIndex)
Sie liefert die Teilkette ab der Position des ersten Arguments bis zur Position vor
dem zweiten Argument. So ergibt der Aufruf
System.out.println( "Die Wetterau".substring(4,10) );
den Text Wetter. Das Verhalten zeigt folgendes Bild:
D i
0 1
e
2
3
W
4
↑
e
5
t
6
t
7
e
8
r
9
a
10
↑
u
11
Die Zeichen von Index 4 bis 9 bilden den neuen String. Dadurch, dass die zweite
Angabe die Position 10 unmittelbar nach dem Ende der Teilkette bezeichnet, gilt
der Zusammenhang
beginIndex + Länge = endIndex
wodurch sich in vielen Fällen die Berechnung der Endposition vereinfacht. Ein
zweite Version von substring mit nur einem Argument liefert die Zeichenkette
bis zum Ende des ursprünglichen Strings. Damit ergibt
System.out.println( "Die Wetterau".substring(4) );
den Text Wetterau.
11.7
11.7.1
Vergleichen und Suchen
Vergleichen
Der Vergleich zweier Strings mit dem == Operator testet, ob beide Referenzen
auf das gleiche Objekt deuten. In der Regel ist dies eine zu starke Forderung.
Meist interessiert nur, ob die beiden Strings die gleiche Zeichenkette enthalten.
(Zur Erinnerung: bei primitiven Typen vergleicht der Operator == den Inhalt,
bei Referenztypen die Referenz.) Die allgemeine Methode zum Vergleichen der
Inhalte ist
116
KAPITEL 11. ZEICHENKETTEN
boolean equals(Object anObject)
Die Methode liefert true wenn das Argument ein String ist und die gleiche Zeichenkette enthält.
Beispiel 11.1 Stringvergleich
text = "Frankfurt";
System.out.println( text.equals( "Frankfurt") );
Der Vergleich ergibt true. Die Methode equals vergleicht auf exakte Übereinstimmung. In dem Beispiel hätte text.equals( "frankfurt") das Resultat
false. Ein Vergleich ohne Berücksichtigung von Groß- und Kleinschreibung bietet
boolean equalsIgnoreCase(String anotherString)
Das Methoden-Paar
boolean startsWith(String prefix)
boolean endsWith(String suffix)
erlaubt gezielte Vergleiche mit dem Anfang beziehungsweise Ende der Zeichenkette. Die Details zu diesen Methoden sowie weiteren Varianten findet man in
der Dokumentation zu Java. Für z. B. die Implementierung von Sortierverfahren
wird häufig ein Vergleich von Strings auf ihre lexikalischen Reihenfolge benötigt.
Mit den Methoden
int compareTo(String str)
int compareToIgnoreCase(String str)
wird ein Integer-Wert berechnet, der den lexikalischen Abstand anzeigt. Das Resultat des Ausdrucks text.compareTo( str) ist:
negativ
0
positiv
text größer als str
text ist gleich str
text kleiner als str
Die Funktionalität entspricht der Bibliotheksfunktion strcmp in C. Es lohnt
sich, an dieser Stelle nochmals die Unterschiede zwischen einer prozeduralen Sprache wie C und einer objektorientierten Sprache wie Java zu betrachten. In C ist
der Aufruf strcmp( s1, s2) – eine Funktion mit zwei Argumenten. Die Funktion existiert sozusagen eigenständig. In Java gibt es keine „freien“ Funktionen
sondern nur Methoden, die an Klassen oder Instanzen „hängen“. Der Aufruf ist
demnach s1.compareTo( s2 ). Die Sichtweise ist: es gibt ein Objekt s1 und dieses Objekt verfügt über eine Methode, um es mit einem zweiten Objekt s2 zu
vergleichen.
11.8. VERÄNDERN VON ZEICHENKETTEN
117
Anmerkung:
Es ist in Java durchaus möglich, prozedural zu programmieren. Man kann z. B.
in einer eigenen Klassen MyString eine Methode static boolean compare(
String s1, String s2) implementieren und dann mit MyString.compare( s1,
s2 ) aufrufen. Da die Methode als statisch deklariert wurde kann sie unabhängig von einem Objekt benutzt werden. Die Frage, ob und wann dies sinnvoll ist,
kann nur im Zusammenhang mit dem Gesamtdesign einer Anwendung beantwortet werden.
11.8
Verändern von Zeichenketten
Einige Methoden erzeugen ein neues String-Objekt mit geändertem Text. Umwandlung in Groß- oder Kleinbuchstaben übernehmen
String toUpperCase()
String toLowerCase()
Als Beispiel liefert
System.out.println("Münchner Straße".toUpperCase() );
MÜNCHNER STRASSE
Die Methoden erkennen auch Sonderzeichen wie Umlaute oder ß und behandeln
sie richtig. Daneben gibt es noch eine einfache Methode zum Ersetzen von Zeichen:
String replace(char oldChar,
char newChar)
Damit werden alle vorkommenden Zeichen oldChar durch newChar ersetzt. Beispiel:
String text = "Die Wetterau";
System.out.println( text.replace( ’e’, ’x’ ) );
ergibt
Dix Wxttxrau
Weiterhin gibt es das Methoden-Paar
String replaceAll(String regex, String replacement)
String replaceFirst(String regex, String replacement)
Hier werden alle beziehungsweise nur das erste Vorkommen des regulären Ausdrucks regex durch den String im zweiten Argument ersetzt.
118
KAPITEL 11. ZEICHENKETTEN
Beispiel 11.2 Reguläre Ausdrücke
String email = "[email protected]";
// Ersetze mnd. durch noe.
String email2 = email.replaceAll("mnd", "noe");
// Ersetze alle Felder durch den Text feld. Ein Feld ist als
// "ein Buchstabe gefolgt von 0 oder mehr Buchstaben und -"
// definiert
String email3 = email.replaceAll("[a-z][a-z-]*", "xxx");
System.out.println( email2 );
System.out.println( email3 );
Ausgabe:
[email protected]
[email protected]
11.8.1
Suchen
Die Methode zum Suchen in Strings ist indexOf. Es gibt davon verschiedene Varianten, je nachdem ob man nach anderen Strings oder einzelnen Zeichen suchen
möchte. Als optionales zweites Argument kann man einen Offset, ab dem erst die
Suche beginnen soll, übergeben. Weiterhin gibt es Methoden lastIndexOf, bei
denen die Suche am Ende des Strings beginnt. Alle Methoden geben die gefundene Position als Integerwert zurück. Wird das Muster nicht gefunden, so ist der
Rückgabewert –1.
Beispiel 11.3 Suche in Strings
String text2 = "In der schönen Wetterau";
//
01234567890123456789012
//
1111111111222
Methodenaufruf
text2.indexOf(’e’)
text2.indexOf("Wett")
text2.indexOf("WETT")
text2.lastIndexOf("e")
text2.lastIndexOf("e", 19)
text2.lastIndexOf("e", 18)
text2.indexOf(’ ’, text2.indexOf(’ ’)+ 1)
Ergebnis
4
15
-1
19
19
16
6
11.9. TOKENIZER
11.9
119
Tokenizer
Für die häufig auftretende Aufgabe der Zerlegung einer Zeichenkette in einzelne Einheiten (Tokens) stellt Java die Klasse StringTokenizer bereit. Für eine
gegebene Zeichenkette erzeugt man durch den Konstruktor
StringTokenizer st = new StringTokenizer( text );
einen Tokenizer 1 . In dieser Form behandelt der Tokenizer alle Leerzeichen (genauer gesagt alle folgende Zeichen ’ ’, ’\t’, ’\n’, ’\r’, ’\f’) als Trennzeichen.
Der Tokenizer durchsucht die Zeichenkette nach den Trennzeichen und bricht
die Kette in entsprechend viele Teile auf. Diese Teile kann man nacheinander
mit der Methode nextToken() abrufen. Wie viele Tokens entstehen, kann mit
der Methode countTokens() abfragen werden. Die Methode gibt die Anzahl der
noch ausstehenden Tokens zurück. Nach einem Aufruf von nextToken() wird
der Zähler entsprechend reduziert. Alternativ kann man mit hasMoreTokens()
testen, ob überhaupt noch Tokens vorhanden sind. Eine typische Schleife um
nacheinander alle Tokens auszugeben ist:
while( st.hasMoreTokens() ) {
System.out.println( st.nextToken() );
}
Falls notwendig, kann man mit einem zweiten Parameter im Konstruktor eine
Zeichenkette mit den zu verwendenden Trennzeichen angeben. Die Trennzeichen
selbst werden standardmäßig entfernt.
Beispiel 11.4 Tokenizer
StringTokenizer st = new StringTokenizer(
"Vom Eise befreit sind Strom und Bäche" );
int i = 0;
while( st.hasMoreTokens() ) {
System.out.println( "Token " + i + ": "+st.nextToken() );
++i;
}
Ausgabe:
Token
Token
Token
Token
1
0:
1:
2:
3:
Vom
Eise
befreit
sind
benötigt import java.util.StringTokenizer;
120
KAPITEL 11. ZEICHENKETTEN
Token 4: Strom
Token 5: und
Token 6: Bäche
Die Klasse StringTokenizer ist recht praktisch, allerdings etwas veraltet. In
der Dokumentation heißt es:
StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended
that anyone seeking this functionality use the split method of String
or the java.util.regex package instead.
Eine entsprechende Konstruktion zur Aufteilung an allen Leerzeichen ist:
String[] parts =
"Vom Eise befreit sind Strom und Bäche".split(" ");
for( String p: parts ) {
System.out.println( p );
}
11.10
Konvertierungen
Für z. B. die Ausgabe ist es notwendig, andere Objekte oder primitiven Datentypen in Zeichenketten umzuwandeln. Alle Objekte implementieren eine Methode
toString(), die eine Darstellung als Zeichenkette liefert. Weiterhin stellt die
Klasse String Klassenmethoden valueOf() zum Konvertieren der primitiven
Datentypen bereit. Explizit kann man dann zur Ausgabe schreiben:
double x = 33.44;
System.out.println( "x = " + String.valueOf(x) );
Der Ausdruck String.valueOf(x) liefert einen String, der wiederum mit dem
ersten String verbunden wird. In solchen Ausdrücken braucht die Umwandlung
nicht explizit angegeben zu werden. Man kann einfacher schreiben
double x = 33.44;
System.out.println( "x = " + x );
und die Konvertierung wird automatisch eingefügt.
11.11
Die Klasse String ist endgültig
Die Klasse String enthält bereits sehr viele Methoden. Trotzdem sind nicht alle
Anwendungen abgedeckt. So fehlt beispielsweise eine Methode startsWithIgnoreCase.
11.12. ÜBUNGEN
121
Im Sinne der Objekt-orientierten Programmierung würde man in einem solchen
Fall eine eigene Klasse aus der String-Klasse ableiten und die fehlenden Methoden ergänzen beziehungsweise vorhandene Methoden mit eigenen Implementierungen überlagern.
Diese Vorgehensweise ist für die Klasse String nicht vorgesehen. Diese Klasse
hat das Attribut final. Damit wird verhindert, dass abgeleitete Klassen erzeugt
werden. Der Hauptgrund ist ein Gewinn an Performanz. Wir werden später sehen, wie abgeleitet Klassen die Erzeugung von Code schwieriger machen. Wenn
es aber keine abgeleiteten Klassen gibt, kann der Compiler effizienteren Code erzeugen. Neue Methoden lassen sich dann nur in anderen Klassen einbauen. Wie
in der Anmerkung zur Diskussion von compareTo gezeigt wurde, ist dies durchaus
möglich. Diese Implementierung ist aber ein gewisser Bruch mit den Ideen einer
streng Objekt-orientierten Programmierung.
11.12
Übungen
Übung 11.1 String text = "Die Wetterau";
String text2 = "In der schönen Wetterau";
//
01234567890123456789012
//
1111111111222
1. Was liefert text.substring(7)?
2. Was ist der Unterschied zwischen text.charAt( 0 ) und text.substring(0,1)?
3. Erzeugen Sie aus text durch Anfügen den neuen String " ** Die Wetterau ** "
4. Was ergibt text2.indexOf( "n W" )?
5. Wie kann man in dem String nach dem ersten "er" irgendwo nach einem
"W" suchen?
6. Testen Sie, ob ein String mit einem ? endet.
Übung 11.2 Reguläre Ausdrücke:
Wie lassen sich mit der Methode replaceAll alle Folgen von mehreren Leerzeichen in jeweils ein einziges Leerzeichen verkürzen? Zum Beispiel soll aus
"Dies
ist
ein Beispiel"
die Zeichenkette "Dies ist ein Beispiel" werden.
Übung 11.3 Telefonnummer:
122
KAPITEL 11. ZEICHENKETTEN
1. Wie kann man mit einem StringTokenizer die Telefonnummer
String telefon = "(49)6031.604-450";
in die Bestandteile zerlegen?
2. Wird der String durch den StringTokenizer verändert?
Übung 11.4 Umwandlung:
Wie ist die Ausgabe von System.out.println( "7 + 5 = " + 7 + 5 )?
Übung 11.5 Email-Adressen:
Gegeben sei das Format vorname.name@provider für Email-Adressen.
1. Schreiben Sie eine Methode, um einen String mit einer solchen Adresse in
seine Bestandteile Vorname, Name und Provider aufzutrennen.
2. Wie kann umgekehrt aus den Bestandteilen eine Email-Adresse gebildet
werden? Beachten Sie dabei, dass Email-Adressen keine Sonderzeichen wie
Umlaute enthalten dürfen.
Kapitel 12
Reguläre Ausdrücke
Suchen und Ersetzen sind häufige Aufgaben. Entweder möchte man selbst im
Editor oder in der Textverarbeitung ein Muster suchen oder durch ein anderes
ersetzen oder Programme sollen diese Aufgabe automatisch ausführen. Ein weiteres Anwendungsfeld sind Plausibiltätsprüfungen. So kann man beispielsweise
in eines Eingabemaske testen, ob die Werte bestimmten Vorgaben entsprechen.
Beispielsweise soll das Feld für die Postleitzahl genau 5 Ziffern enthalten (sofern
man nur Adressen innerhalb Deutschlands betrachtet). Häufig reicht es dabei
nicht aus, einen festen Text zu verwenden. Vielmehr sollen kompliziertere Muster
behandelt werden. Einige Beispiele sind:
• Suche nach allen Datumsangaben in einem Text
• Ersetzen des Dezimalpunktes durch ein Komma (oder umgekehrt)
• Prüfen, ob eine Zeile eine plausible Anschrift enthält (also Straße, Hausnummer, Postleitzahl, Stadt)
• Suchen nach allen C-Dateien, die im Namen den Begriff Euro enthalten
Für solche Fälle unterstützen viele Programmiersprachen und Editoren reguläre
Ausdrücke (regular expressions). Die Realisierungen in den verschiedenen Programmiersprachen unterscheiden sich in einigen syntaktischen Details sowie dem
Leistungsumfang, basieren aber auf dem gleichen Grundprinzip [Fri07]. Im folgenden wird zunächst das Konzept an einigen Beispielen erläutert. Darauf aufbauend wird gezeigt, wie man damit elegant Eingaben suchen und ersetzen kann.
Das erste Beispiel
boolean b = Pattern.matches("berg", text);
zeigt den Mechanismus. Es wird geprüft, ob ein eingegebener Text der Zeichenfolge berg entspricht. Die Methode matches der Klasse Pattern prüft, ob der
regulärer Ausdruck im ersten Parameter in der Zeichenkette im zweiten Parameter enthalten ist. In diesem Fall handelt es sich einfach um die Zeichenfolge berg.
123
124
KAPITEL 12. REGULÄRE AUSDRÜCKE
Das Resultat der Prüfung wird als wahr oder falsch zurückgegeben. Die Suche
nach einem Muster wird beispielhaft durch folgende Methode realisiert:
boolean suche( String suchMuster, String test ) {
// Kompiliere das Suchmuster zu einem RE-Pattern
Pattern p = Pattern.compile(suchMuster );
// Verknuepfe Pattern mit Zeichenkette, die durchsucht werden soll
Matcher m = p.matcher(test);
// fuehre Suche aus, gebe Ergebnis zurueck
return m.find();
}
Wie das Beispiel zeigt, werden Buchstaben als normaler Suchtext behandelt. Das
gleiche gilt für Ziffern und einen Teil der Sonderzeichen. Anderen Sonderzeichen
haben eine besondere Bedeutung. So steht der Punkt als Platzhalter für irgendein
Zeichen. Das Muster a.b deckt damit Folgen in der Art aAb, axb, a2b, u. s. w. ab.
Mehrere Punkte stehen für mehrere Zeichen. Mit .....berg findet man Friedberg,
aber auch Godesberg, eidelberg oder gar _Goldberg und p-n-Überg. Mit den beiden
Zeichen ^ und $ kann man Zeilenanfang und Zeilenende angeben. So sucht ^...$
nach allen Zeilen, die genau drei Zeichen enthalten. Die beiden Zeichen gehören
zu den Boundary matchers. Diese symbolisieren Grenzen und „verbrauchen“ keine
Zeichen im untersuchten Text (zero-length match). Dieser Gruppe enthält unter
anderem auch \b für eine Wortgrenze. Damit gilt:
berg
^berg$
\bberg\b
berg\b
berg irgendwo, auch Friedbergerin
berg alleine in einer Zeile
einzelnes Wort
berg am Wortende, also Friedberg, aber nicht Friedberger
Mehrere Alternativen werden durch ein |-Zeichen getrennt. So kann man mit
berg|stadt gleichzeitig nach berg und stadt suchen. Kompliziertere Ausdrücke
werden durch runde Klammern gegliedert. Beispielsweise bedeutet
(A|B)..(berg|stadt)
alle Muster mit:
• A oder B am Anfang
• dann zwei beliebige Zeichen
• berg oder stadt am Ende
• Beispiele: Arlberg, Bamberg
125
.
|
[]
^
$
()
\
Tabelle 12.1: Metazeichen
Irgendein Zeichen
Auswahl
Zeichenklasse
Zeilenanfang
Zeilenende
Gruppe
Danach wird das nächste Zeichen als normaler Text verwendet
Anstelle von (A|B) kann man kürzer [AB] schreiben. Allgemein kann man mehrere einzelne, alternative Zeichen als so genannte Zeichenklasse in eckigen Klammern angeben. Dann wird an dieser Stelle jedes der aufgeführten Zeichen akzeptiert. In diesem Fall haben die allermeisten Sonderzeichen keine besondere
Bedeutung sondern werden wie normale Zeichen behandelt. Ausnahmen sind das
Minus- und das ^-Zeichen. Mit dem Minuszeichen werden Bereiche spezifiziert.
Beispielsweise umfasst [a-h] alle Buchstaben von a bis h. Ein vorangestelltes
^-Zeichen kehrt die Bedeutung um.
Beispiel 12.1 Zeichenklassen
• [Ff] groß oder klein F
• [a-z] irgendein Kleinbuchstabe
• [A-Za-z] ein Buchstabe
• [0-9] eine Ziffer (entspricht [0123456789])
• [^ijkxyz] alles außer den aufgeführten Buchstaben
• [^0-9] keine Ziffer
• [^ .,;!?] kein Leerzeichen und kein Satzzeichen
Für häufig benutzte Klassen sind in Java eigene Zeichen definiert. So bezeichnet
beispielsweise \d die Klasse aller Ziffern und \w alle alphanumerischen Zeichen
(Ziffern und Buchstaben). Die Umkehrung wird durch einen Großbuchstaben
dargestellt: \D sind alle Zeichen außer Ziffern und \W umfasst alle Sonderzeichen.
Mehrere Klassen können mittels && verknüpft werden. Zwei Beispiele aus der
Java-Dokumentation:
[a-z&&[^bc]] a through z, except for b and c: [ad-z] (subtraction)
[a-z&&[^m-p]] a through z, and not m through p: [a-lq-z](subtraction)
126
KAPITEL 12. REGULÄRE AUSDRÜCKE
Tabelle 12.2: Wiederholungszeichen
*
beliebig oft oder gar nicht
+
mindestens 1-mal
?
1-mal oder gar nicht
{n}
genau n-mal
{n,}
mindestens n-mal
{n,m} mindestens n-mal, höchstens m-mal
Häufig möchte man Wiederholungen von Teilen des Suchmusters spezifizieren.
Ohne weitere Angabe darf jedes Zeichen genau einmal vorkommen. So bedeutet
[0-9] genau eine Ziffer. Dreistellige Zahlen können dann durch Wiederholung als
[0-9][0-9][0-9]
beschrieben werden. Zur Vereinfachung können Wiederholungsfaktoren – so genannte Quantoren – hinter die Ausdrücke geschrieben werden. In Tabelle 12.2 sind
die verschiedenen Möglichkeiten dazu zusammengestellt. Allgemein wird mit der
Form {n,m} angegeben, dass ein Muster mindestens n-mal und höchstens m-mal
auftreten darf. Mit e{2,3} sind zwei oder drei Wiederholungen des Buchstaben
e beschrieben. Die Kombination .* steht als Platzhalter für eine beliebige Folge
von Zeichen einschließlich einer leeren Folge. Für die dreistelligen Zahlen kann
man übersichtlicher schreiben:
[0-9]{3}
Beispiel 12.2 Wiederholungen
• ^[0-9]+$ alle Zeilen, die nur Ziffern enthalten
• [aeiou]{5} alle Muster mit genau 5 aufeinander folgenden Vokalen (z. B.
Treueeid)
• a\w*b\w*c\w*d\w* Wörter, die die Buchstaben a, b, c und d in dieser Reihenfolge enthalten
12.0.1
Gefundene Teile
Ein Matcher-Objekt enthält alle Informationen zu der aktuellen Suche. Über
entsprechende Methoden kann auf diese Informationen zugegriffen werden. Das
nächste Beispiel zeigt die Möglichkeit durch folgende Abfolge:
1. Methode find(), um nächsten Treffer zu finden
12.1. ÜBUNGEN
127
2. Falls erfolgreich, werden über start() und end() die Grenzen abgefragt.
3. Mit den Grenzen wird der entsprechende Teil der Zeichenkette entnommen
und ausgegeben.
Die Methode
public void testeRE() {
String test = "THW Kiel gewann mit 33 zu 21 Toren";
String suche = "[0-9]+"; // eine oder mehr Ziffern
System.out.println(test);
Pattern p = Pattern.compile(suche );
Matcher m = p.matcher(test);
for(;;) {
if( ! m.find() ) break;
System.out.println(test.substring( m.start(), m.end()+1) );
}
}
ergibt
THW Kiel gewann mit 33 zu 21 Toren
33
21
12.1
Übungen
Übung 12.1 Reguläre Ausdrücke
Welche Muster werden durch folgende reguläre Ausdrücke beschrieben:
1. [a-zA-Z_][a-zA-Z_0-9]*
2. [+-]?[0-9]+
3. [a-zA-Z.]*\@[a-zA-Z.]*
4. ([0-9A-F]{2}-){5}[0-9A-F]{2}
Übung 12.2 Reguläre Ausdrücke
Geben Sie für folgende Suchaufgaben reguläre Ausdrücke an:
1. Folgen von zwei oder mehr Leerzeichen
2. Wörter mit mehr als 25 Buchstaben
128
KAPITEL 12. REGULÄRE AUSDRÜCKE
3. Alle achtstelligen Binärzahlen
4. Integerkonstanten in der Sprache C in oktaler oder hexadezimaler Schreibweise
5. Alle Jahreszahlen von 1980 bis 2099
6. Zeitangaben in der Form Stunden:Minuten
7. Alle Autokennzeichen, die mit FB beginnen, und bei denen folgt: ein Leerzeichen, ein oder zwei Großbuchstaben, ein Leerzeichen, ein bis drei Ziffern
8. Zeilen, die mit <h1 oder <h2 beginnen
9. Ausdrücke, die mit <a beginnen und mit a> enden
Kapitel 13
Tools
13.1
Einleitung
Der Satz von elementaren Entwicklungstools wird als JDK (Java Development
Kit) bezeichnet. Dazu gehören eine Reihe von Tools wie der Compiler javac,
der Starter java und der Debugger jdb. Die aktuelle Version ist J2SE 1.2, wobei die Abkürzung für Java 2, Standard Edition steht. Als Erweiterung bietet
Sun die Java 2 Platform, Enterprise Edition, abgekürzt J2EE an. Benutzer, die
nur vorhandene Java-Anwendungen ausführen möchte, können das Java Runtime Environment JRE verwenden. In diesem Kapitel wird eine Einführung zu den
wichtigsten der Tools des JDK gegeben. Details über die zum Teil umfangreichen
Optionen findet man in der Dokumentation von Sun.
13.2
jar
Der Compiler erzeugt aus jeder Klasse eine eigene Bytecode-Datei mit der Endung
.class. Mit wachsender Anzahl von Klassen führt dies zu einer großen Anzahl
von Dateien. Speziell bei Applets, die über das Internet ausgeführt werden, ist dies
nachteilig. Über eine HTTP-Verbindung wird jede Datei – und damit jede Klasse
– einzeln geladen. Daher besteht die Möglichkeit, mehrere (Klassen-) Dateien zu
einem Archiv zusammen zu stellen. Solche Archive können von den JDK-Tools
verwendet werden. Das Tool zum Erstellen und Verwalten der Archive jar -–
Java archive program — basiert auf dem UNIX Programm tar (tape archive
program) und verwendet eine ähnliche Syntax. Der Aufruf, um eine Anzahl von
Dateien zu einem Archiv zusammen zu packen, lautet
jar cvf archiv-file file1 file2 file3 ...
Das erste Argument spezifiziert die auszuführenden Operationen durch eine Reihe
von einzelnen Buchstaben. In dem Beispiel haben die Kommandos die Bedeutung
129
130
KAPITEL 13. TOOLS
c create
Anlegen eines neuen Archivs
v verbose Mit vielen Ausgaben zur Information
f file
Ausgabe in eine Datei (das nächste Argument)
Mit dem Befehl
jar cvf test.jar *.class
Manifest wurde hinzugefügt.
Hinzufügen von: FH.class(ein = 725) (aus= 495)(komprimiert 31 %)
Hinzufügen von: Lehrkraft.class(ein = 174) (aus= 146)(komprimiert 16 %)
Hinzufügen von: Person.class(ein = 401) (aus= 265)(komprimiert 33 %)
Hinzufügen von: Prof.class(ein = 942) (aus= 540)(komprimiert 42 %)
Hinzufügen von: Student.class(ein = 374) (aus= 250)(komprimiert 33 %)
Hinzufügen von: Tutor.class(ein = 882) (aus= 505)(komprimiert 42 %)
Hinzufügen von: Vorlesung.class(ein = 869) (aus= 549)(komprimiert 36 %)
werden alle .class Dateien aus dem Beispiel FH in ein Archiv test.jar kopiert.
Die Ausgabe zeigt, dass dabei die Dateien komprimiert werden. Weiterhin wird
eine Datei mit dem Namen Manifest hinzu gefügt. In dem Beispiel wurde dazu
keine weitere Angabe gemacht und jar erzeugte ein Standardversion dieser Datei
mit dem Inhalt:
Manifest-Version: 1.0 Created-By: 1.4.0-beta3 (Sun Microsystems Inc.)
Im allgemeinen enthält die Datei Informationen über das Archiv. Die Informationen werden als Paar Name: Wert dargestellt. Anstelle der automatisch erzeugten
Version kann man eine eigene Manifest-Datei angeben. Eine Datei mit dem Namen manifest wird durch den Befehl
jar cvmf manifest test.jar *.class
eingebunden. Maßgeblich ist dafür die Option m. Man beachte, dass die Dateien
in der Reihenfolge der Optionen anzugeben sind. Mit diesem Mechanismus kann
in einem Archiv die auszuführende Klasse spezifiziert werden. Dazu trägt man in
die Manifest-Datei den Namen der Hauptklasse ein:
Main-Class: FH
Dann wird bei dem Aufruf
java -jar test.jar
die Methode main in der angegebenen Hauptklasse ausgeführt.
13.3. JAVA UND KLASSENNAMEN
13.3
131
java und Klassennamen
Die Methode main einer Klasse wird über den Aufruf
java Klasse
ausgeführt. Im einzelnen erfolgen die Schritte
1. Die Java VM wird gestartet.
2. Die angegebene Klasse wird geladen.
3. Andere benötigten Klassen werden geladen.
4. Die Methode public static void main(String args[]) wird ausgeführt.
5. Eventuelle weitere Argumente nach dem Klassennamen werden als Parameter im Feld args[] an main übergeben.
Interessant ist dabei die Frage, wie die anderen Klassen gefunden werden oder -–
anders gefragt — wo nach Klassen gesucht wird. Java unterscheidet 3 Hierarchien
bei der Suche:
1. Bootstrap Classes
2. Extension Classes
3. User Classes
Mit Bootstrap Classes sind die Standardklassen von Java gemeint. Sie befinden sich in Archiven (z. B. rt.jar) im Verzeichnis jre\lib innerhalb des SDK.
In dem weiteren Verzeichnis jre\lib\ext können Erweiterungsklassen abgelegt
werden. Diese Klassen müssen in Archiven gepackt sein. Es ist nicht möglich, an
dieser Stelle eine einzelne Klassendatei zur Suche zu verwenden.
Schließlich können eigene Klassen eingesetzt werden. Diese Klassen können
entweder als einzelnen Dateien oder in Archiven vorliegen. Die zu durchsuchenden Verzeichnisse sind als CLASSPATH definiert. Es handelt sich dabei um eine
Liste von Verzeichnisnamen, getrennt mit ; oder : für Windows beziehungsweise UNIX-Umgebungen. Standardmäßig enthält CLASSPATH das aktuelle Verzeichnis (.). Mit der Option –cp oder ausführlich –classpath lässt sich dieser
Standard beim Aufruf von java überschreiben. Eine mittlerweile nicht mehr empfohlene Alternative besteht darin, die Systemvariable CLASSPATH zu setzten.
132
13.4
KAPITEL 13. TOOLS
Klassennamen
Zur besseren Verwaltung und Übersicht können Klassen zu Paketen zusammen
gefasst werden. Die Zugehörigkeit zu einem Paket wird mit der Anweisung
package PaketName;
bestimmt. Diese Anweisung muss ganz am Anfang der Datei stehen. Fehlt eine
solche Angabe, so wird die Klasse dem Default-Paket zugeordnet. Die Paketnamen bestehen aus einem oder mehreren durch Punkte getrennten Namen. Dazu
schlägt Sun ein Schema basierend auf dem eigenen Domainnamen vor. Damit
sollen Namenskonflikte durch gleichlautende Pakete verhindert werden. Konkret
wird vorgeschlagen, den eigenen Domainnamen in umgekehrter Reihenfolge der
Komponenten zu verwenden. Wenn wir z. B. uns in mnd.fh-friedberg.de befinden, so ist ein entsprechender Paketname (ohne Bindestrich)
package de.fhfriedberg.mnd.pg.beisp1;
Das Beispiel der FH-Verwaltung lässt sich leicht umstellen, indem in allen Klassen
eine entsprechende Zeile eingefügt wird. Die Klassen werden automatisch in einer
passenden Struktur von Verzeichnissen abgelegt. Mit dem Aufruf
javac -d . *.java
erzeugt der Compiler ausgehend von dem aktuellen Verzeichnis einen Baum mit
den angegebenen Namen. Angenommen die Klasse FH gehöre nicht zu diesem
Paket. Dann findet der Compiler nicht mehr die Klassen im aktuellen Pfad. Eine Möglichkeit ist dann, die Klassen mit ihrem vollen Namen inklusive Paket
anzugeben (voll qualifizierter Name). Im Beispiel kann man schreiben
de.fhfriedberg.mnd.pg.beisp1.Vorlesung v =
new de.fhfriedberg.mnd.pg.beisp1.Vorlesung();
Durch die Angabe des Paketnamens findet der Compiler und später auch java die
Klassen. Mit dieser Methode lassen sich zwar Klassen eindeutig spezifizieren, aber
für den häufigen Gebrauch ist die volle Namensangabe sehr umständlich. Daher
kann alternativ ein Paket über die import-Anweisung geladen werden. Wenn die
Klasse FH die Anweisung
import de.fhfriedberg.mnd.pg.beisp1.*;
enthält, kann der Paketname entfallen. Beim Kompilieren kann man den Ladevorgang durch Angabe der Option -verbose ausgeben lassen:
>javac -verbose FH.java
[parsing started FH.java]
[parsing completed 140ms]
13.5. JAVADOC
133
[loading c:\programme\j2sdk1.4.0-beta3\jre\lib\rt.jar( java/lang/Object.class)]
[loading c:\programme\j2sdk1.4.0-beta3\jre\lib\rt.jar( java/lang/String.class)]
[checking FH]
[loading .\de\fhfriedberg\mnd\pg\beisp1\Vorlesung.class]
[wrote FH.class]
[total 601ms]
13.5
javadoc
Mit dem Tool javadoc lässt sich eine Dokumentation für die eigenen Klassen
erstellen. Standardmäßig erzeugt es eine Reihe von HTML-Dateien, in denen einerseits die einzelnen Klassen und andererseits die Verwandtschaft zwischen den
Klassen dargestellt wird. Bereits ohne besondere Vorbereitung erhält man auf
diese Art und Weise eine nützliche Übersicht über die Klassen sowie ihre Methoden und Variablen. Als Einstiegspunkt wird die Datei index.html generiert. Ein
entsprechender Aufruf ist
javadoc –private *.java
Die Option –private bestimmt, dass auch Elemente mit dem entsprechenden Attribut einbezogen werden. Durch so genannte Dokumentationskommentare lassen
sich leicht weitere Informationen integrieren. Dokumentationskommentare beginnen mit der Markierung /** und enden wie normale Kommentare mit */. javadoc
interpretiert solche Abschnitte als Kommentar für die jeweils nächste unmittelbar
folgende Einheit. Je nach Position kann sich also ein Kommentar beispielsweise
auf eine Klasse oder eine Methode beziehen. Wichtig ist die unmittelbare Folge. Ein häufiger Fehler ist das Einschieben von import-Anweisungen zwischen
Kommentar und Beginn der Klasse. Dann wird die Dokumentation ignoriert.
Betrachten wir ein Beispiel mit Dokumentationen der Klasse und einer Methode:
/**
* Dies ist die Hauptklasse des Beispiels FH-Verwaltung.
*
* @author S. Euler
* @version .9
*
*/
public class FH {
...
/**
* Eine weitere Methode.
134
KAPITEL 13. TOOLS
* Diese Methode gibt es nur,
* um einige Möglichkeiten von javadoc zu zeigen.
* Der Kommentar kann HTML Elemente
* wie <strong>Hervorhebungen</strong> oder
* <br> Zeilenumbrüche enthalten.
*
* @param level Ein erstes Argument
* @param x Ein zweites Argument
*/
public static void test( int level, double x ) {}
}
Ein Kommentar beginnt mit einer Beschreibung. Die erste Zeile des Textes dient
später als Kurzbeschreibung. Der Text kann HTML-Elemente enthalten. Lediglich die Tags für Überschriften <h1> und <h2> sollten vermieden werden, da sie
auch von javadoc verwendet werden. Die Sternchen dienen nur zur Kennzeichnungen des Kommentars und werden von javadoc entfernt. Nach der allgemeinen
Textbeschreibung können markierte Absätze folgen. Jeder solche Abschnitt beginnt mit einem durch ein @ markierten Tag. Beispiele für diese Tags sind author,
since oder param. Sie werden in eigenen Abschnitten mit spezieller Formatierung
wiedergegeben. Standardmäßig werden nicht alle Tags ausgewertet. So muss die
Darstellung des Tags author mit der Option javadoc –author angefordert werden.
13.6
jdb
Der Debugger für Java ist jdb. Es handelt sich dabei um eine Version basierend
auf der Eingabe von Kommandozeilen. Komfortabler ist die Nutzung über eine graphische Oberfläche wie z. B. integriert in NetBeans. Im folgenden ist eine
Beispiel-Session mit jdb wieder gegeben (Eingaben jeweils nach dem Prompt >
bzw. main[1] ).
>jdb FH
Initializing jdb ...
> stop in FH.main
Deferring breakpoint FH.main.
It will be set after the class is loaded.
> run
run FH
>
VM Started: Set deferred breakpoint FH.main
Breakpoint hit: "thread=main", FH.main(), line=23 bci=0
13.6. JDB
23
135
Vorlesung v = new Vorlesung();
main[1] next
>
Step completed: "thread=main", FH.main(), line=24 bci=9
24
Prof p = new Prof( "Christian Müller" );
main[1] list
20
* @param args the command line arguments
21
*/
22
public static void main (String args[]) {
23
Vorlesung v = new Vorlesung();
24 =>
Prof p = new Prof( "Christian Müller" );
25
Tutor t = new Tutor( "Jens Schneider" );
26
Student s = new Student( "geht nicht" );
27
28
v.gehaltenVon( p );
29
v.gehaltenVon( t );
main[1] print v
v = instance of Vorlesung(id=285)
main[1] dump v
v = {
name: "pg"
}
136
KAPITEL 13. TOOLS
Kapitel 14
Ein- und Ausgabe und Dateien
14.1
Einleitung
Der Themenkomplex Ein- und Ausgabe umfasst eine ganze Reihe von Einzelfragen. Wichtige Punkte sind
• Eingabe von Tastatur
• Ausgabe auf Bildschirm, Lautsprecher, Drucker
• Arbeiten mit Dateien
• Zeichensätze (ASCII, Unicode)
• Formatierte und unformatierte Ein- / Ausgabe
• Datenaustausch – gegebenenfalls über ein Netzwerk – mit anderen Prozessen
Java stellt dazu eine Reihe von Klassen und Methoden bereit. Die große Anzahl
von spezialisierten Klassen und die Möglichkeit diese miteinander zu kombinieren
erschweren den Überblick. Im folgenden werden zunächst einige Aspekte anhand
der Standardein- und –ausgabe diskutiert. Anschließend folgt ein Überblick über
die grundlegenden Designideen. Wichtige Klassen und Methoden werden dann
im Detail besprochen. Den Abschluss bildet die Klasse File zum Umgang mit
Dateien.
14.2
Standardeingabe und Standardausgabe
Die Ausgabe in ein Konsolfenster und die direkte Eingabe von der Tastatur spielen bei Java nicht die zentrale Rolle. Konsolanwendungen sind eher die Ausnahme
gegenüber Anwendungen mit graphischen Benutzeroberflächen. Dort stehen andere Möglichkeiten zur Ein- und Ausgabe zur Verfügung. Allerdings kann auch
137
138
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
dann die – eventuell optionale – Ausgabe auf der Konsole ein gutes Hilfsmittel
zur Ausgabe von Statusinformationen sein.
14.2.1
Ausgabe
Zur Ausgabe haben wir bereits häufig die Konstruktion System.out.print ...
benutzt. System ist eine Klasse mit einer Reihe von allgemeinen Klassenvariablen und Klassenmethoden. Darunter sind die Variablen in, err und out. Die
Standardausgabe erfolgt über out – einem PrintStream. Fehlermeldungen können über einen zweiten Ausgabestrom err erfolgen. PrintStream ist eine Klasse,
die mit entsprechenden Methoden eine einfache Ausgabe von verschiedenen Daten ermöglicht. Sie implementiert eine Reihe von Methoden print bzw. println
für die unterschiedlichsten Datentypen. Das Argument wird – falls erforderlich –
durch den Aufruf der passenden Methode String.valueOf in einen String umgewandelt und ausgegeben. Bei den Varianten println wird gleichzeitig die Zeile
abgeschlossen. Einige Beispiele:
Beispiel 14.1 println:
int i = 1234;
double x = 33.44;
double y = -0.123;
boolean b = true;
String text = "Dies ist ein Test";
double[] feld = {1., 2., 3. };
System.out.println(
System.out.println(
System.out.println(
System.out.println(
System.out.println(
System.out.println(
i );
"x = " + x );
y );
b );
text );
feld );
ergibt
1234
x = 33.44
-0.123
true
Dies ist ein Test
[D@1fcc69
Die Ausgabe stellt die Werte in lesbarer Form dar. Interessant ist die Ausgabe des
Feldes. Hier werden nicht etwa die einzelnen Element ausgegeben. Vielmehr wird
intern die Methode toString des Feldobjektes aufgerufen. Der erzeugte String
14.2. STANDARDEINGABE UND STANDARDAUSGABE
139
gibt den Typ des Objektes „Feld von double“ durch [D an und hängt nach dem
@ den so genannten Hash-Code – eine eindeutige Kennung für das Objekt – an.
Einige Eigenschaften dieser Ausgabe sind:
• Die verschiedenen Methoden print haben nur maximal ein Argument. Mehrere Werte können durch Verknüpfung zu einem String in einem Aufruf
ausgegeben werden.
• Es gibt keine Formatierungsmöglichkeit der Zahlendarstellung beim Aufruf.
• Jedes beliebige Objekt kann als Argument übergeben werden.
• Übergibt man eine leere Referenz, so wird der String null ausgegeben.
14.2.2
Formatierte Ausgabe mit printf
fehlt noch
14.2.3
Eingabe
Der Eingabestrom System.in ist nicht direkt zum komfortablen Einlesen gedacht.
Vielmehr muss man zunächst auf den Eingabestrom einen InputStreamReader
setzen. Er hat die Aufgabe, die gelesenen Bytes in Zeichen umzusetzen. Dieser
Zeichenstrom kann dann in einen BufferedReader geleitet werden, der schließlich
Methoden zum Lesen von Strings bereit stellt. Ausführlich kann man schreiben
InputStreamReader isr = new InputStreamReader( System.in );
BufferedReader din = new BufferedReader( isr );
Da der InputStreamReader aber nur an dieser Stelle benötigt wird, kann man auf
die Referenz isr verzichten und statt dessen die Aufrufe ineinander schachteln:
BufferedReader din = new BufferedReader(
new InputStreamReader( System.in )
);
Dann kann mit
String text = din.readLine();
die nächste Zeile gelesen werden. Ist das Ende der Datei erreicht, gibt die Methode
den Wert null zurück. Hinweis: Die Methode readLine kann einen Ausnahmefehler (Exception) hervor rufen. In einem der nächsten Kapitel werden wir sehen,
wie man solche Ausnahmefehler behandelt. Bis dahin werden wir als einfache Lösung die Fehler „weiter reichen“. Dazu muss in jeder Methode, die readLine benutzt, und den jeweils übergeordneten der Hinweis throws Exception eingefügt
werden.
140
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
Es mag überraschen, dass nur Methoden zum Lesen von einzelnen Zeichen
oder ganzen Zeilen implementiert wurden. Es fehlen die aus anderen Programmiersprachen bekannten Möglichkeiten, direkt verschiedene Formate für Integerund Gleitkommazahlen lesen zu können. In Java ist das Vorgehen anders:
1. Einlesen einer Zeile
2. eventuell Aufteilen, Leerzeichen entfernen (Tokenizer)
3. aus den (Teil)-Zeichenketten die Werte entnehmen
Methoden zur Analyse von Zeichenketten sind in den so genannten WrapperKlassen enthalten. Zu jedem primitiven Datentyp gibt es eine Wrapper-Klasse.
Der Name der Wrapper-Klasse stimmt in der Regel mit dem Namen des Datentyps überein, beginnt aber mit einem Großbuchstaben. So sind Float und
Boolean die Wrapper-Klassen für die primitiven Typen float und boolean.
Lediglich bei Character für char und Integer für int unterscheiden sich die
Namen. Jede dieser Klassen implementiert – neben vielen anderen – Methoden
zur Analyse (Parsen) von Zeichenketten. So ist in der Klasse Long
public static long parseLong(String s)
die Methode, um aus einem String einen long Wert zu erhalten. Die Namen
dieser Methoden werden jeweils aus dem Wort parse und dem Typnamen gebildet, also parseBoolean, parseInt, etc. Als Argument erhalten sie einen String
sowie eventuell noch Informationen zur Formatierung (z. B. Zahlensystem). Bei
Erfolg liefern sie einen entsprechenden Wert zurück, ansonsten kommt es zu einem Ausnahmefehler. Da wir die Ausnahmen bisher nicht behandeln, wird bei
einer Ausnahme das Programm beendete und der Java Interpreter gibt eine Fehlermeldung aus.
Beispiel 14.2 Einlesen von Integerwerten:
for( ;; ) {
System.out.print( "> " );
// Zeile einlesen
String text = din.readLine();
// Kontrollausgabe
System.out.println("text( Länge= " + text.length() +"): " + text );
// aus Zeile einen Integerwert lesen
int iwert = Integer.parseInt( text );
System.out.println( "als Integer : " + iwert );
}
14.3. STREAMS, READER UND WRITER
141
> text( Länge= 4): 2333
als Integer : 2333
> text( Länge= 2): -3
als Integer : -3
> text( Länge= 3): 3.4
java.lang.NumberFormatException: 3.4
at java.lang.Integer.parseInt(Integer.java:423)
at java.lang.Integer.parseInt(Integer.java:463)
at IOtest.main(IOtest.java:65)
Mit entsprechenden Methoden in den Wrapper-Klassen kann man die Konvertierung im Detail festlegen und z. B. auch Oktalzahlen einlesen.
14.2.4
Einlesen mit Scanner
fehlt noch
14.3
Streams, Reader und Writer
Ein- und Ausgabe basiert auf dem allgemeinen Konzept von Datenströmen. Aus
Sicht der Anwendung gibt es Datenkanäle zu Quellen und Senken. Durch diese
Kanäle fließt die Information. Die Anwendung befindet sich an einem Ende und
kann Portionsweise Daten einspeisen oder entnehmen. Am anderen Ende des
Stroms können verschiedene Arten von Quellen oder Senken sein:
• Geräte wie Tastaturen oder Bildschirme
• Dateien
• Speicherbereiche
• Sockets (Schnittstellen für Netzwerkanwendungen)
• Pipes (Ein Kommunikationsmechanismus zwischen zwei Prozessen)
Die Verarbeitung soll aber weitgehend unabhängig von der Art des Partners sein.
Das Bild des Datenstroms abstrahiert weitgehend von Implementierungsdetails.
Die Operationen sind nahezu die gleichen. Man kann:
• einen Datenstrom öffnen
• Daten lesen oder schreiben
• den Datenstrom wieder schließen
Java unterscheidet zwischen zwei grundsätzlichen Arten von Datenströmen:
142
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
• basierend auf einzelnen Bytes (8 Bit)
• basierend auf einzelnen Unicode-Zeichen (16 Bit)
Wohl aus historischen Gründen gibt es dafür zwei getrennte Familien von Klassen:
Byte
InputStream
OutputStream
Zeichen (16 Bit Unicode)
Reader
Writer
Die Klassen Reader und Writer – genauer gesagt die daraus abgeleiteten Klassen – werden empfohlen, wenn eine Anwendung Texte lesen und schreiben will.
Möchte man demgegenüber allgemeine Daten oder Objekte wie z. B. Felder von
float Werten oder selbst definierte Objekte nicht in Textform, sondern in einer
kompakten binären Darstellung schreiben und lesen, so sind die Stream-Klassen
einzusetzen. Besonders bei großen Objekten (Bildern, Musikstücke oder ähnliches) ist diese platzsparende Repräsentation angebracht. Falls erforderlich gibt es
darüber hinaus Konverterklassen zwischen Byte- und Zeichenströmen:
Bytestrom
=⇒ InputStreamReader =⇒ Zeichenstrom
Zeichenstrom =⇒ OutputStreamWriter =⇒ Bytestrom
Im folgenden werden die grundlegenden Mechanismen zunächst anhand der Klassen Reader und Writer besprochen.
14.4
Reader
Die Klasse Reader ist der Ausgangspunkt für alle zeichenbasierte Klassen zum
Einlesen. Von der Klasse selbst können keine Instanzen erzeugt werden, sondern
nur von den abgeleiteten Klassen. In der Klasse sind wesentliche Methoden zur
Behandlung eines Eingangstroms definiert. Die wichtigsten sind:
int read()
Lesen eines Zeichens
int read( char[] cbuf ) Lesen eines Feldes von Zeichen
long skip( long n )
Überspringen von Zeichen
void close()
Schließen des Stroms
boolean ready()
Prüfen ob der Strom lesebereit ist
Eine aus Reader abgeleitete Klasse ist FileReader. Damit kann man in einfacher Weise aus Dateien lesen. Wie jede abgeleitet Klasse implementiert sie alle
Methoden von Reader und eventuell weitere. Bei einem der Konstruktoren wird
der Dateinamen direkt angegeben. Die folgende Klasse benutzt diesen Konstruktor, um die Datei studenten.txt zu öffnen. Anschließend werden portionsweise
Zeichen mit der Methode read eingelesen. Ein Rückgabewert von –1 signalisiert
das Ende der Datei.
14.4. READER
public class RWtest
143
{
public void leseDatei() throws Exception {
char[] cbuf = new char[20];
FileReader fr = new FileReader( "studenten.txt" );
int count = 0;
for( ;; ) {
int i = fr.read( cbuf );
if( i == -1 ) break;
count += i;
}
System.out.println( count + " Zeichen gelesen");
}
}
Das Beispiel ist allerdings noch nicht optimal. Bei einem FileReader wird jede
Leseaktion direkt ausgeführt. Dies kann zu häufigen Zugriffen auf die Festplatte führen. Besser ist es daher, den FileReader durch einen BufferedReader
zu ergänzen. Ein BufferedReader übernimmt intern die Zwischenspeicherung
von größeren Datenmengen und minimiert dadurch die Anzahl der Festplattenzugriffe. Außerdem implementiert die Klasse BufferedReader einige nützliche
Methoden.
14.4.1
Schachtelung von Readern
Die Klasse BufferedReader ist direkt aus Reader abgeleitet. Es handelt sich
nicht etwa um eine aus FileReader abgeleitet Klasse. In dem Programm werden
die beiden Reader hintereinander geschaltet. Anschaulich kann man sagen, der
FileReader „fließt“ in den BufferedReader.
Dementsprechend enthält der Konstruktor für den BufferedReader einen anderen Reader als Argument. Wir sehen hier ein Beispiel für den allgemeinen Mechanismus durch Kombination verschiedener Ströme ein gewünschtes Gesamtverhalten zu erzielen. Die entsprechend geänderte Methode main ist dann:
public void leseDatei() throws Exception {
char[] cbuf = new char[20];
FileReader fr = new FileReader( "studenten.txt" );
BufferedReader br = new BufferedReader( fr );
int count = 0;
for( ;; ) {
int i = br.read( cbuf );
if( i == -1 ) break;
count += i;
}
144
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
System.out.println( count + " Zeichen gelesen");
}
Der Programmcode wird etwas eleganter, wenn man
• keine expliziten FileReader einführt
• die Methode readLine zum Lesen einer ganzen Zeile einsetzt
• die for-Schleife mit break durch eine while-Schleife ersetzt
Dann nimmt der Code folgende Form an:
public void leseDatei() throws Exception {
BufferedReader br = new BufferedReader(
new FileReader( "studenten.txt" )
);
int count = 0;
String text = null;
while( (text = br.readLine() ) != null ) {
count += text.length();
}
System.out.println( count + " Zeichen gelesen");
}
14.4.2
Übersicht Reader
Neben dem FileReader gibt es Klassen, um aus anderen Typen von Quellen zu
lesen:
Reader
FileReader
CharArrayReader
StringReader
PipedReader
Quelle
Datei
Feld von Zeichen
Zeichenkette
Pipe
Die Namen der Klassen sind weitgehend selbsterklärend. So liest ein StringReader
aus einem im Konstruktor angegebenen String. Neben diesen nach ihren Quellen
unterscheidbaren Klassen existieren weiter Klassen, die – ähnlich dem BufferedReader
– erweiterte Funktionalitäten bereit stellen:
Reader
BufferedReader
LineNumberReader
PushBackReader
Funktionalität
Pufferung der Daten
Zählen der Zeilennummern
Möglichkeit, einzelne oder mehrere gelesene Zeichen wieder zurück in den Strom zu legen, um sie
später nochmals zu lesen
14.5. WRITER
145
Ein LineNumberReader verfügt über die zusätzliche Methode getLineNumber(),
um die aktuelle Zeilennummer abzufragen. In manchen Anwendungen sind PushBackReader
praktisch. Man kann sozusagen ein Zeichen im Voraus lesen. Falls es nicht mehr
zu dem aktuellen Element passt, legt man es zurück in den Eingangsstrom. Neben
der Unterteilung nach Funktionalitäten kann man die verschiedenen Klassen auch
als Klassenbaum darstellen (Zur besseren Übersicht auf zwei Bäume aufgeteilt):
Reader
StringReader
PipedReader
CharArrayReader
Reader
InputStreamReader
BufferedReader
FilterReader
FileReader
LineNumberReader
PushbackReader
PushBackReader ist abgeleitet aus der allgemeineren Klasse FilterReader.
Diese Klasse dient auch als Ausgangspunkt für eigene Reader oder Filter. Ein
Filter ist dabei ein Reader, dessen Eingang von einem anderen Strom gespeist
wird. Man kann durch Überlagern der Methoden das gewünschte Verhalten in
den eigenen Filter einbauen. Mögliche Anwendungen solcher Filter sind:
• Datenkonversion
• Datenverschlüsselung
• Datenkompression
14.5
Writer
Das entsprechende Gegenstück zur Klasse Reader ist die Klasse Writer. Der
Klassenbaum hat folgendes Aussehen:
Writer
StringWriter
PipedWriter CharArrayWriter
146
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
Writer
OutputStreamWriter
BufferedWriter
FilterWriter
PrintWriter
FileWriter
Wieder gibt es Methoden für die verschiedene Ziele
• Files
• Strings
• Felder von char
• Pipes
Um in eine Datei zu schreiben benutzt man einen FileWriter. Im Konstruktor
kann man direkt den Namen der Datei angeben:
// Öffnen zweier Dateien zum Schreiben
// Im zweiten Fall mit Anhängen an bestehenden Inhalt
FileWriter fw, fwa;
fw = new FileWriter( "test.txt" );
fwa = new FileWriter( "testplus.txt", true );
Falls erforderlich legt der Konstruktor eine neue Datei an. Der FileWriter kann
wiederum zur besseren Performanz in einen BufferedWriter eingebettet werden. Damit lässt sich leicht unser Beispielprogramm zu einer Kopieranwendung
erweitern:
public void kopiereDatei() throws Exception {
BufferedReader br = new BufferedReader(
new FileReader( "studenten.txt" ) );
BufferedWriter bw = new BufferedWriter(
new FileWriter( "copy.txt" ) );
int count = 0;
// Zeichenzähler
String text = null;
while( (text = br.readLine() ) != null )
bw.write( text );
bw.newLine();
// Zeilenumbruch
count += text.length();
}
{
14.6. PRINTWRITER
147
System.out.println( count + " Zeichen gelesen");
bw.close();
// Datei ordentlich schließen
}
Dabei sind zwei Punkte zu beachten:
1. Bei dem zeichenorientierten Arbeiten geht der Zeilenumbruch verloren. Daher muss explizit ein Zeilenumbruch wieder eingefügt werden. Die Verwendung der Methode newLine() ist besser als das Anhängen von "\n". Diese
Methode fügt die für die aktuelle Systemumgebung richtigen Zeichen zum
Zeilenumbruch ein.
2. Der Strom muss mit close() geschlossen werden. Ansonsten besteht die
Gefahr, dass der letzte Datenpuffer nicht geschrieben wird. Alternativ kann
man mit der Methode flush() alle noch im Puffer befindlichen Daten
schreiben.
14.6
PrintWriter
Für eine formatierte Ausgabe von Daten könnte man selbst die Daten in Strings
umwandeln und anschließend durch einen BufferedWriter ausgeben. Einfacher
ist die Verwendung eines PrintWriters. Ein PrintWriter ist einem PrintStream
wie System.out äquivalent. Er stellt für die primitiven Datentypen Methoden
print und println bereit. Das folgende Beispiel benutzt einen PrintWriter,
um die Werte der Sinus-Funktion in eine Datei zu schreiben.
// Ausgabe einer Sinus-Schwingung in Datei
PrintWriter pr = new PrintWriter(
new FileWriter( "sin.txt" ) );
for( int i=0; i<1000; i++ ) {
// Werte in Intervall [0, 2pi] abbilden
double y = Math.sin( i / 1000. * 2 * Math.PI);
pr.println( y );
}
pr.close(); // alles sichern
Die Datei hat dann den Inhalt
0.0
0.006283143965558951
0.012566039883352607
0.018848439715408175
0.02513009544333748
0.03141075907812829
0.03769018266993454
...
148
14.7
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
Streams
Parallel zu der Klassenhierarchie ausgehend von Reader und Writer gibt es weitgehend äquivalente Klassen für InputStream und OutputStream. Als Beispiel
sei hier der Baum für InputStream angegeben (Aus Platzgründen aufgeteilt auf
zwei Bäume und mit abgekürzten Klassennamen):
InputStream
PipedInputS.
ByteArrayInputS.
FileInputS.
ObjectInputS.
SequenceInputS.
InputStream
FilterInputStream
PushbackInputS. LineNumberInputS.
DataInputS.
BufferedInputS.
Im großen und ganzen findet man Klassen mit den gleichen Funktionalitäten
wieder.
14.8
Random Access File
Verbunden mit der Vorstellung von Datenströmen ist ein sequentieller Zugriff. Die
Daten werden nacheinander gelesen oder geschrieben. Zu vielen Anwendungen
passt allerdings das Konzept eines wahlfreien Zugriffs. Insbesondere bei großen
Datenmenge ist es ineffizient, wenn man um zu einem bestimmten Datum zu
kommen, ein große Anzahl von anderen Daten abarbeiten muss. Java stellt für
solche Zwecke eine einfache Form vom Dateien mit wahlfreiem Zugriff – random
access files – zur Verfügung.
Man kann sich eine solche Datei wie ein großes Feld mit einem Positionszeiger
vorstellen. Jede Lese- oder Schreibaktion wirkt auf die nächste Stelle hinter dem
Positionszeiger. Nach der Aktion wird der Zeiger weiter geschoben. Mit entsprechenden Befehlen kann der Positionszeiger verändert werden. Damit kann gezielt
eine ausgewählte Stelle angesprochen werden. Schreibt man an eine Stelle irgendwo in der Datei, so bleiben alle anderen Daten - vor und hinter der Position
- erhalten. Die Klasse RandomAccessFile verfügt über Methoden zum Schreiben und Lesen der primitiven Datentypen. Die Daten werden in einer genormte
14.9. DIE KLASSE FILE
149
plattform-unabhängigen Darstellung abgelegt. Der Dateizeiger kann mit der Methode seek auf eine bestimmte Byte-Position gesetzt werden. Als Beispiel wird
im folgenden eine Datei mit double Werten gefüllt. Anschließend werden ausgewählte Werte verändert. Zunächst wird eine Datei angelegt und sequentiell mit
1000 Werten der Sinus-Funktion gefüllt:
// Datei anlegen und füllen
RandomAccessFile raf =
new RandomAccessFile( "sin.dat", "rw" );
for( int i=0; i<1000; i++ ) {
double x = Math.sin( i / 1000. * 2 * Math.PI);
raf.writeDouble( x );
}
Im zweiten Schritt wird jeder 25. Wert auf 0 gesetzt. Alle anderen Werte bleiben
erhalten.
// jeden 25. Wert auf 0 setzen
for( int i=0; i<1000; i+=25 ) {
raf.seek( i * 8);
// an Position gehen
raf.writeDouble( 0. );
}
Schließlich wird zur Kontrolle die gesamte Datei nochmals gelesen und die Werte
werden in eine zweite Datei geschrieben.
// Random Access File wieder lesen
// und in neue Textdatei kopieren
raf.seek(0);
pr = new PrintWriter( new FileWriter( "sin2.txt" ) );
for( int i=0; i<1000; i++ ) {
pr.println( raf.readDouble() );
}
pr.close();
Die Klasse RandomAccessFile realisiert nur die einfachsten Operationen für einen
wahlfreien Zugriff. So fehlen Möglichkeiten, um einzelne Einträge zu löschen oder
an beliebiger Stelle einzufügen. Außerdem trägt der Anwender selbst die Verantwortung für die richtige Zuordnung der Bytes zu den Daten. Für anspruchsvolle
Anwendungen ist der Einsatz einer Datenbank sinnvoll.
14.9
Die Klasse File
Die Eigenschaften einer Datei lassen sich über die Methoden der Klasse File
abfragen oder zu verändern. Weitere Methoden existieren, um Dateien zu erzeugen, zu löschen oder umzubenennen. Ein Schwerpunkt liegt auf einer abstrakten,
150
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
plattform-unabhängigen Darstellung des Dateinamens als so genannter abstrakter
Pfadnamen. Aus dieser internen Darstellung werden durch Einfügen der aktuellen Trennzeichen konkrete Pfadnamen generiert. Der einfachste Konstruktor hat
die Form
File( String pathname )
wobei pathname den Namen der Datei einschließlich eventueller Pfadangaben
enthält. Mit
File f = new File( "sin2.txt" );
wird für die oben benutzte Datei ein File-Objekt angelegt. Der Pfad kann relativ
zur aktuellen Position oder absolut angegeben werden. Bei WinXX muss das \Zeichen im Namensstring als \\ eingegeben werden. Ein gültiger Namen hat dann
beispielsweise die Form
c:\\Programme\Java\forte4j
Dabei darf das Anlegen des File-Objektes nicht mit dem Anlegen einer Datei
verwechselt werden. Der Aufruf des Konstruktors erzeugt lediglich ein Informationshülle für die eigentliche Datei. Damit kann man dann die Eigenschaften
abfragen. Die Methode exists() testet, ob die Datei überhaupt vorhanden ist.
Im positiven Fall liefert sie den Wert true zurück. Man kann dann verschiedene
Eigenschaften der Datei abfragen:
System.out.println("AbsolutePath="
System.out.println("exists
="
System.out.println("Length
="
System.out.println("canWrite
="
System.out.println("canRead
="
System.out.println("isFile
="
System.out.println("is hidden
="
System.out.println("isDirectory ="
+
+
+
+
+
+
+
+
f.getAbsolutePath());
f.exists());
f.length());
f.canWrite());
f.canRead());
f.isFile());
f.isHidden());
f.isDirectory());
mit z. B. der Ausgabe:
AbsolutePath=c:\Euler\java\small_tests\FileTest.class
exists
=true
Length
=2360
canWrite
=true
canRead
=true
isFile
=true
is hidden
=false
isDirectory =false
14.10. ÜBUNGEN
151
Verzeichnisse werden wie andere Dateien angegeben. In diesem Fall kann über
die Methode list() ein Feld von Strings mit den Namen aller Dateien in diesem
Verzeichnis abgefragt werden:
if (f.isDirectory()) {
String files[] = f.list();
for (int i=0; i<files.length; ++i) {
System.out.println(" "+files[i]);
}
}
Mit weiteren Methoden aus dieser Klasse kann man
• Informationen über das Dateisystem und Trennzeichen abfragen
• Eigenschaften von Dateien ändern
• Dateien und Verzeichnisse erzeugen, umbenennen oder löschen
• temporäre Dateien anlegen
• Dateinamen in URIs oder URLs konvertieren
In vielen Fällen – auch bei Methoden in anderen Klassen – kann ein File-Objekt
anstelle eines Dateinamens verwendet werden. So gibt es beispielsweise in der
Klasse FileWriter ansonsten gleichwertige Konstruktoren mit entweder einem
Dateinamen als String oder einem File-Objekt als Argument.
14.10
Übungen
Übung 14.1 Lesen aus einer Datei
Ihre DVD-Sammlung ist in einer Datei festgehalten. In jeder Zeile steht der Name
des Films und der Kaufpreis in der Art
Dschungelbuch
17.99
Herr der Ringe, Teil 2 21.90
Vom Winde verweht 12.99
...
Schreiben Sie eine Methode
int preise( String dateiName )
zum Lesen einer solchen Datei. Dabei soll eine kleine Statistik erstellt werden.
Am Ende gibt die Methode die Meldung
nn DVDs, Durchschnittspreis: xx, Gesamtpreis: yy
mit den gefundenen Werten für nn, xx und yy aus. Rückgabewert ist die Anzahl
der DVDs.
152
KAPITEL 14. EIN- UND AUSGABE UND DATEIEN
Kapitel 15
Exception
15.1
Einleitung
Bei der Ausführung einer Anwendung kann es zu Fehlern kommen. Neben Fehlern im Programm können auch äußere Umstände zu einem Fehlverhalten oder
Programmabsturz führen. Häufige Ursachen sind:
• Fehlbedienungen (z. B. falsche Eingaben)
• unzureichende Ressourcen (z. B. zu wenig Speicher, unterbrochene Netzverbindung)
Um ein Programm gegen derartige Fehler zu sichern, muss entsprechender Code
eingefügt werden. Diese Aufgabe ist schwierig und aufwändig. Im konkreten Fall
ist zu entscheiden:
• kann ein Fehler repariert werden oder ist es sinnvoll, die Anwendung zu
beenden?
• wo sollte der Fehler behandelt werden?
• an welcher Stelle soll das Programm fortgesetzt werden?
Generell kann man zwischen zwei Strategien unterscheiden.
1. Der Fehler wird „an Ort und Stelle“ mit normalen Programmiermethoden
behandelt. Ein Beispiel ist die Prüfung, ob eine Datei mit dem angegebenen Namen vorhanden ist. Falls nicht, wird der Benutzer erneut nach dem
Namen gefragt.
2. Ein Fehler wird festgestellt, kann aber nicht behoben werden (z. B. Zugriff
außerhalb der Grenzen eines Feldes). Daher wird der Fehler gemeldet und
kann dann unabhängig vom normalen, linearen Programmfluss behandelt
werden.
153
154
KAPITEL 15. EXCEPTION
Java verfügt über ein flexibles Konzept zur Fehlerbehandlung. Damit ist es möglich, Fehler zu erkennen und entweder selbst zu behandeln oder an die aufrufende
Instanz weiter zu melden.
15.2
Beispiel
Die Laufzeitfehler werden in Java als Exceptions bezeichnet. Wie der Name andeutet, stellen sie Ausnahmen vom normalen Ablauf dar. Bisher hatten wir die
Exceptions nicht behandelt sondern lediglich in den Methoden deklariert, dass
sie eventuell einen solchen Ausnahmefehler hervor rufen könnten (to throw an
exception). Betrachten wir ein Beispiel aus dem Szenario Autovermietung. Die
Fahrzeuge sollen in einer Datei auto.txt mit dem Inhalt von beispielsweise
Sharan
Golf
Jaguar
29999
19999
55000
2000
2001
1998
45000
36777
15666
eingetragen sein. Dann soll die Methode parseLine die einzelnen Zeilen analysieren:
void parseLine( String zeile ) throws Exception {
StringTokenizer st = new StringTokenizer( zeile );
name
= st.nextToken();
kaufPreis = Integer.parseInt( st.nextToken() );
kaufJahr
= Integer.parseInt( st.nextToken() );
kilometer = Integer.parseInt( st.nextToken() );
}
Mit der Angabe throws Exception wird dem Compiler mitgeteilt, dass in
der Methode ein Ausnahmefehler auftreten kann. Dieser Fehler wird von der
Methode selbst nicht behandelt, sondern an die aufrufende Methode gemeldet.
Auf diese Art und Weise können die Fehler von allen übergeordneten Methoden
einschließlich main weiter gereicht werden. Dann übernimmt die Java-Maschine
die Behandlung. Fügt man in die Datei auto.txt eine ungültige Zeile in der Art
Sharan 29999 2000 45000
Golf
19999 2001 36777
ungültig
15000
Jaguar 55000 1998 15666
ein, so tritt in dieser Zeile bei dem dritten Aufruf von nextToken ein Fehler auf.
Die normale Ausführung wird dann unterbrochen und die Exception bis zur JavaMaschine durchgereicht. Diese gibt dann eine Fehlermeldung mit der Ursache und
dem Ort aus:
15.3. TRY - CATCH ANWEISUNG
155
java.util.NoSuchElementException
at java.util.StringTokenizer.nextToken( StringTokenizer.java:235)
at Auto.parseLine(Auto.java:37)
at Verleih.ausDatei(Verleih.java:36)
at Verleih.main(Verleih.java:66)
15.3
try - catch Anweisung
Alternativ zu der Weitergabe kann man selbst Code zur Behandlung solcher Fehler bereit stellen. Dies geschieht mittels einer try - catch Anweisung. Die Anweisung muss die kritische Stelle umfassen, kann aber ansonsten frei platziert werden.
Entweder sie steht in der Methode selbst oder in einer der aufrufenden Methoden. Generell gilt, dass Fehler behandelt werden müssen. Entweder die Methode
kümmert sich selbst darum oder gibt den Fehler weiter (catch-or-throw Regel).
Tritt zur Laufzeit ein Fehler auf, so sucht die Java-Maschine nach einem entsprechenden catch-Teil. Die Suche beginnt in der aktuellen Methode. Wird dort
nichts gefunden, so wird die Suche in der aufrufenden Methode fortgesetzt. Die
Suche wird weitergeführt, bis die oberste Ebene main erreicht ist. Ist auch dort
keine Behandlung vorgesehen, wird die Anwendung beendet und eine Meldung
ausgegeben.
Kriterium für die Platzierung sollte sein: „Kann man an dieser Stelle den
Fehler angemessen behandeln? “ In dem Beispiel kann die Methode parseLine
wenig mit dem Fehler anfangen. Es ist sinnvoll, den Fehler „nach oben“ weiter zu
reichen. Mit folgendem Code wird der Fehler in main behandelt:
String datei = "autos.txt";
try {
System.out.println( "Datei <" + datei + "> lesen" );
v.ausDatei( datei );
}
catch ( Exception ex ) {
System.out.println( "Fehler beim Lesen <"+datei+">" );
System.out.println( "Exception : " + ex );
System.exit(0);
}
Zunächst kommt ein try-Block, der alle kritischen Anweisungen enthält. Eventuelle Fehler werden in einem direkt anschließenden catch-Block aufgefangen.
Genauer gesagt unterbricht ein Fehler die normale Ausführung in dem try-Block
und die Anwendung springt an den Anfang des catch-Blocks. Der Block wird
durch das Schlüsselwort catch sowie einen formalen Parameter eingeleitet. In
dem Beispiel ist als formaler Parameter eine allgemeine Exception angegeben.
Wie bei einem Methodenaufruf übernimmt der Block bei der Ausführung ein
156
KAPITEL 15. EXCEPTION
konkretes Objekt. In diesem Exception-Objekt sind Informationen zu dem aufgetretenen Fehler gespeichert. Die Methode toString (hier implizit durch die
Verkettung der Zeichenketten aufgerufen) liefert eine Textinformation zu der Art
des Fehlers. Dann resultiert folgende Ausgabe:
Datei <autos.txt> lesen
Sharan: gekauft im Jahr 2000 für 29999 Euro
Kilometerstand: 45000
Golf: gekauft im Jahr 2001 für 19999 Euro
Kilometerstand: 36777
Fehler beim Lesen aus Datei <autos.txt>
Exception : java.util.NoSuchElementException
Nach dem Ende des catch-Blocks wird die Anwendung an dieser Stelle („hinter“ der try - catch Anweisung) fortgesetzt. Es gibt keine Möglichkeit, an die
Stelle zurück zu gehen, an der der Fehler auftrat. Für weiter gehende Informationen kann auch die genaue Position des Fehlers angezeigt werden. Die Methode
printStackTrace() gibt die entsprechende Information auf dem Standard Error
Stream aus. In dem Beispiel sieht die Meldung folgendermaßen aus:
java.util.NoSuchElementException
at java.util.StringTokenizer.nextToken(StringTokenizer.java:235)
at Auto.parseLine(Auto.java:37)
at Verleih.ausDatei(Verleih.java:36)
at Verleih.main(Verleih.java:97)
Angezeigt wird der komplette Stapel (Stack) mit Methodenaufrufen, wie er zum
Zeitpunkt des Fehlers vorlag. Hilfreich ist die Angabe der Zeilennummern, mit
deren Hilfe die kritische Stelle im Quellcode sofort nachgeschaut werden kann.
Die Syntax der try - catch Anweisung erlaubt eine Trennung zwischen „normalem“ Code und Code zur Fehlerbehandlung. Es ist möglich, längere Code-Stücke
zusammenhängend zu schreiben und die Fehlerbehandlung für den gesamten Abschnitt daran anzuschließen. Damit wird die Lesbarkeit stark verbessert. Den
catch-Block bezeichnet man nach seiner Funktion auch als Exception Handler.
15.4
Hierarchie von Ausnahmefehlern
In einer Anwendung können verschiedenste Fehler auftreten. So kann zum Beispiel die Methode readLine der Klasse LineNumberReader eine IOException
verursachen. Die Dokumentation spezifiziert für jede Methode, welche Arten von
Fehlern auftreten können. Die Fehler - genauer gesagt die Klassen zur Beschreibung der Fehler - sind hierarchisch organisiert. Die Basisklasse für alle Fehler ist
Throwable. Nur Instanzen dieser Klasse oder daraus abgeleiteter Klassen können
als Parameter an den catch-Block übergeben werden. Danach verzweigt sich der
Ableitungsbaum zu den zwei Klassen Error und Exception.
15.4. HIERARCHIE VON AUSNAHMEFEHLERN
15.4.1
157
Die Klasse Error
In dieser Klasse sind alle schwerwiegenden Fehler enthalten. Normalerweise wird
eine Anwendung diese Fehler nicht behandeln. Beispiele sind VirtualMachineError
oder StackOverflowError. In diesen Fällen treten schwerwiegende Problem mit
der Java-Maschine oder den benötigten Klassen auf. Eine Anwendung hat in solchen Fällen wenig Möglichkeiten zur Reaktion. Fehler der Klasse Error brauchen
nicht mit throws deklariert zu werden.
15.4.2
Die Klasse Exception
Unter dieser Oberklasse finden sich alle Fehler, die eine Anwendung sinnvollerweise behandeln kann. Die Fehler sind in mehreren Ebenen in Unterklassen organisiert. Als Beispiel hat FileNotFoundException folgenden Ableitungsbaum:
java.lang.Object
java.lang.Throwable
java.lang.Exception
java.io.IOException
java.io.FileNotFoundException
Für die Exception greift der Mechanismus Polymorphismus. Eine speziellere Exception kann einer allgemeineren Exception zugewiesen werden. Wir haben bisher
dieses Verhalten ausgenutzt, indem wir stets die allgemeinste Klasse Exception
spezifiziert hatten. Damit wurden die verschiedenen speziellen Fehler aufgefangen.
Auch wenn z. B. die angegebene Datei nicht existiert und damit eine FileNotFoundException
ausgelöst wurde, so wurde dieser Fehler in dem allgemeinen catch-Block aufgefangen. Wir können den catch-Block spezialisieren, indem wir gezielt eine Unterklasse
angeben. Mit
} catch ( NoSuchElementException ex ) {
...
}
wird nur noch diese spezielle Fehlerklasse (und eventuelle Unterklassen) behandelt. In einer try - catch Anweisung können mehrere catch-Blöcke für verschieden
Fehlerklassen stehen. Beispiel:
try {
System.out.println( "Datei <" + datei + "> lesen" );
v.ausDatei( datei );
}
catch ( NoSuchElementException ex ) {
System.out.println("Fehler beim Lesen: <"+ datei +">" );
System.exit(0);
158
KAPITEL 15. EXCEPTION
}
catch ( FileNotFoundException ex ) {
System.out.println("Datei <"+datei+"> nicht gefunden" );
System.exit(0);
}
Dabei gelten folgende Regeln:
• Jeder catch-Block kann nur eine Klasse behandeln.
• Speziellere Handler müssen vor allgemeineren stehen (wird durch Compiler
geprüft).
• Wenn die Methode dies mit throws ... deklariert, brauchen nicht alle
Fehler behandelt zu werden.
Entsprechendes gilt für die Deklaration throws bei einer Methode. Anstelle des
allgemeinen throws Exception können die möglichen Fehler einzeln benannt
werden. Mehrere Fehlerklassen werden durch Komma getrennt.
15.4.3
Die Klasse RuntimeException
Eine besondere Rolle spielt die Fehlerklasse RuntimeException. Dies sind Fehler,
die nahezu überall auftreten können. Darunter fallen
• nicht initialisierte Referenzen: NullPointerException
• Zugriffe außerhalb eines Feldes: IndexOutOfBoundsException
• arithmetische Fehler wie Division durch Null: ArithmeticException
Es wäre sehr aufwendig und wenig hilfreich überall diese Fehler als möglich zu
deklarieren. Daher sind alle Fehlerklassen, die auf RuntimeException basieren,
von der catch-or-throw Regel ausgenommen. Sie können allerdings auch explizit
behandelt werden. Ansonsten werden sie von der Java-Maschine angezeigt.
Beispiel 15.1 Zugriff außerhalb eines Feldes
int[] feld = new int[20];
try {
feld[40] = 4;
}
catch( Exception ex ) {
System.out.println( "Exception : " + ex );
}
ergibt
Exception : java.lang.ArrayIndexOutOfBoundsException
15.5. EIGENE EXCEPTIONS
15.5
159
Eigene Exceptions
In den Exception-Mechanismus können eigene Fehlertypen integriert werden. Ein
Ausnahmefehler wird mit dem Befehl throw ausgelöst. Im einfachsten Fall benutzt man eine der vorhandenen Fehlerklassen und trägt einen eigenen Meldungstext ein. So könnte eine Sicherheitsabfrage über die korrekte Preiseingabe in der
Form
if( kaufPreis < 0 ) throw new Exception("Falscher Preis");
eingebaut werden. Im Fehlerfall würde die Exception von dem allgemeinen catchBlock gefangen werden:
Allgemeiner Fehler bei Datei <autos.txt>
Exception : java.lang.Exception: Falscher Preis
Weiterhin ist es möglich, eigene Fehlerklassen zu erzeugen. Ein Beispiel ist
public class FalschesJahrException
extends java.lang.Exception {
public FalschesJahrException() { }
public FalschesJahrException(String msg) {
super(msg);
}
}
und
if( kaufJahr < 1900 | kaufJahr > 2002 ) {
throw new FalschesJahrException( "Jahr " + kaufJahr );
}
mit dem Ergebnis
Allgemeiner Fehler bei Datei <autos.txt>
Exception : FalschesJahrException: Jahr 12000
15.6
finally-Block
Eine try - catch Anweisung kann mit einen optionalen finally-Block abgeschlossen werden. Der Block steht nach dem letzten catch-Block. Unabhängig davon,
wie die Anweisung verlassen wird – normal oder durch einen Ausnahmefehler –
wird der finally-Block ausgeführt. Dies gilt selbst dann, wenn der try-Block
durch return, break oder continue vorzeitig beendet wird oder die Exception nur weiter gereicht wird. Damit ist der finally-Block der ideale Platz für
allgemeine „Aufräumarbeiten“. Betrachten wir folgendes Beispiel:
160
KAPITEL 15. EXCEPTION
try {
lnr = new LineNumberReader( new FileReader( datei ) );
for( int i=0; i<autos.length; i++ ) {
autos[i] = new Auto();
autos[i].parseLine( lnr.readLine() );
}
}
catch (Exception ex ) {
System.out.println( "Fehler beim Lesen <" +datei+ ">" );
return -1;
}
Angenommen die Datei wird zwar erfolgreich geöffnet, aber beim Lesen der Zeilen
kommt es zu einem Fehler. In diesem Fall wird der try-Block abgebrochen und
der catch-Block ausgeführt. Die Datei würde aber geöffnet bleiben. Durch eine
Block in der Art
finally {
System.out.println( "Datei schliessen" );
lnr.close();
}
kann sichergestellt werden, dass die Datei geschlossen wird. Der finally-Block
wird in jedem Fall ausgeführt. Durch diese Konstruktion braucht der Code zum
gesicherten Freigeben von Ressourcen nur einmal geschrieben zu werden. Bei
umfangreichem Code wird das Programm dadurch übersichtlicher und eine Fehlerquelle wird vermieden.
15.7
Übungen
Übung 15.1 Gegeben sei der folgende Code:
// Klasse ExTest
import java.util.*;
import java.io.*;
public class ExTest {
public static void main(String[] args) throws Exception{
BufferedReader din = new BufferedReader(
new InputStreamReader( System.in ) );
for( ;; ) {
System.out.print( "> " );
15.7. ÜBUNGEN
161
// Zeile einlesen
String text = din.readLine();
test( text );
}
}
static void test( String zeile ) {
String[] teile = zeile.split(" ");
for(int i = 0; i<teile.length; i++ ) {
int wert = Integer.parseInt( teile[i] );
System.out.println( i + ". wert: " + wert );
}
}
}
• Welche Fehler können bei geänderter Eingabe auftreten?
• Behandeln Sie diese Fehler mittels try-catch Blocks in main.
• Falls eine Zahl den Wert 0 annimmt, soll ein neuer Ausnahmefehler angezeigt werden. Implementieren Sie dazu eine Klasse WertNullException.
162
KAPITEL 15. EXCEPTION
Kapitel 16
Dynamische Datenstrukturen
Diese Beschreibung bezieht sich auf traditionelle Collections, die es seit JDK 1.0
gibt. Seit 1.2 gibt es ein neues Collection-API. Das Kapitel müsste aktualisiert
werden. Es fehlen insbesondere Generische Datentypen in der Art
Vector<SpielObjekt> gegenstaende = new Vector<SpielObjekt>();
und Autoboxing.
16.1
16.1.1
Vector
Konstruktor und Einfügen von Elementen
Eine Realisierung einer linearen Liste ist die Klasse Vector. Die wesentlichen
Eigenschaften im Unterschied zu einem Feld sind:
• dynamisches Wachstum
• Einfügen und Löschen von Elementen
Im einfachsten Fall wird ein leerer Vektor mit dem parameterlosen Konstruktor
erzeugt:
Vector v = new Vector();
Elemente können mit verschiedenen Methoden eingefügt werden. Die wichtigsten
sind:
• boolean add(Object o) Anhängen eines Elementes
• void add(int index, Object element) Einfügen eines Elementes an der
angegebenen Stelle
• Object set(int index, Object element) Ersetzen des Elementes an der
angegebenen Stelle
163
164
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
Daneben stehen Methoden zur Verfügung, um mehrere Elemente beispielsweise
aus einem Feld gleichzeitig einzufügen. Die Elemente müssen nicht vom gleichen
Typ sein. Ein Vector kann gleichzeitig Objekte unterschiedlicher Klassen enthalten. In unserer Beispielsapplikation bietet es sich an, die Autos des Verleihs in
einem Vektor zu speichern. Der neue Code hat dann folgendes Aussehen:
public class Verleih2{
Vector
autos = new Vector();
...
public void ausDatei( String datei ) throws Exception {
LineNumberReader lnr;
lnr = new LineNumberReader(new FileReader(datei) );
String zeile;
while( (zeile = lnr.readLine()) != null ) {
Auto a = new Auto();
a.parseLine( zeile );
autos.add( a );
}
lnr.close();
}
...
Jetzt ist es möglich, die Datei einzulesen und direkt die Elemente in den Vektor
abzulegen. Der Vektor wächst nach Bedarf.
16.1.2
Zugriff auf Elemente
Auf die Elemente in einem Vektor kann entweder über den Index oder sequentiell
zugegriffen werden. Die Verwendung des Index betont die Analogie zum Feld.
Einige Methoden dazu sind
• Object firstElement() Liefert das erste Element.
• Object lastElement() Liefert das letzte Element.
• Object get(int index) Liefert das Element an der angegebenen Stelle.
Die Methoden geben jeweils Objekte der Basisklasse Object zurück. Gemäß dem
Polymorphismus kann darin ein beliebiges Objekt enthalten sein. Zur speziellen Verwendung muss das Objekt mit einem Cast auf den gewünschten Typ
gewandelt werden. Als Element in einem Vektor verliert ein Objekt seine spezielle Klassenzuordnung. Es liegt in der Verantwortung des Programmierers bei
der Entnahme die richtige Zuordnung zu treffen. In Zweifelsfällen kann dazu die
16.1. VECTOR
165
Klasseninformation eines Objektes abgefragt werden (z. B. über instanceof oder
die Methode toString() ). Fehlerhafte Zuweisungen führen zur Laufzeit zur einer Exception. Solche Fehler können aber noch nicht zur Compilezeit detektiert
werden. Eine Schleife zur Ausgabe des Vektors mit Autos ist
void printAutos() {
for( int i=0; i<autos.size(); i++ ) {
( (Auto) autos.elementAt(i)).print();
}
}
Die Elemente werden in einer Schleife geholt, in den Typ Auto gewandelt und
dann ausgegeben. Die Bedingung in der Schleife nutzt die Methode
public int size()
zur Abfrage der Größe des Vektors. Elemente können auch wieder aus dem Vektor
entfernt werden:
• Object remove(int index) Löscht das Element an der angegebenen Stelle
und gibt es zurück.
• boolean remove(Object o) Löscht das angegebene Objekt. Der Rückgabewert informiert, ob ein solches Objekt gefunden wurde.
• void removeAllElements() Löscht alle Elemente.
Der Aufruf der Methode toString() liefert eine Textrepräsentation des Vektors
mit einer Liste aller Elemente:
System.out.println( "Vektor autos: " + autos);
Vektor autos: [Auto@2e000d, Auto@55af5, Auto@169e11]
16.1.3
Iterator
Bei der Klasse Vector hatten wir eine Ausgabe aller Elemente mit einer forSchleife über alle Indices realisiert. Bei anderen Strukturen ist die Reihenfolge
weniger leicht zugänglich. Java bietet daher ein allgemeines Konzept für sequentielle Abfragen. Bei Vektoren wird dies durch das Interface Enumeration realisiert.
Eine Enumeration liefert ähnlich wie ein StringTokenizer nach der Initialisierung ein Element nach dem anderen. Für den Vektor autos lässt sich schreiben
Enumeration en = autos.elements();
while( en.hasMoreElements() ) {
( (Auto) en.nextElement() ).print();
}
166
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
oder kompakt mit einer for-Schleife
for(Enumeration e=autos.elements(); e.hasMoreElements();) {
( (Auto) e.nextElement() ).print();
}
Der Aufruf autos.elements() liefert eine Enumeration (genauer gesagt eine
anonyme Klasse aus Vector, die dieses Interface implementiert). Anschließend
werden die Elemente nacheinander ausgegeben. Die Abfrage, ob noch weitere
Elemente vorhanden sind, erfolgt durch die Methode hasMoreElements().
16.1.4
Wrapper Klassen
Vektoren können beliebige Objekte aufnehmen. Es ist allerdings nicht möglich,
primitive Datentypen direkt in einen Vektor zu speichern. Die Anweisung
v.autos.add( 5 );
// geht in alten Java Versionen nicht
liefert einen Fehler beim Kompilieren (no method found . . . ). Dies ist ein Beispiel
für einen Kontext, in dem Objekte benötigt werden. Für solche Fälle existieren
die sogenannten Wrapper-Klassen. Sie bilden eine Objekt-Hülle um einen primitiven Datentyp. Wir hatten Wrapper-Klassen bereits im Kapitel 14 kennen gelernt.
Die Namen der Klassen entsprechen im wesentlichen denen der primitiven Datentypen und werden konventionsgemäß mit einem großen Anfangsbuchstaben
gebildet. Mit einem primitiven Datenwert als Parameter erzeugt der Konstruktor
ein passendes Klassenobjekt. Diese Klassenobjekte können dann als Elemente in
Vektoren eingebaut werden:
Integer wi = new Integer(5);
v.autos.add( wi );
Aus den Klassenobjekte können die Daten mit Zugriffsmethoden in der Form
typeValue() extrahiert werden. Für das Beispiel kann man schreiben
int i = wi.intValue();
Autoboxing übernimmt diese Arbeit!
Vector<Integer> v = new Vector<Integer>();
v.add( 5 );
Integer I = 7;
v.add( I );
System.out.println( v );
16.1. VECTOR
16.1.5
167
Stack
Ein Stapelspeicher (engl. Stack ) ist ein Speicher, bei dem Werte nur am Anfang
angefügt oder entnommen werden können. Anschaulich kann man sich die Werte
übereinander liegend wie bei einem Stapel Spielkarten oder Mensatabletts vorstellen. Man kann jeweils eine weitere Karte bzw. ein weiteres Tablett auflegen
oder vom Stapel nehmen. Diese beiden Operationen nennt man push (Auflegen)
und pop (Wegnehmen). Charakteristisch für ist, dass die Elemente in umgekehrter Reihenfolge entnommen werden (Last In First Out, LIFO). Aus der Klasse
Vector ist die Klasse Stack abgeleitet. Sie stellt vier Methoden bereit, die das
Verhalten eines Stapels charakterisieren:
• Object push(Object item) Legt ein Objekt auf den Stapel.
• Object pop() Nimmt das oberste Objekt vom Stapel.
• Object peek() Liefert das oberste Objekt ohne es zu entnehmen.
• boolean empty() Testet, ob der Stapel leer ist.
Das folgende Beispiel illustriert das Verhalten eines Stapels:
public class StackTest {
public static void main (String args[]) {
Stack st = new Stack();
st.push( "Eins" );
st.push( "Zwei" );
st.push( "Drei" );
while( ! st.empty() ) {
System.out.println( st.pop() );
}
}
}
Ausgabe:
Drei
Zwei
Eins
Es sei betont, dass die Klasse Stack die Klasse Vector erweitert. Alle Methoden
von Vector werden geerbt. Damit kann auf einen Stack wie auf einen Vector
zugegriffen werden.
168
16.2
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
Assoziativspeicher
In vielen Anwendungsfällen kann man einen Datensatz als ein Paar von Schlüssel
(Suchbegriff) und Wert sehen. Beispiele sind:
• Wörterbücher
• Telefonbücher
• Abkürzungsverzeichnisse
Charakteristisch ist, dass der Zugriff in der Regel über einen Schlüssel erfolgt. Die
Daten sind dann so organisiert, dass die Suche in den Schlüsseln schnell erfolgen
kann. Bei Telefonbüchern sind die Schlüssel die Namen. Die Einträge sind alphabetisch sortiert, so dass man einen Namen rasch findet. Die umgekehrte Suche
– der Name zu einer Nummer – ist demgegenüber sehr aufwändig. Da diese Art
von Anwendung häufig vorkommt, lohnt sich der Einsatz von dafür optimierten
Datenstrukturen. Einige Programmiersprachen stellen dazu assoziative Felder bereit. Bei dieser Art von Feldern erfolgt der Zugriff nicht über einen Index sondern
direkt über einen Schlüssel. In der Sprache Perl beispielsweise werden solche Felder unmittelbar durch die Schlüssel adressiert. Das folgende Beispiel Telefonbuch
zeigt die Syntax:
# Aufbau des Telefonbuchs
$dict{"Michael Maier"} =
$dict{"Yvonne Schmidt"} =
$dict{"Pizzeria"}
=
$dict{"Joachim"}
=
$dict{"Jörg"}
=
$dict{"Dekanat MND"}
=
"0788 888 999";
"0179 444 234";
"876878";
"76543";
"12345";
"07887 77889";
# Beispiel für einen Zugriff
$key = "Joachim";
print "$key hat die Telefonnummer $dict{$key} \n";
In Java ist dieses Sprachelement nicht enthalten. Statt dessen gibt es eine Reihe
von Klassen zur effizienten Speicherung solcher Daten.
16.2.1
Hashtable
Die Datenstruktur Hash Table (hash, engl. für zerhacken, vermischen) ist für
einen schnellen Zugriff über einen Schlüssel gedacht. Die Grundidee ist, aus dem
Schlüssel einen Index – d. h. eine Zahl in einem vorgegebenen Intervall – zu berechnen. Dieser Index dient dann zum schnellen Zugriff auf den Wert. Eine Voraussetzung dabei ist die Eindeutigkeit des Schlüssels. Zu einem Schlüssel darf es
16.2. ASSOZIATIVSPEICHER
schlüssel1
169
schlüssel2
$
index
/ W ert
? index
/ W ert
index
/ W ert
index
/ W ert
index
/ W ert
index
/ W ert
Abbildung 16.1: Aufbau einer Hashtable
nur einen Wert geben. Zum Beispiel darf das Telefonbuch nicht zwei identische
Namen mit unterschiedlichen Nummern enthalten. Eine Hash Table besteht aus
zwei Komponenten:
• einem Feld mit linearer Adressierung
• einer Hash-Funktion zur Abbildung der Schlüssel auf Indices
Dieser Aufbau ist in Bild 16.1 dargestellt. Die Hash-Funktion berechnet aus einem Objekt eine Adresse aus dem vorgegebenen Bereich (Hash-Code). Dazu wird
zunächst das Objekt in eine Zahl umgewandelt. Beispielsweise kann man bei Zeichenketten die Zahlenwerte der einzelnen Zeichen addieren. Da diese Zahl im
Allgemeinen außerhalb des Adressbereichs liegen kann, wird sie noch auf diesen
Bereich abgebildet. Dies kann mit der Modulo-Funktion geschehen. Wichtig für
die Hash-Funktion ist:
• kurze Rechenzeit
• möglichst gleichmäßige Verteilung der erzeugten Werte
Ein Vorteil ist, dass die Größe des Feldes keinen Einfluss auf die Rechendauer
bei der Bestimmung des Hash-Codes hat. Damit ist die Zugriffszeit weitgehend
unabhängig von der Größe der Tabelle. Mit anderen Worten: eine Hash Table
bietet eine näherungsweise konstante Zugriffszeit. Verschiedene Objekte können
allerdings den gleichen Hash-Code ergeben. Daher müssen Strategien zur Behandlung von mehreren Objekten mit identischem Hash-Code implementiert werden.
Eine Möglichkeit besteht darin, an jedem Eintrag in dem Feld nicht nur ein einzelnes Objekt sondern eine Liste aller Objekte mit dem zugehörigen Hash-Code
einzubauen. Die Hash-Funktion übernimmt dann die Vorauswahl der Listen.
Wichtig für die Performanz einer Hash Table ist die Größe des Feldes. Ist das
Feld zu klein, so teilen sich zu viele Objekte den gleichen Hash-Code. Ist umgekehrt das Feld zu groß, so wird Speicherplatz verschwendet. Die Implementierung
170
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
in Java verwendete eine Standardgröße von 11 beim Anlegen des Feldes. Erreicht
die Tabelle einen bestimmten Füllstand, so wird das Feld automatisch vergrößert
und die Einträge entsprechend neu angeordnet.
Mit der Methode Object put(Object key, Object value) werden SchlüsselWert-Paare in eine Tabelle eingetragen. War bereits ein Wert zu diesem Schlüssel
vorhanden, so wird der alte Wert zurück gegeben und gleichzeitig durch den neuen ersetzt. Ansonsten ist der Rückgabewert null. Mit Object get(Object key)
wird der Wert zu einem Schlüssel abgefragt. Die Implementierung ist für einen
schnellen Zugriff über einen Schlüssel gedacht. Allerdings stellt die Klasse auch
Methoden für den langsameren Zugriff auf die Werte zur Verfügung. So erhält
man mit elements() einen Iterator über alle Werte. Ein Beispiel für den Einsatz
der Klasse zeigt die folgende Implementierung eines Telefonbuchs.
import java.io.*;
import java.util.*;
public class HashTest extends Object {
public static void main (String args[])
throws Exception{
// Anlegen
Hashtable telefonBuch = new Hashtable();
// füllen
telefonBuch.put(
telefonBuch.put(
telefonBuch.put(
telefonBuch.put(
telefonBuch.put(
telefonBuch.put(
"Michael Maier", "0788 888 999" );
"Yvonne Schmidt","0179 444 234" );
"Pizzeria", "876878" );
"Joachim", "76543" );
"Jörg", "12345" );
"Dekanat MND", "07887 77889" );
// abfragen
BufferedReader br = new BufferedReader(
new InputStreamReader( System.in )
);
for( ;; ) {
System.out.print( ">" );
String name = br.readLine();
if( name.length() == 0 ) break;
String nummer = (String) telefonBuch.get(name);
if( nummer == null ) {
nummer = "Kein Eintrag gefunden";
16.2. ASSOZIATIVSPEICHER
}
System.out.println( name + ": " +
171
nummer );
}
System.out.println( "ENDE" );
}
}
Ein anderes Anwendungsbeispiel nutzt eine Hash Table, um die Häufigkeit von
Zeichenketten zu zählen.
Hashtable ht = new Hashtable();
for( ;; ) {
System.out.print( ">" );
String text = br.readLine();
if( text.length() == 0 ) break;
if( ht.containsKey( text ) ) { // schon in Tabelle?
// Zähler erhöhen und neuen Stand eintragen
int count = ((Integer) ht.get(text)).intValue() + 1;
ht.put( text, new Integer( count ));
System.out.println( text + ": " + count );
} else {
ht.put( text, new Integer(1));
}
}
16.2.2
Properties
Aus Hashtable abgeleitet ist die Klasse Properties. Sie ist auf die Aufnahme
von Zeichenketten spezialisiert. Wie bei Hashtable erfolgt der Zugriff über einen
Schlüssel. Mit dem Methodenpaar
public Object setProperty(String key, String value)
public String getProperty(String key)
werden Eigenschaften gesetzt und gelesen. Im folgenden Beispiel werden einige
von der Klasse System gelieferte Eigenschaften dargestellt:
import java.util.*;
import java.io.*;
public class TestProp
{
public static void main (String args[])
throws Exception{
Properties prop = System.getProperties();
172
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
System.out.print( "Running under "
+ prop.getProperty("os.name") );
System.out.print( " Version "
+ prop.getProperty("os.version") );
System.out.print( " Architecture "
+ prop.getProperty("os.arch") );
System.out.println( );
}
}
Mit der Klasse Properties steht eine Möglichkeit zur Behandlung von Eigenschaften für eine Anwendung zur Verfügung. Dazu dienen insbesondere die Methoden load und save, um solche Eigenschaften einzulesen oder in eine Datei zu
speichern. Die Ergänzung
prop.save(new FileOutputStream( "props.txt"),
"System Properties");
speichert die in der Klasse TestProp geladenen Systemeigenschaften in die Datei
props.txt. Das zweite Argument wird als Kopf eingetragen. Die Eigenschaften
werden in der Form
Schlüssel =Wert
zeilenweise geschrieben. In meinem Beispiel beginnt die Datei mit
#System Properties
#Mon Dec 02 11:53:03 CET 2002
java.runtime.name=Java(TM) 2 Runtime Environment, Standard Edition
sun.boot.library.path=C\:\\PROGRA~1\\java\\JDK13~1.1\\jre\\bin
java.vm.version=1.3.1-b24
java.vm.vendor=Sun Microsystems Inc.
java.vendor.url=http\://java.sun.com/
path.separator=;
java.vm.name=Java HotSpot(TM) Client VM
16.2.3
Bäume
Mit der Klasse TreeMap steht auch eine Klasse für die Speicherung von SchlüsselWerte-Paaren in Bäumen zur Verfügung. Die Zugriffe über put und get sind
analog zur Klasse Hashtable. Für weitere Details sei auf die Java-Dokumentation
verwiesen.
16.3. METHODEN IN DER KLASSE COLLECTIONS
16.3
173
Methoden in der Klasse Collections
Die Klasse Collections enthält eine ganze Reihe von statischen Methoden zum
Arbeiten mit Datenstrukturen. Unterschieden wird dabei zwischen Klassen, die
die beiden Interfaces
• Collection - beliebigen Sets
• List - Liste mit Reihenfolge
implementieren. Unter anderem handelt es sich um Methoden zum
• Sortieren
• Suchen nach Minimum oder Maximum
• Umordnen
• Füllen mit Elementen
Einige dieser Operationen sind nur für Listen mit einer Reihenfolge sinnvoll. So
ist etwa für eine Hashtable das Sortieren nach Reihenfolge nicht angebracht.
16.3.1
Beispiel Kartenspiel
Als eine Anwendung auf der Basis der besprochenen Klassen betrachten wir eine
Klasse zum Austeilen von Spielkarten. Folgende Anforderungen soll es erfüllen:
1. Kartenspiel mit 32 Karten
2. Austeilen für verschiedene Spiele (d. h. unterschiedlich viele Hände mit unterschiedlich vielen Karten)
3. Sortieren der Karten
Die verschiedenen Hände werden jeweils durch einen Vektor mit Karten realisiert.
Das Mischen und Zuordnen der Karten soll in einer Methode austeilen erfolgen.
Diese Methode gibt die Karten aufgeteilt in mehreren Vektoren zurück. Diese
Vektoren werden in einem Feld zusammen gefasst. Umgekehrt erfolgt die Vorgabe,
wie viele Hände mit jeweils wie vielen Karten ausgeteilt werden sollen, über ein
Feld als Parameter. Dieses Feld enthält für jede Hand die Anzahl der Karten.
Insgesamt erhält man dann für die Methode den Kopf
Vector[] austeilen( int[] kartenProHand )
Für das Skatspiel hat das übergebene Feld die Form // 3 Spieler mit je 10 Karten,
2 Karten im Skat
174
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
int[] kartenProHandSkat = {10,10,10,2};
Zur internen Darstellung der Karten wird ebenfalls ein Vektor benutzt. Die Namen der Farben und Kartenwerte sind in Felder gespeichert. Der Konstruktor
baut dann daraus einen Vektor mit allen Karten auf:
String[] farben = {"Kreuz", "Pik", "Herz", "Karo" };
String[] werte = {"As", "König", "Dame", "Bube",
"10", "9", "8", "7"};
Vector kartenWerte = new Vector();
public Karten() {
/** Karten anlegen*/
for( int i=0; i<farben.length; i++ ) {
for( int j=0; j<werte.length; j++ ) {
kartenWerte.add( farben[i] + "_" + werte[j] );
}
}
}
In dem Vektor liegen dann die Karten als Zeichenketten in der Form Farbe_Wert
vor. Mit der Methode Collections.shuffle lässt sich dann Austeilen wie folgt
realisieren:
Vector[] austeilen( int[]
// Karten kopieren und
Vector v = new Vector(
Collections.shuffle( v
kartenProHand ) {
mischen
kartenWerte );
);
// aufteilen
Vector[] vfeld = new Vector[kartenProHand.length];
int index = 0;
for( int i=0; i<kartenProHand.length; i++ ) {
vfeld[i]= new Vector(
v.subList(index, index+kartenProHand[i]) );
index += kartenProHand[i];
}
return vfeld;
}
Die Methode subList aus dem Interface List gibt einen Ausschnitt aus einer
Liste (z. B. einem Vektor) zurück. Zusammen mit der Möglichkeit, in dem Konstruktor für Vector einen anderen Vector mitzugeben, gestatten diese Methoden eine sehr kompakte Realisierung. Mit diesen Teilen kann eine Anwendung
geschrieben werden:
16.3. METHODEN IN DER KLASSE COLLECTIONS
175
import java.util.*;
public class Karten implements java.util.Comparator {
...
public static void main (String args[]) {
int[] kartenProHandSkat = {10,10,10,2};
Karten k = new Karten();
Vector[] hände = k.austeilen( kartenProHandSkat );
// sortierte Ausgabe
for( int i=0; i<hände.length; i++ ) {
System.out.print( i +" ");
Collections.sort( hände[i], k );
System.out.println( hände[i] );
}
}
public int compare(
java.lang.Object obj1,java.lang.Object obj2) {
return kartenWerte.indexOf(obj1)
- kartenWerte.indexOf(obj2) ;
}
}
Um die Vektoren sortieren zu können, implementiert die Klasse das Interface
Comperator. Kriterium für den Vergleich ist der Abstand zwischen zwei Karten
in dem geordneten Vektor.
176
KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN
Kapitel 17
Erweiterungen WS08
17.1
Felder
Mit dem folgenden Code wird ein 3 × 4-Feld angelegt und gefüllt:
int[][] a = new int[3][4];
for( int i=0; i<3; i++ ) {
for( int j=0; j<4; j++ ) {
a[i][j] = i+j;
}
}
Ein solches Feld kann zur Repräsentation einer Matrix verwendet werden. Dann
entspricht a[i][j] dem Element aij in der üblichen Schreibweise. Dementsprechend ist die erste Dimension die Zeilen- und die zweite die Spaltendimension. Im
Beispiel wird jedem Element als Wert die Summe von Zeilen- und Spaltenindex
zugewiesen:
0
1
2
1
2
3
2
3
4
3
4
5
Intern werden 2-dimensionale Felder aus eindimensionalen Feldern zusammengebaut. Demnach ist a selbst ein Feld der Länge 3 – genauer gesagt der Verweis
darauf. Jedes Element dieses Feldes ist wiederum selbst ein Feld, diesmal der Länge 4 und mit Integerwerten als Elementen. Detailliert ergibt sich dann folgendes
Bild:
a
+
a[0]
(
a[1]
(
a[2]
(
0
1
2
3
1
2
3
4
2
3
4
5
177
178
KAPITEL 17. ERWEITERUNGEN WS08
a[i] bezeichnet die i-te Zeile. Man kann diese Bezeichnung wie jedes andere
eindimensionale Feld verwenden. So kann man beispielsweise mit a[i].length
auf die Länge dieser Zeile zugreifen. Allgemein gilt bei mehrdimensionalen Feldern: gibt man alle Klammerpaare an – d. h. bei einem n-dimensionalen Feld n
Klammerpaare – so wird damit ein einzelnes Element angesprochen. Mit weniger
Klammern bezieht man sich auf die entsprechenden Überstrukturen. Betrachten
wir ein 3-dimensionales Feld
int[][][] d3 = new int[3][4][5];
Dann ist:
• d3: Verweis auf ein Feld mit 3 int[][]-Verweisen
• d3[1]: Verweis auf ein Feld mit 4 int[]-Verweisen
• d3[0][2]: Verweis auf ein int[], also ein Feld mit in diesem Fall 5 intWerten
• d3[0][1][2]: ein int-Wert, davon gibt es insgesamt 3 × 4 × 5 = 60.
In der allgemeinen Form ist man nicht mehr auf rechteckige Felder beschränkt.
Für jede Zeile kann eine andere Länge gewählt werden. Das folgende Beispiel zeigt
diese Möglichkeit:
// Feld mit 3 Zeilen
int[][] b = new int[3][];
for( int i=0; i<b.length; i++ ) {
// Jede Zeile mit anderer Länge
b[i] = new int[i+2];
for( int j=0; j<b[i].length; j++ ) {
b[i][j] = i+j;
}
}
Hier ergibt sich folgendes Bild:
b
+
b[0]
(
b[1]
(
b[2]
(
0
1
1
2
3
2
3
4
5
17.1. FELDER
179
Übung 17.1 Gaußsches Eliminationsverfahren
Mit dem Gauß-Verfahren kann man lineare Gleichungssysteme lösen. Mit Koeffizienten aij und den Werten bi hat das lineare Gleichungssystem bei n Unbekannten
die Form
a11 x1 +a12 x2 +· · ·+a1n xn = b1
a21 x1 +a22 x2 +· · ·+a2n xn = b2
..
.
.
.
.
. + .. + . . + ..
= ..
an1 x1 +an2 x2 +. . .+ann xn = bn
(17.1)
Zur Lösung des Gleichungssystems bringt man durch Zeilenumformungen das System in eine Stufenform mit neuen a0ij und b0i :
a011 x1 +a012 x2 +· · ·+a01n xn = b01
0 +a022 x2 +· · ·+a02n xn = b02
..
.
.
.
.
. + .. + . . + ..
= ..
0 + 0 +. . .+a0nn xn = b0n
(17.2)
In dieser Form lassen sich ausgehend von
xn = b0n /a0nn
(17.3)
durch Rückwärtseinsetzen die gesuchten Werte leicht bestimmen.
• Programmieren Sie das Gaußsche Eliminationsverfahren. Betrachten Sie
dabei die erweiterte Koeffizientenmatrix mit den aij und bi als Feld der
Größe n × (n + 1).
• Verwenden Sie die Klasse Matrix als Basis. Mit den Methoden dieser Klasse kann man Felder anlegen und anzeigen lassen. Machen Sie sich zunächst
mit den vorgegebenen Methoden vertraut. Programmieren Sie dann eine
Methode gauss(), die ein vorgegebenes Gleichungssystem löst (Vorschlag:
Werte aus erstelleBeispielMatrix(). Lassen Sie die einzelnen Schritte
anzeigen (siehe Beispiel demo()).
180
17.2
KAPITEL 17. ERWEITERUNGEN WS08
Rekursion
Betrachten wir als Beispiel die Berechnung der Summe der ersten 50 Zahlen. Mit
der Funktion S(N ) als Summe der ersten N Zahlen erhalten wir
S(1)
S(2)
S(3)
..
.
S(50)
= 1
= S(1) + 2
= S(2) + 3
..
.
= S(49) + 50
Dies entspricht unmittelbar der Realisierung durch eine Schleife in der Art
int S=0;
for( int i=1; i<=50; i++ ) {
S = S + i;
}
System.out.println( S );
Der gesuchte Wert wird durch Addition aller Zahlen von 1 bis 50 berechnet.
Formal lässt sich die Reihenfolge der Berechnung genauso gut umkehren. Wenn
wir zunächst so tun, als wäre S(49) bekannt, dann können wir einfach
S(50) = S(49) + 50
schreiben. Mit dem gleichen Argument kann S(49) auf S(48) zurück geführt werden. Insgesamt erhält man damit:
S(50)
S(49)
S(48)
..
.
S(2)
S(1)
= S(49) + 50
= S(48) + 49
= S(47) + 48
..
.
= S(1) + 2
= 1
Da die Berechnung vom Ende her beginnt, spricht man von Rekursion (lat. recurrere zurücklaufen). Demgegenüber wird das schrittweise Berechnen ab dem
Anfang als Iteration (lat. iterare wiederholen) bezeichnet.
Rekursion lässt sich in Java und vielen anderen Programmiersprachen leicht
realisieren. Dazu programmieren wir eine Methode, die entweder bei n = 1 den
Wert 1 zurück gibt oder einen weiteren Rekursionsschritt ausführt:
17.2. REKURSION
181
int S( int n ) {
if( n == 1 ) return 1;
return S(n-1) + n;
}
Der gesuchte Wert kann dann einfach mit
System.out.println( S(50) );
ausgegeben werden. In dieser Form ist die Schleife verschwunden. Stattdessen
steckt die Logik in der Methode. Die Methode ruft sich selbst mit einem um 1
kleineren Parameter immer wieder auf, bis irgendwann der Wert 1 erreicht ist.
Dann wird rückwärts die Kette abgearbeitet.
Diese Beispiel zeigt, wie ein Problem sowohl iterativ als auch rekursiv gelöst
werden kann. In diesem Fall bietet die rekursive Lösung außer der eleganten Formulierung keinen Vorteil. Es ist sogar davon auszugehen, dass sie aufgrund der
vielen Methodenaufrufe mehr Rechenzeit benötigt. Rekursive Lösungen bieten
sich immer dann an, wenn man ein komplexes Problem auf ein einfacheres Problem zurückführen kann. Man kann dann den Lösungsansatz direkt als Java-Code
umsetzen. Die Lösung ist damit elegant und leicht verständlich. Ob die rekursive
oder die immer auch mögliche iterative Lösung aufwandsgünstiger ist, muss im
Einzelfall betrachtet werden.
Übung 17.2 Größter gemeinsamer Teiler
Die folgende Methode berechnet den größten gemeinsamer Teiler (GGT) zweier
natürlicher Zahlen a > b:
int ggt( int a, int b ) {
while( b > 0 ) {
int rest = a % b;
a = b;
b = rest;
}
return a;
}
// Rest bestimmen
// Werte tauschen
Wie sieht eine rekursive Lösung aus?
Übung 17.3 Wegesuche
Vorgegeben ist ein Raster mit N × N -Feldern. Wie viele mögliche Wege führen
von einer Ecke zu der diagonal gegenüber liegenden Ecke? Die Wege sollen nur
horizontal oder vertikal verlaufen und keine Umwege enthalten. Im Fall N = 2
gibt es insgesamt 6 Möglichkeiten:
182
KAPITEL 17. ERWEITERUNGEN WS08
◦
/◦
◦
◦
◦
◦
/◦
²
◦
²
◦
◦
◦
◦
◦
◦
◦
◦
◦
/◦
◦
◦
◦
/◦
²
²
◦
²
◦
/◦
²
◦
◦
◦
/◦
²
◦
◦
◦
/◦
²
◦
◦
◦
◦
◦
◦
◦
/◦
◦
²
²
/◦
²
◦
²
◦
◦
◦
◦
/◦
◦
◦
/◦
/◦
◦
Allgemein gilt für die Anzahl NW der Wege die Formel
Ã
NW
!
2n
(2n)!
=
=
n
n! × n!
Überprüfen Sie diese Formel durch ein entsprechendes Programm. In diesem Programm soll für vorgegebenes N die Anzahl der möglichen Wege durch Zählen aller
Möglichkeiten bestimmt werden. Verwenden Sie dazu eine rekursive Berechnung.
17.3. CLOSE
17.3
close
public static void teste() throws Exception {
schreibe( "zeilen1.txt", false );
schreibe( "zeilen2.txt", true );
}
public static void schreibe(String datei, boolean sichern)
throws Exception {
BufferedWriter wr = new BufferedWriter(
new FileWriter( datei ) );
String text = null;
int i;
for( i=0; i< 100; i++ ) {
wr.write( "Zeile " + i );
wr.newLine();
}
System.out.println( "Alles geschrieben" );
if( sichern ) wr.close();
}
183
184
KAPITEL 17. ERWEITERUNGEN WS08
Übung 17.4 Pythagoreische Tripel
Drei natürliche Zahlen a < b < c mit
a 2 + b2 = c2
bezeichnet man als Pythagoreische Tripel. Wie sind die Werte für das einzige
Tripel, das die Bedingung
a + b + c = 1000
erfüllt? (Problem 9 im Euler Project).
Übung 17.5 Taylor-Reihe
Die Exponentialfunktion lässt sich gemäß
ex =
∞
X
xn
n=0
n!
(17.4)
als Potenzreihe darstellen.
• Schreiben Sie eine Klasse mit Methoden, um für gegebenes x und n den
Wert der Taylor-Reihe zu berechnen. Verwenden Sie dabei die Methode
Math.pow() zur Berechnung der Potenzen xn . Vergleichen Sie den berechneten Wert mit dem Ergebnis von Math.exp(x). Wie nähert sich der Wert
der Potenzreihe mit wachsendem n an den „richtigen“ Wert an?
• Programmieren Sie alternativ die Berechnung der Potenzreihe nach dem
Horner-Schema. Vergleichen Sie die Güte der beiden Berechnungen (z. B.
x = 3, n = 30). Wie schätzen Sie den Rechenaufwand der beiden Berechnungsarten im Vergleich?
Übung 17.6 Collatz-Problem
Gegeben ist eine positive ganze Zahl n. Dann soll mit diesem Startwert nach
folgender Vorschrift eine Folge berechnet werden:
(
n=
n/2
:
3·n+1 :
n gerade
n ungerade
(17.5)
Die Folgen erreichen irgendwann den Wert 1 und laufen dann in eine Schleife
mit der Folge 1 4 2. Als Beispiel erhält man für den Wert 11:
n=11: 34 17 52 26 13 40 20 10 5 16 8 4 2 1
1. Lassen Sie die Folgen für alle Werte n < 100 ausgeben.
2. Bei welchem Startwert ist die Folge am längsten?
3. Bestätigen Sie, dass auch für alle n < 1000000 der Wert 1 erreicht wird.
17.3. CLOSE
185
4. Welches ist jetzt die längste Folge?
5. Welche größte Zahl tritt auf (bei welcher Folge)?
Hinweise:
• Die einzelnen Folgeglieder können recht groß werden.
• Diese Aufgabe finden Sie auch als Problem 14 im Euler Project1
• Sie können $500 Belohnung verdienen wenn Sie die Vermutung, dass die
Folge für jeden Startwert bei 1 ankommt, beweisen.
1
http://projecteuler.net/
186
KAPITEL 17. ERWEITERUNGEN WS08
Literaturverzeichnis
[Boo94] Grady Booch. Objektorientierte Analyse und Design: Mit praktischen
Anwendungsbeispielen. Addison Wesley, 1994.
[Fri07] Jeffrey E. F. Friedl. Reguläre Ausdrücke. O’Reilly, 2007.
[Gol91] David Goldberg. What every computer scientist should know about
floating-point arithmetic. Computing Surveys, 1991.
[Krü02] Guido Krüger. Handbuch der Java-Programmierung. Addison-Wesley,
2002.
187
Index
?-Operator, 21
Fragezeichen-Operator, 21
Funktion, 53
Abstrakte Klassen, 101
Aktivitätsdiagramm, 69
Algorithmus, 63
Applet, 1
Assoziativspeicher, 168
garbage collection, 85
Gaußsches Eliminationsverfahren, 179
Getter, 81
Gleitkommazahlen, 37
Hash-Code, 91
Hashtable, 168
Bereichsüberschreitung, 8
Bit-Operator, 12
BlueJ, 1, 89
Booch, Grady, 74
boolean, 17
Bootstrap Classes, 131
Boundary matchers, 124
BufferedWriter, 143
IEEE 754, 33
import-Anweisung, 132
Information hiding, 81
InputStream, 148
Instanzmethoden, 82
Integer, 5
Interface, 102
Iteration, 180
C++, 76
C#, 77
call by reference, 57
call by value, 55, 57
catch-or-throw Regel, 155
Collections, 173
jar, 129
javadoc, 133
jdb, 134
Klassenmethoden, 82
Debugger, 134
Destruktoren, 85
dynamisches Binden, 100
L-Wert, 11
Lastenheft, 63
LineNumberReader, 145
lvalue, 11
Error, 157
Exception, 157
Makro, 53
Manifest, 130
Mantisse, 31, 33
Mathematisch Funktionen, 39
mehrfache Vererbung, 76
Mehrfachvererbung, 102
Modulo-Operator, 11
Fibonacci Zahlen, 29
Fibonacci-Zahlen, 28, 60
File, 149
finally-Block, 159
Flussdiagramm, 65
foreach, 48
188
INDEX
OutputStream, 148
Paket, 132
Perl, 19, 168
Pflichtenheft, 63
Pipe, 141
Polymorphismus, 100, 157
PrintWriter, 147
Properties, 171
Prozedur, 53
PushBackReader, 145
Quantoren, 126
R-Wert, 11
Random Access File, 148
Reader, 142
regulärer Ausdruck, 117
regular expressions, 123
Regulare Ausdrucke, 123
Rekursion, 180
RuntimeException, 158
rvalue, 11
Schachspiel, 42
Setter, 81
Short-Circuit-Evaluation, 18
Signatur, 56
Socket, 141
Stack, 167
Stapelspeicher, 167
statisches Binden, 100
switch-Anweisung, 22
Türme von Hanoi, 60
throw, 159
Tokenizer, 119
Type-Cast-Operator, 41
UML, 69
Unicode, 111
Unified Modeling Language, 69
Zuweisungsoperator, 10
189
Herunterladen