Informatik II Prof. Jansen Tim Dauer, [email protected] Sommersemester 2004 Stand: 24. Mai 2004 Der Autor übernimmt keinerlei Garantie für Vollständigkeit! 1 Informatik 2 - Script 1. Teil Inhaltsverzeichnis 1 Grundlagen 4 1.1 Algorithmus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Ausdrücke und Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.4 Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4.1 Das While-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 1.4.2 Das Do-While-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.4.3 Das For-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.4.4 Das If-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.4.5 Das Switch-Statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Einfache Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.5.1 Fließkomma-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.5.2 Integer-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.5.3 Der Typ Boolean . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.5.4 Der Typ Char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 Strings und Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.6.1 Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.6.2 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.6.3 Binäre Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 1.6.4 Problem: Mischen von sortierten Sequenzen . . . . . . . . . . . . . . . . . . . . . . 17 1.6.5 Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1.5 1.6 2 Prinzipien der objektorientierten Programmierung 20 2.1 Objekte, Methoden und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 2.2 Formale Beschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.2.1 Klassen und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 2.2.2 Variablen (Deklaration von Instanz- und Klassenbariablen): . . . . . . . . . . . . . 27 2.2.3 Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 Stacks und Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 2.3 2 2.3.1 Stacks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 2.3.2 Queues (Schlangen) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.4 Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.5 Einfach verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 Realisation eines Stacks durch eine einfach verkettete Liste . . . . . . . . . . . . . 42 Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 2.6.1 47 2.5.1 2.6 Exkurs: Prinzipien der objektorientierten Programmierung . . . . . . . . . . . . . 3 1 Grundlagen 1.1 Algorithmus Ein Algorithmus besteht aus einer Folge von Anweisungen (engl. Statements), die vorswchreiben, was zu tun ist. Einzelne Anweisungen werden durch ein Semikolon angeschlossen. Mögliche Anweisungen: 1. Zuweisung v = w (v Variable, w arithmetischer Ausdruck) 2. bedingte Zuweisung if (...) Bedingung → Boolescher Ausdruck ... → Zweige else ... → Folge von Anweisungen 3. Schleifen while (...) → Bedingung = bool ... → Rumpf = Folge von Anweisungen Algorithmen, die auf Anweisungen basieren, heißen imperative Algorithmen - im Gegensatz zu funktionalen Agorithmen, die auf geschachtelten Funktions abstraktionen basieren. 1.2 Syntax Wir möchten die Syntax von Java im Weiteren etwas formaler beschreiben. Dazu verwenden wir den Erweiterten-Backus-Naur-Formalismus (EBNF). Dieser enthält Regeln, gemäß denen syntaktisch korrekte Programme abgeleitet werden können. 4 Der EBNF enthält 4 Ableitungsregeln: 1. A := BC → Satz A besteht aus B gefolgt von C 2. A := B | C → Satz A besteht entweder aus B oder C 3. A := [B] → Satz A besteht aus B oder nichts 4. A := {B}* → Satz A besteht aus beliebig vielen B’s oder nichts Beispiel: Syntax einer Programmiersprache, die nur natürliche Zahlen programmieren kann: <Programm> := { <Anweisung> } * <Anweisung> := [<Name> = <Term> + <Term>;] <Name> := A|B|C|...|Z <Ziffer> := 0|1|2|...|9 Ein Hauptprogramm in Java, das keine weiteren Klassen verwendet, hat folgende Syntax: <Java Programm> := public static void main (string[] args) { <StatementSequence > } <StatementSequence> := {<Statement>;}* <Statement> := <Assignment>|<whileStatement>|<forStatement>| <DoWhileStatement>|<IfStatement>|<SwitchStatement>| <MethodCall>|<ReturnStatement>|<BreakStatement>| <ContinueStatement>|... 1.3 Ausdrücke und Anweisungen Eine Anweisung ist eine Aktion zur Änderung des Zustands des Programms. Dabei ist ein Zustand die Folge der Werte aller Programmvariablen. Die einfachste Anweisung ist eine Zuweisung: <assignment> := <variable> = <expression> 5 Eine Variable wird durch einen frei wählbaren Namen bezeichnet. Jede Variable hat einen bestimmten Typ (”Boolean”, ”Character”, ”Integer”, etc.) und die Typen auf der linken und der rechten Seite müssen identisch oder zumindest verträglich sein. Ein Variablenname besteht aus einem Buchstaben gefolgt von einer beliebigen Folge von Ziffern und Buchstaben. Ausdrücke sind zusammengesetzt aus Konstanten (Literale), Variablen und Operatoren. Arithmetische Ausdrücke: Konstanten: 17, -52, 3.14 Operatoren: +, -, ∗ , /, % (modulo Operation) Bitweise Operatoren für Integer: ∼: Komplement &: AND !: OR ˆ : XOR « , »: SHIFT-Operatoren ≫: zyklischer SHIFT Boolesche Ausdrücke: Konstanten: true, false Operatoren: && (AND), ||(OR), !(NOT), ˆ(XOR), &(AND), |(OR) && und ||haben gegenüber & und | den Vorteil, dass sie den zweiten Ausdruck auf der rechten Seite nicht mehr auswerten, wenn das Ergebnis nach dem ersten Ausdruck schon feststeht. Zeichenbasierte Ausdrücke: Character-Literale: ’a’, ’ !’ String-Literale: ”Example” Operator: + (Konkatenation) In Java gibt es noch andere Zuweisungen der Form: <assignment> := <variable><op> = <expression> Dies ist äquivalent zu: <assignment> := <variable> <op> <expression> 6 Dabei kann <op> sein: +, -, ∗, /, %, &, |, ˆ, «, », ≫ Beispiel: i += 2; c -= 5; (Diese Operatoren sollten Sparsam verwendet werden.) 1.4 1.4.1 Kontrollstrukturen Das While-Statement Das whileStatement sieht in Java wie folgt aus: <whileStatement > := while(<BooleanExpression ) <BodyStatement > <BodyStatement> := <Statement>|{ <StatementSequence>} Semantik: 1. berechne den Booleschen Ausdruck. Falls Wert = true ist, gehe zu (2), andernfalls beende die Anweisung. 2. führe Anweisung in BodyStatement aus und gehe zu (1) Beispiel: Berechnung der Fakultät: Für eine natürliche Zahl n ist n! = 1 ∗ 2 ∗ 3 ∗ ... ∗ n i = 1; f = 1; while (i<=n) { f = f ∗ i; i = i + 1; } Das DoWhileStatement ist eine Schleife, bei der die Schleifenbedingung am Ende jeder Iteration ausgewertet wird. 7 1.4.2 Das Do-While-Statement <DoWhileStatement> := do <BodyState> while <BoolExp> Die Anweisung im Schleifenrumpf werden zunächst einmal ausgeführt. Am Ende jeder Iteration wird der Boolesche Ausdruck ausgewertet. Ist er wahr, so wiederholt man den Schleifenrumpf. Andernfalls bricht man die Anweisung ab. Beispiel: i = 1; f = 1; do { f = f ∗ i; i = i + 1; } while (i <= n); 1.4.3 Das For-Statement <forStatement> := for([<Init.>];[Cond.];[<Inc.>]) <BodyStatement> <Initialization> → Initialisierung einer Indexvariable oder gemeinsame Deklaration. Beispiele: for(i = 0;<Condition>;<Increment>) for(int i = 0;<Condition>;<Increment>) <Condition> → Boolescher Ausdruck, der zu Beginn jeder Iteration ausgewertet wird. <Increment> → Ausdruck, der die Indexvariable erhöht oer verringert. Wird am Ende jeder Iteration ausgeführt. 8 Beispiel: (a) Berechnung der Fakultät int f = 1; for(int i = 1;i <= n;i++) f = f ∗ i; (b) berechne für natürliche Zahl n die Zahl 2n int p = 1; for(int i = 1;i <= n;i++) p = p ∗ 2; 1.4.4 Das If-Statement Das if-Statement ist formal wie folgt definiert: <ifStatement > := if (<BooleanExpression>) <BodyStatement> [else <BodyStatement >] Die booleschen Ausdrücke nach ”if” und dem optionalen ”else if” werden nacheinander ausgewertet. Sobald ein Ausdruck wahr ist, führt man die zugehörigen Anweisungen im BodyStatement aus, womit die Ausführung des If-Statements abgeschlossen ist. Ist kein boolescher Ausdruck wahr, so wird der ”else”-Zweig ausgeführt (sofern vorhanden). Beispiel: Berechnung der Signumfunktion if (x == 0); sg = 0; else if (x < 0) sg = -1; else sg = 1 ; 9 1.4.5 Das Switch-Statement Das Switch-Statement führt in Abhängigkeit von einem ganzzahligem Ausdruck eine von mehreren Alternativen aus: <SwitchStatement > := switch (<Expression>) { {case (<ConstantExpression>) : [<StatementSequence >] } * [default: <StatementSequence >] } Der Ausdruck nach ”switch” muss einen Integer-Wert liefern und die Ausdrücke nach ”case” müssen Integer-Konstanten und paarweise verschieden sein. Der Ausdruck nach ”switch” wird zunächst ausgewertet und nacheinander mit den Kontanten verglichen. Stimmen zwei Werte überein, so führt man die zugeordneten Anweisungen aus. Andernfalls führt man die Anweisung nach dem optionalen ”default”-Zweig aus. Bei der Ausführung des ”switch”-Statements werden normalerweise alle case-Alternativen durchlaufen. Die kann man durch ein ”break”-Statement am Ende einer StatementSequence unterbinden (mehr später). Beispiel: switch (sg) { case 0 : System.out.println(”Die Zahl ist 0”); case -1 : System.out.println(”Die Zahl ist negativ”); default : System.out.println(”Die Zahl ist positiv”); } 1.5 Einfache Datentypen Jede benutzte Variable muss in eine Variablendeklaration eingeführt werden und hat einen Typ. <Declaration > := [<VariableModifier >] <VariableType > <VariableList > <VariableList > := <VariableName > [=<InstantValue >] { <VariableName > [=<InitialValue] }* 10 Beispiele: int x; int height = 10, width = 20; char c; boolean a; float f; Vaiablen desselben Typs können in einer Liste zusammengefasst werden (müssen aber nicht). Eine Variable kann gleich initialisiert werden. <VariableType > := <SimpleType >|<ComplexType > <SimpleType > := boolean|char|byte|int|short|float|... Die komplexeren Datentypen wie Arrays und Objekte (auch Strings) werden später behandelt. <VariableList > := <VariableName > [=<InstantValue >] { <VariableName > [=<InitialValue] }* Typ boolean char byte short int long float double 1.5.1 Inhalt true, false 1 Zeichen Integer mit Vorzeichen Integer mit Vorzeichen Integer mit Vorzeichen Integer mit Vorzeichen Fließkommazahl Fließkommazahl Fließkomma-Typen float, double Literale: 3.14, 4.52E3 (→ zur Basis 10!) Operatoren: +, -, ∗, / 1.5.2 Integer-Typen byte, short, int, long Literale: 17, -52 Arithmetische Operatoren: +, -, ∗, /, % 11 Größe 1 Bit 16 Bit 8 Bit 16 Bit 32 Bit 64 Bit 32 Bit 64 Bit Bitweise Operatoren: &, |, ˆ, ∼ , «, », ≫ Für beide Typen sind Inkrement und Dekrement Operatoren definiert: ++ → Plus 1 - - → Minus 1 Beispiel: n = i++ Wird solch ein Operator einer Variable vorausgestellt, so wird 1 addiert/subtrahiert bevor der Wert in den Ausdruck eingelesen wird . Wird der Operator angehängt, so wird erst eingelesen und dann die Veränderung vorgenommen. Beispiel: int i = 8; System.out.println(i++ + ”, ” + ++i + ”, ” + i- -); produziert → 8, 10, 10. 1.5.3 Der Typ Boolean Literale: true, false Operatoren && (AND), ! (NOT), || (OR), ˆ (XOR) Mit den Operatoren <, <=, >, >=, ==, != kann man boolesche Ausdrücke aufbauen. Beispiele: (1==2) → false (1<=2) → true (1>=2) → false Prioritäten: ! vor && vor || Beispiel: !x || y → (!x) || y 1.5.4 Der Typ Char Literale: 1 Zeichen (eingeschlossen durch ’ ’) Beispiel: ’a’, ’?’ Sonderzeichen: \t, \n Der Typ char speichert Unicode Zeichen. 12 1.6 Strings und Arrays 1.6.1 Strings Der Stringtyp ist eine Klasse (zu finden unter java.lang.string). Sie können in einfachen Datentypen deklariert werden. Ein String-Literal wird durch Anführungszeichen angegeben: String s = ”Now”; String t = s + ” is the time.”; t → ”Now is the time” Übrigens: + ist in diesem Beispiel ein Operator, der String verkettet (Konkateniert)! Die Klasse java.lang.string stellt Methoden zur Verarbeitung von Strings zur Verfügung, z. B. ∗.equals() → zum Vergleich von Strings ⇒ Wert: Boolean ∗.length() → Anzahl der Character im String ∗.compareTo() → zum Vergleich von Strings (Wert: Integer) Stringobjekte sind unveränderlich (d.h. es gibt keine Methoden, die den Inhalt von Strings verändern können). Die Aufrufe der Methoden besprechen wir später. Möchte man den Inhalt verändern, so muss man zunächst aus dem String-Objekt ein sogenanntes String-Buffer-Objekt erzeugen, den Inhalt des Stringbuffers verändern und dann einen neuen String mit dem Inhalt des String-Buffers erzeugen (mehr in Klasse java.lang.StringBuffer). 1.6.2 Arrays Ein Feld oder Array ist eine Folge von Werten des gleichen Typs (genannt Basistyp). Zunächst schauen wir uns 1-Dimensionale Felder an: Beispiel: A: Wert Index 0 1 2 ... ... n Beispiele für Deklarationen: int[] A = new int[N]; double[] V = new double[N]; 13 Variablen A und V besteht aus N Feldern vom Typ Integer bzw. Double. Feldelemente sind in Java immer von 0 bis N-1 durchnummeriert. Ein neues Array wird mit dem Schlüsselwort new erzeugt. Ein Array-Literal kann wie folgt erzeugt werden: int[] A = {2,4,6,8,10 } Jedes Feldelement kann durch Indizierung identifiziert werden: A[i], wobei 0 ≤ i ≤ n-1! Beispiele: 1. Belegung eines Arrays: for(int i = 0;i < N; i++) A[i]=1; 2. Aufsummieren von Feldelementen: int sum = 0; for(int i = 0;i < N; i++) sum = sum + A[i]; 3. Berechnung des kleinsten Wertes in A und zugehörigen Index: int min = A[0]; int m = 0; for(int i = 0;i<N;i++) { if (A[i] < min) } min = A[i]; m = i; } 4. Suche nach einem Element im Array: Gegeben: Array A und Integer x Gesucht: Index i mit A[i] = x Versuch: int i = 0; while (A[i] != x) i++ Dieser Ansatz funktioniert nicht, wenn x nicht in A auftritt. Vereinbarung: Tritt x nicht auf, dann sei i = N; Lösung 1: int i = 0; while ((i<N) && (A[i] != x)) i++; Wir nutzen aus: wenn i = N ist, dann wird der zweite Ausdruck (A[i] != x) nicht mehr ausgewertet. Lösung 2: verwende ein Array mit N + 1 Elementen und setze A[N] = x. int[] A = new int [n + 1]; A[N] = x; int i = 0; while (A[i] != x) i++; 14 Bew: Ist am Ende der Schleife i = N, so gilt: A[i] ungleich x, ∀ i= 0,. . .,i-1 Analyse der Anzahl der benötigten Operationen (Zuweisungen, Vergleiche, Additionen). Die Anzahl hängt von dem Wert i = K nach dem Durchlauf der Schleife ab. Lösung 1: 1 Zuweisung, K Additionen 2(k+1) Vergleiche falls K<N und 2N+1 Vergleiche, falls K=N K + 1 — && -Anweisung falls K < N und N && -Anweisungen falls K = N. Im schlimmsten Fall (worst-case) für K = N ergeben sich 4 N + 2 Operationen. Lösung 2: 2 Zuweisungen, K Additionen, K + 1 Vergleiche; d.h. im schlimmsten Fall für K = N ergeben sich 2N+3 Operationen. Bem.(1): Ist das Array ungeordnet, so gibt es keine Möglichkeit diese Schranke im worst-case wesentlich zu verbessern. Bem.(2): Verbesserte Lösungen sind möglich, wenn das Array sortiert ist. 1.6.3 Binäre Suche Gegeben: ein Feld A mit A[i] ≤ A[i+1] für alle i = 0,...,N-2 und Integer x. Gesucht: Index i mit A[i] = x A= Wert 2 Index 0 3 1 5 2 7 3 11 4 13 5 17 6 19 7 Idee: Vergleiche x mit dem Element in der Mitte A[mid] wobei mid = (N-1) / 2 Drei Fälle sind möglich: 1. (A[mid] == x) → Element gefunden 2. (A[mid] < x) → x liegt an der oberen Hälfte des Arrays zwischen mid+1 und N-1 3. (A[mid] > x) → x liegt an der unteren Hälfte des Arrays zwischen mid-1 und 0 Während der Suche speichern wir Indizes i und j für die untere bzw. oberste Grenze des aktuellen Suchbereichs. 15 Am Anfang: i = 0; j = N-1; Algorithmus: int i = 0; j = N-1, mid; boolean found = false; while ((i <= j) && (!found)) { mid = (i + j) / 2; if (x < A[mid]) j = mid -1; else if (x > A[mid]) i = mid + 1; else found = true; } Wichtig: i ≤ mid ≤ j solange i ≤ j! Analyse der Anzahl der Rechenschritte (Operationen) im worst-case: Im schlimmsten Fall halbiert der Alg. den Suchbereich iterativ bis die Anzahl der Elemente im Suchbereich auf 0 zusammengeschrumpft ist → x ist nicht im Feld A! Beispiel: X = 6, i = 0, j = 7, mid = 7/2 = 3; Wert 2 Index 0 Zeiger i 3 1 5 2 7 11 3 4 mid 13 5 17 6 19 7 j A[mid] = 7 > 6 = x ⇒ Wir betrachten nur noch folgendes Teilarray: i = 0, j = 2, mid = 2/2 = 1; Wert 2 Index 0 Zeiger i 3 5 1 2 mid j ... ... 6 = X > A[mid] = 3 Es folgt also: Wert ... 5 ... Index ... 2 ... Zeiger i,j 16 Bei obigen Beispiel gilt i = j = 2 und mid = (2+2)/2 = 2 Somit: A[mid] = 5; → i = 3, j = 2; ⇒ Abbruch der Schleife! Formal: Wie oft muss der Suchbereich halbiert werden? Es sei Bi für i = 0,...,K die Anzahl der Elemente im Suchbereich (nachdem des i-te Mal der Bereich halbiert wurde). Es gilt B0 = N. Es sei K der erste Index mit BK = 0. Betrachte die Folge B0 , B1 ,...,BK Ziel: bestimme Abschätzung für K. Es gilt weiter Bi ≤ Bi − 1 / 2 ∀ i > 0 ⇒ Bi ≤ B0 / 2i = N / 2i Wenn N / 2i < 1 ⇔ N < 2i ⇔ log2 N < i Für K = log2 N + 1 gilt die Bedingung und damit benötigt der Algorithmus höchstens log2 N + 1 Schleifendurchläufe. In jeder Schleifeniteration werden maximal 4 Vergleiche und 2 Zuweisungen ausgeführt. Aufwand: c log N 1.6.4 Problem: Mischen von sortierten Sequenzen Gegeben sind zwei sortierte Felder: A mit N Zahlen B mit M Zahlen Diese sollen in ein Feld C der Länge N + M gemischt werden (d.h. alle Elemente aus A und B sollen in C enthalten sein und C soll aufsteigend sortiert sein). A: 1 5 7 8 B: 2 3 9 10 ⇒ C: 1 2 3 5 7 8 9 10 Deklaration: int[ ] A = new int[N]; int[ ] B = new int[M]; int[ ] C = new int[N+M]; 17 Idee: nimm jeweils das kleinere der Elemente in den Feldern A und B und stelle es an die nächste Position in C. Am Ende des Algorithmus kann der Rest von B (d.h. 9,10) direkt kopiert werden. Folgende Indizes werden benutzt: i : index bzgl. Feld A (der Teil A[i],...,A[N-1] ist nicht bearbeitet worden. Ist i = N, so ist A leer. j : Index bzgl. Feld B (analog) k : Index bzgl. Feld C (erste freie Position in C) A+B = C: Wert Quellarray 1 2 A B 3 B 5 7 8 9 A A A B 10 B int i = 0, j = 0, k = 0; while((i < N) || (j < M)) { if (i = = N) { //A ist abgearbeitet [(1)] kopiere Rest von B nach C; } else if (j = = M) { //B ist abgearbeitet [(2)] kopiere Rest von A nach C } else { //A und B ist nicht abgearbeitet [(3)] kopiere kleineres der Elemente A[i], B[j] nach C[k] und erhöhe Indizes } Verfeinerung des Algorithmus: 1. for(int l = j;l < M;l++) { c[k] = B[l]; k++; } j = M; 2. for(int l = i;l < M;l++) { c[k] = A[l]; k++; } i = N; 18 3. if (A[i] <= B[j]) { C[k] = A[i]; i++; } else { c[k] = B[j]; j++; } k++; Worst-Case: M + N Vergleiche 1.6.5 Mehrdimensionale Arrays Die Dimension ergibt sich aus der Anzahl der [ ]-Paare bei der Deklaration. Beispiel: int [ ] [ ] matrix = new int [N] [M]; double [ ] [ ] [ ] cube = new double [N] [M] [L]; Mehrdimensionale Arrays sind implementiert als Arrays von Arrays. Beispiel: 2-dimensionales Array 0 0 1 2 .. . M-1 1 2 ... ... ... ... N-1 ... Hierbei ist jedes Feldelement in der 1. Dimension ein Array mit M Feldern. Bemerkung: Bei der Erzeugung eines mehrdimensionalen Arrays müssen nicht alle Dimensionsgrößen direkt spezifiziert werden. Regel: bei dem ersten n ≥ 1 Dimensionen muss die Anzahl der Elemente angegeben sein. Zulässig: int [ ] [ ] [ ] cube = new int [5] [3] [ ]; nicht Zulässig: int [ ] [ ] [ ] cube = new int [5] [ ] [3]; 19 Feldelemente können angesprochen werden wie bei einem Ein-Dimensionalen Array: matrix[i] [j] = ... cube[i] [j] [k] = ... Analog können Literale definiert werden: int [ ] [ ] products = { {0,0,0,0,0 }, {0,1,2,3,4 }, {0,2,4,6,8 }, etc.} Die Größe eines Arrays ist in einer Variable length gespeichert und kann abgefragt werden durch <ArrayName>.length Beispiel: l = A.length; l = matrix[1].length; 2 Prinzipien der objektorientierten Programmierung 2.1 Objekte, Methoden und Klassen Ein Java-Programm besteht aus einer Menge von einer oder mehreren zusammenhängenden Klassen. Klassen sind ein Mittel zur Beschreibung der Eigenschaften und Fähigkeiten der Objekte der realen Welt. Eine Klasse ist genau genommen eine Sammlung von Daten sowie Methoden, die auf diesen Daten arbeiten. Die Daten und Methoden definieren den Inhalt und die Fähigkeiten von einem Objekt. Beispiel: Klasse, die Kreisobjekte definiert Idee: • beschreibe Kreis durch x / y Koordinaten, des Mittelpunktes und des Radius r. • stelle Funktionen zur Berechnung von Umfang und Fläche zur Verfügung. 20 public class Circle { public double x,y; // Koordinaten des Mittelpunkts public double r; //Radius public double circumference(){ //berechnet Umfang return 2 + 3.141 * r; } public double area() { //berechnet Fläche return 3.141 * r * r; } } Die Klasse Circle stellt einen neuen Datentyp zur Verfügung. Um Kreise verwenden zu können, müssen wir eine Variable vom Typ Circle deklarieren und ein Objekt von diesem Typ erzeugen: Circle c = new Circle(); c → Name, der auf Objekt verweist new → erzeugt Objekt Objekte sind Instanzen einer Klasse. Nach der Erzeugung eines Objekts können wir mit den Datenfeldern arbeiten bzw. auf diese zugreifen. Dazu verwendet man den Punkt-Operator: Die Programmzeilen Circle c = new Circle(); c.x = 1.0; c.y = 1.0; c.r = 2.0; Circle d = new Circle(); d.x = c.x; d.y = c.y; d.r = c.r * 2; erzeugen zwei Kreise an der Stelle (1,1) mit Radien 2 und 4. Die Methoden einer Klasse werden in ähnlicher Weise angewandt: double a = c.area(); Wichtig hierbei ist das Objekt c und nicht der Funktionsaufruf area(). Deshalb spricht man von objektorientierter Programmierung. 21 Bemerkung: 1. in c.area() wird kein Parameter übergeben. 2. Das Gleiche gilt für die Definition der Methode area(). Sie hat keine Parameter. Java sieht vor, dass eine Methode an einer Instanz arbeitet! Die area()-Methode verwendet das Datenfeld r. Da die area()-Methode Teil der gleichen Methode ist, wie das Datenfeld r, muss die Variable nicht übergeben werden. Der Radius r ist Teil des Objektes c. Java arbeitet implizit mit einem Argument this, das auf das Objekt verweist. Wir können das Schlüsselwort this auch explizit verwenden wie folgt: public double area() { return 3.141 * this.r * this.r; } Das implizite this-Argument tritt in Methodendefinitionen nicht auf, da es im allgemeinen nicht notwendig ist. Bemerkung: Das this-Schlüsselwort ist aber notwendig, wenn ein Methodenargument oder eine lokale Variable den gleichen Namen wie das Feld einer Klasse hat. Beispiel: public void setRadius(double r) { this.r = r;} } Bei der Objekterzeugung wird eine Funktion Circle c = new Circle() bzw. Konstruktormethode aufgerufen. Wir können Konstruktoren selber definieren (mit dem gleichen Namen wie die Klasse): public Circle(double x, double y, double r) { this.x = x; this.y = y; this.r = r; } Mit diesem Konstruktor können wir einen Kreis wie folgt erzeugen: Circle c = new Circle(1.0,1.0,2.0); 22 Wir können auch mehrere Konstruktoren verwenden (sie müssen aber unterschiedliche Parameterlisten haben): public Circle(double r) { this.x = 0.0; this.y = 0.0; this.r = r; } Die bisher verwendeten Variablen (im Beispiel: x, y, r) werden Instanzvariablen genannt. Jede Instanz der Klasse Circle besitzt eine eigene Kopie der Variablen x, y, r. Es gibt noch statische Variablen (Klassenvariablen). In dem Fall gibt es nur eine Kopie der Variablen. Diese statischen Variablen werden mit dem Schlüsselwort static deklariert. Beispiel: zähle die Anzahl der erzeugten Kreise public class Circle { static int num-circles = 0; public double x,y,r; public Circle(double x, double y, double r) { this.x = x; this.y = y; this.r = r; num-circles++; } .. . } Auf die Klassenvariable können wir wie folgt zugreifen: System.out.println(”Anzahl der erzeugten Kreise: ” + Circle.num-circles;) Wir können auch Konstanten über Klassenvariablen definieren: public class Circle { public class final double PI = 3.14159; public double x,y,r; } Häufig möchte man Methoden definieren, die nicht an einer speziellen Instanz arbeiten. Diese Methoden nennt man Klassenmethoden. Sie verwenden das Schlüsselwort static bei der Deklaration: 23 public Circle { public double x,y,r; public static Circle bigger (Circle a, Circle b) { //liefert den größeren zweier Kreise if (a.r > b.r) return a; else return b; } } Die Klassenmethode bigger könnte man auch als Instanzmethode definieren: public Circle bigger (Circle that) { if (this.r > that.r) return this; else) return that; } Angenomme wir haben zwei Kreise a, b deklariert: Circle a = new Circle(1.0,1.0,2.0); Circle a = new Circle(1.0,1.0,3.0); Die Methoden bigger können wie folgt aufgerufen werden: Circle biggest = Circle.bigger(a,b); //bei Klassenmethode Circle biggest = a.bigger(b); //bei Instanzmethode 2.2 2.2.1 Formale Beschreibung Klassen und Objekte Klassen bestehen im wesentlichen aus Instanz- und Klassenvariablen sowie Methoden. Klassen können Subklassen enthalten und verwandte Klassen können zu Paketen zusammengefasst werden. 24 Aufbau eines Programms Superklasse Klasse z Subklasse Klasse Klasse }| + Instanz- u. Klas- + Methoden senvariablen { Deklaration einer Klasse: [<ClassModifier>] class <className> [extends <SuperClassName>] [implements <InterfaceName>] { //Def. von Variablen und Methoden .. . } Das Schlüsselwort extends wird verwendet um eine Subklasse zu definieren. Das Schlüsselwort implements wird verwendet um Schnittstellen (Interfaces) zu benennen. Interfaces können insbesondere zur Definition von abstrakten Datentypen verwendet werden (d.h. man stellt dem Benutzer einen Datentyp mit einer Reihe von Operationen und Methoden zur Verfügung, ohne die interne Realisation zu zeigen). In dem Fall kapselt man Daten und Methoden (siehe Ende des Kapitels). Der <ClassModifier> kann verschiedene Formen haben: • public: Klasse darf instanziert oder erweitert werden – im selben Paket – überall wo die Klasse importiert wurde • final: keine Subklasse möglich • abstract: – keine Instanzierung möglich – Klasse enthält abstrakte Methoden, die in der Klasse nicht implementiert sind (sondern in den Subklassen) • ... 25 Ist ein <ClassModifier> spezifiziert, so können wir die Klasse überall im selben Paket verwenden. Bemerkung: Im wesentlichen arbeiten wir mit public. Objekterzeugung: Syntax: <VariableName> = new <ClassName> ([<Parameter>, {, <Parameter>} *]); Was passiert, wenn beim Aufruf des new-Operators? 1. Speicher wird für das neue Objekt bereitgestellt. Instanzvariablen werden mit Default-Werten initialisiert. 2. Konstruktormethode mit den spezifizierten Parametern wird aufgerufen. 3. Eine Referenz (Speicheradresse) auf die neue Instanz wird zurückgeliefert. Variable c ⇒ Adr. −→ 0xABCDEF01 .. . In imperativen Programmiersprachen gibt es zusammengesetzte Datentypen wie Records. Diese werden in Java durch Klassen realisiert. Beispiel: Student • Matrikelnummer • Name • Fachrichtung • Semesterzahl public class Student { public int Matrikelnummer; public String Name; public String Fachrichtung; public byte Semesteranzahl; } 26 2.2.2 Variablen (Deklaration von Instanz- und Klassenbariablen): [<VariableModifierList>]<VariableType> <VariableName>[=<InitialValue>] {,<VariableName>[=<InitialValue>]} * Der <VariableModifier> spezifiziert den Sichtbarkeitsbereich einer Variablen (d.h. von wo aus auf die Variable zugegriffen werden kann): • public: Zugriff von überall möglich (wo die Klasse verfügbar ist). • protected: Zugriff von der Klasse (ohne Subklasse) möglich. • private: Zugriff nur von der Klasse aus möglich (nicht von Subklassen). Ist keiner der obigen Schlüsselwörter spezifiziert, so ist ein Zugriff auf die Variable innerhalb desselben Pakets möglich. Weitere Schlüsselwörter: • static: Klassenvariablen, die zu einer Klasse aber nicht zu den einzelnen Instanzen der Klasse gehören. • final: Variablen, deren anfangs zugewiesener Wert nicht verändert werden kann. • static final: Konstanten Methoden können lokale Variablen haben. Sie existieren solange wie die Methode ausgeführt wird. Deklaration: <VariableType> <VariableName> [= <InitialValue>] {,<VariableName> [= <InitialValue>]} *; Haben in einer Klasse verschiedene Variablen den gleichen Namen, so können Sichtbarkeitsbereiche überdeckt werden. Es ist jeweils immer die Variable im inneren Block sichtbar. 27 public class Scope { public static int x = 2; //Klassenvariable Beginn der Sichtbarkeit x = 2 public static void proc(){ int x = 4; //lokale Variable Beginn der Sichtbarkeit x = 4 System.out.println(”Zahl ist ” + x); } Ende der Sichtbarkeit x = 4 public static void main(String[] args){ int x = 6; //lokale Variable Beginn der Sichtbarkeit x = 6 proc(); System.out.println(”Zahl ist ” + x); System.out.println(”Zahl ist ” + scope.x); } Ende der Sichtbarkeit x = 6 } Ende der Sichtbarkeit x = 2 2.2.3 Methoden Zunächst: Konstruktormethoden Der Name einer Konstruktormethode ist immer mit dem Klassennamen indentisch. Ist in einer Klasse kein Konstruktor definiert, so fügt Java automatisch den folgenden Konstruktor ein: public <ClassName>(){ super(); //Konstruktor der Superklasse } Ist keine Superklasse spezifiziert (meißt extends), dann ist die Superklasse die Klasse Object (java.lang.object). 28 Bemerkung: 1. Der Befehl super() ist der erste Befehl in jedem Konstruktor, selbst wenn er nicht explizit aufgeführt ist. 2. Mit dem Schlüsselwort this() wir ein anderer Konstruktor aufgerufen (muss aber 1. Statement sein) public Circle (double x, double y, double r) { this.x = x; this.y = y; this.r = r; num_circles++; } Bei Methoden unterscheiden wir: Funktionen (sie liefern einen Wert zurück) Prozeduren (sie liefern keinen Wert zurück) [<MethodModifierList>]<ReturnType><MethodName>(<ParameterList>) { <MethodBody> } <ParameterList> := [<ParameterType> <ParameterName> {,<ParameterType> <ParameterName>}*] Methoden-Modifikatoren: public|protected|private|abstract|final|static Wie bei Instanzvariablen schränken die Methoden-Modifikatoren den Sichtbarkeitsbereich (wie bei public, protected, private) oder die Methode ein (wie bei abstract, final, static). Bei Funktionen muss der ReturnType den Typ des Rückgabewerts spezifizieren. Dies ist ein einfacher Datentyp oder ein Objekt. Der Wert wird mit dem Befehl return zurückgeliefert. Bei Prozeduren ist der ReturnType void (d.h. ohne Wert). Parameterübergabe: Parameter werden in Java per Wert übergeben (”Call by Value”). Das bedeutet, dass beim Aufruf der Methode eine Kopie der Variablen angelegt wird. Während der Ausführung der Methode wird mit der Kopie gearbeitet. 29 Einfache Datentypen: Ist ein Parameter ein einfacher Datentyp, so kann die Methode den Variablenwert nicht verändern. public static void inc(int x){ x++; } Hier ist der Wert von x nach Abschluss der Methode inc nicht verändert! Besser ist eine Funktion: public static void inc(int x){ return ++x; } Objektvariablen: Objektvariablen haben eine Referenz auf ein Objekt und enthalten das Objekt selber nicht. Daher können Objekte in Methoden verändert werden. public static void reset(Circle c){ c.x = 0.0; c.y = 0.0; c.r = 1.0; } 2.3 Stacks und Queues Stacks (Keller) und Queues (Schlangen) sind einfach und zugleich wichtige Datenstrukturen (z. B. werden sie auf der Systemebene verwendet). 2.3.1 Stacks Ein Stack (Keller) ist ein Behälter, bei dem Objekte gemäß der LIFO-Regel (Last-In-First-Out) eingefügt bzw. entfernt werden. Wir können uns das wie einen Stapel vorstellen, bei dem Objekte obne draufgepackt und nur von oben wieder entfernt werden können. Hinzufügen: Neues Element % bestehender Stack 30 Entfernen: bestehender Stack & Neues Element Beispiele: • Internet WEB Browser speichern die Adressen von ”recently visited pages” in Form eines Stacks. • Texteditoren stellen einen UNDO-Mechanismus zur Verfügung, um die letzten Editor-Operationen rückgängig machen zu können. Folgende Operationen werden von einem Stack unterstützt: push(e): fügt Element e oben im Stack ein pop(): entfernt oberstes Element aus dem Stack und gibt es aus [Ausgabe Objekt] top(): gibt oberstes Element aus ohne es zu entfernen [Ausgabe: Objekt] isEmpty(): prüft, ob der Stack leer ist [Ausgabe: boolean] size(): gibt Anzahl der Elemente im Stack aus [Ausgabe: integer] top → 3 2 1 0 a e b f Zunächst: Implementierung von Stacks mit Hilfe von Arrays. Variable top speichert die Position des obersten Elements im Stack (top = -1 ⇔ Stack leer) public class Stack { public static final int CAPACITY = 1000; //Default-Länge des Arrays public int capacity; private int[] S; private int top = -1; public Stack(){ this(CAPACITY); } public Stack(int cap){ capacity = cap; S = new int[capacity]; } public int size(){ return(top+1); } 31 public boolean isEmpty(){ return(top < 0); } public void push(int element){ if(size < capacity) S[++top] = element; else → Fehlerbehandlung } public int top(){ if(!isEmpty()) return S[top]; else → Fehlerbehandlung } public int pop(){ if(!isEmpty()){ int element = S[top–]; return element; } else → Fehlerbehandlung } } Bemerkung: zur Initialisierung int[] S = new int[capacity]; ⇔ int[] S; S = new int[capacity]; Operation Output Stack Array () push(5) (5) S[0] = push(3) (5,3) S[0] = S[1] = pop() 3 (5) S[0] = push(7) (5,7) S[0] = S[1] = .. .. .. .. . . . . 32 5 5 3 5 5 7 top -1 0 1 0 1 .. . 2.3.2 Queues (Schlangen) Eine Queue (Schlange) ist ein Behälter, bei dem Objekte gemäß der FIFO-Regel (First-In-First-Out) eingefügt bzw. entfernt werden. Objekte werden stets vom Kopf der Schlange entfernt. Neue Objekte werden am Ende der Schlange eingefügt. ← Anfügen: ← Entfernen: Eine Schlange unterstützt folgende Operationen: enqueue(o): fügt Objekt o am Ende der Schlange ein dequeue(): entfernt Objekt am Kopf der Schlange und gibt es aus [Ausgabe: Objekt] isEmpty(): gibt einen Boolean-Wert zurück (wahr, wenn Schlange leer ist)[Ausgabe: boolean] size(): gibt Anzahl der Objekte in der Schlange aus [Ausgabe: integer] front(): gibt Objekt am Kopf der Schlange aus (ohne es zu löschen) [Ausgabe: Objekt] Realisation 1: Wir verwenden ein Array Objekt Zeiger Index 0 1 2 X f↑ 3 X X X X 4 5 6 7 r↑ 8 9 10 11 f: Index der Zelle, die das erste Objekt speichert (sofern Schlange nicht leer ist). r: Index der ersten freien Position. Ist Schlange leer, so soll gelten f = r (am Anfang f = r = 0). Folgendes Problem eröffnet sich: Startarray sei folgendes... Objekt Zeiger Index X X f↑ r↑ 0 1 2 3 4 5 6 7 ,→ diverse Berechnungen später sieht es so aus... Objekt Zeiger Index 0 1 X f↑ 2 X X 3 4 r↑ 5 6 7 33 ,→ wiederum diverse Berechnungen später sieht es schließlich so aus! Objekt Zeiger Index 0 1 2 3 X X f↑ r↑ 5 6 7 4 Resultat: Bei einem erneuten Anhängen würden wir bei bisheriger Implementierung einen Überlauf bekommen, da r auf 8 gesetzt werden muss. Wir können einen Überlauf bekommen, obwohl das Feld schwach besetzt ist. Realisation 2: Wir verwenden wieder ein Array Objekt Zeiger Index 0 1 X f↑ 3 2 X X X X X 4 5 6 7 8 9 10 11 S=6 f: Index der Zelle, die das erste Objekt speichert (sofern Schlange nicht leer ist). S: Anzahl der Elemente in der Schlange Anfangs f = 0; S = 0; Beachte: Es ist auch folgende Konfiguration erlaubt: Objekt Zeiger Index X X X 0 1 2 3 4 5 6 7 X f↑ 8 X X X 9 10 11 S=6 Die Position des letzten Objekts berechnet sich durch (f + S - 1) % capacity 34 public class Queue{ public static final int CAPACITY = 1000; private int capacity; private int[] Q; private int f = 0, s = 0; public Queue(){ this(CAPACITY); } public Queue(int cap){ capacity = cap; Q = new int[capacity]; } public size() { return s; } public boolean isEmpty(){ return (s == 0); } public void enqueue(int element{ int r; if (s < capacity) { r = (f+s)% capacity; Q[r] = element; s++; } else → Fehlerbehandlung } public int dequeue(){ int element; if (s > 0) { element = Q[f]; f = (f+1) % capacity; s–; return element; } else → Fehlerbehandlung } } Operation Output Queue enqueue(5) (5) enqueue(3) (5,3) dequeue() .. . 5 .. . (3) .. . 35 Array Q[0] = 5 Q[0] = 5 Q[1] = 3 Q[1] = 3 .. . f 0 0 s 1 2 1 .. . 1 .. . 2.4 Fehlerbehandlung Exception: Ereignis (Ausnahme), das während der Ausführung eines Programms auftritt. Eine Ausnahmehandlung ermöglicht 1. Korrektur des Fehlers oder 2. kontrollierten Abbruch des Programms. Exceptions sind Objekte in Java bzw. Instanzen der Klasse java.lang.Throwable oder deren Subklassen java.lang.Error java.lang.Exception java.io.IOException java.lang.RuntimeException IndexOutOfBoundsException FileNotFoundException Da Exceptions Objekte sind, können sie Daten enthalten und Methoden definieren. Insbesondere kann man beim Erzeugen des Objekts eine String-Nachricht mitgeben, die eine Fehlermeldung enthält und die später mit der Methode getMessage() vom Typ String ausgegeben werden kann. Eine Beschreibung, wie man Exceptions definiert, erzeugt und behandelt (am Beispiel des Stacks) folgt hier: Definition einer Subklasse von Exception: public class StackException extends Exception{ public StackException(String err){ } } Erzeugen von Exceptions: Tritt in einer Anweisung eine Ausnahme (Exception) auf, wird eine Exception-Instanz erzeugt und von der Methode (in der die Anweisung steht) ausgeworfen (throws an Exception). Die eigentliche Exception muss mit einer throws-Vereinbarung deklariert sein und wird mit dem throw-Befehl erzeugt. In diesem Fall wird der Codeblock (wo die Exception auftritt) abgebrochen. public void push (int element) throws StackException { if (size() == capacity) throw new StackException(”Stack ist voll!”); s[++top] = element; } 36 public void pop() throws StackException { int element; if (isEmpty()) throw new StackException(”Stack ist leer!”); element = s[top- -]; return element; } Behandlung von Exceptions: Exceptions werdem mit dem Befehl try-catch abgefangen. Folgende Syntax: try <StatementBlock> {catch(<ExceptionType><identifier>) <StatementBlock>}* [finally <StatementBlock>] Im try-Block stehen Anweisungen, die Instanzen der Klasse Exception auswerfen können. Danach folgen catch-Blöcke, die angeben wie die einzelnen Exceptions zu behandeln sind. Danach folgt ein optionaler finally-Block, der immer im Anschluss an den try-catch-Block ausgewertet wird (egal, ob eine Exception aufgetreten ist oder nicht). Beispiel: try { push(1); pop(); top(); } catch(StackException e){ System.out.println(e.getMessage()); } catch(IOException e){ . . . } 37 2.5 Einfach verkettete Listen head ↓ a → b → c → d → null • Eine einfach verkettete Liste besteht aus einer Folge von Knoten. Jeder Knoten enthält: 1. ein Datenelement 2. einen Zeiger auf das nächste Element 3. Zeiger des letzten Knotens ist gleich null • Eine solche Liste hat zusätzlich ein Element head, das auf den ersten Knoten der Liste zeigt. Wir nummerieren die Listenpositionen mit 1,2,3,... durch. Die folgenden Operationen sollen für eine Liste realisiert werden: size(): gibt die Anzahl der Knoten in der Liste aus isEmpty(): prüft, ob die Liste leer ist insert(e,p): fügt neues Element e an Position p ein delete(p): löscht Element an Position p und gibt es aus locate(e): gibt Position von Element e in der Liste aus retrieve(p): gibt Element an Position p aus Annahme: Die Elemente sind wieder Integer! public class node { private int element; private Node next; private Node (){ this(0,null); } public node (int e, Node n){ element = e; next = n; } } 38 public class List{ private Node head; private int size; private List(){ head = null; size = 0; } public boolean isEmpty(){ return (size == 0); } public int size(){ return size; } private Node nodeAtPosition(int p) throws ListException{ Node n; if ((p < 1) || (p > size)) throw new ListException (”Unzulässige Position!”); n = head; for(int i = 1;i < p;i++) n = n.next; return n; } public void insert (int e, int p) throws ListException { Node n = new Node(e,null); Node prev; if (p == 1){ n.next = head; head = n; } else { prev = nodeAtPosition(p-1); n.next = prev.next; prev.next = n; } size++; } 39 public void delete (int p) throws ListException{ Node n; int e; if((p < 1) || (p > size)) throw new ListException(”Falsche Position!”); if (p == 1){ e = head.element; head = head.wert; } else { n = nodeAtPosition(p-1); e = n.next.element; n.next = n.next.next; } size- -; return e; } public int locate(int e) ...{ . . . } public int retrieve(int p) ...{ . . . } } Skizzierungen Vorher: Neues Element n und bestehende Liste n → null head ↓ a → b → ... 40 Ellement e an Position 1 einfügen Ergebnis: 1. neue Verknüpfung von n zu a 2. ”umgelegte” Verknüpfung von head nach n head ↓ n → a → b → ... Ellement n an Position p > 1 einfügen Ergebnis: 1. neue Verknüpfung von a zu n 2. neue Verknüpfung von n nach b head ↓ a → n → b → ... Ellement n an Position 1 entfernen Element a entfernen! Ergebnis: 1. neue Verknüpfung von head zu b 2. a und dessen Verknüpfung bleiben ungenutzt im Speicher head a ↓ → b → c → ... 41 Ellement n an Position p > 1 entfernen Ergebnis: 1. neue Verknüpfung von a zu c 2. b und dessen Verknüpfung zu c bleiben ungenutzt im Speicher head ↓ −→ c a b → ... % Für den folgenden Abschnitt schauen wir uns nochmal unsere Klasse node an: public class node { public int element; public Node next; public Node (){ this(0,null); } public node (int e, Node n){ element = e; next = n; } } 2.5.1 Realisation eines Stacks durch eine einfach verkettete Liste public class Stack { private Node top; private int size; public Stack(){ top = null; size = 0;} public int size(){ return size; } 42 public boolean isEmpty(){ if (top==null) return true; return false; } public void push (int elem) { Node v = new Node(elem,top); top = v; size++; } public int top() throws StackException{ . . . } public int pop() throws StackException{ if (isEmpty()) throw new StackException(”Stack ist leer”); int e = top.element; top = top.next; size- return e; } Auch eine Queue kann man durch einfach verkettete Liste realisieren. Nachteil: Beim einfügen muss man einmal durch die Liste laufen. Vermeidbar ist dies durch zusätzliche Variable tail. head tail ↓ a → b → c ↓ → d → null public class Queue { privtae Node head, tail; private int size; public Queue(){ head = null; tail = null; size = 0; } public int size(){ return size; } public boolean isEmpty(){ return (size==0); } 43 public void enqueue(int e){ Node n = new Node(e,null); if (size == 0)//1. Fall head = n; else //2. Fall tail.next = n; tail = n; size++; } public int dequeue throws QueueException{ if (size == 0) throw new QueueException(”Queue ist leer!”); int e = head.element; head = head.next; size - -; if (size == 0) tail = null; return e; } } 1.Fall: Leere Queue head tail ↓ NIL ↓ NIL → NIL Neues Element n: e Ergebnis: head tail & . → NIL e 2.6 Doppelt verkettete Listen Skizze: head ↓ ←− a tail −→ ←− b −→ ←− −→ −→ ... ←− ←− c 44 ↓ x −→ null Wir haben: • eine Folge von Knoten (jeder Knoten hat Zeiger auf Vorgänger und Nachfolger in der Liste, Vorgänger vom ersten Knoten = null; Nachfolger vom letzten Knoten = null). • Variablen head, tail die auf den ersten bzw. letzten Knoten der Liste zeigen class DLNode{ int element; DLNode prev,next; DLNode(){ this(0,null,null); } DLNode(int e, DLNode p, DLNode n){ element = e; prev = p; next = n; } } public class DLList{ private Node head, tail; private int size; public DLList(){ head = null; tail = null; size = 0; } private DLNode dlnodeAtPosition(int p) throws DLListException{ DLNode n; if ((p < 1)||(p > size)) throw new DLListException(”Unzulässige Position!”); if (p <= size/2){ n = head; for(int i = 1;i < p;i++) n = n.next; } else { n = tail; for(int i = size;i > p;i - -) n = n.prev; } return n; } 45 public void insert (int e, int p) throws DLListException{ if (p == 1){ DLNode n = new DLNode(e,null,null); if (head != null) head.prev = n; else tail = n; head = n; } else if (p == (size+1)){ DLNode n = new DLNode(e,tail,null); tail.next = n; tail = n; } else { DLNode m = nodeAtPosition(p-1); DLNode n = new DLNode(e,m,m.next); m.next.prev = n; m.next = n; } size++; } } Beachte: Bei den obigen Methoden insert und delete sind verschiedene Fallunterscheidungen nötig. Vermeidbar ist dieser Umstand durch zwei Hilfsknoten header und trailer die keine Datentypen (bzw. die Zahl 0) enthalten. / −→ ←− a −→ ←− b −→ ←− c −→ ←− / −→ null Anfangs gilt: NIL←− / −→ ←− / −→NIL Listenerzeugung ist aufwendiger, Methoden vereinfachen sich (siehe Übungen!) public class DLLista{ DLNode header,trailer; int size; . . . } 46 Kapselung der Node-Klassen: class DLNode{ private int element; private DLNode prev,next; DLNode(){ this(0,null,null);} DLNode(int e,DLNode p, DLNode n){ element = e; prev = p; next = n;} int getElement(){ return element;} void setElement(int e){ element = e;} DLNode getPrev(){ return prev;} void setPrev(DLNode p){ prev = p;} DLNode getNext(){ return next;} void setNext(DLNode n){ next = n;} } Der Inhalt der Knoten kann jetzt nur noch durch die Methoden getElement, etc. verändert werden. public class DLList{ . . . public void insert(int e, int p) throws DLListException{ . . . } public int delete(int p) throws DLListException{ . . . } 2.6.1 Exkurs: Prinzipien der objektorientierten Programmierung • Abstraktion • Kapselung • Modularität Abstraktion: Ein abstrakter Datentyp (ANT) ist ein mathematisches Modell für eine Datenstruktur. Es spezifiziert: 1. welche Daten gespeichert werden und 2. welche Operationen unterstützt werden. 47 Ein ANT spezifiziert jedoch nicht, wie die Operationen realisiert werden. In Java kann man Implementationen von Methoden verstecken, indem man ein Interface implementiert. Beispiel: public interface Stack { public int size(); public boolean isEmpty(); public int top() throws StackException; public void push(int element) throws StackException; public int pop() throws StackException; } public class ArrayStack implements Stack { public static final int CAPACITY = 1000; private int capacity; . . . } Kapselung realisiert das Prinzip ”Information hiding”, Daten können nur über Methoden verändert werden. Die Realisierung von Methoden wird nicht gezeigt. Modularität: Softwaresystem ist in mehrere eigenständige Komponenten zerlegt. 48