Einführung in die Programmierung in Java Stephan Euler FH-Giessen–Friedberg Fachbereich MND Version 0.95 14. Januar 2009 Wintersemester 2008 ii Dieses Skript wurde mit LATEX 2ε und TEX(Version 3.141592) erstellt. Eingesetzt wurde die integrierte Benutzeroberfläche WinEdt 5.3 zusammen mit MiKTeX Version 2.2. Inhaltsverzeichnis 1 Allererstes Java-Programm 1.1 Klasse für Beispielprogramm 1.2 Übersetzen und Ausführen . 1.3 Hinweise zur Formatierung . 1.4 Erste Rechnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Ganzzahlige Datentypen und erste Programme 2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . 2.2 Ganzzahlige Datentypen . . . . . . . . . . . . . 2.3 Variablen . . . . . . . . . . . . . . . . . . . . . 2.4 Ausdrücke und Wertzuweisungen . . . . . . . . 2.5 Rechnen mit Integerwerten . . . . . . . . . . . . 2.5.1 Bit-Operatoren . . . . . . . . . . . . . . 2.5.2 Inkrement und Dekrement Operator . . 2.5.3 Vereinfachte Zuweisung . . . . . . . . . . 2.5.4 Integer-Quiz . . . . . . . . . . . . . . . . 2.6 Übungen . . . . . . . . . . . . . . . . . . . . . . 3 Abläufe 3.1 Logische Ausdrücke . . . . . . . 3.2 Rangfolge der Operatoren . . . 3.3 if Abfrage . . . . . . . . . . . . 3.4 Else-If . . . . . . . . . . . . . . 3.5 Fragezeichen-Operator . . . . . 3.6 Die switch-Anweisung . . . . . 3.7 Schleifen . . . . . . . . . . . . . 3.7.1 Vorzeitiges Verlassen von 3.7.2 Sprünge . . . . . . . . . 3.8 Beispiele . . . . . . . . . . . . . 3.9 Übungen . . . . . . . . . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 3 4 5 . . . . . . . . . . 7 7 7 8 10 11 12 13 14 14 14 . . . . . . . . . . . 17 17 19 20 21 21 22 23 25 26 27 28 iv INHALTSVERZEICHNIS 4 Verwendung von Gleitkomma-Zahlen 4.1 Gleitkomma-Zahlen . . . . . . . . . . . . . 4.1.1 Gleitkomma- Darstellung . . . . . . 4.1.2 Verwendung von Gleitkommazahlen 4.1.3 Vergleich der Zahlenformate . . . . 4.2 Gleitkommazahlen in Java . . . . . . . . . 4.2.1 Mathematisch Funktionen . . . . . 4.3 Umwandlung zwischen Datentypen . . . . 4.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 31 36 37 37 39 40 42 5 Felder 5.1 Zugriff auf Elemente . . . . . . . . . . . . . . . . . . . . . . . . . 5.2 Mehrdimensionale Felder . . . . . . . . . . . . . . . . . . . . . . 5.3 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45 47 49 50 6 Methoden 6.1 Einleitung . . . . . . . . 6.2 Definition . . . . . . . . 6.3 Überladen von Methoden 6.4 Übergabe von Feldern . 6.5 Rekursion . . . . . . . . 6.6 Anmerkungen . . . . . . 6.7 Übungen . . . . . . . . . . . . . . . . 53 53 54 56 57 58 59 59 . . . . . 61 61 65 66 69 69 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Algorithmen – vom Problem zum Programm 7.1 Algorithmen . . . . . . . . . . . . . . . . . . . 7.2 Flussdiagramm . . . . . . . . . . . . . . . . . 7.3 Struktogramme . . . . . . . . . . . . . . . . . 7.4 Aktivitätsdiagramm . . . . . . . . . . . . . . . 7.5 Übungen . . . . . . . . . . . . . . . . . . . . . 8 Objektorientierte Programmierung 8.1 Einleitung . . . . . . . . . . . . . . . . . . 8.2 Objektorientierte Programmierung (OOP) 8.3 Objektorientierte Programmiersprachen . . 8.4 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 73 74 76 77 9 Klassen 9.1 Einleitung . . . . . . . . . . . . . . . . . . . . 9.2 Klassendefinition . . . . . . . . . . . . . . . . 9.2.1 Zugriff auf Attribute . . . . . . . . . . 9.3 Instanz- und Klassenvariablen und Methoden 9.4 Konstruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79 79 79 81 82 84 . . . . INHALTSVERZEICHNIS 9.5 9.6 9.7 v Destruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namenskonventionen . . . . . . . . . . . . . . . . . . . . . . . . . Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Objekte und Klassen 10.1 Einleitung . . . . . . . . . . . . . . . . . . 10.2 Basisklassen . . . . . . . . . . . . . . . . . 10.3 Klasse Fahrzeug . . . . . . . . . . . . . . . 10.4 static Elemente . . . . . . . . . . . . . . . 10.5 Konstruktoren und die Klasse Auto . . . . 10.5.1 Die Klasse Auto . . . . . . . . . . . 10.5.2 Verkettung von Konstruktoren . . . 10.6 Die Klasse Verleih . . . . . . . . . . . . . . 10.7 Überlagerung von Methoden . . . . . . . . 10.7.1 Dynamische Suche . . . . . . . . . 10.8 Attribute . . . . . . . . . . . . . . . . . . 10.9 Mehrfachvererbung und Interfaces . . . . . 10.9.1 Einleitung . . . . . . . . . . . . . . 10.10Beispiel FH-Verwaltung . . . . . . . . . . 10.11Beispiel Sortieren . . . . . . . . . . . . . . 10.11.1 Interface in eigener Klasse . . . . . 10.11.2 Interface integriert in andere Klasse 10.12Anonyme Klassen . . . . . . . . . . . . . . 10.13Lokale Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 Zeichenketten 11.1 Einleitung . . . . . . . . . . . . . . . . . . . . 11.2 Datentyp char . . . . . . . . . . . . . . . . . . 11.3 Konstruktoren für String . . . . . . . . . . . . 11.4 Länge von Zeichenketten und einzelne Zeichen 11.5 Arbeiten mit Zeichenketten . . . . . . . . . . 11.6 Teilketten . . . . . . . . . . . . . . . . . . . . 11.7 Vergleichen und Suchen . . . . . . . . . . . . . 11.7.1 Vergleichen . . . . . . . . . . . . . . . 11.8 Verändern von Zeichenketten . . . . . . . . . . 11.8.1 Suchen . . . . . . . . . . . . . . . . . . 11.9 Tokenizer . . . . . . . . . . . . . . . . . . . . 11.10Konvertierungen . . . . . . . . . . . . . . . . . 11.11Die Klasse String ist endgültig . . . . . . . . . 11.12Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 86 86 . . . . . . . . . . . . . . . . . . . 89 89 89 91 92 94 94 96 97 98 99 100 102 102 103 105 106 107 108 109 . . . . . . . . . . . . . . 111 111 111 112 113 113 115 115 115 117 118 119 120 120 121 vi INHALTSVERZEICHNIS 12 Reguläre Ausdrücke 123 12.0.1 Gefundene Teile . . . . . . . . . . . . . . . . . . . . . . . . 126 12.1 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 13 Tools 13.1 Einleitung . . . . . . . 13.2 jar . . . . . . . . . . . 13.3 java und Klassennamen 13.4 Klassennamen . . . . . 13.5 javadoc . . . . . . . . . 13.6 jdb . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 Ein- und Ausgabe und Dateien 14.1 Einleitung . . . . . . . . . . . . . . . . 14.2 Standardeingabe und Standardausgabe 14.2.1 Ausgabe . . . . . . . . . . . . . 14.2.2 Formatierte Ausgabe mit printf 14.2.3 Eingabe . . . . . . . . . . . . . 14.2.4 Einlesen mit Scanner . . . . . . 14.3 Streams, Reader und Writer . . . . . . 14.4 Reader . . . . . . . . . . . . . . . . . . 14.4.1 Schachtelung von Readern . . . 14.4.2 Übersicht Reader . . . . . . . . 14.5 Writer . . . . . . . . . . . . . . . . . . 14.6 PrintWriter . . . . . . . . . . . . . . . 14.7 Streams . . . . . . . . . . . . . . . . . 14.8 Random Access File . . . . . . . . . . 14.9 Die Klasse File . . . . . . . . . . . . . 14.10Übungen . . . . . . . . . . . . . . . . . 15 Exception 15.1 Einleitung . . . . . . . . . . . . . . . 15.2 Beispiel . . . . . . . . . . . . . . . . 15.3 try - catch Anweisung . . . . . . . . 15.4 Hierarchie von Ausnahmefehlern . . . 15.4.1 Die Klasse Error . . . . . . . 15.4.2 Die Klasse Exception . . . . . 15.4.3 Die Klasse RuntimeException 15.5 Eigene Exceptions . . . . . . . . . . . 15.6 finally-Block . . . . . . . . . . . . . . 15.7 Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 129 129 131 132 133 134 . . . . . . . . . . . . . . . . 137 137 137 138 139 139 141 141 142 143 144 145 147 148 148 149 151 . . . . . . . . . . 153 153 154 155 156 157 157 158 159 159 160 INHALTSVERZEICHNIS 16 Dynamische Datenstrukturen 16.1 Vector . . . . . . . . . . . . . . . . . 16.1.1 Konstruktor und Einfügen von 16.1.2 Zugriff auf Elemente . . . . . 16.1.3 Iterator . . . . . . . . . . . . 16.1.4 Wrapper Klassen . . . . . . . 16.1.5 Stack . . . . . . . . . . . . . . 16.2 Assoziativspeicher . . . . . . . . . . . 16.2.1 Hashtable . . . . . . . . . . . 16.2.2 Properties . . . . . . . . . . . 16.2.3 Bäume . . . . . . . . . . . . . 16.3 Methoden in der Klasse Collections . 16.3.1 Beispiel Kartenspiel . . . . . . vii . . . . . . . Elementen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 163 163 164 165 166 167 168 168 171 172 173 173 17 Erweiterungen WS08 177 17.1 Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177 17.2 Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180 17.3 close . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183 Literaturverzeichnis 185 viii INHALTSVERZEICHNIS Kapitel 1 Allererstes Java-Programm 1.1 Klasse für Beispielprogramm Java ist eine moderne und leistungsfähige Programmiersprache. Die Sprache wurde etwa ab 1991 bei der Firma Sun entwickelt. Java übernimmt wesentliche Elemente aus C und C++, verzichtet aber auf einige der komplizierten, fehlerträchtigen Möglichkeiten. Die Sprache ist stärker in Richtung Klarheit und Einfachheit ausgerichtet. Zur Unterstützung des Benutzers enthält Java ein automatisches Speichermanagement sowie eine umfangreiche Bibliothek u.a. mit Graphikmöglichkeiten. In der Form von Applets können Java Programme in Web-Seiten eingebunden werden. Java ist eine objektorientierte Sprache und basiert auf dem Konzept von Klassen. Was das bedeutet werden wir im Laufe der Vorlesung sehen. Zunächst geht es darum, die Grundelemente der Sprache zu erlernen. Wir werden dazu zunächst einfache Programme untersuchen, bei denen die Klassen noch keine Rolle spielen. Die Grundelemente sind bis auf wenige Ausnahmen sehr ähnlich zu C und C++. Daher können die Programme leicht in diese oder weitere, verwandte Sprachen übertragen werden. Um Programme ausführen zu können, benötigen wir allerdings einen entsprechenden Rahmen mit einer Klasse. Das bedeutet, wir müssen in jedem Fall eine Klasse anlegen, in die wir das Programm einbetten – selbst wenn in der Anwendung selbst keinerlei objektorientierte Techniken Verwendung finden. Betrachten wir einen konkreten Fall. Dazu generieren wir mit aus einer Entwicklungsumgebung heraus eine Klasse Ue1. Die Entwicklungsumgebung – in diesem Fall BlueJ – erspart uns einige Tipparbeit und generiert automatisch ein Standardmuster für Klassen. Man kann aber genau so gut mit irgend einem Editor arbeiten und den Text selbst eingeben. Wichtig ist dabei, dass die Datei den Namen der Klasse mit der Endung .java bekommt. Das Muster hat folgende Form: /** * Beschreiben Sie hier die Klasse Ue1. 1 2 KAPITEL 1. ALLERERSTES JAVA-PROGRAMM * * @author (Ihr Name) * @version (eine Versionsnummer oder ein Datum) */ public class Ue1 { // Definieren Sie ab hier die Klassenvariablen für Ue1 // Definieren Sie ab hier die Objektvariablen für Ue1 // Definieren Sie ab hier die KOnstruktoren für Ue1 /** * Konstruktor für Objekte der Klasse Ue1 */ public Ue1() { // Objektvariable initialisieren } // Definieren Sie ab hier die Methoden für Ue1 /** * Diese Methode leistet.... * * @param Parameter... * @return Rückgabewert... */ void sageHallo() { System.out.println( "Hallo" ); } } Im Kopf steht zunächst ein Kommentar. Kommentare beginnen mit einem /* und enden mit einen */. Sie können mehrere Zeilen umfassen. Kommentare können nicht geschachtelt werden. Kommentare haben keinerlei Einfluss auf die Ausführung des Programms. Sie dienen lediglich der Dokumentation. Der erste Kommentar in dem Beispiel enthält den Namen der Klasse sowie das Datum der Erstellung. Klassennamen beginnen üblicherweise mit einem Großbuchstaben. Allerdings ist dies lediglich eine allgemein anerkannte Konvention, keine Pflicht. Der zweite Kommentar hat eine spezielle Form beginnend mit /** und dient als Eingabe für eine automatischen Erstellung der Dokumentation. Wir werden diesen Kommentar fürs erste ignorieren. Mit der Zeile public class Ue1 { 1.2. ÜBERSETZEN UND AUSFÜHREN 3 wird eine Klasse Ue1 angelegt. Der zu dieser Klasse gehörende Code steht in einem Block begrenzt durch die geschweiften Klammern. Die Entwicklungsumgebung hat in der Klasse Platz für Konstruktoren und Methoden angelegt. Konstruktoren dienen zur Erzeugung von Objekten. Sie tragen den gleichen Namen wie die Klasse. Die für uns relevante Stelle ist die Methodendefinition void sageHallo() { System.out.println( "Hallo" ); } Damit wird eine Methode mit dem Namen sageHallo definiert. Methoden sind in sich abgeschlossene Blöcke, die als Einheit ausgeführt werden. Beim Aufruf kann man gegebenenfalls Parameter übergeben und am Ende (bei der Rückkehr) können Methoden ein Resultat – den Rückgabewert – liefern. In anderen Programmiersprachen spricht man von Funktionen, Prozeduren oder Modulen und die Parameter werden oft als Argumente bezeichnet. Zur Ausgabe steht in Java die Methode System.out.println zur Verfügung. Ein Text kann direkt in der Form System.out.println( "Hallo" ); ausgegeben werden. Der Text wird durch zwei Anführungszeichen begrenzt und wird als Parameter an die Methode übergeben. Das Ende einer Anweisung wird durch das Semikolon markiert. Die Methode übernimmt dann die Aufgabe, diesen Text am Bildschirm anzuzeigen. Es ist wie wir noch sehen werden eine recht universelle Methode, die alle Arten von Parameter in jeweils sinnvoller Weise ausgeben kann. Die Endung ln steht für einen Zeilenumbruch, d. h. nach der Ausgabe springt der Cursor in eine neue Zeile. Ist dies nicht gewünscht, so kann man auf eine Variante System.out.print zurückgreifen. 1.2 Übersetzen und Ausführen Die Klasse ist jetzt fertig und soll ausprobiert werden. Verwendet man eine Entwicklungsumgebung, so genügt in der Regel das Klicken auf einen entsprechenden Knopf. Mehr Einblick in den Vorgang erhält man, wenn man die notwendigen Schritte einzeln ausführt. Dazu benötigt man ein entsprechendes Fenster (Eingabeaufforderung unter WinXX, shell unter Linux). Am einfachsten geht man dann zu dem Verzeichnis, in dem die Java-Datei steht. Dort führt man den Befehl javac Ue1.java aus. Damit wird der Java-Compiler javac gestartet. Der Compiler überprüft das Programm auf formale Korrektheit. Findet er einen oder mehrere Fehler, so werden diese mit einer mehr oder weniger hilfreichen Meldung angezeigt. Vergisst man beispielsweise das abschließende Semikolon, so erhält man die Fehlermeldung 4 KAPITEL 1. ALLERERSTES JAVA-PROGRAMM Ue1.java:23: ’;’ expected System.out.println( "Hallo" ) ^ 1 error inklusive Angabe der Stelle (Zeile 23), an der der Fehler gefunden wurde. Ist das Programm fehlerfrei, so erzeugt der Compiler eine Datei mit dem gleichen Namen und der Endung .class in dem selben Verzeichnis. Mit dieser Datei wird durch den Aufruf java Ue1 schließlich das Programm ausgeführt. (Wichtig: die Endung .class darf bei diesem Aufruf nicht angegeben werden.) An dieser Stelle unterscheidet sich Java von vielen anderen Programmiersprachen. Es wird keine für sich alleine lauffähige Anwendung erzeugt, sondern der Code wird durch die Java-Maschine ausgeführt. Zur Ausführung wird der Code in den im Allgemeinen mehreren .class–Dateien interpretiert und in ausführbare Befehle umgesetzt. Dieser Ansatz hat mehrere Vorteile: • Unabhängigkeit vom Betriebssystem • Kleine Dateien, d. h. schnelle Übertragung, geringer Platzbedarf • Höhere Flexibilität Dem stehen zwei Nachteile gegenüber: • Zusätzlicher Aufwand für die Interpretation bedeutet langsamere Ausführung • Auf Zielsystem muss eine Java-Maschine installiert sein (heute in aller Regel gegeben) 1.3 Hinweise zur Formatierung Java erlaubt eine freie Gestaltung des Programmcodes. So akzeptiert der Compiler durchaus auch folgende, wenig empfehlenswerte Form eines Beispielprogramms public class Ue1{public static void main(String args[]){System.out.println( "Hallo");}} Gerade wegen dieser großen Freiheit ist eine Strukturierung im Sinne einer besseren Lesbarkeit und Wartbarkeit essentiell. Einige Grundsätze: • Beginn eines Programms in der ersten Spalte 1.4. ERSTE RECHNUNG 5 • In der Regel nur eine Anweisung pro Zeile • Blöcke einrücken • Leerzeilen und Leerzeichen zur Gliederung einsetzen • Auch mal einen Kommentar einfügen (aber mit nützlicher Information, nicht Selbstverständlichkeiten) 1.4 Erste Rechnung Als erstes Beispiel für eine Berechnung betrachten wir folgende Methode etwas genauer: void rechneStunden() { int stunden = 6; int wochen = 14; int gesamt = stunden * wochen; System.out.println( "Gesamtstunden: " + gesamt ); } Als Name wurde rechneStunden gewählt. Die Methode braucht nichts zurückzugeben. Dies wird durch das Attribut void (engl. für leer, nichtig) angezeigt. In den runden Klammern nach dem Methodenname können Parameter stehen. Bis auf weiteres werden wir dies nicht benötigen. Aber die Klammern müssen trotzdem an dieser Stelle bleiben, damit dies als gültiger Methodenname erkannt wird. Schließlich folgt der Inhalt der Methode in dem durch geschweifte Klammern markierten Block. Der Block beginnt mit den Variablenvereinbarungen. Jede Variable muss vor ihrer Verwendung in dieser Form angelegt werden. Die Variablenvereinbarungen müssen allerdings nicht am Anfang des Blocks stehen. Eine Variablenvereinbarung besteht aus • Typbezeichnung • Name • optionaler Anfangswert (nach einem = Zeichen) Anweisungen werden in Java durch ein Semikolon beendet. In dem Beispiel handelt es sich um den Typ int für Integer. Darunter versteht man ganze Zahlen. Die so deklarierten Variablen können nur ganze Zahlen (2, 22, -3376, etc.) enthalten. Die ersten beiden Variablen werden mit Anfangswerten belegt. Dazu folgt nach dem Namen ein =-Zeichen und dahinter der Wert. Der Inhalt der Variable gesamt wird auf das Ergebnis der Multiplikation gesetzt. Die beiden beteiligten Variablen werden nicht verändert. Schließlich wird das Ergebnis mit dem Aufruf println ausgegeben. 6 KAPITEL 1. ALLERERSTES JAVA-PROGRAMM Kapitel 2 Ganzzahlige Datentypen und erste Programme 2.1 Einleitung Eine Speicherzelle in einem Computer enthält nur ein Muster mit Werten von 0 und 1. Erst durch die Festlegung, wie dieses Bitmuster zu interpretieren ist, wird der tatsächliche Wert festgelegt. Man spricht dann von einem Datentyp. Der Datentyp legt darüber hinaus fest, welche Operationen mit Elementen dieses Typs erlaubt sind. Einige Datentypen sind ganze Zahlen, Zahlen mit Dezimalpunkt an fester Stelle, logische Ausdrücke, Zeichen, Gleitkommazahlen oder etwa komplexe Zahlen. Verschiedene Programmiersprachen unterstützen in der Regel unterschiedliche Typen. Dabei kann es sein, dass eine Sprache einen Typ gar nicht enthält (z. B. gibt es in Java keine komplexen Zahlen) oder dass sich die Datentypen in Realisierungsdetails unterscheiden. Im folgenden wird zunächst für ganze Zahlen die Realisierung in Java beschrieben. 2.2 Ganzzahlige Datentypen Für die Darstellung von (positiven und negativen) ganzen Zahlen verwendet man den Datentyp Integer (engl. any positive or negative whole number or zero, Webster´s Dictionary). Der Grundtyp int ist 4 Byte groß und enthält Zahlen in 2er-Komplement Darstellung. Der Wertebereich ist dann von -2147483648 bis +2147483647 Konstanten für int können sowohl als Dezimalwerte als auch in den beiden anderen Zahlensystemen angegeben werden. Für Oktalzahlen fügt man eine führende 0 ein (Beispiel 0156), bei Hexadezimalzahlen schreibt man 0x. . . (Beispiel 0xFF). Das Vorzeichen wird durch - oder ein optionales + gekennzeichnet. Neben dem Basistyp int gibt es einige weitere Typen: 7 8KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME Typ byte short int long Speicherbedarf 1 Byte 2 Byte 4 Byte 8 Byte Wertebereich -128 . . . 127 -32768 . . . +32767 -2147483648 . . . +2147483647 -9223372036854775808 ... 9223372036854775807 Es stellt sich die Frage, was passiert bei Bereichsüberschreitungen? Betrachten wir ein Beispiel: int zahl = 2147483647; System.out.println( "i: " + zahl ); ++zahl; // Erhoehe zahl um 1 (Kurzschreibweise) System.out.println( "i: " + zahl ); Die Ausgabe lautet: i: 2147483647 i+1: -2147483648 Indem wir eine 1 zu der größten positiven Zahl addiert haben, sind wir zu der kleinsten negativen Zahl gelangt. Wie in Bild 2.1 dargestellt, kann man sich die Zahlen im Kreis angeordnet vorstellen. -1 0 1 +n ? -2147483648 2147483647 Abbildung 2.1: Anordnung von Integerzahlen 2.3 Variablen In der Regel will man mehrfach auf eine Speicherzelle zugreifen und beispielsweise • einen Wert zuweisen 2.3. VARIABLEN 9 • den Inhalt verändern • den Inhalt abfragen und ausgeben Dazu gibt man der Speicherzelle einen Namen (Bezeichner), unter dem man immer wieder auf sie zugreifen kann und spricht dann von einer Variablen. Dabei gilt • eine Variable hat einen bestimmten Typ • eine Variable muss vor der ersten Benutzung festgelegt (vereinbart) werden • eine Variable kann den Typ nicht ändern Insgesamt kann man sagen, der Name gibt an, wo die Werte gespeichert sind und der Datentyp legt die Größe des Speicherbereichs sowie die Interpretation der Bitwerte fest. In Java wird eine Variable vereinbart (definiert), indem man den Typ und den Variablennamen zusammen angibt. Die Anweisung wird durch ein Semikolon ; abgeschlossen. Beispiel: int i; long anzahlDerStudenten; Mehrere Variablen gleichen Typs können gemeinsam definiert werden. Beispiel: int anzahlHoerer, anzahlHoererinnen; Der Name kann nach folgenden Regeln gebildet werden: • Der Name beginnt mit einem Buchstaben. • Anschließend folgt eine beliebige Folge von alphanumerischen Zeichen. • Der Unterstrich (underscore) kann wie ein Buchstabe eingesetzt werden. • Groß- und Kleinschreibung wird unterschieden. • Java Konvention: Namen von Variablen beginnen mit einem Kleinbuchstaben. Neben den festen Vorgaben sollte man folgende allgemeine Hinweise beachten: • Namen sollten passend und weitgehend selbsterklärend (aussagekräftig) sein (nicht int eineintvariable;). • Namen von Integer Variablen fangen oft mit Buchstaben von i bis n an. • Für kurzlebige Variablen können kurze Bezeichnungen wie i, j, k verwendet werden. 10KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME • Konsequent sein: wenn sich die Bedeutung einer Variable im Programm ändert, auch ihren Namen anpassen. • Die Struktur innerhalb eines Namens wird mit Groß- / Kleinschreibung markiert: – anzahlStudenten – strahlungsDauer • Einheitliche Sprache (Englisch oder Deutsch). • Konsistenz! Ein einmal eingeführter Stil für Namen, Einrückungen, etc. sollte beibehalten werden. Bei großen Software-Projekten werden oft Namenskonventionen verbindlich vorgegeben. 2.4 Ausdrücke und Wertzuweisungen Aus im allgemeinen mehreren Variablen und Konstanten kann man mit entsprechenden Operatoren Ausdrücke bilden. Zum Beispiel ergibt i + 10 + 123 die Summe der drei Werte. Ausdrücke können Variablen zugewiesen werden. Die Syntax in Java ist Variable = Ausdruck Das Zeichen = bezeichnet man als Zuweisungsoperator. Der Ausdruck auf der rechten Seite wird berechnet und dann in die Variable auf der linken Seite gespeichert. Dies ist nicht zu verwechseln mit der Bedeutung als Gleichheitszeichen. In der Sprache Pascal wird dies deutlicher durch das Zeichen := zum Ausdruck gebracht. Beispiel 2.1 Zuweisungen int i; i = 5; i = 5 * 6; Hier werden nacheinander die Werte 5 und 30 in die Variable i geschrieben. Mit jeder Zuweisung wird der alte Wert überschrieben. Die Variable darf auch in dem Ausdruck auf der rechten Seite vorkommen. Bei der Auswertung des Ausdrucks werden die Variablen nicht verändert, sondern es wird nur mit den Werten gerechnet. Erst durch die Zuweisung wird ein neuer Wert in den Speicher geschrieben. So gesehen wird eine Variable auf der rechten Seite genauso behandelt wie eine 2.5. RECHNEN MIT INTEGERWERTEN 11 Konstante. Der prinzipielle Unterschied zwischen Variablen und Konstanten liegt in der Adressierbarkeit. Beide belegen Speicher und sind mit einem Datentyp verknüpft, aber bei Variablen kann man auch den Speicherplatz mit neuen Werten füllen. In diesem Sinn hat die Variable m in m = m + 67; zwei verschiedene Bedeutungen. Auf der rechten Seite der Anweisung ist der Inhalt der Speicherzelle gemeint. Dieser Wert wird zu 67 addiert. Das Ergebnis wird dann in die mit m bezeichnete Speicherzelle geschrieben. Man spricht auch von R-Wert und L-Wert (rvalue, lvalue) wenn man die Bedeutung auf der rechten bzw. linken Seite meint. Mit den englischen Begriffen kann man sich unter den Namen auch read value und location value vorstellen. Eine Konstante kann nur als R-Wert benutzt werden. Eine Anweisung in der Art 7 = 3 + 4; ist weder sinnvoll noch erlaubt. In Java kann (und sollte) bereits bei der Definition einer Variablen ein Wert zugewiesen werden ( int anzahlHoerer = 100;). Eine Variable ohne zugewiesenen Wert wird mit dem Wert 0 initialisiert. 2.5 Rechnen mit Integerwerten Java unterstützt die Grundrechenarten mit den Operatoren +, -, * und /. Daneben gibt es noch den Modulo-Operator %, der den Rest bei der ganzzahligen Division ergibt. Für die Operatoren gilt die übliche Hierarchie. Bei gleichberechtigten Operatoren wird der Ausdruck von links nach rechts abgearbeitet. Bei der Division muss man berücksichtigen, dass das Ergebnis wieder ein Integer Wert ist und damit ein eventueller Rest verloren geht. Dadurch spielt bei komplexeren Ausdrücken u. U. die Reihenfolge eine Rolle. Bei ungeschickter Reihenfolge kann es passieren, dass Zwischenergebnisse nur mit eingeschränkter Genauigkeit berechnet werden. In Folge des damit verbundene Fehlers führt die Auswertung nicht zu dem gewünschten Ergebnis. Beispiel 2.2 Auswertung von Integerausdrücken: Ausdruck 32 / 5 * 5 32 * 5 / 5 21 % 6 28 % 7 21 / 6 23 / 6 Ergebnis 30 32 3 (21 - 3 * 6 ) 0 3 3 Man kann die Reihenfolge durch Klammern () verändern. Bei der Auswertung werden zunächst die Ausdrücke in Klammern berechnet. Klammern können geschachtelt werden. Die Berechnung beginnt dann bei der innersten Klammer. 12KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME Dann wird die Klammer durch das Resultat ersetzt und die Berechnung fortgesetzt. Übung 2.1 Welchen Wert ergeben die folgenden Ausdrücke? 2 * 5 + 6 * 2 2 * ( 5 + 6 * 2) 2 * ( ( 5 + 6 ) * 2) 2.5.1 Bit-Operatoren Ein Bit-Operator betrachtet den Operanden als Folge von Bits. Diese Bitfolge kann manipuliert werden, indem beispielsweise alle Werte um eine gegebene Anzahl von Positionen geschoben werden. Bei der Verknüpfung zweier Operanden werden alle Bits Position für Position miteinander bearbeitet. Die Operanden für Bit-Operatoren müssen ganzzahlig sein. Im Einzelnen bietet Java folgende Bit-Operatoren: Operator ~ & | ^ << >> >>> Funktion bitweises NICHT bitweises UND bitweises ODER bitweises EXOR schieben nach links (shift) schieben nach rechts (shift) rechts shift, 0 nachschieben Anwendung ~ausdruck ausdruck1 & ausdruck2 ausdruck1 | ausdruck2 ausdruck1 ^ ausdruck2 ausdruck1 << ausdruck2 ausdruck1 >> ausdruck2 ausdruck1 >>> ausdruck2 Das bitweise NICHT invertiert jedes Bit des Operanden. Die drei Operationen UND, ODER und EXOR arbeiten entsprechend auf jedem einzelnen Bit. UND wird oft benutzt um Bits auszuschneiden, während mit ODER Bits gezielt gesetzt werden können. Beispiel: i = i & 0xF; i = i | 0xF0; // löscht alle außer den letzten vier Bits // setzt 4 Bits Die Shift Operatoren verschieben den linken Ausdruck um die im rechten Ausdruck angegebene Anzahl von Stellen. Dabei wird beim Schieben nach links stets mit 0 aufgefüllt. Schiebt man nach rechts, so wird bei der Variante >> bei negativen Werten das höchstwertige Bit auf 1 gesetzt. Bei der Variante >>> wird demgegenüber das höchstwertige Bit immer auf 0 gesetzt. Beispiel 2.3 Bit-Operatoren 2.5. RECHNEN MIT INTEGERWERTEN int i=15; // 13 Bitmuster 0000 1111 System.out.println("i: " + Integer.toBinaryString( i )); System.out.println("i << 1: " + Integer.toBinaryString( i << 1 )); System.out.println("i >> 2: " + Integer.toBinaryString( i >> 2 )); System.out.println("i | 0x70: " + Integer.toBinaryString( i | 0x70 )); System.out.println("i & 0xc: " + Integer.toBinaryString( i & 0xc )); System.out.println("~i: " + Integer.toBinaryString( ~i ) ); ergibt: i: 1111 i << 1: 11110 i >> 2: 11 i | 0x70: 1111111 i & 0xc: 1100 ~i: 11111111111111111111111111110000 2.5.2 Inkrement und Dekrement Operator In Java gibt es zwei spezielle Operatoren ++ und --, die eine Variable um 1 erhöhen oder erniedrigen. Diese Operatoren verändern den Operanden und erfordern daher einen lvalue. Sie können beispielsweise nicht auf Konstanten angewandt werden (++5 ist nicht erlaubt). Der Ausdruck mit dem Operator ist selbst wieder ein rvalue ( ++i = ...; geht nicht). Ungewöhnlich ist, dass die Operatoren vor (prefix) oder nach (postfix) dem Operanden stehen können. Im ersten Fall wird die Variable verändert, bevor sie weiter verwendet wird, im zweiten Fall wird erst der Wert benutzt, dann verändert. Mit n = 7 wird durch i = ++n; die Variable i auf 8 gesetzt, bei i = n++; auf 7. In beiden Fällen hat n anschließend den Wert 8. Man kann die Operatoren anwenden, ohne die Variable weiter zu benutzen, d. h. ++n; oder n--; sind mögliche Anweisungen und gebräuchliche Abkürzungen für n = n + 1; bzw. n += 1; (siehe nächster Abschnitt) oder n = n - 1; In diesem Fall spielt die Unterscheidung zwischen präfix und postfix keine Rolle. Ansonsten muss man bei komplizierteren Ausdrücken mit unbeabsichtigten Nebeneffekten rechnen. In jedem Fall leidet die Lesbarkeit des Programms. Gefährlich sind Querbezüge in der Art i = 4; i = i++ * 5; In solchen Fällen ist es besser, eine Zeile mehr zu schreiben und damit klar darzustellen, was gemeint ist. Selbst wenn das Programm tatsächlich das ausführt, was die Programmiererin oder der Programmierer wollte, ist die kompakte Schreibweise schwerer zu verstehen. Es ist auch nicht zu erwarten, dass das entstehende 14KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME Programm schneller läuft. In aller Regel wird der Compiler selbst durch Optimierung den effizientesten Code generieren. 2.5.3 Vereinfachte Zuweisung Häufig hat man Zuweisungen in der Art aktienImDepot = aktienImDepot + kauf; d. h. die Variable auf der linken Seite wird auch als erster Operand auf der rechten Seite benutzt: expr1 = expr1 op expr2 Java bietet dafür bei den meisten Operatoren mit zwei Argumenten die kompakte Schreibweise expr1 op= expr2 (ohne Leerzeichen zwischen op und =) an. Die wahrscheinlich am häufigsten in der Praxis auftretende Form ist die Verbindung mit der Addition oder Subtraktion: i += 5; Hauptvorteil ist die bessere Übersichtlichkeit. Insbesondere wenn der Ausdruck auf der rechten Seite komplex wird, ist in dieser Schreibweise sofort die Bedeutung ersichtlich. Die Schreibweise entspricht der Intention: i soll um 5 erhöht werden. 2.5.4 Integer-Quiz Die Variablen i und j seien vom Typ int. Welche Werte haben sie nach folgenden Anweisungen : i i i i i i i 2.6 = = = = = = = 32 / 6 + 7 % 2; 1 << 3; 0xf; i |= 0xf0; 4; j = i++ * 5; 1 & 2; 2; j = 3; i *= j + 1; 0777; i &= ~077; Übungen Übung 2.2 Welche Namen sind zulässige Bezeichner? • beta • ws01/02 2.6. ÜBUNGEN 15 • Gamma • ___test___ • dritte_loesung • ws2001_okt_08 • ss2001-07-08 • 3eck • monDay Legen Sie für die folgenden Übungen eine Übungsklasse (z. B. UebungenInteger) an. Die Lösungen zu den einzelnen Übungen können Sie dann als Methoden in dieser Klasse realisieren. Übung 2.3 Die Methode Integer.toBinaryString( int i ) erzeugt eine Zeichenkette mit der Darstellung des Wertes von i als Binärzahl. Verwenden Sie diese Methode um das Bitmuster folgender Ausdrücke auszugeben: • 185 • 185 & 0xf0 • 185 | 07 Welche anderen Methode stellt die Klasse Integer zur Verfügung, um Werte in verschiedenen Zahlensystemen darzustellen? Übung 2.4 Lassen Sie von einer 4-stelligen Dezimalzahl die Quersumme berechnen. Übung 2.5 Erstellen Sie in Ihrer Übungsklasse eine Methode testen(), in der folgende Fehlerfälle beim Rechnen mit Integer-Zahlen auftreten: • Bereichsüberschreitungen • Division durch 0 • Shift um mehr Stellen als vorhanden Welches Ergebnis erhält man jeweils? 16KAPITEL 2. GANZZAHLIGE DATENTYPEN UND ERSTE PROGRAMME Kapitel 3 Abläufe Bisher hatten wir nur einfache Programme betrachtet, bei denen die Anweisungen in einer linearen Reihenfolge abgearbeitet wurden. Damit lassen sich allerdings nur einfache Probleme lösen. Oft ist es notwendig, Anweisungen mehrfach auszuführen oder in Abhängigkeit von Zwischenresultaten oder etwa Benutzereingaben verschiedene Anweisungen auszuführen. Mit logischen Ausdrücken und entsprechenden Konstruktionen für Schleifen und Verzweigungen wird der Ablauf eines Programms gesteuert. Abhängig von dem Resultat des logischen Ausdrucks werden alternative Programmzweige durchlaufen, Schleifen beendet, Rückfragen an den Benutzer gestellt, und so weiter. 3.1 Logische Ausdrücke Der weitere Ablauf eines Programms soll in Abhängigkeit von einer Bedingung gewählt werden. Beispielsweise könnte man eine Fallunterscheidung zwischen positiven und negativen Werte benötigen. Dazu stellt Java eine Reihe von Vergleichsoperatoren bereit. Um zu testen, ob der Inhalt einer Variablen test positiv ist, kann man einen Ausdruck test > 0 verwenden. Das Result des Vergleichs ist entweder wahr (true) oder falsch (false). Wird das Ergebnis erst später benötigt, kann es in einer logischen Variablen abgelegt werden. Dazu steht der Datentyp boolean1 zur Verfügung. Variablen vom Typ boolean haben entweder den Wert true oder false. Beispiel 3.1 Verwendung von boolschen Variablen. int i = 6; boolean b1 = i < 5; boolean b2 = true; System.out.println("b1 = " + b1); 1 benannt nach George Boole, engl. Mathematiker 1815-1864, gilt als Begründer der mathematischen Logik 17 18 KAPITEL 3. ABLÄUFE Tabelle 3.1: Vergleichsoperatoren in Java == gleich != nicht gleich > größer >= größer gleich < kleiner <= kleiner gleich Tabelle 3.2: Logische Operatoren in Java ! Nicht & Und | Oder ^ exklusives Oder && Und mit Short-Circuit-Evaluation || Oder mit Short-Circuit-Evaluation System.out.println("b2 = " + b2); ergibt b1 = false b2 = true Tabelle 3.1 enthält eine vollständige Liste der Vergleichsoperatoren. Mehrere logische Ausdrücke können durch logische Operatoren verknüpft werden. So bezeichnet & die Und-Verknüpfung zweier Ausdrücke. Die logischen Operatoren sind in Tabelle 3.2 zusammen gestellt. Es werden wieder die gleichen Zeichen verwendet wie bei den entsprechenden Bit-Operatoren. Eine Verwechslungsgefahr besteht nicht, da Java anhand des Typs der Operanden erkennt, ob eine logische oder bitweise Verknüpfung gemeint ist. Beispiel 3.2 Logische Operatoren. n > 5 & n < 10 i == 2 | i == 4 | i == 6 Eine Besonderheit in Java ist die Unterscheidung zwischen Operatoren mit und ohne so genannter Short-Circuit-Evaluation. Bei der Short-Circuit-Evaluation nutzt man die Eigenschaften der Operatoren zu einer eventuell verkürzten Auswertung aus. Bei einer Und-Verknüpfung müssen beide Operanden wahr sein, damit der Ausdruck selbst auch wahr ist. Stellt man nun fest, dass der erste Operand den Wert falsch hat, so steht bereits fest, dass auch der gesamte Ausdruck den Wert falsch haben wird. Auf die Auswertung des zweiten Ausdrucks kann 3.2. RANGFOLGE DER OPERATOREN 19 dann verzichtet werden. Dadurch kann Rechenzeit gespart werden. Kompliziert wird das Verhalten, wenn bei der Auswertung des zweiten Ausdrucks Nebenwirkungen auftreten. Betrachten wir das Beispiel int n = 4; System.out.println( System.out.println( System.out.println( System.out.println( n > 5 "n = " n > 5 "n = " && n++ < 10 ); + n); & n++ < 10 ); + n); Die Bedingung n > 5 ist nicht erfüllt. Mit Short-Circuit-Evaluation wird der zweite Ausdruck nicht ausgewertet. Dementsprechenden behält die Variable n ihren alten Wert. Ohne Short-Circuit-Evaluation wird auch der zweite Ausdruck berechnet und der Wert von n erhöht. Das Programm liefert demnach die Ausgabe false n = 4 false n = 5 Man kann dieses Verhalten gezielt einsetzen und damit eine Art interne Ablaufsteuerung realisieren. Diesen Stil findet man in anderen Programmiersprachen wie C oder Perl recht häufig. Allerdings sollte man bei der Verwendung vorsichtig sein und sich auf wenige, klar verständliche Fälle beschränken. Beispiel 3.3 Typische Short-Circuit-Evaluation in der Programmiersprache Perl open(IN, ’<’ . $datei) || die "$datei nicht gefunden"; In der Variablen $datei steht der Name einer Datei. Mit open wird versucht, diese Datei zu öffnen. Falls dies nicht funktioniert, gibt open den Wert 0 zurück. Dann und nur dann wird der zweite Teil des Oder-Ausdrucks ausgeführt, in dem mit einer Fehlermeldung abgebrochen wird. Übung 3.1 Warum gibt es keine Form des exklusiven Oder mit Short-CircuitEvaluation? 3.2 Rangfolge der Operatoren In Integer-Ausdrücken können arithmetische und logische Operatoren, Vergleichsoperatoren, u. s.ẇ. auftreten. Die Reihenfolge der bisher behandelten Operatoren ist in Tabelle 3.3 zusammengestellt. Je weiter oben ein Operator in der Tabelle steht desto höher ist sein Rang oder man sagt, er bindet stärker. Am stärksten binden die Operatoren, die nur einen Operanden haben. 20 KAPITEL 3. ABLÄUFE Tabelle 3.3: Reihenfolge der Operatoren ++, -Inkrement, Dekrement ~ bitweise NICHT ! logisches NICHT +, Vorzeichen *, /, % multiplikative Operatoren +, Addition und Subtraktion <<, >> Bit shift <, <=, >=, > Vergleichsoperatoren ==, != Gleichheit, Ungleichheit & bitweises UND ^ bitweises XOR | bitweises ODER && logisches UND || logisches ODER 3.3 if Abfrage Die einfache Fallunterscheidung ist durch die if-else Anweisung realisiert. Die allgemeine Form ist: if( ausdruck ) { anweisung1 } else { anweisung2 } In der runden Klammer steht ein logischer Ausdruck. Abhängig von seinem Wert wird bei Resultat Wahr der erste Block ausgeführt, ansonsten der zweite. Der durch else eingeleitete Block ist optional und kann entfallen. Wenn ein Block nur aus einer einzigen Anweisung besteht, kann man die geschweiften Klammern weg lassen. Beispiel 3.4 if Abfrage if( i < 10 ) { System.out.println("Einstellige Zahl"); } else { System.out.println("Mehrstellige Zahl"); } kann auch als if( i < 10 ) else System.out.println("Einstellige Zahl"); System.out.println("Mehrstellige Zahl"); 3.4. ELSE-IF 21 geschrieben werden. Die Form ohne Klammern sollte nur für einfache und übersichtliche Fälle benutzt werden. 3.4 Else-If Häufig möchte man mehr als zwei Fälle unterscheiden. Dazu kann man die erweiterte Form mit else if benutzen: if( ausdruck1 ) { anweisung1 } else if( ausdruck2 ) { anweisung2 } else if( ausdruck3 ) { anweisung3 } else { anweisung4 } In dieser Form werden mehrere Überprüfungen geschachtelt nacheinander ausgeführt. Sobald eine Bedingung erfüllt ist, wird der zugehörige Block ausgeführt und die ganze Kette verlassen (selbst wenn weitere Bedingungen erfüllt wären). Der letzte (optionale) else Block behandelt dann alle Fälle, die von keiner Bedingung abgedeckt sind, Beispiel 3.5 Else-If Konstruktion if( i < 10 ) { System.out.println("Einstellige Zahl"); } else if( i < 100 ) { System.out.println("Zweistellige Zahl"); } else { System.out.println("Mehrstellige Zahl"); } 3.5 Fragezeichen-Operator Die Auswahl zwischen zwei Alternativen kann kompakt mit dem ?-Operator geschrieben werden. Die allgemeine Syntax ist ausdruck ? alternative_ja : alternative_nein Ist der Ausdruck wahr, so wird die erste Alternative, ansonsten die zweite ausgeführt. Als Beispiel ist 22 KAPITEL 3. ABLÄUFE p = ( x > 0 ) ? x : -x; eine kompakte Alternative zu if( x > 0 ) { p = x; } else { p = -x; } um den Absolutbetrag zu von x berechnen. Die Verwendung des ?-Operators ist Geschmackssache. Man kann damit kompakten und eleganten Code schreiben, aber zumindest für Anfänger ist eine ausführliche Formulierung über if-else übersichtlicher. 3.6 Die switch-Anweisung Wenn man eine Auswahl aus vielen einander sich gegenseitig ausschließenden Alternativen treffen will, werden if ... else if Strukturen recht unübersichtlich. Als Vereinfachung stellt Java die Möglichkeit der switch (engl. Schalter) Anweisung zur Verfügung. Sie hat die Form switch( ausdruck ) { case konst1: anweisung1 case konst2: anweisung2 default: anweisung3 } In der Klammer steht ein Ausdruck, der einen ganzzahligen Wert liefert. Dieser wird dann mit den Konstanten an den case (engl. Fall) Marken verglichen. Bei Gleichheit wird das Programm an dieser Stelle fortgesetzt. Etwas gewöhnungsbedürftig ist die Tatsache, dass die Ausführung nicht automatisch durch das nächste case beendet wird. Soll – wie es in der Regel der Fall ist – die Bearbeitung der switch Anweisung nach einer Übereinstimmung verlassen werden, muss dies explizit durch ein break festgelegt werden. Das optionale default „sammelt“ alle Fälle, in denen keine Übereinstimmung gefunden wurde. Beispiel 3.6 Ausgabe des Wochentages. int wochentag; switch( wochentag ) { case 1: System.out.println("Montag" ); break; 3.7. SCHLEIFEN 23 case 2: System.out.println("Dienstag" ); break; case 3: System.out.println("Mittwoch" ); break; case 4: System.out.println("Donnerstag" ); break; case 5: System.out.println("Freitag" ); break; case 6: System.out.println("Samstag" ); break; case 7: System.out.println("Sonntag" ); break; default: System.out.println("Kein gültiger Wochentag"); } Möchte man mehrere Fälle zusammenfassen, kann man die entsprechenden case Marken direkt aufeinander folgen lassen. Man könnte etwa im obigen Beispiel schrieben case 6: case 7: System.out.println("Wochenende" ); break; 3.7 Schleifen Die logischen Ausdrücke werden auch benutzt, um einen Block von Anweisungen (Schleifenkörper) mehrfach auszuführen bis eine Abbruchsbedingung erfüllt ist. Die einfachste Schleife wird mit while eingeleitet: while( ausdruck ) { anweisung } Der Ausdruck in der while Anweisung wird zunächst ausgewertet. Ergibt er Wahr, so werden die Anweisungen im Block ausgeführt (vorausgehende Bedingungsprüfung). Anschließend wird die Bedingung wieder überprüft und gegebenenfalls der Block erneut ausgeführt. Beispiel: Beispiel 3.7 While-Schleife i = 0; while( i < 10 ) { System.out.println("i = " + i++); } Die Schleife wird durchlaufen, bis der Wert von i größer als 10 wird. Es gibt keinen automatischen Schutz, dass die Schleife irgendwann beendet wird. Lässt man den ++ Operator weg, bleibt das Programm ewig in der Schleife. Falls die Bedingung am Anfang nicht erfüllt ist, wird der Block überhaupt nicht ausgeführt. Man spricht daher auch von einer abweisenden Schleife. Die Wiederholung mit einem Schleifenzähler, die so genannte Zählschleife, kommt so oft vor, dass es dafür ein eigenes Konstrukt gibt – die for Schleife. 24 KAPITEL 3. ABLÄUFE for( ausdruck1; ausdruck2; ausdruck3 ) { anweisungen } dies ist äquivalent mit ausdruck1 while( ausdruck2) { anweisungen ausdruck3 } Der erste Ausdruck wird vor Beginn der eigentlichen Schleife (Initialisierung) ausgeführt. Die zweite Komponente ist die Abbruchsbedingung. Der dritte Ausdruck (Inkrementierung) schließlich wird am Ende jedes Durchgangs angehängt. Aus dem obigen Beispiel wird dann for(i = 0; i < 10; i++ ) { System.out.println("i = " + i); } Dies ist eine Standardform solche Schleifen zu schrieben. Allerdings können in den drei Komponenten von for beliebige Anweisungen stehen. Man könnte auch das Beispiel als for(i = 0; i < 10; System.out.println("i = " + i++) ); schreiben. Derartige Konstruktionen sind aber schwerer zu lesen und sollten daher nicht genutzt werden. Einzelne Komponenten können leer bleiben. Eine leere Kontrollabfrage gilt als immer wahr. Die Schreibweise for( ;; ) ist eine gängige Konstruktion für Endlosschleifen. Übung 3.2 Berechnen Sie in einer for-Schleife die Summe aller ungeraden Zahlen von 1 bis 999. Es gibt noch eine dritte, relativ selten benutzte Form bei der die Bedingung nach der Ausführung des Blocks erfolgt (nachfolgende Bedingungsprüfung): do { anweisungen } while( ausdruck ); In diesem Fall wird die Schleife immer mindestens einmal durchlaufen (nicht abweisende Schleife). Erst am Ende der ersten Schleife wird die Bedingung zum ersten Mal überprüft. Dieser Art von Abfrage ist dann sinnvoll, wenn die Bedingung erst nach der Ausführung ausgewertet werden kann. 3.7. SCHLEIFEN Tabelle 3.4: Übersicht Schleifen Typ Wiederholung mit vorausgehender abweisend Prüfung (Kopfprüfung) Wiederholung mit nachfolgender nicht abweisend Prüfung (Fußprüfung) Zählschleife abweisend 25 Java-Syntax while(){} do{}while() for(;;){} In Tabelle 3.4 sind die drei Grundtypen von Schleifen zusammen mit der JavaSyntax zusammen gestellt. Grundsätzlich kann man noch die Wiederholung ohne Prüfung als eigenen Typ betrachten. Derartige Schleifen werden endlos wiederholt. Einsatzgebiet sind Prozesse, die ständig auf Ereignisse warten. Für Endlosschleifen gibt es in Java keine gesonderte Konstruktion. Üblich sind die Formen for(;;) und while( true ). 3.7.1 Vorzeitiges Verlassen von Schleifen In manchen Fällen ist es erforderlich, eine Schleife entweder ganz zu verlassen oder zumindest den aktuellen Durchgang abzubrechen. In Java gibt es dazu die Anweisungen break und continue. Break beendet die Schleife komplett, der Programmablauf wird mit der ersten Anweisung hinter der Schleife fortgesetzt. Demgegenüber bleibt bei einem continue das Programm in der Schleife, nur der aktuelle Durchgang wird nicht zu Ende geführt. Statt dessen wird bei einer while oder do Schleife die Abbruchbedingung ausgewertet. Bei einer for Schleife wird das Programm mit der Ausführung des dritten Ausdrucks fortgesetzt. Beispiel 3.8 break-Anweisung int i, summe = 0; for( i=0; ; i++ ) { if( ( summe += i ) > 1000 ) break; } System.out.println("Summe " + summe + " bei i=" + i + " erreicht"); In diesem Programm wird in der Abfrage ausgenutzt, dass auch eine Zuweisung einen Ausdruck darstellt. Der Wert der Zuweisung ist genau der zugewiesene Wert. Beispielsweise hat die Zuweisung i = 4 * 5; den Wert (rvalue) 20. 26 3.7.2 KAPITEL 3. ABLÄUFE Sprünge Wesentlich für die flexible Bearbeitung von Befehlsfolgen ist die Möglichkeit, die Ausführung an einer Stelle abzubrechen und an einer anderen fortzusetzen. Die vorgestellten Elemente für Schleifen und Verzweigungen beruhen implizit auf derartigen Sprüngen in der Befehlsfolge. Aufgrund der großen Bedeutung von Sprüngen stellen Prozessoren in der Regel auch eine reiche Auswahl an Befehlen für absolute und relative, bedingte und unbedingte Sprünge zur Verfügung. Entgegen dieser großen Bedeutung von Sprungbefehlen auf unterer Ebene, sollten sie in höheren Programmiersprachen weitgehend vermieden werden. Programme mit expliziten Sprungbefehlen werden schnell unübersichtlich und der Ablauf ist schwer nachvollziehbar. Im Prinzip können mit den bereits vorgestellten Elementen alle Abläufe ohne Sprünge realisiert werden Es gibt allerdings einige wenige Fälle, in denen Sprünge zu klareren Programmen führen. Insbesondere ist hier der Rücksprung aus mehrfach geschachtelten Schleifen zu nennen: for( ...) { for( ...) { for( ...) { if( katastrophe ) goto fehlerBehandlung; ... fehlerBehandlung: ... Diese Art von Fehlerbehandlung kann sinnvoll sein, um verschiedene Fehlerfälle gemeinsam zu behandeln. Die Entwickler von Java haben auf allgemeine Sprunganweisung wie etwa das goto in C verzichtet und lediglich den Rücksprung aus geschachtelten Schleifen in die Sprache eingebaut. Dazu können die Befehle break und continue mit einer Marke versehen werden. Der Name der Zielmarke oder Sprungmarke (label, engl. Etikett) wird nach den Regeln für Variablen gebildet. Das Label wiederum muss eine der umgebenden Kontrollstrukturen markieren. Dazu wird der Name gefolgt von einem Doppelpunkt vor die entsprechende Schleife gesetzt. Für das obige Beispiel könnte man schreiben: f1: for( ...) { for( ...) { for( ...) { if( katastrophe ) break f1; ... In diesem Fall beendet das break die mit f1 markierte umgebende Schleife. Allerdings enthält Java wesentlich leistungsfähigere Konzepte zur Fehlerbehandlung. Die Bedeutung von gelabelten break und continue Anweisungen ist daher nicht sehr groß. 3.8. BEISPIELE 3.8 27 Beispiele Die folgenden kurze Programme illustrieren die Verwendung von Schleifen und Verzweigungen. Beispiel 3.9 Berechnung aller Primzahlen bis 30. int teiler, zahl; boolean istPrimzahl; for( zahl=2; zahl<30; zahl++ ) { istPrimzahl = true; for( teiler=2; teiler<zahl; teiler++ ) { if( zahl % teiler == 0 ) { istPrimzahl = false; break; } } if( istPrimzahl ) { System.out.println( zahl + " ist eine Primzahl"); } else { System.out.println( zahl + " enthält " + teiler); } } Beispiel 3.10 Berechnung der Quersumme einer gegebenen Zahl. Die Variable verbose wird eingesetzt, um den Umfang der Ausgaben zu regulieren. int zahl = 1235601; boolean verbose = true; int querSumme = 0; int stelle; System.out.println( "zahl = " + zahl ); while( zahl > 0 ) { stelle = zahl % 10; querSumme += stelle; if( verbose ) System.out.println(stelle); zahl /= 10; } System.out.println("Quersumme = " + querSumme ); Beispiel 3.11 Wahrheitstabelle: Das Programm gibt für eine Verknüpfung von vier logischen Variablen die Wahrheitstabelle aus. 28 KAPITEL 3. ABLÄUFE int a, b, c, d; System.out.println(" a b c d : ab + c + bd"); for( a=0; a<2; a++ ) { for( b=0; b<2; b++ ) { for( c=0; c<2; c++ ) { for( d=0; d<2; d++ ) { System.out.print(" "+a+" "+b+" "+c+" "+d+" : "); if( (a & b | c | b & d) == 1 ) { System.out.print( "1" ); } else { System.out.print( "0" ); } System.out.println(); } } } } 3.9 Übungen Übung 3.3 Schreiben Sie Schleifen, um die folgenden Sequenzen auszugeben: 1. −10, −8, −6, . . . , 10 2. 1, −2, 3, −4, 5, −6, . . . − 50 3. 1, 2, 4, 7, 11, 16, ... bis Wert > 300 4. 1, 1.1, 1.2, . . . , 1.9, 2 5. 1, 2, 3, 11, 12, 13, 21, 22, 23, . . . , 91, 92, 93 Übung 3.4 Berechnen Sie - soweit definiert - für die ganzen Zahlen von -10 bis +10 die Werte 1. i3 2. 2 ∗ i2 − 5 ∗ i 3. i! Übung 3.5 Fibonacci-Zahlen Die Fibonacci2 -Zahlen sind durch die Startbedingung i1 = 1, i2 = 1 und die 2 Leonardo Pisano genannt Fibonacci, italienischer Mathematiker, ca. 1170-1250 3.9. ÜBUNGEN 29 Rekursion in = in−2 + in−1 definiert. Damit gilt i3 i4 i5 i6 = = = = i1 + i2 i2 + i3 i3 + i4 i4 + i5 =1+1=2 =1+2=3 =2+3=5 =3+5=8 und so weiter. Geben Sie alle Fibonacci Zahlen kleiner als 30000 aus. Welchem Wert nähert sich der Quotient in+1 /in mit wachsendem n an? Übung 3.6 Sie betreiben einen Internet-Handel. Bei jedem Kunden zählen Sie die Anzahl der Einkäufe. Abhängig von dieser Zahl gelten Kunden als Neuling bis 5 Einkäufe Kunde bis 50 Einkäufe Stammkunde bis 500 Einkäufe Gold Kunde ab 501 Einkäufe Wie sieht eine if else if Abfrage aus, um in Abhängigkeit von der Anzahl der Käufe eines konkreten Kunden seinen Status auszugeben? Übung 3.7 Berechnen Sie alle Primzahlen bis 1000. Geben Sie dabei alle PrimzahlPaare (d. h. Primzahlen die den minimalen Abstand von 2 haben wie z. B. 3 und 5) aus. Wie viele Paare finden Sie? Wie groß ist der maximale Abstand zwischen zwei aufeinanderfolgenden Primzahlen? Übung 3.8 Geben Sie untenstehende Tabelle für das Einmaleins aus. ∗ Geben Sie doppelte Werte (z. B. 6*7 und 7*6) nur einmal aus, so dass aus dem Rechteck ein Dreieck wird. 1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10 --------------------------------------: 1 2 3 4 5 6 7 8 9 10 : 2 4 6 8 10 12 14 16 18 20 : 3 6 9 12 15 18 21 24 27 30 : 4 8 12 16 20 24 28 32 36 40 : 5 10 15 20 25 30 35 40 45 50 : 6 12 18 24 30 36 42 48 54 60 : 7 14 21 28 35 42 49 56 63 70 : 8 16 24 32 40 48 56 64 72 80 : 9 18 27 36 45 54 63 72 81 90 : 10 20 30 40 50 60 70 80 90 100 30 KAPITEL 3. ABLÄUFE Übung 3.9 Die Prüfziffer der Internationale Standard Buchnummer ISBN wird nach folgendem Algorithmus berechnet: Zunächst wird aus den 9 Ziffern der eigentlichen Kennung eine gewichtete Summe bestimmt. Dazu wird die erste Ziffer der ISBN mit 10 multipliziert, die zweite mit 9, die dritte mit 8 usw. Die Produkte werden addiert. Von der resultierenden Summe wird die Differenz zur nächstgrößeren durch 11 teilbaren Zahl berechnet. Dieser Wert wird als Prüfziffer angehängt. Der Sonderfall 10 wird durch ein X als Prüfziffer markiert. Implementieren Sie diesen Prüfalgorithmus. Testen die Korrektheit an Hand einiger Beispiele. Kapitel 4 Verwendung von Gleitkomma-Zahlen 4.1 4.1.1 Gleitkomma-Zahlen Gleitkomma- Darstellung Bisher hatten wir Zahlen in der Integerdarstellung betrachtet. Diese Darstellung von ganzen Zahlen und das Rechnen damit ist exakt solange man • im darstellbaren Zahlenbereich bleibt • keine Nachkommstellen betrachtet (z.B. nach Division) In vielen praktischen Anwendungen benötigt man aber eine flexiblere Repräsentation von Zahlen. Oft ist das Rechnen mit Nachkommastellen notwendig oder zumindest natürlich. Viele Angaben enthalten Nachkommastellen (Zinssatz 3,5%, 4,7 Liter auf 100 Km). Die erste Erweiterung ist die Einführung von Nachkommastellen. Bei der Festkommadarstellung gibt man die Anzahl der Vor- und Nachkommastellen fest vor. Als Nachteil bleibt dabei die eingeschränkte Dynamik des Zahlenbereichs. Daher wird diese Darstellung nur in wenigen Spezialanwendungen verwendet. Auch im Alltag und noch mehr in der Technik haben wir das Problem der unterschiedlichen Bereiche. Bei Längen beispielsweise kann je nach Anwendung eine Angabe in mm (Schrauben im Baumarkt) oder in km (Urlaubsreise) sinnvoll sein. Der Trick dabei ist, dass die Länge mit einer Anzahl von signifikanten Stellen und der Größenordnung angegeben wird: 3,5mm oder 650km. Vollständige Genauigkeit 650.245.789mm ist weder sinnvoll noch notwendig. Allgemein schreibt man eine Größe z als Produkt der Mantisse M und einer ganzzahligen Potenz p von 10: z = M · 10p 31 32 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN etwa 3,5 10−3 m. Für Maßangaben gibt es Namen bzw. Vorsilben für die entsprechenden Potenzen von 10 (Kilo, Giga, Mega, Nano, etc). Zum Rechnen muss man Zahlen auf eine einheitliche Darstellung normieren: 3,5mm + 650km = 0,003.5m + 650.000m = 650.000,003.5m Die Beispiele zeigen, wie man durch Verschieben des Kommas in der Mantisse den Exponenten verändert. Es gibt für eine gegebene Zahl (unendlich) viele gleichwertige Darstellungen: 123 = 12, 3 · 10 = 1, 23 · 102 = 1230 · 10−1 = . . . Eine einheitliche Darstellung erreicht man mit der Vereinbarung, dass die Mantisse nur genau eine Vorkommastelle hat. Damit hat man eine eindeutige Abbildung zwischen der Zahl z und ihrer Darstellung durch Mantisse m und Exponent p: z ⇔ (m, p) Die Position des Kommas wird je nach Bedarf verschoben, man nennt daher dieses Format Gleitkommadarstellung (floating point) oder auch halblogarithmische Darstellung. Für die Verwendung in Computern geht man zum Dualsystem über, so dass p dann der Exponent zur Basis 2 ist. In der normalisierten Darstellung wird das Komma so gesetzt, dass nur eine Vorkommastelle bleibt. Die Mantisse dann hat im Dualsystem immer die Form m = 1, . . . Da die führende 1 bei jeder Zahl steht, braucht man sie in der Realisierung nicht abzuspeichern. In der Praxis sind für m und p nur endlich viele Bits verfügbar. Die Größe von m bestimmt die Genauigkeit der Zahlendarstellung und die Größe von p legt den insgesamt abgedeckten Zahlenbereich fest. Aufgrund der endlichen Größe von m und p kann es zu folgenden Fehlern in der Repräsentation kommen: • Die Zahl ist zu groß oder zu klein (z ≥ 2pmax+1 , z ≤ −2pmax+1 ). • Die Zahl ist betragsmäßig zu klein (|z| < 2−pmax bei symmetrischem Zahlenbereich des Exponenten). • Die Mantisse ist nicht groß genug, um die erforderliche Anzahl von Stellen zu repräsentieren (Rundungsfehler). Beispiel 4.1 Geleitkommadarstellung Betrachten wir folgende Zahlendarstellung im Dezimalsystem: ±x.xxx · 10±ee . Geben Sie dazu folgende Wert an: • Kleinste Zahl 4.1. GLEITKOMMA-ZAHLEN 33 • Größte Zahl • Betragsmäßig kleinste Zahl 6= 0 • Abstand zwischen den beiden größten Zahlen • Abstand zwischen den beiden betragsmäßig kleinsten Zahlen Insbesondere die Rundungsfehler bedingen, dass im allgemeinen eine reelle Zahl nicht genau dargestellt werden kann. Berechnet man etwa 1/3 so ist das Resultat 0,33333. . . prinzipiell nicht exakt darstellbar. Für die Repräsentierung einer Gleitkommazahl benötigt man im Detail: • Mantisse • Vorzeichen der Mantisse • Exponent • Vorzeichen des Exponents Weiterhin muss festgelegt werden, wie die insgesamt verfügbaren Bits auf Mantisse und Exponent aufgeteilt werden. Lange Zeit waren die Details der Implementierung von Gleitkommazahlen herstellerabhängig. Da dies zu Problemen beim Datenaustausch und auch bei der Kompatibilität von Programmen führen kann, wurde vor einigen Jahren ein Standard von Normierungsgremien des IEEE (Institute for Electrical and Electronics Engineers, www.ieee.org) verabschiedet. Dieser Standard IEEE 754 definiert 3 Formate: short real long real temporary real 32 Bit 64 Bit 80 Bit einfache Genauigkeit doppelte Genauigkeit erweiterte Genauigkeit Als Beispiel betrachten wir das Format short real näher: Bit 31 Vz 1 Bit Bit 0 Characteristik c 8 Bit Mantisse m 23 Bit mit Vz Vorzeichen der Mantisse (0 positiv, 1 negativ) Characteristik Exponent + 127 Mantisse Nachkommastellen Im Gegensatz zu der bei Integer üblichen Darstellung mit dem 2er-Komplement wird die Mantisse mit Betrag und getrenntem Vorzeichen abgelegt (VorzeichenBetrag-Darstellung). In der Charakteristik wird der um 127 verschobene Exponent (biased exponent) eingetragen. 34 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN Beispiel 4.2 Konvertierung in den Standard IEEE 754 Die Zahl −37, 12510 soll in das Format short real gebracht werden. 1. Konvertierung in Binärdarstellung: 100101, 001 (32 + 4 + 1 + 1/8) 2. Normalisierung: 1, 00101001 · 25 3. Mantisse: 00101001 4. Charakteristik: 5 + 127 = 13210 = 100001002 5. Vorzeichen: 1 für negative Zahl Bit 31 1 1 Bit 1000010 0 8 Bit Bit 0 0010100 10000000 00000000 23 Bit Die Werte 0 und 255 sind reserviert, um den Wert Null sowie einige Spezialfälle darstellen zu können. Die Null ist ein Sonderfall, da für sie keine normalisierte Darstellung mit einer Eins vor dem Komma möglich ist. Daher wurde vereinbart, dass die Null durch ein Löschen aller Bit in Charakteristik und Mantisse dargestellt wird. Im Detail gilt: Nicht normalisiert Null Unendlich (Inf) Keine Zahl (NaN) Vz Charakteristik ± 0 ± 0 ± 255 ± 255 Mantisse 6= 0 0 0 6= 0 Mit den beiden Werten Inf und NaN können Fehlerfälle abgefangen werden. Bei Bereichsüberschreitung wird das Result auf Inf gesetzt, während NaN durch „unerlaubte“ Operationen wie Division von 0 durch 0 oder Wurzel aus einer negativen Zahl entsteht. Damit besteht einerseits die Möglichkeit, solche Fälle zu erkennen. So steht beispielsweise in C die Funktion _isnan zur Verfügung, um auf NaN zu testen. Andererseits kann man auch mit den Werten weiter rechnen. Der Standard spezifiziert das Ergebnis von Operationen wie Inf + 10 = Inf. In manchen Algorithmen kann man damit alle Abfragen auf Sonderfälle vermeiden, die sonst den linearen Programmablauf stören würden. Eine ausführliche Darstellung der Thematik enthält der Artikel von Goldberg [Gol91]. Beispiel 4.3 Inf und NaN Der C-Code printf( "log( 0.) = %15g\n", log( 0.)); printf( "log(-1.) = %15g\n", log(-1.)); liefert das Resultat 4.1. GLEITKOMMA-ZAHLEN log( 0.) = log(-1.) = 35 -1.#INF -1.#IND Bei den beiden größeren Typen long real und temporary real wird sowohl die Genauigkeit erhöht als auch der Wertebereich erweitert. Rechnen mit Gleitkommazahlen bedeutet einen wesentlich größeren Aufwand als das Rechnen mit Integerzahlen. Speziell bei der Addition müssen zunächst die Mantissen und Exponenten verschoben werden, bevor die Mantissen addiert werden können. Das Ergebnis muss anschließend gegebenenfalls wieder in die Normalform gebracht werden. Schnelles Rechen in Gleitkommadarstellung erfordert entsprechenden zusätzlichen Schaltungsaufwand. Früher wurden dafür spezielle Baustein als Co-Prozessoren eingesetzt. Heute ist eine entsprechende Einheit bei den leistungsfähigen Prozessoren bereits integriert. Die Angabe der möglichen Gleitkommaoperationen pro Sekunde (FLOPS: floating point operations per second) ist eine wichtige Kenngröße für die Leistungsfähigkeit eines Computers. Übung 4.1 Gleitkommadarstellung Betrachten Sie folgendes einfaches Format für Gleitkommazahlen: • Bit 15: Vorzeichen des Exponenten (0 für positiv) • Bit 14: Vorzeichen der Mantisse (0 für positiv) • Bit 6-13 Betrag der Mantisse • Bit 0-5 Betrag des Exponenten Welchen Wert haben die folgenden Bitmuster: 0 1 0 0 0110 0000 1010 0000 00 0011 00 0100 Übung 4.2 Wertebereich im Standard IEEE 754 Welches ist jeweils die • kleinste • größte • betragsmäßig kleinste darstellbare Zahl im short real Format? Übung 4.3 Konvertierung in den Standard IEEE 754 Wie wird die Zahl 14, 62510 als Gleitkommazahl im Format real short dargestellt? Geben Sie das Resultat in Binär- und Hexadezimaldarstellung an. 36 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN Übung 4.4 Addition und Multiplikation Berechnen Sie für die beiden Zahlen 7, 5 · 104 und 6, 34 · 102 Summe und Produkt. Geben Sie das Ergebnis jeweils in normierter Form mit einer Vorkommastelle an. Welche einzelnen Rechenschritte sind erforderlich? Übung 4.5 Darstellung der Zahl 0 In IEEE 754 gibt es zwei Bitmuster für die Zahl 0, einmal mit positiven und einmal mit negativem Vorzeichen. Diese Werte kann man als +0 und -0 interpretieren. Wo kann man diese Unterscheidung sinnvoll einsetzen? Welche Probleme können sich aus der doppelten Darstellung ergeben? 4.1.2 Verwendung von Gleitkommazahlen Der große Vorzug von Gleitkommazahlen ist der große Wertebereich mit gleichbleibender relativer Genauigkeit. Andererseits ist die Abdeckung – im Gegensatz zu den Integerzahlen – nicht vollständig. Zwischen benachbarten Gleitkommazahlen ist eine Lücke und die Breite der Lücke hängt von der Größe der Zahlen ab. Dies kann zu Effekten führen, die der Mathematik wiedersprechen. Das folgende Fragment C-Code dient zur Veranschaulichung dieses Verhaltens. In dem Programm wird zu einer Zahl der Wert 1 addiert, wobei die Zahl schrittweise um den Faktor 10 erhöht wird. double test = 1.; double testp1; do{ test *= 10.; testp1 = test + 1.; printf( "%10g %10g %5g \n", test, testp1, testp1-test ); } while( testp1 > test ); Die Ausführung liefert die Ausgabe: 10 100 1000 10000 100000 1e+006 1e+007 1e+008 1e+009 1e+010 1e+011 11 101 1001 10001 100001 1e+006 1e+007 1e+008 1e+009 1e+010 1e+011 1 1 1 1 1 1 1 1 1 1 1 4.2. GLEITKOMMAZAHLEN IN JAVA 1e+012 1e+013 1e+014 1e+015 1e+016 1e+012 1e+013 1e+014 1e+015 1e+016 37 1 1 1 1 0 Zunächst zeigt das Programm das erwartete Verhalten und berechnet die Differenz zu 1. Aber wenn der Wert 1016 erreicht ist, führt die Addition nicht mehr zu einer anderen Zahl und die Differenz zwischen 1016 und 1016 + 1 liefert den Wert 0. Dieser Einfluss der Rundungsfehler ist bei der Programmentwicklung zu berücksichtigen. Beispiel 4.4 Rundungsfehler Die folgende Anweisung in C printf( "5. - sqrt(5)*sqrt(5) = %15g\n", 5. - sqrt(5.)*sqrt(5.)); ergibt 5. - sqrt(5)*sqrt(5) = -8.88178e-016 Übung 4.6 Reihenfolge der Auswertung eines Ausdrucks Sei x = 1030 , y = −1030 und z = 1. Welches Resultat ergibt sich bei dem Rechnen in Gleitkomma-Arithmetik für die beiden Ausdrücke • (x + y) + z • x + (y + z) 4.1.3 Vergleich der Zahlenformate Abschließend sind die wesentlichen Unterschiede in den Zahlendarstellungen in Tabelle 4.1 zusammen gestellt. Abhängig von der Anwendung sind die einzelnen Kriterien unterschiedlich zu gewichten. Beispielsweise spielt bei einer low-costAnwendung der Preis eine entscheidende Rolle, so dass auf eine eigene Einheit für Gleitkommaoperationen verzichtet wird. Dann ist es oft notwendig Berechnungen, für die Gleitkommazahlen besser geeignet wären, aus Performanzgründen trotzdem mit Integerzahlen durchzuführen. 4.2 Gleitkommazahlen in Java Java unterstützt die beiden Gleitkomma-Typen float und double gemäß IEEE754. Zwischen Gleitkomma-Werten gelten die Grundrechenarten mit den Operatoren +, -, * und / mit den schon behandelten Vorrangsregeln. Auch die Vergleichsoperatoren sind für Gleitkomma-Werte gültig. Der Modulo Operator % 38 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN Tabelle 4.1: Vergleich zwischen Integer- und Gleitkommazahlen Integer Gleitkomma Nachkommastellen Nein Ja Genauigkeit Ergebnisse sind exakt Rundungsfehler Bereich eingeschränkt groß Überlauf wird nicht gemeldet Inf Rechenaufwand niedrig groß Speicherbedarf 8-32 Bit 32-80 Bit Bit-Operatoren, modulo allerdings ist in diesem Fall sinnlos und kann daher nicht auf Gleitkomma-Werte angewandt werden. Ebenso sind die Bitoperationen für Gleitkomma-Werte nicht definiert. Die Genauigkeit beträgt etwa 7 Stellen bei float und 15 Stellen bei double. Bei der Eingabe von Konstanten benutzt man den Punkt anstelle des Kommas. Optional kann man einen Zehner-Exponenten angeben. Beispiel 4.5 Gültige Gleitkommazahlen: 0.456 -177.999 99.988e17 -1e-10 .1e-10 Die Werte werden intern normalisiert, bei der Eingabe ist man frei in der Anzahl der Stellen vor dem Dezimalpunkt. Der Dezimalpunkt direkt vor dem Exponenten kann weggelassen werden. Standardmäßig interpretiert der Compiler diese Konstanten als double. Durch Anhängen der Endung f (F) werden sie zu float Werten. Im Gegensatz zu Integerwerten stellt für übliche Anwendungen der Wertebereich kein Problem dar. Über- oder Unterschreitungen sind eher selten. Allerdings muss man stets mit Rundungseffekten rechnen. Problematisch sind Vergleiche in der Art if( x == 5 ) Wenn x das Ergebnis einer Rechnung ist, kann es durchaus sein, dass x aufgrund der Rundungsfehler den Wert 4.99999 hat. Die Abfrage würde dann nicht erfüllt sein. Besser ist, in einem solchen Fall if( Math.abs( x - 5 ) < epsilon ) 4.2. GLEITKOMMAZAHLEN IN JAVA 39 zu schrieben, wobei epsilon die gewünschte oder geforderte Genauigkeit angibt. Mit der gleichen Vorsicht muss man for-Schleifen mit Gleitkommawerten betrachten. In dem Beispiel for( x=0.; x<=1.; x+=0.01 ) ist nicht gewährleistet, dass der Wert 1 exakt getroffen wird, so dass eventuell (z. B. bei x=1.0000001) die Schleife eine Iteration zu früh abgebrochen wird. 4.2.1 Mathematisch Funktionen Eine ganze Reihe von mathematische Funktionen sind in Java standardmäßig verfügbar. Die Funktionen sind als Methoden einer Klasse Math aufrufbar. Wir werden in einem späteren Kapitel auf die Verwendung von Methoden genauer eingehen. An dieser Stelle verwenden wir Methoden als Implementierung von mathematischen Funktionen. Die Syntax in Java für den Aufruf einer Methode ist Klasse.methode( Parameterliste ) Die Methode erhält eine Anzahl von Parameter, berechnet aus diesen Werten ein Ergebnis und gibt dieses Ergebnis zurück. Das Beispiel y = Math.sin( t ); berechnet den Sinus des Wertes in der Variablen t und speichert das Resultat in y. Unter anderem gibt es: • trigonometrische Funktionen: sin(x), cos(x), tan(x), Argument jeweils im Bogenmaß • inverse trigonometrische Funktionen: asin(x), acos(x), atan(x) • Potenzen und Logarithmen: exp(x), log(x), sqrt(x), pow(x,y) • Runden: – ceil(x) kleinste ganze Zahl größer x – floor(x) größte ganze Zahl kleiner x • Betrag: abs(x) • Zufallszahlen: random() liefert eine Zufallszahl aus dem Intervall [0, 1] Probleme entstehen, wenn • das Argument nicht in dem Definitionsbereich liegt (log(-1)) (domain error) 40 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN • das Ergebnis nicht mehr darstellbar ist (pow(1000,1000)) (range error) Die Funktionen geben in diesen Fällen definierte Sonderwerte zurück, die auch bei der Ausgabe als solche dargestellt werden. Beispiel 4.6 Sonderfälle in mathematischen Funktionen. public static void main(String[] args) { /* range error with exp() */ for( double x=0; x<1000; x+=300. ) { System.out.println("x = "+ x +", exp(x) = "+ Math.exp(x) ); } /* domain error with log() */ System.out.println( "log( 0.) = " + Math.log( 0.)); System.out.println( "log(-1.) = " + Math.log(-1.)); } ergibt x = 0.0, exp(x) = 1.0 x = 300.0, exp(x) = 1.9424263952412558E130 x = 600.0, exp(x) = 3.7730203009299397E260 x = 900.0, exp(x) = Infinity log( 0.) = -Infinity log(-1.) = NaN 4.3 Umwandlung zwischen Datentypen In einem Ausdruck können die Operanden verschiedenen Datentyp haben. Der Typ des Ergebnisses hängt dann von den beteiligten Datentypen ab. Auch bei einer Zuweisung hat der Ausdruck auf der rechten Seite manchmal einen anderen Ergebnistyp als die Variable auf der linken Seite. In solchen Fällen erfolgt eine Umwandlung des Typs vor der Rechnung bzw. der Zuweisung. Die Umwandlung erfolgt dabei stets vom „kleineren“ zum „größeren“ Typ (erweiternde Konvertierung). Beispielsweise wird bei der Addition eines int und eines long Wertes zunächst der int Wert in einen long Wert gewandelt. Die Umwandlung erfolgt allerdings „Schritt für Schritt“. Selbst wenn etwa auf der linken Seite einer Anweisung eine double Variable steht, werden nicht notwendigerweise alle Rechnung in double ausgeführt. Das folgende Beispiel demonstriert diesen Effekt: Beispiel 4.7 Umwandlung. double test; test = 10 / 3; System.out.println( "Konvertierung 1, test = " + test); 4.3. UMWANDLUNG ZWISCHEN DATENTYPEN 41 test = 10. / 3; System.out.println( "Konvertierung 2, test = " + test); Im ersten Fall wird die rechte Seite noch als Integer-Rechnung ausgeführt. Erst das Ergebnis wird umgewandelt. Im zweiten Fall ist der erste Operand 10. bereits ein double, so dass vor der Division die 3 umgewandelt wird. Entsprechend liefert das Beispiel Konvertierung 1, test = 3.0 Konvertierung 2, test = 3.3333333333333335 Bei der erweiternde Konvertierung bleibt die Information erhalten. Sie werden daher als sicher angesehen und wenn notwendig vom Compiler automatisch eingefügt. Demgegenüber sind Konvertierungen in die andere Richtung (einschränkende Konvertierung) unsicher. Eventuell verliert man Genauigkeit oder der Wert passt nicht mehr in den geringeren Darstellungsbereich. Daher werden einschränkende Konvertierungen vom Compiler als Fehler behandelt. Die Anweisung int i = 1.5; führt zu der Fehlermeldung FloatTest.java [35:1] possible loss of precision found : double required: int int i = 1.5; ^ 1 error Errors compiling main. Man kann explizit eine Konvertierungen mittels des so genannten Type-CastOperators anfordern. Die allgemeine Form, um einen Ausdruck a in einen anderen Datentyp zu wandeln, ist (Datentyp) a Sofern möglich, wird der Ausdruck in den in Klammern angegebenen Typ konvertiert. Damit werden auch einschränkende Konvertierungen in der Art int i = (int) 1.5; vom Compiler akzeptiert. Allerdings gilt dies nur für legale Konvertierungen. Der Versuch, einen float-Wert in einen boolean zu wandeln, boolean b = (boolean) 1.5; scheitert bereits beim Kompilieren mit der Fehlermeldung 42 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN FloatTest.java [36:1] inconvertible types found : double required: boolean boolean b = (boolean) 1.5; ^ 1 error Errors compiling main. 4.4 Übungen Übung 4.7 Nach einer alten Legende wünschte sich der Erfinder des Schachspiels vom König: • 1 Reiskorn auf dem ersten Feld eines Schachbrettes • 2 Reiskörner auf dem 2. Feld • 4 Reiskörner auf dem 3. Feld • u.s.w. Geben Sie die Anzahl der Reiskörner mit wachsender Anzahl von Feldern aus. Berechnen Sie zusätzlich, wie viele LKWs (je 7,5 Tonnen Ladung) man für den Transport benötigt, wenn jedes Reiskorn 30 mg wiegt. Wenn weiterhin ein Reiskorn ein Volumen von etwa 3.5 · 10−8 m3 hat, wie hoch wird dann ein Fußballfeld (70 auf 105 m) bedeckt? Übung 4.8 Schreiben Sie ein Programm, das die Lösungen der quadratischen Gleichung x2 + p · x + q = 0 berechnet. Dabei soll q den festen Wert 0,1 haben und p in Schritten von 0,1 von 0 bis 2 laufen. Prüfen Sie jeweils, ob eine reelle Lösung existiert. Falls ja, berechnen Sie die beiden Lösungen. Zur Kontrolle setzten Sie die gefundenen Werte in die Gleichung ein und geben das Ergebnis ebenfalls aus. Im Fall von komplexen Lösungen geben Sie einen entsprechenden Hinweis aus. Hinweis: zum Berechnen der Wurzel gibt es die Methode Math.sqrt(); Übung 4.9 Sie nehmen ein Darlehen über 100000 Euro auf. Der jährliche Zinssatz beträgt 4,5%. Jeden Monate zahlen Sie eine Rate von 500 Euro. Verfolgen Sie per Programm die Entwicklung des Darlehens. Berechnen Sie dazu jeden Monat die verbliebene Restschuld und geben diesen Wert jeweils am Ende eines Jahres aus. Wie lange dauert es, das Darlehen zu tilgen? Wie viele Zinsen werden bis zum Ende insgesamt gezahlt worden sein? 4.4. ÜBUNGEN 43 Übung 4.10 Für die Masse m eines Körpers mit der Ruhmasse m0 gilt bei einer Geschwindigkeit v m0 m= s µ ¶2 v 1− c Berechnen Sie für einen Astronauten mit m0 = 80kg die Masse bei Annäherung an die Lichtgeschwindigkeit c = 300000km/s für die Geschwindigkeiten v = 0.01 · c, 0.02 · c, . . . , 0.99 · c. Übung 4.11 Bestimmen Sie durch Monte-Carlo Simulation eine Näherung für die Zahl π. Betrachten Sie dazu ein Quadrat der Seitenlänge 1, in dem ein Viertelkreis mit Radius 1 liegt. Die Fläche FQ des Quadrats ist 1, die des Viertelkreises FV = π/4. Damit gilt die Beziehung π = 4 · FV /FQ . Zur experimentellen Bestimmung des Verhältnis FV /FQ erzeugt man zufällige Punkte im Intervall [0, 1; 0, 1] und zählt, wie viele davon in den Viertelkreis fallen. Übung 4.12 Die Methode Float.floatToRawIntBits( float f ) wandelt einen Wert vom Typ float in einen int Wert mit dem gleichen Bitmuster um. Dieses Bitmuster kann dann ausgegeben werden (Siehe Übung 2.3). Schreiben Sie ein Programm, um das Bitmuster von float Werten auszugeben. Verwenden Sie die Bitoperatoren, um die einzelnen Komponenten Vorzeichen, Charakteristik und Mantisse getrennt auszugeben. 44 KAPITEL 4. VERWENDUNG VON GLEITKOMMA-ZAHLEN Kapitel 5 Felder Häufig benötigt man zur Darstellung eines Sachverhaltes nicht nur eine einzelne Variable eines bestimmten Datentyps, sondern eine Reihe oder Anzahl von gleichartigen Variablen. Beispiele sind: • die Matrikelnummern aller Hörer und Hörerinnen dieser Vorlesung • alle Primzahlen kleiner 1000 Charakteristisch ist, dass ein fester Datentyp mehrfach benötigt wird. In dem Beispiel der Matrikelnummern braucht man eine Struktur, die nacheinander alle Nummern (jeweils in einer int-Variablen) enthält. Im Speicher hat man folgende Darstellung: 1. Matrikelnummer 2. Matrikelnummer ... N-te Matrikelnummer Die N aufeinander folgende Speicherzellen enthalten die Matrikelnummern der Studenten und Studentinnen. Die entsprechende Datenstruktur ist das Feld (engl. array). Da ein Feld aus den einfachen Datentypen aufgebaut ist, spricht man von einem strukturiertem Datentyp. Ein Feld ist charakterisiert durch: • einen Namen (Bezeichner) • einen Datentyp • der vorgegebenen, festen Anzahl der Elemente, d. h. Variablen des Datentyps • Indizes für die Elemente Die Deklaration eines Feldes entspricht der Deklaration einer Variablen ergänzt um ein Paar eckiger Klammern. Die Klammern können nach dem Datentyp oder nach dem Variablennamen stehen: 45 46 KAPITEL 5. FELDER int ifeld[]; byte[] bfeld; // meine bevorzugte Form Nach der Deklaration muss noch Speicherplatz reserviert werden. Dies erfolgt mit dem Operator new. Um für das oben deklarierte Integerfeld eine Größe von 10 Werten anzulegen, schreibt man: ifeld = new int[10]; Deklaration und Speicherreservierung können auch gleichzeitig erfolgen: float[] feld = new float[100]; Der neu reservierte Speicherbereich wird standardmäßig mit Nullen gefüllt. Bei Zahlentypen ist dies der Wert 0 während Felder von boolean mit false belegt werden. Beispiel 5.1 Anlegen eines Feldes Nach der Anweisung double[] messwerte = new double[30] gilt: • Es gibt ein Feld mit dem Namen messwerte. • Das Feld besteht aus 30 Speicherzellen. • Jede Speicherzelle hat Platz für einen Wert vom Typ double. • Alle Speicherzellen enthalten als Anfangswert 0. Eine zweite Form der Initialisierung erlaubt es, direkt die Werte (Literale) anzugeben. Die Größe des Feldes wird dann automatisch bestimmt. Beispiel 5.2 Anlegen eines Feldes mit 4 Werten short[] sfeld = { 1, 3, 5, 7}; Diese Form kann nur zusammen mit der Deklaration verwendet werden. Die Felder in Java sind semidynamisch. Ihre Größe wird zur Laufzeit festgelegt, ist danach aber fest. Es ist nicht möglich, ein bestehendes Feld zu erweitern. Allerdings kann jederzeit ein neuer Speicherbereich mit unterschiedlicher Größe angefordert werden. ifeld = new int[10]; ... ifeld = new int[20]; Eine zweite oder weitere Reservierung legt einen vollständig neuen Speicherbereich an. Die alten Werte gehen verloren. 5.1. ZUGRIFF AUF ELEMENTE 5.1 47 Zugriff auf Elemente In Java bieten die Felder einen Namen für eine Reihe gleichartiger Elemente. Man kann nicht direkt mit Feldern rechnen. Der Versuch in der Art int[] a = new int[3], b = new int[3]; a += b; führt zu der Fehlermeldung Feldtest.java [22:1] operator + cannot be applied to int[],int[] a += b; ^ 1 error Zum Rechnen muss man auf die einzelnen Elemente zugreifen. Der Zugriff erfolgt über den Index, d. h. der Position im Feld. Die Zählung der Elemente beginnt in Java immer mit dem Index 0. Bei einem Feld mit 10 Elementen haben die einzelnen Element die Indizes 0, 1, 2, . . ., 9. Ansprechen kann man ein Element über den Namen des Feldes und den Index in eckigen Klammern: a[2] ist das 3. Element (das Element mit Index 2) des Feldes a. Mit einem solchen Element kann man umgehen wie mit einer Variablen. Es kann sowohl in Ausdrücken eingesetzt werden als auch Ziel einer Zuweisung sein. Beispiel 5.3 Verwendung von Elementen aus Feldern int[] a = new int[3], b = new int[10]; ... a[0] = 50 * b[2] + 30 * b[3]; Um alle Elemente eines Feldes zu bearbeiten, kann man beispielsweise for Schleifen benutzen. Beispiel 5.4 Füllen eines Feldes mit Quadratzahlen int size = 10; int[] feld = new int[size]; int i; for( i=0; i<size; i++ ) { feld[i] = i * i; } 48 KAPITEL 5. FELDER Der Zugriff auf ein Element außerhalb des definierten Bereichs führt zu einem Laufzeitfehler. Beispiel 5.5 Zugriff auf Elemente außerhalb eines Feldes int size = 10; int[] feld = new int[size]; feld[11] = 111; führt zu der Fehlermeldung java.lang.ArrayIndexOutOfBoundsException at Feldtest.main(Feldtest.java:31) Exception in thread "main" Damit werden eine Vielzahl von Programmierfehler abgefangen. In anderen Sprachen wie C ohne Bereichsprüfung sind Fehler durch falsch berechnete Zugriffe oft nur sehr schwer zu finden. Der Preis dafür ist die langsamere Ausführung durch die internen Prüfabfragen. Intern wird ein Feld als Objekt behandelt. In einem Feld-Objekt ist auch die Information über die Länge abgelegt. Diese Information kann über eine Variable namens length abgefragt werden. Die genaue Syntax ist feldname.length. Dann kann ein Feld in der Form for(int j=0; j<feld.length; j++ ) { System.out.println( feld[j] ); } ausgegeben werden. Es es ist auf diese Art und Weise jederzeit möglich, die aktuelle Länge eines Feldes abzufragen. Seit Version 1.5 bietet Java für solche Fälle eine erweiterte Form der forSchleife an. Anstelle eines expliziten Zählers werden nacheinander alle Elemente eines Feldes angesprochen. Diese Art von Schleifen werden allgemein als foreachSchleifen bezeichnet. Die Ausgabe aller Elemente wird dann in der Form for(int w : feld ) { System.out.println( w ); } realisiert. In der Schleife werden nacheinander der Variablen w alle Elemente des Feldes feld zugewiesen. Beispiel 5.6 Sieb des Eratosthenes Bestimmung von Primzahlen nach der Methode des Eratosthenes1 . 1 griechischer Mathematiker, 276-195 v. Chr. 5.2. MEHRDIMENSIONALE FELDER 49 public static void main(String[] args) { int max = 1000; boolean[] istPrimzahl = new boolean[max+1]; int i; // Hypothese: alles sind Primzahlen for( i=1; i<=max; i++ ) istPrimzahl[i] = true; int test = 2; while( test < max / 2 ) { // naechste Primzahl suchen while( ! istPrimzahl[test] ) test++; // ganzzahlige Vielfache dieser Primzahl streichen for( i=test+test; i<=max; i+=test ) istPrimzahl[i] = false; ++test; } // verbliebene Primzahlen ausgeben for( i=2; i<=max; i++ ) { if( istPrimzahl[i] ) System.out.println( "Primzahl " + i ); } } 5.2 Mehrdimensionale Felder Mehrdimensionale Felder werden durch mehrere Klammerpaare spezifiziert: Beispiel 5.7 Zweidimensionales Feld // Zweidimensionales Feld mit 10 x 20 Elementen int[][] zifeld = new int[10][20]; Zweidimensionale Felder sind intern ineinander geschachtelte Felder. Damit kann man auch nicht-rechteckige Felder erzeugen. Die Anweisung zifeld[0] = new int[30]; führt dazu, dass das erste Unterfeld jetzt 30 Elemente hat. Beispiel 5.8 Ineinander geschachtelte Felder Im folgenden Code wird ein Feld für alle Tage eines Jahres geordnet nach Monaten angelegt. Für jeden Tag wird über den logischen Wert dargestellt, ob es sich um einen Urlaubstag handelt. 50 KAPITEL 5. FELDER boolean[][] urlaub = new boolean[12][]; urlaub[0] = new boolean[31]; urlaub[1] = new boolean[28]; ... urlaub[7][0] = true; // 1. August ist ein Urlaubstags urlaub[11][24] = true; // 25. Dezember ist ein Urlaubstags 5.3 Übungen Übung 5.1 Fußball In den letzten 5 Jahren haben Professoren und Studenten gegeneinander Fußball gespielt. Die jeweils erzielten Tore sind in zwei Feldern int[] stud = { 3, 2, 5, 7, 1}; int[] prof = { 0, 4, 2, 1, 1}; gespeichert. Im ersten Jahr war das Ergebnis 3:0, dann 2:4 und so weiter. Schreiben Sie eine Auswertung um • die Gesamt-Punkte für die Studenten (Sieg 3 Punkte, Remis 1 Punkt) • das Gesamt-Torverhältnis zu ermitteln. Die Ausgabe könnte wie folgt aussehen (willkürliche Zahlenwerte): Studenten gegen Professoren Punkte: 33 Tore: 25:23 Übung 5.2 In der folgenden Methode wird ein Feld mit zufälligen Werten gefüllt: public void auswerten() { int anzahl = 100; double[] werte = new double[anzahl]; for( int i=0; i<werte.length; i++) { werte[i] = Math.random(); } } Lassen Sie aus diesem Feld folgende Größen berechnen: • Mittelwert • kleinster Wert 5.3. ÜBUNGEN 51 • größter Wert Übung 5.3 Definieren Sie ein int Feld, das für jeden Monat (0-11) die Anzahl der Tage enthält (kein Schaltjahr). Prüfen Sie die Eingabe, indem Sie alle Werte addieren. Wie kann man unter Verwendung dieses Feldes ausrechnen, in den wievielten Monat der Tage M (0-364) eines Jahres fällt? Übung 5.4 Verwenden Sie das Feld aus Übung 5.3 für eine elegantere Lösung zum Aufbau des zweidimensionalen Urlaub-Feldes in Beispiel 5.8. Tragen Sie einige Urlaubstage ein und lassen Sie das Ganze in geeigneter Form ausgeben. Beispiel für eine Ausgabe (verkürzte Darstellung): * 1 1 1 1 1 1 * 1 1 1 1 * 2 2 2 2 2 2 * 2 2 2 2 3 3 3 3 3 3 3 * 3 3 3 3 4 4 4 4 4 4 4 * 4 4 4 4 5 5 5 5 5 5 5 * 5 5 5 5 6 6 6 6 6 6 6 6 6 6 6 6 7 7 7 7 7 7 7 7 7 7 7 7 8 8 8 8 8 8 8 8 8 8 8 8 9 9 9 9 9 9 9 9 9 9 9 9 10 10 10 10 10 10 10 10 10 10 10 10 11 11 11 11 11 11 11 11 11 11 11 11 12 12 12 12 12 12 12 12 12 12 12 12 18 18 18 18 18 18 18 18 18 18 18 18 19 19 19 19 19 19 19 19 19 19 19 19 20 20 20 20 20 20 20 20 20 20 20 20 21 21 21 21 21 21 21 21 21 21 21 21 22 22 22 22 22 22 22 22 22 22 22 22 23 23 23 23 23 23 23 23 23 23 23 23 24 24 24 24 24 24 24 24 24 24 24 24 25 25 25 25 25 25 25 25 25 25 25 * 26 27 28 29 30 31 26 27 28 26 27 28 29 30 31 26 27 28 29 30 26 27 28 29 30 31 26 27 28 29 30 26 27 28 29 30 31 26 27 28 29 30 31 26 27 28 29 30 26 27 28 29 30 31 26 27 28 29 30 * 27 28 29 30 31 Übung 5.5 Felder als Zähler Implementieren Sie auf der Basis der Methode Math.random() eine WürfelSimulation, die Zahlen zwischen 1 und 6 erzeugt. Zählen Sie in einem Feld entsprechender Größe, wie oft jede Zahl bei 10000 Würfen vorkommt. Übung 5.6 Game of Life Ausgedacht hat sich dieses „Spiel“ der amerikanische Mathematiker Conway. Bekannt wurde das Game of Life, als es im Jahr 1970 im Wissenschaftsmagazin Scientific American vorgestellt wurde. Die Regeln sind einfach: Gespielt wird auf einem rechteckigen Feld, das wie ein Schachbrett in lauter quadratische Zellen eingeteilt ist. Eine Zelle ist entweder besetzt oder unbesetzt. Eine Zelle hat bis zu 8 Nachbarzellen, d. h. Zellen die in gerader oder schräger Richtung direkt neben der Zelle liegen. Eine Konfiguration von besetzten und unbesetzten Zellen kann man sich als Generation von Lebewesen vorstellen, aus der sich die nächste Generation nach folgenden Regeln entwickelt: 1. Eine leere Zelle wird in der nächsten Generation besetzt, wenn sie genau drei besetzte Nachbarzellen hat. 2. Eine besetzte Zelle bleibt auch in der nächsten Generation besetzt, wenn sie zwei oder drei besetzte Nachbarzellen hat. 52 KAPITEL 5. FELDER 3. Alle Zellen, bei denen die Voraussetzungen der Regeln 1 und 2 nicht zutreffen, sind in der nächsten Generation unbesetzt. Realisieren Sie eine einfache Version des Spiels. Benutzen Sie zwei 2-dimensionale Felder oder ein 3-dimensionales Feld für die aufeinander folgenden Generationen um eine Version von Life zu realisieren. Wählen Sie die Feldgröße passend zu der Größe des Ausgabefensters. Hinweise: Die Behandlung der Zellen am Rand wird wesentlich vereinfacht, wenn man das Brett um zusätzliche, leere Zeilen und Spalten an den Rändern erweitert. Bei Aufruf der Methode Thread.sleep(long m) wartet das Programm m Millisekunden. Kapitel 6 Methoden 6.1 Einleitung Ein wesentlicher Gedanke der strukturierten Software-Entwicklung ist die Aufteilung einer komplexen Aufgabe in einzelne Teilaufgaben. Programmtechnisch entspricht dies der Aufteilung des Programms in mehrere Untereinheiten oder Module. In Java ist die entsprechende Untereinheit eine Methode. Eine Methode ist ein eigenständiger Verarbeitungsblock mit definierten Eingangs- und Ausgangswerten. Vorteile von Methoden sind: • Berechnungen, die mehrfach benötigt werden, brauchen nur einmal programmiert zu werden. • Kapselung von Funktionalitäten (black box Prinzip, information hiding) • Verbesserte Testmöglichkeiten • Wiederverwendbarkeit in anderen Projekten In verschiedenen Programmiersprachen gibt es unterschiedliche Bezeichnungen und Konzepte für Module. Man kann grob unterscheiden: • Funktionen verfügen über Argumente und Rückgabewerte • Prozeduren oder Unterprogramme (subroutines) haben keinen Rückgabewert • Methoden sind in objektorientierten Sprachen Funktionen, die zu einer Klasse gehören • Makros sind Quellcode-Bausteine, bei denen vor dem Kompilieren ein gemeinsames, in der Regel längeres Stück Code an die entsprechend markierten Stellen eingefügt werden (Makro-Ersetzung). 53 54 6.2 KAPITEL 6. METHODEN Definition Eine Methode ist ein Block von Definitionen und Anweisungen mit einem Namen sowie Eingangswerten und einem Ausgangswert. Methoden stehen auf der ersten Ebene, d. h. die Definitionen können nicht ineinander geschachtelt werden. Allerdings ist es sehr wohl möglich, dass eine Methode eine weiter Methode aufruft. Die allgemeine Form ist attribute rückgabe-typ methodenName( parameter ) . . . Zunächst wird spezifiziert, welchen Typ von Variable die Methode zurück gibt. Anschließend folgt der Name der Methode und dann in runden Klammern die Liste der Eingangswerte (Parameter). Jeder Parameter besteht aus dem Typ und einem Namen. Mehrere Parameter werden durch Komma getrennt. Die verkürzte Schreibweise wie etwa (int i, j) ist nicht erlaubt, jeder Parameter benötigt eine eigene Typangabe: (int i, int j). Die eigentliche Methode folgt dann als Block begrenzt durch geschweifte Klammern. Für die Rückgabe steht der Befehl return ausdruck; zur Verfügung. Der Ausdruck muss dabei dem bei der Definition der Methode angegeben Datentyp entsprechen. Bei Erreichen eines return Befehls wird die Bearbeitung der Methode beendet. Der Ausdruck wird ausgewertet und an das aufrufende Programm zurück gegeben. Innerhalb einer Methode können mehrere return Anweisungen stehen, um alternative Rücksprünge zu realisieren. Beispiel 6.1 Berechnung der Fakultät: int fakultaet( int wert ) { int result = 1; while( wert > 1 ) { result *= wert; --wert; } return result; } Das Beispiel berechnet für den Eingangswertswert die Fakultät und gibt das Ergebnis zurück. Um eine Methode aufzurufen, wird sie über den Namen und mit entsprechenden Argumenten angesprochen. Eine Methode liefert in den meisten Fällen einen Wert zurück. Das aufrufende Programm (Hauptprogramm) kann diesen Wert ignorieren oder in einem Ausdruck als rvalue weiter verwenden. Beispiel 6.2 Verwendung der Methode Fakultät: 6.2. DEFINITION 55 /* gibt den Wert z! aus */ System.out.println( fakultaet(z) ); /* legal aber wenig wirksam */ fakultaet( 2 * 3 ); Bei dem Aufruf wird der Ausdruck in der Klammer ausgewertet und an die Methode übergeben. Selbst wenn man direkt eine Variable als Argument angibt, wird nur der Wert bzw. Inhalt der Variablen übergeben (call by value). Daher erfolgen alle Änderungen in der Methode an einem Argument nur an der lokalen Kopie. Im obigen Beispiel bleibt etwa der Wert der Variablen z unverändert. Grundsätzlich hat einen Methode keinen Zugriff auf die Variablen im aufrufenden Teil. Aufgrund der strikten Trennung gibt es auch keine Konflikte bei gleichen Namen für Variablen. Variablen innerhalb einer Methode haben nur die Lebensdauer eines Methodesaufrufs. Sie werden beim Aufruf angelegt und nach Ende der Methode wieder gelöscht (automatic variable). Beispiel 6.3 Methode ohne Parameter und Rückgabewert: void ausgeben() { System.out.println( "Methode ausgeben" ); } In dem Beispiel sind zwei neue Elemente enthalten: • Die Parameterliste kann leer sein • Eine Methode ohne Rückgabewert hat den Typ void (engl. leer, nichtig). Eine solche Methode endet entweder mit einem return ohne Wert oder an der schließenden Blockklammer. Beispiel 6.4 Berechnung von ab : /* Methode zur Berechnung der Potenz a hoch b * Einschränkung: b >= 0 */ int power( int base, int exponent ) { int i; int result = 1; for( i=0; i<exponent; i++ ) result *= base; return result; } 56 6.3 KAPITEL 6. METHODEN Überladen von Methoden Es ist in Java möglich, unter einem Namen mehrere Methoden mit unterschiedlichen Parameterlisten zu definieren. Die Methoden werden damit überladen. Selbstverständlich sollten die Methoden einen engen Bezug zueinander haben. Zur Unterscheidung der verschiedenen Varianten dient die Signatur einer Methode. Die Signatur als Kennung besteht aus dem Namen der Methode, Anzahl, Reihenfolge und Typen ihrer Parameter und – im allgemeinen aber nicht in Java – dem Typ des Rückgabewertes. Zwei oder mehrere Methoden dürfen den gleichen Namen haben, sofern sie sich in ihrer Signatur unterscheiden. Man kann dieses Konzept einsetzen, um die gleiche Funktionalität für unterschiedliche Datentypen bereitzustellen. Beispielsweise kann man eine Methode power zur Berechnung von Potenzen sowohl für ganzzahlige als auch GleitkommaWerte implementieren: double power( double x ) { ... } int power( int i ) { ... } Abhängig vom Argument wählt der Compiler die passende Methode aus. Gebräuchlich ist dieser Mechanismus auch, um zusätzliche Parameter zu übergeben. Wir können eine weiter Methode zum Ausdrucken definieren, die einen zusätzlichen String als Überschrift erhält: Beispiel 6.5 Überladen der Methode ausgeben: void ausgeben(String titel) { System.out.println(titel); ausgeben(); } Je nach Aufruf – mit oder ohne String-Parameter – wählt der Compiler die richtige Version aus. Die verschiedenen Definitionen müssen nur eindeutig sein, d. h. sie müssen sich im Typ oder in der Anzahl der Parameter unterscheiden. Eine Version kann eine andere Version aufrufen. Eine andere häufig angewandte Technik simuliert Standardbelegungen (Default-Werte) für Parameter. Betrachtet man als Beispiel eine Methode int beispiel( int i, int j ). Wenn man für den zweiten Parameter eine Standardbelegung (z. B. den Wert 0) bereit stellen möchte, kann dies durch eine Methode mit nur einem Parameter implementiert werden: int beispiel( int i ) { // Default Wert 0 für zweiten Parameter return beispiel( i, 0 ); } 6.4. ÜBERGABE VON FELDERN 57 Dann kann man je nach Bedarf entweder die einfache Version der Methode mit nur einem Parameter und einer Standardbelegeung der zweiten Größe oder die allgemeinere Form mit zwei Parametern verwenden. Mit dieser Technik kann man oft die eigentliche Berechnung oder Bearbeitung an einer Stelle konzentrieren. Anstatt den gleichen Code in mehreren Varianten zu kopieren, wird er nur an einer Stelle eingetragen. Verschiedene Varianten rufen dann immer die gleiche Kernmethode auf. Dies ist sehr viel besser, als den gleichen Code mehrfach einzutragen. Korrekturen und Erweiterungen brauchen dann nur an einer Stelle angebracht zu werden. Übung 6.1 Methode print Wie viele überladene Versionen von print und println gibt es? 6.4 Übergabe von Feldern Der Übergabemechanismus call by value, bei dem die Methode mit lokalen Kopien arbeitet, beschränkt sich auf die einfachen Datentypen. Bei komplexeren Objekten wie Feldern wäre dies nicht sinnvoll. Felder stets vollständig zu kopieren würde einen hohen Aufwand bedeuten. Außerdem ist es oft gerade gewünscht, per Methodenaufruf den Inhalt eines Feldes zu verändern. So ist es bei einer Methode zum Sortieren effizienter, das Feld direkt zu bearbeitet. Daher wird bei Feldern statt der Inhalte ein Verweis (Referenz) an die Methode übergeben. Dementsprechend nennt man dieses Verfahren call by reference. Über diese Referenz können die einzelnen Zellen angesprochen werden. Formal verwendet man die gleiche Schreibweise. Der Ablauf ist im folgenden Beispiel dargestellt. Beispiel 6.6 Methode zum Vertauschen zweier Zellen eines Feldes: void tausche( int[] feld, int i, int j ) { int tmp = feld[i]; feld[i] = feld[j]; feld[j] = tmp; } ... void testeTausche() { int[] test = {1,2,3,4,5,6,7,8,9,10}; tausche( test, 0, 1 ); } Nach der Ausführung der Methode sind die beiden ersten Werte in test vertauscht. Die Methode greift über die Referenz auf das in der aufrufenden Methode testeTausche eingeführte Feld zu. Ähnlich ist die Situation bei der Rückgabe eines Feldes. Wird ein Feld in einer Methode angelegt und dann als Rückgabewert an die aufrufende Methode übergeben, so bleibt der Speicherplatz mit den Werten erhalten. 58 KAPITEL 6. METHODEN Beispiel 6.7 Rückgabe eines Feldes. int[] legeQuadrateAn( int l ) { int[] feld = new int[l]; for( int i=0; i<l; i++ ) feld[i] = i*i; return feld; } public void testeLegeQuadrateAn() { int[] q = legeQuadrateAn( 5 ); for( int i=0; i<q.length; i++ ) { System.out.println( "q["+i+"]: "+ q[i] ); } } liefert die Ausgabe q[0]: q[1]: q[2]: q[3]: q[4]: 0 1 4 9 16 Der Datentyp Feld wurde hier stellvertretend für alle komplexere Datentypen verwendet. Die vorgestellten Mechanismen gelten in gleicher Weise für allgemeine Objekte, die wir später behandeln werden. Wichtig ist die Unterscheidung zwischen den beiden Übergabeverfahren. Einfache Werte werden kopiert und Änderungen an den lokalen Kopien haben keine weiteren Auswirkungen. Bei komplexen Werten wird eine Referenz übergeben, mittels derer die Methode auf die Inhalt zugreifen und sie dauerhaft verändern kann. 6.5 Rekursion Wir haben gesehen, dass eine Methode mit lokalen Kopien der Argumenten arbeitet und alle Variablen temporärer Natur sind. Daher ist es möglich, dass eine Methode sich selbst — oder besser gesagt eine neue Instanz von sich selbst – wieder aufruft. Das klassische Beispiel für diese Rekursion ist das Berechnen der Fakultät: int fakultaetRekursiv( int wert ) { if( wert == 0 || wert == 1) return 1; else return fakultaetRekursiv(wert - 1) * wert; } 6.6. ANMERKUNGEN 59 Wenn das Argument 0 oder 1 ist, wird wegen 0! = 1! = 1 direkt der Wert 1 zurück gegeben. Ansonsten wird der aktuelle Wert mit der Fakultät des um 1 kleineren Wertes gemäß n! = n ∗ (n − 1)! berechnet. Beginnend mit einem positiven Wert wird diese Rekursion wiederholt, bis schließlich der immer wieder verminderte Wert auf 1 gefallen ist. Rekursive Lösungen sind nicht unbedingt effizienter in Bezug auf Speicherbedarf oder Rechenzeit. Aber die resultierenden Programm sind sehr kompakt und oft leichter zu schreiben und zu verstehen. 6.6 Anmerkungen In den Beispielen und Übungen hatten wir bereits einige Methoden wie etwa println eingesetzt. In jeder Klasse kann es eine Methode main geben. Diese Methode dient beim direkten Ausführen einer Klasse Startpunkt. Wenn eine Klasse Beispiel eine Methode main enthält, so wird durch den Befehl java Beispiel genau diese Methode aufgerufen. Die vollständige Definition ist public void main(String[] args) d. h. main gibt keinen Wert zurück und hat einen Parameter, dessen Bedeutung wir noch nicht kennen. Nach der Rückkehr aus main werden eventuell belegte Ressourcen freigegeben und die Anwendung beendet. 6.7 Übungen Übung 6.2 Klausurnoten In den Allgemeine Bestimmungen für Bachelorprüfungsordnungen1 ist in § 6 die Bewertung der Leistungen geregelt. Verwenden Sie die Formel N = 4 − 3 ∗ (P − 50)/45 P ≥ 50 zur Berechnung den Note N bei P erzielten Prozent. Implementieren Sie auf dieser Basis Methoden, um die Note zu berechnen. Die Methoden sollen folgende Parameter haben 1. ein int-Wert mit den Prozenten als Zahl zwischen 0 und 100 2. zwei int-Werte, die erzielten Punkte und die maximal möglichen Punkte 3. ein double-Wert mit den Prozenten als Zahl zwischen 0 und 1 1 Zu finden über die Webseite der FH und dann unter FH Download » Studium » Modulhandbücher, Studien- und Prüfungsordnungen, Studienganginfo 60 KAPITEL 6. METHODEN 4. ein Feld von double-Werten mit den Prozenten für mehrere Studierenden, jeweils als Zahl zwischen 0 und 1 Rückgabewert soll jeweils ein double beziehungsweise im letzten Fall ein Feld von double Werten sein. Die Rundung auf eine Nachkommastelle ist nicht notwendig. Schreiben Sie die eigentliche Berechnung nur einmal. Übung 6.3 Suchen Sie alle natürliche Zahlen < 100000, die gleich der Summe der Fakultäten ihrer einzelnen Ziffern sind: abc = a! + b! + c! Beachten Sie die Vereinbarung 0! = 1. Eine Lösung ist 145 = 1! + 4! + 5! = 1 + 24 + 120 Verwenden Sie dazu eine Methode zur Berechnung der Fakultät. Die Ermittlung der einzelnen Stellen ist in Beispiel 3.10 beschrieben. Übung 6.4 Implementieren Sie eine rekursive Lösung zur Berechnung der FibonacciZahlen. Wie ist die Rechenzeit bei größer werdenden Zahlen? Wie erklären Sie diesen Effekt? Übung 6.5 Türme von Hanoi Die folgende Klasse löst die Aufgabe rekursiv. • Wie funktioniert das Programm? • Wie viele Schritte benötigt man für einen Turm der Höhe n? public class Hanoi { int anzahlSchritte; void lege(int n, String von, String nach, String zwischen) { if (n>0) { lege(n-1, von, zwischen, nach); System.out.println(n + ". Scheibe von " + von + " nach " + nach); lege(n-1,zwischen, nach, von); anzahlSchritte++; } } public void bewegen(int maxzahl) { lege(maxzahl, "Turm 1", "Turm 2", "Turm 3"); System.out.println("-----------------------------------------"); System.out.println(anzahlSchritte + " Schritte"); } } Kapitel 7 Algorithmen – vom Problem zum Programm 7.1 Algorithmen Mit den bisher besprochenen Elementen der Sprache Java lassen sich bereits eine Vielzahl von Problemen lösen. Ein solches Beispiel war die Berechnung von Primzahlen. Dem Programm zugrunde liegt ein Verfahren zur Berechnung – „suche systematisch echte Teiler von n“. In einfachen, überschaubaren Fällen reicht eine solche Verfahrensidee bereits als Grundlage für das Programm. Einfach bezieht sich dabei sowohl auf das Problem als auch auf den Lösungsplan. In komplexeren Fällen ist demgegenüber die Ausarbeitung des Lösungsplans selbst eine aufwendige und schwierige Tätigkeit. Wäre das Problem z. B. „suche alle Primzahlen mit bis zu 30 Stellen“, dann würde der einfache Ansatz an der mangelnden Rechengenauigkeit und der benötigten Rechenzeit scheitern und wir müssten zunächst ein geeignetes Verfahren suchen, um solche großen Zahlen zu behandeln. In den Anwendungsbereichen der Informatik fallen viele komplexe Probleme: • effiziente Suche nach Schlüsselwörtern im Internet • Steuerung von Fertigungsabläufen • Verwaltung eines Internet Shops • Verschlüsselung zur Datensicherheit • Vernetzen von sehr vielen Rechnern • eine email mit Anhängen von Australien nach Grönland bringen • Grammatikprüfung eines Textes Auf der anderen Seite verstehen Computer nur sehr einfache Befehle: 61 62 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM • addiere Speicherzelle N zu Akkumulator • erhöhe Indexregister um 1 • springe zur Programmzeile 3678 Verschiedene Schritte auf dem Weg zu einem Programm, das die gestellte Aufgabe löst, sind in der folgenden Tabelle zusammen gestellt. Problem Problemanalyse Spezifikation Design/Entwurf Programmierung ausführbares Programm Test Programm Die höheren Programmiersprachen stellen eine erste Abstraktionsebene zur Verfügung. Der Programmierer braucht sich nicht (kaum) um konkrete Details des Rechners zu kümmern, sondern kann das Lösungsverfahren mit den Sprachelementen formulieren. Ein solches Programm ist dann im Idealfall auch auf anderen Rechnern einsetzbar. Auf der anderen Seite gilt es, das Problem zunächst richtig zu erfassen und dann einen oder mehrere alternative Lösungspläne zu entwerfen. Dabei kann die Problemanalyse bereits ein aufwändiger Prozess sein. Es gilt Randbedingungen zu klären hinsichtlich der Leistungsfähigkeit (bis zu welcher Primzahl soll das Programm funktionieren?) als auch allgemeiner u.U. auch nicht technischer Vorgaben (kompatibel mit Version 1.0, bis Ende nächsten Monats fertig). Elementare Fragen in der Problemanalyse sind • Welches Problem ist zu lösen? • Habe ich die Problemstellung richtig verstanden? • Ist die Aufgabenstellung vollständig beschrieben? • Kenne ich alle Randbedingungen? • Welche Informationen benötige ich gegebenenfalls noch? Die Problemanalyse sollte so genau wie möglich sein, so dass nachträgliche Änderungen, die sehr kosten- und zeitaufwendig sein können, vermieden werden. Änderungen an der Problemanalyse während oder nach der Programmierung sollten unbedingt vermieden werden. Das Ergebnis der Problemanalyse ist eine Spezifikation. Sie enthält Vorgaben für die Funktionalität des zu entwickelnden Programms. Man unterscheidet zwei Stufen: 7.1. ALGORITHMEN 63 1. das Lastenheft, beschreibt allgemein was das Programm (System) leiste sollte 2. das Pflichtenheft, enthält „ausführliche Beschreibung der Leistungen, die erforderlich sind oder gefordert werden, damit die Ziele des Projekts erreicht werden“(DIN 69901), Ausgehend von der Problemanalyse werden in der Regel verschiedene Lösungspläne entworfen und miteinander verglichen. Ein Lösungsplan enthält eine Vorschrift zur schrittweise Bearbeitung der Aufgabe. Eine solche Vorschrift nennt man Algorithmus. Jeder Algorithmus besteht somit aus (vielen) einzelnen Schritten. Größere Aufgaben müssen in der Regel strukturiert und in handhabbare Teilaufgaben zerlegt werden. Daher kann es notwendig werden, einzelne Schritte durch eine feinere Betrachtung wiederum als Algorithmus aufzufassen, der selbst auch wieder aus einzelnen Schritten besteht (Verfeinerung). Dies wird so oft wiederholt, bis aus dem ursprünglich „groben Algorithmus“ ein für die Lösung des Problems hinreichend „feiner Algorithmus“ entwickelt worden ist. Diese so genannte topdown Vorgehensweise ist insbesondere bei komplexen Problemen oft erforderlich. Der Algorithmus stellt jedoch noch kein Programm dar, sondern lediglich eine präzise Verfahrensvorschrift in „umgangssprachlicher Form“ oder wie wir noch sehen werden in graphischer Form. Algorithmen sind nicht auf die Informatik beschränkt sondern begegnen uns auch im täglichen Leben. Typische Beispiele sind • Gebrauchsanleitungen • Anleitungen zum Aufbauen von Möbeln • Kochrezepte Betrachten wir folgenden Algorithmus: 1. Wasser einfüllen 2. Filtertüte einlegen 3. Kaffeepulver einfüllen 4. Gerät einschalten Dies ist offensichtlich ein Algorithmus (untere mehreren) zur Zubereitung von Kaffee. Charakteristisch ist die Abfolge von einzelnen klar abgegrenzten Schritten. Allerdings ist für die Ausführung z. B. Anweisung 3 noch nicht detailliert genug. Eine Verfeinerung könnte sein: 1. Kaffeepulver einfüllen: 64 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM (a) Schrank links unten öffnen (b) Kaffeedose heraus nehmen (c) falls Kaffeedose nicht leer (d) Kaffeedose öffnen (e) Kaffeelöffel aus Dose nehmen (f) einen Löffel Kaffee pro Tasse nehmen . . . Selbst für die einfache Aufgabe ergibt sich bereits ein recht umfangreicher Algorithmus, insbesondere wenn man mögliche Fehlerfälle berücksichtigt (Kaffeedose leer am Sonntag). Aus dem Algorithmus wird allerdings noch kein Kaffee. Dazu muss der Algorithmus noch in ein ausführbares Programm umgesetzt werden. So wird aus dem Punkt „Gerät einschalten“ ein Programmteil „bewege Zeigefinger mit einer Geschwindigkeit von . . . zur Position x,y,z“, der selbst eine Fülle von wohl koordinierten Muskelbewegungen bedingt. Ein Beispiel aus unseren Übungsaufgaben ist die Berechnung der Fakultät einer Zahl: setze ergebnis = 1 setze zähler = 2 solange zähler <= zahl ergebnis = ergebnis * zähler zähler = zähler + 1 Mit diesen Überlegungen kommen wir zu der Definition: Ein Algorithmus ist ein präzise formulierter Plan, wie man durch Ausführen von einzelnen Arbeitsschritten (Aktionen) schrittweise zur Lösung eines Problems kommt. Oft ist ein Algorithmus auf eine ganze Klasse von gleichartigen Problemen anwendbar. Führt der Algorithmus für jede zulässige Eingabe in endlich vielen Schritten zur Lösung heißt er terminierend. Wenn weiterhin der Ablauf eindeutig bestimmt ist, d. h. es gibt zu jeder Aktion genau eine Folgeaktion, so spricht man von deterministischen Algorithmen. Dies impliziert, dass für einen festen Eingangswert stets das gleiche Ergebnis geliefert wird. Nicht-deterministische Algorithmen findet man u.a. im Bereich von Netzwerken, wo der Übertragungsweg von Datenpaketen mit gleichem Start- und Zielort variieren kann. Algorithmen bieten eine übersichtliche und leicht nachvollziehbare Darstellung des Lösungsplans unabhängig von den konkreten Realisierungsdetails. Damit sind sie das geeignete Mittel während des Entwurfs des Lösungsplans und zur Dokumentation oder Erklärung. Es ist sehr viel leichter, einen Lösungsplan anhand des Algorithmus zu erklären als etwa mit dem fertigen Programm. Hat man einen klar spezifizierten Algorithmus als Ausgangsbasis, ist die Umsetzung 7.2. FLUSSDIAGRAMM 65 in eine konkrete Programmiersprache stark vereinfacht. Umgekehrt gibt es auch den „analytische Einsatz“. Man bekommt ein schlecht dokumentiertes Programm und möchte den Ablauf nachvollziehen. In solchen Fällen ist es hilfreich, sich aus dem Code wieder die höhere Darstellung zu rekonstruieren. Zur graphischen Darstellung von Algorithmen gibt es zwei weit verbreitete Formen: Flussdiagramme und Struktogramme nach Nassi-Shneidermann. Im folgenden werden beide Methoden kurz vorgestellt. 7.2 Flussdiagramm Flussdiagramme (engl. flowchart) – auch als Programmablaufpläne bezeichnet – sind, wie der Name sagt, eine graphische Darstellung von Abläufen. Sie werden auch außerhalb der Programmierung zur Veranschaulichung von Abläufen und Vorgängen verwendet. Die einzelnen Symbole sind in DIN 66001 standardisiert. Das grundlegende Element bei Flussdiagrammen ist die Verarbeitung, dargestellt durch ein Rechteck. Der Fluss der Verarbeitung, d. h. die Abfolge der einzelnen Aktionen ist durch Pfeile dargestellt. Anweisung 1 ? Anweisung 2 Bedingungen werden durch eine Raute symbolisiert. Bedingung N- Zweig 2 Y ? Zweig 1 Mit diesen Grundelementen können Schleifen dargestellt werden. Als Beispiel sei eine abweisende Schleife angegeben. 66 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM - N 6 - Bedingung J Schleifenkörper Beispiel 7.1 Die Berechnung der Fakultät einer positiven Zahl. - Fertig N zähler = 2 ergebnis = 1 7.3 - zähler <= zahl 6 J - ergebnis = ergebnis * zähler zähler = zähler + 1 Struktogramme Struktogramme entstanden aus dem Bemühen den Prozess der Programmentwicklung zu systematisieren und dadurch übersichtliche und wartbare Programme zu erhalten. Dabei wird das gesamte Programm in einer top-down Vorgehensweise in möglichst voneinander unabhängige Bausteine - die Strukturblöcke - zu zerlegen. Grundbausteinen von Struktogramme sind Strukturblöcke. Entwickelt wurde diese Darstellungsform 1972/73 von Isaac Nassi und Ben Shneiderman und später als DIN 66261 genormt. Für die Darstellung der Blöcke gilt: • jeder Block ist rechteckig • jeder Block hat oben einen Eingang und unten einen Ausgang • Blöcke stehen untereinander oder sind vollständig ineinander geschachtelt • die Grundtypen sind – Sequenzblock – Selektionsblock 7.3. STRUKTOGRAMME 67 – Iterationsblock Ein Sequenzblock ist ein Rechteck, in dem eine Folge von Schritten (mindestens einem) steht. Die Schritte werden nacheinander ausgeführt. Mehrere Schritte können zur besseren Übersicht in mehrere aufeinander folgende Sequenzblöcke aufgeteilt werden. Nassi-Shneiderman — Sequenz-Symbol Anweisung 1 Anweisung 2 Ein Selektionsblock besteht aus der Abfrage und im einfachsten Fall zwei Alternativen. Nassi-Shneiderman — If-Verzweigung J logische Bedingung Then-Block N Else-Block Bei dem Iterationsblock unterscheidet man zwei Fälle • Prüfung am Anfang (Kopfprüfung, abweisende Schleife) • Prüfung am Ende (Fußprüfung, nicht abweisende Schleife) Dargestellt werden die Iterationsblöcke durch zwei ineinander geschachtelte Rechtecke. Die logische Bedingung wird dabei vor oder nach dem Schleifenkern eingetragen. Nassi-Shneiderman — abweisende Schleife logische Bedingung Schleifenkern 68 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM Nassi-Shneiderman — nicht abweisende Schleife Schleifenkern logische Bedingung Eine sinnvolle Vorgabe ist, dass Struktogramme nicht länger als eine Seite werden sollen. Bei einem umfangreichen Algorithmus kann man dies einhalten, indem man kleinere, in sich abgeschlossene Teile in Modulblöcke zusammen fasst. In dem übergeordneten Struktogramm schreibt man den Modulnamen sowie eine kurze Funktionsbeschreibung. Auf diese Weise kann man zunächst den Algorithmus grob entwickeln und wie oben beschrieben dann weiter verfeinern. Graphisch wird ein Modul wie folgt dargestellt: Nassi-Shneiderman — Modul Beschreibung der Modulfunktion (Modulname) Aus Sicht des übergeordneten Blocks ist bei einem Modul nur die Ein- und Ausgabe sowie die Funktionalität relevant. Die internen Abläufe sind verborgen und sollen keine Rückwirkungen auf die höhere Ebene haben (black box). Beispiel 7.2 Als Beispiel wieder die Berechnung der Fakultät einer positiven Zahl. Nassi-Shneiderman — Fakultät Zähler = 2 Ergebnis = 1 Zähler < = Zahl Ergebnis = Ergebnis * Zähler Zähler = Zähler + 1 7.4. AKTIVITÄTSDIAGRAMM 69 Abbildung 7.1: Aktivitätsdiagramm zur Berechnung der Fakultät 7.4 Aktivitätsdiagramm Im Bereich der objektorientierten Programmierung spielen beide Darstellungsformen keine große Rolle mehr. Stattdessen werden die Aktivitätsdiagramm (engl. activity diagram) aus der Unified Modeling Language (UML) verwendet. Bild 7.1 zeigt die entsprechende Darstellung für die Berechnung der Fakultät. 7.5 Übungen Übung 7.1 Analysieren Sie folgende Strukturen mit Flussdiagrammen oder Struktogrammen. Gibt es einfachere Lösungen? a) if( a > 0 ) { b = a; } else { 70 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM if( a == 0 ) { b = 0; } else { b = -a; } } b) if( a > 0 ) { if ( a < 0 ) { a = 1; } else { a = 2; } } Übung 7.2 Das folgende Struktogramm beschreibt den Euklidischen1 Algorithmus zur Bestimmung des größten gemeinsamen Teilers (ggT) zweier Zahlen a und b. Der Einfachheit halber soll die Ungleichung a > b gelten. Setzen Sie das Struktogramm in ein Java-Programm um. Nassi-Shneiderman — Euklidischer Algorithmus Solange b größer als 0 Bestimme den Rest bei Division von a durch b Kopiere b nach a Ersetze b durch Rest Der ggT steht jetzt in a Übung 7.3 Entwickeln Sie ein Programm, um Messwerte eingeben zu können. Das Programm erwartet positive Zahlen, die der Anwender nacheinander eingibt. Nach jeder Eingabe wird der bis jetzt kleinste und größte Wert sowie die Anzahl der Eingaben ausgegeben. Das Programm wird durch Eingabe einer negativen Zahl oder 0 beendet. Wie sieht ein entsprechendes Struktogramm aus? Hinweis: Um die Zahlen einlesen zu können, muss zunächst ein BufferedReader angelegt werden. Dann kann mit dem unten angegebenen Aufruf ein int-Wert gelesen werden. 1 Euklid, griechischer Mathematiker ca. 325-ca. 270 v. Chr. 7.5. ÜBUNGEN 71 BufferedReader br = new BufferedReader( new InputStreamReader( System.in ) ); ... int iwert = Integer.parseInt( br.readLine() ); Zusätzlich muss die Methode main mit throws Exception deklariert werden. Weiterhin wird ganz am Anfang der Datei die Anweisung import java.io.*; benötigt. Übung 7.4 Zwei Zahlenspielereien: • Gegeben sei eine 4-stellige Zahl abcd. Nach Multiplikation mit einer 1stelligen Zahl n größer 1 sollen die Stellen im Produkt in umgekehrter Reihenfolge stehen: abcd ∗ n = dcba Welche Zahlen erfüllen diese Bedingung? • Suchen Sie natürliche Zahlen (< 100000), die gleich der Summe der Fakultäten ihrer Ziffern sind: abc = a! + b! + c!. Beachten Sie die Vereinbarung 0! = 1. Benutzen Sie zur Entwicklung der Lösungswege Flussdiagramme oder Struktogramme. Beide Aufgaben sind dem Buch M. Gardner, Mathematische Hexereien, Ullstein Verlag 1977, entnommen. 72 KAPITEL 7. ALGORITHMEN – VOM PROBLEM ZUM PROGRAMM Kapitel 8 Objektorientierte Programmierung 8.1 Einleitung In den 80er Jahren erreichten die immer umfangreicher werdenden Programme eine Größe, die mit den bekannten Programmiertechniken kaum noch bewältigt werden konnte (Programmierung im Großen). Ausgehend von den Ansätzen der strukturierten Programmierung suchte man Möglichkeiten, die Komplexität durch Modularisierung und Kapselung zu reduzieren. Wenn es gelingt, ein komplexes System durch kleinere, zusammenwirkende Teilsysteme zu realisieren, kann jedes Teilsystem für sich entworfen, implementiert, getestet und optimiert werden. Im Idealfall können außerdem solche Teilsysteme - wenn sie ausreichend universell ausgelegt sind - in anderen Projekten wieder verwendet werden. Mit prozeduralen Programmiersprachen wie Fortran, C oder Pascal kann man bereits eine weitgehende Modularisierung erreichen. Dabei übernehmen Module (Funktionen) kleinere, in sich abgeschlossene Aufgaben. Funktionen haben klar definierte Schnittstellen: die Argumentliste und den Rückgabewert. Der interne Ablauf hat keinen Einfluss auf den Aufruf und die Ausführung hat keine weiteren äußeren Auswirkungen (black box, Information hiding). Die prozeduralen Programmiersprachen „erzwingen“ allerdings nicht die modulare Programmierung. Durch den Einsatz globaler Variablen kann der Entwickler modulübergreifende Abhängigkeiten einbauen. Neben der funktionalen Gliederung ist auch eine angemessene Darstellung der Daten wichtig. Mit dem in vielen Sprachen eingeführten Datentyp Verbund (Struktur) lassen sich zusammen gehörende Daten als Einheit modellieren. Die einzelnen Komponenten können dabei von unterschiedlichem Typ sein. Aus Verbundvariablen lassen sich größere Datenstrukturen wie Listen oder Bäume aufbauen. Aufbauend auf diese Ansätze - man kann von objektbasierte Programmierung sprechen - wurde eine weitergehende Modularisierung entwickelt, die sowohl Abläufe als auch Daten umfasste. Grundlegend ist die durchgängige Anwendung des 73 74 KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG Konzepts von Objekten in den drei Phasen einer Software-Entwicklung Analyse, Design und Programmierung. Die drei Phasen nennt man dann entsprechend OOA, OOD und OOP. Im folgenden wird eine erste kurze Übersicht zur objektorientierte Programmierung gegeben. Anschließend folgt der Einstieg in die Konzepte von JAVA. Wir werden später zu dem Thema objektorientierte Software-Entwicklung zurück kehren und anhand von Beispielen diese Methodik im Detail studieren. 8.2 Objektorientierte Programmierung (OOP) „Objektorientierte Programmierung ist eine Implementierungsmethode, bei der Programme als kooperierende Ansammlung von Objekten angeordnet sind. Jedes dieser Objekte stellt eine Instanz einer Klasse dar, und alle Klassen sind Elemente einer Klassenhierarchie, die durch Vererbungsbeziehungen gekennzeichnet ist.“ Aus Grady Booch: Objektorientierte Analyse und Design [Boo94] Aus diesem Zitat ergeben sich die Kernpunkte: • Programme basieren auf Objekten • Objekte sind Instanzen einer Klasse • Zwischen den Klassen besteht eine Vererbungshierarchie Klassen bestehen im wesentlichen aus Variablen und Methoden. Die Variablen definieren, welche Daten in der Klasse oder in einem Klassen-Objekt gespeichert werden können - die Eigenschaften oder Attribute einer Klasse. Durch die Methoden (Funktionen) ist das Verhalten festgelegt. Das folgende Beispiel zeigt an Hand einer Klasse Auto, wie durch Variablen und Methoden Eigenschaften und Verhalten spezifiziert werden. Beispiel 8.1 Klasse Auto Daten: String int int int int name; zulassungsJahr; kaufPreis; kilometerStand; AnzahlUnfaelle; Methoden: int float restWert(); kilometerProJahr(); 8.2. OBJEKTORIENTIERTE PROGRAMMIERUNG (OOP) void void void 75 neuerUnfall(); setzeRestWert( int wert ); erhoeheKilometerStand(); Eine Klasse definiert einen Datentyp. Dann können Variablen dieses Datentyps – Objekte oder Instanzen – angelegt werden. Jede Instanz füllt die Variablen mit individuellen Werten. Eine Instanz der Klasse Auto könnte sein: Auto schumisRennwagen; mit den Eigenschaften schumisRennwagen.name schumisRennwagen.zulassungsJahr schumisRennwagen.kaufPreis schumisRennwagen.kilometerStand schumisRennwagen.AnzahlUnfaelle = = = = = "Ferrari"; 0; 300000; 2453; 3; Die Methoden „hängen“ an dem Objekt. Der Aufruf erfolgt ähnlich wie der Zugriff auf die Variable mit Objektname - Punktoperator -Methodenname. Ein Unfall mit Totalschaden als Beispiel hätte die Aufrufe schumisRennwagen.neuerUnfall(); schumisRennwagen.setzeRestWert( 0 ); zur Folge. Klassen in OOP sind aber nicht einfach nur Datenstrukturen mit angehängten Funktionen. Eine solche Art der Programmierung lässt sich bereits recht gut mit prozeduralen Sprachen wie C realisieren. Zur Unterscheidung spricht man in diesem Fall von objektbasierter Entwicklung. Wesentlich für OOP ist der Charakter der Klassen als Teil einer Hierarchie von Klassen. Die Klassen-Hierarchie bildet einen Baum vom Allgemeinen zum Speziellen. Klassen übernehmen Eigenschaften und Verhalten von Oberklassen, spezialisieren oder ergänzen sie und geben sie an Unterklassen weiter. Ein Beispiel für eine Klassen-Hierarchie ausgehend von der Klasse Fahrzeug ist: Fahrzeug Motorrad Fahrrad MountainBike Rennrad Auto PKW Cabrio LKW 76 KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG Eine Klasse übernimmt die Eigenschaften und das Verhalten der übergeordneten Klasse und ergänzt sie um spezifische Details. Sprachlich kann man die Beziehung mit „ist ein“ (engl. „is a“) ausdrücken: • ein Cabrio ist ein Pkw • ein Pkw ist ein Auto • ein Auto ist ein Fahrzeug. Die „ist ein“ Beziehung gilt nicht nur für die direkt übergeordnete Klasse, sondern auch für alle anderen übergeordneten Klassen im entsprechenden Zweig: • ein Cabrio ist ein Auto In dieser Hierarchie werden Eigenschaften und Verhalten entlang eines Zweiges vererbt. Im Beispiel kann Cabrio alle Attribute von Pkw übernehmen und um seine speziellen Eigenschaften ergänzen. Beispielsweise kann die Klasse Cabrio eine zusätzliche Komponente zur Angabe des Dachtyps einführen. Diese Eigenschaft spielt bei den anderen Fahrzeug-Klassen keine Rolle. Abhängig von der Sichtweise kann eine Klasse durchaus in verschiedenen Klassenhierarchien eingeordnet werden. So könnte eine Klasse Rennrad sowohl in den obigen Baum für Fahrzeuge als auch in einen Baum für Sportgeräte eingeordnet werden. Je nach Anwendung - Verkehrsplanung oder Sportartikelversand - kann die eine oder die andere Sichtweise dem Problem angemessen sein. Wenn eine Klasse Attribute und Verhalten aus zwei (oder mehr) Oberklassen erbt, spricht man von mehrfacher Vererbung (Ein Rennrad ist ein Fahrrad und ist ein Sportgerät). 8.3 Objektorientierte Programmiersprachen Es gibt eine große Anzahl (>100) von verschiedenen objektbasierten und objektorientierten Programmiersprachen. Weit verbreitet und wohl mit Abstand die Sprachen mit der derzeit größten praktischen Bedeutung sind JAVA und C++. Beide sind mit C verwandt und übernehmen elementare Datentypen, Operatoren, Kontrollstrukturen, u. s. w. von C. JAVA ist die jüngere von beiden Sprachen. In vieler Hinsicht ist JAVA einfacher als C++. Einige komplexe Sprachelemente aus C++ fehlen, sei es weil die Designer von JAVA sie nicht für notwendig oder empfehlenswert erachteten oder weil sie aufgrund des konsequenteren Designs nicht benötigt werden. Gleichzeitig entfernt sich JAVA stärker von C. Die Sprachelemente von C++ sind eine Obermenge der Sprachelemente von C. Ein C++ Compiler akzeptiert auch C Kode. In der Tat benutzen viele Programmierer nur einige wenige C++ Merkmale wie etwa die vereinfache Ausgabe oder Speicherallokation und schreiben Programme 8.4. ÜBUNGEN Tabelle 8.1: Vergleich JAVA und C++ Funktionalität JAVA einfache Vererbung + Mehrfachvererbung (+) automatische Speicherverwaltung + Überladen von Methoden + Überladen von Operatoren Zeiger Prä-Prozessor Strukturen String und boolean als Datentypen + feste Größe der elementaren Datentypen + 77 C++ + + + + + + + - ansonsten im alten C Stil. Demgegenüber ist JAVA nicht abwärtskompatibel. Ein JAVA Übersetzer wird C Kode nicht akzeptieren. Dies bedeutet allerdings nicht notwendigerweise, dass immer ein sehr hoher Portierungsaufwand von C nach JAVA besteht. Die Tabelle 8.1 zeigt wichtige Gemeinsamkeiten und Unterschiede der beiden Programmiersprachen. Eine neuere Entwicklung ist die Sprache C# (C sharp ausgesprochen) der Firma Microsoft. Sie enthält Elemente beider Sprache. Inwieweit sie sich mittelund langfristig gegen die anderen Sprachen durchsetzen wird, ist derzeit noch nicht abzusehen. 8.4 Übungen Übung 8.1 Ihre Firma entwickelt eine Verwaltungs-Software für Garten- und Baumärkte. Betrachten Sie beispielhaft die Artikel: Hammer Erdbeerpflanzen Zange Spaten Torf Batterien Holzleim Schrauben Glühbirnen Elektrokabel Stichsäge 1. Zeichnen Sie einen Klassenbaum mit mindestens drei Ebenen, d. h. der Ausgangsklasse Artikel, mindestens einer Zwischenebene und dann den Klassen für die einzelnen Artikel. Wählen Sie für die Zwischenebene(n) eine sinnvolle Einteilung. 2. Gibt es Artikel, für die mehrere Zuordnungen sinnvoll wären? 3. Nennen Sie ein Beispiel für eine Eigenschaft, die sinnvoll bereits in der Basisklasse Artikel definiert und von dort an alle anderen Klassen vererbt werden kann. 78 KAPITEL 8. OBJEKTORIENTIERTE PROGRAMMIERUNG 4. Definieren Sie für einen der Artikel eine Java-Klasse mit mindestens 4 Eigenschaften (Variablen passenden Typs) und einem Konstruktor mit entsprechenden 4 Parameter. Kapitel 9 Klassen 9.1 Einleitung Klassen sind das wesentliche Element von Java. Selbst einfache Programme wie unser einführendes Beispiel benötigen eine Klasse – auch wenn bei der Programmierung Objektorientierung keinerlei Rolle spielt. In diesem Kapitel wird das Konzept von Klassen im Detail besprochen. Zunächst noch zur Terminologie: Klassen beschreiben die Eigenschaften und das Verhalten von Objekten. So definierten wir im Beispiel der Klasse Auto Eigenschaften wie Kaufpreis oder Kilometerstand sowie Methoden um beispielsweise diese Eigenschaften abzufragen oder zu verändern. Ein konkretes Auto ist dann ein Objekt oder eine Instanz dieser Klasse. Verschiedene Autos unterscheiden sich in ihren Eigenschaften, stellen aber alle die gleichen Methoden zur Verfügung. 9.2 Klassendefinition In unserer ersten Anwendung verwendeten wir die Konstruktion public class Ue1 { ... } um eine Klasse zu definieren. Im einfachsten Fall besteht die Definition aus dem Schlüsselwort class und dem Namen der Klasse. Häufig steht jede Klasse in einer eigenen Datei, die wiederum den gleichen Namen und die Erweiterung .java haben muss. Wir werden später auch Fälle untersuchen, in denen mehrere Klassen in einer Quelldatei stehen. Aber spätestens beim Kompilieren werden die Klassen in individuelle Dateien – dann mit der Endung .class – aufgeteilt. Die Definition der Klasse kann um Attribute (z. B. public zur Sichtbarkeit) erweitert werden. Weiterhin können nach dem Klassennamen Angaben zur Vererbung folgen (public class HalloFbApplet extends JApplet). In dem Block nach 79 80 KAPITEL 9. KLASSEN der Klassendefinition folgen die Daten und Methoden zu dieser Klasse. Für das Beispiel Auto kann man schreiben: public class Auto { String name; int zulassungsJahr; int kaufPreis; int kilometerStand = 0; int anzahlUnfaelle = 0; public void erhoeheKilometerStand( int neueKm ) { kilometerStand += neueKm; } public void print() { System.out.println(name + ":"); System.out.println("Kilometerstand: " + kilometerStand ); } } Mit diesem Code sind 5 Variablen sowie 2 Methoden definiert. Wir haben damit einige Eigenschaften der Auto-Objekte festgelegt. Die Variablen können explizit mit Werten initialisiert werden. Standardmäßig initialisiert Java die Elemente mit dem Wert 0 beziehungsweise dem Äquivalent des jeweiligen Datentyps. Mit der Methode erhoeheKilometerStand() kann eine der Eigenschaften verändert werden. Die Methode print() gibt die Daten eines Autos aus. Die Methoden einer Klasse können deren Variablen ohne weiter Angabe verwenden. Eine Variable ohne explizite Zuordnung wird automatisch der eigenen Klasse zugeordnet. Objekte dieser Klasse können unter Angabe des Klassennamens angelegt werden. Zunächst wird das Objekt deklariert: Auto meinRenault; Mit dieser Anweisung wird die Referenz auf ein Objekt der Klasse Auto angelegt. Es wird noch kein Speicher reserviert. Um tatsächlich auch ein Objekt zu erzeugen, muss der new-Operator angewendet werden: meinRenault = new Auto(); Jetzt ist Speicher reserviert worden und die Referenz deutet auf diesen neuen Speicherblock. Man kann Deklaration und Initialisierung zusammen fassen und verkürzt schreiben: Auto meinRenault = new Auto(); 9.2. KLASSENDEFINITION 81 In beiden Fällen kann danach meinRenault als ein Objekt der Klasse Auto eingesetzt werden. Zugriff auf die Variablen und die Methoden erfolgt in der Syntax meinRenault Punktoperator Variable oder Methode Für Variablen erlaubt diese Syntax sowohl lesenden als auch schreibenden Zugriff. Die Verwendung illustriert folgendes Beispiel: public void testAuto() { Auto meinRenault = new Auto(); meinRenault.kaufPreis = 10000; meinRenault.print(); } Die Klasse Auto kann wie ein Variablentyp benutzt werden. Man kann beliebig viele Objekte dieser Klasse erzeugen. Jedes Objekt hat seinen eigenen Speicherbereich. Die Objekte können zu Feldern zusammen gefasst werden, an Methoden übergeben werden, etc. 9.2.1 Zugriff auf Attribute Die Möglichkeit des direkten Zugriff auf die Variable kaufPreis über die Referenz auf das Objekt in der Art meinRenault.kaufPreis sollte nur in einfachen und sehr überschaubaren Fällen verwendet werden. Besser ist es, in jeder Klasse zu überlegen, welche Eigenschaften von außen sichtbar oder veränderbar sein sollen. Die Klasse wird damit gegen unbeabsichtigte Eingriffe abgesichert. Gleichzeitig wird die Verwendung vereinfacht, da nur noch die dafür notwendigen Informationen sichtbar sind. Dementsprechend wird dieses Prinzip als Kapselung oder Information hiding bezeichnet. In Java kann man Variablen durch den Modifizierer private schützen. Damit ist die Variable außerhalb der Klasse nicht mehr sichtbar. Sofern man Zugriff gewähren will, kann dies über entsprechende Zugriffsmethoden geschehen. In Java ist es üblich, die Methoden mit get oder set beginnen zu lassen und dann den Namen der Variablen anzufügen. Dementsprechende würde man für den Kaufpreis das Methodenpaar public void setKaufPreis( int kaufPreis ) { this.kaufPreis = kaufPreis; } public int getKaufPreis() { return kaufPreis; } einführen. Viele Tools unterstützen diese Namenskonvention. Die Methoden werden dann als Getter und Setter bezeichnet. 82 KAPITEL 9. KLASSEN Das Beispiel zeigt weiterhin eine allgemein üblich Form der Namensgebung für Parameter. Der Parameter kilometerStand trägt den gleichen Namen wie die Instanzvariable. Die lokale Variable in der Methode überdeckt dann die Instanzvariable. Über die Referenz this, die auf das Objekt selbst verweist, kann wiederum auf die Instanzvariable zugegriffen werden. Diese Schreibweise zeigt deutlich, dass eine Initialisierung ausgeführt wird und man braucht sich keine zusätzlichen Namen für die Parameter auszudenken. 9.3 Instanz- und Klassenvariablen und Methoden Wenn man Methoden wie oben beschrieben definiert, dann können sie nur über ein Objekt der Klasse angesprochen werden. Es gibt keine Möglichkeit, die Methode print() der Klasse Auto aufzurufen, ohne vorher ein Objekt erzeugt zu haben. Erst dann ist die Methode über die Referenz auf das Objekt zugänglich (und sinnvoll). In vielen Fällen haben Methoden jedoch keinen Bezug zu einer Instanz. Dies gilt insbesondere für die funktionale Verwendung. Ein typisches Beispiel sind die mathematischen Funktionen wie sin() oder cos(), wo die Form sin(x) die Bedeutung gut verdeutlicht. Java ermöglicht es daher auch, Methoden für eine Klasse unabhängig von einer konkreten Instanz zu definieren. Entsprechend nennt man derartige Methoden Klassenmethoden im Gegensatz zu den Instanzmethoden. Klassenmethoden werden durch das Attribut static gekennzeichnet. Für die Klasse Auto führen wir eine Klassenmethode printVersion ein, die die aktuelle Versionsnummer der Klasse ausgibt: class Auto { static void printVersion() { System.out.println("Klasse Auto, Version 1.0"); } ... public static void main(String[] args) { Auto.printVersion(); Die Klassenmethoden werden in Verbindung mit dem Klassennamen aufgerufen. Es ist nicht notwendig, vorher eine Instanz der Klasse anzulegen. Entsprechend werden die Methoden der Klasse Math in der Form Math.sin(x) benutzt. Neben Klassenmethoden gibt es auch Klassenvariablen, d. h. Variablen die unabhängig von Instanzen existieren. Sie werden nur einmal angelegt und alle Instanzen haben nur einen gemeinsamen Satz von Klassenvariablen. Sie bestehen während der gesamten Laufzeit eines Programms. In ihrer Anwendung sind sie mit globalen Variablen in anderen Programmiersprachen vergleichbar. Klassenvariablen sind sinnvoll, um übergeordnete Eigenschaften einer Klasse darzustellen. In der Auto Klasse kann man so z. B. einen Zähler für die Anzahl der Fahrzeuge anlegen: 9.3. INSTANZ- UND KLASSENVARIABLEN UND METHODEN 83 Klasse Klassenvariablen Klassenmethoden Objekt Objekt Variablen Objekt Variablen Methoden Variablen Methoden Methoden Abbildung 9.1: Objekt- und Klassenvariablen und Methoden class Auto { ... static int anzahlAutos = 0; ... public static void main(String[] args) { Auto meinRenault = new Auto(); ++Auto.anzahlAutos; Eine andere Anwendung sind Konstanten. Eine nützliche Konstante für die Auto Klasse könnte die TÜV-Periode sein. Eine entsprechende Konstante wird wie eine normale (Klassen-) Variable definiert und erhält zusätzlich das Attribut final um zu zeigen, dass der Wert nicht mehr geändert werden kann. static final int TÜV_PERIODE = 2; Die beiden Konstanten in der Klasse Math sind Math.PI und Math.E. Der Zusammenhang zwischen Methoden und Variablen einer Klasse und ihrer Instanzen ist in Bild 9.1 graphisch dargestellt. Hinweis: Klassenmethoden und Klassenvariablen können auch über den Namen einer Instanz angesprochen werden. Da dies aber nicht dem logischen Zusammenhang entspricht, sollte diese Form nicht ohne gute Gründe verwendet werden. 84 KAPITEL 9. KLASSEN 9.4 Konstruktoren Der Erzeugungsoperator new legt ein neues Objekt der entsprechenden Klasse an. Damit wird Speicher reserviert und die Elemente werden initialisiert. Außerdem wird der Konstruktor der übergeordneten Klasse aufgerufen. Weitergehende Initialisierungen kann man mit speziellen Methoden - den Konstruktoren - ausführen. Konstruktoren haben die Form von normalen Methoden, wobei der Klassenname als Methodenname übernommen wird. Wenn kein eigener Konstruktor angegeben ist, führt Java einen parameterlosen Standard-Konstruktor aus. Wir erweitern die Klasse Auto um einen Konstruktor, der den Namen des Autos als Parameter erhält: Auto( String text) { name = text; } Der Konstruktor hat keinen Rückgabewert - auch nicht void. Mit dem neuen Konstruktor können Auto-Objekte mit Angabe des Namens in der Form Auto meinBMW = new Auto( "BMW"); erzeugt werden. Eine Klasse kann mehrere Konstruktoren haben, die sich dann in der Parameterliste unterscheiden müssen. Sehr gebräuchlich sind verschiedene Varianten um mehr oder weniger viele der Elemente zu initialisieren. In der Beispielklasse wären dies Konstruktoren in der Art Auto( String text) { ... } Auto( String text, int kaufPreis) { ... } Auto( String text, int kaufPreis, int kilometerStand) { ... } Um nicht den gleichen Code mehrfach schreiben zu müssen, ist es sinnvoll die Konstruktoren zu verketten. Konstruktoren können sich gegenseitig aufrufen. Der Aufruf muss dabei jeweils am Anfang der Methode stehen. Der Aufruf erfolgt über den speziellen Namen this: Auto( String text, int kaufPreis, int kilometerStand) { // anderen Konstruktor aufrufen this(text, kaufPreis); // Instanzvariable initialisieren this.kilometerStand = kilometerStand; } Hier ruft der speziellere Konstruktor zunächst einen einfacheren Konstruktor auf und führt dann seine eigene Initialisierung aus. In dieser Art und Weise kann man speziellere Konstruktoren auf einfacheren aufbauen und vermeidet ein mehrfaches Schreiben (und später mehrfaches Ändern) von identischem Code. 9.5. DESTRUKTOREN 85 Sobald in einer Klasse irgendein eigener Konstruktor definiert wird, muss man auch den parameterlosen Konstruktor implementieren. Im einfachsten Fall kann dieser Konstruktor leer sein: Auto() { } Man kann allerdings dort auch Anweisungen eintragen, die immer ausgeführt werden sollen: Auto() { ++anzahlAutos; } Wenn dann jeder andere Konstruktor diesen parameterlosen Konstruktor entweder direkt als this() oder indirekt über einen anderen Konstruktor aufruft, stellt anzahlAutos einen zuverlässigen Zähler für die Anzahl der angelegten Objekte dar. 9.5 Destruktoren Das Gegenstück zu den Konstruktoren sind die Destruktoren. Sie werden aufgerufen, wenn ein Objekt nicht mehr benötigt wird. Beispielsweise werden lokale Objekte am Ende der Methode wieder gelöscht, sofern nicht die Methode eine Referenz auf dieses Objekt zurück gibt. Am Ende der Lebensdauer eines Objektes sollten die gebundenen Ressourcen - in erster Linie der Speicher - wieder frei gegeben werden. In C und C++ dient dazu die Funktion free. Bei großen Anwendungen ist es sehr wichtig, dass der Speicher wieder vollständig freigegeben wird. Ansonsten sammelt sich im Laufe der Zeit unbenutzter und unbrauchbarer Speicher an. Handelt es sich um größere Speichermengen oder läuft das Programm sehr lange, kann daraus eine Belastung für das gesamte System werden. In Java spielen Destruktoren keine große Rolle. In Java ist eine automatische Speicherverwaltung integriert. Sobald Java feststellt, dass ein Objekt nicht mehr benutzt wird, wird dieses Objekt zum Löschen markiert. Ein spezieller Mechanismus – garbage collection (engl. Müllsammlung) – sorgt dann dafür, dass der Speicher wieder frei gegeben wird. Dabei wird auch ein Destruktor – die Methode void finalize( void ) – aufgerufen. Dadurch braucht sich der Anwender in den allermeisten Fällen nicht um die Speicherverwaltung zu kümmern. Nicht mehr benötigte Objekte werden durch den im Hintergrund tätigen garbage collector weggeräumt. Allerdings gibt es keine Garantien, wie schnell der garbage collector arbeitet oder ob er überhaupt aktiv ist. Man sollte sich daher bei speicherkritischen Anwendungen nicht auf den Automatismus verlassen, sondern den Speicher gezielt entfernen lassen. 86 KAPITEL 9. KLASSEN Klassen Methoden Variablen Konstanten 9.6 Tabelle 9.1: Namenskonventionen in Java Regeln Beispiele Klassennamen sollten aus Hauptwörtern Auto, Student, gebildet werden, der erste Buchstabe Image, ImageView ist groß geschrieben. Namensbestandteile sind durch Großschreibung getrennt (mixed case). Um die Aktivität zu verdeutlichen soll- print, erhöheKm, ten Methodennamen aus Verben beste- Ausnahmen: sin, hen. Der erste Buchstabe ist klein ge- cos schrieben, ansonsten gilt wieder „mixed case“. Variablennamen sollten kurz und präg- kaufPreis, nant sein und mit Kleinbuchstaben begin- restWert nen. Für Variablen mit sehr kurzer Lebensdauer können die einfachen Namen wie i, j, x benutzt werden. Nur Großbuchstaben, getrennt durch Un- TÜV_PERIODE terstreichestrich Namenskonventionen Im Prinzip können die Namen für Klassen, Methoden, Variablen, etc. frei gewählt werden. Die Lesbarkeit kann allerdings durch Verwendung einer festen Konvention stark verbessert werden. Beispielsweise kann man für jeden Typ von Bezeichner eine eindeutige Schreibweise vereinbaren. In Tabelle 9.1 sind die wichtigsten Regeln gemäß den Vorschlägen von Sun zusammen gestellt. 9.7 Übungen Übung 9.1 Klasse Auto: Entwickeln Sie nach den Vorlagen im Skript eine Klasse Auto. Welche Variablen und Methoden benötigen Sie? Implementieren Sie die Klasse und testen Sie verschiedene Konstruktoren und andere Methoden. Einige Vorschläge für Methoden: • berechneKmProJahr • berechneZeitwert • zeigeZeitwertÜberJahre Übung 9.2 FH-Verwaltung: Sie wollen ein Programm zur Verwaltung der FH entwickeln. Entwerfen Sie eine 9.7. ÜBUNGEN 87 Klassenhierarchie für die verschiedenen Angehörigen: • Studenten • Professoren • Mitarbeiter Implementieren Sie zwei Klassen: 1. Student 2. FH In der Klasse Student sollen passende Variablen für Eigenschaften wie Name, Studienfach und Semsterzahl enthalten sein. Weiterhin soll es mindestens eine Methode print() geben, um die Eigenschaften einer Instanz der Klasse auszugeben. Verwenden Sie dann die Klasse FH um einige Objekte der Klasse Student zu erzeugen, mit Werten zu belegen und dann über die Methode print() auszugeben. 88 KAPITEL 9. KLASSEN Kapitel 10 Objekte und Klassen 10.1 Einleitung In diesem Kapitel wird die Verwendung von Objekten und Klassen anhand von konkreten Beispielen näher untersucht. Insbesondere wird dabei auf Fragen der Vererbung eingegangen. Als Beispiel dient dabei eine Autovermietung oder allgemeiner eine Vermietung von Fahrzeugen. Der Schwerpunkt liegt allerdings auf der Darstellung der Sprachelemente, weniger auf dem tatsächlichen Nutzen der Klassen. Als Entwicklungsumgebung dient BlueJ. 10.2 Basisklassen Wir beginnen die Entwicklung mit zwei Klassen: • Fahrzeug – die Basisklasse für verschiedene Typen von Fahrzeugen • Vermietung – die Klasse für die Modellierung der Vermietung Nach dem Anlegen hat die Klasse Fahrzeug folgendes Aussehen: /** * Klasse fuer Fahrzeuge * * @author Euler * @version 0.1 Nov. 2008 */ public class Fahrzeug { // Definieren Sie ab hier die Klassenvariablen für Fahrzeug // Definieren Sie ab hier die Objektvariablen für Fahrzeug 89 90 KAPITEL 10. OBJEKTE UND KLASSEN // Definieren Sie ab hier die KOnstruktoren für Fahrzeug /** * Konstruktor für Objekte der Klasse Fahrzeug */ public Fahrzeug() { // Objektvariable initialisieren } } Die Klasse wird direkt von der allgemeinen Basisklasse Object abgeleitet. Explizit könnte man schreiben public class Fahrzeug extends Object Mit dem Schlüsselwort extends wird die Klasse angegeben, die erweitert wird. Fehlt die Angabe, wird automatisch Object als Oberklasse genommen. Die neue Klasse erbt damit alle Variablen und Methoden von Object. Ein Blick in die Dokumentation zeigt, dass Object keine Variablen aber einige Methoden hat. So gibt die Methode toString() einen beschreibenden Text zu dem jeweiligen Objekt zurück. Bereits jetzt können wir ein Fahrzeug-Objekt erzeugen und diese Methoden aufrufen. Ohne BlueJ würde man für solche einfachen Tests die Methode main verwenden. Eigentlich benötigen wir diese Methode nicht, da die Verwaltung später in der Klasse Vermietung implementiert wird. Aber in Java kann jede Klasse eine eigene Methode main haben. Durch den Aufruf java Klassenname wird die main Methode der genannten Klasse aufgerufen. Es stört nicht, wenn andere Klassen in dem Projekt ebenfalls eine main Methode enthalten. Wir verwenden in BlueJ statt main die Methode testeFahrzeug() als Startpunkt. In dem folgende Code wird • ein Fahrzeug-Objekt angelegt • die Methode toString benutzt, um die Textrepräsentation auszugeben • mit der Methode getClass ein zugehöriges Klassenobjekt erzeugt und dessen Methode getName zur Ausgabe des Klassennamens aufgerufen public void testeFahrzeug() { Fahrzeug test = new Fahrzeug(); System.out.println("test: " + test.toString() ); System.out.println("test ist ein Objekt der Klasse <" + test.getClass().getName() + ">"); } 10.3. KLASSE FAHRZEUG 91 Der Aufruf der Methode liefert folgende Ausgabe: test: Fahrzeug@162e295 test ist ein Objekt der Klasse <Fahrzeug> Der Wert hinter dem @-Zeichen ist eine eindeutige Kennung für das Objekt, der so genannte Hash-Code. Erzeugt man ein weiteres Objekt, wird dieses einen anderen Wert haben. 10.3 Klasse Fahrzeug Ausgehend von dem Grundgerüst können erste Erweiterungen an der Klasse Fahrzeug vorgenommen werden. Sinnvoll ist es, übergreifende Variablen und Methoden – d. h. solche die für alle Fahrzeuge und die daraus abgeleiteten Objekte gelten – hier zu definieren. Ohne spezielle Attribute erbt jede abgeleitete Klasse alle diese Variablen und Methoden. Im Beispiel sind dies • drei Variablen name, kaufPreis, kaufJahr • ein Konstruktor bei dem diese drei Variablen durch Parameter gesetzt werden • eine Methode print zur Ausgabe Die Erweiterungen werden in teste() ausprobiert. Dort werden zwei Objekte erzeugt: eines mit dem parameterlosen Konstruktor und das zweite mit dem neuen Konstruktor. Anschließend werden beide Objekte ausgegeben. public class Fahrzeug extends Object { // Definieren Sie ab hier die Klassenvariablen für Fahrzeug // Definieren Sie ab hier die Objektvariablen für Fahrzeug String name; int kaufPreis; int kaufJahr; // Definieren Sie ab public Fahrzeug() public Fahrzeug( String name, this.name = this.kaufPreis = this.kaufJahr = } hier die KOnstruktoren für Fahrzeug { } int kaufPreis, int kaufJahr ) { name; kaufPreis; kaufJahr; 92 KAPITEL 10. OBJEKTE UND KLASSEN void print() { System.out.println(name + ":"); System.out.print("gekauft im Jahr " + kaufJahr ); System.out.print(" für " + kaufPreis + " Euro" ); System.out.println(); } public void testeFahrzeug() { Fahrzeug test = new Fahrzeug(); System.out.println("test: " + test.toString() ); System.out.println("test ist ein Objekt der Klasse <" + test.getClass().getName() + ">"); } public void teste() { Fahrzeug test1, test2; test1 = new Fahrzeug( ); test2 = new Fahrzeug( "Alpha", 24999, 2000); System.out.println("test1: "); test1.print(); System.out.println("test2: "); test2.print(); } } Ausgabe: test1: null: gekauft im Jahr 0 für 0 Euro test2: Alpha: gekauft im Jahr 2000 für 24999 Euro In dem parameterlosen Konstruktor werden die Variablen auf den Wert 0 bzw. null gesetzt. 10.4 static Elemente Neben den Instanzvariablen und –methoden können auch entsprechende Elemente für die Klasse definiert wird. Dem Beispiel aus dem Kapitel Klassen folgend, fügen wir einen Zähler für die Anzahl der Fahrzeug-Objekte ein: 10.4. STATIC ELEMENTE 93 /** * Klasse fuer Fahrzeuge * * @author Euler * @version 0.1 Nov. 2008 */ public class Fahrzeug extends Object { // Definieren Sie ab hier die Klassenvariablen für Fahrzeug static int anzahl; // Definieren Sie ab hier die Objektvariablen für Fahrzeug String name; int kaufPreis; int kaufJahr; // Definieren Sie ab public Fahrzeug() ++anzahl; } public Fahrzeug( String name, this(); this.name = this.kaufPreis = this.kaufJahr = } hier die KOnstruktoren für Fahrzeug { int kaufPreis, int kaufJahr ) { name; kaufPreis; kaufJahr; public static void printAnzahl() { System.out.println("Anzahl Fahrzeuge: " + anzahl); } void print() { System.out.println(name + ":"); System.out.print("gekauft im Jahr " + kaufJahr ); System.out.print(" für " + kaufPreis + " Euro" ); System.out.println(); } public void testeFahrzeug() { Fahrzeug test = new Fahrzeug(); System.out.println("test: " + test.toString() ); System.out.println("test ist ein Objekt der Klasse <" 94 KAPITEL 10. OBJEKTE UND KLASSEN + test.getClass().getName() + ">"); } public void teste() { Fahrzeug test1, test2; test1 = new Fahrzeug( ); test2 = new Fahrzeug( "Alpha", 24999, 2000); System.out.println("test1: "); test1.print(); System.out.println("test2: "); test2.print(); printAnzahl(); } } Dazu werden folgende Erweiterungen eingebaut: • eine static Variable für den Zähler • eine static Methode um den Zähler auszugeben • im parameterlosen Konstruktor die Anweisung, um den Zähler zu erhöhen • im zweiten Konstruktor der Aufruf des ersten Konstruktors Man könnte auch im zweiten Konstruktor direkt den Zähler inkrementieren. Statt dessen wird der erste Konstruktor aufgerufen, in dem der Zähler erhöht wird. Die gewählte Lösung hat den Vorteil, dass nur an einer Stelle Kode für die Erhöhung des Zähler eingetragen wird. Bei eventuellen späteren Änderungen braucht daher nur eine einzige Stelle bearbeitet zu werden. 10.5 10.5.1 Konstruktoren und die Klasse Auto Die Klasse Auto Als erste abgeleitete Klasse betrachten wir eine Klasse Auto. Die Klasse wird aus Fahrzeug abgeleitet (Auto ist ein Fahrzeug). Sie erbt damit Eigenschaften und Verhalten. Spezifische Eigenschaften eines Autos, die nicht jedes Fahrzeug hat, werden dann in diese Klasse eingebaut. In BlueJ wird eine neue Klasse Auto angelegt. Dann kann man durch einen durchgezogenen Pfeil → die Vererbungsbeziehung einfügen. Dann wird die Klassendefinition zu public class Auto extends Fahrzeug { ... 10.5. KONSTRUKTOREN UND DIE KLASSE AUTO Abbildung 10.1: Klasse Auto als Spezialisierung von Fahrzeug 95 96 KAPITEL 10. OBJEKTE UND KLASSEN Die Klasse selbst steht in einer eigenen Datei Auto.java. Bild 10.1 zeigt die Darstellung in BlueJ. Als zusätzliche Eigenschaft wird der Kilometerstand eingeführt: public class Auto extends Fahrzeug { int kilometer; ... 10.5.2 Verkettung von Konstruktoren Die abgeleitete Klasse erbt alle Methoden außer den Konstruktoren. In Java werden Konstruktoren nicht vererbt. Vielmehr wird durch Verkettung der Konstruktoren garantiert, dass auch die Konstruktoren der Elternklasse aufgerufen werden. Dies kann entweder explizit durch eine entsprechende Anweisung erfolgen oder wird implizit durch den Compiler eingefügt. Ein expliziter Aufruf erfolgt über die Methode super als erste Anweisung in einem Konstruktor. Dieser Aufruf führt den Konstruktor der Elternklasse aus. Für den Konstruktor mit drei Parametern kann man schreiben public Auto(String name, int kaufPreis, int kaufJahr) { super( name, kaufPreis, kaufJahr ); } Der Konstruktor enthält vorerst nur den Aufruf des Konstruktors der Klasse Fahrzeug. Fehlt ein solcher Aufruf, so setzt der Compiler automatisch den Aufruf des parameterlosen Konstruktors super() ein. Damit lässt sich schreiben: public class Auto extends Fahrzeug { int kilometer; /** Creates new Auto */ public Auto() { } public Auto(String name, int kaufPreis, int kaufJahr) { super( name, kaufPreis, kaufJahr ); } } In dieser Form werden bei beiden Konstruktoren die entsprechenden Konstruktoren der Elternklasse aufgerufen -– einmal implizit und einmal explizit. Diese Verkettung setzt sich weiter fort. Auch die Konstruktoren in Fahrzeug rufen die entsprechenden Konstruktoren der Elternklasse – in diesem Fall Object – auf. Zusätzlich können wie gesehen auch Konstruktoren untereinander verkettet sein. Im Beispiel läuft beim Aufruf 10.6. DIE KLASSE VERLEIH 97 Auto auto2 = new Auto( "Golf", 19999, 2002); folgende Kette von Konstruktoren ab: super this super Auto( String, int, int) Fahrzeug( String, int, int) Fahrzeug() Object() Die Kette wird von „oben nach unten“ abgearbeitet. Der als letztes aufgerufene Konstruktor Object() wird als erstes ausgeführt. Danach folgt Fahrzeug() und so weiter. Ausgehend vom allgemeinen erfolgt eine zunehmende Verfeinerung oder Spezialisierung. Nach dem gleichen Muster kann man einen weiteren Konstruktor, der auch die zusätzliche Variable kilometer beinhaltet, einführen: public Auto(String name, int kaufPreis, int kaufJahr, int kilometer) { super( name, kaufPreis, kaufJahr ); this.kilometer = kilometer; } Übung 10.1 Ist der folgende Code korrekt und wenn ja, welche Ausgabe resultiert? Fahrzeug f = new Fahrzeug( "Golf", 19999, 2002); Auto a = new Auto( "Sharan", 30000, 1999); Auto.printAnzahl(); 10.6 Die Klasse Verleih Im nächsten Schritt werden in der Klasse Verleih einige Basisfunktionalitäten eingebaut. Dazu werden • ein Feld von Autos • eine Methode erzeugeAutos(), um eine Liste von Autos zu erzeugen • eine Methode print() zur Anzeige der Autos eingefügt. public class Vermietung { // Definieren Sie ab hier die Objektvariablen für Vermietung Auto[] autos; // Definieren Sie ab hier die Konstruktoren für Vermietung 98 KAPITEL 10. OBJEKTE UND KLASSEN public Vermietung() { erzeugeAutos(); } void erzeugeAutos(){ autos = new Auto[3]; autos[0] = new Auto("Sharan", autos[1] = new Auto("Golf", autos[2] = new Auto("Jaguar", } 29999, 2000, 45000); 19999, 2001, 36777); 55000, 1998, 15666); public void print() { for( Auto auto : autos ) { auto.print(); } } } Erzeugt man ein neues Vermietung-Objekt und führt dessen Methode print() aus, so erhält man die Ausgabe Sharan: gekauft im Jahr 2000 für 29999 Euro Golf: gekauft im Jahr 2001 für 19999 Euro Jaguar: gekauft im Jahr 1998 für 55000 Euro 10.7 Überlagerung von Methoden Da bisher nur die Methode print aus Fahrzeug benutzt wird, fehlt noch die Ausgabe des Kilometerstands. Daher ist es notwendig, in Auto die Methode print zu überlagern. Dies wird durch die Definition einer Methode des gleichen Namens in Auto erreicht: void print() { System.out.println(name + ":"); System.out.print("gekauft im Jahr " + kaufJahr ); System.out.print(" für " + kaufPreis + " Euro" ); System.out.println(); System.out.println("Kilometerstand: " + kilometer); } 10.7. ÜBERLAGERUNG VON METHODEN 99 Wenn dann bei einem Auto-Objekt die Methode print aufgerufen wird, wählt der Compiler die Methode aus der eigenen Klasse. Nur wenn in dieser Klasse die Methode nicht implementiert ist, sucht der Compiler in den Elternklassen nach einer Methode mit diesem Namen. Die Suche verläuft von unten nach oben und die zuerst gefundene Methode wird verwendet. Die allgemeineren Methoden sind dabei durch die spezielleren Methoden verdeckt. Im vorliegenden Beispiel wird viel Code aus der übergeordneten Methode dupliziert. Besser ist es – ähnlich wie bei den Konstruktoren – vorhandenen Code in die neue Methode einzubinden. Dazu kann über die Konstruktion super.methode() auf die verdeckte Methode zugegriffen werden. Aus dem Beispiel wird dann void print() { super.print(); // Methode print aus Elternklasse ausführen System.out.println("Kilometerstand: " + kilometer); } 10.7.1 Dynamische Suche Zwischen den abgeleiteten Klassen und den Elternklassen besteht eine ist ein Beziehung. In unserem Beispiel gilt ein Auto ist ein Fahrzeug. Daher kann ein Objekt einer abgeleiteten Klasse -– das ja alle Eigenschaften der Elternklasse aufweist -– die Rolle eines Objektes der Elternklasse annehmen. Somit kann ein Auto-Objekt einem Fahrzeug-Ojekt zugewiesen werden: Auto a = new Auto( "Golf", 19999, 2002); Fahrzeug f2 = a; Die Variablen einer abgeleiteten Klasse sind zuweisungskompatibel zu den Variablen der Elternklasse. Die umgekehrte Richtung ist nicht möglich. Der Versuch ergibt die Fehlermeldung: Verleih.java [55:1] Incompatible type for =. Can’t convert Fahrzeug to Auto. a = f2; ^ Ein Fahrzeug kann nicht die Rolle eines Autos übernehmen. Wenn nun eine Variable für Fahrzeuge auch ein Auto oder irgendein anderes Objekt einer abgeleiten Klasse enthalten kann, stellt sich die Frage, welche Methode ausgewählt wird. Wird bei f2.print(); die Methode der Variablen (Klasse Fahrzeug) oder die gleichnamige Methode des Objekts (Klasse Auto) verwendet? Die Java-Maschine interpretiert die Methodenaufruf zur Laufzeit. Bei dem Aufruf untersucht sie das Objekt und wählt die am 100 KAPITEL 10. OBJEKTE UND KLASSEN besten passende Methode aus. Man spricht dann von dynamischem oder spätem Binden. In dem Beispiel stellt der Interpreter fest, dass es sich um ein AutoObjekt handelt und ruft die Methode aus der Klasse Auto auf. Dieses durchaus wünschenswerte Verhalten bedeutet allerdings einen gewissen Mehraufwand während der Ausführung. Bei jeder Variablen muss geprüft werden, welches Objekt aus dem entsprechenden Vererbungsbaum aktuell referenziert wird. Aus diesem Grund wurde die Klasse String als final deklariert. Damit ist klar, dass es keine abgeleiteten Klassen gibt und die Suche entfällt. Bei Programmiersprachen ohne Interpreter ist diese Methodenauswahl ein Problem. In der Regel wird die Methode dort bereits zum Zeitpunkt des Compilerens festgelegt (statisches oder frühes Binden). Das dynamische Binden wird dann durch spezielle Konstruktionen angefordert. In C++ dient dazu das Schlüsselwort virtual. Die Tatsache, dass eine Variable Objekte unterschiedlicher Klassen enthalten kann, wird als Polymorphismus bezeichnet. Polymorphismus ist ein wesentliches Gestaltungselement der objekt-orientierten Entwicklung. Der große Vorteil liegt in der gemeinsamen Behandlung zusammen gehörender Objekte verschiedenen Typs. So kann in unserem Beispiel eine Variable vom Typ Fahrzeug Objekte aller abgeleiteten Klassen wie Auto, Fahrrad, Flugzeug, etc. aufnehmen. Ruft man eine Methode der Klassen wie z.B print auf, so wird automatisch die zu dem speziellen Objekt passende Variante ausgewählt. Im folgenden Code-Abschnitt wird diese Technik verwendet: Fahrzeug[] fahrzeuge = new Fahrzeug[3]; fahrzeuge[0] = new Auto( "BMW" ); fahrzeuge[1] = new Fahrrad( "Bianci" ); fahrzeuge[2] = new Flugzeug( "Airbus" ); for( int i=0; i<fahrzeuge.length; i++ ) { fahrzeuge[i].print(); } In der Schleife wird automatisch für jedes Objekt die passende Methode print() ausgewählt. Wenn in der jeweiligen Klasse eine 10.8 Attribute Die Sichtbarkeit, Lebensdauer und Ableitbarkeit von Klassen, Methoden und Variablen kann durch verschiedene Attribute bei der Definition festlegen. Die wichtigsten sind: 10.8. ATTRIBUTE public private protected static final abstract 101 Die weitestgehende Festlegung. Variablen, Methoden und Klassen sind überall sichtbar. Nur die aktuelle Klasse sieht die Variablen oder Methoden. Abgeleitete Klassen erben sie nicht. Abgeleitete Klassen und Klassen im gleichen Paket sehen die Variablen oder Methoden. Definition von Klassenvariablen und -methoden Klassen mit dem Attribut final können nicht abgeleitet, Methoden nicht überlagert und Variablen nicht verändert werden. Abstrakte Methoden enthalten keinen Rumpf und können nicht selbst aufgerufen werden (Syntaxbeispiel: abstract void test();). Sie dienen lediglich als Vorlagen für die Methoden in den abgeleiteten Klassen. Eine Klasse mit abstrakten Methoden ist selbst abstrakt und kann nicht instanziert werden. Verschiedene Attribute können miteinander kombiniert werden. Abstrakte Klassen dienen als Muster. Sie können wie andere Klassen abgeleitet werden. Wir könnten beispielsweise die Klasse Fahrzeug als abstract deklarieren. Damit wäre sichergestellt, dass keine Objekte aus dieser Klasse erzeugt werden. Bei dem Versuch resultiert die Fehlermeldung Verleih.java [44:1] Cannot use operator new for this type Fahrzeug f = new Fahrzeug( "Golf", 19999, 2002); ^ Die Objekte aus den abgeleiteten, konkreten Klassen könnten wie gehabt verwendet werden. Abstrakte Klassen können sowohl konkrete als auch abstrakte Elemente enthalten. Um aus einer abstrakten Klasse eine konkrete Klasse abzuleiten, müssen alle abstrakten Elemente konkretisiert werden. Es ist möglich, bei der Ableitung nur einen Teil der Elemente zu konkretisieren. Die resultierende Klasse ist dann aber auch wieder abstrakt. Übung 10.2 Wo ist aus Sicht einer abgeleiteten Klasse der Unterschied zwischen final und private Methoden in der Elternklasse? Übung 10.3 Welche Konstruktionen sind legal: • public static final int TÜV_PERIODE = 2; • abstract private void eingeben(); • abstract protected void ausgeben(); 102 KAPITEL 10. OBJEKTE UND KLASSEN Übung 10.4 Leiten Sie aus Fahrzeug eine Klasse Fahrrad ab. Neue Eigenschaften sollen sein: • Rahmenhöhe • Herren– oder Damenrad 10.9 10.9.1 Mehrfachvererbung und Interfaces Einleitung Nicht immer lassen sich die Klasse in einen einzigen Vererbungsbaum einordnen. In unserem Beispiel basiert der Vererbungsbaum auf der Interpretation der Klassen als Fahrzeuge. Aus einem anderen Blickwinkel könnte aber beispielsweise ein Mountainbike auch als Sportgerät gesehen werden. Ein Mountainbike könnte daher sowohl in dem Baum Fahrzeug als auch in dem Baum Sportgerät sein. Man spricht dann von Mehrfachvererbung. Der Nutzen der Mehrfachvererbung ist nicht unumstritten. Sicherlich gibt es Anwendungsfälle, die durch eine hierarchische Baumstruktur nicht vollständig beschrieben werden können. Andererseits wirft die Mehrfachvererbung eine Reihe von Problemen auf. Beispielsweise gilt es dann, Namenskonflikt in den beiden (oder mehreren) Oberklassen zu vermeiden. Die Entwickler von Java haben sich für einen Mittelweg entschieden. Es gibt nur eine einfache Vererbung, aber über so genannte Interfaces (Schnittstellen) existiert ergänzend zu dem Vererbungsbaum eine zweite Vererbungsmöglichkeit. Interfaces definieren ein bestimmtes Verhalten oder bestimmte Eigenschaften von Klassen. Man kann sie auch als eine Art von Protokoll interpretieren. Die Vererbungshierarchie spielt dabei eine weniger große Rolle. So werden wir in einem der Beispiele sehen, wie durch ein Interface die Eigenschaft Vergleichbar implementiert wird. Diese Eigenschaft kann von ganz unterschiedliche Klassen erfüllt werden. Auch wenn mehrere Klassen diese Eigenschaft haben, begründet dies nicht notwendigerweise eine Verwandtschaftsbeziehung untereinander. Während die Ableitung eine ist ein Beziehung begründet, kann man bei implementierten Interfaces von einer verhält sich wie oder hat Fähigkeiten von Beziehung sprechen. Methoden und Konstanten werden in einem Interface wie in einer normalen Klasse definiert. Ein Interface ist allerdings stets abstrakt. Es können demnach keine Instanzen erzeugt werden. Eine Klasse kann ein Interface implementieren, indem sie alle abstrakten Methoden realisiert. Im Unterschied zur Ableitung bei Klassen spricht man bei Interfaces von Implementierung. Eine Klasse kann mehrere Interfaces implementieren, in dem sie die Methoden konkretisiert. Mit jeder Implementierung verpflichtet sich die Klasse zu dem durch das jeweilige Interface festgelegte Verhalten. 10.10. BEISPIEL FH-VERWALTUNG 10.10 103 Beispiel FH-Verwaltung Für die Verwaltung unserer FH können wir folgenden Klassenbaum verwenden: Person Student Professor Mitarbeiter Tutor Ausgangspunkt ist die allgemeine Klasse Person. Eine einfache Realisierung ist: public class Person extends java.lang.Object { String name; /** Creates new Person */ public Person() { } public Person(String name) { this.name = name; } } Als einzige Eigenschaft ist ein Name realisiert. Davon abgeleitet ist die Klasse Student mit einer zusätzlichen Variablen für die Matrikelnummer. public class Student extends Person { int matrikel; public Student() { } public Student( String name ) { super( name ); } public Student( String name, int matrikel ) { super( name ); this.matrikel = matrikel; } } Interessant in Bezug auf Mehrfachvererbung sind die beiden Klassen Tutor und Professor. Die beiden Klassen befinden sich in unterschiedlichen Zweigen. Trotzdem haben sie eine Gemeinsamkeit: sie können Veranstaltungen übernehmen. Beide können in diesem Sinn als Lehrkraft angesehen werden. Dazu gehört, dass sie Vorlesungen übernehmen. Dieser Zusammenhang kann durch ein entsprechendes Interface realisiert werden. Dazu definieren wir ein Interface wie folgt: 104 KAPITEL 10. OBJEKTE UND KLASSEN // Lehrkraft.java public interface Lehrkraft { public void übernehmeVorlesung(String name, int stunden); } Die einzige Methode dient dazu, eine Vorlesung mit gegebenem Namen und einer Anzahl von Stunden zu übernehmen. Jede Klasse, die die Rolle einer Lehrkraft übernehmen will, verpflichtet sich dann, diese Methode zu implementieren. Betrachten wir zunächst die Klasse Professor: public class Professor extends Person implements Lehrkraft { private int SWS = 0; public Professor() {} public Professor(String name) { super( name ); } public void übernehmeVorlesung(String vorlesung, int stunden) { System.out.println( "Prof. " + name + " übernimmt " + vorlesung); SWS += stunden; System.out.println( "SWS: " + SWS); } } Die Klasse wird von Person abgeleitet und implementiert das Verhalten Lehrkraft. Neu ist die Variable SWS für die Anzahl der Semesterwochenstunden. Die im Interface vorgegebene Methode wird passend realisiert. Die Klasse Tutor hat folgende Form: public class Tutor extends Student implements Lehrkraft { float vergütung = 0; public Tutor() { } public Tutor( String name ) { super( name ); } public void übernehmeVorlesung(String vorlesung, int stunden) { System.out.println( "Tutor " + name + " übernimmt " + vorlesung); vergütung += stunden * 10; System.out.println( "Vergütung: " + vergütung + " Euro"); } } In diesem Fall dient die Methode übernehmeVorlesung auch dazu, das Gehalt festzulegen. Welchen Gewinn bietet die Verwendung des Interfaces? Der Vorteil ist, dass jetzt sowohl Professor als auch Tutor als Lehrkraft verwendet werden können. Beide Klassen erfüllen die Bedingung „ist eine Lehrkraft“. Wir untersuchen diese Möglichkeit an Hand einer weiteren Klasse Vorlesung. 10.11. BEISPIEL SORTIEREN public class Vorlesung String name; int stunden = 2; 105 { public Vorlesung() { } public Vorlesung(String n, Lehrkraft lk) { name = n; lk.übernehmeVorlesung( name, stunden); } } Der zweite Konstruktor erwartet als Parameter eine Referenz auf eine Lehrkraft. Als Interface ist Lehrkraft abstrakt und es können keine Objekte erzeugt werden. Aber statt dessen kann ein Objekt einer Klasse, die dieses Interface implementiert, eingesetzt werden. Ein entsprechendes Beispiel ist: public static void main(String args[]) { Professor p = new Professor( "Claudia Maier" ); Tutor t = new Tutor( "Jens Schmidt"); Vorlesung RN = new Vorlesung( "Rechnernetze", p); Vorlesung RNL1 = new Vorlesung( "Analysis 1", t); } Die Ausgabe ist: Prof. Claudia Maier übernimmt Rechnernetze SWS: 2 Tutor Jens Schmidt übernimmt Analysis 1 Vergütung: 20.0 Euro Durch die Einführung des Interfaces können beide Objekte – obwohl sie Instanzen unterschiedlicher Klassen sind – gleichermaßen als zweiter Parameter übergeben werden. Im Konstruktor wird die Methode übernehmeVorlesung ausgeführt. Automatisch wird die jeweils passende Methode ausgewählt und damit die entsprechende Aktion ausgeführt: bei der Professorin wird die Zahl der SWS erhöht und bei dem Tutor die Vergütung 10.11 Beispiel Sortieren Betrachten wir als weiteres Beispiel das Sortieren des Feldes mit Objekten der Klasse Auto. In der Klasse Arrays finden wir die Methode public static void sort(Object[] a, Comparator c) 106 KAPITEL 10. OBJEKTE UND KLASSEN zum Sortieren von Feldern. Im zweiten Argument wird ein Objekt zum Vergleichen der Feldinhalte übergeben. Der Vergleicher wird durch Implementieren des Interfaces Comparator realisiert. Der Dokumentation entnimmt man: public interface Comparator Method Summary int compare(Object o1, Object o2) boolean equals(Object obj) Compares its two arguments for order. Indicates whether some other object is " Der Quellcode ist public interface Comparator { int compare(Object o1, Object o2); boolean equals(Object obj); } Das Schlüsselwort interface unterscheidet die Definition von einer Klasse. Ein Interface ist per Definition abstrakt. Nach dem Namen könnte noch die Ableitung eines oder mehrerer anderer Interfaces folgen. Im Block steht die Definition der beiden Methoden. Ansonsten darf ein Interface nur noch die Definition von Konstanten enthalten. 10.11.1 Interface in eigener Klasse Der Comparator wird in einer Klasse Vergleich wie folgt implementiert: public class Vergleich extends java.lang.Object implements java.util.Comparator { public int compare( java.lang.Object obj, java.lang.Object obj1) { Auto a1, a2; a1 = (Auto) obj; a2 = (Auto) obj1; // Sortieren nach Preis return a1.kaufPreis - a2.kaufPreis; } } Die Klasse wird von Object abgeleitet und implementiert Comparator. Es fällt auf, dass nur die Methode compare implementiert wird, nicht aber die Methode equals. Dies scheint zunächst der Forderung zu widersprechen, alle Methoden des Interfaces zu implementieren, um zu einer konkreten Klasse zu kommen. Die 10.11. BEISPIEL SORTIEREN 107 Klasse Vergleich braucht allerdings equals nicht selbst zu implementieren, da sie diese Methode von Object erbt. Die Methoden des Interfaces müssen also nicht unbedingt selbst implementiert zu werden, sondern können von irgendeiner der Oberklasse geerbt werden. Damit lässt sich das Feld wie folgt sortieren Vergleich vrgl = new Vergleich(); Arrays.sort( v.autos, vrgl ); oder Arrays.sort( v.autos, new Vergleich() ); 10.11.2 Interface integriert in andere Klasse Ein Interface kann auch von einer normalen Klasse implementiert werden. Wir betrachten eine Erweiterung der Klasse Auto: public class Auto extends Fahrzeug implements Comparable { ... public int compareTo(java.lang.Object p1) { return this.kaufJahr - ((Auto) p1).kaufJahr; } ... Die Klasse implementiert jetzt das Interface Comparable. Dieses Interface hat nur eine Methode compareTo, mit der das aktuelle Objekt mit einem zweiten Objekt verglichen wird. Der Aufruf zum Sortieren des Feldes ist dann Arrays.sort( v.autos ); Es wird kein expliziter Vergleicher angegeben sondern die natural comparison method – die den Objekten eigene Vergleichsmethode – wird verwendet. Da abgeleitete Klassen zuweisungskompatibel zu den Oberklassen sind, kann Auto sowohl als Fahrzeug als auch als Comparable betrachtet werden. Das folgende Beispiel illustriert diese Doppelnatur: Auto a = new Auto("porsche", 40000, 2001); Comparable c = new Auto("golf", 15000, 1999); System.out.println("Comparable c: " + c); System.out.println("Vegleich " + c.compareTo( a ) ); System.out.println("Vegleich " + a.compareTo( c ) ); Ausgabe: Comparable c: Auto@1fcc69 Vegleich -2 Vegleich 2 108 KAPITEL 10. OBJEKTE UND KLASSEN Ein Auto-Objekt hat Zugang zu den Methoden des Interfaces. Umgekehrt kann ein Objekt der Oberklasse Comparable nicht auf die Methoden der Klasse Auto zugreifen: c.print(); // geht nicht Allerdings ist eine explizite Typumwandlung möglich: ( (Auto) c).print(); 10.12 // okay Anonyme Klassen Klassen, die nur an einer Stelle benötigt werden, können dort direkt als so genannte anonyme Klassen eingefügt werden. Aus dem Beispiel mit dem Comparator wird dann Arrays.sort( v.autos, new Comparator() { public int compare( java.lang.Object obj, java.lang.Object obj1) { Auto a1, a2; a1 = (Auto) obj; a2 = (Auto) obj1; return a1.kaufPreis - a2.kaufPreis; } } ); Hier wird sofort der Code für die Methode eingefügt. Vorteilhaft an diesem Vorgehen ist eine gewisse Übersichtlichkeit: • weniger explizite Klassen • Code direkt dort wo er benötigt wird Das Verfahren ist aber auf solche kleinen, einmaligen Klassen beschränkt. Sobald der Code umfangreicher wird, sollte eine eigene Klasse angelegt werden. Häufig findet man anonyme Klassen bei Programmen mit graphischer Oberfläche, um verschiedenste Arten von Ereignissen zu behandeln. Das folgende Beispiel zeigt einen WindowAdapter, in dem das Verhalten bei Schließen des Fensters definiert wird: addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent event) { dispose(); // alle Ressourcen freigeben System.exit(0); // Anwendung beenden } } ); 10.13. LOKALE KLASSEN 10.13 109 Lokale Klassen Der Vollständigkeit halber sollen an dieser Stelle kurz lokale Klassen eingeführt werden. Lokale Klasse werden innerhalb einer anderen Klasse definiert (innerhalb des äußersten {}-Blocks). Sie sind nur innerhalb der äußeren Klasse sichtbar haben aber umgekehrt Zugriff auf alle Merkmale dieser Klasse. Der Compiler erzeugt aus anonymen und lokalen Klassen eigene .class Dateien. Der Namen setzt sich aus dem Namen der äußeren Klasse, einem Dollarzeichen und dem Namen der inneren Klasse zusammen. Anonyme Klassen werden nummeriert. Beispiel 10.1 Innere und anonyme Klassen: Verleih$Innen.class Verleih$1.class 110 KAPITEL 10. OBJEKTE UND KLASSEN Kapitel 11 Zeichenketten 11.1 Einleitung Die Basis für die Verarbeitung von Zeichen ist der Datentyp char (Kurzform für character ). Natürlich ist es möglich, Felder von einzelnen Zeichen anzulegen. In Java gibt es darüber hinaus eine eigene Klasse String für Zeichenketten. Die Verwendung von String Instanzen erleichtert wesentlich die Programmierung. Java stellt eine ganze Reihe von Methoden unter anderem zum Suchen in Zeichenketten sowie zum Vergleichen und Verändern von Zeichenketten zur Verfügung. Aufgrund der großen praktischen Bedeutung der String-Verarbeitung wird die Klasse String bevorzugt behandelt. Der Compiler kennt den internen Aufbau und nutzt diese Kenntnis, um den Code zu optimieren. Weiterhin gibt es für diese Klasse einen speziellen Verknüpfungsoperator. 11.2 Datentyp char Einzelne Zeichen werden in Java durch den Datentyp char dargestellt. Die Konstanten werden in einfache Anführungsstriche gesetzt: char zeichen = ’x’; Dabei wird für die Zeichenkodierung Unicode verwendet, so dass auch nationale Sonderzeichen erfasst sind. Steuerzeichen wie der Zeilenumbruch werden durch ein vorgestelltes \-Zeichen notiert: \n \t \’ \\ \uXXXX newline Zeilenumbruch Horizontaler Tabulator Einfaches Anführungszeichen Backslash Unicode-Zeichen mit den vier Hexadezimalziffern XXXX 111 112 KAPITEL 11. ZEICHENKETTEN 11.3 Konstruktoren für String Eine Referenz der Klasse String wird durch die Anweisung in der Form String text; angelegt. Nach dieser Anweisung ist text eine Referenz auf ein String-Objekt. Entweder direkt bei der Definition oder später an beliebiger Stelle kann die Instanz dann mit einer Zeichenketten-Konstanten (Literal) durch einfache Zuweisung belegt werden: text = "Frankfurt"; Dadurch wird ein String-Objekt mit dem entsprechenden Text als Inhalt erzeugt und die Referenz auf dieses Objekt gesetzt. Das folgende Programm zeigt ein einfaches Beispiel mit einem String, der nacheinander mit zwei verschiedenen Inhalten gefüllt wird. public class TestString { String text; public void teste( ) { System.out.println( "text: " + text ); text = "München"; System.out.println( "text: " + text ); text = "Frankfurt"; System.out.println( "text: " + text ); } } Die Ausgabe lautet: text: null text: München text: Frankfurt Bei der ersten Ausgabe ist der String noch leer -– angezeigt durch den speziellen Wert null. Bei der Zuweisung wird automatisch jeweils ein ausreichender Speicherbereich reserviert. Diese Speicherverwaltung übernimmt das JavaLaufzeitsystem. So ist es kein Problem, dass bei der zweiten Zuweisung der Text länger ist. Die Klasse String enthält weiterhin noch eine Reihe von Konstruktoren. Typisch ist der Konstruktor public String(char[] zeichen) 11.4. LÄNGE VON ZEICHENKETTEN UND EINZELNE ZEICHEN 113 Der Konstruktor erhält ein Feld von Zeichen. Aus diesen Zeichen bildet er ein String-Objekt mit der entsprechenden Zeichenkette. Ein Beispiel dazu ist: char[] zeichen = { ’a’, ’b’, ’c’ }; text = new String( zeichen ); Dann enthält text die Zeichenkette „abc“. Dieses Vorgehen kann nützlich sein, um Texte automatisch zu erzeugen. Dabei ist zu beachten, dass die Zeichen kopiert werden. Spätere Änderungen im Feld wirken sich nicht mehr auf das StringObjekt aus Andere Konstruktoren erlauben es, nur einen Ausschnitt des Feldes mit Zeichen zu verwenden oder anstelle der Zeichen byte-Werte zu übergeben. 11.4 Länge von Zeichenketten und einzelne Zeichen Die erzeugten String-Objekte können wie andere Objekte benutzt werden. Insbesondere gibt es eine ganze Reihe von Methoden, um mit Strings zu arbeiten. Die Länge der Zeichenkette erhält man mit der Methode length(). Der Aufruf text.length() gibt die Länge – d. h. die Anzahl der enthaltenen Zeichen – zurück. Einzelne Zeichen aus der Zeichenkette kann man mit charAt( int index ) abfragen. Wie bei Felder beginnt die Zählung mit dem Index 0. Die folgenden Programmzeilen bilden eine Schleife zur zeichenweise Ausgabe: for( int i=0; i<text.length(); i++ ) { System.out.println( i + ": " + text.charAt(i) ); } 11.5 Arbeiten mit Zeichenketten Die Zeichenketten in String-Objekten sind konstant. Nach der Initialisierung in einer der oben beschriebenen Formen liegt der Text fest und kann nicht mehr verändert werden. Auf den ersten Blick erscheint dies als eine große und wenig einleuchtende Einschränkung. Schließlich ist die Veränderung von Zeichenketten eine wesentliche Aufgabe vieler Anwendungen. Der Widerspruch löst sich auf, wenn man sich klar macht, dass man eigentlich stets mit den Referenzen auf die String-Objekte arbeitet. Ein Text wird dann verändert, indem ein neues Objekt angelegt wird und anschließend die Referenz auf diese neue Objekt gesetzt wird. Das alte Objekt bleibt unverändert bestehen. Betrachten wir als Beispiel die Anwendung der Methode trim() in der Klasse String. Diese Methode hat die Aufgabe, Leerzeichen am Anfang und Ende einer Zeichenkette zu entfernen. Zunächst wird durch die Zuweisung String text = " München "; 114 KAPITEL 11. ZEICHENKETTEN ein Text mit Leerzeichen erzeugt. Der Aufruf text.trim() erzeugt ein neues String-Objekt, füllt es mit dem Text ohne die Leerzeichen und gibt es zurück. Mit der Zuweisung text = text.trim(); wird die Referenz auf das neue Objekt gesetzt. Auf diese Art und Weise wird das Ziel erreicht: text enthält jetzt den gewünschten Text ohne Leerzeichen. Intern gibt es jetzt zwei Objekte: der ursprüngliche String mit den Leerzeichen und ein neuer String ohne Leerzeichen. Falls das erste Objekt – mit den Leerzeichen – nicht mehr von anderen Referenzen angesprochen wird, ist es Aufgabe des Garbage Collectors dieses Objekt zu löschen und den Speicherplatz wieder frei zu geben. Der Ablauf sieht zusammen gefasst wie folgt aus: Ref. 1.) text 2.) text 3.) text Objekte → " München → " München trim() → "München" " München → "München" " " " Ist keine Veränderung notwendig, weil keine Leerzeichen an den Ränder stehen, so wird auch kein neues Objekt benötigt. Der Aufruf gibt dann einfach die Referenz auf das bestehende Objekt zurück. Der Programmierer braucht sich um diese Details nicht zu kümmern. Aus seiner Sicht ist mit der obigen Anweisung der Text verändert worden. Die Verwaltung der Objekte kann er Java überlassen. Wichtig ist nur, dass er die „Falle“ vermeidet, lediglich die Methode aufzurufen ohne das neue Objekt bzw. die neue Referenz anschließend einer Variablen zuzuweisen. Der Aufruf der Methode text.trim() alleine verändert nicht den Inhalt von text. Die intuitive Anwendung der String-Objekte zeigt folgendes Beispiel: text = "Frankfurt"; text += " am Main"; System.out.println( "text: " + text ); Mittels des Operators zur Verkettung von Strings wird scheinbar die Angabe " am Main" direkt angehängt. Intern ist der Vorgang komplexer. Ausführlich geschrieben lautet die Anweisung text = text + " am Main"; Aus den beiden Strings – der Variablen text und dem Literal " am Main" – wird ein neues String-Objekt erzeugt. Die Referenz auf das neue Objekt wird dann wieder text zugewiesen. Diese Verarbeitung – Erzeugung eines neuen String-Objektes für jede Veränderung – bedeutet einen gewissen Mehraufwand. In den meisten Anwendungen 11.6. TEILKETTEN 115 ist dies jedoch nicht relevant. Ansonsten besteht noch die Möglichkeit, die Klasse StringBuffer zu verwenden, die dynamisch veränderbare Zeichenketten realisiert. 11.6 Teilketten Die Methode zum Ausschneiden von Teilen aus Zeichenketten (substrings) ist String substring( int beginIndex, int endIndex) Sie liefert die Teilkette ab der Position des ersten Arguments bis zur Position vor dem zweiten Argument. So ergibt der Aufruf System.out.println( "Die Wetterau".substring(4,10) ); den Text Wetter. Das Verhalten zeigt folgendes Bild: D i 0 1 e 2 3 W 4 ↑ e 5 t 6 t 7 e 8 r 9 a 10 ↑ u 11 Die Zeichen von Index 4 bis 9 bilden den neuen String. Dadurch, dass die zweite Angabe die Position 10 unmittelbar nach dem Ende der Teilkette bezeichnet, gilt der Zusammenhang beginIndex + Länge = endIndex wodurch sich in vielen Fällen die Berechnung der Endposition vereinfacht. Ein zweite Version von substring mit nur einem Argument liefert die Zeichenkette bis zum Ende des ursprünglichen Strings. Damit ergibt System.out.println( "Die Wetterau".substring(4) ); den Text Wetterau. 11.7 11.7.1 Vergleichen und Suchen Vergleichen Der Vergleich zweier Strings mit dem == Operator testet, ob beide Referenzen auf das gleiche Objekt deuten. In der Regel ist dies eine zu starke Forderung. Meist interessiert nur, ob die beiden Strings die gleiche Zeichenkette enthalten. (Zur Erinnerung: bei primitiven Typen vergleicht der Operator == den Inhalt, bei Referenztypen die Referenz.) Die allgemeine Methode zum Vergleichen der Inhalte ist 116 KAPITEL 11. ZEICHENKETTEN boolean equals(Object anObject) Die Methode liefert true wenn das Argument ein String ist und die gleiche Zeichenkette enthält. Beispiel 11.1 Stringvergleich text = "Frankfurt"; System.out.println( text.equals( "Frankfurt") ); Der Vergleich ergibt true. Die Methode equals vergleicht auf exakte Übereinstimmung. In dem Beispiel hätte text.equals( "frankfurt") das Resultat false. Ein Vergleich ohne Berücksichtigung von Groß- und Kleinschreibung bietet boolean equalsIgnoreCase(String anotherString) Das Methoden-Paar boolean startsWith(String prefix) boolean endsWith(String suffix) erlaubt gezielte Vergleiche mit dem Anfang beziehungsweise Ende der Zeichenkette. Die Details zu diesen Methoden sowie weiteren Varianten findet man in der Dokumentation zu Java. Für z. B. die Implementierung von Sortierverfahren wird häufig ein Vergleich von Strings auf ihre lexikalischen Reihenfolge benötigt. Mit den Methoden int compareTo(String str) int compareToIgnoreCase(String str) wird ein Integer-Wert berechnet, der den lexikalischen Abstand anzeigt. Das Resultat des Ausdrucks text.compareTo( str) ist: negativ 0 positiv text größer als str text ist gleich str text kleiner als str Die Funktionalität entspricht der Bibliotheksfunktion strcmp in C. Es lohnt sich, an dieser Stelle nochmals die Unterschiede zwischen einer prozeduralen Sprache wie C und einer objektorientierten Sprache wie Java zu betrachten. In C ist der Aufruf strcmp( s1, s2) – eine Funktion mit zwei Argumenten. Die Funktion existiert sozusagen eigenständig. In Java gibt es keine „freien“ Funktionen sondern nur Methoden, die an Klassen oder Instanzen „hängen“. Der Aufruf ist demnach s1.compareTo( s2 ). Die Sichtweise ist: es gibt ein Objekt s1 und dieses Objekt verfügt über eine Methode, um es mit einem zweiten Objekt s2 zu vergleichen. 11.8. VERÄNDERN VON ZEICHENKETTEN 117 Anmerkung: Es ist in Java durchaus möglich, prozedural zu programmieren. Man kann z. B. in einer eigenen Klassen MyString eine Methode static boolean compare( String s1, String s2) implementieren und dann mit MyString.compare( s1, s2 ) aufrufen. Da die Methode als statisch deklariert wurde kann sie unabhängig von einem Objekt benutzt werden. Die Frage, ob und wann dies sinnvoll ist, kann nur im Zusammenhang mit dem Gesamtdesign einer Anwendung beantwortet werden. 11.8 Verändern von Zeichenketten Einige Methoden erzeugen ein neues String-Objekt mit geändertem Text. Umwandlung in Groß- oder Kleinbuchstaben übernehmen String toUpperCase() String toLowerCase() Als Beispiel liefert System.out.println("Münchner Straße".toUpperCase() ); MÜNCHNER STRASSE Die Methoden erkennen auch Sonderzeichen wie Umlaute oder ß und behandeln sie richtig. Daneben gibt es noch eine einfache Methode zum Ersetzen von Zeichen: String replace(char oldChar, char newChar) Damit werden alle vorkommenden Zeichen oldChar durch newChar ersetzt. Beispiel: String text = "Die Wetterau"; System.out.println( text.replace( ’e’, ’x’ ) ); ergibt Dix Wxttxrau Weiterhin gibt es das Methoden-Paar String replaceAll(String regex, String replacement) String replaceFirst(String regex, String replacement) Hier werden alle beziehungsweise nur das erste Vorkommen des regulären Ausdrucks regex durch den String im zweiten Argument ersetzt. 118 KAPITEL 11. ZEICHENKETTEN Beispiel 11.2 Reguläre Ausdrücke String email = "[email protected]"; // Ersetze mnd. durch noe. String email2 = email.replaceAll("mnd", "noe"); // Ersetze alle Felder durch den Text feld. Ein Feld ist als // "ein Buchstabe gefolgt von 0 oder mehr Buchstaben und -" // definiert String email3 = email.replaceAll("[a-z][a-z-]*", "xxx"); System.out.println( email2 ); System.out.println( email3 ); Ausgabe: [email protected] [email protected] 11.8.1 Suchen Die Methode zum Suchen in Strings ist indexOf. Es gibt davon verschiedene Varianten, je nachdem ob man nach anderen Strings oder einzelnen Zeichen suchen möchte. Als optionales zweites Argument kann man einen Offset, ab dem erst die Suche beginnen soll, übergeben. Weiterhin gibt es Methoden lastIndexOf, bei denen die Suche am Ende des Strings beginnt. Alle Methoden geben die gefundene Position als Integerwert zurück. Wird das Muster nicht gefunden, so ist der Rückgabewert –1. Beispiel 11.3 Suche in Strings String text2 = "In der schönen Wetterau"; // 01234567890123456789012 // 1111111111222 Methodenaufruf text2.indexOf(’e’) text2.indexOf("Wett") text2.indexOf("WETT") text2.lastIndexOf("e") text2.lastIndexOf("e", 19) text2.lastIndexOf("e", 18) text2.indexOf(’ ’, text2.indexOf(’ ’)+ 1) Ergebnis 4 15 -1 19 19 16 6 11.9. TOKENIZER 11.9 119 Tokenizer Für die häufig auftretende Aufgabe der Zerlegung einer Zeichenkette in einzelne Einheiten (Tokens) stellt Java die Klasse StringTokenizer bereit. Für eine gegebene Zeichenkette erzeugt man durch den Konstruktor StringTokenizer st = new StringTokenizer( text ); einen Tokenizer 1 . In dieser Form behandelt der Tokenizer alle Leerzeichen (genauer gesagt alle folgende Zeichen ’ ’, ’\t’, ’\n’, ’\r’, ’\f’) als Trennzeichen. Der Tokenizer durchsucht die Zeichenkette nach den Trennzeichen und bricht die Kette in entsprechend viele Teile auf. Diese Teile kann man nacheinander mit der Methode nextToken() abrufen. Wie viele Tokens entstehen, kann mit der Methode countTokens() abfragen werden. Die Methode gibt die Anzahl der noch ausstehenden Tokens zurück. Nach einem Aufruf von nextToken() wird der Zähler entsprechend reduziert. Alternativ kann man mit hasMoreTokens() testen, ob überhaupt noch Tokens vorhanden sind. Eine typische Schleife um nacheinander alle Tokens auszugeben ist: while( st.hasMoreTokens() ) { System.out.println( st.nextToken() ); } Falls notwendig, kann man mit einem zweiten Parameter im Konstruktor eine Zeichenkette mit den zu verwendenden Trennzeichen angeben. Die Trennzeichen selbst werden standardmäßig entfernt. Beispiel 11.4 Tokenizer StringTokenizer st = new StringTokenizer( "Vom Eise befreit sind Strom und Bäche" ); int i = 0; while( st.hasMoreTokens() ) { System.out.println( "Token " + i + ": "+st.nextToken() ); ++i; } Ausgabe: Token Token Token Token 1 0: 1: 2: 3: Vom Eise befreit sind benötigt import java.util.StringTokenizer; 120 KAPITEL 11. ZEICHENKETTEN Token 4: Strom Token 5: und Token 6: Bäche Die Klasse StringTokenizer ist recht praktisch, allerdings etwas veraltet. In der Dokumentation heißt es: StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead. Eine entsprechende Konstruktion zur Aufteilung an allen Leerzeichen ist: String[] parts = "Vom Eise befreit sind Strom und Bäche".split(" "); for( String p: parts ) { System.out.println( p ); } 11.10 Konvertierungen Für z. B. die Ausgabe ist es notwendig, andere Objekte oder primitiven Datentypen in Zeichenketten umzuwandeln. Alle Objekte implementieren eine Methode toString(), die eine Darstellung als Zeichenkette liefert. Weiterhin stellt die Klasse String Klassenmethoden valueOf() zum Konvertieren der primitiven Datentypen bereit. Explizit kann man dann zur Ausgabe schreiben: double x = 33.44; System.out.println( "x = " + String.valueOf(x) ); Der Ausdruck String.valueOf(x) liefert einen String, der wiederum mit dem ersten String verbunden wird. In solchen Ausdrücken braucht die Umwandlung nicht explizit angegeben zu werden. Man kann einfacher schreiben double x = 33.44; System.out.println( "x = " + x ); und die Konvertierung wird automatisch eingefügt. 11.11 Die Klasse String ist endgültig Die Klasse String enthält bereits sehr viele Methoden. Trotzdem sind nicht alle Anwendungen abgedeckt. So fehlt beispielsweise eine Methode startsWithIgnoreCase. 11.12. ÜBUNGEN 121 Im Sinne der Objekt-orientierten Programmierung würde man in einem solchen Fall eine eigene Klasse aus der String-Klasse ableiten und die fehlenden Methoden ergänzen beziehungsweise vorhandene Methoden mit eigenen Implementierungen überlagern. Diese Vorgehensweise ist für die Klasse String nicht vorgesehen. Diese Klasse hat das Attribut final. Damit wird verhindert, dass abgeleitete Klassen erzeugt werden. Der Hauptgrund ist ein Gewinn an Performanz. Wir werden später sehen, wie abgeleitet Klassen die Erzeugung von Code schwieriger machen. Wenn es aber keine abgeleiteten Klassen gibt, kann der Compiler effizienteren Code erzeugen. Neue Methoden lassen sich dann nur in anderen Klassen einbauen. Wie in der Anmerkung zur Diskussion von compareTo gezeigt wurde, ist dies durchaus möglich. Diese Implementierung ist aber ein gewisser Bruch mit den Ideen einer streng Objekt-orientierten Programmierung. 11.12 Übungen Übung 11.1 String text = "Die Wetterau"; String text2 = "In der schönen Wetterau"; // 01234567890123456789012 // 1111111111222 1. Was liefert text.substring(7)? 2. Was ist der Unterschied zwischen text.charAt( 0 ) und text.substring(0,1)? 3. Erzeugen Sie aus text durch Anfügen den neuen String " ** Die Wetterau ** " 4. Was ergibt text2.indexOf( "n W" )? 5. Wie kann man in dem String nach dem ersten "er" irgendwo nach einem "W" suchen? 6. Testen Sie, ob ein String mit einem ? endet. Übung 11.2 Reguläre Ausdrücke: Wie lassen sich mit der Methode replaceAll alle Folgen von mehreren Leerzeichen in jeweils ein einziges Leerzeichen verkürzen? Zum Beispiel soll aus "Dies ist ein Beispiel" die Zeichenkette "Dies ist ein Beispiel" werden. Übung 11.3 Telefonnummer: 122 KAPITEL 11. ZEICHENKETTEN 1. Wie kann man mit einem StringTokenizer die Telefonnummer String telefon = "(49)6031.604-450"; in die Bestandteile zerlegen? 2. Wird der String durch den StringTokenizer verändert? Übung 11.4 Umwandlung: Wie ist die Ausgabe von System.out.println( "7 + 5 = " + 7 + 5 )? Übung 11.5 Email-Adressen: Gegeben sei das Format vorname.name@provider für Email-Adressen. 1. Schreiben Sie eine Methode, um einen String mit einer solchen Adresse in seine Bestandteile Vorname, Name und Provider aufzutrennen. 2. Wie kann umgekehrt aus den Bestandteilen eine Email-Adresse gebildet werden? Beachten Sie dabei, dass Email-Adressen keine Sonderzeichen wie Umlaute enthalten dürfen. Kapitel 12 Reguläre Ausdrücke Suchen und Ersetzen sind häufige Aufgaben. Entweder möchte man selbst im Editor oder in der Textverarbeitung ein Muster suchen oder durch ein anderes ersetzen oder Programme sollen diese Aufgabe automatisch ausführen. Ein weiteres Anwendungsfeld sind Plausibiltätsprüfungen. So kann man beispielsweise in eines Eingabemaske testen, ob die Werte bestimmten Vorgaben entsprechen. Beispielsweise soll das Feld für die Postleitzahl genau 5 Ziffern enthalten (sofern man nur Adressen innerhalb Deutschlands betrachtet). Häufig reicht es dabei nicht aus, einen festen Text zu verwenden. Vielmehr sollen kompliziertere Muster behandelt werden. Einige Beispiele sind: • Suche nach allen Datumsangaben in einem Text • Ersetzen des Dezimalpunktes durch ein Komma (oder umgekehrt) • Prüfen, ob eine Zeile eine plausible Anschrift enthält (also Straße, Hausnummer, Postleitzahl, Stadt) • Suchen nach allen C-Dateien, die im Namen den Begriff Euro enthalten Für solche Fälle unterstützen viele Programmiersprachen und Editoren reguläre Ausdrücke (regular expressions). Die Realisierungen in den verschiedenen Programmiersprachen unterscheiden sich in einigen syntaktischen Details sowie dem Leistungsumfang, basieren aber auf dem gleichen Grundprinzip [Fri07]. Im folgenden wird zunächst das Konzept an einigen Beispielen erläutert. Darauf aufbauend wird gezeigt, wie man damit elegant Eingaben suchen und ersetzen kann. Das erste Beispiel boolean b = Pattern.matches("berg", text); zeigt den Mechanismus. Es wird geprüft, ob ein eingegebener Text der Zeichenfolge berg entspricht. Die Methode matches der Klasse Pattern prüft, ob der regulärer Ausdruck im ersten Parameter in der Zeichenkette im zweiten Parameter enthalten ist. In diesem Fall handelt es sich einfach um die Zeichenfolge berg. 123 124 KAPITEL 12. REGULÄRE AUSDRÜCKE Das Resultat der Prüfung wird als wahr oder falsch zurückgegeben. Die Suche nach einem Muster wird beispielhaft durch folgende Methode realisiert: boolean suche( String suchMuster, String test ) { // Kompiliere das Suchmuster zu einem RE-Pattern Pattern p = Pattern.compile(suchMuster ); // Verknuepfe Pattern mit Zeichenkette, die durchsucht werden soll Matcher m = p.matcher(test); // fuehre Suche aus, gebe Ergebnis zurueck return m.find(); } Wie das Beispiel zeigt, werden Buchstaben als normaler Suchtext behandelt. Das gleiche gilt für Ziffern und einen Teil der Sonderzeichen. Anderen Sonderzeichen haben eine besondere Bedeutung. So steht der Punkt als Platzhalter für irgendein Zeichen. Das Muster a.b deckt damit Folgen in der Art aAb, axb, a2b, u. s. w. ab. Mehrere Punkte stehen für mehrere Zeichen. Mit .....berg findet man Friedberg, aber auch Godesberg, eidelberg oder gar _Goldberg und p-n-Überg. Mit den beiden Zeichen ^ und $ kann man Zeilenanfang und Zeilenende angeben. So sucht ^...$ nach allen Zeilen, die genau drei Zeichen enthalten. Die beiden Zeichen gehören zu den Boundary matchers. Diese symbolisieren Grenzen und „verbrauchen“ keine Zeichen im untersuchten Text (zero-length match). Dieser Gruppe enthält unter anderem auch \b für eine Wortgrenze. Damit gilt: berg ^berg$ \bberg\b berg\b berg irgendwo, auch Friedbergerin berg alleine in einer Zeile einzelnes Wort berg am Wortende, also Friedberg, aber nicht Friedberger Mehrere Alternativen werden durch ein |-Zeichen getrennt. So kann man mit berg|stadt gleichzeitig nach berg und stadt suchen. Kompliziertere Ausdrücke werden durch runde Klammern gegliedert. Beispielsweise bedeutet (A|B)..(berg|stadt) alle Muster mit: • A oder B am Anfang • dann zwei beliebige Zeichen • berg oder stadt am Ende • Beispiele: Arlberg, Bamberg 125 . | [] ^ $ () \ Tabelle 12.1: Metazeichen Irgendein Zeichen Auswahl Zeichenklasse Zeilenanfang Zeilenende Gruppe Danach wird das nächste Zeichen als normaler Text verwendet Anstelle von (A|B) kann man kürzer [AB] schreiben. Allgemein kann man mehrere einzelne, alternative Zeichen als so genannte Zeichenklasse in eckigen Klammern angeben. Dann wird an dieser Stelle jedes der aufgeführten Zeichen akzeptiert. In diesem Fall haben die allermeisten Sonderzeichen keine besondere Bedeutung sondern werden wie normale Zeichen behandelt. Ausnahmen sind das Minus- und das ^-Zeichen. Mit dem Minuszeichen werden Bereiche spezifiziert. Beispielsweise umfasst [a-h] alle Buchstaben von a bis h. Ein vorangestelltes ^-Zeichen kehrt die Bedeutung um. Beispiel 12.1 Zeichenklassen • [Ff] groß oder klein F • [a-z] irgendein Kleinbuchstabe • [A-Za-z] ein Buchstabe • [0-9] eine Ziffer (entspricht [0123456789]) • [^ijkxyz] alles außer den aufgeführten Buchstaben • [^0-9] keine Ziffer • [^ .,;!?] kein Leerzeichen und kein Satzzeichen Für häufig benutzte Klassen sind in Java eigene Zeichen definiert. So bezeichnet beispielsweise \d die Klasse aller Ziffern und \w alle alphanumerischen Zeichen (Ziffern und Buchstaben). Die Umkehrung wird durch einen Großbuchstaben dargestellt: \D sind alle Zeichen außer Ziffern und \W umfasst alle Sonderzeichen. Mehrere Klassen können mittels && verknüpft werden. Zwei Beispiele aus der Java-Dokumentation: [a-z&&[^bc]] a through z, except for b and c: [ad-z] (subtraction) [a-z&&[^m-p]] a through z, and not m through p: [a-lq-z](subtraction) 126 KAPITEL 12. REGULÄRE AUSDRÜCKE Tabelle 12.2: Wiederholungszeichen * beliebig oft oder gar nicht + mindestens 1-mal ? 1-mal oder gar nicht {n} genau n-mal {n,} mindestens n-mal {n,m} mindestens n-mal, höchstens m-mal Häufig möchte man Wiederholungen von Teilen des Suchmusters spezifizieren. Ohne weitere Angabe darf jedes Zeichen genau einmal vorkommen. So bedeutet [0-9] genau eine Ziffer. Dreistellige Zahlen können dann durch Wiederholung als [0-9][0-9][0-9] beschrieben werden. Zur Vereinfachung können Wiederholungsfaktoren – so genannte Quantoren – hinter die Ausdrücke geschrieben werden. In Tabelle 12.2 sind die verschiedenen Möglichkeiten dazu zusammengestellt. Allgemein wird mit der Form {n,m} angegeben, dass ein Muster mindestens n-mal und höchstens m-mal auftreten darf. Mit e{2,3} sind zwei oder drei Wiederholungen des Buchstaben e beschrieben. Die Kombination .* steht als Platzhalter für eine beliebige Folge von Zeichen einschließlich einer leeren Folge. Für die dreistelligen Zahlen kann man übersichtlicher schreiben: [0-9]{3} Beispiel 12.2 Wiederholungen • ^[0-9]+$ alle Zeilen, die nur Ziffern enthalten • [aeiou]{5} alle Muster mit genau 5 aufeinander folgenden Vokalen (z. B. Treueeid) • a\w*b\w*c\w*d\w* Wörter, die die Buchstaben a, b, c und d in dieser Reihenfolge enthalten 12.0.1 Gefundene Teile Ein Matcher-Objekt enthält alle Informationen zu der aktuellen Suche. Über entsprechende Methoden kann auf diese Informationen zugegriffen werden. Das nächste Beispiel zeigt die Möglichkeit durch folgende Abfolge: 1. Methode find(), um nächsten Treffer zu finden 12.1. ÜBUNGEN 127 2. Falls erfolgreich, werden über start() und end() die Grenzen abgefragt. 3. Mit den Grenzen wird der entsprechende Teil der Zeichenkette entnommen und ausgegeben. Die Methode public void testeRE() { String test = "THW Kiel gewann mit 33 zu 21 Toren"; String suche = "[0-9]+"; // eine oder mehr Ziffern System.out.println(test); Pattern p = Pattern.compile(suche ); Matcher m = p.matcher(test); for(;;) { if( ! m.find() ) break; System.out.println(test.substring( m.start(), m.end()+1) ); } } ergibt THW Kiel gewann mit 33 zu 21 Toren 33 21 12.1 Übungen Übung 12.1 Reguläre Ausdrücke Welche Muster werden durch folgende reguläre Ausdrücke beschrieben: 1. [a-zA-Z_][a-zA-Z_0-9]* 2. [+-]?[0-9]+ 3. [a-zA-Z.]*\@[a-zA-Z.]* 4. ([0-9A-F]{2}-){5}[0-9A-F]{2} Übung 12.2 Reguläre Ausdrücke Geben Sie für folgende Suchaufgaben reguläre Ausdrücke an: 1. Folgen von zwei oder mehr Leerzeichen 2. Wörter mit mehr als 25 Buchstaben 128 KAPITEL 12. REGULÄRE AUSDRÜCKE 3. Alle achtstelligen Binärzahlen 4. Integerkonstanten in der Sprache C in oktaler oder hexadezimaler Schreibweise 5. Alle Jahreszahlen von 1980 bis 2099 6. Zeitangaben in der Form Stunden:Minuten 7. Alle Autokennzeichen, die mit FB beginnen, und bei denen folgt: ein Leerzeichen, ein oder zwei Großbuchstaben, ein Leerzeichen, ein bis drei Ziffern 8. Zeilen, die mit <h1 oder <h2 beginnen 9. Ausdrücke, die mit <a beginnen und mit a> enden Kapitel 13 Tools 13.1 Einleitung Der Satz von elementaren Entwicklungstools wird als JDK (Java Development Kit) bezeichnet. Dazu gehören eine Reihe von Tools wie der Compiler javac, der Starter java und der Debugger jdb. Die aktuelle Version ist J2SE 1.2, wobei die Abkürzung für Java 2, Standard Edition steht. Als Erweiterung bietet Sun die Java 2 Platform, Enterprise Edition, abgekürzt J2EE an. Benutzer, die nur vorhandene Java-Anwendungen ausführen möchte, können das Java Runtime Environment JRE verwenden. In diesem Kapitel wird eine Einführung zu den wichtigsten der Tools des JDK gegeben. Details über die zum Teil umfangreichen Optionen findet man in der Dokumentation von Sun. 13.2 jar Der Compiler erzeugt aus jeder Klasse eine eigene Bytecode-Datei mit der Endung .class. Mit wachsender Anzahl von Klassen führt dies zu einer großen Anzahl von Dateien. Speziell bei Applets, die über das Internet ausgeführt werden, ist dies nachteilig. Über eine HTTP-Verbindung wird jede Datei – und damit jede Klasse – einzeln geladen. Daher besteht die Möglichkeit, mehrere (Klassen-) Dateien zu einem Archiv zusammen zu stellen. Solche Archive können von den JDK-Tools verwendet werden. Das Tool zum Erstellen und Verwalten der Archive jar -– Java archive program — basiert auf dem UNIX Programm tar (tape archive program) und verwendet eine ähnliche Syntax. Der Aufruf, um eine Anzahl von Dateien zu einem Archiv zusammen zu packen, lautet jar cvf archiv-file file1 file2 file3 ... Das erste Argument spezifiziert die auszuführenden Operationen durch eine Reihe von einzelnen Buchstaben. In dem Beispiel haben die Kommandos die Bedeutung 129 130 KAPITEL 13. TOOLS c create Anlegen eines neuen Archivs v verbose Mit vielen Ausgaben zur Information f file Ausgabe in eine Datei (das nächste Argument) Mit dem Befehl jar cvf test.jar *.class Manifest wurde hinzugefügt. Hinzufügen von: FH.class(ein = 725) (aus= 495)(komprimiert 31 %) Hinzufügen von: Lehrkraft.class(ein = 174) (aus= 146)(komprimiert 16 %) Hinzufügen von: Person.class(ein = 401) (aus= 265)(komprimiert 33 %) Hinzufügen von: Prof.class(ein = 942) (aus= 540)(komprimiert 42 %) Hinzufügen von: Student.class(ein = 374) (aus= 250)(komprimiert 33 %) Hinzufügen von: Tutor.class(ein = 882) (aus= 505)(komprimiert 42 %) Hinzufügen von: Vorlesung.class(ein = 869) (aus= 549)(komprimiert 36 %) werden alle .class Dateien aus dem Beispiel FH in ein Archiv test.jar kopiert. Die Ausgabe zeigt, dass dabei die Dateien komprimiert werden. Weiterhin wird eine Datei mit dem Namen Manifest hinzu gefügt. In dem Beispiel wurde dazu keine weitere Angabe gemacht und jar erzeugte ein Standardversion dieser Datei mit dem Inhalt: Manifest-Version: 1.0 Created-By: 1.4.0-beta3 (Sun Microsystems Inc.) Im allgemeinen enthält die Datei Informationen über das Archiv. Die Informationen werden als Paar Name: Wert dargestellt. Anstelle der automatisch erzeugten Version kann man eine eigene Manifest-Datei angeben. Eine Datei mit dem Namen manifest wird durch den Befehl jar cvmf manifest test.jar *.class eingebunden. Maßgeblich ist dafür die Option m. Man beachte, dass die Dateien in der Reihenfolge der Optionen anzugeben sind. Mit diesem Mechanismus kann in einem Archiv die auszuführende Klasse spezifiziert werden. Dazu trägt man in die Manifest-Datei den Namen der Hauptklasse ein: Main-Class: FH Dann wird bei dem Aufruf java -jar test.jar die Methode main in der angegebenen Hauptklasse ausgeführt. 13.3. JAVA UND KLASSENNAMEN 13.3 131 java und Klassennamen Die Methode main einer Klasse wird über den Aufruf java Klasse ausgeführt. Im einzelnen erfolgen die Schritte 1. Die Java VM wird gestartet. 2. Die angegebene Klasse wird geladen. 3. Andere benötigten Klassen werden geladen. 4. Die Methode public static void main(String args[]) wird ausgeführt. 5. Eventuelle weitere Argumente nach dem Klassennamen werden als Parameter im Feld args[] an main übergeben. Interessant ist dabei die Frage, wie die anderen Klassen gefunden werden oder -– anders gefragt — wo nach Klassen gesucht wird. Java unterscheidet 3 Hierarchien bei der Suche: 1. Bootstrap Classes 2. Extension Classes 3. User Classes Mit Bootstrap Classes sind die Standardklassen von Java gemeint. Sie befinden sich in Archiven (z. B. rt.jar) im Verzeichnis jre\lib innerhalb des SDK. In dem weiteren Verzeichnis jre\lib\ext können Erweiterungsklassen abgelegt werden. Diese Klassen müssen in Archiven gepackt sein. Es ist nicht möglich, an dieser Stelle eine einzelne Klassendatei zur Suche zu verwenden. Schließlich können eigene Klassen eingesetzt werden. Diese Klassen können entweder als einzelnen Dateien oder in Archiven vorliegen. Die zu durchsuchenden Verzeichnisse sind als CLASSPATH definiert. Es handelt sich dabei um eine Liste von Verzeichnisnamen, getrennt mit ; oder : für Windows beziehungsweise UNIX-Umgebungen. Standardmäßig enthält CLASSPATH das aktuelle Verzeichnis (.). Mit der Option –cp oder ausführlich –classpath lässt sich dieser Standard beim Aufruf von java überschreiben. Eine mittlerweile nicht mehr empfohlene Alternative besteht darin, die Systemvariable CLASSPATH zu setzten. 132 13.4 KAPITEL 13. TOOLS Klassennamen Zur besseren Verwaltung und Übersicht können Klassen zu Paketen zusammen gefasst werden. Die Zugehörigkeit zu einem Paket wird mit der Anweisung package PaketName; bestimmt. Diese Anweisung muss ganz am Anfang der Datei stehen. Fehlt eine solche Angabe, so wird die Klasse dem Default-Paket zugeordnet. Die Paketnamen bestehen aus einem oder mehreren durch Punkte getrennten Namen. Dazu schlägt Sun ein Schema basierend auf dem eigenen Domainnamen vor. Damit sollen Namenskonflikte durch gleichlautende Pakete verhindert werden. Konkret wird vorgeschlagen, den eigenen Domainnamen in umgekehrter Reihenfolge der Komponenten zu verwenden. Wenn wir z. B. uns in mnd.fh-friedberg.de befinden, so ist ein entsprechender Paketname (ohne Bindestrich) package de.fhfriedberg.mnd.pg.beisp1; Das Beispiel der FH-Verwaltung lässt sich leicht umstellen, indem in allen Klassen eine entsprechende Zeile eingefügt wird. Die Klassen werden automatisch in einer passenden Struktur von Verzeichnissen abgelegt. Mit dem Aufruf javac -d . *.java erzeugt der Compiler ausgehend von dem aktuellen Verzeichnis einen Baum mit den angegebenen Namen. Angenommen die Klasse FH gehöre nicht zu diesem Paket. Dann findet der Compiler nicht mehr die Klassen im aktuellen Pfad. Eine Möglichkeit ist dann, die Klassen mit ihrem vollen Namen inklusive Paket anzugeben (voll qualifizierter Name). Im Beispiel kann man schreiben de.fhfriedberg.mnd.pg.beisp1.Vorlesung v = new de.fhfriedberg.mnd.pg.beisp1.Vorlesung(); Durch die Angabe des Paketnamens findet der Compiler und später auch java die Klassen. Mit dieser Methode lassen sich zwar Klassen eindeutig spezifizieren, aber für den häufigen Gebrauch ist die volle Namensangabe sehr umständlich. Daher kann alternativ ein Paket über die import-Anweisung geladen werden. Wenn die Klasse FH die Anweisung import de.fhfriedberg.mnd.pg.beisp1.*; enthält, kann der Paketname entfallen. Beim Kompilieren kann man den Ladevorgang durch Angabe der Option -verbose ausgeben lassen: >javac -verbose FH.java [parsing started FH.java] [parsing completed 140ms] 13.5. JAVADOC 133 [loading c:\programme\j2sdk1.4.0-beta3\jre\lib\rt.jar( java/lang/Object.class)] [loading c:\programme\j2sdk1.4.0-beta3\jre\lib\rt.jar( java/lang/String.class)] [checking FH] [loading .\de\fhfriedberg\mnd\pg\beisp1\Vorlesung.class] [wrote FH.class] [total 601ms] 13.5 javadoc Mit dem Tool javadoc lässt sich eine Dokumentation für die eigenen Klassen erstellen. Standardmäßig erzeugt es eine Reihe von HTML-Dateien, in denen einerseits die einzelnen Klassen und andererseits die Verwandtschaft zwischen den Klassen dargestellt wird. Bereits ohne besondere Vorbereitung erhält man auf diese Art und Weise eine nützliche Übersicht über die Klassen sowie ihre Methoden und Variablen. Als Einstiegspunkt wird die Datei index.html generiert. Ein entsprechender Aufruf ist javadoc –private *.java Die Option –private bestimmt, dass auch Elemente mit dem entsprechenden Attribut einbezogen werden. Durch so genannte Dokumentationskommentare lassen sich leicht weitere Informationen integrieren. Dokumentationskommentare beginnen mit der Markierung /** und enden wie normale Kommentare mit */. javadoc interpretiert solche Abschnitte als Kommentar für die jeweils nächste unmittelbar folgende Einheit. Je nach Position kann sich also ein Kommentar beispielsweise auf eine Klasse oder eine Methode beziehen. Wichtig ist die unmittelbare Folge. Ein häufiger Fehler ist das Einschieben von import-Anweisungen zwischen Kommentar und Beginn der Klasse. Dann wird die Dokumentation ignoriert. Betrachten wir ein Beispiel mit Dokumentationen der Klasse und einer Methode: /** * Dies ist die Hauptklasse des Beispiels FH-Verwaltung. * * @author S. Euler * @version .9 * */ public class FH { ... /** * Eine weitere Methode. 134 KAPITEL 13. TOOLS * Diese Methode gibt es nur, * um einige M&ouml;glichkeiten von javadoc zu zeigen. * Der Kommentar kann HTML Elemente * wie <strong>Hervorhebungen</strong> oder * <br> Zeilenumbr&uuml;che enthalten. * * @param level Ein erstes Argument * @param x Ein zweites Argument */ public static void test( int level, double x ) {} } Ein Kommentar beginnt mit einer Beschreibung. Die erste Zeile des Textes dient später als Kurzbeschreibung. Der Text kann HTML-Elemente enthalten. Lediglich die Tags für Überschriften <h1> und <h2> sollten vermieden werden, da sie auch von javadoc verwendet werden. Die Sternchen dienen nur zur Kennzeichnungen des Kommentars und werden von javadoc entfernt. Nach der allgemeinen Textbeschreibung können markierte Absätze folgen. Jeder solche Abschnitt beginnt mit einem durch ein @ markierten Tag. Beispiele für diese Tags sind author, since oder param. Sie werden in eigenen Abschnitten mit spezieller Formatierung wiedergegeben. Standardmäßig werden nicht alle Tags ausgewertet. So muss die Darstellung des Tags author mit der Option javadoc –author angefordert werden. 13.6 jdb Der Debugger für Java ist jdb. Es handelt sich dabei um eine Version basierend auf der Eingabe von Kommandozeilen. Komfortabler ist die Nutzung über eine graphische Oberfläche wie z. B. integriert in NetBeans. Im folgenden ist eine Beispiel-Session mit jdb wieder gegeben (Eingaben jeweils nach dem Prompt > bzw. main[1] ). >jdb FH Initializing jdb ... > stop in FH.main Deferring breakpoint FH.main. It will be set after the class is loaded. > run run FH > VM Started: Set deferred breakpoint FH.main Breakpoint hit: "thread=main", FH.main(), line=23 bci=0 13.6. JDB 23 135 Vorlesung v = new Vorlesung(); main[1] next > Step completed: "thread=main", FH.main(), line=24 bci=9 24 Prof p = new Prof( "Christian Müller" ); main[1] list 20 * @param args the command line arguments 21 */ 22 public static void main (String args[]) { 23 Vorlesung v = new Vorlesung(); 24 => Prof p = new Prof( "Christian Müller" ); 25 Tutor t = new Tutor( "Jens Schneider" ); 26 Student s = new Student( "geht nicht" ); 27 28 v.gehaltenVon( p ); 29 v.gehaltenVon( t ); main[1] print v v = instance of Vorlesung(id=285) main[1] dump v v = { name: "pg" } 136 KAPITEL 13. TOOLS Kapitel 14 Ein- und Ausgabe und Dateien 14.1 Einleitung Der Themenkomplex Ein- und Ausgabe umfasst eine ganze Reihe von Einzelfragen. Wichtige Punkte sind • Eingabe von Tastatur • Ausgabe auf Bildschirm, Lautsprecher, Drucker • Arbeiten mit Dateien • Zeichensätze (ASCII, Unicode) • Formatierte und unformatierte Ein- / Ausgabe • Datenaustausch – gegebenenfalls über ein Netzwerk – mit anderen Prozessen Java stellt dazu eine Reihe von Klassen und Methoden bereit. Die große Anzahl von spezialisierten Klassen und die Möglichkeit diese miteinander zu kombinieren erschweren den Überblick. Im folgenden werden zunächst einige Aspekte anhand der Standardein- und –ausgabe diskutiert. Anschließend folgt ein Überblick über die grundlegenden Designideen. Wichtige Klassen und Methoden werden dann im Detail besprochen. Den Abschluss bildet die Klasse File zum Umgang mit Dateien. 14.2 Standardeingabe und Standardausgabe Die Ausgabe in ein Konsolfenster und die direkte Eingabe von der Tastatur spielen bei Java nicht die zentrale Rolle. Konsolanwendungen sind eher die Ausnahme gegenüber Anwendungen mit graphischen Benutzeroberflächen. Dort stehen andere Möglichkeiten zur Ein- und Ausgabe zur Verfügung. Allerdings kann auch 137 138 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN dann die – eventuell optionale – Ausgabe auf der Konsole ein gutes Hilfsmittel zur Ausgabe von Statusinformationen sein. 14.2.1 Ausgabe Zur Ausgabe haben wir bereits häufig die Konstruktion System.out.print ... benutzt. System ist eine Klasse mit einer Reihe von allgemeinen Klassenvariablen und Klassenmethoden. Darunter sind die Variablen in, err und out. Die Standardausgabe erfolgt über out – einem PrintStream. Fehlermeldungen können über einen zweiten Ausgabestrom err erfolgen. PrintStream ist eine Klasse, die mit entsprechenden Methoden eine einfache Ausgabe von verschiedenen Daten ermöglicht. Sie implementiert eine Reihe von Methoden print bzw. println für die unterschiedlichsten Datentypen. Das Argument wird – falls erforderlich – durch den Aufruf der passenden Methode String.valueOf in einen String umgewandelt und ausgegeben. Bei den Varianten println wird gleichzeitig die Zeile abgeschlossen. Einige Beispiele: Beispiel 14.1 println: int i = 1234; double x = 33.44; double y = -0.123; boolean b = true; String text = "Dies ist ein Test"; double[] feld = {1., 2., 3. }; System.out.println( System.out.println( System.out.println( System.out.println( System.out.println( System.out.println( i ); "x = " + x ); y ); b ); text ); feld ); ergibt 1234 x = 33.44 -0.123 true Dies ist ein Test [D@1fcc69 Die Ausgabe stellt die Werte in lesbarer Form dar. Interessant ist die Ausgabe des Feldes. Hier werden nicht etwa die einzelnen Element ausgegeben. Vielmehr wird intern die Methode toString des Feldobjektes aufgerufen. Der erzeugte String 14.2. STANDARDEINGABE UND STANDARDAUSGABE 139 gibt den Typ des Objektes „Feld von double“ durch [D an und hängt nach dem @ den so genannten Hash-Code – eine eindeutige Kennung für das Objekt – an. Einige Eigenschaften dieser Ausgabe sind: • Die verschiedenen Methoden print haben nur maximal ein Argument. Mehrere Werte können durch Verknüpfung zu einem String in einem Aufruf ausgegeben werden. • Es gibt keine Formatierungsmöglichkeit der Zahlendarstellung beim Aufruf. • Jedes beliebige Objekt kann als Argument übergeben werden. • Übergibt man eine leere Referenz, so wird der String null ausgegeben. 14.2.2 Formatierte Ausgabe mit printf fehlt noch 14.2.3 Eingabe Der Eingabestrom System.in ist nicht direkt zum komfortablen Einlesen gedacht. Vielmehr muss man zunächst auf den Eingabestrom einen InputStreamReader setzen. Er hat die Aufgabe, die gelesenen Bytes in Zeichen umzusetzen. Dieser Zeichenstrom kann dann in einen BufferedReader geleitet werden, der schließlich Methoden zum Lesen von Strings bereit stellt. Ausführlich kann man schreiben InputStreamReader isr = new InputStreamReader( System.in ); BufferedReader din = new BufferedReader( isr ); Da der InputStreamReader aber nur an dieser Stelle benötigt wird, kann man auf die Referenz isr verzichten und statt dessen die Aufrufe ineinander schachteln: BufferedReader din = new BufferedReader( new InputStreamReader( System.in ) ); Dann kann mit String text = din.readLine(); die nächste Zeile gelesen werden. Ist das Ende der Datei erreicht, gibt die Methode den Wert null zurück. Hinweis: Die Methode readLine kann einen Ausnahmefehler (Exception) hervor rufen. In einem der nächsten Kapitel werden wir sehen, wie man solche Ausnahmefehler behandelt. Bis dahin werden wir als einfache Lösung die Fehler „weiter reichen“. Dazu muss in jeder Methode, die readLine benutzt, und den jeweils übergeordneten der Hinweis throws Exception eingefügt werden. 140 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN Es mag überraschen, dass nur Methoden zum Lesen von einzelnen Zeichen oder ganzen Zeilen implementiert wurden. Es fehlen die aus anderen Programmiersprachen bekannten Möglichkeiten, direkt verschiedene Formate für Integerund Gleitkommazahlen lesen zu können. In Java ist das Vorgehen anders: 1. Einlesen einer Zeile 2. eventuell Aufteilen, Leerzeichen entfernen (Tokenizer) 3. aus den (Teil)-Zeichenketten die Werte entnehmen Methoden zur Analyse von Zeichenketten sind in den so genannten WrapperKlassen enthalten. Zu jedem primitiven Datentyp gibt es eine Wrapper-Klasse. Der Name der Wrapper-Klasse stimmt in der Regel mit dem Namen des Datentyps überein, beginnt aber mit einem Großbuchstaben. So sind Float und Boolean die Wrapper-Klassen für die primitiven Typen float und boolean. Lediglich bei Character für char und Integer für int unterscheiden sich die Namen. Jede dieser Klassen implementiert – neben vielen anderen – Methoden zur Analyse (Parsen) von Zeichenketten. So ist in der Klasse Long public static long parseLong(String s) die Methode, um aus einem String einen long Wert zu erhalten. Die Namen dieser Methoden werden jeweils aus dem Wort parse und dem Typnamen gebildet, also parseBoolean, parseInt, etc. Als Argument erhalten sie einen String sowie eventuell noch Informationen zur Formatierung (z. B. Zahlensystem). Bei Erfolg liefern sie einen entsprechenden Wert zurück, ansonsten kommt es zu einem Ausnahmefehler. Da wir die Ausnahmen bisher nicht behandeln, wird bei einer Ausnahme das Programm beendete und der Java Interpreter gibt eine Fehlermeldung aus. Beispiel 14.2 Einlesen von Integerwerten: for( ;; ) { System.out.print( "> " ); // Zeile einlesen String text = din.readLine(); // Kontrollausgabe System.out.println("text( Länge= " + text.length() +"): " + text ); // aus Zeile einen Integerwert lesen int iwert = Integer.parseInt( text ); System.out.println( "als Integer : " + iwert ); } 14.3. STREAMS, READER UND WRITER 141 > text( Länge= 4): 2333 als Integer : 2333 > text( Länge= 2): -3 als Integer : -3 > text( Länge= 3): 3.4 java.lang.NumberFormatException: 3.4 at java.lang.Integer.parseInt(Integer.java:423) at java.lang.Integer.parseInt(Integer.java:463) at IOtest.main(IOtest.java:65) Mit entsprechenden Methoden in den Wrapper-Klassen kann man die Konvertierung im Detail festlegen und z. B. auch Oktalzahlen einlesen. 14.2.4 Einlesen mit Scanner fehlt noch 14.3 Streams, Reader und Writer Ein- und Ausgabe basiert auf dem allgemeinen Konzept von Datenströmen. Aus Sicht der Anwendung gibt es Datenkanäle zu Quellen und Senken. Durch diese Kanäle fließt die Information. Die Anwendung befindet sich an einem Ende und kann Portionsweise Daten einspeisen oder entnehmen. Am anderen Ende des Stroms können verschiedene Arten von Quellen oder Senken sein: • Geräte wie Tastaturen oder Bildschirme • Dateien • Speicherbereiche • Sockets (Schnittstellen für Netzwerkanwendungen) • Pipes (Ein Kommunikationsmechanismus zwischen zwei Prozessen) Die Verarbeitung soll aber weitgehend unabhängig von der Art des Partners sein. Das Bild des Datenstroms abstrahiert weitgehend von Implementierungsdetails. Die Operationen sind nahezu die gleichen. Man kann: • einen Datenstrom öffnen • Daten lesen oder schreiben • den Datenstrom wieder schließen Java unterscheidet zwischen zwei grundsätzlichen Arten von Datenströmen: 142 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN • basierend auf einzelnen Bytes (8 Bit) • basierend auf einzelnen Unicode-Zeichen (16 Bit) Wohl aus historischen Gründen gibt es dafür zwei getrennte Familien von Klassen: Byte InputStream OutputStream Zeichen (16 Bit Unicode) Reader Writer Die Klassen Reader und Writer – genauer gesagt die daraus abgeleiteten Klassen – werden empfohlen, wenn eine Anwendung Texte lesen und schreiben will. Möchte man demgegenüber allgemeine Daten oder Objekte wie z. B. Felder von float Werten oder selbst definierte Objekte nicht in Textform, sondern in einer kompakten binären Darstellung schreiben und lesen, so sind die Stream-Klassen einzusetzen. Besonders bei großen Objekten (Bildern, Musikstücke oder ähnliches) ist diese platzsparende Repräsentation angebracht. Falls erforderlich gibt es darüber hinaus Konverterklassen zwischen Byte- und Zeichenströmen: Bytestrom =⇒ InputStreamReader =⇒ Zeichenstrom Zeichenstrom =⇒ OutputStreamWriter =⇒ Bytestrom Im folgenden werden die grundlegenden Mechanismen zunächst anhand der Klassen Reader und Writer besprochen. 14.4 Reader Die Klasse Reader ist der Ausgangspunkt für alle zeichenbasierte Klassen zum Einlesen. Von der Klasse selbst können keine Instanzen erzeugt werden, sondern nur von den abgeleiteten Klassen. In der Klasse sind wesentliche Methoden zur Behandlung eines Eingangstroms definiert. Die wichtigsten sind: int read() Lesen eines Zeichens int read( char[] cbuf ) Lesen eines Feldes von Zeichen long skip( long n ) Überspringen von Zeichen void close() Schließen des Stroms boolean ready() Prüfen ob der Strom lesebereit ist Eine aus Reader abgeleitete Klasse ist FileReader. Damit kann man in einfacher Weise aus Dateien lesen. Wie jede abgeleitet Klasse implementiert sie alle Methoden von Reader und eventuell weitere. Bei einem der Konstruktoren wird der Dateinamen direkt angegeben. Die folgende Klasse benutzt diesen Konstruktor, um die Datei studenten.txt zu öffnen. Anschließend werden portionsweise Zeichen mit der Methode read eingelesen. Ein Rückgabewert von –1 signalisiert das Ende der Datei. 14.4. READER public class RWtest 143 { public void leseDatei() throws Exception { char[] cbuf = new char[20]; FileReader fr = new FileReader( "studenten.txt" ); int count = 0; for( ;; ) { int i = fr.read( cbuf ); if( i == -1 ) break; count += i; } System.out.println( count + " Zeichen gelesen"); } } Das Beispiel ist allerdings noch nicht optimal. Bei einem FileReader wird jede Leseaktion direkt ausgeführt. Dies kann zu häufigen Zugriffen auf die Festplatte führen. Besser ist es daher, den FileReader durch einen BufferedReader zu ergänzen. Ein BufferedReader übernimmt intern die Zwischenspeicherung von größeren Datenmengen und minimiert dadurch die Anzahl der Festplattenzugriffe. Außerdem implementiert die Klasse BufferedReader einige nützliche Methoden. 14.4.1 Schachtelung von Readern Die Klasse BufferedReader ist direkt aus Reader abgeleitet. Es handelt sich nicht etwa um eine aus FileReader abgeleitet Klasse. In dem Programm werden die beiden Reader hintereinander geschaltet. Anschaulich kann man sagen, der FileReader „fließt“ in den BufferedReader. Dementsprechend enthält der Konstruktor für den BufferedReader einen anderen Reader als Argument. Wir sehen hier ein Beispiel für den allgemeinen Mechanismus durch Kombination verschiedener Ströme ein gewünschtes Gesamtverhalten zu erzielen. Die entsprechend geänderte Methode main ist dann: public void leseDatei() throws Exception { char[] cbuf = new char[20]; FileReader fr = new FileReader( "studenten.txt" ); BufferedReader br = new BufferedReader( fr ); int count = 0; for( ;; ) { int i = br.read( cbuf ); if( i == -1 ) break; count += i; } 144 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN System.out.println( count + " Zeichen gelesen"); } Der Programmcode wird etwas eleganter, wenn man • keine expliziten FileReader einführt • die Methode readLine zum Lesen einer ganzen Zeile einsetzt • die for-Schleife mit break durch eine while-Schleife ersetzt Dann nimmt der Code folgende Form an: public void leseDatei() throws Exception { BufferedReader br = new BufferedReader( new FileReader( "studenten.txt" ) ); int count = 0; String text = null; while( (text = br.readLine() ) != null ) { count += text.length(); } System.out.println( count + " Zeichen gelesen"); } 14.4.2 Übersicht Reader Neben dem FileReader gibt es Klassen, um aus anderen Typen von Quellen zu lesen: Reader FileReader CharArrayReader StringReader PipedReader Quelle Datei Feld von Zeichen Zeichenkette Pipe Die Namen der Klassen sind weitgehend selbsterklärend. So liest ein StringReader aus einem im Konstruktor angegebenen String. Neben diesen nach ihren Quellen unterscheidbaren Klassen existieren weiter Klassen, die – ähnlich dem BufferedReader – erweiterte Funktionalitäten bereit stellen: Reader BufferedReader LineNumberReader PushBackReader Funktionalität Pufferung der Daten Zählen der Zeilennummern Möglichkeit, einzelne oder mehrere gelesene Zeichen wieder zurück in den Strom zu legen, um sie später nochmals zu lesen 14.5. WRITER 145 Ein LineNumberReader verfügt über die zusätzliche Methode getLineNumber(), um die aktuelle Zeilennummer abzufragen. In manchen Anwendungen sind PushBackReader praktisch. Man kann sozusagen ein Zeichen im Voraus lesen. Falls es nicht mehr zu dem aktuellen Element passt, legt man es zurück in den Eingangsstrom. Neben der Unterteilung nach Funktionalitäten kann man die verschiedenen Klassen auch als Klassenbaum darstellen (Zur besseren Übersicht auf zwei Bäume aufgeteilt): Reader StringReader PipedReader CharArrayReader Reader InputStreamReader BufferedReader FilterReader FileReader LineNumberReader PushbackReader PushBackReader ist abgeleitet aus der allgemeineren Klasse FilterReader. Diese Klasse dient auch als Ausgangspunkt für eigene Reader oder Filter. Ein Filter ist dabei ein Reader, dessen Eingang von einem anderen Strom gespeist wird. Man kann durch Überlagern der Methoden das gewünschte Verhalten in den eigenen Filter einbauen. Mögliche Anwendungen solcher Filter sind: • Datenkonversion • Datenverschlüsselung • Datenkompression 14.5 Writer Das entsprechende Gegenstück zur Klasse Reader ist die Klasse Writer. Der Klassenbaum hat folgendes Aussehen: Writer StringWriter PipedWriter CharArrayWriter 146 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN Writer OutputStreamWriter BufferedWriter FilterWriter PrintWriter FileWriter Wieder gibt es Methoden für die verschiedene Ziele • Files • Strings • Felder von char • Pipes Um in eine Datei zu schreiben benutzt man einen FileWriter. Im Konstruktor kann man direkt den Namen der Datei angeben: // Öffnen zweier Dateien zum Schreiben // Im zweiten Fall mit Anhängen an bestehenden Inhalt FileWriter fw, fwa; fw = new FileWriter( "test.txt" ); fwa = new FileWriter( "testplus.txt", true ); Falls erforderlich legt der Konstruktor eine neue Datei an. Der FileWriter kann wiederum zur besseren Performanz in einen BufferedWriter eingebettet werden. Damit lässt sich leicht unser Beispielprogramm zu einer Kopieranwendung erweitern: public void kopiereDatei() throws Exception { BufferedReader br = new BufferedReader( new FileReader( "studenten.txt" ) ); BufferedWriter bw = new BufferedWriter( new FileWriter( "copy.txt" ) ); int count = 0; // Zeichenzähler String text = null; while( (text = br.readLine() ) != null ) bw.write( text ); bw.newLine(); // Zeilenumbruch count += text.length(); } { 14.6. PRINTWRITER 147 System.out.println( count + " Zeichen gelesen"); bw.close(); // Datei ordentlich schließen } Dabei sind zwei Punkte zu beachten: 1. Bei dem zeichenorientierten Arbeiten geht der Zeilenumbruch verloren. Daher muss explizit ein Zeilenumbruch wieder eingefügt werden. Die Verwendung der Methode newLine() ist besser als das Anhängen von "\n". Diese Methode fügt die für die aktuelle Systemumgebung richtigen Zeichen zum Zeilenumbruch ein. 2. Der Strom muss mit close() geschlossen werden. Ansonsten besteht die Gefahr, dass der letzte Datenpuffer nicht geschrieben wird. Alternativ kann man mit der Methode flush() alle noch im Puffer befindlichen Daten schreiben. 14.6 PrintWriter Für eine formatierte Ausgabe von Daten könnte man selbst die Daten in Strings umwandeln und anschließend durch einen BufferedWriter ausgeben. Einfacher ist die Verwendung eines PrintWriters. Ein PrintWriter ist einem PrintStream wie System.out äquivalent. Er stellt für die primitiven Datentypen Methoden print und println bereit. Das folgende Beispiel benutzt einen PrintWriter, um die Werte der Sinus-Funktion in eine Datei zu schreiben. // Ausgabe einer Sinus-Schwingung in Datei PrintWriter pr = new PrintWriter( new FileWriter( "sin.txt" ) ); for( int i=0; i<1000; i++ ) { // Werte in Intervall [0, 2pi] abbilden double y = Math.sin( i / 1000. * 2 * Math.PI); pr.println( y ); } pr.close(); // alles sichern Die Datei hat dann den Inhalt 0.0 0.006283143965558951 0.012566039883352607 0.018848439715408175 0.02513009544333748 0.03141075907812829 0.03769018266993454 ... 148 14.7 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN Streams Parallel zu der Klassenhierarchie ausgehend von Reader und Writer gibt es weitgehend äquivalente Klassen für InputStream und OutputStream. Als Beispiel sei hier der Baum für InputStream angegeben (Aus Platzgründen aufgeteilt auf zwei Bäume und mit abgekürzten Klassennamen): InputStream PipedInputS. ByteArrayInputS. FileInputS. ObjectInputS. SequenceInputS. InputStream FilterInputStream PushbackInputS. LineNumberInputS. DataInputS. BufferedInputS. Im großen und ganzen findet man Klassen mit den gleichen Funktionalitäten wieder. 14.8 Random Access File Verbunden mit der Vorstellung von Datenströmen ist ein sequentieller Zugriff. Die Daten werden nacheinander gelesen oder geschrieben. Zu vielen Anwendungen passt allerdings das Konzept eines wahlfreien Zugriffs. Insbesondere bei großen Datenmenge ist es ineffizient, wenn man um zu einem bestimmten Datum zu kommen, ein große Anzahl von anderen Daten abarbeiten muss. Java stellt für solche Zwecke eine einfache Form vom Dateien mit wahlfreiem Zugriff – random access files – zur Verfügung. Man kann sich eine solche Datei wie ein großes Feld mit einem Positionszeiger vorstellen. Jede Lese- oder Schreibaktion wirkt auf die nächste Stelle hinter dem Positionszeiger. Nach der Aktion wird der Zeiger weiter geschoben. Mit entsprechenden Befehlen kann der Positionszeiger verändert werden. Damit kann gezielt eine ausgewählte Stelle angesprochen werden. Schreibt man an eine Stelle irgendwo in der Datei, so bleiben alle anderen Daten - vor und hinter der Position - erhalten. Die Klasse RandomAccessFile verfügt über Methoden zum Schreiben und Lesen der primitiven Datentypen. Die Daten werden in einer genormte 14.9. DIE KLASSE FILE 149 plattform-unabhängigen Darstellung abgelegt. Der Dateizeiger kann mit der Methode seek auf eine bestimmte Byte-Position gesetzt werden. Als Beispiel wird im folgenden eine Datei mit double Werten gefüllt. Anschließend werden ausgewählte Werte verändert. Zunächst wird eine Datei angelegt und sequentiell mit 1000 Werten der Sinus-Funktion gefüllt: // Datei anlegen und füllen RandomAccessFile raf = new RandomAccessFile( "sin.dat", "rw" ); for( int i=0; i<1000; i++ ) { double x = Math.sin( i / 1000. * 2 * Math.PI); raf.writeDouble( x ); } Im zweiten Schritt wird jeder 25. Wert auf 0 gesetzt. Alle anderen Werte bleiben erhalten. // jeden 25. Wert auf 0 setzen for( int i=0; i<1000; i+=25 ) { raf.seek( i * 8); // an Position gehen raf.writeDouble( 0. ); } Schließlich wird zur Kontrolle die gesamte Datei nochmals gelesen und die Werte werden in eine zweite Datei geschrieben. // Random Access File wieder lesen // und in neue Textdatei kopieren raf.seek(0); pr = new PrintWriter( new FileWriter( "sin2.txt" ) ); for( int i=0; i<1000; i++ ) { pr.println( raf.readDouble() ); } pr.close(); Die Klasse RandomAccessFile realisiert nur die einfachsten Operationen für einen wahlfreien Zugriff. So fehlen Möglichkeiten, um einzelne Einträge zu löschen oder an beliebiger Stelle einzufügen. Außerdem trägt der Anwender selbst die Verantwortung für die richtige Zuordnung der Bytes zu den Daten. Für anspruchsvolle Anwendungen ist der Einsatz einer Datenbank sinnvoll. 14.9 Die Klasse File Die Eigenschaften einer Datei lassen sich über die Methoden der Klasse File abfragen oder zu verändern. Weitere Methoden existieren, um Dateien zu erzeugen, zu löschen oder umzubenennen. Ein Schwerpunkt liegt auf einer abstrakten, 150 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN plattform-unabhängigen Darstellung des Dateinamens als so genannter abstrakter Pfadnamen. Aus dieser internen Darstellung werden durch Einfügen der aktuellen Trennzeichen konkrete Pfadnamen generiert. Der einfachste Konstruktor hat die Form File( String pathname ) wobei pathname den Namen der Datei einschließlich eventueller Pfadangaben enthält. Mit File f = new File( "sin2.txt" ); wird für die oben benutzte Datei ein File-Objekt angelegt. Der Pfad kann relativ zur aktuellen Position oder absolut angegeben werden. Bei WinXX muss das \Zeichen im Namensstring als \\ eingegeben werden. Ein gültiger Namen hat dann beispielsweise die Form c:\\Programme\Java\forte4j Dabei darf das Anlegen des File-Objektes nicht mit dem Anlegen einer Datei verwechselt werden. Der Aufruf des Konstruktors erzeugt lediglich ein Informationshülle für die eigentliche Datei. Damit kann man dann die Eigenschaften abfragen. Die Methode exists() testet, ob die Datei überhaupt vorhanden ist. Im positiven Fall liefert sie den Wert true zurück. Man kann dann verschiedene Eigenschaften der Datei abfragen: System.out.println("AbsolutePath=" System.out.println("exists =" System.out.println("Length =" System.out.println("canWrite =" System.out.println("canRead =" System.out.println("isFile =" System.out.println("is hidden =" System.out.println("isDirectory =" + + + + + + + + f.getAbsolutePath()); f.exists()); f.length()); f.canWrite()); f.canRead()); f.isFile()); f.isHidden()); f.isDirectory()); mit z. B. der Ausgabe: AbsolutePath=c:\Euler\java\small_tests\FileTest.class exists =true Length =2360 canWrite =true canRead =true isFile =true is hidden =false isDirectory =false 14.10. ÜBUNGEN 151 Verzeichnisse werden wie andere Dateien angegeben. In diesem Fall kann über die Methode list() ein Feld von Strings mit den Namen aller Dateien in diesem Verzeichnis abgefragt werden: if (f.isDirectory()) { String files[] = f.list(); for (int i=0; i<files.length; ++i) { System.out.println(" "+files[i]); } } Mit weiteren Methoden aus dieser Klasse kann man • Informationen über das Dateisystem und Trennzeichen abfragen • Eigenschaften von Dateien ändern • Dateien und Verzeichnisse erzeugen, umbenennen oder löschen • temporäre Dateien anlegen • Dateinamen in URIs oder URLs konvertieren In vielen Fällen – auch bei Methoden in anderen Klassen – kann ein File-Objekt anstelle eines Dateinamens verwendet werden. So gibt es beispielsweise in der Klasse FileWriter ansonsten gleichwertige Konstruktoren mit entweder einem Dateinamen als String oder einem File-Objekt als Argument. 14.10 Übungen Übung 14.1 Lesen aus einer Datei Ihre DVD-Sammlung ist in einer Datei festgehalten. In jeder Zeile steht der Name des Films und der Kaufpreis in der Art Dschungelbuch 17.99 Herr der Ringe, Teil 2 21.90 Vom Winde verweht 12.99 ... Schreiben Sie eine Methode int preise( String dateiName ) zum Lesen einer solchen Datei. Dabei soll eine kleine Statistik erstellt werden. Am Ende gibt die Methode die Meldung nn DVDs, Durchschnittspreis: xx, Gesamtpreis: yy mit den gefundenen Werten für nn, xx und yy aus. Rückgabewert ist die Anzahl der DVDs. 152 KAPITEL 14. EIN- UND AUSGABE UND DATEIEN Kapitel 15 Exception 15.1 Einleitung Bei der Ausführung einer Anwendung kann es zu Fehlern kommen. Neben Fehlern im Programm können auch äußere Umstände zu einem Fehlverhalten oder Programmabsturz führen. Häufige Ursachen sind: • Fehlbedienungen (z. B. falsche Eingaben) • unzureichende Ressourcen (z. B. zu wenig Speicher, unterbrochene Netzverbindung) Um ein Programm gegen derartige Fehler zu sichern, muss entsprechender Code eingefügt werden. Diese Aufgabe ist schwierig und aufwändig. Im konkreten Fall ist zu entscheiden: • kann ein Fehler repariert werden oder ist es sinnvoll, die Anwendung zu beenden? • wo sollte der Fehler behandelt werden? • an welcher Stelle soll das Programm fortgesetzt werden? Generell kann man zwischen zwei Strategien unterscheiden. 1. Der Fehler wird „an Ort und Stelle“ mit normalen Programmiermethoden behandelt. Ein Beispiel ist die Prüfung, ob eine Datei mit dem angegebenen Namen vorhanden ist. Falls nicht, wird der Benutzer erneut nach dem Namen gefragt. 2. Ein Fehler wird festgestellt, kann aber nicht behoben werden (z. B. Zugriff außerhalb der Grenzen eines Feldes). Daher wird der Fehler gemeldet und kann dann unabhängig vom normalen, linearen Programmfluss behandelt werden. 153 154 KAPITEL 15. EXCEPTION Java verfügt über ein flexibles Konzept zur Fehlerbehandlung. Damit ist es möglich, Fehler zu erkennen und entweder selbst zu behandeln oder an die aufrufende Instanz weiter zu melden. 15.2 Beispiel Die Laufzeitfehler werden in Java als Exceptions bezeichnet. Wie der Name andeutet, stellen sie Ausnahmen vom normalen Ablauf dar. Bisher hatten wir die Exceptions nicht behandelt sondern lediglich in den Methoden deklariert, dass sie eventuell einen solchen Ausnahmefehler hervor rufen könnten (to throw an exception). Betrachten wir ein Beispiel aus dem Szenario Autovermietung. Die Fahrzeuge sollen in einer Datei auto.txt mit dem Inhalt von beispielsweise Sharan Golf Jaguar 29999 19999 55000 2000 2001 1998 45000 36777 15666 eingetragen sein. Dann soll die Methode parseLine die einzelnen Zeilen analysieren: void parseLine( String zeile ) throws Exception { StringTokenizer st = new StringTokenizer( zeile ); name = st.nextToken(); kaufPreis = Integer.parseInt( st.nextToken() ); kaufJahr = Integer.parseInt( st.nextToken() ); kilometer = Integer.parseInt( st.nextToken() ); } Mit der Angabe throws Exception wird dem Compiler mitgeteilt, dass in der Methode ein Ausnahmefehler auftreten kann. Dieser Fehler wird von der Methode selbst nicht behandelt, sondern an die aufrufende Methode gemeldet. Auf diese Art und Weise können die Fehler von allen übergeordneten Methoden einschließlich main weiter gereicht werden. Dann übernimmt die Java-Maschine die Behandlung. Fügt man in die Datei auto.txt eine ungültige Zeile in der Art Sharan 29999 2000 45000 Golf 19999 2001 36777 ungültig 15000 Jaguar 55000 1998 15666 ein, so tritt in dieser Zeile bei dem dritten Aufruf von nextToken ein Fehler auf. Die normale Ausführung wird dann unterbrochen und die Exception bis zur JavaMaschine durchgereicht. Diese gibt dann eine Fehlermeldung mit der Ursache und dem Ort aus: 15.3. TRY - CATCH ANWEISUNG 155 java.util.NoSuchElementException at java.util.StringTokenizer.nextToken( StringTokenizer.java:235) at Auto.parseLine(Auto.java:37) at Verleih.ausDatei(Verleih.java:36) at Verleih.main(Verleih.java:66) 15.3 try - catch Anweisung Alternativ zu der Weitergabe kann man selbst Code zur Behandlung solcher Fehler bereit stellen. Dies geschieht mittels einer try - catch Anweisung. Die Anweisung muss die kritische Stelle umfassen, kann aber ansonsten frei platziert werden. Entweder sie steht in der Methode selbst oder in einer der aufrufenden Methoden. Generell gilt, dass Fehler behandelt werden müssen. Entweder die Methode kümmert sich selbst darum oder gibt den Fehler weiter (catch-or-throw Regel). Tritt zur Laufzeit ein Fehler auf, so sucht die Java-Maschine nach einem entsprechenden catch-Teil. Die Suche beginnt in der aktuellen Methode. Wird dort nichts gefunden, so wird die Suche in der aufrufenden Methode fortgesetzt. Die Suche wird weitergeführt, bis die oberste Ebene main erreicht ist. Ist auch dort keine Behandlung vorgesehen, wird die Anwendung beendet und eine Meldung ausgegeben. Kriterium für die Platzierung sollte sein: „Kann man an dieser Stelle den Fehler angemessen behandeln? “ In dem Beispiel kann die Methode parseLine wenig mit dem Fehler anfangen. Es ist sinnvoll, den Fehler „nach oben“ weiter zu reichen. Mit folgendem Code wird der Fehler in main behandelt: String datei = "autos.txt"; try { System.out.println( "Datei <" + datei + "> lesen" ); v.ausDatei( datei ); } catch ( Exception ex ) { System.out.println( "Fehler beim Lesen <"+datei+">" ); System.out.println( "Exception : " + ex ); System.exit(0); } Zunächst kommt ein try-Block, der alle kritischen Anweisungen enthält. Eventuelle Fehler werden in einem direkt anschließenden catch-Block aufgefangen. Genauer gesagt unterbricht ein Fehler die normale Ausführung in dem try-Block und die Anwendung springt an den Anfang des catch-Blocks. Der Block wird durch das Schlüsselwort catch sowie einen formalen Parameter eingeleitet. In dem Beispiel ist als formaler Parameter eine allgemeine Exception angegeben. Wie bei einem Methodenaufruf übernimmt der Block bei der Ausführung ein 156 KAPITEL 15. EXCEPTION konkretes Objekt. In diesem Exception-Objekt sind Informationen zu dem aufgetretenen Fehler gespeichert. Die Methode toString (hier implizit durch die Verkettung der Zeichenketten aufgerufen) liefert eine Textinformation zu der Art des Fehlers. Dann resultiert folgende Ausgabe: Datei <autos.txt> lesen Sharan: gekauft im Jahr 2000 für 29999 Euro Kilometerstand: 45000 Golf: gekauft im Jahr 2001 für 19999 Euro Kilometerstand: 36777 Fehler beim Lesen aus Datei <autos.txt> Exception : java.util.NoSuchElementException Nach dem Ende des catch-Blocks wird die Anwendung an dieser Stelle („hinter“ der try - catch Anweisung) fortgesetzt. Es gibt keine Möglichkeit, an die Stelle zurück zu gehen, an der der Fehler auftrat. Für weiter gehende Informationen kann auch die genaue Position des Fehlers angezeigt werden. Die Methode printStackTrace() gibt die entsprechende Information auf dem Standard Error Stream aus. In dem Beispiel sieht die Meldung folgendermaßen aus: java.util.NoSuchElementException at java.util.StringTokenizer.nextToken(StringTokenizer.java:235) at Auto.parseLine(Auto.java:37) at Verleih.ausDatei(Verleih.java:36) at Verleih.main(Verleih.java:97) Angezeigt wird der komplette Stapel (Stack) mit Methodenaufrufen, wie er zum Zeitpunkt des Fehlers vorlag. Hilfreich ist die Angabe der Zeilennummern, mit deren Hilfe die kritische Stelle im Quellcode sofort nachgeschaut werden kann. Die Syntax der try - catch Anweisung erlaubt eine Trennung zwischen „normalem“ Code und Code zur Fehlerbehandlung. Es ist möglich, längere Code-Stücke zusammenhängend zu schreiben und die Fehlerbehandlung für den gesamten Abschnitt daran anzuschließen. Damit wird die Lesbarkeit stark verbessert. Den catch-Block bezeichnet man nach seiner Funktion auch als Exception Handler. 15.4 Hierarchie von Ausnahmefehlern In einer Anwendung können verschiedenste Fehler auftreten. So kann zum Beispiel die Methode readLine der Klasse LineNumberReader eine IOException verursachen. Die Dokumentation spezifiziert für jede Methode, welche Arten von Fehlern auftreten können. Die Fehler - genauer gesagt die Klassen zur Beschreibung der Fehler - sind hierarchisch organisiert. Die Basisklasse für alle Fehler ist Throwable. Nur Instanzen dieser Klasse oder daraus abgeleiteter Klassen können als Parameter an den catch-Block übergeben werden. Danach verzweigt sich der Ableitungsbaum zu den zwei Klassen Error und Exception. 15.4. HIERARCHIE VON AUSNAHMEFEHLERN 15.4.1 157 Die Klasse Error In dieser Klasse sind alle schwerwiegenden Fehler enthalten. Normalerweise wird eine Anwendung diese Fehler nicht behandeln. Beispiele sind VirtualMachineError oder StackOverflowError. In diesen Fällen treten schwerwiegende Problem mit der Java-Maschine oder den benötigten Klassen auf. Eine Anwendung hat in solchen Fällen wenig Möglichkeiten zur Reaktion. Fehler der Klasse Error brauchen nicht mit throws deklariert zu werden. 15.4.2 Die Klasse Exception Unter dieser Oberklasse finden sich alle Fehler, die eine Anwendung sinnvollerweise behandeln kann. Die Fehler sind in mehreren Ebenen in Unterklassen organisiert. Als Beispiel hat FileNotFoundException folgenden Ableitungsbaum: java.lang.Object java.lang.Throwable java.lang.Exception java.io.IOException java.io.FileNotFoundException Für die Exception greift der Mechanismus Polymorphismus. Eine speziellere Exception kann einer allgemeineren Exception zugewiesen werden. Wir haben bisher dieses Verhalten ausgenutzt, indem wir stets die allgemeinste Klasse Exception spezifiziert hatten. Damit wurden die verschiedenen speziellen Fehler aufgefangen. Auch wenn z. B. die angegebene Datei nicht existiert und damit eine FileNotFoundException ausgelöst wurde, so wurde dieser Fehler in dem allgemeinen catch-Block aufgefangen. Wir können den catch-Block spezialisieren, indem wir gezielt eine Unterklasse angeben. Mit } catch ( NoSuchElementException ex ) { ... } wird nur noch diese spezielle Fehlerklasse (und eventuelle Unterklassen) behandelt. In einer try - catch Anweisung können mehrere catch-Blöcke für verschieden Fehlerklassen stehen. Beispiel: try { System.out.println( "Datei <" + datei + "> lesen" ); v.ausDatei( datei ); } catch ( NoSuchElementException ex ) { System.out.println("Fehler beim Lesen: <"+ datei +">" ); System.exit(0); 158 KAPITEL 15. EXCEPTION } catch ( FileNotFoundException ex ) { System.out.println("Datei <"+datei+"> nicht gefunden" ); System.exit(0); } Dabei gelten folgende Regeln: • Jeder catch-Block kann nur eine Klasse behandeln. • Speziellere Handler müssen vor allgemeineren stehen (wird durch Compiler geprüft). • Wenn die Methode dies mit throws ... deklariert, brauchen nicht alle Fehler behandelt zu werden. Entsprechendes gilt für die Deklaration throws bei einer Methode. Anstelle des allgemeinen throws Exception können die möglichen Fehler einzeln benannt werden. Mehrere Fehlerklassen werden durch Komma getrennt. 15.4.3 Die Klasse RuntimeException Eine besondere Rolle spielt die Fehlerklasse RuntimeException. Dies sind Fehler, die nahezu überall auftreten können. Darunter fallen • nicht initialisierte Referenzen: NullPointerException • Zugriffe außerhalb eines Feldes: IndexOutOfBoundsException • arithmetische Fehler wie Division durch Null: ArithmeticException Es wäre sehr aufwendig und wenig hilfreich überall diese Fehler als möglich zu deklarieren. Daher sind alle Fehlerklassen, die auf RuntimeException basieren, von der catch-or-throw Regel ausgenommen. Sie können allerdings auch explizit behandelt werden. Ansonsten werden sie von der Java-Maschine angezeigt. Beispiel 15.1 Zugriff außerhalb eines Feldes int[] feld = new int[20]; try { feld[40] = 4; } catch( Exception ex ) { System.out.println( "Exception : " + ex ); } ergibt Exception : java.lang.ArrayIndexOutOfBoundsException 15.5. EIGENE EXCEPTIONS 15.5 159 Eigene Exceptions In den Exception-Mechanismus können eigene Fehlertypen integriert werden. Ein Ausnahmefehler wird mit dem Befehl throw ausgelöst. Im einfachsten Fall benutzt man eine der vorhandenen Fehlerklassen und trägt einen eigenen Meldungstext ein. So könnte eine Sicherheitsabfrage über die korrekte Preiseingabe in der Form if( kaufPreis < 0 ) throw new Exception("Falscher Preis"); eingebaut werden. Im Fehlerfall würde die Exception von dem allgemeinen catchBlock gefangen werden: Allgemeiner Fehler bei Datei <autos.txt> Exception : java.lang.Exception: Falscher Preis Weiterhin ist es möglich, eigene Fehlerklassen zu erzeugen. Ein Beispiel ist public class FalschesJahrException extends java.lang.Exception { public FalschesJahrException() { } public FalschesJahrException(String msg) { super(msg); } } und if( kaufJahr < 1900 | kaufJahr > 2002 ) { throw new FalschesJahrException( "Jahr " + kaufJahr ); } mit dem Ergebnis Allgemeiner Fehler bei Datei <autos.txt> Exception : FalschesJahrException: Jahr 12000 15.6 finally-Block Eine try - catch Anweisung kann mit einen optionalen finally-Block abgeschlossen werden. Der Block steht nach dem letzten catch-Block. Unabhängig davon, wie die Anweisung verlassen wird – normal oder durch einen Ausnahmefehler – wird der finally-Block ausgeführt. Dies gilt selbst dann, wenn der try-Block durch return, break oder continue vorzeitig beendet wird oder die Exception nur weiter gereicht wird. Damit ist der finally-Block der ideale Platz für allgemeine „Aufräumarbeiten“. Betrachten wir folgendes Beispiel: 160 KAPITEL 15. EXCEPTION try { lnr = new LineNumberReader( new FileReader( datei ) ); for( int i=0; i<autos.length; i++ ) { autos[i] = new Auto(); autos[i].parseLine( lnr.readLine() ); } } catch (Exception ex ) { System.out.println( "Fehler beim Lesen <" +datei+ ">" ); return -1; } Angenommen die Datei wird zwar erfolgreich geöffnet, aber beim Lesen der Zeilen kommt es zu einem Fehler. In diesem Fall wird der try-Block abgebrochen und der catch-Block ausgeführt. Die Datei würde aber geöffnet bleiben. Durch eine Block in der Art finally { System.out.println( "Datei schliessen" ); lnr.close(); } kann sichergestellt werden, dass die Datei geschlossen wird. Der finally-Block wird in jedem Fall ausgeführt. Durch diese Konstruktion braucht der Code zum gesicherten Freigeben von Ressourcen nur einmal geschrieben zu werden. Bei umfangreichem Code wird das Programm dadurch übersichtlicher und eine Fehlerquelle wird vermieden. 15.7 Übungen Übung 15.1 Gegeben sei der folgende Code: // Klasse ExTest import java.util.*; import java.io.*; public class ExTest { public static void main(String[] args) throws Exception{ BufferedReader din = new BufferedReader( new InputStreamReader( System.in ) ); for( ;; ) { System.out.print( "> " ); 15.7. ÜBUNGEN 161 // Zeile einlesen String text = din.readLine(); test( text ); } } static void test( String zeile ) { String[] teile = zeile.split(" "); for(int i = 0; i<teile.length; i++ ) { int wert = Integer.parseInt( teile[i] ); System.out.println( i + ". wert: " + wert ); } } } • Welche Fehler können bei geänderter Eingabe auftreten? • Behandeln Sie diese Fehler mittels try-catch Blocks in main. • Falls eine Zahl den Wert 0 annimmt, soll ein neuer Ausnahmefehler angezeigt werden. Implementieren Sie dazu eine Klasse WertNullException. 162 KAPITEL 15. EXCEPTION Kapitel 16 Dynamische Datenstrukturen Diese Beschreibung bezieht sich auf traditionelle Collections, die es seit JDK 1.0 gibt. Seit 1.2 gibt es ein neues Collection-API. Das Kapitel müsste aktualisiert werden. Es fehlen insbesondere Generische Datentypen in der Art Vector<SpielObjekt> gegenstaende = new Vector<SpielObjekt>(); und Autoboxing. 16.1 16.1.1 Vector Konstruktor und Einfügen von Elementen Eine Realisierung einer linearen Liste ist die Klasse Vector. Die wesentlichen Eigenschaften im Unterschied zu einem Feld sind: • dynamisches Wachstum • Einfügen und Löschen von Elementen Im einfachsten Fall wird ein leerer Vektor mit dem parameterlosen Konstruktor erzeugt: Vector v = new Vector(); Elemente können mit verschiedenen Methoden eingefügt werden. Die wichtigsten sind: • boolean add(Object o) Anhängen eines Elementes • void add(int index, Object element) Einfügen eines Elementes an der angegebenen Stelle • Object set(int index, Object element) Ersetzen des Elementes an der angegebenen Stelle 163 164 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN Daneben stehen Methoden zur Verfügung, um mehrere Elemente beispielsweise aus einem Feld gleichzeitig einzufügen. Die Elemente müssen nicht vom gleichen Typ sein. Ein Vector kann gleichzeitig Objekte unterschiedlicher Klassen enthalten. In unserer Beispielsapplikation bietet es sich an, die Autos des Verleihs in einem Vektor zu speichern. Der neue Code hat dann folgendes Aussehen: public class Verleih2{ Vector autos = new Vector(); ... public void ausDatei( String datei ) throws Exception { LineNumberReader lnr; lnr = new LineNumberReader(new FileReader(datei) ); String zeile; while( (zeile = lnr.readLine()) != null ) { Auto a = new Auto(); a.parseLine( zeile ); autos.add( a ); } lnr.close(); } ... Jetzt ist es möglich, die Datei einzulesen und direkt die Elemente in den Vektor abzulegen. Der Vektor wächst nach Bedarf. 16.1.2 Zugriff auf Elemente Auf die Elemente in einem Vektor kann entweder über den Index oder sequentiell zugegriffen werden. Die Verwendung des Index betont die Analogie zum Feld. Einige Methoden dazu sind • Object firstElement() Liefert das erste Element. • Object lastElement() Liefert das letzte Element. • Object get(int index) Liefert das Element an der angegebenen Stelle. Die Methoden geben jeweils Objekte der Basisklasse Object zurück. Gemäß dem Polymorphismus kann darin ein beliebiges Objekt enthalten sein. Zur speziellen Verwendung muss das Objekt mit einem Cast auf den gewünschten Typ gewandelt werden. Als Element in einem Vektor verliert ein Objekt seine spezielle Klassenzuordnung. Es liegt in der Verantwortung des Programmierers bei der Entnahme die richtige Zuordnung zu treffen. In Zweifelsfällen kann dazu die 16.1. VECTOR 165 Klasseninformation eines Objektes abgefragt werden (z. B. über instanceof oder die Methode toString() ). Fehlerhafte Zuweisungen führen zur Laufzeit zur einer Exception. Solche Fehler können aber noch nicht zur Compilezeit detektiert werden. Eine Schleife zur Ausgabe des Vektors mit Autos ist void printAutos() { for( int i=0; i<autos.size(); i++ ) { ( (Auto) autos.elementAt(i)).print(); } } Die Elemente werden in einer Schleife geholt, in den Typ Auto gewandelt und dann ausgegeben. Die Bedingung in der Schleife nutzt die Methode public int size() zur Abfrage der Größe des Vektors. Elemente können auch wieder aus dem Vektor entfernt werden: • Object remove(int index) Löscht das Element an der angegebenen Stelle und gibt es zurück. • boolean remove(Object o) Löscht das angegebene Objekt. Der Rückgabewert informiert, ob ein solches Objekt gefunden wurde. • void removeAllElements() Löscht alle Elemente. Der Aufruf der Methode toString() liefert eine Textrepräsentation des Vektors mit einer Liste aller Elemente: System.out.println( "Vektor autos: " + autos); Vektor autos: [Auto@2e000d, Auto@55af5, Auto@169e11] 16.1.3 Iterator Bei der Klasse Vector hatten wir eine Ausgabe aller Elemente mit einer forSchleife über alle Indices realisiert. Bei anderen Strukturen ist die Reihenfolge weniger leicht zugänglich. Java bietet daher ein allgemeines Konzept für sequentielle Abfragen. Bei Vektoren wird dies durch das Interface Enumeration realisiert. Eine Enumeration liefert ähnlich wie ein StringTokenizer nach der Initialisierung ein Element nach dem anderen. Für den Vektor autos lässt sich schreiben Enumeration en = autos.elements(); while( en.hasMoreElements() ) { ( (Auto) en.nextElement() ).print(); } 166 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN oder kompakt mit einer for-Schleife for(Enumeration e=autos.elements(); e.hasMoreElements();) { ( (Auto) e.nextElement() ).print(); } Der Aufruf autos.elements() liefert eine Enumeration (genauer gesagt eine anonyme Klasse aus Vector, die dieses Interface implementiert). Anschließend werden die Elemente nacheinander ausgegeben. Die Abfrage, ob noch weitere Elemente vorhanden sind, erfolgt durch die Methode hasMoreElements(). 16.1.4 Wrapper Klassen Vektoren können beliebige Objekte aufnehmen. Es ist allerdings nicht möglich, primitive Datentypen direkt in einen Vektor zu speichern. Die Anweisung v.autos.add( 5 ); // geht in alten Java Versionen nicht liefert einen Fehler beim Kompilieren (no method found . . . ). Dies ist ein Beispiel für einen Kontext, in dem Objekte benötigt werden. Für solche Fälle existieren die sogenannten Wrapper-Klassen. Sie bilden eine Objekt-Hülle um einen primitiven Datentyp. Wir hatten Wrapper-Klassen bereits im Kapitel 14 kennen gelernt. Die Namen der Klassen entsprechen im wesentlichen denen der primitiven Datentypen und werden konventionsgemäß mit einem großen Anfangsbuchstaben gebildet. Mit einem primitiven Datenwert als Parameter erzeugt der Konstruktor ein passendes Klassenobjekt. Diese Klassenobjekte können dann als Elemente in Vektoren eingebaut werden: Integer wi = new Integer(5); v.autos.add( wi ); Aus den Klassenobjekte können die Daten mit Zugriffsmethoden in der Form typeValue() extrahiert werden. Für das Beispiel kann man schreiben int i = wi.intValue(); Autoboxing übernimmt diese Arbeit! Vector<Integer> v = new Vector<Integer>(); v.add( 5 ); Integer I = 7; v.add( I ); System.out.println( v ); 16.1. VECTOR 16.1.5 167 Stack Ein Stapelspeicher (engl. Stack ) ist ein Speicher, bei dem Werte nur am Anfang angefügt oder entnommen werden können. Anschaulich kann man sich die Werte übereinander liegend wie bei einem Stapel Spielkarten oder Mensatabletts vorstellen. Man kann jeweils eine weitere Karte bzw. ein weiteres Tablett auflegen oder vom Stapel nehmen. Diese beiden Operationen nennt man push (Auflegen) und pop (Wegnehmen). Charakteristisch für ist, dass die Elemente in umgekehrter Reihenfolge entnommen werden (Last In First Out, LIFO). Aus der Klasse Vector ist die Klasse Stack abgeleitet. Sie stellt vier Methoden bereit, die das Verhalten eines Stapels charakterisieren: • Object push(Object item) Legt ein Objekt auf den Stapel. • Object pop() Nimmt das oberste Objekt vom Stapel. • Object peek() Liefert das oberste Objekt ohne es zu entnehmen. • boolean empty() Testet, ob der Stapel leer ist. Das folgende Beispiel illustriert das Verhalten eines Stapels: public class StackTest { public static void main (String args[]) { Stack st = new Stack(); st.push( "Eins" ); st.push( "Zwei" ); st.push( "Drei" ); while( ! st.empty() ) { System.out.println( st.pop() ); } } } Ausgabe: Drei Zwei Eins Es sei betont, dass die Klasse Stack die Klasse Vector erweitert. Alle Methoden von Vector werden geerbt. Damit kann auf einen Stack wie auf einen Vector zugegriffen werden. 168 16.2 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN Assoziativspeicher In vielen Anwendungsfällen kann man einen Datensatz als ein Paar von Schlüssel (Suchbegriff) und Wert sehen. Beispiele sind: • Wörterbücher • Telefonbücher • Abkürzungsverzeichnisse Charakteristisch ist, dass der Zugriff in der Regel über einen Schlüssel erfolgt. Die Daten sind dann so organisiert, dass die Suche in den Schlüsseln schnell erfolgen kann. Bei Telefonbüchern sind die Schlüssel die Namen. Die Einträge sind alphabetisch sortiert, so dass man einen Namen rasch findet. Die umgekehrte Suche – der Name zu einer Nummer – ist demgegenüber sehr aufwändig. Da diese Art von Anwendung häufig vorkommt, lohnt sich der Einsatz von dafür optimierten Datenstrukturen. Einige Programmiersprachen stellen dazu assoziative Felder bereit. Bei dieser Art von Feldern erfolgt der Zugriff nicht über einen Index sondern direkt über einen Schlüssel. In der Sprache Perl beispielsweise werden solche Felder unmittelbar durch die Schlüssel adressiert. Das folgende Beispiel Telefonbuch zeigt die Syntax: # Aufbau des Telefonbuchs $dict{"Michael Maier"} = $dict{"Yvonne Schmidt"} = $dict{"Pizzeria"} = $dict{"Joachim"} = $dict{"Jörg"} = $dict{"Dekanat MND"} = "0788 888 999"; "0179 444 234"; "876878"; "76543"; "12345"; "07887 77889"; # Beispiel für einen Zugriff $key = "Joachim"; print "$key hat die Telefonnummer $dict{$key} \n"; In Java ist dieses Sprachelement nicht enthalten. Statt dessen gibt es eine Reihe von Klassen zur effizienten Speicherung solcher Daten. 16.2.1 Hashtable Die Datenstruktur Hash Table (hash, engl. für zerhacken, vermischen) ist für einen schnellen Zugriff über einen Schlüssel gedacht. Die Grundidee ist, aus dem Schlüssel einen Index – d. h. eine Zahl in einem vorgegebenen Intervall – zu berechnen. Dieser Index dient dann zum schnellen Zugriff auf den Wert. Eine Voraussetzung dabei ist die Eindeutigkeit des Schlüssels. Zu einem Schlüssel darf es 16.2. ASSOZIATIVSPEICHER schlüssel1 169 schlüssel2 $ index / W ert ? index / W ert index / W ert index / W ert index / W ert index / W ert Abbildung 16.1: Aufbau einer Hashtable nur einen Wert geben. Zum Beispiel darf das Telefonbuch nicht zwei identische Namen mit unterschiedlichen Nummern enthalten. Eine Hash Table besteht aus zwei Komponenten: • einem Feld mit linearer Adressierung • einer Hash-Funktion zur Abbildung der Schlüssel auf Indices Dieser Aufbau ist in Bild 16.1 dargestellt. Die Hash-Funktion berechnet aus einem Objekt eine Adresse aus dem vorgegebenen Bereich (Hash-Code). Dazu wird zunächst das Objekt in eine Zahl umgewandelt. Beispielsweise kann man bei Zeichenketten die Zahlenwerte der einzelnen Zeichen addieren. Da diese Zahl im Allgemeinen außerhalb des Adressbereichs liegen kann, wird sie noch auf diesen Bereich abgebildet. Dies kann mit der Modulo-Funktion geschehen. Wichtig für die Hash-Funktion ist: • kurze Rechenzeit • möglichst gleichmäßige Verteilung der erzeugten Werte Ein Vorteil ist, dass die Größe des Feldes keinen Einfluss auf die Rechendauer bei der Bestimmung des Hash-Codes hat. Damit ist die Zugriffszeit weitgehend unabhängig von der Größe der Tabelle. Mit anderen Worten: eine Hash Table bietet eine näherungsweise konstante Zugriffszeit. Verschiedene Objekte können allerdings den gleichen Hash-Code ergeben. Daher müssen Strategien zur Behandlung von mehreren Objekten mit identischem Hash-Code implementiert werden. Eine Möglichkeit besteht darin, an jedem Eintrag in dem Feld nicht nur ein einzelnes Objekt sondern eine Liste aller Objekte mit dem zugehörigen Hash-Code einzubauen. Die Hash-Funktion übernimmt dann die Vorauswahl der Listen. Wichtig für die Performanz einer Hash Table ist die Größe des Feldes. Ist das Feld zu klein, so teilen sich zu viele Objekte den gleichen Hash-Code. Ist umgekehrt das Feld zu groß, so wird Speicherplatz verschwendet. Die Implementierung 170 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN in Java verwendete eine Standardgröße von 11 beim Anlegen des Feldes. Erreicht die Tabelle einen bestimmten Füllstand, so wird das Feld automatisch vergrößert und die Einträge entsprechend neu angeordnet. Mit der Methode Object put(Object key, Object value) werden SchlüsselWert-Paare in eine Tabelle eingetragen. War bereits ein Wert zu diesem Schlüssel vorhanden, so wird der alte Wert zurück gegeben und gleichzeitig durch den neuen ersetzt. Ansonsten ist der Rückgabewert null. Mit Object get(Object key) wird der Wert zu einem Schlüssel abgefragt. Die Implementierung ist für einen schnellen Zugriff über einen Schlüssel gedacht. Allerdings stellt die Klasse auch Methoden für den langsameren Zugriff auf die Werte zur Verfügung. So erhält man mit elements() einen Iterator über alle Werte. Ein Beispiel für den Einsatz der Klasse zeigt die folgende Implementierung eines Telefonbuchs. import java.io.*; import java.util.*; public class HashTest extends Object { public static void main (String args[]) throws Exception{ // Anlegen Hashtable telefonBuch = new Hashtable(); // füllen telefonBuch.put( telefonBuch.put( telefonBuch.put( telefonBuch.put( telefonBuch.put( telefonBuch.put( "Michael Maier", "0788 888 999" ); "Yvonne Schmidt","0179 444 234" ); "Pizzeria", "876878" ); "Joachim", "76543" ); "Jörg", "12345" ); "Dekanat MND", "07887 77889" ); // abfragen BufferedReader br = new BufferedReader( new InputStreamReader( System.in ) ); for( ;; ) { System.out.print( ">" ); String name = br.readLine(); if( name.length() == 0 ) break; String nummer = (String) telefonBuch.get(name); if( nummer == null ) { nummer = "Kein Eintrag gefunden"; 16.2. ASSOZIATIVSPEICHER } System.out.println( name + ": " + 171 nummer ); } System.out.println( "ENDE" ); } } Ein anderes Anwendungsbeispiel nutzt eine Hash Table, um die Häufigkeit von Zeichenketten zu zählen. Hashtable ht = new Hashtable(); for( ;; ) { System.out.print( ">" ); String text = br.readLine(); if( text.length() == 0 ) break; if( ht.containsKey( text ) ) { // schon in Tabelle? // Zähler erhöhen und neuen Stand eintragen int count = ((Integer) ht.get(text)).intValue() + 1; ht.put( text, new Integer( count )); System.out.println( text + ": " + count ); } else { ht.put( text, new Integer(1)); } } 16.2.2 Properties Aus Hashtable abgeleitet ist die Klasse Properties. Sie ist auf die Aufnahme von Zeichenketten spezialisiert. Wie bei Hashtable erfolgt der Zugriff über einen Schlüssel. Mit dem Methodenpaar public Object setProperty(String key, String value) public String getProperty(String key) werden Eigenschaften gesetzt und gelesen. Im folgenden Beispiel werden einige von der Klasse System gelieferte Eigenschaften dargestellt: import java.util.*; import java.io.*; public class TestProp { public static void main (String args[]) throws Exception{ Properties prop = System.getProperties(); 172 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN System.out.print( "Running under " + prop.getProperty("os.name") ); System.out.print( " Version " + prop.getProperty("os.version") ); System.out.print( " Architecture " + prop.getProperty("os.arch") ); System.out.println( ); } } Mit der Klasse Properties steht eine Möglichkeit zur Behandlung von Eigenschaften für eine Anwendung zur Verfügung. Dazu dienen insbesondere die Methoden load und save, um solche Eigenschaften einzulesen oder in eine Datei zu speichern. Die Ergänzung prop.save(new FileOutputStream( "props.txt"), "System Properties"); speichert die in der Klasse TestProp geladenen Systemeigenschaften in die Datei props.txt. Das zweite Argument wird als Kopf eingetragen. Die Eigenschaften werden in der Form Schlüssel =Wert zeilenweise geschrieben. In meinem Beispiel beginnt die Datei mit #System Properties #Mon Dec 02 11:53:03 CET 2002 java.runtime.name=Java(TM) 2 Runtime Environment, Standard Edition sun.boot.library.path=C\:\\PROGRA~1\\java\\JDK13~1.1\\jre\\bin java.vm.version=1.3.1-b24 java.vm.vendor=Sun Microsystems Inc. java.vendor.url=http\://java.sun.com/ path.separator=; java.vm.name=Java HotSpot(TM) Client VM 16.2.3 Bäume Mit der Klasse TreeMap steht auch eine Klasse für die Speicherung von SchlüsselWerte-Paaren in Bäumen zur Verfügung. Die Zugriffe über put und get sind analog zur Klasse Hashtable. Für weitere Details sei auf die Java-Dokumentation verwiesen. 16.3. METHODEN IN DER KLASSE COLLECTIONS 16.3 173 Methoden in der Klasse Collections Die Klasse Collections enthält eine ganze Reihe von statischen Methoden zum Arbeiten mit Datenstrukturen. Unterschieden wird dabei zwischen Klassen, die die beiden Interfaces • Collection - beliebigen Sets • List - Liste mit Reihenfolge implementieren. Unter anderem handelt es sich um Methoden zum • Sortieren • Suchen nach Minimum oder Maximum • Umordnen • Füllen mit Elementen Einige dieser Operationen sind nur für Listen mit einer Reihenfolge sinnvoll. So ist etwa für eine Hashtable das Sortieren nach Reihenfolge nicht angebracht. 16.3.1 Beispiel Kartenspiel Als eine Anwendung auf der Basis der besprochenen Klassen betrachten wir eine Klasse zum Austeilen von Spielkarten. Folgende Anforderungen soll es erfüllen: 1. Kartenspiel mit 32 Karten 2. Austeilen für verschiedene Spiele (d. h. unterschiedlich viele Hände mit unterschiedlich vielen Karten) 3. Sortieren der Karten Die verschiedenen Hände werden jeweils durch einen Vektor mit Karten realisiert. Das Mischen und Zuordnen der Karten soll in einer Methode austeilen erfolgen. Diese Methode gibt die Karten aufgeteilt in mehreren Vektoren zurück. Diese Vektoren werden in einem Feld zusammen gefasst. Umgekehrt erfolgt die Vorgabe, wie viele Hände mit jeweils wie vielen Karten ausgeteilt werden sollen, über ein Feld als Parameter. Dieses Feld enthält für jede Hand die Anzahl der Karten. Insgesamt erhält man dann für die Methode den Kopf Vector[] austeilen( int[] kartenProHand ) Für das Skatspiel hat das übergebene Feld die Form // 3 Spieler mit je 10 Karten, 2 Karten im Skat 174 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN int[] kartenProHandSkat = {10,10,10,2}; Zur internen Darstellung der Karten wird ebenfalls ein Vektor benutzt. Die Namen der Farben und Kartenwerte sind in Felder gespeichert. Der Konstruktor baut dann daraus einen Vektor mit allen Karten auf: String[] farben = {"Kreuz", "Pik", "Herz", "Karo" }; String[] werte = {"As", "König", "Dame", "Bube", "10", "9", "8", "7"}; Vector kartenWerte = new Vector(); public Karten() { /** Karten anlegen*/ for( int i=0; i<farben.length; i++ ) { for( int j=0; j<werte.length; j++ ) { kartenWerte.add( farben[i] + "_" + werte[j] ); } } } In dem Vektor liegen dann die Karten als Zeichenketten in der Form Farbe_Wert vor. Mit der Methode Collections.shuffle lässt sich dann Austeilen wie folgt realisieren: Vector[] austeilen( int[] // Karten kopieren und Vector v = new Vector( Collections.shuffle( v kartenProHand ) { mischen kartenWerte ); ); // aufteilen Vector[] vfeld = new Vector[kartenProHand.length]; int index = 0; for( int i=0; i<kartenProHand.length; i++ ) { vfeld[i]= new Vector( v.subList(index, index+kartenProHand[i]) ); index += kartenProHand[i]; } return vfeld; } Die Methode subList aus dem Interface List gibt einen Ausschnitt aus einer Liste (z. B. einem Vektor) zurück. Zusammen mit der Möglichkeit, in dem Konstruktor für Vector einen anderen Vector mitzugeben, gestatten diese Methoden eine sehr kompakte Realisierung. Mit diesen Teilen kann eine Anwendung geschrieben werden: 16.3. METHODEN IN DER KLASSE COLLECTIONS 175 import java.util.*; public class Karten implements java.util.Comparator { ... public static void main (String args[]) { int[] kartenProHandSkat = {10,10,10,2}; Karten k = new Karten(); Vector[] hände = k.austeilen( kartenProHandSkat ); // sortierte Ausgabe for( int i=0; i<hände.length; i++ ) { System.out.print( i +" "); Collections.sort( hände[i], k ); System.out.println( hände[i] ); } } public int compare( java.lang.Object obj1,java.lang.Object obj2) { return kartenWerte.indexOf(obj1) - kartenWerte.indexOf(obj2) ; } } Um die Vektoren sortieren zu können, implementiert die Klasse das Interface Comperator. Kriterium für den Vergleich ist der Abstand zwischen zwei Karten in dem geordneten Vektor. 176 KAPITEL 16. DYNAMISCHE DATENSTRUKTUREN Kapitel 17 Erweiterungen WS08 17.1 Felder Mit dem folgenden Code wird ein 3 × 4-Feld angelegt und gefüllt: int[][] a = new int[3][4]; for( int i=0; i<3; i++ ) { for( int j=0; j<4; j++ ) { a[i][j] = i+j; } } Ein solches Feld kann zur Repräsentation einer Matrix verwendet werden. Dann entspricht a[i][j] dem Element aij in der üblichen Schreibweise. Dementsprechend ist die erste Dimension die Zeilen- und die zweite die Spaltendimension. Im Beispiel wird jedem Element als Wert die Summe von Zeilen- und Spaltenindex zugewiesen: 0 1 2 1 2 3 2 3 4 3 4 5 Intern werden 2-dimensionale Felder aus eindimensionalen Feldern zusammengebaut. Demnach ist a selbst ein Feld der Länge 3 – genauer gesagt der Verweis darauf. Jedes Element dieses Feldes ist wiederum selbst ein Feld, diesmal der Länge 4 und mit Integerwerten als Elementen. Detailliert ergibt sich dann folgendes Bild: a + a[0] ( a[1] ( a[2] ( 0 1 2 3 1 2 3 4 2 3 4 5 177 178 KAPITEL 17. ERWEITERUNGEN WS08 a[i] bezeichnet die i-te Zeile. Man kann diese Bezeichnung wie jedes andere eindimensionale Feld verwenden. So kann man beispielsweise mit a[i].length auf die Länge dieser Zeile zugreifen. Allgemein gilt bei mehrdimensionalen Feldern: gibt man alle Klammerpaare an – d. h. bei einem n-dimensionalen Feld n Klammerpaare – so wird damit ein einzelnes Element angesprochen. Mit weniger Klammern bezieht man sich auf die entsprechenden Überstrukturen. Betrachten wir ein 3-dimensionales Feld int[][][] d3 = new int[3][4][5]; Dann ist: • d3: Verweis auf ein Feld mit 3 int[][]-Verweisen • d3[1]: Verweis auf ein Feld mit 4 int[]-Verweisen • d3[0][2]: Verweis auf ein int[], also ein Feld mit in diesem Fall 5 intWerten • d3[0][1][2]: ein int-Wert, davon gibt es insgesamt 3 × 4 × 5 = 60. In der allgemeinen Form ist man nicht mehr auf rechteckige Felder beschränkt. Für jede Zeile kann eine andere Länge gewählt werden. Das folgende Beispiel zeigt diese Möglichkeit: // Feld mit 3 Zeilen int[][] b = new int[3][]; for( int i=0; i<b.length; i++ ) { // Jede Zeile mit anderer Länge b[i] = new int[i+2]; for( int j=0; j<b[i].length; j++ ) { b[i][j] = i+j; } } Hier ergibt sich folgendes Bild: b + b[0] ( b[1] ( b[2] ( 0 1 1 2 3 2 3 4 5 17.1. FELDER 179 Übung 17.1 Gaußsches Eliminationsverfahren Mit dem Gauß-Verfahren kann man lineare Gleichungssysteme lösen. Mit Koeffizienten aij und den Werten bi hat das lineare Gleichungssystem bei n Unbekannten die Form a11 x1 +a12 x2 +· · ·+a1n xn = b1 a21 x1 +a22 x2 +· · ·+a2n xn = b2 .. . . . . . + .. + . . + .. = .. an1 x1 +an2 x2 +. . .+ann xn = bn (17.1) Zur Lösung des Gleichungssystems bringt man durch Zeilenumformungen das System in eine Stufenform mit neuen a0ij und b0i : a011 x1 +a012 x2 +· · ·+a01n xn = b01 0 +a022 x2 +· · ·+a02n xn = b02 .. . . . . . + .. + . . + .. = .. 0 + 0 +. . .+a0nn xn = b0n (17.2) In dieser Form lassen sich ausgehend von xn = b0n /a0nn (17.3) durch Rückwärtseinsetzen die gesuchten Werte leicht bestimmen. • Programmieren Sie das Gaußsche Eliminationsverfahren. Betrachten Sie dabei die erweiterte Koeffizientenmatrix mit den aij und bi als Feld der Größe n × (n + 1). • Verwenden Sie die Klasse Matrix als Basis. Mit den Methoden dieser Klasse kann man Felder anlegen und anzeigen lassen. Machen Sie sich zunächst mit den vorgegebenen Methoden vertraut. Programmieren Sie dann eine Methode gauss(), die ein vorgegebenes Gleichungssystem löst (Vorschlag: Werte aus erstelleBeispielMatrix(). Lassen Sie die einzelnen Schritte anzeigen (siehe Beispiel demo()). 180 17.2 KAPITEL 17. ERWEITERUNGEN WS08 Rekursion Betrachten wir als Beispiel die Berechnung der Summe der ersten 50 Zahlen. Mit der Funktion S(N ) als Summe der ersten N Zahlen erhalten wir S(1) S(2) S(3) .. . S(50) = 1 = S(1) + 2 = S(2) + 3 .. . = S(49) + 50 Dies entspricht unmittelbar der Realisierung durch eine Schleife in der Art int S=0; for( int i=1; i<=50; i++ ) { S = S + i; } System.out.println( S ); Der gesuchte Wert wird durch Addition aller Zahlen von 1 bis 50 berechnet. Formal lässt sich die Reihenfolge der Berechnung genauso gut umkehren. Wenn wir zunächst so tun, als wäre S(49) bekannt, dann können wir einfach S(50) = S(49) + 50 schreiben. Mit dem gleichen Argument kann S(49) auf S(48) zurück geführt werden. Insgesamt erhält man damit: S(50) S(49) S(48) .. . S(2) S(1) = S(49) + 50 = S(48) + 49 = S(47) + 48 .. . = S(1) + 2 = 1 Da die Berechnung vom Ende her beginnt, spricht man von Rekursion (lat. recurrere zurücklaufen). Demgegenüber wird das schrittweise Berechnen ab dem Anfang als Iteration (lat. iterare wiederholen) bezeichnet. Rekursion lässt sich in Java und vielen anderen Programmiersprachen leicht realisieren. Dazu programmieren wir eine Methode, die entweder bei n = 1 den Wert 1 zurück gibt oder einen weiteren Rekursionsschritt ausführt: 17.2. REKURSION 181 int S( int n ) { if( n == 1 ) return 1; return S(n-1) + n; } Der gesuchte Wert kann dann einfach mit System.out.println( S(50) ); ausgegeben werden. In dieser Form ist die Schleife verschwunden. Stattdessen steckt die Logik in der Methode. Die Methode ruft sich selbst mit einem um 1 kleineren Parameter immer wieder auf, bis irgendwann der Wert 1 erreicht ist. Dann wird rückwärts die Kette abgearbeitet. Diese Beispiel zeigt, wie ein Problem sowohl iterativ als auch rekursiv gelöst werden kann. In diesem Fall bietet die rekursive Lösung außer der eleganten Formulierung keinen Vorteil. Es ist sogar davon auszugehen, dass sie aufgrund der vielen Methodenaufrufe mehr Rechenzeit benötigt. Rekursive Lösungen bieten sich immer dann an, wenn man ein komplexes Problem auf ein einfacheres Problem zurückführen kann. Man kann dann den Lösungsansatz direkt als Java-Code umsetzen. Die Lösung ist damit elegant und leicht verständlich. Ob die rekursive oder die immer auch mögliche iterative Lösung aufwandsgünstiger ist, muss im Einzelfall betrachtet werden. Übung 17.2 Größter gemeinsamer Teiler Die folgende Methode berechnet den größten gemeinsamer Teiler (GGT) zweier natürlicher Zahlen a > b: int ggt( int a, int b ) { while( b > 0 ) { int rest = a % b; a = b; b = rest; } return a; } // Rest bestimmen // Werte tauschen Wie sieht eine rekursive Lösung aus? Übung 17.3 Wegesuche Vorgegeben ist ein Raster mit N × N -Feldern. Wie viele mögliche Wege führen von einer Ecke zu der diagonal gegenüber liegenden Ecke? Die Wege sollen nur horizontal oder vertikal verlaufen und keine Umwege enthalten. Im Fall N = 2 gibt es insgesamt 6 Möglichkeiten: 182 KAPITEL 17. ERWEITERUNGEN WS08 ◦ /◦ ◦ ◦ ◦ ◦ /◦ ² ◦ ² ◦ ◦ ◦ ◦ ◦ ◦ ◦ ◦ ◦ /◦ ◦ ◦ ◦ /◦ ² ² ◦ ² ◦ /◦ ² ◦ ◦ ◦ /◦ ² ◦ ◦ ◦ /◦ ² ◦ ◦ ◦ ◦ ◦ ◦ ◦ /◦ ◦ ² ² /◦ ² ◦ ² ◦ ◦ ◦ ◦ /◦ ◦ ◦ /◦ /◦ ◦ Allgemein gilt für die Anzahl NW der Wege die Formel à NW ! 2n (2n)! = = n n! × n! Überprüfen Sie diese Formel durch ein entsprechendes Programm. In diesem Programm soll für vorgegebenes N die Anzahl der möglichen Wege durch Zählen aller Möglichkeiten bestimmt werden. Verwenden Sie dazu eine rekursive Berechnung. 17.3. CLOSE 17.3 close public static void teste() throws Exception { schreibe( "zeilen1.txt", false ); schreibe( "zeilen2.txt", true ); } public static void schreibe(String datei, boolean sichern) throws Exception { BufferedWriter wr = new BufferedWriter( new FileWriter( datei ) ); String text = null; int i; for( i=0; i< 100; i++ ) { wr.write( "Zeile " + i ); wr.newLine(); } System.out.println( "Alles geschrieben" ); if( sichern ) wr.close(); } 183 184 KAPITEL 17. ERWEITERUNGEN WS08 Übung 17.4 Pythagoreische Tripel Drei natürliche Zahlen a < b < c mit a 2 + b2 = c2 bezeichnet man als Pythagoreische Tripel. Wie sind die Werte für das einzige Tripel, das die Bedingung a + b + c = 1000 erfüllt? (Problem 9 im Euler Project). Übung 17.5 Taylor-Reihe Die Exponentialfunktion lässt sich gemäß ex = ∞ X xn n=0 n! (17.4) als Potenzreihe darstellen. • Schreiben Sie eine Klasse mit Methoden, um für gegebenes x und n den Wert der Taylor-Reihe zu berechnen. Verwenden Sie dabei die Methode Math.pow() zur Berechnung der Potenzen xn . Vergleichen Sie den berechneten Wert mit dem Ergebnis von Math.exp(x). Wie nähert sich der Wert der Potenzreihe mit wachsendem n an den „richtigen“ Wert an? • Programmieren Sie alternativ die Berechnung der Potenzreihe nach dem Horner-Schema. Vergleichen Sie die Güte der beiden Berechnungen (z. B. x = 3, n = 30). Wie schätzen Sie den Rechenaufwand der beiden Berechnungsarten im Vergleich? Übung 17.6 Collatz-Problem Gegeben ist eine positive ganze Zahl n. Dann soll mit diesem Startwert nach folgender Vorschrift eine Folge berechnet werden: ( n= n/2 : 3·n+1 : n gerade n ungerade (17.5) Die Folgen erreichen irgendwann den Wert 1 und laufen dann in eine Schleife mit der Folge 1 4 2. Als Beispiel erhält man für den Wert 11: n=11: 34 17 52 26 13 40 20 10 5 16 8 4 2 1 1. Lassen Sie die Folgen für alle Werte n < 100 ausgeben. 2. Bei welchem Startwert ist die Folge am längsten? 3. Bestätigen Sie, dass auch für alle n < 1000000 der Wert 1 erreicht wird. 17.3. CLOSE 185 4. Welches ist jetzt die längste Folge? 5. Welche größte Zahl tritt auf (bei welcher Folge)? Hinweise: • Die einzelnen Folgeglieder können recht groß werden. • Diese Aufgabe finden Sie auch als Problem 14 im Euler Project1 • Sie können $500 Belohnung verdienen wenn Sie die Vermutung, dass die Folge für jeden Startwert bei 1 ankommt, beweisen. 1 http://projecteuler.net/ 186 KAPITEL 17. ERWEITERUNGEN WS08 Literaturverzeichnis [Boo94] Grady Booch. Objektorientierte Analyse und Design: Mit praktischen Anwendungsbeispielen. Addison Wesley, 1994. [Fri07] Jeffrey E. F. Friedl. Reguläre Ausdrücke. O’Reilly, 2007. [Gol91] David Goldberg. What every computer scientist should know about floating-point arithmetic. Computing Surveys, 1991. [Krü02] Guido Krüger. Handbuch der Java-Programmierung. Addison-Wesley, 2002. 187 Index ?-Operator, 21 Fragezeichen-Operator, 21 Funktion, 53 Abstrakte Klassen, 101 Aktivitätsdiagramm, 69 Algorithmus, 63 Applet, 1 Assoziativspeicher, 168 garbage collection, 85 Gaußsches Eliminationsverfahren, 179 Getter, 81 Gleitkommazahlen, 37 Hash-Code, 91 Hashtable, 168 Bereichsüberschreitung, 8 Bit-Operator, 12 BlueJ, 1, 89 Booch, Grady, 74 boolean, 17 Bootstrap Classes, 131 Boundary matchers, 124 BufferedWriter, 143 IEEE 754, 33 import-Anweisung, 132 Information hiding, 81 InputStream, 148 Instanzmethoden, 82 Integer, 5 Interface, 102 Iteration, 180 C++, 76 C#, 77 call by reference, 57 call by value, 55, 57 catch-or-throw Regel, 155 Collections, 173 jar, 129 javadoc, 133 jdb, 134 Klassenmethoden, 82 Debugger, 134 Destruktoren, 85 dynamisches Binden, 100 L-Wert, 11 Lastenheft, 63 LineNumberReader, 145 lvalue, 11 Error, 157 Exception, 157 Makro, 53 Manifest, 130 Mantisse, 31, 33 Mathematisch Funktionen, 39 mehrfache Vererbung, 76 Mehrfachvererbung, 102 Modulo-Operator, 11 Fibonacci Zahlen, 29 Fibonacci-Zahlen, 28, 60 File, 149 finally-Block, 159 Flussdiagramm, 65 foreach, 48 188 INDEX OutputStream, 148 Paket, 132 Perl, 19, 168 Pflichtenheft, 63 Pipe, 141 Polymorphismus, 100, 157 PrintWriter, 147 Properties, 171 Prozedur, 53 PushBackReader, 145 Quantoren, 126 R-Wert, 11 Random Access File, 148 Reader, 142 regulärer Ausdruck, 117 regular expressions, 123 Regulare Ausdrucke, 123 Rekursion, 180 RuntimeException, 158 rvalue, 11 Schachspiel, 42 Setter, 81 Short-Circuit-Evaluation, 18 Signatur, 56 Socket, 141 Stack, 167 Stapelspeicher, 167 statisches Binden, 100 switch-Anweisung, 22 Türme von Hanoi, 60 throw, 159 Tokenizer, 119 Type-Cast-Operator, 41 UML, 69 Unicode, 111 Unified Modeling Language, 69 Zuweisungsoperator, 10 189