Kapitel 5 Objekte, Typen, Datenstrukturen

Werbung
Kapitel 5
Objekte, Typen, Datenstrukturen:
Einführung und Beispiele
5.1
Datentypen und Operationen
Die Syntax einer algorithmischen Sprache beschreibt die formalen Regeln, mit denen ein Algorithmus
formuliert werden kann. Sie erklärt jedoch nicht die Bedeutung der Daten und Operationen, die in
einem in einer bestimmten algorithmischen Sprache geschriebenen Algorithmus zulässig sind. Dies
ist ein Problem der Semantik.
Für Daten eines vorgegebenen Typs ergibt sich die Semantik aus den möglichen Werten und den
zugelassenen Operationen auf diesen Werten. Beide zusammen bilden einen Typ oder Datentyp.
Definition: Ein Datentyp (kurz Typ) besteht aus
• dem Wertebereich (domain) des Typs
• einer Menge von Operationen (Methoden) auf diesen Werten
Jede Programmiersprache verfügt über eingebaute (Standard) Datentypen. Andere müssen als sogenannte abstrakte oder selbst definierte Datentypen mit den Ausdrucksmitteln der Programmiersprache
definiert werden.
Beispiel 5.1 (Der Java Typ int)
Wertebereich: Die endliche Teilmenge
{Nmin , Nmin + 1, Nmin + 2, . . . , 0, 1, 2, . . . , Nmax }
der ganzen Zahlen, wobei Nmin = −231 und Nmax = 231 − 1 ist.
Version vom 24. November 2004
69
70
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Operationen:
=
Zuweisung
+
Addition
Subtraktion
*
Multiplikation
/
Ganzzahlige Division
%
Rest bei ganzzahliger Division
==
Test auf Gleichheit, Ergebnis ist vom Typ boolean
!=
Test auf Ungleichheit, Ergebnis ist vom Typ boolean
und viele andere mehr (vgl. Übung).
Beispiel 5.2 (Ein selbstdefinierter Typ Fraction)
Wertebereich: Alle Brüche der Form r = p/q wobei p und q int Werte sind und q > 0 ist.
Operationen:
Fraction(a,b)
toString()
doubleValue()
simplify()
getNumerator()
getDenominator()
multiply(s)
equals(s)
Erzeuge den Bruch r aus gegebenen int Zahlen a,b
Schreibe den Bruch r in der Form “p/q”
Berechne den entsprechenden double Wert
Reduziere r auf die Form p/q, so dass p und q teilerfremd sind
Gebe den Zähler (numerator) zurück
Gebe den Nenner (denominator) zurück
Multipliziere Bruch mit Bruch s
Teste, ob Bruch = s ist (2/4 = 1/2)
Der Typ Fraction ist in Java nicht vorhanden, kann aber (über Klassen, vgl. Kapitel 7.4.1) implementiert werden.
Wir werden jedoch bereits in unserem Pseudocode die Schreibweise (und Ausdrucksweise) der Java
Klassen übernehmen. toString(),. . ., equals() sind Methoden der Klasse Fraction.
Die Deklaration von Variablen vom Typ Fraction erfolgt gemäß
Fraction r, s;
Man nennt dann r, s Instanzen (Objekte) der Klasse. Auf die zugehörigen Objektmethoden wird mit
r.toString();
usw. zugegriffen. Diese Anweisung bewirkt das Schreiben des Bruches r als String in der Form a/b.
Die Erzeugung (auch Instantiierung genannt) erfolgt wie bei Objekten üblich mit new.
Fraction r = new Fraction(3, 4);
bedeutet also die Erzeugung der Variablen (des Objektes) r vom Typ Fraction mit dem Wert 3/4.
In Java sind also folgende Anweisungen denkbar (bei Umsetzung obiger Methoden):
71
5.1. DATENTYPEN UND OPERATIONEN
Fraction r = new Fraction(3, 4)
Fraction s = new Fraction(4, 8);
s.simplify();
r.multiply(s);
if(r.equals(s)) ...
// r := 3/4
// x := 4/8
// s = 1/2
// r := r · s = 34 · 21 = 38
// Test auf Gleichheit
Beispiel 5.3 (Skatkarte)
Wertebereich:={Karo 7, . . . , Karo Ass, . . . , Kreuz 7, . . . , Kreuz Ass}, d. h. 32 Werte, die die Spielkarten im Skat Spiel darstellen.
Funktionen:
farbe()
wert()
Skatkarte()
Skatkarte(f,w)
Gibt die Farbe (Kreuz, Pik, Herz oder Karo) einer Karte an
Gibt den Wert (7,. . .,10, Bube, Dame, König oder Ass)
einer Karte an
Konstruktor, zieht eine zufällige Karte
Konstruktor, erzeugt eine Karte mit Farbe f und Wert w
Dann weist die Sequenz
Skatkarte karte = new Skatkarte();
w = karte.wert();
der Variablen w den Wert einer zufälligen Skatkarte zu.
Auch dieser Typ ist in Java nicht vorhanden, könnte aber ähnlich wie der Typ Fraction als Klasse
implementiert werden.
Wichtig ist die Unterscheidung zwischen (abstrakten) Datentypen und Implementationen (z. B. in
Java) solcher Datentypen. Algorithmenentwicklung basiert nur auf abstrakten Datentypen. Die Umsetzung abstrakter Datentypen in eine Implementation erfolgt entweder erst danach, oder ist unnötig,
da bereits Implementationen existieren, die man verwenden kann (Wiederverwendbarkeit wird gerade
von Java besonders unterstützt).
Datentypen sind extrem wichtig für die Compilierung, da der Compiler dann
• beim Compilieren allen definierten Objekten den erforderlichen Speicherplatz zuweisen kann,
• die Typinformation zur Überprüfung der Zulässigkeit von Programmstatements benutzen kann
(syntaktisch und partiell auch semantisch),
• den Typ des Wertes eines Ausdrucks bereits (weitgehend) ermitteln kann, ohne den Rechenprozess durchführen zu müssen (zum Beispiel ist die Multiplikation einer int Zahl mit einer
double Zahl vom Typ double, siehe unten.).
72
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Dies setzt voraus, dass der Datentyp jedes Identifiers im Programm deklariert bzw. definiert wird und
damit “zur Compilierzeit” bekannt ist. Diese Eigenschaft kennzeichnet statisch getypte Sprachen wie
C++, Pascal und Java.
Bei der Typüberprüfung (type checking) unterscheidet man zwischen strikter Typüberprüfung wie
in Pascal und nicht strikter Typüberprüfung wie in Java. Bei strikter Typüberprüfung ist das “Mischen”
von Typen stark eingeschränkt. In Java sind Typumwandlungen nur explizit durch das sogenannte
casting oder type casting möglich. Besonders wichtig wird die Typumwandlung im Zusammenhang
mit der Vererbung bei Klassen, vgl. Abschnitt 7.3.3.
Ist intVar eine int Variable und floatVar eine float Variable, so bedeutet:
intVar = (int) floatVar;
eine Zuweisung mit expliziter Typumwandlung.
Ein Beispiel liefert die Division. Da / bei ganzen Zahlen die ganzzahlige Division bezeichnet, hat
5/8 den ganzzahligen Wert 0 und 5.0/8 den gebrochenen Wert 0.625. Statt 5.0/8 kann man auch
(double) 5 / (double) 8 schreiben, was vor allem für Ausdrücke wie (double) x / (double)
y wichtig ist, in denen x, y ganzzahlige Werte annehmen, man aber die reelle Division meint.
Bei offensichtlichen Operationen wie
floatVar = floatVar * intVar;
findet eine implizite Typumwandlung (float) intVar statt.
In Pseudocode werden Typvereinbarungen in der Form
TypName identifier;
geschrieben.
Java stellt elementare Datentypen mit zugehörigen Operationen bereit für
• ganze Zahlen: int, long, short, byte.
• Gleitkommazahlen: float, double.
• Zeichen: char.
• Wahrheitswerte: boolean
Die genaue Behandlung dieser Datentypen erfolgt in der Übung.
5.2
Strukturierte Datentypen (Datenstrukturen)
Die bisherigen Beispiele waren Beispiele für einfache (unstrukturierte oder primitive) Datentypen.
73
5.2. STRUKTURIERTE DATENTYPEN (DATENSTRUKTUREN)
Neben einfachen Datentypen gibt es sogenannte zusammengesetzte oder strukturierte Datentypen. Sie
setzen sich aus bereits eingeführten Datentypen gemäß bestimmter Strukturierungsmerkmale zusammen. Stehen die Strukturierungsmerkmale im Vordergrund (und nicht der Typ der “Grunddaten”), so
redet man von Datenstrukturen.
Strukturierte Typen oder Datenstrukturen haben (neben Wertebereich und Operationen)
• Komponenten-Daten, die atomar oder wieder strukturiert sein können
• Regeln, die das Zusammenwirken der Komponenten zur gesamten Struktur definieren.
Programmiersprachen haben i. a. nur wenige Datenstrukturen als eingebaute Typen (alle haben z. B.
Arrays). Die meisten muss der Programmierer selber implementieren, wobei ihm Java mächtige Konstruktionsmöglichkeiten (vor allem Klassen und Vererbung) bereitstellt.
Wir behandeln zunächst die wichtigsten Datenstrukturen aus abstrakter Sicht. Die Implementation in
Java wird erst jeweils dann erfolgen, wenn die nötigen Konstruktionsmöglichkeiten besprochen sind.
Abbildung 5.1 gibt eine hierarchische Übersicht über einige der wichtigsten Datenstrukturen.
Datenstrukturen
X
XX
XXX
XXX
X
Linear
Nichtlinear
hhhh
hhhh
@
hhh
hhh
@
@
Direkter Zugriff
Sequentieller Zugriff
Set
```
HH `
```
@
HH
@
```
``
H
@
Homogene
Heterogene
Last-In
First-In
Allgemein
Komponenten
Komponenten
First-out
First-Out
Array
Record
Liste
Stack
Queue
Abbildung 5.1: Klassifikation einiger wichtiger Datenstrukturen.
Eine lineare Datenstruktur hat (bei mindestens 2 Komponenten) eine Ordnung auf den Komponenten
mit folgenden Eigenschaften:
• Es gibt eine eindeutige erste Komponente.
• Es gibt eine eindeutige letzte Komponente.
• Jede Komponente (außer der ersten) hat einen eindeutigen Vorgänger.
• Jede Komponente (außer der letzten) hat einen eindeutigen Nachfolger.
74
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Direkter Zugriff (auch random access genannt) bedeutet, dass man auf jede beliebige Komponente
zugreifen kann, ohne vorher auf andere Komponenten zugreifen zu müssen. Beispiele sind:
– Ein Regalbrett mit Büchern; auf jedes Buch kann direkt zugegriffen werden.
– CDs mit direkter Ansteuerung von Musikstücken.
Sequentieller Zugriff bedeutet, dass man auf die i-te Komponente nur zugreifen kann, nachdem man
vorher auf die Komponenten 1, 2, . . . , i − 1 zugegriffen hat. Beispiele sind:
– Ein Stapel von Büchern; um das i-te zu nehmen, müssen erst die i − 1 obersten entfernt werden.
– Musikstücke auf einem Tonband.
5.3
Arrays
Das Array ist die verbreitetste Datenstruktur; in einigen Programmiersprachen (Fortran, Basic, Algol
60) sogar die einzige.
Kennzeichen der Datenstruktur Array sind:
• feste Komponentenzahl (die in Java erst zur Laufzeit festgelegt werden muss),
• direkter Zugriff auf Komponenten mittels Indizes,
• homogener Grundtyp,
• Indizes können berechnet werden.
Ist k die Anzahl der Komponenten und A der Wertebereich des Grundtyps, so ist der Wertebereich X
des Arrays das kartesische Produkt A × . . . × A (k-fach).
Die k Komponenten haben in der Regel ganze Zahlen (meist 0, 1, . . . , k − 1) als Index. Mathematisch
entspricht also X den Vektoren der Länge k mit Komponenten aus A, d. h.
X = {(a0 , a1 , . . . , ak−1 ) | ai ∈ A, i = 0, . . . , k − 1}
Zu den Operationen auf Arrays gehören (abstrakt formuliert):
value(a, i)
store(i, v)
a := b
a=b
Ermittelt den Wert der Komponente eines Arrays a mit Index i, also den
Wert der (i + 1)-ten Komponente. Ist a = (a0 , . . . , ak−1 ), so liefert
value(a, i) den Wert ai .
Weist der Komponente von a mit Index i den Wert v zu. Danach ist ai = v.
Zuweisung von Arrays. Danach gilt ai = bi , i = 0, . . . , k − 1.
Test auf Gleichheit. Liefert den Wert true genau dann, wenn ai = bi für
i = 0, . . . , k − 1.
75
5.3. ARRAYS
5.3.1
Arrays in Java
In Java existiert bereits ein eingebauter Array Typ als Referenztyp. Durch die Anweisung
int[] x = new int[10]; (auch int x[] = ...)
wird ein Array mit 10 Komponenten mit Komponententyp int definiert, das unter dem Namen x
ansprechbar ist. Die Komponenten haben die Indizes 0,1,...,9. In Java sind 0,1,...,n-1 die
einzig möglichen Indizes eines Arrays mit n Komponenten.
Arraytypen in Java sind Referenztypen. Arrayvariable (x in obiger Anweisung) verweisen daher auf
Array-Objekte, die wie üblich mit new erzeugt werden. Diese Objekte verfügen über ein Feld length,
das die Anzahl der Komponenten angibt. Hierauf wird (wie immer bei Objekten) mit dem . zugegriffen, also x.length.1
Der Operation value(a,i) entspricht in Java der Feldzugriff gemäß der Syntax
<Feldzugriff >::=<Referenzausdruck>[<Ausdruck>]
Dabei wird der Referenzausdruck zuerst ausgewertet und liefert die Referenz (Adresse) des Arrays.
Der Ausdruck in den Klammern [. . .] muss einen int Wert liefern, der den Index der Komponente
berechnet. Zur Laufzeit wird in Java automatisch überprüft, ob der berechnete Index wirklich im
vereinbarten Bereich des Arrays liegt. Ist dies nicht der Fall, so erfolgt eine Exception-Meldung durch
die Laufzeitumgebung (ArrayIndexOutOfBoundsException).
Durch
a = x[3];
wird also der Variablen a der Wert der 4-ten Komponente von x zugewiesen. Entsprechend wird durch
x[i] = v;
der (i + 1)-ten Komponente von x der Wert von v zugewiesen.
Bei der Deklaration von Arrays kann eine Initialisierung ohne Benutzung von new vorgenommen
werden, wie in
int[] x = { 1, 2, 3, 4 };
Zuweisung und Test auf Gleichheit existieren nicht in Java, können aber einfach über for Schleifen
realisiert werden. So initialisiert
int n = 10;
int[] x = new int[n];
int[] y;
for (int i = 0; i < x.length; i++) x[i] = i*i;
das Array x mit den Werten
1 Aber
0
1
4
9 16 25 36 49 64 81
0
1
2
3
4
nicht x.length(), da length keine Methode ist.
5
6
7
8
9
76
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
y ist zunächst undefiniert. Die Zuweisung y = x ist zwar zulässig, weist aber nur y die Adresse von x
zu. Beide Variablen zeigen dann auf dasselbe Array. Will man in y eine Kopie anlegen, so muss man
dies entweder selber realisieren oder spezielle Methoden wie clone() verwenden.
Bei dem dem int Array x lässt sich einfach eine Kopie mit einer for-Schleife herstellen:
y = new int[n];
for (int i = 0; i < n; i++) y[i] = x[i];
Diese einfache Art geht jedoch nicht mehr bei Objekten als Grundtyp, da durch die Zuweisung y[i]
= x[i] nur die Referenzen zugewiesen würden und nicht die Werte. Hier hilft die Methode clone(),
über die die meisten Referenztypen (darunter Arraytypen) verfügen. 2 Ist etwa
String[] stringArr = {"Hallo!", "Wie gehts?", "Gut."};
ein Array aus drei Strings, so lässt sich mit der Anweisung
String[] copyOfstringArr = (String[]) stringArr.clone();
eine echte“ Kopie von stringArr herstellen. Die Anweisung stringArr.clone() kopiert den
”
vom Array stringArr belegten Speicherinhalt in ein allgemeines Objekt, das mit der Castanweisung (String[]) auf den richtigen Typ (Array von Strings) gecastet werden muss. Jetzt sprechen
stringArr und copyOfstringArr verschiedene Speicherbereiche an und können unabhängig voneinander geändert und verwendet werden.
Als kompliziertes Beispiel für den Komponentenzugriff gemäß der Regel
<Feldzugriff >::=<Referenzausdruck>[<Ausdruck>]
betrachten wir das Fragment
int[] x = {1, 2, 3, 4},
y = {5, 6, 7, 8},
z;
y[x [0]] = 9;
// ergibt y[1] == 9
(z = y)[(y = x)[0] + y[1]] = 0;
In der letzten Zeile wird zunächst der Referenzausdruck (z = y) ausgewertet. Er ist ein Zuweisungsausdruck, der den Wert von y, also die Adresse von y ergibt, wobei diese als Seiteneffekt z zugewiesen
wird. Hiernach wird unter z also das Array y angesprochen!
In der Auswertung von [<Ausdruck>] geschieht Ähnliches mit dem Referenzausdruck y = x. Daher wird (y = x)[0] zu x[0], also zu 1 ausgewertet, und der gesamte Ausdruck in [...] zu 1 +
x[1] = 1 + 2 = 3. Als Seiteneffekt zeigt y jetzt auf x.
Die gesamte Zeile bewirkt also eine Zuweisung an z[3], so dass y und z (einschließlich der Änderung
von y und der beiden Seiteneffekte) am Ende die Werte
2 Genauer:
alle Typen, die das Interface Clonable implementieren, vgl. Abschnitt 7.3.7.
77
5.3. ARRAYS
y
1
2
3
4
0
1
2
3
z
5
9
7
0
0
1
2
3
haben.
5.3.2
Mehrdimensionale Arrays
Der Komponententyp eines Arrays kann natürlich wieder ein strukturierter Typ sein. Insbesondere
sind so Arrays von Arrays möglich. So deklariert
double[][] table = new double[5][10];
ein Array table mit 5 Komponenten, wobei jede Komponente wiederum ein Array mit 10 Komponenten vom Grundtyp double ist.3
table ist ein Beispiel eines 2-dimensionalen Arrays. Man stellt es sich am besten so vor:
table
table[0]
table[1]
table[2]
table[3]
table[4]
table[4][5]
table[i] greift dann auf die (i + 1) Komponente von table zu (also das Array table[i]), und
table[i][j] auf die ( j + 1)-te Komponente des Arrays table[i].
Ein zweidimensionales Array kann aus Arrays unterschiedlicher Länge bestehen. So erzeugt
int[][] table = new int[5][]; // 5 Zeilen
for (int i = 0; i < table.length; i++) {
int[] tmp = new int[i+1];
for (int j = 0; j < tmp.length; j++)
tmp[j] = i+j; // initialisiere tmp[j]
table[i] = tmp;
}
das Array
3 Bei
der Stellung der Klammern [][] sind auch double[] table[] bzw. double table[][] zulässig.
78
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
table
0
1
2
3
4
2
3
4
5
4
5
6
6
7
8
0
1
2
3
4
Dies ist möglich, da Arraytypen Referenztypen sind. Der Komponententyp des Arrays table ist der
Referenztyp int [], und dieser kann Referenzen auf int-Arrays unterschiedlicher Länge haben.
Das Array im Beispiel ließe sich auch durch direkte Initialisierung erzeugen:
int[][] table = { {
{
{
{
{
};
0 },
1, 2 },
2, 3, 4 },
3, 4, 5, 6 },
4, 5, 6, 7, 8 }
Als abschließenden Beispiel dieser Einführung von Arrays betrachten wir das Umdrehen eines Arrays.
Beispiel 5.4 (Umdrehen eines Arrays) Ein 1-dimensionales Array mit n int Komponenten soll umgedreht werden.
4
1
3
2
−→
2
3
1
4
int[] vector = new int[n];
// Initialisierung der Komponenten
int temp;
// Hilfsvariable
int limit = vector.length/2;
// obere Grenze in der for Schleife
for (int i = 0; i < limit; i++){
temp = vector[i];
vector[i] = vector[n-1-i]; // Zugriff auf Komponente n-1-i
vector[n-1-i] = temp;
// Zuweisung an Komponente n-1-i
}
Hier wird in vector[n-1-i] der Index n-1-i berechnet und dann auf die entsprechende Komponente von vector zugegriffen bzw. ihr etwas zugewiesen.
Eine andere Implementation (unter voller Ausnutzung der Möglichkeiten von for-Schleifen) ist:
for (int i = -1, j = vector.length; ++i < --j;){
temp = vector[i];
vector[i] = vector[j];
vector[j] = temp;
}
5.4. STRINGS
5.4
79
Strings
Strings sind Zeichenketten. Sie sind extrem wichtig für die EDV, werden aber sehr unterschiedlich in
den verschiedenen Programmiersprachen behandelt.
Kennzeichen der Datenstruktur String sind:
• variable Komponentenzahl,
• Komponenten sind homogen vom Typ char,
• direkter Zugriff auf Komponenten,
• typische Stringoperationen wie:
– Verkettung,
– Finden von Substrings,
– Vergleich bezüglich lexikographischer Ordnung,
– Einfügen/Lesen von Zeichen an bestimmter Stelle.
Der Wertebereich von Strings ist die Menge der Zeichenketten aus char Zeichen (einschließlich der
leeren Zeichenkette).
5.4.1
Strings in Java
In Java gibt es zwei Klassen für Strings, String und StringBuffer.
Die Klasse String hat als Objekte Zeichenketten, die sich nach der Erzeugung nicht ändern. Sie ist
speziell optimiert für die Verwaltung konstanter Zeichenketten.
Die Klasse StringBuffer hat als Objekte Zeichenketten, die sich im Verlauf des Programms ändern
können (kürzer werden, wachsen, Teilstrings ändern, . . .). Beispielsweise werden Operationen aus
dieser Klasse zur Implementation des “+” Operators der Klasse String verwendet.
Im Gegensatz zu C oder C++ sind Strings in Java keine Arrays von char.
char[] string = { ’H’, ’a’, ’l’, ’l’, ’o’ };
ist nicht dasselbe wie
String str = "Hallo";
Allerdings gibt es in der Syntax viele Zugeständnisse an C-Programmierer, zum Beispiel die Zuweisung
String str = "Hallo!";
80
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
die gleichzeitig neben der Java-konformen Erzeugung mit new existiert: 4
String str = new String("Hallo!");
Es gibt viele Methoden für Strings, die für alle gängigen Stringoperationen ausreichen. Man schaue
sich dafür die Klassen String und StringBuffer in der Java-Dokumentation an. Als Beispiel führen
wir drei Konstruktoren der Klasse String auf.
public String();
public String(String value);
public String(char[] value);
5.4.2
// konstruiert den leeren String
// konstruiert einen String mit Wert value
// wandelt ein char-Array in einen String um.
Manipulation von Strings: ein erster Ansatz
Als Beispiel für den Umgang mit Arrays und Strings betrachten wir zwei Java Programme für das
Einlesen einer Folge von Zahlen aus einer TextArea in ein Array. Das erste Programm macht sehr
spezielle Annahmen über die gegebene Zahlenfolge, und fängt noch keine Fehler ab, die aus der
Verletzung dieser Annahmen resultieren. Das zweite Programm nutzt die Werkzeuge der Java Klasse
StringTokenizer und beinhaltet ein Exception Handling für den Umgang mit Fehlern.
Gegeben ist in beiden Fällen eine Folge von int-Zahlen n, a0 , a1 , . . . , am , die durch die sogenannten
white space, d. h. ein oder mehrere Leerzeichen (blanks), Tabulatoren und Zeilenumbrüche getrennt
sind, etwa
4
10
40
20
50
30
Die erste Zahl gibt die Länge des Arrays an, in das die Zahlen a0 , a1 , . . . , an−1 aus der Folge eingelesen
werden sollen. Daher muss m ≥ n − 1 sein. Ferner wird vorausgesetzt, dass außer int-Zahlen und
white space keine anderen Strings in der TextArea stehen.
Im ersten Programm StringDemo wird zusätzlich angenommen, dass nach jeder Zahl ein Leerzeichen ’’ steht. Dieses Leerzeichen wird als Trennsymbol verwendet, um das Ende einer int-Zahl zu
erkennen.
StringDemo verwendet Methoden der Klassen String und StringBuffer, u. a.
4 Tatächlich
gibt es zwischen beiden Arten der Erzeugung einen diffizilen Unterschied. Zwei mit new erzeugte Strings
str1, str2 mit gleichem Wert Hallo! haben (wie bei Referenztypen zu erwarten) unterschiedliche Adressen. Dagegen
ergeben Zuweisungen
String str1 = "Hallo!";
...
String str2 = "Hallo!";
desselben Strings an verschiedene Variable auch dieselbe Adresse, da der Compiler überprüft, ob es die Stringkonstante
Hallo! bereits gibt und sie dann wiederverwendet. Wenn man also stets diese Art Erzeugung (ohne new) verwendet, lässt
sich Gleichheit von Strings auch mit (str1 == str2) überprüfen (weiteres Zugeständnis an C-Programmierer), während
allgemein bei Objekten mit == nur Gleichheit der Adressen geprüft wird. Gleichheit der Werte muss man mit der Methode
equals() überprüfen, also str1.equals(str2).
81
5.4. STRINGS
trim()
// Wegschneiden von white space vor und nach dem String
charAt(pos)
// Gibt das Zeichen an Position pos zurück
getChars(pos1, pos2, charArray, begin)
// Kopiert Zeichen des Strings von pos1 bis pos2 - 1
// in das char-Array charArray ab Index begin
aus der Klasse String, und
append(str)
toString()
// Hängt den String str an das StringBuffer-Objekt an
// Konvertiert ein StringBuffer-Objekt in ein String-Objekt
aus der Klasse StringBuffer.
Programm 5.1 StringDemo.java
// StringDemo.java
//
// Demonstrates use of the classes Strings and StringBuffer
// together with arrays
//
// A string of integer numbers in which each number is terminated by a blank
// is converted into an array of integers
//
// Assumptions:
// input is a sequence of numbers n, a0, a1, a2 ...
// n specifies the length of an array
// a0, a1, ... are the array entries (must be at least n)
// numbers are terminated by a blank with possibly more white space
//
between them (return, tab or newline)
//
// Take care! You will get exceptions if these assumptions are violated.
import
import
import
import
java.awt.*;
java.applet.Applet;
java.awt.event.ActionListener;
java.awt.event.ActionEvent;
public class StringDemo extends Applet {
TextArea input, output;
Button startBtn;
int[] vec;
// setup the graphical user interface components
82
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
public void init() {
//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
// set font
setFont(new Font("Times", Font.PLAIN, 24));
input = new TextArea("4 10 20 30 40 ", 5, 30);
add(input);
// put input on applet
startBtn = new Button("Start");
startBtn.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(startBtn);
output = new TextArea(10, 30);
output.setEditable(false);
add(output);
}
// process user’s action on the input text area
public void runDemo() {
String inputStr = input.getText();
String str, currStr;
int n; // will be the first number in the
// it determines the length of vec
text area
// delete leading and trailing white space,
// but preserve blank after last number
currStr = new String(inputStr.trim() + " ");
// find first blank that terminates the string
// representing the first number
int pos = 0;
while (currStr.charAt(pos) != ’ ’) {
pos++;
}
// chars 0 to pos-1 contain first number
// copy these chars and convert them to an int
char[] charArr = new char[pos];
currStr.getChars(0, pos, charArr, 0);
str = new String(charArr);
83
5.4. STRINGS
n = Integer.valueOf(str).intValue();
// delete these chars by copying
// the rest of the string
charArr = new char[currStr.length() - pos];
currStr.getChars(pos, currStr.length(), charArr, 0);
currStr = new String(charArr);
// define the array of the right length n
vec = new int[n];
// read the n numbers into the array
for (int i = 0; i < n; i++) {
// delete leading white space
currStr = new String(currStr.trim() + " ");
// find first blank
pos = 0;
while (currStr.charAt(pos) != ’ ’) {
pos++;
}
// chars 0 to pos-1 contain first number
// copy these chars and convert them to an int
charArr = new char[pos];
currStr.getChars(0, pos, charArr, 0);
str = new String(charArr);
vec[i] = Integer.valueOf(str).intValue();
// delete these chars by copying
// the rest of the string
charArr = new char[currStr.length() - pos];
currStr.getChars(pos, currStr.length(), charArr, 0);
currStr = new String(charArr);
}
// Construct the output string in a StringBuffer object
StringBuffer outputBuf = new StringBuffer();
for (int i = 0; i < vec.length; i++) {
outputBuf.append(i + ": "
+ Integer.toString(vec[i]) + "\n");
}
// show string str in output
output.setText(outputBuf.toString());
}
84
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
}
5.4.3
Manipulation von Strings mit der Klasse StringTokenizer und Exception Handling
Das bessere Programm nutzt die Java-Klasse StringTokenizer. Diese Klasse hat einen Konstruktor StringTokenizer(str), der den String str in die durch white space getrennten Teile zerlegt,
die sogenannten Tokens.5 Die Klasse verfügt ebenfalls über sehr mächtige Methoden, um mit diesen
Tokens umzugehen, etwa
hasMoreTokens()
nextToken()
countTokens()
// liefert Wert true, falls noch weitere Tokens existieren
// liefert das nächste Token als String zurück.
// Dieses gilt dann als “verbraucht”
// gibt die Anzahl der Tokens als int-Wert zurück
Um mögliche Fehler abzufangen, verwenden wir das Exception-Handling mit try und catch. Sie hat
die folgende Syntax (Darstellung gemäß Codekonvention):6
try {
statements
} catch(Exceptionklasse1 Exceptionvariable1 ) {
Statements für Exceptionbehandlung
...
} catch(Exceptionklassek Exceptionvariablek ) {
Statements für Exceptionbehandlung
}
Falls nötig, kann ein finally Block angehängt werden.
finally {
weitere Anweisungen für Exceptionbehandlung
}
Tritt bei Abarbeitung des try Blocks eine Exception auf, so wird in den zugehörigen catch Block
gesprungen und dieser durchgeführt. Anschließend wird der optionale finally Block durchgeführt
und danach der umgebende Programmtext weiter abgearbeitet.
Um Exceptions mit try und catch abfangen zu können, muss man in selbsr geschriebenen Methoden auch dafür sorgen, dass Exceptions erzeugt werden. Dies geschieht mit der throw Anweisung
im Rumpf der Methode. Im Kopf der Methode wird zusätzlich durch throws ExceptionKlasse dem
Compiler kenntlich gemacht, dass die Methode Ausnahmen erzeugen kann, die in anderen Programmteilen mit try und catch abgefangen werden können. Als Beispiel betrachten wir die Berechnung
des größten gemeinsamen Teilers als Methode:
5 Auch
6 Zur
andere Trennzeichen als white space sind möglich; siehe die Java Dokumentation.
vollständigen Syntax siehe die Java Dokumentation.
85
5.4. STRINGS
int ggT(int a, int b) throws IllegalArgumentException {
if ((a <= 0) || (b <= 0)) {
throw new IllegalArgumentException(
"No negative numbers allowed.");
}
// insert code for computing the ggT
Hierin ist IllegalArgumentException eine in Jave bereits vorhandene Klasse, aus der in der newAnweisung new IllegalArgumentException("No negative numbers") ein Konstruktor verwendet wird, dem man Strings als Argumente übergeben kann. Diese Strings können in einer catchAnweisung entsprechen für Fehlermeldungen verwendet werden.
Wird die throw-Anweisung ausgeführt, so wird die Methode danach beendet und der Rest des Rumpfes nicht ausgeführt.
In unserem Beispiel für Strings können drei Typen von Exceptions auftreten, die ebenfalls in Java als
Klassen vorhanden sind:
NumberFormatException
NoSuchElementException
NegativeArraySizeException
Der gelesene String stellt keine int-Zahl dar
Das erste oder nächste Token existiert nicht
Die erste gelesene Zahl ist negativ
Sie werden entsprechend abgefangen und in Mitteilungen an den Benutzer umgesetzt. Um den Typ
der Exception zu ermitteln, reicht es, den entsprechenden Fehler beim Ablauf zu erzeugen und die
Meldung der Laufzeitumgebung auf dem Bildschirm zu studieren.7
Programm 5.2 StringTokenizerDemo.java
// StringTokenizerDemo.java
//
// Demonstrates use of the classes String and StringTokenizer
// together with arrays
//
// A string of integer numbers that are separated by white space
// is converted into an array of integers
//
// Assumptions:
// input is a sequence of numbers n, a0, a1, a2 ...
// n specifies the length of an array
// a0, a1, ... are the array entries, there must be at least n
// numbers are separated by white space
//
// now we do exception handling
7 Unter
Unix/Linux die Shell, in der der Appletviewer gestartet wurde.
86
import
import
import
import
import
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
java.awt.*;
java.applet.Applet;
java.util.*;
// for class StringTokenizer
java.awt.event.ActionListener;
java.awt.event.ActionEvent;
public class StringTokenizerDemo extends Applet {
TextArea input, output;
Button startBtn;
int[] vec;
// setup the graphical user interface components
public void init() {
//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
//set Font
setFont(new Font("Times", Font.PLAIN, 24));
input = new TextArea("4 10 20 30 40 ", 5, 30);
add(input);
// put input on applet
startBtn = new Button("Start");
startBtn.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(startBtn);
output = new TextArea(10, 30);
add(output);
}
// process user’s action on the input text field
public void runDemo() {
output.setText("");
String inputStr = input.getText();
String str = "";
StringTokenizer inputTokens = new StringTokenizer(inputStr);
try {
int n; // will be the first number that determines
87
5.4. STRINGS
//
the length of vec
str = inputTokens.nextToken();
// throws NoSuchElementException
// if there is no next token
n = Integer.valueOf(str).intValue();
// define
vec = new
// throws
// if str
the array of the right length n
int[n];
NegativeArraySizeException
is a negative int
// read the n numbers into the array
for (int i = 0; i < n; i++) {
str = inputTokens.nextToken();
// throws NoSuchElementException
// if there is no next token
vec[i] = Integer.valueOf(str).intValue();
// throws NumberFormatException
// if str is not an int
}
// Construct the output string in a StringBuffer object
StringBuffer outputBuf = new StringBuffer();
for (int i = 0; i < vec.length; i++) {
outputBuf.append(i + ": "
+ Integer.toString(vec[i]) + "\n");
}
output.setText(outputBuf.toString());
} catch (NoSuchElementException e) {
output.setText("Zu wenige Zahlen eingegeben!.");
} catch (NumberFormatException e) {
output.setText("Bitte nur ganze Zahlen eingeben.");
} catch (NegativeArraySizeException e) {
output.setText("Die erste Zahl muss "
+ "eine positive ganze Zahl sein.");
}
}
}
88
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
5.5
Records
Kennzeichen der Datenstruktur Record sind:
• feste Komponentenzahl,
• direkter Zugriff auf Komponenten mittels Namen,
• heterogene Komponententypen, dafür keine Berechnung von Indizes.
Die Komponenten von Records heißen auch Felder, oder Datenfelder. Record Typen werden in der
Pseudosprache wie folgt deklariert
type StudentRec = record
String
name;
String
adresse;
Integer matrikelnr;
String
fach;
end record
Selektion von Komponenten findet mit dem Punkt (.) statt.
StudentRec student;
student.matrikelnr := 127538;
In Java gibt es keine eigene Datenstruktur Record, sie kann jedoch als Spezialfall einer Klasse aufgefasst werden. Klassen enthalten zusätzlich zu den Datenfeldern Konstruktoren und Methoden (Operationen), die auf den Datenfeldern operieren. Details über Klassen werden in Kapitel 7.3 behandelt.
Eine rudimentäre Klasse für Studenten könnte wie folgt aussehen:
public class Student {
public String name;
public String adresse;
public int matrikelnr;
public String fach;
public int fachsemester;
// Konstruktor fuer Neueinschreibung
public Student(String aktName, String aktAdr, int aktMatrNr,
String aktFach) {
name = aktName;
adresse = aktAdr;
matrikelnr = aktMatrNr;
fach = aktFach;
fachsemester = 1;
}
5.6. LISTEN
89
public void changeAddress(String newAdr){
adresse = newAdr;
}
// weitere Konstruktoren und Methoden
}
Eine typische Verwenung der Klasse Student ist ein Array, dessen Komponenten Studenten sind, etwa
alle Studenten der Coma:
Student[] comaTeilnehmer = new Student[200];
comaTeilnehmer[27] = new Student("Hans Meier", "unbekannt", 20307, "TWM");
5.6
Listen
Eine Liste ist eine lineare Datenstruktur. Ihre Komponenten werden Items oder Listenelemente genannt. Das erste Element heißt Anfang oder Kopf (head) der Liste, das letzte Element heißt Ende
(tail). Kennzeichen der Datenstruktur Liste sind:
• veränderliche Länge (Listen können wachsen und schrumpfen),
• homogene Komponenten (elementar oder strukturiert),
• sequentieller Zugriff auf Komponenten durch einen (impliziten) Listenzeiger, der stets auf ein
bestimmtes Element der Liste zeigt, und nur immer ein Listenelement vor oder zurück gesetzt
werden kann8 .
Ein Beispiel sind Güterwaggons eines Zuges an einer Verladestation (= Listenzeiger), die nur einen
Waggon zur Zeit beladen kann. Listen werden immer dann angewendet, wenn man an beliebigen
Stellen noch Elemente einfügen oder löschen möchte. So sind in der Textverarbeitung Zeilen Listen
von Wörtern, Paragraphen Listen von Zeilen, Kapitel Listen von Paragraphen usw.
Typische Listenoperationen sind das Einfügen in eine Liste, der sequentielle Übergang zum nächsten
Element, das Löschen eines Elementes usw. Wir werden sie nachstehend als Java Methoden einer
Klasse LinkedList wiedergeben, die ihrerseits auf der Klasse ListNode für Listenelemente beruht.
Dies greift dem später eingeführten Klassenkonzept von Java vor (vgl. Kapitel 7.3). Der hier gewonnene Vorteil ist, dass diese Methoden bereits genutzt werden können, ohne ihre Implementationsdetails
zu kennen.
5.6.1
Einschub: Javadoc
Wir verwenden außerdem eine weitere, wichtige Methode um Java-Code zu dokumentieren. Neben
den Kommentar-Zeichen
8 sowie
auf die Stelle hinter dem letzten Element, wodurch der Zustand “end of list” gekennzeichnet wird.
90
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
//
/* ... */
alles ab hier bis Ende der Zeile ist Kommentar
alles zwischen /* und */ ist Kommentar
gibt es in Java die zusätzlichen Kommentar-Klammern
/** ... */
Sie wirken wie /* und */, aber dienen als Input für das Dokumentationsprogramm javadoc. Ruft
man dieses Programm mit
javadoc *.java
auf, so erstellt es für jede .java Datei der momentanen Directory eine entsprechende .html Datei, in
der Informationen abgelegt werden, die aus den /** ... */ Kommentaren und den Deklarationen
von Klassen, Methoden, Konstruktoren, Feldern usw. gewonnen werden. Dazu müssen die Kommentare den Deklarationen vorausgehen und sie sinnvoll dokumentieren. Zusätzlich erstellt javadoc in
der Datei tree.html eine Einordnung der eigenen Klassen in die Klassenhierarchie von Java (durch
die man sich dann auch per html-Browser bewegen kann), in der Datei AllNames.html einen Index
aller Klassen, Methoden, Konstruktoren, Feldern usw. aus den analysierten .java Dateien und noch
vieles, vieles mehr.
Dieses Dokumentationswerkzeug ist sehr mächtig und bietet einen guten Zugriff auf Klassen, die
Klassenhierarchie, und die Methoden einer Klasse. Die /** ... */ Kommentare können mit htmlFormatierungsbefehlen angereichert werden, wie etwa
<code> ... </code>
erzeugt Schreibmaschinentext
Außerdem sind spezielle tags verwendbar um die html-Seiten entsprechend zu strukturieren, z. B.:
@see
@author
@version
@param
@return
@exception
für Verweise
Nennung des Autors
Versionsnummer
Erläuterung der Parameter
Erläuterung der Rückgabe von Methoden
Erläuterung von Ausnahmen
Als Beispiel schaue man sich die folgenden, entsprechend dokumentierten Dateien ListNode und
LinkedList.java an. Ausschnitte der von javadoc aus LinkedList.java erzeugten html-Datei
LinkedList.html sind in Abbildung 5.2 und Abbildung 5.3 in Browsersicht dargestellt. Weitere
Informationen über javadoc erhält man in der Javadokumentation oder mit man javadoc in den
Unix/Linux Manual Pages.
5.6.2
Eine Klasse für Listen
Programm 5.3 ListNode.java
5.6. LISTEN
91
Abbildung 5.2: Anfang der von javadoc erzeugten Datei LinkedList.html.
/**
* A generic class for list nodes.
* A list node consists of a data component and a link to the next list node
*/
public class ListNode {
/**
* data component is of the general type Object
*/
private Object data;
92
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Abbildung 5.3: Methodenteil der von javadoc erzeugten Datei LinkedList.html.
/**
* next points to the next list node
*/
private ListNode next;
/**
* Construct a list node containing a specified object
5.6. LISTEN
* @param o the object for the list node
*/
public ListNode(Object o) {
data = o;
next = null; // leave next uninitialized
}
/**
* Return the data in this node.
* @return the data of this node.
*/
public Object getData() {
return data;
}
/**
* Set the data of the node.
* @param o the data object for the node.
*/
public void setData(Object o) {
data = o;
}
/**
* Return the next node.
* @return the reference to the next node.
*/
public ListNode getNext() {
return next;
}
/**
* Set the next node.
* @param n the next node.
*/
public void setNext(ListNode n) {
next = n;
}
}
Programm 5.4 LinkedList.java
import java.util.NoSuchElementException;
/**
93
94
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
* The <code>LinkedList</code> class implements a dynamically growable
* list of objects. <code>LinkedList</code> administers a cursor to
* point to the active list node.
*
* @see ListNode
*
* Each list is a collection of ListNodes along with an
* implicit list cursor in the range 1..n+1, where n is
* the current length of the list
*/
public class LinkedList {
/**
* a list pointer to the first node
*/
private ListNode firstNode;
/**
* a list pointer to the current node
*/
private ListNode currNode;
/**
* a list pointer to node preceding the current node
*/
private ListNode prevNode;
/**
* Constructs an empty list.
*
*/
public LinkedList() {
firstNode = prevNode = currNode = null;
}
/**
* Tests if this list has no entries.
*
* @return <code>true</code> if the list is empty; <code>false</code>
* otherwise
*/
public boolean isEmpty() {
return (firstNode == null);
}
5.6. LISTEN
95
/**
* Set the list cursor to the first list element.
*/
public void reset() {
currNode = firstNode;
prevNode = null;
}
/**
* Test if the list cursor stands behind the last element of the list.
*
* @return <code>true</code> if the cursor stands behind the last
* element of the list; <code>false</code> otherwise
*/
public boolean endOfList() {
return (currNode == null);
}
/**
* Advance the list cursor to the next list node
* Throws <code>NoSuchElementException</code> if
* <code>endOfList() == true</code>
*/
public void advance() throws NoSuchElementException {
if(endOfList()) {
throw new NoSuchElementException(
"No further list node.");
}
prevNode = currNode;
currNode = currNode.getNext();
}
/**
* Return the value of the current node.
* Throws <code>NoSuchElementException</code> if
* there is no current node
*/
public Object currentData() throws NoSuchElementException {
if(currNode == null) {
throw new NoSuchElementException(
"No current list node.");
}
return currNode.getData();
96
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
}
/**
* Inserts a new list node before the current node.
* If the list is empty insert at front.
* The cursor points to the new list node.
*
* @param someData the object to be added.
*/
public void insertBefore(Object someData) {
ListNode newNode = new ListNode(someData);
if (isEmpty()) {
firstNode = currNode = newNode;
} else {
newNode.setNext(currNode);
currNode = newNode;
if (prevNode != null) {
prevNode.setNext(newNode);
} else {
firstNode = newNode;
}
}
}
/**
* Inserts a new list node after the current node. The cursor points
* to the new list node.
* Throws <code>NoSuchElementException</code> if
* there is no current node
*
* @param someData the object to be added.
*/
public void insertAfter(Object someData) throws NoSuchElementException {
ListNode newNode = new ListNode(someData);
if (isEmpty()) {
firstNode = currNode = newNode;
} else {
if (currNode == null) {
throw new NoSuchElementException(
"Cursor not on a valid element.");
}
newNode.setNext(currNode.getNext());
currNode.setNext(newNode);
prevNode = currNode;
97
5.6. LISTEN
currNode = newNode;
}
}
/**
* Delete the current node from the list.
* Throws <code>NoSuchElementException</code> if
* there is no current node
*/
public void delete() throws NoSuchElementException {
if (currNode == null) {
throw new NoSuchElementException(
"No element for deletion.");
}
if (currNode == firstNode) {
firstNode = currNode = currNode.getNext();
} else {
currNode = currNode.getNext();
prevNode.setNext(currNode);
}
}
}
Als Demonstration der Listenoperationen behandeln wir folgendes Beispiel.
Beispiel 5.5 (Einlesen eines Strings in eine Liste)
Es sollen folgende Aktionen ausgeführt werden:
– Einlesen eines Strings in eine Liste von char,
– Ausgabe der Liste auf dem Bildschirm,
– Löschen des Anfangs der Liste bis zu einem vorgegebenen Zeichen (ergibt die leere Liste, falls
das Zeichen nicht vorkommt),
– Ausgabe der gekürzten Liste.
Dies leistet das nachstehende Java Applet. Es verwendet als Datenteil data der Listenelemente CharacterObjekte, da ja nur Objekte in Listenknoten erlaubt sind. Dazu müssen mit
Ch = new Character(ch);
einzelne char-Zeichen ch in entsprechende “Wrapperobjekte” Ch der Klasse Character eingepackt
werden. Um die allgemeinen Datenobjekte der Liste dann wieder als Character-Objekte behandeln
zu können, ist ein Casting erforderlich. Die Anweisungen
98
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Ch = (Character) stringList.currentData();
ch = Ch.charValue();
packen das im aktuellen Listenknoten enthaltene char-Zeichen aus und weisen es der Variablen ch
zu.
Programm 5.5 List-Demo.java
/*
* List-Demo.java
*
* Represents strings as list of char and manipulates them
*/
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class ListDemo extends Applet {
String str;
StringBuffer strBuf;
char ch;
Character Ch, Cha; // needed as subclass of Object
// setup the graphical user interface components
// and initialize labels and text fields
Label inputPrompt1, inputPrompt2;
TextField input1, input2, output1, output2;
Panel p1, p2, p3, p4, p5;
public void init() {
//set layout
setLayout(new GridLayout(5, 1));
//set Font
setFont(new Font("Times", Font.PLAIN, 24));
p1
p2
p3
p4
p5
=
=
=
=
=
new
new
new
new
new
Panel();
Panel();
Panel();
Panel();
Panel();
p1.setLayout(new FlowLayout(FlowLayout.LEFT));
inputPrompt1 = new Label("Schreiben Sie einen String "
99
5.6. LISTEN
+ "und beenden Sie ihn mit Return.");
p1.add(inputPrompt1);
p2.setLayout(new FlowLayout(FlowLayout.LEFT));
input1 = new TextField("Hello World", 40);
input1.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
p2.add(input1);
p3.setLayout(new FlowLayout(FlowLayout.LEFT));
output1 = new TextField("Der String ist: Hello World", 40);
p3.add(output1);
output1.setEditable(false);
p4.setLayout(new FlowLayout(FlowLayout.LEFT));
inputPrompt2 = new Label("Geben Sie das Zeichen an, "
+ "bis zu dem gelöscht wird:");
p4.add(inputPrompt2);
input2 = new TextField("W", 1);
input2.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
p4.add(input2);
p5.setLayout(new FlowLayout(FlowLayout.LEFT));
output2 = new TextField("Gekürzter String: World", 40);
p5.add(output2);
output2.setEditable(false);
add(p1);
add(p2);
add(p3);
add(p4);
add(p5);
}
// process user’s action
public void runDemo() {
100
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
// list for representing string
LinkedList stringList = new LinkedList();
// read input string
str = input1.getText();
// represent str as a list of Character
// use length() and charAt() methods of class String
for (int i = 0; i < str.length(); i++) {
ch = str.charAt(i);
// get the char
Ch = new Character(ch);
// convert it to a
// Character object
stringList.insertAfter(Ch); // insert it in the list
}
// use list methods to write string in first output field
if (stringList.isEmpty()) {
output1.setText("Der String ist leer.");
} else {
stringList.reset();
strBuf = new StringBuffer();
while (! stringList.endOfList()) {
// cast Object to Character
Ch = (Character) stringList.currentData();
// convert Character to char ch
ch = Ch.charValue();
// append ch to strBuf
strBuf.append(ch);
stringList.advance();
}
output1.setText("Der String ist: "
+ strBuf.toString());
}
// read char until which will be deleted
ch = input2.getText().charAt(0);
if (! stringList.isEmpty()) { // nothing to do otherwise
stringList.reset();
while (((Character) stringList.currentData())
.charValue() != ch) {
stringList.delete();
// leave while loop if stringList is empty
if (stringList.isEmpty()) break;
}
}
101
5.7. STACKS
// write remaining string in second output field
if (stringList.isEmpty()) {
output2.setText("Die gekürzte Liste ist leer.");
} else {
strBuf = new StringBuffer();
while (! stringList.endOfList()) {
Ch = (Character) stringList.currentData();
ch = Ch.charValue();
strBuf.append(ch);
stringList.advance();
}
output2.setText("Gekuerzte Liste: "
+ strBuf.toString());
}
}
}
5.7
Stacks
Stacks sind eine eingeschränkte Form von Listen, bei denen das Einfügen und Löschen nur am Kopf
(genannt top) möglich ist. Als Liste gesehen kann der Listenzeiger also nur auf das erste Element
zeigen. Ein Beispiel ist ein Bücherstapel in einem engen Karton, man hat immer nur auf das obere
Buch Zugriff. Man nennt daher Stacks auch Last-In, First-Out oder LIFO Listen.
Wie bei Listen sehen wir uns die Stack Operationen ausgedrückt als Methoden einer Java Klasse an.9
Programm 5.6 Stack.Java
import java.util.NoSuchElementException;
/**
* The <code>Stack</code> class implements a Stack
* of objects.
*
* @see ListNode
*/
public class Stack {
/**
* a list pointer to the first node
*/
private ListNode top;
9 Es gibt in Java eine eigene Klasse Stack im Package java.util, die auf der Klasse Vector basiert. Aus didaktischen
Gründen verwenden wir hier eine eigene Klasse.
102
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
/**
* Constructs an empty stack.
*
*/
public Stack() {
top = null;
}
/**
* Tests if this stack has no entries.
*
* @return <code>true</code> if the stack is empty;
* <code>false</code> otherwise
*/
public boolean isEmpty() {
return (top == null);
}
/**
* Return the value of the top node.
*/
public Object top() throws NoSuchElementException {
if (top == null) {
throw new NoSuchElementException("No top node.");
}
return top.getData();
}
/**
* Inserts a new stack node at the top with <code>someData</code>
* as data
*
* @param someData the data object of the new node
*/
public void push(Object someData) {
ListNode newNode = new ListNode(someData);
if (isEmpty()) {
top = newNode;
} else {
newNode.setNext(top);
top = newNode;
}
}
103
5.7. STACKS
/**
* Delete the top node from the stack.
*/
public void pop() throws NoSuchElementException {
if (top == null) {
throw new NoSuchElementException("No element "
+ "for deletion.");
}
top = top.getNext();
}
}
Diese Klasse verwaltet wieder Objekte der allgemeinen Klasse Object. Betrachten wir die drei CharObjekte.
Character A = new Character(’a’);
Character B = new Character(’b’);
Character C = new Character(’c’);
So erzeugt die Folge der Java Anweisungen
Stack myStack = new Stack();
myStack.push(A);
myStack.push(B);
myStack.push(C);
StringBuffer strBuf = new StringBuffer();
while(! myStack.isEmpty()) {
strBuf.append((Character) myStack.top());
myStack.pop();
}
String str = strBuf.toString();
die folgende Folge von Belegungen der Instanz mystack:
leer
a
b
a
c
b
a
b
a
a
leer
Top
Nach Abarbeitung der while-Schleife hat str den Wert cba.
Stacks sind fundamental für viele Aufgabenstellungen der Informatik, z. B.
• Laufzeitverwaltung von Funktions- und Prozeduraufrufen,
104
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
• Realisierung von Rekursion,
• Auswertung von Ausdrücken in Postfixnotation, z. B. in HP-Taschenrechnern, etwa
Eingabe:
Stackfolge:
abc+*
leer
a
b
a
c
b
a
c+b
a
(c+b)*a
Top
Beispiel 5.6 (Erkennung von korrekten Klammerausdrücken)
Klammerausdrücke sind auf Korrektheit zu überprüfen und einander entsprechende Klammern sind
zu paaren.
So ist z. B. (()()))() nicht korrekt, aber {{}{{}}} korrekt. Die entsprechende Paarung des zweiten
Ausdrucks ist
{ { } { { } } }
Dies geschieht mit dem folgenden Algorithmus.
Algorithmus 5.1 (Erkennung von korrekten Klammerausdrücken)
1. Lese die Folge der Klammern von links nach rechts.
2. Falls “(”, so pushe diese auf den Stack.
3. Falls “)” so poppe eine “(” vom Stack, erkläre diese als zur momentan gelesenen “)”
gehörig.
4. Erkläre den Klammerausdruck als korrekt, falls der Stack am Ende leer ist,
jedoch zwischendurch nie vom leeren Stack gepoppt wird.
Als konkretes Beispiel betrachten wir (1 (2 (3 )4 (5 )6 )7 (8 )9 )10 , wobei die Klammern zur besseren
Identifizierung mit Indizes versehen sind. Dann ergibt sich in Algorithmus 5.1 die in Abbildung 5.4
dargestellte Stackfolge.
Sie zeigt, dass der Ausdruck korrekt ist mit der folgenden Klammerung:
(1 (2 (3 )4 (5 )6 )7 (8 )9 )10
Bei dem Beispiel (1 )2 )3 (4 )5 ergibt sich die Stackfolge
105
5.7. STACKS
0
leer
1
(1
2
(2
3
(3
4
(2
5
(5
6
(2
(1
(2
(1
(2
(1
(1
7
(1
8
(8
9
(1
10
leer
⇓
Paar
(8 )9
⇓
Paar
(1 )10
(1
(1
⇓
Paar
(3 )4
⇓
Paar
(5 )6
⇓
Paar
(2 )7
Abbildung 5.4: Ein Beispiel zu Algorithmus 5.1.
0
leer
1
(1
2
leer
3
Error
Also wird (1 )2 )3 (4 )5 als nicht-korrekter Klammerausdruck erkannt.
Satz 5.1 (Erkennung korrekter Klammerausdrücke) Algorithmus 5.1 erkennt genau die korrekten
Klammerausdrücke als korrekt und identifiziert zueinander gehörende Klammerpaare richtig.
Beweis: Der Beweis wird durch Induktion nach der Anzahl k der Klammern im Ausdruck geführt.
Induktionsanfang: k = 2. Offenbar wird bei k = 2 Klammern genau () als korrekt erkannt, und die
Klammerkorrespondenz hergestellt.
Induktionsvoraussetzung: Die Methode arbeitet für alle Klammerausdrücke der Länge < k korrekt
(k > 2).
Induktionsschluss auf die Länge k:
1. Ist A ein korrekter Klammerausdruck, so sind die Stack Bedingungen erfüllt und korrespondierende Klammern werden richtig ermittelt.
Sei A korrekt mit der Länge k > 2. Dann gibt es aufgrund der Syntax für korrekte Klammerausdrücke (vgl. 4.1) 2 Fälle: a) A = B ·C oder b) A = (B), wobei B,C kürzere korrekte Klammerausdrücke sind, auf die dann die Induktionsvoraussetzung zutrifft.
a) Da auf B,C die Induktionsvoraussetzung zutrifft, werden sie als korrekt erkannt und werden die zugehörigen Korrespondenzen richtig ermittelt.
Die Stackfolge für A ergibt sich als Konkatenation der Stackfolgen von B und C. Also
gelten auch in A die Stack Bedingungen.
Da der Stack am Ende von B leer ist, können auch innerhalb von A Klammern aus B nur
mit Klammern aus B korrespondieren. Diese Korrespondenzen werden nach Induktionsvoraussetzung richtig erkannt. Das gilt entsprechend auch für C.
b) Da auf B die Induktionsvoraussetzung zutrifft, wird B als korrekt erkannt und werden die
zugehörigen Korrespondenzen richtig ermittelt. Dies bedeutet, dass sich in A die äußeren
106
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Klammern entsprechen müssen, da alle Klammern in B bereits für die Korrespondenzen
innerhalb von B “verbraucht” werden.
Die Stackfolge für A ergibt sich also aus der Stackfolge von B durch Anhängen der ersten
“(” von A als unterste Komponente des Stacks. Also gelten auch in A die Stack Bedingungen.
Da der Stack am Ende von B genau noch die erste “(” von A enthält, werden die äußeren
Klammern als korrespondierend erkannt. Da innerhalb von B stets die erste “(” von A als
unterste Komponente im Stack enthalten ist, werden die Korrespondenzen innerhalb von
B auch nach Induktionsvoraussetzung richtig erkannt.
Abbildung 5.5 illustriert die Stackfolgen für die beiden auftretenden Fälle.
2. Sind die Stack Bedingungen erfüllt, so ist A ein korrekter Klammerausdruck.
Betrachte die erste Stelle `, an der der Stack nach der zugehörigen Push/Pop Operation leer ist.
Es gibt 2 Fälle: a) ` < k oder b) ` = k.
a) Aus ` < k folgt, dass sich A zerlegen lässt in einen ersten Teil B := a1 a2 . . . a` und einen
zweiten Teil C := a`+1 . . . ak ; ai ∈ {(, )}. Da die Stack Bedingungen in A erfüllt sind, sind
sie nach der Wahl von ` auch in B und C erfüllt, und die Induktionsvoraussetzung trifft
wegen ` < k auf B und C zu. Also sind B und C korrekt, und damit nach den Syntaxregeln
auch A = BC.
b) Aus ` = k folgt, dass in der Stackfolge die zuerst gelesene “(” von A bis zum Schluss auf
dem Stack bleibt. Da die Stack Bedingungen erfüllt sind, muss die letzte Klammer von A
eine “)” sein, die dann mit der ersten Klammer korrespondiert. Also ist A von der Form
A = (B).
Die Stackfolge von B ist dann gleich der Stackfolge von A ohne die erste Klammer “(”
von A. Also folgt, dass auch die Stack Bedingungen für B erfüllt sind. Da B kürzer als A
ist, ist B nach Induktionsvoraussetzung korrekt, und damit nach den Syntaxregeln auch A.
Aus 1 und 2 folgt die Behauptung.
Wir betrachten jetzt eine Implementation von Algorithmus 5.1 für Strings, der außer den Klammern (
und ) auch andere Zeichen enthalten kann. Der Test auf korrekte Klammern bezieht sich auf ( und ).
Dazu verwenden wir ein Hilfsarray
partner : Array von int
Am Ende soll partner[i] die
Dabei soll gelten (mit k > 0):

 k
partner[i] :=

−1
zum Zeichen an Position i im String zugehörige Klammer angeben.
charAt(k) und charAt(i) bilden ein Paar (..) oder
charAt(i) und charAt(k) bilden ein Paar (..),
charAt(i) 6= (,).
5.7. STACKS
107
Fall a)
(B1
leer
p p p
leer
(C1
p p p
leer
Stackfolge für B
Stackfolge für C
Fall b)
leer
(1
p p p
(2
(1
leer
(1
Stackfolge für B mit (1 als zusätzlicher,
unterer Komponente
Abbildung 5.5: Die Stackfolgen aus dem Beweis von Satz 5.1.
Für den String ((a+b)(-1))/(2+c) ergibt sich
10
0
5
-1
2
-1
-1
1
4
9
-1
6
-1
8
6
0
-1
10
16
12
-1
-1
14
-1
12
16
als Belegung für das Array partner.
Um diese Belegung von partner zu erreichen wird statt der “(” in einem Stack jeweils die Position
i im String abgespeichert, d. h. man definiert den benötigten Stack als Stack von Integer Objekten.
Abbildung 5.6 zeigt ein Struktogramm für die Verfeinerung von Algorithmus 5.1 mit diesen Datenstrukturen. Eine Implementierung in Java gibt Programm 5.7.
Programm 5.7 StackDemo.java
import java.awt.*;
import java.applet.Applet;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
/**
* reads string of parantheses and other chars
* uses stack to check for correct parantheses rules
* @see Stack.java
*/
public class StackDemo extends Applet {
// setup the graphical user interface components
// and initialize labels and text fields
108
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Ermittle die Länge n des gegebenen Strings str
Initialisiere das Array partner zu -1,...,-1
Definiere den Stack s {Einrichten des leeren Stacks}
i := 0 {Initialisierung der Zählvariablen}
korrekt := true {Boolesche Variable; bleibt true bis festgestellt wird, dass
str nicht korrekt ist}
i < n and korrekt
hhhh
hh
’(’
hhh
hh
str.charAt(i)
hhhh
hhh
hhhh
’)’
hhh
hhh
X
XX
XXX
s.isEmpty()
X
XX
true
false
XX
hhh
s.push(i)
{merke die Position
der “(” im Stack}
korrekt := false
{es kann bei “)”
nicht vom Stack gepoppt werden}
m := s.top()
{merke Position der
“(” in der Variablen
m}
s.pop()
{entferne “(”}
partner[i] := m
partner[m] := i
i := i+1
` ``
```
true
korrekt and s.isEmpty()
```
```
```
``
Gebe partner aus {Der Ausdruck
ist korrekt}
false
Fehlermeldung: Der Ausdruck ist an
Stelle i-1 inkorrekt
Abbildung 5.6: Struktogramm für Algorithmus 5.1.
Label inputPrompt;
TextField input;
TextArea output;
public void init() {
//set layout
setLayout(new FlowLayout(FlowLayout.LEFT));
setFont(new Font("Times", Font.PLAIN, 24));
Font fixedWidthFont = new Font("Courier", Font.PLAIN, 24);
109
5.7. STACKS
inputPrompt = new Label("Schreiben Sie einen String und "
+ "beenden Sie ihn mit Return.");
add(inputPrompt);
input = new TextField("((a+b)-(c-d))/(x-y)", 50);
input.setFont(fixedWidthFont);
input.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
runDemo();
}
});
add(input);
output = new TextArea(10, 50);
output.setFont(fixedWidthFont);
add(output);
}
public void runDemo() {
String str;
StringBuffer outputBuf = new StringBuffer();
Integer intObj;
// read the string
str = input.getText();
// define partner and initialize to -1 ... -1
int[] partner = new int[str.length()];
int i;
for (i = 0; i < str.length(); i++) {
partner[i] = -1;
}
// define Stack object intStack
Stack checkStack = new Stack();
// main loop, use stack for checking correctness
i = 0;
boolean correct = true;
int m;
while (i < str.length() && correct) {
switch (str.charAt(i)) {
110
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
case ’(’ : {
intObj = new Integer(i);
// remember position i of ’(’ on stack
checkStack.push(intObj);
break;
}
case ’)’ : {
if (checkStack.isEmpty()) {
// no corresponding ’(’
correct = false;
} else {
// corresponding ’(’ is in position m
intObj = (Integer) checkStack.top();
m = intObj.intValue();
// remove the position of ’(’
checkStack.pop();
// positions i and m have
// coresponding ’(’ and ’)’
partner[i] = m;
partner[m] = i;
}
break;
}
default : { // neither ’(’ nor ’)’ at position i
;
// nothing to do,
// partner[i] is already -1
}
}//end switch
i++;
}//end while: if not correct, then at
// position i-1 (counting from 0)
// output correct pairs or report error
if (correct && checkStack.isEmpty()) {
// correct paranthesises
// output correct pairs
outputBuf.append("Der String ist korrekt mit "
+ "folgender Klammerung:\n");
outputBuf.append(str);
i = 0;
while (true) {
// look for next ’(’
while (str.charAt(i) != ’(’
111
5.8. QUEUES (WARTESCHLANGEN)
&& i < str.length() - 1) {
i++;
}
// leave while loop if at end of str
if (i == str.length() - 1) break;
// newline on current ’(’
outputBuf.append(’\n’);
// indent until current ’(’
for (m = 0; m < i ; m++) {
outputBuf.append(’ ’);
}//end for
// write current ’(’
outputBuf.append(str.charAt(i));
for (m = i + 1; m < partner[i] ; m++) {
// indent until corresponding ’)’
outputBuf.append(’ ’);
}//end for
// write corresponding ’)’
outputBuf.append(str.charAt(partner[i]));
i++;
// increase i for next while loop
}//end while
} else {
outputBuf.append("Die Klammerung ist an Position "
+ i + " nicht korrekt.\n");
// indicate the wrong position
outputBuf.append(str + ’\n’);
// indent until wrong position
for (m = 0; m < i - 1; m++) {
outputBuf.append(’ ’);
}//end for
// write ’!’ at wrong position
outputBuf.append(’!’);
}
output.setText(outputBuf.toString());
}
}
5.8
Queues (Warteschlangen)
Queues sind wie Stacks eingeschränkte Listen, bei denen das Einfügen nur am Ende (rear oder tail)
und das Löschen nur am Kopf (front oder head) möglich ist. Sie bilden also die geeignete Datenstruktur für das, was man im täglichen Leben unter “Warteschlange” versteht. Man nennt daher Queues
112
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
auch First-In, First-Out oder FIFO Listen.
Wie bei Stacks drücken wir die Operationen als Methoden einer Java-Klasse Queue aus. Die Implementation bleibt zunächst offen. 10
/**
* Tests if this queue has no entries.
*
* @return <code>true</code> if the queue is empty; <code>false</code>
* otherwise
*/
abstract public boolean isEmpty();
/**
* Return the value of the current node.
*/
abstract public Object front() throws NoSuchElementException;
/**
* Inserts a new queue node at the rear.
*
* @param <code>someData</code> the object to be added.
*/
abstract public void enqeue(Object someData);
/**
* Delete the front node from the list.
*/
abstract public void dequeue() throws NoSuchElementException;
Queues haben viele Anwendungen, z. B. in Rechnerbetriebssystemen (Verwaltung wartender Jobs,
Pufferung von Ein/Ausgabe) und bei der Simulation von “Wartesituationen” in vielen Algorithmen
und Modellen betrieblicher Abläufe (Abfertigung an Bankschaltern u. a.)
5.9
Zusammenfassung
Die diskutierten Beispiele von Datenstrukturen zeigen:
• Datentypen und zugehörige Operationen bilden eine Einheit und können nicht getrennt gesehen
werden.
10 In Java nennt man solche Methoden abstrakt und kennzeichnet sie durch das Schlüsselwort abstract, vgl. Abschnitt 7.3.5.
113
5.9. ZUSAMMENFASSUNG
• Die Semantik ergibt sich erst durch den Wertebereich und die Operationen (mit den zugehörigen
Axiomen)
Es gibt verschiedene Methoden, um Datentypen zu definieren:
a) Die konstruktive Methode
Hierzu gehört die Definition von Arrays in Java. Die Definition “höherer” Datentypen erfolgt
aus bereits eingeführten Datentypen nach folgendem Muster.
einfache Datentypen
|
↑
Konstruktor Selektor
↓
|
strukturierte Objekte 1. Stufe
|
↑
Konstruktor Selektor
↓
|
strukturierte Objekte 2. Stufe
|
↑
Konstruktor Selektor
↓
|
..
.
niedrigste Stufe, z. B. int
z. B. int[] vec; // Vektoren
z. B. double[][] table; // Matrizen
Aus der Mathematik ist diese fortgesetzte Abstraktion z. B. von Mengen bekannt:
Elemente einer Grundmenge
↓
↑
Mengen von Elementen (Menge)
↓
↑
Mengen von Mengen (Potenzmenge)
b) Die axiomatische Methode
Beispiele hierzu sind die Datenstrukturen Liste, Stack, Queue. Die Definition geschieht implizit
Definition mittels Operationen und deren Eigenschaften in Form von Axiomen.
Auch diese Methode ist in der Mathematik gebräuchlich, z. B.
– Peano Axiome für natürliche Zahlen
– Inzidenzbeziehungen in der Geometrie usw.
Die Vorteile der axiomatischen Methode ist die Abstraktion von der Implementation (Implementationsdetails sind unwesentlich). Dies erlaubt eine genauere Spezifikation und leichtere
Korrekheitsbeweise. Ein (leichter) Nachteil besteht darin, dass i. a. verschiedene Interpretationen (Modelle) einer abstrakten Datenstruktur möglich sind. Daher ist die Übereinstimmung von
Modell und Spezifikation i. a. nicht leicht überprüfbar, insbesondere für ungeübte Benutzer.
114
KAPITEL 5. OBJEKTE, TYPEN, DATENSTRUKTUREN
Zusammenfassend lässt sich feststellen, dass die konstruktive Methode sich bereits als Implementationsvorschrift verstehen lässt, während die axiomatische Methode wesentlich mehr Freiheitsgrade
erlaubt.
5.10
Literaturhinweise
Datenstrukturen werden in nahezu allen Büchern über Entwurf und Analyse von Algorithmen behandelt. Ihre
Realisierung in Java wird ausführlich in [GT04] erläutert.
Die hier gegebene Darstellung lehnt sich an [HR94] an. Dort wird im Gegensatz zu den meisten Büchern
ausführlich auf die Umsetzung von der abstrakten Spezifikation zu Programmen eingegangen, allerdings in
C++. Die hier gegebenen Definitionen bzw. Deklarationen der Java Funktionen für Listen, Stack und Queues
sind eine leichte Modifikation der dort gegebenen Darstellung in C++.
Herunterladen