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++.