Informatik I+II, Vorlesungen im WS’04/05, SS’05 Barbara Hammer 11. Januar 2006
Inhaltsverzeichnis 1 Was ist Informatik? 1.1 Gebiete der Informatik 1.2 Algorithmen . . . . . . 1.3 Darstellung . . . . . . 1.4 Eine kurze Historie . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
1 . 2 . 3 . 6 . 10
2 Erste Schritte in Java 10 2.1 Hello World! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.2 Elementare Datenstrukturen und Operationen . . . . . . . . . . . . . . . . . . . . 14 2.3 Elementare Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 3 Eigenschaften von Algorithmen 22 3.1 Syntax und Semantik von Programmen . . . . . . . . . . . . . . . . . . . . . . . 22 3.2 Korrektheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.3 Komplexit¨at . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 4 Datenstrukturen 4.1 Boolesche Werte . . . 4.2 Ganze Zahlen . . . . . 4.3 Floating point . . . . . 4.4 Klassen und Methoden
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
37 38 42 46 53
5 Algorithmen fur ¨ abstrakte Datentypen 5.1 Arrays . . . . . . . . . . . . . . . . 5.2 Listen . . . . . . . . . . . . . . . . 5.3 B¨aume . . . . . . . . . . . . . . . . 5.4 Graphen . . . . . . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
68 69 78 87 106
6 Algorithmenentwurf 6.1 Divide and Conquer . . . . . . 6.2 Dynamische Programmierung 6.3 Greedy-Verfahren . . . . . . . 6.4 Backtracking . . . . . . . . . 6.5 Randomisierte Verfahren . . . 6.6 Nebenl¨aufigkeit . . . . . . . . 6.7 Fehler . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
126 126 127 131 132 133 135 147
. . . . . . .
. . . . . . .
. . . . . . .
7 Programmierstile 149 7.1 Imperativ – deklarativ – objektorientiert . . . . . . . . . . . . . . . . . . . . . . . 150 7.2 Ein paar Schlagworte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 7.3 Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 8 Information 8.1 Entropie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2 Kodierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3 Kompression und Kryptographie . . . . . . . . . . . . . . . . . . . . . . . . . . .
168 168 170 176
9 Informationsverarbeitung im Rechner 9.1 Prozesse . . . . . . . . . . . . . . 9.2 Speicher . . . . . . . . . . . . . . 9.3 Filesystem . . . . . . . . . . . . . 9.4 IO . . . . . . . . . . . . . . . . . 9.5 Kern . . . . . . . . . . . . . . . . 9.6 Good bye! . . . . . . . . . . . . .
177 179 180 181 182 183 184
10 Literatur
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
186
1
1 Was ist Informatik? Informatik ist ein Kunstwort, das in den 60er Jahren f¨ur eine sich neu entwickelnde Disziplin geschaffen wurde: Informatik = Information + Automatik Es geht also um die automatische Verarbeitung von Information. Mit Hinblick auf den englischen Begriff Computer Science, also Computerwissenschaften, f¨ugen wir hinzu: es geht um die automatische Verarbeitung von Information mithilfe von Rechnern. Diese Erl¨auterung ist noch ziemlich abstrakt. Informatik umfaßt in diesem Zusammenhang tats¨achlich verschiedenste Bereiche. Anstelle einer Pr¨azisierung geben wir einige Beispiele an, die unterschiedliche Aspekte der Informatik beleuchten: • Ein handels¨ublicher Arbeitsplatzrechner basiert im Kern auf elektronischer Datenverarbeitung, d.h. auf der Festplatte oder im Arbeitsspeicher sind Informationen in sogenannten Bits, bin¨aren 0/1-Werten gespeichert. Die elektronischen Signale werden mithilfe elementarer Schaltungen, die auf den Computerchips angesiedelt sind und die elementaren Signale geeignet kombinieren, verarbeitet. Im Kern eines Computers steht also der Entwurf geeigneter elektronischer Schaltungen und Speicherbausteine, ein Bereich der Hardwaretechnik. • Ein u¨ blicher Nutzer eines Computers arbeitet nicht auf der im Zentrum des Computers stehenden elementaren Ebene, sondern in einer f¨ur ihn komfortableren Umgebung. Er spricht in der Regel nicht einzelne Bits an, sondern er speichert Informationen in Dateien (files), die in Verzeichnissen (directories) organisiert werden k¨onnen. Abl¨aufe im Rechner werden durch Prozesse und geeignete Prozessteuerung realisiert. Verantwortlich f¨ur die komfortable Nutzung des Computers ist das auf dem Computer installierte Betriebssystem, meist Linux, Windows oder Mac OS X. • Normalerweise stehen Rechner nicht einzeln auf dem Arbeitsplatz, sondern sie sind mit weiteren Rechnern vernetzt. Spezielle Dienste wie etwa die Vorhaltung der Heimatverzeichnisse oder die Speicherung von Rechnernamen sind h¨aufig zentral auf einem daf¨ur eingereichteten Rechner angesiedelt, der die anderen Computer im Netz bedient. Rechner k¨onnen u¨ ber das Internet miteinander kommunizieren. Diese Aspekte reichen in den Bereich der verteilten Systeme und Netzwerke. • Im Zusammenhang mit dem Internet und seinen Diensten, die vielen durch e-mails oder Web-Browsing gel¨aufig sind, stehen Fragestellungen wie etwa spezielle Internet-Programme, Darstellung von Informationen auf Web-Seiten, Web-Suche, aber auch Sicherheit der einzelnen Systeme gegen Missbrauch von aussen im Mittelpunkt. • Bei der Vernetzung und Verarbeitung von Informationen braucht man sich nicht auf den engen Bereich handels¨ublicher PCs zu beschr¨anken. Hinzu kommen z.B. Audio- und Videodaten in Multimediapplikationen, oder eine Vernetzung mit weiteren elektronischen Ger¨aten wie einem Handy, MP3-Player, . . . Informatikkomponenten in Gebrauchsger¨aten werden in den Bereichen eingebettete Systeme und ubiquita¨ re Programmierung betrachtet. • In einer Bibliothek sind die vorhandenen B¨ucher, Zeitschriften und sonstigen Werke (in einer Universit¨atsbibliothek z.B. Dissertationen oder Technical Reports) in einem Rechner gespeichert. Man kann innerhalb dieser Information nach Titel oder Autor suchen, neue Werke einf¨ugen, aussortierte Werke l¨oschen, ausgeliehene B¨ucher vermerken, . . . . Es geht hier um das strukturierte Vorhalten der Information, so daß Operationen wie die Suche nach
1 WAS IST INFORMATIK?
2
verschiedenen Kriterien m¨oglichst effizient m¨oglich sind. In der Praxis wird f¨ur die Speicherung derartiger Information meist eine Datenbank eingesetzt. • Der Einsatz von Rechnern in unterschiedlichsten Bereichen mit unterschiedlichsten Nutzeranforderungen bedingt Forschungsarbeiten im Bereich der Mensch-Maschine Kommunikation, um den Umgang mit Rechnern m¨oglichst intuitiv, einfach und nat¨urlich zu machen. Fragestellungen sind dabei etwa die M¨oglichkeit, Rechner durch nat¨urlichsprachige Eingaben zu steuern. • Die allermeisten klassischen Arbeiten auf Rechnern im Bereich der Informatik geschehen auf der Basis von Programmen in einer gegebenen Programmiersprache. In diesem Kurs werden wir die Programmiersprache Java kennenlernen. Im Programmierkurs, der in diesem Semester angeboten wird, haben Sie die Gelegenheit, die sehr wichtige Programmiersprache C zu lernen. • Das Gebiet des Compilerbaus besch¨aftigt sich mit den Techniken, aus den meist in Hochsprachen formulierten Programmen durch den Computer ausf¨uhrbare Maschinenbefehle zu generieren. • Egal welche Programmiersprache benutzt wird, im Zentrum der Programme stehen Abl¨aufe, die die auszuf¨uhrenden Prozesse abstrakt formalisieren und die Durchf¨uhrung der Einzelschritte regeln. Dieses ist das Gebiet der Algorithmik. • Wichtige Eigenschaften von Algorithmen, die einer genaueren Untersuchung bed¨urfen, sind deren Komplexit¨at, d.h. deren Speicherplatzbedarf und Laufzeit. Interessant ist allgemeiner, welche Probleme effizient mit Algorithmen gel¨ost werden k¨onnen, und welche Probleme machen oder sogar u¨ berhaupt nicht durch Rechner l¨osbar sind, die Bereiche der Komplexit¨atstheorie und Rekursionstheorie. • Zu einem gegebenen Programm sollte sichergestellt sein, daß es auch wirklich implementiert, was es soll. Methoden, dieses nachzupr¨ufen, offerieren Programmtests und pr¨azise Verifikation der Algorithmen.
1.1 Gebiete der Informatik Klassischerweise unterteilt man die Informatik in vier verschiedene, teilweise u¨ berlappende Gebiete, die sich in die oben bereits angedeuteten (und weitere) Teilaspekte gliedern: • In der Technischen Informatik geht es darum, wie man Rechner und Automaten entwirft, vernetzt und steuert. Wesentliche Aspekte sind der Entwurf m¨oglichst effizienter Schaltkreise und Rechnerchips, Speichermedien und -formate, Schnittstellen der einzelnen Rechnermodule und Peripherie, Vernetzung und Kommunikation, Effektoren und Robotik. Da die technischen und physikalischen Gegebenheiten der verwendeten Materialien ber¨ucksichtigt werden m¨ussen, grenzt die technische Informatik an Elektrotechnik und Physik. • In der Praktischen Informatik geht es um die Bereitstellung von Methoden, die dem Menschen die Kommunikation mit dem Rechner vereinfachen und ein Arbeiten am Rechner, wie wir es kennen bzw. kennen lernen, erm¨oglichen. Typische Aspekte sind Compilerbau, Programmiersprachenentwicklung, Internetprotokolle, Betriebssysteme, Softwaretechnik, Datenbanken und Oberfl¨achenprogrammierung.
1.2 Algorithmen
3
• In der Theoretischen Informatik geht es um die Definition, Untersuchung, mathematische Fundierung und Entwicklung von Algorithmen und Konzepten f¨ur die Informatik. Typische Fragestellungen besch¨aftigen sich mit Berechenbarkeit, Komplexit¨at, Verifikation, Kodierungstheorie, Algorithmentheorie. Die Theoretische Informatik baut in vielen Bereichen auf Techniken der Mathematik und Logik auf. • In der Angewandten Informatik geht es um die Anwendung der Informatik f¨ur konkrete Probleme wie etwa Medizin- und Bioinformatik, Geographische Informationssysteme, Multimediaapplikationen. Dieses grenzt nat¨urlicherweise an das jeweilige Anwendungsgebiet an. Diese vier Bereiche sind nicht disjunkt; typischerweise startet man in der Praxis von einem konkreten Problem, etwa dem Auftrag einer Bank, eine Finanzsoftware zu entwickeln, und ist dann mit allen Aspekten der Informatik konfrontiert: Formalisierung der zu automatisierenden Abl¨aufe, Planen, Bereitstellen und Vernetzen der Hardwarekomponenten, Einrichten einer Datenbank, Implementation der Algorithmen- und Abl¨aufe, Verifikation, Test und Dokumentation. Es ist also essentiell, als Informatiker von allen Bereichen Kenntnisse zu besitzen. Nicht nur in der Industrie, auch an Universit¨aten besch¨aftigen sich Forscher mit Gebieten, die alle vier Bereiche ber¨uhren; etwa in der Robotik hat man es vom Design der Hardware, der Programmierung von Basisfunktionalit¨aten und Kommunikation der Einzelteile, bis hin zum Entwurf von komplexen Algorithmen etwa zum autonomen Zurechtfinden in neuen Umgebungen zu tun. Weitere Aspekte, die teilweise in anderen Disziplinen angesiedelt, aber wesentlich durch die Informatik gepr¨agt sind und deren man sich als Informatiker bewußt sein sollte, sollten hier erw¨ahnt werden: ein wichtiger Punkt ist die Auswirkung der Informatik auf die Gesellschaft. Sei es durch die Effekte auf Arbeitspl¨atze (Bildschirmarbeitspl¨atze, Rationalisierung, Fertigungsstraßen, Computer-unterst¨utztes Arbeiten), sei es durch die Pr¨agung der Informationsgesellschaft durch das Internet, sei es durch den Einsatz von Computertechnik in Krisensituationen (Hochwassermanagement, Kryptosysteme f¨ur Geheimdienste). Diese Aspekte ber¨uhren unter anderem die Gesellschaftswissenschaften, Psychologie und Ethik.
1.2 Algorithmen Zentral f¨ur die Informatik ist der Begriff des Algorithmus. Das ist nichts anderes als eine Vorschrift f¨ur ein wie auch immer geartetes ausf¨uhrendes Medium, z.B. einen Computer oder eine Maschine, eine Anzahl von Operationen durchzuf¨uhren, die einen gew¨unschten Prozeß automatisieren. Ein sch¨ones Beispiel f¨ur einen Algorithmus ist ein einfaches Kochrezept: *************************************************************** Muffins: Verr¨ uhre 200 g Butter, 200 g Zucker, 4 Eier, 1 Pck Backpuler, 1 Pck Vanillezucker, 250 g Mehl, 3 Eßl. Rum und drei mittelgroße gesch¨ alte und zerteilte ¨ Apfel; f¨ ulle den Teig in Muffinf¨ ormchen; backe bei 175-200 Grad f¨ ur etwa 30 min; best¨ aube die Muffins mit etwas Puderzucker *************************************************************** Folgt man diesen umgangssprachlich formulierten Anweisungen, erh¨alt man das erw¨unschte Ergebnis, fertige Muffins. Dieses Rezept formalisiert durch eine Folge von Anweisungen also den
1 WAS IST INFORMATIK?
4
Prozeß des Muffin-Backen. Ein Algorithmus in der Informatik ist nichts anderes als eine u¨ blicherweise in einer vorgegebenen Programmiersprache formalisierte Folge von Anweisungen, die Prozesse der Informationsverarbeitung operationalisieren, z.B. das Sortieren gegebener Zahlen aufsteigend nach der Gr¨oße. Unser Rezept ist von einer sehr einfachen Form, denn es besteht lediglich aus einer Aneinanderreihung einfacher elementarer Anweisungen. Komplexere Algorithmen k¨onnen weitere Strukturen enthalten. Wichtige Konstrukte sind bedingte Anweisungen, etwa: *************************************************************** Muffins: Verr¨ uhre 200 g Butter, 200 g Zucker, 4 Eier, 1 Pck Backpulver, 1 Pck Vanillezucker, 250 g Mehl, 3 Eßl. Rum; falls es Apfelzeit ist f¨ uge drei mittelgroße gesch¨ alte und zerteilte ¨ Apfel hinzu; sonst f¨ uge 100 g Schokoladenst¨ uckchen hinzu; f¨ ulle den Teig in Muffinf¨ ormchen; backe bei 175-200 Grad f¨ ur etwa 30 min; best¨ aube die Muffins mit etwas Puderzucker *************************************************************** Weitere Strukturen sind Schleifen, etwa: *************************************************************** Muffins: Verr¨ uhre 200 g Butter, 200 g Zucker, 4 Eier, 1 Pck Backpulver, 1 Pck Vanillezucker, 250 g Mehl, 3 Eßl. Rum; falls es Apfelzeit ist f¨ uge drei mittelgroße gesch¨ alte und zerteilte ¨ Apfel hinzu; sonst f¨ uge 100 g Schokoladenst¨ uckchen hinzu; f¨ ulle den Teig in Muffinf¨ ormchen; solange die Muffins noch nicht gar sind backe bei 175-200 Grad; best¨ aube die Muffins mit etwas Puderzucker *************************************************************** Algorithmen k¨onnen Variablen enthalten, die von außen gesetzt werden, etwa: *************************************************************** Muffins: Frage nach, wieviele Personen da sind; Speichere die Antwort in der Variablen x; y=x/4; Verr¨ uhre y*200 g Butter, y*200 g Zucker, y*4 Eier, y Pck Backpulver, y Pck Vanillezucker, y*250 g Mehl, y*3 Eßl. Rum; falls es Apfelzeit ist f¨ uge y*drei mittelgroße gesch¨ alte und zerteilte ¨ Apfel hinzu; sonst f¨ uge y*100 g Schokoladenst¨ uckchen hinzu;
1.2 Algorithmen
5
f¨ ulle den Teig in Muffinf¨ ormchen; solange die Muffins noch nicht gar sind backe bei 175-200 Grad; best¨ aube die Muffins mit etwas Puderzucker *************************************************************** Algorithmen k¨onnen sich auf mehrere Teilprogramme aufteilen, um die Struktur klarer zu machen, etwa: *************************************************************** Muffins: Frage nach, wieviele Personen da sind; Speichere die Antwort in der Variablen x; y=x/4; Verr¨ uhre y*200 g Butter, y*200 g Zucker, y*4 Eier, y Pck Backpulver, y Pck Vanillezucker, y*250 g Mehl, y*3 Eßl. Rum; falls es Apfelzeit ist f¨ uge y*drei mittelgroße gesch¨ alte und zerteilte ¨ Apfel hinzu; sonst f¨ uge y*100 g Schokoladenst¨ uckchen hinzu; f¨ ulle den Teig in Muffinf¨ ormchen; abchentest liefert noch nicht fertig solange Teilprogramm St¨ backe bei 175-200 Grad; aube die Muffins mit etwas Puderzucker best¨ ************************************************************** abchentest: Teilprogramm St¨ abchen in das erste Muffin; stecke ein Holzst¨ falls Teig kleben bleibt noch nicht fertig; sonst fertig *************************************************************** Algorithmen k¨onnen Zufallskomponenten enthalten: *************************************************************** abchentest: Teilprogramm St¨ allig heraus; suche ein Muffin zuf¨ abchen in das Muffin; stecke ein Holzst¨ falls Teig kleben bleibt noch nicht fertig; sonst fertig *************************************************************** Programme k¨onnen durch Rekursion auf sich selbst Bezug nehmen: *************************************************************** abchentest: Teilprogramm St¨ allig heraus; suche ein Muffin zuf¨ abchen in das Muffin; stecke ein Holzst¨
1 WAS IST INFORMATIK?
6
falls Teig kleben bleibt noch nicht fertig; sonst teste nochmal genauer durch Probieren des Muffin; falls schmeckt gut fertig; sonst rufe das Teilprogramm St¨ abchentest wieder auf *************************************************************** Dieses ist ein rekursiver Algorithmus: Das Teilprogramm St¨abchentest ruft sich selbst wieder auf. Rekursion ist ein sehr wichtiges Programmierprinzip der Informatik. Eine andere Sache ist h¨aufig, die hier auch das erste Mal (und im Verlaufe Ihres Studiums wahrscheinlich noch shr sehr h¨aufig) auftritt: Programmierfehler! Dieses ist tats¨achlich ein erstes Beispiel eines fehlerhaften Algorithmus: es kann in obigem Algorithmus der Fall eintreten, daß die Anweisungen nicht durchgef¨uhrt werden k¨onnen, n¨amlich dann, falls durch wiederholtes rekursives Aufrufen des St¨abchentests alle Muffins verk¨ostigt wurden und keines mehr im Ofen steckt. Korrekt muß der Algorithmus diesen Fall gesondert ber¨ucksichtigen: *************************************************************** abchentest: Teilprogramm St¨ falls noch mindestens ein Muffin im Ofen allig heraus; suche ein Muffin zuf¨ stecke ein Holzst¨ abchen in das Muffin; falls Teig kleben bleibt noch nicht fertig; sonst teste nochmal genauer durch Probieren des Muffin; falls schmeckt gut fertig; sonst abchentest wieder auf; rufe das Teilprogramm St¨ sonst acker um die Ecke besorge Kuchen beim B¨ *************************************************************** Der Algorithmus ist jetzt schon ganz sch¨on kompliziert geworden. Er ist in gewisser Hinsicht noch einfach, denn es handelt sich um eine sequentielle Folge von Anweisungen. Das heißt, ein einziger Koch ist hier am Werk, der die Anweisungen nacheinander ausf¨uhrt. Alternative sind parallele Programme, in denen mehrere Anweisungen gleichzeitig durch verschiedene Prozessoren ausgef¨uhrt ¨ werden k¨onnen. Beim Backen k¨onnte etwa der K¨uchenjunge schonmal die Apfel sch¨alen, w¨ahrend der Chefkoch den Teig zubereitet. Die Aktionen m¨ussen in parallelen Programmen geeignet syn¨ chronisiert, d.h. abgestimmt werden. Etwa der K¨uchenjunge muß mit den Apfeln fertig sein, bevor der Chefkoch sie zum Teig f¨ugen kann.
1.3 Darstellung Wir haben hier schon einige wichtige Komponenten von Algorithmen informell kennengelernt. Um Algorithmen in der Informatik pr¨azise behandeln zu k¨onnen, ben¨otigt man geeignete Darstellungen.
1.3 Darstellung
7
Allgorithmen beschreibt man h¨aufig in konkreten Programmiersprachen wie C oder Java oder auch in Pseudocode, d.h. einer intuitiven Notation, die sich an u¨ bliche Strukturen von Programmiersprachen anlehnt, aber technische Details ignoriert. Eine Darstellung im Pseudocode ist meist selbsterkl¨arend, wie etwa unser Muffin-Beispiel. Eine gebr¨auchliche Form, Algorithmen zusammen mit ihrer logischen Struktur deutlich zu machen, sind Flußdiagramme. Ein Flußdiagramm besteht aus den elementaren Bausteinen Zuweisungen (dargestellt in K¨astchen), Eingabe/Ausgabe (dargestellt in Parallelogrammen) und Tests (dargestellt in Rauten), die entsprechend ihrer Abarbeitung in einem Algorithmus durch Pfeile miteinander verbunden sind. Zuweisungen sind durch einen Pfeil mit der nachfolgend bearbeiteten Anweisung verbunden. Tests weisen entsprechend dem Ergebnis (ja/nein) zwei Pfeile auf. Zus¨atzlich f¨ugt man gesonderte Symbole zu, die den Start und das Ende des Programms (in Kreisen) explizit machen. Als ein Beispiel betrachten wir ein einfaches Programm in Pseudocode, die sogenannte Collatz-Funktion: lese x; y:=0; solange x6=1 wenn x gerade x:=x/2; sonst x:=x*3+1; y:=y+1; gebe y aus Die Notation x:= deutet an, daß der Variablen x das Ergebnis des auf der rechten Seite stehenden Ausdrucks zugewiesen wird. Diese Funktion manipuliert eine eingegebene nat¨urliche Zahl x iterativ, bis der Wert 1 erhalten ist, und gibt dann die Anzahl der Manipulationen der Zahl aus. Sie ist in der Informatik vor allem deswegen ber¨uhmt, weil bis heute nicht bewiesen werden konnte, daß dieses Programm immer terminiert, d.h. durch die Operationen immer irgendwann der Wert 1 erreicht wird. Das Programm selbst ist nichtsdestotrotz leicht verst¨andlich. Ein Flußdiagramm macht den Ablauf klar:
Start lese x y:=0
x=1
ja
nein ja
x gerade
x:=x/2
Ende nein x:=3x+1
y:=y+1
schreibe y
1 WAS IST INFORMATIK?
8
Flußdiagramme werden h¨aufig verwendet, komplexere Algorithmenstrukturen graphisch deutlich zu machen. Man sieht hier sehr sch¨on, wenn Operationen von anderen unabh¨angig sind, weil sie in parallelen Pfaden des Diagramms angesiedelt sind. Anweisungen im selben Pfad beeinflussen sich potentiell. Man sieht zudem, in welchen F¨allen das Programm in Zyklen, potentiell in eine Endlosschleife laufen kann, da diese Kreisen im Graphen entsprechen. M¨ochte man einen Algorithmus in Form eines Computerprogramms aufschreiben, muß zun¨achst pr¨azisiert werden, wie ein g¨ultiges Programm prinzipiell aussehen kann. Wir haben bisher Anweisungen in umgangssprachlichen Pseudocode gefaßt, den wir aufgrund unseres Sprachverst¨andnis interpretieren konnten. Ein Computer hat kein solches Hintergrundwissen. Klassische Programmiersprachen bestehen aus nach pr¨azisen Regeln gebildeten Folgen. Die Regeln bestimmen die g¨ultige Syntax der Programmiersprache. Programme, die nicht diesen Regeln folgen, sind syntaktisch inkorrekt und werden vom Computer nicht verstanden. Es gibt verschiedene M¨oglichkeiten zu definieren, wie syntaktisch korrekte Programme prinzipiell aussehen. Wie f¨ur wirkliche Sprache auch, gibt man f¨ur Computersprachen deren Grammatik an. Eine sehr gebr¨auchliche Weise, eine solche spezielle Grammatik darzustellen, ist die sogenannte erweiterte Backus-Naur-Normalform (EBNF). 1 Bevor wir dazu kommen, wie so eine EBNF aussieht, steht hier noch einmal die Bemerkung: die nach spezifizierten Regeln gebildeten Programme bilden syntaktisch korrekte Programme, d.h. Programme, die der Computer versteht. Das bedeutet nicht, daß die Programme sinnvoll sind und das tun, was wir m¨ochten, d.h. das impliziert nicht die semantische Korrektheit. Auch dieses ist so wie in der nat¨urlichen Sprache. Etwa der Satz Das Muffin f¨uhrt die Leiter.‘ ist ein syntaktisch korrekter Satz, der aber keine sinnvolle ’ Bedeutung hat. Eine Grammatik in EBNF-Form besteht aus folgenden vier Komponenten • Einer Menge T von sogenannten Terminalsymbolen, d.h. den Bestandteilen, aus denen die Sprache zusammengesetzt ist. In Programmen sind das z.B. Schl¨usselw¨orter wie ‘main’ oder Symbole ‘{’. • Einer Menge von H Hilfssymbolen, die gebraucht werden, die Sprache effizient zu beschreiben, aber nicht selbst in der Sprache vorkommen. • Ein ausgesuchtes Startsymbol aus H. • Eine endliche Menge von Produktionsregeln der Form linke Seite ::= rechte Seite Die linke Seite solcher Ausdr¨ucke besteht aus einem Hilfssymbol aus H. Die rechte Seite hat die Form Alternative1 | . . . |Alternativen mit einer beliebigen Anzahl n von Alternativen. Alternativen selbst bestehen aus einer Folge von Terminal- oder Hilfssymbolen und den Operatoren [·] und {·}, die Symbolfolgen umschließen k¨onnen. Eine gegebene EBNF repr¨asentiert genau alle W¨orter, die man erh¨alt, wenn man angefangen vom Startsymbol eine endliche Anzahl von Produktionsregeln anwendet, bis man zu einem Wort, das nur aus Terminalen besteht, kommt. Das Anwenden einer Produktionsregeln linke Seite ::= 1
Man kann f¨ur alle sogenannten kontextfreien eine Grammatik in dieser Form finden. Es gibt Sprachen, die nicht kontextfrei sind, z.B. nat¨urliche Sprache. Man kann also f¨ur nat¨urliche Sprache keine solche EBNF-Grammatik finden, und es m¨ussen z.B. f¨ur Deutsch komplizierte Regeln gelernt werden. Die allermeisten Programmiersprachen sind allerdings so einfach, daß eine EBNF-Grammatik ausreicht, sie (fast) vollst¨andig zu charakterisieren.
1.3 Darstellung
9
rechte Seite bedeutet dabei folgendes: in dem bisher gebildeten Wort darf das Hilfssymbol einer linken Seite durch eine beliebige Alternative der rechten Seite ersetzt werden. Die speziellen Ausdr¨ucke [·] und {·} bedeuten dabei, daß man die in den Klammern [·] stehende Symbolfolge auch weglassen kann und die in den Klammern {·} stehende Symbolfolge beliebig oft (auch keinmal) wiederholen darf. Ein Beispiel macht dieses Vorgehen deutlich. Wir sind interessiert an ganzen Zahlen. Ganze Zahlen sind Ziffernfolgen der Form 42, 333, 4711, 2004, -89, . . . Es sind also beliebig lange Folgen von Zeichen 0-9 eventuell mit einem negativen Vorzeichen versehen. Hinzu kommt, daß an der ersten Stelle der Ziffern keine 0 stehen sollte, es sei denn es handelt sich um die Zahl 0 selbst. Die folgende EBNF-Grammatik beschreibt genau diese Zahlen. • Terminalsymbole sind {‘−’,‘0’,. . . ,‘9’}. • Hilfssymbole sind { Zahl, Zifferohnenull, Ziffer}. • Startsymbol ist Zahl. • Die Produktionsregeln sind Zahl ::= 0 | [−] Zifferohnenull {Ziffer} Ziffer ::= 0|1|2|3|4|5|6|7|8|9 Zifferohnenull ::= 1|2|3|4|5|6|7|8|9 Eine Zahl ist also die Null oder ein optionales Vorzeichen gefolgt von einer Ziffer ungleich Null gefolgt von beliebig vielen weiteren Ziffern. Die Zahl 4711 erh¨alt man u¨ ber die folgenden Produktionen: Zahl
Zifferohnenull
4
Ziffer
7
Ziffer Ziffer
1
1
Die obige EBNF ist nicht die einzige, um ganze Zahlen zu beschreiben. Eine Alternative stellen etwa folgende Produktionsregeln dar: Zahl negativeZahl positiveZahl Ziffernfolge Zifferohnenull Ziffer
::= ::= ::= ::= ::= ::=
0 | negativeZahl | positiveZahl − positiveZahl Zifferohnenull | Zifferohnenull Ziffernfolge Ziffer | Ziffer Ziffernfolge 1|2|3|4|5|6|7|8|9 0|1|2|3|4|5|6|7|8|9
Es ist bei komplexeren Grammatiken nicht sofort offensichtlich, welche W¨orter durch sie erzeugt werden. Die Theoretische Informatik besch¨aftigt sich mit M¨oglichkeiten, wie man effizient testen kann, ob ein Programm den Regeln einer gegebenen Grammatik gehorcht. Prinzipiell ist dieses f¨ur EBNF-Grammatiken machbar und f¨ur die meisten Programmiersprachen auch sehr effizient. Der Prozeß, der die Syntax eines gegebenen Programms pr¨uft, nennt sich Parsing – gehorcht das Programm nicht den Regeln, wird ein parse error gemeldet.
10
2 ERSTE SCHRITTE IN JAVA
1.4 Eine kurze Historie Nachdem wir informell den Begriff des Algorithmus gekl¨art haben und formale Sprachen f¨ur Algorithmen definieren k¨onnen, soll ein kurzer Blick auf die geschichtliche Entwicklung stehen. Einer der ersten in der Menschheitsgeschichte vorgeschlagenen dokumentierten Algorithmen ist Euklids Verfahren, den gr¨oßten gemeinsamen Teiler zweier nat¨urlicher Zahlen zu finden (siebtes Buch der Elemente, ca. 300 vor Christus). Der Begriff des Algorithmus selbst ist abgeleitet von dem Namen Mohammed ibn Musa abu Djafar alChoresmi, der ca. 800 nach Christus eine Vorgehensweise f¨ur Testamentsvollstreckungen beschrieb, und dem griechischen Wort arithmo f¨ur Zahl. Wichtige Schritte zur heutigen Algorithmentheorie waren Algorithmen f¨ur den Umgang mit Zahlen im Rechenbuch von Adam Riese 1574, Logarithmentafeln zur effizienten Berechnung von Produkten (1614), das bin¨are Zahlensystem von Leipniz (1703), und die Church’sche These (1936), die die Intuition formuliert, daß alle sinnvollen Darstellungen eines Algorithmus im wesentlichen dasselbe auf unterschiedliche Arten ausdr¨ucken, alle Programmiersprachen und Formalisierungen also im Wesentlichen a¨ quivalent sind. Rechenmaschinen, die verm¨oge von Algorithmen Prozesse automatisiert ausf¨uhren k¨onnen, beinhalten als fr¨uhe Vertreter den ca. 6000 Jahre alten Abakus und den auf John Napier zur¨uckgehenden Rechenschieber. Doch erst in der industriellen Revolution gab es mit dem 1801 von Josef Maria Jaquard eingef¨uhrten automatischen Webautomaten mit Lochkarten eine wirklich automatisierte Maschine. Die mechanische Differenzmaschine von Charles Babbage (1782-1871), die als erster Rechner bezeichnet werden kann, wurde aufgrund der komplizierten Mechanik nicht fertig gestellt. Im weiteren Verlauf verbesserte Hermann Hollerith die Lochkartentechnik, die damals vor allem bei der Volksz¨ahlung verwendet wurde. In der Telekommunikation entwickelte man Relais, elektromechanische Schalter, die auch in der Computertechnik verwendet wurden. Konrad Zuse baute 1936 den ersten Computer mit Relais, die Z1, gefolgt von einer Anzahl weiterer, verbesserter Rechner. Im zweiten Weltkrieg nahm die Bedeutung der Informationstheorie zu und schnelle Kodierung und Dekodierung von Nachrichten war ein entscheidender Aspekt der Kriegsf¨uhrung. Die Amerikaner Eckert und Mauchly bauten den ersten Computer in R¨ohrenbauweise, doch erst John von Neumann und die Halbleitertechnik brachten einen durchschlagenden Erfolg in der Computertechnik. Die 1944/45 vorgeschlagene von Neumann Architektur, deren Ideen teilweise schon in Zuses Z1 enthalten waren, bildet die Grundlage f¨ur heutige Rechner. In Abbildung 1 ist das Prinzip eines von Neumann Rechners dargestellt. Ein Prozessor verarbeitet bin¨are im Arbeitsspeicher vorgehaltene Informationen. Mithilfe der Ein- und Ausgabesteuerung kann der Nutzer mit dem Rechner kommunizieren, es kann Information extern gelagert werden, oder ein weiterer Rechner angesprochen werden. Die interne Steuerung des Rechners basiert auf Maschinensprache, einer sehr kleinschrittigen Folge von von der CPU verstandenen Instruktionen. Um den reibunglosen und komfortablen Ablauf der Kommunikation mit dem Nutzer zu gew¨ahrleisten, ist ein Betriebssystem installiert, h¨aufig Linux, Windows, oder Mac OS X. Dieses erm¨oglicht zusammen mit weiteren Programmen wie einem Internet Browser oder einer Java Installation, Dateien zu pflegen, u¨ ber das Internet zu kommunizieren oder eigene Programme zu schreiben.
2 Erste Schritte in Java Der Schwerpunkt dieser Vorlesung liegt in einer Einf¨uhrung in elementare Algorithmen, Datenstrukturen und deren Formalisierung. Wir lernen kennen, wie man elementare Zuweisungen und Kontrollstrukturen im Rechner realisiert, mit unterschiedlichen Datentypen wie nat¨urlichen Zahlen, Wahrheitswerten, reellen Zahlen oder sogenannten abstrakten Datentypen im Rechner umgeht, und was man beim Entwurf eines Algorithmus in Bezug auf deren Bedeutung beachten sollte. Wir
11 Computer Zentraleinheit (CPU) Prozessor
Arbeitsspeicher
Ein− und Ausgabesteuerung
Eingabe: Tastatur, Maus, Kamera, ...
Ausgabe: Bildschirm, Lautsprecher, Drucker, ...
direkter Benutzer
externer Speicher: Festplatte, CD/DVD, ...
Archivierung
Netz: ISDN, Modem, Netzkarte, ...
anderer Rechner
Abbildung 1: Prinzip eines von Neumann Rechners werden uns dabei der Programmiersprache Java bedienen. Wir schließen mit einem kurzen Blick auf andere Programmiersprachen und einer generellen Formalisierung von Information. Was ist Java? Java ist in Amerika das Synonym f¨ur Kaffee. Daneben bezeichnet es eine Programmiersprache, die Programmiersprache f¨ur Web-Anwendungen. Angefangen hat alles mit einer Arbeitsgruppe, die James Gosling, der Erfinder von Java, 1990 bei SUN gr¨undete, weitere Mitglieder waren Patrick Naughton und Mike Sheridan. Es ging um eine einfache Programmiersprache f¨ur Computerchips, benannt Oak – die Eiche – nach einem Baum vor Goslings B¨uro. 1992 wurde hiermit ein Hardware-Ger¨at zum Bedienen eines Fernsehers entwickelt. Ein kleiner Wicht, Duke (jetzt Maskottchen von Java) f¨uhrte den Benutzer durch ein virtuelles Haus und programmierte dort den Videorecorder. Ziel einer daraufhin gegr¨undeten Firma, First Person, war, interaktives Fernsehen mit Oak zu programmieren. Das Projekt wurde ein finanzieller Verlust, da der Auftrag leider an Silicon Graphics statt SUN ging. Nachdem der erste WWW-Browser, Mosaic, 1993 fertiggestellt wurde, gab es allerdings eine neue Aufgabe f¨ur Oak als plattformunabh¨angige Grundlage f¨ur das Internet. 1995 wurde die Programmiersprache umgenannt, Oak gab es schon f¨ur eine andere Programmiersprache. Von einem T-Shirt Aufdruck ’It’s a jungle out there, so drink your Java’ kam der jetzige Name. F¨ur die von Gosling verbesserte Sprache schrieb Noughton eine Anwendung in Form eines Browsers, WebRunner, aufgrund rechtlicher Gr¨unde umbenannt in HotJava. Netscape lizensierte Java und u¨ bernahm dessen Vermarktung. 1997 wurde JDK1.1 (Java Development Kit) freigegeben und von den f¨uhrenden Software-Firmen unterst¨utzt. Java2 kam 1998 auf den Markt. Ab 1999 ist Java als OpenSource-Lizenz (d.h. inklusive Quellcode) verf¨ugbar. Eine exzellente online-Referenz zu Java bietet das Java-Tutorial von SUN: http://java.sun.com/docs/books/tutorial/.
2 ERSTE SCHRITTE IN JAVA
12
2.1 Hello World! Starten wir mit unserem ersten Java-Programm. Gem¨aß einer alten Tradition der Informatik ist das einfach ein Programm, das ‘Hello world!’ ausgibt: /************************************************************ * * HelloWorld.java * *************************************************************/ class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } Um Hello World! zu erhalten, m¨ussen wir drei Schritte tun: 1. Wir m¨ussen das Programm in irgendeinem Text-Editor schreiben und unter dem Namen HelloWorld.java abspeichern. 2. Wir m¨ussen das Programm in einen f¨ur den Rechner ausf¨uhrbaren Code u¨ bersetzen, indem wir auf der Kommandozeile javac HelloWorld.java eintippen. Es entsteht so eine Datei mit Namen HelloWorld.class in sogenanntem Java Bytecode. 3. Wir m¨ussen das Programm durch den Ausdruck java HelloWorld auf der Kommandozeile ausf¨uhren. Wenn alles geklappt hat, erscheint dann der Text Hello World! auf der Kommandozeile. Verantwortlich daf¨ur ist der Java Interpreter, die auf dem System installierte Java Virtual Machine. F¨ur den Schritt 1. ben¨otigen Sie irgendeinen Text-Editor, etwa vim, emacs oder word-editor. Die Schritte 2. und 3. sind plattformspezifisch und gelten f¨ur die verbreiteten Systeme Linux, Windows und Mac OS X. Sie setzen voraus, daß Java in dem System installiert ist und die Pfade zu den Java-Sources korrekt gesetzt sind; sollte das nicht bereits der Fall sein, dann erhalten Sie das Java Software Development Kit f¨ur verschiedenste Plattformen etwa von Sun unter http://java.sun.com/j2se/1.5.0/index.jsp Schauen wir uns obiges Vorgehen noch einmal genauer an und kl¨aren einige der auftretenden Begriffe: Java Programme sind nichts weiteres als Text in einer speziellen, der Syntax von Java gehorchenden Form, der in einer oder mehreren Dateien abgespeichert ist. Die Syntax und ihre Bedeutung werden wir im weiteren n¨aher kennenlernen. ¨ Java Bytecode wird aus Java Programmen durch Kompilieren oder Ubersetzen erzeugt. Die Pro¨ grammiersprache Java selbst ist eine intuitive und – mit etwas Ubung – gut lesbare Hochsprache, die es dem Benutzer einfach m¨oglich machen soll, Programmabl¨aufe zu automatisieren. Java Bytecode ist eine niedrigere Ausdrucksweise derselben Inhalte in maschinennaher, also f¨ur den Rechner leichter verst¨andlicher Sprache. Im Gegensatz zu Maschinensprache ist Java Bytecode aber plattformunabha¨ ngig und kann von der Java Virtual Machine auf einer beliebigen Plattform ausgef¨uhrt werden.
2.1 Hello World!
13
Die Java Virtual Machine bezeichnet einen Interpreter f¨ur Java Bytecode f¨ur die gegebene Plattform, d.h. sie f¨uhrt Java Bytecode auf der konkreten Maschine aus. Java Bytecode wird h¨aufig in Applikationen auf dem Rechner, auf dem auch die Java Virtual Machine l¨auft, bereitgestellt. Wir werden uns hier auf solche Applikationen beschr¨anken. Java Bytecode kann alternativ in sogenannten applets innerhalb von Internetseiten zur Verf¨ugung gestellt werden und durch die Java Virtual Machine des Rechners, dessen Benutzer die Internetseiten gerade mit einem Internet-Browser betrachtet, ausgef¨uhrt werden. Aufgrund dieser M¨oglichkeit ist Java eine sehr wichtige Sprache im Zusammenhang mit der Internet-Programmierung, da sie einen plattformunabh¨angigen Austausch von Programmen erm¨oglicht. JAva wird dabei w¨ahrend der Ausf¨uhrung interpretiert, nicht vorab einmalig in maschinensprache compiliert, da dieses die Austauschbarkeit f¨ur verschiedene Plattformen verletzen w¨urde. Das ist allerdings langsam, da eine Optimierung der Operationen vor deren Ausf¨uhrung nicht durchgef¨uhrt wird. In vielen Viele Systeme Java-Umgebungen ist daher ein Just-in-Time Compiler integriert. Der Compiler u¨ bersetzt den Code w¨ahrend der Abarbeitung in Maschinensprache und optimiert dabei schon u¨ bersetzte (insbesondere mehrfach auftretende) Stellen. Zur¨uck zum Java-Programm. Die ersten f¨unf Zeilen enthalten lediglich Kommentare, die den Programmnamen und Spezifika beschreiben, um die Lesbarkeit der Programme durch den Nutzer zu erh¨ohen. H¨aufig wird in einem Kommentar am Kopf der Dateien die Funktion des Programms kurz zusammengefaßt, und weitere Information zur Historie des Programms wird integriert. Kommentar innerhalb eines Programms erl¨autert die jeweiligen Programmschritte und Variablen. F¨ur eine gute Programmierung und Wiederverwertbarkeit von Programmen ist eine sinnvolle Kommentierung essentiell! Die Kommentare beeinflussen aber nicht den Ablauf des Programms in Bezug auf den Rechner, er wird beim Parsing ignoriert. Kommentar ist • alles, was (auch u¨ ber mehrere Zeilen) zwischen den Symbolen /* und */ steht, • alles, was hinter dem Symbol \\ bis zum Zeilenende steht. Das Programm besteht dann aus f¨unf Zeilen. Generell ist es so, daß Abst¨ande und Zeilenumbr¨uche zwischen den einzelnen W¨ortern und Symbolen f¨ur die Ausf¨uhrung des Programms irrelevant sind und lediglich der besseren Lesbarkeit dienen. Wir h¨atten das Programm auch z.B. auf nur eine Zeile schreiben k¨onnen, ohne dessen Inhalt zu a¨ ndern. Groß- und Kleinschreibung wird vom Rechner allerdings unterschieden und muß beachtet werden! Das Java-Programm selbst besteht aus einer sogenannten Klasse, der Klasse HelloWorld. Java ist im Gegensatz zu etwa C eine sogenannte objektorientierte Programmiersprache. Die kleinsten Einheiten von Java sind Klassen, d.h. Deklarationen von Objekten, die eine Anzahl von Informationen in Feldern speichern k¨onnen und Methoden zur Verf¨ugung stellen, die die Informationen bearbeiten. Im Gegensatz dazu sind die kleinsten Einheiten von C als sogenannter imperativer Programmiersprache Prozeduren, die eine Anzahl von Variablen bearbeiten und deren Wert a¨ ndern. Den Unterschied imperativer, objektorientierter und weiterer Programmierstile werden wir sp¨ater noch genauer kennenlernen. Eine Klasse ist angedeutet durch das Schl¨usselwort class gefolgt von dem Namen f¨ur die Klasse. Danach stehen in geschweiften Klammern die Elemente der Klasse. Unsere Klasse besitzt nur ein Element, die Methode main. Dieser Name referiert auf eine spezielle Methode, die durch den Java-Interpreter automatisch ausgef¨uhrt wird. Wird durch den Aufruf java Klassenname ein Programm ausgef¨uhrt, sucht der Java-Interpreter nach der main-Methode und f¨uhrt diese aus. Die main-Methode ist immer o¨ ffentlich (public) und inerhalb des Programms nicht mehr ver¨anderbar (static, dieses wird sp¨ater noch n¨aher erl¨autert). Vor und hinter dem Methodennamen stehen in Java Typbezeichner: vor dem Methodennamen der Typ eines Wertes, den die
2 ERSTE SCHRITTE IN JAVA
14
Methode zur¨uckgibt. In unserem Fall ist das void, das heißt nichts. In runden Klammern stehen die Argumente, die die Methode erwartet. Bei main hat dieses immer die Form String[] args und erlaubt, der Methode eine Menge von Zeichenketten mitzugeben. Wir d¨urften also das Programm auch in der Form java HelloWorld los jetzt aufrufen. Da die Argumente nicht weiter verwendet werden, hat dieses keine Auswirkungen auf das Programm. Wir nehmen diesen Kopf‘ der Methode f¨ur unsere ersten Programme erstmal als gegeben hin. Die Methode ’ main f¨uhrt den Befehl System.out.println("Hello, World!"); aus, der den String Hello, World! gefolgt von einem Zeilenumbruch auf der Kommandozeile ausgibt.
2.2 Elementare Datenstrukturen und Operationen An dieser Stelle soll zun¨achst ein Schnelldurchgang durch Kontrollstrukturen und Datentypen von Java stehen, der den Aspekt der Objektorientierung weitgehend außer Acht l¨aßt, sondern auf die Umsetzung von Algorithmen fokussiert. Wir werden uns zun¨achst auf nur eine Klasse beschr¨anken. Wir werden dabei nicht alle in Java m¨oglichen Konstrukte einf¨uhren, sondern uns auf die wichtigsten beschr¨anken. Es gibt sehr viele n¨utzliche Alternativen, die z.B. eine k¨urzere Schreibweise erlauben. Diese finden Sie z.B. in der Java-Dokumentation von Sun. Starten wir mit Variablen, die die Bausteine bilden, um Information zu speichern und in Programmen zu manipulieren. Eine Variable ist zun¨achst ein Bezeicher, etwa ein Buchstabe x oder ein Wort sch¨ onevariable. In Java bereits belegte Schl¨usselw¨orter wie etwa main d¨urfen dabei nicht als Variablenbezeichner verwendet werden. In Java ist es m¨oglich, auch ‘ungew¨ohnliche’ Zeichen, etwa chinesische Schriftzeichen, als Variablenbezeichner zu verwenden. Variablen k¨onnen einen Wert speichern. Java ist eine sogenannte typisierte Sprache, d.h. Variablen besitzen einen bestimmten Typ und k¨onnen nur Werte dieses Typs annehmen. Es gibt zwei verschiedene Arten von Typen • einfache (primitive) Typen und • Referenztypen Referenztypen behandeln wir erst sp¨ater im Abschnitt abstrakte Datentypen. Sie sind extrem wichtig, um objektorientierte Programmierung zu realisieren. Die einfachen Typen in Java sind • boolean: die Werte true und false • char: Charakter, d.h. einzelne Zeichen. In Java sind das sogenannte 16-Bit Unicode Zeichen, die nicht nur alle Zeichen einer deutschen oder amerikanischen Tastatur umfassen, sondern z.B. auch griechische oder chinesische Zeichen. 2 • Verschiedene Typen, um mit ganzen Zahlen umzugehen. In einem Rechner k¨onnen nicht beliebig große Zahlen dargestellt werden. Zahlen werden in Dualdarstellung repr¨asentiert. Wir werden sp¨ater noch die sogenannte polyadische Darstellung pr¨azise einf¨uhren. Die Typen unterscheiden sich hinsichtlich des Bereichs, der abgedeckt wird; dieser ist in der dualen Darstellung durch Zweierpotenzen beschr¨ankt. – byte umfaßt die mit 8 Bit darstellbaren Zahlen −27 bis 27 − 1. – short umfaßt die mit 16 Bit darstellbaren Zahlen −215 bis 215 − 1. – int: Integer ist der gebr¨auchlichste Typ, wenn es um ganze Zahlen geht. Er umfaßt alle mit 32-Bit darstellbaren Zahlen, d.h. die ganzen Zahlen zwischen −2 31 und 231 −1. 2
Sie m¨ussen nur eine passende Tastatur zum Eintippen finden . . .
2.2 Elementare Datenstrukturen und Operationen
15
– long beinhaltet die Zahlen von 2−63 bis 263 − 1. • Einige Typen, um mit reellen Zahlen umzugehen. Auch diese k¨onnen nicht beliebig groß werden und nicht mit beliebiger Genauigkeit betreffend der Nachkommastellen dargestellt werden. Auch die genaue Darstellung reller Zahlen werden wir sp¨ater noch behandeln. – float: alle mit 32 Bit darstellbaren Zahlen in IEEE 754 Gleitkommadarstellung. – double: die gebr¨auchlichste Variante, alle mit 64 Bit darstellbaren Zahlen in IEEE 754 Gleitkommadarstellung. Was das genau heißt, werden wir im Abschnitt u¨ ber reelle Zahlen genauer kennen lernen. Zudem ben¨otigen wir hier auch einen Referenztypen, ohne n¨aher auf die Besonderheiten von Referenztypen einzugehen: • String: eine Zeichenfolge endlicher L¨ange. Wie gebraucht man Variablen? Vor dem ersten Gebrauch innerhalb einer Klassenmethode m¨ussen Variablen deklariert werden. Eine Variablendeklaration hat die Form Variablentyp Variablenbezeichner; oder, wenn man mehrere Variablen vom gleichen Typ deklariert, auch Variablentyp Variablenbezeichner1 , Variablenbezeicher2 , . . . , Variablenbezeichnern ; Ein Beispiel: int x,y,z; int a; char einzeichen; boolean Wahrheitswert; String ganzeswort; Wenn die Variablen deklariert sind, m¨ochte man ihnen Werte zuweisen. Geschieht das nicht, ist in der Regel der Wert der Variablen undefiniert. Variablenzuweisungen haben die Form Variablenbezeichner = rechte Seite; Ein Beispiel: a=42; einzeichen=’B’; Wahrheitswert=true; ganzeswort="huhu"; z=2; In dieser Notation haben wir den Variablen Konstanten (Terminale) zugewiesen. Das sind zum Beispiel die Begriffe true und false f¨ur Boolean, evtl. mit Vorzeichen versehene Ziffernfolgen f¨ur ganze Zahlen, in Hochkommata eingeschlossene Buchstaben f¨ur Charakter oder in Doppelhochkommata eingeschlossene Buchstabenfolgen f¨ur Strings. Die rechte Seite kann allgemeiner ein Ausdruck sein, der einen Wert desselben Typs wie die Variable liefert. Etwa Variablen, die selbst schon einen Wert besitzen: b=a; c=a;
2 ERSTE SCHRITTE IN JAVA
16
oder aus einfachen Ausdr¨ucken mithilfe von Operatoren gebildete komplexere Ausdr¨ucke, wobei die Werte aller auf einer rechten Seite benutzten Variablen definiert sein m¨ussen. Wir listen kurz die gebr¨auchlichsten Operatoren auf: boolean: Stelligkeit einstellig zweistellig zweistellig
Operator ! && ||
Bedeutung Negation logisches und logisches oder
String: Stelligkeit Operator Bedeutung zweistellig + Konkatenation ganze Zahlen: Stelligkeit einstellig zweistellig zweistellig zweistellig zweistellig zweistellig
Operator + * / %
Bedeutung das Negative Addition Subtraktion Multiplikation ganzzahlige Division ohne Nachkommastellen modulo, d.h. Rest bei ganzzahliger Division
reelle Zahlen: Stelligkeit einstellig zweistellig zweistellig zweistellig zweistellig
Operator Bedeutung das Negative + Addition Subtraktion * Multiplikation / Division
Dabei haben wir etwas geschummelt: && berechnet genauer das sogenannte bedingte logische und, das, sofern der linke Term false ergibt, die Auswertung bereits abbricht und false zur¨uckliefert. || ist das bedingte logische oder, daß im Fall von true f¨ur den linken Operatoren bereits true ergibt, ohne den rechten Operator noch zu testen. Die zweistelligen Operatoren werden infix geschrieben, etwa 2.0+42.99 Um die Zugeh¨origkeiten zu definieren, kann man Klammerungen verwenden: (2.0+42.99)*(42.99+4711) Verwendet man keine Klammern, dann sind die Priorit¨aten in der Ausf¨uhrung nach u¨ blichen Konventionen festgelegt, es gilt etwa Punkt- vor Strichrechnung: 2.0+42.99*42.99+4711 = 2.0+(42.99*42.99)+4711 Im Zweifel sollte man die Priorit¨aten nachschauen oder Klammern setzen. Diese Operationen erm¨oglichen, Variablen den Werte komplexerer Ausdr¨ucke zuzuweisen, etwa boolean a, b=true, c; c = !b; a = b || (b && !c); Hierbei haben wir f¨ur b die Konvention verwendet, daß eine Wertzuweisung auch direkt bei der Deklaration der Variablen stehen kann. Zus¨atzlich zu diesen (und weiteren) Operatoren gibt es eine Anzahl zweistelliger Vergleichsoperatoren, die zu zwei Variablen einen booleschen Wert, das Ergebnis des Vergleichs, liefern.
2.2 Elementare Datenstrukturen und Operationen Operator == != < >=
17
Bedeutung gleich ungleich kleiner kleiner oder gleich gr¨oßer gr¨oßer oder gleich
Dieses erm¨oglicht Zuweisungen der Form boolean ergebnis = (42 == 2*21); welches ergebnis den Wert true zuweist. Einige abk¨urzende Schreibweisen f¨ur Zuweisungen, die es auch in C gibt, sind sehr gebr¨auchlich in Java, etwa i++; statt i=i+1;, i+=42; statt i=i+42; und weitere. Variablen verschiedenen Typs haben zun¨achst nichts miteinander zu tun. Da es allerdings h¨aufig vorkommt, daß man Variablen verwendeten Typs miteinander verkn¨upfen m¨ochte, ist die Konvertierung von Typen in Java durch sogenannte Casts der Form (Typ) Variablenbezeichner in vielen F¨allen m¨oglich. Etwa int a = (int)0.42; liefert eine Integervariable a mit dem Wert 0, dem Integeranteil der rellen Zahl 0.42. Casting ist nur f¨ur sinnvolle Typumwandlungen m¨oglich, nicht etwa zwischen rellen Zahlen und boolean, wohl aber zwischen ganzen und reellen Zahlen. Einige Typumwandlungen sind so h¨aufig, daß sie sogar implizit geschehen: ruft man einen Operator mit einer ganzen und einer reellen Zahl auf, wird die ganze Zahl erst in eine reelle Zahl umgewandelt, z.B. 3 * 42.42 liefert 127.26. Generell geschieht das so, daß keine Information verloren geht, d.h. der Typ, der den kleineren Bereich abdeckt, wird zum Typen mit gr¨oßerem Bereich. 3 Auf eine h¨aufige implizite Typumwandlung soll hier explizit hingewiesen werden: die Konkatenation von Strings liefert einen String zur¨uck, etwa zeit = 10; satz = "die Uhr schl¨ agt "; system.out.printeln(satz+zeit); agt 10. liefert den Satz die Uhr schl¨ Hier kommen wir auch schon zum letzten Aspekt, der uns bei Variablen f¨ur’s erste interessieren soll, die Ein- und Ausgabe. Ausgabe von Text auf die Kommandozeile geschieht durch den Aufruf system.out.println(<String>); der den u¨ bergebenen String gefolgt von einem Zeilenumbruch ausgibt, system.out.print(<String>); 3
Das ist ein Unterschied zu C. Außerdem kann aufgrund der nur approximativen Darstellung mancher Zahlen trotzdem Information verloren gehen, wie wir sp¨ater noch sehen werden.
2 ERSTE SCHRITTE IN JAVA
18
unterdr¨uckt den Zeilenumbruch. Der Zeilenumbruch ist u¨ brigens durch den String "\n" charakterisiert. Umgekehrt m¨ochte man Variablen Werte zuweisen. Das Einlesen ist in Java etwas ungew¨ohnlich, da f¨ur andere Zwecke optimiert. Wir beschr¨anken uns daher zun¨achst auf die Kommandozeileneingabe. Ruft man die Klasse der main Methode auf, kann man ihr eine Anzahl von Strings mitgeben, die in der main Methode durch args[0], args[1], . . . ansprechbar sind. Wir k¨onnen also u¨ ber die Kommandozeile Strings einlesen. Strings kann man durch in Java bereitgestellte Methoden in u¨ bliche Datentypen verwandeln: byte b = Byte.parseByte(String0); short s = Short.parseShort(String1); int i = Integer.parseInt(String2); long i = Long.parseLong(String3); float f = Float.parseFloat(String4); double d = Double.parseDouble(String5); Die Umwandlungen werden vorgenommen, so die eingegebenen Strings den entsprechenden Typen beschreiben, ansonsten meldet Java einen Fehler (eine Exception, deren Behandlung wir sp¨ater noch kennenlernen).4 Das soll f¨ur unsere Zwecke erstmal reichen. Wir sind jetzt also in der Lage, komplexe Zuweisungen zu implementieren. Ein Beispiel: /************************************************************ * * HelloWorld advanced.java * *************************************************************/ class HelloWorld advanced { public static void main(String[] args) { String name=args[0]; int anz=Integer.parseInt(args[1]); double mult= Double.parseDouble(args[2]); System.out.println("Hello, "+ name + ", you want the number "+(int)(anz*mult) + "!"); } } Die Eingabe java HelloWorld advanced HAL 2 21.3 liefert den String Hello, HAL, you want the number 42!
2.3 Elementare Kontrollstrukturen Das obige Programm ist immer noch recht langweilig, da wir lediglich Werte zuweisen, aber keinerlei wirklichen Kontrollfluß implementieren k¨onnen. Dieser Abschnitt f¨uhrt einige wesentlich Kontrollstrukturen in Java ein (die es auch in a¨ hnlicher Form in C gibt). Wir haben die Kontrollstrukturen tats¨achlich schon bei unserem anf¨anglichen Kochrezept kennengelernt: 4
Die double- und float-Konvertierungen gelten ab Java 1.2.
2.3 Elementare Kontrollstrukturen
19
• if-Anweisung – die bedingte Ausf¨uhrung hat die Form: if (){ } else { } Ein boolescher Ausdruck ist dabei jeder Ausdruck, der sich zu einem booleschen Wert auswerten l¨aßt. Ein Kommandoblock ist eine durch Semikolons getrennte Folge von Kommandos (auch gar keins), etwa Zuweisungen, Schreibanweisungen, Aufruf von Methoden oder Kontrollstrukturen. Die Anweisung f¨uhrt je nach Wert des booleschen Ausdrucks den ersten oder zweiten Kommandoblock aus. Ein Beispiel: if (i%2 == 0){ System.out.println("Die Zahl "+ i + " ist gerade."); } else { System.out.println("Die Zahl "+ i + " ist ungerade."); } Das Programm testet, ob eine Zahl durch zwei teilbar ist. Die geschweiften Klammern d¨urfen weggelassen werden, wenn gar keins oder nur ein Kommando folgen, so z.B. bei iterierten if-statements. Ein Beispiel: if (i%2 == 0) System.out.println("Die Zahl "+i+" ist gerade."); else if (i%3 == 0) System.out.println("Die Zahl "+i+" ist durch 3 teilbar."); else if (i%4 == 0) System.out.println("Die Zahl "+i+" ist durch 4 teilbar."); else ; Ein leeres else-statement wie dieses letzte d¨urfte man auch weglassen. • while-Schleife – die Wiederholung von Bl¨ocken hat die Form: while () { } Dieses Programm wiederholt den Kommandoblock so h¨aufig (auch keinmal), bis die Bedingung nicht mehr erf¨ullt ist. Ein Beispiel j=2; while ((j0) { hξ, x > 0i b[i]:=x%B; x:=x/B; i++; ξ } hξ, x ≤ 0i P j hx0 = i−1 j=0 b[j] · B i
P j – x = x0 ≥ 0 → (x ≥ 0 ∧ x0 = x · B 0 + 0−1 j=0 b[j] · B ), da die Summe leer ist, gilt das offensichtlich. P j i+1 – (x ≥ 0 ∧ x0 = x · B i + i−1 + j=0 b[j] · B ∧ x > 0) → (x/B ≥ 0 ∧ x0 = x/B · B P i−1 i j 13 x%B · B + j=0 b[j] · B ). Das gilt, da x = x/B · B + x%B gilt. P Pi−1 j j – (x ≥ 0∧x0 = x·B i + i−1 j=0 b[j]·B ∧x ≤ 0) → x0 = j=0 b[j]·B ist offensichtlich, da x = 0 ist. 13
Der Summand f¨ur i ist gesondert geschrieben, da eine Termersetzung f¨ur b[i] vorgenommen wurde.
4 DATENSTRUKTUREN
44 • Die B-adische Darstellung ist eindeutig. Beweis: Wir nehmen an, es gibt zwei Darstellungen der Zahl x=
n1 X
i
bi · B =
i=0
n2 X
b0i · B i
i=0
mit bi , b0i ∈ {0, . . . , B − 1} ohne f¨uhrende Nullen. Wir nehmenP O.E. an, x > 0 (0 ist trivial, f¨ur negative Zahlen betrachten wir einfach das Inverse). Es gilt ni=0 (B − 1) · B i = B n+1 − 1 < B n+1 . Sei n1 ≥ n2 . Wir teilen beide Seiten ganzzahlig durch B n1 . Wir erhalten f¨ur die linke Seite nX 1 −1 bi /B n1 = bn1 b n1 + |i=0 {z } y > 0. Der Rest ist a¨ hnlich. Wir k¨onnen ohne Einschr¨ankung annehmen, daß x die Form x 0 .x1 . . . xp−1 hat. Wir verschieben y entsprechend und ignorieren alle rechten Ziffern, die nicht in p + 1 Bits (Genauigkeit und guard Bit) passen. Die Zahl sei y 0 . Es gilt y − y 0 ≤ (B − 1)(B −p−1 + . . . + B −p−k ) f¨ur ein k. Die Subtraktion mit einem guard Digit liefert den Wert x − y 0 + δ mit dem durch Rundung m¨oglichen Fehler |δ| ≤ B/2 · B −p . Der Fehler |(x − y) − (x − y 0 − δ)|/(y − y 0 − δ) ist von Interesse. Es gibt drei F¨alle: – x − y ≥ 1. Dann ist der relative Fehler maximal (y − y 0 + δ)/1 ≤ B −p ((B − 1)(B −1 + . . . + B −k ) + B/2) < B −p (1 + B/2). – x − y 0 < 1. Dann ist δ = 0, denn es mußte nicht gerundet werden. x − y ist mindestens 1.0 . . . 0 − 0. 0 . . 0} B . . B − 1} > (B−1)(B −1 +. . .+B −k ). Der relative Fehler ist also maximal (B−1)B −p (B −1 + | .{z | − 1 .{z k
p
. . . + B −k )/((B − 1)(B −1 + . . . + B −k )) = B −p .
– x − y < 1 und x − y 0 ≥ 1. Dann muß x − y 0 = 1 gelten, also δ = 0, also dieselbe Argumentation wie eben ist korrekt.
4 DATENSTRUKTUREN
52
Man kann nachrechnen, daß sich eigentlich 10 ergeben sollte. Java liefert aber 11.5! Problem: Sehr kleine Zahlen wurden aus sehr großen Zahlen durch Subtraktion gewonnen. Die ersten Stellen stimmen exakt u¨ berein und heben sich also weg. Die letzten Stellen r¨ucken nach, haben aber nur noch eine sehr geringe Akkurazit¨at, die dann f¨ur das ungenaue Ergebnis verantwortlich ist. Man sollte also vermeiden, sehr a¨ hnliche Zahlen zu summieren. Analoge Probleme treten auf, wenn man gr¨oßenordnungsm¨aßig sehr verschiedene Zahlen summiert, wobei die kleinere Zahl im schlimmsten Fall die gr¨oßere gar nicht a¨ ndert, da sie nur irrelevante Bits beeinflußt. Ein weiteres Beispiel: wir berechnen ex nach der Taylorformel (eine Methode der Mathematik, Funktionen lokal durch Polynome anzun¨ahern) als ex = 1 + x + x2 /2 + x3 /3! + x4 /4! + . . . f¨ur negative Zahlen x. Die Reihe konvergiert absolut und gleichm¨aßig, dennoch liefert Java horrende Ergebnisse f¨ur negative Zahlen. Das Problem ist, daß man in der Summe viele Paare wie (f¨ur x = −25) 2524 /24! und −2525 /25! erh¨alt, die sich in Theorie, nicht aber in der konkreten Rechnung im Fließkommaformat zu Null addieren. • Over- und Underflow: Bei Multiplikationen und Divisionen muß man sehr große und sehr kleine Werte m¨oglichst vermeiden, da sie sonst leicht in nicht mehr darstellbare Bereiche nahe Null (d.h. die Werte werden f¨alschlicherweise exakt Null) oder ±∞ f¨uhren k¨onnen. Je nach Gegebenheiten muß man Abhilfe schaffen, z.B. durch k¨unstliche Setzung auf kleine oder große Werte ungleich Null und Unendlich, oder Ersetzen der Multiplikation durch eine Addition und Exponentiation. ¨ • Instabile Numerik: Programme, bei denen kleine Anderungen der Eingabe nur zu kleinen ¨ Anderungen der Ausgabe f¨uhren, heißen numerisch stabil. Wir sind an solchen interessiert. Oft kann man durch geeignete Vorsichtsmaßnahmen Berechnungen numerisch stabil gestalten, wobei allerdings das Design stabiler Algorithmen hochgradig nicht trivial ist! Es gibt allerdings auch Probleme, die prinzipiell keine stabilen Verfahren zulassen (ill-conditioned problems). Ein Beispiel ist die logistische Gelichung, die zu einem sogenannten chaotischen ¨ System f¨uhrt, bei dem im Limes sehr kleine Anderungen sehr große Auswirkungen haben. Es soll beginnend von einem Startwert x(0) zum Zeitpunkt t die Gr¨oße x(t) = Rx(t − 1)(1 − x(t − 1)) berechnet werden. F¨ur große R ist das nicht mehr stabil m¨oglich! Solche chaotischen Systeme treten auch in der Natur auf (etwa in Wachstumsprozessen oder auch die Trajektorie vom Orbit von Pluto)! Problem: das Iterieren von Rechnungen mit kleinem Fehler kann zu beliebig großen Abweichungen f¨uhren. Es gibt also einige Probleme und auch prinzipiell nicht numerisch stabil l¨osbare Aufgaben. In den allermeisten F¨allen treten aber ungew¨unschte Verhalten aufgrund einer nicht ad¨aquaten Behandlung der Numerik auf, die sich gelegentlich erst bei extremen Werten zeigt. Zum Abschluss zwei Beispiele aus dem wirklichen Leben: • Ariane rocket 5 explodierte 40 Sekunden nach Start bei der Jungfernfahrt, 7 Billionen Dollar auf einen Schlag vernichtend. Das Problem lag an Werten f¨ur die Beschleunigung, die einen ¨ Uberlauf in einem Teil des Programms hervorriefen, das f¨ur die Rekalibrierung der Steuerung n¨otig war. Eine Kopplung von weiteren Effekten f¨uhrte dann zum Ende von Ariane. • 1991 traf eine amerikanische Patriotrakete statt des anvisierten Ziels eine (bewohnte) Baracke. Der initiale Fehler wurde durch die nicht exakte Darstellbarkeit von 1/10 im Programm verursacht.
4.4 Klassen und Methoden
53
4.4 Klassen und Methoden Wir k¨onnen jetzt komplexe Kontrollstrukturen schreiben und deren Auswirkung formal spezifizieren und sogar verifizieren. Diese Programme k¨onnen allerdings je nach den Gegebenheiten sehr un¨ubersichtlich werden. Wir haben bei unserem anf¨anglichen Kochrezept bereits eine M¨oglichkeit verwendet, Programme u¨ bersichtlicher und k¨urzer zu gestalten: die Einteilung des Kontrollflusses in mehrere Module. Definition von Klassen Die Grundbausteine von Java als objektorientierter Programmiersprache sind Klassen. Das ist unterschiedlich zu einer imperativen Programmiersprache wie C. Klassen sind von der Idee her komplexere Variablentypen mit mehreren Eintr¨agen und Werten, die alle Methoden, diese zu beeinflussen, innerhalb der Datenstruktur selbst zur Verf¨ugung stellen. Sie bilden die kleinsten Einheiten in Java. Durch die Kapselung aller eine Klasse betreffende Information und aller damit zusammenh¨angenden Operationen innerhalb der Klassen ist eine sehr u¨ bersichltliche, modulare und gut wartbare Programmierung m¨oglich. Man kann, wenn Klassen definiert sind, Variablen dieser Klasse benutzen, sogenannte Objekte. Objekte sind Instanzen einer Klasse. Eine Klasse in Java hat generell folgende Form class { <Methoden> } Variablen und Methoden d¨urfen dabei in beliebiger Reihenfolge vorkommen. Ein Beispiel class Kreis{ public double radius; static private final double pi=3.14; public double flaeche(){ return (pi*radius*radius);} } Elemente der Klasse sind class members, Klassenmitglieder, d.h. Variablen oder Methoden. Variablen referiert auf Variablendeklarationen und etwaige Instanziierungen z.B. mit Konstanten; in obigem Fall hat die Klasse Kreis die Felder radius und pi. Variablendeklarationen bestehen, wie wir schon wissen, aus einem Variablentypen, z.B. einem primitiven Typen double, und einem Variablenbezeichner. Diesem darf an Ort und Stelle ein Wert zugewiesen werden, wie oben mit pi=3.14 geschehen. Oben sind verschiedene zus¨atzliche Charakterisierungen gebraucht: • final bedeutet bei Variablen, daß deren Wert genau einmal gesetzt wird und sich danach nicht mehr a¨ ndert. Final-Variablen m¨ussen initialisiert werden. Sie sind zum Beispiel zur Definition von Konstanten gebr¨auchlich. Der Versuch, final-Variablen zu a¨ ndern, bewirkt einen Fehler. • Es gibt sogenannte Klassen- und Instanzvariablen. Klassenvariablen sind durch static markiert. Es sind Felder, die sich alle Objekte der Klasse teilen. Instanzvariablen (ohne static) sind Felder, die jedes Objekt der Klasse einzeln besitzt. Klassenvariablen speichern also etwas, das bei allen Objekten gleich ist. In obigem Fall geh¨ort das Feld pi gemeinsam allen Kreis-Objekten, die Kreise besitzen nicht gesonderte Konstanten pi.
4 DATENSTRUKTUREN
54
• Variablen k¨onnen eine unterschiedliche Sichtbarkeit haben. private bedeutet, daß nur das Objekt selbst dieses Feld sehen und beeinflussen darf; das sind also Geheimnisse der Klasse, ¨ die kein anderer wissen darf, z.B. weil deren unkontrolierte Anderung zu einem inkonsistenten Zustand der Objekte f¨uhren k¨onnte. public bedeutet, daß alle auch von außen auf dieses Feld zugreifen d¨urfen, das sind also Informationen, die jeder wissen sollte. Zudem gibt es noch protected, was Visibilit¨at innerhalb des Pakets, der Klasse und abgeleiteter Klassen bedeutet. Letzlich gibt es package, was Sichtbarkeit im Paket bedeutet. Wir behandeln hier keine Pakete, sondern werden, da wir nur kleinere Beispiele betrachten, alle Klassen in eine Datei schreiben. Der Name ergibt sich aus dem Namen der Klasse, die die main-Methode implementiert. Alle Klassen in einer Datei sind automatisch Elemete eines anonymen Paketes. Der Compiler u¨ bersetzt die Klassen in durch .class bezeichnete Dateien. Per Default (also ohne Angabe) sind Variablen nicht static, package (also Paketsichtbar, d.h. in unserem Fall von nur einem Paket sichtbar) und nicht final. Als Bemerkung: Man k¨onnte auch Klassen selbst mit dem Zusatz public versehen. Dann m¨ussen sie in einem File gleichen Namens stehen und sind u¨ berall sichtbar. Default bei Klassen ist Paketsichtbarkeit. Methoden haben generell die Form <Modifikation> (Variablenliste){ } Eine Modifikation a¨ hnlich wie bei Variablen kann vorgegeben werden: • final bedeutet, daß die Methoden sp¨ater nicht mehr ge¨andert werden d¨urfen (wie das ginge, sogenanntes Overriding, diskutieren wir sp¨ater). • static referiert auf Klassenmethoden statt Instanzmethoden. Klassenmethoden sind f¨ur die Klasse und keine einzelnen Objekte zust¨andig und d¨urfen nur auf Klassenvariablen zugreifen, aber nicht auf Instanzvariablen der Klasse. • private bedeutet, daß die Methode nur aus der Klasse selbst heraus sichtbar ist, public ver¨andert die Sichtbarkeit auf die Außenwelt, protected bedient die Klasse selbst, das Paket und alle abgeleiteten Klassen, package bedient das Paket und die Klasse selbst. Per default sind Methoden vom Paket zugreifbar, nicht final und nicht static. Methoden k¨onnen innerhalb einer (m¨oglicherweise leeren) Variablenliste Variablen u¨ bernehmen, die innerhalb der Methode verwendet werden k¨onnen. Diese werden call by value u¨ bergeben, d.h. Ver¨anderungen primitiver Typen innerhalb der Methode haben keinen Effekt auf die Außenwelt. Was sie bei Objekten bewirken, diskutieren wir sp¨ater. Eine leere Liste kann auch als void geschrieben werden. Die Methode muß einen Wert zur¨uckgeben, induziert durch den Typ vor dem Methodennamen. Innerhalb eines Anweisungsblocks dient der Befehl return (<Wert>); dazu, einen Wert zur¨uckzugeben. Wird dieses Kommando ausgef¨uhrt, dann gibt die Methode die Auswertung von Wert zur¨uck und terminiert. Wird nichts zur¨uckgegeben, steht vor der Methode void. Objekte Jetzt zu Objekten. Objekte einer Klasse k¨onnen genau wie alle anderen Variablen auch deklariert werden:
4.4 Klassen und Methoden
55
; Die Deklaration eines Objektes bedeutet allerdings noch nicht, daß diese Objekte existieren. Eine Deklaration benachrichtigt lediglich den Compiler, daß in Zukunft der Identifier f¨ur Objekte dieser Klasse benutzt wird. Ein tats¨achliches Objekt bekommt man durch eine Instanziierung, die dem Compiler sagt, tats¨achlich Speicherplatz f¨ur dieses Objekt neu zur Verf¨ugung zu stellen. Auf das Objekt wird dann anschließend im Programmcode durch eine Referenz, d.h. einen Zeiger, der sagt, wo dessen Werte anzutreffen sind, verwiesen. Dann kann man den enthaltenen Feldern Werte zuweisen, sie initialisieren und mit dem Objekt rechnen. Beachten Sie, daß wir im Fall von primitiven Typen nicht zwischen Deklaration und Instanziierung unterschieden haben. Bei primitiven Typen ist der ben¨otigte Platz f¨ur ihre Darstellung a priori klar. Eine Deklaration stellt sofort diesen Platz zur Verf¨ugung. Bei Klassen ist der ben¨otigte Speicherplatz aufwendiger zu berechnen. Der Computer stellt daher bei der Deklaration lediglich den Speicherplatz f¨ur die Referenz auf das Objekt zur Verf¨ugung. Erst bei der Instanziierung wird Speicherplatz f¨ur die Felder des Objektes beschafft und die Referenz verweist dann auf diesen Speicherplatz. Verm¨oge der Referenz k¨onnen die Felder initialisiert und belegt werden. Klassen sind daher als Referenztypen bezeichnet. Die folgende Graphik zeigt den Unterschied. <primitiver Typ> X;
Wert
Y;
Referenz
Feld1 Feld2 Feld3
Konstruktoren Wie instanziiert man Referenztypen? In vielen F¨allen verwendet man dazu den Befehl new gefolgt vom Namen der Klasse, den sogenannten Default-Konstruktor. Etwa Kreis kreis; kreis=new Kreis(); hat den Effekt, daß die Variable kreis deklariert und mithilfe der Instanziierung Speicherplatz zur Verf¨ugung gestellt wird. Anschließend kann man auf die Elemente des Objekts durch einen Punkt zugreifen: kreis.radius=42; System.out.println(kreis.flaeche()); Unsere Programmierung an dieser Stelle ist unsch¨on, da es gegen einen wesentlichen Aspekt der objektorientierten Programmierung, die weitm¨oglichste Kapselung, verst¨oßt. Wenn der Radius von außen gesetzt wird, dann muß die exakte Art, wie der Radius intern gespeichert und verwandt wird, z.B. der genaue Datentyp bekannt sein. Das kann leicht zu Fehleren f¨uhren, wenn z.B. aus welchen Gr¨unden auch immer ein System intern mit anderen Variablentypen rechnen m¨ochte (z.B. integer statt double). Greifen Programme von außen direkt auf radius zu, dann h¨atte eine ¨ Anderung des Formats von radius Auswirkungen auf das gesamte Programm. Man w¨urde daher
56
4 DATENSTRUKTUREN
lieber den Zugriff auf das Feld Radius vermeiden. Da allerdings der Radius irgendwie von außen vor Beginn aller weiteren Rechnungen gesetzt werden muß, ben¨otigt man zus¨atzliche Konstrukte, sogenannte Konstruktoren. Konstruktoren sind a¨ hnlich zu Methoden, die den Namen der Klasse selbst tragen, keinen Wert zur¨uckliefern und zur Instanziierung eines Objekts statt des DefaultKonstruktors aufgerufen werden k¨onnen. Etwa in unserem Fall: class Kreis{ double radius; static private final double pi=3.14; public Kreis(double radius){ this.radius=radius;} public double flaeche(){ return (pi*radius*radius);} } Der Aufruf Kreis kreis=new Kreis(42.0); deklariert und instanziiert einen Kreis mit Radius 42. Eine Besonderheit: insbesondere innerhalb ¨ von Konstruktoren ist es u¨ blich, Variablen ‘doppelt’ zu benennen; in diesem Fall heißt der Ubergabeparameter des Konstruktors genauso wie ein Feld der Klasse. Man kann doppelte Variablen nat¨urlich umgehen. M¨ochte man sie doch nutzen, dann u¨ berdeckt die letzte Definition alle vorher¨ gehenden, kreis bezieht sich innerhalb des Konstruktors auf den Ubergabeparameter. Man muß daher explizit darauf hinweisen, daß das Feld der zur Zeit betrachteten Instanz this.radius benutzt wird. this referiert innerhalb der Klasse explizit auf die derzeitige Instanz (d.h. deren Referenzwert). Polymorphie Sobald ein Konstruktor definiert wurde, existiert der Default-Konstruktor nicht mehr. Kreis kreis=new Kreis(); w¨are also in obiger Klasse verboten. Man kann allerdings explizit mehr als einen Konstruktor mit demselben Namen, aber unterschiedlicher Parameterliste verwenden: class Kreis{ private double radius; static private final double pi=3.14; public Kreis(double radius){ this.radius=radius;} public Kreis(){ this.radius=1.0;} public double flaeche(){ return (pi*radius*radius);} } ¨ Dieses f¨allt unter das Stichwort Uberladen und Polymorphie: man darf denselben Namen f¨ur Methoden und Konstruktoren unterschiedlicher Stelligkeit oder unterschiedlichen Parametertypen verwenden. Die aktuelle Parameterliste entscheidet dann, welche Methode verwendet wird. In obiger Situation ist also folgendes m¨oglich Kreis kreis1 = new Kreis(); Kreis kreis2 = new Kreis(42.0);
4.4 Klassen und Methoden
57
Aufgrund dieses Mechanismus muß man bei den Variablentypen exakt sein: Methoden und Konstruktoren unterscheiden sich, wenn sie denselben Namen, aber Parameter unterschiedlichen Typs haben. Der Aufruf Kreis kreis2=new Kreis(42); w¨urde in obigem Fall einen Konstruktor mit Integer-Parameter aufrufen, so es ihn g¨abe. Klassen-/Instanzvariablen und -methoden Kommen wir, ausgestattet mit diesem Arsenal, nochmal zu den Stichw¨ortern Klassen- versus Instanzvariable zur¨uck. Wir m¨ochten aus welchen Gr¨unden auch immer unsere Kreise durchnummerieren. Neben dem Radius hat also jeder Kreis auch ein Feld id, eine nat¨urliche Zahl deklariert als private int id; Angefangen von 0 m¨ussen wir mit jedem neuen Kreis-Objekt den aktuellen zu vergebenden Identifier um eins erh¨ohen. Das kann nat¨urlich von außen geschehen: der Konstruktor wird zu Kreis(double radius,int maxid){ this.radius=radius; this.id=maxid;} und Aufrufe haben die Form int maxid=0; Kreis kreis1 = new Kreis(42,maxid); maxid++; Kreis kreis2 = new Kreis(1.0,maxid); Das ist nat¨urlich unsch¨on, da man die richtige Z¨ahlung außerhalb der Klasse vorhalten muß. Alternative bietet eine Klassenvariable, d.h. einer Variable maxid, die von allen Instanzen geteilt und bei Instanziierung eines neuen Objekts automatisch erh¨oht wird: class Kreis{ double radius; private int id; static private final double pi=3.14; static private int maxid=0; public Kreis(double radius){ this.radius=radius; this.id=maxid; maxid++;} public Kreis(){ this.radius=1.0; this.id=maxid; maxid++;} public double flaeche(){ return (pi*radius*radius);} } Noch etwas eleganter unter Vermeidung von Codeverdoppelung ist die folgende Alternative, die die Tatsache verwendet, daß man innerhalb von Methoden und Konstruktoren andere Konstruktoren verwenden darf:
4 DATENSTRUKTUREN
58
class Kreis{ double radius; private int id; static private final double pi=3.14; static private maxid=0; public Kreis(double radius){ this.radius=radius; this.id=maxid; maxid++;} public Kreis(){ this(1.0);} public double flaeche(){ return (pi*radius*radius);} } Das Argument this referiert auf einen bereits definierten Konstruktor der Klasse, der f¨ur das Element ausgef¨uhrt wird. Dieser Aufruf muß als erstes im neuen Konstruktor verwandt werden. Dieses ist ein Beispiel, wo eine Klassenmethode sinnvoll werden k¨onnte: public static int whatismaxid(){return(maxid);} ¨ Klassenvariablen sind vorhanden, sobald das System die Klasse gesehen hat. Offentliche Klassenvariablen und Klassenmethoden k¨onnen daher verwendet werden, auch wenn kein Element der Klasse erzeugt wurde. Sie k¨onnen durch den Klassennamen statt Objektnamen referenziert werden. In obigem Beispiel liefert also der Aufruf System.out.println(Kreis.whatismaxid()); vor Erzeugung eines Kreis-Objekts den Wert 0. Parameterubergabe ¨ Kommen wir nochmal zur¨uck zu Variablen, Parameter¨ubergabe und Zuweisungen. Generell wird bei der Parameter¨ubergabe und einer Zuweisung der Wert der betrachteten Variablen verwandt, ¨ und nicht die Variable selber. Das hat zur Folge, daß Anderungen des Wertes keine direkten Auswirkungen auf das umgebende (aufrufende) Programm haben. Ruft man eine Methode mit dem Integer x auf und a¨ ndert x innerhalb der Methode, wird der Wert von x außerhalb also nicht tangiert. Bei Referenztypen sieht das allerdings ein bißchen komplizierter aus: Der Wert einer betrachteten Variablen ist • der tats¨achliche Wert bei primitiven Typen, • eine Referenz auf die Felder bei Referenztypen. Das hat zur Folge daß sich diese unterschiedlichen Variablentypen unterschiedlich verhalten und unterschiedlich behandelt werden m¨ussen. Prozedur¨ubergaben in Java sind call by value: F¨ur primitive Typen wird also der finale Wert ¨ u¨ bergeben und ver¨andert, nicht die Speicherstelle, wo die Variable steht. Anderungen stehen also an einer anderen Speicherstelle und haben daher keinen Einfluß auf die Originalvariable. F¨ur Refe¨ renztypen wird die Referenz kopiert. Andert man (was man normalerweise nicht tut) die Referenz ¨ selbst, hat das keinen Einfluß auf die Originalreferenz. Andert man aber verm¨oge der Kopie der Referenz die (original) Felder, auf die die Kopie zeigt, hat das einen Einfluß auf die Felder und also das originale Objekt. Folgende Graphik macht das deutlich:
4.4 Klassen und Methoden <primitiver Typ> X;
59 Methode(X);
Y;
Kopie Wert
Methode(Y); Kopie
Referenz
Wert
Referenz
Feld1 Feld2 Feld3
Ein Beispiel der Effekte: class Einpaarzahlen{ int i; double x; public Einpaarzahlen(int i, double x){ this.i=i; this.x=x;} void Tuwas(Einpaarzahlen zahlen, int i, double x){ zahlen.i++; zahlen.x++; i++; x++; System.out.println(i); System.out.println(x); System.out.println(zahlen.i); System.out.println(zahlen.x); } public static void main(String[] args){ int i=42; double x=42.0; Einpaarzahlen zahlen = new Einpaarzahlen(i,x); System.out.println(i); System.out.println(x); System.out.println(zahlen.i); System.out.println(zahlen.x); zahlen.Tuwas(zahlen,i,x); System.out.println(i); System.out.println(x); System.out.println(zahlen.i); System.out.println(zahlen.x); } } Die Aufrufe liefern : 42 42.0 42 42.0 43 43.0 43 43.0 42 42.0 43 43.0
i x zahlen.i zahlen.x i x zahlen.i zahlen.x i x zahlen.i zahlen.x
print in main
print in Tuwas
print in main
4 DATENSTRUKTUREN
60 Vergleich- und Zuweisung von Referenztypen
Entsprechend muß man bei Operationen und Zuweisungen der unterschiedlichen Typen aufpassen. Primitive Variablen werden zu ihrem Wert ausgewertet, wenn man vergleicht oder zuweist, wie man es erwartet. Referenztypen werden zu dem Wert der Referenz ausgewertet. Weist man Referenztypen wie folgt zu kreis1=kreis2;
/* falsch f¨ ur Referenztypen! */
dann ist das Ergebnis also etwas unerwartet: in zwei Variablen hat man dieselbe Referenz auf die Felder des Kreis gespeichert, die Felder des Kreis existieren selbst nur einmal. M¨ochte man wirklich eine Kopie aller Felder und deren Werte bekommen, muß man dieses geeignet implementieren: man ben¨otigt eine Methode, die alle Felder des betrachteten Objekts mit primitiven Datentypen und evtl. rekursive alle durch Felder mit Referenzen dargestellten Subobjekte kopiert. Eine Kopiermethode der Klasse Kreis k¨onnte etwa wie folgt aussehen: public Kreis cloneKreis() { /* Kopie f¨ ur Referenztypen */ Kreis helpme = new Kreis(this.radius); return (helpme); } Eine Kopie erh¨alt man dann durch17 kreis2=kreis1.cloneme(); Analog testen Vergleiche der Form kreis1==kreis2 lediglich, ob in den Variablen dieselben Referenzen gespeichert sind. In der Regel ist das nicht das gew¨unschte Ergebnis. Man betrachtet Objekte als gleich, wenn der Inhalt aller ihrer Felder inklusive eventueller Subobjekte gleich ist. Also muß auch dieser Operator gesondert implementiert werden, etwa f¨ur Kreis (bei Gleichheit von zwei Kreisen f¨ur gleiche Radien) public boolean isequal(Kreis you){ if (you.radius==this.radius) return (true); else return (false); } ¨ Diese Methode erlaubt die korrekte Uberpr¨ ufung18 kreis1.isequal(kreis2) Wie schon erw¨ahnt ist String ein Referenztyp, der Zuweisungen im Text der Form String string="Hello World!"; zul¨aßt. Dieses kann man als spezielle Instanziierung sehen: In Hochkommata geschriebene Strings k¨onnen direkt verwendet werden und sind intern durch eine feste Referenz gespeichert. Strings m¨ussen aber durch die durch Java bereitgestellte Methode equals verglichen werden und Kopien der Inhalte statt Referenzen (so man sie braucht) bekommt man durch clone. 17
Java stellt die generelle Klasse clone(); zur Verf¨ugung, die h¨aufig f¨ur die ben¨otigten Zwecke angepaßt werden kann. Wir wollen das an dieser Stelle nicht vertiefen. 18 Java bietet die Methode equals, die f¨ur solche Zwecke u¨ berschrieben werden sollte.
4.4 Klassen und Methoden
61
class strings{ public static void main(String[] args){ String string1=args[0]; String string2=args[1]; String string3="Hello World!"; if ((string1+" "+string2+"!")==string3) System.out.println("ja"); else System.out.println(nein"); if ((string1+" "+string2+"!").equals(string3)) System.out.println("ja"); else System.out.println(nein"); } } Diese Programm liefert mit strings Hello World aufgerufen f¨ur den ersten Vergleich nein, denn es werden die Referenzen verglichen, f¨ur den zweiten Vergleich ja. Vererbung Ein letzter Punkt zu Klassen und Methoden: diese werden sehr n¨utzlich, sobald sie als Klassenhierarchie verwendet werden. Klassen k¨onnen als Unterklassen bereits bestehender Klassen definiert werden. Sie erben dann alle relevanten Eigenschaften der Oberklasse und k¨onnen aufbauend auf diesem Verm¨achtnis noch beliebig viele zus¨atzliche Funktionalit¨aten implementieren. Die Tatsache, daß eine Klasse erbt, wird durch den Zusatz extends deutlich gemacht. Ein Beispiel, betrachten wir eine Variation unserer Kreis Klasse von vorhin: class Kreis{ double radius; static final double pi=3.14; Kreis(double radius){this.radius=radius;} Kreis(){this.radius=1.0;} double flaeche(){return(pi*radius*radius);} } Eine sinnvolle Unterklasse w¨are, Kreise mit Mittelpunkt und zus¨atzlichen Funktionalit¨aten auszustatten: class Kreismitmittelpunkt extends Kreis{ double x,y; Kreismitmittelpunkt(double radius, double x, double y){ super(radius); this.x=x; this.y=y;} Kreismitmittelpunkt(){ Kreismitmittelpunkt(1.0,0.0,0.0);} void verschiebe(double x,double y){ this.x+=x; this.y+=y;} } Eine abgeleitete Klasse • erbt (d.h. es ist sichtbar) von der Oberklasse
4 DATENSTRUKTUREN
62
– alle Variablen, die in der Oberklasse zur Verf¨ugung stehen und nicht durch private markiert sind, – alle Methoden, die in der Oberklasse zur Verf¨ugung stehen und nicht durch private markiert sind, • erbt nicht von der Oberklasse – mit private markierte Variablen und Methoden, – Konstruktoren In obigem Fall erbt die Klasse Kreismitmittelpunkt also die Variablen radius, pi und double und die Methode flaeche. Klassen k¨onnen nur eine Oberklasse haben, nicht mehrere. Es ist n¨otig, neue Konstruktoren zu definieren, da ja in der Regel zus¨atzliche Funktionalit¨aten implementiert werden m¨ussen. Man kann den Konstruktor der Oberklasse f¨ur die neue Definition verwenden, so wie oben dargestellt. Das Statement super darf nur am Anfang des Konstruktors stehen. Es ruft den entsprechenden Konstruktor der Oberklasse auf. Da Java sicherstellen muß, daß die Objekte inklusive aller ererbten Felder ad¨aquat initialisiert sind, ruft Java immer implizit entweder den Konstruktor der Oberklasse auf, so das nicht explizit wie oben geschieht oder ein anderer Konstruktor bem¨uht wird. Wird f¨ur Unterklassen kein Konstruktor definiert, dann ist der Default-Konstruktor folgendes public (){ super();} Dieses schl¨agt fehl, wenn kein parameterloser Konstruktor der Superklasse existiert. Soweit zu den Konstruktoren. Nun zu den anderen Elementen: die Unterklasse erbt alles wie oben angegeben. Da keine als private markierten Methoden und Variablen und keine Instanzmethoden und Variablen geerbt werden, k¨onnen diese nicht angesprochen werden. Nichtsdestotrotz existieren sie als Elemente der Oberklasse und ererbte Methoden der Oberklasse, die auf diese Elemente zugreifen, funktionieren. H¨atten wir also radius oder pi als private deklariert, h¨atten wir keinen Zugriff darauf von der Unterklasse und diese Elemente w¨aren nicht sichtbar in der Unterklasse. Nichtsdestotrotz existieren diese Elemente als Felder der Objekte und k¨onnen durch die ererbten Methoden der Oberklasse manipuliert werden. Die F¨ahigkeiten der Oberklasse bleiben also erhalten, auch wenn sie der Unterklasse verborgen sein k¨onnen. Die Unterklasse kann lediglich die Interna dieser F¨ahigkeiten nicht einsehen und benutzen, sondern nur die entsprechend als nicht private gekennzeichneten Methoden. Schematisch erhalten wir also etwa folgendes Bild, wobei die gepunkteten Linien die Elemente der Oberklasse, die gestrichelten die davon abgeleitete Klasse markieren:
Konstruktoren
benutzt
Methoden/Variablen
private Methoden private Variablen
Konstruktoren
benutzt
public/protected/package Methoden und Klassen
sichtbar in der Unterklasse existiert in der Unterklasse
4.4 Klassen und Methoden
63
Unterklassen k¨onnen weitere Unterklassen haben, wir k¨onnten also etwa eine weitere Klasse wie Kreismitmittelpunktundfarbe extends Kreismitmittelpunkt definieren. Wir sagten bereits, daß Methoden, die nicht private deklariert sind, in der Unterklasse sichtbar sind. Ab und zu m¨ochte man aber die Methoden der Oberklasse nicht in dieser Form verwenden. Etwa wenn man eine Implementation vorfindet, die fast stimmt, aber in einem Punkt bezogen auf einige Methoden ge¨andert werden sollte, m¨ochte man die Methoden austauschen. Das geht, ¨ es nennt sich Uberschreiben oder Overriding. Nehmen wir an, die Klasse Kreis bes¨aße sch¨one und schwierig zu implementierende Funktionalit¨aten wie etwa graphische Darstellungsm¨oglichkeiten etc. Allerdings w¨urden wir gerne Quadrate statt Kreise verwenden. Quadrate besitzen einen anderen Fl¨acheninhalt. Wir m¨ussen also die Methode flaeche u¨ berschreiben. class Quadrat extends Kreis{ Kreis(double radius){super(radius);} Kreis(){super();} double flaeche(){return(radius*radius);} Die Klasse erbt die Felder radius und pi. Die Konstruktoren werden weitgehend u¨ bernommen. Die Methode Fl¨ache wird u¨ berschrieben. Der Aufruf Kreis kreis=new(42.0); Quadrat quadrat=new(42.0); System.out.println(kreis.flaeche()); System.out.println(quadrat.flaeche()); Liefert die Ausgabe 5538.96 1764.0 Man kann nicht nur Methoden, sondern auch Variablen u¨ berschreiben, ihnen zum Beispiel einen anderen Typ oder (z.B. als final Konstante) einen anderen Wert geben. Ab und zu kommt es vor, daß man doch die Methode der Oberklasse verwenden m¨ochte, was dann durch eine explizite Referenz super geschehen kann. Ein Beispiel: class Quadrat extends Kreis{ static final double pi=3.14159; Quadrat(double radius){super(radius);} Quadrat(){super();} double flaeche(){return(super.flaeche()/pi);} Es wird hier die Fl¨achenberechnung der Oberklasse verwendet, um die Fl¨achenberechnung f¨ur Quadrat zu implementieren. Die Felder der Oberklasse bekommt man analog durch das Schl¨usselwort super. Damit ist die Methode flaeche f¨ur diese Unterklasse neu definiert. Immer, wenn f¨ur ein Objekt der Klasse Quadrat die Methode flaeche benutzt wird, ist dieses die Methode ¨ der Unterklasse. Achtung: bei der Ubergabe von Parametern kann ein Parameter einer Klasse als Objekt einer Unterklasse instanziiert werden, d.h. ein Parameter Kreis kann mit einem Quadrat belegt werden. Auch dann wird die f¨ur Quadrat zust¨andige Methode flaeche verwendet. Intern ist dieses durch sogenannte dynamic method invocation realisiert. Die in einem Programm gebrauchten Methoden sind an das betrachtete Objekt gebunden und werden ausgehend vom Objekt dynamisch w¨ahrend der Laufzeit aufgerufen. M¨ochte man verhindern, daß Methoden u¨ berschrieben werden, deklariert man sie als final. M¨ochte man verhindern, daß Klassen abgeleitet werden k¨onnen, verwendet man ebenfalls final, d.h. in beiden F¨allen ist dieses die endg¨ultige Version der Klasse bzw. Methode.
4 DATENSTRUKTUREN
64
Dieses war eine schnelle Tour durch die Grundz¨uge der Objektorientierung. Da sehr viele neue ¨ Begriffe betrachtet wurden, hier noch einmal der Uberblick der wichtigen Elemente, die wir gelernt haben: • Klassen und Objekte einer Klasse • Zugriffsrechte public/private/protected/package • Methoden und Konstruktoren ¨ • Uberladen • Klassen- und Instanzvariablen • Primitive Typen und Referenztypen • Vererbung ¨ • Uberschreiben Diese Prinzipien erlauben einen sehr eleganten Entwurf von Programmen, der vor allem durch Kapselung (also leichte Wartung und Austauschbarkeit der einzelnen Einheiten) und Modularit¨at entspricht. Rekursion Wir wollen als letzten wichtigen Punkt in diesem Kapitel noch ein mit der Nutzung von Methoden m¨ogliches Paradigma kennenlernen. Methoden erlauben nicht nur, Programmteile zu kapseln und Modularisieren, sondern auch prinzipielle Steuerung des Kontrollfluß durch rekurisve Methodenaufrufe. Was ist Rekursion? Schauen Sie in diesem Skript auf Seite 64 nach! Wenn Sie jetzt bl¨attern, werden Sie merken, daß dieser Verweis ein Bezug auf die Stelle ist, an der wir uns gerade befinden, ein R¨uckbezug. Rekursion bedeutet, daß Methoden sich gegenseitig verwenden, insbesondere auch sich selbst verwenden k¨onnen, wenn nur passende Objekte da sind. Wir demonstrieren dieses an einem einfachen Beispiel: Stille Post. Stille Post funktioniert so, daß ein eine Anzahl Leute in einer Reihe stehen und, angefangen vom ersten, ein Wort weitergefl¨ustert wird. Dieses k¨onnte man mit den bisherigen M¨oglichkeiten z.B. in einer Schleife implementieren. Wir verwenden hier eine rekursive L¨osung. class Mensch{ int hoervermoegen; int message; Mensch naechster; Mensch(int hoervermoegen, Mensch naechster){ this.hoervermoegen=hoervermoegen; this.naechster=naechster; this.message=0; } Mensch(int hoervermoegen){ this.hoervermoegen=hoervermoegen; this.naechster=this; this.message=0; }
4.4 Klassen und Methoden
65
void pass(int message){ // Weitergeben der Nachricht message+=this.hoervermoegen; this.message=message; if (!this.last()) naechster.pass(message); } void output(){ System.out.print(this.message); if (!this.last()) naechster.output(); } boolean last() { return (this.naechster==this); } } class stillepost{ public static int Max; // wieviele public static int Nachricht;// erste Nachricht public static void main(String[] args){ Max=Integer.parseInt(args[0]); Nachricht=Integer.parseInt(args[1]); // Achtung: einfacher Generator!!!! Mensch letztermensch= new Mensch((int)(Math.random()*10-5)); Mensch mensch=letztermensch; for (int i=0;i<Max-1;i++) mensch=new Mensch((int)(Math.random()*10-5),mensch); mensch.pass(Nachricht); letztermensch.output(); } } Wir haben hier Nachrichten einfach als Integer realisiert. Die Tatsache, daß die Menschen normalerweise nicht genau h¨oren und etwas falsches weitergeben wurde hier durch die Addition einer Zufallszahl realisiert. Man sieht, die Weitergabe geschieht rekursive, der Mensch veranlasst, so er nicht der letzte Mensch ist, den jeweils n¨achstn, die Nachricht weiterzugeben. Zum Schluß dieses Kapitels steht hier ein l¨angeres (klassisches) Beispiel eines objektorientierten Entwurfs, das Rekursion sehr viel extensiver gebraucht: das acht Damen Problem. Acht, allgemeiner n Damen sollen so auf einem Schachbrett platziert werden, daß sie sich nicht gegenseitig bedrohen. Zwei Damen bedrohen sich, wenn sie in derselben Zeile, Spalte oder Diagonale stehen. F¨ur n = 4 gibt es zwei L¨osungen, f¨ur n = 8 schon einige mehr. Dennoch ist das Problem sehr aufwendig, da man ja im Prinzip alle m¨oglichen Stellungen der Damen ausprobieren muß. Wie k¨onnen davon ausgehen, daß in jeder Spalte genau eine Dame steht, und die Damen dementsprechend nummerieren. Dann bleiben aber noch einige Zeilen auszuprobieren, f¨ur die erste Damen n m¨ogliche Zeilen, f¨ur die zweite n = 1, . . . , insgesamt n!. Die Basisidee f¨ur einen objektorientierten Entwurf ist, nacheinander in jede Spalte eine Dame zu stellen, die sich selbst eine zul¨assige Zeile sucht. Dazu muß sie u¨ berpr¨ufen, ob sie nicht von einer anderen Dame geschlagen wird und, so sie keine Zeile mehr findet, Vorg¨angerdamen eventuell anschubsen, damit diese an neue Positionen wandern. Das Program besteht so nur aus lokalen Operationen und Messages der Objekte. Der Gesamtablauf ist dann denkbar einfach: die oberste Ebene generiert nacheinander acht Damen in den acht Spalten und sagt ihnen sukzessive, sie m¨ogen doch eine L¨osung finden. Die L¨osung wird dann ausgedruckt:
4 DATENSTRUKTUREN
66
class Queens{ public static int MAX; // Anzahl der Zeilen public static void main(String[] args){ MAX = Integer.parseInt(args[0]); // Lese Anzahl Zeilen Queen lastQueen = new Queen(1); // Setze Queen in Row 1 boolean geht=true; // die anderen Queens for (int i=2; (i "+nach); } } void zieheviele(int wieviel,int von,int nach){ if (wieviel>0){ zieheviele(wieviel-1,von,frei(von,nach)); zieheeins(von,nach); zieheviele(wieviel-1,frei(von,nach),nach); } } int frei(int eins, int zwei){return (3-eins-zwei);} } Dabei haben wir die neue Methode setzmichint benutzt, die ein Inhaltsobjekt mithilfe eines int statt String setzt. Die Initialisierung eines Hanoi-Turms mit n Scheiben und der Aufruf zieheviele(n,0,2) f¨uhren dann zu einer L¨osung, n Scheiben von 0 nach 2 zu bewegen.
5.3 B¨aume
87
5.3 B¨aume Wir kommen jetzt zu nicht-linear verzeigerten Datenstrukturen. Als erstes betrachten wir B¨aume, die eine in der Informatik sehr n¨utzliche Datenstruktur darstellen z.B. um tats¨achliche Abstammungsb¨aume zu modellieren, Terme und Formeln darzustellen, Klassenhierarchien von Java zu visualisieren, etc. Formal ist ein Baum wie folgt definiert: ein Baum ist entweder • der leere Baum, • oder er besteht aus einem Knoten, der ein Element speichert, und auf eine Liste von Teilb¨aumen t1 , . . . , tn f¨ur ein n ∈ N verweist. Diese Teilb¨aume d¨urfen auch leer sein. B¨aume findet man h¨aufig wie folgt dargestellt: 1
2
4
3
7
6
5
9
10
8
11
Knoten sind durch Kreise markiert, die durch Pfeile zu ihren Teilb¨aumen verbunden sind. Leere B¨aume werden dabei u¨ blicherweise nicht dargestellt. In den Kreisen steht in diesem Fall der Inhalt, eine einfache Zahl. Bei diesem Baum sind die Knoten einfach nur durchnummeriert. Man k¨onnte statt einer einfachen Durchnummerierung eine Formel darstellen: +
*
4
+
−
9
*
6
10
8
11
Dieser Baum repr¨asentiert die Formel (4 ∗ (−9)) + (6 + (10 ∗ 11) + 8). Einige Sprechweisen • Knoten in diesem Baum stehen durch die durch Teilb¨aume gegebenen Verbindungen in direkter Beziehung zueinander. Sind zwei Knoten n1 und n2 miteinander verbunden, und ist n2 in einem zu n1 zugeordneten Teilbaum, dann heißt n1 Vater/Elter von n2 und n2 Kind von n1 . Z.B. ist 1 Elter von 2 und 3, 2 und 3 sind Kinder von 1. • Knoten, die keine Kinder haben, heißen Bla¨ tter. Es gibt genau einen Knoten, der keinen Elter hat, dieser heißt Wurzel. Alle anderen Knoten heißen innere Knoten. In obigem Baum ist 1 die Wurzel, 4, 9, 6, 10, 11, und 8 die Bl¨atter.
¨ ABSTRAKTE DATENTYPEN 5 ALGORITHMEN FUR
88
• Ein Pfad in einem Baum ist eine Folge von Knoten n1 , . . . , ni , so daß jeweils ni Elter von ni+1 ist. Etwa 1, 3, 6 oder 1, 2, 5. Der leere Pfad enth¨alt gar keinen Knoten. Die L a¨ nge eines Pfades ist die Anzahl der in ihm enthaltenen Knoten. • Die H¨ohe eines Baumes ist die maximale L¨ange der Pfade in diesem Baum. Etwa im Beispiel ist die H¨ohe 4. Der leere Baum hat H¨ohe 0. Die Tiefe eines Knotens ist die L¨ange eines Pfades von der Wurzel bis zum Knoten. Etwa 5 hat die Tiefe 3. • Ein Baum heißt k-Baum, falls alle Knoten maximal k Kinder haben. Ein Baum heißt bin a¨ rer Baum, falls alle Knoten maximal zwei Kinder haben. Wir betrachten hier exemplarisch bin¨are B¨aume. Was f¨ur Methoden sollten zur Verf¨ugung stehen? Genau wie bei Listen m¨ochte man • den leeren Baum initialisieren, • testen, ob der Baum leer ist, • die Wurzel addressieren, • testen, ob wir an einem Blatt sind, • zum linken oder rechten Kind gehen, • von einem Knoten den Wert auslesen oder setzen, • Knoten einf¨ugen und l¨oschen; Knoten einf¨ugen und l¨oschen ist allerdings etwas problematisch: was sollen wir mit den Kindern tun, wenn wir einen Knoten l¨oschen bzw. einen Knoten einf¨ugen. Eine sinnvolle Beschr¨ankung ist daher, nur im leeren Baum oder leeren Kindern einf¨ugen zu k¨onnen und nur Bl¨atter l¨oschen zu d¨urfen. Anders als bei Listen ist es nicht unbedingt ratsam oder erforderlich, den Baum zusammen mit einer aktuellen Sichtbarkeitsposition zu realisieren, da man sich am Baum nicht in linearer Weise ‘langhangeln’ kann. Wir ben¨otigen also keinen Verweis auf eine aktuelle Position im Baum. Stattdessen verwendet man oft rekursive Aufrufe, gegeben eine konkrete Knotenposition im Baum. Es ist daher n¨utzlich, wenn man sich von einer gegebenen Knotenposition aus am Baum entlanghangeln kann. Wir statten daher Baumelemente mit entsprechenden Methoden aus, die ein Navigieren im Baum gestatten. Baumalgorithmen sind entsprechend der rekursiven Struktur von B¨aumen h¨aufig rekursiv aufgebaut. Ein einfaches Beispiel, das Berechnen der H¨ohe eines bin¨aren Baums etwa, k¨onnte wie folgt aussehen: wir nehmen an, daß zu einem konkreten Knoten act der linke Teilbaum durch die Methode left, der rechte Teilbaum durch die Methode right erhalten werden kann. Die Methode isempty testet, ob ein Baum leer ist. Der Algorithmus ist dann wie folgt: int height(knoten act) { if (act.isempty()) return 0; int a = height(act.left()); int b = height(act.right()); if (a0){ root=new Baumelement(); rootreadfrom(args,0); } } String out(){ // Ausgabe if (isempty()) return " "; return root.out(); } int eval(){ //Auswertung if (isempty()) return 0; return root.eval(); } public static void main(String[] args){ Baum term = new Baum(); term.readfrom(args); System.out.print("Der Term "+term.out()); System.out.println(" ist "+term.eval()); } } Mithilfe dieser Klasse werden Ausdr¨ucke rekursiv als Terme interpretiert, entsprechend ihrer Struktur in einem Baum gespeichert mit Operationen als innere Knoten und Werten als Bl¨attern, und rekursive ausgewertet. Der Aufruf java Baum ( ( 4 - 2 ) * ( ( 6 + 3 ) + ( ( 7 - 4 ) * 4 ) ) ) ¨ man f¨uhrt zur Ausgabe Der Term ((4-2)*((6+3)+((7-4)*4))) ist 42. 19 Ubergibt einen nicht g¨ultigen Ausdruck, terminiert das Programm entweder vorzeitig mit einem Fehler, oder es wird ignoriert (fehlende Klammern am Ende werden etwa einfach u¨ bersehen). Heapsort Baumstrukturen spielen auch eine große Rolle beim Abspeichern von Daten. Sie erm¨oglichen, Daten schnell nach ihrer Gr¨oße geordnet abzuspeichern und wiederzufinden. Es existieren dazu 19 Man muß bei der Eingabe die Zeichen *, ( und ) sch¨utzen, da sie sonst von der Shell als Sonderzeichen gedeutet werden, das geschieht z.B. mit \. Der Aufruf ist also eigentlich
java Baum \( \( 4 - 2 \) \* \( \( 6 + 3 \) + \( \( 7 - 4 \) \* 4 \) \) \)
5.3 B¨aume
93
verschiedene spezielle Typen von B¨aumen, die geeignete Bedingungen an die Anordnung der Elemente erf¨ullen, so daß man Auslesen und Einf¨ugen sehr effizient gestalten kann. Wir wollen hier als kleine Demonstration dessen einen weiteren Sortieralgorithmus kennenlernen, den Heapsort. Ein Heap ist ein bin¨arer Baum mit der Bedingung, daß der Wert eines Knotens im Baum gr¨oßer ist als der Wert seiner Kinder. Insbesondere kann man in einem Heap das Maximum aller Werte sehr schnell finden: es ist der Wert an der Wurzel des Baums. Dieses dient als Idee f¨ur folgendes Sortierverfahren: Man liest die Elemente nacheinander ein, so daß sie einen Heap bilden. Danach entfernt man nacheinander jeweils das Element and der Wurzel (das noch verbleibende gr¨oßte Element) und repariert den Heap, bis kein Element mehr u¨ brig bleibt. Die Folge der Elemente ist dann der Gr¨oße nach sortiert. Man ben¨otigt f¨ur diese Prozedur zwei zentrale Methoden: swim und sink. • swim: Reparatur des Heap, wenn ein neues Element als Blatt eingef¨ugt wird. F¨ur ein beliebiges Element, das als Blatt eingef¨ugt wird, muß die Bedingung des Heap nicht erf¨ullt sein. Ist das Element zu groß, dann muß es an einer h¨oheren Stelle des Heap aufgeh¨angt werden, es muß quasi nach oben schwimmen, bis es richtig steht. Algorithmisch kann man dieses dadurch realisieren, daß man das neu eingef¨ugte Element mit seinem Elter vergleicht. Ist der Elter kleiner, tauscht man das neue Element mit dem Elter. Und f¨ahrt mit dem Knoten fort, an dem das Element jetzt steht, da es immer noch gr¨oßer als sein neuer Elter sein kann. Sp¨atestens an der Wurzel ist dann Schluß. Diese Prozedur ben¨otigt man beim sukzessiven Einf¨ugen der Elemente in den Heap. • sink: Reparatur des Heap, wenn ein neues Element als Wurzel eingef¨ugt wird. Genau umgekehrt zur vorherigen Prozedur kann es sein, daß die Wurzel zu klein ist. Sie muß also soweit nach unten sinken, bis sie an der richtigen Stelle steht. Dazu vergleicht man die Wurzel mit dem gr¨oßeren der Kinder, und tauscht, sodern das Kind gr¨oser als der Elter ist. Mit dem neuen Knoten f¨ahrt man dann fort, bis keine Fehlstellung mehr vorliegt oder man an den Bl¨attern angekommen ist. Diese Prozedur wird ben¨otigt, wenn die Wurzelelemente des Heap iterativ gel¨oscht und durch ein beliebiges anderes Element des Heaps erzetzt wird. Diese beiden Methoden bilden das Kernst¨uck des Heapsort. Wir stellen hier eine konkrete Implementation innerhalb von einem Array vor. Man kann die Elemente eines bin¨aren Baums in einem Feld abspeichern und hier sehr schnell f¨ur ein gegebenes Element Kinder/Eltern referenzieren: Element 1 speichert die Wurzel, 2 und 3 dessen Kinder, 4 und 5 die Kinder von Knoten 2, 6 und 7 die Kinder von Knoten 3 usw. Allgemein findet man die Kinder von Element i an den Stellen 2i und 2i + 1, den Elter von i an der Stelle i/2 (ganzzahlige Division). Damit kann der Heapsort sehr einfach wie folgt in einem Feld implementiert werden: class heap{ int[] h; int n; heap(String[] wovon){ n=wovon.length+1; h = new int[n]; for (int i=1;i0)&&(j>0)&(imove(ob2); write("Der L¨ owe befindet sich jetzt im K¨ afig.\n"); return 0; } Zur weiteren Erheiterung: das Shell-Script: #!/bin/sh for $tier in $W¨ uste do owe ] if [ $tier = $L¨ afig; exit 0 then mv $tier $K¨ fi done Die L¨osung als Pseudocode, wobei, wie u¨ blich, einige unwesentliche Details weggelassen wurden: MODULE Fang; FROM Problem IMPORT Loesung; BEGIN; Loesung; END Fang.
Es gibt weitere L¨osungen, informatorische, aber auch Beitr¨age der Mathematik und Physik, sehen Sie z.B. http://www.uni-graz.at/imawww/pages/humor/lions.html
7.2 Ein paar Schlagworte Es ist Zeit, ein paar technische Terme im Zusammenhang mit Programmiersprachen, die wir teilweise schon gebraucht haben, zu erl¨autern. • Compiler/Interpreter: Sprachen, die nicht selbst Maschinensprache sind, werden entweder compiliert, d.h. ein und f¨ur alle Mal in Maschinensprache u¨ bersetzt, oder interpretiert, d.h. erst w¨ahrend des Ausf¨uhrens St¨uck f¨ur St¨uck in Maschinensprache umgewandelt. Compilierte Programme sind in der Regel schneller, da einige Optimierungen gemacht werden k¨onnen. Interpretierte Programme sind dagegen plattformunabh¨angiger, und erlauben eine ¨ Anderung des Programmtexts zur Laufzeit. Java wird in eine Zwischensprache compiliert und dann interpretiert.
156
7 PROGRAMMIERSTILE
• Skriptsprache/Hochsprache/Maschinensprache/Anwendungssprache: Es gibt verschiedene Ebenen der Programmierung mit jeweils unterschiedlichen Anforderungen. Maschinensprache ist direkt, sehr elementar und f¨ur den Menschen schwer verst¨andlich. Maschinennahe Sprachen werden in zeitkritischen und Hardware-nahen Bereichen verwendet. Hochsprachen sind multi-purpose Programmiersprachen wie etwa Java, die u¨ bliche Algorithmen wie Sortieren, Rechnen, . . . umsetzen k¨onnen. Skriptsprachen sind mit den wichtigsten Kontrollstrukturen versehene Sprachen meist auf Betriebssystemebene, die ein einfaches Automatisieren von Systemaufrufen erm¨oglichen, wie etwa den mehrfachen Start eines Java-Programms mit unterschiedlichen Parametern und die Auswertung der Ergebnisdateien. Skriptsprachen beinhalten Systembefehle und einige M¨oglichkeiten, Terme zu manipulieren, allerdings meist keine modularen oder prozeduralen Elemente oder Typen. Anwendungssprachen wie etwa Mathematica und Matlab sind speziell f¨ur Anwendungsbereiche wie etwa mathematische Symbolmanipulation oder Darstellung mathematischer Mannigfaltigkeiten geschrieben. Sie beinhalten umfassende fertige Algorithmen des jeweiligen Anwendungsgebietes (z.B. L¨osen linearer Gleichungssysteme), dazu u¨ bliche Kontrollstrukturen, aber nur wenig Modularisierung und Typen. • Typenbindung: Typengebundene Sprachen verwenden Variablen strikt nur f¨ur den Typen, f¨ur den sie deklariert wurden, z.B. Integer. Bei nicht typengebundenen Sprachen kann man Variablen benutzen und ihnen im Laufe des Programms einen beliebigen Inhalt zuweisen. Skriptsprachen sind u¨ blicherweise nicht typengebunden. In maschinennahen Sprachen wie C ist der vermischte Gebrauch mehrerer Typen (etwa Zeiger und Arrays) m¨oglich, da man diekten Speicherzugriff hat. Logische Sprachen erlauben (in purer Form) teilweise nur Symbole (sogenannte Herbrandalgebra). • Speicherverwaltung: In klassischen Hochsprachen ist der Programmierer in der Regel selbst f¨ur die Speicherverwaltung zust¨andig. D.h. er muß f¨ur Variablen, deren Gr¨oße nicht klar ist (Listen etc.) geeigneten Speicherplatz explizit reservieren und wieder freigeben. Das ist eine sehr schnelle und effiziente M¨oglichkeit, da man unter anderem direkt an den Speicherpl¨atzen rechnen kann, aber auch eine stete Quelle f¨ur Programmierfehler, wenn man auf unzul¨assigen Speicherplatz zugreift (die ber¨uhmten segmentation faults in C. . . ). Java nimmt dem Programmierer die Arbeit ab. Der Befehl new reserviert entsprechenden ben¨otigten Speicherplatz und liefert einen Verweis auf den Speicher. Java sorgt selbst daf¨ur, daß nicht mehr ben¨otigter Speicher freigegeben wird. Nicht ben¨otigter Speicher ist dabei solcher, auf den keine im Programm noch ben¨otigte Referenz verweist. Der sogenannte garbage collector ist ein stetig je nach Systemauslastung im Hintergrund laufender Prozess, der diesen freien Speicher entdeckt, aufsammelt und wieder freigibt. • call by value/call by reference: Variablen¨ubergabe f¨ur Methoden oder Prozeduren kann auf zwei verschiedene Arten geschehen: durch eine Kopie einer Referenz auf ein Objekt, oder durch eien Kopie des im Objekt stehenden Inhalts. Die Auswirkungen sind unterschiedlich, ¨ da Anderungen des Objekts Auswirkungen auf das gesamte Programm haben oder nicht. In Java sind alle Variablen call-by value, also als Inhalt u¨ bergeben, wobei sich dieses bei Referenztypen als call by reference auswirkt, da die Variablen nur als Referenz tats¨achlich vorliegen. • Debugging: nennt man den Prozess der Fehlersuche. Da das einen großen Teil der Entwicklungszeit einnimmt, gibt es hierf¨ur m¨achtige Tools, f¨ur Java etwa den Java Debugger jdb als Kommandozeilenorientierte Version oder graphische Pendants.
7.3 Prolog
157
• Dokumentation: eine gute Dokumentation von Programmen ist essentiell insbesondere f¨ur ihre Wiederverwendung oder f¨ur die Benutzung durch andere Leute. Unumg¨anglich ist eine angemessene Dokumentation auch beim Zusammenspiel mehrerer Programmst¨ucke von verschiedenen Programmierern. Wir wissen, wie man Kommentare in Java integriert. Ein Benutzer m¨ochte allerdings nicht in den Code schauen m¨ussen, um den prinzipiellen Aufbau eines Programms zu vesrtehen. Java stellt hier eine elegante M¨oglichkeit, automatisch aus Code Kommentare in Form geeigneter html-Seiten (also mit einem Internet-browser betrachtbarer Seiten) zu extrahieren. Javadoc ist in Java integriert und extrahiert aus dem Programmcode html-Texte, sofern sie mit einem doppelten ∗ (statt nur einfachem ∗) gekennzeichnet sind. Zus¨atzlich zur Extraktion dieser gekennzeichneten Bereiche extrahiert javadoc selbst¨andig eine Visualisierung der Klassen: jede Klasse wird in der durch die Unified Modeling Language (UML) vereinbarten Weise charakterisiert, d.h. ihre Methoden und ariablen werden zusammen mit der Sichtbarkeit (und etwaigen Kommentaren) dargestellt. Klassendiagramme und weitere Abh¨angigkeiten sind ebenfalls integriert.
7.3 Prolog Wir haben jetzt nur einen sehr kurzen Blick auf andere Sprachen geworfen. Diesen wollen wir f¨ur eine fundamental von Java verschiedene Sprache etwas ausweiten: Prolog, einer logischen Programmiersprache. Prolog basiert auf Logikkonzepten und ist tats¨achlich aus Entwicklungen entstanden, logische Aussagen automatisch zu beweisen. Der Kern hinter Prolog ist das sogenannte Resolutionskalk¨ul, ein Logikkalk¨ul zum Beweis von Aussagen. Es gibt verschiedenste Prolog-Systeme, bekannt sind etwa Quintus-Prolog oder das freie und mit Quintus weitgehend kompatible SWI-Prolog (siehe etwa http://www.swi-prolog.org/). Wir f¨uhren hier die Grundlagen von Prolog ein, wobei wir im Hinterkopf haben, daß es um logische Formeln in den Programmen geht. Programme bestehen tats¨achlich aus logischen Formeln in spezieller Form. Entsprechend wiederholen wir vieles, was wir im Zusammenhang mit logischen Formeln schon geh¨ort haben. Variablen Variablen und Variablenbelegungen in Prolog sind die Grundeinheiten von Formeln: Terme. Ein Term ist entweder eine • Variable, in Prolog entweder ein mit einem Großbuchstaben anfangendes Wort wie X oder Aha, oder eine sogenannte anonyme Variable, die nur einmal auftaucht und daher gar keinen Name braucht, markiert durch . • Konstantensymbole, in Prolog mit einem Kleinbuchstaben beginnende W¨orter oder bekannte Konstantensymbole wie Zahlen 0, 1, 42, . . . • zusammengesetzte Terme der Form f (t1 , . . . , tn ) mit einem Funktionssymbol f (mit Kleinbuchtsaben beginnend) und Subtermen ti , etwa f (X, g(h(Y, ))), es gibt die u¨ blichen Funktionen +, ∗, . . . , die auch infix geschrieben werden k¨onnen. Wir haben gelernt, daß man Variablen und Symbole auswerten kann u¨ ber gegebenen Algebren. Prolog verwendet (bis auf einige F¨alle, die wir noch bekommen) eine spezielle Algebra mit einer trivialen Auswertung: die sogenannte Herbrandalgebra. Diese Algebra lebt u¨ ber Zeichenketten und interpretiert den Wert solcher Terme als die Zeichenkette, die diesen Tem beschreibt. Man l¨aßt also de facto die Terme so stehen, wie sie sind, und rechnet rein symbolisch weiter (das kennen Sie evtl. schon aus symbolischen mathematischen Systemen, etwa Maple kann rein symbolisch
7 PROGRAMMIERSTILE
158
Funktionen ableiten oder umwandeln, ohne irgendetwas tats¨achlich zu einer Zahl auszuwerten). Diese prinzipielle Sichtweise macht manches einfacher: statt komplizierten Datenstrukturen und deren Darstellung nachzugehen, nimmt man Zeichenketten, wie sie kommen, und k¨ummert sich nicht um etwaige Bedeutungen. Dieser Standpunkt ist gerechtfertigt, da es in Prolog um den Test geht, ob Formelmengen Modelle haben. Man kann zeigen, daß eine Formelmenge dann und nur dann ein Modell besitzt, wenn sie ein Herbrandmodell besitzt. Im Klartext also: wir brauchen uns nicht um aufwendige Algebren und Grundbereiche zu sorgen, die Symbole selbst reichen v¨ollig aus! Horn-Klauseln Wir erinnern uns: logische Formeln bestehen aus • atomaren Formeln, das sind entweder die Konstanten true und false (in Prolog fail), oder Pr¨adikatssymbole mit eingesetzten Termen, also etwa X < Y , oder p(X,Y), falls p ein Pr¨adikatssymbol ist, • negierte Formeln ¬ϕ • und-, oder-Verkn¨upfungen, folgt, oder a¨ quivalent, • quantisierte Formeln. Wir lassen erstmal Quantoren weg. Wir erinnern uns: jede Formel kann man in konjunktiver Normalform schreiben, d.h. als Konjunktion von Disjunktionen von Literalen, wobei ein Literal eine atomare oder negierte atomare Formel ist. Oder-Vekn¨upfungen von Literalen nennt man auch Klauseln, und man sagt, daß. eine Klausel gilt, falls die oder-Verkn¨upfung gilt. Eine Menge von Klauseln wird als die und-Verkn¨upfung der Klauseln interpretiert, sie gilt also, wenn jede Klausel gilt. Damit kann man (¨uber die konjunktive Normalform) jede quantorenfreie Formel in Klauselform bringen. Prolog-Programme bestehen aus logischen Formeln einer speziellen Form, sogenannten Horn-Klauseln. Horn-Klauseln sind Klauseln mit maximal einem positiven Literal. Es sind • Fakten: genau ein positives Literal. In Prolog wird das etwa als likes(john,mary). geschrieben. • Regeln: ein positives und mehrere negative Literale. Also so etwas wie a ∨ ¬b ∨ ¬c. Beachten Sie, daß man dieses auch als (b ∧ c) → a schreiben kann. Genauso notiert Prolog solche Regeln, mit dem Kopf a und dem Rumpf (b, c), einfach durch Komma statt durch und getrennt. Aus historischen Gr¨unden schreibt man − : statt → und den Kopf vorne, also a : −b, c. oder auch likes(mary,X):-human(X),honest(X). • Anfragen: sie enthalten nur negative Literale, etwa ¬b ∨ ¬c. man schreibt das in Analogie zu Regeln als ? − b, c.. Anfragen werden an ein Prolog Programm gestellt, und Prolog versucht, die Anfrage aus dem Wissen, also den bekannten Fakten und Regeln herzuleiten. Etwa ?-likes(mary,john).
7.3 Prolog
159
Ein m¨ogliches Prologprogramm w¨are also etwa die Wissensbasis likes(john,mary). honest(john). human(john). likes(mary,X):-human(X),honest(X). Durch die Einschr¨ankung auf Horn-Klauseln hat man tats¨achlich eine Teilmenge der logischen Formeln ausgezeichnet, man erh¨alt so nicht mehr alle m¨oglichen Formeln. Der Vorteil ist, daß man mit Hornklauseln logische Schl¨usse gezielt in einer normierten Reihenfolge und sehr effizient vornehmen kann. F¨ur allgemeine Formeln w¨are das nicht so einfach der Fall. Programmablauf Ein Programmablauf besteht rein formal in dem Versuch, zu zeigen, daß aus einem gegebenen Programm eine nachgefragte Formel folgt, etwa aus obigem Programm folgt likes(mary,john). Formal zeigt man das, indem man zeigt, daß die Formelmenge des Programms und das Negat der nachgefragten Sache kein Modell besitzt (das ist a¨ quivalent dazu, daß die Formel selbst folgt). Daher Anfragen (negierte Formeln). Die spezielle Form von Prologprogrammen macht es jetzt m¨oglich gezielt vorsugehen. Entweder ist das Nachgefragte ein Fakt in der Wissensbasis, d.h. man findet die Nachfrage als Tatsache, mehrere Schritte durch Regeln hergeleitet werden. Dazu muß das Nachgefragte Kopf einer Regel sein, und Prolog versucht, statt dem Kopf (der ja eigentlich zu zeigen ist), alle Literale im K¨orper zu zeigen (was ausreicht, da daraus ja der Kopf folgte). Dieses Vorgehen nennt man auch Resolution. In obigem Fall ergibt das: ?-likes(mary,john). ?-human(john),honest(john). ?-honest(john). yes
+ + +
likes(mary,X):-human(X),honest(X). human(john). honest(john).
Prolog hat also alles der Reihe nach nachgewiesen, wobei X am Anfang durch das zu betrachtende Individuum john ersetzt wurde. Heraus kam die leere Menge, d.h. die leere Klausel, die kein Modell hat. Die Aussage muß also aus der Regelmenge folgen, Prolog quittiert dieses mit yes. Fragen wir etwas anderes: ?-likes(mary,tweety). Prolog quittiert mit no, denn es kann die Frage zwar auf ?-human(tweety),honest(tweety). zur¨uckf¨uhren, aber dann ist Schluß, es ist nicht bekannt, ob tweety ein Mensch oder evtl. doch eher ein Pinguin ist, und die Ehrlichkeit ist auch fraglich. Beachten Sie, daß diese Abgleich mit Regelk¨opfen nur deswegen m¨oglich ist, weil man sich auf Hornklauseln beschr¨ankt hat. Mehr als ein positives Literal w¨urde zwei oder mehr Elemente im Kopf verursachen, man k¨onnte sich also nicht sicher sein, welches der beiden man inferieren kann. In Hornklauseln ist man sich dessen sicher. Logisch entspricht das dem Faktum, daß Hornformeln immer zu einem eindeutigen kleinsten Herbrandmodell f¨uhren, das Prolog auf diese Art berechnet. Kleinst bedeutet dabei, daß Prolog u¨ ber Sachen,¨uber die es nichts sicher weiß, negativ bescheidet (tweety k¨onnte ja durchaus ehrlich sein, nur weiß Prolog das nicht und verneint deswegen.) Backtracking Es kann sein, daß der erste Versuch einer Regel nicht unbedingt zum Erfolg f¨uhrt, oder daß es mehr als eine M¨oglichkeit zum Erfolg gibt. Prolog testet tats¨achlich der Reihe nach alle Fakten oder Regeln durch, bis entweder Erfolg aufgetreten ist, oder keine weitere M¨oglichkeit mehr u¨ brig ist. Das Backtracking bezieht sich dabei auf alle Zwischenschritte. Ein Beispiel: Pfade in einem azyklischen Graphen finden. Wir haben die folgende Regelbasis:
7 PROGRAMMIERSTILE
160 edge(a,b). edge(a,e). edge(b,c). edge(b,f). edge(e,f). edge(e,d). edge(g,d). edge(g,h). path(X,X). path(X,Y) :- edge(X,Z), path(Z,Y).
Die ersten acht Fakten beschreiben den Graphen, das Fakt path(X,X) sagt, daß es immer einen Pfad zu sich selbst gibt, und die Regel beschreibt die transitive H¨ulle, die man u¨ ber Zwischenknoten enth¨alt. Auf die Anfrage ?-path(a,c). hin wird folgender Suchbaum aufgebaut: ?−path(a,c).
?−path(b,c).
?−path(c,c)
?−path(e,c).
?−path(f,c).
?−path(f,c).
?−path(d,c).
Der linkeste Pfad f¨uhrt zum Erfolg, die anderen hier nicht. Prolog sucht in diesem Baum per Tiefensuche von links nach rechts nach einer L¨osung. Es kann dabei auch mehrere geben (die Prolog dann der Reihe nach findet, Eingabe ist ’;’), oder auch gar keine (der ganze Baum f¨uhrt zu Mißerfolg). Die Reihenfolge der Regeln bestimmt die Reihenfolge, in der der Baum aufgebaut wird, also auch das zuerst gefundene Ergebnis. Achtung: es kann unendliche Pfade geben! Trifft Prolog vor einem Erfolgspfad einen unendlichen Pfad, terminiert es nicht. Ein Beispiel: betrachten Sie die Erweiterung obigen Programms um das Faktum edge(b,a). Der Graph wird damit zyklisch! Wenn jetzt die Anfrage ?-path(a,g). gestellt wird, terminiert das Programm nicht, obwohl es offensichtlich ist, daß es so einen Pfad nicht geben kann. Was ist das Problem? Wir haben einen unendlichen Programmablauf bekommen: Die Anfrage f¨uhrt u¨ ber den Zyklus von a nach b zu folgender Abarbeitung: ?-path(a,g). ?-edge(a,Z),path(Z,g). ?-path(b,g). ?-edge(b,Z),path(Z,g). ?-path(a,g). Dieses muß man im Programm verhindern. Wie, sehen wir sp¨ater.
7.3 Prolog
161
Variablen/Unifikation Bisher haben wir Variablen nur marginal und implizit benutzt. Was ist die Rolle von Variablen? Prolog kennt keine Existenzquantoren, alle auftauchenden Variablen sind impliziert allquantifiziert. Etwa die Regel p(X) : −q(Y ), c(Z). bedeutet also in Formeln ∀X∀Y ∀Z((q(Y ∧ c(Z)) → p(X)). Dieses bedeutet keine Einschr¨ankung, da man in einer Formel Quantoren immer ganz nach vorne ziehen kann, und man auf Existenzquantoren verzichten kann (durch die sogenannte SkolemNormalform, auf die wir hier nur hinweisen). Es stehen also implizit allquantifizierte Formeln in Prolog. Es kommt in Prolog vor, daß man zwei Terme ’gleich machen’ m¨ochte, d.h. man m¨ochte Variablen in den Termen so belegen, daß zwei betrachtete Terme u¨ bereinstimmen. Dieses dient dazu, Wissen innerhalb von Formeln zu propagieren. Wir haben das tats¨achlich in kleinem Rahmen die ganze Zeit getan: wir haben Variablen Konstanten zugewiesen. Das ganze k¨onnte bei komplexeren Termen aber auch schwieriger aussehen. ’Gleich machen’ von Termen nennt man Unifikation. Mathematisch sucht man dabei f¨ur gegebene Terme t1 und t2 eine Substitution σ der Variablen in den Termen, so daß t1 σ = t2 σ gilt. Ein Beispiel macht dieses sofort klar: Gegeben seien die Terme f (X, g(b, c)) und f (d, g(Z, C)). Wir sehen sofort: ersetzen wir X durch d, Z durch b und C durch c, dann sind die beiden Terme gleich. Zudem gibt es keine andere Substitution, die dasselbe liefert. Unifikation einer Variablen X und einer Konstanten c ersetzt X durch c. Was ist ¨ mit g(Z, f (A, 17, B), A + B, 17) und g(C, f (D, D, E), C, E). Ein wenig Uberlegung zeigt: wir m¨ussen Z durch 17 + 17, A durch 17, B durch 17 ersetzen. Es gibt auch die Situation, daß Terme nicht unifizierbar sind, etwa g(X) und f (Y ) – die Funktionssymbole sind unterschiedlich, also n¨utzen alle Variablenbelegungen nichts. Oder auch X und f (X) – bei jeder Bekegung von X w¨urden wir dasselbe im rechten Term auch tun, sind also mit dem lonken immer ’einen Schritt zur¨uck’. Solche Terme heißen nicht unifizierbar. Es gibt den weiteren Fall, daß Terme auf mehr als eine Weise unifizierbar sind. Ein Beispiel: f (X, Y ) und g(Z, Y ). Hier k¨onnte man X = Y = Z setzen. Es reichte aber auch, nur X = Y zu setzen. Die letztere Unifkation ist allgemeiner, als die erste, denn man kann die erste aus der zweiten durch weiteres Substitutieren erhalten. Die zweite substituiert quasi nur das, was ebene gerade n¨otig ist, um die gleichen Terme zu nerhalten, aber nicht mehr. Man kann zeigen, daß dieser allgemeinste Unifikator von zwei Termen, so es ihn gibt, bis auf Variabkenumbenennung eindeutig ist. Es idt der sogenannte most general unifier (mgu). Zu seiner Berechnung gibt es einen Algorithmus, der sukzessive die Variablen ersetzt, um alles gleich zu machen. Unifikationsalgorithmus ist in Prolog eingebaut, wenn Prolog die K¨opfe von Regeln betrachtet, und seine genaue Form braucht uns hier nicht zu interessieren. Wenn Prolog K¨opfe mit Anfragen abgleicht, unifiziert es also und setzt die sich ergebenden Variablen an alle anderen Stellen der Formel. Die im Endeffekt berechneten Variablen einer Anfrage werden am Schluß von Prolog ausgegeben und k¨onnen verwendet werden, um Informationen zu berechnen. Termauswertung Bevor wir uns Unifkation und Variablenbelegungen zunutze machen, eine weitere Konstruktion. Prolog rechnet u¨ ber Herbrandalgebren, was im Zuge von arithmetischen Ausdr¨ucken sehr l¨astig ¨ werden kann. Betrachten Sie etwa 42 und 2 ∗ 21. Uber einer Herbrandalgebra sind die beiden Terme weder gleich noch unifizierbar, da sie sich ja zu den (v¨ollig verschiedenen) Zeichenketten auswerten. Im wirklichen Leben m¨ochte man h¨aufig solche arithmetischen Ausdr¨ucke tats¨achlich auswerten (und zwar u¨ ber den nat¨urlichen oder reellen Zahlen!). Man muß Prolog hierzu explizit anhalten durch das Wort is. Die Aussage X is 2*21 f¨uhrt dazu, daß der rechte Term ausgewertet und der Wert X zugewiesen wird.
162
7 PROGRAMMIERSTILE
Kommen wir mit dieser Zusatzinformation an eines unserer ersten Programme, den ggt. In Prolog k¨onnen wir den ggt durch folgende Wissensbasis berechnen: ggt(X,0,X). ggt(0,X,X). ggt(X,Y,Z) :- Y>0, X1 is X-Y, X1 >= 0, ggt(X1,Y,Z). ggt(X,Y,Z) :- X>0, Y1 is Y-X, Y1 >= 0, ggt(X,Y1,Z). Diese vier Regeln besagen, daß der ggt von einer Zahl und Null die Zahl gibt, und im anderen Fall man den ggt nicht a¨ ndert, wenn man von der gr¨oßeren Zahl die kleinere abzieht (wie wir beim euklidischen Algorithmus gesehen haben). Die Anfrage ggt(15,5,X). f¨uhrt zu der Antwort yes mit Wert X=5. Dabei wurde dreimal Regel 1 angewandt und je der ggt von 10 und 5, 5 und 5, bzw. 0 und 5 nachgefragt. f¨ur 0 und 5 trifft das zweite Faktum zu, das dann auch Z auf 5 setzt. Man kann auch Anfragen der Form ?-ggt(1,2,3). stellen, die mit no quittiert werden. Anfragen der Form ?-ggt(X,2,2). sind hier nicht gestattet, da bei der Auswertung von is die beteiligten Variablen belegt sein m¨ussen (Symbole kann man nicht arithmetisch auswerten). Was w¨are passiert, wenn wir unser Programm abgewandelt h¨atten zu ggt(X,0,X). ggt(0,X,X). ggt(X,Y,Z) :- Y>=0, X1 is X-Y, X1 >= 0, ggt(X1,Y,Z). ggt(X,Y,Z) :- X>=0, Y1 is Y-X, Y1 >= 0, ggt(X,Y1,Z). Das Programm ist logisch immer noch richtig, wie man leicht sieht, doch f¨uhren Anfragen der Form ?-ggt(1,2,3). zu einer Endlosschleife, wie folgt: ?-ggt(1,2,3). ?-ggt(1,1,3). ?-ggt(1,0,3). ?-ggt(1,0,3). ?-ggt(1,0,3). ... Listen Datenstrukturen in Prolog sind per default Symbole in der Herbrandalgebra oder, wenn man es explizit verlangt, arithmetische Ausdr¨ucke, wie wir gesehen haben. Eine der wichtigsten Datenstrukturen, die man in Prolog verwendet, sind Listen von Objekten, f¨ur die daher eine spezielle Notation existiert. Eine Liste ist generell eine endliche Folge von Objekten, die man in Prolog in eckige Klammern einschließt: [a,b,c,a,e,f] bezeichnet die Liste mit Elementen a, b, c, d, a, e, f, [ ] bezeichnet die leere Liste. Man darf Listen auch beliebig schachteln, etwa [a,[a,b],c,[a,[e,f]]] bezeichnet die Liste mit Elementen a, [a,b], c und [a,[e,f]]. Man kann solche Listen direkt in Prolog verwenden, wobei sie durchaus auch Variablen enthalten d¨urfen;etwa
7.3 Prolog
163
[X,Y,X,Y] referiert auf irgendeine Liste mit vier Elementen, wobei das erste und dritte, und das zweite und vierte Element gleich sind. Allgemein ist eine Liste entweder die leere Liste, oder sie ist zusammengesetzt aus einem ersten Element und einer darauf folgenden (evtl. leeren) Restliste. Auf dieser rekursiven Notation basierend stellt Prolog weitere Techniken bereit, die Elemente einer Liste anzusprechen: [X|L] referiert auf die Liste mit erstem Element X und Restliste L. Dabei wird vereinbart: [a|[]] ist die Liste [a], es a¨ ndert also nichts, wenn die leere Liste an eine bereits bestehende geh¨angt wird. Man kann so etwa die Elemente einer Liste der Reihe nach durchgehen. Als Beispiel betrachten wir ein Programm, das testet, ob ein gegebenes Element in einer Liste vorhanden ist: iselement(X,[X|_]). iselement(X,[_|L]):-iselement(X,L). Ein Element ist in einer Liste vorhanden, wenn es entweder das erste Element ist, oder in der Restliste vorhanden ist. Ein zweites Beispiel, wie berechnet man aus einer gegebenen Liste von Zahlen das Maximum? max(X,L):-hilfsmax(-100000,X,L). hilfsmax(X,X,[]). hilfsmax(X,M,[A|L]) :- A>X, hilfsmax(A,M,L). hilfsmax(X,M,[A|L]) :- X>=A, hilfsmax(X,M,L). Dieses Vorgehen berechnet iterativ das Maximum der Listenelemente, wobei in einer Hilfsvariablen das Maximum des schon besuchten Anfangsst¨ucks mitgef¨uhrt wird. Die beiden Regeln zu der Hilfsfunktion unterscheiden die F¨alle, ob aktuell ein noch gr¨oßeres Element gefunden wurde. Sobald die Liste abgearbeitet wurde, ist das bis dahin gefundene Maximum auch das gesamte, d.h. der Wert wird in die ’R¨uckgabevariable’ X geschrieben. Wir k¨onnen Listen auch dazu verwenden, das Problem der unendlichen Pfaden bei zyklischen Graphen zu l¨osen: wir f¨uhren einfach jeweils in einer Liste alle schon auf dem aktuellen Versuch besuchten Knoten mit und testen nur solche, die noch neu sind. Als Programm: path(X,Y) :- legalpath(X,Y,[]). legalpath(X,X,_). legalpath(X,Y,H):- edge(X,Z), legal(Z,H), legalpath(Z,Y,[Z|H]). legal(_,[]). legal(Z,[H|T]) :- Z\==H, legal(Z,T). begin{verbatim} Wir haben hier also das Hilfspr¨adikat legalpath verwendet. Dieses speichert je alle schon besuchten Ecken und f¨ugt nur neue legale an, d.h. solche, die noch nicht Element der Besuchtliste sind. Dazu testet man die Ungleichheit f¨ur jedes Element der Liste. Ungleichheit ist in purem Prolog eigentlich nicht vorgesehen, da man in den zugrundeliegenden Hornklauseln mehr als ein positives Literal erhalten w¨urde. Die eingebaute Funktion X\==Y ist dann und nur dann wahr, wenn X und Y nicht unifizierbar sind. F¨ur bereits instanziierte Variablen ist das tats¨achlich ein Test auf Ungleichheit.
164
7 PROGRAMMIERSTILE
Negation as failure Prolog bedingt durch die Einschr¨ankung auf Hornklauseln eine echte Einschr¨ankung allgemeiner logischer Aussagen, das nur ein positives Literal pro Regel existieren darf. Um manche Sachen zu formulieren, kommt man daher um eine Art der Erweiterung um mehr als ein positives Literal nicht herum. Realisiert wird dieses durch eine spezielle Art der Negation, die auf der rechten Seite von Regeln erlaubt ist. \+ aussage bedeutet das Negat von Aussage. Dabei ist dieses nicht die streng logische Negation, sondern Prolog interpretiert dieses operational durch sogenanntes negation as failure. Sobald eine solche Negation angetroffen wird, versucht Prolog, die Aussage zu beweisen. Falls dieses fehlschl¨agt, wird das Negat als g¨ultig angesehen. Es werden bei diesem Versuch grunds¨atzlich keine Variablen zur¨uckgegeben. Es kann also bei der Abarbeitung von \+ aussage dreierlei passieren: • aussage kann aus der Wissensbasis abgeleitet werden (mit irgendeiner Variablenbelegung). Dann schl¨agt \+ aussage fehl. • aussage kann aus der Wissensbasis nicht abgeleitet werden. Dann ist \+ aussage wahr, aber es werden keine neuen Variablenbelegungen zur¨uckgegeben. • Der Versuch, aussage abzuleiten, f¨uhrt zu einer Endlosschleife. Das Programm \+ aussage terminiert dann nicht. Dieses ist kein logisches Negat, selbst wenn die Aussage abgeleitet werden kann, da es von der sogenannten closed world assumption ausgeht: alles, was nicht ableitbar ist, gilt nicht (obwohl wir das ja eigentlich gar nicht wissen k¨onnen). Ein Beispiel f¨ur die Anwendung: wir h¨atten obiges eingebautes Zeichen \== ersetzen k¨onnen durch legal(Z,L) :- \+ iselement(X,L). Zwei Demonstrationen der closed world assumption: guilty(X) :- \+ innocent(X). innocent(peter_pan). innocent(winnieh_the_pooh). Die Anfrage ?- guilty(tweety) f¨uhrt zu yes. good_hotel(goedels). good_hotel(freges). expensive_hotel(goedels). reasonable_hotel(X) :- \+ expensive_hotel(X). Die Anfrage ?-good hotel(X),reasonable hotel(X). liefert yes mit der Belegung X=freges. Das liegt daran, daß zun¨achst X mit goedels initialisiert wird, und dann der Nachweis reasonable hotel(goedels) fehlschl¨agt. Danach wird X mit freges belegt, und das Programm ist erfolgreich. Die Anfrage ?-reasonable hotel(X),good hotel(X). hingegen liefert no. Das liegt daran, daß zuerst mit nicht initialisiertem X die Anfrage reasonable hotel(X) gestellt wird. Dieses wird (noch ohne Wahlm¨oglichkeiten) zur Anfrage \+ expensive hotel(X) resolviert. Prolog versucht also, expensive hotel(X) zu beweisen, was mit X als goedels gelingt. Der Versuch schl¨agt also fehl und es steht auch keine Alternative zur Verf¨ugung, denn X ist ja uninstanziiert in das negation as failure gegangen – daß es eine weitere Belegung g¨abe, f¨ur die alles gelten w¨urde, interessiert hier nicht. Sie sehen also, es kann innerhalb dieses nicht mehr puren Prolog merkw¨urdige Effekte geben, daher sollte man solche Elemente m¨oglichst gering halten.
7.3 Prolog
165
Cut Cut, geschrieben als ’!’, ist ein weiteres prozedurales Element, das u¨ ber die logische Grundlage von Prolog hinausgeht. ’!’ ist ein Pr¨adikat, das immer verifiziert werden kann. Es hat den Effekt, daß mit Abarbeiten von ’!’ alle m¨oglichen Backtrackingm¨oglichkeiten f¨ur die gerade betrachtete Regel annuliert werden; das betrifft alternative Aufrufe zur gerade betrachteten Regel und innerhalb der Regel selbst schon gespeicherte Backtrackingm¨oglichkeiten. Es k¨onnen nur noch der jetzt betretene Pfad, Backtrackingm¨oglichkeiten bezogen auf eine andere Regel vor Aufruf der jetzigen Regel oder neu entstehende Backtrackingm¨oglichkeiten begangen werden. Alle schon bestehenden Backtrackingm¨oglichkeiten bezogen auf die Regel selbst werden abgeschnitten. Der Effekt von ’!’ kann bezogen auf den Ableitungsbaum graphisch so dargestellt werden: cut−Regel: A :− B1, B2, ..., Bn, !, C1, C2, ..., Cm.
?−A, rest.
backtracking vor dem Aufruf von A durch cut abgeschnitten
Versuche, A ohne cut−Regel abzuleiten
?− B1, B2, ..., Bn, !, C1, C2, ..., Cm, rest
durch cut abgeschnitten Versuche, B1, ... abzuleiten ?− !, C1, ..., Cm, rest.
backtracking von B1, ..., Bn
?−C1, ..., Cm, rest.
Betrachten wir als ein Beispiel das Programm liebt(X,Y):-male(X),female(Y). liebt(a,b). female(hanna). female(mary):-!. female(sarah). male(joe). male(john):-!. male(007). L¨osungen sind
weitere Versuche, A abzuleiten
166
7 PROGRAMMIERSTILE
• joe, hannah • joe, mary • john, hanna • john, mary • a, b Der ’!’ bei male(john):-!. f¨uhrt dazu, daß kein weiterer Versuch, male(X) anders zu beweisen, mehr unternommen wird. Ein gleiches gilt f¨ur female(mary):-!. liebt ist ein den Regeln mit ’!’ u¨ bergeordnetes Pr¨adikat, das noch weiter abgetestet wird und durch den ’!’ nicht beeinflusst ist. ’!’ wird aus verschiedenen Gr¨unden verwandt. Ein Grund ist etwa, ein nicht-deterministisches Pr¨adikat in ein deterministisches zu verwandeln, wo es nur eine M¨oglichkeit der Verifikation gibt, nicht mehrere. Als Beispiel: wir haben ein Pr¨asikat geschrieben, das testet, ob ein Element in einer Liste vorhanden ist. Ist ein Element mehrfach vorhanden, dann kann man dieselbe Aussage auf mehrere Weisen verifizieren. Prolog geht diese im Zweifel alle durch. Das ist sehr n¨utzlich, wenn man alle Elemente der Liste der Reihe nach aufz¨ahlen will, aber eher l¨astig, wenn man nur testen will, ob ein Element enthalten ist. ’!’ schafft Abhilfe: elcheck(X,[X|_]):-!. elcheck(X,[_|L]):-elcheck(X,L). Der ’!’ verhindert, daß ein einmal gefundenes Element nochmal gefunden werden kann. Ein anderer Einsatz von ’!’ ist, Exklusionen elegant zu erkennen. Das Programm max(X,Y,X) :- X>=Y. max(X,Y,Y) :- X=Y,!,Z=X. max(_,Y,Y).
7.3 Prolog
167
Assert/Retract Wir haben zur Behebung unendlicher Zyklen bei der Pfadsuche in zyklischen Graphen die in diesem Versuch bereits besuchten Knoten mitgef¨uhrt. Das verhindert Zyklen, ist allerdings trotzdem noch ziemlich ineffizient: beim Backtracking werden ja die besuchten Knoten der Liste zur¨uckgenommen, und es wird ein Knoten in einem neuen Versuch evtl. nochmal besucht. Prinzipiell kann also eine nicht erfolgreiche Suche so lange dauern, wie es zyklenfreie Pfade im Graphen gibt (das k¨onnen viele sein). Was wir hier br¨auchten, w¨are eine globale Markierung der schon besuchten Knoten. Prolog l¨aßt f¨ur solche Zwecke zu, die Wissensbasis dynamisch zu a¨ ndern. Das Pr¨adikat asserta(fakt). f¨ugt fakt. vorne an die Wissensbasis an (assertz(fakt). f¨ugt hinten an). Entsprechend l¨oscht retract(fakt). das erste unifizierbare fakt der Wissensbasis, retractall(fakt. l¨oscht alle. Wir erhalten so eine effizienter L¨osung unserer Pfadsuche path(X,Y) :- retractall(besucht(_)),!,legalpath(X,Y). legalpath(X,X):-!. legalpath(X,Y):- edge(X,Z), \+besucht(Z), asserta(besucht(Z)),legalpath(Z,Y). asserta und retract haben u¨ ber Backtracking hinausgehende permanente Wirkung f¨ur die Wissensbasis, sie k¨onnen also zur Umsetzung globaler Informationen (etwa bei dynamischem Programmieren) benutzt werden. Effizienz Prolog basiert massiv auf Backtrcking und Symbolmanipulation und ist damit besonders geeignet f¨ur Probleme, die sich mit solchen Paradigmen l¨osen lassen. Dazu geh¨oren insbesondere Algorithmen der symbolischen K¨unstlichen Intelligenz, etwa Logikalgorithmen (Beweiser) oder Planungsstrategien. Prolog kann aber sehr schnell sehr ineffizient werden, wenn zu viele Backtrackinglevel gespeichert werden m¨ussen. Daher haben viele Systeme Optimierungsstrategien eingebaut. Eine wichtige Optimierung bei quasi iterativ laufenden Regelableitungen ist die Optimierung der sogenannten tail-Rekursion. Betrachten wir dazu unser vorheriges Programm zum Berechnen des Maximums in Listen: max(X,L):-hilfsmax(-100000,X,L). hilfsmax(X,X,[]). hilfsmax(X,M,[A|L]) :- A>X, hilfsmax(A,M,L). hilfsmax(X,M,[A|L]) :- X>=A, hilfsmax(X,M,L). Wir h¨atten stattdessen auch schreiben k¨onnen maximum(X,Y,X):-X>=Y. maximum(X,Y,Y):-X 0}. Es muß f¨ur die Wortl¨angen li0 von C 0 f¨uP r Symbole mit Wahrscheinlichkeit pi > 0 gelten li0 ≤ L(C, S)/p, denn sonst h¨atten wir L(C 0 , S) = pj lj0 ≥ pli0 > p·L(C, S)/p = L(C, S). Da die Symbole mit Wahrscheinlichkeit Null nichts zum Code beitragen, gibt es also nur endlich viele verschiedene bessere Werte als L(C, S) entsprechend diesen endlich vielen verschiedenen W¨ortern begrenzter L¨ange. Wie bekommen wir konkret optimale Codes? Eine sehr bekannte Methode sind sogenannte Huffman-Codes. Wir beschr¨anken uns hier der Einfachheit halber auf bin¨are Huffmann-Codes. ¨ Diese werden iterativ konstruiert und beruhen auf folgenden zwei Uberlegungen:
8.2 Kodierung
173
• Wir identifizieren die Buchstaben sn−1 und sn mit einem einzigen Buchstaben s0 . Sei w1 , . . . , wn−1 ein instantaner Code von s1 , . . . , sn−2 , s0 . Dann ist w1 , . . . , wn−2 , wn−1 0, wn−1 1 instantaner Code von s1 , . . . , sn−2 , sn−1 , sn . (Dieses sieht man leicht mithilfe der Pr¨afixeigenschaft.). • Die Buchstaben mit geringer Wahrscheinlichkeit d¨urfen l¨angere Codeworte haben. Ein Huffmann-Code identifiziert sukzessive die zwei Buchstaben mit den kleinsten Wahrscheinlichkeiten miteinander, bis nur noch ein Symbol zu kodieren ist. F¨ur dieses wird ein Codewort, n¨amlich das leere Wort verwendet (das eigentlich nicht zul¨assig ist, aber hier trotzdem verwandt wird, um den Algorithmus elegant zu formulieren). Zum Originalalphabet kommt man, indem man die Codeworte wie oben f¨ur die jeweils zusammengef¨ugten Symbole durch Zuf¨ugen von 0 und 1 erweitert. Ein Beispiel: die Zahlen 1 bis 5 haben die Wahrscheinlichkeiten 0.3, 0.2, 0.2, 0.2, 0.1. Man erh¨alt: (1 − 0.3)
(2 − 0.2)
(3 − 0.2)
10
00
(4 − 0.2) 010
11
(5 − 0.1) 011
(4’ − 0.3) (2’ − 0.4)
(1’ − 0.6)
01
1 0 (1’’ − 1)
Die Symbole werden zun¨achst sukzessive vereinigt, so daß ein Baum entsteht. Entlang dieses Baums k¨onnen die Codierungen mit der beschriebenen Vorschrift gew¨ahlt werden. Theorem 8.9 Der Huffman-Code ist optimal. Es ist bereits klar, daß ein instantaner Code ensteht. Es muß also nur die Optimalit¨at gezeigt werden. Dazu zwei Hilfslemmata: • Sei ein Code C 0 f¨ur r Symbole und Source S 0 aus C f¨ur r − 1 Symbole und Source S wie im Huffman-Code durch Zuf¨ugen von 0 bzw. 1 gewonnen. Sei p = p i + pj die Wahrscheinlichkeit des ’vereinigten’ Symbols aus S 0 in S. Dann ist (mit li = lj = l + 1 f¨ur ein l nach Konstruktion) L(C 0 , S 0 ) − L(C, S) = pi (l + 1) + pj (l + 1) − pl = pi + pj = p. • Jede Source S hat einen optimalen bin¨aren Code, in dem zwei der l¨angsten Codew¨orter im Baum Kinder desselben Elter sind. Beweis: Sei C optimaler Code und xt l¨angstes Codewort mit einem Buchstaben t und einem Wort x. Sei t¯ die Zahl 1 − t (also 0 f¨ur t = 1 und umgekehrt). Wenn xt¯ auch Codewort ist, sind wir fertig. Wenn es nicht Codewort ist, dann ist das einzige Codewort mit Anfangsst¨uck x das Wort xt selbst. Wir k¨onnen also xt durch x ersetzen und behalten einen Pr¨afixcode, der allerdings k¨urzere mittlere L¨ange hat. Widerspruch. Jetzt k¨onnen wir die Optimalit¨at des Huffman-Code mit vollst¨andiger Induktion u¨ ber die Anzahl der zu kodierenden Symbole beweisen: • F¨ur ein Symbol ist der Code optimal.
8 INFORMATION
174 • Gelte die Aussage f¨ur Sourcen mit r − 1 Symbolen.
• Sei S 0 die aus S erhaltene Source, indem man zwei Symbole si und sj mit kleinster Wahrscheinlichkeit vereinigt. Sei C Huffmancode f¨ur S und C 0 Huffmancode f¨ur S 0 . Wir finden L(C, S) − L(C 0 , S 0 ) = p mit p = pi + pj . Sei D ein optimaler Code f¨ur r Symbole, und x0, x1 l¨angste Codew¨orter, die nach unserem Hilfslemma existieren m¨ussen. Wir zeigen, daß man diese Worte als Codew¨orter von si und sj w¨ahlen kann. Zun¨achst zu si , das OE kleinste Wahrscheinlichkeit pi hat. Falls das Codewort von si nicht x0 oder x1 ist, sondern etwa x0 Codewort von sk , dann tauschen wir die Codew¨orter von sk und si . Wir ersetzen damit in der L¨ange von D den Ausdruck pk |D(sk )|+pi |D(si )| durch pk |D(si )|+pi |D(sk )|. Es gilt pk |D(sk )|+pi |D(si )|−pk |D(si )|− pi |D(sk )| = (pk − pi )(|D(sk )| − |D(si )|). Das ist nicht negativ, da pi kleinste Wahrscheinlichkeit und D(sk ) l¨angstes Codewort ist. Es ist also auch der neue Code optimal und der Ausdruck notwendigerweise null. Genauso sehen wir, daß wir x1 als Code von s j w¨ahlen k¨onnen. Aus D l¨aßt sich ein Code D 0 f¨ur S 0 gewinnen, indem wir das Codewort des neuen Symbols als x nehmen. Wie wir gesehen haben, ist L(D, S) − L(D 0 , S 0 ) = p. Also L(D, S) − L(D 0 , S 0 ) = L(C, S) − L(C 0 , S 0 ). C 0 ist Huffmancode f¨ur S 0 und nach Induktionsvoraussetzung optimal. Daher ist L(C 0 , S 0 ) ≤ L(D 0 , S 0 ) und also L(C, S) ≤ L(D, S). Damit ist also auch C optimal, da es D ist. Das zeigt die Behauptung. Wir haben also einen optimalen Code konstruiert, wissen aber noch nichts u¨ ber seine tats¨achliche L¨ange in Abh¨angigkeit von S. Die L¨ange ist optimal, aber wie groß kann sie konkret werden, d.h. wieviel Zeit und Bandbreite muß man konkret f¨ur ein gegebenes S zur Verf¨ugung stellen? Hier hilft uns der Begriff der Entropie. Jedes Symbol, das von S emittiert wird, beinhaltet im Mittel H(S) Informationseinheiten. Falls das kodiert werden soll, ohne Information zu verlieren, ben¨otigt man im Mittel durch diese Zahl gegebene Codel¨ange geteilt durch den Informationsgehalt der Codes. Jedes Einzelsymbol des Codes bei r (gleichverteilten) Symbolen hat den Informationsgehalt − log2 1/r = log2 r. F¨ur Sources, die sehr informativ sind, ben¨otigt man also l¨angere Codeworte. Formal ist dieses in folgendem Theorem festgehalten: Theorem 8.10 Sei C instantaner Code u¨ ber r Symbolen zur Source S. Dann gilt L(C, S) ≥ H(S)/ log2 r Um das zu beweisen, zun¨achst eine Feststellung: f¨ur x > 0 ist ln x ≤ x − 1, Gleichheit gilt genau f¨ur x = 1. Das beweisen wir nicht, sondern schauen uns einfach die Graphen der beiden Funktionen an:
x−1
ln x
8.2 Kodierung
175
Damit erhalten wir folgende Aussage: P P • Sei xi ≥ 0, yi > 0, xi = yi = 1. Dann ist X X − xi log2 xi ≤ − xi log2 yi
P P P Die Differenz der beiden Seiten ergibt x log (y /x ) = x ln(y /x )/ ln 2 ≤ xi (yi /xi − i i i i i i 2 P P 1)/ ln 2 = ( xi − yi )/ ln 2 = 0 mit Gleichheit nur f¨ur yi /xi = 1.
Seien die Wortl¨angen des Codes C die Zahlen l1 , . . . , lr . Wir finden P H(S) = − P pi log2 pi P −li −li ≤ − p log r / i 2 P P −li r li = P pi log2 (r · r P ) P = P pi li log2 r + log2 (P r −li ) · pi = P pi li log2 r + log2 ( r −li ) ≤ pi li log2 r = L(C, S) log2 r P −li wobei wir die Kraft’sche Ungleichung verwendet haben, also r ≤ 1. Damit gilt die UngleiP −li chung log2 ( r ) ≤ 0. Codel¨angen sind also immer nach unten durch die Entropie der Quelle beschr¨ankt. Optimale Codes haben genau diese L¨ange, falls oben an allen Stellen Gleichheit werden EiP erreicht P kann. −li −li −li nerseits das (erste Ungleichung und Hilfssatz) pi = r / r und log2 ( r ) = 0 P bedeutet also r −li = 1. Da heißt pi = r −li bzw. − logr (pi ) = li ist eine nat¨urliche Zahl. Ist umgekehrt − logr (pi ) eine P P nat¨urliche Zahl, und setzen wir (was dann geht) li = − logr (pi ), dann finden wir 1/r li = pi = 1, es ist also die Kraft’sche Ungleichung erf¨ullt und es gibt Codes mit dieser L¨ange. D.h. L(C, S) = H(S)/ log2 r gilt dann und nur dann, wenn − logr (pi ) eine nat¨urliche Zahl ist f¨ur alle i. Ansonsten ist ein optimaler Code l¨anger als die untere Grenze angibt. Je nachdem, wie weit die Ausdr¨ucke − log r (pi ) von einer nat¨urlichen Zahl abweichen, kann das erheblich l¨anger sein, als die untere Schranke durch die Entropie angibt. Man kann auch obere Schranken f¨ur die L¨ange optimaler Codes finden. Sogenannte Shannon-Fano-Codes bieten eine M¨oglichkeit dazu. Shannon-Fano Codes sind solche, die die Wortl¨ange l i als die n¨achstbeste Wahl nehmen: als kleinste nat¨urliche Zahl li ≥ − logr (pi ). Wir wissen, daß es solche Codes geben muß aufgrund der Kraft’schen Ungleichung. (Wie auch immer sie konkret aussehen; das haben wir hier nicht behandelt. Wir wissen lediglich, daß es sie gibt!) Diese Codes sind nicht unbedingt P optimal, aber − log r pi ≤ li ≤ − logr pi +1, also − pi logr pi ≤ P solch ein PCode C erf¨ullt Pdie Ungleichung P pi li ≤ − pi logr pi + p1 = − pi logr pi + 1. Wegen logr pi = log2 pi / log2 r finden wir also H(S)/ log2 r ≤ L(C, S) ≤ H(S)/ log2 r + 1 Das gibt uns eine obere Schranke f¨ur optimale Codes. Man kann diese Schranken noch weiter betrachten, um Aussagen im Mittel bzw. f¨ur immer l¨angere Wiederholungen der Nachrichten¨ubertragung zu erhalten. Der Unterschied von 1 verschwindet tats¨achlich, wenn man immer l¨angere zu kodierende Texte betrachtet. Sei S n die n-fache Iteration der Source S. Sei Cn ein optimaler Code von S n mit mittlerer L¨ange Ln . Dieser Code kodiert implizit auch S selbst und ben¨otigt daf¨ur die mittlere L¨ange L n /n. Dann gilt: lim Ln /n = H(S)/ log2 r
n→∞
Das ist Shannons erstes Theorem. (F¨ur dessen Beweis uns lediglich noch die Feststellung H(S 1 S2 ) = H(S1 ) + H(S2 ) fehlte.) Sehr lange Nachrichten lassen sich also optimal kodieren mit einer durch die Entropie der Quelle determinierten mittleren Wortl¨ange.
176
8 INFORMATION
¨ Das ist ein erster Schritt in die durch Shannon begr¨undete Informationstheorie, die die Ubertragung von Informationen auf rein formalem, axiomatischem Wege untersucht und zu interessanten Aussagen u¨ ber Machbarkeit, Aufwand, Kodierung, etc. sowie konkret einsetzbaren Verfahren kommt.
8.3 Kompression und Kryptographie Im Zuge der Kodierung sind weitere Formalisierungen f¨ur die Praxis von Belang: Bisher sind wir von einer zuverl¨assigen Daten¨ubertragung ausgegangen. In der Praxis ist das nicht der Fall, Daten k¨onnen durch technische St¨orungen zuf¨allig ge¨andert werden, so daß eine verrauschte Kodierung ankommt. Bewegen sich die Fehler in Maßen, kann man sie mit hoher Wahrscheinlichkeit durch geeignete Kodierungen und Checks korrigieren. Das ist der Bereich der Nachrichten u¨ bertragung unter Rauschen und fehlerkorrigierender Codes. Wichtig in diesem Zusammenhang sind eta CRCCodes, d.h. eine sehr leistungsf¨ahige und effiziente Fehlererkennung f¨ur bin¨are Daten. Die Idee ist, neben den Daten Pr¨ufbits zu verwenden, die anzeigen, ob ein oder mehrere Bits ver¨andert wurden (und die Nachricht evtl. nochmal versendet werden muß). Die Berechnung entsprechender Pr¨ufbits kann dabei sehr effizient in digitalen Schaltungen realisiert werden. Besonders relevant ist dieses Thema bei sehr großen Datenmengen, wo einerseits leicht Fehler auftreten und andererseits die Schnelligkeit der u¨ bertragung gew¨ahrt werden soll. H¨aufig werden Daten dabei zudem komprimiert u¨ bertragen – in gewissem Rahmen haben wir bereits das Gebiet der Kompression gestreift, einer Darstellung der Daten mit m¨oglichst kurzen W¨ortern. Dabei k¨onnen Zusatzbedingungen gestellt werden, wie etwa einer m¨oglichst einfachen Dekompressionsvorschrift, um dieses online (z.B. beim Laden von Webseiten u¨ ber das Internet) darstellen zu k¨onnen. H¨aufig ist man an einer verlustfreien Daten¨ubertragung interessiert. Es kann aber auch das Ziel sein, die Daten m¨oglichst kompakt darzustellen (mit einem optimalen Code), so daß alles Wesentliche rekonstruiert werden kann. Zul¨assig sind dann geringf¨ugige, f¨ur den Betrachter irrelevante Abweichungen, also eine verlustbehaftete Kompression. Kompression ist insbesondere bei großen Bilddaten wichtig. Es gibt viele prominente Formate: • GIF-Bilder sind verlustfrei komprimiert mit einem Algorithmus, der h¨aufig vorkommende Muster (insbesondere Farbmuster) durch k¨urzere Darstellungen ersetzt. • JPEG-Bilder sind verlustbehaftet abgespeichert, wobei ein menschlicher Wahrnehmer die Unterschiede nicht merkt. Die Kompression besteht aus einer Kosinustransformation, einer geeigneten auf die menschliche Wahrnehmung angepasste Quantisierung, und einer Kodierung. Wir sehen also, die theoretischen Informationsbegriffe haben sehr praktische Anwendungen und beinhalten Algorithmen, die zur generellen Informations¨ubertragung z.B. im Internet gebraucht werden – und programmiert werden m¨ussen. Wir haben bisher zugelassen, daß jeder die Nachricht empfangen und dekodieren kann. Es kann sein, daß es um geheime Dokumente geht, die verschl u¨ sselt werden sollen. Dann bewegt man sich in das Gebiet der Kryptographie, der Entwicklung von Codes, so daß nur autorisierte Benutzer sie verstehen k¨onnen. In diesem Bereichen gibt es sehr interessante und h¨aufig nicht triviale Theorie. In diesem Zusammenhang tauchen auch insbesondere in Multimedia und Internet einige Schlagworte auf: • Data Encryption Standard: Ein 1977 von IBM durch die US-Regierung u¨ bernommenes Verschl¨usselungsprogramm, das heute aber nicht mehr als sicher gilt. Es verschl¨usselt je 64Bit-Bl¨ocke mit einem 56-Bit-Schl¨ussel in 19 Stufen, d.h. es verwendet grunds¨atlich eine einfache Ersetzung von St¨ucken des Originals in gleich lange Codew¨orter.
177 • Kerberos, der mehrk¨opfige H¨ollenhund des Hades, lieh seinen Namen einem am MIT entwickelten Authentifizierungsverfahren, das in Rechnernetzwerken oft eingesetzt wird. • Digitale Signaturen ersetzen Unterschriften etwa in der Finanzwelt. Sie basieren auf geeigneten Kombinationen von private- oder public-key Verfahren um sicherzustellen, daß der Empf¨anger den Sender sicher identifizieren kann, der Sender nicht die Nachricht r¨uckg¨angig machen kann und der Empf¨anger nicht die Nachricht a¨ ndern kann. • Secure socket layer (SSL) ist ein von Netscape entwickelter Protokollstandard zur Netzkommunikation, der Verschl¨usselung und Authentifizierung beinhaltet und so u¨ ber das Web bereitgestellte Informationen und Informations¨ubertragung sicherer machen soll. • Public key RSA Kryptoverfahren sind heute ein weit verbreiteter Standard, da man den Schl¨ussel, d.h. die Codew¨orter nicht auszutauschen braucht. RSA ist eines der gebr¨auchlich¨ sten Verfahren, basierend auf relativ einfachen zahlentheoretischen Uberlegungen. Prinzipiell ist es so, daß ein Empf¨anger einen o¨ ffentlichen Verschl¨usselungsschl¨ussel bereitstellt und den privaten und nur ihm bekannten Entschl¨usselungsschl¨ussel f¨ur sich beh¨alt. Ein Knackpunkt, dessen Erkenntnis wesentlich zur Entwicklung dieser Verfahren beigetragen hat, ist offensichtlich, daß man Verschl¨usselung und Entschl¨usselung v¨ollig unabh¨angig voneinander berechnen kann. Dieses macht sich besondere Eigenschaften der nat¨urlichen Zahlen und der (bisher noch) nur ineffizient berechenbaren Primfaktorzerlegung von großen Zahlen zunutze. Eine der essentiellen Stellen ist dabei, daß mit einem Rechner sehr schnell große zuf¨allige Primzahlen erzeugt werden k¨onnen (wie wir gesehen haben), aber Primfaktoren aus Produkten nur sehr ineffizient zur¨uckgewonnen werden k¨onnen (bzw. hier kein effizienter Algorithmus mit standard-Rechnern bekannt ist. Einer der Gr¨unde, warum die Forschung an sogenannten Quantencomputern sehr interessant ist, besteht in der Tatsache, daß f¨ur solche Rechenmodelle efiziente Primfaktorzerlegungen bekannt sind – nur die entsprechenden Quantencomputer sind technisch noch nicht realisierbar).
9 Informationsverarbeitung im Rechner Wie wird die Information im Rechner prinzipiell verarbeitet und vorgehalten? Logisch teilt man die auf einem Rechner angesiedelten Programme und informationsverarbeitenden Einheiten in mehrere Ebenen auf: • Die Hardwareebene bestehend aus – Physikalischen integrierten Schaltungen, Kabeln, Stromversorgung, Speicher, . . . – der Mikroarchitekturebene, in der die physikalischen Einheiten zu kleinen logischen Einheiten, wie etwa der CPU oder der arithmetisch logischen Einheit zusammengefasst sind, – der Maschinensprache, die mit typischerweise 50 bis 300 Basisinstruktionen die physikalischen Einheiten steuert. • Die Ebene der Systemprogramme, bestehend aus – Betriebssystem – Compiler, Editoren, Kommandozeileninterpreter • Die Ebene der Anwendungsprogramme wie Web-Browser, Datenbanksystem, spezielle Firmensoftware, . . .
178
9 INFORMATIONSVERARBEITUNG IM RECHNER
Wir haben uns bereits mit speziellen Sprachen (Java und Prolog) und darin gechriebenen Anwendungsprogrammen (etwa Sortieren von Zahlen) besch¨aftigt, und auch im Rahmen Boolescher Funktionen einen (sehr kurzen) Blick auf Schaltungen, also den Hardware-Bereich geworfen. Das Betriebssystem ist eine Schnittstelle zwischen Hardware und Anwendungssoftware und regelt die Verteilung von Ressourcen wie etwa Speicherplatz, Rechenzeit, etc. und die Kommunikation und Synchronisation der Prozesse. Je nach Gegebenheiten unterscheidet man zwischen verschiedenen Modi: • Mainframe-Systeme unterscheiden sich von u¨ blichen B¨urocomputern durch die wesentlich gr¨oßere LEistung und hohe I/O-Kapazit¨at, entsprechend sind die Betriebssysteme daraufhin optimiert, sehr viele Ein-/Ausgabe-intensive Prozesse gleichzeitig bedienen zu k¨onnen. Relevant sind Mainframes etwa als Großrechner in großen kommerziellen Betrieben zur Verwaltung zentraler Datenbanken oder Web-Seiten. • Server, h¨aufig leistungssf¨ahige PCs, workstations oder gr¨oßere Maschinen, stellen NetzDienste bereit, etwa die Verwaltung von Druck-Jobs, Dateien, Webdienste, etc. Relevant ist hier ein robuster und sicherer Betrieb f¨ur viele Anfragen. • Multiprozessor-Systeme durch Integration mehrerer CPUs oder physikalischer Rechner nehmen f¨ur umfangreiche Rechnungen einen immer h¨oheren Stellenwert ein. Wichtig sind in diesem Kontext gute Scheduling-Algorithmen, um Jobs auf den Prozessoren zu verteilen. • Personal Computer haben die Aufgabe, f¨ur einzelne Benutzer komfortable und gleichzitig kosteng¨unstige Kompaktl¨osungen bereitzustellen. • Real-time Systeme etwa in industriellen Prozessen haben strikte Anforderungen an die Einhaltung von zeitlichen Bedingungen. • Eingebettete Systeme wie etwa ein palmtop Computer stellen reduzierte Funktionalit¨aten auf kleinem Raum bereit. • SmartCards treiben dieses ins Extreme, mit oft sehr spezifizierten Funktionen auf kleinstem Raum und g¨unstiger Fertigung. Die Entwicklung von Betriebssystemen f¨ur heutige Rechner wird h¨aufig in vier Phasen eingeteilt: in der ersten Generation (1945-55), Computer bestanden noch aus Elektronenr¨ohren, wurden Programme manuell via Steckleisten eingegeben. In der zweiten Generation u¨ bernehmen Transistoren die Arbeit, jetzt m¨ogliche Batch-Jobs sind auf Magnetb¨andern gespeichert. Die dritte Phase (19651980) setzt auf integrierte Schaltkreise, so daß mehrere Teile ausgelagert werden k¨onnen, Spooling = Simultaneous Peripheral Operation On Line. Zweitens kann man mehrere Programme/Benutzer gleichzeitig bedienen, Multiprogramming und Time-sharing werden relevant. Die vierte Generation ist durch VLSI-Chips (very large scale integration) bestimmt. Es entstehen die heute bekannten PCs und Workstations mit komfortabler Oberfl¨ache und Multitasking, desweiteren Netzwerksysteme und verteilte Systeme. Was u¨ berwachen Betriebssysteme? Auf der Hardware-Seite stehen folgende Stichpunkte: • Fetch-Execution-Zyklus des Prozessors: im Inneren eines Rechners werden im Prinzip lediglich iterativ Basisanweisungen vom Anweisungsstack geholt und ausgef¨uhrt. Die Anweisungen sind dabei maschinenspezifisch, und enthalten typische Anweisungen wie Register lesen/schreiben, Speicher adressieren, etc. Jede Anweisung enth¨alt alle ben¨otigte Information.
9.1 Prozesse
179
• Auf dieser Ebene steuern Interrupts das Zusammenspiel insbesondere bezogen auf Eingaben und (durch Maskierung, d.h. Deaktivierung bestimmter Interrupts) bezogen auf monolithisch zu verarbeitende kritische Bl¨ocke. • Die Ein-/Ausgabe ist u¨ ber verschiedene Buse realisiert, je nach Art des anh¨angenden Ger¨ates. Es gibt hier verschiedene K¨urzel: ISA, PCI, USB, SCSI, IEEE1394, . . . . Besonders interessant ist die Kommunaktion bei sich a¨ ndernder Konfiguration (etwa eingestecktem USB Stick). Hier ist das von Microsoft entwickelte plug and play-Konzept n¨utzlich: es werden w¨ahrend des Betriebs Informationen u¨ ber die sich evtl. ver¨andernden Zusta¨ande gesammelt und vom System-BIOS (Basic Input Ouput Systems) direkt per low-level Software verwaltet. • Informationen werden einerseits in unterschiedlichen Registern gespeichert, andererseits muß man auf weniger schnelle (aber g¨unstige und damit gr¨oßere) Medien zugreifen: mehrere Caches speichern h¨aufig gebrauchte Information, der Hauptspeicher l¨adt einen großen Teil der Daten eines Programms zur weiteren Verarbeitung, die Festplatte dient f¨ur die u¨ bliche Information u¨ ber alle m¨oglichen Daten und persistente Speicherung, und dann kann man noch auf verschiedenste externe Speicher ausweichen. Das Betriebssystem hat die Aufgabe, Informationen auf h¨oherer Ebene, etwa vom Benutzer gegeben Eingaben, auf diese Ebene herunterzubrechen. Konzeptuell kann man es dazu in drei Bereiche einteilen: Kommandozeileninterpreter und/oder graphische Benutzeroberfl¨ache, low-level Funktionalit¨aten, und den Kern, das Herz eines Betriebssystems.
9.1 Prozesse Ein paar Grundbegriffe sind f¨ur jedes Betriebssystem relevant. Die Aufgabe eines Betriebssystems ist die, w¨ahrend eines Programmablaufs logischen Einheiten gegeben durch Prozesse zu verwalten. Ein Prozeß ist ein Programm in der Ausf¨uhrung. Prozesse leben in einem gewissen Adressraum, d.h. Platz des Speichers, den der Prozeß beschreiben und l¨oschen darf. Prozesse werden charakterisiert durch einen Prozesskontrollblock, der folgendes beinhaltet: • die Nummer des Prozeß, • den Zustand des Prozeß (running, sleeping, . . . ), • der gespeicherte Zustand der Prozeßregister, • Scheduling Information wie etwa die Priorit¨at, • Verweise auf Datei- oder Netzwerkverbindungen des Prozeß, • Besitzerinformation (Gruppen und User id des Benutzers), die verwandt werden, um Zugriffsrechte zu u¨ berwachen, • Zeiger zu anderen Prozessen, etwa Kindern oder Eltern. Ein Prozeß kann einen anderen Prozeß abspalten (in Unix durch fork), dieser Kindprozeß erbt dann die Rechte des Vaterprozeß. Viele Betriebssysteme speichern alle aktuellen Prozeße in einer Prozeßtabelle zusammen mit ihrem aktuellen Zustand, der ben¨otigten Zeit, etc. Diese Tabelle wird genutzt, Prozesse geordnet zu schedulen: die CPU-Zeit wird nach berits vergebener Rechenzeit und Priorit¨at des Prozeß gescheduled. Wird ein Prozeß unterbrochen, muß sein aktueller Zustand (etwa Dateiverbindungen und Register) gespeichert und der Prozeß sp¨ater an der gleichen Stelle fortgesetzt werden. Dabei
180
9 INFORMATIONSVERARBEITUNG IM RECHNER
k¨onnen, wie wir es schon aus Threads kennen, Racing consitions, starvation oder deadlocks auftreten, die vom Betriebssystem m¨oglichst verhindert werden sollten (aber es nicht immer werden, der Befehl kill unter Unix leistet in einem solchen Fall gute Dienste). Threads unterscheiden sich von Prozessen dahingehend, daß sie keinen eigenen Speicher besitzen, sondern als Teilst¨ucke eines Prozeß parallel laufen. Trotzdem gelten die bereits zu Threads genannten Aspekte des Scheduling (etwa round-robin Scheduling oder Scheduling mit Priorit¨aten) f¨ur Prozesse analog.
9.2 Speicher Eng mit Prozessen verkn¨upft ist die Frage des Memory-Management. Ein Prozeß greift auf Speicher zu, wobei die Speichermedien unterschiedlich schnell sind, angefangen von Registern u¨ ber Cache zur Festplatte. Zun¨achst ist bei Betriebssystemen ein Bereich des Hauptspeichers immer f¨ur den Kern des Betriebssystems sowie Ger¨atetreiber und weitere Dinge reserviert und kann weder von einzelnen Benutzerprozessen beschrieben noch ausgelagert werden, der sogenannte kernel space. F¨ur Prozesse wird der user space verwandt, wo entsprechend der Systemauslastung zugegriffen, beschleunigt, oder auch ausgelagert werden kann. In vielen Betriebssystemen wie Unix erh¨alt jeder Prozeß seinen eigenen getrennten Bereich des user space, den er verwaltet. Da in Benutzerprozessen der Speicherbedarf nicht vorausgesagt werden kann und in vielen Anwendungen gr¨oßer als der verf¨ugbare Hauptspeicher wird, wird heute meist die Technik des virtuellen Speichers verwandt. Das bedeutet, daß Speicherzellen nur indirekt referenziert werden und nach Bedarf in schnellen Medien gelagert oder auch auf die Festplatte ausgelagert werden k¨onnen. Da der Ort dieses Speichers wechseln kann, greift der Kern eines Betriebssystems nie direkt darauf zu. Die von Programmen erzeugten Speicheradressen werden bei variablem Memory nicht direkt an den Memory Bus geschickt, sondern an die MMU (Memory Management Unit). Diese rechnet virtuellen in tats¨achlichen Speicher um. Eine gebr¨auchliche Technik ist die Fragmentierung: es wird je sukzessive st¨uckweise auf noch freie Bl¨ocke geschrieben und der jeweils anschließende logisch dazugeh¨orende Speicherplatz gespeichert. Diese Technik nutzt den gesamten Platz aus, hat aber den Nachteil, daß sie zu langen Zugriffszeiten f¨uhren kann und Systeme durch eine u¨ berm¨aßige Fragmentierung (das heißt nur sehr gest¨uckelte Nutzung des Speichers) in die Knie zwingt – eine explizite Defragmentierung ist dann n¨otig. Eine der gebr¨auchlichsten neueren Techniken, virtuellen Speicher effizient zu verwalten, ist das paging. Beim Paging ist die Grundidee, physikalischen Speicher und logischen Speicher in gleich großen Bl¨ocken zu organisieren, wobei man virtuelle Pl¨atze sehr schnell in physikalische umrechnen kann. Dieses verhindert eine Fragmentierung des Speichers und erlaubt zudem, ganze Seiten auf einmal aus- und einzulagern. Die Seiten werden in einer Paging-Tabelle gespeichert, die den Index der benutzten Seiten und ihre physikalische Adresse speichert. Logische Adressen und physikalische Adressen werden dann durch Tupel charakterisiert, gegeben durch den Index f¨ur Pages bzw. den physikalischen Startpunkt f¨ur tats¨achliche Seiten sowie den (identischen) Offset des Eintrags zur tats¨achlichen Speicherstelle. Dabei muß darauf geachtet werden, daß der Zugriff auf die Paging-Tabelle sehr schnell ist. Paging kann mehrfach hintereinander angewendet werden, um gr¨oßere Bl¨ocke effizient auszulagern oder kleinere Einheiten zu erreichen (die Sparc 32-bit Architektur etwa erlaubt dreifaches Paging), was aber sehr langsam werden kann. Paging Strategien bieten sich besonders an, Speicher auszulagern, da man ganze Bereiche auf einmal umschreiben kann. Die Entgscheidung, welche Information wo am effizientesten gespeichert werden soll, verlangt passende Strategien, die einerseits schnell und robust implementiert werden k¨onnen, andererseits die h¨aufig gebrauchten Speicherzellen m¨oglichst in schnellen Spei¨ chermedien belassen. Ubliche Strategien sind etwa • First in first out, mit dem Hintergrung, daß die aktuellsten Ergebnisse hoffentlich die zur Zeit
9.3 Filesystem
181
am meisten gebrauchten sind; • Ersetzen der am wenigsten gebrauchten Page; • Ersetze die am l¨angsten nicht mehr gebrauchte Page; • Ersetze die Page, die in Zukunft als sp¨atestes gebraucht wird (was man dann aber irgendwie absch¨atzen muß, etwa durch die Laufzeiten der aktuellen Prozesse und deren bisherige Zugriffe.)
9.3 Filesystem Dateien sind persistente Container f¨ur Daten, die meist beliebige Informationen enthalten k¨onnen. H¨aufig wird durch das Dateiende das Format des Inhalts nahegelegt und eine Verkn¨upfung mit einem entsprechenden Anzeigeprogramm realisiert (.gz f¨ur gezippte Dateien, .tar f¨ur Archive, .ps f¨ur Postscript, .pdf f¨ur Portable Document Formate, . . . ). Gelegentlich werden einige (in Unix alle) weiteren Objekte als Dateien behandelt: Peripherie (/dev/mouse), Prozesse (/proc), Internetverbindungen (/net/tcp), . . . . Dateien sind logisch innerhalb hierarchischer Namensr¨aume angesiedelt, als Bl¨atter in einer Baumstruktur von Verzeichnissen und Dateien. Ihre absoluten Pfade (also die Hintereinanderh¨angung aller Verzeichnis- und Dateinamen angefangen von der Wurzel) m¨ussen dabei eindeutig sein. Eine strikte Baumstruktur kann durch symbolische Links (also Verkn¨upfungen zu anderen Dateien oder Verzeichnissen) durchbrochen werden. Verzeichnisse k¨onnen entweder als spezielle Objekte oder als normale Dateien mit speziellem Inhalt realisiert werden. In Linux und Unix k¨onnen ganze Verzeichnisse partiell in den Dateibaum eingebunden (gemounted) werden, etwa beim Lesen einer CD oder eines Memory-Stick. Dateien k¨onnen durch ihre absoluten Namen (mit vollem Pfad) oder die relativen Namen bezogen auf das aktuelle Verzeichnis identifiziert werden. Da Dateien groß werden k¨onnen, sind sie nicht unbedingt physikalisch zusammenh¨angend gespeichert, sondern in mehreren St¨ucken mit Verweisen auf die je nachfolgenden St¨ucke; der Benutzer sieht diese physikalische Zerst¨uckelung nicht. Dateien werden durch spezielle Informationen charakterisiert, die zu den Dateien abgespeichert sind und den Namen, Zugriff, etc. regeln. Diese Information unterscheidet sich je nach Betriebsssysten. F¨ur Linux und Unix wird im sogenannten i-node (der Ursprung dieses Namens ist dabei ein R¨atsel auch f¨ur die Unix-Pioniere) vorgehalten • die L¨ange in Bytes, • die Identifikation des Mediums, auf dem die Datei existiert, • die Identifikation des Besitzers, • die Gruppe des Besitzers, • eine eindeutige Kennziffer (inode number) der Datei, • der Zugriffsmodus, darstellbar als sieben-Tupel mit x-Bit (ausf¨uhrbar), Lese-/Schreib- und Ausf¨uhrungsrechte f¨ur Besitzer, Gruppe, und Welt (etwa -rw-r–r–), ¨ • Zeitstempel der Erzeugung, der letzten Anderung, und des letzten Zugriff, • die Anzahl der auf die Datei verweisenden anderen Dateien oder Verzeichnisse.
182
9 INFORMATIONSVERARBEITUNG IM RECHNER
In anderen Systemen gibt es andere (mitunder sehr viel unkomfortablere) Vereinbarungen. Es kann auftreten, daß Dateien oder wichtige Informationen verloren gehen, etwa bei einem Systemcrash oder unvorsichtigem Abschalten des Rechners. Daf¨ur sind in den Betriebssystemen mehrere Sicherheiten eingebaut. Etwa in Linux werden inzwischen sogenannte jorunaling files¨ systems verwandt. Das bedeutet, daß Anderungen (etwa L¨oschoperationen) in einem separaten Block dokumentiert werden, bevor sie tats¨achlich vorgenommen werden. Passiert etwa innerhalb ¨ der Anderungen ein Crash, dann kann das System anhand der Protokolle den urspr¨unglichen Zustand wieder herstellen. Betriebssysteme beschr¨anken sich dabei h¨aufig auf eine Dokumentation der Metadaten und nicht des tats¨achlichen Inhalts, da dieses leicht zu kostspielig wird. Metadaten beinhalten etwa die Adressr¨aume, Erweiterungen, etc. die dann immerhin noch ein bedingtes Reparieren aufgetretener Fehler gestatten. Eine zweite Kontrolle, die bei un¨uberlegtem Abschalten oder Crashs automatisch beim n¨achsten Bootvorgang aufgerufen wird, ist ein Check der inode-Zahlen auf Konsistenz, um etwa fehlende Dateien (nicht benutzte, aber nicht als frei deklarierte Bl¨ocke) zu entdecken.
9.4 IO Die Ein- und Ausgabe wird je nach Ger¨at und Betriebssystem unterschiedlich behandelt. Charakteristisch f¨ur Windows ist etwa die einfache (aber manchmal fehlschlagende – ’plug and pray’) plug-and-play-Realisierung. In Unix k¨onnen Ein-/Ausgaben universell als Datenstr¨ome mithilfe von Pipes behandelt werden. Einige Stichpunkte sollten in diesem Rahmen erw¨ahnt werden: Ein-/Ausgabe sind h¨aufig langwierigere Prozesse. Um nicht f¨ur jedes Zeichen den Benutzer (bzw. Benutzerprozess) zu bem¨uhen, werden Eingaben in St¨ucken in einem Puffer zwischengespeichert, bevor sie angezeigt oder in eine Datei geschrieben werden. Verwandt wird dabei meist ein double buffeering, d.h. zwei Bl¨ocke sind im Hauptspeicher reserviert, von denen je ein aktueller von der I/O beschrieben und der andere weiterverarbeitet (abgespeichert oder ausgegeben) wird. Auch bei der Ein- und Ausgabe k¨onnen Daten falsch geschrieben werden, oder es kann unpassend unterbrochen werden. Sogenanntes stable storage liest und schreibt Dateien mit anschließendem Pr¨ufvergleich, so daß Fehler auffallen und die Aktion evtl. wiederholt werden kann. Die Performanz von I/O hat sich technisch gesehen nicht in demselben Rahmen entwickelt wie etwa CPU-Zeit. Sichere und g¨unstige, schnell beschreibbare Medien sind daher heute h¨aufig durch einen Trick realisiert: RAIDs (Redundant Array of Inexpensive Disks), d.h. eine Ansammlung von g¨unstigen und schnell zugreifbaren einfachen Medien, in diesem Fall schnellen kleineren Platten. Diese k¨onnen entweder direkt ausschließlich in Hardware oder u¨ ber entsprechende Software und mehrere Rechner realisiert werden. Es gibt verschiedene Ans¨atze, Daten m¨oglichst effizient verteilt abzulegen und dabei auch gegebenenfalls m¨ogliche Ausf¨alle von einzelnen Platten zu kompensieren. Einfaches Striping zerlegt die Daten in Bl¨ocke, die auf mehreren sukzessiven Platten gespeichert werden. Damit kann parallel geschrieben und gelesen werden, allerdings ist keinerlei Redundanz eingebaut, also keine Reparatur bei Ausfall von Platten m¨oglich. Redundanz erh¨alt man, indem man diesen Ansatz dahingehend erweitert, jede Information zweimal abzuspeichern, allerdings wird der Plattenplatz damit effektiv halbiert. Einen Kompromiß bildet, zu den Daten Pr¨ufbits abzuspeichern. In der Regel verwendet man zus¨atzlich zur gespeicherten verteilten Information u¨ ber die Platten hinweg berechnete Parity-Bits, die eine Korrektur gestatten, solange nur eine Platte gleichzeitig ausf¨allt. Dabei sollten die Parity Bits nicht auf einer Platte gelagert sein!
9.5 Kern
183
9.5 Kern Der Kern eines Betriebssystems organisert den sicheren Zugriff von Computerprogrammen auf die Maschinenhardware, und zwar sowohl bezogen auf die Zeitaufteilung als auch bezogen auf den prinzipiellen Zugriff. Vieole Aspekte haben wir bereits diskutiert. Beim prinzipiellen Design, d.h. der Implementation von Kernen wird zwischen verschiedenen Sichtweisen unterschieden. Klassisch sind monolithische Kerne, startend mit Unix, welches ein (das erste) effizientes funktionsf¨ahiges und elegantes Betriebssystem offerierte. Die Grundidee von Unix war, alle Operationen einfach als Dateioperationen zu realisieren und dem Benutzer durch Dateimanipulationen mit dem System kommunizieren zu lassen. Alle Operationen speichern und/oder liefern Werte. Das Umlenken dieser Str¨ome und Verketten erlaubt komplexe Funktionalit¨aten. Umlenken der Einund Ausgabe geschieht u¨ ber Pipes, die die Daten zwischenpuffern und weitergeben. Mithilfe diser prinzipiellen Ansicht k¨onnen Benutzer mit nur sehr wenigen Befehlen (Befehle als Ein-/Ausgabe und Pipes) das gesamte Betriebssystem beherrschen. Mit der Entwicklung neuer Medien und insbesondere graphischer Benutzeroberfl¨achen br¨ockelte dieses Konzept; monolithische Kerne sprengten die urspr¨unglichen etwa 100000 Zeilen Code von Unix zu u¨ ber 33 Millionen f¨ur Linux. An der Carnegie Mellon University wurde mit dem Mach-Kern der Prototype f¨ur ein neues schlankeres Kerneldesign gelegt und die T¨ur zu sogenannten Mikrokernen aufgestoßen. Die Grundidee f¨uhrt dabei die Unix-Realisierung von Pipes weiter: Systeme bestehen aus kleinen Einheiten, die durch Pipes miteinander kommunizieren, durch die jetzt aber jede Art von Information (nicht nur Dateiformate) geschickt werden kann. So entstanden die Bausteine, um die sich derartige Betriebssysteme gruppieren: • ein Task besteht aus Resourcen, welche erm¨oglichen, daß Threads laufen, • ein Thread ist eine Einheit von Code, die auf einem einzelnen Prozessor l¨auft, • ein Port definiert die Schnittstelle zwischen Prozessen mittels einer sicheren Pipe, • Messages beliebieger Art k¨onnen u¨ ber diese Pipes geschickt werden. Der Mach Kern war insbesondere der einzige, der erlaubte eine einzelne Task durch mehrere Threads zu realisieren, heute der Standard. Das verschob die Sicht von einem monolithisch agierenen Betriebssystem zu einer verteilten Implementation mithilfe von einzelnen Servern f¨ur spezielle Aufgaben, die mithilfe von Ports komunizieren. Mit der tats¨achlichen Nutzung des Mach-Kerns tauchten allerdings fundamentale Effizienzprobleme auf: Zugriff auf Speicher und das Umschreiben von Information zwischen Prozessen ist extrem teuer, aber extensiv genutzt insbesondere bei Prozeßkommunikation und dem Systemaufruf von Prozessen durch andere u¨ ber Server hinweg. Paging-Mechanismen, die in einem monolithischen System noch an die konkreten Bed¨urfnisse angepasst werden konnten, m¨ussen in verteilten Systemen allgemein und entsprechend ineffizient gehalten werden. Das von Unix u¨ bernommene Konzept beruht aber extensiv auf der Interprozesskommunikation und dem Umschalten zwischen verschiedenen Servern (und Adressr¨aumen). Mikrokernel wurden erst wieder popul¨ar mit der Verkn¨upfung der Idee einzelner Server und aus der Windows-Welt stammenden Konzepten: gibt es nur einen großen Addressraum, m¨ussen die Daten nicht zwischen verschiedenen R¨aumen umgeshiftet werden, und ein Großteil des Overheads des Mach-Kerns f¨allt weg. Sicherheitsaspekte k¨onnen in die einzelnen Anwendungsprogramme verlagert werden, statt in jedem Port integriert sein. Mit diesen Verschlankungen ist das Mikrokernel-Konzept insbesondere f¨ur verteilte low-level Anwendungen attraktiv.
184
9 INFORMATIONSVERARBEITUNG IM RECHNER
9.6 Good bye! Wir m¨ochten zum Schluß in Java ein kleines Fenster programmieren, das den Text ’Good bye!’ ausgibt. Das Ganze soll als applet in einem Browser laufen. Ein Applet ist nichts anderes als ein Java-Programm, das von der Klasse Applet abgeleitet ist und einige Konventionen ber¨ucksichtigt. Die Klasse kann dann direkt in html-Code eingef¨ugt werden und auf anderen Rechnern u¨ ber das Internet gestartet werden. Dazu m¨ussen einige Konventionen befolgt werden: der fremde Rechner muß dieses Programm ausf¨uhren k¨onnen. Einerseits kann es dabei Probleme geben, weil nicht dieselbe Version von Java auf dem fremden Rechner implementiert ist, andererseits hat das Programm in der Regel sehr beschr¨ankte Rechte, kann also keine Dateien manipulieren, Netzverbindungen aufbauen oder a¨ hnliches. Unser erstes Applet hat einfach folgende Form: import java.applet.Applet; import java.awt.Graphics; public class Goodbye extends Applet { public void paint(Graphics g) { g.drawString("Good bye!", 50, 25); } } Dieses Applet benutzt zwei Pakete, die Java liefert, applet, um Applets auszuf¨uhren, und die Graphik des abstract windowing toolkit (awt), um ein Fenster zu zeichnen. Das obige Programm implementiert die Methode paint. Jedes Applet muß eine der Methoden init, start oder paint implementieren, damit etwas passiert. Applets brauchen nicht die Methode main zu implementieren, die f¨ur Applets nicht aufgerufen wird. Obige Methode u¨ bernimmt ein Graphikobjekt (genauer den Kontext des aktuellen Applets) und zeichnet mit dessen Hilfe ein Fenster mit dem String "Good bye!", wobei die Position der linken unteren Ecke angegeben wurde. Benutzt wird dieses Applet durch Einbinden in ein html-Dokument mithilfe des Tag . Good bye! Good-bye applet
Es muß der Klassenname angegeben werden, und es wird erwartet, daß der Code der Klasse im selben Verzeichnis wie das html-Dokument steht, sofern das nicht durch codebase=<wo> spezifiziert ist. Zudem muß die Gr¨oße des Applets (in Punkten) angegeben werden. Nicht Java-f¨ahige Browser stellen den unter alt angegebenen Text dar. Man kann Parameter an das Applet geben, induziert durch das Tag param mit angegebenem Namen und Wert (als String). Die Klasse muß wie u¨ blich mit javac u¨ bersetzt werden. Ein Aufruf dieser Seite f¨uhrt dann zur Ausf¨uhrung des Programms. Java offeriert auch die M¨oglichkeit, sich die Funktion von Applets im Appletviewer anzusehen. Das geschieht durch den Aufruf java sun.applet.AppletViewer gefolgt vom Namen der html-Seite.
9.6 Good bye!
185
Das obige Applet ist sehr simpel, es stellt lediglich einen Text dar (impliziter Aufruf der ¨ paint Methode), sonst passiert nichts. Ublicherweise berechnen Applets w¨ahrend ihres gesamten Lebenszyklus etwas, daher werden meist komplexere Methoden implementiert. Der u¨ bliche Lebenszyklus von Applets wird durch die im Original noch leeren Methoden init (Initialisierung), start (Start beim neuen oder ersten Betrachten der Webseite), stop (beim Iconifizieren oder Verlassen der Webseite) und destroy (beim L¨oschen des Applet) bestimmt. In obigem Applet wird allerdings keine einzige dieser Methoden u¨ berschrieben, sondern lediglich die Graphikmethode paint ver¨andert und der Text auf die Seite geschrieben. Den Lebenszyklus eines Applets kann man gut in folgendem Beispiel sehen: import java.applet.Applet; import java.awt.Graphics; public class Lifecycle extends Applet { StringBuffer buffer; public void init() { buffer = new StringBuffer(); addItem("initializing... "); } public void start() { addItem("starting... "); } public void stop() { addItem("stopping... "); } public void destroy() { addItem("preparing for unloading..."); } void addItem(String newWord) { System.out.println(newWord); buffer.append(newWord); repaint(); } public void paint(Graphics g) { g.drawString(buffer.toString(), 5, 15); } } In einem gepufferten String werden hier pro Aufruf Messages hinzugef¨ugt und ausgegeben. Man sieht sehr sch¨on, wie sich bei jedem Iconifizieren und Deiconifizieren des Fensters der Text verl¨angert, da stop und start aufgerufen werden. Ein explizites stop und destroy ist insbesondere dann n¨otig, wenn man innerhalb eines Applets mehrere Threads startet – diese werden nicht automatisch gestoppt und verschwenden daher Speicherplatz oder Rechenzeit, auch wenn die Seite des Applets u¨ berhaupt nicht betrachtet wird. Applets sind sehr m¨achtig, man kann etwa: • pro Webseite mehrere Applets erzeugen und diese gegenseitig ansprechen, • von Webseiten Parameter einlesen, • mit einem Applet eine neue Webseite aufrufen, • auf Aktionen des Benutzers wie Eingaben oder Mousebewegungen reagieren,
10 LITERATUR
186 • Multimediadateien abspielen • u¨ ber awt oder swing Oberfl¨achen einbinden, • etc.
Das soll hier nicht mehr Thema sein. Wir wollen lediglich zum Schluß das obige Applet Goodbye so ver¨andern, daß wir einen Knopf dr¨ucken k¨onnen, woraufhin sich der Text (auf dem Button) zu "Hello world!" a¨ ndert und umgekehrt. Dazu ben¨otigen wir ein Objekt Button, das von Java zur Verf¨ugung gestellt wird. Ein Listener horcht auf Ereignisse, etwa auf den Knopfdruck. Das Applet ist: import java.applet.*; import java.awt.*; import java.awt.event.*; public class goodbyebutton extends Applet implements ActionListener{ Button byebutton; private int status = 0; public void init(){ byebutton = new Button("Good bye!"); add(byebutton); byebutton.addActionListener(this); } public void actionPerformed(ActionEvent event){ if (event.getSource() == byebutton){ if (status == 0) byebutton.setLabel("Hello world!"); else byebutton.setLabel("Good bye!"); status = 1-status; repaint(); } } public void start(){ } public void stop(){ } public void destroy(){ } }
10 Literatur • Java-Online-Referenz: http://java.sun.com/j2se/1.4.2/download.html • Einf¨uhrungen mit Java: – Einf¨uhrung in die Informatik: Objektorientiert mit Java, Wolfgang K¨uchlin, Andreas Weber, Springer, mehrere Auflagen – Algorithmen in Java, Hans Werner Lang, Oldenbourg, 2003
187 – Java: eine Einf¨uhrung, Martin Schader, Lars Schmidt-Thieme, Springer, 2003 • Algorithmen-Klassiker: – Algorithmen und Datenstrukturen, Ottmann/Wiedmayer, Spektrum Verlag, mehrere Auflagen – Agorithms in C (Java), Sedgewick, Addison Wesley • Theoretische Informatik: Theoretische Informatik - eine problemorientierte Einf¨uhrung, Sperschneider/Hammer, Springer, 1996, online auf meiner homepage http://www.informatik.uni-osnabrueck.de/barbara/papers/pub hammer.html • Boolesche Algebren, Schaltalgebren, Zahldarstellung: – Grundz¨uge der Digitaltechnik, Hentschke, Teubner, 1988 – Informatik: Rechenanlagen, Hotz, Teubner, 1972 • Floating Point und Numerik: – Numerische Mathematik, Stoer, Springer, mehrere Auflagen – Numerical Recipes in C, Press et al., Cambridge, mehrere Auflagen • Parallele Prozesse, Betriebsysteme: – Modern Operating Systems, Tanenbaum, Prentice Hall – Concurrent Programming in Java, Lea, Addison-Wesley • Prolog und Logik: – Clause and Effect, Clocksin, Springer – Logic – A Foundation for Computer Science, Sperschneider/Antoniou, Addison-Wesley • Historie: – Der Computer Mein Lebenswerk, Zuse, Springer, 1984 – Vom Abakus zum Internet: die Geschichte der Informatik, Friedrich Naumann, Wissenschaftliche Buchgesellschaft, 2001 • Informationstheorie: Informations- und Kodierungstheorie, Herbert Klimant, Rudi Piotraschke, Dagmar Sch¨onefeld, Teubner, 2003 (2te Auflage) • Zwei sehr gute online-Skripten zur Informatik, es gibt viele weitere – aus Magdeburg: http://ivs.cs.uni-magdeburg.de/sw-eng/agruppe/lehre/ead.shtml – aus Princeton: http://www.cs.princeton.edu/introcs/home/ • und alles weitere k¨onnen Sie selbst im Internet suchen http://www.google.de