JAVA Skript 2012 Feb
JAVA Skript 2012 Feb
Programmiersprache Java
Hans Joachim Pug Rechen- und Kommunikationszentrum der RWTH Aachen [email protected] 3. Februar 2012
Inhaltsverzeichnis
1 Lineare Programme 1.1 Hello world . . . . . . . . . . . . . . . . . . . . . . . . 1.1.1 Start eines Java-Programms . . . . . . . . . . . 1.1.2 Der Programmrahmen . . . . . . . . . . . . . . 1.1.3 Formatierung . . . . . . . . . . . . . . . . . . . 1.1.4 Die Anweisung . . . . . . . . . . . . . . . . . . 1.2 Variablen . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Datentypen . . . . . . . . . . . . . . . . . . . . 1.2.2 Deklaration von Variablen . . . . . . . . . . . . 1.2.3 Namen von Variablen . . . . . . . . . . . . . . 1.2.4 Rechnen mit Variablen . . . . . . . . . . . . . . 1.2.5 Besonderheiten bei Rechenoperationen . . . . . 1.2.6 Operationen f ur Character und Strings . . . . . 1.3 Kommentare . . . . . . . . . . . . . . . . . . . . . . . 1.3.1 Was soll kommentiert werden? . . . . . . . . . 1.4 Aufruf von Funktionen . . . . . . . . . . . . . . . . . . 1.4.1 Packages . . . . . . . . . . . . . . . . . . . . . . 1.4.2 Ausgabe von Daten auf den Bildschirm . . . . 1.4.3 Einlesen von Daten von der Tastatur . . . . . . 1.4.4 Umwandlung zwischen Zahlen und Strings . . . 1.4.5 Mathematische Funktionen: Klasse Math . . . . 1.4.6 Sonstiges . . . . . . . . . . . . . . . . . . . . . 1.4.7 Ubergabeparameter . . . . . . . . . . . . . . . 1.5 Lineare Programme . . . . . . . . . . . . . . . . . . . 1.5.1 Eingabe - Verarbeitung - Ausgabe (EVA) . . . 1.5.2 Struktogramme . . . . . . . . . . . . . . . . . . 1.5.3 Flussdiagramm / Programmablaufplan (PAP) . 1.5.4 Aktivit atsdiagramm . . . . . . . . . . . . . . . 2 Kontrollstrukturen 2.1 Auswahl (Selektion) . . . . . . . . . . . 2.1.1 Entweder-oder-Entscheidungen . 2.1.2 Boolean-Variablen . . . . . . . . 2.1.3 Vergleichsoperatoren . . . . . . . 2.1.4 Boolean-Werte als Bedingung f ur 2.1.5 Logikoperatoren . . . . . . . . . 3 9 9 9 10 11 11 11 12 13 13 14 15 18 19 20 20 20 21 21 22 23 24 24 25 25 25 26 26 27 27 27 29 29 30 30
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verzweigungen . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
INHALTSVERZEICHNIS 2.1.6 Operatorhierarchie . . . . . . . . . . . . . . . . . . . . 2.1.7 Weglassen der Klammern . . . . . . . . . . . . . . . . 2.1.8 if-else-Kaskaden . . . . . . . . . . . . . . . . . . . . . 2.1.9 Mehrseitige Auswahl . . . . . . . . . . . . . . . . . . . Schleifen(Iteration) . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Die Z ahlschleife . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Besonderheiten bei for-Schleifen . . . . . . . . . . . . . 2.2.3 Schleife mit Anfangsabfrage (Kopfgesteuerte Schleife) 2.2.4 Schleife mit Endabfrage (Fugesteuerte Schleife) . . . 2.2.5 break und continue . . . . . . . . . . . . . . . . . . . . 2.2.6 Mehrere verschachtelte Schleifen . . . . . . . . . . . . 2.2.7 Aufz ahlungsschleife . . . . . . . . . . . . . . . . . . . . 2.2.8 Geltungsbereich von Variablen . . . . . . . . . . . . . 2.2.9 Tabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 32 32 34 35 36 37 39 39 41 42 42 43 45 45 45 46 47 47 48 48 49 52 55 57 57 58 60 61 61 63 63 64 64 65 65 65 66 66 69 69 69 70 71 73
2.2
3 Das objektorientierte Konzept von Java 3.1 Einf uhrung: Objekte der realen Welt . . . . . . . 3.1.1 Fachbegrie . . . . . . . . . . . . . . . . . 3.1.2 Anwender- und Entwicklersicht . . . . . . 3.2 Software-Objekte aus Anwendersicht . . . . . . . 3.2.1 Einleitung . . . . . . . . . . . . . . . . . . 3.2.2 Die Anwenderschnittstelle (API) . . . . . 3.2.3 Variablen und Objekte . . . . . . . . . . . 3.2.4 Exceptions . . . . . . . . . . . . . . . . . 3.2.5 Methoden . . . . . . . . . . . . . . . . . . 3.2.6 Aliasing . . . . . . . . . . . . . . . . . . . 3.3 Interne Darstellung . . . . . . . . . . . . . . . . . 3.3.1 Interne Darstellung . . . . . . . . . . . . . 3.3.2 Lebensdauer eines Objekts . . . . . . . . 3.3.3 Wann sind Objekte gleich? . . . . . . . . 3.4 Felder (Arrays) . . . . . . . . . . . . . . . . . . . 3.4.1 Grundfunktionen . . . . . . . . . . . . . . 3.4.2 Felder kopieren . . . . . . . . . . . . . . . 3.4.3 Die Aufz ahlungsschleife . . . . . . . . . . 3.4.4 Einfache Ausgabe des Feldinhalts . . . . . 3.4.5 Mehrdimensionale Felder . . . . . . . . . 3.5 Strings . . . . . . . . . . . . . . . . . . . . . . . . 3.5.1 Besonderheiten . . . . . . . . . . . . . . . 3.5.2 Methoden von Strings . . . . . . . . . . . 3.5.3 Escape-Sequenzen . . . . . . . . . . . . . 3.5.4 Regul are Ausdr ucke . . . . . . . . . . . . 3.6 Entwicklung eines Beispiel-Objekts . . . . . . . . 3.6.1 Software-Objekte aus Entwicklersicht . . 3.6.2 Zusammengesetzte Datentypen . . . . . . 3.6.3 Benutzung der Klasse aus Anwendersicht 3.6.4 Schreiben einer Methode . . . . . . . . . . 3.6.5 Datenkapselung . . . . . . . . . . . . . . .
INHALTSVERZEICHNIS 3.6.6 Getter- und Setter-Methoden . . . 3.6.7 Konstruktoren . . . . . . . . . . . 3.6.8 Invarianten . . . . . . . . . . . . . 3.6.9 Andere Methoden . . . . . . . . . Weitere Details . . . . . . . . . . . . . . . 3.7.1 Objekte als Attribute . . . . . . . 3.7.2 Statische Methoden und Variablen 3.7.3 Objekte als Ubergabeparameter . . 3.7.4 Mehrere R uckgabewerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5 75 78 81 82 82 82 84 87 91 93 . . . . . . . . . . . . . 93 94 95 95 96 97 97 98 99 99 99 100 101 103 103 103 103 104 107 107 107 108 108 109 110 111 113 115
3.7
4 Ausnahmebehandlung (Exception Handling) 4.1 Ausl osen und Fangen von Exceptions (Zusammenfassung von Kapitel 3) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Reaktion auf Ausnahmen . . . . . . . . . . . . . . . . . . . . . 4.2.1 Analyse von Ausnahmen im catch-Block . . . . . . . . . 4.2.2 Reaktion auf Ausnahmen . . . . . . . . . . . . . . . . . 4.2.3 Mehrere unterschiedliche Exceptions . . . . . . . . . . . 4.2.4 Verschachtelte try-catch-Bl ocke . . . . . . . . . . . . . . 4.3 Checked und unchecked Exceptions . . . . . . . . . . . . . . . . 4.3.1 Welche Exception werfe ich? . . . . . . . . . . . . . . . 4.3.2 Besonderheiten beim Ausl osen einer checked exception 4.4 Vererbung und Exceptions . . . . . . . . . . . . . . . . . . . . . 4.4.1 Schreiben eigener Exceptions . . . . . . . . . . . . . . . 4.4.2 Vererbungshierarchie von Exceptions . . . . . . . . . . . 4.4.3 Fangen ganzer Gruppen von Exceptions . . . . . . . . . 5 Die Java-Klassenbibliothek 5.1 Programmiersprache Java und Java-Plattform . . . . . . . . . . 5.2 Eingabe und Ausgabe von Daten . . . . . . . . . . . . . . . . . 5.2.1 Eingabe u ber die Konsole . . . . . . . . . . . . . . . . . 5.2.2 Ein- und Auslesen einer Datei . . . . . . . . . . . . . . . 5.2.3 Schreiben und Lesen in gepacktem Format . . . . . . . . 5.3 Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.1 Neue Aufgabenstellung . . . . . . . . . . . . . . . . . . 5.3.2 Generische Datentypen (Generics) . . . . . . . . . . . . 5.3.3 Wrapper-Klassen . . . . . . . . . . . . . . . . . . . . . . 5.3.4 Autoboxing . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.5 Einsatz von ArrayLists mit Wrapper-Klassen und Autoboxing . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.3.6 L osung der Aufgabenstellung mit ArrayLists . . . . . . 5.4 Assoziative Felder . . . . . . . . . . . . . . . . . . . . . . . . . 5.5 StringBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Interfaces 6.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.1.1 Problemstellung . . . . . . . . . . . . . . . . . . . . . . 6.1.2 Einfacher L osungsansatz . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . .
INHALTSVERZEICHNIS 6.1.3 Verbesserter L osungsansatz . . . . . . . . . . . . . . . 6.1.4 Bemerkungen . . . . . . . . . . . . . . . . . . . . . . . H auger Gebrauch von Interfaces . . . . . . . . . . . . . . . . 6.2.1 Entwurfsmuster (Programming Patterns) . . . . . . . 6.2.2 Das Entwurfsmuster Strategie (Strategy) . . . . . . . 6.2.3 Benutzung vordenierter Interfaces . . . . . . . . . . . 6.2.4 Drittes Beispiel: Zusammenhang mit Funktionszeigern . . . . . . . . . . . . . . 119 122 123 123 124 124 127
6.2
7 Vererbung 7.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . 7.1.1 Beispielhafte Problemstellung . . . . . . . . . . . 7.1.2 Terminologie . . . . . . . . . . . . . . . . . . . . 7.1.3 Konstruktoren . . . . . . . . . . . . . . . . . . . 7.1.4 Lesbarkeit . . . . . . . . . . . . . . . . . . . . . . 7.1.5 UML-Diagramm . . . . . . . . . . . . . . . . . . 7.1.6 Einfach- und Mehrfachvererbung . . . . . . . . . 7.1.7 Methoden u berschreiben . . . . . . . . . . . . . . 7.1.8 Zugri auf die u berschriebene Methode . . . . . 7.2 Beispiele aus der API . . . . . . . . . . . . . . . . . . . 7.2.1 Object . . . . . . . . . . . . . . . . . . . . . . . . 7.2.2 Die toString()-Methode . . . . . . . . . . . . . . 7.3 Bindungsarten . . . . . . . . . . . . . . . . . . . . . . . 7.3.1 Statischer und dynamischer Typ . . . . . . . . . 7.3.2 Uberschreiben von Elementen . . . . . . . . . . . 7.3.3 Bindungsarten . . . . . . . . . . . . . . . . . . . 7.3.4 Vorteile der dynamischen Bindung . . . . . . . . 7.3.5 Speichern in Feldern und Listen . . . . . . . . . . 7.3.6 Wann wird statisch bzw. dynamisch gebunden? . 7.3.7 Zusammenfassung: Uberschreiben und Verdecken 7.4 Interfaces als Ersatz f ur Mehrfachvererbung . . . . . . . 7.5 Anonyme innere Klassen . . . . . . . . . . . . . . . . . . 7.6 Abstrakte Klassen . . . . . . . . . . . . . . . . . . . . . 8 Rekursive Algorithmen 8.1 Einleitung . . . . . . . . . . . . . . . . . . . . 8.2 Interne Umsetzung im Stack . . . . . . . . . . 8.3 Verwendung von rekursiven Algorithmen . . . 8.3.1 Rekursive und iterative Algrorithmen 8.4 Beispiele . . . . . . . . . . . . . . . . . . . . . 8.4.1 Die Fibonacci-Folge . . . . . . . . . . 8.4.2 Variationen . . . . . . . . . . . . . . . 9 Gro ere Programmeinheiten 9.1 Bibliotheken . . . . . . . . . . . . . . 9.1.1 Einbinden von Bibliotheken . 9.1.2 Eigene Bibliotheken erstellen 9.2 Pakete . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
131 . 131 . 131 . 132 . 132 . 133 . 134 . 134 . 134 . 135 . 135 . 135 . 136 . 137 . 137 . 138 . 139 . 139 . 140 . 141 . 142 . 143 . 144 . 146 149 149 150 153 153 155 155 156 159 159 159 161 162
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
INHALTSVERZEICHNIS 9.2.1 Laden von Paketen . . . . . . . . . . . . . . . . . 9.2.2 Erstellen eigener Pakete . . . . . . . . . . . . . . 9.2.3 Eindeutigkeit von Paketen . . . . . . . . . . . . . 9.2.4 Pakete und Sichtbarkeitsgrenzen . . . . . . . . . Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3.1 Statische innere Klassen . . . . . . . . . . . . . . Uber Java hinaus: Namensr aume . . . . . . . . . . . . . 9.4.1 Statische innere Java-Klassen und Namensr aume . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7 162 164 166 166 167 167 168 169 171 171 172 175 175 176 176 176 177 178 178 179 179 180 181 183 183 183 184 185 185 185 186 187 189 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 191 193 193 193 194 195
9.3 9.4
10 Lesbarkeit eines Programms 10.1 Programmierrichtlinien . . . . . . . . . . . . . . . . . . . . . . . 10.1.1 Ausgew ahlte Richtlinien aus den Java Code Conventions 10.1.2 Formatierung des Quelltexts . . . . . . . . . . . . . . . 10.2 Dokumentationskommentare . . . . . . . . . . . . . . . . . . . . 10.2.1 Einteilung der Dokumentationskommentare . . . . . . . 10.2.2 Javadoc von Klassen . . . . . . . . . . . . . . . . . . . . 10.2.3 Javadoc von Attributen . . . . . . . . . . . . . . . . . . 10.2.4 Javadoc von Methoden und Konstruktoren . . . . . . . 10.2.5 Weitere Tags und Formatierungsm oglichkeiten . . . . . 10.2.6 Erzeugung und Kontrolle der Javadoc . . . . . . . . . . 10.3 Verwendung von Kommentaren . . . . . . . . . . . . . . . . . . 10.3.1 Notwendigkeit von Kommentaren . . . . . . . . . . . . . 10.3.2 Einsatzgebiete von Kommentaren . . . . . . . . . . . . . 10.3.3 Fehler bei der Kommentierung . . . . . . . . . . . . . . A Formatierung von Ausgaben A.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . A.2 Struktur des Formatstrings . . . . . . . . . . . . A.2.1 Rechts- und linksb undiges Formatieren . . A.2.2 Dezimalbr uche mit Nachkommastellen . . A.2.3 Denierte Stellenanzahl f ur ganze Zahlen A.2.4 Formatieren ohne Bildschirmausgabe . . . A.3 Andere Formatierungsklassen . . . . . . . . . . . B Escape-Sequenzen C ASCII-Zeichen D Struktogramme und Flussdiagramme D.1 Elemente von Struktogrammen . . . . D.1.1 Endlosschleife . . . . . . . . . . D.1.2 Aussprung (break) . . . . . . . D.1.3 Aufruf eines Unterprogramms . D.2 Elemente von Flussdiagrammen . . . . E API der Beispielklasse Bruch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
INHALTSVERZEICHNIS
Kapitel 1
Lineare Programme
1.1 Hello world
Das traditionell erste Programm, das man in einer neuen Sprache schreibt, heit Hello World. Es macht nichts weiter, als die Textzeile Hello World auszugeben. Die Schwierigkeit besteht darin, das ganze Drumherum so zum Laufen zu kriegen, dass u uhrt werden kann. Im berhaupt ein Programm ausgef ersten Schritt werden wir versuchen, das Hello-World-Programm zu starten, im zweiten Schritt werden wir das Programm etwas n aher unter die Lupe nehmen.
1.1.1
Ein Java-Programm kann ganz ohne Entwicklungsumgebung mit einem Texteditor erstellt und dann aus einer Eingabeauorderung (oder einer Linux-Konsole) gestartet werden. Es ist lehrreich, dies in einem ersten Versuch auch zu tun. Sp ater werden wir allerdings die Entwicklungsumgebung Eclipse verwenden. Zun achst starten wir einen Texteditor und tippen folgende Zeilen ein: public class HelloWorld { public static void main(String args[]) { System.out.println("Hello World"); } } Dann speichern wir den Text unter dem Namen HelloWorld.java. Es muss genau dieser Name gew ahlt werden, da er mit dem Namen aus der ersten Zeile des Programms u bereinstimmen muss. Anschlieend kann das Programm com piliert werden. Dazu gibt man in einer Eingabeauorderung die Zeile javac HelloWorld.java ein. Unter Windows kann m oglicherweise das Programm javac nicht automatisch gefunden werden. Es bendet sich oft unter einem Pfad, wie C:\Programme\Java\jdk1.5.0\bin\javac.exe 9
10
javac erzeugt aus der java-Datei (auch Source-Code oder Quellcode genannt), eine class-Datei, die hier den Namen HelloWorld.class hat. Die class-Datei besteht aus Java-Bytecode. Die class-Datei kann nicht direkt ausgef uhrt werden, 1 sondern ben otigt dazu ein weiteres Programm namens java . Das entsprechende Kommando heit: java HelloWorld Das Programm java.exe bendet sich im gleichen Verzeichnis, wie javac.exe.
1.1.2
Der Programmrahmen
Sehen wir uns noch einmal den Code an. Zur besseren Ubersicht erh alt jede Zeile eine Zeilennummer: 1 2 3 4 5 public class HelloWorld { public static void main(String args[]) { System.out.println("Hello World"); } }
Die Zeilen 1, 2, 4 und 5 bilden einen Rahmen, der in jedem Java-Programm vorhanden sein muss. Die Form dieses Rahmens ist: public class HelloWorld { public static void main(String args[]) { } } Diese Form m ussen wir zun achst einmal hinnehmen, ohne nach dem tieferen Sinn zu fragen. Merken m ussen wir uns: In der ersten Zeile muss immer der Dateiname (ohne die Endung .java) eingesetzt werden (in diesem Beispiel also HelloWorld). Das Programm startet in der ersten Zeile, die auf public static void main(String args[]) { folgt und beendet sich, wenn es auf die zugeh orige geschweifte Klammer zu st ot. Alle Zeilen dazwischen werden der Reihe nach ausgef uhrt. Das Programm public class HelloWorld { public static void main(String args[]) { System.out.println("Hallo"); System.out.println("und tschuess"); } }
1
11
1.1.3
Formatierung
Java selbst ist die Formatierung des Codes recht gleichg ultig. Die Zeilen public class HelloWorld{public static void main(String args[]) { System.out.println("Hello World");}} funktionieren genauso gut, wie das letzte Beispiel. Um den Quellcode auch f ur Menschen lesbar zu machen, gibt es zus atzliche Regeln, die Java Code Conventions. Die vollst andigen Code Conventions sind im Netz unter http://java.sun.com/docs/codeconv/index.html zu nden. Die ersten wichtigen Punkte sind: Nur eine Anweisung pro Zeile 4 Spalten Einr uckung pro sich o nender geschweifter Klammer, sowie 4 Spalten zur uck nach jeder sich schlieenden geschweiften Klammer.2
1.1.4
Die Anweisung
Die einzige Anweisung ist System.out.println("Hello World"); Dazu ist zu sagen: Jede Anweisung muss mit einem Semikolon abgeschlossen werden. Die Anweisung System.out.println gibt alles, was zwischen den nachfolgenden Klammern steht, auf dem Bildschirm aus. Die Anf uhrungszeichen sagen, dass es sich hierbei um ein St uck Text handelt, der so, wie er ist (ohne Anf uhrungszeichen), auszugeben ist.
1.2
Variablen
Eine wichtige Eigenschaft einer Programmiersprache ist die F ahigkeit, Variablen zu speichern. Eine Variable kann man sich als einen Platz im Hauptspeicher vorstellen, der einen Namen erhalten hat, gewissermaen eine Box mit einem Namen und einem Inhalt. fred Hello. hour 11 minute 59
In diesem Skript wird hiervon leicht abgewichen und nur 2 Spalten einger uckt. 4 Spalten haben den Nachteil, dass der Text sehr schnell nach rechts r uckt. Dagegen sehe ich bei 2 Spalten noch keinen Verlust an Ubersichtlichkeit.
2
12
1.2.1
Datentypen
Auer dem Wert und dem Namen hat jede Variable einen Typ, der angibt, ob in der Variable eine Zeichenkette, eine ganze Zahl, eine reelle Zahl oder etwas anderes gespeichert werden kann. Java kennt zwei verschieden Arten von Typen den primitiven Datentyp und die Klasse. In diesem Kapitel besch aftigen wir uns, mit einer Ausnahme, nur mit primitiven Datentypen. Die einzige Ausnahme ist der Datentyp String, der zwar eine Klasse ist, aber aufgrund einiger Besonderheiten wie ein primitiver Datentyp behandelt werden kann. Java kennt 8 primitive Datentypen: Typ boolean char byte short int3 long oat double Werte true oder false. Ein Zeichen. Ganze Zahl zwischen -128 und 127. Ganze Zahl zwischen -32768 und 32767. Ganze Zahl zwischen ca. -2 Milliarden und 2 Milliarden. Ganze Zahl zwischen ca. 1020 und 1020 (100 Trilliarden). Fliekommazahl mit ca. 8 Stellen zwischen 1038 und 1045 . Fliekommazahl mit ca. 16 Stellen zwischen 10308 und 10324 . Bit 1 16 8 16 32 64 32 64
Ein String nimmt eine ganze Zeichenkette (bis max. 2 Milliarden Zeichen) auf. Auch wenn wir zun achst Strings wie primitive Datentypen behandeln, m ussen wir einen Unterschied immer beachten: Primitive Datentypen werden in Java klein geschrieben, also: char, int, oat, . . . . Klassen werden in Java gro geschrieben, also: String. Somit hat jede Variable einen Namen, einen Typ und einen Wert. 3 Beispiele: Name fred hour minute
3
Wert Hello. 11 59
int ist der Standard-Ganzzahltyp. Rechenoperationen gehen mit int schneller als mit byte, short oder long. byte und short werden nur bei Speicherplatzmangel benutzt, long nur bei sehr groen Zahlen.
1.2. VARIABLEN
13
1.2.2
Variablen m ussen deklariert werden. Dabei erh alt eine Variable einen bestimmten Namen und einen bestimmten Typ. Weiterhin bekommt die Variable ein St uck des Hauptspeichers als Platz f ur den Variablenwert. Die Variable erh alt aber noch keinen bestimmten Wert. Die Deklaration sieht im Code wie folgt aus: String fred; int hour; int minute; Es ist m oglich der Variable bei der Deklaration gleich einen Anfangswert zuzuweisen. Dies zeigt das folgende Code-St uck: String fred = "Hallo"; int hour = 10; int minute = 30; Wird der Variablen bei der Deklaration kein Wert zugewiesen, muss das zu einem sp ateren Zeitpunkt geschehen. Es ist nicht m oglich, mit einer Variablen zu rechnen, die noch keinen Anfangswert hat. Versucht man das, meldet der Compiler einen Fehler. Eine Deklaration darf u berall im Programm vorkommen, auch nach ausf uhrbaren Anweisungen. System.out.println("Hallo"); int a; a=5;
1.2.3
Hier gibt es sowohl zwingende Regeln als auch weitergehende Einschr ankungen durch die Java Code Conventions. Zusammengefasst gilt: Variablennamen beginnen mit einem Kleinbuchstaben. Die restlichen Zeichen d urfen aus Klein- und Grobuchstaben, Ziern und dem Unterstrich (_) bestehen. Gro- und Kleinschreibung wird beachtet: name, nAme und nAME sind unterschiedliche Namen. Besteht ein Variablenname aus mehreren Worten, werden diese direkt zusammengesetzt. Jedes Teilwort beginnt mit einem Grobuchstaben. Beispiel: Eine Variable, die Worte z ahlt, k onnte wortZaehler heien.4 Allgemein soll nat urlich aus der Bezeichnung der Variablen m oglichst klar ihre Bedeutung hervorgehen. Sogenannte Einweg- (throwaway)-Variablen, die nur in einem sehr kurzen Teilst uck verwendet werden, d urfen auch aus einem einzelnen Buchstaben bestehen. G angig sind i, j, k, m und n f ur Integer-Einweg-Variablen.
4 In C w urde man statt dessen eher wort zaehler oder nach ungarischer Notation iWort zaehler schreiben.
14
1.2.4
Einfache Ausgabe von Variablen Bevor wir mit Variablen rechnen, zun achst eine einfache Methode, den Wert einer Variablen auf dem Bildschirm auszugeben. Die Anweisung System.out.println(hour) Gibt den Wert der Variablen hour auf dem Bildschirm aus. Beachten Sie, dass im Vergleich zu den vorigen Beispielen die Anf uhrungszeichen in der Klammer fehlen. Dies bedeutet, dass nicht der Text hour, sondern der Wert der Variablen hour ausgegeben werden soll. Rechnen mit Variablen Nun weisen wir den Variablen einen Wert zu. Dazu ben otigen wir den sogenannten Wertzuweisungsoperator =. Beispiel: int minute; minute = 5; Das = in der zweiten Zeile hat die Bedeutung: Die Variablen auf der linken Seite erh alt das Ergebnis des Ausdrucks auf der rechten Seite. Es handelt sich dabei ausdr ucklich nicht (wie wir gleich sehen werden) um eine mathematische Gleichung. Deshalb ist die Zeile minute = minute + 5; auch kein mathematischer Unsinn, sondern bedeutet: Nimm den Wert der Variablen minute, addiere 5 und weise das Ergebnis wieder der Variable minute zu. Auf der rechten Seite der Zuweisung darf ein Ausdruck stehen. Es sind dabei eine ganze Anzahl von Operatoren m oglich, von denen hier auer den 4 Grundrechenarten +, , , / nur den Modulo-Operator % erw ahnt sein soll. a % b ergibt den Rest der Ganzzahldivision von a durch b.5 Mit diesem Wissen k onnen wir schon ein kleines Programm schreiben, das den Kehrwert einer Zahl berechnet: public class Kehrwert { public static void main(String args[]) { double a; a = 7; a = 1/a; System.out.println(a); } }
5
Vorsicht: Modulo verh alt sich anders als erwartet, wenn a oder b kleiner sind als 0.
15
Auch Operationen innerhalb eines Funktionsaufrufs sind m oglich, wie das folgende Beispiel zeigt: System.out.println(2+5); Kurzschreibweisen Folgende Kurzschreibweisen sind m oglich: Lang a a a a a a = = = = = = a a a a a a + * / + x; x; x; x; 1; 1; Kurz a += a -= a *= a /= a++; a--; x; x; x; x; (nur bei Ganzzahlen) (nur bei Ganzzahlen) --> Gibt die Zahl 7 auf dem Bildschirm aus
1.2.5
Rechenoperationen in Java sind zwar prinzipiell einfach, aber oft stolpert man u unschtes Ergebnis erzeugen. ber einige Besonderheiten, die ein unerw Literale Im Ausdruck a = 3 + 4 * 2; stehen drei Zahlen direkt im Code. Solche direkt im Code stehende Werte heien Literale. Literale haben nicht nur einen Wert, sondern auch einen Typ, der sich aus der genauen Schreibweise ergibt. Es gibt int-, long-, double-, oat-, boolean-, char- und String-Literale. Die Kennzeichen sind: Literal Beispiele Reine Zahl 12; -74 Zahl mit angeh angtem l 12l; -74l Zahl mit Dezimalpunkt 12.3; -74. Zahl mit angeh angtem d 4d; -12d Zahl in Exponentialschreibweise 1e5 (f ur 105 ), 4e-3 (f ur 4 103 ) Zahl mit angeh angtem f 1f; 1.3e4f true oder false true; false Einzelner Buchstabe in Hochkoma; 3 mas String Zeichenkette in doppelten Hochf; hallo; 123 kommas Eine Variable kann stets nur Werte des eigenen Typs aufnehmen. int i = 12 ist ok, int i= 123 nicht. Typ int long double double double oat boolean char
Folgende Zeilen verursachen einen Compiler-Fehler: int i; double d = 10; i = d; int j = 2.5; Was ist der Unterschied? Java konvertiert den Typ automatisch, wenn das ver lustlos m oglich ist; also in der Richtung: byte -> short -> int -> long -> float -> double In der anderen Richtung k onnte der Wert nicht in die neue Variable passen. Hier muss man dem Compiler explizit sagen, dass man eine Typkonversion haben will. int i; double d = 10.5; i = (int) d; //Typkonversion auf int. i erhaelt den Wert 10 int j = (int) 2.5; Konvertieren von einer Fliekomma- in eine Festkommazahl rundet immer ab, selbst wenn der Nachkommaanteil 0.999999999 ist. Typ von Ergebnissen Wenn die Operanden einer Rechenoperation unterschiedlich sind, wird einer der beiden Operanden vor der Rechenoperation im Typ gewandelt. Der Operand, dessen Typ in der Hierarchie byte -> short -> int -> long -> float -> double weiter links steht, wird in den Typ des anderen Operanden gewandelt. Letztere ist auch der Typ des Ergebnisses. Beispiel: Die Operation 3 + 6. hat (wegen der verwendeten Literale) die Operanden int und double. Vor der Summation wird der Integer-Operand automatisch in double konvertiert. Das Ergebnis ist dann ebenfalls vom Typ double. Wichtig wird diese Vorgehensweise beim n achsten Abschnitt.
17
Bei einer Division zweier ganzer Zahlen kann eine gebrochene Zahl als Ergebnis herauskommen. Java h alt sich aber streng an die im letzten Abschnitt formulierten Regeln und erzeugt als Ergebnis der Division zweier Ganzzahlen wieder eine Ganzzahl. Dabei wird der Nachkommateil des Ergebnisses abgeschnitten. Diese Art der Division nennt man auch Ganzzahldivision. Oft ist dieser Eekt erw unscht, manchmal aber auch unbeabsichtigt. Beispiele: double d1 = 1/2; double d2 = 1/2.; //Ganzzahldivision, ergibt 0 //Zweiter Operand ist double, deshalb Wandlung //der 1 in double, anschliessend Fliesskommadivision
Im ersten Beispiel werden zun achst die beiden Integer-Werte 1 und 2 per Ganzzahldivision geteilt. Das Ergebnis ist der Integer-Wert 0. Dieser Wert wird automatisch in den double-Wert 0 gewandelt und der Variable d1 zugewiesen. Im zweiten Beispiel hat die Divsion zwei Operanden unterschiedlichen Typs. Da double in der Hierarchie u achst die 1 in einen double-Wert ber int steht, wird zun gewandelt. Dann wird mit Fliekommadivision geteilt und das double-Ergebnis 0.5 der Variablen d2 zugewiesen. Auswertungsreihenfolge Zwischen den einzelnen Operatoren gibt es eine Hierarchie, die festlegt, welche Operationen zuerst ausgef uhrt werden. F ur die uns bekannten Operatoren heit sie schlicht Punkt- vor Strichrechnung, wobei der %-Operator zur Punktrech nung z ahlt. Um die Auswertungsreihenfolge zu andern, d urfen im Ausdruck Klammern gesetzt werden: int a, b; a = 3 + 4 * 2; b = (3 + 4) * 2; --> ergibt 11 --> ergibt 14
Bei mehreren Operatoren werden die Operationen von links nach rechts ausgef uhrt. Besonders bei der Division ist das wichtig: a a a a = = = = 12 12 12 12 / / / / 2 / 2 10 / 2 10 / 2. 10. / 2 --> --> --> --> ergibt ergibt ergibt ergibt 3 = (12/2)/2 0 (Ganzzahl-Divisionen) 0.5 (nur erste Divsion Ganzzahl) 0.6 (beide Divisionen Fliesskomma)
Das Setzen von Klammern ist auch in vielen F allen sinnvoll, in denen es gar nicht n otig w are: Wenn man die Regeln im einzelnen nicht genau im Kopf hat und nicht nachsehen will. Selbst wenn man die Regeln im Kopf hat, ist das bei anderen Leuten, die sich den Code nachher ansehen m ussen, noch lange nicht der Fall. Daher sollte man bei nichttrivialen F allen Klammern zur Verdeutlichung setzen.
18
1.2.6
Konvertierung zwischen Integern und Charactern Character-Variablen k onnen in Integer-Variablen (oder andere Ganzzahl-Typen) konvertiert werden und umgedreht. Beispiel: char c = A; int z = c; int y = 48; char b = (char) y; Welche Werte enthalten jetzt z und b? Dies richtet sich nach dem ASCII-Wert der Zeichen. Diesen Wert kann man der Tabelle in Anhang C entnehmen. In diesem Beispiel ist also z=65 und b=0. Ist ein Character eine Zier, kann man mit char c = 3; int z = c-48; int y = c-0; //Ergibt 3 //Ergibt ebenfalls 3
diese Zier als Integer-Wert erhalten. Verkettungsoperator fu r Strings F ur Strings gibt es einen einzigen m oglichen Operator: +. Er h angt 2 Strings aneinander. Beispiel: String s = "Hallo "+"Welt"; Konvertieren in Strings Ist ein Argument eines +-Ausdrucks ein String, so werden alle anderen Sum manden automatisch in Strings verwandelt. Zum Beispiel: int i = 10; String s = "i = "+i; --> ergibt i = 10 --> ergibt "Hallo Welt"
System.out.println("Wert fuer i: "+i); Trick: Will man eine String-Darstellung eines primitiven Datentyps, kann man das nach folgendem Muster erhalten: int i = 10; String s = ""+i; //s hat jetzt den Wert "10"
Leider ist es nicht so einfach, Strings in primitive Datentypen zu wandeln. Wir werden einen Weg im n achsten Kapitel kennenlernen. Vorsicht Falle: Die Reihenfolge der Auswertung geht auch hier von links nach rechts vor sich. Dies kann auch f ur erfahrene Programmierer zur Falle werden:
1.3. KOMMENTARE System.out.println("a"+1+2); System.out.println(1+2+"a"); System.out.println("a"+1*2); System.out.println("a"+1-2); --> --> --> -->
19 a12 3a a2 (Punkt- vor Strichrechnung) Fehler: Substraktion von Strings nicht erlaubt.
1.3
Kommentare
Ein Kommentar ist ein St uck deutscher (oder englischer) Text, der gew ohnlich erkl art, was ein Programm an dieser Stelle tut. Java selbst ignoriert die Kommentare beim Compilieren. Zeilenkommentare: Das Kommentarzeichen f ur Zeilenkommentare besteht aus zwei Schr agstrichen //. Der Text ab dem Kommentarzeichen bis zum Zeilenende z ahlt als Kommentar. //Zeilenkommentar System.out.println("Hallo");
//Weiterer Zeilenkommentar
Blockkommentare: Blockkommentare beginnen mit /* und enden mit */. Alle Zeichen dazwischen z ahlen als Kommentar. /* Das ist ein grosser Blockkommentar */ Auch hierf ur gibt es Regeln in den Code Conventions. Blockkommentare, die u ber mehrere Zeilen gehen, soll eine Leerzeile vorausgehen, um den Kommen tar von den vorigen Zeilen abzusetzen. Der Kommentar selbst soll folgendes Aussehen haben:
/* * Here is a block comment. */ Eclipse versucht automatisch, einen Blockkommentar auf diese Weise zu formatieren. Ein weiterer, aus C bekannter Stil von Blockkommentaren ist: /*#*****************************************************\ * * * Hervorgehobener Teil * * * \*******************************************************/
20
Solche Kommentare sind stark hervorgehoben und gliedern den Quelltext gut. Man beachte das Doppelkreuz (#) als drittes Zeichen der ersten Zeile. Hier darf man keinen Stern verwenden, weil Java den Kommentar sonst als JavaDokumentationskommentar interpretieren w urde. Aus mir nicht bekannten Gr unden wird diese Form in den Code Conventions ausdr ucklich missbilligt: Comments should not be enclosed in large boxes drawn with asterisks or other characters.
1.3.1
Auch hier gibt es Regeln aus den Java Code Conventions. Die wichtigsten sind frei u bersetzt: Kommentare sollten einen Uberblick u atzliche ber den Code geben und zus Information bieten, die nicht aus dem Code selbst ersichtlich ist. Erl auterungen nicht oensichtlicher Design-Entscheidungen sollten kommentiert werden, jedoch keine Informationen, die klar aus dem Code her vorgehen. Die Gefahr ist zu gro, dass bei Code-Anderungen die Kommentare nicht aktualisiert werden und in Widerspruch zum Code geraten. Die Anzahl der Kommentare gibt manchmal die schlechte Qualit at des Codes wieder. Statt einen Kommentar hinzuzuf ugen, sollte man lieber erw agen, den Code besser und klarer zu schreiben. F ur uns bedeutet das zur Zeit: 1. Zu Beginn des Programms sollte ein Kommentar ber den Zweck des Programms stehen. 2. Zwischen gr oeren Code-Abschnitten sollte ebenfalls ein Kommentar stehen (zum Beispiel: Laden der Daten / Verarbeiten der Daten). Weitere Kommentare werden wir erst im sp ateren Verlauf des Kurses ben otigen.
1.4
Zu Java geh ort die sogenannte Java-Klassenbibliothek mit einer fast un uberschaubaren Anzahl von M oglichkeiten. Die meisten k onnen wir erst nutzen, wenn wir die Funktionsweise von Klassen kennen. Daneben m ussen wir uns auch mit Interfaces, abstrakten Klassen und Exceptions besch aftigt haben. Einige M oglichkeiten (die sogenannten Funktionen oder statische Methoden) k onnen wir aber jetzt schon ohne groe Vorbereitung nutzen.
1.4.1
Packages
Die Java-Klassenbibliothek ist in eine Anzahl von packages aufgeteilt. Um eine spezielle Klasse oder Methode (Funktion) zu nutzen, muss man ganz zu Beginn des Programms das Package importieren. Die Anweisung dazu steht noch vor der class-Anweisung und heit:
21
Eine einziges Package wird allerdings schon automatisch importiert: java.lang. Die meisten der nachfolgend beschriebenen Methoden benden sich in java.lang, so dass man sich die Import-Anweisung import java.lang.*; sparen kann (sie schadet aber auch nicht).
1.4.2
System.out.println Eine Methode haben wir schon kennengelernt: System.out.println. Das Argument wird auf dem Bildschirm ausgegeben, egal, ob man einen String oder einen primitiven Datentyp u achlich funktioniert es sogar bei bebergibt. Tats liebigen Objekten. Da die Klasse System in java.lang steht, m ussen Sie auch nichts importieren. System.out.print Wie System.out.println, allerdings ohne automatischen Zeilenvorschub am Ende. System.out.printf Formatierte Ausgabe. Das Format entspricht dem printf-Kommando aus C und wird in Anhang A erl autert. Existiert erst ab Java 5.
1.4.3
Das Einlesen von Daten von der Tastatur ging in den ersten Java-Versionen noch recht umst andlich und wurde dann Schritt f ur Schritt vereinfacht. Darum nden sich in unterschiedlichen B uchern, je nach Erscheinungsdatum, verschiedene Wege zur Eingabe von Daten. Alle Methoden sollen hier der Vollst andigkeit halber erw ahnt werden. Nur die letzten beiden beschr anken sich auf statische Methoden und sind zum jetzigen Zeitpunkt komplett zu verstehen. Als Beispiel wird jeweils eine Textzeile von der Tastatur in einen String eingelesen. Die alteste Methode benutzt einen BueredReader. String str; BufferedReader inp = new BufferReader(new InputStreamReader(System.in)); str = inp.readLine(); Ab Version 1.5 kann dies mit einem Scanner abgek urzt werden. Der Scanner hat deutlich m achtigere Befehle als der BueredReader.
22
KAPITEL 1. LINEARE PROGRAMME String str; Scanner inp = new Scanner(System.in); str = inp.nextLine(); Ab Version 1.6 gibt es eine noch einfachere M oglichkeit: String str; str = System.console().readLine(); Alternativ kann ab Version 1.2 ein grasches Eingabefenster mit einer Eingabeauorderung ge onet und ein String aus diesem Fenster eingelesen werden. Hier wird nur eine einzige statische Methode benutzt. Beispiel: String str; str = JOptionPane.showInputDialog("Bitte Text eingeben."); Die Klasse JOptionPane steht in der Package javax.swing. Als erste Zeile ist also import javax.swing.*; einzugeben. Ein vollst andiges Programm lautet: import javax.swing.*; public class Eingabe { public static void main(String[] args) { String name; System.out.println("********"); name = JOptionPane.showInputDialog("Bitte Name eingeben."); System.out.println(name); System.out.println("********"); } } Wenn Sie sich an einem bislang nicht erkl arbaren Parameter null nicht st oren, k onnen Sie auch Text in einem Fenster ausgeben: JOptionPane.showMessageDialog(null, String text)
1.4.4
23
verwandelt einen String in einen Integer, falls das geht. Ansonsten wird das Programm mit einer Fehlermeldung beendet. Das nachfolgende Beispiel liest einen String von Tastatur ein, wandelt ihn in eine Integer-Zahl um, erh oht die Zahl um 1 und gibt das Ergebnis auf dem Bildschirm aus. import javax.swing.*; public class Eingabe { public static void main(String[] args) { String name; int zahl; System.out.println("********"); name = JOptionPane.showInputDialog("Bitte Zahl eingeben."); zahl = Integer.parseInt(name); System.out.println(zahl+1); System.out.println("********"); } } Es gibt auch Long.parseLong(), Double.parseDouble(), Float.parseFloat() usw.. Alle Klassen Integer, Long, Double, Float (mit groen Anfangsbuchstaben) stehen in java.lang. Will man andersherum Zahlen in einen String verwandeln, gibt es dazu die Funktion String.valueOf: int a = 5; String s = String.valueOf(a); Oft wird auch der bereits beschriebene Trick verwendet, dass die Zahl zu einem Leerstring addiert wird: int a = 5; String s = "" + a;
1.4.5
In Math (java.lang) stehen ausschlielich statische Methoden. Zum Beispiel: Math.sin Math.sqrt Math.PI Beispiel: import javax.swing.*; public class Wurzel { public static void main(String[] args) { String name;
//Konstante PI
24
KAPITEL 1. LINEARE PROGRAMME int zahl; double wurzel; name = JOptionPane.showInputDialog("Bitte Zahl eingeben."); zahl = Integer.parseInt(name); wurzel = Math.sqrt(zahl); System.out.println("Die Wurzel aus "+zahl+" ist "+wurzel); }
} Sehen Sie einmal im Internet nach: http://java.sun.com/j2se/1.5.0/docs/api/index.html Jetzt links unten Math anklicken. Eine kurze Ubersicht u oglichkeiten bendet sich in Anber die Formatierungsm hang A.
1.4.6
Sonstiges
System.exit Der Befehl System.exit(int val); beendet das Programm sofort. Der Wert val wird aus dem Java-Programm zur uckgegeben und kann in einem Windows-Batch-Programm oder einer UNIXShell weiterverwendet werden.
1.4.7
Ubergabeparameter
Die Parameter, die wir beim Funktionsaufruf mitgeben, heien Ubergabepara meter. Es kann einen Ubergabeparameter geben (wie bei System.out.println) oder mehrere, wie im folgenden Beispiel: double x = 3; double y = 2; double z = Math.pow(x,y);
//Berechnet 3 hoch 2 = 9
Es gibt auch Funktionen ganz ohne Ubergabeparameter. Eine davon zeigt das n achste Beispiel: //Berechnet eine Zufallszahl im Intervall [0,1[ double z = Math.random();
25
1.5
1.5.1
Lineare Programme
Eingabe - Verarbeitung - Ausgabe (EVA)
Das EVA-Prinzip ist ein wichtiger Grundsatz in der Softwareentwicklung. Er bedeutet, dass ein Programm aus den drei Teilen Eingabe, Verarbeitung und Ausgabe besteht, die in dieser Reihenfolge bearbeitet werden. Obwohl das Prinzip sehr oensichtlich scheint, muss man bei komplexeren Programmen aufpassen, dass man nicht dagegen verst ot (und beispielsweise versucht, Daten zu vearbeiten, ohne dass welche eingegeben wurden). Mit dem bis jetzt erworbenen Kenntnissen k onnen wir kleine Programme schreiben, die das EVA-Prinzip voll umsetzen. Betrachten wir als Beispiel ein kleines Programm, das zwei IntegerZahlen a und b einliest und den Rest der Division von a/b ausgibt. public class Divisionsrest { public static void main(String[] args) { //Eingabe String zeile = JOptionPane.showInputDialog("Dividend: "); int a = Integer.parseInt(zeile); zeile = JOptionPane.showInputDialog("Divisor: "); int b = Integer.parseInt(zeile); //Verarbeitung int c = a%b; //Ausgabe System.out.println("Der Rest der Division ist "+c); } }
1.5.2
Struktogramme
Ein Struktogramm ist ein Konzept, ein Programm zu visualisieren. Struktogramme werden nach den Entwicklern Isaac Nassi und Ben Shneiderman auch Nassi-Shneiderman-Diagramme genannt (im Englischen wird das dann oft zu NS diagram abgek urzt). Struktogramme bestehen aus Elementen, die nach DIN 66261 genormt sind. Es gibt lediglich 11 solcher Elemente in der DIN. Einige andere sind g angig, aber nicht in der DIN-Norm enthalten. Nassi-Shneiderman-Diagramme wurden 1972/73 entwickelt, die DIN-Norm wurde 1985 aufgestellt. Die Weiterentwicklungen der letzten 20 Jahre im Bezug auf Programmiersprachen, vor allem die Objektorientierung, k onnen nicht mit Nassi-Shneiderman-Diagrammen wiedergegeben werden. In der Softwareentwicklung werden Nassi-Shneiderman-Diagramme eher selten eingesetzt, da normaler Programmcode einfacher zu schreiben und zu ver andern ist: Korrigiert man einen Fehler oder macht eine Erg anzung, muss man ein Nassi-Shneiderman Diagramm in der Regel komplett neu zeichnen. Daher
26
werden sie in der Regel nach Erstellung des Codes von entsprechenden Hilfsprogrammen automatisch erzeugt und dienen der Dokumentation. In der Lehre werden Struktogramme dagegen h aug verwendet, damit Sch uler den Aufbau logischer Abl aufe, die f ur die Programmierung n otig sind, trainieren k onnen. Die Erstellung von Struktogrammen ist immer noch Bestandteil vieler schulischer Abschlusspr ufungen. Das Elemente von Struktogrammen sind in Anhang D zu nden.
1.5.3
Flussdiagramme (auch Programmablaufpl ane genannt) stammen aus den 1960er Jahren und sind damit noch alter als Struktogramme. Objektorientierte Programmkonzepte lassen sich auch hier nicht darstellen. Der Vorteil gegen uber Struktogrammen ist, dass sich kleinere Anderungen leichter einbauen lassen, was besonders f ur die ersten Konzepte von Vorteil ist. Demgegen uber neigen Flussdiagramme dazu, zum un ubersichtlichen Pfeilsalat zu degenerieren. Auch lassen sich Struktogramme schneller und direkter in Programmcode u bersetzen. Zum Beispiel lassen sich Schleifen in Flussdiagrammen nicht auf den ersten Blick identizieren. Flussdiagramme sind nach DIN 66001 genormt. Die Elemente von Flussdiagrammen sind ebenfalls in Anhang D zu nden.
1.5.4
Aktivit atsdiagramm
Eine Weiterentwicklung des Flussdiagramms ist das Aktivit atsdiagramm. Das Aktivit atsdiagramm (engl. activity diagram) ist eine der Diagrammarten in der Unied Modeling Language (UML). Mit Aktivit atsdiagrammen lassen sich beispielsweise auch Exceptions oder parallele Programme modellieren. Allerdings gelten die Hauptnachteile der Flussdiagramme auch f ur Aktivit atsdiagramme. Ausf uhrlich werden Aktivit atsdiagramme in der Vorlesung Software Enginee ring behandelt. Wir werden uns hier auf die sehr ahnlichen, aber einfacheren Flussdiagramme beschr anken.
Kapitel 2
Kontrollstrukturen
2.1
2.1.1
Auswahl (Selektion)
Entweder-oder-Entscheidungen
Ein Java-Programm kann, abh angig von einem Kriterium, eine Entweder-oderEntscheidung treen, d.h. entweder einen bestimmten Programmteil A aus f uhren oder einen anderen bestimmten Programmteil B. Der n otige Code l asst sich sehr intuitiv verstehen. Sehen wir uns als Beispiel den Code an, der, abh angig vom Wert der Variablen note, den Text Bestanden oder Durchge fallen ausgibt:
Zuvorderst steht ein neues Schl usselwort: if (wenn). Dann folgt in Klammern das Kriterium: Wenn note kleiner als 5 ist. Wenn das der Fall ist, werden alle Anweisungen in dem folgenden, von geschweiften Klammern eingeschlossenen Block ausgef uhrt. Dann folgt ein weiteres Schl usselwort else (sonst), gefolgt von einem weiteren Block, der ausgef uhrt wird, wenn note eben nicht kleiner als 5 ist. Danach ist die Verzweigung beendet, das heit, der folgende Code wird auf jeden Fall ausgef uhrt und h angt nicht mehr von der if-Bedingung ab. Im Struktogramm sieht das so aus: 27
28
KAPITEL 2. KONTROLLSTRUKTUREN
Note < 5? ja
Ausgabe Bestanden
nein
Ausgabe Durchgefallen
Man kann auch den else-Teil komplett weglassen. Wenn man zum Beispiel die Meldung Durchgefallen nicht braucht, kann man schreiben: if (note<5) { System.out.println("Bestanden"); } System.out.println("Verzweigung beendet"); Das ist die sogenannte Einseitige Auswahl im Gegensatz zur Zweiseitigen Auswahl, die wir eben kennengelernt haben. Das Struktogramm hat jetzt folgendes Aussehen:
Note < 5? ja
Ausgabe Bestanden Ausgabe Verzweigung beendet
nein
Wenn wir anders herum den ja-Zweig (korrekt w urde man sagen: if-Zweig) weglassen und nur den nein-Zweig (korrekt: else-Zweig) implementieren wol len, wird es etwas schwieriger. Wir wandeln unser Beispiel so ab, dass nur der Text Durchgefallen ausgegeben werden soll. M oglich w are folgendes: if (note<5) { //kein guter Stil } else { System.out.println("Durchgefallen"); } Das l auft zwar, ist aber kein guter Stil. Besser ist es, die Bedingung umzudrehen: if (note>=5) { System.out.println("Durchgefallen"); }
29
Kommen wir schlielich noch zur Bedingung, die in den Klammern steht. Was darin steht, ist das Ergebnis der Rechenoperation note<5. Die Rechenoperation ist eine sogenannte Vergleichsoperation und das Ergebnis ist ein Wahrheitswert. Ein Wahrheitswert kann nur die beiden Werte wahr und falsch annehmen.
2.1.2
Boolean-Variablen
In Java gibt es einen eigenen Variablentyp f ur Wahrheitswerte. Dieser Typ hat den Namen boolean (deutsch: Boolesche Variable, benannt nach dem Mathematiker George Boole). Sie kann die Werte true (wahr) oder false (falsch) annehmen. boolean b; b = true; b = false; System.out.println(b);
//Ergibt false
2.1.3
Vergleichsoperatoren
Vergleichsoperationen liefern als Ergebnis einen Wahrheitswert: int a = 5; boolean b = a > 4; Der Ausdruck a > 4 hat als Ergebnis true, falls a > 4 und false, falls a 4. In diesem Beispiel h atte b also den Wert true. Es gibt 6 Vergleichsoperatoren: a a a a a a > b; >= b; == b; != b; <= b; < b;
//Gleichheitsoperator //Ungleichheitsoperator
Die Bezeichnungen sind selbsterkl arend. Nur zum Gleichheitsoperator == ist eine Erkl arung angebracht. Zun achst ein Beispiel: int a = 5; boolean b = a == 4; boolean c = a == a; b ist false, denn 5 = 4. c ist immer true, gleichg ultig, welchen Wert a hat. Der Gleichheitsoperator besteht aus zwei Ist-Gleich-Zeichen, da das einzelne Ist-Gleich-Zeichen schon vom Zuweisungsoperator belegt ist. Ein beliebter Fehler ist, statt dem Gleichheitsoperator == den Zuweisungs operator = zu benutzen. In Java f uhrt das in den meisten F allen gl ucklicher weise gleich zu einem Compilerfehler, so dass man die Verwechslung schnell bemerkt. Sollten sie dagegen C oder C++ Programme erstellen, kann die Verwechslung Ihnen schwer aundbare Fehler bescheren.
30
KAPITEL 2. KONTROLLSTRUKTUREN
2.1.4
Verzweigungen erfolgen immer aufgrund eines boolean-Wertes. Dies kann der Inhalt einer Variablen, das Ergebnis einer Vergleichsoperation oder einer Logikoperation (siehe n achster Abschnitt) sein. boolean x = (b!=0); if (x) { ... } oder if (b!=0) { ... }
2.1.5
Logikoperatoren
Logikoperatoren verkn upfen ein bis zwei boolean-Werte zu einem neuen booleanWert. Der einfachste Logik-Operator ist das Ausrufezeichen, das die Bezeichnung nicht tr agt. Dieser Operator negiert den Wert einer boolean Variablen. boolean b = false; boolean c = ! b; //ergibt true Weiterhin gibt es die Operatoren und (&&), oder (||) und exklusiv-oder (xor) (^). Dabei gilt: a && b ist true, falls a und b true sind. a || b ist true, falls a oder b true ist. a ^ b ist true, falls a und b nicht die gleichen Werte haben (xor-Operation). Beispiel: boolean boolean boolean boolean boolean a b c d e = = = = = true; false; a && b; a || b; a ^ b;
Sollten sie aus Versehen einmal & statt && oder | statt || schreiben, wird das in aller Regel auch funktionieren. Der Unterschied zwischen & und && bzw. zwischen | und || soll hier nur angedeutet werden. Bei den verdoppelten Operatoren wird der zweite Operand nicht mehr ausgewertet, wenn das Ergebnis nach dem ersten Operanden schon feststeht. Bei int a = int b = boolean boolean 1; 0; c = (b!=0) && (a/b == 1); d = (b!=0) & (a/b == 1);
ergibt sich c=false, die Bestimmung von d bricht wegen einer Division durch 0 ab. Wir werden immer && und || verwenden.
31
2.1.6
Operatorhierarchie
Wir kennen jetzt insgesamt die Operatoren + - * / % < > <= >= == != ! && || ^ Um die Abarbeitungs-Reihenfolge zwischen allen diesen Operatoren festzulegen, wird die Regel Punkt- vor Strichrechnung zu einer Operatorhierarchie erweitert. Weiter oben stehende Operatoren haben Vorrang vor weiter unten stehenden. * + < == ^ && || / % > <= >= !=
Der Ausdruck boolean b = a % 5 > 2 && a > 0 pr uft, ob der Rest von a oer als 2 und gleichzeitig a > 0 ist, ergibt also true 5 gr bei a = 3, 4, 8, 9, 13, 14, . . ..
2.1.7
Die onende und die schlieende geschweifte Klammer darf in der if-Anweisung weggelassen werden. Dies ist jedoch immer schlechter Stil und kann leicht zu schwer aundbaren Fehlern f uhren. Werden die Klammern weggelassen, besteht der if-Block aus der Zeile, die der if-Anweisung folgt und der else-Block besteht aus der Zeile, die der else-Anweisung folgt. Beispiel: if (wert%2==0) System.out.println("Die Zahl ist gerade"); Die Gefahr darin zeigt sich in folgendem Codeausschnitt: if (wert%2==0) System.out.println("Die Zahl ist gerade"); System.out.println("Die Zahl ist durch 2 teilbar"); Entgegen dem Anschein wird die zweite println-Zeile auch bei ungeraden Zahlen ausgef uhrt, denn Java verwendet wegen der fehlenden geschweiften Klammern nur die erste println-Zeile f ur den if-Block. Dies ist ein nachtr aglich schwer zu ndender Fehler, der von vornherein vermieden werden kann, wenn man konsequent Klammern f ur den if und den else-Block setzt. Ein weiteres schwer aundbarer Fehler ist:
32
Diesen Code muss man folgendermaen interpretieren: Falls die Bedingung wahr ist, werden die Anweisungen bis zum n achsten Semikolon ausgef uhrt, also bis zum Semikolon am Ende der if-Zeile. Anschlieend ist die if-Verweigung zu Ende. Die println-Anweisung wird also immer ausgef uhrt, gleichg ultig ob wert gerade oder ungerade ist.
2.1.8
if-else-Kaskaden
In einem Sonderfall l asst man teilweise die geschweiften Klammern aber doch weg. Manchmal gibt es mehr als zwei F alle, die unterschiedlich behandelt werden m ussen. In diesem Fall schachtelt man mehrere if-Anweisungen ineinander und erh alt die sogenannte if-else-Kaskade. Im nachfolgenden Beispiel bauen wir die Ausgabe einer Schulnote so weit aus, dass f ur jede Note ein individueller Text ausgegeben wird. if (note==1) { System.out.println("sehr gut"); } else if (note==2) { System.out.println("gut"); } else if (note==3) { System.out.println("befriedigend"); } else if (note==4) { System.out.println("ausreichend"); } else if (note==5) { System.out.println("mangelhaft"); } else { System.out.println("Fehler im Programm"); }
2.1.9
Mehrseitige Auswahl
In allen neueren Sprachen, gibt es eine Verzweigung, die, abh angig von einer Integer-Variablen, einen von mehreren Programmbl ocken anspringt. Das Notenprogramm, das im vorigen Kapitel mit einer if-else-Kaskade gel ost wurde, ist ein gutes Beispiel daf ur. Das Struktogramm dazu ist:
note 1
Ausgabe sehr gut
2
Ausgabe gut
3
Ausgabe befriedi gend
4
Ausgabe ausrei chend
5
Ausgabe mangel haft
sonst
Ausgabe Programm fehler
33
Die entsprechende Anweisung heit in Java switch-Anweisung. In anderen Sprachen ist sie als case- oder select-Anweisung bekannt. Sie beginnt in Java mit einer Zeile switch, gefolgt von der Variablen, deren Wert f ur die Verzweigung herangezogen wird: switch (note) { Dann folgt f ur jede Note ein sogenannte case-Block. Die erste Zeile eines caseBlocks wird eingeleitet durch das Schl usselwort case, gefolgt von dem Wert, f ur den der Block ausgef uhrt werden soll und einem Doppelpunkt: case 1: Dann kommen die Anweisungen f ur den Block. Es gibt keine geschweiften Klammern. Ein Block wird mit dem Befehl break abgeschlossen. switch(note) { case 1: System.out.println("sehr gut"); break; case 2: System.out.println("gut"); break; case 3: System.out.println("befriedigend"); break; case 4: System.out.println("ausreichend"); break; case 5: System.out.println("mangelhaft"); break; default: System.out.println("Fehler."); }//switch Am Ende der switch-Anweisung darf man noch einen sogenannten default-Block unterbringen, der immer dann ausgef uhrt wird, wenn keiner der vorigen caseBl ocke zutreend war. Man kann ihn auch weglassen. Dann wird statt dessen der switch-Block komplett u bersprungen. Die switch-Anweisung ist in Java f ur byte, short, char und int erlaubt. Strings funktionieren erst ab Java 7. long oder andere Datentypen sind in Java nicht m oglich (anders als in C# oder Skriptsprachen). Die switch-Anweisung hat ihre T ucken. Vor allem darf man das break am Ende nicht vergessen. Man kann das so verstehen: Die case- Zeilen sind Ansprungstellen. Das heit, wenn jetzt zum Beispiel die Note gleich 2 ist, wird die Zeile mit case 2 angesprungen. Dann l auft das Programm Zeile f ur Zeile weiter. Wird ein break erreicht, springt das Programm aus dem switch-Block heraus. Haben wir jetzt beispielsweise das break nach case 2 vergessen, dann l auft das Programm einfach Zeile f ur Zeile weiter, bis der switch-Block zu Ende ist oder ein break erreicht wurde. In unserem Beispiel w urde dann
34 gut befriedigend
KAPITEL 2. KONTROLLSTRUKTUREN
ausgegeben. Diesen Eekt kann man durch geschickte Programmierung auch ausnutzen und Zweige f ur ganze Bereiche denieren. Im folgenden Beispiel wird bei den Noten 1-4 der Text bestanden ausgegeben. switch(note) { case 1: case 2: case 3: case 4: System.out.println("bestanden"); break; case 5: System.out.println("mangelhaft"); break; default: System.out.println("Fehler."); }//switch Eine andere M oglichkeit, Bereiche bei switch-Anweisungen anzugeben, gibt es in Java (anders als z.B. in Pascal) leider nicht. Wenn man das folgende Beispiel mit einer switch-Verzweigung umsetzen wollte, br auchte man mehrere hundert case-Anweisungen. In diesem Fall greift man besser wieder auf if-else-Kaskade zur uck. if (windgeschwindigkeit<=2) { System.out.println("Windstille"); } else if (windgeschwindigkeit<=45) { System.out.println("schwacher Wind"); } else if (windgeschwindigkeit<=75) { System.out.println("starker Wind"); } else if (windgeschwindigkeit<=120) { System.out.println("Sturm"); } else if (windgeschwindigkeit<=200) { System.out.println("Orkan"); } else { System.out.println("Messgeraet kaputt, weil Wind zu stark"); }
2.2
Schleifen(Iteration)
Beispiel zur Einfu hrung Es soll ein Programm geschrieben werden, das einen Countdown simuliert. Es soll also nacheinander die Werte von 10 bis 0 auf dem Bildschirm ausgeben. Mit den bisherigen Mitteln w urden wir dies so l osen:
35
Es werden 10 fast gleiche Zeilen ausgef uhrt. Da muss es doch eine k urzere L osung geben. Wir br auchten eine Anweisung wie: Fuehre die Zeile System.out.println(i) nacheinander mit i=10,9,8,...,1,0 aus. Dies f uhrt zur h augsten Schleifenvariante: der Z ahlschleife.
2.2.1
Die Z ahlschleife
Die Z ahlschleife ist eine Schleifenart, bei der von Anfang an feststeht, wieviele Wiederholungen der Schleife ausgef uhrt werden. Es gibt einen Z ahler (Laufvariable) der von einem Anfangswert bis zu einem Endwert l auft und sich bei jedem Durchlauf um einen festen Betrag andert. Das Struktogramm der Z ahlschleife ist:
Anweisungsblock
Anweisungsblock
Z ahlschleifen werden in Java mit dem Schl usselwort for eingeleitet. for (i=10; i<=20; i++) { System.out.println(i); }
36
KAPITEL 2. KONTROLLSTRUKTUREN
Dem Schl usselwort folgt eine Parameterliste, die in einer runden Klammer zusammengefasst ist und aus 3 Teilen besteht, die jeweils durch ein Semikolon getrennt sind. Die 3 Teile sind: 1. Initialisierung der Laufvariablen. 2. Abbruchbedingung (kein Abbruch, solange die Bedingung erf ullt ist). 3. Ver anderung der Laufvariablen. Die Reihenfolge, in der die Teile abgearbeitet werden, veranschaulicht das folgende Flussdiagramm:
i = 10
i <= 20?
ja
Anweisungsblock System.out.println(i);
i++
nein
Schleifenende
Das Countdown-Programm w urde damit wie folgt aussehen: int i; for (i=10; i>=0; i--) { System.out.println(i); }
2.2.2
Anderung der Laufvariablen im Schleifenko rper Es ist m oglich, die Laufvariable im Schleifenk orper, also zwischen den geschweiften Klammern, zu a ndern. Ein Beispiel daf u r ist: for (i=10; i>=0; i++) { i=i-2; System.out.println(i); } Damit ist die Schleife aber keine reine Z ahlschleife mehr. In anderen Sprachen (z.B. Pascal) ist das auch verboten. In Java ist es m oglich, aber schlechter Programmierstil. Nat urlich ist es beim obigen Beispiel besonders unsinnig, aber man sollte es generell unterlassen und lieber auf eine while-Schleife (folgt in K urze) ausweichen.
2.2. SCHLEIFEN(ITERATION) Deklaration der Laufvariablen in der Schleife Es ist m oglich, die Laufvariable im Schleifenkopf zu deklarieren: for (int i=10; i>=0; i--)
37
Dann ist die Laufvariable nur in der Schleife g ultig und kann nach Beendigung der Schleife nicht mehr angesprochen werden. Diese Form ist die u blichste Form einer for-Schleife. Schleife ohne geschweifte Klammern Die Anweisung for (int i=10; i>=0; i--); ist eine leere Schleife. Die Variable i wird von 10 auf 0 heruntergez ahlt. Endlosschleife Die Anweisung for(;;) ist eine Endlosschleife. Der Anweisungsblock wird endlos wiederholt. Da innerhalb des Anweisungsblocks mit break die Schleife verlassen werden kann (siehe dazu die folgenden Kapitel), kann diese Anweisung sinnvoll eingesetzt werden. Dagegen ist for(;;); eine leere Endlosschleife, die fortgesetzt durchlaufen wird. Aus Benutzersicht r uhrt sich dann gar nichts mehr, bis der Benutzer das Programm abbricht. Man sagt, das Programm hat sich aufgeh angt.
2.2.3
Bei manchen Schleifen steht zu Anfang die Anzahl der Durchl aufe noch nicht fest. Es kann sein, dass es mehrere Abbruchbedingungen gibt oder dass die Laufvariable ihre Werte unvorhersehbar ver andern kann. Hier benutzt man entweder die kopfgesteuerte oder die fugesteuerte Schleife. Die kopfgesteuerte Schleife hat das Aussehen while(Bedingung) { Anweisungs-Block } Das bedeutet, dass der Anweisungsblock ausgef uhrt wird, solange die Bedingung in der while-Zeile den Wert true ergibt. Das entsprechende Struktogramm hat das Aussehen:
38
KAPITEL 2. KONTROLLSTRUKTUREN
while Bedingung
Anweisungsblock
Sinnvolle Beispiele sind bereits etwas komplizierter. Wir nehmen ein CountdownProgramm, dass von 100 r uckw arts bis 10 z ahlt, aber alle durch 7 teilbaren Zahler ausl asst: int i=100; while (i>=10) { System.out.println(i); i--; if (i%7==0) { i--; //Durch 7 teilbare Zahlen "uberspringen } } for- und while-Schleifen Im vorigen Kapitel wurde erw ahnt, dass for-Schleifen in C bzw. Java u ber reine Z ahlschleifen hinaus einsetzbar sind. Tats achlich sind sie nur eine Kurzform f ur eine while-Schleife. Die Schleifen: int i; for (i=0; i<10; i++) { System.out.println(i); } und int i=0; while (i<10) { System.out.println(i); i++; } enstsprechen sich exakt. for- und while-Schleifen lassen sich immer gegenseitig umwandeln. Es gibt daher eine Grundregel, die sagt: for-Schleifen werden nur bei wirklichen Z ahlschleifen eingesetzt, ansonsten werden kopf- oder fugesteuerte Schleifen benutzt.
2.2. SCHLEIFEN(ITERATION)
39
2.2.4
Die Schleife mit Endabfrage ahnelt der Schleife mit Anfangsabfrage. Allerdings wird die fugesteuerte Schleife mindestens einmal durchlaufen, w ahrend demgegen uber die kopfgesteuerte Schleife gar nicht durchlaufen wird, wenn die Anfangsbedingung vor dem 1. Durchlauf nicht erf ullt ist. Die Schleife mit Endabfrage hat folgendes Aussehen: do { //Anweisungsblock } while (Bedingung) Das Struktogramm ist:
Anweisungsblock
while Bedingung
Das Countdown-Beispiel hat hier folgendes Aussehen: int i=10; do { System.out.println(i); i--; } while (i>=0); In C und Java wird diese Schleife auch do-while-Schleife genannt, in Unterscheidung zur kopfgesteuerten while-Schleife. In Pascal spricht man stattdessen von einer repeat-until-Schleife. Da die fugesteuerte Schleife sich immer mit einer kopfgesteuerten nachbilden l asst, besitzen manche Sprachen (z.B. Fortran, Python) keine fugesteuerte Schleife.
2.2.5
continue Innerhalb einer Schleife kann mit dem Befehl continue der aktuelle Schleifendurchlauf abgebrochen werden, d.h. das Programm wird mit dem Beginn des n achsten Schleifendurchlaufs fortgesetzt. Die continue-Anweisung kann in allen Schleifenvarianten benutzt werden. Das folgende Countdown-Programm ist mit continue so abgewandelt, dass die Zahl 3 ausgelassen wird.
KAPITEL 2. KONTROLLSTRUKTUREN
Der break-Befehl bewirkt, dass eine Schleife komplett abgebrochen wird. Das Programm for (int i=10; i>=0; i--) { if (i==3) { break; } System.out.println(i); } z ahlt nur bis zur Zahl 4 herunter. Mittengesteuerte Schleifen In manchen F allen m ochte man gerne die Abbruchbedingung in der Mitte der Schleife testen. Mit Hilfe eines Flags (einer Boolean-Variablen) kann man dies mit einer fugesteuerten Schleife nachbilden. Ein typisches Beispiel ist die Behandlung einer Benutzereingabe, die im folgenden Flussdiagramm erl autert ist.
Eingabeauorderung
Benutzereingabe
Eingabe korrekt?
nein
Fehlermeldung
ja
Schleifenende
Mit einem Flag und einer fugesteuerten Schleife ergibt das: boolean korrekt = false; String dreiB; do { dreiB = JOptionPane.showInputDialog("Bitte 3 Buchstaben eingeben: "); if (dreiB.length() == 3) { korrekt = true; } else { System.out.println("Fehler bei der Eingabe.");
41
K urzer (aber f ur manche Puristen unsauber) geht es mit einer Endlosschleife und der break-Anweisung. Als Syntax f ur Endlosschleifen haben wir schon for (;;) { //Anweisungsblock } kennengelernt. Eine Alternative ist: while(true) { //Anweisungsblock } Die Eingaberoutine sieht jetzt wie folgt aus: String dreiB; while(true) { dreiB = JOptionPane.showInputDialog("Bitte 3 Buchstaben eingeben: "); if (dreiB.length() == 3) { break; } System.out.println("Fehler bei der Eingabe."); }
2.2.6
Mehrere verschachtelte Schleifen sind m oglich, wie am nachfolgenden Beispiel mehrerer verschachtelter for-Schleifen zu sehen ist. Es gibt ein Dreieck aus Sternen aus: * ** *** **** ***** Der Code ist: public class Dreieck { public static void main(String[] args) { int max=5; for (int i=0; i<max; i++) { for (int j=0; j<=i; j++) { System.out.print("*"); }
KAPITEL 2. KONTROLLSTRUKTUREN
break- und continue-Befehle in verschachtelten Schleifen wirken sich nur auf die jeweils innerste Schleife aus. Soll sich ein break oder continue direkt auf eine auere Schleife auswirken, muss diese Schleife mit einem Label versehen werden. Das folgende Beispiel gibt das kleine Einmaleins aus, bis eine Zahl > 50 auftaucht (also bis 9*6=54). outer: for (int i=1; i<=10; i++) { inner: for (int j=1; j<=10; j++) { System.out.printf("%d*%d=%2d\n",i,j,i*j); if (i*j>50) { break outer; } } }
2.2.7
Aufz ahlungsschleife
Der letzte Schleifentyp, die Aufz ahlungs- oder foreach-Schleife, m ussen wir hinter das n achste Kapitel verschieben, da wir dazu Felder ben otigen.
2.2.8
Variablen k onnen an jeder Stelle im Code deklariert werden, also auch innerhalb von Verzweigungen und Schleifen. Solche Variablen sind allerdings auch nur innerhalb der jeweiligen Schleife oder Verzweigung g ultig. Auerhalb sind sie nicht mehr ansprechbar.
//Compiler-Fehler
//i=1
2.2. SCHLEIFEN(ITERATION)
43
//Compiler-Fehler
Die Laufvariable von for-Schleifen kann innerhalb der for-Anweisung deklariert werden. Diese Variable ist nur innerhalb der for-Schleife g ultig. Java, C++, C# for (int i=1; i<=10; i++) { System.out.println(i); } System.out.println(i); //Compiler-Fehler --------------------------int i; for (i=1; i<=10; i++) { System.out.println(i); } System.out.println(i);
//ergibt 11
Eine typische Pascal-Falle tritt in dieser Art in Java nicht auf: Pascal-Falle: FOR i:=1 TO 10 DO BEGIN writeln(i); END; writeln(i); (* i ist unbestimmt, nicht unbedingt=11 *)
2.2.9
Tabellen
Die Ausgabe von Tabellen wird sehr h aug gebraucht, daher hier ein einfaches Beispiel. Wir wollen eine Logarithmus-Tabelle mit den Logarithmen von 1-20 in Schritten von 0,1 ausgeben. public class Logarithmus { public static void main(String[] args) { for (double i=1; i<=20; i+=0.1) { System.out.printf("%4.1f %7.5f\n",i,Math.log(i)); } } }
44
KAPITEL 2. KONTROLLSTRUKTUREN
Kapitel 3
Um die Frage zu kl aren, was Objekte nun genau sind, machen wir einen kurzen Ausug von der Computer- in die richtige Welt. Ein Beispiel f ur ein Objekt, das sich gut als Anschauung f ur Objekte in Java eignet, ist ein Klammerger at, wie es in der folgenden Abbildung zu sehen ist:
Wendematrize Erm oglicht feste und oene Heftung Onen des Klammermagazins Heften
Ein solches Klammerger at ist ein Objekt. Jedes Klammerger at hat einen bestimmten Zustand. Bei unserem Klammerger at k onnten wir den Zustand folgendermaen beschreiben: Klammermagazin oen oder geschlossen. 45
46
KAPITEL 3. DAS OBJEKTORIENTIERTE KONZEPT VON JAVA Anzahl der Klammern im Magazin. Wendematrize eingestellt f ur oene oder geschlossene Heftung.
Andere Zust ande (zerkratzt, verbeult, kaputt) vernachl assigen wir einmal. Ferner kann man mit einem Klammerger at bestimmte Aktionen durchf uhren. In der objektorientierten Sprache heit das, ein Objekt hat bestimmte Methoden. Generell unterscheidet man zwischen Abfragemethoden, die den Zustand der Objekts nicht andern und Modizierern, die den Zustand des Objekts andern. Modizierer sind in unserem Beispiel: Klammern. Der Zustand wird ge andert, da eine Klammer aus dem Magazin entfernt wird. Wenn noch Klammern vorhanden waren, wird auerdem der Zustand des Papierstapels (oder des Daumens) ver andert. Onen des Klammermagazins. Schlieen des Klammermagazins. Hinzuf ugen oder Entfernen von Klammern aus dem Magazin (geht nur bei ge onetem Magazin). Drehen der Wendematrize. Abfragemethoden sind Feststellen des Zustands der Wendematrize. Feststellen des Zustands des Klammermagazins (oen/geschlossen). Ermitteln der Anzahl der Klammern im Magazin. Wichtig ist, dass am Ende der urspr ungliche Zustand wiederhergestellt wird, d.h. wenn vorher das Klammermagazin geschlossen war, muss es am Ende auch wieder geschlossen werden. Objekte und Klassen Jedes Objekt ist ein Objekt einer bestimmten Klasse. In unserem Beispiel heit die Klasse Klammerger at. Ein Objekt der Klasse Klammerger at ist ein bestimmtes Klammerger at. Zwei verschiedene Objekte unterscheiden sich m oglicherweise durch ihren Zustand, also beispielsweise durch die Anzahl von Klammern in ihrem Magazin. Aber auch wenn ihr Zustand gleich ist, sind zwei Klammerger ate doch zwei verschiedene Objekte. Man sagt dann, die Objekte sind Kopien oder Klone voneinander.
3.1.2
Wichtig bei der Objektorientierung ist die Unterscheidung zwischen Anwenderund Entwicklersicht. In unserem Beispiel haben wir uns bisher nur mit der Anwendersicht besch aftigt. Der Entwickler des Klammerger ats hat aber einen umfassenderen Blickwinkel:
47
Er kennt verborgene Details, wie z.B. Materialeigenschaften, mit denen sich der Anwender nicht besch aftigt. Ihm steht eine gr oere Funktionalit at zur Verf ugung, z.B. setzt er ein Klammerger at zusammen oder nimmt es auseinander. Er legt, und das ist eine wichtige Aufgabe, die Methoden fest, die der Anwender ausf uhren darf. Er entscheidet z.B., ob er dem Anwender die M oglichkeit geben will, das Klammerger at auseinanderzunehmen. Die Liste der Anwender-M oglichkeiten, die der Entwickler festlegt, ist die Anwenderschnittstelle. Die g angige Abk urzung in der Software-Entwicklung daf ur ist API (application programming interface, deutsch: Schnittstel le zur Anwendungsprogrammierung). Die Anwenderschnittstelle f ur das Klammerger at sind in unserem Beispiel die oben aufgelisteten Methoden. Andere Sichten Sp ater werden wir die Rolle des Anwenders noch dierenzieren m ussen: In den Projektentwickler, den Endanwender und den Wiederverwerter. Diese Dierenzierung gibt unser anschauliches Beispiel aber noch nicht her.
3.2
3.2.1
Mathematische Bru che als Objekte In der Software-Entwicklung werden wir nat urlich keine Klammerger ate, sondern passendere Objekte verwenden. Wir nehmen als erstes Beispiel eine Klasse namens Bruch, die rationale Zahlen repr asentiert. Diese Klasse ist uns vorgegeben und bendet sich in den Vorlesungsunterlagen. Die Attribute dieser Klasse sind der Z ahler und der Nenner des Bruchs. Es gibt Methoden zum Abfragen und Setzen der Attribute und f ur diverse mathematische Operationen. Bruch Attribute: int zaehler int nenner setZaehler setNenner getZaehler getNenner multipliziere ... weitere Methoden
Wir werden sie in diesem Kapitel komplett betrachten, anwenden und ihre Eigenschaften untersuchen. In einem sp ateren Kapitel werden wir sie schlielich selbst nachprogrammieren. Bis auf Weiteres sehen wir die Klasse Bruch aber rein aus Anwendersicht.
48
Aufgabenstellung Zum Testen der Klasse Bruch formulieren wir uns eine erste kleine Aufgabenstellung. Wir wollen ein Programm schreiben, das den Endanwender nach einem Bruch fragt und diesen Bruch gek urzt als gemischten Bruch auf dem Bildschirm ausgibt. Also z.B.: Eingabe: Eingabe: Eingabe: Eingabe: Eingabe: 5/4 8/6 2/2 2/8 2/3 -> -> -> -> -> Ausgabe: Ausgabe: Ausgabe: Ausgabe: Ausgabe: 1+1/4 1+1/3 1 0+1/4 0+2/3
3.2.2
Wir wollen zu unserer Berechnung die Klasse Bruch benutzen. Sehen wir uns zun achst die entsprechende API an. Sie liegt in der sogenannten javadoc-Form vor. Das ist die Form, in der auch die Java-API geschrieben ist. Die wichtigsten Ausschnitte davon sind in Anhang E zu nden. Am Anfang der Javadoc bendet sich eine kurze Beschreibung der Klasse. Sie lautet: Repr asentiert einen Bruch mit Integer-Werten als Z ahler und Nenner. Invariante: Der Bruch ist immer gek urzt. Der Nenner ist immer gr oer als 0. Eine Invariante ist eine Eigenschaft, die aus Anwendersicht immer erhalten bleibt. Unsere Bruch-Objekte werden also immer gek urzt sein. Das ist f ur unser Programm schon einmal sehr praktisch. Wir entnehmen ferner, dass der Nenner nicht 0 werden darf und dass negative Nenner umgewandelt werden. Der Bruch 1 1 2 wird also zu 2 . Das werden wir hier nicht brauchen, da wir der Einfachheit halber negative Br uche ganz ausgeschlossen haben.
3.2.3
Konstruktoren Zun achst sehen wir uns aus der API die sogenannten Konstruktoren (englisch Constructor) an. Aus der Tabelle entnehmen wir die folgenden drei Konstruktoren: Bruch(int zaehler, int nenner) Erzeugt einen Bruch mit dem gegebenen Z ahler und Nenner. Bruch(String s) Erzeugt einen Bruch aus einem String der Form Z ahler/Nenner. Bruch(Bruch r) Erzeugt eine Kopie (Klon) des u bergebenen Bruchs (Copy-Konstruktor). Uber die Konstruktoren k onnen wir den Anfangswert des Bruchs festlegen. Wir haben die Auswahl, ob wir Z ahler und Nenner als Integer-Wert u bergeben wollen oder als String oder ob der neue Bruch eine Kopie eines schon bestehenden sein soll.
49
Der ganze Vorgang hat zwei Stufen. Zun achst einmal wird hier ohne weitere Begr undung der Vorgang der Deklaration und Erzeugung erkl art. Die Begr undung folgt sp ater in mehreren Schritten. Zun achst wird eine Variable vom Typ Bruch deklariert: Bruch b; Dann wird mit dem new-Operator und einem der Konstruktoren ein neues Objekt erzeugt (hier ein Bruch vom Wert 1 3 ). b = new Bruch(1,3); Beide Schritte kann man auch in einem zusammenfassen: Bruch b = new Bruch(1,3); Start des Testprogramms Mit dieser Information k onnen wir beginnen, uns um das Testprogramm zu k ummern. Zuerst fragen wir den Endanwender in einer JOptionPane nach einer Bruchzahl. Danach verwenden wir zweckm aigerweise den Konstruktor mit dem String, um ein Bruch-Objekt zu erzeugen. public static void main(String[] args) { Bruch b; String s = JOptionPane.showInputDialog("Bruch eingeben (Zaehler/Nenner)"); b = new Bruch(s); } Solange die Benutzereingabe ein korrekter Bruch ist, klappt das.
3.2.4
Exceptions
Geben wir in die JOptionPane statt Zahlen Buchstaben ein, so bricht das Programm ab und auf dem Bildschirm erscheint die Fehlermeldung: Exception in Thread "main" java.lang.NumberFormatException: Format muss zaehler/nenner sein. Als n achstes versuchen wir, einen Bruch mit dem Nenner 0 zu erzeugen. Auch hier bricht das Programm ab und die Fehlermeldung ist: Exception in Thread "main" java.lang.ArithmeticException: Nenner gleich 0. Es w are sicher sinnvoll, die Eingabe wiederholen zu lassen. Um zu verstehen, wie man das erreicht und was das Wort Exception bedeutet, m ussen wir etwas ausholen:
50
Verbotene Operationen Wir haben schon die sogenannte Invarianten angesprochen: Eigenschaften des Objekts, die aus Anwendersicht immer erhalten bleiben. Versucht der Anwender, eine Invariante zu verletzen, also hier den Nenner explizit auf Null zu setzen, darf das Objekt dem nicht Folge leisten. Die Frage ist allerdings, was das Objekt statt dessen machen soll. Soll es den Nenner so lassen, wie er ist? Eine Fehlermeldung auf dem Bildschirm ausgeben? Soll es den Nenner auf den kleinstm oglichen Wert setzen? Egal, was das Bruch-Objekt macht, es wird das gesamte Programm in einen Zustand versetzen, der so nicht vorgesehen war. Das Objekt k onnte zwar eine Fehlervariable setzen, aber dann w are es Aufgabe des Anwenders, zu u berpr ufen, ob das Objekt noch in einem korrekten Zustand ist. Wenn der Umgang mit dem Objekt aber sicher sein soll, dann muss das Objekt dem Anwender(-programm) sagen, dass ein Fehler aufgetreten ist, und zwar so, dass der Anwender es zur Kenntnis nehmen muss. Das tut es zur Zeit auch, in dem es das Programm einfach abbricht. Aber nat urlich darf ein Programm auch nicht bei jedem Fehler sofort abbrechen. Verfeinerung der Rollenperspektiven Jetzt ist es an der Zeit f ur eine Verfeinerung der Perspektive des Anwenders: Den Entwickler der Klasse Bruch nennen wir zur besseren Unterscheidung jetzt Modulentwickler. Wir entwickeln zur Zeit ein komplettes Programm, das die Klasse Bruch benutzt. Wir sind sozusagen Anwender der Klasse Bruch. Gleichzeitig sind wir aber auch der Entwickler eines fertigen Programms (es gibt daher auch eine main-Funktion). Unsere Rolle nenner wir daher Projekt-Entwickler. Damit ist gemeint, dass wir Module anwenden und zu einem Projekt zusammenfassen. Unser Programm wird am Ende von einem Endanwender bedient. Der Endanwender ist kein Programmierer, sondern benutzt lediglich die Bedienober ache. Der Wiederverwender ist ein Anwender, der Objekte f ur einen anderen Zweck benutzen will, als den, f ur das sie eigentlich gedacht sind. Dazu muss er die Klasse erweitern oder modizieren. Dazu ist das Prinzip der Objektorientierung besonders geeignet. Wir werden in einem sp ateren Kapitel darauf zur uckkommen. Wir sitzen also zwischen Modul-Entwickler und Endanwender. F uhren wir uns folgende Uberlegungen vor Augen: Wir haben im Bruch-Objekt eine verbotene Operation ausgel ost. Der Modul-Entwickler kann nicht wissen, wie wir darauf reagieren wollen und meldet uns den Fehler.
51
Der Endanwender (der vielleicht eine falsche Eingabe gemacht hat) will eine sinnvolle Fehlermeldung sehen und keinesfalls einen Programmabsturz mit einer Java-Fehlermeldung. Das heit zusammengefasst: Sie als Modul-Anwender m ussen auf die Fehlermeldung des Objekts reagieren und dem Endanwender eine sinnvolle Fehlermeldung pr asentieren. Fehlerbehandlung Das Bruch-Objekt l ost genau genommen eine sogenannte Exception (deutsch: Ausnahme) aus. Umgangssprachlich sagt man: Die Klasse wirft eine Excepti on. Es gibt verschiedene Arten von Exceptions. Wir m ussen hier mit ArithmeticExceptions und NumberFormatExceptions rechnen.1 Eine ausgel oste Exception muss von uns als Modul-Anwender gefangen werden, ansonsten kommt es zum Programmabbruch. Die L osung sieht wie folgt aus: try { b = new Bruch(s); } catch (ArithmeticException e) { //Fehlerbehandlung, wenn der Nenner 0 ist } catch (NumberFormatException e) { //Fehlerbehandlung, wenn sich s nicht parsen laesst } Wir nden hier zwei neue Schl usselworte try und catch. Wenn im ersten Block, dem sogenannten try-Block eine Exception ausgel ost wird, dann wird gesucht, ob f ur diese Exception ein zugeh origer catch-Block existiert. Wenn ja, dann werden die Programmzeilen im catch-Block ausgef uhrt und das Programm hinter dem letzten catch-Block fortgesetzt. Wir wollen dem Endanwender nach einer Fehleingabe eine Fehlermeldung ausgeben und ihn die Eingabe wiederholen lassen. Dazu gibt es einen Trick, der sehr gut die Funktionsweise des try-catch-Blocks zeigt. Das Flussdiagramm und der zugeh orige Java-Code ist wie folgt:
Eingabeauorderung
Exception
Fehlermeldung
Diese Information steht in dem Teil der javadoc, der im Skript nicht abgebildet ist
52
//break bei korrekter Benutzereingabe while(true) { try { String s = JOptionPane.showInputDialog("Bruch eingeben (Zaehler/Nenner)"); b = new Bruch(s); //Bis hierhin kommt das Programm nur, wenn keine Exception auftrat break; //while(true) wird verlassen } catch(NumberFormatException e) { System.out.println("Eingabeformat stimmt nicht."); } catch(ArithmeticException e) { System.out.println("Nenner darf nicht Null sein."); } //Nach einer Exception wird hier fortgesetzt und //der naechste Schleifendurchlauf begonnen } Die Einleseroutine steht in einer Endlosschleife. Der Trick ist, dass nach einem catch-Block das Programm hinter den catch-Bl ocken fortgesetzt wird, was bedeutet, dass ein neuer Schleifendurchlauf beginnt. Der break -Befehl wird nur erreicht, falls beim Erzeugen des Bruch-Objekts keine Exception aufgetreten ist. Die genaue Notation des try- und des catch-Blocks nehmen wir zun achst einfach hin. Wir werden sie sp ater noch genauer untersuchen.
3.2.5
Methoden
Methoden aufrufen Wir haben also jetzt unser Bruch-Objekt erzeugt und k onnen es unter dem Variablennamen b ansprechen. Nun wollen wir die gemischten Br uche berechnen. Negative Br uche lassen wir der Einfachheit halber auen vor. Zun achst bestimmen wir den ganzzahligen Anteil des Bruchs. Er berechnet sich aus der Integer-Division von Z ahler und Nenner. Diese beiden Werte erhalten wir, indem wir vom Bruch b die Methoden getZaehler() und getNenner() aufrufen: int ganzzahl = b.getZaehler() / b.getNenner(); Das Format ist also Objektname Punkt Methodenname. Der Z ahler des Restbruchs (also des Bruchs abz uglich des ganzzahligen Anteils) berechnet sich zu Z ahler mod Nenner. Wir setzen ihn mit der Methode setZaehler(int zaehler). Dabei ist der Wert in der Klammer der sogenannte Ubergabeparameter . int restZaehler = b.getZaehler() % b.getNenner(); b.setZaehler(restZaehler); Wir haben den Vorteil, dass der Bruch bereits automatisch von der BruchKlasse gek urzt wird und wir das nichts mehr selbst machen m ussen. Wir m ussen nur noch den ganzzahligen Anteil und den Rest auf dem Bildschirm ausgeben:
3.2. SOFTWARE-OBJEKTE AUS ANWENDERSICHT System.out.print(ganzzahl); if (restZaehler>0) { System.out.print("+"); System.out.println(b); } else { System.out.println(); } Die Zeile System.out.println(b); wird intern automatisch in System.out.println(b.toString());
53
umgewandelt. Es wird also der String ausgegeben, den man von der Methode toString zur uckerh alt. Zusammenh angend ergibt sich also f ur die Verarbeitung: int ganzzahl = b.getZaehler() / b.getNenner(); int restZaehler = b.getZaehler() % b.getNenner(); b.setZaehler(restZaehler); System.out.print(ganzzahl); if (restZaehler>0) { System.out.print("+"); System.out.println(b); } else { System.out.println(); } Beispiel 2 Wir stellen uns eine weitere Aufgabe. Wir wollen ein Programm schreiben, das vom Benutzer die Eingabe einer nat urlichen Zahl n verlangt und daraufhin die Summe
n
sn =
i=1
1 i
als Bruch auf dem Bildschirm ausgibt. Hier sparen wir uns der Einfachheit halber die Benutzereingabe und nehmen an, n sei schon besetzt. Zun achst erzeugen wir uns eine Bruch-Variable f ur die Summe und eine f ur das Reihenglied. Bruch summe = new Bruch(0,1); Bruch element = new Bruch(1,1); Nun setzen wir in einer Schleife den Nenner des n achsten Elements und addieren es zur Summe. Wieder haben wir den Vorteil, dass die Br uche schon automatisch gek urzt werden. for (int i=1; i<=n; i++) { element.setNenner(i); summe.add(element); }
54
Am Ende wird das Ergebnis (als Bruch und als double-Wert) ausgegeben: System.out.println(summe); System.out.println(summe.getDoubleWert()); Es fehlen uns allerdings noch einige wichtige Details zur Benutzung der restlichen Methoden der Bruch-Klasse. Diese werden wir im n achsten Kapitel betrachten. Polymorphie In der API nden wir zwei Versionen der Methode mult: Bruch Bruch mult(Bruch r) Multipliziert den Bruch mit dem u bergebenen Bruch. mult(int faktor) Multipliziert den Bruch mit dem angegebenen Faktor.
Generell kann es mehrere Methoden mit gleichem Namen aber unterschiedli chen Ubergabeparametern geben, d.h. mit Ubergabeparametern unterschiedlicher Anzahl oder unterschiedlichen Typs. Je nachdem, welche Parameter man beim Aufruf u bergibt, wird automatisch die passende Methode herausgesucht. Man sagt, die Methode ist u berladen. Dies ist ein Beispiel, dass ein Methode mit gleichem Namen mehrfach implementiert ist. Allgemein nennt man diese Eigenschaft der Objektoriertierung Polymorphie, d.h. Vielgestaltigkeit. Unver anderliche Objekte Es gibt ein weiteres Paar von Befehlen, n amlich Bruch Bruch getInverse() Gibt die Inverse des Bruchs zur uck. inverse() Invertiert den Bruch.
Beide Befehle berechnen die Inverse eines Bruchs. Der Unterschied wird im folgenden Codest uck deutlich. //Version 1 Bruch b = new Bruch(1,2); b.inverse(); //invertiert b. b hat jetzt den Wert 2/1. //Version 2 Bruch x = new Bruch(1,2); Bruch y = x.getInverse(); //x hat immer noch den Wert 1/2. //y hat den Wert 2/1. Version 1 (inverse()) invertiert das Objekt selbst. Version 2 (getInverse()) l asst das Objekt unver andert und erzeugt ein zweites Objekt, das den Wert der Inversen hat. Dazu ist zu bemerken:
3.2. SOFTWARE-OBJEKTE AUS ANWENDERSICHT Version 1 ist schneller, da kein neues Objekt erzeugt werden muss.
55
Der Umgang mit einer Klasse wird vereinfacht, wenn entweder durchgehend Version 1 oder Version 2 benutzt wird.2 Wenn eine Klasse ausschlielich Methoden der Version 2 hat, dann ist der Inhalt der Objekte unver anderlich, kann also einmal im Konstruktor gesetzt und danach nie wieder ver andert werden. Wie wir noch sehen werden, hat die Klasse String diese Eigenschaft und ebenso viele andere Klassen der JavaBibliothek.3 Version 2 erm oglicht das sogenannte daisy chaining4 . Damit ist gemeint, dass man mehrere Operationen verketten kann, wie in dem folgenden Beispiel gezeigt wird: String a = "new Bruch(2,3); Bruch b = a.getInverse().getInverse(); //Daisy chaining //Damit ist jetzt b=1/(1/a), also b=a. //Kann z.B. in String sehr sinnvoll eingesetzt werden.
3.2.6
Aliasing
Klebezettel - ein anschauliches Modell Hier eine erste Erl auterung, was bei der Deklaration und Erzeugung genau passiert. Die folgende Erl auterung hat den Vorteil, sehr anschaulich zu sein. Sp ater werden wir noch eine exakte (aber nicht so anschauliche) Erl auterung sehen. Bisher haben wir Variablen als eine Art Beh alter gesehen, der einen Namen einen Typ und nat urlich auch einen Inhalt hat. Diese Veranschaulichung passt f ur primitive Datentypen gut; bei Objekten f uhrt sie aber in die Irre. Wir brauchen statt dessen eine neue Veranschaulichung: Die Deklaration Bruch b; stellen wir uns so vor, dass wir einen Klebezettel erzeugen, der mit dem Namen b beschriftet ist.
Die vorliegende Klasse Bruch hat nur zu Lehrzwecken Methoden beider Versionen. Ein sp atere Ubungsaufgabe wird sein, auch die Klasse Bruch unver anderlich zu machen. 4 daisy chain heit auf deutsch G ansebl umchenkette.
56
Mit der Zuweisung zur Variablen b b = new Bruch(1,3); kleben wir den Klebezettel auf das Objekt. Damit ist das Bruch-Objekt unter dem Namen b ansprechbar. Bruch
1 3
Aliasing Wir haben uns also einen Bruch b erzeugt. Wichtig ist, zu verstehen, was in der Zeile Bruch c = b; passiert. Diese Zeile ist eine direkte Zuweisung. Hier passiert folgendes: Wir erzeugen uns einen neuen Klebezettel mit der Aufschrift c und kleben ihn an unser Bruch-Objekt. Bruch
1 3
Die n achste Zeile ist: b.setZaehler(2); Wir haben nun den Wert unseres Objekts ver andert. Die Situation ist nun: Bruch
2 3
57
den Wert 2/3 zur uckliefern. Das Objekt hat zwei Alias-Namen, unter denen es ansprechbar ist. Davon leitet sich auch der Begri Aliasing ab, der den ganzen Vorgang beschreibt. Will man Aliasing vermeiden, muss man ein zweites Objekt erzeugen und das erste dort hinein kopieren. Gew ohnlich gibt es daf ur einen speziellen Konstruktor, den sogenannten Copy-Konstruktor. Die Zeilen
Bruch b
1 3
Damit wirkt sich eine Anderung von b nicht mehr auf c aus.
3.3
Interne Darstellung
Letztlich enthielt das letzte Kapitel nur eine leicht zu begreifende Anschauung, wie man in Java mit Objekten umgeht. F ur uns ist es aber auch wichtig zu sehen, was in Java wirklich passiert.
3.3.1
Interne Darstellung
Es gibt einen groen Unterschied zwischen Objekten und primitiven Datentypen. Dazu sehen wir uns an, wie Variablen vom Typ Bruch im Vergleich zu Variablen vom Typ int angelegt werden. Eine int-Variable erh alt 4 Bytes an Speicherplatz, in denen der int-Zahlenwert steht. Eine Bruch-Variable erh alt ebenfalls 4 Byte an Speicherplatz. In diesen 4 Byte steht eine Speicheradresse. Das bedeutet folgendes: Der gesamte Speicher des Rechners ist von 0 aufw arts durchnummeriert. Eine Speicheradresse bezeichnet also ein ganz bestimmtes Byte des Speichers. In unserem Fall ist die Speicheradresse der Beginn des Bereichs, an dem das Objekt tats achlich steht. Bei Objekten enthalten die Variablen also keinen Wert, sondern einen Verweis (Referenz, Zeiger, Pointer) auf das Objekt.
58
int-Variable:
Wert: 1234
Adresse: 8235752
Auch hier gibt es wieder spezielle Ausdr ucke: Variablen k onnen entweder ein Wert (value) oder eine Referenz (reference) sein. In Java sind primitive Datentypen Werte; Objekte und Felder sind Referenzen. Da man die Speicheradressen in Java nicht auslesen und nicht mit ihnen rechnen kann, heien sie implizite Referenzen. Explizite Referenzen kennt Java nicht. Eine typische Programmiersprache f ur explizite Referenzen ist C. Referenzen werden auch Zeiger oder englisch Pointer genannt.
5
3.3.2
Das null-Objekt Sehen wir uns den Vorgang der Erzeugung eines Objekts genau an (f ur Felder, die auch Objekte sind, gilt das gleiche). Nach der Deklaration: Bruch t; hat die Variable t den Wert null. null ist ein Java-Schl usselwort f ur den speziellen Wert kein Objekt. null bedeutet englisch ung ultig und wird klar von der Zahl Null unterschieden. Im Deutschen ist das gerade in der gesprochenen Sprache nat urlich viel schwieriger. Hier spricht man
5
59
zur Unterscheidung von der Zahl 0 oft vom Null-Pointer oder spricht null als nall aus. Intern ist null eine Speicheradresse, von der Java wei: Dort kann kein Objekt stehen. In Java gibt es keine M oglichkeit, den genauen Wert von null zu erfahren. Er kann je nach Implementation unterscheidlich sein, ist oft aber tats achlich die Zahl 0. Man kann auch explizit Bruch t = null; schreiben. Wenn man versucht, die Werte eines Objekts null auszulesen, erh alt man eine Fehlermeldung NullPointerException. public static void main(String[] args) { Bruch t = null; t.zaehler = 10; //NullPointerException } //main Interne Vorg ange beim Erzeugen mit new() t = new Bruch(1,3); erzeugt ein neues Objekt im Speicher und weist der Variablen t die Anfangsadresse dieses Objekts zu. In diesem Konstruktor werden anschlieend auch Z ahler und Nenner belegt.
zaehler: 1 nenner: 3
Garbage Collection Durch t = new Bruch(1,3); t = null; sieht die Lage so aus:
60
Das eigentliche Objekt ist vom Programm nicht mehr erreichbar und aus Sicht des Programms Datenm ull. Java startet automatisch in regelm aigen Abst anden ein kleines Programm Garbage Collector, dessen Aufgabe es ist, solchen M ull im Speicher zu l oschen, damit der Speicher wieder f ur andere Zwecke genutzt werden kann. In der Klebezettel-Darstellung w urde t=null bedeuten, dass man den Klebezettel t vom Objekt abreit und anschlieend einen Klebezettel hat, der auf keinem Objekt klebt und ein Objekt, auf dem kein Klebezettel klebt. Letzteres wird vom Garbage-Collector gel oscht.
3.3.3
Wann primitive Datentypen gleich sind, ist einfach zu beantworten. F ur primitive Datentypen (z.B. int) gilt, dass sie genau dann gleich sind, wenn ihre Werte gleich sind. Bei Objekten ist das etwas komplizierter. Nehmen wir die folgende Situation: Bruch b = new Bruch(1,3); Bruch c = new Bruch(b); Das ergibt in der Klebezettel-Darstellung: Bruch
1 3
Bruch b
1 3
b und c sind also Klone voneinander. Jetzt kann man Gleichheit auf unterschiedliche Arten denieren: 1. Zwei Variablen sind gleich, wenn sie Aliase f ur das gleiche Objekt sind. In diesem Fall w aren b und c nicht gleich. 2. Zwei Variablen sind gleich, wenn sie Aliase oder Klone sind. In diesem Fall w aren b und c gleich. Beide F alle k onnen in Java u uft werden: berpr if (b==c) { System.out.println("b und c sind Aliase"); } if (b.equals(c)) { System.out.println("b und c sind Aliase oder Klone"); } Dazu gibt es verschiedene Anmerkungen: Eine typische Java-Anf anger-Falle ist es == zu benutzen, wenn man equals benutzen will. Es ist also wichtig, dass sie sich den Unterschied klarmachen.
3.4. FELDER (ARRAYS) Im zweiten Fall kann man nat urlich auch c.equals(b) benutzen.
61
Wie equals funktioniert, h angt davon ab, wie der Modulentwickler die Methode programmiert hat. Es ist eine Konvention, sie so wie beschrieben zu programmieren. Java hindert einen nicht daran, die Methode anders (oder fehlerhaft) zu implementieren. Lesen sie in Zweifelsfall lieber noch einmal in der API nach.
3.4
Felder (Arrays)
Eine Liste von Variablen gleichen Typs kann in Java in einer Feld-Variablen untergebracht werden. Eine Feldvariable hat neben ihrem Namen und ihrem Typ auch eine bestimmte L ange, die angibt, wie viele Elemente in ihr untergebracht werden k onnen. Jedes Element hat einen Index, unter dem es angesprochen werden kann. Der Index des ersten Elements ist immer 0. Die folgende Abbildung zeigt als Beispiel ein Feld mit dem Namen count und mit 5 Elementen, die Werte zwischen 3 und 8 besitzen. Name: count Typ der Elemente: int L ange: 5 Index: 0 1 2 3 4 Wert: 8 3 3 7 5
3.4.1
Grundfunktionen
Felder sind Klassen und haben viele Eigenschaften, die wir aus dem letzten Kapitel kennen. Allerdings gibt es auch ein paar Besonderheiten, die Felder von anderen Klassen unterscheiden. Erzeugen von Feldern Wie Klassen werden sie zun achst deklariert und dann erzeugt. Der Code sieht ein wenig anders aus als gewohnt, da es keinen echten Konstruktor gibt. int[] count; //Deklarieren count = new int[10]; //Erzeugen und Festlegen der Groesse int[] count = new int[10]; Literale fu r Felder Bei der Deklaration einer Feld-Variablen kann man diese auch direkt vorbelegen. Dazu nutzt man ein Feld-Literal, das wie folgt aussieht: int[] x = {1,2,3,5,8}; Dieses Literal ist allerdings nicht so universell einsetzbar, wie andere Literale. Es ist nicht m oglich, einem bereits existierenden Feld per Literal Werte zuzuweisen. //Beides zusammengefasst
62
x = {1,2,6,3};
Mit einem Trick geht es dann allerdings doch: x = new int[] {1,2,6,3}; Ansprechen von Elementen Elemente werden wie in den folgenden Beispielen angesprochen: count[0] = 7; count[1] = count[0] * 2; count[2]++; count[3] -= 60; Wird ein Feld mit einem fehlerhaften Index angesprochen, bricht das Programm mit einer Fehlermeldung ab. Genauer gesagt wird eine ArrayIndexOutOfBoundsException geworfen, die in einem sp ateren Kapitel behandelt wird. Ein Feld erh alt, anders als in anderen Sprachen, automatisch einen voreingestellten Wert. Man kann also Felder gleich nach der Zuweisung des Speichers auslesen (f uhrt in C oder Pascal zu undenierten Werten). Typ Zahlen (int, oat, ...) char boolean Objekte Voreingestellter Wert 0 ASCII-Zeichen 0 false null
//-> 0
Beim Zugri auf Feldelemente kann man auch eine Integer-Variable als Index benutzen. Das ist der groe Vorteil von Feldern. int[] a = new int[2]; int i = 1; a[i] = 3; Besonders praktisch ist das in Kombination mit Z ahlschleifen. Dazu brauchen wir jedoch noch eine weitere Eigenschaft von Feldern: Es ist erlaubt, die L ange eines Feldes mit length auszulesen.6 Es sind immer die Elemente 0 bis length-1 vorhanden. int[] f = new int[30]; System.out.println(f.length); //-> 30; Elemente 0..29
Damit k onnen wir ein Feld mit 100 Elementen erzeugen, deren Inhalt der Index zum Quadrat ist:
6 Ohne Klammern. Es handelt sich nicht um eine Methode, sondern um das Auslesen einer internen Variablen.
3.4. FELDER (ARRAYS) int[] q = new int[100]; for (int i=0; i<q.length; i++) { q[i] = i*i; }
63
3.4.2
Felder kopieren
Es gibt einige M oglichkeiten, ein Feld in ein anderes Feld zu kopieren. Hier erst einmal eine kurze Ubersicht. Dabei soll jeweils ein Feld von src nach dst kopiert werden. Kopieren in einer for-Schleife. System.arraycopy(src, src_pos, dst, dst_pos, length); dst = Arrays.copyOf(src, src.length); dst = Arrays.copyOfRange(src, 0, src.length); dst = src.clone(); Am einfachsten zu merken ist sicherlich der clone-Befehl: x = new int[]{1,2,3}; int[] y = x.clone(); Am exibelsten ist der Befehl System.arraycopy: System.arraycopy(src, src_pos, dst, dst_pos, length); Die Parameter heien: Parameter Bedeutung src Ursprung (source). src_pos Index des ersten zu kopierendes Elements in src. dst Ziel (destination). dst_pos Dorthin (ab diesen Index) soll das Feld kopiert werden. length Anzahl der zu kopierenden Elemente. int[] b = new int[a.length]; System.arraycopy(a, 0, b, 0, a.length);
3.4.3
Sehr h aug muss ein Feld elementweise durchlaufen werden, z.B. um es auf dem Bildschirm auszugeben. Die Standard-Schleife hierzu ist (am Beispiel des Integer-Feldes dat): for (int i=0; i<dat.length; i++) { System.out.println(dat[i]); }
64
Alternativ dazu kann man auch die sogenannte Aufz ahlungs- oder foreachSchleife verwenden. Der entsprechende Code ist: for (int z: dat) { System.out.println(z); } Gesprochen: F ur jedes z im Feld dat. Die Variable z nimmt also der Reihe nach alle Werte an, die im Feld dat stehen. Aus dem Satzteil f ur jedes leitet sich auch der Name foreach-Schleife ab. Java benutzt allerdings statt foreach das normale Schl usselwort for.
3.4.4
Ab Java 6 kann man den Inhalt eines Feldes einfach ausgeben mit System.out.println(Arrays.toString(feld)); Bei mehrdimensionalen Feldern (siehe n achstes Kapitel) verwendet man statt dessen: System.out.println(Arrays.deepToString(feld));
3.4.5
Mehrdimensionale Felder
Felder k onnen auch mehrdimensional sein. Im folgenden Beispiel wird ein zweidimensionales Feld mit 3 Zeilen und 5 Spalten erzeugt. double[][] feld = new double[3][5]; feld[0][0] = 5; //Element in der linken oberen Ecke feld[2][4] = 2; //Element in der rechten unteren Ecke Solche Felder werden gew ohnlich in 2 geschachtelten for-Schleifen beschrieben und ausgelesen. Nehmen wir als Feld int[][] diff = new int[10][10]; Die Anzahl der Zeilen erfragt man mit int z = diff.length; Die L ange einer Zeile (im Beispiel der Zeile 0) erfragt man mit int s = diff[0].length; Im folgenden Beispiel wird ein Feld erzeugt, in dessen Elementen jeweils die Dierenz zwischen Zeilen- und Spaltennummer steht: int[][] diff = new int[10][10]; for (int i=0; i<diff.length; i++) { for (int j=0; j<diff[0].length; j++) diff[i][j] = i-j; } }
3.5. STRINGS
65
An der Art, wie die L ange einer Zeile bestimmt wird, kann man erkennen: Das Feld muss nicht rechteckig sein. So etwas wird man allerdings nur selten nden. Hier ein kleines Beispiel dazu: int[][] dreieck = new int[10][]; //letzter Index wird frei gehalten for (int i=0; i<dreieck.length; i++) { dreieck[i] = new int[i+1]; //Erzeugen der Zeile Nr. i } Die L ange in Richtung des 1. Index erfragt man mit dreieck.length. Die L ange in Richtung des 2. Index erfragt man mit dreieck[i].length. Die L ange kann je nach Position i variieren. Man kann auch ein rechteckiges Feld nachtr aglich zu einem nicht rechteckigen machen: feld[2] = new double[10]; Diese Besonderheiten teilt Java mit C# und Python (Lists). In C und Pascal sind mehrdimensionale Felder immer rechteckig. C# hat den Vorteil, dass es beide Varianten gibt. Dort wird unterschieden zwischen jagged arrays (gezackte Felder) f ur Felder wie in Java und rectangular arrays (rechteckige Felder) f ur Felder wie in C.
3.5
Strings
Neben Feldern sind auch Strings Objekte mit einigen Besonderheiten. Die beiden wichtigsten Besonderheit behandeln wir gleich vorweg:
3.5.1
Besonderheiten
Literale Es gibt String-Literale. Das hat auch zur Folge, dass wir ein String-Objekt nicht mit new und einem Konstruktor erstellen m ussen, sondern einfach das entsprechende Literal verwenden k onnen. String s = "Hallo"; Die einzigen anderen Objekte mit Literalen sind Felder. Unver anderlichkeit Nachdem ein String-Objekt einmal erstellt wurde, ist es unver anderlich. Alle Methoden zur String-Manipulation erzeugen einen neuen, ver anderten String.
3.5.2
Strings haben zahlreiche vorgegebene Methoden, von denen hier nur die wichtigsten aufgez ahlt sind:
66
KAPITEL 3. DAS OBJEKTORIENTIERTE KONZEPT VON JAVA Zeichen an Position index Sind 2 Strings gleich? Position des ersten Vorkommens von str Stringl ange Teilt String in Teilstrings auf. regex ist das Trennzeichen, an dem aufgetrennt wird.7 Gibt Teilstring zwischen beginIndex (einschlielich) und endIndex (ausschlielich) zur uck.
char charAt(int index) boolean equals(Object anObject) int indexOf(String str) int length() String[] split(String regex)
String
Den Gebrauch der Methoden erkennt man am besten am nachfolgenden kleinen Beispiel: String a = "Hallo"; String b = "tschuess"; int l = b.length(); char c = a.charAt(0); int pos = a.indexOf("ll"); String[] x = b.split("u"); //ergibt //ergibt //ergibt //ergibt 8 H 2 ["tsch","ess"]
Es gibt u oglichkeit, einzelne Zeichen in String direkt zu ver andern. brigens keine M Man muss hier den Umweg u ber die Klasse StringBuilder nehmen, die in ei nem sp ateren Kapitel erl autert wird.
3.5.3
Escape-Sequenzen
In Strings nimmt das Zeichen \ (Backslash) eine Sonderstellung ein. Es ist ein sogenanntes Metazeichen und wird speziell interpretiert. Zum Beispiel ist \n ein Zeilenvorschub. Die Zeile System.out.println("abc\ndef"); erzeugt die Ausgabe abc def \n heisst dabei Escape-Sequenz. Eine Auistung der wichtigsten Escape-Sequenzen ndet sich im Anhang. Will man den Backslash selbst ausgeben, muss man einen doppelten Backslash \\ angeben.
3.5.4
3.5. STRINGS
67
verlangt als Ubergabeparameter einen sogenannten regul aren Ausdruck. Diese Ausdr ucke sind Strings, in denen es weitere Metazeichen gibt, die speziell interpretiert werden: [ ] ( ) { } | ? + - * ^ $ \ . Damit ein Metazeichen nicht interpretiert wird, muss man einen Backslash davor setzen. Da ein einfacher Backslash f ur eine Escape-Sequenz gehalten werden w urde, muss das in Java ein doppelter Backslash sein. Will man beispielsweise einen String nach dem Punkt-Zeichen trennen, muss man String[] x = s.split("\\."); schreiben. Die Metazeichen selbst werden in dieser Vorlesung nicht behandelt. Nur ein Hinweis dazu. Das Metazeichen + bedeutet: ein oder mehrere Exemplare des vorangegangenen Zeichens. Beispiel: String s = "Unterschiedlicher Abstand zwischen String[] x1 = s.split(" "); String[] x2 = s.split(" +"); System.out.println(Arrays.toString(x1)); System.out.println(Arrays.toString(x1)); Worten .";
//ergibt //[Unterschiedlicher, , , , , Abstand, zwischen, , Worten, .] //[Unterschiedlicher, Abstand, zwischen, Worten, .] In der ersten Version wird an einem Leerzeichen getrennt. Folgen mehrere Leerzeichen aufeinander, entstehen im Array leere Eintr age. In der zweiten Version wird an ein oder mehreren Leerzeichen getrennt. Hier gibt es keine leeren FeldEintr age mehr. Vergleich von Strings Wie f ur andere Objekte gilt auch hier: Der Vergleich mit == u uft, ob es berpr sich um Aliase handelt. Der Vergleich mit equals u uft, ob der Inhalt der berpr Strings gleich ist. Dies ist der Fall, wenn es sich um Klone oder Aliase handelt. Da man gew ohnlich wissen will, ob die Inhalte zweier Strings gleich sind, benutzt man normalerweise equals. if (s1.equals(s2)) { System.out.println("s1 und s2 haben den gleichen Inhalt"); } In der Sun-Implementation liefern aber erstaunlicherweise die folgenden Zeilen den Wert true: String s1 = "Hallo"; String s2 = "Hallo"; boolean g = (s1==s2);
68
s1 und s2 sind also Aliase. Der Grund ist, dass beim Anlegen von s2 Java erkennt, dass es den String "Hallo" schon gibt. Daraufhin legt Java kein neues String-Objekt an, sondern erzeugt einfach einen neuen Verweis auf das bestehende Objekt. Da die Objekte unver anderlich sind, ist das kein Problem (wenn sie ver anderlich w aren, w are es sehr wohl eines, denn dann w urde man, wenn man s1 ver andert, gleichzeitig auch s2 ver andern).
69
3.6
3.6.1
In diesem Kapitel wollen wir die Klasse Bruch, die wir im letzten Kapitel benutzt haben, selbst nachprogrammieren. Wir wollen darauf achten, dass die Eigenschaften, die wir im letzten Kapitel aus Anwendersicht kennengelernt haben, auch in der selbst programmierten Klasse erhalten bleiben. Wir werden hier nicht die komplette API implementieren, aber genug davon, dass alle wichtigen Prinzipien klar sind.
3.6.2
Zusammengesetzte Datentypen
Klassen in ihrer einfachsten Form sind zusammengesetzte Datentypen. Sie sind in etwa vergleichbar mit einem RECORD in Pascal oder einem struct in C. Eine ganz einfache Bruch-Klasse in Java ist ein Datentyp, der sich aus zwei IntegerVariablen, je einer f ur Z ahler und Nenner, zusammensetzt. Das Aussehen ist wie folgt:
Das public-Schl usselwort in der ersten Zeile hat nur mit externen Paketen Bedeutung.8 Vor den beiden Variablen-Deklaratinen heit public, dass die Methode f ur den Anwender benutzbar ist und in der API auftaucht. Wenn an dieser Stelle private stehen w urde, k onnte man sie nur in der Klasse selbst verwenden (d.h. nur der Modulentwickler). Die Daten, die von Klassen zusammengefasst werden, heien die Attribute oder die Eigenschaften der Klasse. Im Beispiel sind die Attribute der Klasse Bruch vom Typ int und haben die Namen zaehler und nenner.
UML-Klassendiagramme Eine Klasse wird h aug mit Hilfe eines UML-Diagramms dargestellt. Das UMLKlassendiagramm f ur die Klasse Bruch sieht folgendermaen aus:
8 Wir werden sp ater sehen, dass die Klasse nur dann von externen Paketen aus benutzt werden kann, wenn hier das Schl usselwort public angegeben wurde.
70
Klassenname
Attribute Methoden
Das + steht f ur public. Ein steht f ur private. Da es keine Methoden gibt, ist das untere K astchen noch leer.
3.6.3
Um eine Klasse aus Anwendersicht testen zu k onnen, braucht man eine Testklasse. Diese k onnte etwa so aussehen: public class BruchTest { public static void main(String[] args) { //Mein Testcode } } Die Testklasse BruchTest selbst benutzt keine speziellen objektorientierten Eigenschaften. Ihr Aufbau ist so, wie wir es aus den letzten Kapiteln kennen. Formal ist sie aber dennoch eine Klasse. Wird sie von Java aus gestartet, so wird wie gewohnt die main-Funktion der Klasse aufgerufen. Man k onnte die main-Funktion auch in die Klasse Bruch selbst schreiben und diese dann beim Programmstart aufrufen. Das w urde prinzipiell funktionieren, aber man hat dann nicht exakt die Anwendersicht, sondern darf noch zus atzlich einige Dinge, die nur aus Entwicklersicht erlaubt sind. Wir werden das sp ater noch genauer sehen. Einstweilen halten wir uns einfach an die Regel, dass die main-Funktion f ur den Test auerhalb der zu testenden Klasse stehen sollte. Klassen und Dateien Das heit in der Regel auch, dass man zum Test einer Klasse eine eigene Datei mit dem Test-Code schreiben muss. Die Regel in Java ist n amlich, dass jede Klasse in eine eigene Datei geschrieben wird. Der Dateiname setzt sich aus dem Klassennamen und der Endung .java zusammen. Gr oere Programme sind so gut wie immer auf mehrere Klassen und damit auch auf mehrere Dateien verteilt. Diese Dateien sollten alle im selben Verzeichnis liegen.
71
Sehen wir uns dies an einem Beispiel an. Die Klasse Bruch sieht folgendermaen aus: public class Bruch { //Attribute public int zaehler; public int nenner; }//public class Bruch In der Testklasse benutzten wir das Objekt aus Anwendersicht: public class BruchTest { public static void main(String args[]) { Bruch r1 = new Bruch(); //Deklaration und Konstruktorausruf r1.zaehler = 4; r1.nenner = 5; //Zaehler setzen //Nenner setzen
//Zaehler und Nenner auslesen System.out.println("Zaehler = "+r1.zaehler); System.out.println("Nenner = "+r1.nenner); } } Wird die Klasse BruchTest gestartet, st ot Java in der ersten Zeile auf das Objekt der Klasse Bruch. Nun sucht Java nach der entsprechenden Datei. Dabei werden die Java-Systemordner und das aktuelle Verzeichnis durchsucht (wie man das andert, folgt in einem sp ateren Kapitel). Wird die Datei gefunden, wird sie nachgeladen. Wir k onnen bereits ein Objekt der Klasse Bruch erzeugen. Dazu benutzen wir einen Konstruktor, dem keine Parameter u bergeben werden. Anschlieend k onnen wir die Bestandteile des zusammengesetzten Datentyps wie dargestellt ver andern oder auslesen. Vorbesetzt werden die Attribute wie bei Feldern mit 0. Damit haben wir schon unser erstes kleines objektorientiertes Programm geschrieben.
3.6.4
toString Um unsere Klasse besser testen zu k onnen, wollen wir als erstes die M oglichkeiten zur Darstellung eines Objekts untersuchen. Wir andern dazu unsere Testfunktion ab:
72
public static void main(String args[]) { Bruch r1 = new Bruch(); //Deklaration und Konstruktorausruf r1.zaehler = 4; r1.nenner = 5; System.out.println(r1); } Eigentlich w unschen wir uns die Ausgabe 4/5, wie in unserer Vorlage. Leider ist die tats achliche Ausgabe aber in der Art, wie Bruch@10b62c9 Daran erkennt man den Namen der Klasse und den Speicherplatz der Objekts. Im letzten Kapitel haben wir gelernt, dass genau der String ausgegeben wird, der von der Methode toString() zur uckgegeben wird. Bisher haben wir nur eine Art Default-Implementierung.9 Um die Ausgabe zu verbessern, m ussen wir eine neue Methode toString schreiben. Diese hat das folgende Aussehen: public String toString() { String r = this.zaehler + "/" + this.nenner; return r; } Die erste Zeile ist die Kopfzeile oder Signatur. Sie besteht aus 4 Teilen: 1. public heit, dass die Methode f ur den Anwender benutzbar ist und in der API auftaucht. Wenn an dieser Stelle private stehen w urde, k onnte man sie nur in der Klasse selbst verwenden (d.h. nur der Modulentwickler). 2. String ist der R uckgabeparameter. Wenn diese Methode aufgerufen wird, gibt sie als Ergebnis einen String zur uck. 3. toString ist der Name der Methode. 4. (). Hier stehen die Ubergabeparameter. Da es keine Ubergabeparameter gibt, steht hier einfach eine leere Klammer. In der zweiten Zeile wird der String aus Z ahler und Nenner zusammengebaut. Wichtig ist dabei das this-Schl usselwort, dem ein eigener kleiner Abschnitt gewidmet sein soll:
9 Die Default-Implementierung steht in der Klasse Object, von der Bruch automatisch abgeleitet ist. Dies werden wir noch im Kapitel u uhrlich betrachten. ber Vererbung ausf
73
Vergegenw artigen wir uns noch einmal unsere augenblickliche Rolle als Entwickler der Klasse. Wir entwickeln eine Art Schablone, nach der sich der Modulanwender sp ater auf folgende Weise Objekte herstellt und benutzt: Bruch x1 = new Bruch(); Bruch x2 = new Bruch(); String s1 = x1.toString(); String s2 = x2.toString(); Das heit, der Anwender ruft die Methode toString eines bestimmten Objekts auf und will nat urlich die Werte dieses Objekts zur uckgegeben bekommen. Doch woher wissen wir als Entwickler der Klasse, von welchem Objekt der Anwender toString aufgerufen hat? Die Antwort darauf ist das Schl usselwort this. Dieses Schl usselwort verweist immer auf das Objekt, von dem der Benutzer eine bestimmte Methode aufgerufen hat. Dieses Objekt nennt man aus Entwicklersicht das this-Objekt. this.zaehler und this.nenner greifen damit auf die Attribute genau des Objekts zur uck, das der Benutzer angesprochen hat. In diesem einfachen Fall kann man die beiden this-Schl usselworte sogar weglassen. Sie werden dann von Java automatisch hinzugef ugt. String r = zaehler + "/" + nenner; Es ist aber guter Stil, sie explizit hinzuschreiben. Fortsetzung der toString-Methode Die letzte Zeile return r; beendet die Methode und sorgt daf ur, dass der String r als Ergebnis zur uckgegeben wird. Wenn wir den Testcode von oben noch einmal laufen lassen, erhalten wir jetzt das gew unschte Ergebnis 4/5.
3.6.5
Datenkapselung
Die objektorientierte Programmierung verf ahrt nach den drei Basisprinzipien Kapselung, Vererbung und Polymorphie (Vielgestaltigkeit). Ein Beispiel f ur Polymorphie haben wir schon kennengelernt. In der Klasse Bruch gibt es zwei Versionen der Methode mult, jeweils mit unterschiedlichen Ubergabeparametern. Die Methode mult ist somit polymorph (vielgestaltig). Nun begegnet uns das n achste Grundprinzip, n amlich die Kapselung. Beginnen wir mit der bereits bekannten Klasse f ur Br uche:
74
public class Bruch { //Attribute public int zaehler; public int nenner; } //public class Bruch
Der Nenner eines Bruchs muss, wie schon erw ahnt, ungleich 0 sein. Im Programm ist es im Moment noch kein Problem ein ung ultigen Bruch zu erzeu gen:
Eines der Ziele der Objektorientierung ist, dass solche Operationen verhindert werden sollen. Anders ausgedr uckt: Es soll erreicht werden, dass die Attribute eines Objekts keine unsinnigen Werte mehr annehmen k onnen. Dazu muss als erstes verhindert werden, dass die Attribute frei gesetzt werden d urfen. Dazu wird das Schl usselwort public durch private ersetzt:
public class Bruch { //Attribute private int zaehler; private int nenner; } //public class Bruch
Dieses Prinzip heit Datenkapselung und ist eines der erw ahnten Grundprinzipien der Objektorientierung. Leider sind die Attribute jetzt etwas u bertrieben gekapselt, denn der Anwender kann sie u andern und auch berhaupt nicht mehr nicht auslesen. Wir m ussen also noch eine M oglichkeit implementieren, Daten kontrolliert ver andern zu k onnen. Das ist eine Hauptaufgabe der Methoden. Methoden sind dabei so etwas, wie die Schnittstelle zu den Attributen. Ein Zugri auf die Attribute eines Objekts darf nur u ber seine Methoden erfolgen. Das UML-Diagramm unserer Vorlage sieht (verk urzt) folgendermaen aus:
3.6. ENTWICKLUNG EINES BEISPIEL-OBJEKTS Bruch zaehler: int nenner: int + + + + + + getZaehler(): int getNenner(): int setZaehler(zaehler: int): void setNenner(nenner: int): void getDoubleWert(): double mult(faktor: int): void Klassenname
75
Attribute
Methoden
3.6.6
Die Getter- und Setter-Methoden dienen dazu, Attribute zu andern (Setter) oder auszulesen (Getter). F ugen wir zun achst die beiden Methoden f ur den Z ahler hinzu: public class Bruch { //Attribute private int zaehler; private int nenner; public void setZaehler(int zaehler) { this.zaehler = zaehler; } public int getZaehler() { return this.zaehler; } }//public class Bruch Auch hier gibt es einige interessante Punkte: In der Deklaration public void setZaehler(int zaehler) { taucht ein Ubergabeparameter auf. Er wird in der Form Datentyp Variablenname
76
KAPITEL 3. DAS OBJEKTORIENTIERTE KONZEPT VON JAVA angegeben. Das bedeutet, wenn wir die Methode aufrufen, z.B. mit Bruch b = new Bruch(); b.setZaehler(2); dass dann innerhalb der Methode setZaehler eine Variable zaehler verf ugbar ist, die den Wert 2 hat (aber ganz normal ge andert werden kann). Die n achste Zeile this.zaehler = zaehler; ist ebenfalls interessant. Die Variable zaehler enth alt den Ubergabeparameter, den wir gerade besprochen haben. Mit this.zaehler wird das Attribut zaehler der Klasse Bruch angesprochen. Es wird also das Attri but auf den Wert der Ubergabevariablen gesetzt. Man muss Ubergabeparameter und Attribut nicht gleich benennen. Es ist aber in Java bei Setter-Methoden u blich. In der Getter-Methode gibt es keine lokale Variable gleichen Namens. Daher k onnte man dort statt this.zaehler auch einfach zaehler schreiben. Methoden einer Klasse k onnen auf Attribute zugreifen, auch wenn sie private sind. Das heit, innerhalb der Klasse Bruch ist ein Zugri auf das Attribut zaehler m oglich, von anderen Klassen jedoch nicht. Der Name setZaehler ist nicht zwingend vorgeschrieben, folgt aber einer Java-Konvention. G angigerweise w ahlt man den Namen einer Getter- bzw. Setter-Methode wie folgt: Setter-Methode Getter-Methode Getter-Methode (boolean-Variable) set+Variablenname get+Variablenname is+Variablenname
Beispiel: Die Methode zum Setzen der Variable zaehler w urde man setZaehler(..) nennen. Der erste Buchstabe des Variablennamens wird dabei gro geschrieben. Hinweis: Die Verwendung deutscher Variablennamen f uhrt zu englischdeutschen Wortgemischen in den Getter- und Setter-Methoden. Dies ist ein starkes Argument f ur englische Variablennamen. Hier wird darauf verzichtet, da setZaehler und setNenner verst andlicher sind, als die englischen Entsprechungen setNumerator und setDenominator.
3.6. ENTWICKLUNG EINES BEISPIEL-OBJEKTS Einschr ankung des Zugris auf den Nenner
77
F ur den Nenner sieht die Getter-Methode ahnlich aus wie beim Z ahler. In der Setter-Methode erh alt in unserer Vorlage der Anwender jedoch eine Exception, falls er versucht, den Nenner auf 0 zu setzen. Dieses Verhalten m ussen wir als Entwickler jetzt hinzuf ugen. Man sagt dazu, wir m ussen eine Exception ausl osen oder (etwas umgangssprachlicher) eine Exception werfen. Das geht folgendermaen: public void setNenner(int nenner) { if (nenner==0) { //Exception ausloesen ArithmeticException e = new ArithmeticException ("Nenner darf nicht 0 werden."); throw e; } else { this.nenner = nenner; } } Zum Ausl osen einer Exception erzeugt man sich ein Objekt einer ExceptionKlasse (hier: ArithmeticException). Dem Konstruktor kann man einen String mit einer n aheren Erkl arung als Parameter u bergeben, der dann in der Fehlermeldung erscheint. Dieses Exception-Objekt wird in der n achste Zeile durch das Schl usselwort throw ausgel ost. Diese beiden Zeilen kann man auch in eine zusammenfassen: throw new ArithmeticException("Nenner darf nicht 0 werden."); Wir testen das Programm wieder aus Anwendersicht: public class BruchTest { public static void main(String[] args) { Bruch r = new Bruch(); r.setNenner(0); } } ergibt die gew unschte Fehlermeldung (die nat urlich vom Anwender auch mit try-catch gefangen werden kann): Exception in thread "main" java.lang.ArithmeticException: Nenner darf nicht 0 werden. at Bruch.setNenner(Bruch.java:21) at BruchTest.main(BruchTest.java:4)
78
3.6.7
Konstruktoren
Obwohl wir in unserer Bruch-Klasse bisher keinerlei Konstruktoren implementiert haben, k onnen wir uns bereits Bruch-Objekte erzeugen. Java stellt uns daf ur automatisch einen Konstruktor zur Verf ugung, n amlich den Default- oder parameterlosen Konstruktor. Parameterlos heit, dass wir beim new-Aufruf Bruch b = new Bruch(); keine Parameter u bergeben haben. Die 3 Konstruktoren mit Parametern aus unserer Vorlage gibt es aber noch nicht. Das wollen wir jetzt andern. Generell dienen Konstruktoren dazu, die Attribute gleich beim Erzeugen des Objekts setzen zu k onnen. Fangen wir mit dem Konstruktor an, dem Z ahler und Nenner als Integer-Wert u bergeben werden: public Bruch(int zaehler, int nenner) { setZaehler(zaehler); setNenner(nenner); } Ein Konstruktor sieht ahnlich aus wie eine Methode, unterscheidet sich aber in folgenden Punkten: Der Methodenname ist gleich dem Namen der Klasse. In unserem Bei spiel ist er also Bruch. Es gibt keinen R uckgabewert. Es wird noch nicht einmal void als R uckgabewert angegeben. Konstruktoren k onnen grunds atzlich keinen R uckgabewert haben, weswegen die Angabe void einfach eingespart wird. Machen wir uns noch einmal an einem Beispiel klar, dass es unm oglich ist, dass ein Konstruktor einen R uckgabewert zur uckgibt. Ein Konstruktor wird ausschlielich dann aufgerufen, wenn ein neues Objekt mit new erzeugt wird, also z.B. Bruch r = new Bruch(5, 3); Ein Konstruktor wie public int Bruch(int zaehler, int nenner) { setZaehler(zaehler); setNenner(nenner); return 1; } h atte keine M oglichkeit, die zur uckgegebene 1 an eine Variable zu u bergeben. r wird ja schon mit dem neu erzeugten Objekt belegt.
79
In unserem Konstruktor haben wir die Methoden setZaehler und setNenner aufgerufen. Methodenausrufe sind Konstruktoren erlaubt. Es ist hier auch sinnvoll, weil nat urlich auch im Konstruktor darauf geachtet werden muss, dass der Anwender nicht mit z.B. Bruch b = new Bruch(1,0); einen Bruch mit dem Nenner 0 erzeugt. Um die Fehlerkontrolle nicht zweimal programmieren zu m ussen, ruft man einfach die Methode setNenner auf. Der obige Aufruf l ost dann eine Exception aus. Es ist allerdings nicht erlaubt, aus einer Methode heraus einen Konstruktor direkt (also ohne new) aufzurufen. Ein weiterer Eekt ist, dass das Objekt nicht erzeugt wird, wenn im Konstruktor eine Exception auftritt. Die Zeilen: Bruch b; try { b = new Bruch(1,0); } catch (ArithmeticException e) { //Fehler ignorieren } System.out.println(b); w urden als Ergebnis null ergeben, denn das zu b geh orige Objekt wurde nicht erzeugt. Der Default-Konstruktor Wenn wir unseren neuen Konstruktor geschrieben haben, dann funktioniert der Aufruf Bruch r = new Bruch(); pl otzlich nicht mehr. Was passiert hier? Der Default-Konstruktor public Bruch() { } wird von Java immer genau dann hinzugef ugt, wenn der Entwickler selbst keinen Konstruktor angegeben hat. Ist ein Konstruktor angegeben, f allt der DefaultKonstruktor automatisch weg. Ist er dennoch gew unscht, muss er wieder hinzugef ugt werden. Aussehen k onnte das z.B. so: //Default-Konstruktor public Bruch() { this.zaehler = 1; this.nenner = 1; } Der Vorteil ist, dass der Default-Konstruktor jetzt einen Bruch 1 1 anstelle des unsinnigen Bruchs 0 erzeugt. Nat u rlich ist es in diesem Beispiel noch besser, 0 den Default-Konstruktor ganz wegzulassen.
80
Der Copy-Konstruktor Dieser Konstruktor-Typ ist f ur Java ein Konstruktor wie jeder andere. Er hat einen speziellen Namen, weil er in der Praxis sehr h aug auftaucht. Er erzeugt ein neues Objekt, dessen Attribute aus einem zweiten, u bergebenen Objekt kopiert werden. Ein Copy-Konstruktor zur Klasse Bruch sieht folgendermaen aus: public Bruch(Bruch r) { this.zaehler = r.zaehler; this.nenner = r.nenner; } Auch hier gibt es einige interessante Bemerkungen: Obwohl Z ahler und Nenner private sind, konnten wir direkt auf r.zaehler und r.nenner zugreifen. Die Regel dazu ist folgende: Aus der Klasse Bruch heraus kann auf die privaten Attribute aller BruchObjekte (nicht nur des this-Objekts) zugegrien werden. Man kann das so verstehen: Dem Entwickler der Klasse Bruch wird die M oglichkeit einger aumt, auf die privaten Attribute aller Bruch-Objekte zugreifen zu k onnen. Der Anwender der Klasse darf direkt auf kein privates Attribut zugreifen. Wir haben hier die Attribute direkt u ber eine Zuweisung gesetzt und nicht u ur ist, dass ber den Aufruf der Setter-Methoden. Der Grund daf hier schon garantiert ist, dass der Nenner von r ungleich 0 ist, und wir das nicht noch einmal u ufen m ussen. berpr Aufruf von Konstruktoren aus anderen Konstruktoren Konstruktoren d urfen aus anderen Konstruktoren heraus aufgerufen werden. Dazu gibt es den this-Befehl. Zum Beispiel: this(10, 10); Da dies aber relativ selten vorkommt, werden wir nicht weiter darauf eingehen. Wichtig ist dabei, dass der this-Befehl der erste Befehl innerhalb des Konstruktors sein muss. Sichtbarkeit von Konstruktoren Es ist m oglich, Konstruktoren private zu deklarieren. Dann kann ein Konstruktor nicht mehr auerhalb der eigenen Klasse aufgerufen werden. In speziellen F allen ist das auch sinnvoll, aber hier sind wir bereits in einem fortgeschrittenen Gebiet. In den allermeisten F allen werden Konstruktoren als public deklariert.
81
3.6.8
Invarianten
Bisher wurden in unserer Bruch-Klasse die Br uche nicht automatisch gek urzt. Auerdem kann man den Nenner problemlos auf eine negative Zahl setzen. Die beiden Bedingungen Der Bruch ist gek urzt. Der Nenner des Bruchs ist gr oer als 0. nennt man die Invarianten unserer Vorlage. Invarianten sind Bedingungen, die jeder Methode, bevor sie sich beenden darf, erf ullen muss. Aus Anwendersicht sind die Invarianten also immer erf ullt. Aus Entwicklersicht ist das nicht ganz der Fall, da die Invarianten mitten in einer Methode verletzt werden d urfen, solange sie nur vor dem Methodenende wieder hergestellt werden. Nehmen wir als Beispiel eine Methode setZaehler, die die Invariante Bruch ist gek urzt einh alt: Ein Bruch b habe den Wert 4/9. Der Anwender ruft die b.setZaehler(3) auf. Innerhalb der Methode geschieht folgendes: Der Z ahler wird auf 3 gesetzt. Damit hat der Bruch den Wert 3/9. Die Invariante Bruch ist gek urzt ist verletzt. Der Anwender kann dies aber nicht feststellen. Der Bruch wird gek urzt. Er hat jetzt den Wert 1/3. Nach dem Ende der Methode sieht der Anwender das Ergebnis: 1/3. Die Invariante ist aus Anwendersicht eingehalten. Wir programmieren im Folgenden eine Methode, die die Invarianten wieder herstellt, und die von allen Methoden, die sie verletzen k onnten, am Ende aufgerufen wird. Diese Methode soll private sein, denn sie dient nur internen Zwecken und soll nicht in der API auftauchen. private void normalisiere() { if (nenner < 0) { nenner = nenner * -1; zaehler = zaehler * -1; } // Bruch kuerzen int a = zaehler; if (a < 0) { a = -a; } int b = nenner; // Suchen des GGT // Euklidischer Algorithmus while (b != 0) {
82
KAPITEL 3. DAS OBJEKTORIENTIERTE KONZEPT VON JAVA int h = a % b; a = b; b = h; } // a ist der GGT zaehler = zaehler / a; nenner = nenner / a;
} Zum K urzen verwenden wir den sogenannten Euklidischen Algorithmus, der hier nicht weiter erkl art sein soll. Nun m ussen wir noch alle Methoden korrigieren, die die Invarianten eventuell verletzen k onnten. Als Beispiel dazu diene setZaehler: public void setZaehler(int zaehler) { this.zaehler = zaehler; normalisiere(); } Damit der Konstruktor nicht zweimal normalisiert, m ussen wir ihn noch anpassen: public Bruch(int zaehler, int nenner) { this.zaehler = zaehler; //normalisiert nicht setNenner(nenner); //normalisiert }
3.6.9
Andere Methoden
Die restlichen Methoden der Klasse Bruch sind jetzt vergleichsweise einfach. Zum Beispiel sehen getDoubleWert und multipliziere(int faktor) folgendermaen aus: public double getDoubleWert() { //Casten in double fuer Fliesskomma-Division return this.zaehler / (double) this.nenner; } public void multipliziere(int faktor) { this.zaehler = this.zaehler * faktor; }
3.7
3.7.1
Weitere Details
Objekte als Attribute
3.7. WEITERE DETAILS public class ZweiBrueche { public Bruch c1; public Bruch c2; } Die Verwendung geht wie folgt: ZweiBrueche z = new ZweiBrueche(); z.c1 = new Bruch(3,5); z.c1.mult(2);
83
Das Attribut eines Objekts kann auch die gleiche Klasse haben wie das Objekt selbst. Das ist kein Problem, wie folgendes Code-Beispiel zeigt: public class Kette { Kette k; int wert;
public static void main(String[] args) { Kette k1 = new Kette(); k1.wert = 1; Kette k2 = new Kette(); k2.wert = 2; k1.k = k2; System.out.println(k1.wert); System.out.println(k1.k.wert); System.out.println(k1.k.k); } //main } //class Intern sieht das so aus:
Kette k1:
84
Diese Konstruktion heit einfach verkettete Liste und wird in der Vorlesung Algorithmen und Datenstrukturen im n achsten Semester ausf uhrlicher behandelt. Auch Referenzen auf sich selbst sind kein Problem: k1.k = k1; Dies sieht intern so aus:10
Kette k1:
3.7.2
Nehmen wir an, wir wollen einen mathematischen L osungsalgorithmus programmieren, der an mehreren Stellen die Fakult at einer Zahl berechnet: n! = 1 2 . . . n Nach unserem bisherigen Wissenstand w urden wir das etwa so anfangen: public class Fakultaet { public int fakultaet(int n) { int faku=1; for (int i=2; i<=n; i++) { faku = faku * i; } return faku; } public static void main(String[] args) { Fakultaet f = new Fakultaet(); System.out.println(f.fakultaet(8)); }
10
85
Es f allt auf, dass die Methode fakultaet auf keinerlei Attribute der Klasse zur uckgreift. Man k onnte die Methode in jede beliebige andere Klasse kopieren und sie w urde immer noch funktionieren. Man kann sie mit einer abgeschlossenen Box vergleichen, die aus Eingangsparametern einen Ausgabewert erzeugt. Grasch kann man das als Box mit interner Funktionsweise (White-Box) und ohne interne Funktionsweise (Black-Box) darstellen.
faku n!
Solche Programmteile heien Funktionen. In Java nennt man sie auch statische Methoden, aus Gr unden, die wir sp ater noch sehen werden. Da solche Funktionen auch ganz ohne zugeh orige Objekte funktionieren, gibt es eine M oglichkeit, sich die Erzeugung des Objekts zu sparen. Dazu dient das Schl usselwort static im Funktionskopf: public class Fakultaet { public static int fakultaet(int n) { int faku=1; for (int i=2; i<=n; i++) { faku = faku * i; } return faku; } public static void main(String[] args) { System.out.println(Fakultaet.fakultaet(8)); } Das Schl usselwort static bedeutet: Die Funktion kann aufgerufen werden, ohne dass man sich von der entsprechenden Klasse ein Objekt erzeugen muss. Die Syntax ist Klassenname.Funktionsname(Uebergabeparameter)
86
KAPITEL 3. DAS OBJEKTORIENTIERTE KONZEPT VON JAVA Sie erh alt ihre Eingangsdaten als Ubergabeparameter und gibt das Ergebnis als R uckgabewert zur uck. Innerhalb der Funktion gibt es kein this-Objekt. Die Funktion kann also nicht auf Attribute eines this-Objekts zugreifen.
Wenn innerhalb einer Methode nicht auf Attribute zur uckgegrien wird, ist es sinnvoll, sie durch das Schl usselwort static statisch (d.h. zu einer Funktion) zu machen. Wir haben schon eine ganze Reihe von Funktionen (statischen Methoden) aus der Java-Bibliothek kennengelernt, z.B. String.format, JOptionPane.showInputDialog oder Integer.parseInt. Man erkennt sie daran, dass sie in der Notation Klassenname.Funktionsname(Uebergabeparameter) aufgerufen werden. Die Bedeutung von public static void main(String args[]) Auch die Funktion main, mit der ein Programm startet, besitzt Ubergabeparameter. Um zu verstehen, welche Werte dort u ussen wir bergeben werden, m wissen, was intern geschieht, wenn man in einer Windows-Eingabeauorderung oder einer Linux-Konsole ein Java-Programm mit folgender Zeile startet: java ClassXYZ par1 par2 Es wird die Datei ClassXYZ.class gesucht. Innerhalb dieser Klasse wird die Funktion public static void main(String args[])} gesucht. Wenn sie nicht gefunden wird, wird eine Fehlermeldung zur uckgegeben. Die Strings par1 und par2 (die sogenannten Kommandozeilen-Parameter werden in ein String-Feld gepackt und stehen in der Main-Routine als args[] zur Verf ugung. In Eclipse kann man einem Java-Programm auf folgende Weise KommandozeilenParameter mitgeben: Run Run Congurations... Im Auswahlfenster Name des Java-Programms ausw ahlen. Reiter Arguments. Kommandozeilen-Parameter unter Program arguments eintragen.
87
Auch Variablen k onnen das Schl usselwort static erhalten. Damit erh alt man eine Variable, die mit Klassenname.variablenname aus allen Klassen heraus angesprochen werden kann. Eine solche Variable nennt man auch globale Variable. Sie existiert pro Klasse nur einmal, gleichg ultig, wie viele Objekte es von dieser Klasse gibt. Als Beispiel wollen wir ein Programm erstellen, das z ahlt, wieviele Objekte einer Klasse erstellt wurden. public class ObjektZaehler { private static int anzahl = 0; public ObjektZaehler() { ObjektZaehler.anzahl++; } public static int getAnzahl() { return ObjektZaehler.anzahl; } } Es gibt die statische Variable ObjektZaehler.anzahl, die eindeutig ist und nur ein einziges Mal existiert (innerhalb der Klasse ObjektZaehler k onnte man sie auch einfach mit anzahl ansprechen). Jedes Mal, wenn ein neues Objekt vom Typ ObjektZaehler erzeugt wird, wird die Variable um eins hochgez ahlt. Die Funktion getAnzahl zur Abfrage der Variablen ist ebenfalls statisch, kann auf statische Variablen (im Gegensatz zu Attributen) aber problemlos zugreifen. //statische Variable
3.7.3
Zu diesem Kapitel ben otigen wir wieder die Klasse Bruch. Auerdem benutzen wir eine zweite Klasse Haupt, die keine Attribute und nur statische Methoden hat. public class Haupt { public static void zahlMalZwei(double i) { i = i * 2; } public static void bruchMalZwei(Bruch r) { r.multipliziere(2); } public static void main(String[] args) {
88
//-->5
Bruch b = new Bruch(5,1); bruchMalZwei(b); System.out.println(b); //-->10/1 } } Die Parameter ubergabe beim Aufruf von ZahlMalZwei kann man folgendermaen darstellen: Name: z Typ: int Wert: 5 Name: z Typ: int Wert: 5 Name: z Typ: int Wert: 5 Name: z Typ: int Wert: 5 Wichtig ist, dass die Variable z beim Methodenaufruf in die Variable i kopiert wird. Wird i in der Methode ver andert, hat das keine Auswirkung auf z. Dies nennt man Call by value oder Wert ubergabe. Bei der zweiten Methode BruchMalZwei wird im Gegensatz dazu ein Objekt u asst sich gut mit der Klebezettel-Darstellung bergeben. Die Situation hier l 11 verstehen: Bruch
5 1
11 Schattierte Variablennamen bedeuten, dass diese Variable im momentanen Fokus nicht ansprechbar ist.
89
Bruch
10 1
Bruch
10 1
Der springende Punkt ist, dass b eine Objekt-Variable ist und Objekte als Refe renz gespeichert werden. Bei der Ubergabe des Wertes wird nicht der Wert selbst kopiert, sondern die Speicheradresse des Werts. b und r verweisen also auf das gleiche Objekt. Wenn also r ver andert wird, wird gleichzeitig auch b ver andert. Diese Ubergabe nennt man Call by Reference oder Referenz ubergabe. Bei Call by Value wird der Wert selbst kopiert. Bei Call by Reference wird nur die Speicheradresse kopiert, d.h. Anderungen des Wertes werden quasi mit zur uckgegeben. In Java h angt es vom u ubergabe oder Refebergebenen Datentyp ab, ob Wert renz ubergabe ausgef uhrt wird. Primitive Datentypen sind Wertparameter: Die Ubergabe erfolgt als Wert uber gabe; Anderungen innerhalb der Methode wirken sich auerhalb nicht aus. Objekte sind Referenzparameter: Die Ubergabe erfolgt als Referenz uber gabe; Anderungen innerhalb der Methode wirken sich auch auerhalb aus. Wir betrachten noch einmal eine andere Situation. Wir wollen eine Methode schreiben, die einen Bruch auf den Wert 1 1 setzt. Der Aufruf soll folgendermaen aussehen: Bruch b1 = new Bruch(1,2); //Dummy-Wert setToOne(b1); //b1 wird auf 1/1 gesetzt. Unser erster Versuch hat das folgende Aussehen: public void setToOne(Bruch x1) { Bruch z = new Bruch(1,1); x1 = z; }
90
Da Br uche als Referenzen u bergeben werden, hoen wir, dass am Ende der Methode b1 den Wert 1 besitzt. Leider ist dies nicht der Fall. Wir betrachten uns dazu f ur jeden Schritt die Variablen: Vor dem Methodenaufruf Bruch
1 2
b1
b1
x1
Bruch b1 x1
1 1
Nach x1 = z; Bruch
1 2
b1
1 1
x1
b1
Wichtig ist hier, dass der Aufruf x1 = z; zwar den Zettel x1 umklebt, aber nicht den Zettel b1. Man muss f ur den gew unschten Eekt das u bergebene Objekt selbst ver andern, so wie im folgenden Beispiel: public void setToOne(Bruch x1) { x1.setZaehler(1); x1.setNenner(1); }
91
3.7.4
Mehrere Ru ckgabewerte
Eine Methode oder eine Funktion kann mehrere Eingangsparameter, aber maximal einen Ausgangsparameter haben. Es gibt aber F alle, in denen man sich eine Funktion mit mehreren Ausgangsparametern w unscht. Nehmen wir als Beispiel eine Funktion zerlege, die ein Bruch-Objekt x als Eingangsparameter erh alt und zwei Bruch-Objekte zur uckgeben soll, in denen der ganzzahlige Anteil sowie der Rest enthalten ist. Es gibt drei M oglichkeiten, dies zu erreichen: Man gibt ein Feld mit zwei Bruch-Werten zur uck: public static Bruch[] zerlege(Bruch x) Man schreibt f ur den R uckgabewert eine eigene Klasse (hier ZerlegterBruch): public static ZerlegterBruch zerlege(Bruch x) Man verwendet einen (oder zwei) zus atzliche Ubergabeparameter als R uck gabewerte. Bedingung ist, dass die zus atzlichen Ubergabeparameter Objekte sind und damit als Referenz u bergeben werden: public void zerlege(Bruch b, Bruch ganzzahl, Bruch rest) Diese M oglichkeit wird wie folgt aufgerufen: Bruch b = new Bruch(7,5); Bruch ganzzahl = new Bruch(1,1); //Dummy-Wert Bruch rest = new Bruch(1,1); //Dummy-Wert zerlege(b, ganzzahl, rest); //innerhalb von zerlege(..) erhalten ganzzahl und rest die //gewuenschten Werte.
92
Kapitel 4
4.1
Beginnen wir noch einmal mit den verschiedenen Rollenperspektiven. Der Modulentwickler schreibt eine bestimmte Klasse K. Diese Klasse soll m oglichst universell in mehreren Projekten verwendet werden. Der Projektentwicker schreibt ein komplettes Programm P und benutzt dabei die Klasse K. Der Endanwender benutzt das Programm P. Wir nehmen an, der Projektentwickler benutzt die Klasse K fehlerhaft. Beispiel: K ist die Klasse, die einen Bruch repr asentiert und der Projektentwickler versucht, den Nenner des Bruchs auf 0 zu setzen. Dann muss der Modulentwicker daf ur sorgen, dass die Klasse K eine Exception ausl ost. Dazu erzeugt er sich ein Objekt einer Exception-Klasse und l ost sie mit dem Schl usselwort throw aus. Beispiel: public void setNenner(int nenner) { if (nenner==0) { throw new ArithmeticException("Nenner darf nicht 0 werden."); } else { this.nenner = nenner; } 93
94 KAPITEL 4. AUSNAHMEBEHANDLUNG (EXCEPTION HANDLING) Falls der Projektentwickler die Exception nicht f angt, bricht das Programm (genauer: der Thread) mit einer Java-Fehlermeldung ab. Da der Endanwender dar uber sicherlich nicht erfreut w are, muss der Projektentwickler die Exception fangen und entsprechend darauf reagieren. Zum Beispiel k onnte er dem Endanwender eine passende Fehlermeldung ausgeben.
Modul-Entwickler Modul-Anwender (Projekt-Entwickler) Methodenaufruf Exception throw new Exception() try { ...} catch { ...} Sinnvolle Fehlermeldung Operation wiederholen Abbruch Endanwender
Exceptions werden gefangen, wenn sie in einem try-Block stehen und es einen catch-Block f ur die passende Exception gibt. Beispiel: try { bruch.setNenner(x); System.out.println("Eingabe ok"); } catch (ArithmeticException e) { System.out.println("Eingabe wird ignoriert"); } System.out.println("Weiter gehts"); //1 //2 //3 //4 //5 //6 //7
Falls der Aufruf von setNenner erfolgreich war, ist die Reihenfolge der durchlaufenen Zeilen 1,2,3,7. Gab es bei setNenner eine Exception, ist die Reihenfolge der durchlaufenen Zeilen 1,2,5,7.
4.2
Im letzten Abschnitt haben wir gesehen, wie man Exceptions f angt und eigene Fehlerbehandlungsroutinen aufruft. Im Folgenden gehen wir u ber den Sto von Kapitel 3 hinaus. Die n achste Frage ist, was im entsprechenden catch-Block sinnvollerweise zu tun ist. Wir untersuchen jetzt also den Code. . . try { ... } catch (xyzException e) { //... der an dieser Stelle stehen sollte. }
95
4.2.1
Zun achst wollen wir m oglichst viel Information u ber die Art des Fehlers erhalten. Es ist wichtig zu wissen, dass Exceptions eigentlich nur eine spezielle Art von Klassen sind, die die Besonderheit haben, dass sie geworfen (oder ausgel ost) werden k onnen. Sehen wir uns an einem Beispiel den genauen Ablauf an: public class ExceptionTest { public static void main(String[] args) { try { int c = 5/0; System.out.println("Division gelungen"); } catch (ArithmeticException e) { System.out.println(e.getMessage()); //ergibt / by zero } } } Die Zeile int c=5/0; bewirkt, dass Java intern ein neues Objekt der Klasse ArithmeticException erzeugt, dass dieses Objekt geworfen oder ausgel ost und damit die Fehlerbe handlungsroutine angesprungen (oder das Programm beendet) wird und im catch-Block u ber die Variable e auf das Exception-Objekt zugegrien werden kann. Es entspricht einer Konvention, als Variable e zu verwenden, der Name ist aber beliebig: catch (ArithmeticException beliebigerName) Dem Exception-Objekt kann man in der Fehlerbehandlungsroutine mehr Informationen u ber den aufgetretenen Fehler entlocken: 1. Die Exception-Klasse (hier ArithmeticException) weist schon auf die Art des Fehlers hin. 2. Mit String s = e.getMessage(); erh alt man eine n ahere Erl auterung des Fehlers. 3. Mit e.printStackTrace(); kann man die u bliche Fehlermeldung auf dem Bildschirm ausgeben und mit e.getStackTrace(); kann man die Programmzeile ermitteln, an der der Fehler aufgetreten ist (f ur genauere Information bitte in der API nachschlagen).
4.2.2
Was schreibt man nun in einen catch-Block? Man muss sich f ur eine von drei M oglichkeiten entscheiden: Wiederholen: Zum Beispiel Benutzereingaben kann man wiederholen lassen.
96 KAPITEL 4. AUSNAHMEBEHANDLUNG (EXCEPTION HANDLING) Default-Werte: Man kann versuchen, den fehlerhaften Wert durch einen Default-Wert zu ersetzen. Exisitiert z.B. eine Kongurations-Datei nicht, kann man eine Default-Konguration benutzen. Abbrechen: Der Fehler ist so schwerwiegend, dass das Programm nicht korrekt fortgesetzt werden kann (z.B. Festplatte nicht erreichbar) oder ein Programmierfehler ist aufgetreten. Dann kann man das Programm abbrechen. Eine naheliegende Wahl ist ein Abbruch, der dem normalen Abbruch gleicht, wenn die Exception nicht gefangen wurde. Die Zeilen dazu sind: .... }catch (Exception e) { e.printStackTrace(); System.exit(1); }
4.2.3
Im letzten Beispiel wurde eine ArithmeticException gefangen. Andere Exceptions l osen dort nach wie vor einen Programmabbruch aus. Ein try-catch-Block darf aber auch mehrere verschiedenen Exceptions fangen. Die Syntax hierzu sieht folgendermaen aus: try { .... } catch (ArrayIndexOutOfBoundsException e) { .... } catch (NumberFormatException e) { .... } Dann wird, je nach Exception-Klasse, eine der Fehlerbehandlungsroutinen angesprungen. Es ist m oglich, alle Exceptions in einer einzigen Fehlerbehandlungsroutine abzufangen, indem man try { .... } catch (Exception e) { .... } schreibt. Damit kann man verhindern, dass eine unerwartete Exception ein Programm zum Absturz bringt. Um dann allerdings feststellen zu k onnen, welche Exception nun aufgetreten ist, braucht man den instanceof-Operator, der erst in den n achsten Kapiteln erl autert wird. Allgemein wird diese Methode als schlechter Stil betrachtet.
97
4.2.4
try-catch-Bl ocke d urfen auch verschachtelt werden. Sowohl im try- als auch im catch-Block d urfen weitere try-catch-Bl ocke eingebettet sein. Falls eine Exception auftritt, wird der innerste passende catch-Block angesprungen. Verschach telte try-catch-Bl ocke sind schlecht f ur die Ubersichtlichkeit eines Programms. Verwenden sie sie nur sparsam.
4.3
Die Exceptions, die wir bisher betrachtet haben, geh oren alle zu den unchecked exceptions. Es gibt auerdem noch die checked exceptions, die sich etwas anders verhalten. Die H augsten sind die IOException und die FileNotFoundException. Diese Exceptions werden ausgel ost, wenn z.B. beim Schreiben oder Lesen von Dateien ein Fehler auftritt. Wenn checked exceptions nicht mit try-catch gefangen werden, l asst sich das Programm nicht compilieren (und noch weniger ausf uhren). Auch Eclipse zeigt gleich eine Fehlermeldung an. Es gilt also festzuhalten: Unchecked Exceptions k onnen durch try-catch abgefangen werden. Bei nicht-Abfangen wird das Programm mit einer Fehlermeldung beendet. Checked Exceptions m ussen mit try-catch abgefangen werden. Die Philosophie der Java-Entwickler ist, dass sauber geschriebene Programme f ur manche Fehler eine Fehlerbehandlungsroutine beinhalten m ussen. Ein gutes Beispiel ist das Onen einer Datei auf der Festplatte: Scanner sc = new Scanner(new File("matrix.dat")); Hier sagen die Entwickler: Wenn ein Java-Programmierer eine Datei onet, muss er eine Fehlerbehandlung f ur den Fall schreiben, dass die Datei nicht auf der Festplatte vorhanden ist. In dieser Fehlerbehandlung k onnte er den Benutzer eine andere Datei ausw ahlen lassen, Default-Werte nehmen oder das Programm abbrechen lassen. Vergleich der Java-Syntax mit der anderer Sprachen Java hat zwei Besonderheiten, die sich von sprachenunabh angigen Einteilungen, aber auch von C# oder Python deutlich abheben. Was allgemein als Exception bezeichnet wird, heit in Java Throwa ble. Throwables teilen sich auf in Errors und Exceptions. Java versucht, zwischen Systemfehlern (Errors) und Programmfehlern (Exceptions) zu unterscheiden, was in anderen Sprachen so nicht anzutreen ist. Checked Exceptions sind ebenfalls eine Java-Spezialit at. In anderen Sprachen, wie C++, C# oder Python sind alle Exceptions unchecked.
98 KAPITEL 4. AUSNAHMEBEHANDLUNG (EXCEPTION HANDLING) Ob eine Exception eine checked ist oder nicht, kann man in der Java-API daran erkennen, ob ganz oben in der Darstellung der Klassenhierarchie das Wort RuntimeException vorkommt oder nicht.1 Beispiele: IOException: checked java.lang.Object extended by java.lang.Throwable extended by java.lang.Exception extended by java.io.IOException
ArithmeticException: unchecked java.lang.Object extended by java.lang.Throwable extended by java.lang.Exception extended by java.lang.RuntimeException extended by java.lang.ArithmeticException
4.3.1
Zun achst steht die Entscheidung an, ob die Exception checked oder unchecked sein soll. Die Frage ist: Will ich mich und andere dazu zwingen, den Fehler zu behandeln oder nicht? Dazu gibt es keine klaren Regeln, nur Hinweise: Wenn die ArithmeticException checked w are, m usste jede Integer-Division in einen try-catch-Block gesteckt werden. Das w urde den Programmcode stark aufbl ahen. Es ist schlechter Programmierstil, eine Datei zu onen und nicht zu u berpr ufen, ob die Datei u berhaupt vorhanden ist. Irgendwann wird die Datei einmal nicht vorhanden sein. Daher ist die FileNotFoundException checked. Anschlieend sieht man in der Java-API nach, ob es eine passende Exception gibt (z.B. wenn in der Java-Bibliothek ein ahnlicher Fehlerfall auftritt). Falls keine passende Exception vorhanden ist, kann man selbst eine Exception-Klasse schreiben, wozu wir aber noch weiteres Wissen u ber die Objektorientierung brauchen. Daher nur die Stichworte: checked Exceptions m ussen von Exception abgeleitet werden. unchecked Exceptions m ussen von RuntimeException abgeleitet werden.
1
Das heit, ob die Klasse von der Klasse RuntimeException abgeleitet ist oder nicht.
99
4.3.2
Entscheidet man sich beim vorigen Beispiel f ur eine checked exception, gibt es eine Besonderheit: public void setNenner(int nenner) throws IOException { if (nenner != 0) { this.nenner = nenner; } else { throw new IOException("Nenner gleich 0"); } } Im Methodenkopf m ussen alle checked exceptions angegeben werden, die aus der Methode herauskommen k onnen. Dies geschieht mit dem Schl usselwort throws, das von den m oglichen Exceptions (evtl. durch Komma getrennt) gefolgt wird. Unchecked exceptions k onnen, aber m ussen hier nicht angegeben werden.
4.4
W ahrend der gr ote Teil dieses Unterkapitels Sto aus Kapitel 7 voraussetzt, kann man den folgenden Abschnitt auch mit den bisherigen Kenntnissen benutzen, wenn man hinnimmt, dass der genaue Mechanismus noch nicht komplett verstanden werden kann.
4.4.1
Oft ndet man in der Java-API keine Exception mit dem passenden Namen. Manchmal will man auch von vornherein eine eigene Exception benutzen und Verwechslungen mit der Java-API ausschlieen. Zum Beispiel wollen wir uns eine Exception mit dem Namen BruchException erzeugen. Wir haben die Wahl, ob die neue Exception checked oder unchecked sein soll. F ur eine unchecked Exception sieht der Code folgendermaen aus: public class BruchException extends RuntimeException { public BruchException(String s) { super(s); } } F ur eine checked Exception muss nur in der ersten Zeile das Wort RuntimeException durch Exception ersetzt werden: public class BruchException extends Exception { public BruchException(String s) { super(s); } }
4.4.2
Ab hier sind Kenntnisse aus Kapitel 7 n otig. Exceptions sind in Java Unterklassen der Klasse Exception. Auer Exceptions k onnen in Java auch Errors geworfen werden, doch dies sollte der Java-Runtime selbst vorbehalten bleiben. Die Java-Vererbungshierarchie sieht stark verk urzt wie folgt aus: Throwable (unchecked) | | +------- Error (unchecked) | +------- Exception (checked) | | +--------- IOException (checked) | | | +-- FileNotFoundException (checked) | +--------- RuntimeException (unchecked) | +--------- ... Zum Ableiten bieten sich besonders die Exception-Basisklassen an. Je nachdem, welche Basisklasse man w ahlt, erh alt man Exceptions unterschiedlichen Typs: Basisklasse Throwable Error Exception IOException Bemerkung Nicht empfohlen. Nicht empfohlen (werden nur vom System ausgel ost). Checked Exception ohne spezielle Eigenschaften. Checked Exception im Zusammenhang mit IOOperationen. Vorteil: Alle IOExceptions k onnen mit catch(IOException e) gemeinsam gefangen werden. Unchecked Exception ohne spezielle Eigenschaften.
RuntimeException
Auch in anderen objektorientierten Sprachen ndet sich solch eine Hierarchie. Die Trennung in Throwable, Error und Exception ist in Java recht eigenwillig. In C# oder Python sind alle Exceptions von Exception abgeleitet. Beispiel: Ein Programm soll aus einer Datei eine Matrix einlesen. Falls die Daten keine Matrix darstellen, soll eine MatrixFormatException geworfen werden, die selbst zu schreiben ist. Die Exception soll checked sein. Da der Fehler beim Einlesen einer Datei auftritt, soll es eine IOException sein. Der einfachste Code ist:
4.4. VERERBUNG UND EXCEPTIONS public class MatrixFormatException extends IOException {} Die Exception wird dann wie eine normale Exception eingesetzt. if(....) { throw new MatrixFormatException(); } und muss entsprechend in einem try-catch-Block gefangen werden.
101
Exceptions aus der Java-Klassenbibliothek haben noch einen String message als Zusatzinformation, die der Exception im Konstruktor u bergeben wird. Um das bei der selbstgeschriebenen Exception nutzen zu k onnen, muss der Klasse ein entsprechender Konstruktor hinzugef ugt werden: public class MatrixFormatException extends IOException { public MatrixFormatException(String message) { super(message); } } Der Konstruktor ruft einfach den Konstruktor der Basisklasse auf. Es ist prinzipiell m oglich (aber nur beschr ankt sinnvoll), der Exception noch beliebige andere Funktionalit at hinzuzuf ugen.
4.4.3
Es ist m oglich, in einem catch-Block ganze Gruppen von Exceptions zu fangen, wenn man die Exception-Basisklassen f angt. Zum Beispiel f angt try { ... } catch (RuntimeException e) { ... } alle Runtime-Exceptions, d.h. alle Klassen, die von RuntimeException abgeleitet sind. Die Variable e im catch-Block hat dann aber auch den (statischen) Typ RuntimeException. Wenn man wissen will, welchen dynamischen Typ e hat, also welche Exception wirklich aufgetreten ist, muss man mit instanceof den dynamischen Typ abfragen. Noch mehr Exceptions werden abgefangen, wenn man die Typen Exception oder sogar Throwable verwendet.
Kapitel 5
Die Java-Klassenbibliothek
5.1 Programmiersprache Java und Java-Plattform
Gedanklich sollte man zwei Bedeutungen von Java nicht vermischen. Es gibt zum einen die Programmiersprache Java. Zum anderen gibt es die Java-Plattform, die aus der Laufzeitumgebung besteht und auch die Java-Klassenbibliothek umfasst, d.h. alle Systemaufrufe von System.out.println bis JOptionPane. showInputDialog. Gew ohnlich programmiert man auf der Java-Plattform in der Programmiersprache Java. Es gibt aber auch andere Sprachen, die die JavaPlattform benutzen, z.B. Groovy. Microsoft hat prinzipiell ein ahnliches Konzept, trennt die Begrie aber. Das Pendant der Java-Plattform ist dort .NET, und der Programmiersprache Java entspricht die Sprache C#.1 Andere Sprachen f ur .NET sind C++ oder VB.NET. Neue Programmiersprachen bauen oft auf eine der beiden Plattformen auf. Beide Plattformen sind sehr aufwendig und haben einen groen Vorsprung gegen uber Neuentwicklungen. Die Zahl der Plattformen ist daher wesentlich kleiner als die Zahl der Programmiersprachen. Wir haben jetzt das R ustzeug, um die meisten Informationen aus der JavaAPI zu verstehen. Sehen wir uns also einige n utzliche Klassen aus der JavaKlassenbibliothek (d.h. der Java-Plattform) an.
5.2
5.2.1
Bisher musste f ur die Eingabe u onet ber Tastatur immer ein Grakfenster ge werden. Man kann zwar auch direkt u ber die Konsole (den Standard-Input) Daten eingeben, aber wir haben erst jetzt die n otigen Vorkenntnisse, um die dazu n otigen Anweisungen komplett zu verstehen. 2 Zur Eingabe u otigt man ein Objekt der Klasse ber die Konsole ben java.util.Scanner. Mit den Methoden dieses Objekts kann man primitive DaDer Standard der Plattform heit Common Language Infrastructure (CLI). Die bekanntesten Implementationen der CLI sind .NET und Mono. 2 Leider funktioniert System.console().readLine() noch nicht zusammen mit Eclipse.
1
103
104
tentypen und Strings einlesen. Zun achst muss das Paket java.util importiert werden. Dann muss ein Scanner-Objekt erzeugt werden. Im Konstruktor wird dem Scanner mitgeteilt, woher er seine Daten beziehen soll. Scanner sc = new Scanner(System.in); erzeugt einen Scanner, der seine Daten von der Konsole einliest. Mit diesem Objekt kann man nun, neben vielen anderen sch onen Sachen, zeilenweise Daten von der Konsole (bzw. der Tastatur) einlesen. import java.util.*; public class Tastatur { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("Eingabe: "); String s = sc.nextLine(); System.out.println("Eingabe: "+s); } } //class Wenn man Integer-Zahlen einlesen will, muss man den String jetzt noch mit Integer.parseInt() umwandeln. Das Umwandeln kann man sich sparen, wenn man statt dessen zum Einlesen sc.nextInt() benutzt. Diese next-Befehle gibt es f ur alle Datentypen. F ur den Datentyp char heit die Methode einfach sc.next(), die anderen Namen bilden sich aus next+dem Namen des Datentyps. Ich empfehle diese Methode aber nur f ur Fortgeschrittene. Es gibt einige Fallstricke bei der Festlegung von Trennzeichen (sc.useDelimiter()) und bei der Fehlerbehandlung, die f ur diese Einf uhrung zu weit gehen w urden. Die etwas umst andlichere, aber sicherere Methode ist, die Daten zeilenweise einzulesen und dann anschlieend umzuwandeln. Ein gutes Beispiel daf ur sehen wir im n achsten Abschnitt.
5.2.2
Aufgabenstellung F ur diesen Abschnitt stellen wir uns folgende Aufgabe: Die Wahlergebnisse in Nordrhein-Westfalen werden auf einer Seite des Innenministeriums ver oentlicht: http://www.wahlergebnisse.nrw.de Man kann sie als nach Wahlkreisen aufgeschl usselte Textdatei herunterladen. Wir nehmen als Beispiel die Datei der Landtagswahlen 2010. Diese Datei k onnen wir unter LandtagswahlenLandtagswahl 2010Wahlkreisergebnisse insgesamt (TXT) herunterladen. Wir speichern sie in die Datei Wahldaten.ein im Projektverzeichnis von Eclipse. Der Aufbau ist (verk urzt, die Datei hat u ber 50 Spalten) so:
5.2. EINGABE UND AUSGABE VON DATEN 1;Aachen I;54540;17666;14810;10918;4053 ... 128;Olpe;62445;31935;15980;4593;4240
105
Eine Zeile steht f ur einen Wahlkreis. Die Spalten stehen nacheinander f ur Nummer und Name des Wahlkreises, die Gesamtanzahl der Zweitstimmen und f ur die Zweitstimmen von CDU, SPD, Gr unen und FDP. Die Eintr age sind jeweils durch ein Semikolon getrennt. Was uns nat urlich viel mehr interessiert, sind die Prozentangaben. Also wollen wir ein Programm schreiben, das die Datei einliest, in Prozentangaben umrechnet und das Ergebnis in eine neue Datei Wahldaten.aus schreibt. Einlesen einer Datei Als ersten Zwischenschritt lesen wir die Datei einfach ein und geben sie auf dem Bildschirm aus. Auch hierzu k onnen wir einen Scanner benutzen. Der Code dazu ist der folgende: import java.util.*; import java.io.*; public class Wahl { public static void main(String[] args) { File f = new File("Wahldaten.ein"); Scanner sc = null; try { sc = new Scanner(f); } catch (FileNotFoundException e) { System.out.println("Datei "+f+" nicht vorhanden"); System.exit(1); } while (sc.hasNextLine()) { String s = sc.nextLine(); System.out.println(s); } sc.close(); } } //class Wir erzeugen zun achst ein File-Objekt, das einen Dateinamen repr asentiert. Im Konstruktor wird der Dateiname angegeben. File ist Teil des Paketes java.io, das importiert werden muss. Das erzeugte File-Objekt gibt man im Konstruktor des Scanners an. Falls der Dateiname nicht existiert, l ost dieser Konstruktor eine FileNotFoundException aus. Diese Exception ist checked, d.h. Java verlangt hier einen try-catch-Block und eine Fehlerbehandlung. Wenn kein Fehler aufgetreten ist, k onnen die Daten der Reihe nach eingelesen werden. Auch hier kann nextLine() verwendet werden. Im Unterschied zum Einlesen von Tastatur sollte man vorher u ufen, ob man nicht bereits am Dateiende angekommen ist. berpr
106
Der Befehl dazu heit hasNextLine(). Am Ende muss mit close() der Scanner (und damit die Datei) wieder geschlossen werden. Vorsicht: Beim Einlesen von Tastatur darf der Scanner am Ende nicht geschlossen werden. Danach w aren keine Tastatureingaben u ber System.in mehr m oglich. Schreiben in eine Datei Die Ausgabe in eine Datei geht am einfachsten mit der Klasse java.io.PrintWriter. Das Onen einer Datei zum Schreiben geht ahnlich wie beim Lesen: File f = new File("Wahldaten.aus"); PrintWriter pw = null; try { pw= new PrintWriter(f); } catch (FileNotFoundException e) { System.out.println("Datei "+f+" laesst sich nicht oeffnen"); System.exit(1); } Der Konstruktor des PrintWriters erh alt ein File-Objekt. Falls die Datei nicht existiert, wird sie angelegt. Eine FileNotFoundException erh alt man, wenn der Ordner nicht exisitiert oder schreibgesch utzt ist. Falls bereits eine Datei gleichen Namens exisitiert, wird ihr Inhalt gel oscht. Soll die alte Datei nicht gel oscht, sondern neue Daten angeh angt werden, kann man den Befehl pw = new PrintWriter(new FileWriter(f, true)); verwenden, wobei das letzte true f ur append-Modus steht. Ein PrintWriter hat dieselben Methoden wie System.out. Es gibt also println, print und printf. Auch der PrintWriter ist am Ende wieder mit close() zu schlieen. Lo sung der Aufgabenstellung Wir onen zun achst die beiden Dateien zur Ein- und Ausgabe der Wahldaten. Den Code daf ur kann man aus den obigen Beispielen kopieren. Daher u berspringen wir diesen Teil und beginnen gleich mit dem Auslesen und Bearbeiten:
public class Wahl { public static void main(String[] args) { Scanner sc = null; PrintWriter pw = null; //Erzeugen des Scanners sc und des PrintWriters pw //wird hier uebersprungen while (sc.hasNextLine()) { String s = sc.nextLine();
5.3. LISTEN
String[] dat = s.split(";"); int gesamt = Integer.parseInt(dat[2]); //Wahlkreisnummer und Wahlkreis ausgeben pw.printf("%-3s %-20s",dat[0],dat[1]); //Zweitstimmen ausgeben pw.printf(" %5.2f",Integer.parseInt(dat[3])*100./gesamt); pw.printf(" %5.2f",Integer.parseInt(dat[4])*100./gesamt); pw.printf(" %5.2f",Integer.parseInt(dat[5])*100./gesamt); pw.printf(" %5.2f",Integer.parseInt(dat[6])*100./gesamt); pw.println(); //Zeilenumbruch } sc.close(); pw.close(); } }
107
Damit wird in die Datei Wahldaten.aus das gew unschte Ergebnis geschrieben. 1 Aachen I ... 128 Olpe 32,39 27,15 20,02 51,14 25,59 7,36 7,43 6,79
5.2.3
Mit Scanner und PrintWriter werden Zahlen als Klartext in die Datei geschrieben bzw. aus der Datei gelesen. Das heit, sie sind in einem Editor lesbar. Es gibt auch die M oglichkeit Zahlen in einem gepackten Format zu speichern, das der internen Java-Zahlendarstellung entspricht. Der Vorteil ist, dass die Dateien kleiner werden. Der Nachteil ist, dass sie nicht in einem Editor gelesen werden k onnen. Die Klassen dazu heien ObjectInputStream und ObjectOutputStream. In dieser Vorlesung wird nicht weiter darauf eingegangen.
5.3
5.3.1
Listen
Neue Aufgabenstellung
Wir erweitern die Aufgabenstellung aus dem letzten Abschnitt dahingehend, dass die Wahlkreise sortiert ausgegeben werden sollen. Sortiert werden soll nach den Prozentzahlen einer Partei. Als Beispiel nehmen wir die erste Partei in der Liste, also die CDU, ohne damit irgendeine Pr aferenz auszudr ucken. Im Unterschied zur letzten Aufgabenstellung k onnen wir die Datei nicht mehr Zeile f ur Zeile bearbeiten, sondern m ussen erst die gesamte Liste in den Speicher laden, dann sortieren und danach erst wieder in die Ausgabedatei schreiben. Wir wollen das Programm so exibel halten, dass die Anzahl der Wahlkreise variieren kann. Somit wissen wir vor dem Start des Programms nicht, wie viele Datens atze gespeichert werden m ussen. Neuere Programmiersprachen machen es uns aber an dieser Stelle einfach, denn sie besitzen gew ohnlich Felder, die mitwachsen, wenn man sie mit immer weiteren Werten bef ullt. Diese Felder
108
werden allgemein Listen genannt. Die meistbenutzte Klasse dieser Art heit in Java ArrayList. In diesem Kapitel soll beschrieben werden, wie ArrayLists einzusetzen sind, nicht jedoch, wie sie intern funktionieren. Letzteres ist Bestandteil der Algorithmen-Vorlesung. F ur die Experten wird allerdings in einem Absatz kurz auf die Unterschiede zwischen ArrayList und den ahnlichen Klassen Vector und LinkedList eingegangen. ArrayList liegt im Package java.util. Um mit ArrayLists umgehen zu k onnen, braucht man zun achst einmal Grundkenntnisse in zwei anderen Gebieten: Den Generics (oder generischen Datentypen) und den Wrapper-Klassen.
5.3.2
Die erste Aufgabe ist, festzulegen, aus welcher Art von Daten die Liste bestehen soll. Dazu ben otigt man die sogenannten generischen Datentypen. Ein generische ArrayList wird zum Beispiel so erzeugt: ArrayList<String> zeilen = new ArrayList<String>(); String ist der Typparameter, der in zwei spitzen Klammern steht. Das Beispiel erzeugt eine ArrayList, die nur Strings aufnehmen kann. Generic-Programmierung ist ein komplexes Thema, auf das im Rahmen dieser Vorlesung nicht noch n aher eingegangen wird. Sie ahneln den Templates aus C++, welche allerdings nochmal deutlich m achtiger sind. Eine Besonderheit wird allerdings noch im n achsten Kapitel angesprochen.
5.3.3
Wrapper-Klassen
In Generics d urfen nur Klassen angegeben werden, keine primitiven Datentypen. Die Zeile //KEIN KORREKTES JAVA ArrayList<int> zahlen = new ArrayList<int>(); ist in Java falsch. Wenn primitive Datentypen gespeichert werden sollen, muss man den Umweg u ahlen. Es gibt f ur jeden primitiven ber die Wrapper-Klassen w Datentyp eine eigene Wrapper-Klasse. Der Name der Wrapper-Klasse leitet sich vom primitiven Datentyp ab. Gew ohnlich wird der erste Buchstabe gro statt klein geschrieben (Ausnahmen: int, char). prim. Datentyp boolean char byte short int long oat double Wrapper-Klasse Boolean Character Byte Short Integer Long Float Double
5.3. LISTEN
109
Die Wrapper-Klasse packt einen primitiven Datentyp in ein Objekt ein. Der wesentliche Teil des Codes der Klassenbibliothek sieht folgendermaen aus: public class Integer { private final int value; public Integer(int value) { this.value = value; } public int intValue() { return value; } } Das Schl usselwort final bedeutet, dass der Wert von value nur ein einziges Mal gesetzt werden kann (in diesem Fall im Konstruktor). Man packt einen int-Wert mit int i = 5; Integer wr = new Integer(i); ein und mit int j = wr.intValue(); wieder aus.3 iehe auch den folgenden Abschnitt: Autoboxing.
5.3.4
Autoboxing
Das Verfahren zum Ein- und Auspacken in Wrapper-Klassen wurde mit Java 1.5 durch das sogenannte Autoboxing, eine Art automatischer Umwandlung zwischen primitivem Datentyp und Wrapperklasse, stark vereinfacht. Ein- und Auspacken geschieht in den folgenden Zeilen automatisch im Hintergrund: int i= 5; Integer wr = i; int j = wr;
//Einpacken //Auspacken
Sollen primitive Datentypen in einer ArrayList gespeichert werden, gibt man im Generic die entsprechende Wrapper-Klasse an: ArrayList<Integer> liste = new ArrayList<Integer>(); Beim Hinzuf ugen von int-Werten werden diese automatisch in ein WrapperObjekt gepackt.
3
110
5.3.5
Zun achst wird eine ArrayList erzeugt. Wir erzeugen jeweils eine f ur Strings (ohne Wrapper-Klassen) und Integers (mit Wrapper-Klassen). ArrayList<String> stringListe = new ArrayList<String>(); ArrayList<Integer> intListe = new ArrayList<Integer>(); Die Methode add f ugt einer ArrayList ein neues Element hinzu: stringListe.add("Hallo"); intListe.add(5); Die Elemente sind innerhalb der Liste durchnummeriert. Das erste Element hat den Index 0. Die aktuelle L ange der Liste kann man mit int len = stringListe.size(); erfragen. Das Element mit dem Index i kann man wie im folgenden Beispiel gezeigt auslesen und setzen: int a = intListe.get(i); String s = stringListe.get(i); intListe.set(i, 45); stringListe.set(i, "Tschuess"); Hier eine Zusammenfassung der interessantesten Methoden der Klasse ArrayList. E steht f ur den Datentyp, den man im Generic (in den spitzen Klammern) angegeben hat. boolean add(E o) H angt ein neues Element hinten an. Gibt immer true zur uck. L oscht die Liste. Gibt das Element Nr. index zur uck. Gibt den Index zur uck, an dem das Element zum 1. Mal vorkommt (sonst -1). L oscht das Element an der Position index. Gibt die Zahl der Elemente zur uck.
Eine Methode zum Einf ugen von Elementen gibt es in ArrayList nicht. In diesem Fall muss man auf die sehr ahnliche Klasse Vector ausweichen, die die Methode insertElementAt() besitzt.
5.3. LISTEN ArrayLists ohne Generics ArrayLists k onnen auch ohne Generics erzeugt werden: ArrayList l = new ArrayList();
111
Solche Listen k onnen alle Objekte speichern. Um diese Variante zu verstehen, fehlen uns allerdings noch einige Grundlagen der Objektorientierung. Fu r Experten Es gibt auer ArrayList zwei weitere ahnliche Klassen: Vector und LinkedList. Diese drei Klassen k onnen wie oben angegeben benutzt werden. Die Unterschiede liegen eher im Detail. ArrayList und Vector sind sehr ahnlich. Der Hauptunterschied besteht darin, dass auf Vector nur ein einziger Thread gleichzeitig zugreifen kann. Die dazu n otigen Synchronisationsmechanismen machen Vector langsamer als ArrayList. LinkedList hat als interne Datenstruktur eine verkettete Liste, w ahrend ArrayList ein dynamisches Feld besitzt. Meistens ist ein dynamisches Feld schneller als eine verkettete Liste. Eine Ausnahme ist allerdings, wenn oft Elemente vorne in der Liste eingef ugt werden.
5.3.6
F ur unsere L osung entwerfen wir zweckm aigerweise erst einmal eine Klasse Wahlkreis. Diese Klasse fasst die Informationen aus einem Wahlkreis zusammen. Wir fassen uns hier m oglichst kurz, halten die Attribute public und spendieren lediglich einen Konstruktor. public class Wahlkreis { public int nr; public String name; public double cdu, spd, gruene, fdp; public Wahlkreis(int nr, String name, double cdu, double spd, double gruene, double fdp) { this.nr = nr; this.name = name; this.cdu = cdu; this.spd = spd; this.gruene = gruene; this.fdp = fdp; } }
112
Zum Bearbeiten der Datei ben otigen wir einen Scanner und einen PrintWriter. Das Onen der Dateien funktioniert analog wie im letzten Abschnitt. Auerdem brauchen wir eine ArrayList, die als Elemente Objekte vom Typ Wahlkreis enth alt: ArrayList<Wahlkreis> liste = new ArrayList<Wahlkreis>(); Beim Einlesen erzeugen wir uns pro Zeile ein neues Wahlkreis-Objekt und f ugen es in die Liste ein: while (sc.hasNextLine()) { String s = sc.nextLine(); String[] dat = s.split(";"); int nr = Integer.parseInt(dat[0]); String name = dat[1]; int gesamt = Integer.parseInt(dat[2]); double cdu = Integer.parseInt(dat[3])*100./gesamt; double spd = Integer.parseInt(dat[4])*100./gesamt; double gruene = Integer.parseInt(dat[5])*100./gesamt; double fdp = Integer.parseInt(dat[6])*100./gesamt; Wahlkreis w = new Wahlkreis(nr, name, cdu, spd, gruene, fdp); liste.add(w); } Im n achsten Schritt wird die Liste sortiert. W are es eine Liste aus Strings oder Zahlwerten, k onnten wir einfach die Bibliotheksfunktion Collections.sort(liste); verwenden. Bei unserer Wahlkreis-Liste funktioniert das leider nicht, weil Java nicht wei, nach welchen Kriterien Wahlkreise sortiert werden m ussen. Wir k onnten das Interface Comparable oder besser noch Comparator implementieren. Dann w urde Collections.sort klappen, aber leider lernen wir erst im n achsten Kapitel, wie das geht. Also m ussen wir das Sortierverfahren selbst programmieren. Ein kurzer, nicht besonders schneller Algorithmus geht wie folgt.4 for (int i=0; i<liste.size(); i++) { for (int j=i; j<liste.size(); j++) { if (liste.get(j).cdu > liste.get(i).cdu) { Collections.swap(liste, i, j); //Elemente vertauschen } } }
Es handelt sich um eine vereinfachte Variante des Selection-Sorts. F ur die wenigen Daten, die wir zu sortieren haben, reicht dieser Algorithmus voll aus. Viel ausf urlicher wird dieses Thema in der Vorlesung Algorithmen behandelt.
4
113
Damit ist das Feld wie gew unscht sortiert und wir k onnen das Ergebnis ausgeben: //foreach-Schleife. Geht auch mit normaler for-Schleife for (Wahlkreis w: liste) { pw.printf("%-3d %-20s %5.2f %5.2f %5.2f %5.2f\n", w.nr, w.name, w.cdu, w.spd, w.gruene, w.fdp); } Aus der Ausgabedatei k onnen wir dann einfach die beiden Wahlkreise mit dem gr oten und dem kleinsten Anteil an CDU-Stimmen ablesen: 100 Paderborn I ... 15 Koeln III 51,74 23,09 9,01 6,71 4,75
Nat urlich w are es sinnvoll, wenn man die Partei ausw ahlen k onnte, nach der sortiert wird. Diese Aufgabe sparen wir uns aber lieber auf, bis wir Interfaces kennengelernt haben.
5.4
Assoziative Felder
Bisher hatten wir Felder mit fortlaufendem Index erzeugt. Wir wollen jetzt eine Tabelle anlegen, die die St adtek urzel auf Nummernschildern den jeweiligen St adten zuordnet. Sehr praktisch w are ein Feld, dessen Index auch aus Strings bestehen kann. Ein Aufruf sieht dann aus, wie: // C# ! Kein Java ! tabelle["AC"]="Aachen"; tabelle["HS"]="Heinsberg"; tabelle["DN"]="Dueren"; String x = tabelle["AC"]; //Aachen
Assoziative Felder sind wichtig und werden h aug gebraucht. In vielen Programmiersprachen ist exakt diese Syntax m oglich. In Java gibt es zwar auch assoziative Felder, aber die Syntax ist etwas anders. Zur besseren Beschreibung f uhren wir zun achst einige Begrie ein. In der Zeile // C# ! Kein Java ! tabelle["AC"]="Aachen"; ist AC der Schl ussel (bei normalen Feldern w urde man Index sagen). Aachen ist der Wert, der zum Schl ussel AC geh ort. Ein Assoziatives Feld speichert also immer Schl ussel-Wert-Paare. F ur assoziative Felder gibt es in Java mehrere Klassen. Die meistgebrauchte davon heit HashMap. Im Konstruktor wird zun achst per Generic (siehe das Kapitel u ussel ber dynamische Felder) festgelegt, von welchem Datentyp Schl und Wert sein sollen. Im folgenden Beispiel sind Schl ussel und Wert beide vom Datentyp String.
114
HashMap<String, String> tabelle = new HashMap<String, String>(); Dann kann man mit put(Schluessel, Wert) Werte unter einer Schl usselbezeichnung speichern: tabelle.put("AC","Aachen"); und anschlieend auf das entsprechende Feldelement zugreifen: String s = tabelle.get("AC"); Die Abfrage eines Wertes, der nicht vorhanden ist String s = tabelle.get("K"); gibt null zur uck. Man kann auch alle anderen Klassen f ur Schl ussel und Wert w ahlen. W ahlt man Integer als Schl ussel, kann man ein Feld erstellen, deren Indizes nicht fortlaufend sein m ussen, wie das folgende Beispiel einer Liste von Aachener Buslinien zeigt: HashMap<Integer, String> linien=new HashMap<Integer, String>(); linien.put(2, "Eilendorf - Bushof"); linien.put(5, "Uniklinik - Driescher Hof"); linien.put(33, "Fuchserde - Vaals"); HashMaps ohne Generics Ebenso wie ArrayLists k onnen HashMaps erzeugt werden, ohne Generics anzugeben: HashMap h = new HashMap(); Eine solche HashMap kann als Schl ussel wie auch als Wert alle Objekte aufnehmen. Auch hier fehlen uns dazu noch einige Grundlagen der Objektorientierung. Geschwindigkeit Die der Klasse HashMap zugrundeliegene Datenstruktur ist die Hashtabelle, die in der Vorlesung Algorithmen und Datenstrukturen erkl art wird. Diese Da tenstruktur erm oglicht, als Schl ussel beliebige Datentypen zu verwenden und trotzdem sowohl den Speicherverbauch gering zu halten, als auch relativ schnell den einem Schl ussel zugeh origen Wert zu nden. Es geht deutlich schneller als das ganze Feld zu durchsuchen. Fu r Experten Es gibt auer HashMap zwei weitere ahnliche Klassen: Hashtable und TreeMap. Alle diese drei Klassen k onnen wie oben angegeben benutzt werden. Die Unterschiede liegen eher im Detail.
5.5. STRINGBUILDER
115
HashMap und Hashtable sind sehr ahnlich. Der Hauptunterschied besteht darin, dass auf Hashtable nur ein einziger Thread gleichzeitig zugreifen kann. Die dazu n otigen Synchronisationsmechanismen machen Hashtable langsamer als HashMap. TreeMap hat als interne Datenstruktur einen Rot-Schwarz-Baum, w ahrend HashMap eine Hashtabelle besitzt. Eine Hashtabelle ist deutlich schneller als ein Rot-Schwarz-Baum und auch als die bin are Suche in einem Feld. Ein Rot-Schwarz-Baum hat allerdings den Vorteil, dass die Elemente sortiert vorliegen.
5.5
StringBuilder
M oglicherweise haben sie bereits eine Funktion vermisst, die einzelne Buchstaben in Strings a ndern kann. Eine Art setChar(..) gibt es in der Klasse String nicht, wohl aber in der Klasse StringBuilder.5 Zun achst wird ein StringBuilder f ur einen String erzeugt: String t = "test"; StringBuilder sb = new StringBuilder(t); Jetzt kann mit der Methode setCharAt(int index, char c) ein einzelnes Zeichen ver andert werden: sb.setCharAt(0, f); //ergibt "fest"
Anschlieend kann der StringBuilder mit toString() wieder in einen String zur uckverwandelt werden: t = sb.toString(); StringBuilder k onnen auch die Ausf uhrungsgeschwindigkeit eines Programms verbessern. Wird z.B. folgende Zeile ausgef uhrt: String hw = "Hello"+"world"; dann werden zun achst intern die beiden Strings Hello und world erzeugt, beide in StringBuilder umgewandelt, sie mit append aneinander geh angt und das Ergebnis wieder in einen String gewandelt. Betrachten wir zum Vergleich zwei Methoden, die beide einen String zur uckgeben, indem sie count Mal einen String x hintereinanderh angen. Die zweite Variante arbeitet mit einem StringBuilder, die erste mit der Addition von Strings. public static String mult1(String x, int count) { String ret = ""; for (int i=0; i<count; i++) {
5 Es gibt das historisch altere Pendant StringBuffer, das aber heute nicht mehr empfohlen wird.
public static String mult2(String x, int count) { StringBuilder xbuild = new StringBuilder(x); StringBuilder ret = new StringBuilder(); for (int i=0; i<count; i++) { ret.append(xbuild); } return ret.toString(); } Bei hohen Werten von count ist die Variante mit einem StringBuilder erheblich schneller. Eine Zeitmessung ergab f ur 100000 Aneinanderkettungen: mult1 (String) mult2 (StringBuilder) 26 s 0.01 s
Die Klasse StringBuilder hat noch einige andere n utzliche Methoden, die man in der Klasse String vermisst. Wichtig beim Umgang ist: Ein String kann nicht nachtr aglich ver andert werden. Die String-Methoden erzeugen neue Strings. Ein StringBuilder kann dagegen sehr wohl nachtr aglich ver andert werden. Beispiel: String s = "Hallo"; String t = s.substring(2,3); //Erzeugt neuen String, //s bleibt unveraendert StringBuilder sb = new StringBuilder("Hallo"); sb.substring(2,3); //sb wird veraendert Eine Auswahl der n utzlichen Methoden ist: Methode delete(int start, int end) deleteCharAt(int index) insert(int offset, String s) replace(int start, int end, String s) Beschreibung Entfernt die Zeichen von start bis end-1. Entfernt Zeichen an Stelle index. F ugt String an Position oset ein. Ersetzt Zeichen von start bis end durch s (ohne regul are Ausdr ucke).
Kapitel 6
Interfaces
6.1
6.1.1
Grundlagen
Problemstellung
Bei unserer Problemstellung gehen wir wieder von der Klasse Bruch aus Kapitel 3 aus. Die Klasse repr asentiert eine rationale Zahl. Es gibt unter anderem einen Konstruktor, dem Z ahler und Nenner u bergeben werden und eine toStringMethode zur Ausgabe. Die Zeilen Bruch b = new Bruch(2,3); System.out.println(b); geben 2/3 auf dem Bildschirm aus. Jetzt kommt eine neue Anforderung auf. Das Ausgabeformat soll umstellbar sein. Es sollen auch die Formate 2 3 0.666666666666666 m oglich sein. Erschwerend kommt hinzu, dass der Modulbenutzer sich auch eigene Formate denieren will.
6.1.2
Einfacher L osungsansatz
Zun achst einmal soll gesagt sein, dass der Ansatz, der toString-Methode einfach einen Parameter hinzuzuf ugen public String toString(int format) { ... } nicht funktioniert, denn bei System.out.println(b) wird gezielt die parameterlose Variante gesucht und, wenn diese nicht selbst implementiert wurde, wird 117
118
KAPITEL 6. INTERFACES
wieder die Default-Variante (vom Typ Bruch@235ab41) benutzt. Das l asst sich aber noch relativ leicht verbessern. Ein verbesserter, aber nicht objektorientierter L osungsansatz ist der folgende: Wir f ugen der Klasse Bruch ein neues Attribut f ur das Ausgabeformat hinzu: private int ausgabeformat; Dazu kommen noch die entsprechenden Getter- und Setter-Methoden.1 Auerdem denieren wir uns einige statische Konstanten: public static final int EINZEILIG = 0; public static final int MEHRZEILIG = 1; public static final int DOUBLE = 2; und bauen in die toString-Methode ein switch-Konstrukt ein: public String toString() { switch(ausgabeformat) { case EINZEILIG: return zaehler+"/"+nenner; case MEHRZEILIG: return ...... case DOUBLE: return ""+getDoubleValue(); } } Dann k onnen wir einfach das Ausgabeformat umstellen: Bruch b = new Bruch(2,3); b.setAusgabeformat(Bruch.DOUBLE); System.out.println(b); //ergibt 0.666666666666666 Probleme Das Problem bei dieser Vorgehensweise ist folgendes: Wenn der Anwender ein neues, selbstdeniertes Format hinzuf ugen will, muss er an mindestens zwei Stellen den Code der Bruch-Klasse andern:2 Er muss eine neue Konstante hinzuf ugen. Er muss der switch-Anweisung einen neuen Zweig hinzuf ugen. Ein Prinzip der objektorientierten Programmierung ist aber gerade, dass der Modulanwender den Code der Modul-Klassen nicht andern soll. Man sagt:
Im Grunde sollte man statt dem int-Attribut eine Enumeration (Schl usselwort enum) verwenden, aber Enumerations werden in dieser Vorlesung nicht behandelt. 2 Eventuell muss zus atzlich auch die Setter-Methode ge andert werden.
1
6.1. GRUNDLAGEN
119
Der Source-Code der Klasse soll f ur den Anwender geschlossen sein, also nicht anderbar (und m oglicherweise gar nicht verf ugbar). Dennoch soll die Klasse oen sein f ur Anpassungen. Bei gr oeren Klassen w urde es den Anwender auch viel Zeit und M uhe kosten, sich in den Code eines fremden Programmierers einzudenken. Und was nach einigen Anderungen diverser Anwender dann als Code herauskommt, will auch keiner mehr wirklich haben. Also brauchen wir eine andere L osung.
6.1.3
Verbesserter L osungsansatz
Als Modell f ur unseren objektorientierten Ansatz nehmen wir ein Multifunktionswerkzeug. Ein solches Werkzeug kann man sich als einen kleinen Bohrer vorstellen, an deren Spitze man ganz unterschiedliche Aufs atze befestigen kann. Es gibt zum Beispiel Aufs atze zum Bohren, zum Schleifen oder zum Polieren.
Kupplung
Bohren Power
Schleifen
Polieren
Speed
Kupplung
Multifunktionswerkzeug
Wir unterscheiden drei Teile: Die Maschine ist das eigentliche Werkzeug mit den Bedienelementen. Der Aufsatz ist das Teil, das auswechselbar ist. Die Kupplung ist das Teil, an dem Maschine und Aufsatz zusammengesteckt werden. Wichtig dabei ist dass die Kupplungen von Maschine und Aufsatz zueinander passen. Dieses Schema kann man mit etwas Uberlegung gut auf unser Problem mit der Bruch-Klasse u achst die Bruch-Klasse mit bertragen. Wir identizieren zun der Maschine. Der Aufsatz dazu wandelt ein Bruch-Objekt in einen String um, beinhaltet also eine Methode, wie:
120
KAPITEL 6. INTERFACES
public String getStringDarstellung(Bruch b) { return b.getZaehler+"/"+b.getNenner(); } Dieser Aufsatz soll von der Maschine (dem Bruch-Objekt) aus aufgerufen werden. Die Kupplung, u ber die das geschieht, ist das Aussehen des Methodenaufrufs: public String getStringDarstellung(Bruch b) Diese Kupplung wird in Java (und auch in C#) durch ein sogenanntes Interface denert. Ein Interface umfasst die Denition eines oder mehrerer Methodenaufrufe. Der Java-Code dazu sieht wie folgt aus: public interface BruchDarstellung { String getStringDarstellung(Bruch b); } An der Stelle, an der sonst class steht, ndet sich jetzt das Schl usselwort interface. Von der Methode ist nur die Kopfzeile angegeben, gefolgt von einem Semikolon. Auerdem fehlt bei der Methode das Schl usselwort public, das man weglassen kann, weil Methoden eines Interfaces schon automatisch public sind. Im n achsten Schritt wird der Aufsatz mit einer Kupplung verbunden. Ein Aufsatz ist eine Klasse mit der Methode getStringDarstellung. Auerdem muss Java wissen, dass der Aufsatz zu der angegebenen Kupplung geh ort. Dazu dient das Schl usselwort implements: public class EinzeiligeDarstellung implements BruchDarstellung { public String getStringDarstellung(Bruch b) { return b.getZaehler+"/"+g.getNenner(); } } Dieses Schl usselwort gibt an, dass die Klasse EinzeiligeDarstellung ein Aufsatz ist, der zur Kupplung BruchDarstellung geh ort. Der Java-Compiler verlangt dann auch, dass die Klasse die Methode getStringDarstellung implementiert.
Maschine:
Klasse Bruch
Kupplung:
Interface BruchDarstellung
Aufs atze:
z.B. Klasse EinzeiligeDarstellung
6.1. GRUNDLAGEN
121
Als n achstes m ussen wir auch an der Bruch-Klasse noch die Kupplung anbringen und uns u atze ausgetauscht werden sollen. Zuletzt berlegen, wie Aufs m ussen wir daf ur sorgen, dass der Aufsatz innerhalb der toString-Methode auch angesprochen wird. Die Kupplung in der Bruch-Klasse ist ein Attribut vom Typ des Interfaces. Zum Auswechseln des Aufsatzes ben otigen wir eine entsprechende Setter-Methode. Eine Getter-Methode ist hilfreich, aber nicht elementar n otig. public class Bruch { ... private BruchDarstellung darstellung; public void setDarstellung(BruchDarstellung darstellung) { this.darstellung = darstellung; } ... } Am Ende andern wir die toString-Methode so ab, dass auf die getStringDarstellungMethode eben dieses Objekts zugegrien wird. public String toString() { String ret = darstellung.getStringDarstellung(this); return ret; Damit ist aus Entwicklersicht alles erledigt. Verwendung aus Benutzersicht Die Verwendung des Aufsatzes aus Benutzersicht sieht so aus: public static void main(String[] args) { Bruch b = new Bruch(1,2); EinzeiligeDarstellung brda = new EinzeiligeDarstellung(); b.setDarstellung(brda); System.out.println(b); Bemerkenswert sind die beiden mittleren Zeilen. Aufsatz erzeugen: In der 2. Zeile wird ein Aufsatz erzeugt. Es handelt sich um ein Objekt der Klasse EinzeiligeDarstellung. Aufsatz an Maschine anstecken: Das Aufsatz-Objekt wird der Methode setDarstellung u bergeben. Das funktioniert, obwohl als Ubergabetyp der Typ des Interfaces angegeben ist. Es funktioniert immer dann, wenn das u onnten also bergebene Objekt das Interface implementiert. Wir k
122
KAPITEL 6. INTERFACES auch ein Objekt eines anderen Typs MehrzeiligeDarstellung u bergeben, solange es das Interface BruchDarstellung implementiert. Das u bergebene Objekt wird dann im entsprechende Attribut abgespeichert. Maschine starten: Beim Aufruf der toString-Methode wird die Methode des Aufsatz-Objekts angesprochen und ihr Ergebnis zur uckgegeben.
6.1.4
Bemerkungen
Terminologie Der deutsche Ausdruck f ur Interface ist Schnittstelle. Die Vererbung eines Interface nennt man Schnittstellenvererbung oder Subtyping. Speziell in Java spricht man statt einer Vererbung meist von der Implementation eines Interfaces (so auch im weiteren Skript). UML-Diagramm
interface BruchDarstellung
Einzeilige Darstellung
Im Kopf des UML-Diagramms wird mit interface angezeigt, dass es sich um ein Interface handelt. Die Methodennamen werden kursiv geschrieben. Wird ein Interface von einer anderen Klasse implementiert, so wird das durch eine gestrichtelten Linie mit nicht ausgef ulltem Pfeil angezeigt. Falls viele Klassen mehrere Interfaces implementieren, kann dies zu einem Pfeilsalat f uhren. Des halb ist die folgende Kurzschreibweise ( Lollipop-Form) m oglich:
Einzeilige Darstellung
Interface BruchDarstellung
123
Hier stoen wir zum ersten Mal auf die Tatsache, dass der Typ einer Variablen und der Typ des Objekts, auf das die Variable zeigt, nicht u bereinstimmen m ussen. Die Zeile BruchDarstellung bd = new EinzeiligeDarstellung(); funktioniert unter der Voraussetzung, dass EinzeiligeDarstellung das Interface BruchDarstellung implementiert. Allerdings kann man von der Variablen bd jetzt nur noch die Methoden aufrufen, die im Interface deniert sind. Ausf uhrlich werden wir das Ganze noch im n achsten Kapitel (Vererbung) betrachten.
6.2
6.2.1
Entwurfsmuster sind L osungs-Schablonen f ur wiederkehrende Probleme aus der Software-Entwicklung. Eigentlich sind Entwurfsmuster ein Thema aus der Vorlesung Software Engineering, aber wir k onnen an ihnen jetzt schon gut nach vollziehen, wie Interfaces typischerweise eingesetzt werden. Andersherum gesagt: Wenn man Interfaces an einem sinnvollen Beispiel erl autert, landet man fast automatisch bei einem Entwurfsmuster. Der Schritt, dieses dann auch korrekt zu benennen, ist dann nur noch klein. Trotzdem sei hier noch eine kurze allgemeine Erl auterung vorangestellt. Es gibt bei der Java-Programmierung unterschiedliche Ebenen, die nacheinander erlernt werden m ussen: Zun achst muss man die Sprachkonstrukte als solche beherrschen. In der zweiten Ebene werden kleine algorithmische Probleme in Java wiedergegeben und gel ost. In der dritten Ebene werden gr oere Probleme mit objektorientierten Methoden strukturiert und gel ost. Die vierte Ebene, die wir jetzt angehen, ist, typische Muster aus mehreren Objekten zusammenzustellen. Es gibt sehr viele solche Muster. Am bekanntesten sind aber die ca. 20 Muster, die in dem Standardwerk Design Patterns von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides zusammengestellt sind.3 4
Die vier Autoren sind auch als Gang of Four, abgek urzt GoF bekannt. Empfehlenswerter f ur Anf anger ist das Buch Entwurfsmuster von Kopf bis Fu von Eric Freemann und Elisabeth Freeman.
4 3
124
KAPITEL 6. INTERFACES
6.2.2
Am Anfang des Kapitels hatten wir ja bereits ein sinnvolles Beispiel f ur den Einsatz eines Interfaces. Damit haben wir, ohne es zu kennen, bereits das erste Entwurfsmuster programmiert. Es heit Strategie. Die formale Denition nach Freeman ist: Das Strategy-Muster deniert eine Familie von Algorithmen, kapselt sie einzeln und macht sie austauschbar. Das Strategy-Muster erm oglicht es, den Algorithmus unabh angig von den Clients, die ihn einsetzen, variieren zu lassen. 5 Diese Denition ist ziemlich einfach zu verstehen, wenn man sie in unser Multifunktionsger at-Beispiel u bersetzt:
Maschine:
Der Client
Kupplung
Interface
Aufs atze:
Die Familie von austauschbaren Algorithmen.
Dass man den Algorithmus unabh angig von den Clients variieren lassen kann, bedeutet nichts anderes, als dass man die Aufs atze austauschen kann, ohne die Maschine selbst ver andern zu m ussen. Ubrigens hat man dieses Entwurfsmuster schon vor der objektorientierten Programmierung angewandt. Es hie damals nur anders. In C hat man das Problem mit Funktionszeigern gel ost. Wir werden sp ater noch einmal darauf zur uckkommen.
6.2.3
Sortieren von Bru chen Die Aufgabe, die wir uns in diesem Abschnitt stellen, ist folgende: Wir haben eine ArrayList von Br uchen public ArrayList<Bruch> bruchListe; und wollen diese Liste nach dem Wert der Br uche sortieren. Mit dem Datentyp ArrayList<String> hatten wir daf ur einfach die Methode Collections.sort(liste);
5 Die Bemerkung dazu im Buch selbst ist: Nehmen sie DIESE Denition, wenn sie Freunde beeindrucken wollen oder Vorgesetzte beeinussen m ussen.
125
benutzt. Nur leider funktioniert das hier nicht. Eclipse (bzw. der Java-Compiler) gibt uns die Fehlermeldung namens Bound mismatch. Die Ursache daf ur ist, dass Java nicht wei, wie Bruch-Objekte u berhaupt zu sortieren sind. Java 6 benutzt n amlich zum Sortieren auch das Strategie-Entwurfsmuster. Interessant daran ist, dass unsere Klasse Bruch jetzt nicht mehr in der Rolle der Maschine ist, sondern in der des Aufsatzes.
Maschine:
Collections.sort(..)
Kupplung:
Interface Comparable
Aufs atze:
z.B. Klasse Bruch
Die Methode Collections.sort kann Objekte sortieren, die auf die entsprechende Kupplung passen. Das zugeh orige Interface heit Comparable. Es ist Teil der Java-Bibliothek und steht in java.lang. Es ist m oglich und ratsam, das Interface mit einem Generic zu spezizieren. Im folgenden Beispiel wird das gleich deutlich werden. Wir modizieren die Bruch-Klasse so, dass Java erkennt, wie man zwei Br uche miteinander vergleicht: public class Bruch implements Comparable<Bruch> { ... public int compareTo(Bruch b) { //Implementation, folgt gleich } } Das Interface Comparable<Bruch> besagt, dass ein Objekt dieser Klasse mit einem Bruch vergleichbar ist. Damit sind zwei Br uche miteinander vergleichbar. Man k onnte als Generic bei Comparable auch andere Klassen als Bruch angeben, zum Beispiel: public class Bruch implements Comparable<Integer> Sinnvoll ist das aber erst, wenn man beidseitig vergleichen kann, also die IntegerKlasse auch das Interface Comparable<Bruch> implementiert, was diese Klasse nat urlich nicht tut. Normalerweise vergleicht man nur zwei Objekte derselben Klasse.
Da nur Teile des Sortieralgorithmus ausgetauscht werden, benutzt Java streng genommen das verwandte Entwurfsmuster Template Method (Schablonenmuster). F ur unsere Belange k onnen wir diesen feinen Unterschied vernachl assigen.
6
126
KAPITEL 6. INTERFACES
Die Methode, die vom Interface verlangt wird, ist compareTo. Die Methode muss so implementiert werden, dass das Ergebnis die folgende Bedingung erf ullt: (falls a > b) 0 (falls a = b) a.compareTo(b) = Wert kleiner 0 (falls a < b)
oer 0 Wert gr
Unsere einfache Implementation ist, beide Br uche in doubles zu wandeln, die Dierenz davon zu berechnen und die Signum-Funktion davon zur uckzugeben.7 Das ergibt: public int compareTo(Bruch b) { double x = this.getDoubleValue()-b.getDoubleValue(); return (int) Math.signum(x); } Sobald Bruch das Interface implementiert, kann ein Feld Bruch[] mit der statischen Methode Arrays.sort(feld); und eine ArrayList<Bruch> mit der statischen Methode Collections.sort(liste); automatisch sortiert werden.8
9
Zweites Beispiel: Rechtecke Nachfolgend ein Beispiel f ur das Interface Comparable in einer Klasse Rechteck. compareTo vergleicht zwei Rechtecke nach ihrem Fl acheninhalt. import java.util.*; public class Rechteck implements Comparable<Rechteck> { private double hoehe, breite; public Rechteck (double hoehe, double breite) { this.hoehe = hoehe; this.breite = breite; } public int compareTo(Rechteck r) { //Vergleicht die Flaeche zweier Rechtecke
Die Signum-Funktion sgn(x) ergibt -1, falls x < 0, 0, falls x = 0 und 1, falls x > 0. Die Klassen Arrays und Collections aus java.util enthalten auch noch andere Hilfsfunktionen f ur Felder. 9 Das Sortierkriterium ist hier fest an die Klasse gebunden. Soll das Sortierkriterium von Zeit zu Zeit wechseln, ist es besser, nicht das Interface Comparable zu implementieren, sondern einen Comparator zu benutzen.
8 7
6.2. HAUFIGER GEBRAUCH VON INTERFACES //this > r --> Rueckgabewert 1 //this < r --> Rueckgabewert -1 //Gleiche Flaeche --> Rueckgabewert 0 //Ungleiche Rechtecke koennen gleiche Flaechen haben double dieseFlaeche = hoehe*breite; double andereFlaeche = r.hoehe * r.breite; if (dieseFlaeche > andereFlaeche) { return 1; } if (dieseFlaeche < andereFlaeche) { return -1; } return 0; } Weitere Interfaces aus der Java-API
127
Comparable: Ein Objekt ist mit einem anderen Objekt (der gleichen oder einer anderen Klasse) vergleichbar. Die Methode compareTo(Object o) muss implementiert sein. Diese Objekte k onnen dann automatisch sortiert werden. Cloneable: Ein Objekt kann mit dem Befehl Object clone() geklont (bzw. kopiert) werden. Serializable: Ein Objekt kann mit einem einzigen Befehl (ObjectStream.writeObject()) auf der Festplatte gespeichert bzw. von dort gelesen werden. Andere Beispiele: Adjustable, Appendable, Callable, Closeable, Destroyable, Flushable, Formattable, Iterable, Joinable, Pageable, Printable, Readable, Referencable, Refreshable, Runnable, Scrollable, Streamable, Transferable, ...
6.2.4
Aufgabenstellung Nehmen wir an, wir wollen einen Funktionsplotter entwerfen, der beliebige Funktionen plotten kann. Beginnen wir zun achst mit einem Programm, das die Sinusfunktion zwischen -10 und 10 plottet. Die Kernroutine ist dabei public void draw() { for (double i=-10; i<10; i+=0.001) { plot(i, Math.sin(i)); } }
128
KAPITEL 6. INTERFACES
plot rechnet die x-y-Koordinaten in Pixelkoordinaten um und gibt den Punkt auf dem Bildschirm aus. Wenn eine andere Funktion geplottet werden soll, muss die draw-Methode ge andert werden. Das m ochte man nicht. N utzlich w are es, der draw-Methode die zu plottende Funktion als Argument zu u bergeben, etwa wie draw(Math.sin());. Die hierzu u bliche Vorgehensweise in Java beinhaltet ein Interface und eine anonyme innere Klasse. Wir werden uns bis zum Ende von Kapitel 7 Schritt f ur Schritt an die optimale L osung herantasten. Die Konstrukte, die wir brauchen, sehen zwar ungew ohnlich aus, sind aber im Zusammenhang mit Funktionszeigern in Java absolut u blich. Funktionszeiger Aus Kapitel 3 kennen wir schon Zeiger auf Objekte. Nehmen wir als Beispiel die Zeile String x = "Hallo"; Die Variable x enth alt eine Zahl mit der Speicheradresse, ab der das Objekt "Hallo" gespeichert ist. x ist ein Zeiger (oder eine Referenz) auf den String Hallo. x: Adresse 8235748 8235752 Hallo Auch Funktionen und Methoden stehen im Speicher. Es spricht nichts gegen eine Variable, die auf die Anfangsadresse einer Funktion zeigt. Den Typ einer solchen Variablen nennt man Funktionszeiger. x: Adresse 1763578 1763582
public void draw() { for(double i ...
Adresse: 8235752
Adresse: 1763582
In C-Programmen werden Funktionszeiger h aug verwendet. Objektorienterte Programmiersprachen sollten urspr unglich Funktionszeiger u ussig machen. ber Daher fehlen Funktionszeiger auch in Java. Es zeigte sich aber schnell, dass das
129
Ersatzkonstrukt zwar auch recht kurz ist, aber doch deutlich weniger elegant. Daher haben viele objektorientierte Sprachen Funktionszeiger- ahnliche Konstrukte wieder eingef uhrt, z.B. die Delegates in C# oder die Closures in vielen Skriptsprachen. Auch in Java 7 sollen Closures eingef uhrt werden. Bis dahin m ussen wir aber mit dem objektorientierten Ersatzkonstrukt auskommen. Implementierung in Java Das Ersatzkonstrukt f ur Funktionszeiger l auft auf das Entwurfsmuster Strategie hinaus. Nehmen wir wieder als Beispiel unsere Methode draw. Wir brauchen dazu einen Aufsatz, der die zu plottende Funktion enth alt. Grasch sieht das so aus:
Maschine:
Plotter.draw(..)
Kupplung:
Interface PlotFunktion
Aufs atze:
z.B. Klasse Sinus
Das Interface nennen wir PlotFunktion. public interface PlotFunktion { double getY(double x); } Die Aufs atze implementieren das Interface und berechnen eine bestimmte Funktion, wie hier zum Beispiel f (x) =sin(x) oder f (x) = x2 : public class Sinus implements PlotFunktion { public double getY(double x) { return Math.sin(x); } }
public class Quadrat implements PlotFunktion { public double getY(double x) { return x*x; } }
130
KAPITEL 6. INTERFACES
Die Maschine ist die draw-Funktion. Sie erh alt einen Ubergabeparameter vom Typ des Interfaces und nutzt die darin vorhandene Methode. public static void draw(PlotFunktion f) { for (double i=-10; i<10; i+=0.001) { plot(i, f.getY(i)); } } Der Aufruf der draw-Methode ist: PlotFunktion f = new Sinus(); draw(f); //oder kurz draw(new Sinus()); Diese L osung modelliert einen Funktionszeiger. Sie ist noch nicht optimal. Wir brauchen mindestens 2 Klassen und ein Interface und der Code ist sehr auseinandergerissen. Allerdings k onnen wir das in Java noch deutlich verbessern. Da wir dazu jedoch anonyme innere Klassen ben otigen, m ussen wir das auf Kapitel 7 verschieben.
Kapitel 7
Vererbung
7.1
7.1.1
Grundlagen
Beispielhafte Problemstellung
Neue Rolle: Der Wiederverwender Nehmen wir an, sie haben den Auftrag, ein mathematisches Paket zu entwickeln. Da sie nicht die Zeit haben, alles von Grund auf neu zu programmieren, versuchen sie, m oglichst viele Klassen aus vorherigen Projekten wiederzuverwenden. Damit nehmen sie eine neue Rolle ein: die des Wiederverwenders. Also sehen sie sich auch die Klasse Bruch aus Kapitel 3 an (die Erweiterungen aus Kapitel 6 ignorieren wir der Einfachheit halber hier). Die k onnten sie gut gebrauchen, wenn da nicht ein paar typische Wiederverwendungs-Probleme auftreten w urden: Die Klasse Bruch br auchte unbedingt noch zwei Methoden getInt und getFrac, um den ganzzahligen Anteil des Bruchs und den Rest auszugeben. Beispiel: Der ganzzahlige Anteil von
5 3 2 ist 1, der Rest ist 3 .
Sie haben nicht die Zeit oder die M oglichkeit, den Source-Code der Klasse zu a ndern. Der Entwickler der Klasse kann ihnen dabei auch nicht helfen. Er muss sich voll auf ein anderes Projekt konzentrieren (freundlichere, aber unrealistische Alternative: Er macht gerade Urlaub in der Karibik). Sie brauchen also eine M oglichkeit, die Klasse zu erweitern, ohne den SourceCode der Klasse andern zu m ussen. Genau das ist eine der Hauptst arken der objektorientierten Programmierung. Sie k onnen Klassen erweitern, obwohl sie nur die API, aber nicht den Source-Code kennen. Die Klasse XBruch Wir nennen die erweiterte Klasse XBruch f ur erweiterten (extended) Bruch. Dieser Klasse f ugen wir die Methoden getInt() und getFrac(): 131
132 public class XBruch extends Bruch { public int getInt() { return getZaehler()/getNenner(); } public int getFrac( { return getZaehler()%getNenner(); }
KAPITEL 7. VERERBUNG
Neu ist in der ersten Zeile das Schl usselwort extends. Es bedeutet soviel wie erweitert. Die Klasse XBruch erweitert die Klasse Bruch. Ein Objekt der Klasse XBruch besitzt neben den eigenen Attributen und Methoden auch die Attribute und Methoden aus Bruch. Sie k onnen angesprochen werden, sofern sie nicht private sind. Ehe wir die Konsequenzen daraus betrachten, gehen wir der Klarheit wegen einige Begrie aus der objektorientierten Programmierung durch.
7.1.2
Terminologie
Die Klasse Bruch heit die Basisklasse (Oberklasse, Superklasse). In den Basisklassen stehen allgemeine Eigenschaften. Die Klasse XBruch ist die Unterklasse (Subklasse, abgeleitete Klasse) von Bruch. In den Unterklassen werden diese Eigenschaften ausgebaut und spezialisiert. Die Unterklasse wird von der Basisklasse abgeleitet. Die Unterklasse erbt die Eigenschaften (d.h. Methoden und Attribute) der Basisklasse. Die Beziehung zwischen Unter- und Basisklasse nennt man ist-Beziehung . Beispiel: Ein XBruch ist ein Bruch.
7.1.3
Konstruktoren
War es das jetzt schon? Wir testen unsere erweiterte Klasse einmal aus: XBruch v = new XBruch(2,3); System.out.println(v.getDoubleValue()); System.out.println(v.getInt());
Im Grunde sieht das schon ganz gut aus. Es fehlt aber noch eine Kleinigkeit. Wir k onnen zwar die Methoden aus Bruch benutzen, aber nicht die Konstruktoren. Diese werden n amlich prinzipiell nicht vererbt. Wir m ussen der Klasse XBruch noch selbst einen Konstruktor hinzuf ugen. Unser erster Versuch ist:
133
Mal ganz abgesehen davon, dass die Klasse XBruch Probleme beim K urzen bek ame: Der Java-Compiler akzeptiert diesen Code nicht. Der Grund ist folgender: Jeder Konstruktor einer Unterklasse ruft automatisch als erstes den Konstruktor seiner Basisklasse auf. Normalerweise ist dies der parameterlose Konstruktor der Basisklasse. In der Klasse Bruch gibt es aber keinen parameterlosen Konstruktor. Also meldet uns Java einen Fehler. Die L osung dazu ist, dass wir Java auordern k onnen, einen anderen Konstruktor als den parameterlosen zu verwenden. Dazu gibt es das Schl usselwort super: public XBruch(int zaehler, int nenner) { super(zaehler, nenner); } Der super-Befehl muss im Konstruktor an erster Stelle stehen. Jetzt wird der Basisklassen-Konstruktor mit zwei int-Parametern anstelle des parameterlosen Konstruktors ausgerufen. Als Parameter u ahler und den bergeben wir den Z Nenner des Bruchs. Damit werden jetzt auch die Attribute richtig gesetzt. Auch der oben gezeigte Testcode funktioniert jetzt.
7.1.4
Lesbarkeit
Die Unterklasse kann auf alle Attribute und Methoden der Oberklasse zugreifen, sofern sie nicht private sind, also z.B. auf alle public-Methoden. Zur besseren Dierenzierung gibt es einen weiteren Zugris-Modizierer: protected. Auf eine protected-Methode oder ein protected-Attribut d urfen zugreifen: Objekte der eigenen Klasse. Objekte einer Unterklasse. Als Besonderheit in Java (im Gegensatz zu C#) Objekte der Klassen des eigenen Packages. Packages dienen zur Abgrenzung gr oerer Programmteile. Sie werden allgemein in einem sp ateren Kapitel eingef uhrt. Bisher arbeiten wir nur im sogenannten Default-Package. Wenn wir als Entwickler der Klasse Bruch dem Wiederverwender die M oglichkeit geben wollen, auf die Attribute zuzugreifen, dem Anwender das aber verbieten wollen, k onnen wir die beiden Attribute auf protected setzen: public class Bruch { protected int zaehler; protected int nenner; }
134
KAPITEL 7. VERERBUNG
Nat urlich k onnte sich der Anwender als Wiederverwender tarnen, indem er die Klasse Bruch u berschreibt, und dann munter auf die Attribute zugreifen. Deshalb muss man sich als Entwickler gut u oglichkeit berlegen, ob man eine solche M zul asst.
7.1.5
UML-Diagramm
Im UML-Diagramm wird die Verebungsbeziehung durch einen Pfeil ausgedr uckt, der von der Unterklasse zur Basisklasse zeigt. Er bedeutet: Attribute und Methoden (sofern nicht private) der Basisklasse k onnen mitbenutzt werden. Der Zugangsmodizierer protected wird durch ein Doppelkreuz # statt durch ein + (public) oder ein - (private) kenntlich gemacht. Bruch
- zaehler: int - nenner: int
XBruch
7.1.6
Eine Java-Klasse kann nur eine Basisklasse haben (Einfachvererbung). In manchen Sprachen, wie C++ oder Python gibt es auch Mehrfachvererbung, d.h. eine Klasse kann auch mehrere Basisklassen haben. In Java oder C# ist das nicht m oglich. Hier besitzt die Klassenhierarchie eine Baumstruktur. Es gibt eine Wurzel--Klasse, die in Java Object heit. Von dieser Wurzel-Klasse werden verschiedene Unterklassen abgeleitet, die wiederum Unterklassen und UnterUnterklassen besitzen, was sich bis in beliebige Tiefe fortsetzen kann. Jede Klasse hat dabei (bis auf die Wurzelklasse) genau eine (direkte) Basisklasse.
7.1.7
Methoden u berschreiben
Wir wollen eine neue Anforderung an unsere XBruch-Klasse stellen. Die toStringMethode soll einen String zur uckgeben, der die Form Ganzzahl + Rest hat. 7 Beispiel: Der Bruch hat den Wert 3 . Dann soll die Ausgabe 2+1/3 sein. Bei 7 3 soll die Ausgabe -2-1/3 sein. Dabei k onnten wir gut das Interface aus Kapitel 6 gebrauchen. Es steht uns aber hier nicht zur Verf ugung. Uns steht als Wiederverwerter aber noch ein anderer Weg oen. In Java ist es m oglich, eine Methode der Basisklasse durch eine Methode der Unterklasse zu ersetzen. Die genaue Bedeutung dieses Begris lernen wir sp ater noch kennen. Der Fachausdruck daf ur heit u berschreiben. Dazu implementieren wir die Methode in der Unterklasse einfach noch einmal.
7.2. BEISPIELE AUS DER API public class XBruch extends Bruch { public String toString() { Bruch frac = getFrac();
135
7.1.8
Wenn eine Methode u berschrieben ist, bedeutet das, dass es ohne weiteres nicht m oglich ist, an die Methode der Basisklasse heranzukommen. Um dies dennoch zu erreichen, gibt es in den meisten objektoriertierten Sprachen ein spezielles Schl usselwort. In Java heit es super. Man kann von der Unterklasse aus mit dem Aufruf super.methodenname(. . . ) auf Methoden der Basisklasse zur uckgreifen, auch wenn diese u berschrieben sind. Als einfaches Beispiel u berschreiben wir die Klasse Bruch mit einer toString-Methode, die vor und hinter den 5 Bruch ein X setzt, also z.B. statt 5 uckgibt. 3 den String X 3 X zur public class XBruch extends Bruch { public String toString() { return "X"+super.toString()+"X"; } }
7.2
7.2.1
Die Klasse Object ist die Wurzelklasse, von der alle anderen Klassen erben. Mit anderen Worten: Auch wenn in einer Klasse keinerlei Methoden und Attribute implementiert werden, bekommt man automatisch von der Klasse Object einige mitgeliefert. Eine Auswahl davon sind: public Class getClass(): Liefert ein Class-Objekt, das die Klasse des Objekts beschreibt. Damit sind zum Beispiel die Methoden getName() oder getMethods() m oglich.
136
KAPITEL 7. VERERBUNG
public void finalize(): Wird automatisch aufgerufen, wenn das Objekt vom Garbage-Collector gel oscht wird. Die Methode in Object ist leer. Damit etwas passieren soll, muss sie u berschrieben werden. public int hashCode(): Liefert eine eindeutige ID f ur jedes Objekt. Die Beschreibung in der Java-API lautet: As much as is reasonably practical, the hashCode method dened by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.) public String toString(): Liefert eine String-Darstellung des Objekts. Wird automatisch aufgerufen, falls das Objekt mit System.out.println() ausgegeben wird. Da diese Methode sehr lehrreich ist, werden wir im n achsten Kapitel ausf uhrlicher auf sie eingehen. Beispiel: public class Minimal {} public class Test { public static void main(String[] args) { Minimal m = new Minimal(); System.out.println(m.getClass().getName()); System.out.println(m.hashCode()); System.out.println(m.toString()); } }
7.2.2
Die toString()-Methode
Wenn ein Objekt mit System.out.println() ausgegeben wird, dann wandelt Java das Objekt zun achst mit toString() in eine Stringdarstellung um. Weil toString() bereits in der Klasse Object vorhanden ist, hat jedes Objekt eine toString()-Methode. Diese Methode dient als Default und sieht in der SunJava Klassenbibliothek so aus: public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } So ergibt die Zeile System.out.println(System.out); dieses Ergebnis (die Zahl am Ende kann von Durchlauf zu Durchlauf variieren) java.io.PrintStream@10b62c9
7.3. BINDUNGSARTEN
137
Wenn wir die toString()-Methode einer Klasse implementieren, dann u berschreiben wir die toString()-Methode von Object und es wird statt dessen unsere toString()-Methode aufgerufen. In manchen Klassen u berschreibt Java die toString()-Methode auch selbst. Darum werden z.B. bei Exceptions, bei StringBuilder- oder bei File-Objekten andere, lesbarere Darstellungen ausgegeben.
7.3
7.3.1
Bindungsarten
Statischer und dynamischer Typ
In Java ist die folgende Variablendeklaration m oglich: Bruch b = new XBruch(1,2); Der Variablen vom Typ Bruch wird ein Objekt vom Typ XBruch zugewiesen. Dies ist m oglich, weil XBruch eine Unterklasse von Bruch ist. Bevor wir u ussen wir uns etwas ber die Konsequenzen daraus nachdenken, m mit der korrekten Terminologie besch aftigen. In unserem Beispiel nennt man Bruch den statischen Typ der Variablen b. Dieser Typ ist, nachdem er einmal festgelegt wurde, nicht wieder ver anderbar. Der Typ des Objekts, also XBruch heit dynamischer Typ oder Laufzeittyp von b. Dieser Typ kann sich w ahrend des Programms a ndern. Zum Beispiel kann man mit der Zeile b = new Bruch(1,5); den dynamischen Typ von b in Bruch a ndern. Der Name Laufzeittyp kommt daher, dass zur Compile-Zeit dieser Typ nicht bekannt ist. Er k onnte es theoretisch sein, aber dazu m usste der Compiler das gesamte Programm analysieren, was vom Aufwand her nicht zu schaen ist. Dagegen ist der statische Typ dem Compiler bekannt. Daher merkt er, wenn man zum Beispiel mit b = b.getSquare(); eine Methode aufrufen will, die gar nicht exisitiert. Uberpr u fung des dynamischen Typs Es gibt mehrere M oglichkeiten, den dynamischen Typ zu u ufen. Der Ausberpr druck br instanceof XBruch gibt true zur uck, falls der dynamische Typ von br die Klasse XBruch ist und anderenfalls false. Der Ausdruck br instanceof Bruch
138
KAPITEL 7. VERERBUNG
gibt true zur uck, falls der dynamische Typ von br Bruch oder XBruch ist. Der Operator instanceof pr uft ob die angegebene Variable vom Typ der angegebenen Klasse oder einer ihrer Unterklassen ist. Eingesetzt wird der Operator z.B. wie folgt: if (br instanceof XBruch) { //Sonderbehandlung fuer XBrueche } Eine andere Methode haben wir schon gesehen. Der Aufruf: getClass().getName() liefert f ur jedes Objekt eine String-Darstellung des dynamischen Typs. Zum Beispiel ist die Variable System.in vom statischen Typ InputStream (was man mit Eclipse leicht feststellen kann). Die String-Darstellung liefert aber den dynamischen Typ BueredInputStream, der von InputStream abgeleitet ist (je nach Java-Version kann auch ein anderer Typ zur uckgeliefert werden).
7.3.2
Die Frage, die sich uns jetzt stellt, ist: Was passiert bei den Aufrufen Bruch k = new XBruch(5,3); //Statischer Typ: Bruch //Dynamischer Typ: XBruch int x = k.getInt(); String s = k.toString(); F ur die Zeile k.getInt() gibt es prinzipiell zwei m ogliche Antworten: 1. Es k onnte die Methode der statischen Klasse (Bruch) ausgef uhrt werden. Dann g abe es einen Fehler, denn diese Methode existiert gar nicht. 2. Es k onnte aber auch die Methode der dynamischen Klasse (XBruch) ausgef uhrt werden. Dann w urde x den Wert 1 erhalten. Ahnlich verh alt es sich mit der zweiten Zeile k.toString(): 1. Es k onnte die Methode der statischen Klasse (Bruch) ausgef uhrt werden. Dann w urde s der String 5/3 zugewiesen. 2. Es k onnte aber auch die Methode der dynamischen Klasse (XBruch) ausgef uhrt werden. Dann w urde s der String 1+2/3 zugewiesen. Die Antwort darauf ist zum Verst andnis der Objektorientierung wichtig. Sie heit: In Java bestimmt die statische Klasse, welche Methoden aufgerufen werden k onnen und die dynamische Klasse, wie diese Methoden implementiert sind. Anders ausgedr uckt:
7.3. BINDUNGSARTEN
139
Ist die Methode nur in der dynamischen Klasse vorhanden, aber nicht in der statischen Klasse, gibt es einen Compiler-Fehler (bzw. Eclipse meldet einen Fehler). Ansonsten wird immer die Methode der dynamischen Klasse aufgerufen. Bei der Zeile k.getInt() ist also Antwort 1 richtig, bei der Zeile k.toString() Antwort 2. Wir werden die Grundlagen dazu im n achsten Abschnitt n aher betrachten.
7.3.3
Bindungsarten
Wenn beim Abarbeiten eines Java-Programms das Java-Laufzeit-System auf einen Methodenaufruf tri, geht es wie folgt vor: 1. Es sieht nach, welchen dynamischen Typ die Variable hat und 2. ruft die Methode des dynamischen Typs auf. Der Fachausdruck daf ur heit dynamische Bindung (sp ate Bindung, Laufzeitbindung). Das Gegenst uck dazu heit statische Bindung (fr uhe Bindung), bei der die Methode des statischen Typs ausgerufen wird. Dieser Typ und die dazugeh orige Methoden werden bereits vom Compiler ermittelt. Da so der Typ w ahrend der Laufzeit schon bekannt ist, geht dieser Methodenaufruf schneller. statische Bindung Ermittelt Methode des statischen Typs. Ruft ermittelte Methode auf. dynamische Bindung Ermittelt dynamischen Typ und ruft die Methode des dynamischen Typs auf. niedrig
Compiler Laufzeitsystem
Schnelligkeit
hoch
7.3.4
Wenn die statische Bindung nun schneller ist, warum bindet Java dann Methoden im Regelfall dynamisch? Nehmen wir dazu noch einmal unsere BruchKlasse: Bruch br = new XBruch(1,2); Durch die dynamische Bindung verh alt sich das XBruch-Objekt stets und u berall wie ein XBruch-Objekt. Das heit, die toString-Methode liefert immer den gemischten Bruch als Ergebnis. Wenn ein Objekt sein Verhalten andern w urde, sobald sich der statische Typ der Variablen andert, w urde das Verhalten weitaus komplizierter werden. Das Einzige, was sich bei der Zuweisung zum Typ Bruch andert, ist, dass der Blickwinkel auf das Objekt eingeschr ankt wird. Man sieht n amlich nur noch die Methoden, die in Bruch deniert sind.
getFrac()
Typumwandlungen Nach der Zeile Bruch br = new XBruch() wissen wir, dass die Variable br den dynamischen Typ XBruch hat. Wenn man das Objekt wieder einer Variable vom Typ XBruch zuweisen will, ben otigt man einen expliziten Cast. Das heit: XBruch xb = new XBruch(1,2); Bruch b = new Bruch(1,2); b = xb; xb = (XBruch) b; //XBruch -> Bruch: Geht ohne expliziten Cast //Bruch -> XBruch: Braucht expliziten Cast
Der Unterschied zwischen beiden F allen ist: Ein XBruch ist immer auch ein Bruch, aber ein Bruch ist noch lange nicht automatisch ein XBruch. Der Compiler kann nicht von selbst ermitteln, welchen dynamischen Typ b hat. Es w are theoretisch m oglich, aber die M oglichkeiten des Compilers reichen dazu nicht aus. Daher verlangt der Compiler einen expliziten Cast. Was passiert nun in folgendem Fall? Bruch br = new Bruch(); XBruch xbr = (XBruch) br; Es wird versucht, br in eine Variable vom statischen Typ XBruch zu casten, obwohl br gar kein XBruch ist. Das Ergebnis ist eine ClassCastException.
7.3.5
Als N achstes wollen wir Objekte der Klassen Bruch und XBruch gemeinsam in einer ArrayList speichern. ArrayList<Bruch> reihe; F ur Objekte vom Typ Bruch geht das wie gewohnt. Objekte vom statischen Typ XBruch k onnen ebenfalls problemlos eingef ugt werden:
141
Holt man Elemente aus der Liste heraus, so haben die Elemente den statischen Typ Bruch. Handelt es sich um einen XBruch, kann man ihn mit einem expliziten Cast umwandeln. Bruch s = reihe.get(0);2 XBruch xs = (XBruch) s; Statt einer Liste kann man auch ein Feld Bruch[] feld; verwenden. Das Prinzip bleibt das gleiche. Felder und Listen vom Typ Object Noch exibler sind Listen vom Typ Object: ArrayList<Object> liste1 = new ArrayList<Object>(); ArrayList liste2 = new ArrayList(); Die zweite Form ist dabei eine Kurzform f ur die erste. In solchen Listen kann man alle Java-Objekte speichern, unabh angig davon, welche Klasse sie haben. Beim Zugri auf die Elemente muss man sich allerdings im Klaren dar uber sein, auf welches Objekt man zugreift. liste1.add("Hallo"); liste1.add(new Bruch(1,2)); Object ob1 Object ob2 String s = Bruch st = = liste1.get(0); = liste1.get(1); (String)ob1; (Bruch)ob2;
7.3.6
Wenn wir uns etwas von der Programmiersprache Java l osen, kann man prinzipiell f ur jede Methode (und auch f ur jedes Attribut) frei zwischen statischer und dynamischer Bindung w ahlen. Allerdings: Vom Uberschreiben einer Methode spricht man nur, wenn die Methode dynamisch gebunden ist. Wenn die Methode statisch gebunden ist, wird sie von einer Unterklasse nicht u berschrieben, sondern verdeckt (geht in Java nicht).
142 Zur Ubersicht: Methode u berschreiben dynamisch Methode des dynamischen Typs
KAPITEL 7. VERERBUNG
Bindungsart Aufruf
F ur Attribute gilt generell das Gleiche wie f ur Methoden. Attribute k onnen dynamisch oder statisch gebunden sein. Je nach Bindung k onnen Attribute u bzw. Verdecken ist f ur berschrieben oder verdeckt werden. Das Uberschreiben Attribute aber nicht so wichtig wie bei Methoden.
7.3.7
Java
Java bindet Methoden generell dynamisch. Methoden werden u berschrieben und bei der Ausf uhrung wird die Methode des dynamischen Typs ausgew ahlt. Mit dem Schl usselwort nal kann man Java anweisen, eine Methode statisch zu binden: public final double getName() {...} Eine solche Methode kann aber in Java nicht verdeckt (und nat urlich auch nicht u usselwort berschrieben) werden. Dieser Zusatzeekt gibt dem Schl nal (endg ultig) seinen Namen. Die Vorteile von nal sind also: Die Funktionalit at der Methode kann durch Uberschreiben oder Verdecken nicht ver andert werden. Durch die statische Bindung wird der Methodenaufruf schneller. Attribute werden in Java generell statisch gebunden. Sie k onnen verdeckt werden und bei der Ausf uhrung wird das Attribut der statischen Klasse verwendet. Die Kombination von u berschriebenen Methoden und ver deckten Variablen kann dazu f uhren, dass man nur schwer die Ubersicht beh alt, welche Variablen denn nun tats achlich verwendet werden. Da es kaum n otig ist, Attribute zu verdecken, rate ich stark, folgende Regel einzuhalten: Finger weg vom Verdecken von Attributen. Statische Methoden werden ebenfalls statisch gebunden und verdeckt. Das Problem ist aber recht gering, solange man statische Methoden auch statisch aufruft (d.h. mit Klassenname.Methodenname(..)).
143
Gerade hier unterscheidet sich das Konzept von Sprache zu Sprache stark, so dass eine Kenntnis der Grundlagen sehr wichtig ist. C++ und C# stellen eine Gruppe dar, die sich teilweise entgegengesetzt zu Java verh alt. In Skriptsprachen wie Python gibt es keinen statischen Typ und damit auch keine Unterschiede zwischen u berschreiben und verdecken. In Python wird oft der Ausdruck ersetzen (redene, replace) statt u berschreiben (override) oder verdecken (hide) benutzt.
7.4
Beim Entwurf von Klassen gibt es oft Probleme, eine baumf ormige Klassenhierarchie aufzubauen. Manche Klassenbeziehungen lassen sich nicht mit baumf ormigen Hierarchien darstellen. Als Beispiel betrachten wir Kompressionsverfahren und Verschl usselungsverfahren. Es gibt Verfahren, die sowohl verschl usseln, als auch komprimieren. Andere Verfahren verschl usseln nur oder komprimieren nur (sind also einfach zu entschl usseln). Die naheliegende Vererbungshierarchie ist:
Verschl usselungsverfahren
Kompressionsverfahren
Verfahren1
Verfahren2
Verfahren3
In Java kann dies nicht mit Basisklassen, wohl aber mit Interfaces umgesetzt werden. Die beiden Interfaces Krypto und Kompress haben dann das Aussehen: public interface Krypto { String verschluessele(String klartext); String entschluessele(String packtext); } public interface Kompress { String komprimiere(String klartext); String entpacke(String packtext); }
Die einzelnen Verfahren implementieren dann, je nach Funktionalit at, die Interfaces.
144
KAPITEL 7. VERERBUNG
public class Verfahren1 implements Krypto { ... } public class Verfahren2 implements Krypto, Kompress { ... } public class Verfahren3 implements Kompress { ... }
7.5
Wiederholung: Funktionsplotter Im Kapitel u ber Interfaces hatten wir einen einfachen Funktionsplotter entworfen, der f ur eine w ahlbare Funktion die Funktioswerte zwischen -10 und 10 auf dem Bildschirm ausgegeben hat. Der Code dazu war auf mindestens drei Dateien verteilt und f ur jede weitere Funktion kam eine Datei hinzu. Der Code war kurz wie folgt: Es gab ein Interface PlotFunktion: public interface PlotFunktion { double getY(double x); } F ur jede konkrete Plot-Funktion gab es eine Klasse, die das Interface implementiert: public class Sinus implements PlotFunktion { public double getY(double x) { return Math.sin(x); } } Dann gab es noch eine Hauptklasse mit einer draw-Methode. public void draw(PlotFunktion f) { for (double i=-10; i<10; i+=0.001) { plot(i, f.getY(i)); } } Das wollen wir jetzt deutlich kompakter schreiben.
145
Wir schreiben daf ur Funktionszeiger als sogenannte anonyme innere Klassen. Es ist m oglich, eine Objekt-Variable zu erzeugen, deren dynamischer Typ namenlos ist (dabei aber nat urlich vom statischen Typ abgeleitet). Daher sind die Klassen anonym. Innere Klassen sind es, weil sie innerhalb einer anderen Klasse deniert werden.1 Dazu gibt es eine spezielle Kurzsyntax. Das folgenden Beispiel belegt die Variable f mit einer Plot-Funktion y = x2 in der Form einer anonymen inneren Klasse. PlotFunktion f = new PlotFunktion() { public double getY(double x) { return x*x; } }; draw(f); Am Ende der ersten Zeile steht statt eines Semikolons eine onende geschweifte Klammer. Das heit, dass f nicht mit einem Objekt der Klasse PlotFunktion belegt wird (das ginge auch gar nicht, da PlotFunktion ein Interface ist), sondern mit einer namenlosen Unterklasse von PlotFunktion. Zwischen der geschweiften Klammer auf und der zugeh origen Klammer zu benden sich die neuen Methoden der Unterklasse. Es gibt eine Besonderheit bei anonymen inneren Klassen: Dies wird im folgenden Beispiel deutlich. Wir ben otigen dazu wieder ein Interface und eine auere Klasse, in der eine anonyme innere Klasse erzeugt wird: public interface TestInterface { public void start(); } public class Aussen { TestInterface innen; private void aussenMethode() { System.out.println("Testmethode"); } public void makeAnonym() { //Erzeugen der inneren Klasse innen = new TestInterface() { public void start() { //Kann auf Attribute und Methoden der
Alle anonymen Klassen sind in Java innere anonyme Klassen. Andererseits gibt es in Java drei weitere Arten von inneren Klassen, von denen wir in dieser Vorlesung nur die statische innere Klasse kennenlernen werden.
1
146
} } Die Besonderheit ist, dass aus der start-Methode der inneren Klasse auch auf die Attribute und Methoden des umgebenden aueren Objekts zugegriffen werden kann. Die Klasse Aussen erzeugt in der Methode makeAnonym den Funktionszeiger innen als anonyme innere Klasse. Von dort wird die Methode aussenMethode der aueren Klasse aufgerufen. this.aussenMethode ist statt dessen nicht zul assig, da this auf das Objekt innen verweist. Java sucht zun achst in der inneren Klasse nach der Methode aussenMethode und, wenn es sie dort nicht gibt, anschlieend in der a ueren. Abschlieend kann man sagen: Funktionszeiger als anonyme innere Klassen in Java, als Delegates in C# oder als Closures in Skriptsprachen sehen gar nicht so unterschiedlich aus. Hat man einmal das Prinzip verstanden, l asst sich der Code schnell von Java in andere Sprachen oder zur uck transformieren. Das ist wahrscheinlich auch der Grund, warum in Java bis heute keine Closures eingef uhrt wurden. Am Anfang sieht die Syntax in anderen Sprachen einfacher aus. Nach einer Zeit hat man sich in Java aber so an die anonymen inneren Klassen gew ohnt, dass man nicht l anger dar uber nachdenkt.2
7.6
Abstrakte Klassen
Hier kommen wir noch einmal auf unsere Klasse Bruch zur uck. In den letzten beiden Kapiteln haben wir zwei M oglichkeiten gelernt, die String-Ausgabe eines Bruches variabel zu gestalten: 1. Mit dem Entwurfsmuster Strategie und einem Interface. 2. Mit einer Unterklasse von Bruch. Die zweite M oglichkeit war bisher eher ein Nebeneekt der Objektorientierung und in erster Linie f ur den Wiederverwender gedacht. Der Entwickler der Klasse kann sie aber auch sehr bewusst f ur den Anwender konzipieren und damit wesentlich n aher an das Entwurfsmuster Strategie r ucken. Er k onnte sich zum Beispiel folgendes u berlegen: Die toString-Methode der Klasse Bruch ist eine Default-Methode und gleichzeitig ein sogenannter Hook, also ein Haken, an dem sich der Anwender mit seiner eigenen Methode einh angen
2 Auf den zweiten Blick gibt es doch Unterschiede zwischen anonymen inneren Klassen in Java, anonymen Delegates in C# und Closures. Diese betreen die Variablenkapselung und werden nicht weiter behandelt. Dar uber hinaus besitzen Delegates in C# Methoden f ur das Entwurfsmuster Beobachter (Observer) und das Thread-Pool-basierte Fork-Join-Pattern, ein parelleles Entwurfsmuster.
147
kann, indem er eine Unterklasse bildet. Der Schritt liegt nahe, die DefaultMethode wegzulassen und den Anwender zu zwingen, eine Unterklasse zu bildent. Damit r uckt man noch ein St uck n aher an das Entwurfsmuster Strategie heran. Folgende Anderungen sind n otig: Man entfernt die Implementation der toString-Methode und ersetzt sie durch eine Methodendenition ahnlich wie in einem Interface. public abstract class Bruch { public abstract String toString(); //...weitere Methoden } Neu ist das Schl usselwort abstract. Es taucht zweimal auf. Bei der Klassendenition bedeutet es, dass es nicht erlaubt ist, ein Objekt dieser Klasse zu erzeugen. Wohl aber kann man die Klasse u berschreiben, muss dann aber die als abstrakt deklarierten Methoden (in unserem Beispiel toString) implementieren.3 Das heit, wir k onnen den Aufsatz in einer Unterklasse hinzuf ugen: public class DoubleAusgabeBruch extends Bruch { public String toString() { return ""+getDoubleWert(); } } Der Unterschied zum Entwurfsmuster Strategie ist allerdings: Der Aufsatz l asst sich nachtr aglich nicht mehr austauschen. Er bildet mit der Maschine eine Einheit, die der Anwender nicht mehr zertrennen kann. Eigenschaften von abstrakten Klassen Abstrakte Klassen und Methoden haben die folgenden speziellen Eigenschaften: Abstrakte Klassen k onnen nicht instanziiert werden (es k onnen keine Objekte von abstrakten Klassen erzeugt werden). Abstrakte Methoden m ussen u uhrt berschrieben werden, damit sie ausgef werden k onnen. Von abstrakten Methoden wird nur der Methodenkopf angegeben. Dem Methodenkopf folgt statt der geschweiften Klammern ein Semikolon. Der Methodenrumpf (-inhalt) fehlt. Nur abstrakte Klassen k onnen abstrakte Methoden besitzen. In den Unterklassen muss eine abstrakte Methode u berschrieben werden (es sei, denn, die Unterklassen sind ebenfalls abstrakt).
3
148
KAPITEL 7. VERERBUNG
Abstrakte Klassen k onnen neben abstrakten Methoden auch Attribute und normale Methoden enthalten. Alle drei folgenden F alle sind m oglich: Es gibt abstrakte Klassen, bei denen alle Methoden abstrakt sind. Andere abstrakte Klassen (und das ist der Hauptteil) haben sowohl abstrakte als auch normale Methoden. Nur die abstrakten Methoden m ussen u berschrieben werden. Es gibt aber auch abstrakte Klassen ohne abstrakte Methoden. In diesem Fall ist zwar die Klasse als abstrakt deklariert, aber keine Methode. Diese Klasse kann nicht instanziiert werden, man muss beim Ableiten aber keine Methode u berschreiben. UML-Diagramm Im UML-Diagramm gibt es zwei Schreibweisen. In der ersten wird der Klassenname kursiv geschrieben, zum Zeichen daf ur, dass die Klasse abstrakt ist. Dies ist speziell bei handschriftlichen Diagrammen unpraktikabel. Daher gibt es noch die M oglichkeit, das Wort abstract vor den Klassennamen zu setzen. Auch abstrakte Methoden werden kursiv geschrieben. Ich rate dazu, bei handgeschriebenen UML-Diagrammen das Schl usselwort abstract vor den Methodennamen zu setzen. Dies entspricht zwar nicht dem Standard, vermeidet aber Verwechslungen durch schlechte Handschrift.
Himmelskoerper
abstract Himmelskoerper
Insgesamt werden abstrakte Klassen deutlich seltener eingesetzt, als die verwandten Interfaces. Interfaces haben den groen Vorteil, dass eine Klasse mehrere Interfaces implementieren und zus atzlich eine Basisklasse haben kann. Dagegen kann eine Klasse, die eine abstrakte Basisklasse hat, von keiner weiteren Klasse mehr erben.
Kapitel 8
Rekursive Algorithmen
8.1 Einleitung
Ein Algorithmus heit rekursiv, wenn es sich selbst als Teil enth alt oder mithilfe von sich selbst deniert ist. Ein mathematischer rekursiver Zusammenhang ist z.B. eine m ogliche Denition der Fakult at: n! = 1 ; n=0 n (n 1)!; n > 0
Solch eine Denition kann in einem Java-Programm umgesetzt werden. Dabei ruft eine Funktion sich selbst wieder auf. Wir werden das in 2 Schritten umsetzen: Erstens f akultaet(n) = Zweitens: public long fakultaet(int n) { if (n==0) { return 1; } else { return n*fakultaet(n-1); } } Wichtig bei der Rekursion ist die Abbruchbedingung. Man muss daf ur sorgen, dass die Rekursionsaufrufe nicht bis in alle Ewigkeit (d.h. bis der Speicher u auft) fortgesetzt werden, sondern irgendwann abbrechen. berl Wenn eine Funktion sich selbst direkt rekursiv aufruft, nennt man das direkte Rekursion. Wenn eine Funktion a eine weitere Funktion b aufruft, die wiederum a aufruft, ist das ebenfalls eine Rekursion, die man indirekte Rekursion nennt. Wenn eine Methode eines Objekts dieselbe Methode eines anderen Objekts der gleichen Klasse aufruft, ist das ebenfalls eine Rekursion. 149 1 ; n=0 n f akultaet(n 1); n > 0
150
8.2
Zum Verst andnis der internen Abl aufe bei einem Rekursionsaufruf erinnern wir uns zun achst an die Art, wie Java Daten speichert (in anderen Programmierspachen ist es ahnlich): Programmcode und statische Variablen werden in der Method Area gespeichert. Attribute eines Objekts werden auf dem Heap gespeichert. Lokale Variablen (die in einer Methode deniert wurden) und Ubergabeparameter werden auf dem Stack gespeichert. Bei der klassischen Rekursion wird nur mit lokalen Variablen und Ubergabe parametern gearbeitet. Uns interessiert daher von den drei Bereichen nur der Stack. Stack bedeutet im Deutschen Stapel. Gestapelt werden sogenannte StackFrames. In einem Stack-Frame stehen alle in einer Methode benutzten lokalen Variablen, wobei gilt: Bei primitiven Datentypen stehen die Werte der Variablen im StackFrame. Bei Objekten steht nur ein Verweis (die Adresse) des Objekts im StackFrame. Das Objekt selbst steht im Heap.1 Jedesmal, wenn eine neue Untermethode aufgerufen wird, wird der Stapel um ein Frame erh oht. Immer, wenn eine Methode beendet wird, wird der entsprechende Frame vom Stapel genommen. Mit diesem Wissen k onnen wir schon das Beispiel Fakultaet aus dem letzten Abschnitt analysieren. Der vollst andige Code lautet: public class Fakultaet { public static long fakultaet(int n) { if (n==0) { return 1; } else { long m = n * fakultaet(n-1); return m; } } public static void main(String[] args) { long f = fakultaet(5); System.out.println(f); } }
1 Wir werden, um die Beispiele einfach zu halten, zun achst nur primitive Datentypen verwenden
151
Am Beginn der Programms wird ein Stack-Frame main auf den Stack gelegt. In diesem Frame ist zu Beginn nur die Variable args abgelegt. In der ersten Zeile der main-Methode wird die Methode fakultaet aufgerufen. Damit wird ein neuer Frame angelegt, der auf den Stapel wandert und dort den Frame main u amlich den berdeckt. Der neue Frame besitzt zu Beginn nur eine Variable, n Ubergabeparameter n.
In der fakultaet-Methode wird, wenn n > 0 ist, fakultaet(n-1) aufgerufen und damit ein weiteres Stack-Frame angelegt.
static fakultaet int n = 4 static fakultaet int n = 5 static main String[ ] args
Ohne Abbruchbedingung w urden jetzt immer weiter Stack-Frames aufeinandergestapelt, bis der Speicher f ur den Stack u auft. Anschlieend w urde eine berl Fehlermeldung wie Exception in thread "main" java.lang.StackOverflowError at Fakultaet.fakultaet(Fakultaet.java:7) at Fakultaet.fakultaet(Fakultaet.java:7) at Fakultaet.fakultaet(Fakultaet.java:7) at Fakultaet.fakultaet(Fakultaet.java:7) ... ausgegeben werden, wobei zahlreiche weitere gleiche Zeilen folgen. Jede Zeile entspricht einem verschachtelten Methodenaufruf.2 Die Meldung bedeutet, dass der Speicher f ur die Stack-Frames u bergelaufen ist.
2
152
Daher ist die Abbruchbedingung wichtig. In unserem Beispiel wird sie erreicht, wenn n = 0 ist. Der Stack sieht jetzt folgendermaen aus:
static fakultaet int n = 0 static fakultaet int n = 1 static fakultaet int n = 2 static fakultaet int n = 3 static fakultaet int n = 4 static fakultaet int n = 5 static main String[ ] args
Da n = 0 ist, wird der Wert 1 zur uckgegeben. Das oberste Stack-Frame wird aufgel ost und das darunterliegende Frame kommt zum Vorschein. Dort wird der R uckgabewert mit der Variablen n multipliziert und in einer neu erzeugen Variable m gespeichert.
static fakultaet int n = 1, m = 1 static fakultaet int n = 2 static fakultaet int n = 3 static fakultaet int n = 4 static fakultaet int n = 5 static main String[ ] args
Die Variable m wird aus der Methode zur uckgegeben und in der darunterliegenden Methode mit n aus dem entsprechenden Stack-Frame multipliziert. Dies setzt sich fort, bis die oberste fakultaet-Methode erreicht ist:
153
Dieser Wert wird an die main-Methode zuruckgegeben. Im zugeh origen StackFrame wird eine neue Variable f angelegt, in der der R uckgabewert gespeichert wird.
8.3
8.3.1
Den rekursiven Algorithmen stehen die nicht-rekursiven oder iterativen Algorithmen gegen uber, die Schleifen verwenden. Es gilt: 1. Jeder iterative Algorithmus l asst sich auch rekursiv schreiben. Die iterative Losung ist meistens klarer und schneller programmiert. Allerdings ist das zum Teil auch Geschmackssache. Andersherum lassen sich rekursive Algorithmen leicht iterativ schreiben, falls in der Methode nur ein einziger Rekursionsaufruf erfolgt. 2. Gibt es mehrere Rekursionsaufrufe in einer Methode, kann ein entsprechendes iteratives Programm deutlich l anger und komplizierter werden. Zu (1): Es ist m oglich, jeden Algorithmus ganz ohne Schleifen und lediglich mit Rekursionsaufrufen zu schreiben. Die funktionalen Programmiersprachen besitzen keine Schleifenkonstrukte und benutzen statt dessen Rekursionsaufrufe. Die bekanntesten funktionalen Sprachen sind Lisp und Haskell. Das folgende einfache Beispiel gibt die Zahlen von 1 bis 10 auf dem Bildschirm aus. Die bekannte iterative L osung ist:
154
for (int i=1; i<=10; i++) { System.out.println(i); } Die rekursive L osung basiert auf einer Methode printZahl(int start, int end) die alle Zahlen von start bis end auf dem Schirm ausgibt. Die Funktionsweise l asst sich als Formel so beschreiben:
start == end: Gib start aus
public void printZahl(int start, int end) { System.out.println(start); if (start<end) { printZahl(start+1, end); } } Die rekursive Version ist schwerer zu lesen und auerdem langsamer als die iterative Version, denn f ur jeden Rekursions-Durchlauf muss ein neues StackFrame angelegt werden. Auch das Programm zur Fakult ats-Berechnung ist in der folgenden iterativen Version kompakter und schneller als rekursiv. public long fakultaet(int n) { int result = 1; for (int i=1; i<=n; i++) { result *= i; } return result; } Je nach Neigung wird man einzelnen F allen dennoch eine rekursive L osung vorziehen. Zu (2): Rekursive Programmierung ist dort tats achlich sinnvoll, wo mehrere Rekursionsaufrufe in einer Methode verwendet werden. Hier ist eine iterative L osung allgemein deutlich komplizierter. Zu den typischen Anwendungen ist es hilfreich, sich das Ablaufschema eines solchen Programms anzusehen. Im folgenden Bild wird das Beispiel eines rekursiven Programms gezeigt, das in einer Methode zwei Rekursionsaufrufe hat.
155
Die Rekursionsaufrufe bilden eine Baumstruktur. Rekursive Programmierung eignet sich vor allem dann, wenn das Problem in eine Baumstruktur abbildbar ist. Im n achsten Semester werden wir eine Reihe von Problemen und L osungskonzepten kennenlernen, die auf eine Baumstruktur hinauslaufen. In dieser Ver anstaltung werden wir uns auf einfache Ubungsbeispiele beschr anken: Die rekursive Berechnung eines Elements der Fibonacci-Folge und die Berechnung von Variationen von Zahlen, ein Problem, das sich nicht so einfach iterativ l osen l asst.
8.4
8.4.1
Beispiele
Die Fibonacci-Folge
Die Fiboniacci-Folge hat ihren Namen nach einer Aufgabe, die von Fibonacci 1202 ver oentlicht wurde. Sie heit sinngem a: Ein Kaninchenpaar wirft vom zweiten Monat an ein junges Paar und in jedem weiteren Monat ein weiteres Paar. Die Nachkommen verhalten sich ebenso. Wie viele Kaninchenpaare gibt es nach n Monaten? In Worten ausgedr uckt ist die Antwort: Nach n Monaten gibt es alle Kaninchenpaare aus dem (n-1)ten Monat zuz uglich der Anzahl der Paare, die im (n-2)ten Monat schon gelebt und ein weiteres Paar geboren haben. Die Fibonacci-Folge ist also deniert als: 0; x = 0 1; x = 1 fx = f x1 + fx2 ; x > 1
Dies l asst sich direkt in ein rekursives Programm u bersetzen (es wird vorausgesetzt, dass keine negativen Zahlen u bergeben werden): public static int fibonacci(int x) { if (x<=1) { return x; } return fibonacci(x-1)+fibonacci(x-2); }
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987
8.4.2
Variationen
Ein einfaches Beispiel, das sich nur schwer iterativ umsetzen l asst, ist folgendes: Schreiben Sie eine Funktion public static void printVar(int n) die alle n-stelligen Zahlen ausgibt, die nur die Ziern 1 bis 3 enthalten. F ur n = 2 w are das Ergebnis 11,12,13,21,22,23,31,32,33. F ur n = 3 w are das Ergebnis 111,112,113,121,122,123,131,132,133,211,212,213,221,222,223,231, 232,233,311,312,313,321,322,323,331,332,333. Falls man sich auf ein festes n festlegen kann, ist die Funktion kein Problem. Zum Beispiel k onnte man f ur n = 3 schreiben: for (int i=1; i<=3; i++) { for (int j=1; j<=3; j++) { for (int k=1; k<=3; k++) { System.out.println(""+i+j+k); } } } Wenn n aber variabel sein soll, br auchte man n ineinandergeschachtelte forSchleifen, wobei n erst zur Laufzeit bekannt ist. Ein solches Konstrukt gibt es in Java nicht. Es gibt zwar m ogliche iterative Ans atze, aber die haben ein deutlich anderes Aussehen. Rekursiv kann man das Problem aber recht direkt l osen. Die Funktion printVar ruft zuerst eine zweite Funktion private static void printRekVar(String s, int n) auf. Dabei ist s der Ausgabestring, der nach und nach zusammengebaut werden soll. n ist nach wie vor die Anzahl der Stellen, die das Ergebnis haben soll. Die rekursive Funktion pr uft zun achst, ob der zusammengebaute String schon lang genug ist. Wenn ja, wird der String ausgegeben. Dies ist gleichzeitig die Abbruchbedingung. Wenn nein, dann wird dreimal rekursiv printRekVar aufgerufen, wobei an s jeweils eine 1, eine 2 oder eine 3 angeh angt wird. Der komplette Code ist:
8.4. BEISPIELE private static void printRekVar(String s, int n) { if (s.length()==n) { //Abbruchbedingung System.out.println(s); return; } for (int i=1; i<=3; i++) { printRekVar(s+i,n); //Rekursiver Aufruf, String-Addition } } public static void printVar(int n) { printRekVar("", n); }
157
Wenn man sich die einzelnen Funktionsaufrufe ansieht, erh alt man wieder eine Baumstruktur. In der folgenden Grak werden die n otigen Funktionsaufrufe f ur n = 2 abgebildet. In den K astchen steht jeweils der Ubergabeparameter s. Beim Durchlauf wird an der Wurzel (hier oben abgebildet) begonnen. Anschlieend werden die Verzweigungen nach unten von links nach rechts durchlaufen. Das gleiche geschieht auch auf den n achsten Ebenen, bis es keine weiteren Verzweigungen nach unten mehr gibt. Man ist dann an den sogenannten Bl attern des Baums angelangt. Dort wird der String jeweils ausgegeben.
1 11 12 13 21 2 22 23 31 3 32 33
Einige ahnliche Probleme (z.B. das Rucksack-Problem) werden wir noch ausf uhrlich in der Vorlesung Algorithmen behandeln.
158
Kapitel 9
Gr oere Programmeinheiten
9.1 Bibliotheken
Die gr ote Programmeinheit in Java ist die Bibliothek (Library). Alle JavaKlassen werden automatisch einer Bibliothek zugeordnet. Eine Bibliothek kann zwei verschiedene Formen annehmen: 1. eine jar- oder zip-Datei 2. ein Verzeichnis (einschlielich der Unterverzeichnisse).
9.1.1
Java unterscheidet zwischen den Java-Standardbibliotheken (bootstrap classes) und den benutzerspezischen Bibliotheken und legt dazu zwei getrennte BibliotheksListen an. Die Liste der Standardbibliotheken sollte niemals ver andert werden, wogegen die Liste der benutzerspezischen Bibliotheken ver anderbar ist. Beide Listen k onnen ausgegeben werden mit: //Benutzerspezifische Bibliotheken System.out.println(System.getProperty("java.class.path")); //Standardbibliotheken System.out.println(System.getProperty("sun.boot.class.path")); Per Default steht in der Liste der benutzerspezischen Bibliotheken nur der aktuelle Pfad (bzw. in Eclipse der Projekt-Pfad). Das heit, die einzige benutzerspezische Bibliothek umfasst alle Dateien im aktuellen Verzeichnis und in den Unterverzeichnissen.1 Um weitere Bibliotheken hinzuzuladen, muss der sogenannte Classpath erweitert werden. Dazu gibt es mehrere M oglichkeiten: Setzen der Umgebungsvariablen CLASSPATH: Da die Umgebungsvariable den Default-Wert u berschreibt, muss unbedingt der aktuelle Pfad mit gesetzt werden, sonst werden die Java-Dateien im aktuellen Pfad nicht mehr gefunden. Beispiel:
1 Die Dateien in den Unterverzeichnissen liegen in Paketen, siehe dazu den entsprechenden Abschnitt.
159
160
Start eines Java-Programms mit java -cp <Liste der Bibliotheken> <Startdatei> Auch hier muss der aktuelle Pfad mit angegeben werden, damit die JavaDateien im aktuellen Pfad noch gefunden werden. Die Vorgehensweise in Eclipse wird an einem Beispiel im folgenden Abschnitt beschrieben. Benutzen einer Mathematikbibliothek mit Java Im Folgenden soll ein kleines Beispielprogramm erstellt werden, das die Mathematikbibliothek JScience nutzt. JScience enth alt Klassen f ur Lineare Algebra, Br uche, Polynome und komplexe Zahlen. Zun achst laden wir die Bibliothek herunter. Die JScience-Seite im Web ist http://jscience.org. Sie ben otigen die Librarys JScience und Javolution (jeweils eine jar-Datei). Zum Test legen wir die beiden jar-Dateien in dem Verzeichnis C:\JScience ab. Jetzt m ussen die Dateien zur Liste der benutzerspezischen Bibliotheken hinzugef ugt werden. Der einfachste Weg ist, die Umgebungsvariable CLASSPATH zu setzen:2 set CLASSPATH=".;C:\JScience\jscience-5.0-SNAPSHOT.jar; C:\JScience\javolution-5.5.1.jar" Der Punkt am Anfang ist wichtig, da sonst Java nicht mehr im aktuellen Pfad sucht und damit wahscheinlich das eigentliche Programm nicht mehr ndet. Alternativ kann die Liste auch beim Aufruf von java oder javac mit dem Kommandozeilenparameter -cp angegeben werden. Wir wollen f ur unser Programm (wie u onen wir blich) Eclipse benutzen. Dazu eine neues Eclipse-Projekt und erzeugen dort eine Klasse Mathtest.java. Dort geben wir ein: import org.jscience.mathematics.number.*; public class Mathtest { public static void main(String[] args) { Complex c1 = Complex.valueOf(1, 5); Complex c2 = Complex.valueOf(3, 2); Complex c3 = c1.times(c2); System.out.println(c3); } }
Hier mit den im Juli 2010 aktuellen Versionen. Die gezeigte Syntax ist die f ur Windows. Unter Linux ist das Trennzeichen zwischen den Pfadangaben kein Semikolon, sondern ein Doppelpunkt.
2
9.1. BIBLIOTHEKEN
161
Das Programm testet die komplexen Zahlen aus dem JScience-Paket. Eclipse ben otigt jetzt noch die Information, wo die JScience-Bibliothek zu nden ist. Dazu ist folgendes in Eclipse einzugeben: (englische Version): Men upunkt P roject P roperties, Men upunkt Java Build Path und Reiter Libraries w ahlen, Button Add External JARs... dr ucken, gew unschte Jar-Dateien ausw ahlen, zweimal OK dr ucken. (deutsche Version): Men upunkt P rojekt Eigenschaf ten, Men upunkt Java-Erstellungspfad und Reiter Bibliotheken w ahlen, Button Externe JARs hinzuf ugen dr ucken, gew uschte Jar-Dateien ausw ahlen, zweimal OK dr ucken. Anschlieend kann das Programm ganz normal gestartet werden.
9.1.2
Aufbau einer jar-Datei Eine jar-Datei hat den gleichen Aufgau wie eine zip-Datei. Man kann eine Datei xyz.jar in xyz.zip umbennenen und sie anschlieend z.B. mit WinZip o atzlich eine nen. Hauptunterschied zu einer normalen zip-Datei ist, dass zus Datei Mainfest.mf mit eingepackt wird. Hauptbestandteil der Manifest-Datei ist der Klassenname der Start-Klasse. Damit ist es m oglich, eine java-Datei direkt zu starten. Es wird dann die main-Methode der Start-Datei ausgef uhrt. Die Manifest-Datei kann von Hand editiert, vom Programm jar, das im Java-Paket enthalten ist, erzeugt oder auch von Eclipse automatisch generiert werden. Erzeugung einer jar-Datei aus Eclipse (englische Version): Men upunkt F ile Export, Java JAR f ile w ahlen, Next dr ucken, die Projekte ausw ahlen, die in die jar-Datei aufgenommen werden sollen, im Feld Select the export destination den Namen der jar-Datei angeben, 2 mal Next dr ucken, im Feld Select the class of the application entry point die Klasse angeben, dessen main-Methode gestartet werden soll (f ur reine Bibliotheksdateien muss hier nichts angegeben werden),
Men upunkt Datei Exportieren, JAR Datei w ahlen, Weiter dr ucken, Die Projekte ausw ahlen, die in die jar-Datei aufgenommen werden sollen, im Feld Exportziel auswaehlen den Namen der jar-Datei angeben, 2 mal Next dr ucken, im Feld Klasse des Eingangspunkts f ur die Anwendung ausw ahlen die Klasse angeben, dessen main-Methode gestartet werden soll (f ur reine Bibliotheksdateien muss hier nichts angegeben werden). Fertig stellen dr ucken. Eclipse zippt die Dateien in eine jar-Datei und f ugt automatisch eine Manifest Datei hinzu. Starten einer jar-Datei Eine jar-Datei kann direkt ausgef uhrt werden. Es wird dann die Klasse gestartet, die in Eclipse (bzw. in der Manifest-Datei) als Start-Datei angegeben ist. In Windows wird eine jar-Datei durch Doppelklick auf den Dateinamen im Windows-Explorer gestartet. In Linux bzw. in einer Windows-Eingabeauorderung verwendet man die Kommandozeile java -jar <jar-Dateiname>
9.2
9.2.1
Pakete
Laden von Paketen
Betrachten wir eine einfache Java-Klasse, in der eine komplexe Zahl eingelesen wird: public class Mathtest { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("Realteil: "); double re = sc.nextDouble(); System.out.print("Imaginaerteil: "); double im = sc.nextDouble(); Complex c = Complex.valueOf(re, im); System.out.println(c); } }
9.2. PAKETE
163
Diese Programm ist so nicht lau ahig. Beim Start (und in Eclipse) wird f ur die beiden Klassen Scanner und Complex die Fehlermeldung ... cannot be resolved to a type ausgegeben. Das bedeutet, dass die beiden Klassen nicht gefunden werden k onnen. Rufen wir uns zun achst in Erinnerung: Die beiden Klassen liegen in Bibliotheken. Die Bibliotheken m ussen Java bekannt sein. Scanner liegt in der Java-Standardbibliothek, die immer bekannt ist. Complex liegt in der Bibliothek JScience, die entsprechend dem vorigen Kapitel eingebunden werden muss. Innerhalb der Bibliotheken liegen die Klassen in Paketen. Scanner liegt in java.util, Complex liegt in org.jscience.mathematics.number. Die vollst andigen Klassennamen lauten demgem a: java.util.Scanner org.jscience.mathematics.number.Complex oglichkeiten: Wenn eine Klasse aus einem Paket genutzt werden soll, gibt es 3 M Sie wird mit dem vollst andigen Klassennamen angegeben. In unserem Beispiel w are das public class Mathtest { public static void main(String[] args) { java.util.Scanner sc = new java.util.Scanner(System.in); System.out.print("Realteil: "); double re = sc.nextDouble(); System.out.print("Imaginaerteil: "); double im = sc.nextDouble(); org.jscience.mathematics.number.Complex c = org.jscience.mathematics.number.Complex.valueOf(re, im); System.out.println(c); } } Das f uhrt nat urlich zu sehr un ubersichtlichen Klassennamen. Die Klasse wird durch eine import-Anweisung importiert. Die importAnweisung steht vor der ersten Klassen-Denition. Beispiel: import org.jscience.mathematics.number.Complex; import java.util.Scanner; public class Mathtest { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("Realteil: ");
164
KAPITEL 9. GROSSERE PROGRAMMEINHEITEN double re = sc.nextDouble(); System.out.print("Imaginaerteil: "); double im = sc.nextDouble(); Complex c = Complex.valueOf(re, im); System.out.println(c); } } Unter Eclipse kann man die import-Anweisungen automatisch mit SHIFTSTRG-o erzeugen.
Das komplette Paket wird importiert. Dazu sind die import-Anweisungen aus dem letzten Beispiel durch import org.jscience.mathematics.number.*; import java.util.*; zu ersetzen. Die letzte Variante wird import on demand genannt. Viele import on demands bewirken, dass sich die Zeit f ur den Compiler-Durchlauf etwas erh oht, haben aber keine weiteren Auswirkungen. Der Grund ist: Die import-Anweisung sagt dem Java-Compiler, wo er die ben otigten Klassen ndet. In unserem letzten Beispiel w urde der Compiler eine Klasse XYZ in zwei Schritten suchen: Er durchsucht alle Bibliotheken nach den Paketen Default-Paket (Paket ohne Namen) java.lang (Automatisch eingebunden) org.jscience.mathematics.number java.util Sind die Pakete gefunden, werden sie nach der Klasse XYZ durchsucht. Wird statt import on demand die spezische Klasse importiert: import org.jscience.mathematics.number.Complex; import java.util.Scanner; dann wird f ur die Klasse XYZ nur noch das Default-Paket und java.lang durchsucht, was nat urlich schneller geht. Die class-Datei wird dadurch aber nicht kleiner und das Programm, wenn es einmal compiliert ist, nicht schneller.
9.2.2
Unsere bisherigen Klassen wurden bereits, ohne dass wir es beabsichtigt haben, einem Paket zugeordnet, n amlich dem sogenannten Default-Paket. F ur das Default-Paket gilt: 1. Die Klassen k onnen direkt, ohne import-Anweisung verwendet werden. Der vollst andige Klassenname ist gleich dem eigentlichen Klassennamen.
9.2. PAKETE
165
2. Die Klassen m ussen direkt in einem Bibliotheksverzeichnis stehen, nicht in einem Unterverzeichnis. Das haben wir erf ullt, da wir in Eclipse die Java-Dateien direkt in das Projektverzeichnis geschrieben haben. Nun wollen wir die Klassen in Pakete legen. Zum Beispiel wollen wir eine Klasse Pakettest angelegen, die im Paket mypack stehen soll. Zun achst legen wir versuchsweise die Klasse Pakettest in Eclipse an und geben in der Zeile Paket/Package den Namen mypack an. Eclipse erzeugt die Datei package mypack; public class Pakettest { } und legt sie im Unterverzeichnis mypack des Projektverzeichnisses an. Damit sind auch schon die zwei Bedingungen genannt, die erf ullt sein m ussen. Erstens muss in der ersten Zeile des Programms eine package-Anweisung stehen. Zweitens muss die class-Datei in einem Ordner der Form Bibliothekspfad\Paketpfad liegen. Wenn also der Bibliothekspfad C:\lib heit, muss die Datei im Verzeichnis C:\lib\mypack stehen. Zum Starten der Datei aus der Kommandozeile ist das Kommando java mypack.Packtest zu benutzen. Es k onnen auch tiefer verschachtelte Ordnerpfade angegeben werden. Ersetzt man im vorigen Beispiel die package-Anweisung durch package myapp.mypack; dann erwartet Java die class-Datei im Ordner C:\lib\myapp\mypack und der Startbefehl f ur Java lautet java myapp.mypack.Packtest Ist die Bibliothek eine jar-Datei, dann entspricht der Paketname dem Pfad der class-Datei innerhalb der jar-Datei. Beispiel: Die Klasse Complex in JScience liegt im Paket org.jscience.mathematics.number. Das heit, dass in der Datei jscience.jar eine Datei org\jscience\mathematics\numbers\Complex.class vorhanden ist. Auch die Dateien der Java-Klassenbibliothek kann man auf diese Weise identizieren. Die Klasse String im Paket java.lang bendet sich im Java-Installationsverzeichnis, Unterordner lib, Bibliotheks-Datei rt.jar und dort im Unterverzeichnis java\lang in der Datei String.class (Sun-Java 1.5). Die Java-Sourcen nden sich u brigens in der direkt im Java-Installationsverzeichnis gelegenen Bibliotheks-Datei src.zip.
166
9.2.3
Ziel der Java-Entwickler ist es, dass die vollst andigen Klassennamen aller auf der Welt erstellten Klassen eindeutig sind. Das ist deswegen sinnvoll, weil prinzipiell alle Klassen miteinander kombiniert werden k onnen. Daher gibt es die Vorgabe, sich bei der Namensgebung eines Pakets an die eigene Internet-Adresse anzulehnen. Genauer gesagt, sollen die Paketnamen mit dem eigenen Domain-Namen beginnen, wobei allerdings die Reihenfolge der Namensbestandteile herumgedreht ist. Beispiele: 1. JScience hat den Domain-Namen jscience.org. Alle JScience-Pakete beginnen deshalb mit org.jscience, wie auch unser Beispiel: org.jscience.mathematics.number 2. Eclipse-Pakete beginnen mit org.eclipse, z.B. org.eclipse.jface.dialogs 3. Bei uns im Haus erstellte Java-Pakete m ussten nach diesem Schema sinnvollerweise mit de.rwth_aachen.rz beginnen.3
9.2.4
Bisher kannten wir nur eine Sichtbarkeitsgrenze, n amlich die Klasse. Methoden und Attribute, die als private deklariert sind, k onnen nur in der eigenen Klasse gesehen werden, als public deklarierte Methode und Attribute auch in anderen Klassen. Java kennt noch zwei weitere Sichtbarkeitsgrenzen: Die abgeleiteten Klassen und die Pakete. Daf ur gibt es die Zugangsbeschr ankungen protected und default (das ist, wenn man gar nichts davorschreibt). Die Zugangsbeschr ankungen sind kurz gefasst: public : Alle Klassen haben Zugri. protected : Alle Klassen des eigenen Pakets und alle abgeleiteten Klassen (auch in anderen Paketen) haben Zugri. default : Nur Klassen des eigenen Pakets haben Zugri. private : Nur die eigene Klasse hat Zugri. Zweierlei ist zu beachten: 1. protected ist, anders als es die Namenswahl suggeriert, freiz ugiger als default.
3 Der korrekte Domain-Name ist rz.rwth-aachen.de. Ein Bindestrich ist jedoch in JavaPaketnamen nicht zugelassen. Nach Java-Konvention ersetzt man ihn durch einen Unterstrich.
9.3. DATEIEN
167
2. Eine Zugrisbeschr ankung Die eigene Klasse und alle davon abgeleite ten gibt es nicht. Das ist Java-typisch. In C# sieht es z.B. ganz anders aus:4 Java C# Zugri von public public u berall protected protected internal Eigenes Package (Assembly) und abgeleitete Klassen default internal Eigenes Package (Assembly) protected Eigene und abgeleitete Klassen private default, private Nur eigene Klasse
9.3
Dateien
Eine weitere gr oere Programmeinheit ist die Datei (Sun sagt dazu auch compilation unit). Dazu gibt es einige sehr Java-spezische Regeln: 1. Eine Datei darf mehrere Klassen enthalten. 2. Nur eine Klasse davon darf public sein. Das heit, nur eine davon darf mit public class ... deklariert werden. Diese Klasse muss in der Datei zuoberst stehen. 3. Die anderen Klassen m ussen mit class ... deklariert werden. Sie d urfen nicht auerhalb des Pakets benutzt werden. Weiterhin k onnen sie nicht als Start-Klasse dienen. Diese Regelungen haben zur Folge, dass Java-Projekte die Neigung haben, sich auf viele winzige Dateien aufzusplittern. Das ist zwar von den Java-Entwicklern so beabsichtigt, kann aber durchaus l astig sein. Ein Trick, mehrere Java-Klassen in einer Datei zusammenzufassen, sind die statischen inneren Klassen, die im folgenden Kapitel erkl art werden.5
9.3.1
Diese Kapitel zeigt, wie statische innere Klassen eigesetzt werden, um mehere Klassen in einer Datei zusammenzufassen und welche sonstigen Vorteile sich daraus entwickeln. Dazu betrachten wir ein Beispiel:
Die C#-Sichtbarkeitsgrenze Assembly entspricht eher einer Bibliothek als einem Paket. Es gibt insgesamt 4 Arten von inneren Klassen. In dieser Vorlesung werden nur die statischen und die anonymen inneren Klassen (Kapitel 6) angesprochen.
5 4
168
public class Sternkatalog { public static class Stern { public String name; public double hell; public double entf; public String bild; } public static class Katalog { public ArrayList<Stern> katalog = new ArrayList<Stern>(); } } Die auere Klasse Sternkatalog enth alt nichts, auer zwei inneren Klas sen Stern und Katalog. Die inneren Klassen sind mit den Schl usselworten public static deklariert. Sie unterscheiden sich im Verhalten nicht von einer normalen Klasse. Der Hauptunterschied liegt in der Namensgebung. Die Klasse Katalog aus diesem Beispiel w urde man mit Sternkatalog.Katalog testKatalog = new Sternkatalog.Katalog(); erzeugen, was auch auerhalb des Pakets m oglich ist. Das macht einerseits deutlich, dass die Klasse Katalog nur im Zusammenhang mit Sternkatalog sinnvoll ist. Andererseits verhindert es Namens uberschneidungen. Man kann statischen inneren Klassen Namen wie Element oder Debug geben, ohne bef urchten zu m ussen, dass es bereits andere Klassen dieses Namens gibt. Die auere Klasse hat nur den Zweck, als Namensraum zu dienen. Namensr aume werden im n achsten Kapitel erl autert.
9.4
Abstrahieren wir an dieser Stelle etwas und l osen uns von Java. Der vollst andige Name einer Klasse setzt sich zusammen aus zwei Bestandteilen: dem Namensraum (Namespace) und dem eigentlichen Klassennamen. Die Form dabei ist gew ohnlich: Namensraum.Klassenname wobei der Namensraum durch beliebig viele Punkte gegliedert sein darf. In Java k onnen wir 1. Pakete und 2. Klassen (f ur statische innere Klassen) als Namensr aume betrachten. Alle Klassen innerhalb desselben Namensraums k onnen sich gegenseitig ansprechen, ohne dass die lange (vollst andige) Version des Klassennamens benutzt werden muss. Klassen aus fremden Namensr aumen brauchen die lange Version, was nat urlich sehr umst andlich w are, wenn man
169
Klassen nicht aus Namensr aumen importieren k onnte. Importieren heit: Eine Klasse in den eigenen Namensraum mit hineinnehmen, so dass der Klassenname auch in der kurzen Form benutzt werden kann. Dieses Konzept ndet sich in allen objektorientierten Programmiersprachen. Unterschiede bestehen allerdings dahin, was als Namensraum betrachtet werden kann. In C# kann man sich mit der Anweisung namespace recht frei Namensr aume zusammenstellen. In Visual Studio wird automatisch ein Namensraum mit dem Projektnamen vorgegeben. In Python ist eine Datei ein Namensraum mit dem entsprechenden Dateinamen.
9.4.1
In Java ist die Verwendung von aueren Klassen als Namensr aume in einem Punkt eingeschr ankt. Soll eine a uere Klasse als Namensraum importiert wer den, darf sie nicht im Default-Paket liegen. In anderen Paketen klappt es. Beispiel: package testpaket; public class Aussen { public static class Innen1 { //.. } } Im import-Befehl kann jetzt der Name der aueren Klasse verwendet werden. import testpaket.Aussen.*; public static void Start(String[] args) { Innen1 = new Innen1(); }
170
Kapitel 10
10.1
Programmierrichtlinien
Programmierrichtlinien sollen die Lesbarkeit von Programmcode erh ohen. Traditionell sind diese Richtlinien rmenspezisch. Eine gewisse Bedeutung haben z.B. die Firmenrichtlinien von Microsoft oder die GNU Coding Standards. F ur Java gibt es Programmierrichtlinien von Sun, die von den meisten JavaProgrammierern (nicht allen) befolgt werden. Die Richtlinien sind in den 20seitigen Java Code Conventions zusammengefasst, die man unter der Adresse http://java.sun.com/docs/codeconv im Internet ndet. Im folgenden Kapitel werden einige interessante Teile der Richtlinie vorgestellt, wobei die Richtlinien u ber Kommentare in einem eigenen Kapitel zusammengefasst werden.
1
Die Java Code Conventions von Sun sprechen von 80% Wartung.
171
172
Umgang mit Programmierrichtlinien Es k onnen zwei verschiedene Situationen auftreten: Die Programmierrichtlinien sind von der Firma oder vom Projektgeber verbindlich vorgeschrieben. Dann ist die Situation klar. Es gibt im konkreten Projekt keine verbindlichen Programmierrichtlinien. Dann sollte man dennoch eine verbindliche Festlegung treen und diese auch dokumentieren. Dabei ist es gut, sich an eine weitverbreitete Programmierrichtlinie zu halten, in Java also in erster Linie an die Java Code Conventions von Sun. Man sollte aber immer den Zweck von Programmierrichtlinien im Hinterkopf haben, n amlich die Erh ohung der Lesbarkeit von Programmen. Gut begr undete Verst oe sind also erlaubt, wenn sie der Erh ohung der Lesbarkeit dienen. Style Checker Zur Unterst utzung bei der Einhaltung von Programmierrichtlinien gibt es Hilfsprogramme, die Style Checker oder Code Reviewer genannt werden. In Eclipse ist ein einfacher Style Checker bereits eingebaut. Zwei Funktionen stehen zur Verf ugung: STRG-I oder Source Correct Indentation korrigiert die Einr uckungen des markierten Textes (Markieren des gesamten Textes: STRG-A). SHIFT-STRG-F oder Source Format formatiert den markierten Text nach dem im folgenden Punkt einstellbaren Regeln. Die Regeln zur Formatierung sind unter Window Preferences Java Code Style und den entsprechenden Unterpunkten einstellbar. Ein weit m achtigerer Style-Checker f ur Java ist das Programm Checkstyle. Mit der Erweiterung eclipse-cs kann man Checkstyle als Plugin in Eclipse einbinden. Checkstyle ist frei und kann von dieser URL heruntergeladen werden: http://checkstyle.sourceforge.net/ Es lohnt sich, mal ein selbstgeschriebenes Programm mit Checkstyle zu kontrollieren. Vorher sollte man jedoch unter Window Preferences Checkstyle die Check Conguration auf Sun Checks (Eclipse) a ndern, da sonst die von Eclipse automatisch gesetzten Tabs alle als Fehler erkannt werden.
10.1.1
10.1. PROGRAMMIERRICHTLINIEN
int level; // indentation level int size; // size of table is preferred over int level, size;
173
174
Namenskonventionen
9 Naming Conventions Naming conventions make programs more understandable by making them easier to read. They can also give information about the function of the identier - for example, whether its a constant, package, or class - which can be helpful in understanding the code. The conventions given in this section are high level. Further conventions are given at (to be determined). Identier Type Rules for Naming Examples Classes Class names should be nouns, in class Raster; mixed case with the rst letter of class ImageSprite; each internal word capitalized. Try to keep your class names simple and descriptive. Use whole words - avoid acronyms and abbreviations (unless the abbreviation is much more widely used than the long form, such as URL or HTML). Interfaces Interface names should be capitali- interface RasterDelegate; zed like class names. interface Storing; Methods Methods should be verbs, in mixed run(); case with the rst letter lowercase, runFast(); with the rst letter of each internal getBackground(); word capitalized. Variables Except for variables, all instance, int i; class, and class constants are in mi- char cp; xed case with a lower case rst let- float myWidth; ter. Internal words start with capital letters. Variable names should be short yet meaningful. The choice of a variable name should be mnemonic - that is, designed to indicate to the casual observer the intent of its use. Onecharacter variable names should be avoided except for temporary throwaway variables. Common names for temporary variables are i, j, k, m, and n for integers; c, d, and e for characters. The names of variables declared class constants and of ANSI constants should be all uppercase with words separated by underscores ( ). (ANSI constants should be avoided, for ease of debugging.)
Constants
int MIN WIDTH = 4; int MAX WIDTH = 999; int GET THE CPU = 1;
175
10.2 Referring to Class Variables and Methods Avoid using an object to access a class (static) variable or method. Use a class name instead. For example: classMethod(); AClass.classMethod(); anObject.classMethod(); //OK //OK //AVOID!
10.1.2
Auch hier gibt es zahlreiche Regeln in den Java Code-Konventionen, die hier jedoch nicht wiedergegeben werden sollen. Eclipse versucht, den Quelltext m oglichst automatisch in der richtigen Weise zu formatieren. Die Regeln dazu kann man unter W indow P ref erences Java CodeStyle F ormatter einsehen und auch gegebenenfalls andern. Mit der Tastenfolge STRG-SHIFT-F veranlasst man Eclipse, die gesamte Datei entsprechend der eingestellten Regeln neu zu formattieren.
10.2
Dokumentationskommentare
Dokumentationskommentare k onnen mit dem Befehl javadoc in html-Text umgewandelt werden. Ein Beispiel f ur die daraus entstehende Dokumentation ist die Java-API, die komplett aus Dokumentationskommentaren besteht. Dokumentationskommentare haben folgendes Format: /** Dokumentationskommentar */
176
Die Java-Programmierrichtlinien empfehlen das folgende Format, das Eclipse auch automatisch nach der Eingabe von /** und <return> setzt: /** * * Dokumentationskommentar * */
10.2.1
Dokumentationskommentare tauchen vorzugsweise an drei Orten auf: vor Klassendenitionen (Klassen-Javadoc), vor der Denition von Attributen (Attribut-Javadoc) und vor der Denition von Methoden und Konstruktoren (MethodenJavadoc)
10.2.2
Die typische Javadoc einer Klasse hat das Aussehen: /** * * Genaue Beschreibung der Klasse, gegebenenfalls mit * Beispielen. * * * @version 1.5, November 2007 * @author Herbert Mustermann */ public class Testklasse { ... } @version und @author sind spezielle Tags, die Javadoc nutzt, um die f ur Javadoc-Dokumente typische Seitenstruktur aufzubauen.
10.2.3
Bei Attributen ist es u blich, eine einfache Javadoc-Zeile ohne Tags vor die Denition des Atrtibuts zu setzen. Beispiel: /** Beschreibung des Attributs maxLen */ public double maxLen;
10.2. DOKUMENTATIONSKOMMENTARE
177
10.2.4
Hier nden die meisten Tags Verwendung. Die wichtigsten sind: @param f ur die Beschreibung eines Ubergabeparameters. Es folgt der Name und die Beschreibung des Parameters. Beispiel: @param breite Breite des Rechtecks @return f ur die Beschreibung des R uckgabewerts. Beispiel: @return Flaeche des Rechtecks @throws f ur die Beschreibung der m oglichen Exceptions. Es folgt der Name und die Beschreibung der m oglichen Exception. Beispiel: @throws ArithmeticException falls Laenge oder Breite kleiner 0. Nachfolgend ein ausf uhrliches zusammenh angendes Beispiel. Eine Methode public static String wandleZiffer(String zahl, int systemAlt, String systemNeu) die zahl vom Zahlensystem systemAlt in das Zahlensystem systemNeu wandelt, wird kommentiert. /** * *Wandelt eine Zahl von einem Zahlensystem in ein anderes. *Alle Zahlensysteme zwischen dem 2er und dem 20er-System *sind erlaubt. Ziffernwerte ab 10 werden durch Buchstaben *dargestellt, beim 20er System gibt es also die Ziffern *0123456789ABCDEFGHIJ. * *@param zahl Die zu wandelnde Zahl als String. Der String *darf nur die Zeichen enthalten, die aufgrund des *Zahlensystems systemAlt erlaubt sind. * *@param systemAlt Das Ausgangs-Zahlensystem. Erlaubt sind *die Werte 2 bis 20. * *@param systemNeu Das neue Zahlensystem. Erlaubt sind *die Werte 2 bis 20. * *@return Die Zahl im neuen Zahlensystem in String-Darstellung. * *@throws NumberFormatException falls zahl falsche Zeichen *enthaelt. *
178
*@throws ArithmeticException falls systemAlt oder systemNeu *nicht zwischen 2 und 20 liegen. * */ public static String wandleZiffer(String zahl, int systemAlt, int systemNeu) { ... } Der erste Satz des Kommentars wird im API-Dokument als Kurzbeschreibung verwendet. Der komplette erste Teil (vor den Tags) steht in der ausf uhrlichen Beschreibung.
10.2.5
Weitere m ogliche Tags, die sowohl bei Klassen als auch bei Attributen und Methoden auftauchen d urfen sind: @see: Verweis auf eine andere Stelle in der Dokumentation. Beispiele: @see "Hinweis, z.B. auf Buch" @see package.class#member @see <a href="URL#value">label</a> Erzeugt eine See Also-Sektion mit dem entsprechenden Link. {@link}: Verweis auf eine andere Stelle in der Dokumentation. Im Gegensatz zu @see wird der Link direkt in den Text gesetzt und keine See Also-Sektion erzeugt. Andere Tags werden z.B. in Wikipedia erkl art (Stichwort javadoc). Neben den Javadoc-Tags d urfen auch beliebige HTML-Tags verwendet werden. Ausnahmen sind <H1>. . . <H6> und <HR>. Umlaute in der Javadoc Da alle Zeichen direkt in HTML u ussen Umlaute ausbernommen werden, m geschrieben oder als HTML-Tag geschrieben werden, also z.B. ae oder ä statt a. Emtwicklungsumgebungen wie Eclipse setzen die Umlaute beim Erstellen der Javadoc automatisch um.
10.2.6
Auf der Kommandozeile k onnen die Java-Dateien des aktuellen Verzeichnisses mit javadoc *.java
179
in eine Javadoc umgesetzt werden. Die verschiedenen Optionen des JavadocBefehls sollen hier nicht erkl art werden. In Eclipse kann die gesamte Javadoc des Projekts mit P roject GenerateJavadoc erzeugt werden. Es gibt in Eclipse noch eine weitere M oglichkeit, Dokumentationskommentare zu sehen. Wenn man im unteren Fenster (wo normalerweise die Konsole sichtbar ist) den Reiter Javadoc anklickt, erscheint immer dann, wenn sich der Cursor in einem Dokumentationskommentar bendet, dort der zugeh orige formatierte Text.
10.3
In Kommentaren k onnen prinizipiell beliebige Texte untergebracht werden. Trotzdem haben sich Regeln ausgebildet, wann Kommentare sinnvoll eingesetzt werden.
10.3.1
Dar uber, wie viele Kommentare in einem Programm angebracht sind, bestehen stark unterschiedliche Meinungen. Bei der Assembler-Programmierung kann es durchaus sinnvoll sein, jede Programmzeile einzeln zu kommentieren, wie das nachfolgende Beispiel zeigt: INC 2 JEC 3 J3P OUT J2P ENT2 2 1 1B -20,2 DONE 0 Increase buffer pointer Repeat ten times (PRINTER) Have we printed both lines? Set buffer pointer for 2nd buffer
Gelegentlich trit man diese Regel auch in Hochsprachen an, durchgesetzt hat sich jedoch eine andere Kommentierungsweise: Kommentiere nur, was zum Verst andnis des Programms n otig ist. Die Kenntnis der Java-Bibliothek sollte dabei vorausgesetzt werden. Kommentiere den Zweck und das Ziel eines Code-Bereiches. Tricks und schwierige Code-Stellen m ussen kommentiert werden. Es gibt also ein zu wenig und ein zu viel an Kommentaren, was auch die folgenden Zitate verdeutlichen: Von der M oglichkeit, Kommentare in Programme einzuf ugen, sollte reger Gebrauch gemacht werden.2
2
180
The frequency of comments sometimes reects poor quality of code. When you feel compelled to add a comment, consider rewriting the code to make it clearer. (Java Code Conventions) Hier noch ein kurzes Zitat aus dem englischen Wikipedia: Good comments ... clarify intent..
10.3.2
Anfangskommentare Vor der ersten Java-Zeile steht ein Anfangskommentar. Der Anfangskommentar ist kein (javadoc-) Dokumentationskommentar, sondern ein normaler Block kommentar. In den Java-Programmierrichtlinien ndet man: All source les should begin with a c-style comment that lists the programmer(s), the date, a copyright notice, and also a brief description of the purpose of the program. For example: /* * Classname * * Version info * * Copyright notice */ Oft steht hier auch die sogenannte History, d.h. die Aufz ahlung der Anderungen seit der 1. Programmversion. Diese Liste ist oft ziemlich lang. In der Regel wird sie automatisch von Versionsverwaltungssystemen (CVS, Subversion) erstellt. Dieser Kommentar ist nicht identisch mit der Klassen-Javadoc. Zwischen Anfangskommentar und Klassen-Javadoc stehen die package- und die importAnweisungen. Gliederung des Quelltexts Kommentare gliedern einen Quelltext in Abschnitte. Sie helfen, sich in einem l angeren Code zurechtzunden. Vor dem Kommentar sollte eine (oder mehrere) Leerzeilen stehen. Erl auterung einer einzelnen Zeile Kommentare k onnen eine schwierige Programmzeile erl autern, damit andere oder der Autor selbst diese sp ater leichter verstehen.
181
Es gibt spezielle Kommentare f ur verbesserungsw urdige oder fehlerhafte Programmteile. Die Java-Programmierrichtlinien sagen: Use XXX in a comment to ag something that is bogus but works. Use FIXME to ag something that is bogus and broken. Auerdem gibt es noch TODO als Platzhalter f ur fehlende Codest ucke. Eclipse f arbt die Kommentare //XXX //FIXME //TODO in einer speziellen Farbe (blaugrau) ein und setzt eine spezielle Markierung an den rechten Rand. Auskommentierung von Code Mit /* und */ k onnen Codes ucke (vor ubergehend) ung ultig gemacht werden. Besonders h aug werden Zeilen, die dem Debugging dienen, auskommentiert.
10.3.3
Widerspru che von Code und Kommentar Eine Gefahr bei der nachtr aglichen Anderung eines Programmes ist, dass vergessen wird, auch den Kommentar entsprechend zu a ndern. In diesem Fall bleibt ein Kommentar zur uck, der im Widerspruch zum Programmcode steht und verwirrt, statt zu unterst utzen. Daher gilt die wichtige Regel: Beim Andern von Programmcode muss stets auch der Kommentar ge andert werden. In den Java Programmierrichlinien wird diese Regel andersherum gesehen: In general, avoid any comments that are likely to get out of date as the code evolves. Schimpfworte im Kommentar Muss das gesagt werden? Un atige Kommentare u ber die Programmiersprache, das Betriebssystem, die Entwicklungsumgebung, die Projektbedingungen, die Arbeitskollegen oder den Chef sind unprofessionell (und dennoch immer wieder zu nden).
182
Anhang A
A.1
Syntax
//Gibt formatiert auf dem Bildschirm aus System.out.printf(formatstring, wert1, wert2, ...); //Schreibt formatierten Text in einen String String s = String.format(formatstring, wert1, wert2, ...); Es k onnen beliebig viele Wert in einem Rutsch formatiert werden. Wieviele Werte auf welche Weise formatiert werden, h angt vom Formatstring ab.
A.2
Die Regeln f ur den Formatstring stammen aus der Sprache C, in der es einen a hnlichen Befehl (printf) gibt. Die Regeln wurden auer in C++ und Java auch in den wichtigsten Skriptsprachen, wie Perl, Python und Ruby u bernommen. Sprachen mit anderen Regeln sind C#, Pascal und Fortran. Im einfachsten Fall besteht den Formatstring aus einem beliebigen Text, der 1:1 ausgegeben wird.1 Weitere Parameter gibt es nicht. Beispiel:
1 Die Ausgabe erfolgt ohne Zeilenvorschub, entspricht also eher print als println. Zum Zeilenvorschub in printf-Befehlen siehe das Kapitel u ber Escape-Sequenzen.
183
184
System.out.printf("Hello world"); In diesen Text kann man Umwandlungsbefehle einbetten. Ein Umwandlungsbefehl beginnt immer mit einem %-Zeichen und endet mit einem speziellen Umwandlungsbuchstaben. Beispiel: int eu = 2; int ce = 10; System.out.printf("Endpreis: %d Euro %d Cent", eu, ce); //Ausgabe: 2 Euro 10 Cent Der erste Umwandlungsbefehl (%d) holt sich den auf den Formatstring folgenden Parameter (eu), formatiert ihn (in diesem Fall als Ganzzahl) und setzt ihn an genau diese Stelle ein. Der zweite Umwandlungsbefehl macht das gleiche mit dem n achsten Parameter (ce). Der Umwandlungsbuchstabe muss dem Typ des dazugeh origen Parameters entsprechen (ansonsten gibt es einen Laufzeitfehler). M ogliche Umwandlungsbuchstaben sind: Symbol %b %c %d %f %e %s %x M ogliche Typen boolean char byte, short, int, long oat, double oat, double String byte, short, int, long Beispiel true oder false a 200 45.460 4.546e+01 (Exponentialdarst.) Java ist toll d2 (Hexadezimal)
Zwischen dem %-Zeichen und dem Umwandlungsbuchstaben k onnen noch verschiedene weitere Zeichen eingef ugt werden. Damit k onnen viele Eekte er reicht werden. Wir werden uns der Ubersichtlichkeit halber auf drei wichtige beschr anken.
A.2.1
Zwischen %-Zeichen und Umwandlungsbuchstabe wird eine Zahl eingef ugt. Java interpretiert den Betrag der Zahl als Mindestbreite. Ist die Zahl positiv, wird rechtsb undig formatiert, bei negativen Zahlen linksb undig. Beispiele (Leerzeichen werden als wiedergegeben): printf-Befehl .printf("%5d", 123) .printf("%-8d", 123) .printf("%10s", "Hallo") .printf("%-10s", "Hallo") Ausgabe 123 123 Hallo Hallo
185
A.2.2
Bei %f und %e kann im Anschlu an die Mindestbreite noch die Anzahl der Nachkommastellen angegeben werden. Es wird gerundet. Mindestbreite und Nachkommastellen werden durch einen Punkt getrennt. Es kann wahlweise auch eine der beiden Angaben weggelassen werden. Beispiele (Leerzeichen werden als wiedergegeben): printf-Befehl .printf("%.2f", 34.565) .printf("%.5f", 34.565) .printf("%7.2f", 34.565) .printf("%7f", 34.565) .printf("%.3e", 34.565) Ausgabe 34.57 34.56500 34.57 34.565000 3.457e01 Kommentar 2 NKS (Nachkommastellen) 5 NKS Mindestbreite 7, 2 NKS Mindestbreite 7, 6 NKS (default) Exponentialform, 3 NKS
A.2.3
Sollen ganze Zahlen ziernweise bearbeitet werden, ist es sinnvoll, sie vorher auf eine denierte Stellenanzahl zu bringen. Dazu gibt es zwei weitere Formatierungsregeln: Steht vor der Mindestbreite eine 0 (nur sinnvoll bei rechtsb undiger Formatierung), dann wird nicht mit Leerzeichen, sondern mit Nullen aufgef ullt. Steht vor der Mindestbreite ein +, dann beginnen positive Zahlen mit einem +. Beispiele (Leerzeichen werden als printf-Befehl .printf("%03d", 5) .printf("%+03d", 5) .printf("%+03d", -5) .printf("%+3d", 5) .printf("%-+3d", 5) .printf("%+-3d", 5) wiedergegeben):
A.2.4
Oft soll der formatierte Text in einem String gespeichert statt auf dem Bildschirm ausgegeben werden. Dazu verwendet man den Befehl String.format, der wie printf funktioniert. Die Zeile String x = String.format("%03d",5); speichert den String 005 in der Variablen x.
186
A.3
Andere Formatierungsklassen
Generell bietet Java zwei M oglichkeiten an, Zeichenketten zu formatieren: Die Klasse java.util.Formatter und die Klasse java.text.Format. Beide Klassen haben nicht nur ahnliche Namen, sondern auch ahnliche Funktionalit at, allerdings ist die Syntax f ur den Formatierungsstring ganz unterschiedlich: Die Klasse Formatter wird intern von printf benutzt, kann aber auch direkt verwendet werden. Die Regeln f ur den Formatierungsstring entsprechen denen von printf. Die Klasse Format und davon abgeleitete Klassen, wie z.B. DecimalFormat bieten eine andere, Java-spezische Formatierungsm oglichkeit. Ist eine bestimmte Formatierung in einem System nicht m oglich, kann man eventuell auf das andere ausweichen. Der umst andlichere, aber objektorientert sauberere Weg ist Format.
Anhang B
Escape-Sequenzen
Innerhalb von Strings kann man mit den sogenannten Escape-Sequenzen Zeichen darstellen, die entweder u ber die Tastatur nicht zu erreichen sind oder in Strings nicht direkt verwendet werden d urfen.1 Eine Escape-Sequenz beginnt immer mit einem Backslash (\). Die wichtigsten Escape-Sequenzen sind: Sequenz \n \b \t \" \ \\ \u00B1 Bedeutung Neue Zeile (line feed) Backspace Tab Doppeltes Anf uhrungszeichen (") Einfaches Anf uhrungszeichen () Backslash (\) Unicode-Zeichen (hier B1(hex): )
Die Unicode-Zeichen bis 127 entsprechen den ASCII-Zeichen. Diese sind im n achsten Kapitel in einer Tabelle zusammengefasst.
1 Ein Beispiel f ur den zweiten Fall ist das doppelte Anf uhrungszeichen, das als String-Ende interpretiert wird.
187
188
ANHANG B. ESCAPE-SEQUENZEN
Anhang C
ASCII-Zeichen
Einige ASCII-Codes1 kommen so h aug vor, dass man sie auswendig kennen sollte. Es sind dies: Zeichen Leerzeichen Die Zier 0 A a ASCII-Code 32 48 65 97
Die anderen Buchstaben und Ziern ergeben sich aus dem Ziernwert bzw. der Position im Alphabet. Die komplette ASCII-Tabelle sieht wie folgt aus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 0 1 2 3 4 5 6 7 8 9 : ; < = > ? 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @ A B C D E F G H I J K L M N O 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 P Q R S T U V W X Y Z [ \ ] ^ _ 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 a b c d e f g h i j k l m n o 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 p q r s t u v w x y z { | } ~ DEL
! " # $ % & ( ) * + , . /
189
190
ANHANG C. ASCII-ZEICHEN
Anhang D
Verzweigung (if-then-else)
Bedingung ja
Anweisungsblock 1 Anweisungsblock 2
nein
191
192
Wert 2
Anweisungsblock 2
Wert 3
Anweisungsblock 3
Wert 4
Anweisungsblock 4
Wert 5
Anweisungsblock 5
sonst
Anweisungsblock 6
Anweisungsblock
Anweisungsblock
while Bedingung
Anweisungsblock
H aug ndet man bei der for-Schleife statt der DIN-Norm die folgende Variante:
193
Anweisungsblock
D.1.1
Endlosschleife
Anweisungsblock
D.1.2
Aussprung (break)
D.1.3
194
D.2
Start/Stop
Rechteck: Operation
Operation
Unterprogramm nein
Raute: Verzweigung
Bedingung ja
Anhang E
API der Beispielklasse Bruch
Class Bruch
public class Bruch extends java.lang.Object Repr asentiert einen Bruch mit Integer-Werten als Z ahler und Nenner. Invariante: Der Bruch ist immer gek urzt. Der Nenner ist immer gr oer als 0. Author: H. Pug, RZ, RWTH Aachen
Constructor Summary
Bruch(Bruch r) Erzeugt eine Kopie (Klon) des u bergebenen Bruchs (Copy-Konstruktor). Bruch(int zaehler, int nenner) Erzeugt einen Bruch mit dem gegebenen Z ahler und Nenner. Bruch(String s) Erzeugt einen Bruch aus einem String der Form Z ahler/Nenner.
Method Summary
void boolean double int int void void void void void String add(Bruch b) Addiert den u bergebenen Bruch. equals(java.lang.Object o) Gibt zur uck, ob die Z ahler und Nenner zweier Br uche gleich sind. getDoubleWert() Gibt den Wert des Bruchs als double-Wert zur uck. getNenner() Gibt den Nenner des Bruchs zur uck. getZaehler() Gibt den Z ahler des Bruchs zur uck. inverse() Invertiert den Bruch. mult(Bruch r) Multipliziert den Bruch mit dem u bergebenen Bruch. mult(int faktor) Multipliziert den Bruch mit dem angegebenen Faktor. setNenner(int nenner) Setzt den Nenner des Bruchs. setZaehler(int zaehler) Setzt den Z ahler des Bruchs. toString() Gibt String in der Form Z ahler/Nenner zur uck.
195