Ulrike Böttcher, Dirk Frischalowski
Java 5 Programmierhandbuch
Ulrike Böttcher, Dirk Frischalowski
Programmierhandbuch Einstieg und professioneller Einsatz
Böttcher, Ulrike / Frischalowski, Dirk: Java 5 – Programmierhandbuch Einstieg und professioneller Einsatz ISBN 3-935042-63-9
© 2005 entwickler.press, ein Imprint der Software & Support Verlag GmbH
http://www.software-support.biz/ http://www.entwickler-press.de/ Ihr Kontakt zum Lektorat und dem Verlag:
[email protected] Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Korrektorat: Petra Kienle Satz: text & form GbR, Carsten Kienle Umschlaggestaltung: Melanie Hahn Belichtung, Druck und Bindung: M.P. Media-Print Informationstechnologie GmbH, Paderborn. Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.
Inhaltsverzeichnis Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
Teil I 1
Einführung und Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
1.1 1.2
Ein paar Worte zu Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Installation der J2SE 5.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.1 Download der Installationsdateien . . . . . . . . . . . . . . . . . . . . . . . 1.2.2 Installation unter Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.3 Installation unter Linux. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.4 Das JDK deinstallieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2.5 Verwendung der Dokumentation . . . . . . . . . . . . . . . . . . . . . . . . Die Verzeichnisstruktur und wichtige Dateien des JDK . . . . . . . . . . . . . Gängige Abkürzungen im Java-Umfeld . . . . . . . . . . . . . . . . . . . . . . . . . .
21 24 24 25 27 29 30 31 32
Die erste Java-Anwendung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33
2.1 2.2
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eingabe des SourceCodes. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2.1 Ein einfacher Editor – ConTEXT . . . . . . . . . . . . . . . . . . . . . . . . 2.2.2 Grundelemente einer Java-Anwendung in der Übersicht . . . . . . Übersetzen von Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ausführen der Anwendung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der Klassenpfad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Applets mit dem Appletviewer ausführen . . . . . . . . . . . . . . . . . . . . . . . . Verwendung der Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Datenein- und -ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kurzes Glossar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33 34 34 35 38 44 48 49 51 51 52
Grundlegende Sprachelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
3.1
55 55 55 56 57 57 59 59 61 63
1.3 1.4 2
2.3 2.4 2.5 2.6 2.7 2.8 2.9 3
3.2
Elemente eines Programms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.1 Anweisungen, Anweisungsblöcke . . . . . . . . . . . . . . . . . . . . . . . 3.1.2 Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.3 Reservierte Wörter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.4 Literale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.1.5 Bezeichner. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Primitive Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.1 Numerische Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.2 Datentyp Zeichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2.3 Logische Datentypen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 5 Programmierhandbuch
5
Inhaltsverzeichnis
3.3
Variablen und Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.1 Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.2 Wertzuweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.3 Typumwandlungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.3.4 Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Operatoren und Ausdrücke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1 Arithmetische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.2 Vergleichsoperatoren (relationale Operatoren) . . . . . . . . . . . . . 3.4.3 Logische Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.4 Bitweise Operatoren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Steuerung des Programmflusses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.1 if-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.2 if-else-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.3 Der Bedingungsoperator ? : . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.4 switch-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.5 for-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.6 Die verbesserte for-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.7 while-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.8 do-while-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.9 Programmfluss mit break und continue beeinflussen . . . . . . . .
63 63 65 66 68 69 70 72 73 75 77 77 78 81 82 85 87 88 90 92
Klassen, Interfaces und Objekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97
3.4
3.5
4
4.1
4.2 4.3 4.4
4.5 4.6 4.7 4.8 4.9
4.10 4.11
6
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.1 Klassen definieren Baupläne . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.1.2 Objekte sind die konkrete Realisierung des Bauplans . . . . . . . . Einfache Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Objekte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.4.1 Einfache Methoden ohne Parameterübergabe . . . . . . . . . . . . . . 4.4.2 Parameter übergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Konstruktoren und Destruktoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriffsattribute und Sichtbarkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statische Klassenelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufzählungstypen mit Enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vererbung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9.1 Die Klasse Object als Basisklasse aller Klassen . . . . . . . . . . . . 4.9.2 Klassen ableiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9.3 Konstruktoraufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.9.4 Vererbungsketten und Zuweisungskompatibilität . . . . . . . . . . . 4.9.5 Finale Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Adapterklassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97 97 98 99 100 104 105 107 113 118 119 121 127 127 130 132 134 135 136 141
Inhaltsverzeichnis
4.12 4.13 4.14 4.15
4.16 5
5.2 5.3
155 155 157 157 158 158 160
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klasse Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wrapper-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.1 Nützliche Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.2 Auto(un)boxing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.3.3 Bitmanipulation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
163 167 168 169 170 172
Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 7.1 7.2 7.3 7.4 7.5
7.6 8
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.1 Package-Hierarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.2 Benannte und unbenannte Packages . . . . . . . . . . . . . . . . . . . . . . 5.1.3 Zugriffsrechte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5.1.4 Aufteilung einer Anwendung in Packages . . . . . . . . . . . . . . . . . Packages importieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Statischer Import. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Arrays, Wrapper und Auto(un)boxing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 6.1 6.2 6.3
7
142 144 146 147 148 148 151
Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155 5.1
6
Abstrakte Klassen und Methoden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Methoden überschreiben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Polymorphie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Innere, verschachtelte und lokale Klassen . . . . . . . . . . . . . . . . . . . . . . . . 4.15.1 Innere Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.15.2 Verschachtelte Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anonyme Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exceptions behandeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exceptions weitergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufräumen mit finally . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Exceptions auslösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5.1 Exceptions erzeugen und auslösen . . . . . . . . . . . . . . . . . . . . . . . 7.5.2 Exceptions erneut auslösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7.5.3 Exception-Ketten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Exceptions verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
175 177 180 182 183 183 185 186 188
Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 8.1 8.2
8.3
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Informationen zum Einsatz von Assertions . . . . . . . . . . . . . . . . . . . . . . . 8.2.1 Seiteneffekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.2.2 Einsatzgebiete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aktivieren von Assertions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3.1 Übersetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3.2 Ausführen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 5 Programmierhandbuch
191 192 192 193 196 196 196 7
Inhaltsverzeichnis
8.3.3 8.3.4 9
Zeichenkettenverarbeitung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 9.1
9.2
9.3
10
Mit String-Objekten arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 9.1.1 Ein String-Objekt erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 9.1.2 Länge eines Strings und Position einzelner Zeichen . . . . . . . . . 200 9.1.3 Strings verketten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 9.1.4 String-Objekte ändern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 9.1.5 Strings vergleichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202 9.1.6 Zeichenketten manipulieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206 9.1.7 Formatierte Strings erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 9.1.8 Andere Datentypen in einen String konvertieren . . . . . . . . . . . . 208 StringBuilder- und StringBuffer-Objekte verwenden . . . . . . . . . . . . . . . 209 9.2.1 Ein StringBuffer-Objekt erzeugen . . . . . . . . . . . . . . . . . . . . . . . 210 9.2.2 Ein StringBuffer- in ein String-Objekt umwandeln . . . . . . . . . . 210 9.2.3 Daten anhängen und einfügen . . . . . . . . . . . . . . . . . . . . . . . . . . 211 9.2.4 Löschen und Verändern von Zeichen im StringBuffer. . . . . . . . 212 9.2.5 String-Länge und Puffergröße bestimmen . . . . . . . . . . . . . . . . . 212 9.2.6 Vergleich von StringBuffer-Objekten. . . . . . . . . . . . . . . . . . . . . 213 9.2.7 Performance-Steigerung durch die Klassen StringBuffer und StringBuilder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 Formatierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215 9.3.1 Formatierung mithilfe der Klasse Formatter . . . . . . . . . . . . . . . 215 9.3.2 Formatter-Objekt erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 9.3.3 Daten konvertieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216 9.3.4 Weitere Methoden der Klasse Formatter . . . . . . . . . . . . . . . . . . 226 9.3.5 Formatierung von Zahlen über die Klasse NumberFormat . . . . 226
Nützliche Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231 10.1 10.2 10.3
10.4 10.5
8
Verhindern der Einbindung in die *.class-Datei . . . . . . . . . . . . 197 Sicherstellung der Aktivierung. . . . . . . . . . . . . . . . . . . . . . . . . . 198
Datum und Uhrzeit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.1.1 Die Klassen Calendar und GregorianCalendar . . . . . . . . . . . . . Zufallszahlen erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Klasse System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.3.1 Standardeingabe, Standardausgabe . . . . . . . . . . . . . . . . . . . . . . 10.3.2 Zugriff auf Systemeigenschaften und Umgebungsvariablen . . . 10.3.3 Weitere Methoden der Klasse System . . . . . . . . . . . . . . . . . . . . Die Klassen Process, ProcessBuilder und Runtime. . . . . . . . . . . . . . . . . Reguläre Ausdrücke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.5.1 Suchmuster definieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10.5.2 Die Klassen Pattern und Matcher. . . . . . . . . . . . . . . . . . . . . . . . 10.5.3 Reguläre Ausdrücke in Scanner-Objekten anwenden . . . . . . . .
231 231 239 242 242 242 246 248 252 253 257 264
Inhaltsverzeichnis
11
Datei- und Verzeichniszugriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 11.1 11.2
11.3
11.4 12
File-Objekt erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Informationen über Dateien ermitteln . . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2.1 Verzeichnisse, Pfade und Dateinamen . . . . . . . . . . . . . . . . . . . . 11.2.2 Eigenschaften von Dateien bzw. Verzeichnissen bestimmen . . . 11.2.3 Eigenschaften von Dateien setzen. . . . . . . . . . . . . . . . . . . . . . . . Verzeichnisse und Dateien anlegen, löschen und umbenennen . . . . . . . . 11.3.1 Verzeichnisse erstellen und löschen . . . . . . . . . . . . . . . . . . . . . . 11.3.2 Dateien anlegen, löschen und umbenennen . . . . . . . . . . . . . . . . 11.3.3 Temporäre Dateien erzeugen und löschen . . . . . . . . . . . . . . . . . Dateien und Verzeichnisse auflisten lassen . . . . . . . . . . . . . . . . . . . . . . . 11.4.1 URL oder URI eines File-Objekts . . . . . . . . . . . . . . . . . . . . . . .
269 271 271 272 273 273 273 273 276 277 281
Ein- und Ausgabe / Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 12.1 12.2 12.3
12.4
12.5
12.6
12.7
Ein- und Ausgabe auf Standardgeräte . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Stream-Konzept von Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Character-Streams. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3.1 Gemeinsame Methoden der Writer-Klassen . . . . . . . . . . . . . . . . 12.3.2 Gemeinsame Methoden der Reader-Klassen . . . . . . . . . . . . . . . 12.3.3 Ein- und Ausgabe in Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3.4 Ein- und Ausgabe in Character-Arrays und StringBuffer. . . . . . 12.3.5 Pufferung erhöht die Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3.6 Formatierte Ausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3.7 Filter verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.3.8 Datenaustausch zwischen Threads . . . . . . . . . . . . . . . . . . . . . . . Byte-Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.4.1 Gemeinsame Methoden der OutputStream-Klassen . . . . . . . . . . 12.4.2 Gemeinsame Methoden der InputStream-Klassen . . . . . . . . . . . 12.4.3 Formatierte Ausgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.4.4 Ausgeben und Einlesen primitiver Datentypen. . . . . . . . . . . . . . 12.4.5 Verknüpfen mehrerer InputStreams . . . . . . . . . . . . . . . . . . . . . . ObjectStreams. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.5.1 Die Klasse ObjectOutputStream . . . . . . . . . . . . . . . . . . . . . . . . . 12.5.2 Die Klasse ObjectInputStream . . . . . . . . . . . . . . . . . . . . . . . . . . 12.5.3 Änderungen an serialisierbaren Klassen (Versionsverwaltung) . Zip-Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.6.1 Zip-Dateien anlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.6.2 Daten in die Zip-Datei ausgeben. . . . . . . . . . . . . . . . . . . . . . . . . 12.6.3 Inhalt der ZIP-Dateien auslesen . . . . . . . . . . . . . . . . . . . . . . . . . 12.6.4 Die Klasse ZipEntry . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.6.5 Objekte komprimieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dateien mit wahlfreiem Zugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 5 Programmierhandbuch
283 285 289 290 291 292 295 297 302 307 311 311 313 313 314 315 320 321 321 323 328 331 332 333 334 337 340 341
9
Inhaltsverzeichnis
12.8
13
13.2 13.3
13.4 13.5 13.6
13.7
14.5 14.6 14.7 14.8
363 366 373 374 375 375 376 378 381 382 384 385 387 389 392 393
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Type Erasure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wildcards und Bounds. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.4.1 Wildcards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14.4.2 Bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Generische Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Standardcode und Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einschränkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Kovariante Rückgabetypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
397 399 402 404 404 406 408 411 413 414
JAR-Archive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419 15.1 15.2 15.3
10
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.1.1 Das Collection-Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . BitSet . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3.1 Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3.2 Stack. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.3.3 ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Mengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Schlangen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abbildungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.6.1 Hashtable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.6.2 Properties-Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Algorithmen der Klasse Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7.1 Sortieren von Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7.2 Synchronisierte Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7.3 Unveränderliche Collections . . . . . . . . . . . . . . . . . . . . . . . . . . .
Generics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397 14.1 14.2 14.3 14.4
15
346 346 353 356 359
Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363 13.1
14
Das Package java.NIO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.8.1 Die Pufferklassen des NIO. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.8.2 Memory Mapping. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.8.3 Channels. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.8.4 Charsets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Manifest und das Verzeichnis META-INF . . . . . . . . . . . . . . . . . . . . Verwendung des jar-Tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.1 Archive erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.2 Verwendung einer Dateiliste . . . . . . . . . . . . . . . . . . . . . . . . . . .
419 420 422 424 425
Inhaltsverzeichnis
15.4 15.5 16
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Anwendung des Kommandozeilentools . . . . . . . . . . . . . . . . . . . . . . . . . . Dokumentationskommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . API-Schnittstelle von javadoc. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Doclets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Taglets. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
433 435 439 442 443 447
Internationalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449 17.1 17.2 17.3
17.4
18
425 426 426 426 427 428
Javadoc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433 16.1 16.2 16.3 16.4 16.5 16.6
17
15.3.3 Archive aktualisieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.4 Archivinhalte auflisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.5 Archive auspacken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15.3.6 Indexdateien erzeugen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verwendung von Archiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Signieren von Archiven . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Sprach- und Ländereinstellungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zahlen, Texte und Datum formatieren . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.3.1 Zahlen formatieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.3.2 Texte formatieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.3.3 Datums- und Zeitangaben formatieren . . . . . . . . . . . . . . . . . . . . ResourceBundles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.4.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17.4.2 Erzeugen und Verwenden von ResourceBundles . . . . . . . . . . . .
449 449 452 452 454 456 458 458 459
Anwendungen weitergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465 18.1
18.2 18.3
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.1.1 Installation unter Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18.1.2 Installation unter Linux. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Angepasste Installationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Wie werden Anwendungen weitergegeben?. . . . . . . . . . . . . . . . . . . . . . .
465 465 466 466 468
Teil II 19
Einführung in die grafische Programmierung . . . . . . . . . . . . . . . . . . . . . . . . 471 19.1 19.2
20
Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471 Ein einführendes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
Das Abstract Windowing Toolkit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 20.1
Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 20.1.1 Fenster anzeigen und schließen. . . . . . . . . . . . . . . . . . . . . . . . . . 478 20.1.2 Fenstereigenschaften. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
Java 5 Programmierhandbuch
11
Inhaltsverzeichnis
20.2
20.3
20.4
20.5
20.6
21
489 489 490 493 499 500 503 518 518 520 539 549 549 551 552 554 556 558 559 561 561 563 565 565 566 567 567
Swing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 575 21.1 21.2
21.3 21.4
12
Ereignisse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.2.1 Das Delegation Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.2.2 Ereignisklassen und Listener . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.2.3 Implementierung von Listenern . . . . . . . . . . . . . . . . . . . . . . . . . 20.2.4 Ereignisse aktivieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.2.5 Überblick über AWT-Ereignisse . . . . . . . . . . . . . . . . . . . . . . . . 20.2.6 Auf Low-Level-Ereignisse reagieren . . . . . . . . . . . . . . . . . . . . . AWT-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.3.1 Übersicht der AWT-Komponenten. . . . . . . . . . . . . . . . . . . . . . . 20.3.2 Verwendung der Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . 20.3.3 Menüs. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Dialoge. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.4.1 Selbst erstellte Dialoge. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.4.2 Vordefinierte Dialoge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . LayoutManager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.5.1 Das FlowLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.5.2 Das BorderLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.5.3 Das GridLayout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.5.4 Oberflächen gestalten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einfache Grafikprogrammierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.2 Farben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.3 Formen zeichnen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.4 Linien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.5 Rechtecke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.6 Polygone (n-Ecke) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20.6.7 Ellipsen und Ellipsenbogen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grundlagen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fensterklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2.1 Aufbau von Swing-Fenstern . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2.2 Swing-Fenster schließen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2.3 Das Look & Feel – Aussehen der Fenster . . . . . . . . . . . . . . . . . 21.2.4 Interne Fenster . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.2.5 Dialoge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die Model-View-Controller-Architektur . . . . . . . . . . . . . . . . . . . . . . . . . Swing-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4.1 Ein Vergleich mit AWT-Komponenten . . . . . . . . . . . . . . . . . . . 21.4.2 Allgemeine Eigenschaften von Swing-Komponenten . . . . . . . . 21.4.3 Komponenten mit Icons . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
575 575 576 577 578 580 582 588 591 591 593 595
Inhaltsverzeichnis
21.5 22
22.3
22.4 22.5 22.6 22.7
22.8
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Aufbau von Applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.2.1 Ein Applet erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.2.2 Der Lebenszyklus des Applets . . . . . . . . . . . . . . . . . . . . . . . . . . Applets starten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.3.1 Applets aus einer HTML-Datei starten. . . . . . . . . . . . . . . . . . . . 22.3.2 Parameterübergabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.3.3 Der Appletviewer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Informationen zum Applet anzeigen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Applets mit Animationen und Sound . . . . . . . . . . . . . . . . . . . . . . . . . . . . Den Applet-Kontext nutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Plug-Ins verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.7.1 Installation des Plug-Ins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22.7.2 Anpassen der HTML-Dateien. . . . . . . . . . . . . . . . . . . . . . . . . . . Sicherheit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
623 624 625 626 627 627 629 631 631 632 636 641 642 642 643
Drucken. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645 23.1 23.2
23.3
24
598 599 615 616 620
Applets. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623 22.1 22.2
23
21.4.4 Weitere Swing-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4.5 Ausgewählte Swing-Komponenten. . . . . . . . . . . . . . . . . . . . . . . 21.4.6 Swing-Menüs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21.4.7 Die Zwischenablage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Drag & Drop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Drucken über das Java 2 Print API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.2.1 Druckseite aufbauen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.2.2 Druckdokument erstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.2.3 Druckausgabe über die Klasse PrinterJob. . . . . . . . . . . . . . . . . . Drucken mit dem Java Print Service. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.3.1 Attribute festlegen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.3.2 Druckformat auswählen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.3.3 Drucker suchen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23.3.4 Druckdaten bereitstellen und Druck starten . . . . . . . . . . . . . . . . 23.3.5 Druckereignisse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
645 645 645 646 647 649 650 653 654 654 655
JavaBeans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 659 24.1 24.2 24.3 24.4
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . JavaBeans implementieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24.2.1 Eigenschaftsmethoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ereignisse durch JavaBeans auslösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Die BeanBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 5 Programmierhandbuch
659 659 661 665 670
13
Inhaltsverzeichnis
Teil III 25
Debuggen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 675 25.1
25.2 25.3 26
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.1.1 Fehlerklassifizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.1.2 Anwendung eines Debuggers. . . . . . . . . . . . . . . . . . . . . . . . . . . 25.1.3 Weitere Debug-Möglichkeiten . . . . . . . . . . . . . . . . . . . . . . . . . . Der Java Debugger jdb. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Grafische Debugger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25.3.1 JBuilder 2005 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Reflection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 685 26.1 26.2
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Klassenobjekte ermitteln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.2.1 Klassenobjekt über ein Objekt ermitteln . . . . . . . . . . . . . . . . . . 26.2.2 Klassenobjekt über die Klasse ermitteln . . . . . . . . . . . . . . . . . . 26.2.3 Klassenobjekt über den Klassennamen ermitteln . . . . . . . . . . . 26.3 Klasseninstanzen dynamisch erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . 26.4 Informationen über eine Klasse ermitteln . . . . . . . . . . . . . . . . . . . . . . . . 26.4.1 Klassenobjekte besitzen einen Basistyp. . . . . . . . . . . . . . . . . . . 26.4.2 Modifizierer auslesen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.4.3 Variablen ermitteln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.4.4 Informationen zu Methoden. . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.5 Methoden aufrufen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.6 Konstruktoren ermitteln und aufrufen . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.7 Arbeit mit Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.8 Aufzählungen ermitteln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.9 Dynamisches Laden von Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26.10 Proxy-Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
27
27.4 27.5 27.6
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vordefinierte Annotation-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Eigene Annotation-Typen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27.3.1 Deklaration der Annotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27.3.2 Annotations verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Annotations für Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff zur Laufzeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Annotation Processing Tool – apt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
713 715 716 717 719 720 721 724
Logging. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 729 28.1 28.2
14
685 687 687 687 688 688 689 690 691 692 697 700 701 702 705 706 708
Annotations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 713 27.1 27.2 27.3
28
675 675 676 677 678 680 681
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 729 Logger erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 730
Inhaltsverzeichnis
28.3 28.4 28.5 28.6 28.7 29
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Speichern und Laden von Einstellungen . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf die Hierarchie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Reagieren auf Änderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Preferences exportieren und importieren . . . . . . . . . . . . . . . . . . . . . . . . .
745 747 749 753 754
Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 757 30.1 30.2 30.3 30.4 30.5 30.6 30.7 30.8 30.9 30.10
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads über die Klasse Thread erzeugen . . . . . . . . . . . . . . . . . . . . . . . . Threads über das Interface Runnable erzeugen . . . . . . . . . . . . . . . . . . . . Threads unterbrechen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zustände eines Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Prioritäten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Daemon-Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Timer. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Thread-Gruppen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Synchronisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.10.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.10.2 Einfache Synchronisationsmechanismen . . . . . . . . . . . . . . . . . . 30.10.3 Monitore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.10.4 Kooperation zwischen Threads . . . . . . . . . . . . . . . . . . . . . . . . . . 30.10.5 Das Attribut volatile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.10.6 Deadlocks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.11 Datenaustausch zwischen Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30.12 Die Concurrency Utilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
731 734 738 739 742 743
Preferences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 745 29.1 29.2 29.3 29.4 29.5
30
Log-Einträge erzeugen und Log-Level setzen . . . . . . . . . . . . . . . . . . . . . Handler verwenden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Der LogManager. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28.5.1 Konfigurationsdatei. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Filter verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Log4j. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
757 758 761 763 766 768 770 771 774 776 776 776 778 782 787 787 789 792
Netzwerkanwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 795 31.1 31.2 31.3
31.4
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf Netzadressen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Arbeiten mit URLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.3.1 URL-Objekte erzeugen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.3.2 URLs parsen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.3.3 Daten verarbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Socket-Verbindungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.4.1 ClientSockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Java 5 Programmierhandbuch
795 800 804 804 805 805 807 808
15
Inhaltsverzeichnis
31.5
31.6
32
32.4
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XML-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XML-Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.3.1 SAX-Parser. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.3.2 DOM-Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . XSLT-Transformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.4.1 Kommandozeilenversion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.4.2 DOM-Bäume speichern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32.4.3 XML-Dokumente transformieren . . . . . . . . . . . . . . . . . . . . . . .
835 836 838 839 848 854 857 858 859
JDBC – Datenbankzugriff . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 861 33.1
33.2
33.3
33.4
33.5
16
812 817 821 821 823 825 825 830 832
XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 835 32.1 32.2 32.3
33
31.4.2 ServerSockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.4.3 Verwaltung mehrerer paralleler Verbindungen . . . . . . . . . . . . . Datagramme. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.5.1 Client-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.5.2 Server-Anwendungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Java Mail API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.6.1 Mails senden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.6.2 Mails empfangen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.6.3 Anhänge verschicken und empfangen . . . . . . . . . . . . . . . . . . . .
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.1.1 JDBC-Treiber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.1.2 Treibertypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.1.3 Architektur von Datenbankanwendungen . . . . . . . . . . . . . . . . . Einrichten einer Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.2.1 MySQL. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.2.2 Firebird installieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Herstellen der Datenbankverbindung . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.3.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.3.2 JDBC-Treiber laden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.3.3 Die Verbindung herstellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . SQL-Anweisungen einsetzen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.4.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.4.2 Anweisungen ausführen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.4.3 Vorbereitete Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.4.4 Stored Procedures verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . 33.4.5 Batch-Mode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf die Ergebnismengen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.5.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.5.2 Werte auslesen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
861 861 862 864 865 866 867 869 869 870 872 874 874 876 878 880 884 886 886 887
Inhaltsverzeichnis
33.6
33.7
33.8 33.9 34
889 890 891 895 895 896 897 899 901 902 903 904 905
JNDI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 907 34.1 34.2 34.3 34.4
35
33.5.3 Navigation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.5.4 Konfiguration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.5.5 Werte ändern und zurückschreiben . . . . . . . . . . . . . . . . . . . . . . . Transaktionsverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.6.1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.6.2 Transaktionen unter JDBC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.6.3 Isolationsstufen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33.6.4 Sicherungspunkte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zugriff auf Metadaten einer Datenbank . . . . . . . . . . . . . . . . . . . . . . . . . . 33.7.1 Informationen zu den Datenbankelementen . . . . . . . . . . . . . . . . 33.7.2 Informationen zur Ergebnismenge . . . . . . . . . . . . . . . . . . . . . . . Datenbankzugriff über Applets. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fehlersuche in JDBC-Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Benötigte Software . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Namensdienste verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Verzeichnisdienste verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
907 909 910 916
JUnit – Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 919 35.1 35.2 35.3 35.4
Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Testfälle. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . TestSuites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Tests ausführen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
919 921 925 927
Anhang: Die CD zum Buch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 931 Stichwortverzeichns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 933
Java 5 Programmierhandbuch
17
Vorwort Wenn man anfängt über Java-Grundlagen zu schreiben, weis man gar nicht, womit man beginnen soll. Und natürlich sind wir nicht die Ersten, die dieses Thema aufgreifen. Zum Glück bietet das JDK 5.0 eine Menge an neuen Dingen, über die es sich zu schreiben lohnt. Dieses Buch bietet Ihnen einen fundierten und breit gefächerten Einstieg in die Java-Programmierung. Wir haben wichtigen Neuerungen des JDK 5.0 wie Generics oder Annotations eigene Kapitel gewidmet, einige Themen ausführlich erläutert, einige angerissen. Zur Programmierung von grafischen Oberflächen oder den Umgang mit XML haben wir uns bewusst auf eine Einführung beschränkt, um den anderen Themen mehr Aufmerksamkeit zu widmen. Der Anfänger wird über detaillierte Anleitungsschritte zur Lösung eines Problems geführt. Der bereits fortgeschrittene Programmierer erfährt etwas über die Neuerungen des JDK 5.0 und erhält ein Nachschlagewert mit zahllosen Beispielen. Gliederung Das Buch ist in drei Bereiche untergliedert. In den ersten Kapiteln wird die Installation des JDK sowie das Übersetzen und die Ausführung von Java-Anwendungen erläutert, also die Grundlagen für die weiteren Kapitel. Danach lernen Sie den Aufbau von einfachen JavaAnwendungen und wichtige Sprachkonstrukte, wie Alternativen und Schleifen kennen. Es werden häufig verwendete Klassen vorgestellt, die bereits für die Erstellung einfacher JavaAnwendungen benötigt werden, so z.B. für die Ein- und Ausgabe, den Umgang mit Zeichenketten und die Behandlung von Exceptions. Der zweite Teil beschäftigt sich mit dem Aufbau grafischer Anwendungen und Applets. Dazu gehören neben der Anzeige von Fenstern und dem Erzeugen von Grafiken auch die Programmsteuerung über Ereignisse und die automatische Anordnung von Komponenten durch Layoutmanager. Für die Programmierung grafischer Oberflächen wird das AWT sowie auch Swing vorgestellt. Der dritte Teil des Buches beinhaltet weiterführende Themen, wie Multithreading, Netzwerkzugriffe, Reflection, Annotations, Datenbankzugriff über JDBC und die Verarbeitung von XML-Daten. Diese Kapitel können zwar meist unabhängig voneinander bearbeitet werden, allerdings gehen wir immer davon aus, dass die Inhalte der vorhergehenden Kapitel bekannt sind. Dem Einsteiger empfehlen wir, die ersten 18 Kapitel nacheinander durchzuarbeiten. Danach kann er wahlweise den Grafikteil oder einzelne Kapitel der fortgeschrittenen Themengebiete bearbeiten. Kapitelaufbau In jedem Kapitel des Buches wird ein Thema vollständig besprochen, auch wenn besonders zu Beginn noch nicht alle Informationen zu diesem Themengebiet benötigt werden. Als Einsteiger sollte man bei schwierigen Themen nicht verzagen. In den folgenden Kapiteln wird immer häufiger darauf Bezug genommen und damit erschließen sich auch diese Inhalte. Später eignet sich dieser kompakte Aufbau wesentlich besser zum Nachschlagen.
Java 5 Programmierhandbuch
19
Vorwort
Beispiele Alle im Buch beschriebenen Fakten werden durch leicht nachvollziehbare Beispiele untermauert, die über die Konsole und nur mit den Mitteln des JDK ausführbar sind. Dabei wurde das Hauptaugenmerk auf die zu beschreibende Technologie und nicht auf ein besonders umfangreiches Beispiel gelegt. Voraussetzungen Sie können zur Bearbeitung der Beispiele eine Programmierumgebung, wie z.B. Eclipse oder JBuilder einsetzen, die Sie bei der Eingabe und Übersetzung des SourceCodes unterstützt. Dies ist aber weder eine Voraussetzung noch gehen wir darauf näher ein. Als Voraussetzung erwarten wir von Ihnen grundlegende Kenntnisse über die Arbeitsweise des Computers und des Betriebssystems sowie Basiskenntnisse in der Programmierung. Die notwendige Software finden Sie auf der CD, Installationshinweise zu sämtlichen Tools und sonstiger benötigter Software in den entsprechenden Kapiteln. Anregungen und Kontakt Zum Buch wurde eine Webseite von uns eingerichtet, die Sie über die URL http:// www.j2sebuch.de/ besuchen können. Hier finden Sie weitere Informationen, Neuigkeiten und die Möglichkeit, mit den Autoren in Verbindung zu treten. Sagen Sie uns Ihre Meinung zum Buch. Für konstruktive Tipps, Hinweise zu Fehlern und Verbesserungsmöglichkeiten sind wir jederzeit dankbar. Sie erreichen uns unter
[email protected] und
[email protected] sowie unter
[email protected]. Wir bedanken uns beim Software & Support Verlag für die Möglichkeit, unser erstes Buch zu schreiben und bei Sun Microsystems für die stabilen Betas und letztendlich das gelungene Release der J2SE 5.0. Wir wünschen Ihnen viel Spaß und Erfolg beim Lernen und Programmieren mit Java. Ulrike Böttcher und Dirk Frischalowski, Dezember 2004
20
1 1.1
Einführung und Installation Ein paar Worte zu Java
Die Programmiersprache Java gibt es nun seit ca. zehn Jahren und die Anzahl der Entwickler, die Java nutzen, wächst weiter. Wie jede andere Programmiersprache, die sich länger als ein bis zwei Jahre gehalten hat, besitzt Java Anwendungsgebiete, für die es sehr gut geeignet ist, und welche, für die es sich weniger gut eignet. Wenn Sie eine Anwendung für die Ausführung unter mehreren Betriebssystemen benötigen, dann ist Java wahrscheinlich die erste Wahl. Für Echtzeitanwendungen bzw. Anwendungen, die direkt auf die Hardware eines Computers zugreifen, ist Java weniger geeignet (und auch nicht entwickelt worden). Historische Hintergründe und Vergleiche von Java mit anderen Programmiersprachen finden Sie im Web mehr als genug. Ziel dieses Buchs ist es vielmehr, dass Sie einen Einblick in die verschiedenen Bereiche erhalten, die durch die Programmierung mit Java abgedeckt werden. Eine vollständige Übersicht ist in den seltensten Fällen möglich. Es sollte aber nach dem Durcharbeiten eines Kapitels klar sein, wozu die erläuterten Dinge dienen, und ein Basiswissen vorhanden sein, um sich später tiefer in dieses Thema einzuarbeiten. Eigenschaften von Java Der große Vorteil, den Java von Beginn an mitbrachte, war die Plattformunabhängigkeit. Was bedeutet dies? Wenn Sie eine Java-Anwendung entwickeln, können Sie diese auf verschiedenen Betriebssystemen ausführen, obwohl diese technisch völlig unterschiedliche Architekturen besitzen. Als Plattform muss aber nicht nur das Betriebssystem allein betrachtet werden, sondern es kommt auch die Prozessorarchitektur hinzu. So gibt es beispielsweise 32- und 64-bit-Betriebssysteme. Das Zauberwort, das diese Plattformunabhängigkeit ermöglicht, heißt ByteCode. Eine Java-Anwendung wird nicht im Maschinencode weitergegeben (Anweisungen, die direkt für den Prozessor unter einem speziellen Betriebssystem bestimmt sind), sondern in einem Zwischencode, der dann auf der jeweiligen Plattform interpretiert und ausgeführt wird. Die neue Namensgebung Die aktuelle Version mit dem Codenamen Tiger wurde nicht mehr anhand des JDK weiter nummeriert, sondern erhielt die neue Versionsnummer 5.0. Damit lautet der neue Name der Java-Plattform: Java 2 Platform Standard Edition 5.0
oder kurz: J2SE 5.0
Java 5 Programmierhandbuch
21
Ein paar Worte zu Java
Das JDK und das JRE werden demnach benannt mit: JDK 5.0 bzw. J2SE Development Kit 5.0 JRE 5.0 bzw. J2SE Runtime Environment 5.0
Aus dem Kürzel j2sdk wird nun wieder jdk und aus j2re wird jre (z.B. für die Installationsverzeichnisse). Die Versionsnummer 1.5 wird dennoch genutzt, z.B. in: java -version - Anzeige der Version des Interpreters javac -source 1.5 - Übersetzen für eine bestimmte Version java.version - eine Systemeigenschaft @since 1.5 - in der Dokumentation jdk1.5.0 - das Installationsverzeichnis http://java.sun.com/j2se/1.5.0/ - Homepage des JDK
Neuerungen in der J2SE 5.0 Ein kurzer Vergleich der Anzahl der Dateien im Archiv [InstallJDK]\jre\lib\rt.zip zeigt, dass sie von ca. 9352 Dateien im JDK 1.4.2 zu 12867 im JDK 5.0 angestiegen ist. Es hat sich also viel getan, aber was wollen wir sonst auch mit unserem P4 und 1 GB RAM anfangen? Sie könnten sich ja langweilen. Aber mal ernsthaft. Bei der Entwicklung der J2SE 5.0 wurden verschiedene Schwerpunkte gesetzt. Diese liegen zunächst auf einer herausragenden Qualität, der Verbesserung der Performance und Skalierbarkeit, erweiterten Überwachungs- und Managementfunktionen (z.B. Profiling), verbesserten Funktionen für den Desktop Client (z.B. neue Look&Feels), erstmals neuen Spracherweiterungen und nicht zuletzt der Aktualisierung der Tools zum Parsen und Verarbeiten von XML. Eine vollständige Übersicht finden Sie z.B. in der Dokumentation unter [InstallJDK]\docs\relnotes\ features.html. Die Java-Editionen J2SE, J2EE und J2ME Jedes spezielle Einsatzgebiet verlangt nach einer speziellen Software. So wird Java in verschiedenen Editionen für Standardanwendungen (J2SE – Java 2 Platform, Standard Edition), Server-Anwendungen im Unternehmensumfeld (J2EE – Java 2 Platform, Enterprise Edition) und für mobile Geräte wie Mobiltelefone oder PDAs (J2ME – Java 2 Platform, Micro Edition) bereitgestellt. Die Basisfunktionalität befindet sich in der J2SE. Die J2EE setzt mit zusätzlichen Bibliotheken und Tools darauf auf. Die J2ME verwendet dagegen nur eine Teilmenge der J2SE, da die mobilen Geräte nicht über den Speicher und die Rechenleistung gängiger PCs verfügen. Unterschied zwischen dem JDK und dem JRE Das Java Development Kit (bzw. J2SE Development Kit) stellt mit seinen Entwicklungswerkzeugen die Basis zur Entwicklung von Java-Anwendungen und -Applets dar. Es enthält die Bibliotheken zur Übersetzung und Ausführung von Java-Anwendungen. Das Java Runtime Environment ist nur für die Ausführung von Java-Anwendungen gedacht. Es ent-
22
1 – Einführung und Installation
hält deshalb beispielsweise auch nicht den Java-Compiler. Weiterhin ist die Größe des JRE geringer als die des JDK. Möchten Sie Java-Anwendungen auf andere Rechner übertragen, die noch keine Java-Unterstützung besitzen, verwenden Sie das JRE. Internetressourcen Es gibt mittlerweile so viele Informationen zu Java im Internet, dass Sie z.B. über Google keine Probleme haben sollten, etwas Passendes zu finden. Im Folgenden werden deshalb nur einige URLs ohne weitere Kommentare angegeben, die als erste Anlaufstelle dienen können: http://java.sun.com/j2se/1.5.0/ http://java.sun.com/j2se/naming_versioning_5_0.html http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html http://java.sun.com/developer/technicalArticles/releases/j2se15/ http://jcp.org/en/jsr/all http://www.javaworld.com/ http://www-130.ibm.com/developerworks/java/ http://www.javamagazin.de/ http://www.j2sebuch.de/ Konventionen im Buch Wir möchten an dieser Stelle kurz ein paar der im Buch verwendeten Konventionen und Bezeichner vorstellen. Thema
Erläuterung
[InstallJDK]
Das Installationsverzeichnis Ihres JDK werden wir mit diesem Akronym ansprechen. Haben Sie das JDK beispielsweise nach D:\jdk1.5.0 installiert, steht [InstallJDK] genau für diesen Pfad.
Konsole
Jedes Betriebssystem besitzt die Möglichkeit, ein Konsolenfenster anzuzeigen. Darüber können direkt Kommandos ausgeführt werden. Für die Verwendung von Java werden über diese Kommandos zusätzliche Optionen und Dateinamen an die Java-Tools übergeben. Die Konsole wird auch als Eingabeaufforderung oder DOS-Box (unter Windows) oder Shell (unter Linux) bezeichnet.
Java Interpreter
Die Anwendung zum Ausführen einer Java-Anwendung werden wir immer als Java Interpreter bezeichnen. Die dahinter stehende Technik hat aber nichts mehr mit den aus den 90er Jahren bekannten Interpretern zu tun, die ein Programm zeilenweise lesen und in Maschinencode übersetzen. Der korrekte Name dieses Tools ist Java Application Launcher bzw. Java-Anwendungsstarter.
Java 5 Programmierhandbuch
23
Installation der J2SE 5.0 Thema
Erläuterung
Klassen
Wenn von Klassen gesprochen wird, sind in der Regel auch Interfaces und seit der J2SE 5.0 auch Aufzählungstypen gemeint. Werden in bestimmten Fällen ausschließlich Klassen gemeint, wird dies explizit angegeben.
Anzeige der Beispiele
Die abgedruckten Beispiele sind voll lauffähig. Es wurde in der Regel nur die package-Anweisung zu Beginn weggelassen. Diese setzt sich aus der Anweisung package de.j2sebuch. und dem jeweiligen Kapitelnamen zusammen, z.B. de.j2sebuch.kap01.
Ausführen der Beispiele
Die Beispiele nutzen die neuen Features der J2SE 5.0, wo es möglich ist. Sie sind deshalb nicht in jedem Fall auch unter einem anderen JDK ablauffähig.
Benötigte Software
Es wird in der Regel angegeben, wo und wie Sie die benötigte Software beziehen und installieren. Auf der mitgelieferten CD finden Sie im Verzeichnis \Software die meisten der angegebenen Dateien wieder. Sie müssen also nicht zwingend Dateien downloaden, wenn Sie nicht die aktuellsten Versionen benötigen.
1.2 1.2.1
Installation der J2SE 5.0 Download der Installationsdateien
Die Vorgehensweise zum Download der Installationsdateien ist für Windows und Linux gleich. Sie benötigen jeweils eine Installationsdatei für das JDK und eine für die Dokumentation. Beide Dateien sind um die 44 MB groß.
Die Offline-Installationsdateien sowie die Dokumentation liegen immer nur in Englisch vor.
Installationsdateien des JDK downloaden 쐌 Öffnen Sie die URL http://java.sun.com/j2se/1.5.0/download.jsp in Ihrem Browser und klicken Sie im Bereich J2SE 5.0 JDK auf den Link DOWNLOAD JDK. 쐌 Auf der nächsten Seite bestätigen Sie die Lizenzbestimmungen durch das Anklicken von ACCEPT und klicken danach auf CONTINUE. 쐌 Im Bereich WINDOWS PLATFORM wählen Sie den Link WINDOWS OFFLINE INSTALLATION, MULTI-LANGUAGE (JDK-1_5_0-WINDOWS-I586.EXE). Mithilfe dieser Datei können Sie das JDK später auch ohne Internetverbindung installieren. 쐌 Für die Linux-Installationsdateien wählen Sie z.B. den Link LINUX SELF-EXTRACTING FILE (JDK-1_5_0-LINUX-I586.BIN).
24
1 – Einführung und Installation
Dokumentation des JDK downloaden 쐌 Öffnen Sie die URL http://java.sun.com/j2se/1.5.0/download.jsp in Ihrem Browser und klicken Sie im Bereich J2SE 5.0 DOCUMENTATION auf den Link DOWNLOAD. 쐌 Auf der nächsten Seite bestätigen Sie die Lizenzbestimmungen durch das Anklicken von ACCEPT und klicken danach auf CONTINUE. 쐌 Laden Sie die Dokumentation, die im ZIP-Format vorliegt, über den Link JDK-1_5_0-DOC.ZIP.
1.2.2
Installation unter Windows
Sie benötigen für die Installation unter Windows eines der Betriebssysteme Server 2003, XP Prof. mit SP1 (Service Pack 1) bzw. XP Home, 2000 Prof. mit SP3, ME oder 98 (2. Edition). Auf der Festplatte sollten für das JDK ca. 130 MB freier Speicherplatz sein. Die Installation der Dokumentation benötigt dann noch einmal ca. 250 MB. Unter Windows 2000 und XP sind Administratorrechte zur Durchführung der Installation erforderlich. Installation des JDK 쐌 Starten Sie die Installation durch das Ausführen der Datei jdk-1_5_0-windowsi586.exe. 쐌 Bestätigen Sie die Lizenzbedingungen durch das Markieren der Option I ACCEPT ... und klicken Sie auf die Schaltfläche NEXT. 쐌 Ändern Sie gegebenenfalls das Installationsverzeichnis. Wir benutzen zur Installation den Pfad D:\jdk1.5.0. Dadurch verkürzen sich später die Verzeichnisangaben. Im Buch verweisen wir über das Akronym [InstallJDK] wie bereits erwähnt auf das Installationsverzeichnis. Betätigen Sie anschließend die Schaltfläche NEXT. Das Abwählen von Optionen macht in der Regel wenig Sinn, da man früher oder später doch noch das eine oder andere Feature benötigt. 쐌 Die Installation des JDK wird nun durchgeführt. Danach beginnt die Installation des JRE. Dieses wird standardmäßig in das Verzeichnis C:\Programme\Java\jre1.5.0 installiert. Der Pfad sollte so belassen werden. Gegebenenfalls können Sie die Installation der zusätzlichen Sprachdateien deaktivieren, um 20 MB Platz zu sparen. Klicken Sie auf NEXT. 쐌 Damit Applets, die das JDK 5.0 nutzen, im Browser ausgeführt werden können, muss das Java Plug-in installiert werden. Starten Sie jetzt die Installation durch das Betätigen der Schaltfläche NEXT. 쐌 Schließen Sie die Installation durch Klicken auf FINISH ab.
Java 5 Programmierhandbuch
25
Installation der J2SE 5.0 Durch die Installation des JRE wird automatisch der Java Update Service im Hintergrund gestartet. Der Service findet sich im Taskmanager unter dem Namen jusched.exe wieder. Im Java Control Panel können Sie die Verwendung des Update Services deaktivieren. Öffnen Sie dazu das Control Panel über START – EINSTELLUNGEN – SYSTEMSTEUERUNG – JAVA und wählen Sie das Register AKTUALISIERUNG aus. Deaktivieren Sie die Option AUTOMATISCH NACH AKTUALISIERUNGEN SUCHEN. Weiterhin erfolgt eine Installation von Java Web Start, um Anwendungen über das Internet zu laden und zu starten. Standardmäßig wird dazu ein Symbol auf dem Desktop angelegt.
Installation der Dokumentation Um die Dokumentation zu entpacken, können Sie ein beliebiges ZIP-Programm verwenden. Entpacken Sie damit die Datei jdk-1_5_0-doc.zip direkt im Installationsverzeichnis des JDK. Alternativ kopieren Sie die Datei jdk-1_5_0-doc.zip in das Installationsverzeichnis des JDK und entpacken sie über das Kommando .\bin\jar xf jdk-1_5_0-doc.zip
In beiden Fällen wird ein Unterverzeichnis ..\docs erstellt. Konfigurieren des JDK Damit Sie die Tools des JDK jederzeit aufrufen können, sollten Sie das Verzeichnis [InstallJDK]\bin in die PATH-Umgebungsvariable aufnehmen. Im Folgenden wird angenommen, dass das JDK im Verzeichnis D:\jdk1.5.0 installiert wurde. 쐌 Unter Windows 98 fügen Sie in der Datei C:\Autoexec.bat den folgenden Eintrag hinzu. SET Path=%Path%;D:\jdk1.5.0\bin
쐌 Unter den anderen Windows-Versionen wechseln Sie in die Systemsteuerung und wählen das Symbol SYSTEM. Unter dem Register UMGEBUNG bzw. ERWEITERT finden Sie eine Schaltfläche oder einen Bereich zum Setzen der Umgebungsvariablen. Fügen Sie dort den Pfad zum JDK hinzu. Die Vorgehensweise kann abhängig von Ihrer Windows-Version geringfügig abweichen. Test der Installation Zum Test der erfolgreichen Installation und Einrichtung des JDK öffnen Sie eine Konsole. Geben Sie das Kommando java -version
ein. Es sollte die installierte Version 1.5.0 ausgegeben werden. Konnte der Befehl java nicht gefunden werden, haben Sie wahrscheinlich Ihre PATH-Variable nicht korrekt gesetzt.
26
1 – Einführung und Installation
Wechseln Sie nun in das Verzeichnis [InstallJDK]\demo\jfc\TableExample\src, um eines der mitgelieferten Beispiele zu übersetzen. Geben Sie zum Übersetzen das folgende Kommando ein. javac TableExample3.java
War die Übersetzung erfolgreich, werden einige neue Dateien erstellt. Es werden auch zwei Warnungen (Note) ausgegeben, die im Moment aber keine besondere Bedeutung haben. Beachten Sie, dass bei der Übersetzung Groß- und Kleinschreibung eine Rolle spielt. Im nächsten Schritt führen Sie die übersetzte Anwendung aus. Geben Sie dazu das Kommando java TableExample3
ein. Es wird ein neues Fenster geöffnet, das eine Tabelle anzeigt. Schließen Sie es wieder, z.B. über ÇÌ. Herzlichen Glückwunsch! Die Installation war erfolgreich. Um die Installation der Dokumentation zu prüfen, öffnen Sie die Datei index.html im Verzeichnis [InstallJDK]\docs. Es wird die Startseite der Dokumentation gezeigt. Um darin beispielsweise die API-Dokumentation des JDK zu öffnen, klicken Sie auf den Link JAVA 2 PLATFORM API SPECIFICATION.
Einige der Links in der Dokumentation benötigen eine aktive Internetverbindung.
1.2.3
Installation unter Linux
Bei der Linux-Installation werden zahlreiche Systeme unterstützt. Am besten, Sie werfen im Zweifelsfall selbst einen Blick auf die Liste unter http://java.sun.com/j2se/1.5.0/systemconfigurations.html. SuSE 8 und 9 sowie Red Hat 9.0 sind in jedem Fall dabei. Auf der Festplatte sollten für das JDK ca. 130 MB freier Speicherplatz verfügbar sein. Die Installation der Dokumentation benötigt dann noch einmal ca. 250 MB. Sie können das JDK als Nutzer in Ihrem HOME-Verzeichnis installieren oder als Administrator, um es mehreren Anwendern zur Verfügung zu stellen. Beachten Sie, dass einige Distributionen bereits ein JRE installieren, so dass der Java Interpreter java bereits zur Verfügung steht, leider aber in der »falschen« Version. Entweder Sie deinstallieren dieses JRE oder entfernen den Pfad aus der PATH-Variablen. Installation des JDK 쐌 Begeben Sie sich in das Verzeichnis, in welchem Sie das JDK installieren möchten. Vergeben Sie gegebenenfalls noch die Ausführungsrechte für die Datei jdk-1_5_0-linuxi586.bin durch den Aufruf des Kommandos chmod +x jdk-1_5_0-linux-i586.bin
Java 5 Programmierhandbuch
27
Installation der J2SE 5.0
쐌 Starten Sie die Installation über das Kommando ./jdk-1_5_0-linux-i586.bin
쐌 Begeben Sie sich an das Ende der Lizenzbedingungen (seitenweise mit der þLeertasteÿ, Zeilenweise durch Æ, direkt an das Ende mit Ÿ+C) und geben Sie zur Bestätigung den Buchstaben y ein. 쐌 Es wird sofort die Installation gestartet und dabei ein neues Unterverzeichnis jdk1.5.0 erstellt. Im Buch verweisen wir auf das Installationsverzeichnis über das Akronym [InstallJDK]. Installation der Dokumentation Um die Dokumentation zu entpacken, können Sie ein beliebiges ZIP-Programm verwenden. Entpacken Sie damit die Datei jdk-1_5_0-doc.zip direkt im Installationsverzeichnis des JDK. Alternativ kopieren Sie die Datei jdk-1_5_0-doc.zip in das Installationsverzeichnis des JDK und entpacken sie über das Kommando ./bin/jar xf jdk-1_5_0-doc.zip
In beiden Fällen wird ein Unterverzeichnis ..\docs erstellt. Konfigurieren des JDK Damit Sie die Tools des JDK jederzeit aufrufen können, sollten Sie das Verzeichnis [InstallJDK]\bin in die PATH-Umgebungsvariable aufnehmen. Im Folgenden wird angenommen, dass das JDK im Verzeichnis /home/benutzer installiert wurde. Die konkrete Festlegung der PATH-Variablen hängt von Ihrer verwendeten Shell ab. Wir erläutern hier die Vorgehensweise für die Bash-Shell. 쐌 Öffnen Sie in Ihrem HOME-Verzeichnis (dahin wechseln Sie z.B. nach Eingabe des Kommandos cd) die Datei .profile in einem Editor, z.B. joe .profile
쐌 Fügen Sie die Zeilen PATH=$PATH:/home/benutzer/jdk1.5.0/bin export PATH
hinzu. Speichern Sie die Datei (Ÿ+K+X in joe) und melden Sie sich erneut an. Jetzt können Sie die Kommandos java und javac überall verwenden.
28
1 – Einführung und Installation
Test der Installation Zum Test der erfolgreichen Installation und Einrichtung des JDK öffnen Sie eine Konsole. Geben Sie den Befehl java -version
ein. Es sollte die installierte Version 1.5.0 ausgegeben werden. Konnte der Befehl java nicht gefunden werden, haben Sie wahrscheinlich Ihre PATH-Variable nicht korrekt gesetzt. Wird eine andere Version angezeigt, z.B. 1.4.2, müssen Sie diese Version deinstallieren oder den Pfad aus der PATH-Variablen entfernen. Ansonsten lassen sich die Anwendungen nicht ausführen. Wechseln Sie nun in das Verzeichnis [InstallJDK]\demo\jfc\TableExample\src, um eines der mitgelieferten Beispiele zu übersetzen. Geben Sie zum Übersetzen das folgende Kommando ein. javac TableExample3.java
War die Übersetzung erfolgreich, werden einige neue Dateien erstellt. Es werden auch zwei Warnungen (Note) ausgegeben, die im Moment aber keine besondere Bedeutung haben. Beachten Sie, dass bei der Übersetzung Groß- und Kleinschreibung eine Rolle spielt. Im nächsten Schritt führen Sie die übersetzte Anwendung aus. Geben Sie dazu das Kommando java TableExample3
ein. Es wird ein neues Fenster geöffnet, das eine Tabelle anzeigt. Schließen Sie es über das Schließfeld. Herzlichen Glückwunsch! Die Installation war erfolgreich. Um die Installation der Dokumentation zu prüfen, öffnen Sie die Datei index.html im Verzeichnis [InstallJDK]\docs. Es wird die Startseite der Dokumentation gezeigt. Um darin beispielsweise die API-Dokumentation des JDK zu öffnen, klicken Sie auf den Link JAVA 2 PLATFORM API SPECIFICATION.
Einige der Links in der Dokumentation benötigen eine aktive Internetverbindung.
1.2.4
Das JDK deinstallieren
Unter Windows stehen in der Systemsteuerung unter dem Symbol SOFTWARE separate Einträge für das JDK und das JRE zur Deinstallation zur Verfügung. Entfernen Sie gegebenenfalls noch die Einstellungen in der PATH-Variablen. Unter Linux löschen Sie einfach das Verzeichnis der JDK-Installation und entfernen Sie auch hier gegebenenfalls PATH-Einträge aus der Profildatei.
Java 5 Programmierhandbuch
29
Installation der J2SE 5.0
1.2.5
Verwendung der Dokumentation
Die Startseite der HTML-Dokumentation ist die Datei [InstallJDK]\docs\index.html. Von hier aus können Sie zu den Neuerungen der aktuellen Version, den Beschreibungen der Tools sowie zur API-Dokumentation verzweigen. Sie können die API-Dokumentation auch direkt unter [InstallJDK]\docs\api\index.html öffnen. Da Sie diese häufiger benötigen, sollten Sie sich eine Verknüpfung auf dem Desktop anlegen.
Abb. 1.1: Startseite der API-Dokumentation
Die API-Dokumentation besteht aus drei Bereichen. Links oben können die Packages der im darunter liegenden Bereich angezeigten Klassen, Interfaces, Aufzählungstypen und Exceptions ausgewählt werden. Entweder Sie klicken auf den Link ALL CLASSES, um alle Typen anzuzeigen, oder Sie wählen ein Package (z.B. java.applet) aus, um nur die Typen des Package anzuzeigen. Im linken unteren Bereich können Sie auf einen Link klicken, um im rechten Bereich die Dokumentation zu diesem Typ anzuzeigen. Wenn Sie beispielsweise auf die Klasse AbstractList klicken, wird rechts eine Kurzbeschreibung sowie die Beschreibung der in der Klasse enthaltenen Elemente angezeigt. In den Beschreibungen dieses Buchs werden immer nur die wichtigsten Elemente einer Klasse beschrieben. Wenn Sie alle Elemente (Konstanten, Methoden, Konstruktoren etc.) kennen lernen möchten, müssen Sie in jedem Fall in der API-Dokumentation nachschlagen.
30
1 – Einführung und Installation
1.3
Die Verzeichnisstruktur und wichtige Dateien des JDK
Die Verzeichnisstruktur der installierten J2SE ist unter allen Betriebssystemen gleich. Unterschiede gibt es nur in den betriebssystemspezifischen Dateien, die zur Ausführung der Java-Tools und -Anwendungen benötigt werden. Verzeichnisse ..\jdk1.5.0\
Lizenz- und Readme-Dateien, SourceCode der J2SE
..\jdk1.5.0\bin
Anwendungen und Tools der J2SE
..\jdk1.5.0\demo
Beispielanwendungen mit SourceCode
..\jdk1.5.0\docs
Dokumentation (nach separater Installation)
..\jdk1.5.0\include
C-Header-Dateien für JNI und JVMDI
..\jdk1.5.0\jre
Wurzelverzeichnis der Java-Laufzeitumgebung
..\jdk1.5.0\jre\bin
Anwendungen und Bibliotheken der Java-Laufzeitumgebung
..\jdk1.5.0\jre\lib
Archive zur Ausführung von Java-Anwendungen
..\jdk1.5.0\jre\lib\ext
Verzeichnis für Erweiterungen (Archive), die automatisch geladen werden
..\jdk1.5.0\lib
Zusätzliche Dateien für Entwicklungstools, z.B. JBuilder
..\jdk1.5.0\sample
Beispiele für JNLP (Java Network Launching Protocol)
Dateien ..\jdk1.5.0\bin\appletviewer
Ausführen von Applets
..\jdk1.5.0\bin\jar
Archivierungsprogramm
..\jdk1.5.0\bin\java
Java Interpreter (Java Application Launcher), startet eine Java-Anwendung
..\jdk1.5.0\bin\javac
Java-Compiler zum Übersetzen von *.java-Dateien
..\jdk1.5.0\bin\javadoc
Erzeugt Dokumentationen
..\jdk1.5.0\bin\javaw
Startet grafische Java-Anwendungen
..\jdk1.5.0\docs\index.html
Startseite der Dokumentation
..\jdk1.5.0\jre\lib\rt.jar
Archiv, welches die Basisklassen der Laufzeitumgebung enthält
..\jdk1.5.0\lib\dt.jar
Archiv, welches Informationen zu JavaBeans für grafische Entwicklungstools enthält
..\jdk1.5.0\lib\tools.jar
Archiv, welches Hilfsklassen für Tools enthält
Java 5 Programmierhandbuch
31
Gängige Abkürzungen im Java-Umfeld
1.4
Gängige Abkürzungen im Java-Umfeld
AWT
Abstract Window Toolkit
CORBA
Common Object Request Broker Architecture
EJB
Enterprise JavaBeansTM
IDL
Interface Definition Language
IIOP
Internet Inter ORB Protocol TM
J2EE
Java 2 Platform, Enterprise Edition
J2SETM
JavaTM 2 Platform, Standard Edition
JAAPI
Java Accessibility API
JAAS
Java Authentication and Authorization Service
JAXP
Java API for XML Processing
JCF
Java Collection Framework
JCP
Java Community Process TM
JDBC
Java Database Connectivity
JDC
Java Developer Connection
JDI
Java Debug Interface
JFC
Java Foundation Classes
JMX
Java Management Extensions
JNDI
Java Naming and Directory InterfaceTM
JNI
Java Native Interface
JPDA
Java Platform Debugger Architecture
JPLIS
Java Programming Language Instrumentation Services
JRMP
Java Remote Method Protocol
JSR
Java Specification Request
JVM
TM
Java Virtual Machine
JVMDI
Java Virtual Machine Debug Interface
JVMTI
Java Virtual Machine Tool Interface
LDAP
Lightweight Directory Access Protocol
MBeans
Managed Beans
OMG
Object Management Group
ORB
Object Request Broker
RMI
Remote Method Invocation
SDK
Software Developer Kit
SPI
Service Provider Interface
VM
Virtual Machine
32
2
Die erste Java-Anwendung
2.1
Einführung
In diesem Kapitel lernen Sie, wie Sie eine einfache Java-Anwendung erstellen, und legen damit die Grundlage für alle folgenden Kapitel. Es lässt sich hier nicht vermeiden, dass einige Begriffe zunächst noch unverständlich bleiben bzw. nur sehr kurz erläutert werden. Wenn Sie aber tapfer die weiteren Kapitel durcharbeiten, werden auch diese Dinge klarer. Das Erstellen einer Java-Anwendung erfolgt in den folgenden Schritten: 1. Sie schreiben in einem Texteditor ein Java-Programm. Dazu kann ebenso eine Entwicklungsumgebung (auch IDE – Integrated Development Environment) verwendet werden. Das eingegebene Java-Programm (es muss auch nicht immer gleich ein ganzes Programm sein) wird auch als SourceCode oder Quellcode bezeichnet. Wir verwenden im Folgenden den Begriff SourceCode. Die SourceCode-Dateien werden mit der Endung *.java versehen. 2. Nach der Eingabe des SourceCodes wird dieser über einen Compiler in einen vom Betriebssystem unabhängigen Zwischencode (ByteCode) übersetzt. 3. Nach einer erfolgreichen Übersetzung können Sie jetzt die Java-Anwendung über den Java Interpreter ausführen.
TextEditor
SourceCode
Java Compiler
plattformunabhägiger ByteCode
Java Interpreter Hot Spot Engine
Anwendung ausführen
Abb. 2.1: Erstellung einer Java-Anwendung In diesem Buch wird weitestgehend davon ausgegangen, dass Sie den SourceCode mit einem Texteditor erfassen. Wenn Sie über eine IDE wie z.B. Eclipse oder JBuilder verfügen und diese beherrschen, steht deren Verwendung natürlich nichts im Wege. Unser Ziel ist es aber, dass Sie zuerst die Verwendung der Java-Tools ohne weitere Hilfsmittel erlernen. Dies führt dazu, dass Sie später die Arbeitsweise einer IDE besser verstehen und auftretende Fehler meist besser erkennen können.
Java 5 Programmierhandbuch
33
Eingabe des SourceCodes Die folgenden Erläuterungen beziehen sich auf kleine Anwendungen, die nur aus einer *.java-Datei bestehen. Dies reicht zum Kennenlernen der wesentlichen Bestandteile der Sprache Java meist aus. Java-Anwendungen bestehen in der Regel aber aus vielen *.java-Dateien. Wie Sie mit mehreren Dateien arbeiten, erfahren Sie später.
2.2
Eingabe des SourceCodes
Das Erstellen eines Java-Programms erfolgt durch die Eingabe von SourceCode in einem Texteditor. Alternativ kann auch eine IDE verwendet werden, die z.B. eine Hilfe für die Eingabe besitzt.
2.2.1
Ein einfacher Editor – ConTEXT
Der hier vorgestellte Editor ist kostenlos, einfach zu bedienen, hat eine Syntaxhervorhebung und kann einfache Java-Anwendungen übersetzen und ausführen. Er ersetzt keine IDE, die wesentlich mehr Möglichkeiten bietet, ist anfangs aber leichter zu verwenden. 쐌 Laden Sie ConTEXT von der URL http://www.context.cx/download.html. 쐌 Installieren Sie ConTEXT über das Setupprogramm. 쐌 Laden Sie die deutsche Sprachdatei und entpacken Sie diese in das Verzeichnis ..\Language des Installationsverzeichnisses. 쐌 Starten Sie ConTEXT. 쐌 Klicken Sie auf den Menüpunkt OPTIONS – ENVIRONMENT OPTIONS. 쐌 Wählen Sie unter dem Register GENERAL ganz unten im Feld LANGUAGE den Eintrag DEUTSCH aus. 쐌 Starten Sie ConTEXT erneut, um die deutschen Beschriftungen zu erhalten. 쐌 Klicken Sie auf den Menüpunkt EINSTELLUNGEN – UMGEBUNGSEINSTELLUNGEN und öffnen Sie dann das Register AUSFÜHRUNGSTASTEN. 쐌 Klicken Sie auf HINZUF. und geben Sie JAVA ein. Bestätigen Sie mit OK. 쐌 Klicken Sie auf den Untereintrag F9 von JAVA. Geben Sie unter AUSFÜHREN den Text javac ein, unter START IN den Text %p, unter PARAMETER den Text %f, unter SPEICHERN wählen Sie AKTUELLE DATEI BEVOR AUSFÜHRUNG und markieren Sie das Kontrollfeld AUFZEICHNUNG KONSOLENAUSGABE. 쐌 Unter dem Eintrag F10 geben Sie unter AUSFÜHREN den Text java und unter PARAMETER den Text %F an. Die anderen Einstellungen übernehmen Sie von F9. Sie können jetzt Java-Anwendungen durch Betätigen von Ñ übersetzen und über Ò ausführen. Allerdings dürfen Sie dann keine Packages verwenden (wird noch erklärt)! Die Ausgaben des Programms werden im unteren Teil des Editors in einem separaten Fenster angezeigt. Benutzereingaben können hier nicht erfolgen. Die Syntaxhervorhebung für *.java-Dateien wird übrigens erst dann aktiv, wenn Sie die Datei mit der Endung *.java gespeichert haben. Dies sollten Sie also tun, bevor Sie mit der Eingabe beginnen.
34
2 – Die erste Java-Anwendung
2.2.2
Grundelemente einer Java-Anwendung in der Übersicht
Die Beispiele dieses Buchs haben in den ersten Kapiteln immer den im Folgenden erklärten Grundaufbau. Die Elemente des Beispiels werden nur so weit erläutert, dass ein prinzipielles Verständnis für die Arbeitsweise vorhanden ist. Umfangreichere Informationen liefern die entsprechenden Kapitel. Sollten Sie unter Windows arbeiten, schalten Sie die Anzeige der Dateierweiterungen unbedingt ein. Dadurch können Sie besser die erzeugten Dateitypen erkennen (im Windows Explorer Menüpunkt EXTRAS - ORDNEROPTIONEN, Register ANSICHT, Option ERWEITERUNGEN BEI BEKANNTEN DATEITYPEN AUSBLENDEN deaktivieren).
Eine einfache Beispielanwendung Achten Sie bei der Eingabe der Beispiele und der Benennung der Dateien immer auf die korrekte Groß- und Kleinschreibung, da diese unter Java berücksichtigt wird. Listing 2.1: \Beispiele\de\j2sebuch\kap02\Beispiel.java 1) 2) 3) 4) 5) 6) 7) 8) 9) 10) 11) 12) 13)
package de.j2sebuch.kap02; public class Beispiel { public Beispiel() { System.out.println("Hallo Kleinkleckersdorf ..."); } public static void main(String[] args) { new Beispiel(); System.out.println("Meine erste Java-Anwendung ..."); } }
Packages Die erste Zeile erzeugt die Zugehörigkeit der Klasse Beispiel (Zeile 2) zum Package de.j2sebuch.kap02 (Zeile 1). Dies bedeutet, dass sich die Datei Beispiel.java (benannt nach der Klasse Beispiel) in einem Verzeichnis ..\de\j2sebuch\kap02 befinden muss! Der Packagename entspricht also einem Verzeichnisnamen und ermöglicht das Erstellen einer Verzeichnisstruktur für die erstellten Java-Anwendungen. Eine Klasse befindet sich immer in einem bestimmten Package. package de.j2sebuch.kap02.Beispiel;
Java 5 Programmierhandbuch
35
Eingabe des SourceCodes
Klassen In Zeile 2 wird eine Klasse mit dem Namen Beispiel deklariert. Der Inhalt der Klasse wird durch die geschweiften Klammern der Zeilen 3 und 13 eingeschlossen. Da die Klasse das Attribut public besitzt, muss der Dateiname zwingend Beispiel.java lauten. Wird eine Klasse nicht mit public eingeleitet, ist der Dateiname grundsätzlich egal. public class Beispiel
Methoden Das Beispiel enthält weiterhin die beiden Methoden Beispiel() und main(). Methoden erkennen Sie daran, dass ihrem Namen ein Klammerpaar () folgt. In den Klammern können weitere Angaben stehen, wie im Falle der Methode main(). Der Inhalt der Klammern wird als Parameter an die Methode übergeben. Die Festlegung static vor der Methode main() bedeutet, dass Sie diese Methode auch dann aufrufen können, wenn noch kein Exemplar der Klasse Beispiel vorhanden ist. Die Methode ist kurz gesagt immer verfügbar. public Beispiel() { System.out.println("Hallo Kleinkleckersdorf ..."); } public static void main(String[] args) { new Beispiel(); System.out.println("Meine erste Java-Anwendung ..."); }
Die Methode Beispiel() wird als Konstruktor der Klasse Beispiel bezeichnet. Sie erkennen dies daran, dass die Methode den gleichen Namen wie die umschließende Klasse besitzt.
Funktionsweise dieser Anwendung Bei der Ausführung einer Anwendung in Java wird nach einer Methode main() mit dem angegebenen Aufbau gesucht. Dieser Aufbau wird auch als Signatur einer Methode bezeichnet. Die main()-Methode muss sich in der Klasse befinden, die dem Java Interpreter als Parameter übergeben wird (dazu später). Die erste Anweisung dieser Methode new Beispiel();
erzeugt ein Exemplar (Instanz, Objekt) der Klasse Beispiel. Diese Objekterzeugung ist mit dem Bau eines Autos aufgrund eines Bauplans vergleichbar. Jetzt wird die Methode public Beispiel()
36
2 – Die erste Java-Anwendung
ausgeführt. Diese enthält eine Anweisung, die einen Text auf der Konsole ausgibt. System.out.println("Hallo Kleinkleckersdorf ...");
Damit ist die Methode Beispiel() beendet. Anschließend wird die zweite Anweisung der main()-Methode abgearbeitet. Diese erzeugt eine weitere Konsolenausgabe. System.out.println("Meine erste Java-Anwendung ...");
Nun ist das Ende des Java-Programms erreicht. Übersetzen und Ausführen der Beispielanwendung Angenommen, die Datei Beispiel.java befindet sich im Verzeichnis C:\Beispiele\de\ j2sebuch\kap02\. Unter Linux könnte die Verzeichnisstruktur /home/frischa/Beispiele/de/ j2sebuch/kap02 lauten. Wechseln Sie nach dem Öffnen einer Konsole in das Verzeichnis C:\Beispiele. Dann übersetzen Sie die Beispielanwendung. Es entsteht im selben Verzeichnis, in dem sich die Datei Beispiel.java befindet, die Datei Beispiel.class. javac de\j2sebuch\kap02\Beispiel.java
Um die Anwendung auszuführen, verwenden Sie das folgende Kommando. Der Java Interpreter sucht die Klasse Beispiel in der Datei Beispiel.class, die sich im Unterverzeichnis ..\de\j2sebuch\kap02 befindet, ausgehend vom aktuellen Verzeichnis (C:\Beispiel). java de.j2sebuch.kap02.Beispiel
Sollte jetzt die folgende Ausgabe bei Ihnen erscheinen, sieht's gut aus. Hallo Kleinkleckersdorf ... Meine erste Java-Anwendung ...
Formatierung des SourceCodes Zur Formatierung von SourceCode gibt es zahlreiche Möglichkeiten. Dabei kommt es im Prinzip auf zwei Dinge an. Wie erfolgt die Klammersetzung und mit welchem Einzug arbeitet man? Geschweifte Klammern schließen immer einen Anweisungsblock ein. Im Buch werden die Klammerpaare immer untereinander geschrieben. Sie beginnen unter der Anweisung, welcher der Anweisungsblock zugeordnet ist. Für den Einzug der Anweisungen innerhalb der Klammern werden zwei Leerzeichen verwendet. Dadurch wird der SourceCode auch bei umfangreichen Verschachtelungen nicht zu breit. for(int i = 0; i < 10; i++) { System.out.println(i); }
Java 5 Programmierhandbuch
37
Übersetzen von Anwendungen
Ist kein Anweisungsblock nötig, werden die Klammern weggelassen und es wird nur die folgende Anweisung eingezogen. for(int i = 0; i < 10; i++) System.out.println(i);
Alternativ kann z.B. mit einer anderen Klammersetzung und einem Einzug von vier Leerzeichen gearbeitet werden. for(int i = 0; i < 10; i++) { System.out.println(i); }
Namenskonventionen Für die Vergabe von Namen in einer Java-Anwendung bestehen bestimmte Konventionen. Auch wenn diese nicht zwingend eingehalten werden müssen, verbessert ihre Einhaltung die Lesbarkeit des SourceCodes. Dies ist bei Teamarbeit besonders wichtig. Element
Konvention
Packages
Die Bestandteile werden immer in Kleinbuchstaben angegeben und durch Punkte getrennt, z.B. de.j2sebuch.kap02.
Klassen, Interfaces
Beginnen mit einem Großbuchstaben und auch jedes Hauptwort wird mit einem Großbuchstaben begonnen, z.B. StringBuilder.
Methoden
Beginnen klein, jedes Hauptwort wird mit einem Großbuchstaben begonnen, z.B. zeigeFarbe().
Variablen
Beginnen klein, jedes weitere Hauptwort wird mit einem Großbuchstaben begonnen, z.B. roteFarbe.
Konstanten
Werden immer vollständig groß geschrieben, z.B. MEHRWERTSTEUER.
2.3
Übersetzen von Anwendungen
Nach der Eingabe und Überprüfung des SourceCodes muss er übersetzt werden. Diese Aufgabe übernimmt der Java Compiler javac, der sich im ..\bin-Verzeichnis Ihrer JDKInstallation befindet. Haben Sie bei der Eingabe des SourceCodes keinen Fehler gemacht, werden eine oder mehrere *.class-Dateien erzeugt. Diese Dateien enthalten den plattformunabhängigen ByteCode, der nicht mehr mit einem Texteditor bearbeitet oder gelesen werden kann.
38
2 – Die erste Java-Anwendung
Für den Aufruf des Compilers gibt es einige Dinge zu beachten, die im Folgenden aufgeführt werden: 쐌 Öffnen Sie eine Konsole. 쐌 Die einfachste Möglichkeit, eine *.java-Datei zu übersetzen, ist deren Angabe nach dem Aufruf von javac, z.B. javac Datei1.java
oder um alle Dateien eines Verzeichnisses zu übersetzen: javac *.java
oder um alle Dateien des Package de.j2sebuch.kap02 zu übersetzen: javac de\j2sebuch\kap02\*.java
쐌 Verwenden Sie bei der Angabe der *.java-Dateien keine Verzeichnisangaben, müssen sich diese Dateien im aktuellen Verzeichnis befinden. Anderenfalls können Sie durch die Angabe eines relativen (de\j2sebuch\kap02\Beispiel.java) oder absoluten (D:\Beispiele\de\j2sebuch\kap02\Beispiel.java) Pfads auch andere Speicherorte angeben. 쐌 Die Endung der Dateien muss zwingend auf .java lauten. Außerdem wird die Groß- und Kleinschreibung der Dateinamen beachtet. 쐌 Nach einer erfolgreichen Übersetzung befinden sich eine oder mehrere neue Dateien im Verzeichnis der *.java-Datei, die den gleichen Dateinamen, aber die Endung *.class besitzen. Standardmäßig erzeugt der Compiler die *.class-Dateien immer im selben Verzeichnis, in dem sich auch die *.java-Dateien befinden. 쐌 Wenn Sie den Compiler ohne weitere Angaben aufrufen, wird eine Hilfe zum Aufruf und den möglichen Optionen ausgegeben. 쐌 Der Compiler wird folgendermaßen aufgerufen: javac [Optionen] Java-Dateien | Parameterdatei
Wenn Sie ConTEXT korrekt eingerichtet haben, können Sie Java-Anwendungen auch darüber erzeugen. Allerdings ist die Übergabe von Optionen etc. dann nicht ohne Änderungen der Einstellungen im Editor möglich.
Java 5 Programmierhandbuch
39
Übersetzen von Anwendungen
Compiler-Optionen Beim Aufruf des Compilers können zahlreiche Optionen angegeben werden. Die wichtigsten werden im Folgenden erläutert. Option
Erläuterung
-classpath pfade
Hiermit können Sie die Verzeichnisse und Archive festlegen, in denen der Compiler nach benötigten Informationen (Klassen, Interfaces, Aufzählungstypen) suchen soll. Mehrere Einträge werden durch ein Semikolon (Windows) oder einen Doppelpunkt (Linux) getrennt. Sie überschreiben damit die CLASSPATH-Variable (dazu später).
-d verzeichnis
Geben Sie das Zielverzeichnis für die übersetzten *.class-Dateien an. Wenn Sie mit Packages arbeiten, werden die entsprechenden Unterverzeichnisse automatisch in diesem Verzeichnis erzeugt. Das Zielverzeichnis muss bereits bestehen, da es nicht automatisch erzeugt wird.
-deprecation
Es wird die Stelle im Programm genauer gekennzeichnet, die eine veraltete Klasse, Methode oder anderes verwendet.
-g, -g:none
Verwenden Sie die Option -g, um Debuginformationen in die *.class-Datei aufzunehmen oder -g:none, um dies zu verhindern. Sie können durch die Angabe der Debuginformationstypen (source – Dateiinfos, lines – Zeilennummern, vars – lokale Variablen), die auch einzeln verwendet werden können, die Erzeugung bestimmter Informationen steuern. Standardmäßig wird bei der Angabe von -g nur source und lines verwendet.
-g:source,lines,vars
-help -X
Es werden die vom Compiler unterstützten Optionen und die Syntax für deren Aufruf ausgegeben. Die Option -X zeigt die Optionen an, die nicht zum Standardaufruf gehören.
-nowarn
Es werden keine Warnungen ausgegeben.
-source version
Sie können dem Compiler mitteilen, mit welcher Version des JDK der SourceCode erstellt wurde. Standardmäßig ist die Version 1.5 bzw. 5 voreingestellt, so dass alle neuen Spracheigenschaften unterstützt werden. Besitzen Sie aber ältere Quelltexte, die z.B. das Schlüsselwort assert als Bezeichner nutzen, müssen Sie diese mit der Version 1.3 übersetzen.
40
2 – Die erste Java-Anwendung Option
Erläuterung
-sourcepath
Diese Option arbeitet im Prinzip wie -classpath. Hier wird jedoch vorrangig nach *.java-Dateien gesucht, die dann übersetzt werden. In Verbindung mit der Option -d können Sie die Speicherorte der *.java-Dateien und der *.class-Dateien trennen.
-target
Um Bytecode für die Ausführung unter einer älteren JVM zu erzeugen, nutzen Sie die Option target. Der SourceCode darf dann aber keine neuen Sprachfeatures nutzen. Außerdem müssen Sie die Option -source angeben, die keine neuere Version als die unter -target verwendete sein darf.
-verbose
Es werden umfangreichere Meldungen zum Übersetzungsvorgang ausgegeben wie die geladenen Klassen oder der verwendete Klassenpfad.
-Xlint
Es werden umfangreichere Informationen für die Anzeige von Warnungen ausgegeben. Diese Option schließt z.B. die Option -deprecation ein.
In Java werden Klassen, Interfaces, Methoden, Exceptions und Variablen als deprecated (veraltet) gekennzeichnet, wenn die Unterstützung in einer der nächsten Versionen des JDK entfallen kann. Sie werden nur noch aus Gründen der Kompatibilität mit älteren Versionen geduldet. Um alle als deprecated gekennzeichneten Elemente anzuzeigen, öffnen Sie die API-Dokumentation und klicken im rechten oberen Bereich auf den Link DEPRECATED.
Arbeit mit Parameterdateien Wenn Sie sehr viele einzelne *.java-Dateien übersetzen möchten und zahlreiche Optionen verwenden, ist die Verwendung von Parameterdateien sinnvoll. Fügen Sie die Optionen und die Namen der *.java-Dateien, durch Leerzeichen oder Zeilenumbrüche getrennt, in eine Textdatei ein. Der Name der Datei kann beliebig sein. Beachten Sie weiterhin: 쐌 Sie dürfen keine Wildcardzeichen (Platzhalter) wie * oder ? in den Dateinamen verwenden. 쐌 Die Dateinamen werden relativ zum Verzeichnis interpretiert, in dem der Compiler aufgerufen wird, nicht zum Verzeichnis, in dem sich die Parameterdatei befindet. 쐌 Die Dateien werden mit dem Zeichen @ vor dem Dateinamen an den Compiler übergeben. Die erste Datei Optionen.txt enthält beispielsweise die folgenden Optionen: -classpath . -d classes
Java 5 Programmierhandbuch
41
Übersetzen von Anwendungen
Und die zweite Datei Dateien.txt enthält die zu übersetzenden *.java-Dateinamen: de\j2sebuch\kap02\Beispiel.java de\j2sebuch\kap02\Beispiel2.java
Rufen Sie jetzt den Compiler folgendermaßen auf: javac @Optionen.txt @Dateien.txt
Abhängige *.java-Dateien übersetzen Komplexere Anwendungen bestehen in der Regel aus sehr vielen SourceCode-Dateien. Wenn Sie nur eine Datei bei der Übersetzung angeben, sucht der Compiler nach allen Dateien (bzw. Klassen), die von dieser Datei verwendet werden. Dabei können mehrere Fälle auftreten: 쐌 Findet er nur die benötigte *.java-Datei, wird diese ebenfalls übersetzt. 쐌 Findet er nur eine *.class-Datei, wird diese verwendet. Da kein Quelltext vorliegt, kann er auch nichts übersetzen. 쐌 Findet er eine *.java- und eine *.class-Datei, überprüft er, ob die *.java-Datei neuer als die *.class-Datei ist. Dies würde bedeuten, dass nach der letzten Übersetzung Änderungen am SourceCode vorgenommen wurden. In diesem Fall wird die *.java-Datei erneut übersetzt, anderenfalls wird die *.class-Datei verwendet. Getrennte Verwaltung des SourceCodes und der übersetzten Dateien Für die getrennte Verwaltung des SourceCodes und der übersetzten *.class-Dateien gibt es viele Gründe. So reicht z.B. für die Datensicherung der SourceCode aus. Wenn Sie Anwendungen ausliefern, werden dagegen in der Regel keine SourceCode-Dateien mitgegeben. Angenommen, der SourceCode Ihrer Anwendung liegt im Verzeichnis ..\Beispiele\de\ j2sebuch\kap02. Weiterhin benötigen Sie einige Zusatzdateien im Verzeichnis ..\Beispiele\ de\j2sebuch\util. Ihr aktuelles Verzeichnis ist ..\Beispiele. Sie möchten nun die *.classDateien für beide Verzeichnisse in einem neuen Verzeichnispfad speichern. Wählen Sie die Option -sourcepath, um den Pfad zu den SourceCode-Dateien, hier ..\de\j2sebuch\util, zu setzen. Über die Option -d geben Sie das Zielverzeichnis für die *.class-Dateien an. javac -sourcepath . -d dist de\j2sebuch\kap02
Beispiele Die Datei Beispiel.java wird übersetzt. javac de\j2sebuch\kap02\Beispiel.java
42
2 – Die erste Java-Anwendung
Die erzeugten *.class-Dateien werden nun entsprechend der Package-Struktur im Verzeichnis ..\Ausgabe erstellt. Das Verzeichnis ..\Ausgabe muss bereits existieren. Außerdem werden keine Warnmeldungen ausgegeben. javac -d Ausgabe -nowarn de\j2sebuch\kap02\Beispiel.java
Da der SourceCode keine speziellen Spracheigenschaften der J2SE 5.0 besitzt, wird er zusätzlich mit der Version 1.4 des JDK übersetzt und kann auch mit diesem ausgeführt werden. javac -source 1.4 -target 1.4 de\j2sebuch\kap02\Beispiel.java
Was tun, wenn die Übersetzung fehlschlägt? Bei der Übersetzung einer Java-Anwendung können verschiedene Probleme und Fehler auftreten. Die häufigsten werden im Folgenden erläutert. 1. Eine solche oder ähnlich lautende Fehlermeldung weist darauf hin, dass der Pfad zum Verzeichnis [InstallJDK]\bin nicht der PATH-Variablen hinzugefügt wurde, so dass der Compiler nicht gefunden wird. Es kann natürlich auch sein, dass Sie das JDK noch nicht installiert haben. Der Befehl "javac" ist entweder falsch geschrieben oder konnte nicht gefunden werden.
2. Befindet sich in der Datei eine Klasse mit dem Zugriffsattribut public, muss der Name der Klasse mit dem Dateinamen übereinstimmen. Die gezeigte Fehlermeldung besagt, dass die Klasse Beispiel2 heißt, die Datei aber Beispiel.java. de\j2sebuch\kap02\Beispiel.java:3: class Beispiel2 is public, should be declared in a file named Beispiel2.java public class Beispiel2 ^ 1 error
3. In Zeile 5 der Datei Beispiel.java befindet sich ein Syntaxfehler. Die betreffende Stelle wird mit dem Zeichen ^ markiert. de\j2sebuch\kap02\Beispiel.java:5: cannot find symbol symbol : class voi location: class de.j2sebuch.kap02.Beispiel public static voi main(String[] args) ^ 1 error
Java 5 Programmierhandbuch
43
Ausführen der Anwendung
4. Die zu übersetzende Datei de\j2sebuch\kap02\Beispiel3.java existiert nicht. error: cannot read: de\j2sebuch\kap02\Beispiel3.java 1 error
5. Das in der import-Anweisung angegebene Package existiert nicht. de\j2sebuch\kap02\Beispiel.java:3: package xyz does not exist import xyz.*; ^ 1 error
6. Einige Texteditoren hängen an eine Datei automatisch eine bestimmte Endung an, z.B. *.txt. Eine gespeicherte Datei heißt dann nicht Beispiel.java, sondern Beispiel.java.txt. Diese Datei wird dann natürlich vom Compiler nicht gefunden.
2.4
Ausführen der Anwendung
Nachdem Sie Ihre Anwendung erfolgreich übersetzt haben, kann sie ausgeführt werden. Dazu kommt der Java Application Launcher (Anwendungsstarter) java zum Einsatz. Er startet die JVM (Java Virtual Machine), die den ByteCode der *.class-Dateien ausführt. Die JVM benimmt sich dabei wie ein virtueller Computer, der immer die gleichen Eigenschaften besitzt. Dadurch ist es möglich, dass ein und dasselbe Programm auf verschiedenen Betriebssystemen identisch abläuft. Da der Name Java Application Launcher etwas lang ist, wird er in diesem Buch als Java Interpreter bezeichnet. Dieser hat aber mit der Arbeitsweise des Java Interpreters der ersten Versionen des JDK nicht viel gemeinsam.
Auch bei der Verwendung des Java Interpreters gibt es einige Punkte zu beachten. 쐌 Zum Starten einer Java-Anwendung muss dem Interpreter die Klasse übergeben werden, die eine Methode mit der Signatur public static void main(String[] args)
쐌 쐌 쐌 쐌
44
enthält. Die Klasse selbst muss das Zugriffsattribut public besitzen. Die Endung *.class darf nicht angegeben werden. Statt der Verzeichnistrennzeichen werden Punkte verwendet. Es wird zwischen Groß- und Kleinschreibung unterschieden. Der Aufruf ohne weitere Parameter gibt die möglichen Optionen und die Syntax für den Aufruf aus.
2 – Die erste Java-Anwendung
쐌 Die nach dem Klassennamen angegebenen Argumente werden an die Anwendung als Parameter übergeben. 쐌 Der Interpreter wird folgendermaßen aufgerufen: java [Optionen] Java-Klasse [Argumente]
Interpreter-Optionen Beim Aufruf des Interpreters können zahlreiche Optionen angegeben werden. Die wichtigsten werden im Folgenden erläutert. Option
Erläuterung
-client -server
Es wird die Verwendung der Hot Spot Client VM (Standardeinstellung) bzw. der Hot Spot Server VM aktiviert.
-classpath pfade -cp pfade
Wie beim Compiler setzen Sie hier die Pfade zu Klassen und Archiven. Beim Interpreter ist zusätzlich die verkürzte Option -cp verfügbar.
-DName=Wert
Bestimmte Systemeigenschaften für das Ausführen der Anwendung legen Sie mittels der Option –D fest. So können Sie z.B. das Verzeichnis der Standarderweiterungen setzen: java -Djava.ext.dirs=C:\Extensions
-enableassertions -ea -disableassertions -da
Die Interpretation von Assertions wird durch die beiden ersten Optionen aktiviert und durch die beiden letzten Optionen deaktiviert (Standardeinstellung). Durch das Mischen der Optionen können Sie diese klassenweise (de)aktivieren.
-jar
Es wird eine Anwendung ausgeführt, die sich in einem Archiv befindet, z.B. java -jar MeineAnwendung.jar.
-verbose
Es werden umfangreichere Meldungen beim Ausführen der Anwendung ausgegeben, z.B. die geladenen Klassen.
-showversion
Es wird die Versionsnummer angezeigt und die Ausführung fortgesetzt.
-version
Es wird die Versionsnummer angezeigt und die Ausführung beendet.
-? -help -X
Es werden die vom Interpreter unterstützten Optionen und die Syntax für dessen Aufruf ausgegeben. Die Option -X zeigt die Optionen an, die nicht zum Standardaufruf gehören.
Java 5 Programmierhandbuch
45
Ausführen der Anwendung Option
Erläuterung
-Xms[n] -Xmx[n] -Xss[n]
Es wird in der angegebenen Reihenfolge die Startgröße bzw. die Maximalgröße des Speicherpools sowie die Stackgröße für Threads festgelegt. Die Angabe erfolgt in Vielfachen von 1024 und muss im Falle von -Xms größer als 1 MB (Standard ist 2 MB) und im Falle von -Xmx größer als 2 MB (Standard ist 64 MB) sein. Die Angaben können in Kilobyte (k), Megabyte (m) oder in Byte (-) erfolgen, z.B. java -Xms3m java -Xms3072k java -Xms3145728 Diese Optionen sind für Anwendungen interessant, die z.B. sehr speicherintensive Grafiken anzeigen oder Berechnungen durchführen.
-Xprof
Es wird der interne Profiler aktiviert. Damit können Sie beispielsweise die Aufrufhäufigkeit von Methoden sowie die Ausführungsdauer ermitteln.
Unterschied zwischen java und javaw Beide Interpreter führen Java-Anwendungen aus. Java verwendet dabei immer ein Konsolenfenster und gibt Ausgaben darin aus. Das Fenster bleibt geöffnet, solange die Anwendung läuft, und kann in dieser Zeit nicht für die Ausführung anderer Kommandos genutzt werden. Wurde das Fenster durch die Ausführung von java geöffnet, wird es nach dem Beenden der Anwendung wieder geschlossen. Javaw zeigt kein Konsolenfenster an bzw. kehrt nach dem Aufruf sofort zurück. Das heißt, Sie können ein bereits geöffnetes Konsolenfenster nach dem Aufruf von javaw für die Eingabe weiterer Befehle nutzen. Tritt beim Aufruf von javaw ein Fehler auf, wird ein Dialogfenster angezeigt. Javaw wird normalerweise zum Starten grafischer Anwendungen verwendet. Was tun, wenn die Ausführung fehlschlägt? Bei der Ausführung einer Java-Anwendung können verschiedene Probleme und Fehler auftreten. Die häufigsten werden im Folgenden erläutert. 1. Dem Java Interpreter muss nur der Klassenname, nicht der Dateiname übergeben werden. Die Endung *.class muss deshalb weggelassen werden, z.B. java Beispiel statt java Beispiel.class. Es kann auch sein, dass Sie den Klassennamen falsch geschrieben haben oder die Klasse nicht existiert. Exception in thread "main" java.lang.NoClassDefFoundError: de/j2sebuch/kap02/Beispiel/class
46
2 – Die erste Java-Anwendung
2. Die Klasse wurde mit der package-Anweisung de.j2sebuch.kap03 erstellt, befindet sich aber im Verzeichnis ..\de\j2sebuch\kap02. Der Pfad und der Package-Name müssen aber übereinstimmen. Exception in thread "main" java.lang.NoClassDefFoundError: de/j2sebuch/kap02/Beispiel (wrong name: de/j2sebuch/kap03/Beispiel/Beispiel)
3. Sie haben dem Interpreter eine Klasse übergeben, die keine Methode main() mit der Signatur public static void main(String[] args) besitzt. Exception in thread "main" java.lang.NoSuchMethodError: main
Beispiele Beim einfachsten Aufruf wird dem Interpreter nur der Name der Klasse übergeben, die über eine main()-Methode verfügt. Die folgende Klasse verwendet keine package-Anweisung. java Beispiel
Üblicherweise werden Klassen in Packages verwaltet. In diesem Fall wird beim Aufruf des Interpreters die Verzeichnisstruktur als Package-Name vor dem Klassennamen angegeben. Die einzelnen Bestandteile werden durch Punkte getrennt. Befindet sich die Klasse Beispiel ausgehend vom aktuellen Verzeichnis unter ..\de\j2sebuch\kap02, wird der Interpreter folgendermaßen aufgerufen: java de.j2sebuch.kap02.Beispiel
Als Optionen werden nun der Klassenpfad und das Verzeichnis der Erweiterungen gesetzt. Außerdem wird die Anzeige der Meldungen aktiviert. java -classpath .;C:\Archive -Djava.ext.dirs=C:\Extensions -verbose Beispiel
Eine spezielle Variante ist das Starten einer Java-Anwendung aus einem Archiv heraus. Diese Möglichkeit wird genauer im Kapitel zu JAR-Dateien erläutert. java -jar MeineAnwendung.jar
Java 5 Programmierhandbuch
47
Der Klassenpfad
2.5
Der Klassenpfad
Eine der häufigsten Fehlerquellen, nicht nur bei Neulingen, ist ein fehlerhafter Klassenpfad (CLASSPATH). Wenn der Java Compiler und der Java Interpreter nach weiteren Klassen suchen, die Ihre Anwendung benötigt, verwenden sie eine festgelegte Strategie. 1. Zuerst wird in den so genannten Bootstrap-Klassen gesucht. Dies sind die Klassen, die standardmäßig in der J2SE zur Verfügung stehen. Diese werden immer automatisch gefunden und befinden sich zum Großteil im Archiv [InstallJDK]\jre\lib\rt.jar. 2. Jetzt wird automatisch in den Erweiterungsklassen im Verzeichnis ..\jre\lib\ext gesucht. Diese liegen hier immer als JAR-Archiv vor. 3. Zuletzt wird in benutzerdefinierten Pfaden, dem Klassenpfad, gesucht. Für die Festlegung des Klassenpfads gibt es vier Möglichkeiten: 1. Geben Sie keinen Klassenpfad an, werden Klassen nur ausgehend vom aktuellen Verzeichnis gesucht. 2. Definieren Sie eine Umgebungsvariable CLASSPATH, welche die Namen von Verzeichnissen und Archiven enthält, z.B. SET CLASSPATH=.;C:\MeineKlassen;C:\MeineArchive\Archiv1.jar
bzw. für Linux set CLASSPATH=.:/home/meier/MeineKlassen export CLASSPATH
Unter Windows werden mehrere Pfade durch ein Semikolon, unter Linux durch Doppelpunkte getrennt. Der Punkt kennzeichnet das aktuelle Arbeitsverzeichnis, das damit immer zum Klassenpfad gehört. Die Definition der Variablen CLASSPATH überschreibt den Standardwert (das aktuelle Verzeichnis). Sie können den Inhalt der Umgebungsvariablen auch wieder entfernen. Setzen Sie dazu den Klassenpfad in der betreffenden Konsole über set CLASSPATH=
Unter Windows können Sie die Umgebungsvariablen auch dauerhaft in den Systemeigenschaften setzen bzw. unter Linux in den Benutzereinstellungen der Datei .profile. 3. Geben Sie bei der Verwendung der JDK-Tools die Option -classpath an. Sie überschreiben damit wiederum die Einstellungen der Umgebungsvariablen CLASSPATH. Dies ist die bevorzugte Vorgehensweise, da Sie damit für jede Anwendung einen angepassten Klassenpfad verwenden können. java -classpath .;C:\MeinArchiv
48
2 – Die erste Java-Anwendung
4. Verwenden Sie beim Aufruf des Interpreters die Option -jar, werden alle Klassenpfaddefinitionen überschrieben. java -jar MeinArchiv.jar
Bei der Angabe mehrerer Verzeichnisse bzw. Archive im Klassenpfad werden diese der Reihenfolge nach durchsucht. Wird die gesuchte Datei gefunden, werden die weiteren Angaben ignoriert.
Beispiel Angenommen, Sie besitzen die folgende Verzeichnisstruktur und die Klasse Beispiel befindet sich im Package de.j2sebuch.kap02. C:\Beispiele\de\j2sebuch\kap02\Beispiel.class
Wenn Sie sich im Verzeichnis C:\Beispiele befinden, können Sie die Standardeinstellung des Klassenpfads nutzen, in der das aktuelle Verzeichnis verwendet wird. Ausgehend von diesem Verzeichnis wird die Klasse Beispiel im Package de.j2sebuch.kap02 gesucht. Befinden Sie sich im Verzeichnis C:\Temp, können Sie nicht die Standardeinstellung nutzen, da die Klasse darüber nicht gefunden wird. Setzen Sie in diesem Fall die Umgebungsvariable CLASSPATH auf den Wert C:\Beispiele. Dann lässt sich die Anwendung von jeder Stelle innerhalb des Verzeichnissystems starten. Der Interpreter wird den Klassenpfad auswerten und ausgehend von dessen Einträgen nach der Klasse Beispiel suchen. Er nimmt also den Pfad C:\Beispiele, hängt die Verzeichnisstruktur an, die sich durch die Angabe des Package ergibt (C:\Beispiele\de\j2sebuch\kap02) und findet dort die Klasse Beispiele. Alternativ können Sie den Interpreter auch mit der Option -classpath aufrufen, z.B. java -classpath C:\Beispiele de.j2sebuch.kap02.Beispiel
2.6
Applets mit dem Appletviewer ausführen
Applets sind eine besondere Form einer Java-Anwendung. Sie werden nicht über den Java Interpreter gestartet, sondern über eine HTML-Seite in einem Web-Browser geladen und dort angezeigt. Der Nachteil beim Testen von Applets in einem Browser ist, dass der Browser das aktuelle JDK über ein Plug-in unterstützen muss und die Fehlersuche umständlicher ist. Zum Test eines Applets können Sie deshalb das Tool appletviewer verwenden. Der Aufruf des Appletviewers erfolgt über das Kommando appletviewer Dateiname.html
Java 5 Programmierhandbuch
49
Applets mit dem Appletviewer ausführen
Es wird also nicht die *.class-Datei des Applets, sondern die HTML-Seite, die das betreffende Applet lädt, als Parameter angegeben. Beispiel Wechseln Sie in das Verzeichnis ..\Beispiele\de\j2sebuch\kap02 und übersetzen Sie das Applet über javac AppletBeispiel.java
Rufen Sie danach den Appletviewer auf. appletviewer AppletBeispiel.html
Es wird ein Fenster mit einem grünen Hintergrund und der Aufschrift Willkommen geöffnet. Wenn Sie das Fenster schließen, wird das Applet beendet. Was tun, wenn die Ausführung fehlschlägt? Auch bei der Anwendung des Appletviewers kommt es schnell einmal zu einer Fehlermeldung. 1. Wenn Sie beim Appletviewer statt der benötigten *.html-Datei die Klasse des Applets ohne die Endung *.class angeben, erhalten Sie die folgende Ausgabe. E/A-Ausnahme beim Lesen: C:\BEISPI~1\de\j2sebuch\kap02\ AppletBeispiel (Das System kann die angegebene Datei nicht finden)
2. Wenn Sie statt der *.html-Datei die Klasse mit der Endung *.class angeben, erfolgt keine Rückmeldung. Das Applet wird aber auch nicht geladen. 3. Es wird ein Appletfenster mit weißem Hintergrund angezeigt und unten erscheint die Meldung Start: Applet nicht initialisiert. Sie haben zwar die *.html-Datei angegeben, die Einbindung des Applets war jedoch nicht korrekt oder die Klasse des Applets wurde nicht gefunden. Unterschiede zwischen Anwendungen und Applets Java-Anwendungen werden eigenständig ausgeführt und unterliegen standardmäßig keinen Einschränkungen bei ihrer Ausführung. Der Einsprungpunkt in eine Anwendung ist die Methode main() der beim Aufruf des Interpreters angegebenen Klasse. Anwendungen können im Text- oder im Grafikmodus ausgeführt werden. Applets werden immer im Kontext eines Browsers ausgeführt und für sie gelten gewisse Sicherheitseinschränkungen. So darf ein Applet standardmäßig nicht auf Dateien des Systems zugreifen. Applets werden immer im Grafikmodus ausgeführt. Sie besitzen keine Methode main() und werden von der Klasse java.applet.Applet abgeleitet.
50
2 – Die erste Java-Anwendung
2.7
Verwendung der Beispiele
Die Beispieldateien liegen in zweierlei Form auf der mitgelieferten CD vor. Im Verzeichnis \Beispiele finden Sie alle Dateien in ungepackter Form wieder. Wenn Sie diese jedoch auf Ihre Festplatte kopieren, kann es sein, dass sie noch schreibgeschützt sind. Verwenden Sie in diesem Fall die ZIP-Datei \Daten\Beispiele50.zip und entpacken Sie diese in einem beliebigen Verzeichnis. Alle Beispiele verwenden Packages, d.h., sie müssen entsprechend übersetzt und ausgeführt werden. Haben Sie die Beispiele z.B. nach C:\Beispiele extrahiert, entsteht eine Verzeichnisstruktur der Form C:\Beispiele\de\j2sebuch\.. Zum Ausführen und Übersetzen der Beispiele öffnen Sie eine Konsole im Verzeichnis C:\Beispiele und verwenden die folgenden Befehle: javac de\j2sebuch\kap02\Beispiel java de.j2sebuch.kap02.Beispiel
Die Beispiele sind für die Ausführung unter der J2SE 5.0 entwickelt worden und verwenden auch die neuen Spracheigenschaften und Klassen dieser Version. Im Verzeichnis \Beispiele\Beispiele142 bzw. in der ZIPDatei \Beispiele\Beispiele142.zip befinden sich angepasste Beispiele zur Verwendung in der Version 1.4.2. Es können dann natürlich nicht die neuen Features der J2SE 5.0 verwendet werden.
2.8
Datenein- und -ausgabe
Zur Ein- und Ausgabe von Daten werden in den Beispielen größtenteils die folgenden Anweisungen verwendet. Die konkrete Erläuterung ihrer Verwendung erfolgt in einem späteren Kapitel. Ausgaben Über die folgenden Anweisungen können Sie Zeichenketten oder einzelne Zahlen ausgeben. Mehrere Zeichenketten oder Zahlenangaben können Sie über das Pluszeichen miteinander verknüpfen. System.out.println("Hallo"); System.out.println(10 + " mal Hallo");
Die folgenden Anweisungen entsprechen der Verwendung der Funktion printf() aus der Programmiersprache C. Sie besteht aus einem Formatstring (der erste Teil) und den Parametern (der zweite Teil). Sie können darüber formatierte Ausgaben erzeugen. System.out.printf("%s", "Eine Zeichenkette\n"); System.out.printf("Herr %s ist %d Jahre alt.", "Meier", 100);
Java 5 Programmierhandbuch
51
Kurzes Glossar
Eingaben Zur Eingabe von Zahlen oder Zeichenketten steht seit der J2SE 5.0 eine einfache Variante über die Klasse Scanner aus dem Package java.util zur Verfügung. Im Folgenden wird eine ganze Zahl, eine Gleitkommazahl und eine Zeichenkette von der Konsole eingelesen. Nach der Eingabe jedes einzelnen Werts muss die Æ-Taste betätigt werden. Beachten Sie, dass hier die Gleitkommazahl mit einem Komma und nicht mit einem Punkt eingeben werden muss. Das anzugebende Dezimaltrennzeichen ist grundsätzlich von der Ländereinstellung des betreffenden Computers abhängig. import java.util.*; ... Scanner sc = new Scanner(System.in); int i = sc.nextInt(); System.out.println(i); double d = sc.nextDouble(); System.out.println(d); String s = sc.next(); System.out.println(s);
2.9
Kurzes Glossar
Hot Spot Die JVM des JDK 5.0 unterstützt nur noch die Hot-Spot-Technologie. Das wichtigste Element ist dabei der adaptive Compiler. Aufgrund der Tatsache, dass oft nur ein bestimmter Teil des Codes einer Anwendung überhaupt zur Ausführung kommt, wird eine Anwendung auf die am häufigsten durchlaufenen und zeitkritischsten Stellen hin untersucht. Dies erfolgt nicht sofort, sondern während der Ausführung der Anwendung. Diese Hot Spots (heißen Stellen) werden dann besonders gut optimiert. Bei der Verwendung der Hot Spot Performance Engine (wie sie vollständig genannt wird) wird noch zwischen Client- und Server-Hot Spot unterschieden. Während der Client-Hot Spot weniger Speicher benötigt und einen schnelleren Anwendungsstart ermöglicht, bietet der Server-Hot Spot eine schnelle Ausführungsgeschwindigkeit, auch bei Belastungsspitzen. Sie können beim Start einer Java-Anwendung festlegen, welchen Hot Spot Compiler Sie nutzen wollen. Die J2SE 5.0 unterstützt die dynamische Ermittlung des Rechnertyps (Client oder Server). Dabei werden über die Server-Class Machine Detection sofort die optimalen Einstellungen für eine Anwendung vorgenommen, falls keine weiteren Konfigurationen festgelegt wurden. JIT Just-In-Time Compiler werden heute nicht mehr von Java verwendet, da ihre Ausführungsgeschwindigkeit zu langsam ist. Ein JIT übersetzt den Java ByteCode während seiner Ausführung direkt in Maschinencode des betreffenden Systems. Die verwendeten Optimierungstechniken haben jedoch negative Auswirkungen auf den Start einer Anwendung (er
52
2 – Die erste Java-Anwendung
dauert länger) und sind nicht so effektiv wie beim Hot Spot. Während ein JIT den gesamten ausgeführten Programmcode übersetzt und optimiert, führt die Hot Spot Engine dies nur für ca. 20%, den am häufigsten ausgeführten Code, durch. Diese Optimierung ist nicht so zeitintensiv und kann effektiver erfolgen. Garbage Collector Der Müllsammler dient dazu, den nicht mehr benötigten Speicher einer Java-Anwendung aufzusammeln und freizugeben. Dazu läuft der Garbage Collector, kurz GC, im Hintergrund einer Anwendung. Es ist deshalb nicht notwendig, dass Sie die Freigabe des Speichers selbst durchführen. Dies ist einer der Vorteile von Java gegenüber anderen Programmiersprachen, in denen ein beliebter Fehler darin besteht, belegten Speicher nicht wieder freizugeben. Class Data Sharing Durch das Class Data Sharing, welches erstmals mit der J2SE 5.0 eingeführt wird, kann die Startzeit von Anwendungen verkürzt werden. Systemklassen werden in einem so genannten Shared-Archiv von mehreren Anwendungen gleichzeitig verwendet. Über die Kommandozeilenoption -Xshare kann die Verwendung durch den Java Interpreter konfiguriert werden. Das Shared-Archiv findet man z.B. unter ..\jre\bin\client\classes.jsa. Da die Verwendung des Class Data Sharing automatisch erfolgt, können Sie sich am schnelleren Starten Ihrer Anwendungen erfreuen. Optionale Packages und der Erweiterungsmechanismus Einer J2SE-Installation können über optionale Packages, früher Standarderweiterungen, weitere Bibliotheken (Archive) hinzugefügt werden. Die Bibliotheken enthalten *.classDateien, die von Ihren Anwendungen häufig benötigt werden. Der Vorteil dieses Erweiterungsmechanismus liegt darin, dass diese Bibliotheken automatisch von Java verwendet werden. Sie müssen also nicht im Klassenpfad enthalten sein. Optionale Packages werden in die Verzeichnisse [InstallJDK]\jre\lib\ext bzw. in das Verzeichnis ..\jre1.5.0\lib\ext des JRE eingefügt und bestehen aus einer oder mehreren *.jar-Dateien. Diese Form nennt sich installierbare, optionale Packages. Über die Systemeigenschaft java.ext.dirs können Sie noch zusätzliche Pfade zu Erweiterungen angeben. Mehrere Verzeichnispfade werden dabei unter Windows mit Semikolon, unter Linux durch Doppelpunkte getrennt. Diese optionalen Packages stehen allen Anwendungen zur Verfügung. Eine weitere Form sind die downloadbaren optionalen Packages. Dabei wird innerhalb eines JAR-Archivs auf weitere Archive verwiesen, die dann automatisch nachgeladen werden. Die betreffenden JAR-Archive können sich prinzipiell in beliebigen Verzeichnissen befinden. Endorsed Standards Override Mechanism Ein so genannter Endorsed (empfohlen, bestätigt) Standard wird nicht durch den Java Community Process (JCP) definiert. Dies betrifft z.B. die Implementation des XML-Parsers
Java 5 Programmierhandbuch
53
Kurzes Glossar
oder des XSLT-Prozessors der Apache Group. Diese Standards werden nicht von Sun entwickelt. Häufig stehen neuere Versionen zur Verfügung, die sich noch nicht in der aktuellen Version der J2SE befinden. Damit diese neuen Versionen genutzt werden können, werden die betreffenden Bibliotheken über JAR-Archive in speziell dafür vorgesehene Verzeichnisse kopiert. Dieses Verzeichnis muss in diesem Fall erst angelegt werden. Erstellen Sie dazu das Verzeichnis ..\endorsed als Unterverzeichnis von ..\jre1.5.0\lib. In diesem Fall ist das Verzeichnis gemeint, das über die Umgebungsvariable JAVA_HOME referenziert wird. Unter Windows ist das bei einem installierten JRE nicht das Verzeichnis [InstallJDK], sondern das Verzeichnis ..\Programme\Java\jre1.5.0. Das Verzeichnis für die Endorsed-Erweiterungen kann auch über die Systemvariable java.endorsed.dirs ermittelt werden, z.B. public class SystemInfo { public static void main(String[] args) { System.out.println(System.getProperty("java.endorsed.dirs")); } }
54
3
Grundlegende Sprachelemente
3.1
Elemente eines Programms
Jede einzelne Zeile eines Programms ist nach den syntaktischen Regeln der verwendeten Programmiersprache zu formulieren. Jede Programmiersprache besitzt eine eigene Syntax. Die Syntax legt fest, wie Anweisungen, Ausdrücke, Typdeklarationen usw. angegeben werden und nach welchen Regeln sie aufgebaut sein müssen. Eine syntaktische Regel ist beispielsweise der Abschluss von Anweisungszeilen in Java mit einem Semikolon. Entspricht eine Anweisung Ihres Programms nicht der Syntax der Sprache, kann der Compiler diese nicht übersetzen und gibt eine Fehlermeldung aus.
3.1.1
Anweisungen, Anweisungsblöcke
Ein Programm besteht aus einer Folge von Anweisungen, die – in einer bestimmten Reihenfolge ausgeführt – die Lösung einer vorgegebenen Aufgabe ergeben. Eine Anweisung ist somit ein Schritt auf dem Weg zur Gesamtlösung. In Java wird jede Anweisung durch ein Semikolon ; abgeschlossen. Mehrere Anweisungen werden in Anweisungsblöcken zusammengefasst. Ein solcher Block wird in geschweifte Klammern { ... } eingeschlossen. { a = 1; b = 2; ... }
// // // // //
hier beginnt der Anweisungsblock Anweisung Anweisung weitere Anweisungen hier endet der Anweisungsblock
Beachten Sie, dass die Klammern immer paarweise anzugeben sind. Anweisungsblöcke können ineinander verschachtelt werden, d.h., ein Anweisungsblock kann wiederum weitere Anweisungsblöcke besitzen. Eine Anweisung kann verschiedene Sprachelemente enthalten, beispielsweise reservierte Wörter, Bezeichner, Literale, Kommentare usw. Auf die einzelnen Sprachelemente gehen wir in den folgenden Abschnitten ein.
3.1.2
Kommentare
Mithilfe von Kommentaren können Sie für einzelne Anweisungen oder Programmabschnitte Informationen hinterlegen. Es fällt Ihnen somit leichter, den Programmcode auch nach längerer Zeit noch nachzuvollziehen. Für Ihre Teamkollegen sind Kommentare eine wertvolle Unterstützung bei der Verwendung Ihres SourceCodes. In Java gibt es drei Typen von Kommentaren.
Java 5 Programmierhandbuch
55
Elemente eines Programms
쐌 Der einzeilige Kommentar kann am Ende der Programmzeile angegeben werden oder einzeln auf einer Zeile stehen. Er wird durch zwei Schrägstriche // eingeleitet. 쐌 Mehrzeilige Kommentare können sich über mehrere Zeilen erstrecken und werden in die Zeichen /* ... */ eingeschlossen. 쐌 Dokumentationskommentare können ebenfalls mehrere Zeilen einnehmen und werden hauptsächlich für die Beschreibung von Klassen und Methoden eingesetzt. Sie werden von Javadoc für die automatische Generierung der Dokumentation im HTML-Format verwendet (vgl. dazu Kapitel Javadoc). Dokumentationskommentare werden durch die Zeichen /** ... */ begrenzt. Bei der Kompilierung des Quellcodes werden Kommentare einfach überlesen und nicht in das ausführbare Programm aufgenommen. Somit haben Kommentare keinen Einfluss auf die Ausführung des Programms. Gehen Sie nicht zu sparsam, aber auch nicht zu verschwenderisch mit Kommentaren um. Das Kommentieren von trivialen Anweisungen ist beispielsweise nicht sinnvoll bzw. deren Bedeutung kann auch direkt aus dem Code entnommen werden, z.B. // hier wird die Summe aus 10 und 11 berechnet int summe = 10 + 11;
Beispiel für die Verwendung von Kommentaren Listing 3.1: Kommentare verwenden /** * Die Funktion summe() addiert zwei Zahlen * Dieser Text geht in die Dokumentation ein. */ public int summe() { int zahl1, zahl2; // Zwei Variablen werden deklariert /* Nun müssen den Variablen Werte zugewiesen werden, mit denen dann gerechnet wird */ zahl1 = 4; zahl2 = 6; summe = zahl1 + zahl2; // die Summe der Zahlen wird berechnet }
3.1.3
Reservierte Wörter
Die Anweisungen des Programms enthalten häufig reservierte Wörter (Schlüsselwörter). Diese reservierten Wörter sind Bestandteil der Programmiersprache. Sie werden an den entsprechenden Stellen im Programmcode verwendet. Möchten Sie z.B. eine Variable zum Speichern einer ganzen Zahl definieren, benutzen Sie das reservierte Wort int. Reservierte
56
3 – Grundlegende Sprachelemente
Wörter dürfen Sie nicht für Bezeichner (Namen von Variablen, Funktionen, Klassen usw.) einsetzen. Von Java reservierte Wörter sind in der Tabelle 3.1 zusammengefasst. Tabelle 3.1: Reservierte Wörter von Java abstract
assert
boolean
break
byte
case
catch
char
class
continue
default
do
double
else
enum
extends
final
finally
float
for
if
implements
import
instanceof
int
interface
long
native
new
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
throw
throws
transient
try
void
volatile
while
Es gibt weitere reservierte Wörter, die in der aktuellen Version nicht genutzt werden, aber auch nicht als Bezeichner verwendet werden dürfen. Tabelle 3.2: Zurzeit in Java nicht verwendete reservierte Wörter byvalue
cast
const
future
generic
goto
inner
operator
outer
rest
var
3.1.4
Literale
Als Literale werden unveränderliche Werte bezeichnet. Einige Literale sind durch die Programmiersprache bereits vorgegeben, wie z.B. die logischen Literale (true, false) und der Nullwert (null). Auch im Programm vorhandene konstante Werte sind Literale, z.B. die Zahl 4, das Zeichen 'h' oder die Zeichenfolge "Hallo".
3.1.5
Bezeichner
Bezeichner sind Namen von Variablen, Konstanten, Klassen, Methoden und Schnittstellen. Über den Bezeichner können Sie innerhalb des Programms auf das entsprechende Element
Java 5 Programmierhandbuch
57
Elemente eines Programms
zugreifen. Geben Sie beispielsweise in einer Berechnungsformel den Bezeichner einer Variablen an, wird der Inhalt (der Wert) der Variable für die Berechnung benutzt. Ein Bezeichner besteht aus einer Folge von Unicode-Zeichen, die Sie nach folgenden Regeln festlegen können: 쐌 Ein Bezeichner kann beliebig lang sein. Zur Identifikation eines Elements werden immer alle Zeichen betrachtet, es sind also alle Stellen des Bezeichners signifikant. 쐌 Das erste Zeichen muss ein Buchstabe ('A' .. 'Z', 'a' .. 'z'), der Unterstrich '_' oder das Dollarzeichen '$' sein. Die folgenden Stellen können auch aus Ziffern bestehen, Leeroder Sonderzeichen sind aber nicht erlaubt. 쐌 In Java wird zwischen Groß- und Kleinschreibung unterschieden (man sagt auch Java ist case-sensitive). Das gilt auch für Bezeichner. So geben beispielsweise Text, TEXT und text drei unterschiedliche Programmelemente an. 쐌 Durch die Verwendung von Unicode-Zeichen ist es möglich, auch Buchstaben anderer Alphabete zu nutzen. Dies ist wegen der schlechteren Lesbarkeit aber nicht zu empfehlen. 쐌 In dem Bereich des Programms, in dem der Bezeichner verwendet wird, dem so genannten Gültigkeitsbereich, muss er eindeutig sein. 쐌 Ein Bezeichner darf kein reserviertes Wort und kein vordefiniertes Literal sein. Beispiele Gültige Namen für Bezeichner sind z.B.: MeineKlasse
Besteht nur aus Buchstaben.
_abc
Ein Unterstrich darf als erstes Zeichen stehen.
a1b1
Das erste Zeichen ist ein Buchstabe.
Dagegen sind folgende Namen keine gültigen Bezeichner: Meine Klasse
Enthält ein Leerzeichen.
private
Ist ein reserviertes Wort.
3a4b
Das erste Zeichen ist eine Zahl.
HalloWieGehts?
Enthält ein Sonderzeichen.
Tipps und Richtlinien zur Namensgebung Die Bezeichner sollten so gewählt werden, dass aus dem Namen auf die Verwendung des Elements geschlossen werden kann. Beispielsweise kann eine Variable, welche die Höhe eines Gegenstands speichern soll, mit dem Bezeichner hoehe benannt werden. Eine weitere Variable, die zum Zählen von Elementen dient, könnte zaehler oder anzahl heißen.
58
3 – Grundlegende Sprachelemente
Für die Java-Programmierung haben sich bestimmte Konventionen zur Namensgebung durchgesetzt, welche die Lesbarkeit des Quellcodes erleichtern, vgl. auch Kapitel 2. Gültigkeitsbereiche Ein Bezeichner muss im Gültigkeitsbereich eindeutig sein, d.h., der Name darf nur einmal darin definiert werden. Der Gültigkeitsbereich einer Variablen ist beispielsweise der umschließende Anweisungsblock. In einem Anweisungsblock dürfen Sie einen Variablennamen nur einmal vergeben. Am Ende des Anweisungsblocks wird der Variablenname ungültig. public int summe() { int zahl1, zahl2; ... } // zahl1 und zahl2 verlieren hier ihre Gültigkeit
Der Gültigkeitsbereich der Bezeichner kann durch die Angabe von Zugriffsattributen festgelegt werden. Mehr zu diesem Thema erfahren Sie im Kapitel Klassen, Interfaces und Objekte.
3.2
Primitive Datentypen
Bevor Sie in einem Programm eine Variable oder Konstante verwenden können, müssen Sie diese deklarieren, d.h. dem Programm bekannt machen. Beim Deklarieren wird der Datentyp der Variablen bzw. Konstante angegeben. Der Datentyp bestimmt die Größe des Speicherplatzes und den Wertebereich. Es gibt in Java drei elementare Datentypen – den numerischen, den logischen und den Zeichen-Datentyp. Haben Sie einer Variablen einmal einen Datentyp zugewiesen, ist eine Änderung nicht mehr möglich. Ein Vorteil der Sprache Java ist die Plattformunabhängigkeit der Datentypen. Es spielt keine Rolle, unter welchem Betriebssystem Sie Ihr Java-Programm ausführen, die Speichergröße und der Wertebereich der Datentypen sind immer gleich. Außer den primitiven Datentypen gibt es noch Referenzdatentypen. Sie speichern Verweise auf Objekte von Klassen. Mehr darüber erfahren Sie im Kapitel Klassen, Interfaces und Objekte.
3.2.1
Numerische Datentypen
Zu den numerischen Datentypen zählen ganzzahlige Datentypen und Gleitpunkt-Datentypen. Sie sind immer vorzeichenbehaftet und werden z.B. für Berechnungen oder als Zähler in Schleifen eingesetzt. Ganzzahlige Datentypen Ganzzahlige Datentypen werden auch als Integer-Typen bezeichnet und besitzen keine Nachkommastellen. Es gibt vier Integer-Typen, die sich durch die Speicherplatzgröße un-
Java 5 Programmierhandbuch
59
Primitive Datentypen
terscheiden und damit durch den Wertebereich der Zahlen, die in ihnen gespeichert werden können. Tabelle 3.3: Ganzzahlige Datentypen Typ
Größe
Wertebereich
byte
1 Byte
-128 … 127 (-27 … 27 – 1)
short
2 Byte
-32 768 … 32 767 (-215 ... 215 – 1)
int
4 Byte
-2 147 483 648...2 147 483 647 (-231 ... 231 – 1)
long
8 Byte
-9 223 372 036 854 775 808 ... 9 223 372 036 854 775 807 (-263 ... 263 – 1)
Sie können diese Datentypen zur Speicherung von Dezimal-, Oktal- und Hexadezimalzahlen benutzen. 쐌 Geben Sie eine Dezimalzahl als Literal, z.B. 75 an, wird sie immer als Integer-Zahl behandelt. Um eine Zahl als Long zu kennzeichnen, muss ihr der Suffix L oder l angehängt werden. 쐌 Bei Oktalzahlen handelt es sich um Zahlen zur Basis 8. Sie bestehen aus einer Folge der Ziffern von 0 bis 7. Zur Darstellung einer Oktalzahl als Literal verwenden Sie das Präfix 0. 쐌 Hexadezimalzahlen sind Zahlen zur Basis 16. Sie werden aus den Ziffern 0 ... 9 und den Buchstaben A ... F zusammengesetzt. Das Kennzeichen bei Literalen ist das Präfix 0x. Beispiele -73271
negative Integer-Zahl
95178L
Dezimalzahl 95178 wird als long-Zahl gespeichert
0236
Oktalzahl 236, entspricht dezimal 158 = 2*72+3*71+6*70
0x12AF
Hexadezimalzahl 12AF, entspricht dezimal 4783
Gleitpunktzahlen Für die Speicherung von Zahlen mit Dezimalstellen (reelle Zahlen) gibt es Gleitpunktzahlen (auch Gleitkommazahlen). Die Anzahl der Dezimalstellen, die gespeichert werden kann, hängt vom benutzten Datentyp ab. Tabelle 3.4: Gleitpunkt-Datentypen Typ
Größe
Wertebereich (circa)
float
4 Byte (single precision)
1.4024E-45… 3.4028E+38
double
8 Byte (double precision)
4.9407E-324... 1.7976E+308
60
3 – Grundlegende Sprachelemente
Soll eine als Literal im Programmcode angegebene Zahl als Gleitpunktzahl interpretiert werden, muss diese einen Dezimalpunkt besitzen. Nullen können vor dem Dezimalpunkt weggelassen werden. Zum Beispiel wird aus der Angabe 0.5 die verkürzte Schreibweise .5. Ebenso kann auf die Angabe der Dezimalstellen verzichtet werden, wenn diese nur Nullen aufweisen, z.B. wird aus 7.00 die Angabe 7.. Mindestens auf einer Seite des Dezimalpunkts muss aber eine Zahl stehen. Ein weiteres Erkennungsmerkmal einer Gleitkommazahl ist der Exponent, der durch E oder e gekennzeichnet wird. Es folgt die Größe des Exponenten, die positiv oder negativ sein kann. Durch einen Suffix kann dem Compiler mitgeteilt werden, ob er die angegebene Dezimalzahl als Float- oder Double-Zahl behandeln soll: 쐌 Hängen Sie der Zahl das Suffix F oder f an, wird sie als float-Zahl interpretiert. 쐌 Ohne Suffix wird eine Dezimalzahl als double-Zahl angesehen. Sie können dies aber auch explizit durch das Anfügen von D oder d erreichen. Beispiele 123.45
Die Dezimalzahl 123.45 wird als double-Zahl verwendet.
67.89F
Zur Kennzeichnung der Zahl 67.89 als float-Zahl wird das Suffix F angefügt.
.12345F
Für die Zahl 0.12345 kann die Null vor dem Dezimalpunkt weggelassen werden. Durch das Suffix F wird sie als float-Zahl interpretiert.
143.
Damit die ganze Zahl 143 als double-Zahl verwendet wird, muss der Dezimalpunkt angegeben werden. Dies kann z.B. in einer Formel erforderlich sein.
2.5E5
Hier folgt hinter der Zahl ein Exponent. Das Beispiel entspricht der Zahl 250000 = 2.5 * 105.
9.67E-5
Sehr kleine Zahlen können mithilfe von negativen Exponenten dargestellt werden. 0,0000967 = 9.67 * 10-5.
3.2.2
Datentyp Zeichen
In Variablen vom Datentyp char ist es möglich, ein beliebiges Zeichen des Unicode-Zeichensatzes zu speichern. Ein Unicode-Zeichen kann ein Buchstabe, eine Zahl oder ein Sonderzeichen sein. Auch Steuerzeichen, so genannte Escape-Sequenzen, lassen sich in einer char-Variablen ablegen. Wird ein Unicode-Zeichen als Literal angegeben, ist es in Hochkommata einzuschließen, z.B. 'a'. Die Angabe des Zeichencodes, z.B. 41 für den Buchstaben A, ist aber auch erlaubt. Tabelle 3.5: Zeichen-Datentypen Typ
Größe
Wertebereich
char
2 Byte
Unicode Zeichen (0...65535)
Java 5 Programmierhandbuch
61
Primitive Datentypen
Escape-Sequenzen Escape-Sequenzen sind Steuerzeichen, die beispielsweise bei der Ausgabe von Text auf der Konsole verwendet werden. Sie erhalten zur Kennzeichnung einen Backslash '\'. In der folgenden Tabelle sind die wichtigsten Escape-Sequenzen zusammengefasst. Tabelle 3.6: Escape-Sequenzen Escape-Sequenz
Unicode-Wert
Bedeutung
\b
8
Rückschritt (Backspace BS)
\t
9
Horizontaler Tabulator (HT)
\n
10
Zeilenschaltung (Newline NL)
\f
12
Seitenumbruch (Formfeed)
\r
13
Wagenrücklauf (Carriage return CR)
\"
34
Doppeltes Anführungszeichen
\'
39
Einfaches Anführungszeichen
\\
92
Backslash
\u0041
65
\u gibt an, dass ein Zeichencode (hexadezimal) folgt, z.B. die Hexadezimalzahl 0041 für den Buchstaben A
Beispiele char c = 'A';
Das Zeichen A wird der Variablen c zugewiesen.
char c = '\\';
Das Zeichen \ wird der Variablen c mithilfe der EscapeSequenz \\ zugewiesen.
char c = '\u0088';
Der Variablen c wird über den hexadezimalen Zeichencode das Fragezeichen ? mittels Escape-Sequenz zugewiesen.
char c = 0x0088;
Es ist auch möglich, den hexadezimalen Zeichencode als Literal zuzuweisen.
char c = 136;
Hier wird c der dezimale Unicode-Wert für das Fragezeichen ? zugewiesen.
Der Quellcode des Java-Programms wird im 8-Bit-ASCII-Code gespeichert. Bei der Übersetzung werden die Zeichen aber in 16 Bit große Unicode-Zeichen umgewandelt. Der Datentyp char kann nur zum Speichern eines Zeichens benutzt werden. Für die Speicherung einer Folge von Zeichen stellt Java die Klasse String bereit.
62
3 – Grundlegende Sprachelemente
3.2.3
Logische Datentypen
Der Datentyp boolean kann Wahrheitswerte (logische Werte) aufnehmen. In Java gibt es die beiden Literale true und false. Die Angabe von 0 bzw. 1, wie in einigen anderen Programmiersprachen üblich, ist hier nicht zulässig. Ein logischer Wert ist z.B. das Ergebnis eines Vergleichs. Dieses Ergebnis können Sie in einer Variablen vom Datentyp boolean speichern. Tabelle 3.7: Logische Datentypen Typ
Größe
Wertebereich
boolean
1 Byte
true, false
Beispiele boolean b = true;
Der Variablen b wird der logische Wert »wahr« zugewiesen.
boolean b = false;
Der Variablen b wird der logische Wert »falsch« zugewiesen.
boolean b = a > c;
Der Variablen b wird das Ergebnis eines logischen Ausdrucks (a > c) zugewiesen.
3.3
Variablen und Konstanten
Variablen und Konstanten sind symbolische Namen für Speicherplätze im Hauptspeicher. Werte, die man im Programm beispielsweise für eine Berechnung benötigt, werden im Hauptspeicher zwischengespeichert. Über eine Variable oder Konstante können Sie während der Programmausführung auf diese Speicherplätze zugreifen. Bevor Sie eine Variable oder Konstante im Programm verwenden können, müssen Sie diese vereinbaren (deklarieren). Sie legen dabei den Datentyp fest. Bei Konstanten wird bei der Deklaration auch der Wert zugewiesen, welcher sich während der Programmausführung nicht mehr ändern lässt.
3.3.1
Variablen
Einer Variablen können Sie während der Programmausführung beliebig oft einen neuen Wert zuweisen. Der jeweilige (aktuelle) Wert der Variablen kann über den Variablennamen ermittelt werden. Die Deklaration der Variablen muss vor der ersten Verwendung erfolgen. Es sind der Variablenname und der Datentyp zu definieren. Für die Variable wird bei der Übersetzung ein Speicherplatz, dessen Größe vom Datentyp abhängig ist, bereitgestellt. Die Variable kann nur Werte aufnehmen, die dem festgelegten Datentyp entsprechen oder sich in solche umwandeln lassen. Dies wird durch den Compiler überprüft, wodurch Java zu einer typsicheren Sprache wird.
Java 5 Programmierhandbuch
63
Variablen und Konstanten
Typen von Variablen Variablen werden danach unterschieden, ob sie primitive Werte oder Referenzen speichern und an welcher Stelle des Programms sie deklariert werden. 쐌 Lokale Variablen werden innerhalb einer Methode deklariert und sind auch nur in dieser Methode gültig. Sie können primitive Werte oder Referenzen enthalten. Referenzen sind Adressen im Hauptspeicher, in denen Daten (z.B. Objekte) gespeichert sind. 쐌 Variablen, die in einem Objekt während dessen gesamter Lebensdauer gültig sind, heißen Instanzvariablen. 쐌 Ist eine Variable nicht an ein Objekt, sondern an eine Klasse gebunden, so nennt man diese Klassenvariable. Sie ist somit immer verfügbar. Im Folgenden wird immer verkürzt von Variablen gesprochen. Nur wenn die Unterscheidung bedeutsam ist, wird die genaue Typbezeichnung angegeben. Für Variablen eines Objekts werden auch oft die Bezeichnungen Member-, Referenz- oder Objektvariablen bzw. Felder verwendet.
Deklaration von Variablen Für die Deklaration einer Variablen muss der Datentyp und der Variablenname angegeben werden. Variablennamen sind Bezeichner und müssen den Regeln für Bezeichner entsprechen. Am Ende der Deklaration steht ein Semikolon. Variablenname;
In einer Deklarationsanweisung können Sie mehrere Variablen gleichen Typs deklarieren. Die Variablennamen sind, durch Kommata getrennt, aufzulisten. Es ist allerdings aus Gründen der besseren Lesbarkeit und zu Dokumentationszwecken unüblich, mehrere Variablen in einer Anweisung zu deklarieren. Variablenname1, Variablenname2,...;
Bereits bei der Deklaration kann der Variablen ein Wert aus dem Wertebereich des Datentyps zugewiesen werden. Dies wird als Initialisierung der Variablen bezeichnet. Der Wert kann z.B. ein Literal oder auch ein berechneter Wert sein. Variablenname1 = Wert1;
Der Deklaration von Instanz- und Klassenvariablen kann zusätzlich ein Zugriffsattribut (private, public oder protected) vorangestellt werden, wodurch der Zugriff auf die Variable gesteuert wird. [Modifizierer] Variablenname1 = Wert1;
64
3 – Grundlegende Sprachelemente
Beispiele int zahl1;
Die Variable zahl1 vom Datentyp int wird deklariert.
double x = 8.888;
Die Variable x wird mit dem Wert 8.888 initialisiert.
private long l1, l2;
Die zwei Variablen l1 und l2 werden als long deklariert. Mit dem Modifizierer private darf diese Deklaration nur außerhalb einer Methode stehen.
3.3.2
Wertzuweisungen
Mithilfe des Zuweisungsoperators '=' kann einer Variablen ein Wert zugewiesen werden. Die Variable steht auf der linken Seite der Wertzuweisung. Die rechte Seite der Wertzuweisung ist ein Literal, ein Ausdruck oder ein Methodenaufruf. Der Wert muss dem Datentyp der Variablen entsprechen oder diesen umfassen. variable = Wert;
Beispiele // Deklaration int zahl1; int zahl2; char c1, c2; // eher unüblich aber nicht falsch // Zuweisungen korrekter Werte zahl1 = 567; c = '4'; zahl2 = 456 + 623; // fehlerhafte Wertzuweisung zahl1 = 1.34; // 1.34 ist kein Integer-Wert zahl2 = 7.37 * 3.45 // der Ausdruck liefert keinen Integer-Wert c2 = "abc"; // "abc" ist kein Zeichen
Initialisierung Bevor Sie auf den Wert einer Variablen zugreifen können, muss diese initialisiert werden, d.h. einen Ausgangswert besitzen. Variablen primitiver Datentypen müssen Sie einen Wert zuweisen. Die Initialisierung kann bereits bei der Deklaration oder später während der Programmausführung erfolgen. Der Compiler prüft, ob eine Variable beim ersten Lesezugriff bereits initialisiert wurde. Ansonsten gibt er eine Fehlermeldung aus. Instanz- und Klassenvariablen werden beim Erzeugen automatisch initialisiert. Wird ihnen in der Deklarationsanweisung nicht explizit ein Wert zugewiesen, besitzen Sie einen Standardwert (0 bzw. 0.0 für Zahlen, null für Objekte, 0 für Zeichen, false für Wahrheitswerte).
Java 5 Programmierhandbuch
65
Variablen und Konstanten Listing 3.2: Initialisierung von Variablen public static void main(String[] argv) { // Variablen deklarieren int wert1; // Deklaration ohne Initialisierung int summe; // Deklaration ohne Initialisierung int wert2 = 1; // Deklaration mit Initialisierung wert1 = 4; // Initialisierung von wert1 // Summe berechnen summe = wert1 + wert2; // Ergebnis ausgeben System.out.println("Summe = " + summe); }
Kommentieren Sie die Zeile wert1 = 4; aus und übersetzen Sie das Programm, erkennt der Compiler, dass der Variablen wert1 noch kein Wert zugewiesen wurde, diese jedoch für die Berechnung der Summe benötigt wird. Er gibt die Fehlermeldung variable wert1 might not have been initialized aus.
3.3.3
Typumwandlungen
Implizite Typumwandlung Sie können einer Integer-Variablen nicht einfach einen double-Wert zuweisen. Bereits der Compiler erkennt den möglichen Verlust an Informationen und gibt eine entsprechende Fehlermeldung aus. Umgekehrt ist dies jedoch möglich, weil der Wertebereich des Typs double den Wertebereich des Typs int einschließt. Umfasst der Datentyp auf der linken Seite der Wertzuweisung den zugewiesenen Datentyp, wird automatisch eine entsprechende Typumwandlung durchgeführt. Dies wird als implizite Datentypkonvertierung bezeichnet. Beispiele int i = 'x'; double d = 5;
// Umwandlung von char in int // Umwandlung von int in double
In der Abbildung 3.1 sind die Datentypen in einem Mengendiagramm dargestellt. Kann ein Datentyp automatisch in einen anderen Datentyp umgewandelt werden, ist er in diesem enthalten.
66
3 – Grundlegende Sprachelemente
char
byte short int long float double
Abb. 3.1: Umwandlung von Datentypen Die Konvertierung vom Datentyp long in den Datentyp float kann zu Verlusten bei der Genauigkeit führen, da float-Zahlen nur eine Genauigkeit von acht Stellen besitzen. [IconInfo]
long l = 1234567890L; // f hat nach der Zuweisung den Wert 1.23456794E9 float f = l;
Explizite Typumwandlung In einigen Fällen ist es erforderlich, eine Konvertierung von Datentypen durchzuführen, die nicht zuweisungskompatibel sind (reduzierende Konvertierung). Eine explizite Datentypumwandlung wird durch die Angabe eines Cast-Operators erzwungen. Der Zieldatentyp wird dabei in runden Klammern vor dem Wert angegeben. (datentyp)
Beispiele int i short float int i
= s f =
123; = (short)i; = 1.23F; (int)f;
// int wird in short umgewandelt (s = 123) // float wird in int umgewandelt (i = 1)
Bei der reduzierenden Konvertierung werden z.B. Zahlen, die den Wertebereich des Zieldatentyps überschreiten, verfälscht. [IconInfo]
Java 5 Programmierhandbuch
67
Variablen und Konstanten int i = 32768; short s = (short)i;
Der größte mögliche Wert, den der Datentyp short besitzen kann, beträgt 32767. Die Variable s kann den in i gespeicherten Wert nicht aufnehmen. Die Kodierung des Werts 32768 als int-Datentyp entspricht dem Wert -32768 im short-Datentyp. Achten Sie deshalb bei derartigen Zuweisungen darauf, dass der Wertebereich des Zieldatentyps für die Aufnahme von Werten anderer Datentypen ausreicht.
3.3.4
Konstanten
Konstanten verbessern die Lesbarkeit eines Programms und erleichtern Ihnen die Arbeit bei Änderungen im SourceCode. Oft ist auch ein geeigneter Konstantenname in einer Formel aussagekräftiger als der konstante Wert selbst. Beispielsweise kann für Berechnungen mit der Mehrwertsteuer statt der Zahl 0.16 der Konstantenname in den Formeln verwendet werden. Ändert sich die Mehrwertsteuer, ist nur die Zahl in der Initialisierung der Konstanten zu korrigieren. Wenn Sie den Zahlenwert direkt in die Formeln einsetzen, muss jede Formel gesucht und geändert werden. Das ist zeitaufwendig und fehlerträchtig. Konstanten werden mit dem Schlüsselwort final deklariert. Der Wert kann in der Deklarationsanweisung oder später während der Programmausführung zugewiesen werden. Wurde einer Konstanten ein Wert zugewiesen, ist keine weitere Wertzuweisung an sie erlaubt. Auf diese Weise sind auch dynamische Initialisierungen von Konstanten möglich, die erst während der Ausführung einer Anwendung durchgeführt werden. final Konstantenname [= Wert];
Beispiel Listing 3.3: \Beispiele\de\j2sebuch\kap03\KonstantenVerwenden.java public class KonstantenVerwenden { public static void main(String[] argv) { final double MWST = 0.16; // Konstanten-Deklaration double preis; // Variablen-Deklaration double nettoPreis; nettopreis = 100.0; // Initialisierung // preis berechnen preis = nettopreis + nettopreis * MWST; // Ergebnis ausgeben System.out.println("Preis incl. Mehrwertsteuer = " + preis); } }
68
3 – Grundlegende Sprachelemente
Konstanten können Sie beispielsweise auch für die maximale Anzahl von Elementen eines Arrays oder für Programmeinstellungen, wie die Höhe und Breite eines Fensters oder die aktuelle Hintergrundfarbe, benutzen.
3.4
Operatoren und Ausdrücke
Operatoren Bei Berechnungen werden die Werte der Operanden mithilfe von Operatoren verknüpft. Das Ergebnis der Berechnung kann über den Zuweisungsoperator einer Variablen übergeben werden. Operatoren sind Zeichen, wie z.B. +, -, *, && oder >. Sie werden in Ausdrücken eingesetzt, um die Ausführung der entsprechenden Operationen zu bewirken. Operatoren lassen sich nach ihrem Verwendungszweck mehreren Gruppen zuordnen. So werden beispielsweise für Berechnungen arithmetische Operatoren benutzt und für Vergleiche logische Operatoren. Für die bitweise Verknüpfung gibt es bitweise Operatoren. In den folgenden Abschnitten werden die einzelnen Gruppen erläutert. Abhängig vom Operator müssen unterschiedlich viele Operanden angegeben werden. Einstellige (unäre) Operatoren
z.B. ein Vorzeichen oder der Inkrement-Operator -a, ++a
Zweistellige (binäre) Operatoren
z.B. Rechen- oder Vergleichsoperationen a + b und a > b
Dreistellige Operatoren
Der Fragezeichenoperator ist der einzige dreistellige Operator in Java: a ? b : c
Ausdrücke In Ausdrücken werden Operanden und Operatoren so miteinander kombiniert, dass sie ein entsprechendes Ergebnis liefern. Als Operanden können Sie Variablen, Konstanten oder Literale angeben, deren Werte dann für die Operationen verwendet werden. Auch ein weiterer Ausdruck ist als Operand erlaubt. Vor der Operation wird der Wert dieses Ausdrucks ermittelt. Der Datentyp des Ergebnisses richtet sich nach den Datentypen der Operanden oder nach dem verwendeten Operator. Addiert man beispielsweise zwei int-Werte, ist das Ergebnis ebenfalls vom Typ int. Werden die beiden int-Werte aber verglichen, ist das Ergebnis ein logischer Wert. Beispiele für Ausdrücke a * (b + c) - 6 (a + b) < (c – d) (a > 5) && (b < 8) a++
Java 5 Programmierhandbuch
69
Operatoren und Ausdrücke
Auch eine einzelne Variable, eine Konstante oder ein Literal kann ein Ausdruck sein.
Auswertung von Ausdrücken Prinzipiell wird in Java ein Ausdruck von links nach rechts ausgewertet. Der linke Operand wird vollständig abgearbeitet und erst dann mit dem nächsten Operanden verknüpft. So ergibt beispielsweise der Ausdruck ++b * b nicht das gleiche Ergebnis wie der Ausdruck b * ++b: int b = 3, e; e = ++b * b;
Als Erstes wird der linke Operand ausgewertet. Die Variable b wird um 1 erhöht und hat anschließend den Wert 4. Also hat auch der rechte Operand den Wert 4. Jetzt erfolgt die Multiplikation. Der Variablen e wird der Wert 16 zugewiesen.
int b = 3, e; e = b * ++b
Die Auswertung des linken Operanden ergibt den Wert 3. Der rechte Operand wird vor der Multiplikation um 1 erhöht und hat den Wert 4. Es wird die Multiplikation mit den Werten 3 und 4 ausgeführt. Die Variable e erhält den Wert 12.
Die Reihenfolge der ausgeführten Operationen hängt von den Operatoren ab. Wie in der Mathematik gilt z.B. die Regel, dass Punkt- vor Strichrechnung erfolgt. Für die Veränderung bzw. Sicherstellung der Reihenfolge sollten Sie die betreffenden Teile eines Ausdrucks in Klammern setzen. Das Setzen von Klammern verbessert zusätzlich die Lesbarkeit eines Ausdrucks.
3.4.1
Arithmetische Operatoren
Berechnungen werden mit arithmetische Operatoren ausgeführt. In Java stehen Operatoren für die vier Grundrechenarten und der Modulo-Operator zur Verfügung. Außerdem gibt es unäre Operatoren für das Vorzeichen, zum Inkrementieren und Dekrementieren einer Zahl. Tabelle 3.8: Arithmetische Operatoren Operator
Erläuterung
+ - * /
Grundrechenarten (Addition, Subtraktion, Multiplikation, Division)
%
Modulo (Division mit Rest)
+ -
Vorzeichen festlegen
++ --
Inkrementierung (Variablenwert + 1), Dekrementierung (Variablenwert - 1) Der Operator kann vor oder hinter dem Variablennamen angegeben werden (Präinkrement und Prädekrement bzw. Postinkrement und Postdekrement)
70
3 – Grundlegende Sprachelemente
Anwendung arithmetischer Operatoren // Vorzeichenoperatoren int a = -1, int b = +2;
Die Zahl 1 wird mit negativem Vorzeichen versehen. Die Zahl 2 erhält ein positives Vorzeichen.
// Addition, Subtraktion int a = 8 + 9; double d = 9.9 – 7.4;
Addition zweier int-Werte Subtraktion von double-Werten
// Multiplikation long l = (3 + 4) * 27;
Ohne Klammern erfolgt die Multiplikation vor der Addition. Hier erzwingen die Klammern die vorrangige Ausführung der Addition.
// Division int a = 15 / 2; double d = 15 / 2;
double d = 15.0 / 2; double d = 15 / 2.0; //Division durch 0 double d = 15 / 0; double d = 15.0 / 0; double d = 0.0 / 0.0; // Rest der ganzzahligen // Division int a = 15 % 2;
Bei der Verwendung ganzzahliger Operanden ist das Ergebnis auch eine ganze Zahl. Der Rest wird nicht berücksichtigt. Die Variable a erhält den Wert 7. Auch wenn die Variable, welcher das Ergebnis zugewiesen wird, vom Typ double ist, wird eine ganzzahlige Division ausgeführt. Die Variable d hat deshalb den Wert 7.0. Ist einer der beiden Operanden eine Gleitpunktzahl, ist auch das Ergebnis von diesem Typ. Die Variable d besitzt demnach den Wert 7.5. Die ganzzahlige Division durch 0 erzeugt einen Fehler und führt zum Abbruch des Programms. Die Division durch 0.0 mit Gleitpunktzahlen liefert den Wert Infinity (unendlich) zurück. Werden zwei Nullwerte als Operanden angegeben, ist das Ergebnis NaN (Not a Number – keine Zahl). Der Modulo-Operator gibt den Rest der Division zurück. a = 1, da a = 2 * 7 + 1
// Inkrementierung int i = 1; int a = i++; int i = 1; int b = ++i;
Kurzschreibweise für i = i + 1; Der Wert der Variablen i wird a zugewiesen und anschließend um 1 erhöht, a = 1, i = 2. Der Wert der Variablen i wird um 1 erhöht und anschließend der Variablen b zugewiesen. b = 2, i = 2.
// Dekrementierung int i = 5; int a = i--; int i = 5; int b = --i;
Kurzschreibweise für i = i - 1; Der Wert der Variablen i wird a zugewiesen und anschließend um 1 verringert, a = 5, i = 4. Der Wert der Variablen i wird um 1 verringert und anschließend b zugewiesen, b = 4, i = 4.
Java 5 Programmierhandbuch
71
Operatoren und Ausdrücke
Die Inkrementierung und die Dekrementierung sind verkürzte Schreibweisen für die Anweisungen x = x + 1 bzw. x = x – 1. In Java gibt es weitere verkürzte Schreibweisen für arithmetische Operationen, die eine Variable auf der rechten und linken Seite der Wertzuweisung verwenden. Der Operator setzt sich aus dem arithmetischen Operator und dem Gleichheitszeichen zusammen: +=, -=, *=, /= und %=. Auf der linken Seite des Operators wird der Variablenname angegeben. Rechts steht der Ausdruck, dessen Wert für die Berechnung herangezogen wird. Als Beispiel ist hier die Addition angeführt. += ; // ist gleichbedeutend mit = + ;
Beispiele Für die Beispiele gelten folgende Deklarationen: int i = 10, b = 3; double d = 10.0, c = 2.5;
i += 5;
Kurzschreibweise für i = i + 5; Zum Wert der Variablen i (10) wird 5 addiert. i hat nach der Operation den Wert 15.
i -= b * 2;
Kurzschreibweise für i = i – b * 2; Erst wird der Ausdruck b * 2 (6) berechnet, dann erfolgt die Subtraktion i – 6 (4).
i *= b + 2;
Kurzschreibweise für i = i * (b + 2); Auch wenn der Ausdruck b + 2 hier nicht geklammert ist, wird er zuerst berechnet (5) und anschließend die Multiplikation mit i ausgeführt (50).
d /= c;
Kurzschreibweise für d = d / c; Die Variable d hat nach der Division durch c (10.0 / 2.5) den Wert 4.0.
i %= b;
Kurzschreibweise für i = i % b; Bei der Modulo-Operation i % b (10 % 3) ergibt sich der Rest 1, welcher der Variablen i nach der Operation zugewiesen wird.
3.4.2
Vergleichsoperatoren (relationale Operatoren)
Mithilfe von Vergleichsoperatoren können Sie feststellen, welcher von zwei Werten der größere bzw. der kleinere ist, ob sie gleich groß oder ungleich sind. Für den Vergleich können als Operatoren Variablen, Konstanten oder die Werte von Ausdrücken verwendet werden. Vergleichsoperatoren werden für Werte primitiver Datentypen benutzt. Vergleiche von Objekten, z.B. von Strings, sind auf diese Weise nicht möglich.
72
3 – Grundlegende Sprachelemente
Java kennt sechs verschiedenen Vergleichsoperatoren, die in der folgenden Tabelle aufgeführt sind. Tabelle 3.9: Vergleichsoperatoren Operator
Erläuterung
> größer als
Der Ausdruck liefert den Wert true, wenn der Wert des linken Operanden größer als der Wert des rechten Operanden ist, sonst false.
>= größer gleich
Der Ausdruck liefert den Wert true, wenn der Wert des linken Operanden größer oder gleich dem Wert des rechten Operanden ist, sonst false.
< kleiner als
Der Ausdruck liefert den Wert true, wenn der Wert des linken Operanden kleiner ist als der Wert des rechten Operanden, sonst false.
j;
// das Ergebnis des Vergleichs (false) // wird der Variablen b zugewiesen
// Vergleich als Bedingung in einer if-Anweisung if (i < 15) ... // Vergleich als Bedingung einer Schleife for(i = 1; i 5)) ... // logische Operation in der Bedingung einer Schleife // der Ausdruck liefert true, wenn c gleich dem Zeichen 'j' ist // und gleichzeitig i kleiner als 10 oder j größer als 4 ist while((c == 'j') && ((i < 10) || (j > 4))) ...
74
3 – Grundlegende Sprachelemente
Werden in einem logischen Ausdruck mehrere Operanden verknüpft, wird der Ausdruck schnell unübersichtlich. Legen Sie die Reihenfolge der Auswertung auch hier durch das Setzen von Klammern fest, um unerwünschten Ergebnissen vorzubeugen.
3.4.4
Bitweise Operatoren
Die bitweisen Operatoren können auf ganze Zahlen und Zeichen angewendet werden. Sie nutzen die interne Darstellung der Zahlen, der Binärdarstellung. Bis auf Schiebe-Operatoren arbeiten bitweise Operatoren ähnlich wie die logischen Operatoren. Es werden hier aber keine logischen Werte (true und false) miteinander verknüpft, sondern einzelne Bits (1 und 0). Schiebe-Operatoren verschieben die einzelnen Bits innerhalb der Binärdarstellung einer Zahl nach rechts oder nach links. Frei werdende Stellen werden mit Nullen aufgefüllt. Tabelle 3.11: Bitweise Operatoren Operator
Erläuterung
Beispiel
~
NEGATION (KOMPLEMENT) Unärer Operator, alle Bits des Operanden werden negiert.
Regeln der Komplement-Bildung:
UND (AND) Die entsprechenden Bits der beiden Operanden (z.B. das erste Bit des linken und das erste Bit des rechten Operanden) werden UND-verknüpft.
Regeln der UNDVerknüpfung:
ODER (OR) Die entsprechenden Bits der beiden Operanden (z.B. das erste Bit des linken und das erste Bit des rechten Operanden) werden ODER-verknüpft.
Regeln der ODERVerknüpfung:
EXKLUSIVES ODER (XOR) Die entsprechenden Bits der beiden Operanden (z.B. das erste Bit des linken und das erste Bit des rechten Operanden) werden EXKLUSIV ODER-verknüpft.
Regeln der EXKLUSIV ODERVerknüpfung:
&
|
^
Java 5 Programmierhandbuch
10 01
1&11 0&10 1&00 0&00
1|11 0|11 1|01 0|00
1^10 0^11 1^01 0^00
75
Operatoren und Ausdrücke Tabelle 3.11: Bitweise Operatoren (Forts.) Operator
Erläuterung
Beispiel
>>
RECHTS-Schiebe-Operator mit Berücksichtigung des Vorzeichens. Die Bits des linken Operanden werden um die im rechten Operanden angegebene Anzahl nach rechts verschoben. Das Vorzeichen wird beibehalten.
–
>>>
RECHTS-Schiebe-Operator ohne Berücksichtigung des Vorzeichens. Die Bits des linken Operanden werden um die im rechten Operanden angegebene Anzahl nach rechts verschoben. Ein negatives Vorzeichenbit wird auf 0 gesetzt.
–
> 2;
In der Binärdarstellung der Zahl 15 wird eine Rechtsverschiebung um zwei Stellen durchgeführt. Die Variable i hat nach der Operation den Wert 3.
>> 2 1000 (15) 0010 ( 3)
76
3 – Grundlegende Sprachelemente
3.5
Steuerung des Programmflusses
Die Anweisungen eines Programms werden nacheinander (sequentiell) abgearbeitet. Häufig müssen aber bestimmte Programmteile mehrmals oder nur unter bestimmten Bedingungen ausgeführt werden. Das wiederholte Ausführen von Anweisungen können Sie durch eine Schleife erreichen. Um Anweisungen nur unter bestimmten Bedingungen auszuführen, werden Alternativen (if-, if-else- oder switch-Anweisungen) eingesetzt. Die darin verwendeten Ausdrücke werden zur Laufzeit des Programms ausgewertet und ermöglichen auf diese Weise verschiedene Ausführungsmöglichkeiten.
3.5.1
if-Anweisung
Die if-Anweisung wird eingesetzt, wenn eine oder mehrere Anweisungen nur dann ausgeführt werden sollen, wenn eine bestimmte Bedingung erfüllt ist. Mehrere Anweisungen sind in einem Anweisungsblock zusammenzufassen. Trifft die Bedingung nicht zu, wird die Anweisung bzw. der Anweisungsblock übersprungen und die Programmausführung mit der folgenden Anweisung fortgesetzt. Man spricht auch von einer Verzweigung des Programms. Syntax der if-Anweisung if (ausdruck) Anweisung; // oder mit Anweisungsblock if (ausdruck) { Anweisung1; Anweisung2; ... }
Die if-Anweisung wird durch das reservierte Wort if eingeleitet. Als Bedingung wird ein logischer Ausdruck formuliert, der ein Ergebnis vom Typ boolean liefert. Der logische Ausdruck wird in runden Klammern angegeben. Der Bedingung folgt die Anweisung bzw. der Anweisungsblock. Er wird abgearbeitet, wenn die Bedingung erfüllt ist, also der Ausdruck den logischen Wert true liefert. Setzen Sie hinter dem logischen Ausdruck kein Semikolon. Es würde die if-Anweisung beenden und die folgende Anweisung wird immer ausgeführt.
Java 5 Programmierhandbuch
77
Steuerung des Programmflusses Bei einer Lotterie wird für alle Losnummern, die durch 7 teilbar sind, ein Sonderpreis vergeben. Die Überprüfung der Losnummern erfolgt durch die Anwendung des Modulo-Operators (Modulo 7). Ist das Ergebnis der Modulo-Operation gleich Null, so ist die Bedingung erfüllt und die Ausgabeanweisung wird ausgeführt. Trifft die Bedingung nicht zu, wird die Ausgabeanweisung übersprungen.
Losnummer durch 7 teilbar ? nein
ja Ausgabe: Sonderpreis
weitere Anweisungen
Abb. 3.2: if-Anweisung
Listing 3.4: \Beispiele\de\j2sebuch\kap03\Lotterie.java import java.util.Scanner; public class Lotterie { public static void main(String[] argv) { long i; System.out.print("Geben Sie Ihre Losnummer ein: "); Scanner sc = new Scanner(System.in); i = sc.nextInt(); if(i % 7 == 0) System.out.println("Gratulation, Sie haben einen " "Sonderpreis gewonnen!"); } }
3.5.2
+
if-else-Anweisung
Die if-else-Anweisung ist eine Erweiterung der if-Anweisung. Hier ist zusätzlich das Ausführen einer Anweisung bzw. eines Anweisungsblocks vorgesehen, falls die Bedingung nicht erfüllt ist. Syntax der if-else-Anweisung if(Ausdruck) Anweisung; else Anweisung;
Der erste Teile der if-else-Anweisung entspricht der if-Anweisung. Für den Fall, dass die Bedingung nicht erfüllt ist, gibt es einen weiteren Zweig, den else-Zweig. Die Anweisung bzw. der Anweisungsblock des else-Zweigs wird abgearbeitet, wenn die Bedingung
78
3 – Grundlegende Sprachelemente
nicht erfüllt ist, der Ausdruck also den logischen Wert false liefert. Sollen in einem Zweig mehrere Anweisungen ausgeführt werden, sind diese durch geschweifte Klammern als Anweisungsblock zu kennzeichnen. Nun soll das LotterieBeispiel so abgeändert werden, dass eine Ausgabe immer erfolgt, also auch falls kein Sonderpreis gewonnen wurde. Dies wird durch Hinzufügen des else-Zweigs realisiert.
Losnummer durch 7 teilbar ? ja
nein
Ausgabe: Sonderpreis
Ausgabe: kein Preis
weitere Anweisungen
Abb. 3.3: if-else-Anweisung
Listing 3.5: \Beispiele\de\j2sebuch\kap03\Lotterie2.java import java.util.Scanner; public class Lotterie2 { public static void main(String[] argv) { long i; System.out.print("Geben Sie Ihre Losnummer ein: "); Scanner sc = new Scanner(System.in); i = sc.nextInt(); if(i % 7 == 0) System.out.println("Gratulation, Sie haben einen " + "Sonderpreis gewonnen!"); else System.out.println("Schade, Sie haben keinen " + "Sonderpreis gewonnen!"); } }
Schachteln von if-Anweisungen Hängt die Ausführung bestimmter Anweisungen von mehreren Bedingungen ab, können Sie dies durch das Schachteln von if- bzw. if-else-Anweisungen erreichen. Eine Fehlerquelle ist hier das so genannte »Dangling else« (nachlaufendes else). Schreiben Sie eine verschachtelte if-Anweisung in der folgenden Form, sieht es so aus, als gehöre der else-Zweig zur äußeren if-Anweisung.
Java 5 Programmierhandbuch
79
Steuerung des Programmflusses if (ausdruck) if (ausdruck) anweisung; else anweisung;
// else gehört zur inneren if-Anweisung!
Bei geschachtelten if-else-Anweisungen wird der else-Zweig aber immer der letzten if-Anweisung zugeordnet, die noch keinen else-Zweig besitzt. Die Einrückung des Programmcodes ist irreführend. Damit der else-Zweig zur äußeren if-Anweisung gehört, muss die innere if-Anweisung in geschweifte Klammern gesetzt werden. if (ausdruck) { if (ausdruck) anweisung; } else // else gehört zur äußeren if-Anweisung! anweisung;
Achten Sie immer darauf, dass die gewünschte Struktur der Anweisungen erhalten bleibt, wenn diese ineinander verschachtelt werden. Dies können Sie durch das Setzen von geschweiften Klammern erreichen. Im Lotterie-Beispiel soll nun zusätzlich noch überprüft werden, ob die Losnummern fünfstellig sind. Alle anderen Nummern sind ungültig. Die Prüfung auf die Teilbarkeit durch 7 wird nur für gültige Losnummern ausgeführt.
Listing 3.6: \Beispiele\de\j2sebuch\kap03\Lotterie3.java import java.util.Scanner; public class Lotterie3 { public static void main(String[] argv) { long i; System.out.print("Geben Sie Ihre Losnummer ein: "); Scanner sc = new Scanner(System.in); i = sc.nextInt(); if((i >= 10000) && (i < 100000)) { if(i % 7 == 0) System.out.println("Gratulation, Sie haben einen " + "Sonderpreis gewonnen!");
80
3 – Grundlegende Sprachelemente Listing 3.6: \Beispiele\de\j2sebuch\kap03\Lotterie3.java (Forts.) else System.out.println("Schade, Sie haben keinen " + "Sonderpreis gewonnen!"); } else System.out.println("Falsche Losnummer, " + "die Nummer muss 5 Stellen haben"); } }
In diesem Beispiel wurde die Struktur der äußeren if-Anweisung durch das Setzen der geschweiften Klammern hervorgehoben. Die Klammern sind hier nicht unbedingt erforderlich, dienen aber der Übersichtlichkeit und Lesbarkeit des Codes.
3.5.3
Der Bedingungsoperator ? :
Der dreistellige Bedingungsoperator ermöglicht eine verkürzte Schreibweise bestimmter if-else-Anweisungen. Er kann z.B. für Konstrukte der folgenden Art verwendet werden: if(a > b) max = a; else max = b;
Mit dem Bedingungsoperator lässt sich diese if-else-Anweisung wie folgt schreiben: max = (a > b) ? a : b;
Syntax des Bedingungsoperators Ergebnis = Ausdruck ? Anweisung1: Anweisung2;
Als Bedingung wird ein logischer Ausdruck (Ausdruck) formuliert. Liefert dieser den Wert true, wird Anweisung1 abgearbeitet. Gibt er den Wert false zurück, wird Anweisung2 ausgewertet. Das Ergebnis der ausgeführten Anweisung ist das Resultat dieser Operation. Es kann einer Ergebnisvariablen vom gleichen Datentyp zugewiesen werden. Auch mit dem Bedingungsoperator können Sie die Ausführung von Anweisungen von mehreren Bedingungen abhängig machen, d.h., Sie können auch hier Schachtelungen vornehmen. Der Code wird allerdings schnell unübersichtlich. Aus drei vorgegebenen Zahlen soll die größte ermittelt und ausgegeben werden. Hierfür werden mehrere Bedingungen mithilfe des Bedingungsoperators in einer Anweisung geprüft.
Java 5 Programmierhandbuch
81
Steuerung des Programmflusses Listing 3.7: \Beispiele\de\j2sebuch\kap03\Maximum.java public class Maximum { public static void main(String[] argv) { int i = 1111; int j = 888; int k = 777; int max; max = (i > j) ? ((i > k) ? i : k) : ((j > k) ? j : k); System.out.println("Maximum ist " + max); } }
3.5.4
switch-Anweisung
Die switch-Anweisung ist eine mehrseitige Auswahl. Sie wird auch als Fallauswahl bezeichnet. Hier wird der Wert einer Variablen oder eines Ausdrucks (des Selektors) verwendet und mit den Auswahlwerten der einzelnen Fälle verglichen. Stimmt der Wert des Selektors mit dem Auswahlwert überein, werden die entsprechenden Anweisungen ausgeführt. Der Wert des Selektors und damit auch der Auswahlwert kann vom Datentyp char, byte, short und int sein. Andere Datentypen sind nicht zulässig. Stimmt der Selektorwert mit keinem der Auswahlwerte überein, wird der Default-Zweig abgearbeitet, der optional angegeben werden kann. Steht der Default-Zweig nicht zur Verfügung, wird die Programmabarbeitung mit der Anweisung fortgesetzt, die der switch-Anweisung folgt. Selector =
Wert 1
Wert 2
Anweisung(sblock)
Anweisung(sblock) ˚ ˚ ˚
Wert x
Default
Anweisung(sblock)
Anweisung(sblock)
weitere Anweisungen
Abb. 3.4: switch-Anweisung
82
3 – Grundlegende Sprachelemente
Syntax der switch-Anweisung switch(selector) { case wert1: anweisung1; [break;] case wert2: anweisung2; [break;] ... [default anweisung;] }
Die switch-Anweisung wird mit dem reservierten Wort switch eingeleitet. In Klammern folgt der Selektor-Ausdruck. Im switch-Block werden alle Fälle aufgeführt. Jeder Fall beginnt mit dem Schlüsselwort case, gefolgt vom Auswahlwert. Der Datentyp des Auswahlwerts muss mit dem Datentyp des Selektors übereinstimmen. Anschließend werden die entsprechenden Anweisungen geschrieben. Möchten Sie die switch-Anweisung nach dem Ausführen der zum Fall gehörenden Anweisungen verlassen, müssen Sie die Anweisung break angeben. Anderenfalls werden alle weiteren Anweisungen des switch-Blocks ausgeführt, bis zum Ende des Blocks oder bis zum nächsten break. Ein Vergleich mit den Auswahlwerten wird dabei nicht noch einmal durchgeführt. Für den Fall, dass keiner der Auswahlwerte zutrifft, können Sie den optionalen default-Zweig verwenden. Das Programm gibt zu einer eingegebenen Note die Textform aus. Zunächst wird die Note über die Konsole eingelesen und in der Variablen i gespeichert. Die Variable i ist der Selektor in der switch-Anweisung. In Abhängigkeit vom Wert der Variablen i wird eine entsprechende Meldung auf dem Bildschirm ausgegeben und die switch-Anweisung wird über break verlassen. Liegt der eingegebene Wert nicht im Bereich von 1 bis 6, wird der default-Zweig angesprungen und eine Fehlermeldung angezeigt.
Listing 3.8: \Beispiele\de\j2sebuch\kap03\Noten.java import java.util.Scanner; public class Noten { public static void main(String[] argv) { int i; System.out.print("Geben Sie Ihre Note ein: "); Scanner sc = new Scanner(System.in); i = sc.nextInt(); switch (i) { case 1: System.out.println("1 entspricht sehr gut"); break;
Java 5 Programmierhandbuch
83
Steuerung des Programmflusses Listing 3.8: \Beispiele\de\j2sebuch\kap03\Noten.java (Forts.) case 2: System.out.println("2 entspricht gut"); break; case 3: System.out.println("3 entspricht befriedigend"); break; case 4: System.out.println("4 entspricht ausreichend"); break; case 5: System.out.println("5 entspricht mangelhaft"); break; case 6: System.out.println("6 entspricht ungenuegend"); break; default: System.out.println("Falsche Eingabe"); } } }
In der Praxis müssen manchmal für verschiedene Fälle die gleichen Anweisungen ausgeführt werden. Dies kann durch die Angabe mehrerer Fälle hintereinander erreicht werden. Das Programm erwartet die Eingabe eines Monats als Zahl (z.B. 5 für den Monat Mai) und gibt die Anzahl der Tage in diesem Monat zurück. Dabei werden die Monate, die 30 bzw. 31 Tage haben, zu einem Fall zusammengefasst.
Listing 3.9: \Beispiele\de\j2sebuch\kap03\TageProMonat.java import java.util.Scanner; public class TageProMonat { public static void main(String[] argv) { int monat; System.out.print("Geben Sie den Monat ein: "); Scanner sc = new Scanner(System.in); monat = sc.nextInt(); switch (monat) { case 2: System.out.println("der " + monat + "-te Monat hat 28 oder 29 Tage"); break; case 4: case 6: case 9:
84
3 – Grundlegende Sprachelemente Listing 3.9: \Beispiele\de\j2sebuch\kap03\TageProMonat.java (Forts.) case 11: System.out.println("der " + monat + "-te Monat hat 30 Tage"); break; case 1: case 3: case 5: case 7: case 8: case 10: case 12: System.out.println("der " + monat + "-te Monat hat 31 Tage"); break; default: System.out.println("Falsche Eingabe"); } } }
Die Angabe von Bereichen (z.B. case 1..10) ist in Java nicht möglich.
3.5.5
for-Anweisung
Schleifen Es gibt in Java drei verschiedene Arten von Schleifen, durch die ein Anweisungsblock wiederholt ausgeführt wird. Eine Schleife besteht aus zwei Teilen: der Schleifensteuerung und dem Schleifenkörper. Der Unterschied zwischen den Schleifen liegt in der Schleifensteuerung, d.h. ob die Wiederholung der Anweisungen von einer Variablen oder einer Bedingung abhängt und wann diese Bedingung getestet wird. Wiederholungen
zählergesteuerte Wiederholung
bedingte Wiederholung
kopfgesteuerte Wiederholung
fußgesteuerte Wiederholung
Abb. 3.5: Wiederholungen
Java 5 Programmierhandbuch
85
Steuerung des Programmflusses
Zählergesteuerte Wiederholung Die for-Anweisung ist eine zählergesteuerte Wiederholung (Zählschleife). Die Anzahl der Wiederholungen hängt vom Wert eines Ausdrucks ab. Die Schleifenvariable wird auch Lauf- oder Zählvariable genannt. Der Schleifenkörper wird so lange durchlaufen, bis die Laufvariable bzw. die Laufvariablen den in der Bedingung festgelegten Wert erreicht oder überschritten haben. Die Variablenwerte werden in jedem Schleifendurchlauf über einen Aktualisierungsausdruck verändert.
Laufvariable < Endwert? nein
ja Anweisung
Anweisung
Anweisung
weitere Anweisungen
Abb. 3.6: Zählergesteuerte Wiederholungen
Syntax der for-Anweisung for ([Initialisierung]; [Bedingung]; [Aktualisierung]) Anweisung(-sblock)
Die for-Anweisung wird durch das reservierte Wort for eingeleitet. In runden Klammern ( ) wird die Laufvariable initialisiert, die Abbruchbedingung für die Schleife festgelegt und die Aktualisierung der Laufvariable vorgenommen. Jeder der drei Ausdrücke ist optional. Wenn Sie einen Teil nicht angeben, schreiben Sie nur das Semikolon. Werden im Extremfall alle drei Teile weggelassen, hat die Anweisung die Form for(;;). Im Initialisierungsteil können Sie eine oder mehrere Variablen deklarieren und initialisieren. Diese Variablen sind nur innerhalb der for-Anweisung gültig. Nach der Beendigung der Schleife ist kein Zugriff mehr auf diese Variablen möglich. Variablen, die in der forSchleife deklariert werden, dürfen nicht bereits als lokale Variablen vorliegen, da der Compiler auf diese doppelte Deklaration mit einer Fehlermeldung reagiert. Haben Sie die für die Schleife vorgesehenen Variablen bereits vorher deklariert und initialisiert, kann der Initialisierungsteil in der for-Anweisung leer bleiben. Während der Programmausführung er-
86
3 – Grundlegende Sprachelemente
folgt die Initialisierung beim Eintritt in die Schleife, also vor dem ersten Schleifendurchlauf. Die for-Schleife wird so lange ausgeführt, wie der im Bedingungsteil angegebene logische Ausdruck den Wert true liefert. Sind mehrere Variablenwerte zu überprüfen, müssen die Ausdrücke mit logischen Operatoren verknüpft werden, z.B. (a > 100) && (b > 30). Mehrere logische Ausdrücke können nicht mit Komma getrennt werden. Ist der Bedingungsteil leer, wird automatisch der Wert true angenommen. Die Schleife muss durch eine break-, continue- oder return-Anweisung innerhalb der Schleife beendet werden, sonst läuft sie endlos weiter. Im Aktualisierungsteil kann der Wert der Laufvariablen mit einer festgelegten Schrittweite verändert werden. Hierfür können Sie z.B. die Operatoren ++, --, +=, -=, *= und /= verwenden. Die Änderung der Variablenwerte kann aber auch in den Anweisungen der Schleife erfolgen. Dieser Teil bleibt dann leer. In einer Schleife werden die Zahlen von 1 bis 100 aufsummiert. Im Schleifenkopf wird die Laufvariable i deklariert und mit dem Wert 1 initialisiert. Nach jedem Schleifendurchlauf wird i inkrementiert (i++). Die Schleife wird so lange durchlaufen, bis die Laufvariable i den Wert der Konstanten MAX überschritten hat. Bei jedem Schleifendurchlauf wird der aktuelle Wert der Laufvariablen i zum aktuellen Wert der Variablen summe100 addiert.
Listing 3.10: \Beispiele\de\j2sebuch\kap03\Summe.java public class Summe { public static void main(String[] argv) { int summe100 = 0; final int MAX = 100; for(int i = 1; i 1000) || (i < -1000)) { System.out.println("ungueltige Eingabe: " + "Die Zahl muss zischen -1000 und 1000 liegen"); continue; } summe = summe + i; } System.out.println("Summe der eingegebenen Zahlen: " + summe); } }
break und continue mit Label Wird break oder continue innerhalb einer geschachtelten Schleife eingesetzt, beschränkt sich die Wirkung nur auf die jeweils innere Schleife. Haben Sie beispielsweise drei Schleifen ineinander geschachtelt und in der innersten Schleife den Aufruf einer break-Anweisung verwendet, wird nur die innerste Schleife verlassen.
Java 5 Programmierhandbuch
93
Steuerung des Programmflusses
Java bietet auch die Möglichkeit, die break- und die continue-Anweisung mit einem Label (Sprungmarke) zu versehen, um auch aus verschachtelten Schleifen herausspringen zu können. Auch zum Verlassen eines Blocks kann die benannte break-Anweisung benutzt werden. Bei Verwendung eines Labels ist Folgendes zu beachten: 쐌 Das Label muss unmittelbar vor der Schleife platziert werden, die über die zugehörige break- bzw. continue-Anweisung beendet werden soll. Über eine benannte breakAnweisung kann auch ein Anweisungsblock (der keine Schleife enthält) verlassen werden. 쐌 Für das Label können Sie einen beliebigen Namen festlegen. Es gelten hier dieselben Namenskonventionen wie für Bezeichner. 쐌 Hinter dem Labelnamen wird ein Doppelpunkt angegeben. 쐌 Auf ein Label können auch mehrere break- bzw. continue-Anweisungen verweisen. 쐌 Hinter der break- bzw. continue-Anweisung wird der Labelname (ohne Doppelpunkt) angegeben. 쐌 Wird continue mit einem Label eingesetzt, wird die Anweisung ausgeführt, die dem Label folgt, d.h., die Schleife wird erneut ausgeführt. for-Schleifen werden aber nicht noch einmal initialisiert. 쐌 Wird eine break-Anweisung mit einem Label verwendet, wird die Anweisung abgearbeitet, die dem benannten Anweisungsblock folgt. Auch wenn es vielleicht den Anschein erwecken könnte, die benannte break-Anweisung kann nicht mit der goto-Anweisung anderer Programmiersprachen gleichgesetzt werden. Goto-Anweisungen erlauben Sprünge an beliebige Stellen im Programm. Mithilfe der break-Anweisung sind aber nur gezielte Sprünge an das Ende einer Schleife oder eines Anweisungsblocks möglich. In diesem Beispielprogramm werden zwei Arrays daraufhin untersucht, ob sie einen gemeinsamen Wert enthalten. Es wird jedes Element des ersten Arrays mit jedem Element des zweiten Arrays verglichen. Wird ein gemeinsames Element gefunden, kann die Suche beendet werden. Die Suche wird mit zwei ineinander geschachtelten for-Anweisungen durchgeführt. Nachdem ein gemeinsames Element gefunden ist, werden beide Schleifen mithilfe einer benannten break-Anweisung abgebrochen. Ohne die Sprungmarke würde nur die innere Schleife beendet und die Suche fortgeführt werden. Da das Label vor einem Anweisungsblock steht (der die Schleifen und eine Ausgabeanweisung enthält), erfolgt die Ausgabe »keine gleichen Werte gefunden!« nur, wenn die Schleifen bis zum Ende abgearbeitet werden.
94
3 – Grundlegende Sprachelemente Listing 3.15: \Beispiele\de\j2sebuch\kap03\Summe2.java public class Summe2 { public static void main(String[] argv) { int [] feld1 = {11, 25, 13, 24, 19, 8, 62}; int [] feld2 = {3, 4, 6, 62, 5, 19, 1, 10}; marke: { for (int i : feld1) for (int j : feld2) if (i == j) { System.out.println("erster gemeinsamer Wert: " + i); break marke; } System.out.println("keine gleichen Werte gefunden!"); } } }
Gehen Sie so sparsam wie möglich mit benannten break-Anweisungen um. Sie können die Lesbarkeit und Übersichtlichkeit des Programms beeinträchtigen.
Java 5 Programmierhandbuch
95
4
Klassen, Interfaces und Objekte
4.1
Einführung
Das Verständnis der in diesem Kapitel vorgestellten Sprachelemente von Java stellt die Basis für alle folgenden Kapitel dar. Allerdings wird man nach dem einmaligen Lesen sicher nicht sofort alle Konzepte erfassen oder anwenden können, weil einfach die Anwendungsmöglichkeiten fehlen. Diese werden Sie aber im Laufe der Bearbeitung der anderen Themen zur Genüge erhalten. Später sollte man dieses Kapitel noch einmal durcharbeiten und es erschließen sich weitere Inhalte. Wir werden hier keinen Exkurs in die Verwendung der UML (Unified Modeling Language) oder in objektorientierter Programmierung geben. Vielmehr werden wir hier mit einfachsten Mitteln die Sprachmöglichkeiten zur objektorientierten Programmierung mit Java vorstellen.
4.1.1
Klassen definieren Baupläne
Ohne Klassen geht nichts in Java. Bis auf Kommentare, die package- und die import-Anweisung befinden sich alle anderen Anweisungen innerhalb einer Klasse. Normalerweise besteht eine Java-Anwendung aus zahlreichen Klassen, auch wenn wir bisher immer nur mit einer Klasse gearbeitet haben. Klassen definieren den Bauplan eines abstrakten Objekts. Wie in der Industrie beschreiben Sie über Klassen ein bestimmtes Objekt, z.B. ein abstraktes Auto vom Typ BMW 318i. Das Auto hat bestimmte Eigenschaften wie eine Farbe, eine Breite und eine Länge. Weiterhin besitzt es Methoden, z.B. um die Farbe zu ändern. Damit wären wir auch schon bei einem Grundprinzip der objektorientierten Programmierung. Eine Klasse kapselt Daten, auf die über eine Schnittstelle, die Methoden, zugegriffen werden kann. Ein direkter Zugriff auf die Daten wird in der Regel nicht erlaubt. Diese Kapselung bewirkt, dass man die Daten nicht versehentlich oder unbemerkt verändern kann. Als einzige Zugriffsmöglichkeit auf die Daten eines Objekts stehen nur die Methoden der entsprechenden Klasse zur Verfügung und diese können jeden Zugriff auf die Daten überprüfen. Da man nur mit den Methoden einer Klasse kommunizieren kann, werden diese als Schnittstelle der Klasse mit der Außenwelt bezeichnet. In der Abbildung 4.1 kennzeichnen die Kreise die Datenelemente der Klasse. Diese sind von außen nicht direkt zugänglich. Über die rechts dargestellte Schnittstelle kann mit der Klasse kommuniziert werden.
Klasse
Abb. 4.1: Klasse mit Datenelementen und Schnittstelle
Java 5 Programmierhandbuch
97
Einführung
4.1.2
Objekte sind die konkrete Realisierung des Bauplans
Nachdem nun über eine Klasse ein Bauplan vorliegt, können konkrete Objekte, auch Instanzen genannt, erzeugt werden. Endlich erhalten Sie einen echten BMW und nicht nur die Schablone davon. Das konkrete Objekt hat auch ganz spezifische Eigenschaften, z.B. eine eindeutige Fahrzeugnummer. Die gesamten Eigenschaften eines Objekts, die durch die Werte der Daten repräsentiert werden, nennt man auch den aktuellen Zustand des Objekts. Damit hat jedes Objekt einen individuellen Zustand. Alle Objekte können aber über die gleichen Methoden bearbeitet werden. Sie können ja alle mit Ihrem BMW in die gleiche Werkstatt fahren. In der Abbildung 4.2 wurden von einer Klasse zwei Objekte erzeugt. Jedes Objekt besitzt Datenelemente mit individuellen Werten.
Klasse
Objekt 1
Objekt 2
1
4 2
7
0 4
8
7
Abb. 4.2: Klasse mit Objekten
Wie man Klassen aus einem gegebenen Problembereich identifiziert und entwickelt, ist nicht Gegenstand dieses Buchs. Grundsätzlich ist es jedoch so, dass verschiedene Objekte Ihrer Anwendung identifiziert werden und im Zuge der Abstraktion dieser Objekte mehrere Klassen entstehen. Die Daten, die ein Objekt identifizieren, werden in einer Klasse gekapselt und mithilfe einer Schnittstelle wird der Zugriff auf bestimmte Daten erlaubt. Stellen Sie fest, dass mehrere Klassen einen gemeinsamen Kern besitzen, können Sie eine übergeordnete Klasse definieren und die anderen davon ableiten. Es entstehen Klassenhierarchien. Der Vorgang der Ableitung einer Klasse von einer anderen wird Vererbung genannt. Der Vorteil dieser Hierarchien ist, dass man keine Mega-Klassen besitzt, die über sehr viel Funktionalität verfügen (hier leidet die Übersichtlichkeit), und dass Änderungen leichter durchführbar sind (ändern Sie etwas in einer Klasse, werden diese Änderungen sofort in allen abgeleiteten Klassen sichtbar). Die abgeleiteten Klassen können also auf die Funktionalität der übergeordneten Klasse zurückgreifen.
98
4 – Klassen, Interfaces und Objekte
Die abgeleiteten Klassen (auch Subklassen) erben die Funktionalität der Basisklasse. Die Basisklasse wird auch als Superklasse oder übergeordnete Klasse bezeichnet.
Basisklasse
abgeleitete Klasse
abgeleitete Klasse
Abb. 4.3: Klassenhierarchien durch Vererbung Variablen innerhalb einer Klasse werden auch als Member oder Felder bezeichnet. Wir verwenden hier oft nur den Begriff Variable, unabhängig davon, ob sich diese auf einen primitiven Datentyp wie int oder ein Objekt bezieht. Eine echte Unterscheidung besteht zwischen Instanz- und Klassenvariablen. Während Erstere mit einem Objekt verbunden sind, existieren Letztere nur einmal innerhalb der Klasse (unabhängig von einem Objekt).
[IconInfo]
4.2
Einfache Klassen
Klassen definieren die Baupläne von Objekten. Sie enthalten Datenelemente und Methoden, die mit den Daten arbeiten. Auf der Basis einer Klasse wird später ein Objekt erzeugt. Klassen stellen die Basis sämtlicher Java-Anwendungen dar. Ohne eine Klasse können Sie keine Java-Anwendung erstellen. Klassen können von anderen Klassen abgeleitet werden (Vererbung), sie können abstrakt sein (sie sind noch unvollständig, es können keine Objekte davon erzeugt werden) und sie können verschachtelt werden. Im Folgenden wird zunächst die Syntax für einfache Klassen vorgestellt. Beispiel Die Klasse Mathe besitzt zwei Variablen zahl1 und zahl2 und eine Methode rechneSumme(), die mit den beiden Variablen arbeitet. public class Mathe { private int zahl1; private int zahl2; public int rechneSumme() { return zahl1 + zahl2; } }
Java 5 Programmierhandbuch
99
Objekte
Syntax Optional können Sie die Zugriffsattribute abstract, final und public angeben. Die gleichzeitige Verwendung von abstract und final ist nicht möglich. Nach dem Zugriffsattribut folgen das Schlüsselwort class und der Name der Klasse, der sich an die Regeln eines Bezeichners halten muss. Der Inhalt einer Klasse wird in ein geschweiftes Klammerpaar eingeschlossen. In einer Klasse können Sie in beliebiger Reihenfolge Konstanten, Variablen und Methoden deklarieren. [Attribut] class { // Konstanten // Variablen // Methoden }
Weiter ist zu beachten: 쐌 Innerhalb einer *.java-Datei darf es nur eine Klasse mit dem Zugriffsattribut public geben. In diesem Fall ist es notwendig, dass der Dateiname (ohne Endung) mit dem Klassennamen identisch ist. 쐌 Klassennamen sollten immer groß geschrieben werden. Besteht der Klassenname aus mehreren Wörtern, wird jedes Hauptwort mit einem Großbuchstaben begonnen. Der Name einer Klasse sollte dabei Auskunft über deren Funktion geben und nur aus den Buchstaben (a–z, A–Z), Zahlen und dem Unterstrich bestehen. 쐌 Klassen mit dem Zugriffsattribut public können von allen anderen Klassen genutzt werden. Besitzen Klassen kein Zugriffsattribut, können Sie nur innerhalb des betreffenden Package verwendet werden. 쐌 Verfügt eine Klasse über eine Methode mit der Signatur public static void main(String[] args), kann sie vom Java-Interpreter als Anwendung ausgeführt werden.
4.3
Objekte
Eine Klassendefinition liefert den Bauplan für einen bestimmten Objekttyp. Auf dieser Basis muss jetzt das Objekt erzeugt werden. Zum Erstellen von Objekten verwenden Sie den Operator new. Beispiel Um ein Objekt der Klasse Mathe zu generieren, wird der Typ des Objekts (Mathe) und ein Bezeichner (m) auf der linken Seite des Gleichheitszeichens angegeben. Dem Bezeichner m wird das über new erzeugte Mathe-Objekt zugewiesen. Die Variable m nennt man auch Objektvariable, Instanzvariable oder Referenzvariable. Diese Bezeichnungen weisen darauf hin, dass über m Zugriff auf ein Objekt besteht. Der Begriff Referenzvariable stammt daher, dass sich das Objekt irgendwo im Speicher befindet und die Variable darauf verweist (referenziert). Es können sogar mehrere Variablen das gleiche Objekt referenzieren.
100
4 – Klassen, Interfaces und Objekte Mathe m = new Mathe(); // oder eine getrennte Deklaration und Initialisierung Mathe m; m = new Mathe();
Eine Ausnahme bei der Objekterzeugung mit new bilden die Klassen String und Array. Objekte dieser Klassen können auch über Literale (unveränderliche Werte) erzeugt werden. Die beiden folgenden Anweisungen sind deshalb gleichwertig. [IconInfo]
String s = "Dies ist ein String"; String s = new String("Dies ist ein String");
Syntax Zuerst müssen Sie eine Variable vom Typ einer Klasse deklarieren. In einer weiteren Anweisung weisen Sie der Variablen durch die Angabe von new, dem Namen der Klasse und einem Klammerpaar ein Objekt zu. Sie können beide Anweisungen auch in einer Anweisung zusammenfassen. ; = new (); // oder = new ();
Über die Objektvariable lässt sich auf die öffentlichen Methoden und Variablen einer Klasse zugreifen. Objekterzeugung Durch die folgende Anweisung wird der Variablen ein Objekt vom Typ der angegebenen Klasse zugewiesen. Der Vorgang der Objekterzeugung besteht konkret darin, dass Speicher für das Objekt bereitgestellt, das Objekt erzeugt und initialisiert wird. Anschließend wird der Variablen eine Referenz auf das Objekt zugewiesen. Über diese Referenz kann die Variable mit dem Objekt arbeiten. Für jedes Objekt werden Kopien der Instanzvariablen erzeugt. Auf diese Weise ergibt sich der aktuelle Zustand eines Objekts aus den aktuellen Werten dieser Variablen. Die Methoden einer Klasse werden dagegen von allen Objekten dieses Typs gemeinsam genutzt. Jede Methode erhält hierfür intern eine Referenz auf das zu verwendende Objekt. Instanzvariable = new (); Instanzvariable.ObjektMethode(); ... Instanzvariable.ObjektVariable ... ;
Java 5 Programmierhandbuch
101
Objekte
Der Zugriff einer Instanzvariablen auf die Methoden und Variablen eines Objekts kann nur für die öffentlichen Elemente erfolgen. Hierfür wird hinter dem Namen der Instanzvariablen ein Punkt und das betreffende Element, z.B. eine Methode oder eine Variable, angegeben. Je nachdem, wo Sie eine Objektvariable deklarieren, kann die sofortige Erzeugung des Objekts Pflicht sein. Die Variable m wird im folgenden Beispiel innerhalb der Klasse ObjektTest, aber außerhalb einer Methode deklariert und bekommt sofort ein Objekt zugewiesen. Die Variable m2 wird nur deklariert. In der Methode test() wird eine weitere Variable m3 vom Typ Mathe deklariert. Hier besteht die Pflicht, diese vor ihrer ersten Verwendung zu initialisieren, das heißt, ihr ein Objekt zuzuweisen. Einen Aufruf der Methode addiere() für m3 vor der Erzeugung des Objekts quittiert der Compiler mit einer Fehlermeldung der Art variable m3 might not have been initialized (die Variable m3 ist nicht initialisiert). Benutzen Sie die Variable m2 dagegen vor ihrer Initialisierung, ist der Compiler zufrieden (er kann ja nicht prüfen, ob Sie die Variable m2 nicht vielleicht in einer anderen Methode initialisiert haben). Sie erhalten aber eine NullPointerException zur Laufzeit der Anwendung. public class ObjektTest { Mathe m = new Mathe(); Mathe m2; public void test() { m2.addiere(10, 11); // Laufzeitfehler Mathe m3; m3.addiere(10, 11); // Compilerfehler m3 = new Mathe(); m2 = new Mathe(); } }
Null-Werte Wurde einer Instanzvariablen noch kein Objekt zugewiesen, verweist sie auf den Wert null. Versuchen Sie in diesem Fall, Methoden des Objekts aufzurufen, wird eine NullPointerException ausgelöst. Mathe m; m.rechneSumme();
// => NullPointerException
Sind Sie nicht sicher, ob eine Instanzvariable auf null verweist, prüfen Sie dies mit der folgenden if-Anweisung. if(m != null) m.rechneSumme();
102
4 – Klassen, Interfaces und Objekte
Lebensdauer von Objekten Im gezeigten Beispiel der Klasse ObjektTest existiert das Objekt m, solange es ein Objekt der Klasse ObjektTest gibt. Das Objekt m3 existiert dagegen nur während der Ausführung der Methode test(). Ein Objekt lebt grundsätzlich mindestens so lange, wie noch Variablen das Objekt referenzieren. Die konkrete Freigabe des Objekts hängt aber vom Zeitpunkt der Entsorgung durch den Garbage Collector ab. Sie selbst haben keine Möglichkeit, ein Objekt explizit freizugeben. Allerdings können Sie eine Referenz auf ein Objekt entfernen und es damit zum Abschuss freigeben. Weisen Sie der Instanzvariablen in diesem Fall einfach den Wert null zu. Mathe m = new Mathe(); ... m = null;
Der Garbage Collector Existiert keine Referenz mehr auf ein Objekt, kann es vom Garbage Collector eingesammelt und der Speicher freigegeben werden. In Java müssen Sie sich also nicht selbst um die Speicherfreigabe bemühen. Die interne Funktionsweise des Garbage Collector, kurz GC, wurde ständig optimiert. Er läuft immer im Hintergrund einer Anwendung mit niedriger Priorität und erledigt seine Aufgabe. Wann er genau ein Objekt beseitigt, kann nicht beeinflusst werden. Ist eine Java-Anwendung beendet, wird automatisch der von ihr belegte Speicher freigegeben. Diese Arbeit wird aber nicht mehr vom GC erledigt, sondern vom Java-Laufzeitsystem. Die Referenz auf das eigene Objekt über this Wird ein Objekt erzeugt, steht automatisch über die Variable this eine Referenz auf dieses Objekt zur Verfügung. Innerhalb einer Methode beziehen Sie sich also mit this immer auf das aktuell zu verwendende Objekt. Es gibt verschiedene Anwendungsmöglichkeiten für das Schlüsselwort this. 쐌 Sie können sich über this immer auf die Variablen des aktuellen Objekts beziehen. Dies wird häufig bei der Parameterübergabe an Methoden genutzt, die denselben Namen besitzen wie die Variable des Objekts. class ZahlenSpeicher { private int zahl; public void setZahl(int zahl) { this.zahl = zahl; } }
Java 5 Programmierhandbuch
103
Methoden
쐌 Erwartet eine Methode eine Referenz auf das eigene Objekt oder gibt sie einen solchen Wert zurück, wird this benutzt. Auch beim Vergleich anderer Objekte mit dem aktuellen Objekt wird this benötigt. Im folgenden Beispiel testet die Methode pruefe(), ob der übergebene Parameter das gleiche Objekt referenziert. class ObjektVergleich { public boolean pruefe(ObjektVergleich ov) { return (this == ov); } }
쐌 Bei der Verkettung von Konstruktoren wird ebenfalls this benötigt (siehe später).
4.4
Methoden
Die eigentliche Arbeit in Ihren Anwendungen wird innerhalb einer Methode durchgeführt. Methoden entsprechen Funktionen, Prozeduren oder Unterprogrammen im Sprachgebrauch anderer Programmiersprachen. Die Bezeichnung Methode kennzeichnet allerdings speziell die Zugehörigkeit zu einer Klasse. Sie haben als Programmierer die Aufgabe, die Arbeitsschritte Ihres Programms sinnvoll auf Methoden zu verteilen. Eine Methode sollte genau eine bestimmte Aufgabe erfüllen. Dadurch kann eine Methode von mehreren Stellen einer Anwendung genutzt werden. Methoden definieren das Verhalten von Objekten, während Variablen den Zustand eines Objekts charakterisieren. Beispiel Sie möchten eine Anwendung schreiben, die zu einem Gewicht und der Größe einer Person den BMI (Body Mass Index) berechnet. Werte größer als 30 sollten Ihnen übrigens zu denken geben. Der BMI berechnet sich aus dem Gewicht, geteilt durch das Produkt der Größe in Metern. Wenn Sie also beispielsweise 80kg wiegen und 1,80 Meter groß sind, haben Sie einen BMI von 80 / (1,8 * 1,8) = 24,69. Da haben wir aber noch einmal Glück gehabt. Die Werte sollen über ein Eingabefenster erfasst und das Ergebnis auch darin ausgegeben werden. Sie können folgende Methoden implementieren: 쐌 Die Methode auslesen() liest die Daten aus dem Eingabefenster und prüft die eingegebenen Werte. So sind beispielsweise nur Zahleneingaben möglich. Zusätzlich könnten Sie noch auf sinnvolle Werte prüfen (Gewicht > 40kg usw.). 쐌 Über die Methode berechne() bestimmen Sie aus den ermittelten Werten den BMI. 쐌 Eine letzte Methode ausgeben() dient zur Anzeige des berechneten Werts im Fenster. Warum dieser Aufwand? Warum werden nicht alle Anweisungen in eine Methode eingefügt? Wenn Sie beispielsweise den berechneten Wert nicht im Fenster ausgeben wollen, sondern auf einen Drucker (schön groß zum an die Wand hängen), müssen Sie nur die Methode ausgeben() überarbeiten. Die beiden anderen Methoden bleiben unberührt.
104
4 – Klassen, Interfaces und Objekte
4.4.1
Einfache Methoden ohne Parameterübergabe
Bei der einfachsten Verwendung einer Methode wird diese direkt aufgerufen und gibt optional einen Wert zurück. Methoden müssen sich immer innerhalb einer Klasse befinden. Sie bestehen aus einem Deklarationsteil und einem Methodenrumpf. Syntax der Methodendeklaration 쐌 Eine Methode wird immer innerhalb einer Klasse deklariert. 쐌 Sie können optional ein Zugriffsattribut vor die Methode setzen. 쐌 Anschließend muss der Rückgabetyp angegeben werden. Dieser kann ein primitiver Typ, ein Referenztyp oder void (keine Rückgabe) sein. 쐌 Es folgt der Name der Methode, der sich an die Konventionen eines Bezeichners halten muss. Dem Methodennamen folgt ein Klammerpaar. 쐌 Innerhalb der Methode werden die Anweisungen eingefügt. Sie können innerhalb einer Methode ebenfalls Variablen, Konstanten oder sogar Klassen deklarieren. Diese sind nur innerhalb der Methode gültig. Es wird dann beispielsweise von lokalen Variablen gesprochen. 쐌 Wenn Sie einen Rückgabetyp außer void verwenden, muss mindestens eine returnAnweisung am Ende der Methode angegeben werden, die einen Wert von diesem Typ liefert. Der an return übergebene Ausdruck muss nicht zwingend in Klammern eingeschlossen werden. Besitzt die Methode mehrere Ausführungszweige, die zum Verlassen der Methode führen (if-else usw.), muss jeder über eine return-Anweisung verfügen. Ansonsten meldet der Compiler einen Fehler. Liefert eine Methode void zurück, kann sie optional über die Angabe von return jederzeit verlassen werden. 쐌 Durch den Aufruf von return beenden Sie eine Methode sofort. Sie können prinzipiell beliebig viele return-Anweisungen verwenden, sofern der dahinter liegende Code noch erreicht werden kann. Sonst meldet der Compiler auch in diesem Fall einen Fehler. class { [public|protected|private] MethodenName() { Anweisungen; [return ;] } }
Die einzige Methode, die keinen Rückgabetyp benötigt, ist der Konstruktor einer Klasse. Er hat den gleichen Namen wie die Klasse, in der er sich befindet. Konstruktoren werden im Folgenden noch vorgestellt. [IconInfo]
Java 5 Programmierhandbuch
105
Methoden
Syntax des Methodenaufrufs 쐌 Methoden der eigenen Klasse können direkt aufgerufen werden, wie z.B. die Methode getZahl() in der Methode test() des folgenden Beispiels. Dem Namen der Methode muss in jedem Fall ein Klammerpaar angefügt werden. 쐌 Besitzt eine Methode einen Rückgabewert wie z.B. getZahl(), kann dieser einer Variablen zugewiesen werden. Sie können die Methode aber auch ohne die Auswertung des Rückgabewerts aufrufen (siehe Methode test()). 쐌 Für den Einsatz der Methoden eines anderen Objekts verwenden Sie den Objektnamen und fügen durch einen Punkt getrennt den Namen der Methode an. 쐌 Um die Methoden eines Objekts zu nutzen, muss das Objekt über new erzeugt worden sein. Die Anweisung k1.macheNix(); ist in Ordnung, weil k1 ein Objekt über new zugewiesen wurde. Die Anweisung k2.macheNix(); führt zur Ausführungszeit der Anwendung zu einer NullPointerException, da k2 kein Objekt zugewiesen wurde und der Standardwert null ist. Letztendlich müssen lokale Variablen wie k2Local sofort initialisiert werden, weil Anweisungen wie k2Lokal.macheNix(); mit einem Compilerfehler quittiert werden. class Klasse1 { Klasse2 k1 = new Klasse2(); Klasse2 k2; int getZahl() { return 10; } void test() { int i = getZahl(); getZahl(); k1.macheNix(); k2.macheNix(); Klasse2 k2Lokal; k2Lokal.macheNix(); } } class Klasse2 { void macheNix() {} }
Im Konstruktor Methoden1() wird die Methode sum() aufgerufen, welche die Summe zweier Zahlen berechnet und das Ergebnis ausgibt. [IconInfo]
106
4 – Klassen, Interfaces und Objekte Listing 4.1: \Beispiele\de\j2sebuch\kap04\Methoden1.java public class Methoden1 { public Methoden1() { sum(); } public void sum() { int zahl1 = 10; int zahl2 = 11; System.out.printf("Die Summe von %d und %d ist %d", zahl1, zahl2, (zahl1 + zahl2)); } public static void main(String[] args) { new Methoden1(); } }
4.4.2
Parameter übergeben
Die Methode sum() im Listing 4.1 hatte den Nachteil, dass die beiden zu addierenden Zahlen in der Methode deklariert wurden (man könnte auch bemerken, dass die Methode auf diese Weise nicht verwendbar ist). Besser wäre es, man könnte die beiden Zahlen als Parameter übergeben. Sie erreichen dies, wenn Sie in den Klammern im Methodenkopf jeweils pro Parameter einen Datentyp und einen Bezeichner angeben. Der Bezeichner kann dann in der Methode wie eine lokale Variable genutzt werden. Beim Aufruf der Methode können Sie Werte in Form von Literalen oder als Variablen vom geforderten Typ übergeben. public int sum(int zahl1, int zahl2) { return zahl1 + zahl2; } ... System.out.printf("Die Summe von %d und %d ist %d.", 10, 11, sum(10, 11));
Syntax 쐌 Definieren Sie die Methode wie bisher. In den Klammern geben Sie für jeden Parameter den Typ sowie einen Bezeichner an. 쐌 Besitzt der Bezeichner den gleichen Namen wie eine Instanz- oder Klassenvariable, wird diese dadurch verdeckt. In den Methoden kann auf Instanzvariablen über this zugegriffen werden. Lokale Variablen und Parameternamen müssen dagegen immer verschieden sein.
Java 5 Programmierhandbuch
107
Methoden
쐌 Mehrere Parameter werden durch Kommata getrennt. 쐌 Beim Aufruf einer Methode mit Parametern müssen alle Parameter in der entsprechenden Reihenfolge und mit dem verlangten Typ in Klammern an die Methode übergeben werden. Die Parameter und der Name einer Methode definieren zusammen die Signatur dieser Methode. Zwei Methoden sind demnach gleich, wenn sie denselben Namen und die gleiche Parameterliste besitzen. Der Rückgabewert spielt keine Rolle. [IconInfo]
Ändern der Parameterwerte Sie können die Werte der übergebenen Parameter natürlich in einer Methode verändern. Die Parameterübergabe erfolgt in Java immer über Call By Value. Dies bedeutet, es wird eine Kopie des originalen Werts übergeben. Bei primitiven Typen hat dies die Wirkung, dass Änderungen eines Parameters in der Methode keine Auswirkungen auf seinen originalen Wert haben. Übergeben Sie jedoch Objektreferenzen an eine Methode, wird zwar auch eine Kopie der Referenzvariablen erstellt, die Referenz zeigt aber immer noch auf das gleiche Objekt. Änderungen am Objekt wirken sich also hier tatsächlich auf das originale Objekt aus.
[IconInfo]
Im Beispiel wird eine Variable zahlP von einem primitiven Typ int sowie eine Objektvariable zahlO vom Typ Zahl deklariert. Beide werden mit einem Wert initialisiert und zusätzlich mit einem Literal (1000) an die Methode sum() übergeben. In der Methode sum() wird beiden Parametern ein neuer Wert zugewiesen. Es wird die Summe mit den neuen Werten berechnet. Nach der Beendigung der Methode werden die Werte der Variablen zahlP und zahlO ausgegeben. Wie Sie sehen können, wurde nur der Wert von zahlO dauerhaft geändert, während zahlP ihren alten Wert behalten hat.
Listing 4.2: \Beispiele\de\j2sebuch\kap04\Methoden2.java public class Methoden2 { public Methoden2() { int zahlP = 100; Zahl zahlO = new Zahl(); zahlO.zahl = 10000; sum(zahlP, 1000, zahlO); System.out.println("zahlP hat den Wert: " + zahlP); System.out.println("zahlO hat den Wert: " + zahlO.zahl); }
108
4 – Klassen, Interfaces und Objekte Listing 4.2: \Beispiele\de\j2sebuch\kap04\Methoden2.java (Forts.) public void sum(int zahl1, int zahl2, Zahl zahl3) { zahl1 = 10; zahl3.zahl = 20; System.out.printf("Die Summe von %d und %d und %d ist %d\n", zahl1, zahl2, zahl3.zahl, (zahl1 + zahl2 + zahl3.zahl)); } public static void main(String[] args) { new Methoden2(); } } class Zahl { int zahl = 0; }
Ausgabe: Die Summe von 10 und 1000 und 20 ist 1030 zahlP hat den Wert: 100 zahlO hat den Wert: 20
Variable Parameterlisten Zur Verwendung in Methoden, die eine unterschiedliche Anzahl von Parametern verarbeiten müssen (z.B. wie bei der Methode printf() des Objekts System.out), kann ein variabler Parameter benutzt werden. Ein variabler Parameter muss als letzter Parameter angegeben werden. Er wird mit einer Ellipse (3 Punkten) eingeleitet. Für den variablen Parameter können Sie beliebig viele Parameterwerte vom gleichen Typ an die Methode übergeben. Ist die Methode überladen, werden zuerst die überladenen Methoden aufgerufen. Gelesen wird der variable Parameter als »Sequenz von «, also im folgenden Beispiel als Sequenz von int. int sum(int ...feld) { ... } ... System.out.println(sum(1, 2, 3));
Java 5 Programmierhandbuch
109
Methoden
[IconInfo]
Es werden drei verschiedene Aufrufe der Methode sum() durchgeführt. Da es zwei Methoden sum() mit einem und zwei normalen Parametern gibt, werden diese der variablen Parameterliste vorgezogen. Erst beim Aufruf mit fünf Parametern wird die Methode mit der variablen Parameterliste verwendet. Auch die Parameter an die Methode main() können jetzt in einer anderen Schreibweise übergeben werden. Der erzeugte ByteCode ist letztendlich der gleiche.
Listing 4.3: \Beispiele\de\j2sebuch\kap04\VarArgs.java public class VarArgs { public VarArgs() { System.out.println(sum(10)); System.out.println(sum(10, 11)); System.out.println(sum(10, 11, 12, 13)); } public int sum(int zahl1) { System.out.println("Summe 1"); return zahl1; } public int sum(int zahl1, int zahl2) { System.out.println("Summe 2"); return zahl1 + zahl2; } public int sum(int ... zahlen) { int erg = 0; System.out.println("Summe variabel"); System.out.println("Es wurden " + zahlen.length + " Parameter übergeben."); for(int elems: zahlen) erg = erg + elems; return erg; } public static void main(String ...args) { new VarArgs(); } }
110
4 – Klassen, Interfaces und Objekte
Überladen Methoden werden in Java über ihren Namen und die Anzahl und Typen ihrer Parameter unterschieden. Dies erlaubt es, Methoden mit gleichem Namen, aber verschiedenen Parameterlisten zu benutzen. Verwenden Sie diese Möglichkeit, wenn mehrere Methoden die gleichen Aufgaben erledigen, sich aber nur in den übergebenen Parametern unterscheiden. Wenn Sie z.B. zwei Zahlen addieren möchten, diese aber verschiedene Datentypen besitzen können, erstellen Sie für jede Typkombination eine separate Methode. Die Methoden können aber alle denselben Namen besitzen, wie im Folgenden die Methode add(). int add(int zahl1, int zahl2) double add(double zahl1, double zahl2) float add(float zahl1, float zahl2)
[IconInfo]
Passt beim Aufruf einer überladenen Methode keine Typkombination, versucht Java durch seine automatische Typkonvertierung eine passende Methode zu finden. So können Sie z.B. die Methode add() auch mit den Werten 10 und 10.0 aufrufen, solange Sie das Ergebnis einem doubleTyp zuweisen. Der Wert 10 wird automatisch (und verlustfrei, sonst geht es nicht) in den Typ double konvertiert (gecastet).
Die Methode add() wird überladen, damit sie einmal mit int- und ein weiteres Mal mit double-Parametern aufgerufen werden kann. [IconInfo]
Listing 4.4: \Beispiele\de\j2sebuch\kap04\Ueberladen.java public class Ueberladen { public Ueberladen() { System.out.println(add(10, 11)); System.out.println(add(10.0, 11.0)); } public int add(int zahl1, int zahl2) { return zahl1 + zahl2; } public double sum(double zahl1, double zahl2) { return zahl1 + zahl2; }
Java 5 Programmierhandbuch
111
Methoden Listing 4.4: \Beispiele\de\j2sebuch\kap04\Ueberladen.java (Forts.) public static void main(String[] args) { new Ueberladen(); } }
Rekursion Für den rekursiven Aufruf von Methoden gibt es kein spezielles Sprachmittel in Java. Dies ist aber eine immer wiederkehrende Aufgabe und soll deshalb hier kurz erläutert werden. Bei einer Rekursion ruft sich eine Methode immer wieder selbst auf. Damit die Rekursion irgendwann beendet wird, muss ein Abbruchkriterium definiert werden. Wann kann beispielsweise eine Rekursion eingesetzt werden? 쐌 Wenn Sie die Dateien eines Verzeichnisses inklusive aller Unterverzeichnisse ermitteln wollen, können Sie rekursiv vorgehen. 쐌 Die Berechnung der Fakultät n! (3! = 3*2*1) oder die Addition einer Zahlenfolge (1+2+3+...) kann rekursiv erfolgen.
[IconInfo]
[IconInfo]
Bei der Verwendung der Rekursion muss immer beachtet werden, dass das Abbruchkriterium auch wirklich einmal erfüllt ist. Weiterhin müssen Sie daran denken, dass bei jedem neuen Rekursionsschritt ein erneuter Methodenaufruf inklusive einer optionalen Parameterübergabe erfolgt. Wenn Sie eine Methode 100.000 Mal mit 10 Parametern aufrufen, wird also schon mindestens 1 MB Speicher für die Parameter benötigt (wenn man davon ausgeht, dass ein Parameter mindestens 1 Byte in Anspruch nimmt).
Zur Ermittlung der Summe aller ganzen Zahlen von 1 bis n wird der Methode zahlenFolgeSumme() nur die letzte Zahl n übergeben. Die Summe ergibt sich beispielsweise bei einem Wert von n=10 aus der Berechnung von 1+2+3+4+5+6+7+8+9+10 (=55). Natürlich geht es auch einfacher ((n*(n+1))/2), aber das soll jetzt mal keine Rolle spielen. Innerhalb der Methode zahlenFolgeSumme() wird geprüft, ob der Parameter größer als 0 ist. In diesem Fall wird der Parameterwert mit dem Rückgabewert der Methode zahlenFolgeSumme() addiert. Es wird der Methode zahlenFolgeSumme() jedoch ein Wert übergeben, der um 1 kleiner ist. Ist der Parameterwert 0, wird die Methode nicht erneut aufgerufen, sondern es wird 0 zurückgegeben und die Rekursion wird beendet. Sie erhalten damit die folgende Aufrufreihenfolge: zahlenFolgeSumme(10) => 10 + zahlenFolgeSumme(9) => 10 + 9 + zahlenFolgeSumme(8) => ... => 10 + 9 + ... + 1 + zahlenFolgeSumme(0) => 10 + 9 + ... + 1 + 0 = 55
112
4 – Klassen, Interfaces und Objekte Listing 4.5: \Beispiele\de\j2sebuch\kap04\Rekursion.java public class Rekursion { public Rekursion() { System.out.println(zahlenFolgeSumme(10)); } public int zahlenFolgeSumme(int zahl) { if(zahl > 0) return zahl + zahlenFolgeSumme(zahl - 1); else return zahl; } public static void main(String[] args) { new Rekursion(); } }
4.5
Konstruktoren und Destruktoren
Wenn Sie ein Objekt über den Operator new erzeugen, wird automatisch der zugehörige Konstruktor des Objekts aufgerufen. Über einen Konstruktor können Sie bestimmte Eigenschaften des Objekts schon bei dessen Anlegen initialisieren. Auch wenn Sie keinen Konstruktor für eine Klasse bereitstellen, wird vom Compiler automatisch ein Standardkonstruktor (Defaultkonstruktor) erzeugt. Syntax 쐌 Ein Konstruktor entspricht in seinem Aufbau einer Methode, besitzt das Zugriffsattribut public, keinen Rückgabetyp, auch nicht void, und wird nach der Klasse benannt, in der er sich befindet. 쐌 Konstruktoren werden automatisch beim Erzeugen eines Objekts mit new aufgerufen. Sie können einen Konstruktor nicht direkt aufrufen. 쐌 Konstruktoren lassen sich wie Methoden überladen. 쐌 Innerhalb eines Konstruktors kann über this ein anderer Konstruktor aufgerufen werden. this wird in diesem Fall wie eine Methode verwendet. Die Angabe von this muss als erste Anweisung erfolgen, ansonsten meldet der Compiler einen Fehler. Diese Technik wird als Konstruktorverkettung bezeichnet. Sie erlaubt die Wiederverwendung von Code, in dem sämtliche Initialisierungen nur in einem Konstruktor erfolgen, der über Standardparameter von den anderen Konstruktoren aufgerufen wird. 쐌 Besitzt eine Klasse keinen Konstruktor, wird über den Compiler ein parameterloser Standardkonstruktor bereitgestellt. Dieser ist zum Anlegen eines Objekts notwendig.
Java 5 Programmierhandbuch
113
Konstruktoren und Destruktoren
쐌 Wenn Sie einen beliebigen Konstruktor mit oder ohne Parameter bereitstellen, wird kein Standardkonstruktor vom Compiler erzeugt. In einigen Fällen benötigen Sie jedoch zwingend einen Standardkonstruktor, den Sie dann dennoch bereitstellen müssen. 쐌 Beim Aufruf des Operators new können Sie in Klammern Parameter angeben. In diesem Fall wird der passende Konstruktor zur Objekterzeugung verwendet. class KlassenName { public KlassenName() {} public KlassenName(Parameter) { this(Parameter); } } ... t = new ([Parameter]);
Instanzinitialisierer Neben einem Konstruktor können optional ein oder mehrere Instanzinitialisierer (auch Initialisierungsblöcke genannt) definiert werden, die beim Anlegen einer Objektinstanz noch vor den Konstruktoren aufgerufen werden. Der Initialisierungsblock wird in einer Klasse durch einen einfachen Anweisungsblock ohne Namen definiert. Sie können auch mehrere solche Anweisungsblöcke definieren. Intern werden die Anweisungen der Initialisierungsblöcke in jeden Konstruktor eingefügt. Bei der Verkettung von Konstruktoren wird der Initialisierungsblock so eingefügt, dass er genau einmal aufgerufen wird. Befindet sich umfangreicher Code in den Initialisierungsblöcken, sollte dieser besser in eine Methode ausgelagert und die Methode aufgerufen werden. Instanzinitialisierer werden beispielsweise bei anonymen, inneren Klassen benötigt, weil diese keine Konstruktoren besitzen. Die Klassen haben keinen Namen (sie sind eben anonym), deshalb kann auch der Konstruktor nicht benannt werden. Beispiel class KlassenName { { Anweisungen; } }
114
// Beginn des Instanzinitialisierers // Ende
4 – Klassen, Interfaces und Objekte
[IconInfo]
Im Instanzinitialisierer wird lediglich eine Nachricht ausgegeben und die Variable wert mit 1 initialisiert. Der Standardkonstruktor ruft über die Konstruktorverkettung einen weiteren Konstruktor auf und übergibt als Standardwert für die Variable wert den Wert 0. Die Reihenfolge der Aufrufe ist also: Instanzinitialisierer, Standardkonstruktor und parametrisierter Konstruktor.
Listing 4.6: \Beispiele\de\j2sebuch\kap04\Konstruktor.java public class Konstruktor { int wert; { // hier beginnt der Initialisierungsblock System.out.println("Initialisierungsblock"); wert = 1; } // und hier endet er public Konstruktor() { this(0); System.out.println("Standardkonstruktor"); } public Konstruktor(int wert) { this.wert = wert; System.out.println("Konstruktor (int wert)"); } public static void main(String[] args) { new Konstruktor(); } }
Fabriken Möchten Sie verhindern, dass von einer Klasse über den Operator new Objekte erstellt werden können, versehen Sie den Standardkonstruktor mit dem Attribut private. Dadurch kann er nicht mehr zur Objekterzeugung verwendet werden. Stellen Sie stattdessen eine statische Methode zur Verfügung, die ein Objekt der Klasse generiert. Diese Methoden werden auch als Fabrik-Methoden bezeichnet (factory methods).
[IconInfo]
Die Objekterstellung über den Operator new scheitert hier, weil der Standardkonstruktor nicht verfügbar ist. Stattdessen wurde in der Klasse Fabrik eine statische Methode newInstance() definiert (diese existiert in der Klasse unabhängig von einem Objekt), die ein Objekt vom Typ Fabrik erzeugt und zurückgibt. Auf diese Weise haben Sie die volle Kontrolle über die Objekterzeugung für diese Klasse.
Java 5 Programmierhandbuch
115
Konstruktoren und Destruktoren Listing 4.7: \Beispiele\de\j2sebuch\kap04\Fabrik.java public class Fabrik { public static Fabrik newInstance() { return new Fabrik(); } private Fabrik() { } public void ausgabe() { System.out.println("Hallo"); } public static void main(String[] args) { Fabrik f = Fabrik.newInstance(); f.ausgabe(); } }
Destruktoren Wir verwenden hier den Begriff Destruktor nur deshalb, weil er als Gegenpart eines Konstruktors geläufig ist. Normalerweise wird ein Destruktor aufgerufen, wenn ein Objekt zerstört wird. In Java gibt es keine Möglichkeit, ein Objekt explizit zu zerstören, d.h. den Speicher des Objekts freizugeben. Stattdessen wird ein Objekt automatisch vom Garbage Collector entsorgt, wenn es keine Referenzen mehr darauf gibt. Destruktoren werden in anderen Programmiersprachen oft dazu eingesetzt, den Speicher für angelegte Objekte freizugeben. Dies ist in Java nie notwendig, da der Speicher immer automatisch freigegeben wird. Anders sieht es aus, wenn im Konstruktor geöffnete Datenbankverbindungen oder Dateien wieder geschlossen werden müssen. Dies ist auch unter Java nicht automatisch möglich. Java stellt mit der Methode finalize() eine Möglichkeit zur Verfügung, Anweisungen auszuführen, wenn der Garbage Collector ein Objekt beseitigt. Das Problem ist hierbei nur, dass nicht feststeht, wann und ob dies überhaupt jemals passiert und auch nicht in welcher Reihenfolge. Da der GC als Hintergrundprozess arbeitet, ist sein Ausführungszeitpunkt nicht festgelegt. Wird eine Anwendung vor dem Aufruf des GC beendet, kommt es nicht zur Ausführung der Methode finalize(). Der Speicher wird aber trotzdem freigegeben. Die Methode finalize() ist demnach kein richtiger Destruktor und ihre Verwendung ist nur in wenigen Fällen sinnvoll. Wo kann man demnach diese Methode nutzen ? 쐌 Wenn Sie über JNI (Java Native Interface) native Erweiterungen mit einer eigenen Speicherverwaltung entwickeln, kann finalize() zur Ressourcenfreigabe genutzt werden. Allerdings stellt sich auch hier das Problem, dass die Methode nicht immer aufgerufen wird.
116
4 – Klassen, Interfaces und Objekte
쐌 Sie können Statistiken über die Speicherverwaltung Ihrer Anwendung erstellen. 쐌 Über eine Folge von Aufrufen der Methode System.runFinalization() und das Prüfen von Flags, die in finalize() nach dessen Aufruf gesetzt wurden, können Sie feststellen, ob finalize() tatsächlich ausgeführt wurde. Auf diese Weise lässt sich wenigstens der Aufruf von finalize() sicherstellen. protected void finalize() {}
Über die Methode gc() der Klasse System können Sie den GC explizit starten. Er wird aber nicht unbedingt so lange ausgeführt, bis wirklich alle nicht mehr benötigten Objekte entsorgt sind. Die Methode runFinalization() der Klasse System bewirkt, dass die Methode finalize() der entsorgten Objekte aufgerufen wird, bei denen dies noch nicht erfolgt ist. Auch dies geschieht nicht unbedingt sofort, wenn das Objekt vom GC eingesammelt wird. Weiterhin wird auch hier nicht garantiert, dass dies bei wirklich allen entsorgten Objekten erfolgt. System.gc(); System.runFinalization();
[IconInfo]
Es gibt sicher noch Anwendungsgebiete, in denen die Methode finalize() Sinn macht. Für die Erstellung von Standardanwendungen sollten Sie diese Methode besser aus dem Gedächtnis streichen. Sie wurde hier nur deshalb erläutert, weil Sie existiert und man beispielsweise als C++Programmierer nach einem Destruktor in Java sucht.
Wie sieht nun die Lösung für die Implementierung eines Destruktors aus? Implementieren Sie eine eigene Cleanup-Methode und rufen Sie sie manuell auf, wenn Sie das Objekt nicht mehr benötigen. Als Name der Methode wird häufig dispose() (beseitigen) gewählt. Sie haben den Vorteil, dass Sie die Methode zu dem Zeitpunkt aufrufen können, an dem Sie sie benötigen, und dass die Übergabe beliebiger Parameter möglich ist. Nach dem Erstellen des Destruktor-Objekts wird für Aufräumarbeiten explizit die hierfür bereitgestellte Methode dispose() aufgerufen. Die Programmausführung ist für den Start des GC zu kurz, deshalb wird die Methode finalize() nicht aufgerufen. [IconInfo]
Entfernen Sie die Kommentarzeichen vor den Anweisungen in der Methode main(), wird finalize() aufgerufen, weil nun der GC seine Arbeit verrichten kann. Erzeugen Anwendungen aber Hunterttausende von Objekten, werden diese nicht unbedingt beim einmaligen Aufruf des GC wieder beseitigt. Dort reichen diese beiden Anweisungen nicht mehr aus. Gegebenenfalls müssen sie innerhalb einer Schleife mehrmals ausgeführt werden.
Java 5 Programmierhandbuch
117
Zugriffsattribute und Sichtbarkeit Listing 4.8: \Beispiele\de\j2sebuch\kap04\Destruktor.java public class Destruktor { protected void finalize() { System.out.println("Finalize"); } public void dispose() { System.out.println("Aufräumen"); } public static void main(String[] args) { Destruktor d = new Destruktor(); d.dispose(); // System.gc(); // System.runFinalization(); } }
4.6
Zugriffsattribute und Sichtbarkeit
Die Zugriffsattribute public, protected, private, final und »package-sichtbar« steuern, welche Bestandteile einer Klasse für deren Instanzen und für abgeleitete Klassen sichtbar sind. Auch Klassen und Interfaces können mit Attributen versehen werden. In der Syntaxbeschreibung der entsprechenden Elemente wird angegeben, welche Zugriffsattribute in welcher Situation verfügbar sind. Attribut
Erläuterung
public
Die mit public gekennzeichneten Elemente einer Klasse stellen deren öffentliche Schnittstelle dar. Nur diese Elemente können wirklich überall benutzt werden. In der Regel sollten nur Methoden öffentlich sein. Der Zugriff auf die privaten Daten sollte über Methoden geregelt werden. Dies erlaubt beispielsweise eine Prüfung der an die Variablen zugewiesenen Werte.
protected
Auf diese Elemente können nur abgeleitete Klassen (die Verwendung ist nur bei inneren Klassen möglich) sowie Instanzen innerhalb des Package zugreifen.
private
Diese Elemente sind außerhalb einer Klassendefinition für niemanden sichtbar. Die Daten einer Klasse sollten in der Regel mit diesem Attribut versehen werden. Nur öffentliche Methoden erlauben den Zugriff auf diese Daten. Auf diese Weise kapseln Sie die Daten in der Klasse über eine definierte Menge von Methoden. Die Daten können auf keinem anderen Weg außerhalb der Klasse geändert werden.
118
4 – Klassen, Interfaces und Objekte Attribut
Erläuterung
final
Diese Elemente können nicht modifiziert werden. Dies heißt, aus Variablen werden Konstanten, Methoden lassen sich nicht überschreiben und von Klassen kann nicht abgeleitet werden.
packagesichtbar
Wird kein Zugriffsattribut angegeben, ist die Sichtbarkeit des Elements auf das Package beschränkt. Dies stellt grundsätzlich keinen großen Schutz dar, weil Sie als Programmierer jederzeit ein gleichnamiges Package erzeugen und auf diese Elemente zugreifen können. Klassen und Interfaces eines Package mit demselben Namen müssen sich nicht notwendigerweise im gleichen Verzeichnis befinden.
[IconInfo]
4.7
Innerhalb einer Klasse sind alle Elemente öffentlich, d.h., eine Methode kann problemlos auf die privaten Variablen der Klasse zugreifen. Die Zugriffsattribute public, protected sowie private können für die Elemente einer Klasse verwendet werden, insbesondere für Methoden und Variablen.
Statische Klassenelemente
Bisher waren bis auf die Methode main() alle Variablen und Methoden an ein Objekt gebunden. Solche Variablen werden auch als Instanzvariablen bezeichnet, da sie an die Instanz einer Klasse, also ein bestimmtes Objekt, gebunden sind. Statische Variablen und Methoden sind nicht an ein Objekt, sondern direkt an die Klasse gebunden, das heißt, sie existieren genau einmal mit der Klassendefinition. Statische Variablen werden auch als Klassenvariablen bezeichnet. Statische Methoden können nur auf statische Variablen und Konstanten zugreifen, weil bei ihrer Verwendung kein Objekt vom Typ der Klasse existieren muss. Umgekehrt können nichtstatische Methoden jederzeit auf statische Elemente zugreifen, denn diese existieren immer. Die Änderung des Werts einer statischen Variablen ist für alle Objekte sichtbar. Innerhalb statischer Methoden ist keine Verwendung von this möglich. Es existiert kein Objekt, auf das this verweisen könnte. Statische Methoden und Konstanten werden z.B. in der Klasse java.lang.Math verwendet. Die mathematischen Funktionen benötigen zur Ausführung kein Objekt, da außer dem Funktionsaufruf keine weiteren Operationen auf dem Objekt ausgeführt werden. Das Erstellen des Objekts kostet außerdem nur Zeit und Speicherplatz. Die Klasse Math besteht nur aus statischen Elementen, deshalb wurde ihr ein privater Standardkonstruktor hinzugefügt, so dass man keine Objekte von dieser Klasse erstellen kann. Weitere Anwendungsmöglichkeiten statischer Klassenelemente sind: 쐌 die Methode main(), die unabhängig von einem Objekt einer Klasse zum Starten einer Anwendung aufgerufen werden muss, 쐌 die Bereitstellung von allgemeinen Methoden, die nicht an ein spezielles Objekt gebunden sind, wie z.B. mathematische Funktionen,
Java 5 Programmierhandbuch
119
Statische Klassenelemente
쐌 das Zählen von erzeugten Instanzen einer Klasse, 쐌 das Definieren von Konstanten innerhalb einer Klasse; ab dem JDK 5.0 sollten Sie aber die neuen Aufzählungstypen verwenden. Beispiel Die Klasse Mathe besitzt eine statische Variable i, eine Konstante KONSTANTE1 und eine Methode addZahlen(). Bei der Verwendung der Methode und Konstanten wird dann direkt über den Klassennamen darauf zugegriffen. public class Mathe { private static int i = 10; public static final int KONSTANTE1 = 100; public static int addZahlen(int zahl1, int zahl2) { return zahl1 + zahl2; } private Mathe() {} } ... { int ergebnis = Mathe.addZahlen(10, 11); int ergebnis2 = 30 * Mathe.KONSTANTE; }
Syntax Geben Sie optional ein Zugriffsattribut und danach das Attribut static an. Definieren Sie anschließend die Variable, Konstante oder Methode wie bisher. [Attribut] static ; [Attribut] static final = ; [Attribut] static { }
Statische Initialisierer Jede Klasse kann einen oder mehrere statische Initialisierungsblöcke besitzen, in denen Sie z.B. statische Variablen initialisieren können. Laden Sie eine Klasse mit statischen Elementen das erste Mal, werden diese Elemente initialisiert, bevor der statische Initialisierungsblock ausgeführt wird. Statische Initialisierer werden über das Attribut static eingeleitet, dem ein geschweiftes Klammerpaar folgt. Sie sind so gesehen statische Methoden ohne einen Namen. Besitzt eine Klasse mehrere solche Blöcke, werden diese in der definierten Reihenfolge ausgeführt.
120
4 – Klassen, Interfaces und Objekte
Beispiel public class Mathe { private static int i; static { i = 10; } }
[IconInfo]
Das Beispiel definiert die statische Variable faktor, die in einem statischen Initialisierungsblock den Wert 10 erhält. Weiterhin wird eine statische Konstante MWST mit dem Wert 0.16 erzeugt. Die Methode ausgabeMwSTSatz() wurde statisch definiert, deshalb ist sie nicht an ein Objekt der Klasse gebunden und kann direkt in der Methode main() aufgerufen werden.
Listing 4.9: \Beispiele\de\j2sebuch\kap04\Statisch.java public class Statisch { private static int faktor; public static final double MWST = 0.16; static { faktor = 10; } public static void ausgabeMwSTSatz() { System.out.println("MwSt-Satz: " + MWST); } public static void main(String[] args) { ausgabeMwSTSatz(); } }
4.8
Aufzählungstypen mit Enum
So genannte Aufzählungskonstanten wurden bisher über int-Konstanten implementiert. Hierfür wurden einzelne Konstanten mit festen Werten belegt, z.B. public final int OPTION1 = 1; public final int OPTION2 = 2;
Java 5 Programmierhandbuch
121
Aufzählungstypen mit Enum
Der Nachteil dieser Konstanten ist, dass sie nicht typsicher sind, d.h., statt der Konstanten kann jede beliebige int-Zahl verwendet werden bzw. die Konstanten können überall dort benutzt werden, wo ein int-Wert benötigt wird. Weiterhin ist der Name der Konstanten nicht automatisch verfügbar. Integer-Konstanten werden zudem in den ByteCode kompiliert. Wenn eine Klasse die Konstante OPTION1 verwendet, wird deren Wert 1 im ByteCode gespeichert. Ändern Sie den Wert der Konstanten, muss die Klasse neu übersetzt werden. Aufzählungstypen, die Sie über das Schlüsselwort enum deklarieren, stellen eine eigene Klasse dar und jede Aufzählungskonstante ist ein echtes Objekt dieser Klasse. Beispiel Die einfachste Form einer Aufzählung verwendet das Schlüsselwort enum. Diesem folgen der Name der Aufzählung sowie in geschweiften Klammern die Aufzählungskonstanten. public enum Noten { EINS, ZWEI };
Die folgenden Informationen sind einerseits für bereits fortgeschrittenere Java-Programmierer gedacht. Andererseits runden sie das Thema um Aufzählungstypen ab und zeigen auch einmal einen Decompiler in Aktion (der auch in anderen Situationen durchaus nützlich sein kann). [IconInfo]
Der über den Compiler erzeugte ByteCode kann über einen Decompiler (z.B. den DJ Java Decompiler, zu beziehen unter http://members.fortunecity.com/neshkov/dj.html) wieder in SourceCode überführt werden. Dieser SourceCode sieht dann folgendermaßen aus (gekürzt). Die Klasse Noten wird von der Klasse Enum erweitert. Es wird eine Methode values() hinzugefügt, die ein Array von Noten-Objekten zurückgibt. Über die Methode valueOf() kann zu einer Zeichenkette, z.B. ZWEI, das zugehörige Objekt der Aufzählung bestimmt werden. Noten n2 = valueOf("ZWEI");
Der private Konstruktor einer Aufzählungsklasse übernimmt eine Zeichenkette (die Textrepräsentation der Konstanten) sowie einen automatisch generierten Index für diese Konstante. Für jede Konstante einer Aufzählung wird nun ein Objekt in der Klasse angelegt. Es werden zuerst passende Variablen definiert, die in einem statischen Konstruktor mit einem Namen und einem Index erzeugt werden. Als Resultat erhalten Sie wieder eine »normale« Klasse, deren Inhalt aber größtenteils automatisch vom Compiler erzeugt wurde.
122
4 – Klassen, Interfaces und Objekte Listing 4.10: Vom Compiler generierter Code für die Aufzählung Noten public final class Noten extends Enum { public static final Noten[] values() { return (Noten[])$VALUES.clone(); } public static Noten valueOf(String s) { ... } private Noten(String s, int i) { super(s, i); } public static final Noten EINS; public static final Noten ZWEI; private static final Noten $VALUES[]; static { EINS = new Noten("EINS", 0); ZWEI = new Noten("ZWEI", 1); $VALUES = (new Noten[] { EINS, ZWEI }); } }
Damit haben Aufzählungstypen die folgenden Eigenschaften: 쐌 Die Aufzählungskonstanten sind Objekte der Aufzählungsklasse. 쐌 Beim Hinzufügen neuer Konstanten ist keine Neukompilierung der Clients notwendig, da nicht mit int-Konstanten, sondern mit Objekten gearbeitet wird. 쐌 Jede Konstante besitzt automatisch eine Stringrepräsentation, die mit ihrem Namen identisch ist. 쐌 Über die Methode values() wird ein Array aller Konstanten geliefert. 쐌 Für jede Konstante wird automatisch eine Ordinalzahl vergeben, die über die von der Klasse Enum vererbte Methode ordinal() ermittelt werden kann. 쐌 Es können keine Objekte für eine enum-Klasse erzeugt werden, weil diese einen privaten Konstruktor besitzt. Syntax Einer Aufzählung kann optional ein Attribut wie public vorangestellt werden. In diesem Fall gelten die gleichen Regeln wie bei Klassen, d.h., der Dateiname muss mit dem Namen des Bezeichners übereinstimmen. Der Bezeichner der Aufzählung entspricht gleichzeitig dem Klassennamen. Eine Aufzählung kann von einem Interface abgeleitet werden. Als Erstes müssen Sie in der Aufzählungsklasse die Aufzählungskonstanten angeben. Wahlweise können Sie in Klammern Parameter anfügen, die an einen entsprechenden Konstruktor
Java 5 Programmierhandbuch
123
Aufzählungstypen mit Enum
übergeben werden. Aufzählungen können, wie normale Klassen, Konstruktoren, Methoden, Variablen und Konstanten beinhalten. [Attribut] enum [implements Interface] { KONSTANTE1, KONSTANTE2, ...; // oder mit Parametern KONSTANTE1(...), KONSTANTE2(...), ...; // Methoden // Konstruktoren // Variablen und Konstanten }
Aufzählungstypen können in switch-Anweisungen verwendet werden, sich innerhalb einer anderen Klasse befinden, Methoden und Konstruktoren sowie konstanten-abhängige Methoden enthalten. Im Folgenden wird je ein Beispiel dazu vorgestellt. Die Klasse Noten enthält fünf Konstanten für die Zahlen 1 bis 5. Die Namen werden nochmals in Englisch innerhalb der Klasse Wrapper in einer weiteren Aufzählung definiert. Dadurch ist später ein anderer Zugriff auf die Konstanten notwendig. [IconInfo]
Über die neue for-Schleife werden die Konstanten der beiden Aufzählungen durchlaufen. Ein Array aller Konstanten wird über die Methode values() der Aufzählungsklasse geliefert. Mit der Methode valueOf() kann nach Übergabe eines Strings das zugehörige Konstantenobjekt ermittelt werden. Dieses wird in einer switch-Anweisung ausgewertet, die zur Unterstützung von Konstanten erweitert wurde. Listing 4.11: \Beispiele\de\j2sebuch\kap04\Aufzaehlungen1.java public class Aufzaehlungen1 { public Aufzaehlungen1() { for(Noten n: Noten.values()) System.out.println(n); for(Wrapper.Noten n2: Wrapper.Noten.values()) System.out.println(n2); Noten nSwitch = Noten.valueOf("EINS"); switch(nSwitch) { case EINS: System.out.println(">>EINS"); break; case ZWEI: System.out.println(">>ZWEI"); break; } }
124
4 – Klassen, Interfaces und Objekte Listing 4.11: \Beispiele\de\j2sebuch\kap04\Aufzaehlungen1.java (Forts.) public static void main(String[] args) { new Aufzaehlungen1(); } } enum Noten { EINS, ZWEI, DREI, VIER, FUENF }; class Wrapper { enum Noten { ONE, TWO, THREE, FOUR, FIVE }; }
[IconInfo]
In der Aufzählungsklasse Fahrzeuge werden drei Konstanten deklariert. Die Klasse besitzt zusätzlich eine Variable für die PS-Zahl, die an den Konstruktor als Parameter übergeben wird. Beim Durchlaufen der Konstanten in der for-Schleife wird die Methode getPS() aufgerufen, um die konstantenspezifische PS-Zahl auszugeben.
Listing 4.12: \Beispiele\de\j2sebuch\kap04\Aufzaehlungen2.java public class Aufzaehlungen2 { public Aufzaehlungen2() { for(Fahrzeuge fz: Fahrzeuge.values()) System.out.println(fz + ": " + fz.getPS()); } public static void main(String[] args) { new Aufzaehlungen2(); } } enum Fahrzeuge { BMW(110), AUDI(100), OPEN(50); private int ps; Fahrzeuge(int ps) { this.ps = ps; } public int getPS() { return ps; } }
Java 5 Programmierhandbuch
125
Aufzählungstypen mit Enum
[IconInfo]
Dieses Beispiel zeigt, wie Sie konstantenabhängig Methoden unterschiedlich implementieren können. In der Klasse MinMax wird eine Methode rechne() implementiert, die mit der switch-Anweisung den Typ des aktuellen Konstantenobjekts prüft. Vom Ergebnis der Prüfung abhängig, wird eine Operation ausgeführt. Beachten Sie, dass Sie in der switchAnweisung auch wirklich jeden Konstantenwert abfragen. Die Klasse MinMax2 umgeht dieses Problem, indem sie in der Klasse eine abstrakte Methode rechne() definiert. Die Implementierung der Methode erfolgt durch die jeweiligen Konstanten, die ja im statischen Initialisierer erzeugt werden. Sie implementieren dort auf verschiedene Arten die Methode rechne(). Vergessen Sie dies, meldet der Compiler bereits einen Fehler. Für beide Klassen werden die enthaltenen Konstanten durchlaufen und die Methode rechne() wird konstantenspezifisch ausgeführt.
Listing 4.13: \Beispiele\de\j2sebuch\kap04\Aufzaehlungen3.java public class Aufzaehlungen3 { public Aufzaehlungen3() { for(MinMax mm: MinMax.values()) System.out.printf("%s von 10 und 11 ist %d\n", mm, mm.rechne(10, 11)); for(MinMax2 mm2: MinMax2.values()) System.out.printf("%s von 10 und 11 ist %d\n", mm2, mm2.rechne(10, 11)); } public static void main(String[] args) { new Aufzaehlungen3(); } } enum MinMax { MINIMUM, MAXIMUM; int rechne(int zahl1, int zahl2) { switch(this) { case MINIMUM: return Math.min(zahl1, zahl2); case MAXIMUM: return Math.max(zahl1, zahl2); default: return 0; } } }
126
4 – Klassen, Interfaces und Objekte Listing 4.13: \Beispiele\de\j2sebuch\kap04\Aufzaehlungen3.java (Forts.) enum MinMax2 { MINIMUM{ int rechne(int zahl1, int zahl2) { return Math.min(zahl1, zahl2); } }, MAXIMUM{ int rechne(int zahl1, int zahl2) { return Math.max(zahl1, zahl2); } }; abstract int rechne(int zahl1, int zahl2); }
4.9 4.9.1
Vererbung Die Klasse Object als Basisklasse aller Klassen
Das Vererben von Elementen einer Klasse an eine andere ist eines der Hauptmerkmale objektorientierter Softwareentwicklung. Java unterstützt die einfache Vererbung. Sie können also eine Klasse von genau einer anderen Klassen ableiten. Eine Mehrfachvererbung, d.h. das Ableiten einer Klasse von mehreren anderen Klassen, unterstützt Java nicht. Die Vererbung von Klassen kann beliebig tief geschachtelt sein. Innerhalb der Klassenhierarchie von Java gibt es genau eine Basisklasse, von der direkt oder indirekt alle anderen Klassen abgeleitet sind. Dies ist die Klasse Object. Alle anderen Klassen erben deren öffentliche Methoden. Die Klasse Object ist die einzige Klasse, die selbst keine übergeordnete Klasse besitzt. Einige Methoden der Klasse Object Da letztendlich alle Klassen die Methoden der Klasse Object erben, sollen die wichtigsten Methoden kurz vorgestellt werden. Um eine Kopie (einen Klon) eines Objekts zu erstellen, kann die Methode clone() verwendet werden. Bei der Implementierung ist zu beachten, dass eine Klasse, die diese Methode überschreibt, das Interface Cloneable implementieren muss. Sonst kommt es beim Aufruf von clone() zu einer CloneNotSupportedException. Die Klasse Object stellt in der Methode bereits sicher, dass alle Datenelemente eines Objekts beim Aufruf von clone() kopiert werden. Dies heißt aber auch, dass die Referenzen auf Objekte kopiert werden. Sollen statt der Referenzen neue Objekte angelegt werden, müssen Sie clone() überschreiben. Object clone()
Java 5 Programmierhandbuch
127
Vererbung
[IconInfo]
In der J2SE 5.0 werden so genannte kovariante Rückgabetypen unterstützt. Sie müssen beim Überschreiben von clone() nicht mehr zwingend Object-Typen zurückgeben, sondern Sie können sofort ein Objekt des betreffenden Typs zurückliefern. Weitere Informationen finden Sie im Kapitel über Generics.
Zum Vergleich zweier Objekte dient die Methode equals(). Werden nur Objektreferenzen verglichen, müssen Sie die Methode nicht überschreiben. Anders sieht es aus, wenn Sie beispielsweise zwei Objekte als gleich ansehen möchten, wenn sie nur in bestimmten Werten übereinstimmen. Wenn die Methode equals() die Gleichheit zweier Objekte feststellt, muss für diese beiden Objekte auch derselbe Wert durch die Methode hashcode() berechnet werden. Aus diesem Grund müssen immer beide Methoden überschrieben werden. boolean equals(Object obj) int hashCode()
Die folgende Methode liefert ein Class-Objekt, welches die Klasse des Objekts identifiziert. Diese Methode kann nicht überschrieben werden, weil sie als final deklariert ist. Class getClass()
Eine String-Repräsentation des Objekts liefert die Methode toString(). Der Inhalt der Repräsentation wird vom betreffenden Objekttyp festgelegt. String toString()
[IconInfo]
Das folgende Beispiel implementiert für die Klasse ObjectMethoden die Methoden clone(), toString(), equals() und hashCode(). Zum Kopieren wird die Methode clone() der Basisklasse aufgerufen. Dies sollte in einer Klassenhierarchie bis zum Aufruf der Methode clone() der Klasse Object führen, die dann ein neues Objekt anlegt und die Datenelemente des Objekts kopiert. Alle anderen Klassen innerhalb der Vererbungshierarchie dienen dann nur dazu, über deren überschriebene clone()-Methode statt Kopien der Objektreferenzen die betreffenden Objekte wirklich neu zu erzeugen und zu initialisieren. Im Beispiel erfolgt dies z.B. durch die Methode clone() des Array-Objekts. Die Methode toString() gibt einfach den Objektnamen und die Werte der Objektvariablen aus. Innerhalb der Methode equals() wird zuerst geprüft, ob es sich bei dem übergebenen Objekt um das gleiche Objekt handelt. Der Vergleich mit null oder einem anderen Objekttyp liefert den Rückgabewert false. Mit der Methode equals() der Klasse Arrays wird die Gleichheit der
128
4 – Klassen, Interfaces und Objekte
[IconInfo]
Arrayinhalte geprüft. Sind diese sowie der Wert der Variablen i gleich, sollen auch die Objekte als gleich angesehen werden. Die Methode hashCode() wird so implementiert, dass bei gleichen Werten der Variablen i und der Elemente des Arrays iArray auch der gleiche Hashcode berechnet wird. Beim Test der Methode clone() werden die Werte im originalen und im geklonten Array verändert, um zu prüfen, ob tatsächlich ein neues Array als Kopie verwendet wird.
Listing 4.14: \Beispiele\de\j2sebuch\kap04\ObjectMethoden.java import java.util.*; public class ObjectMethoden implements Cloneable { int i = 10; int[] iArray = {1, 2, 3}; public ObjectMethoden() { ObjectMethoden om = (ObjectMethoden)this.clone(); iArray[1] = 4; om.iArray[2] = 5; for(int ia: iArray) System.out.println(ia); System.out.println("-------------"); for(int ia: om.iArray) System.out.println(ia); System.out.println(toString()); System.out.println("-------------"); ObjectMethoden om2 = (ObjectMethoden)this.clone(); if(equals(om2)) System.out.println("Sie sind gleich!"); } protected ObjectMethoden clone() { try { ObjectMethoden om = (ObjectMethoden)super.clone(); om.iArray = (int[])iArray.clone(); return om; } catch(CloneNotSupportedException cnsEx) { return null; } }
Java 5 Programmierhandbuch
129
Vererbung Listing 4.14: \Beispiele\de\j2sebuch\kap04\ObjectMethoden.java (Forts.) public String toString() { return "ObjectMethoden: " + i + "," + Arrays.toString(iArray); } public boolean equals(Object obj) { if(this == obj) return true; if((obj == null) || (this.getClass() != obj.getClass())) return false; return ((Arrays.equals(iArray,((ObjectMethoden)obj).iArray)) & (((ObjectMethoden)obj).i == this.i)); } public int hashCode() { int sum = 0; for(int iA: iArray) sum += iA; return i + sum; } public static void main(String[] args) { new ObjectMethoden(); } }
4.9.2
Klassen ableiten
Bei der Vererbung erbt eine abgeleitete Klasse alle public-, protected- und packagesichtbaren Elemente der übergeordneten Klasse. Es werden immer nur die Elemente der unmittelbar übergeordneten Klasse geerbt. Sie können die geerbten Elemente dann wie die Elemente der abgeleiteten Klasse verwenden. Nicht immer sollen alle Methoden oder Variablen so verwendet werden, wie sie die übergeordnete Klasse vererbt. Fügen Sie in diesem Fall eine Methode oder Variable mit dem gleichen Namen wie in der übergeordneten Klasse ein. Das Element der übergeordneten Klasse wird dadurch verdeckt. Das Zugriffsattribut dieser Methoden darf in der Sichtbarkeit jedoch nicht vermindert werden (eine Methode mit dem Attribut public darf nicht private deklariert werden), bei Variablen ist dies jedoch erlaubt. Syntax 쐌 Hinter dem Schlüsselwort class geben Sie den Klassennamen der abgeleiteten Klasse an. 쐌 Dann folgt das Schlüsselwort extends und der Name der Basisklasse.
130
4 – Klassen, Interfaces und Objekte class Unterklasse extends Basisklasse { }
[IconInfo]
Die Klasse Vererbung wird von der Klasse Basis abgeleitet. Sie erbt die Methoden publicBasis(), protectedBasis() und basis() die jeweils verschiedene Zugriffsattribute besitzen. Weiterhin definiert sie eine private Variable bBasis, welche die öffentliche Variable bBasis der übergeordneten Klasse verdeckt. Bei Methoden ist es nicht möglich, eine öffentliche Methode durch eine private zu überschreiben.
Listing 4.15: \Beispiele\de\j2sebuch\kap04\Vererbung.java public class Vererbung extends Basis { private boolean bBasis; public Vererbung() { publicBasis(); protectedBasis(); basis(); // privateBasis(); // Aufruf nicht möglich, da private } public static void main(String[] args) { new Vererbung(); } } class Basis { public boolean bBasis; public void publicBasis() { System.out.println("public Basis"); } protected void protectedBasis() { System.out.println("protected Basis"); } void basis() { System.out.println("package Basis"); } private void privateBasis() { System.out.println("private Basis"); } }
Java 5 Programmierhandbuch
131
Vererbung
4.9.3
Konstruktoraufrufe
Ist eine Klasse von einer anderen abgeleitet, garantiert der Compiler, dass von einem Konstruktor der abgeleiteten Klasse aus automatisch der passende Konstruktor der Basisklasse aufgerufen wird. Beinhaltet die abgeleitete Klasse keinen Konstruktor, wird automatisch ein parameterloser Standardkonstruktor erzeugt, der den Standardkonstruktor der Basisklasse aufruft. Hier verbirgt sich allerdings eine mögliche Fehlerquelle. Besitzt die Basisklasse keinen Standardkonstruktor, dafür aber einen parametrisierten Konstruktor, führt dies zu einem Compilerfehler. Konstruktoren werden nicht vererbt. Sie müssen alle benötigte Konstruktoren in jeder abgeleiteten Klasse neu definieren. Soll ein Konstruktor den passenden Konstruktor der Basisklasse (mit der entsprechenden Parameterliste) aufrufen, ist als erste Anweisung der Aufruf von super() mit Angabe der Parameter einzufügen. Erfolgt der Aufruf der Methode super() nicht als erste Anweisung, meldet der Compiler einen Fehler. public AbgelKonstruktor(int index, String name) { super(index, name); // Konstruktoraufruf der Basisklasse ... // weitere Anweisungen }
Die Aufrufreihenfolge bei der Vererbung beginnt jeweils beim Konstruktor der Basisklasse, so dass zuerst der Konstruktor der Klasse Object aufgerufen wird. Noch vor dem Konstruktor wird der optionale Initialisierungsteil der entsprechenden Klasse abgearbeitet. Als letzter Konstruktor wird der aktuelle Konstruktor abgearbeitet. Über die Konstruktorverkettung innerhalb einer Klasse können Sie über this() einen anderen Konstruktor der Klasse aufrufen. Der letzte Konstruktor dieser Kette ruft dann den Konstruktor der Basisklasse auf. Für die Methode finalize() wird kein automatischer Aufruf der Methode in der Basisklasse garantiert. Sie können sie aber jederzeit über die folgende Anweisung innerhalb der Methode finalize() der aktuellen Klasse aufrufen. super.finalize();
[IconInfo]
132
Die Klasse Basisklasse enthält einen Initialisierungsblock sowie zwei Konstruktoren. Die davon abgeleitete Klasse enthält einen Initialisierungsblock und drei Konstruktoren. In der main()-Methode werden diese drei Konstruktoren nacheinander aufgerufen. Anhand der Ausgaben auf der Konsole können Sie die bereits angegebene Aufrufreihenfolge (Initialisierungsblock und Konstruktor der Basisklasse, Konstruktor der abgeleiteten Klasse) prüfen. Erfolgt in einem Konstruktor kein expliziter Aufruf von super(), wird automatisch der Standardkonstruktor der Basisklasse verwendet.
4 – Klassen, Interfaces und Objekte Listing 4.16: \Beispiele\de\j2sebuch\kap04\VererbKonstruktor.java public class VererbKonstruktor extends Basisklasse { { System.out.println("Statische Initialisierer abgel. Klasse"); } public VererbKonstruktor() { System.out.println("VererbKonstruktor ()"); } public VererbKonstruktor(int index) { super(index); System.out.println("VererbKonstruktor (int)"); } public VererbKonstruktor(int index, String name) { System.out.println("VererbKonstruktor (int, String)"); } public static void main(String[] args) { new VererbKonstruktor(); System.out.println("----------------------"); new VererbKonstruktor(1); System.out.println("----------------------"); new VererbKonstruktor(1, ""); } } class Basisklasse { { System.out.println("Statische Initialisierer Basisklasse"); } public Basisklasse() { System.out.println("Basisklasse ()"); } public Basisklasse(int index) { System.out.println("Basisklasse (int)"); } }
Java 5 Programmierhandbuch
133
Vererbung
Ausgabe: Statische Initialisierer Basisklasse Basisklasse () Statische Initialisierer abgel. Klasse VererbKonstruktor () ---------------------Statische Initialisierer Basisklasse Basisklasse (int) Statische Initialisierer abgel. Klasse VererbKonstruktor (int) ---------------------Statische Initialisierer Basisklasse Basisklasse () Statische Initialisierer abgel. Klasse VererbKonstruktor (int, String)
4.9.4
Vererbungsketten und Zuweisungskompatibilität
Innerhalb einer Klassenhierarchie sind immer Objekte der abgeleiteten Klassen zu Objekten der Basisklasse zuweisungskompatibel. Dies ist möglich, da diese Objekte ja mindestens die Funktionalität und Schnittstelle der Basisklasse (und deren Basisklasse usw.) unterstützen. Die Zuweisung eines Objekts einer abgeleiteten Klasse an ein Objekt der Basisklasse wird auch als Upcasting bezeichnet. Das untergeordnete Objekt wird hinauf zur Basisklasse gecastet. Diese Casts erfolgen automatisch und sind immer sicher. Der umgekehrte Cast (Downcast) ist auch möglich, aber nur erfolgreich, wenn das Objekt vom gleichen Objekttyp ist oder eines davon abgeleiteten Typs. Hier kommen potenzielle Fehlerquellen zum Tragen, da der Compiler keine Typprüfung mehr vornehmen kann. Sie könnten ansonsten Methoden aufrufen, die es zwar für den Casting-Typ gibt, aber nicht für das betreffende Objekt. Beispiel Die Klasse Ebene2 seine eine von Ebene1 abgeleitete Klasse. class Ebene1 {} class Ebene2 extends Ebene1 {}
Da ein Objekt vom Typ Ebene2 mindestens die Funktionalität (und die öffentliche Schnittstelle) der Klasse Ebene1 umfasst, ist die erste Zuweisung korrekt. Die zweite Anweisung erzeugt bei der Kompilierung noch keinen Fehler, da Sie über den Cast (die Typumwandlung) die Typprüfung des Compilers umgehen. Zur Laufzeit der Anwendung kommt es aber zu einem Laufzeitfehler, da die Objekttypen einander nicht zugewiesen werden können.
134
4 – Klassen, Interfaces und Objekte Ebene1 e1 = new Ebene2(); Ebene2 e2 = (Ebene2)new Ebene1(); // Laufzeitfehler
Möchten Sie vor der Zuweisung von Objekten deren Zuweisungsverträglichkeit prüfen, können Sie den Operator instanceof verwenden. Der erste Test liefert true, da e1 auf ein Objekt vom Typ Ebene2 verweist. Hätten Sie e1 ein Objekt vom Typ Ebene1 zugewiesen, wäre der Test negativ ausgefallen. Dasselbe trifft für den zweiten Test zu. Es wird also über den Operator instanceof und die Methode getClass() bzw. .class der konkrete Typ eines Objekts überprüft bzw. zurückgegeben. if(e1 instanceof Ebene2) System.out.println("e1 ist zu Ebene2-Objekten zuweisbar."); if(e1.getClass() == Ebene2.class) System.out.println("Ok");
4.9.5
Finale Klassen
Ähnlich wie bei der Verwendung von final für die Definition von Konstanten, können Sie auch Klassen gegenüber »Änderungen« schützen. Der Schutz besteht hier darin, dass keine anderen Klassen von dieser Klasse abgeleitet und damit auch keine Methoden überschrieben werden können. Die Funktionsweise der Klasse kann also nicht durch das Ableiten geändert werden. Außer einer Schutzfunktion haben finale Klassen eine weitere praktische Bedeutung. Der Compiler muss keine dynamischen Methodenaufrufe ermöglichen, die erst zur Laufzeit einer Anwendung für ein bestimmtes Objekt ausgeführt werden. Die Ausführung von Methoden finaler Klassen erfolgt deshalb schneller. Auch hier ist die finale Klasse Math wieder ein Beispiel für die Bereitstellung von effizient ausführbaren Methoden. Die Verwendung von statischen Methoden erfordert nicht einmal ein Objekt der Klasse Math. Syntax Geben Sie zusätzlich das Attribut final bei der Klassendeklaration an. Versuchen Sie, anschließend eine andere Klasse von dieser Klasse abzuleiten, erhalten Sie einen Compilerfehler. public final class { }
Java 5 Programmierhandbuch
135
Interfaces
4.10 Interfaces Angenommen, Sie gehen in ein Autohaus, um ein Auto zu kaufen. Sie sagen dem Verkäufer, der Wagen müsse fünf Türen und mindestens 90 PS haben. Außerdem soll er ein gutes Autoradio mit CD-Wechsler besitzen. Der Verkäufer stellt Ihnen daraufhin fünf Wagen vor, die diese Kriterien erfüllen. Interfaces (Schnittstellen) erfüllen in Java einen ähnlichen Zweck. Über ein Interface definieren Sie Methoden, die eine Klasse, die dieses Interface implementiert, enthalten muss. Später können Sie dann prüfen, ob eine Klasse ein bestimmtes Interface implementiert. Sie erhalten dadurch die Sicherheit, dass Sie die Methoden des Interfaces über ein Objekt der Klasse aufrufen können. Alle Klassen, die das gleiche Interface implementieren, haben demnach eine gleiche Schnittstelle. Über eine Variable vom Typ des Interfaces können dann die Schnittstellenmethoden verschiedener Klassen aufgerufen werden. Ein weiterer Vorteil von Interfaces ist es, dass eine Klasse unter verschiedenen Typen auftreten kann, indem sie mehrere Schnittstellen unterstützt. Noch ein Beispiel Sie haben eine Anwendung entwickelt, welche Daten in einer MySQL-Datenbank verwaltet. Der Zugriff auf die Datenbank erfolgt über die Methoden tabelleOeffnen() und datenSpeichern() des Interfaces DatenbankZugriff. interface DatenbankZugriff { boolean tabelleOeffnen(String name); boolean datenSpeichern(String[] werte); }
Weiterhin wurde eine Klasse MySQLDatenbankZugriff entwickelt, welche das Interface DatenbankZugriff implementiert und die Methoden für den Zugriff auf eine MySQLDatenbank mit Leben füllt. class MySQLDatenbankZugriff implements DatenbankZugriff { boolean tabelleOeffnen(String name) { // Anweisungen zum Öffnen einer Tabelle } boolean datenSpeichern(String[] werte) { // Anweisungen zum Speichern der übergebenen Werte } }
136
4 – Klassen, Interfaces und Objekte
Sie verwenden die folgenden Anweisungen, um ein Objekt vom Typ des Interfaces DatenbankZugriff zum Zugriff auf die MySQL-Datenbank zu erzeugen und darüber in die Tabelle KUNDEN einen Datensatz einzufügen. DatenbankZugriff dz = new MySQLDatenbankZugriff(); dz.tabelleOeffnen("Kunden"); dz.datenSpeichern(new String[]{"Meier", "Leipzig"});
Wie zu erwarten, kommt von Ihrem Kunden nach kurzer Zeit die Anforderung, die Daten nicht in MySQL, sondern in einem anderen Datenbanksystem zu speichern, welches der Kunde bereits im Einsatz hat. Dies ist dank der Verwendung von Interfaces nicht sehr aufwändig, da lediglich eine neue Klasse zum Zugriff auf das andere Datenbanksystem entwickelt werden muss. Die Methoden zum Zugriff auf die Datenbank bleiben durch die Verwendung von Interfaces gleich. class XYZDatenbankZugriff implements DatenbankZugriff { boolean tabelleOeffnen(String name) {} boolean datenSpeichern(String[] werte) {} } ... // nur der im Folgenden zugewiesene Objekttyp ändert sich DatenbankZugriff dz = new XYZDatenbankZugriff(); dz.tabelleOeffnen("Kunden"); dz.datenSpeichern(new String[]{"Meier", "Leipzig"});
Interfaces dürfen Klassendefinitionen, Interfaces, Konstanten und abstrakte Methoden beinhalten. Abstrakte Methoden enthalten keinen Rumpf, sondern nur die Methodendeklaration. Weil Interfaces nur abstrakte Methoden enthalten dürfen, können auch keine Objekte des Interfacetyps erzeugt werden. Außerdem sind Interfaces dadurch immer implizit abstract, ohne dass dies angegeben werden muss. Sie können nun eine Variable vom Typ eines Interface verwenden und ihr ein Objekt einer Klasse zuweisen, die das Interface implementiert. Über diese Variable können Sie dann die Methoden der Klasse verwenden, die durch das Interface vorgegeben werden, wie das im gezeigten Beispiel gemacht wurde.
[IconInfo]
Das Interface Ausgabe definiert eine Konstante LAENGE und eine Methode ausgabe(). Die Klasse Schnittstellen1 implementiert das Interface und muss damit die Methode ausgabe() überschreiben. Der Zugriff auf die Konstante LAENGE kann ohne den Namen des Interfaces erfolgen.
Java 5 Programmierhandbuch
137
Interfaces Listing 4.17: \Beispiele\de\j2sebuch\kap04\Schnittstellen1.java nterface Ausgabe { final int LAENGE = 100; void ausgabe(String s); } public class Schnittstellen1 implements Ausgabe { public void ausgabe(String s) { System.out.println("Wert von LAENGE: " + LAENGE); System.out.println(s); } public static void main(String[] args) { new Schnittstellen1().ausgabe("Test"); } }
Syntax Geben Sie das Attribut public an, um ein öffentliches Interface zu definieren. Die Datei muss dann wie im Fall einer Klasse den Namen des Interfaces tragen. Danach folgt das Schlüsselwort interface und der Bezeichner des Interfaces. Optional kann ein Interface von einem oder mehreren anderen Interfaces erweitert werden. Geben Sie in diesem Fall das Schlüsselwort extends und die Namen der Interfaces, getrennt durch Kommata, an. Innerhalb des Interfaces können Sie Konstanten und Methodenrümpfe deklarieren. Konstanten sind dabei automatisch public static final, so dass diese Attribute nicht angegeben werden müssen (und möglichst auch nicht angegeben werden sollten). Der Wert einer Konstanten kann sich auch aus dem Rückgabewert einer Methode ergeben. Methoden sind immer public abstract, so dass auch diese Attribute nicht angegeben werden müssen. Statt eines Methodenrumpfs wird hinter der Methodendeklaration nur ein Semikolon angefügt. Dadurch müssen die Methoden beim Implementieren in einer Klasse ebenfalls als public deklariert werden. Ein Interface beschreibt also immer die öffentliche Schnittstelle einer Klasse. [public] interface [extends , [ ...]] { [public] [static] [final] ; [public] [abstract] (); }
138
4 – Klassen, Interfaces und Objekte
Eine Klasse kann ein Interface implementieren, indem sie alle Methoden des Interfaces vollständig definiert. Der Klassedeklaration werden dazu das Schlüsselwort implements und die zu implementierenden Interfaces angefügt. Im Gegensatz zur Vererbung kann eine Klasse mehrere Interfaces implementieren. Implementiert eine Klasse nicht alle Methoden der angegebenen Interfaces, wird sie zu einer abstrakten Klasse, von der keine Objekte erzeugt werden können. class implements , {}
Verwenden Sie den Operator instanceof um zu prüfen, ob ein Objekt ein bestimmtes Interface implementiert. if(Bezeichner instanceof InterfaceTyp) ...
[IconInfo]
Interfaces müssen nicht unbedingt Methoden besitzen. In diesem Fall dienen sie lediglich zur Markierung einer Klasse, dass sie eine bestimmte Schnittstelle besitzt. Ein solches Interface des JDK ist beispielsweise java.io.Serializable. Damit signalisieren Sie für eine Klasse, dass deren Objekte serialisiert werden können (ihren Zustand speichern).
Probleme bei der Vererbung von Interfaces Wenn Sie Interfaces vererben, kann es zu Mehrdeutigkeiten kommen. Ein abgeleitetes Interface erbt alle Konstanten und Methoden seines Basisinterface. So kann es passieren, dass ein Interface über verschiedene Wege eine gleichnamige Konstante oder Methode erbt.
[IconInfo]
Das Interface K4 erbt von den Interfaces K2 und K3 die Konstante KONSTANTE1. Die Interfaces K2 und K3 erben die Konstante KONSTANTE1 wiederum von K1. Beim Zugriff auf die Konstante KONSTANTE1 im Interface K4 tritt ein Compilerfehler auf, denn es ist nicht klar, welche Konstante benutzt werden soll. Aus diesem Grund wird die Konstante in K4 erneut definiert. Sie überschreibt damit die geerbten Konstanten und es gibt keine Probleme bei der Kompilierung.
Listing 4.18: \Beispiele\de\j2sebuch\kap04\Schnittstellen2.java interface K1 { int KONSTANTE1 = 1; int KONSTANTE2 = 20; void ausgabe(); }
Java 5 Programmierhandbuch
139
Interfaces Listing 4.18: \Beispiele\de\j2sebuch\kap04\Schnittstellen2.java (Forts.) interface K2 extends K1 { int KONSTANTE1 = 2; void ausgabe(); } interface K3 extends K1 { int KONSTANTE1 = 3; void ausgabe(); } interface K4 extends K2, K3 { final int KONSTANTE1 = 4; } public class Schnittstelle2 implements K4 { public void ausgabe() { System.out.println(K1.KONSTANTE1); System.out.println(K2.KONSTANTE1); System.out.println(K3.KONSTANTE1); System.out.println(K4.KONSTANTE1); System.out.println(K4.KONSTANTE2); } public static void main(String[] args) { new Schnittstelle2().ausgabe(); } }
Es gelten die folgenden Regeln: 쐌 Wird ein und dieselbe Konstante über zwei Wege (K2 und K3) vererbt, z.B. die Konstante KONSTANTE2 des Interfaces K1, führt dies nicht zu einem Fehler beim Zugriff über das Interface K4 (K4.KONSTANTE2). 쐌 Eine neue Konstantendeklaration überdeckt eine vorhandene mit demselben Namen. Im Beispiel überdeckt jeweils die KONSTANTE1 des betreffenden Interface die gleichnamige(n) Konstanten des/der Basisinterfaces. Deshalb führt der Zugriff über die verschiedenen Interfaces bei der Ausgabe nicht zu einem Fehler. 쐌 Werden zwei Konstanten mit dem gleichen Namen geerbt, führt das noch nicht zu einem Compilerfehler. Der Zugriff muss aber in diesem Fall über den Namen des deklarierenden Interfaces erfolgen. Wenn Sie die Deklaration der Konstanten KONSTANTE1 im Interface K4 entfernen, führt dies zu einem Compilerfehler, weil jetzt die Anweisung K4.KONSTANTE1 mehrdeutig ist.
140
4 – Klassen, Interfaces und Objekte
쐌 Beim Vererben von Methoden, welche den gleichen Namen und die gleiche Signatur besitzen, gibt es keine Probleme, da eine Klasse die betreffende Methode sowieso implementieren muss und ein Interface nur die Deklaration des Methodenkopfs enthält.
4.11 Adapterklassen Einige Interfaces besitzen zahlreiche Methoden, wie z.B. das Interface List mit über 20 Methoden. In vielen Fällen möchte eine Klasse nur einige Methoden eines Interface implementieren, wie es häufig bei der Implementierung von Ereignissen in der grafischen Programmierung benötigt wird. Aus diesem Grund werden für einige Interfaces so genannte Adapterklassen bereitgestellt, welche die Methoden des Interfaces implementieren. Oft verfügen diese nur über leere Methodenrümpfe oder einen Standardrückgabewert. Anstatt das betreffende Interface zu implementieren, leiten Sie eine Klasse von der Adapterklasse ab. Adapterklassen können natürlich außer den vom Interface implementierten Methoden noch weitere Methoden besitzen. Sie werden meist über den Namen des Interfaces und das Suffix Adapter benannt, z.B. InterfaceXYZAdapter. Da Java nur einfache Vererbungshierarchien unterstützt, ist diese Verwendung von Adapterklassen nicht immer möglich bzw. ratsam. Dieses Problem wird z.B. im Bereich der grafischen Programmierung über die Verwendung von inneren Klassen bzw. anonymen Klassen gelöst.
[IconInfo]
Das Interface Adresse definiert zwei Methoden, die einen Namen und einen Vornamen zurückgeben. Die Klasse AdresseAdapter implementiert dieses Interface und liefert jeweils einen Standardwert in den Methoden zurück. Letztendlich erweitert die Klasse Adapter die Klasse AdresseAdapter und implementiert die Methoden der Adapterklasse neu.
Listing 4.19: \Beispiele\de\j2sebuch\kap04\Adapter.java public class Adapter extends AdresseAdapter { public Adapter() { System.out.printf("%s %s", getVorname(), getName()); } public String getVorname() { return "Dagobert"; } public static void main(String[] args) { new Adapter(); } }
Java 5 Programmierhandbuch
141
Abstrakte Klassen und Methoden Listing 4.19: \Beispiele\de\j2sebuch\kap04\Adapter.java (Forts.) interface Adresse { String getName(); String getVorname(); } class AdresseAdapter implements Adresse { public String getName() { return "Duck"; } public String getVorname() { return "Donald"; } }
4.12 Abstrakte Klassen und Methoden In umfangreicheren Anwendungen kann es innerhalb einer Klassenhierarchie notwendig sein, dass bestimmte Klassen gewisse gemeinsame Grundeigenschaften besitzen. Über Interfaces lässt sich z.B. sicherstellen, dass eine Klasse, die ein Interface implementiert, alle Methoden des Interfaces zur Verfügung stellt. Abstrakte Klassen bieten eine weitere Möglichkeit, eine Schnittstelle für Klassen vorzugeben. Sie können, wie in Interfaces, nur die Methodendefinitionen ohne deren Implementierung enthalten. Ebenso wie in »normalen« Klassen können sie aber auch vollständig implementierte Methoden und Variablen besitzen. Es ist nicht möglich, von abstrakten Klassen Objekte zu erzeugen. Den Versuch quittiert bereits der Compiler mit einer Fehlermeldung. Beispiel public abstract class Ausgabe { public abstract void ausgeben(String s); } // und die Implementierung public class TestAusgabe extends Ausgabe { public void ausgeben(String s) { System.out.println(s); } }
142
4 – Klassen, Interfaces und Objekte
Eine Klasse ist abstrakt, wenn sie mit dem Attribut abstract versehen ist. Enthält eine Klasse mindestens eine abstrakte Methode, muss das Attribut abstract bei der Klasse angegeben werden. Die abstrakte(n) Methode(n) kann/können in der Klasse selbst deklariert werden oder stammen aus implementierten Interfaces oder erweiterten Basisklassen. Sie können aber auch eine Klasse als abstract definieren, wenn sie keine abstrakten Methoden enthält. Implementiert eine abgeleitete Klasse die abstrakten Methoden der Basisklasse, wird daraus eine konkrete Klasse (wie sie bisher verwendet wurden). Werden dagegen nicht alle abstrakten Methoden implementiert, handelt es sich wiederum um eine abstrakte Klasse. Abstrakte Klassen werden eingesetzt, wenn eine bestimmte Grundfunktionalität (bereits implementierte Methoden) verbunden mit der Definition einer Schnittstelle (die abstrakten Methoden) gebraucht wird. Wenn Sie nur eine Schnittstelle benötigen, das heißt nur abstrakte Methoden in einer Klasse definieren, ist der Einsatz eines Interface ratsamer. Sie haben dann immer noch die Möglichkeit, die implementierende Klasse von einer anderen abzuleiten. Außerdem können Sie durch die Verwendung mehrerer Interfaces verschiedene Schnittstellen in einer Klasse realisieren. Syntax Die Angabe des Zugriffsattributs public zu Beginn ist erforderlich, wenn Sie die Klasse auch außerhalb des Package verwenden möchten. Es folgt das Attribut abstract, das im Falle einer abstrakten Klasse immer angegeben werden muss. Besitzt die Klasse abstrakte Methoden, wird diesen ebenfalls das Attribut abstract vorangestellt. Die Methode enthält dann statt eines Rumpfs in geschweiften Klammern nur die Angabe eines Semikolons. [public] abstract class { [Attribut] abstract (); }
Wie im Falle von Interfaces ist es möglich, Variablen vom Typ einer abstrakten Klasse zu erzeugen. Diesen können dann aufgrund der Zuweisungskompatibilität Objekte von abgeleiteten, konkreten Klassen zugewiesen werden. [IconInfo]
[IconInfo]
Die abstrakte Klasse Zahlenausgabe enthält bereits die vollständig implementierte Methode ausgabeStd(), die eine Zahl ohne spezielle Formatierung ausgibt. Es sollen aber alle Klassen, die diese Klasse erweitern, die Methode ausgabeSpecial() implementieren, die eine speziell formatierte Ausgabe der übergebenen Zahl ermöglicht.
Java 5 Programmierhandbuch
143
Methoden überschreiben Listing 4.20: \Beispiele\de\j2sebuch\kap04\Abstrakt.java public class Abstrakt extends Zahlenausgabe { void ausgabeSpecial(int zahl) { System.out.printf("%06d\n", zahl); } public static void main(String[] args) { new Abstrakt().ausgabeSpecial(11); } } abstract class Zahlenausgabe { abstract void ausgabeSpecial(int zahl); void ausgabeStd(int zahl) { System.out.printf("%d", zahl); } }
4.13 Methoden überschreiben Sie haben bisher das Überladen von Methoden kennen gelernt, wobei mehrere Methoden mit gleichem Namen, aber unterschiedlichen Parameterlisten zugelassen wurden. Beim Überschreiben von Methoden wird eine Methode einer Basisklasse in einer abgeleiteten Klasse mit der identischen Signatur implementiert. Die neue Methode überschreibt (verdeckt) damit die Methode der Basisklasse. Eine Basisklasse kann auf diese Weise eine Grundfunktionalität zur Verfügung stellen, die in abgeleiteten Klassen anders implementiert wird. Optional lässt sich in der Methode der abgeleiteten Klasse auch die Methode der Basisklasse aufrufen, um diese Grundfunktionalität zu nutzen. Syntax 쐌 In einer abgeleiteten Klasse wird eine Methode definiert, welche die gleiche Signatur einer Methode der Basisklasse besitzt. Damit wird die Methode der Basisklasse überschrieben (neu implementiert). 쐌 Es können nur Methoden mit den Zugriffsattributen public, protected und package-Sichtbarkeit überschrieben werden. 쐌 Die Sichtbarkeit der Methode darf vergrößert, aber nicht verkleinert werden. Mit protected gekennzeichnete Methoden dürfen demnach mit public überschrieben werden, aber nicht umgekehrt. 쐌 Über die Anweisung super.methodenName() können Sie an beliebiger Stelle die Methode der Basisklasse aufrufen, um zusätzlich deren Funktionalität zu nutzen.
144
4 – Klassen, Interfaces und Objekte
쐌 Es lassen sich keine Methoden über die Verkettung von super aufrufen, wie z.B. in super.super.methodenName(). Dies ist nur für die unmittelbare Basisklasse möglich. Private Methoden einer Klasse können nicht in einer abgeleiteten Klasse überschrieben werden. Erstellen Sie in diesem Fall eine neue Methode. Der Aufruf der Methode der Basisklasse über super() ist dadurch auch nicht möglich. [IconInfo]
[IconInfo]
Die Klasse FormatierteAusgabe enthält eine Methode ausgabeSpecial(), die eine Zahl ohne spezielle Formatierungen ausgibt. In der davon abgeleiteten Klasse Ueberschreiben wird diese Methode überschrieben. Dazu wird zuerst die Methode der Basisklasse aufgerufen und danach ein Zeilenumbruch in die Ausgabe eingefügt.
Listing 4.21: \Beispiele\de\j2sebuch\kap04\Ueberschreiben.java public class Ueberschreiben extends FormatierteAusgabe { void ausgabeSpecial(int zahl) { super.ausgabeSpecial(zahl); System.out.printf("\n"); } public Ueberschreiben() { ausgabeSpecial(10); ausgabeSpecial(11); ausgabeSpecial(12); } public static void main(String[] args) { new Ueberschreiben(); } } class FormatierteAusgabe { void ausgabeSpecial(int zahl) { System.out.printf("%d", zahl); } }
Java 5 Programmierhandbuch
145
Polymorphie
4.14 Polymorphie Ein weiteres Hauptmerkmal objektorientierter Sprachen ist die Polymorphie, die bei der Ausführung von Methoden eine Rolle spielt. Am besten lässt sich Polymorphie anhand eines Beispiels erläutern. Beispiel Angenommen, Sie besitzen eine abstrakte Klasse bzw. ein Interface mit dem Namen DatumAusgabe und einer Methode print(). Andere Klassen können diese abstrakte Klasse erweitern bzw. das Interface implementieren. Da Sie keine besonderen Anforderungen an die Implementierung der Methode print() gestellt haben, kommen unterschiedliche Ausgaben zustande. Wenn Sie jetzt beispielsweise einem Objekt vom Typ DatumAusgabe eine Referenz auf ein Objekt der implementierenden Klasse zuweisen, ist das Ergebnis der Ausgabe von der konkreten Implementierung abhängig. 12.10.2004 12/10/2004 Heute ist der 12. Oktober 2004.
Es wird also nicht die Methode des Objekts DatumAusgabe, sondern die Methode des zugewiesenen Objekts ausgeführt. Dies ist eigentlich schon das Wesen der Polymorphie. Obwohl Sie immer die Methode print() aufgerufen haben, ist die Ausgabe dennoch bei jeder Implementierung unterschiedlich. Dabei haben Sie die Methode über ein Objekt vom Typ des Interfaces bzw. der abstrakten Klasse aufgerufen. Bei der Ausführung der Anwendung wird eine dynamische Bindung vorgenommen. Das heißt, die konkret aufzurufende Methode wird erst zur Laufzeit bestimmt. Java verwendet immer die dynamische Bindung bei Methoden, die nicht die Zugriffsattribute final, private oder static besitzen. Mit final gekennzeichnete Methoden können nicht überschrieben werden. Mit private gekennzeichnete Methoden sind außerhalb der Klasse nicht sichtbar und damit automatisch final. Das Zugriffsattribut final schaltet somit die dynamische Bindung ab. Dies hat den Vorteil, dass der Aufruf einer Methode schneller durchgeführt werden kann, weil zur Laufzeit nicht die konkret aufzurufende Methode bestimmt werden muss.
[IconInfo]
146
Die Klassen AusgabeVariante1 und AusgabeVariante2 implementieren beide das Interface DatumAusgabe auf unterschiedliche Weise (deutsche und englische Formatierung). Im Konstruktor der Klasse Polymorph wird ein Array vom Interfacetyp DatumAusgabe erzeugt und jeweils ein AusgabeVariante1- und ein AusgabeVariante2-Objekt eingefügt. Anschließend wird die Methode print() für die beiden ArrayElemente aufgerufen. Obwohl die Array-Elemente vom Typ DatumAusgabe sind, werden die korrekten Methoden der zugewiesenen Objekte ausgeführt. Dies ist das Ergebnis der dynamischen Bindung.
4 – Klassen, Interfaces und Objekte Listing 4.22: \Beispiele\de\j2sebuch\kap04\Polymorph.java public class Polymorph { public Polymorph() { DatumAusgabe[] da = new DatumAusgabe[2]; da[0] = new AusgabeVariante1(); da[1] = new AusgabeVariante2(); da[0].print(); da[1].print(); } public static void main(String[] args) { new Polymorph(); } } interface DatumAusgabe { void print(); } class AusgabeVariante1 implements DatumAusgabe { public void print() { System.out.println("12.10.2004"); } } class AusgabeVariante2 implements DatumAusgabe { public void print() { System.out.println("12/10/2004"); } }
4.15 Innere, verschachtelte und lokale Klassen Sie können innerhalb eines beliebigen Anweisungsblocks weitere Klassen definieren. Dadurch ergeben sich folgende Möglichkeiten: 쐌 Definieren Sie eine nicht statische Klasse innerhalb einer anderen Klasse, wird von einer inneren Klasse gesprochen. 쐌 Definieren Sie eine statische Klasse innerhalb einer anderen Klasse, nennt man dies eine verschachtelte Klasse. 쐌 Wenn Sie eine Klasse in einer Methode definieren, wird von einer lokalen Klasse gesprochen. Dieser Anwendungsfall ist aber eher selten. Lokale Klassen können nur auf Konstanten der äußeren Klassen zugreifen und sind nur innerhalb der Methode verwendbar.
Java 5 Programmierhandbuch
147
Innere, verschachtelte und lokale Klassen
4.15.1
Innere Klassen
Klären wir zuerst die Eigenschaften von inneren Klassen, bevor wir deren Anwendungsgebiete erläutern. Innere Klassen können auch die Zugriffsattribute private und protected besitzen. Damit lassen sich innere Klassen innerhalb eines Package »verstecken«. Die Methoden der inneren Klassen können auf alle Elemente der äußeren Klasse zugreifen, auch auf private. Ein Objekt der inneren Klasse ist immer von einem Objekt der äußeren Klasse abhängig, d.h., es muss immer ein Objekt der äußeren Klasse existieren. Deshalb können innere Klassen keine statischen Elemente besitzen. Sie können nur innerhalb einer äußeren Klasse Objekte der inneren Klasse erzeugen. Für diese Aufgabe wird der Operator new über das Objekt der äußeren Klasse verwendet. Innen in = new Innen(); Aussen au = in.new Innen(); // oder kürzer Innere in = new Aussen().new Innen();
Für innere Klassen werden ebenfalls separate *.class-Dateien erzeugt. Diese verwenden eine besondere Namensgebung. So wird der äußere Klassenname vom inneren Namen durch das $-Zeichen getrennt, z.B. Aussen$Innere1$Innere2.class. Anwendungsbeispiele Müssen Sie einen bestimmten Typ in einer Methode zurückgeben und benötigen gleichzeitig noch Zugriff auf alle Elemente der betreffenden Klasse, können Sie z.B. innere Klassen verwenden. Über innere Klassen lässt sich eine Klasse oder ein Interface erweitern, ohne dass davon die äußere Klasse betroffen ist. Angenommen, Sie möchten in einer Klasse 100 Zahlen verwalten. Weiterhin möchten Sie anderen Klassen eine Möglichkeit bieten, diese Zahlen nacheinander zu durchlaufen. Wenn Sie diese Funktionalität direkt in der Klasse implementieren, müssen Sie sich immer die aktuelle Position merken. Wenn aber mehrere andere Klassen die Zahlen durchlaufen wollen, würden alle die gleiche Position verwenden. Eine Lösung bietet eine innere Klasse, die einen Index verwaltet und Methoden zum Durchlaufen der gespeicherten Zahlen besitzt. Ein Beispiel für die Verwaltung von Strings folgt gleich.
4.15.2
Verschachtelte Klassen
Statische innere Klassen sind nicht von einem Objekt der äußeren Klasse abhängig. Sie können völlig unabhängig von der äußeren Klasse verwendet werden. Im Gegensatz zu inneren Klassen können sie auch statische Elemente besitzen. Der Zugriff auf den Klassennamen erfolgt über den Namen der äußeren Klasse. Auf diese Weise können auch Objekte von verschachtelten Klassen erstellt werden. Einzig die erzeugte *.class-Datei hat einen speziellen Namen, z.B. Aussen$Innen.class wie im folgenden Beispiel.
148
4 – Klassen, Interfaces und Objekte public class Aussen { static class Innen { } public static void main(String[] args) { Aussen.Innen ai = new Aussen.Innen(); } }
Verschachtelte Klassen werden eingesetzt, wenn sie hauptsächlich als Hilfsklasse der äußeren Klasse genutzt werden. Haben sie eine allgemeinere Verwendung, können sie besser als »echte« Klassen angelegt werden.
[IconInfo]
[IconInfo]
Das folgende Programm ist etwas komplexer, zeigt aber an einem interessanten Beispiel die Verwendung von inneren Klassen. Das Collection API nutzt ebenfalls diese Funktionalität. Bei Interesse können Sie sich den SourceCode in der Datei [InstallJDK]\src.zip für die Datei AbstractList.java anschauen. Darin wird z.B. eine innere Klasse Itr definiert, die eine ähnliche Funktion besitzt.
Dieses Beispiel definiert eine Klasse StringArray, die ein Feld aus fünf Zeichenketten (den Zahlen 1 bis 5) verwaltet (weitere Informationen zu Arrays finden Sie in Kapitel 6). Sie besitzt nur die Methode getStringIterator(), die ein Objekt vom Typ der inneren Klasse SI zurückgibt. Dieses Objekt wird anschließend benutzt, um die Elemente des Arrays vorwärts oder rückwärts zu durchlaufen und auszugeben. Das Interface StringIterator besitzt zwei Methoden, die das nächste bzw. das vorige Element des Stringarrays liefern. Die Klasse StringArray definiert ein Array feld mit fünf Einträgen. Weiterhin besitzt es eine innere Klasse SI, die das Interface StringIterator implementiert. Der Vorteil der Verwendung der inneren Klasse besteht darin, dass sie einen Index für die aktuelle Position verwaltet und direkt auf die Elemente des Arrays zugreifen kann. Sie können dadurch mehrere solcher Iteratoren erzeugen und die Elemente des Felds unabhängig voneinander durchlaufen. Der Parameter pos steuert, ob die Liste von vorn oder von hinten durchlaufen werden soll.
Java 5 Programmierhandbuch
149
Innere, verschachtelte und lokale Klassen Listing 4.23: \Beispiele\de\j2sebuch\kap04\InnereKlassen.java (Auszug) interface StringIterator { String getNext(); String getPrev(); } class StringArray { String[] feld = {"1", "2", "3", "4", "5"}; class SI implements StringIterator { int index = -1; public SI(boolean pos) { if(pos) index = feld.length; } public String getNext() { index++; if(index < feld.length) return feld[index]; else return null; } public String getPrev() { index--; if(index >= 0) return feld[index]; else return null; } } public StringIterator getStringIterator(boolean pos) { return new SI(pos); } }
Zum Test wird nun im Konstruktor der Klasse InnereKlassen ein Objekt vom Typ StringArray erzeugt. Über dessen Methode getStringIterator() werden zwei StringIterator-Objekte zurückgegeben, welche die Elemente einmal vorwärts und einmal rückwärts durchlaufen.
150
4 – Klassen, Interfaces und Objekte Listing 4.24: \Beispiele\de\j2sebuch\kap04\InnereKlassen.java (Auszug) public class InnereKlassen { public InnereKlassen() { StringArray sa = new StringArray(); StringIterator siVor = sa.getStringIterator(false); StringIterator siZur = sa.getStringIterator(true); String s = ""; while((s = siVor.getNext()) != null) System.out.println(s); while((s = siZur.getPrev()) != null) System.out.println(s); } public static void main(String[] args) { new InnereKlassen(); } }
4.16 Anonyme Klassen Die letzte Steigerungsstufe sind die anonymen Klassen. Dies sind unbenannte innere Klassen. Sie verbinden die Klassendefinition und die Objekterzeugung. Anonyme Klassen besitzen keinen Namen, deshalb können Sie diese später auch nicht mehr ansprechen. Eine anonyme Klasse muss von einem Interface oder einer Klasse erweitert werden. Objekte einer anonymen Klasse werden in der Regel innerhalb einer Methode als Parameter erzeugt. Da nur das Objekt einer anonymen Klasse verwendet werden kann, macht die Definition von nicht öffentlichen Methoden kaum Sinn. Meist implementieren anonyme Klassen ein bestimmtes Interface oder überschreiben ausgewählte Methoden einer Adapterklasse. Diese Vorgehensweisen werden Sie noch in den Kapiteln zur Ereignisbehandlung in Grafikanwendungen kennen lernen. Die Syntax anonymer Klassen ist nicht unbedingt einfach zu lesen. Sie sollten deshalb anonyme Klassen wirklich nur dort einsetzen, wo es sinnvoll ist und wo relativ wenig SourceCode zur Implementierung notwendig ist. [IconInfo]
Beispiel Der Methode rufeAnonym() wird als Parameter ein Objekt einer anonymen Klasse übergeben. Die Klasse implementiert das Interface Versionsinfo, in dem es die Methode getVersion() mit Leben füllt. Eine andere Möglichkeit besteht in der Zuweisung eines Objekts einer anonymen Klasse an eine Referenzvariable, wie im zweiten Beispiel gezeigt
Java 5 Programmierhandbuch
151
Anonyme Klassen
wird. Da eine anonyme Klasse keinen Namen besitzt, wird vom Compiler einfach eine fortlaufende Nummer für die erzeugte *.class-Datei vergeben, z.B. Aussen$1.class. interface Versionsinfo { int getVersion(); } ... rufeAnonym( new Versionsinfo() // hier beginnt die anonyme Klasse { public int getVersion() { return 1; } } // und hier endet sie ); // oder Verwendung in einer Zuweisung Iterable c = new java.util.ArrayList() { ... };
Syntax Hinter dem Schlüsselwort new ist eine Basisklasse bzw. ein Interface anzugeben, von dem die anonyme Klasse erweitert wird. Es folgt ein Klammerpaar, in dem beim Ableiten von einer Klasse Parameter an einen Konstruktor übergeben werden können. Anonyme Klassen besitzen selbst keinen Konstruktor. Sie können aber optional einen Initialisierungsblock enthalten. new Basisklasse | Interface() { { System.out.println("Initialisierung"); } // Methoden und Variablendeklarationen }
Auch dieses Beispiel muss etwas weiter ausholen, um das Anwendungsgebiet von anonymen Klassen zu zeigen. Es wird ein Interface PrintOut definiert, das eine Methode ausgabe() enthält. [IconInfo]
Die Klasse LogAusgabe besitzt eine Methode ausgeben(), die ein Objekt vom Typ des Interfaces PrintOut entgegennimmt und die Methode ausgabe() aufruft.
152
4 – Klassen, Interfaces und Objekte In der Klasse AnonymeKlasse wird ein Objekt der Klasse LogAusgabe erzeugt und danach dessen Methode ausgeben() aufgerufen. Jetzt kommt die anonyme Klasse ins Spiel. Das an die Methode übergebene PrintOut-Objekt wird nun über eine anonyme Klasse erstellt, welche die Methode ausgabe() des Interfaces implementiert. Da diese Funktionalität nur an dieser Stelle benötigt wird, wäre der Aufwand für die Definition einer separaten Klasse zu groß. Insbesondere, weil die Implementierung sehr übersichtlich ist.
Listing 4.25: \Beispiele\de\j2sebuch\kap04\AnonymeKlassen.java public class AnonymeKlassen { public AnonymeKlassen() { LogAusgabe la = new LogAusgabe(); la.ausgeben(new PrintOut() { public void ausgabe() { System.out.println("und jetzt kommts ..."); } }); } public static void main(String[] args) { new AnonymeKlassen(); } } interface PrintOut { void ausgabe(); } class LogAusgabe { public void ausgeben(PrintOut p) { p.ausgabe(); } }
Java 5 Programmierhandbuch
153
5 5.1
Packages Einführung
Sehr viele Entwickler erzeugen Java-Klassen bzw. -Anwendungen, deshalb muss sichergestellt werden, dass sich die verwendeten Namen nicht überschneiden. Setzen Sie beispielsweise zwei Bibliotheken ein, welche Klassen mit gleichen Namen besitzen, liefert der Compiler eine Fehlermeldung. Für den Compiler muss die Klasse immer eindeutig identifizierbar sein. Java verwaltet so genannte Kompiliereinheiten (Compilation Units) über Packages. Kompiliereinheiten sind Klassen, Interfaces und Aufzählungen. Der Name eines Package entspricht dabei in der Regel einem bestimmten Verzeichnis auf einem Datenträger. In diesem Verzeichnis befinden sich alle Klassen, Interfaces und Aufzählungen, die zu diesem Package gehören. Grundsätzlich hängt es vom benutzten Betriebssystem ab, wie ein Package-Name interpretiert wird. Ein Package kann auch einer Tabelle oder einer Datenbank entsprechen. Im Folgenden wird immer davon ausgegangen, dass Packages auf Verzeichnisse abgebildet werden. [IconInfo]
Im Klassenpfad sucht Java nach Klassen, Interfaces und Aufzählungen. Der Klassenpfad enthält hierfür Verzeichnisangaben und/oder Pfade zu Archiven (d.h. einzelnen JAR-Dateien). Ausgehend von diesen Verzeichnissen oder innerhalb des Archivs wird die Verzeichnisstruktur des Package bestimmt. Im Package werden wiederum die Klassen etc. gefunden. Beispiel Der Klassenpfad enthält das Verzeichnis C:\MeineBibos sowie das Archiv C:\MeineArchive\ Hilfsklassen.jar. Sucht der Compiler nach der Klasse de.j2sebuch.kap05.PackageText, wird der Pfad zu den Bibliotheken um den relativen Pfad ..\de\j2sebuch\kap05, dem Package-Namen, erweitert. Im Pfad C:\MeineBibos\de\j2sebuch\kap05 wird nach der Klasse PackageTest gesucht. Wird sie hier nicht gefunden, prüft der Compiler das Archiv nach dem entsprechenden Eintrag. Das Archiv muss deshalb auch die Pfadangaben der Dateien beinhalten.
5.1.1
Package-Hierarchie
Für eine sinnvolle und eindeutige Aufteilung der Klassen auf Packages reicht eine Ebene nicht aus. Sie können deshalb unter einem Package ein weiteres Package definieren. Diese Verschachtelung lässt sich mehrfach durchführen. Verschachtelte Packages werden auch als Unter- oder Subpackages bezeichnet. Java gruppiert zusammengehörige Typen (Klassen, Interfaces, Aufzählungen) in PackageHierarchien. So finden Sie im Package java.lang die Standardklassen und unter dem
Java 5 Programmierhandbuch
155
Einführung
Package java.util zahlreiche Hilfsklassen. Die einzelnen Klassen werden aber nicht einzeln auf dem Datenträger verwaltet, sondern in einem Archiv zusammengefasst. Dies ist platzsparender und erlaubt einen einfacheren Zugriff. Das Archiv unter [InstallJDK]\jre\ lib\rt.jar fasst die meisten Klassen, Interfaces und Aufzählungen des Java-Laufzeitsystems zusammen. Syntax Package-Namen entsprechen der Verzeichnisstruktur, unter der eine bestimmte Klasse etc. abgelegt ist. Dabei werden statt der Verzeichnistrenner wie / oder \ Punkte zum Trennen der Verzeichnisse verwendet. Die Namen der Verzeichnisse und damit auch der des Package werden standardmäßig klein geschrieben. Für die Zuordnung einer Datei zu einem Package benutzen Sie die package-Anweisung. Sie muss immer am Anfang der Datei stehen. Nur Kommentare sind vor dieser Anweisung erlaubt. Nach der package-Anweisung folgt der Name des Package. Beispiel Sie haben eine Verzeichnisstruktur de\j2sebuch\kap05 angelegt und in diesem Verzeichnis die Datei PackageTest.java gespeichert. Für die Zuordnung der Datei zum Package fügen Sie die folgende Anweisung zu Beginn der Datei ein. package de.j2sebuch.kap05;
Was passiert, wenn Sie die package-Anweisung weglassen? Angenommen, Sie befinden sich im Verzeichnis, das de\j2sebuch\kap05 übergeordnet ist. Sie können die Datei PackageTest.java wie folgt übersetzen: [IconInfo]
javac de\j2sebuch\kap05\PackageTest.java Möchten Sie die Anwendung aber starten (in der Annahme, dass die Datei eine Methode main() besitzt), gelingt dies nicht. java de.j2sebuch.kap05.PackageTest java de\j2sebuch\kap05\PackageTest java PackageTest Der erste Aufruf funktioniert nicht, da sich die Klasse PackageTest nicht im Package de.j2sebuch.kap05 befindet. Es fehlt die package-Anweisung in der Klassendefinition. Der zweite Aufruf geht ebenfalls schief, weil der gesamte Name als Klassenname interpretiert wird und diese im aktuellen Verzeichnis gesucht wird. Schließlich schlägt auch der letzte Aufruf fehl, da im aktuellen Verzeichnis keine Klasse mit dem Namen PackageTest existiert.
156
5 – Packages Zum Starten der Anwendung haben Sie nun zwei Möglichkeiten. Entweder Sie wechseln in das Verzeichnis ..\de\j2sebuch\kap05 und verwenden den Aufruf java PackageTest oder Sie fügen eine package-Anweisung ein und benutzen den ersten Aufruf, nachdem Sie die Anwendung erneut übersetzt haben. java de.j2sebuch.kap05.PackageTest
5.1.2
Benannte und unbenannte Packages
Besitzt eine *.java-Datei eine package-Anweisung, wird von einem benannten Package gesprochen. Damit die Klassen etc. des Package gefunden werden, muss sich das Startverzeichnis des Package im Klassenpfad befinden. Hat eine *.java-Datei keinen Package-Namen, gehört diese Datei zum unbenannten Package (auch Default- oder Standardpackage). Das unbenannte Package entspricht dem aktuellen Arbeitsverzeichnis. Enthält ein Verzeichnis *.java-Dateien ohne package-Anweisungen, können Sie diese Dateien direkt übersetzen und ausführen. Unbenannte Packages sollten nur für kleine Anwendungen oder kurze Beispiele eingesetzt werden. Sie lassen keine Strukturierung der Anwendung zu und verursachen schnell Namenskonflikte. Normalerweise werden unbenannte Packages auch in allen Verzeichnissen des Klassenpfads gesucht. Allerdings sollte man sich nicht darauf verlassen, da dies vom verwendeten Betriebssystem abhängig ist. [IconInfo]
5.1.3
Zugriffsrechte
Innerhalb von Packages kommt ein weiteres Zugriffsattribut zum Einsatz. Das »package«Attribut, welches nicht separat angegeben wird, erhalten alle Elemente, denen Sie keines der Attribute public, protected oder private zugeordnet haben. Die Sichtbarkeit ist damit auf Package-Ebene festgelegt. Beispiel Die beiden ersten Klassen befinden sich im gleichen Package und können gegenseitig aufeinander zugreifen. Die letzte Klasse befindet sich in einem anderen Package. Auf sie kann erst nach einem Import zugegriffen werden. Voraussetzung ist natürlich, dass diese Klassen ohne Zugriffsattribut definiert wurden.
Java 5 Programmierhandbuch
157
Packages importieren de.j2sebuch.kap05.PackageImport de.j2sebuch.kap05.StatischeInhalte de.j2sebuch.kap05.util.Hilfsklasse
Durch das Weglassen aller Zugriffsattribute sind die betreffenden Elemente für den Zugriff durch andere ungeschützt. Man muss lediglich eine Klasse in dem Package ablegen und erhält Zugriff auf alle Package-sichtbaren Elemente. Verwenden Sie deshalb die Zugriffsattribute private und final, um den Zugriff auf bestimmte Elemente zu unterbinden.
[IconInfo]
5.1.4
Aufteilung einer Anwendung in Packages
Bei der Strukturierung Ihrer Anwendungen, Bibliotheken und sonstigen *.java-Dateien sollten Sie die folgenden Konventionen beachten. Damit der Package-Name weltweit eindeutig ist, wird als Verzeichnisstruktur die Umkehrung der ersten beiden Teile des DomainNamens Ihrer Firma verwendet. Ist dies nicht möglich, weil Sie nicht in einer Firma arbeiten bzw. die Firma keine Internetpräsenz besitzt, können Sie auch de für Deutschland und eine Kurzform Ihres Familiennamens einsetzen. Beispiel Firma: Zwergkaninchen AG Domain: www.zwergkaninchenAG.de Package-Name: de.zwergkaninchenag Jetzt können Sie Subpackages definieren, um eine weitere Strukturierung zu schaffen. So ist es beispielsweise möglich, dass einige Anwendungen bestimmte Klassen gemeinsam benutzen oder dass Sie bestimmte Klassen für die Zucht von Zwergkaninchen entwickelt haben. Die folgende Struktur stellt nur eine Möglichkeit der Strukturierung dar. ..\de\zwergkaninchenag\zucht ..\de\zwergkaninchenag\rassen ..\de\zwergkaninchenag\util ..\de\zwergkaninchenag\loewenkopf ..\de\zwergkaninchenag\hermelin
5.2
Anwendung für die Zucht Anwendung für die Verwaltung der Rassen Hilfsklassen (z.B. eine Klasse Kaninchen) spezielle Klassen für Löwenkopfkaninchen spezielle Klassen für Hermelinkaninchen
Packages importieren
Über die package-Anweisung können Sie eine Verbindung einer Klasse, eines Interface oder einer Aufzählung zu einem Package herstellen. Beim Einsatz von anderen Klassen etc. müssen Sie Java mitteilen, in welchem Package sich diese befinden. Auf alle Typen des eigenen Package haben Sie sofort Zugriff. Weiterhin sind automatisch alle Typen des Package java.lang verfügbar, weil es sich hier um die Standardklassen von Java handelt. Bleibt noch die Frage, wie Sie Typen anderer Packages bereitstellen.
158
5 – Packages
Referenzieren Sie jeden benötigten Typ mit seinem vollqualifizierten Namen. Dies ist jedoch eine schreibintensive Arbeit und sie trägt sicher nicht zur guten Lesbarkeit des Programmcodes bei. java.util.ArrayList lst = new java.util.ArrayList();
Importieren Sie besser über die import-Anweisung einzelne Typen oder alle öffentlichen Typen eines Package und verwenden Sie anschließend nur noch den Namen des Typs. import java.util.*; // importiert alle Typen import java.util.ArrayList; // importiert nur den Type ArrayList ... ArrayList lst = new ArrayList();
Während der Import aller Typen eines Package diese in nur einer Anweisung einbindet, erkennt man beim Import eines einzelnen Typs leichter seine Package-Zugehörigkeit. Beide Importe sind aber gleichwertig. Das Importieren aller Typen bedeutet im Übrigen nicht, dass diese wirklich alle geladen bzw. in die *.class-Datei eingebunden werden. Es werden letztendlich nur die benötigten Typen importiert. Beachten Sie die folgenden Hinweise zum Import: 쐌 Es wird immer nur das angegebene Package ohne Subpackages importiert. Diese müssen bei Bedarf über zusätzliche import-Anweisungen verfügbar gemacht werden. 쐌 Das Package java.lang wird automatisch importiert. 쐌 Grundsätzlich wird nicht ein Package importiert, sondern seine Typen. 쐌 Es kann möglich sein, dass die Verwendung eines Typs beim Import mehrerer Packages nicht eindeutig ist. So befindet sich beispielsweise die Klasse Date in den Packages java.sql und java.util. Haben Sie beide Packages eingebunden und möchten die Klasse Date nutzen, ist die Angabe des voll qualifizierten Namens notwendig, z.B. java.sql.Date. 쐌 Existiert ein Typ in keinem der importierten Packages, meldet der Compiler einen Fehler. 쐌 Wenn Sie den einfachen Typimport verwenden, darf ein Typ mit demselben Namen nicht zweimal importiert werden (die ersten beiden Anweisungen). Beim Import aller Typen eines Package gibt es jedoch keine Probleme (die letzten beiden Anweisungen). // ---- Datei1 ---import java.sql.Date; import java.util.Date; // ---- Datei2 ---import java.sql.*; import java.util.*;
Java 5 Programmierhandbuch
// Compilerfehler // Compilerfehler // erlaubt // erlaubt
159
Statischer Import
[IconInfo]
Die Klasse Hilfsklasse enthält eine Methode, die Sie häufiger benötigen. Deshalb wurde als Package de.j2sebuch.kap05.util gewählt. Zur korrekten Arbeitsweise muss sich die Datei Hilfsklasse.java in einem Unterverzeichnis ..\util unter ..\de\j2sebuch\kap05 befinden. In der Anwendung PackageImport importieren Sie dieses Package und benutzen die Hilfsklasse. Beachten Sie, dass Sie sich beim Ausführen der Anwendung in dem Verzeichnis befinden, das ..\de\.. übergeordnet ist. Ansonsten werden die Packages nicht gefunden.
Listing 5.1: \Beispiele\de\j2sebuch\kap05\util\Hilfsklasse.java package de.j2sebuch.kap05.util; public class Hilfsklasse { public int add(int zahl1, int zahl2) { return (zahl1 + zahl2); } } Listing 5.2: \Beispiele\de\j2sebuch\kap05\PackageImport.java package de.j2sebuch.kap05; import de.j2sebuch.kap05.util.*; public class PackageImport { public PackageImport() { Hilfsklasse hk = new Hilfsklasse(); System.out.println("10 + 11 = " + hk.add(10, 11)); } public static void main(String args[]) { new PackageImport(); } }
5.3
Statischer Import
Zur besseren Lesbarkeit und einfacheren Verwendung von statischen Konstanten und Methoden werden durch den statischen Import die statischen Inhalte von Klassen importiert. Die Funktionsweise entspricht dem Import von Packages. Der statische Import soll auch den »Missbrauch« von Interfaces als Hülle für Konstanten verhindern. Er ist ab dem JDK 5.0 verfügbar.
160
5 – Packages
Beispiel Früher mussten Sie auf statische Methoden der Klasse java.lang.Math immer über den Klassennamen zugreifen. Dies vereinfacht die Lesbarkeit von umfangreichen Formeln nicht gerade. int absWert = 100 * Math.abs(-1000) / Math.min(100, 200);
Der Import der statischen Methoden der Klasse Math vereinfacht die Formel. import static java.lang.Math.*; ... int absWert = 100 * abs(-1000) / min(100, 200);
Sie haben zwei Möglichkeiten, die statischen Inhalte einer Klasse zu importieren. Entweder importieren Sie genau eine statische Variable bzw. Methode oder Sie importieren alle. import import import import
static static static static
Package.Interface.*; Package.Klasse.*; Package.Interface.Bezeichner; Package.Klasse.Bezeichner;
Das Einbinden gleichnamiger Bezeichner führt noch nicht zu einem Fehler. Wenn Sie diese aber in der verkürzten Schreibweise einsetzen, kann der Compiler den konkreten Bezeichner nicht ermitteln und meldet einen Fehler. [IconInfo]
Die Klasse StatischeInhalte definiert jeweils eine statische Konstante, Variable und Methode. Über den statischen Import werden diese in der Klasse StatischerImport eingebunden und verwendet. [IconInfo]
Listing 5.3: \Beispiele\de\j2sebuch\kap05\StatischeInhalte.java public class StatischeInhalte { public static final double PI = 3.14; public static String version = "1.0"; public static void ausgabe() { System.out.println("Statischer Import"); } }
Java 5 Programmierhandbuch
161
Statischer Import Listing 5.4: \Beispiele\de\j2sebuch\kap05\StatischerImport.java import static StatischeInhalte.*; public class StatischerImport { public StatischerImport() { System.out.println("Konstante PI: " + PI); System.out.println("Version : " + version); ausgabe(); } public static void main(String args[]) { new StatischerImport(); } }
162
6 6.1
Arrays, Wrapper und Auto(un)boxing Einführung
Müssen Sie sehr viele Werte eines bestimmten Datentyps verwalten, sind Variablen, die nur einen Wert speichern, ungeeignet. Insbesondere, wenn die benötigte Anzahl vorher noch nicht feststeht. Alle Werte einer einzigen Struktur lassen sich besser in Arrays (Feldern) zusammenfassen. Die Länge des Arrays wird beim Anlegen einmalig festgelegt. Der Zugriff auf die einzelnen Werte erfolgt über einen Index. In Java werden Arrays über Objekte realisiert. Das bedeutet, dass sie über den new-Operator dynamisch erzeugt werden müssen. Alternativ können Arrays auch über ein Literal erzeugt und initialisiert werden. Nach dem Erstellen eines Arrays ist dessen Länge fest bestimmt und kann nachträglich nicht mehr geändert werden. Benötigen Sie ein größeres oder kleineres Array, müssen Sie dieses neu erzeugen und die Werte des originalen Arrays hineinkopieren. Beispiele int[] zahlenFeld = new int[100]; // ein Array-Objekt erzeugen int zahlenFeld[] = {1, 2, 3}; // Verwendung eines Literals int matrix[][] = {{1, 2}, {2, 3}}; zahlenFeld[1] = 100; // Zugriff auf die Elemente matrix[0][1] = 200;
Syntax zum Anlegen von Arrays Zuerst geben Sie den Datentyp des Arrays an. Dies kann ein primitiver oder ein Objektdatentyp sein. Es folgt für jede Dimension ein Klammerpaar und der Name des Arrays. Die Reihenfolge der Angabe des Array-Namens und der Klammern kann auch vertauscht werden. Bevorzugt wird die Schreibweise, bei der die Klammern direkt nach dem Array-Typ gesetzt werden. Auf diese Weise werden die Typangabe und der Array-Name besser sichtbar getrennt. int[] zahlenFeld; int[][] zahlenFeld; int zahlenFeld[];
// bevorzugte Schreibweise // bevorzugte Schreibweise
Auch eine gemischte Schreibweise ist möglich. Allerdings werden dann alle Klammersetzungen berücksichtigt. int[] zahlenFeld[];
// erzeugt ein Array vom Typ int[][]
Nach der Deklaration muss das Array erzeugt werden. Hierfür haben Sie zwei Möglichkeiten. Verwenden Sie den Operator new und geben Sie den Datentyp des Arrays sowie die Array-Größe in Klammern an oder benutzen Sie eine Initialisiererliste. Diese Liste können
Java 5 Programmierhandbuch
163
Einführung
Sie jedoch nur direkt in Verbindung mit der Deklaration angeben. Die Feldgröße richtet sich dann nach der Anzahl der Werte in der Liste. int[] zahlenFeld = new int[100]; int[] zahlenFeld2; zahlenFeld2 = new int[100]; int[] zahlenFeld3 = {1, 2, 3};
// Initialisiererliste
Die Inhalte von nicht initialisierten Arrays bestehen aus den Standardwerten der betreffenden Datentypen. Zur Erstellung mehrdimensionaler Arrays geben Sie mehrere Klammerpaare an. Das Erzeugen des Arrays kann für jede Dimension einzeln oder für das gesamte Array in einer Anweisung erfolgen. int[][] matrix = new int[2][3]; // oder int[][] matrix2 = new int[2][]; for(int i = 0; i < matrix2.length; i++) matrix2[i] = new int[3];
Es ist aber auch die Angabe einer Initialisiererliste möglich. Die Größe jeder Dimension kann separat festgelegt werden. Deshalb müssen sie auch nicht notwendigerweise gleich groß sein. int matrix3[][] = {{1, 2, 3}, {4}};
Weiterhin können Sie so genannte anonyme Arrays definieren, die als Parameter an Methoden übergeben und später nicht mehr benötigt werden. Innerhalb der Methode ist ein Zugriff über den Parameternamen auf die Feldinhalte möglich. public void ausgabe(int[] werte) { for(int i: werte) System.out.println(i); } ... ausgabe(new int[]{1,2,3}); // ein anonymes Array übergeben
Arrays sind Objekte, deshalb können deren Inhalte bei der Übergabe an eine Methode dauerhaft geändert werden. Beachten Sie, dass beim Zuweisen einer Array-Variablen an eine andere Array-Variable nicht das Array selbst kopiert wird, sondern dessen Referenz. Die zweite Variable verweist anschließend auf dasselbe Array.
164
6 – Arrays, Wrapper und Auto(un)boxing
Alle Arrays besitzen die Variable length, über die Sie die Größe des Arrays ermitteln können. Von den geerbten Methoden der Klasse Object stellt die Methode clone() einen Sonderfall dar. Beim Klonen eines Arrays werden nur die Elemente der betreffenden Dimension geklont. Dies bedeutet im Falle eines mehrdimensionalen Arrays, dass die Elemente der Unterarrays nicht kopiert werden. Beachten Sie weiterhin, dass im Falle von Objekten nicht die Objekte, sondern nur die Objektreferenzen kopiert werden. int len = zahlenFeld.length;
Syntax zum Zugriff auf die Array-Elemente Auf die einzelnen Elemente eines Arrays greifen Sie über die Angabe eines Index zu. Der Index läuft von 0 bis Array-Länge-1. Er kann ein Literal oder ein Zahlenwert der Datentypen char, byte, short oder int sein. zahlenFeld[2] = 100; int i = zahlenFeld[2];
Im Falle von mehrdimensionalen Arrays verwenden Sie mehrere Klammerpaare, um den betreffenden Wert zu ermitteln. int i = matrix[1][0];
Benutzen Sie einen fehlerhaften Index, tritt zur Laufzeit eine java.lang.ArrayIndexOutOfBoundsException auf. Der häufigste Fehler ist dabei, dass der Index gleich der Länge des Arrays gesetzt wird. Der größte Indexwert ist jedoch um 1 kleiner. int[] zahlenFeld = new int[2]; int i = zahlenFeld[2]; // Fehler, da der Index nur von 0..1 läuft
[IconInfo]
Die Anwendung gibt alle möglichen Produkte der Zahlen von 1 bis 10 aus. Vor jeder Zeile wird zusätzlich eine Beschriftung eingefügt. Während die Produkte im Array matrix erst zur Laufzeit der Anwendung berechnet werden, wird der Inhalt des Arrays beschriftung über eine Initialisiererliste bereitgestellt. Die Methode ausgabe() verwendet die neue printf()-Methode zur Textausgabe. Über %10s wird eine Zeichenkette in der Breite von 10 Zeichen angezeigt. Am Anfang wird mit Leerzeichen aufgefüllt. Durch die Angabe von %4d wird eine Zahl in der Breite von 4 Zeichen ausgegeben.
Java 5 Programmierhandbuch
165
Einführung Listing 6.1: \Beispiele\de\j2sebuch\kap06\Zahlenfolgen.java public class Zahlenfolgen { public Zahlenfolgen() { int[][] produktMatrix = new int[10][10]; String[] beschriftung = {"Zeile 1", "Zeile 2", "Zeile 3", "Zeile 4", "Zeile 5", "Zeile 6", "Zeile 7", "Zeile 8", "Zeile 9", "Zeile 10"}; for(int i = 0; i < produktMatrix.length; i++) for(int j = 0; j < produktMatrix[i].length; j++) produktMatrix[i][j] = (i + 1)*(j + 1); for(int k = 0; k < 10; k++) ausgabe(beschriftung[k], produktMatrix[k]); } public void ausgabe(String text, int[] werte) { System.out.printf("%10s: ", text); for(int i: werte) System.out.printf("%4d", i); System.out.printf("%n"); } public static void main(String args[]) { new Zahlenfolgen(); } }
Arrays kopieren Müssen Sie den Inhalt eines Arrays in ein anderes Array kopieren, können Sie dies über eine Schleife oder die statische Methode arraycopy() der Klasse System durchführen. Die Positionsangaben im zweiten und vierten Parameter beziehen sich auf den Index des Arrays und beginnen deshalb auch mit 0. static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
Um beispielsweise nur das zweite und dritte Element eines Arrays an den Anfang eines zweiten Arrays zu kopieren, verwenden Sie die folgenden Anweisungen: int[] zahlenFolge = {1, 2, 3, 4}; int[] zahlenFolge2 = new int[4]; System.arraycopy(zahlenFolge, 1, zahlenFolge2, 0, 2);
166
6 – Arrays, Wrapper und Auto(un)boxing
6.2
Die Klasse Arrays
Die Klasse Arrays aus dem Package java.util besitzt zahlreiche überladene Methoden zur Verarbeitung von Arrays. Über die Methode deepToString() kann der Inhalt mehrdimensionaler Arrays als String-Repräsentation geliefert werden. static String deepToString(Object[] array)
Mit der folgenden Methode prüfen Sie, ob der Inhalt der Elemente in beiden der übergebenen Arrays identisch ist. static boolean equals([] array, [] array2)
Für die Initialisierung der Inhalte aller Elemente eines Arrays mit einem bestimmten Wert eignet sich die Methode fill() hervorragend. Über eine zweite Form der Methode können Sie auch nur die Elemente in einem bestimmten Bereich initialisieren. static void fill([] array, wert) static void fill([] array, int startIndex, int endIndex, wert)
Die Methode sort() wird zum automatisierten, absteigenden Sortieren der Elemente eines Arrays eingesetzt. Durchlaufen Sie das Array in umgekehrter Reihenfolge, erhalten Sie leicht auch die aufsteigende Reigenfolge. static void sort([] array)
Die Methode toString() eines Arrays eignet sich nicht dazu, die Elementinhalte auf einfache Weise auszugeben. Hierfür lässt sich aber die folgende Methode nutzen, der als Parameter das Array zu übergeben ist. Die Werte werden in eckige Klammern eingeschlossen und mit einem Komma getrennt. Sie erhalten mit dieser Methode aber nur die Inhalte einer Dimension. static String toString([] a)
[IconInfo]
Zuerst wird ein neues Array kopie deklariert und ihm die gleiche Länge wie die des Arrays args zugewiesen. Der Inhalt von args wird nach kopie übertragen. Beide Arrays besitzen somit denselben Inhalt, deshalb wird der Inhalt der if-Anweisung ausgeführt. Anschließend werden die Inhalte des Arrays kopie sortiert und sein Inhalt wird ausgegeben. Rufen Sie die Anwendung über java Parameter A B G E H bzw.
Java 5 Programmierhandbuch
167
Wrapper-Klassen
java de.j2sebuch.kap06.Parameter A B G E H auf. Die Buchstaben werden der Anwendung als zusätzliche Parameter übergeben. Als Ausgabe erhalten Sie den String [A, B, E, G, H].
Listing 6.2: \Beispiele\de\j2sebuch\kap06\Parameter.java import java.util.*; public class Parameter { public static void main(String args[]) { String[] kopie = new String[args.length]; System.arraycopy(args, 0, kopie, 0, args.length); if(Arrays.equals(args, kopie)) System.out.println("Sie haben die gleichen Inhalte"); Arrays.sort(kopie); System.out.println(Arrays.toString(kopie)); } }
6.3
Wrapper-Klassen
Viele Methoden, z.B. die des Collection Frameworks, erwarten Parameter vom Typ Object. Um diesen Methoden Werte eines primitiven Datentyps zu übergeben, stellt Java für jeden primitiven Datentyp und für void eine Wrapper-Klasse zur Verfügung, die diesen Wert kapselt. Die Wrapper-Klassen stammen alle aus dem Package java.lang und stehen somit automatisch zur Verfügung. Jede Wrapper-Klasse besitzt unter anderem eine Methode, um den Wert des primitiven Datentyps zurückzugewinnen. Der Vorgang des Ver- bzw. Entpackens wird auch Boxing bzw. Unboxing genannt. Primitiver Datentyp bzw. void
Wrapper-Klasse
boolean
Boolean
byte
Byte
char
Character
double
Double
float
Float
int
Integer
168
6 – Arrays, Wrapper und Auto(un)boxing Primitiver Datentyp bzw. void
Wrapper-Klasse
long
Long
short
Short
void
Void
Zur Erstellung eines Wrapper-Objekts übergeben Sie dem Konstruktor der betreffenden Klasse den Wert des primitiven Typs oder eine Zeichenkette, die in einen solchen Wert konvertiert werden kann. Integer I = new Integer(10); Integer I2 = new Integer("10");
Eine Ausnahme bildet die Klasse Void, die weder einen Konstruktor noch spezielle Methoden bietet. [IconInfo]
6.3.1
Nützliche Methoden
Die Wrapper-Klassen Byte, Double, Float, Integer, Long und Short besitzen eine gemeinsame abstrakte Basisklasse Number. Diese stellt zahlreiche Methoden zur Verfügung, um den in der Wrapper-Klasse gespeicherten Wert in einen bestimmten Datentyp umzuwandeln. Voraussetzung ist natürlich, dass die Wrapper-Klasse einen Wert des entsprechenden Typs enthält. Wenn Sie beispielsweise den Integerwert 10.000 über die Methode byteValue() in einen byte-Wert konvertieren, wird der Wert 16 geliefert. byte byteValue() double doubleValue() float floatValue() int intValue() long longValue() short shortValue()
Im Folgenden sollen am Beispiel der Klasse Integer einige weitere nützliche Methoden der Wrapper-Klassen vorgestellt werden. Sie unterscheiden sich nur im Namen der Methode, z.B. intValue() oder doubleValue(), bzw. den verwendeten Parameter- und Rückgabetypen. Die Funktionsweise ist aber gleich. Der in der Wrapper-Klasse gespeicherte Wert wird zurückgegeben. Im Falle der WrapperKlasse Double ist der Methodenname beispielsweise doubleValue(). int intValue()
Java 5 Programmierhandbuch
169
Wrapper-Klassen
Die übergebene Zeichenkette wird in einen int-Wert konvertiert. Schlägt die Konvertierung fehl, wird eine NumberFormatException ausgelöst. static int parseInt(String s)
Der über den Wrapper gespeicherte Wert wird als String zurückgegeben. String toString()
Für die direkte Umwandlung eines int-Werts in einen String benutzen Sie die folgende Methode. static String toString(int i)
Um ein Integer-Objekt auf Basis eines int- oder String-Werts zu erzeugen, verwenden Sie eine der folgenden Methoden. static Integer valueOf(int i) static Integer valueOf(String s)
Wrapper-Objekte als Parameter in Methoden Zur Änderung des innerhalb einer Wrapper-Klasse gespeicherten Werts gibt es keine Methode. Dies ist insbesondere dann von Nachteil, wenn Sie ein Objekt einer Wrapper-Klasse als Parameter an eine Methode übergeben und deren gespeicherten Wert modifizieren möchten. Als Lösung bietet sich der Einsatz von Arrays statt Wrapper-Objekten an oder Sie geben einen Rückgabewert vom Typ der Wrapper-Klasse zurück. Integer neuerWert(Integer wert) { int i = wert.intValue() + 10; return new Integer(i); } ... Integer I = neuerWert(new Integer(10));
6.3.2
Auto(un)boxing
Das manuelle Umwandeln von primitiven in Objektdatentypen und umgekehrt ist eigentlich eine lästige Aufgabe. Allerdings wird sie relativ häufig benötigt, z.B. bei Aufnahme primitiver Datentypen in Collections. Aus diesem Grund wurde in der Version 5.0 des JDK eine automatische Konvertierung von primitiven Datentypen in die korrespondierenden Wrapper und umgekehrt implementiert. Dies vereinfacht die Übergabe der betreffenden Typen und die Anwendungen werden besser lesbar.
170
6 – Arrays, Wrapper und Auto(un)boxing
Die Anwendung dieses automatischen (Un)Boxing wird dementsprechend Auto(un)boxing genannt. Benutzen Sie anstelle eines Objektdatentyps einfach den entsprechenden primitiven Datentyp oder umgekehrt. Beispiel Ohne Auto(un)boxing müssen Sie immer den korrekten Typ bei der Parameterübergabe bzw. Wertzuweisung verwenden. Dies macht die Erzeugung von Wrapper-Klassen und die Rückkonvertierung in die primitiven Typen mittels zusätzlicher Methodenaufrufe notwendig. Integer getInt(Integer value) { int i = value.intValue(); return new Integer(i); } ... System.out.println(getInt(new Integer(10)));
Der entsprechende Code mit Auto(un)boxing ist leichter zu lesen und weniger umfangreich. Integer getInt(Integer value) { int i = value; return i; } ... System.out.println(getInt(10));
Objektvariablen können null-Werte besitzen. Wenn Sie diese automatisch in primitive Datentypen konvertieren wollen, wird eine Null-
PointerException ausgelöst. [IconInfo]
Durch das Autounboxing können jetzt auch die Typen von Wrapper-Klassen in den Kontrollstrukturen wie if oder switch genutzt werden, z.B.
[IcoiInfo]
Boolean b = new Boolean(true); if(b) ... Integer I = new Integer(10); switch(I) ...
Java 5 Programmierhandbuch
171
Wrapper-Klassen
6.3.3
Bitmanipulation
Die Wrapper-Klassen Character, Integer, Long und Short implementieren zusätzliche Methoden zur Bitmanipulation. Je nach Wrapper-Klasse werden nicht alle der vorgestellten Methoden unterstützt. Alle Methoden sind statisch und liefern einen int-Wert zurück. Bis auf die Methode reverseBytes(), die von allen vier Wrappern implementiert wird, liegen alle anderen Methoden nur in den Wrappern Integer und Long vor. Methode
Erläuterung
int bitCount(int i)
Es wird die Anzahl der Bits ermittelt, die den Wert 1 enthalten.
int lowestOneBit(int i)
Es wird der Wert des ersten 1-Bits von rechts ermittelt.
int numberOfLeadingZeros(int i)
Es wird von links die Anzahl von 0-Bits bestimmt, bis ein 1-Bit erreicht wird.
int numberOfTrailingZeros(int i)
Es wird von rechts die Anzahl von 0-Bits bestimmt, bis ein 1-Bit erreicht wird.
int rotateLeft(int i, int anz)
Die Bits werden um anz Stellen links herum rotiert.
int rotateRight(int i, int anz)
Die Bits werden um anz Stellen rechts herum rotiert.
int reverse(int i)
Die Bitfolge wird vertauscht.
int reverseBytes(int i)
Die Reihenfolge der Bytes wird vertauscht.
int signum(int i)
Der Rückgabewert hängt davon ab, ob i größer, kleiner oder gleich null ist. i > 0 => 1, i < 0 => -1, i == 0 => 0
Einsatzgebiete dieser Methoden sind häufig mathematische Algorithmen. So können Sie über die Methode rotateLeft() Multiplikationen mit 2, über rotateRight() Divisionen mit 2 durchführen, z.B. dezimal 12 = binär 001100 001100 um 1 nach rechts verschoben ergibt 000110 = dezimal 6 001100 um 1 nach links verschoben ergibt 011000 = dezimal 24
Die folgende Beispielanwendung wendet alle Bitoperationen auf den Wert 200 an. Seine Binärdarstellung wird im ersten Kommentar gezeigt. [IconInfo]
172
6 – Arrays, Wrapper und Auto(un)boxing Listing 6.3: \Beispiele\de\j2sebuch\kap06\Bitmanipulation.java public class Bitmanipulation { public static void main(String args[]) { // 200 - entspricht 00000000 00000000 00000000 11001000 System.out.println(Integer.bitCount(200)); System.out.println(Integer.lowestOneBit(200)); System.out.println(Integer.numberOfLeadingZeros(200)); System.out.println(Integer.numberOfTrailingZeros(200)); System.out.println(Integer.rotateLeft(200, 2)); System.out.println(Integer.rotateRight(200, 2)); System.out.println(Integer.reverse(200)); System.out.println(Integer.reverseBytes(200)); System.out.println(Integer.signum(200)); } }
Die Ergebnisse entstehen wie folgt: bitCount()
= 3 (es gibt dreimal den Wert 1)
lowestOneBit()
= 8 (1000)
numberOfLeadingZeros() numberOfTrailingZeros() rotateLeft() rotateRight() reverse() reverseBytes() signum()
= = = = = = =
Java 5 Programmierhandbuch
24 (es stehen 24 Nullen vor der ersten 1) 3 (am Ende sind 3 Nullen) 00000000 00000000 00000011 00100000 00000000 00000000 00000000 00110010 00010011 00000000 00000000 00000000 11001000 00000000 00000000 00000000 1, da 200 > 0
173
7 7.1
Exceptions Einführung
Entwickeln Sie Anwendungen – sei es mit Java oder einer anderen Programmiersprache –, ist es kaum auszuschließen, dass Fehler auftreten. Fehler sind vermeidbar oder auch unvermeidbar. Vermeidbare Fehler können durch Sie, d.h. durch eine sorgfältige Programmentwicklung, theoretisch ausgeschlossen werden. Tritt allerdings ein Hardwareproblem auf oder wird die Java Virtual Machine nicht korrekt ausgeführt, haben Sie keine Einflussmöglichkeiten. Java verwendet Exceptions, um den Programmierer auf eine besondere Situation im Programmablauf hinzuweisen. Dies heißt insbesondere, dass eine Exception nicht unbedingt auf einem Fehler beruhen muss. Eine Exception (Ausnahme) ist ein Ereignis, das den normalen Programmablauf unterbricht. Über das Schlüsselwort throw (werfen) wird eine Exception ausgelöst. Die Verarbeitung erfolgt in einem catch-Block (auffangen). Der Einsatz von Exceptions beeinflusst die Geschwindigkeit der Programmausführung nur minimal. Auch die Größe der *.class-Datei vergrößert sich nur geringfügig. Exceptions sollten niemals zum Steuern des Kontrollflusses eines Programms eingesetzt werden. Für diese Aufgabe gibt es z.B. die if-Anweisung. Ein weiterer Grund besteht darin, dass das häufige Auslösen von Exceptions sowie deren Behandlung die Ausführungsgeschwindigkeit stark beeinträchtigen kann (zum Teil ist die Ausführung um den Faktor 100 langsamer). Beispiel Über die statische Methode parseInt() der Klasse Integer lassen sich Zeichenketten in Zahlen konvertieren. Ist dies nicht möglich, wird eine Exception vom Typ NumberFormatException ausgelöst. Anweisungen, die Exceptions verursachen, werden in einen try-Block eingeschlossen. Innerhalb des catch-Blocks erfolgt die Behandlung. String text = "a124"; int zahl; try { zahl = Integer.parseInt(text); } catch(NumberFormatException nfEx) { System.out.println("Dies war keine Zahl!"); }
Exceptions werden in Java durch Klassen repräsentiert. Beim Auslösen einer Exception wird ein Objekt vom Typ einer bestimmten Exception-Klasse erzeugt.
Java 5 Programmierhandbuch
175
Einführung
Klassenhierarchie Basisklasse aller Exceptions ist die Klasse Throwable. Sie sollten eigentlich niemals direkt mit dieser Klasse in Berührung kommen, da sie weder beim Erzeugen noch beim Abfangen von Exceptions genaueres über den Grund der Ausnahme aussagt. Hierfür wurden speziellere Exception-Typen definiert. Exceptions vom Typ Error und die davon abgeleiteten Klassen weisen auf einen schweren Fehler bei der Ausführung der JVM hin. Solche Exceptions sollten von Ihnen nicht abgefangen werden, weil sie den Grund für das Auslösen nicht beseitigen können. In der Regel sollte die Standardfehlerbehandlung beibehalten werden, die mit einer Beendigung der Anwendung abgeschlossen wird. Eine weitere Ausführung der Anwendung ist nicht sinnvoll, denn es kann kein konsistenter Zustand der ausführenden Umgebung garantiert werden. Exceptions, die von einer Anwendung behandelt werden sollten, sind von der Klasse Exception direkt oder indirekt abgeleitet. Von den Klassen Error, Exception und RuntimeException gibt es innerhalb der Klassenhierarchie zahlreiche Unterklassen, die in der Abbildung 7.1 nicht dargestellt sind.
Throwable
Error
VirtualMachineError
Exception
IllegalAccessException
RuntimeException
NullPointerException
Abb. 7.1: Klassenhierarchie der Exceptions
Markierte und unmarkierte Exceptions Grundsätzlich besteht in Java die Pflicht, eine Exception zu behandeln oder weiterzugeben. Eine Unterscheidung wird dennoch zwischen markierten (checked) und unmarkierten (unchecked) Exceptions gemacht. Für markierte Exceptions gilt das eben Gesagte. Dies betrifft alle Exceptions, die von der Klasse Exception mit Ausnahme von RuntimeException und deren Unterklassen abgeleitet sind. Werden diese Exceptions nicht behandelt oder weitergegeben, meldet der Compiler einen Fehler und bricht den Übersetzungsvorgang ab. Unmarkierte Exceptions sind vom Typ Error oder RuntimeException sowie deren Unterklassen. Sie müssen nicht abgefangen werden, denn es existiert eine Standardbehandlung. Sie besteht in der Ausgabe einer Meldung und der Beendigung der Anwendung. Ex-
176
7 – Exceptions
ceptions vom Typ Error müssen nicht behandelt werden, da Sie keinen Einfluss auf deren Auftreten haben. Im Falle einer RuntimeException können Sie wiederum durch sorgfältige Programmierung sicherstellen, dass diese nicht auftreten. So können Sie die Division durch Null (ArithmeticException) genauso wie die Übergabe fehlerhafter Parameter an eine Methode (IllegalArgumentException) vermeiden. Beide Exceptions sind Unterklassen von RuntimeException.
7.2
Exceptions behandeln
Java verlangt die Behandlung oder Weitergabe aller auftretenden Exceptions, die nicht vom Typ RuntimeException oder Error sind. Sonst bricht der Compiler mit einer Fehlermeldung ab. Die Fehlermeldung enthält den Namen der Datei, die Zeilennummer sowie den Typ der Exception, die aufgetreten ist, z.B.: Beispiel.java:22: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown new java.io.FileOutputStream("Test"); ^ 1 error
Syntax 쐌 Die Methodenaufrufe, die Exceptions auslösen können, werden in einen try-Block eingeschlossen. Treten keine Exceptions auf, werden alle Anweisungen im try-Block ausgeführt und danach wird die Programmausführung hinter dem letzten zum tryBlock gehörenden catch-Block fortgesetzt. 쐌 In den folgenden catch-Blöcken erfolgt die Behandlung der Exceptions. Anschließend werden die Anweisungen hinter dem letzten catch-Block ausgeführt. 쐌 Es können mehrere catch-Blöcke angegeben werden. Jeder Block behandelt einen bestimmten Exception-Typ. Wichtig ist dabei die verwendete Reihenfolge. Durch die Angabe eines Exception-Typs werden auch alle davon abgeleiteten Typen verarbeitet. 쐌 Einem catch-Block wird ein Parameter vom Exception-Typ übergeben. Über diesen Parameter haben Sie Zugriff auf die Methoden der entsprechenden Exception-Klasse. 쐌 Wird ein catch-Block verlassen, ist diese Exception-Behandlung abgeschlossen. Das Exception-Objekt wird später vom Garbage Collector entsorgt. 쐌 Wurde eine Exception durch keinen catch-Block verarbeitet, wird nach einem umschließenden try-catch-Block gesucht. 쐌 Es kann nicht an die Stelle zurückgesprungen werden, an der die Exception entstanden ist. 쐌 Unbehandelte Exceptions führen zum sofortigen Verlassen der aktuellen Methode.
Java 5 Programmierhandbuch
177
Exceptions behandeln try { // Anweisungen, die Exceptions auslösen können } catch(ExceptionTyp1 ex1) { // Behandlung } catch(ExceptionTyp1 ex2) { // Behandlung } // weitere Anweisungen
Suche nach einem try-catch-Block Tritt eine Exception auf, wird nach einem try-Block gesucht, der die Methode umschließt. Danach wird geprüft, ob die Exception im zugehörigen catch-Block verarbeitet wird. Ist dies nicht der Fall, geht die Suche nach einem umschließenden try-Block weiter. Es wird zuerst innerhalb einer Methode, anschließend anhand des Aufrufstacks gesucht (d.h. nach der Reihenfolge, in der die Methoden aufgerufen wurden). Wird eine Exception nicht verarbeitet, erfolgt die Verarbeitung wie in der Abbildung 7.2 dargestellt. Durch die ständige Weitergabe von Exceptions (siehe später) kann die Anwendung zwar erfolgreich übersetzt werden, beim Auftreten einer Exception wird sie aber letztendlich beendet.
Exception tritt auf
Suche nach umschließendem try-catch-Block
gefunden
nicht gefunden
Standardbehandlung
Abb. 7.2: Verarbeitung von Exceptions
178
Compiler meldet Fehler bzw. Anwendung wird beendet
7 – Exceptions
Methoden der Klasse Throwable Die folgenden Methoden stehen über die Klasse Throwable auch allen anderen Exception-Typen zur Verfügung. Methode
Beschreibung
String getMessage()
Es wird die Nachricht zurückgegeben, die im Konstruktor einer Exception als String-Parameter übergeben wurde. Liegt keine Nachricht vor, wird null geliefert.
void printStackTrace()
Der Inhalt des Aufrufstacks wird auf der Konsole ausgegeben, z.B. java.lang.ArithmeticException: DivDurchNl at de.j2sebuch.Test.test(Test.java:17) at de.j2sebuch.Test.main(Test.java:42) Er umfasst den Exception-Typ mit dem Meldungstext sowie die Methodenaufrufe in umgekehrter Reihenfolge. In Klammern werden der Klassenname und die Zeilennummer angegeben. Wenn Sie eine Exception nicht behandeln, wird intern diese Methode aufgerufen. Bei einer markierten Exception wird zusätzlich das Programm beendet.
String toString()
Es wird der vollständige Name des Exception-Typs und der Inhalt von getMessage(), getrennt durch einen Doppelpunkt und ein Leerzeichen, ausgegeben, z.B. java.lang.ArithmeticException: durch null ist aufgetreten.
Throwable fillInStackTrace()
[IconInfo]
Division
Der Inhalt des Stacks wird mit der aktuellen Aufrufreihenfolge neu initialisiert. Dies ist notwendig, wenn Sie eine Exception erneut auslösen wollen, aber nicht den ursprünglichen Inhalt des Aufrufstacks benötigen.
Es wird noch einmal das Beispiel der Konvertierung einer Zeichenkette in eine Zahl verwendet. Die Zeichenkette ist keine Zahl, deshalb wird eine NumberFormatException ausgelöst und über einen catch-Block abgefangen. Anschließend werden die verschiedenen Methoden der Klasse Throwable zur Ausgabe von Informationen über die Exception aufgerufen.
Java 5 Programmierhandbuch
179
Exceptions weitergeben Listing 7.1: \Beispiele\de\j2sebuch\kap07\Behandlung.java public class Behandlung { public Behandlung() { String strZahl = "KeineZahl"; int zahl = 0; try { zahl = Integer.parseInt(strZahl); } catch(NumberFormatException nfEx) { System.out.println("==== getMessage() ===="); System.out.println(nfEx.getMessage()); System.out.println("==== toString() ===="); System.out.println(nfEx.toString()); System.out.println("==== printStackTrace() ===="); nfEx.printStackTrace(); } } public static void main(String args[]) { new Behandlung(); } }
7.3
Exceptions weitergeben
Abhängig vom Typ der Exception müssen Sie diese behandeln oder weitergeben. Die Verarbeitung einer Exception mit catch-Blöcken wurde bereits erläutert. Wenn Sie eine markierte Exception nicht in einer Methode behandeln wollen, müssen Sie diese weitergeben. In diesem Fall wird im Kopf einer Methode festgelegt, welche Exceptions darin auftreten können. Diese Information ist auch Teil der öffentlichen Schnittstelle einer Methode, d.h., der Aufrufer muss wissen, was ihn möglicherweise beim Verwenden der Methode erwartet. Syntax 쐌 Im Methodenkopf werden hinter dem Schlüsselwort throws durch Kommas getrennt die Exceptions aufgelistet, die innerhalb der Methode ausgelöst werden können, aber darin nicht behandelt werden. 쐌 Diese Angabe ist nur für markierte Exceptions notwendig. 쐌 Wenn Sie das Schlüsselwort throws auch für die Methode main() verwenden, wird eine markierte Exception möglicherweise nicht von der Anwendung behandelt und die Anwendung wird dadurch beendet.
180
7 – Exceptions public methode([parameter]) throws Exc1, Exc2 { }
Die Angabe von throws ist beispielsweise notwendig, wenn Sie selbst eine markierte Exception in einer Methode auslösen. In der API-Dokumentation des JDK erkennen Sie die von einer Methode ausgelösten Exceptions durch die Angabe von throws in der Methodendeklaration. In der Abbildung 7.3 ist dies beispielsweise für die Methode parseInt() der Klasse Integer zu sehen. [IconInfo]
Abb. 7.3: API-Dokumentation einer Methode mit throws-Angabe
[IconInfo]
Eine Datei soll über einen FileInputStream geöffnet werden. Dabei kann eine Exception vom Typ FileNotFoundException auftreten. Die Exception soll in der Methode nicht selbst behandelt werden, deshalb wird sie über throws weitergegeben. Wenn Sie die Exception auch nicht in der Methode main() verarbeiten, ist dort ebenso eine throws-Angabe notwendig.
Listing 7.2: \Beispiele\de\j2sebuch\kap07\Weitergabe.java import java.io.*; public class Weitergabe { public Weitergabe() throws FileNotFoundException { FileInputStream fis = new FileInputStream("nicht da"); } public static void main(String args[]) { try { Weitergabe w = new Weitergabe(); } catch(FileNotFoundException e) { System.out.println("Datei nicht gefunden."); } } }
Java 5 Programmierhandbuch
181
Aufräumen mit finally
7.4
Aufräumen mit finally
Der Garbage Collector der JVM sorgt immer dafür, dass der nicht mehr benötigte Speicher freigegeben wird. Sie müssen diese Aufgabe nicht selbst erledigen. Anders verhält es sich jedoch bei geöffneten Dateien oder Netzwerkverbindungen. Tritt im Programmablauf eine Exception auf, muss eine Möglichkeit geschaffen werden, diese wieder zu schließen oder andere Aufräumarbeiten durchzuführen. Prinzipiell ist dies auch mit try-catch-Blöcken möglich. In diesem Fall müssen aber immer an mehreren Stellen die gleichen Anweisungen eingefügt werden. Das folgende Beispiel ist nur ausreichend, wenn in den catch-Blöcken alle möglichen Exceptions abgefangen werden. try { // Datei öffnen // Datei schliessen } catch(ExceptionTyp1 e) { // Datei schliessen } catch(ExceptionTyp2 e) { // Datei schliessen }
Java bietet mit finally eine bessere Möglichkeit. Fügen Sie am Ende eines try-catchBlocks einen finally-Block an. Die Anweisungen des finally-Blocks werden immer ausgeführt, d.h. unabhängig davon, ob eine Exception aufgetreten ist oder nicht. Syntax 쐌 쐌 쐌 쐌
Die try-Anweisung leitet einen Exception-Block ein. Dem Exception-Block können optional ein oder mehrere catch-Blöcke folgen. Zum Schluss folgt der finally-Block. Der finally-Block wird in jedem Fall ausgeführt, d.h. – wenn keine Exception ausgelöst wurde, – wenn eine Exception ausgelöst wurde, – wenn eine Exception in einem der catch-Blöcke behandelt wurde, – und auch wenn die Exception nicht behandelt wurde, – wenn der try-Block mit break, continue oder return verlassen wurde. 쐌 Nur wenn Sie den try-Block mit System.exit(0) verlassen, wird der finallyBlock nicht ausgeführt.
182
7 – Exceptions try { } catch(ExceptionTyp e) { } finally { }
Normalerweise werden nicht behandelte Exceptions an die umgebenden try-Blöcke weitergegeben. Eine Ausnahme bildet jedoch die Verwendung der Schlüsselwörter throw und return in einem finally-Block. Diese durchtrennen die Exception-Kette, da der try-Block beendet wird. [IconInfo]
try { throw new Exception(); } finally { return; // die Exception wird nicht weitergegeben }
7.5
Exceptions auslösen
Bisher haben Sie nur auf das Eintreten von Exceptions reagiert. Im Folgenden werden Sie selbst Exceptions auslösen. Hierfür haben Sie verschiedene Möglichkeiten. Entweder Sie erzeugen ein neues Exception-Objekt oder Sie benutzen ein bereits existierendes.
7.5.1
Exceptions erzeugen und auslösen
Das Auslösen einer Exception erfolgt in zwei Schritten. Zuerst erzeugen Sie ein Objekt vom Typ der gewünschten Exception. Beachten Sie, dass jeder Exception-Typ über unterschiedliche Konstruktoren verfügen kann. In der Regel besitzt allerdings jeder ExceptionTyp einen Standardkonstruktor sowie einen Konstruktor, der einen Meldungstext entgegennimmt. Nach dem Erzeugen des Exception-Objekts muss es mit dem Schlüsselwort throw ausgelöst (geworfen) werden. Syntax 쐌 Definieren Sie ein neues Exception-Objekt. 쐌 Lösen Sie die Exception durch die Angabe von throw, gefolgt vom Exception-Objekt, aus.
Java 5 Programmierhandbuch
183
Exceptions auslösen
쐌 Sie können beide Schritte zusammenfassen, wenn Sie das Exception-Objekt nicht weiter benötigen. ExceptionTyp e = new ExceptionTyp(...); throw e; // oder kurz throw new ExceptionTyp();
[IconInfo]
In dem Moment, wo Sie ein Exception-Objekt erzeugen, werden Informationen zum Stack-Inhalt (welche Methode wurde mit welchen Parametern in welcher Reihenfolge aufgerufen) in das Objekt aufgenommen. Dies kann problematisch sein, wenn Sie das Exception-Objekt nicht an der Stelle im Programm erzeugen, an der Sie es über throw auslösen. Wenn Sie beispielsweise die gleiche Exception an verschiedenen Stellen auslösen wollen, wäre die Stack-Information in jedem Fall die gleiche. Dies kann aber die Fehlersuche erschweren.
Wird der Methode ausgabe() als Parameterwert null übergeben, lösen Sie über throw eine IllegalArgumentException aus. Da diese nicht behandelt wird, erzeugt die Standardbehandlung eine Ausgabe auf der Konsole. [IconInfo]
Listing 7.3: \Beispiele\de\j2sebuch\kap07\Ausloeser.java public class Ausloeser { private void ausgabe(String s) { if(s == null) throw new IllegalArgumentException("Keine Zeichenkette."); } public Ausloeser() { ausgabe(null); } public static void main(String args[]) { new Ausloeser(); } }
184
7 – Exceptions
7.5.2
Exceptions erneut auslösen
In einem catch-Block wird normalerweise eine Exception behandelt und damit beseitigt. Es kann allerdings in verschachtelten Methodenaufrufen notwendig sein, dass an mehreren Stellen (also den catch-Blöcken) auf die Exception reagiert wird. Hierfür wird eine abgefangene Exception erneut ausgelöst. Geben Sie dazu innerhalb eines catch-Blocks nach dem Schlüsselwort throw das Exception-Objekt an. catch(Exception e) { // Behandlung throw e; // erneutes Auslösen }
Wenn Sie auf diese Weise eine Exception erneut auslösen, bleibt der Inhalt des Stacks unverändert, d.h., die Methode printStackTrace() liefert denselben Inhalt. Dies kann dann unerwünscht sein, wenn Sie stattdessen die Position beim erneuten Auslösen verwenden möchten (Zeilennummer, Methode etc.). In diesem Fall benutzen Sie die Methode fillInStackTrace() des Exception-Objekts. catch(Exception e) { throw e.fillInStackTrace(); // => neuer StackTrace ... }
[IconInfo]
Das folgende Beispiel demonstriert die Verwendung der Methoden printStackTrace() und fillInStackTrace(). Im Konstruktor Ausloeser2 erzeugt der Aufruf der Methode ausgabe() eine Exception. Sie wird abgefangen und der Inhalt des Aufrufstacks wird ausgegeben. Danach wird die Exception erneut ausgelöst, jedoch mit einem neuen Inhalt des Aufrufstacks. Die nachfolgende Ausgabe von printStackTrace() ist damit eine andere. Würden Sie stattdessen throw iaEx; zum erneuten Auslösen angeben, wären beide Ausgaben von printStackTrace() identisch.
Listing 7.4: \Beispiele\de\j2sebuch\kap07\Ausloeser2.java public class Ausloeser2 { private void ausgabe(String s) { if(s == null) throw new IllegalArgumentException("Keine Zeichenkette."); }
Java 5 Programmierhandbuch
185
Exceptions auslösen Listing 7.4: \Beispiele\de\j2sebuch\kap07\Ausloeser2.java (Forts.) public Ausloeser2() { try { ausgabe(null); } catch(IllegalArgumentException iaEx) { iaEx.printStackTrace(); throw iaEx.fillInStackTrace(); } } public static void main(String args[]) { try { new Ausloeser2(); } catch(IllegalArgumentException iaEx) { iaEx.printStackTrace(); } } }
7.5.3
Exception-Ketten
Bei der Arbeit mit Exceptions stellt sich ab und zu die Aufgabe, innerhalb eines catchBlocks eine weitere Exception auszulösen. Es sollen dabei aber keine Informationen über die behandelte Exception verloren gehen. Seit dem JDK 1.4 werden deshalb verkettete Exceptions (chained exceptions) unterstützt. Die Klasse Throwable wurde um zwei weitere Konstruktoren erweitert, denen ein Throwable-Objekt (die zu behandelnde Exception) übergeben wird. public Throwable(Throwable ex) public Throwable(String nachricht, Throwable ex)
Weiterhin wurden Throwable zwei Methoden hinzugefügt. Über die Methode getCause() ermitteln Sie ein Exception-Objekt, welches in einer anderen Exception verpackt ist. Die Methode initCause() wird benötigt, wenn der Exception-Typ nicht die beiden erweiterten Konstruktoren zur Verfügung stellt (z.B. ArithmeticException). Einer Exception wird dann über diese Methode die zu verkettende Exception zugewiesen. Throwable getCause() Throwable initCause(Throwable exception)
186
7 – Exceptions
[IconInfo]
Im Konstruktor ExceptionKetten wird eine ArithmeticException ausgelöst. Im darauffolgenden catch-Block wird eine weitere Exception ausgelöst, der jedoch zusätzlich das behandelte Exception-Objekt als Parameter übergeben wird. In der Behandlung dieser Exception wird ein ArithmeticException-Objekt erzeugt. Da diese Exception keinen erweiterten Konstruktor besitzt, wird ihr über die Methode initCause() die zu verkettende Exception zugewiesen. Es liegt jetzt eine ExceptionKette aus drei Exceptions vor. Innerhalb der main()-Methode werden über eine do-while-Schleife die inneren Exceptions über die Methode getCause() ausgepackt und die Nachrichten ausgegeben.
Listing 7.5: \Beispiele\de\j2sebuch\kap07\ExceptionKetten.java public class ExceptionKetten { public ExceptionKetten() { try { throw new ArithmeticException("1. Rechenfehler"); } catch(ArithmeticException arEx) { try { throw new IllegalArgumentException("Parameterfehler",arEx); } catch(IllegalArgumentException iaEx) { ArithmeticException arEx2 = new ArithmeticException("2. Rechenfehler"); arEx2.initCause(iaEx); throw arEx2; } } } public static void main(String args[]) { try { ExceptionKetten ek = new ExceptionKetten(); } catch(Exception e) { do { System.out.println(e.getMessage());
Java 5 Programmierhandbuch
187
Eigene Exceptions verwenden Listing 7.5: \Beispiele\de\j2sebuch\kap07\ExceptionKetten.java e = (Exception)e.getCause(); } while(e.getCause() != null); } System.out.println("Alle behandelt."); } }
7.6
Eigene Exceptions verwenden
Beim Auslösen von Exceptions sind Sie nicht nur auf die vordefinierten Exception-Klassen beschränkt. Wenn Sie eigene Methoden, Klassen oder ganze Klassenbibliotheken entwickeln, kann die Verwendung neuer, spezialisierter Exception-Typen notwendig werden. Auf diese Weise können Sie gezielter auf das aufgetretene Problem hinweisen. Wenn Sie beispielsweise eine Klasse definieren, die Berechnungen mit Temperaturen durchführt (Durchschnitt, Minimum, Maximum), sollen die Werte nur aus dem Bereich von –50° bis +60° zugelassen werden. Werden diese Bedingungen verletzt, können spezialisierte Exceptions ausgelöst werden. Eine neue Exception wird von einer bereits vorhandenen Exception-Klasse abgeleitet. Jetzt stellt sich natürlich die Frage, von welcher Klasse abgeleitet werden soll. Die Klasse Throwable als Basisklasse aller Exceptions ist sicher nicht geeignet, da sie zu allgemein ist. Die Klasse Error sowie alle ihre Unterklassen sind ebenfalls keine gute Wahl, weil diese Exceptions einen kritischen Zustand der JVM kennzeichnen und zu einer Programmbeendigung führen sollten. Damit bleibt noch die Klasse Exception. Leiten Sie von RuntimeException oder einer ihrer Unterklassen ab, wenn es sich um einen vermeidbaren Fehler handelt. Zum Beispiel kann der Programmierer sicherstellen, dass der Wertebereich der Temperaturen eingehalten wird. Für alle anderen möglichen Fehlerquellen (z.B. wenn der Anwender die Werte noch an anderer Stelle eingeben kann) verwenden Sie als Basisklasse Ihrer Exception die Klasse Exception oder eine ihrer Unterklassen. Syntax 쐌 Die neue Klasse wird von der Klasse Exception oder einer ihrer Unterklassen abgeleitet. Der Name der neuen Klasse sollte sich aus einer Beschreibung der abgefangenen Ausnahme sowie dem Suffix »Exception« zusammensetzen. Damit ist eine Klasse sofort als Exception-Klasse erkennbar, z.B. TemperaturBereichException. 쐌 Der Klasse werden zwei Konstruktoren hinzugefügt, ein parameterloser Standardkonstruktor sowie ein Konstruktor, der einen Meldungstext entgegennimmt. In allen Konstruktoren sollten die Konstruktoren der Basisklasse über super() bzw. super(nachricht) aufgerufen werden. Dadurch wird der Aufrufstack gespeichert und im Falle der Übergabe einer Nachricht die Rückgabe von getMessage() festgelegt. 쐌 Sie können der Klasse beliebige weitere Konstruktoren und Methoden hinzufügen, z.B. um die Fehlerquelle besser zu spezifizieren.
188
7 – Exceptions
Beispiel class MeineException extends ExceptionKlasse { public MeineException() { super(); } public MeineException(String nachricht) { super(nachricht); } // weitere Methoden }
[IconInfo]
Die folgende Exception-Klasse soll zum Erzeugen von Exceptions verwendet werden, wenn bei der Definition einer Zahlenfolge ein Eingabefehler unterlaufen ist. Zusätzlich zu den beiden Standardkonstruktoren wird ein weiterer Konstruktor definiert, dem die notwendigen Ober- und Untergrenzen der Werte der Zahlenfolge übergeben werden. Diese können im Fehlerfall ausgegeben werden.
Listing 7.6: \Beispiele\de\j2sebuch\kap07\ZahlenfolgeException.java public class ZahlenfolgeException extends Exception { public ZahlenfolgeException() { super(); } public ZahlenfolgeException(String nachricht) { super(); } public ZahlenfolgeException(String nachricht, int untereGrenze, int obereGrenze) { super(); } }
Java 5 Programmierhandbuch
189
8 8.1
Assertions Einführung
Beim Aufruf einer Methode wird häufig von bestimmten Annahmen ausgegangen, die zur korrekten Ausführung erfüllt sein müssen: den Vorbedingungen. Am Ende einer Methode wird ein bestimmtes Ergebnis erwartet: die Nachbedingung. Beim Softwareentwurf können diese Bedingungen in der Spezifikation einer Methode angegeben werden. Eine weitere Möglichkeit besteht in der Definition von Invarianten. Die Invariante einer Klasse, welche die Temperatur von Flüssigkeiten verwaltet, könnte beispielsweise darin liegen, dass der absolute Nullpunkt nie unterschritten wird. Über Assertions (Behauptungen), die erstmals mit dem JDK 1.4 eingeführt wurden, können Sie die Erfüllung dieser Bedingungen automatisiert prüfen. Dazu wurde das neue Schlüsselwort assert bereitgestellt.
[IconInfo]
Der Test im Ausdruck der folgenden Assertion wird zu false ausgewertet, denn die Zahl 10 ist nicht größer als die Zahl 11. Im ersten Fall wird eine Exception vom Typ AssertionError ausgelöst und ihr der Text »Meldungstext« übergeben. Da Sie die zweite Assertion nicht über ein Exceptionhandling abfangen, wird das Programm danach beendet. Zur Auswertung der Assertions zur Laufzeit muss der Interpreter mit der Option -ea aufgerufen werden, z.B. java –ea Annahme1
Listing 8.1: \Beispiele\de\j2sebuch\kap08\Annahme1.java public class Annahme1 { public static void main(String args[]) { try { assert 10 > 11: "Meldungstext"; } catch(AssertionError e) { System.out.println(e.getMessage()); } assert 10 > 11; System.out.println("Diese Anweisung wird nicht erreicht."); } }
Java 5 Programmierhandbuch
191
Informationen zum Einsatz von Assertions
Syntax 쐌 Assertions werden mit dem Schlüsselwort assert eingeleitet. 쐌 Es folgt ein Bedingungsausdruck, der den Rückgabetyp boolean liefert. Wenn der Wert des Ausdrucks zu true ausgewertet wird, erfolgt keine weitere Aktion. Liefert der Ausdruck dagegen den Wert false, löst dies eine Exception vom Typ java.lang. AssertionError aus. 쐌 Über einen optionalen Fehlermeldungsausdruck, welcher durch einen Doppelpunkt getrennt an den ersten Ausdruck angefügt wird, können Sie einen eigenen Meldungstext für die Exception festlegen. Dessen Inhalt kann über die Methode getMessage() des Exception-Objekts ausgewertet werden. Der Rückgabetyp des Ausdrucks ist String. Lassen Sie den zweiten Ausdruck weg, wird keine weitere Information beim Auftreten der Exception ausgegeben. assert Bedingungsausdruck; assert Bedingungsausdruck: Fehlermeldungsausdruck;
[IconInfo]
8.2 8.2.1
Grundsätzlich können Sie Assertions auch durch if-Anweisungen nachbilden. Allerdings entstehen dadurch mehrere Nachteile. Der benötigte Code ist umfangreicher und lässt auf den ersten Blick nicht erkennen, dass es sich um die Prüfung einer Behauptung handelt. Letztendlich können Sie die Verwendung von Assertions einfacher (de)aktivieren.
Informationen zum Einsatz von Assertions Seiteneffekte
Assertions dienen während der Programmentwicklung zur Überprüfung, ob die verwendeten Methoden mit den korrekten Datenwerten arbeiten. Bei der Ausführung beim Anwender sollten Sie Assertions in der Regel nicht einsetzen. Der korrekte Programmablauf sollte mit anderen Mitteln sichergestellt werden. Aus diesem Grund ist es notwendig, dass die Verwendung von Assertions ein- und ausgeschaltet werden kann. Diese Möglichkeit führt zu dem folgenden wichtigen Designhinweis. Assertions sollten niemals Seiteneffekte besitzen, welche die normale Programmausführung beeinflussen. Ein Seiteneffekt wäre der Aufruf einer Methode, die bei der Deaktivierung von Assertions nicht aufgerufen wird. Im folgenden Beispiel wird ein Seiteneffekt umgangen, indem der Aufruf der Methode und die Prüfung des Rückgabewerts in zwei Anweisungen aufgesplittet wurden. Der Methodenaufruf wird dabei in jedem Fall ausgeführt, unabhängig davon, ob Assertions (de)aktiviert sind. boolean ergebnis = methode(); assert ergebnis;
192
8 – Assertions
statt assert methode();
Eine Ausnahme bilden Anweisungen, die Werte und Zustände ändern, die nur innerhalb von Assertions eingesetzt werden. Dies kann z.B. eine Variable sein, welche die Aufrufe einer Assertion zählt oder eine Methode, die eine Information zur Assertion ausgibt. Beispiel int couter; ... assert counter++ > 100: "Diese Assertion wird zu oft " + "aufgerufen";
8.2.2
Einsatzgebiete
Assertions dienen der Überprüfung von Vor- und Nachbedingungen sowie Invarianten. Im Falle von öffentlichen Methoden (public) sollten Sie Assertions nicht einsetzen. Da öffentliche Methoden die Schnittstelle einer Klasse darstellen, muss die Überprüfung der korrekten Funktionsweise in jedem Fall in der Methode implementiert werden. So sollten fehlerhafte Parameter beispielsweise zu einer IllegalArgumentException, IllegalStateException, IndexOutOfBoundsException oder NullPointerException führen. Damit ist sichergestellt, dass die Methode auch bei ausgeschalteten Assertions korrekt arbeitet. Insbesondere müssen Benutzereingaben überprüft werden, weil sie fehlerhafte Daten an eine Anwendung übergeben können. Beispiel Die folgende Methode berechnet den größten gemeinsamen Teiler zweier Zahlen. Werden Zahlen übergeben, die nicht größer als Null sind, wird eine Exception ausgelöst, denn es handelt sich um eine öffentliche Methode. Sie muss in jedem Fall eine korrekte Funktionsweise, auch bei fehlerhaften Parametern, sichern. public int berechneggT(int zahl1, int zahl2) { if((zahl1 > 0) && (zahl2 > 0)) throw new IllegalArgumentExcection();
Beim Einsatz von Assertions wird die Ausführungsgeschwindigkeit der Anwendung nicht sonderlich beeinflusst. Werden allerdings durch die eingesetzten Assertions sehr häufig Exceptions ausgelöst, die später behandelt werden, kann sich die Programmausführung merklich verlangsamen.
Java 5 Programmierhandbuch
193
Informationen zum Einsatz von Assertions
Vorbedingungen In geschützten Methoden (private, protected) können Sie auf eine integrierte Überprüfung der übergebenen Parameterwerte und das Auslösen von Exceptions im Fehlerfall verzichten. Theoretisch können Sie durch eine korrekte Programmierung sicherstellen, dass diese Methoden immer mit richtigen Werten aufgerufen werden. Während der Entwicklung und beim Test können jedoch Parameterüberprüfungen oder Zustandstests über Assertions erfolgen. Vorbedingungen einer Methode werden vor der ersten Anweisung der Methode geprüft. private int kalkulieren(int wert1, int wert2) { assert wert1 >= wert2: "Der 1. Wert ist kleiner als der 2.!"; // weitere Anweisungen ... }
Nachbedingungen Vor jeder return-Anweisung, über die eine Methode verlassen werden kann, bzw. vor der letzten Anweisung einer Methode erfolgt die Überprüfung von Nachbedingungen. So können Sie z.B. prüfen, ob sich der berechnete Wert innerhalb eines definierten Intervalls befindet. In der folgenden Methode soll beispielsweise ein Feld geleert werden. private void leereFeld(ArrayList al) { // Operationen zum Leeren eines Feldes assert al.isEmpty(): "Das Feld ist nicht leer"; }
Invarianten und Kontrollflusssteuerung Es gibt verschiedene Formen von Invarianten. Bereits vorgestellt wurde die Klasseninvariante. Dabei ändert sich ein bestimmter Zustand der Klasse nicht bzw. befindet sich immer in einem bestimmten Intervall. Eine weitere Form besteht in der Prüfung mit if-Anweisungen, ob ein bestimmter Zweig ausgeführt wird, der eigentlich nie zur Ausführung kommen darf. Innerhalb einer switch-Anweisung kann über den default-Zweig getestet werden, ob die verwendeten case-Zweige alle möglichen Werte abdecken. if(wert > 0) ... else assert wert > 0: "Der Wert muss grösser als 0 sein !";
oder
194
8 – Assertions switch(wert) { case 1: ... case 2: ... default: assert false: "Keine anderen Werte erlaubt";
Wenn Sie sehr komplexe if-else-Konstrukte oder andere Anweisungen nutzen und deren Korrektheit letztendlich mittels Assertions sicherstellen möchten, sollten Sie besser darüber nachdenken, den Programmcode zu vereinfachen. [IconInfo]
Dieses Beispiel nutzt Vor- und Nachbedingungen, um die korrekte Funktionsweise der Methode berechneggT() (größter gemeinsamer Teiler) sicherzustellen. Beide Zahlen müssen bei der Übergabe an die Methode größer als Null sein. Welche der beiden Zahlen größer als die andere ist, spielt für den Algorithmus keine Rolle. Als Nachbedingung wird geprüft, ob sich beide Zahlen ohne Rest durch den ggT teilen lassen. Die Prüfung ist dabei etwas schwächer. Sie stellt nicht sicher, dass die Zahl tatsächlich die kleinste ist, die beide Zahlen ohne Rest teilt.
[IconInfo]
Listing 8.2: \Beispiele\de\j2sebuch\kap08\Annahme2.java public class Annahme2 { public Annahme2() { System.out.println("Der ggT von 12 und 8 ist: " + berechneggT(12,8)); } private int berechneggT(int zahl1, int zahl2) { assert(zahl1 > 0) && (zahl2 > 0); int rest = 0; int z1 = zahl1; int z2 = zahl2; while(z2 != 0) { rest = z1 % z2; z1 = z2; z2 = rest; // Ist der Rest 0, ist z1 der ggT } assert((zahl1 % z1 == 0) && (zahl2 % z1 == 0)); return z1; }
Java 5 Programmierhandbuch
195
Aktivieren von Assertions Listing 8.2: \Beispiele\de\j2sebuch\kap08\Annahme2.java (Forts.) public static void main(String args[]) { new Annahme2(); } }
8.3 8.3.1
Aktivieren von Assertions Übersetzung
Vor dem JDK 1.4 konnte assert als normaler Bezeichner eines Programmelements benutzt werden. Dies änderte sich mit der Einführung von assert als neues Schlüsselwort. Damit der Compiler beim Übersetzungsvorgang assert als Schlüsselwort interpretiert, muss er wissen, dass der vorliegende SourceCode das JDK 1.4 und aufwärts verwendet. Während hierfür im JDK 1.4 noch der Compilerschalter -source 1.4 angegeben werden musste, ist dies ab dem JDK 5.0 nicht mehr notwendig, weil als SourceCode standardmäßig die Version 5 bzw. 1.5 angenommen wird. // der SourceCode verwendet das JDK 5.0 // assert wird als Schlüsselwort interpretiert javac *.java // der Source-Code verwendet das JDK 1.3 // assert ist als Bezeichner zugelassen javac -source 1.3 *.java // hier werden aber Warnungen ausgegeben
8.3.2
Ausführen
Obwohl Assertions ab dem JDK 1.4 unterstützt werden, ist deren Unterstützung zur Laufzeit einer Anwendung standardmäßig ausgeschaltet. Trifft eine Anwendung auf eine Assertion, wird der betreffende Programmcode nicht ausgeführt. Der Interpreter verfügt deshalb über mehrere Optionen, über die Sie Assertions per Klasse oder Package, inklusive Subpackages, aktivieren können. Option
Beschreibung
-ea -enableassertions
Die erste Option ist die Kurzform der zweiten Option. Assertions werden für alle Klassen eingeschaltet, ausgenommen den Systemklassen.
-da -disableassertions
Assertions werden deaktiviert. Dies ist die Standardeinstellung.
-esa -das
Über diese Schalter aktivieren Sie Assertions für die Systemklassen (Langform enablesystemassertions, disablesystemassertions).
196
8 – Assertions Option
Beschreibung
-ea:Klassenname -da:Klassenname
Sie können Assertions auch für einzelne Klassen (de)aktivieren. Geben Sie dazu nach dem entsprechenden Parameter durch einen Doppelpunkt getrennt den Klassennamen an.
-ea:Package -da:Package
Wenn Sie Assertions für ein bestimmtes Package (de)aktivieren, sind immer auch alle Subpackages davon betroffen.
-ea:... -da:...
Durch die Angabe von drei Punkten hinter dem Doppelpunkt wird das unbenannte Package des aktuellen Arbeitsverzeichnisses benutzt.
Beispiele Die einzelnen Optionen können auch gemischt und mehrfach verwendet werden. So können Assertions in bestimmten Klassen und/oder Packages eingeschaltet, in anderen dagegen ausgeschaltet werden. java -ea de.j2sebuch.kap08.Annahme1 java -ea:de.j2sebuch.kap08.Annahme1 de.j2sebuch.kap08.Annahme1 java -ea:de.j2sebuch -da:java.util de.j2sebuch.kap08.Annahme1
8.3.3
Verhindern der Einbindung in die *.class-Datei
Unabhängig davon, ob Sie Assertions aktivieren oder deaktivieren, werden sie mit in die *.class-Datei aufgenommen. Wenn die ausgelieferte Anwendung für den Kunden jedoch keine Assertions benötigt und aus Geschwindigkeits- und Platzgründen jede Optimierungsmöglichkeit ausgeschöpft werden soll, müssen Sie schon bei der Kompilierung die Aufnahme des Codes in die *.class-Datei verhindern. Es steht jedoch dazu kein Compilerschalter zur Verfügung. Allerdings können Sie auf die Möglichkeit der bedingten Kompilierung zurückgreifen. Trifft der Compiler auf eine if-Anweisung, deren Anweisungen niemals ausgeführt werden, nimmt er diese nicht mit in die *.class-Datei auf. Definieren Sie eine Variable, die festlegt, ob die Assertions aufgenommen werden sollen oder nicht. Fügen Sie vor jeder Verwendung einer Assertion eine if-Anweisung ein, die den Wert der Variablen prüft. private final boolean assertAktiv = false; // oder true ... if(assertAktiv) assert ...;
Java 5 Programmierhandbuch
197
Aktivieren von Assertions
8.3.4
Sicherstellung der Aktivierung
Wenn Sie sichergehen möchten, dass Assertions bei der Ausführung einer Anwendung aktiviert sind, können Sie das folgende Codefragment einsetzen. Beim ersten Laden der Klasse wird zuerst deren statischer Initialisierungsblock ausgeführt. Sind keine Assertions aktiviert, wird die Anweisung assert assertsAktiv = true; nicht ausgeführt. Es wird beim folgenden Prüfen des Werts von assertsAktiv über die if-Anweisung die Exception ausgelöst, da der Wert immer noch false ist. Sind Assertions dagegen aktiviert, wird die Anweisung assert assertsAktiv = true; ausgeführt und zu true ausgewertet, so dass die Assertion keine Exception auslöst. static { boolean assertsEnabled = false; assert assertsEnabled = true; if(!assertsEnabled) throw new RuntimeException("Assertions nicht aktiviert!"); }
198
9
Zeichenkettenverarbeitung
Eine Zeichenkette (ein String) ist eine Folge von einzelnen Zeichen. In Java bestehen Zeichenketten aus Unicode-Zeichen. Sie benötigen den doppelten Speicherplatz (2 Byte) wie ASCII-Zeichen. Für den Programmierer hat dieser Umstand aber keine Auswirkungen, weil der Unicode-Zeichensatz zum ASCII-Code kompatibel ist. Zeichenketten werden in doppelten Hochkommata angegeben, wenn sie als Literale verwendet werden. Als Einsatzgebiete seien besonders der Ein- und Ausgabedialog mit den Benutzern und die Verarbeitung von Zeichenfolgen, z.B. als regulärer Ausdruck, genannt. Es gibt prinzipiell zwei Möglichkeiten, in einem Java-Programm mit Zeichenketten zu arbeiten. 쐌 Ein String-Objekt speichert eine unveränderliche Zeichenkette. String-Objekte können aber nicht nur wie Konstanten benutzt werden. Die Klasse String kennt viele Methoden für den Umgang mit Zeichenketten, beispielsweise zum Vergleichen, zum Suchen und zur Ermittlung von Teilen einer Zeichenkette. 쐌 Eine Zeichenkette, die in einem StringBuilder- oder StringBuffer-Objekt gespeichert wird, kann verändert und erweitert werden. Die Klassen StringBuilder und StringBuffer unterstützen besonders die Änderung von Zeichenketten.
9.1
Mit String-Objekten arbeiten
Zeichenketten werden in Objekten gespeichert. Sie bestehen nicht, wie in anderen Programmiersprachen, aus einem Array von Zeichen und sind auch nicht, wie in C, nullterminiert. Bevor eine Zeichenkette bearbeitet werden kann, muss ein String-Objekt erzeugt werden.
9.1.1
Ein String-Objekt erzeugen
String-Objekte können, im Gegensatz zu »normalen« Objekten, auch ohne die Verwendung des Operators new erzeugt werden. Bei der Angabe eines Zeichenkettenliterals wird automatisch ein String-Objekt erstellt, welches der entsprechenden Variablen zugewiesen wird. String s1; s1 = "der erste String"; String s2 = "ein neuer String";
Die Klasse String besitzt aber auch eine große Anzahl von Konstruktoren, um beispielsweise eine leere Zeichenkette anzulegen, einen String zu kopieren oder einen String aus einem Array von Zeichen oder Bytes zu bilden:
Java 5 Programmierhandbuch
199
Mit String-Objekten arbeiten String() String(String original) String(byte[] bytes) String(char[] value)
// // // //
Leer-String Kopie des original-String String aus byte-Array String aus char-Array
Sehen Sie sich die folgenden Beispiele an: // String s1 wird kopiert und s11 zugewiesen String s1Kopie = new String(s1); // Ein Array von Zeichen wird in einen String umgewandelt char[] zeichen = {'H', 'a', 'l', 'l', 'o'}; String s3 = new String(zeichen);
Enthält ein String keine Zeichen, wird er als Leer-String oder Null-String bezeichnet. Er kann durch den entsprechenden Konstruktor oder durch die Angabe von zwei aufeinander folgenden doppelten Hochkommata gebildet werden. String leer1 = new String(); String leer = "";
9.1.2
Länge eines Strings und Position einzelner Zeichen
Ein String-Objekt hat eine feste Länge. Die Anzahl dieser Zeichen können Sie mithilfe der Methode length() abfragen. int length()
In den Methoden der Klasse String wird häufig die Position eines einzelnen Zeichens verwendet. Die Stellung eines Zeichens im String wird über den Index angegeben. Das erste Zeichen hat den Index 0, das letzte Zeichen den Index String-Länge - 1.
H
a
l
l
o
Abb. 9.1: Indizes der Zeichen im String
Die Methoden indexOf() und charAt() sind typische Beispiele. Sie ermitteln den Index eines bestimmten Zeichens oder den Beginn einer Teilzeichenkette bzw. das Zeichen, das sich an einer bestimmten Position befindet.
200
9 – Zeichenkettenverarbeitung Methode
Beschreibung
int indexOf(int ch)
Die Methode liefert den Index, an dem das Zeichen erstmalig im String vorkommt.
int indexOf(String str)
Analog gibt die Methode den Index zurück, an dem das erste Zeichen der Zeichenkette str zum ersten Mal im String erscheint.
char charAt(int index)
Über die Methode charAt können Sie das Zeichen an der Position index ermitteln.
Beispiel String s = "das Java-Programm"; // Index ermitteln, an dem a das erste mal vorkommt => 1 System.out.println("Index des ersten a " + s.indexOf("a")); // Index ermitteln, an dem a das zweite mal vorkommt => 5 System.out.println("Index des zweiten a :" + s.indexOf("a", s.indexOf("a") + 1)); // das zehnte Zeichen des Strings ermitteln => P System.out.println("Zeichen an Position 9: " + s.charAt(9));
[IconInfo]
9.1.3
Geben Sie als Parameter einer Methode einen Index an, der für diesen String nicht existiert, wird eine Exception vom Typ StringIndexOutOfBoundsException ausgelöst. Ist es zum Zeitpunkt des Programmentwurfs noch nicht vorhersehbar, ob dieser Index für den String vorhanden ist, sollten Sie vorher eine entsprechende Überprüfung durchführen oder diese Exception abfangen.
Strings verketten
Zwei Strings lassen sich auf einfache Weise mit dem Verkettungsoperator + verbinden. String anrede = "Guten Tag, "; String name = "Herr Hase"; String anredeMitName = anrede + name; // = "Guten Tag, Herr Hase"
Wird der Verkettungsoperator auf einen String und ein Objekt einer anderen Klasse oder einen anderen Datentyp angewendet, erfolgt vor der Verkettung eine Umwandlung in einen String. Bei Objekten wird die Methode toString() benutzt. String s = "Nummer"; int i = 7; String s7 = s + " " + i;
Java 5 Programmierhandbuch
// => s7 = "Nummer 7"
201
Mit String-Objekten arbeiten
Diese Anwendung des Verkettungsoperators können Sie bei der Umwandlung von Zahlen oder Zeichen in einen String einsetzen. s7 = i; s7 = "" + i;
9.1.4
// erzeugt einen Fehler, da i vom Typ Integer ist // i wird in einen String umgewandelt und mit dem // leeren String verknüpft
String-Objekte ändern
Am Anfang wurde davon ausgegangen, dass in einem String-Objekt eine unveränderliche Zeichenkette gespeichert wird. Was passiert aber, wenn einem String-Objekt eine neue Zeichenkette zugewiesen wird? Betrachten Sie folgendes Beispiel: String s = "abc"; s = s + "def";
// s = "abcdef"
String-Objekte sind nicht dynamisch. Bei der Initialisierung werden Inhalt und Länge festgelegt und können nicht mehr geändert werden. Wird in einer Anweisung eine Operation mit einem String-Objekt (hier s) ausgeführt, wird dieses vorher kopiert und die Operation an der Kopie durchgeführt. Der Variablen s wird anschließend die geänderte Kopie zugewiesen. Das ursprüngliche Objekt wird später vom Garbage Collector beseitigt. Diese Technik erweckt aber den Eindruck, dass wir die Zeichenketten selbst geändert haben.
9.1.5
Strings vergleichen
Vergleich ganzer String-Objekte Für eine korrekte Anwendung von Vergleichsmethoden und -operatoren ist es wichtig zu wissen, wie String-Objekte in Java gespeichert und verwaltet werden. 쐌 Für Strings, deren Inhalt zur Zeit der Kompilierung bekannt ist, wird ein String-Pool angelegt. Wird mehreren String-Objekten die gleiche Zeichenkette zugewiesen, verweisen all diese String-Objekte auf dieselbe Zeichenkette, sie besitzen also die gleiche Referenz. 쐌 Wird eine Zeichenkette dynamisch zur Laufzeit erzeugt, legt Java immer ein neues String-Objekt an. Methode/Operator
Beschreibung
boolean equals( Object obj)
Die Methode equals() dient zum Vergleichen des Inhalts zweier String-Objekte. Das Ergebnis ist true, wenn die Zeichenketten übereinstimmen, sonst false.
boolean equalsIgnoreCase( String str)
Diese Methode arbeitet wie equals(), es wird aber beim Vergleich die Groß- und Kleinschreibung vernachlässigt.
202
9 – Zeichenkettenverarbeitung Methode/Operator
Beschreibung
int compareTo( String str)
Verwenden Sie die Methode compareTo(), um String-Objekte lexikalisch zu vergleichen. Bei dieser Art des Vergleichs wird ermittelt, ob ein Objekt größer, gleich oder kleiner als das Vergleichsobjekt ist. Es werden die einzelnen Zeichen der beiden Strings paarweise betrachtet. Beispielsweise ist der String »ABC« größer als »AAB«. Die Methode liefert einen Integer-Wert größer als Null zurück. Ist die erste Zeichenkette kleiner als die zweite, wird ein Wert kleiner Null zurückgegeben. Der Wert 0 ergibt sich bei Gleichheit.
int compareToIgnoreCase( String str)
Diese Methode arbeitet wie compareTo(), es wird aber die Groß- und Kleinschreibung vernachlässigt.
String s1 = ... String s2 = ...
Benutzen Sie den Operator == zum Vergleich zweier String-Objekte, wird nicht deren Inhalt, sondern deren Referenz analysiert. Verweisen zwei String-Variablen auf die gleiche Zeichenkette, liefert die Operation das Ergebnis true. Anderenfalls ist das Ergebnis false, auch wenn der Inhalt der Zeichenketten identisch ist, dafür aber zwei verschiedene Objekte angelegt wurden. Dies ist bei dynamisch erzeugten Strings der Fall.
if(s1 == s2) ...
Das folgende Beispielprogramm zeigt die Anwendung der Vergleichsmethoden equals() und compareTo(). Außerdem wird der Unterschied zwischen equals() und dem Vergleichsoperator == demonstriert. [IconInfo]
Listing 9.1: \Beispiele\de\j2sebuch\Kap09\StringVergleiche.java class StringVergleiche { public static void main(String[] args) { String a = "abc"; String b = "abc"; String c = new String("abc"); String d = "aab"; String e = "def";
Java 5 Programmierhandbuch
203
Mit String-Objekten arbeiten Listing 9.1: \Beispiele\de\j2sebuch\Kap09\StringVergleiche.java (Forts.) /* Vergleich der Referenzen der String-Objekte */ System.out.println("a == b ? " + (a == b)); // --> true, da a und b auf das selbe String-Objekt zeigen System.out.println("a == c ? " + (a == c)); // --> false, da a und c auf verschiedene String-Objekte // zeigen; c wurde dynamisch erzeugt /* Vergleich der Inhalte der String-Objekte */ System.out.println("a.equals(b) ? " + (a.equals(b))); // --> true, da die Inhalte von a und b übereinstimmen System.out.println("a.equals(c) ? " + (a.equals(c))); // --> true, da die Inhalte von a und c übereinstimmen /* lexikalischer Vergleich der der String-Objekte */ System.out.println("a.compareTo(c) ? " + (a.compareTo(c))); // --> 0, da die Inhalte von a und c übereinstimmen System.out.println("a.compareTo(d) ? " + (a.compareTo(d))); // --> 1 (> 0), da a lexikalisch größer ist als d System.out.println("a.compareTo(e) ? " + (a.compareTo(e))); // --> -3 (< 0), da a lexikalisch kleiner ist als e } }
Vergleiche mit Teil-Strings Weiterhin besitzt die Klasse String die Methoden startsWith() und endsWith(). Mit ihnen kann geprüft werden, ob ein String mit einem bestimmten Zeichen bzw. einer bestimmten Zeichenfolge beginnt oder endet. Das Suchen nach einer Telzeichenkette innerhalb eines Strings erfolgt mit der Methode regionMatches(). Methode/Operator
Beschreibung
boolean startsWith(String s)
Die Methode prüft, ob ein String mit dem als Parameter übergebenen String beginnt. In diesem Fall gibt sie true, sonst false zurück.
boolean endsWith(String s)
Analog zu startsWith() testet die Methode, ob ein String mit dem übergebenen String endet.
204
9 – Zeichenkettenverarbeitung Methode/Operator
Beschreibung
boolean regionMatches( int toffset, String other, int ooffset, int len)
Für die Suche nach gleichen Zeichenfolgen in zwei Strings kann die Methode regionMatches() benutzt werden. Es sind folgende Parameter anzugeben: –
Vergleichsposition im String
–
Vergleichs-String
–
Vergleichsposition im anderen String
–
Länge des zu vergleichenden Teilstücks
An diesem Beispielprogramm wird der Einsatz der Methoden zum Vergleichen von Teilstrings gezeigt. Im ersten Teil werden aus einem Array von Strings die Strings herausgesucht, die mit »K« und »Hu« beginnen und auf »d« enden. [IconInfo]
Der zweite Teil untersucht zwei Strings bezüglich gleicher Zeichenfolgen, die zwei Zeichen lang sind. Die Namen Meier und Schneider enthalten beide die Zeichenfolgen »ei« und »er«. Mit zwei ineinander geschachtelten Schleifen wird die Position der Zeichenfolgen bestimmt.
Listing 9.2: \Beispiele\de\j2sebuch\kap09\TeilStringVergleiche.java class TeilStringVergleiche { public static void main(String[] args) { String [] tiere = {"Huhn", "Maus", "Hund", "Katze", "Kuh", "Pferd"}; // Zeichen am Anfang des Strings vergleichen for(String s: tiere) if(s.startsWith("K")) System.out.println(s + " beginnt mit K"); else if(s.startsWith("Hu")) System.out.println(s + " beginnt mit Hu"); // Zeichen am Ende des Strings vergleichen for(String s: tiere) if(s.endsWith("d")) System.out.println(s + " endet mit d");
Java 5 Programmierhandbuch
205
Mit String-Objekten arbeiten Listing 9.2: \Beispiele\de\j2sebuch\kap09\TeilStringVergleiche.java (Forts.) // 2 gleiche Zeichen in zwei unterschiedlichen Strings suchen String a = "Meier"; String b = "Schneider"; for(int i = 0; i < a.length(); i++) for(int j = 0; j < b.length(); j++) if(a.regionMatches(i, b, j, 2)) System.out.println(a + " und " + b + " enthalten zwei "+ "gleiche Zeichen ab Position " + i + " und " + j); } }
9.1.6
Zeichenketten manipulieren
Teilstrings ermitteln Die Methode substring() gibt einen Teil eines Strings zurück. Über den Parameter beginIndex wird die Position des ersten Zeichens mitgeteilt, das zum Ergebnisstring gehören soll. Als zweiter Parameter wird das erste Zeichen angegeben, welches nicht mehr Bestandteil des Ergebnisstrings sein soll. Der Ergebnisstring wird also aus den Zeichen von beginIndex bis endIndex - 1 gebildet. Wird der zweite Parameter weggelassen, werden alle Zeichen bis zum Ende des Originalstrings verwendet. Beachten Sie auch hier wieder, dass der Index des ersten Zeichens Null ist. String substring(int beginIndex) String substring(int beginIndex, int endIndex)
Beispiel String s = "Hallo, Java-Fan"; Stirng s1 = s.substring(7); Stirng s2 = s.substring(7, 11));
// => s1 = "Java-Fan" // => s2 = "Java"
Teile eines Strings ersetzen Zeichen oder Teile von Strings können durch andere Zeichen bzw. Zeichenketten ersetzt werden. String replace(char oldChar, char newChar) String replace(String target, String replacement)
Die Methode replace() ersetzt das Zeichen oldchar bzw. alle Teilstrings target durch das Zeichen newChar bzw. den String replacement.
206
9 – Zeichenkettenverarbeitung
Beispiel Die erste replace()-Anweisung tauscht alle Kommata durch Semikolon aus. Die zweite replace()-Anweisung ersetzt jede Angabe des Teilstrings »00« durch den String »000«. String s = "100, 200, 300"; String s1 = s.replace(',', ';'); // => s1 = "100; 200; 300" String s2 = s.replace("00", "000"); // => s2 = "1000, 2000, 3000"
Leerzeichen entfernen Mithilfe der Methode trim() werden in einem String alle führenden und abschließenden Leerzeichen entfernt. String trim()
Zeichenketten anhängen Die Methode concat() hängt den als Parameter angegebenen String an den Originalstring an. Das gleiche Ergebnis erhalten Sie beim Operator + für zwei Strings. String concat(String str)
Buchstaben in Groß- oder Kleinbuchstaben konvertieren Nicht immer muss bei der Zeichenkettenauswertung, z.B. bei einigen Vergleichen von Zeichenketten, die Groß- oder Kleinschreibung beachtet werden. Für solche Auswertungen können alle Buchstaben eines Strings in Klein- oder in Großbuchstaben umgewandelt werden. Hierfür gibt es die Methoden toLowerCase() und toUpperCase(). String toLowerCase() String toUpperCase()
Beispiel Zwei Strings, die das Wort »Javabuch« in unterschiedlicher Groß-/Kleinschreibung enthalten, werden definiert. Der erste String enthält zusätzlich noch Leerzeichen vor und hinter dem Wort. In einer if-Anweisung wird geprüft, ob beide Strings die gleiche Zeichenkette enthalten. Auf den ersten String wird die Methode trim() zum Entfernen zusätzlicher Leerzeichen angewandt. Da die Groß-/Kleinschreibung keine Bedeutung für den Vergleich besitzt, wird für beide Strings die Methode toLowerCase() aufgerufen, um die Strings in Kleinbuchstaben umzuwandeln. Analog hätten Sie hier die Methode toUpperCase() verwenden können. Der Vergleich selbst erfolgt mit der Methode equals(). String s1 = " JaVaBuCh "; String s2 = "Javabuch"; if(s1.trim().toLowerCase().equals(s2.toLowerCase())) System.out.println("die Woerter sind gleich");
Java 5 Programmierhandbuch
207
Mit String-Objekten arbeiten
9.1.7
Formatierte Strings erzeugen Seit dem JDK 5.0 stellt die Klasse String die Methode format() bereit. Mithilfe dieser Methode ist es möglich, einen formatierten String aus den Werten mehrerer Variablen verschiedener Datentypen zusammenzustellen.
[IconInfo]
Die Methode gibt es mit zwei verschiedenen Parameterlisten. static String format(String format, Object... args) static String format(Locale l, String format, Object... args)
Sie gleicht der Methode format() der Klasse Formatter, die im letzten Abschnitt dieses Kapitels ausführlich erklärt wird. An dieser Stelle wird die Verwendung der Methode nur an einem Beispiel gezeigt. Beispiel Es werden drei Variablen verschiedenen Datentyps deklariert und mit Werten belegt. Diese Werte sollen ihrem Datentyp entsprechend formatiert ausgegeben werden. Der Methode format() werden ein Formatierungs-String und die Liste der auszugebenden Werte übergeben. int anzahl = 12; double preis = 27.87; String waehrung = "Euro"; System.out.println( String.format("%1$3d Stuecke kosten %2$8.2f %3$2s", anzahl, anzahl * preis, waehrung));
Ausgabe des Programms: 12 Stuecke kosten
334,44 Euro
Die Methode format() ist in der Klasse String statisch (final) deklariert und kann, ohne dass ein String-Objekt vorhanden sein muss, aufgerufen werden. [IconInfo]
9.1.8
Andere Datentypen in einen String konvertieren
Häufig müssen Werte anderer Datentypen in einen String konvertiert werden, beispielsweise bei der Ausgabe eines Zahlenwerts auf dem Bildschirm. Für jeden primitiven Datentyp, für Zeichen-Arrays und für den Datentyp Object existiert für diese Aufgabe die statische Methode valueOf() in der Klasse String. Diese statische Methode kann aber auch aufgerufen werden, ohne vorher ein String-Objekt zu erzeugen.
208
9 – Zeichenkettenverarbeitung static static static static ... static
String String String String
valueOf(boolean b) valueOf(char c) valueOf(char[] data) valueOf(int i)
String valueOf(Object obj)
Die Methode valueOf() ruft bei der Konvertierung von Zahlen-Datentypen in Strings die Methode toString() der entsprechenden Wrapperklasse auf. Auch alle Objekte besitzen die Methode toString(), die bei der Ausführung der Methode valueOf() verwendet wird. Beispiel Zwei String-Objekten soll der Wert der double-Variable d zugewiesen werden. Es wird einmal die Methode valueOf() der Klasse String und einmal die Methode toString() der Wrapperklasse Double verwendet. Beide Anweisungen haben die gleiche Wirkung. Auch der Verkettungsoperator, den wir in früheren Beispielen schon häufig benutzt haben, führt eine Konvertierung in den Datentyp String durch, wenn der erste Operand ein String ist. double d = 1.2345; String s1 = String.valueOf(d); String s2 = Double.toString(d); System.out.println("s1 = s2 ? " + s1.equals(s2)); String s3 = "" + d; System.out.println("s1 = s3 ? " + s1.equals(s3));
// => true // => true
String ist eine finale Klasse, also mit dem Schlüsselwort final deklariert. Das bedeutet, dass von dieser Klasse keine weitere Klasse abgeleitet werden kann. So gibt es keine Möglichkeit, die Methoden dieser Klasse zu erweitern bzw. zu modifizieren. [IconInfo]
9.2
StringBuilder- und StringBuffer-Objekte verwenden
Der Umgang mit String-Objekten ist relativ einfach. Es gibt viele Methoden zur Manipulation von Strings. String-Objekte selbst können nicht bearbeitet werden. Einer StringVariablen kann zwar ein anderes String-Objekt zugewiesen werden, dabei wird aber nur die Referenz der Variablen geändert und nicht das String-Objekt selbst. Da der Programmierer von der internen Umsetzung nicht direkt etwas bemerkt, ist dies für viele Anwendungsfälle kein Problem. Bei einigen Aufgabenstellungen ist es aber von Vorteil, wenn Zeichenketten dynamisch geändert werden können. Die Klassen StringBuffer und StringBuilder unterstützen mit ihren Methoden die dynamische Verwaltung von Zeichenketten. Die Ausführung der Operationen erfolgt wesentlich schneller als bei StringObjekten.
Java 5 Programmierhandbuch
209
StringBuilder- und StringBuffer-Objekte verwenden Seit dem JDK 5.0 existiert die Klasse StringBuilder, welche die gleiche Funktionalität wie die Klasse StringBuffer besitzt. Beide Klassen verfügen über die gleichen Konstruktoren und Methoden. Unter den meisten Implementierungen arbeitet die Klasse StringBuilder aber etwas schneller als die Klasse StringBuffer. Sie ist aus diesem Grund vorzuziehen. Der Grund liegt in der Thread-Sicherheit, die nur durch die Klasse StringBuffer gewährleistet ist, aber in vielen Fällen nicht benötigt wird.
[IconInfo]
In den folgenden Abschnitten wird nur die Klasse StringBuffer erklärt. Die Erläuterungen treffen aber ebenso auf die Klasse StringBuilder zu.
Ein StringBuffer-Objekt besitzt einen Puffer, der sich der Größe des Strings dynamisch anpasst. Die Puffergröße wird automatisch verdoppelt, wenn sie nicht mehr ausreicht. Dadurch wird allerdings mehr Speicher bereitgestellt, als eigentlich benötigt wird. Auch die Referenzierung gleicher Strings ist durch StringBuffer-Objekte nicht möglich.
9.2.1
Ein StringBuffer-Objekt erzeugen
Zur Erzeugung eines StringBuffer-Objektes muss einer der vier Konstruktoren verwendet werden. Benutzen Sie den parameterlosen Konstruktor, wird ein leerer Puffer mit einer Kapazität von 16 Zeichen angelegt. Soll von Anfang an ein größerer (leerer) Puffer erstellt werden, kann die gewünschte Kapazität als Parameter übergeben werden. Übergeben Sie dem Konstruktor einen String oder ein Zeichen-Array, wird ein für die Zeichenkette entsprechend großer Puffer erzeugt und dieser damit initialisiert. StringBuffer() StringBuffer(int capacity) StringBuffer(CharSequence seq) StringBuffer(String str)
9.2.2
// leerer Puffer für 16 Zeichen // leerer Puffer für // capacity Zeichen // StringBuffer wird mit seq oder // str initialisiert
Ein StringBuffer- in ein String-Objekt umwandeln
Für diese Umwandlung gibt es zwei Möglichkeiten: 쐌 Sie können zum einen die Methode toString() der Klasse StringBuffer einsetzen. StringBuffer sb = new StringBuffer("Teetasse"); String s1 = sb.toString();
쐌 Oder Sie benutzen den Konstruktor der Klasse String, der ein StringBuffer-Objekt als Parameter akzeptiert. String s2 = new String(sb);
210
9 – Zeichenkettenverarbeitung
9.2.3
Daten anhängen und einfügen
Einem StringBuffer-Objekt können mithilfe der append()-Methode Zeichen hinzugefügt werden. Der Methode können unter anderem Strings, primitive Datentypen und Zeichen-Arrays übergeben werden. Bevor die Daten an die vorhandene Zeichenkette angefügt werden, erfolgt eine Konvertierung in einen String. StringBuffer StringBuffer StringBuffer ... StringBuffer
append(char c) append(char[] str) append(double d) append(String str)
Zeichen können an der Indexposition mit der Methode insert() eingefügt werden. Auch hier ist ein Aufruf mit verschiedenen Datentypen möglich. StringBuffer insert(int offset, char c) StringBuffer insert(int offset, int i) ... StringBuffer insert(int offset, String str)
Im folgenden Beispiel wird ein StringBuffer-Objekt erzeugt und mehrere Daten (ein String-, StringBuffer- und ein double-Wert) werden angehängt. Anschließend wird der Wert einer int-Variablen in die Zeichenkette eingefügt. [IconInfo]
Listing 9.3: \Beispiele\de\j2sebuch\kap09\StringBufferVerwenden.java class StringBufferVerwenden { public static void main(String[] args) { StringBuffer sb = new StringBuffer("Programmieren"); String s = " mit Java "; int i = 2; StringBuffer sb2 = new StringBuffer(" Standard Edition "); double d = 5.0; sb.append(s).append(sb2).append(d); // Daten anhängen System.out.println("sb = " + sb); // Ausgabe: sb = Programmieren mit Java Standard Edition 5.0 sb.insert(23, i); // Zahl einfügen System.out.println("sb = " + sb); // Ausgabe: sb = Programmieren mit Java 2 Standard Edition 5.0 } }
Java 5 Programmierhandbuch
211
StringBuilder- und StringBuffer-Objekte verwenden
Ein Beispiel für die interne Verwendung eines StringBuffers ist die Verkettung von Strings mithilfe des Operators +. Intern wird ein StringBuffer-Objekt für den ersten Operanden erzeugt, dem die anderen Operanden über die append-Methode hinzugefügt werden.
9.2.4
Löschen und Verändern von Zeichen im StringBuffer
Einzelne Zeichen können Sie mit der Methode delete() löschen. Die Methode setCharAt() ersetzt ein Zeichen an der angegebenen Indexposition und über replace() lässt sich ein Teil der Zeichenkette durch einen anderen String ersetzen. StringBuffer delete(int start, int end) void setCharAt(int index, char ch) StringBuffer replace(int start, int end, String str)
9.2.5
String-Länge und Puffergröße bestimmen
Die Länge des im StringBuffer gespeicherten Strings und die Puffergröße sind zwei unterschiedliche Größen. Der Puffer wird der Größe des zu speichernden Strings dynamisch angepasst und ist immer größer als der String. Wie bei String-Objekten bestimmen Sie mit der Methode length() die Länge der Zeichenkette. Die Methode capacity() bestimmt die Puffergröße. int length() int capacity()
[IconInfo]
Die dynamische Änderung der Puffergröße soll an diesem Beispiel verdeutlicht werden. Zunächst wird ein leerer StringBuffer erzeugt. In einer Schleife wird die im StringBuffer gespeicherte Zeichenkette um jeweils 10 Zeichen vergrößert und die Puffergröße sowie die Zeichenkettenlänge werden ausgegeben.
Listing 9.4: \Beispiele\de\j2sebuch\kap09\Puffergroesse.java class Puffergroesse { public static void main(String[] args) { StringBuffer x = new StringBuffer(); for(int i = 1; i < 1200; i += 10) { System.out.println("Puffergroesse: " + x.capacity() + " Laenge: " + x.length()); x.append("0123456789"); } } }
212
9 – Zeichenkettenverarbeitung
Ausgabe des Programms: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse: Puffergroesse:
9.2.6
16 Laenge: 0 16 Laenge: 10 34 Laenge: 20 34 Laenge: 30 70 Laenge: 40 70 Laenge: 50 70 Laenge: 60 70 Laenge: 70 142 Laenge: 80 ...
Vergleich von StringBuffer-Objekten
Ein direkter Vergleich der in StringBuffer-Objekten gespeicherten Zeichenketten ist mit der Methode equals() wie bei String-Objekten nicht möglich. Die Methode equals() kann zwar für StringBuffer-Objekte aufgerufen werden, sie ist aber ein Erbstück der Klasse Object und vergleicht nur Objekte miteinander. Zur Durchführung eines Vergleichs der Zeichenketten muss zuvor eine Konvertierung in ein String-Objekt erfolgen. Beispiel StringBuffer sb1 = new StringBuffer("Hallo"); StringBuffer sb2 = new StringBuffer("Hallo"); System.out.println("sb1 = sb2 ? " + sb1.toString().equals(sb2.toString()));
// => true
Ein String-Objekt und ein StringBuffer-Objekt können einfacher verglichen werden. Hierfür stellt die Klasse String die Methode contentEquals() bereit. boolean contentEquals(StringBuffer sb)
Für StringBuilder-Objekte gibt es keine Überladung dieser Methode. Stattdessen kann der Vergleich mit der String-Methode contains() erfolgen. Die Zeichenkette des StringBuilder-Objekts wird mit der Methode toString() ermittelt. boolean contains(String s)
Beispiel String s1 = "Hallo"; StringBuffer s2 = new StringBuffer("Hallo"); StringBuilder s3 = new StringBuilder("Hallo"); System.out.println("s1 = s2 ? " + s1.contentEquals(s2)); System.out.println("s1 = s3 ? " + s1.contains(s3.toString()));
Java 5 Programmierhandbuch
213
StringBuilder- und StringBuffer-Objekte verwenden
In beiden Fällen fällt der Vergleich positiv aus, d.h., die Methodenaufrufe liefern beide den Wert true.
9.2.7
Performance-Steigerung durch die Klassen StringBuffer und StringBuilder
Die Zeiteinsparung bei der Verwendung von StringBuffer- bzw. StringBuilder- anstelle von String-Objekten kann ganz erheblich sein, wenn der Schwerpunkt der Anwendung in der Änderung der darin gespeicherten Zeichenkette liegt. Die Entscheidung, ob Sie im Programm für die Speicherung der Zeichenketten String- oder StringBuffer- bzw. StringBuilder-Objekte einsetzen, sollte also auch von der Häufigkeit deren Verwendung abhängen. In diesem Testprogramm soll dies nachgewiesen werden. Es misst die benötigte Zeit, um 5000-mal ein Zeichen an ein StringBuffer-Objekt anzuhängen, und gibt sie danach aus. Gleiches wird anschließend mit einem String- und mit einem StringBuilder-Objekt durchgeführt. [IconInfo]
Hinweis: Verändern Sie die Anzahl der Schleifendurchläufe, indem Sie der Konstanten anzahl einen anderen Wert zuweisen, entsprechend der Geschwindigkeit Ihres Rechners, um ein aussagekräftiges Ergebnis zu erhalten.
Listing 9.5: \Beispiele\de\j2sebuch\kap09\PerformanceTest.java class PerformanceTest { public static void main(String[] args) { StringBuffer sb = new StringBuffer(); String s = ""; StringBuilder sb2 = new StringBuilder(); long dauer; long start; final int anzahl = 5000; // Dauer bei der Verwendung von StringBuffer ermitteln start = System.currentTimeMillis(); // aktuelle Zeit in ms for(int i = 1; i < anzahl; i++) sb.append("a"); dauer = System.currentTimeMillis() - start; System.out.println("Dauer bei der Verkettung mit " + "StringBuffer: " + dauer); // Dauer bei der Verwendung eines String-Objekts ermitteln start = System.currentTimeMillis(); // aktuelle Zeit in ms for(int i = 1; i < anzahl; i++) s = s + "a";
214
9 – Zeichenkettenverarbeitung Listing 9.5: \Beispiele\de\j2sebuch\kap09\PerformanceTest.java (Forts.) dauer = System.currentTimeMillis() - start; System.out.println("Dauer bei der Verkettung von " + "Strings: " + dauer); // Dauer bei der Verwendung eines StringBuilder ermitteln start = System.currentTimeMillis(); // aktuelle Zeit in ms for(int i = 1; i < anzahl; i++) sb2.append("a"); dauer = System.currentTimeMillis() - start; System.out.println("Dauer bei der Verkettung mit " + "StringBuilder: " + dauer); } }
9.3 9.3.1
Formatierung Formatierung mithilfe der Klasse Formatter
Die Klasse Formatter ermöglicht die formatierte Aufbereitung von Daten wie Strings, Zahlen, Datums- und Zeitwerten. Es kann beispielsweise die Ausrichtung der Daten, die gewünschte Form der Zahlendarstellung, z.B. die Anzahl der Stellen vor und nach dem Komma, oder die Ausgabeform von Datums- bzw. Zeitwerten festgelegt werden. Die Formatierung kann landesspezifisch erfolgen. Für die Benutzung der Klasse Formatter müssen Sie das Package java.util einbinden. Die Klasse Formatter ist im JKD 5.0 neu hinzugekommen. Sie vereinfacht und erweitert die Möglichkeiten der Formatierung. In älteren Versionen kann zur Formatierung die Klasse Format benutzt werden, die aber weniger Datentypen unterstützt und umständlicher zu handhaben ist. [IconInfo]
Die Klasse Formatter unterstützt die Formatierung folgender Datentypen: 쐌 쐌 쐌 쐌
Strings primitive Datentypen Datum- und Zeit-Datentypen die Klassen Calender, BigInteger und BigDecimal
Ist Ihnen die Sprache C bekannt, werden Sie das Prinzip der Funktion printf() wiedererkennen. [IconInfo]
Java 5 Programmierhandbuch
215
Formatierung
9.3.2
Formatter-Objekt erzeugen
Für eine Formatierung mithilfe der Klasse Formatter benötigen Sie ein Objekt dieser Klasse. Erzeugen Sie dieses über einen der vorhandenen Konstruktoren, von denen drei an dieser Stelle aufgeführt sind: Formatter(Appendable a) Formatter(Appendable a, Locale l) Formatter(String fileName)
Der erste Parameter einiger Konstruktoren ist vom Typ Appendable. Dieses Interface wurde im JDK 5.0 neu aufgenommen. Es beinhaltet zwei append()-Methoden, denen ein Zeichen (char) oder eine Zeichenfolge (CharSequence) übergeben werden kann. Implementiert wurde dieses Interface in alle Klassen, deren Instanzen formatierte Ausgaben über Formatter empfangen sollen. Hierzu gehören z.B. die verschiedenen Writer- und StreamKlassen (vergleiche Kapitel 11), die zur Ausgabe dienen. Über den Parameter vom Typ Locale können Sie das Land angeben, dessen landesübliche Formatierung benutzt werden soll (z.B. für Dezimaltrennzeichen in Zahlen). Wird ein Dateiname (fileName) als Parameter angegeben, erfolgt die Ausgabe in die über den Namen spezifizierte Datei. Die Daten werden ungepuffert an den Dateiinhalt angehängt. Beispiel Für die Beispiele im folgenden Abschnitt wird das hier deklarierte Formatter-Objekt formatter verwendet.
[IconInfo]
Beachten Sie bei den Ausgaben, dass als Lokalität Deutschland eingestellt ist. Dadurch werden beispielsweise Gleitkommazahlen über ein Komma und nicht wie in der Programmierung mit Java über einen Punkt dargestellt.
StringBuilder sb = new StringBuilder(); Formatter formatter = new Formatter(sb, Locale.GERMANY);
Die Ausgabe des Formatter-Objekts soll in den StringBuilder sb erfolgen. Bei der Formatierung sind die Besonderheiten unseres Landes zu berücksichtigen. Dies wird durch den zweiten Parameter bewirkt. Mehr über das Thema landesspezifische Besonderheiten erfahren Sie im Kapitel »Internationalisierung«.
9.3.3
Daten konvertieren
Die wichtigste Methode dieser Klasse heißt format(). Sie fügt dem formatierten Ergebnis-String das mit dem Formatter verbundene Objekt hinzu. Die Methode kann mit zwei verschiedenen Parameterlisten aufgerufen werden.
216
9 – Zeichenkettenverarbeitung Formatter format(Locale l, String format, Object... args) Formatter format(String format, Object... args)
Als erster Parameter kann bei Bedarf die Landeskennung angegeben werden. Der Parameter format enthält die für die Formatierung notwendigen Informationen, den FormatString. Anschließend werden die Werte übergeben, die nach den Vorgaben im FormatString formatiert werden. Die Methode format() mit den gleichen Parameterlisten, aber einem String-Objekt als Rückgabewert, ist in der Klasse String implementiert. Die folgenden Ausführungen gelten analog auch für diese Methode. [IconInfo]
Der Format-String Den Format-String können Sie sich wie eine Schablone vorstellen. Er enthält alle konstanten Ausgabedaten sowie Platzhalter (Format-Spezifizierer) für die Ausgabe der variablen Werte. In den Format-Spezifizierern wird die Formatierung der entsprechenden Werte festgelegt, beispielsweise die maximale Anzahl der Stellen einer Zahl. Für jeden Parameter (der Liste args), der hinter dem Format-String geschrieben wird, muss ein Format-Spezifizierer definiert werden. Beispiel int anzahl = 12; double preis = 27.87; String waehrung = "Euro"; formatter.format("%1$3d Stuecke kosten %2$8.2f %3$2s", anzahl, anzahl * preis, waehrung));
Format-String: (format)
"1$3d Stuecke kosten %2$8.2f %3$2s"
Werte (args)
anzahl
anzahl * preis
waehrung
Abb. 9.2: Zuordnung der Variablen zu den Format-Spezifizierern
Der Format-String enthält hier drei Format-Spezifizierer. Sie legen fest, wie die Argumente behandelt und an welcher Stelle sie eingefügt werden sollen. Die Format-Spezifizierer beginnen mit einem Prozentzeichen. Ihm folgt die Nummer des Arguments, auf welches sie sich beziehen. Im Beispiel sind dies die Argumente 1 bis 3. Also müssen nach dem FormatString drei Argumente angegeben werden. Die Argumente können Ausdrücke, Variablen oder Literale sein.
Java 5 Programmierhandbuch
217
Formatierung
Ein Format-Spezifizierer hat folgenden allgemeinen Aufbau: %[argument_index$][flags][width][.precision]conversion
쐌 Der Format-Spezifizierer wird mit einem Prozentzeichen % eingeleitet. Bis auf conversion sind alle Teile optional. 쐌 argument_index: gibt die Position des Arguments in der Argumentliste an. Mit 1$ wird das erste Argument, mit 2$ das zweite bezeichnet usw. 쐌 flags: Zeichen, welches die Form der Ausgabe beeinflusst. Welche Flags erlaubt sind, hängt vom Datentyp ab. 쐌 width: minimale Anzahl von Zeichen, die in die Ausgabe eingefügt werden. Beachten Sie, dass bei Zahlen auch Komma, Vorzeichen, Exponent usw. berücksichtigt werden müssen. 쐌 precision: Anzahl der Nachkommastellen bei Gleitkommazahlen oder der auszugebenden Zeichen bei Strings. 쐌 conversion (Konvertierungszeichen): Zeichen, das die Formatierung des Arguments festlegt. Welches Zeichen benutzt werden kann, bestimmt der Datentyp. Haben Sie in den Format-Spezifizierern keinen Argumentindex (argument_index) festgelegt, wird die Argumentliste in der angegebenen Reihenfolge verwendet, d.h., das erste Argument wird dem ersten Format-Spezifizierer zugeordnet, das zweite dem zweiten usw. Der Argumentindex (argument_index) ist also nur erforderlich, wenn die Argumente nicht in der gleichen Reihenfolge angegeben werden wie die zugehörigen Format-Spezifizierer. Betrachten Sie beispielsweise die folgende Abbildung.
Format-String: (format)
"2$3d Stuecke kosten %3$8.2f %1$2s"
Werte (args)
waehrung
anzahl
anzahl * preis
Abb. 9.3: Zuordnung der Variablen zu den Format-Spezifizierern
Mehrere Format-Spezifizierer können sich auch auf ein Argument beziehen. Es müssen dann ebenfalls die Argumentindizes benutzt werden.
Format-String: (format)
Werte (args)
"1$d entspricht dem Zeichen %1$c"
152
Abb. 9.4: Zuordnung eines Werts zu mehreren Format-Spezifizierern
218
9 – Zeichenkettenverarbeitung
Die möglichen Angaben für flags und precision hängen vom zu formatierenden Datentyp ab. Der Datentyp wird durch das Konvertierungszeichen bestimmt. Fehlerhafte Format-Spezifizierer verursachen eine Exception. Wird beispielsweise ein Wert für precision angegeben, wenn der Datentyp Integer oder ein Datum konvertiert werden soll, wird eine IllegalFormatPrecisionException ausgelöst. In den folgenden Abschnitten finden Sie eine Auswahl der möglichen Zeichen für die Konvertierung (conversion) und für die möglichen Flags. Die vollständige Liste können Sie in der API-Dokumentation nachschlagen. [IconInfo]
Flags Flag
Bedeutung
Gültigkeit
'-'
Linksbündige Ausrichtung im Bereich des Format-Spezifizierer
allgemein
'^'
Umwandlung in Großbuchstaben
allgemein
'+'
Das Vorzeichen wird immer mit ausgegeben
für Zahlen
'('
Negative Ergebnisse werden in Klammern eingeschlossen
für Zahlen
','
Der Gruppierungs-Separator (Tausender-Trennzeichen) wird ausgegeben
für Zahlen
'0'
Die für das Argument vorgegebene Breite wird mit führenden Nullen aufgefüllt
für Zahlen
In einem Format-Spezifizierer können mehrere Flags kombiniert werden. Beispielsweise lassen sich Gleitpunktzahlen mit Vorzeichen, führenden Nullen und Tausender-Trennzeichen ausgeben: "%1$+,012.2f ". Einige Kombinationen sind aber auch unzulässig, wie die Angabe von '–' und '0' in einem Format-Spezifizierer. Sie verursachen eine Exception vom Typ IllegalFormatFlagsException. Zeichen und Strings ausgeben Zeichen
Ausgabe
anwendbar auf
'c'
Ein Unicode-Zeichen wird ausgeben, Zahlenwerte werden entsprechend umgewandelt.
char, Character, byte, Byte, short, Short
's'
Ausgabe eines Strings. Ist das Argument kein String-Objekt, wird die Methode toString() aufgerufen. Implementiert das Argument das Interface Formattable, wird die Methode formatTo() gestartet.
beliebige Datentypen
Java 5 Programmierhandbuch
219
Formatierung
Zeichen und Strings können mithilfe des Flags '^' in Großbuchstaben umgewandelt werden. Haben Sie eine minimale, nicht ausgefüllte Breite definiert, werden die Zeichen linksbündig ausgegeben, wenn das Flag '-' benutzt wird. Verwenden Sie das Konvertierungszeichen 'c', wird genau ein Zeichen ausgegeben. Wird 's' eingesetzt, richtet sich die Anzahl der Zeichen nach dem String, den das Argument liefert. Legen Sie eine minimale Breite (width) fest, werden mindestens so viele Zeichen berücksichtigt. Besitzt der String weniger Zeichen, wird mit Leerzeichen aufgefüllt. Auf diese Weise können Werte tabellenartig untereinander gesetzt werden. Mit dem Konvertierungszeichen 's' lässt sich eine Genauigkeit (precision) definieren. Sie bestimmt die Anzahl der Zeichen, die vom Argument-String für die Ausgabe verwendet werden. Für 'c' ist die Angabe der Genauigkeit nicht erlaubt. Beispiel char z = 'a'; byte b = 76; short s = 105; double d = 12.86; String zk = "abcdefg"; // Ausgaben über das Konvertierungszeichen 'c' formatter.format("Zeichen: %c Byte: %c Short: %c", z, b, s)); // Ausgaben über das Konvertierungszeichen 's' formatter.format( "Zeichen: %s Byte: %s Short: %s Double: %s z, b, s, d, zk));
String: %5.3s ",
Ausgaben: Zeichen: a Zeichen: a
Byte: L Byte: 76
Short: i Short: 105
Double: 12.86
String:
abc
Diese Ausgabe zeigt die Auswirkung des Konvertierungszeichens. Wird z.B. ein Bytewert als Zeichen formatiert, erscheint in der Ausgabe das dem Bytewert entsprechende UnicodeZeichen. Erfolgt die Ausgabe als String, wird der Zahlenwert verwendet. Die Ausgabe des Strings zk zeigt die Wirkungsweise der Angabe von width und precision. Es werden nur die ersten drei Zeichen des Strings berücksichtigt. Die letzten beiden Zeichen sind Leerzeichen.
220
9 – Zeichenkettenverarbeitung
Zahlenwerte ausgeben Für die Ausgabe von Zahlen muss zwischen Integer- und Gleitkommatypen unterschieden werden. Besitzt eine Variable den Wert NaN oder Unendlich, so werden die Literale "NaN" bzw. "Infinity" für positiv und "Infinity" für negativ Unendlich geschrieben. Durch die Angabe verschiedener Flags kann die Ausgabe der Zahlen noch besser gestaltet werden. So können Sie beispielsweise Tausendertrennzeichen verwenden, die Zahlen mit Vorzeichen versehen und führende Nullen erzeugen. Integer-Zahlen Die folgenden Konvertierungszeichen können sowohl für die Datentypen Byte, Short, Integer und Long (und dank Autoboxing auch für die korrespondierenden primitiven Typen) als auch für BigInteger-Objekte benutzt werden. 'd'
Gibt eine Integer-Zahl aus (Dezimalzahl).
'o'
Gibt eine oktale Integer-Zahl aus. Es sind nicht alle Flags, die für Zahlen gültig sind, einsetzbar.
'x', 'X'
Gibt eine hexadezimale Integer-Zahl aus. Bei Verwendung des groß geschriebenen Konvertierungszeichens werden die Buchstaben in der Hex-Zahl in Großbuchstaben geschrieben, sonst in Kleinbuchstaben.
Ohne den Gebrauch von Flags werden Integer-Zahlen standardmäßig rechtsbündig ausgerichtet. Sie enthalten keine Tausendertrennzeichen. Negative Zahlen besitzen ein Vorzeichen, positive Zahlen nicht. Die Breite der Integer-Zahlen in der Ausgabe richtet sich nach der Größe der Zahl (Anzahl der Stellen). Wurde eine minimale Breite (width) zugewiesen, welche die Zahl nicht nutzt, wird der Platz mit Leerzeichen oder, wenn das Flag '0' gesetzt wurde, mit führenden Nullen aufgefüllt. Ist die Zahl größer als die festgelegte minimale Breite, werden trotzdem alle Stellen der Zahl ausgegeben. Die Breite der Zahl bezieht sich nicht nur auf die Anzahl der Ziffern, es werden auch das Vorzeichen, die Tausendertrennzeichen und die Klammern berücksichtigt. Die Genauigkeit (precision) kann für Interger-Zahlen nicht definiert werden. Sie würde zum Auslösen einer Exception führen. Beispiel int i = 34728; long l = 286352725; BigInteger bi = new BigInteger("643642348236823"); formatter.format("Integer: %1$+,9d Hexa: %1$X " + "%nLong: %2$+,d BigInteger: 3$+,d ", i, l, bi));
Java 5 Programmierhandbuch
221
Formatierung
Ausgabe: Integer: +34.728 Long: +286.352.725
Hexa: 87A8 BigInteger: +643.642.348.236.823
Die Ausgabe der ganzen Zahlen erfolgt mit Vorzeichen (Flag '+') und Tausendertrennzeichen (Flag ','), was besonders bei sehr großen Zahlen ratsam ist. Die Integer-Zahl i wird zusätzlich noch als Hexadezimalzahl geschrieben. Es wird für die Konvertierung das Zeichen 'X' verwendet, damit die Buchstaben der Hexadezimalzahl als Großbuchstaben ausgegeben werden. Gleitkommazahlen Die folgenden Formatierungszeichen können Sie für die Datentypen float, Float, double, Double sowie die Klasse BigDecimal einsetzen. 'e'
Gleitkommazahl in wissenschaftlicher Schreibweise (mit Exponent)
'g'
Ausgabe im Dezimalformat bei Zahlenwerten zwischen 10-3 und 107, sonst als Gleitpunktzahl in wissenschaftlicher Schreibweise
'f'
Ausgabe im Dezimalformat
Haben Sie keine Flags benutzt, werden auch Gleitkommazahlen standardmäßig rechtsbündig, ohne Tausendertrennzeichen und ohne Vorzeichen bei positiven Zahlen ausgegeben. Gleitkommazahlen werden in der vom gewählten Format vorgegebenen Breite ausgegeben. Die Angabe der minimalen Breite (width) wirkt wie bei Integer-Zahlen. Die Breite der Zahl bezieht sich hier nicht nur auf die Anzahl der Ziffern. Es werden auch das Vorzeichen, die Tausender- und Dezimaltrennzeichen, das Exponentensymbol und der Exponent sowie die Klammern einbezogen. Die Angabe der Genauigkeit (precision) spielt bei Gleitkommazahlen eine große Rolle. Sie legt die Anzahl der Nachkommastellen fest, die standardmäßig 6 beträgt. Wird beispielsweise bei der Verwendung des Formatierungszeichens 'e' keine Genauigkeit angegeben, werden nur insgesamt sieben Ziffern (eine vor dem Dezimalpunkt und sechs Nachkommastellen) berücksichtigt, auch wenn die Zahl mehr Stellen ungleich Null besitzt. Beispiele Die beiden definierten double-Zahlen werden als Beispiele für verschiedene Format-Spezifizierer eingesetzt. double d1 = 353472.87978; double d2 = -0.0003321;
222
9 – Zeichenkettenverarbeitung Formatspezifizierer
Ausgabe
// wissenschaftliche Schreibweise - Standard formatter.format("%e", d1)
3.534728e+05
formatter.format("%e", d2)
-3.321000e-04
// wissenschaftliche Schreibweise – // mit 10 Nachkommastellen formatter.format("%.10e", d1);
3.5347287978e+05
formatter.format("%.10e", d2);
-3.3210000000e-04
// allgemeine Darstellung mit 10 Stellen // nach dem Komma formatter.format("%.10g", d1);
353472,8797800000
formatter.format("%.10g", d2);
-3.3210000000e-04
// Dezimalzahl mit Vorzeichen formatter.format("%,+f", d1);
+353.472,879780
formatter.format("%f", d2);
-0,332100
Das letzte Beispiel zeigt, dass das Formatierungszeichen 'f' für derartig kleine Zahlen nicht geeignet ist. Die Zahl wird verfälscht. Benutzen Sie in solchen Fällen besser das Formatierungszeichen 'g'. Prozentzahlen Eine Prozentzahl können Sie als die entsprechend formatierte Interger- oder Gleitpunktzahl schreiben. Die Ausgabe eines einzelnen Prozentzeichens im Formatierungs-String ist nicht möglich, weil es einen Format-Spezifizierer einleitet. Geben Sie hierfür zwei Prozentzeichen hintereinander an "%%". Die Angabe der Breite (z.B. in "%4%") wird ignoriert. Beispiel Die als Argument benutzte Integervariable proz wird über den Format-Spezifizierer "%1$3d" formatiert. Die folgenden zwei Prozentzeichen bewirken die Ausgabe des Prozentzeichens hinter der Zahl. int proz = 98; formatter.format("Prozentangabe: %1$3d%% ",
proz);
Ausgabe: Prozentangabe:
98%
Java 5 Programmierhandbuch
223
Formatierung
Datum/Zeit ausgeben Für die Ausgabe von Datum und Zeit sind die Datentypen Date, Calendar, long und Long zulässig. Die Konvertierungsvorschrift für Datums- und Zeitangaben setzt sich aus zwei Zeichen zusammen. Sie beginnt mit dem Präfix 't'. Das zweite Zeichen (siehe Tabelle zu Datum und Zeit) bestimmt die Vorschrift genauer. 't'
Präfix für Datums- und Zeitangaben
Datum 'Y'
Jahr, vierstellig z.B. 2004
'y'
Jahr, zweistellig z.B. 04
'm'
Monat, zwei Ziffern (01 ... 12)
'B'
Monat, voller Name (»Januar« ... »Dezember«)
'b'
Monat, abgekürzter Name (»Jan« ... »Dez«)
'A'
Tag der Woche, voller Name (»Montag«... »Sonntag«)
'a'
Tag der Woche, abgekürzter Name (»Mon«... »Son«)
'j'
Tag des Jahres (001 ... 366)
'd'
Tag des Monats (01 ... 31)
'D'
Datum mit dem Format "%tm/%td/%ty"
Beispiel In diesem Beispiel beziehen sich alle Format-Spezifizierer auf das erste Argument. Der Argumentindex ist unbedingt notwendig, weil sonst eine MissingFormatArgumentException ausgelöst wird. Durch den Einsatz verschiedener Konvertierungszeichen erhalten Sie verschiedene Informationen des Calendar-Objekts. // Calendar-Objekt für den 20.11.2004 erzeugen Calendar c = new GregorianCalendar(2004, 10, 20); formatter.format("Der %1$td.%1$tB %1$tY ist ein %1$tA und " + "der %1$tj-te Tag des Jahres", c));
Ausgabe: Der 20.November 2004 ist ein Samstag und der 325-te Tag des Jahres
224
9 – Zeichenkettenverarbeitung
Zeit 'H'
Stunden einer 24-Stunden-Uhr, zwei Zeichen mit führender Null (00 – 23)
'I'
Stunden einer 12-Stunden-Uhr, zwei Zeichen mit führender Null (00 – 12)
'M'
Minuten, zwei Zeichen mit führender Null (00 – 59)
'S'
Sekunden, zwei Zeichen mit führender Null (00 – 60)
'L'
Millisekunden, drei Zeichen mit führenden Nullen (000 – 999)
'R'
Zeit mit dem Format "%tH:%tM"
'T'
Zeit mit dem Format "%tH:%tM:%tS"
Beispiel // aktuelle Zeit long zeit = System.currentTimeMillis(); formatter.format("Aktuelle Zeit: %1$tH:%1$tM:%1$tS", zeit));
Ausgabe: Aktuelle Zeit: 11:18:27
Für die Ausgabe der aktuellen Zeit mit Stunden, Minuten und Sekunden werden drei Format-Spezifizierer benötigt. Die Informationen stammen wieder aus nur einem Argument zeit. Weitere Konvertierungszeichen 'b'
Gibt den logischen Wert true oder false aus. Das Argument muss vom Datentyp boolean oder Boolean sein.
'h'
Gibt den Hashcode-Wert eines Objekts aus.
'n'
Das plattformspezifische Zeichen für den Zeilenumbruch wird ausgegeben.
Beispiel boolean bool = false; formatter.format("%nlogischer Wert: %b" + "%nHashcode des Formatter-Objekts: %h", bool, formatter);
Ausgabe: logischer Wert: false Hashcode des Formatter-Objekts: 9304b1
Java 5 Programmierhandbuch
225
Formatierung
Das Konvertierungszeichen 'b' wird für die Ausgabe des booleschen Werts verwendet. Der Format-Spezifizierer %n bewirkt einen Zeilenvorschub. Anschließend erfolgt die Ausgabe des Hashcodes des Formatter-Objekts. Dies wird durch Angabe des Konvertierungszeichens 'h' erreicht. Als Argument ist hier nur die Angabe des Objekts erforderlich.
Die einzelnen Beispiele dieses Abschnitts befinden sich zusammengefasst in der Datei \Beispiel\de\j2sebuch\kap09\ZeichenFormatierung.java. [IconInfo]
9.3.4
Weitere Methoden der Klasse Formatter
Mithilfe der Methode locale() können Sie die Ländereinstellung des Formatter-Objekts ermitteln. Locale locale()
Die Methode out() gibt den Ausgabestring an das Ausgabeziel, z.B. den StringBuilder, aus. Appendable out()
Der Aufruf der Methode close() schließt den Formatter und beendet damit die Verbindung zum Ausgabeobjekt. void close()
9.3.5
Formatierung von Zahlen über die Klasse NumberFormat
Die Formatierung von Zahlen kann auch über die Klasse NumberFormat und die davon abgeleitetete Klasse DecimalFormat erfolgen, welche auch schon in älteren Java-Versionen zur Verfügung stehen. Die Klasse NumberFormat ist eine abstrakte Basisklasse für die Zahlenformatierung. Davon abgeleitet ist die Klasse DecimalFormat, die zur Formatierung von Dezimalzahlen benutzt werden kann. Ein Objekt der Klasse DecimalFormat lässt sich über einen der folgenden Konstruktoren erstellen. DecimalFormat() DecimalFormat(String pattern)
Weitere Möglichkeiten zur Erzeugung eines DecimalFormat-Objekts sind die FactoryMethoden der Klasse NumberFormat. Beispielsweise kann die Methode getNumberInstance() angewendet werden. static NumberFormat getNumberInstance() static NumberFormat getNumberInstance(Locale inLocale)
226
9 – Zeichenkettenverarbeitung
Wird dieser Methode ein Locale-Objekt übergeben, erfolgt die Formatierung in der landesüblichen Darstellung. Beispielsweise kann zur Verwendung der amerikanischen Zahlendarstellung das DecimalFormat-Objekt wie folgt erzeugt werden: DecimalFormat f = (DecimalFormat)NumberFormat.getNumberInstance(Locale.US);
Weitere Informationen zur Klasse Locale finden Sie im Kapitel »Nützliche Klassen«. [IconInfo]
Der Formatierungs-String kann bei der Definition des Objekts oder später über die Methode applyPattern() festgelegt werden. Mit dieser Methode kann der FormatierungsString auch geändert werden. void applyPattern(String pattern)
Ein Formatierungs-String (pattern) besteht aus einer Folge von Zeichen, die das Aussehen der Zahl beschreiben, z.B. die Anzahl der Stellen vor und nach dem Dezimaltrennzeichen und ob Tausendertrennzeichen und führende Nullen auszugeben sind. Folgende Zeichen sind in einem Formatierungs-String zugelassen: Zeichen
Bedeutung
'0'
eine Ziffer
'#'
eine Ziffer oder leer bei führenden Nullen
'.'
Dezimaltrennzeichen
'-'
negatives Vorzeichen
';'
trennt positive und negative Teile des Formatierungs-Strings
','
Gruppierungstrennzeichen
'E'
Exponentialdarstellung für wissenschaftliche Notationen
'%'
Ausgabe als Prozentzahl
'
Ausgabe von Zeichen, die sonst als Konvertierungszeichen interpretiert werden, z.B. '# gibt das Zeichen # aus
Ein Formatierungs-String kann weitere beliebige Zeichen beinhalten, die dann unverändert ausgegeben werden. Soll der Text ein Zeichen besitzen, das als Formatierungszeichen benutzt werden kann, wie beispielsweise ein Komma, ist diesem ein Hochkomma (') voran-
Java 5 Programmierhandbuch
227
Formatierung
zustellen. Haben Sie einen fehlerhaften Formatierungs-String angegeben, wird eine IllegalArgumentException verursacht. Die eigentliche Konvertierung übernimmt die Methode format(). Sie kann mit zwei verschiedenen Parametern aufgerufen werden. Für die Formatierung ganzer Zahlen muss ein long-Wert, für die Formatierung von Dezimalzahlen ein double-Wert übergeben werden. final String format(long number) final String format(double number)
Der Formatierungs-String wurde im DecimalFormat-Objekt definiert, deshalb kann die Methode format() mit diesem Formatierungs-String für beliebig viele Zahlen verwendet werden. DecimalFormat f = new DecimalFormat("#,###,##0.00 Euro"); System.out.println(f.format(83452.47)); System.out.println(f.format(8345223));
Die Zahl 1234,567 wird mithilfe eines DecimalFormat-Objekts in verschiedenen Formaten ausgegeben. Die am Ende dargestellte Ausgabe erhalten Sie bei der Wahl von Deutschland in den Ländereinstellungen Ihres Computers. [IconInfo]
Listing 9.6: \Beispiele\de\j2sebuch\kap09\ZahlenFormatieren.java import java.text.*; public class ZahlenFormatieren { public static void main(String[] args) { double d = 1234.567; System.out.println(new DecimalFormat("#0.0").format(d)); System.out.println(new DecimalFormat("#0.00").format(d)); System.out.println(new DecimalFormat("#0.00000").format(d)); System.out.println( new DecimalFormat("000000.000").format(d)); System.out.println( new DecimalFormat("#,###,##0.000").format(d)); System.out.println(new DecimalFormat("0.000E00").format(d)); System.out.println( new DecimalFormat("#,###,##0.00 Euro").format(d)); } }
228
9 – Zeichenkettenverarbeitung
Ausgabe: 1234,6 1234,57 1234,56700 001234,567 1.234,567 1,235E03 1.234,57 Euro
Java 5 Programmierhandbuch
229
10 Nützliche Klassen 10.1 Datum und Uhrzeit Zur Arbeit mit Datum- und Zeitangaben wird die Klasse Calendar und die davon abgeleitete Klasse GregorianCalendar verwendet. Die abstrakte Klasse Calendar besitzt zahlreiche Methoden, um Datumswerte zu setzen, zu ändern und auszulesen. Weitere Methoden ermöglichen es, mit Datumswerten zu rechnen und Vergleiche auszuführen. Als Datumswerte werden hier nicht nur das Tagesdatum, sondern auch die Uhrzeit betrachtet. Methoden geben häufig den Datumswert oder die Zeit als long-Wert zurück. Diese Zahl nennt die Anzahl der Millisekunden, die seit dem 01.01.1970 00:00:00.000 GMT vergangen sind.
10.1.1
Die Klassen Calendar und GregorianCalendar
Die Klasse GregorianCalendar basiert auf dem in vielen Ländern gültigen gregorianischen Kalender. Sie ist derzeit die einzige von Calendar abgeleitete Klasse. Über einen der sieben verfügbaren Konstruktoren können Sie ein Objekt dieser Klasse erzeugen. Es wird im Folgenden eine Auswahl angegeben. GregorianCalendar(int year, int month, int dayOfMonth) GregorianCalendar(int year, int month, int dayOfMonth, int hourOfDay, int minute, int second) GregorianCalendar() GregorianCalendar(TimeZone zone, Locale aLocale)
Sie können den Konstruktor mit dem aus Jahr, Monat und Tag bestehenden Datum aufrufen. Möchten Sie die Uhrzeit mit einschließen, nutzen Sie die zweite Variante. Ein GregorianCalendar-Objekt mit dem aktuellen Datum wird über den parameterlosen Konstruktor generiert. Bei der letzten Form kann die aktuelle Zeit einer anderen Zeitzone mit den Ländereinstellungen des entsprechenden Lands angegeben werden. Soll eine Anwendung für den internationalen Einsatz erstellt werden, kann ein Calendar-Objekt über die Factory-Methode getInstance() erzeugt werden, welches dann die landesspezifischen Einstellungen des zugrunde liegenden Systems benutzt. [IconInfo]
Calendar c = Calendar.getInstance();
Java 5 Programmierhandbuch
231
Datum und Uhrzeit
Eigenschaften der Klasse GregorianCalendar Die Klasse GregorianCalendar besitzt zahlreiche, größtenteils von der Klasse Calendar geerbte Eigenschaften, welche die einzelnen Bestandteile des Datums, wie z.B. den Tag, den Monat und die Stunde, repräsentieren. Tabelle 10.1: Eigenschaften der Klasse GregorianCalendar Feldname
Bedeutung
Wertebereich
ERA
Zeit vor oder nach Christi Geburt
0, 1 Vergleichswert: BC (before Christ), AD (Anno Domini)
YEAR
Jahr
1 ... 5.000.000
MONTH
Monat Achtung: Monate beginnen hier bei 0 (für Januar)
0 ... 11 Vergleichswert: JANUARY ... DECEMBER
DATE
Tag (Synonym für DAY_OF_MONTH)
1 ... 31
WEEK_OF_YEAR
Kalenderwoche
1 ... 54
WEEK_OF_MONTH
Woche des Monats
1 ... 6
DAY_OF_WEEK_IN_MONTH
Woche im Monat
-1 ... 6
DAY_OF_YEAR
Tag des Jahres
1 ... 366
DAY_OF_MONTH
Tag des Monats (Synonym für DATE)
1 ... 31
DAY_OF_WEEK
Tag der Woche Achtung: Der erste Tag der Woche ist hier der Sonntag
1 ... 7 Vergleichswert: SUNDAY, MONDAY, SATURDAY
AM_PM
Zeit am Vormittag oder Nachmittag
0, 1 Vergleichswert: AM, PM
HOUR_OF_DAY
Stunde des Tages
0 ... 23
HOUR
Stunde
0 ... 12
MINUTE
Minute
0 ... 59
SECOND
Sekunde
0 ... 59
MILLISECOND
Millisekunde
0 ... 999
232
...
10 – Nützliche Klassen Tabelle 10.1: Eigenschaften der Klasse GregorianCalendar (Forts.) Feldname
Bedeutung
Wertebereich
ZONE_OFFSET
Zeitverschiebung von der Zeitzone GMT (Greenwich Mean Time) in ms
–
DST_OFFSET
Sommerzeitverschiebung in ms
–
FIELD_COUNT
Anzahl der Felder
–
Diese Eigenschaften lassen sich mithilfe der get()-Methode lesen und über die set()Methode ändern. int get(int field) void set(int field, int value)
Die Methode set() ist aber auch zum Modifizieren bzw. zum Setzen mehrerer Felder anwendbar. void set(int year, int month, int date) void set(int year, int month, int date, int hourOfDay, int minute) void set(int year, int month, int date, int hourOfDay, int minute, int second)
Der Wertebereich der Eigenschaft MONTH beginnt mit 0. Deshalb muss bei der Festlegung des Monats immer die Zahl 1 abgezogen werden. Beim Lesen des Datums ist entsprechend zum Monat die Zahl 1 zu addieren. [IconInfo]
Zum Beispiel wird das Datum 01.11.2004 wie folgt übergeben: GregorianCalendar gc = new GregorianCalendar(2004, 10, 1);
Für die korrekte Wiedergabe des Datums muss zu dem Wert, den die get()-Methode für den Monat zurückgibt, die Zahl 1 addiert werden. System.out.println("Monat: " + (gc.get(Calendar.MONTH) + 1));
Java 5 Programmierhandbuch
233
Datum und Uhrzeit
[IconInfo]
In diesem Beispielprogramm wird ein GregorianCalendar-Objekt mit den aktuellen Werten für das Datum und die Uhrzeit erzeugt. Verschiedene Eigenschaften dieses Objekts werden ermittelt und auf dem Bildschirm angezeigt. Die Eigenschaft DAY_OF_WEEK liefert einen Zahlenwert zurück, deshalb wird die Methode wochentag() zur Bestimmung des entsprechenden Wochentags aufgerufen.
Listing 10.1: \Beispiele\de\j2sebuch\kap10\DatumZeitAnzeigen.java import java.util.*; public class DatumZeitAnzeigen { public static void main(String[] args) { GregorianCalendar gc = new GregorianCalendar(Locale.GERMANY); System.out.println("Heute ist " + wochentag(gc.get(Calendar.DAY_OF_WEEK)) + ", der " + gc.get(Calendar.DAY_OF_MONTH) + "." + (gc.get(Calendar.MONTH) + 1) + "." + gc.get(Calendar.YEAR) + ", der " + gc.get(Calendar.DAY_OF_YEAR) + " Tage des Jahres in der " + gc.get(Calendar.WEEK_OF_YEAR) + "-ten Kalenderwoche"); System.out.println("aktuelle Uhrzeit: " + gc.get(Calendar.HOUR) + ":" + gc.get(Calendar.MINUTE) + ":" + gc.get(Calendar.SECOND)); System.out.println("Wir haben eine Sommerzeitverschiebung von" + (gc.get(Calendar.DST_OFFSET) / 3600000) + " Stunde(n)"); } private static String wochentag(int tag) { switch(tag) { case Calendar.SUNDAY: return("Sonntag"); case Calendar.MONDAY: return("Montag"); case Calendar.TUESDAY: return("Dienstag"); case Calendar.WEDNESDAY: return("Mittwoch"); case Calendar.THURSDAY: return("Donnerstag"); case Calendar.FRIDAY: return("Freitag"); case Calendar.SATURDAY: return("Samstag"); } return ""; } }
234
10 – Nützliche Klassen
Ausgabe: Heute ist Dienstag, der 27.7.2004, der 209 Tage des Jahres in der 31-ten Kalenderwoche Zeit: 9:28:21 Wir haben eine Sommerzeitverschiebung von 1 Stunde(n)
Die Ausgabe von Datum und Uhrzeit kann komfortabler über die Methode printf() erfolgen, welche die vielen langen Aufrufe der get-Methoden erspart. Ausführliche Informationen zur Formatierung finden Sie im Kapitel zur Zeichenkettenverarbeitung. [IconInfo]
Die Ausgaben des in Listing 10.1 abgedruckten Beispiels können mithilfe der Methode printf() fast ohne Aufrufe der get()-Methode realisiert werden. Allerdings sind nicht für alle Eigenschaften des GregorianCalendar-Objekts Konvertierungszeichen verfügbar. Beispielsweise gibt es für die Eigenschaft WEEK_OF_YEAR kein Konvertierungszeichen. Vorteilhaft ist aber die Möglichkeit, den Wochentag oder den Monat als Zahl, als Wort oder als Abkürzung ausgeben zu lassen, ohne vorher switch-Anweisungen programmieren zu müssen.
Das Programm erzeugt die gleichen Ausgaben wie das in Listing 10.1 abgedruckte Programm. [IconInfo]
Listing 10.2: \Beispiele\de\j2sebuch\kap10\DatumZeitAnzeigenFormatiert.java import java.util.*; public class DatumZeitAnzeigenFormatiert { public static void main(String[] args) { GregorianCalendar gc = new GregorianCalendar(Locale.GERMANY); System.out.printf("Heute ist %1$tA, der %1$td.%1$tm.%1$tY, " + "der %1$tj. Tage des Jahres " + "in der %2$d-ten Kalenderwoche%n", gc, gc.get(Calendar.WEEK_OF_YEAR)); System.out.printf("aktuelle Uhrzeit: %1$tH:%1$tM:%1tS%n", gc); System.out.printf("Wir haben eine Sommerzeitverschiebung von"+ %d Stunde(n)", gc.get(Calendar.DST_OFFSET) / 3600000); } }
Java 5 Programmierhandbuch
235
Datum und Uhrzeit
Ungültige Datumswerte Sind die zugewiesenen Datumswerte ungültig, wie z.B. der 32.06.2004, wird automatisch eine Umwandlung in ein passendes gültiges Datum durchgeführt. Das Datum wird dabei um die überzähligen zwei Tage weitergerückt. Bekommt ein einzelnes Feld eines sonst gültigen Datums einen ungültigen Wert zugewiesen, wird automatisch der nächste gültige Wert gesucht (wie bei einer Wertebereichsüberschreitung der Methode roll()), ohne die anderen Felder zu verändern. gc.set(2004, 5, 32); gc.set(Calendar.DATE, 40);
// => 02.07.2004 // => 10.07.2004
Calendar-Objekte löschen Mit der Methode clear() lässt sich ein im GregorianCalendar-Objekt gespeichertes Datum löschen. Das Datum steht anschließend auf dem Initialwert 01.01.1970 00:00:00. Der Wert eines einzelnen Felds kann durch Übergabe des Feldnamens, z.B. DATE, an die Methode gelöscht werden. void clear() void clear(int field)
Vergleichsmethoden für Calendar-Objekte Für Vergleiche von Calendar-Objekten stellt die Klasse die Methoden before(), equals() und after() bereit. Mit diesen können Sie prüfen, ob ein Datum vor oder nach dem Datum eines anderen Calendar-Objekts liegt oder ob beide gleich sind. boolean before(Object datum) boolean equals(Object datum) boolean after(Object datum)
Ändern von Datumswerten Ein Datum kann mittels der Methode roll() hoch- oder runtergezählt werden. Die Methode steht mit zwei unterschiedlichen Parameterlisten zur Verfügung. void roll(int field, boolean up) void roll(int field, int amount)
Über den ersten Parameter geben Sie das zu verändernde Feld an, beispielsweise DATE oder MONTH. Der zweite Parameter legt mit einem logischen Wert fest, ob das Feld hoch gezählt (true) oder herunter gezählt (false) werden soll. Die Angabe einer Schrittweite (amount) ist auch erlaubt. Sie kann positiv oder negativ sein. Beachten Sie, dass bei der Änderung des Datums mittels roll() nur das Feld andere Werte erhält, welches als erster Parameter übergeben wurde. Befindet sich beispielsweise das Datum auf dem letzten Tag des Monats und der Tag wird hochgezählt, so wird der Tag auf
236
10 – Nützliche Klassen
den Ersten gesetzt und der Monat bleibt gleich. Die Modifizierung des Datums kann auch über die Methode add() erfolgen. void add(int field, int amount)
Der Methode add() wird ebenfalls der Name des Felds und der Wert, um den das Feld geändert werden soll, übergeben. Überschreitet der errechnete neue Feldwert nicht den Wertebereich des Felds, arbeitet die Methode add() wie die Methode roll(). Im Unterschied zu roll() erhalten aber alle betroffenen Felder andere Werte, wenn der Wertebereich des Felds überschritten wird. Beispiele 쐌 Das Datum steht vor dem Hochzählen auf dem letzten Tag des Monats, z.B. auf dem 31.01.2005. Der Tag wird durch das Ausführen der roll()-Methode um die Zahl 1 hochgezählt. GregorianCalendar gc = new GregorianCalendar(2005, 0, 31); gc.roll(Calendar.DATE, true); System.out.printf("%1$td.%1$tm.%1$tY", gc); // 01.01.2005
쐌 Wird das Datum mit der Methode add() hochgezählt, ist auch der Monat betroffen, wenn der Wertebereich überschritten wird. gc.set(2005, 0, 31); gc.add(Calendar.DATE, 1); System.out.printf("%1$td.%1$tm.%1$tY%n", gc);
// 31.01.2005 // 01.02.2005
쐌 Bei dem folgenden Aufruf der Methode add() wird durch das Hochzählen der Stunden auch der Tag, der Monat und das Jahr überschritten. gc.set(2004, 11, 31, 12, 30, 0); // 31.12.2004 12:30 gc.add(Calendar.HOUR, 20); System.out.printf("%1$td.%1$tm.%1$tY %1$tH:%1$tM:%1tS%n", gc); // 01.01.2005 8:30
쐌 In einer Schleife wird das Feld MONTH zwölfmal herunter gezählt. Die Methode roll() modifiziert den Datumswert nicht. gc.set(2004, 5, 1); // 01.06.2004 for(int i = 1; i 2004-08-01T16:43:33 1091371413906 0 INFO java.util.logging.LogManager$RootLogger log 10 Information ...
Die Methoden getHead() und getTail() eines Formatters liefern einen String, der den Kopf und das Ende der Ausgabe kennzeichnet (z.B. sinnvoll für eine HTML-Ausgabe). Die Methode formatMessage() kann zur Lokalisierung der Ausgabe verwendet werden.
[IconInfo]
Möchten Sie den FileHandler verwenden, geben Sie in dessen Konstruktor einen Dateinamen an. Ohne eine Pfadangabe wird die Datei in dem Verzeichnis erstellt, von dem die Anwendung aufgerufen wird. Über die Methode addHandler() wird der neue Handler dem Logger zugeordnet. Wenn Sie die Verwendung der Eltern-Handler nicht über die Methode setUseParantHandlers() deaktivieren, wird auch eine Ausgabe auf der Konsole über den Standard-ConsoleHandler durchgeführt.
Java 5 Programmierhandbuch
735
Handler verwenden Listing 28.3: \Beispiele\de\j2sebuch\kap28\FileHandlerTest.java import java.util.logging.*; import java.io.*; public class FileHandlerTest { public FileHandlerTest() { Logger l = Logger.getLogger(""); // l.setUseParentHandlers(false); FileHandler fh = null; try { fh = new FileHandler("Kap28Log.txt"); l.addHandler(fh); } catch(IOException ioEx) { System.out.println("Konnte Datei nicht anlegen."); } l.log(Level.INFO, "Information"); l.log(Level.WARNING, "Warnung"); } public static void main(String args[]) { new FileHandlerTest(); } }
Eigene Handler und Formatter definieren Um eine eigene Ausgabe und eventuell eine eigene Formatierung zu verwenden, können Sie eigene Handler und Formatter definieren. Ein neuer Formatter kann von der Klasse Formatter abgeleitet werden und überschreibt deren Methoden. Der Formatter wird einem Handler über die Methode setFormatter() der Klasse Handler zugewiesen und über die Methode getFormatter() ausgelesen. Da nur der Handler selbst und gegebenenfalls davon abgeleitete bzw. übergeordnete Klassen den Formatter nutzen, können Sie die Formatierung bei eigenen Handlern auch direkt durchführen. Bei sehr komplexen Formatierungen ist die Definition eines separaten Formatter wiederum sinnvoll. Wenn Sie einen eigenen Handler einsetzen wollen, ist optional die Weiterreichung der LogEinträge an die Eltern-Handler zu deaktivieren. Verwenden Sie die bereits vorgestellte Methode setUseParentHandlers(). Als Vorlage für einen korrekt implementierten Handler können Sie die SourceCodes der Handler des Logging API als Vorlage verwenden (Datei src.zip im JDK-Verzeichnis öffnen und die entsprechende *.java-Datei auswählen). Um einen Handler zu erzeugen, gehen Sie wie folgt vor: 1. Leiten Sie eine neue Klasse von der Klasse Handler oder einer der davon abgeleiteten Klassen ab.
736
28 – Logging
2. Implementieren Sie die Methoden publish() zur Ausgabe (ihr werden die Daten des Log-Eintrags in Form eines LogRecord-Objekts übergeben), flush() zum Leeren des Buffers und close() zum Schließen des Ausgabemediums (z.B. einer Datei). 3. Optional können Sie dem Handler einen Filter zuweisen. 4. Weisen Sie den Handler dem Logger über die Methode addHandler() zu.
[IconInfo]
Die neue Handler-Klasse und das Testprogramm befinden sich beide in derselben Datei. Die Klasse FormatHandler wird von der Klasse Handler erweitert. Der parameterlose Konstruktor kann z.B. dazu verwendet werden, den Formatter zu setzen. Die Methode publish() wird aufgerufen, wenn ein Log-Eintrag ausgegeben werden soll. Der Handler gibt untereinander den Log-Level, die Nachricht sowie die laufende Nummer des Eintrags aus. Über die Methode isLoggable() wird geprüft, ob der Log-Eintrag verarbeitet werden soll. Es wird ermittelt, ob ein Filter existiert und dieser gegebenenfalls aufgerufen. Da die Ausgabe auf das Standardausgabegerät erfolgt, ist die Implementierung der Methoden flush() und close() nicht notwendig. Wenn Sie aber die Daten beispielsweise in eine Datenbank schreiben, können Sie hier die Verbindung schließen.
Listing 28.4: \Beispiele\de\j2sebuch\kap28\FormatHandlerTest.java class FormatHandler extends Handler { public FormatHandler() { // hier könnte ein Formatter gesetzt werden } public void publish(LogRecord lr) { if(isLoggable(lr)) { // hier könnte der Formatter genutzt werden System.out.println("Level: " + lr.getLevel()); System.out.println("Nachricht: " + lr.getMessage()); System.out.println("No: " + lr.getSequenceNumber()); } } public boolean isLoggable(LogRecord lr) { Filter f = getFilter(); if(f != null) return f.isLoggable(lr); else return true; } public void flush() {} public void close() {} }
Java 5 Programmierhandbuch
737
Der LogManager
Der neue Handler wird nun in einem Beispielprogramm verwendet. Damit der Standardhandler nicht benutzt wird, ruft der Logger die Methode setUseParentHandlers() mit dem Parameter false auf. Listing 28.5: \Beispiele\de\j2sebuch\kap28\FormatHandlerTest.java import java.util.logging.*; public class FormatHandlerTest { public FormatHandlerTest() { Logger l = Logger.getLogger("de.j2sebuch.kap28.FormatHandlerTest"); l.setUseParentHandlers(false); FormatHandler fh = new FormatHandler(); l.addHandler(fh); l.log(Level.INFO, "Information"); l.log(Level.WARNING, "Warnung"); } public static void main(String args[]) { new FormatHandlerTest(); } }
28.5 Der LogManager Der LogManager verwaltet eine Hierarchie von Logger-Objekten und die Standardeinstellungen für alle Logger. Er wird automatisch bereitgestellt und ist als Singleton implementiert (es gibt nur eine Instanz). Eine Referenz auf den LogManager kann mit der Methode getLogManager() der Klasse LogManager ermittelt werden. Logger-Hierarchie Die Namen der Logger werden innerhalb einer Hierarchie verwaltet. Der Logger ohne Namen dient als Root-Logger. Die einzelnen Ebenen der Hierarchie werden durch Punkte (wie Package-Namen) getrennt. Der Vorteil dieser Hierarchie besteht darin, dass die Methoden immer auf ganze Zweige der Hierarchie angewendet werden, z.B. beim Setzen des Log-Levels. Wird dieser für den Root-Logger gesetzt, wird die Einstellung für alle anderen Logger übernommen. Die Namen der Logger müssen lediglich durch einen Punkt getrennt sein, um die Hierarchie zu erzeugen. Es ist aber nicht zwingend notwendig, die Package-Struktur der LoggerKlasse zu verwenden. Der Vorteil der Package-Struktur ist wie schon bei der Benutzung in Klassen, dass eindeutige Namen für die Logger entstehen, besonders, wenn mit vielen Klassenbibliotheken gearbeitet wird. Unbenannte Logger (anonyme) haben nur den RootLogger als Eltern-Logger.
738
28 – Logging
""-Root-Logger
com
entwickler
java
javamag
Abb. 28.2: Hierarchie der Logger im LogManager
28.5.1
Konfigurationsdatei
Bei der Initialisierung des LogManagers wird standardmäßig die Konfigurationsdatei unter [InstallJDK]\jre\lib\logging.properties eingelesen. Diese hat den folgenden, um die Kommentare bereinigten Aufbau: handlers= java.util.logging.ConsoleHandler .level= INFO java.util.logging.FileHandler.pattern = %h/java%u.log java.util.logging.FileHandler.limit = 50000 java.util.logging.FileHandler.count = 1 java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter java.util.logging.ConsoleHandler.level = INFO java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter de.j2sebuch.kap28.level = SEVERE
Der seit dem JDK 5.0 verfügbaren Eigenschaft handlers werden Handler durch Leerzeichen getrennt zugewiesen. Diese Handler werden standardmäßig dem Wurzel-Logger mit dem Namen ("") zugewiesen. Durch die aktuelle Einstellung dieser Datei werden demnach die Ausgaben auf der Konsole durchgeführt, wenn kein Handler angegeben wurde, da die Log-Einträge immer an die Eltern-Logger weitergegeben werden. Der Eintrag .level gilt für alle Logger, weil kein Name angegeben wurde. Wollen Sie Logger mit anderen Levels versehen, können diese zusätzlich angegeben werden. Beachten Sie, dass die Angaben der Konfigurationsdatei sequentiell ausgewertet werden und Sie auf diese Weise vorhandene Einstellungen überschreiben. Für die beiden Handler vom Typ FileHandler und ConsoleHandler werden ebenfalls einige Voreinstellungen vorgenommen, wie die Einstellung des Log-Levels und der zu verwendende Formatierer.
Java 5 Programmierhandbuch
739
Der LogManager
Die letzte Zeile stellt lediglich ein Beispiel für eigene Log-Level-Definitionen dar. Die gesetzten Eigenschaften für Logger werden entsprechend der Hierarchie (also dem Namen des Loggers) vererbt. Konfiguration zur Laufzeit einlesen Zur Laufzeit bietet die Klasse LogManager über die folgenden Methoden die Möglichkeit, die Konfiguration wiederherzustellen bzw. diese aus einer speziellen Datei zu laden. public void readConfiguration() // wiederherstellen public void readConfiguration(InputStream ins)
Sie können z.B. die Datei xyz.properties wie im Folgenden angegeben einlesen. Die entsprechenden Eigenschaften der Logger werden dadurch neu initialisiert. LogManager lm = LogManager.getLogManager(); lm.readConfiguration(new FileInputStream("xyz.properties "));
[IconInfo]
Für das folgende Beispiel erstellen Sie eine eigene Konfigurationsdatei LoggingInit.properties, die Handler für die Klasse LogMngTest sowie einen Dateinamen für die Ausgabe des FileHandler vorgibt. Über die Einstellung config wird eine Klasse angegeben, die gestartet wird und weitere Konfigurationseinstellungen vornehmen kann. In der Klasse LogMngTest wird die eigene Konfigurationsdatei über die Methode readConfiguration() eingelesen. Es werden dann einige Log-Einträge erzeugt. In der Klasse LogMngInit wird innerhalb des Standardkonstruktors eine einfache Textausgabe durchgeführt. An dieser Stelle können Sie stattdessen weitere Einstellungen für das Logging vornehmen.
Listing 28.6: \Beispiele\de\j2sebuch\kap28\LoggingInit.properties de.j2sebuch.kap28.LogMngTest.handlers= java.util.logging.ConsoleHandler, java.util.logging.FileHandler java.util.logging.FileHandler.pattern=C:/Temp/Logging.txt java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter de.j2sebuch.kap28.LogMngTest.useParentHandlers=false de.j2sebuch.kap28.LogMngTest.level=FINEST config=de.j2sebuch.kap28.LogMngInit
740
28 – Logging Listing 28.7: \Beispiele\de\j2sebuch\kap28\LogMngTest.java import java.util.logging.*; import java.io.*; public class LogMngTest { public LogMngTest() { try { LogManager.getLogManager().readConfiguration( new FileInputStream( "de/j2sebuch/kap28/LoggingInit.properties")); } catch(IOException ioEx) { System.out.println("Fehler bei der Initialisierung"); } Logger l = Logger.getLogger("de.j2sebuch.kap28.LogMngTest"); l.log(Level.INFO, "Information"); l.log(Level.WARNING, "Warnung"); l.log(Level.FINEST, "nur ne kleine Info"); } public static void main(String args[]) { new LogMngTest(); } } Listing 28.8: \Beispiele\de\j2sebuch\kap28\LogMngInit.java public class LogMngInit { public LogMngInit() { System.out.println("Initialisierung des LogManagers"); } }
Systemeigenschaften für das Logging Beim Start einer Anwendung können weitere Systemeigenschaften für das Logging gesetzt werden. Der Eigenschaft java.util.loggin.config.class kann eine Klasse zugewiesen werden, welche Initialisierungen vornimmt, z.B. das Setzen von Log-Levels oder das Bereitstellen weiterer Handler. Nach der Initialisierung der Klasse wird deren Defaultkonstruktor ausgeführt. Wurde diese Eigenschaft nicht gesetzt, kann über java.util.logging.config.file eine andere Konfigurationsdatei angegeben werden. Wurden beide Eigenschaften nicht ge-
Java 5 Programmierhandbuch
741
Filter verwenden
setzt, liest der LogManager die bereits vorgestellte Initialisierungsdatei ein. Die Eigenschaften können z.B. folgendermaßen beim Aufruf des Interpreters übergeben werden: java -Djava.util.logging.config.file=MeineLogKonfig.properties
28.6 Filter verwenden Durch die Verwendung eines Filters können Sie die zu protokollierenden Nachrichten wesentlich gezielter auswählen. Über den Log-Level lässt sich nur eine untere Grenze für den Level festlegen. Filter ermöglichen es, Einschränkungen für alle Bestandteile eines LogEintrags festzulegen. Für jeden Logger und Handler kann maximal ein Filter gesetzt werden. Standardmäßig werden keine Filter benutzt. Zur Verwendung eines Filters gehen Sie folgendermaßen vor: 1. Leiten Sie eine neue Klasse von der Klasse Filter ab. 2. Überschreiben Sie die Methode isLoggable(). Dieser wird ein LogRecord-Objekt übergeben, dessen Werte Sie auswerten können. Ist der Rückgabewert dieser Methode true, wird der Eintrag geloggt, sonst nicht. 3. Um den Filter für einen Logger oder Handler zu setzen, verwenden Sie deren Methode setFilter(), der Sie das Filter-Objekt übergeben. Ein erneuter Aufruf überschreibt den alten Filter. Übergeben Sie der Methode den Wert null, wird der Filter deaktiviert bzw. kein Filter verwendet.
[IconInfo]
Die Klasse InfoFilter am Ende des Listings implementiert das Interface Filter. Es besteht nur aus der Methode isLoggable(), die true zurückgibt, wenn der Log-Eintrag verarbeitet werden soll, sonst false. Der Filter im Beispiel lässt nur die Log-Einträge durch, die vom Level INFO sind. Der Logger setzt den Filter über die Methode setFilter(). Auf die gleiche Weise können Sie auch einem Handler einen Filter zuweisen. Existiert bereits ein Filter, wird dieser überschrieben. Es ist auch möglich, den Filter über eine anonyme Klasse zu implementieren. Diese wird in der Methode setFilter() definiert.
Listing 28.9: \Beispiele\de\j2sebuch\kap28\LogFilter.java import java.util.logging.*; public class LogFilter { public LogFilter() { Logger l = Logger.getLogger("de.j2sebuch.kap28"); l.setFilter(new InfoFilter()); /* // oder über eine anonyme Klasse l.setFilter(new Filter()
742
28 – Logging Listing 28.9: \Beispiele\de\j2sebuch\kap28\LogFilter.java (Forts.) { public boolean isLoggable(LogRecord lr) { return (lr.getLevel() == Level.INFO); } }); */ l.log(Level.SEVERE, "Grober Fehler"); l.log(Level.WARNING, "Warnung"); l.log(Level.INFO, "Information"); } public static void main(String args[]) { new LogFilter(); } } class InfoFilter implements Filter { public boolean isLoggable(LogRecord lr) { return (lr.getLevel() == Level.INFO); } }
Wie bereits beschrieben, können Sie nur einen Filter pro Logger und Handler zuordnen. Durch eine geschickte Implementierung der Methode isLoggable() können aber auch Filterketten definiert werden, die dabei durchlaufen werden. [IconInfo]
28.7 Log4j Da das Logging API erst seit dem JDK 1.4 zur Verfügung steht, verwenden viele das Logging API log4j der Apache Group, das es schon früher gab. Log4j besitzt außerdem ein wesentlich umfangreicheres API, das kaum Wünsche offen lässt. Aus diesen Gründen soll kurz eine Einführung hierfür gegeben werden. Zu beziehen ist Log4j unter der URL http://logging.apache.org/. Die aktuelle Version 1.2.8 können Sie als ZIP-Datei jakarta-log4j-1.2.8.zip mit einer Größe von ca. 3 MB laden. Entpacken Sie die Datei in ein beliebiges Verzeichnis und kopieren Sie die Datei ..\dist\lib\log4j-1.2.8.jar in die Verzeichnisse ..\jre\lib\ext der JDK- und JRE-Installation. Die wichtigsten Klassen und Interfaces befinden sich im Package org.apache.log4j.
Java 5 Programmierhandbuch
743
Log4j
[IconInfo]
Die Arbeitsweise des Programms entspricht fast der des Logging API (oder umgekehrt?!). Ein Logger wird wie bisher über die Methode getLogger() erzeugt. Anschließend wird die Konfiguration initialisiert und es werden alle Handler (hier Appender) entfernt. Dem Logger wird für die Ausgabe auf der Konsole ein ConsoleAppender hinzugefügt, der seine Ausgaben im HTML-Format durchführen soll. Die Log-Einträge werden wieder durch entsprechende Methoden erzeugt.
Listing 28.10: \Beispiele\de\j2sebuch\kap28\LogLog4j.java import org.apache.log4j.*; public class LogLog4j { public LogLog4j() { Logger l = Logger.getLogger(""); BasicConfigurator.configure(); l.removeAllAppenders(); l.addAppender(new ConsoleAppender(new HTMLLayout())); l.debug("Debug-Info"); l.info("Einfache Information"); } public static void main(String[] args) { new LogLog4j(); } }
744
29 Preferences 29.1 Einführung Viele Anwendungen müssen bestimmte Konfigurationsdaten dauerhaft speichern und beim erneuten Start wieder einlesen. Bisher wurden dazu verschiedene Wege beschritten. Solche Daten lassen sich in individuellen Datenbanken, XML-Dateien, Binärdateien oder *.properties-Dateien speichern. Dabei können mehrere Probleme auftreten. Der Speicherort der Daten ist nicht genau definiert und der Zugriff darauf nicht standardisiert. Dies kann z.B. beim Zugriff verschiedener Anwendungen auf diese Daten ungünstig sein. Bei der Verwendung von Dateipfaden müssen die verschiedenen Konventionen der Betriebssysteme beachtet werden (Wurzelverzeichnis, Trennzeichen zwischen den Pfadangaben usw.). Seit dem JDK 1.4 liegt nun das Preferences API vor, dessen Klassen und Interfaces sich im Package java.util.pref befinden. Über das API soll die Verwaltung von Konfigurationsdaten bzw. Programmeinstellungen vereinfacht werden. Das API hat die folgenden Eigenschaften: 쐌 Die Verwaltung der Einstellungen erfolgt innerhalb einer Hierarchie, die wie ein Verzeichnisbaum aufgebaut ist. 쐌 In den Hierarchieebenen werden die Einstellungen als Name-Wert-Paare gespeichert. 쐌 Es erfolgt eine Trennung zwischen Benutzer- und Systemeinstellungen. 쐌 Eine Ereignisunterstützung benachrichtigt Sie bei Änderungen, die z.B. durch andere Anwendungen durchgeführt wurden. 쐌 Die Einstellungen können direkt im XML-Format ex- und importiert werden. 쐌 Das API dient nicht dazu, größere Datenmengen zu speichern, um zum Beispiel eine Datenbank zu ersetzen. Für diese Aufgabe ist es zu ineffizient und es gibt auch gewisse Einschränkungen. System- und Benutzereinstellungen Innerhalb des Preferences API wird zwischen System- und Benutzereinstellungen unterschieden. Während erstere für alle Anwendungen lesend und schreibend verfügbar sind, kann auf die benutzerdefinierten Einstellungen nur vom aktuell angemeldeten Benutzer zugegriffen werden. So können über systemweite Einstellungen z.B. Pfadangaben, die für mehrere Java-Anwendungen gelten, verwaltet werden. Das folgende Beispiel fügt in der Wurzel der Preferences-Hierarchie jeweils in den Benutzer- und Systemeinstellungen ein neues Name-WertPaar ein. Anschließend wird der Wert aus den Systemeinstellungen ausgelesen und ausgegeben. [IconInfo]
Java 5 Programmierhandbuch
745
Einführung Listing 29.1: \Beispiele\de\j2sebuch\kap29\Einstellungen.java import java.util.prefs.*; public class Einstellungen { public Einstellungen() { Preferences prefUser = Preferences.userRoot(); prefUser.put("Name", "Wert"); Preferences prefSystem = Preferences.systemRoot(); prefSystem.put("Name", "Wert"); System.out.println(prefSystem.get("Name", "Standardwert")); } public static void main(String args[]) { new Einstellungen(); } }
Speicherort der Einstellungen Der Speicherort der Einstellungen wird im API als Backing Store bezeichnet. Grundsätzlich ist die Kenntnis seiner Lage für den Programmierer nicht notwendig, weil dies betriebssystemabhängig unterschiedlich implementiert wird. Dies ist ja auch der Vorteil des Preferences API, dass Sie sich nicht um die konkrete Ablage der Daten kümmern müssen. Als Speicherorte kommen z.B. das Dateisystem, die Registry unter Windows oder eine Datenbank in Frage. In jedem Fall wird sichergestellt, dass die Daten persistent, d.h. dauerhaft an dieser Stelle gespeichert werden. Unter Windows und Linux finden Sie die Ablageorte wie in der folgenden Tabelle dargestellt. Betriebssystem/Type
Speicherort
Windows/Benutzerdaten
HKEY_CURRENT_USER\Software\JavaSoft\Prefs
Windows/Systemdaten
HKEY_LOCAL_MACHINE\SOFTWARE\JavaSoft\Prefs
Linux/Benutzerdaten
Unter dem HOME-Verzeichnis des Benutzers werden zwei weitere Verzeichnisse .java/.userPrefs/ erstellt. Darin werden die Preferences als XML-Datei hinterlegt.
Linux/Systemdaten
Im Verzeichnis /etc/.java/.systemPrefs werden weitere Dateien verwaltet.
In den Fällen, in denen die Preferences in Verzeichnissen gespeichert werden, kann das Wurzelverzeichnis über die Systemeigenschaften java.util.prefs.systemRoot und java.util.prefs.userRoot für die System- und die Nutzerdaten ermittelt oder festgelegt werden. Unter Windows haben diese z.B. den Wert null, weil die Daten in der Registry gespeichert werden und Sie den Speicherort dort nicht ändern können.
746
29 – Preferences
Manuelle Verwaltung des Speicherorts Wenn Sie die Verwaltung des Speicherorts selbst in die Hand nehmen möchten, ist dies ebenfalls möglich. Die Implementierung ist etwas umfangreicher, da Sie die Verwaltung der Preferences sowie das Speichern und Laden vollständig selbst implementieren müssen. Gehen Sie in diesem Fall folgendermaßen vor: 쐌 Leiten Sie eine Klasse von der abstrakten Klasse PreferencesFactory ab und implementieren Sie deren abstrakte Methoden. 쐌 Die Methoden geben jeweils ein Preferences-Objekt zurück. Deshalb müssen Sie eine weitere Klasse von der abstrakten Klasse Preferences ableiten und die entsprechenden Methoden implementieren. Objekte dieser Klasse werden von den Methoden der Klasse PreferencesFactory zurückgegeben. 쐌 Setzen Sie die Systemeigenschaft java.util.prefs.PreferencesFactory auf den Namen der neuen Factory-Klasse. Zugriff auf die Benutzer- und Systemeinstellungen herstellen Wenn Sie mit Preferences arbeiten, benötigen Sie lediglich ein Objekt vom Typ der gleichnamigen Klasse. Die Klasse Preferences besitzt dazu zwei Factory-Methoden, über welche die entsprechenden Objekte, jeweils für die Benutzer- und Systemeinstellungen, erzeugt werden. Die erste Methode liefert ein Preferences-Objekt, das auf die Wurzel der Systemeinstellungen verweist, während die zweite Methode dies für die Benutzereinstellungen durchführt. static Preferences systemRoot() static Preferences userRoot()
Wo sich die Wurzel der Einstellungen befindet und wie diese gespeichert werden, wird durch das Preferences API, wie bereits erwähnt, verborgen.
29.2 Speichern und Laden von Einstellungen Besitzen Sie ein Preferences-Objekt, können Sie Einstellungen speichern und laden. Einstellungen sind Name-Wert-Paare. Im Unterschied zu *.properties-Dateien, die nur Strings verwalten, unterstützen Preferences alle primitiven Datentypen, Strings und Bytearrays. Für jeden Datentyp stehen lesende und schreibende Methoden zur Verfügung. Bei den lesenden Methoden wird im ersten Parameter der Name der Einstellung angegeben und im zweiten Parameter ein Standardwert, falls eine Einstellung mit diesem Namen nicht existiert bzw. der Speicherort der Einstellungen nicht verfügbar ist. Somit ist sichergestellt, dass Ihre Anwendung immer mit gültigen Werten arbeitet. Bei den schreibenden Methoden ist zu beachten, dass nicht vorhandene Einstellungen neu angelegt und bereits existierende überschrieben werden.
Java 5 Programmierhandbuch
747
Speichern und Laden von Einstellungen String get(String name, String standardWert) boolean getBoolean(String name, boolean standardWert) byte[] getByteArray(String name, byte[] standardWert) double getDouble(String name, double standardWert) float getFloat(String name, float standardWert) int getInt(String name, int standardWert) long getInt(String name, long standardWert)
Entsprechend existieren Methoden zum Anlegen bzw. Überschreiben von Werten. Die Namen der Einstellungen können Sonderzeichen enthalten und unterscheiden zwischen Groß- und Kleinschreibung. Namen dürfen jedoch nicht das Zeichen / oder den Wert null besitzen. void put(String name, String wert) void putBoolean(String name, boolean wert)
Um eine Einstellung zu löschen, verwenden Sie die Methode remove() und übergeben den Namen der Einstellung. void remove(String name)
Zum Löschen aller Einstellungen der aktuellen Ebene nutzen Sie die Methode clear(). void clear()
Das Preferences API sichert, dass vorgenommene Einstellungen sofort für alle Anwendungen verfügbar sind. Der tatsächliche Schreibvorgang auf den Hintergrundspeicher kann aber asynchron erfolgen. Um das Schreiben der Einstellungen explizit durchzuführen, rufen Sie die Methode flush() auf. Bei einem normalen Programmablauf ist der Aufruf jedoch nicht notwendig. void flush()
Möchten Sie alle Namen der Einstellungen der aktuellen Ebene ermitteln, verwenden Sie die Methode keys(). Die Namen werden als String-Array zurückgegeben. String[] keys()
Test des Backing Store Normalerweise sollte die Verfügbarkeit des Backing Store immer gegeben sein. Muss Ihre Anwendung dessen hundertprozentige Verfügbarkeit sicherstellen, können Sie einen Test über den folgenden Code durchführen. Ist er nicht verfügbar, wird eine BackingStoreException ausgelöst.
748
29 – Preferences Preferences pref = Preferences.systemRoot(); try { pref.put("Test", "Testwert"); String s = pref.get("Test", "Standardwert"); pref.flush(); } catch(BackingStoreException bsEx) {... }
Einschränkungen Für die Vergabe von Namen für die Einstellungen und die Größe des zugeordneten Werts gibt es bestimmte Längenbeschränkungen. Diese werden durch die Konstanten Preferences.MAX_KEY_LENGTH und Preferences.MAX_VALUE_LENGTH
vorgegeben und betragen für die Länge eines Namens 80 Zeichen und die des Inhalts eines Strings oder Bytearrays 8192 Zeichen. Beachten Sie, dass hier die tatsächliche Länge der Zeichen gemeint ist. Dies ist insofern von Bedeutung, da Strings in Unicode kodiert werden (2 Byte pro Zeichen) und Bytearrays ebenfalls eine Kodierung verwenden, die nicht der konkreten Länge des Inhalts entspricht. Objekte speichern Zum Speichern von Objekten gibt es keine Methode im Preferences API. Sollte dies einmal notwendig sein, können Sie zwei Wege gehen. Entweder Sie speichern die Daten des Objekts manuell mit einer separaten Methode oder Sie verwenden einen ObjectOutputStream und speichern das Objekt in einem Bytearray. Beachten Sie in jedem Fall die maximal mögliche Länge des Streams bei der Objektserialisierung.
29.3 Zugriff auf die Hierarchie Das Speichern von Einstellungen in der Wurzel der Benutzer- oder Systemeinstellungen ist sicher nicht optimal, da verschiedene Anwendungen gleiche Namen verwenden können. Aus diesem Grund wird die Verwendung einer Hierarchie unterstützt, die ähnlich dem Dateisystem oder den Package-Namen aufgebaut ist. Die Elemente dieser Hierarchie werden als Knoten bezeichnet. Unterhalb der Knoten werden die Name-Wert-Paare verwaltet. Für die Vergabe der Namen der Knoten existieren die gleichen Konventionen wie für die Namen der Einstellungen.
Abb. 29.1: Hierarchie von Knoten
Java 5 Programmierhandbuch
749
Zugriff auf die Hierarchie
Einen Knoten öffnen Ein Preferences-Objekt arbeitet immer mit genau einem Knoten. Wenn Sie über eine der Methoden der Klasse Preferences ein Preferences-Objekt zurückerhalten, ist dies mit genau einem Knoten verknüpft. Über die folgenden Methoden können Sie einen anderen Knoten auswählen. Es können relative oder absolute Pfadangaben angegeben werden. Absolute Pfade beginnen mit einem Slash /. Alle anderen Pfadangaben sind relative Angaben. Im Gegensatz zu Verzeichnispfaden, in denen auch in übergeordnete Verzeichnisse gewechselt werden kann (z.B. über ..), ist dies hier nicht möglich. Einzelne Bestandteile eines Pfads zu einem Knoten werden ebenfalls durch einen Slash getrennt. Der Wurzelknoten hat keinen Namen bzw. er wird durch den leeren String "" gekennzeichnet. Die allgemeinste Methode zur Navigation zu einem bestimmten Knoten ist die Methode node(). Als Parameter wird der Name des gewünschten Knotens angegeben, mit dem das zurückgegebene Preferences-Objekt arbeiten soll. Existiert der Knoten nicht, wird er angelegt. Preferences node(String name)
Beispiele Die Variable pref1 wird zuerst mit dem Wurzelknoten verbunden und dann sofort über die Methode node() und den relativen Pfad de/j2sebuch auf einen anderen Knoten gesetzt. Ausgehend von diesem Knoten wird die Variable pref2 auf den Kindknoten kap29 gesetzt, so dass deren absoluter Pfad jetzt /de/j2sebuch/kap29 ist. Sie können natürlich in der Methode node() auch einen absoluten Pfad angeben, wie dies im dritten Beispiel erfolgt. Preferences pref1 = Preferences.userRoot().node("de/j2sebuch"); Preferences pref2 = pref1.node("kap29"); Preferences pref3 = pref2.node("/de/j2sebuch/kap29");
Wenn Sie direkt den Knoten des aktuellen Package für die Benutzer- oder Systemeinstellungen öffnen möchten, verwenden Sie die folgenden Methoden. Obwohl Sie den vollständigen Klassennamen angeben, wird nur der Package-Name benutzt. Preferences systemNodeForPackage(Class klassenName) Preferences userNodeForPackage(Class klassenName)
Beispiele Den Klassennamen des aktuellen Objekts ermitteln Sie am einfachsten über die Methode getClass(). Das Resultat übergeben Sie einer der beiden vorgestellten Methoden. Alternativ können Sie ein Class-Objekt auch über die Methode forName() erzeugen. Preferences pref = systemNodeForPackage(this.getClass()); Preferences pref = userNodeForPackage(Class.forName("de.j2sebuch.kap29.Test"));
750
29 – Preferences Möchten Sie die angegebenen Methoden in statischen Methoden nutzen, existiert noch kein Objekt der Klasse. Verwenden Sie stattdessen die Variable class einer Klasse, um den Klassennamen zu bestimmen, z.B. [IconInfo]
[IconInfo]
Preferences p = userNodeForPackage(Klassenname.class);
Wenn Sie Knoten, Namen und Werte unter Windows erzeugen bzw. speichern, werden diese in der Registry verwaltet. Bei der Schreibweise im Preferences API wird die Groß- und Kleinschreibung beachtet, in der Registry nicht. Aus diesem Grund wird vor Großbuchstaben ein Slash eingefügt. Dieser Slash hat hier nicht die Bedeutung des Pfadtrennzeichens wie im Preferences API.
Knoten bearbeiten Über ein Preferences-Objekt können Sie bestimmte Informationen zum aktuellen Knoten in der Hierarchie ermitteln. Die folgende Methode liefert alle Unterknoten als StringArray. String[] childrenNames()
Wenn Sie nicht wissen, ob ein bestimmter Knoten existiert, können Sie dies mit der Methode nodeExists() prüfen. Sie können relative und absolute Pfade übergeben. boolean nodeExists(String pfad)
Den absoluten Pfad zum aktuellen Knoten liefert die Methode absolutePath() als String. String absolutePath()
In einigen Fällen können Sie anhand des Preferences-Objekts nicht unterscheiden, ob es sich um Benutzer- oder Systemeinstellungen handelt. Die folgende Methode liefert true, wenn es sich um einen Knoten in den Benutzereinstellungen handelt, sonst false. boolean isUserNode()
Es wird über die Methode name() der relative Pfad (Name) des Knotens geliefert. String name()
Letztendlich können Sie mit der folgenden Methode den aktuellen Knoten löschen. Beachten Sie, dass Sie jetzt z.B. keine neuen Einstellungen mehr unter diesem Knoten speichern können. Der Wurzelknoten kann nicht gelöscht werden. void removeNode()
Java 5 Programmierhandbuch
751
Zugriff auf die Hierarchie
Das Preferences API bietet keine Methoden, um einen Knoten umzubenennen oder zu verschieben. Dies kann jedoch durch das Anlegen neuer Knoten und das Löschen überflüssiger Knoten erfolgen. [IconInfo]
Im folgenden Beispiel werden verschiedene Methoden zum Verzweigen auf die Knoten der Hierarchie gezeigt. Beachten Sie beim Löschen des aktuellen Knotens, dass Sie vor dem Aufruf bestimmter Methoden, wie z.B. nodeExists(), dem Preferences-Objekt wieder einen vorhandenen Knoten zuweisen.
[IconInfo]
Listing 29.2: \Beispiele\de\j2sebuch\kap29\KnotenZugriff.java import java.util.prefs.*; public class KnotenZugriff { public KnotenZugriff() { Preferences prefU = Preferences.userRoot().node("de/j2sebuch"); prefU = prefU.node("kap29/KnotenZugriff"); System.out.println(prefU.name()); System.out.println(prefU.absolutePath()); prefU = Preferences.userNodeForPackage(KnotenZugriff.class); System.out.println(prefU.absolutePath()); if(prefU.isUserNode()) System.out.println("Benutzereinstellungen..."); try { prefU.removeNode(); prefU = Preferences.userRoot(); if(! prefU.nodeExists("/de/j2sebuch/kap29/KnotenZugriff")) System.out.println("Knoten erfolgreich entfernt!"); } catch(BackingStoreException bsEx) {} } public static void main(String args[]) { new KnotenZugriff(); } }
752
29 – Preferences
29.4 Reagieren auf Änderungen Der Zugriff auf die Preferences ist nicht zwingend nur auf eine Anwendung begrenzt. So können mehrere Anwendungen bestimmte Konfigurationsdaten über Preferences gemeinsam nutzen. Diese können sich sowohl in den Benutzer- als auch den Systemeinstellungen befinden. Damit die Anwendungen gegenseitig über Änderungen informiert werden, lassen sich Listener registrieren, die beim Ändern von Werten und Knoten aufgerufen werden. Um beim Erzeugen oder Löschen von Einstellungen und Wertänderungen informiert zu werden, kann über die Methode addPreferenceChangeListener() der Klasse Preferences ein Objekt vom Typ des Interfaces PreferenceChangeListener registriert werden. Dazu müssen Sie die Methode preferenceChange() implementieren. interface PreferenceChangeListener void addPreferenceChangeListener(PreferenceChangeListener pcl) void preferenceChange(PreferenceChangeEvent evt)
Werden Knoten neu eingefügt oder entfernt, kann ebenfalls ein Listener über die Methode addNodeChangeListener() hinzugefügt werden. Übergeben wird ihr ein Objekt vom Typ des Interfaces NodeChangeListener. Das Interface enthält die Methoden childAdded() und childRemoved(), die aufgerufen werden, wenn ein Knoten unterhalb des aktuellen Knotens hinzugefügt bzw. entfernt wurde. interface NodeChangeListener void addNodeChangeListener(NodeChangeListener ncl) void childAdded(NodeChangeEvent evt) void childRemoved(NodeChangeEvent evt)
Zum Entfernen eines Listener rufen Sie die Methode removeNodeChangeListener() bzw. removePreferenceChangeListener() auf.
[IconInfo]
Um bei Änderungen in den Einstellungen und beim Anlegen bzw. Löschen von Knoten informiert zu werden, registriert die Anwendung die beiden Listener über die entsprechenden Methoden. Die Interfaces werden durch anonyme Klassen implementiert. Zum Abschluss werden einige Änderungen durchgeführt, die auf der Konsole protokolliert werden.
Listing 29.3: \Beispiele\de\j2sebuch\kap29\EinstellungenInfo.java import java.util.prefs.*; public class EinstellungenInfo { public EinstellungenInfo() { Preferences prefUser = Preferences.userRoot(); prefUser = prefUser.node("/de/j2sebuch/kap29"); prefUser.addPreferenceChangeListener(
Java 5 Programmierhandbuch
753
Preferences exportieren und importieren Listing 29.3: \Beispiele\de\j2sebuch\kap29\EinstellungenInfo.java (Forts.) new PreferenceChangeListener() { public void preferenceChange(PreferenceChangeEvent evt) { System.out.println("Aenderungen Name/Wert-Paar"); System.out.println(evt.getNode().absolutePath()); System.out.println(evt.getKey()); System.out.println(evt.getNewValue()); } }); prefUser.addNodeChangeListener( new NodeChangeListener() { public void childAdded(NodeChangeEvent nce) { System.out.println("Neuer Knoten"); System.out.println(nce.getParent().absolutePath()); } public void childRemoved(NodeChangeEvent nce) { System.out.println("Knoten geloescht"); System.out.println(nce.getParent().absolutePath()); } }); prefUser.put("Test", "Wert"); prefUser.remove("Test"); prefUser.node("new"); } public static void main(String args[]) { new EinstellungenInfo(); } }
29.5 Preferences exportieren und importieren Möchten Sie die gemachten Einstellungen auf einfache Weise exportieren, importieren oder weiterverarbeiten, können Sie die folgenden Methoden nutzen. Sie verwenden zur Verwaltung der Eigenschaften das XML-Format. Die Daten werden über Streams bereitgestellt, so dass der Ex- bzw. Import in eine Datei sehr einfach realisiert werden kann. Über die export-Methoden werden die Einstellungen des aktuellen Knotens bzw. auch die seiner untergeordneten Knoten exportiert. void exportNode(OutputStream os) void exportSubtree(OutputStream os)
754
29 – Preferences
Beim Import werden bereits vorhandene Einstellungen überschrieben. Die Einstellungen werden in die durch das aufrufende Preferences-Objekt definierten Benutzer- oder Systemeinstellungen eingefügt. Benötigte Knoten werden erstellt. void importPreferences(InputStream is)
Die Anwendung speichert den gesamten Baum ab dem Knoten /com der Benutzereinstellungen in eine Datei Backup.xml. Beim Zugriff auf die Dateien und den Backing Store können Fehler auftreten, deshalb müssen die entsprechenden Exceptions abgefangen werden. [IconInfo]
Listing 29.4: \Beispiele\de\j2sebuch\kap29\Export.java import java.io.*; import java.util.prefs.*; public class Export { public Export() { try { Preferences prefUser = Preferences.userRoot(); prefUser = prefUser.node("/com"); prefUser.exportSubtree(new FileOutputStream( new File("de/j2sebuch/kap29/Backup.xml"))); } catch(IOException ioEX) {} catch(BackingStoreException bsEX) {} } public static void main(String args[]) { new Export(); } }
Die erzeugte XML-Datei hat den folgenden Aufbau. Über das Element und dessen Attribut type wird festgehalten, ob es sich um Benutzer- oder Systemeinstellungen in der XML-Datei handelt.
Java 5 Programmierhandbuch
755
Preferences exportieren und importieren Listing 29.5: Aufbau der exportierten XML-Datei
756
30 Threads 30.1 Einführung Bei der Ausführung einer Anwendung wird ein so genannter Prozess gestartet. Die meisten aktuellen Betriebssysteme unterstützen die gleichzeitige Ausführung mehrerer Prozesse. Diese Fähigkeit wird Multitasking genannt. Besitzen Sie in Ihrem Rechner nur einen Prozessor, werden die Prozesse quasiparallel (auch als nebenläufig bezeichnet) abgearbeitet. Das Betriebssystem weist den Prozessen nacheinander eine bestimmte Rechenzeit zu, bevor der nächste Prozess an der Reihe ist. Ein Prozess kann wiederum aus mehreren Threads (Programmfäden) bestehen. Der Hauptthread wird mit dem Start des Prozesses, z.B. einer Java-Anwendung, ausgeführt. Mithilfe von Threads wird ein Prozess in mehrere, parallel ablaufende Programmteile unterteilt. Die Fähigkeit eines Betriebssystems, mehrere Threads innerhalb eines Prozesses zu unterstützen, wird Multithreading genannt. Threads werden als leichtgewichtige Prozesse bezeichnet, da der Aufwand für deren Verwaltung geringer als der für einen Prozess ist. Wenn Threads nicht durch das Betriebssystem unterstützt werden, übernimmt die Java Virtual Machine diese Aufgabe. Prozesse (die Hauptthreads) und weitere Threads werden durch das Betriebssystem wechselseitig ausgeführt. Wer wann wie viel Rechenzeit erhält, regelt ein Scheduler. Auf diese Weise werden Threads immer im Wechsel gestartet und wieder beendet, vgl. Abbildung 30.1. Bei der Zuteilung der Rechenzeit müssen einerseits die Threads einer Anwendung, andererseits alle laufenden Anwendungen berücksichtigt werden. Thread 1
Hauptprogramm
Thread 2
Abb. 30.1: Rechenzeitzuteilung für Threads einer Anwendung
Anwendungsfälle Threads können z.B. dort eingesetzt werden, wo ein Programm auf Eingaben durch den Benutzer wartet oder auf Daten, die über einen anderen Prozess oder über das Netzwerk bereitgestellt werden. Damit in diesen Fällen nicht die gesamte Anwendung warten muss, wird die Kommunikation mit den Anwendern oder dem Netzwerk in ein oder mehrere Threads verlagert. Dadurch kann das Hauptprogramm mit seiner Ausführung fortfahren.
Java 5 Programmierhandbuch
757
Threads über die Klasse Thread erzeugen
Wenn Sie beispielsweise mehrere Dateien gleichzeitig aus dem Internet laden, kann dies durch mehrere parallele Threads erfolgen. In einer Client-Server-Anwendung kann ein Server über Threads mehrere Clients separat verwalten. Die größte Zeitverschwendung für den Prozessor ist das Warten auf Benutzereingaben. Ob es sich dabei um eine Textverarbeitung oder die Eingabe von Daten in einer Datenbankanwendung handelt, meistens langweilt sich der Prozessor. Diese Zeit kann von nebenläufigen Threads zur Durchführung anderer Aufgaben genutzt werden, z.B. für die Ausführung eines Virenscanners im Hintergrund. Threads in Java Java erlaubt die Erzeugung von Threads durch das Ableiten einer neuen Klasse von der Klasse Thread oder das Implementieren des Interfaces Runnable. Beide befinden sich im Package java.lang, so dass sie ohne weitere import-Anweisungen sofort zur Verfügung stehen. In beiden Fällen müssen Sie eine Methode run() implementieren, die beim Start des Threads ausgeführt wird. Klasse Thread
Interface Runnable
Vorteil
Sie erben die Methoden der Klasse Thread.
Eine Klasse besitzt nun immer noch die Möglichkeit, von einer anderen Klasse abgeleitet zu werden.
Nachteil
Sie können nicht noch von einer weiteren Klasse ableiten.
Der Start eines Threads ist etwas umständlicher und die Methoden der Thread-Klasse müssen über ein Hilfsobjekt gestartet werden.
[IconInfo]
Die Beispielanwendungen dieses Kapitels werden nicht in jedem Fall automatisch beendet. Dies hängt damit zusammen, dass eine Anwendung nicht beendet wird, solange noch Threads (genauer gesagt User-Threads) laufen. Beenden Sie die Anwendungen auf der Konsole durch das Betätigen von Ÿ + C. Weiterhin kann es unter Linux beim Ausführen der Beispiele zu Problemen kommen, da der Scheduler die Threads nicht wie unter Windows behandelt.
30.2 Threads über die Klasse Thread erzeugen Die einfachste Variante einen Thread zu erzeugen, besteht im Erweitern der Klasse Thread. Es muss nur noch die Methode run() überschrieben werden, die in der Klasse Thread selbst keine Funktionalität besitzt. Danach wird ein Objekt vom Typ der ThreadKlasse erzeugt und dessen Methode start() aufgerufen, die es von der Klasse Thread geerbt hat. Die Methode start() initialisiert ihrerseits den Thread und ruft die Methode
758
30 – Threads run() auf. Beachten Sie, dass der direkte Aufruf von run() nicht wie erwartet zur Ausführung eines neuen Threads, sondern nur zu einem normalen sequentiellen Methodenaufruf führt. Es fehlt in diesem Fall die Initialisierung des Threads, die speziell in der Methode start() durchgeführt wird.
Die Klasse Thread ist Bestandteil des Package java.lang, deshalb benötigen Sie keine import-Anweisung. Zum Erstellen eines Threads über eine Thread-Klasse sind die beiden folgenden Konstruktoren zu verwenden. Während der erste einen unbenannten Thread erzeugt, können Sie im zweiten Konstruktor einen Namen für den Thread vergeben. Thread() Thread(String name)
Zum Starten eines Threads wird die Methode start() aufgerufen. Die Methode run() müssen Sie überschreiben, um die Funktionalität des Threads zu implementieren. void start() void run()
Der aktuelle Thread kann jederzeit über die statische Methode currentThread() ermittelt werden. static Thread currentThread()
Der Name eines Threads lässt sich später noch mit der Methode setName() setzen und über die Methode getName() auslesen. void setName(String name) String getName()
Um den aktuell laufenden Thread für eine bestimmte Zeit zu unterbrechen, wird die statische Methode sleep() verwendet. Der erste Parameter gibt die Wartezeit in Millisekunden an. Im zweiten Methodenaufruf können Sie zusätzlich die Anzahl der Nanosekunden im Bereich von 0 bis 999999 angeben. Sie werden aber nur dann berücksichtigt, wenn dies auch vom Betriebssystem unterstützt wird. Der Aufruf der Methode interrupt() für den Thread löst eine InterruptedException aus, die abgefangen werden muss. static void sleep(long millis) static void sleep(long millis, int nanos)
Beenden eines Threads und einer Anwendung Ein Thread wird auf natürliche Weise beendet, nachdem die Methode run() abgearbeitet wurde. Dies kann nach der vollständigen Abarbeitung aller Anweisungen oder nach dem Auftreten einer nicht behandelten Exception sein. Eine Java-Anwendung wird erst dann beendet, wenn kein Thread mehr läuft (mit Ausnahme von Daemon-Threads, siehe später).
Java 5 Programmierhandbuch
759
Threads über die Klasse Thread erzeugen
Das heißt, auch nach der Abarbeitung der Anweisungen in der Methode main() kann eine Anwendung noch weiterarbeiten. Wenn Sie eine Anwendung mit System.exit() beenden, werden alle laufenden Threads beendet und damit auch die Anwendung.
[IconInfo]
[IconInfo]
[IconInfo]
Seit dem JDK 1.2 sind die Methoden stop(), suspend() und resume() als deprecated gekennzeichnet und sollten nicht mehr verwendet werden. Sie dienten dem Beenden, Anhalten und Fortsetzen eines Threads. Diese Methoden werden deshalb nicht weiter erläutert. Eine Begründung findet man in der Dokumentation des JDK unter [JavaDir]\docs\guide\misc\threadPrimitiveDeprecation.html. Grundsätzlich hat es mit Synchronisationsproblemen bei Verwendung mehrerer Threads zu tun.
Die Beispiele in diesem Kapitel dienen dazu, hauptsächlich die Verwendung von Threads zu erläutern, und besitzen in der Regel eine minimale Funktionalität. Es wurde deshalb aus Platzgründen darauf verzichtet, unnötige Funktionalität in der Methode run() unterzubringen. Die verbrauchte Rechenzeit der entsprechenden Operationen wird durch die Ausführung der Methode sleep() simuliert.
Im Hauptprogramm (welches ja auch einen Thread darstellt) und in einem weiteren Thread werden über zwei Schleifen je zehn Nachrichten auf der Konsole ausgegeben. Im jeweiligen Thread wird zwischen 0,1 und 0,5 Sekunden gewartet, indem die Methode sleep() aufgerufen wird. Beide Schleifen werden parallel abgearbeitet. Die Reihenfolge der Ausgabe hängt von der Zuteilung der Prozessorzeit für das Hauptprogramm und den Thread sowie der generierten Zufallszahlen ab. Das Beispiel veranschaulicht, wie das Hauptprogramm und der Thread nebeneinander abgearbeitet werden. Nachdem ein Objekt vom Typ der Klasse ThreadTest erzeugt wurde, wird dessen Methode start() ausgeführt. Dadurch wird der Thread gestartet und die Methode run() ausgeführt. Die Klasse ThreadTest erweitert die Klasse Thread und überschreibt deren Methode run().
Listing 30.1: \Beispiele\de\j2sebuch\kap30\ThreadKlasse.java import java.util.*; public class ThreadKlasse { Random rd = new Random(); public ThreadKlasse() { ThreadTest tt = new ThreadTest(); tt.start();
760
30 – Threads Listing 30.1: \Beispiele\de\j2sebuch\kap30\ThreadKlasse.java (Forts.) for(int i = 0; i < 10; i++) { System.out.println("Hauptprogramm: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } } public static void main(String[] args) { new ThreadKlasse(); } } class ThreadTest extends Thread { Random rd = new Random(); public void run() { for(int i = 0; i < 10; i++) { System.out.println("Thread: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } } }
[IconInfo]
Der Start eines Threads kann auch innerhalb seines Konstruktors erfolgen, indem dort die Methode start() aufgerufen wird. Diese Vorgehensweise ist aber nicht zu empfehlen, weil auf diese Weise die Initialisierung über den Konstruktor und der Start des Threads miteinander vermischt werden.
30.3 Threads über das Interface Runnable erzeugen Wurde eine Klasse bereits von einer anderen Klasse erweitert bzw. besteht gegebenenfalls diese Notwendigkeit, kann ein Thread auch durch die Implementierung des Interfaces Runnable erzeugt werden. Die Klasse Thread implementiert dieses Interface übrigens
Java 5 Programmierhandbuch
761
Threads über das Interface Runnable erzeugen
auch. Das Interface besitzt nur die Methode run(). Das Interface Runnable befindet sich, wie auch die Klasse Thread, im Package java.lang. Nach der Implementierung des Interfaces muss der Thread noch erzeugt werden. Dies erfolgt unter Verwendung zweier spezieller Konstruktoren der Klasse Thread. Ihnen wird als erster Parameter ein Objekt übergeben, welches das Interface Runnable implementiert. Thread(Runnable thread) Thread(Runnable thread, String name)
Bei Verwendung eines Interface wird ebenfalls ein Thread-Objekt benötigt, über das die Methode start() aufgerufen werden kann. Die Klasse ThreadIntf implementiert hier das Interface Runnable. Thread t = new Thread(new ThreadIntf()); t.start();
[IconInfo]
Die Thread-Klasse ThreadIntfTest wird in diesem Beispiel durch die Implementierung des Interfaces Runnable realisiert. Der Inhalt der Klasse entspricht genau dem der Klasse ThreadTest aus dem Listing 30.1. Das Hauptprogramm unterscheidet sich nur in der Erzeugung des Threads. Dem Konstruktor der Klasse Thread wird ein Objekt vom Typ der Klasse ThreadIntfTest übergeben und über das Thread-Objekt wird wiederum die Methode start() ausgeführt.
Listing 30.2: \Beispiele\de\j2sebuch\kap30\ThreadInterface.java import java.util.*; public class ThreadInterface { Random rd = new Random(); public ThreadInterface() { Thread tt = new Thread(new ThreadIntfTest()); tt.start(); for(int i = 0; i < 10; i++) { System.out.println("Hauptprogramm: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } }
762
30 – Threads Listing 30.2: \Beispiele\de\j2sebuch\kap30\ThreadInterface.java (Forts.) public static void main(String[] args) { new ThreadInterface(); } } class ThreadIntfTest implements Runnable { Random rd = new Random(); public void run() { for(int i = 0; i < 10; i++) { System.out.println("Thread: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } } }
30.4 Threads unterbrechen Die Anweisungen der Methode run() in einem Thread können einfach sequentiell abgearbeitet werden oder Sie verwenden Endlosschleifen, um den Thread dauerhaft auszuführen. Ein Thread wird aber unter anderem nur dann beendet, wenn die Methode run() abgearbeitet wurde. Die einzige Möglichkeit, einen Thread von außen zu unterbrechen oder dauerhaft zu beenden, besteht in der Verwendung von Variablen, die als Flag eingesetzt werden. Sie werden außerhalb des Threads gesetzt und innerhalb eines Threads ausgewertet. Je nach dem Wert der Variablen, wird die Ausführung beendet oder gestoppt. boolean threadStop; ... threadStop = true; ... while(true) { if(threadStop) break; else ... }
// Variable im Hauptprogramm // Wert zum Beenden des Threads setzen // Endlosschleife in der Methode run()
// Endlosschleife verlassen und damit run() // beenden
Java 5 Programmierhandbuch
763
Threads unterbrechen
Die Klasse Thread stellt über einige Methoden bereits einen Mechanismus bereit, über den ein Thread unterbrochen werden kann. Dazu wird über die Methode interrupt() des betreffenden Thread ein Flag gesetzt, das durch die Methode isInterrupted() abgefragt werden kann. Die Reaktion auf die Unterbrechungsanforderung muss durch Sie implementiert werden. Sie können sie aber auch ignorieren. Durch die statische Methode interrupted() wird einerseits das Flag für den aktuellen Thread abgefragt und andererseits das Flag wieder zurückgesetzt. void interrupt() boolean interrupted() boolean isInterrupted()
Tritt eine Unterbrechungsanforderung ein, während die Ausführung des Threads gerade durch die Methoden join(), sleep() oder wait() angehalten wurde, wird das Flag sofort wieder zurückgesetzt und eine InterruptedException ausgelöst. Dies macht es beispielsweise erforderlich, dass im catch-Block einer sleep()-Anweisung erneut die Methode interrupt() aufgerufen wird, weil ansonsten das Flag verloren geht. Alternativ bricht man den Thread sofort im catch-Block ab. try { Thread.sleep(10000); } catch(InterruptedException ieEx) { interrupt(); }
[IconInfo]
764
Im fünften Schleifendurchlauf des Hauptprogramms soll der parallel laufende Thread beendet werden. Hierfür wird dessen Methode interrupt() aufgerufen. In der Thread-Klasse ThreadStopTest wird in jedem Schleifendurchlauf über den Aufruf der Methode isInterrupted() das Abbruchflag überprüft. Wurde das Unterbrechungsflag gesetzt, wird über die break-Anweisung die for-Schleife beendet. Wird das Unterbrechungsflag bei der Ausführung von sleep() gesetzt (dies ist am wahrscheinlichsten, da der Thread länger schläft als er arbeitet – kommt uns das bekannt vor?), wird im catch-Block die Methode interrupt() erneut aufgerufen, um das Flag wiederherzustellen.
30 – Threads Listing 30.3: \Beispiele\de\j2sebuch\kap30\ThreadStop.java import java.util.*; public class ThreadStop { Random rd = new Random(); public ThreadStop() { ThreadStopTest tt = new ThreadStopTest(); tt.start(); for(int i = 0; i < 10; i++) { if(i == 4) { System.out.println("Thread unterbrechen"); tt.interrupt(); } System.out.println("Hauptprogramm: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } } public static void main(String[] args) { new ThreadStop(); } } class ThreadStopTest extends Thread { Random rd = new Random(); public void run() { for(int i = 0; i < 10; i++) { if(this.isInterrupted()) { System.out.println("Thread wird beendet"); break; } System.out.println("Thread: " + i); try { Thread.sleep((rd.nextInt(5) + 1) * 100); }
Java 5 Programmierhandbuch
765
Zustände eines Threads Listing 30.3: \Beispiele\de\j2sebuch\kap30\ThreadStop.java catch(InterruptedException ieEx) { System.out.println("InterruptedException"); interrupt(); } } } }
[IconInfo]
Um die als deprecated gekennzeichneten Methoden stop(), suspend() und resume() nachzubilden, eignen sich die Methoden zum Unterbrechen eines Threads nicht. Informationen finden Sie zu diesem Thema z.B. in der Dokumentation des JDK unter ..\docs\guide\misc\threadPrimitiveDeprecation.html.
30.5 Zustände eines Threads Jeder Thread durchläuft von seiner Erstellung bis zu seiner Terminierung mehrere Zustände. Für den Wechsel von einem Zustand in einen anderen ist einerseits das Betriebssystem verantwortlich, andererseits können Sie dazu die entsprechenden Methoden der Klasse Thread verwenden. läuft
nicht existent
erzeugt
bereit
terminiert
blockiert
Abb. 30.2: Zustände eines Threads
Solange nur eine Variable vom Typ des Threads vorhanden ist, existiert noch kein Thread. Über new wird ein Thread-Objekt ERZEUGT, der Thread aber noch nicht gestartet. Dies bedeutet, es wird momentan noch gar nichts getan. Erst nach dem Aufruf der Methode start() wird die Laufzeitumgebung eines Threads initialisiert und der Thread tritt in den Zustand BEREIT. Wann er wirklich gestartet wird, hängt vom Scheduler ab. Wenn ein Thread LÄUFT, kann er durch den Scheduler nach dem Ablauf seiner zugeteilten Rechenzeit wieder in den Zustand BEREIT versetzt werden. Wird eine der Methoden sleep(), wait() oder yield() aufgerufen, BLOCKIERT der Thread, d.h., er wartet darauf, dass ein bestimmtes Ereignis eintritt. Der Eintritt dieses Ereignisses wird ihm über die Methoden notify() oder notifyAll() mitgeteilt und er befindet sich wieder im Zustand BEREIT. Alternativ BLOCKIERT ein Thread, wenn er auf Daten wartet, die ihm beispielsweise über einen Stream bereitgestellt werden. Liegen die Daten
766
30 – Threads
vor und teilt ihm der Scheduler wieder Rechenzeit zu und der Thread LÄUFT wieder. Wird die Methode run() eines Threads beendet oder die Methode System.exit() aufgerufen, TERMINIERT der Thread. Über die Methode isAlive() können Sie prüfen, ob ein Thread bereit ist, gerade läuft (Rückgabe von true), gerade neu erzeugt wurde oder bereits terminiert wurde (Rückgabe false). boolean isAlive()
Im JDK 5.0 ist die Methode getState() hinzugekommen, die differenziertere Informationen zum Zustand eines Threads liefert. Dazu besitzt die Klasse Thread die Aufzählung State mit den Konstanten NEW, BLOCKED, RUNNABLE, TERMINATED und TIME_WAITING, WAITING. Thread.State getState()
[IconInfo]
Zur Ausgabe der verschiedenen Zustände eines Threads wird ein neuer Thread erzeugt (Status NEW) und ausgeführt. Das Hauptprogramm wird für 500 ms schlafen gelegt, so dass der Thread ausgeführt wird (Status RUNNABLE). Dann wird der Thread für 1s schlafen gelegt, so dass wieder das Hauptprogramm zur Ausführung gelangt. Jetzt wird beim Thread der Status TIMED_WAITING ausgegeben. Das Hauptprogramm wird erneut unterbrochen, damit der Thread erwachen und beendet werden kann. Als letzter Status wird TERMINATED ausgegeben. Beachten Sie, dass die Ausführung eines Threads hier nur bedeutet, dass er im Zustand bereit ist. Der konkrete Ausführungszeitpunkt wird letztendlich vom Scheduler festgelegt.
Listing 30.4: \Beispiele\de\j2sebuch\kap30\ThreadStatus.java public class ThreadStatus { public ThreadStatus() { ThreadStatusTest tst = new ThreadStatusTest(); System.out.println(tst.getState()); tst.start(); System.out.println(tst.getState()); try { Thread.sleep(500); System.out.println(tst.getState()); Thread.sleep(1500); System.out.println(tst.getState()); }
Java 5 Programmierhandbuch
767
Prioritäten Listing 30.4: \Beispiele\de\j2sebuch\kap30\ThreadStatus.java (Forts.) catch(InterruptedException ieEx) {} } public static void main(String[] args) { new ThreadStatus(); } } class ThreadStatusTest extends Thread { public void run() { try { Thread.sleep(1000); } catch(InterruptedException ieEx) {} } }
30.6 Prioritäten Für die Umschaltung zwischen mehreren Threads verwendet das Betriebssystem verschiedene Informationen. Es muss ja nun der Thread bestimmt werden, der als Nächster an die Reihe kommt. Grundsätzlich muss man feststellen, dass die Implementierungen von Betriebssystem zu Betriebssystem anders gelöst sind. Eine Erläuterung der verschiedenen Verfahren soll an dieser Stelle nicht gegeben werden. Die Stelle im Betriebssystem, die für die Priorisierung verantwortlich ist, wird Scheduler genannt. Java bietet dem Entwickler über das Setzen von Prioritätsstufen die Möglichkeit, ein Scheduling auf Basis einer Thread-Priorität durchzuführen. Es werden zehn Prioritätsstufen angeboten, die durch die Zahlen 1 bis 10 gesetzt werden. Die Klasse Thread legt die obere und untere Grenze über die Konstanten MAX_PRIORITY und MIN_PRIORITY fest. Je höher die Priorität eines Threads ist, desto mehr wird er bei der Auswahl des nächsten auszuführenden Threads vom Scheduler bevorzugt. Obwohl Java zehn Prioritäten verwendet, müssen diese nicht zwangsläufig auf jedem Betriebssystem zur Verfügung stehen. Windows nutzt in den meisten Versionen sieben Stufen. Wie die Abbildung der zehn Stufen von Java auf die sieben Stufen von Windows erfolgt, hängt von der JVM ab. Beim Erzeugen eines Threads erbt dieser die Prioritätsstufe von seinem übergeordneten Thread. Haben Sie keine Prioritäten vergeben, wird meist die Priorität 5 verwendet, die durch eine weitere Konstante NORM_PRIORITY festgelegt wird.
768
30 – Threads
Wenn Sie die Ausführungspriorität eines Threads zu sehr anheben, kann dies dazu führen, dass alle anderen Anwendungen eines Systems beginnen einzufrieren, d.h., sie werden nur noch schleppend ausgeführt. Gehen Sie deshalb behutsam mit den Stufen um. Wollen Sie innerhalb einer Anwendung einem Thread den Vorzug geben, sollten Sie besser ein Verhältnis von 2:6 oder 2:7 statt ein Verhältnis 5:9 oder 5:10 wählen. Die Priorität eines Threads kann über die Methode getPriority() ermittelt und mit setPriority() jederzeit geändert werden. final int getPriority() final void setPriority(int prioritaet)
[IconInfo]
[IconInfo]
Um innerhalb einer Anwendung bestimmte Threads in der Ausführung zu bevorzugen oder eine Reihenfolge der Thread-Ausführung über Prioritäten zu regeln, sollten Sie besser einen eigenen Thread-Handler programmieren, da sich die Umsetzung der Prioritäten stark zwischen den verschiedenen Betriebssystemen unterscheiden kann. Insbesondere darf die korrekte Ausführung einer Anwendung oder eines Algorithmus nicht von der Priorität eines Threads abhängen.
Die folgende Beispielanwendung erzeugt zwei Threads, die mit den Prioritäten 3 und 5 versehen werden. Nach dem Start beider Threads ist z.B. unter Windows erkennbar, dass die Schleife in der Methode run() beider Threads durch den Thread mit der höheren Priorität schneller abgearbeitet wird. Die Schleife wird dazu 500000 Mal durchlaufen und alle 50000 Durchläufe wird eine Ausgabe durchgeführt. Ist die Bearbeitungszeit eines Schleifendurchlaufs zu klein, werden eventuell alle Durchläufe schon innerhalb einer Ausführungseinheit ausgeführt. Erhöhen Sie in diesem Fall einfach die Werte. Zur Anzeige des Threads, der die aktuelle Ausgabe durchführt, werden beide Threads benannt. Im Konstruktor der Thread-Klasse wird deshalb der Konstruktor der Basisklasse mit dem Thread-Namen aufgerufen.
Listing 30.5: \Beispiele\de\j2sebuch\kap30\ThreadPrio.java public class ThreadPrio { public ThreadPrio() { ThreadPrioTest tpt1 = new ThreadPrioTest("Thread 1"); ThreadPrioTest tpt2 = new ThreadPrioTest("Thread 2"); tpt1.setPriority(3); tpt2.setPriority(5); tpt1.start(); tpt2.start(); }
Java 5 Programmierhandbuch
769
Daemon-Threads Listing 30.5: \Beispiele\de\j2sebuch\kap30\ThreadPrio.java public static void main(String[] args) { new ThreadPrio(); } } class ThreadPrioTest extends Thread { public ThreadPrioTest(String name) { super(name); } public void run() { for(int i = 1; i < 500000; i++) { if(i % 50000 == 0) System.out.println(getName()); } } }
30.7 Daemon-Threads Wie bereits erwähnt, wird eine Java-Anwendung erst dann beendet, wenn keine Threads mehr laufen. Jeder Thread muss deshalb entweder regulär beendet werden, indem dessen Methode run() verlassen oder die Anwendung über System.exit() beendet wird. Speziell bei Threads, die während der gesamten Ausführungszeit einer Anwendung laufen, ist die Beendigung der Methode run() nur über ein Flag möglich. Kann bzw. soll eine Anwendung nicht durch System.exit() beendet werden, bleibt noch eine weitere Möglichkeit, solche Threads automatisiert zu beenden. Ein als Daemon-Thread gekennzeichneter Thread wird beim Beenden einer Anwendung automatisch terminiert. Daemon-Threads sollten deshalb keine heiklen Aufgaben durchführen. Sie können z.B. zur Überwachung von Netzwerkverbindungen, dem Logging oder in einem Browser zum Laden von Bildern eingesetzt werden. Wenn Sie einen Thread erzeugen, wird dieser standardmäßig als so genannter User-Thread angelegt. Erst durch den Aufruf der Methode setDaemon() der Klasse Thread mit dem Parameter true kann aus einem User-Thread ein Daemon-Thread gemacht werden. Auch der umgekehrte Weg ist möglich. Der Aufruf von setDaemon() muss aber noch vor dem Start des Threads durchgeführt werden. Über die Methode isDaemon() können Sie prüfen, ob ein bestimmter Thread ein Daemon-Thread ist. void setDaemon(boolean flag) boolean isDaemon()
770
30 – Threads
[IconInfo]
Wenn Sie die folgende Anwendung ohne die Anweisung setDaemon(true) ausführen, würde sie unendlich lange oder bis zum nächsten Stromausfall laufen. Die aktuelle Implementierung gibt auf der Konsole eine Zeichenkette aus und beendet damit das Hauptprogramm. Da keine weiteren User-Threads existieren, wird die gesamte Anwendung beendet.
Listing 30.6: \Beispiele\de\j2sebuch\kap30\ThreadDaemonTest.java public class ThreadDaemonTest extends Thread { public void run() { while(true) ; } public static void main(String[] args) { ThreadDaemonTest tdt = new ThreadDaemonTest(); tdt.setDaemon(true); tdt.start(); System.out.println("das war's schon ... "); } }
30.8 Timer Für zeitgesteuerte Abläufe kann auf die Implementierung eines eigenen Timer-Threads verzichtet werden. Die Klassen Timer und TimerTask aus dem Package java.util können verwendet werden, um Zeitgeber zu realisieren oder zu festgelegten Zeitpunkten bestimmte Anweisungen im Hintergrund über Threads auszuführen. Die Threads können dabei auch wiederholt abgearbeitet werden. Ein Timer verwaltet eine Warteschlange von Tasks, die nacheinander zur Ausführung kommen. Müssen Tasks parallel ausgeführt werden, verwenden Sie entsprechend separate Timer. Die Tasks der Warteschlange werden durch TimerTask-Objekte realisiert. Diese implementieren implizit das Interface Runnable und werden unter der Verwaltung des Timers ausgeführt. Timer t = new Timer(); t.schedule(new TimerTaskImpl(), 1000, 1000); ... class TimerTaskImpl extends TimerTask { public void run() { } }
Java 5 Programmierhandbuch
771
Timer
Damit der Timer-Thread nach der Ausführung aller Tasks das Beenden der Anwendung nicht verhindert, kann er als Daemon-Thread erzeugt werden. Übergeben Sie hiefür im Konstruktor den Wert true. Timer t = new Timer(true);
Mit der folgenden überladenen Methode fügen Sie über den ersten Parameter eine Task der Warteschlange des Timers hinzu. Der Parameter verz legt die Verzögerung in Millisekunden fest, nach der die Task gestartet werden soll. Die dritte Methode versucht bei längeren Verzögerungen die Perioden der einzelnen Tasks in der Summe einzuhalten. Dazu wird die Verzögerung immer ausgehend vom ersten Start der Tasks berechnet. Entsprechend existieren auch Methoden, die keine Verzögerungszeit, sondern eine konkrete Uhrzeit zum Start entgegennehmen. Beachten Sie, dass die Tasks immer sequentiell ausgeführt werden. Benötigt eine Task beispielsweise 3 Sekunden und eine andere Task soll ab sofort jede Sekunde ausgeführt werden, müssen getrennte Timer verwendet werden. void void void void
schedule(TimerTask task, long schedule(TimerTask task, long scheduleAtFixedRate(TimerTask schedule(TimerTask task, Date
verz) verz, long periode) task, long verz, long periode) zeit)
Die Anwendung läuft nach dem Erzeugen des Timer-Threads so lange, bis dieser beendet wird. Zur Beendigung des Threads sollte idealerweise dessen Methode cancel() aufgerufen werden oder er wird bereits im Konstruktor als Deamon-Thread erzeugt. Alternativ muss die Anwendung über System.exit() verlassen werden. Die Methode purge() entfernt alle beendeten Tasks aus der Task-Warteschlange, die dann durch den Garbage Collector eingesammelt werden können. void cancel() int purge()
Entwickeln Sie grafische Anwendungen können Sie auf die Timer-Klasse javax.swing.Timer zurückgreifen. Diese Timer-Klasse arbeitet mit einem einzigen Timer-Thread und verwendet Listener, wie sie bei grafischen Anwendungen zum Einsatz kommen. [IconInfo]
Die Klasse TimerTask ist relativ trivial anzuwenden. Leiten Sie davon einfach eine Klasse ab und überschreiben Sie deren abstrakte Methode run(). Einen Konstruktor können Sie hier nicht zur Initialisierung verwenden, weil dieser bereits in der Klasse TimerTask das Attribut protected besitzt. Über die Methode cancel() können Sie einen noch nicht gestarteten Task beenden. Laufende Tasks werden durch den Aufruf von cancel() nicht beendet. Periodisch ausgeführte Tasks werden beim nächsten Mal nicht wieder gestartet.
772
30 – Threads boolean cancel() abstract void run()
[IconInfo]
Mit einem Timer werden zwei Threads über die Methode schedule() in einen Zeitplan eingetragen. Da die Methode sofort zurückkehrt, wird das Hauptprogramm für 3,2 s schlafen gelegt. Nach einer Sekunde startet der erste Thread. Er läuft genau eine Sekunde, gibt in zehn Schleifen eine Zeichenkette aus und wird wieder beendet. Dann kommt der nächste Thread zum Zuge. Sein Start beginnt eine Sekunde verzögert und er wird jede Sekunde neu gestartet. Nachdem er einmal gestartet wurde, sind mittlerweile 3 s seit dem Start der Anwendung vergangen. Nach seinem zweiten Start wird nach 0,2 Sekunden das Hauptprogramm aus seinem Schlaf geweckt und ruft die Methode cancel() des zweiten Threads auf. Der Thread wird noch vollständig beendet und anschließend aus dem Zeitplaner des Timers entfernt. Damit wird auch die Anwendung beendet.
Listing 30.7: \Beispiele\de\j2sebuch\kap30\Zeitgeber.java import java.util.*; public class Zeitgeber { public Zeitgeber() { Timer tm = new Timer(); ThreadTimerTest ttt1 = new ThreadTimerTest(); ttt1.setName("Thread 1"); tm.schedule(ttt1, 1000); ThreadTimerTest ttt2 = new ThreadTimerTest(); ttt2.setName("Thread 2"); tm.schedule(ttt2, 1000, 1000); try { Thread.sleep(3200); } catch(InterruptedException ieEx) {} ttt2.cancel(); } public static void main(String[] args) { new Zeitgeber(); } } class ThreadTimerTest extends TimerTask { String name;
Java 5 Programmierhandbuch
773
Thread-Gruppen Listing 30.7: \Beispiele\de\j2sebuch\kap30\Zeitgeber.java (Forts.) public void setName(String name) { this.name = name; } public void run() { for(int i = 0; i < 10; i++) { System.out.println(name + ": Runde " + i); try { Thread.sleep(100); } catch(InterruptedException ieEx) {} } } }
30.9 Thread-Gruppen Wollen Sie häufiger bestimmte Operationen mit mehreren Threads ausführen, können Sie Thread-Gruppen bilden. Dazu erzeugen Sie ein ThreadGroup-Objekt und übergeben dieses den folgenden Konstruktoren der Klasse Thread. Thread(ThreadGroup group, Runnable target) Thread(ThreadGroup group, String name) Thread(ThreadGroup group, Runnable target, String name)
Ein Thread gehört immer nur einer Gruppe an, die später auch nicht mehr gewechselt werden kann. Geben Sie beim Erstellen eines Threads keine Thread-Gruppe an, wird er automatisch der Gruppe des erzeugenden Threads zugeordnet. Im Falle des Hauptprogramms ist der Name der Thread-Gruppe main. Die Thread-Gruppe eines Threads können Sie mit der Methode getThreadGroup() ermitteln. ThreadKlasse tk = new ThreadKlasse(); ThreadGroup tg = tk.getThreadGroup(); System.out.println(tg.getName());
Die Klasse ThreadGroup befindet sich ebenfalls im Package java.lang. Um ein ThreadGruppenobjekt zu erzeugen, stehen zwei Konstruktoren zur Verfügung. In jedem Fall sollte eine Thread-Gruppe einen aussagekräftigen Namen erhalten. Über den zweiten Konstruktor können Sie Hierarchien von Thread-Gruppen bilden. Einige der Methoden der Klasse ThreadGroup können dann auf alle Threads der Gruppe und Untergruppen angewandt werden.
774
30 – Threads ThreadGroup(String name) ThreadGroup(ThreadGroup parent, String name)
Über die Thread-Gruppe können Sie nun verschiedene Operationen ausführen. Das Aufzählen aller Threads einer Thread-Gruppe erfolgt etwas umständlich und Java-unüblich über die Bestimmung der Anzahl der aktiven Threads durch die Methode activeCount(), der Erzeugung eines Arrays vom Typ Thread der Größe anzahl und der Übergabe des Arrays an die Methode enumerate(). Sind inzwischen weitere Threads hinzugekommen oder beendet, müssen Sie dies selbst überprüfen. Auf diese Weise lassen sich z.B. alle laufenden Threads ermitteln. Bei der Verwendung von hierarchischen ThreadGruppen muss gegebenenfalls noch über alle Gruppen iteriert werden. ThreadGroup tg = Thread.currentThread().getThreadGroup(); int anzahl = tg.activeCount(); Thread[] threads = new Threads[anzahl]; tg.enumerate(threads);
Den Namen der Thread-Gruppe bestimmen Sie über die Methode getName() und die übergeordnete Thread-Gruppe über die Methode getParent(). Die oberste Thread-Gruppe der Hierarchie liefert beim Aufruf von getParent() den Wert null zurück. String getName() ThreadGroup getParent()
Besitzen eine Thread-Gruppe und alle ihre Untergruppen keine aktiven Threads mehr, kann die Thread-Gruppe durch den Aufruf von destroy() aufgelöst werden. Über die Methode interrupt() wird für alle Threads der Gruppe inklusive der Untergruppen deren Methode interrupt() aufgerufen. void destroy() void interrupt()
Die folgenden Methoden prüfen, ob es sich um eine Daemon-Thread-Gruppe handelt bzw. ändern die enthaltenen Threads in Daemon- bzw. User-Threads. boolean isDeamon() void setDeamon(boolean flag)
Java 5 Programmierhandbuch
775
Synchronisation
30.10 Synchronisation 30.10.1 Einführung Die bisher verwendeten Beispiele enthielten Threads, die nur auf ihre eigenen Daten zugegriffen haben. Häufig werden bestimmte Daten, Methoden oder Ressourcen von mehreren Threads benötigt. Über Synchronisationsmechanismen muss dann sichergestellt werden, dass keine inkonsistenten Zustände entstehen, dass bestimmte Methoden immer vollständig ausgeführt werden, bevor eine Task-Umschaltung erfolgt oder dass Ressourcen nur von einer bestimmten Anzahl von Threads genutzt werden.
30.10.2 Einfache Synchronisationsmechanismen Die Methoden join() und yield() dienen zwar nur in einem weiteren Sinne der Synchronisation mehrerer Threads, sollen aber hier mit vorgestellt werden. Auf andere Threads mit join warten Verrichten mehrere Threads im Hintergrund Operationen, von deren Abschluss der weitere Programmablauf abhängt, können Sie die Methode join() einsetzen. Der aktuelle Thread wartet beim Aufruf von join() auf die Beendigung des Threads, über den die Methode aufgerufen wurde. Mit zusätzlichen Parametern können Sie die maximale Wartezeit in Milli- und Nanosekunden festlegen. void join() void join(long millis) void join(long millis, int nanos)
So könnte beispielsweise ein Browser über einen Thread A mit höherer Priorität eine HTML-Seite laden und den Seitenaufbau im Browser berechnen, während andere Threads Grafiken nachladen. Dann wartet der Thread A auf die Beendigung der anderen Threads.
[IconInfo]
Die Thread-Klasse ThreadWarteTest besitzt einen Konstruktor, über den ein Name für den Thread sowie eine Wartezeit übergeben werden kann. Über den Aufruf von super() wird der Konstruktor der Klasse Thread aufgerufen, der den Namen des Threads festlegt. Eine Schleife wird zehnmal ausgeführt und darin wird jeweils eine bestimmte Zeit gewartet, bevor der Thread beendet wird. Hier würde also in Ihrer Anwendung der entsprechende (sinnvolle) Code stehen. Von der Thread-Klasse werden zwei Objekte erzeugt, die mit unterschiedlichen Wartezeiten arbeiten. Beide werden gestartet. Über den Aufruf von join() wird auf die Beendigung beider Threads gewartet. Die Reihenfolge ist dabei egal. Ist ein Thread bereits beendet, kehrt der Aufruf von join() sofort zurück. Ist der Thread noch nicht beendet, blockiert join() die weitere Programmausführung.
776
30 – Threads Listing 30.8: \Beispiele\de\j2sebuch\kap30\ThreadWarte.java public class ThreadWarte { public ThreadWarte() { ThreadWarteTest twt1 = new ThreadWarteTest("Thread 1", 100); ThreadWarteTest twt2 = new ThreadWarteTest("Thread 2", 200); twt1.start(); twt2.start(); try { twt1.join(); twt2.join(); } catch(InterruptedException ieEx) {} System.out.println("Beide fertig"); } public static void main(String[] args) { new ThreadWarte(); } } class ThreadWarteTest extends Thread { private int millis = 0; public ThreadWarteTest(String name, int millis) { super(name); this.millis = millis; } public void run() { for(int i = 0; i < 10; i++) { try { sleep(millis); } catch(InterruptedException ieEx) {} System.out.println(getName()); } } }
Java 5 Programmierhandbuch
777
Synchronisation
Threads über yield pausieren lassen Führt ein Thread eine sehr zeitintensive Rechenoperation aus und besitzt er zudem eine hohe Priorität oder arbeitet der Scheduler eines Betriebssystems nicht sonderlich fair, kann ein Thread alle anderen Threads bezüglich ihrer Ausführungszeit lahm legen. Um anderen Threads Rechenzeit zukommen zu lassen, kann der Thread die statische Methode yield() aufrufen und sich damit wieder in die Warteschlange des Schedulers einreihen. static void yield()
30.10.3 Monitore Beim Zugriff mehrerer Threads auf die gleichen Daten, z.B. durch den Aufruf einer Methode des gleichen Objekts, kann es zu inkonsistenten Zuständen kommen, wenn die Anweisungen der Methode nicht vollständig durchlaufen werden. In den folgenden Anweisungen wird beispielsweise eine Umbuchung getätigt. Vom Konto 2 sollen 100 Einheiten auf das Konto 1 übertragen werden. Nach der Ausführung der ersten Anweisung wird der betreffende Thread suspendiert. konto1 = konto1 + 100; konto2 = konto2 - 100;
Jetzt werden die folgenden Anweisungen von einem zweiten Thread ausgeführt. Es sollen damit die Zinsen (auf sehr vereinfachte Weise) gutgeschrieben werden. Für die Bank ist die Operation von Nachteil, da Konto 1 bereits 100 Einheiten gutgeschrieben wurden, die aber bei Konto 2 noch nicht abgezogen sind. konto1 = konto1 + 0.02 * konto1; konto2 = konto2 + 0.02 * konto2;
Eine Lösung wäre, dass die beiden Abschnitte nur atomar, d.h. ohne Unterbrechung, ausgeführt werden dürfen. Die Klasse Konto verwaltet einen Kontostand, der mit 0 initialisiert wird. Über die Methode aendern() kann der Wert des Kontostands geändert, über getKontoStand() abgerufen werden. [IconInfo]
Es werden zwei Threads erzeugt, die beide den Kontostand manipulieren. Den Threads wird das gleiche Konto-Objekt im Konstruktor übergeben. In einer Endlosschleife werden die Kontostände verändert. Da der Scheduler einen Thread in dieser Anwendung an einer beliebigen Stelle unterbrechen kann, ist die zweite Änderung des Kontostands eventuell noch nicht durchgeführt worden. Kommt dann der nächste Thread zum Zuge, ist der Kontostand nach beiden Änderungen nicht 0, sondern -100. An diesem Beispiel können Sie gut sehen, dass eine Unterbrechung durch den Scheduler mitten in der Abarbeitung einer Methode erfolgen kann.
778
30 – Threads Listing 30.9: \Beispiele\de\j2sebuch\kap30\SyncFehler.java public class SyncFehler { public SyncFehler() { Konto k = new Konto(); SyncFehlerTest sft1 = new SyncFehlerTest(k); SyncFehlerTest sft2 = new SyncFehlerTest(k); sft1.start(); sft2.start(); } public static void main(String[] args) { new SyncFehler(); } } class Konto { private static int kontostand = 0; void aendern(int wert) { kontostand = kontostand + wert; } int getKontoStand() { return kontostand; } } class SyncFehlerTest extends Thread { Konto k; public SyncFehlerTest(Konto k) { this.k = k; } public void run() { int zaehler = 0; while(true) { zaehler++; k.aendern(100); k.aendern(100); if(k.getKontoStand() != 0) { System.out.println("Runde: " + zaehler);
Java 5 Programmierhandbuch
779
Synchronisation Listing 30.9: \Beispiele\de\j2sebuch\kap30\SyncFehler.java (Forts.) System.out.println(k.getKontoStand()); break; } } } }
Damit die gezeigten Probleme aus dem Beispiel SyncFehler.java verhindert werden können, stellt Java das Monitorkonzept zur Verfügung. Es werden dabei eine Methode oder mehrere Anweisungen durch einen synchronized-Block eingeschlossen. Die Methode oder die Anweisungen werden jetzt ohne Unterbrechung durchlaufen. Dies bedeutet natürlich eine gewisse Verantwortung für den Programmierer. Wenn Sie lang anhaltende Operationen in einem synchronized-Block ausführen, sind für diese Zeit die anderen Threads einer Anwendung blockiert. public void synchronized umbuchung(int wert) { konto1 = konto1 + wert; konto2 = konto2 - wert; }
Die durch synchronized eingeschlossenen Anweisungen werden auch kritischer Block genannt, da hier Probleme beim Zugriff mehrerer Threads oder der Unterbrechung der Ausführung auftreten können. Die Funktionsweise von synchronized basiert auf der Tatsache, dass mit jedem JavaObjekt (einschließlich des Klassenobjekts) eine Warteschlange eingerichtet wird. Betritt ein Thread einen synchronisierten Bereich, wird der Zutritt zu diesem Bereich für andere Threads blockiert. Diese werden in die Warteschlange eingereiht. Verlässt der Thread den synchronisierten Bereich, wird die Sperre freigegeben und der nächste Thread erhält Zutritt. Diese Arbeitsweise lässt auch erkennen, dass sich die Verwendung von synchronized nicht ganz ohne Geschwindigkeitsverlust realisieren lässt. Gehen Sie deshalb so sparsam und sorgsam wie möglich mit synchronized um. Syntax Synchronized kann für Methoden und Anweisungsblöcke eingesetzt werden. Im Falle einer Methode wird synchronized als zusätzliches Attribut angegeben. Die Reihenfolge der Attribute spielt keine Rolle. Verwenden Sie synchronized, um mehrere Anweisungen einzuschließen, müssen Sie in Klammern ein Objekt angeben. Dieses Objekt verwaltet die Warteschlange. Dies impliziert, dass beliebige Objekte für die Verwaltung der Warteschlange verwendet werden können.
780
30 – Threads synchronized methode() {} synchronized(Object o) {}
Möchten Sie das aktuelle Objekt zur Verwaltung der Warteschlange nutzen, können Sie auch this als Parameter übergeben. Dies ist auch die Vorgehensweise, die Java einsetzt, wenn Sie synchronized auf Methoden anwenden. Die beiden folgenden Anweisungsblöcke sind demnach gleichwertig. synchronized void test() { System.out.println("..."); } // und void test() { synchronized(this) { System.out.println("..."); } }
Besitzt eine Klasse mehrere synchronized-Blöcke, wird beim Betreten eines solchen Blocks durch einen Thread das gesamte Objekt der Klasse blockiert. Möchten Sie Anweisungen in statischen Methoden synchronisieren, können Sie auch das Klassenobjekt zur Verwaltung des Monitors nutzen, welches Sie über getClass() ermitteln. synchronized(this) {} synchronized(getClass()) {}
Alternativ verwenden Sie eine einfache Objektvariable, die zum Synchronisieren von Anweisungen in statischen wie auch nicht statischen Methoden genutzt werden kann. class Test { private static Object o = new Object(); static void test() { synchronized(o) {} } }
Java 5 Programmierhandbuch
781
Synchronisation
[IconInfo]
[IconInfo]
[IconInfo]
Die Anzahl der zu synchronisierenden Anweisungen sollte so gering wie möglich sein, damit andere Threads nicht zu lange blockiert werden und der Scheduler die Rechenzeit auf die laufenden Threads gleichmäßiger verteilen kann. Ist eine Methode als synchronized deklariert, lässt sich dies aber häufig besser im Programmcode erkennen, als wenn nur einzelne Anweisungen eingeschlossen werden. Es gilt also auch hier einen vernünftigen Kompromiss zu finden.
Wenn sich ein Thread in einem synchronized-Block eines Objekts befindet (einer Methode oder in einem Anweisungsblock), kann er auch alle anderen synchronisierten Blöcke betreten, ohne zu warten. Diese Eigenschaft eines Monitors wird als reentrant (wiedereintrittsfähig) bezeichnet.
Damit die Berechnung der Kontoänderungen nicht unterbrochen wird, werden die Anweisungen der Methode run() des Beispiels SyncFehler.java aus dem Listing 30.9 in einen synchronized-Block eingeschlossen. Das Konto-Objekt verwaltet den Monitor, der den Zutritt zu den Anweisungen kontrolliert.
Listing 30.10: \Beispiele\de\j2sebuch\kap30\Monitor.java synchronized(k) { zaehler++; k.aendern(100); k.aendern(-100); if(k.getKontoStand() != 0) { System.out.println("Runde: " + zaehler); System.out.println(k.getKontoStand()); break; } }
30.10.4 Kooperation zwischen Threads Über synchronisierte Abschnitte lassen sich zwischen Threads auch Daten austauschen. Finden ein oder mehrere Threads nicht die gewünschten Daten (natürlich nacheinander) vor, verlassen sie den Abschnitt wieder. Dies sorgt aber nur für unnötige Rechenzeitverschwendung. Vielmehr wird eine Möglichkeit benötigt, die wartende Threads benachrichtigt, wenn die gewünschten Daten oder die Informationen vorliegen. Java bietet mit den Methoden wait(), notify() und notifyAll() der Klasse Object genau diese Funktionalität.
782
30 – Threads
Beispiel Ein Webbrowser verwendet mehrere Threads, um eine HTML-Seite mit einigen Bildern von einem Webserver zu laden. Für die Anzeige der Bilder ist es notwendig, dass diese vollständig übertragen sind. Der Thread, der für die Anzeige der gesamten HTML-Seite verantwortlich ist, begibt sich dazu in einen synchronisierten Abschnitt eines Objekts, der das vollständig geladene Bild zur Verfügung stellt. Ist es noch nicht verfügbar, begibt sich der Thread in eine Wartestellung. Der synchronisierte Abschnitt wird dadurch wieder zum Betreten für andere Threads frei. Hat der Thread, der für das Laden des Bilds verantwortlich ist, dieses vollständig übertragen, begibt er sich in den synchronisierten Abschnitt, stellt das Bild zur Verfügung und informiert alle wartenden Threads, dass diese nun weiterarbeiten können. In diesem Fall wird der wartende Thread, der auf das Bild wartet, wieder aufgeweckt und arbeitet weiter. Produzenten und Konsumenten Diese Vorgehensweise wird in der Regel durch ein Produzent-Konsument-Szenario beschrieben. Ein Produzent erzeugt etwas (Zufallszahlen, Statistiken etc.) oder stellt etwas zur Verfügung (Daten einer Netzwerkverbindung oder aus einer Datei). Ein Konsument verarbeitet diese Daten. Liegen keine Daten vor, begibt sich der Konsument in eine Wartestellung, der Thread blockiert. Hat ein Produzent Daten bereitgestellt, informiert er alle Wartenden (also die Konsumenten), dass die gewünschten Daten nun vorliegen. Warten und benachrichtigen Ein Thread kann nur in einen Wartezustand treten bzw. andere Threads aufwecken, wenn er sich in einem synchronisierten Abschnitt befindet. Wird eine der Methoden wait(), notify() oder notifyAll() außerhalb eines solchen Abschnitts aufgerufen, wird eine IllegalMonitorStateException ausgelöst. Die Warteliste eines Objekts ist zu Beginn leer. Beansprucht ein Thread den Monitor eines Objekts, kann er die Methode wait() aufrufen und sich in die Warteschlange einreihen. Damit wird der Monitor des Objekts wieder freigegeben und ein anderer Thread kann den Monitor für sich beanspruchen. Durch den Aufruf von wait() wird der Thread nicht mehr vom Scheduler berücksichtigt, so dass er im schlechtesten Fall ewig wartet, wenn er nicht über den Aufruf von notify() oder notifyAll() geweckt wird. Um einen Timeout vorzugeben, können Sie dessen Wartezeit über die Anzahl der Millisekunden und zusätzlich der Nanosekunden festlegen. Wird für einen durch wait() blockierten Thread die Methode interrupt() aufgerufen, wird eine InterruptedException ausgelöst. Dies ist eine weitere Möglichkeit, einen Thread aus seiner Wartestellung zu befreien und ihn wieder dem Scheduler zuzuführen. void wait() void wait(long millis) void wait(long millis, int nanos)
Java 5 Programmierhandbuch
783
Synchronisation
Der das Objekt blockierende Thread kann über den Aufruf der Methode notify() einen oder über notifyAll() alle wartenden Threads aufwecken. Sie haben dann wieder die Möglichkeit, den Monitor für sich zu beanspruchen. Der die Methoden aufrufende Thread muss aber erst den Monitor verlassen, bis der nächste Thread eintreten kann. Welcher Thread den Monitor betreten darf, ist implementationsabhängig und kann nicht beeinflusst werden. Werden notify() oder notifyAll() aufgerufen und es befinden sich keine Threads in der Warteschlange, haben die Aufrufe keine weiteren Auswirkungen. final void notify() final void notifyAll()
[IconInfo]
Der Thread Produzent erzeugt alle 100 ms eine Zufallszahl im Bereich von 1 bis 20 und fügt diese in eine ArrayList ein. Zwei Konsumenten lesen diese Zahlen aus der ArrayList aus, löschen sie aus der Liste und geben den Wert auf der Konsole aus. Da der Produzenten-Thread nur alle 100 ms eine Zufallszahl erzeugt und die Konsumenten diese schneller verarbeiten, rufen die Konsumenten die Methode wait() bei einer leeren ArrayList auf und versetzen sich in einen Wartezustand. Normalerweise warten dann beide Konsumenten auf eine neue Zufallszahl. Wird diese erzeugt, ruft der Thread Produzent die Methode notifyAll() auf und weckt beide Konsumenten. An der Ausgabe der Anwendung sehen Sie, dass die Ausgaben und Wartemeldungen nicht im Wechsel, sondern eher zufällig erfolgen. Dies hängt damit zusammen, dass notifyAll() grundsätzlich keine bestimmte Reihenfolge beim Aufwecken der Threads berücksichtigt. Da kein Abbruchkriterium vorgesehen ist, müssen Sie die Anwendung manuell beenden, z.B. durch Ÿ+C. Zum Speichern der Zufallszahlen wird eine generische Collection verwendet, die nur Integer-Objekte speichern kann. Durch das Auto(un)boxing entfallen später auch alle Umwandlungen von int nach Integer und umgekehrt. Im Konstruktor der Klasse Kooperation werden der Produzent und die Konsumenten erstellt. Alle erhalten im Konstruktor eine Referenz auf das ArrayList-Objekt. Für die Ausgabe auf der Konsole wird zur Unterscheidung für jeden Konsumenten-Thread ein Name vergeben. Danach werden alle Threads gestartet.
Listing 30.11: \Beispiele\de\j2sebuch\kap30\Kooperation.java (Auszug) import java.util.*; public class Kooperation { ArrayList al = new ArrayList(); public Kooperation() { Produzent prod = new Produzent(al); Konsument kon1 = new Konsument(al, "Konsument 1"); Konsument kon2 = new Konsument(al, "Konsument 2");
784
30 – Threads Listing 30.11: \Beispiele\de\j2sebuch\kap30\Kooperation.java (Auszug) (Forts.) prod.start(); kon1.start(); kon2.start(); } public static void main(String[] args) { new Kooperation(); } }
Der Produzent verwaltet intern eine Referenz auf das ArrayList-Objekt. Zum Erzeugen der Zufallszahlen wird ein Random-Objekt aus dem Package java.util verwendet. Der Zugriff auf das ArrayList-Objekt erfolgt in einem synchronisierten Block, um Probleme beim gleichzeitigen Lesen anderer Threads zu verhindern und die Kommunikation über wait() und notify() überhaupt realisieren zu können. Nach dem Erzeugen einer Zufallszahl über die Methode nextInt() wird 100 ms geschlafen, um anschließend alle wartenden Threads zu benachrichtigen, dass für sie eine neue Zufallszahl generiert wurde. Listing 30.12: \Beispiele\de\j2sebuch\kap30\Kooperation.java (Auszug) class Produzent extends Thread { ArrayList al; Random rd = new Random(); public Produzent(ArrayList al) { this.al = al; } public void run() { while(true) { synchronized(al) { al.add(new Integer(rd.nextInt(20) + 1)); try { sleep(100); } catch(InterruptedException ieEx) {} al.notifyAll(); } } } }
Java 5 Programmierhandbuch
785
Synchronisation
Auch der Konsument verwaltet eine Referenz auf das ArrayList-Objekt. Diese wird für den Monitor zum Synchronisieren benötigt. Im synchronized-Block wird zuerst geprüft, ob die ArrayList leer ist. In diesem Fall wird eine Meldung ausgegeben und der Thread begibt sich in die Warteschlange. Ist die Liste dagegen nicht leer, wird der erste Wert ausgelesen, entfernt und auf der Konsole ausgegeben. Listing 30.13: \Beispiele\de\j2sebuch\kap30\Kooperation.java (Auszug) class Konsument extends Thread { ArrayList al; public Konsument(ArrayList al, String name) { super(name); this.al = al; } public void run() { while(true) { synchronized(al) { if(al.isEmpty()) { System.out.println(getName() + ": ich warte ..."); try { al.wait(); } catch(InterruptedException ieEx) {} } else { int wert = al.get(0); al.remove(0); System.out.println(getName() + ": " + wert); } } } } }
786
30 – Threads
30.10.5 Das Attribut volatile In Zusammenhang mit Threads spielt das Attribut volatile bei der Deklaration von Variablen eine Rolle. Hintergrund ist die Tatsache, dass für die von Threads genutzten Variablen aus dem gemeinsam genutzten Hauptarbeitsspeicher einer Anwendung eine Arbeitskopie für den Thread erzeugt wird. Zu Optimierungszwecken wird nicht immer sofort ein Abgleich beider Werte durchgeführt. Damit beide Werte immer sofort aktualisiert werden und der Compiler keine Optimierungen durchführt, muss eine Variable als volatile deklariert werden. Dies ist beispielsweise dann notwendig, wenn eine Variable im Hauptprogramm gesetzt wird und ein Thread unbedingt den aktuellsten Wert benötigt (z.B. um seinen Abbruch zu erkennen). volatile int name;
30.10.6 Deadlocks Bei der Arbeit mit mehreren Threads, die auf mehrere gemeinsame Ressourcen zugreifen, kann es zu Deadlocks (Verklemmungen) kommen. Wenn Sie nur mit einem Thread arbeiten oder nur eine Ressource von mehreren Threads verwendet wird, sind Deadlocks ausgeschlossen. Die Ressourcen sind in diesem Fall über synchronized abgesichert, so dass sie nur von einem Thread genutzt werden können. Ein Deadlock tritt dann auf, wenn ein Thread A eine Ressource X bereits belegt hat und eine Ressource Y benötigt, während ein Thread B die Ressource Y bereits belegt und X benötigt. Die beiden Threads warten jetzt gegenseitig auf die Freigabe der benötigten Ressource. belegt
benötigt
Thread A
Ressource X
Ressource Y
Thread B
Ressource Y
Ressource X
Die Java Virtual Machine kann solche Situationen nicht erkennen und den Deadlock nicht verhindern. Dies muss durch eine sehr sorgfältige Programmierung erfolgen. Die Java HotSpot VM besitzt seit dem JDK 1.4.1 eine integrierte Deadlock-Erkennung, wenn er bereits vorliegt. Ist ein Deadlock aufgetreten, betätigen Sie bei laufender (bzw. hängender) Anwendung auf der Konsole unter Windows die Tastenkombination Ÿ + þPauseÿ bzw. unter Linux Ÿ + \. Das Tool hilft nur dem Entwickler beim Test einer Anwendung auf Deadlocks. Für den Anwender ist es nicht sonderlich hilfreich, bis auf die Tatsache, dass er Ihnen die Informationen zum Deadlock schicken kann.
Java 5 Programmierhandbuch
787
Synchronisation
[IconInfo]
Zur Demonstration eines Deadlocks werden zwei Objekte verwendet, die für die Verwaltung der Monitore benutzt werden. Dies könnten z.B. auch zwei Konto-Objekte wie aus den vorigen Beispielen sein. Danach werden zwei Threads erzeugt, denen die Objekte im Konstruktor übergeben werden. Die beiden Threads besitzen in ihrer run()-Methode zwei verschachtelte synchronized()-Blöcke, welche die beiden Objekte als Monitor nutzen. Der einzige Unterschied der beiden Threads besteht darin, in welcher Reigenfolge die beiden Objekte blockiert werden. Dies ist in einer solchen Beispielanwendung sicher einfach zu erkennen und zu beheben, kann aber bei komplexerem Code durchaus übersehen werden. Da die run()-Methoden eine unendlich lang laufende Schleife besitzen, tritt irgendwann der Fall ein, dass ein ThreadDead1-Objekt das Objekt o1 blockiert und danach der Thread ThreadDead1 zum Zuge kommt. Das ThreadDead2-Objekt blockiert zuerst das Objekt o2 und wartet auf die Freigabe von o1. Dieses ist aber bereits vom Thread td1 gesperrt, der wiederum auf Freigabe von o2 wartet. Es liegt ein Deadlock vor.
Listing 30.14: \Beispiele\de\j2sebuch\kap30\Deadlocks.java public class Deadlocks { Object o1 = new Object(); Object o2 = new Object(); public Deadlocks() { ThreadDead1 td1 = new ThreadDead1(o1, o2); ThreadDead2 td2 = new ThreadDead2(o1, o2); td1.start(); td2.start(); } public static void main(String[] args) { new Deadlocks(); } } class ThreadDead1 extends Thread { Object o1; Object o2; public ThreadDead1(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } public void run() { while(true) {
788
30 – Threads Listing 30.14: \Beispiele\de\j2sebuch\kap30\Deadlocks.java (Forts.) synchronized(o2) { synchronized(o1) { System.out.println("Thread 1"); }}}}} class ThreadDead2 extends Thread { Object o1; Object o2; public ThreadDead2(Object o1, Object o2) { this.o1 = o1; this.o2 = o2; } public void run() { while(true) { synchronized(o1) { synchronized(o2) { System.out.println("Thread 2"); }}}}}
Betätigen Sie, nachdem die Anwendung keine Ausgaben mehr durchführt, unter Windows die Tastenkombination Ÿ + þPauseÿ, wird eine umfangreiche Ausgabe auf der Konsole erzeugt. Im letzten Abschnitt dieser Ausgabe finden Sie die Stellen im Quellcode, an denen der Deadlock erzeugt wurde. Java stack information for the threads listed above: =================================================== "Thread-1": at de.j2sebuch.kap30.ThreadDead2.run(Deadlocks.java:61) - waiting to lock (a java.lang.Object) - locked (a java.lang.Object) "Thread-0": at de.j2sebuch.kap30.ThreadDead1.run(Deadlocks.java:38) - waiting to lock (a java.lang.Object) - locked (a java.lang.Object) Found 1 deadlock.
30.11 Datenaustausch zwischen Threads Der Austausch von Daten über Thread-Grenzen hinaus kann z.B. über Variablen einer gemeinsamen Klasse oder dafür vorgesehene Methoden erfolgen. Auch der Einsatz von Listenern ist möglich. Es registriert sich ein Thread bei einem anderen, um bei einem bestimmten Ereignis benachrichtigt zu werden.
Java 5 Programmierhandbuch
789
Datenaustausch zwischen Threads
Eine weitere Möglichkeit ist die Verwendung von Pipes, die durch Streams realisiert werden. Ein Thread schreibt dazu in einen Stream (Produzent), den ein anderer Thread liest (Konsument). Durch eine gepufferte Datenübertragung geht nichts verloren. Kann der Konsumenten-Thread nicht so schnell lesen wie der Produzenten-Thread schreibt, wird der Produzenten-Thread blockiert. Liegen keine zu lesenden Daten vor, blockiert der Konsumenten-Thread. Die Synchronisationsarbeit zwischen den Threads wird durch die Lese- und Schreibmethoden realisiert. Je nachdem, ob Sie byte- oder zeichenweise Daten über die Pipe übertragen möchten, stehen Ihnen verschiedene Pipe-Klassen zur Verfügung. Für die Übertragung von Bytes werden der PipedInputStream und der PipedOutputStream verwendet, zur Übertragung von Zeichen der PipedReader und der PipedWriter. Beim Erzeugen der schreibenden Pipe wird im Konstruktor die lesende Pipe angegeben. Damit wird eine Verbindung zwischen den Pipes hergestellt. Alternativ können Sie die Methode connect() der schreibenden Pipe verwenden, der als Parameter die lesende Pipe übergeben wird. Die Verbindung kann bereits im Hauptprogramm hergestellt werden. Die Pipes werden dann über die Konstruktoren den Threads übergeben. Es wird noch einmal ein Produzent-Konsument-Beispiel verwendet, um die Kommunikation über Pipes zu zeigen. Der Produzent schickt über die Pipe einige Zeichenketten, die der Konsument auf der Konsole ausgibt. Die Zeichenkette QUIT beendet die Kommunikation. [IconInfo]
Der Thread ThreadProduzent benutzt zum Schreiben in die Pipe einen PipeWriter und einen BufferedReader. Damit lassen sich Zeichenketten einfach übertragen. Jede Zeile wird mit einem Zeilenumbruch (statt \n kann plattformunabhängig auch newLine() verwendet werden) und dem Leeren des Puffers mit flush() einzeln übertragen.
Listing 30.15: \Beispiele\de\j2sebuch\kap30\ThreadKommunikation.java class ThreadProduzent extends Thread { private PipedWriter pipeOut = null; private BufferedWriter bw = null; public ThreadProduzent(PipedWriter pipeOut) { this.pipeOut = pipeOut; bw = new BufferedWriter(pipeOut); } public void run() { try { bw.write("Halli\n"); bw.flush();
790
30 – Threads Listing 30.15: \Beispiele\de\j2sebuch\kap30\ThreadKommunikation.java bw.write("QUIT\n"); bw.flush(); } catch(IOException ioEx) {} } }
Der Thread ThreadKonsument liest die Daten mit einem BufferedReader und einem PipedReader aus der Pipe. Damit lassen sich Zeichenketten einfach zeilenweise lesen. Nach der Übergabe der Zeichenkette QUIT wird die while-Schleife und damit auch die Methode run() beendet. Listing 30.16: \Beispiele\de\j2sebuch\kap30\ThreadKommunikation.java class ThreadKonsument extends Thread { private PipedReader pipeIn = null; private BufferedReader br = null; public ThreadKonsument(PipedReader pipeIn) { this.pipeIn = pipeIn; br = new BufferedReader(pipeIn); } public void run() { String zeile = ""; while(!zeile.equals("QUIT")) { try { zeile = br.readLine(); System.out.println(zeile); } catch(IOException ioEx) {} } } }
Das Hauptprogramm erstellt die Pipes und die Threads. Beim Erzeugen der Pipes ist zu beachten, dass der schreibenden Pipe das Endstück, also die lesende Pipe, als Parameter übergeben wird. Den Threads wird im Konstruktor jeweils das entsprechende Pipe-Objekt übergeben.
Java 5 Programmierhandbuch
791
Die Concurrency Utilities Listing 30.17: \Beispiele\de\j2sebuch\kap30\ThreadKommunikation.java import java.io.*; public class ThreadKommunikation { public ThreadKommunikation() { PipedReader pipeIn = new PipedReader(); PipedWriter pipeOut = null; try { pipeOut = new PipedWriter(pipeIn); } catch(IOException ioEx) {} ThreadProduzent tp = new ThreadProduzent(pipeOut); ThreadKonsument tk = new ThreadKonsument(pipeIn); tp.start(); tk.start(); } public static void main(String[] args) { new ThreadKommunikation(); } }
30.12 Die Concurrency Utilities Der Entwickler des Concurrency API, Doug Lea, beschäftigte sich schon einige Jahre mit nebenläufigen Prozessen in Java. Seine Entwicklungen halten nun Einzug in die J2SE 5.0. Im JCP werden sie unter dem JSR 166 beschrieben. Die Klassen und Interfaces sind über die Packages java.util.concurrent, java.util.concurrent.atomic sowie java.util.concurrent.locks verteilt. Die Standardmöglichkeiten von Java zur Synchronisation mehrerer Threads sind nicht sehr umfangreich. Außerdem ist hier bisher sehr viel Handarbeit notwendig gewesen, um die Synchronisation korrekt zu implementieren. Die Concurrency Utilities umfassen die folgenden Funktionalitäten: 쐌 쐌 쐌 쐌 쐌 쐌 쐌
Verwaltung von Threadpools Verwaltung mehrerer Tasks Thread-sichere Warteschlangen Synchronisation (z.B. über Semaphore) Thread-sichere Collections Thread-sicherer Zugriff auf Variablen (atomarer Zugriff) Sperren und bedingte Sperren
792
30 – Threads
Beispiele Beim Ändern von Werten einer Variablen kann es passieren, dass der Wert zwar geändert wurde, aber das Auslesen erst bei der nächsten Ausführung des Threads erfolgt. Wurde der Wert in der Zwischenzeit durch einen anderen Thread geändert, ist der Rückgabewert verfälscht. Durch atomare Operationen wird dies verhindert. AtomicInteger ai = new AtomicInteger(0); int wert = ai.addAndGet(2);
Die Überweisung von einem Konto auf ein anderes ist immer eine atomare Operation, die aber aus zwei Anweisungen besteht. Der Betrag wird von einem Konto abgebucht und auf das andere Konto überwiesen. Dazu wird in einem Beispielprogramm über die Methode umbuchung() ein Betrag wechselseitig auf ein Konto gebucht und von einem anderen abgebucht. Irgendwann wird der Thread nach nur einem Buchungsvorgang unterbrochen, so dass inkonsistente Werte zu diesem Zeitpunkt auf den Konten entstehen. Listing 30.18: \Beispiele\de\j2sebuch\kap30\Semaphor.java class SemaphorTest1 extends Thread { private static int konto1 = 0; private static int konto2 = 100; static void umbuchung(int wert) { konto1 = konto1 + wert; konto2 = konto2 - wert; } public void run() { while(true) { umbuchung(100); umbuchung(-100); if(konto1 == konto2) { System.out.println("Inkonsistenter Zustand"); break; } } } }
Semaphore dienen dazu, den Zugriff auf bestimmte Systemressourcen zu beschränken. Im Falle von nebenläufigen Threads heißt dies, dass nur eine begrenzte Anzahl von Threads einen bestimmten Programmabschnitt durchlaufen darf, im einfachsten Fall nur einer.
Java 5 Programmierhandbuch
793
Die Concurrency Utilities Durch die Verwendung eines Semaphors wird im folgenden Beispiel nur ein Thread in den kritischen Abschnitt der Methode umbuchung() gelassen (der erste Parameter im Konstruktor des Semaphors). Über den zweiten Parameter legen Sie fest, dass wartende Threads fair behandelt werden, d.h., dass sie in der ankommenden Reihenfolge in den kritischen Abschnitt gelassen werden. Die Methode acquireUninterruptibly() blockiert so lange alle anderen Threads, bis der aktuelle Thread den Abschnitt bis zum Aufruf von release() durchlaufen hat.
[IconInfo]
Listing 30.19: \Beispiele\de\j2sebuch\kap30\ Semaphor.java class SemaphorTest2 extends Thread { private static Semaphore s = new Semaphore(1, true); private static int konto1 = 0; private static int konto2 = 100; static void umbuchung(int wert) { s.acquireUninterruptibly(); konto1 = konto1 + wert; konto2 = konto2 - wert; s.release(); } public void run() { while(true) { umbuchung(100); umbuchung(-100); if(konto1 == konto2) { System.out.println("Inkonsistenter Zustand"); break; } } } }
794
31 Netzwerkanwendungen 31.1 Einführung Für die Kommunikation zwischen mehreren Computern wird eine Möglichkeit benötigt, Daten in beiden Richtungen auszutauschen. In der Regel werden diese Computer über ein Netzwerk miteinander verbunden. Das bedeutendste Netzwerk ist sicher das Internet, über das täglich Millionen von Nutzern miteinander kommunizieren. Dabei kann die Kommunikation beliebig untereinander stattfinden. Mehrere Computer (Clients) nehmen dabei die Serviceleistungen anderer Computer (Server) in Anspruch. Ein Computer kann als Client wie auch als Server auftreten. Typische Netzwerkanwendungen sind: 쐌 Datenbankanwendungen, bei denen sich auf einem Rechner das Datenbanksystem befindet und darauf von mehreren anderen Rechnern zugegriffen wird 쐌 Chat-Anwendungen 쐌 Peer-to-Peer-Anwendungen, z.B. zum Austausch von Daten 쐌 Browser, die eine Verbindung zu einem Webserver herstellen 쐌 Mail-Anwendungen 쐌 Sämtliche Anwendungen, die eine Kommunikation zwischen zwei oder mehreren Rechnern herstellen (FileServer) Für die Herstellung einer Verbindung zwischen mehreren Computern im Netzwerk müssen sich diese auf gewisse Kommunikationsregeln, so genannte Protokolle, verständigen. Protokolle regeln, wie eine Verbindung zustande kommt, wie Anfragen gestellt und beantwortet werden. Weiterhin wird eine Möglichkeit benötigt, einen Computer in einem Netzwerk überhaupt ausfindig zu machen und ihn später während der Kommunikation immer eindeutig zu identifizieren. Ein Computer benötigt also eine Adresse, unter der man ihn erreichen kann. In Netzwerken werden dazu IP-Adressen eingesetzt. Die Kommunikation zwischen mehreren PCs wird zur Veranschaulichung über verschiedene Ebenen beschrieben. Das ISO/OSI-Referenzmodell unterscheidet beispielsweise sieben Schichten, vgl. Abbildung 31.1. Jede Schicht verfügt über ein oder mehrere Protokolle, die auf die darunter liegenden Protokolle aufbauen. Kommunizieren zwei PCs über die Schicht 5, werden zuerst alle Protokolle der Schichten 5 bis 1 auf dem ersten PC durchlaufen, dann werden die Daten über das physikalische Netzwerk (die Leitung) zum PC 2 übertragen, wo sie wieder die Schichten 1 bis 5 durchlaufen. Die Schichten 1 und 2 sorgen mit der physikalischen Bereitstellung (Kabel, Netzwerkkarten) und dem Aufteilen der Daten in Pakete sowie deren Erweiterung um Prüfsummen für die Basis eines Datenaustauschs. Die eigentliche Übertragung der Pakete, das Finden eines Wegs von A nach B, wird in Schicht 3 erledigt. Hierfür wird heutzutage in der Regel das IPProtokoll (Internet Protocol) eingesetzt. Die vierte Schicht ist für den Transport eines Datenpakets zuständig, das ihr von einer der darüber liegenden Schichten übergeben wurde. Im Falle von TCP (Transmission Control Protocol) wird sichergestellt, dass die Datenpakete in der richtigen Reigenfolge beim Empfänger ankommen und verloren gegangene erneut gesendet werden. Auf diese Weise wird sichergestellt, dass auch wirklich das gesamte Datenpaket beim Empfänger ankommt. Diese Anforderungen stellen hohe Ansprüche an
Java 5 Programmierhandbuch
795
Einführung
Schichten PC 1
Protokolle
7. Anwendungsschicht 6. Darstellungsschicht
Schichten PC 2
7. Anwendungsschicht Verschiedene Protokolle HTTP, FTP SMTP, POP3
5. Sitzungsschicht
6. Darstellungsschicht 5. Sitzungsschicht
4. Transportschicht
TCP oder UDP
4. Transportschicht
3. Netzwerkschicht
IP
3. Netzwerkschicht
2. Leitungsschicht
2. Leitungsschicht Hardware
1. Physikalische Schicht
1. Physikalische Schicht
Abb. 31.1: Das ISO/OSI-Referenzmodell
das Übertragungsprotokoll und benötigen relativ viel Aufwand. UDP (User Datagram Protocol), ein weiteres Protokoll der Schicht 4, wird verwendet, wenn der Aufwand von TCP nicht im Verhältnis zum Nutzen steht. UDP sendet immer nur einzelne Datenpakete und stellt beim Absenden mehrerer Datenpakete nicht die Ausgangsreihenfolge sicher. UDP sichert nicht einmal, dass ein Datenpaket überhaupt ankommt. Dies muss von den Anwendungen bemerkt werden, die ein erneutes Versenden des Datenpakets initiieren können. Das Versenden ähnelt damit bei UDP dem Einwerfen von 100 Briefen in einen Briefkasten. Diese müssen nicht zwangsläufig am gleichen Tag ankommen und es kann theoretisch auch einer verloren gehen. Selbst wenn alle am selben Tag ankommen, kann nicht wieder die Reihenfolge wie beim Einwerfen hergestellt werden (außer man nummeriert die Briefe vorher). Die Schichten 5 bis 7 setzen nun auf die beiden Übertragungsmöglichkeiten TCP/IP und UDP/IP auf. Diese können weitere Protokolle implementieren, wie HTTP (hypertext transfer protocol) für den Datenaustausch im WWW oder FTP (file transfer protocol) für die Übertragung von Dateien. Da beide auf die vollständige und gesicherte Datenübertragung angewiesen sind, setzen sie beispielsweise auf TCP/IP auf. Die Protokolle für DNS (domain name service) oder TFTP (trivial file transfer protocol) setzen auf UDP/IP auf, weil es hier auf hohe Performance ankommt bzw. im Falle von DNS der Aufwand für die Verwendung von TCP nicht im Verhältnis zum Nutzen steht. Sollte ein Paket nicht beim Client ankommen, fragt dieser eben erneut nach. IP-Adressen Für die Adressierung der Rechner in einem Netzwerk (auch als Hosts bezeichnet) werden so genannte IP-Adressen verwendet. Diese bestehen aus vier Teilen, die durch Punkte getrennt sind. Jeder Teil besitzt einen Wertebereich von 0 bis 255, so dass eine IP-Adresse
796
31 – Netzwerkanwendungen
über 4 Byte kodiert werden kann. Theoretisch kann man auf diese Weise ca. 4 Milliarden IP-Adressen vergeben. Da dies aufgrund der Vielzahl von Rechnern und reservierten Bereiche kaum noch reicht, wurden neue IP-Adressen definiert (IPv6), die durch 16 Byte kodiert werden. Bisher kommen diese aber in öffentlichen Netzwerken nicht zum Einsatz und werden im Folgenden nicht verwendet. Die IP-Adressen innerhalb eines geschlossenen Netzwerks müssen immer eindeutig sein. Deshalb werden die IP-Adressen für an das Internet angeschlossene Rechner durch eine zentrale Stelle vergeben. Verbinden Sie sich über ein Modem oder DSL in das Internet, weist Ihnen Ihr Internet Service Provider (z.B. T-Online oder 1&1) dynamisch eine IPAdresse zu. In einem Netzwerk existieren einige reservierte Adressen bzw. Adressbereiche. Die Loopback-Adresse 127.0.0.1, auch als Localhost bezeichnet, dient der Verbindungsaufnahme mit dem eigenen Rechner. Dadurch können Sie Netzwerkanwendungen mit Clients und Servern entwickeln, die auf dem gleichen Rechner laufen. Eine Broadcast-Adresse versendet eine Nachricht an alle Rechner eines Netzwerks. Der entsprechende Teil der IP-Adresse erhält in diesem Fall den Wert 255. Weitere Adressbereiche sind für interne Netzwerke reserviert, die nicht mit dem Internet verbunden sind. Sie stellen verschiedene Adressbereiche mit unterschiedlichem Umfang bereit. Adresse/Adressbereich
Bedeutung
255.255.255.255
Broadcast
127.0.0.1
Localhost - Loopback (eigener Rechner)
Klasse-A-Netzwerk
10.0.0.0 bis 10.255.255.255
Klasse-B-Netzwerk
172.16.0.0 bis 172.31.255.255
Klasse-C-Netzwerk
192.168.0.0 bis 192.168.255.255
Domain Name Service Da sich IP-Adressen schlecht merken lassen, wurden Hostnamen eingeführt, die einer IPAdresse zugeordnet werden. Für eine IP-Adresse kann es mehrere Hostnamen geben. Ein Hostname ist aber immer mit genau einer IP-Adresse verbunden. Wenn Sie nun einen Hostnamen verwenden, muss irgendwie die zugehörige IP-Adresse ermittelt werden, um den Rechner zu erreichen. Dies übernimmt der Domain Name Service. DNS-Server, die lose miteinander verbunden sind, verwalten Tabellen, die Zuordnungen zwischen Hostnamen und IP-Adressen enthalten. URLs Mit einem Hostnamen wird ein bestimmter Rechner in einem Netzwerk identifiziert. Um einzelne Ressourcen (Dateien, Datenbanken) auf einem Host anzusprechen, werden z.B.
Java 5 Programmierhandbuch
797
Einführung
URLs (Uniform Resource Locator) verwendet. Manchmal wird auch der umfassendere Begriff URI (Uniform Resource Identifier) zur Identifikation von Netzwerkressourcen benutzt. Ein URI ist dabei ein URL oder ein URN (Uniform Ressource Name). URNs werden momentan kaum verwendet, so dass wir wieder bei URLs sind.
URI URL
URN
Abb. 31.2: URI, URL und URN
Ports Grundsätzlich ist ein Computer über genau eine Verbindung, z.B. eine Netzwerkkarte, mit dem Netzwerk verbunden. Mehrere Anwendungen können jetzt über das Netzwerk mit anderen Rechnern kommunizieren. Damit die übertragenen Daten den verschiedenen Anwendungen zugeordnet werden können, kommen so genannte Ports zum Einsatz. Diese werden durch eine 2 Byte große, vorzeichenlose Zahl identifiziert. Dadurch stehen Portnummern von 0 bis 65535 zur Verfügung. Die Portnummern von 0 bis 1023 sind für Standarddienste reserviert und sollten nicht von Ihnen verwendet werden. Man bezeichnet sie auch als well-known Ports. Die Portnummern von 49152 bis 65535 werden oft vom Betriebssystem vergeben, wenn dynamisch Portnummern benötigt werden. Manchmal kommen dazu auch die Portnummern von 1024 bis 5000 zum Einsatz. Am sichersten ist also die Verwendung von Portnummern zwischen 10000 und 40000. Dies sollte in der Regel für alle Anwendungsfälle ausreichen. In der Datei mit dem Namen service werden bereits Zuordnungen von Ports zu Diensten vorgenommen. In Windows XP finden Sie die Datei z.B. unter [WindowsDir]\system32\drivers\etc und in Linux unter /etc. Besonders unter Linux ist der Inhalt der Datei sehr aufschlussreich, da diese sehr viele Einträge enthält. Port/Protokoll
Beschreibung
7 (TCP/UDP)
Der Echo-Dienst liefert die an ihn gesendeten Daten wieder an den Client zurück.
21 (TCP)
FTP dient der Übertragung von Dateien.
23 (TCP)
Über Telnet kann man auf einem entfernten Rechner arbeiten.
25 (TCP)
Über SMTP werden Mails versendet.
80 (TCP)
Das HTTP-Protokoll dient zum Datenaustausch im WWW.
110 (TCP)
Der Empfang von E-Mails wird durch POP3 ermöglicht.
798
31 – Netzwerkanwendungen
Eine Verbindung wird damit einerseits unter Einsatz einer IP-Adresse aufgebaut, die einen bestimmten Rechner identifiziert, und andererseits über eine Portnummer, die mit einer Anwendung verbunden ist. Sockets Die Kombination aus einer Portnummer und einer IP-Adresse wird durch einen Socket beschrieben. Häufig wird noch das verwendete Übertragungsprotokoll einbezogen, also TCP oder UDP, da eine Portnummer für jedes Protokoll separat bereitgestellt wird. Eine Verbindung zwischen einem Client und einem Server erfolgt also zusätzlich über einen bestimmten Port. RFCs Viele der angesprochenen Protokolle und Dienste werden durch so genannte RFCs (request for comments) beschrieben, die im Textformat verfasst sind. Sie stehen kostenlos zum Download zur Verfügung und dienen als gute Informationsquelle, wenn Sie sich mit einem Protokoll oder Dienst näher beschäftigen wollen (oder müssen). Als Startpunkt kann z.B. die Webseite http://www.ietf.org/rfc.html dienen. Hier können Sie eine Liste aller verfügbaren RFCs und die Dokumente selbst laden. Beachten Sie, dass es zu einem Protokoll oder Dienst auch mehr als ein RFC geben kann. RFC-Nummer
Inhalt
114, 959
FTP
791
IP
821
SMTP
1180
TCP/IP Tutorial
1939
POP3
1945, 2068
HTTP 1.0 und 1.1
2168
DNS
Netzwerkanwendungen mit Java – eine Übersicht Die meisten Klassen und Interfaces zur Arbeit mit Netzwerken finden Sie im Package java.net. Weitere Klassen gibt es in den Packages javax.net und javax.net.ssl. Sämtliche Netzwerkfunktionalität in Java baut auf den Protokollen TCP/IP und UDP/IP auf. Für das IP-Protokoll werden in jedem Fall IP-Adressen benötigt, die in Java durch die Klasse InetAddress verwaltet werden. Die speziellen Klassen Inet4Address und Inet6Address dienen der Verwaltung der IP4v- bzw. IPv6-Adressen.
Java 5 Programmierhandbuch
799
Zugriff auf Netzadressen
Für den Zugriff auf Netzwerkressourcen dienen die Klassen URL, URLConnection und HttpURLConnection. Sie können damit beispielsweise Dateien recht einfach von einem Webserver laden oder zu einem Webserver senden. Stream-Sockets (oder kurz Sockets) stellen eine TCP-Verbindung her. Nach dem Aufbau besteht die Verbindung so lange, bis sie beendet wird, d.h. sämtliche Daten übertragen wurden. Deshalb wird TCP auch als verbindungsorientiertes Protokoll bezeichnet. Datagramme werden innerhalb einer UDP-Verbindung eingesetzt. Sie werden an die Zieladresse abgeschickt, ohne dass auf eine Rückantwort gewartet wird. Da hier keine Verbindung mit dem Zielrechner zustande kommt, wird UDP auch als verbindungsloses Protokoll bezeichnet. Server stellen in einem Netzwerk einen Service zur Verfügung, der durch Clients genutzt werden kann. Wenn ein Server mit einem Client wechselseitig kommuniziert, kann auch ein Client Serverfunktionalität besitzen. Ein Server arbeitet immer passiv, d.h., er wartet auf eingehende Verbindungen. Im Gegensatz zu der Serverfunktionalität auf einem Client (der ja auch auf Antworten vom Server warten muss) können bei einem Server mehrere Anfragen von verschiedenen Clients eintreffen. Über Threads können Sie diese Funktionalität für Netzwerkzugriffe realisieren.
[IconInfo]
Wenn Sie Netzwerkanwendungen mit Java erstellen, kann es trotz korrekter Anwendungen vorkommen, dass kein Verbindungsaufbau bzw. kein Empfang der Daten möglich ist. Dies kann an Firewalls und/oder Proxys liegen, die den Datenverkehr zu bestimmten Ports oder Internetadressen verhindern. Fragen Sie in diesem Fall Ihren Netzwerkadministrator. Proxys werden auch durch einige Java-Klassen unterstützt. Sie benötigen hierfür die Einstellungen und Zugriffsrechte zur Herstellung einer Verbindung über den Proxy (in der Regel beim Zugriff auf das Internet).
31.2 Zugriff auf Netzadressen Bei der Erstellung eines Socket kann später als Parameter ein Objekt vom Typ InetAddress übergeben werden, um einen Host zu identifizieren. Vorteilhaft daran ist, das eine IP-Adresse und der zugehörige Hostname nur einmal verwaltet werden müssen. Ein InetAddress-Objekt verwaltet eine IP-Adresse und eventuell auch einen Hostnamen. Eine IP-Adresse identifiziert einen Host innerhalb eines Netzwerks immer eindeutig. Allerdings lässt sich eine IP-Adresse vom Menschen schlechter merken als ein Name wie www.javamagazin.de. Über einen so genannten Namensdienst, der durch DNS-Server bereitgestellt wird, kann für einen Hostnamen eine IP-Adresse ermittelt werden. Auch der umgekehrte Weg ist möglich. Die Namensauflösung eines Hostnamens in eine IP-Adresse wird durch die Klasse InetAddress automatisch mithilfe eines DNS-Servers durchgeführt.
800
31 – Netzwerkanwendungen
[IconInfo]
Meistens existiert zusätzlich eine Datei mit dem Namen Hosts, in der ebenfalls Beziehungen zwischen IP-Adressen und Hostnamen eingetragen sind. Unter Linux befindet sich diese Datei im Verzeichnis /etc, unter Windows XP beispielsweise unter [WindowsDir]\system32\drivers\etc. Die Datei hat keine Endung.
Ein InetAddress-Objekt wird nicht über einen Konstruktor erstellt, sondern über eine der folgenden statischen Methoden der Klasse InetAddress. Durch die Angabe eines ByteArrays wird bei der Verwendung von 4 Byte eine IPv4, bei Angabe von 16 Byte eine IPv6Adresse erzeugt. Im zweiten Konstruktor wird der Hostname gleich mitgeliefert, so dass eine Auflösung nach dem Hostnamen unterdrückt wird. Im dritten Konstruktor wird automatisch eine Auflösung nach der IP-Adresse durchgeführt. Die lokale IP-Adresse des Rechners erhalten Sie über die vierte Variante. InetAddress InetAddress InetAddress InetAddress
getByAddress(byte[] addr) getByAddress(String host, byte[] addr) getByName(String host) getLocalHost()
Nachdem Sie nun ein InetAddress-Objekt besitzen, können Sie es an andere Methoden übergeben, welche die enthaltene IP-Adressangabe nutzen können. Die Klasse InetAddress enthält nützliche Methoden, von denen einige vorgestellt werden sollen. Die Methode getAddress() liefert z.B. die IP-Adresse in ihren Bestandteilen. Die Länge ist von der verwendeten IP-Adresse abhängig. byte[] getAddress()
Über die folgende Methode werden alle IP-Adressen des angegebenen Hosts bestimmt. InetAddress[] getAllByName(String host)
Es lassen sich verschiedene Repräsentationen der IP-Adresse oder des Hostnamens als String ermitteln. Der kanonische Hostname entspricht dem Domainnamen, dem die IPAdresse angehört. Alle anderen Werte sind bekannt. Hinter der Methode wird jeweils ein Beispielwert angegeben. String String String String
getCanonicalHostName() getHostAddress() getHostName() toString()
// // // //
DNS-Host 192.168.10.10 Mein-PC Mein-PC/192.168.10.10
Um die Erreichbarkeit eines Hosts zu prüfen, ist die Methode isReachable() nützlich. Als Parameter wird die Wartezeit auf eine Antwort in Millisekunden angegeben. boolean isReachable(int timeout)
Java 5 Programmierhandbuch
801
Zugriff auf Netzadressen
Die Klasse InetAddress verwendet einen internen Cache, um bereits durchgeführte Umwandlungen zwischen IP und Hostnamen zu speichern. Ändern sich aber solche Beziehungen, werden diese eventuell nicht erkannt. Über die folgenden Systemeigenschaften können Sie das Cache-Verhalten steuern: networkaddress.cache.ttl networkaddress.cache.negative.ttl ttl steht hier für time to live und wird in Sekunden angegeben. Die Standardwerte sind -1 für die erste und 10 für die zweite Eigenschaft. Der Wert -1 steht für eine dauerhafte Speicherung. Um einmal alle IP-Adressen des lokalen Hosts zu bestimmen, kann das folgende Beispiel genutzt werden. Bei Verwendung der Loopback-Adresse localhost ist es immer die IP 127.0.0.1. Benutzen Sie statt der Methode getLocalHost() die Methode getByName() und übergeben Sie einen gültigen Hostnamen, z.B. java.sun.com, werden dessen IP-Adressdaten ausgegeben.
[IconInfo]
Listing 31.1: \Beispiele\de\j2sebuch\kap31\InternetAdressen.java import java.net.*; public class InternetAdressen { public InternetAdressen() { try { InetAddress ia1 = InetAddress.getLocalHost(); // InetAddress ia1 = InetAddress.getByName("java.sun.com"); System.out.println(ia1.getHostName()); System.out.println(ia1.getHostAddress()); System.out.println(ia1.getCanonicalHostName()); System.out.println(ia1.toString()); System.out.println("----------------------------"); InetAddress[] ia = InetAddress.getAllByName("localhost"); for(InetAddress a: ia) System.out.println(a.toString()); } catch(Exception _uh) {} } public static void main(String args[]) { new InternetAdressen(); } }
802
31 – Netzwerkanwendungen
Netzwerkschnittstellen Ein Rechner kann heutzutage mehrere Zugänge zu verschiedenen oder dem gleichen Netzwerk besitzen. So gibt es den klassischen Weg über eine Netzwerkkarte, die Verbindung zum Internet (über eine Netzwerkkarte oder ein Modem), eine Infrarotschnittstelle und natürlich Funkverbindungen. Über jede dieser Schnittstellen können Sie unter einer anderen IP-Adresse arbeiten. Um alle IP-Adressen der verschiedenen Netzwerkschnittstellen zu bestimmen, können Sie die Klasse NetworkInterface nutzen.
[IconInfo]
Über die Methode getNetworkInterfaces() wird ein EnumeratorObjekt zurückgegeben, mit dessen Hilfe die Schnittstellen durchlaufen werden können. Durch die Verwendung von Generics kann man sich den Cast beim Aufruf von nextElement() sparen. Den Namen der Schnittstelle ermittelt die Methode getName(). Die Internetadressen der Schnittstelle lassen sich über ein zweites Enumerator-Objekt bestimmen. Testen Sie das Programm beispielsweise mit und ohne Internetverbindung, wenn Sie über ein Modem oder DSL verfügen. Sie werden eine Schnittstelle mehr angezeigt bekommen, wenn Sie verbunden sind.
Listing 31.2: \Beispiele\de\j2sebuch\kap31\Netzschnittstellen.java import java.net.*; import java.util.*; public class Netzschnittstellen { public Netzschnittstellen() { try { Enumeration nis = NetworkInterface.getNetworkInterfaces(); while(nis.hasMoreElements()) { NetworkInterface ni = nis.nextElement(); System.out.println(ni.getName()); Enumeration ias = ni.getInetAddresses(); while(ias.hasMoreElements()) { InetAddress ia = ias.nextElement(); System.out.println(ia.getHostAddress()); } System.out.println("========================"); } } catch(Exception _uh) {} }
Java 5 Programmierhandbuch
803
Arbeiten mit URLs Listing 31.2: \Beispiele\de\j2sebuch\kap31\Netzschnittstellen.java (Forts.) public static void main(String args[]) { new Netzschnittstellen(); } }
31.3 Arbeiten mit URLs Die meisten kennen URLs in der Form, dass sie Ressourcen im Internet referenzieren, z.B. HTML-Seiten. Sie können aber auch auf lokale Dateien oder andere Dinge verweisen. Der allgemeine Aufbau einer URL (in diesem Fall speziell für die Verwendung im HTTP-Protokoll) ist: Protokoll://Nutzer:Passwort@Hostname:Port/Pfad#Anker?Parameter
Als Protokolle kommen beispielsweise http, ftp oder file in Frage. Letzteres kann für den Zugriff auf lokale Dateien genutzt werden. Die Angabe eines Benutzernamens und Passworts ist optional und erlaubt z.B. das automatische Anmelden auf einem Webserver. Der Hostname identifiziert den Rechner, mit dem eine Verbindung aufgebaut werden soll. Optional kann eine Portnummer angegeben werden. Ohne Angabe der Portnummer wird die Verbindung über die Standardportnummer des betreffenden Protokolls hergestellt, z.B. über den Port 80 beim HTTP-Protokoll. Der Pfad legt den Zugriffspfad der gewünschten Ressource auf dem Host fest. Ein optionaler Anker verweist auf eine Position innerhalb der Ressource. Mit einem Fragezeichen getrennt können z.B. an Webserver Parameter übergeben werden, die dort von entsprechenden Anwendungen ausgewertet werden. Typische URLs sind: http://www.javamagazin.de/ http://www.j2sebuch.de/index.html
31.3.1
URL-Objekte erzeugen
Bevor von einer URL Daten geladen werden können, muss über einen Konstruktor ein gültiges URL-Objekt erzeugt werden. Dazu stehen Ihnen verschiedene Methoden zur Verfügung. Alle Konstruktoren können eine MalformedURLException auslösen, wenn die Parameter nicht korrekt sind. In der einfachsten Form übergeben Sie im Konstruktor die URL als String. Ein Konstruktor erzeugt hier immer nur das URL-Objekt, baut aber keine Internetverbindung auf. Die URL repräsentiert in diesem Fall eine absolute URL, d.h., sie enthält alle Informationen, um eine Ressource zu identifizieren. URL(String urlName)
Bezogen auf eine absolute URL können im folgenden Konstruktor relative URLs verwendet werden.
804
31 – Netzwerkanwendungen URL(URL basisURL, String relURLName)
Zwei weitere Konstruktoren dienen zur Erstellung einer URL auf der Basis ihrer Grundbestandteile. URL(String protokoll, String host, String datei) URL(String protokoll, String host, String port, String datei)
Beispiele An die absolute URL des ersten Konstruktors wird im zweiten Konstruktor ein Dateiname und ein Anker angefügt. URL url1 = new URL("http://www.javamagazin.de/"); URL url2 = new URL(url1, "index.html#Anker");
31.3.2
URLs parsen
Hat man bereits ein URL-Objekt, können über verschiedene Methoden die Bestandteile der gespeicherten URL ausgewertet werden. Dies sind in der angegebenen Reihenfolge der Dateiname, der Hostname, die Portnummer, das verwendete Protokoll und der Anker (#Anker). Die letzte Methode liefert die URL wieder als String zurück. String getFile() String getHost() int getPort() String getProtocol() String getRef() String toString()
31.3.3
Daten verarbeiten
Sie können nun ein URL-Objekt erzeugen, das auf eine gültige URL verweist, und von dort Daten lesen. Die einfachste Möglichkeit ist die Verwendung der Methode openStream(), die ein InputStream-Objekt liefert. Auf diese Weise lassen sich die Daten einfach auf die Festplatte speichern.
[IconInfo]
Der Zugriff auf Dateien im Internet kann über ein URL-Objekt sehr einfach erfolgen. Dazu wird in der folgenden Anwendung die Webseite index.html der Tagesschau unter dem Dateinamen C:\Temp\index.html gespeichert. Passen Sie gegebenenfalls die Pfadnamen an. Das URL-Objekt wird mit der vollständigen URL erzeugt. Über die Methode openStream() wird ein InputStream zurückgegeben, über das die Daten gelesen und in eine Datei geschrieben werden. Wichtig ist das anschließende Schließen der Streams.
Java 5 Programmierhandbuch
805
Arbeiten mit URLs Listing 31.3: \Beispiele\de\j2sebuch\kap31\URLDatenLesen.java import java.net.*; import java.io.*; public class URLDatenLesen { public URLDatenLesen() { URL url1 = null; try { url1 = new URL("http://www.tagesschau.de/index.html"); } catch(MalformedURLException muEx) {} try { FileWriter outURL = new FileWriter("C:/Temp/index.html"); BufferedReader inURL = new BufferedReader( new InputStreamReader(url1.openStream())); String zeile = ""; while((zeile = inURL.readLine()) != null) outURL.write(zeile, 0, zeile.length()); inURL.close(); outURL.close(); } catch(IOException ioEx) {} } public static void main(String args[]) { new URLDatenLesen(); } }
URL-Verbindungen Die Methode openStream() der Klasse URL ist nur eine Kurzschreibweise des Aufrufs von openConnection().getInputStream(). Die Methode openConnection() liefert ein Objekt vom Typ URLConnection zurück, über das sich zahlreiche Operationen in Zusammenhang mit der Datenübertragung von und zu einer URL (in der Regel mit einem Webserver) ausführen lassen. Eine weitere Form der Methode openConnection() erwartet ein Proxy-Objekt als Parameter, so dass die Kommunikation auch über Proxy-Server ablaufen kann. Die meisten Methoden der Klasse URLConnection sind auf die Kommunikation mit einem Webserver ausgelegt. Sie besitzt z.B. zahlreiche Methoden, um Informationen aus dem HTTP-Header auszulesen. Der HTTP-Header wird immer beim Senden von Daten über das
806
31 – Netzwerkanwendungen
HTTP-Protokoll übertragen und enthält Informationen zum Typ des übertragenen Inhalts (HTML-Test, Bilder, Videos) und zur verwendeten Kodierung. Zusätzlich bietet die Klasse Methoden zum Übertragen von Daten zu einem Webserver. Ein Anwendungsfall ist beispielsweise das Senden von Formulardaten eines HTML-Formulars. Auf dem Webserver werden die übertragenen Daten an entsprechende Anwendungen übergeben, die wiederum eine Rückgabe erzeugen. Diese Programme können CGI-Skripte sein (z.B. Perl oder PHP) oder Servlets und JSPs.
[IconInfo]
Für eine einfache und professionelle Kommunikation über das HTTP-Protokoll bietet die Klasse URLConnection zu wenig. Die spezialisierte Klasse HttpURLConnection besitzt noch weitere angepasste Methoden. Der freie HttpClient, der unter http://jakarta.apache.org/commons/ httpclient/ bezogen werden kann, verfügt da schon über wesentlich mehr Funktionalität.
31.4 Socket-Verbindungen Viele Anwendungen, die auf eine Kommunikation zwischen zwei und mehr Rechnern angewiesen sind, benötigen eine dauerhafte und sichere Verbindung. Daten dürfen nicht verloren gehen und die Reihenfolge der Daten muss beibehalten werden. Dies können Datenbankanwendungen sein oder einfach nur die Übertragung einer Datei. Als Protokoll bietet sich TCP/IP an. Auf jedem Rechner wird dazu ein Socket erzeugt. Ein Socket ist ein Endpunkt in einer Netzwerkverbindung zweier Rechner in einem Netzwerk, die über das TCP/ IP-Protokoll kommunizieren. Die Verbindung bleibt so lange bestehen, bis ein Socket geschlossen wird. Aufbau einer Verbindung mit Sockets – eine Übersicht Java verwendet zwei Klassen zum Aufbau einer Client-Server-Verbindung über Sockets. Über die Klasse ServerSocket wird der Socket auf dem Server mit einem bestimmten Port verbunden. Danach wird die Methode accept() aufgerufen, die jetzt auf eingehende Client-Verbindungen wartet. Möchte sich ein Client mit dem Socket verbinden, wird ein neues Socket-Objekt von accept() zurückgegeben, über das die Verbindung mit dem Client abgewickelt wird. Dadurch kann die Methode accept() auf weitere Anforderungen warten. Der neue Socket besitzt die gleiche Portnummer wie der ServerSocket. Die Portnummer auf den Clients wird dagegen in der Regel dynamisch vergeben.
[IconInfo]
Bei der Verwendung von Sockets können zahlreiche Exceptions auftreten. In den Beispielen dieses Kapitels werden diese, um den Programmcode nicht noch weiter zu vergrößern, nur teilweise bzw. gar nicht behandelt. Damit der Quellcode auch bei sorgfältiger Auswertung von Exceptions nicht zu umfangreich wird, sollten Sie Klassen entwickeln, die Ihre jeweilige Netzwerkanwendung bzw. -kommunikation kapseln und die alle möglichen Exceptions entsprechend behandeln.
Java 5 Programmierhandbuch
807
Socket-Verbindungen Server
Client
erzeuge Instanz der Klasse ServerSocket 1 rufe Methode accept() auf und warte
erzeuge Instanz der Klasse Socket
2
3 4 erzeuge Instanz der Klasse Socket
Abb. 31.3: Herstellen einer Socket-Verbindung
31.4.1
ClientSockets
Ein ClientSocket wird über die Klasse Socket erstellt. Diese besitzt verschiedene Konstruktoren, denen unter anderem eine IP-Adresse oder ein Hostname sowie eine Portnummer übergeben werden können. Ist die Erstellung des Socket nicht möglich, wird eine IOException ausgelöst. Socket(String host, int port) Socket(InetAddress address, int port)
Nach dem Erstellen eines Socket können Streams zum Lesen und Schreiben von Daten geöffnet werden. Hierfür werden die Methoden getOutputStream() und getInputStream() verwendet. Nachdem die Kommunikation über den Socket abgeschlossen ist, sollten zuerst die Streams und danach der Socket wieder geschlossen werden, damit keine Systemressourcen belegt bleiben. Socket sClient = new Socket("HostName", portNr); PrintWriter sOut = new PrintWriter( sClient.getOutputStream(), true); BufferedReader sIn = new BufferedReader( new InputStreamReader(sClient.getInputStream())); ... sIn.close(); sOut.close(); sClient.close();
808
31 – Netzwerkanwendungen
Über Streams können Sie binäre wie auch Textdaten austauschen. Bei der Übertragung von Textdaten und dem zeilenweisen Lesen dieser Daten müssen Sie darauf achten, dass einerseits die Zeilenumbrüche mit übertragen werden und andererseits der Ausgabepuffer immer mit der Methode flush() geleert wird. sOut.write("StartGame\n"); // bzw. systemunabhängig sOut.write("StartGame"); sOut.newLine(); // und Puffer leeren sOut.flush();
// Zeilenumbruch über \n
Damit besteht das Hauptproblem bei Socket-Anwendungen in der entsprechenden Implementierung des Protokolls zwischen Client und Server. Dazu wird beispielsweise in einer while-Schleife ununterbrochen gelesen und der gelesene String über eine Methode verarbeiteEingang() ausgewertet. Die Methode sendeDaten() schickt das Ergebnis der Verarbeitung zurück. String eingangString = ""; while((eingangString = br.readLine()) != null) { verarbeiteEingang(eingangString); sendeDaten(); }
Das byteweise Lesen kann folgendermaßen erfolgen. Es lassen sich beispielsweise Zahlen einfacher übertragen und die Menge der zu übertragenden Daten ist geringer als bei einer textbasierten Kommunikation. Allerdings wird mehr Aufwand für die Verarbeitung der Daten benötigt. int len; byte[] daten = new byte[256]; InputStream sIn = cSock.getInputStream(); while((len = sIn.read(daten)) != -1) ... // oder byte daten; while((daten = (byte)sIn.read()) != -1) ...
Wichtige Methoden Nachdem die Kommunikation über einen Socket beendet wurde, sollte er wieder geschlossen werden. void close()
Java 5 Programmierhandbuch
809
Socket-Verbindungen
Um Daten aus einem Socket zu lesen, wird ein InputStream-Objekt geholt. Zur einfacheren Verarbeitung, z.B. von Textdaten, kann dieses Objekt an einen BufferedReader weitergegeben werden. InputStream getInputStream()
Über die Methode getLocalPort() wird die Portnummer des eigenen Socket zurückgegeben. int getLocalPort()
Zum Schreiben über einen Socket wird ein OutputStream-Objekt genutzt, das mit der Methode getOutputStream() ermittelt wird. OutputStream getOutputStream()
Die Portnummer des Socket auf der Gegenseite erhalten Sie über die folgende Methode. int getPort()
Die Methoden isClosed() und isConnected() liefern Statusinformationen zu einem Socket. Sie prüfen, ob der Socket geschlossen oder verbunden ist. boolean isClosed() boolean isConnected()
Setzen Sie über die folgende Methode einen Timeout für Leseoperationen. Die Angabe erfolgt in Millisekunden. Ein Wert von 0 deaktiviert den Timeout und Lesezugriffe auf einem Socket warten unendlich lange. Werden bei Verwendung eines Timeout innerhalb der angegebenen Zeit keine Daten gelesen, löst dies eine SocketTimeoutException aus. void setSoTimeout(int millis)
Zugriff auf einen Web-Server Für Socket-Anwendungen werden natürlich immer ein Client und ein Server benötigt. Webserver stehen in den meisten Fällen zahlreich über das Internet zur Verfügung. Im folgenden wird ein Webclient erzeugt, der den Inhalt einzelner HTML-Dateien eines Webservers lokal auf der Standardausgabe anzeigt. Zur Aufnahme der Verbindung benötigen Sie den Hostnamen des Webservers. Als Port kommt in der Regel Port 80 zum Einsatz. Eine Webseite wird über den GET-Befehl des HTTP-Protokolls GET /index.html HTTP/1.0
810
31 – Netzwerkanwendungen
angefordert und in Textform über den OutputStream des Socket an den Webserver gesandt. Als Antwort erhalten Sie die durch den zweiten Parameter im GET-Befehl identifizierte Datei. Der GET-Befehl wird über das HTTP-Protokoll definiert und im HTTP-Header versendet. Der GET-Befehl sowie noch einmal der gesamte HTTP-Header (der hier nur aus diesem einen Befehl besteht) müssen mit einem Zeilenumbruch abgeschlossen werden. Dazu wird die Escapesequenz \r\n (Wagenrücklauf, Zeilenvorschub) verwendet.
[IconInfo]
Der folgende HTTPClient verbindet sich mit dem Webserver des Hosts java.sun.com und kommuniziert mit ihm über das HTTP-Protokoll. Dazu fordert der Client über den GET-Befehl die HTML-Seite an, die standardmäßig beim Laden des Wurzelverzeichnisses verwendet wird. Über die Standardausgabe werden über Statusinformationen die lokale und die remote Portnummer vor und nach dem Verbindungsaufbau, eine Information, nachdem die Verbindung hergestellt wurde, sowie der Inhalt der angeforderten HTML-Datei ausgegeben. Nach der erfolgreichen Verbindung und dem Download der HTML-Datei werden zuerst die Streams und danach der Socket geschlossen.
Listing 31.4: \Beispiele\de\j2sebuch\kap31\HTTPClient.java import java.net.*; import java.io.*; public class HTTPClient { public HTTPClient() { Socket httpClient = null; InetAddress httpHost = null; PrintWriter sOut = null; BufferedReader sIn = null; String httpStr = ""; try { httpHost = InetAddress.getByName("java.sun.com"); httpClient = new Socket(httpHost, 80); System.out.println("Socket-Port: " + httpClient.getLocalPort()); System.out.println("Server-Socket-Port: " + httpClient.getPort()); httpClient.setSoTimeout(5000); sOut = new PrintWriter(httpClient.getOutputStream(), true); sIn = new BufferedReader( new InputStreamReader(httpClient.getInputStream())); sOut.println("GET / HTTP/1.0" + "\r\n\r\n"); if(httpClient.isConnected()) System.out.println("Verbindung hergestellt"); while((httpStr = sIn.readLine()) != null) System.out.println(httpStr);
Java 5 Programmierhandbuch
811
Socket-Verbindungen Listing 31.4: \Beispiele\de\j2sebuch\kap31\HTTPClient.java (Forts.) System.out.println("Socket-Port: " + httpClient.getLocalPort()); System.out.println("Server-Socket-Port: " + httpClient.getPort()); sIn.close(); sOut.close(); httpClient.close(); } catch(UnknownHostException uhEx) { System.out.println("Hostname unbekannt."); System.exit(1); } catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen."); System.exit(1); } } public static void main(String args[]) { new HTTPClient(); } }
31.4.2
ServerSockets
Serverseitig werden Sockets durch die Klasse ServerSocket implementiert. Es unterscheiden sich die Konstruktoren von einem ClientSocket, da keine Verbindung zu einem anderen Host aufgebaut werden muss. Stattdessen verbindet sich der Server mit einem Port auf dem lokalen Rechner und wartet auf eingehende Anfragen. Als Portnummer sollte mindestens eine Zahl größer als 10000 verwendet werden, da es außer den Standardports von 0 bis 1023 noch zahlreiche weitere etablierte Portnummern für Datenbankserver etc. gibt. Kann die Verbindung zum Socket nicht hergestellt werden, z.B. wenn er bereits verwendet wird, löst dies eine IOException aus. Geben Sie keine Portnummer an, wird diese dynamisch zugewiesen. ServerSocket ss = new ServerSocket(portNr);
Ein anschließender Aufruf der Methode accept() nimmt die eingehenden Anfragen von Clients entgegen. Sie gibt bei einer erfolgreichen Verbindungsaufnahme ein Socket-Objekt zurück. Dieses Socket-Objekt wird mit demselben Port verbunden wie der Ausgangssocket. Die Verwendung eines neuen Socket ist notwendig, da der existierende ServerSocket ja noch genutzt wird, um weitere ankommende Clientanfragen entgegenzunehmen. Über den neuen Port kommuniziert der Server dann mit dem Client.
812
31 – Netzwerkanwendungen Socket sClient1 = ss.accept();
Zum Lesen und Schreiben von Daten werden wie im Falle des Clients die entsprechenden Streams geöffnet. PrintWriter sOut = sClient1.getOutputStream(), true); BufferedReader sIn = new BufferedReader( new InputStreamReader(sClient1.getInputStream()));
Die Kommunikation muss über ein vordefiniertes (HTTP, POP, SMTP) oder ein eigenes Protokoll erfolgen. Sie müssen die Anfrage eines Clients bearbeiten und die entsprechende Antwort liefern. Die Methode verarbeite() übernimmt im folgenden Codeauszug die Verarbeitung der Daten vom Client und generiert einen entsprechenden Antwortstring. Dieser wird an den Client zurückgesendet, der eine ähnliche Verarbeitungsschleife implementieren kann. String eingang; String ausgang; while((eingang = sIn.readLine()) != null) { ausgang = verarbeite(eingang); sOut.println(ausgang); }
[IconInfo]
Das Spiel Zahlenraten soll über eine Client-Server-Anwendung implementiert werden. Der Server erzeugt einen Socket am Port 10001 und wartet auf eine Client-Anfrage. Die Abwicklung des Protokolls wird in der Methode doZahlenRaten() implementiert. Beim Lesen des Kommandos startgame wird eine neue Zufallszahl im Bereich von 1 bis 20 über die Methode erzeugeZufallszahl() generiert. Wird das Kommando endegame gelesen, wird der Server beendet. Bei allen anderen Leseoperationen wird angenommen, dass eine Zahl übermittelt wurde. Die Zeichenkette wird in eine Zahl konvertiert und danach mit der generierten Zufallszahl verglichen. Je nach Ergebnis wird eine entsprechende Rückgabe erzeugt und über die Methode write() in den Ausgabestrom geschrieben. Zum Beenden einer Zeile wird vereinfacht die Escapesequenz \n verwendet. Alternativ kann systemunabhängig die Methode newLine() eingesetzt werden. Für die sofortige Übertragung der Daten wird der Ausgabepuffer über flush() geleert.
Java 5 Programmierhandbuch
813
Socket-Verbindungen Listing 31.5: \Beispiele\de\j2sebuch\kap31\ZahlenRatenServer.java import java.net.*; import java.io.*; import java.util.*; public class ZahlenRatenServer { private int zufallsZahl = 0; public ZahlenRatenServer() { try { Socket cSock = null; BufferedReader br = null; BufferedWriter bw = null; String eingang = ""; ServerSocket sSock = new ServerSocket(10001); cSock = sSock.accept(); System.out.println("Client angemeldet"); br = new BufferedReader( new InputStreamReader(cSock.getInputStream())); bw = new BufferedWriter( new OutputStreamWriter(cSock.getOutputStream())); doZahlenraten(cSock, br, bw); sSock.close(); } catch(Exception _uh) {} } public void erzeugeZufallszahl() { Random rd = new Random(); zufallsZahl = rd.nextInt(20) + 1; } public void doZahlenraten(Socket cSock, BufferedReader br, BufferedWriter bw) { String sendeString = ""; String eingangString = ""; int zahl = 0; try { while((eingangString = br.readLine()) != null) { System.out.println(eingangString); if(eingangString.toLowerCase().equals("startgame")) { System.out.println("Neue Partie");
814
31 – Netzwerkanwendungen Listing 31.5: \Beispiele\de\j2sebuch\kap31\ZahlenRatenServer.java (Forts.) erzeugeZufallszahl(); System.out.println("Die Zahl ist " + zufallsZahl); } else { if(eingangString.toLowerCase().equals("endegame")) { bw.close(); br.close(); cSock.close(); break; } else { try { zahl = Integer.parseInt(eingangString); if(zahl == zufallsZahl) sendeString = "Richtig"; if(zahl < zufallsZahl) sendeString = "Kleiner"; if(zahl > zufallsZahl) sendeString = "Groesser"; } catch(NumberFormatException nfEx) { sendeString = "KeineZahl"; } bw.write(sendeString + "\n"); bw.flush(); } } } } catch(Exception _uh) {} } public static void main(String[] args) { new ZahlenRatenServer(); } }
Werden beide Anwendungen auf dem lokalen Rechner ausgeführt, verbindet sich der Client mit dem lokalen Rechner Localhost und der Portnummer des Servers. Danach werden Streams zum Lesen und Schreiben von Daten geöffnet. In der Methode doZahlen-
Java 5 Programmierhandbuch
815
Socket-Verbindungen Raten() werden die Kommandos an den Server gesandt und die Rückgabewerte angezeigt. Es wird immer ein Befehl gesandt und der Puffer geleert. Das Beispiel arbeitet momentan mit festen Werten, wertet nicht die Rückgabewerte aus und läuft auch nicht so lange, bis die Zufallszahl erraten wurde. Diese Fleißarbeit überlassen wir Ihnen, da sie nichts an der prinzipiellen Funktionsweise der Anwendung ändert. Listing 31.6: \Beispiele\de\j2sebuch\kap31\ZahlenRatenClient.java import java.net.*; import java.io.*; public class ZahlenRatenClient { public ZahlenRatenClient() { BufferedReader br = null; BufferedWriter bw = null; try { Socket cSock = new Socket("localhost", 10001); br = new BufferedReader( new InputStreamReader(cSock.getInputStream())); bw = new BufferedWriter( new OutputStreamWriter(cSock.getOutputStream())); doZahlenRaten(br, bw); cSock.close(); } catch(Exception _uh) {} } public void doZahlenRaten(BufferedReader br, BufferedWriter bw) { String eingangString = ""; try { bw.write("StartGame\n"); bw.flush(); bw.write("12\n"); // ab hier raten bw.flush(); eingangString = br.readLine(); System.out.println(eingangString); // bis hier bw.write("EndeGame\n"); bw.flush(); } catch(Exception _uh) {} }
816
31 – Netzwerkanwendungen Listing 31.6: \Beispiele\de\j2sebuch\kap31\ZahlenRatenClient.java (Forts.) public static void main(String[] args) { new ZahlenRatenClient(); } }
31.4.3
Verwaltung mehrerer paralleler Verbindungen
Etwas aufwändiger ist die Verwaltung paralleler Verbindungen mehrerer Clients mit dem Server. Am Port des Servers werden die Anfragen in jedem Fall in eine Warteschlange eingereiht. Der Server kann diese Anfragen sequentiell entgegennehmen und bearbeiten oder er verarbeitet die Anfragen parallel über mehrere Threads. Nachdem die Methode accept() eine Anfrage entgegengenommen hat, wird hierfür ein neuer Thread erzeugt, der die Lese- und Schreibvorgänge mit dem Client durchführt. Die Generierung der Threads kann vereinfacht folgendermaßen erfolgen: ServerSocket sSock = new ServerSocket(Portnummer); while(true) { Socket cSock = sSock.accept(); ServerThread st = new ServerThread(cSock); }
[IconInfo]
Über einen Mathe-Server soll eine einfache Anwendung eines Servers gezeigt werden, der mehrere Clients parallel über Threads verwalten kann. Es werden über drei separate Konsolefenster zuerst der Server und kurz danach zwei Clients gestartet. Die Clients lassen auf dem Server zwei Zahlen addieren und erhalten das Ergebnis zurück. Die Anwendungsklasse MatheServer wartet in einer Endlosschleife auf eingehende Anforderungen. Bei jeder Client-Verbindung wird ein interner Zähler hochgezählt, über den die Clients später identifiziert werden können. Mit der Methode setSoTimeout() wird zusätzlich ein Timeout für die Methode accept() gesetzt, so dass der Server nach 10 s ohne Client-Anforderungen beendet wird (es wird eine SocketTimeoutException ausgelöst). Sie müssen deshalb die Verbindungen der Clients sehr zügig herstellen oder Sie entfernen diese Anweisung. Nach einer Textausgabe auf der Konsole wird ein neuer Thread erzeugt, dem das neue Socket-Objekt und die Client-Nummer übergeben wird. Über die Methode start() wird der Thread gestartet.
Java 5 Programmierhandbuch
817
Socket-Verbindungen Listing 31.7: \Beispiele\de\j2sebuch\kap31\MatheServer.java import java.net.*; import java.io.*; public class MatheServer { private int clientNo = 0; public MatheServer() { try { Socket cSock = null; ServerSocket sSock = new ServerSocket(10002); System.out.println("Mathe-Server läuft ..."); sSock.setSoTimeout(10000); while(true) { cSock = sSock.accept(); clientNo++; System.out.println("Neuer Client angemeldet: " + clientNo); MatheServerThread mst = new MatheServerThread(cSock, clientNo); mst.start(); } } catch(Exception _uh) {} System.out.println("Mathe Server is shutting down..."); } public static void main(String[] args) { new MatheServer(); } }
Im Konstruktor des Threads werden die Ein- und Ausgabestreams geöffnet. Nach dem Starten des Threads wird dessen Methode run() ausgeführt. Diese wartet jetzt auf Kommandos vom Client. Es werden vom Server momentan nur die Kommandos ADD und QUIT unterstützt, wobei Letzteres die Verbindung beendet. Nach dem Kommando ADD werden zwei Zahlen übertragen, die vom Server addiert und als Ergebnisstring zurückgegeben werden. Da immer drei Zeilen als Kommando erwartet werden, müssen auch beim Kommando QUIT drei Zeilenumbrüche gesendet werden.
818
31 – Netzwerkanwendungen Listing 31.8: \Beispiele\de\j2sebuch\kap31\MatheServerThread.java import java.net.*; import java.io.*; public class MatheServerThread extends Thread { private BufferedReader br = null; private BufferedWriter bw = null; private Socket cSock = null; private int clientNo = 0; public MatheServerThread(Socket cSock, int clientNo) { this.cSock = cSock; this.clientNo = clientNo; try { br = new BufferedReader( new InputStreamReader(cSock.getInputStream())); bw = new BufferedWriter( new OutputStreamWriter(cSock.getOutputStream())); } catch(Exception _uh) {} } public void run() { String kommando = ""; String zahl1 = ""; String zahl2 = ""; int z1 = 0; int z2 = 0; try { while(true) { kommando = br.readLine(); zahl1 = br.readLine(); zahl2 = br.readLine(); if(kommando.equals("QUIT")) break; if(kommando.equals("ADD")) { try { z1 = Integer.parseInt(zahl1); z2 = Integer.parseInt(zahl2); bw.write((z1 + z2) + "\n"); bw.flush(); }
Java 5 Programmierhandbuch
819
Socket-Verbindungen Listing 31.8: \Beispiele\de\j2sebuch\kap31\MatheServerThread.java (Forts.) catch(NumberFormatException nfEx) { bw.write("Keine Zahlen...\n"); bw.flush(); } } else { bw.write("Inkorrektes Kommando\n"); bw.flush(); } } cSock.close(); System.out.println("Client " + clientNo + " abgemeldet"); } catch(Exception _uh) {} } }
Der Client verbindet sich mit dem Server auf dem lokalen Rechner und öffnet die Lese- und Schreib-Streams. Danach sendet er das Kommando ADD und die zwei zu addierenden Zahlen. Nach dem Auslesen des Rückgabewerts wartet er 5 Sekunden und sendet das Kommando QUIT an den Server. Listing 31.9: \Beispiele\de\j2sebuch\kap31\MatheClient.java import java.net.*; import java.io.*; public class MatheClient { public MatheClient() { BufferedReader br = null; BufferedWriter bw = null; try { Socket cSock = new Socket("localhost", 10002); br = new BufferedReader( new InputStreamReader(cSock.getInputStream())); bw = new BufferedWriter( new OutputStreamWriter(cSock.getOutputStream())); bw.write("ADD\n"); bw.flush(); bw.write("100\n"); bw.flush();
820
31 – Netzwerkanwendungen Listing 31.9: \Beispiele\de\j2sebuch\kap31\MatheClient.java (Forts.) bw.write("200\n"); bw.flush(); System.out.println(br.readLine()); Thread.sleep(5000); bw.write("QUIT\n\n\n"); bw.flush(); cSock.close(); } catch(Exception _uh) {} } public static void main(String[] args) { new MatheClient(); } }
31.5 Datagramme Während über Sockets verbindungsorientierte TCP/IP-Verbindungen hergestellt werden, dienen Datagramme zum Betrieb von UDP/IP-Verbindungen. Die Datenpakete werden hier unabhängig voneinander versendet. Es besteht keine Garantie, dass sie beim Empfänger ankommen. Da die Datenpakete immer einzeln (d.h. ohne Abhängigkeit zu anderen Paketen) geschickt werden, ist auch keine Reihenfolge zu beachten. Der Umfang der Pakete darf laut Standard maximal 64 Kbyte, abzüglich der Header-Informationen, betragen. Die IP-Adresse des Absenders wird bei Datagrammen im Datenpaket gespeichert. Darüber kann der Server später den Absender bestimmen.
31.5.1
Client-Anwendungen
Clients wie auch Server arbeiten bei der Übertragung von Daten mit dem UDP-Protokoll mit den gleichen Klassen DatagramSocket und DatagramPacket. Zum Erstellen von DatagramSocket-Objekten existieren mehrere Konstruktoren, wobei die folgenden eine beliebig verfügbare bzw. eine lokale Portnummer verwenden. Im Falle eines Clients wird die Portnummer dynamisch vergeben. Erstellen Sie einen Server, benutzen Sie den zweiten Konstruktor. DatagramSocket() DatagramSocket(int port)
Zum Senden und Empfangen der Daten wird ein DatagramPacket-Objekt benötigt. Im Falle eines Servers wird dem Objekt der Speicherbereich im Konstruktor übergeben, in dem die eingehenden Daten gepuffert werden. Zum Senden von Daten sind noch die Zieladresse des Servers sowie der Port zu übergeben.
Java 5 Programmierhandbuch
821
Datagramme DatagramPacket(byte[] buf, int len) DatagramPacket(byte[] buf, int len, InetAddress adr, int port)
Nachdem ein Datenpaket erzeugt wurde, kann es mit der Methode send() des DatagramSocket-Objekts verschickt werden. Wird der DatagramSocket nicht mehr benötigt, sollte er über die Methode close() geschlossen werden.
[IconInfo]
Diese Anwendung sendet über einen Client einzelne Datenpakete an einen Server, die lediglich etwas Text enthalten. Dieser Weg kann beispielsweise von einer Chat-Anwendung gewählt werden, da hier keine dauerhafte Verbindung zwischen den Endpunkten benötigt wird. Der im Folgenden erstellte Client sendet einen einfachen Text an den Server und danach das Kommando QUIT zum Beenden der einseitigen Kommunikation. Als Datenpuffer wird ein 128 Byte großes Array verwendet. Nachdem das Datenpaket mit dem Textinhalt und der Zieladresse (lokaler Host, Port 5001) verschnürt wurde, wird ein neuer UDP-Socket erzeugt und das Paket über die Methode send() verschickt. Da die Arbeitsweise zum Senden der Daten immer die gleiche ist, kann dieser Code später auch in eine eigene Methode verlagert werden.
Listing 31.10: \Beispiele\de\j2sebuch\kap31\DatagramClient.java import java.net.*; import java.io.*; public class DatagramClient { public DatagramClient() { DatagramSocket dSock = null; DatagramPacket dPack = null; try { byte[] daten = new byte[128]; String s = "HalliHallo"; daten = s.getBytes(); dPack = new DatagramPacket(daten, daten.length, InetAddress.getLocalHost(), 5001); dSock = new DatagramSocket(); dSock.send(dPack); s = "QUIT"; daten = s.getBytes(); dPack = new DatagramPacket(daten, daten.length, InetAddress.getLocalHost(), 5001); dSock.send(dPack); dSock.close(); }
822
31 – Netzwerkanwendungen Listing 31.10: \Beispiele\de\j2sebuch\kap31\DatagramClient.java catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen."); System.exit(1); } } public static void main(String args[]) { new DatagramClient(); } }
31.5.2
Server-Anwendungen
Ein Server erzeugt ein neues DatagramSocket-Objekt und bindet dieses an einen Port. Danach generiert er ein DatagramPacket-Objekt, welches die Daten von eingehenden Datagrammen aufnimmt. Über die Methode receive() des DatagramSocket-Objekts wartet er nun auf eingehende Nachrichten. Soll nicht unendlich lang gewartet werden, muss mit einem Thread und einem Timeout gearbeitet werden. Die Daten des eingegangenen Datagramms werden in dem DatagramPacket-Objekt gespeichert und können verarbeitet werden. Ist der Pufferbereich kleiner als das angekommene Paket, wird der restliche Teil des Datagramms abgeschnitten. byte daten[] = new byte[4096]; DatagramSocket dSock = new DatagramSocket(5001); DatagramPacket dPack = new DatagramPacket(daten, daten.length); dSock.receive(dPack);
Will der Server dem Client antworten, kann er die IP-Adresse und die Portnummer des Clients aus dem Datenpaket extrahieren und zum Senden eines eigenen Datagramms nutzen. Die folgenden Methoden der Klasse DatagramPacket liefern den Port und die Internetadresse des Clients. int getPort() InetAddress getAddress()
Natürlich müssen sowohl der Client wie auch der Server die Methode receive()verwenden, um eingehende Datenpakete zu verarbeiten. Im Gegensatz zum Server muss er aber den DatagramSocket nicht an einen festen Port binden, weil der Port dynamisch beim Erstellen des DatagramSocket zugewiesen wird.
Java 5 Programmierhandbuch
823
Datagramme
[IconInfo]
Die folgende Server-Anwendung wartet am Port 5001 auf eingehende UDP-Pakete und gibt deren Inhalt sowie Informationen zum Absender (IPAdresse, Port) aus. Die Methode receive() wartet nach dem Erstellen des Socket auf eingehende Client-Verbindungen und nimmt deren Datenpakete auf. Beginnt das Datenpaket mit dem Text QUIT, wird die whileSchleife verlassen und der Server beendet. Wichtig ist der Aufruf der Methode Arrays.fill(), um den Inhalt des Pufferbereichs nach jedem Eingang eines Datenpakets zu leeren.
Listing 31.11: \Beispiele\de\j2sebuch\kap31\DatagramServer.java import java.net.*; import java.io.*; import java.util.*; public class DatagramServer { public DatagramServer() { byte daten[] = new byte[128]; DatagramSocket dSock = null; DatagramPacket dPack = null; try { dSock = new DatagramSocket(5001); System.out.println("Warte ..."); while(true) { dPack = new DatagramPacket(daten, daten.length); dSock.receive(dPack); String eingang = new String(dPack.getData()); System.out.println("Len: " + eingang.length()); if(eingang.startsWith("QUIT")) { System.out.println("QUIT"); break; } System.out.println(eingang); System.out.println(dPack.getPort()); System.out.println(dPack.getAddress()); Arrays.fill(daten, (byte)0); } dSock.close(); } catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen.");
824
31 – Netzwerkanwendungen Listing 31.11: \Beispiele\de\j2sebuch\kap31\DatagramServer.java (Forts.) System.exit(1); } } public static void main(String args[]) { new DatagramServer(); } }
31.6 Das Java Mail API Die J2SE enthält bis heute keine Klassen, die speziell das Senden und Empfangen von E-Mails unterstützen. Das Java Mail API ist ein optionales Package, das ab dem JDK 1.1.6 verwendet werden kann und Bestandteil der J2EE ist. Das API dient zum Senden und Empfangen von E-Mails, nicht zum Erstellen eines Mail-Servers. Für seine Verwendung benötigen Sie zusätzlich das Java Beans Activation Framework. Es wird zur Auswertung der verschiedenen Datenformate, die mit einer Mail versendet werden können, benötigt. Beide APIs können Sie unter den folgenden URLs beziehen: http://java.sun.com/products/javamail/ http://java.sun.com/products/javabeans/jaf/index.jsp
Extrahieren Sie die ZIP-Dateien und kopieren Sie die Dateien mail.jar und activation.jar in die Verzeichnisse ..\jre\lib\ext im Installationsverzeichnis des JDK und des JRE. Alternativ können Sie die Archive auch in den Klassenpfad einbinden. Zum Senden von E-Mails wird meist das Simple Mail Transfer Protocol (SMTP) verwendet, zum Empfangen das Post Office Protocol 3 (POP3). Weiterhin spielt der MIME-Type (Multipurpose Internet Mail Extensions) noch eine Rolle, da durch ihn der Inhalt einer Mail identifiziert wird (Text- oder HTML-Mail, Anhänge). IMAP (Internet Message Access Protocol) wird von Java Mail ebenfalls unterstützt. IMAP wird aber, obwohl es mehr leistet, selten verwendet, weil es einen höheren Verwaltungsaufwand auf dem Mail-Server bedeutet.
31.6.1
Mails senden
Das Mail API besitzt nicht gerade wenige Klassen, so dass auf den ersten Blick der Eindruck entsteht, dass dessen Verwendung keine leichte Aufgabe ist. Für das Versenden von Standardmails sind aber nur vier bis fünf Klassen notwendig. Diese werden zum Teil auch wieder beim Empfangen von Mails benötigt. Die meisten dieser Klassen befinden sich im Package javax.mail. Im Package javax.mail.internet befinden sich noch einige Hilfsklassen. Das Senden einer Mail erfolgt über die folgenden Schritte: 쐌 Erstellen Sie ein Session-Objekt, das den Mailverkehr übergeordnet regelt. 쐌 Danach benötigen Sie ein Message-Objekt, über das die Nachricht konfiguriert wird (Empfänger, Absender, Inhalt, Betreff usw.).
Java 5 Programmierhandbuch
825
Das Java Mail API
쐌 Address-Objekte werden verwendet, um die E-Mail-Adressen des Absenders und der Empfänger festzulegen. Sie werden später dem Message-Objekt zugewiesen. 쐌 Die Klasse Transport implementiert das Übertragungsprotokoll zum Mailserver und versendet die Mail. 쐌 Eine optionale Authentifizierung erfolgt mithilfe der Klasse Authenticator. Die Klasse Session Über die Klasse Session wird eine Mail-Verbindung verwaltet. Hierüber werden bestimmte Systemeigenschaften für den Mail-Verkehr ausgewertet, der Debug-Modus kann eingeschaltet und Authentifizierungsinformationen können festgelegt werden. Die folgenden statischen Methoden liefern ein Session-Objekt zurück. Die Klasse selbst besitzt keinen Konstruktor, um ein Session-Objekt zu erzeugen. Über die Methode getDefaultInstance() lässt sich die Standardsession ermitteln, die von mehreren Anwendungen genutzt werden kann. Für individuelle Sessions verwenden Sie die Methoden getInstance(). Die Parameter vom Typ java.util.Properties dienen der Übergabe von Eigenschaften an die Session, z.B. den Mailhost. Muss man sich beim Mailserver authentifizieren, kann zusätzlich ein Authenticator-Objekt übergeben werden. Session Session Session Session
getInstance(Properties props) getInstance(Properties props, Authenticator aut) getDefaultInstance(Properties props) getDefaultInstance(Properties props, Authenticator aut)
Wenn Sie den Debug-Modus durch Übergabe von true aktivieren, wird die gesamte Kommunikation mit dem Mailserver auf der Konsole (System.out) ausgegeben. Mit der zweiten Methode können Sie einen anderen Ausgabestream für die Debug-Meldungen festlegen. void setDebug(boolean debug) void setDebugOut(PrintStream out)
Zum Empfangen von Mails benötigen Sie ein Store-Objekt. Als Parameter wird das verwendete Protokoll zum Abholen der Mails angegeben, z.B. pop3. Store getStore(String protokoll)
Zum Versenden von Mails ist ein Transport-Objekt erforderlich, das z.B. das Protokoll smtp verwendet. Transport getTransport(String protocol)
826
31 – Netzwerkanwendungen
Die Klasse Message Diese Klasse dient der Festlegung der Nachricht selbst und beschreibt den Inhalt, den Betreff und den Absender sowie den Empfänger. Die Klasse Message ist eine abstrakte Klasse, so dass die einzige im Mail API davon abgeleitete konkrete Klasse MimeMessage aus dem Package javax.mail.internet verwendet wird, um die Nachricht zu erzeugen. Die Klasse besitzt mehrere Konstruktoren, wobei der folgende am häufigsten eingesetzt wird. Ihm wird ein Session-Objekt übergeben. MimeMessage msg = new MimeMessage(session);
Die Klasse MimeMessage besitzt eine Unmenge an Methoden, um eine Nachricht zu erzeugen. Einige werden im folgenden Beispiel verwendet. Die ersten beiden Anweisungen setzen den Absender und den Empfänger. Über die Konstanten der inneren Klasse RecipientType der Klasse Message können Sie den Empfängertyp festlegen (TO – Hauptempfänger, CC – Kopien, BCC – Blindkopien). Die Objekte vom Typ InternetAddress werden später erläutert. Das Betrefffeld wird über die Methode setSubject() und der Inhalt über die Methode setContent() bestimmt. Die Methode setText() kann als Kurzform von setContent() benutzt werden, wenn es sich beim Inhalt der Mail um den Mimetype text/plain handelt. msg.setFrom(new InternetAddress("
[email protected]")); msg.setRecipient(Message.RecipientType.TO, new InternetAddress("
[email protected]")); msg.setSubject("Meine erste Mail"); msg.setContent("Ich lerne Java", "text/plain" ); msg.setText("Ich lerne Java");
Die Klasse Address Auch diese Klasse ist abstrakt, so dass die davon abgeleitete Klasse InternetAddress aus dem Package javax.mail.internet verwendet wird. Dem Konstruktor wird eine Mail-Adresse und optional ein Name übergeben. Es erfolgt jedoch durch das Mail API niemals eine Prüfung, ob es sich um eine gültige Mail-Adresse handelt. Sie können aber zu einer allgemeinen Prüfung reguläre Ausdrücke verwenden. InternetAddress(String mail) InternetAddress(String mail, String name)
Die Klasse Transport Die Klasse ist dafür verantwortlich, dass eine Mail an den Mailserver übertragen wird. In der Regel wird das Protokoll SMTP verwendet. Das Versenden kann über zwei Arten erfolgen. Benutzen Sie die statische Methode send(), wenn Sie nur eine Mail versenden möchten, da danach die Verbindung zum Mailserver wieder abgebaut wird. Transport.send(msg);
Java 5 Programmierhandbuch
827
Das Java Mail API
Über die folgenden Anweisungen teilen Sie die Herstellung und Trennung der Verbindung und das Versenden der eigenen Nachricht in mehrere Anweisungen auf. Dies ist sinnvoll, wenn Sie mehrere Mails an den Server verschicken möchten. Transport tp = session.getTransport("smtp"); tp.connect("host", "benutzer", "passwort"); tp.sendMessage(msg, msg.getAllRecipients()); tp.sendMessage(msg2, msg2.getAllRecipients()); tp.close();
Die Klasse Authenticator Muss man sich beim Mailserver anmelden, kann diese Anmeldung über eine von der Klasse Authenticator abgeleitete Klasse erfolgen, die ein Dialogfenster anzeigt oder eine *.properties-Datei auswertet. In der abgeleiteten Klasse muss die Methode getPasswordAuthentication() überschrieben werden. Bei der Erstellung der Beispiele war dies die einzige Möglichkeit, sich beim 1&1-Mail-Server anzumelden. Außerdem muss die Systemeigenschaft mail.smtp.auth mit dem Wert true belegt werden. Authenticator auth = new EasyAuthenticator(); Session session = Session.getDefaultInstance(props, auth); ... class EasyAuthenticator extends Authenticator { public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("benutzer", "passwort"); } }
[IconInfo]
828
Sie haben jetzt alle zum Versenden von E-Mails benötigten Klassen kennen gelernt, so dass wir nun eine Beispielanwendung erstellen können. Zuerst werden als Systemeigenschaften der SMTP-Host sowie die Verwendung der Authentifizierung gesetzt. Ein Authenticator-Objekt wird über die Klasse EasyAuthenticator definiert, die bei Bedarf über die Methode getPasswordAuthentication() den Benutzernamen sowie das Passwort liefert (passen Sie diese Angaben für Ihren Server an). Der SMTP-Server von 1&1 verlangt beispielsweise ein Login. Das SessionObjekt wird mithilfe des Authentificator-Objekts erzeugt. Benötigen Sie kein Login, übergeben Sie der Methode getDefaultInstance() als zweiten Parameter den Wert null. Zur Ausgabe der SMTP-Kommandos wird der Debug-Modus aktiviert. Nach dem Zusammensetzen der Nachricht in einem Message-Objekt wird sie über die Methode send() an den Mail-Server verschickt.
31 – Netzwerkanwendungen Listing 31.12: \Beispiele\de\j2sebuch\kap31\MailSenden.java import java.util.*; import javax.mail.*; import javax.mail.internet.*; public class MailSenden { public MailSenden() { try { Properties props = new Properties(); props.put("mail.smtp.host", "smtp.1und1.com"); props.put("mail.smtp.auth", "true"); Authenticator auth = new EasyAuthenticator(); Session session = Session.getDefaultInstance(props, auth); session.setDebug(true); MimeMessage msg = new MimeMessage(session); msg.setFrom(new InternetAddress("
[email protected]")); msg.setRecipient(Message.RecipientType.TO, new InternetAddress("
[email protected]")); msg.setSubject("Test-Mail"); msg.setContent("Hello again", "text/plain" ); Transport.send(msg); } catch(Exception ex) {} } public static void main(String args[]) { new MailSenden(); } } class EasyAuthenticator extends Authenticator { public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("benutzer", "passwort"); } }
Java 5 Programmierhandbuch
829
Das Java Mail API
31.6.2
Mails empfangen
Die Klasse Store Der Zugriff auf ein Mail-Konto zum Lesen der Nachrichten wird über ein Store-Objekt durchgeführt. Die Methode getStore() eines Session-Objekts, der das verwendete Protokoll übergeben werden muss, liefert ein Store-Objekt zurück. Danach verbindet man sich über die Methode connect() unter Übergabe des Mailservers und der Benutzerdaten mit dem Postfach. Store store = session.getStore("pop3"); store.connect("pop.1und1.com", "benutzer", "passwort");
Die Klasse Folder Nachdem die Verbindung zum Postfach hergestellt ist, kann man sich mit einem Ordner des Postfachs verbinden. Im Falle des POP3-Protokolls heißt dieser Ordner immer INBOX. Bei Verwendung von IMAP kann es verschiedene Ordner geben, die mit den Methoden getPersonalNamespaces() und getSharedNamespaces() ermittelt werden können. Ein Folder-Objekt wird in der Regel über die Methode getFolder() eines Store-Objekts erzeugt. Mit der Methode open() öffnen Sie einen Ordner. Ordner können zum Lesen oder zum Lesen und Schreiben geöffnet werden. Entsprechend übergeben Sie die Konstanten Folder.READ_ONLY oder Folder.READ_WRITE beim Aufruf der Methode open(). Die Nachrichten können Sie über die Methode getMessages() abfordern. Sie werden über diesen Aufruf aber noch nicht übertragen. Nachdem Sie die Arbeit mit dem Postfach beendet haben, sollten der Ordner und das Postfach wieder geschlossen werden. Folder folder = store.getFolder("INBOX"); folder.open(Folder.READ_ONLY); Message msgs[] = folder.getMessages(); ... folder.close(false); store.close();
Zum Löschen von Nachrichten muss der Ordner im Lese-/Schreibmodus geöffnet werden. Sie markieren die zu löschenden Nachrichten über ein Flag. Beim Schließen des Ordners übergeben Sie der Methode close() den Wert true. Dadurch wird der Löschvorgang auf dem Server durchgeführt. folder.open(Folder.READ_WRITE); ... msg.setFlag(Flags.Flag.DELETED, true); ... folder.close(true);
830
31 – Netzwerkanwendungen Die folgende Anwendung gibt maximal die Betreffzeilen, den Absender und den Inhalt von drei Mails des Ordners INBOX (der einzige Ordner beim POP3-Protokoll) aus. Ändern Sie gegebenenfalls diese Einschränkungen in der for-Schleife. Als Systemeigenschaft wird nur der Name des POP3-Hosts gesetzt. Die Anmeldung erfolgt hier über die Methode connect(). Da die Mails nur gelesen werden, wird der Ordner im Lesemodus geöffnet. Nach dem Lesen der Mails wird der Ordner und anschließend die Verbindung zum Mailserver geschlossen.
[IconInfo]
Listing 31.13: \Beispiele\de\j2sebuch\kap31\MailLesen.java import java.util.*; import java.io.*; import javax.mail.*; import javax.mail.internet.*; public class MailLesen { public MailLesen() { try { Properties props = new Properties(); props.put("mail.smtp.host", "pop.1und1.com"); Session session = Session.getDefaultInstance(props); Store store = session.getStore("pop3"); store.connect("pop.1und1.com", "benutzer", "passwort"); Folder folder = store.getFolder("INBOX"); folder.open(Folder.READ_ONLY); Message msgs[] = folder.getMessages(); for(int idx = 0; idx < Math.max(3, msgs.length); idx++) { Message msg = msgs[idx]; System.out.println("Nachricht Nr: " + idx); System.out.println("From: " + msg.getFrom()[0]); System.out.println("Subject: " + msg.getSubject()); msg.writeTo(System.out); } folder.close(false); store.close(); } catch(Exception ex) { System.out.println("Fehler beim Lesen Ihrer Mails."); } }
Java 5 Programmierhandbuch
831
Das Java Mail API Listing 31.13: \Beispiele\de\j2sebuch\kap31\MailLesen.java (Forts.) public static void main(String args[]) { new MailLesen(); } }
31.6.3
Anhänge verschicken und empfangen
Mails besitzen sehr oft einen Anhang. Diese Anhänge müssen versendet, empfangen und gegebenenfalls auf der Festplatte gespeichert werden. Eine Mail besteht jetzt aus mehreren Teilen, die es zusammenzusetzen gilt. Dazu werden einzelne Teile (Anhänge oder der Textinhalt der Mail) über Part-Objekte erzeugt und in ein Multipart-Objekt eingefügt. Das Interface Part sowie die abstrakte Klasse Multipart werden durch die konkreten Klassen BodyPart und MimeMultipart implementiert. Beispiel Fügen Sie die import-Anweisung für das Activation-Framework zu Beginn und den Inhalt zwischen den fett dargestellten Anweisungen in die Datei MailSenden.java ein. Alternativ verwenden Sie die Beispieldatei MailAnhangSenden.java. Zuerst wird der Textinhalt der Mail erzeugt. Danach wird ein Multipart-Objekt generiert und der erste Teil der Mail eingefügt. Des Weiteren benötigen Sie ein BodyPart-Objekt, dem über die Methoden setDataHandler() und setFileName() der Inhalt einer Datei sowie ein Dateiname zugewiesen werden. Dieser Teil wird ebenfalls dem Multipart-Objekt hinzugefügt. Auf diese Weise können Sie auch mehrere Anhänge erzeugen. Zum Abschluss wird der Methode setContext() kein Text, sondern das Multipart-Objekt zur Festlegung des Inhalts der Mail übergeben. Listing 31.14: \Beispiele\de\j2sebuch\kap31\MailAnhangSenden.java import javax.activation.*; ... msg.setSubject("TestMail"); BodyPart part = new MimeBodyPart(); part.setText("Textinhalt der Mail"); Multipart mpart = new MimeMultipart(); mpart.addBodyPart(part); part = new MimeBodyPart(); DataSource ds = new FileDataSource("DateinameMitPfad"); part.setDataHandler(new DataHandler(ds)); part.setFileName("Dateiname"); mpart.addBodyPart(part);
832
31 – Netzwerkanwendungen Listing 31.14: \Beispiele\de\j2sebuch\kap31\MailAnhangSenden.java (Forts.) msg.setContent(mpart); Transport.send(msg);
Mails mit einem Anhang sind vom MIME-Typ multipart/mixed und müssen separat verarbeitet werden. Dazu sind die Teile der Mail einzeln anzusprechen. Anhänge werden durch ihren Typ Part.ATTACHMENT oder Part.INLINE identifiziert. Im Beispiel wird über alle Nachrichten iteriert und anschließend der MIME-Typ überprüft. Eine weitere Prüfung wertet das Betrefffeld aus (z.B. als Spamfilter). Der Inhalt der Mail wird über die Methode getContent() als Multipart-Objekt zurückgegeben. Über diese Teile wird erneut iteriert. Die Methode getDisposition() liefert eine Zeichenkette, die den Typ des jeweiligen Teils beschreibt. Nach dem Test auf null und dem Test auf einen Anhang wird der Dateiname ausgegeben. Der Zugriff auf den Inhalt kann über die beiden im Kommentar angegebenen Methoden erfolgen. Listing 31.15: \Beispiele\de\j2sebuch\kap31\MailAnhangLesen.java for(int idx = 0; idx < msgs.length; idx++) { if(msgs[idx].isMimeType("multipart/mixed")) { if(msgs[idx].getSubject().equals("Betreff-Feld")) { Message msg = msgs[idx]; System.out.println("Nachricht: " + idx); System.out.println("From: " + msg.getFrom()[0]); System.out.println("Subject: " + msg.getSubject()); Multipart mpart = (Multipart)msg.getContent(); for(int idxP = 0; idxP < mpart.getCount(); idxP++) { Part part = mpart.getBodyPart(idxP); String type = part.getDisposition(); if(type != null) { if(type.equals(Part.ATTACHMENT) || type.equals(Part.INLINE)) { System.out.println(part.getFileName()); // Zugriff auf Inhalt über part.getInputStream() // oder part.writeTo(OutputStream os) } } } } } }
Java 5 Programmierhandbuch
833
Das Java Mail API
[IconInfo]
834
Viele Klassen des Mail API besitzen die Möglichkeit, Listener zu registrieren, die beim Auftreten von Ereignissen aufgerufen werden. So verfügt die Klasse Transport beispielsweise über die Methode addTransportListener() der ein TransportListener-Interface-Objekt zu übergeben ist. Die Methoden dieses Interfaces werden beispielsweise aufgerufen, wenn eine Nachricht (nicht) erfolgreich versendet werden konnte.
32 XML 32.1 Einführung Die Verarbeitung von XML-Daten ist inzwischen nichts Besonderes mehr. Viele Anwendungen speichern Einstellungen oder Inhalte im XML-Format. Dabei kann das Resultat eine Datei sein oder die Daten werden in Form von Streams weitergegeben, z.B. über das Internet. Deshalb wird im Folgenden immer von XML-Dokumenten und nicht von XMLDateien gesprochen. In diesem Kapitel wird das Parsen von XML-Dokumenten über SAX und DOM beschrieben. Mit DOM haben Sie auch die Möglichkeit, XML-Dokumente zu bearbeiten. Über XSLT können Sie XML-Dokumente transformieren, beispielsweise in eine Textdatei. Ressourcen im Web Dieses Kapitel kann nicht alle Möglichkeiten, die sich im Zusammenhang mit XML bieten, erläutern. Sie sollen aber mit den Basisoperationen vertraut gemacht werden. Weitere Quellen finden Sie im Internet z.B. unter: 쐌 http://java.sun.com/j2ee/1.4/docs/tutorial/doc/index.html 쐌 http://www.w3.org/TR/xml11/ 쐌 http://xml.apache.org Für den Zugriff auf XML-Dokumente gibt es verschiedenste Implementierungen. Das JAXP (Java API for XML Processing) stellt eine standardisierte Schnittstelle dar, über die XML-Dokumente verarbeitet und XSL-Transformationen durchgeführt werden können. Für Letztere wird manchmal auch der Begriff TrAX (Transformation API for XML) für den dazu zuständigen Teil von JAXP verwendet. JAXP-Versionen JAXP liegt in der J2SE 5.0 in der Version 1.3 vor. In der J2EE 1.4 war es die Version 1.2 und in der J2EE 1.3 und J2SE 1.4 das JAXP 1.1. Die Bestandeile der einzelnen Versionen können Sie in der Dokumentation des JDK 5.0 unter [InstallJDK]\docs\guide\xml\jaxp\ ReleaseNotes_150.html nachlesen. Die wichtigsten Neuerungen sind, dass nun als XML-Parser Xerces in der Version 2.6.2 und nicht mehr Crimson verwendet wird. Der XSLT-Prozessor Xalan wurde auf die Version 2.6.0 aktualisiert. Package-Namen Bei Verwendung von JAXP spielen die konkreten Package- und Klassennamen der Parser und XSLT-Prozessoren keine Rolle, da der Zugriff direkt über das JAXP-API erfolgt. Nur bei der direkten Verwendung eines Parsers ist die Kenntnis der Package-Namen von Bedeutung. Aktuellere XML-Parser wurden bisher über den Endorsed Standards Override Mechanism in die J2SE 1.4 integriert. Sie überschrieben damit die bereits vorhandenen Klas-
Java 5 Programmierhandbuch
835
XML-Grundlagen
sen und Packages. Zur einfacheren Einbindung künftiger aktuellerer Versionen von Xerces und Xalan über den CLASSPATH wurden die internen Package-Namen umbenannt. API
bisheriges Package
neues Package
JAXP
org.apache.crimson org.apache.xml
com.sun.org.apache.xerces.internal com.sun.org.apache.xml.internal
org.apache.xalan org.apache.xpath org.apache.xalan.xsltc
com.sun.org.apache.xalan.internal com.sun.org.apache.xpath.internal com.sun.org.apache.xalan.internal.xsltc
TrAX
[IconInfo]
Die Verwendung eines XML-Parsers und XSL-Prozessors kann über die standardisierte Schnittstelle JAXP oder den direkten Zugriff auf die entsprechenden Klassen erfolgen. In diesem Kapitel wird immer JAXP eingesetzt, so dass Sie gegebenenfalls auch einen anderen XML-Parser oder XSLT-Prozessor nutzen können, solange dieser JAXP unterstützt.
Zu den Klassen von Xerces und Xalan wird keine API-Dokumentation mitgeliefert. Lediglich die Quellen finden Sie in der Datei src.zip im Installationsverzeichnis des JDK. Benötigen Sie diese Hilfen, laden Sie Xerces und Xalan separat von http://xml.apache.org/. [IconInfo]
32.2 XML-Grundlagen Es werden nun kurz die Grundlagen von XML angerissen. Für eine vollständige Einführung sei auf die zahlreich vorhandene Literatur oder Tutorials im Internet verwiesen. XML (eXtensible Markup Language) dient der Strukturierung von Daten, die durch so genannte Markups (Tags) eingeschlossen werden. Durch die Verschachtelung der Tags wird eine Hierarchie erzeugt. Da XML textbasiert ist, kann es mit jedem Editor bearbeitet werden. Die Weitergabe und die Verarbeitung sind dadurch ebenfalls sehr einfach. Die von XML verwendeten Namen der Tags können vollständig von Ihnen festgelegt werden. Dadurch kann die Beschreibung der Daten prinzipiell beliebig erweitert werden. Sie müssen sich lediglich an ein paar einfache Regeln halten. Ein XML-Dokument enthält keine direkten Informationen, wie die enthaltenen Daten formatiert werden sollen, d.h., es gibt keine Vorschrift, wie ein XML-Dokument angezeigt wird. Wenn Sie mit XML Daten in Datensätzen strukturieren (wie in einer Datenbank), besitzt das Dokument einen durchgehenden Aufbau (Datenstruktur). Sollen dagegen Texte verwaltet werden, ist die Struktur eher unregelmäßig (Dokumentstruktur). Außerdem muss
836
32 – XML
festgelegt sein, welche Tags eine bestimmte Textformatierung bewirken sollen (die konkrete Formatierung wird über diese Tags aber nicht festgelegt). Datenstruktur
Dokumentstruktur
Meier Franz Mueller Kurt
Wer reitet sospaet durch Wind und Nacht, es ist der Vater es ist gleich acht.
Tags werden immer in Paaren verwaltet. Zu einem öffnenden Tag wie muss es immer ein schließendes Tag geben. Der Inhalt eines Tag befindet sich zwischen dem öffnenden und dem schließenden Tag. Ein Tag kann zusätzlich Attribute besitzen. Den Attributen wird durch ein Gleichheitszeichen ein Wert in Anführungszeichen zugewiesen, wie z.B. . Eine Ausnahme ist das leere Tag, das sofort wieder geschlossen wird und keinen Inhalt besitzt, z.B. . Kommentare werden in XML durch die Zeichenfolgen eingeschlossen. Die Zeile zu Beginn der XML-Datei ist der Prolog, der durch eine so genannte Processing Instruction festgelegt wird. Er gibt in diesem Fall nur die verwendete Versionsnummer an. Wichtig ist später beim Parsen eines XML-Dokuments, dass es wohlgeformt ist. Ansonsten versagt uns der Parser seinen Dienst. Wohlgeformte XML-Dokumente haben die folgenden Eigenschaften: 쐌 Es gibt genau ein Wurzelelement, das alle anderen Elemente einschließt. Im gezeigten Beispiel für eine Datenstruktur ist dies z.B. das Element . 쐌 Jedes öffnende Element besitzt genau ein schließendes Element. 쐌 Die Elemente sind korrekt paarweise verschachtelt. Folgt einem öffnenden Tag ein weiteres öffnendes Tag , muss zuerst geschlossen werden, z.B. . Eine Überkreuzverschachtelung wie in ist nicht erlaubt.
Java 5 Programmierhandbuch
837
XML-Parser
Gültige XML-Dokumente müssen die folgenden Eigenschaften besitzen: 쐌 Zur Beschreibung des Aufbaus des XML-Dokuments muss eine DTD (oder eine Schema-Datei) vorhanden sein. 쐌 Das XML-Dokument muss sich an die Regeln der DTD bzw. des Schemas halten. DTD – Document Type Definition Eine DTD definiert die Struktur eines bestimmten XML-Dokumenttyps. Über die DTD wird beispielsweise festgelegt, welche Unterelemente und Attribute ein Element besitzen kann. Auf diese Weise lassen sich später durch XML-Parser diese Eigenschaften des XMLDokuments prüfen (Syntaxprüfung). Es kann z.B. sichergestellt werden, dass jeder Kunde genau einen Namen und einen Vornamen besitzen muss. ...ATTRIBUTE.... ]>
32.3 XML-Parser Für den Zugriff und die Verarbeitung von XML-Dokumenten existieren die zwei Techniken SAX und DOM, die sich weitestgehend durchgesetzt haben. Ein XML-Dokument wird von einem Parser eingelesen, der die Struktur des Dokuments analysiert. SAX-Parser (SAX – Simple API for XML) lesen ein XML-Dokument nur einmal und erzeugen beim Auftreffen eines bestimmten Merkmals (Dokumentbeginn, Elementbeginn, Attributwert, Elementende, Dokumentende) ein Ereignis. In diesem Ereignis können die entsprechenden Daten verarbeitet werden. Nach dem Lesen des Dokuments beendet der Parser seine Arbeit und der Dokumentinhalt steht über den Parser nicht mehr zur Verfügung. SAX ist ein de facto Standard und eignet sich durch seine Verarbeitungsart z.B. für weniger speicherintensive Anwendungen, die den Inhalt eines XML-Dokuments in einem Arbeitsgang verarbeiten. Es können über SAX keine Änderungen am XML-Dokument vorgenommen werden. DOM-Parser (Document Object Model) lesen ein XML-Dokument vollständig ein und verwalten dessen Aufbau in einer Baumstruktur im Speicher. Weiterhin bietet das DOM Methoden, um auf Elemente des Baums lesend und schreibend zuzugreifen. Aufgrund der Tatsache, dass stets der gesamte Baum im Speicher gehalten werden muss, ist dies eine sehr ressourcenintensive Vorgehensweise und für sehr große Dokumente teilweise ungeeignet. Das JDK liefert bereits einen SAX- und einen DOM-Parser mit. Es existieren noch zahlreiche weitere Versionen, die für bestimmte Anwendungszwecke besser geeignet sind.
838
32 – XML
Sie finden diese Parser unter: 쐌 http://www.jdom.org/ 쐌 http://www.dom4j.org/ 쐌 http://jcp.org/en/jsr/detail?id=173 (StAX – Streaming API for XML)
32.3.1
SAX-Parser
Ein SAX-Parser ist eine sehr schnelle und wenig speicherintensive Lösung, ein XMLDokument in einem Vorgang zu analysieren und dessen Daten zu verarbeiten. Während der Parser das XML-Dokument analysiert, erzeugt er beim Auftreffen bestimmter Dokumenteigenschaften ein Ereignis, auf das Sie reagieren können. Die Informationen im Ereignis betreffen immer nur das aktuelle Element. Wenn Sie beispielsweise wissen wollen, ob sich das Element unter befindet, müssen Sie diese Information manuell verwalten. Packages org.xml.sax
Das Interface ContentHandler wird implementiert, um auf die Ereignisse des SAX-Parser zu reagieren. Dazu stellt das Interface für jedes Ereignis eine spezielle Methode bereit.
org.xml.sax.helpers
Damit Sie nicht alle Methoden des Interfaces ContentHandler implementieren müssen, stellt die Klasse DefaultHandler bereits leere Methodenrümpfe zur Verfügung. Sie müssen nur die für Sie relevanten Methoden überschreiben.
javax.xml.parsers
Für das Parsen über SAX und JAXP werden die benötigten Klassen SAXParserFactory und SAXParser aus diesem Package verwendet.
Vorgehensweise Im Folgenden wird der Weg über die Klassen SAXParserFactory und SAXParser genommen. Dabei wird der konkrete XML-Parser dynamisch beim Aufruf der Methode newSAXParser() der Klasse SAXParser bestimmt. Die SAXParser-Fabrik wird ebenfalls nach dem folgenden Muster dynamisch ermittelt. Verwenden Sie einen anderen XML-Parser, können Sie dessen Fabrikklasse über die folgenden Einstellungen angeben. 쐌 Die Klasse wird über die Systemeigenschaft javax.xml.parsers.SAXParserFactory ausgelesen. Normalerweise ist diese Eigenschaft nicht belegt und kann über die folgende Kommandozeile gesetzt werden: java -D javax.xml.parsers.SAXParserFactory=KlassenName. 쐌 Es wird die Datei ..\jre\lib\jaxp.properties ausgewertet, falls sie existiert.
Java 5 Programmierhandbuch
839
XML-Parser
쐌 In allen zur Laufzeit verwendeten JAR-Archiven wird nach einer Datei META-INF/services/javax.xml.parsers.SAXParserFactory
gesucht, die den Klassennamen der Fabrik enthält. 쐌 Zu guter Letzt wird die voreingestellte Klasse com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl verwendet (sieht Datei SAXParserFactory.java in src.zip). Nachdem gezeigt wurde, wie der SAX-Parser ermittelt wird, folgt jetzt eine Übersicht zur Vorgehensweise der Verwendung eines SAX-Parser. 쐌 Erstellen Sie eine neue Klasse, die das Interface ContentHandler implementiert oder die Klasse DefaultHandler erweitert. 쐌 Erzeugen Sie eine SAX-Parser-Fabrik. 쐌 Aktivieren Sie optional die Validierung der XML-Datei über eine DTD oder SchemaDatei. 쐌 Ermitteln Sie über die Fabrik den benötigten SAX-Parser und legen Sie optional weitere Eigenschaften fest. 쐌 Weisen Sie dem Parser den Defaulthandler zu, dessen Methoden beim Eintritt von bestimmten Ereignissen aufgerufen werden. 쐌 Implementieren Sie die entsprechenden Methoden des Handlers, um auf die Ereignisse des Parsers zu reagieren. 쐌 Durch den Aufruf der Methode parse() des SAX-Parsers wird das XML-Dokument verarbeitet. Wenn Sie die Beispiele verwenden, müssen sich die benötigten Dateien immer in dem Verzeichnis befinden, von dem aus die Anwendung gestartet wird. Neue Dateien werden ebenfalls in diesem Verzeichnis erzeugt. [IconInfo]
Die Klasse SAXParser1 stellt das Rahmengerüst zum Parsen eines XML-Dokuments über SAX dar. Es eignet sich auch zur Validierung eines XML-Dokuments. Um später auf die Ereignisse des SAX-Parser zu reagieren, wird das Interface DefaultHandler implementiert. [IconInfo]
Der Methode parse() werden die XML-Datei sowie der Handler für die Ereignisverarbeitung, in diesem Fall das eigene Objekt, übergeben.
Listing 32.1: \Beispiele\de\j2sebuch\kap32\SAXParser1.java (Auszug) import java.io.*; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.*; public class SAXParser1 extends DefaultHandler { public SAXParser1() {
840
32 – XML Listing 32.1: \Beispiele\de\j2sebuch\kap32\SAXParser1.java (Auszug) (Forts.) try { DefaultHandler df = this; SAXParserFactory saxFac = SAXParserFactory.newInstance(); SAXParser saxP = saxFac.newSAXParser(); saxP.parse(new File("Kunden.xml"), df); } catch(Exception _uh) {} } public static void main(String[] args) { new SAXParser1(); } }
ContentHandler implementieren Das Interface ContentHandler enthält zahlreiche Methoden, die durch die Klasse DefaultHandler bereits leer implementiert sind. Im Folgenden werden nur die wichtigsten Methoden erläutert. Sofort nach dem Beginn des Parsens eines XML-Dokuments wird die Methode startDocument() aufgerufen. Das Ende des Parse-Vorgangs wird über den Aufruf der Methode endDocument() mitgeteilt. Beide Methoden dienen also nur informellen Zwecken, ohne weitere Informationen zum Aufbau des XML-Dokuments zu liefern. void startDocument() void endDocument()
Trifft der Parser auf ein öffnendes bzw. schließendes Tag, werden die beiden folgenden Methoden aufgerufen. Da ein öffnender Tag Attribute besitzen kann, wird der Methode startElement() ein weiterer Parameter zu deren Auswertung übergeben. Der erste Parameter gibt den verwendeten Namensraum an (wird im Beispiel nicht verwendet). Im zweiten Parameter wird der einfache Name des Tags und im dritten der vollqualifizierte Name übergeben. Der erste und dritte Parameter enthalten nur dann Werte, wenn die Verwendung von Namensräumen aktiviert ist und das XML-Dokument Namensräume benutzt. void startElement(String url, String lokName, String qualName, Attributes attribute) void endElement(String url, String lokalerName, String qualName)
Sonstige Zeichen, die der Parser innerhalb und außerhalb der Tags liest, werden der Methode characters() übergeben, wobei die Zeichen nicht zusammenhängend angeordnet sein müssen. Die ermittelten Zeichen werden im Array zeichen bereitgestellt. Über die
Java 5 Programmierhandbuch
841
XML-Parser
Parameter start und laenge werden noch der Startindex und die Anzahl der Zeichen des Arrays spezifiziert. void characters(char[] zeichen, int start, int laenge)
Die folgende Methode wird einmalig aufgerufen und gibt unter anderem die Lage des XML-Dokuments, z.B. den Dateinamen, an. void setDocumentLocator(Locator locator)
[IconInfo]
Es werden nun die Methoden des Interfaces ContentHandler, die durch die Klasse DefaultHandler mit leeren Rümpfen implementiert wurden, mit Leben gefüllt. Trifft der Parser beispielsweise auf ein Element, werden auch dessen Attribute ermittelt und ausgegeben. Der Einzug der Elemente wird über die Variable einzug gesteuert. Das Flag inTag wird genutzt, um mit der Methode characters() nur die Zeichen innerhalb eines Tag auszugeben. Ansonsten würden alle gefunden Zeichen, also auch die Zeilenumbrüche innerhalb des XML-Dokuments, verarbeitet.
Listing 32.2: \Beispiele\de\j2sebuch\kap32\SAXParser1.java (Auszug) public class SAXParser1 extends DefaultHandler { ... private int einzug = 0; private boolean inTag = false; private void printEinzug(int wert) { for(int i = 0; i < einzug; i++) System.out.printf("%s", " "); } public void setDocumentLocator(Locator l) { System.out.printf("%s\n", l.getSystemId()); } public void endDocument() { System.out.println("===== Dokumentende ======"); } public void startDocument() { System.out.println("===== Dokumentbeginn ======"); } public void startElement(String url, String lokalerName, String qualName, Attributes attribute) { inTag = true;
842
32 – XML Listing 32.2: \Beispiele\de\j2sebuch\kap32\SAXParser1.java (Auszug) (Forts.) einzug++; printEinzug(1); System.out.printf(""); } public void endElement(String url, String lokalerName, String qualName) { inTag = false; printEinzug(-1); einzug--; System.out.printf("\n", qualName); } public void characters(char[] zeichen, int start, int laenge) { if(inTag) { String s = new String(zeichen, start, laenge); if(!s.equals("")) System.out.printf("%s", s); } } }
Fehlerbehandlung Beim Parsen eines XML-Dokuments können verschiedene Fehler auftreten. Es wird zwischen schweren Fehlern, Fehlern und Warnungen unterschieden. Schwere Fehler beenden den Parse-Vorgang. Ist ein XML-Dokument nicht wohlgeformt, beendet der Parser seine Arbeit, da er nicht mehr für die Korrektheit der Ergebnisse garantieren kann. Ist ein Tag beispielsweise nicht korrekt abgeschlossen, ist das XML-Dokument nicht wohlgeformt. Die Klasse SAXException ist die Basisklasse aller Exceptions, die bei einem SAX-Parser auftreten können. Die davon abgeleitete Klasse SAXParseException (beide aus org.xml.sax) stellt Methoden bereit, um den Fehler im XML-Dokument genauer zu lokalisieren. Die Fehlermeldungen sind vom verwendeten Parser abhängig. int getColumnNumber() // Spaltennummer des Fehlers int getLineNumber() // Zeilennummer des Fehlers int getSystemId() // Dokumentname
Einfache Fehler führen nicht zum Abbruch des Parse-Vorgangs. Wird bei einer mitgelieferten DTD beispielsweise kein gültiges XML-Dokument vorgefunden, wird keine weitere Meldung ausgegeben. Einfache Fehler werden standardmäßig nicht weiter berücksichtigt.
Java 5 Programmierhandbuch
843
XML-Parser
Um diese Fehler zu verarbeiten, müssen Sie die Methoden error() und warning() des Interfaces ErrorHandler implementieren. Die Klasse DefaultHandler implementiert auch dieses Interface bereits mit leeren Methodenrümpfen. Die XML-Datei KundenFehler.xml ist nicht wohlgeformt, da das erste Element kein schließendes Tag besitzt. Es wird in der Exception die Zeilennummer, die Fehlermeldung sowie der Dateiname ausgegeben. [IconInfo]
Listing 32.3: \Beispiele\de\j2sebuch\kap32\SAXParser2.java import java.io.*; import org.xml.sax.*; import org.xml.sax.helpers.*; import javax.xml.parsers.*; public class SAXParser2 extends DefaultHandler { public SAXParser2() { try { DefaultHandler df = this; SAXParserFactory saxFac = SAXParserFactory.newInstance(); SAXParser saxP = saxFac.newSAXParser(); saxP.parse(new File("KundenFehler.xml"), df); } catch(SAXParseException spEx) { System.out.println(spEx.getLineNumber()); System.out.println(spEx.getMessage()); System.out.println(spEx.getSystemId()); } catch(Exception _uh) {} } public static void main(String[] args) { new SAXParser2(); } }
Eigenschaften einstellen Die XML-Parser unterstützen unterschiedliche Eigenschaften (Propertys) und Merkmale (Features). Diese können über die folgenden Methoden der Klasse XMLReader gelesen bzw. gesetzt werden. Der XMLReader ist der konkrete Parser, der ein XML-Dokument verarbeitet.
844
32 – XML
Sie erhalten ihn über die Anweisungen: SAXParser saxP = saxFac.newSAXParser(); XMLReader xmlR = saxP.getXMLReader();
Während Merkmale über boolesche Werte aktiviert bzw. deaktiviert werden, sind Eigenschaften über Object-Typen (z.B. Strings) festzulegen. Ob ein Parser ein Feature tatsächlich unterstützt, erkennen Sie daran, ob beim Aufruf von setFeature() mit dem Namen des Merkmals und dem Wert true eine Exception ausgelöst wird. boolean getFeature(String name) void setFeature(String name, boolean value) Object getProperty(String name) void setProperty(String name, Object value)
Eine Übersicht der von Xerces unterstützten Features und Propertys finden Sie unter 쐌 http://xml.apache.org/xerces2-j/features.html 쐌 http://xml.apache.org/xerces2-j/properties.html
[IconInfo]
Um die Validierung über eine DTD oder Schemadatei zu aktivieren, kann die Methode setValidating() aufgerufen werden oder es wird das entsprechende Feature gesetzt. Die Features werden aber nicht durch den Parser, sondern im konkret eingesetzten XMLReader verwendet. Da die Features standardmäßig deaktiviert sind, wird bei der Ausgabe der Features für die einfache Validierung true, für die Validierung über ein Schema aber false ausgegeben.
Listing 32.4: \Beispiele\de\j2sebuch\kap32\SAXParser3.java import org.xml.sax.*; import javax.xml.parsers.*; public class SAXParser3 extends DefaultHandler { private String[] xmlFeatures = { "http://xml.org/sax/features/validation", "http://apache.org/xml/features/validation/schema"}; public SAXParser3() { try { SAXParserFactory saxFac = SAXParserFactory.newInstance(); saxFac.setValidating(true); SAXParser saxP = saxFac.newSAXParser(); XMLReader xmlR = saxP.getXMLReader(); for(String s: xmlFeatures) System.out.println(s + ": " + xmlR.getFeature(s)); }
Java 5 Programmierhandbuch
845
XML-Parser Listing 32.4: \Beispiele\de\j2sebuch\kap32\SAXParser3.java (Forts.) catch(Exception _uh) {} } public static void main(String[] args) { new SAXParser3(); } }
XML-Dokumente validieren Ein SAX-Parser führt bereits während des Parse-Vorgangs eine einfache Validierung auf ein wohlgeformtes XML-Dokument durch. Damit beispielsweise eine DTD genutzt wird, müssen das XML-Dokument und die DTD vorliegen. Zusätzlich muss 쐌 die Validierung über die Methode setValidating() der SAXParserFactory-Klasse explizit aktiviert werden, 쐌 die Methode error() des Interfaces ErrorHandler implementiert werden, damit Fehler beim Verwenden einer DTD auch bemerkt werden. Unter JAXP 1.3 wird unter dem Package javax.xml.validation über die Klasse Validator die Möglichkeit geboten, ein XML-Dokument direkt zu validieren, ohne dass erst eine Instanz eines Parser benötigt wird. [IconInfo]
[IconInfo]
Das folgende Beispiel validiert eine Datei KundenDTD.xml mit der DTD KundenDTD.dtd. Das XML-Dokument ist gültig, deshalb wird kein Fehler ausgegeben. Ändern Sie den Aufbau der XML-Datei, indem Sie beispielsweise ein Element bei einem Kunden hinzufügen, wird eine Fehlermeldung erzeugt. Eine DTD definiert alle Elemente eines XML-Dokuments und in Klammern deren mögliche Unterelemente sowie deren Häufigkeit.
Listing 32.5: \Beispiele\de\j2sebuch\kap32\KundenDTD.dtd Kunde (Name, Vorname?)> Name (#PCDATA)> Vorname (#PCDATA)> Name ID CDATA #REQUIRED>
Die XML-Datei enthält eine Verknüpfung auf die extern vorliegende DTD und kann nun auf Gültigkeit geprüft werden.
846
32 – XML Listing 32.6: \Beispiele\de\j2sebuch\kap32\KundenDTD.xml Meier Franz
Nach der Aktivierung der Validierung wird die Datei KundenDTD.xml geparsed. Hinzugekommen ist die Implementierung der Methode error(), die bei Verstößen gegen die Gültigkeit des Dokuments aufgerufen wird. Listing 32.7: \Beispiele\de\j2sebuch\kap32\SAXParserDTD.java import java.io.*; import org.xml.sax.*; import org.xml.sax.helpers.*; import javax.xml.parsers.*; public class SAXParserDTD extends DefaultHandler { public SAXParserDTD() { try { DefaultHandler df = this; SAXParserFactory saxFac = SAXParserFactory.newInstance(); saxFac.setValidating(true); SAXParser saxP = saxFac.newSAXParser(); saxP.parse(new File("KundenDTD.xml"), df); } catch(Exception _uh) {} } public void error(SAXParseException spEx) { System.out.println(spEx.getMessage()); } public static void main(String[] args) { new SAXParserDTD(); } }
Java 5 Programmierhandbuch
847
XML-Parser
32.3.2
DOM-Parser
Der DOM-Parser arbeitet etwas langsamer als der SAX-Parser, weil er die Objektstruktur des XML-Dokuments im Speicher aufbauen muss. Außerdem benötigt er mehr Ressourcen, die bei großen XML-Dokumenten durchaus beachtenswert sind (das bis zu Zehnfache der eigentlichen Dokumentgröße). Der Parse-Vorgang erfolgt ähnlich dem SAX-Parser mit dem Unterschied, dass keine Ereignisse ausgelöst werden. Stattdessen wird am Ende über eine Variable eine Referenz auf das im Speicher nachgebildete XML-Dokument zu dessen Verarbeitung genutzt. Im Gegensatz zu SAX können Sie nun beliebig lange das XMLDokument lesen oder auch bearbeiten. Die Spezifikation zu DOM finden Sie unter http://www.w3.org/DOM/. Die aktuelle Version ist Level 3. DOM definiert lediglich eine Menge von Interfaces, über die Dokumente bearbeitet werden können. Packages org.xml.sax
Beim Parsen eines XML-Dokuments werden zur Vereinfachung die Exceptions des SAX-Parsers wie SAXException verwendet.
org.w3c.dom
Für den Zugriff auf ein XML-Dokument befinden sich in diesem Package zahlreiche Interfaces, wie das Interface Document zum Zugriff auf das gesamte Dokument oder Node als Basisinterface für die verschiedenen Knotentypen.
javax.xml.parsers
Für das Parsen über DOM und JAXP werden die benötigten Klassen DocumentBuilderFactory und DocumentBuilder aus diesem Package verwendet. Die Vorgehensweise entspricht damit auch der zum Erstellen eines SAXParsers.
XML-Dokumente parsen Im Folgenden wird der Weg über die Klassen DocumentBuilderFactory und DocumentBuilder genommen. Der konkrete XML-Parser wird dynamisch beim Aufruf der Methode newDocument() der Klasse DocumentBuilder bestimmt. Die DocumentBuilder-Fabrik wird nach dem gleichen Muster wie die von SAX ermittelt. Verwenden Sie einen anderen XML-Parser, können Sie dessen Fabrikklasse über die folgenden Einstellungen angeben. 쐌 Die Klasse wird über die Systemeigenschaft javax.xml.parsers.DocumentBuilderFactory
ausgelesen. Normalerweise ist diese Eigenschaft nicht belegt und kann beispielsweise über die folgende Kommandozeile gesetzt werden. java -D javax.xml.parsers.DocumentBuilderFactory=KlassenName
쐌 Es wird die Datei ..\jre\lib\jaxp.properties ausgewertet, wenn sie existiert.
848
32 – XML
쐌 In allen zur Laufzeit verwendeten JAR-Archiven wird nach der Datei META-INF/services/javax.xml.parsers.DocumentBuilderFactory
gesucht, die den Klassennamen der Fabrik enthält. 쐌 Zu guter Letzt wird die voreingestellte Klasse com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
verwendet (sieht Datei DocumentBuilderFactory.java in src.zip) Der Zugriff auf die Elemente des Baums erfolgt lesend und schreibend über verschiedene Interfaces, die sich alle im Package org.w3c.dom befinden. Ausgangsbasis ist das Document-Interface, das beim Parsen eines Dokuments über die Methode parse() oder über die Methode newDocument(), die ein neues Dokument erzeugt, zurückgegeben wird. Beide Methoden befinden sich in der Klasse DocumentBuilder. Im Folgenden werden die wichtigsten Interfaces vorgestellt. Interface
Erläuterung
Attr
Es wird der Zugriff auf die Attribute eines Elements hergestellt.
Document
Dieses Interface repräsentiert den gesamten DOM-Baum.
Element
Elemente (Tags) werden über das Element-Interface bearbeitet. Beachten Sie, dass es im DOM keinen Knoten für das Ende-Tag gibt.
NamedNodeMap
Es werden mehrere Knoten über diese Sammlung vereinigt, z.B. die Attribute eines Knotens, die ebenfalls als Knoten im DOM vorliegen.
Node
Dieses Interface repräsentiert einen einzelnen Knoten im DOM. Die meisten anderen Interfaces sind direkt oder indirekt davon abgeleitet. Es definiert die Zugriffsmethoden für einen Knoten sowie zahlreiche Konstanten.
NodeList
Eine Knotenliste enthält ein geordnetes Array von Node-Objekten, z.B. die Knoten, die einem bestimmten Knoten untergeordnet sind.
Text
Enthält ein Element einen Text, wird er über dieses Interface bearbeitet.
Elemente eines DOM werden z.B. ausgehend vom Wurzelelement bearbeitet. Alternativ können Sie auch Filter verwenden, um nur einen bestimmten Elementtyp zu bearbeiten. Es gibt im DOM keine Methode, um alle Knoten hintereinander zu durchlaufen. Stattdessen müssen Sie rekursiv die Kindelemente eines Knotens ermitteln und verarbeiten, bis der Knoten keine Kindknoten mehr besitzt. Die Kindelemente werden in einer NodeList zurückgegeben, über deren Eigenschaft item() auf die einzelnen Node-Elemente zugegriffen wird. Zum Auslesen von Informationen über einen Knoten stehen die folgenden Methoden des Interfaces Node bereit.
Java 5 Programmierhandbuch
849
XML-Parser Methode
Erläuterung
getAttributes()
In einer NamedNodeMap werden alle Attribute eines Elementknotens geliefert.
getChildNotes()
Es werden alle untergeordneten Knoten eines Knotens ermittelt.
getFirstChild()
Es wird der erste untergeordnete Knoten geliefert.
getLastChild()
Es wird der letzte untergeordnete Knoten geliefert.
getNextSibling()
Es wird der nächste Knoten geliefert, der dem aktuellen Knoten direkt folgt.
getNodeName()
Es wird der Name eines Knotens geliefert. Im Falle eines Tags ist dies der Tag-Name, im Falle eines Textknotens der Name #text. Eine Liste der möglichen Werte finden Sie in der API-Dokumentation zum Interface Node.
getNodeType()
Es wird der Typ eines Knotens geliefert, z.B. Node.ELEMENT_ NODE für einen Elementknoten oder Node.TEXT_NODE für einen Textknoten. Mögliche Werte stellen die Konstanten des Interfaces Node dar.
getNodeValue()
Es wird der Wert eines Knotens geliefert. Dies ist im Falle eines Textknotens dessen Text, im Falle eines Elementknotens der Wert null.
hasAttributes()
Besitzt ein Knoten (Element) Attribute, wird true geliefert, sonst false.
[IconInfo]
Die Anwendung parsed die Datei Kunden.xml und gibt deren Inhalt auf der Konsole aus. Zum Aufbau des DOM wird über die DocumentBuilderFactory ein DocumentBuilder bereitgestellt und über dessen Methode parse() die Datei Kunden.xml in das DOM überführt. Da ein Document-Objekt vom Typ des Interfaces Node ist, können nun dessen Kindelemente rekursiv über die Methode zeigeKnoten() bestimmt werden. Zuerst werden in der Methode zeigeKnoten() die Kindelemente des übergebenen Knoten über die Methode getChildNodes() bestimmt. Danach wird über die Elemente der NodeList iteriert und deren Typ ausgewertet. Für Elemente wird geprüft, ob diese Attribute besitzen. Ist dies der Fall, werden sie bestimmt und ausgegeben. Im String elementName wird der Name des aktuellen Elements gespeichert, um nach dem Durchlaufen aller Kindelemente das schließende Tag auszugeben. Für jedes Element wird rekursiv die Methode zeigeKnoten() aufgerufen, um dessen Kindelemente zu verarbeiten. Trifft die Methode auf einen Textknoten, wird dessen Inhalt ausgegeben.
850
32 – XML Listing 32.8: \Beispiele\de\j2sebuch\kap32\DOMParser1.java import java.io.*; import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.*; public class DOMParser1 { private Document doc = null; public DOMParser1() { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); doc = db.parse(new File("Kunden.xml")); zeigeKnoten(doc); } catch(SAXParseException spEx) {} catch(Exception ex) {} } private void zeigeKnoten(Node nd) { String elementName = ""; NodeList nl = nd.getChildNodes(); for(int i = 0; i < nl.getLength(); i++) { switch(nl.item(i).getNodeType()) { case Node.ELEMENT_NODE: System.out.printf(""); elementName = nl.item(i).getNodeName(); zeigeKnoten(nl.item(i)); System.out.printf("", elementName); break;
Java 5 Programmierhandbuch
851
XML-Parser Listing 32.8: \Beispiele\de\j2sebuch\kap32\DOMParser1.java (Forts.) case Node.TEXT_NODE: System.out.printf("%s", nl.item(i).getNodeValue()); break; } } } public static void main(String[] args) { new DOMParser1(); } }
XML-Dokumente erstellen und bearbeiten Über die Interfaces des DOM können Sie vollständige Dokumente im Speicher erstellen und bearbeiten. Wenn Sie ein Element erstellen, können Sie dessen Eigenschaften über das zurückgelieferte Node-Objekt bearbeiten. Sonst müssen Sie erst das gewünschte Node-Objekt bestimmen, z.B. über die Methoden getChildNodes() oder getNextSibling(). Mit der Methode createElement() des Interfaces Document wird ein Knoten vom Typ Element erzeugt. Dieser Knoten ist momentan noch nicht im Baum eingeordnet. Dies erfolgt in einem zweiten Schritt, z.B. über die Methode appendChild() des Interfaces Node. Das Interface Node stellt weiterhin die folgenden Methoden bereit, um Elemente im Baum einzufügen oder zu löschen. Methode
Erläuterung
Node appendChild( Node newChild)
Es wird unter dem aktuellen Knoten ein neuer untergeordneter Knoten (am Ende) eingefügt. Dieser Knoten wird als Argument übergeben.
Node removeChild( Node oldChild)
Der als Parameter übergebene Knoten wird entfernt.
void setNodeValue( String nodeValue)
Es wird der Wert des Knotens gesetzt.
XML-Dokumente speichern Das DOM stellt keine Methoden zur Verfügung, um einen Baum in irgendeiner Weise zu speichern. Dies kann manuell erfolgen oder Sie verwenden die Hilfsklasse XMLSerializer von Xerces zu diesem Zweck. Sie befindet sich im Package com.sun.org.apache.xml.internal.serialize. Dem Konstruktor werden ein Writer- und ein OutputFormat-Objekt übergeben. Das kann beispielsweise ein FileWriter sein, wenn die Daten in einer Datei zu speichern sind. Das Ausgabeformat kann über Methoden der Klasse OutputFormat konfiguriert werden, die sich im gleichen Package befindet. Die Klasse XMLSerializer besitzt eine Methode serialize() der das zu serialisierende Dokument übergeben wird. 852
32 – XML
[IconInfo]
Dieses Beispiel erzeugt im Speicher ein XML-Dokument über das DOM und speichert es als Datei NeuKunde.xml ab. Das neue Dokument wird über die Methode newDocument() des DocumentBuilder-Objekts erstellt. Danach werden in der Methode erzeugeDokument() mit der Methode createElement() neue Knoten angelegt. Diese werden über die Methode appendChild() in den Baum eingefügt. Nach der Erstellung des DOM wird über die Methode speichereDokument() das erzeugte Dokument in einer Datei gespeichert. Das Ausgabeformat wird so konfiguriert, dass die Elemente mit einem Einzug dargestellt werden.
Listing 32.9: \Beispiele\de\j2sebuch\kap32\DOMParser2.java import com.sun.org.apache.xml.internal.serialize.*; import java.io.*; import javax.xml.parsers.*; import org.w3c.dom.*; import org.xml.sax.*; public class DOMParser2 { public DOMParser2() { Document doc = null; try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); doc = db.newDocument(); erzeugeDokument(doc); speichereDokument(doc); } catch(Exception ex) {} } private void erzeugeDokument(Document doc) { Element kunden = doc.createElement("Kunden"); doc.appendChild(kunden); Element kunde = doc.createElement("Kunde"); kunden.appendChild(kunde); Text text = doc.createTextNode("Kurt Meier"); kunde.appendChild(text); kunde = doc.createElement("Kunde"); kunden.appendChild(kunde); text = doc.createTextNode("Irma Schulze"); kunde.appendChild(text); }
Java 5 Programmierhandbuch
853
XSLT-Transformationen Listing 32.9: \Beispiele\de\j2sebuch\kap32\DOMParser2.java (Forts.) private void speichereDokument(Document doc) { try { FileWriter fw = new FileWriter("NeuKunde.xml", false); OutputFormat of = new OutputFormat(doc); of.setIndenting(true); XMLSerializer xmlS = new XMLSerializer(fw, of); xmlS.serialize(doc); } catch(Exception ex) { System.out.println(ex.getMessage()); } } public static void main(String[] args) { new DOMParser2(); } }
32.4 XSLT-Transformationen Zur Verarbeitung von XML-Dokumenten gibt es verschiedene Techniken. Die XSL (eXtensible StyleSheet Language) umfasst dazu die Spezifikationen XSLT und XSL FO. XSLT (eXtensible StyleSheet Language for Transformations) definiert die Transformation eines XML-Dokuments in einen anderen Dokumenttyp, z.B. wieder nach XML oder nach HTML. Dabei wird XPath zur Adressierung der Teile eines XML-Dokuments genutzt. XSL FO (XSL Formatting Objects) definiert einen allgemeinen Aufbau eines XML-Dokuments für Druckdokumente. Über einen FO-Prozessor kann aus einem FO-Dokument z.B. ein PDF-Dokument erzeugt werden. In JAXP ist allerdings nur XSLT und XPath enthalten. Beide werden durch Xalan, einen XSLT-Prozessor, bereitgestellt. XSL FO kann z.B. über http://xml.apache.org/ bezogen werden. Wozu soll nun eine XML-Transformation nützlich sein? 쐌 Enthält ein XML-Dokument Informationen zu 10000 Kunden und Sie möchten nur die Kundeninformationen der Kunden extrahieren, die in Leipzig wohnen, können Sie dies durch eine XSL-Transformation durchführen. 쐌 Möchten Sie ein XML-Dokument als HTML-Dokument im Internet zur Verfügung stellen, können Sie ebenfalls XSLT nutzen. 쐌 Nicht zuletzt kann man XSLT auch zum Speichern eines DOM-Baums verwenden.
854
32 – XML
Durch XSLT ist es also möglich, die Informationen eines XML-Dokuments zu filtern (zu verringern) oder um weitere zu bereichern (z.B. bei der HTML-Generierung). Das notwendige Package zur Verwendung von XSLT ist javax.xml.transform. Darin befindet sich eine Klasse TransformerFactory, die ein Transformer-Objekt bereitstellt. Die Vorgehensweise zur Suche einer konkreten Implementierung eines XSLT-Transformers erfolgt über die Systemeigenschaft javax.xml.transform.TransformerFactory, die Datei jaxp.properties oder die Standardvorgabe über die Klasse com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl. Die Transformation eines XML-Dokuments über JAXP beginnt mit der Ermittlung der aktuellen Transformer-Fabrik, die ihrerseits ein Transformer-Objekt erzeugt. Optional kann ein XSL-StyleSheet übergeben werden. Nachdem der Transformer besteht, kann er aus XML-Dokumenten auf Basis des verwendeten StyleSheets ein Ergebnisdokument erzeugen.
TransformerFactory
XML-Dokument
XSL-Dokument
Transformer
Ziel
Abb. 32.1: Transformation über JAXP
Das beim Erstellen des Transformer-Objekts verwendete Dokument sowie die Ein- und Ausgabedokumente können über drei verschiedene Quell- bzw. Zieltypen angegeben werden. Sie können als Eingabe einen Stream oder das Ergebnis eines SAX- oder DOM-Parsers nutzen. Bei der Ausgabe ist es genau so. Alle Klassen implementieren das Interface Source oder Result und können dadurch später an die betreffenden Methoden übergeben werden. Klassen
Package
DOMSource, DOMResult
javax.xml.transform.dom
SAXSource, SAXResult
javax.xml.transform.sax
StreamSource, StreamResult
javax.xml.transform.stream
Beispiel Eine Artikelliste wird als XML-Dokument zur Verfügung gestellt. Das Wurzelelement umschließt alle anderen Artikelelemente . Jeder Artikel besitzt einen Namen und eine Anzahl.
Java 5 Programmierhandbuch
855
XSLT-Transformationen Listing 32.10: \Beispiele\de\j2sebuch\kap32\Artikel.xml Schraube 32.x 100 ...
Die folgende XSL-Datei (eine XML-Datei mit bestimmten Transformationsanweisungen) selektiert mit XPath-Ausdrücken bestimmte Elemente eines XML-Dokuments. Über die Elemente wird nach einem Element (xsl:template) gesucht, das einen bestimmten Pfad besitzt (match="/"). In diesem Fall ist dies der Pfad zum Wurzelelement. Nach der Ermittlung des Wurzelelements wird dieses mit einem HTMLRahmen versehen und mit dem Ausdruck wird dessen Inhalt verarbeitet. Beim Auffinden eines -Elements wird eine horizontale Linie () und der Inhalt des Elements (sein Textinhalt) eingefügt. Listing 32.11: \Beispiele\de\j2sebuch\kap32\Artikel.xsl
Als Ergebnis wird die folgende (in der Formatierung angepasste) HTML-Ausgabe erzeugt. Listing 32.12: Das Ergebnis der Transformation von Artikel.xml mit Artikel.xsl
856
32 – XML Listing 32.12: Das Ergebnis der Transformation von Artikel.xml mit Artikel.xsl (Forts.) Schraube 32mm 100 ...
32.4.1
Kommandozeilenversion
Der im JDK integrierte XSLT-Prozessor Xalan verfügt eigentlich über eine Kommandozeilenversion, die man im JDK aus unerfindlichen Gründen nicht aktiviert hat. Die Methode main() der Klasse Process wurde dazu nach _main() umbenannt. Über einen Umweg kann sie aber doch genutzt werden.
[IconInfo]
Der Zugriff auf die Klasse Process muss über den vollständigen Package-Namen erfolgen, da sich im Package java.lang ebenfalls eine Klasse Process befindet und dieses Package standardmäßig eingebunden wird. Es werden wieder die Argumente direkt an Xalan weitergeleitet. Um aus einer XML- und XSL-Datei eine Ausgabe auf der Konsole bzw. in eine Datei zu erzeugen, werden die beiden folgenden Aufrufe verwendet: java RunXalan -IN Artikel.xml -XSL Artikel.xsl java RunXalan -IN Artikel.xml -XSL Artikel.xsl -OUT Artikelliste.html
Listing 32.13: \Beispiele\de\j2sebuch\kap32\RunXalan.java public class RunXalan { public static void main(String[] args) { com.sun.org.apache.xalan.internal.xslt.Process._main(args); } }
Auf dieselbe Weise kann von Xalan eine Anwendung aufgerufen werden, welche die aktuellen Umgebungseinstellungen der XML-Tools ausgibt. Auch hier wurde die Methode main() lediglich umbenannt. Die Parameter werden wieder einfach weitergereicht. Die folgende Anwendung gibt Informationen zu JAXP, den verwendeten Versionen von SAX und DOM sowie den Versionen von Xerces sowie Xalan aus. [IconInfo]
Java 5 Programmierhandbuch
857
XSLT-Transformationen Listing 32.14: \Beispiele\de\j2sebuch\kap32\TestXMLUmgebung.java import com.sun.org.apache.xalan.internal.xslt.*; public class TestXMLUmgebung { public static void main(String[] args) { EnvironmentCheck._main(args); } }
32.4.2
DOM-Bäume speichern
Ein Transformer-Objekt verwendet normalerweise ein XSL-StyleSheet, um ein XMLDokument in ein anderes Dokument zu überführen. Geben Sie allerdings kein XSL-StyleSheet an, kann es zu einer so genannten identischen Transformation genutzt werden. Das XML-Dokument wird wieder in das gleiche XML-Dokument überführt, mit dem Unterschied, dass als Ziel der Transformation eine Datei (oder ein Stream) angegeben werden kann.
[IconInfo]
Die Anweisungen zum Einlesen des XML-Dokuments Artikel.xml in einen DOM-Baum sind bereits bekannt. Danach wird eine Instanz der Klasse TransformerFactory über die Methode newInstance() geholt. Anschließend wird über die Factory ein Transformer-Objekt mit der Methode newTransformer() generiert, das eine identische Transformation erzeugt. Im Aufruf der Methode transform() wird als erster Parameter das DOM-Objekt über eine DOMSource übergebeben und die Ausgabe in eine Datei umgeleitet.
Listing 32.15: \Beispiele\de\j2sebuch\kap32\Transform1.java import java.io.*; import org.w3c.dom.*; import javax.xml.parsers.*; import javax.xml.transform.*; import javax.xml.transform.dom.*; import javax.xml.transform.stream.*; public class Transform1 { public Transform1() { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File("Artikel.xml")); TransformerFactory tf = TransformerFactory.newInstance();
858
32 – XML Listing 32.15: \Beispiele\de\j2sebuch\kap32\Transform1.java (Forts.) trf.transform(new DOMSource(doc), new StreamResult( new File("Daten.xml"))); } catch(Exception ex) {} } public static void main(String[] args) { new Transform1(); } }
32.4.3
XML-Dokumente transformieren
Zum Transformieren eines XML-Dokuments mit einem XSL-StyleSheet gehen Sie folgendermaßen vor. Erstellen Sie ein Transformer-Objekt und geben Sie dabei das StyleSheet mit dem entsprechenden Source-Typ an (hier als Stream aus der Datei Artikel.xsl). Über entsprechende Source- bzw. Result-Objekte werden dem Transformer-Objekt anschließend die zu transformierende XML-Datei und die Zieldatei übergeben.
[IconInfo]
Als Eingabe erhält der Transformer diesmal eine Datei und keinen DOMBaum. Außerdem wird beim Anlegen des Transformer ein XSL-StyleSheet festgelegt. Geben Sie als Parameter im Konstruktor von StreamResult den Wert System.out an, wird die Ausgabe auf der Konsole durchgeführt.
Listing 32.16: \Beispiele\de\j2sebuch\kap32\Transform2.java import java.io.*; import javax.xml.transform.*; import javax.xml.transform.stream.*; public class Transform2 { public Transform2() { try { TransformerFactory tf = TransformerFactory.newInstance(); Transformer trf = tf.newTransformer( new StreamSource("Artikel.xsl")); trf.transform(new StreamSource("Artikel.xml"), new StreamResult(new File("Artikelliste.html"))); } catch(Exception ex) {} }
Java 5 Programmierhandbuch
859
XSLT-Transformationen Listing 32.16: \Beispiele\de\j2sebuch\kap32\Transform2.java (Forts.) public static void main(String[] args) { new Transform2(); } }
860
33 JDBC – Datenbankzugriff 33.1 Einführung Aufgrund der immer größer werdenden Informationsmengen ist die Verwendung von Datenbanken zum Speichern dieser Informationen nicht mehr wegzudenken. Java bietet über das JDBC API die Schnittstelle zur Anbindung einer Anwendung oder eines Applets an eine Datenbank. JDBC wird von Sun nicht als Abkürzung, sondern als ein Markenzeichen verwendet, obwohl der Schluss nahe liegt, JDBC als Java Database Connectivity zu interpretieren. Wahrscheinlich war es zu Beginn auch so. Für den einheitlichen Zugriff auf relationale Datenbanken (diese speichern die Daten in Tabellen) unterschiedlicher Hersteller musste eine Spezifikation geschaffen werden, die einen standardisierten Zugriff definiert. Sie können dadurch mit den gleichen Anweisungen auf Oracle, DB2 oder MySQL zugreifen. Seit dem JDK 1.4 wird die Spezifikation JDBC 3.0 unterstützt. Diese vereint die vorher getrennt verfügbaren Packages java.sql (core – Basis) und javax.sql (optional – für serverseitige Anwendungen). Das API besitzt zahlreiche Interfaces, die durch die entsprechenden JDBC-Treiber implementiert werden. Neben dem reinen Datenbankzugriff wird eine weitere Sprache benötigt, über die man die gewünschten Informationen aus der Datenbank gewinnt. Dafür wird, wie in relationalen Datenbanken üblich, SQL (Structured Query Language – strukturierte Abfragesprache) eingesetzt. Die über SQL gewonnenen Informationen werden innerhalb einer Ergebnismenge von der Datenbank zurückgegeben. SQL kann aber auch für Aktionen auf der Datenbank genutzt werden. So können Sie z.B. Daten löschen oder Tabellen anlegen.
33.1.1
JDBC-Treiber
Damit über JDBC eine Datenbank angesprochen werden kann, wird ein JDBC-Treiber benötigt. Alle bekannten Hersteller stellen diese meist kostenlos zur Verfügung. Über die URL http://java.sun.com/products/jdbc/ gelangen Sie nach einem Klick auf den Link DRIVER DATABASE zur URL http://servlet.java.sun.com/products/jdbc/drivers. Hier können Sie sich über die Verfügbarkeit der JDBC-Treiber für ein bestimmtes Datenbanksystem informieren. Jeder Hersteller liefert zu seinem Treiber eine Liste der unterstützten Features des JDBC API und von SQL mit. Damit bei der Verwendung eines JDBC-Treibers eine gewisse Grundfunktionalität gesichert wird, muss ein solcher Treiber mindestens den SQL ANSI 92 Entry Level unterstützen. Dieser beinhaltet unter anderem das Erstellen von Tabellen, Indizes und Views sowie die Ausführung einfacher Datenbankabfragen. JDBC-Versionsübersicht Version
Erläuterung
1.x
Die Version 1.0 war noch als zusätzliches Package zu laden. In der Version 1.1 war das Package java.sql Bestandteil des JDK.
Java 5 Programmierhandbuch
861
Einführung Version
Erläuterung
2.x
Im JDK 1.2 wurden das Core-API des Package java.sql sowie das optionale API javax.sql umfangreich erweitert. Dies umfasste scrollbare Ergebnismengen, Batch Updates und programmierbare Updates, Connection Pooling, verteilte Transaktionen und das RowSet API.
3.x
Im JDK 1.4 wurde die Version 3.0 eingeführt. Das Core-API und das optionale API wurden erstmals zusammengelegt. Neue Features waren beispielsweise Sicherungspunkte oder zusammengefasste Anweisungen.
33.1.2
Treibertypen
Für die Entwicklung eines JDBC-Treibers muss ein Hersteller sicherstellen, dass er die geforderten standardisierten Schnittstellen unterstützt. Wie er jedoch konkret auf die betreffende Datenbank zugreift, ist ihm überlassen. Die Art und Weise dieses Zugriffs wird in vier Kategorien unterteilt. Im Sprachgebrauch wird auch von Klasse 1- bis Klasse 4-Treibern gesprochen. Das Hauptunterscheidungsmerkmal besteht in der Notwendigkeit, auf Client-Seite zusätzlich zur Java-Anwendung und dem JDBC-Treiber weitere, herstellerabhängige Software zu installieren. Dies kann dazu führen, dass Ihre Anwendungen nicht mehr plattformunabhängig sind. In der folgenden Tabelle werden die vier Treibertypen vorgestellt. Die Namen der Treiber werden in der Literatur häufig anders benannt, die Typklasse ist jedoch immer gleich. Treibertyp
Erläuterung
JDBC-ODBCBrücke
Zu Beginn der Entwicklung von JDBC waren noch nicht viele Treiber verfügbar. Über die JDBC-ODBC-Brücke war es möglich, eine Datenbankverbindung über vorhandene ODBC-Treiber herzustellen. Die Operationen werden vom JDBC-Treiber an den ODBC-Treiber weitergegeben. ODBC-Treiber müssen separat auf jedem Client-Rechner installiert werden und laufen nur auf einem bestimmten Betriebssystem. Dies macht die Installation einer plattformunabhängigen Java-Anwendung sehr aufwendig, da für jedes zu unterstützende Betriebssystem ein ODBC-Treiber benötigt wird. Meist werden auch noch zusätzliche Treiber für das betreffende DBS auf dem Client benötigt. Bei der Verwendung in Applets kommt hinzu, dass diese standardmäßig keinen Zugriff auf Anwendungen des Betriebssystems haben und somit spezielle Rechte eingerichtet werden müssen.
Klasse 1-Treiber
Natives API Klasse 2-Treiber
862
Diese JDBC-Treiber kommen zwar ohne die Installation von ODBC aus, sind aber selbst plattformabhängig. Die Kommunikation mit dem DBS erfolgt über einen betriebssystemabhängigen Teil, der wiederum mit dem Treiber des DBS zusammenarbeitet. Dadurch benötigen Sie wiederum für jedes zu unterstützende Betriebssystem spezielle Bibliotheken für den JDBC- und den Datenbanktreiber. Der Zugriff auf die Datenbank erfolgt hier jedoch relativ schnell, da keine Umsetzung auf eine weitere Schnittstelle wie ODBC erfolgen muss.
33 – JDBC – Datenbankzugriff Treibertyp
Erläuterung
Reiner JavaTreiber
Auf der Client-Seite wird nur der betreffende JDBC-Treiber für das DBS benötigt, der vollständig in Java implementiert ist. Damit erreichen Sie eine vollständige Plattformunabhängigkeit. Über ein Standardnetzwerkprotokoll wie z.B. TCP/IP oder HTTP kommuniziert der Treiber mit einer serverseitigen Komponente. Sie kann direkt im DBS implementiert sein oder muss als separater Bestandteil installiert werden.
Klasse 3-Treiber
Auch dieser Treiber liegt vollständig in Java vor. Im Gegensatz zu Klasse 3-Treibern verwendet er ein datenbankspezifisches Protokoll zur Kommunikation über das Netzwerk. Dieses Protokoll ist in der Regel immer vom verwendeten DBS abhängig.
Reiner JavaTreiber und natives DBSProtokoll Klasse 4-Treiber
Ein wichtiger Gesichtspunkt bei der Verwendung der Klasse 3- und 4-Treiber ist die Kommunikation über geschützte Netzwerkressourcen wie Firewalls und Proxy-Server. Während Klasse 3-Treiber Standardports für die Kommunikation nutzen, setzen Klasse 4-Treiber spezielle Ports ein. Die Freischaltung dieser Ports bedeutet häufig einen Verlust an Sicherheit im Netzwerk und macht den Netzwerkadministrator nervös (besonders, wenn eine Anbindung an das Internet besteht).
[IconInfo]
Klasse 1 - Treiber
Klasse 2 - Treiber
Klasse 3 - Treiber
Klasse 4 - Treiber
Java - Anwendung JDBC-Treiber
JDBC-Treiber
ODBC-Treiber
DBS-Treiber
JDBC-Treiber DBS-Treiber
JDBC-Treiber Client
DBS-Treiber
Netzwerk
DBS-Treiber
DBS-Treiber
DBS-Treiber
DBS-Treiber Server
Datenbanksystem
Abb. 33.1: Die verschiedenen Klasse X-Treiber
Java 5 Programmierhandbuch
863
Einführung
33.1.3
Architektur von Datenbankanwendungen
Zwei- und mehrschichtige Anwendungen Zweischichtige Anwendungen sind die klassische Art des Datenbankzugriffs. Dieses Modell wird auch häufig als Client-Server-Architektur bezeichnet. Auf dem Client-Rechner wird ein JDBC-Treiber benötigt, der über ein Netzwerk direkt mit dem Datenbanksystem kommuniziert. Als Netzwerk kann hierbei ein Intranet oder das Internet dienen. In einer mehrschichtigen Anwendung benötigt der Client keine Datenbankkomponenten. Er sendet seine Anforderungen über ein Netzwerkprotokoll (TCP/IP, HTTP, RMI oder andere) an eine mittlere Schicht. Diese Mittelschicht wird auch als Application Server bezeichnet. Die mittlere Schicht stellt die Verbindung zur Datenbank her und sendet das Ergebnis über Standardnetzwerkprotokolle zurück zum Client. Statt einer mittleren Schicht können auch mehrere Schichten hinzugefügt werden. Der Vorteil dieser Architektur ist, dass so genannte Thin Clients (schlanke Clients) erzeugt werden, da die Logik und die Software für den Datenbankzugriff in der mittleren Schicht liegen. Änderungen an der Datenbankschnittstelle müssen nur noch dort vorgenommen werden. Dies ist insbesondere dann interessant, wenn sehr viele Clients zum Einsatz kommen. Durch Lastverteilung, den Einsatz mehrerer Datenbanken und anderer Techniken wird eine erhöhte Sicherzeit geboten und die Verarbeitungsgeschwindigkeit ist in der Regel höher.
Zweischichtig
Mehrschichtig
Java - Anwendung Client JDBC
Netzwerk API
Netzwerk
Applicationserver Middle-Tier JDBC
Datenbanksystem
Server
Abb. 33.2: Zwei- und mehrschichtige Architekturen
864
33 – JDBC – Datenbankzugriff
Klassen einer Datenbankanwendung Als Überblick zum Aufbau einer JDBC-Anwendung sollen kurz die Klassen und Schnittstellen vorgestellt werden, die für einfache Datenbankwendungen benötigt werden. 1. Zuerst werden ein oder mehrere JDBC-Treiber über den Treibermanager geladen, die danach von der Klasse DriverManager verwaltet werden. 2. Über den DriverManager wird eine Verbindung zu einer Datenbank aufgebaut und in einem Connection-Objekt zurückgegeben. 3. Über eine Verbindung können verschiedene Anweisungen an die Datenbank gesendet werden. 4. Zwei dieser Anweisungstypen liefern eine Ergebnismenge zurück, die über ein ResultSet-Objekt ausgewertet werden kann. JDBC-Treiber 1 DriverManager
JDBC-Treiber 2
Connection
CallableStatement
Statement
PreparedStatement
ResultSet
Abb. 33.3: Klassen und Interfaces des JDBC API
Bis auf die Klasse DriverManager handelt es sich bei den anderen Elementen um Interfaces. Die Implementierung dieser Schnittstellen obliegt dem JDBC-Treiber. Wenn Sie mit Objekten vom Typ dieser Interfaces arbeiten, werden diese Objekte vom JDBC-Treiber erzeugt.
33.2 Einrichten einer Datenbank Für die Verwendung von JDBC ist natürlich eine Datenbank erforderlich. Dazu werden im Folgenden zwei Datenbanksysteme (DBS) vorgestellt und deren Installation und rudimentäre Verwendung unter MS Windows beschrieben. Beide DBS stehen auch für zahlreiche andere Betriebssysteme kostenfrei zur Verfügung. Werfen Sie in jedem Fall einen Blick auf die aktuellen Lizenzbestimmungen.
Java 5 Programmierhandbuch
865
Einrichten einer Datenbank
33.2.1
MySQL
Die vielleicht bekannteste Datenbank im Bereich der Web-Entwicklung ist MySQL. Besondere Merkmale sind die einfache Installation und hohe Leistungsfähigkeit. Zu beachten ist, dass MySQL in der aktuellen Version bestimmte Eigenschaften anderer moderner DBS nicht implementiert wie z.B. Trigger und Stored Procedures. In den kommenden Versionen soll dies jedoch nachgeholt werden. Download Sie benötigen einerseits die MySQL-Datenbank, andererseits einen MySQL-JDBC-Treiber. Beide können Sie unter den folgenden URLs beziehen. Verwenden Sie für MySQL die Version mit Installer, im Falle des JDBC-Treibers die ZIP-Version. http://dev.mysql.com/downloads/mysql/4.0.htmlaktuelle Version 4.0.20 http://dev.mysql.com/downloads/connector/j/3.0.htmlaktuelle Version 3.0.14 Installation von MySQL 쐌 Entpacken Sie die ZIP-Datei und führen Sie das Setup aus. Geben Sie gegebenenfalls ein anderes Installationsverzeichnis an. Alle anderen Einstellungen können auf dem angebotenen Wert belassen werden. 쐌 Starten Sie im ..\bin-Verzeichnis der Installation die Anwendung winmysqladmin. Geben Sie einen beliebigen Benutzernamen und ein Passwort ein. Es wird in Ihrem Windows-Verzeichnis eine Konfigurationsdatei my.ini erzeugt. Diese enthält Standardeinstellungen, den eingegebenen Benutzernamen und das Passwort. 쐌 Klicken Sie rechts unten im Infobereich auf das neu hinzugekommene Ampelsymbol und danach auf den Menüpunkt WIN NT - INSTALL THE SERVICE. Dadurch wird der MySQL-Service installiert und automatisch bei jedem Start von Windows ausgeführt. 쐌 Um den MySQL-Service sofort zu starten, öffnen Sie erneut das Menü des Ampelsymbols und wählen den Menüpunkt WIN NT - START THE SERVICE. Das Ampelsymbol schaltet auf Grün und Sie können jetzt auf die Datenbank zugreifen. 쐌 Nehmen Sie das Verzeichnis [InstallDir]\bin in die PATH-Angabe auf, damit Sie das Kommandozeilentool mysql jederzeit aufrufen können. Installation des MySQL-JDBC-Treibers 쐌 Entpacken Sie die ZIP-Datei in ein beliebiges Verzeichnis. 쐌 Kopieren Sie das JAR-Archiv mysql-connector-java-3.0.14-production-bin.jar in die Verzeichnisse C:\Programme\Java\jre1.5.0\lib\ext und [InstallJDK]\jre\lib\ext. Verwendung von MySQL Öffnen Sie eine Konsole (Eingabeaufforderung, DOS-Prompt) und geben Sie das Kommando mysql ein. Es wird der mysql-Prompt angezeigt, über den Sie Kommandos an die Datenbank schicken können.
866
33 – JDBC – Datenbankzugriff
Erstellen Sie eine neue Datenbank mit dem Namen Kunden. Neue Datenbanken werden standardmäßig im Verzeichnis [InstallDir]\Data als neues Unterverzeichnis angelegt. mysql> CREATE DATABASE Kunden;
Um eine Datenbank zu verwenden, wird das Kommando USE angegeben. mysql> USE Kunden;
Erstellen Sie jetzt eine Tabelle mit dem Namen Kunde. Diese besitzt das Feld ID zur Nummerierung der Kunden und das Feld Name für den Namen des Kunden. mysql> CREATE TABLE Kunde(ID INTEGER, Name VARCHAR(30));
Fügen Sie zwei Datensätze in die Tabelle ein. mysql> INSERT INTO Kunde VALUES(1, "Meier"); mysql> INSERT INTO Kunde VALUES(2, "Schulze");
Verwenden Sie die folgende Abfrage, um alle Datensätze der Tabelle Kunde anzuzeigen. mysql> SELECT * FROM Kunde;
Zum Schließen der mysql-Konsole geben Sie das Kommando QUIT ein. mysql> QUIT
33.2.2
Firebird installieren
Als Borland vor einigen Jahren seine Datenbank Interbase als OpenSource zur Verfügung stellte, spalteten sich verschiedene Entwicklungszweige ab. Einer war das Projekt Firebird. Obwohl Firebird ein sehr leistungsfähiges Datenbanksystem ist, das auch Features wie Trigger und Stored Procedures unterstützt, gestaltet sich die Installation und Inbetriebnahme sehr einfach. Download Wie auch bei MySQL benötigen Sie die Firebird-Datenbank und den separaten JDBC-Treiber. 쐌 Öffnen Sie die URL http://firebird.sourceforge.net/ und klicken Sie auf den Menüpunkt DOWNLOAD - FIREBIRD RELATIONAL DATABASE. Unter der Überschrift DOWNLOADS wählen Sie den Link Official Windows Setup and Installer For Classic and SuperServer V1.5. Es öffnet sich eine neue Seite, auf der Sie den Server mit den Setup-Dateien auswählen können.
Java 5 Programmierhandbuch
867
Einrichten einer Datenbank
쐌 Öffnen Sie die URL http://firebird.sourceforge.net/ und klicken Sie auf den Menüpunkt DOWNLOAD - FIREBIRD CLASS 4 JCA-JDBC-DRIVER. Wählen Sie den Link Jaybird for JDK 1.4. Es öffnet sich wieder eine neue Seite, auf der Sie den Download-Server auswählen können. Installation von Firebird 쐌 Starten Sie das Setup. Wählen Sie bei der Auswahl der Komponenten FULL INSTALLATION OF SUPER SERVER AND DEVELOPMENT TOOLS. Alle anderen Einstellungen können mit den Vorgabewerten übernommen werden. Nach der Installation wird sofort der Dienst gestartet und der Datenbankserver kann angesprochen werden. 쐌 Öffnen Sie die Datei services in Ihrem Windows-System-Verzeichnis, z.B. unter [WinDir]\system32\drivers\etc\services in einem Texteditor. Fügen Sie die Zeile gds_db
3050/tcp
#Firebird
hinzu. Darüber wird der Port 3050 mit der Firebird-Datenbank verknüpft. 쐌 Nehmen Sie das Verzeichnis [InstallDir]\bin in die PATH-Angabe auf, damit Sie unter anderem das Kommandozeilentool isql jederzeit aufrufen können. Installation des Firebird-JDBC-Treibers 쐌 Entpacken Sie die ZIP-Datei in ein beliebiges Verzeichnis. 쐌 Kopieren Sie das JAR-Archiv firebirdsql-full.jar in die Verzeichnisse C:\Programme\ Java\jre1.5.0\lib\ext und [InstallJDK]\jre\lib\ext. Verwendung von Firebird Öffnen Sie eine Konsole (Eingabeaufforderung, DOS-Prompt) und geben Sie das Kommando isql (interactive sql) ein. Es wird der SQL-Prompt angezeigt, über den Sie Kommandos an die Datenbank schicken können. Andere Datenbankserver, wie z.B. der MS SQL-Server, besitzen ebenfalls ein isql-Tool. Rufen Sie in diesem Fall isql direkt aus dem ..\bin-Verzeichnis von Firebird auf. Erstellen Sie eine neue Datenbank mit dem Namen Kunden. In Firebird benötigen Sie zum Öffnen und Erstellen einer Datenbank ein Benutzerkonto. Standardmäßig ist der Benutzer SYSDBA mit dem Passwort masterkey angelegt. Datenbanken werden in Firebird als Dateien verwaltet. Geben Sie dazu immer den vollständigen Pfadnamen an. SQL> CREATE DATABASE "C:\Temp\Kunden.gdb" USER "SYSDBA" PASSWORD "masterkey";
Um eine bereits vorhandene Datenbank zu öffnen, müssen Sie sich mit ihr verbinden. SQL> CONNECT "C:\Temp\Kunden.gdb" USER "SYSDBA" PASSWORD "masterkey";
868
33 – JDBC – Datenbankzugriff
Erstellen Sie jetzt eine Tabelle mit dem Namen Kunde. Diese besitzt das Feld ID zur Nummerierung der Kunden und das Feld Name für den Namen des Kunden. SQL> CREATE TABLE Kunde(ID INTEGER, Name VARCHAR(30));
Fügen Sie zwei Datensätze in die Tabelle ein. Im Gegensatz zu MySQL müssen Sie hier einfache Anführungszeichen verwenden. SQL> INSERT INTO Kunde VALUES(1, 'Meier'); SQL> INSERT INTO Kunde VALUES(2, 'Schulze');
Benutzen Sie die folgende Abfrage, um alle Datensätze der Tabelle Kunde anzuzeigen. SQL> SELECT * FROM Kunde;
Zum Schließen der SQL-Konsole geben Sie das Kommando QUIT ein. SQL> QUIT;
[IconInfo]
Wenn Sie über eine Java-Anwendung Änderungen in einer FirebirdDatenbank durchführen, sind diese eventuell nicht sofort über isql sichtbar. Rufen Sie in diesem Fall in isql das Kommando COMMIT; auf. Dadurch werden die Anweisungen in isql bestätigt und man erhält eine aktuelle Sicht auf die Datenbank.
33.3 Herstellen der Datenbankverbindung 33.3.1
Einführung
Sie haben zwei Möglichkeiten, eine Verbindung zu einer Datenbank herzustellen. Die häufiger eingesetzte ist das Laden des JDBC-Treibers über den Treibermanager mit einer anschließenden Erstellung eines Connection-Objekts. Über eine Datasource (Datenquelle) können Sie über einen anderen Weg eine Verbindung herstellen. Dazu werden alle Einstellungen für den Zugriff auf eine Datenbank vorgenommen und diese unter einem bestimmten Namen im JNDI (Java Naming and Directory Interface) registriert. Die Registrierung kann von mehreren Anwendungen genutzt werden. Um eine Datasource zu nutzen, implementiert ein JDBC-Treiber das Interface DataSource. Die Implementierung kann sich jedoch zwischen den Treibern unterscheiden, so dass die Verwendung für jedes Datenbanksystem anders erfolgen kann.
Die folgenden Erläuterungen und Beispiele verwenden zur Herstellung der Verbindung zu einer Datenbank immer den Treibermanager. [IconInfo]
Java 5 Programmierhandbuch
869
Herstellen der Datenbankverbindung
33.3.2
JDBC-Treiber laden
Über die Klasse java.sql.DriverManager werden die JDBC-Treiber verwaltet. Außerdem stellen Sie über den Treibermanager die Verbindung zur Datenbank her. Dazu wird der vorher geladene Treiber eingesetzt. Um einen Treiber über den Treibermanager zu laden, gibt es zwei Vorgehensweisen. Laden der Treiberklasse Über die Methode forName() der Klasse Class wird die Klasse des Treibers vom ClassLoader geladen. Die Implementierung eines JDBC-Treibers enthält in der Treiberklasse einen statischen Initialisierungsblock. Darin wird eine Instanz der Treiberklasse erzeugt und er registriert sich beim Treibermanager. Die folgenden Anweisungen laden die JDBC-Treiber für Firebird und MySQL. Kann eine Treiberklasse nicht gefunden werden, wird eine ClassNotFoundException ausgelöst. try { Class.forName("org.firebirdsql.jdbc.FBDriver"); Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) {}
Verwendung der Systemeigenschaft jdbc.drivers Nach dem Laden der Klasse DriverManager durch den ClassLoader wertet diese die Systemeigenschaft jdbc.drivers aus und lädt alle darin angegebenen Treiber. Sie können diese Eigenschaft beim Aufruf des Java Interpreters angeben. Mehrere Treiber werden durch einen Doppelpunkt voneinander getrennt. java -Djdbc.drivers=org.firebirdsql.jdbc.FBDriver: com.mysql.jdbc.Driver
Wenn Sie alle benötigten Informationen zum Laden des JDBC-Treibers und zum Aufbau der Datenbankverbindung in eine *.properties-Datei einfügen, können Sie diese ändern, ohne die Anwendung erneut übersetzen zu müssen. [IconInfo]
870
33 – JDBC – Datenbankzugriff
Methoden des Treibermanagers Alle Methoden der Klasse DriverManager sind statisch. Über die überladene Methode getConnection() können Sie eine Verbindung zu einer Datenbank aufbauen. Connection getConnection(String url) Connection getConnection(String url, Properties p) Connection getConnection(String url, String benutzer, String passwort)
Mit der Methode getDriver() erhalten Sie eine Referenz auf einen geladenen Treiber und können dessen Methoden aufrufen. Driver getDriver(String url)
Für die Ermittlung aller geladenen Treiber verwenden Sie die folgende Methode. Enumeration getDrivers()
Möchten Sie die Meldungen des Treibermanagers protokollieren, setzen Sie einen LogWriter. Durch die Übergabe von null wird der Mechanismus deaktiviert. setLogWriter(PrintWriter pw)
Nach der erfolgreichen Installation von MySQL und/oder Firebird werden beide Treiber über den Treibermanager geladen. Mithilfe der Methode getDrivers() werden die geladenen JDBC-Treiber ermittelt und ein paar Informationen ausgegeben. [IconInfo]
Listing 33.1: \Beispiele\de\j2sebuch\kap33\TreiberManager.java import java.io.*; import java.util.*; import java.sql.*; public class TreiberManager { public TreiberManager() { DriverManager.setLogWriter(new PrintWriter(System.out)); try { Class.forName("com.mysql.jdbc.Driver"); Class.forName("org.firebirdsql.jdbc.FBDriver"); }
Java 5 Programmierhandbuch
871
Herstellen der Datenbankverbindung Listing 33.1: \Beispiele\de\j2sebuch\kap33\TreiberManager.java (Forts.) catch(ClassNotFoundException cnfEx) { System.out.println("Konnte Treiber " + cnfEx.getMessage() + " nicht laden."); } for(Enumeration driver = DriverManager.getDrivers(); driver.hasMoreElements();) { Driver d = driver.nextElement(); System.out.println("Treiber: " + d.toString()); System.out.println("Version: " + d.getMajorVersion()); } } public static void main(String args[]) { new TreiberManager(); } }
33.3.3
Die Verbindung herstellen
Wurde der JDBC-Treiber für das entsprechende Datenbanksystem erfolgreich geladen, können Sie eine Verbindung zu einer Datenbank herstellen. Dazu rufen Sie die Methode getConnection() der Klasse DriverManager auf und erhalten im Falle einer erfolgreichen Verbindung ein Connection-Objekt zurück. Der Methode getConnection() muss ein Verbindungsstring übergeben werden, der aus drei Teilen besteht. Es gibt dafür aber keine Normierung, deshalb kann der Aufbau für jeden JDBC-Treiber unterschiedlich sein. Der String besteht aus den Teilen: jdbc:Protokoll:Name
Der String beginnt immer mit jdbc. Alle drei Teile werden durch einen Doppelpunkt voneinander getrennt. Als Protokoll wird der Name eines JDBC-Treibers oder eines weiteren Protokolls, z.B. ODBC, angegeben. Der letzte Teil stellt den Zugriffspfad für die Datenbank dar. Dies kann ein Dateiname, eine URL oder ein Aliasname sein. Je nach verwendetem Datenbanksystem wird er anders interpretiert. Beispiele Der erste String verwendet die JDBC-ODBC-Brücke. Der Name Kunden ist ein Name einer Datenquelle in der ODBC-Verwaltung. Eine Datenquelle kann wiederum auf eine Datenbank verweisen, im Falle von Firebird z.B. auf eine Datei. Das zweite Beispiel benutzt den JDBC-Treiber von MySQL und greift auf die lokale Datenbank Kunden zu. Da MySQL alle Datenbanken an einer definierten Position im Dateisystem speichert, muss keine Pfad-
872
33 – JDBC – Datenbankzugriff
angabe verwendet werden. Beim zweiten Zugriff auf die MySQL-Datenbank wird zusätzlich ein Benutzername angegeben. Weitere Parameter können durch ein Fragezeichen getrennt mitgeteilt werden. Eine vollständige Pfadangabe ist für die Angabe der Datenbank in Firebird im vierten Verbindungsstring notwendig. jdbc:odbc:Kunden jdbc:mysql://localhost/Kunden jdbc:mysql://localhost/Kunden?user=root jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb
Einige DBS benötigen zusätzlich zur Angabe der Datenbank einen Benutzernamen und ein Passwort, z.B. Firebird. Diese werden als zweiter und dritter Parameter der Methode getConnection() übergeben oder, wie im Falle von MySQL, durch einen erweiterten Verbindungsstring. Zur Ausführung des Beispiels benötigen Sie entweder eine MySQL- oder eine Firebird-Datenbank mit dem Namen Kunden. Passen Sie gegebenenfalls die Pfadnamen an und kommentieren Sie die Anweisungen für den Zugriff auf die nicht vorhandenen Datenbanken aus. [IconInfo]
Es wird jeweils ein Datenbanktreiber für MySQL und Firebird geladen und eine Verbindung zu einer Datenbank hergestellt. Im Falle von MySQL wird als Zugriffsstring ein Hostname und der Datenbankname verwendet. Firebird benötigt eine vollständige Pfadangabe. Außerdem müssen Sie einen Benutzernamen und ein Passwort an die Pfadangabe anhängen (MySQL) bzw. die zusätzlichen Parameter der Methode getConnection() dazu verwenden.
Listing 33.2: \Beispiele\de\j2sebuch\kap33\Verbindung.java import java.sql.*; public class Verbindung { public Verbindung() { try { Class.forName("com.mysql.jdbc.Driver"); Class.forName("org.firebirdsql.jdbc.FBDriver"); } catch(ClassNotFoundException cnfEx) { System.out.println("Konnte Treiber " + cnfEx.getMessage() + " nicht laden."); System.exit(1); }
Java 5 Programmierhandbuch
873
SQL-Anweisungen einsetzen Listing 33.2: \Beispiele\de\j2sebuch\kap33\Verbindung.java (Forts.) try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root"); Connection verbFB = DriverManager.getConnection( "jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb", "SYSDBA", "masterkey"); } catch(SQLException sqlEx) { System.out.println("Konnte Verbindung nicht herstellen: " + sqlEx.getMessage()); } } public static void main(String args[]) { new Verbindung(); } }
[IconInfo]
Der MySQL-JDBC-Treiber ruft in den mitgelieferten Beispielen des Treibers nach der Methode forName() noch die Methode newInstance() auf, weil es wohl sonst Probleme beim Laden des Treibers bei einigen Java-Implementationen gibt. Damit wird ein Objekt der Treiberklasse angelegt, obwohl dies eigentlich Aufgabe des Treibermanager ist. Bei der Ausführung in unseren Beispielen haben wir auf den zusätzlichen Aufruf verzichtet, da es keine Probleme bei der Ausführung der Anwendungen gab. Class.forName("com.mysql.jdbc.Driver").newInstance();
33.4 SQL-Anweisungen einsetzen 33.4.1
Einführung
Nachdem eine Verbindung zu einer Datenbank hergestellt wurde, können Sie SQL-Anweisungen ausführen. Diese können eine Ergebnismenge (mehrere Datensätze, die aus mehreren Feldern bestehen können) oder einen einzelnen Wert zurückliefern. Andere Anweisungen führen eine Aktion in der Datenbank aus. So können Sie neue Datensätze hinzufügen oder vorhandene löschen bzw. ändern. JDBC stellt drei Interfaces bereit, über die SQLAnweisungen ausgeführt werden können.
874
33 – JDBC – Datenbankzugriff Interface
Erläuterung
Statement
Dieses Interface verwenden Sie, wenn die SQL-Anweisung ein Ergebnis zurückliefert. Möchten Sie die Anweisung mehrmals ausführen, sollten Sie das Interface PreparedStatement nutzen. Die Rückgabe einer Ergebnismenge über die Methoden des Interfaces schließt eine bereits vorliegende Ergebnismenge. Beachten Sie weiterhin, dass immer nur eine Ergebnismenge zur gleichen Zeit bearbeitet werden kann.
PreparedStatement
Jede SQL-Anweisung muss in Anweisungen des Datenbanksystems übersetzt werden. Führen Sie eine Abfrage mehrmals aus, gegebenenfalls über verschiedene Parameter, nutzen Sie dieses Interface. Die Abfrage wird nur einmalig vorbereitet und danach schneller ausgeführt. Das Vorbereiten kostet etwas mehr Aufwand als bei der Verwendung des Interfaces Statement.
CallableStatement
Um Stored Procedures aufzurufen, nutzen Sie dieses Interface. Der Vorteil liegt darin, dass für alle Datenbanksysteme dieselbe Syntax eingesetzt werden kann. Allerdings muss diese auch von den betreffenden JDBC-Treibern des DBMS unterstützt werden.
Anweisungsobjekte werden über ein Connection-Objekt erzeugt. Dazu stehen drei verschiedene Implementierungen der Methode createStatement() zur Verfügung, von denen die am häufigsten verwendete im Folgenden erläutert wird. Nachdem Sie ein Statement-Objekt besitzen, können Sie mit ihm eine SQL-Anweisung ausführen. Connection con = ... Statement stmt = con.createStatement();
Allgemeine Methoden des Interfaces Statement Das Interface bietet Methoden zum Zugriff auf die zurückgegebene Ergebnismenge und zum Bearbeiten von deren Eigenschaften. Da die beiden anderen Interfaces zum Ausführen von SQL-Anweisungen von Statement abgeleitet sind, können Sie die Methoden auch dort verwenden. Die Ausführung einer SQL-Anweisung wird mit dem Aufruf von cancel() abgebrochen. Diese Funktionalität muss vom JDBC-Treiber und dem DBMS unterstützt werden, sonst ist der Aufruf wirkungslos. void cancel()
Wenn Sie keinen Zugriff auf die Ergebnisse einer Anweisung mehr benötigen, können Sie durch die Ausführung von close() Systemressourcen freisetzen. void close()
Java 5 Programmierhandbuch
875
SQL-Anweisungen einsetzen
Für die Ermittlung des Connection-Objekts einer Anweisung rufen Sie diese Methode auf. Connection getConnection()
Mit den folgenden Methoden können Sie die Wartezeit zur Ausführung einer SQL-Anweisung auslesen bzw. setzen. Die Angaben erfolgenden in Sekunden. Bei einem Wert von 0 wird unendlich lange gewartet. int getQueryTimeout() void setQueryTimeout(int sekunden)
33.4.2
Anweisungen ausführen
Über das Statement-Objekt können jetzt SQL-Anweisungen ausgeführt werden. Außerdem bietet das Interface zahlreiche weitere Methoden, die in den entsprechenden Abschnitten vorgestellt werden. Steht der Rückgabewert der SQL-Anweisung nicht fest (eine oder mehrere Ergebnismengen, kein Rückgabewert oder ein Wert vom Typ int), nutzen Sie die Methode execute(). Ist der Rückgabewert true, liegt eine Ergebnismenge vor. Für den Zugriff auf das Ergebnis der Ausführung nutzen Sie dann die Methoden getResultSet(), getMoreResults() oder getUpdateCount() des Interfaces Statement. boolean execute(String sqlAnweisung)
Typischerweise werden mit der folgenden Methode SQL-SELECT-Anweisungen ausgeführt. Als Rückgabewert wird eine Ergebnismenge geliefert. Auch wenn die Ergebnismenge leer ist, wird nicht der Wert null geliefert, sondern das entsprechende ResultSetObjekt enthält dann keine Datensätze. ResultSet executeQuery(String sqlAnweisung)
Die Methode executeUpdate() führt SQL-Anweisungen aus, die zum Erstellen von Tabellen oder Indizes oder zum Anlegen, Löschen und Ändern von Datensätzen dienen. Als Rückgabewert wird die Anzahl der betroffenen Datensätze bzw. der Wert 0 zurückgegeben. int executeUpdate(String sqlAnweisung)
[IconInfo]
876
Das JDBC-API sieht keine Möglichkeit vor, Datenbanken zu erzeugen. Dies muss mit einem separaten Tool durchgeführt werden, welches meist mit dem DBS installiert wird, z.B. isql. Über Java haben Sie aber die Möglichkeit, solche Anwendungen zu starten und über ein Skript die Datenbank zu erstellen.
33 – JDBC – Datenbankzugriff Dieses Beispiel verwendet MySQL zum Erstellen einer Tabelle Artikel und fügt darin zwei Datensätze ein. Zur Verwendung von Firebird müssen Sie dessen Treiber laden und die Verbindung zur Datenbank auf die bereits beschriebene Weise herstellen. [IconInfo]
Über die Methode executeUpdate() wird die Tabelle Artikel erstellt und die beiden Datensätze werden eingefügt. Existiert die Tabelle schon, wird eine Exception ausgelöst.
Listing 33.3: \Beispiele\de\j2sebuch\kap33\Anweisungen.java import java.sql.*; public class Anweisungen { public Anweisungen() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden"); Statement stmtMySQL = verbMySQL.createStatement(); stmtMySQL.executeUpdate( "CREATE TABLE Artikel(ID INTEGER, Name VARCHAR(30))"); stmtMySQL.executeUpdate( "INSERT INTO Artikel VALUES(1, 'Schrauben')")); stmtMySQL.executeUpdate( "INSERT INTO Artikel VALUES(2, 'Muttern')")); } catch(SQLException sqlEx) { System.out.println(sqlEx.getMessage()); } } public static void main(String args[]) { new Anweisungen(); } }
Java 5 Programmierhandbuch
877
SQL-Anweisungen einsetzen
33.4.3
Vorbereitete Anweisungen
Wenn Sie eine SQL-Anweisung sehr häufig einsetzen, können Sie diese vorbereiten lassen. Die Vorbereitung wird durch den Datenbankserver oder dessen Treiber vorgenommen. Eine vorbereitete Anweisung kann schneller ausgeführt werden, da z.B. der SQL-Code nicht erneut geparst, übersetzt und optimiert werden muss. Natürlich wird es eher selten der Fall sein, dass wirklich immer die gleiche SQL-Anweisung verwendet werden soll. Der übliche Anwendungsfall für vorbereitete SQL-Anweisungen ist die Verwendung in parametrisierten Abfragen. Angenommen, Sie möchten eine Abfrage verwenden, die alle registrierten Produkte eines bestimmten Kunden ermittelt. In einem Support-Center werden diese Informationen häufig benötigt. Der Parameter der Abfrage ist damit der betreffende Kunde, der in der folgenden Anweisung als Fragezeichen dargestellt wird. SELECT ProdukteName FROM Produkte WHERE Kunde=?
Vorbereitete Anfragen werden über die Methode prepareStatement() der Klasse Connection erzeugt. Über das Interface PreparedStatement haben Sie Zugriff auf das Anweisungsobjekt. Im Gegensatz zum Erzeugen einer einfachen Anweisung müssen Sie schon beim Erstellen des Anweisungsobjekts die SQL-Anweisung angeben. PreparedStatement pStat = conn.prepareStatement("SQL-Anweisung"); pStat.executeQuery();
Parameter verwenden In einer SQL-Anweisung werden Parameter durch das Fragezeichen dargestellt. Sie können jedoch nicht die gesamte Anweisung über Parameter zusammenstellen. Stattdessen ist es nur erlaubt, Teile der Anweisung durch Parameter zu ersetzen, die Werten in einer Tabelle einer Datenbank entsprechen. Dies ist z.B. der Name eines Kunden, aber nicht der Name einer Tabelle. Nachdem Sie ein Anweisungsobjekt erzeugt haben, können Sie die Parameter durch die konkreten Werte ersetzen und die Abfrage ausführen. Beispiel PreparedStatement pStat = conn.prepareStatement( "SELECT * FROM Kunden WHERE Name=?");
Zum Setzen der Parameterwerte verfügt das Interface PreparedStatement über zahlreiche Methoden. Mit der folgende Methode setzen Sie die Werte aller Parameter zurück. void clearParameters()
Zum Setzen der Parameterwerte übergeben Sie in den folgenden Methoden einen Index und einen Wert. Der Index entspricht der Stellung des Fragezeichens in der SQL-Anweisung. Das erste Fragezeichen hat den Wert 1, das nächste den Wert 2 usw. Der zweite Parameter
878
33 – JDBC – Datenbankzugriff
legt den zu setzenden Wert fest. Es existieren noch weitere Methoden zum Setzen von Datums- oder Blob-Werten. void void void void
[IconInfo]
setBoolean(int index, boolean wert) setDouble(int index, double wert) setInt(int index, int wert) setString(int index, String wert)
In die Tabelle Artikel sollen mehrere Datensätze eingefügt werden (das Beispiel fügt aber nur einen Datensatz ein). In diesem Fall eignet sich die Verwendung einer vorbereiteten SQL-Anweisung. Die in die Tabelle einzufügenden Werte werden in der SQL-Anweisung durch ein Fragezeichen gekennzeichnet. Die Parameter werden über die entsprechenden Methoden des Interfaces PreparedStatement gesetzt und die Anweisung wird ausgeführt. Da keine Ergebnismenge geliefert wird, verwenden wir die Methode executeUpdate() zum Ausführen der Anweisung.
Listing 33.4: \Beispiele\de\j2sebuch\kap33\Parameter.java import java.sql.*; public class Parameter { public Parameter() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden"); PreparedStatement pStat = verbMySQL.prepareStatement( "INSERT INTO Artikel VALUES(?, ?)"); pStat.setInt(1, 3); pStat.setString(2, "Nägel"); pStat.executeUpdate(); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Parameter(); } }
Java 5 Programmierhandbuch
879
SQL-Anweisungen einsetzen
33.4.4
Stored Procedures verwenden
Speziell für die Einsatz von Stored Procedures wird von JDBC das Interface CallableStatement zur Verfügung gestellt. Der Vorteil der Verwendung dieses Interfaces ist, dass der Aufruf von Stored Procedures für alle Datenbanksysteme auf die gleiche Art und Weise erfolgt. Stored Procedures (gespeicherte Prozeduren) werden auf dem Datenbankserver über SQL definiert. Über sie können komplexe Operationen ausgeführt werden. Die Verwaltung durch das Datenbanksystem hat mehrere Vorteile. Die Prozeduren können bereits vorkompiliert und optimiert werden. Die Verwaltung erfolgt zentral auf dem Server, so dass Änderungen sofort für alle Clients verfügbar sind. Die Ausführungsgeschwindigkeit ist höher, unter anderem da nur wenige Daten für den Aufruf der Prozedur notwendig sind und damit über das Netzwerk übertragen werden müssen. Es gibt vielfältige Verwendungsmöglichkeiten von Stored Procedures. Sie können ihnen Parametern übergeben, die teilweise auch über Ein- und Ausgabeparameter unterschieden werden. Als Ergebnis des Aufrufs können, wie in einer SELECT-Anfrage, Ergebnismengen oder einzelne Werte zurückgegeben werden. Es ist aber auch der reine Aufruf ohne Ergebnisrückgabe möglich. Beispiel Wenn Sie einen Kunden löschen möchten, sollen außerdem alle seine Aufträge und sonstigen Daten aus der Datenbank entfernt werden. Vorher erstellen Sie noch Sicherungskopien der gelöschten Datensätze. Als einziger Parameter für diese Operationen genügt die Angabe einer eindeutigen Kundennummer durch den Client. Die SQL-Anweisungen, die zur Durchführung der genannten Operationen notwendig sind, befinden sich alle innerhalb einer Stored Procedure auf dem Server. Der Client muss also nur eine einzige Prozedur mit einem Parameter aufrufen, statt sämtliche SQL-Anweisungen einzeln auszuführen.
[IconInfo]
Stored Procedures werden nicht von allen Datenbanksystemen unterstützt. So bietet MySQL momentan keine Möglichkeit, Stored Procedures zu verwenden. Weiterhin implementiert jedes Datenbanksystem unterschiedliche Eigenschaften von Stored Procedures. Dies umfasst die Definition über SQL sowie den Aufruf der Prozeduren.
Prozeduren anlegen Zur Nutzung von Prozeduren müssen sie im ersten Schritt auf dem Datenbanksystem angelegt werden. Um in Firebird eine Prozedur anzulegen, starten Sie die Anwendung isql im ..\bin-Verzeichnis der Firebird-Installation. Die Definition einer Stored Procedure wird sich in der Regel in anderen Datenbanksystemen von der hier vorgestellten unterscheiden. Nach dem Öffnen einer Datenbank geben Sie den folgenden Quellcode hintereinander ein und bestätigen nach der Anweisung END; mit Æ. Um die Prozedurdefinition festzuschreiben, bestätigen Sie außerdem noch mit COMMIT;.
880
33 – JDBC – Datenbankzugriff
Die folgende Prozedur soll nach der Übergabe einer ID und eines Namens einen neuen Datensatz in der Tabelle Artikel anlegen. Um auf die Parameter im Prozedurkopf zuzugreifen, werden ihnen bei der Verwendung Doppelpunkte vorangesetzt. CREATE PROCEDURE NeuerArtikel(ID INTEGER, Name VARCHAR(30)) AS BEGIN INSERT INTO Artikel VALUES(:ID, :Name); END;
Eine zweite Prozedur soll eine Ergebnismenge liefern. Dazu wird ihr als Parameter eine Artikelnummer übergeben. Es sollen alle Artikel geliefert werden, die eine größere Artikelnummer als der übergebene Parameter besitzen. CREATE PROCEDURE ZeigeArtikel(ANr INTEGER) RETURNS (AID Integer, AName VARCHAR(30)) AS BEGIN FOR SELECT Id, Name FROM Artikel WHERE ID = :ANr INTO :AID, :AName DO SUSPEND; END;
Prozedur ausführen Die angelegten Prozeduren können bereits über isql getestet werden. Verwenden Sie dazu die folgenden SQL-Anweisungen und bestätigen Sie jede Zeile mit Æ. EXECUTE PROCEDURE NeuerArtikel(1, 'xxxx'); COMMIT; SELECT * FROM ZeigeArtikel(4);
Für den Aufruf dieser Prozeduren über JDBC gibt es mehrere Möglichkeiten. Die beiden dargestellten Prozeduren lassen sich auch mithilfe von Statement- und PreparedStatement-Objekten ausführen. PreparedStatement ps = verbFB.prepareStatement( "EXECUTE PROCEDURE NeuerArtikel(101, 'Seife')"); ps.executeUpdate(); Statement stmtFB = verbFB.createStatement(); ResultSet rs2 = stmtFB.executeQuery( "SELECT * FROM ZeigeArtikel(101)");
Möchten Sie den Aufruf unabhängig vom Datenbanksystem gestalten, verwenden Sie das Interface CallableStatement. Der Aufruf der Prozedur wird in einer so genannten Escape-Syntax angegeben. Der angegebene Text wird in die datenbankspezifische Syntax
Java 5 Programmierhandbuch
881
SQL-Anweisungen einsetzen
durch den JDBC-Treiber übersetzt. Auf diese Weise stehen die folgenden Aufrufe zur Verfügung (ohne Parameter, mit Parameter, mit Rückgabe einer Ergebnismenge). Beachten Sie, dass die JDBC-Treiber oft nicht alle Möglichkeiten der Escape-Syntax implementieren. {call ProzedurName} {call ProzedurName(?, ?, ...)} {? = call ProzedurName(?, ?, ...)}
Der Aufruf der Prozedur erfolgt nun in vier Schritten. Zuerst wird die Anweisung vorbereitet, dann werden die Eingabeparameter belegt, die Ausgabeparameter werden registriert und es erfolgt abschließend der Prozeduraufruf. Über die Methode prepareCall() des Verbindungsobjekts wird der Prozeduraufruf vorbereitet. Übergeben wird die entsprechende Escape-Syntax. CallableStatement cs = verb.prepareCall("{call ProzedurName(?}");
Jetzt werden die Eingabeparameter belegt. Die Methoden entsprechen denen der vorbereiteten Anweisungen des PreparedStatement-Interface. Die Parameter werden von links nach rechts mit 1 beginnend durchnummeriert. cs.setInt(1, 10); cs.setString(2, "...");
Einige Datenbanksysteme unterstützen Ausgabeparameter. Deren Typ muss vor dem Aufruf der Prozedur registriert werden. Nach dem Aufruf werden die Werte über getXXX()Methoden ausgelesen. cs.registerOutParameter(1, Types.VARCHAR); cs.execute(); cs.getString(1);
Der Aufruf der Prozedur erfolgt über die Methode execute(). cs.execute();
[IconInfo]
882
Zur Ausführung des Beispiels wird Firebird benötigt und Sie müssen die bereits vorgestellten Stored Procedures erfolgreich in Firebird angelegt haben. Die Stored Procedure NeuerArtikel wird einmal mit einem CallableStatement und einem PreparedStatement aufgerufen. Die Prozedur ZeigeArtikel liefert eine Ergebnismenge zurück, deshalb kann sie wie eine Tabelle in einer SELECT-Anweisung verwendet werden.
33 – JDBC – Datenbankzugriff Listing 33.5: \Beispiele\de\j2sebuch\kap33\StoredProcedures.java import java.sql.*; public class StoredProcedures { public StoredProcedures() { try { Class.forName("org.firebirdsql.jdbc.FBDriver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbFB = DriverManager.getConnection( "jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb", "SYSDBA", "masterkey"); CallableStatement csFB = verbFB.prepareCall( "{call NeuerArtikel(?, ?)}"); csFB.setInt(1, 102); csFB.setString(2, "Seife"); csFB.execute(); PreparedStatement ps = verbFB.prepareStatement( "EXECUTE PROCEDURE NeuerArtikel(103, 'Handschuhe')"); ps.executeUpdate(); Statement stmtFB = verbFB.createStatement(); ResultSet rs2 = stmtFB.executeQuery( "SELECT * FROM ZeigeArtikel(102)"); while(rs2.next()) System.out.println(rs2.getString(2)); } catch(SQLException sqlEx) { System.out.println(sqlEx.getMessage()); } } public static void main(String args[]) { new StoredProcedures(); } }
Java 5 Programmierhandbuch
883
SQL-Anweisungen einsetzen
33.4.5
Batch-Mode
Innerhalb einer Batch-Update-Anweisung können Sie mehrere Aufrufe von executeUpdate() in einem Methodenaufruf zusammenfassen. Dies umfasst alle SQL-Anweisungen, die einen Wert der geänderten Datensätze zurückliefern (keine Ergebnismenge). Je nach verwendeter Datenbank können dadurch Geschwindigkeitsvorteile erreicht und die Transaktionsverwaltung vereinfacht werden (siehe später). Zur Verwendung von Batch-Updates gehen Sie wie folgt vor. Zuerst fügen Sie alle SQL-Anweisungen über die Methode addBatch() dem Anweisungsobjekt hinzu. Danach führen Sie alle Anweisungen über einen Methodenaufruf aus. Im Anschluss lässt sich überprüfen, ob alle Anweisungen erfolgreich ausgeführt wurden. Optional können Sie die automatische Transaktionsverwaltung deaktivieren, um im Fehlerfall alle Anweisungen wieder rückgängig zu machen. Über die Methode addBatch() fügen Sie eine SQL-Anweisung in die Batch-Verarbeitung ein. Die Anweisungen werden in derselben Reihenfolge ausgeführt, in der sie über addBatch() eingefügt wurden. void addBatch(String sqlAnweisung)
Zum Entfernen aller SQL-Anweisungen aus der Batch-Verarbeitung rufen Sie die Methode clearBatch() auf. void clearBatch()
Wenn die Batch-Verarbeitung über die Methode executeBatch() gestartet wurde, erhalten Sie in einem Array vom Typ int das Ergebnis der einzelnen Anweisungen, z.B. die Anzahl der geänderten Datensätze durch eine UPDATE-Anweisung. Die Länge des Arrays entspricht der Anzahl der über addBatch() hinzugefügten SQL-Anweisungen. int[] executeBatch()
Beim Ausführen von Batch-Updates können zwei Exceptions auftreten. Haben Sie eine SQL-Anweisung hinzugefügt, die eine Ergebnismenge liefert, wird eine SQLException ausgelöst. Im Falle einer BatchUpdateException konnte eine SQL-Anweisung nicht erfolgreich abgeschlossen werden. Es wurden aber dennoch alle SQL-Anweisungen ausgeführt. Über die Methode getUpdateCount() dieser Exception erhalten Sie wie auch bei der Methode executeBatch() ein Array, welches Informationen bezüglich des Erfolgs der Ausführung der SQL-Anweisungen enthält (0 – z.B. beim Erstellen von Tabellen, >0 – Anzahl der betroffenen Datensätze, Konstante Statement.EXECUTE_FAILED – bei einer fehlerhaften Ausführung). int[] getUpdateCounts()
884
33 – JDBC – Datenbankzugriff
[IconInfo]
Über eine Batch-Anweisung sollen die Tabelle Artikel erstellt und vier Datensätze eingefügt werden. Nachdem die SQL-Anweisungen über die Methode addBatch() gesammelt wurden, werden sie über executeBatch() ausgeführt. Da die Tabelle Artikel über einen Primärschlüssel verfügt, dürfen nicht zwei Datensätze mit gleicher ID eingefügt werden. Deshalb wird die dritte Anweisung zum Einfügen des Artikels Muttern nicht ausgeführt. Dies lässt sich mithilfe der Methode getUpdateCounts() und Auswertung der Rückgabewerte prüfen. Die Erstellung der Tabelle liefert übrigens den Wert 0, da keine Datensätze geändert wurden.
Listing 33.6: \Beispiele\de\j2sebuch\kap33\BatchMode.java import java.sql.*; public class BatchMode { public BatchMode() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root"); Statement stmtMySQL = verbMySQL.createStatement(); stmtMySQL.addBatch("CREATE TABLE Artikel(ID INTEGER, Name"+ " VARCHAR(30), PRIMARY KEY(ID))"); stmtMySQL.addBatch( "INSERT INTO Artikel VALUES(1, 'Schrauben')"); stmtMySQL.addBatch( "INSERT INTO Artikel VALUES(1, 'Muttern')"); stmtMySQL.addBatch( "INSERT INTO Artikel VALUES(2, 'Nägel')"); stmtMySQL.addBatch( "INSERT INTO Artikel VALUES(3, 'Dübel')"); try { int resultate[] = stmtMySQL.executeBatch(); } catch(BatchUpdateException buEx) { System.out.println("Batch-Fehler"); int resultate[] = buEx.getUpdateCounts();
Java 5 Programmierhandbuch
885
Zugriff auf die Ergebnismengen Listing 33.6: \Beispiele\de\j2sebuch\kap33\BatchMode.java (Forts.) for(int anzahl: resultate) { if(anzahl == Statement.EXECUTE_FAILED) System.out.println("Fehler"); else System.out.println(anzahl); } } } catch(SQLException sqlEx) { System.out.println("SQL-Fehler"); } } public static void main(String args[]) { new BatchMode(); } }
33.5 Zugriff auf die Ergebnismengen 33.5.1
Einführung
Das Ergebnis einer SELECT-Abfrage ist entweder ein einzelner Wert oder eine Menge von Datensätzen, die Ergebnismenge. Wenn Sie eine SQL-Anweisung über die Methode executeQuery() des Interfaces Statement ausführen, wird ein Objekt vom Typ ResultSet zurückgegeben. Darüber haben Sie Zugriff auf die abgefragten Daten. Die Ergebnismenge besteht aus Zeilen und Spalten. Eine Zeile kennzeichnet alle Eigenschaften eines Datensatzes einer Tabelle, z.B. die Eigenschaften eines bestimmten Kunden. Eine Spalte dagegen liefert ein bestimmtes Merkmal der ausgewählten Datensätze, z.B. die Namen aller Kunden. Abhängig vom verwendeten Datenbanksystem können Sie folgende Operationen mit einer Ergebnismenge durchführen: 쐌 쐌 쐌 쐌
Vor der Ausführung einer Abfrage können Sie deren Eigenschaften konfigurieren. Sie können vorwärts und rückwärts navigieren, z.B. zum zehnten Datensatz. Über verschiedene Methoden können die Daten gelesen werden. Die Ergebnismenge kann bearbeitet werden, ohne dass Änderungen in der zugrunde liegenden Datenbank stattfinden. 쐌 Änderungen können in die Datenbank zurückgeschrieben werden. Wie auch mit Anweisungen ist die Arbeit mit Ergebnismengen sehr speicherintensiv. Geben Sie deshalb nicht mehr benötigte ResultSetObjekte manuell über die Methode close() frei. [IconInfo]
886
33 – JDBC – Datenbankzugriff
33.5.2
Werte auslesen
Nach dem Ausführen einer SQL-Anweisung wird ein ResultSet-Objekt zurückgegeben. Ein so genannter Cursor (Datensatzzeiger) steht danach vor dem ersten Datensatz. Beachten Sie, dass für ein ResultSet-Objekt auf diese Weise nie ein null-Wert zurückgegeben wird. ResultSet rs = stmt.executeQuery("SELECT * FROM Artikel");
Im nächsten Schritt durchlaufen Sie die Ergebnismenge. Dazu verwenden Sie die Methode next() des Interfaces ResultSet. Ist die Ergebnismenge leer bzw. kein Datensatz mehr vorhanden, wird von der Methode der Wert false zurückgegeben. while(rs.next()) { }
Befinden Sie sich auf einem Datensatz und kennen Sie dessen Aufbau, können Sie die Werte der einzelnen Felder auslesen. Dazu bietet das Interface ResultSet zahlreiche getXXX()-Methoden an. Der Zugriff auf eine Spalte erfolgt über einen Index, der von 1 bis n läuft, oder über den Spaltennamen. In der Regel ist der Zugriff über einen Index schneller. Durch die Angabe des Spaltennamens ist der Programmcode besser lesbar. Außerdem, und das ist der wichtigste Unterschied, sind Sie von der tatsächlichen Spaltenposition unabhängig. Sie könnte sich ja im Laufe der Zeit ändern. Im Folgenden werden einige der Methoden gezeigt. boolean getBoolean(int spaltenIndex) boolean getBoolean(String spaltenName) int getInt(int spaltenIndex) int getInt(String spaltenName) String getString(int spaltenIndex) String getString(String spaltenName)
Jetzt haben Sie alle Voraussetzungen geschaffen, um den Inhalt der Tabelle Artikel der verwendeten Beispieldatenbank auf der Konsole auszugeben.
[IconInfo]
Auf der Konsole werden alle Datensätze der Tabelle Artikel ausgegeben. Durch die SELECT-Anweisung werden zwei konkrete Spalten selektiert, deren Inhalte später über die getXXX()-Methoden des ResultSet datensatzweise ausgelesen werden. Die Methode next() liefert so lange den Rückgabewert true, bis kein Datensatz mehr verfügbar ist.
Java 5 Programmierhandbuch
887
Zugriff auf die Ergebnismengen Listing 33.7: \Beispiele\de\j2sebuch\kap33\Ergebnismenge.java import java.sql.*; public class Ergebnismenge { public Ergebnismenge() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root"); Statement stmtMySQL = verbMySQL.createStatement(); ResultSet rsMySQL = stmtMySQL.executeQuery( "SELECT ID, Name FROM Artikel"); while(rsMySQL.next()) System.out.println(rsMySQL.getInt(1) + ":" + rsMySQL.getString(2)); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Ergebnismenge(); } }
[IconInfo]
Kennen Sie nicht den Aufbau der Ergebnismenge, lässt sich deren Aufbau über einen Aufruf der Methode getMetaData() und des davon zurückgegebenen ResultSetMetaData-Objekts bestimmen. Die Vorgehensweise finden Sie in diesem Kapitel im Abschnitt »Zugriff auf Metadaten einer Datenbank«.
Nullwerte Wenn Sie einen Datensatz in einer Datenbank anlegen, müssen Sie für bestimmte Werte keine Angaben machen. Der Inhalt des betreffenden Eintrags ist dann nicht belegt. Zur Kennzeichnung dieser Werte verwenden Datenbanksysteme einen speziellen Null-Wert, der aber weder etwas mit der Zahl 0 noch mit dem Wert null eines Java-Objekts zu tun hat.
888
33 – JDBC – Datenbankzugriff
Eine Zeichenkette kann beispielsweise einen leeren Text enthalten "". Um zu kennzeichnen, dass sie gar keinen Inhalt besitzt, kommt wieder der Null-Wert ins Spiel. Der NullWert ist also ein symbolischer Wert für einen nicht belegten Eintrag. Zur Überprüfung, ob ein Feld eines Datensatzes der Ergebnismenge den Wert Null besitzt, verwenden Sie die Methode wasNull() des ResultSet-Objekts. Sie wird direkt nach dem Abfragen eines bestimmten Werts aufgerufen. String name = rs.getString(1); if(rs.wasNull()) ...
Die getXXX()-Methoden liefern bei Null-Feldern verschiedene Werte, abhängig vom Datentyp des Felds. Handelt es sich um boolean-Werte, wird false geliefert, 0 bei Zahlen und null bei Objekten wie String oder Date.
33.5.3
Navigation
Über die Methode next() können Sie sich nur einmal in einer bestimmten Reihenfolge durch die Ergebnismenge bewegen. Das Interface ResultSet bietet aber noch andere Methoden, um den Cursor auf beliebige Datensätze zu positionieren. Je nach der Konfiguration der Ergebnismenge oder den Möglichkeiten des verwendeten Datenbanksystems kann die Ausführung einiger Methoden nicht möglich sein. So ist es beispielsweise nicht immer möglich, eine Ergebnismenge rückwärts zu durchlaufen, da dies erhöhte Anforderungen an das Datenbanksystem und den JDBC-Treiber stellt. In diesem Fall können Sie auf RowSets ausweichen. Eine Implementierung für JDBC finden Sie im Interface javax.sql.rowset.JdbcRowSet. Das Interface ResultSet stellt die folgenden Methoden zur Navigation bereit. Nur die Methoden, deren Ausführung nicht wie gewünscht erfolgen kann, liefern einen Rückgabewert vom Typ boolean. So können Sie z.B. nicht den nächsten Datensatz auswählen, wenn die Ergebnismenge leer ist. Andererseits ist es immer möglich, den Cursor vor den ersten oder hinter den letzten Datensatz zu bewegen. Die Methode absolute() positioniert den Datensatzzeiger auf der als Parameter übergebenen Zeile, während die Methode relative() den Zeiger, ausgehend von der aktuellen Position, um die angegebene Zeilenzahl verschiebt. Der Index läuft von 1 bis n. Negative Positionsangaben gehen vom letzten Datensatz aus. boolean absolute(int zeile) boolean relative(int zeilen)
Mit den nächsten beiden Methoden bewegen Sie den Datensatzzeiger hinter den letzten bzw. vor den ersten Datensatz. void afterLast() void beforeFirst()
Java 5 Programmierhandbuch
889
Zugriff auf die Ergebnismengen
Die folgenden Methoden bewegen den Cursor zum ersten, letzten, nächsten und vorigen Datensatz. boolean boolean boolean boolean
first() last() next() previous()
Für die Ermittlung der aktuellen Position innerhalb der Ergebnismenge verwenden Sie die Methode getRow(). Die Zeilennummern laufen von 1 bis n. int getRow()
Die folgenden Methoden eignen sich zur Überprüfung, ob sich der Datensatzzeiger hinter dem letzten, vor dem ersten, auf dem ersten oder auf dem letzten Datensatz befindet. boolean boolean boolean boolean
33.5.4
isAfterLast() isBeforeFirst() isFirst() isLast()
Konfiguration
Die Methoden createStatement(), prepareStatement() und prepareCall() des Interfaces Connection zum Erzeugen von Anweisungsobjekten besitzen zwei weitere Varianten, denen zusätzliche Parameter übergeben werden können. Diese sollen am Beispiel der Methode createStatement() erläutert werden. Ob der jeweilige Parameter vom JDBC-Treiber unterstützt wird, lässt sich über die Methoden supportsResultSetType(), supportsResultSetConcurrency() und supportsResultSetHoldability() des Interfaces DatabaseMetaData bestimmen. Statement createStatement(int int Statement createStatement(int int int
resultSetType, resultSetConcurrency) resultSetType, resultSetConcurrency, resultSetHoldability)
Über den Parameter resultSetType legen Sie fest, wie Sie sich in der Ergebnismenge bewegen können und ob die Ergebnismenge Änderungen in der Datenbank reflektiert. Die zu verwendenden Konstanten befinden sich im Interface ResultSet. Unterstützt der JDBCTreiber eine Einstellung nicht, wird eine SQL-Warnung erzeugt.
890
33 – JDBC – Datenbankzugriff Konstante
Erläuterung
TYPE_FORWARD_ONLY
Dies ist die Standardeinstellung. Der Cursor kann nur vorwärts bewegt werden, was nicht so ressourcenintensiv ist.
TYPE_SCROLL_INSENSITIVE
Der Cursor kann in beide Richtungen bewegt werden. Die Ergebnismenge wird bei Änderungen in der Datenbank nicht aktualisiert.
TYPE_SCROLL_SENSITIVE
Der Cursor kann in beide Richtungen bewegt werden. Die Ergebnismenge wird bei Änderungen in der Datenbank aktualisiert.
Über den Parameter resultSetConcurrency können Sie festlegen, ob die Ergebnismenge bearbeitbar ist (ResultSet.CONCUR_READ_ONLY – nein, dies ist die Standardeinstellung, ResultSet.CONCUR_UPDATABLE – ja). Mit einem letzten Parameter resultSetHoldability können Sie das Verhalten der Ergebnismenge bei einem Commit steuern. Verwenden Sie die Konstante ResultSet.HOLD_CURSORS_OVER_COMMIT, um die Ergebnismenge auch nach einem Commit geöffnet zu halten, sonst ResultSet.CLOSE_CURSORS_AT_COMMIT. Über die folgenden Methoden können Sie den Wert für die Bearbeitungsfähigkeit der Ergebnismenge, den Typ der Ergebnismenge und die Durchlaufrichtung ermitteln oder setzen. Werden beim Durchlaufen einer Ergebnismenge neue Datensätze benötigt, kann über die beiden letzten Methoden die Menge der nachzuladenden Datensätze bestimmt oder gesetzt werden. int getConcurrency() int getType() int getFetchDirection() void setFetchDirection() int getFetchSize() void setFetchSize()
33.5.5
Werte ändern und zurückschreiben
Einige JDBC-Treiber und Datenbanksysteme erlauben das Ändern, Einfügen und Löschen von Werten innerhalb der Ergebnismenge und die Übernahme dieser Änderungen in die Datenbank. MySQL verlangt dazu, dass ein Primärschlüssel für die betreffenden Tabellen existiert. Firebird unterstützt diese Funktionalität momentan nicht. Um für ein DBMS zu prüfen, ob Änderungen in der Ergebnismenge möglich sind, können Sie das Connection-Objekt nutzen. Speziell für eine Ergebnismenge ist das ResultSetObjekt zu verwenden. In jedem Fall muss die Eigenschaft CONCUR_UPDATABLE unterstützt werden.
Java 5 Programmierhandbuch
891
Zugriff auf die Ergebnismengen Connection verbMySQL = DriverManager.getConnection("..."); ResultSet rsMySQL = ... ... DatabaseMetaData dmd = verbMySQL.getMetaData(); if(dmd.supportsResultSetConcurrency( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) System.out.println("MySQL unterstützt Updates"); if(rsMySQL.getConcurrency() == ResultSet.CONCUR_UPDATABLE) System.out.println("MySQL unterstützt Updates");
Beim Erstellen des Anweisungsobjekts ist die erweiterte Form zu nutzen, um die Ergebnismenge bearbeitbar zu machen, z.B. Statement stmtMySQL = verbMySQL.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
Änderungsoperationen werden durch die folgenden Methoden des Interfaces ResultSet realisiert. Möchten Sie die Änderungen rückgängig machen, können Sie die Methode cancelRowUpdates() nach den updateXXX()-Methoden, aber noch vor dem Aufruf von updateRow() oder insertRow() ausführen. void cancelRowUpdates()
Zum Löschen des aktuellen Datensatzes rufen Sie die folgende Methode auf. void deleteRow()
Der vorher über die Methode moveToInsertRow() und Aufrufe von updateXXX()-Methoden neu angelegte Datensatz wird in die Datenbank übertragen. void insertRow()
Die erste Methode setzt den Datensatzzeiger nach einer Einfügeoperation wieder an die vorher gültige Position. Über die zweite Methode fügen Sie einen neuen Datensatz ein und setzen den Datensatzzeiger darauf. Jetzt können die Werte des Datensatzes über die Methoden updateXXX() festgelegt werden. Die Einfügeoperation muss über den Aufruf von insertRow() abgeschlossen werden. void moveToCurrentRow() void moveToInsertRow()
892
33 – JDBC – Datenbankzugriff
Um den Wert einer Spalte bei Änderungen oder beim Einfügen von Datensätzen zu bearbeiten, werden für zahlreiche Datentypen Methoden bereitgestellt, die mit dem Präfix update und dem Datentypnamen bezeichnet werden, z.B. updateInt(). Diesen updateXXX()-Methoden wird im ersten Parameter entweder der Spaltenindex oder der Spaltenname übergeben. Der Spaltenindex bezieht sich hier immer auf die Position der Spalte im ResultSet. Bei den Spaltennamen wird keine Groß-/Kleinschreibung berücksichtigt. Der zweite Parameter ist vom entsprechenden Datentyp der Spalte, deren Inhalt geändert werden soll. void updateInt(int columnIndex, int x) void updateInt(String columnName, int x)
// speziell für int // speziell für int
Die durchgeführten Änderungen über die Methoden updateXXX() werden in die Datenbank übertragen. Die Methode muss aufgerufen werden, solange Sie sich auf dem betreffenden Datensatz befinden. void updateRow()
Änderungen werden durch zwei Schritte am aktuellen Datensatz durchgeführt. Zuerst werden die Werte über die updateXXX()-Methoden gesetzt. Danach werden die Änderungen über die Methode updateRow() in die Datenbank übertragen. Über die Methode deleteRow() löschen Sie immer den aktuellen Datensatz. Das Einfügen von Datensätzen erfolgt wieder über mehrere Schritte. Zuerst wird über moveToInsertRow() der Datensatzzeiger auf einen speziellen Datensatz gesetzt, der nur zum Einfügen neuer Datensätze verwendet wird. Werden jetzt die updateXXX()-Metho-
den aufgerufen, setzen Sie die Daten des neuen Datensatzes. Erst nach dem Aufruf von insertRow() wird der Datensatz endgültig übernommen.
[IconInfo]
Zuerst werden alle Datensätze der Tabelle Artikel der MySQL-Datenbank Kunden ermittelt und als Ergebnismenge zurückgegeben. Danach wird geprüft, ob die Datenbank und die Ergebnismenge, die Änderung der Ergebnismenge und das Rückschreiben der Änderungen in die Datenbank erlaubt sind. Dazu wird für die Datenbank das Interface DatabaseMetaData und die Methode supportsResultSetConcurrency() verwendet, für die Ergebnismenge die Methode getConcurrency(). Im Beispiel wird davon ausgegangen, dass Änderungen möglich sind. Deshalb wird ein neuer Datensatz über moveToInsertRow() angelegt und mit den entsprechenden Werten gefüllt. Die Methode insertRow() überträgt die Daten in die Datenbank. Nach dem Einfügen wird der Datensatzzeiger mit moveToCurrentRow() wieder an die alte Position gesetzt.
Java 5 Programmierhandbuch
893
Zugriff auf die Ergebnismengen Listing 33.8: \Beispiele\de\j2sebuch\kap33\Aenderungen.java import java.sql.*; public class Aenderungen { public Aenderungen() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root"); Statement stmtMySQL = verbMySQL.createStatement( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE); ResultSet rsMySQL = stmtMySQL.executeQuery( "SELECT ID, Name FROM Artikel"); DatabaseMetaData dmd = verbMySQL.getMetaData(); if(dmd.supportsResultSetConcurrency( ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE)) System.out.println("MySQL unterstützt Updates"); if(rsMySQL.getConcurrency() == ResultSet.CONCUR_UPDATABLE) System.out.println("MySQL unterstützt Updates"); rsMySQL.moveToInsertRow(); rsMySQL.updateInt(1, 1001); rsMySQL.updateString("Name", "Waschlappen"); rsMySQL.insertRow(); rsMySQL.moveToCurrentRow(); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Aenderungen(); } }
894
33 – JDBC – Datenbankzugriff
Das Einfügen, Ändern und Löschen von Werten kann unabhängig von einer Ergebnismenge durch entsprechende SQL-Anweisungen wie INSERT, UPDATE oder DELETE durchgeführt werden. [IconInfo]
33.6 Transaktionsverwaltung 33.6.1
Einführung
Zur Sicherstellung der Datenintegrität und einer konsistenten Sicht auf eine Datenbank werden Transaktionen verwendet. Sie werden dazu eingesetzt, dass entweder alle oder keine Operationen einer Operationsfolge durchgeführt werden. Das hierfür üblicherweise herangezogene Beispiel ist eine Kontobewegung. Sie besteht in der Regel aus mindestens zwei Operationen. Von einem Konto wird abgebucht und der Betrag wird auf ein anderes Konto gutgeschrieben. Es macht für diese beiden Operationen keinen Sinn, wenn nur eine erfolgreich abgeschlossen wird. So kann die Abbuchung erfolgreich sein. Existiert aber das Zielkonto nicht, z.B. durch einen Schreibfehler auf dem Überweisungsformular, kann der Betrag nicht gutgeschrieben werden. Schade, Sie haben wahrscheinlich gerade die Bank glücklich gemacht. Noch schlechter sieht es aus, wenn niemand den Fehler bemerkt. In diesem Fall arbeitet die Bank mit inkonsistenten Daten, da ein Fehlbetrag besteht. Transaktionen arbeiten immer nach dem ACID-Prinzip. Es beschreibt auf kurze Weise die Eigenschaften einer Transaktion. Eigenschaft
Erläuterung
Atomicity (Atomarität)
Die in einer Transaktion durchgeführten Operationen werden als atomar, d.h. als unteilbar, aufgefasst. Sie werden alle ausgeführt oder keine.
Consistency (Konsistenz)
Die Konsistenz der Datenbank wird durch die Transaktion nicht gefährdet.
Isolation (Isolation)
Im Mehrbenutzerbetrieb wird sichergestellt, dass die Transaktionen keinen Einfluss aufeinander haben. Jede Transaktion agiert, als ob sie die einzige Transaktion im System ist.
Durability (Dauerhaftigkeit)
Wurde eine Transaktion erfolgreich beendet, wird sichergestellt, dass die durchgeführten Operationen dauerhaft in der Datenbank bleiben, unabhängig davon, ob ein Softwarefehler oder andere Probleme aufgetreten sind.
Java 5 Programmierhandbuch
895
Transaktionsverwaltung
33.6.2
Transaktionen unter JDBC
Standardmäßig werden die von Ihnen ausgeführten SQL-Anweisungen im AutoCommitModus ausgeführt. Wurde eine SQL-Anweisung erfolgreich ausgeführt, sind ihre Ergebnisse in der Datenbank festgeschrieben. Kurz gesagt wurde die SQL-Anweisung als einzige Anweisung einer erfolgreichen Transaktion ausgeführt. Um Transaktionen sinnvoll zu nutzen, müssen Sie zuerst den AutoCommit-Modus deaktivieren. Das Interface Connection ist innerhalb einer Datenbankverbindung für die Steuerung der Transaktionsverwaltung zuständig. Befinden Sie sich nicht im AutoCommit-Modus, müssen Sie eine oder mehrere SQL-Anweisungen immer manuell bestätigen. Erst nach einem Commit werden die Anweisungen innerhalb der Datenbank festgeschrieben. Ansonsten werden die Änderungen verworfen. void commit()
Es wird der aktuelle Zustand des AutoCommit-Modus zurückgegeben. Ist er aktiviert, erhalten Sie den Rückgabewert true. boolean getAutoCommit()
Um alle Operationen einer Transaktion zurückzusetzen, führen Sie ein Rollback durch. Ein Rollback sollte auch dann durchgeführt werden, wenn innerhalb einer Transaktion eine Exception aufgetreten ist. void rollback()
Zur Aktivierung des manuellen Transaktionsmodus übergeben Sie der Methode setAutoCommit() den Wert false. void setAutoCommit(boolean wert)
Die Datenbank selbst nutzt Sperrmechanismen zur Sicherstellung von Transaktionen.
[IconInfo]
896
MySQL unterstützt derzeit keine Transaktionen, deshalb kommt der Firebird-Server zum Einsatz. Es wird zuerst der AutoCommit-Modus deaktiviert und damit die manuelle Transaktionsverwaltung eingeschaltet. Nachdem zwei Datensätze in die Tabelle Artikel eingefügt wurden, werden diese Änderungen über den Aufruf von rollback() wieder zurückgenommen. Die folgenden Anwendungen werden über commit() bestätigt und befinden sich damit sicher in der Datenbank. Anschließend wird der AutoCommit-Modus und damit die automatische Transaktionsverwaltung wieder aktiviert. Sie können das Ergebnis z.B. über isql überprüfen.
33 – JDBC – Datenbankzugriff Listing 33.9: \Beispiele\de\j2sebuch\kap33\Transaktionen.java import java.sql.*; public class Transaktionen { public Transaktionen() { try { Class.forName("org.firebirdsql.jdbc.FBDriver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbFB = DriverManager.getConnection( "jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb", "SYSDBA", "masterkey"); verbFB.setAutoCommit(false); Statement stmtFB = verbFB.createStatement(); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(10,'A1')"); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(11,'A2')"); verbFB.rollback(); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(12,'A3')"); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(13,'A4')"); verbFB.commit(); verbFB.setAutoCommit(true); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Transaktionen(); } }
33.6.3
Isolationsstufen
Über Isolationsstufen wird festgelegt, wie der parallele Zugriff von SQL-Anweisungen verschiedener Transaktionen auf dieselben Werte erfolgt. Wenn beispielsweise eine Transaktion einen Wert 100 von A nach B bucht, werden zwei Operationen ausgeführt. Zuerst wird der Wert 100 von A abgezogen, danach B hinzugefügt. Wird nun parallel eine zweite Transaktion gestartet, welche die beiden Werte von A und B liest, kann es passieren, dass der Wert 100 von A abgezogen, aber noch nicht B hinzugefügt wurde.
Java 5 Programmierhandbuch
897
Transaktionsverwaltung
Die Frage ist, wie man diesem Problem begegnet. Im einfachsten Fall sperrt man alle Datensätze, die eine Transaktion bearbeitet. Dies ist nicht nur mit einem hohen Aufwand verbunden. Es werden auch alle anderen Transaktionen, die diese Datensätze bearbeiten, aufgehalten und die Implementierung ist aufwändig. Es müssen nämlich vor der Ausführung der Transaktion erst einmal alle zu bearbeitenden Datensätze bestimmt werden. Aus diesen Gründen kann man über verschiedene Isolationsstufen steuern (also wie Transaktionen voneinander abgeschottet werden), wie Datensätze gesperrt werden sollen. Greifen mehrere Transaktionen auf denselben Wert zu, können die folgenden Situationen entstehen. Fachbegriff
Erläuterung
dirty read (d)
Transaktionen können Werte anderer Transaktionen lesen, die noch nicht mit Commit bestätigt wurden. Werden die Änderungen über ein Rollback rückgängig gemacht, werden inkonsistente Daten verwendet.
non repeatable read (n)
Nachdem eine Transaktion einen Wert gelesen hat, wird dieser von einer anderer Transaktion geändert. Ein erneutes Lesen des Werts führt zu einem anderen Ergebnis.
phantom read (p)
Eine Transaktion verwendet Daten, die so nicht mehr in der Datenbank existieren, da sie bereits durch eine weitere Transaktion geändert oder gelöscht wurden.
Um eine Isolationsstufe in JDBC zu setzen, benutzen Sie eine der Konstanten des Interfaces Connection. In der zweiten Spalte der Tabelle werden in Klammern die möglichen Situationen angegeben, die dabei auftreten können. Konstante
Erläuterung
TRANSACTION_NONE
Transaktionen werden nicht unterstützt (-).
TRANSACTION_READ_COMMITTED
Es können nur Werte anderer Transaktionen gelesen werden, die mit Commit bestätigt wurden (n, p).
TRANSACTION_READ_UNCOMMITTED
Transaktionen können Werte anderer Transaktionen lesen, die noch nicht mit Commit bestätigt oder über ein Rollback zurückgesetzt wurden (d, n, p).
TRANSACTION_REPEATABLE_READ
Über diese Stufe wird gesichert, dass eine Transaktion auch beim wiederholten Lesen des gleichen Werts immer dasselbe Ergebnis erhält (p).
898
33 – JDBC – Datenbankzugriff Konstante
Erläuterung
TRANSACTION_SERIALIZABLE
Diese Stufe stellt die sicherste dar. Es muss technisch sichergestellt werden, dass immer nur eine Transaktion ausgeführt wird. Durch die Serialisierung werden Transaktionen in eine Reihenfolge gebracht und hintereinander ausgeführt. Dadurch kann es keine Überschneidungen geben (-).
Die Isolationsstufe für Transaktionen wird ebenfalls durch Methoden des Verbindungsobjekts vom Typ Connection durchgeführt. Beachten Sie, dass ein JDBC-Treiber bzw. das betreffende Datenbanksystem nicht alle bzw. überhaupt keine Isolationsstufen unterstützen muss. Zur Festlegung einer Isolationsstufe übergeben Sie der folgenden Methode eine der angegebenen Konstanten des Interfaces Connection. Ein JDBC-Treiber verwendet standardmäßig bereits einen geeigneten Wert. void setTransactionIsolation(int type)
Für die Ermittlung der aktuelle Isolationsstufe rufen Sie die folgende Methode auf. int getTransactionIsolation()
33.6.4
Sicherungspunkte
Gerade in umfangreichen Transaktionen ist es ärgerlich und zeitaufwändig, wenn bereits zahlreiche SQL-Anweisungen erfolgreich ausgeführt wurden und dann ein Problem auftritt. Damit man nicht mehr die gesamte Transaktion rückgängig machen muss, können Sicherungspunkte gesetzt werden. Eine komplexe Transaktion wird dadurch in mehrere Abschnitte aufgeteilt. Jetzt sind Rollbacks zu diesen Sicherungspunkten möglich. Auf diese Weise können Sie beispielsweise die SQL-Anweisungen, die ein Problem verursacht haben, erneut ausführen oder die Anweisungen verändern. Beginn einer Transaktion
1. Sicherungspunkt
2. Sicherungspunkt
Problem
Transaktionsende
Rollback zum 2. Sicherungspunkt
Abb. 33.4: Sicherungspunkte in Transaktionen
Sicherungspunkte werden über die folgenden Methoden des Interfaces Connection unterstützt. Ein Sicherungspunkt wird über ein Savepoint-Objekt erzeugt. Nach der Beendigung einer Transaktion werden die Sicherungspunkte automatisch freigegeben. Über die Methode releaseSavePoint() können Sie selbst einen Sicherungspunkt freigeben.
Java 5 Programmierhandbuch
899
Transaktionsverwaltung void releaseSavepoint(Savepoint sp)
Innerhalb einer Transaktion setzen Sie über die beiden folgenden Methoden einen benannten oder unbenannten Sicherungspunkt. Savepoint setSavePoint() Savepoint setSavePoint(String name)
Um ein Rollback zu einem Sicherungspunkt durchzuführen, wird der Methode rollback() ein Savepoint-Objekt übergeben. void rollback(Savepoint sp)
[IconInfo]
Das Transaktionsbeispiel des Listings 33.9 wird nun leicht verändert. Nach dem Einfügen des ersten Datensatzes wird ein Sicherungspunkt über die Methode setSavepoint() gesetzt. Nach dem Einfügen des zweiten Datensatzes wird ein Rollback zum ersten Sicherungspunkt durchgeführt, so dass der zweite Datensatz nicht in die Tabelle Artikel übernommen wird. Tritt eine Exception auf, sollen alle Änderungen unabhängig von den Sicherungspunkten rückgängig gemacht werden. Da die Methode rollback() ebenfalls eine Exception auslösen kann, ist ein weiterer Exception-Block notwendig.
Listing 33.10: \Beispiele\de\j2sebuch\kap33\Sicherungspunkte.java Connection verbFB = null; try { verbFB = DriverManager.getConnection( "jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb", "SYSDBA", "masterkey"); verbFB.setAutoCommit(false); Statement stmtFB = verbFB.createStatement(); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(10, 'A1')"); Savepoint sp1 = verbFB.setSavepoint(); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(11, 'A2')"); verbFB.rollback(sp1); stmtFB.executeUpdate("INSERT INTO Artikel VALUES(12, 'A2')"); verbFB.commit(); verbFB.setAutoCommit(true); } catch(SQLException sqlEx) { try { verbFB.rollback(); }
900
33 – JDBC – Datenbankzugriff Listing 33.10: \Beispiele\de\j2sebuch\kap33\Sicherungspunkte.java (Forts.) catch(SQLException sqlEx2) { } }
33.7 Zugriff auf Metadaten einer Datenbank Jedes Datenbanksystem unterstützt eine bestimmte Menge an SQL-Anweisungen und besitzt eine gewisse Funktionalität. Wenn Sie mit einer Datenbank arbeiten, können Sie Ihre Anwendung auf deren Möglichkeiten anpassen. Eventuell sind auch die verwendeten Tabellen sowie deren Aufbau bekannt. Wenn Sie aber zur Laufzeit einer Anwendung Informationen zu einem Datenbanksystem, zu den vorhandenen Tabellen oder dem Aufbau einer Ergebnismenge benötigen, unterstützen Sie dabei drei Interfaces. Die genannten Informationen werden auch als Metadaten bezeichnet. Beispiel Sie möchten einen Datenbankbrowser entwickeln. Nach der Herstellung einer Verbindung zu einer Datenbank soll er alle vorhandenen Tabellen, Abfragen und Sichten auflisten. Nach der Auswahl einer Tabelle sollen deren Datensätze angezeigt werden. Interface
Erläuterung
DatabaseMetaData
Sie erhalten Informationen über die Eigenschaften des Datenbanksystems und den eingesetzten JDBC-Treiber.
ParameterMetaData
Hiermit werden Informationen zu den Parametern verfügbar gemacht, die in einem PreparedStatement-Objekt verwendet werden.
ResultSetMetaData
Informationen über den Aufbau einer Ergebnismenge liefern die Methoden dieses Interfaces.
Um ein Objekt vom Typ der genannten Interfaces zu erhalten, besitzen die entsprechenden Objekte spezielle Methoden. Connection verb; PreparedStatement ps; ResultSet rs; ... DatabaseMetaData dmd = verb.getMetaData(); ParameterMetaData pmd = ps.getParameterData(); ResultSetMetaData rmd = rs.getMetaData();
Java 5 Programmierhandbuch
901
Zugriff auf Metadaten einer Datenbank
33.7.1
Informationen zu den Datenbankelementen
Das Interface DatabaseMetaData verfügt über sehr viele Methoden und statische Eigenschaften. Sie liefern Informationen zur Funktionsweise und den Eigenschaften des Datenbanksystems. Die Rückgabewerte sind vom einfachen Typ int, String oder boolean, aber auch vom Typ ResultSet. Letztere können wie Ergebnismengen, die durch Abfragen erzeugt wurden, verarbeitet werden.
[IconInfo]
Mit den Methoden des Interfaces DatabaseMetaData werden einige Informationen zum verwendeten JDBC-Treiber ausgegeben. Danach werden alle Tabellen inklusive der Systemtabellen über die Methode getTables() ermittelt. Das Ergebnis vom Typ ResultSet wird wie eine übliche Ergebnismenge durchlaufen. Informationen zum Aufbau der Ergebnismenge und den zu verwendenden Parametern finden Sie in der Hilfe zu den jeweiligen Methoden.
Listing 33.11: \Beispiele\de\j2sebuch\kap33\DBMetadaten.java import java.sql.*; public class DBMetadaten { public DBMetadaten() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden"); DatabaseMetaData dmd = verbMySQL.getMetaData(); System.out.println(dmd.getDatabaseProductName()); System.out.println(dmd.getDriverVersion()); System.out.println("Unions: " + dmd.supportsUnion()); System.out.println("-------------------"); System.out.println("Tabellen der Datenbank:"); ResultSet rsTabs = dmd.getTables( null, null, "%", new String[] {"TABLES"}); while(rsTabs.next()) { System.out.println(rsTabs.getString(3));
902
33 – JDBC – Datenbankzugriff Listing 33.11: \Beispiele\de\j2sebuch\kap33\DBMetadaten.java (Forts.) ResultSet rsFelder = dmd.getColumns( null, null, rsTabs.getString(3), "%"); while(rsFelder.next()) System.out.println(" " + rsFelder.getString(4)); } } catch(SQLException sqlEx) { } } public static void main(String args[]) { new DBMetadaten(); } }
33.7.2
Informationen zur Ergebnismenge
Wenn Sie für Tabellen, deren Aufbau Sie nicht kennen, SQL-Anweisungen der Form SELECT * FROM TabellenName nutzen, haben Sie keine Kenntnis über die Anzahl der zurückgelieferten Spalten und deren Typ. So macht es beispielsweise keinen Sinn, Daten eines binären Blob-Felds in einer Tabellenstruktur anzuzeigen. Über die Methoden des Interfaces ResultSetMetaData können Sie beispielsweise die Anzahl der Spalten, deren Typ und die Namen ermitteln. Als Parameter erwarten, bis auf die Methode getColumnCount(), welche die Anzahl der Spalten liefert, alle anderen Methoden einen Spaltenindex, der von 1 bis n läuft. Ein Objekt vom Typ ResultSetMetaData erhalten Sie durch den Aufruf der Methode getMetaData() eines ResultSet. Das Interface ResultSetMetaData besitzt zahlreiche Methoden, von denen hier nur einige vorgestellt werden. Die Methoden liefern der Reihe nach die Anzahl der Spalten der Ergebnismenge, den Spaltennamen, den Typ der Spalte als int-Wert und als String sowie die Information, ob Nullwerte in der Spalte erlaubt sind. int getColumnCount() String getColumnName(int column) int getColumnType(int column) String getColumnTypeName(int column) int isNullable(int column)
Beispiel Nachdem eine Ergebnismenge vorliegt, können deren Metadaten über die Methode getMetaData() bestimmt werden. Zuerst wird die Anzahl der Spalten, danach für alle Spalten deren Name und der Typ ausgegeben.
Java 5 Programmierhandbuch
903
Datenbankzugriff über Applets ResultSetMetaData rmd = rsTabs.getMetaData(); System.out.println("Anzahl Spalten: " + rmd.getColumnCount()); for(int i = 1; i