281 87 17MB
German Pages 488 [486] Year 2006
eXamen. p r e s s
eXamen.press ist eine Reihe, die Theorie und Praxis aus allen Bereichen der Informatik für die Hochschulausbildung vermittelt.
Peter Pepper
Programmieren lernen Eine grundlegende Einführung mit Java 2. Auflage Mit 128 Abbildungen und 13 Tabellen
a - Springer
Peter Pepper Technische Universität Berlin Fakultät IV - Elektrotechnik und Informatik Institut für Softwaretechnik und Theoretische Informatik Franklinstraße 28/29 10587 Berlin [email protected]
Die erste Auflage erschien 2004 im Springer-Verlag Berlin Heidelberg unter dem Titel Programmieren mit Java. Eine grundlegende Einführung für Informatiker und Inzenieure, ISBN 3-540-20957-3.
Bibliografische Information der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http:lldnb.ddb.de abrufbar.
ISSN 1614-5216 ISBN-10 3-540-32712-6 Springer Berlin Heidelberg New York ISBN-13 978-3-540-32712-7 Springer Berlin Heidelberg New York ISBN-10 3-540-20957-3 1. Auflage Springer Berlin Heidelberg New York Dieses Werk ist urheberrechtlich geschützt. Die dadurch begründeten Rechte, insbesondere die der Übersetzung, des Nachdrucks, des Vortrags, der Entnahme von Abbildungen und Tabellen, der Funksendung, der Mikroverfilmung oder der Vervielfältigung auf anderen Wegen und der Speicherung in Datenverarbeitungsanlagen, bleiben, auch bei nur auszugsweiser Verwertung, vorbehalten. Eine Vervielfältigung dieses Werkes oder von Teilen dieses Werkes ist auch im Einzelfall nur in den Grenzen der gesetzlichen Bestimmungen des Urheberrechtsgesetzes der Bundesrepublik Deutschland vom 9. September 1965 in der jeweils geltenden Fassung zulässig. Sie ist grundsätzlich vergütungspflichtig. Zuwiderhandlungen unterliegen den Strafbestimmungen des Urheberrechtsgesetzes. Springer ist ein Unternehmen von Springer Science+Business Media
O Springer-Verlag Berlin Heidelberg 2004,2006
Printed in Germany Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften. Text und Abbildungen wurden mit größter Sorgfalt erarbeitet. Verlag und Autor können jedoch für eventuell verbliebene fehlerhafte Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung übernehmen. Satz: Druckfertige Daten des Autors Herstellung: LE-T& Telonek, Schmidt & Vöckler GbR, Leipzig Umschlaggestaltung: KünkelLopka Werbeagentur, Heidelberg Gedruckt auf säurefreiem Papier 3313100 YL - 5 4 3 2 1 0
Vorwort
Ich untevichte es rrur; ich habe nicht gesagt, dass ich etwas davon verstehe. Robin Williams i n Good Will Hunting
Was ma.cht eigentlich eine Programmiersprache aus? Die Frage ist schwerer zu beantworten, als es auf den ersten Blick scheinen mag. An der Oberfläche ist eine Sprache definiert durch ihre Syntax und Semantik. Das heiRt, man muss wissen, welche Konstrukte sie enthält, mit welchen Schlüsselworten diese Konstrukte notiert werden und wie sie funktionieren. Aber ist das schon die Sprache? Bei einfachen Sprachcn mag das so sein. Aber bei größeren professionellen Sprachen ist das nur ein Bruchteil des Bildes. Ein typisches Beispiel ist JAVA. Der Kern von JAVA, also die Syntax und Semantik, ist relativ klein und iiberschaubar. Ihre wahre Mächtigkeit zeigt die Sprache erst in ihren Bibliotheken. Dort gibt es Hunderte von Klassen mit Tausenden von Methoden. Diese Bibliotheken erlauben es dem Prograrrimierer, bei der Lösung seiner Aufgaben aus dem Vollen zu schöpfen und sie auf hohen1 Niveau zu konzipieren, weil er viel technischen Kleinkram schon vorgefertigt geliefert bekommt. Doch hier steckt auch eine Gefahr. Denn die Kerrisprache ist (hoffentlich) wohl definiert und vor allem standardisiert. Bei Bibliotheken dagegen droht immer Wildwuclis. Auch JAVA ist nicht frei von diesem Problem. Zwar hat rnari sich grundsä.tzlich große Mühe gegeben, die Bibliotheken einigermaBen systematisch und einheitlich zu gestalten. Aber im Laufe der Jahre sind zahlreiche Ergänzungen, Nachbesserungen und Änderungen entstanden, die es immer schwerer machen, sich in dem gewaltigen Wust zurechtzufinden. Aber da ist noch rnehr. Zu einer Sprachc gehört auch noch eine Samrnlung von Werkzeugen, die das Arbeiten mit der Sprache unterstützen. Auch hier glänzt .JAVA mit einem durcha.us beachtlichen Satz von Tools, angefangen vom Compiler und Interpreter bis hin zu Dokumentations- und Archivicrungshilfen.
V111
Vorwort
Und auch das ist noch nicht alles. Denn eine Sprache verlangt auch nach einer bestimmten Art des Umgangs mit ihr. Es gibt Techniken und Methoden des Programrnierens, die zu der Sprache passen und die man sicli zu Eigen machen muss, wenn man wirklich produktiv mit ihr arbeiten will. Und es gibt Arbcitsweisen, die so konträr zur Sprachphilosophie sind, dass nur Schauriges entstehen ka.nn. Irgeridwie müssen sich alle dicse Aspekte in einem Buch wiederfinden. Und gleichzeitig soll es irn Umfang noch überschaubar bleiben. Bei JAVA kommt das der Quadratur des Kreises gleich. So gibt es zum Beispiel zwci Bücher mit dcn schöncn Titeln „Java in a Nutshell" 1151 und „Java.Foimhtions Classes in a Nutshell" 1141. Beides sind reine Nachschlagewerke, die nichts enthalten als Aufzählungen von JAVA-Features, ohne die geringsten didaktischen Ambitionen. Das erste behandelt nur die grundlegenden Pxkages von JAVA und hat 700 Seiten, das a.ndere befasst sich mit den Packages zur grafischen Fenster-Gestaltung und hat 800 Seiten. Offensichtlicli miiss es viele Dinge geben, die in einem Einfiihri~rigsbuchnicht stehen können. Das vorliegende Buch hat das Programmierenlernen als Thema und JAVA als Vehikel. Und es geht um eine Einführung, nicht um eine erschöpfende Abhandlung über alles und jedes. Deshalb muss vieles uribehandelt bleiben. Alles andere wäre auch hoffniungslos. Aus diesem Blickwinkel heraus war es ein echtes Problem, dass während dcs Schreibens des Buches das sog. Beta-Release der neuen Version J a v a 1.5 erschien. Ini Gegensatz zu den früheren Versionen sind hier wirkliche Neiierungen enthalten. Vor allem aber sind unter diesen Neuerungen auch einige, die echte Liicken im bisherigen Sprachdesign schließen. Deshalb habe ich mich entschlossen, die wichtigsten Erweiterungen von JAVA 1.5 in den Text aufzunehmen. Das kleine Risiko, dass sich vorn Beta-Release zur endgültigen Version noch etwas ändern kann, scheint tolerierbar. Jedes Eiriführungsbuch in JAVA ha.t mit einem Problem zu kämpfen: JAVA ist für erfa.hrene Programmierer konzipiert worden, nicht für Anfänger. Deshalb begarincn dic erstcn JAVA-Büchermeist mit einem Kapitel der Art W a s ist anders als in, C ? Inzwischen hat die Sprache aber einen Reife- und Verbreiturigsgrad gefunden, der diese Form des Einstiegs iiberfliissig macht. Deshalb findet man hcutc vorwiegend drei Arten von Büchern: Die eine Gruppe bietet einen Einstieg in JAVA.Das IieiBt, es werden die elementaren Konzepte von JAVA vermittelt. Deshalb wenden sich dicse Biicher vor allem an JAVA-Neulingeoder gar Programmier-Neulinge. Die zwcitc Gruppc taucht erst in rieuerer Zeit auf. Diese Bücher konzentrieren sich auf fortgeschrittene Aspekte von JAVA und wendcn sicli daher an erfahrene JAVA-Programmierer. Typische Beispielc sind [SO] und [33]. Dic drittc Gruppc sind Nachschlagewerke. Sie erheben keinen didaktischen Anspruch, sondern listen nur die JAVA-Featuresfür bcstinimtc Anwendurigsfelder auf. In diese Gruppe gehören x. B. die schon erwähnten Titcl
Vorwort
IX
1141 und [15], sowie das umfangreiche Handbuch [27], aber a.iich das erfreulich knappe Biichlein 1451. Das vorliegende Buch gehört in die erste Gruppe. Es beschränkt sich aber nicht darauf, nur eine Einfüllrurig in JAVA zu sein. Vielmehr geht es darum, Prinzipien des Progra.mmierens vorzustellen und sie in JAVA zu repräsentieren. Auf der anderen Seite habe ich groBc Miihe daraiif verwendet, nicht einfach die klassischen Programmiertechniken von l>A'ASCALauf .JAVA umzuschreiben (was man in der Litera.tur leider allzu oft findet). Stattdessen werden die Lösungen grundsätzlich im objektorientierten Paradigma entwickelt und auf die Eigenheiten von JAVA abgest,immt. Weil JAVA fiir erfahrene Programmierer konzipiert wurde, fehlen in der Sprache leider einige Elcmcnte, die den Einstieg fiir Anfänger wesentlich erleichtern wiirdcn. Das ist umso bedauerlicher, weil die Hinzurlahme dieser Elemente leicht möglich gewesen wäre. Wir haben an der T U Berlin aber davon abgesehen, sic in Form von Priiprozessoren hinzuzufügen, weil es wichtig ist, dass eine Sprache wie JAVA in ihrer Originalform vermittelt wird. Damit wird das Lehren von JAVA für Anfänger aus didaktischer Sicht eine ziemliclie Herausforderung. Dieser Herausforderung gerecht zu werdcn, wa.r ein vorrangiges Anliegen beim Schreiben dieses Buches. Das Buch ist aus einer zwcisernestrigeri Vorlesung an der Technischen Universität Bcrlin hervorgegangen, die vor allem Studierenden der Elektrotechnik und auch Wirtschaftsingenieuren eine Einführung in dic Informatik geben soll. Die Erfahrungen, die in dieser Vorlesung i h r mehrere Jahre hinweg mit .JAVA gewonnen wurden, haben die Struktur des Buches wesentlich geprä.gt. Mcin besonderer Dank gilt den Mitarbeitern, die während der letzten Jahre viel zur Gestaltung der Vorlesung und damit zu diesem Buch beigetragen haben, insbesondere Michael Cebiilla, Martin Grabmiiller, Tllorrlas Nitsclie und Baltasar Trancori y Widmann. Ma.rtin Grabmüller ha.t viel Mühc darauf verwendet, die Programme in diesem Buch zu prüfen und zu verbessern. Die Mitarbeiter des Springer-Verlags haben durch ihre kompetente Unterstiitziing viel zu der jetzigen Gestalt des Buches beigetragen. Bcrliri, im August 2004
Peter Pepper
Inhaltsverzeichnis
Teil I Objektorientiertes Programmieren 1
Objekte und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.1 Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Beschreibung von Objekten: Klassen . . . . . . . . . . . . . . . . . . . . . . . 1.3 Klassen und Konstriiktormethodcn . . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Beispiel: Punkte irr1 R? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.2 Klassen in JAVA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3.3 Koristruktor-Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4 Objekte als Attribute von Objekten . . . . . . . . . . . . . . . . . . . . . . . . 1.4.1 Beispiel: Linien iin R? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Anonyme Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5 Objekte in R.eili iind Glied: Arrays . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.1 Beispiel: Polygone in1 R? . . . . . . . . . . . . . . . . . . . . . . . . . . 1.5.2 Arrays: Eine erste Einführung . . . . . . . . . . . . . . . . . . . . . . . 1.6 Zusammenfassung: Ol-~jekteund Klassen . . . . . . . . . . . . . . . . . . . .
3 3 G 8 8 9 10 14 14 16 16 17 18 21
2
Typen. Werte und Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.1 Beispiel: Elementare Datentyperi von JAVA . . . . . . . . . . . . . . . . . 2.2 Typen u ~ i dKlassen. Werte und Ot~jekte. . . . . . . . . . . . . . . . . . . . 2.3 Die Benerinimg von Werten: Variablen . . . . . . . . . . . . . . . . . . . . . 2.4 Konstanten: Das hohe Gut der Beständigkeit . . . . . . . . . . . . . . . . 2.5 Meta.morphoscn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.1 Casting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.5.2 Von Typen zu Klassen (iirid zilriick) . . . . . . . . . . . . . . . . . 2.6 Zirsarnrnenfassimg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23 24 27 27 29 30 30 32 33
3
Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1 Methoden sind Prozeduren oder Fiiriktiorieri . . . . . . . . . . . . . . . . 3.1.1 Eimktiorieri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.2 Prozeduren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35 35 35 37
XI1
4
Inhaltsverzeichnis 3.1.3 Methoden und Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.4 Overloading (Überlagerung) . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Lokale Variablen und Konstant.en . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Lokale Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Lokale Konstantcn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.3 Parameter als verkappte lokale Variablen* . . . . . . . . . . . . 3.3 Beispiele: Punkte und Linien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Die Klasse Point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Die Klasse Line . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Private Hilfsmethodcn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.4 Fazit: Methoden sind Fiinktionen oder Prozediireri . . . . .
38 40 40 40 42 42 44 44 47 47 48
Programmieren in Java . Eine erste Einführung . . . . . . . . . . . 4.1 Programme schreiben und ausführen . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Der Programmierprozess . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.2 Die Hauptklasse und die Methode main . . . . . . . . . . . . . . 4.2 Ein einfaches Beispiel (mit ein bisschen Physik) . . . . . . . . . . . . . 4.3 Bibliothckcn (Packages) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.1 Packages: Eine erste Einfiilirung . . . . . . . . . . . . . . . . . . . . . 4.3.2 Öffentlich, halböffentlicli und privat . . . . . . . . . . . . . . . . . . 4.3.3 Standardpackages von JAVA . . . . . . . . . . . . . . . . . . . . . . . . 4.3.4 Die Java-Klasse Math . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.5 Die Klassc Terminal: Einfache Ein-/Ausgabe . . . . . . . . . 4.3.6 Kleine Beispiele mit Grafik . . . . . . . . . . . . . . . . . . . . . . . . . 4.3.7 Zeichnen in JAVA: Elementare Grundbegriffe . . . . . . . . .
51 51 52 54 55 58 59 59 59 60 62 63 66
Teil I1 Ablaufkontrolle 5
Kontrollstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1 Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Elementare Anweisungen und Blöcke . . . . . . . . . . . . . . . . . . . . . . . 5.3 Man muss sich auch entscheiden können . . . . . . . . . . . . . . . . . . . . 5.3.1 Die i f -Anweisurig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.2 Die switch-Anweisurig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4 Immer und immer wieder: Iteration . . . . . . . . . . . . . . . . . . . . . . . . 5.4.1 Die while-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4.2 Die f or-Schleife . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.4.3 Die break- und cont inue-Anweisung . . . . . . . . . . . . . . . . 5.5 Beispiele: Sclilcifen und Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 6.1 R.ekursive Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 6.2 Fimktioriirrt das wirklich? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
71 71 73 74 74 76 78 78 80 81 83
Inhaltsverzeichnis
XI11
Teil I11 Eine Sammlung von Algorithmen 7
Aspekte der Programmiermethodik . . . . . . . . . . . . . . . . . . . . . . . . 7.1 Mari rriuss sein Turi auch erläutern: Dokumentation . . . . . . . . . . 7.1.1 Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2 Zusicherungen (Assertions) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.2.1 Allgemeine Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 Aufwand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.3 Beispiel: Mittelwert und Standardabweichung . . . . . . . . . . . . . . . 7.5 Beispiel: Fläche eines Polygons . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.6 Beispiel: Sieb des Eratosthenes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.7 Beispiel: Zinsrechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
Suchen und Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 8.1 Ordnung ist die halbe Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117 8.2 Wer sucht, der findet (oder auch nicht) . . . . . . . . . . . . . . . . . . . . . 118 8.2.1 Lineares Suchen: Die B ~ i t i s h - M u s e u mMethod . . . . . . . . . 118 8.2.2 Suchen mit Bisektiori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 8.3 Wer sortiert, findet sclineller . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 8.3.1 Selection sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125 8.3.2 Insertion. sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 8.3.3 @L.L cksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 8.3.4 Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132 8.3.5 Heupsort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134 8.3.6 Mit Mogeln gehts schnellcr: B ~ ~ c k sort e t . . . . . . . . . . . . . . 140 8.3.7 Verwandte Probleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
9
Numerische Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 9.1 Vektoren und Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141 9.2 Gleichurigssystenie: Gai&Elirniriation . . . . . . . . . . . . . . . . . . . . . . 144 9.2.1 Lösung von Dreieckssysternen . . . . . . . . . . . . . . . . . . . . . . . 147 9.2.2 LU-Zerlegung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148 9.2.3 Pivot-Elcmcnte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 9.2.4 Nacliiteration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151 9.3 Wiirzelberechriurig und Nullstelleri von Funktionen . . . . . . . . . . . 152 9.3 Differenzieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 9.5 Integriere11. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157 9.6 Interpolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160 9.6.1 Für Gcizhälsc: Speichcrplatz sparen . . . . . . . . . . . . . . . . . . 165 9.6.2 Extrapolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166 . . . . . . . . . . . . . . . . . . . . 169 9.7 Lösung einfacher Dif~erenzialgleicl~u~ige~i 9.7.1 Einfache Einschrittverfahrcn . . . . . . . . . . . . . . . . . . . . . . . . 170 9.7.2 Mchrsclirittvcrfaliren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171 9.7.3 Extrapolation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
97 97 98 99 101 102 106 107 110 112
XIV
Inhaltsverzeichnis
9.7.4
Schrittweitensteuerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
Teil IV Weitere Konzepte objektorientierter Programmierung 10 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 10.1 Vererbung = Si~btyp?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 10.2 Sub- lind Superklassen in JAVA . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 10.2.1 „Mutierte1' Vererbung und dynamische Bindung . . . . . . . 181 10.2.2 Was bist du? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 10.2.3 Ende der Vererbung: Object und f i n a l . . . . . . . . . . . . . . 184 10.2.4 Mit super zur Superklasse . . . . . . . . . . . . . . . . . . . . . . . . . . 186 10.2.5 Casting: Zurück zur Sub- oder Superklassc . . . . . . . . . . . 187 10.3 Abstrakte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188
11 Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.1 Melirfachvererbung und Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . 11.2 Anwendung: Suchen und Sortieren richtig gelöst . . . . . . . . . . . . . 11.2.1 Das Interface S o r t a b l e . . . . . . . . . . . . . . . . . . . . . . . . . . . .
191 191 195 196
12 Generizität (Polymorphie) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.1 Des einen Vergangenheit ist des anderen Zukunft . . . . . . . . . . . . 12.2 Die Idee der Polymorphie (Generizität) . . . . . . . . . . . . . . . . . . . . . 12.3 Generizität in JAVA 1.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
199 199 200 201
13 Und dann war da noch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 13.1 Einer für a.lle. s t a t i c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 203 13.2 Initialisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 13.3 Innere imd lokale Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 13.4 Anonynle Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207 13.5 Enunierationstypen in Java 1.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209 13.6 Anwendung: Methoden höherer Ordriurig . . . . . . . . . . . . . . . . . . . 209 13.6.1 Fun als Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 13.6.2 Verweridung a.nonyrner Klassen . . . . . . . . . . . . . . . . . . . . . . 211 13.6.3 Interpolation als Implementierung von Fun . . . . . . . . . . . 212 13.7 Ein bisscheri Eleganz: Methoden als Resultate . . . . . . . . . . . . . . . 212 14 Namen. Scopes und Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 14.1 Das Prinzip der (Un-)Sichtbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . 215 14.2 Gültigkeitsbereich (Scope) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 14.2.1 Klassen als Giiltigkeitsbereich . . . . . . . . . . . . . . . . . . . . . . . 217 14.2.2 Methoden als Gültigkeitsbereich . . . . . . . . . . . . . . . . . . . . . 218 14.2.3 Blöcke als Gültigkeitsbereicli . . . . . . . . . . . . . . . . . . . . . . . . 218 14.2.4 Vcrschattung (holes in the scope) . . . . . . . . . . . . . . . . . . . . 219 14.2.5 Uberla.gerinig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Inhaltsverzeichnis
XV
14.3 Packages: Scopes „iin GroBenL'. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220 14.3.1 Volle Klasserinarnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 14.3.2 Irriport . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222 14.4 Gelieirriniskrämerei . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 14.4.1 Geschlossene Gesellschaft: Packagc . . . . . . . . . . . . . . . . . . 223 14.4.2 Herstellen von Öffentlichkeit: p u b l i c . . . . . . . . . . . . . . . . 223 14.4.3 Maximale Verschlossenheit: p r i v a t e . . . . . . . . . . . . . . . . . 224 14.4.4 Vertrauen zu Subklassen: p r o t e c t e d . . . . . . . . . . . . . . . . . 224 14.4.5 Zusannrierifassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Teil V Datenstrukturen 15 Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.1 Nichts währt ewig: Lebensdauern . . . . . . . . . . . . . . . . . . . . . . . . . . 15.2 Referenxen: „Ich weiX, wo mans findet" . . . . . . . . . . . . . . . . . . . . . 15.3 Referenzen in JAVA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.1 Zur Funktionsweise von Referenzen . . . . . . . . . . . . . . . . . . 15.3.2 Referenzen lind Methodenaiifriife . . . . . . . . . . . . . . . . . . . . 15.3.3 Wer bin ich'!: t h i s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.4 Gleichheit und Kopien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.5 Die Wahrheit über Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.6 Abfallheseitigurig (Gcrbage collection) . . . . . . . . . . . . . . . . . . . . . .
229 229 231 232 232 235 237 237 239 240
16 Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 16.1 Listen als verkettete Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 16.1.1 Listenzellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244 16.1.2 Elementares Arbeiten mit Listcn . . . . . . . . . . . . . . . . . . . . 246 16.1.3 Traversieren von Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 16.1.4 Generische Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 16.1.5 Zirkulare Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250 16.1.6 Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . 251 16.1.7 Eine methodische Schwäche und ihre Gefahren . . . . . . . . 252 16.2 Listen als Abstrakter Datentyp ( L i n k e d ~ i s t .) . . . . . . . . . . . . . . 253 16.3 Listcnartige Strukturen in JAVA . . . . . . . . . . . . . . . . . . . . . . . . . . 256 16.3.1 C o l l e c t i o n . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258 16.3.2 L i s t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 16.3.3 S e t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259 16.3.4 LinkedList, ArrayList und Vector . . . . . . . . . . . . . . . . . 259 16.3.5 Stack . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 260 16.3.6 Queue (,,WarteschlangcL1). . . . . . . . . . . . . . . . . . . . . . . . . . . 261 16.3.7 Priority Qiieiies: Vordrängeln ist erlaubt . . . . . . . . . . . . . 262 16.4 Einer nach dem andern: Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . 263 16.5 Neue f or-Schleife in JAVA 1.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 264
XVI
Inhaltsverzeichnis
17 Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 17.1 Bäume: Grundbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267 17.2 Iniplementierung durch Verkettung . . . . . . . . . . . . . . . . . . . . . . . . 268 17.2.1 Binärhäunie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 17.2.2 Allgemeinc Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 17.2.3 Binärbäunie als Abstrakter Datentyp . . . . . . . . . . . . . . . . 272 17.3 Traversieren von Bäumen: Baum-Iteratoren . . . . . . . . . . . . . . . . . 273 17.4 Suchbäume (geordnete Bäume) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 17.4.1 Suchbäiirne als Abstrakter Datentyp: SearchTree . . . . . . 278 17.4.2 Implenieiitierung von Suchbäumcn . . . . . . . . . . . . . . . . . . . 279 17.5 Balancierte Suchbäurrie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284 17.5.1 2-3-Bäume und 2-3-4-Bäurnc . . . . . . . . . . . . . . . . . . . . . . . . 286 17.5.2 Rot-Schwarz-Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288 17.6 Baumdarstellung von Sprachen (Syntaxbäurrie) . . . . . . . . . . . . . . 293 18 Graphen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.1 Beispiele für Graphcn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.2 Grundbegriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.3 Adjazenzlisten iirid Adjazenzinatrizen . . . . . . . . . . . . . . . . . . . . . . 18.4 Erreichbarkeit und verwandte Aufgaben . . . . . . . . . . . . . . . . . . . . 18.4.1 Konxeptueller Entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.4.2 Klassische Programmierung in Java . . . . . . . . . . . . . . . . . . 18.4.3 Eine gcnuin objcktoricntierte Sicht von Grapl~algoritl~rrle ................................. 18.4.4 Tiefen- und Breitensuche . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.5 Kürzeste Wege (von einem Knoten aus) . . . . . . . . . . . . . . . . . . . . 18.6 Aufspannende Bäume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.7 Transitive Hülle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.8 Wcitere Graphalgorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
299 299 301 302 304 305 306 308 309 311 312 313 316
Teil V1 Programmierung von Software-Systemen 19 Keine Regel ohne Ausnahmen: Exceptions . . . . . . . . . . . . . . . . . 321 19.1 Manchmal gehts eben schief . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321 19.2 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 323 19.3 Man versuchts halt mal: t r y und catch . . . . . . . . . . . . . . . . . . . . 325 19.4 Exceptions verkünden: throw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327 10.5 Mcthodcn mit Exceptions: throws . . . . . . . . . . . . . . . . . . . . . . . . . 328
20 Ein- und Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.1 Olinr Verwaltung geht gar nichts . . . . . . . . . . . . . . . . . . . . . . . . . . 20.1.1 Pfade und Datrinameri in Windows und Unix . . . . . . . . . 20.1.2 F i l e : Die Klasse zur Dateivcrwalturig . . . . . . . . . . . . . . . 20.1.3 Programmieren der Dateivcrwalturig . . . . . . . . . . . . . . . . .
331 332 333 334 336
Inhaltsverzeichnis
XVII
20.2 Was man Lesen und Schreiben kann . . . . . . . . . . . . . . . . . . . . . . . 337 20.3 Dateien mit Direktzugriff („Externe Arrays") . . . . . . . . . . . . . . . . 339 20.4 Seqilenzielle Dateien („Externe Listen", Ströme) . . . . . . . . . . . . . 340 20.4.1 Die abstrakte Superklasse InputStream . . . . . . . . . . . . . . 342 20.4.2 Die konkreten Klasscn für Eingabeströrne . . . . . . . . . . . . 342 20.4.3 Ausgabeströmc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344 20.4.4 Das Ganze nochmals niit Unicode: Reader und Writer . . 345 20.5 Progra.rnrnieren mit Dateien und Strömen . . . . . . . . . . . . . . . . . . 346 20.6 Terniirial-Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347 20.7 . . .und noch ga.nz viel Spezicllcs . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 20.7.1 Seria.lisierimg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 20.7.2 Interne Kommunikation über Pipes . . . . . . . . . . . . . . . . . . 352 20.7.3 Konkatenation von Strömen: SequenceInputStream . . . 352 20.7.4 Simulierte Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 21 Konkurrenz belebt das Geschäft: Threads . . . . . . . . . . . . . . . . . 355 21.1 Threads: Leichtgewichtige Prozesse . . . . . . . . . . . . . . . . . . . . . . . . 355 21.2 Die Klasse Thread . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 359 21.2.1 Entstehen Arbeiten Sterben . . . . . . . . . . . . . . . . . . . . . 360 21.2.2 Schlafe nur ein Weilchen . . . ( s l e e p ) . . . . . . . . . . . . . . . . . 361 21.2.3 Jetzt ist mal ein anderer dran . . . ( y i e l d ) . . . . . . . . . . . . . 362 21.2.4 Ich warte auf dein Ende . . . ( j o i n ) . . . . . . . . . . . . . . . . . . . 362 21.2.5 Unterbrich mich nicht! ( i n t e r r u p t ) . . . . . . . . . . . . . . . . . 364 21.2.6 Ich bin wichtiger als du! (Prioritäten) . . . . . . . . . . . . . . . . 365 21.3 Synchronisation und Kommunikaiion . . . . . . . . . . . . . . . . . . . . . . 366 21.3.1 Vorsicht, es klemmt! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368 21.3.2 Warten Sie, bis Sie aufgerufen werden! (wait, n o t i f y ) . 369 21.4 Das Interface Runnable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372 21.5 Ist dasgenug? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373 21.5.1 Gemeinsam sind wir stark (Thread-Gruppen) . . . . . . . . . 373 21.5.2 Dämonen sterben heimlich . . . . . . . . . . . . . . . . . . . . . . . . . . 374 21.5.3 Zu 1a.ngsa.rnfür die reale Zeit? . . . . . . . . . . . . . . . . . . . . . . . 374 21.5.4 Vorsicht, veraltet! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 21 5 . 5 Neues in Java 1.5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375 -
-
22 Das ist alles so schön bunt hier: Grafik in JAVA . . . . . . . . . . . 377 22.1 Historische Vorbemerkung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377 22.1.1 Awt und Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378 22.1.2 Entwicklungsumgeburigen . . . . . . . . . . . . . . . . . . . . . . . . . . 379 22.2 Grundlegende Konzepte von GUIs . . . . . . . . . . . . . . . . . . . . . . . . . 380
XVIII Jnhaltsverzeichnis
23 GUI: Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383 23.1 Die Superklassen: Component und JComponent . . . . . . . . . . . . . . 385 23.2 Elementare GUI-Elemente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386 23.2.1 Beschriftungen: Label/ JLabel . . . . . . . . . . . . . . . . . . . . . . 386 . .n. . . . . . . . . . . . . . . . . . 387 23.2.2 Zum Anklicken: ~ u t t o n ~/ ~ u t t o 23.2.3 Editierbarer Text: ~ e x t ~ i e l d / ~ ~ e x t ~.i. e. .l. d. . . . . . 389 23.3 Behälter: Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 392 23.3.1 Das Hauptfenster: ~ r a m e / ~ F r a m. e. . . . . . . . . . . . . . . . . . . 392 23.3.2 Lokale Container: Panel /JPanel . . . . . . . . . . . . . . . . . . . . 396 23.3.3 Layout-Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 23.3.4 Statischer Import in Java 1.5 . . . . . . . . . . . . . . . . . . . . . . . 399 23.3.5 Mehr i h e r Farben: Color . . . . . . . . . . . . . . . . . . . . . . . . . . 400 23.3.6 Fenster-Geometrie: Point und Dimension . . . . . . . . . . . . 402 23.3.7 GröBenl->estirnrriii~~g von Fenstern . . . . . . . . . . . . . . . . . . . . 402 23.4 Selbst Zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 405 23.4.1 Die Methode paint . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406 23.4.2 Die Methode paintcomponent . . . . . . . . . . . . . . . . . . . . . . 407 23.4.3 W e m man nur zeichr~enwill . . . . . . . . . . . . . . . . . . . . . . . . 408 23.4.4 Zeichnen niit Graphics und Graphics2D . . . . . . . . . . . . . 409 24 Hallo Programm! . Hallo GUI! . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 24.1 Auf GUIs ein- und ausgeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411 24.2 Von Ereignissen getrieben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412 24.3 Immerzu lauschen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414 24.3.1 Beispiel: Eingabe im Displayfeld . . . . . . . . . . . . . . . . . . . . . 414 24.3.2 Arbeiten mit Buttons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416 24.3.3 Listener-Arten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 418 .
25 Beispiel: Taschenrechner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.1 Tasclienrechrier: Die globale Struktur . . . . . . . . . . . . . . . . . . . . . . 25.2 Taschenrechner: Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.3 Taschenrechner: View . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.4 Taschenrechner: Control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.5 F a ~ i .t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
421 422 423 426 433 436
Teil V11 Ausblick 26 Es gäbe noch viel zu tun . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.1 Java und Netzwerke: Von Sockets bis Jini . . . . . . . . . . . . . . . . . . . 26.1 .1 Die OSI-Hierarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.1.2 Sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.1.3 Wenn die Methoden weit weg sind: RMI . . . . . . . . . . . . . . 26.1.4 Wie komme ich ins Netz? (Jini) . . . . . . . . . . . . . . . . . . . . . 26.2 Javauriddas Web . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
439 439 440 443 443 445 445
Inhaltsverzeichnis
26.3
26.4 26.5 26.6 26.7
A
26.2.1 Applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.2.2 Servlets (Server Applets) . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.2.3 JSP: JavaServer Pages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.2.4 Java und XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.2.5 Java und Email . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sicher ist sicher: Java-Secilrity . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.3.1 Sandbox und Sccurity Manager . . . . . . . . . . . . . . . . . . . . . 26.3.2 Verschliisselung und Signaturen . . . . . . . . . . . . . . . . . . . . . Reflection und Introspectiori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Java-Komponenten-Technologie: Beans . . . . . . . . . . . . . . . . . . . . . Java und Datenbanken: JDBC . . . . . . . . . . . . . . . . . . . . . . . . . . . . Direktzugang ziirn Rechner: Von JNI bis Realzeit . . . . . . . . . . . . 26.7.1 Die Java Virtual Machine (JVM) . . . . . . . . . . . . . . . . . . . . 26.7.2 Das Java Native Interface (JNI) . . . . . . . . . . . . . . . . . . . . . 26.7.3 Externe Prozesse starten . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.7.4 Java und Realzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
XIX
445 449 450 450 451 451 452 453 453 454 457 457 457 458 459 459
Anhang: Praktische Hinweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 461 A.l Javabcschaffcn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 461 A.2 Java installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 462 A.3 Java-Programme iibersetzen ( j avac) . . . . . . . . . . . . . . . . . . . . . . . 463 A.3.1 Verwendung von zusätzlicheri Directorys . . . . . . . . . . . . . . 464 A.3.2 Verwendung des Classpath . . . . . . . . . . . . . . . . . . . . . . . . . 465 A.3.3 Konflikte zwischcn Java 1.4 und Java 1.5 . . . . . . . . . . . . . 466 A.4 Java-Programme ausführen ( java und j avaw) . . . . . . . . . . . . . . . 466 A.5 Directorys, Classpath und Packages . . . . . . . . . . . . . . . . . . . . . . . . 468 A.6 Java-Archive verwenden ( j a r ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 469 A.7 Dokurrientation generieren mit j avadoc . . . . . . . . . . . . . . . . . . . . 471 A.8 Weitere Werkzeuge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 473 A.9 Die Klassen Terminal und Pad dieses Bilches . . . . . . . . . . . . . . . 473 A.10 Materialien zu diesem Buch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 474
Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 475 Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 Hinweis: Eine Errata-Liste und weitere Hinweise zu diesem Buch sind über die WebAdresse h t t p : //www .uebb . CS .t w b e r l i n .de/books/ javazu erreichen . Näheres findet sich irn Anhang .
Teil I
Objektorientiertes Programmieren
Man sollte auf den Schultern seiner Vorgänger stehen, nicht auf ihren Zehenspitzen. (Sprichwort)
Die Welt ist voller Objekte. Ob Autos oder Konten, ob Gehaltsabrechnunyen oder Messfühle7; alles kann als „Ob,jektd'betrachtet werden. Was liegt also näher, als ein derart universell anwendba,res Kon,zept auch, zur Basis des Programmieren~von Computern, 271, ma,chen,. Denn letztendlich enthält jedes Computerprogramm eine Art „Schattenwelt': i n der jedes (für das Programm relevante) Ding der realen Welt ein virtuelles Gegenstück besitzt. Und die Hoffnung ist, dass die Programme besser mit der realen Welt harm.onieren, wenn beide auf die gleiche Weise organisiert ,werden,. In den 80er- und 90er-Jahren des zwanzigsten Jahrhunderts hat sich auf dieser Basis eine Programmiertechnik etabliert, die ~ ~ n t dem e r Schlagwort objektorientierte Programmierung zu einem der wichtigsten Trends i m modernen So.ftware-Engineering geworden ist. Dabei 7uar aa dieser Methode eigentlich gar nichts Neues dran. Sie ist vielrr~,elvrein geschicktes Konglomerat uon diversen Techn,iken, die jede ,für. sich seit Jahren in der Informatik wohl bekannt w.nd intensiv erforscht war.
Und das ist auch keine Schande. I m Gegenteil: Gute Ingenieurleistungen erkennt m a n daraß, dass sie wohl bekannte u,rr,d sichere Technolo,qierr,z~ neuen, sinnvollen und niitzlichen Systemen kombinieren. Das ist allemal besser, als inno,uativ u m jeden Preis sein zu wollen und unerprohte u,nd riskan,te Experim,entalsysteme auf die Menschheit loszulassen. Deshalb wurde die objektorientierte Pr~gram~mierung auch eine Erfolgsstory: Sie hat Wohlfundiertes und Bewährtes zusammengefiigt. Leider gibt es aber einen kleinen Haken bei der Geschichte. Die Protagonisten der Methode wollten aus welchem Grund auch immer innovativ erschein,en,. Um, das zu erreichen,, wandten, sie einen sim,plen Trick aa: Sie haben alles anders genannt, als es bis dahin hiej3. Das hat zwar kurzzeitig funktioniert, es letztlich aber nur schwerer gemacht, der objektorientierten Programmierung ihre wohl definierte Rolle i7n Softu1are-Enqir~~eering zuzuweisen. l n den fo1geade.n. Kapiteln werden die grundlegenden Ideen der ohjektorientierten Programmierung eingeführt und ihre spezielle Realisierung i m Rahmen der Sprache JAVA skizziert. Da.bei wird aber auch die BrYicke zu den traditionellen Begri;rfEichkeiten der Informatik geschlagen. -
-
Objekte und Klassen
Wo Begriffe fehlen, stellt ein Wort zur rechten Zeit sich ein. Goethe. Faust
Bei der objektorientierten Programmierung geht es wie der Name vermuten lässt inn Objekte. Leider ist ,,Objektu ein Allerweltswort, das etwa den gleichen Grad von Bestimmtheit hat wie Ding, Sache, haben, tun oder sein. Damit stehen wir vor einem Problem: Ein Wort, das in der Unigangssprache für tausenderlei Dinge stehen kann, muss plötzlich mit einer ganz bcstimmten technischen Bedeutung verbimderi werden. Natürlich steht hinter einer solchen Wortwahl auch eine Idee. In diesem Fall gcht CS um cinen Paradigmenwechsel in der Program,m,ierun,g.Während klassischerweise die Algorithmen im Vordergrund standen, also das, was die Programme bei ihrer Ausführung tun, geht es jetzt nielir um Strukturierung der Programme, also um die Organisation der Software. Kurz: Nicht mehr „ Wie wirds getan? ist die primäre Frage, sondern „ Wer tvts." -
-
1.1 Objekte Um den Para =
Der Parameter a wird hier als eine lokale Variable 1nissbra.ucht. Da.s heiBt, das Bild, das wir in Abschnitt 3.2.1 bei dcr Methode heron gezeichnet liaberi,
*
Dieser Abschnitt kann beim erstcn Lescn übersprungen werden
3.2 Lokale Variablen und Konstanten
43
entspricht nicht ganz der Realität. Die Parameter müssen ebenfalls als „SlotsL' behandelt werdcn. Wir illustricrcn das arihand der Methode f 0 0 : - f oo ( a )
(Pa.rametcr)
long a int
n
X
x=a+l ; a=x*x ; r e t u r n a+x;
Was passiert z. B. bei einem Aufruf der folgenden Art? i n t k = 2; i n t s = f oo(k) ; int j =k; // j wird auf 2 gesetzt Beim Aufruf von f oo wird der Parameter a auf den Wert von k, also 2, gesetzt.
Slot eingetmgen. Da.nn wird im Da.s heifit, die 2 wird in den entspre . . - - " i Variante dreht sie den Punkt um einen beliebigen . . . . anderen Punkt c herum. (Man sieht hier wieder die X Möglichkeit von JAVA Methoden zu überlagern, d. h., den gleichen Namen zu verweriden, solange die Parameter verschieden sind.) Das kann man einfach so irnplerrieritiereri, dass rnari der1 Drehpunkt c mittels s h i f t zum Ursprung eines rieiien Koordiriaterisysterris macht, dann in diesem System da.s cinfa.che r o t a t e a.usführt, und danach wicder ins alte Koordinatensystem zurückshiftet. Man beachte, dass der Winkel für r o t a t e in Grad angegeben wird, die Funktionen s i n und cos aber im BogenmaB (auch Radiant genannt und mit rad bezeichnet) berechnet werden. Dazu dient die in JAVA vordefinierte Furiktiori Math. toRadians. Die Prozedur r o t a t e ist ohne eine grafische Erläuterung nicht verständlich. Am einfachsten wird die Berechnung, wenn wir nicht den Punkt p = (X,y) i ~ gegebenen n Koordinatensystem in die Position p' = (X',y') drehen, sondern stattdessen das Koordinatensystem rotieren und den Origirialpunkt p in dem neueri System betrachten, wo er die Koordinaten (X',y') hat. -
-
1,:
Dem rechten Bild entnimmt man sofort die folgenden Bcziehungen:
_
3.3 Beispiele: Punkte und Linien
45
Programm 3.1 Die Klasse P o i n t class Point C // Attribute: Koordinaten. double X; double y; // Konstruktor-Methode Point ( double X, double y ) C this.x = X ; this.y = y ; 1 // Point // Methoden für Polarkoordinaten double dist C double d = Math.sqrt (x*x + y*y) ; return d; 1 // dist double angle 0 C double phi = Math.atan(y/x) ; return phi ; 1 // angle // verschieben void shift ( double dx , double dy C this.X = this .X + dx; this.y = this.y + dy; 1 // shift // rotae,re,r~ void rotate ( double angle ) // Note: angle is given as 0" . . . 360" double phi = Math.toRadians (angle) ; double xOld = this .X; double yOld = this .y; this .X = x0ld * Math.cos(phi) - yOld * Math.sin(phi) ; this.y = x0ld * Math.sin(phi) + yOld * Math.cos(phi) ; 1 // rotate void rotate ( Point center, double angle ) // Note: angle is given as 0' . . . 360' double phi = Math.toRadians (angle) ; this .rotate(angle) ; this.shift (center.X, center .y) ; 1 // rotate 1 // end of dass Point
Damit ergeben sich folgende Rcchnuiigcn, um die neuen Koordinaten X' und ?J' in Abhängigkeit von den altcn Koordinaten z,y und dem Winkel cp zu erhalten:
46
3 Methoden 2' = 21 coscp
= (X
- n:2)
cos p
= X cos cp
- x2 cos (P xcoscp - yi sincpcoscp = .r cos cp - 1 "siri cp cos cp ,-Os iP
=
=
xcoscp
-
ysincp
=
Y'
z2sincp + ycoscp
Y; +Y; = (x2sincp+ gcoscp) + x l s i n p = z s i n p + ycosp =
Für Interessierte. In den Staridardbibliotlieken von JAVA (auf die wir in Kap. 14.3 noch genauer eingehen werden) gibt es ein Package j ava. awt .geom, in dem eine Klasse Af f ineTransf o r m enthalten ist. Diese Klasse stellt Opcratiorieri bereit, die unserem s h i f t und r o t a t e entsprechen; dazu kommen noch die 0pera.tionen s c a l e , die eine Deliniing des Koordiriatensysterns bewirkt, und shear, die eine Verzerrung des Koordinatcrisystcms bewirkt. Alle diese Operationen lassen sich kompakt in einer Matrixdarstellung folgender Art repräsentieren. Dabei wird eine dritte Zeile hinziigefügt, damit auch die additiven Bestandteile bei s h i f t berücksichtigt werden können.
rotate(cp1:
rotate(c, p):
(?)(si;:;
:)
cosp -sincpO cyp
(); ( =
x .coscp - y . sincp G)=(x.sinp:y.coscp)
cos cp - sin p (C, - c, . cos cp sin cp cos cp (cu - cZ . sin cp 0 0 1
+ C, + C,
. sin cp) . cos cp)
Man rechnet sofort nach, dass die Matrix von r o t a t e ( c , cp) sich aus dein Produkt der Matrizen
ergibt. Und das entspricht genau unserer Methode r o t a t e (cent e r , angle), weil die Anwendung der drei Funktionen ja von rechts nach links zu lcscn ist. Zum Scliluss sei noch erwähnt, dass die bciden weiteren Methoden der ~ A ~ ~ - K l aAsf sf eineTransf orm sich durch folgende Matrizen darstellen lassen:
3.3 Beispiele: Punkte und Linien
47
Genauso wie oben gezeigt, lassen sich alle rnögliclien Kornbinationeri dieser Operationen durch entsprechende Multiplikation der Matrizcn erreichen. Die Matrixforni liefert also eine Möglichkeit, auch komplexe geometrische Manipulationen auf kompakte Weise darzustellen. 3.3.2 Die Klasse Line
Diese ganze niatliematische Mühe zahlt sich jetzt sehr schön aus. Denn nachdem wir in der Klasse Point die relevanten Methoden definiert haben, bekonimen wir die Klasse Line „fast geschenkt". Die Operationen s h i f t und r o t a t e müssen nur jeweils auf die Endpunkte angewandt werden. Und für die Länge der Strecke und den Steigungswinkel p stehen die entsprechenden Ausdrücke J ( x 2 x1)2 + (y2 und tanp = X L Z ~ L in jeder rriatlierriatischen For1-2-51 melsaninilung. Man muss allerdings aufpassen, dass man keine senkrechte Linie hat, weil dann der Steigimgswinkel unendlich ist (s. Abschnitt 2.1). Programm 3.2 enthält den Code. -
-
Übung 3.1. Man ergänze die Klasse L i n e um weitere Funktionen der Analytischen Geometrie, z. B. 0 0
boolean c o n t a i n s ( P o i n t p ) : Liegt p auf der Linie? P o i n t i n t e r s e c t i o n ( L i n e o t h e r ) : Schnittpunkt der beiden Linien (falls definiert). boolean i s P a r a i l e l ( L i n e o t h e r ) : Sind die beiden Linien parallel?
Übung 3.2. Man ergänze die Klasse L i n e u m eine weitere Konstruktor-Methode 0
L i n e ( P o i n t p, double l e n g t h , double angle)
die den Anfangspunkt, die Länge und den Steigungswinkel vorgibt.
3.3.3 Private Hilfsmethoden
Die Methode square in der Klasse Line cnthält etwas Ncucs. Vor den Typ haben wir noch das Schlüsselwort p r i v a t e gesetzt! Was bedeutet das? Offensichtlich ist das Quadrieren einer Zahl im Gegensatz zu s h i f t , r o t a t e etc. keine Funktion, die zur geometrischen Idee dcr ,,Linieu gehört. Wir benötigen diese Funktion nur, weil damit die Programmierung der Funktion l e n g t h etwas kürzer wird. Solche Hilfsfunktionen sollen deshalb aiich innerhalb der Klasse verborgen werden. Der Effekt ist, dass bei cineni Objekt Line 1= new Line ( p , q) der Aufruf 1.square (X) vom JAVA-Conipiler als Fehler zurückgewiesen wird. Geriauer werden wir dieses Thema in Abschnitt 14.4 behandeln. -
-
48
3 Methoden
Programm 3.2 Die Klasse Line class Line C Point pl; // Attribute: Endpunkte Point p2; // Kon,struktor-Methode Line ( Point pl, Point p2 ) C this.pl = pl; this.p2 = p2; 3 // Point double length 0 // Länge return Math.sqrt (square(p2.X-pl.X) + square(p2.y-pl.y) ) ; 3 // length //HzlJ~fun,k:tioa(primt!) private double Square ( double X ) C return x*x; 3 // square // steiy.clrt,g (0" . . . 36'0") double gradient 0 C double phi = Math.atan( (p2.y-pl.Y) / (p2.X-pl.X)) ; return Math.toDegrees (phi) ; 3 // gradient // verschieben void shift ( double dx, double dy ) this.pl.shift(dx,dy); this.p2.shift(dx,dy); 3 // shift // rotieren (0" . . . ,360") void rotate ( double angle ) this .pl.rotate(angle) ; this.p2.rotate(angle); 1 // rotate void rotate ( Point center, double angle ) C this .pl.rotate(center ,angle) ; this .p2.rotate(center,angle) ; 3 // rotate 3 // end of class Line
3.3.4 Fazit: Methoden sind Funktionen oder Prozeduren
Funktionen und Prozeduren werden in JAVA prinzipiell nicht durch die Notation unterschieden. Das einzige Untersclleidungsmcrkmal ist, dass Prozeduren als „ErgebnistypU den Pseudotyp v o i d haben. Der Ergebnistyp wird wie in JAVA generell iiblicli vor den Funktionsnamen geschrieben. Die Liste der formalen Parameter besteht aus null, einem oder mehreren getypten Namen, die durch Komma getrennt sind. Die Klammern sind zwingend vorgeschrieben; d. li., Methoden ohne Parameter werden durch die ,,leeren Klammcrd' ( ) charakterisicrt. -
-
3.3 Beispiele: Punkte und Linien
49
Der Rumpf wird in die Klammern C . . . ) eingeschlossen und enthält die Aktionen, die die Methode bei ihrem Aufruf ausführt. AuBerdern köiinen im Rumpf auch noch lokale Hilfsvariablen und -konstanten eingeführt werden. Bei Funktionen steht im Rumpf ein Ausdruck, der das Ergebnis liefert. (Üblicherweise aber nicht notwendigerweise - ist dics die letzte Anweisung des Rxmpfes.) Dieser Ausdruck folgt auf das Schliisselwort r e t u r n . -
Übrigens: Auch die Konstruktormethoden sind offensichtlich Funktionen, denn sie liefern als Resultat ja gerade ein neiies Objekt der entprcchcnden Klasse. Aber sie sind die einzigen Methoden, bei denen JAVA auf die Angabe des Ergebnistyps verzichtet. Eine Schreibweise wie Point Point (double X, double y)C. . . I sähe ja auch zu komisch aus.
Programmieren in Java - Eine erste Einführung
One programs into a language, not ,in it. David Grzes 1211
Im letzten Kapitel haben wir die Grundclcrncntc des objektoricnticrten Prograrnniiereiis kennen gelernt. Jetzt wollen wir mit der ta.tsä.chlichen Progranrnierung in der Sprache JAVA beginnen. Dabei müssen wir folgende Aspekte unterscheiden:
0
den Programmierprozess, d. h. die von uns als Programmierer auszuüberiden Aktivitäten; das Programm, d. h. diejenigen Dirigc („Artefakte1'),die beim Prograrnniieren entstehen; die P~ogr(xrr~rr~:ierum,qeb,~~r~g, d. 11. die Sammlung von Wcrkzcugen, die vom Betriebssystem und vom .JAVA-Systembereitgestellt werden; die Bibliotheken, d. h. die Sa.mrnlungeri von Klassen, die von den JAVAEntwicklern bereits vordefiniert wurden, damit wir bcim Programmieren weniger Arbeit haben.
4.1 Programme schreiben und ausführen Zunächst ist „das ProgrammierenLL ein ingeniei~rrnäfiigorgmisierter Arbeitsprozess, in dem man im Wesentlichen folgende Tätigkeiten durchführen muss: 0
0
Modellieren (des Problems) Spezifizieren (der genauerl Aiifgabenstellung) Entwurf (der Lösung) Codiercri (in der Programmiersprache) Testen (mit systernatiscll ausgewählten Testfällen) Dokumentieren (während aller Phasen)
52
4 Programmieren in Java
-
Eine erste Einführung
Wie man sieht, ist das eigentliche Programmieren (irn Sinne von „Programmtexte in Sprache X eintippen") nur ein ganz kleiner Teil dieses Prozesses. Allerdings ist die Beherrschurig dieses Teils unabdingbare Voraussetzung für alles andere! Beim Entwickelri von Software stehen uns eine ganze Reihe von Werkxeugen (engl.: tools) zur Verfiigung. Ohne dicsc Werkzeuge ist eine Programmerzeugung nicht möglich, weshalb ihre Beherrschung ebenfalls zu den notweridigen Fertigkeiten von Informatikern und Ingenieuren gehört.
4.1.1 Der Programmierprozess „Die schlimmsten Fehler inacht m a n 2n der Absicht, einen Fehler gutzumach,en. " ( J e a n Pavl)
Der iibliche Arlxitsablauf ist in Abb. 4.1 dargestellt.
MyProg .java
MyProg.class
Abb. 4.1. Arbeitsablauf bei der Programmerstellung
1. Zunächst wird mit Hilfe eines Editors der Programnltext geschrieben und in einer Datei gespeichert. Wir nennen diese Textdateien hier Programmdateien. Da.bei sind in JAVA folgende Bedingungen zu erfüllen: 0 Die Datei muss die Endung „ .j avaLL haben. 0 Der Name der Datei muss mit dem Namen der Hauptklasse des Programms üI->ereiristirnrnen.(In unserem Beispiel in Abb. 4.1 muss die Hauptklasse also c l a s s MyProg C . . . 1 sein.) 2. Dann wird diese Textdatei dem .JAVA- Compiler iibergeben. Das geschieht, indem ma.n in dcr Rctriebssystem-Shcll das Kommando j avac MyProg . j a v a eingibt. Der Compiler tut darin zweierlei: 0 Zunächst analysiert er das Prograrnrri und generiert gegebenenfalls Fclilcrrrieldurige~i. 0 Falls das Prograrnrri korrekt ist, erzeugt er Ma,schin,encode und speichert ihn in cincr Da.tei. Diese Datei hat folgende Eigens~haften:
4.1 Programme schreiben und ausführen
53
Sie hat den gleichen Namen wie die eingegebene Textdatei. Sie hat die Endimg „ . class". 3. Die Ausführung dieses Maschinencodes' kann dann beliebig oft, inid jeweils mit anderen Eingabedaten erfolgen. Dies geschieht durch das Betriebssysteni-Komnia~~do java MyProg Hier darf die Eridinig „ . class" nicht angegeben werden. -
-
In diesen1 Prozess gibt es zwei Stellen, an denen man iiblicherweise mehrfach iterieren muss: Wenn der Compiler Fehler im Programmtext findet, iniiss man sie mit dem Editor korrigieren. Und wenn bei den ersten Testl ä u f e n nicht die erwarteten Resultate herauskommen, muss man die Gründe dafür suchen und die entsprechenden Programmierfehler elrxnfalls mit dem Editor korrigieren. Abb. 4.2 zeigt den EEekt der Übersetzung irn Betriebssystem. (In diesem Fall handelt es sich um WINDOWS XP, wobei für die .java- und für die . classDateien spezielle Icons dcfinicrt wurden.)
Datei
Bearbeiten
Ansicht
Fauorien
Extras
7
$@
Datei
Bearbeiten
Ansicht
Favoriten
Extras
7
$1
Abb. 4.2. ~ a t e i & i . ~und o r riacli der Übersetzung
Man sieht, dass aus den vier . . . 1 Klassen, die in der Prograinmdatei MyProg .j ava definiert class A C ... 3 sind, vier individuelle . classclass B I ... 3 Dateien werden. Dabei niuss class C C ... 3 die Hauptklasse so heiKen wie die Datei, in unsercin Fa11 also MyProg. Darauf gellen wir unten gleich noch genai~erein. classMyProg ( p u b l i c s t a t i c voidmain ( S t r i n g l l a r g s ) ( > / / e n d OS closs MyProy
Es handelt sich um Code für die sog. JVM (.Java Virtual Machine).
54
4 Programmieren in Java
-
Eine erste Einführung
Variationen. Die obige Prozessbeschreibung trifft nur a.uf die allereinfacl~stenFälle zu. In dcr Praxis crgebcri sich Variationen. 0
0
Ein Progra.mm, das aus mehreren Klassen besteht, kann anch auf mehrere Dateien verteilt werden. In diesem Fall ist es guter Brauch, dass darin jede Datei nur eine Klasse enthält (deren Na.nieri sie dann trägt). Meistens ist der JAVA-Compiler so nctt, allc für ciri Progranini benötigten Dateien automatisch zusarnnierizusuchen und zu cornpilieren, sobald man die Hauptdatei conipiliert. (Leider versagt dieser Automatismus aber in gewissen subtilen Situationen, was zu verwirrenden Fehlersituationeri führen kann. Denn obwohl man den Fehler in der Progranirndatei korrigiert hat, tritt er beim Testen immer noch a.uf.)
4.1.2 Die Hauptklasse und die Methode main
Es gibt noch eine weiterc Besonderheit von JAVA, die wir beriicksichtigen miissen. Sie betrifft die Hauptklasse eines Programnis. Im Beispiel von Abb. 4.1 haben wir angenommen, dass dies die Klasse MyProg ist und deshalb die Ausführimg des Prograrrirris mit dem Bcfclil j a v a MyProg
gestartet. Eine solche Kla.ssc:ka.nn a.ber viele Methoden iimfassen. Woher weiß das ~ ~ v ~ - S y s tdann, e n i mit welcher Methode es die Arbeit beginnen soll? Dies ist ein generelles Problern, das alle Program~niersprachenhaben. Es lässt sich auf zwci Weisen lösen. Entweder man verlangt, dass beim Startbefehl nicht nur die Klasse, sondern auch die Methode angcgcben wird. Odcr man legt fest, dass die Startmethode irnnier den gleichen Namen haben miss. Die Designer von JAVA haben sich für die zweite Regel entschieden. Und der Starldardnarne fiir die Startnicthode ist „mainL1.Die Anforderungen sind aber noch schärfer: main muss inirner den gleichen Typ haben. Für unser Beispiel gilt somit, dass die Hauptklasse MyProg folgendcs Aussehen haben muss: c l a s s MyProg C p u b l i c s t a t i c v o i d main ( S t r i n g [ 1 a r g s ) C // Sturtmethode
.. .
1 // end of method m a i n
1 // end
o,f
class MyProg
Ini Augenblick ignorieren wir, was die zusätzlicheri Arigaheri „ p u b l i c Uund „ s t a t i c L Lbedeuten iirid wozu der Parariicter „ a r g s U dient. Wir merken uns nur, dass „mainL1immer so aussehen niuss. Da.mit kiinnen wir iins die Ausfiil~rungcines Prograrnrnes folgcridcrmaßcn vorstellen:
4.2 Ein eirifa.clies Beispiel (mit ein bisscl-ieri Physik)
0
55
Wenn das JAVA-Systemmit eirierri Befehl wie j ava MyProg gestartet wird, kreiert es als Erstes ein (anonymes) Objekt der Klasse MyProg. Dann ruft das Systeni die Methode main dieses anonymen Objektes auf. Danach geschieht das, was wir irn Runipf der Methode main programmiert haben. Wenn alle Aktionen irr1 R.urnpS von main abgearbeitet sind, beendet das System iinser Prograrrirn.
Im Prinzip können wir beliebig viel in den Rumpf von main hineinpacken. Und die Hauptklasse kann auch beliebig viele weitere Methoden enthalten. In der Pra.xis hat sich &er die Konvention bewährt, die Hauptklasse so kna.pp wie möglich zu fassen und die ganze eigentliche Arbeit in andere Klassen zu delegieren. (Was das heißt, werden wir gleich an Beispielen sehen.) P r i n z i p d e r P r o g r a m m i e r u n g : Restrzktive Benutz.trng von main Die Methode main, die a.ls Startmethode jedes 1a.uffähigen JAVAProgra.mms zu verwenden ist, sollte so wcriig Code wie möglich enthalten. Idealerweise kreiert main nur ein Anfangsobjekt und übergibt da.nn diesem Objekt die weitere Kontrolle.
4.2 Ein einfaches Beispiel (mit ein bisschen Physik) Um das bisher Gesagte zu illustrieren, betrachten wir ein vollständiges Beispiel. Da unsere Progra.mmicrmitte1 bisher noch sehr bcschrä.nkt sind, muss das ein sehr kleines Beispiel sein. Aber wir werden es auch benutzen, inn ein paar weitere Konzepte einzufiihren. In Pliysikbüchern kann man folgende Berechnung fiir den „schiefen WurfLL nachlcscn: Ein Körpcr wird in cinern Winkel p rnit einer Anfangsgcscliwiridigkeit , u ~geworfen. Für die Höhe und die Weite dieses Wurfes ergeben sich die mathematischen Formeln ans Ahb. 4.3.
Wi~rfliöhe: h = Winfweitr: W
cri
4 sinLp 29
=
"sin 2 9 "lL
9
Abb. 4.3. Schiefer Wurf
Wir haben es bci dicscm Progranim mit mindestens drei Klasscri zu tun, nanllicli mit den I-leiden vordefinierten Klassen Terminal lind Math sowie rnit iinserern eigentlichen Programm. Auf die vordefinierten Klassen Terminal und
4 Programmieren in Java
56
-
Eine erste Einführung
Math gehen wir später noch geiiauer ein. Zunächst konzentrieren wir uns auf
unsere eigene Programmierung. Wir haben schon in Abschnitt 4.1 (auf Seite 5 5 ) festgestellt, dass man die Methode main so knapp wie möglich fassen sollte.
Prinzip der Programmierung Die fiir JAVA notwendige Methode main wird in eine Miriiklasse eingepackt. Die Methode main t u t nichts arideres als ein Objekt zu kreieren, das dann die eigentliche Arbeit leistet. Das neu zu generierende Objekt wird durch eine eigene Klasse beschrieben. Daniit erhalten wir insgesanlt vier Objekte (vgl. Abb. 4.4): JAVA erzeugt beim Prograninistart ein anonymes Startobjckt zur Klasse Wurf. Dieses generiert (in der Methode main) nur ein Objekt werf e r , das dann zusammen mit Terminal und Math die eigentliche Arbeit leistet. -
-
f
«anonym» (Wurf)
\ r
\
'r
C
I
... \
d
Terminal
\
...
...
...
...
werf e r
d
Math
\
...
1 -
... \
&
... b Abb. 4.4. Programm mit Tcrminal-Ein-/Ausgabe und Mathematik \
Der Prograrnrncode hat die Struktur von Programm 4.1: Die Klasse Wurf enthält nur die Methode main. (Auf die Annotation p u b l i c gehen wir gleich in Abschnitt 4.3.2 ein.) In der Methode main wird zunächst ein ncues Objekt werf e r kreiert, dessen Beschreibung in der Kla.sse Werfer enthalten ist. Dann wird die Methode werfen dieses Objekts aufgerufen. Die Klasse Werfer genauer: das Objekt werf e r , das durch die Klasse heschrieheri wird leistet die eigentliche Arbeit. Die Klasse Werfer iirrifasst die eigentlich interessierende Methode werfen sowie einige Hilf'sfunktionen, nämlich weite, höhe lind bogen. Ai&xlein gibt es noch die Gravitatioriskonstante G. Da es sich bei allcn um HilfsgriiBen handelt, sind sie als p r i v a t e gekennzeichnet (s. Abschnitt 3.3.3). Die zentrale I\/lethode werfen funktioniert folgeridermaBen: -
-
0
Da.s Progrurnrn gibt zuerst eine Überschrift aus und fragt dann nach zwei reellem Za.hler1. Das geschieht über eine spezielle vordefinierte Klasse Terminal (s. Abschnitt 4.3.5).
4.2 Ein einfaches Beispiel (mit ein bisscheri Physik)
57
Programm 4.1 Das Programm Wurf public class Wurf C public static void main (String [ I args) Werfer werfer = new Werfer 0; werfer.werfen 0;
1 1 // end
of class
C
Wurf
class Werfer C void werfen C Terminal.println( " \ nSchiefer Wurf \ n") ; double vO = Terminal.askDouble ("vO = ? ; double winke1 = Terminal.askDouble("phi = ? 'I) ; double phi = bogen(winke1) ; Terminal.println(""); Terminal .println( "Weite = " + weite (vO ,phi) ) ; Terminal.println( "Höhe = " + höhe (vO ,phi) ) ; 'I)
3 private double weite ( double vO, double phi) 1 return (vO*vO)/G * Math.sin(2*phi) ;
1 private double höhe ( double vO, double phi) double s = Math.sin (phi) ; return (vO*v0)/(2*G) * (s*s) ;
1 private double bogen ( double grad return grad * (Math .PI/ 180) ;
)
1
3 private double G = 9.81; 1 // end of d a s s Werfer
0
0
Da wir den Winkel in Grad eingeben wollen, JAVA aber alle trigonometrischen Funktiorlen irr1 BogenrriaK berechnet, müssen wir den Winkel entsprechend konvertieren (niit der Hilfsfunktion bogen). Danach wird eine Leerzeile ausgegeben und dann folgen die beidcn Ergcbnisse.
In den Hilfsfimktioneri w e i t e lind höhe I-)erechrlenwir die entsprechenden physikalischen Formeln. Dazu brauchen wir Funktionen wie s i n und Konstanten wie P I , die von JAVA in der vordefinierten Klasse Math bereitgestellt weiden (s. Absclinitt 4.3.4). Übrigens: Die Hilfsfunktiori bogen hätten wir nicht selbst zu prograrnmieren k~raucheri.Die Klasse Math bietet uns dafiir die Methode toRadians an (s. Abschnitt 4.3.4). Anincrkung: Es ist klar, dass wir beim Aufruf von Metliodcn dcs eigenen, Objekts keim Punkt-Notation brailclieri. Das heist, während wir bci
4 Programmieren in Java
58
-
Eine erste Einführung
fremden Objekten z. B. sdireiben müssen Terminal.println( . . . ) , genügt es natürlich nur z. B. weite (vO ,phi) zu sclireiben. (Es wäre aber auch legal, this .weite(v0,phi) zu schreiben aber das würde die Lesbarkeit massiv stören.) Dieses Programm kann z. B. zu folgendem Ablauf fiihren. (Mari beachte, dass die Weite aiifgrurid diverser Rundungsfehlcr nicht 0 ist, sondern eine winzige Zahl N 1OpS?) Die Bcnutzereingabe kennzeichnen wir durch Kursivschrift. -
> javac Wurf. java
> java Wurf Schiefer Wurf vo = ? 1 0 phi = ? 90 Weite Hoehe
= =
1.248365748366313E-15 5.09683995922528
> Am Ende zeigt uns das sog. Prompt 5'a.n, dass das Programm beendet ist und das Betriebssystem (z. B. UNIX oder wrivnows) wieder bereit ist, neue Aufträge von uns entgegenzuriehmeri. Übung 4.1. [Zins] Ein Anfangskapital K werde m i t jährlich p% verzinst. Wie hoch ist das Kapital nach n Jahren? Wie hoch ist das Kapital, wenn man zusätzlich noch jedes Jahr einen festen Betrag E einzahlt? Sei ein Anfangskapital K gegeben, das nach folgenden Regeln aufgebraucht wird: Im ersten Jahr verbraucht man den Betrag V ; aufgrund der Inflationsrate wächst dieser Verbrauch jährlich um p%. Wann ist das Kapital aufgebraucht? Hinweis Für alle drei Aufgaben gibt es geschlossene Formeln. Insbesondere gilt für
q
#
1 die Gleichung Cr="=, q' =
1-'77>
11
4.3 Bibliotheken (Packages) Es wärc äuibcrst unökonomisch, wcmi man bei jedrni Programmierauftrag das Rad immer wieder neu erfiiiden wiirde. Deshalb gibt es groRe Sairirrilurigen von nützlichen Klassen, auf die Inan zurückgreifen k a m . Solche Sannnlungeil werden Bibliotheken genannt; iri JAVA heiBen sie Packages. Es gibt im Weseritliclicii drei Artrri von Bibliotliekeil: Gcwisse Bibliotheken bekommt man mit der Prograrnniierspraclie mitgeliefert. Viele Firmen kreieren in1 Laufe der Zeit eigene Bibliotheken für die firmeiispezifisclicri Applikationen.
4.3 Bibliotheken (Pa.cka.ges)
59
SchlieBlich schaffen sich auch viele Programmierer irn Laufe der Jahre eine eigene Bibliotheksurngebung.
4.3.1 Packages: Eine erste Einführung Ein Package in JAVA ist eine Sammlung von Klassen. (Später werden wir sehen, dass auBerdein noch sog. Interfaces liirizukommen.) Werin man so wie wir das irn Augenblick noch tun einfach eine Sammlung von Klasscn in eincr odcr mehreren Textdateien definiert und diese dann übersetzt und ausführt, gcricricrt JAVA dafür ein (anonymes) Package, in dem sie alle gesarnmrll werden. Wenn man seine Klassen in ehern P a c k a g ~san~melrirriöchte, darin muss man an1 Anfang jcdcr Datei als erste Zeile schreiben package mypackage ; Das führt dazu, da.ss alle in der Datei definierten Klassen zimi Package mypackage gehören. Wenn man also in fünf verschiedericn Dateien jeweils diese erste Zeile schreibt, dann gehören alle Klassen dieser fiinf Dateien zum selber1 Package, das den schörieri Namen mypackage trägt. Diese Packages haben subtile Querverbindurigeri zuin Dateisystern des jeweiligen Betrichssytems, weshalb wir ihre Bchandlimg auf Kap. 14 verschicben. Wir wollen zunäclist auch keinc eigenen Packages schreiben (weil uns das anonyme Package genügt), sondern nur vordefinierte Packages von JAVA benutzen. -
4.3.2 Öffentlich, halböffentlich und privat Wir ha.tten in Abschnitt 3.3.3 gesehen, dass man Methoden und Attribute in einer Klasse verstecken kann, indem man sie als p r i v a t e kennzeichnet. Von aiiiierhalb der Klasse sind sie dann nicht rrielir zugänglich. Wir werden in Kap. 14 sehen, dass normale Klassen, Attribute lind Methodcri „halböffcritlich" sind. (Das heiBt im Wesentlichen, dass sie in ihrem Package sichtbar sind.) Wenn man sie wirklich global verfügbar machen will (also auch aiißerhalb ihres Pxkages), niim ma.n sie als p u b l i c kerirrzeichrien. Wir könrien auf die geriaueri Spielregeln fiir die Vergabe der public- und private-Qualifikatoren erst in Kap. 14 cirigeheii. Bis dahin halten wir iiris an die Intuition, dass wir diejenigen Klassen und Methoden, die wir „öfferitlich verfügbarL'machen wollen, als p u b l i c kennzeichnen.
4.3.3 Standardpackages von JAVA Das JAVA-Systemist mit einer Reihe von vordefinierten Pa.ckages ausgestattet. Da dieser Vorrat über die JAVA-Versionenhinweg ständig wächst, geben wir hier nur eine Aiiswahl der wichtigsten Packages ari. 0
java. lang: Einige Kerriklassen wie z. B. Math, S t r i n g , System iuid Obj e c t .
60
0 0
0
0
0 0 0 0 0
0 0 0 0
4 Programmieren in Java
-
Eine erste Einführung
j a v a . i o : Klassen zur Ein- und Ausgabe auf Dateien etc. j a v a . u t i l : Vor allem Klassen für einige nützliche Datenstrukturen wie Stack oder Hashtable. j ava .n e t : Klassen für das Arbeiten mit Netzwerken. j ava . s e c u r i t y : Klassen zur Realisierung des JAVA-Sicherheitsrriodells. j a v a . a p p l e t : Die Applet-Klasse, über die JAVA mit www-Seiten interagiert. j ava .beans: „ JAVA-Beans",eine Unterstützung zum Schreiben wiederverwendbarer Software-Kornponeriten. j ava .math: Klassen für beliebig g r o k Integers. j ava .r m i : Klassen zur Remote Method Iwuocatior~. j ava .s q l : Klassen zum Dateribankzugriff. j ava .t e x t : Klassen zum Management von Texten. j a v a . awt: Das JAVA Abstract Windo,wing Toolkit; Klassen und Interfa.ces, niit denen n1a.n grafische Benutzerschnittstellen (GUIs, „Fenstersysteme") programmieren kann. j avax .swing: Die modernere Version der GUI-Klassen. j avax . crypto: Klassen für kryptographische Methoden. javax. sound. . . : Klassen zum Arbeiten mit Midi-Dateien etc. j avax .xml . . . : Klassen fiir das Arbeiten mit XML.
Einige dieser Packagcs haben weitere Unterpackages. Das Abstract Wzndowzng Toolktt j ava .awt hat z. B. rieben vielen eigenen Klassen auch noch die Unterpackages j ava .awt .image und j ava . awt .p e e r . Als neueste Entwicklung gibt es das j avax. swing-Package (das seinerseits aus 14 Unterpackages besteht), mit dem wesentlich flexiblere und ausgefeiltere GUI-Prograrnrriieriing möglich ist. (Darauf gehen wir in den Kapiteln 22-25 noch genauer ein.) 4.3.4 Die Java-Klasse Math Ein typisches Beispiel fiir eine vor>>=
&=.
I=
Tabelle 5.2. Abkürzungen für spezielle Zuweisungen
Eine weitere Besoridcrheit von JAVA sollte auch nicht unerwähnt bleiben, obwohl sie einen Verstoß gcgcri gutcn Programmierstil darstellt: Man kann z. B. schreiben i=( j = i + l ) ; oder noch schlimmer i=j =i+i ;. Das ist dann gleichbedeutend mit den zwei Zuweisungen j = i + i ; i = j ;. Der gesparte Schreibaufwand wiegt i. Allg. nicht den Verlust an Lesbarkeit auf.
5.3 Man muss sich auch entscheiden können
. ..
In praktisch allcn Algorithmcn muss man rcgclmäfiig Entscheidungen treffen, welche Anweisimgen als Nächstes ai*szuführen sind. In Mathernatikbüchern findet man dazu Schrcibwciscn wie z. B. muz(u, b) =
a b
falls a sonst
>b
Leider hat JAVA der schlechten Tradition der Sprache C folgend - hier eine wesentlich unleserlichere Notation gewählt als andcrc Progranimicrsprachen (wie x. B. Pascal): -
i f (a>b)
max = a ; 1 else ( max= b ;
1
// // then,-Zweig (Bed~n~gung true) // // else-Zweig (Bedingung false) //
Das heiibt, ein 'then' fehlt in JAVA,weshalb Klammern und Konventionen zur Einrückung die Lesbarkeit wenigstens notdürftig retten müssen. 5.3.1 Die i f -Anweisung
Mit der if-Anweisung erhält man zwei Möglichkeiten, den Abla.uf eines Programms dynamisch von Bedingungen abhängig zu machen: 0
Man kann eine Anweisiing niir bedingt aiisfiihren ( if - then-Anweisung) .
5.3 Man muss sich auch entscheiden können . . .
75
Man kann eine von zwei Anweisungen alternativ auswählen (if-then-elseAnweisung).
if ( ) {= b ) C return a; 1 else { return b; )
1 (2) Folgende geschachtelte Falluriterschcidung kann zur Bestirrimung der Notc in einer Klausur genommen werden. void benotung ( int punkte ) { int note = 0; if ( punkte >= 87 ) { note = 1; ) else if ( punkte >= 75 ) ( note = 2; 1 else if ( punkte >= 63 ) { note = 3; 1 else if ( punkte >= 51 ) { note = 4;) else {note = 5; 1 Terrninal.println("Note: " + note) ;
1 (3) Das Vorzeichen einer Zahl wird diirch folgende Funktion bestirrirnt: int sign ( int a ) C if ( a > 0 ) Creturn+i; 1 else if ( a == 0 ) { return 0; 1 else { return -1; 1
1 Die Falli1ntersc2leidi1rig ohne Else-Teil kommt seltener vor. Typische Applikationen sind z. B. Situationen, in denen unter bestimmten Urnständen zwar
76
5 Kontrollstrukturen
eine Warnung ausgegeben werden soll, ansonsten aber die Berechnung weitergehen kann: ... i f ( « k r i t i s c h » ) ( ({melde Warnung)));
.. . Übung 5.2. Man bestimme das Maximum dreier Zahlen a , b ,
C.
Übung 5.3. Sei eine Tierpopulation P gegeben, die sich jährlich um p% vermehrt. Gleichzeitig gibt es aber eine jährliche ,,Abschussquote" von k Exemplaren. Wie groß ist die Population P, nach n Jahren? , f a l l s q f 1, ,PI Hinweis: M i t q = 1 & gilt die Gleichung P, = P-k", sonst.
+
5.3.2 D i e switch-Anweisung
Es gibt einen Spezialfall der Falluntcrscheidung, der mit geschachtelten i f Anweisungen etwas aufwendig zu schreiben ist. Deshalb hat JAVA wie viele andere Sprachen auch - dafiir eine Spezialkonstruktion vorgesehen: Wenn man die Auswahl abhängig von einfachen Werten treffen will, nimmt man die switch-Anweisung. -
switch ( =O); Man beachte: Ohne das if würde das Programm beim Ende-Signal noch versuchen, die Wurzel aus der negativen Zahl zu ziehen und dadurch einen Fehler generieren. Viele Programmierer finden das zusätzliche if lästig und verwenden lieber eines der Spraclifeatures break oder continue:
do { a = Terminal.askDouble("a = ") ; if (a < 0) { break; 1 Terrninal.println(">>> sqrt(a) =
"
+ Math.sqrt(a)) ;
1 while (a>=O); Die Anweisung „breakl' hat zur Folge, dass die Schleife abgebrochen wird. Hätte man stattdesscn „if (a < 0 ) { continue; )" geschrieben, so wäre nur der aktuelle Schleifendurchlauf abgebrochen und der nächste Durchlauf mit dem while-Test gestartet worden. Da in diesem Fall der erneute Test "while (a>=O)" aber auch fehlschlägt, wäre (in diesem Beispiel) kein Unterschied zwischen break und continue. Da mit break die Schleife abgebrochen wird, kann man auf einen echten while-Testsogar ga.nz verzichten:
while (true) { a = Terminal.askDouble("a = ") ; if (a < 0) { break; 1 Terminal.println(">>> sqrt(a) = " + Math.sqrt (a)) ;
3 Das hcifit, wir schreiben eine uner~dlicheSchleife mit break-Anweisung. Warn,un,g! Das ist ein,e ziemlich gefährliche Konstruktion, die erfahrungsgemä$ schon bei klein,sten Program,mierun,yen,a,~~i,qkeiten, ~liirklich,zur Nichtterminier.ung fiihrt. Die Gefährlichkeit sieht m a n schon daran,, dass es jetzt fata.1 luare, das b r e a k durch ein cont inue zu ersetzen.
5.5 Beispiele: Schleifen und Arrays
83
Als Alternative zu all diesen gefährlichen Varianten kann man da.s erste Lcsen aus der Schleife herausziehen und dann wieder eine saubere Wiederholung benutzen. a = Terminal. askDouble("a = "1 ; while (a>=O) 1
"
~erminal.println("sqrt(a) =
a
Terminal. askDouble("a
=
=
+ Math. s q r t ( a ) ) ;
"1 ;
1 Das ist die rnetliodisch sauberste Lösung, auch wenn ein Lesebefehl dabci zweimal hingeschrieben werden rimss. Anmerkung: Man kann bei geschachtelten Sdileiferi mit Hilfe von „Ma.rkenLL die einzelnen Schleifenstufen verlassen. Das funktioniert nach dem Schema des fblgeiiden Beispiels: m1: while ( . . . ) {
m2: while ( . . . ) { if ( ...
C continue ml; 1
1 // while m,2 .. .
1 // while m l Wenn die cont inue-Anweisung ausgeführt wird, wird die Bearbeitung urimittelbar niit einem neuen Durchlauf der äuiieren Schleife fortgesetzt, geriauer: mit dem while-Test dieser Schleife. Hätten wir stattdessen continue m2; geschrieben, wiirde sofort ein rieiier Diirclila,iif der inneren Schleife starten (mit dem entsprcchenden while-Tcst). Die analogen Konstruktionen sind auch mit break möglich. Irn obigen Pr.ogra.rnni würde z. B. ein break m2 ; a.nstelle des cont inue ml ; bewirken, dass die innere Schleife abgebrochen und die Arbeit unmittelbar daliiriter fortgesetzt wird. Warnung! Auch diese Konstn~ktionkann leicht zu undurchschaubaren Programmen füh,rer~mit dem Potenzial zu subtilen, Fehlern,.
5.5 Beispiele: Schleifen und Arrays Die Beliebtheit der f or-Schleife basiert vor allein auf ihrer engen Kopplung mit Arrays. Denn die häufigste Anwendung ist sicher das Durchlaufen und Verarbeiten von Arrays. Dabei ha.t man im Wescritlichen drei Artcn von Aufgaben: kum,ulierender Dvrchla~~f, m,odifizierender Durchlauf lind generierender Durchlauf. Von der Programmierung her tritt dabei immer wieder das gleiche Muster auf:
84
I I
5 Kontrollstrukturen
Prinzip der Programmierung Bei der Verarbeitung von Arrays hat man oft das Prograrnrnierrriuster f o r (i = 0 ; i < a . l e n g t h ; i++) ... Die Verwendung des Symbols '5 l k==n)
// ASSERT n if ( k = = O
{
return 1; 1 else C r e t u r n binom(n-I, k-I) + binom(n-I, k) ;
3 //if
>//bino,rr~ In diesem Programm benutzen wir erstmals ein Dokumentationsmittel, da,? uns noch viel niitzeri wird. In einer Zusich,erwn,g (erigl.: c~scertion)setzen wir ziisiitzliche Eirischriirikungeri für die Parameter fest, die fiir das Funktionieren der Methode notwendig sind. (Wir gehen in1 nächsten Kapitel genauer auf Assertions ein.)
über die Verfügbarkeit von Klasscn und Mctlioden machen, die wir mit iinsereri jetzigen Mitteln noch nicht besclireiben kiinnen. Aber intuitiv sollte das Progranlrri 6.2 trotzdem verstä.iidlich sein.
Programm 6.2 Die Türme von Hanoi Der Algorithmiis, der in Abb. 6.1 skUziert ist, lässt sich unmittelbar in eine rekursive JAVA-Methodeumschreiben. void hanoi ( i n t n , Peg a , Peg b, Peg C ) C // von a über b nach C i f (n == 1) C move(a,c) ; // Stein von n nach c 1 else C hanoi(n-I, a, C , b ) ; // n- 1 Steine von a über c nach b
// Stern von a nach C // n 1 Steine u m b iibrr
U
rrach c
Dabei lassen wir offen, wie die Klasse Peg und die 0pera.tion move implcrnentiert sind.
Als letztes dieser einführenden Beispiele soll cinc Frage dienen, die sich Leonardo von Pisa (genannt Fibonacci) gestellt hat: ,,Wie schnell verrnehren sich Kaninchen?" h b e i sollen folgende Spielregeln gelten: (1) Zum Zeitpunkt
92
6 Rekursion
i gibt es Ai alte inid J; junge Paare. (2) In einer Zeiteinheit erzeugt jedes altc Paar ein junges Paa.r, und jedes jungc Kaninchen wird erwachsen. (3) Kaninchen sterben nicht. Wenn man mit einem jungen Paar beginnt, wie viele Kaninchen hat man nach n Zeiteinheiten? Die Antwort gibt das Programm 6.3.
Programm 6.3 Die Vermehrung von Kaninchen (nach Fibonacci) Die Spielregeln des Leonardo von Pisa lassen sich sofort in folgcndc rnathcmatische Gleichungen umschreiben:
Das kann man direkt in ein Paar rekursiver JAVA-Funktionenumschreiben. i n t kaninchen ( i n t i ) C r e t u r n alteKaninchen ( i ) + jungeKaninchen ( i ) ; )// kaninchen i n t alteKaninchen ( i n t i ) C i f ( i == 0) C r e t u r n 0; ) else { r e t u r n alteKaninchen ( i - 1 ) + jungeKaninchen ( i - 1 ) ; 1 )// a l k K a n i n c h e n i n t jungeKaninchen ( i n t i ) C i f ( i == 0) C r e t u r n I ; ) else C r e t u r n alteKaninchen(i-1) ; ) )// jungeKaninchen
Dieses Progranirn umfasst direktc und indirekte Rekursioncn. Die Funktion jungeKaninchen ist indirekt rekursiv, die Funktion alteKaninchen ist sowohl direkt als auch indirekt rekursiv. Übung 6.1. Für die Kaninchenvermehrung kann man zeigen, dass die Zahl K , sich auch direkt berechnen lässt vermöge der Gleichungen
(1) Man zeige, dass diese Gleichungen in der Tat gelten. (2) Man programmiere die Gleichungen als direkt rekursive JAVA-Funktion. (Das ist die Form, in der die Funktion üblicherweise als „ Fibonacci-Funktion" bekannt ist.)
6.2 Funktioniert das wirklich? Ein bisschen sehen diese rekursiven Furiktionen aus wic der Versuch des Barons von Münchhausen, sich a.m eigenen Schopf aus dem Siirnpf zu ziehen. Dass es aber kein Taschenspielertrick ist, sondern seriöse Teclinologie, kann man sich schncll lilarmachen. Allcrdings sollten wir dazu ein kürzeres Beispiel verwenden als die bisher betrachteten. Progranirn 6.4 enthält die rekursive Funktion zur ßerechr~iingder Fakultät n! = 1 . 1 . 2 . 3 . . .,r~,.
6.2 Funktioniert das wirklich?
93
Programm 6.4 Fakultät Die „Fakultäts-Funktion" in der Mathematik meist geschrieben als ri! berechnet das Produkt aller Zahlen 1, 2, . . . , n. Das wird rekursiv folgendermaßen geschrieben: -
0! (n
-
=1
+ I)! = ( n + 1) * n!
Offensichtlich lässt sich dieser Algorithmus ganz cinfach als Funktion hinschreiben: int fac ( int n ) C if (n > 0) C return n * fac(n-1) ; // rekurszuer Aufruf! 1 else C return 1; 1 // endzf
1 // fac
An diesem einfachen Beispiel können wir uns jetzt klarrriachen, wie R.ekursion funktioniert. Erinnern wir uns: Ein Funktionsaufruf (analog Prozeduraufruf) wird ausgewertet, indem die Argumente an Stelle der Parameter im Rumpf eirlgefiigt werden i~ridder so entsteheride Aiisdrilck a.usgewertet wird: f ac (4) = (if 4 > 0 then 4*f ac(4-1) else 1) = 4*f ac (3) = 4*Cif 3 > 0 then 3*fac(3-1) else 1) = 4*3*f ac(2) = 4*3*(if 2>0 then 2*fac(2-1) else 1) = 4*3*2*f ac (I) = 4*3*2*Cif 1>0 then l*fac(l-1) else 1) = 4*3*2*1*f ac(0) = 4*3*2*1*(if 0 > 0 then O*fac(O-I) else 1) = 4*3*2*1*1
// // // // // // // // // //
Ei~~,setze.ri, Auswerten Ein,setzen Auswerten Einsetzen Auswerten Einsetzen, Auswerten, Einsetzen Auswerten
Zwei wichtige Dinge lassrn sich hier deutlich erkennen: Rekursion führt dazu, dass der Zyklus „Einsetzen Auswerten" iteriert wird. Die dabei irnrrler wieder auftretenden rleuen Aufrufe der Funktiori/Proxedi~rnennt man Inkarnationen. Offensichtlich k m n es bei schlechter Programmierung - passieren, dass dieser Prozess nie endet: Da.nri haben wir ein nichttemviniereßdes Proyrnrnm geschrieben. Uni das zu verhindern, müssen wir sicherstellen, dass die Argumente bei jeder Irlkarriation „kleineru werden und dass diese Verkleinerung nicht I-~eliebiglange stattfinden kmn. Wenn wir uns die vorletzte Zeile arischen, danri konnnt dort in1 thenZweig der Ai~sdrilck0-1 vor. Wenn wir die Fakultät, wie in der Ma.therna.tik üblich, über den natürlichen Zahlen berechnen wollen, d m n ist diese Subtraktion nicht definiert! -
-
94
6 Rekursion
Hier kommt eine wichtige Eigenschaft der Fallunterscheidung zum Tragen: Der then-Zweig wird nur ausgewertet, wenn die Bedingung wahr ist; ansonsten wird er ignoriert. (Analoges gilt natürlich fiir den ehe-Zweig.) Man kann sich den Prozess bildlich auch so vorstellen wie in Abb. 6.2 skizziert. Wir hatten in Abschnitt 3.2 gesellen, dass wir lokale Variablen und
Abb. 6.2. Illustration des Rekursionsrnechanismus
Pa.ra.meter als „Slots" auffa.sscn können, die zur jeweiligen Inkarnation der Methode gehören. Bei rekiirsiven Methoden ist jeweils nur die „obersteLL Inkarnation aktiv. Alle Berechniingen betreffen nur ihre Slots, die der anderen Inkarnationen bleiben davon unberührt. Wenri eine Inkarnation abgearbeitet ist, wird die darimterliegende aktiv. Deren Slots Paranieter und lokale Varia.blen sind unverändert geblieben. Damit sieht man den wesentlichen Unterschied zwischen den lokalen Variablen und den Attribi~tvariableiider Klasse (bzw. des Objekts). Wenri eine Inkarnation solche Attributvariablen verändert, dann sind diese Äriderurigen über ihr Ende hinaus wirksam. Die dariniterliegende Inkarnation arbeitet deshalb mit den modifizierten Werten weiter. Das kann, je nach Aufgabenstellung, erwünscht odcr fatal sein. -
Übung 6.2. Man programmiere die „Türme von Hanoi" in
--
JAVA.
(a) Ausgabe ist die Folge der Züge. (b) Ausgabe ist die Folge der Turm-Konfigurationen (in einer geeigneten grafischen Darstellung).
Teil I11
Eine Sammlung von Algorithmen
Bisher hubesn wir vor allem Sprachkonzepte vorgestellt und sie m i t winzigen Program7nfrugrnenten illustriert. .letz1 isl es a n der Zeit, etwas gröfiere und ~iollständigeProyr.amrne zu betrachten. W i r beginnen zunächst m i t kleineren Beispielalgorithmen. Anhand dieser Algorithmen führen wir auch m,etr%odischeKonzepte ein, die z u m Programmieren ebenso dazz~gehiiren,wie der eigentliche Prograrnrncode. ( W i r rwiirden gerne v o n Methoden des Software Engimering sprechen, aber dazu. sind die Programme i m m e r noch zu klein.) Danach wenden wir. un,s zwei grojlen Komplenien der Programm,ierz~ngzu. Der erste betrifft klassische Informatikproblerne, nämlich Suchen und Sortieren ( i n Arraqs). Der zweite befu,sst sich m,it cher ingen,ieurmäjligen, Fragestellungen, nämlich der Implementierung rl,urnerischer Berechnz~ngen.
Aspekte der Programmiermethodik
„If the code und the cornrnents disagree, then both are probably wrong. " Norrr~Schryer, Bell Labs
Die meisten der bisherigen Programme wa.ren winzig klein, weil sie niir den Zweck hatten, jeweils ein bestirrirntes Sprachkonstriikt zu illustrieren. Jetzt betrachten wir erstmals Programme, bei denen es uni die Lösung einer gegebenen Aufgabe geht. (So richtig groß sind die Programme allerdings noch immer nicht .) Damit begeben wir uns in einen ßcreich, in dem das Programmieren nicht mehr dlein aus dem Schreiben von ein paar Codezeilen in JAVA besteht, sondern als ingenieurmäj3ige Entwicklungsaufgabe begriffen werden muss. Das heißt, neben die Frage ,,Wie formuliere ichs in JAVA'!" treten jetzt noch Fragen wie „Mit welclicr Methodc löse ich die Aufgabe?" und ,,Wie mache ich meine Lösung für anderc nachvollziehbar?" Gerade Letzteres ist in der Praxis essenziell. Denn man schätzt, dass weltweit über 80% der Progran1rriierark)eit nicht in die Eiitwickliing neuer Sofiware gehen, sondern in die Modifikation existierender Softwa.re.
7.1 Man muss sein Tun auch erläutern: Dokumentation „ T h job's not over until the paperwork is done."
Als Erstes rrliissen wir ein iirlgeliebtes, aber wichtiges Thema ansprechen: Dokumentation. Die Bedeutung dieser Aktivität kann gar nicht geriiigencl betollt werden.' Man erinnere sich niir an die Gebrwichsanleitiing seines letzten Ikea-Schrankes oder Videorecorders und halte sich dann vor Augen, iirn wie viel komplcxcr Softwaresgsteine sind!
98
7 Aspekte der Programmiermethodik
Prinzip der Programmierung Jedes Programm muss dokumentiert werden. Ein nicht oder imgeriügend kommentiertes Programm ist genauso schlimm wie ein falsches Programm.
7.1.1 Kommentare Die Minimalanforder~~ngen a.n eine Dokumentation sind Kommentare. Sie stellen den Teil der Dokumentation dar, der in den Progranimtext selbst eingestreut ist. Die verschiedenen Programmiersprachen sehen dafiir leicht imterschiedliche Notationen vor. In JAVA gilt: 0
0
Zeilenkommentare werden mit dem Zeichen / / eingeleitet, das den Rest der Zeile zum Kommentar macht. X = x + l ; // X u m 1 erhöhen (ein ausgesprochen dummer Kommentar!) Blockkommentare werden zwischen die Zeichen /* i u d */ eingeschlossen und können sich übcr beliebig viele Zcilcri erstrecken. /* Dieser Kommentar erstreckt sich über mehrere Zeilen (wenn auch grundlos) */ Übrigens: Iin Gegensatz zu vielen anderen Sprachen dürfen Blockkonimeritare in JAVA nicht geschachtelt werden.
An,merkung: JAVA hat auch noch die Kor~vention,dass ein Blockkoniinentar , der mit / * * beginnt, ein sog. „Dokumeritationskommentar" ist. Da.s heifit , er wird von gewissen Dokumentationswerkzeugen wie j avadoc speziell behandelt. So viel zur äuEseren Form, die JAVA für Kommentare vorschreibt. Viel wichtiger ist der Inhalt, d. h. das, was in die Kornnieiitare hineingeschrieben wird. Auch wenn es dafür natürlich keine formalen Kriterien gibt, liefern die folgenden Faiistregeln wenigstens einen giiten Anhaltspunkt. 1. Für jcdcs Stück Software müssen Autor, Erstellungs- bzw. Ändcrungsdatum sowie ggf. die Version verzeichnet sein. (Auch auf jedem Plan cines Architekten oder Autoingeriieurs sind diese Angaben zu finden.) 2. Bei gröfieren Softwareprodiikten kommen noch die Angaben iiber das Projekt, Teilprojekt etc. hinzu. 3. Die Eiribettung in den Kontext des Gesamtprojekts muss klar sein; das betrifft insbesondere die Schnittstcllc. 0 Welche Rolle spielt die vorliegende Komponente im Gesamtkontext'? Welche Annahmen wcrdcn übcr den Kontext gemacht? 0 Wie kann die gegebene Koniponente aus dem Kontext angesprochen werden'!
7.2 Zusicherungen (Assertions)
99
4. Ein Kommentar muss primär den Zweck des jeweiligen Progranimstiicks beschreiben. 0 Bei einer Klasse muss z. B. allgemein beschrieben werden, welche Aufgabe sie irn Rahmen des Projekts erfüllt. Das wird meistens eine sumarischc, qualitative Skizze ihrer Methoden und Attribute einsclilielieri (a.ber keine Eirizelauflistung) . 0 Bei einem Attribut wird zu sagen sein, welche Rolle sein Inhalt spielt, wozu er dient, ob und in welcher Forni er anderbar ist etc. 0 Bei Methoden gilt das Gleiche: Wozu dienen sie lind wie verhalten sie sich? 5. Neben dem Zweck rnüsscri noch die Annahmen über den Kontext beschrieben werden, insbesondere die Art der Verwendung: 0 Bei Klassen ist wichtig, ob sie nur ein Objekt haben werden oder viele Objekte. 0 Bei Methoden miissen Angaben über Restriktionen enthalten sein (z. B. Argument darf nicht null sein, Za.hlen dürfen nicht zu gro%sein etc.) Bei Attributen können ebenfalls Beschränkungen bzgl. Größe, Änderbarkcit ctc. a~izugcbensein. 6. Manchmal ist auch hilfreich, einen Überblick über die Struktur zu geben. Diese Art von Lesehilfc ist z. B. dann notwendig, wenn mehrere ziisarrlmengehörige Klassen sich über einige Scitcn Progra.mnitcxt erstrecken. Es mag auch riützlich sein, sich einige typische Fchlrr beim Schreiben von Kornrnentareri vor Augen zu halten: 0 0
0
Kommentare sollen knapp und präzise sein, nicht geschwätzig und nebulös. Korrinientare sollen keine offensichtlicllen Banalitäten enthalten, die im Progranim dirrkt sichtbar sind (s. das obige Beispiel bei X = x+l). Das Layout der Kommentare darf nicht das eigentliche Prograrnnl ,,verdecken" oder iirilesbar machen.
7.2 Zusicherungen (Assertions) Ein wichtiges Hilfsmittel fiir die Entwicklung hochwertiger Software sind sog. Z u s i c h e r u n g e n (engl.: a s s e r t i o n ) . Mit ihrer Hilfe 1ä.sst sich sogar die Korrektheit von Programmen mathematisch beweiseri.%llerdings geht die Teclinik der formalen Korrektheitsbeweise weit über den Rahmen dieses Eiriführungsbuches hinaus. Aber auch werin man keine rriatherriatischen Korrekthcitsbeweise plant, sind Assertio~isäi~Eserstnützlich. Die Methode geht ~irspriiriglichauf Ideen von Floyd zuriick. Darauf aufhauend hat Hoare einen formalen Kalkül entwickelt, der heute seinen Na.men trägt. Von Dijkstra. kamcn cinige wichtigc Beiträ.ge fiir die pra.ktisclie Verwendung der Methode hinzu. Einc exzellcntc Bcschrcibung dcs Kalküls und der mit ihm verbundenen Programmiermethodik findet sich in dem Buch von David Gries [21].
7 Aspekte der Programmierrnethodik
100
Wir schreiben Assertions hier als ,,formalisierte KommentareL', die wir durch das Wort ASSERT einleiten (vgl. Programm 7.1). Sie werden vor allem verwandt, um 0
0
Restriktionen fiir die Parameter und globalen Variablen von Methoden anzugeben; an zentralen Programmpurikten wichtige Eigenschaften explizit festzuhalten.
Programm 7.1 Skalarprodukt zweier Vektoren u . V = Cr'1 u i . v, double skalProd ( double [ 1 U, double [ 1 V ) C
// ASSERT u.length = wlength; double s = 0; for (int i = 0; i < u.length; i++)
// ASSERT s CLI~U , s = s + u[il * v[il; )//for -
C
. V.,
return s;
1//slcalProd
Die erste Assertion in Programm 7.1 legt fest, dass die Methode nur mit gleich 1a.ngen Arrays aufgerufen werden darf. Die zweite Assertion beschreibt eine sog. Invariante: Am Beginn jedes Schleifendurchlaufs enthält die Variable s das Skalarprodukt der bisherig verarbeiteten Teilvektoren.
Prinzip der Programmierung: Assertions Assertions sind ein zentrales Hilfsrriittel fiir Korrektheitsarialyse~iund tragen wesentlich zum Verständnis eines Programms bei. Eine Zusiclierung bedeutet, dass das Programm immer, wenn es bei der Ausführiing an der betreffenden Stelle ist, die angegebene Eigcnscha.ft erfüllt. Bei Methoden liefern Assertions ein Hilfsmittel, rnit dem Korrektheitsanalysen modularisiert werden können. 0
0
Eine Zusicherung über die Paxameter (und globalen Variablen) einer Methode erlaubt, lokal iririerhalb der Methode eine Korrektheitsanalyse durchzuführen. An den Aufrufstellen der Methode braucht man nur noch zu prüfen, ob die Zusicherung eingehalten ist ohne den Code selbst studieren zu müssen. -
Anmerkung: Mari spricht bei dieser modularisierten Korrektheitsana1,yse auch von der Rel.y/G~~,nrantec-Methode:Wcnn in der Umgebung - also ari den Aufrufstel-
7.2 Ziisicheriingeri (Assertions)
101
len die Anforderungen an die Parameter eingehalten werden, dann liefert die Methode garantiert ein korrektes Ergebnis. -
Da wir Assertions als reine Kommentare behandeln, können wir alle Arten der Forniidierung verwenden, von reiner Umgangssprache bis zu fornialer Mathematik. Aber die Idee von Assertions als Basis für Korrektheitsanalysen legt natürlich nahe, einen weitgehend formalisierten Stil zu verwenden. In JAVA 1.4 wurde als rieues Schlüsselwort assert aufgenommen.%an kann also z. B. schreiben int binom ( int n, int k ) ( assert n >= k
... )//binom Normalerweise wird das vorn .JAVA-Systemals Kommentar behandelt, also genauso wie unser //ASSERT rL > k . Wenn man jedoch das Programm in der Form java -enableassertionsMyProg
startet, da.nn wird bei jedem Aufruf von binom auch bei den rekursiven! die Bedingung n >= k getestet. Falls sie erfüllt ist (was eigentlich immer der Fall sein sollte), geschieht nichts. Falls sie verletzt ist, wird ein sog. AssertionError ausgelöst. Damit k a m rrian sehr gut gewisse Kontrollen in die Software einba.uen und sie nach dem Ende der Testphase einfach abschalten. Aber das Verfa,lireri hat auch gravierende Nachteile: Die Asscrtioris werden zum rcirieri Testinstrument, während sie ursprünglich für formale Korrektheitsanulysen gedacht waren. Schlimmer wiegt aber, dass man nur Ausdrücke angeben kann, die sclbst wieder ausführbares .JAVA sind. Gerade bei Assertions ist a.ber wichtig, da.ss man die ganze Rilächtigkeit der Mathematik (und der Fachsprache der jeweiligen Applikation, also Aerodynarnik, Graphtlieorie, Steuerrecht etc.) zur Verfügung hat. Und nicht zuletzt gibt es das subtile Problem, dass man in die Assertions selbst nicht neue Programmierfehler einbauen darf. Deshalb werden wir die assert-Anweisung von JAVA ignorieren urid lieber mit Kommentaren dcr Art //ASSERT . . . arbeiten. -
-
7.2.1 Allgemeine Dokumentation „If you can't write it down in, English, you can't code it." (Peter Hdpern)
Kornrnentare informelle ebenso wie forrrde können nur Dinge beschreiben, die sich unniittelbar auf eine oder höchstens einige wenige Codezeilen beziehen. Eine ordentliche Dokinnentation verlangt aber auch, dass man globale Aiissa.gcn über die generelle Lösungsidee urid ihre ingenieurtechriische -
-
Wenn man es benutzen will, muss der Compiler mit der entsprechenden Option aufgerufen werden, also in der Form javac -sourCe 1.4 «Datei„.
7 Aspekte der Programmiermethodik
102
Umsetzung macht. Tm Rahmen dieses Buches beschränken wir das auf vier zentrale Aspekte: 0
0
Wir geben jeweils eine Spezifikation der Aufgabe an, indern wir sagen, was gegeben und gesucht ist und welche R.andbedingungeri zu beachten sind. Danach I-)eschreiben wir informell die Lösungsmethode, die in den1 Programm verwendet wird. Dazu gehören ggf. auch Angaben über Klassen und Methoden, die man von a.ndcrcn Stellen „importiert“. Ziir Abrundung erfolgt dann die Evaluation der Lösung, das heift: eirie Aufwandsabschätzung (s. unten, Abschnitt 7.3) lind eine Analyse der relemriten Testfälle. Zuletzt diskutieren wir ggf. noch Variationen der Aufgabcnstcllung oder mögliche alternative Lösurigsansätze. -
0
Fiir diese Beschreibungen ist alles zulässig, was den Zweck erfüllt. Textuelle Erlauterurigen in Deutsch (oder Englisch) sind ebenso miiglich wie Diagrarrirne gute und mathematische Formeln. Und manchmal wird auch sog. Pse~~docode Dienste tun. In den meisten Fällen wird man eirie Mischung aus mehreren dieser Besclireiburigsrriittel verwenden. Anmerkung.: Diese Art von Beschreibung entspricht in weiten Zügen dem, was in der Literatur iri neuerer Zeit (inter dem Schlagwort Design Patterns [18, 341 Furore macht. Der wesentliche Unterschied ist, dass bei Design Patterns die Einhaltung einer strengeren Form gefordert wird, als wir das hier tun.
7.3 Aufwand Bei jedem Ingenieurprodukt stellt sich die Frage der Kosten. Was nützt das eleganteste Programm, wenn es seine Ergebnisse erst nach einigen Tauscnd oder gar Millionen Jahren liefert? (Vor allem, wenn da.nn nur 42 heraiiskomrrt.) Eine Aufwandsbestimmung bis auf die einzelne Mikrosekunde ist in der Praxis weder möglich noch notwer~dig.~ Die Frage, ob ein bestimmter Rechcnschritt fünf oder fünfzig Maschineninstruktionen braucht ist bei der Geschwindigkeit heutiger Rechner nicht mehr besonders rclevarit. Im Allgemeinen braucht man cigentlich nur zu wissen, wie da.s Programm a.iif doppelt, dreimal, zehnmal, tausendmal so grofie Eingabe reagiert. Das heifit, man stellt sich Fragen wie: ,,Wenn ich zehnma.1 so viel Eingabe habe, werde ich dann zehnmal so lange warten müssen?" "ie Ausnahme sind gewisse, sehr spezielle Steuersysteme bei extreri~zeitkritischen teclinischen Anwendungen wie z. B. die Auslösung eines Airbags oder eine elekt,roriische Brnzineiris~>rit,xiing. Bei solchen Aufgabcn muss Inan U.U. tatsächlich jede einzelne Maschineninstruktion akribisch zählen, um sicherzustellen, dass inan irn Zeitraster bleibt. (Aber auch hier wird das Problem mit zunehmender Geschwindigkeit der verfügbaren Hardware immer weniger kritisch.)
7.3 Aufwand
103
Diese Art von Feststellungen wird in der sog. „Big-Oh-Notation" formuliert. Dabei ist z. B. O ( n 9 zu lesen als: ,,Wenn die Eingabe die Größe n hat, dann liegt der Arbeitsaufwand in der GröJenor~d;n,un,q n2." Und es spielt keine Rolle, ob der Aufwand tatsächlich 5 n h d e r 50n"eträgt. Das lieiBt, konstante Faktoren werden einfach ignoriert.
Definition (Aufwand) Der Aufwand eines Programms (auch K o s t e n genannt) ist der Bedarf an Ressourcen, den seine Abläufe verursachen. Dabei kann man den maximalen Aufwand oder den diirchschnittlichen Aufwand txtrachteri. AuRerdern wird unterschieden in -
-
Zeitaufwand, also Anzahl der ausgeführten Einxelschritte, und Platzaufwand, also Bedarf an Speicherplatz.
Der Aufwand wird in Abhängigkeit von der GröBe N dcr Eingabedaten gemessen. Er wird allerdings nur als Gröjlenordnung angegeben in der . . ). 5g Notation 0(. Für gewisse standardinägige Kostenfunktioncn hat man eine gute intuitive Vorstellung von ihrer Bedeutimg. In Tabelle 7.1 sind die wichtigsten dieser Standardfuriktiorier~aufgelistet. Name konstant logarithmisch linear „n log n" quadratisch kubisch polynomial exponentiell
Intuition: Tausendfache Eingabe hezflt . . . . . . gleiche Arbeit O(c) O(1og n ) . . . nur zehnfache Arbcit . . . auch t,ausendfa.che Arbeit o(n) O ( n log n ) . . . zehntausendfache Arbeit . . . millionenfache Arbeit O(n9 . . . milliardenfache Arbeit O(n3) . . . gigantisch viel Arbeit (für groRes C ) O(nr) . . . hoffnungslos O(2")
Kürzel
Tabelle 7.1. StandardmäEsige Kostenfunktionen
Tabelle 7.2 illustriert, weshalb Algorithmen mit exponentiellen1 Aufwand a priori iinbrai~chbarsind: Wenn wir um des Beispiels willen von Einzelscliritten ausgehen, bei denen die Ausführung eine Mikrosekuride dauert, dann ist zum Beispiel bei cincr winzigen Eingabcgrößc n = 40 selbst bei kubischem Wachstim der Aufwand noch unter einer Zehntelsekiinde, während irn exponentiellen Fall der Rechner bereits zwei Wochen lang arbeiten niuss. -
104
7 Aspekte der Programmiermethodik
Und schon bei etwas über 50 Eingabedaten reicht die Lebenserwartung eines Merlschen nicht mehr aus, um das Resultat noch zu erleben." quadratisch
linear
TL
1
1
10 20
10 1 , s
100
20
I,S
41111l i i
30
:$(J
iLs
!JIIO
40 50 60
G'S
I,h
WS
exponentiell 2
1 &'L;
1 sec 18 min
27 ins
13 Tage
b
2 rris
G 4 ms
:J, Ins
125 I I ~ S
4 ins
210:
60 ,,s
100
100 I , s
1000
I 111s
10 m s
1 sec
/L%
1 ins
1 rns
8 ins
~ L S
10 I' 50
1 F"
kubisch
ills
36 Jahre 36 560 Jahre
1 sec
4 - 1016 Jahre
17 min
...
Tabelle 7.2. Wachstum von exponentiellen Algorithmen
Das folgende kleine Beispiel zeigt, wie leicht man exponentielle Programme schreiben k a ~ m .Dic Kaninchcnvcrmehrung nach Fibonacci (vgl. Programm 6.3) kann auch wie in Programm 7.2 geschrieben werden.
Programm 7.2 Die Fibonacci-Funktion int fib ( int n ) ( if (n == 0 I n == 1) C r e t u r n 1; 3 else C r e t u r n f ib(n-1) + f i b h - 2 ) ;
3 //~f 3/ / f i 6
Urn eine Vorstellung vom Aufwand dieser Methode zu bekommen, illustrieren wir die Aufrufe grafisch:
fih(1) fib(0)
w a s Altcr des Universiims wird auf ca. 10'" Jalirc geschätzt.
7.3 Aufwand
105
Man sieht, dass man einen sog. B a u m von Aufrufen erhält. Das heiRt (zwar nicht immer, aber) in sehr vielen Fällen, dass man es mit einem exponentiellen Programmaiifwarid zu tim hat. Folgende back-of-the-envelope-Recliniing bestätigt diesen Verdacht: Sei A(n)der Aufwand, den f i b ( n ) verursacht. Dann können wir aufgrund der Rekursionsstruktur von Programm 7.2 folgende ganz grobe Abscliätziing machen: A(n) N A(n - 1 ) A(n 2) A(n 2) A(n - 2) = 2 . A(n - 2) = 2 . 2 . . . 2 = 2S N 0(2?~) Obwohl wir bei der Ersetzung von A(n - 1) durch A(n 2) sehr viel Bcreclinungsaiifwand ignoriert haben, hat der Rest immer noch exponentiellen Aufwand. Urid das gilt dann erst recht fiir das vollständige Prograrnrn.
+
-
>
-
+
-
Unglücklicherweise sind zahlreiche wichtige Aufgaben in der Informatik vorn Prinzip her exponentiell, sodass man sich mit heuristischen Näherungslösungen begnügen muss. Dazu gehören nicht nur Klassiker wie das Scllaclispiel, sondcrri auch alle möglichen Arten vori Optirriieriirigsaiifgaheri in Wirtschaft und Technik. Anmerkung: Dic Aufwmds- oder Kostenanal,yse, wie wir sie hier betrachten, ist zu unterschcidcn von cincm vcrwandtcn Gebiet der Theoretischen Informatik, der sog. K o m p l e x i t ä t s t h e o r i e . Während wir die Frage analysieren, welchen Aufwand ein konkrct gegebcncs Programm niadit, wird in der Ko~nplexitätstheorieuntersucht, mit welchem Aufwand ein bestimmtes Problem gelöst werden kann. Das heilt, man argumentiert hier über alle denldmren Programme, die ,geschriebenen ebenso wie die noch ungeschrieberien. (Das klingt ein bisschen nach Zauberei, hat aber eine wohlhndierte mathematische Basis 124, 411.)
Damit können wir einen wichtigen MaBstab für die Qiia.lität vori Algorithnien forrnuliereri. Definition: Ein Algorithrniw ist efizienter als ein anderer Algorithmus, wenn er dieselbe Aufgabe niit weniger Aufwand löst. Ein Algorithmus heißt efizient, wenn er weniger Aufwand braucht als alle anderen bekannten Lösungen für dasselbe Problem, oder wenn er dem (aus der Koniplexitätstlieorie bekannten) tlieoretiscli rriöglicheri Miriinialaiifwand nahe kornrnt. CJI! Dieser Begriff der Effizienz ist zu untersclieideii von einerr~anderen Begriff: Definition: Ein Algorithrnus ist effektiv, w e m die zur Verfügung stehenden Ressourcen an Zeit und Platz zu seiner Ausführung ausreichen. Q
Beispiel. Die Zerlegung einer Zahl in ihre PrimJakloren hat eine einfache mathematische Lösung. Aber alle zurzeit bek:ann,tert, Ver:fahran sind e:yonen,tiell. Desh,alb sind z. B. Zahlen mit 200 D e ~ i m a ~ b t e l l enicht n eflektiu faktorisierbar. ( D a v o n leben alle gängigen Verschlüsselungsue~fahren.)
106
7 Aspekte der Programmiermethodik
7.4 Beispiel: Mittelwert und Standardabweichung Ein klassischer Problenikreis, bei dem Arrays benutzt werden, ist die Analyse von Messwerten. Das folgende Prograninifragnierit liefert Methoden zur Ermittlung des Mittelwerts M und der S t r e ~ ~ u nSg (auch Standardabweichung genannt) einer Folge von Messwertcn.
Aufgabe: Mittelwert, Strevuny Gegeben: Eine Folge von Mcsswerten zi, . . . , X„. Gesucht: Dcr Mittelwcrt M und die Streuung S:
Voraussetzung: Die Liste der Messwerte darf nicht leer sein.
Methode: Das Programm lässt sich dilrch einfache Schleifen realisieren. Die entsprechenden Methoden sind in Progranirn 7 . 3 angegeben.
Programm 7.3 Mittelwert und Streuung class Statistik ( double mittelwert (double [ I a ) ( //ASSERT a nicht leer double s = 0; for (int j=O; j//for i f (j>last) (return true;) else Creturn f a l s e ; )
1
// prüfe alle // teilbar? // alle iiberstanden // war tezlbar
I// ercd of filter p r i v a t e boolean t e i l t ( i n t X, i n t y) r e t u r n (y % X == 0 ) ;
C // Rest bei Dzuision
1 // end o,f teilt 1 // end o,f dass Primzahden Hinweis: Die if-Anweisung in der Methode f i l t e r könnte auch elcgariter als r e t u r n ( j > l a s t ) geschrieben werden. Aber aus Dokumentationsgründen haben wir die iimständlichc Form gewählt (die der Compiler ohnehin generieren würde).
ganzen, neu kreierten Array als Ergebnis liefert. Ein Aufruf der Funktion primes kann also folgendermagen aussehen: Primzahlen p = new Primzahlen0 ; // Objekt kreieren int [ I f irstHundredPrimes = p.primes(100) ;// Methode ausführen
Weil in JAVA nichts geschehen kann ohne ein Objekt, das CS t u t , müsscn wir zunächst cin Objekt p kreieren, von dem wir dann die Methode primes aiisfiihren lassen, um den Array mit dem schönen Namen f irstHundredPrimes zu generieren. Weil das Objekt abcr unwichtig ist, können wir es auch anonym lassen. Das sieht dann so aus:
112
7 Aspekte der Programmiermethodik
int[] firstHundredPrimes
=
(new ~rimzahlen~)).primes(100);
Der Aufwand dieser Funktion kann nicht angegeben werden, weil wir keine mathematischen Aussagen darüber besitzen, wie vielc Zahlen durch den Filter fallen. Anmerkung: Viele Verschlüsselungsverfahren basieren auf groXen Primzahlen (100-200 Dezimalstcllcn). Für diese Verfahren ist es essenziell, dass bis heute noch niemand eine Methode gefunden hat, um eine Zahl effizient in ihre Primfa,ktoren zu zerlegen. Das ist ein Beispiel dafür, dass es manchmal auch nützlich sein kann, keine effizienteLösung für ein Problem zu haben.
7.7 Beispiel: Zinsrechnung Als letztes dicscr Beispiele wollcn wir ein vollständiges Programm betrachten, also die eigentliche Rechnung inklusive der notwendigen Ein-/Ausgabe. Jemand habe ein Darlehen D genommen und einen iesten jährlichen Zins von p% vereinbart. AuBcrdem wird arn Ende jedes Jahres (nach der Berechnung des Zinses) ein fester Betrag R zurückbezahlt. Wir wollen den Riickzah1urigsverla.uf darstellen.
Aufgabe: Gegeben: Darlehen D; Zinssatz p%; Rate R. Gesuch,t: Verlauf der Rückzahlung. Voraussetzung: ,,Plausible Werte" (Zinssatz zwischen 0% und 10%; Darlchcn und Rate > 0; Rate gröKer als Zins). Diese Plausibilitätskontrollen sollen explizit durchgeführt werden.
Methode: Wir trcnnen die Methoden zur Datenerfassung von den eigentlichen Berechnungen. Die Berechnungen erfolgcn in einer einfachen Schleife, in der wir den Ablauf in der realen Welt jahresweise simulieren. Als Rahmen für unser Progrannn haben wir irn Prinzip wieder zwei Objekte, nänilich das eigentliche Programm und das Tcrrninal. Das führt zu der Architektur von Abb. 7.2 ZinsProgramm
Terminal
Abb. 7.2. Architektur des Zinsprogramms
7.7 Beispiel: Zinsrechnung
113
Diese Architektur führt zu dern Programrnrahrnen 7.6. Wie üblich wird im Hauptprogramni main nirr ein Hilfsobjekt z kreiert, dcssen Methode zins 0 die eigentliche Arbeit iibernimrrit. Urri eine klare Struktur zu erhalten, fassen wir die logischen Teilaufgaben der Methode zins () jcwcils in cntsprcchende Hilfsmcthodcn einlesen0 und darlehensverlauf0 zusammen. Da es sich dabei iirn zwei Hilfsrnethoden handelt, werden sie als private gckcnnzeichnet. Programm 7.6 Das Protrramm ZinsProaramm public class ZinsProgramm
C
public static void main (StringCI args) Zins z = neu Zins0 ; z.zins 0 ; 1//rr~arn, ) // end of class ZinsProgramm class Zins private private private private private private
C
C
// // // // // // //
int darlehen; int schuld; int rate ; int zahlung = 0; double q ; int jahr = 0 ;
Hilfsklasse m f ä n g l i c h e s Darlehen aktuelle Schuld ,r/ere'&barte Riickzahkur~gsratr. umfgelaufer~oGescxr1,tzc~hl71~ri,g Z i r ~ s s a t z(z. B . 5.75% als 1.0575) Zäh.ler für die Jahre
void zins 0 C Terminal.println(" \ nDarlehensverlauf \ n") ; einlesen0 ; // ( p l a ~ ~ s i b l eWerte ) heschaff'en darlehensverlauf 0 ; // & ~ eeigentliche Berechnung 1 // zins private void einlesen 0
C
// s. Pro,qram,m. 7 . 7
.. .
1 // einlesen, private void darlehensverlauf
0C
// s. P r o g r m r r ~7.8
...
1 // end of class Z i n s
Die Klassc Zins sicht alle relevanten Daten als (private) Attribiite vor. Diese werden uninitialisiert definiert, weil sie bei der Programrriaiisführung jeweils aktiiell vom Benutzer erfra,gt wcrdcn rnüssen. Beini Einlesen der Daten wollen wir -- im Gegensatz zu unseren bisherigen Einführungsbeispieleri auch PZausibilitätsbontroZZenniit cinbauen. Denn die Berechnung rriaclit nur Sinn, wenn ein cchtcs Darlehen und echte R.iickzah-
114
7 Aspekte der Programmiermethodik
lungsraten angenommen werden. Und der Zinssatz muss natiirlich zwischen 0% und 100% liegen (anständigerweise sogar zwischen 0% und 10%)
I
P r i n z i p der P r o g r a m m i e r u n g : Plausibilitütskont7ollen
Wie man in Programm 7.7 sieht, erfordern solche Plausibilitätskontrollen einen ganz erheblichen Programmieraufwarid (irn Allgemeinen zwar nicht iritellektuell herausfordernd, aber fa.st immer länglich). P r o g r a m m 7.7Das Prograrnrn ZinsProgramm:Die Eingaberoutine private void einlesen ( while (true) ; this . darlehen = Terminal.askInt ( ' I \ nDarlehen = if (this .darlehen > 0) C break; ) Terminal.print("\007Nur echte Darlehen!"); }// u~hile double p = -1; // Zinssatz in Prozent while (true) C p = Terminal.askDouble ( " \ nzinssatz = I ' ) ; if (p >= 0 & p < 10) ( this . q = 1 + (p/100) ; // Zir~ssatzz.B. 1.0575 break; ) Terminal.print("\007Muss im Bereich 0 . . 10 liegen! ' I ) ; I')
}// while while (true) ( this.rate = Terminal.ask~nt("\n~ückzahlungsrate= "1; if (this .rate > 0 ) break; ) Terminal .print("\007Nur echte Raten! 'I) ;
)// while )// einlesen
Wir miissen uni jede Eingabcaufforderung eine Schleife lierurnbauen, in der wir so lange verweilen, bis die Eingabe den Plausibilitälstcst besteht. Bei fehlerhafter Eingabe muss natürlich ein Hinweis an den Benutzer erfolgen, wo das Problem steckt. Das ist einer der wenigen Falle, in dencn einc ,,unendlicl~c" Schleife mit while (true) inid break akzeptabel ist. Jetzt wenden wir uns der Methode darlehensverlauf0 in Programm 7.8 zu. Zunächst nliissen wir uns die Lösungsidee klarmachen: Wir bezeichnen mit S, den Schuldenstand a.m Ende des Jahres i . Damit gilt dann:
so= D
=q.S,R
mit q = I +
&
7.7 Beispiel: Zinsrechnurig
115
Damit ist die Struktur der eigentlichen Schleife evident. Es gibt allerdir~gs noch eine Reihe von Randbcdingungen zii bcachtcn: 0
Wir müssen verhindern, dass das Prograrnnl unendlich lange Ausgaben produziert, wenn der Zins die Riickzahlung übersteigt. In dieseln Fall wolIcn wir nur den Stand nach dem ersten Jahr und eine entsprechende Warnung ausgeben. Wir müssen beachten, dass die letzte Rückzahlung i. Allg. nicht genau R sein wird.
Programm 7.8 Das Progranim ZinsProgramm:Die Haiiptroiitine private void darlehensverlauf 0 ( this.schuld = this.darlehen; // Anfangsstand umgeben zeigen0 ; int alteschuld =this.schuld; // fCr Wachstumsvergleich, // erstes Jahr berechen jahresschritt 0 ; if (this.schuld > alteschuld) C Terminal.println("\007Zins ist höher als die Raten! ' I ) ; 1 else ( while (this . schuld > 0) C jahresschritt 0;
1 Terminal.println Terminal.println ( " ('I
\ nLaufzeit : + this .jahr + " Jahre") ; \ nGesamtzahlung : + this . zahlung \ n") ; "
"
+I1
1 private void jahresschritt 0 ( this . schuld = (int) (this .schuld * this . q) ; if (this . schuld < this .rate) C
// Cent kappen (Cast)
this . schuld = 0 ;
1 else C this.zahlung = this.zahlung + this.rate; this.schuld =this.schuld - this.rate;
1 this.jahr = this.jahr + 1; zeigen0 ; I / / jahresschritt private void zeigen 0 Terminal .println ( "Schuld am Ende von Jahr )//zeLgen
"
+ this . jahr + " : " + this . schuld) ;
Man sieht in Programm 7.8, dass auch hier die Verwendung weiterer Hilfsmethoden wesentlich fiir die Lesbarkeit ist. In darlehensverlauf0 wird die
7 Aspekte der Programmiermetliodik
116
Hauptschleife zur Berechnung des gesamten Schuldenverlaufs realisiert. Dabei muss das erste Jahr gesondert behandelt werden, u m ggf. den Fehler unendlich wachsender Schulden zii vernieiden. Die Methode j a h r e s s c h r i t t 0 führt die Berechnung am Jahresende also Zirisberechniiiig und Ratenzahlung aus. Dabei muss das letzte Jahr gesondert behandelt werden. Hier benötigen wir zum ersten Mal wirklich Casting, weil wir die Gleitpunktzahl, die bei der Multiplikation mit dem Zir~ssatz entsteht, wieder in eine ganze Zahl vcrwandcln müssen. Wcil die Ausgabe des aktuellen Standes an niehr als einer Stelle im Prograrilrri vorkorrinlt, wird sie in eine Methode zeigen () eingepa.ckt. In diesem Progranirn wird grundsätzlich das Schlüsselwort t h i s verwendet, wenn auf Klassenattribute zugegriffen wird. Das ist zwar von1 Compiler nicht gefordert, aber es erhöht den Dokiirrieritationswert.
>
Übung 7.3. Es gibt die These, dass die Schulden am Ende von Jahr i (z 1) sich auch mit einer geschlossenen Formel direkt berechnen lassen. Für diese Formel liegen drei Vermutungen vor (mit q = 1 f &):
S,=D.ql-R.q- 1 S - n . q " l R . ~ 7
q--1
S,=D.qZ-R.k cl-1
Man überprüfe ,,experimentell" (also durch Simulation am Computer), welche der drei Hypothesen in frage kommt. (Für diese müsste dann noch ein Induktionsbeweis erbracht werden, um Gewissheit zu haben). Übung 7.4. Statt den Darlehensverlauf als lange Zahlenkolonne auszugeben, kann man ihn auch grafisch anzeigen. Das könnte etwa folgendermaßen aussehen: Schuld
I
Jahre
Die Punkte muss man m i t drawDot (x,y) zeichnen (s. das Objekt Pad in Abb. 4.3 von Abschnitt 4.3.7). Das Hauptproblem ist dabei sicher, die Größe des Fensters (dargestellt durch ein ~ a d - O b ~ e kund t ) die Achsen abhängig von den Eingabewerten richtig zu skalieren (Hinweis: Bei der x-Achse - also den Jahren - könnte man eine konstante Skalierung vornehmen, die spätestens bei 100 Jahren aufhört.) Übung 7.5. Man verwende die lllustrationstechnik aus der vorigen Aufgabe, um die obigen Tests der Hypothesen grafisch darzustellen. Übung 7.6. Man gebe tabellarisch die Zuordnung der Temperaturen -20" den entsprechenden Windchill- Tempera turen aus (vgl. Aufg. 5.1). Variation: Man gebe die Temperaturen jeweils auch in Fahrenheit an.
...
I o zu
Suchen und Sortieren
W e r die Ordnung liebt, ist ,nwr ZU f a d zwm Suchen. (Sprichwort)
Zu den Standardaufgaben in der Informatik gehören das Suchen von Elemcnten in Datenstruktiiren und als Vorbereitimg dazu das Sortieren von Datenstriikturen. Die Bedeutung des Sortierens als Voraiissetzmg für das Suchen kann man sich a.n ga.nz einfachen Beispielen vor Augen führen: -
0
0
-
Man versuche im Bcrlincr Telefonbuch einen Teilnehmer zu finden, von den1 man nicht dcn Namcn, sondern nur die Telefonnummer hat! Die Rechtschreibung eines Wortes klärt Inan besser mithilfe eines Dudcns als durch Suche in diversen Tageszeitungen.
Es ist verblüffend, wie oft Suchen und Sortieren als Bestandteile zur L& sung umfassenderer Probleme gebraucht werden. Das Thema stellt sich dabei meist in leicht unterschiedlichen Varianten, je nachdem, was für Datenstruktiiren vorlicgen. Wir betrachten hier Prototypen dieser Prograrnnie für unsere bisher cinzige Datenstruktur: Arrays.
8.1 Ordnung ist die halbe Suche Wenn die Gegenstä.ndc keine Ordnung besitzen, dann hilft beim Suchen nur noch die British,-Museum, Method: Ma.n schaut sich alle Elemente der Reihe nach an, bis ma.n das gewünsc:hte entdeckt hat (sofern es iiberhaiipt vorhanden ist). Efizientes Suchen hängt davon ab, ob die Elemente ,,sortiertKsind und zum Begriff der Sortiertheit gehört zwingend, dass auf den Elenieriten eine Ordnung cxistiert. Diese Ordnung wird in der Mathematik üblicherweise als „= 0 ) ;
C
1//has public int find ( long [ I a, long int low = 0 ; int high = a.length- 1 ; int med; int index = -1 ; while (low m) C break; ) // b/i..m] komplett übertragen 1//i,f )//for if (aFrom > j) C / / R e s t von b i U System.arraycopy(b, bFrom, a, to, m-bFrom+l);
1/ / i f )//merge )//end of class Mergesort
134
8 Suchen und Sortieren
der Daten enthält. (Andernfalls würden i. Allg. seine Elemente von denen in b iiberschrieben werden.) Programm 8.6 eritllält den vollständigen Code. Die Methode s o r t gencriert ziniächst einen Hilfsarray b gleicher Längc und ruft dann die zentrale Hilfsmcthode m s o r t auf. Die Methodc m s o r t sortiert einen Teilarray a [ i . .jl unter Verwendung eines Hilfsarrays b, genauer des Teilarrays b Ci. .j l . Das Ergebnis wird im Teila.rray a Ci. .jl abgelegt. Der Teilarray b [i . .j] hat an1 Ende der Methode einen nicht hestinirriharen Inhalt. Für einelenientige Arrays ist nichts zu tun, bei zweielenientigen Arrays ist höchstens ein swap nötig. Zum Kopieren der vorderen Hälfte von a nach b verwenden wir die in JAVA vordefinierte Methode a r r a y c o p y (s. Abschnitt 5.5). Bcirn Zusa.rnmcnrnischen in der Methode merge ist wichtig, dass die untere Hälfte des Zielarrays a nicht besetzt ist. Denn sonst würden i. Allg. einige Elernerite von a durch Elernerite von b überschrieben. AuBerderri muss rnari bei gleichen Elementcn jeweils zuerst dic aus b nehmen, um Stabilität zu garantieren. Wenn der Teilarray als Erster vollstäridig übertragen ist, muss der Rest von b noch nach a kopiert werden (sortiert ist er ja schon). Falls b zuerst fertig ist, kann man aufhören, weil dann die restlichen Elernente von a schon korrekt positioniert sind.
Evaluation: Autwand: Das Verfahren Ilal, den Aufwand O ( N log N ) Dieser Aiifwand wird jetzt sogar immer garantiert, d a bei der Zerlegung grundsätzlich die Längen der Arrays halbiert werden. (Der Rumpf der Methode enthält aber mehr Operationen als der von Quicksort, weshalb Quicksort im Durchschnitt etwas schneller ist.) Eigenschaften: 0 Das Verfahren ist stabil. Das Verfahren arbeitet nicht in situ. Standardtests: Leerer, eiri-, zweielenieritiger (Teil-)Array. Alle Elemente links sind kleiner/größer als alle Elemente rechts. -
-
Hinmieis: Die Idee des Mergesorts kann auch benutzt werden, um grolSe Plattendateien zu sortieren, die nicht in den Ha.uptspeicher passen. Da.nn zerlegt man die Datei in Fragmente passender GröBe, sortiert diese jeweils irn Hauptspeicher (geht viel schneller!) und misclit dann die Fragmente zusanmen. 8.3.5 Heapsort
Beim Heapsort wird der Aufwand zwischen der Zerleginig und dem Zissamrnenbauen gleichmäfiig aufgeteilt. Zwar findet hier wic beim Quicksort in der Zerlegungsphase eine teilweise Vorsortieriing statt. Irn Gegensa.tz zum Quicksort trennt die Vorsortierung die Elernerite a.t)er nicht so schön in ,,links die
8.3 Wer sortiert, findet schneller
135
kleinen" und „rechts die großen", sondern nimmt eine schwächere Anordnung vor, sodass beim Zusammenfügen immer noch etwas Arbeit bleibt. Die Motiaation, für die Vorsortierung des Heapsorts kommt aus deni Selection sort (s. Abschnitt 8.3.1): Dort ist der zeita.ufwendige Teilprozess die Suche nach dem Minimum/Maximum des wcißcn Bereiches. Wcnn es gelingt, diese Suche schnell zu machen, dann ist der ganze Sortierprozess wesentlich beschleunigt. Und geriau das macht Heapsort. Das Verfahren ist korueptuell ein bischen schwieriger zii verstehen, hat aber gegenüber Quicksort und Mergesort gewisse Vorteile: Statistische Messungen zeigen, dass das Verfahren im Mittel etwas langsanier ist als Quicksort (allerdings nur iirn einen konstanten Faktor). DaSür ist es aber wie auch Mergesort irr1 *u~orstcase immer noch gleich schnell, nänilich O ( N log N). Im Gegensatz zum Mergcsort arbcitct da.s Verfahren aber i n situ. -
-
Methode: 2-Phasen-Prozess Da.s Verfahren arbeitet in 2 Phasen: In Phase 1 wird a.us dcni ungeordnctcn Array ein teilweise vorgcordneter Heap. In Phase 2 wird aus dem Heap dann ein vollständig sortierter Array.
-
-
Wenn wir den Hcapsort von vornhcrcin auf Arrays bcschrcibcn wollten, dann niüssteri wir Bilder der folgenden Bauart malen:
Das ist offensi=l; i--) // A S S E R T heide Unterbäum,e von i sind Heaps sinkCa, i, N) ; 3 / / for }// array ToHeap private void sink ( long [ I a, int i, int N ) C while (i N ) C j = left (i) ; 3 else if (a[node(left(i))l >= a[node(right(i))]) C j = left(i); 1 else C j = right (1) ; } / / i f if ( a [node (j)] < a[node (i)] ) .E break; 1 // Ziel erreich,t swap(a, node(i), nodecj));
private void heapToArray ( longC] a ) final int N = a .length ; for (int j=N; j>=2; j--) 1 // A S S E R T all. .jl ist ein Heap swap(a, node(l), node(j)); sink(a, 1, J-1); 1// ,for 3 / / heap ToArray
// Phase 2
// tausche Wurzel ++ letztes Element // Beinahe-Heap reparieren
private int node (int i) C return i-I; 1 private int left (int i) C return 2*i; 3 private int right (int i) return 2*i+l; } }//end of dass Heapsort
P M
B
/ \
D
/ \ I
Z
verkürztcr Heap
/ \
M
B
/ \
V
I
Z
verkürzter „Beinahe-Heap"
F
D \
R
V
A
/ \
I
Z
weiter verkürzter Heap
1
8.3 Wer sortiert, findet schneller
139
Als Nächstes wird P mit B vertauscht. Und so weiter. Man beachte, dass wir es jetzt mit verkürzten Heaps zu tun haben, sodass die Operation sink rnit dem jeweils aktuellen Ende j aufgerufen werden miiss.
Evaluation: (Phuse 2) Aufu~and: Diese zweite Phase Ir>eliandelt alle Kr~oten,wobei jeder Knoten von der Wurzel a.us bis zu log N Stufen absinkcn niuss. Insgesamt erhalten wir damit O ( N log N ) Schritte. Verbesserungen. Der Heapsort arbeitet in situ; das rnacht ihn dem Mergesort iiberlegen. Und er gara.ntiert immer O ( N log N ) Schritte; das macht ihn dem Quicksort überlegen, weil der irn worst case auf O ( N 9 schritte ansteigt. W:im der Quicksort jedoch seinen Normalfall mit O ( N log N ) Schritten erreicht, dann ist er schneller als Heapsort, weil er weniger Opera.tioncn pro Schritt braucht. Aber diese Konstante lässt sich im Heapsort noch verbessern. Wir hetrachten nur Phase 2, weil sie die teiire ist. Die Operation sink bra.uchl füni elementare Operationen: zwei Vergleiche (weil man ja den gröBeren der beiden Kindknoteri bestimmen ~nuss)und die drei Operationen von swap. Wir können aber folgende Variation programmieren (illustriert anhand der zweiten der beiden obigen Bilderserien): Das Wurzclclcmcnt V wird nicht mit dcrn letzten Elcrncnt D vertauscht, sondern nur an die letzte Stelle geschriel)en; D wird in einer Hilfsvariablen a.ufbcwahrt. Dann schieben wir der Reihe nach den jeweils gröfiereri der beiden Kiridkrioten nach oben. Unten angekomnlen, wird D aus der Hilfsvaria.blen in dir Liicke geschrieben.
„BeirialirHeapUniit Lücke
verkürzter Heap
,,Beinahe-Heap"
Dieses Verfahren ist rund 60% schneller, weil es pro Schritt nur noch zwei Operationen braucht: einen für die Bestimmung des gröBeren Kindknotens und eine Zuweisung dieses Kindelenlents a.n das Eltcrnclcmcnt. Aber das ist so noch falsch! Wie man an dcrn Bild sieht, kann die Liicke „überschieBenL':Das Element D ist jetzt zu weit unten. Also brauchen wir eine Operation ascend das duale Gegenstück zu sink , rnit deni das Element wieder an die korrekte Position hochsteigen kann. Diese Operation brauclit pro Scliritt einen Vergleich mit dem Elternknoten und die Zuweisung dieses Elterrlelements an den Kindknoten. Wenn die richtige Stcllc erreicht ist, wird der zwischengcspcichcrtc Wert - in unserem Beispiel D eingetragen. Irri statistischen Mittel ist dieses Überschie~enmit a.rischlie$cndem Wiederaufstieg billiger, a.ls während des Abstiegs irnmcr einen zweiten Vergleich -
-
140
8 Suchen und Sortieren
zu machen, weil das Element in unserem Beispiel D i. Allg. sehr klein ist (es kommt ja von einem Blatt) und deshalb gar nicht oder höchstens ein bis zwei Stufen hochsteigen wird. -
-
Übung 8.3. Man programmiere den modifizierten Heapsort.
8.3.6 Mit Mogeln gehts schneller: Bucket sort
Wir ha.bcn gesehen, dass die besten Verfahren nämlich Quicksort, Mergesort und Heapsort - jeweils U ( N log N) Aufwand machen. Diese Abschätzungen sind auch opti~nal:In der Theoretischen Informatik wird bewiesen, dass Sortieren genercll nicht schneller gehen kann als mit U ( N log N) Aufwand. Für den Laien ist es angcsichts dicses Rcsultats verblüffend, wenn er auf einen Algorithmiis stögt, der linear arbeitet, also mit U ( N ) Aufwand. Ein solchcr Algorithnius ist Bucket sort. Dieses Verfahren funktioniert nach folgendem Prinzip: Wir haben einen Array A von Elenie~iteneines Typs a . .Jedes Element besitzt. einen Schliissel (z. B. Postleitzahl, Datum etc.), nach dem die Sortierung erfolgen soll. Jetzt führen wir einc Tabelle B ein, die jcdcm Schlüsselwert cine Liste von a-Elementen ziiordnet (die ,,biickets"). Das Sortieren geschieht dann einfach so, dass wir der Reihe nach die Elemente aus dem Array A holen und sie in ihre jeweilige Liste eintragen offensichtlich ein linearer Prozess. Aber das ist natürlich gemogelt: Denn die theoretische Abschätzung, dass U(N1og N ) unsclilagbar ist, gilt für belicbige Elernenttypcn a. Dcr Bucket sort funktioniert aber nur für spezielle Typen, nämlich solche, die eine kleine Schlüsselrrierige als Sortiergriiridlage verweriden. (Andernfalls macht die Verwendung ciner Tabcllc keinen Sinn.) -
--
8.3.7 Verwandte Probleme Zum Abschluss sei noch kiirz erwähnt, dass es mhlreiche andere Fragestellungen gibt, die mit den gleichen Programmiertechniken funktioniercil wie das Sortieren. Zwei Beispiele:
Median: Gesucht ist das „mittlereL'Element eines Arrays, d. h. dasjenige Element z = A[i]mit der Eigenschaft, dass $ Elemente vor1 A gröfer imd Elemente kleiner sind. Allgemeiner kann man nach dem k-ten Element (der GröRe nach) fragen. Of£'ensiclitlicli gibt es eine U ( N log N)-Lösung: Mari sortiere den Array und greife direkt auf das gewiinschte Element zu. Aber es geht auch linear! Mari rriuss nur die Idee des Quicksorl verweridcri, aber ohne gleich den garizen Array zu sorticrcn. k-Quantilen: Diejenigen Werte, die dic sortiertcn Arrayclemente in k gleich große Gruppen einteilen würden. Übung 8.4. Man adaptiere die Quicksort-Idee so, dass ein Programm zur Bestimmung des Medians entsteht.
Numerische Algorithmen
Dieses Buch soll Grundlagen der Informatik für Ingenieure vermitteln. Deshalb müssen wir bei den behandelten Themen eine gewisse Bandbreite sicherstellen. Zii einer solchen Bandbreite gehören mit Sicherheit auch n,~~rnerzsch,e Probleme, also die zahlenmäfsige Lösung mathematischer Aufgabenstellungcn. Der begrenzte Platz erlaubt nur eine exemplarische Behandlung ciniger weniger phänotypischer Algorithmen. Dabei müssen wir uns auch auf die Fragen der programmiertechnischen Implementieriing konzentrieren. Die weitaus komplexeren Aspekte der numerischen Korrektheit, also Wolildefiniertheit, Korivergenzgeschwindigkeit, Ruridungsfehler etc., überlassen wir den Kollegen aus der Wer cs genauer wissen möchtc, dcr sci auf entsprecherlde Lehrbücher der Nurneriscllen Mathematik verwiesen, z. B. [49, 401. -
9.1 Vektoren und Matrizen Numerischc Algorithmen basiercri häufig auf Vektoren und Matrizen. Bcidc wcrden progranirniertcchnisch als ein-, zwei- oder inehrdimensiorialc Arrays dargestellt. Eindimensionale Arra.ys ha.ben wir in den vorausgegangenen Kapiteln scliori benutzt. Jetzt wollen wir zweidimensionale Arrays betra.chten. Die Verallgemeinerung auf drei und niehr Dimensionen funktioniert m c h dem gleichen Schema. Zweidimensionale Arrays werden in JAVA einfach als Arrays von Arrays dargestellt. Damit sieht a. B. eine (10 X 20)-Matrix folgentlerrnaiieri aus: d o u b l e [I
[I m
= new d o u b l e Cl01
C201 ;
// (10 X 20)-Matrix
Der Zugriff auf die Elemente erfolgt in einer Form, wir in der folgenden Ziiweisung illustriert: Das ist eine typische Situation für Informatiker: Sie müssen sich darauf verlassen, dass das, was ihncn dic Experten des jeweiligcn Anwcndungsgebicts sagen, auch stimmt. Sie schreiben dann „nuru die Programme dazu.
142
9 Numerische Algorithmen
In JAVA gibt es keine vorgegebene Zuordnung, was Zeilen und was Spalten sind. Das kann der Programmierer in jeder Applikation selbst entscheideri. Wir verwenden hier folgende Ko*nventzon: 0
0
die erste Dirncnsion steht für die Zeilen; die zweite Dimension steht fiir die Spalten.
Die Initiulisierun,g mehrdimensionaler Arra.ys erfolgt meistens in geschachtelten f or-Schleifen. Aber man kann auch eine kompakte Initialisierurig der einzelnen Zcilcn vornehmen.
Beispiel 1. Die Initialisierung einer dreidimensionalen Matrix mit Zufallszahlen kanri folgendermahn geschrieben werden. double [I [I [I r = new double [I01 C51 C201 ; for (int i = 0;i < r.length; i++) { //O.. 9 for (int j = 0;j < r[O] .length;j++) { //U.. 4 for (int k = O ; k//COP y
Beispiel 4. JAVA kennt auch das Konzept unregelmäjkiger Arrays. Das bedcutct, dass z. B. Matrizcn mit Zeilen unterscliiedliclier Länge möglich sind. Eine untere Dreiecksmatrix der GröBe N mit Diagonale 1 und sonst 0 wird SolgendcrrnaBen definiert. double[] [ I 10werTriangularMatrix ( int N ) C double [I [I a = new double [NI [I ; // zweidim,en,sion,nler Arrny for (int i = 0;i N; i++) C a[il = new double [i+il ; // Zeile der Länge i for (int j = 0; j < i; j++) 1 // Elemente sind 0 aCil [jl = 0; I//for j // Diagonale I a[il Ci] = 1; I//for i return a; I//lowerTriangularMutriz
An diesen Beispielen kann Inan folgcnde Aspekte von rnchrdimensionalen Arrays sehcri: Der Ausdruck a.length gibt die Gri-iBeder ersten Dimension (Zeilenzahl) an. Der Ausdruck a [i] .length gibt die GröBe der zweiten Dimension an (Spaltenzahl der i-ten Zeile). Bei der Deklaration mit new muss nicht für alle Dirnensioncn dic Größc angegeben werden; einige der „hinterenMDimensionen dürfen offen bleiben. (Verboten ist allerdings so etwas wie new double Cl01 [I C151 .) Die einzelnen Zeilen können Arrays untcrscliiedliclier Länge sein. Die Initialisicrung und die Zuweisung können entweder elementweise oder fiir ganze Zeilen kompakt erfolgen (Letzteres allerdings nur für die letzte Dimension). Das Arbeiten mit Matrizen ist häufig mit der Verwendung gescliachteltcr Schleifen verbunden. Zur Illustration betrachten wir eine klassische Aufgabe aus der Linearen Algebra. Prograrnrn 9.1 zeigt die Multiplikation eincr ( M ,K)-Matrix mit einer (K,N)-Matrix. Dabei vcrwendcn wir eine Hilfsfunktion skalProd,die das Skalarprodukt der I-teii Zrile und der J-ten Spalte berechnet.
144
9 Numerische Algorithmen
Programm 9.1 Matrixmultiplikation public class MatMult C public double [I [ ]mult ( double [I [ 1 a, double [I [ 1 b ) // A S S E R T a ist eine ( M ,K)-Matrix und b eine ( K , N)-Matrix final int M = a .length; final int N = b [Ol . length; // Ergebnismatrix double C [I [ 1 = new double [MI [NI ; for (int i = O ; i < M ; i++) C // alle Elemente von for (int j = 0;j < N; j++) C c[i] [j] = skalProd(a,i,b,j); // E l e m m t setzen l//for j l//for i return C; )//mult private double skalProd ( double [I 1 a, int i, double [I [ 1 b, int j ) C // Skalarprodukt der Zeile ab] [.] ,und der Spalte b[.][ j ] final int K = b . length; // Zeilenzah.1 von b // HzlJSvn.riable double s = 0; for (int k = 0; k < K; k++) C // aufsumm.ieren s = s + aCil [kl*b[kl [jl ; l//for k return s; )//skalProd )//end of class M a t M d t
C
Der Anfwand der Matrixniultiplikatiori hat die Gröfienordninig 0 ( N 3 ) genauer: O ( N . K . M ) .
9.2 Gleichungssysteme: GauB-Elimination Gleichungssysterne lösen, lernt man in der Scliule je nacli Scliule auf unterschiedlichem Niveau. Spätestens auf der Universität wird diese Aufgabe dann in die Matrix-basierte Form A . X = b gebracht. Aber in welcher Form das Problem auch immer gestellt wird, letztlich ist es nur eine Menge stupider Rechrierei also eine Aufgabe für Computer. Die Methode, nacli der diese Berechnung erfolgt, geht auf GauB zurück und wird deshalb auch als Gau$-Elimination bezeic2iriet. Wir wollen die Aufgabe gleich in einer leicht verallgemeinerten Form besprechen. Es kommt nämlich relativ häufig vor, dass man das gegebene System für untcrschiedliche rechte Seiten lösen soll, also der Reihe nach A . x i = b i , . . . , A . x „ = b„. Deshalb ist es günstig, den Großteil der Arbeit nur einmal zu investieren. Das geht arn besten, irideni ina,ri die Matrix A in das Produkt zweier Dreiecksniatrizeri zerlegt (s. Abb. 9.1): -
-
9.2 Gleichungssysteme:GauB-Elimination
145
mit einer unteren Dreiecksmatrix L (lower) und einer oberen Dreiecksmatrix U (upper). Wenn man da.s geschafft hat, kann man jedes der Gleichiingssystenie in zwei Schritten lösen, nämlich
L . yi
= b,,
und dann
U . xi
= yi
Diesc Zerlegung ist in Abb. 9.1 grafisch illustriert. Bei dieser Zerlegung gibt es noch Freiheitsgrade, die wir riutzeri, um die Diagonale von L auf 1 zu setzen.
Abb. 9.1.LU-Zerlegung
In1 Folgenden diskutieren wir zuerst ganz kurz, weshalb Dreiecksrnatrizen so schön sind. Da.nach wenden wir iins dem eigentlichen Problem zu, nämlich der Programmierung der L U-Zerleg~~ng. Alle Teilalgoritlirnen werden am besten als Teile einer iimfa.ssenden Klasse konzipiert, die wir in Progra.mni 9.2 skizzieren. Als Attribute der Klasse brauchen wir dic beidcn Drciccksmatrizcn L und U sowie eine Kopie der Matrix A (weil sonst die Originalrriatrix zerstört wiirde). Wir verbergen L und U als private. Denn L und U diirferl niir vor1 der Methode factor gesetzt werden. Jede direkte Änderung von auBen hat i. Allg. desaströse Effekte. Also sichert man die Matrizen gegen Direktzugriffe ab. Man beachte, dass wir hier die Konventionen von JAVA verletzen. Eigentlich rriiissten wir die Matrizennanien A, L und U kleinsclireihi, weil es sich iim Variablen handelt. Aber hier ist für uns die Kompatibilität mit den matheniatischen Formeln (und deren Konventionen) wichtiger. Die Prin~ipicnder objektorientierten Progra.inmierung legen es nahe, für jedes Gleichungssysterri ein eigenes Objekt zu erzeugen. Wir benutzen deshalb eine Konstruktormethode, die die Matrix A sofort in die Matrizen L und U zerlegt. Danach kann rnan mit solve (bl), solve (b2), . . . beliebig viele Glcichungcn lösen. Die Anwendiing der GauB-Eliniiriation erfolgt i. Allg. in folgender Forni (für gegebene Matrizen A und B): GaussEliminationgauss = new GaussElimination(A); double [ 1 xl = gauss . solve ( b l ) ; double [ 1 xn
=
gauss . solve (bn) ;
146
9 Numerische Algorithmen
Programm 9.2 Gleichurigslösung nach dcni GauB-Verfahren: Klassenrahnien public class GaussElimination C private double C 1 C 1 A; private double [ 1 [ 1 L; private double C 1 C 1 U; private int N;
// // // //
Hzlfsrnatrix erste Resultatmatrix zweite Resultatmatrix Gröfle der Matrix
public GaussElimination ( double [I [ 1 A ) C // A S S E R T A ist ( N X N ) - M a t r i x this .N = A .length; // Anzahl der Zeilen (und Spalten) this .A = new double [NI [NI ; // Hilfsmatrix kreieren this .L = new double [NI [NI ; // erste Resultatmatrix kreieren this .U = new double [NI [NI ; // zweite Resultatmatrix kreieren // kopieren A H th2s.A for (int i = 0; i < N; i++) C System.arraycopy (A [i] ,0,this .A [i] ,0,A Ci] .length) ; // zeilenweise l//for factor (0); // LU-Zerleguag starten )//Konstruktor public double [ 1 solve ( double [ 1 b ) / / A S S E R T Faktorisierung hat schon stattgefunden //Lösung der Dreieckssysteme L y = b und U z = y double [ 1 y = solvelower(this .L, b) ; // unteres Dreieckssystem double [ 1 X = solveupper(this .U, y) ; // oberes Dreieckssystem return X ; )//sol.ue private double [I solveLower ( double [ 1 [ 1 L , double [ 1 b ) . . . «siehe Programm 9.3)) . . . private double
I
solveupper ( double
C
1 1 U, double 1 b ) C
. . . «analog zu Programm 9.3)) . . .
1//solve Upper private void factor ( int k ) C . . . «siehe Programm 9 . 4 ~. . . )//factor
)//end of class GavssElim,ination
Das lieiBt, wir erzeugen ein Objekt gauss der Klasse GaussElimination, von dem wir sofort die Operation f a c t o r ailsfiihren hssen. Danach besitzt dieses Objekt die beiden Matrizen L und U als Attribute. Deshalb können anschließend für mehrere Vektoren b l , . . . , b , die Gleichungen gelöst werden. Wenn ma.n mehrere Matrizen A l , . . . , An hat, fiir die man jeweils ein oder mehrere Gleichungssysteme lösen muss, dann generiert ma.n entsprechend n Gauss-Objekte.
9.2 Gleichungssysteme: GauB-Elimination
147
9.2.1 Lösung von Dreieckssystemen
Weshalb sind Dreiecksmatrizen so günstig? Das inacht man sich ganz sclinell an cincrn Beispiel klar. Mari betrachte das System
Hier beginnt man in der ersten Zeile und erhält der Reihe nach die Rechriungen
Das lässt sich ganz leicht in (las iterative Prograniiu 9.3 inmetzen. Programm 9.3 Lösen eines (unteren) Dreieckssystems L . y
=b
public class GaussElimination public double [ I solveLower ( double 1 [ 1 L, double 1 b ) C // A S S E R T L ist untere ( N X N)-Dreiecksmatrix m i t Diagonale I // A S S E R T b isl Vektor der Länge N final int N = L.length; // Resultatvektor double C 1 y = new double [NI ; // fGr jedes y, (jede Zeile L[i][.]) for (int i = 0; i < N; i++) double s = 0; // für Zwisclzene~ebn~isse for (int j = 0; j < i; j++) // Zeile L[i] X Spalte y s = s + L[il [jI*y[jI;
1/ / f 0 ~ y[i] = b[i] l//for i return y ; l//solveLower
-
s;
// Y,,
= bi
-
L [ i ]X y
)//end of dass GaussElimination
Übung 9.1. Man programmiere die Lösung eines oberen Dreieckssystems beachte man, dass die Diagonale jetzt nicht m i t 1 besetzt ist.
Ux = y . Dabei
Übung 9.2. Man kann die obere und untere Dreiecksmatrix als zwei Hälften einer gemeinsamen Matrix abspeichern (s. Abschnitt 9.2.2). Ändert sich dadurch etwas an den Programmen?
148
9 Numerische Algorithmen
Bleibt also ,pur1' noch das Problem, die Matrizen L und U zu finden. Die Berecliniing dieser Matrizen ist in Abi). 9.2 illustriert. Aus dieser Abbildung
Abb. 9.2. LU-Zerlegung
könrien wir die folgenden Gleicliirrigen ablesen (wobei wir die Elemente 1, u und a als einclementige Matrizen lesen müssen):
Wie man sieht, ist die erste Zeile von U identisch mit der ersten Zeile von A. Die erste Spalte von L ergibt sich, indem man jedes Element der ersten Spalte von A mit dem Wert multipliziert. Die Werte der Matrix A" ergeben sich als = - l ~ ,uj. ~. Diese Berechnungen lassen sich ziemlich direkt in das Programm 9.4 umsetzen. Dabei arbeiten wir auf eincr privaten Kopic A der Eingabematrix, weil sie sich während der Berechnung ändert. Die Methode f actor hat eigentlich zwei Ergebnisse, nämlich die beiden Matrizen L und U. Wir speicliern tliese als Attribute der Klasse. (Aus Gründen der Lesbarkeit lassen wir hier bei Zugriffen auf die Attribute das „ t h i s . " weg, obwohl wir es sonst wegen der besseren Dokurrientation immer schreiben.) i
Anmerkung: In den Frühzeiten der Informatik, als Speicher knapp, teuer und langsam war, musste man mit ausgefeilten Tricks arbeiten, um die Programme effizienter z i ~machen, ohne Rücksicht a,uf Verständlichkeit. Diese 'Tricks findet man heute noch in vielen Mathernatikbiichern: Die beiden Dreiecksmatrizen L und U kann man in einer gemeinsamen Matrix speichern; da die Diagonalelerrtente von L immer 1 sind, steht die Diagonale für die Elemente von U zur Verfiigung. Da immer nur der R.est von A gebraucht wird, kann man sogar die Matrix A sukzessive mit den Eleineriten von L und U iiberschreiben.
9.2 Gleichungssysteme: Gauß-Elimination
149
Programm 9.4 Die LU-Zerlegung nach GauB public class GaussElimination private double [I C 1 A; private double [I C 1 L; private double C] [ 1 U; private int N; private void factor ( int k ) ( // A S S E R T 0 5 Ic < N L Ckl Ckl = I; U Ckl Ckl = A Ckl Ckl ; System.arraycopy(ACk] ,k+l,U[k] ,k+l,N-k-1); double V = 1/U [k] [k] ; for (int i = k+l; i < N; i++) L Ci] [k] = A Ci] [k] *V; l//for for (int i = k+i; i < N; i++) ( for (int j = k + l ; j < N; j++) ( ACil Cjl = ACil Cjl - LCil [kl*U[kl Cjl; 1//for i 1/ / f i r j if (k < N-1) ( factor (k+l) ; l 1//factor )//end of class GuussElimination,
// // // //
Hilfsm.atrzx erste Resultatm,atrix zweite Resultatrnatrix Gröfle der Matrix
// Diagonalelement setzen // Element U setzen, // Zeile kopieren // Hilfsgröjle: Falctor l/u // Spalte 11 berechnen
// Au berechnen
// reku~simerAujruf für At'
Heute ist das Kriterium Speicherbedarf nachrangig geworden. Wichtiger ist die Verständlichkeit und Fehlerresistenz der Programmierung. Auch die Robustheit des Codes gegen irrtümlich falsche Verwendung ist wichtig. Deshalb haben wir eine aufwendigere, aber sichere Variante progran~rnicrt. Übung 9.3. Um den rekursiven Aufruf für L' . U' = A" zu realisieren, haben wir die private Hilfsmethode factor rekursiv mit einem zusätzlichen Index Ic programmiert. Man kann diesen rekursiven Aufruf auch ersetzen, indem man eine zusätzliche Schleife verwendet. Man programmiere diese Variante. (In der rekursiven Version ist das Programm lesbarer. ) Übung 9.4. Das Kopieren der Originalmatrix in die Hilfsmatrix ist zeitaufwendig Man kann es umgehen, indem man nicht die Matrix A" berechnet, sondern A' unverändert lässt. Bei der Berechnung von U , 11 und ii in der Methode factor müssen die fehlenden Operationen dann ~eweilsnachgeholt werden. Man vergewissert sich schnell, dass diese Werte nach folgenden Formeln bestimmt werden:
Man programmiere diese Variante.
150
9 Numerische Algorithmen
9.2.3 Pivot-Elemente
Der Algoritlmus in Programm 9.4 hat noch einen gravierenden Nachteil. Wir brauchen zur Berechnung von 1L den Wcrt Was ist, wenn der Wert a null ist? Die Lösung dieses Problems ergibt sich crfrculicherwcise als Nebeneffekt der Lösung eines anderen Problems. Denn die Division ist auch kritisch, wenn der Wert a sehr klein ist, weil sich dann die Rundungsfehler verstärken. Also sollte a möglichst große Werte haben. Die Lösung von Gleichiingssystemen ist invariant gegeniiber der Vertauscliimg von Zeilen, sofern man die Vertauschung sowohl in A als auch in b vornimmt. Deshalb sollte man in dcr Abb. 9.2 zunächst das gröKte Elemcrit des Spalterivcktors al bcstimnien - man nennt es das Pivot-Element - und dann die entsprechende Zeile mit der ersten Zeile vertaiischen. (In der Methode f a c t o r muss natiirlich eine Vertaim:himg mit der k-ten Zeile erfolgen.) Mathematisch gesehen laufen diese Vertauschungen auf die Multiplikation Pk hinaus. Diese Matrizen sind in Abb. 9.3 skiamit Perm~~tat.ion,smo,trizen ziert; dabei steht j für den Index der Zeile, die in Schritt k also in der Methode f a c t o r ( k ) - das größte Element der Spalte enthält. Wenn wir mit
i.
-
Abb. 9.3. Permutationsmatrix PA.
P = P,-i . . .Pl das Produkt dicscr Matrizen bezeichnen, dann kann man zeigen, dass insgesamt gilt:
P.L.U.x=P.A.x=P.b Als Ergebnis der Methode f a c t o r entstehen jetzt zwei modifizierte Matrizen L' und U', fiir die gilt: L'.U1 = P.L.U. Also muss auch die Permutationsmatrix P gespeichert werden, damit ma.n sie aiif b anwenden kann. Programmiertechnisch wird die h h t r i x P arn besten als Folge (Array) der jeweiligen Pivot-Indizes j repräsentiert. Übung 9.5. Man modifiziere Programm 9.4 so, dass es mit Pivotsuche erfolgt Übung 9.6. M i t Hilfe der Matrizen L und U kann man auch die Inverse A ' einer Matrix A leicht berechnen. Man braucht dazu nur die Gleichungssysteme L . U . a, = P . e, zu lösen, wobei a, die i-te Spalte von A p l ist und e, der i - t e Achsenvektor.
9.2 Gleichungssysteme: Gauf-Elimination
151
9.2.4 Nachiteration Bei der LU-Faktorisierung können sich die Rundungsfehler akkumulieren. Das lässt sich reparieren, indem eine Nachiteration angewandt wird. Ausgangspunkt ist die Beobachtung, dass am Ende der Methode f actor nicht dic rnathematisch exakten Matrizen L und U mit L . U = A entstehen, sondern nur Näherungen L und U mit L . U N A. Das Gleiche gilt für den Ergebnisvektor X, der auch nur eine Näherung an das echte Ergebnis X ist. Sei B eine beliebige (nichtsinguläre) Matrix; dann gilt wegen Ax = b trivialcrwcise Ex+ (A - B ) x = b. Wenn wir dagegen die Näherung X betrachten, dann erhalten wir nur noch
Man karin jetzt ausgehend von mittels der Itcrationsvorschrift -
X(") =
X
-
eine Folge von
X(')
berechnen
In jedem Schritt muss dabei das entsprechende Gleichungssysterri für X("') gelöst werden. Man hört auf, wenn die Werte x ( ~ + 'und ) x ( ~bis ) auf die gewünschte Genauigkeit E übercinstimnien. (Das heißt bei Vektoren, dass alle Komponenten bis auf E übereinstimmen.) Aus der Niirrierischeri Mathematik ist bekannt, dass dieses Verfa.hren konvergiert, iirid zwar urnso schneller, je näher B an A liegt. Das ist sicher der Fall, werin wir B folgendermagen wählen:
Wenn wir die obige Gleichung na.ch X("') ergibt sich
auflösen und dieses B einsetzen,
Da.bei ergibt sich r(" als Lösung der Dreiecksgleichungeri
Mit dieser Nacliiteratiori wird i. Allg. schon nach ein bis zwei Schritten das Ergebnis auf Maschiricngcnauigkcit korrekt sein. Übung 9.7. Man programmiere das Verfahren der Nachiteration
9 Numerische Algorithmen
152
9.3 Wurzelberechnung und Nullstellen von Funktionen In dem Standardobjekt Math der Sprache .JAVA ist unter anderem die Methode s q r t zur Wurzclbcrcchriung vordefiniert. Wir wollen uns jetzt ansehen, wie man solche Verfahren bei Bedarf (man hat nicht immer eine Sprache wie JAVA zur Verfügung) selbst programmieren kann. Aiifierdem werden wir dabei ailch sehen, wie nian kiibische und andere Wurzeln berechnen kann, also Üblicherweise nimnit man ein Verfahren, das auf Newton ziiriickgelit iirid das sehr schnell konvergiert. Dieses Verfahren liefert eine generelle Möglichkeit, die Nullstelle einer Funktion zii berechnen. Also müsscri wir unsere Aufgabe zuerst in ein solches Nullstellcnproblerri umwandeln. Das geht ganz einfach mit elenieritarer Schiilmathematik. Und weil es Math .s q r t schon gibt, illustrieren wir da.s Problem anha.nd der kubischen Wurzel. (Allerdings wird es diese irn neuen JAVA 1.5 auch vordefiriiert geben.)
*.
X=,&
.T 3
-
a
23-a=o
Urri unsere gesuchte Quadratwurzel zu finden, müssen wir also eirie Nullstelle der folgerideri Finiktion berechrien:
Damit haben wir da.s spezielle Problem der Wixzelberecliniirig auf das allgemeinere Problem der Nullstellenbesti~nrnungzurückgeführt.
Aufgabe: Nullstellenbest.immung Gegeben: Eine relle Funktion f : R -, R. Gesu,cht: Ein Wert X , für den f Ni111 ist, also f (X) = 0. Vora~~ssetzung: Die Funktion f muss differenzierbar sein. Die Lösungsidec für diese Art von Problemen gcht auf Newton zurück: Abbildung 9.4 illiistriert, dass fiir differenzierbare Funktionen die Gleichung
einen Punkt z' liefert, der näher am Nullpunkt liegt als die wcsentliclie Idee für da.s Lösiingsverfalircn.
:X.
Daraus erhält man
Methode: Approximation Viele Aufgaben nicht nur in der Numerik lassen sicli durch eirie schrittweise Approximation lösen: Ausgehend von einer groben Näherung an die Liisiing werden naclieiriarider irrirrier bessere Approxirriatiorlen I->estirnrnt, bis die Lösung erreicht oder wcr~igstcnshinreichend gut angenähert ist. Der zentrale Aspekt bei dieser Methode ist die Frage, was jeweils der Schritt von einer Nä,herurigslösung zur nä.chstbesseren ist. In unserem Beispiel lässt sicli ausgehend von einem geeigneten Startwert z" mithilfe der Gleichung (*) eine Folge von Werten -
-
-
-
9.3 Wurzelberechnung und Nullstellen von Funktionen
153
Abb. 9.4. Illustration des Newton-Verfahrens
berechnen, dic zur gewünschten Nullstellc konvergieren. (Dic gcnaueren Details Riiiiduiigsfehleranalyse, Konvergenzgrad etc. iiherlasseii wir den Kollegen aus der Mathematik.) -
-
Bezogen aiif unsere spezielle Anwendung der Wurzell~ereclniiingheißt das, dass wir zunächst die Ableitung der Funktion f (X) Ef z" a brauchen, also f l ( x ) = 3x2.Damit ergibt sich als Schritt X, ++ xt+l für die Berechnung der Folge:
Aus diesen Überlegungen erhalten wir iirirnittelbar das Programm 9.5, in dem wir wie üblich die eigentlich interessierende Methode cubicRoot in eine Klasse einpacken. Das gibt uns auch die Chance, eine Reihe von Hilfsnietlioder~ zu verwenden, die rnittcls private vor dem Zugriff von außen geschützt sind. Durch diese Hilfsniethodcri wird die Beschrciburig und darnil die Lesba.rkcit des Progra.inms wcscntlich übersichtlicher und ggf. änderungsfreundlicller. Die Berechnung des Starkwerts ist hier ziemlich ad Iioc vorgenommen. Generell gilt: Je näher der Startwert am spä.teren Resu1ta.t liegt, umso schneller konvergiert der Algorithinus. Idealerweise könnten wir den Startwert folgenderrria8en bestirnrneri: Wenn CI. = 0.-~mtr,nLisse,>. 10".'," gilt, dann liefert die Setzurig :L,, = 1 . 10',""/ einen guten Startwert. Leider gibt es aber in JAVA- und auch allen anderen gängigen Prograrnrnierspracheri keine einfache Methode, auf den Exponenten einer Gleitpiinktzahl zuzugreifen. -
-
Bei numerischen Algorithineii gibt es iinnier ein ganz großes Probleni: Es betrifft ein grundsätzliches Dejizzt der Gleztpunktzahlen: Die Matlieniatik arheitct mit reellen Zahlen, Computer I->~sitzrii nur grobe Approxirnatiorien in Form von Gleitpunktzahlcn. Deshalb ist Inan immer mit dem Phänomen der Rundungsfehler korifronticrt.
154
9 Numerische Algorithmen
Programm 9.5 Die Berechnung der kubischen Wiirzel public class CubicRoot public double cubicRoot (double a) { double xOld = a; double xNew = startwert (a) ; while ( notClose (xNew, xOld) ) { xOld = xNew; xNew = step (xOld,a) ;
// Vorbereitun.g
// aktuellen Wert merken // Newton-Formel für z, H z,+i
)
return xNew ; )//cubicRoot
1
private double startwert (double a) { return a /10; // irgendein Startwert (s. Text) 1 // startwert private double step (double X, double a) { return X - (X - a/ (x*x) ) / 3; // Newton-Formel I// step private boolean notClose (double X, double y) { return Math.abs(x - y) > 1E-10; // nahe genug? )// close // end of dass Cul>icRoot
Das hat insbesondere zur Folge, dass ein Gleichheitstest der Art (x==y) für Gleitpunktzahlen a priorz sinnlos ist! Aus diesem Grund müssen wir Funktionen wie c l o s e oder n o t c l o s e benutzen, in denen geprüft wird, ob die Differenz kleiner als eine kleine Schranke E ist. Auf wie viele Stellen Genauigkeit dieses E festgesetzt wird, hangt von der Applikation ab. Man sollte auf jeden Fall eine solche Funktion im Programm verwenden und nicht einen Test wie ( . . . uchenwir dic minimalcn mathematischen Voraiissetzimgen. Eine Niihemmy an den Wert f l ( x ) liefcrt der Diflerenzenquotient, das heiBt
sofern der Wert h klein genug ist. Das wird durch folgende Skizze illustriert:
Das Problem ist nur, das richtige h zu finden. Das lösen wir ganz einfach durch cirien schrittweiscn Approxirriationsprozess. Methode: App,roz.irr~ation Das Grundprinzip der Approximation winde scliorl in Abschnitt 9.3 eingeführt. In unsereni Beispiel betrachten wir die Folge der Werte
h hp hp hp ph ' 2 ' 4 ' 8 ' 16"" und hören auf, wenn die zugehörigen Differenzeriqilotienten sich nicht mehr wesentlich ändern. Damit ist die Lösungsidee skizziert, iind wir könnten eigentlich mit dem Programmieren begiririen.
Aber da gzbt es ein Problem.
156
9 Numerische Algorithmen
Wenn wir für eine gegebene Funktion f den Wert f ' ( x ) der Ableitung an der Stelle X berechnen wollen, dann müssten wir die Methode d i f f (f , X ) aufrufen. Aber .JAVA erlaubt k e i m F~~nktionen, (Methoden,) als Arytrm,ente von Funktionen! Wir werden dieses Problern in voller Allgemeinheit erst in Kap. 13 behandeln können. Im Augenblick begnügen wir uns mit einem Notbehelf der allerdings bereits die endgültige Lösung in Kap. 13 vorbereitet. JAVA erlaubt auf Paranieterposition nur Werte und Objekte. Also müssen wir unsere Funktion in ein geeignetes Objekt einpacken. Nehmen wir an, wir wollen die Ableitung für die Funktion f ( X ) = . e2"' berechnen. Dann definieren wir folgende Klasse -
&
c l a s s Fun { double apply ( double )//end of class Fun
X
)
C r e t u r n Math. exp(2*x*x) / ( x + l ) ; 1
Dann definieren wir die gewünschte Funktion f als ein Objekt dieser Klasse: Fun f
=
new Fun ( ) ;
Jetzt können wir aufrufen: d i f f (f , X ) . Allerdings müssen wir an allen Stellen, an denen in der Mathematik f (. . . ) geschrieben wird, stattdessen f .apply ( . . . ) schreiben. Aber mit dieser kleinen Merkwiirdigkeit können wir leben. h m i t ha.ben wir die Voraiissetziing geschaffen, iim das Programm 9.6 fiir numerisches Differenzieren zu schreiben. Programm 9.6 Numerisches Differenzieren public class Differenzieren C public double dif f ( Fun f , double double h = 0.01 ; double d = diffquot (f ,x,h) ; double dOld; do C d0ld = d; h=h/2; d = dif fquot (f ,X,h) ; ) while ( notClose(d, dOld) ) ; return d; 1/ / d i f f
X )
C
// // // // //
Differenzial f '(X) Startwert Startwerl Hilfs,uar~able mindestens einmal
// kleinere Schritbweite // neuer D2flerenzenquotien.t // Approz. gu,t genmg?
private double diffquot ( Fun f, double X, double h ) return ( f .apply (x+h) - f .apply (X-h) ) / (2*h) ; 1//diffquot
// Diff.quotient
private boolean notClose ( double X, double y ) return Math. abs(x-y) > 1E-10; // yeuiünschte Geauuigkeit
1 )// end of cluss Differenwieren,
9.5 Integrieren
157
Evaluation: Aufwand: Die Zahl dcr Schlcifendurchlaufc hangt von der Konvergenzgeschwiridigkeit ab. Derartige Analysen sind Gegenstand der Numerischen Mathematik und gehen damit über dcn Rahmen dieser Vorlesung hinaus. Standardtests: Unterschiedliche Arten von Funktionen f , insbesondere konstante Funktionen; Verhalten an extrem „steilen1' Stellen (z.B. Tangens, Kotangens). Übung 9.11. Betrachten Sie das obige Beispiel zur Berechnung der Ableitung einer Funktion: Modifizieren Sie das Beispiel so, dass die Folge der Schrittweiten h, $ , $ , Modifizieren Sie das Beispiel so, dass der einseitige Differenzenquotient
6 ,. . . ist. f('r)
genommen wird. Testen Sie, inwieweit sich diese Änderungen auf die Konvergenzgeschwindigkeit auswirken.
9.5 Integrieren Das Gegenstück zum Differerizieren ist das Integrieren. Dic Lösung des Integrationsproblerns
.I"
f
(:I:)
dn:
vcrlangt noch etwas mathematische Vorarbeit. Dabei können wir uns die Grurididee mit ein bisschen Schillrriatherriatik schnell klarrr~aclien.Die Übcrlegungen, unter welchen Umständen diese Lösung funktioniert und waruni, müssen wir allerdings wieder einmal dcn Mathernatikcrn genaiier: den Niimerikern überlassen. Zur Illustration betrachten wir Abb. 9.5. -
I
b
Abb. 9.5. Approximation cincs Integrals durch Trapezsummerl
9 Numerische Algorithmen
158
Idee 1: Wir teilen das Intervall [U,b] in n Teilintervalle ein, berechnen die jeweiligen Trapezflächen Ti, . . . , T„und summieren sic auf. Scien also h = und y, = f ( X ,) = f ( ( I + I . h). Dann gilt:
df!f
=
T S u m f (a, b ) (n)
Die Tra.pezsumme TSumf(a,h)(n,) liefert offcnsichtlich cinc Approximation an den gesuchten Wert des Integrals. Die Güte dieser Approximation wird durch die Anzahl n (und damit die Breite h ) der Intervalle bestimmt in Abhängigkeit von dcr jcweiligen Funktion f . Damit hal-)en wir eiri Dilernma: Ein zu grobes h wird i. Allg. zu schlechten Approxima.tioncn führen. Andererseits bedeutet eiri zu feines h sehr viel R e clienaufwand (und birgt außerdem noch dic Gefahr von akkumulicrtcn R.undungsfehlern). Und das Ganze wird noch dadurch verschlimmert, dass dic h von den Eigenschaften dcr jeweiligen Funktion f abWahl des ,,richtigenLL hängt. Also müssen wir uns noch ein bisschen mehr überlegen. -
Idee 2: Wir beginnen mit einem groben h und verfeinern die Intervalle schrittweise immer weiter, bis die jeweiligen Approxiniationswerte genau genug sind. Das heifit, wir bctrachten z. B. die Folge
und die zugehörigen Approximationen
Das Programm &für wäre sehr schnell zu schrcibcn - es ist cine wcitcre Anwendung des Konvergenzprinzips, das wir schon friiher bei der Niillstellenbestimmung und der Differenziation a.ngewandt haben. Abcr diese na.ive Prograrnrnierung würdc schr viele Doppelberechriungen bewirken. Um das crkennen zu können, rrliissen wir ims noch etwas weiter in die Mallieniatik vcrticfcn.
Idee 3: Wir wollen bereits berechnete Teilergebnisse übcr Iterationen hiilwcg „rcttenU.Man betrachte zwei aufeinander folgende Verfeirierurigsschritte (wobei wir mit der Nolalion yi+g andeuten, dass der entsprechende Wert f ( 2 i $) ist): Bei n Intervallen haben wir den Wert
+
Bei 271 Intervallen ergibt sich
9.5 Integrieren
159
Diese Version niitzt die zuvor berechneten Teilergebnisse jeweils maximal aus und reduziert den Rechenaufwand &mit beträchtlich. Deshalb wollen wir diese Version jetzt in ein Programm umsetzen (s. Programm 9.7). In dicsem
Programm 9.7 Berechnung des Integrals J: f (z)dz public class Integrieren C public double integral ( Fun f, double a, double b ) C int n = 1; double h = b - a; double s = h * ( f .apply(a) + f .apply(b) ) / 2; double sOld; do C Sold = s; s = (s + h * sum (n, f, a+(h/2), h)) /2; n=2*n; h=h/2; 1 while ( notClose (s, s0ld ) ) ;/ / d o return s; I// integral private double sum (int n, Fun f, double initia.1,double double r = 0; for (int j = 0; j < n; j++) C r = r + f . apply (initial + j*h) ; l/(for return r; //surn private boolean notclose ( double return Math.abs(x-y) > 1E-10; l//notClose )// end of class Integrieren
X,
double y ) C
Programm bcreclineri wir folgende Folge von Werten:
SO> SI,
5'2,
s3, s 4 ,
5'5,
.. .
wobei jeweils S, = TSuml(a, b ) ( 2 7 gilt. Damit folgt irishesontlere der Zusarnnienliang
160
9 Numerische Algorithmen
mit den Startwerten
Auch hier haben wir wieder eine Variante unseres Konvergenzschemas, jetzt allerdings mit zwei statt nur einem Parameter. Dieses Sclieina lässt sich aiicli wieder ganz einfach in das Programm 9.7 uinsetzcn. Bezüglich der Funktion f müssen wir wic schon bei der Diffcrc~iziation wieder die Einbettung in eine Klasse Fun vornehmen. Man beachte, dass die Setzungen n = 2*n und h = h/2 erst nach der Neuberechilurig von s erfolgen dürfen, weil bei dieser noch die alten Werte von n und h benötigt werden. Hinweis: Mari sollte anders als wir es irn obigen Progranini gemacht haben eine gewisse Mininialzalil von Schritten vorselieii, bevor man einen Abbruch zulässt. Denn in pathologischeri Fallen kann cs ein „PseudoendcLL geben. Solche kritischen Situationen können z. B. bei periodischen Funktionen wie Sinus oder Kosinus auftreten, wo die ersten Intervallteilungen auf lauter identische Werte stofieri können. -
-
-
-
9.6 Interpolation Naturwissenschaftler und Ingenieure sind häufig mit eiiieni unangenehmen Problem konfrontiert: Mari weiB qualitativ, dass zwischen gewissen GröBen eine funktionale Abhängigkeit f besteht, aber man kennt diese Abhängigkeit nicht quantitativ, das lieifit, n1a.n ha.t keim geschlossene Forrncl für die Furiktion f . Alles, was inan hat, sind ein paar Stichproben, also Messwerte (zu,yO),. . . (X",, yn). Trotzdem muss nian den Funktionswert L/ = f (z)an einer gegebenen Stelle errnittelri d. 1i. möglichst gut abschätzen. Und diese Stelle X ist i. Allg. nicht unter den Stichproben enthalten. Diese Aufgabe der sog. Interpolation ist in Abb. 9.6 vcra.iischaulicht: Dic Messwerte ( x o ,yo), . . . , ( x r i ,y n ) werden als Stützstellen bezeichnet. Was wir brauchen, ist ein „dazu passeiider" Wert ?/ an einer Stelle X, die selbst kein Messpunkt ist. Uni das „passend festzulegen, gellen wir davon aus, dass der fiinktioriale Ziisarrniieriliarig „gutarti$ ist, d. h., durch eine möglichst „glalte" Funktionskiirve adäqiiat wiedergegeben wird. U I I ~fiir diese imbekar~nte Funktion f wollen wir (tarin dcri Wert f ( X ) berechnen. Da. wir die Funktion f selbst nicht kennen, erset,zen wir sie durch eine andere Funktion 11, die wir tat,sä.clilich konstruieren können. Unter der Hypothese, dass f hinreichend „glattu ist, können wir p so gestalten, dass es sehr nahe an f liegt. Und dann berecliiien wir ?/ = P ( % ) N j"(3). -
Abb. 9.6. Das Interpolationsproblern
Häufig nirnrnt man als Näherung p an die gesuchte Funktion f ein geeignetes Polynom. Ziir Erinnerung: Ein Polynom vom Grad n ist ein Ausdruck der Form
mit gewissen Koeffizienten a,. Das für unsere Zwecke grundlegende Theorem besagt dabei, dass ein Polynom n-tcn Grades durch ( n + l ) Stützstellen eindeutig bestimmt ist. Bleibt also „riiirL'das Problem, das Polynom p zu hereclineri. In anderen Worten: Wir miissen die Koeffizienten a , bestimmen.
Aufgabe: Numerische Interpolation Gegeben: Eine Liste von Stiitzstellen (xi, g , ) , i = 0, . . . , n , dargestellt durch einen Array points; außerdem ein Wert 3. Gesucht: Ein Polynom p n-ten Grades, das die Stützstellen interyoliert, d. 11. p ( z i ) = y+ fiir i = 0 , . . . , r2. Voraussetzun,g: Einige numerische Forderiingeri bzgl. der „Gutartigkeit" der Daten (worauf wir hier nicht näher eingehen können). Die Lösungsidee. I11 den cornputerlosen Jahrliunderten war es zum Glück viel wichtiger als heiitc, dass Bereclinungcn so ökonomisch wie möglich erfolgen konnten. Das hat 1)rilliante Mathematiker wie Newton beflügelt, sich clevere Rechenverfahren auszudenken. Für da.s Problem der Interpolation hat er einen Lösungsweg gefunden, der unter dem Namen dividierte Differenzen in die Literatur eingcgarigeri isl. Wir verwenden folgende Nota.tiori: p $ ( ~ist. ) dasjenige Polynom vorn Gra.d j - i , da.s die Stützstellen i , . . . ,j erfasst, also ~ I ; ( x , = ) yi, . . . , p ( . r J ) = 9 , . In dieser Notation ist unser gesuchtes Polynom also p ( z ) = &(X). Wie so oft hilft ein rekursiver Ansatz, d. h. die Zurückführung des gegebenen Problems auf ein kleineres Problem. Wir stellen unser gesiiclites Polynom pj', (X) als Sunime zwcicr Polynome dar: p:
(T) =
(T) + q„ ( X ) ,
q„ geeignetes Polynom vorn Grad rr
(9.2)
162
9 Numerische Algorithmen
Das ist in Abb. 9.7 illi~striert,wobei wir als Beispieldaten die Stützpunkte (X) und (0, I), (1,5),(3,l)und (4,2) benützen. Weil q„(x) = (X) -
Abb. 9.7. Beziehung der Polynome
und
9.3
I ( J , ) fiir i = 0, . . . , n - I, sind zo,. . . , T„-1 Nullstellen von P: (X,)= I/, = q„(z). Damit kann q„ (X) in folgcndcr Form dargestellt werden:
mit eirieni unbekannten Koeffizienten U ; . Diese R.echnung kann rekursiv auf und alle weiteren Polynome fortgesetzt werden, sodass sich letztlich ergibt:
&(X)
=
a:,(x
-
xo) . . . (X - X , , )
(X)
Das liefert folgende Gleichung fiir das Polynom p(x) = p:L
(X):
Die Strategie von Newton. Bleibt das Problem, die Koeffizienten a: a.usziirechncn. Das könnte rriari irr1 Prinzip mit den Gleichungeri (9.4) tun. Denn wegen pU(zo) = yo gilt ab = C/,). Entsprechend folgt a,us py(xi) = yl sofort a i = Und so weiter. Aber das ist cinc reclieniritcnsivc und urnstäridliche Strategie. Die Idee von Newton orga.nisiert diese Rerechniing wesentlicli geschickter ~ m dschneller.
z.
9.6 Interpolation
Wir verallgenieinern die Reki~rsiorisbeziehiing(9.2) von ergibt ganz analog die Gleichung
&
163
ailf 113. Das
mit einem unbekarinten Koeffizienten ai,, . Offensichtlich gilt uo,, = uj, sodass wir unsere gesucliteri Koeffizienten erhalten. Durch Induktion2 kann man zeigen, dass folgende Reki~rrenzbeziehungfür diese a ; , , bestellt:
Die Koeftizienteri werden traditionell in der Form f [X„ . . , X,] geschrieben und als ncwtonsche dzvzdzerte Dzfferenzen bezeichnet. Die Rekurrenzbeziehungen (9.7) führen zu den Abhängigkeiten, die in Abb. 9.8 gezeigt sind. Man
erkennt, d a s die Koeffizienten a,,, als Elemcntc cincr obcrcn Drciecksmatrix gespeichert werden könsien. Die Diagonalelernente sind die Werte y, und die erste Zeile enthält die gesuchten Koeffizienten des Polynoms (9.5).
Dus Programm. Das Programm 9.8 ist eine nahezu triviale Umsetzung der Strategie aus Abb. 9.8 mit den G1eichung.cn (9.7). Das Design folgt wicdcr den Grundprinzipien der objektorientierten Programmiernng, indem zu jeder Menge von Stützstellen ein Objekt erzeugt wird. Der Konstruktor berechnet sofort die entsprechenden Koeffizienten der Koeffizientenniatrix a. Für die Berechnung dieser Matrix gibt es aufgrund der Abhängigkeiten aus Ahb. 9.8 drei Mögliclikeiten: Wir rechnen den Beweis hier nicht explizit vor, sondern verweisen auf die Literatur, x. B. [49]
164
9 Numerische Algorithmen
Programm 9.8 Intcrpolation mit dividicrten Differenzen von Newton public class Interpolation C p r i v a t e double C 1 X ; p r i v a t e double [I C 1 a ; private i n t n; public Interpolation ( Point n = p o i n t s . l e n g t h - 1; X = new double [n+l] ; a = new double [n+ll [n+ll ; f o r ( i n t i = 0 ; i =O;i--)C a [ i ] [j] = ( a [ i + i ] [ j ] - a [ i ] [j-11)
// siehe Abb. 9.8 // Spalten links + rechts //Zeilenuntenioben / ( x [ j l - x [ i l ) ; // siehe (0.7)
>//for i l//for j )//newlon, p u b l i c double apply ( double X ) C // Auswertung; siehe (9.5) double sum = a C01 C01 ; // sum = ah double f a c t o r = 1 ; // neutral initzalisiert f o r ( i n t j = 1 ; j < = n ; j++) C f a c t o r = f a c t o r * (X - t h i s . x [ j - 1 1 ) ; // ( X - xo) . . . (X - ~ ~ sum = sum + aC01 [ j ] * f a c t o r ; //.s+aj(z-zo)...(z-x,-l )//for j r e t u r n sum;
1
)
)
)//apply )//end of class Interpolation
0 0
0
Man kann Diagonale für Dia.gonale berechnen. Man kanri zeilenweise von unten nach oben und innerhalb jeder Zeile von links nach rechts arbeiten. Man kann spaltenweise von links nach rechts lind innerhalb jcder Spalte von untcn nach oben arbcitcri.
Wir wählen unter diesen ansonsten glcichwertigen - Varianten die letzte, weil sie besser zur spiitereri Extrapolation passt. Die Auswertung der Gleichung 9.5 in der Methode apply erfolgt meistens nach dem sog. Horncr-Schema, weil man daniit ein paar Additionen sparen kanri. Ebenfalls orientiert an der späteren Extrapolation wählen wir hier eine -
9.6 Interpolation
165
leicht andere Form, bei der in einer Variablen f actor jeweils das Teilprodukt (X zo). . . (X X + ) mitgerechnet wird. Diese Klasse wird üblicherweise so verwendet, dass man zu jeder Menge points von Stützstellen ein entsprechendes Objekt kreiert. Interpolation p = new Interpolation(points) ; double yql = p. apply ( x q l ) ; -
-
.. . double yqn = p .apply (xqn) ; Das heißt, nachdem das interpolierende Polynom genauer: die Koeffizienten ao,? berechnet sind, kann man beliebig viele interpolierte Punkte ausrechnen. Vorsicht! Die Werte 3, an denen man interpoliert, müssen mnerhalb der Stützstcllcn xo,. . . , X „ liegen. An dcn Rändern und vor allem außerhalb beginnt das Polynom i. Allg. stark zu oszillicrcn, sodass erratische Werte entstehen. (In Abb. 9.7 deutet sich das beim Polynorn & ( X ) scllon an: Rechts von seiner letzten Stiitzstelle ( 3 , l ) stürzt die Kurvc steil ab.) -
-
9.6.1 Für Geizhälse: Speicherplatz sparen
1111Programm 9.8 haben wir eine Matrix a bcnutzt. In Büchern zur numerischen Ma.thema.tik findet man die Programme aber i. Allg. in einer Form, die mit eiriern eindinlensionalen Array ai~skornriit.Denn die Alhängigkeiteri der Ma.trixfelder sind so, dass man immer alle ta.tsächlich noch benötigten Werte in einem Array halten kann. Betrachten wir nochmals Abb. 9.8. An Stelle der Matrix a können wir mit einem Array a arbeiten, in dem wir zuerst die Diagonalelerriente speichern. Darin beginnen wir von unten her die Ele~neritezu überschrcibcn. Zuerst wird a4,4 diirch a:iing der beiden Siiperklassen A u t o i ~ n dBoot charakterisiert oder Quadrate als Übcrlappung von rechteckigen und gleichseitigen Figurcn. Im Allgemeinen machen solche Überlappiingen keine Probleme. Schwierig wird es nur, wenn in zwei solchen Superklassen die gleiche Methode deklariert wird (genaiier: zwei verschiedene Methoden mit dem gleichen Namen). Folgendes Prograrnnifragrnent illustriert diese Situation:
192
11 Interfaces
class Auto C void fahren 0 (
...
1 class Boot ( void fahren 0 C
...
)
1 class Amphibienfahrzeug extends j k k & e e k
// n,icht JAVA!!
Hier ist völlig unklar, welche der beiden Definitionen von fa h r e n o gemeint ist. Und auch Hilfsmittel wie super helfen nicht mehr weiter. Für dieses grundlegende Problem gibt es in der Literatur die iinterschiedliclisteri Lösurigsansätze. JAVA wählt einen ziemlich rigoroser1 Ausweg: Es verbietet die Situation schlichtweg. Da das grundlegende Prinzip aber sinnvoll ist und in der Praxis auch hä.ufig auftaucht, niiiss JAVA eine Ersatzlösimg anbieten. Deshalb giht es die Idee der
I.r~terfo,ces. Definition (Interface) Ein Interface ist eine Sammliirig von Methodenköpfen ohne Riirnpfe. Ziisätzlich köririeri noch einige Konstanten enthalten sein.
I Interface I interf ace
I «Name» C «Methodenköpf e~ > }
I
Interfaces werden durch Klassen implementiert. Dazu muss die Klasse jeweils alle irri Interface geforderten Methoden realisieren.
Implementierung d a s s «NameK1„„»
implements «Namernte,face" C
...
}
Fiir diejenigen Methoden der Klasse, die die Interface-Methoden realisieren, gilt noch eine Zusatzbedirigung: Sie tnüssen als public gckennzeichnct sein. (Näheres dazu in Kap. 14.) Iritcriaces dienen als reine Schnit2stelle~~beschreib.1~ngcn. und sagen nichts über die zugehörigen Irnplerrientierungen aus.' Irn Gegensatz zu abstrakten Klassen, die i. Allg. zumindest eincri Teil ihrer Methodcri selbst rcalisicrcri, stellen Interfaces imr Auffoderungen,(an ihre Irnplernerltieri~ngrn)dar, gewisse Methoden verfiigbw zii machen.
' Man kann Interfaces als so etwas wie „Typen von Klassen" Typisicrungskonzept auf der nächsthölieren Ebene.
auffassen, also als ein
11.1 Mehrfachvererbung und Interfaces
193
Ebenso wie bei abstrakten Klassen ist es unmöglich, a.us Interfaces mithilfe von new Objekte zu kreieren. Darüber hinaus ist es aber auch unmöglich, aus Interfaces mittels V r r e r h n g Subklassen zu h i l d e r 2 Allerdings können Subinterfaces gebildet werden (s. unten). Ansonsten können Interfaces abcr gcnauso wic Klasscn bcnutzt werdcn: Man kann mit ihncn Variablcn ebcnso wie Resultate und Paramcter von Mcthoden typisieren, rnari kann sie in Arrays verwenden usw. Interfaces sind in verschiedenen Situationen nützlich, wie wir weiter unten anhand typischer Beispiele skizzieren werden: Man kann Klassen zusa.mmenfassen, die zwar sehr unterschiedliche Implenientierungstechniken realisieren, abcr letztlich dem gleichen Zwcck dienen (was sich in der gemeinsamen Schnittstelle widerspiegelt). Man kann von einer Klasse die Schnittstelle bekanrit geben, ohne auch ihre Implcmenticrung offen lcgcn zu müssen. Ma.n kann Anforderiingen, die Algorithmen an ihre Parameter stellen, präziser cha.raktcrisieren. In Abb. 11.1 sieht man eine typische Situation, in der mehrere Klassen die gleiche Schnittstelle realisieren. Dinge, die sich fahren lassen, brauchen Methoden wie s t a r t , s t o p , a c c e l e r a t e , t u r n . Aber die konkreten R.ealisierungen dieser Methoden sehen bei Autos anders aus als bei Flugzeugen oder Schiffen.
I interf ace Drivable j - .............................................. . ., .-. . . . . N . . . . . . . . . . . . + . . . . . . . . . . . . K . . . : . . 0
\
Abb. 11.1.Ein Interface mit mehreren Implementierungen
Progra~nmiertechnischführt das auf Besclireibirrigen folgender Bauart: i n t e r f ace Drivable ( boolean s t a r t 0 ; // Starte Motor (erfolgreich?) void s t o p 0 ; // Stoppe Motor v o i d a c c e l e r a t e (f l o a t acc) ; // Beschle~rnigen void t u r n ( i n t degree) ; // Drehen
1 Das zeigt, dass Interfaces nich,t das Problem der Mehrfachvererhung lösen. Sie schaffen aber bei einigen praktisch relevanten Anwendungssituationen einen Ersatz dafür.
11 Interfaces
194
Diese Schnittstelle kann a.uf folgende Weise implementiert werden: class CarimplementsDrivable C float CurrentSpeed = 0; ... public boolean start 0 ( . . . «Implementierung der Start-Methode)) . . .
3 public void stop () C . . . «Implementierung der Stopp-Methode)) . . .
I public void accelerate ( f loat a) ( . . . dmplementierungder Beschleunigungs-Methode))
3 public void turn (int d) C . . . ({Implementierungder Dreh-Methode)) . . .
3 )//end
o,f c l a s s
Cur
Der Modifikator public wird erst in Kap. 14 genauer beschrieben. Aber wir merken hier schon an, dass alle Methoden von Interfaces griindsä.tzlich public sind. Allerdings muss man das nur in den implementierer~denKlassen explizit hinschreiben. Im Interface selbst darf man das public weglassen; es wird vom Compiler automatisch ergänzt. Verwenden kann man die so definierten Klassen, Interfaces und Methoden in Anweisungen wie Drivable d; Car C = new Car 0 ; d = C; boolean success = C.start() ; if (success) ( d.turn(l0) ; c.stop0 ; ...
>/ / z f
Nach der Zuweisung d=c ist es in den darauf folgenden Anweisungen egal, ob wir jeweils C odcr d vcrwcridcn. Ohne dicsc Zuwcisung wäre dagegen d .turn ( 10) cirie illegale Anweisung. Interfaces mit Vererbung.
Man kann aus Interfaces zwar keine Siibklassen ableiten, aber innerlialb von 1ntcrfa.ces selbst ka.nn man Vererbungshiera.rcllier~aufbauen. Da.bei ist es irr1 Gegensatz zu Klassen sogar möglich, Mehrfachvererburig zu verwenden: -
-
11.2 Anwendung: Suchen und Sortieren richtig gelöst
195
i n t e r f a c e I extends 11, 12, I 3 {
Allerdings darf es dabei nzch,t vorkommen, dass z. B. in I1 und I 3 die gleiche Methode vorgesehen wird. Denn damit wäre ihr Ursprung mehrdeutig. Allerdings darf in I jede Methode aus 11,I 2 oder I 3 wieder. aufgeführt werden. Zusammenfassung
Die folgende Tabelle zeigt im Überblick den Vergleich von Klassen, abstrakten Klassen und Interfaces.
erlaubt e r l a b t verwen,dbar in V e r e r b ~ ~ nMeh,rfachg new vererbun,g Typisierun,g Kla,ssen J J J J abstakte Klassen J J J J In,terfuces verwendbar ZILT
11.2 Anwendung: Suchen und Sortieren richtig gelöst Bei der Behandliing von Such- und Sortieralgorithnien in Kap. 8 haben wir uns mit einem Trick aus der Affiire gezogen. Unsere Prograrrime hatten eine Form wie (vgl. Programm 8.4) p r i v a t e void i n s e r t ( lang[] a , i n t int i ; f o r ( i = W ;i >= 1; i - - ) { i f ( aCi-11
W
) {
// H?;&yri$e
// Ziel rrreicht // a[w/ jetzt a n der Slelle 2-1
l//for )//irr,sert Das heißt, wir haben das Suchen und Sortieren nur anhand von longArrays vorgeführt. Aber die algorithrnisclier~Ideen beim Suche,r/,un,d Sortieren funktionicren auf Arrays belicbigcr Daten. Egal ob ich cincn Array von longZahlen oder einen Array von Kiiilden sortiere, die Idee Ir~,,sert?;on sort oder Qwicksort bleibt immer die gleiche. In den Urzeiten der Informatik hat inan das nur dadurch lösen können, dass nia.n die eine Methodc wic i n s e r t für jcdc Art von Arra.y neu prograniniicrt - gena.ucr: abgeschricbcri und leicht adaptiert - ha.t. Das würde in JAVA dann z. B. auf folgcndc Mcthode führen:
196
11 Interfaces
p r i v a t e void i n s e r t ( Kunde[] a , i n t W ) { int i ; // Hzlfsgröge f o r ( i = W ; i >= 1 ; i - - ) { i f ( a[i-11 . l e ( a [ i l ) ) C break; ) // Ziel erreicht swap(a, i-1, i ) ; // a[w] jetzt a n der Stelle 2-1
l//for )//znsert Man sieht, dass sich hicr nur zwci Dinge ändern: Der Parainetcr ist jetzt ein Kundenarray Kunde C I a. Und der Vergleich ist jetzt nicht mehr der Operator 'aiise" ist, nach deren Entwurf konkrete Objcktc crzcugt werden (mittels new). Dabei gilt insbesondere, dass die Attribute in der Klassendefinitiori „Slotsl' beschreiben, in denen bei dcri konkreten Objekten jeweils spezifische Werte stehen. Mariclirnal rriöchte nian aber, dass ein bestimmter Slot bei allen Objekten gleich belegt ist. Dieser Wert kann sich zwar zur Laufzeit immer wieder ändern (er ist also kcine Konstante wic die Krcisza.hl T , die Gravitation g oder die Mehrwertsteuer), aber er soll sich imrner für alle Objekte auf gleiche Weise ändern. Beispiel. Ein typisches Beispiel für so eine Situation findet sich in grafisclien Darstellungen. Betrachten wir z. B. Abb. 10.1 in Abschnitt 10.1. Dort haben wir viele Bildcr von Klassen, deren grafischc Syrribole die Idee der „Blaiipa.iisenLL reflektieren sollen. Diese Synibole sind in1 Zeiclienprograrnm - Objekte einer Kla.sse Blueprint. Die GröBe dieser Symbole variiert i. Allg. aufgrund der unterschiedlich langen Klassennarnen. Aber innerhalb eiricr Grafik sollten sie aus ästhetischen Gründen gleich groß sein (bestimmt durch die Länge des längsten vorkornnienden Narnens). Das heiKt, die Attribute width und height aller Objekte der Klasse Blueprint innerlialb einer Grafik solleri gleich sein wenn auch ggf. von Grafik zu Grafik anders. -
-
204
13 U n d d a n n war d a noch . . .
Damit solche Probleme leicht zu lösen sind, stellt JAVA ein spezielles Feature bereit: Statzsche Attrzbute. Die Situation des obigen Beispiels lässt sich durch eine Klassendefiriitiori folgendcr Bauart beliaridelri: class Blueprint { static int width; static int height; String name ; ... )//end of class Bluep+nt
// gemeinsame Breite aller Objekte // gemeinsame Höhe aller Objekte // Nume (.uersctuieden für jedes Objekt)
Nehmen wir a.n, wir haben zwei Objekte dieser Klasse eingeführt: Blueprint one = new ~lueprint () ; Blueprint two = new Blueprinto;
Dann können wir folgende Zuweisungen vornehrncn (unter der Annahrnc, da.ss die Funktion wd die Breite eines Strings liefert): one.name = "Klasse"; two.name = "NochEineKlasse"; one.width = max( wd(one.name) , wd(two.name) ) ;
Nach dieser Zuweisung erhält rriari auch beim Zugriff two .width den gleichen Wert wie bei one .width. .JAVAgeht sogar noch einen Schritt weiter: Da die statischen Attribute direkt zur Klasse selbst assoziiert sind, kann man auch direkt iiber den Klassennamen auf sie zugreifen (was bei Attributen, die sich von Objekt zu Objekt unterscheiden können, nicht sinnvoll ist). Folgende Zugriffe sind also gleichwerlig: . . . one .width . . . . . . two.width . . . . . . Blueprint .width . . .
// // gleichwertig //
Definition: Ein statisches Attribut (auch statische Variable oder Klassenaariable genannt) „lebt1'in der Klasse selbst und wird von allen Objekten der Klasse gerneinsam benutzt. Statische Attribute werden durch das Schlüsselwort static ausgezeichnet. Sie können sowohl über die Objekte der Klasse als auch über die Klasse selbst arigesproclleri werden. 81 Sta.tisclie Attribute haben einige typische Anwendiiiigsbereiche: 0 0
Eine Kla.sse kann über alle für sie kreierten Objekte Buch führen. Eine Klassc kann objcktübcrgreifcndc Inforrnationcn haltcn. Bcispiel: class Bird 1 static String[] BirdTypes;
13.1 Einer für alle: s t a t i c
205
Damit stellt die Klasse eine Liste aller bekannten Vogelnamen bereit, auf die die einzelnen Bird-Objekte - insbesondere bei ihrer Erzeugung zugreifen können. Mari kann ,,klassenweiteLL Konstanten definieren. s t a t i c f i n a l float EARTH-GRAVITY= 9.8lF; -
Nicht nur Attribute, auch Methoden können statisch sein:
Definition: Eine statische Methode gehört zur Klasse und nicht zu den einzelnen Objekten. Statische Methoden werden ebenfalls durch das Schliisselwort s t a t i C ausgezeichnet. Statische Methoden können nur statische Attribute benutzen und andere statische Methoden aufrufen (denn es wäre nicht klar, von welchen1 Objekt die objektspezifischen Attribute und Methoden genornrnen werden sollten). Anmerkung: Jetzt wird die Konvention klar, dass man die Startklasse eines Programms nur verwendet, u m ein Objekt einer zweiten Klasse zu erzeugen, das dann die eigentliche Arbeit übernimmt. Denn die Startrnethvde main hat die Form public s t a t i c void main ( . . . )
C
...
Das bedeutet, dass aus main heraus wieder nur statische Methoden aufgerufen werden können. Deshalb miiss man in main schnell ein „Programmol-~jekt"generieren, mit dem man Aexibel arheiten kann.
Während die Nützlichkeit der statiscllen Attribute iirmiittelbar einsichtig ist (iiber sie stehen allen Objekten gemeinsame Informationen zur Verfügung), ist das bei Methoden nicht so klar. SchlieHicli sind Methoden ohnehin für alle Objekte gleich. Der wesentliche Vorteil liegt darin, dass sta.tische Methoden analog zu statischen Attributen direkt über die Klasse a.ngcsprochen werden können und nicht notwendigerweise Objekte benötigen: -
class C C s t a t i c void f oo
0
.. .)
)//end of dass C Jetzt können wir die Methode direkt mit C. f oo () a.iifrufen. Das heifit, es ist nicht zwingend notwendig (aber natürlich möglich) zuerst ein Objekt C C = new CO zu kreieren, uni dann a.ufzurufen C . f oo 0. Das wird vor allem in sog. Utility-Klassen gerne benutzt. Beispiele sind JAVA-Staridardklassenwie Math, // mathematische Funktionen System, // Systerr~/i,.r~form(j,tionen (Ncl.;mer/.von E/A-Kanülen etc.) oder die Spezialklassen für dieses Buch Terminal // simple Ein-/Ausgabe Pad // simple Grafik
206
13 Und dann war da noch . . .
13.2 Initialisierung Durch die Uritersclieidimg in statische und riorrnale Klassenattribute wird die Frage der Initialisierung relevant. Betrachten wir ein schematisches Beispiel: class C C s t a t i c i n t X = 1; int y = 2; // Konstr~~ktor C 0 C ... 1
Die statische Variable X wird auf 1 gesetzt, sobald die Klasse geladen wird. Die Objektvariable y wird dagegen erst auf 2 gesetzt, wenn der Konstruktor ausgeführt wird, also bei C C = new CO. Übrzgens: Irn Gegensatz zu lokalen Variablen von Methoden werden Klassenattributc (egal ob statisch odcr nicht) auch dann initialisirrt, wenn der Prograrnruierer keine Initialisieriing angibt. Abhängig von1 Typ wird vorn Compiler ein Defaultwert genommen: bei Referenztypen der Wert n u l l , bei zahlartigen Typen der Wert 0 . Anmerkung: Es gibt in JAVA auch noch die Möglichkeit, ganze Initialisierungsblöcke anzugeben, in denen mehrere Attribute gemeinsam mit komplexeren Berechnungen initialisiert werden können. Das ist aber ein so fehleranfälliges Feature, dass es praktisch nie benutzt wird.
13.3 Innere und lokale Klassen Es gibt Anwendungen, in denen man Klassen ausschlieBlich als Hilfsklassen fiir andere Klassen braucht. (Eine solche Situation werden wir in Abschnitt 16.2 vorfinden.) Daraus hat man in JAVA die Konsequenzen gezogen und Klassen genauso flexibel gemacht wie Methoden und Variablen.
Definition (innere und lokale Kla.ssen) Eine innere Klasse wird innerhalb einer anderen Kla.sse deklariert. Sie kann auch als s t a t i c gekennzeichnet sein. Eine lokale Klasse wird innerhalb einer Methode odcr eines Blocks deklariert. ( s t a t i c macht hier keinen Sinn.) Ein inneres Interface ist automatisch s t a t i c . In inneren Klassen sind alle Attribute und Methoden der umfassenden Klasse sichtbar imd somit direkt verwendbar. Beispiel:
13.4 Anonyme Klassen
207
class A C i n t a = X . . .»; i n t f o o ( i n t X) C . . . ) dass I C // innere Klasse i n t i = foo(a) ;
)//end of class I
Inncre Klassen dürfen keine static-Elcnlcnte besitzen. Außerdem sind innere Klassen de ,facto immer pnivat. Für t h i s , new und super (s. Abschnitt 10.2.4) gibt es eine erweiterte Syntax. So kann nian irr1 obigen Beispiel z. B. innerhalb von I schreiben t h i s . i = A . t h i s . a. Denn t h i s . a alleine geht nicht, weil I kein Feld nanieris a hat. Also wird t h i s erweitcrt zu C lassname.this, wobci classname cinc umfassende Klasse ist. (Für wciterc Details und Zusatzc vcrweiseri wir auf die .JAVA-Doki~rnentation in der Literatur.) Lokale Klassen wcrden inncrhalb von Methodcn/Blöcken dcfinicrt. Sie sind daher in völliger Analogie zu lokalen Variablen auch nur in diesen Metlioden/Blöcken sichtbar. Solche lokalen Klassen können in ihrer Deklaration auf alle Elemente zugreifen, die an dieser Stelle bekannt sind, sogar auf die (mit f i n a i gekennzeichneten) lokalen Konstanten und Parameter der Methode, aber n,icht auf ihre Variablen. Beispiel: -
-
void f oo ( i n t a , f i n a l i n t b) i n t X = «...B; f i n a l i n t y = «. . .»; dass I
C
C // lokale Klasse
intg=b+y; -., )//end 0.f class I U
,
-7.
// FEHLER!!!
l//foo Es gelten die gleichen Restriktionen wie fiir innere Klassen. Modifikatoren wie p r i v a t e sind verboten (wic auch bci lokalen Variablen).
13.4 Anonyme Klassen Anonyme Klassen sind wie lokale Klassen, aber ohne die Notwendigkeit, sich für sie einen Namen ausdenken zu müssen. Dazu muss natürlich die Syntax für den new-Operator erweitert werden.
208
13 Und dann wa.r da noch . . .
1 ~ n o n y r n eKlasse
I
new ~ N a m e ~ ~( „. ) ( «Klassenrumpf» 1
Es gelten die gleichen Restriktionen wie für lokale Klassen. AuiSerdem gibt es (offensichtlich) kein,en, Kon,str~~ktor. Damit diese Konstruktion leserlich bleibt, sollte ma.n sie nur bei ganz kurzen Klasscnrümpfen (ein paar Zeilen) verwenden. Typischerweise wird dieses Feature im Zusaninierihang mit grafischen Benutaerschriittstelleri eingesetzt. auf Maus-Aktionen oft folgende Situation, in So hat man z. B. zum ,,J30rchenLL der cinc lokale Klassc nur kreiert wird, um sie sofort anschlieBend mit genau einem Objekt zu instanziieren. voidfoo ( . . .) C
... class MyListener extends MouseAdapter // lokale Klasse public void mouseClicked( . . . ) ( dosomethingo ; 3 }//end of class MyListener window. addMouseListener( new MyListenerO ) ; // Objekt-hstanz
Unter Verwendung einer anonymen Klasse kann die Klassendeklaration in die Objektgerlerierung mit new hineingezogen werden. void foo ( . . . ) C
... window.addMouseListener( new MouseAdapterO public voidmouseClicked(
. . .) C
// anonyme Klasse dosomething0; )
)//end of anonymous dass of function addMouseListener
) ;//end
Man beachte die Folge J);", die mit „Iuzunächst den Rumpf der anonymen Klasse abschliefit, dann mit „)" die Argunientliste von addMouseListener zumacht und schlieBlich mit dem Serriikolori die Anweisung beeridet. Man bea.chte, da.ss irrlrrler eine Superklasse oder ein Iriterlace angegeben sein muss, weil sonst die Typisierung völlig unklar bliebe. Ob diese ,relativ mystische Nota.tion wirklich den, Aui~uandwert ist, lassen, wir einmal dahingestelt . . . .
13.6 Anwendung: Methoden höherer Ordnung
209
13.5 Enumerationstypen in Java 1.5 Das neue JAVA 1.5 sieht ein weiteres nützliches Konstrukt vor, nämlich sog. Enumerationstypen. (Gcnau genommen ist dieser Name irreführend, weil es sich in Wirklichkeit um eine Abkürzimgsnotation fiir bestimmte Arten von Klassen handelt.) Ein typisches Beispiel ist der Typ AmpelFarbe: enum AmpelFarbe ( r o t , g e l b , grün ) ; Hier wird eine Klasse AmpelFarbe eingeführt, die die Mitglieder r o t , g e l b und grün besitzt. Diese Mitglieder können dann in switch-Anweisungen, in f or-Schleifen (vor allem in den neuen Varianten von JAVA 1.5), in geiierischeii Typen usw. sehr gut verwendet werden. Ein typisches Beispiel sieht folgendermafien aus (wobei wir die neiie f or-Schleife von JAVA 1.5 benutzen s. Abschnitt 16.5): f o r ( AmpelFarbe f a r b e : AmpelFarbe. values ( ) ) 1 Terminal . p r i n t l n ( f arbe) ; 1/ / f o r Als Ergebnis werden nacheinander die drei Namen r o t , g e l b und grün auf dem Terminal ausgegeben. Wie man hier sieht, gehört zu jedem Enumerationstyp die Methode values 0, die einen Array liefert, in dem alle Mitglieder in der Reihenfolge ihrer Deklaration enthalten sind. Das obige Beispiel zeigt nur die elementarste Form eines Enumerationstyps. Das Konzept erlaubt noch viele trickreichc Variationen wie z. B. die Assoziation besti~nmterWerte mit den Mitgliedern. -
enum Coin penny ( I ) , n i c k e l ( 5 1 , dime(iO), quarter(25) ; // die Mitglieder Coin ( i n t value ) ( t h i s .value = value; ) // Konstruktor private f i n a l i n t val ; // in,terner Wert p u b l i c i n t value 0 r e t u r n t h i s . v a l ; ) // Wert liefern ) ;//end of enum Coin Damit kann man z. B. schreiben Coin C = n i c k e l ; int V = c.value0; Eine detaillierte Diskussion aller Features der enum-Konstruktion geht über ein Einfülirungsbuch hinaus. Deshalb verweisen wir auf die entsprechende Dokumentation zu JAVA 1.5.
13.6 Anwendung: Methoden höherer Ordnung Wir wollen anhand eines praktischen Beispiels zeigen, dass die Programmiermittel der inneren, lokalen und anoiyrrleri Klassen in gewissen Anwendungen durchaus niitzlich sein können.
13 Und dann war d a noch . . .
210
Ein schwerwiegender Mangel von JAVA ist, dass Funktionen nicht als Parameter an andere Funktionen übergeben werden können.' Wir sind zum ersten Mal auf dieses Defizit gestofien, als wir in Kap. 9 Prograrnrne für das Differenzieren und Integrieren entworfen haben. Aber auch in Kap. 11 hatte sich gezeigt, dass man beim Sorticren manchmal die Vergleichsoperation als Argument mitgeben miisste. Als besonders lästig wird sich das Fehlen dieses Konzepts später noch erweisen, wenn wir grafische Beilutzerschnittstellen (GUIs) behandeln. Zum Glück lässt sich das Defizit mit den1 Mittel der Interfaces wenigstens teilweise beheben, wenn auch sehr umständlich und „geschwätzig". Wir erinnern an die Programme zum Differenzieren (Programm 9.6 in Abschnitt 9.4) und Integrieren (Prograrnrn 9.7 in Abschnitt 9.5). Diese Programme definieren die beidcn zentralen Funktionen doublediff ( F u n f , doublex) { ... ) double i n t e g r a l ( Fun f , double a , double b ) ( . . . )
Dabei hatten wir die konkrete Funktion f , die differenxiert oder integriert werden sollte, mithilfe einer Klasse der Art c l a s s Fun { double apply ( double )//end of class Fun
X
) ( r e t u r n Math.exp(2*x*x)
/ (x+l); )
in ein Objekt eingebettet. Mit dieser Klasse liefert dann z. B. Fun f = new F u n 0 ; double y = i n t e g r a l ( f
, 0 , 1);
1
dcn Wert des Integrals J . &c."."'dz.
Was aber, wenn wir auch noch das
Integral: :J sin g d z brauchen? Die Kla.sse Fun ist ja schon für den ersten Ausdruck verbraucht. 13.6.1 Fun als Interface
Die Lösung liegt offensichtlich in1 Konzept der Interfaces. Wir fiihren Fun nicht als Klasse, sor~dernals Interface ein. i n t e r f ace Fun { double apply ( double
X
);
1 Die Methoden d i f f und i n t e g r a l bleiben unverändert, aber der Paranietertgp Fun bezieht sich jetzt auf dieses Interface. Auf dieser Ba.sis können wir verschiedene Funktionen in jeweils eigene Klassen packen, die als Implementierungen des Interfaces Fun gekenrixeichriet werden. Beispiele sind etwa Es ist unverständlich, wasum dieses Feature beim Design der Sprache weggelassen wurde, obwohl es seit Jahrzehnten zum Standardrcpertoire von Programmiersprachen gehört.
211
13.6 Anwendung: Methoden höherer Ordnung
c l a s s ExpFunl implements Fun ( p u b l i c double apply ( double X ) C r e t u r n ~ a t hexp(2*x*x) . / (x+l) ;
3 3 c l a s s Sin3 implements Fun C p u b l i c double apply ( double x ) C r e t u r n Math. s i n ( x / 3 ) ;
1 3 Die beiden obigen Integrale werden dann durch folgende Aufrufe berechnet: double y 1 = i n t e g r a l ( new ExpFunl () , 0 , 1 ) ; double y2 = i n t e g r a l ( new S i n 3 0 , -Math.PI , Math.PI ) ; Dabei ha.ben wir den bcidcn Funktionen - gcnauer: Funktionsobjekten keine eigenen Namen gegeben, sondern den new-Operator direkt als Argumenta.usdruck verwendet. Das ist zwar alles ziemlich unelega.nt und länglich (engl. „cl~rm,sy"),aber es ist wenigstens machbar. -
13.6.2 Verwendung anonymer Klassen
Manche Leute enipfiriden es als lästig, für jede Funktion einen neiieri Klasseniianieri erfinden zu müssen, obwohl man die Funktion doch nur einmal als Parameter z. B. von i n t e g r a l benötigt. Um dicsc Faulheit zu unterstützen sieht JAVA das Mittel der anonym,ea Klassen vor. An Stelle unseres Beispiels i n t e g r a l (new ExpFunl() , 0 , 1 ) könnte man auch schreiben double y l = i n t e g r a l ( new F u n 0 ( p u b l i c double apply ( double X ) 1 r e t u r n Math. exp(2*x*x) / ( x + l ) ;
~,//~PP~Y 3,//Fun 0 , 1);
Was passiert hier? Hinter dem new-Operator geben wir das Interface Fun an was eigentlich bei Interfaces gar nicht erlaubt ist. Es ist aber in diesem speziellen Fall zulässig, weil sofort anscliliefierid die Definition der irnplernentierenden Klasse folgt, allerdings nur der Rumpf; Namen bekommt diese Klasse keixicn. -
Anmerkung: O b sich - angesichts der horriblen (Un-)Lesba,rkeit - die Aufnahme dieses Features in die Sprache lohnt, bleibt zweifelhaft. Und das umso mehr, weil das Ganze nur ein Ersatz für Funktionen als Parameter ist, was in fast allen anderen Sprachen ganz natürlich und unspekta.kular gelöst ist.
212
13 Und dann war d a noch . . .
13.6.3 Interpolation als Implementierung von Fun
In Abschnitt 9.6 haben wir in Programm 9.8 ein Verfahren gezeigt, mit dem zu einer gegebenen Menge von Stützstellen (Messwerten) ein interpolierendes Polynom bestimmt werden kann. Die wesentliche Funktion zur Berechnung des interpolierendcn Wertes haben wir dort mit apply bezeichnet. Wenn wir das Programm jetzt noch tcclinisch so adaptieren, dass CS die Form hat class Interpolationimplements Fun ( public double apply ( double
X )
1. . .
}
... ) / / e n d of class Interpolation dann können wir die interpolierte Funktion sogar differenzieren und integrieren (obwohl wir sie gar nicht selbst kennen). Bei dieser Anwendung erscheint der Trick mit dem Interface Fun überhaupt nicht mehr als „Overkill".
13.7 Ein bisschen Eleganz: Methoden als Resultate Im vorigen Abschnitt haben wir uns mit dem Problem von Methoden als Parameter anderer Methoden befasst und gesehen, dass das im Gegensatz zu den meisten anderen Programmiersprachen - in JAVA nur mühselig simuliert werden kann. In vielen Sprachen kann man noch einen Schritt weiter gehcn und sogar Funktionen schreiben, die als Resultat neue Funktionen oder Prozeduren erzeugen. Man spricht dann von Funktionen h ö h e r e r Ordnung.%ebraucht werden solche Funktionen höherer Ordnung in mathematischen Anwendungen ebenso wie z. B. zur Programmierung von allgemeinen ,Qretty-printing1'Verfahren zur cxternen Darstellung von internen Daten. Besonders nützlich sind sie auch zur Irnplc~ricnticrungvon gcnerellen Programmen für Standardalgorithmeri wie „A~iflistungaller . . .", „Summe über alle . . . ", „ Teilmenge aller . . ." usw. Weriri in JAVA schon Methoden als Parameter fehlen, ist es nicht iiberraschend, da.ss auch Methoden als Resultate nicht eingebaut wurden. Aber die Iriterfaccs bieten auch hier einen ,,Workaround". Auf Grund der Bedeutung für viele Anwcndungcn wollen wir diesen Workaround hier wenigstens skizzieren, auch wenn die Eleganz zu wünschen übrig lässt. Beispiel: Wir illustrieren das Konzept anhand rnathematischcr Beispicle. Wenn eine reelle Funktion f (X) gegeben ist, können wir sie z. B. verschieben, -
"n
einigen klassischen Sprachen wie PASCAL oder C sind diese Möglichkeiten zwar eingebaut, aber nicht besonders gut unterstützt. In den neuen funktionalen Programm.iersprach.en wie ML [35],H A ~ K E L L[471 oder OPAL [39] sind sie dagegen ein zentrales Konzept, das entscheidend zur Eleganz und Kompaktheit der Programme beiträgt.
13.7 Ein bisschen Eleganz: Methoden als Resultate
213
spiegeln oder strecken (siehe Abb. 13.1).In der Schreibweise der Mathematik
(b) m,irror
(a) shzft
(C)stretch
Abb. 13.1. Einige Manipulationen reeller Funktionen
die auch die Schreibweise funktionaler Programmiersprachen ist - können wir die abgeleitete Funktion g jeweils folgendermaßen definieren: d. 11. g(x) = f (X - A) = ~ h i f l ( fA) , g = mirror( f ) d. h. g(x) = f (-X) g = stretch( f , T) d. h. g(n:) = f (%/T) Dabei sind shzft, mirror und stretch Funktionen höherer Ordnung, die jeweils einer gegebenen Funktion f eine entsprechende Funktion g zuordnen. Wie können wir das in JAVA simulieren? Wir wählen zur Illustration die Funktion shift. Diese Funktion braucht zwei Parameter: eine reelle Funktion f : W -t W und einen reellen Wert A E W. Letzteres gibt es in JAVA, Ersteres müssen wir iiber Objekte sirriiilieren. Das heiBt, wir bra.uchen ein Objekt, in das die Fnnktion „eingebettetu ist. Damit kommt wieder iinser Interface Fun zum Einsatz. Mit seiner Hilfe können wir die Funktion sh,ift adäquat typisieren und programmieren. (Der Modifier f i n a l wird aus technischen Gründen vom Compiler gefordert.) -
Fun s h i f t ( f i n a l F u n f , f i n a l double d e l t a ) C r e t u r n new Fun() 1 p u b l i c double apply (double X) C r e t u r n f . apply (X - d e l t a ) ; 1//o,ppl:y 1;//Fun )//sh,ift Hier wird lokal innerhalb der Methode s h i f t eine anonyme Klasse als Implementierung des Interfaces Fun deklariert. Innerhalb dieser Klasse wird die vorn Interface geforderte Funktion apply so definiert, wie es die Idee von s h @ verlangt, riäriilich als f ( X - A). Weil die Argumentfunktion f in ein Fun-Objekt eingebettet ist, müssen wir ihre Applikation mittels f .apply ( . . . ) realisieren. Wie sehen jetzt mögliche Applikationen aus? Neliinen wir als Beispiel die Definition cos = shift(sin, (auch wenn es aus Gründen der niirnerischen -
-
-
-5)
214
13 Und dann war da noch . . .
Exaktheit keine gute Definition ist). Zunächst müssen wir die Funktion s i n in ein Fun-Objekt einbetten. Das kann niithilfe einer anonymen Klasse geschehen: Fun s i n
=
new F u n 0 C p u b l i c double apply (double X) C r e t u r n Math. s i n ( x ) ; >;
Dann können wir die Funktion cos mittels s h i f t generieren, allerdings wieder eingebettet in ein Fun-Objekt: Fun cos
=
s h i f t ( s i n , -Math.P1/2) ;
Wenn wir diese generierte Funktion anwenden wollen, niüssen wir das natürlich mittels der apply-Mcthodc des Objekts cos tun. doublez
=
cos.apply( . . .) ;
Wie man an diesem Beispiel sieht, braucht das Prinzip der Funktionen höherer Ordnung in JAVA einigcn ~iotationcllcnAufwand. Auch w e m rnari das unschön oder gar abschreckend finden mag, entscheidend ist, dass cs iiberliaiipt geht und somit diese wichtige Programmicrteclinik dem JAVAProgrammicrcr nicht gänzlich verwehrt bleibt. Anmerkung: Aus Sicht eines Compilerbauers entbehrt diese Situation nicht einer gewissen Komik. .Jeder Student lernt in den Grundlagen des Cornj>ilerbans,wie man Methoden höherer Ordnung mit ciner einfachen Technik, närnlich der sog. ClosureBildung, automatisch und effizient implementieren kann. Diese Closures sind letztlich gcnau dic Tcchnik, die wir oben skizzicrt habcn. Der arme JAVA-Programmierer muss also höchst aufwendig von Hand machen, was ihm in anderen Sprachen die Compiler als Komfort bieten. Und u m die Skurrilität noch zu steigern, hat man in JAVA das gcsamte GUIKonzept u m diese Krücke herum gebastelt. Verkauft werden die resultierenden Listener und ähnliche Gebilde aber nicht als unbeholfener Workaround, sondcrn a,ls bedcutcndcs „Feature". Gute P R ist cbcn dlcs . . .
Namen, Scopes und Packages
Wer darf das Kznd beim rechten No.m.en nenn,en? Goethe, Faust 1
Gute Namen sind eine sehr knappe Ressource. Leider braucht man davon a,ber sehr viele: Namen für Klassen, für Interfaces, für Objekte, für Methoden, für Konstanten, für Variablen, fiir Parameter iisw. In groBen Softwaresystemen mit Hunderten oder gar Tausenden von Klassen gibt es daher Zelintausende von Namen. JAVA ist nicht als Lernsprache für Anfänger und Laienprogrammierer konzipiert worden, sondern als Arbeitsmittel für professionelle Software-Entwickler. Das spiegelt sich nicht mir (negativ) in einigen iirinötig sperrigen Schreibweisen wider, sondern auch (positiv) in der sprachlichen Unterstiitziing einiger wichtiger Konzepte des Software-Engineerings. Eines der wichtigsten dieser Konzepte betrifft die Sichtbarkeit bzw. Unsichtbarkeit der Internas von Modulen, Prozeduren etc. Die Grundprinzipien dieser Korixepte sind schon seit den frühesten Programmiersprachen (z. B. ALGOL oder LISP in den frühen 60er-Jahren) bekannt, a.ber JAVA hat sie elwas weiter aiisgebaiit i ~ n din die Sprache integriert, als das bisher in Sprachen getan wurde.
14.1 Das Prinzip der (Un-)Sichtbarkeit Es ist a priori hoffnungslos, ein Softwareprojekt so zu organisieren, dass alle Programmierer garantiert mit verschiedenen Namen arbeiten.' Aus diesem Grund tauchen in großen Softwaresystemen (mich schon in kleinen) dieselben JAVA wird auch zur Programmierung von „Appletsl' benutzt, die aus dem Internet geladen werdcn können. Damit sind weltweit alle Programmierer potenzielle Projektpartner.
14 Namen, Scopes und Packages
216
Namen immer wieder auf. Also ist es Aufgabe des Spra.chdesigns, mit den Namenskollisionen (engl.: name clashes) umzugehen. Das führt zu einem Satz von Regeln, nach denen Nanien sichtbar oder unsichtbar gemacht werden. Irr1 Software-Engineeririg spricht man von Hiding. Gcnaucr gcsagt, habcn wir CS mit dcm fundamentalcn Prinzip dcr sichtbaren Schn.ittstellen und verborgenen Implem,entier7~ngenxii tiin (vgl. Abb. 14.1).
Die grundlegende Idee dabei ist, dass in der Schnittstelle diejenigen Teile stehen, die nach auReri verfügbar gemacht werden, während die Irnplementierung verborgen bleibt, wodiirch dort intern benötigte Hilfsklassen, -methoden lind -variablen problcrnlos definiert werden können ohne dass man Angst vor Narnenskonflikten haben niuss.' -
14.2 Gültigkeitsbereich (Scope) Jeder Name in einem Progra.mm darf normalerweise nur in einem begrenzten Teil des Programmtexts benutzt werden.
Definition (Gültigkeitsbereich, Scope) Der Gültigkeitsbereich (Scope) eines Namens ist derjenige Bereich des Programmtcxts, in dem der Name „bekanntu ist, d. h. benutzt werden kann.
LJ Anmerkung: Der Begriff des Gültigkeitsbereichs ist genau von dem der Lebensdauer (vgl. Kap. 15) zu unterscheiden, auch wenn natürlich gewisse Zusammenhänge bestehen. Die Lebensdauer einer Variablen, einer Methode oder eines Objekts bezeichnet dcn Zcitraurn, den sie wiihrend der Ausführung des Programms existiert; sie ist also ein d,ynamistbes Konzept. Der Gültigkeitsbereich bezeichnet ein Textfragment, ist also ein statisches Konzept. Die Vermeidung von Narnenskonflikten ist nicht der einzige Grund für das HidinyPrinzip. Ebenso wiclitig ist, dass man in der internen Implementierung jederzeit Änderungen vornehmen darf. Solange diese Änderungen die Schnittstelle intakt lassen, sind sie kein Problem für die anderen Projektmitarbeiter.
14.2 Gültigkeitsbereich (Scope)
217
Was genau dieser Giiltigkeitsbereicli ist, hängt von der Art des Namens (und natürlich von der Prograsnmiersprache) ab. Abb. 14.2 illustriert die Arten von Gültigkeitsbereichen in JAVA. Die Bereiche sind dabei ineinander ge-
U Abb. 14.2. Arten von Gültigkeitsbereichen (in Java)
schachtelt, d.h., Packages (s. unten) bilden einen Scope, innerlialb von Packages bildet jede Klasse einen Scope, innerhalb einer Klasse bildet wiederum jede Metliode einen Scope und innerhalb einer Methode können noch Blöcke a.ls lokale Scopes eingeführt wcrden. Welche Regcln da.bei für die gegenseitigen Sichtbarkeiten gelten, soll im Folgenden diskutiert werden. Wir beginnen mit den Dingen, die wir schon kennen: Klassen und Methoden. 14.2.1 Klassen a l s Gültigkeitsbereich
Alle in einer Klasse definierten Attribute (also Va.riablen und Konstanten) und Methoden haben als Gültigkeitsbereich die ganze Klasse. Die Reihenfolge der Aufschreibung spielt dabei keine R.olle. Beispiel: c l a s s Foo C int a; boolean b; List 1; voidf ( . . . ) ( . . . a , b , l , f , g . . . 1 i n t g ( . . .) C . . . a , b , l , f , g . . . I
I Hier sind a , b, 1,f und g überall bekannt, dürfen also auch überall benutzt werden. Gewisse Einschränkungen entstehen nur durch lokale ,,Verschatturigen", auf die wir weiter unten eingehen werden (Abschnitt 14.2.4). Für die Attribute von Klasscn gilt übrigens cine Initialisierungsregel: Bei der Objekteraeugung werden die Attribute mit Standardwerten vorbesetzt, und awa.r boolesclic Attribute mit f a l s e , Zahlattribute mit 0 und Re-
218
14 Namen, Scopes und Packages
ferenzattribute (also alle anderen) mit n u l l . (Auf Referenzen und n u l l gehen wir in Kap. 15 ein.) Im obigen Beispiel gilt also nach der Initialisierung b == f a l s e , a == 0 und 1= = n u l l . Wenn die Variablen initialisiert definiert werden, also in einer Form wie int a=l;
haben sie natürlich die dabei angegebenen Werte. 14.2.2 Methoden als Gültigkeitsbereich
Auch Methoden induzieren Gültigkeitsbereiche, und zwar sowohl für ihre Parameter als auch für ihre lokalen Variablen und Konstanten. Beispiel: v o i d f oo ( i n t a , f l o a t b ) C i n t X; ... a , b , x . . .
1 Hier sind a , b und X im ganzen Rumpf benutzbar. (Natürlich ist auch f oo benutzbar, denn es ist ja in der ganzen umfassenden Klasse bekannt, und das schlieBt den Rumpf von f oo seiht mit ein.) Es gibt hier aber im Gegensatz zu den Klasscnattributcn - keine Initialisierung. Das heigt, X ist hier nicht mit 0 vorbesetzt; stattdessen mahnt der Compiler es als Fehler an, wenn der Programmierer nicht selbst eine entsprechende Initialisierung vornimmt, sei es gleich bei der Deklaration oder später in entspreclienden Zuweisungen. -
14.2.3 Blöcke als Gültigkeitsbereich
Die Gültigkeitsbereiclie von lokalen Variablen und Konstanten können noch weiter eingeengt werden. Denn genau genommen ist der Gültigkeitsbereich einer deklarierten Variablen oder Konstanten der kleinste umfassende Block. Dabei ist ein Block ein Programmfragriient, das in Klammern C . . .) eingc schlossen ist. (Insbesondere ist also der Rumpf einer Methode auch ein Block.) Solche Blöcke treten insbesondere bei while-, i f - und ähnlichen Anweisungen auf. Beispiel: i n t foo ( i n t a ) C i n t X =2 ; i f (. . . I C i n t y = 0; i n t z = 2; r e t u r n a * X +a * z + y ; ) else C i n t y = 1; returna * X + y - z;
1
// FEHLER! ( z unbekannt)
14.2 Gültigkeitshereirh (Scope)
219
Diese Methode hat drei Blöcke, die jeweils die Giiltigkeitsbereiclie fiir die in ihnen deklarierten Variablen darstellen. Block
Man beachte, dass die kleiden y ve7i;chiedene Variablen sind. (Die eine könnte also dcn Typ int und dic anderc den Typ String haben.) Ma.11 beachte ebenso, dass der else-Zweig nzcht zum Gültigkeitsbereich von z gehört; die Verwendung von z in der letzten Zeile ist also ein Fehler. 14.2.4 Verschattung (holes in the scope)
Lokale Variablen, Konstanten und Parameter ciner Mcthodc können Klasseriattribute verschatten. Beispiel: c l a s s Foo C int a = 100; int b = 200; int f ( int a ) { int b = 1; return a + b;
1 1 Dieses Progranini ist korrekt und der Aufruf f (2) liefert den Wert 3;denn in return a+b bezieht sich das a auf den Parameter und das b auf die lokale Variable. Man sagt, durch dic gleich benanntcn lokalen Namen en,steh,t eine Lücke i m Gültigkeitsbereich der Klassen-globalen Namen (erigl.: hole in, the scope). Eine solche Verschattung funktioniert allerdings nicht für die Blöcke innerhalb einer Methode. Beispiel: int f o o ( int a ) C int b = 0; if ( . . . ) { int a = 1; int b = 2;
// FEHLER! // FEHLER!
...
1 1 Hier I-~escliwertsich der Compiler, dass die Na.men a und b schon deklariert wurdcn. Anmerkimg: JAVA weicht hier von den Gepflogenheiten der meisten Programmiersprachen ab, indem es eine Mischstra,tegie aiis Erhubnis und Verbot der Verschattiing wählt. Ühlicherweise ist Vcrscl~attiingentweder für alle Scopes erlaubt oder gar nicht.
220
14 Namen, Scopes und Packages
14.2.5 Überlagerung Nur der Vollständigkeit halber sei hier noch eirinial an ein weiteres Fea.ture der Namensgebung in JAVA erinnert: Überlagerung (engl.: overloading). Methoden mit gleichen Namen dürfen im selben Scope koexistieren, wenn sie sich in der Anzahl undIoder den Typen ihrer Parameter unterscheiden (s. Abschnitt 3.1.4). Beispiel: c l a s s Foo ( intfoo 0 (... ) i n t foo ( i n t a) C . . . 1 f l o a t f oo ( i n t a ) C . . . 1 // FEHLER!!! i n t foo ( f l o a t a) C .. . 1 i n t f o o ( f l o a t X , f l o a t y) .. . 1
1 Beim zweiten und dritten f oo liegt ein Fehler vor, weil sie sich nicht im Pararneter, sondern nur ini Ergebnis unterscheiden.
14.3 Packages: Scopes „im GroBen" Die Gültigkeitsregeln der vorigen Abschnitte entsprechen i m Wesentlichen den Konzepten, die seit langem i n Programmiersprachen üblich sind (wenn auch mit kleinen Variationen). Aber i m modernen Software-En,gineerin,g hat m,an erkannt, dass das nicht ausreicht, u m die Anforderungen grofler Softwareprojekte zu meistern. Dem hat m a n in JAVA- zumindest partiell Rechnung getragen und weitere Konzepte zum Namensmanagement hinzugefügt. Wirklich groBe Softwareprojekte umfassen Hunderte oder gar Tausende von Klassen, was zusätzliche Strukturierungsmittel erfordert. Denn eine solche Fülle von Klassen muss organisiert werden, Namenskonflikte müssen vermieden werden und selektive Nutzung muss ermöglicht werden. Dazu dienen in JAVA die Packages (die wir in Kap. 4 schon kurz angesprochen haben). -
Definition (Package) Ein Package ist eine Samrrilinig von Klassen lind Interfaces. 0 Dem ~ ~ ~ ~ - C o r r l pwird i l e rimmer eine Datei Übergebcn. (Nach Konvcntion muss diesc Datci in dcm Suffix . j a v a enden.) Wenn man Packages schaffen will, dann muss als erste Anweisung in der Datei eine package-Anweisung stehen. package mytools; c l a s s Tooll C . . . I c l a s s T0012 C . . . I i n t e r f ace I f 1 1 . . . I Das hat zur Folge, dass die Klassen T o o l i und T0012 sowie das Iriterfxe I F I zii dem Package mytools hinzugefügt werden. Auf diese Weise können im Rahmen von mehreren Textdateien Packages Stück für Stück ausgcbaut werden (vgl. Abb. 14.3).
14.3 Packages: Scopes „im GroBen"
221
Datei 1 package mytools; c l a s s Tooll ( . . . I c l a s s T0012 C . . interface I f l C . . . I
c l a s s T0013 C . . . interface I f 2 ( . . .
_ _ _ .......... ..
1
...... '..
, I F J-) ]'; ,
............. . ., ~...____.___._..~.
I
Ifl
;
.............
................. ,. i If2 ................. .,
i
................ , " ' . ..........: ................... .. ................. . ...... ..............................................
Abb. 14.3. Dateien und Packages
Das a n o n y m e Standardpackage. JAVA generiert automatisch ein (nanienloses) Standardpackage, in das alle Klassen kommen, fiir die kein explizites Package angegeben wurde (d. h. alle Dateien, in denen keine Anweisung package « . . . » arrl Anfang steht).
Package-Namen
Package-Namen sind i. Allg. ganz normale Identifier wie z. B. mytools irr1 obigen Beispiel. Aber die Designer von JAVA haben sicli noch ein besonderes Feature ausgedacht. Da die Packages etwa,s mit den Directory-Systemen in modernen Dateisysterneri zu tun haben, lassen sicli die dortigen Strukturen in den Package-Narnen nachvollzieheri: E.i.r~Package-Name besteht aus einem, oder melrreren Identzfiern, die durch P u n k t e verbunden sind. Beispiele: mytools.texttools java .awt .event
Hier ist der Package-Name zwei- bzw. dreiteilig. Mari beachte jedoch: Da.s ist nur eine Benennungskonvention, es bedeutet nicht eine Schachtelimg von Packages. Die Packages in JAVA sind ,flachL',d. h., sie enthalten nur Klassen und Interfaces; so etwas wie Subpackagcs gibt es nicht. Anrnerkung: Da Klassen auch i m Interriet benutzt werden können, muss man ggf fiir weltweit einzigartige Benennungen sorgen. Nach dem Vorschlag von srJN sollte Inan daher den Domain-Namen der deweiligen Institution an den Anfang der Packagc-Namen stellen (und zwar invertiert). Das würde für mich bedeuten, dass n verfiigbar meine Packagc-Narricn - zumindest bei dcn Packagcs, die ich i ~ Internet niachen will s o ausseheri sollten: de. t u b e r l i n . c s .pepper. j a v a . e t e c h n i k . . 14.3.1 Volle Klassennamen
Die Packages können inirrier den Klassennarnen vorangestellt werden. Damit können sic a.uch benutzt werdcn, um Namenskollisionen auj"1~1Ösen.Nehmen wir einmal an, wir hätten noch ein weiteres Package o t h e r t o o l s , in dem ebenfalls eine Kla.sse T o o l i definiert ist. Dann können wir in einem Programrri schreiben = newmytools.Tooll~); mytools.Toollmt o t h e r t o o l s . T o o l l o t = new o t h e r t o o l s . T o o l i ( ) ; Durch die Qiialifikation rnit dem jeweilgen Package-Namen 1ä.sst sich in so cinem Fall der Namcnskonflikt auflösen. Genau genommen gilt sogar: Die Klassennamen in JAVA sind immer die ,,vollen" Namen, also einsclilieBlicli der Annotation mit den1 Pa.ckage-Namen. Aber im Interesse der leichteren Schreib- und vor allem Lesbarkeit ergänzt der Cornpilcr die Annotationen, wo immer das möglich ist. Da.zu dient das Konzept der „ImporteLL. 14.3.2 Import Da die vollständig annotierteri Namen i. Allg. viel zu lang und unlcsbar sind, gibt CS natürlich eine Abkürzungsmögliclikeit. Allerdings muss der Cornpiler dazu wissen, in welchen Packages er nach der cntsprechendcn Klasse suchen soll. Wcnn man also die Kla.ssen aus eincm Packagc in cincm Programm oder einem anderen Package verwenden will, dann muss man diese Klassen importieren. Das geschieht in einer Form wie importmytools.texttools.TextTooll; importmytools.texttools.TextTool2;
Mit diesen Import-Anweisungen werden die beiden Klassen TextTooli und TextTool2 aus dem Package mytools. t e x t t o o l s verfiighar gema.cht. Damit kann ich dann z. B. sclireiben TextTooll tt = new TextToollO ; Das ist offcnsiclitlich besser als das langc und unleserliche mytools.texttools.TextToolltt =newmytools.texttools.TextTooll(); das ohne den Import nötig wäre. Will niari alle Klassen eines Packages haben, dann kann man die ,,Wildcard"Notation benutzen: import m y t o o l s . t e x t t o o l s . * ; Irri neiien JAVA 1.5 ist auch der statische Import erlaubt, mit den1 das lästige Qualifizieren von Konstanten mit ihrer Klasse entfällt. Wenn wir z. B. schreiben
14.4 Geheimniskrämerei
223
import static java.lang.Math.*; dann können wir danach schreiben double r = cos(P1 * phi) ; Das ist besser lesbar als das alte cos (Math.PI * phi). Wirklich nützlich ist dieses Feature vor allem im Zusammenhang mit den grafischen Benutzerschnittstellen (s. Kap. 23).
14.4 Geheimniskrämerei Mit den Packages haben wir eine weitere Dimension des Narnensinanagen-lerits erhalten. Aber JAVA begniigt sich nicht damit, eine weitere Hierarchiecbene für das Scopirig einzuführen, sondern stellt zusä.txliche Sprachmittel bereit, die eine wesentlich filigranere Kontrolle über die Namensräunie gestatten. 14.4.1 Geschlossene Gesellschaft: Package
Ein Package bildet im Prinzip einen geschlossenen Nainensraum. Das heifit, a.lle im Package enthaltenen Klassen (und Interfaces) kennen sich gegenseitig und können somit ihre Methoden lind Attribute uneingeschränkt wechselscitig benutzen. Aber gegen die AiiBenwelt also andere Packages sind sie abgeschirmt (vgl. Abb. 14.2). Damit haben Packages im Normalfall also den gleichen Effekt wie Klassen, Methoden und Blöcke: Sie konstituieren einen lokalen Gültigkeitsbereich für ihre Elemente. Aber JAVA erlaubt den Prograrnrnierern bei den Packages eine wesentlich filigranere Kontrolle über die Sichtbarkeiten. Dies geschieht mithilfe von drei Schlüsselwörtern: public, protected iind private. -
-
14.4.2 Herstellen von Öffentlichkeit: public
Wir haben gesehen, dass - ohne weitere Zusätze Packages jeweils als geschlossene „schwarze Kästen" fungieren, die ihren Inhalt völlig verbergen. Damit brauchbare Schnittstellen entstehen, muss 1na.n den Modifikator public verwenden. -
0
0
Klassen u n d Interfaces, die m,it p u b l i c gekennzeichnet sind, sind uuch ( ~ u f l e r J ~ (des ~ l h Packages sichtbar. Restriktion: Innerhalb einer Datei kann höchstens eine Klasse oder ein Interface als public gekennzeichnet werden. Deshalb ist es notwendig, dass Packages über mehrere Dateien verteilt definiert werden können. Attribute u n d Methoden, die als pub 2ic gekennzeichnet sind, sind überall sichtbar; also in allen Klassen aller Packages. Na.türlich macht das nur Sinn, wenn die Klasse, in der sie definiert sind, auch public ist. Typischerweise sieht das dann so aus:
224
14 Namen, Scopes und Packages
package mytools; p u b l i c c l a s s T00111 p u b l i c i n t Max; p u b l i c v o i d f oo () ( . . . ) v o i d b a r () { . . .)
1 c l a s s AuxTool ( . . . }
Die Klasse T o o l l ist überall vcrfügbar (wo sie importiert wird). Und dann sind auch das Attribut Max und die Methode f oo bekannt. Aber b a r bleibt ebenso verborgen wie die Klasse AuxTool. Die Attribute u n d Methoden eines Interfaces gelten grundsätzlich als pub Zic, auch w e n n der entsprechende Modifikator nicht explizit angegeben ist. 14.4.3 Maximale Verschlossenheit: p r i v a t e
Während p u b l i c ma,ximale Offenheit herstellt, kann man mit p r i v a t e rriaxiniale Geheininiskramerei betreiben: Nicht einmal die Kla.ssen im eigenen Pa.ckage können dann noch zugreifen.
W e n n Attribute u n d Methoden, als private gekennzeichnet sind, d a n n sind sie n u r in der eigenen Klasse bekannt. Wenn wir also zwei Klassen der Bauart class A 1 p r i v a t e void f oo ()
1 . . .)
1 class B
1
. . .A a = n e w A ( ) ; . . . . . . a . f oo() . . . // FEHLER! (foo unbekannt)
1 haben, dann ist es in B nicht möglicli, a .f oo 0 aufzurufen. Denn die Mcthodc f oo ist außcrhalb von A nicht bekannt. Offensichtlich ist es nicht sinnvoll, den Modifikator p r i v a t e für Klassen oder Interfaces vorzusehen. Deshalb verbietet JAVA ihn aucli. 14.4.4 Vertrauen zu Subklassen: p r o t e c t e d
Neben den Extremen p u b l i c und p r i v a t e sieht JAVA noch eine weitere Variante vor: Eine Klasse vertraut den eigenen Subklassen (s. Kap. 10) und gewährt ihnen Zugriff auf Attribute und Methoden.
14.4 Geheimniskrämerei
225
Methoden und Attribute, die mit p r o t e c t e d gekennzeichnet sznd, sind i n allen Klassen desselben Packages bekannt und zusätzlich noch i n allen Subklassen (auch wenn diese auiierhalb des eigenen Packa.ges definiert sind). Diese Variante ist also liberaler als die Default-Regel (ohne jeden Modifikator) - was das Schlüsselwort protected etwas verwirrend rnacht. 14.4.5 Zusammenfassung
Aiifgrund der Fülle dieser Modifikatoren und Regeln fassen wir sie noch einmal in einem tabellarischen Überblick zusammen, wo die Elemente (Attribute und Methoden) einer Klasse jeweils sichtbar sind:
Element-Modifikator public protected private Element ist sichtbar in, . . . J J . . . der Klasse selbst J ] J J . . . den Klassen im gleichen Package J J J . . . den Suhklassen in anderen Packages J J . . . allen Klassen aller Packages -
Der Modifikator public kann auch bei Klassen angegeben werden. Dann ist die Klasse in allen anderen Packages sichtbar. (Die beiden mderen Modifikatoreri machen für Klassen keinen Sinn.)
Klassen-Modifikutor publicl Klasse ist sichtbar . . . . . . überall im cleichen Pa.ckace J I J I. . . in anderen Packages 11 J I I
Teil V
Datenstrukturen
Es genügt nicht zu sagen, was zu t u n ist, m a n muss auch wissen, womit es getan werden soll. Operationen und Daten sind zu~eiSeiten der gleichen Medaille. Neben den Algorithmen gibt es deshalb beim Prvyrum.rrt,iewn einezr~zuleiten grolßen Komplex: die Datenstrukturen. Bisher haben wir i m Wesentlichen nur zu~eiArten uon Daten,stn~kturen,kennxn, gelernt, nnmlich elemen.tare Datentypen wie int, float, double etc., sowie Arrays. Aber die Informatik verdankt ihren grolßen, Facettenreichtum mindestens in, gleichem MaJe der F U e ~ ~ o n Datenstrukturen wie der Fülle ,von, Algorith,merr. Bei diesen Dutenstr.ukt.uwr~~r~'üssen u~krzwischen zwei ,qrund,uerschie$enen, Sichtweisen unterscheiden: 0
Abstrakte Datentypen sind konzeptuelle Sichten auf Datenstrukturen. Das heilßt, die Datezr~sind Gber die Miiylichkeilen ihrer Beriutzimg (Kreierung, Art,deruag, Z ~ ~ , q r + fchamkterisiert e) ,und n,ich,t d ) e r ihre interne Darstellung i m Rechner. I n JAVA bietet das Konzept der Klassen und Interfaces für diese Sichtweise eine ideale Voraussetzung. (Historisch war das Prinzip der a.bstrakten Datenlypen sogar eines der Motive fiir die objektorien2ier~enSprachen.) Konkrete Datenstrukturen sind implemctierungstecllnische Sichten, bei denen die tatsächliche Darstellung der Daten i m Rechner betrachtet wird.
W i r werden uns von der zweiten der konkreten zur ersten der abstrakt e n Sicht.weise vorarbeiten i n der Hoffnung, dass der historische Lernprozess i n diesem Fall auch didaktisch hilft. Auj3erdem. ist zu berücksichtigen, dass die Standardbibliotheken von JAVA eine Fülle von. vordeJn,ierten Datenstrukturen bereitstellen, an denen wir m s i n unserer Disk~issionorientieren wollen. -
-
-
-
Referenzen
„Ihre persönliche Stellvertreterin". Darüber kann man ganze Abende nachdenken. Kurt Tucholsky
Eigentlich haben wir am Anfang des Buches vor allem in Kap. 1 und 2 gelogen. Dort haben wir so getan, als ob z.B. mit einer Anweisung wie new Line(new Point (I,I), newpoint (2,211 wirklich ein Objekt der Art Line entsteht, das als Attribute tatsächlich zwei Objekte der Art Point enthält (so wie in Ahb. 1.4 auf Seite 15 dargestellt). In Wirklichkeit a.rbeitet der Compiler intern aber mit etwas komplexeren und nmschinennäheren Konzepten, die allgemein als Referenzen oder Pointer bezeichnet werden. Leider lässt sich diese Tatsache auch nicht ganz ignorieren obwohl das aus softwaretechnischer Sicht zu wünschen wäre , weil es einige Effekte gibt, die dem Programmierer nicht verborgen bleiben. -
-
15.1 Nichts währt ewig: Lebensdauern Bevor wir uns mit dem eigentlichen Thema dieses Kapitels den Referenzen befasen, müssen wir noch einen Begriff ansprechen, der zum besseren Verständnis einiger Aspekte notwendig ist. Ein ganz zentrales Konzept bei der Aiwfiihrung von Progranimen ist das Pliänorrieri der „Lebensdauer". Dabei versteht man unter Lebensdauer wie auch überall sonst die Zeitspanne, während der ein Ding existiert. In1 Ziisarnmenhang mit Programmen können diese „Dingeu alles Mögliche sein, z. B. -
-
-
Das Program,m selbst; seine Lebensdauer ist jeweils die Zeitspanne vom Start bis zum Ende. Man sieht hier schon das wesentliche Grundmuster: Lebensdauern betreffen immer „Inkarnationen1':Jedes Mal, wenn ich ein Programm starte, erhalte ich eine neue Inkarnation des Programms; und die Lebensdauer ist dann die Laufzeit dieser Ir~karnat~ion. Das gilt auch fiir die weiteren Dinge wie z. B.
230 0
0
15 Referenzen
Objekte; ihre Lebensdauer beginnt mit ihrer Erzeilginig (mittels des Operators new) und endet, wenn sie „nicht mehr gebraucht" und dcshalb gelöscht werden, also spätestens mit Ende des Programms. (In JAVA übernimmt netterweise das System die Entscheidung, wann die Zeit zum Löschen gekommen ist .) Methoden; die Lebenszeit einer Methode beginnt mit ihrem Aufruf urid endet, wenn sie ihre Aktivititen beendet hat (z. B. mit r e t u r n bei Funktionen). Hier sieht ma.n deutlich, dass Lebensdauern sich auf Inkarnat,ionen beziehen. Man betrachte eine rekursive Funktion wie z. B. die Fakultätsfunktion: int fac ( int n ) C i f (n == 0 ) C r e t u r n 1; 1 e l s e
C r e t u r n n * fac(n-1) ; 1
1 Hier sind die Leberisdauern der einzelnen Inkarnationen ineinander erithalten (s. Abb. 15.1).
Abb. 15.1. Lebensdauern von Funktionsinkarnationen
Die Lebensdauer von lokalen Variablen und Konstanten ist die jeweilige Inkarnation. Bezspiel: P o i n t f oo 0 C P o i n t p = new P o i n t ( 1 , l ) ; P o i n t q = new P o i n t ( 2 , 2 ) ; r e t u r n q; 1/ / f o o Die Lcbensdaucr des Punktes ( 1 , l ) endet mit f oo, weil die Lebensdauer der lokalen Variablen p endet (urid der Punkt sonst nirgends gespeichert wurdc). Der Punkt ( 2 , 2 ) dagegen iiberlebt f 00: Zwar endet die Lebensdauer der lokalen Variablen q auch mit f oo, aber der in q enthaltene Punkt wird als Resultat nach auBen weitergereicht. Dieser fundamentale Begrifl der Lebensdauern spielt eint: zentrale Rolle zum Verständnis der folgenden Konzepte.
15.2 Referenzen: „Ich weis, wo mans findet"
231
15.2 Referenzen: ,,Ich weiß, wo mans findet" Wir haben in unseren bisherigen Programrnbeispielen schon oft Objekte kreiert, was dann i. Allg. so aussah wie Point p = new P o i n t ( . . . ) ; Dabei ist p eine Variable (entweder ein Attribut eines anderen Objekts oder eine Methoden-lokale Variable). Mit dieser Schreibweise haben wir die Vorstellung verbunden, dass das neu erzeugte Punktobjekt in die Variable also den „SlotU p eingetragen wird. Das stimmt aber nicht. Objekte sind meistens sehr groB. Dann ist es aufwendig, sie immer selbst in die Variablen (Slots) hineinzulegen. Stattdessen hantiert man lieber mit „Stellvertretern". Das wollen wir iins isn Folgenden gena.uer ansehen. Die interne Realisierung von komplexeren Objekten und Datenstrukturen basiert auf einem zentralen Konzept, das es schon seit den Tagen der ersten Computer gibt: Refereruen (a.uch Zeiger oder Pointer genannt). Letztendlich steht dahinter nichts anderes als die Beobachtung, die für Computer grisndsätzlich gilt: Alle Daten stehen irri Speicher in Zellen, die über ihre Adressen a.ngesprocheriwerden. Wenn nian von diesem Adressbegriff abstrahiert, landet man bei der Idee der Referenzen. Allerdings ist eiri solcher Abstraktionsschritt oft essenziell für die Beniitzbarkeit eines Konzepts. Im Falle der Referenzen (insbcsoridere irn Stile von JAVA) betrifft dies die Aspekte Typisierung und Sicherftwit, d. h. die Vermeidung „gefährlicheru Adressrriarii~>islatiorier~ (die z. B. in der Spra.chc C bzw. C+-'- noch möglich sind). -
-
Definition (Referenz) Eine Referenz ist eiri Verweis auf ein Objekt. (In 1361: A reference is a stron,gly typed h,an,dlc for an ohject.) Über eine Referenz haben wir also jederzeit Zugriff auf ein Objekt, ohne immer gezwungen zu sein, das (möglicherweise sehr große) Objckt selbst mit uns ,,herumzutragen“. Das legt eine Analogie nahe: Eine Referenz erfiillt den gleichen Zweck wie eine Codekarte für ein Schließfach. Vorteile: ( I ) Die Codekarte (- Referenz) kann leichter transportiert werden als das Objekt irn SchlicBfach selbst. (2) Man ka.nn -\ mehrere Codelarten für das gleiche SchlieB/ fach ausstellen und somit mehreren Leuten Zugang gewähren. Nachteile: (1) Der Zugriff auf das Objekt erfordert zusätzlichen Aufwand, da man erst an das SchlieBfach herankommen muss. (2) Es haben mehrere Leute auf das Objekt im SchlieBfach Zugriff. Punkt (2) braucht wohl eine Erläuterinig, da die Eigenschaft sowohl als Vor- als auch als Na.chtei1 gewertet wird. Die Erklärung ist einfach: Manche
232
15 Referenzen
Applikationen verla.ngen danach, dass nlehrere Leute (- Prozesse, Methoden) auf ein Objekt zugreifen können. Aber in diesem Mehrfachzugriff steckt a.iich eine Gefahr: Denn der eine kann das Objekt verändern, es herausnehmen oder sogar durch ein anderes ersetzen, ohne dass der andere das erfährt. Wenn der Zweite dann auf das „falscheLL Objekt zugreift, kann das zu sehr subtilen Programmfehlern führen. Übrigens: Die Metapher mit der Codekarte beschreibt auch gut das (oben erwähnte) Sicherheitskonzept von JAVA. Ich kann Codehrten duplizieren und weitergeben, a.ber ich kann n.icht die Coderiummer ä.ndern. Das heißt, ich kornrne mit meiner Karte nie an ein anderes SchlieBfach heran. (In Sprachen wie C hzw. C I $ ist das beliebige Manipulieren der Codekarten und damit der Zugriff auf frenide Schließfächer - fast uneingeschränkt möglich.) -
15.3 Referenzen in JAVA In JAVA gilt folgende Regel: Mit Ausnahme der elementaren Werte wie Zahlen, Characters etc. werden alle Objekte nur über Referenzen angesprochen und verwaltet. Diese Unterscheidung ist in Tab. 15.1 zusammengefasst. primitive Typen
IReferenz-Typen
lalle Klassen, insbcs. auch String und Arravs Tabelle 15.1. Typen in JAVA
boolean, char, byte, short, i n t , long, f l o a t double
.
15.3.1 Zur Funktionsweise von Referenzen
Wir können uns die Verwendung von Referenzen und die damit zilsarnmenhängenden Phänomene an einem schematischen Beispiel verdeutlichen. D a ~ ubetrachten wir wieder die Definition der Klasse P o i n t zur Beschreibung von Punkten ini zwcidirnensionalen Raum. Diese Klasse hat u. a. ... ... ...... ........ zwei Attribute („Slots") X und y, in die die x...... .................................. und y-Koordinate des jeweiligen Punktes eindouble y j ... ... ... .. ......... getragen werden. (Die weiteren Attribute und ... die Methoden z. B. dist 0 für die Entfernung vorn Nullpunkt sind für unsere folgende Diskussion nicht von Belang.) Diese Klasse können wir verwenden, um Variablen des entsprechcriden Typs zu deklarieren. (Um die Analogie zu verdeutlichen, betrachten wir ziirn Vergleich die Deklaration einer Integer-Variablen.) -
-
15.3 Rcfcrenzen in JAVA
Pointp;
I
233
intn;
Hier wird jeweils eine Variable des entsprechenden Typs deklariert. Aber diese Variablen haben (noch) keinen wohl definierten Wert! Im Falle von R e f e renzvariablen wie p wird das in JAVA durch den „Nicht-Pointer" n u l l ausgedrückt. (Deshalb heißt die Fehlermeldung bci cincm Zugriffsvcrsuch über eine solche Nicht-Referenz auch NullPointerException) Wir illustrieren nichtinitialisierte Variablen auf folgende Art:
Also niüssen den beiden Variablen durch entsprechende Zuweisungen Werte gegeben werden. (Diese Zuweisungen körineri irn Ans(:hluss an die Deklaration erfolgen oder auch als initialisiercnde Zuweisungen zusammen niit der Deklaration.) Point p; p = new P o i n t ( ) ;
I
i n t n; n = 5;
Durch den Ausdruck new P o i n t ( ) wird ein Objekt des Typs Point kreiert; das Ergebnis des Ausdrucks ist aber nicht dieses neue Objekt selbst, sondern die Referenz auf das Objekt und diese ist es, die der Variablen p zugewiesen wird. (Man beachte, dass die Attribute X und y des neuen Objekts noch keine definierten Werte haben!)
Jetzt kreieren wir jeweils eine zweite Variable und weisen ihr den Wert der ersten zu: P o i n t p; p = new P o i n t o ; Point q; 9 = P;
i n t n; n = 5; i n t k; k = n;
Als Ergebnis enthält dic neue Variable jeweils den gleichen Wert wie die alte. Aber irii Falle der Point-Variablen q ist das die gleiche Referenz; d. h., nur die Referenz ist zweimal da, das Objekt selbst wird nicht dupliziert (a.uch der primitive Wert 5 ist zweimal da).
234
15 Referenzen
Das hat natürlich tief greifende Auswirkungen auf das Arbeiten mit diesen Variablen. Nehirieri wir an, wir führen gleich noch neuc Zuweisungen mittels q bzw. k aus: Point p; p = new P o i n t () ; Point q ; q = p; q.x = 3.2;
int n = int k = k =
n; 5; k; n; 4;
Die Ärideriing in der Variablen k ha.t keinerlei Ailswirki~rigenauf der1 Wert von n, denn die beiden 5en haben nichts miteinander zu tun. Wenn wir a.ber über die Variable q ein Attribut des Objekts ändern, dann geschieht dieselbe Änderung implizit auch für die Variable p.
Diese Situation ist auch völlig in Ordnung Denn p und q haben die gleiche Referenz (in unserer obigen Analogie: den Zugangscode zum gleichen SchlieBfach). Und die Mariipulationen betreffen das refererlzierte Objekt, nicht die Referenzen selbst. Die obige Zuweisung q . X = 3 . 2 ist eben nzcht das Gegcristück zu k = 4, denn es wird ja nicht der Wert von q geändert, sondern das Objekt, auf das q unverändert zeigt. Ein tatsächliches Gegenstück zu k = 4 wäre eine Zuweisung wie q = r (mit einer geeigneten Point-Variablen r) oder q
=
new P o i n t 0. Point p; p = new P o i n t o ; Point q; q = P; q.x = 3.2; q = new P o i n t o ;
Das würde dann zu folgender Situation führen:
int n = int k = k =
n; 5; k; n; 4;
15.3 Referenzen in JAVA
235
Übung 15.1. Man betrachte eine Deklaration der Bauart Point p = new Point 151. Welche Situation ist danach entstanden? Welche Fehler können jetzt noch passieren? Was muss man tun, um diesen Fehlern vorzubeugen?
15.3.2 Referenzen und Methodenaufrufe Ganz entsprechend verhält es sich mit Methoden. Aiicli hier muss man zwei Arten von Parametern unterscheiden: primitive Werte und Referenzen. Dafür haben sich in der Literatur spezielle Begriffe gebildet.'
Definition (Call-by-value, Call-by-refererice) Wcnri beim Aufruf einer Mcthodc cin Argument direkt a,ls Wert übergeben wird, sprechen wir von Call-by-value. Wird dagegen nur eine Referenz a.uf ein Objekt übergeben, d m i i sprechen wir von Call-by-reference. Q Betrachten wir als Beispiel eine Methode void f o o ( P o i n t s , i n t i ) C . . . 1 Wenn wir jetzt cincn Aufruf . . . f oo ( p , k) ; . . . mit den Variablen p und k vom Ende des vorigen Abschnitts betrachten, darin erhalten wir folgende Sitiiatioii:
Während also für den Parameter i tatsächlich der Wert 4 selbst übergeben wird, haben wir beim Parameter s wieder nlir die Referenz auf das Objekt. Das hat massive Auswirkungen auf die möglichen Effekte in der Methode f 0 0 . Betrachten wir zwei Ariweisungcri i ~ nRumpf von f 00: I
Es gibt noch wcitcre, teils recht subtile Begriffsvariantcn, die uns aber im Zusammenhang mit JAVA nicht zu kümmern brauchen.
15 Referenzen
236
void f oo ( Point s, int i )
1 Was bedcutet das für den Aufruf . . .f oo ( p , k) . . .? Da der Parameter s die Referenz aus der Variablen p erhält, zeigt er auf das gleiche Objekt. Die Ziiweisiing s .X = 4 . 1 ändert also in der Tat die X-Komponente des Objekts ab, sodass wir nach Ausführung dcr Methode unter der Varia.blen p ein geändertes Objekt vorfinden. Anders verhält es sich dagegen mit dem Parameter i. Er bekommt den Wert der Variablen k, also die 4. Durch die Zuweisung i = 3 wird zwar lokal innerhalb der Methode f oo der Wert des Parameters i geändert2 d. h., nach der Zuweisung liefert eine Verwendung von i den Wert 3 , aber der Wcrt der Variablen k bleibt unverändert 4, auch nach dem Aufruf von f oo, sodass wir nach diesem Aufruf folgende Situation habcn: -
Anmerkung: Auch wenn wir den Parameter s von f o o mit dem Schlüsselwort f i n a l unveränderbar rnacher~,dann hat das letztlich keinen Einfluss: void f oo ( f i n a l Point s , i n t i )
1
Das Schlüsselwort f i n a l schiitzt nur den Parameter s vor einer Zuweisung der Bauart s = . . . , hat aber keine Auswirkungen auf die Änderung von Attributen des Objekts, also Dinge der Bauart s . X = . . .. Übung 15.2. Wenn wir in der obigen Prozedur f o o eine Zuweisung der Art s = new
Point O schreiben würden, welche Effekte hätte das?
"ndere Programmiersprachen vcrmcidcn das, indem sie Zuweisungen an ,,byva1ue"Parameter grundsätzlich verbieten. Aber JAVA behandelt Parameter innerhalb dcr Methode so, also ob sie lokale Variablen waren.
15.4 Gleichheit und Kopien
237
15.3.3 Wer bin ich?: t h i s Manchmal gibt es Situationen, in denen ein Objekt seine eigene Referenz kennen muss. (In unserer Metapher heiBt das: Das SchlieKfach muss seine eigene Coderiiimrner kennen.) Das wird in JAVA durch ein spezielles Scliliisselwort realisiert, das wir früher schon intuitiv verwendet haben: t h i s . Die folgenden beiden Beispiele illustrieren typische Anwendungen für diese Referenz auf sich selbst: 1. Sci C einc Klasse und f oo einc Methode von C, die als Parameter ein (anderes) C-Objekt erwartet. Um sicherziistellen, dass der Parameter nicht gerade das Objekt selbst ist, schreibt ma.n void foo ( C X ) ( i f ( X == t h i s ) (
... 1 else C ... 1
1 Der Test vergleicht, ob der Parameter - der ja eine Referenz auf ein C-Objekt ist auf das Objekt selbst zeigt. 2. Sei D eine Klasse und b a r eine Funktion, die ein D-Objekt zurückliefert (genauer: eine Referenz auf ein D-Objekt). Unter gewissen Bedingungen soll es (cine Rcferenz auf) sich selbst liefern. Das kann so geschehen: D b a r ( . . .) ( i f (. ..) ( return t h i s ; 1 e l s e ( . . . 1 -
1 Die Anweisung r e t u r n t h i s liefert gerade die Referenz auf das Objekt selbst.
15.4 Gleichheit und Kopien Die spezifischen Eigenheiten von Referenzen haben auch Auswirkungen auf die Fra.ge nach der „Gleichheitu von WertenIObjekten sowie auf das Probleni des Kopierens. Denn während bei zwei Anweisungen der Art n=5; k=n; der primitive Wert 5 problenilos in die Va.riable k kopiert wird, ist das, wie wir gesehen haben, bei Objekten nicht so: Hier werden nur die R.efcrcnzen kopiert. Hinweis: Wir erwähnen die entsprechenden Konzepte hier der Vollsländigkeit halber. Für den „Normal,qeebruuch" sind sie i. Allg. nicht wichtig.
Gleichheit
Für den Gleichheitstest hat da.s wichtige Auswirkungen. Betrachten wir folgenden Progra.mmcode. P o i n t p = new P o i n t ( 3 . 2 , 7 . 1 ) ; Point q = p; Point r
=
new P o i n t (3.2, 7.1) ;
Diese Folge von Arlweisimgen führt auf folgende Situation:
15 Referenzen
238
Wenn wir den Gleichheitstest (p==q) ausführen, erhalten wir true, während (p==r) dcn Wcrt f alse liefert. Denn p und r enthalten Referenzen auf vcrschiedene Objekte, und d a spielt es keine Rolle, dass diese Objekte zufällig die gleichen Attributwerte haben. Uni diesem Problem zu begegnen, stellt ,JAVA (in der Klasse Object) eine Mcthode equals bereit, mit der Objekte auf inh,altliche Gleichheit getestet werden. (Man sollte besser von Äquivalenz statt von Gleiclihcit sprechcri.) In unserem obigen Beispiel sollte - wenn es richtig programmiert wurde gelten: -
p,rllfaisel true
I
Damit das so ist, muss in Point die crcrbte Mcthodc von Object entsprcchend redefinicrt werden. Aber Vorsicht! Die Methode equals ist in Obj ect definiert als public boolean equals ( Obj ect o ) Wenn wir diese Methode für Point Überschreiben wollen, dann rnüssen wir sie genau mit diesem Parametertyp dcfiriiercn. Eine Definition der Art public boolean equals (Point p) würde einen zweiten, überlagerten Test einfiilireri.
Kopieren Werin wir ein Objekt kopieren (also ein Duplikat erzeugen) wollen, dann kann das wie wir gesehen haben nicht einfach durch einc Zuweisung der Art r = p erfolgen. Stattdessen rnüssen wir ein neues Objekt kreieren und dann alle Attribute kopiercn. Die Sprache JAVA unterstützt das durch die Methode c l o n e 0 aus der Klasse Object. Allerdings muss dazu die Klasse, deren Objekte wir klonen wollen, als Implementierung des Interfaces Cloneable gekennzeichnet werden. (Das hat technische Gründe; wenn man es vergisst, erhält man den Fehler CloneNotSupportedException.) In unserem Beispiel müssten wir a.lso schreiben -
-
class Point implements Cloneable
1 Dann könnten wir z. B. schreiben Point r
=
(Point) (p.cloneo) ;
1
15.5 Die Wahrheit über Arrays
239
(Die Methode c l o n e liefert ein Objekt des Typs Object. Deshalb müssen wir wie üblich in JAVA mittels Casting wieder den Typ P o i n t herstellen.) Man beachte: Die Methode c l o n e produziert eine bitweise Kopie des 01.1jekts. Das ist sehr effizient und es genügt auch in vielen Fällen (wie z. B. für unser Beispiel P o i n t ) . Wenn aber das zu kopierende Objekt Attribute hat, dir keine primitiven Werte sind, sondern (Referenzen auf) weitere Objekte, dann werden durch clone nur deren Referenzen kopiert. Wenn man diese „innerenLL Objekte auch kopieren will, muss man selbst eine entsprechende Kopieroperation schreiben. Wie wichtig das ist, werden wir in den nächsten Kapiteln im Zusammenhang mit komplexeren Datenstruktiiren sehen. -
-
15.5 Die Wahrheit über Arrays Wir hatten schon ganz am Anfang die Idee dcr Arrays cingcfiihrt (vgl. Kapitel 1.5). Sie stellen die einfachste Art dar, uni mehrere Werte oder Objekte zu einen1 neuen Objekt zusarnrnenzufassen. Um zu sehen, was bei dcr Dcklaration von Arrays genau geschieht, betrachte11 wir das folgende kleine Progranimfragmcnt. S t r i n g [ ] A = C "aO", " a l " , "a2I1, " a 3 " , "a4" S t r i n g [ ] B; B = A; BCl] = " b l " ; Terminal. p r i n t (A [I] ) ;
1;
Als Ergebnis dieses Progranirrifragments wird ' b l ' ausgegeben! Denn eine Arraydeklaration genrriert ein Arrayobjekt und liefert folglich die Referenz auf dieses Objekt zurück. Durch die Zuweisung B = A zcigcri beidc Variablen auf denselben Array:
Ganz analog ist es bei rnehrdiniensiorialeil Arrays. Hier erhält man einen Array von Referenzen auf Arrays. Das wird durch folgendes kleine Beispiel illustriert. i n t [I C 1 A = new i n t C31 C 1 ; i n t [ ] AO = C 10, 11, 12, 13); i n t [ l A l = C 2 0 , 21, 2 2 1 ; i n t [ ] A2 = C 30, 31, 32, 33, 34, 35 1 ; A[O] = AO; AC11 = Al; AC21 = A2;
Hier werden folgende vier Arra.ys generiert:
240
15 Referenzen
Damit ist auch klar, wcshalb JAVA so problcmlos mehrdimerisioriale Arrays verkraften kann, deren Komponentenarrays unterschiedliche Längen haben: Es sind alles eigenständige Objekte! Diese Technik ist etwas langsamer als die in anderen Sprachen wie PASCAL und (vor allem) FORTRAN übliche, bei der auch in rnchrdimensionalen Arrays alle Zugriffe direkt sind. Aber sie ist dafür flexibler.
15.6 Abfallbeseitigung (Garbage collection) Die Diskussion von Referenzen ist eine gute Gelegenheit, um ein spezielles Problem anziisprecheri: die Behandlung von obsolet gewordenen Objekten.
Definition (Ga.rba.gecollection) Untcr Garbage collection versteht man dic Bcscitigiing von nicht rnehr benötigten Objekten aus dern Speicher eines Programms. Durch den new-Operator wird ein ncues Objekt des cntsprechenden Typs krciert, also z. B. durch Point p = new Point 0 ein Objekt des Typs (- der Klasse) Point. Die Variable p enthält dariri die Referenz auf das neuc Objekt. Was hcifit in dicseni Ziisarnrnenhang „kreiert"? Technisch gesehen also intern in der Maschine wird genügend Hauptspeicher reserviert, um das Objekt aufzunehmen. (Wie viel gebraucht wird, kann der Compiler aus dem Typ, d. h. der Klasse, ausrechnen.) Und die Adresse dieses reservierten Hauptspeicherbercichs wird in Form cincr Refcrenz in dcr Variablcri p vermerkt. Während der weiteren Laufzeit des Prograrrims erfolgen alle Zugriffe auf das Objekt über die Referenz in der Variablen p. Wenn wir jetzt die Referenz aus der Variablen p ,,löschenu (z. B. durch eine neue Zuweisung an p), dann ist das Objekt nicht mehr erreichbar. Und das heifit, der Speiclierplatz wird unnötig blockiert. Aiis Griiriden der Ökonomie rnöclite man eine solche Verschwendung von blockiertem Speicher unterbinden. (In praktischen Anwendungen können da schon ciriige Megabytes zusammenkommen.) Ältere Sprachen wie C, C++ oder auch PASCAL haben die Verantwortung dafür dem Progra.mmierer auferlegt, der Anweisungen schreiben muss, mit denen der belegte Speicher „zurückgegeben" wird. Beim Auftreten neuer new-Operatoren wird dieser Speicher dann wieder verwendet. Diese benutzergesteuerte Wiederverwendung von Speicher ist abcr ungo mein fehleranfiillig. Denn es kann ja sein, da.ss das Objekt auch von anderen Variablen aiw noch erreichbar ist. Erinnern wir uns: Mit Anweisungen wie q = p oder a C i ] = p werden Kopien der Referenz in andere Variablen übertra,geri. Selbst wenn p dann gelöscht wird, ist das Objekt immer nocli erreichbar. -
-
15.6 Abfallbeseitigurig (Garbage collection)
241
Also muss der Prograrnrriierer genaii wissen, ob r i o d ~irger~dwoReferenzen auf das Objekt existieren, wenn er es zur Wiederverwendung zurückgeben möchte. Um in unserer früheren Metapher zu bleiben: Bevor man das Schliefifach aufgibt (also den Inhalt wegwirft und es ggf. jemand anderem zur Verfügung stellt), muss man ganz sicher sein, dass es riienianden mehr mit einer Codekarte gibt. Pr0gra.mDie Sprache JAVA hat daraus die Konsequenzen gezogen."er mierer hat keine Möglichkeit mehr, Speicher ziir Wiederverwendung freizugeben. Das Fehlerrisiko ist viel zu groß. Stattdessen führt der Compiler selbst die notwendigen Analysen durch, ariharid derer die Freigabe von Speicher erfolgt. Das kostet zwar ein bisschen Zeit während der Progranirnausfiihr11ng, aber das ist der Gewinn an Programrnsicherheit allerrial wert. Dic genaucn Techniken, mit denen diese Speichcrfreigabe erfolgt, werden unter dem Namen Garbalge collection, subsumiert. Die Details brauchen uns hier nicht zu kümmern.
" Die sog. funktionalen Programmiersprachen realisiert.
haben das schon vor Jahrzehnten
Listen
Es gibt ein sehr allgemeines intuitivw Konzept, das mit Begriffen wie ,polge~i'',„SequenzenLL oder „ListenMassoziiert wird. Man stellt sich darunter Aneinanderreihungen von Werten vor, die meistens in einer der folgenden Arten dargestellt werden:
1
17 1 - 3
1
( 17 , - 3 ,
1
I
0
1 , 0
I
0 1-231 12
1
17 1-5
1
12
1
, 0 ,-23, 1 2 , 1 7 , - 5 , 12 )
Eine solche Struktur scllcint auf den ersten Blick durch cirieri Array rcpräsciltierbar zu sein. Aber es gibt zwei Bedingur~gen,unter denen ein Array keine geeignete Darstellung liefert: Die Langc der Folge ist nicht vorhersagbar und wächst oder schrumpft dyn,am,~ischzur Laufzeit des Programms. Ein direkter Zugriff auf die Elemente im Inneren ist nicht nötig (oder braucht zumindest nicht effizient zu sein); nur die Elemente an den Erideil werden unmittelbar angesprochen. irn Folgenden betrachten wir alternative Lösungsmöglichkeiten für diese Arten von Listen.
16.1 Listen als verkettete Objekte Betrachten wir die obige Beispielliste. Eine weitere grafische Darstellung sielit SO aus: 17
1
-3
1
...
-5
Diese Darstellung passt zu folgender Sichtweise von Listen:
12
'244
16 Listen
Definition (Liste, Listenzelle) Eine Liste besteht aus „Zellen", wobei jede Zelle aus zwei Teilen besteht: - Der erste Teil ist das eigentliche Listcnclement, also der Inhalt der Zelle. -
Der zweite Teil dient der Verkettung der Liste; er ist eine Referenz auf das riächste Listenelenient (genauer: auf die Zelle für das nächste Listenrlernent) . Da die letzte Zelle definitionsgemäß keine ,pächstd Zelle mehr hat, muss dort die Nicht-Referenz n u l l stehen.
Eine Liste besteht aus einer linearen Folge von derart vcrkettctcn Zellcn.
L2 16.1.1 Listenzellen
Wir arbeiten zunächst mit der klassischen Variante, bei der Listen und Listenzellen allgemein über Elementen der Universalklasse Obj e c t definiert werden. Auf die (bessere) generische Variante von JAVA 1.5 gehen wir später noch kurz ciri. c l a s s Ce11 Object content Ce11 next Gell( Object
X,
// I n h a l t // nächste Z e l l e Ce11 n) / / Konstruktor
Dicse Klasse ist im Programm 16.1 definiert. Man beachte, dass die Klasse Ce11 ein Attribut besitzt, das selbst vom Typ Ce11 ist. Das ist abcr kein Programm 16.1 Die Klasse Ce11 für Listenzellen class Ce11 ( Object content; Ce11 next ; Ce11 ( Object X, Ce11 n ) this.content = X; this .next = n;
C
// der eigentkiche Inh.alt // dze nächste Zelle // Konstruktor
>//Ce11 > / / e n d of class Ce11
Circulus vitiosus, weil de facto ja nur eine Referenz auf eine Zelle eingetragen wird. Mit dieser Kla.sse können wir z. B. die folgende kleine Liste aufbauen:
16.1 Listen als verkettete Objekte
245
Der Code dazu könnte etwa so aussehen (wobei wir davon ausgehen, dass A, B und C gegebene Objekte sind): C e 1 1 c3 = n e w C e l l ( C , n u l l ) ; // C e 1 1 c2 = n e w C e l l ( B , c3) ; // C e 1 1 C I= n e w C e l l ( A , c2) ; //
-----------
vernünftige Variante ---
Man bea.chte, dass ma.n die Zellen der Liste von, hinten her einführen muss, weil ma.n bei der Deklaration jeder Zelle den Nachfolger braucht. Eine ebenso zulässige aber etwas fehleranfällige Variante kommt mit einer Variablen aus: -
-
C e 1 1 C = n e w C e l l ( C , n u l l ) ; // C = n e w C e l l ( B , C) ; // fehleranfällzge Varzante C = n e w C e l l ( A , C) ; //P--P
-
Das ist ein bisschen trickreich und funktioniert nur deshalb, weil in einer Zuweisung zuerst die rechte Seite ausgewertet und dann erst die eigentliche Zuweisung vorgenommen wird. Dadurch wird jeweils die (alte) Referenz aus der Variablen C in das neu kreierte Objekt übernommen, bevor die Variable mit der neuen Referenz überschrieben wird. Lästig ist in bciden Fällen, dass man die Liste von hinten her aufbauen muss. Das kann man durch folgenden Code vermeiden; Ce11
C
=
n e w C e l l ( A, n e w Gell( B , newCell(C, null)));
// // eleganteste Va/alran,te //--------------
Mari beachte, dass hier die Zelleriobjekte in einem geschachtelten Ausdruck eingeführt werden. Anrrierkung: Man sollte Listenzelle~igrundsiitzlich als Paare bestehend aus einem Inhalt und einer Verkettung beschreiben. Manche Programmierer rnachen den Fehler, a. B. bei einer Liste von Punkten hlgende Konstruktion zu wählen: c l a s s PointCell C float X; float y; PointCell next ;
...
......................... // Vorsicht: // nvise,rubler // Progrum,mie7-slzl!
.........................
> / / e n d of Point Cell Warum ist das falsch:? (Der Compiler würde CS schliei2lich problerrilos akzeptieren.) Der Grund ist, dass cs rncthodisch mangelhaft ist. Man sollte grundsätzlich eine saubere lienniing der unterschiedlichen Aspekte vollziehen: Die eigentlichen Irihalt c rnüssen als in sich abgcsclilosscne Objekte erkennbar und vemrbeitbar sein, die nichts mit der Listenorga~lisatiun zu tun haben. Und die Zellcn werden allein zurListe~iorganisationherar~gezogtm,ohne mit Aspekten der Inhalte belastet zu werden.
246
16 Listen
P r i n z i p d e r P r o g r a m m i e r u n g : Zellen sind ei,yenst&diye Typen Wenn nian Datenstruktiiren aus Zellen aufbaut, dann müssen diese Zellen eigenstä.ndige Klassen sein, in denen der eigentliche 1nha.lt genau ein Attribut ist. Alle anderen Attribute (und alle Methoden) sind ausschließlich auf den Aufbau der Struktur bezogen.
16.1.2 E l e m e n t a r e s A r b e i t e n m i t L i s t e n
Die obigen Minibeispiele zeigen bereits, dass man Listen flexibel auf- und abbauen können muss. Dazu gibt es ganz einfache Star~da.rdverfahren. Vorne anfügen Sehr oft möchte man an1 Anfang einer Liste ein rieues Element a.nfügen. Reispiel: 1@ vorher: B D 1 nachher:
B
C
D
Dieser E-tfekt wird durch folgende Anweisung erreicht: 1 = new Cell( A, 1 ) ;
Einfügen Wenn wir irgendwo irnlerhalb der Liste ein Element einfügen wollen, iniissen wir einen Zugriff auf das Vorgängerclcmcnt haben.
k nuchher:
B
C
IH D
kt+IE(i
Der Code ist a.uch hier denkbar einfach: Irn Attribut k.next steht die R.eierenz auf die Zelle mit E. Diese Referenz muss in das neiie Ol->jekt als „nächste Zelle" eingetragen werden. Und das so kreierte Objekt muss dann als neues „nächstesu in das Attribut k .next eingetragen werden. k.next
=
new Cell( D, k.next);
16.1 Listen als verkettete Objekte
247
Hinten anfügen Wenn wir arn Ende der Liste ein Elenient anfügen wollen, dann müssen wir Zugriff' auf die letzte Zelle ha.ben.
vorher:
nachher:
D
C
Der Code ist denkbar einfach. Mari muss nur beachten, dass die neue letzte Zelle eine Nich-Rcfcrenz null als „nächsteLL habcn muss. k .next
=
new Ce11 ( D, null) ;
Interessanterweise hätten wir diescn Spezialfall gar nicht zu unterscheiden brauchen, denn weil hier k.next ohnehin null ist, hätte der Codc zum Einfügen geriau den gleichen EEekt gehabt.
Löschen Das Löschen eines Elements geht ganz ähnlich. Wir lwtracliten das Eliminieren eines Elements aus dem Inneren einer Liste. Mari beachte, dass man dazu den Zugrilf auf das Vorgängerelerrierit braucht! 1 vorher:
A
nachher:'
B
C
D
E
9.i7+p+$p,22
Der Code funktioniert ebenfalls als Einzeiler:
k .next
=
k .next .next ;
Mau beachte, dass die Zelle mit D jetzt zu Garbage geworden ist (sofern tlic Referenz nicht noch über einen anderen Weg, z. B. eine andere Variable, erreichbar ist). Als Garbage wird das JAVA-Systemsie über kurz oder lang aus dem Speicher entfernen. Übung 16.1. Man überlege sich, wie das Löschen des ersten bzw. letzten Elements einer Liste aussehen muss.
16.1.3 Traversieren von Listen
Cliarakteristisch für Listcn isl, dass sie „traversiert" werden, d. 1i. Elenient fiir Element a.bgearbeitet werden. Typische Beispiele für solche Traversierungsaufgabcri sind (ähnlich wie bei Arrays):
248
16 Listen
Suche in der Liste, ob eiri bestinirrites Element bzw. eiri Element mit einer bestimmten Eigenschaft vorhanden ist. Filtere alle Elemente mit einer bestimmten Eigenschaft aus der Liste. Bilde die Summe, den Durchschnitt etc. aller Werte in der Liste. Modifiziere alle Elemente der Liste nach bestimmten Regeln (mathematisch formuliert: Wende eine Funktion f auf jedes Element an). Kopiere die Liste bzw. eine Teilliste. Hole das letzte Element der Liste hzw. bestimme die Länge der Liste. Und so weiter. Wir betrachten stellvertretend zwei dieser Beispiele. (1) Das Suchen nach einem bestirnrnten Ok~jektkann irnplernentiert werden wie in Prograrnrri 16.2 beschrieben. Dabei verwenden wir die Operation e q u a l s aus der Klasse Object zum Vergleich von Objekten (s. Abschnitt 10.2.3).
Programm 16.2 Suchen in einer Liste Ce11 search ( Ce11 s t a r t , Object X ) { Ce11 a c t u a l = s t a r t ; while ( a c t u a l != n u l l ) i f ( a c t u a l .content .equals( X ) ) { break; a c t u a l = a c t u a l .next ;
// könnte null sezn!
1
)//whzle return actual;
// gesuchte Zelle oder null
)//senrch
Diese Methode liefert entweder die erste Zelle, die das gesuchte Objekt enthält (genauer: die Referenz auf diese Zelle), oder sie liefert die Nicht-Referenz n u l l als Zeichen dafür, dass das Objekt nicht gefunden wurde. (2) Gegeben sei eine Liste von Messungen. Da.bei umfa.sse eine „MessungLL eine ganze Reihe von Informationen wie z. B. Datum, Uhrzeit, Raumte~nperatur etc., die in einer Klasse Measurement beschrieben sind. Von allen diesen Iriformatiorien interessiert hier nur der eigentliche Messwert, der ini Attribnt value steckt. Der Mittelwert der Messreihe lässt sich mit der Methode aus Programm 16.3 bestirnrrien. Mari bea,chte, dass beim Zugriff auf den Zelleriinhalt eiri explizites Casting vom Typ Object zum tatsächli//if )//mittekurert
Übung 16.3. Gegeben sei eine Klasse Auftrag, die Attribute enthält wie Kundennummer, Kaufdatum, Artikelnummer, Anzahl und Stückpreis. Es liege eine Liste solcher Aufträge vor, die nach Kundennummern sortiert ist. Aus dieser Liste soll eine neue Liste erstellt werden, die pro Kunde eine ,,Rechnungd'enthält. Dabei sei Rechnung einfach eine Klasse, die die Attribute Kundennummer und Rechnungsbetrag enthält. Übung 16.4. Bei einem Skirennen sollen die aktuellen Zwischenstände immer am Bildschirm verfügbar sein. Deshalb soll eine Liste mitgeführt werden, in denen die Ergebnisse der Teilnehmer stehen. Dabei sei ein Teilnehmer einfach durch eine Klasse beschrieben, die als Attribute seine Startnummer und seine gefahrene Zeit umfasst. Man überlege sich eine gute Organisation für diese Liste. Welche Methoden braucht man? Wie geht man mit ausgeschiedenen Teilnehmern um? Welche Implementierung sollte man wählen, um zusätzlich für jeden Läufer noch Informationen wie Name, IVationalität, Platz in der Weltrangliste etc. zur Verfügung zu haben.
16.1.4 Generische Listen
Wie schon rnelnfacli festgestellt, ist die Zellendefinition basierend auf Obj ect als T y p fiir die Elemente sowohl unhandlich (wegen der notwendigen Castings) als auch fehleranfällig (weil verschiedenartige Objcktc in eine Liste gepackt werden können, was zii hässlichen Laufzeitfchlern führt). Irn neueri JAVA 1.5 wird deshalb das Mittel der generischen Klassen bcreitgcstellt (s. Kap. 12). Das führt zu einer Variante von Prograrnni 16.1, in der die Art der Elemente iibrr einen Parameter Data dargestellt wird. Prograrnrn 16.4 enthält den niodifizierteri Code.
250
16 Listen
Programm 16.4 Die generische Klasse Cell für Listenzellen class Cell { Data content ; Cell next ; Ce11 ( Data X, Celln this.content = X ;
) {
// der eigentliche Inhalt // die nächste Zelle // Konstruktor
TJnser obiges Beispiel des Mittelwerts einer Liste von Messungen vereinfacht sich dann zu folgender Form (wobei wir nur die geänderten Zeilen zeigen). doublemittelwert ( Cell start ) (
... Cell actual= start; while ( actual ! = null ) ( Measurementm = actual.content;
// kein Casting mehr
16.1.5 Zirkuläre Listen
In manchen Anwendungen braucht man Listen, die nicht einen Anfang und ein Ende haben, sondern bei denen es nach dem Ende gleich wieder mit dem Anfang losgehen soll. Dann setzt man den Schlusszeiger nicht null, sondern lässt iliri auf das erste Elerriert verweisen. Als Beispiel können wir ein Viereck als ein geschlossenes Polygon betrachten, das aus den vier Punkten A, B, C, D besteht.
Dieses Viereck lässt sich mit folgendem Programmfragmcnt aufbauen. Cell last Cell poly
=
=
last .next = poly;
new Cell (D, null) ; // letzte Zelle new Cell( A, new Cell ( B, new Cell( C, last 1))
// Zyklus schliejlen
Dieses Vorgehen ist typisch für das Aulbauen zyklischer Strukturen. Man baut zuriächst eine nichtzyklische Struktur auf, in der die letzte Zelle (noch) den
16.1 Listen als verkettete Objekte
251
null-Zeiger enthält. Dazu braucht man eine Hilfsvariable (hier l a s t genannt). Am Ende schlieBt man den Zyklus, indcni man in der letzten Zclle den n u l l Zeiger durch eine Referenz auf die erste Zelle ersetzt. Beini Traversieren einer zirkulären Liste miss nian aufpassen, dass man nicht ewig kreist! Das wird in Programm 16.5 illustriert, das prüft, ob ein Punkt in einem Polygon vorkonnnt. Der wesentliche Aspekt in diesem ProProgramm 16.5 Suchen in einer zvklischen Liste Cell search (Cell s t a r t , P o i n t p )
C
//ASSERT s t a r t zeigt i n e i m zyklische Liste Cell r e s u l t = n u l l ; Cell actual = s t a r t ; do C i f (actual.content.equals(p)) result = actual; break;
C
>//if actual = actual .next ; 1 while ( a c t u a l ! = s t a r t ) ; return result ;
)//search
gramm ist das Kriterium zum Aufhören. Die Variable a c t u a l wandert von Zelle zu Zelle, beginnend niit der Zelle s t a r t . Wenn der gesuchte Punkt vorhanden ist, erfolgt ein Abbruch der Suche mit break. Ansonsten stoppt der Prozess, wenn die Anfangszclle s t a r t wicdcr erreicht ist. Die Variable r e s u l t enthält dann immer noch n u l l als Zeichen für „nicht gefunden". 16.1.6 Doppelt verkettete Listen
Die Listen aus dem vorigen Abschnitt haben einen gravierenden Nachteil: Ma.n kann zwar von vorne nach hinten laufen, aber man ka.nn nicht rückwärts gehen. Die einzige Möglichkeit zum Zurücksetzen wäre, ganz an den Anfang zu springen und dann erneut durcl~zulauferi. Dieser Nachteil lässt sich beheben, indem man doppelt verkettete Listen nimmt (engl.: doubly lin,ked list). Bei diesen Listen hat jede Zclle zwei Zeiger, einen zur nächsten und einen zur vorausgehenden Zclle.
Diese Art von Listen basieren auf einem Zellentyp, der in der Klasse DCell irn Prograrrini 16.6 beschrieben ist. Da,bei zeigen wir die generische Variante.
252
16 Listen
Programm 16.6 Zellen für doppelt verkettete Listen (generische Variante) c l a s s DCell ( Data c o n t e n t ; // der eigentliche Inhalt DCell p r e v ; // die vorige Zelle DCell n e x t ; // die nächste Zelle DCell (Data X , DCell p , DCell n ) C t h i s .c o n t e n t = X ; = p; t h i s .prev = n; t h i s .next
)//DCell )//end of class DCell
Der Nachteil von doppelt verketteten Listeri ist ein gewisscr Overhead an Speicherplatz, den dic zusätzlichc Rcfcrenz benötigt. Aber das ist i. Allg. vernachlässigbar. Übung 16.5. Man adaptiere die Operationen zum Einfügen, Löschen und Traversieren auf doppelt verkettete Listen. Welche zusätzlichen Methoden wird man dann sinnvollerweise einführen ? Übung 16.6. Man führe das Polygon-Beispiel aus dem vorigen Abschnitt als zirkuläre doppelt verkettete Liste ein.
16.1.7 Eine methodische Schwäche und ihre Gefahren Der Umgang mit Listen, wie wir ihn gezeigt haben, hat fundamentale Probleme! Diese Probleme sind allerdings kein Spezifikum von JAVA, sondern gelten für praktisch alle imperativen Sprachen (zu denen die objektorientierten Sprachen auch gehören) . I Eigentlich ist eine Liste eine Einheit, d. h. ein einziges Datum oder Objekt. Man kann in ihr liiri und her wandern, Inan kann die Elemente ansehen oder auch äridcrn, man kann Elemente hinzufügen usw. In dcr Implementieriuig mittels Zellen ist das aber ganz anders: Eine Liste ist dort nur als ein Konglomerat von einzelnen Listenzellen realisiert. Die Tatsache, dass diese Zellcn zusarnmcn die Idcc ciner ,ListcUrealisicrcn, liegt nur an unserem disziplinierten Umgang mit den Listenzellen. Urri das Problem zu sehen, betrachte man nur einmal folgerldc Fehlermöglichkrit. Seien zwei Listen gegeben:
I
In den sog. funktionalen Programw~iersprachen,ist. dieses Problern dagegen korrekt gclöst .
16.2 Listen als Al~strakterDatentyp ( ~ i n k e d ~ i s t ) 253
Wenn wir jetzt dic Referenzen undisziplinicrt umsetzen, können wir daraus eine Situation wie die folgeiitle lierstelleri:
Offensichtlich ka.nn man jetzt bei L 1 und L2 beim besten Willen nicht mehr von Listcn sprechen. Das ist ein grundsätzliches Problem aller Sprachen, bei denen komplexe, konzeptuell als Einheit gedachte Datenstrukturen auf dem Umweg über ein Konglomerat einzelner Zellen realisiert werden müssen. Aber zum Gliick bietet das Mittel der Klassen hier einen Ausweg. Denn indcrn wir z. B. eine Klasse LinkedList schreiben, erlauben wir auf die entsprechenden Objcktc nur noch Zugriffe iibcr geeignet eingesclirankte Methoden. Das lieiBt, wenn wir bei der Prograrnrnierung dieser Methoden die nötige Disziplin walten lassen, dann kann nichts mehr schief gehen: Alle mit diesen Methoden erzeugbarei~Strukturen sind in der Tat Listen. Entscheidend dafür ist, dass wir von außen her keinc Manipiilationsniöglichkeit der next-Zeiger mehr haben.
Prinzip der Programmierung Werin man komplexe Dateristruktiiren mitt,els Referenzen aiiflmuen will, dann niuss man sie in entsprechende Klassen einkapseln. Dabei ist sicherziistellen, dass keine Referenzen von auBen direkt nianipiilierbar sind; alle Änderungen an der Struktur dürfen nur über „sichereM Methoden erfol~en. Dieses Konzept soll im Folgenden ausgearbeitet werderi.
16.2 Listen als Abstrakter Datentyp (~inkedList) Wenn man eine Datenst,riiktiir hat,, fiir die es eine Reihe von Standardoperac tionen gibt, die in vielen Anwendungen immer wieder gebraucht werden, dann sollte man diese in einer Klasse zusam~nenfasseri.Und wenn die Klasse ganz besonders wichtig ist, kann man sie sogar in eine Bibliothek einbinden und so allgemein verfügbar machen. Für Listen gilt das ganz offcnsichtlicli, weshalb eine Klasse LinkedList in JAVA vordefiniert ist (in1 Package j ava. u t i l ) . Allerdings haben wir dahei sclion wieder das Problem, ob wir die alte Version, die noch auf Elementen der Art Object basiert, präsentieren sollen, oder schon die ncue von JAVA 1.5, in der LinkedList generisch ist. Wir entscheiden uns fiir die rrio(1erilere Variante,
254
16 Listen
deren wichtigste Methoden in Abb. 16.1 aufgelistet sind. (Man erhält die alte Form, indem ma.n wcglässt und ansonsten iiberall Data durch Obj e c t ersetzt.) Die einzige etwas merkwürdige Methode ist t o b r r a y . Sie gibt es jetzt
class LinkedList Konstruktor LinkedList 0 void addFirst (Data X) vorne anhängen void addlast (Data X) hinten anhängen Data getFirst 0 erst es El ement Data getLast 0 letztes Element Data removeFirst 0 erstes Element entfernen Data removelast 0 letztes Element entfernen void Data void void
add(int i, Data X) get(int i) remove(int i) set(int i, Data X)
int s i z e 0
an der Stelle i Element an der Element an der Element an der
einfügen Stelle i Stelle entfernen Stelle i ersetzen
Anzahl der Elemente
Object C] toArray 0 Liste in Ob j e c t -Array verwandeln Data C] toArray (Data[] ) Liste in Data-Array verwandeln
Abb. 16.1. Eine gencrischc Klasse für Listen
in zwei Versionen. Die erste liefert (wie im altcn JAVA) eincn Object-Array, in dem die Listerielemente in ihrer Reihenfolge abgelegt sind. Die zweite ist in der modernen generischen Forrn; aber bei ihr muss rnari bereits einen Array (der passenden Art) bereitstellen, in den die Listerielemente darin eingetragen werden. Ist der Array zu lang, wird er mit null-Elementen aiifgefiillt. Ist er zu kurz, wird ein neuer Array der passenden Länge generiert.' Man beachte auch die aufwcndigc Schrcibwcisc, in der ein generischer Ergebnisarray angegeben werderl rrliiss. Die Iv~plevlerr,t.l;er.un,,yerfolgt sinrivollerweise mithilfe von doppelt verketteten, Listen, und je cincm Zcigcr auf dcn Anfa,ng und das Ende der Liste.
Diese korriplexe Konstruktion hat mit technischen Schwierigkeiten des Laufzeitsystems zu t u n , die uns hier nicht zu interesieren braiicheri.
JAVA-
16.2 Listen als Abstraktcr Datentyp (LinkedList)
255
Programm 16.7 enthält eine Skizze der Irriplerricritieri~rigdieser Klasse. Dabei beschränken wir uns allerdings auf einige exemplarische Methoden, Programm 16.7 Implementierung der (generischen) Klasse LinkedList public d a s s LinkedList( private DCell first ; private DCell last ; private int length; LinkedList0 C first = null ; last = null; length = 0;
1; public void addFirst ( Data X ) 1 // vorne anfügen DCell aux = new DCell(X, null, first) ; if (first == null) { first = last = aux; 1 else C first .prev = aux; first = aux;
1////if length--;
1/ / i f return result ;
public int size 0 { return length; 1
// letztes Element entfernen
// bei einelementzger Liste
256
16 Listen
weil die meisten Methoden schon in den vorausgegangenen Abschnitten 16.1.2 bis 16.1.6 gezeigt oder als Üburigsai~f~abe gestellt wurden. Die erste Gruppe von Operationen ist sehr effizient, weil man a.uf den Zellen a.rbeitet, die durch die Zeiger f irst und l a s t direkt angesprochen werden. Man muss nur den Sonderfall der leeren Liste abfangen. Die Operationen der zweiten Gruppe, die mit Indizes arbeiten, sind dagegen ineffizient, weil man erst in einer Schleife i Zellen weit wandern muss, bevor man die eigentliche Operation ausführen kann. Um die Operation s i z e 0 effizient zu machen, wurde ein zusätzliches Attribut length eingeführt. Sehr bequem ist auch die Operation t o A r r a y 0 , mit der man bei Bedarf von Listcn zu Arrays wechseln kann. Das Interessanteste an dieser Implementierung ist aber etwa.s a.nderes: Sie zeigt ziun ersten Mal die Nützlichkeit von inneren Klassen. Wir hatten in Al->schnitt16.1.7 festgestellt, wie gefährlich da.s unkontrollierbare Manipulieren der R.efercnzen sein kann. Indem wir DCell zu einer privaten inneren Klasse machen, verhindern wir, dass irgeridjernand von außen die Listenzeiger manipulieren ka.nn. Alle Operationen auf der Liste erfolgen ausschließlich durch die von uns freigegebenen Operationen. Da.mit ist garantiert, dass wir es immer mit korrekten doppelt verketteten Listen zu tun haben. P r i n z i p der P r o g r a m m i e r u n g : Ahstmktc: Datentypen Wenn man Datenstrukturcn (wie z. B. Listcn) nur noch über ausgcwählte Methoden verarbeiten kann, da.iin erhält man eine a b s t r a k t e Sicht der Strukturen, bei der die interne Realisierung völlig verschatt,et ist. Solche abstrakten Sichten von Strukturen nennt man abstrakte Datentypen.
Übung 16.7. Man ergänze die fehlenden Methoden der obigen Klasse LinkedList.
16.3 Listenartige Strukturen in JAVA Die Klasse LinkedList zeigt eine mögliche Siclitweise für die generelle Idee „ListeL'.Daneben gibt es zahlrcichc weitere, damit eng verwandte Sichten. Die Spra.che JAVA bietet deshalb (irn Packet j ava. u t i l ) eine ga.rixe Familie von listenartigen Klassen und Interfaces, die zucina.nder in diversen Vererl-)iingsrelationen stehen. Einen Auszug aus diesem Familienbaum zeigt Abb. 16.2. Es ist eine beliebte Technik in JAVA,jedem Interface eine abstrakte Klasse zuzuordnen, in der einige der Methoden defaultmägig vordefiniert sind. Das macht es manchmal bequemer, eigene Implerrieritierurigeri der Interfaces zu schreiben: Man weist sie als Sihklasserl dieser abstrakten Dcfaultklassen aus, sodass man einige der Methoden erben ka.nn. Allerdings miss man oft
16.3 Listenartige Strukturen in JAVA
257
.............................. - ., I Collection j ................................ . .. ..n".......4.............-
V ....
...................... . ....................... .,
........................ .. ... ....................... .,
I List j ....................... ,. ........ .+. ......... .
I Set j -,... .................. ,. .......... ........... .
_I Stack
Abb. 16.2. Listenartige Klassen in java.uti1
a.uch einige der Methoden übersclireiberi, wa.s i. Allg. die Verständlichkeit des Progra~nmserschwert und die Fehleranfälligkeit erhöht. AuBerdem wird ma.n manchmal auch durch da.s Verbot der Mchrfachvercrbung an cinem Riickgriff auf die Defaultklasseri gehindert, weil man eine andere Klasse dringender erben muss. Wie man in Abb. 16.2 sieht, ist dort jedem Interface eine entsprechende abstrakte Klasse zugeordnet. Wir gehen hier nicht näher auf sie ein. (Details kann man in jeder JAVA-Dokumentationnachlesen.) Dieser Farnilieribainri vori JAVA liegt etwas windschief zu den listeriartigen Strukturen, die man in der Informatik standardmäßig verwendet, insbesoridere: 0
0
Stack („lest-in first-out", LIFO): Hinzufiigeri und Wegrielinieri findet am gleichen Erde statt. Queue („first-in fir'st-outL',FIFO): Hinzufiigcn und Wegnehmen findet an den entgegengesetzten Enden statt. Deque („double-mded qumre"): Kombination von Stack und Queue. Sequence: Zusätzlich zu den Deque-Operationen ist auch noch die Konkatenation ganzer Listen möglich.
258
16 Listen
1111 Folgeriden skizzieren wir allerdings riur kursorisch - die listcnartigcri Strukturen. (Die Klassen HashSet und TreeSet könnrri wir erst später rrläutern.) Dabei beschränken wir uns auf die „altenL'Variantcn, die rioch auf Object basieren. -
16.3.1 Collection Das Interface Collection umfasst diejenigen Methoden, die in allen Klassen verfügbar sind, die irgendwie die Idee einer „ArisarnmlungLL oder „Kollektion" von Objekten realisieren. Abb. 16.3 listet dic wichtigsten dieser Methoden auf. ..................................................................................................................................
interface Collection boolean add(0bject o) boolean addAll(Col1ection C) void clearo boolean contains(0bject o) boolean containsAll(Collection C) boolean isEmpty0 boolean remove(0bject o) boolean removeAll(Collection C) boolean retainAll(Col1ection C) int size0 Object [I toArray 0 Iterator iteratoro
...
Element hinzufügen alle Elemente hinzufügen alles löschen ist Objekt vorhanden? sind Objekte vorhanden? leer? Objekt entfernen alle Objekte entfernen andere Objekte entfernen Anzahl der Elemente in Array umwandeln assoziierter ,,IteratorU
...
..................................................................................................................................
Abb. 16.3.Das Interface Collection (alte Form)
Die Elemente in diesen Kollektionen können geordnet sein oder nicht, es können Elemente mehrfach vorllanden sein oder nicht; das ist in diesem Intcrfacc allcs offen gelassen. Einige dieser Methoden, z. B. add oder remove,haben überraschenderweise den Ergebnistyp boolean aristatt wie zu erwarten void. In diesen Fällen zeigt das Erghnis true an, dass sich die Kollektion durch die Operation geändert hat. Zum Beispiel bei Mengen heifit das irn Falle add(x), dass das Element X rioch nicht in der Menge cntha.lteri war. Irn Spczialfall der Mengen entsprechen die Methoderi addAll, removeAl1 lind retainAl1 gerade den klassischen Operationen Vereinig?~ng,Differenz und Durchschn,itt. Die Methode contains entspricht bei Mengen dem Elernenttest und containsAll dcrn Teilmengentest. Bei nicht-mengenartigen Kollektionen (wie z. B. Listen und Arrays) sind diese Operationen entsprechend zu übertragen. Jteratorenf werden wir in Ahsclinitt 16.4 diskutieren.
16.3 Listenartige Strukturen in JAVA
259
16.3.2 List Das Interface List repräsentiert eine geordnete Kollektion. Abb. 16.4 listet die wichtigsten Methoden auf, die zu denen von Collection noch hinzukommen.
i
i n t e r f a c e L i s t extends C o l l e c t i o n
...
...
boolean a d d ( i n t i , Object o) boolean a d d A l l ( i n t i, C o l l e c t i o n C) Object g e t ( i n t i ) i n t index0f (Object o) Object removecint i ) Object s e t ( i n t i , Object o) L i s t s u b L i s t ( i n t from, i n t t o )
an S t e l l e z hinzufügen an S t e l l e z hinzufügen Element an S t e l l e L, P o s i t i o n d e s Objekts (oder -1) Objekt an S t e l l e z e n t f e r n e n Element an S t e l l e z s e t z e n Teilliste
.. ......... ........................... ........ ........ .............. .. ...
Abb. 16.4. Das Iriterfacc L i s t (alte Form)
Dabei handelt es sich im Wesentlichen uni Methoden, die sich auf die Position i von Elenlenten beaiehen. Daran erkennt nmn, dass List diejenige Spezialisierung von Collection ist, in der die Elemente angeordnet sind. 16.3.3 Set
Das Interface Set repräsentiert die klassische Struktur der Mengen. Es enthält die gleichen Methoden wie Collection, verlangt aber eine andere Semantik. Bei add und addAll dürfen keine zwei Objekte xl und x2 aufgenommen werden, für die xl.equals(x2) gilt. Weil Set genauso aussieht wie Collection, brauchen wir cs hier nicht anzugeben. 16.3.4 LinkedList, ArrayList und Vector Listesiirnplementierungen sind in JAVA in drei Varia.nten verfügbar. LinkedList haben wir schon in Abschnitt 16.2 intensiv diskutiert. ArrayList stellt eine Variation dieser Implementierung dar. Der Hauptunterschied ist, da.ss der Zugriff mit get (i) und set (i ,X) sehr effizient ist, weil die interne Darstellung nicht auf verketteten Zellen basiert, sondern auf Arrays. Dafiir werden riatiirlich Operationen wie add(i ,X) lind remove(i , X ) teiirer, weil jetzt ganze Teilarrays verschoben werden müssen, um Platz zu machen oder um Lücken zu scliliefien.
f irst
last
last first
260
16 Listen
Aus den Zeigern f irst und last der Iniplementicrung in LinkedList werden jetzt Indizes. Dabei kann es passieren, dass z. B. der last-Index einen Wrap-around macht, wenn er arn Ende des Arrays ankommt und vorne noch Platz ist. Man muss also bei allen Operationen unterscheiden, in welcher der beiden oben skizzierten Situationen man ist. Der Array ist ,,vollLL, wenn nur noch ein Element frei ist. (Warum?) Dann muss man einen gröBeren Array kreieren und den alten mittels arraycopy iibertragen (s. Abschnitt 5.5). Auch das kann zu massivcn Effizienzverlusten führen. Man wird also ArrayList vor allem in den Situationen verwenden, in denen man sehr oft auf die Elemente mit get (i) oder Set (i ,X) zugreift, aber relativ selten neue Elemente hinzufügt oder bestehende Elemente löscht. Mit anderen Worten: Eerin die Liste relativ stat,isch ist. Bei sehr dynamisch vcrändcrlichcn Listen ist LinkedList besser. Weitere Details über ArrayList findct man in cntsprcchcndcn JAVADokumentationen. Die Klasse Vector ist schon lange in JAVA verfügbar (seit der Originalversion JAVA 1.0). Und cigcntlich wäre man sie gerne los. Aber wic das so ist mit Systemen, die schon lange draulSen beim Kunden sind: Die alten Sa,chen müssen weiter mitgeschleppt werden, weil sie in alten Applika,tionen noch vorkornnien. (Man spricht hier von Legacy-Software.) Besonders ärgerlich ist, dass mit dieser Klasse ein Name verschwendet wird, der in mathernatisclien Anwendungen niit Vektoren und Matrizen dringend gebraucht würde. Im Wesentliclien verhält sich Vector genauso wie ArrayList, nämlich als Array, der wachsen lind schri~nipfenkann. Aber einige Methoden existieren doppelt, einmal unter dem alten Namcn aus der Vcrsion 1.0 und cin zwcitcs Mal unter dem Namen, der zu Collection passt. Anmerkung: Es gibt cincn wichtigcn Unterschied zwischen ArrayList und Vector, der aber erst i m Zusammenhang mit parallelen Threads (vgl. Kap. 21) relevant wird. Die Methoden in Vect or sind synchronisiert (was sie allerdings auch langsamer macht).
16.3.5 Stack
-
Dcr Stack (oft auch Stapel oder Keller genannt) ist eine der ältesten und häufigsten Strukturen der Inforrnatik."ie Grurididee ist, dass man die Daten, die zuletzt hineingesteckt wurden, als Erstcs wicdcr zurückbekomn~t.Damit sind die charakteristischen Operationen gerade dic Zugriffe „an1 einen EndeLL. Im Wesentlichen werden dabei einige der Methoden von LinkedList unter anderen (nämlich den traditionellcn) Namen noch einmal bereitgestellt.
" Die Übersetzurig von (rekursiven) Funktionen und Prozcdurcn in Maschinencode basiert auf dem Kellerprinzip.
16.3 Listenartige Strukturen in JAVA
261
(
class Stack extends Vector Stack0 Konstruktor void push(0bject o) Element hinzufügen (oben) Object popo oberstes Element wegnehmen Object peek0 oberstes Element ansehen leer? boolean empty 0 int search(0bject o) Position suchen
Abb. 16.5. Die Methoden der Klasse Stack (alte Form)
Stacks spielen eine große Rolle in vielen Bereichen der Informatik, vor allern irn Coriipilerbau. Denn mit ihrer Hilfe lassen sich alle rekursiven Methoden auf elementarere Kontrolln~cchanismcnzurückführen. (Wir werden eine Anwendung dieser Technik in Abschnitt 17.3 irn Zusammenhang mit Bäumen sehen.) 16.3.6 Queue (,,Warteschlange")
--
Die Queue (oft auch Warteschlange oder FIFO-Liste genannt) ist ebenfalls eine häufig vorkommende und lange bekannte Struktur der Informatik. Die Grundidee ist, dass die Daten in der Reihenfolge, in der sie in die Queue hineingesteckt wurden, auch wieder herauskornrnen (irii Gegensatz zum Stack, bei dern die R.eihenfolge gerade invertiert wird). Damit ergeben sich die Opera.tionen aus Tabelle 16.6.
d a s s Queue extends LinkedList Konstruktor Queue() void push(0bject o) Element hinzufügen (hinten) Object popo vorderstes Element wegnehmen vorderstes Element ansehen Object peek0 leer? boolean empty 0 int search(0bject o) Position suchen
C Abb. 16.6. Die Klasse Queue (so in JAVA nicht vordefiniert)
Wie man sieht, sind die Operationen fast identisch zu denen von Stack. Der zentrale Unterschied liegt auch nicht in ihrer Anzahl oder in ihren Namen, sondern in ihrem Verhalten. Während bei Stack die Folge S .push (X) ; y = S .POP() dazu führt, dass X und y gleich sind, ist das bei Queue gerade nicht der Fall (auBer bei der eirielerrientigen Queue).
262
16 Listen
Anmerkung: Es gibt einen uralten Streit iri der Iriformatik, ob man Opcrationen, die zwar die gleiche Idee repräsentieren (hinzufügen, wegnehmen etc.), aber i m konkreten Verhalten doch verschieden sind, gleich benennen soll oder unterschiedlich. Wir haben uns hier entschieden, die analogcn Operationen von Stack und Queue gleich zu benennen. Ihre unterschiedliche Verhaltensweise kommt dadurch zum Ausdruck, da,ss dic cinen zu Stack-Objekten gehören und die anderen zu Queue-Ob,jekten.
Die Implemeritierung als Subklasse von LinkedList ist trivial. Die Methoden push, peek, pop etc. sind nur Urnbenennungen der Methoden addLast, g e t F i r s t , removeFirst etc. Im Gegensatz zum Stack wird Queue nicht irn ~ ~ v ~ - P a c k jaava. ge util bereitgestellt; man muss sie selbst prograniniieren. Anmerkung: Das ändert sich i m neiien JAVA 1.5. Dort wird ein generisches Irlterface Queueeingeführt zusammen mit einer Reihe von implementierenden Klassen. Diese Klassen sind stark an den Bedürfnissen konkurrierender Prozesse orientiert, die über Queues kornmimizierer~. Wir wSt1len in diesem Abschiitt eine einfachere Variante, die Queues „nuru als normale Datenstruktur betrachtet.
In der Informatik finden sich noch weitere listcnartige Strukturcn wic z. B. Deque (double-ended queue), die dir Kombination von Stack und Queue ist, odcr Sequence, die zusätzlich Korikateriation erlaubt. Aber diese Strukturen sind Spezialfälle von LinkedList i ~ n dbrauchen deshalb nicht extra eingeführt zu werden. 16.3.7 Priority Queues: Vordrängeln ist erlaubt Wir wollen noch eine Datenstruktur wenigstens erwähnen, die auch das Wort „Queueu im Namen trägt, aber eigentlich etwas anders implementiert wird als die obigen Strukturen. (AuBerdem hätte man sie gemuso gut Priority Stack nennen könneri, aber der Name Queue hat sich in der Literatur eingebürgert.) Die Idcc ist einfach, dass man Elemente hat, die eine „Prioritätu besitzen (z. B. einzelne Prozesse in einer Produktionssteuerung oder Ereignisse in einer Kontrollsteuerurig für Autos, Flugzeuge etc.). Wenn man hier nach dem „ersten" Element verlangt, will man nicht das zeitlich erste (wie bei der Queue) oder das zeitlich letzte (wie beim Stack), sondern das ani höchsten priorisierte. Als effiziente Implementierung wählt man hier abcr keine verkettete Liste, sondern eine baurnartige Struktur. Diese Art von Struktur haben wir schon beim Sortieren kennen gelernt. Der Heap, der beim Heapsort konzeptiiell verwcndct wird, liefert genau das, was wir fur Priority Queues brauchen. Man sollte sie allerdings anstelle von Arrays jetzt besser mithilfe von echten „Bäumen" realisieren. (Bäume sind das Thema des nächsten Kapitels.)
16.4 Einer nach dem andern: Iteratoreri
263
16.4 Einer nach dem andern: Iteratoren Schon bei den Arrays war eine der Hauptaktivitäten das Durchlaufen aller Elemente, sei es uni zu suchen, uni zu akkuniulieren oder um sie sonst wie zu verarbeiten. Dafür gibt es sogar ein Standardmuster: for (int i = 0 ; i < a.length; i++) X = aCi1;
. ..
C // Element selektieren // Verarbeiten der Arrayelemente
l/(for Auch bei den listenartigen Strukturen dürfte das Diirchlaiiferi aller Elemente zu den häufigsten Aktivitäten gehören. Also hätte man gerne eine vergleichbare Notation. Diesen Wunsch haben die Designer von JAVA erhört und Entsprechendes geschaffen, nämlich die Iteratoren. Sei z. B. eine Liste der Art LinkedList myList gegeben. Dann kann Inan alle Elemente dieser Liste mit folgender Konstruktion durchlaufen. for (Iterator i X = i.next();
... l//for
=
myList .iterator() ; i.hasNext0 ; ) (
// nächstes Element selektieren // Ele,m,ent verarbeiten
Man Iwaclite, dass das Gegenstiick zur Anweisung i++ fehlt. Der Grund ist, dass irn Rumpf bei der Anweisung X = i .next ( ) automatisch weitergeschaltet wird. Damit das funktioniert, werden zwei Dinge benötigt: 0
0
zum Ersten eine Kla.sse Iterator. Das Interface dieser Klasse ist in Abb. 16.7 angegeben. zum Zweiten eine Operation iteratoro in LinkedList, die uns ein passendes Objekt der Art Iterator beschafft. Wie man an1 Interface Collection in Abb. 16.3 sieht, existiert diese Operation sogar in allen Klassen, die wir in diesem Kapitel betrachtet haben.
I
i i i
.
interf ace Iterator boolean hasNext0 noch Elemente vorhanden? Object n e x t 0 aktuelles Element void removeo aktuelles Element entfernen
i
. ........ ,. ....,...... .... .. .,. .,...... ... ........ .. ...... ................................ ..... .... ..... ,
Abb. 16.7. Das Interface Iterator
Die Operation hasNext ist einfach. Die Methode next beschafft das nächst e Element (und schaltet intern zum Folgeelement weiter).
16 Listen
264
Dir einzige kritische Operation ist remove. Sie entfernt das gerade mit next beschaffte Objekt. Das ist die einzige sichere Methode, mit der während der Iteration Elemente aus der Kollektion entfernt werden dürfen.
ListIterator Es gibt eine Subklasse L i s t I t e r a t o r von I t e r a t o r , die zusätzlich die analogen Operationen hasprevious und p r e v i o u s besitzt, sodass man durch die Liste sowohl vorwärts als auch rückwärts laufen kann. AuBerdem gibt es Methoden add, remove und s e t . Alle drei Methoden sind aber mit Vorsicht zu genießen, weil sic merkwürdige Restriktionen haben. (Näheres kann rnan in JAVA-Dokumentationenfinden.)
Implementierung Wir wollen uns zumindest eine grobe Vorstellung verschaffen, was so ein Iterator ist. Deshalb skizzieren wir kurz seine Implementierung z. B. in1 Fall der Klasse LinkedList. (Aus Gründen der Vereinfachung lassen wir aber die Operation remove weg.) Wir beziehen uns auf das Programm 16.7 in Abschnitt 16.2 (Seite 255). In dieses Prograrnrn müssen wir cine weitere innere Klasse LinkedList I t e r a t o r einfügen. Das ist in Programm 16.8 gezeigt. Die Implementierung ist hier so, dass n e x t irrirrier eine Zelle weitergeht und deren I n l d t da.nn als aktuelles Element abliefert. Ganz am Anfang muss das das erste Element sein. Mari beachte, dass die innere Klasse Zugriff auf die Attribute der umfassenden Klasse hat. Dieses Progra.mm zeigt aber auch, wie subtil die Zusammenhänge zwischen Generizität und anderen Konzepten wie z. B. inneren Klassen sein können. Man würde erwa.rten, da.ss man auch die innere Klasse generisch definieren muss, also in der Form LinkedList I t e r a t o r < D a t a > . Darauf reagiert der Compiler aber mit einer Fehlermeldung! Denn die inncrc Klasse ist - wie alles in LinkedList - bcreits mit pararnetrisiert. Also darf man den Parameter nicht ein zweites Mal hinschreiben.
16.5 Neue for-schleife in JAVA 1.5 Mithilfe vor1 Iteratorcn kann rnan relativ bequem alle Arten von Kollektionen durchlaufen. Aber ein Muster wie
... f o r ( I t e r a t o r i = p o l y g o n . i t e r a t o r 0 ; i . h a s ~ e x t ( ;) ) P o i n t p = ( ~ o i n t () i . n e x t 0 ) ; // nichste Ecke des Polygons . . p ... // Punkt p verarbeiten,
l//for
16.5 Neue for-Schleifein JAVA 1.5
265
Programm 16.8 Ein Iterator für die Klasse LinkedList public class LinkedList implements CollectionC private DCell first ; private DCell last ; private int length; public Iterator iterator 0 C return new LinkedListIteratoro;
// Iterator erzeugen,
1 private class LinkedListIterator implements IteratorC // innere K1. private DCell current ; LinkedListIterator
0 C current = null; 1
// Konstruk:tor
public boolean hasNext 0 C return current ! = last; 1 public Data next 0 C if (current == null) C current = first ; 1 else C current = current.next;
>/Af return current . content ; 1/ h e x t
>//end of inner class Lin.kedListIterator )//end of class LinkedList
ist immer noch mit beachtlichem „ , f ~ ~ mnoise" al belastet, vor allem wegen der notwendigcn Ca.stings. Deshalb führt JAVA 1.5 eine spezielle Kurznotation ein, mit der solche Standardsitua.tionen besonders knapp gefasst werden können. LinkedList polygon;
... for (Point p ... p ... l//for
:
polygon) -C
// P u n k t p uerarbeiten
Das ist zu lesen als „f?i:r alle P u n k t e p in polygon". Bei genauererri Hinsehen erkennt man, da.ss diese Schreibweise in der Ta.t alle notwendigen Informationen enthält, die der Compiler braucht, um &raus die ursprüngliche gcschwätzigc Vcrsion zu rekonstruicrcri. M a n beachte aber; dass das nur ,fkr J r r ~ , p l r m e n t . i edes ~ 1rrter:fafaces Collection 7md fiir Arrays geht.
Bäume
I , ~ Lder I , n f o m a ~ i kwach,sen die Bäwrr~enicht i n den Himmel. K. Samelson
Mit den Listcn habcn wir die cirifachstc aller Datenstrukturen betrachtet, die man mit Verkettung aufbauen kann. Die nachstc bedcutende Struktur in der Informatik sind die sog. B ä u m e . Auch diese Struktur findet ma.n in vielfiiltigen Anwendungen.
17.1 Bäume: Grundbegriffe Bäume werdcn üblicherwcisc grafisch dargestellt, indem amgehend von der sog. Wurzel die weiteren Knoten unten angefügt werden:
'I\.
h . 1 (a) Ein allgemeiner Baiim
., (b) Ein Binärbainri
Definition (Baum) Ein Baum besteht aus cincr Menge von Knoten imd Kanten, für die gilt: Der oberste Knoten ist die Wurzel des Ba.uines. Die untersten Knoten lieifien Blätter und die übrigen werden a.ls innere Knoten bezeichnet. Alle Knoten auBer der Wurzel haben geriau einen Elternknoten, mit dem sie durch eine Kante verbunden sind. Die Blätter sind dadurch cha.rakterisiert, dass sie keine Kiridknoten besitzen. Ma.n verwendet manchmal auch den leeren Baum, der durch eine „Kante ohne Blatt" dargestellt wird. 81
17 Bäume
268
Hinuieis: Was wir schon in Abschnitt 16.1.7 für die Listen festgestellt haben, gilt leider auch hier. Aus methodischer Sicht sollte man nicht von einzelnen Knoten sprechen, sondern von ganzen (Unter-)Bäumen. Das heißt, anstelle der Sichtweise „der Knoten hat Kindknoten" sollte eigentlich das Prinzip ,,der Bauni hat Unterbäurne" stehen. Auch hier wird wie bei den Listen die Lösung des Dilemmas wieder darin bestehen, Abstakte Datentypen für Bäume einzuführen. Das heißt, wir definieren eine geeignete Klasse für Bäume, mit der wir den mcthodisch korrekten TJmgang sicherstellen, während die interne Repräsentation mittels Referenzen in einer inneren Klasse verborgen wird. Bäume kornnlen in unterschiedlichen Varianten vor. Besonders wichtig ist der Spezialfall der Binärbäume. Hier hat jeder Knoten (außer den Blättern) geiiau zwei Kindknoten; dabei sind manchnial auch leere Bäume als Kindknoten zugelassen. Eine weitere Variante betrifft die Frage, ob die Werte an den inneren Knoten und an den Blättern den gleichen Typ haben oder verschiedene Typen. Manchmal tragen die innercri Knoten gar keine Wcrtc, sodass alle relevante Inforrriation an den Blättern zu finden ist. Und so weiter. Bäume haben in der Programmierung im Wesentlichen zwei Arten von Anwendungen: -
0
-
Es gibt Applikationen, bei dcncn man auf natürliche Weise auf baumartigc Strukturen stögt. Die bekanntesten Beispiele dafür sind: Sprachverarbeitung. Sowohl bei künstlichen Sprachen (wie Programmierspra.cheri) als auch bei natürlichen Sprachen (wie Deutsch, Eriglisch etc.) führen die grarnrriatikalisclien Strukturen unmittelbar auf Bäume. H T M L , X M L . Diese Interriet-Sprachen sind nichts anderes als geradezu frappierend hässliche und unleserliche - Formen der Baumbeschreibung . Dateisysteme. Die Folder- und Dateihierarchien in Betriebssystemen wie UNIX oder WINDOWS sind ebenfalls baumartig organisiert. In viclcn Applikationen lassen sich Bäume zur Beschleunigung von Algoritlirrien einsetzen. Das Standardbeispiel hier sind diverse Arteri von Suchbäum,en. Der Grund ist auch ganz einsichtig: Wir wissen von unseren Suchalgorithmen auf Arrays, dass die Bisektionssi~cheniir logarithmischen Aufwand hat. Und die Struktur dieser Suchmethode ist gerade baumartig. -
-
0
-
17.2 Implementierung durch Verkettung Wie bei den Listen wollen wir uns auch hier dem Thenia von der konkreten Irripleineritierurig aus nä.hern. In Analogie zu den Listen arbeiten wir auch hier mit Baumzellen.
17.2 Implementierung durch Verkettung
269
Ein Binärbaiim
A r ~ m e r k m g :Wir können bei den Blättern auf die Feldcr für die bciden Zeiger verzichten. Aber es ist rnarichrnal einfacher, nur mit einem Zellcntyp zu arbeiten und die Zeigerfclder mit n u l l zu besetzen. Für die Bäume gilt das Gleiche wie für die Listen: Die Struktur ihres Aufbaus ist völlig unabhängig davon, ob wir an den Knoten Zahlen ablegen oder Kundendaten oder Dateinamen. Deshalb definieren wir die folgenden Klassen über Knoteninhalten der Art Obj e c t . (Wir bei Listenzellen ist auch eine generische Variante möglich.) 17.2.1 Binärbäume Die elementarste Form von Bäumen sind die sog. Binärbäume, bei denen jeder Knoten (aufier den Blättern) genau zwei Kinder hat. Die entsprechende Klasse für die Zellen solcher Bäumc ist in Abb. 17.1 angegcberi. Wir könncri den gleichen Klassennamen wie bei den Listen verwenden, ohne dass Konflikte zu befiirchten sind. Denn auch diese Klasse wird als innere Klasse in einer umfassenden Klasse Tree verschwinden. (Interessanterweise sind diese Knoten bis auf die Attributnamen nicht von denen für doppelt verkettete Listen unterscheidbar. )
class Ce11
// // Ce11 right // Cell(0bject C, Ce11 1, Ce11 r) // Cell(0bject C) // Object content
Attribut: Inhalt
Ce11 left
Attribut: linkes Kind Attribut: rechtes Kind Konstruktor Konstruktor
Abb. 17.1. Zellen für Binärbäume
Für die Blätter führen wir mittels Overloading einen zweiten Konstruktor ein, sodass wir new Ce11 ( X ) anstelle von new Ce11( X , n u l l , n u l l ) schreiben können.
Die Definition dieser Klasse ist in Programm 17.1 angegeben. Auch hier enthä.lt die Klassc wiedcr Attribute, die selbst vom Typ Ce11 sind.
Programm 17.1 Die Klasse Ce11 für Biriärbaurri-Zellen class Ce11 ( Obj ect content ; Ce11 left ; Ce11 right ; Cell(0bject C, Ce11 1, Ce11 r) ( this.content = C; this . left = 1 ; this .right = r;
// // // //
der eigentliche Inhalt linker Kindknoten ,rech,ter Kindknoten. innerer Knoten
)//Konstruktor
// Blatt this.content = C ; this .left = null ; this.right = null;
)//Konstruktor )//end of dass Ce11
Mit dieser Klasse können wir z. B. den Binärbaum vom Anfang dieses Abschnitts aufbauen, indem wir von den Blättern ausgehen und ihn Stück für Stück von unten nach oben zusammensetzen. (Dabei gehen wir davon aus, dass die entsprechenden Inhalt-Objekte A, B, . . . , H gegeben sind.)
// die Blätter Ce11 C Ce11 d Ce11 g Ce11 h Ce11 i
new new = new = new = new =
=
Cell(C) ; Ce11 (D) ; Cell(G) ; Cell(H) ; Cell(1) ;
// innere Knoten der Stufe I Ce11 b Ce11 f
new Cell(B, C , d) ; new Ce11 (F, g , h) ; // innere Knoten der Stufe 2 Ce11 e = new Cell(E, f, i) ; = =
// innere Knoten der Stufe 3 (Wurzel) Ce11 a
=
new Cell(A, b, e) ;
Lästig ist, dass man den Baum von unten her aufbauen muss. Das kann man durch folgerlden Code vermeiden:
17.2 Implementierung durch Verkettung
271
Ce11 a = new C e l l ( A , newCell( B , new C e l l ( C ) , new C e l l ( D ) ) , new Gell( E , new C e l l ( F, new C e l l ( G ) , new C e l l ( H ) ) , new C e l l ( I ) ) ) ;
Man beachte, dass hier die Zellenobjekte in einem geschachtelten Aiisdruck eingeführt werden. Dabei nutzen wir das Layout aus, um wenigstens notdürftig den Überblick über die korrekte Zusarnniensetzung zu behalten. (Das Layout sieht nicht von ungefähr so aus, wie man es von den Datcihierarchien in WINDOWS und UNIX kennt.) Aber das Beispiel macht bereits deutlich, dass man Räume i. Allg. nicht als Konstanten notiert, sondern systematisch über geeignete Algorithmen aufbaut (s. später). 17.2.2 Allgemeine Bäume
Wie stellt man allgemeine Bäume dar, a.lso Bäume, deren Knoten auch mehr als zwei Kindknoten haben können? (Diese werden in manchen Büchern auch als p-adische Bcurne bezeichnet.) Eine Möglichkeit ist, ein Attribut „Liste der Kindknoten" vorzusehen. Das bedeutet, dass wir einfach die Klasse LinkedList wieder verwenden. Allcrdings hat dicsc Version einen Nachteil: Jetzt treten als Elemente der Listen Objekte der Art Ce11 auf, fiir die wir dann immer wieder geeignete „Castingsu (s. Kap. 10) vornehmen müssten. Um das zu vermeiden, codieren wir lieber die Listenstruktur in unsere Baumzellen hinein. Der allgemeine Baum vom Anfang dieses Kapitels kann dann dargestellt werden, wie in Abb. 17.2 gezeigt (wobei allcrdings leere Bäumc keinen Sinn machen).
Abb. 17.2. Ein allgemeiner Baum
272
17 Baume
Interessanterweise können wir hier exakt dieselben Arten von Zellen benutzen wie für die Binärbäume. Allerdings ist die Bedeutung der Zeigerattribute jetzt eine ganz andere! Der ,,rechteL'Zeiger meint jetzt nicht mehr den zweiten Kindknoten, sondern den rechten Geschwisterknoten. 17.2.3 Binärbäume als Abstrakter Datentyp
Was für Listen gilt, trifft aiich auf Bäume zu. Eine Ansammlung von verzeigerten Zellen stellt nur ,,zufällig" einen Baum dar. Das wird alleine schon dadurch deutlich, dass unsere Zellen sowohl für Binärbäume als auch für p-adische Bäume geeignet sind, und sich außerdem nicht von denen bei doppelt verketteten Listen unterscheiden. Die Lösung ist auch die gleiche wie bei Listen: Abstrakte Da,tentypen. Wir definieren zwei Klassen, eine fiir Binarbäume und eine für allgemeine Bäume. Beide setzen auf der gleichen Art von Baunizellen auf, interpretiercn und verarbeiten diese aber unterschiedlich. Für die Binarbäume ist die entsprechende Klasse in Abb. 17.3 angegeben. Wir zeigen die generische Variante.
class BinTree BinTree 0 // Konstruktor (leer) // Konstruktor (Blatt) BinTree( Data X ) BinTree( Data X, BinTree 1, BinTree r ) / / Konstruktor (normal) Data getValue0 BinTree getleft 0 BinTree getRight0 boolean ishpty 0 boolean isleaf 0 boolean isInner0 void setValue (Data X)
// // // // // // //
Wert am Knoten linker Unterbaum rechter Unterbaum Test, ob leer Test, ob Blatt Test, ob innerer K. Knotenwert setzen
I
t
Abb. 17.3. Generische Binarbäume
Ein repräsentativer Ausschnitt aus der I~nplerneritierungdieser Klasse ist in Programm 17.2 angegeben. Diese Implementierung sieht etwa.skomisch aus, weil letztlich die Klasse BinTree nur ein Rahmen um die Zelle Ce11 root ist, die dann die Verzeigerung enthält. Beim Zugriff' z. B. auf den linken Unterbauni erhält ma.n aus der Zelle root zunächst nur eine weitere Zelle; diese muss dann zu einen1 BinTree verpackt werden, bevor sie als Ergebnis von getLef t abgeliefert werden kann. Wenn man diese Struktur mit LinkedLi st vergleicht (s. Abschnitt l6.2), dann fehlen vor allem auch Operationen zum Hinzufügen und Wegnehmen von
17.3 Traversieren von Bäumen: Baum-Iteratoren
273
Programm 17.2 Die Klasse Tree der Binärbäume public class BinTree C private Cell root ; public BinTree this .root = null;
// leerer B a u m
1 // einelementiger B a u m (Blatt) public BinTree ( Data X ) C this .root = new Cell (X, null, null) ;
>
public BinTree ( Data X, BinTree 1, BinTree r ) t h i s . r o o t = n e w C e l l < D a t a > ( x , l.root, r.root);
1 public Data getValue 0 C returnthis.root.content; )//get Value public BinTree getLeft 0 BinTree b = new BinTreeO ; return b; )//getLeft
Elementen. Der Grund dafür ist, dass wir für diese Operationen in vcrschic denen Szenarien unterschiedliche Techniken verwenden. Darauf gehen wir in den nächsten Abschnitten genauer ein. Übung 17.1. Man implementiere den Rest der Klasse BinTree. Übung 17.2. Man entwerfe und implementiere die Klasse Tree für allgemeine Bäume.
17.3 Traversieren von Bäumen: Baum-Iteratoren Alle wesentlichen Baumalgorithrnen laufen letztlich auf das Traversieren des Baunies hinaus: Egal ob wir z. B. ein Element suchen, alle Elernente aufaddieren oder alle Knoten abändern wollen, immer miissen wir die Knoten des Baumes irgendwie nacheina.nder aba.rbeiten. Während es für dief i c d ses Durchlaufen bei Listen nur eine sinnvolle Möglichkeit gibt - nämlich von vorne nach hinten , haben wir bei g h Bäumen mehrere Möglichkeiten.
A
/I i\ I\
Irn Folgerden zeigen wir die wesentlichen Varianten der Baumtraversierung am Beispiel der Binärbäume. Als Beispiel benutzen wir den nebenstehenden Baum. Es gibt drei essenziell verschiedene Möglichkeiten zur Traversierung von Binärbäumen: Preorder: Der Baurn wird in der Reihenfolge K n o t e n linker Un,terbaum, rechter Unterbaum durchlaufen. In urisereni Beispielbauni liefert das die Reihenfolge a-b-c-d-e-f-g-h-i. P o s t o r d e r : Der Baum wird in der Reihenfolge linker Un,terbaum rechter Unterhaum, K n o t e n (li~rchlaufen. In unserem Beispielbaum liefert das die Reihenfolge C-d-b-g-h-f-i-e-a. Inorder: Der Baum wird in der Reihenfolge linker Unterbaum K n o t e n rechter Unterbaum durchla.ufen. In unserem Beispielbaurn liefert das die Reihenfolge c-b-d-a-g-f-h-e-i. (Mari kann sich das bildlicli so vorstellen, dass alle Knoten ,senkrecht nach unten fallen".) --
-
-
-
-
Diese drei Möglichkeiten sind sehr leicht zu prograrrirriieren (s. Progranini 17.3). Dabei bezeichnen wir mit «action(. . . )» diejenigen Tätigkeiten, die beim Durchlauf mit den Knoteninhalten geschehen sollen. Programm 17.3 Baumtraversierunr void preorder (BinTree t) C if (!t.isEmptyO) I «action(. . .) ; D preorder (t .getlef t 0) ; ~ r e o r d e r(t .getRight 0) ;
1//2f >//preorder void postorder (BinTree t) C if (!t.isihpty 0)C ~ostorder(t .getlef t 0) ; postorder (t .getRight 0) ; «action( . . . ) ;B
>/ / i f > //postorder
void inorder (BinTree t) C if (!t.isEmptyo) C inorder (t .getlef t 0) ; «action(. . .) ;» inorder (t .getRight 0 ) ;
1/ / i f
> //in,order
Leider ist es nicht trivial, daraiis einen Iterator zu machen. Denn dazu muss die schöne Rekursion der obigen Methoden in einzelne, nacheinander auszuführende next-Operationen umgewandelt werden. Dazu gibt es zwei einfache und eine komplizierte Methode. Man kann die drei Methoden in cincn parallel abla.ufenden „ThreadL'(s. Kap. 21) einpacken, der an jedem Knoten unterbrochen wird, um den Inhalt abzuliefern. Man kann aus dem Baum tatsächlich eine Liste extrahieren, indem man als « a c t i o n ( . . . ) » jeweils das aktuelle Elernent an die Liste anfügt. Als Variante kann Inan die Baumzellcn auch mit einem dritten Zeiger vorsehen, der die jeweilige Traversierurigsfolge (wie einen Ariadnefaden) als zusiitzliche Verkettung in die Knoten einträgt. Dadurch werden der Baum und die Liste de facto verschmolzen. Man simuliert das, was Compiler intern immer tim, wenn sie rekursive Methoden realisieren. Mari hält die partiell abgea.rbeiteten Knoten in eiriern Stack. Wir skizzieren hier kurz die dritte dicscr Lösungcn. Prograrnrn 17.4 definiert einen It,erator für die Preorder-Traversierung. Programm 17.4 Prcordcr-Traversicrung als generischer Iterator class PreorderIterator implements Iterator C private Stackstack; public PreorderIterator ( Cell root ) stack = new StackO; if (root != null) C stack.push(root) ; )
C
1 public boolean hasiiext
0 C return ! (stack.empty 0); 3
public Data next 0 C Cell actual = stack.pop() ; if (actual .right != null) stack.push(actua1.right) ; ) if (actual.left != null) C stack.push(actual.1eft) ; ) return actual.content;
)//next public void remove
0 C)
// nötig wegen Iterator
3 / / end of class Preorderlterator
Die Kla.sse BinTree a.iis Programm 17.2 muss dann um eine Operation i t e r a t o r zur Generierung des Iterators erweitert werden: p u b l i c I t e r a t o r < D a t a > i t e r a t o r () r e t u r n new PreorderIterator(this.r o o t ) ; )//zterator
276
17 Baume
Außerdem muss P r e o r d e r I t e r a t o r genau wie Ce11 eine innere Klassen von sein, weil sonst Ce11 nicht sichtbar ist. Um dieses Programm besser zu verstehen, seheri wir uns seiri Verhalten wä.hrend der Traversierung des Baums von1 Anfang dieses Abschnitts an BinTree
An1 Anfang ist die Wurzel a i ~ nStack. Beim ersten Aufruf von n e x t wird a entfernt und dafür der rechte und linke Kiridknoten (in dieser Reihenfolge) eingetragen. Beim zweiten n e x t wird der oberste Knoten b aus dem Stack entfernt und seiri rechter und linker Kindknoten eingetragen. Und so weiter. Übung 17.3. Man programmiere die lteratoren für die Postorder- und die InorderTraversierung.
Anmerkung: Es gibt in der älteren Literatur noch ein weiteres Verfahren zur Sie erlaubt es, ohne zusätdid~en Baiimtravcrsieri~ng,die sog. Wirbelt.rc~ver,s%erur~,g. Speicherplatz auszukommen. Aber dazu muss man temporär die Baurnstruktur zerstören. Das gilt heute als ein viel zu hoher Preis für ein bisschen Speicherersparnis.
17.4 Suchbäume (geordnete Bäume) Jetzt wenden wir uns einer der wichtigsten Applikationen von Bäumen zu: den Suchbäumen. Wir haben schon früher gesehen, dass man niit Bisektionsverfahren oft besonders schnelle Algorithmen erhält. Daher ist die Idee nahe liegend, das Prinzip der Bisektion auch in Datenstrukturen einzubauen. In der Praxis trifft nian auf diese Art von Problenien meistens in der Form, da.ss eine bestimmte Art von Da.tcn vorliegt, auf denen eine Ordnung definiert ist. Ein typisches Beispiel sind „Kiinden". Sie enthalten eine Fülle von Da.ten, z. B. Name, Adresse, Bankverbiridung, B0nitä.t usw. Aber geordnet werden sie nach der Kundennummer; diese fungiert bei der Suche als Schlüssel. Diese Situation fassen wir in Abb. 17.4 etwas allgcmcincr in einer Klasse fiir Suchbüurne zusamnien. Unser Ansatz ist rriiriirrialistiscli; wir seheri nur die Methoden add, d e l und f i n d vor. AuBerdem halten wir diese Operationen besonders eirifach. Aus softwaretechnischer Sicht müsste nian bei add prüfen, ob ein Element mit diesem Schlüssel schon vorliegt und entsprechende Fehlermeldungen generieren. Wir machen es uns hier - der Kürze halber - eirifach und ersetzen das alte Element durch das rieuc. Analog müsstc bei d e l eine Meldung erfolgen, ob das zii löschende Element iiberhaupt existiert. Wir tun hier beim Fehlen des Elements einfach nichts.
17.4 Suchbäume (geordnete Bäume)
277
class SearchTree SearchTree Object find ( int key ) void add ( int key, Object element ) void del ( int key ) C
U
Abb. 17.4. Die Klasse für Suchbäume
Bei der Implemcnticrung können wir die Cleverness graduell steigern. Das heiBt, wir beginnen mit einer einfa.chen und leicht verständlichen Lösung, die aber bzgl. der Effizienz Schwächen hat. In der zweiten Ausbaustufc fügen wir dann Effizienz hinzu um den Preis höherer Programmkomplcxität. -
Geordnete Bäume Um ein Objekt mit einem bestimrntcn Schlüssel jeweils sehr schnell finden zu können, speichern wir alle Elemente in einem Baum, der ,,nach Schlüsseln sorticrt" ist. Dazu verwenden wir eine Baumvariante, bei der die eigentlicheil Elemente nur an den Blättern abgespeichert sind. An den inneren Knotcn vermerken wir lediglich Schlüsselwertc als Suchhilfe. Als Beispiel ist in Abb. 17.5 ein Baum für Kunderidaten angegeben.
Abb. 17.5. Ein Suchbaiim fiir Kundendaten
Definition (Geordneter Baim) Wir nennen einen Binärbaum geordnet, wenn gilt: Alle Knoten im linken Unterbaum sind kleiner oder gleich der Wurzel, und alle Knoten im rechten Unterbaurn sind größer als die Wurzel. Diese Bedingung muss auch in allen Unterbäumen gelten. Der Baum in Abb. 17.5 ist ein Beispiel fiir einen geordneten Baum. Man beachte, dass die obige Bedingung nzcht verlangt, dass die inneren Knoten genau den größten Schlüssel des linken Unterbaumes widerspiegeln.
278
17 Bäume
Zur Programmierung der zentralen Aufgaben Hinzufiigen,, Lösch,en urid Such,en haben wir zwei griindlegende Möglichkeiten: 1. Entwedcr wir schreibcn drei grofic (rekursivc) Operationen add, del und find, die jeweils die einzelnen Knotenarten unterscheiden und dann die entsprechcriden Aktionen ausführen. 2. Oder wir versehen jede Knoteriart niit der lokalen Fähigkeit, hinzuzufügen, ZU löschen und zu suchen. Dazu muss jeder Knoterl dann natürlich mit seinen Unterknotcn interagicren. Da die zweitc Möglichkeit wesentlich besser dem objektoricntiertcri Paradigma. entspricht, wählen wir diese Variante. Allerdings machen wir bei der PrRsentation einen Kompromiss: Wir bchandeln die Operationen der Rcihe nach und zeigen, wie sie in dcn verschicdenen Knotenklassen jewcils aiisschen.'
Prinzip der Programmierung: Prograrnrnierstile Eine gegebene Programmieraufgabe kann i. Allg. auf verschiedene Arten gelöst werden. Im Laufe der Jahre haben sich dabei in der Informatik ganz unterschiedliche „ParadigrnenLLder Programmierung lierausgcbildet. Untcr Paradzgma vcrstcht man dabci jcweils cirie ganz bestirnrnte Art, an Problerne prograrrlrniertecl~niscliheranzi~gehen. Zwei solche Paradigmen lassen sich sehr gut arn Beispiel der Methoden add, del und find erläutern: 1. Die obige Variante (1)entspricht dem klassischen Prograrrlnlierstil (wie er etwa in Sprachen wie PASCAL oder C umgesetzt wird). Dieser Stil lässt sich natürlich auch in JAVA realisieren. 2. Die Variante (2) entspricht dem objektorientierten Stil, bei dem jedes Objekt alle relevanten Operationen für sich lokal realisiert.
17.4.1 Suchbäume als Abstrakter Datentyp: SearchTree.
Die Klasse SearchTree ist trivial. Sie dient letztlich nur dazu, eine Hülle um die Knoten zu legen. Denn wir müssen wie schon im vorigen Kapitel beim Thema „Listen als Abstrakte Datentyperi" diskutiert die Knotenzellen vor einer direkten Manipulation schützen, indem wir sie in einer urrifassenden Klassc vcrbergen. Wenn nur noch dic Mcthodcn add, del und find vcrfügbar sind, kann der Baum nicht zerstört wcrdcn. Programm 17.5 zeigt, dass ein Suchbaurn letztlich nur eine Referenz auf cinen Wurzelknotcn bcsitxt und alle Aufträge an dicsen Knotcn weiterreicht. -
-
Im SoftwarcEnginccring ist dieses Phänomen der unterschicdlichcn Sichtcn auf ein und dasselbe Artefakt seit einiger Zeit erkannt worden und h a t zu neuen aber zurzeit iiodl ziemlich unausgereiften Ideen geführt, die unter dein Schlagwort Aspect-or-~en,tertpri?yrurram?.n,g tliskir1,iert werden.
17.4 Suchhäume (geordnete Bäume)
279
Eine kleine Kornplikatiori entsteht dadi~rcli,dass wir jeweils den Randfall des leeren Baums abfangen müssen.
public class private Node root ; public SearchTree 0
C C root = null; ) ( int key ) C C
public Object if (root !=null ) return root . find(key) ; ) else C return null ; >//2f //find
// Auftrag an root leiten // leerer B a u m
( int key, Object element )
C
root = root.add(key, element);
1 else C root = neu Leaf (key, element) ; )//if l//add public void ( int key ) if ( root !=null ) C root = root . del(key1; l//if )//del )//end of class SearchTree
// A11jtro.g an root leiten // leerer B a u m // wird z u m Blatt
C // A u f t ~ qan rool leiten
Auf den erstJen Blick sehen die Zi~weisurigeriroot = root.add( . . . ) und root = root .d e l ( . . .) etwas überraschend aus. Aber wir werden gleich bei der Implementierung schen, dass u. U. tatsächlich der Baum so geändert wird, dass eine andere Wurzcl entsteht und dic wird von dcri Operationen add und del jeweils zurückgeliefert. In den meisten Fä.llen wird allerdings nur die iirsprüngliche Wurzel selbst zurückgeliefert; dann entsprechen dic Zuweisungcn letztlich nichts anderem als root = root,was harmlos ist. -
17.4.2 Implementierung von Suchbäumen
Weil unsere Suchbäurne auf einen abgeschirmten a.bstrakteri Datentyp fiihren, ist es durchaus vernünftig und auch softwaretechriisch zulässig, hier direkt auf die Zellcri zuzugreifen, was uns auch erlaubt, spezielle Zellenklassen zu verwenden. 1nsgesa.mt erhöht das die Effizienz. Deshalb k~enutzenwir zur Im-
280
17 ßäunie
plernentierung der Siichbäume zwei Arten von Knoten (vgl. Abb. 17.6 und Programm 17.6): B l ä t t e r (Leaf) und innere Knoten (Fork).
L,- - - - - - - - - '1 C----------T\
uses -F
i
Node
I
I
L-T---x-L
Abb. 17.6. Die Klassen für Suchbäume Die Blätter enthalten sowohl einen Schlüssel als auch das zugehörige Elernent,die inneren Knoten brauchen nur einen Schlüssel (wie im Beispielbaurn
Programm 17.6 Die Knotentypen für Suchbäume abstract class C int key; // Schlüssel (für Fork und Leaf) abstract Object find ( int key 1; abstract Node add ( int key, Object element ) ; abstract Node del ( int key ) ; >//end of class Node clas extends Node C No Node right ; Fork ( Node left , int key, Node right ) this .key = key; this .left = left ; this .right = right ;
// linker Unterbaum (nur Fork) // rechter Un.terbaum (nur Fork)
C
1 Object find ( int key ) Node add ( int key, Object element ) Node del ( int key ) )//end of class Fork clas extends Node C Ob tent ; Leaf ( int key, Object content ) this.key = key; this.content = content ;
C C C
«siehe Programm 1 7 . 7 ~ 1 «siehe Programm 17.8» 1 «siehe Programm l 7 . h 1
// Inhalt (nur Leaf)
C
1 Object find ( int key ) Node add ( int key, Object element ) Node del ( int key ) )//end of class Leaf
C C C
«siehe Programm 17.7)) 1 «siehe Programm 17.8)) 1 «siehe Programm 17.9,~ 1
17.4 Siiclibaurne (geordnete Bäurne)
281
in Abb. 17.5 zu sehen ist). Bcidcs sind Untcrklasscri der abstrakten Klasse Node. Die eigentlich interessierende Klasse SearchTree benutzt diese Knotenklassen als internc Hilfsklassen. Da Suchbäume spezifische Anforderungen stcllen, variieren wir die Knoten etwas gegenüber den allgemeinen Baumzellen, die wir am Anfang des Kapitels diskutiert haben. Sowohl Leaf als auch Fork besitzen einen Schlüssel; deshalb ist dieser in der (abstrakten) Superklasse angegeben. Aber in den übrigen Attributen unterscheiden sich die beiden Subklassen. Fork enthält Referenzen auf dcn linken und rechten Unterbaum, Leaf enthält dic cigcntlichcn Datenelemente.
Suchen (find) Programm 17.7 enthält die Silchmethode find in den beiden Siibklassen Fork und Leaf. Bei einem inneren Knoten erfolgt die Weitersuche abhängig vorn Schlüssel im linken oder irn rechten Unterbaurn. Deshalb niiisseri die Bäurne geordnet sein. Das Suchergebnis wird als Resultat „nach oben" durchgereicht. Bei einem Blatt wird der Suchschlüssel mit dem gespeicherten Schliissel verglichen. Abhängig vom Ergebnis wird das Objekt oder null geliefert.
Programm 17.7 Suchen in geordneten Bäumen (find) class
extends Node C
Object ( int key ) C // find bei Fork if (key / / e n d of cluss Fork class
extends Node
C
// add bei Leaf ( int key, Object element ) C Node Leaf = new Leaf (key ,element) ; Le if (key < this .key) C return new Fork(newLeaf , key, this) ; ) else if (key == this .key) C return newLeaf ; 3 else C // key this.key return new Fork(this, this.key, newLeaf); 3 //if 1//Udd ,
) / / e n d of d a s s Leaf
17.4 Siicht>ä.ume(geordnete Bäume)
283
der alte Inhalt durch den neisen ersetzt (genauer: ein rieues Blatt mit den1 neuen Inhalt kreiert). Ansonsten wird das neue Bla.tt mit dem alten zu einem Binärbaum zusammengesetzt, wobei die Reihenfolge von den Schliisseln abhängt. Der neu erzeugte Binärbaum muss im Elternknoten (im Beispiel ist das der Knoten 21) anstelle des alten Blattes eingesetzt werden (vgl. Abb. 17.7). Das kann niaii an1 einfachsten dadurch bewerkstelligen, dass die Operation add den jeweiligen Knoten als Ergebnis abliefert meistens ist das der alte Knoten selbst ( t h i s ) , manchmal aber auch der nen generierte. Und dieser Knoten wird dann in1 Elternknoten als t h i s .l e f t bxw. t h i s .r i g h t gespeichert. Um das noch einmal deutlich zu sagen. In den allernieistcn Fällen wird irn Endeffekt nur t h i s . l e f t = t h i s . l e f t bzw. t h i s . r i g h t = t h i s . r i g h t aiisgefiihrt; das heifit, es ändert sich nichts. Aber manchmal wird eben der neuc Knoten eingetragen. Der Reiz dieses Tricks ist, dass man sich aufwendige Falliinterscheidimgeri spart. -
Löschen ( d e l ) Prograrnni 17.9 enthält die beiden Instanzen der Methode d e l . Aiich beim Löschen müssen wir uns in bewährter Manier durch den Baum nach unten zu den Blä.ttern arbeiten. Beim Blatt gibt es zwei Möglichkeiten: Entweder die Schlüssel stimmen übcrcin; dann wird das Blatt tatsächlich gelöscht, d. h., es wird n u l l zuriickgeliefert. Oder die Schliissel sind verschieden; dann bleibt das Blatt erhalten (denn es war ja. niclit gemeint) und folglich wird cs selbst zurückgeliefert.
Abb. 17.8. L6schen aus einem Suchbaum (del)
Der Elternknoten (in Abb. 17.8 der Knoten mit dem Schlüssel 26) erkennt am Rückgabewcrt, was gcschclien ist. Wenn n u l l ziiriickkommt, ist das entsprechende Blatt vcrschwundcn. Damit hätte der Fork-Knoten nur noch ein Kind, was niclit zulässig ist. Folglich ist der Knoten jetzt überflüssig und muss durch sein verbliebenes Kind (im Beispiel der Knoten 32) ersetzt werden. Dcshalb gibt er nicht sich selbst als Resultat zurück, sondern dieses verblicbcne Kind. Der übergeordnete Knoten (irr1 Beispiel 58) trägt diesen Riickgabewert
17 Bäume
284
Programm 17.9 Löschen aus geordneten Bäumen (del) d a s s Fork extends Node C
// del bei Fork this.left = this.left .del(key) ; // war Leaf; wurde gelösch,t if (this.left == null) C return this.right; 3 else C return this ; )/H wull 1 else C this.right =this.right.del(key); // war Leaf; wurde gelösch,t if (this.right == null) C return this.left; 3 else C return this ; )//if n,ull 1/ / 2 f
)//M ... )//end of dass Fork d a s s Leaf extends Node
C
Node ( int key ) C if (key == this .key) return null ; ) else C return this; >//2 f )//del
// del bei Leaf
)//end of dass Leaf
griindsä.tzlich als linkcn bzw. rcchten Unterbaum ein. Damit ist das gelöschte Blatt (im Beispiel 21) samt Elternknoten (im Beispiel 26) verschwunden.
17.5 Balancierte Suchbäume Aus Effizienzgründen ist es fiir die Suche in geordneten Bäumen offensichtlich wünschenswert, dass die Pfade durch dcn Baum möglichst glcich lang sind, der Bauni also balanciert ist.
17.5 Balancierte Suchbäume
285
Definition: Ein Baum heißt balanciert (oder ausgewogen), wenn die Längen der cinzelnen Pfade von der Wurzel bis zu den Blättern etwa gleich lang sind (d. h. sich höchstens um 1 unterscheiden). LJ Unglücklicherweise wird durch das Hinzufügen und Löschcn i. Allg. kein balancicrtcr Baum entstehen. Im schlimmsten Fall könnte sogar ein sog. linksoder rechtsgekämmter Baum entstehen (s. Abb. 17.9) rechtsgekämmter Baum
balancierter Baum
/\
d
linksgekämmter Baum
/\
e f g I\. /\ h l j k
Abb. 17.9. Extreme Beispiele für Bäume
Offensichtlich gilt für das Suchen (und die andcren Bauniopcrationen) bei eincrn Baum mit n Knoten: In einen1 rechts- oder linksgckämmtcn Baum ist dcr Sucliaufwa,rid linear, also ( ? ( T L ) . In einem balancierten Baum ist der Siicha.ufwand logarithmisch, also 0(log n) . Da keiner dieser beiden Extremfälle sehr walirscheirilich ist, stellt sich die Frage, was wir im Schnitt erwarten können. Aho und Ullman ([I],S. 258) argumentieren, dass rnan mit logarithmischem Aufwand reclirieii darf. Ihre Begründung ist: In1 Allgemeinen wird für jedcn (Uritcr-)Baum dic Auftcilung der Knoten auf den rechten inid linken Uriterbaum in der Mitte zwischcn bcstem und schlcchtestem Verhalten liegen, also bei einem Verhältnis von $ zu $. Aiif dieser Basis lässt sich dann der Aufwa.nd ungefähr zu 2.5 . log n abschätzen. Wenn man sich auf diese Art von statistischer (Un-)Sichcrlicit nicht einlassen will, muss man durch gecignete Maßnalimcn sicherstclleri, tlass die Bäi~ine immer ausgewogen siritl. Dazu finden sich in der Litrmtur einc Reihc von Vorschlägen, z. B. AVL-Bäumc, 2-3-Bäumc, 2-3-4-Bä.ume oder Rot-SchwarzBäumc. Eine gcnauere Bchandlung dieser verschiedenen Varianten geht aber iiber den Rahmen dieses Buches hinaus. (Genaueres ka.rni rnan in diversen Büchern finden, z. B. [I, 2, 9, 43, 441.) Wir begnügen uns damit, dic Grundidee aiihand der Rot-Schwarz-Bäume zu illustricren. Dazu ist es als Vorbereitung allerdings niitzlicli, zumindest die Gri~nditleeder 2-3-Bäumc und der 2-3-4Bäume kurz ai~ziisprechen.
286
17 Bäume
17.5.1 2-3-Bäume und 2-3-4-Bäume
Die Idee, Ausgewogenheit dadurch zu erreichen, dass rrlan von Binärbäurnen a.uf 2-3-Bäume iibergelit, stammt von J.E. Hopcroft (1970). Diese wurden zur weiteren Effizienzsteigerung dann noch auf 2-3-4-Bäimie erweitert. Definition (2-3-Baum, 2-3-4-Baum) Ein 2-3-Baum ist ein geordneter, balancierter Baum, in dem jeder innere Knoten xwci oder drei Kindknoten hat. (Analog sind 2-3-4-Baume definiert.) Q Wir beschränken uns zunächst auf 2-3-Baume und betrachten zur Illustration als Erstes die Operation des H~nzufugens. Die Grundidee ist hier, dass man 1x4 den Blattern einfügen kann, ohne dass die Balance des Gesamtbaums gestört wird. Betrachten wir den Fall eines 2-Knotens, der direkt iiber den Blättern liegt:
add(50,Otto)
Wir realisieren diese Einfügung in einem Zwei-Schritt-Prozess: Wir reichen den add-Auftrag bis zum Blatt durch. Als Ergebnis entsteht dort ein Binärbaiim (Fork), der aber um eins zu hoch ist. Wir dcuten das durch die Verwerdung einer Raute als Knotensyrnbol an. Der Elternknoten (im Beispiel 32) erkennt die falsche Tiefe und verwandelt sich in einen 3-Baum ( ~ o r k 3 ) .
Was passiert, wenn ein %Baum einen zu hohen Unterbauni zurückbekommt? Wenn es keine 4-Bäume gibt, bleibt nichts anderes übrig, als zwei 2-Räume zu kreieren und den entstehenden oberen 2-Baum als zu hoch zu charakterisieren.
& Hiibri
Ahrl
Rleiei
Otto
In1 schlimmsten Fall propagiert sich das Wachstum bis zur Wiirzel hoch. Dann ist der Baum zwar insgesamt höher geworden, aber er ist wieder balanciert.
17.5 Balancierte Suchbäume
287
Beini Löschen geht man analog vor. Allerdings entstehen jetzt nicht zu lange, sondern zu kurze Bäume, die entsprechend repariert werden müssen. Als Beispiel betrachten wir einen 2-Baiirn mit einem zu kurzen Unterbaurn. Bei der Reparatur entsteht ein zu kurzer 3-Baum; d. h., der Reparaturbedarf propagiert weiter nach oben.
Ein 3-Baum mit einen1 zu kurzen Unterbaum kann die Reparatur dagegen endgiiltig dilrchfiihren. Wenn der Nachbarknoten ein 2-Knoten ist, verwandelt er sich in einen 3-Knoten und ninlrnt den zu kurz geratenen Geschwisterknoten einfach als Kind auf.'
Wenn der Geschwisterknoten ein 3-Baum ist, dann mutiert er in zwei 2Bäiiine, von denen einer den zu kurzen Unterbaurn als Kind erhält.
Fazit Wie man an diesen Beispielen sieht, benötigt die Repa.ratur sowohl beim Hinzufiigen als ailcli heim Löschen irn schlirnrnsteri Fall log n Schritte. Das heißt, allc 0pcra.tionen auf 2-3-ßäuinen sind logarithmische Prozesse. Und das ist sehr effizient! Mit den 2-3-Bainnen haben wir also eine In~plerneritierungstechnikgefunden, die keinerlei Bescliränkungeri hzgl. des dynamischen Wa.chsens und Schrunipferis der Datenmenge auferlegt und trotzdcnl allc relevanten Operationen sehr effizient ausführt. Ma.n ka.nn die Implementierung noch etwas beschleunigen, intlern man beim Suchen und Löschen jeweils schon auf dem Weg von der Wurzel zum passenden Blatt die spätere Reparatur ~)onuegnimmt. Allerdings l~rauchtman dazu eine etwas gröBere Flexibilität und die liefern die sog. 2-3-4-Bäume. -
Dic Familienmetapher sollte mari jetzt nicht mehr allzu wörtlich rielimen
17 Bäume
288
Dann ist arn Blatt nur noch genau ein Schritt notwendig. (Näheres entnehme man der Literatur, z.B. 11, 2, 91.) Das klingt alles zu schön, um wahr zu sein. Und in der Tat gibt es einen Wermutstropfen in dem schönen Kelch der Freude. Wenn man sich die Programme 17.6 bis 17.9 ansieht, dann sind die Operationen f i n d , add und d e l nur deshalb so überschaubar, weil wir es mit Binärbäiurien zu tun ha.ben. Bei den 2-3- und 2-3-4-Bäumen erhalten wir seitenlange Fallunterscheidungcn, um herauszubekommen, ob wir links, halblinks, in der Mitte, halbrechts oder rechts arbeiten lind ob wir es mit einem 2-Baurn, 3-Baum oder 4-Baum zu tun haben. Das macht die Programme unleserlich und fehlerträchtig, und es kostet Laufzeit. Das alles führt zuni Wunsch nach besseren Lösungen. Die Antwort lleiBt Rot-Schwarz-Bäume. 17.5.2 Rot-Schwarz-Bäume Die Idee der Rot-Schwarz-Bäume ist an sich ganz einfach. Man möchte die gute Performanz der 2-3- und 2-3-4-Bäume beibehalten, aber wieder zurückkehren zu den guten alten Binärbäumen. Also stellt Inan 3-Bäume und 4-Bäume ganz einfach als Binärbäume dar, wie in Abb. 17.10 illustriert.
I A
R
\ C
/ \ A
B
.
C
A
B
C
D
A
B
C
.
D
Abb. 17.10. Von 2-3- lind 2-3-4-Bäiimeri zu Rot-Schwarz-Bäumen
Die zusätzlichen Knoten, die sozusagen die interne Struktur der 3- und 4Bäume darstellen, nennt man rote Kn,oteri, (bei iins notgedrungen grau gezeichnet). Bei 3-Bäumen kann der rote Knoten auch rechts sein. Auf Grund dieser Konstruktion kann ma.n die entstehenden Rot-SchwarzBäume folgeridermaBen charakterisieren:
Definition (Rot-Schwarz-Baum) Ein Rot-Schwarz-Baum ist ein geordneter Binärbaum mit folgenden zusätzlichen Eigenschaften: -
-
-
-
Die Knoten sind entweder rot oder schwarz. Die Blätter sind schwarz. Alle Pfade von der Wurzel zu den Blättern enthalten gleich viele schwarze Knoten (Ii). Wir nennen das die schwarze Pfadläng~. Es dürfen nie zwei rote Knoten unmittelbar aufeinander folgen (I2).
Von besonderer Bedeiitiing sind die Invarianten (11) imd (12). CI
17.5 Balancierte Suchbäume
289
Diese Eigenschaften garantieren eine „akzeptable Balanciertheit". Denn alle I 5 2s, wobei Pfade von der Wurzel zu den Blättern haben eine Länge s s die schwarze Pfadlänge ist. Damit gilt insbesondere 1 N U(logn,); d. 11., die Operationen bleiben logarithmisch. Der Aufwand der Operationen f i n d , add und d e l wird in der Praxis sogar geringer als bci 2-3- und 2-3-4-Bäumen. Die Pfade werden zwar im rriorst case doppelt so lang, aber dafür ist pro Knoten viel weniger Arbeit zu tun. Weil wir es jetzt wieder mit reinen Binärbäumen zu tim haben, entfallen die Myriaden von Fallunterscheidurig-eriund internen 0pera.tiorien. Dieser Gewinn wiegt die etwas gröBere Pfadlänge um ein Mehrfaches auf. Ein weiterer schöner Nebeneffekt ist, dass die Opera.tion f i n d jetzt unverändert aus dem Programm 17.7 üt>ernornrrien werden kann. Denn die Knoterifarbe spielt an keiner Stelle eine Rolle und kein Knoten wird verändert. Also müssen wir nur die Operationen add und d e l umprogrammieren. Und auch liier gilt, dass wir die Griindstruktiir der alten Programme beibehalten können, weil wir es nach wie vor rnit Binärbäurnen zu tun haben.
//Zf
3//report T o Die Methode accept (die nur vom Anführer z ausgeführt wird) sammelt alle ankornrnenden Meldungen in einer Liste: void accept ( Node other ) C this.blackNodes =this.blackNodes.add(other); 3 //accep t Allerdings gibt es bei diesem Design noch einen wichtigen Aspekt zu beachte~~: Nachdem der Algorithmus beendet ist, muss in einer zweiten Nachrichtenwelle allen Knoten mitgeteilt werden, dass sie sich wieder weiB färben dürfen. Sonst würden bei der nächsten Erreichbarkeitsberechnurig erratische Ergebnisse entstehen. 18.4.4 Tiefen- und Breitensuche
Das Erreichbarkeits-Programm ist phänotypisch für vielc Graph-Algorithmen: Es traversiert den Graphen (oder zumindest einen Teilgraplien). Das heiBt, die Knoten werden der Reihe riach besucht und geeignet verarbeitet. Worin die Bearbcitung besteht, variiert von Aufgabenstellung zu Aiifgabenstelliing; bei unserem Beispicl wcrdcn die Knoten nur (als schwarz) markiert, d. h. in einer cntsprechenen Menge gesammelt. Das obige Progranini lässt noch offcn, wie wir dcil Graphen traversieren. Dafür gibt es zwei prinzipielle Möglichkeiten:
Tiefensuche (engl.: depth-first search) Breitensuche (engl.: breadth,-first search) Betrachten wir als Beispiel den Landkarten-Graphen aus Abb. 18.2 und berechnen dort die von B a.us erreichbaren Knoten. Nach dem crstcn Schleiferldurchlauf erhalten wir die Situation von Abb. 18.6(a). Als Nächstes wählcn
310
18 Graphen
( a ) 1. Schritt
(C)
3. Schritt (Breitensiiche)
( b ) 2. Schritt
(d) 3. Schritt (Tiefensuche)
Abb. 18.6. Traversieriing des Graphen aus Abb. 18.2
wir von den beiden grauen Knoten z. B. den Knoten A aus, was zu der Situation in Abb. 18.6ib) führt. An dieser Stelle beginnen sich die Strategien zu unterscheiden: Bei der Breitensuche müssen wir zuerst alle Nachbarn „ersten GradesLL von B behandeln, bevor wir zu den Nachbarn „zweiten GradesL' übergehen; und das heißt, da.ss der Knoten E zu nehmen ist. Bei der Tiefensuche wird dagegen sofort ein „Nachbar des Nachbarnu A genommen, also z. B. C. In unserem Programm finden sich die beiden Strategien letztlich in der Implementieriing der Liste NodeList gray wieder, und zwar im Zusa.mmenspie1 der Operationen arb und add: Wenn wir dabei ein Stacka.rtigesVerhalten realisieren (vgl. Absclinitt 16.3.5), dann erhalten wir Tzqfens~~che. Wählen wir dagegen ein Queue-artiges Verhalten (vgl. Abschnitt 16.3.6), dann crhaltcn wir Breitensuche. Anmerkung: Man kann bei dem Algorithmus auch ohne dic Listc g r a y dcr gmuen Knoten auskommen, indem man die Methode reachable rekursiv auf alle N a c h barn von z anwendet. Man muss allerdings sicherstellen, dass bei bereits schwarzen Knotcn dic R,ckursion gestoppt wird, wcil sonst i. Allg. die Terrninierung verlorer1 geht. (Diese Rekursionstechnik führt auf Tiefensuche was einen Hinweis auf den engen Zusammenhang zwischen R,ekursion und Stack-artigem Verhalten liefert,.) -
Übung 18.2. Man programmiere eine rekursive Version von reachable, die ohne die Menge g r a y auskommt.
18.5 Kürzeste Wege (von einem Knoten aus)
311
18.5 Kürzeste Wege (von einem Knoten aus) Wir wollen von den anfangs erwähnten Variationen des ErreichbarkeitsProblems nur eine skizzieren: Wenn wir nicht nur wissen wollen, ob ein Knoten I/ von X aus erreichbar ist, sondern a.ucli seine Entfernung von z benötigen, müssen wir unseren Algorithmus leicht adaptieren. (Die Strategie bleibt aber die gleiche.) Die Menge black wird jetzt nicht mehr als boolcscher Array realisiert, sondern als i n t - oder f loat-Array, in dern jeweils die (zurzeit hekarinte) minimale Entferriung von X steht. (Unerreichbarkeit, also die Entfernung „unendlichN, muss dabei geeignet codiert werden.) Ebenso müssen die Knoten in der Merige gray rnit ihrer (jeweils aktuell bekannten) Miriinialdistanz zu X anriotiert sein. Der entscheidende Punkt ist die Bildung der Menge newgray auf der Basis von neighbours(y). Sei z ein solcher Knoten, also z E neighbours(y). Dann gibt cs zwei Fälle: 0
0
-
z ist noch weiB: Dann wird z in black eingetra.gen, wobei seine Entfernung gerade die (bekannte) Länge des Pfades X -0- y plus die Kantenlänge y z ist. z ist schon grau oder schwarz. Dann addieren wir ebenfalls die Lä.nge des Pfadcs X -0- y zur Kantenlängc y z . Diese Distanz ist jetzt aber mit der (also der Länge eines anderen Pfades schon vorhandenen Distanz x -0-z von X nach z) zu vergleichen; genommen wird das Minimum der beiden Werte.
-
Dijkstras Algorithmus:Eine etwa,sbessere Strategie wurde von Dijkstra vorgeschlagen. Sein Algorithmus sieht in Pseudocode folgendermaßen aus; die zentrale Operation r e l a x wird im Anschluss erläutert: -
NodeSet reachable ( Graph g , Node ~ o d e ~black et = 0; NodeSet white = i n i t (g,x) ; while (white != @) Node y = min(white) ; black = black U {Y); r e l a x ( w h i t e , y) ;
) // PSEUDOCODE !!! // anfangs leer // (siehe Text) // solange es wezj'e K n o t e n gibt // nimm kleinsten we$en K n o t e n // färbe i h n schumrz // akt~~alisiere Nachbarn
X
1 returnblack; )
Zunächst werden allc Knoten in die w c i k Mengc aufgeriornmeri und dabei mit ihrcr momentanen Entfernungsschatzung znztzalzszert: d. h., alle Knoten erhalten die Entfernung „uriendlichLL; nur X selbst erhält die Eritferriiirig 0. Solange es noch weiiie Knoten gibt, wählen wir jeweils den mit der minirnalen Bewertung aus und färben ihn schwarz. (Bcim ersten Durchlauf ist das r selbst.) Dann aktualisieren wir mittels der Operation r e l a x alle
312
18 Graphen
Nachbarn z von y. Das heiKt, wir bilden das Minimum der bisherigen Entfernungsschätzung für z (möglicherweise oo) und der Summe des Weges X - 0 - y pliis der Kante ?J-z. Diese neue Entfernung wird in der Menge white an den entsprechenden Knoten vermerkt. Damit dieser Algorithmus möglichst schnell läuft, wird die Menge white am besten als Priority Q u e u e (vgl. Abschnitt 16.3.7) implementiert; diese Struktur erlaubt den schnellen Zugriff auf das minimale Elernent einer Menge (und war deshalb auch Teil des Heapsort-Algorithmus in Abschnitt 8.3.5). Übung 18.3. Man implementiere einen Algorithmus zur Berechnung der minimalen Distanz aller Knoten von einem gegebenen Knoten X.
18.6 Aufspannende Bäume Unser Erreichbarkeits-Algorithmus stellt noch weitere attraktive Möglichkeiten bereit. Die Idee der Menge newgray ist es, die jeweils noch nicht besuchten Na.chfolger von y zu erfassen. Wenn wir von y aus Kanten zu diesen Knoten bilden, erhalten wir insgesamt eine neue Datenstruktur, und zwar einen B a u m mit X als Wurzel, der zu jedem von X aus erreichbaren Knoten eirien Pfad enthält. Dieser B a u m ist e i n Teilgraph des Originalgraphen. Dieses Konzept lässt sich leicht so modifizieren, dass man zu jeder (strengen) Zusammenhangskorripo11e11te genaii einen derartigen Baum erhält. Dieser wird dann als ein aufspannender B a u m der Komponente bezeichnet. Wenn Inan zu jeder Zusammenhangskomponente einen solchen Baum erzeugt, spricht man von einem aufspannenden Wald. Die Bedeutung dieser Struktur ist, dass man über sie alle Knoten des Graphen erreicht, ohne aber auf Doppelverarbeitung oder gar Zyklen achten zu müssen. Wenn man einen Graphen oft traversieren muss, kann es sich daher lohnen, zuerst den aufspannenden Baum (bzw. Wald) zu bilden und dann niit den einfacheren Traversierungsalgorithmen auf diesem Baum zu arbeiten.
Beispiel. Der Konfliktgraph aus Ahb. 18.2 enthält z. B. den aufspannenden Wald aus Abb. 18.7. Dabei sind z. B. B und H die Wurzeln der beiden
Abb. 18.7. Aufspannender Wald zum Graphen in Abb. 18.2
aufspannenden Bäinne. (Wie man sieht, gibt es zu einem Graphen i. Allg. sehr viele aufspannende Bäinrie.)
18.7 Transitive Hülle
313
18.7 Transitive Hülle In den Algorithmen des vorigen Abschnitts sind wir immer von einem Knoten X ausgegangen. In vielen Fragestellungen will man aber die entsprechenden Beziehungen zwischen allen Knoten berechnen.
Beispiel. Aus einem Wegegraphen wie in Abb. 18.1 will man oft eine Entfernungstabelle ableiten, die die Gesarntentfernung zwischen je zwei beliebigen Städten wiedergibt. (Solche Tabellen finden sich üblicherweise in StraBenkarten oder Taschenkalendern.) Auch hier gibt es die üblichen Variationen: 0
0
Wenn wir nur wissen wollen, ob es eine Verbindung gibt, reichen uns boolesche Werte aus. Man spricht dann auch von der transitiven Hülle G* des Graphen G. Wir betrachten irn Folgenden aber den Fall, dass wir die rninirnalen Entfernungen wissen wollen.
Aufgabe: Alle minimalen Wege Gegeben: Ein kantenbewerteter Graph G (mit nichtnegativen Werten). Gesucht: Die minimalen Entfernungen zwischen allen Knoten. Voraussetzung: Der Graph ist statisch, d. h., er ändert sich wahrend der Berechnung nicht. Eine naive Lösung bestiiride darin, einfach den Dijkstra-Algorithmus für Erreichbarkeit zu nehrnen und ihn nacheinander auf alle Knoten anzuwenden. Das würde aber zu viel Doppelarbeit fiihren. Eine bessere Lösung wurde von Floyd bzw. Warshall vorgeschlagen.'
Methode: Relaxatzon und „Färbungfi (nach Floyd und Warshall) Die Grimdidee des Verfahrens lässt sich wieder gut erklären, indem wir die Knoten in Gedanken ,färben1': Am Anfang sird alle Knoten weiß.; der Graph enthält die Originalkanten. In jedem Schritt färben wir einen beliebigen weißen Knoten schwarz. Der Graph wird dabei immer so verändert, dass er für jedes Knotenpaar die Länge des kürzesten Pfades wiedergibt, der nur über schwarze Zwischenknoten läuft. (Anfangs- und Endknoteri dürfen weiB sein.) -
-
Beispiel: In Abb. 18.8 ist die Arbeitsweise des Floyd-Warshall-Algorithmus illustriert. Zunächst sind alle Knoten weiß., alle Kanten haben ihre i~rspriingliclie Bewertimg. In Bild (b) haben wir den Knoten B schwarz gefärbt. Das
' Warshall hat 1962 die Variante für reine Erreichbarkeit
(auf booleschen Adjazenzmatrizeri) angegeben; Floyd hat im selben Jahr die Variante für die minirnalcn Wege beschrieben.
314
18 Graphen
(a) Initialisierurig
(b) 1.Schritt
(C)
2.Schritt
Abb. 18.8. Illustration des Floyd-Warshall-Algorithmus
-
erlaubt jetzt den neuen Weg A- B D. Weil dieser Weg kürzer ist als die alte Kante A D , wird die Bewertung dieser Kante entsprechend adaptiert. In Bild (C) habcn wir als Nächstes den Knotcn D schwarz gefärbt. Damit entstehen die Wege A - D - B , A-D-E, B-D-E, A-B-D-A und A B D E . Dic Effekte der beiden letzten (langen) Pfade sind schon in denen der anderen (kurzen) Pfade enthalten, weshalb wir sie grundsätzlich ignorieren können. Also betrachten wir die drei kurzen Pfade: Der erste ist länger als die Kante A- B und bewirkt daher keine Änderung. Die beiden anderen Wege führcri dagegen zur Einführung rieuer Kanten mit den entsprechenden Bewertungen.
-
-- -
Übung 18.4. Man vervollständige den Prozess des obigen Beispiels, indem man der Reihe nach die Knoten C. E und A schwarz färbt.
Lösungsentwurf Wir geben die Lösung wieder in Pscudocodc an. Zunächst crzcugcn wir eine Kopie des Graphen (weil wir i. Allg. das Original rioch brauchen i d es deshalb nicht zerstören sollten). Das Schwarzfärben repräsentieren wir diesmal dadurch, dass wir die jeweils verbleibenden weiBeri Knoten in einer Menge white halten. Graph distances ( Graph graph ) ( // Graph g = graph. copy 0 ; NodeSet white = g.nodes0 ; while (white ! = 0) Node y = arb(white); white.delete(y); f oral1 a,b E neighbours(y) ( dist = g.edge(a, y) + g.edge(y g.updateEgde(a,b,dist);
I//forall )//~uhile return g ; )//distances
PSEUDOCODE !!! // Original erhalten // anfangs alle Knoten wezlß // solange es wezge Knoten gibt // beliebigen Knoten auswählen // schwarz fil:rber~, // alle betroffenen Pfade ,b) ; // neue Lunge // K m t e aktualisieren
18.7 Transitive Hülle
315
Wenn wir einen Knoteri schwarz gefärbt haben, betrachten wir die diirch ihn möglichen neuen Wege und berechnen jeweils ihre Länge. Die Operation updateEdge hat einen von drei möglichen Effekten: Wenn noch keine Kante existiert, dann wird eine mit der entsprechenden Länge hinzugefügt (vgl Abb. 18.8(C)).Wcnn eine Kante existiert, abcr die Entfernung gröficr ist als das neue d i s t , dann wird die Bewertung aktiialisiert (vgl. Abb. 18.8 (b)). Andcrnfalls blcibt die Kantc invariant.
Evaluation: Aufwand: In1 Laufe des Verfahrens wird jeder Knoten schwarz gefärbt. Für jcden Knoten wcrden dann alle Paare von Nachbarn betrachtet, was irn schlinimsten Fall n,%irid, wobei n die Zahl der Knoten des Graphen ist. Dcr Aufwand ist also O(n3). Implementierung in Java Die Implementierung kann sehr effizient erfolgen, wenn man spezifische Darstellungen wählt: Der Graph sollte als Atljazerizrr~atrixtiargestellt werden. Dabei werden nicht vorhandcne Kantcn an1 besten durch die Entfernung „CO" repräsentiert. Da. fiir die Ma.trix-Da.rstellungdie Knoten ohnehin von 0, . . . , n - 1 durchnuninieriert sind, kann nian auf die Menge white vcrzichtcn; die Knotcn werden einfach der R.eille nach schwarz gefsrbt; d. h., die while-Schleife kann durch eine f or-Schleife ersetzt werden. Der Kern dcr Schlcifc ist die Behandlung aller Paare von Nachbarn von y (die f orall-Schleife im obigen Pseudocode). Wegen unserer Entscheidung, nicht vorhandene Kanten durch ce zu repräsentieren, wird die Operation updat eEdge einfach zu G Ca1 Cbl = min(G Ca1 [bl , G Ca1 Cyl + G Cyl Cbl 1 ; In der Matrixdarstellung hat man die Menge neighbours(y) nicht direkt zur Verfiigung. Die Schleife f o r a l 1 a , b E neighbours (y) wird deshalb durch eine doppelte Schleife über alle Knotenpaare ersetzt: f o r ( a = O ; a < n ; a++) 1 f o r (b = 0 ; b < n ; b++) 1 GCal Cbl = min(GCa1 Cbl , G Ca1 Cyl + G Cyl Cbl 1 ; I3 A n r n e r h n y : Diese Doppelschleife behandelt zwa.r viele Knoten, die nicht zu neighbours(y) gehören, aber das ist harmlos, weil diese nicht vorhandenen Kanten durch oo dargestellt sind und somit bei der Miriiniunibildung nicht schaden. Der Mehraufwand könnte nur vermieden wcrden, wcnn nian zusätzlich die Mengcn neighbours rnitschlcppen wiirde was i. Allg. noch teurer ist. -
316
18 Graphen
Übung 18.5. Man implementiere den Floyd- Warshall-Algorithmus in
.JAVA.
Übung 18.6. Man vergleiche den Aufwand des Floyd-Warshall-Algorithmus mit dem Aufwand, der bei der naiven Lösung mit dem iterierten Dijkstra-Algorithmus entsteht.
18.8 Weitere Graphalgorithmen Unsere obigen Beispiele sollten genügen, um einc erste Vorstellung von Graphalgorithmen zu bekommen. Natürlich gibt es noch eine Fiille von anderen Aufgabenstellungcn im Zusammcnhang mit Graphen. Beispiele: F l u s s m a x i m i e r u n g : Graphen können auch Flussnetze darstellen (Verkehrsfluss, Materialfluss etc.). Dabei sind die Kanten niit der maximalen Kapazität der entsprechenden Leitimg markiert. An den Knoten können die Materialflüsse gesplittet und zusammengeführt werden. Die Aufgabe ist dann, den maximalen Fluss zwischen (je) zwei Punkten zu bestimmen. Umleitungen: Prüfe, ob es zwischen zwei gegebenen Knoten mindestens zwei disjunkte Wege gibt. (Man nennt diese Knoten dann „zweifacl~verbunden".) ,,Handlungsreisender": Finde einen minimalen Weg, der eine bestimrnte vorgegebene Menge von Knoten überdeckt. Graphfärbung: Insbesondere für Konfliktgraphen ist folgendes Problem spannend: Gegeben sei eine limitierte Anzahl von ,,Farben"; kann man die Knoten des Graphen so färben, dass keine zwei benachbarten Knoten die gleiche Farbe erhalten? Eine Variante fragt nach der kleinsten Zahl von Farben, mit denen das geht (der sog. chromatischen Zahl des Graphen). Anwendungen für dieses Problem gibt es a . B. im Compilerbau, wo man versucht, mit den verfügbaren Registern der Maschine eine optiniale Verwaltung der Programmvariablen zu erreichen, oder in Stundenplänen, wo z. B. zwei Vorlesungen, die dieselben Personen oder Raume betreffen, nicht glcichzeitig stattfinden können. Ein berühmtes Problcm der Mathematik war die These, dass für die Färbung einer Landkarte (also für einen Konfliktgraphen im Stil von Abb. 18.2) immer vier Farben ausreichen, egal wie kompliziert die Grenzen verlaufen. Dabei gilt allerdings, dass hier nur eine Teilklasse von Graphen auftritt, nämlich sog. planare Graphen.2 Zusammenhangslcomponenten: Wir haben bereits die (slreiigen) Ziisammenhangskomponenten erwähnt. Eine offensichtlich notwendige Aufgabe ist es, ihre Identifizierung zu programrnieren. P
!
Vor einiger Zeit wurde ein Beweis dieses Theorems erbracht, der erstmalig in der Geschichte der Mathematik unter massivem Computereinsatz geführt wurde. Allerdings betrachten viele Leute das Problem nach wie vor als offen, weil sie große Zweifel an der Korrektheit der verwendeten Programme haben. -
18.8 Weitere Graphalgorithmen
317
,,Gleichheit" von Graphen: Für zwei gegebene Graphen kann man die Frage stellen, ob sie „strukturell gleich" sind. (Mathematisch spricht man dann von Isomorphze.) Das heißt, gibt es eine 1-1-Zuordnung der Knoten und Kanten der beiden Graphen, sodass sie gleich werden? In der Darstellung der Adjazenznlatrizen kann man das Problem so forrnulieren: Lassen sich die Knoten des einen Graphen so innnummerieren (und die Zeilen und Spalten der Matrix entsprechend permutieren), dass die Matrix des anderen Graphen entsteht? Eine Variante ist die Prüfung, ob ein Graph Teilgraph eines anderen Graphen ist. Viele dieser Probleme fallcn in die Kla.sse der sog. NP-vollständigen Probleme. Das sind Probleme, von denen man bis heute nur exponentielle Algorithmen kennt; aber die Frage ist ungeklä.rt, ob sie nicht doch mit polynoniialerri Aufwand lösbar wären. In der Praxis behilft man sich mit Heuristiken, dic üblichcrweise ausreichend schncll und gut funktionieren.
Weiterführende Literatur Die in dicscrn Kapitel angegebenen Algorithmen sind nur ein klciner Ausschnitt dcssen, was in dcr Literatur zu Graphalgorilhnien bekannt ist. Eine größrre Übersicht bieten z. B. die Lelirbiiclier von Alm und Ullrnan 11, 21 sowie von Cormen r t al. [9] und Sedgewick 143, 4/11,
Teil V1
Programmierung von Software-Systemen
Moderne Programm,ierung besteht schon lüngst nicht rrlel~raus dem Entwerfen von - mehr oder weniger cleveren, Alyorithrnen oder ,von mehr oder weniger genialen Datenstrukturen. Man hat es v i e h e h r mit der Lösun,g korryletter Aufgaben zu tun, die aus dem Zusammenspiel zahlloser Aspekte bestehen. Die Ein- und Ausgabe auf Dateien, Drucker, Bildschirme gehört eben,so dazu wie Z ~ ~ g r i f af e ~ das ~ f interne Rechernetz oder soyar das ganze Internet. Den Benutzern, sind grafische Schnittstellen, sog. GUIs, anzubieten. Das wiederum führt dazu, dass Teile des Programms parallel zueinander ausgeführt werden müssen. Und so weiter. Viele dieser Aktivitäten können potenziell auf „wohl definierte Fehlersituationen" Jühren. Das klin,gt zwar wie ein On;ym,oron, ist aber schon vernünft i g . Denn Situationen wie -ThreadTest. LIMIT; i--) .
C
,
-10 ) C ta.setPriority( tb.getPriority0 + 1 ) ; 1/ / l j i f (i
==
)//for System.out.println(" )//run )// end of class ThreadB
B done") ;
Der Effekt ist, dass nach der Ausgabe von "B : -10" der Tliread tb imterbrochen wird und ta sciric gesamte Arbeit vollständig erledigen darf, bevor tb wieder an die Reihe koni~ntund seinerseits den Rest seiner Arbeit Es scheint a.uf dem Markt auch Versionen der JVM zu gcben, in denen das falsch implementiert ist, sodass t b zu Ende läuft, bevor ta z u m Zuge konimt.
21 Konkurrenz belebt das Geschäft: Thrrads
366
21.3 Synchronisation und Kommunikation Parallele Threads wären ziemlich nutzlos, wenn sie keine Inforrnationeii austa.uschcn könntcri. Unter dcn verscliiedencn Arten der Komniunikation paralleler Prozesse, die man in der Literatur findet, hat JAVA sich fiir die einfachste Form entschieden: Die Threads greifen auf die gleichen Variablen zu. Korrlrnuriikation heifit in JAVA: Dcr eirie schreibt, der a.ridere liest. Aber auch das h a t seine Tücken. Wenn ein Prozess Variablen schreibt, die auch von eirieni anderen Prozess (lescrid oder schreibend) genutzt werden, können crstaunlichc Dinge passicrcn. Der Grund ist, dass viele Operationcn, die uns als „atomaru erscheinen, in Wirklichkeit aiis niehreren Teiloperationen bestehen. Und das bedeutet, dass sie an den unpa.ssendsten Stellen unterbroclien werden können. Irn Programm 21.4 illustrieren wir das Problem dadurch, da.ss wir lokale Hilfsvariableri verwenden. Wir betrachten eirie Klasse Account fiir Bankkonten. Diese Konten haben cirien Kontostand balance und verfügcn über zwei Methoden zum Einzahlen bzw. Abheben. Nehmen wir an, dass ein Thread t 1 eine Summe einzahlt, und Programm 21.4 Synchronisierter Zugriff auf Bankkonten c l a s s Account C p r i v a t e long balance ; i d d e p o s i t ( l o n g amount ) C
// ausbuchen aux
=
aux + amount ;
// einbuchen i d withdraw ( long amount )
C // ausbuchen
i f (aux >= amount) C aux = aux - amount ; t h i s .balance = aux;
// einb~rch,en
1/ / i f )//withdraw ) / / e n d of d a s s Account
ein anderer Thread t2 geriau die gleiche Summe abhebt. Danach sollte das Konto unverändert sein. Und meistens ist das auch so. Wenn das Schlüsselwort synchronized auf das wir gleich genauer eingehen nicht da wä.re, könnte aber Mi~rpliy'sLaw zuschlagen und dcn Ablauf in Abb. 21.4 geschehen lassen. Wie dieses Szenario zeigt, hängt die Korrektheit dcr beidcn Mcthoden a.deposit 0 und a.withdrawo für das Konto a entscl~eidenddavon ab, dass sie imnier vollständig ausgefiihrt werden. Das heifit, -
-
2 1.3 Synchronisation und Kommunikation
t2
tl a.deposit (200) long aux = balance; aux = aux + amount;
balance
=
( u m ) a.withdraw(200) (1000) (1200) long aux = balance; aux = aux - amount;
(aux)
=
Konto 1000
(1000) (800)
aux; balance
367
aux:
1200 800
Abb. 21.4. Pathologischer Ablauf bei Threads
es darf kcine andere Methode auf dem Konto a gleichzeitig ausgeführt werden. Genau das stellt das Schlüsselwort synchronized sicher.
Definition (synchronized Methode, Lock) Wenn eiri Thread t eine synchronized-Methode ausführt, dann erhält er ein sog. Lock aiif das Objekt. Weil eiri Objekt zu jedem Zeitpunkt liöchsteris ein Lock tragen darf, kann kein anderer Thrcad einc synchronizedMethode auf dein Objekt ausführen, solange t das Lock hält. Man muss nicht irnrrier ganze Methoden schützen. Oft genügt es, ciricn gcwissen Block von Anweisungen zu schiitzen. Und das blockierte Objckt muss auch nicht immer dasjenige sein, das die Methode ausfiihrt. Man karin irgendeiii Objekt blockieren, je nachdem, was in der jeweiligen Applikation angemessen ist. Programm 21.5 zeigt ein typisches Beispiel. Das Vertauschen
void swap ( Object [I array, int i, int j ) C synchronized (array) C Object aux = array Ci] ; array [il = array [j1 ; array [jl = aux;
)//synchronized l//.swap
von Array-Elementen braucht eine Zwischenspeicherung in einem Hilfseleriicnt aux. Dic Bcfehlsfolgc darf unter keinen Umstanden von cincm anderen Thread unterbrochen werden. Deshalb wird der Array mit einem Lock versehen.
Definition (synchronized Block, Lock) Man kann a,uch einen Anweisungsblock absichern, indem man schreibt synchronized(«Objekt») 1 «Anweisungen» } Das Lock wird dann aiif das angegebene Objekt gesetzt.
2 1 Konkurrenz belebt das Geschäft: Threads
368
Das zeigt übrigens, dass die synchronized-Methoden nur eine Schreibabkürzung zur Sprache hinzufiigeri.
Kurzform Lunqform synchronizedvoid f o o ( ...I C void f o o ( . . . ) ((Rumpf»
I
I
3
Wcnn eine Methode als synchronized deklariert wird, dann ist ihr ganzer Rumpf geschützt, und das ~ o c k wird auf das Objekt ( t h i s ) selbst Mit Hilfe des Schliisselwortes synchronized kann eine Methode „atomaru gemacht werden. Zum Vcrständriis muss man aber zwei Dinge bcachtcn: Es werden nicht Methoden atonlar gernacht, sondern Objekte blockiert. Wcnn das Objekt neben synchronisierteri Methoden auch (gefährliche) niclitsyncliroiiisicrte Methoden bereitstellt, ist der Schutz wcg. Definition (Monitor) Wenn ein Objekt nur private Felder hat i i r d diese nur mit synchror~izecMethoden bcarbeitet werden, 6 i i i i irenrrt wari es eirieii Monitor buicki~Monitore spielen eine wichtige Rolle oeirn Maiidgeri~ei~t kcmplrxcr qysteme vor1 pan alleleri Prozessen. (Sic sind rine relativ robuste, aber ri.cliL die r u , g e Art der Prozcssstci~erung,gei~auereDctails findet man z E i i - 1231) 21.3.1 Vorsicht, es klemmt!
Mit der Lc
1
Als Ergebnis erhalten wir jetzt einen Taschenrechner mit etwas gröj3eren Tasten. Anmerkung: Man sollte tunlichst vermeiden, für äußere Container feste Größen vorzugcbcn, denn das führt oft zu merkwürdigcn Verhältnissen. Stattdessen sollte man so vorgehen, dass die „natürlichen" Größen sich vm iririen nach außen autornatisch bestimmen.
23.4 Selbst Zeichnen Die GUI-Klasscn, die wir in diesem Kapitel besprochen haben, besitzen jeweils ein bestimmtes, vordefiriiertes Ersclieiniirigsbild. Welche Miiglichkeiten haben wir aber, selbst etwas zu zeichnen? In Abb. 23.16 wiederholen wir noch einmal das Bild von Abb. 23.9 auf Seite 395, allerdings mit kleinen Ergänzungen. Das tatsächliche Zeichnen er-
Komponenten
Spexhw
Abb. 23.16. Prozess der Fenstcrgenerierurig
folgt im AWT-Systemdurch sog. Graphics-Objekte. Diese Objekte belierrscher1 Operationen wie drawline, drawRectangle, drawcircle, drawstring etc. (von denen wir schon einige in der - für das Buch vordefinierten Klasse Pad kennen gelernt haben; s. Tab. 4.3 in Abschnitt 4.3.7). -
406
23 GUI: Layout
Das Zusammenspiel zwischen dem Programm, den GUI-Komponenten und dem Graphics-Objekt ist etwas kompliziert. Es basiert auf den drei Methoden r e p a i n t , update und p a i n t , die in der Klasse Component definiert sind und deshalb in jeder Kornpoiieiite verfügbar sind. In Abb. 23.17 wird das Grundprinzip illustriert (vgl. auch [33]).Es gibt im Wesentlichen zwei Gründe,
6.~0%~ .......................
Manager ...................
C .p a i n t (g) c.update(g)
P
\ C .p a i n t (g)
?~a?
"*.2, Abb. 23.17. Zeichnen in1 AWT
weshalb ein (Neu-)Zeichnen von GUI-Komponenten notwendig werden kann:
Von der Umgebung initiiert: Weriri ein Fenster zum ersten Mal auf dein Bildschirm erscheint oder wenn es voriibergeliend von einem anderen Fenster verdeckt war iirid jetzt wieder siclitlmr wird, dann informiert die Urrigebung den AWT-Manager,dass alle oder zumindest einige der Komponenten gezeichnet werden müssen. Der AWT-Manager ruft dann für alle betroffenen Komponenten C deren Mcthode C .p a i n t (g) auf (s. nächster Abschnitt). V o m Programm initiiert: Wenn das Programm entscheidet, dass eine Komponente C auf dem Bildschirm geändert werden soll, dann riifi, es die Methode C .r e p a i n t 0 dieser Komponente auf. Diese Methode iniorrniert den AWT-Manager,dass die Komponente C gezeichnet werden muss. Der ~ w ~ - M a n a gruft e r dann aber nicht direkt C . p a i n t (g) auf, sondern die Methode C .update (g) , die darin ihrerseits C .p a i n t (g) aufriift, riaclidern sie den Hintergrund der Kornporieiite gelöscht hat. Man beachte: Der Aufruf von r e p a i n t 0 ist die einzige Art, 71m a,us dem Program,m heraus das Neuzeichnen uon Komponenten, zu heulirken,. Die Methodm p a i n t lind update sollten nie direkt u7~fgeruienwerden! Anrncrkung: Dcr klcinc Umweg iiber update soll die Flexibilität crhöhen. Versierte Programmierer können durch R,edefinition der Methode update das Löschen unterdrücken und so spezielle Eff'cktcrealisicrcn. In der Praxis kornrnt das a.bcr so gut wie nie vor.
23.4.1 Die Methode p a i n t Die Methode p a i n t enthält die Instriiktiorien, mit denen die betreffende Koniponerite gezeichnet wird. Wie in Abb. 23.17 xii sehen ist, wird diese Metho-
23.4 Selbst Zeichnen
407
de nicht dirckt aus dem Prograrrirn heraus aufgerufen, sondern vom AWTManager. Dieser gibt beim Aufruf das zuständige Graphics-Objekt g (s. Abb. 23.16) als Argument mit. Dieses Graphics-Objekt g führt dann das eigentliche Zeichnen aus. Jede der vordefinierten Kornpoiier~terlarteri also Label, JLabel, Button, JButton usw. besitzt eine vordefinierte paint-Methode, die alle iiotweridigen Zeichenroutiiieri enthält. Das heiKt, als Programmierer braucht man sich im Normalfall nicht mit diesen Fragen zu befassen. Wenn man aber unbedingt selbst etwas dazirzeiclinen will, dann muss man eine Subklasse der entsprechenden Aw~-Klassedefinieren und dort die Methode p a i n t redefinieren. (Das gilt nur für AWT!Bei SWING ist es anders s. unten.) Beispiel: -
-
-
. .. p u b l i c void p a i n t ( Graphics g ) { «eigene Zeichenroutinen» l//paint
// selbst Zeichnen (nur
AWT!)
I / / end of MyButton Das Zeichnen selbst wird von dem Graphics-Objekt g aiisgefiihrt. Das geschieht über Methoden wie g .drawLine ( . . .) etc. Darauf gehen wir gleich in Abschnitt 23.4.4 ein. JAVA ist hier Cbrigens irrk:onsequen,t! Nach den iiblichen Spielregel11 für Methoden in Subklassen müsste die redefinierte paint-Methode mit der Zeile s u p e r . p a i n t (g) ; beginnen, damit (im obigen Beispiel) zunächst ein normaler Button gezeichnet wird, bevor unsere zusätzlichen Zeichenaktioneri stattfinden. Aber bei p a i n t macht JAVA eine Ausrialirne: Hier findet das p a i n t der Superklasse auch dann statt, wenn wir es nicht explizit aufrufen.
I n SWING geht das anders! Das Redefinieren der Methode p a i n t kann in SWING-Komponentenzu unvorhersagbaren Effekten führen, weil dort p a i n t eine Menge interner Arbeit lcistet. Wenn wir in SWING sclbst zeichricn wollen, müssen wir paintcomponent 1)eriiitzen (s. nächster Abschnitt). 23.4.2 Die Methode paintcomponent
In SWING-Korriporieritenruft die Methode p a i n t (g) irnrrier die Methode paintcomponent (g) auf. Sie ist in der Klasse JComponent definiert und steht daher in allen SWING-Komponentenzur Verfügung. Wenn man in eine SWING-Komponentehineinzeichnen will, dann muss r m r i eine Subkla,sseeinführen und die paintcomponent-Methode redefinieren. Beispiel:
23 GUI: Layout
408
c l a s s M y B u t t o n e x t e n d s JButtonC
... p u b l i c v o i d paintcornponent (Graphics g)
// Zeichnen
(SWING)
«eigene Zeichenroutinen» )//paint Componen,t
... )// end of MyButton Ansonstcri ist die Situation analog zu p a i n t bei AWT.Insbesondere gilt auch hier, dass s u p e r . paintcornponent (g) implizit ausgeführt wird. 23.4.3 Wenn man nur zeichnen will
. ..
In dcn vorausgegangencn Abschnittcn hatten wir gesehen, dass Inan in beliebige Komponenten wie Buttons, Labels etc. selbst hineinzeichnen kann, indem man p a i n t bzw. paintcornponent redefiniert. Wie geht rnan aber vor, wenn man nur auf eine leere Fläche zeichnen will? In AWT ist es üblich, dazu dic Klasse Canvas zu nehmen. In SWING wird davon abgeraten; stattdessen sollte man die Klasse JPanel benutzen. In beiden Fällen wird dann die Methodc p a i n t bzw. paintcornponent redefiniert. Beispiel 23.7 Olympische Ringe
Wenn wir das RingProgram aus Programm 4.2 in Abschnitt 4.3.6 direkt i n realisieren wollen, ohne die spezielle Klasse Pad zu benutzen, dann müssen wir folgende eigene Klasse einführen:
SWING
c l a s s MyPad extends JPanel C publicvoidpaintComponent ( G r a p h i c s g ) g.setColor(Color.red); g . drawOval(x[O], y C01 , r a d , r a d ) ;
... )//paintComponent )//end of MyPad Die Anmieismgen, zum Zeichnen,, die uiir i m Programm 4.3 in der Methode draw zusammengefasst hatten, werden jetzt i n die Methode paintcornponent gesch~ieben. (Man kann natürlich auch die Methode draw beibehalten und darm irr paintcornponent nur draw(g) aufrufen.) Aujlerdem muss ein Ob.jekt MyPad pad = new MyPadO erzeugt ~ ~ inn dein geeignetes Fenster der Art JFrame ein,gehettet werden. (All das wird von, Pad
automatisch miterledigt.) Wic rnan an diesem Beispiel siebt, werden Operationen wie s e t c o l o r , drawline, drawRect, drawOval etc. von dem Graphics-Objekt g ausgrfiihrt. Sie sind in dcr Klasse Graphics definiert (s. Abschnitt 23.4.4).
23.4 Selbst Zeichnen
409
Anmerkung: Die für das Buch vordefinierte Klasse Pad stellt die Operationen setcolor,drawline,drawRect,drawOval etc. direkt zur Verfügung; intern werden sie einfach an das entsprechendeGraphics-Objektdurchgereicht.Es gibt aber weitere Unterschiede.So ist z.B. cine Methode drawcircle als Spezialfall von drawOval verfügbar, und anstelle der X- und y-Komponente kann m a n in den Zeichenoperationen auch ein Ob~jektvom Typ Point angeben.
23.4.4 Zeichnen mit Graphics und Graphics2D Wie wir in den vorausgegangencn Abschnitten gesellen haben, werden die eigentlichen Zeichenoperationen wie drawline, drawRect etc. von Objekten der Klasse Graphics ausgefülirt, die in Abb. 23.18 auszugsweise gezeigt ist.
1
abstract class Graphics void drawLine(xl,yl,x2,y2) void drawRect(x,y,wd,ht) void draw0val (X,y ,wd,ht) void fill~ect(x,y,wd,ht) void fillOval(x,y,wd,ht)
I
I
Linie von (x1,yl) nach (x2,y2) Rechteck Ellipse gefülltes Rechteck gefüllte Ellipse
...
I I
I I
I I I I
I
I I I
void setFont(f ont) Font getFont ( ) void drawString(text.x,y)
setze aktuellen Font aktueller Font
II I
I
schreibe text an die Stelle (x,y) I
...
I
I
void setColor(c) Color getColor0
setze Farbe auf aktuelle Farbe
C
I
I I I
I
...
I
Abb. 23.18.Die Klasse Graphics (Ausschnitt)
Mit der Version JAVA 1.2 wurde cine Subklassc Graphics2D eingeführt, die einige Schwächen von Graphics repariert und einige zusätzliche Möglichkcitcn einführt. Untcr a.nderem wird die Positionierui~gvon int-Koordinaten auf f loat-Koordinaten umgestellt, was in manchen Anwendungen dcutlich bessere Bilder hervorbringt. Als besonders nützliche Erweiterung werden die Spezialfälle drawline, drawRect , drawOval ctc. auf einc gemeinsame Methode void draw ( Shape s ) zuriickgeführt, die beliebige „ShapesUzeichnen kann. Fiir weitere Details verweisen wir auf dic Litcratur, insbcsonderc 1141, 1331 und vor allem [26].
Hallo Programm!
-
Hallo GUI!
Bisher haben wir beschrieben, wie das grafische Erscheinungsbild eines GUI gestaltet werden kann. Jetzt müssen wir uns damit befassen, wie die Intcrak-
I
Benutzer
m
pGUI G
&
q
vonstatten g e h . Dabei niüssen wir zwei grundsätzlich verschiedene Aspekte berücksichtigen: 0
0
Zum einen gibt es die Frage, lme vom Programm aus die Inhalte von GUIKomponenten abgefragt (,,EingabeLL) und verändert („Ausgabeu) werden können. Zum arldren muss man wissen, wann es sich lohnt, die Inhalte von Komponenten zu lesen: Man muss imnier genau dann reagieren, wenn der Benutzer aktiv geworden ist (Taste auf der Tastatur gedrückt, Maus geklickt etc.).
Der erste Punkt wird mit den Methoden bewerkstelligt, die wir in den vorausgegangenen Abschnitten (auszugsweise) vorgestellt haben; wir fassen das noch einmal ini nächsten Abschnitt 24.1 zusamnien. Der zweite Punkt erfordert das genaue Studium eines speziellen Interaktions-Modells, das in JAVA beriiitzt wird. Dieses Modell ist Gegenstand der Abschnitte 24.2 und 24.3.
24.1 Auf GUIs ein- und ausgeben Wir fassen hier noch eirirnal kurz zusarrimen, wie die Ein-/Ausgabe auf GUIKomponenten erfolgt. Betrachten wir dazu unser durchgängiges Beispiel des Ta.sclienrec1iners. Allerdirigs rnodifizicren wir da.s Progra.mni so, dass das JTextField für da.s Eingaberegister auch schreibbar ist; d. h., wir setzen in Beispiel 23.2 ini Koristruktor Register das entsprechende Attribut mittels setEditable(true).
412
24 Ha110 Programm!
-
Hallo GUI!
Jetzt können wir Zahlen nicht nur mithilfe der Maus über die Buttons eingeben, sondern sie auch direkt über die Tastatur in das Displayfeld eintippen. Nehnien wir an, wir geben den String 123456789 ein iirid benutzen dariri die Maus, um den Teilstring 345 zu selektieren. Dann erhalten wir die Situation von Abb. 24.1. Wenn im Programm an der passenden Stelle - folgendes -
Abb. 24.1. Taschenreclirier mit selektierter Eingabc
Codefragment steht (s. Abb. 23.6) S t r i n g t e x t = register.get~electedText0; ~erminal.println(">>> " + text); ... dann erscheint auf der Standardausgabe der String >>> 345 Dieses Beispiel zeigt, dass man mithilfe der Operationen g e t . . . 0 und s e t . . . 0 auf GUI-Komponenten ähnliche Ein-/Ausgabe betreiben kann wie mittels r e a d ( ) und w r i t e ( ) auf dem Terminal. Aber dieses Beispiel deutet auch schon das Hauptproblern an: Wie ,wissen wir, wanm GUI-Inhalte gelesen werden sollen? Die Lösung dieses Problerris ist Gegenstand des nächsten Abschnittes.
24.2 Von Ereignissen getrieben
...
Die folgenden Programme benötigen den Import irnport jaua. awt . event . *! Wir hatten schon früher erwähnt, dass die Programmierung mit GUIs nach einem Modell abläuft, wie es in Abb. 24.2 skizziert ist. Dieses Modell basiert auf folgenden Prinzipien:
24.2 V o n Ereignissen getrieben . . .
413
Abb. 24.2. Layout und Interaktion
Jede GUI-Komponente ist in der Lage, unterschiedliche Events („Ereignisse") auszulöscn. Welche Ereignisse ausgelöst werden, hängt von den Aktionen ab, die der Benutzer vornimmt (Taste drücken, Maus klicken, Maus verschieben etc.). Die Generierung der zu den Aktionen passenden Events übernimmt das AWT-System,ebenso wie die Entscheidung, zu welcher GUI-Komponente das Event gehört. Zu jeder GUI-Komponente k a m man Objekte assoziieren, die auf gewisse Arten von Events ,,lauschen". Diese Objekte heiBen deshalb Listener. Wenn an einer GUI-Komponente ein Event auftritt, dann leitet die Komponente dieses Event an alle assoziierten Listener weiter (abhängig von der Art des Events). In den Listenern wird der Code programmiert, der beim Auftreten des jeweiligen Events aiisgefiihrt werden soll. Im Folgenden wollen wir diese Konzepte Stück für Stück erarbeiten. Dabei orientieren wir uns wie üblich an unserem durchgängigen Beispiel des Taschenrechners. Anmerkung: Bei genauerer Analyse zeigt sich, dass der ganze Listener-Apparat in JAVA nur deshalb benötigt wird, weil man Methoden nicht zii Parametern von anderen Methoden machen kann. W i r hatten das gleiche Phänornen schon in dcn Nurnerikbeispieleri vor1 Abschnitt 9.4 und 9.5 kennen gelernt, wo wir mithilfe des Interfaces Fun einen umständlichen Workaround programmieren mussten (vgl. auch Abschnitt 13.6). Mit den Listeriern wird gcnau der gleiche Workaround für das GUI-Management eingeführt (wa,sman durchaus als Designschwäche von JAVA sehen darf).
414
24 Hallo Programm!
-
Hallo GUI!
24.3 Immerzu lauschen
...
Listener sind Objekte, die auf Ereignisse ,,lausclienL'.Das heifit, sie werden vom A W T - S ~ Sgetriggert, ~ ~ I ~ sobald ein entsprechendes Ereignis eintritt. Uni das Ganze etwas griffiger zu machen, betrachten wir zunächst ein Beispiel. 24.3.1 Beispiel: Eingabe im Displayfeld Wir betrachten wieder unseren Taschenrechner und nehmen an, dass wir zum Testen - jedes Mal, wenn das: Feld von1 Benutzer geändert wird (Ziffer eingetippt, Ziffer gelöscht etc.), den aktuellen String auf dem Terminal ausgeben wollen. Um das Objekt displayHandler aus Abb. 24.2 zu generieren, benötigen wir eine entsprechende Klasse. Diese Klasse muss nach den AWT-Prinzipienein passender Listerier sein, in unserem Fall ein TextListener, der in Abb. 24.3 bcschriebcn ist. (Auf die verschiedenen Arten von Listeriern gehen wir gleich in Abschnitt 24.3.3 ein.) Wie man erkennen kann, verlangt das Interface -
........................................................................................................................ ._......................................................................................................................
i
i
.. ..
: