Universität Bielefeld

Größe: px
Ab Seite anzeigen:

Download "Universität Bielefeld"

Transkript

1 Universität Bielefeld Interner Bericht der Technischen Fakultät Abteilung Informationstechnik Algorithmen und Datenstrukturen II Skript zur Vorlesung im Sommersemester 2010 Universität Bielefeld Postfach Bielefeld Germany

2 Impressum: Herausgeber: Robert Giegerich, Ralf Hofestädt, Peter Ladkin, Ralf Möller, Helge Ritter, Gerhard Sagerer, Jens Stoye, Ipke Wachsmuth Technische Fakultät der Universität Bielefeld, Abteilung Informationstechnik, Postfach , Bielefeld, Germany

3 Vorwort Vorwort zur ersten Auflage: Dieses Skript ist aus der Vorlesung Algorithmen und Datenstrukturen II hervorgegangen, die ich im Sommersemester 1997 an der Universität Bielefeld gehalten habe. Seine Entstehung ist einzig und allein der Initiative der Studierenden Marcel Holtmann und Tanja Becker zu verdanken. Sie haben sich bereit erklärt, das handschriftliche Manuskript nach L A T E X zu übertragen. Jeder, der so etwas schon einmal gemacht hat, weiß, wieviel Arbeit und Mühe in diesem Skript steckt. Ich danke den beiden ganz herzlich. Das letzte Kapitel wurde von Christian Büschking gestaltet, der mich in der letzten Vorlesungswoche wegen einer Konferenzteilnahme vertreten hat. Auch ihm gilt mein Dank. Schließlich möchte ich mich bei Nicole Schwerdt bedanken, die mich während der letzten Überarbeitung des Skriptes bei Schreibarbeiten unterstützt hat. Das vorliegende Skript enthält mit an Sicherheit grenzender Wahrscheinlichkeit Fehler. Wenn jemand einen Fehler findet, oder meint, ein Sachverhalt sei unklar formuliert, kann er/sie eine Nachricht an enno@techfak.uni-bielefeld.de schicken. Bielefeld, im Oktober 1997 Enno Ohlebusch

4 Vorwort zur Auflage 2005: Mittlerweile wurde das Skript durch verschiedene Autoren aktualisiert und ergänzt. Im Sommersemester 2001 hat Robert Giegerich Änderungen in Kapitel 1 eingefügt. Für das Sommersemester 2002 habe ich kleinere Umstellungen in den Kapiteln 2 und 7 vorgenommen, um die objektorientierten Anteile der Programmiersprache Java etwas stärker zu betonen. Zum Sommersemester 2004 hat es einige weitere kleine Ergänzungen gegeben, insbesondere ausführliche einleitende Beispiele in den Kapiteln 4.6 und 8, für die ich Peter Menke und Sebastian Kespohl herzlich danke. Bielefeld, im August 2005 Jens Stoye Vorwort zur Auflage 2006: Für das Sommersemester 2006 habe ich nur wenige kleine Korrekturen vorgenommen und die Grafiken verfeinert. Bielefeld, im Mai 2006 Tim Nattkemper Vorwort zur Auflage 2008: Im Sommersemester 2008 wurde das Skript um drei Kapitel erweitert: das Kapitel Graphen gibt eine kurze Einführung in Graphrepräsentationen und einige wenige beispielhafte Algorithmen auf Graphen. Nils Hoffmann hat eine kurze Einführung in Threads verfasst, Jan Krüger eine Einführung in GUI-Programmierung in Java mittels Swing. Bielefeld, im Mai 2008 Alexander Sczyrba ii

5 Inhaltsverzeichnis 1 Syntax und Semantik Einführung EBNF-Definitionen Syntaxdiagramme Aufgaben Java: Der Einstieg Grundlegendes zu Java Historisches Eigenschaften von Java Sicherheit Erstellen eines Java-Programms Grundzüge imperativer Sprachen Das Behältermodell der Variablen Konsequenzen Klassen, Objekte und Methoden im Überblick Klassen Das Erzeugen von Objekten Klassenvariablen Methoden Klassenbezogene Methoden Vererbung, Pakete und Gültigkeitsbereiche Vererbung Pakete Gültigkeitsbereiche Aufgaben Imperative Programmierung in Java Mini-Java Von Mini-Java zu Java Elementare Datentypen Kommentare Bool sche Operatoren Bitoperatoren Inkrement und Dekrement iii

6 Inhaltsverzeichnis Zuweisungsoperatoren Die nichtabweisende Schleife for-schleife if-then-else Anweisung Mehrdimensionale Felder switch-anweisung Aufgaben Algorithmen zur exakten Suche in Texten Die Klasse String Grundlegende Definitionen Das Problem der exakten Suche Der Boyer-Moore-Algorithmus Die bad-character Heuristik Die good-suffix Heuristik Der Boyer-Moore-Horspool-Algorithmus Der Knuth-Morris-Pratt-Algorithmus Einführendes Beispiel Funktionsweise Korrektheit der Berechnung der Präfixfunktion Aufgaben Objektorientierte Programmierung in Java Traditionelle Konzepte der Softwaretechnik Beispiel: Der ADT Stack Konzepte der objektorientierten Programmierung Klassen und Objekte Kommunikation mit Nachrichten Vererbung Konstruktoren und Initialisierungsblöcke Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen Methoden in Java Unterklassen und Vererbung in Java Überschreiben von Methoden und Verdecken von Datenfeldern Konstruktoren in Unterklassen Reihenfolgeabhängigkeit von Konstruktoren Abstrakte Klassen und Methoden Aufgaben Übergang von funktionaler zu OOP Imperative vs. funktionale Programmierung Listen in Java iv

7 Inhaltsverzeichnis 6.3 Vergleich zwischen Haskell und Java Aufgaben Programmieren im Großen Schnittstellen Beispiel: Die vordefinierte Schnittstelle Enumeration Pakete Paketinhalte Paketbenennung Ausnahmen (Exceptions) throw und throws try, catch und finally Aufgaben Graphen Anwendungen von Graphen Terminologie Repräsentation von Graphen Ein Abstrakter Datentyp (ADT) Graph Breitensuche Tiefensuche Topologisches Sortieren Hashing Einführendes Beispiel Allgemeine Definitionen Strategien zur Behandlung von Kollisionen Direkte Verkettung Open Hashing Die Klasse Hashtable in Java Aufgaben Ein- und Ausgabe Ströme Die Klasse InputStream File-Ströme Gepufferte Ströme Datenströme Stream Tokenizer Graphische Benutzeroberflächen mit Swing Historisches Ein Fenster zur Welt v

8 Inhaltsverzeichnis 11.3 Swing Komponenten Eine einfache Schaltfläche - JButton Ein Texteingabefeld - JTextField Container LayoutManager FlowLayout BorderLayout GridLayout BoxLayout Ereignisse und deren Behandlung ActionListener Events und Listener Java Applet vs. Java WebStart vs. Java Application Java (Webstart) Application Java Applets Java Sicherheitskonzepte Parallele Ausführung - Nebenläufigkeit - Threads Das Threadmodell von Java Thread Pools Besonderheiten bei Swing - Der Event Dispatch Thread Weiterführende Konzepte Java Beans Konventionen Speichern und Laden vi

9 1 Syntax und Semantik Bevor wir richtig in das Thema dieses zweiten Teiles der Vorlesung Algorithmen und Datenstrukturen einsteigen, werden in diesem Einführungskapitel zunächst einige der notwendigen Definitionen behandelt, um Programmiersprachen überhaupt formal exakt beschreiben und die Bedeutung ihrer Programme festlegen zu können: Formale Sprachen, Grammatiken, Syntax und Semantik. Dies wird anhand von drei Beispielen geschehen: der Steuerung eines Bohrkopfes, einer Sprache zur Beschreibung von RNA-Sekundärstrukturen und an Mini-Java, einer Teilsprache von Java. 1.1 Einführung Programmiersprachen sind formale Sprachen. Es ist präzise festgelegt, welche Zeichenreihen überhaupt Programme einer Sprache L sind (Syntax), welche Ein-/Ausgabefunktion ein Programm berechnet (Semantik). konkrete Syntax: abstrakte Syntax: genaue textuelle Aufschreibung für Programme. Bindeglied zur Semantik; gibt an, wie ein Programm(-stück) aufgebaut ist. Beispiel abstrakt konkret x assign var add const y 5 x := y + 5 Pascal x = y + 5 C, Fortran, Java LET x = y + 5 Basic (anno 1963) ADD 5 TO y GIVING x COBOL STORE y + 5 TO x dbase 1

10 1 Syntax und Semantik In der abstrakten Syntax tauchen die primitiven Konstrukte einer Programmiersprache auf, sowie Operatoren, die diese zu neuen Konstrukten kombinieren. primitiv: Bezeichner, Zahl Kombination: var: Bezeichner Ausdruck const: Zahl Ausdruck add: Ausdruck Ausdruck Ausdruck assign: Bezeichner Ausdruck Anweisung Für einen Sprachdesigner und den Übersetzer ist die abstrakte Syntax die wesentliche Programmdarstellung. Zum Programmieren dagegen braucht man die konkrete Syntax. Definition Ein Alphabet A ist ein endlicher Zeichenvorrat (eine endliche Menge). Die Mengen aller endlichen Zeichenreihen über einem Alphabet A bezeichnen wir mit A. Das leere Wort der Länge 0 bezeichnen wir mit ε. Definition Eine Menge L A heißt formale Sprache über dem Alphabet A. Einen abstrakteren Sprachbegriff kann man kaum definieren. Die einzige Frage, die man sich über w A stellen kann, ist: Gilt w L oder w L? Diese Frage nennt man das Wortproblem von L. Definition Eine Programmiersprache ist ein Paar (L, L), wobei L A eine formale Sprache und L : L (A A ) die Semantik von L ist. Damit ordnet L jedem L-Programm l L als seine Bedeutung die Ein-/Ausgabefunktion L(l) zu, wobei Ein- und Ausgabe ebenfalls Zeichenreihen über A sind. Für L(l) schreiben wir auch kurz Ll. Definition Eine kontextfreie Grammatik ist ein 4-Tupel G = (N, A, P, S), wobei 1. N ein Alphabet von sogenannten Nonterminalsymbolen, 2. A ein Alphabet von sogenannten Terminalsymbolen mit N A =, 3. P eine endliche Menge von Produktionen (Regeln) der Form V α mit V N und α (V A) und 4. S N ein Startsymbol ist. 2

11 1.1 Einführung Die Menge S(G) der Satzformen von G ist die kleinste Teilmenge von (N A) mit den folgenden Eigenschaften: 1. S S(G). 2. Wenn αv β S(G) für ein Nonterminalsymbol V N und Zeichenfolgen α, β (N A) und wenn V γ P eine Regel ist, so gilt auch αγβ S(G) ( Ableitungsschritt ). Die durch G definierte Sprache ist L(G) def = S(G) A. Den Test, ob ein gegebenes Wort w durch eine Grammatik G erzeugt werden kann, also ob w L(G) gilt, nennt man das Wortproblem von G. Beispiel (Syntax und Semantik von Sprachen: Bohrkopfsteuerung) Wir betrachten sehr einfache Programme zur Steuerung eines Bohrkopfes. Das Gerät kennt die folgenden Befehle: N W E O S N, E, W, S: Bewegung um 1 Gitterpunkt (north, east, west, south) O: Einstellen auf Nullpunkt (origin) D: Senken des Bohrkopfes mit Bohrung (drill) U: Heben des Bohrkopfes (up) Programme zur Maschinensteuerung sind z.b. ON N N EEDO ODU DU DU DUNDUW DUSDU Bohrung am Punkt (2,3) mit Rückkehr zum Nullpunkt Dreifach-Bohrung am Nullpunkt bohrt Gitterquadrat, wo der Kopf gerade steht In der ersten Variante unserer Steuersprache lassen wir beliebige Befehlsfolgen zu. Außerdem kann jedes Programm als Eingabe ein Paar von Start-Koordinaten erhalten. L 1 = {N, E, W, S, U, D, O Eingabe: (Int, Int) 3

12 1 Syntax und Semantik Formal heißt dies, dass zu unserem Alphabet auch Koordinatenpaare gehören, die aber in den Programmen selbst nicht auftreten. Intuitiv scheint die Semantik der Sprache klar der Bohrkopf wird bewegt und bohrt mit jedem D-Befehl am aktuellen Gitterpunkt. Allerdings gibt es da ein paar Feinheiten, auf die wir später zurückkommen. Zunächst definieren wird die Semantik der Sprache L 1. Was soll die Ausgabe sein? Real ist es das gebohrte Werkstück. Mathematisch gesehen entspricht dem eine Liste der Koordinaten aller erfolgten Bohrungen. L 1 : L 1 (Int, Int) [(Int, Int)] Wir beschreiben die Semantik einzelner Befehle als Transformation des Maschinenzustands. Dieser enthält vier Komponenten: x y l cs aktuelle x-koordinate aktuelle y-koordinate aktueller Hebezustand des Bohrkopfes: 0 gesenkt, 1 oben Liste bisheriger Bohr-Koordinaten Den Effekt einzelner Befehle beschreibt die Funktion bef :: Befehl Zustand Zustand bef b (x, y, l, cs) = case b of O (0, 0, 1, cs) N (x, y + 1, l, cs) W (x 1, y, l, cs) S (x, y 1, l, cs) E (x + 1, y, l, cs) D (x, y, 0, (x, y) : cs) U (x, y, 1, cs) Die Abarbeitung einer Befehlsfolge beschreibt die Funktion beff :: Befehl Zustand [(Int, Int)] beff [ ](x, y, l, cs) = reverse cs beff (b : bs)(x, y, l, cs) = beff bs(bef b (x, y, l, cs)) Am Ende drehen wir die Liste der Bohrkoordinaten um, damit sie in der getätigten Reihenfolge erscheint. Mathematisch gesehen ist das nicht nötig. Die Semantik L 1 wird beschrieben durch die Funktion prog :: Befehl (Int, Int) [(Int, Int)] prog bs (i, j) = beff bs (i, j, 1, []) Programmiersprache L 1 und ihre Semantik L 1 sind sehr naiv. Man sollte zum Beispiel bedenken, dass das Werkstück und vielleicht auch die Maschine beschädigt 4

13 1.1 Einführung werden, wenn mit abgesenktem Bohrkopf Bewegungen ausgelöst werden. Freilich solange niemand solche Steuerprogramme erzeugt, geht alles gut. Jedoch wollen wir uns nicht darauf verlassen... Generell gibt es zwei Möglichkeiten, eine Sprache (L, L) zu verfeinern: syntaktisch oder semantisch. Semantisch heisst: das Unglück kann programmiert werden, tritt aber nicht ein. Syntaktisch heisst: das Unglück wird bereits als syntaktisch fehlerhaftes Programm abgewiesen. Aufgabe Modifiziere die Semantik L 1 (bei unverändertem L 1 ) in zwei verschiedenen Weisen. Tritt eine Bewegung mit abgesenktem Bohrkopf auf, so wird a) die Ausführung des Programms abgebrochen und nur die Koordinaten der bisherigen Bohrungen ausgegeben, b) die Ausführung des Programms abgebrochen und keine Koordinaten angegeben. Was bedeuten diese semantischen Unterschiede in der Bohrpraxis? Wir entscheiden uns nun für die syntaktische Verfeinerung und stellen Forderungen an die Steuersprache L: 1. Auf Befehl D muss stets U oder O folgen. 2. Befehl O ist immer möglich, U nur unmittelbar nach D. 3. Alle Programme enden mit dem Bohrkopf am Nullpunkt. 4. Auf dem Weg von einer Bohrung zur nächsten sind gegenläufige Richtungswechsel unerwünscht, zum Beispiel... NSNS... oder... NES..., weil sie die Maschine in Schwingung versetzen können. Alle diese Forderungen lassen sich durch Einschränkungen von L 1 erfüllen, erfordern aber verfeinerte Methoden zur syntaktischen Sprachbeschreibung. Solche Mittel sind Grammatiken. Grammatik G 1 (zur Beschreibung von L 1 ) A = { N, E, W, S, U, D, O N = { moves, move S = moves P = { moves ε move moves move N E W S U D O 5

14 1 Syntax und Semantik Hier gilt L(G 1 ) = A. Es ist w L(G 1 ) mit w = W ENDEDENUDOSUED Übung: Leite w mit dieser Grammatik ab. Verfeinerte Grammatik G 2 (berücksichtigt Forderungen (1) - (3), aber nicht (4)). A, N, S wie G 1 P = { moves O DO move moves move N E W S O DU DO Frage: Warum brauchen wir die Regel moves DO überhaupt? Antwort: Sonst ist DO / S(G 2 ). Warum ist nun w = W ENDEDENUDOSUED / S(G 2 )? Versuch einer Ableitung: moves move moves W moves W move moves W E moves 3 W E N move moves W E N? Hier kann nur DU oder DO erzeugt werden, aber nicht D allein oder DE. Verfeinerte Grammatik G 3 (berücksichtigt Forderungen (1) und (4), (2) nur teilweise und (3) gar nicht): A, S wie G 1 N = { moves, ne, nw, se, sw, drill P = { moves ε ne moves nw moves se moves sw moves ne N ne E ne drill se S se E se drill nw N nw W nw drill sw S sw W sw drill drill DU DO Durch die Nichtterminale ne, nw, se, sw entscheidet die Grammatik zunächst über die grobe Lage des nächsten Bohrloches, das dann ohne gegenläufigen Richtungswechsel angesteuert wird. Aufgabe Schreibe eine Grammatik, die alle Forderungen (1) - (4) berücksichtigt. 6

15 1.1 Einführung Aufgabe Schreibe eine Grammatik, deren Programme den Befehl O nicht benutzen und den Bohrkopf nach der letzten Bohrung in Umkehrung des gefahrenen Weges zum Startpunkt zurückführen. Ignoriere Forderungen (1) - (4). Hinweis: Verwende Produktionen der Form moves N moves N... Beispiel (RNA-Sekundärstrukturen) Ein RNA-Molekül besteht aus Basen A,C,G,U. Durch Wasserstoff-Brücken zwischen A U, G C, G U bilden sich Basenpaarungen, die zu einer Sekundärstruktur führen. Primärsequenz C A C C U A A G G U C C Sekundärstruktur C A C C U A A G G U C C C A U C C C C GHelix-Bildung schafft Stabilität G U A A Grammatik zur Beschreibung der Sekundärstruktur (Ausschnitt): A = { A, C, G, U N = { struct, any, stack, loop S = struct P = { struct any any struct struct any stack ε any A C G U stack A stack U U stack A G stack C C stack G G stack U U stack G loop loop any loop any any any Allein mit den ersten beiden Produktionen kann man alle RNA-Sequenzen ableiten: struct any struct A struct A any struct AC struct... Damit ist L(G) = A. Der Witz der Grammatik ist, dass manche Ableitungen das Vorliegen möglicher Sekundärstrukturen anzeigen dann nämlich, wenn sie die Produktionen für stack benutzen. struct 2 C struct 4 C struct CC C stack CC CA stack UCC 2 CACC stack GGUCC CACC loop GGUCC 4 CACCUAAGGUCC 7

16 1 Syntax und Semantik Noch deutlicher wird das Erkennen der Sekundärstruktur, wenn wir diese Ableitung als sogenannten Syntaxbaum ausschreiben: C U struct any struct struct any any C struct C stack A U stack C G stack C stack G any loop any any A A Wer also mit dieser Grammatik und einer gegebenen RNA-Sequenz w das Wortproblem w L(G) löst, kann Aussagen über mögliche Sekundärstrukturen machen. Die Chomsky-Hierarchie In den 50er Jahren wurden von Noam Chomsky vier Typen von Grammatiken vorgestellt, die unterschiedlich komplexe Klassen von Produktionen erlauben. Wir geben jeweils ein Beispiel für den erlaubten Regeltyp an. Es ist X, Y, Z N, a, b, c A. Typ 0: XaY b cxz (allgemein) Typ 1: axb azy b (kontextsensitiv) Typ 2: X ay bzc (kontextfrei) Typ 3: X ay (regulär) Die Sprachklassen vom Typ 0 3 bilden eine echte Hierarchie, ihr Wortproblem liegt in unterschiedlichen Komplexitätsklassen: Typ 0: Typ 1: Typ 2: Typ 3: unentscheidbar exponentiell polynomiell (Θ(n 3 ) oder besser; bei Grammatiken für Programmiersprachen in der Regel Θ(n)) linear 8

17 1.2 EBNF-Definitionen 1.2 EBNF-Definitionen Historisches: Syntaxbeschreibung von FORTRAN und COBOL (am Anfang) durch Beispiele und Gegenbeispiele formale Beschreibung der Syntax von ALGOL durch John Backus; Backus-Normalform (BNF). Kleine Verbesserungen in der Notation durch Peter Naur, daher spricht man heute von der Backus-Naur-Form (BNF). Niklaus Wirth hat die Backus-Naur-Form noch einmal überarbeitet und erweitert (EBNF Extended BNF). Definition Die Metazeichen der EBNF (vgl. Klaeren [7], S. 104) sind: das Definitionszeichen = das Alternativzeichen die Anführungszeichen " " die Wiederholungsklammern { die Optionsklammern [ ] die Gruppenklammern ( ) der Punkt. Die Menge ET der EBNF-Terme ist gegeben durch: 1. Ist V eine Folge von Buchstaben und Ziffern, die mit einem Buchstaben beginnt, so gilt V ET und gilt als Nonterminalsymbol. 2. Ist w eine Folge von beliebigen Symbolen, so ist w ET und gilt als ein (!) Terminalsymbol. 3. Für α ET sind auch a) (α) ET, b) [α] ET und c) {α ET. 4. Für α 1,..., α n ET sind auch a) α 1... α n ET und b) α 1 α 2... α n ET. 9

18 1 Syntax und Semantik Eine EBNF-Definition besteht aus einer endlichen Menge von EBNF-Regeln der Form V = α. wobei V ein Nonterminalsymbol entsprechend obiger Konvention und α ein EBNF-Term ist. Das Nonterminalsymbol auf der linken Seite der ersten Regel ist das Startsymbol. Beispiel EBNF-Definition für Mini-Java, einer Teilsprache von Java. program = class ident { mainmethod. mainmethod = public static void main ( String [ ] argsident ) block. statement = int ident = expression ; ident = expression ; if ( condition ) statement while ( condition ) statement block System. out. println ( expression ) ; ; int [ ] arrayident = new int [ expression ] ; arrayident [ expression ] = expression ;. block = { { statement. condition = expression ( ==!= < <= > >= ) expression. expression = [ ( + - ) ] term { ( + - ) term. term = factor { ( * / ) factor. factor = ident number ( expression ) Integer. parseint ( argsident [ expression ] ) argsident. length arrayident. length arrayident [ expression ]. ident = ( letter _ $ ) { letter digit. number = ( 0 digit { digit 0 ). digit = letter argsident arrayident = A... Z a... z. = ident. = ident. 10

19 1.2 EBNF-Definitionen Um die Semantik einer EBNF-Definition zu definieren, benötigen wir folgende Operationen auf Sprachen: Definition Seien L, L 1 und L 2 beliebige Sprachen (Wortmengen) über einem gemeinsamen Alphabet. Dann definieren wir: 1. Komplex-Produkt: L 1 L 2 def = {w 1 w 2 w 1 L 1, w 2 L 2 (also L = L = ; L{ε = {εl = L) 2. n-fache Iteration: L 0 def = {ε, L n+1 := LL n 3. Stern-Operation: L def = n N Ln Beispiel {aa, ab {aa, ε = {aaaa, abaa, aa, ab 2. {a, b, c 2 = {a, b, c {a, b, c = {aa, ab, ac, ba, bb, bc, ca, cb, cc 3. {a, b = {ε, a, b, aa, ab, ba, bb,... Definition Die Semantik der EBNF definieren wir durch Rekursion über die EBNF-Terme. Sei E eine EBNF-Definition (wobei S das Startsymbol, N die Menge der Nonterminals und A die Menge der Terminals sei) und ET die Menge der EBNF-Terme. Dann ist die von E erzeugte Sprache L(E) definiert als S E, wobei E : ET P (A ) wie folgt definiert ist (vgl. Klaeren [7], S. 107): { def αe falls V = α. eine Regel in E ist 1. Für V N ist V E = sonst 2. w E def = {w 3. (α) E 4. [α] E def = α E def = {ε α E 5. {α E def = α E 6. α 1... α n E def = α 1 E... α n E 7. α 1 α n E def = α 1 E α n E 11

20 1 Syntax und Semantik Beispiel Gegeben sei die EBNF-Definition E mit dem Startsymbol Rna sowie den beiden Regeln Rna = Any [ A ] und Any = ( A C G U ). Durch wiederholte Anwendung der verschiedenen Gleichungen aus der Semantikdefinition ergibt sich die von dieser EBNF definierte Sprache folgendermaßen: (1) Rna E = Any [ A ] E (6) = Any E [ A ] E (1),(4) = ( A C G U ) ( ) E {ε A E (3),(2) ( ) = A C G U E {ε {A ) ( A E C E G E U E {ε, A (7) = = ( {A {C {G {U ) {ε, A = {A, C, G, U{ε, A = {A, C, G, U, AA, CA, GA, UA. Beispiel Folgende Zeichenkette ist ein syntaktisch korrektes Mini-Java Programm (gemäß der EBNF-Definition aus Beispiel 1.2.2). class BubbleSort { public static void main(string[] args) { int[] array = new int[args.length]; int i = 0; while (i < args.length) { array[i] = Integer.parseInt(args[i]); i = i+1; i = 1; while (i < array.length) { int j = array.length - 1; while (j >= i) { if (array[j] < array[j-1]) { int tmp = array[j]; array[j] = array[j-1]; array[j-1] = tmp; j = j-1; 12

21 1.3 Syntaxdiagramme i = i+1; i = 0; while (i < array.length) { System.out.println(array[i]); i = i+1; 1.3 Syntaxdiagramme Eine EBNF-Definition kann man folgendermaßen in Syntaxdiagramme überführen: w : w für alle w A. V : V für alle V N. [α] : {α : α α α 1... α n : α 1 α n α 1 α n : α 1. α n 13

22 1 Syntax und Semantik Beispiel Syntaxdiagramme für Mini-Java program class ident { mainmethod mainmethod public static void main ( String [ ] argsident ) block statement ident if ( condition ) statement while ( condition ) statement block System. out. println ( expression ) ; int = expression ; ident = expression ; ; int arrayident [ ] arrayident = new int [ expression ] [ expression ] = expression ; ; block { statement condition expression!= == expression < <= >= > expression + - term - term + 14

23 1.3 Syntaxdiagramme term factor factor factor * / ident number ( expression ) Integer. parseint ( argsident [ expression ] ) argsident. length arrayident. length arrayident [ expression ] ident letter _ $ digit argsident ident letter digit number digit 0 letter A... Z a... z arrayident ident digit 0 15

24 1 Syntax und Semantik 1.4 Aufgaben Aufgabe Der Linguist NOAM CHOMSKY war sehr enttäuscht, als er feststellte, dass auch die von ihm klassifizierte Typ-0 Sprache sich nicht zur Beschreibung der englischen Sprache eignete. Erst recht hat die kontexfreie Grammatik (Typ-2 Sprache) ihre Grenzen in der Beschreibungsfähigkeit von Sprachen. (a) Die Sprache (a n b n c n ) lässt sich nicht durch eine kontextfreie Grammatik beschreiben. Geben Sie dafür einen plausiblen Grund an (keinen Beweis). (b) Geben Sie eine kontextfreie Grammatik an, die die Sprache (a n b m c n ) beschreibt. Aufgabe Palindrome sind Worte, Sätze oder Verse, die von rechts nach links gelesen gleich lauten wie von links nach rechts 1 (abgeleitet vom griechischen palindromos für wieder zurücklaufend ). Geben Sie eine EBNF-Definition an, welche die Sprache der Palindrome über dem Alphabet A = {a, b, c beschreibt. Aufgabe Ribosomen übernehmen in biologischen Zellen eine wichtige Aufgabe bei der Proteinbiosynthese: Sie setzen aus einzelnen Aminosäuren ein bestimmtes Protein zusammen. Der Syntheseweg wird durch eine Nukleotidensequenz bestimmt, der mrna. Jeweils drei Nukleotide werden zu einem sogenannten Triplett zusammengefasst; fast alle der möglichen Tripletts (Anzahl 4 3 = 64) codieren für Proteine, diese Tripletts werden Codons genannt. Im folgenden soll ein Mini-Ribosom besprochen werden. Unser Ribosom verarbeitet als mrna eine Sequenz über dem Alphabet {A, C, G, U zu den Proteinen Asparagin, Glutamin, Arginin und Lysin. Das Ribosom erkennt eine korrekte mr- NA dann, wenn sie aus einem Codon besteht, welches links von dem Startcodon Methionin und rechts von einem Stopcodon flankiert wird. Das Ribosom basiert auf der Grammatik G = (N, A, P, Protein), deren Komponenten unten definiert sind. (a) Erstellen Sie die zur Grammatik gehörende EBNF-Definition und die entsprechenden Syntaxdiagramme. (b) Bestimmen Sie die von der EBNF-Definition erzeugte Sprache (die Semantik der EBNF-Definition). (c) In welchem gravierenden, unrealistischen Punkt weicht die Grammatik von der biologischen Realität ab? 1 Radar Roma tibi subito motibus ibit amor Able was I ere I saw Elba Madam I m Adam Ein Neger mit Gazelle zagt im Regen nie 16

25 1.4 Aufgaben N = {Protein, Aminosäure, Met, Glu, Asp, Arg, Lys, Start, Stop A = {A, C, G, U P = { Protein Start Aminosäure Stop Start Met Aminosäure Asp Aminosäure Glu Aminosäure Lys Aminosäure Arg Stop U A A Met A U G Glu G A A Glu G A G Asp G A C Asp G A U Arg C G C Arg C G G Arg C G A Arg C G U Arg A G A Arg A G G Lys A A G Lys A A A Stop U A A 17

26 1 Syntax und Semantik 18

27 2 Java: Der Einstieg Im vorigen Kapitel haben wir die Syntax der Sprache Mini-Java kennengelernt. Bevor wir in Kapitel 3 ausführlich auf Syntax und Semantik der Programmiersprache Java eingehen werden, soll dieses Kapitel zunächst einige grundlegende Vorbemerkungen zu Java machen, sowie zu den beiden zugrundeliegenden Sprachkonzepten, der imperativen und der objektorientierten Programmierung. 2.1 Grundlegendes zu Java Historisches : Entwicklung der Programmiersprache OAK durch James Gosling von Sun Microsystems (zunächst für Toaster, Mikrowellen etc.; unabhängig vom Chip, extrem zuverlässig) Umbenennung in Java 1995: α und β Release von IBM, SGI, Oracle und Microsoft lizensiert Eigenschaften von Java durch den Bytecode (Zwischensprachencode) unabhängig von der Plattform Syntax an C und C++ angelehnt objektorientiert streng typisiert unterstützt parallele Abläufe (Nebenläufigkeit / Threads) Graphical User Interface (GUI) netzwerkfähig 19

28 2 Java: Der Einstieg modularer Aufbau Nachteil: Effizienz leidet (ca. 5 10mal langsamer als C und C++) Selbstständig laufende Anwendung Quellprogramm Java-Compiler Java-Bytecode Java-Interpreter Ablauf des Programms Applet Quellprogramm Java-Compiler Java-Bytecode auf dem Server Übertragung per Internet Bytecodes auf dem Rechner des Benutzers (Client) Java-Interpreter im Browser oder Applet-Viewer Ablauf des Programms ABBILDUNG 2.1: Vom Quellprogramm zum Ablauf Abbildung 2.1 zeigt die notwendigen Schritte, um ein Java-Programm (selbstständig laufende Anwendung) bzw. ein Applet ablaufen zu lassen. Ein Applet ist eine in ein HTML-Dokument eingebettete Anwendung, die auf dem Rechner des Anwenders abläuft. Wir beschränken uns in diesem Skript auf selbstständig laufende Anwendungen. 20

29 2.1 Grundlegendes zu Java Sicherheit keine Pointerarithmetik wie in C Garbage Collection 1 Überprüfungen zur Laufzeit (Datentypen, Indizes, etc.) durch Mechanismen zur Verifizierung von Java-Bytecode bei der Übertragung dennoch ist die (Netz-)Sicherheit umstritten Erstellen eines Java-Programms 1. Quellprogramm erstellen: class Hello { public static void main(string[] args) { System.out.println("Hello World"); Dieses Programm besteht aus der Klasse Hello. Der Klassenname muss mit dem Dateinamen übereinstimmen, d.h. das oben gezeigte Programm muss in der Datei Hello.java abgelegt sein. Die Klasse Hello enthält eine Methode namens main. Ein Java-Programm muss eine main-methode enthalten, denn die main-methode wird bei der Interpretation aufgerufen. 2. Übersetzen eines Programms: > javac Hello.java liefert eine Datei Hello.class, die das Programm in Bytecode enthält. 3. Interpretation des Bytecodes: > java Hello 1 Ein Garbage Collector entfernt automatisch Objekte, Felder und Variablen, auf die keine Referenz mehr vorhanden ist, aus dem Speicher (siehe Arnold & Gosling [1], S. 12, Kapitel 1.6). 21

30 2 Java: Der Einstieg 2.2 Grundzüge imperativer Sprachen Das Behältermodell der Variablen Imperative Programmierung geht aus vom Modell eines Speichers, aufgegliedert in einzelne Variablen, in denen Werte abgelegt werden können. Der Speicher bzw. die Variablen werden verändert durch Befehle bzw. Anweisungen, die selbst vom aktuellen Speicherinhalt abhängen. Ein typisches Beispiel ist die Anweisung x = y + z. Sie bedeutet: Addiere die Variableninhalte von y und z und lege die Summe in der Variablen x ab. Während diese Semantikbeschreibung nun suggeriert, dass der einzige Effekt der Anweisung eine Änderung des Variableninhalts von x ist, ist in der Tat diese Vorstellung zu einfach: nicht nur der Variableninhalt von x kann sich ändern, sondern auch der von y und z sowie aller möglicher anderer Variablen; falls x y oder y z, dann ist dies unmittelbar einsichtig. Also ist x = y + z keine Gleichheit, die zwischen den Werten (Inhalten) von x, y und z gilt, und mittels der wir über Programme nachdenken und Beweise führen können. Das Prinzip der referential transparency (Werttreue), das in der funktionalen Programmierung gilt, ist in der imperativen verletzt Konsequenzen 1. Der Nachweis von Programmeigenschaften wird viel schwieriger, ebenso das Verstehen von Programmen. 2. Die Semantik eines Programms hängt von einem strikten Nacheinander der Ausführung der einzelnen Anweisungen ab. 3. Wiederverwendung von Programmteilen in anderem Kontext bedarf besonderer Vorsicht. 2.3 Klassen, Objekte und Methoden im Überblick Java-Programme werden aus Klassen aufgebaut. Aus einer Klassendefinition lassen sich beliebig viele Objekte erzeugen, die auch Instanzen genannt werden (vgl. Arnold & Gosling [1], Kapitel 1.6 und 1.7). 22

31 2.3 Klassen, Objekte und Methoden im Überblick Eine Klasse enthält folgende Bestandteile: Objektvariablen (objektbezogene Datenfelder) objektbezogene Methoden Klassenvariablen (klassenbezogene Datenfelder) klassenbezogene Methoden Datenfelder (Synonym: Attribute) enthalten den Zustand des Objektes oder der Klasse. Methoden sind Sammlungen von imperativ formulierten Anweisungen, die auf den Datenfeldern operieren, um deren Zustand zu ändern Klassen Beispiel der Deklaration einer einfachen Klasse: class Point { double x, y; In der Klasse Point sind zwei Objektvariablen deklariert, die die x- und y-koordinate eines Punktes repräsentieren. double bedeutet, dass die Variablen vom Typ double 2 sind. Bis jetzt gibt es noch kein Objekt vom Typ Point, nur einen Plan, wie ein solches Objekt aussehen wird Das Erzeugen von Objekten Objekte werden mit dem Schlüsselwort new erzeugt. Neu geschaffene Objekte bekommen innerhalb eines Bereiches des Speichers (welcher Heap genannt wird) einen Speicherplatz zugewiesen und werden dort abgelegt. Auf alle Objekte in Java wird über Objektreferenzen zugegriffen jede Variable, die ein Objekt zu enthalten scheint, enthält tatsächlich eine Referenz auf dieses Objekt (bzw. auf deren Speicherplatz). Objektreferenzen haben den Wert null, wenn sie sich auf kein Objekt beziehen. Wir werden im folgenden Objekte und Objektreferenzen synonym verwenden, es sei denn, die Unterscheidung ist wichtig. Erzeugung und Initialisierung Point lowerleft = new Point(); Point upperright = new Point(); 2 Gleitkommazahlen 23

32 2 Java: Der Einstieg Wertzuweisung lowerleft.x = 0.0; lowerleft.y = 0.0; upperright.x = ; upperright.y = ; Klassenvariablen class Point { double x, y; static Point origin = new Point(); Wird eine Variable als static deklariert, so handelt es sich um eine Klassenvariable. Durch obige Deklaration gibt es genau eine Klassenvariable names Point.origin, egal ob und wie viele Point-Objekte erzeugt werden. Point.origin hat den Wert (0,0), weil dies die Voreinstellung für numerische Datenfelder ist, die nicht explizit auf einen anderen Wert initialisiert werden (das wird später noch genauer besprochen). Allerdings kann der Wert von Point.origin geändert werden. Ist dies nicht erwünscht, soll Point.origin also eine Konstante sein, so muss man den Modifizierer final benutzen. static final Point origin = new Point(); Methoden Eine Methode ist eine Funktion bzw. Prozedur. Sie kann parameterlos sein oder Parameter haben. Sie kann einen Rückgabewert liefern oder als void deklariert sein, wenn sie keinen Wert zurückliefert. Methoden dürfen nicht geschachtelt werden. Innerhalb von Methoden dürfen lokale Variablen deklariert werden. class Point { double x, y; void clear() { x = 0.0; y = 0.0; Um eine Methode aufzurufen, gibt man ein Objekt und den Methodennamen, getrennt durch einen Punkt, an. 24

33 2.4 Vererbung, Pakete und Gültigkeitsbereiche lowerleft.clear(); upperright.clear(); Nun definieren wir eine Methode, die die Distanz zwischen dem Punkt, auf den sie angewendet wird und einem übergebenen Punkt p zurückgibt. (Math.sqrt() ist vordefiniert und liefert die Wurzel einer Zahl.) double distance(point p) { double xdiff, ydiff; // Beispiel fuer lokale Variablen xdiff = x - p.x; ydiff = y - p.y; return Math.sqrt(xdiff*xdiff + ydiff*ydiff); Aufruf: double d = lowerleft.distance(upperright); Klassenbezogene Methoden Klassenbezogene Methoden werden durch das Schlüsselwort static deklariert, z.b. ist Math.sqrt() eine Klassenmethode der vordefinierten Klasse Math. distance als Klassenmethode static double distance(point p1, Point p2) { double xdiff = p1.x - p2.x; double ydiff = p1.y - p2.y; return Math.sqrt(xdiff*xdiff + ydiff*ydiff); Aufruf: double d = Point.distance(lowerLeft, upperright); In der Klasse Point selbst kann man Point. weglassen, d.h. es reicht ein Aufruf der Form: double d = distance(lowerleft, upperright); 2.4 Vererbung, Pakete und Gültigkeitsbereiche Vererbung Klassen in Java können um zusätzliche Variablen und Methoden erweitert werden. Dies wird durch das Schlüsselwort extends angezeigt. Die entstehende Unterklasse besitzt dann alle Eigenschaften der Oberklasse und zusätzlich die in der jeweiligen Erweiterung angegebenen Eigenschaften. Dieses Konzept wird auch als Vererbung bezeichnet, weil die Unterklasse alle Eigenschaften der Oberklasse erbt. 25

34 2 Java: Der Einstieg Zum Beispiel ist ein farbiger Punkt eine Erweiterung eines Punktes: class ColoredPoint extends Point { String color; Pakete Bei größeren Softwareprojekten ist es häufig ratsam, diese in verschiedene, unabhängige Teile aufzuteilen. Solche Teile werden als Module oder Pakete bezeichnet. Java besitzt einige Eigenschaften, die es erlauben, Software modular aufzubauen: Verschiedene (i.d.r. logisch zusammengehörige) Klassen können in einem Paket zusammengefasst werden. Die Klassendefinitionen können in verschiedenen Dateien enthalten sein. Der Paketname muss im Header jeder Datei angegeben sein: A.java C.java package abc; package abc; public class A { class C { class B {... Um Definitionen aus anderen Paketen sichtbar zu machen, müssen sie mit dem Schlüsselwort import importiert werden: import abc.a; class test { A a; Weitere Details zu Paketen folgen in Kapitel Gültigkeitsbereiche Bei dem Zugriff auf Definitionen anderer Klassen oder Pakete gelten Beschränkungen, die beachtet werden müssen. Die Zugriffsrechte werden durch Gültigkeitsmodifizierer geregelt, die sich folgendermaßen auf Datenfelder, Methoden und Klassen auswirken (für Klassen gibt es nur public und default): 26

35 2.5 Aufgaben zugreifbar für Nicht-Unterklassen im selben Paket zugreifbar für Unterklassen im selben Paket zugreifbar für Nicht-Unterklassen in einem anderen Paket zugreifbar für Unterklassen in einem anderen Paket public default (package) protected private ja ja ja nein ja ja ja nein ja nein nein nein ja nein ja nein 2.5 Aufgaben Aufgabe Erstellen Sie eine Klasse Circle, deren Instanzen Kreise im zweidimensionalen Raum repräsentieren. Ein Kreis ist z.b. bestimmt durch seinen Radius r und durch die x- und y-koordinate seines Mittelpunktes in der Ebene. Implementieren Sie für diese Klasse folgende Methoden: (i) circumference berechnet den Umfang des Kreises. (ii) area berechnet den Inhalt des Kreises. Wie können die Methoden in einem Programm aufgerufen werden? 27

36 2 Java: Der Einstieg 28

37 3 Imperative Programmierung in Java Im vorigen Kapitel haben wir generelle Eigenschaften der imperativen wie der objektorientierten Programmierung kennengelernt. Auch Teile der Syntax der objektorientierten Anteile von Java wurden vorgestellt. Dieses Kapitel wird sich nun mit der Syntax und Semantik der imperativen Anteile von Java beschäftigen, d.h. im wesentlichen mit den Rümpfen von Methoden. Weder Syntax noch Semantik werden allerdings formal eingeführt, wie wir es in Kapitel 1 kennengelernt haben, sondern im wesentlichen anhand von Beispielen. Zunächst wird die Semantik von Mini-Java erläutert, dessen Syntax wir bereits in Kapitel 1 definiert haben. Danach werden die wichtigsten der noch fehlenden Java-Konstrukte vorgestellt. Eine komplette Syntaxbeschreibung von Java findet sich auf der folgenden SUN-Webseite: Mini-Java Ein Mini-Java Programm besteht aus genau einer Klasse. In dieser Klasse gibt es genau eine main-methode. Folgende Konstrukte sind Anweisungen (statements gemäß Mini-Java-Syntax, vgl. Beispiel 1.2.2): 1. Die Deklaration einer Variablen vom Typ int mit sofortiger Initialisierung: int ident = expression; Jeder Bezeichner (ident) darf in höchstens einer Variablendeklaration vorkommen. Diese kontextsensitive Bedingung lässt sich nicht in der EBNF- Definition formulieren. 2. Die Zuweisung eines Wertes an eine Variable: ident = expression; Diese Variable muss vorher deklariert worden sein und den gleichen Typ wie der Ausdruck haben. Diese Nebenbedingung ist ebenfalls nicht in der EBNF-Definition ausgedrückt. 29

38 3 Imperative Programmierung in Java 3. Eine bedingte Anweisung (if-then Anweisung): if(condition) statement Der Bool sche Ausdruck (condition) wird ausgewertet; ist er true, so wird die Anweisung (statement) ausgeführt. Ist er false, so wird die Anweisung nicht ausgeführt und die Programmausführung mit der nächsten Anweisung hinter der if-then Anweisung fortgesetzt. 4. Eine abweisende Schleife (while-schleife): while(condition) statement Der Bool sche Ausdruck wird ausgewertet; ist er true, so wird die Anweisung so lange ausgeführt, bis der Bool sche Ausdruck false wird. 5. Ein Block. { statement1; statement2;... Die Statements in der geschweiften Klammer werden von links nach rechts nacheinander abgearbeitet. 6. Eine Anweisung zum Schreiben auf der Standardausgabe: System.out.println(...); System ist eine Klasse, die klassenbezogene Methoden zur Darstellung des Zustandes des Systems bereitstellt. out ist eine Klassenvariable der Klasse System, ihr Inhalt ist der Standardausgabestrom. Die Methode println wird also auf das klassenbezogene Datenfeld out angewendet es wird ein String mit abschließendem Zeilenvorschub auf dem Standardausgabestrom ausgegeben. 7. Die leere Anweisung. ; Es geschieht nichts. 8. Die Deklaration eines eindimensionalen Feldes (Arrays) mit sofortiger Initialisierung: int[] array = new int[3]; deklariert ein Feld namens array, erzeugt ein Feld mit drei int-komponenten und weist dieses der Feldvariablen array zu. Beachte, dass die Dimension (3) einer Feldvariablen nicht bei der Deklaration (int[] array) angegeben wird, sondern nur bei der Erzeugung (new int[3]). Die erste Komponente eines Feldes hat den Index 0. Die Länge des Feldes kann aus dessen Datenfeld length ausgelesen werden (array.length). 30

39 3.1 Mini-Java 9. Die Zuweisung eines Wertes an die i-te Komponente eines Feldes, wobei 0 i array.length 1: array[i] = expression; Alle weiteren Konstrukte haben eine offensichtliche Bedeutung, bis auf Integer.parseInt(argsIdent[expression]) Betrachten wir hierfür noch einmal die main-methode: Sie hat als Parameter ein Feld von Zeichenketten. Diese Zeichenketten sind die Programmargumente und werden normalerweise vom Anwender beim Programmaufruf eingegeben. class Echo { public static void main(string[] args) { int i = 0; while(i < args.length) { System.out.println(args[i]); i = i+1; > java Echo > Um eine Zeichenkette in eine ganze Zahl zu konvertieren, wird die Klassenmethode parseint der Klasse Integer mit dieser Zeichenkette als Argument aufgerufen. Sie liefert die entsprechende ganze Zahl als Ergebnis zurück bzw. meldet einen Fehler, falls die Zeichenkette keine ganze Zahl dargestellt hat. class BadAddOne { public static void main(string[] args) { int i = 0; while(i < args.length) { int wert = args[i]; wert = wert+1; System.out.println(wert); i = i+1; 31

40 3 Imperative Programmierung in Java > javac BadAddOne.java BadAddOne.java:6: Incompatible type for declaration. Can t convert java.lang.string to int. int wert = args[i]; > Stattdessen muss eine explizite Typkonvertierung stattfinden: class AddOne { public static void main(string[] args) { int i = 0; while(i < args.length) { int wert = Integer.parseInt(args[i]); wert = wert+1; System.out.println(wert); i = i+1; > java AddOne > 3.2 Von Mini-Java zu Java Jedes Mini-Java Programm ist ein Java Programm. In diesem Abschnitt werden die Datentypen und imperativen Konstrukte von Java erläutert, die nicht bereits in Mini-Java vorhanden sind Elementare Datentypen Unicode: Java, als Sprache für das World Wide Web, benutzt einen 16-Bit Zeichensatz, genannt Unicode. Die ersten 256 Zeichen von Unicode sind identisch mit dem 8-Bit Zeichensatz Latin-1, wobei wiederum die ersten 128 Zeichen von Latin-1 mit dem 7-Bit ASCII Zeichensatz übereinstimmen. 32

41 3.2 Von Mini-Java zu Java Elementare Datentypen und deren Literale: Typ boolean true und false Typ int 29 (Dezimalzahl) oder 035 (Oktaldarstellung wegen führender 0) oder 0x1D (Hexadezimaldarstellung wegen führendem 0x) oder 0X1d (Hexadezimaldarstellung wegen führendem 0X) Typ long 29L (wegen angehängtem l oder L) Typ short short i = 29; (Zuweisung, es gibt kein short-literal) Typ byte byte i = 29; (Zuweisung, es gibt kein byte-literal) Typ double 18.0 oder 18. oder 1.8e1 oder.18e2 Typ float 18.0f (wegen angehängtem f oder F) Typ char Q, \u0022, \u0b87 Typ String "Hallo" (String ist kein elementarer Datentyp; s. Kapitel 4) Initialbelegungen: Während ihrer Deklaration kann eine Variable wie in Mini- Java initialisiert werden. final double PI = ; float radius = 1.0f; Sind für Datenfelder einer Klasse keine Anfangswerte angegeben, so belegt Java sie mit voreingestellten Anfangswerten. Der Anfangswert hängt vom Typ des Datenfeldes ab: Feld-Typ Anfangswert boolean false char \u0000 Ganzzahl (byte, short, int, long) 0 Gleitkommazahl +0.0f oder +0.0d andere Referenzen null Lokale Variablen in einer Methode (oder einem Konstruktor oder einem klassenbezogenen Initialisierungsblock) werden von Java nicht mit einem Anfangswert initialisiert. Vor ihrer ersten Benutzung muss einer lokalen Variablen ein Wert zugewiesen werden (ein fehlender Anfangswert ist ein Fehler) Kommentare // Kommentar bis zum Ende der Zeile /* Kommentar zwischen */ 33

42 3 Imperative Programmierung in Java Achtung: /* */ können nicht geschachtelt werden! /* falsch /* geschachtelter Kommentar */ */ Bool sche Operatoren && logisches und logisches oder! logisches nicht Die Auswertung eines Bool schen Ausdrucks erfolgt von links nach rechts, bis der Wert eindeutig feststeht. Folgender Ausdruck ist deshalb robust: if(index>=0 && index<array.length && array[index]!=0) Bitoperatoren Die Bitoperatoren & (und) und (oder) sind definiert durch: & int-zahlen werden durch diese Operatoren bitweise behandelt. Beispiel Es seien x und y folgendermaßen gewählt: x = 60 (in Binärdarstellung ) und y = 15 (binär: ). In diesem Fall ist x&y = 12 und x y = 63: x & y (60) & (15) (12) x y (60) (15) (63) Wenn man 0 als false und 1 als true interpretiert, so entspricht & dem logischen und (&&) und dem logischen oder ( ). 34

43 3.2 Von Mini-Java zu Java Inkrement und Dekrement Man kann den Wert einer Variablen x (nicht den eines Ausdrucks) durch den Operator ++ um 1 erhöhen bzw. durch -- um 1 erniedrigen. Es gibt Präfixund Postfixschreibweisen, die unterschiedliche Wirkungen haben: Bei der Präfixschreibweise wird der Wert zuerst modifiziert und danach der veränderte Wert zurückgeliefert. Bei der Postfixschreibweise wird zuerst der Wert der Variablen zurückgeliefert, dann wird sie modifiziert. int i = 10; int i = 10; int j = i++; int j = ++i; System.out.println(j); System.out.println(j); > > Der Ausdruck i++ ist gleichbedeutend mit i = i+1, jedoch wird i nur einmal ausgewertet! Beispiel (A) (B) arr[where()]++; Die Methode where() wird einmal aufgerufen. arr[where()] = arr[where()]+1; Hierbei wird die Methode where() jedoch zweimal aufgerufen. Seiteneffekte können hier sogar das Ergebnis beeinflussen: In dem Kontext arr[0] = 0; arr[1] = 1; arr[2] = 2; und private static int zaehler = 0; private static int where() { zaehler = zaehler+1; return zaehler; liefert (A) arr[1] = 2 bzw. (B) arr[1] = Zuweisungsoperatoren i += 2; ist gleichbedeutend mit i = i+2; außer, dass der Ausdruck auf der linken Seite von i += 2; nur einmal ausgewertet wird (vgl. Inkrement und Dekrement). Entsprechend sind -=, &= und = definiert. 35

44 3 Imperative Programmierung in Java Die nichtabweisende Schleife Zusätzlich zur abweisenden Schleife gibt es eine nichtabweisende Schleife in Java: do statement while(condition); Die condition wird erst nach der Ausführung von statement ausgewertet. Solange sie true ist, wird statement wiederholt for-schleife for(init-statement; condition; increment-statement) statement ist gleichbedeutend mit (mit Ausnahme vom Verhalten bei continue): { init-statement while(condition) { statement increment-statement Übliche Verwendung der for-schleife: for(int i=0; i<=10; i++) { System.out.println(i); Der Gültigkeitsbereich der (Lauf-)Variablen i beschränkt sich auf die for-schleife! int i = 0; for(int i=0; i<=10; i++) { System.out.println(i); ist jedoch nicht möglich, da die Variable i vorher schon deklariert wurde. Die Initialisierungs- bzw. Inkrementanweisung einer for-schleife kann eine durch Kommata getrennte Liste von Ausdrücken sein. Diese werden von links nach rechts ausgewertet. 36

45 3.2 Von Mini-Java zu Java Beispiel (Arnold & Gosling [1], S. 144) public static int zehnerpotenz(int wert) { int exp, v; for(exp=0,v=wert; v>0; exp++, v=v/10) ; // leere Anweisung return exp; Alle Ausdrücke dürfen auch leer sein; dies ergibt eine Endlosschleife: for(;;) { System.out.println("Hallo"); if-then-else Anweisung if(condition) statement1 else statement2 Die condition wird ausgewertet; ist sie true, so wird statement1 ausgeführt. Ist sie false, so wird statement2 ausgeführt. Der else-zweig darf entfallen; dies ergibt dann die if-then Anweisung aus Mini-Java. Ein else bezieht sich immer auf das letzte if, das ohne zugehöriges else im Programm vorkam. Was passiert, wenn mehr als ein if ohne ein else vorangeht? Das folgende Beispiel zeigt eine falsche (d.h. nicht intendierte) und eine richtige Verwendung (Schachtelung) von if-then-else Anweisungen. Beispiel (Arnold & Gosling [1], S. 139) public double positivesumme(double[] werte) { double sum = 0.0; if(werte.length > 1) for(int i=0; i<werte.length; i++) if(werte[i] > 0) sum += werte[i]; else // hoppla! sum = werte[0]; return sum; 37

46 3 Imperative Programmierung in Java Der else-teil sieht so aus, als ob er an die Feldlängenprüfung gebunden wäre, aber das ist eine durch die Einrückung erweckte Illusion, und Java ignoriert Einrückungen. Statt dessen ist ein else-teil an das letzte if gebunden, das keinen else-teil hat. So ist der vorangehende Block äquivalent zu: public double positivesumme(double[] werte) { double sum = 0.0; if(werte.length > 1) for(int i=0; i<werte.length; i++) if(werte[i] > 0) sum += werte[i]; else // hoppla! sum = werte[0]; return sum; Das war vielleicht nicht beabsichtigt. Um den else-teil an das erste if zu binden, müssen Klammern zur Erzeugung eines Blocks verwendet werden: public double positivesumme(double[] werte) { double sum = 0.0; if(werte.length > 1) { for(int i=0; i<werte.length; i++) if(werte[i] > 0) sum += werte[i]; else { sum = werte[0]; return sum; 38

47 3.2 Von Mini-Java zu Java Mehrdimensionale Felder Mehrdimensionale Felder werden in Java durch Felder von Feldern realisiert. Beispiel (Jobst [6], S. 37) public class Array2Dim { public static void main(string[] args) { int[][] feld = new int[3][3]; //Weise feld[i][j] den Wert (i+1)*10+j zu for(int i=0; i<feld.length; i++) { for(int j=0; j<feld[i].length; j++) { feld[i][j] = (i+1)*10+j; System.out.print(feld[i][j]+" "); System.out.println(); > java Array2Dim Da Felder in Java dynamisch sind, kann bei mehrdimensionalen Feldern jedes verschachtelte Feld eine andere Größe aufweisen. Beispiel (Jobst [6], S. 38) public class DemoArray { public static void main(string[] args) { int[][] feld = new int[3][]; for(int i=0; i<feld.length; i++) { feld[i] = new int[i+1]; for(int j=0; j<feld[i].length; j++) { feld[i][j] = (i+1)*10+j; System.out.print(feld[i][j]+" "); 39

48 3 Imperative Programmierung in Java System.out.println(); > java DemoArray Felder können bei ihrer Deklaration sofort initialisiert werden: Beispiel (Jobst [6] S. 39) public class DemoFeldInitial { public static void main(string[] args) { int[][] feld = {{1,2,3,{4,5,{7,8,9,10; //Ausgabe des Feldes for(int i=0; i<feld.length; i++) { for(int j=0; j<feld[i].length; j++) System.out.print(feld[i][j]+" "); System.out.println(); > java DemoFeldInitial switch-anweisung switch(expression) { case const1: statement1 break; case const2: statement2 break;... default: statement 40

49 3.2 Von Mini-Java zu Java Der Ausdruck (expression) muss ganzzahlig sein. Nach case müssen Konstanten stehen, die bei der Übersetzung des Programms berechnet werden können. Der Ausdruck wird berechnet. Danach wird das Programm an derjenigen case- Anweisung fortgesetzt, deren Konstante dem Wert des Ausdrucks entspricht. Mit break kann man switch verlassen. Nach case darf nur jeweils eine Konstante stehen. Wenn es keine passende Konstante gibt, so wird das Programm bei der default Anweisung fortgesetzt (falls vorhanden). Achtung: Das nächste case erzwingt nicht das Verlassen der switch-anweisung. Es impliziert auch nicht das Ende der Anweisungsausführung. Beispiel (Jobst [6], S. 15) public class DemoFuerSwitch { public static void main (String[] args) { for(int i=0; i<=10; i++) switch(i) { case 1: case 2: System.out.println(i+" Fall 1,2"); // Weiter bei Fall 3 case 3: System.out.println(i+" Fall 3"); // Weiter bei Fall 7 case 7: System.out.println(i+" Fall 7"); break; default: System.out.println(i+" sonst"); > java DemoFuerSwitch 0 sonst 1 Fall 1,2 1 Fall 3 1 Fall 7 2 Fall 1,2 2 Fall 3 2 Fall 7 3 Fall 3 3 Fall 7 4 sonst 5 sonst 6 sonst 41

50 3 Imperative Programmierung in Java 7 Fall 7 8 sonst 9 sonst 10 sonst > 3.3 Aufgaben Aufgabe Schreiben Sie ein Programm Primes in Mini-Java, das eine Zahl als Argument erhält und überprüft, ob diese Zahl eine Primzahl ist. Wenn sie eine ist, dann soll die Zahl wieder ausgegeben werden; wenn nicht, dann sollen die Zahlen, durch die sie teilbar ist, ausgegeben werden. Aufgabe Implementieren Sie eine Klasse Factorial, in der die Fakultät einer Zahl durch die Methoden fwhile und ffor berechnet werden kann. Dabei soll die Methode fwhile die Fakultät einer Zahl mit einer while-schleife berechnen, während ffor dazu eine for-schleife benutzt. Aufgabe Schreiben Sie ein Programm, das das Pascalsche Dreieck bis zu einer Tiefe von zwölf berechnet, dabei jede Reihe des Dreiecks in einem Array mit entsprechender Länge speichert und alle zwölf Arrays in einem Array von int- Arrays hält. Entwerfen Sie Ihre Lösung so, dass die Ergebnisse durch eine Methode ausgegeben werden, die die Länge der einzelnen Arrays des Hauptarrays berücksichtigt und nicht von einer konstanten Länge 12 ausgeht. Anschließend sollten Sie Ihren Code bezüglich der Konstante 12 modifizieren können, ohne dabei die Ausgabemethode ändern zu müssen. Aufgabe Schreiben Sie unter Verwendung von if/else bzw. unter Verwendung von switch je eine Methode, die einen Stringparameter erhält und einen String zurückliefert, in dem alle Sonderzeichen des Ursprungsstrings durch ihre Java-Äquivalente ersetzt wurden. Ein String, der beispielsweise ein Anführungszeichen (") enthält, sollte einen String zurückliefern, in dem das " durch \" ersetzt worden ist. Bitte ersetzen Sie alle Zeichen, die in der folgenden Tabelle aufgeführt werden durch ihre Äquivalente: Sonderzeichen Java-Äquivalent " \" \ \ \\ 42

51 4 Algorithmen zur exakten Suche in Texten In diesem Kapitel wird ein Problem aus der Sequenzanalyse näher betrachtet, die exakte Textsuche: Gegeben ein Text und ein Muster, finde alle Vorkommen von dem Muster in dem Text. Man wird sehen, dass zur Lösung dieses einfachen Problem einige nicht-triviale, aber sehr effiziente Algorithmen entwickelt wurden. Zunächst soll jedoch ein kurzer Blick auf die Art und Weise geworfen werden, wie Sequenzen üblicherweise in Java repräsentiert sind. 4.1 Die Klasse String Zeichenketten sind in Java Objekte. Sie können mit dem +-Operator zu neuen Objekten verknüpft werden. String str1 = "hello"; String str2 = " world"; String str3 = str1 + str2; Grundlegende objektbezogene Methoden der Klasse String: public int length() liefert die Anzahl der Zeichen in der Zeichenkette. public int charat(int index) liefert das Zeichen an der Position index der Zeichenkette. public int indexof(char ch, int fromindex) liefert die erste Position fromindex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. 1, falls das Zeichen nicht vorkommt. public int lastindexof(char ch, int fromindex) liefert die letzte Position fromindex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. 1, falls das Zeichen nicht vorkommt (lastindexof liest also im Gegensatz zu indexof die Zeichenkette von rechts nach links). 43

52 4 Algorithmen zur exakten Suche in Texten public boolean startswith(string prefix) liefert true gdw. die Zeichenkette mit dem angegebenen prefix beginnt. public boolean endswith(string suffix) liefert true gdw. die Zeichenkette mit dem angegebenen suffix endet. public String substring(int beginindex, int endindex) liefert das Teilwort der Zeichenkette zwischen den Positionen beginindex und endindex 1. public char[] tochararray() wandelt die Zeichenkette in ein Feld von Zeichen um. public boolean regionmatches (int start, String other, int ostart, int len) liefert true gdw. der Bereich der Zeichenkette mit dem Bereich der Zeichenkette String other übereinstimmt (s.u.). Der Vergleich beginnt bei der Position start bzw. ostart. Es werden nur die ersten len Zeichen verglichen. public boolean equals(object anobject) liefert true gdw. die übergebene Objektreferenz auf ein Objekt vom Typ String mit demselben Inhalt zeigt. (Dies steht im Gegensatz zu ==, was die Objekt-(referenz-)gleichheit testet.) Man beachte den Rückgabetyp int von charat bzw. den Parametertyp int von ch in indexof und lastindexof. Dennoch können die Methoden auf (Unicode-) Zeichen angewendet werden: Ein char-wert in einem Ausdruck wird automatisch in einen int-wert umgewandelt, wobei die oberen 16 Bit auf Null gesetzt werden. Beispiel public class DemoStringClass { public static void main(string[] args) { String str = "bielefeld"; System.out.println(str.length()); System.out.println(str.charAt(0)); System.out.println(str.indexOf( e,3)); System.out.println(str.lastIndexOf( e,3)); System.out.println(str.startsWith("biele")); System.out.println(str.endsWith("feld")); System.out.println(str.substring(0,5)); System.out.println(str.substring(5,9)); System.out.println(str.regionMatches(0,"obiele",1,5)); if(str == args[0]) 44

53 4.2 Grundlegende Definitionen System.out.println("str == args[0]"); if(str.equals(args[0])) System.out.println("str.equals(args[0])"); > java DemoStringClass bielefeld 9 b 4 2 true true biele feld true str.equals(args[0]) 4.2 Grundlegende Definitionen Die Suche nach allen (exakten) Vorkommen eines Musters in einem Text ist ein Problem, das häufig auftritt (z.b. in Editoren, Datenbanken etc.). Wir vereinbaren folgende Konventionen (wobei Σ ein Alphabet ist): Muster P = P [0... m 1] Σ m Text T = T [0... n 1] Σ n P [j] = Zeichen an der Position j P [b... e] = Teilwort von P zwischen den Positionen b und e Beispiel P = abcde, P [0] = a, P [3] = d, P [2... 4] = cde. Notation: leeres Wort: ε Länge eines Wortes x: x Konkatenation zweier Worte x und y: xy Definition Ein Wort w ist ein Präfix eines Wortes x (w x), falls x = wy gilt für ein y Σ. w ist ein Suffix von x (w x), falls x = yw gilt für ein y Σ. x ist also auch ein Präfix bzw. Suffix von sich selbst. 45

54 4 Algorithmen zur exakten Suche in Texten Lemma Es seien x, y und z Zeichenketten, so dass x z und y z gilt. Wenn x y ist, dann gilt x y. Wenn x y ist, dann gilt y x. Wenn x = y ist, dann gilt x = y. Beweis: Übung (Aufgabe 4.7.1). 4.3 Das Problem der exakten Suche Definition Ein Muster P kommt mit der Verschiebung s im Text T vor, falls 0 s n m und T [s... s + m 1] = P [0... m 1] gilt. In dem Fall nennen wir s eine gültige Verschiebung. Das Problem der exakten Textsuche ist nun, alle gültigen Verschiebungen zu finden. Mit den Bezeichnungen S 0 = ε und S k = S[0... k 1] (d.h. S k ist das k-lange Präfix eines Wortes S Σ l für 0 k l) können wir das Problem der exakten Textsuche folgendermaßen formulieren: Finde alle Verschiebungen s, so dass P T s+m. Beispiel T = bielefeld oh bielefeld P = feld Gültige Verschiebungen: s = 5 und s = 18. Die naive Lösung des Problems sieht in Pseudo-Code wie folgt aus: Naive-StringMatcher(T, P ) 1 n length[t ] 2 m length[p ] 3 for s 0 to n m do 4 if P [0... m 1] = T [s... s + m 1] then 5 print Pattern occurs with shift s Am Beispiel T = abrakadabraxasa und P = abraxas erkennt man das ineffiziente Vorgehen, insbesondere wenn Text und Muster immer komplett verglichen werden: 46

55 4.3 Das Problem der exakten Suche a b r a k a d a b r a x a s a a b r a x a s a b r a x a s a b r a x a s a b r a x a s a b r a x a s a b r a x a s a b r a x a s a b r a x a s a b r a x a s Eine Implementierung in Java verläuft analog: public static void naivematcher(string text, String pattern) { int m = pattern.length(); int n = text.length(); for(int s=0; s<=n-m; s++) { if(text.regionmatches(s,pattern,0,m)) System.out.println("naiveMatcher: Pattern occurs with shift "+s); Aber selbst wenn Text und Muster zeichenweise von links nach rechts und nur bis zum ersten Mismatch 1 verglichen werden, ist die worst-case Zeiteffizienz des naiven Algorithmus O ( n m ), z.b. wird für T = a n, P = a m die for-schleife n m + 1 mal ausgeführt und in jedem Test in Zeile 4 werden m Zeichen verglichen. Wie kann man den naiven Algorithmus verbessern? Idee 1: Überspringe ein Teilwort w von T, falls klar ist, dass w P (BM-Algorithmus 1977). Idee 2: Merke Informationen über bisherige Vergleiche und nutze diese, um neue, unnötige Vergleiche zu vermeiden (KMP-Algorithmus 1977). 1 Die zwei Zeichen stimmen nicht überein. 47

56 4 Algorithmen zur exakten Suche in Texten 4.4 Der Boyer-Moore-Algorithmus Der BM-Algorithmus legt wie der naive Algorithmus das Muster zunächst linksbündig an den Text, vergleicht die Zeichen des Musters dann aber von rechts nach links mit den entsprechenden Zeichen des Textes. Beim ersten Mismatch benutzt er zwei Heuristiken, um eine Verschiebung des Musters nach rechts zu bestimmen Die bad-character Heuristik Falls beim Vergleich von P [0... m 1] und T [s... s + m 1] (von rechts nach links) ein Mismatch P [j] T [s + j] für ein j mit 0 j m 1 festgestellt wird, so schlägt die bad-character Heuristik eine Verschiebung des Musters um j k Positionen vor, wobei k der größte Index (0 k m 1) ist mit T [s + j] = P [k]. Wenn kein k mit T [s + j] = P [k] existiert, so sei k = 1. Man beachte, dass j k negativ sein kann Die good-suffix Heuristik Falls beim Vergleich von P [0... m 1] und T [s... s + m 1] (von rechts nach links) ein Mismatch P [j] T [s + j] für ein j mit 0 j m 1 festgestellt wird, so wird das Muster so weit nach rechts geschoben, bis das bekannte Suffix T [s + j s + m 1] wieder auf ein Teilwort des Musters passt. Hierfür ist eine Vorverarbeitung des Musters notwendig, auf die wir hier aber nicht näher eingehen wollen. Im Boyer-Moore Algorithmus wird das Muster um das Maximum von beiden vorgeschlagenen Verschiebungen verschoben. Es kann gezeigt werden, dass die Laufzeitkomplexität dann im worst case O(n + m) ist. Beispiel Die Ausgangssituation ist in Abbildung 4.1(a) dargestellt. Die badcharacter Heuristik schlägt eine Verschiebung von j k = (m 3) 5 = (12 3) 5 = 4 Positionen vor; dies ist im Fall (b) illustriert. Aus der Darstellung (c) wird ersichtlich, dass die good-suffix Heuristik eine Verschiebung von 3 Positionen vorschlägt. Also wird das Muster um max{4, 3 = 4 Positionen nach rechts verschoben. 48

57 4.5 Der Boyer-Moore-Horspool-Algorithmus bad character good suffix {{... w r i t t e n _ n o t i c e _ t h a t... s r e m i n i s c e n c e (a)... w r i t t e n _ n o t i c e _ t h a t... s + 4 r e m i n i s c e n c e (b)... w r i t t e n _ n o t i c e _ t h a t... s + 3 r e m i n i s c e n c e (c) ABBILDUNG 4.1: Verhalten des Boyer-Moore Algorithmus 4.5 Der Boyer-Moore-Horspool-Algorithmus Der BM-Algorithmus verdankt seine Schnelligkeit vor allem der bad-charakter Heuristik. Daher wurde 1980 von Horspool eine Vereinfachung des BM-Algorithmus vorgeschlagen: Die bad-charakter Heuristik wird derart modifiziert, dass sie immer eine positive Verschiebung vorschlägt. Damit wird die good-suffix Heuristik überflüssig (und auch die Vorverarbeitung einfacher). Der BMH-Algorithmus geht in den meisten Fällen analog zur bisherigen badcharacter Heuristik vor. Aber: Falls P [j] T [s + j] für ein j (0 j m 1) gilt, so wird s um m 1 k erhöht, wobei k der größte Index zwischen 0 und m 2 ist mit T [s+m 1] = P [k]. Wenn kein k (0 k m 2) mit T [s + m 1] = P [k] existiert, so wird s um m erhöht. Das Muster wird also um λ [ T [s+m 1] ] = min ({m { m 1 k 0 k m 2 und T [s+m 1] = P [k] ) verschoben. Wenn ein Match gefunden wurde, dann wird ebenfalls um λ [ T [s + m 1] ] verschoben. Die Funktion λ lässt sich wie folgt berechnen: computelastoccurrencefunction(p, Σ) 1 m length[p ] 2 for each character a Σ do 3 λ[a] = m 4 for j 0 to m 2 do 49

58 4 Algorithmen zur exakten Suche in Texten... g o l d e n _ f l e e c e _ o f... s r e m i n i s c e n c e... g o l d e n _ f l e e c e _ o f... s + 3 r e m i n i s c e n c e ABBILDUNG 4.2: Verhalten des BMH-Algorithmus bei einem Mismatch... g o l d e n _ f l e e c e _ o f... s f l e e c e... g o l d e n _ f l e e c e _ o f... s + 2 f l e e c e ABBILDUNG 4.3: Verhalten des BMH-Algorithmus bei einem Treffer 5 λ [ P [j] ] m 1 j 6 return λ Die worst-case-komplexität von computelastoccurrencefunction ist O ( Σ + m ). Der BMH-Algorithmus sieht in Pseudo-Code folgendermaßen aus: BMH-Matcher(T, P, Σ) 1 n length[t ] 2 m length[p ] 3 λ computelastoccurrencefunction(p, Σ) 4 s 0 5 while s n m do 6 j m 1 7 while j 0 and P [j] = T [s + j] do 8 j j 1 9 if j = 1 then 10 print Pattern occurs with shift s 11 s s + λ [ T [s + m 1] ] Zur Verifikation, ob eine Verschiebung s gültig ist, wird O(m) Zeit benötigt. Ins- 50

59 4.6 Der Knuth-Morris-Pratt-Algorithmus gesamt hat der BMH-Algorithmus die worst-case Zeitkomplexität O ( n m + Σ ) (z.b. für T = a n, P = a m ). 4.6 Der Knuth-Morris-Pratt-Algorithmus Einführendes Beispiel Der naive Algorithmus ist ineffizient, da unabhängig von bereits stattgefundenen erfolgreichen Vergleichen das Muster immer um ein Zeichen verschoben wird. Im folgenden Beispiel ist dies noch einmal anhand der Suche nach dem Wort abraxas im Text abrakadabra dargestellt. a b r a k a d a b r a a b r a x a s a b r a x a s a b r a x a s a b r a x a s Man erkennt, dass im zweiten und dritten Schritt das Zeichen a an Position 1 im Muster mit den Zeichen b und r an der zweiten und dritten Position des Textes verglichen wird, obwohl bereits nach dem positiven Vergleich von abra klar sein musste, dass hier keine Übereinstimmung existieren kann. Verwendet man dagegen Information aus vorangegangenen Vergleichen, so muss man nach einer Verschiebung mit den zeichenweisen Vergleichen zwischen Muster und Text nicht wieder am Anfang des Musters anfangen. Der Knuth-Morris-Pratt-Algorithmus geht nach folgendem Schema vor: Wenn ein Teilwort (Präfix) des Musters bereits erkannt wurde, aber dann ein mismatch auftritt, so ermittelt der Algorithmus das längste Präfix dieses Teilwortes, das gleichzeitig echtes Suffix davon ist, und schiebt das Muster dann so weit nach rechts, dass dieses Präfix an der bisherigen Position des Suffixes liegt. Beispiel Im obigen Beispiel ist das bereits erkannte Teilwort abra. Das längste Präfix von abra, das gleichzeitig echtes Suffix davon ist, ist a. Also schiebt der Algorithmus das Muster so, dass das erste a von abraxas an der Position steht, an der sich vorher das zweite a befunden hat. Der nächste stattfindende Vergleich ist der von k mit b, da die Verschiebung ja gerade so durchgeführt wurde, dass die beiden as übereinander liegen und somit nicht mehr verglichen werden müssen. 51

60 4 Algorithmen zur exakten Suche in Texten a b r a k a d a b r a a b r a x a s a b r a x a s Man erkennt, dass hier drei unnötige Vergleiche vermieden wurden Funktionsweise Beim KMP-Algorithmus wird das Muster P zeichenweise von links nach rechts mit dem Text T verglichen. Es werden Informationen über bereits stattgefundene Vergleiche ausgenutzt, um weitere unnötige Vergleiche zu vermeiden. Als Ausgangssituation sei P q ein Präfix von P, das an Position s im Text vorkommt.... P q... s P q Dann gehen wir gemäß der folgenden Fälle vor: (A) q = m, d.h. s ist gültige Verschiebung: 1. Bestimme k = π[m] = max{k : k < m und P k P m. 2.a) Falls k > 0, verschiebe das Muster nach rechts, so dass das Präfix P k von P unter dem Suffix P k von T [s... s + P 1] liegt.... P k... P k... P k... P k 2.b) Falls k = 0, verschiebe das Muster P um die Länge P

61 4.6 Der Knuth-Morris-Pratt-Algorithmus (B) q < m:... {{ P k a... P q P k {{ b P q a) a b und q 0: bestimme π[q] = max{k : k < q und P k P q, d.h. die Länge des längsten Präfixes von P, das auch Suffix von P q ist; verschiebe P nach rechts, so dass das Suffix P k von P q über dem Präfix P k von P liegt.... P k a... P k b b) a b und q = 0: c) a = b: verschiebe P um 1 nach rechts.... a... b mache weiter mit q P q a... P q a... P q+1... s P q+1 Beobachtung: Für jeden der vier Fälle gilt: das Muster wird nie nach links geschoben entweder wird ein neues Zeichen des Textes gelesen (B.c), oder das Muster wird um mindestens eine Position nach rechts geschoben (A), (B.a), (B.b). 53

62 4 Algorithmen zur exakten Suche in Texten Insgesamt können die Fälle also höchstens 2n-mal vorkommen. Wir bestimmen die worst-case Zeitkomplexität des KMP-Algorithmus: Fälle (B.b) und (B.c) erfordern jeweils konstanten Zeitaufwand. In (A) und (B.a) wird die Funktion π benötigt mit π[q] = max{k : k < q und P k P q, d.h. π[q] = Länge des längsten Präfixes von P, das ein echtes Suffix von P q ist. π hängt nur von P ab und kann daher in einem Vorverarbeitungsschritt (V V ) berechnet und als Tabelle abgespeichert werden. Gegeben diese Tabelle, erfordern auch (A) und (B.a) nur konstanten Zeitaufwand. Damit ergeben sich folgende Komplexitäten: O(V V )+O(n) Zeit und O(m) Speicherplatz im worst-case! Beispiel Die Präfixfunktion π : {1, 2,..., m {0, 1,..., m 1 für ein Muster P [0... m 1] ist definiert durch π[q] = max{k : k < q und P k P q, also die Länge des längsten Präfixes von P, das ein echtes Suffix von P q ist. Sie lautet für unser Beispielwort P = abrakadabra: q P [q] a b r a k a d a b r a π[q] Für q = 9 wäre das entsprechende Teilmuster also abrakadab. Das längste Präfix, das gleichzeitig echtes Suffix davon ist, ist ab. Es hat die Länge 2, darum findet sich in der Tabelle an der Position q = 9 der Eintrag π[q] = 2. Die Präfixfunktion π kann naiv folgendermaßen berechnet werden (die Laufzeit beträgt O(m 3 ), aber wir werden später sehen, wie es auch effizienter geht): computeprefixfunction(p ) 1 m length[p ] 2 π[1] 0 3 for q 2 to m do 4 k q 1 5 while k > 0 and P [0... k 1] P [0... q 1] do 6 k k 1 7 π[q] k 8 return π Damit können wir den KMP-Algorithmus in Pseudo-Code formulieren: KMP-Matcher(T, P ) 1 n length[t ] 2 m length[p ] 54

63 4.6 Der Knuth-Morris-Pratt-Algorithmus 3 π computeprefixfunction(p ) 4 q 0 5 for i 0 to n 1 do 6 while q > 0 and P [q] T [i] do 7 q π[q] 8 if P [q] = T [i] then 9 q q if q = m then 11 print Pattern occurs with shift i m q π[q] Wir gehen nun der Frage nach, wie die Funktion π effizienter berechnet werden kann. Unter der Annahme, dass die Werte von π für 1,..., q 1 schon berechnet sind, wollen wir π[q] berechnen. Gesucht: π[q] = max{k : k < q und P k P q Welche P k kommen in Frage? Alle P k = P [0... k 1] für die gilt: P {{ k 1 P {{ q 1 P [0...k 2] P [0...q 2] und P [k 1] = P [q 1], also alle echten Suffixe von P q 1, die sich zu einem echten Suffix von P q erweitern lassen. Die Menge der Längen aller echten Suffixe von P q 1, die auch Präfixe von P sind, ist: {k 1 : k 1 < q 1 : P k 1 P q 1 = {k : k < q 1 : P k P q 1. Damit muss gelten: π[q] = max{k + 1 : k < q 1, P k P q 1 und P [k ] = P [q 1]. Nun kann man folgende Gleichheit zeigen (Lemma 4.6.4): {k : k < q 1 : P k P q 1 = {π[q 1], π [ π[q 1] ], π 3 [q 1],..., π t [q 1], {{ =π 2 [q 1] wobei π t [q 1] = 0 gilt. Schließlich erhalten wir: π[q] = max{k + 1 : k {π[q 1],..., π t [q 1] und P [k ] = P [q 1]. Damit erhalten wir folgendes Verfahren, um π[q] zu berechnen: Wir schauen π[q 1] in der Tabelle von π nach und prüfen, ob sich das echte Suffix P π[q 1] von P q 1 zu einem echten Suffix von P q erweitern lässt. Wenn ja, so ist π[q] = 55

64 4 Algorithmen zur exakten Suche in Texten π[q 1]+1. Wenn nein, so iterieren wir diesen Prozess, d.h. wir machen das Gleiche mit π(π[q 1]) usw. Diese Überlegungen sollten die folgende Berechnungsmethode der Funktion π motivieren. Wir werden deren Korrektheit im nächsten Unterabschnitt beweisen. computeprefixfunction2(p ) 1 m length[p ] 2 π[1] 0 3 k 0 4 for q 2 to m do 5 while k > 0 and P [k] P [q 1] do 6 k π[k] 7 if P [k] = P [q 1] then 8 k k π[q] k 10 return π Die Laufzeit von computeprefixfunction2 beträgt O(m). Dies kann man folgendermaßen sehen: 1. Die for-schleife in Zeile 4 wird m 1-mal durchlaufen, demzufolge auch die Zeilen Der Wert von k ist anfangs 0 und wird insgesamt höchstens m 1-mal in Zeile 8 um jeweils 1 erhöht. 3. Bei jedem Durchlauf der while-schleife in den Zeilen 5 und 6 wird der Wert von k um mindestens 1 erniedrigt. Da k nicht negativ wird (Zeile 5), kann die while-schleife insgesamt also höchstens k-mal durchlaufen werden Korrektheit der Berechnung der Präfixfunktion Das wesentliche Lemma im Korrektheitsbeweis besagt, dass man alle Präfixe P k, die Suffix eines gegebenen Präfixes P q sind, berechnen kann, indem man die Präfixfunktion π iteriert. Definition Es sei π [q] = { q, π[q], π 2 [q], π 3 [q],..., π t [q] wobei π 0 [q] = q und π i+1 [q] = π [ π i [q] ] für i 0; dabei sei vereinbart, dass die Folge in π [q] abbricht, wenn π t [q] = 0 erreicht wird. Lemma Es sei P [0... m 1] ein Muster und π die dazu gehörige Präfixfunktion. Dann gilt π [q] = {k : P k P q für q = 1,..., m. 56

65 4.6 Der Knuth-Morris-Pratt-Algorithmus Beweis: Wir beweisen das Lemma, indem wir (1) π [q] {k : P k P q und (2) {k : P k P q π [q] zeigen. 1. Wir zeigen: i π [q] impliziert P i P q. Sei also i π [q]. Für i gibt es ein u N mit i = π u [q]. Wir beweisen P π u [q] P q durch Induktion über u. Induktionsanfang: u = 0. Dann ist i = π 0 [q] = q und die Behauptung gilt. Induktionshypothese: Für i 0 = π u 0 [q] gilt P i0 P q. Induktionsschritt: Zu zeigen ist: für i = π u0+1 [q] gilt P i P q. Es gilt i = π [ π u 0 [q] ] = π[i 0 ]. Mit der Induktionshypothese folgt P i0 P q. Weiterhin gilt i = π[i 0 ] = max{k : k < i 0 und P k P i0, also gilt P i P i0 P q. Da transitiv ist, folgt P i P q. 2. Wir beweisen {k : P k P q π [q] durch Widerspruch. Wir nehmen an j {k : P k P q \π [q]. Ohne Einschränkung der Allgemeinheit sei j die größte Zahl mit dieser Eigenschaft. Da j q und q {k : P k P q π [q] gilt, folgt j < q. Es sei j die kleinste Zahl in π [q], die größer als j ist (da q π [q] gilt, existiert solch ein j immer). Wegen j {k : P k P q gilt P j P q. Weiterhin impliziert j π [q] wg. (1) P j P q. Mit j < j folgt damit P j P j. Es gibt kein j, j < j < j, mit P j P j P j Gäbe es solch ein j, so müsste j π [q] gelten, denn j ist die größte Zahl mit j {k : P k P q \π [q]. j π [q] kann aber nicht gelten, denn j ist die kleinste Zahl in π [q], die größer als j ist. Also muss j = max{k : k < j und P k P j = π[j ] und damit j π [q] gelten. Dieser Widerspruch beweist {k : P k P q π [q]. Definition Für q = 2, 3,..., m sei E q 1 = { k : k < q 1, k π [q 1], P [k] = P [q 1]. Die Menge E q 1 π [q 1] enthält alle mit k < q 1 und P k+1 P q ; denn aus k π [q 1] = {k : P k P q 1 P k = P [0... k 1] P q 1 = P [0... q 2] und P [k] = P [q 1] folgt P k+1 = P [0... k] P q = P [0... q 1]). 57

66 4 Algorithmen zur exakten Suche in Texten Lemma Sei P [0... m 1] ein Muster und π die zugehörige Präfixfunktion. Für q = 2, 3,..., m gilt: π[q] = { max{k E q 1 falls E q 1 = falls E q 1 Beweis: Mit der Vereinbarung max{ = 0 gilt: π[q] = max { k : k < q und P k P q = max { k : k < q, P k 1 P q 1 und P [k 1] = P [q 1] (4.6.4) = max { k : k 1 < q 1, k 1 π [q 1] und P [k 1] = P [q 1] k =k 1 = max { k + 1 : k < q 1, k π [q 1] und P [k ] = P [q 1] = max { k + 1 : k E q 1 Fall 1: E q 1 = : Dann gilt π[q] = max{ = 0. Fall 2: E q 1 : Dann gilt π[q] = max{1 + k : k E q 1 = 1 + max{k : k E q 1. Satz computeprefixfunction2(p) berechnet die Funktion π korrekt. Beweis: Am Anfang jeder Iteration der for-schleife gilt k = π[q 1], denn beim ersten Betreten der for-schleife gilt k = 0, q = 2 und π[q 1] = 0 = k und diese Eigenschaft bleibt wegen Zeile 9 bei jeder Iteration der for-schleife erhalten. Die Eigenschaft k = π[q 1] wird daher auch Invariante der Schleife genannt. Die while-schleife untersucht alle Werte k π [q 1]\{q 1, bis einer mit P [k] = P [q 1] gefunden wird; an diesem Punkt gilt k = max{k E q 1, so dass π[q] den Wert k + 1 = 1 + max{k E q 1 erhält. Wird kein solcher Wert k gefunden, so wird π[q] der Wert 0 zugewiesen, denn es gilt k = 0 in den Zeilen 7-9. Das in Zeile 10 zurückgegebene Feld π hat also folgende Werte: π[1] = 0 und für q = 2, 3,..., m π[q] = { 0 falls Eq 1 = 1 + max{k E q 1 falls E q 1 58

67 4.7 Aufgaben 4.7 Aufgaben Aufgabe Beweisen Sie Lemma Aufgabe Implementieren Sie den Boyer-Moore-Horspool-Algorithmus in Java. Aufgabe Bestimmen Sie die worst-case Zeiteffizienz der naiven Implementierung von computeprefixfunction. Aufgabe Implementieren Sie den Knuth-Morris-Pratt-Algorithmus in Java. Aufgabe Geben Sie konkrete Beispiele an, in denen (a) der Knuth-Morris-Pratt-Algorithmus (b) der Boyer-Moore-Horspool-Algorithmus (c) der Boyer-Moore-Algorithmus sich besser verhält als die beiden anderen (unter Vernachlässigung der Vorverarbeitung). Das Kriterium ist hierbei die Anzahl der Vergleiche von Zeichen, die jeweils durchgeführt werden müssen. Aufgabe Man erreicht eine verbesserte Version des Knuth-Morris-Pratt- Matchers, indem man π in der Zeile 7 (nicht aber in Zeile 12) durch π ersetzt. π ist rekursiv für q = 1, 2,..., m 1 definiert: 0 wenn π[q] = 0, π [q] = π [π[q]] wenn π[q] 0 und P [π[q]] = P [q], π[q] wenn π[q] 0 und P [π[q]] P [q]. Erklären Sie, warum der modifizierte Algorithmus korrekt ist und inwiefern diese Modifikation eine Verbesserung darstellt. Stellen Sie die Funktion π für das Muster P = ababababca tabellarisch dar. Aufgabe Geben Sie einen Algorithmus an, der herausfindet, ob ein Text T eine zyklische Verschiebung eines Textes T ist. Zum Beispiel ist der Text reif eine zyklische Verschiebung von frei. Der Algorithmus soll linear im Zeitaufwand sein. Implementieren Sie Ihren Algorithmus in Java. Aufgabe Erklären Sie, wie man alle Vorkommen des Musters P im Text T unter Zuhilfenahme der π Funktion für die Zeichenkette P T bestimmen kann. P T ist die Konkatenation von P und T (also m+n Zeichen lang). Implementieren Sie Ihren Vorschlag in Java. 59

68 4 Algorithmen zur exakten Suche in Texten 60

69 5 Objektorientierte Programmierung in Java Grundzüge der objektorientierten Programmierung haben wir bereits in Kapitel 2 kennengelernt, auch Teile der entsprechenden Java- Syntax. Dieses Kapitel soll nun etwas systematischer und ausführlicher noch einmal die entsprechenden Java-Konstrukte zur Umsetzung objektorientierter Programmierung sowie Ausnahmen und Spezialfälle behandeln. Beginnen wollen wir jedoch mit einem Blick in die Historie, denn die Objektorientierung ist nur der (derzeitige) Schlusspunkt einer längeren Entwicklung. 5.1 Traditionelle Konzepte der Softwaretechnik Folgende traditionelle Konzepte des Software-Engineering werden u.a. im objektorientierten Ansatz verwendet: Datenabstraktion (bzw. Datenkapselung) und Information Hiding Die zentrale Idee der Datenkapselung ist, dass auf eine Datenstruktur nicht direkt zugegriffen wird, indem etwa einzelne Komponenten gelesen oder geändert werden, sondern, dass dieser Zugriff ausschließlich über Zugriffsoperatoren erfolgt. Es werden also die Implementierungen der Operationen und die Datenstrukturen selbst versteckt. Vorteil: Implementierungdetails können beliebig geändert werden, ohne Auswirkung auf den Rest des Programmes zu haben. abstrakte Datentypen (ADT) Realisiert wird die Datenabstraktion duch den Einsatz abstrakter Datentypen, die Liskov & Zilles (1974) folgendermaßen definierten: An abstract data type defines a class of abstract objects which is completely characterized by the operations available on those objects. This means that an abstract data type can be defined by defining the characterizing operations for that type. 61

70 5 Objektorientierte Programmierung in Java Oder etwas prägnanter: Datentyp = Menge(n) von Werten + Operationen darauf abstrakter Datentyp = Operationen auf Werten, deren Repräsentation nicht bekannt ist. Der Zugriff erfolgt ausschließlich über Operatoren. Datenabstraktion fördert die Wiederverwendbarkeit von Programmteilen und die Wartbarkeit großer Programme Beispiel: Der ADT Stack Stack: Eine Datenstruktur über einem Datentyp T bezeichnet man als Stack 1, wenn die Einträge der Datenstruktur als Folge organisiert sind und es die Operationen push, pop und peek gibt: push fügt ein Element von T stets an das Ende der Folge. pop entfernt stets das letzte Element der Folge. peek liefert das letzte Element der Folge, ohne sie zu verändern. Prinzip: last in first out (LIFO) Typen der Operationen: initstack: Stack push: T Stack Stack pop: Stack Stack peek: Stack T empty: Stack boolean Spezifikation der Operationen durch Gleichungen. Sei x eine Variable vom Typ T, stack eine Variable vom Typ Stack: empty (initstack) empty (push (x, stack)) peek (push (x, stack)) pop (push (x, stack)) = true = false = x = stack initstack und push sind Konstruktoren (sie konstruieren Terme), daher gibt es keine Gleichungen für sie. 5.2 Konzepte der objektorientierten Programmierung Ziel jeglicher Programmierung ist: Modellierung von Ausschnitten der Realität 1 bedeutet soviel wie Keller oder Stapel 62

71 5.2 Konzepte der objektorientierten Programmierung sachgerechte Abstraktion realitätsnahes Verhalten Nachbildung von Ähnlichkeit im Verhalten Klassifikation von Problemen Je nach Problem können verschiedene Klassifikationen sachgerecht sein, dies ist anhand eines Beispiels aus der Biologie in der Abbildung 5.1 dargestellt. Tiere Insekten Fische Säugetiere Tiere Zuchttiere Wild Störtiere ABBILDUNG 5.1: Phylogenetische (oben) und ökonomische Klassifizierung (unten). Es werden immer bestimmte Funktionen auf bestimmte Daten angewendet. Soll nun die Architektur eines Systems (Modells) auf den Daten oder auf den Funktionen aufbauen? Grundsätzlich gibt es drei Vorgehensweisen: 1. die funktionsorientierte 2. die datenorientierte 3. die objektorientierte 63

72 5 Objektorientierte Programmierung in Java Der Kerngedanke des objektorientierten Ansatzes besteht darin, Daten und Funktionen zu verschmelzen. Im ersten Schritt werden die Daten abgeleitet, im zweiten Schritt werden den Daten die Funktionen zugeordnet, die sie manipulieren. Die entstehenden Einheiten aus Daten und Funktionen werden Objekte genannt. Wir schränken den Begriff Objektorientierung gemäß folgender Gleichung von Coad & Yourdon weiter ein: Objektorientierung = Klassen und Objekte + Kommunikation mit Nachrichten + Vererbung Im folgenden erläutern wir diese Konzepte kurz. 5.3 Klassen und Objekte Eine Klasse besteht konzeptionell aus einer Schnittstelle und einem Rumpf. In der Schnittstelle sind die nach außen zur Verfügung gestellten Methoden (und manchmal auch öffentlich zugängliche Daten), sowie deren Semantik aufgelistet. Diese Auflistung wird oft als Vertrag oder Nutzungsvorschrift zwischen dem Entwerfer der Klasse und dem sie verwendenen Programmierer gedeutet. Der Klassenrumpf enthält alle von außen unsichtbaren Implementierungdetails. Historisch gesehen ist der Klassenbegriff älter als der Begriff des abstrakten Datentypen (ADT). In der Programmiersprache Simula 67 gab es bereits Klassen als Mechanismus zur Datenkapselung (Abstakte Datentypen wurden erstmals 1974 von Liskov & Zilles definiert). Der Kerngedanke der Objektorientierung, Daten und Funktionen konsequent als Objekte zusammenzufassen, wird jedoch auf die Programmiersprache Smalltalk zurückgeführt (entwickelt seit Beginn der 70er Jahre). 5.4 Kommunikation mit Nachrichten Objekte besitzen die Möglichkeit, mit Hilfe ihrer Methoden Aktionen auszuführen. Das Senden einer Nachricht stößt die Ausführung einer Methode an. Eine Nachricht besteht aus einem Empfänger (das Objekt, das die Aktionen ausführen soll), einem Selektor (die Methode, deren Aktionen auszuführen sind) und gegebenenfalls aus Argumenten (Werte, auf die während der Ausführung der Aktion zugegriffen wird). 64

73 5.5 Vererbung 5.5 Vererbung Gleichartige Objekte werden zu Klassen zusammengefasst. Häufig besitzen Objekte zwar bestimmte Gemeinsamkeiten, sind aber nicht völlig gleichartig. Um solche Ähnlichkeiten auszudrücken, ist es möglich, zwischen Klassen Vererbungsbeziehungen festzulegen. Dazu wird das Verhalten einer existierenden Klasse erweitert. Die Erweiterung erzeugt eine von ihr alle Attribute und Methoden erbende neue Klasse, die um weitere Attribute und Methoden ergänzt wird. Die neue Klasse wird Unterklasse, die ursprüngliche Klasse Oberklasse genannt. Gemeinsamkeiten: Unterschiede: in der Oberklasse in der Unterklasse Eine Unterklasse kann auch von der Oberklasse ererbte Methoden redefinieren (überschreiben). Wir sprechen von Einfachvererbung, wenn jede neue Klasse genau eine Oberklasse erweitert (Abbildung 5.2). Object System Math Point... ABBILDUNG 5.2: Einfachvererbung (Java) Tiere... Pflanzen Fleischfresser... ABBILDUNG 5.3: Mehrfachvererbung 65

74 5 Objektorientierte Programmierung in Java Wenn eine Klasse mehrere Oberklassen besitzen kann, sprechen wir von Mehrfachvererbung (Abbildung 5.3). In Java gibt es nur Einfachvererbung (aus gutem Grund). Die einzige Klasse, die keine Oberklasse erweitert, ist die vordefinierte Klasse Object. Klassen, die nicht explizit andere Klassen erweitern, erweitern implizit die Klasse Object. Alle Objektreferenzen sind in polymorpher Weise von der Klasse Object, so dass Object die generische Klasse für Referenzen ist, die sich auf Objekte jeder beliebigen Klasse beziehen können. Das nächste Beispiel verdeutlicht dies. Object oref = new Point(); oref = "eine Zeichenkette"; 5.6 Konstruktoren und Initialisierungsblöcke Einem neu erzeugten Objekt wird ein Anfangszustand zugewiesen. Datenfelder können bei ihrer Deklaration mit einem Wert initialisiert werden, was manchmal ausreicht, um einen sinnvollen Anfangszustand sicherzustellen. Oft ist aber mehr als nur einfache Dateninitialisierung zur Erzeugung eines Anfangszustands nötig; der erzeugende Code muss vielleicht Anfangsdaten liefern oder Operationen ausführen, die nicht als einfache Zuweisungen ausgedrückt werden können. Um mehr als einfache Initialisierungen bewerkstelligen zu können, können Klassen Konstruktoren enthalten. Konstruktoren sind keine Methoden, aber methodenähnlich: Sie haben denselben Namen wie die von ihnen initialisierte Klasse, haben keine oder mehrere Parameter und keinen Rückgabetyp. Bei der Erzeugung eines Objekts mit new werden eventuelle Parameterwerte nach dem Klassennamen in einem Klammernpaar angegeben. Bei der Objekterzeugung werden zuerst den Instanzvariablen ihre voreingestellten Anfangswerte zugewiesen, dann ihre Initialisierungsausdrücke berechnet und zugewiesen und dann der Konstruktor aufgerufen. Im folgenden benutzen wir die Klasse Circle als Standardbeispiel. Ein Kreis besteht aus einer x-koordinate, einer y-koordinate sowie dem Radius r. Desweiteren wird die Anzahl der erzeugten Kreise gezählt durch die Anweisung numcircles++;, die bei jedem Aufruf des parameterlosen Konstruktors ausgeführt wird. public class Circle { int x=0, y=0, r=1; static int numcircles=0; public Circle() { numcircles++; 66

75 5.6 Konstruktoren und Initialisierungsblöcke public double circumference() { return 2*Math.PI*r; public double area() { return Math.PI*r*r; public static void main(string[] args) { Circle c = new Circle(); System.out.println(c.r); System.out.println(c.circumference()); System.out.println(c.area()); System.out.println(numCircles); Statt des parameterlosen Konstruktors hätten wir in der Klasse auch einen Konstruktor mit drei Parametern definieren können, der nicht nur Einheitskreise erzeugen kann: public Circle(int xcoord, int ycoord, int radius) { numcircles++; x = xcoord; y = ycoord; r = radius; Standardmäßig benennt man die Parametervariablen im Konstruktor genauso wie die Variablen in der Klasse. Da aber hierbei Namenskonflikte entstehen, muss man die Variable des Objektes mit this.variable referenzieren. public Circle(int x, int y, int r) { numcircles++; this.x = x; this.y = y; this.r = r; Für eine Klasse kann es in Java auch mehrere Konstruktoren geben. Diese müssen sich allerdings in der Anzahl der Attribute bzw. deren Typen unterscheiden. Dies nennt man Überladen von Konstruktoren. In der folgenden Klasse gibt es drei Konstruktoren namens Circle. Die Konstruktoren mit Parametern rufen den 67

76 5 Objektorientierte Programmierung in Java parameterlosen Konstruktor mittels this() auf. Dies hat den Vorteil, dass Änderungen an den Konstruktoren nicht an drei Stellen gemacht werden müssen (was fehleranfällig ist), sondern nur im parameterlosen Konstruktor. Um die Anzahl der erzeugten Kreise zu zählen, muss man die Programmzeile numcircles++; nur dem parameterlosen Konstruktor hinzufügen. public class Circle { int x = 0, y = 0, r = 1; static int numcircles; public Circle() { numcircles++; public Circle(int x, int y, int r) { this(); this.x = x; this.y = y; this.r = r; public Circle(int r) { this(0,0,r); public static void main(string[] args) { Circle c1 = new Circle(); Circle c2 = new Circle(1,1,2); Circle c3 = new Circle(3); System.out.println(numCircles); Klassenvariablen werden initialisiert, wenn die Klasse das erste Mal geladen wird. Das Analogon zu Konstruktoren, um komplexe Initialisierungen von Klassenvariablen durchzuführen, sind die sogenannten Initialisierungsblöcke. Diese Blöcke werden durch static {... umschlossen, wie folgendes Beispiel demonstriert. Beispiel (Flanagan [5], S. 59) public class Circle { public static double[] sines = new double[1000]; public static double[] cosines = new double[1000]; 68

77 5.7 Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen static { double x, delta_x; int i; delta_x = (Math.PI/2)/(1000-1); for(i=0,x=0; i<1000; i++,x+=delta_x) { sines[i] = Math.sin(x); cosines[i] = Math.cos(x); Es können mehrere klassenbezogene Initialisierungsblöcke in einer Klasse enthalten sein. Die Klasseninitialisierung erfolgt von links nach rechts und von oben nach unten. 5.7 Java-Klassen als Realisierung und Implementierung von abstrakten Datentypen Durch den Modifizierer private können wir Implementierungsdetails verstecken, denn als private deklarierte Attribute und Methoden sind nur in der Klasse selbst zugreifbar 2. Folgende Klasse implementiert einen ADT Stack mittels eines Feldes: public class Stack { private Object[] stack; private int top = -1; private static final int CAPACITY = 10000; /** liefert einen leeren Keller. */ public Stack() { stack = new Object[CAPACITY]; /** legt ein Objekt im Keller ab und liefert dieses Objekt zusaetzlich zurueck. */ public Object push(object item) { stack[++top] = item; return item; 2 Synonyme für Zugreifbarkeit sind: Gültigkeit bzw. Sichtbarkeit. 69

78 5 Objektorientierte Programmierung in Java /** entfernt das oberste Objekt vom Keller und liefert es zurueck. Bei leerem Keller wird eine Fehlermeldung ausgegeben und null zurueckgeliefert. */ public Object pop() { if (empty()) { System.out.println("Method pop: empty stack"); return null; else return stack[top--]; /** liefert das oberste Objekt des Kellers, ohne ihn zu veraendern. Bei leerem Keller wird eine Fehlermeldung ausgegeben und null zurueckgeliefert. */ public Object peek() { if (empty()) { System.out.println("Method peek: empty stack"); return null; else return stack[top]; /** liefert true genau dann, wenn der Keller leer ist. */ public boolean empty() { return (top == -1); /** liefert die Anzahl der Elemente des Kellers. */ public int size() { return top+1; Der Dokumentationskommentar /**... */ wird zur automatischen Dokumentierung der Attribute und Methoden einer Klasse benutzt. Das Programm javadoc generiert ein HTML-File, in dem alle sichtbaren Attribute und Methoden mit de- 70

79 5.8 Methoden in Java ren Parameterlisten aufgezeigt und dokumentiert sind. > javadoc Stack.java Dieses HTML-File ist der Vertrag (die Schnittstelle) der Klasse und entspricht dem ADT Stack, wobei die Operationen bzw. Methoden allerdings nur natürlichsprachlich spezifiziert wurden. Die obige verbale Spezifikation entspricht weitgehend der der vordefinierten Java-Klasse Stack (genauer java.util.stack). Man beachte, dass (aus diesem Grund) die obige Spezifikation von der Gleichungsspezifikation aus dem Unterabschnitt abweicht. 5.8 Methoden in Java Methoden können wie Konstruktoren überladen werden. In Java besitzt jede Methode eine Signatur, die ihren Namen sowie die Anzahl und Typen der Parameter definiert. Zwei Methoden können denselben Namen haben, wenn ihre Signaturen unterschiedliche Anzahlen oder Typen von Parametern aufweisen; dies wird als Überladen von Methoden bezeichnet. Wird eine Methode aufgerufen, vergleicht der Übersetzer die Anzahl und die Typen der Parameter mit den verfügbaren Signaturen, um die passende Methode zu finden. Die Parameterübergabe zu Methoden erfolgt in Java durch Wertübergabe (call by value). D.h., dass Werte von Parametervariablen in einer Methode Kopien der vom Aufrufer angegebenen Werte sind. Das nächste Beispiel verdeutlicht dies. public class CallByValue { public static int sqr(int i) { i = i*i; return(i); public static void main(string[] args) { int i = 3; System.out.println(sqr(i)); System.out.println(i); > java CallByValue

80 5 Objektorientierte Programmierung in Java Allerdings ist zu beachten, dass nicht Objekte, sondern Objektreferenzen übergeben werden. Wir betrachten unser Standardbeispiel Circle in folgender abgespeckter Form (gemäß der Devise, Implementierungsdetails zu verbergen, werden die Datenfelder als private deklariert). public class Circle { private int x,y,r; public Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; public double circumference() { return 2 * Math.PI * r; public double area() { return Math.PI * r * r; public static void settozero (Circle arg) { arg.r = 0; arg = null; public static void main(string[] args) { Circle kreis = new Circle(10,10,1); System.out.println("vorher : r = "+kreis.r); settozero(kreis); System.out.println("nachher: r = "+kreis.r); > java Circle vorher : r = 1 nachher: r = 0 Dieses Verhalten entspricht jedoch nicht der Parameterübergabe call by reference, denn bei der Wertübergabe wird eine Kopie der Referenz erzeugt und die 72

81 5.9 Unterklassen und Vererbung in Java ursprüngliche Referenz bleibt erhalten. Bei call by reference würde die übergebene Referenz eben nicht kopiert und daher in der Methode settozero auf null gesetzt. 5.9 Unterklassen und Vererbung in Java Wir wollen die Klasse Circle so erweitern, dass wir deren Instanzen auch graphisch darstellen können. Da ein solcher graphischer Kreis ein Kreis ist (es herrscht eine ist-ein Beziehung), erweitern wir die Klasse Circle zu der neuen Klasse GraphicCircle 3. Durch das Schlüsselwort extends wird GraphicCircle eine Unterklasse von Circle. Wir sagen auch GraphicCircle erweitert die (Ober- )Klasse Circle. Damit erbt die Klasse GraphicCircle alle Attribute und Methoden von Circle, nur die als private deklarierten sind nicht über ihren Namen zugreifbar. Damit ist unsere Entscheidung, die Attribute x, y und r privat zu halten, nicht mehr sinnvoll. Um diese Attribute dennoch vor unerwünschten Zugriffen zu schützen, werden sie als protected deklariert. Damit sind sie zugreifbar für Unterklassen und werden an diese vererbt, in anderen Klassen sind sie nicht zugreifbar 4. import java.awt.color; import java.awt.graphics; public class GraphicCircle extends Circle { protected Color outline; // Farbe der Umrandung protected Color fill; // Farbe des Inneren public GraphicCircle(int x,int y,int r,color outline) { super(x,y,r); this.outline = outline; this.fill = Color.lightGray; public GraphicCircle(int x,int y,int r,color outline,color fill) { this(x,y,r,outline); this.fill = fill; public void draw(graphics g) { 3 Nur wenn eine solche ist-ein Beziehung herrscht, ist eine Erweiterung sinnvoll. Beispielsweise wäre eine Erweiterung der Klasse Circle zu einer Klasse Ellipse ein Design-Fehler, da eine Ellipse kein Kreis ist. Umgekehrt wäre dieses sinniger, da ein Kreis eine Ellipse ist. 4 Es sei denn, die Klasse befindet sich im selben Paket (siehe Abschnitt 2.4.2)! 73

82 5 Objektorientierte Programmierung in Java g.setcolor(outline); g.drawoval(x-r, y-r, 2*r, 2*r); g.setcolor(fill); g.filloval(x-r, y-r, 2*r, 2*r); public static void main(string[] args) { GraphicCircle gc = new GraphicCircle(0,0,100,Color.red,Color.blue); double area = gc.area(); System.out.println(area); Circle c = gc; double circumference = c.circumference(); System.out.println(circumference); GraphicCircle gc1 = (GraphicCircle) c; Color color = gc1.fill; System.out.println(color); Color und Graphics sind vordefinierte Klassen, die durch import zugreifbar gemacht werden (vgl. Abschnitt 2.4.2). Diese Klassen werden z.b. in [5] beschrieben. Zum Verständnis reicht es hier zu wissen, dass der erste Konstruktor den Konstruktor seiner Oberklasse aufruft (vgl. Abschnitt 5.11) und das Kreisinnere die Farbe hellgrau erhält, sowie, dass die Methode draw einen farbigen Kreis zeichnet. Da GraphicCircle alle Methoden von Circle erbt, können wir z.b. den Flächeninhalt eines Objektes gc vom Typ GraphicCircle berechnen durch: double area = gc.area(); Jedes Objekt gc vom Typ GraphicCircle ist ebenfalls ein Objekt vom Typ Circle bzw. vom Typ Object. Deshalb sind folgende Zuweisungen korrekt. Circle c = gc; double area = c.area(); Man kann c durch casting 5 in ein Objekt vom Typ GraphicCircle zurückverwandeln. GraphicCircle gc1 = (GraphicCircle)c; Color color = gc1.fill; Die oben gezeigte Typumwandlung funktioniert nur, weil c tatsächlich ein Objekt vom Typ GraphicCircle ist. 5 explizite Typumwandlung 74

83 5.10 Überschreiben von Methoden und Verdecken von Datenfeldern 5.10 Überschreiben von Methoden und Verdecken von Datenfeldern Wir betrachten folgendes Java-Programm (Arnold & Gosling [1], S. 66): public class SuperShow { public String str = "SuperStr"; public void show() { System.out.println("Super.show: "+str); public class ExtendShow extends SuperShow { public String str = "ExtendStr"; public void show() { System.out.println("Extend.show: "+str); public static void main(string[] args) { ExtendShow ext = new ExtendShow(); SuperShow sup = ext; sup.show(); ext.show(); System.out.println("sup.str = "+sup.str); System.out.println("ext.str = "+ext.str); Verdecken von Datenfeldern Jedes ExtendShow-Objekt hat zwei String-Variablen, die beide str heißen und von denen eine ererbt wurde. Die neue Variable str verdeckt die ererbte; wir sagen auch die ererbte ist verborgen. Sie existiert zwar, man kann aber nicht mehr durch Angabe ihres Namens auf sie zugreifen. Überschreiben von Methoden Die Methode show() der Klasse ExtendShow überschreibt die gleichnamige Methode der Oberklasse. Dies bedeutet, dass die Implementierung der Methode der Oberklasse durch eine neue Implementierung der Unterklasse ersetzt wird. Dabei 75

84 5 Objektorientierte Programmierung in Java müssen Signatur und Rückgabetyp dieselben sein. Überschreibende Methoden besitzen ihre eigenen Zugriffsangaben. Eine in der Oberklasse als protected deklarierte Methode kann wieder als protected redeklariert werden 6, oder sie wird mit dem Modifizierer public erweitert. Der Gültigkeitsbereich kann aber nicht z.b durch private eingeschränkt werden. (Eine Begründung dafür findet man in Arnold & Gosling [1], S. 66.) Wenn eine Methode von einem Objekt aufgerufen wird, dann bestimmt immer der tatsächliche Typ des Objektes, welche Implementierung benutzt wird. Bei einem Zugriff auf ein Datenfeld wird jedoch der deklarierte Typ der Referenz verwendet. Daher erhalten wir folgende Ausgabe beim Aufruf der main-methode: > java ExtendShow Extend.show: ExtendStr Extend.show: ExtendStr sup.str = SuperStr ext.str = ExtendStr Die Objektreferenz super Das Schlüsselwort super kann in allen objektbezogenen Methoden und Konstruktoren verwendet werden. In Datenfeldzugriffen und Methodenaufrufen stellt es eine Referenz zum aktuellen Objekt als eine Instanz seiner Oberklasse dar. Wenn super verwendet wird, so bestimmt der Typ der Referenz über die Auswahl der zu verwendenden Methodenimplementierung. Wir illustrieren dies wieder an einem Beispielprogramm. public class T1 { protected int x = 1; protected String s() { return "T1"; public class T2 extends T1 { protected int x = 2; protected String s() { return "T2"; protected void test() { System.out.println("x= "+x); 6 Dies ist die übliche Vorgehensweise. 76

85 5.11 Konstruktoren in Unterklassen System.out.println("super.x= "+super.x); System.out.println("((T1)this).x= "+((T1)this).x); System.out.println("s(): "+s()); System.out.println("super.s(): "+super.s()); System.out.println("((T1)this).s(): "+((T1)this).s()); public static void main(string[] args) { new T2().test(); > java T2 x= 2 super.x= 1 ((T1)this).x= 1 s(): T2 super.s(): T1 ((T1)this).s(): T Konstruktoren in Unterklassen In Konstruktoren der Unterklasse kann direkt einer der Oberklassenkonstruktoren mittels des super() Konstruktes aufgerufen werden. Achtung: Der super-aufruf muss die erste Anweisung des Konstruktors sein! Wird kein Oberklassenkonstruktor explizit aufgerufen, so wird der parameterlose Konstruktor der Oberklasse automatisch aufgerufen, bevor die Anweisungen des neuen Konstruktors ausgeführt werden. Verfügt die Oberklasse nicht über einen parameterlosen Konstruktor, so muss ein Konstruktor der Oberklasse explizit mit Parametern aufgerufen werden, da es sonst einen Fehler bei der Übersetzung gibt. Ausnahme: Wird in der ersten Anweisung eines Konstruktors ein anderer Konstruktor derselben Klasse mittels this aufgerufen, so wird nicht automatisch der parameterlose Oberklassenkonstruktor aufgerufen. Java liefert einen voreingestellten parameterlosen Konstruktor für eine erweiternde Klasse, die keinen Konstruktor enthält. Dieser ist äquivalent zu: 77

86 5 Objektorientierte Programmierung in Java public class ExtendedClass extends SimpleClass { public ExtendedClass () { super(); Der voreingestellte Konstruktor hat dieselbe Sichtbarkeit wie seine Klasse. Ausnahme: Enthält die Oberklasse keinen parameterlosen Konstruktor, so muss die Unterklasse mindestens einen Konstruktor bereitstellen Reihenfolgeabhängigkeit von Konstruktoren Wird ein Objekt erzeugt, so werden zuerst alle seine Datenfelder auf voreingestellte Werte initialisiert. Jeder Konstruktor durchläuft dann drei Phasen: Aufruf des Konstruktors der Oberklasse. Initialisierung der Datenfelder mittels der Initialisierungsausdrücke. Ausführung des Rumpfes des Konstruktors. Beispiel public class X { protected String infix = "fel"; protected String suffix; protected String alles; public X() { suffix = infix; alles = verbinde("ap"); public String verbinde(string original) { return (original+suffix); public class Y extends X { protected String extra = "d"; public Y() { suffix = suffix+extra; 78

87 5.13 Abstrakte Klassen und Methoden alles = verbinde("biele"); public static void main(string[] args) { new Y(); Die Reihenfolge der Phasen ist ein wichtiger Punkt, wenn während des Aufbaus Methoden aufgerufen werden (wie im obigen Beispiel). Wenn man eine Methode aufruft, erhält man immer die Implementierung dieser Methode für den derzeitigen Objekttyp. Verwendet die Methode Datenfelder des derzeitigen Typs, dann sind diese vielleicht noch nicht initialisiert worden. Die folgende Tabelle zeigt die Inhalte der Datenfelder beim Aufruf der main-methode (d.h. des Y- Konstruktors). Schritt Aktion infix extra suffix alles 0 Datenfelder auf Voreinstellungen 1 Y-Konstruktor aufgerufen 2 X-Konstruktor aufgerufen 3 X-Datenfeld initialisiert fel 4 X-Konstruktor ausgeführt fel fel Apfel 5 Y-Datenfeld initialisiert fel d fel Apfel 6 Y-Konstruktor ausgeführt fel d feld Bielefeld Die während des Objektaufbaus aufgerufenen Methoden sollten unter Beachtung dieser Faktoren entworfen werden. Auch sollte man alle vom Konstruktor aufgerufenen Methoden sorgfältig dokumentieren, um diejenigen, die den Konstruktor überschreiben möchten, von den potentiellen Einschränkungen in Kenntnis zu setzen Abstrakte Klassen und Methoden Ein sehr nützliches Merkmal der objektorientierten Programmierung ist das der abstrakten Klasse. Mittels abstrakter Klassen können Klassen deklariert werden, die nur einen Teil der Implementierung definieren und erweiternden Klassen die spezifische Implementierung einiger oder aller Methoden überlassen. Abstraktion ist hilfreich, wenn Teile des Verhaltens für alle oder die meisten Objekte eines gegebenen Typs richtig sind, es aber auch Verhalten gibt, das nur für bestimmte Objekte sinnvoll ist und nicht für alle. Es gilt: eine abstrakte Methode hat keinen Rumpf; 79

88 5 Objektorientierte Programmierung in Java jede Klasse, die eine abstrakte Methode enthält, ist selbst abstrakt und muss als solche gekennzeichnet werden; jede abstrakte Klasse muss mindestens eine abstrakte Methode besitzen; man kann von einer abstrakten Klasse keine Objekte erzeugen; von einer Unterklasse einer abstrakten Klasse kann man Objekte erzeugen vorausgesetzt sie überschreibt alle abstrakten Methoden der Oberklasse und implementiert diese; eine Unterklasse, die nicht alle abstrakten Methoden der Oberklasse implementiert ist selbst wieder abstrakt. Beispiel (vgl. Arnold & Gosling [1], S. 72 ff.) Wir wollen ein Programm zur Bewertung von Programm(teilen) schreiben. Unsere Implementierung weiß, wie eine Bewertung gefahren und gemessen wird, aber sie kann nicht im voraus wissen, welches andere Programm bewertet werden soll. Die meisten abstrakten Klassen entsprechen diesem Muster: eine Klasse ist zwar Experte in einem Bereich, doch ein fehlendes Stück kommt aus einer anderen Klasse. In unserem Beispiel ist das fehlende Stück ein Code, der bewertet werden muss. Eine solche Klasse könnte wie folgt aussehen: public abstract class Benchmark { public abstract void benchmark(); public long repeat(int count) { long start = System.currentTimeMillis(); for(int i=0; i<count; i++) benchmark(); return (System.currentTimeMillis()-start); Die Klasse ist als abstract deklariert, weil eine Klasse mit abstrakten Methoden selbst als abstract deklariert werden muss. Diese Redundanz hilft dem Leser, schnell zu erfassen, dass die Klasse abstrakt ist, ohne alle Methoden der Klasse durchzusehen, ob zumindest eine von ihnen abstrakt ist. Die Methode repeat stellt das Sachwissen zur Bewertung bereit. Sie weiß, wie der Zeitbedarf für die Ausführung von count Aufrufen des zu bewertenden Codes zu messen ist. Wird die Messung komplizierter (vielleicht durch Messung der Zeiten jeder Ausführung und Berechnung der Varianz als statistisches Maß darüber), so kann diese Methode verbessert werden, ohne die Implementierung des speziellen zu bewertenden Codes in einer erweiternden Klasse zu beeinflussen. 80

89 5.14 Aufgaben Die abstrakte Methode benchmark muss von jeder selbst nicht wieder abstrakten Unterklasse implementiert werden. Deshalb gibt es in dieser Klasse keine Implementierung, sondern nur eine Deklaration. Hier nun ein Beispiel einer einfachen Erweiterung von Benchmark: public class MethodBenchmark extends Benchmark { public void benchmark() { public static void main(string[] args) { int count = Integer.parseInt(args[0]); long time = new MethodBenchmark().repeat(count); System.out.println(count+" Methodenaufrufe in "+time+ " Millisekunden"); Die Implementierung von benchmark ist denkbar einfach: die Methode hat einen leeren Rumpf. Man kann daher den Zeitbedarf von n Methodenaufrufen feststellen, indem man die main-methode der Klasse MethodBenchmark mit der Angabe n der gewünschten Testwiederholungen laufen lässt Aufgaben Aufgabe Eine Folge heißt Schlange (engl. queue), wenn Elemente eines gegebenen Datentyps T nur am Ende eingefügt und am Anfang entfernt werden dürfen (FIFO-Prinzip: first in first out). In Analogie zum abstrakten Datentypen Stack sollen Sie hier einen abstrakten Datentypen Queue spezifizieren, der folgende Operationen enthält: initqueue: Erzeugen einer leeren Schlange. enqueue: Einfügeoperation. dequeue: Entfernt das vorderste Element. peek: Liefert das vorderste Element der Schlange, ohne die Schlange zu verändern. empty: Liefert true gdw. die Schlange leer ist. Die Operationen sind durch Gleichungen zu spezifizieren. Hinweis: Fallunterscheidungen über Schlangen mit nur einem Element und Schlangen mit mindestens zwei Elementen sind hilfreich. Aufgabe Implementieren Sie eine Klasse Rent in Java, die die Klasse Stack benutzt. Nehmen Sie an, Herr Meier ist Besitzer eines Buches. Herr Meier, der Eigentümer, wird im ersten Eintrag des Stapels beschrieben. Leiht jemand das Buch aus, vielleicht Herr Schmidt, wird dessen Name auf dem Stapel abgelegt. Verleiht 81

90 5 Objektorientierte Programmierung in Java Herr Schmidt es wiederum weiter, z.b. an Herrn Müller, erscheint dessen Name an der Spitze des Stapels, usw. Wird das Buch an seinen Vorgänger zurückgegeben, wird der Name des Entleihers vom Stapel entfernt. Z.B. wird der Name Müller vom Stapel entfernt, wenn er das Buch Herrn Schmidt zurückgibt. Der letzte Name wird nie aus dem Stapel entfernt, denn sonst ginge die Information über den Bucheigentümer verloren. Hinweis: Die Klassen Stack und Rent müssen sich im selben Verzeichnis befinden. Aufgabe (a) Harry Hacker hat wieder einmal programmiert, ohne genau nachzudenken. Er wollte mit der folgenden Methode einen Stack kopieren (d.h. einen neuen Stack mit den gleichen Werten kreieren): public static Stack copy(stack stack) { Stack cpstack = stack; return cpstack; Was hat Harry nicht bedacht? Und wie kann man Harry helfen? Schreiben Sie in Java eine Methode bettercopy, die den ursprünglichen Gedanken von Harry erfüllt. Ergänzen Sie ebenfalls eine main-methode, in der die beiden copy- Methoden aufgerufen werden, so dass der Unterschied deutlich wird. (b) Implementieren Sie statt der klassenbezogenen Methode bettercopy eine objektbezogene Methode gleichen Namens, die dasselbe leistet. Aufgabe Objektorientierte Programmierung ermöglicht eine relativ einfache Modellierung von Ausschnitten der realen Welt. In dieser Aufgabe sollen Sie eine Klasse Vehicle implementieren, die zwei Unterklassen enthält: (i) motorgetriebene Fahrzeuge (Motorrad, Auto, Bus, LKW,...) und (ii) personengetriebene Fahrzeuge (Fahrrad, Tretroller, Inliner,...). Diese Klassen sollen wiederum Unterklassen besitzen. Z.B. kann man die motorgetriebenen Fahrzeuge in zweirädrige, vierrädrige und mehr-als-vierrädrige Fahrzeuge unterteilen. Modellieren Sie Fahrzeuge in sinnvoller Klassenhierarchie. Obligatorisch sind folgende Attribute und Methoden: (a) Die Klasse Vehicle sollte mindestens Datenfelder für die aktuelle Geschwindigkeit, die aktuelle Richtung in Grad, den Preis und den Besitzernamen enthalten. (b) Eine Klasse EnginePoweredVehicle soll mindestens Datenfelder über die Leistung in kw, Front- oder Heckantrieb und Höchstgeschwindigkeit besitzen. (c) Die Klasse PersonPoweredVehicle soll mindestens ein Datenfeld besitzen, das Auskunft über die Anzahl der Personen gibt, die das Fahrzeug antreiben. (d) Es sollen Klassen Car, Bus, Truck, Bike, Motorbike, Inliner und Scooter geben. Alle besitzen ein Datenfeld für eine eindeutige Identifikationsnummer. 82

91 5.14 Aufgaben (e) Schreiben Sie Methoden, die die einzelnen Eigenschaften verändern könnnen. Z.B. sollte die Klasse EnginePoweredVehicle eine Methode besitzen, die es ermöglicht, die Höchstgeschwindigkeit zu setzen (verändern). Jede Klasse soll mindestens zwei Methoden enthalten! (f) Schreiben Sie schließlich eine Klasse SomeVehicles mit einer main-methode, die sechs Fahrzeuge konstruiert. Darauf sollen jeweils mindestens zwei Methoden angewendet werden. (g) Wenn diese Aufgabe Sie unterfordert, brechen Sie die Übung ab. Hauptsache, Sie haben das Prinzip verstanden. Aufgabe Schreiben Sie eine Klasse LinkedList, die ein Datenfeld vom Typ Object und eine Referenz zum nächsten LinkedList-Element in der Liste enthält. Schreiben Sie zusätzlich für Ihre Klasse LinkedList eine main-methode, die einige Objekte vom Typ Vehicle erzeugt und sie aufeinanderfolgend in die Liste einfügt. Können Sie mit Ihrer Implementierung eine leere Liste erzeugen? Aufgabe Sie haben schon die Klasse Circle kennengelernt, die drei Datenfelder besaß: die Koordinaten x und y, die den Mittelpunkt eines Kreises angeben, und eine Variable r, die den Radius enthält. (a) Schreiben Sie analog dazu ein Klasse Rectangle, die vier Datenfelder besitzt. Je zwei Koordinaten x1 und y1, sowie x2 und y2, beschreiben die Endpunkte der Diagonalen eines Rechtecks, das damit vollständig beschrieben ist. (b) Schreiben Sie eine abstrakte Klasse Shape, die die beiden abstrakten Methoden area (berechnet den Inhalt eines geometrischen Objekts) und circumference (berechnet den Umfang eines geometrischen Objekts) beinhaltet. (c) Die Klassen Circle und Rectangle sollen als erweiternde Klassen von Shape implementiert werden. (d) Schreiben Sie dann noch eine main Methode, in der ein Array von Shape- Objekten der Länge 5 konstruiert wird, das Circle- und/oder Rectangle-Objekte enthalten kann. Dann soll das Array mit 5 entsprechenden Objekten gefüllt werden und der Gesamtumfang bzw. der Gesamtflächeninhalt aller Objekte ausgegeben werden. 83

92 5 Objektorientierte Programmierung in Java 84

93 6 Übergang von funktionaler zu OOP Nachdem wir nun mit Java und seinen objektorientierten und imperativen Anteilen einigermaßen vertraut sind, ist es an der Zeit für einen Vergleich zwischen diesen Programmierkonzepten und der funktionalen Programmierung, wie wir sie bei Haskell kennengelernt haben. Zunächst wollen wir allgemein funktionale und imperative Programmierung anhand einiger Beispiele gegenüberstellen. Dann gehen wir konkreter auf die Unterschiede zwischen den beiden Sprachen Haskell und Java ein. Diese Gegenüberstellung beginnt mit einem ausführlichen Vergleich einer Java-Implementierung des abstrakten Datentyps Liste mit den vordefinierten Listen in Haskell. Danach werden weitere Punkte diskutiert, in denen sich die beiden Sprachen unterscheiden, und es werden, wo dies möglich ist, Techniken angegeben, wie die Konzepte der einen Sprache in der anderen nachempfunden werden können. 6.1 Imperative vs. funktionale Programmierung Plakativ lassen sich folgende Aussagen treffen: funktional: imperativ: Berechnung von Werten von Ausdrücken Berechnung des Kontrollflusses Beispiele zur Unterscheidung: Java vs. Haskell 1. Berechnung des Betrages einer ganzen Zahl n: public static int abs(int n) { if (n >= 0) return n; else return -n; > abs :: (Ord a, Num a) => a -> a > abs n n >= 0 = n > otherwise = -n 85

94 6 Übergang von funktionaler zu OOP 2. Berechnung von sum(n) = n i=1 i 2 public static int sum(int n) { int s = 0; for(int i=1; i<=n; i++) s = s + i * i; return s; > sum :: (Enum a, Num a) => a -> a > sum n = foldl g 0 [1..n] > where g x y = x + y * y 3. Berechnung von nextsquare(n) = min{q q > n, q = s 2, s N public static int nextsquare(int n) { int i = 1; int q = 1; while (q <= n) { i++; q = i*i; return q; > nextsquare :: (Num a, Enum a) => a -> a > nextsquare n = (head.dropwhile (<=n).map (^2)) [1..] 86

95 6.2 Listen in Java Es gibt folgende Entsprechungen zwischen beiden Welten: funktional imperativ Liste von Zwischenergebnissen (anonym) Listentyp [t] Rekursionsschemata (foldl, map) abstrahiert von Zwischenergebnissen Speicheraufwändig Listen spielen eine zentrale Rolle Wertabfolgen in Behältern (benannt) Behältertyp t spezielle Anweisungen (while, for) Behälter werden weiterbenutzt Speicherökonomisch Listen sind ein Datentyp wie viele andere 6.2 Listen in Java Wir geben beispielhaft eine (der vielen möglichen) Implementierungen von (Haskell- ) Listen in Java an. Der Typ der Listenelemente ist int. Unsere Klasse IntList benutzt die Klasse Node. public class Node { protected int element; protected Node next; public Node(int val, Node node) { element = val; next = node; public int element() { return element; public Node next() { return next; 87

96 6 Übergang von funktionaler zu OOP Knoten werden nun folgendermaßen zu Listen verknüpft: [1,2] ˆ= 1 2 Die Konstruktoren von IntList public class IntList { private Node first = null; public IntList() { private IntList(Node first) { this.first = first; IntList() liefert also eine leere Liste, während IntList(Node first) einen Knoten (dessen next-datenfeld wiederum auf einen anderen Knoten zeigen kann) in eine Liste verwandelt (es handelt sich also um eine Typkonversion). public IntList () first private IntList (Node first) first 1 2 n Die Methode empty /** Tests whether a list is empty. */ public boolean empty() { return first == null; Die Methode cons /** cons builds up lists. Returns a new reference to the list cons(val,xs), does not change xs. In order to emphasize that cons doesn t change the list it acts upon, it is declared static. */ 88

97 6.2 Listen in Java public static IntList cons(int val, IntList xs) { Node n = new Node(val, xs.first); return new IntList(n); Beispiel cons(x,xs) wobei in (a) xs leer ist und in (b) xs der Liste [2,3] entspricht. (a) xs.first return value xs.first x (b) xs.first 2 3 xs.first return value 2 3 x Die Methoden head und tail Die Funktionen head und tail sind in Haskell folgendermaßen definiert: head (x:xs) = x tail (x:xs) = xs Da die Funktionen in Haskell partiell definiert sind, haben wir freie Wahl in der Implementierung von head [] und tail []. In unserer Implementierung in Java wird eine Fehlermeldung ausgegeben und der Wert 1 bzw. null zurückgegeben. (Das Auslösen einer Ausnahme wäre besser; vgl. Abschnitt 7.3). /** Returns the first element of a list. Returns -1 and prints an error message if the list is empty; does not change the list. */ public int head() { if (empty()) { System.out.println("Error at method head(): Empty List"); return -1; // an exception would be better return first.element(); 89

98 6 Übergang von funktionaler zu OOP /** Returns a new reference to the list obtained by removing the first element. Returns null and prints an error message if the list is empty; does not change the list. */ public IntList tail() { if (empty()) { System.out.println("Error at method tail(): Empty List"); return null; // an exception would be better return new IntList(first.next()); Die Methode append Die Definition von append lautet in Haskell: append [] ys = ys append (x:xs) ys = x:append xs ys Wir betrachten zwei Versionen von append. Eine ist rekursiv und eine ist nicht rekursiv definiert. /** Returns a new reference to the list obtained by concatenation of xs and ys; does not change the lists. In order to emphasize this, it is declared static. */ public static IntList append(intlist xs,intlist ys) { if (xs.empty()) return ys; else return cons(xs.head(),append(xs.tail(),ys)); private static IntList append2(intlist xs,intlist ys) { Node tmp; if (xs.empty()) return ys; else { for(tmp=xs.first; tmp.next!=null; tmp=tmp.next) ; // Find last node tmp.next = ys.first; 90

99 6.2 Listen in Java return xs; Beispiel xs.first 1 2 ys.first zs = append(xs,ys) bzw. zs = append2(xs,ys) append (rekursiv) (nicht desktruktive Variante) append2 (nicht rekursiv) (desktruktive Variante) xs.first 1 2 ys.first zs.first xs.first 1 zs.first ys.first Das Verhalten von append2 ist destruktiv, denn beim Verketten von xs und ys wird xs zerstört (oder milder ausgedrückt, verändert). Diesen Seiteneffekt muss man bei jeder Anwendung von append2 berücksichtigen! Aber auch die erste Version birgt eine Gefahr: Wenn nach zs = append(xs,ys) die Liste ys verändert wird, verändert sich damit auch zs. 91

100 6 Übergang von funktionaler zu OOP Beispiel Sonderfall xs = ys xs.first 1 2 zs = append(xs,xs) bzw. zs = append2(xs,xs) xs.first zs.first append append2 xs.first 1 zs.first 2 Im Fall von append2 erhält man also eine endlose Liste ohne terminierende null-referenz. 6.3 Vergleich zwischen Haskell und Java 1) Parametrischer Typpolymorphismus Die Haskell-Funktion swap ist definiert durch: swap (x,y) = (y,x) (der Typ ist (a,b) -> (b,a)) Beispielsweise ist (1,"a") das Ergebnis von swap("a",1). Um diese Funktion in Java zu implementieren, benutzen wir den Typ Object. public class Pair { protected Object x; protected Object y; public Pair(Object x, Object y) { this.x = x; this.y = y; 92

101 6.3 Vergleich zwischen Haskell und Java public void swap() { Object tmp = x; x = y; y = tmp; public static void main(string[] args) { Pair p = new Pair("a",new Integer(1)); p.swap(); System.out.println(((Integer)(p.x)).intValue()+(String)p.y); Da elementare Daten keine Objekte sind, müssen sie in Objekte verwandelt werden. Dies geschieht mit Hilfe der Hüllenklassen elementare Daten werden durch einen Hüllenklassenkonstruktor eingehüllt und damit zu Objekten. Object Boolean Char Number Float Double Integer Long ABBILDUNG 6.1: Klassenhierarchie der Hüllenklassen Die Klassenhierarchie bezüglich der Hüllenklassen ist in Abbildung 6.1 dargestellt. Nachteil dieser Implementierung sind die vielen notwendigen expliziten Typkonversionen. Wenn man die Methode swap z.b. nur auf int- Zahlen benötigt, kann man folgende Implementierung ohne Typkonversionen wählen. public class IntPair { protected int x; protected int y; public IntPair(int x, int y) { this.x = x; this.y = y; 93

102 6 Übergang von funktionaler zu OOP public void swap() { int tmp = x; x = y; y = tmp; Man muss unter Umständen aber für jeden elementaren Datentyp eine Version der Klasse Pair erstellen. Dies widerspricht natürlich dem Wiederverwendungsgedanken, denn es wird viel Code dupliziert. Seit Java 5 können Klassen mit Typparametern definiert werden. Solche Klassen werden Generische Klassen oder Parametrisierte Klassen genannt. Der Typparameter wird in spitzen Klammern <> nach dem Klassennamen angegeben. Generische Klassen werden wie andere Klassen auch benutzt, jedoch muss bei der Instanziierung der Typ angegeben werden Beispiel Die Klasse Pair mit Generic Types: class Pair<E> { protected E x; protected E y; public Pair(E x, E y) { this.x = x; this.y = y; public void swap() { E tmp = x; x = y; y = tmp; public static void main(string[] args) { Pair<Integer> intpair = new Pair<Integer>(1,2); Pair<String> charpair = new Pair<String>("a","b"); intpair.swap(); charpair.swap(); System.out.println(intPair.x + "," + intpair.y); System.out.println(charPair.x + "," + charpair.y); 94

103 6.3 Vergleich zwischen Haskell und Java 2) Funktionen höherer Ordnung In Haskell gibt es keinen Unterschied zwischen Funktionen und Daten: Funktionen können Argumente anderer Funktionen sein, Funktionen können Funktionen als Wert liefern etc. In Java sind Funktionen (Methoden) Bestandteil von Objekten. Da Funktionen Objekte als Argumente oder Rückgabewert haben können, unterstützt Java insofern Funktionen höherer Ordnung. 3) lokale Änderung großer Datenstrukturen ist in Java ohne weiteres möglich; in Haskell simulierbar durch Monaden 1. 4) Klassen, Objekte und Vererbung in Java kein Problem (objektorientierte Programmiersprache); Haskell-Klassen definieren abstrakte Methoden, jedoch keine Objekte. Eine Instanz einer Klasse muss diese abstrakten Methoden implementieren (d.h. eine eigene Definition angeben). Insofern entspricht eine Haskell-Klasse grob gesprochen einer abstrakten Klasse in Java, die nur abstrakte Methoden enthält (genauer einer Schnittstelle). 5) Keine Entsprechung gibt es z.b. in Java für die unendlichen Datenstrukturen in Haskell in Haskell für die von Java unterstützte Nebenläufigkeit 6) Algebraische Datentypen und Pattern Matching Pattern Matching kann nach einer Idee von Odersky & Wadler (1997) auf sehr elegante Art und Weise simuliert werden. Wir demonstrieren dies an Hand der append-funktion auf Listen. public class List { protected static final int NIL_TAG = 0; protected static final int CONS_TAG = 1; protected int tag; public List append(list ys) { switch (this.tag) { case NIL_TAG: return ys; case CONS_TAG: char x = ((Cons)this).head; 1 Monaden stellen in Haskell eine Möglichkeit dar, imperativ zu programmieren, d.h. es wird ein Kontrollfluss simuliert. 95

104 6 Übergang von funktionaler zu OOP List xs = ((Cons)this).tail; return new Cons(x, xs.append(ys)); default: return new Nil(); //an exception would be better public class Cons extends List { protected char head; protected List tail; public Cons(char head, List tail) { this.tag = CONS_TAG; this.head = head; this.tail = tail; public class Nil extends List { public Nil() { this.tag = NIL_TAG; 6.4 Aufgaben Aufgabe In der Vorlesung Algorithmen und Datenstrukturen I haben Sie das folgende Haskell-Programm für Insertion-Sort kennengelernt: isort :: [Integer] -> [Integer] isort [] = [] isort (a:x) = insert a (isort x) insert :: Integer -> [Integer] -> [Integer] insert a [] = [a] insert a (b:x) = if a<=b then a:b:x else b:insert a x (a) Schreiben Sie ein Java-Programm InsertionSort, das eine Folge von int- Zahlen einliest, diese gemäß obiger Spezifikation sortiert und dann wieder ausgibt. (b) Implementieren Sie ein nicht rekursives Programm, das dasselbe leistet. (c) Bestimmen Sie die Komplexität Ihrer Implementierungen. 96

105 6.4 Aufgaben Aufgabe Das Haskell-Programm reverse reverse :: [a] -> [a] reverse [] = [] reverse (x:xs) = reverse xs ++ [x] ist Ihnen ebenfalls aus Algorithmen und Datenstrukturen I bekannt. (a) Implementieren Sie reverse in Java. Welche Komplexität hat Ihr Programm? (b) Implementieren Sie ein nicht rekursives Programm der Komplexität O(n), das dasselbe leistet. Aufgabe Schreiben Sie eine Methode length, die die Länge einer Liste vom Typ IntList in konstanter Zeit liefert. Dabei ist es erlaubt, Objekte vom Typ IntList um weitere Datenfelder zu erweitern. Aufgabe Eine einfach verkettete Liste haben Sie bereits kennengelernt. In dieser Aufgabe geht es um doppelt verkettete Listen. Eine doppelt verkettete Liste ist ein Liste, deren Knoten nicht nur auf den nächsten Knoten zeigen, sondern auch auf den vorherigen (vgl. Cormen et al. [2], S. 204 ff.). Die abstrakte Klasse Dictionary (genauer java.util.dictionary) enthält abstrakte Methoden zur Speicherung und Ermittlung von Elementen, die über einen Schlüssel indiziert werden (vgl. auch Kapitel 9). Die Methoden von Dictionary sind: public abstract Object put(object key, Object element); Legt element im Dictonary unter key ab. Das alte unter dem Schlüssel gespeicherte Element wird zurückgegeben. Falls es keins gab, wird null zurückgegeben. public abstract Object get(object key); Es wird das mit dem Schlüssel key assoziierte Objekt aus dem Dictionary zurückgegeben. Wenn der Schlüssel nicht definiert ist, wird null zurückgegeben. public abstract Object remove(object key); Der zu key passende Eintrag wird aus dem Dictionary entfernt, und das Element zum Schlüssel key wird zurückgegeben. Wenn es key nicht gibt, wird null zurückgegeben. public abstract int size(); Es wird die Anzahl der im Dictionary definierten Einträge zurückgegeben. public abstract boolean isempty(); Liefert true gdw. das Dictionary keine Einträge enthält. 97

106 6 Übergang von funktionaler zu OOP public abstract Enumeration keys(); Es wird eine Aufzählung der Schlüssel im Dictionary zurückgegeben. public abstract Enumeration elements(); Es wird eine Aufzählung der Elemente aus dem Dictionary zurückgegeben. Schreiben Sie eine Klasse DoLiList, die die ersten fünf abstrakten Methoden von Dictionary mit Hilfe von doppelt verketteten Listen implementiert. Aufgabe Implementieren Sie unter Zuhilfenahme verketteter Listen eine Klasse (a) Stack, die einen Stack implementiert und (b) Queue, die eine Queue implementiert. Aufgabe Harry Hacker hat ebenfalls Listen in Java implementiert. Seine Klasse IntList sieht wie folgt aus: public class IntList { private Node first = null; public IntList() { public void cons(int val) { first = new Node(val,first); public int head() { if (first == null) { System.out.println("Error at method head(): Empty List"); return -1; return first.element(); public void tail() { if (first == null) System.out.println("Error at method tail(): Empty List"); else first = first.next(); public void append(intlist ys) { if (first == null) first = ys.first; 98

107 6.4 Aufgaben else { int hd = head(); tail(); append(ys); cons(hd); Lisa Lista kennt sich jedoch mit Listen bestens aus und sieht sofort, dass zumindest die Implementierung einer Methode die entsprechende funktionale Spezifikation nicht erfüllt. Was geht schief? 99

108 6 Übergang von funktionaler zu OOP 100

109 7 Programmieren im Großen Die sinnvolle Strukturierung des zu entwickelnden Programmcodes ist eine der Grundvoraussetzungen für die erfolgreiche Durchführung größerer Softwareprojekte. Dieses Kapitel soll zeigen, wie in Java über Klassen und Vererbung hinaus die strukturierte Programmierung mit Hilfe von Schnittstellen und Paketen unterstüzt wird. Zur einheitlichen Fehlerbehandlung, ohne den Programmcode mit bedingten Abfragen zu überfrachten, gibt es eine spezielle Ausnahmebehandlung in Java, die wir ebenfalls kennenlernen werden. 7.1 Schnittstellen Es sei die in Abbildung 7.1 dargestellte Klassenhierarchie gegeben (vgl ). Circle Object Shape Drawable Rectangle GraphicCircle GraphicRec ABBILDUNG 7.1: Eine Klassenhierarchie Bei Instanzen der Klassen GraphicCircle und GraphicRec handelt es sich um Objekte, die man zeichnen kann. Um diese zeichenbaren Objekte einheitlich behandeln zu können, wäre es wünschenswert, eine abstrakte Oberklasse Drawable der beiden Klassen zu haben. Problem: Es gibt nur Einfachvererbung in Java. Lösung: Schnittstellen (engl. Interfaces). 101

110 7 Programmieren im Großen Eine Schnittstelle kann man sich als abstrakte Klasse vorstellen, die nur abstrakte objektbezogene Methoden enthält. Während eine abstrakte Klasse auch nichtabstrakte Methoden definieren darf, sind in einer Schnittstelle alle Methoden implizit abstrakte Methoden. Neben abstrakten Methoden darf eine Schnittstelle nur Konstanten enthalten. Beispiel Eine Schnittstellendeklaration: import java.awt.graphics; public interface Drawable { public void draw(graphics g); Eine Klasse darf gleichzeitig eine andere Klasse erweitern und (evtl. mehrere) Schnittstellen implementieren, wie folgendes Beispiel (vgl. Abschnitt 5.9) zeigt. Beispiel import java.awt.color; import java.awt.graphics; public class GraphicCircle extends Circle implements Drawable { protected Color outline; // Farbe der Umrandung protected Color fill; // Farbe des Inneren public GraphicCircle(int x,int y,int r,color outline,color fill) { super(x,y,r); this.outline = outline; this.fill = fill; public void draw(graphics g) { g.setcolor(fill); g.filloval(x-r, y-r, 2*r, 2*r); g.setcolor(outline); g.drawoval(x-r, y-r, 2*r, 2*r); Eine Schnittstelle ist ein Ausdruck reinen Entwurfs, wohingegen eine (abstrakte) Klasse eine Mischung aus Entwurf und Implementierung ist. Schnittstellen können auch mit Hilfe von extends erweitert werden. Im Gegensatz zu Klassen können sie mehr als eine Schnittstelle erweitern. Die Menge der Obertypen einer Klasse besteht aus der von ihr erweiterten Klasse und den von ihr implementierten Schnittstellen einschließlich der Obertypen dieser Klasse und dieser 102

111 7.1 Schnittstellen Schnittstellen. Der Typ eines Objektes ist also nicht nur seine Klasse, sondern auch jeder seiner Obertypen einschließlich der Schnittstellen. Das folgende Beispiel demonstriert dies. Beispiel import java.applet.applet; import java.awt.color; import java.awt.graphics; public class DemoShape extends Applet { public void paint(graphics g) { Shape[] shapes = new Shape[3]; Drawable[] drawables = new Drawable[3]; Drawable gc = new GraphicCircle(300,200,200,Color.red, Color.blue); Drawable gr1 = new GraphicRec(450,200,100,300,Color.green, Color.yellow); Drawable gr2 = new GraphicRec(50,400,300,100,Color.black, Color.magenta); shapes[0] = (Shape) gc; shapes[1] = (Shape) gr1; shapes[2] = (Shape) gr2; drawables[0] = gc; drawables[1] = gr1; drawables[2] = gr2; double totalarea = 0; for(int i=0; i<shapes.length; i++) { totalarea = totalarea+shapes[i].area(); drawables[i].draw(g); Double total = new Double(totalArea); String str = "Total area = "+total.tostring(); g.setcolor(color.black); g.drawstring(str,100,550); 103

112 7 Programmieren im Großen Beispiel: Die vordefinierte Schnittstelle Enumeration Die Schnittstelle Enumeration dient zur Aufzählung aller Elemente eines Datentyps. Sie deklariert zwei Methoden: public abstract boolean hasmoreelements() liefert true zurück, wenn die Aufzählung noch mehr Elemente (als bisher aufgezählt) enthält. Sie darf auch mehr als einmal zwischen aufeinanderfolgenden Aufrufen von nextelement() aufgerufen werden. public abstract Object nextelement() gibt das nächste Element der Aufzählung zurück. Aufrufe dieser Methode zählen aufeinanderfolgende Elemente auf. Wenn keine weiteren Elemente existieren, wird NoSuchElementException ausgelöst (vgl. Abschnitt 7.3). Beispiel Wir wollen alle Elemente eines Stacks aufzählen. Die Klasse Stack (vgl. Abschnitt 5.7) benutzt dazu die Klasse StackEnum. import java.util.enumeration; class StackEnum implements Enumeration { private Stack st; private int pos; protected StackEnum(Stack stack) { st = stack; pos = stack.top; public boolean hasmoreelements() { return (pos!= -1); public Object nextelement() { if(pos!= -1) return st.stack[pos--]; else return null; // an exception would be better Man beachte, dass die Deklaration der Klasse StackEnum keinen Gültigkeitsmodifizierer enthält. Damit erhält diese Klasse automatisch (default) den Gültigkeitsbereich package. Desweiteren ist zu beachten, dass die Datenfelder stack und 104

113 7.2 Pakete top der Klasse Stack im Abschnitt 5.7 als private deklariert wurden. D.h. sie sind in der Klasse StackEnum nicht zugreifbar. Aus diesem Grund müssen diese Datenfelder z.b. als protected deklariert werden. Die Klasse Stack muss nun um die Methode elements erweitert werden. public Enumeration elements() { return new StackEnum(this); Die Anwendung der Methode erfolgt dann durch das Erstellen eines neuen Objektes vom Typ Enumeration. Enumeration e = stack.elements(); while(e.hasmoreelements()) System.out.println(e.nextElement()); Man beachte, dass die Implementierung von elements() völlig verborgen ist und der Typ StackEnum in der Klasse Stack überhaupt nicht auftaucht (stattdessen wird der Typ Enumeration der implementierten Schnittstelle benutzt). Achtung: Die Schnittstelle Enumeration hat keine Schnappschussgarantie. Wird der Inhalt der Sammlung während der Aufzählung verändert, kann das die von den Methoden zurückgegebenen Werte beeinflussen. Ein Schnappschuss würde die Elemente so zurückgeben, wie sie waren, als das Enumeration-Objekt erzeugt wurde. 7.2 Pakete Pakete und Gültigkeitsbereiche wurden schon kurz in Abschnitt 2.4 behandelt. Hier folgt nun eine ausführlichere Beschreibung. Pakete enthalten inhaltlich zusammenhängende Klassen und Schnittstellen. Die Klassen und Schnittstellen können gebräuchliche öffentliche Namen (wie get und put) verwenden, denn durch Voranstellen des Paketnamens können eventuelle Namenskonflikte vermieden werden. Beispielsweise macht es Sinn, die Klassen Stack und StackEnum in ein Paket stack zu stecken. 1 Am Anfang jeder Quelltextdatei, deren Klassen zu dem stack-paket gehören sollen, muss die Paketdeklaration package stack; stehen. package stack; package stack; public class Stack {... class StackEnum {... 1 Paketnamen werden nach Konvention klein geschrieben. 105

114 7 Programmieren im Großen Der Paketname ist implizit jedem im Paket enthaltenen Typnamen vorangestellt. Benötigt außerhalb des Paketes definierter Code innerhalb des Paketes deklarierte Typen, kann er sich auf zwei Arten auf diese beziehen: 1. Voranstellen des Paketnames, z.b. stack.stack. 2. Importieren (von Teilen) des Paketes durch import stack.stack; oder auch durch Importieren aller zugreifbaren Klassen eines Paketes mit *: import stack.*; Der Paket- und Importmechanismus ermöglicht die Kontrolle über möglicherweise in Konflikt geratene Namen. Es gibt z.b. schon eine vordefinierte Java-Klasse Stack. Diese befindet sich im Paket java.util. Sollen beide Klassen im selben Quelltext verwendet werden, so kann dies geschehen durch: Angabe der voll qualifizierten Namen (stack.stack und java.util.stack). Importieren von z.b. java.util.stack oder java.util.* und verwenden von Stack für java.util.stack sowie des vollen Namens von stack.stack bzw. umgekehrt Paketinhalte Enthält eine Quelltextdatei keine Paketdeklarationen, gehen die in ihr deklarierten Typen in ein unbenanntes Paket ein. Pakete sollen sorgfältig entworfen werden, damit sie nur von der Funktionalität zusammengehörige Klassen und Schnittstellen enthalten, denn Klassen in einem Paket können auf die nichtprivaten Attribute und Methoden anderer Klassen frei zugreifen. Pakete können ineinander geschachtelt werden (z.b. java.util). Die Schachtelung ermöglicht ein hierarchisches Benennungssystem, stellt aber keinen speziellen Zugriff zwischen Paketen zur Verfügung Paketbenennung Paketnamen sollen einmalig sein. Folgende Konvention soll dies sicherstellen: package DE.Uni-Bielefeld.TechFak.juser.stack; Der Code für ein Paket muss sich in einem diesen Namen widerspiegelnden Verzeichnis befinden (auf Details wollen wir hier nicht eingehen). 106

115 7.3 Ausnahmen (Exceptions) 7.3 Ausnahmen (Exceptions) Java stellt einen umfangreichen Mechanismus zur Behandlung sog. Ausnahmen (z.b. Fehlermeldungen) bereit. Wir stellen die Vorteile von Ausnahmen gegenüber der traditionellen Fehlerbehandlung schlagwortartig dar. Ausnahmen sind eine saubere Art, Fehlerprüfungen vorzunehmen, ohne den Quelltext zu überfrachten; vermeiden eine Überflutung des grundlegenden Ablaufs des Programms mit vielen Fehlerprüfungen; zeigen Fehler direkt an, statt Variablen oder Seiteneffekte auf Datenfelder zu nutzen, die anschließend zu prüfen sind; machen Fehlerbedingungen zum expliziten Bestandteil der Spezifikation einer Methode; sind daher für Programmierer sichtbar und bei einer Analyse überprüfbar. Eine Ausnahme ist ein Signal, das auf eine unerwartete Fehlerbedingung hinweist. Eine Ausnahme wird ausgelöst durch das throw-konstrukt. Die Ausnahme wird durch ein umgebendes Konstrukt irgendwo entlang der aktuell ablaufenden Methodenaufrufe abgefangen 2. Wird die Ausnahme nicht abgefangen, tritt die standardmäßige Ausnahmebehandlung in Kraft. Die Klassenhierarchie für Ausnahmen ist in Abbildung 7.2 dargestellt. Man unterscheidet zwischen geprüften und ungeprüften Ausnahmen. Es gilt: ungeprüfte Ausnahmen erweitern die Klasse Error und RuntimeException und werden nicht abgefangen; geprüfte Ausnahmen erweitern die Klasse Exception (nach allgemeiner Übereinkunft nicht die Klasse Throwable): der Übersetzer überprüft, dass Methoden nur die von ihnen deklarierten Ausnahmen auslösen. Jedes Objekt vom Typ Throwable hat ein Datenfeld vom Typ String, welches mit der Methode getmessage() gelesen werden kann; dieser String enthält eine Fehlermeldung, die die Ausnahme beschreibt. Das Datenfeld wird bei der Erzeugung eines Ausnahmeobjekts mit Hilfe eines Konstruktors gesetzt. 2 Dies bezeichnet man auch als catch. 107

116 7 Programmieren im Großen Object Throwable Error Exception AbstractMethodError RuntimeException ArithmeticException... ABBILDUNG 7.2: Klassenhierarchie für Ausnahmen Beispiel Der folgende Programmcode implementiert die hier dargestellte Hierarchie von Ausnahmeklassen: Exception MyException MyOtherException MySubException class MyException extends Exception { public MyException() { super(); public MyException(String s) { super(s); class MyOtherException extends Exception { public MyOtherException() { super(); 108

117 7.3 Ausnahmen (Exceptions) public MyOtherException(String s) { super(s); class MySubException extends MyException { public MySubException() { super(); public MySubException(String s) { super(s); Es gibt zwei wesentliche Gründe für die Einführung neuer Ausnahmetypen: Hinzufügen nützlicher Informationen; der Typ selbst ist eine wichtige Information über die Ausnahmebedingung, da Ausnahmen aufgrund ihres Typs abgefangen werden throw und throws Ausnahmen werden ausgelöst durch die ein Objekt als Parameter erhaltene throw- Anweisung. Beispiel import java.util.emptystackexception; public class Stack {... public Object pop() { if(empty()) throw new EmptyStackException(); else return stack[top--]; public Object peek() { if(empty()) throw new EmptyStackException(); else 109

118 7 Programmieren im Großen return stack[top]; Alle geprüften Ausnahmen, die in einer Methode ausgelöst und nicht in dieser abgefangen und behandelt werden, müssen deklariert werden durch throws. Das Fehlen von throws bedeutet, dass die Methode keine geprüfte Ausnahme auslöst. In der Tat ist EmptyStackException eine Unterklasse von RuntimeException. Daher musste EmptyStackException in Beispiel nicht deklariert werden. Im folgenden Beispiel ist dies anders. Beispiel MyException und MySubException seien wie in Beispiel definiert. public static int c(int i) throws MyException { switch(i) { case 0: throw new MyException("input too low"); case 1: throw new MySubException("input still too low"); default: return i*i; Die Methode c kann die beiden Ausnahmen MyException und MySubException auslösen. Diese müssen daher deklariert werden. Da allerdings MySubException eine Unterklasse von MyException ist, reicht es, MyException zu deklarieren. Ruft man eine Methode auf, die nach throws eine zu berücksichtigende Ausnahme aufführt, hat man die drei Möglichkeiten: (a) die Ausnahme abzufangen und zu behandeln, (b) die Ausnahme abzufangen und auf eine eigene Ausnahme abzubilden, die man dann selbst auslösen kann, und deren Typ im eigenen throws- Konstrukt deklariert ist, (c) den Ausnahmetyp im eigenen throws-konstrukt zu deklarieren und die Ausnahme ohne Behandlung die Methode passieren zu lassen (dabei kann ein eigenes finally-konstrukt vorher noch zum Aufräumen aktiv werden) try, catch und finally Das allgemeine Muster zum Umgang mit Ausnahmen in Java ist die try-catchfinally-sequenz: Man versucht etwas; wenn dieses eine Ausnahme auslöst, fängt man die Ausnahme ab; und schließlich räumt man nach Ende des normalen Ablaufs oder des Ausnahmeablaufs auf, was immer auch passiert ist. Die Syntax der try-catch-finally-sequenz ist folgendermaßen: 110

119 7.3 Ausnahmen (Exceptions) try block1 catch(exception_type identifier) block2 catch(exception_type identifier) block3... finally blockl Der Rumpf (block1) der try-anweisung wird ausgeführt, bis entweder eine Ausnahme ausgelöst oder der Rumpf erfolgreich abgeschlossen wird. Wenn eine Ausnahme ausgelöst wurde, werden die catch-konstrukte betrachtet, um darin die Klasse der Ausnahmen oder eine ihrer Oberklassen zu finden. Wird kein solches catch gefunden, so wird die Ausnahme über diese try-anweisung hinaus an einen umgebenden try-block zur Behandlung weitergeleitet. Die Zahl der catch-konstrukte ist beliebig, sogar kein catch ist möglich. Wenn kein catch in einer Methode zur Behandlung einer Ausnahme in Frage kommt, wird die Ausnahme dem Aufrufer der Methode zur Behandlung weitergereicht. Wenn ein finally-konstrukt in einem try vorhanden ist, so wird dessen Block nach allen anderen Anweisungen in dem try ausgeführt. Dies erfolgt unabhängig davon, wie die vorherigen Anweisungen abgeschlossen wurden; sei es regulär, durch Ausnahmen oder durch den Ablauf beeinflussende Anweisungen wie return 3. Die catch-konstrukte werden der Reihe nach untersucht. Deshalb ist es ein Fehler, wenn in den catch-konstrukten ein Ausnahmetyp vor einer Erweiterung desselben abgefangen wird (denn das catch mit dem Untertyp würde niemals erreicht werden). Dies wird in folgendem Beispiel deutlich. Beispiel (vgl. Arnold & Gosling [1], S. 155 ff.) MyException und MySubException seien wie in Beispiel definiert. class BadCatch { public void goodtry() { try { throw new MySubException(); catch(myexception myref) { // Abfangen von sowohl MyException als auch MySubException catch(mysubexception mysubref) { // Dies wird nie erreicht 3 Auch break ist so eine Anweisung, wir werden sie aber nicht genauer behandeln. 111

120 7 Programmieren im Großen Nur eine Ausnahme wird in einem try-konstrukt behandelt. Wenn eine weitere Ausnahme ausgelöst wird, werden die catch-konstrukte des try nicht ein weiteres Mal aktiviert. Das finally-konstrukt in der try-anweisung ermöglicht es unabhängig davon, ob eine Ausnahme ausgelöst wurde abschließend noch ein Programmstück auszuführen (z.b. das Schließen von offenen Dateien; so kann sparsam mit dieser begrenzten Ressource umgegangen werden). Wenn der finally-block durch return oder Auslösen einer Ausnahme verlassen wird, kann ein ursprünglicher Rückgabewert in Vergessenheit geraten. Beispiel (Arnold & Gosling [1], S. 157) try { //... irgendein Code... return 1; finally { return 2; Es würde immer der Wert 2 zurückgegeben werden. Beispiel (Flanagan [5], S. 43 ff.) MyException, MyOtherException und MySubException seien wie in Beispiel definiert. Um das nachfolgende Programm verstehen zu können, muss man wissen, dass instanceof feststellt, ob ein Objekt von einem gegebenen Typ ist. Die null-referenz ist keine Instanz irgendeines Typs, daher ist false der Rückgabewert von null instanceof Typ. public class ThrowTest { public static void main(string[] args) { int i; try { i = Integer.parseInt(args[0]); catch(arrayindexoutofboundsexception e) { System.out.println("Must specify an argument"); return; 112

121 7.3 Ausnahmen (Exceptions) catch(numberformatexception e) { System.out.println("Must specify an integer argument"); return; a(i); public static void a(int i) { try { b(i); catch(myexception e) { if(e instanceof MySubException) System.out.print("MySubException:"); else System.out.print("MyException:"); System.out.println(e.getMessage()); System.out.println("Handled at point 1"); public static void b(int i) throws MyException { int result; try { System.out.println("i= "+i); result = c(i); System.out.println("c(i)= "+result); catch(myotherexception e) { System.out.println("MyOtherException: "+e.getmessage()); System.out.println("Handled at point 2"); finally { System.out.println(); public static int c(int i) throws MyException,MyOtherException { switch(i) { case 0: throw new MyException("input too low"); case 1: throw new MySubException("input still too low"); case 99: throw new MyOtherException("input too high"); default: return i*i; 113

122 7 Programmieren im Großen > java ThrowTest hello Must specify an integer argument > java ThrowTest 0 i= 0 MyException: input too low Handled at point 1 > java ThrowTest 1 i= 1 MySubException: input still too low Handled at point 1 > java ThrowTest 2 i= 2 c(i)= 4 > java ThrowTest 99 i= 99 MyOtherException: input too high Handled at point Aufgaben Aufgabe Vervollständigen Sie die Klasse DoLiList aus Aufgabe 6.4.4, so dass diese als nicht-abstrakte Unterklasse der abstrakten Klasse Dictionary deklariert werden kann. Aufgabe Reimplementieren Sie schon erstellte Programme derart, dass diese auf dem Ausnahmekonzept beruhen. 114

123 8 Graphen Graphen und Graphalgorithmen sind allgegenwärtig in der Informatik. Hunderte von interessanten Problemen lassen sich mit Hilfe von Graphen formulieren. Dieses Kapitel beschreibt zunächst Methoden zur Repräsentation von Graphen. Danach wird eine kleine Auswahl der wichtigsten Algorithmen bei ihrer Verarbeitung vorgestellt. Literatur: [3, 4] 8.1 Anwendungen von Graphen Um einen Eindruck von der Vielfalt der Einsatzmöglichkeiten von Graphen und Graphalgorithmen zu bekommen, wollen wir einige Beispiele betrachten: Karten Wenn wir eine Reise planen, wollen wir Fragen beantworten wie: Was ist der kürzeste Weg von Bielefeld nach München? Was der schnellste Weg? Um diese Fragen beantworten zu können, benötigen wir Informationen über Verbindungen (Reiserouten) zwischen Objekten (Städten). Hypertexts Das ganze Web ist ein Graph: Dokumente enthalten Referenzen (Links) auf andere Dokumente, durch die wir navigieren. Graphalgorithmen sind essentielle Komponenten der Suchmaschinen, die uns Informationen im Web finden lassen. Schaltkreise Für elektrische Schaltungen sind wir an kreuzungsfreien Chip-Layouts interessiert und müssen Kurzschlüsse vermeiden. Zeitpläne Die Erledigung einiger Aufgaben hängt evtl. von der Erledigung anderer ab. Diese Abhängigkeiten können als Verbindungen von Aufgaben modelliert werden. Ein klassisches scheduling Problem wäre dann: Wie arbeiten wir die Aufgaben unter den gegeben Voraussetzungen am schnellsten ab? Netzwerke Computernetzwerke bestehen aus untereinander verbundenen Einheiten, die Nachrichten senden, empfangen und weiterleiten. Wir sind nicht nur 115

124 8 Graphen daran interessiert, welchen Weg eine Nachricht von einem Ort zum anderen nehmen muss. Genauso will man sicherstellen, dass die Konnektivität aller Orte auch dann gewähleistet ist, wenn sich das Netzwerk ändert (Ausfallsicherheit). Genauso muss der Datenfluss sichergestellt sein, so dass das Netzwerk nicht verstopft. 8.2 Terminologie Graphen werden als eine durch Kanten verbundene Menge von Knoten definiert. Definition Ein Graph G = (V, E) besteht aus einer Menge von Knoten V (auch vertices oder nodes) und einer Menge von Kanten E (edges). Jede Kante ist ein Paar (v, w) V, das Paare von Knoten verbindet. Wir beschränken uns im Folgenden auf Graphen, die keine doppelten (oder parallele) Kanten besitzen. (Graphen, die doppelte Kanten enthalten, nennt man Multigraphen.) Definition Ein gewichteter Graph (weighted graph) ist ein Graph, in dem Kanten mit Gewichten versehen sind. Gewichtete Graphen enthalten somit zusätzliche Attribute, in denen z.b. die Länge einer Kante (bei der Modellierung eines Straßennetzes) repräsentiert werden können. Beispiel Abbildung 8.1 zeigt einen gewichteten Graphen, der Bahnverbindungen zwischen einigen Großstädten Deutschlands repräsentiert. Die Städte sind als Knoten, direkte Verbindungen als Kanten zwischen den jeweiligen Städten dargestellt. Die Entfernungen zwischen zwei Städten ist als Gewicht an der jeweiligen Kante vermerkt. Wir können z.b. folgende Fragen stellen, die mit Hilfe des Graphen beantwortet werden können: Gibt es eine direkte Verbindung zwischen Stadt BI und M? Welches ist der kürzeste Weg von BI nach S? Welches ist der kürzeste Weg, der in Stadt F startet und alle Städte einmal besucht? Definition Ein gerichteter Graph (directed graph, digraph) ist ein Graph, in dem jede Kante eine Richtung hat. Für u, v V ist dann (u, v) (v, u). In gerichteten Graphen können zwei Knoten durch zwei Kanten verbunden sein, allerdings nur durch je eine Kante in jede Richtung. 116

125 8.2 Terminologie HH H 293 B 115 BI DO F S 230 M ABBILDUNG 8.1: Ein Graph zur Repräsentation von Bahnverbindungen zwischen verschiedenen Städten. Definition Ein Teilgraph (subgraph) von (V, E) ist ein Paar (V, E ), mit V V und E = {(u, v) (u, v) E : u V, v V. Definition Ein Graph heißt verbunden (connected), wenn jeder Knoten von jedem anderen Knoten aus erreicht werden kann. Ein Graph, der nicht verbunden ist, besteht aus einer Menge von Zusammenhangskomponenten (connected components), die maximal verbundene Teilgraphen sind. Definition Zwei Knoten u, v V mit u v heißen benachbart (adjacent), wenn (u, v) E oder (v, u) E. Definition Bei der Bestimmung des Grads (degree) eines Knotens muss man zwischen ungerichteten und gerichteten Graphen unterscheiden: ungerichtete Graphen: Der Grad eines Knotens ist die Zahl seiner Nachbarn. gerichtete Graphen: Der Eingangsgrad (in-degree) eines Knotens v V ist die Zahl der Kanten (u, v) E. 117

126 8 Graphen Der Ausgangsgrad (out-degree) eines Knotens v V ist die Zahl der Kanten (v, u) E. Der Grad ist die Summe von Eingangs- und Ausgangsgrad. Definition Ein Pfad (path) von u nach v ist einen Folge von Knoten u 1, u 2,..., u k, so daß u 1 = u und u k = v und (u i, u i+1 ) E für alle 1 i < k. Definition Ein Zyklus (cycle) ist ein Pfad, in dem Start- und Endknoten identisch sind. Definition Auch Bäume sind spezielle Graphen: Ein Baum (tree) ist ein ungerichteter, verbundener Graph ohne Zyklen (genauer: ohne Kreise, also Zyklen, in denen nur Anfangs- und Endpunkt identisch sind). Eine Menge von Bäumen heißt Wald (forest). Ein Spannbaum (spanning tree) eines verbundenen Graphen (V, E) ist ein Teilgraph, der alle Knoten V enthält und ein Baum ist. 8.3 Repräsentation von Graphen Um Graphen darzustellen gibt es zwei Standardmöglichkeiten: Adjazenzlisten und Adjazenzmatrizen, die beide sowohl für gerichtete als auch ungerichtete Graphen verwendet werden können. Bei dünn besetzten (sparse) Graphen, bei denen die Anzahl der Kanten E viel kleiner als V 2 ist, liefern Adjazenzlisten eine kompakte Darstellung. Die Repräsentation durch Adjazenzmatrizen wird vorgezogen, wenn der Graph dicht besetzt (dense) ist (d.h. wenn E nahe an V 2 liegt, oder wenn ein Algorithmus möglichst schnell hearusfinden muss, ob zwei Knoten verbunden sind. Adjazenzlisten Die Abbildungen 8.2(b) und 8.3(b) zeigen ein Beispiele für die Repräsentation eines Graphen als Adjazenzliste: ein Array Adj enthält für jeden Knoten aus V eine Liste. Für jeden Knoten u V enthält die Liste Adj[u] alle Knoten v, so dass (u, v) E. Die Knoten werden dabei üblicherweise in beliebiger Reihenfolge gespeichert. Wenn die Kanten Gewichte haben, werden diese ebenfalls in der Liste gespeichert. Diese Darstellung braucht Θ(V + E) Platz. Alle Knoten, die adjazent zu u sind, können in Θ(degree(u)) bestimmt werden. Um zu überprüfen, ob (u, v) E gilt, wird O(degree(u)) Zeit benötigt. Adjazenzmatrizen Die Abbildungen 8.2(c) und 8.3(c) zeigen ein Beispiele für die Repräsentation eines Graphen als Adjazenzmatrix: die Knoten des Graphen seien in beliebiger 118

127 8.4 Ein Abstrakter Datentyp (ADT) Graph (a) (b) (c) ABBILDUNG 8.2: Zwei Repräsentationen eines ungerichteten Grapen mit 5 Knoten und 7 Kanten (a). (b) zeigt die Adjazenzlisten-Repräsentation des Graphen, (c) die entsprechende Adjazenzmatrix. (Nach [3], S. 528) (a) (b) (c) ABBILDUNG 8.3: Zwei Repräsentationen eines gerichteten Grapen mit 6 Knoten und 8 Kanten (a). (b) zeigt die Adjazenzlisten-Repräsentation des Graphen, (c) die entsprechende Adjazenzmatrix. (Nach [3], S. 528) Reihenfolge nummeriert. Dann kann der Graph durch eine V V -Matrix A = (a ij ) beschrieben werden, mit den Elementen a ij = { 1 falls (i, j) E, 0 sonst. Die Adjazenzmatrix-Darstellung benötigt O(V 2 ) Platz. Alle Knoten, die adjazent zu u sind, können in Θ(V ) bestimmt werden. Um zu überprüfen, ob (u, v) E gilt, wird O(1) Zeit benötigt. Gewichtete Graphen können dargstellt werden, indem man in der Matrix Gewichte statt Bits speichert. 8.4 Ein Abstrakter Datentyp (ADT) Graph Wir wollen nun einen abstrakten Datentypen für Graphen beschreiben, der als Java-Interface definiert wird. Dieses sehr einfach gehaltene Interface genügt, um die in den nächsten Abschnitten beschriebenen Algorithmen zu implementieren. 119

128 8 Graphen Der Graph Konstruktor bekommt zwei Parameter: einen Integer-Wert für die Anzahl der Knoten im Graphen und einen Boolean, der angibt, ob der Graph gerichtet ist oder nicht. Die Operationen beschränken sich zunächst auf: numofv() gibt die Anzahl der Knoten zurück numofe() gibt die Anzahl der Kanten zurück directed() gibt an, ob der Graph gerichtet ist insert(e) fügt eine Kante in den Graphen ein remove(e) löscht einen Kante aus dem Graphen edge(v,w) überprüft, ob es eine Kante zwischen Knoten v und w gibt getadjlist(v) stellt einen Iterator zur Verfügung, der alle benachbarten Knoten von v aufzählt Das Java-Interface ist folgendermaßen definiert: public interface Graph { int numofv(); int numofe(); boolean directed(); void insert(edge e); void remove(edge e); boolean edge(int v, int w); AdjList getadjlist(int v); public interface AdjList { int begin(); int next(); boolean end(); public class Edge { int v; int w; Edge(int v, int w) { this.v = v; this.w = w; 120

129 8.5 Breitensuche Das oben angegebene Interface ist ein möglichst einfach gehaltenes Beispiel für einen Graph-ADT. Bestimmte Algorithmen erfordern eventuell eine Anpassung dieses Interfaces, z.b. durch Einführen einer speziellen Klasse Vertex oder Erweiterung der Klasse Edge, um zusätzliche Informationen in den Knoten oder Kanten (z.b. Gewichte) speichern zu können. Auch können in unserem Beispiel noch keine Knoten hinzugefügt oder gelöscht werden. Ebenso finden keine Überprüfungen statt, ob z.b. parallele Kanten zwischen Knoten eingefügt werden. Dies wiederum ist abhängig von der jeweiligen Anwendung des Graphen und müßte entsprechend behandelt werden. 8.5 Breitensuche Die Breitensuche (breadth-first search) ist eine der einfachsten Algorithmen zur Suche in Graphen. Gegeben einen Graphen G und einen Startknoten s, durchsucht die Breitensuche systematisch jede Kante von G, um alle Knoten zu finden, die von s erreichbar sind. Dabei wird die Distanz (kleinste Anzahl von Kanten) von s zu jedem erreichbaren Knoten berechnet. Bei der Suche wird ein breadthfirst tree mit Wurzel s erzeugt, der alle erreichbaren Knoten enthält. Der kürzeste Pfad von s nach v in dem Baum entspricht dem kürzesten Pfad von s nach v im Graphen G. Der Name Breitensuche läßt sich dadurch erklären, dass die Suche die Grenze zwischen besuchten und unbesuchten Knoten gleichmäßig über die Breite der Grenze ausdehnt. D.h. der Algorithmus besucht zunächst alle Knoten mit Distanz k von s, bevor Knoten mit Distanz k + 1 besucht werden. Während der Breitensuche werden Knoten weiß, grau oder schwarz eingefärbt. Wenn ein Knoten während der Suche entdeckt wird, ändert sich seine Farbe. Graue und schwarze Knoten sind bereits entdeckt worden, und diese Information wird ausgenutzt, um die Breitensuche voranzutreiben. Wenn es eine Kante (u, v) E gibt, und u ist schwarz, dann ist v entweder grau oder schwarz. Das bedeutet, dass alle benachbarten Knoten eines schwarzen Knotens bereits entdeckt wurden. Graue Knoten können noch weiße Nachbarn haben; sie repräsentieren die Grenze zwischen entdeckten und unentdeckten Knoten. Die Breitensuche konstruiert einen Breitensuchbaum, dessen Wurzel der Startknoten s ist. Mithilfe dieses Baumes lassen sich Vorgänger- und Nachfolgerrelationen (relativ zu s) aufstellen. Immer dann, wenn ein Knoten entdeckt wird (und auch nur dann), wird dem Baum eine Kante hinzugefügt. Der folgende Algorithmus führt eine Breitensuche in einem Graphen aus. Abbildung 8.4 zeigt die Suche an einem Beispiel. Die Farbe eines Knotens u V wird in color[u] gespeichert, der Vorgänger von u in π[u]. Wenn u keinen Vorgänger hat (z.b. s oder wenn u noch nicht entdeckt wurde), ist π[u] = NIL. Der Algorithmus berechnet auch die Distanz von s zu jedem Knoten u und speichert sie in d[u] (initial ist die Distanz von s zu allen Knoten unendlich groß). Außerdem wird 121

130 Breadth-first Search 8 Graphen Breadth-first Search (a) r s t u 0 Breadth-first Search v w x y Q s 0 (b) r s t u 1 0 Breadth-first Search 1 v w x y Q w 1 r 1 (c) r s t u Breadth-first Search 1 2 v w x y Q r 1 t 2 x 2 (d) r s t u Breadth-first Search v w x y Q t 2 x 2 v 2 (e) r s t u Breadth-first Search v w x y Q x 2 v 2 u 3 (f) r s t u Breadth-first Search v w x y Q v 2 u 3 y 3 (g) r s t u Breadth-first Search v w x y Q u 3 y 3 (h) r s t u v w x y Q y 3 (i) r s t u v w x y Q ABBILDUNG 8.4: Operationen der Breitensuche auf einem ungerichteten Graphen. Die Kanten des bei der Suche entstehenden Baumes sind schattiert dargestellt. Jeder Knoten u enthält den Wert d[u]. Die Queue Q ist jeweils zu Beginn jeder Iteration der while- Schleife gezeigt. Knotenabstände sind jeweils unter der Queue dargestellt. (Nach [3], S. 533) noch eine Queue Q verwendet, um die grauen Knoten zwischenzuspeichern. Die Laufzeit des Algorithmus zur Breitensuche ist O(V + E). BFS(G, s) 1 for each vertex u V [G] {s 2 do color[u] W HIT E 3 d[u] 4 π[u] NIL 5 color[s] GRAY 6 d[s] 0 7 π[s] NIL 8 Q 9 ENQUEUE(Q, s) 122

131 8.6 Tiefensuche 10 while Q 11 do u DEQUEUE(Q) 12 for each v Adj[u] 13 do if color[v] = W HIT E 14 then color[v] GRAY 15 d[v] d[u] π[v] u 17 ENQUEUE(Q, v) 18 color[u] BLACK 8.6 Tiefensuche Die Strategie der Tiefensuche besteht darin, immer zuerst tiefer im Graphen zu suchen. Dabei werden zunächst die Kanten durchsucht, die von dem zuletzt entdeckten Knoten v ausgehen. Erst wenn alle Kanten von v untersucht wurden, geht die Suche zurück zu dem Knoten, von dem aus v entdeckt wurde, um dort wieder unentdeckte Knoten zu finden. Dieser Prozess läuft solange, bis alle Knoten entdeckt wurden, die vom Startknoten aus erreichbar sind. Sollte es dann noch unentdeckte Knoten geben, wird einer von diesen als neuer Startknoten ausgewählt und die Suche von dort neu gestartet. Dies passiert so oft, bis alle Knoten gefunden wurden. Entsprechend zur Breitensuche wird, wenn ein Knoten v während der Suche in der Adjazenzliste eines bereits entdeckten Knotens u gefunden wird, dieses Ereignis notiert. Dazu wird der Vorgänger von v in π gespeichert: π[v] = u. Knoten werden wieder entsprechend ihres Status eingefärbt. Anfangs sind alle knoten weiß, sie werden grau, wenn sie entdeckt werden und schwarz, wenn die Suche abgeschlossen ist (d.h. die Adjazenzliste des Knotens abgearbeitet wurde). Außerdem werden alle Knoten während der Suche mit Zeitstempeln (timestamps) versehen. Jeder Knoten v hat zwei solche Zeitstempel: der erste d[v] notiert, wann v erstmals entdeckt (und grau eingefärbt) wurde, der zweite f[v], wann v abgearbeitet (und schwarz eingefärbt) wurde. Diese Zeitstempel werden in vielen Graphalgorithmen verwendet. Der folgende Pseudocode DFS zeigt den grundlegenden Algorithmus zur Tiefensuche. Der Eingabegraph G kann gerichtet oder ungerichtet sein, die globale Variable time wird zum Zeitstempeln genutzt. Abbildung 8.5 illustriert den Prozess der Tiefensuche an einem Beispiel. 123

132 Depth-first8 Search Graphen Depth-first Search Depth-first Search Depth-first Search u v w u v w u v w u v w 1/ 1/ 2/ 1/ 2/ 1/ 2/ Depth-first Search Depth-first Search Depth-first Search 3/ Depth-first Search 4/ 3/ x y z x y z x y z x y z (a) (b) (c) (d) u v w u v w u v w u v w 1/ 2/ 1/ 2/ 1/ 2/ 1/ 2/7 B B B B Depth-first Search 4/ 3/ Depth-first Search 4/5 3/ Depth-first Search 4/5 3/6 Depth-first Search 4/5 3/6 x y z x y z x y z x y z (e) (f) (g) (h) u v w u v w u v w u v w 1/ 2/7 1/8 2/7 1/8 2/7 9/ 1/8 2/7 9/ C F F F F B B B B Depth-first Search 4/5 3/6 Depth-first Search 4/5 3/6 Depth-first Search 4/5 3/6 Depth-first Search 4/5 3/6 x y z x y z x y z x y z (i) (j) (k) (l) u v w u v w u v w u v w 1/8 2/7 9/ 1/8 2/7 9/ 1/8 2/7 9/ 1/8 2/7 9/12 F C C C C F F F B B B B 4/5 3/6 10/ 4/5 3/6 10/ B 4/5 3/6 10/11 B 4/5 3/6 10/11 x y z x y z x y z x y z (m) (n) (o) (p) B ABBILDUNG 8.5: Tiefensuche auf einem gerichteten Graphen. Die Kanten, die gerade von dem Algorithmus verarbeitet werden, sind entweder schattiert (Kanten des Suchsbaumes) oder gestrichelt dargestellt. Kanten, die nicht Teil des Suchbaumes sind, sind mit B,C oder F bezeichnet, je nachdem ob sie back, cross oder forward Kanten sind. Die Knoten sind mit den Zeitpunkten ihrer Entdeckung und Abarbeitung (Endzeit) benannt. (Nach [3], S. 542) DFS(G) 1 for each vertex u V [G] 2 do color[u] W HIT E 3 π[u] NIL 4 time 0 5 for each vertex u V [G] 6 do if color[u] = W HIT E 7 then DFS-VISIT(u) 124

133 8.7 Topologisches Sortieren DFS-VISIT(u) 1 color[u] GRAY White vertex u has just been discovered. 2 time time d[u] time 4 for each v Adj[u] Explore edge (u, v). 5 do if color[v] = W HIT E 6 then π[v] u 7 DFS-VISIT(v) 8 color[u] BLACK Blacken u; it is finished. 9 f[u] time time Topologisches Sortieren Als nächstes wollen wir und ein Beispiel ansehen, wie die Tiefensuche verwendet werden kann, um topologisches Sortieren auf einem gerichteten Graphen durchzuführen. Eine topologisches Sortierung eines gerichteten Graphen ist eine lineare Anordnung aller seiner Knoten, so dass für jede Kante (u, v), der Knoten u vor v einsortiert wird. (Dazu muss der Graph azyklich sein, sonst ist eine lineare Anordnung nicht möglich.) Man kann diese Sortierung als Sortierung entlang einer horizontalen Linie ansehen, so dass alle gerichteten Kanten immer von links nach rechts zeigen. Gerichtete azyklische Graphen werden häufig verwendet, um Prioritäten zwischen Ereignissen zu beschreiben. Abbildung 8.6 zeigt ein Beispiel: Wenn Harry Hacker sich morgens anzieht, muss er bestimmte Kleidungsstücke vor anderen anlegen (z.b. die Socken vor den Schuhen). Andere sind unabhängig voneinander, wie z.b. Socken und Gürtel. Eine gerichtete Kante (u, v) gibt dann an, dass Kleidungsstück u vor v angelegt werden muss (Abb.8.6a). Mithilfe einer topologische Sortierung kann Harry Hacker sich einen Plan erstellen, wie er sich ankleiden muss (Abb. 8.6b). Der folgende einfache Algorithmus sortiert den Graphen G topologisch: TOPOLOGICAL-SORT(G) 1 call DFS(G) to compute finishing times f[v] for each vertex v 2 as each vertex is finished, insert it into the front of a linked list 3 return the linked list of vertices 125

134 8 Graphen 11/16 undershorts socks 17/18 watch 9/10 12/15 pants shoes 13/14 (a) 6/7 belt shirt 1/8 tie 2/5 jacket 3/4 (b) socks undershorts pants shoes watch shirt belt tie jacket 17/18 11/16 12/15 13/14 9/10 1/8 6/7 2/5 3/4 ABBILDUNG 8.6: (a) Wenn Harry Hacker sich morgens anzieht, sortiert er seine Kleider zunächst topologisch. Jede gerichtete Kante (u, v) bedeutet, dass Kleidungsstück u vor v angelegt werden muss. Die Entdeckungs- und Endzeiten einer Tiefensuche sind neben den Knoten dargestellt. (b) zeigt denselben Graphen wie (a), diesmal topologisch sortiert.die Knoten sind von links nach rechts anhand der fallenden Endzeiten sortiert. Beachte, dass alle gerichteten Kanten von links nach rechts verlaufen. (Nach [3], S. 550) 126

135 9 Hashing Dieses Kapitel beschäftigt sich mit einem wichtigen Speicherungsund Suchverfahren, bei dem die Adressen von Daten aus zugehörigen Schlüsseln errechnet werden, dem Hashing. Dabei stehen die Algorithmen und verschiedene Heuristiken der Kollisionsbehandlung im Vordergrund. Es wird aber auch auf die in Java vordefinierte Klasse Hashtable eingegangen. 9.1 Einführendes Beispiel Ein Pizza-Lieferservice in Bielefeld speichert die Daten seiner Kunden: Name, Vorname, Adresse und Telefonnummer. Wenn ein Kunde seine Bestellung telefonisch aufgibt, um dann mit der Pizza beliefert zu werden, dann muss er seine 127

136 9 Hashing Telefonnummer angeben, da er über diese Nummer eindeutig identifiziert werden kann. Natürlich existiert in Bielefeld jede Telefonnummer nur einmal, während es mehrere Menschen mit gleichem Vornamen, Nachnamen oder Adresse gibt. Das bedeutet: Wenn der Telefonist in der Pizzeria die Telefonnummer des Kunden erfragt (oder von dem Display seines Telefons abliest) und diese in seinen Computer eingibt, dann bekommt er genau einen Kunden mit Name, Vorname und Adresse angezeigt, vorausgesetzt, der Kunde wurde schon einmal in die Datenbank eingetragen. Die Telefonnummer ist also eine Art Schlüssel für die Suche nach einem Datensatz, in diesem Fall die Kundendaten. Abstrakter bedeutet dies, dass wir über einen Schlüssel (key) Zugriff auf einen Wert (value) erhalten. Stellen wir uns die Repräsentation der Daten in dem Programm, das die Pizzeria benutzt, so vor: Telefonnummer Name Vorname PLZ Straße Müller Heinz Unistraße Schmidt Werner Grünweg Schultz Hans Arndtstraße Meier Franz Kirchweg Neumann Herbert Jägerallee Schröder Georg Mühlweg 2 Die Daten werden also in einer Tabelle gespeichert. Dabei entsprechen die Zeilen den Telefonnummern, die es in Bielefeld möglicherweise geben könnte, nämlich den achtstelligen Zahlen von bis wobei natürlich einige Zahlen, wie etwa die keine gültigen Telefonnummern sind und wir vereinfachend annehmen wollen, dass Telefonnummern, die weniger als acht Stellen haben, mit führenden Nullen aufgefüllt werden können. Bis hierher würde die Datentabelle also so aussehen wie in der obigen Abbildung mit 10 8 also 100 Millionen verschiedene Nummern, die jeweils einer Zeile in der Tabelle entsprechen. Hierbei fällt schon auf, dass diese Zahl natürlich viel zu groß ist, denn es gibt sicherlich nicht annähernd so viele Telefonanschlüsse in Bielefeld. Die Menge der möglichen Schlüssel, also die Zahlen bis ist gegenüber der Zahl der tatsächlich informativen Einträge viel zu groß, so dass eine große Menge Speicher für Telefonnummern verbraucht wird, die nie zugeordnet werden. Weiterhin ist zu bedenken, dass nicht jeder Einwohner Bielefelds eine Telefonnummer hat und dass nicht alle, die eine Nummer haben, beim Pizza-Service bestellen, und wenn sie doch eine Pizza bestellen, nicht unbedingt bei dieser Pizzeria. Gehen wir also davon aus, dass von der Menge aller Schlüssel nur ein kleiner Teil wirklichen Datenbankeinträgen entspricht. Bielefeld hat ca Einwoh- 128

137 9.2 Allgemeine Definitionen ner, dann gibt es vielleicht Telefonnummern. Davon bestellt jeder fünfte eine Pizza bleiben potentielle Einträge, verteilt auf mehrere Pizza- Lieferservices. Optimistisch geschätzt wird unsere Pizzeria also ca Kunden haben. Damit bleiben von den 100 Millionen Schlüsseln nur tatsächlich benutzte übrig, das sind 0,01 Prozent. Dies bedeutet für die Tabelle der Kundendaten, dass sie erheblich verkleinert werden kann nämlich auf Zeilen, denn mit mehr Kunden braucht die Pizzeria nicht zu rechnen. Da stellt sich folgende Frage: Wir wissen doch gar nicht, welche Telefonnummern bestellen werden wie sollen denn dann die Zeilen benannt werden? Unsere Aufgabe ist es, alle 100 Millionen Telefonnummern (denn jede einzelne könnte ja theoretisch bestellen) so abzubilden, dass sie in eine Zeilen große Tabelle passen. Hierzu machen wir uns jetzt eine mathematische Operation zunutze, die Modulo-Operation: x mod y liefert als Ergebnis den Rest der ganzzahligen Division x/y. Beispielsweise ergibt 117 mod 20 = 17, da 117 = Wenn wir jetzt jede angegebene Telefonnummer modulo nehmen, bekommen wir ein Ergebnis zwischen 0 und Somit können wir alle theoretischen Telefonnummern und damit Pizza-Besteller in einer Tabelle abbilden, die gerade mal Zeilen hat. Die Funktion, die wir dazu benutzen, lautet: oder allgemein: h(telefonnummer) = Telefonnummer mod Tabellenlänge, h(k) = k mod m mit h für Hashfunktion, k für key und m für Tabellenlänge. Wir benutzen also diese Hashfunktion, um jedem Schlüssel einen Index (hier eine Zahl zwischen 0 und 9999) in einer verkleinerten Tabelle (der sogenannten Hashtabelle) zuzuordnen und damit eine Menge Platz zu sparen. Leider kann es allerdings passieren, dass in ungünstigen Fällen zwei oder mehr Schlüssel (Telefonnummern) auf denselben Index in der Hashtabelle abgebildet werden, z.b. ist mod = mod = Wie solche Kollisionen behandelt werden können, wird im übernächsten Abschnitt diskutiert. 9.2 Allgemeine Definitionen Formal gesehen ist Hashing ein abstrakter Datentyp, der die Operationen insert, delete und search auf (dynamischen) Mengen effizient unterstützt. Hashing ist im Durchschnitt sehr effizient unter vernünftigen Bedingungen werden obige Operationen in O(1) Zeit ausgeführt (im worst-case kann search O(n) 129

138 9 Hashing U (Universum der Schlüssel) K (Aktuelle Schlüssel) T ABBILDUNG 9.1: Direkte Adressierung Zeit benötigen). In unserer Darstellung folgen wir Cormen et al. [2]. Zur Vereinfachung nehmen wir an, dass die Daten keine weiteren Komponenten enthalten (d.h. mit den Schlüsseln übereinstimmen). Wenn die Menge U aller Schlüssel relativ klein ist, können wir sie injektiv auf ein Feld abbilden; dies nennt man direkte Adressierung (Abbildung 9.1). Ist die Menge U aller Schlüssel aber sehr groß (wie im obigen Beispiel des Pizza- Services), so können wir nicht mehr direkt adressieren. Unter der Voraussetzung, dass die Menge K aller Schlüssel, die tatsächlich gespeichert werden, relativ klein ist gegenüber U, kann man die Schüssel effizient in einer Hashtabelle abspeichern. Dazu verwendet man allgemein eine Hashfunktion h : U {0, 1,..., m 1, die Schlüssel abbildet auf Werte zwischen 0 und m 1 (dabei ist m die Größe der Hashtabelle). Dies illustriert Abbildung 9.2. Beispiel Eine typische Hashfunktion h für U = N ist h(k) = k mod m. 130

139 9.3 Strategien zur Behandlung von Kollisionen 0.. U (Universum der Schlüssel) k 1 k 4 K (Aktuelle Schlüssel) k 2 k 5 k T..... h(k 1 ) h(k 4 ) h(k 2 ) = h(k 5 ) h(k 3 ) m 1 ABBILDUNG 9.2: Eine Hashfunktion h weist Schlüsseln ihren Platz in der Hashtabelle T zu. Dabei sollte m eine Primzahl sein, die nicht (zu) nahe an Potenzen von 2 liegt. Der Grund dafür wird in Cormen et al. [2], S. 228, erläutert. Da Hashfunktionen nicht injektiv sind, tritt das Problem der Kollision auf: zwei Schlüsseln wird der gleiche Platz in der Hashtabelle zugewiesen. Kollisionen sind natürlich unvermeidbar, jedoch wird eine gute Hashfunktion h die Anzahl der Kollisionen gering halten. D.h. h muss die Schlüssel gleichmäßig auf die Hashtabelle verteilen. Außerdem sollte h einfach zu berechnen sein. In Cormen et al. [2] findet man weitere Erläuterungen zum Thema Hashfunktionen. 9.3 Strategien zur Behandlung von Kollisionen Direkte Verkettung Man kann Kollisionen auflösen, indem jeder Tabellenplatz einen Zeiger auf eine verkettete Liste enthält. Schlüssel mit demselben Hashwert werden in dieselbe Liste eingetragen (Abbildung 9.3). 131

140 9 Hashing. k 4 k k 5 k 2 k 2 k 5 ABBILDUNG 9.3: Direkte Verkettung Damit ergeben sich folgende worst-case Laufzeiten: insert: O(1) (neues Element vor die Liste hängen) search: O(n) (n = Länge der Liste) delete: O(1) (vorausgesetzt, man verwendet doppelt verkettete Listen und hat das Element schon gefunden) Open Hashing Beim Open Hashing werden alle Einträge in der Hashtabelle gehalten. Ist eine Komponente der Tabelle schon belegt, so wird ein freier Platz für einen weiteren Eintrag gesucht. k 4 k 5 k 2.. ABBILDUNG 9.4: Open Hashing mit linearer Verschiebung Es gibt u.a. folgende Strategien zur Suche eines freien Platzes: 1. Lineare Verschiebung 132

141 9.3 Strategien zur Behandlung von Kollisionen Falls h(k) bereits durch einen anderen Schlüssel besetzt ist, wird versucht, k in den Adressen h(k) + 1, h(k) + 2,... unterzubringen (Abbildung 9.4). Präziser gesagt, wird folgende Hashfunktion verwendet: h (k, i) = ( h(k) + i ) mod m mit i = 0, 1, 2,..., m 1. Unter der Annahme, dass jeder Tabellenplatz entweder einen Schlüssel oder NIL enthält, wenn der Tabellenplatz leer ist 1, können die Operationen insert und search wie folgt implementiert werden: Hash-Insert(T, k) 1 i 0 2 repeat 3 j h (k, i) 4 if T [j] = NIL then 5 T [j] k 6 return j 7 i i until i = m 9 error hash table overflow Hash-Search(T, k) 1 i 0 2 repeat 3 j h (k, i) 4 if T [j] = k then 5 return j 6 i i until T [j] = NIL or i = m 8 error NIL 2. Quadratische Verschiebung Es wird die Hashfunktion h (k, i) = (h(k) + c 1 i + c 2 i 2 ) mod m mit i = 0, 1, 2,..., m 1 verwendet. Dabei sind c 1, c 2 N und c 2 0 geeignete Konstanten (s. Cormen et al. [2]). 3. Double Hashing Die Hashfunktion h wird definiert durch h (k, i) = (h 1 (k) + i h 2 (k)) mod m mit i = 0, 1, 2,..., m 1, 1 Wenn die Schlüssel natürliche Zahlen sind, dann können wir z.b. den Wert 1 zur Markierung eines leeren Tabellenplatzes benutzen. 133

142 9 Hashing wobei h 1 und h 2 Hashfunktionen sind. Die Verschiebung wird dabei durch eine zweite Hashfunktion realisiert. D.h. es wird zweimal, also doppelt, gehasht. Beispiel Wir illustrieren Open Hashing mit linearer Verschiebung an einem Beispiel. Unsere Hashtabelle hat nur fünf Plätze und die Hashfunktion sei h(k) = k mod 5. Die Werte 0 und 5 seien bereits eingetragen insert insert delete d search Man erkennt: Gelöschte Felder müssen markiert werden, so dass ein Suchalgorithmus nicht abbricht, obwohl das Element doch in der Liste gewesen wäre. Natürlich kann in die gelöschten Felder wieder etwas eingefügt werden. Dieses Problem muss in der obigen Implementierung zusätzlich berücksichtigt werden. Der Ladefaktor α für eine Hashtabelle T ist definiert als n, wobei n die Anzahl m der gespeicherten Schlüssel und m die Kapazität der Tabelle sind. Theoretische Untersuchungen und praktische Messungen haben ergeben, dass der Ladefaktor einer Hashtabelle den Wert 0.8 nicht überschreiten sollte (d.h. die Hashtabelle darf höchstens zu 80% gefüllt werden). Ist der Ladefaktor 0.8, so treten beim Suchen im Durchschnitt 3 Kollisionen auf. Bei einem höheren Ladefaktor steigt die Zahl der Kollisionen rasch an. 9.4 Die Klasse Hashtable in Java Die Klasse java.util.hashtable implementiert alle Methoden der abstrakten Klasse java.util.dictionary (vgl. Aufgabe 6.4.4). Außerdem enthält Hashtable noch folgende Methoden 2 : public synchronized boolean containskey(object key) Es wird true zurückgegeben gdw. die Hashtabelle ein Element unter key verzeichnet hat. public synchronized boolean contains(object element) Gdw. das angegebene element ein Element der Hashtabelle ist, wird true 2 Das Voranstellen des Schlüsselworts synchronized bewirkt, dass eine Methode nicht in mehreren nebenläufigen Prozessen (threads) gleichzeitig mehrfach ausgeführt wird. Ansonsten könnte es zu Inkonsistenzen in der Hashtabelle kommen. 134

143 9.4 Die Klasse Hashtable in Java zurückgegeben. Diese Operation ist teurer als die containskey-methode, da Hashtabellen nur beim Suchen nach Schlüsseln effizient sind. public synchronized void clear() Alle Schlüssel in der Hashtabelle werden gelöscht. Wenn es keine Referenzen mehr auf die Elemente gibt, werden sie vom Garbage-Collector aus dem Speicher entfernt. public synchronized Object clone() Es wird ein Klon der Hashtabelle erzeugt. Die Elemente und Schlüssel selbst werden aber nicht geklont. Ein Hashtabellenobjekt wächst automatisch, wenn es zu stark belegt wird. Es ist zu stark belegt, wenn der Ladefaktor der Tabelle überschritten wird. Wenn eine Hashtabelle wächst, wählt sie eine neue Kapazität, die ungefähr das doppelte der aktuellen beträgt. Das Wählen einer Primzahl als Kapazität ist wesentlich für die Leistung, daher wird das Hashtabellenobjekt eine Kapazitätsangabe auf eine nahegelegene Primzahl anpassen. Die anfängliche Kapazität und der Ladefaktor kann von den Konstruktoren der Klasse Hashtable gesetzt werden: public Hashtable() Es wird eine neue, leere Hashtabelle mit einer voreingestellten Anfangskapazität von 11 und einem Ladefaktor von 0.75 erzeugt. public Hashtable(int initialcapacity) Eine neue, leere Hashtabelle mit der Anfangskapazität initialcapacity und dem Ladefaktor 0.75 wird generiert. public Hashtable(int initialcapacity, float loadfactor) Es wird eine neue, leere Hastabelle erzeugt, die eine Anfangskapazität der Größe initialcapacity und einen Ladefaktor von loadfactor besitzt. Der Ladefaktor ist eine Zahl zwischen 0.0 und 1.0 und definiert den Beginn eines rehashings der Tabelle in eine größere. Beispiel Um die Benutzung der Klasse Hashtable als Wörterbuch (Dictionary) zu demonstrieren, entwerfen wir zunächst eine Klasse Pair zur Speicherung von Name-Wert Paaren. Namen sind vom Typ String. Werte können beliebigen Typ haben, deshalb wird der Wert in einer Variable vom Typ Object abgelegt. class Pair { private String name; private Object value; public Pair(String name, Object value) { 135

144 9 Hashing this.name = name; this.value = value; public String name() { return name; public Object value() { return value; public Object value(object newvalue) { Object oldvalue = value; value = newvalue; return oldvalue; Der Name ist nur lesbar, denn er soll als Schlüssel in einer Hashtabelle benutzt werden. Könnte das Datenfeld für den Namen von außerhalb der Klasse modifiziert werden, so könnte der zugehörige Wert verlorengehen: Er würde immer noch unter dem alten Namen eingeordnet sein und nicht unter dem modifizierten Namen. Der Wert hingegen kann jederzeit verändert werden. Folgende Schnittstelle deklariert drei Methoden: eine, um einem Dic-Objekt ein neues Paar hinzuzufügen; eine, um herauszufinden, ob in einem Dic-Objekt bereits ein Paar mit gegebenem Namen enthalten ist; und eine, um ein Paar aus einem Dic-Objekt zu löschen. interface Dic { void add(pair newpair); Pair find(string pairname); Pair delete(string pairname); Hier nun das Beispiel einer einfachen Implementierung von Dic, die die Hilfsklasse java.util.hashtable verwendet: import java.util.hashtable; class DicImpl implements Dic { protected Hashtable pairtable = new Hashtable(); 136

145 9.5 Aufgaben public void add(pair newpair) { pairtable.put(newpair.name(), newpair); public Pair find(string name) { return (Pair) pairtable.get(name); public Pair delete(string name) { return (Pair) pairtable.remove(name); Der Initialisierer für pairtable erzeugt ein Hashtable-Objekt, um Paare zu speichern. Die Klasse Hashtable erledigt die meiste anfallende Arbeit. Sie verwendet die Methode hashcode des Objekts, um jedes ihr als Schlüssel übergebene Objekt einzuordnen. Wir brauchen keine explizite Hashfunktion bereitzustellen, denn String enthält schon eine geeignete hashcode-implementierung. Kommt ein neues Paar hinzu, wird das Pair-Objekt in einer Hashtabelle unter seinem Namen gespeichert. Wir können dann einfach die Hashtabelle benutzen, um Paare über deren Namen zu finden und zu entfernen. 9.5 Aufgaben Aufgabe Implementieren Sie in Java das Verfahren zum Open-Hashing. Beschränken Sie sich dabei auf eine Methode hashinsert zum Einfügen in die Hashtabelle. Verwenden Sie die primäre Hashfunktion h(k) = k mod m und die folgenden drei Verfahren zur Kollisionsbehandlung: (1) lineare Verschiebung; (2) quadratische Verschiebung mit c 1 = 1 und c 2 = 3; (3) double hashing mit h 1 (k) = k mod m und h 2 (k) = 1 + (k mod (m 1)). Verwenden Sie nun Ihre Implementierung, um die Schlüssel 10, 22, 31, 4, 15, 28, 17, 88, 59 in eine Hashtabelle der Größe m = 11 einzutragen. Geben Sie die Hashtabelle nach dem Einfügen jedes Schlüssels an. Aufgabe Sei m die Größe einer Hashtabelle. Wir definieren die Hashfunktion hstring, die für einen String s einen ganzzahligen Wert hstring(m, s) liefert, wie folgt: hstring(m, s) = hstring (0, s) mod m. 137

146 9 Hashing Dabei ist hstring in Haskell-ähnlicher Notation wie folgt definiert: hstring(i, []) = i und hstring (i, a : w) = hstring (i 31 + ord(a), w) wobei ord eine Abbildung ist, die für ein Zeichen aus dem ASCII-Alphabet eine eindeutige ganze Zahl zwischen 0 und 255 liefert, nämlich ihren ASCII-Wert. 1. Implementieren Sie die Hashfunktion hstring in Java. hstring sollte dabei iterativ implementiert werden. Die Summen zur Berechnung des Hashwertes wachsen sehr schnell. Verwenden Sie daher long-werte zur Summation, um einen Überlauf zu vermeiden. 2. Erstellen Sie sich eine Datei strings mit 1000 ASCII-Zufallssequenzen der Länge 8. Wenden Sie hstring auf jede dieser Sequenzen an. Wählen Sie dabei nacheinander in verschiedenen Programmläufen m {67, 101, 113, Eine gute Hashfunktion wird die 1000 Strings gleichmäßig auf die m Einträge in der Hashtabelle abbilden, d.h. im Idealfall werden etwa 1000/m Strings auf jeden Eintrag abgebildet. Bestimmen Sie die Güte der Hashfunktion hstring, indem Sie g(m, hstring) = 1 m m 1 i=1 1000/m occ(i) berechnen. Dabei ist occ(i) die Anzahl der Strings w mit hstring(m, w) = i. Welcher Wert ergibt sich für die Güte g(m, hstring) für m {67, 101, 113, 197? 138

147 10 Ein- und Ausgabe In den bisherigen Programmbeispielen wurden die Benutzereingaben immer über die Kommandozeile und Ausgaben immer durch Aufruf der Methode System.out.println realisiert. Tatsächlich sind die Ein- und Ausgabemöglichkeiten von Java weit mächtiger. Dieses Kapitel behandelt die Grundlagen hiervon, Ströme und einige Beispiele für deren Verwendung Ströme Input und Output (kurz I/O) in Java basieren auf Strömen (engl. streams). Sie sind geordnete Folgen von Daten, die einen Ursprung (input stream) oder ein Ziel (output stream) haben. Vorteil: Der Programmierer wird von den spezifischen Details des zugrundeliegenden Betriebssystems befreit, indem Zugriffe auf Systemressourcen einheitlich mittels Strömen möglich sind. Die Standard-Ströme eines Java-Programms sind in Abbildung 10.1 dargestellt. Standard Eingabestrom System.in Java Programm Standard Ausgabestrom System.out Standard Fehlerstrom System.err ABBILDUNG 10.1: Standardströme Das Paket java.io definiert abstrakte Klassen für grundlegende Ein- und Ausgabeströme. Diese abstrakten Klassen werden dann erweitert und liefern mehrere nützliche Stromtypen. Derzeit enthält das I/O-Paket 13 Unterklassen, die teilweise wiederum Unterklassen mit weiteren Unterklassen besitzen (Abbildung 10.2). 139

148 10 Ein- und Ausgabe Object InputStream OutputStream FileInputStream FileOutputStream FilterInputStream DataInputStream... FilterOutputStream 3 PrintStream DataOutputStream... BufferedInputStream BufferedOutputStream Die Klasse InputStream ABBILDUNG 10.2: I/O-Klassenhierarchie Alle InputStream erweiternden Klassen können Bytes aus verschiedenen Quellen einlesen. Das Einlesen kann byteweise oder in Blöcken von Bytes beliebiger Größe durchgeführt werden. Wir listen hier nur die Methoden von InputStream auf und gehen nicht detailliert auf deren Unterklassen ein. public abstract int read() throws IOException; Es wird das nächste Byte aus dem Strom gelesen und (als Integerzahl im Bereich von 0 bis 255) zurückgegeben. Bei Erreichen des Stromendes wird 1 zurückgegeben. public int read(byte b[]) throws IOException; Es werden b.length Bytes aus dem Strom gelesen und in dem Feld b abgelegt. Der Rückgabewert ist die tatsächliche Anzahl der gelesenen Bytes oder bei Erreichen des Stromendes 1. public int read(byte b[], int off, int len) throws IOException; Es werden len Bytes gelesen und ab der Position off im Feld b abgelegt. Die Anzahl der gelesenen Bytes wird wieder zurückgegeben. public long skip(long n) throws IOException; Die nächsten n Bytes werden überlesen. 140

149 10.1 Ströme public int available() throws IOException; Es wird die Anzahl der Bytes zurückgegeben, die von dem Strom noch gelesen werden können. public void close() throws IOException; Der Strom wird geschlossen und die benutzten Ressourcen sofort wieder freigegeben. public boolean marksupported(); Es wird geprüft, ob der Strom die Methoden mark und reset unterstützt. public synchronized void mark(int readlimit); Die aktuelle Position im Strom wird markiert. public synchronized void reset() throws IOException; Es wird zu der Position zurückgesprungen, die mit mark markiert wurde. Analog zu InputStream und seinen Unterklassen gibt es die Klasse OutputStream und deren Erweiterungen, die zum Schreiben von Zeichen benötigt werden File-Ströme Um Dateien einlesen zu können, reicht es, ein FileInputStream-Objekt anzulegen. Beispiel import java.io.*; class ReadDemo { public static void main(string[] args) throws FileNotFoundException, IOException { FileInputStream fileinputstream = new FileInputStream(args[0]); int ch, count=0; while((ch = fileinputstream.read())!= -1) { count++; System.out.println("Die Datei enthält "+count+" Bytes."); Ausgaben werden durch die Klasse FileOutputStream ermöglicht, z.b. FileOutStream fos = new FileOuputStream("out.txt"); fos.write( a ); 141

150 10 Ein- und Ausgabe Gepufferte Ströme Die Klassen BufferedInputStream und BufferedOutputStream unterstützen Objekte, die ihre Daten puffern. Damit verhindern diese, dass jedes Lesen oder Schreiben sofort an den nächsten Strom weitergegeben wird. Diese Klassen werden oft in Verbindung mit File-Strömen verwendet, denn es ist relativ ineffizient, auf eine Datei zuzugreifen, die auf der Festplatte gespeichert ist. Das Puffern hilft, diesen Aufwand erheblich zu reduzieren. Ein BufferedInputStream oder BufferedOutputStream entspricht in seinen Methoden dem InputStream bzw. OutputStream. Die Konstruktoren nehmen einen entsprechenden Strom als Parameter. Bei einem Methodenaufruf werden die Daten gepuffert und bei einem vollen Puffer mit den entsprechenden Methoden des Stroms geschrieben bzw. gelesen. Manuell lässt sich ein Puffer auch immer mit der Methode public void flush() leeren. Beispiel import java.io.*; import java.util.*; class WriteDemo { private static float time(outputstream os, long j) throws IOException { Date start = new Date(); for(int i=0; i<j; i++) { os.write(1); os.close(); Date end = new Date(); return (float)(end.gettime()-start.gettime()); public static void main(string[] args) throws IOException { FileOutputStream unbufstream; BufferedOutputStream bufstream; long iterate; iterate = Long.parseLong(args[0]); unbufstream = new FileOutputStream("f.unbuffered"); bufstream = new BufferedOutputStream( new FileOutputStream("f.buffered")); float t1 = time(unbufstream,iterate); 142

151 10.1 Ströme System.out.println("Write file unbuffered: "+t1+" ms"); float t2 = time(bufstream,iterate); System.out.println("Write file buffered: "+t2+" ms"); System.out.println("Thus, the version with buffered streams is "+ (t1/t2)+" times faster."); > java WriteDemo Write file unbuffered: ms Write file buffered: 4.0 ms Thus, the version with buffered streams is 25.5 times faster. > java WriteDemo Write file unbuffered: ms Write file buffered: 14.0 ms Thus, the version with buffered streams is times faster Datenströme Man stellt bei häufigem Gebrauch von Strömen schnell fest, dass Byteströme allein kein Format bieten, in das alle Daten eingezwängt werden können. Vor allem die elementaren Datentypen von Java können in den bisher behandelten Strömen weder gelesen noch geschrieben werden. Die Klassen DataInputStream und DataOutputStream definieren Methoden zum Lesen und Schreiben, die komplexe Datenströme unterstützen. FileInputStream fis = new FileInputStream(args[0]); BufferedInputStream bis = new BufferedInputStream(fis); DataInputStream dis = new DataInputStream(bis); oder als ein Befehl DataInputStream dis = new DataInputStream( new BufferedInputStream( new FileInputStream(args[0]))); Beispiel import java.io.*; class DataIODemo { public static void main(string[] args) throws FileNotFoundException, 143

152 10 Ein- und Ausgabe IOException { DataOutputStream dataoutputstream = new DataOutputStream(new FileOutputStream(args[0])); double[] doubles = {16.57,15.44,9.99; for(int i=0; i<doubles.length; i++) { dataoutputstream.writedouble(doubles[i]); dataoutputstream.close(); DataInputStream dis = new DataInputStream(new FileInputStream(args[0])); double sum = 0.0; try { while(true) { //Read next double until EOF is reached and an EOFException //is thrown; add the double to sum sum += dis.readdouble(); catch(eofexception e) { System.out.println("Sum of doubles: "+sum); finally { dis.close(); > java DataIODemo test.dat Sum of doubles : Stream Tokenizer Das Zerlegen von Eingabedaten in Token ist ein häufiges Problem. Java stellt eine Klasse StreamTokenizer für solche einfachen lexikalischen Analysen zur Verfügung. Dabei wird gegenwärtig nur mit den untersten 8 Bits von Unicode, den Zeichen aus dem Latin-1 Zeichensatz, gearbeitet, da das interne, die Zeichentypen markierende, Feld nur 256 Elemente hat. 144

153 10.2 Stream Tokenizer Erkennt nexttoken() ein Token, gibt es den Tokentyp als seinen Wert zurück und setzt das Datenfeld ttype auf denselben Wert. Es gibt vier Tokentypen: TT_WORD: Ein Wort wurde eingescannt. Das Datenfeld sval vom Typ String enthält das gefundene Wort. TT_NUMBER: Eine Zahl wurde eingescannt. Das Datenfeld nval vom Typ double enthält den Wert der Zahl. Nur dezimale Gleitkommazahlen (mit oder ohne Dezimalpunkt) werden erkannt. Die Analyse versteht weder 3.4e79 als Gleitkommazahl noch 0xffff als Hexadezimalzahl. TT_EOL: Ein Zeilenende wurde gefunden (nur wenn eolissignificant true ist). TT_EOF: Das Eingabeende wurde erreicht. Beispiel import java.io.*; class TokenDemo { public static void main(string[] args) throws IOException { FileReader filein = new FileReader(args[0]); StreamTokenizer st = new StreamTokenizer(fileIn); int words = 0; // total word number. int numbers = 0; // total int number. int eols = 0; // total eols. int others = 0; // total others. st.eolissignificant(true); // to treat eols as a character while(st.nexttoken()!= StreamTokenizer.TT_EOF) { // to read next Token and check EOF if(st.ttype == StreamTokenizer.TT_WORD) words++; // if word then total words++. else if(st.ttype == StreamTokenizer.TT_NUMBER) numbers++; // if number then total numbers++. else if(st.ttype == StreamTokenizer.TT_EOL) eols++; else others++; // if eols total eols++. // else others++. System.out.println("File "+args[0]+" contains\n\t"+ words+" words,\n\t"+ 145

154 10 Ein- und Ausgabe numbers+" numbers,\n\t"+ eols+" end-of-lines, and\n\t"+ others+" other characters."); > java TokenDemo TokenDemo.java File TokenDemo.java contains 61 words, 6 numbers, 31 end-of-lines, and 88 other characters. 146

155 11 Graphische Benutzeroberflächen mit Swing Anmerkung: Dieses Kapitel des Skripts soll einen kleinen Einblick in die Programmierung von graphischen Benutzeroberflächen mit Java geben. Es erhebt keinen Anspruch auf Vollständigkeit. Literatur: [9, 10] Historisches Die erste API zur Generierung von grafischen Benutzeroberflächen für Java war das Abstract Window Toolkit (AWT). AWT bietet einen Satz von GUI Elementen, Ereignisbehandlung und einfache Funktionen. Jede grafische Komponente des AWT wird auf eine Komponente der darunter liegenden Plattform abgebildet. Auf Grund der Plattformunabhängigkeit Javas und der Verschiedenheit der unterstützten Plattformen beschränkt sich das AWT auf den kleinsten gemeinsamen Nenner an GUI Elementen, die in allen System vorhanden sind. Da jede AWT Komponente Resourcen vom nativen System bezieht und diese nicht von der Java Virtual Machine (JVM) verwaltet werden, spricht man bei AWT Komponenten von sogenannten Schwergewichtigen Komponenten (heavyweight components). Die später entwickelten und mit JDK 1.1 erstmals als Add-On verfügbaren Java Found Classes (JFC), auch Swing genannt, sind sogenannte Leichtgewichtige Komponenten (lightweight components). Swing Komponenten haben kein direktes Gegenstück im nativen System, sondern werden von Java verwaltet und gezeichnet. Der größte Vorteil von JFC Swing sind die Vielzahl von GUI Komponenten, die mit jedem Java Release anwachsen und einfach zu erweitern sind. (JFC/Swing ist im Gegensatz zu dem AWT vollständig OO implementiert) Ein Fenster zur Welt Den Einstieg in jedes graphische Programm ist ein Fenster, der sogenannte Top-Level Container. Ein Fenster kann mit der Swing Klasse JFrame erzeugt werden. 147

156 11 Graphische Benutzeroberflächen mit Swing Beispiel : import javax.swing.jframe; 02: public class HelloSwingFrame { 03: public static void main(string[] args) { 04: JFrame f = new JFrame("Das Fenster zur Welt!"); 05: f.setdefaultcloseoperation(jframe.exit_on_close); 06: f.setsize(300,200); 07: f.setvisible(true); 08: 09: ABBILDUNG 11.1: Screenshot: Ein Fenster zur Welt! 11.3 Swing Komponenten Die Klasse JComponent bildet die Basisklasse für alle Swing Komponenten und stellt viele grundlegende Funktionen zur Verfügung, wie z.b. die austauschbare Darstellung (Look&Feel), Tooltips, Rahmen. Die Klassen JWindow (Repräsentation eines Fenster) und JFrame sind von ihren jeweiligen AWT Penedants abgeleitet. Neben den im folgenden vorgestellten Komponenten JButton und JTextField gibt es eine (mit jedem Java Release wachsende) Anzahl von GUI Kompenten. Komponenten, die keine Container sind, also keine weiteren Komponenten enthalten können, heissen atomare Komponenten Eine einfache Schaltfläche - JButton Eine Schaltfläche ermöglicht es dem Anwender, eine Aktion auszulösen. Schaltflächen sind in der Regel beschriftet und/oder haben eine Grafik(Icon). Wie 148

157 11.3 Swing Komponenten ABBILDUNG 11.2: Klassenhierachie der grundlegendesten Swing Komponenten man der Klassenhierachie (siehe Abbildung 11.3) entnehmen kann, basieren alle Schaltflächen von Swing auf der gemeinsamen Oberklasse AbstractButton, welche die grundlegendsten Eigenschaften, die allen Schaltflächen gemein sind, implementiert. Beispiel ABBILDUNG 11.3: Klassenhierachie: Schaltflächen 01:import javax.swing.jbutton; 02:import javax.swing.jframe; 03: 04:public class Beispiel_JButton { 05: 06: public static void main(string [] args){ 07: JFrame f = new JFrame("Das Fenster zur Welt!"); 08: f.setdefaultcloseoperation(jframe.exit_on_close); 09: f.add(new JButton("Ich bin ein JButton!")); 10: f.setsize(300,200); 11: f.setvisible(true); 12: 13: 149

158 11 Graphische Benutzeroberflächen mit Swing ABBILDUNG 11.4: Screenshot: Eine einfache Schaltfläche - JButton Ein Texteingabefeld - JTextField Das Texteingabefeld (JTextField) von JFC/Swing ist die einfachste Möglichkeit kleinere (einzeilige) Mengen Text von Seiten des Benutzers (weiter-)zuverarbeiten. Wenn die Texteingabe abgeschlossen ist (durch Return/Enter) wird ein ActionEvent ausgelöst. Weitere Möglichkeiten der Textverarbeitung sind u.a. Labels (JLabel, für statische, durch den Benutzer unveränderbare Textinformationen) und Textflächen (JTextArea- mehrzeilige Benutzereingaben). ABBILDUNG 11.5: Klassenhierachie: Textkomponenten Beispiel :import javax.swing.jframe; 02:import javax.swing.jtextfield; 03: 04:public class Beispiel_JTextField { 05: 06: public static void main(string[] args) { 150

159 11.4 Container 07: JFrame f = new JFrame("Das Fenster zur Welt!"); 08: f.setdefaultcloseoperation(jframe.exit_on_close); 09: f.add(new JTextField("Ich bin ein JTexfield!",60)); 10: f.setsize(300,200); 11: f.setvisible(true); 12: 13: 14: ABBILDUNG 11.6: Screenshot: Ein Texteingabefeld - JTextfield 11.4 Container Alle Swing Komponenten müssen in einem Container plaziert werden. Container sind Komponenten, die dazu dienen, andere Komponenten aufzunehmen und zu verwalten. Der wichtigste und bekannteste Container neben dem grundlegenden JFrame ist das JPanel, das die ihm zugeordeneten JComponents nach einem zugewiesenen Layoutverfahren anordnet. Daneben gibt es noch JScrollPane, JTabbedPane, JSplitPane, JToolBar und viele mehr LayoutManager Ein LayoutManager ist dafür zuständig, die Komponenten eines Containers anzuordnen. Je nach Art des verwendeten LayoutManagers ist die Strategie der Anordung verschieden. Im folgenden werden die wichtigsten LayoutManager vorgestellt. Die Methode void setlayout(layoutmanager) weist einem Container einen LayoutManager zu. Ausser den hier kurz vorgestellten LayoutManagern FlowLayout, BorderLayout, GridLayout und BoxLayout gibt es noch die 151

160 11 Graphische Benutzeroberflächen mit Swing GridBagLayout, CardLayout, SpringLayout und GroupLayout Manager. Komplexe grafische Oberflächen lassen sich i.d.r. durch geschicktes Kombinieren der hier vorgestellten LayoutManager erreichen FlowLayout Der FlowLayout Manager ordnet die Komponenten von links nach rechts an, ohne die Grösse der Komponenten anzupassen und ist der StandardLayoutManager eines Containers. Beispiel :import javax.swing.jbutton; 02:import javax.swing.jframe; 03:import javax.swing.jpanel; 04: 05:public class Beispiel_FlowLayout extends JPanel{ 06: 07: public Beispiel_FlowLayout(){ 08: for(int i = 1; i <= 5; ++i){ 09: add(new JButton("Button "+(Math.pow(10, i)))); 10: 11: 12: 13: public static void main(string[] args) { 14: JFrame f = new JFrame("FlowLayout"); 15: f.add(new Beispiel_FlowLayout()); 16: f.pack(); 17: f.setvisible(true); 18: 19: 20: ABBILDUNG 11.7: Screenshot: FlowLayout 152

161 11.5 LayoutManager BorderLayout Der BorderLayout Manager ordnet die Komponenten nach den Himmelsrichtungen an (Norden, Osten, Süden, Westen, Mitte). Beispiel :import java.awt.borderlayout; 02:import javax.swing.jbutton; 03:import javax.swing.jframe; 04:import javax.swing.jpanel; 05: 06:public class Beispiel_BorderLayout extends JPanel{ 07: 08: public Beispiel_BorderLayout(){ 09: setlayout(new BorderLayout()); 10: add(new JButton("Norden"),BorderLayout.NORTH); 11: add(new JButton("Westen"),BorderLayout.WEST); 12: add(new JButton("Osten"),BorderLayout.EAST); 13: add(new JButton("S\"uden"),BorderLayout.SOUTH); 14: add(new JButton("Mitte"),BorderLayout.CENTER); 15: 16: 17: public static void main(string[] args) { 18: JFrame f = new JFrame("BorderLayout"); 19: f.add(new Beispiel_BorderLayout()); 20: f.pack(); 21: f.setvisible(true); 22: 23: ABBILDUNG 11.8: Screenshot: BorderLayout 153

162 11 Graphische Benutzeroberflächen mit Swing GridLayout Der GridLayout Manager ordnet die Komponenten in einem Raster (Gitter) gegebener Grösse an, die Ausmasse aller Komponenten sind gleich. Beispiel :import java.awt.gridlayout; 02:import javax.swing.jbutton; 03:import javax.swing.jframe; 04:import javax.swing.jpanel; 05: 06:public class Beispiel_GridLayout extends JPanel { 07: 08: public Beispiel_GridLayout(){ 09: setlayout(new GridLayout(3,3)); 10: for (int i = 9; i >= 1; --i){ 11: add(new JButton(new Integer(i).toString())); 12: 13: 14: 15: public static void main(string[] args) { 16: JFrame f = new JFrame("GridLayout"); 17: f.add(new Beispiel_GridLayout()); 18: f.pack(); 19: f.setvisible(true); 20: 21: ABBILDUNG 11.9: Screenshot: GridLayout 154

163 11.5 LayoutManager BoxLayout Der BoxLayout Manager ordnet die Komponenten in X bzw. Y Richtung an, die Aussmasse aller Komponenten sind gleich. Beispiel :import javax.swing.boxlayout; 02:import javax.swing.jbutton; 03:import javax.swing.jframe; 04:import javax.swing.jpanel; 05: 06:public class Beispiel_BoxLayout extends JPanel { 07: 08: public Beispiel_BoxLayout(){ 09: this(boxlayout.x_axis); 10: 11: 12: public Beispiel_BoxLayout(int direction){ 13: setlayout(new BoxLayout(this, direction)); 14: for(int i = 1; i <=5; ++i){ 15: add(new JButton(new Integer(i).toString())); 16: 17: 18: 19: public static void main(string[] args) { 20: JFrame f = new JFrame("BoxLayout"); 21: f.add(new Beispiel_BoxLayout(BoxLayout.Y_AXIS)); 22: f.pack(); 23: f.setvisible(true); 24: 25: ABBILDUNG 11.10: Screenshot: BoxLayout mit Stil BoxLayout.Y_AXIS 155

Java: Der Einstieg. Algorithmen und Datenstrukturen II 1

Java: Der Einstieg. Algorithmen und Datenstrukturen II 1 Java: Der Einstieg Algorithmen und Datenstrukturen II 1 Grundlegendes zu Java: Historisches 1990-1991: Entwicklung der Programmiersprache OAK durch James Gosling von Sun Microsystems (zunächst für Toaster,

Mehr

Algorithmen und Datenstrukturen II. Algorithmen und Datenstrukturen II 1

Algorithmen und Datenstrukturen II. Algorithmen und Datenstrukturen II 1 Algorithmen und Datenstrukturen II Algorithmen und Datenstrukturen II 1 Juniorprofessor Dr.-Ing. Tim W. Nattkemper Raum: M7-130 Sprechstunde: Di 13:00 ct - 14:00 Tel.: 0521/106-6059 Email: tnattkem@techfak.uni-bielefeld.de

Mehr

Imperative Programmierung in Java. Algorithmen und Datenstrukturen II 1

Imperative Programmierung in Java. Algorithmen und Datenstrukturen II 1 Imperative Programmierung in Java Algorithmen und Datenstrukturen II 1 Mini-Java Ein Mini-Java Programm besteht aus genau einer Klasse. In dieser Klasse gibt es genau eine main-methode. Folgende Konstrukte

Mehr

Kapitel 2. Java: Der Einstieg. 2.1 Grundlegendes zu Java. 2.1.1 Historisches. 2.1.2 Eigenschaften von Java

Kapitel 2. Java: Der Einstieg. 2.1 Grundlegendes zu Java. 2.1.1 Historisches. 2.1.2 Eigenschaften von Java Kapitel 2 Java: Der Einstieg Im vorigen Kapitel haben wir die Syntax der Sprache Mini-Java kennengelernt. Bevor wir in Kapitel 3 ausführlich auf Syntax und Semantik der Programmiersprache Java eingehen

Mehr

Imperative Programmierung in Java. Algorithmen und Datenstrukturen II 1

Imperative Programmierung in Java. Algorithmen und Datenstrukturen II 1 Imperative Programmierung in Java Algorithmen und Datenstrukturen II 1 Mini-Java Ein Mini-Java Programm besteht aus genau einer Klasse. In dieser Klasse gibt es genau eine main-methode. Folgende Konstrukte

Mehr

Java: Der Einstieg. Algorithmen und Datenstrukturen II 1

Java: Der Einstieg. Algorithmen und Datenstrukturen II 1 Java: Der Einstieg Algorithmen und Datenstrukturen II 1 Grundlegendes zu Java: Historisches 1990-1991: Entwicklung der Programmiersprache OAK durch James Gosling von Sun Microsystems (zunächst für Toaster,

Mehr

Algorithmen und Datenstrukturen II

Algorithmen und Datenstrukturen II Syntax und Semantik Java: Der Einstieg Imperative Programmierung in Java Algorithmen zur exakten Suche in Texten Objektori Algorithmen und Datenstrukturen II AG Praktische Informatik Technische Fakultät

Mehr

Algorithmen und Datenstrukturen II

Algorithmen und Datenstrukturen II Algorithmen und Datenstrukturen II AG Praktische Informatik Technische Fakultät Vorlesung Sommer 2009 Teil I Imperative Programmierung in Java Mini-Java Ein Mini-Java Programm besteht aus genau einer Klasse.

Mehr

Imperative Programmierung in Java

Imperative Programmierung in Java Kapitel 3 Imperative Programmierung in Java Im vorigen Kapitel haben wir generelle Eigenschaften der imperativen wie der objektorientierten Programmierung kennengelernt. Auch Teile der Syntax der objektorientierten

Mehr

Repetitorium Informatik (Java)

Repetitorium Informatik (Java) Repetitorium Informatik (Java) Tag 6 Lehrstuhl für Informatik 2 (Programmiersysteme) Übersicht 1 Klassen und Objekte Objektorientierung Begrifflichkeiten Deklaration von Klassen Instanzmethoden/-variablen

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java Vorlesung vom 18.4.07, Grundlagen Übersicht 1 Kommentare 2 Bezeichner für Klassen, Methoden, Variablen 3 White Space Zeichen 4 Wortsymbole 5 Interpunktionszeichen 6 Operatoren 7 import Anweisungen 8 Form

Mehr

Java Einführung Methoden in Klassen

Java Einführung Methoden in Klassen Java Einführung Methoden in Klassen Lehrziel der Einheit Methoden Signatur (=Deklaration) einer Methode Zugriff/Sichtbarkeit Rückgabewerte Parameter Aufruf von Methoden (Nachrichten) Information Hiding

Mehr

Einführung Datentypen Verzweigung Schleifen. Java Crashkurs. Kim-Manuel Klein May 4, 2015

Einführung Datentypen Verzweigung Schleifen. Java Crashkurs. Kim-Manuel Klein May 4, 2015 Java Crashkurs Kim-Manuel Klein (kmk@informatik.uni-kiel.de) May 4, 2015 Quellen und Editoren Internet Tutorial: z.b. http://www.java-tutorial.org Editoren Normaler Texteditor (Gedit, Scite oder ähnliche)

Mehr

Programmierung WS12/13 Lösung - Übung 1 M. Brockschmidt, F. Emmes, C. Otto, T. Ströder

Programmierung WS12/13 Lösung - Übung 1 M. Brockschmidt, F. Emmes, C. Otto, T. Ströder Prof. aa Dr. J. Giesl Programmierung WS12/13 M. Brockschmidt, F. Emmes, C. Otto, T. Ströder Tutoraufgabe 1 (Syntax und Semantik): 1. Was ist Syntax? Was ist Semantik? Erläutern Sie den Unterschied. 2.

Mehr

Java für Anfänger Teil 2: Java-Syntax. Programmierkurs Manfred Jackel

Java für Anfänger Teil 2: Java-Syntax. Programmierkurs Manfred Jackel Java für Anfänger Teil 2: Java-Syntax Programmierkurs 06.-10.10.2008 Manfred Jackel 1 Syntax für die Sprache Java public class Welcome { } Schlüsselworte Reservierte Worte Keywords Wortsymbol Syntax: griech.

Mehr

Beispiele für Ausdrücke. Der imperative Kern. Der imperative Kern. Imperativer Kern - Kontrollstrukturen. Deklarationen mit Initialisierung

Beispiele für Ausdrücke. Der imperative Kern. Der imperative Kern. Imperativer Kern - Kontrollstrukturen. Deklarationen mit Initialisierung Beispiele für Ausdrücke Der imperative Kern Deklarationen mit Initialisierung Variablendeklarationen int i = 10; int j = 15; Beispiele für Ausdrücke i+j i++ i & j i j [] [static]

Mehr

Java für Anfänger Teil 2: Java-Syntax. Programmierkurs Manfred Jackel

Java für Anfänger Teil 2: Java-Syntax. Programmierkurs Manfred Jackel Java für Anfänger Teil 2: Java-Syntax Programmierkurs 11.-15.10.2010 Manfred Jackel 1 Syntax für die Sprache Java public class Welcome { } Schlüsselworte Reservierte Worte Keywords Wortsymbol Syntax: griech.

Mehr

Primitive Datentypen

Primitive Datentypen Primitive Datentypen 2 Arten von Datentypen: primitive Datentypen (heute) Objekte (später) Java ist streng typisiert, d.h. für jede Variable muß angegeben werden was für eine Art von Wert sie aufnimmt.

Mehr

Java 8. Elmar Fuchs Grundlagen Programmierung. 1. Ausgabe, Oktober 2014 JAV8

Java 8. Elmar Fuchs Grundlagen Programmierung. 1. Ausgabe, Oktober 2014 JAV8 Java 8 Elmar Fuchs Grundlagen Programmierung 1. Ausgabe, Oktober 2014 JAV8 5 Java 8 - Grundlagen Programmierung 5 Kontrollstrukturen In diesem Kapitel erfahren Sie wie Sie die Ausführung von von Bedingungen

Mehr

Java Einführung VARIABLEN und DATENTYPEN Kapitel 2

Java Einführung VARIABLEN und DATENTYPEN Kapitel 2 Java Einführung VARIABLEN und DATENTYPEN Kapitel 2 Inhalt dieser Einheit Variablen (Sinn und Aufgabe) Bezeichner Datentypen, Deklaration und Operationen Typenumwandlung (implizit/explizit) 2 Variablen

Mehr

Grundlagen der Programmierung Teil1 Einheit III Okt. 2010

Grundlagen der Programmierung Teil1 Einheit III Okt. 2010 Grundlagen der Programmierung Teil1 Einheit III - 22. Okt. 2010 GDP DDr. Karl D. Fritscher basierend auf der Vorlesung Grundlagen der Programmierung von DI Dr. Bernhard Pfeifer Einschub Bevor wir mit den

Mehr

Arbeitsblätter für die Lehrveranstaltung OOP JAVA 1

Arbeitsblätter für die Lehrveranstaltung OOP JAVA 1 Fachhochschule Stralsund Fachbereich Maschinenbau Lehrgebiet Informatik Prof. Dr.-Ing. Ch.Wahmkow Arbeitsblätter für die Lehrveranstaltung OOP I. Aufbau eines Java-Programmes JAVA 1 Escape-Sequenzen zur

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 34 Einstieg in die Informatik mit Java Klassen mit Instanzmethoden Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 34 1 Definition von Klassen 2 Methoden 3 Methoden

Mehr

Algorithmen und Programmierung II

Algorithmen und Programmierung II Algorithmen und Programmierung II Vererbung Prof. Dr. Margarita Esponda SS 2012 1 Imperative Grundbestandteile Parameterübergabe String-Klasse Array-Klasse Konzepte objektorientierter Programmierung Vererbung

Mehr

Programmieren II. Innere Klassen. Heusch 10, Ratz 5.2.1, Institut für Angewandte Informatik

Programmieren II. Innere Klassen. Heusch 10, Ratz 5.2.1, Institut für Angewandte Informatik Programmieren II Innere Klassen Heusch 10, 13.10 Ratz 5.2.1, 9.8 KIT Die Forschungsuniversität in der Helmholtz-Gemeinschaft www.kit.edu Innere Klassen Bisher kennen wir nur Klassen, die entweder zusammen

Mehr

Datenbankanwendungsprogrammierung Crashkurs Java

Datenbankanwendungsprogrammierung Crashkurs Java Datenbankanwendungsprogrammierung Crashkurs Java Denny Priebe Datenbankanwendungsprogrammierung p. Unterschiede zu C, C++ typedefs, Präprozessor Strukturen, Unions globale Funktionen Mehrfachvererbung

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 25 Einstieg in die Informatik mit Java Objektorientierte Programmierung und Klassen Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 25 1 Die Philosophie 2 Definition

Mehr

Kapitel 4. Programmierkurs. Datentypen. Arten von Datentypen. Wiederholung Kapitel 4. Birgit Engels, Anna Schulze WS 07/08

Kapitel 4. Programmierkurs. Datentypen. Arten von Datentypen. Wiederholung Kapitel 4. Birgit Engels, Anna Schulze WS 07/08 Kapitel 4 Programmierkurs Birgit Engels, Anna Schulze Wiederholung Kapitel 4 ZAIK Universität zu Köln WS 07/08 1 / 23 2 Datentypen Arten von Datentypen Bei der Deklaration einer Variablen(=Behälter für

Mehr

Probeklausur: Programmierung WS04/05

Probeklausur: Programmierung WS04/05 Probeklausur: Programmierung WS04/05 Name: Hinweise zur Bearbeitung Nimm Dir für diese Klausur ausreichend Zeit, und sorge dafür, dass Du nicht gestört wirst. Die Klausur ist für 90 Minuten angesetzt,

Mehr

JAVA - Methoden

JAVA - Methoden Übungen Informatik I JAVA - http://www.fbi-lkt.fh-karlsruhe.de/lab/info01/tutorial Übungen Informatik 1 Folie 1 sind eine Zusammenfassung von Deklarationen und Anweisungen haben einen Namen und können

Mehr

Werkzeuge zur Programmentwicklung

Werkzeuge zur Programmentwicklung Werkzeuge zur Programmentwicklung B-15 Bibliothek Modulschnittstellen vorübersetzte Module Eingabe Editor Übersetzer (Compiler) Binder (Linker) Rechner mit Systemsoftware Quellmodul (Source) Zielmodul

Mehr

Java: Eine kurze Einführung an Beispielen

Java: Eine kurze Einführung an Beispielen Java: Eine kurze Einführung an Beispielen Quellcode, javac und die JVM Der Quellcode eines einfachen Java-Programms besteht aus einer Datei mit dem Suffix.java. In einer solchen Datei wird eine Klasse

Mehr

Elementare Konzepte von

Elementare Konzepte von Elementare Konzepte von Programmiersprachen Teil 2: Anweisungen (Statements) Kapitel 6.3 bis 6.7 in Küchlin/Weber: Einführung in die Informatik Anweisungen (statements) in Java Berechnung (expression statement)

Mehr

Syntax von Programmiersprachen

Syntax von Programmiersprachen "Grammatik, die sogar Könige zu kontrollieren weiß... aus Molière, Les Femmes Savantes (1672), 2. Akt Syntax von Programmiersprachen Prof. Dr. Christian Böhm in Zusammenarbeit mit Gefei Zhang WS 07/08

Mehr

JavaScript. Dies ist normales HTML. Hallo Welt! Dies ist JavaScript. Wieder normales HTML.

JavaScript. Dies ist normales HTML. Hallo Welt! Dies ist JavaScript. Wieder normales HTML. JavaScript JavaScript wird direkt in HTML-Dokumente eingebunden. Gib folgende Zeilen mit einem Texteditor (Notepad) ein: (Falls der Editor nicht gefunden wird, öffne im Browser eine Datei mit der Endung

Mehr

TEIL I: OBJEKTORIENTIERUNG UND GRUNDKURS JAVA GRUNDLAGEN DER PROGRAMMIERUNG... 4

TEIL I: OBJEKTORIENTIERUNG UND GRUNDKURS JAVA GRUNDLAGEN DER PROGRAMMIERUNG... 4 Inhaltsverzeichnis TEIL I: OBJEKTORIENTIERUNG UND GRUNDKURS JAVA... 1 1 GRUNDLAGEN DER PROGRAMMIERUNG... 4 1.1 Das erste Java-Programm... 4 1.2 Programme und ihre Abläufe... 6 1.3 Entwurf mit Nassi-Shneiderman-Diagrammen...

Mehr

Javakurs FSS Lehrstuhl Stuckenschmidt. Tag 3 - Objektorientierung

Javakurs FSS Lehrstuhl Stuckenschmidt. Tag 3 - Objektorientierung Javakurs FSS 2012 Lehrstuhl Stuckenschmidt Tag 3 - Objektorientierung Warum Objektorientierung Daten und Funktionen möglichst eng koppeln und nach außen kapseln Komplexität der Software besser modellieren

Mehr

1. Der Begriff Informatik 2. Syntax und Semantik von Programmiersprachen. I.2. I.2. Grundlagen von von Programmiersprachen.

1. Der Begriff Informatik 2. Syntax und Semantik von Programmiersprachen. I.2. I.2. Grundlagen von von Programmiersprachen. 1. Der Begriff Informatik 2. Syntax und Semantik von Programmiersprachen I.2. I.2. Grundlagen von von Programmiersprachen. - 1 - 1. Der Begriff Informatik "Informatik" = Kunstwort aus Information und Mathematik

Mehr

Programmierkurs Java

Programmierkurs Java Programmierkurs Java Dr. Dietrich Boles Aufgaben zu UE3-Syntaxdiagramme (Stand 05.11.2010) Aufgabe 1: Entwickeln Sie Regeln zur Übersetzung von EBNF in Syntaxdiagramme. Aufgabe 2: Eine Zahl ist entweder

Mehr

Objektorientierte Programmierung

Objektorientierte Programmierung Objektorientierte Programmierung 1 Geschichte Dahl, Nygaard: Simula 67 (Algol 60 + Objektorientierung) Kay et al.: Smalltalk (erste rein-objektorientierte Sprache) Object Pascal, Objective C, C++ (wiederum

Mehr

RO-Tutorien 3 / 6 / 12

RO-Tutorien 3 / 6 / 12 RO-Tutorien 3 / 6 / 12 Tutorien zur Vorlesung Rechnerorganisation Christian A. Mandery WOCHE 2 AM 06./07.05.2013 KIT Universität des Landes Baden-Württemberg und nationales Forschungszentrum in der Helmholtz-Gemeinschaft

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 47 Einstieg in die Informatik mit Java Anweisungen Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 47 1 Ausdrucksanweisung 2 Einfache Ausgabeanweisung 3 Einfache Eingabeanweisung,

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 41 Einstieg in die Informatik mit Java Vererbung Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 41 1 Überblick: Vererbung 2 Grundidee Vererbung 3 Verdeckte Variablen

Mehr

Einführung Datentypen Verzweigung Schleifen Funktionen Dynamische Datenstrukturen. Java Crashkurs. Kim-Manuel Klein (kmk@informatik.uni-kiel.

Einführung Datentypen Verzweigung Schleifen Funktionen Dynamische Datenstrukturen. Java Crashkurs. Kim-Manuel Klein (kmk@informatik.uni-kiel. Java Crashkurs Kim-Manuel Klein (kmk@informatik.uni-kiel.de) May 7, 2015 Quellen und Editoren Internet Tutorial: z.b. http://www.java-tutorial.org Editoren Normaler Texteditor (Gedit, Scite oder ähnliche)

Mehr

3. Anweisungen und Kontrollstrukturen

3. Anweisungen und Kontrollstrukturen 3. Kontrollstrukturen Anweisungen und Blöcke 3. Anweisungen und Kontrollstrukturen Mit Kontrollstrukturen können wir den Ablauf eines Programmes beeinflussen, z.b. ob oder in welcher Reihenfolge Anweisungen

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 39 Einstieg in die Informatik mit Java Objektorientierte Programmierung und Klassen mit Instanzmethoden Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 39 1 Überblick:

Mehr

Javakurs für Anfänger

Javakurs für Anfänger Javakurs für Anfänger Einheit 04: Einführung in Kontrollstrukturen Lorenz Schauer Lehrstuhl für Mobile und Verteilte Systeme Heutige Agenda 1. Teil: Einführung in Kontrollstrukturen 3 Grundstrukturen von

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java Vorlesung vom 6.11.07, Weitere Anweisungen Übersicht 1 Verbundanweisung 2 Bedingte Anweisung 3 Auswahlanweisung 4 for Schleife 5 while Schleife 6 do Schleife 7 break Anweisung 8 continue Anweisung 9 Leere

Mehr

Das erste Programm soll einen Text zum Bildschirm schicken. Es kann mit jedem beliebigen Texteditor erstellt werden.

Das erste Programm soll einen Text zum Bildschirm schicken. Es kann mit jedem beliebigen Texteditor erstellt werden. Einfache Ein- und Ausgabe mit Java 1. Hallo-Welt! Das erste Programm soll einen Text zum Bildschirm schicken. Es kann mit jedem beliebigen Texteditor erstellt werden. /** Die Klasse hello sendet einen

Mehr

Kapitel 11: Wiederholung und Zusammenfassung

Kapitel 11: Wiederholung und Zusammenfassung Wiederholung und Zusammenfassung 1: Begriff und Grundprobleme der Informatik Begriff Informatik Computer als universelle Rechenmaschine Grenzen der Berechenbarkeit Digitalisierung Problem der Komplexität

Mehr

Gliederung. Tutorium zur Vorlesung. Gliederung. Gliederung. 1. Gliederung der Informatik. 1. Gliederung der Informatik. 1. Gliederung der Informatik

Gliederung. Tutorium zur Vorlesung. Gliederung. Gliederung. 1. Gliederung der Informatik. 1. Gliederung der Informatik. 1. Gliederung der Informatik Informatik I WS 2012/13 Tutorium zur Vorlesung 1. Alexander Zietlow zietlow@informatik.uni-tuebingen.de Wilhelm-Schickard-Institut für Informatik Eberhard Karls Universität Tübingen 11.02.2013 1. 2. 1.

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 16 Einstieg in die Informatik mit Java Innere Klassen Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 16 1 Einführung 2 Element-Klassen 3 Lokale Klassen 4 Anonyme Klassen

Mehr

Programmieren I + II Regeln der Code-Formatierung

Programmieren I + II Regeln der Code-Formatierung Technische Universität Braunschweig Dr. Werner Struckmann Institut für Programmierung und Reaktive Systeme WS 2016/2017, SS 2017 Programmieren I + II Regeln der Code-Formatierung In diesem Dokument finden

Mehr

Silke Trißl, Prof. Ulf Leser Wissensmanagement in der Bioinformatik. Jede Applikation braucht eine Klasse mit einer main-methode

Silke Trißl, Prof. Ulf Leser Wissensmanagement in der Bioinformatik. Jede Applikation braucht eine Klasse mit einer main-methode Methoden und Klassen Silke Trißl, Prof. Ulf Leser Wissensmanagement in der Bioinformatik Wiederholung Jede Applikation braucht eine Klasse mit einer main-methode Eintrittspunkt in das Programm Die main-methode

Mehr

Einführung in die Informatik. Programming Languages

Einführung in die Informatik. Programming Languages Einführung in die Informatik Programming Languages Beschreibung von Programmiersprachen Wolfram Burgard Cyrill Stachniss 1/15 Motivation und Einleitung Wir haben in den vorangehenden Kapiteln meistens

Mehr

Innere Klassen. Gerd Bohlender. Institut für Angewandte und Numerische Mathematik. Vorlesung: Einstieg in die Informatik mit Java

Innere Klassen. Gerd Bohlender. Institut für Angewandte und Numerische Mathematik. Vorlesung: Einstieg in die Informatik mit Java Innere Klassen Gerd Bohlender Institut für Angewandte und Numerische Mathematik Vorlesung: Einstieg in die Informatik mit Java 13.06.07 G. Bohlender (IANM UNI Karlsruhe) Innere Klassen 13.06.07 1 / 11

Mehr

Alphabet, formale Sprache

Alphabet, formale Sprache n Alphabet Alphabet, formale Sprache l nichtleere endliche Menge von Zeichen ( Buchstaben, Symbole) n Wort über einem Alphabet l endliche Folge von Buchstaben, die auch leer sein kann ( ε leere Wort) l

Mehr

Einfache Rechenstrukturen und Kontrollfluss II

Einfache Rechenstrukturen und Kontrollfluss II Einfache Rechenstrukturen und Kontrollfluss II Martin Wirsing in Zusammenarbeit mit Moritz Hammer und Axel Rauschmayer http://www.pst.informatik.uni-muenchen.de/lehre/ss06/infoii/ SS 06 Ziele Lernen imperative

Mehr

Tag 4 Repetitorium Informatik (Java)

Tag 4 Repetitorium Informatik (Java) Tag 4 Repetitorium Informatik (Java) Dozent: Patrick Kreutzer Lehrstuhl für Informatik 2 (Programmiersysteme) Friedrich-Alexander-Universität Erlangen-Nürnberg Wintersemester 2016/2017 Willkommen zum Informatik-Repetitorium!

Mehr

Hello World. Javakurs 2014, 1. Vorlesung. Sebastian Schuck. basierend auf der Vorlage von Arne Kappen. wiki.freitagsrunde.org. 3.

Hello World. Javakurs 2014, 1. Vorlesung. Sebastian Schuck. basierend auf der Vorlage von Arne Kappen. wiki.freitagsrunde.org. 3. Hello World Javakurs 2014, 1. Vorlesung Sebastian Schuck basierend auf der Vorlage von Arne Kappen wiki.freitagsrunde.org 3. März 2014 This work is licensed under the Creative Commons Attribution-ShareAlike

Mehr

Vorlesung Programmieren

Vorlesung Programmieren Vorlesung Programmieren 3. Kontrollstrukturen 04.11.2015 Prof. Dr. Ralf H. Reussner Version 1.1 LEHRSTUHL FÜR SOFTWARE-DESIGN UND QUALITÄT (SDQ) INSTITUT FÜR PROGRAMMSTRUKTUREN UND DATENORGANISATION (IPD),

Mehr

Die Programmiersprache C Eine Einführung

Die Programmiersprache C Eine Einführung Die Programmiersprache C Eine Einführung Christian Gentsch Fakutltät IV Technische Universität Berlin Projektlabor 2. Mai 2014 Inhaltsverzeichnis 1 Einführung Entstehungsgeschichte Verwendung 2 Objektorientiert

Mehr

Einführung in die Programmierung 1

Einführung in die Programmierung 1 Einführung in die Programmierung 1 Einführung (S.2) Einrichten von Eclipse (S.4) Mein Erstes Programm (S.5) Hallo Welt!? Programm Der Mensch (S.11) Klassen (S.12) Einführung Wie Funktioniert Code? Geschriebener

Mehr

Überblick und Wiederholung

Überblick und Wiederholung Annabelle Klarl Zentralübung zur Vorlesung Einführung in die Informatik: http://www.pst.ifi.lmu.de/lehre/wise-14-15/infoeinf WS14/15 Klausurinformationen 6 ECTS: Klausur 07.02.2015 10:15 12:15 Uhr (120

Mehr

Letztes Mal. static int ggt(int a, int b) { if (a == b) return a; else if (a > b) return ggt(a-b,b); else if (a < b) return ggt(a,b-a);

Letztes Mal. static int ggt(int a, int b) { if (a == b) return a; else if (a > b) return ggt(a-b,b); else if (a < b) return ggt(a,b-a); Letztes Mal static int ggt(int a, int b) { if (a == b) return a; else if (a > b) } return ggt(a-b,b); else if (a < b) return ggt(a,b-a); Darf hier nicht stehen! Compiler sagt: Missing return statement

Mehr

1. Referenzdatentypen: Felder und Strings. Referenz- vs. einfache Datentypen. Rückblick: Einfache Datentypen (1) 4711 r

1. Referenzdatentypen: Felder und Strings. Referenz- vs. einfache Datentypen. Rückblick: Einfache Datentypen (1) 4711 r 1. Felder und Strings Eigenschaften von Referenzdatentypen 1. Referenzdatentypen: Felder und Strings Referenzdatentypen sind Konstrukte, mit deren Hilfe wir aus einfachen Datentypen neue eigene Typen erzeugen

Mehr

Vorkurs Informatik WiSe 16/17

Vorkurs Informatik WiSe 16/17 Java Schleifen und Arrays Dr. Werner Struckmann / Stephan Mielke, Jakob Garbe, 06.10.2016 Technische Universität Braunschweig, IPS Überblick Kommentare Typen Kontrollstrukturen Arrays 06.10.2016 Dr. Werner

Mehr

Organisatorisches. drei Gruppen Gruppe 1: 10:10-11:40, Gruppe 2: 11:45-13:15 Gruppe 3: 13:20-14:50

Organisatorisches. drei Gruppen Gruppe 1: 10:10-11:40, Gruppe 2: 11:45-13:15 Gruppe 3: 13:20-14:50 Organisatorisches Vorlesung Donnerstag 8:35 bis 10:05 Übung drei Gruppen Gruppe 1: 10:10-11:40, Gruppe 2: 11:45-13:15 Gruppe 3: 13:20-14:50 Tutorium (Mehr oder weniger) abwechselnd Mo und Mi 10-11:30 Termine

Mehr

Klausur Grundlagen der Programmierung

Klausur Grundlagen der Programmierung Klausur Grundlagen der Programmierung Aufgabenstellung: Martin Schultheiß Erreichte Punktzahl: von 60 Note: Allgemeine Hinweise: Schreiben Sie bitte Ihren Namen auf jedes der Blätter Zugelassene Hilfsmittel

Mehr

Einführung in den Einsatz von Objekt-Orientierung mit C++ I

Einführung in den Einsatz von Objekt-Orientierung mit C++ I Einführung in den Einsatz von Objekt-Orientierung mit C++ I ADV-Seminar Leiter: Mag. Michael Hahsler Syntax von C++ Grundlagen Übersetzung Formale Syntaxüberprüfung Ausgabe/Eingabe Funktion main() Variablen

Mehr

Algorithmen & Programmierung. Steuerstrukturen im Detail Selektion und Iteration

Algorithmen & Programmierung. Steuerstrukturen im Detail Selektion und Iteration Algorithmen & Programmierung Steuerstrukturen im Detail Selektion und Iteration Selektion Selektion Vollständige einfache Selektion Wir kennen schon eine Möglichkeit, Selektionen in C zu formulieren: if

Mehr

Inhaltsverzeichnis. Einführende Bemerkungen 11. Das Fach Informatik 11 Zielsetzung der Vorlesung Grundbegriffe

Inhaltsverzeichnis. Einführende Bemerkungen 11. Das Fach Informatik 11 Zielsetzung der Vorlesung Grundbegriffe Inhaltsverzeichnis Einführende Bemerkungen 11 Das Fach Informatik 11 Zielsetzung der Vorlesung 12 1. Grundbegriffe 1 3 1.1 1.2 1.3 1.4 1.5 1.6 1.7 Information und Nachricht 1.1.1 Information 1.1.2 Nachricht

Mehr

Übersicht. Vorstellung des OO-Paradigmas

Übersicht. Vorstellung des OO-Paradigmas Java, OO und UML Vorstellung des OO-Paradigmas Übersicht Umsetzung des OO-Paradigmas in Java Einführung (seeeeeehr rudimenter) in UML zur graphischen Darstellung von OO Grammatik und Semantik von Java

Mehr

Einführung in die Programmierung mit VBA

Einführung in die Programmierung mit VBA Einführung in die Programmierung mit VBA Vorlesung vom 07. November 2016 Birger Krägelin Inhalt Vom Algorithmus zum Programm Programmiersprachen Programmieren mit VBA in Excel Datentypen und Variablen

Mehr

1. Der Einstieg in Java. Was heißt Programmieren?

1. Der Einstieg in Java. Was heißt Programmieren? 1. Der Einstieg in Java Lernziele: Am Ende dieses Kapitels sollen Sie wissen, aus welchen Bestandteilen ein Java-Programm besteht, Java-Programme übersetzen und ausführen können, Möglichkeiten der Kommentierung

Mehr

AuD-Tafelübung T-B5b

AuD-Tafelübung T-B5b 6. Übung Sichtbarkeiten, Rekursion, Javadoc Di, 29.11.2011 1 Blatt 5 2 OOP Klassen Static vs. Instanzen Sichtbarkeit 3 Stack und Heap Stack Heap 4 Blatt 6 1 Blatt 5 2 OOP Klassen Static vs. Instanzen Sichtbarkeit

Mehr

Kapitel 2. Methoden zur Beschreibung von Syntax

Kapitel 2. Methoden zur Beschreibung von Syntax 1 Kapitel 2 Methoden zur Beschreibung von Syntax Grammatik, die sogar Könige zu kontrollieren weiß... aus Molière, Les Femmes Savantes (1672), 2. Akt 2 Ziele Zwei Standards zur Definition der Syntax von

Mehr

Tutoraufgabe 1 (Zweierkomplement): Lösung: Programmierung WS16/17 Lösung - Übung 2

Tutoraufgabe 1 (Zweierkomplement): Lösung: Programmierung WS16/17 Lösung - Übung 2 Prof. aa Dr. J. Giesl Programmierung WS16/17 F. Frohn, J. Hensel, D. Korzeniewski Tutoraufgabe 1 (Zweierkomplement): a) Sei x eine ganze Zahl. Wie unterscheiden sich die Zweierkomplement-Darstellungen

Mehr

JAVA für Nichtinformatiker - Probeklausur -

JAVA für Nichtinformatiker - Probeklausur - JAVA für Nichtinformatiker - Probeklausur - Die folgenden Aufgaben sollten in 150 Minuten bearbeitet werden. Aufgabe 1: Erläutere kurz die Bedeutung der folgenden Java-Schlüsselwörter und gib Sie jeweils

Mehr

Wo sind wir? Kontrollstrukturen

Wo sind wir? Kontrollstrukturen Wo sind wir? Java-Umgebung Lexikale Konventionen Datentypen Kontrollstrukturen Ausdrücke Klassen, Pakete, Schnittstellen JVM Exceptions Java Klassenbibliotheken Ein-/Ausgabe Collections Threads Applets,

Mehr

Kontrollstrukturen. Wo sind wir? Anweisung mit Label. Block. Beispiel. Deklarationsanweisung

Kontrollstrukturen. Wo sind wir? Anweisung mit Label. Block. Beispiel. Deklarationsanweisung Java-Umgebung Lexikale Konventionen Datentypen Kontrollstrukturen Ausdrücke Klassen, Pakete, Schnittstellen JVM Exceptions Java Klassenbibliotheken Ein-/Ausgabe Collections Threads Applets, Sicherheit

Mehr

Kapitel 8. Programmierkurs. Methoden. 8.1 Methoden

Kapitel 8. Programmierkurs. Methoden. 8.1 Methoden Kapitel 8 Programmierkurs Birgit Engels Anna Schulze Zentrum für Angewandte Informatik Köln Objektorientierte Programmierung Methoden Überladen von Methoden Der this-zeiger Konstruktoren Vererbung WS 07/08

Mehr

Wie entwerfe ich ein Programm?

Wie entwerfe ich ein Programm? Wie entwerfe ich ein Programm? Welche Objekte brauche ich? Flussdiagramme für Programmablauf Vorcode Testcode Hauptcode Wir spielen Lotto! Borchers: Programmierung für Alle (Java), WS 06/07 Kapitel 5 +

Mehr

Übersicht. 4.1 Ausdrücke. 4.2 Funktionale Algorithmen. 4.3 Anweisungen. 4.4 Imperative Algorithmen Variablen und Konstanten. 4.4.

Übersicht. 4.1 Ausdrücke. 4.2 Funktionale Algorithmen. 4.3 Anweisungen. 4.4 Imperative Algorithmen Variablen und Konstanten. 4.4. Übersicht 4.1 Ausdrücke 4.2 Funktionale Algorithmen 4.3 Anweisungen 4.4 Imperative Algorithmen 4.4.1 Variablen und Konstanten 4.4.2 Prozeduren 4.4.3 Verzweigung und Iteration 4.4.4 Globale Größen Einführung

Mehr

Programmierung. Grundlagen. Tina Wegener, Ralph Steyer. 2. Ausgabe, 1. Aktualisierung, April 2014

Programmierung. Grundlagen. Tina Wegener, Ralph Steyer. 2. Ausgabe, 1. Aktualisierung, April 2014 Programmierung Tina Wegener, Ralph Steyer 2. Ausgabe, 1. Aktualisierung, April 2014 Grundlagen PG 6 Programmierung - Grundlagen 6 Grundlegende Sprachelemente In diesem Kapitel erfahren Sie was Syntax und

Mehr

Kapitel 3. Programmierkurs. Arten von Anweisungen. 3.1 Was sind Anweisungen?

Kapitel 3. Programmierkurs. Arten von Anweisungen. 3.1 Was sind Anweisungen? Kapitel 3 Programmierkurs Birgit Engels, Anna Schulze ZAIK Universität zu Köln Anweisungen, Variablen Arten von Anweisungen Variablen Konstanten Höchste Zeit für ein Programm Gültigkeitsbereich von Variablen

Mehr

Einführung in die Informatik I (autip)

Einführung in die Informatik I (autip) Einführung in die Informatik I (autip) Dr. Stefan Lewandowski Fakultät 5: Informatik, Elektrotechnik und Informationstechnik Abteilung Formale Konzepte Universität Stuttgart 24. Oktober 2007 Was Sie bis

Mehr

3.4 Struktur von Programmen

3.4 Struktur von Programmen 3.4 Struktur von Programmen Programme sind hierarchisch aus Komponenten aufgebaut. Für jede Komponente geben wir Regeln an, wie sie aus anderen Komponenten zusammengesetzt sein können. program ::= decl*

Mehr

Einführung in die Programmierung mit Java

Einführung in die Programmierung mit Java Einführung in die Programmierung mit Java Martin Wirsing 2 Ziele Geschichte der OO-Programmiersprachen Warum Java als Programmiersprache verwenden? Ein einfaches Java-Programm erstellen, übersetzen und

Mehr

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 12/13. Kapitel 3. Grunddatentypen, Ausdrücke und Variable

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 12/13. Kapitel 3. Grunddatentypen, Ausdrücke und Variable 1 Kapitel 3 Grunddatentypen, Ausdrücke und Variable 2 Eine Datenstruktur besteht aus Grunddatentypen in Java einer Menge von Daten (Werten) charakteristischen Operationen Datenstrukturen werden mit einem

Mehr

JAVA - Methoden - Rekursion

JAVA - Methoden - Rekursion Übungen Informatik I JAVA - Methoden - Rekursion http://www.fbi-lkt.fh-karlsruhe.de/lab/info01/tutorial Übungen Informatik 1 1 Methoden Methoden sind eine Zusammenfassung von Deklarationen und Anweisungen

Mehr

Einstieg in die Informatik mit Java

Einstieg in die Informatik mit Java 1 / 22 Einstieg in die Informatik mit Java Generics Gerd Bohlender Institut für Angewandte und Numerische Mathematik Gliederung 2 / 22 1 Überblick Generics 2 Generische Klassen 3 Generische Methoden 4

Mehr

1 Syntax von Programmiersprachen

1 Syntax von Programmiersprachen 1 Syntax von Programmiersprachen Syntax ( Lehre vom Satzbau ): formale Beschreibung des Aufbaus der Worte und Sätze, die zu einer Sprache gehören; im Falle einer Programmier-Sprache Festlegung, wie Programme

Mehr

Beispiel: Temperaturumwandlung. Imperative Programmierung. Schwerpunkte. 3. Grundlegende Sprachkonstruktionen imperativer Programme

Beispiel: Temperaturumwandlung. Imperative Programmierung. Schwerpunkte. 3. Grundlegende Sprachkonstruktionen imperativer Programme Schwerpunkte 3. Grundlegende Sprachkonstruktionen imperativer Programme Java-Beispiele: Temperature.java Keyboard.java Imperative Programmierung Beispiel für ein Programm aus drei Komponenten Variable,

Mehr

Die for -Schleife HEUTE. Schleifen. Arrays. Schleifen in JAVA. while, do reichen aus, um alle iterativen Algorithmen zu beschreiben

Die for -Schleife HEUTE. Schleifen. Arrays. Schleifen in JAVA. while, do reichen aus, um alle iterativen Algorithmen zu beschreiben 18.11.5 1 HEUTE 18.11.5 3 Schleifen Arrays while, do reichen aus, um alle iterativen Algorithmen zu beschreiben Nachteil: Steuermechanismus ist verteilt Übersicht nicht immer leicht dazu gibt es for (

Mehr

Algorithmen und Datenstrukturen I - Exkurs Formale Sprachen -

Algorithmen und Datenstrukturen I - Exkurs Formale Sprachen - Algorithmen und Datenstrukturen I - Exkurs Formale Sprachen - Thies Pfeiffer Technische Fakultät tpfeiffe@techfak.uni-bielefeld.de Vorlesung, Universität Bielefeld, Winter 2012/2013 1 / 1 Exkurs: Formale

Mehr

Programmieren I. Kapitel 5. Kontrollfluss

Programmieren I. Kapitel 5. Kontrollfluss Programmieren I Kapitel 5. Kontrollfluss Kapitel 5: Kontrollfluss Ziel: Komplexere Berechnungen im Methodenrumpf Ausdrücke und Anweisungen Fallunterscheidungen (if, switch) Wiederholte Ausführung (for,

Mehr

Grundlagen der Programmierung Teil1 Einheit III Okt. 2009

Grundlagen der Programmierung Teil1 Einheit III Okt. 2009 Grundlagen der Programmierung Teil1 Einheit III - 23. Okt. 2009 GDP DDr. Karl D. Fritscher basierend auf der Vorlesung Grundlagen der Programmierung von DI Dr. Bernhard Pfeifer Ausdrücke & Anweisungen

Mehr

7 Funktionen. 7.1 Definition. Prototyp-Syntax: {Speicherklasse} {Typ} Name ({formale Parameter});

7 Funktionen. 7.1 Definition. Prototyp-Syntax: {Speicherklasse} {Typ} Name ({formale Parameter}); S. d. I.: Programieren in C Folie 7-1 7 Funktionen 7.1 Definition Prototyp-Syntax: Speicherklasse Typ Name (formale Parameter); der Funktions-Prototyp deklariert eine Funktion, d.h. er enthält noch nicht

Mehr