Dirk Frischalowski, Ulrike Böttcher Java 6 Programmierhandbuch
Dirk Frischalowski Ulrike Böttcher
Java 6 Programmierhandbuch
Ulrike Böttcher, Dirk Frischalowski: Java 6 Programmierhandbuch ISBN: 978-3-939084-12-9
© 2007 entwickler.press Ein Imprint der Software & Support Verlag GmbH
http://www.entwickler-press.de http://www.software-support.biz
Ihr Kontakt zum Verlag und Lektorat:
[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: mediaService, Siegen Satz: mediaService, Siegen Umschlaggestaltung: Melanie Hahn, Maria Rudi Belichtung, Druck & 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
Teil I Grundlagen
21
23
1
Einführung und Installation
1.1
Installation der Java SE 6 Download der Installationsdateien Installation unter Windows Installation unter Linux Das JDK deinstallieren Verwendung der Dokumentation
28 28 29 31 33 34
1.2
Die Verzeichnisstruktur und wichtige Dateien des JDK
35
1.3
Gängige Abkürzungen im Java-Umfeld
36
2
Die erste Java-Anwendung
39
2.1
Einführung
39
2.2
Eingabe des Sourcecodes Ein einfacher Editor – ConTEXT Grundelemente einer Java-Anwendung in der Übersicht
40 40 41
2.3
Übersetzen von Anwendungen
45
2.4
Ausführen der Anwendung
49
2.5
Der Klassenpfad
53
2.6
Applets mit dem Appletviewer ausführen
55
2.7
Verwendung der Beispiele
56
2.8
Datenein- und -ausgabe
57
2.9
Kurzes Glossar
58
3
Grundlegende Sprachelemente
61
3.1
Elemente eines Programms Anweisungen und Anweisungsblöcke Kommentare Reservierte Wörter Literale Bezeichner
61 61 61 63 63 63
Java 6
25
5
Inhaltsverzeichnis
6
3.2
Primitive Datentypen Numerische Datentypen Datentyp Zeichen Logische Datentypen
65 65 67 68
3.3
Variablen und Konstanten Variablen Wertzuweisungen Typumwandlungen Konstanten
69 69 71 72 73
3.4
Operatoren und Ausdrücke Arithmetische Operatoren Vergleichsoperatoren (relationale Operatoren) Logische Operatoren Bitweise Operatoren
74 76 78 79 80
3.5
Steuerung des Programmflusses if-Anweisung if-else-Anweisung Der Bedingungsoperator ? : switch-Anweisung for-Anweisung Die verbesserte for-Anweisung while-Anweisung do-while-Anweisung Programmfluss mit break und continue beeinflussen
82 82 83 86 87 91 93 94 96 98
4
Klassen, Interfaces und Objekte
101
4.1
Einführung Klassen definieren Baupläne Objekte sind die konkrete Realisierung des Bauplans
101 101 102
4.2
Einfache Klassen
103
4.3
Objekte
105
4.4
Methoden Einfache Methoden ohne Parameterübergabe Parameter übergeben
108 109 112
4.5
Konstruktoren und Destruktoren
118
4.6
Zugriffsattribute und Sichtbarkeit
124
4.7
Statische Klassenelemente
125
4.8
Aufzählungstypen mit Enum
127
4.9
Vererbung Die Klasse Object als Basisklasse aller Klassen Klassen ableiten Konstruktoraufrufe Vererbungsketten und Zuweisungskompatibilität Finale Klassen
134 134 137 139 141 142
Inhaltsverzeichnis
4.10 Interfaces
143
4.11 Adapterklassen
148
4.12 Abstrakte Klassen und Methoden
149
4.13 Methoden überschreiben
152
4.14 Polymorphie
153
4.15 Innere, verschachtelte und lokale Klassen Innere Klassen Verschachtelte Klassen
155 155 157
4.16 Anonyme Klassen
160
5
Packages
163
5.1
Einführung Package-Hierarchie Benannte und unbenannte Packages Zugriffsrechte Aufteilung einer Anwendung in Packages
163 164 165 165 166
5.2
Packages importieren
167
5.3
Statischer Import
169
6
Arrays, Wrapper und Auto(un)boxing
171
6.1
Arrays
171
6.2
Die Klasse Arrays
175
6.3
Wrapper-Klassen Nützliche Methoden Auto(un)boxing Bitmanipulation
177 178 179 180
7
Exceptions
183
7.1
Einführung
183
7.2
Exceptions behandeln
185
7.3
Exceptions weitergeben
189
7.4
Aufräumen mit finally
190
7.5
Exceptions auslösen Exceptions erzeugen und auslösen Exceptions erneut auslösen Exception-Ketten
192 192 193 195
7.6
Eigene Exceptions verwenden
197
8
Assertions
201
8.1
Einführung
201
8.2
Informationen zum Einsatz von Assertions Seiteneffekte Einsatzgebiete
203 203 203
Java 6
7
Inhaltsverzeichnis
8
8.3
Aktivieren von Assertions Übersetzung Ausführen Verhindern der Einbindung in die *.class-Datei Sicherstellung der Aktivierung
206 206 207 207 208
9
Zeichenkettenverarbeitung
9.1
Mit String-Objekten arbeiten Ein String-Objekt erzeugen Länge eines Strings und Position einzelner Zeichen Strings verketten String-Objekte ändern Strings vergleichen Zeichenketten manipulieren Formatierte Strings erzeugen Andere Datentypen in einen String konvertieren
9.2
StringBuilder- und StringBuffer-Objekte verwenden 219 Ein StringBuffer-Objekt erzeugen 220 Ein StringBuffer- in ein String-Objekt umwandeln 220 Daten anhängen und einfügen 221 Löschen und Verändern von Zeichen im StringBuffer 222 String-Länge und Puffergröße bestimmen 222 Vergleich von StringBuffer-Objekten 223 Performance-Steigerung durch die Klasse StringBuffer und StringBuilder 224
9.3
Formatierung Formatierung mithilfe der Klasse Formatter Formatter-Objekt erzeugen Daten konvertieren Weitere Methoden der Klasse Formatter Formatierung von Zahlen über die Klasse NumberFormat
226 226 226 227 236 237
10
Nützliche Klassen
241
209 209 209 210 211 212 212 216 218 218
10.1 Datum und Uhrzeit Die Klassen Calendar und GregorianCalendar
241 241
10.2 Zufallszahlen erzeugen
249
10.3 Die Klasse System Standardeingabe, Standardausgabe Zugriff auf die Textkonsole Zugriff auf Systemeigenschaften und Umgebungsvariablen Weitere Methoden der Klasse System
251 251 252 253 257
10.4 Die Klassen Process, ProcessBuilder und Runtime
259
10.5 Splash Screens
264
Inhaltsverzeichnis
10.6 Reguläre Ausdrücke Suchmuster definieren Die Klassen Pattern und Matcher Reguläre Ausdrücke in Scanner-Objekten anwenden
266 266 270 277
11
281
Datei- und Verzeichniszugriffe
11.1 File-Objekt erzeugen
281
11.2 Informationen über Datei ermitteln Verzeichnisse, Pfade und Dateinamen Eigenschaften von Dateien bzw. Verzeichnissen bestimmen Attribute von Dateien setzen
283 283 284 285
11.3 Verzeichnisse und Dateien anlegen, löschen und umbenennen Verzeichnisse erstellen und löschen Dateien anlegen, löschen und umbenennen Temporäre Dateien erzeugen und löschen
285 286 286 289
11.4 Dateien und Verzeichnisse auflisten lassen
291
12
Ein- und Ausgabe / Streams
295
12.1 Ein- und Ausgabe auf Standardgeräte
295
12.2 Das Stream-Konzept von Java
298
12.3 Character-Streams Gemeinsame Methoden der Writer-Klassen Gemeinsame Methoden der Reader-Klassen Ein- und Ausgabe in Dateien Ein- und Ausgabe in Character-Arrays und StringBuffer Pufferung erhöht die Effizienz Formatierte Ausgaben Filter verwenden Datenaustausch zwischen Threads
301 302 303 304 307 309 315 321 326
12.4 Byte-Streams Gemeinsame Methoden der OutputStream-Klassen Gemeinsame Methoden der InputStream-Klassen Formatierte Ausgaben Ausgeben und Einlesen primitiver Datentypen Verknüpfen mehrerer InputStreams
326 327 327 328 328 333
12.5 ObjectStreams Die Klasse ObjectOutputStream Die Klasse ObjectInputStream Änderungen an serialisierbaren Klassen (Versionsverwaltung)
335 335 336 343
12.6 Zip-Streams Zip-Dateien anlegen Daten in die Zip-Datei ausgeben Inhalt der ZIP-Dateien auslesen Die Klasse ZipEntry Objekte komprimieren
345 346 347 347 351 354
Java 6
9
Inhaltsverzeichnis
12.7 Dateien mit wahlfreiem Zugriff
355
12.8 Das Package java.NIO Die Pufferklassen des NIO Memory Mapping Channels Charsets
359 360 366 369 373
13
377
13.1 Einführung Das Collection-Interface
377 381
13.2 BitSet
387
13.3 Listen Vektoren Stack ArrayList
388 388 389 390
13.4 Mengen
392
13.5 Schlangen Queues
395 395
13.6 Abbildungen Hashtable Properties-Dateien
396 399 399
13.7 Algorithmen der Klasse Collections Sortieren von Collections Synchronisierte Collections Unveränderliche Collections
402 404 407 408
14
411
Generics
14.1 Einführung
411
14.2 Type Erasure
414
14.3 Generische Typen
417
14.4 Wildcards und Bounds Wildcards Bounds
418 419 421
14.5 Generische Methoden
423
14.6 Standardcode und Generics
427
14.7 Einschränkungen
428
14.8 Kovariante Rückgabetypen
429
15
10
Collections
JAR-Archive
433
15.1 Einführung
433
15.2 Das Manifest und das Verzeichnis META-INF
434
Inhaltsverzeichnis
15.3 Verwendung des jar-Tools Archive erstellen Verwendung einer Dateiliste Archive aktualisieren Archivinhalte auflisten Archive auspacken Indexdateien erzeugen
436 438 439 439 439 440 440
15.4 Verwendung von Archiven
441
15.5
Signieren von Archiven
442
16
Javadoc
447
16.1 Einführung
447
16.2 Anwendung des Kommandozeilentools
449
16.3 Dokumentationskommentare
452
16.4 API-Schnittstelle von Javadoc
456
16.5 Doclets
457
16.6 Taglets
461
17
Internationalisierung
465
17.1 Einführung
465
17.2 Sprach- und Ländereinstellungen
466
17.3 Zahlen, Texte und Datum formatieren Zahlen formatieren Zeichen und Texte formatieren Datums- und Zeitangaben formatieren
468 470 472 473
17.4 ResourceBundles Einführung Erzeugen und Verwenden von ResourceBundles
475 475 476
18
Anwendungen weitergeben
483
18.1 Einführung Installation unter Windows Installation unter Linux
483 484 484
18.2 Angepasste Installationen
485
18.3 Wie werden Anwendungen weitergegeben?
486
Teil II Grafische Programmierung 19
Einführung in die grafische Programmierung
489 491
19.1 Allgemeines
491
19.2 Ein einführendes Beispiel
492
Java 6
11
Inhaltsverzeichnis
20
499
20.1 Fenster Fenster anzeigen und schließen Fenstereigenschaften
500 500 504
20.2 Ereignisse Das Delegation Model Ereignisklassen und Listener Implementierung von Listenern Ereignisse aktivieren Überblick über AWT-Ereignisse Auf Low-Level-Ereignisse reagieren
510 510 511 514 520 522 523
20.3 AWT-Komponenten Übersicht der AWT-Komponenten Verwendung der Komponenten Menüs
539 539 541 560
20.4 Dialoge Selbst erstellte Dialoge Vordefinierte Dialoge
570 570 572
20.5 LayoutManager Das FlowLayout Das BorderLayout Das GridLayout Oberflächen gestalten
574 576 577 579 580
20.6 Einfache Grafikprogrammierung Einführung Farben Formen zeichnen Linien Rechtecke Polygone (n-Ecke) Ellipsen und Ellipsenbogen
582 582 584 586 587 587 588 588
20.7 TrayIcons
595
20.8 Anwendungen über Dateiverknüpfungen ausführen
599
21
12
Das Abstract Window Toolkit
Swing
605
21.1 Grundlagen
605
21.2 Fensterklassen Aufbau von Swing-Fenstern Swing-Fenster schließen Das Look & Feel – das Erscheinungsbild der Fenster Interne Fenster Dialoge
606 606 607 608 610 613
21.3 Die Model-View-Controller-Architektur
621
Inhaltsverzeichnis
21.4 Swing-Komponenten Ein Vergleich mit AWT-Komponenten Allgemeine Eigenschaften von Swing-Komponenten Komponenten mit Icons Weitere Swing-Komponenten Ausgewählte Swing-Komponenten Drucken von Textfeld-Inhalten Swing-Menüs Die Zwischenablage
625 625 626 628 630 632 647 649 650
21.5 Drag & Drop
655
21.6 Der SwingWorker
658
22
Applets
663
22.1 Einführung
663
22.2 Aufbau von Applets Ein Applet erzeugen Der Lebenszyklus des Applets
664 665 666
22.3 Applets starten Applets aus einer HTML-Datei starten Parameterübergabe Der Appletviewer
667 667 670 671
22.4 Informationen zum Applet anzeigen
672
22.5 Applets mit Animationen und Sound
672
22.6 Den Applet-Kontext nutzen
677
22.7 Plug-Ins verwenden Installation des Plug-Ins Anpassen der HTML-Dateien
682 682 683
22.8 Sicherheit
684
23
Drucken
687
23.1 Entwicklung
687
23.2 Drucken über das Java 2 Print API Druckseite aufbauen Druckdokument erstellen Druckausgabe über die Klasse PrinterJob
687 687 688 689
23.3 Drucken mit dem Java Print Service Attribute festlegen Druckformat auswählen Drucker suchen Druckdaten bereitstellen und Druck starten Druckereignisse
691 692 694 695 695 696
Java 6
13
Inhaltsverzeichnis
24
JavaBeans
701
24.1 Einführung
701
24.2 JavaBeans implementieren Eigenschaftsmethoden
701 703
24.3 Ereignisse durch JavaBeans auslösen
707
24.4 Java Beans in Eclipse erzeugen, testen, verwenden Java Beans testen Java Beans verwenden Java Beans erstellen
712 712 712 715
24.5 Weitere Informationen über Beans bereitstellen
716
Teil III Fortgeschrittene Themen 25
14
Debuggen
719 721
25.1 Einführung Fehlerklassifizierung Anwendung eines Debuggers Weitere Debug-Techniken
721 721 722 723
25.2 Der Java Debugger jdb
724
25.3 Grafische Debugger Vorgehensweise beim grafischen Debuggen
726 727
26
731
Reflection
26.1 Einführung
731
26.2 Klassenobjekte ermitteln Klassenobjekt über ein Objekt ermitteln Klassenobjekt über die Klasse ermitteln Klassenobjekt über den Klassennamen ermitteln
733 733 734 734
26.3 Klasseninstanzen dynamisch erzeugen
734
26.4 Informationen über eine Klasse ermitteln Klassenobjekte besitzen einen Basistyp Modifizierer auslesen Variablen ermitteln Informationen zu Methoden
735 736 737 739 744
26.5 Methoden aufrufen
746
26.6 Konstruktoren ermitteln und aufrufen
748
26.7 Arbeit mit Arrays
749
26.8 Aufzählungen ermitteln
752
26.9 Dynamisches Laden von Klassen
753
26.10 Proxy-Klassen
755
Inhaltsverzeichnis
27
Annotations
27.1 Einführung
759 759
27.2 Vordefinierte Annotation-Typen
761
27.3 Eigene Annotation-Typen Deklaration der Annotation Annotations verwenden
764 764 766
27.4 Annotations für Packages
767
27.5 Zugriff zur Laufzeit
768
27.6 Annotation Processing Tool – apt
771
28
Logging
777
28.1 Einführung
777
28.2 Logger erzeugen
779
28.3 Log-Einträge erzeugen und Log-Level setzen
779
28.4 Handler verwenden
782
28.5 Der LogManager Konfigurationsdatei
786 787
28.6 Filter verwenden
790
28.7 Log4j
792
29
Preferences
793
29.1 Einführung
793
29.2 Speichern und Laden von Einstellungen
795
29.3 Zugriff auf die Hierarchie
797
29.4 Reagieren auf Änderungen
801
29.5 Preferences exportieren und importieren
803
30
Threads
805
30.1 Einführung
805
30.2 Threads über die Klasse Thread erzeugen
807
30.3 Threads über das Interface Runnable erzeugen
810
30.4 Threads unterbrechen
812
30.5 Zustände eines Threads
815
30.6 Prioritäten
817
30.7 Daemon-Threads
819
30.8 Timer
821
30.9 Thread-Gruppen
824
Java 6
15
Inhaltsverzeichnis
30.10 Synchronisation Einführung Einfache Synchronisationsmechanismen Monitore Kooperation zwischen Threads Das Attribut volatile Deadlocks
825 825 826 828 832 837 837
30.11 Datenaustausch zwischen Threads
840
30.12 Die Concurrency Utilities
843
31
847
31.1 Einführung
847
31.2 Zugriff auf Netz-Adressen
852
31.3 Arbeiten mit URLs URL-Objekte erzeugen URLs parsen String getFile()Daten verarbeiten Einen WebServer erstellen
858 858 859 859 861
31.4 Socketverbindungen ClientSockets ServerSockets Verwaltung mehrerer paralleler Verbindungen
866 867 871 876
31.5 Datagramme Client-Anwendungen Server-Anwendungen
881 881 883
31.6 Das Java Mail API Mails senden Mails empfangen Anhänge verschicken und empfangen
885 886 890 892
32
16
Netzwerkanwendungen
XML
895
32.1 Einführung
895
32.2 XML-Grundlagen
896
32.3 XML-Parser SAX-Parser DOM-Parser
898 899 908
32.4 XSLT-Transformationen Kommandozeilenversion DOM-Bäume speichern XML-Dokumente transformieren
915 917 919 920
32.5 StAX – Streaming von XML-Daten XML-Daten lesen XML-Daten schreiben
921 922 926
Inhaltsverzeichnis
32.6 JAXB – XML Bindungen Schema-nach-Java Java-nach-Schema 33
JDBC – Datenbankzugriff
930 930 934 939
33.1 Einführung JDBC-Treiber Treibertypen Architektur von Datenbankanwendungen
939 939 940 942
33.2 Einrichten einer Datenbank MySQL Firebird installieren
944 944 946
33.3 Herstellen der Datenbankverbindung Einführung JDBC-Treiber laden Die Verbindung herstellen
948 948 949 951
33.4 SQL-Anweisungen einsetzen Einführung Anweisungen ausführen Vorbereitete Anweisungen Stored Procedures verwenden Batch-Mode
953 953 955 956 959 964
33.5 Zugriff auf die Ergebnismengen Einführung Werte auslesen Navigation Konfiguration Werte ändern und zurückschreiben
967 967 967 970 971 972
33.6 Transaktionsverwaltung Einführung Transaktionen unter JDBC Isolationsstufen Sicherungspunkte
975 975 976 978 979
33.7 Zugriff auf Metadaten einer Datenbank Informationen zu den Datenbankelementen Informationen zur Ergebnismenge
981 982 983
33.8 Datenbankzugriff über Applets
984
33.9 Fehlersuche in JDBC-Anwendungen
985
33.10 Java DB
986
33.11 Annotations in JDBC 4.0
990
34
JNDI
993
34.1 Einführung
993
34.2 Benötigte Software
995
Java 6
17
Inhaltsverzeichnis
34.3 Namensdienste verwenden 34.4 Verzeichnisdienste verwenden 35
JUnit
1005
35.1 Einführung Was sind Unit Tests? Test-Driven Development (TDD) Zeitpunkt der Testerstellung Refactoring Weitere Vorteile
1005 1005 1006 1006 1007 1007
35.2 Installation von JUnit Das Prinzip von JUnit 4
1008 1009
35.3 Testfälle Tests über Behauptungen definieren Eigene Testlogiken verwenden Tests initialisieren
1010 1013 1014 1015
35.4 TestSuite
1017
35.5 Spezialfälle beim Testen Tests deaktivieren Exceptions testen Maximale Ausführungsdauer testen
1018 1018 1019 1019
35.6 Parametrisierte Tests
1020
35.7 Tests manuell ausführen und auswerten
1022
36
Scripting
1027
36.1 Einführung Das Scripting API
1027 1028
36.2 Skripte in Java einbinden Parameterübergabe und Rückgabewerte Skripte aus Dateien laden Aufruf von Methoden Informationen zur Skript Engine Skripte kompilieren
1028 1029 1031 1033 1034 1036
36.3 Java in Skripten verwenden Skripte über die Kommandozeile ausführen Zugriff auf Java-Klassen und -Objekte
1037 1038 1039
37
Web Services
37.1 Einführung
18
996 1003
1043 1043
37.2 Grundlagen von Web Services
1044
37.3 Web Services im JDK Web Service erstellen
1045 1045
37.4 Web Service Client erstellen
1049
Inhaltsverzeichnis
38
Monitoring, Management und Compiler API
1053
38.1 Einführung
1053
38.2 Das Compiler API
1053
38.3 JMX und MBeans Standard-MXBeans verwenden jconsole Standard-MBeans erstellen Dynamisch MXBeans
1056 1057 1059 1064 1069
A
Die Programmierumgebung Eclipse
1075
A.1
Download, Installation und Start
1075
A.2
Erste Schritte Einstellung der IDE Workspaces und Projekte
1076 1076 1078
A.3
Die Workbench
1079
A.4
Anwendungen übersetzen und ausführen
1083
A.5
Bibliotheken und Archive verwenden
1086
A.6
Refactoring Umbenennen Methoden verschieben Member in die Basisklasse verschieben Schnittstellen extrahieren
1089 1090 1090 1091 1092
A.7
Plug-Ins installieren Manuelle Installation Installation über den Update-Manager
1092 1092 1093
A.8
SWT - Standard Widget Toolkit SWT – Vergleich mit AWT und Swing Eine SWT-Rahmenanwendung erstellen Die Klassen des SWT Ereignisbehandlung Weitergabe und Besonderheiten
1094 1095 1096 1098 1100 1101
A.9
Debuggen
1101
B
Inhalt der Buch-CD
1105
B.1
Beispiele
1105
B.2
Special
1105
B.3
Software
1105
B.4
Browser
1105
Stichwortverzeichnis
1107
Java 6
19
Vorwort Herzlich Willkommen zur Neuauflage unseres Buches zum Thema Java, jetzt in der Version 6. Natürlich ist nicht alles neu, altbewährtes blieb erhalten, einige Themen wurden deutlich erweitert, wie z.B. die XML-Verarbeitung und es kamen neue Themen hinzu wie Scripting oder JMX. Da die meisten Entwickler mit einer IDE arbeiten, haben wir zum Abschluss auch eine kurze Einführung in Eclipse beigefügt. Allerdings sollten Sie zu Beginn Ihres Studiums auf die Verwendung einer IDE zu Ihrem eigenen Vorteil verzichten.
Kurzüberblick zum Inhalt Dieses Buch bietet Ihnen einen fundierten und breit gefächerten Einstieg in die Java-Programmierung. Wir haben den umfangreichsten Neuerungen des JDKs 6.0 wie Scripting und Web Services eigene Kapitel gewidmet und einige Kapitel wie z.B. die XML-Verarbeitung erweitert. Viele Themen werden sehr ausführlich erläutert, einige nur angerissen. Zur Programmierung von grafischen Oberflächen oder den Umgang mit Web Services haben wir uns bewusst auf eine Einführung beschränkt, damit wir uns auch den zahlreichen anderen Themen widmen können. 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 JDKs 6.0 und erhält ein umfangreiches und trotz seiner Dicke komprimiertes Nachschlagewert mit zahllosen Beispielen.
Gliederung Das Buch ist in drei Bereiche untergliedert. In den ersten Kapiteln werden die Installation des JDKs sowie das Übersetzen und die Ausführung von Java-Anwendungen erläutert. Danach lernen Sie den Aufbau von einfachen Java-Anwendungen und wichtige Sprachkonstrukte wie Alternativen und Schleifen kennen. Es werden häufig verwendete Klassen vorgestellt, die bereits für die Erstellung einfacher Java-Anwendungen 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 dem Aufbau von Fenstern und dem Erzeugen von Grafiken auch die Programmsteuerung über Ereignisse und die automatische Anordnung von Komponenten durch die Layoutmanager. Für die Programmierung grafischer Oberflächen wird das AWT sowie auch Swing vorgestellt. Der dritte Teil des Buches beinhaltet verschiedene unabhängige Themen wie Multithreading, Netzwerkzugriffe, Reflection, Annotations, den Datenbankzugriff über JDBC, die Verarbeitung von XML-Daten, Scripting und das Management und Monitoring von JavaAnwendungen. Diese Themen können zwar meist unabhängig voneinander bearbeitet werden, allerdings gehen wir immer davon aus, dass die Inhalte der vorigen Kapitel bekannt sind.
Java 6
21
Vorwort
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 darauf Bezug genommen und damit erschließen sich auch diese Inhalte. Später eignet sich dieser kompakte Aufbau wesentlich besser zum Nachschlagen.
Beispiele Alle im Buch beschriebenen Fakten werden durch leicht nachvollziehbare Beispiele untermauert, die nur mit den Mitteln des JDKs über die Konsole ausführbar sind. Dabei wurde das Hauptaugenmerk auf die zu beschreibende Technologie und nicht auf besonders umfangreiche Beispiele gelegt.
Voraussetzungen 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 werden in den entsprechenden Kapiteln gegeben. Zur Bearbeitung der Beispiele empfehlen wir zu Beginn zum besseren Verständnis einen einfachen Texteditor zu verwenden. Später können Sie eine Programmierumgebung wie z.B. Eclipse oder NetBeans einsetzen, die Sie unter anderem bei der Eingabe und Übersetzung des Codes unterstützt. Es wird innerhalb der einzelnen Kapitel nicht auf eine spezielle IDE eingegangen, allerdings beschreiben wir im Anhang kurz die Verwendung von Eclipse.
Anregungen und Kontakt Zum Buch wurde von uns eine Webseite eingerichtet, die Sie über die URL http:// www.j2sebuch.de/jse6buch/ besuchen können. Hier stellen wir bei Bedarf zusätzliche Informationen bereit, wie z.B. Errata. 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]. Wir bedanken uns bei unserer Lektorin Christiane Auf bei entwickler.press für ihr Engagement sowie Herrn Andreas Franke von mediaService für die gelungene Umsetzung des Geschriebenen in das vorliegende Buch. Wir wünschen Ihnen viel Spaß und Erfolg beim Lernen und Programmieren mit Java. Ulrike Böttcher und Dirk Frischalowski, Dezember 2006
22
Grundlagen
Einführung und Installation Ein paar Worte zu Java Die Programmiersprache Java gibt es nun seit mehr als zehn Jahren und die Anzahl der Entwickler, die Java nutzen, wächst immer 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 dazu 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. Nach dem Durcharbeiten eines Kapitels sollte ein Basiswissen über das vermittelte Thema 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 (Anweisungen, die direkt für den Prozessor unter einem speziellen Betriebssystem bestimmt sind) weitergegeben, sondern in einem Zwischencode, der dann auf der jeweiligen Plattform interpretiert und ausgeführt wird.
Die neue Namensgebung Mit diesem Release wurde der Name der Plattform von Java 2 Platform Standard Edition (kurz J2SE) zu Java Platform, Standard Edition 6 geändert. Die neue Version besaß den Codenamen Mustang und erhält jetzt die Versionsnummer 6 bzw. 1.6.0. Der neue Name der Java Plattform lautet also Java Platform, Standard Edition 6
Java 6
25
1 – Einführung und Installation
oder kurz: Java SE 6 JSE 6
Das JDK und das JRE werden folgendermaßen benannt: JDK 6 bzw. Java SE Development Kit 6 JRE 6 bzw. Java SE Runtime Environment 6
Auch die nach dem alten Schema fortgeführte Versionsnummer 1.6 bzw. 1.6.0 identifiziert die neue Java-Version und wird für verschiedene Bezeichnungen genutzt, z.B. in: java -version javac -source 1.6 java.version @since 1.6 jdk1.6.0 jre1.6.0
-
Anzeige der Version des Interpreters Übersetzen für eine bestimmte Version eine Systemeigenschaft in der Dokumentation das Installationsverzeichnis des JDK das Installationsverzeichnis des JRE
Neuerungen in der Java SE 6 Ein kurzer Vergleich der Anzahl der Dateien im Archiv [InstallJDK]\jre\lib\rt.zip zeigt, dass sie von ca. 13000 Dateien im JDK 5 zu 15846 im JDK 6 angestiegen ist. Bei der Entwicklung der JSE 6 wurden verschiedene Schwerpunkte gesetzt. Bei der Entwicklung der JSE 6 wurde weniger Augenmerk auf die Revolutionierung der Sprache und die Einführung neuer Konzepte gelegt, dies ist erst mit der nächsten Version zu erwarten. Die JSE 6 wurde aber mit etlichen interessanten und nützlichen Erweiterungen und Verbesserungen ausgestattet. So wurde ein Framework zum Einbinden von Scriptsprachen (JavaScript, Perl, Python und Ruby) integriert und eine Reihe von Web Service-Features hinzugefügt. Weitere Neuerungen liegen z.B. in der Erweiterung der APIs zur Verarbeitung von XML-Daten, in der Verbesserung von Sicherheit und Performance, in der Unterstützung von JDBC 4.0 sowie einer Weiterentwicklung des Annotation APIs. Eine vollständige Übersicht finden Sie z.B. in der Dokumentation unter http://java.sun.com/ javase/6/webnotes/features.html. Die JSE6 ist übrigens noch nicht Open Source. Dies wird erst für die nächste Version erwartet.
Die Java-Editionen J2SE, J2EE und J2ME Jedes spezielle Einsatzgebiet verlangt nach einer speziellen Software. So wird Java in verschiedenen Editionen für Standardanwendungen (Java SE – Java Platform, Standard Edition), Server-Anwendungen im Unternehmensumfeld (Java EE – Java Platform, Enterprise Edition) und für mobile Geräte wie Mobiltelefone oder PDAs (Java ME – Java Platform, Micro Edition) bereitgestellt. Die Basisfunktionalität befindet sich in der Java SE. Die Java EE setzt mit zusätzlichen Bibliotheken und Tools darauf auf. Die Java ME verwendet dagegen nur eine Teilmenge der Java SE, da die mobilen Geräte nicht über den Speicher und die Rechenleistung gängiger PCs verfügen.
26
Unterschied zwischen dem JDK und dem JRE Das Java Development Kit (bzw. Java SE 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 enthä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/javase/6/ http://java.sun.com/javase/6/webnotes/version-6.html http://java.sun.com/javase/6/webnotes/index.html http://java.sun.com/javase/technologies/index.jsp http://jcp.org/en/jsr/all http://www.javaworld.com/ http://www.apache.org/ http://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 meist mit diesem Akronym ansprechen. Haben Sie das JDK beispielsweise nach D:\jdk1.6.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 JavaTools übergeben. Die Konsole wird auch als Eingabeaufforderung (unter Windows) oder Shell (unter Linux) bezeichnet.
Tabelle 1.1: Konventionen, Platzhalter und Bezeichner im Buch
Java 6
27
1 – Einführung und Installation
Thema
Erläuterung
Java Interpreter
Die Anwendung zum Ausführen einer Java-Anwendung werden wir immer als Java Interpreter oder kurz als 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.
Typen
Wenn von Typen gesprochen wird, sind in der Regel Klassen, 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.jse6buch. und dem jeweiligen Kapitelnamen zusammen, z.B. package de.jse6buch.kap01;.
Ausführen der Beispiele Die Beispiele nutzen die neuen Features der Java SE 6, 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 können. 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.
Tabelle 1.1: Konventionen, Platzhalter und Bezeichner im Buch (Forts.)
1.1
Installation der Java SE 6
1.1.1
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 zwischen 50-60 MB groß.
Installationsdateien des JDK downloaden 쮿
Öffnen Sie die URL http://java.sun.com/javase/downloads/index.jsp in Ihrem Browser und klicken Sie im Bereich JDK 6 auf den Button DOWNLOAD.
쮿
Auf der nächsten Seite markieren Sie die Option ACCEPT, um die Lizenzbestimmungen zu akzeptieren. Danach werden die Links für den Download frei geschaltet.
쮿
Im Bereich WINDOWS PLATFORM wählen Sie den Link WINDOWS OFFLINE INSTALLATION, MULTI-LANGUAGE (jdk-6-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 FILE (jdk-6-linux-i586.bin).
28
SELF-EXTRACTING
Installation der Java SE 6
Dokumentation des JDK downloaden 쮿
Öffnen Sie die URL http://java.sun.com/javase/downloads/index.jsp in Ihrem Browser und klicken Sie im Bereich JAVA SE 6 DOCUMENTATION auf den Button DOWNLOAD.
쮿
Auf der nächsten Seite markieren Sie die Option ACCEPT, um die Lizenzbestimmungen zu akzeptieren. Danach werden die Links für den Download frei geschaltet.
쮿
Laden Sie die Dokumentation, die im ZIP-Format vorliegt, über den Link JAVA(TM) SE DEVELOPMENT KIT DOCUMENTATION 6 (jdk-6-doc.zip).
1.1.2
Installation unter Windows
Sie benötigen für die Installation unter Windows eines der Betriebssysteme Windows Vista, Windows Server 2003, Windows XP Professional bzw. Home oder Windows 2000 Professional bzw. Server. Die älteren Betriebssysteme Windows ME und Windows 98 werden nicht mehr unterstützt. 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-6-windows-i586.exe.
쮿
Bestätigen Sie die Lizenzbedingungen durch das Klicken auf den Button ACCEPT .
쮿
Ändern Sie gegebenenfalls das Installationsverzeichnis. Wir benutzen zur Installation den Pfad D:\jdk1.6.0. Dadurch verkürzen sich später die Verzeichnisangaben. Im Buch verweisen wir wie bereits erwähnt über das Akronym [InstallJDK] auf das Installationsverzeichnis. 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. Betätigen Sie anschließend die Schaltfläche NEXT.
쮿
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.6.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 6 nutzen, im Browser ausgeführt werden können, muss das Java Plug-In (Option: DEFAULT JAVA FOR BROWSERS) installiert werden. Es steht für den Internet Explorer und die Mozilla-Familie zur Verfügung. Starten Sie jetzt die Installation durch das Betätigen der Schaltfläche NEXT.
쮿
Schließen Sie die Installation durch Klicken auf FINISH ab.
Java 6
29
1 – Einführung und Installation
Hinweis 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.
Installation der Dokumentation Um die Dokumentation zu entpacken, können Sie ein beliebiges ZIP-Programm verwenden. Entpacken Sie damit die Datei jdk-6-doc.zip direkt im Installationsverzeichnis des JDK. Alternativ kopieren Sie die Datei jdk-6-doc.zip in das Installationsverzeichnis des JDK und entpacken sie über das Kommando .\bin\jar xf jdk-6-doc.zip
Das Tool jar ist Bestandteil der JDK-Installation. 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.6.0 installiert wurde. 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.6.0 ausgegeben werden. Konnte der Befehl java nicht gefunden werden, haben Sie wahrscheinlich Ihre PATH-Variable nicht korrekt gesetzt. 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 *.java
30
Installation der Java SE 6
War die Übersetzung erfolgreich, werden einige neue Dateien erstellt. Es werden auch zwei Warnungen (Notes) ausgegeben, die im Moment aber keine besondere Bedeutung haben. Beachten Sie, dass bei der Ausführung die Groß- und Kleinschreibung eine Rolle spielt. Im nächsten Schritt führen Sie die übersetzte Anwendung TableExample3 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 die Tastenkombination (Alt)+(F4). 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 unter der Überschrift API, LANGUAGE, AND VIRTUAL MACHINE DOCUMENTATION auf den Link JAVA PLATFORM API SPECIFICATION.
Hinweis Einige der Links in der Dokumentation benötigen eine aktive Internetverbindung.
1.1.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/javase/6/webnotes/ install/system-configurations.html. Auf der Festplatte sollten für das JDK ca. 200 MB freier Speicherplatz verfügbar sein. Die Installation der Dokumentation benötigt dann noch einmal ca. 300 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-6-linuxi586.bin durch den Aufruf des Kommandos chmod +x jdk-6-linux-i586.bin
쮿
Starten Sie die Installation über das Kommando ./jdk-6-linux-i586.bin
Java 6
31
1 – Einführung und Installation 쮿
Begeben Sie sich an das Ende der Lizenzbedingungen (seitenweise mit der (Leertaste), zeilenweise durch (¢), direkt an das Ende mit (Strg)+(C)) und geben Sie zur Bestätigung den Buchstaben (Y) gefolgt von (¢) ein.
쮿
Es wird sofort die Installation gestartet und dabei ein neues Unterverzeichnis jdk1.6.0 erstellt. Im Buch verweisen wir auf das Installationsverzeichnis über das Akronym [InstallJDK].
Hinweis Neben der hier vorgestellten Installationsart über die *.bin-Datei können Sie auch eine RPM-Datei mittels des rpm-Tools für die Installation nutzen. Sun stellt einen entsprechenden Download zur Verfügung.
Installation der Dokumentation Um die Dokumentation zu entpacken, können Sie ein beliebiges ZIP-Programm verwenden. Entpacken Sie darüber die Datei jdk-6-doc.zip direkt im Installationsverzeichnis jdk1.6.0 des JDK. Alternativ kopieren Sie die Datei jdk-6-doc.zip in das Installationsverzeichnis des JDK und entpacken sie über das Kommando ./bin/jar xf jdk-6-doc.zip
Das Tool jar ist Bestandteil der JDK-Installation. 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]/jdk1.6.0 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 folgenden Zeilen hinzu: PATH=/home/benutzer/jdk1.6.0/bin:$PATH export PATH
쮿
32
Speichern Sie die Datei ((Strg)+(K)+(X) in joe) und melden Sie sich erneut an. Jetzt können Sie die Kommandos java und javac überall verwenden.
Installation der Java SE 6
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.6.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.5.0, 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 *.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 die 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 unter der Überschrift API, LANGUAGE, AND VIRTUAL MACHINE DOCUMENTATION auf den Link JAVA PLATFORM API SPECIFICATION.
Hinweis Einige der Links in der Dokumentation benötigen eine aktive Internetverbindung.
1.1.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 6
33
1 – Einführung und Installation
1.1.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.
Abbildung 1.1: Startseite der API-Dokumentation
Die API-Dokumentation besteht aus drei Bereichen. Links oben können Sie ein Package auswählen, dessen Klassen, Interfaces, Aufzählungstypen und Exceptions danach im darunter liegenden Bereich angezeigt werden. Klicken Sie auf den Link ALL CLASSES, werden alle Java-Typen angezeigt. 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.
Hinweis In den Beschreibungen dieses Buchs werden immer nur die wichtigsten Bestandteile einer Klasse oder eines anderen Typs beschrieben. Wenn Sie alle Elemente (Konstanten, Methoden, Konstruktoren etc.) kennen lernen möchten, müssen Sie in jedem Fall in der API-Dokumentation nachschlagen.
34
Die Verzeichnisstruktur und wichtige Dateien des JDK
1.2
Die Verzeichnisstruktur und wichtige Dateien des JDK
Die Verzeichnisstruktur des installierten JDK 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. ..\jdk1.6.0\
Lizenz- und Readme-Dateien, SourceCode der Java SE (src.zip)
..\jdk1.6.0\bin
Anwendungen und Tools der Java SE
..\jdk1.6.0\demo
Beispielanwendungen mit SourceCode
..\jdk1.6.0\docs
Dokumentation (nach separater Installation)
..\jdk1.6.0\include
C-Header-Dateien für JNI und JVMDI
..\jdk1.6.0\jre
Wurzelverzeichnis der Java-Laufzeitumgebung
..\jdk1.6.0\jre\bin
Anwendungen und Bibliotheken der Java-Laufzeitumgebung
..\jdk1.6.0\jre\lib
Archive zur Ausführung von Java-Anwendungen
..\jdk1.6.0\jre\lib\ext
Verzeichnis für Erweiterungen (Archive), die automatisch geladen werden
..\jdk1.6.0\lib
Zusätzliche Dateien für Entwicklungstools, z.B. Eclipse
..\jdk1.6.0\sample
Verschiedene Beispiele
Tabelle 1.2: Verzeichnisse des JDKs ..\jdk1.6.0\bin\appletviewer
Ausführen von Applets
..\jdk1.6.0\bin\jar
Archivierungsprogramm
..\jdk1.6.0\bin\java
Java Interpreter (Java Application Launcher), startet eine Java-Anwendung
..\jdk1.6.0\bin\javac
Java-Compiler zum Übersetzen von *.java-Dateien
..\jdk1.6.0\bin\javadoc
Erzeugt API-Dokumentationen
..\jdk1.6.0\bin\javaw
Startet grafische Java-Anwendungen
..\jdk1.6.0\docs\index.html
Startseite der Dokumentation
..\jdk1.6.0\jre\lib\rt.jar
Archiv, welches die Basisklassen der Laufzeitumgebung enthält
..\jdk1.6.0\lib\dt.jar
Archiv, welches Informationen zu JavaBeans für grafische Entwicklungstools enthält
..\jdk1.6.0\lib\tools.jar
Archiv, welches Hilfsklassen für Tools enthält
Tabelle 1.3: Ausgewählte Dateien des JDKs
Java 6
35
1 – Einführung und Installation
1.3
Gängige Abkürzungen im Java-Umfeld
AWT
Abstract Window Toolkit
CORBA
Common Object Request Broker Architecture
DOM
Document Object Model
DTD
Document Type Definition
EE
Enterprise Edition
EJB
Enterprise JavaBeans
IDL
Interface Definition Language
IIOP
Internet Inter ORB Protocol
J2EE
Java 2 Platform, Enterprise Edition
J2SE
Java 2 Platform, Standard Edition
Java EE
Java Platform, Enterprise Edition
Java SE
Java Platform, Standard Edition
JAAPI
Java Accessibility API
JAAS
Java Authentication and Authorization Service
JAXB
Java Architecture for XML Binding
JAXP
Java API for XML Processing
JCF
Java Collection Framework
JCP
Java Community Process
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 Interface
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
Java Virtual Machine
36
Gängige Abkürzungen im Java-Umfeld
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
StAX
Streaming API for XML
VM
Virtual Machine
XML
Extended Markup Language
XSL
Extensible StyleSheet Language
XSLT
Extensible StyleSheet Language for Transformations
Java 6
37
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: Sie schreiben in einem Texteditor ein Java-Programm. Dazu kann ebenso eine Entwicklungsumgebung wie Eclipse (auch IDE genannt – Integrated Development Environment) verwendet werden. 1. 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 meist 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, dem 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
Abbildung 2.1: Erstellung einer Java-Anwendung
Java 6
39
2 – Die erste Java-Anwendung
Hinweis 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.
Hinweis 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 gegebenenfalls die deutsche Sprachdatei und entpacken Sie diese in das Verzeichnis ..\Language des Installationsverzeichnisses. Standardmäßig ist die deutsche Übersetzung in ConTEXT enthalten.
쮿
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 und bestätigen Sie Ihre Einstellungen.
쮿
Starten Sie ConTEXT erneut, um die deutschen Beschriftungen zu erhalten.
쮿
Klicken Sie auf den Menüpunkt EINSTELLUNGEN – UMGEBUNGSEINSTELLUNGEN und öffnen Sie dann das Register BENUTZERBEFEHLE.
쮿
Klicken Sie auf NEU und geben Sie JAVA ein. Bestätigen Sie mit OK.
40
Eingabe des Sourcecodes 쮿
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 und markieren Sie das Kontrollfeld KONSOLENAUSGABE AUFZEICHNEN.
쮿
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 (F9) übersetzen und über (F10) 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.
2.2.2 Grundelemente einer Java-Anwendung in der Übersicht Die Beispiele dieses Buchs haben in den ersten Kapiteln immer den Grundaufbau, wie er im Folgenden erläutert wird. Die Elemente des Beispiels werden nur so weit erläutert, dass ein prinzipielles Verständnis für die Arbeitsweise vorhanden ist. Umfangreichere Informationen liefern dann die entsprechenden Kapitel.
Hinweis 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. 1) 2) 3) 4) 5) 6) 7) 8) 9) 10) 11) 12) 13)
package de.jse6buch.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 ..."); } }
Listing 2.1: Beispiele\de\jse6buch\kap02\Beispiel.java
Java 6
41
2 – Die erste Java-Anwendung
Packages Die erste Zeile des Beispiels erzeugt die Zugehörigkeit der Klasse Beispiel (Zeile 2) zum Package de.jse6buch.kap02 (Zeile 1). Dies bedeutet, dass sich die Datei Beispiel.java (benannt nach der Klasse Beispiel) in einem Verzeichnis ..\de\jse6buch\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.jse6buch.kap02;
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 Angabe 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 ..."); }
Hinweis Die spezielle 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.
42
Eingabe des Sourcecodes
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. Dazu wird die Methode public Beispiel()
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\ jse6buch\kap02\. Unter Linux könnte die Verzeichnisstruktur /home/frischa/Beispiele/de/ jse6buch/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\jse6buch\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\jse6buch\kap02 befindet, ausgehend vom aktuellen Verzeichnis (C:\Beispiel). java de.jse6buch.kap02.Beispiel
Sollte jetzt die folgende Ausgabe bei Ihnen erscheinen, sieht's gut aus: Hallo Kleinkleckersdorf ... Meine erste Java-Anwendung ...
Java 6
43
2 – Die 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); }
Ist kein Anweisungsblock nötig, werden die Klammern weggelassen und es wird nur die folgende Anweisung eingerückt. 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.jse6buch.kap02
Klassen, Interfaces
Sie beginnen mit einem Großbuchstaben und auch jedes Hauptwort wird mit einem Großbuchstaben begonnen, z.B. StringBuilder
Methoden
Sie beginnen klein und jedes folgende Hauptwort wird mit einem Großbuchstaben begonnen, z.B. zeigeFarbe()
Variablen
Sie beginnen ebenfalls klein und jedes weitere Hauptwort wird mit einem Großbuchstaben begonnen, z.B. roteFarbe
Konstanten
Konstanten werden immer vollständig groß geschrieben, z.B. MEHRWERTSTEUER
Tabelle 2.1: Namenskonventionen der Java-Bezeichner
44
Übersetzen von Anwendungen
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. Für den Aufruf des Compilers gibt es einige Dinge zu beachten, die im Folgenden aufgeführt werden: 쮿
Öffnen Sie eine Konsole (Eingabeaufforderung, Shell).
쮿
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 Packages de.jse6buch.kap02 im Verzeichnis .\de\jse6buch\ kap02 zu übersetzen: javac de\jse6buch\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\jse6buch\kap02\Beispiel.java) oder absoluten (D:\Beispiele\de\jse6buch\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
Hinweis 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 6
45
2 – Die erste Java-Anwendung
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 -g:source,lines,vars
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.
-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 6 bzw. 1.6 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.
-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.
Tabelle 2.2: Compiler-Optionen
46
Übersetzen von Anwendungen
Hinweis 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 APIDokumentation 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 zu dem 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
Und die zweite Datei Dateien.txt enthält die zu übersetzenden *.java-Dateinamen: de\jse6buch\kap02\Beispiel.java de\jse6buch\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.
Java 6
47
2 – Die erste Java-Anwendung 쮿
Findet er eine *.java- und eine *.class-Datei, überprüft er, ob die *.java-Datei neuer als die *.class-Datei ist. Das ist dann der Fall, wenn 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\ jse6buch\kap02. Weiterhin benötigen Sie einige Zusatzdateien im Verzeichnis ..\Beispiele\ de\jse6buch\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\ jse6buch\util, zu setzen. Über die Option -d geben Sie das Zielverzeichnis für die *.classDateien an. javac -sourcepath . -d dist de\jse6buch\kap02\*.java
Beispiele Die Datei Beispiel.java wird übersetzt. javac de\jse6buch\kap02\Beispiel.java
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\jse6buch\kap02\Beispiel.java
Da der Sourcecode keine speziellen Spracheigenschaften von Java SE 6 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\jse6buch\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.
48
Ausführen der Anwendung
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\jse6buch\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 10 der Datei Beispiel.java befindet sich ein Syntaxfehler. Die betreffende Stelle wird mit dem Zeichen ^ markiert. de\jse6buch\kap02\Beispiel.java:10: cannot find symbol symbol : class voi location: class de.jse6buch.kap02.Beispiel public static voi main(String[] args) ^ 1 error
4. Die zu übersetzende Datei de\jse6buch\kap02\Beispiel3.java existiert nicht. error: error reading de\jse6buch\kap02\Beispiel3.java; de\jse6buch\kap02\ Beispiel3.java (Das System kann die angegebene Datei nicht finden) 1 error
5. Das in der import-Anweisung angegebene Package existiert nicht. de\jse6buch\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 (zumindest theoretisch).
Java 6
49
2 – Die erste Java-Anwendung
Hinweis 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 mehr 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)
enthält. Die Klasse sollte das Zugriffsattribut public besitzen, kann aber auch ohne Zugriffsattribut ausgeführt werden. 쮿
Die Endung *.class darf nicht angegeben werden.
쮿
Statt der Verzeichnistrennzeichen werden Punkte verwendet.
쮿
Es wird zwischen Groß- und Kleinschreibung unterschieden.
쮿
Der Aufruf des Interpreters ohne weitere Parameter gibt die möglichen Optionen und die Syntax für den Aufruf aus.
쮿
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
Tabelle 2.3: Interpreter-Optionen
50
Ausführen der Anwendung
Option
Erläuterung
-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
-showversion
Es wird die Versionsnummer angezeigt und die Ausführung fortgesetzt
-splash
Durch Übergabe einer Grafik im Format GIF, JPG oder PNG kann während des Anwendungsstarts ein Splash Screen angezeigt werden, z.B. java -splash:Willkommen.jpg. Dadurch erhalten die Anwender bei langen Startzeiten eine visuelle Rückmeldung. Über die Klasse SplashScreen kann über die Anwendung auf den SplashScreen zugegriffen werden.
-verbose
Es werden umfangreichere Meldungen beim Ausführen der Anwendung ausgegeben, z.B. die geladenen Klassen
-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.
-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 umfangreiche 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.
Tabelle 2.3: Interpreter-Optionen (Forts.)
Unterschied zwischen java und javaw Beide Interpreter führen Java-Anwendungen aus. Der Interpreter 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.
Java 6
51
2 – Die erste Java-Anwendung
Der Interpreter 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, die keine Konsole benötigen.
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/jse6buch/kap02/Beispiel/class
2. Die Klasse wurde mit der package-Anweisung de.jse6buch.kap03 erstellt, befindet sich aber im Verzeichnis ..\de\jse6buch\kap02. Der Pfad und der Package-Name müssen aber übereinstimmen. Exception in thread "main" java.lang.NoClassDefFoundError: de/jse6buch/kap02/Beispiel (wrong name: de/jse6buch/kap03/Beispiel) at....
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\jse6buch\kap02, wird der Interpreter folgendermaßen aufgerufen: java de.jse6buch.kap02.Beispiel
52
Der Klassenpfad
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
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 Java SE 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/frischa/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=
Java 6
53
2 – Die erste Java-Anwendung
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 ...
4. Verwenden Sie beim Aufruf des Interpreters die Option -jar, werden alle Klassenpfaddefinitionen überschrieben. java -jar MeinArchiv.jar
Hinweis 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.
Hinweis Das JDK 6.0 unterstützt die Verwendung von Wildcardzeichen im Klassenpfad, um alle Archive eines Verzeichnisses einzubinden. Es werden allerdings weder *.classDateien noch Unterverzeichnisse, sondern nur die JAR-Archive berücksichtigt. java -classpath .;D:\Daten\* ...
oder SET CLASSPATH=.;D:\Daten\*
Beispiel Angenommen, Sie besitzen die folgende Verzeichnisstruktur und die Klasse Beispiel befindet sich im Package de.jse6buch.kap02. C:\Beispiele\de\jse6buch\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.jse6buch.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.
54
Applets mit dem Appletviewer ausführen
Er nimmt also den Pfad C:\Beispiele, hängt die Verzeichnisstruktur an, die sich durch die Angabe des Package ergibt (C:\Beispiele\de\jse6buch\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.jse6buch.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
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\jse6buch\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\jse6buch\kap02\ AppletBeispiel (Das System kann die angegebene Datei nicht finden)
Java 6
55
2 – Die erste Java-Anwendung
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.
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\Beispiele60.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\jse6buch\.. Zum Ausführen und Übersetzen der Beispiele öffnen Sie eine Konsole im Verzeichnis C:\Beispiele und verwenden die folgenden Befehle: // zum Übersetzen aller *.java-Dateien eines Verzeichnisses javac de\jse6buch\kap02\*.java // bzw. für eine konkrete Datei javac de\jse6buch\kap02\Beispiel.java // und zum Ausführen java de.jse6buch.kap02.Beispiel
Hinweis Die Beispiele sind für die Ausführung unter der JSE 6 entwickelt worden und verwenden zum Teil auch die neuen Klassen dieser Version. Sie sollten aber in den meisten Fällen auch unter der J2SE 5.0 lauffähig sein.
56
Datenein- und -ausgabe
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 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. Für den Platzhalter %s können Zeichenketten, für %d ganze Zahlen eingesetzt werden. System.out.printf("%s", "Eine Zeichenkette\n"); System.out.printf("Herr %s ist %d Jahre alt.", "Meier", 100);
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 werden 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 eingegeben 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);
Java 6
57
2 – Die erste Java-Anwendung
2.9
Kurzes Glossar
Hot Spot Die JVM von Java SE 6 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 Java SE 6 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 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 wurde, 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.
58
Kurzes Glossar
Optionale Packages und der Erweiterungsmechanismus Einer Java SE 6-Installation können über optionale Packages, früher Standarderweiterungen, weitere Bibliotheken (Archive) hinzugefügt werden. Die Bibliotheken enthalten *.class-Dateien, 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.6.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 weitere Pfade zu Erweiterungen angeben. Mehrere Verzeichnispfade werden dabei unter Windows mit einem 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 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 Java SE 6 befinden. Damit diese neuen Versionen genutzt werden können, werden die betreffenden Bibliotheken über JAR-Archive in ein speziell dafür vorgesehenes Verzeichnis kopiert. Dieses Verzeichnis muss in diesem Fall erst angelegt werden. Erstellen Sie dazu das Verzeichnis ..\endorsed als Unterverzeichnis des Verzeichnisses [InstallJDK]\jre\lib. Das Verzeichnis für die Endorsed-Erweiterungen kann auch über die Systemvariable java.endorsed.dirs ermittelt werden, z.B. durch folgende kleine Java-Anwendung: public class SystemInfo { public static void main(String[] args) { System.out.println(System.getProperty("java.endorsed.dirs")); } }
Java 6
59
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 verwendeten Sprache, kann der Compiler diese nicht übersetzen und gibt eine Fehlermeldung aus.
3.1.1
Anweisungen und Anweisungsblöcke
Ein Programm besteht aus einer Folge von Anweisungen, die – in einer bestimmten Reihenfolge ausgeführt – die Lösung einer vorgegebenen Aufgabe ermöglichen. 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 wird in den folgenden Abschnitten noch konkreter eingegangen.
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 Kommen-
Java 6
61
3 – Grundlegende Sprachelemente
tare eine wertvolle Unterstützung bei der Verwendung Ihres Sourcecodes. In Java gibt es drei Typen von Kommentaren. 쮿
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 Zeichenfolgen /* und */ eingeschlossen.
쮿
Dokumentationskommentare können ebenfalls mehrere Zeilen einnehmen und werden hauptsächlich für die Beschreibung von Klassen und Methoden eingesetzt. Mit dem Tool Javadoc lassen sich daraus später automatisch Dokumentationen im HTML-Format generieren (vgl. dazu Kapitel »Javadoc«). Dokumentationskommentare sind in die Zeichenfolgen /** und */ einzuschließen.
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.
Hinweis Gehen Sie nicht zu sparsam, aber auch nicht zu verschwenderisch mit Kommentaren um. Das Kommentieren von trivialen Anweisungen ist beispielsweise nicht notwendig, da deren Bedeutung auch direkt aus dem Code entnommen werden kann. Der folgende Kommentar ist demnach unnötig. // hier wird die Summe aus 10 und 11 berechnet int summe = 10 + 11;
Beispiel für die Verwendung von Kommentaren /** * Die Methode summe() addiert zwei Zahlen. * Dieser Text geht ebenfalls 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 } Listing 3.1: Kommentare verwenden
62
Elemente eines Programms
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 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. 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
Tabelle 3.1: Reservierte Wörter von Java
Es gibt weitere reservierte Wörter, die in der aktuellen Version nicht genutzt werden, aber auch nicht als Bezeichner verwendet werden dürfen. byvalue
cast
const
future operator
generic
goto
inner
outer
rest
var
Tabelle 3.2: Zurzeit in Java nicht verwendete reservierte Wörter
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 zugreifen. Geben Sie beispielsweise in einer Berechnungsformel den Bezeichner einer Variablen an, wird der Inhalt (der Wert) der Variable für die Berechnung benutzt.
Java 6
63
3 – Grundlegende Sprachelemente
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 bezeichnen beispielsweise Text, TEXT und text drei unterschiedliche Programmelemente.
쮿
Durch die Verwendung von Unicode-Zeichen ist es möglich, auch Buchstaben anderer Alphabete zu nutzen. Dies ist aber wegen der schlechteren Lesbarkeit 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
als erstes Zeichen darf ein Unterstrich stehen
a1b1
das erste Zeichen ist ein Buchstabe
Dagegen sind folgende Namen keine gültigen Bezeichner: Meine Klasse
enthält ein Leerzeichen
private
dies 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 hoeheGegenstand benannt werden. Eine weitere Variable, die zum Zählen von Elementen dient, könnte zaehlerElementTyp oder anzahlElementTyp heißen. Die Verwendung eines kurzen Bezeichners wie hoehe oder anzahl sagt dagegen nichts darüber aus, worauf sich die Höhe und die Anzahl beziehen. Für die Java-Programmierung haben sich bestimmte Konventionen zur Namensgebung durchgesetzt, welche die Lesbarkeit des Quellcodes erleichtern, vgl. auch Kapitel 2.
64
Primitive Datentypen
Gültigkeitsbereiche Ein Bezeichner muss in seinem 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 Variable bzw. der Konstante angegeben. Der Datentyp bestimmt dabei die Größe des Speicherplatzes und den Wertebereich. Es gibt in Java drei elementare Datentypen – den numerischen, den logischen und den Zeichendatentyp. 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ößen und die Wertebereiche 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 Gleitpunktdatentypen. 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 unterscheiden und damit durch den Wertebereich der Zahlen, die in ihnen gespeichert werden können.
Java 6
65
3 – Grundlegende Sprachelemente
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)
Tabelle 3.3: Ganzzahlige Datentypen
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
die Dezimalzahl 95178 wird als long-Zahl gespeichert
0236
Oktalzahl 236, entspricht dezimal 158 = 2*82+3*81+6*80
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. 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
Tabelle 3.4: Gleitpunkt-Datentypen
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 Dezi-
66
Primitive Datentypen
malpunkts 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 interpretiert.
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, z.B. 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 Escapes-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. Typ
Größe
Wertebereich
char
2 Byte
Unicode Zeichen (0...65535)
Tabelle 3.5: Zeichendatentypen
Escape-Sequenzen Escape-Sequenzen sind Steuerzeichen, die beispielsweise bei der Ausgabe von Text auf der Konsole verwendet werden. Ihnen wird zur Kennzeichnung ein Backslash \ vorangestellt. In der folgenden Tabelle sind die wichtigsten Escape-Sequenzen zusammengefasst.
Java 6
67
3 – Grundlegende Sprachelemente
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
Apostroph
\\
92
Backslash
\u0041
65
\u gibt an, dass ein Zeichencode (hexadezimal) folgt, z.B. die Hexadezimalzahl 0041 für den Buchstaben A
Tabelle 3.6: Escape-Sequenzen
Beispiele char c = 'A';
Das Zeichen A wird der Variablen c zugewiesen
char c = '\\';
Das Zeichen \ wird der Variablen c mithilfe der Escape-Sequenz \\ zugewiesen
char c = '\u0088';
Der Variablen c wird über den hexadezimalen Zeichencode das Fragezeichen ? mittels einer 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
Hinweis Der Quellcode eines 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.
3.2.3
Logische Datentypen
Der Datentyp boolean kann Wahrheitswerte (logische Werte) aufnehmen. In Java gibt es dazu die beiden Literale true (wahr) und false (falsch). 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. Das Ergebnis des Vergleichs können Sie in einer Variablen vom Datentyp boolean speichern. Typ
Größe
Wertebereich
boolean
1 Byte
true, false
Tabelle 3.7: Logische Datentypen
68
Variablen und Konstanten
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.
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 werden.
쮿
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 Variable eine Klassenvariable. Sie ist somit immer verfügbar.
Java 6
69
3 – Grundlegende Sprachelemente
Hinweis 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 einzigen Anweisung zu deklarieren. Variablenname1, Variablenname2,...; // unüblich
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;
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.
70
Variablen und Konstanten
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. Instanzund 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). class VariablenTest { public static void main(String[] argv) { // Variablen deklarieren int zahl1; // Deklaration ohne Initialisierung int summe; // Deklaration ohne Initialisierung int zahl2 = 1; // Deklaration mit Initialisierung zahl1 = 4;
// Initialisierung von zahl1
Listing 3.2: Initialisierung von Variablen
Java 6
71
3 – Grundlegende Sprachelemente
// Summe berechnen summe = zahl1 + zahl2; // Ergebnis ausgeben System.out.println("Summe = " + summe); } } Listing 3.2: Initialisierung von Variablen (Forts.)
Kommentieren Sie die Zeile zahl1 = 4; aus und übersetzen Sie das Programm, erkennt der Compiler, dass der Variablen zahl1 noch kein Wert zugewiesen wurde, obwohl dieser für die Berechnung der Summe benötigt wird. Er gibt die Fehlermeldung variable zahl1 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. Ein Datentyp kann automatisch in einen anderen Datentyp umgewandelt werden, wenn er in dessen Kreis enthalten ist.
char
byte short int long float double
Abbildung 3.1: Umwandlung von Datentypen
72
Variablen und Konstanten
Hinweis 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. 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)
Hinweis Bei der reduzierenden Konvertierung werden z.B. Zahlen, die den Wertebereich des Zieldatentyps überschreiten, verfälscht. 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 Initialisie-
Java 6
73
3 – Grundlegende Sprachelemente
rung der Konstanten zu korrigieren. Wenn Sie den Zahlenwert direkt in die Formeln einsetzen, muss dagegen jede Formel gesucht und geändert werden. Das ist zeitaufwändig 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 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 inkl. Mehrwertsteuer = " + preis); } } Listing 3.3: \Beispiele\de\jse6buch\kap03\KonstantenVerwenden.java
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
74
Operatoren und Ausdrücke
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 Inkrementoperator -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 miteinander, 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++
Hinweis Auch eine einzelne Variable, eine Konstante oder ein Literal kann ein Ausdruck sein.
Auswertung von Ausdrücken In Java wird 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.
Java 6
75
3 – Grundlegende Sprachelemente
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 arithmetischen 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. 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)
Tabelle 3.8: Arithmetische Operatoren
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; // Division int a = 15 / 2;
double d = 15 / 2;
76
Ohne Klammern erfolgt die Multiplikation vor der Addition. Hier erzwingen die Klammern die vorrangige Ausführung der Addition. 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.
Operatoren und Ausdrücke
double d = 15.0 / 2; double d = 15 / 2.0;
Ist einer der beiden Operanden eine Gleitpunktzahl, ist auch das Ergebnis von diesem Typ. Die Variable d besitzt demnach den Wert 7.5.
//Division durch 0 double d = 15 / 0;
double d = 0.0 / 0.0;
Die ganzzahlige Division durch 0 erzeugt einen Fehler und führt zum Abbruch des Programms. Die Division durch 0.0 mit Gleitpunktzahlen liefert dagegen den Wert Infinity (unendlich) zurück. Werden zwei Nullwerte als Operanden angegeben, ist das Ergebnis NaN (Not a Number – keine Zahl).
// Rest der ganzzahligen Division int a = 15 % 2;
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;
i++ bzw. ++i ist 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.
double d = 15.0 / 0;
b = 2, i = 2. // Dekrementierung int i = 5; int a = i--; int i = 5; int b = --i;
i-- bzw. --i ist 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.
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;
Java 6
77
3 – Grundlegende Sprachelemente
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 Operanden 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. Java kennt sechs verschiedene Vergleichsoperatoren, die in der folgenden Tabelle aufgeführt sind. 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))) ...
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 Schiebeoperatoren 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). Schiebeoperatoren verschieben die einzelnen Bits innerhalb der Binärdarstellung einer Zahl nach rechts oder nach links. Frei werdende Stellen werden mit Nullen aufgefüllt. Operator Erläuterung ~
NEGATION (KOMPLEMENT) Unärer Operator, alle Bits des Operanden werden negiert
Regeln der Komplement-Bildung: 0 1 1 0
&
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 UND-Verknüpfung: 1&1 1 0&1 0 1&0 0 0&0 0
Tabelle 3.11: Bitweise Operatoren
80
Operatoren und Ausdrücke
Operator Erläuterung |
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 ODER-Verknüpfung: 1&1 1 0&1 1 1&0 1 0&0 0
^
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 ODER-Verknüpfung: 1&1 0 0&1 1 1&0 1 0&0 0
>>
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 Rechtsverschie- >> 2 bung um zwei Stellen durchgeführt. Die Variable i hat nach der Operation den Wert 3.
Java 6
OR
0011 0000 (48) 1100 1111 (-49)
0011 0011 (51) 0001 1010 (26) 0011 1011 (59)
1000 0010
(8) (3)
81
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 wird 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.
Hinweis Setzen Sie hinter dem logischen Ausdruck kein Semikolon. Dies würde die if-Anweisung beenden und die folgende Anweisung wird immer ausgeführt.
82
Steuerung des Programmflusses
Beispiel 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 (die Zahl ist ohne Rest durch 7 teilbar) 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
Abbildung 3.2: if-Anweisung 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!"); } } Listing 3.4: \Beispiele\de\jse6buch\kap03\Lotterie.java
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.
Java 6
83
3 – Grundlegende Sprachelemente
Syntax der if-else-Anweisung if(Ausdruck) Anweisung; else Anweisung;
Der erste Teil 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 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.
Beispiel Das Lotterie-Beispiel soll nun so verändert werden, dass immer eine Ausgabe erfolgt, auch wenn kein Sonderpreis gewonnen wurde. Dies wird durch Hinzufügen des elseZweigs realisiert. Losnummer durch 7 teilbar ? ja
nein
Ausgabe: Sonderpreis
Ausgabe: kein Preis
weitere Anweisungen
Abbildung 3.3: if-else-Anweisung 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) Listing 3.5: \Beispiele\de\jse6buch\kap03\Lotterie2.java
84
Steuerung des Programmflusses
System.out.println("Gratulation, Sie haben einen " + "Sonderpreis gewonnen!"); else System.out.println("Schade, Sie haben keinen " + "Sonderpreis gewonnen!"); } } Listing 3.5: \Beispiele\de\jse6buch\kap03\Lotterie2.java (Forts.)
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. if(Ausdruck) if(Ausdruck) Anweisung; else Anweisung;
// innere if-Anweisung // else gehört zur inneren if-Anweisung!
Bei geschachtelten if-else-Anweisungen wird der else-Zweig aber immer der letzten ifAnweisung zugeordnet, die noch keinen else-Zweig besitzt. Die Einrückung des Programmcodes ist hier also 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.
Beispiel 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.
Java 6
85
3 – Grundlegende Sprachelemente
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!"); else System.out.println("Schade, Sie haben keinen " + "Sonderpreis gewonnen!"); } else System.out.println("Falsche Losnummer, " + "die Nummer muss 5 Stellen haben"); } } Listing 3.6: \Beispiele\de\jse6buch\kap03\Lotterie3.java
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: int max, a = 10, b = 11; 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;
86
Steuerung des Programmflusses
Syntax des Bedingungsoperators Ergebnis = Ausdruck ? Anweisung1: Anweisung2;
Als Bedingung wird ein logischer 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.
Beispiel 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. 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); } } Listing 3.7: \Beispiele\de\jse6buch\kap03\Maximum.java
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 können 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 switchAnweisung folgt.
Java 6
87
3 – Grundlegende Sprachelemente
Selector =
Wert 1
Wert 2
Anweisung(sblock)
Anweisung(sblock) ° ° °
Wert x
Default
Anweisung(sblock)
Anweisung(sblock)
weitere Anweisungen
Abbildung 3.4: switch-Anweisung
Syntax der switch-Anweisung switch(Selektor) { 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 möglichen 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 angegeben. Möchten Sie die switchAnweisung 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 defaultZweig verwenden.
88
Steuerung des Programmflusses
Beispiel Das Programm gibt zu einer eingegebenen Note die Textform aus. Zunächst wird die Note über die Konsole eingelesen und in der Variablen note gespeichert. Die Variable note ist der Selektor der switch-Anweisung. In Abhängigkeit vom Wert der Variablen note 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. import java.util.Scanner; public class Noten { public static void main(String[] argv) { int note; System.out.print("Geben Sie Ihre Note ein: "); Scanner sc = new Scanner(System.in); note = sc.nextInt(); switch(note) { case 1: System.out.println("1 entspricht sehr gut"); break; 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"); } } } Listing 3.8: \Beispiele\de\jse6buch\kap03\Noten.java
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.
Java 6
89
3 – Grundlegende Sprachelemente
Beispiel 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. 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: 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"); } } } Listing 3.9: \Beispiele\de\jse6buch\kap03\TageProMonat.java
90
Steuerung des Programmflusses
Hinweis 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 dabei 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
Abbildung 3.5: Schleifen unterscheiden sich durch die Art der Wiederholungen
Zählergesteuerte Wiederholung
Laufvariable < Endwert? ja
nein
Anweisung
Anweisung
Anweisung
weitere Anweisungen
Abbildung 3.6: Zählergesteuerte Wiederholungen
Java 6
91
3 – Grundlegende Sprachelemente
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.
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 for-Schleife deklariert werden, dürfen nicht bereits als lokale Variable 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 erfolgt 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. Eine solche 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. Der Aktualisierungsteil könnte dann leer bleiben.
Beispiel 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.
92
Steuerung des Programmflusses
public class Summe { public static void main(String[] argv) { int summe100 = 0; final int MAX = 100; for(int i = 1; i 1000) || (zahl < -1000)) { System.out.println("ungueltige Eingabe: " + "Die Zahl muss zischen -1000 und 1000 liegen"); continue; } summe = summe + zahl; } System.out.println("Summe der eingegebenen Zahlen: " + summe); } } Listing 3.14: \Beispiele\de\jse6buch\kap03\Addiere2.java (Forts.)
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 breakAnweisung verwendet, wird nur die innerste Schleife verlassen. 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 break-Anweisung 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 allerdings nicht noch einmal initialisiert.
쮿
Wird eine break-Anweisung mit einem Label verwendet, wird die Anweisung abgearbeitet, die dem benannten Anweisungsblock folgt.
Java 6
99
3 – Grundlegende Sprachelemente
Auch wenn es vielleicht den Anschein erweckt, kann die benannte break-Anweisung nicht mit der goto-Anweisung anderer Programmiersprachen gleichgesetzt werden. Goto-Anweisungen erlauben Sprünge an fast beliebige Stellen im Programm bzw. in einer Methode. Mithilfe der break-Anweisung sind aber nur gezielte Sprünge an das Ende einer Schleife oder eines Anweisungsblocks möglich.
Beispiel 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 dann, wenn die Schleifen bis zum Ende abgearbeitet werden. 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!"); } } } Listing 3.15: \Beispiele\de\jse6buch\kap03\Summe2.java
Hinweis Gehen Sie so sparsam wie möglich mit benannten break-Anweisungen um. Sie können die Lesbarkeit und Übersichtlichkeit des Programms sehr beeinträchtigen.
100
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 stellen wir auf einfache Weise die Sprachmöglichkeiten zur objektorientierten Programmierung mit Java vor.
4.1.1
Klassen definieren Baupläne
Ohne Klassen geht nichts in Java. Bis auf Kommentare, die package- und die importAnweisung 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 zugegriffen werden kann. Diese Schnittstelle bilden die Methoden der Klasse. Ein direkter Zugriff auf die Daten wird in der Regel nicht erlaubt. Diese Kapselung bewirkt, dass die Änderung der Daten kontrolliert durchgeführt wird. 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.
Java 6
101
4 – Klassen, Interfaces und Objekte
Klasse
Abbildung 4.1: Klasse mit Datenelementen und Schnittstelle
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 oder ihn in derselben Farbe lackieren lassen. 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
4 8
Abbildung 4.2: Klasse mit Objekten
102
0
7
Einfache Klassen
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 (Generalisierung) und speziellere Klassen davon ableiten (Spezialisierung). Es entstehen dadurch 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 nämlich die Übersichtlichkeit), und dass Änderungen leichter durchführbar sind (ändern Sie etwas in einer übergeordneten 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. Die abgeleiteten Klassen (auch Subklassen genannt) erben die Funktionalität der Basisklasse. Die Basisklasse wird auch als Superklasse oder übergeordnete Klasse bezeichnet.
Basisklasse
abgeleitete Klasse
abgeleitete Klasse
Abbildung 4.3: Klassenhierarchien durch Vererbung
Hinweis Variablen innerhalb einer Klasse werden auch als Member oder Felder bezeichnet. Wir verwenden hier oft einfach nur den Begriff Variable, unabhängig davon, ob sich diese auf einen primitiven Datentyp wie int oder ein Objekt bezieht. Eine echte Unterscheidung besteht tatsächlich zwischen Instanz- und Klassenvariablen. Während Erstere mit einem Objekt verbunden sind, existieren Letztere nur einmal innerhalb der Klasse, unabhängig von einem konkreten Objekt.
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, d.h., ohne eine Klasse können Sie keine Java-Anwendung erstellen.
Java 6
103
4 – Klassen, Interfaces und Objekte
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; } }
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. Der Dateiname (ohne Endung) muss mit dem Namen der public-Klasse identisch sein.
쮿
Klassennamen sollten immer groß geschrieben werden. Besteht der Klassenname aus mehreren Wörtern, wird jedes Hauptwort mit einem Großbuchstaben begonnen (Pascalschreibweise). 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.
104
Objekte 쮿
Klassen mit dem Zugriffsattribut public können von allen anderen Klassen genutzt werden. Besitzt eine Klasse kein Zugriffsattribut, kann sie nur innerhalb des betreffenden Packages 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 erzeugen, 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 (es referenziert). Es können auch mehrere Variablen das gleiche Objekt referenzieren. Mathe m = new Mathe(); // oder eine getrennte Deklaration und Initialisierung Mathe m; m = new Mathe();
Hinweis 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. 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 ein neues Objekt zu, indem Sie new, den Namen der Klasse und ein Klammerpaar angeben. Sie können beide Anweisungen auch zu einer zusammenfassen.
Java 6
105
4 – Klassen, Interfaces und Objekte
; = new (); // oder = new ();
Über die Objektvariable lässt sich auf die öffentlichen Methoden und Variablen einer Klasse zugreifen.
Objekterzeugung Durch die folgende Anweisung wird einer Variablen ein Objekt vom Typ der festgelegten 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 ... ;
Der Zugriff 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. ein Methoden- oder Variablenname, 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 dagegen 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()
106
Objekte
{ 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();
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 Collectors, 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
Java 6
107
4 – Klassen, Interfaces und Objekte
ihr belegte Speicher freigegeben. Diese Arbeit wird aber nicht mehr vom GC erledigt, sondern vom Java-Laufzeitsystem.
Über this das eigene Objekt referenzieren 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 verwendete 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 in Methoden genutzt, bei denen die Parameter denselben Namen besitzen wie die Variable des Objekts. class ZahlenSpeicher { private int zahl; public void setZahl(int zahl) { this.zahl = zahl; } }
쮿
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 von Methoden 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, gegebenen-
108
Methoden
falls in mehreren Klassen, zu verteilen. Eine Methode sollte genau eine bestimmte Aufgabe erfüllen. Dadurch kann eine Methode von mehreren Stellen einer Anwendung genutzt werden. Methoden definieren sozusagen 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 Maß 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 Sie 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 Wertes 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.
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 dann ein Klammerpaar.
쮿
Innerhalb der Methode, im Methodenrumpf, 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.
Java 6
109
4 – Klassen, Interfaces und Objekte 쮿
Wenn Sie einen anderen Rückgabetyp als void verwenden, muss mindestens eine return-Anweisung am Ende der Methode angegeben werden, die einen Wert dieses Typs 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 Zweig über eine returnAnweisung 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 ;] } }
Hinweis 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.
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 für die Variable k2 der Wert null ist. Letztendlich müssen lokale Variablen wie k2Local sofort initialisiert werden, weil Anweisungen wie k2Lokal.macheNix(); mit einem Compilerfehler quittiert werden.
110
Methoden
class Klasse1 { Klasse2 k1 = new Klasse2(); Klasse2 k2; int getZahl() { return 10; } void test() { int i = getZahl(); // Rückgabewert wird i zugewiesen getZahl(); // Rückgabewert wird nicht verwendet k1.macheNix(); k2.macheNix(); // Laufzeitfehler, nicht initialisiert Klasse2 k2Lokal; k2Lokal.macheNix();
// Compilerfehler, nicht initialisiert
} } class Klasse2 { void macheNix() {} }
Beispiel Im Konstruktor Methoden1() wird die Methode sum() aufgerufen, welche die Summe zweier Zahlen berechnet und das Ergebnis ausgibt. 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)); Listing 4.1: \Beispiele\de\jse6buch\kap04\Methoden1.java
Java 6
111
4 – Klassen, Interfaces und Objekte
} public static void main(String[] args) { new Methoden1(); } } Listing 4.1: \Beispiele\de\jse6buch\kap04\Methoden1.java (Forts.)
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 gerade sinnvoll 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 festlegen. 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 diesen Methoden kann auf Instanzvariablen dann nur über this zugegriffen werden. Lokale Variablen und Parameternamen müssen dagegen immer verschieden sein.
쮿
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 an die Methode übergeben werden. Sie werden hinter dem Methodennamen in Klammern geschrieben.
112
Methoden
Hinweis 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, da er nicht ausgewertet werden muss und der Compiler deshalb die Methoden nicht unterscheiden könnte.
Ä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 hier also tatsächlich auf das originale Objekt aus.
Beispiel 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. 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); } public void sum(int zahl1, int zahl2, Zahl zahl3) { zahl1 = 10; Listing 4.2: \Beispiele\de\jse6buch\kap04\Methoden2.java
Java 6
113
4 – Klassen, Interfaces und Objekte
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; } Listing 4.2: \Beispiele\de\jse6buch\kap04\Methoden2.java (Forts.)
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 bei gleicher Eignung zuerst die Methoden mit fester Parameterliste 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));
114
Methoden
Beispiel 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. 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(); } } Listing 4.3: \Beispiele\de\jse6buch\kap04\VarArgs.java
Java 6
115
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 Parametertypen 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)
Hinweis 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 double-Typ zuweisen. Der Wert 10 wird automatisch (und verlustfrei, sonst geht es nicht) in den Typ double konvertiert (gecastet).
Beispiel Die Methode add() wird überladen, damit sie einmal mit int- und ein weiteres Mal mit double-Parametern aufgerufen werden kann. 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 add(double zahl1, double zahl2) { return zahl1 + zahl2; } Listing 4.4: \Beispiele\de\jse6buch\kap04\Ueberladen.java
116
Methoden
public static void main(String[] args) { new Ueberladen(); } } Listing 4.4: \Beispiele\de\jse6buch\kap04\Ueberladen.java (Forts.)
Rekursion Für den rekursiven Aufruf von Methoden gibt es kein spezielles Sprachmittel in Java. Da dies aber eine immer wiederkehrende Aufgabe ist, soll sie 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.
Hinweis 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). 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; } Listing 4.5: \Beispiele\de\jse6buch\kap04\Rekursion.java
Java 6
117
4 – Klassen, Interfaces und Objekte
public static void main(String[] args) { new Rekursion(); } } Listing 4.5: \Beispiele\de\jse6buch\kap04\Rekursion.java (Forts.)
Beispiel 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
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. Dadurch stehen verschiedene Möglichkeiten zur Verfügung, ein Objekt einer Klasse zu initialisieren.
118
Konstruktoren und Destruktoren 쮿
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, sonst 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.
쮿
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.
쮿
Dem Konstruktor können Sie in Klammern Parameter übergeben. In diesem Fall wird zur Objekterzeugung der Konstruktor mit der passenden Parameterliste 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.
Java 6
119
4 – Klassen, Interfaces und Objekte
Beispiel class KlassenName { { Anweisungen; } }
// Beginn des Instanzinitialisierers // Ende
Beispiel 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. 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(); } } Listing 4.6: \Beispiele\de\jse6buch\kap04\Konstruktor.java
120
Konstruktoren und Destruktoren
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 erzeugt. Diese Methoden werden auch als Fabrikmethoden bezeichnet (factory methods).
Beispiel 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. 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(); } } Listing 4.7: \Beispiele\de\jse6buch\kap04\Fabrik.java
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 (d.h. wenn es keiner mehr benutzt).
Java 6
121
4 – Klassen, Interfaces und Objekte
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. Das 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 das 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.
쮿
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 nicht mehr referenzierten Objekte aufgerufen wird. 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();
Hinweis Es gibt sicher noch Anwendungsgebiete, in denen die Methode finalize() sinnvoll ist. 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.
122
Konstruktoren und Destruktoren
Wie sieht nun die Lösung für die Implementierung eines eigenen Destruktors aus? Implementieren Sie eine eigene Cleanup-Methode (Säuberungsmethode) und rufen Sie sie manuell auf, wenn Sie ein 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.
Beispiel Nach dem Erstellen des Destruktor-Objekts in der Methode erzeugeObjekt() 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. 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. Die Auslagerung der Anweisungen zum Erzeugen des Destruktor-Objekts in eine eigene Methode ist notwendig, da die Lebensdauer des Objekts beim Verlassen der Methode endet und es dadurch vom GC entsorgt werden kann. public class Destruktor { protected void finalize() { System.out.println("Finalize"); } public void dispose() { System.out.println("Aufräumen"); } private static void erzeugeObjekt() { Destruktor d = new Destruktor(); d.dispose(); } public static void main(String[] args) { erzeugeObjekt(); // System.runFinalization(); Listing 4.8: \Beispiele\de\jse6buch\kap04\Destruktor.java
Java 6
123
4 – Klassen, Interfaces und Objekte
//
System.gc(); }
} Listing 4.8: \Beispiele\de\jse6buch\kap04\Destruktor.java (Forts.)
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 Überprüfung der Werte vor der Zuweisung an eine Variable.
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.
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 mehr abgeleitet werden.
package-sichtbar 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 Packages mit demselben Namen müssen sich nicht notwendigerweise im gleichen Verzeichnis befinden. Tabelle 4.1: Übersicht der Zugriffsattribute
Hinweis 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.
124
Statische Klassenelemente
4.7
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 eine 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 den Funktionsaufrufen keine weiteren Operationen ausgeführt werden, die für verschiedene Objekte unterschiedlich wären. 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 kann,
쮿
die Bereitstellung von allgemeinen Methoden, die nicht an ein spezielles Objekt gebunden sind, wie z.B. mathematische Funktionen,
쮿
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 der 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;
Java 6
125
4 – Klassen, Interfaces und Objekte
} 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 solcher Blöcke, werden diese in der definierten Reihenfolge ausgeführt.
Beispiel public class Mathe { private static int i; static { i = 10; } }
126
Aufzählungstypen mit Enum
Beispiel 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. 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(); } } Listing 4.9: \Beispiele\de\jse6buch\kap04\Statisch.java
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;
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.
Java 6
127
4 – Klassen, Interfaces und Objekte
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 };
Hinweis 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). Der über den Compiler erzeugte Bytecode kann über einen Decompiler (z.B. den DJ Java Decompiler, zu beziehen unter http://www.kpdus.com/jad.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. 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) Listing 4.10: Vom Compiler generierter Code für die Aufzählung Noten
128
Aufzählungstypen mit Enum
{ 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 }); } } Listing 4.10: Vom Compiler generierter Code für die Aufzählung Noten (Forts.)
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 von einer 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 übergeben werden. Aufzählungen können, wie normale Klassen, Konstruktoren, Methoden, Variablen und Konstanten beinhalten. [Attribut] enum [implements Interface] { KONSTANTE1, KONSTANTE2, ...; // oder mit Parametern
Java 6
129
4 – Klassen, Interfaces und Objekte
KONSTANTE1(...), KONSTANTE2(...), ...; // Methoden // Konstruktoren // Variablen und Konstanten }
Aufzählungstypen können auch in switch-Anweisungen verwendet werden. Sie dürfen sich innerhalb einer anderen Klasse befinden und Methoden, Konstruktoren, sowie Konstanten-abhängige Methoden enthalten. Im Folgenden wird je ein Beispiel dazu vorgestellt.
Beispiel 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. Über eine 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. 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 + ": " + n2.ordinal()); Noten nSwitch = Noten.valueOf("EINS"); switch(nSwitch) { case EINS: System.out.println(">>EINS"); break; case ZWEI: System.out.println(">>ZWEI"); break; } } public static void main(String[] args) { new Aufzaehlungen1(); Listing 4.11: \Beispiele\de\jse6buch\kap04\Aufzaehlungen1.java
130
Aufzählungstypen mit Enum
} } enum Noten { EINS, ZWEI, DREI, VIER, FUENF }; class Wrapper { enum Noten { ONE, TWO, THREE, FOUR, FIVE }; } Listing 4.11: \Beispiele\de\jse6buch\kap04\Aufzaehlungen1.java (Forts.)
Als Ergebnis wird die folgende Ausgabe erzeugt: EINS ZWEI DREI VIER FUENF ONE: 0 TWO: 1 THREE: 2 FOUR: 3 FIVE: 4 >>EINS
Beispiel 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. public class Aufzaehlungen2 { public Aufzaehlungen2() { for(Fahrzeuge fz: Fahrzeuge.values()) System.out.println(fz + ": " + fz.getPS()); } public static void main(String[] args) { new Aufzaehlungen2(); } Listing 4.12: \Beispiele\de\jse6buch\kap04\Aufzaehlungen2.java
Java 6
131
4 – Klassen, Interfaces und Objekte
} enum Fahrzeuge { BMW(110), AUDI(100), OPEN(50); private int ps; Fahrzeuge(int ps) { this.ps = ps; } public int getPS() { return ps; } } Listing 4.12: \Beispiele\de\jse6buch\kap04\Aufzaehlungen2.java (Forts.)
Es wird die folgende Ausgabe erzeugt: BMW: 110 AUDI: 100 OPEL: 50
Beispiel 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 switch-Anweisung 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 bereits der Compiler einen Fehler. Für beide Klassen werden die enthaltenen Konstanten durchlaufen und die Methode rechne() wird konstantenspezifisch ausgeführt. public class Aufzaehlungen3 { public Aufzaehlungen3() { for(MinMax mm: MinMax.values()) Listing 4.13: \Beispiele\de\jse6buch\kap04\Aufzaehlungen3.java
132
Aufzählungstypen mit Enum
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; } } } 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); } Listing 4.13: \Beispiele\de\jse6buch\kap04\Aufzaehlungen3.java (Forts.)
Als Ausgabe erhalten Sie: MINIMUM MAXIMUM MINIMUM MAXIMUM
Java 6
von von von von
10 10 10 10
und und und und
11 11 11 11
ist ist ist ist
10 11 10 11
133
4 – Klassen, Interfaces und Objekte
4.9
Vererbung
4.9.1
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 Klasse 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()
Hinweis Seit der Java-Version 5.0 (J2SE 5.0) werden so genannte kovariante Rückgabetypen unterstützt. Sie müssen beim Überschreiben von clone() nicht mehr zwingend ObjectTypen 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 ihrer verwalteten Daten ü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()
134
Vererbung
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()
Beispiel 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 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. 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) Listing 4.14: \Beispiele\de\jse6buch\kap04\ObjectMethoden.java
Java 6
135
4 – Klassen, Interfaces und Objekte
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; } } 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; Listing 4.14: \Beispiele\de\jse6buch\kap04\ObjectMethoden.java (Forts.)
136
Vererbung
return i + sum; } public static void main(String[] args) { new ObjectMethoden(); } } Listing 4.14: \Beispiele\de\jse6buch\kap04\ObjectMethoden.java (Forts.)
Die Ausgabe ist hier: 1 4 3 ------------1 2 5 ObjectMethoden: 10, [1, 4, 3] ------------Sie sind gleich!
4.9.2
Klassen ableiten
Bei der Vererbung erbt eine abgeleitete Klasse alle public-, protected- und package-sichtbaren 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 und kann nicht mehr direkt verwendet werden. 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.
class Unterklasse extends Basisklasse { }
Java 6
137
4 – Klassen, Interfaces und Objekte
Beispiel 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. 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"); Listing 4.15: \Beispiele\de\jse6buch\kap04\Vererbung.java
138
Vererbung
} } Listing 4.15: \Beispiele\de\jse6buch\kap04\Vererbung.java (Forts.)
4.9.3
Konstruktoraufrufe
Wird 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. Dies liegt auch daran, dass Konstruktoren nicht vererbt werden. Sie müssen alle benötigten 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.
Beispiel 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.
Java 6
139
4 – Klassen, Interfaces und Objekte
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)"); } } Listing 4.16: \Beispiele\de\jse6buch\kap04\VererbKonstruktor.java
140
Vererbung
Die Ausgabe ist hier: 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)
Hinweis 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();
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 dann 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 sei eine von Ebene1 abgeleitete Klasse. class Ebene1 {} class Ebene2 extends Ebene1
Java 6
141
4 – Klassen, Interfaces und Objekte
{} Ebene1 e1 = new Ebene2(); Ebene2 e2 = (Ebene2)new Ebene1(); // Laufzeitfehler
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. Möchten Sie vor der Zuweisung von Objekten deren Zuweisungsverträglichkeit prüfen, können Sie den Operator instanceof verwenden. if(e1 instanceof Ebene2) System.out.println("e1 ist zu Ebene2-Objekten zuweisbar."); if(e1.getClass() == Ebene2.class) System.out.println("Ok");
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.
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 java.lang.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 { }
142
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 MP3-fähigen 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, welche eine Klasse besitzen muss, die dieses Interface implementiert (eine Art Vorgabe). 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. Ein Objekt einer solchen Klasse kann einer Variablen vom Typ der Schnittstelle zugewiesen werden. Die Schnittstellenmethoden können damit auch über eine Variable vom Typ des Interfaces aufgerufen werden. Anders betrachtet können Sie über Variablen vom Typ des Interfaces die Schnittstellenmethoden verschiedener Klassen aufrufen. 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 } }
Java 6
143
4 – Klassen, Interfaces und Objekte
Die folgenden Anweisungen verwenden Sie nun, 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. // statt einen Objekt der Klasse wird ein Objekt vom Interface-Typ // verwendet 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 aber Variablen vom Typ eines Interfaces verwenden und ihnen Objekte von Klassen zuweisen, die das Interface implementieren. Über diese Variablen können Sie dann die Methoden der Klassen aufrufen, die durch das Interface vorgegeben sind. Dies wurde im gezeigten Beispiel realisiert.
Beispiel 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 das Voranstellen des Interfacenamens erfolgen.
144
Interfaces
interface 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"); } } Listing 4.17: \Beispiele\de\jse6buch\kap04\Schnittstellen1.java
Syntax Geben Sie das Attribut public an, wenn Sie ein öffentliches Interface definieren möchten. 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 durch ein oder mehrere andere Interfaces erweitert werden. Geben Sie in diesem Fall das Schlüsselwort extends und die Namen der Interfaces, durch Kommata getrennt, 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. Damit müssen die Methoden beim Implementieren in einer Klasse ebenfalls als public deklariert werden. Ein Interface beschreibt also immer die (bzw. eine von vielen) öffentliche Schnittstelle einer Klasse. Statt eines Methodenrumpfs wird hinter der Methodendeklaration nur ein Semikolon angefügt. [public] interface [extends , [ ...]] { [public] [static] [final] ; [public] [abstract] (); }
Java 6
145
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. Eine Klasse kann zwar nur von einer anderen Klasse erben, sie kann aber 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 , {}
Mit dem Operator instanceof lässt sich prüfen, ob ein Objekt ein bestimmtes Interface implementiert. if(BezeichnerEinesObjekts instanceof InterfaceTyp) ...
Hinweis Interfaces müssen nicht unbedingt Methoden besitzen. In diesem Fall dienen sie lediglich zur Markierung einer Klasse, dass diese 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 (ihren Zustand speichern) können.
Probleme bei der Vererbung von Interfaces Wenn Sie Interfaces vererben, kann es zu Mehrdeutigkeiten kommen. Ein abgeleitetes Interface erbt alle Konstanten und Methoden seines Basisinterfaces. So kann es passieren, dass ein Interface über verschiedene Wege eine gleichnamige Konstante oder Methode erbt.
Beispiel Das Interface K4 erbt hier 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. interface K1 { int KONSTANTE1 = 1; int KONSTANTE2 = 20; void ausgabe(); Listing 4.18: \Beispiele\de\jse6buch\kap04\Schnittstellen2.java
146
Interfaces
} 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(); } } Listing 4.18: \Beispiele\de\jse6buch\kap04\Schnittstellen2.java (Forts.)
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 Interfaces die gleichnamige(n) Konstanten des/der Basisinterfaces. Deshalb führt der Zugriff über die verschiedenen Interfaces bei der Ausgabe nicht zu einem Fehler.
Java 6
147
4 – Klassen, Interfaces und Objekte 쮿
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.
쮿
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 Interfaces 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 sämtliche 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 jetzt 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 die einfache Vererbung unterstützt, ist die 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.
Beispiel 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 eine Methode der Adapterklasse neu. public class Adapter extends AdresseAdapter { public Adapter() { System.out.printf("%s %s", getVorname(), getName()); } public String getVorname() Listing 4.19: \Beispiele\de\jse6buch\kap04\Adapter.java
148
Abstrakte Klassen und Methoden
{ return "Dagobert"; } public static void main(String[] args) { new Adapter(); } } interface Adresse { String getName(); String getVorname(); } class AdresseAdapter implements Adresse { public String getName() { return "Duck"; } public String getVorname() { return "Donald"; } } Listing 4.19: \Beispiele\de\jse6buch\kap04\Adapter.java (Forts.)
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 jedoch nicht möglich, von abstrakten Klassen Objekte zu erzeugen. Den Versuch quittiert bereits der Compiler mit einer Fehlermeldung.
Java 6
149
4 – Klassen, Interfaces und Objekte
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); } }
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 Interfaces 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 Methoden enthalten dann statt eines Rumpfs in geschweiften Klammern nur die Angabe eines Semikolons.
150
Abstrakte Klassen und Methoden
[public] abstract class { [Attribut] abstract (); }
Hinweis 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.
Beispiel 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 (hier 000011). 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); } } Listing 4.20: \Beispiele\de\jse6buch\kap04\Abstrakt.java
Java 6
151
4 – Klassen, Interfaces und Objekte
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 wie eine Methode der Basisklasse besitzt. Damit wird die Methode der Basisklasse überschrieben (neu implementiert).
쮿
Es können nur Methoden mit den Zugriffsattributen public, protected und packageSichtbarkeit ü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.
쮿
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.
Hinweis Private Methoden einer Klasse können nicht in einer abgeleiteten Klasse überschrieben werden. Erstellen Sie in diesem Fall eine neue Methode. Der Aufruf einer privaten Methode der Basisklasse über super() ist ebenfalls nicht möglich.
Beispiel 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. public class Ueberschreiben extends FormatierteAusgabe { void ausgabeSpecial(int zahl) Listing 4.21: \Beispiele\de\jse6buch\kap04\Ueberschreiben.java
152
Polymorphie
{ 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); } } Listing 4.21: \Beispiele\de\jse6buch\kap04\Ueberschreiben.java (Forts.)
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.
Java 6
153
4 – Klassen, Interfaces und Objekte
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() für die gleiche Variable (vom Typ des Interfaces oder der abstrakten Klasse) aufgerufen haben, ist die Ausgabe unterschiedlich, je nach Typ des gespeicherten Objekts. 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.
Beispiel 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. Diesem werden jeweils ein AusgabeVariante1- und ein AusgabeVariante2-Objekt hinzugefügt. Anschließend wird die Methode print() für die beiden Array-Elemente auf die gleiche Weise 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. 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 { Listing 4.22: \Beispiele\de\jse6buch\kap04\Polymorph.java
154
Innere, verschachtelte und lokale Klassen
void print(); } class AusgabeVariante1 implements DatumAusgabe { public void print() { System.out.println("12.10.2006"); } } class AusgabeVariante2 implements DatumAusgabe { public void print() { System.out.println("12/10/2006"); } } Listing 4.22: \Beispiele\de\jse6buch\kap04\Polymorph.java (Forts.)
Die Ausgabe ist: 12.10.2006 12/10/2006
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.
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 Packages »verstecken«. Die Methoden der inneren Klassen können auf alle Elemente der äußeren Klasse zugrei-
Java 6
155
4 – Klassen, Interfaces und Objekte
fen, 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 Objekte der inneren Klasse auch nur innerhalb einer äußeren Klasse erzeugen. Für diese Aufgabe wird der Operator new über das Objekt der äußeren Klasse verwendet.
Beispiel Damit ein Objekt der Klasse Innen erzeugt werden kann, ist immer ein Objekt der Klasse Aussen notwendig. Dadurch ergeben sich einige neue Schreibweisen zur Objekterzeugung wie new Aussen().new Innen() oder au.new Innen(). public class InnenAussenTest { public static void main(String[] args) { // Objekterzeugung außerhalb der Klasse Aussen Aussen.Innen i = new Aussen().new Innen(); // oder Aussen au = new Aussen(); Aussen.Innen in = au.new Innen(); } } class Aussen { // Objekterzeugung innerhalb der Klasse Aussen public void Test() { Innen i = new Aussen().new Innen(); // oder Aussen au = new Aussen(); Innen in = au.new Innen(); } public class Innen { } }
Für innere Klassen werden separate *.class-Dateien mit einem besonderen Namen erzeugt. So wird der äußere Klassenname vom Namen der inneren Klasse durch ein $Zeichen getrennt, z.B. Aussen$Innen.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
156
Innere, verschachtelte und lokale Klassen
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, das diese Vorgehensweise nutzt, 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. 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.
Hinweis 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.
Java 6
157
4 – Klassen, Interfaces und Objekte
Beispiel Dieses Beispiel definiert zunächst ein Interface StringIterator, welches zwei Methoden besitzt. Die Methoden sollen das nächste bzw. das vorige Element eines Stringarrays liefern. Die Klasse StringArray verwaltet ein Feld, welches aus fünf Zeichenketten (den Zahlen 1 bis 5) besteht (weitere Informationen zu Arrays finden Sie in Kapitel 6). Sie besitzt nur die eine Methode getStringIterator(), die ein Objekt vom Typ der inneren Klasse SI zurückgibt. Dieses Objekt wird verwendet, um die Elemente des Arrays vorwärts oder rückwärts zu durchlaufen und auszugeben. Weiterhin besitzt die Klasse StringArray eine innere Klasse SI, die das Interface StringIterator implementiert. Der Vorteil der Verwendung der inneren Klasse besteht nun 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 Feldes unabhängig voneinander durchlaufen. Der Parameter pos steuert, ob die Liste von vorn oder von hinten durchlaufen werden soll. 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 { index = feld.length; Listing 4.23: \Beispiele\de\jse6buch\kap04\InnereKlassen.java (Auszug)
158
Innere, verschachtelte und lokale Klassen
return null; } } public String getPrev() { index--; if(index >= 0) return feld[index]; else { index = -1; return null; } } } public StringIterator getStringIterator(boolean pos) { return new SI(pos); } } Listing 4.23: \Beispiele\de\jse6buch\kap04\InnereKlassen.java (Auszug) (Forts.)
Zum Test wird nun im Konstruktor der Klasse InnereKlassen ein Objekt vom Typ StringArray erzeugt. Über dessen Methode getStringIterator() werden zwei StringIteratorObjekte zurückgegeben, welche die Elemente einmal vorwärts und einmal rückwärts durchlaufen. 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); } Listing 4.24: \Beispiele\de\jse6buch\kap04\InnereKlassen.java (Auszug)
Java 6
159
4 – Klassen, Interfaces und Objekte
public static void main(String[] args) { new InnereKlassen(); } } Listing 4.24: \Beispiele\de\jse6buch\kap04\InnereKlassen.java (Auszug) (Forts.)
4.16 Anonyme Klassen Die letzte Steigerungsstufe sind die anonymen Klassen. Dies sind unbenannte innere Klassen. Sie verbinden die Klassendefinition direkt mit der Objekterzeugung. Anonyme Klassen besitzen keinen Namen und können deshalb später auch nicht mehr angesprochen werden. 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 wird oft in der Ereignisbehandlung in Grafikanwendungen verwendet.
Hinweis 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.
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 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() { public int getVersion()
160
// hier beginnt die anonyme Klasse
Anonyme Klassen
{ 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 }
Beispiel 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. Die Klasse LogAusgabe besitzt eine Methode ausgeben(), die ein Objekt vom Typ des Interfaces PrintOut entgegennimmt und die Methode ausgabe() aufruft. 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.
Java 6
161
4 – Klassen, Interfaces und Objekte
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(); } } Listing 4.25: \Beispiele\de\jse6buch\kap04\AnonymeKlassen.java
162
Packages 5.1
Einführung
Da sehr viele Entwickler Java-Klassen bzw. -Anwendungen erzeugen, 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 Packages 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.
Hinweis 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. 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 JARDateien). Ausgehend von diesen Verzeichnissen oder innerhalb des Archivs wird die Verzeichnisstruktur des Packages bestimmt. Im Package werden wiederum die Klassen und anderen Typen gefunden.
Beispiel Der Klassenpfad enthält das Verzeichnis C:\MeineBibos sowie das Archiv C:\MeineArchive\Hilfsklassen.jar. Sucht der Compiler nach der Klasse de.jse6buch.kap05.PackageTest, wird der Pfad zu den Bibliotheken um den relativen Pfad ..\de\jse6buch\kap05 erweitert. Dieser Pfad entspricht dabei dem Package-Namen. Im Pfad C:\MeineBibos\de\jse6buch\ kap05 wird jetzt nach der Klasse PackageTest gesucht, die sich in der Datei PackageTest.class befindet. 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.
Java 6
163
5 – Packages
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 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 Packages werden standardmäßig klein geschrieben, in jedem Fall wird bei beiden die Groß-/Kleinschreibung beachtet. 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 Packages.
Beispiel Sie haben eine Verzeichnisstruktur de\jse6buch\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.jse6buch.kap05;
Hinweis Was passiert, wenn Sie die package-Anweisung weglassen? Angenommen, Sie befinden sich im Verzeichnis, das de\jse6buch\kap05 übergeordnet ist. Sie können dann die Datei PackageTest.java wie folgt übersetzen: javac de\jse6buch\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.jse6buch.kap05.PackageTest java de\jse6buch\kap05\PackageTest java PackageTest
164
Einführung
Der erste Aufruf funktioniert nicht, da sich die Klasse PackageTest nicht im Package de.jse6buch.kap05 befindet. Es fehlt die package-Anweisung in der Klassendefinition. Der zweite Aufruf geht ebenfalls schief, weil der gesamte Name als Klassen- und nicht als Dateiname interpretiert wird und somit die Klasse mit dem Namen »de\ jse6buch\kap05\PackageTest« 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. Zum Starten der Anwendung haben Sie nun zwei Möglichkeiten. Entweder Sie wechseln in das Verzeichnis ..\de\jse6buch\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.jse6buch.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 Packages gefunden werden, muss sich das Startverzeichnis des Packages 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 dabei 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.
Hinweis 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.
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 explizit zugeordnet haben. Die Sichtbarkeit ist damit auf die Package-Ebene festgelegt.
Java 6
165
5 – Packages
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. de.jse6buch.kap05.PackageImport de.jse6buch.kap05.StatischeInhalte de.jse6buch.kap05.util.Hilfsklasse
Hinweis 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.
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 Domain-Namens 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
Anwendung für die Zucht
..\de\zwergkaninchenag\rassen
Anwendung für die Verwaltung der Rassen
..\de\zwergkaninchenag\util
Hilfsklassen (z.B. eine Klasse Kaninchen)
..\de\zwergkaninchenag\loewenkopf
spezielle Klassen für Löwenkopfkaninchen
..\de\zwergkaninchenag\hermelin
spezielle Klassen für Hermelinkaninchen
166
Packages importieren
5.2
Packages importieren
Über die package-Anweisung können Sie eine Verbindung einer Klasse, eines Interfaces 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 Packages haben Sie sofort Zugriff. Weiterhin sind automatisch alle Typen des Packages java.lang verfügbar, weil es sich hier um die Standardklassen von Java handelt. Bleibt noch die Frage, wie Sie Typen anderer Packages bereitstellen. Eine Möglichkeit besteht darin, jeden benötigten Typ mit seinem vollqualifizierten Namen zu referenzieren. 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();
Besser ist es, wenn Sie über die import-Anweisung einzelne Typen oder alle öffentlichen Typen eines Packages importieren und anschließend nur noch den Namen des Typs verwenden. import java.util.*; // importiert alle Typen import java.util.ArrayList; // importiert nur den Typ ArrayList ... ArrayList lst = new ArrayList();
Während der Import aller Typen eines Packages diese in nur einer einzigen 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, d.h., Sie können jetzt in der verkürzten Form auf diese zugreifen. 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 Sie 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.
Java 6
167
5 – Packages 쮿
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 Packages 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.*;
// Compilerfehler // Compilerfehler // erlaubt // erlaubt
Beispiel Die Klasse Hilfsklasse enthält eine Methode, die Sie häufiger benötigen. Deshalb wurde als Package de.jse6buch.kap05.util gewählt (util – Kurzform für utility). Zur korrekten Arbeitsweise muss sich die Datei Hilfsklasse.java in einem Unterverzeichnis ..\util unter ..\de\jse6buch\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 dem Verzeichnis ..\de\jse6buch\kap05 übergeordnet ist. Ansonsten werden die Packages nicht gefunden. package de.jse6buch.kap05.util; public class Hilfsklasse { public int add(int zahl1, int zahl2) { return (zahl1 + zahl2); } } Listing 5.1: \Beispiele\de\jse6buch\kap05\util\Hilfsklasse.java package de.jse6buch.kap05; import de.jse6buch.kap05.util.*; public class PackageImport { public PackageImport() { Hilfsklasse hk = new Hilfsklasse(); System.out.println("10 + 11 = " + hk.add(10, 11)); } Listing 5.2: \Beispiele\de\jse6buch\kap05\PackageImport.java
168
Statischer Import
public static void main(String args[]) { new PackageImport(); } } Listing 5.2: \Beispiele\de\jse6buch\kap05\PackageImport.java (Forts.)
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 seit dem JDK 5.0 verfügbar.
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, da der Klassenname nicht mehr angegeben werden muss. 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;
Hinweis 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.
Java 6
169
5 – Packages
Beispiel Die Klasse StatischeInhalte definiert jeweils eine statische Konstante, Variable und Methode. Über den statischen Import werden diese in der Klasse StatischerImport eingebunden und verwendet. 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"); } } Listing 5.3: \Beispiele\de\jse6buch\kap05\StatischeInhalte.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(); } } Listing 5.4: \Beispiele\de\jse6buch\kap05\StatischerImport.java
170
Arrays, Wrapper und Auto(un)boxing 6.1
Arrays
Müssen Sie sehr viele Werte eines bestimmten Datentyps verwalten, sind Variablen, die nur einen Wert speichern, ungeeignet. Insbesondere dann, wenn die benötigte Anzahl der zu verwalteten Werte 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 folgen 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 allerdings 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; // bevorzugte Schreibweise int[][] zahlenFeld; // bevorzugte Schreibweise int zahlenFeld[]; // ebenfalls erlaubte Schreibweise
Auch eine gemischte Schreibweise ist möglich. Allerdings werden dann alle Klammersetzungen berücksichtigt. int[] zahlenFeld[]; // erzeugt ein Array vom Typ int[][]
Java 6
171
6 – Arrays, Wrapper und Auto(un)boxing
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 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.
Mehrdimensionale Arrays Zur Erstellung mehrdimensionaler Arrays geben Sie mehrere Klammerpaare an. Das Erzeugen des Arrays kann für das gesamte Array in einer Anweisung oder für jede Dimension einzeln erfolgen. Die Größe der einzelnen Dimensionen kann verschieden sein. 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 auch die Angabe einer Initialisiererliste möglich. Für jede Dimension ist dazu eine separate, in geschweifte Klammern eingeschlossene Liste mit den Werten anzugeben. Die Größe der einzelnen Dimensionen wird durch die Anzahl der Werte festgelegt, die nicht notwendigerweise für jede Dimension gleich sein muss. 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
172
Arrays
Hinweis Arrays sind Objekte, deshalb können deren Inhalte bei der Übergabe an eine Methode dauerhaft geändert werden. Alle Arrays besitzen die Variable length, über die Sie die Größe des Arrays ermitteln können. int len = zahlenFeld.length;
Syntax zum Zugriff auf die Array-Elemente Auf die einzelnen Elemente eines Arrays greifen Sie über die Angabe eines Indexes 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 in Klammerpaare eingeschlossene Indizes, 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 immer um 1 kleiner als die Größe des Arrays. int[] zahlenFeld = new int[2]; int i = zahlenFeld[2]; // Fehler, da der Index nur von 0..1 läuft
Beispiel 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 Methode printf() zur formatierten Textausgabe. Über %10s wird darüber eine Zeichenkette in der Breite von zehn Zeichen angezeigt. Am Anfang wird mit Leerzeichen aufgefüllt. Durch die Angabe von %4d wird eine Zahl in der Breite von vier Zeichen ausgegeben.
Java 6
173
6 – Arrays, Wrapper und Auto(un)boxing
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(); } } Listing 6.1: \Beispiele\de\jse6buch\kap06\Zahlenfolgen.java
Es wird die folgende Ausgabe erzeugt: Zeile 1: Zeile 2: Zeile 3: Zeile 4: Zeile 5: Zeile 6: Zeile 7: Zeile 8: Zeile 9: Zeile 10:
174
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100
Die Klasse Arrays
Arrays kopieren Weisen Sie eine Array-Variable einer anderen Array-Variablen zu, wird nicht das Array selbst kopiert, sondern dessen Referenz. Die zweite Variable verweist anschließend auf dasselbe Array. Die Kopie eines Arrays können Sie über die Methode clone() erzeugen. Von den geerbten Methoden der Klasse Object stellt die Methode clone() hier aber 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. 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);
6.2
Die Klasse Arrays
Die Klasse Arrays aus dem Package java.util besitzt zahlreiche überladene statische Methoden zur Verarbeitung von Arrays. Im JDK 6 kamen neue Methoden hinzu, um das Kopieren von Teilen eines Arrays in ein anderes besser zu unterstützen. Insbesondere trat oft das Problem auf, Arrays von primitiven Typen wie int oder double zu verkürzen oder zu vergrößern, da zu Beginn nicht die benötigte Größe des Arrays klar war. Dies ließ sich bisher nur mit der Methode arraycopy() der Klasse System bewerkstelligen (oder einer eigenen Implementierung), die allerdings nur mit Object-Typen arbeiten kann. Das unnötige (Un)Boxing der Datentypen kostete aber nur wertvolle Zeit. Mittels der Methode copyOf() kann ein Array jetzt durch Angabe einer geringeren Länge verkürzt oder bei Angabe einer größeren Länge vergrößert werden. Die neuen Elemente werden dabei mit 0 oder null initialisiert. Mittels der Methode copyOfRange() kann ein neues Arrays basierend auf einem ausgewählten Bereich eines anderen Arrays erstellt werden. int[] copyOf(int[] original, int newLength) int[] copyOfRange(int[] original, int from, int to)
Über die Methode deepToString() kann der Inhalt mehrdimensionaler Arrays als StringRepräsentation geliefert werden. static String deepToString(Object[] array)
Java 6
175
6 – Arrays, Wrapper und Auto(un)boxing
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 Reihenfolge. 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([] arrayName)
Beispiel Zuerst wird ein neues Array kopie deklariert und ihm die gleiche Länge wie die des Arrays args (Parameter der main()-Methode) zugewiesen. Der Inhalt von args wird in das Array 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 z.B. über java Parameter A B G E H bzw. java de.jse6buch.kap06.Parameter A B G E H
auf. Die einzelnen Buchstaben werden der Anwendung als zusätzliche Parameter übergeben. Als Ausgabe erhalten Sie einen String mit den sortierten Buchstaben [A, B, E, G, H].
176
Wrapper-Klassen
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)); } } Listing 6.2: \Beispiele\de\jse6buch\kap06\Parameter.java
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
long
Long
short
Short
void
Void
Tabelle 6.1: Übersicht der Wrapper-Klassen
Java 6
177
6 – Arrays, Wrapper und Auto(un)boxing
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");
Hinweis Eine Ausnahme bildet die Klasse Void, die weder einen Konstruktor noch spezielle Methoden bietet.
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 Wrapper-Klasse Double ist der Methodenname beispielsweise doubleValue(). int intValue()
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()
178
Wrapper-Klassen
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 WrapperKlasse 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 Datentypen in Objektdatentypen und umgekehrt ist eigentlich eine lästige Aufgabe. Allerdings wird sie relativ häufig benötigt, z.B. bei der Aufnahme primitiver Datentypen in Collections. Aus diesem Grund wurde beginnend mit 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. 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.
Java 6
179
6 – Arrays, Wrapper und Auto(un)boxing
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));
Hinweis Objektvariablen können auch null-Werte besitzen. Wenn Sie diese automatisch in primitive Datentypen konvertieren wollen, wird eine NullPointerException ausgelöst.
Hinweis Durch das Autounboxing können jetzt auch die Typen von Wrapper-Klassen in den Kontrollstrukturen wie if oder switch genutzt werden, z.B. Boolean b = new Boolean(true); if(b) ... Integer I = new Integer(10); switch(I) ...
6.3.3
Bitmanipulation
Die Wrapper-Klassen Character, Integer, Long und Short implementieren zusätzliche Methoden zur Bitmanipulation. Alle Methoden sind statisch und liefern einen int-Wert zurück. Je nach Wrapper-Klasse werden nicht alle der hier vorgestellten Methoden unterstützt. Bis auf die Methode reverseBytes(), die von allen vier Wrappern implementiert wird, liegen alle anderen Methoden nur in den Wrappern Integer und Long vor.
180
Wrapper-Klassen
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
Tabelle 6.2: Methoden zur Bitmanipulation
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
Beispiel Die folgende Beispielanwendung wendet alle Bitoperationen auf den Wert 200 an. Seine Binärdarstellung wird im ersten Kommentar gezeigt. 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)); Listing 6.3: \Beispiele\de\jse6buch\kap06\Bitmanipulation.java
Java 6
181
6 – Arrays, Wrapper und Auto(un)boxing
System.out.println(Integer.signum(200)); } } Listing 6.3: \Beispiele\de\jse6buch\kap06\Bitmanipulation.java (Forts.)
Die Ergebnisse entstehen wie folgt: bitCount() lowestOneBit() numberOfLeadingZeros() numberOfTrailingZeros() rotateLeft() rotateRight() reverse() reverseBytes() signum()
182
= = = = = = = = =
3 (es gibt dreimal den Wert 1) 8 (1000) 24 (es stehen 24 Nullen vor der ersten 1) 3 (am Ende befinden sich 3 Nullen) 00000000 00000000 00000011 00100000 00000000 00000000 00000000 00110010 00010011 00000000 00000000 00000000 11001000 00000000 00000000 00000000 1, da 200 > 0
Exceptions 7.1
Einführung
Entwickeln Sie Anwendungen, sei es mit Java oder einer anderen Programmiersprache, ist es kaum auszuschließen, dass Fehler auftreten. Einige Fehler sind vermeidbar, andere eher unvermeidbar. Vermeidbare Fehler können durch Sie, d.h. durch eine sorgfältige Programmentwicklung, theoretisch ausgeschlossen werden (man sagt auch, das Problem sitzt vor dem Bildschirm). Tritt allerdings ein Hardwareproblem auf oder wird die Java Virtual Machine nicht korrekt ausgeführt, haben Sie allerdings wenig 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 doch stark beeinträchtigen kann (zum Teil ist die Ausführung um den Faktor 100 langsamer). Sie müssen also hier zwischen dem reinen Einsatz (kaum Einfluss auf die Ausführungsgeschwindigkeit) und der Behandlung (sehr zeitaufwändig) von Exceptions unterscheiden. In der Regel sollte es nicht der Standard sein, dass in einer Anwendung sehr viele Exceptions auftreten, denn das wäre dann doch ein Zeichen dafür, dass sich noch zu viele Problemstellen darin befinden.
Beispiel Über die statische Methode parseInt() der Klasse Integer lassen sich Zeichenketten in Zahlen konvertieren. Ist eine Umwandlung nicht möglich, wird eine Exception vom Typ NumberFormatException ausgelöst. Anweisungen, die Exceptions verursachen, werden in einen try-Block eingeschlossen. Innerhalb des darauf folgenden catch-Blocks erfolgt die Exceptionbehandlung. String text = "a124"; int zahl; try
Java 6
183
7 – Exceptions
{ 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.
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 alle dargestellt sind. Throwable
Error
VirtualMachineError
Exception
IllegalAccessException
RuntimeException
NullPointerException
Abbildung 7.1: Klassenhierarchie der Exceptions
184
Exceptions behandeln
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. Exceptions 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 z.B. 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. Ansonsten 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.
Java 6
185
7 – Exceptions 쮿
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. try { // Anweisungen, die Exceptions auslösen können } catch(ExceptionTyp1 ex1) { // Behandlung } catch(ExceptionTyp1 ex2) { // Behandlung } // weitere Anweisungen
Suche nach einem try-catch-Block Exception tritt auf
Suche nach umschließendem try-catch-Block
gefunden
nicht gefunden
Standardbehandlung
Compiler meldet Fehler bzw. Anwendung wird beendet
Abbildung 7.2: Verarbeitung von Exceptions
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
186
Exceptions behandeln
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.
Methoden der Klasse Throwable Die folgenden Methoden stehen über die Klasse Throwable auch allen anderen ExceptionTypen 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: / by zero at de.jse6buch.Test.test(Test.java:17) at de.jse6buch.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: Division durch null ist aufgetreten
Throwable fillInStackTrace()
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.
Tabelle 7.1: Methoden der Klasse Throwable
Beispiel Es wird noch einmal das Beispiel der Konvertierung einer Zeichenkette in eine Zahl verwendet. Die in der Variablen strZahl gespeicherte Zeichenkette ist keine Zahl. Deshalb wird bei der Konvertierung in eine Zahl 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 6
187
7 – Exceptions
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(); } } Listing 7.1: \Beispiele\de\jse6buch\kap07\Behandlung.java
Als Ausgabe erhalten Sie: ==== getMessage() ==== For input string: "KeineZahl" ==== toString() ==== java.lang.NumberFormatException: For input string: "KeineZahl" ==== printStackTrace() ==== java.lang.NumberFormatException: For input string: "KeineZahl" at java.lang.NumberFormatException.forInputString( NumberFormatException.java:48) at java.lang.Integer.parseInt(Integer.java:447) at java.lang.Integer.parseInt(Integer.java:497) at de.jse6buch.kap07.Behandlung.(Behandlung.java:12) at de.jse6buch.kap07.Behandlung.main(Behandlung.java:27)
188
Exceptions weitergeben
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. public methode([parameter]) throws Exc1, Exc2 { }
Die Angabe von throws ist beispielsweise notwendig, wenn Sie selbst eine markierte Exception in einer Methode auslösen.
Hinweis 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.
Abbildung 7.3: API-Dokumentation einer Methode mit throws-Angabe
Beispiel Eine Datei soll über einen FileInputStream geöffnet werden. Dabei kann eine Exception vom Typ FileNotFoundException auftreten. Die Exception soll nicht direkt in der Methode 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.
Java 6
189
7 – Exceptions
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."); } } } Listing 7.2: \Beispiele\de\jse6buch\kap07\Weitergabe.java
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 Verbindungen wieder zu schließen oder andere Aufräumarbeiten durchzuführen. Prinzipiell ist es in den try-catch-Blöcken möglich, solche und weitere Aufräumarbeiten auszuführen. In diesem Fall müssen aber immer an mehreren Stellen die gleichen Anweisungen eingefügt werden. Das folgende Beispiel demonstriert dies. Es muss hier sichergestellt sein, dass in den catch-Blöcken alle möglichen Exceptions abgefangen werden. try { // Datei öffnen // Datei schliessen } catch(ExceptionTyp1 e) { // Exception behandeln
190
Aufräumen mit finally
// Datei schliessen } catch(ExceptionTyp2 e) { // Exception behandeln // 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 kann der finally-Block angefügt werden.
쮿
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 finally-Block nicht ausgeführt. try { } catch(ExceptionTyp e) { } finally { }
Hinweis 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.
Java 6
191
7 – Exceptions
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 Exception-Typ 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.
쮿
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();
Hinweis 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.
192
Exceptions auslösen
Beispiel 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. 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(); } } Listing 7.3: \Beispiele\de\jse6buch\kap07\Ausloeser.java
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 }
Java 6
193
7 – Exceptions
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 ... }
Beispiel 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 erfolgt ein erneutes Auslösen der Exception, 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. public class Ausloeser2 { private void ausgabe(String s) throws IllegalArgumentException { if(s == null) throw new IllegalArgumentException("Keine Zeichenkette."); } public Ausloeser2() throws IllegalArgumentException { try { ausgabe(null); } catch(IllegalArgumentException iaEx) { iaEx.printStackTrace(); iaEx.fillInStackTrace(); throw iaEx; } } Listing 7.4: \Beispiele\de\jse6buch\kap07\Ausloeser2.java
194
Exceptions auslösen
public static void main(String args[]) { try { new Ausloeser2(); } catch(IllegalArgumentException iaEx) { iaEx.printStackTrace(); } } } Listing 7.4: \Beispiele\de\jse6buch\kap07\Ausloeser2.java (Forts.)
Es wird die folgende Ausgabe erzeugt: java.lang.IllegalArgumentException: Keine Zeichenkette. at de.jse6buch.kap07.Ausloeser2.ausgabe(Ausloeser2.java:8) at de.jse6buch.kap07.Ausloeser2.(Ausloeser2.java:14) at de.jse6buch.kap07.Ausloeser2.main(Ausloeser2.java:27) java.lang.IllegalArgumentException: Keine Zeichenkette. at de.jse6buch.kap07.Ausloeser2.(Ausloeser2.java:19) at de.jse6buch.kap07.Ausloeser2.main(Ausloeser2.java:27)
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 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)
Java 6
195
7 – Exceptions
Beispiel 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 Exception-Kette 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. 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(); } Listing 7.5: \Beispiele\de\jse6buch\kap07\ExceptionKetten.java
196
Eigene Exceptions verwenden
catch(Exception e) { do { System.out.println(e.getMessage()); e = (Exception)e.getCause(); } while(e != null); } System.out.println("Alle behandelt."); } } Listing 7.5: \Beispiele\de\jse6buch\kap07\ExceptionKetten.java (Forts.)
Die Ausgabe ist: 2. Rechenfehler Parameterfehler 1. Rechenfehler 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. Von der Klasse RuntimeException oder einer ihrer Unterklassen sollten Sie ableiten, wenn es sich um einen vermeidbaren Fehler handelt. So kann der Programmierer zum Beispiel sicherstellen, dass der Wertebereich bei der Übergabe von Datenwerten 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.
Java 6
197
7 – Exceptions
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.
Beispiel class MeineException extends ExceptionKlasse { public MeineException() { super(); } public MeineException(String nachricht) { super(nachricht); } // weitere Methoden }
Beispiel 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.
198
Eigene Exceptions verwenden
public class ZahlenfolgeException extends Exception { public ZahlenfolgeException() { super(); } public ZahlenfolgeException(String nachricht) { super(nachricht); } public ZahlenfolgeException(String nachricht, int untereGrenze, int obereGrenze) { super(nachricht + "; der Wert muss zwischen " + untereGrenze + " und " + obereGrenze + " liegen"); } static void main(String args[]) { try { ZahlenfolgeException ek = new ZahlenfolgeException("Grenzwertueberschreitung", -50, 60); throw ek; } catch(Exception e) { System.out.println(e.getMessage()); } } } Listing 7.6: \Beispiele\de\jse6buch\kap07\ZahlenfolgeException.java
Java 6
199
Assertions 8.1
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 z.B. 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.
Beispiel 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 »Vergleich fehlgeschlagen« übergeben. Die zweite Assertion wird nicht durch ein Exceptionhandling abgefangen, sodass das Programm danach beendet wird. Zur Auswertung der Assertions zur Laufzeit muss der Interpreter java mit der Option -ea aufgerufen werden, z.B. java –ea Annahme1
Ansonsten wird nur der Text der println()-Methode ausgegeben und die Assertions werden nicht beachtet. public class Annahme1 { public static void main(String args[]) { try { assert 10 > 11: "Vergleich fehlgeschlagen"; } catch(AssertionError e) Listing 8.1: \Beispiele\de\jse6buch\kap08\Annahme1.java
Java 6
201
8 – Assertions
{ System.out.println(e.getMessage()); } assert 10 > 11; System.out.println("Diese Anweisung wird nicht erreicht."); } } Listing 8.1: \Beispiele\de\jse6buch\kap08\Annahme1.java (Forts.)
Es erfolgt die Ausgabe: Vergleich fehlgeschlagen Exception in thread "main" java.lang.AssertionError at de.jse6buch.kap08.Annahme1.main(Annahme1.java:16)
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 nach einen Doppelpunkt an den ersten Ausdruck angefügt wird, können Sie einen eigenen Meldungstext für die Exception festlegen. Der Inhalt dieser Fehlermeldung 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;
Hinweis 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. Außerdem können Sie die Verwendung von Assertions einfacher (de)aktivieren.
202
Informationen zum Einsatz von Assertions
8.2
Informationen zum Einsatz von Assertions
8.2.1
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 dann 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 berechne() und die Prüfung des Rückgabewerts in zwei Anweisungen aufgesplittet werden. Der Methodenaufruf wird dabei in jedem Fall ausgeführt, unabhängig davon, ob Assertions (de)aktiviert sind. boolean ergebnis = berechne(); assert ergebnis;
Der folgende Methodenaufruf findet dagegen nur statt, wenn Assertions aktiviert sind. assert berechne();
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 counter; ... 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.
Java 6
203
8 – Assertions
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 sichern. Bei fehlerhaften Parametern kann sich dies z.B. im Auslösen einer Exception auswirken, die der Aufrufer behandeln muss. public int berechneggT(int zahl1, int zahl2) { if((zahl1 > 0) && (zahl2 > 0)) throw new IllegalArgumentExcection();
Durch den 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.
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 lässt sich 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. Die 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"; }
204
Informationen zum Einsatz von Assertions
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 ifAnweisungen, 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 switch(wert) { case 1: ... case 2: ... default: assert false: "Keine anderen Werte erlaubt";
Hinweis 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.
Beispiel 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 größte ist, die beide Zahlen ohne Rest teilt. public class Annahme2 { public Annahme2() { System.out.println("Der ggT von 12 und 8 ist: " + berechneggT(12,8)); Listing 8.2: \Beispiele\de\jse6buch\kap08\Annahme2.java
Java 6
205
8 – Assertions
} 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; } public static void main(String args[]) { new Annahme2(); } } Listing 8.2: \Beispiele\de\jse6buch\kap08\Annahme2.java (Forts.)
8.3
Aktivieren von Assertions
8.3.1
Ü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 6 bzw. 1.6.0 angenommen wird. // der Sourcecode verwendet das JDK 1.4, 5.0, 6 // 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
206
Aktivieren von Assertions
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)
-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
Tabelle 8.1: Optionen zum Einsatz von Assertions für den Java-Interpreter
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.jse6buch.kap08.Annahme1 java -ea:de.jse6buch.kap08.Annahme1 de.jse6buch.kap08.Annahme1 java -ea:de.jse6buch -da:java.util de.jse6buch.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.
Java 6
207
8 – Assertions
Definieren Sie dazu 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 ...;
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 Assertions nicht aktiviert, wird auch die Anweisung assert assertsAktiv = true; nicht ausgeführt. Dies bewirkt bei der folgenden Prüfung des Werts von assertsAktiv über die if-Anweisung das Auslösen einer Exception, 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 Prüfung keine Exception auslöst. static { boolean assertsEnabled = false; assert assertsEnabled = true; if(!assertsEnabled) throw new RuntimeException("Assertions nicht aktiviert!"); } Listing 8.3: \Beispiele\de\jse6buch\kap08\Annahme2.java
208
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 Anführungszeichen 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 der Sprache C, nullterminiert. Bevor eine Zeichenkette bearbeitet werden kann, muss ein StringObjekt 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 zugewie-
sen wird. String s1; s1 = "der erste String"; String s2 = "ein neuer String";
Java 6
209
9 – Zeichenkettenverarbeitung
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: String() String(String original) String(byte[] bytes) String(char[] value)
// // // //
Leer-String Kopie des originalen Strings String aus einem byte-Array String aus einem char-Array
Sehen Sie sich die folgenden Beispiele an: // String s1 wird kopiert und s1Kopie 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 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()
Um den String speziell auf eine leere Zeichenkette zu prüfen, steht die Methode isEmpty() zur Verfügung. Diese liefert true, wenn die Länge des Strings 0 ist, sonst false. boolean isEmpty()
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
0
1
...
l
o
4
Abbildung 9.1: Indizes der Zeichen im String
210
Mit String-Objekten arbeiten
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. Methode
Beschreibung
int indexOf(int ch)
Die Methode liefert den Index, an dem das Zeichen erstmalig im String vorkommt
int indexOf(String str)
Analog sucht die Methode die Zeichenkette str im String. Sie gibt den Index des ersten Zeichens der gefundenen Teilzeichenkette zurück
char charAt(int index)
Über die Methode charAt können Sie das Zeichen an der Position index ermitteln
Tabelle 9.1: Methoden der Klasse String
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));
Hinweis 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.
9.1.3
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 dazu die Methode toString() benutzt.
Java 6
211
9 – Zeichenkettenverarbeitung
String s = "Nummer"; int i = 7; String s7 = s + " " + i; // => s7 = "Nummer 7"
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. Nachteilig ist daran, dass der alte Wert der Zeichenkette vom GC entsorgt werden muss, was bei umfangreichen String-Operationen eine beachtenswerte Zeitkomponente darstellt. Mehr dazu aber später.
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.
212
Mit String-Objekten arbeiten
Methode/Operator
Beschreibung
boolean equals(Object obj)
Die Methode equals() dient zum Vergleichen des Inhalts zweier StringObjekte. Das Ergebnis ist true, wenn die Zeichenketten übereinstimmen, sonst false.
boolean equalsIgnoreCase Diese Methode arbeitet wie equals(), es wird aber beim Vergleich die (String str) Groß- und Kleinschreibung vernachlässigt 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 IntegerWert 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 Diese Methode arbeitet wie compareTo(), es wird aber die Groß- und (String str) Kleinschreibung vernachlässigt String s1 = ... String s2 = ... if(s1 == s2) ...
Benutzen Sie den Operator == zum Vergleich zweier String-Objekte, wird nicht deren Inhalt, sondern deren Referenz analysiert. Verweisen zwei String-Variablen auf dieselbe Zeichenkette, liefert die Operation das Ergebnis true. Andernfalls 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.
Tabelle 9.2: Vergleichsmethoden der Klasse String
Beispiel Das folgende Beispielprogramm zeigt die Anwendung der Vergleichsmethoden equals() und compareTo(). Außerdem wird der Unterschied zwischen equals() und dem Vergleichsoperator == demonstriert. 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"; /* Vergleich der Referenzen der String-Objekte System.out.println("a == b ? " + (a == b));
*/
Listing 9.1: \Beispiele\de\jse6buch\Kap09\StringVergleiche.java
Java 6
213
9 – Zeichenkettenverarbeitung
// --> 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 } } Listing 9.1: \Beispiele\de\jse6buch\Kap09\StringVergleiche.java (Forts.)
Es wird die folgende Ausgabe erzeugt: a == b ? true a == c ? false a.equals(b) ? true a.equals(c) ? true a.compareTo(c) ? 0 a.compareTo(d) ? 1 a.compareTo(e) ? -3
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 Teilzeichenkette 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.
Tabelle 9.3: Methoden zum Teil-Stringvergleich 214
Mit String-Objekten arbeiten
Methode/Operator
Beschreibung
boolean endsWith(String s)
Analog zu startsWith() testet die Methode, ob ein String mit dem übergebenen String endet
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, die umfassender ist als indexOf(). Es sind folgende Parameter anzugeben: 1. Vergleichsposition im String 2. Vergleichs-String 3. Vergleichsposition im anderen String 4. Länge des zu vergleichenden Teilstücks
Tabelle 9.3: Methoden zum Teil-Stringvergleich (Forts.)
Beispiel 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 solche, die auf »d« enden. 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. 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")) Listing 9.2: \Beispiele\de\jse6buch\kap09\TeilStringVergleiche.java
Java 6
215
9 – Zeichenkettenverarbeitung
System.out.println(s + " endet mit d"); // 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); } } Listing 9.2: \Beispiele\de\jse6buch\kap09\TeilStringVergleiche.java
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); // => s1 = "Java-Fan" Stirng s2 = s.substring(7, 11)); // => 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 Teil-Strings target durch das Zeichen newChar bzw. den String replacement.
216
Mit String-Objekten arbeiten
Beispiel Die erste replace()-Anweisung tauscht alle Kommata durch Semikolon aus. Die zweite replace()-Anweisung wechselt jede Angabe des Teilstrings »00« durch den String »000« aus. 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 6
217
9 – Zeichenkettenverarbeitung
9.1.7
Formatierte Strings erzeugen
Hinweis 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. 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
Hinweis Die statische (final) Methode format() ist in der Klasse String deklariert und kann, ohne dass ein String-Objekt vorhanden sein muss, aufgerufen werden.
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
218
StringBuilder- und StringBuffer-Objekte verwenden
die statische Methode valueOf() in der Klasse String. Diese statische Methode kann aufgerufen werden, ohne vorher ein String-Objekt zu erzeugen. 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)); // => true String s3 = "" + d; System.out.println("s1 = s3 ? " + s1.equals(s3)); // => true
Hinweis 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.
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 Anwen-
Java 6
219
9 – Zeichenkettenverarbeitung
dungsfä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.
Hinweis 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, die aber in vielen Fällen nicht benötigt wird. In den folgenden Abschnitten wird nur die Klasse StringBuffer vorgestellt. 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 Zuordnung gleicher Strings an mehrere StringBuffer-Objekte, wie es bei der Definition von Strings realisiert wird, ist hier 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 über einen Parameter festgelegt 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)
// leerer Puffer für 16 Zeichen // leerer Puffer für // capacity Zeichen StringBuffer(CharSequence seq) // StringBuffer wird mit seq oder StringBuffer(String str) // str initialisiert
9.2.2
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();
220
StringBuilder- und StringBuffer-Objekte verwenden 쮿
Oder Sie benutzen den Konstruktor der Klasse String, der ein StringBuffer-Objekt als Parameter akzeptiert. String s2 = new String(sb);
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)
Beispiel 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. class StringBufferVerwenden { public static void main(String[] args) { StringBuffer sb = new StringBuffer("Programmieren"); String s = " mit Java "; String s2 = "Standard Edition "; double d = 6.0; sb.append(s).append(d); // Daten anhängen System.out.println("sb = " + sb); // Ausgabe: sb = Programmieren mit Java 6.0 Listing 9.3: \Beispiele\de\jse6buch\kap09\StringBufferVerwenden.java
Java 6
221
9 – Zeichenkettenverarbeitung
sb.insert(23, s2); // Zeichen einfügen System.out.println("sb = " + sb); // Ausgabe: sb = Programmieren mit Java Standard Edition 6.0 } } Listing 9.3: \Beispiele\de\jse6buch\kap09\StringBufferVerwenden.java (Forts.)
Ein praktisches 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()
Beispiel 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 zehn Zeichen vergrößert und die Puffergröße sowie die Zeichenkettenlänge werden ausgegeben. class Puffergroesse { public static void main(String[] args) Listing 9.4: \Beispiele\de\jse6buch\kap09\Puffergroesse.java
222
StringBuilder- und StringBuffer-Objekte verwenden
{ StringBuffer sb = new StringBuffer(); for(int i = 1; i < 1200; i += 10) { System.out.println("Puffergroesse: " + sb.capacity() + " Laenge: " + sb.length()); sb.append("0123456789"); } } } Listing 9.4: \Beispiele\de\jse6buch\kap09\Puffergroesse.java (Forts.)
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) boolean contentEquals(CharSequence cb)
Java 6
223
9 – Zeichenkettenverarbeitung
Für StringBuilder-Objekte gibt es keine direkte Überladung dieser Methode. Da ein StringBuilder aber das Interface CharSequence implementiert, kann die zweite Variante genutzt werden. Außerdem kann der Vergleich noch mit der String-Methode contains() erfolgen. Die Zeichenkette des StringBuilder-Objekts wird dabei 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()));
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 Klasse 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.
Beispiel In diesem Testprogramm soll dies nachgewiesen werden. Es misst die benötigte Zeit, um 50.000-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. Hinweis: Verändern Sie die Anzahl der Schleifendurchläufe entsprechend der Geschwindigkeit Ihres Rechners, indem Sie der Konstanten anzahl einen anderen Wert zuweisen, um ein aussagekräftiges Ergebnis zu erhalten. class PerformanceTest { public static void main(String[] args) { StringBuffer sb = new StringBuffer(); Listing 9.5: \Beispiele\de\jse6buch\kap09\PerformanceTest.java
224
StringBuilder- und StringBuffer-Objekte verwenden
String s = ""; StringBuilder sb2 = new StringBuilder(); long dauer; long start; final int anzahl = 50000; // 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"; 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); } } Listing 9.5: \Beispiele\de\jse6buch\kap09\PerformanceTest.java (Forts.)
Ausgabe des Tests: Dauer bei der Verkettung mit StringBuffer: 15 Dauer bei der Verkettung von Strings: 10281 Dauer bei der Verkettung mit StringBuilder: 0
Java 6
225
9 – Zeichenkettenverarbeitung
9.3
Formatierung
9.3.1
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.
Hinweis Die Klasse Formatter steht erst seit dem JKD 5.0 zur Verfügung. 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. Die Klasse Formatter unterstützt die Formatierung folgender Datentypen: 쮿
Strings
쮿
primitive Datentypen
쮿
Datum- und Zeit-Datentypen
쮿
die Klassen Calender, BigInteger und BigDecimal
Hinweis Ist Ihnen die Sprache C bekannt, werden Sie das Prinzip der Funktion printf() wiedererkennen.
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 steht ebenfalls seit dem JDK 5.0 zur Verfügung. Es beinhaltet zwei append()-Methoden, denen ein Zeichen (char) oder eine Zeichenfolge (CharSequence) übergeben werden kann. Implementiert wurde dieses Interface in allen Klassen, deren Instanzen formatierte Ausgaben
226
Formatierung
über Formatter empfangen sollen. Hierzu gehören z.B. die verschiedenen Writer- und Stream-Klassen (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 Hinweis Für die Beispiele im folgenden Abschnitt wird das hier deklarierte Formatter-Objekt formatter verwendet. Beachten Sie bei den Ausgaben, dass als Lokalität Deutschland eingestellt ist. Dadurch werden beispielsweise Gleitkommazahlen mit einem Komma und nicht wie in der Programmierung mit Java mit einem 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 Formatter 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. 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.
Hinweis 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.
Java 6
227
9 – Zeichenkettenverarbeitung
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
Abbildung 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 Format-String drei Argumente angegeben werden. Die Argumente können Ausdrücke, Variablen oder Literale sein. 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.
228
Formatierung 쮿
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
Abbildung 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
Abbildung 9.4: Zuordnung eines Werts zu mehreren Format-Spezifizierern
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.
Hinweis In den folgenden Abschnitten finden Sie eine Auswahl häufig benötigter 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.
Java 6
229
9 – Zeichenkettenverarbeitung
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
Tabelle 9.4: Auswahl von Format-Spezifizierern
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
Tabelle 9.5: Format-Spezifizierer zur Zeichen- und Stringausgabe
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.
230
Formatierung
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 String: %5.3s ", z, b, s, d, zk));
Ausgaben: Zeichen: a Zeichen: a
Byte: L Short: i Byte: 76 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 Unicode-Zeichen. 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.
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" oder "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.
Java 6
231
9 – Zeichenkettenverarbeitung
'd'
Gibt eine Integer-Zahl aus (Dezimalzahl)
'o'
Gibt eine oktale Integer-Zahl aus. Es sind hier aber 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.
Tabelle 9.6: Format-Spezifizierer zur Ganzzahlausgabe
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 Integer-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));
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.
232
Formatierung
'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
Tabelle 9.7: Format-Spezifizierer zur Gleitkommazahlausgabe
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 insgesamt nur 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; Formatspezifizierer
Ausgabe
// wissenschaftliche Schreibweise - Standard formatter.format("%e", d1) formatter.format("%e", d2)
3.534728e+05 -3.321000e-04
// wissenschaftliche Schreibweise – // mit 10 Nachkommastellen formatter.format("%.10e", d1); formatter.format("%.10e", d2);
3.5347287978e+05 -3.3210000000e-04
// allgemeine Darstellung mit 10 Stellen // nach dem Komma formatter.format("%.10g", d1); formatter.format("%.10g", d2);
353472,8797800000 -3.3210000000e-04
// Dezimalzahl mit Vorzeichen formatter.format("%,+f", d1); formatter.format("%f", d2);
+353.472,879780 -0,332100
Tabelle 9.8: Beispiele zur Anwendung der Format-Spezifizierer
Java 6
233
9 – Zeichenkettenverarbeitung
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 Integer- 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%
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"
Tabelle 9.9: Format-Spezifizierer zur Datumsausgabe
234
Formatierung
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.2006 erzeugen Calendar c = new GregorianCalendar(2006, 10, 20); formatter.format("Der %1$td.%1$tB %1$tY ist ein %1$tA und " + "der %1$tj. Tag des Jahres", c));
Ausgabe: Der 20.November 2006 ist ein Montag und der 324. Tag des Jahres
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"
Tabelle 9.10: Format-Spezifizierer zur Zeitausgabe
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.
Java 6
235
9 – Zeichenkettenverarbeitung
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
Tabelle 9.11: Weitere Format-Spezifizierer
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
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.
Beispiel Die einzelnen Beispiele dieses Abschnitts befinden sich zusammengefasst in der Datei \Beispiel\de\jse6buch\kap09\ZeichenFormatierung.java.
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()
236
Formatierung
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 schon seit einigen 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 dazu die Methode getNumberInstance() angewendet werden. static NumberFormat getNumberInstance() static NumberFormat getNumberInstance(Locale inLocale)
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);
Hinweis Weitere Informationen zur Klasse Locale finden Sie im Kapitel »Nützliche Klassen«. Der Formatierungs-String kann bei der Definition des Objekts oder später über die Methode applyPattern() festgelegt werden. Mit dieser Methode lässt sich der Formatierungs-String auch ändern. 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
Tabelle 9.12: Übersicht der Formatierungs-String-Zeichen
Java 6
237
9 – Zeichenkettenverarbeitung
Zeichen Bedeutung '-'
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
Tabelle 9.12: Übersicht der Formatierungs-String-Zeichen (Forts.)
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 Apostroph (') voranzustellen. 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));
Beispiel 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.
238
Formatierung
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)); } } Listing 9.6: \Beispiele\de\jse6buch\kap09\ZahlenFormatieren.java
Ausgabe: 1234,6 1234,57 1234,56700 001234,567 1.234,567 1,235E03 1.234,57 Euro
Java 6
239
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 beide im Package java.util zu finden sind. 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 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 locale)
Sie können den Konstruktor mit einem bestimmten Datum, bestehend aus Jahr, Monat und Tag, 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.
Hinweis 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. Calendar c = Calendar.getInstance();
Java 6
241
10 – Nützliche Klassen
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. Feldname
Bedeutung
Wertebereich
AM_PM
Zeit am Vormittag oder Nachmittag
0, 1
Vergleichswert: AM, PM DATE
Tag (Synonym für DAY_OF_MONTH)
1 ... 31
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
DAY_OF_WEEK_IN_MONTH
Woche im Monat
-1 ... 6
DAY_OF_YEAR
Tag des Jahres
1 ... 366
DST_OFFSET
Sommerzeitverschiebung in ms
ERA
Zeit vor oder nach Christi Geburt
Vergleichswert: SUNDAY, MONDAY, ... SATURDAY
0, 1
Vergleichswert: BC (before Christ), AD (Anno Domini) FIELD_COUNT
Anzahl der Felder
HOUR
Stunde
0 ... 12
HOUR_OF_DAY
Stunde des Tages
0 ... 23
MILLISECOND
Millisekunde
0 ... 999
MINUTE
Minute
0 ... 59
MONTH
Monat 0 ... 11 Achtung: Monate beginnen hier bei 0 (für Januar) Vergleichswert:
SECOND
Sekunde
0 ... 59
WEEK_OF_MONTH
Woche des Monats
1 ... 6
WEEK_OF_YEAR
Kalenderwoche
1 ... 54
YEAR
Jahr
1 ... 5.000.000
ZONE_OFFSET
Zeitverschiebung von der Zeitzone GMT (Greenwich Mean Time) in ms
JANUARY ... DECEMBER
Tabelle 10.1: Eigenschaften der Klasse GregorianCalendar
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)
242
Datum und Uhrzeit
Die Methode set() kann aber auch zum Modifizieren bzw. zum Setzen mehrerer Felder eingesetzt werden. 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)
Hinweis 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. Zum Beispiel wird das Datum 01.11.2006 wie folgt übergeben: GregorianCalendar gc = new GregorianCalendar(2006, 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));
Beispiel 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. 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) + ". Tag des Jahres in der " + Listing 10.1: \Beispiele\de\jse6buch\kap10\DatumZeitAnzeigen.java
Java 6
243
10 – Nützliche Klassen
gc.get(Calendar.WEEK_OF_YEAR) + ". 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 ""; } } Listing 10.1: \Beispiele\de\jse6buch\kap10\DatumZeitAnzeigen.java (Forts.)
Ausgabe: Heute ist Mittwoch, der 1.11.2006, der 305. Tag des Jahres in der 44. Kalenderwoche Zeit: 10:00:00 Wir haben eine Sommerzeitverschiebung von 1 Stunde(n)
Hinweis 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.
244
Datum und Uhrzeit
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.
Beispiel Das Programm erzeugt die gleichen Ausgaben wie das in Listing 10.1 abgedruckte Programm. 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. Tag des Jahres " + "in der %2$d. 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); } } Listing 10.2: \Beispiele\de\jse6buch\kap10\DatumZeitAnzeigenFormatiert.java
Ungültige Datumswerte Sind die zugewiesenen Datumswerte ungültig, wie z.B. der 32.10.2006, 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(2006, 5, 32); gc.set(Calendar.DATE, 40);
Java 6
// => 02.07.2006 // => 10.07.2006
245
10 – Nützliche Klassen
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 (true) oder herunter gezählt (false) werden soll. Auch die Angabe einer Schrittweite (amount) ist 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 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() werden 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 angegebenen Feldes überschritten wird.
246
Datum und Uhrzeit
Beispiele 쮿
Das Datum steht vor dem Hochzählen auf dem letzten Tag des Monats, z.B. auf dem 31.01.2007. Nur der Tag wird durch das Ausführen der roll()-Methode um die Zahl 1 hoch gezählt, wobei wieder bei 1 begonnen wird. GregorianCalendar gc = new GregorianCalendar(2007, 0, 31); gc.roll(Calendar.DATE, true); System.out.printf("%1$td.%1$tm.%1$tY", gc); // 01.01.2007
쮿
Wird das Datum mit der Methode add() hochgezählt, ist auch der Monat betroffen, wenn der Wertebereich überschritten wird. gc.set(2007, 0, 31); // 31.01.2007 gc.add(Calendar.DATE, 1); System.out.printf("%1$td.%1$tm.%1$tY%n", gc); // 01.02.2007
쮿
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(2006, 11, 31, 12, 30, 0); // 31.12.2006 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.2007 8:30
쮿
In einer Schleife wird das Feld MONTH zwölfmal herunter gezählt. Die Methode roll() modifiziert hier nur den Wert des Monats, so dass man am Ende wieder beim Anfangsdatum landet. gc.set(2006, 5, 1); // 01.06.2006 for(int i = 1; i 2006-11-01T16:43:33 1091371413906 0 INFO java.util.logging.LogManager$RootLogger log 10 Information ...
Tabelle 28.4: Übersicht der Formatierer im Loggins API
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.
Beispiel Möchten Sie den FileHandler verwenden, geben Sie in dessen Konstruktur einen Dateinamen an. Ohne eine Pfadangabe wird die Datei in dem Verzeichnis erstellt, von dem aus 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 6
783
28 – Logging
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.xml"); 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(); } } Listing 28.3: \Beispiele\de\jse6buch\kap28\FileHandlerTest.java
Eigene Handler und Formatter definieren Um eine eigene Ausgabe und evt. 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 Formatters wiederum sinnvoll. Wenn Sie einen eigenen Handler einsetzen wollen, ist optional die Weiterreichung der Log-Einträge an die Eltern-Handler zu deaktivieren. Verwenden Sie dazu die bereits vorgestellte Methode setUseParentHandlers(). Als Vorlage für einen korrekt implementierten Handler können Sie die Sourcecodes der Handler des Logging API's als Vorlage verwenden (Datei src.zip im JDK-Verzeichnis öffnen und die entsprechende *.java-Datei auswählen). Um einen Handler zu erzeugen,
784
Handler verwenden
1. Leiten Sie eine neue Klasse von der Klasse Handler oder einer der davon abgeleiteten Klassen ab. 2. Implementieren Sie die Methoden publish() zur Ausgabe (ihr werden die Daten des Log-Eintrages 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 dem Logger über die Methode addHandler()den Handler zu.
Beispiel Die neue Handler-Klasse und das Testprogramm befinden sich beide in der selben 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 Eintrages aus. Über die Methode isLoggable() wird geprüft, ob der LogEintrag 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. 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); Listing 28.4: \Beispiele\de\jse6buch\kap28\FormatHandlerTest.java
Java 6
785
28 – Logging
else return true; } public void flush() {} public void close() {} } Listing 28.4: \Beispiele\de\jse6buch\kap28\FormatHandlerTest.java (Forts.)
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. import java.util.logging.*; public class FormatHandlerTest { public FormatHandlerTest() { Logger l = Logger.getLogger("de.jse6buch.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(); } } Listing 28.5: \Beispiele\de\jse6buch\kap28\FormatHandlerTest.java
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.
786
Der LogManager
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 bei Packagenamen) 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, übernehmen alle anderen Logger diese Einstellung. ""-Root-Logger
com
entwickler
java
javamag
Abbildung 28.2: Hierarchie der Logger im LogManager
Die Namen der Logger müssen lediglich durch einen Punkt getrennt sein, um die Hierarchie zu erzeugen. Es ist aber nicht zwingend notwendig, die Packagestruktur der Logger-Klasse zu verwenden. Der Vorteil der Packagestruktur ist wie schon bei der Benutzung in Klassen, dass eindeutige Namen für die Logger entstehen, besonders dann, wenn mit vielen Klassenbibliotheken gearbeitet wird. Unbenannte Logger (anonyme) haben nur den Root-Logger als Eltern-Logger.
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.jse6buch.kap28.level = SEVERE
Java 6
787
28 – Logging
Der 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. 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 "));
Beispiel 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 FileHandlers vorgibt. Über die Einstellung config wird eine Klasse angegeben, von der automatisch eine Instanz erzeugt wird und die über den Konstruktor 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.
788
Der LogManager
de.jse6buch.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.jse6buch.kap28.LogMngTest.useParentHandlers=false de.jse6buch.kap28.LogMngTest.level=FINEST config=de.jse6buch.kap28.LogMngInit Listing 28.6: \Beispiele\de\jse6buch\kap28\LoggingInit.properties import java.util.logging.*; import java.io.*; public class LogMngTest { public LogMngTest() { try { LogManager.getLogManager().readConfiguration( new FileInputStream("de/jse6buch/kap28/LoggingInit.properties")); } catch(IOException ioEx) { System.out.println("Fehler bei der Initialisierung"); } Logger l = Logger.getLogger("de.jse6buch.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.7: \Beispiele\de\jse6buch\kap28\LogMngTest.java public class LogMngInit { public LogMngInit() { Listing 28.8: \Beispiele\de\jse6buch\kap28\LogMngInit.java
Java 6
789
28 – Logging
System.out.println("Initialisierung des LogManagers"); } } Listing 28.8: \Beispiele\de\jse6buch\kap28\LogMngInit.java (Forts.)
Systemeigenschaften für das Logging Beim Start einer Anwendung können weitere Systemeigenschaften für das Logging gesetzt werden. Der Eigenschaft java.util.logging.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 gesetzt, 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 Log-Eintrages festlegen. 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.
Beispiel 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.
790
Filter verwenden
import java.util.logging.*; public class LogFilter { public LogFilter() { Logger l = Logger.getLogger("de.jse6buch.kap28"); l.setFilter(new InfoFilter()); /* // oder über eine anonyme Klasse l.setFilter(new Filter() { 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); } } Listing 28.9: \Beispiele\de\jse6buch\kap28\LogFilter.java
Hinweis Wie bereits beschrieben, können Sie nur einem Filter pro Logger und Handler zuordnen. Durch eine geschickte Implementierung der Methode isLoggable() können aber auch Filterketten definiert werden, die dabei durchlaufen werden.
Java 6
791
28 – Logging
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 schon früher zur Verfügung stand. Log4j besitzt außerdem ein wesentlich umfangreicheres API, das kaum Wünsche offen lässt. Aus diesen Gründen soll es hier kurz erwähnt werden. Zu beziehen ist Log4j unter der URL http://logging.apache.org/. Die aktuelle Version 1.2.13 können Sie als ZIP-Datei logging-log4j-1.2.13.zip der Größe von ca. 3,4 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 Version arbeitete problemlos mit der JSE 6 zusammen. Die wichtigsten Klassen und Interfaces befinden sich im Package org.apache.log4j. Im Verzeichnis ..\docs\manual.html der log4jInstallation finden Sie ein gut zwanzigseitiges Manual, das Ihnen den Einstieg erleichtert.
Beispiel Die Arbeitsweise des Programms entspricht fast dem des Logging APIs. Ein Logger wird wie bisher über die Methode getLogger() der Klasse Logger 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. 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(); } } Listing 28.10: \Beispiele\de\jse6buch\kap28\LogLog4j.java
792
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- und Binärdateien oder in *.properties-Dateien speichern. Dabei können aber gleich 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 steht den Entwicklern das Preferences API zur Verfügung, 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.
Java 6
793
29 – Preferences
Beispiel Das folgende Beispiel fügt in der Wurzel der Preferences-Hierarchie jeweils in den Benutzer- und Systemeinstellungen ein neues Name-Wert-Paar ein. Anschließend wird der Wert aus den Systemeinstellungen ausgelesen und ausgegeben. 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(); } } Listing 29.1: \Beispiele\de\jse6buch\kap29\Einstellungen.java
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 (in der Registry) und Linux (im Dateisystem) 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
Tabelle 29.1: Ablageorte von Benutzerdaten im Preferences API
794
Speichern und Laden von Einstellungen
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.
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.
Java 6
795
29 – Preferences
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. 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.
796
Zugriff auf die Hierarchie
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.
Abbildung 29.1: Hierarchie von Knoten
Java 6
797
29 – Preferences
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/jse6buch 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/jse6buch/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/jse6buch"); Preferences pref2 = pref1.node("kap29"); Preferences pref3 = pref2.node("/de/jse6buch/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.jse6buch.kap29.Test"));
798
Zugriff auf die Hierarchie
Hinweis 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. Preferences p = userNodeForPackage(Klassenname.class);
Hinweis 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 6
799
29 – Preferences
Hinweis 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.
Beispiel 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. import java.util.prefs.*; public class KnotenZugriff { public KnotenZugriff() { Preferences prefU = Preferences.userRoot().node("de/jse6buch"); 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/jse6buch/kap29/KnotenZugriff")) System.out.println("Knoten erfolgreich entfernt!"); } catch(BackingStoreException bsEx) {} } public static void main(String args[]) { new KnotenZugriff(); } } Listing 29.2: \Beispiele\de\jse6buch\kap29\KnotenZugriff.java
800
Reagieren auf Änderungen
Als Ausgabe wird erzeugt: KnotenZugriff /de/jse6buch/kap29/KnotenZugriff /de/jse6buch/kap29 Benutzereinstellungen... Knoten erfolgreich entfernt!
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.
Beispiel 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.
Java 6
801
29 – Preferences
import java.util.prefs.*; public class EinstellungenInfo { public EinstellungenInfo() { Preferences prefUser = Preferences.userRoot(); prefUser = prefUser.node("/de/jse6buch/kap29"); prefUser.addPreferenceChangeListener( 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(); } } Listing 29.3: \Beispiele\de\jse6buch\kap29\EinstellungenInfo.java
802
Preferences exportieren und importieren
Es erfolgt die Ausgabe: Aenderungen Name/Wert-Paar /de/jse6buch/kap29 Test Wert Aenderungen Name/Wert-Paar /de/jse6buch/kap29 Test null Neuer Knoten /de/jse6buch/kap29
29.5 Preferences exportieren und importieren Möchten Sie die festgelegten 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 Exportmethoden werden die Einstellungen des aktuellen Knotens bzw. auch die seiner untergeordneten Knoten exportiert. void exportNode(OutputStream os) void exportSubtree(OutputStream os)
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)
Beispiel Die Anwendung speichert den gesamten Baum ab dem Knoten /de 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. import java.io.*; import java.util.prefs.*; public class Export { public Export() { Listing 29.4: \Beispiele\de\jse6buch\kap29\Export.java
Java 6
803
29 – Preferences
try { Preferences prefUser = Preferences.userRoot(); prefUser = prefUser.node("/de"); prefUser.exportSubtree(new FileOutputStream( new File("de/jse6buch/kap29/Backup.xml"))); } catch(IOException ioEX) {} catch(BackingStoreException bsEX) {} } public static void main(String args[]) { new Export(); } } Listing 29.4: \Beispiele\de\jse6buch\kap29\Export.java (Forts.)
Die erzeugte XML-Datei kann beispielsweise den folgenden Aufbau besitzen. Über das Element und dessen Attribut type wird festgehalten, ob es sich um Benutzer- oder Systemeinstellungen in der XML-Datei handelt. Listing 29.5: Aufbau der exportierten XML-Datei
804
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 dazu den Prozessen nacheinander eine bestimmte Rechenzeit zu. 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
Abbildung 30.1: Rechenzeitzuteilung für Threads einer Anwendung
Java 6
805
30 – Threads
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. Wenn Sie beispielsweise mehrere Dateien gleichzeitig aus dem Internet laden, kann dies durch mehrere parallele Threads erfolgen, da jeder Download eventuell mit unterschiedlicher Geschwindigkeit erfolgt und die Anwendung den Hauptteil der Zeit auf Daten warten muss. Da mehrere Threads die Daten laden, blockieren sich die Downloads nicht gegenseitig. 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 einer Rechtschreibprüfung 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
Tabelle 30.1: Vergleich zwischen der Klasse Thread und dem Interface Runnable
Hinweis 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 in diesem Fall auf der Konsole durch das Betätigen von (Strg)+(C). Weiterhin kann es unter Linux beim Ausführen der Beispiele zu Problemen kommen, da der Scheduler die Threads nicht wie unter Windows behandelt.
806
Threads über die Klasse Thread erzeugen
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 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 in der Methode start() durchgeführt wird. Die Klasse Thread ist Bestandteil des Packages 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 sleep() für den Thread kann eine InterruptedException auslösen, die abgefangen werden muss. static void sleep(long millis) static void sleep(long millis, int nanos)
Java 6
807
30 – Threads
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). 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.
Hinweis 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\technotes\guides\concurrency\threadPrimitiveDeprecation.html. Grundsätzlich hat es mit Synchronisationsproblemen bei Verwendung mehrerer Threads zu tun.
Hinweis 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.
Beispiel 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().
808
Threads über die Klasse Thread erzeugen
import java.util.*; public class ThreadKlasse { Random rd = new Random(); public ThreadKlasse() { ThreadTest tt = new ThreadTest(); 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) {} } } 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) {} } } } Listing 30.1: \Beispiele\de\jse6buch\kap30\ThreadKlasse.java
Java 6
809
30 – Threads
Als eine mögliche Ausgabe erhalten Sie: Hauptprogramm: Thread: 0 Hauptprogramm: Thread: 1 Thread: 2 Hauptprogramm: Hauptprogramm: Thread: 3 ... Hauptprogramm:
0 1
2 3
9
Hinweis 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 abgeleitet bzw. besteht gegebenenfalls diese Notwendigkeit, kann eine neue Thread-Klasse auch durch die Implementierung des Interfaces Runnable erzeugt werden. Die Klasse Thread implementiert dieses Interface übrigens auch. Das Interface Runnable besitzt nur die Methode run() und 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 Interfaces 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();
810
Threads über das Interface Runnable erzeugen
Beispiel 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. 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) {} } } 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); Listing 30.2: \Beispiele\de\jse6buch\kap30\ThreadInterface.java
Java 6
811
30 – Threads
try { Thread.sleep((rd.nextInt(5) + 1) * 100); } catch(InterruptedException ieEx) {} } } } Listing 30.2: \Beispiele\de\jse6buch\kap30\ThreadInterface.java (Forts.)
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 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
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 Threads 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()
812
Threads unterbrechen
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(); }
Beispiel Im fünften Schleifendurchlauf des Hauptprogramms soll der parallel laufende Thread beendet werden. Hierfür wird dessen Methode interrupt() aufgerufen. In der ThreadKlasse 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 catchBlock die Methode interrupt() erneut aufgerufen, um das Flag wiederherzustellen. 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(); } Listing 30.3: \Beispiele\de\jse6buch\kap30\ThreadStop.java
Java 6
813
30 – Threads
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); } catch(InterruptedException ieEx) { System.out.println("InterruptedException"); interrupt(); } } } } Listing 30.3: \Beispiele\de\jse6buch\kap30\ThreadStop.java (Forts.)
814
Zustände eines Threads
Das Ergebnis der Ausführung ist: ... Thread: 5 Hauptprogramm: 3 Thread unterbrechen Hauptprogramm: 4 InterruptedException Thread wird beendet Hauptprogramm: 5 ... Hauptprogramm: 9
Hinweis 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\technotes\guides\concurrency\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
Abbildung 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.
Java 6
815
30 – Threads
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 anschließend wieder im Zustand BEREIT. Alternativ BLOCKIERT ein Thread, wenn er auf Daten wartet, die ihm beispielsweise über einen Stream bereitgestellt werden. Liegen die Daten vor und teilt ihm der Scheduler wieder Rechenzeit zu, LÄUFT der Thread 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()
Über die Methode getState() können Sie differenziertere Informationen zum Zustand eines Threads erhalten. Dazu besitzt die Klasse Thread die Aufzählung State mit den Konstanten NEW, BLOCKED, RUNNABLE, TERMINATED und TIME_WAITING, WAITING. Thread.State getState()
Beispiel 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. public class ThreadStatus { public ThreadStatus() { ThreadStatusTest tst = new ThreadStatusTest(); System.out.println(tst.getState()); tst.start(); System.out.println(tst.getState()); try { Listing 30.4: \Beispiele\de\jse6buch\kap30\ThreadStatus.java
816
Prioritäten
Thread.sleep(500); System.out.println(tst.getState()); Thread.sleep(1500); System.out.println(tst.getState()); } 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) {} } } Listing 30.4: \Beispiele\de\jse6buch\kap30\ThreadStatus.java (Forts.)
30.6 Prioritäten Für die Umschaltung zwischen mehreren Threads verwendet das Betriebssystem verschiedene Informationen. Es muss 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.
Java 6
817
30 – Threads
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 z.B. nur 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. 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, wobei eine höhere Zahl hier eine höhere Priorität bedeuten soll. 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)
Hinweis 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.
Beispiel 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 500.000 Mal durchlaufen und alle 50.000 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.
818
Daemon-Threads
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(); } 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()); } } } Listing 30.5: \Beispiele\de\jse6buch\kap30\ThreadPrio.java
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.
Java 6
819
30 – Threads
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 UserThread 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()
Beispiel 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. 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 ... "); } } Listing 30.6: \Beispiele\de\jse6buch\kap30\ThreadDaemonTest.java
820
Timer
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() { } }
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 hierfür im Konstruktor den Wert true. Timer t = new Timer(true);
Mit der überladenen Methode schedule() 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 drei Sekunden und eine andere Task soll ab sofort jede Sekunde ausgeführt werden, müssen getrennte Timer verwendet werden. void void void void
Java 6
schedule(TimerTask task, long schedule(TimerTask task, long scheduleAtFixedRate(TimerTask schedule(TimerTask task, Date
verz) verz, long periode) task, long verz, long periode) zeit)
821
30 – Threads
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()
Hinweis 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. 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 eine noch nicht gestartete Task beenden. Laufende Tasks werden durch den Aufruf von cancel() nicht beendet. Periodisch ausgeführte Tasks werden beim nächsten Mal nicht wieder gestartet. boolean cancel() abstract void run()
Beispiel 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 s 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. import java.util.*; public class Zeitgeber { public Zeitgeber() Listing 30.7: \Beispiele\de\jse6buch\kap30\Zeitgeber.java
822
Timer
{ 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; 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) {} } } } Listing 30.7: \Beispiele\de\jse6buch\kap30\Zeitgeber.java (Forts.)
Java 6
823
30 – Threads
Sie erhalten ungefähr die folgende Ausgabe: Schlafe 3.2 Sekunden Thread 1: Runde 0 ... Thread 1: Runde 9 Thread 2: Runde 0 ... Thread 2: Runde 9 Thread 2: Runde 0 Und weiter geht's Der 2. Thread wird gecancelt. Thread 2: Runde 1 ... Thread 2: Runde 9
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 die Untergruppen angewandt werden. ThreadGroup(String name) ThreadGroup(ThreadGroup parent, String name)
824
Synchronisation
Ü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 Thread-Gruppen 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)
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.
Java 6
825
30 – Threads
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.
Beispiel 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. 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(); Listing 30.8: \Beispiele\de\jse6buch\kap30\ThreadWarte.java
826
Synchronisation
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()); } } } Listing 30.8: \Beispiele\de\jse6buch\kap30\ThreadWarte.java (Forts.)
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()
Java 6
827
30 – Threads
30.10.3 Monitore Beim Zugriff mehrerer Threads auf die gleichen Daten, z.B. durch den Aufruf einer Methode desselben 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.
Beispiel Die Klasse Konto verwaltet einen Kontostand, der mit 0 initialisiert wird. Über die Methode aendern() kann der Kontostand geändert, über getKontoStand() abgerufen werden. Es werden zwei Threads erzeugt, die beide den Kontostand manipulieren. Den Threads wird dasselbe 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. public class SyncFehler { public SyncFehler() { Konto k = new Konto(); SyncFehlerTest sft1 = new SyncFehlerTest(k); Listing 30.9: \Beispiele\de\jse6buch\kap30\SyncFehler.java
828
Synchronisation
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); System.out.println(k.getKontoStand()); break; Listing 30.9: \Beispiele\de\jse6buch\kap30\SyncFehler.java (Forts.)
Java 6
829
30 – Threads
} } } } Listing 30.9: \Beispiele\de\jse6buch\kap30\SyncFehler.java (Forts.)
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. synchronized methode() {} synchronized(Object o) {}
830
Synchronisation
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 6
831
30 – Threads
Hinweis 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.
Hinweis 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.
Beispiel 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. synchronized(k) { zaehler++; k.aendern(100); k.aendern(-100); if(k.getKontoStand() != 0) { System.out.println("Runde: " + zaehler); System.out.println(k.getKontoStand()); break; } } Listing 30.10: \Beispiele\de\jse6buch\kap30\Monitor.java
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.
832
Synchronisation
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 wurden. Der Thread, der für die Anzeige der gesamten HTMLSeite 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 damit in die Warteschlange einreihen. Dadurch 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 6
833
30 – Threads
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()
Beispiel 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 bei einer leeren ArrayList die Methode wait()auf und gehen in einen Wartezustand über. 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 (Strg)+(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. 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"); Listing 30.11: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug)
834
Synchronisation
prod.start(); kon1.start(); kon2.start(); } public static void main(String[] args) { new Kooperation(); } } Listing 30.11: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug) (Forts.)
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. 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) {} Listing 30.12: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug)
Java 6
835
30 – Threads
al.notifyAll(); } } } } Listing 30.12: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug) (Forts.)
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. 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); Listing 30.13: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug)
836
Synchronisation
System.out.println(getName() + ": " + wert); } } } } } Listing 30.13: \Beispiele\de\jse6buch\kap30\Kooperation.java (Auszug) (Forts.)
Eine mögliche Ausgabe wäre: Konsument Konsument Konsument Konsument Konsument Konsument Konsument
1: 1: 1: 2: 1: 1: 1:
2 5 ich warte ... ich warte ... 12 6 4
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 beispielsweise 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.
Java 6
837
30 – Threads
belegt
benötigt
Thread A
Ressource X
Ressource Y
Thread B
Ressource Y
Ressource X
Tabelle 30.2: Deadlock-Szenarien
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 (Strg)+(Pause) bzw. unter Linux (Strg)+(\). 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.
Beispiel 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 Reihenfolge 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. 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(); } Listing 30.14: \Beispiele\de\jse6buch\kap30\Deadlocks.java
838
Synchronisation
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) { 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) { Listing 30.14: \Beispiele\de\jse6buch\kap30\Deadlocks.java (Forts.)
Java 6
839
30 – Threads
synchronized(o1) { synchronized(o2) { System.out.println("Thread 2"); } } } } } Listing 30.14: \Beispiele\de\jse6buch\kap30\Deadlocks.java (Forts.)
Betätigen Sie, nachdem die Anwendung keine Ausgaben mehr durchführt, unter Windows die Tastenkombination (Strg)+(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.jse6buch.kap30.ThreadDead2.run(Deadlocks.java:61) - waiting to lock (a java.lang.Object) - locked (a java.lang.Object) "Thread-0": at de.jse6buch.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. 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.
840
Datenaustausch zwischen Threads
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.
Beispiel 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. 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. 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("Hallo\n"); bw.flush(); bw.write("QUIT\n"); bw.flush(); } catch(IOException ioEx) {} } } Listing 30.15: \Beispiele\de\jse6buch\kap30\ThreadKommunikation.java
Java 6
841
30 – Threads
Der Thread ThreadKonsument liest die Daten mit einem BufferedReader und einem PipedReader aus der Pipe. Damit lassen sich die Zeichenketten zeilenweise verarbeiten. Nach der Übergabe der Zeichenkette QUIT wird die while-Schleife und damit auch die Methode run() beendet. 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) {} } } } Listing 30.16: \Beispiele\de\jse6buch\kap30\ThreadKommunikation.java
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 PipeObjekt übergeben. import java.io.*; public class ThreadKommunikation { public ThreadKommunikation() { PipedReader pipeIn = new PipedReader(); Listing 30.17: \Beispiele\de\jse6buch\kap30\ThreadKommunikation.java
842
Die Concurrency Utilities
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(); } } Listing 30.17: \Beispiele\de\jse6buch\kap30\ThreadKommunikation.java (Forts.)
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 wurden in die J2SE 5.0 integriert. Im JCP (Java Community Process) 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 unter anderem 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
Java 6
843
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. 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; } } } } Listing 30.18: \Beispiele\de\jse6buch\kap30\Semaphor.java
844
Die Concurrency Utilities
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.
Beispiel 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. 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; } } } } Listing 30.19: \Beispiele\de\jse6buch\kap30\Semaphor.java
Java 6
845
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 aus aller Welt miteinander kommunizieren. Dabei kann die Kommunikation untereinander in beliebiger Richtung und zwischen beliebig vielen Kommunikationspartnern stattfinden. Einige Computer (Clients) nehmen dabei die Serviceleistungen anderer Computer (Server) in Anspruch. Ein Computer kann während dieser Kommunikation als Client wie auch als Server auftreten. Typische Netzwerkanwendungen sind: 쮿
Datenbankanwendungen, bei denen sich auf einem Rechner das Datenbanksystem befindet und auf das 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, die E-Mails an einen Mailserver schicken und davon abrufen
쮿
Sämtliche Anwendungen, die eine Kommunikation zwischen zwei oder mehreren Rechnern herstellen (z.B. FileServer)
Für die Herstellung einer Verbindung zwischen mehreren Computern im Netzwerk müssen sich diese auf gewisse Kommunikationsregeln, so genannte Protokolle, verständigen. Die 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 7 Schichten, vgl. Abbildung 31.1. Jede Schicht verfügt über ein oder mehrere Protokolle, die auf den darunter liegenden Protokollen 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, WLan o.a.) zum PC 2 übertragen, wo sie wieder die Schichten 1 bis 5 durchlaufen.
Java 6
847
31 – Netzwerkanwendungen
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
Abbildung 31.1: Das ISO/OSI-Referenzmodell
Die Schichten 1 und 2 sorgen mit der physikalischen Bereitstellung (Kabel, Netzwerkkarten, Funkverbindungen) 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 Weges von A nach B, wird in der Schicht 3 erledigt. Hierfür wird heutzutage in der Regel das IP-Protokoll (Internet Protocol) eingesetzt. Die 4. 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 Reihenfolge 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 das Übertragungsprotokoll und erfordern einen relativ hohen 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 dabei auch nicht sicher, dass die Datenpakete in der Ausgangsreihenfolge beim Empfänger ankommen. UDP sichert nicht einmal, das 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-
848
Einführung
ü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 IPAdresse über vier Byte kodiert werden kann. Theoretisch kann man auf diese Weise ca. vier Milliarden IP-Adressen vergeben. Da dies aufgrund der Vielzahl von Rechnern und reservierten Bereichen kaum noch reicht, wurden neue IP-Adressen definiert (IPv6), die durch 16 Bytes kodiert werden. Bisher kommen diese aber in öffentlichen Netzwerken nicht bzw. kaum zum Einsatz und werden im Folgenden nicht weiter berücksichtigt. Die IP-Adressen innerhalb eines geschlossenen Netzwerkes 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, WLAN oder DSL in das Internet, weist Ihnen Ihr Internet Service Provider (z.B. T-Online oder 1&1) dynamisch eine IP-Adresse 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 Netzwerkes. 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 einem unterschiedlichen Umfang (Anzahl der verfügbaren IP-Adressen) 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
Tabelle 31.1: Netzwerk-Klassen im Internet
Java 6
849
31 – Netzwerkanwendungen
Domain Name Service Da sich IP-Adressen schlecht merken lassen, wurden sprechendere Hostnamen eingeführt, die einer IP-Adresse 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. URLs (Uniform Ressource Locator) verwendet. Manchmal wird auch der umfassendere Begriff URI (Uniform Ressource Identifier) zur Identifikation von Netzwerkressourcen benutzt. Eine URI ist dabei eine URL oder eine URN (Uniform Ressource Name). URNs werden momentan kaum verwendet, so dass wir wieder bei URLs sind.
URI URL
URN
Abbildung 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. Diese Anzahl 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 Linus unter /etc. Besonders unter Linux ist der Inhalt der Datei sehr aufschlussreich, da diese sehr viele Einträge enthält.
850
Einführung
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
Tabelle 31.2: Übersicht der Portbelegung von Standarddiensten
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 einem 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 einen 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
Tabelle 31.3: Auswahl einiger RFCs
Java 6
851
31 – Netzwerkanwendungen
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. 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 solange, 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 feste 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.
Hinweis 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 Proxies liegen, die den Datenverkehr zu bestimmten Ports oder Internetadressen verhindern. Fragen Sie in diesem Fall Ihren Netzwerkadministrator. Proxies 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 Netz-Adressen Bei der Erstellung eines Sockets kann später als Parameter ein Objekt vom Typ InetAddress übergeben werden, um einen Host zu identifizieren. Vorteilhaft daran ist, dass 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 Netzwerkes immer eindeutig. Aller-
852
Zugriff auf Netz-Adressen
dings 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 mit Hilfe eines DNS-Servers durchgeführt.
Hinweis 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 vier Byte eine IPv4, bei Angabe von 16 Byte eine IPv6-Adresse 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 IPAdresse 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 als Byte-Array. 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
Java 6
getCanonicalHostName() getHostAddress() getHostName() toString()
// // // //
DNS-Host 192.168.10.10 Mein-PC Mein-PC/192.168.10.10
853
31 – Netzwerkanwendungen
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)
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 Cacheverhalten steuern: networkaddress.cache.ttl networkaddress.cache.negative.ttl
Das Kürzel 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.
Beispiel Um 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. 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()); } Listing 31.1: \Beispiele\de\jse6buch\kap31\InternetAdressen.java
854
Zugriff auf Netz-Adressen
catch(Exception _uh) {} } public static void main(String args[]) { new InternetAdressen(); } } Listing 31.1: \Beispiele\de\jse6buch\kap31\InternetAdressen.java (Forts.)
Die Ausgabe könnte z.B. so aussehen: MeinRechner 192.168.0.12 MeinRechner MeinRechner/192.168.0.12 ---------------------------localhost/127.0.0.1
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, die in der JSE 6 einige Erweiterungen erfahren hat. Zu jeder Netzwerkschnittstelle lassen sich zahlreiche Informationen abfragen. Mittels der Methode getName() wird der Name der Schnittstelle erfragt. Die damit verbundenen Internetadressen gibt die Methode getInetAddresses() als Aufzählung zurück. Von den neu hinzugekommenen Methoden liefert die Methode isUp() eine Information, ob die Schnittstelle aktiv ist. Der Test der Schnittstelle auf eine mögliche Loopback-Schnittstelle erfolgt mittels der Methode isLoopback(). Möchten Sie in Ihren Router einen MAC-Filter aktivieren, ist die Kenntnis der MAC-Adresse, d.h. die weltweit eindeutig Adresse Ihrer Netzwerkkarte von Interesse. Die MAC-Adresse wird durch die Methode getHardwareAddress() als Byte-Array geliefert. Zur Optimierung der Datenübertragung im Netzwerk wird oft der MTU-Wert (Maximum Transmission Unit) benötigt und entsprechend geändert. Die Methode getMTU() liefert diesen Wert zurück. String getName() Enumeration getInetAddresses() boolean isUp() byte[] getHardwareAddress() int getMTU()
Java 6
855
31 – Netzwerkanwendungen
Beispiel Über die Methode getNetworkInterfaces() wird ein Enumerator-Objekt zurückgegeben, mit dessen Hilfe die einzelnen Netzwerkschnittstellen durchlaufen werden können. Durch die Verwendung von Generics kann man sich den Cast beim Aufruf von nextElement() sparen. 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. Zu jeder Schnittstelle werden verschiedene Informationen angezeigt. Die Methode getMACAddress() dient zur einfachen Formatierung der MAC-Adresse. Unter Windows
können Sie die Ausgabe durch den Aufruf des Tool ipconfig auf der Konsole überprüfen, z.B. > ipconfig /all 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("Name: " + ni.getName()); System.out.println("Display-Name: " + ni.getDisplayName()); System.out.println("Aktiv: " + ni.isUp()); System.out.println("MAC-Adresse: " + getMACAddress(ni.getHardwareAddress())); System.out.println("MTU: " + ni.getMTU()); Enumeration ias = ni.getInetAddresses(); while(ias.hasMoreElements()) { InetAddress ia = ias.nextElement(); System.out.println(ia.getHostAddress()); } System.out.println("========================"); } Listing 31.2: \Beispiele\de\jse6buch\kap31\Netzschnittstellen.java
856
Zugriff auf Netz-Adressen
} catch(Exception _uh) { } } public String getMACAddress(byte[] address) { String adr = ""; if(address == null) return "[keine]"; for(int i = 0; i < address.length; i++) { if(i == 0) adr = String.format("%X", address[i]); else adr = adr + "-" + String.format("%X", address[i]); } return adr; } public static void main(String args[]) { new Netzschnittstellen(); } } Listing 31.2: \Beispiele\de\jse6buch\kap31\Netzschnittstellen.java (Forts.)
Als Ausgabe erhalten Sie etwa: Name: lo Display-Name: MS TCP Loopback interface Aktiv: true MAC-Adresse: MTU: 1520 127.0.0.1 ======================== Name: eth0 Display-Name: Marvell Yukon Gigabit Ethernet 10/100/1000Base Aktiv: MAC-Adresse: 0-E-1A-B4-E2-C3 MTU: 1500 192.168.0.12 ========================
Java 6
true
857
31 – Netzwerkanwendungen
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 Protokoll 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 an 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/jse6buch/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. 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)
858
Arbeiten mit URLs
Beispiele An die absolute URL des ersten Konstruktors werden 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.
31.3.3 String getFile()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 auf einfache Weise auf die Festplatte speichern.
Beispiel 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 TagesschauWebSeite 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. Beachten Sie, wenn Sie die geladene WebSeite anzeigen, dass aufgrund fehlender StyleSheets und anderer Dateien die Anzeige der Seite nicht sehr attraktiv ist. Dazu müssten Sie alle weiteren benötigten Dateien nachladen. import java.net.*; import java.io.*; public class URLDatenLesen { public URLDatenLesen() { URL url1 = null; try { Listing 31.3: \Beispiele\de\jse6buch\kap31\URLDatenLesen.java
Java 6
859
31 – Netzwerkanwendungen
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(); } } Listing 31.3: \Beispiele\de\jse6buch\kap31\URLDatenLesen.java (Forts.)
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 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.
860
Arbeiten mit URLs
Hinweis 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.3.4 Einen WebServer erstellen Etwas versteckt hat Sun im neuen JDK Klassen zur Erstellung eines (kleinen) HTTP-Servers untergebracht. Das »klein« bezieht sich hier aber eher auf die zu nutzende Grundfunktionalität, denn fast die gesamte Logik der URL-Verarbeitung muss manuell implementiert werden. Allerdings stehen ausreichend Klassen zur Verfügung, um eingehende Anfragen zu verarbeiten, eine Authentifizierung vorzunehmen, das HTTP- wie auch das HTTPS-Protokoll zu nutzen oder Filter und Handler zu entwickeln. Die Klassen finden Sie im Package com.sun.net.httpserver, was auf einen eher internen Gebrauch schließen lässt, obwohl sie alle im Archiv rt.jar der Laufzeitumgebung enthalten sind. Die APIDokumentation öffnen Sie über die Datei index.html im Verzeichnis ..\docs\jre\api\net\ httpserver\spec. Da die WebServer-Klassen nicht zum Standard-API gehören, wird deren Gebrauch leider kaum von SUN erläutert.
Den Server erstellen Ausgangspunkt für eigene Entwicklungen ist die abstrakte Klasse HttpServer (oder HttpsServer für die sichere Kommunikation). Mittels der Methode create() wird eine Instanz der Klasse erstellt. In der zweiten Variante kann der Server an einen konkreten Port gebunden werden und festlegen, wie viele eingehende Verbindungen in eine Warteschlange eingereiht werden können. static HttpServer create() static HttpServer create(InetSocketAddress adr, int backlog)
Normalerweise werden an einen WebServer Anfragen nach einer bestimmten URL gestellt und diese von ihm beantwortet. Die Anfrage nach der Seite http://localhost:8080/ index.html liefert die HTML-Datei index.html vom lokalen Rechner (localhost), wobei der Server auf dem Port 8080 läuft. Die Angabe von 8080 ist hier notwendig, wenn der Server nicht auf dem Standard-HTTP-Port 80 läuft. Die Funktionalität, auf Anfrage eine bestimmte Datei zu öffnen und deren Inhalt an den Browser (o.a.) zurückzuliefern, ist bei diesen Klassen nicht automatisch vorhanden und muss von Ihnen selbst implementiert werden. Die Verarbeitung einer Anfrage nach einer bestimmten (oder allen möglichen) URL wird durch einen HttpHandler durchgeführt. Dazu implementiert eine Klasse das Interface HttpHandler, indem es die Methode handle() mit Leben füllt. Nachdem ein Handler vorliegt, kann die Methode createContext() der Klasse HttpServer aufgerufen werden, um eine relative URL mit einem Handler zu verknüpfen.
Java 6
861
31 – Netzwerkanwendungen
HttpContext createContext(String path) HttpContext createContext(String path, HttpHandler handler)
Jetzt muss der Server nur noch über die Methode start() gestartet werden. Das Beenden des Servers erledigt die Methode stop(). void start() void stop(int wartezeit)
Handler verwenden Ein oder mehrere Handler, die mit einer URL verknüpft werden, verarbeiten sämtliche Anfragen, die an diese URL und alle damit beginnenden Anfragen gerichtet sind. Angenommen, es sind die folgenden Handler mit den vorn angegebenen URLs verknüpft (Wildcardzeichen werden hier leider nicht unterstützt): / /images/ /images/gif/
- MainHandler - ImageHandler - ImageGifHandler
Je nach Anfrage werden diese dann an die folgenden Handler weitergeleitet: /index.html /daten/test.zip /images/bild1.jpg /images/gif/bild1.gif
=> => => =>
MainHandler MainHandler ImageHandler ImageGifHandler
Eine Anfrage (Request) wird über ein HttpExchange-Objekt an die Methode handle() eines HttpHandlers weitergegeben. void handle(HttpExchange exchange)
Darüber können die Methoden getRequestHeaders() und getResponseHeaders() aufgerufen werden, um Informationen zum HTTP-Header der Anfrage zu erhalten und andererseits den HTTP-Header für die Antwort (Response) zu bearbeiten. Headers getRequestHeaders() Headers getResponseHeaders()
Um beispielsweise festzulegen, dass die Antwort aus HTML besteht und die Anfrage fehlerfrei bearbeitet wurde (Code 200 und Länge der Antwort unbegrenzt) werden die beiden ersten Anweisungen verwendet. Dabei wird sofort der Antwort-HTTP-Header versendet (sendResponseHeaders()). Um Zugriff auf den Antwortstream zu erhalten, wird die Methode getResponseBody() aufgerufen.
862
Arbeiten mit URLs
responseHeaders.set("Content-Type", "text/html"); ex.sendResponseHeaders(200, 0); OutputStream responseBody = ex.getResponseBody();
Über die Methode write() können Sie nun die Antwort zusammenstellen. Diese kann aus statischem Text, Text aus einer Datei oder auch binären Daten wie Bildern oder dem Inhalt von PDF-Dateien bestehen. Im Falle von Bildern und PDF-Dateien müssen Sie allerdings das Attribut Content-Type im HTTP-Header mit den entsprechenden Werten versehen. Möchten Sie statt HTML nur Text-Daten zurückgeben (dann werden z.B. HTML-Tags auch nicht vom Browser verarbeitet) verwenden Sie den Typ text/plain. text/plain text/html image/gif image/jpg application/pdf
einfacher Text HTML-Antwort ein GIF-Bild ein JPG-Bild ein PDF-Dokument
Beispiel Die folgende Implementierung eines kleinen WebServers verwendet den Port 8080 des lokalen Rechners. Für alle URL-Anfragen an die Wurzel der »virtuellen Verzeichnisstruktur« des WebServers (die tatsächliche Lage der Dateien ist ja vom Handler abhängig) wird der Handler MainHandler() registriert. Damit der Server dauerhaft läuft, wird eine while-Schleife ohne Abbruchkriterium verwendet. Um den Server zu beenden, steht Ihnen momentan nur die Tastenkombination (Strg)+(C) zur Verfügung. Die Klasse MainHandler() implementiert das Interface HttpHandler() und damit auch die Methode handle(). Darin werden drei Anfragen an den Server behandelt. Die Angabe der URL http://localhost:8080/ in Ihrem Browser führt zur Ausgabe eines statischen Textes. Verwenden Sie stattdessen die URL http://localhost:8080/index.html, wird der Inhalt der Datei index.html an den Client zurückgegeben. Durch den Einsatz weiterer geeigneter Handler können Sie die Anfragen noch gezielter verarbeiten. Das Beispiel zeigt eine HTML-Seite an, die außerdem ein StyleSheet verwendet. Der Browser merkt beim Laden der HTML-Seite, dass eine weitere Datei benötigt wird und fordert dann auch diese vom WebServer an. Anfragen an weitere URLs werden momentan nicht verarbeitet. import com.sun.net.httpserver.*; import java.io.*; import java.net.*; public class WebServer { HttpServer webServer; Listing 31.4: \Beispiele\de\jse6buch\kap31\WebServer.java
Java 6
863
31 – Netzwerkanwendungen
public WebServer() { try { webServer=HttpServer.create(new InetSocketAddress(8080),0); webServer.createContext("/", new MainHandler()); webServer.start(); while(true) ; } catch(IOException ioEx) { } } public static void main(String[] args) { new WebServer(); } } class MainHandler implements HttpHandler { public void handle(HttpExchange ex) { try { URI uri = ex.getRequestURI(); String path = uri.getPath(); Headers requestHeaders = ex.getRequestHeaders(); Headers responseHeaders = ex.getResponseHeaders(); responseHeaders.set("Content-Type", "text/html"); ex.sendResponseHeaders(200, 0); OutputStream responseBody = ex.getResponseBody(); if(path.equals("/")) { responseBody.write(("Hallo vom Sun-Web-Server" + "").getBytes()); responseBody.write(("Sie haben die URL " + uri.getPath() + " verwendet.").getBytes()); } if(path.equals("/index.html") || path.equals("/css/style.css")) { int readLen; byte[] buff = new byte[32]; FileInputStream fis = new FileInputStream("de/jse6buch/kap31" + path); Listing 31.4: \Beispiele\de\jse6buch\kap31\WebServer.java (Forts.)
864
Arbeiten mit URLs
do { readLen = fis.read(buff); if(readLen > 0) responseBody.write(buff, 0, readLen); else break; } while(true); fis.close(); } responseBody.close(); ex.close(); } catch(IOException ioEx) {} } } Listing 31.4: \Beispiele\de\jse6buch\kap31\WebServer.java (Forts.)
Die Test-HTML-Datei zeigt lediglich in der Titelleiste des Browsers und in seinem Inhalt den Text »Willkommen bei SUN's HTTP-Server« an. Durch das eingebundene CSSStyleSheet, welches ebenfalls vom WebServer angefordert wird, wird die Überschrift rot und in einer anderen Schriftgröße dargestellt. Willkommen bei SUN's HTTP-Server Willkommen bei SUN's HTTP-Server Listing 31.5: \Beispiele\de\jse6buch\kap31\index.html h2 {color: #FF0000; font-size: 15px; } Listing 31.6: \Beispiele\de\jse6buch\kap31\css\style.css
Java 6
865
31 – Netzwerkanwendungen
31.4 Socketverbindungen 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 solange 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 Clientverbindungen 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. 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
Abbildung 31.3: Herstellen einer Socketverbindung
866
Socketverbindungen
Hinweis 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.
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 Sockets nicht möglich, wird eine IOException ausgelöst. Socket(String host, int port) Socket(InetAddress address, int port)
Nach dem Erstellen eines Sockets 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();
Ü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();
Java 6
// Zeilenumbruch über \n
867
31 – Netzwerkanwendungen
Damit besteht das Hauptproblem bei Socketanwendungen 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 wie im folgenden Quelltextausschnitt gezeigt 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 über die Methode close() wieder geschlossen werden. void close()
Um Daten aus einem Socket zu lesen, wird mit Hilfe der Methode getInputStream() 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 Sockets zurückgegeben. int getLocalPort()
Zum Schreiben über einen Socket wird ein OutputStream-Objekt genutzt, das mit der Methode getOutputStream() ermittelt wird.
868
Socketverbindungen
OutputStream getOutputStream()
Die Portnummer des Sockets 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 bzw. 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 Timeouts innerhalb der angegebenen Zeit keine Daten gelesen, löst dies eine SocketTimeoutException aus. void setSoTimeout(int millis)
Zugriff auf einen WebServer Für Socketanwendungen 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 angefordert GET /index.html HTTP/1.0
und in Textform über den OutputStream des Sockets an den WebServer gesendet. 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.
Beispiel 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 entfernte Portnummer vor und nach dem Verbindungsaufbau, eine Information, nachdem die Verbindung hergestellt wurde, sowie der Inhalt der angeforderten HTML-Datei ausgegeben.
Java 6
869
31 – Netzwerkanwendungen
Nach der erfolgreichen Verbindung und dem Download der HTML-Datei werden zuerst die Streams und danach der Socket geschlossen. Steht Ihnen keine Internetverbindung zur Verfügung, können Sie auch den unter dem Abschnitt 31.3.4 entwickelten Mini-WebServer verwenden. 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); 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); Listing 31.7: \Beispiele\de\jse6buch\kap31\HTTPClient.java
870
Socketverbindungen
} catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen."); System.exit(1); } } public static void main(String args[]) { new HTTPClient(); } } Listing 31.7: \Beispiele\de\jse6buch\kap31\HTTPClient.java (Forts.)
31.4.2 ServerSockets Serverseitig werden Sockets durch die Klasse ServerSocket implementiert. Die Konstruktoren unterscheiden sich von denen eines ClientSockets, 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 SocketObjekt zurück. Dieses Socket-Objekt wird mit demselben Port verbunden wie der Ausgangssocket. Die Verwendung eines neuen Sockets 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. 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()));
Java 6
871
31 – Netzwerkanwendungen
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ück gesendet, der eine ähnliche Verarbeitungsschleife implementieren kann. String eingang; String ausgang; while((eingang = sIn.readLine()) != null) { ausgang = verarbeite(eingang); sOut.println(ausgang); }
Beispiel Das Spiel Zahlenraten soll über eine Client-Server-Anwendung implementiert werden. Der Server erzeugt einen Socket am Port 10001 und wartet auf eine Clientanfrage. 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. Über das Kommando endegame 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. import java.net.*; import java.io.*; import java.util.*; public class ZahlenRatenServer { private int zufallsZahl = 0; public ZahlenRatenServer() { try { Socket cSock = null; Listing 31.8: \Beispiele\de\jse6buch\kap31\ZahlenRatenServer.java
872
Socketverbindungen
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"); erzeugeZufallszahl(); System.out.println("Die Zahl ist " + zufallsZahl); } else { if(eingangString.toLowerCase().equals("endegame")) Listing 31.8: \Beispiele\de\jse6buch\kap31\ZahlenRatenServer.java (Forts.)
Java 6
873
31 – Netzwerkanwendungen
{ 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(); } } Listing 31.8: \Beispiele\de\jse6buch\kap31\ZahlenRatenServer.java (Forts.)
874
Socketverbindungen
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 doZahlenRaten() 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 solange, bis die Zufallszahl erraten wurde. Diese Fleißarbeit überlassen wir Ihnen, da sie nichts an der prinzipiellen Funktionsweise der Anwendung ändert. 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(); Listing 31.9: \Beispiele\de\jse6buch\kap31\ZahlenRatenClient.java
Java 6
875
31 – Netzwerkanwendungen
} catch(Exception _uh) {} } public static void main(String[] args) { new ZahlenRatenClient(); } } Listing 31.9: \Beispiele\de\jse6buch\kap31\ZahlenRatenClient.java (Forts.)
Als Ausgabe der Kommunikation erhalten Sie beispielsweise: Auf dem Server: Client angemeldet StartGame Neue Partie Die Zahl ist 3 12 7 14 EndeGame
Auf dem Client: Groesser Groesser Groesser
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); }
876
Socketverbindungen
Beispiel Ü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 vom Server zwei Zahlen addieren und erhalten das Ergebnis zurück. Die Anwendungsklasse MatheServer wartet in einer Endlosschleife auf eingehende Anforderungen. Bei jeder Clientverbindung 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 Sekunden ohne Clientanforderungen 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 Clientnummer übergeben werden. Über die Methode start() wird der Thread gestartet. 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) {} Listing 31.10: \Beispiele\de\jse6buch\kap31\MatheServer.java
Java 6
877
31 – Netzwerkanwendungen
System.out.println("Mathe Server is shutting down..."); } public static void main(String[] args) { new MatheServer(); } } Listing 31.10: \Beispiele\de\jse6buch\kap31\MatheServer.java (Forts.)
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. 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() { Listing 31.11: \Beispiele\de\jse6buch\kap31\MatheServerThread.java
878
Socketverbindungen
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(); } 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) {} } } Listing 31.11: \Beispiele\de\jse6buch\kap31\MatheServerThread.java (Forts.)
Java 6
879
31 – Netzwerkanwendungen
Der Client verbindet sich mit dem Server auf dem lokalen Rechner und öffnet den Leseund den Schreibstream. Danach sendet er das Kommando ADD und die zwei zu addierenden Zahlen. Nach dem Auslesen des Rückgabewertes wartet er 5 Sekunden und sendet das Kommando QUIT an den Server. 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(); 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(); } } Listing 31.12: \Beispiele\de\jse6buch\kap31\MatheClient.java
880
Datagramme
Der Server gibt die folgenden Meldungen aus, bevor er wieder beendet wird: Mathe-Server läuft ... Neuer Client angemeldet: 1 Neuer Client angemeldet: 2 Client 1 abgemeldet Client 2 abgemeldet
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 max. 64 kB, 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. 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.
Java 6
881
31 – Netzwerkanwendungen
Beispiel Diese Anwendung sendet über einen Client einzelne Datenpakete an einen Server, die lediglich etwas Text enthalten. Dieser Weg kann beispielsweise von einer ChatAnwendung 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. 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(); } catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen."); System.exit(1); } } Listing 31.13: \Beispiele\de\jse6buch\kap31\DatagramClient.java
882
Datagramme
public static void main(String args[]) { new DatagramClient(); } } Listing 31.13: \Beispiele\de\jse6buch\kap31\DatagramClient.java (Forts.)
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 muss der Client wie auch der Server die Methode receive()verwenden, um eingehende Datenpakete zu empfangen. Im Gegensatz zum Server muss der Client aber den DatagramSocket nicht an einen festen Port binden, weil der Port dynamisch beim Erstellen des DatagramSockets zugewiesen wird.
Beispiel Die folgende Serveranwendung wartet am Port 5001 auf eingehende UDP-Pakete und gibt deren Inhalt sowie Informationen zum Absender (IP-Adresse, Port) aus. Die Methode receive() wartet nach dem Erstellen des Sockets auf eingehende Clientverbindungen und nimmt deren Datenpakete auf. Beginnt das Datenpaket mit dem Text QUIT, wird die while-Schleife 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. Möchten Sie, dass der Server nicht sofort nach Eingang der Nachricht QUIT beendet wird, lassen Sie den Thread z.B. noch 5 Sekunden schlafen.
Java 6
883
31 – Netzwerkanwendungen
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(); // Kommentare entfernen, damit der Server noch 5s schläft // try // { Thread.sleep(5000); } // catch(InterruptedException e) {} } catch(IOException ioEx) { System.out.println("Konnte Verbindung nicht herstellen."); System.exit(1); } Listing 31.14: \Beispiele\de\jse6buch\kap31\DatagramServer.java
884
Das Java Mail API
} public static void main(String args[]) { new DatagramServer(); } } Listing 31.14: \Beispiele\de\jse6buch\kap31\DatagramServer.java (Forts.)
Ausgabe vom Server: Warte ... Len: 128 HalliHallo 2276 /192.168.0.12 Len: 128 QUIT QUIT 2276 /192.168.0.12
31.6 Das Java Mail API Die JSE 6 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 das Bestandteil der J2EE ist. Das API dient zum Senden und Empfangen von E-Mails aber nicht zum Erstellen eines Mailservers. Das Java Mail API können Sie unter der URL http://java.sun.com/products/javamail/ beziehen. Extrahieren Sie die ZIP-Datei und kopieren Sie die Datei mail.jar in die Verzeichnisse ..\jre \lib\ext in den Installationsverzeichnissen des JDKs und des JREs. Alternativ können Sie das Archiv auch in den Klassenpfad einbinden. Für seine Verwendung benötigen Sie zusätzlich das Java Beans Activation Framework, welches ab dem JDK 6 dessen fester Bestandteil ist. Es wird zur Auswertung der verschiedenen Datenformate, die mit einer Mail versendet werden können, benötigt.
Hinweis Zur Verwendung des Java Beans Activation Frameworks in älteren JDKs können Sie es unter http://java.sun.com/products/javabeans/jaf/index.jsp beziehen. Aus der extrahierten ZIP-Datei benötigen Sie dann das Archiv activation.jar.
Java 6
885
31 – Netzwerkanwendungen
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 Mailserver 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.).
쮿
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 mit Hilfe der Klasse Authenticator.
Die Klasse Session Über die Klasse Session wird eine Mail-Verbindung verwaltet. Hierüber werden bestimmte Systemeigenschaften für den Mailverkehr ausgewertet, der Debugmodus 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
886
getInstance(Properties props) getInstance(Properties props, Authenticator aut) getDefaultInstance(Properties props) getDefaultInstance(Properties props, Authenticator aut)
Das Java Mail API
Wenn Sie den Debugmodus 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 Debugmeldungen 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)
Die Klasse Message Diese Klasse dient der Festlegung der Nachricht selbst und beschreibt den Inhalt, den Betreff, den Absender und 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 geben den Absender und den Empfänger an. Ü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() lässt sich als Kurzform von setContent() verwenden, 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");
Java 6
887
31 – Netzwerkanwendungen
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 MailAdresse 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);
Ü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-Mailserver 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"); } }
888
Das Java Mail API
Beispiel 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 Session-Objekt wird mit Hilfe des AuthentificatorObjekts erzeugt. Benötigen Sie kein Login, übergeben Sie der Methode getDefaultInstance() als zweiten Parameter den Wert null. Zur Ausgabe der SMTP-Kommandos wird der Debugmodus aktiviert. Nach dem Zusammensetzen der Nachricht in einem Message-Objekt wird sie über die Methode send() an den Mailserver verschickt. 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[]) { Listing 31.15: \Beispiele\de\jse6buch\kap31\MailSenden.java
Java 6
889
31 – Netzwerkanwendungen
new MailSenden(); } } class EasyAuthenticator extends Authenticator { public PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication("Benutzer", "Passwort"); } } Listing 31.15: \Beispiele\de\jse6buch\kap31\MailSenden.java (Forts.)
31.6.2 Mails empfangen Die Klasse Store Der Zugriff auf ein Mailkonto 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 sich der Client ü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 Postfaches 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();
890
Das Java Mail API
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);
Beispiel 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 werden der Ordner und anschließend die Verbindung zum Mailserver geschlossen. 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++) { Listing 31.16: \Beispiele\de\jse6buch\kap31\MailLesen.java
Java 6
891
31 – Netzwerkanwendungen
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."); } } public static void main(String args[]) { new MailLesen(); } } Listing 31.16: \Beispiele\de\jse6buch\kap31\MailLesen.java (Forts.)
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.
892
Das Java Mail API
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); msg.setContent(mpart); Transport.send(msg); Listing 31.17: \Beispiele\de\jse6buch\kap31\MailAnhangSenden.java
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. for(int idx = 0; idx < msgs.length; idx++) { if(msgs[idx].isMimeType("multipart/mixed")) { if(msgs[idx].getSubject().equals("Betrefffeld")) { Message msg = msgs[idx]; System.out.println("Nachricht: " + idx); System.out.println("From: " + msg.getFrom()[0]); System.out.println("Subject: " + msg.getSubject()); Listing 31.18: \Beispiele\de\jse6buch\kap31\MailAnhangLesen.java
Java 6
893
31 – Netzwerkanwendungen
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) } } } } } } Listing 31.18: \Beispiele\de\jse6buch\kap31\MailAnhangLesen.java (Forts.)
Hinweis Viele Klassen des Mail APIs 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 TransportListenerInterface-Objekt zu übergeben ist. Die Methoden dieses Interfaces werden beispielsweise aufgerufen, wenn eine Nachricht (nicht) erfolgreich versendet werden konnte.
894
XML 32.1 Einführung Die Verwendung und 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 XML-Dateien 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. Weiterhin werden die neuen APIs StAX und JAXB, die beide Einzug in das JDK gehalten haben, vorgestellt.
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/
쮿
http://java.sun.com/webservices/docs/2.0/tutorial/doc/
쮿
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 Java SE 6.0 unverändert in der Version 1.3 vor. Die Bestandteile der einzelnen Versionen können Sie in der Dokumentation des JDK 6.0 unter [InstallJDK]\docs\ technotes\guides\xml\jaxp\ReleaseNotes_150.html nachlesen. Gegenüber dem JDK 5.0 haben sich die Versionen des XML-Parsers Xerces (2.6.2) und des XSLT-Prozessors Xalan (2.6.0) nicht verändert. Auch sonst ist bei den XML-Basics alles beim Alten geblieben.
Java 6
895
32 – XML
Package-Namen Bei Verwendung von JAXP spielen die konkreten Package- und Klassennamen der Parser und XSLT-Prozessoren keine Rolle, da der Zugriff ü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, z.B. in der J2SE 1.4, integriert. Sie überschrieben damit die bereits vorhandenen Klassen und Packages. Zur einfacheren Einbindung künftiger aktuellerer Versionen von Xerces und Xalan über den CLASSPATH wurden die internen Package-Namen bereits im JDK 5.0 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.xslt
TrAX
Tabelle 32.1: Package-Namen in Xerces und Xalan im Vergleich zum JDK
Hinweis 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.
Hinweis 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/ herunter.
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.
896
XML-Grundlagen
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 Informationen darüber, wie die enthaltenen Daten formatiert werden sollen, d.h., es gibt keine Vorschrift, wie ein XML-Dokument angezeigt wird. Wenn Sie mittels XML die Daten eines Datensatzes 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 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 paarweise verwaltet. Zu einem öffnenden Tag wie muss es immer ein schließendes Tag geben. Der Inhalt eines Tags befindet sich zwischen dem öffnenden und dem schließenden Tag. Eine Ausnahme ist das leere Tag, das sofort wieder geschlossen wird und keinen Inhalt besitzt, z.B. . Ein Tag kann zusätzlich Attribute besitzen. Den Attributen wird durch ein Gleichheitszeichen ein Wert in Anführungszeichen zugewiesen, wie 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:
Java 6
897
32 – XML 쮿
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.
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.
DTDs – Document Type Definitions 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 XML-Dokuments prüfen (Syntaxprüfung). Es kann z.B. sichergestellt werden, dass jeder Kunde genau einen Namen und einen Vornamen besitzen muss. ... WEITERE ATTRIBUTE .... ]>
32.3 XML-Parser Für den Zugriff und die Verarbeitung von XML-Dokumenten existieren in nahezu allen Programmiersprachen die zwei Techniken SAX und DOM, die sich weitestgehend durchgesetzt haben. Ein XML-Dokument wird dazu 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 Auftreten 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 XMLDokuments in einem Arbeitsgang verarbeiten. Es können über SAX keine Änderungen am XML-Dokument vorgenommen werden.
898
XML-Parser
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 XML-Parser, die für bestimmte Anwendungszwecke besser geeignet sind. Sie finden diese Parser unter: 쮿
http://www.jdom.org/
쮿
http://www.dom4j.org/
32.3.1 SAX-Parser Ein SAX-Parser ist eine sehr schnelle und wenig speicherintensive Lösung, ein XML-Dokument in einem Vorgang zu analysieren und dessen Daten zu verarbeiten. Während der Parser das XML-Dokument analysiert, erzeugt er beim Auftreten 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 SAXParsers 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
Tabelle 32.2: Package-Übersicht zur Verwendung von SAX
Vorgehensweise Im Folgenden wird der Weg über die Klassen SAXParserFactory und SAXParser beschrieben. 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.SAXParser-Factory ausgelesen. Normalerweise ist diese Eigenschaft nicht belegt und kann über die folgende Kommandozeile gesetzt werden: java -Djavax.xml.par-sers.SAXParserFactory=KlassenName.
Java 6
899
32 – XML 쮿
Es wird die Datei ..\jre\lib\jaxp.properties ausgewertet, falls sie existiert.
쮿
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 (siehe Datei SAXParserFactory.java in [JDKInstall]\ 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.
Hinweis Wenn Sie die Beispiele dieses Kapitels 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.
Beispiel 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. Der Methode parse() werden die XML-Datei sowie der Handler für die Ereignisverarbeitung, in diesem Fall das eigene Objekt, übergeben.
900
XML-Parser
import java.io.*; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.*; public class SAXParser1 extends DefaultHandler { public SAXParser1() { 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(); } } Listing 32.1: \Beispiele\de\jse6buch\kap32\SAXParser1.java (Auszug)
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 öffnendes 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. Java 6
901
32 – XML
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 an die Methode übergeben werden müssen. Die ermittelten Zeichen werden im Array zeichen bereitgestellt. Über die 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 nur einmal aufgerufen und gibt unter anderem die Lage des XML-Dokuments, z.B. den Dateinamen, an. void setDocumentLocator(Locator locator)
Beispiel 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 gefundenen Zeichen, also auch die Zeilenumbrüche innerhalb des XML-Dokuments, verarbeitet. 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() { Listing 32.2: \Beispiele\de\jse6buch\kap32\SAXParser1.java (Auszug)
902
XML-Parser
System.out.println("===== Dokumentende ======"); } public void startDocument() { System.out.println("===== Dokumentbeginn ======"); } public void startElement(String url, String lokalerName, String qualName, Attributes attribute) { inTag = true; 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); } } } Listing 32.2: \Beispiele\de\jse6buch\kap32\SAXParser1.java (Auszug) (Forts.)
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
Java 6
903
32 – XML
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 vorhandenen DTD (intern oder extern) beispielsweise kein gültiges (aber wohlgeformtes) XMLDokument vorgefunden, wird keine weitere Meldung ausgegeben. Einfache Fehler werden standardmäßig nicht weiter berücksichtigt. 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.
Beispiel 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. 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) { Listing 32.3: \Beispiele\de\jse6buch\kap32\SAXParser2.java
904
XML-Parser
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(); } } Listing 32.3: \Beispiele\de\jse6buch\kap32\SAXParser2.java (Forts.)
Sie erhalten die Ausgabe: 9 The element type "Name" must be terminated by the matching end-tag "". file:/C:/JSE6Buch/Beispiele/KundenFehler.xml
Eigenschaften einstellen Die XML-Parser unterstützen unterschiedliche Eigenschaften (Properties) 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. Sie erhalten ihn über die folgenden 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, dass 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 Properties finden Sie unter 쮿
http://xerces.apache.org/xerces2-j/features.html
쮿
http://xerces.apache.org/xerces2-j/properties.html
Java 6
905
32 – XML
Beispiel 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. 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)); } catch(Exception e) {} } public static void main(String[] args) { new SAXParser3(); } } Listing 32.4: \Beispiele\de\jse6buch\kap32\SAXParser3.java
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
906
XML-Parser 쮿
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.
Hinweis Unter JAXP 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.
Beispiel 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. Kunde (Name, Vorname?)> Name (#PCDATA)> Vorname (#PCDATA)> Name ID CDATA #REQUIRED>
Listing 32.5: \Beispiele\de\jse6buch\kap32\KundenDTD.dtd
Die XML-Datei enthält eine Verknüpfung auf die extern vorliegende DTD und kann nun auf Gültigkeit geprüft werden. Meier Franz Listing 32.6: \Beispiele\de\jse6buch\kap32\KundenDTD.xml
Java 6
907
32 – XML
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. 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(); } } Listing 32.7: \Beispiele\de\jse6buch\kap32\SAXParserDTD.java
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 10- bis 100-fache 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 XMLDokument genutzt, um es zu verarbeiten. Im Gegensatz zu SAX können Sie nun beliebig lange das XML-Dokument lesen oder auch bearbeiten.
908
XML-Parser
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 SAXParsers 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 SAX-Parsers.
Tabelle 32.3: Package-Übersicht zur Verwendung von DOM
XML-Dokumente parsen Dieser Abschnitt beschreibt den Weg über die Klassen DocumentBuilderFactory und DocumentBuilder. 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 -Djavax.xml.parsers.DocumentBuilderFactory=KlassenName 쮿
Es wird die Datei ..\jre\lib\jaxp.properties ausgewertet, wenn sie existiert.
쮿
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 DOM-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.
Java 6
909
32 – XML
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
Tabelle 32.4: Auswahl wichtiger DOM-Interfaces
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: 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 bzw. der letzte untergeordnete Knoten geliefert
getNextSibling() getPreviousSibling()
Es wird der nächste bzw. der vorige Knoten geliefert, der dem aktuellen Knoten direkt folgt bzw. diesem vorausgeht
getNodeName()
Es wird der Name eines Knotens geliefert. Im Falle eines Tags ist dies der TagName, 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
Tabelle 32.5: Methoden des Interfaces Node
910
XML-Parser
Beispiel 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 Knotens ü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. 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(); Listing 32.8: \Beispiele\de\jse6buch\kap32\DOMParser1.java
Java 6
911
32 – XML
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; case Node.TEXT_NODE: System.out.printf("%s", nl.item(i).getNodeValue()); break; } } } public static void main(String[] args) { new DOMParser1(); } } Listing 32.8: \Beispiele\de\jse6buch\kap32\DOMParser1.java (Forts.)
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 NodeObjekt 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.
912
XML-Parser
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
Tabelle 32.6: Methoden von Node zum Hinzufügen und Löschen von Elementen
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.
Beispiel 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. Über die Methode appendChild() erfolgt das Einfügen in den Baum. 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. 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 Listing 32.9: \Beispiele\de\jse6buch\kap32\DOMParser2.java
Java 6
913
32 – XML
{ 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); } 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); fw.close(); } catch(Exception ex) { System.out.println(ex.getMessage()); } } Listing 32.9: \Beispiele\de\jse6buch\kap32\DOMParser2.java (Forts.)
914
XSLT-Transformationen
public static void main(String[] args) { new DOMParser2(); } } Listing 32.9: \Beispiele\de\jse6buch\kap32\DOMParser2.java (Forts.)
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://xmlgraphics.apache.org/fop/ bezogen werden. Wozu soll nun eine XML-Transformation nützlich sein? 쮿
Enthält ein XML-Dokument Informationen zu 10.000 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.
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 Datei jaxp.properties, die Systemeigenschaft javax.xml.transform.TransformerFactory 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 zur Verfügung steht, kann er aus XML-Dokumenten auf Basis des verwendeten StyleSheets ein Ergebnisdokument erzeugen.
Java 6
915
32 – XML
TransformerFactory
XML-Dokument
XSL-Dokument
Transformer
Ziel
Abbildung 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 DOMParsers nutzen. Bei der Ausgabe ist es genauso. 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
Tabelle 32.7: Benötigte Klassen zur XSLT-Transformation
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. Schraube 32.x 100 ... Listing 32.10: \Beispiele\de\jse6buch\kap32\Artikel.xml
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
916
XSLT-Transformationen
einen bestimmten Pfad besitzt (match="/"). In diesem Fall ist dies der Pfad zum Wurzelelement. Nach der Ermittlung des Wurzelelements wird dieses mit einem HTML-Rahmen versehen und über den Ausdruck 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\jse6buch\kap32\Artikel.xsl
Als Ergebnis wird die folgende (in der Formatierung angepasste) HTML-Ausgabe erzeugt. Schraube 32mm 100 ... Listing 32.12: Das Ergebnis der Transformation von Artikel.xml mit Artikel.xsl
32.4.1 Kommandozeilenversion Der im JDK integrierte XSLT-Prozessor Xalan verfügt eigentlich über eine Kommandozeilenversion, die man im JDK 5.0 und 6.0 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, da sie immer noch public ist.
Java 6
917
32 – XML
Beispiel 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 public class RunXalan { public static void main(String[] args) { com.sun.org.apache.xalan.internal.xslt.Process._main(args); } } Listing 32.13: \Beispiele\de\jse6buch\kap32\RunXalan.java
Auf ähnliche Weise kann von Xalan eine Anwendung aufgerufen werden, welche die aktuellen Umgebungseinstellungen der XML-Tools ausgibt. Hier wurde die Methode main() im JDK 5.0 nach _main() umbenannt, im JDK 6.0 ist sie wieder über main() verfügbar. Der Aufruf erfolgt über: java com.sun.org.apache.xalan.internal.xslt.EnvironmentCheck
Die Anwendung gibt Informationen zu JAXP, den verwendeten Versionen von SAX und DOM sowie den Versionen von Xerces sowie Xalan aus. Hier sehen Sie einen Ausschnitt der Ausgabe: #---- BEGIN writeEnvironmentReport($Revision: 1.4 $): version.DOM.draftlevel=2.0fd version.JAXP=1.1 or higher version.xerces2=Xerces-J 2.6.2 version.xalan2_2=Xalan Java 2.6.0 java.version=1.6.0 version.DOM=2.0 version.SAX=2.0 ...
918
XSLT-Transformationen
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 XSLStyleSheet 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 Vorteil, dass als Ziel der Transformation eine Datei (oder ein Stream) angegeben werden kann (sozusagen ein Speichern um vier Ecken).
Beispiel Die Anweisungen zum Einlesen des XML-Dokuments Kunden.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 TransformerObjekt 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. 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("Kunden.xml")); TransformerFactory tf = TransformerFactory.newInstance(); Transformer trf = tf.newTransformer(); trf.transform(new DOMSource(doc), new StreamResult(new File("Daten.xml"))); } catch(Exception ex) {} } Listing 32.14: \Beispiele\de\jse6buch\kap32\Transform1.java
Java 6
919
32 – XML
public static void main(String[] args) { new Transform1(); } } Listing 32.14: \Beispiele\de\jse6buch\kap32\Transform1.java (Forts.)
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.
Beispiel Als Eingabe erhält der Transformer diesmal eine Datei und keinen DOM-Baum. Außerdem wird beim Anlegen des Transformers 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. Beachten Sie wiederum, die verwendeten Dateien in das Ausführungsverzeichnis der Anwendung zu kopieren. 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) {} } Listing 32.15: \Beispiele\de\jse6buch\kap32\Transform2.java
920
StAX – Streaming von XML-Daten
public static void main(String[] args) { new Transform2(); } } Listing 32.15: \Beispiele\de\jse6buch\kap32\Transform2.java (Forts.)
Abbildung 32.2: HTML-Ausgabe der XSL-Transformation
32.5 StAX – Streaming von XML-Daten Mit zahlreichen Neuerungen im Bereich Web Services sind auch die APIs StAX (Streaming API for XML) und JAXB mit in die JSE 6 gewandert. Wenn man über StAX redet und vorher bereits den SAX-Parser besprochen hat, muss man zwangsläufig auch über die verschiedenen Verfahren sprechen, wie das ereignisbasierte Parsen erfolgen kann. Der SAX-Parser verwendet dabei das so genannte Push-Verfahren, d.h., er liefert der Anwendung seine Information in Form von Ereignissen. Wie sollte es anders sein, verwendet StAX das Pull-Verfahren, bei dem die Anwendung sich meldet, wenn sie vom Parser weitere Daten benötigt. StAX wird über das JSR 173 (http://jcp.org/en/jsr/ detail?id=173) beschrieben und liegt seit 2003 in der Version 1.0 vor. Es existieren verschiedene Implementierungen, wobei die von Sun nun im JDK integriert ist. Umfangreiche Informationen zu StAX lassen sich aus dem Web Service-Tutorial unter http:// java.sun.com/webservices/docs/2.0/tutorial/doc/ entnehmen. StAX bietet mit dem Pull-Modell also die Möglichkeit, XML-Daten ereignisbasiert zu verarbeiten, wobei aber die Anwendung steuert, wann sie eine Information haben will. Das Modell soll einfacher zu handhaben sein und auch schneller die Daten bereitstellen. Beides sind sicher auch subjektive Aussagen und sollten vor dem Einsatz von SAX geprüft werden. Die individuelle Steuerung des Parsens kann allerdings tatsächlich ein Plus gegenüber SAX sein, da agieren meist einfacher zu realisieren ist als reagieren. Für den Zugriff auf die XML-Daten bietet StAX zwei Vorgehensmodelle an. Das Cursor API bietet einen einfachen Zugriff auf die Daten, wobei nur eine Schnittstelle zum Einsatz kommt. Das Event Iterator API arbeitet intensiver mit Objekten, die sich dann auch besser weiterverarbeiten lassen. StAX arbeitet bidirektional, wobei hier gemeint ist, das es XMLDaten sowohl lesen als auch schreiben kann. Wie bei SAX können aber einmal gelesene oder geschriebene Daten nicht mehr im gleichen Durchlauf bearbeitet werden.
Java 6
921
32 – XML
Hinweis Da sich die API-Dokumentation von StAX in der Dokumentation des JDK befindet, wird im Folgenden auf die Aufzählung der zahlreichen Konstanten und Klassen für die einzelnen Elemente eines XML-Dokuments verzichtet.
32.5.1 XML-Daten lesen Zum Lesen von XML-Daten über StAX benötigen Sie die Klasse XMLInputFactory sowie einen Stream vom Typ InputStream. Danach müssen Sie sich entscheiden, ob Sie das Cursor oder Event Iterator API nutzen. Kommt es auf maximale Geschwindigkeit an, wäre z.B. das Cursor API zu empfehlen. Eine neue Instanz der Klasse XMLInputFactory erzeugen Sie über deren statische Methode newInstance(). Im folgenden Beispiel wird immer mit der XML-Datei Kunden.xml gearbeitet, die hier über einen FileInputStream geöffnet wird. Die weitere Vorgehensweise hängt vom verwendeten API ab. XMLInputFactory iF = XMLInputFactory.newInstance(); FileInputStream is = new FileInputStream("Kunden.xml");
Die Klassen zur Arbeit mit StAX befinden sich in drei Packages. Im Package javax.xml.stream befinden sich die Interfaces und Klassen zum Zugriff auf die XMLStreams. Im Package javax.xml.stream.event befinden sich Interfaces für die verschiedenen
Eventtypen (d.h. die Elemente eines XML-Dokuments). Einige Tool-Interfaces und -Klassen befinden sich zudem im Package javax.xml.stream.util. Durch StAX ausgelöste Exceptions sind durchgängig vom Typ XMLStreamException.
Event Iterator API Nach der Erstellung der XMLInputFactory muss darüber ein EventReader vom Typ XMLEventReader erzeugt werden. Dazu verwenden Sie die Methode createXMLEventReader(), welcher das XML-Dokument als InputStream übergeben werden muss. Der zurückgegebene XMLEventReader erweitert das Interface Iterator, sodass die Elemente des XMLDokuments über die Methoden hasNext() und nextEvent() (spezielle Implementierung, next() existiert aber auch) durchlaufen werden können. Die zurückgelieferten XML-Elemente sind vom Typ XMLEvent und können dann weiter ausgewertet werden. XMLEventReader er = iF.createXMLEventReader(is); while(er.hasNext()) { XMLEvent ev = er.nextEvent();
Weiterhin besitzt die Klasse XMLEventReader noch einige nützliche Methoden, um den Inhalt von Textelementen zu lesen, zum nächsten Element-Tag zu springen und ein Element zu lesen, ohne den Lesezeiger zu versetzen.
922
StAX – Streaming von XML-Daten
String getElementText() XMLEvent nextTag() XMLEvent peek()
Die Klasse XMLEvent besitzt nun wiederum zahlreiche Methoden um zu prüfen, von welchem Typ ein geparstes XML-Element ist. Danach wird das Element in den korrekten Typ gecastet und kann über eine eigens dafür bereitgestellte Klasse ausgewertet werden. Ein Start-Tag wie wird dann z.B. durch die Klasse StartElement beschrieben. Die Methode getName() liefert davon ein QName-Objekt (Qualified Name), über das der Präfix, der Namensraum und der Elementname ausgelesen werden können. if(ev.isStartElement()) { StartElement se = (StartElement)ev.asStartElement(); System.out.println(se.getName().getLocalPart()); }
Auch wenn der Zugriff anfangs komplizierter aussieht, gelangen Sie schneller an die benötigten Informationen und können diese einfacher weiterverarbeiten. Für die verschiedenen Elemente eines XML-Dokuments stehen unterschiedliche Klassen bereit, deren Verwendung sich aber kaum von der eben gezeigten unterscheidet.
Beispiel Die Datei Kunden.xml wird mittels des StAX APIs geladen und ausgegeben. Dazu werden alle Start- und Endelemente (Start- und Ende-Tags) sowie deren Inhalte ausgegeben. Dass am Ende ein schön formatiertes Dokument in der Ausgabe angezeigt wird liegt daran, dass alle Formatierungszeichen im Originaldokument 1:1 durch das Characters-Objekt durchgereicht werden. import java.io.*; import javax.xml.stream.*; import javax.xml.stream.events.*; public class StAXParser1 { public StAXParser1() { try { XMLInputFactory iF = XMLInputFactory.newInstance(); FileInputStream is = new FileInputStream("Kunden.xml"); XMLEventReader er = iF.createXMLEventReader(is); while(er.hasNext()) { Listing 32.16: \Beispiele\de\jse6buch\kap32\StAXParser1.java
Java 6
923
32 – XML
XMLEvent ev = er.nextEvent(); if(ev.isStartElement()) System.out.print(""); if(ev.isEndElement()) System.out.print(""); if(ev.isCharacters()) System.out.print(((Characters) ev.asCharacters()).getData()); } is.close(); } catch(XMLStreamException ex) { System.out.println(ex.getMessage()); } catch(FileNotFoundException fnfe) {} catch(IOException ioEx) {} } public static void main(String[] args) { new StAXParser1(); } } Listing 32.16: \Beispiele\de\jse6buch\kap32\StAXParser1.java (Forts.)
Cursor API Das Cursor API ist da wesentlich einfacher zu nutzen und auch die performanteste Lösung zum Parsen von XML. Über die Methode createXMLStreamReader() der XMLInputFactory wird ein XMLStreamReader-Objekt zurückgegeben. Diese Klasse implementiert zwar nicht das Interface Iterator, besitzt aber die gleichen Methoden, um den XML-Stream zu durchlaufen. Die Methode next() liefert hier den Typ des aktuellen Elements, dessen Eigenschaften direkt über das XMLStreamReader-Objekt ausgelesen werden. Den Typ des Elements können Sie durch Vergleiche mit den Konstanten des Interfaces XMLStreamConstants ermitteln.
Beispiel Jetzt wird die Datei Kunden.xml mit den Mitteln des Cursor APIs ausgelesen. Der Code ist deutlich kompakter, da keine weiteren Klassen benötigt werden, denn der XMLStreamReader befindet sich sozusagen immer auf dem aktuell gelesenen Element.
924
StAX – Streaming von XML-Daten
import java.io.*; import javax.xml.stream.*; import javax.xml.stream.events.*; public class StAXParser2 { public StAXParser2() { try { XMLInputFactory iF = XMLInputFactory.newInstance(); FileInputStream is = new FileInputStream("Kunden.xml"); XMLStreamReader sr = iF.createXMLStreamReader(is); while(sr.hasNext()) { switch(sr.next()) { case XMLStreamConstants.START_ELEMENT: System.out.print(""); break; case XMLStreamConstants.END_ELEMENT: System.out.print(""); break; case XMLStreamConstants.CHARACTERS: System.out.print(sr.getText()); break; } } is.close(); } catch(XMLStreamException ex) { System.out.println(ex.getMessage()); } catch(FileNotFoundException fnfe) {} catch(IOException ioEx) {} } Listing 32.17: \Beispiele\de\jse6buch\kap32\StAXParser2.java
Java 6
925
32 – XML
public static void main(String[] args) { new StAXParser2(); } } Listing 32.17: \Beispiele\de\jse6buch\kap32\StAXParser2.java (Forts.)
32.5.2 XML-Daten schreiben Die Vorgehensweise beim Schreiben (hier nur mit dem Event Iterator API vorgestellt) verwendet statt der Input- die Output-Variante der StAX-Typen. Zuerst wird deshalb eine XMLOutputFactory und der Ausgabestream für die Daten erzeugt. XMLOutputFactory oF = XMLOutputFactory.newInstance(); FileOutputStream os = new FileOutputStream("NeueDatei.xml");
Zum Schreiben der Daten werden zwei Objekte benötigt. Über ein XMLEventWriter-Objekt wird die Ausgabe in den übergebenen Stream geschrieben. Das XMLEventFactory-Objekt dient zum Erzeugen des benötigten XML-Elements, z.B. eines Tags oder eines Attributs. XMLEventWriter ew = oF.createXMLEventWriter(os); XMLEventFactory eF = XMLEventFactory.newInstance();
Zu Beginn eines XML-Dokuments ist die Angabe der XML-Version und optional des verwendeten Zeichensatzes erforderlich. Dazu wird die Methode createStartDocument() der EventFactory aufgerufen. Das erzeugte Objekt wird über die Methode add() dem XMLEventWriter übergeben und somit an die aktuelle Position im neuen XML-Dokument eingehangen. Der Anfang und das Ende eines Tags wird über die Methoden createStartElement() und createEndElement() erzeugt. Die beiden ersten Parameter entsprechen dem Präfix und Namensraum, der letzte bezeichnet den Elementnamen. ew.add(eF.createStartDocument("ISO-8859-1")); ew.add(eF.createStartElement("", "", "XMLAPIs"));
Um Text in einem Tag unterzubringen, wird die Methode createCharacters() verwendet. Die Reihenfolge der Methodenaufrufe bestimmt dann letztendlich den Aufbau des XML-Dokuments. Im Gegensatz zum DOM müssen Sie sich keine Gedanken machen, wo Sie ein Element einhängen (deshalb gibt's hier natürlich auch weniger Flexibilität). Es wird immer am Ende des Streams angefügt. Durch den Aufruf von createEndElement() wird beispielsweise ein Tag geschlossen, so dass alle folgenden Aufrufe der Methode add() die neuen Elemente im übergeordneten Element einfügen.
926
StAX – Streaming von XML-Daten
Beispiel Es soll eine neue XML-Datei erzeugt werden, welche einige der Abkürzungen der XML-APIs enthält. Als Zeichensatz wird ISO-8859-1 gewählt, damit man später z.B. deutsche Beschreibungstexte verwenden kann und die deutschen Umlaute korrekt interpretiert werden. Da die Elemente keinen Inhalt besitzen, führen die Aufrufe von createStartElement() und createEndElement() dazu, dass Tags der Form erzeugt werden. Nach der Erstellung des XML-Dokuments werden alle Streams und Writer wieder geschlossen. import java.io.*; import javax.xml.stream.*; import javax.xml.stream.events.*; public class StAXWriter { public StAXWriter() { try { XMLOutputFactory oF = XMLOutputFactory.newInstance(); FileOutputStream os = new FileOutputStream("XMLAPIs.xml"); XMLEventWriter ew = oF.createXMLEventWriter(os); XMLEventFactory eF = XMLEventFactory.newInstance(); ew.add(eF.createStartDocument("ISO-8859-1")); ew.add(eF.createStartElement("", "", "XMLAPIs")); ew.add(eF.createStartElement("", "", "JAXP")); ew.add(eF.createEndElement("", "", "JAXP")); ew.add(eF.createStartElement("", "", "StAX")); ew.add(eF.createEndElement("", "", "StAX")); ew.add(eF.createEndElement("", "", "XMLAPIs")); ew.flush(); ew.close(); os.close(); } catch(XMLStreamException ex) { System.out.println(ex.getMessage()); } Listing 32.18: \Beispiele\de\jse6buch\kap32\StAXWriter.java
Java 6
927
32 – XML
catch(FileNotFoundException fnfe) {} catch(IOException ioEx) {} } public static void main(String[] args) { new StAXWriter(); } } Listing 32.18: \Beispiele\de\jse6buch\kap32\StAXWriter.java (Forts.)
Wenn Sie beim Parsen eines XML-Dokuments bestimmte Elemente herausfiltern wollen, können Sie das natürlich beim Durchlaufen der Elemente über hasNext() und next() bzw. nextEvent() durchführen. Ein anderer Weg, die Filterung von der Verarbeitungslogik zu trennen, besteht in der Erstellung eines EventFilters, der beim Reader des Streams registriert werden muss. Ein solcher Filter wird durch eine Klasse erstellt, welche das Interface EventFilter über die Methode accept() implementiert. Die Methode liefert true, wenn das Element verarbeitet werden soll, sonst false. class KundenFilter implements EventFilter { public boolean accept(XMLEvent event) ... }
Die Erstellung des XMLEventReader erfolgt nun zweistufig. Zuerst wird ein ganz normaler XMLEventReader erzeugt. Dieser wird dann bei der Erstellung eines neuen XMLEventReaders als erster Parameter übergeben, während ein Objekt vom Typ der Filterklasse als zweiter Parameter an die Methode createFilteredReader() zu übergeben ist. Danach erfolgt die Filterung automatisch. XMLEventReader er = iF.createXMLEventReader(is); XMLEventReader er2 = iF.createFilteredReader(er, new KundenFilter());
Beispiel Aus der Kundenliste werden alle XML-Tags herausgefiltert, sodass nur noch die Inhalte der Tags ausgegeben werden. Zur Kontrolle wird beim Durchlaufen des XMLStreams versucht, die Start- und Endetags zu verarbeiten.
928
StAX – Streaming von XML-Daten
import java.io.*; import javax.xml.stream.*; import javax.xml.stream.events.*; public class StAXFilter { public StAXFilter() { try { XMLInputFactory iF = XMLInputFactory.newInstance(); FileInputStream is = new FileInputStream("Kunden.xml"); XMLEventReader er = iF.createXMLEventReader(is); XMLEventReader er2 = iF.createFilteredReader(er, new KundenFilter()); while(er2.hasNext()) { XMLEvent ev = er2.nextEvent(); if(ev.isStartElement()) System.out.print(""); if(ev.isEndElement()) System.out.print(""); if(ev.isCharacters()) System.out.print(((Characters) ev.asCharacters()).getData()); } is.close(); } catch(XMLStreamException ex) { System.out.println(ex.getMessage()); } catch(FileNotFoundException fnfe) {} catch(IOException ioEx) {} } public static void main(String[] args) { new StAXFilter(); Listing 32.19: \Beispiele\de\jse6buch\kap32\StAXFilter.java
Java 6
929
32 – XML
} } class KundenFilter implements EventFilter { public boolean accept(XMLEvent event) { return !event.isStartElement() && !event.isEndElement(); } } Listing 32.19: \Beispiele\de\jse6buch\kap32\StAXFilter.java (Forts.)
32.6 JAXB – XML Bindungen Das letzte XML-API, das in diesem Kapitel vorgestellt werden soll, ist JAXB (Java Architecture for XML Binding), das es wie StAX bereits seit 2003 gibt. Nach einer Version 1.0 ist im JDK 6.0 eine stark überarbeitete Version 2.0 enthalten, die unter dem JSR 222 beschrieben wird (http://jcp.org/en/jsr/detail?id=222). Die Version 2.0 benötigt mindestens das JDK 5.0, da reger Gebrauch von Annotations gemacht wird. In der Version 2.0 wird jetzt auch Java-nach-XML (bzw. Schema) unterstützt und Annotations werden zum Mappen von Java- nach XML Schema-Elementen und umgekehrt verwendet. Umfangreichere Informationen zu JAXB können Sie z.B. dem Java EE 5 Tutorial unter http://java.sun.com/javaee/5/docs/tutorial/doc/ entnehmen. JAXB kann zahlreiche Aufgabenstellungen lösen, die sich beim Austausch von Daten mittels XML ergeben. Angenommen Sie haben die Aufgabe, eine XML-Datei, welche Konfigurationseinstellungen oder sonstige Daten enthält, in Java zu verarbeiten. Oder Sie verwalten in einer Java-Klasse Daten, die Sie als XML-Datei weitergeben möchten. JAXB unterstützt Sie in beiden Fällen. Die benötigten Packages befinden sich hauptsächlich in javax.xml.bind und javax.xml. bind.annotation. Drei weitere Unterpackages von javax.xml.bind enthalten weitere Hilfstypen. Weiterhin stehen zwei Tools zur Verfügung, um aus Schema-Dateien Java-Code und umgekehrt zu erzeugen.
32.6.1 Schema-nach-Java Eine typische Vorgehensweise zum Austausch von XML-Daten zwischen zwei Parteien besteht darin, dass eine XML-Schema-Datei erstellt wird, die das Format der zukünftigen XML-Dokumente beschreibt. Ein Problem besteht nun darin, eine (oder mehrere) Klassen zu entwickeln, welche die XML-Daten anhand dieses Schemas einlesen und verarbeiten. Für die Beispieldatei Kunden.xml mit dem folgenden verkürzten Aufbau
930
JAXB – XML Bindungen
Meier Franz ... Listing 32.20: \Beispiele\de\jse6buch\kap32\Kunden.xml
kann beispielsweise folgendes Schema (ohne weitere Erläuterung) verwendet werden: Listing 32.21: \Beispiele\de\jse6buch\kap32\Kunden.xsd
Java 6
931
32 – XML
Der erste Schritt, um auf XML-Dokumente zuzugreifen, welche dem Schema Kunden.xsd genügen, ist die Verwendung des Tools xjc, dem Schema Binding Compiler. Im einfachsten Fall wird ihm der Name der Schemadatei übergeben. Optional können Sie auch einen Packagenamen angeben. Dadurch wird in den generierten Java-Dateien dieser Packagename verwendet und außerdem die entsprechende Verzeichnisstruktur, ausgehend vom Aufruf des Compilers, angelegt. Wenn Sie sich also beim folgenden Aufruf im ..\de\ jse6buch\kap32 übergeordneten Verzeichnis befinden, z.B. ..\Beispiele, wird ein neues Unterverzeichnis ..\de\jse6buch\kap32\jaxb erstellt. Dazu muss sich die Datei Kunden.xsd ebenfalls im Verzeichnis ..\Beispiele befinden. xjc -p de.jse6buch.kap32.jaxb Kunden.xsd
Es werden im Verzeichnis ..\kap32\jaxb zwei Dateien erzeugt, ObjectFactory.java und Kunden.java. Über die erste Datei können neue Kunden-, Kunde- oder Name-Objekte erzeugt werden. Die zweite Datei enthält eine Klasse Kunden mit einer weiteren inneren Klasse Kunde und darunter einer inneren Klasse Name usw. Über diese Klassen sowie deren Variablen und Methoden können Sie auf die Elemente eines eingelesenen XML-Dokuments, das dem Schema Kunden.xsd entspricht, zugreifen, ohne eine Zeile Code zu schreiben. package de.jse6buch.kap32.jaxb; import java.util.*; import javax.xml.bind.annotation.*; @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = {"kunde"}) @XmlRootElement(name = "Kunden") public class Kunden { @XmlElement(name = "Kunde", required = true) protected List kunde; public List getKunde() { if(kunde == null) kunde = new ArrayList(); return this.kunde; @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "", propOrder = {"name", "vorname"}) public static class Kunde { @XmlElement(name = "Name", required = true) protected Name name; Listing 32.22: \Beispiele\de\jse6buch\kap32\jaxb\Kunden.java
932
JAXB – XML Bindungen
public Name getName() { return name; } public void setName(Name value) { this.name = value; } ... } } Listing 32.22: \Beispiele\de\jse6buch\kap32\jaxb\Kunden.java (Forts.)
Jetzt müssen Sie nur noch die generierte Klasse (es können auch mehrere sein, abhängig von der Struktur der Schemadatei) nutzen. Der Zugriff auf die JAXB Runtime erfolgt über die Klasse JAXBContext. Dieser Klasse kann entweder der Typ übergeben werden, der die zu lesende XML-Datei beschreibt oder ein Packagename, in dem sich eine Klasse ObjectFactory befindet. Diese Klasse muss deshalb vorher explizit übersetzt werden, da zu ihr keine Abhängigkeiten bestehen. Spätestens beim Ausführen der Anwendung werden Sie darauf hingewiesen. JAXBContext j = JAXBContext.newInstance("de.jse6buch.kap32.jaxb"); JAXBContext j = JAXBContext.newInstance(Kunden.class);
Danach muss unterschieden werden, ob Sie ein XML-Dokument lesen oder schreiben wollen. Zum Lesen kommt ein Unmarshaller-Objekt zum Einsatz, zum Schreiben ein Marshaller-Objekt. Beide Objekte werden über eine Instanz der Klasse JAXBContext erstellt. Unmarshaller um = jc.createUnmarshaller(); Marshaller m = jc.createMarshaller();
Über die Methode unmarshal() wird ein InputStream eingelesen, der hier durch die bekannte Datei Kunden.xml bereitgestellt wird. Danach muss über einen Cast der Rückgabetyp Object in ein Kunden-Objekt gecastet werden. Jetzt können Sie die über den Schema Binding Compiler generierte Klasse Kunden verwenden. Über die Methode getKunde() erhalten Sie eine Collection von Kunde-Objekten. Da die Klasse eine innere Klasse von Kunden ist, muss auf sie mit Kunden.Kunde zugegriffen werden. Object o = um.unmarshal(new FileInputStream("Kunden.xml")); Kunden k = Kunden.class.cast(o); List kundenListe = k.getKunde(); for(Kunden.Kunde kdn: kundenListe) System.out.println("Name: " + kdn.getName().getValue());
Java 6
933
32 – XML
Beispiel Das Beispiel zeigt nun das Einlesen einer XML-Datei und fügt noch einmal alles zusammen. Vorher müssen Sie mit dem Schema Binding Compiler und dem Schema Kunden.xsd die Datei Kunden.java im Verzeichnis ..\kap32\jaxb generieren. Die Anwendung wird dann wie üblich übersetzt und ausgeführt. import java.util.*; import java.io.*; import javax.xml.bind.*; import de.jse6buch.kap32.jaxb.*; public class SchemaToJava { public SchemaToJava() { try { JAXBContext jc = JAXBContext.newInstance(Kunden.class); Unmarshaller um = jc.createUnmarshaller(); Object o = um.unmarshal(new FileInputStream("Kunden.xml")); Kunden k = Kunden.class.cast(o); List kundenListe = k.getKunde(); for(Kunden.Kunde kdn: kundenListe) System.out.println("Name: " + kdn.getName().getValue()); } catch(Exception ex) { System.out.println(ex.getMessage()); } } public static void main(String[] args) { new SchemaToJava(); } } Listing 32.23: \Beispiele\de\jse6buch\kap32\SchemaToJava.java
32.6.2 Java-nach-Schema Der andere Weg, einen Automatismus zwischen Java-Klassen und XML-Daten herzustellen, beginnt bei der Erstellung einer entsprechend aufgebauten Java-Klasse, oder besser gesagt einer speziell annotierten Klasse. Im Gegensatz zur anderen Richtung benötigen Sie
934
JAXB – XML Bindungen
hier kein Schema mehr. Die JAXB Runtime bzw. das so genannte Binding Framework versucht anhand der Informationen in der Klasse die Daten aus dem XML-Dokument zu lesen. Hierzu benötigen Sie das Package javax.xml.bind.annotation. Darin sind momentan 29 Annotations enthalten, die zum Kennzeichnen der Elemente der Java-Klasse verwendet werden können. Da die Bedeutungen der Annotations vom Kontext abhängen, kann das Einsatzgebiet sehr weitschweifig sein. Annotation
Beschreibung
XmlAttribute
Kennzeichnet ein künftiges Attribut
XmlElement
Kennzeichnet ein künftiges XML-Tag
XmlRootElement
Kennzeichnet z.B. das Wurzelelement in einem XML-Dokument
XmlType
Hiermit können Sie beispielsweise die Reihenfolge der Unterelemente festlegen
Tabelle 32.8: Auswahl von Annotations in JAXB
Die folgende Datei Kunde.java soll zum Zugriff auf XML-Dokumente genutzt werden, die aus einem -Element und zwei untergeordneten Elementen und bestehen. Außerdem besitzt das -Tag noch ein Attribut ID. Die Klasse Kunde wird nun mit der Annotation XmlRootElement versehen, um das Wurzelelement des XMLDokuments zu kennzeichnen. Über die Kennzeichnung der öffentlichen Variablen mittels XmlElement werden die Unterelemente und definiert. Der übergebene Parameter name in den Annotations legt den zukünftigen Namen des XML-Elements fest. Lassen Sie diese Angabe weg, wird standardmäßig der Name der Variablen, z.B. name verwendet. Sie können auch die Annotation XmlElement ganz weglassen. In diesem Fall werden alle öffentlichen Variablen automatisch als XmlElement interpretiert. Zuletzt wird noch die Variable id als Attribut von festgelegt. Statt der hier verwendeten kurzen Schreibweise über öffentliche Variablen, kann auch die JavaBeans-Schreibweise über set- und get-Methoden verwendet werden. import javax.xml.bind.annotation.*; @XmlRootElement(name="Kunde") public class Kunde { @XmlElement(name="Name") public String name; @XmlElement(name="Vorname") public String vorname; @XmlAttribute(name="ID") public int id; } Listing 32.24: \Beispiele\de\jse6buch\kap32\Kunde.java
Jetzt muss zuerst wieder ein JAXB-Kontext erstellt werden. JAXBContext jc = JAXBContext.newInstance(Kunde.class);
Java 6
935
32 – XML
Als Nächstes ist der Inhalt des XML-Dokuments zu erstellen. Dazu wird direkt mit den zuvor annotierten Klassen und deren Objekten gearbeitet. Im Folgenden wird ein Kunde erstellt und seine Eigenschaften werden gesetzt. Kunde kdn = new Kunde(); kdn.name = "Meier"; kdn.vorname = "Martina"; kdn.id = 1001;
Jetzt muss die Übersetzung der annotierten Java-Klasse in die Beschreibung des XMLDokuments erfolgen. Da diesmal die Daten von Java nach XML geschrieben werden sollen, wird ein Marshaller über die Methode createMarshaller() erzeugt. Um eine formatierte Ausgabe zu erhalten, kann noch die Eigenschaft Marshaller.JAXB_FORMATTED_OUTPUT auf true gesetzt werden. Danach wird über die Methode marshal() ein Java-Objekt als XML-Stream in den als zweiten Parameter übergebenen Ausgabestream geschrieben. fos = new FileOutputStream("NeuKunde.xml"); Marshaller m = jc.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); m.marshal(kdn, fos);
Um aus der Java-Klasse optional eine Schemadatei zu erzeugen, verwenden Sie den Schema-Generator schemagen. Ihm wird die Java-Datei mit den Annotations übergeben, welche die Mappings der Java-Elemente auf ein XML-Schema definieren. Dieses Schema kann ein anderer Entwickler wiederum nutzen, um seinerseits den Zugriff auf die XMLDaten herzustellen, möglicherweise auch mit einer anderen Programmiersprache. schemagen Kunde.java
Beispiel Das Beispiel fügt wieder alles zusammen und erstellt aus den Beschreibungen in der Datei Kunde.java, dem Erstellen passender Objekte sowie dem abschließenden Marshalling eine neue XML-Datei. import java.io.*; import javax.xml.bind.*; public class JavaToSchema { private FileOutputStream fos; public JavaToSchema() { try { Listing 32.25: \Beispiele\de\jse6buch\kap32\JavaToSchema.java
936
JAXB – XML Bindungen
fos = new FileOutputStream("NeuKunde.xml"); JAXBContext jc = JAXBContext.newInstance(Kunde.class); Kunde kdn = new Kunde(); kdn.name = "Meier"; kdn.vorname = "Martina"; kdn.id = 1001; Marshaller m = jc.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); m.marshal(kdn, fos); fos.close(); } catch(Exception ex) { System.out.println(ex.getMessage()); } } public static void main(String[] args) { new JavaToSchema(); } } Listing 32.25: \Beispiele\de\jse6buch\kap32\JavaToSchema.java (Forts.)
Die erstellte Datei NeuKunde.xml hat den Inhalt: Meier Martina
Hier wäre nun noch lange nicht Schluss, da JAXB ein etwas umfangreicheres API ist. So können Sie auch verschachtelte Elemente für ein XML-Dokument oder eigene Mappings definieren. Die Mappings steuern dabei, welcher Schema- bzw. Java-Datentyp auf den jeweils anderen abgebildet wird. Bisher wurden dazu die voreingestellten Mappings wie z.B. xsd:string nach String verwendet.
Java 6
937
JDBC – Datenbankzugriff 33.1 Einführung Aufgrund der immer größer werdenden Informationsmengen ist die Verwendung von Datenbanken zum Speichern dieser umfangreichen Daten nicht mehr wegzudenken. Java bietet über das JDBC API eine Schnittstelle zur Anbindung einer Anwendung oder eines Applets an eine Datenbank an. JDBC wird von Sun nicht mehr 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 6.0 wird die Spezifikation JDBC 4.0 unterstützt. Diese vereint die beiden 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 genutzt werden, um Aktionen auf der Datenbank durchzuführen. 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, entweder über ihre WebSeite oder beim Kauf eines ihrer Datenbankprodukte zur Verfügung. Unter der URL http://developers.sun.com/product/jdbc/drivers können Sie sich über die Verfügbarkeit der JDBC-Treiber für ein bestimmtes Datenbanksystem informieren. Jeder Hersteller liefert zu seinem Treiber meist auch eine Liste der unterstützten Features des JDBC API und des SQL-Standards 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. Daraus ergibt sich auch gleich die Folgerung, dass ein JDBC-Treiber nicht immer das aktuellste JDBC API unterstützen muss (und dies meist auch nicht tut).
Java 6
939
33 – JDBC – Datenbankzugriff
Abbildung 33.1: JDBC-Treiberdatenbank 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.
2.x
Im JDK 1.2 wurden das Core-API des Packages 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.
4.x
Die mit dem JDK 6.0 eingeführte Version 4.0 verfügt beispielsweise über Verbesserungen beim Laden von Treibern und unterstützt Annotations zum Mapping von relationalen Daten auf Objekte und SQL/XML
Tabelle 33.1: JDBC-Versionsübersicht
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 der 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.
940
Einführung
In der folgenden Tabelle werden die vier Treibertypen vorgestellt. Die Namen der Treiber werden in der Literatur in einigen Fällen etwas anders benannt, die Typklasse ist jedoch immer gleich. Treibertyp
Erläuterung
JDBC-ODBC-Brücke Klasse 1-Treiber
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 ODBCTreiber 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 aufwändig, 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 (Datenbanksystem) 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.
Natives API Klasse 2-Treiber
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.
Reiner Java-Treiber Klasse 3-Treiber
Auf der Client-Seite wird nur der betreffende JDBC-Treiber für das DBS benötigt, der komplett 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.
Reiner Java-Treiber und natives DBS-Protokoll Klasse 4-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.
Tabelle 33.2: JDBC-Treibertypen
Hinweis 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).
Java 6
941
33 – JDBC – Datenbankzugriff
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
Abbildung 33.2: Die verschiedenen Klasse X-Treiber
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 nur 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 Sicherheit geboten und die Verarbeitungsgeschwindigkeit ist in der Regel höher.
942
Einführung
Zweischichtig
Mehrschichtig
Java - Anwendung Client JDBC
Netzwerk API
Netzwerk
Applicationserver Middle-Tier JDBC
Datenbanksystem
Server
Abbildung 33.3: Zwei- und mehrschichtige Architekturen
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. 쮿
Zuerst werden ein oder mehrere JDBC-Treiber über den Treibermanager geladen, die danach von der Klasse DriverManager verwaltet werden.
쮿
Über den DriverManager wird eine Verbindung zu einer Datenbank aufgebaut und in einem Connection-Objekt zurückgegeben.
쮿
Über eine Verbindung können verschiedene Anweisungen an die Datenbank gesendet werden.
쮿
Zwei dieser Anweisungstypen liefern eine Ergebnismenge zurück, die über ein ResultSet-Objekt ausgewertet werden kann.
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.
Hinweis Die folgenden Ausführungen beziehen sich vorrangig auf die Möglichkeiten des JDBC APIs 3.0, da bisher keine JDBC 4.0-fähigen Datenbanktreiber verfügbar sind.
Java 6
943
33 – JDBC – Datenbankzugriff
JDBC-Treiber 1 DriverManager
JDBC-Treiber 2
Connection
CallableStatement
Statement
PreparedStatement
ResultSet
Abbildung 33.4: Klassen und Interfaces des JDBC API
33.2 Einrichten einer Datenbank Für die Verwendung von JDBC ist natürlich eine Datenbank erforderlich. Dazu werden im Folgenden zwei Datenbanksysteme 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.
Hinweis In den folgenden Beispielen werden die DBS MySQL und Firebird genutzt. Beachten Sie, dass nicht alle Funktionen in beiden DBS zur Verfügung stehen bzw. die Verwendung nicht immer identisch ist. Die Beispiele auf der CD enthalten, soweit es die Übersicht nicht zu sehr beeinträchtigt, die Anweisungen für beide DBS. Im Buch wird meist nur ein DBS im SourceCode verwendet.
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. In der aktuellen Aktion werden inzwischen auch Stored Procedures, Trigger und Transaktionen unterstützt.
944
Einrichten einer Datenbank
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 (Windows (x86)), im Falle des JDBC-Treibers die ZIP-Version: http://www.mysql.org/downloads/mysql/5.0.html
aktuelle Version 5.0.24
http://www.mysql.org/downloads/connector/j/5.0.html
aktuelle Version 5.0.3
Installation von MySQL 쮿
Entpacken Sie die ZIP-Datei mysql-5.0.24-win32.zip und führen Sie das Setup aus. Geben Sie gegebenenfalls ein anderes Installationsverzeichnis an. Dazu muss die Auswahl CUSTOM im Installationswizard angeklickt werden. Alle anderen Einstellungen können auf dem angebotenen Wert belassen werden. Für die folgenden Erläuterungen wird als Installationsverzeichnis C:\mysql angenommen.
쮿
Werden Sie aufgefordert sich bei MySQL.com anzumelden, wählen Sie den Punkt SKIP SIGN-UP. Nach Abschluss der Installation startet standardmäßig die Administration des Servers. Wählen Sie dort den Eintrag STANDARD CONFIGURATION aus. Markieren Sie im folgenden Dialog die Option, das bin-Verzeichnis in die PATH-Variable aufzunehmen. Vergeben Sie danach ein Passwort für den Root-Benutzer (im Folgenden wird als Passwort mysql verwendet). Im folgenden Dialog starten Sie die Übernahme der Einstellungen über die Schaltfläche EXECUTE.
Installation des MySQL-JDBC-Treibers 쮿
Entpacken Sie die ZIP-Datei mysql-connector-java-5.0.3.zip in ein beliebiges Verzeichnis. Es entsteht das Unterverzeichnis mysql-connector-java-5.0.3.
쮿
Kopieren Sie das JAR-Archiv mysql-connector-java-5.0.3-bin.jar in die Verzeichnisse C:\Programme\Java\jre1.6.0\lib\ext und [InstallJDK]\jre\lib\ext.
Verwendung von MySQL Öffnen Sie eine Konsole (Eingabeaufforderung, DOS-Prompt) und geben Sie das Kommando mysql -u root -p ein. Nach der Abfrage und Eingabe des Passworts (welches Sie während der Installation festgelegt haben) wird der mysql-Prompt angezeigt, über den Sie Kommandos an die Datenbank schicken können. 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;
Java 6
945
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. 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;
Der Dialog mit MySQL sollte so aussehen: mysql> CREATE DATABASE Kunden; Query OK, 1 row affected (0.02 sec) mysql> USE kunden; Database changed mysql> CREATE TABLE Kunde (ID INTEGER, Name VARCHAR(30)); Query OK, 0 rows affected (0.05 sec) mysql> INSERT INTO Kunde VALUES(1, "Meier"); Query OK, 1 row affected (0.01 sec) mysql> INSERT INTO Kunde VALUES(2, "Schulze"); Query OK, 1 row affected (0.00 sec) mysql> SELECT * FROM Kunde; +------+---------+ | ID | Name | +------+---------+ | 1 | Meier | | 2 | Schulze | +------+---------+ 2 rows in set (0.00 sec)
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.
946
Einrichten einer Datenbank
Download Wie auch bei MySQL benötigen Sie die Firebird-Datenbank und den separaten JDBCTreiber. 쮿
Öffnen Sie die URL http://www.firebirdsql.org/ und klicken Sie auf den Menüpunkt DOWNLOAD - FIREBIRD RELATIONAL DATABASE. Unter der Überschrift WIN32 SUPERSERVER AND CLASSIC laden Sie die Datei Firebird-1.5.3.4870-0-Win32.exe.
쮿
Wählen Sie nun den Menüpunkt DOWNLOAD - FIREBIRD CLASS 4 JCA-JDBC-DRIVER. Wählen Sie den Link Jaybird 2.1.0 RC1 for JDK 1.5. Es öffnet sich wieder eine neue Seite, auf der Sie den Download-Server auswählen können.
Installation von Firebird 쮿
Starten Sie das Setup über die Datei Firebird-1.5.3.4870-0-Win32.exe. Verwenden Sie jeweils die Standardeinstellungen. Nach der Installation wird sofort der Serverdienst gestartet und der Datenbankserver kann angesprochen werden.
쮿
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 (hier wird nicht automatisch ein neues Unterverzeichnis erstellt!).
쮿
Kopieren Sie das JAR-Archiv jaybird-full-2.0.1.jar in die Verzeichnisse C:\Programme\ Java\jre1.6.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"
Java 6
947
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;
Hinweis Wenn Sie über eine Java-Anwendung Änderungen in einer Firebird-Datenbank 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 jetzt Möglichkeiten, eine Verbindung zu einer Datenbank herzustellen. Die davon 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.
Hinweis Die folgenden Erläuterungen und Beispiele verwenden zur Herstellung der Verbindung zu einer Datenbank immer den Treibermanager.
948
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 dieser 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
Hinweis 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.
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)
Java 6
949
33 – JDBC – Datenbankzugriff
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)
Beispiel 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. 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"); } 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()); Listing 33.1: \Beispiele\de\jse6buch\kap33\TreiberManager.java
950
Herstellen der Datenbankverbindung
System.out.println("Version: " + d.getMajorVersion()); } } public static void main(String args[]) { new TreiberManager(); } } Listing 33.1: \Beispiele\de\jse6buch\kap33\TreiberManager.java (Forts.)
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 Pfadangabe verwendet werden. Beim zweiten Zugriff auf die MySQL-Datenbank werden zusätzlich ein Benutzername und ein Passwort angegeben. Diese Parameter werden vom Verbindungsstring auf die Datenbank durch ein Fragezeichen getrennt, wobei mehrere Parameter durch ein &-Zeichen miteinander verbunden 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&password=mysql jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb
Java 6
951
33 – JDBC – Datenbankzugriff
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.
Beispiel Zur Ausführung des Beispiels benötigen Sie entweder eine MySQL- oder eine Firebird-Datenbank mit dem Namen Kunden (oder beide). Passen Sie gegebenenfalls die Pfadnamen an und kommentieren Sie die Anweisungen für den Zugriff auf die nicht vorhandene Datenbank aus. Es wird jeweils ein Datenbanktreiber für MySQL und Firebird geladen und eine Verbindung zu einer Datenbank hergestellt. Im Falle von MySQL werden als Zugriffsstring ein Hostname und der Datenbankname verwendet. Firebird benötigt eine vollständige Pfadangabe. Außerdem müssen Sie auch hier einen Benutzernamen und das bei der Installation angegebene Passwort an die Pfadangabe anhängen (MySQL) bzw. die zusätzlichen Parameter der Methode getConnection() dazu verwenden. Geht alles gut, erhalten Sie keine weiteren Ausgaben. 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); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root&password=mysql"); Listing 33.2: \Beispiele\de\jse6buch\kap33\Verbindung.java
952
SQL-Anweisungen einsetzen
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(); } } Listing 33.2: \Beispiele\de\jse6buch\kap33\Verbindung.java (Forts.)
Hinweis 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 Treibermanagers 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 SQLAnweisungen 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 SQL-Anweisungen ausgeführt werden können.
Java 6
953
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 dann nur einmalig vorbereitet und danach schneller ausgeführt. Das Vorbereiten kostet aber etwas mehr Aufwand als bei der Verwendung des Interface 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 müssen Stored Procedures auch von den betreffenden JDBC-Treibern des DBMS unterstützt werden.
Tabelle 33.3: Interfaces zur Ausführung von SQL-Anweisungen
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 StatementObjekt besitzen, können Sie mit ihm eine SQL-Anweisung ausführen. Connection con = ... Statement stmt = con.createStatement();
Allgemeine Methoden des Interface 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()
Für die Ermittlung des Connection-Objekts einer Anweisung rufen Sie die Methode getConnection() auf. Connection getConnection()
954
SQL-Anweisungen einsetzen
Mit den folgenden Methoden können Sie die Wartezeit zur Ausführung einer SQLAnweisung auslesen bzw. setzen. Die Angaben erfolgen 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 Typ des Rückgabewertes 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)
Hinweis 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. mit den Tools isql oder mysql. Über Java haben Sie aber die Möglichkeit, solche Anwendungen zu starten und über ein Skript die Datenbank zu erstellen.
Beispiel 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. Über die Methode executeUpdate() wird die Tabelle Artikel erstellt und es werden zwei Datensätze eingefügt. Existiert die Tabelle schon, wird eine Exception ausgelöst.
Java 6
955
33 – JDBC – Datenbankzugriff
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(); } } Listing 33.3: \Beispiele\de\jse6buch\kap33\Anweisungen.java
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 SQLCode 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.
956
SQL-Anweisungen einsetzen
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 folgenden 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 legt den zu setzenden Wert fest. Es existieren noch weitere Methoden zum Setzen von Datums- oder Blob-Werten. void void void void
Java 6
setBoolean(int index, boolean wert) setDouble(int index, double wert) setInt(int index, int wert) setString(int index, String wert)
957
33 – JDBC – Datenbankzugriff
Beispiel In die Tabelle Artikel sollen mehrere Datensätze eingefügt werden (das Beispiel fügt aber nur einen Testdatensatz 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. 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?user=root&password=mysql"); 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(); } } Listing 33.4: \Beispiele\de\jse6buch\kap33\Parameter.java
958
SQL-Anweisungen einsetzen
33.4.4 Stored Procedures verwenden Speziell für den 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 zu löschenden Datensätze. Als einziger Parameter für diese Operationen genügt die Angabe einer eindeutigen Kundennummer durch den Client. Die SQLAnweisungen, 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.
Hinweis Stored Procedures werden nicht von allen Datenbanksystemen unterstützt. Weiterhin implementiert jedes Datenbanksystem die Verwendung von Stored Procedures auf seine Weise. Dies umfasst die Definition über SQL sowie den Aufruf der Prozeduren.
Prozeduren anlegen Im ersten Schritt müssen die Prozeduren 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 Vorgehensweise 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;.
Java 6
959
33 – JDBC – Datenbankzugriff
Hinweis Wenn Sie in isql eine Zeile mit einem Semikolon abschließen, bedeutet das für das Tool, dass Sie das Kommando vollständig eingegeben haben. Im Falle der Definition einer Stored Procedure müssen aber auch die Anweisungen in der Prozedur mit einem Semikolon abgeschlossen werden. Um ein neues, endgültiges Abschlusszeichen zu definieren, geben Sie deshalb das Kommando SET TERM #;
ein. Dadurch ist die Raute das neue Abschlusszeichen, zumindest so lange, bis die Prozedur eingegeben wurde. Im Falle von MySQL setzen Sie das Abschlusszeichen über delimiter #
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 in Firebird bei der Verwendung Doppelpunkte vorangesetzt. Außerdem muss das Abschlusszeichen eines Kommandos während der Eingabe der Stored Procedure auf ein anderes Zeichen gesetzt werden. MySQL: delimiter # CREATE PROCEDURE NeuerArtikel(ID INTEGER, Name VARCHAR(30)) BEGIN INSERT INTO Artikel VALUES(ID, Name); END;# delimiter ; Firebird: SET TERM #; CREATE PROCEDURE NeuerArtikel(ID INTEGER, Name VARCHAR(30)) AS BEGIN INSERT INTO Artikel VALUES(:ID, :Name); END;# SET TERM ;#
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.
960
SQL-Anweisungen einsetzen
MySQL: delimiter # CREATE PROCEDURE ZeigeArtikel(ANr INTEGER, OUT AID INTEGER, OUT AName VARCHAR(30)) BEGIN SELECT Id, Name FROM Artikel WHERE ID = ANr INTO AID, AName END; delimiter ;
Firebird: SET TERM #; 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; SET TERM ;#
Prozedur ausführen Die angelegten Prozeduren können bereits über isql bzw. mysql getestet werden. Verwenden Sie dazu die folgenden SQL-Anweisungen und bestätigen Sie jede Zeile mit (¢). MySQL: mysql> CALL NeuerArtikel(4, 'Name'); mysql> CALL ZeigeArtikel(1, @Id, @Name); mysql> SELECT @Id, @Name;
FireBird: EXECUTE PROCEDURE NeuerArtikel(4, 'Name'); COMMIT; SELECT * FROM ZeigeArtikel(4);
Java 6
961
33 – JDBC – Datenbankzugriff
Für den Aufruf dieser Prozeduren über JDBC gibt es mehrere Möglichkeiten. Die beiden dargestellten Prozeduren lassen sich auch mithilfe von Statement- und PreparedStatementObjekten ausführen, zumindest in Firebird. Im Falle von MySQL müssen für die zweite Prozedur Ausgabeparameter definiert werden. Firebird: 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 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 auch Ausgabeparameter. Deren Typ muss vor dem Aufruf der Prozedur registriert werden. Nach dem Aufruf werden die Werte über getXXX()-Methoden ausgelesen.
962
SQL-Anweisungen einsetzen
cs.registerOutParameter(1, Types.VARCHAR); cs.execute(); cs.getString(1);
Der Aufruf der Prozedur erfolgt über die Methode execute(). cs.execute();
Hinweis 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 ein zweites Mal mit einem PreparedStatement aufgerufen. Die Prozedur ZeigeArtikel liefert eine Ergebnismenge zurück, deshalb kann sie wie eine Tabelle in einer SELECT-Anweisung verwendet werden. 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(); // für MySQL EXECUTE PROCEDURE mit CALL ersetzen PreparedStatement ps = verbFB.prepareStatement( "EXECUTE PROCEDURE NeuerArtikel(103, 'Handschuhe')"); Listing 33.5: \Beispiele\de\jse6buch\kap33\StoredProcedures.java
Java 6
963
33 – JDBC – Datenbankzugriff
ps.executeUpdate(); Statement stmtFB = verbFB.createStatement(); // geht so nicht in MySQL ! 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(); } } Listing 33.5: \Beispiele\de\jse6buch\kap33\StoredProcedures.java (Forts.)
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 nur einen Wert der geänderten Datensätze zurückliefern (also INSERT, UPDATE und DELETE). 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()
964
SQL-Anweisungen einsetzen
Wenn die Batch-Verarbeitung über die Methode executeBatch() gestartet wurde, erhalten Sie in einem Array vom Typ int die Ergebnisse 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 wird aber in jedem Fall versucht, alle SQL-Anweisungen auszuführen. Über die Methode getUpdateCount() der BatchUpdateException 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()
Beispiel Ü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 der Auswertung der Rückgabewerte prüfen. Die Erstellung der Tabelle liefert übrigens den Wert 0, da keine Datensätze geändert wurden. Löschen Sie gegebenenfalls die Tabelle Artikel, wenn sie sich bereits in der Datenbank befindet: DROP TABLE Artikel import java.sql.*; public class BatchMode { public BatchMode() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { Listing 33.6: \Beispiele\de\jse6buch\kap33\BatchMode.java
Java 6
965
33 – JDBC – Datenbankzugriff
System.exit(1); } try { Connection verbMySQL = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root&password=mysql"); 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(); 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(); } } Listing 33.6: \Beispiele\de\jse6buch\kap33\BatchMode.java (Forts.)
966
Zugriff auf die Ergebnismengen
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 so genannte 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 beinhaltet die durch die SQL-Abfrage ausgewählten Eigenschaften eines Datensatzes aus einer oder mehreren Tabelle(n), z.B. die Eigenschaften eines bestimmten Kunden. Eine Spalte dagegen liefert ein bestimmtes Merkmal (Feld) 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.
Hinweis Die Arbeit mit Ergebnismengen ist oft sehr speicherintensiv. Geben Sie deshalb nicht mehr benötigte ResultSet-Objekte manuell über die Methode close() frei.
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. ist kein Datensatz mehr vorhanden, wird von der Methode der Wert false zurückgegeben. while(rs.next()) { }
Java 6
967
33 – JDBC – Datenbankzugriff
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 jedoch 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.
Hinweis Auf der Konsole werden alle Datensätze der Tabelle Kunde ausgegeben. Durch die SELECT-Anweisung werden zwei konkrete Spalten selektiert, deren Inhalte später über die getXXX()-Methoden des ResultSets datensatzweise ausgelesen werden. Die Methode next() liefert so lange den Rückgabewert true, bis kein Datensatz mehr verfügbar ist. import java.sql.*; public class Ergebnismenge { public Ergebnismenge() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verb = DriverManager.getConnection( Listing 33.7: \Beispiele\de\jse6buch\kap33\Ergebnismenge.java
968
Zugriff auf die Ergebnismengen
"jdbc:mysql://localhost/Kunden?user=root&password=mysql"); Statement stmt = verb.createStatement(); ResultSet rs = stmt.executeQuery("SELECT ID, Name FROM Kunde"); while(rs.next()) System.out.println(rs.getInt(1) + ":" + rs.getString(2)); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Ergebnismenge(); } } Listing 33.7: \Beispiele\de\jse6buch\kap33\Ergebnismenge.java (Forts.)
Hinweis Kennen Sie nicht den Aufbau der Ergebnismenge, lässt sich deren Aufbau über einen Aufruf der Methode getMetaData() und des davon zurückgegebenen ResultSetMetaDataObjekts 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. 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 Null-Wert 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.
Java 6
969
33 – JDBC – Datenbankzugriff
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()
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.
970
Zugriff auf die Ergebnismengen
boolean boolean boolean boolean
isAfterLast() isBeforeFirst() isFirst() isLast()
33.5.4 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 JDBCTreiber unterstützt wird, lässt sich über die Methoden supportsResultSetType(), supportsResultSetConcurrency() und supportsResultSetHoldability() des Interfaces DatabaseMetaData bestimmen. Statement createStatement(int resultSetType, int resultSetConcurrency) Statement createStatement(int resultSetType, int resultSetConcurrency, int 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 JDBC-Treiber eine Einstellung nicht, wird eine SQL-Warnung erzeugt. 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.
Tabelle 33.4: Konstanten zur Steuerung des Cursors in der Ergebnismenge
Ü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
Java 6
971
33 – JDBC – Datenbankzugriff
über die beiden letzten Methoden die Menge der nachzuladenden Datensätze bestimmt oder gesetzt werden. int getConcurrency() int getType() int getFetchDirection() 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. Das DBS verlangt dazu, dass ein Primärschlüssel für die betreffenden Tabellen existiert. 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. 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()
972
Zugriff auf die Ergebnismengen
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 mit der Methode insertRow() in die Datenbank übertragen. void insertRow()
Die Methode moveToCurrentRow() setzt den Datensatzzeiger nach einer Einfügeoperation wieder an die vorher gültige Position. Über die Methode moveToInsertRow() fügen Sie einen neuen Datensatz ein und setzen den Datensatzzeiger darauf. Jetzt können die Werte des Datensatzes über die updateXXX()-Methoden festgelegt werden. Die Einfügeoperation muss über den Aufruf von insertRow() abgeschlossen werden. void moveToCurrentRow() void moveToInsertRow()
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.
Java 6
973
33 – JDBC – Datenbankzugriff
Beispiel Zuerst werden alle Datensätze der Tabelle Kunde 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 werden für die Datenbank das Interface DatabaseMetaData und die Methode supportsResultSetConcurrency() verwendet sowie 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. 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&password=mysql"); 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"); Listing 33.8: \Beispiele\de\jse6buch\kap33\Aenderungen.java
974
Transaktionsverwaltung
rsMySQL.moveToInsertRow(); rsMySQL.updateInt(1, 1001); rsMySQL.updateString("Name", "Brotmesser"); rsMySQL.insertRow(); rsMySQL.moveToCurrentRow(); } catch(SQLException sqlEx) { } } public static void main(String args[]) { new Aenderungen(); } } Listing 33.8: \Beispiele\de\jse6buch\kap33\Aenderungen.java (Forts.)
Hinweis 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.
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.
Java 6
975
33 – JDBC – Datenbankzugriff
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
Tabelle 33.5: ACID-Prinzip von Transaktionen
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 beendet, 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 SQLAnweisungen immer manuell bestätigen. Erst nach einem Commit werden die Anweisungen innerhalb der Datenbank festgeschrieben. Ansonsten werden die Änderungen verworfen. void commit()
Von der Methode getAutoCommit() 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)
976
Transaktionsverwaltung
Die Datenbank selbst nutzt verschiedene Sperrmechanismen zur Sicherstellung von Transaktionen.
Hinweis In diesem Beispiel wird die manuelle Transaktionsverwaltung verwendet. Es wird dazu 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 AutoCommitModus und damit die automatische Transaktionsverwaltung wieder aktiviert. Sie können das Ergebnis z.B. über isql oder mysql überprüfen. 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); } Listing 33.9: \Beispiele\de\jse6buch\kap33\Transaktionen.java
Java 6
977
33 – JDBC – Datenbankzugriff
catch(SQLException sqlEx) { } } public static void main(String args[]) { new Transaktionen(); } } Listing 33.9: \Beispiele\de\jse6buch\kap33\Transaktionen.java (Forts.)
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. 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, wie Datensätze gesperrt werden sollen (d.h. wie Transaktionen voneinander abgeschottet werden). Greifen mehrere Transaktionen auf denselben Wert zu, können die folgenden Situationen entstehen: Fachbegriff
Erläuterung
dirty read
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 Nachdem eine Transaktion einen Wert gelesen hat, wird dieser von einer anderen Transaktion geändert. Ein erneutes Lesen des Wertes führt zu einem anderen Ergebnis. phantom read
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.
Tabelle 33.6: Problemfälle bei Ausführung mehrerer paralleler Transaktionen
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.
978
Transaktionsverwaltung
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 Wertes immer dasselbe Ergebnis erhält (p)
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 (-).
Tabelle 33.7: Konstanten zum Setzen von Isolationsstufen
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 aktuellen 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 Sie nicht mehr die gesamte Transaktion rückgängig machen müssen, können Sie Sicherungspunkte setzen. 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.
Abbildung 33.5: Sicherungspunkte in Transaktionen
Java 6
979
33 – JDBC – Datenbankzugriff
Sicherungspunkte werden über die folgenden Methoden des Interface 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. 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)
Hinweis 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. Connection verb = null; try { verb = DriverManager.getConnection( "jdbc:firebirdsql://localhost/C:/Temp/Kunden.gdb", "SYSDBA", "masterkey"); verb.setAutoCommit(false); Statement stmt = verb.createStatement(); stmt.executeUpdate("INSERT INTO Artikel VALUES(10, 'A1')"); Savepoint sp1 = verb.setSavepoint(); stmt.executeUpdate("INSERT INTO Artikel VALUES(11, 'A2')"); verb.rollback(sp1); stmt.executeUpdate("INSERT INTO Artikel VALUES(12, 'A2')"); verb.commit(); Listing 33.10: \Beispiele\de\jse6buch\kap33\Sicherungspunkte.java
980
Zugriff auf Metadaten einer Datenbank
verb.setAutoCommit(true); } catch(SQLException sqlEx) { try { verb.rollback(); } catch(SQLException sqlEx2) { } } Listing 33.10: \Beispiele\de\jse6buch\kap33\Sicherungspunkte.java (Forts.)
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
Tabelle 33.8: Interfaces zur Auswertung von Metadaten
Um ein Objekt vom Typ der genannten Interfaces zu erhalten, besitzen die entsprechenden Objekte spezielle Methoden.
Java 6
981
33 – JDBC – Datenbankzugriff
Connection verb; PreparedStatement ps; ResultSet rs; ... DatabaseMetaData dmd = verb.getMetaData(); ParameterMetaData pmd = ps.getParameterData(); ResultSetMetaData rmd = rs.getMetaData();
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 Typ int, String oder boolean, können aber auch vom Typ ResultSet sein. Letztere können wie Ergebnismengen, die durch Abfragen erzeugt wurden, verarbeitet werden.
Beispiel 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. import java.sql.*; public class DBMetadaten { public DBMetadaten() { try { Class.forName("com.mysql.jdbc.Driver"); } catch(ClassNotFoundException cnfEx) { System.exit(1); } try { Connection verb = DriverManager.getConnection( "jdbc:mysql://localhost/Kunden?user=root&password=mysql"); Listing 33.11: \Beispiele\de\jse6buch\kap33\DBMetadaten.java
982
Zugriff auf Metadaten einer Datenbank
DatabaseMetaData dmd = verb.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[] {"TABLE", "SYSTEM TABLE"}); while(rsTabs.next()) { System.out.println(rsTabs.getString(3)); 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(); } } Listing 33.11: \Beispiele\de\jse6buch\kap33\DBMetadaten.java (Forts.)
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-Feldes in einer Tabellenstruktur anzuzeigen. Über die Methoden des Interfaces ResultSetMetaData können Sie beispielsweise die Anzahl der zurückgegebenen Spalten, deren Typen und die Namen ermitteln. Als Parameter erwarten alle anderen Methoden einen Spaltenindex, der von 1 bis n läuft. Eine Ausnahme ist die Methode getColumnCount(), welche die Anzahl der Spalten liefert. Ein Objekt vom Typ ResultSetMetaData erhalten Sie durch den Aufruf der Methode getMetaData() eines ResultSets. 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.
Java 6
983
33 – JDBC – Datenbankzugriff
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 werden die Anzahl der Spalten, danach für alle Spalten der Name und der Typ ausgegeben. ResultSetMetaData rmd = rsTabs.getMetaData(); System.out.println("Anzahl Spalten: " + rmd.getColumnCount()); for(int i = 1; i quit()
Wie die Java-Objekte in der Skriptsprache verwendet werden, hängt von der Sprache ab. Informationen zur Vorgehensweise in Rhino erhalten Sie z.B. unter der URL http:// www.mozilla.org/rhino/ScriptingJava.html. Die Shellkommandos der originalen Rhino-Shell werden bis auf quit() aktuell nicht unterstützt. So funktionieren beispielsweise Kommandos wie help() oder runCommand() nicht. Dem Tool jrunscript können verschiedene Parameter übergeben werden, die z.B. auch die Ausführung eines Skripts direkt von der Kommandozeile aus erlauben.
1038
Java in Skripten verwenden
Parameter
Beschreibung
-classpath oder -cp
Setzt den Klassenpfad, über den benutzerdefinierte *.class-Dateien gesucht werden
-e
Führt ein Skript von der Kommandozeile aus, z.B. jrunscript -e print('Test')
-f
Führt die Skriptdatei aus. Aktuell muss der Parameter nicht angegeben werden. jrunscript -cp . VerwendeBibo.js jrunscript -cp . -f VerwendeBibo.js
-? und -help
Zeigen eine Hilfe an
-q
Es werden alle verfügbaren Script Engines aufgezählt
Tabelle 36.1: Parameter von jrunscript
36.3.2 Zugriff auf Java-Klassen und -Objekte Für alle Top-Level-Java-Packages besteht in Rhino bereits Zugriff über den Java-üblichen Packagenamen. Alle Packages sind dabei einer Variablen Packages untergeordnet. Dies ist insofern bedeutsam, wenn Sie eigene Packages mit Ihren Klassen über die Funktion importPackage() registrieren möchten. In diesem Fall müssen Sie diesen Variablennamen mit angeben. js> importPackage(Packages.de.jse6buch.kap36) js> var jb = new JavaBibo()
oder js> var jb = new Packages.de.jse6buch.kap36.JavaBibo()
Obwohl Rhino das Package java.lang nicht implizit einbindet, ist es zumindest in der im JDK integrierten Version bereits verfügbar. js> print(Math.PI)
Neue Objekte werden mit new erzeugt. Allerdings wird bei der Variablen kein Datentyp angegeben. Die Verwendung von var vor dem Variablennamen kann sich positiv auf die Performance auswirken, ist aber optional. js> var jb = new Packages.de.jse6buch.kap36.JavaBibo() js> jb = new Packages.de.jse6buch.kap36.JavaBibo()
Nach dem Erstellen der Variablen können die Methoden des Objekts verwendet werden. js> var jb = new Packages.de.jse6buch.kap36.JavaBibo() js> jb.add(10, 11)
Java 6
1039
36 – Scripting
Ein nettes Feature ist die Auflistung der Variablen und Methoden eines Objekts. js> var jb = new Packages.de.jse6buch.kap36.JavaBibo() js> for (member in jb) println(m)
liefert: notifyAll toString equals class wait hashCode getClass notify add
Benötigen Sie einmal einen Taschenrechner, lässt sich über die Shell auch rechnen. js> (3 * 5) + 4 19.0
Beispiel Die Verwendung von den Standard-Java-Klassen wurde bereits kurz gezeigt. Das Beispiel zeigt nun auch, wie Sie eigene Klassen nutzen können. Die Klasse JavaBibo besitzt eine Methode add(), die wieder einmal zwei Zahlen addiert. Diese Methode soll in einem Script verwendet werden. Im Skript muss dazu zuerst das Package importiert werden. In diesem Fall ist der Präfix Packages vor dem Packagenamen voranzustellen. Die weiteren Schritte entsprechen dann wieder der gleichen Vorgehensweise wie eben gezeigt. Das Tool jrunscript wird nun mit dem Dateinamen des Skripts aufgerufen. Damit das eigene Package gefunden wird, muss außerdem der Klassenpfad gesetzt werden. jrunscript -cp . VerwendeBibo.js package de.jse6buch.kap36; public class JavaBibo { public int add(int zahl1, int zahl2) { return zahl1 + zahl2; } } Listing 36.9: \Beispiele\de\jse6buch\kap36\JavaBibo.java
1040
Java in Skripten verwenden
importPackage(Packages.de.jse6buch.kap36) var bibo = new JavaBibo() var summe = bibo.add(10, 11) print("Summe = " + summe) Listing 36.10: \Beispiele\de\jse6buch\kap36\VerwendeBibo.js
Hinweis Bei allen positiven Aspekten, die mit der Verwendung von Skripts zum Tragen kommen, gibt es auch einige Schattenseiten. So liegen die Skripte im einfachsten Fall im Sourcecode vor und können somit von jedermann geändert oder einfach nur eingesehen werden. Ist Ihnen das zu heikel, sollten Sie mit verschlüsselten Dateien arbeiten. Genau das gleiche Problem betrifft die Integration der Skripte in den Java-Sourcecode. Durch Dekompilieren (z.B. über den DJ Decompiler) erhält man hier wieder den Sourcecode der Skripte und könnte sie sogar modifizieren. Im Kapitel zu JAR-Archiven wurden einige Hinweise zum Signieren von Archiven gegeben. Eine solche Signatur stellt zumindest schon einmal einen kleinen Schutz vor unbefugten Handlungen her.
Java 6
1041
Web Services 37.1 Einführung Das neue Geheimnis erfolgreicher Anwendungen ist nach Ansicht vieler Experten SOA – Service Oriented Architecture (Serviceorientierte Architekturen). Dahinter verbirgt sich eine Anwendungsarchitektur, die auf die Verwendung von lose gekoppelten Diensten basiert. Eine Möglichkeit solche Dienste zur Verfügung zu stellen sind Web Services. Ein Web Service ist dabei eine Anwendung, die eine bestimmte Serviceleistung über ein Netzwerk (z.B. das Internet) zur Verfügung stellt. Dieser Service kann prinzipiell durch beliebige Clients in Anspruch genommen werden; wobei hier das besondere ist, dass als Austauschdatenformat XML dient und der Client in einer beliebigen Programmiersprache entwickelt werden kann, die Web Services unterstützt. Neben der Programmiersprache ist auch das verwendete Betriebssystem, das der Client nutzt, beliebig. Der Vorteil, der hierdurch entsteht, liegt in der nun möglichen Verknüpfung von verschiedenen Anwendungsteilen, die auf unterschiedlichste Weise implementiert sind. Sie verfügen über eine programmiersprachen-neutral definierte Schnittstelle (nennen wir sie momentan einfach Web Service-Schnittstelle) wobei die Anwendungsteile auf Windows, Linux oder einem anderen Betriebssystem laufen können. Durch die lose Kopplung besteht eine relativ hohe Wiederverwendungsmöglichkeit, wenn die Schnittstellen entsprechend entworfen werden.
Internet-Anwendungen Web Services fügen sich in eine Reihe verschiedener Anwendungstypen ein, die über ein Netzwerk kommunizieren. Dieses Netzwerk muss nicht zwingend das Internet sein, es kann sich genauso gut um ein internes Firmennetzwerk (Intranet) handeln. Der Unterschied zwischen den Anwendungen liegt darin, wie die Aufgaben verteilt sind und welche Software auf Client- und Serverseite notwendig ist. Anwendungstyp
Beschreibung
Netzwerkanwendungen
Dies ist der allgemeinste Anwendungstyp, der als Übertragungsprotokoll nur TCP/ IP bzw. UDP/IP voraussetzt. Darauf können prinzipiell beliebige proprietäre oder standardisierte Protokolle aufsetzen. Oft nehmen einige Rechner die Rolle eines Servers ein, von denen Clients Dienste in Anspruch nehmen. Die Anwendungen können auf verschiedenen Plattformen laufen.
Peer to Peer
Im Gegensatz zu Netzwerkanwendungen sind alle Rechner gleichberechtigt, das heißt, sie verfügen über die gleiche bzw. ähnliche Funktionalität. Chatanwendungen sind ein Beispiel dafür. Es wird spezielle Software für jedes Betriebssystem benötigt.
Tabelle 37.1: Übersicht der Typen von Internet-Anwendungen
Java 6
1043
37 – Web Services
Anwendungstyp
Beschreibung
Web-Anwendungen
Die Anwendungslogik befindet sich hier auf einem WebServer, auf die über einen Browser zugegriffen wird. Dabei wird vom WebServer im einfachsten Fall statisches oder auch dynamisch erzeugtes HTML zurückgegeben. Damit entfällt die Entwicklung eines speziellen Clients. Die Web-Anwendung kann über Java Servlets, PHP oder ASP.NET und darauf aufbauende Frameworks realisiert werden.
Web Services
Der Web Service wird als Erweiterung eines Web- oder eines Application-Servers implementiert. Für den Zugriff auf den Web Service muss ein spezieller Client entwickelt werden. Ein solcher Client kann verschiedene Web Services in Anspruch nehmen.
Tabelle 37.1: Übersicht der Typen von Internet-Anwendungen (Forts.)
37.2 Grundlagen von Web Services Arbeitsweise Ein Web Service wird in der Regel durch einen Web- oder Application-Server gehostet, der Anfragen nach einem Dienst an die Web Services weiterleitet. Die Kommunikation eines Clients mit dem Web Service erfolgt über das SOAP-Protokoll. Darin sind z.B. die aufgerufene Methode, Parameter und Rückgabewerte verschlüsselt. Der Web Service führt im Falle eines synchronen Aufrufs eine Operation durch und schickt seine Antwort an den Client zurück. Die Umwandlung der übertragenen Daten wie Zahlen oder Strings in das systemeigene Format, der Aufruf der Methoden und der Versand von Rückgabewerten wird durch die Runtime des Web Service Frameworks realisiert. Um einen bestimmten Web Service zu finden, werden Verzeichnisdienste im Internet bereitgestellt. Darin können sich neue Web Services registrieren und Clients können nach einer Funktionalität suchen, die durch irgendeinen Web Service bereitgestellt wird. Damit ein Client weiß, welche Methoden ein Web Service anbietet und welche Parameter und Rückgabewerte diese besitzen, kann er ein WSDL-Dokument vom Web Service anfordern, das diese Informationen enthält.
Das Übertragungsprotokoll SOAP Die Datenübertragung mit einem Web Service erfolgt standardmäßig über das HTTPProtokoll. Dies hat den Vorteil, dass die Kommunikation nicht durch Firewalls verhindert wird, obwohl diese Web Services mittlerweile auch blockieren können. Es wäre aber auch die Kommunikation rein über TCP/IP möglich. Über das HTTP-Protokoll wird nun ein XML-Dokument mit dem Web Service ausgetauscht, dessen Aufbau dem SOAP-Protokoll genügt (Simple Object Access Protocol). Normalerweise benötigen Sie keine Kenntnisse über den Aufbau des Protokolls, da diese Aufgabe das Framework (z.B. die JAX-WS Runtime) auf Client- und Serverseite übernimmt.
1044
Web Services im JDK
WSDL Die Web Services Description Language beschreibt die Funktionalität eines Web Services, d.h. seine angebotenen Methoden mit ihren Parametern und Rückgabewerten. Die Beschreibung basiert auf XML und dient dem Entwickler eines Web Service-Clients dazu, einen Proxy zu erzeugen (oder erzeugen zu lassen), der Methoden zum Zugriff auf den Web Service bereitstellt. Ein Web Service stellt seine WSDL-Beschreibung normalerweise automatisch zur Verfügung, so dass Sie sich auch hier nicht selbst mit dem Format beschäftigen müssen. Ein WSDL-Dokument beschreibt auch die zu übertragenen Datentypen, z.B. die Parameter bei einem Methodenaufruf. Die Konvertierung zwischen der textuellen Beschreibung eines Datentyps inklusive seinem Wert (bei der Übertragung mittels SOAP) in einen echten Typ, z.B. java.lang.Integer, wird durch Data Binding erreicht, wozu JAX-WS auf JAXB zurückgreift.
UDDI Speziell für Web Services wurde ein eigener Verzeichnisdienst Universal Description, Discovery and Integration eingerichtet, über den sich Web Services auffinden lassen. Diese müssen sich dazu vorher bei diesem Dienst registrieren. Damit diese Verzeichnisse automatisch abgefragt werden können, gibt es spezielle APIs (Inquiry, Publishing), siehe auch http://www.xmethods.net/ve2/Interfaces.po.
37.3 Web Services im JDK Mit dem JDK 6 sind erstmals Web Service APIs von der Java Enterprise Edition in die Standard Edition verlagert worden. Sie dienen hauptsächlich dazu, Client-Anwendungen zu erstellen, die Web Services nutzen. Da diese APIs auch JAXB und StAX verwenden, sind diese APIs auch gleich mit in das JDK verlagert worden. JAX-WS 2.0 (Java API for XML Web Services) wird im JSR 224 beschrieben (http://jcp.org/jsr/detail/224.jsp) und realisiert das API zum Zugriff auf Web Services, d.h. die Client-Seite. Das API firmierte bisher unter dem Namen JAX-RPC. Das neue API bietet aber auch die Möglichkeit Web Services zu erzeugen und bereitzustellen, so dass Sie bereits mit der JSE 6 die Client- wie auch die Serverseite eines Web Services entwickeln können. Die aktuelle Version verwendet auf beiden Seiten (Client und Server) Annotations, um Klassen, Methoden und Parameter zu kennzeichnen, die von Web Services benötigt werden. Deshalb ist auch mindestens die Verwendung des JDK 5.0 notwendig.
37.3.1 Web Service erstellen Ein Web Service kann auf vielfältige Weise erstellt werden. Grundsätzlich müssen Sie sich entscheiden, ob Sie eine WSDL-Beschreibung oder die implementierende Klasse zuerst erstellen wollen. Die Erstellung einer Java-Klasse erscheint uns als einfachere Variante für den Einstieg.
Java 6
1045
37 – Web Services
Die Klasse besitzt einige öffentliche Methoden, welche über einen Web Service bereitgestellt werden sollen. Der Aufbau der Klasse enthält vorerst keine Besonderheiten. public class MeinWebService { public MeineWebMethode() {} }
Um daraus einen Web Service zu machen, sind lediglich einige Annotations einzufügen, den Rest erledigt JAX-WS. Die Annotations stammen dabei aus dem Package javax.ws. Die meisten Annotations können durch verschiedene zusätzliche Parameter konfiguriert werden. Die Annotation WebService kennzeichnet hier die Klasse als einen Web Service Endpunkt. Eine vom Web Service bereitgestellte Methode wird durch die Annotation WebMethod gekennzeichnet. @WebService public class MeinWebService { @WebMethod public MeineWebMethode() {} } Annotation
Beschreibung
WebMethod
Damit wird eine Methode gekennzeichnet, die durch einen Web Service bereitgestellt werden soll. Über das Attribut operationName kann der Name der Methode beim Aufruf über den Web Service umbenannt werden. Dies ist z.B. nützlich, wenn mehrere überladene Methoden verwendet werden sollen (Web Services unterstützen keine überladenen Methoden). Die Methoden müssen public sein und dürfen nicht als static oder final deklariert werden. Die Parametertypen müssen von JAXB unterstützt werden.
WebParam
Kennzeichnet einen Parameter einer Methode
WebResult
Kennzeichnet den Rückgabetyp einer Methode
WebService
Markiert eine Java-Klasse oder ein Interface, das Methoden eines Web Services enthält. Eine Klasse benötigt mindestens einen Standardkonstruktor.
Tabelle 37.2: Annotations für Web Services
Es können durchaus noch weitere Annotations eingesetzt werden, z.B. um den Bindungstyp im SOAP-Protokoll festzulegen (javax.jws.soap.SOAPBinding). Als Typ wird im Folgenden RPC verwendet, da bei Verwendung des Typs DOCUMENT weitere Annotations zu den Parametern der Methoden angegeben werden müssen. Aber auch das wäre später kein Problem mehr, da diese Informationen über Tools generiert werden können. Die Klassen, Interfaces und Annotations für Web Services befinden sich in den Packages javax.jws und javax.jws.soap sowie javax.xml.ws und einigen Unterpackages davon.
1046
Web Services im JDK
Beispiel Es soll im Folgenden ein Web Service entwickelt werden, der einige Hilfsmethoden enthält. Eine Methode soll zwei Zahlen addieren, eine zweite liefert die umgekehrte Reihenfolge einer Zeichenkette. Die Klasse UtilWS und die Methoden werden dazu mit entsprechenden Annotations versehen. Die Methode add() erhält außerdem einen neuen Namen. Die Klassen und Interfaces werden zur besseren Übersicht für den Service und die Server-Anwendung sowie für den Client in verschiedenen Packages untergebracht. package de.jse6buch.kap37.server; import javax.jws.*; import javax.jws.soap.*; @WebService @SOAPBinding(style=SOAPBinding.Style.RPC) public class UtilWS { @WebMethod(operationName="addition") public int add(int a, int b) { return a + b; } @WebMethod public String reverse(String original) { StringBuffer sb = new StringBuffer(original); return sb.reverse().toString(); } } Listing 37.1: \Beispiele\de\jse6buch\kap37\server\UtilWS.java
Nachdem der Web Service implementiert ist, muss er irgendwie bereitgestellt werden. Dies erfolgt unter Java normalerweise durch ein Deployment (Bereitstellung) auf einem Application Server. Ein solcher Server steht in der Java SE nicht zur Verfügung, weshalb eine andere Variante genutzt wird. Da die Klasse des Web Services mit der Annotation WebService versehen ist, wird sie als Endpunkt in einer Kommunikation zwischen Client und Web Service betrachtet (auch Service Endpoint Implementation genannt – kurz SEI). JAX-WS unterstützt die Verwendung einer Instanz einer EndPoint-Klasse aus dem Package javax.xml.ws, die einen Endpunkt eines Web Services darstellt. Über die Methode publish() kann der Web Service veröffentlicht werden. Dazu wird intern eine Instanz eines HTTP-Servers erstellt, der auf einem von Ihnen vorgegebenen Port läuft. Als Parameter werden die URL des Web Services sowie eine Instanz der Web Service-Klasse übergeben.
Java 6
1047
37 – Web Services
UtilWS mws = new UtilWS(); Endpoint ep = Endpoint.publish("http://localhost:8160/utilities", mws);
Beispiel Um den Web Service zum Aufruf durch einen Client bereitzustellen, wird jetzt ein Endpunkt erzeugt und veröffentlicht. Der Web Service steht jetzt unter der URL http:// localhost:8160/utilities für Anfragen bereit. Damit der Server ununterbrochen läuft, wird noch eine Endlosschleife angefügt. Die Portnummer, hier 8160, ist grundsätzlich beliebig, sofern nicht ein weiterer Server bei Ihnen auf diesem Port arbeitet. package de.jse6buch.kap37.server; import javax.xml.ws.*; public class UtilWSServer { public static void main(String[] args) { UtilWS mws = new UtilWS(); Endpoint ep = Endpoint.publish("http://localhost:8160/utilities", mws); while(true) ; } } Listing 37.2: \Beispiele\de\jse6buch\kap37\server\UtilWSServer.java
Server starten und WSDL auslesen Sie können jetzt den Server und damit auch den Web Service durch den Aufruf java de.jse6buch.kap37.server.UtilWSServer
starten. Danach öffnen Sie ein Browserfenster und geben als URL http://localhost:8160/ utilities?wsdl ein (oder die von Ihnen verwendete Portnummer). Im Browser wird daraufhin die WSDL-Beschreibung des Web Services angezeigt. Die Erzeugung einer WSDL-Datei, und sei es nur als Vorlage, ist auf diese Weise sicher am einfachsten zu bewerkstelligen. Listing 37.3: WSDL-Beschreibung des Utility-Web Services (Auszug)
1048
Web Service Client erstellen
Listing 37.3: WSDL-Beschreibung des Utility-Web Services (Auszug) (Forts.)
Um eine WSDL-Datei zu erzeugen, können Sie aber auch das Tool wsgen verwenden, z.B. wenn Sie keine Server-Anwendung benötigen bzw. die Erzeugung automatisieren möchten. wsgen -wsdl de.jse6buch.kap37.server.UtilWS
Eine WSDL-Datei wird später benötigt, um die Schnittstelle zum Zugriff auf den Web Service für den Client zu erzeugen.
37.4 Web Service Client erstellen Die Erstellung eines Clients für einen Web Service ist eine eher typische Aufgabe in der Entwicklung von Desktopanwendungen mit der Java SE. Aus diesem Grund ist das JAXWS-API auch in das JDK 6 verlagert worden. Die Erstellung des Web Services im vorigen Abschnitt sollte Ihnen allerdings zeigen, wie einfach Web Services zu erstellen sind. Außerdem wird der Service natürlich auch als Dienst verwendet, der von dem jetzt zu erstellenden Client aus genutzt werden kann.
Hinweis Im Internet stehen zahlreiche Web Services zur Verfügung, die z.B. auch von Amazon oder Google angeboten werden. Diese werden hier nicht verwendet, da Sie sich dazu meist registrieren müssen und sich die Schnittstellen der Services ab und zu ändern. Bei Interesse können Sie z.B. unter http://www.xmethods.net/sd/2001/BabelFishService.wsdl die WSDL-Datei eines kostenfreien Übersetzungsdienstes nutzen, um eine entsprechende Client-Anwendung zu erstellen. Die Vorgehensweise zur Erstellung eines Clients ist allerdings in der Regel immer gleich. In jedem Fall benötigen Sie eine Beschreibung der Schnittstellen des Web Services, da diese nicht immer nur aus ein oder zwei Methoden bestehen muss.
Java 6
1049
37 – Web Services
Für den Zugriff auf einen Web Service wird ein so genannter Proxy benötigt, der einen Aufruf an den Web Service umleitet. Für die Erstellung der notwendigen Klassen und Interfaces kann das Tool wsimport genutzt werden. Der Parameter -s legt fest, wo die erzeugten Sourcedateien abgelegt werden, über den Parameter -p legen Sie das Zielpackage fest. Dies ist notwendig, damit Sie sich nicht die Serverdateien überschreiben, da das Tool standardmäßig die in der WSDL verwendeten Package- und Porttypnamen verwendet. Nach dem Aufruf entsteht ein neues Verzeichnis ..\de\jse6buch\kap37\client, in dem zwei neue Dateien generiert wurden. wsimport -s . -p de.jse6buch.kap37.client http://localhost:8160/utilities?wsdl
Die erzeugte Klasse UtilWS.java, die sich im Package de.jse6buch.kap37.client befindet, enthält die Schnittstellenbeschreibung des Web Services. Darin sehen Sie auch, dass hier statt dem Methodennamen add() der in der Annotation WebMethod im Attribut operationName angegebene Name addition verwendet wird. Zusätzlich wurden noch Annotations für die Parameter hinzugefügt. package de.jse6buch.kap37.client; import javax.jws.*; import javax.jws.soap.*; @WebService(name="UtilWS", targetNamespace="http://server.kap37.jse6buch.de/") @SOAPBinding(style=SOAPBinding.Style.RPC) public interface UtilWS { @WebMethod @WebResult(targetNamespace="http://server.kap37.jse6buch.de/", partName="return") public int addition(@WebParam(name = "arg0", partName = "arg0") int arg0, @WebParam(name = "arg1", partName = "arg1") int arg1); @WebMethod @WebResult(targetNamespace="http://server.kap37.jse6buch.de/", partName="return") public String reverse(@WebParam(name="arg0", partName="arg0") String arg0); } Listing 37.4: \Beispiele\de\jse6buch\kap37\client\UtilWS.java (Auszug, umformatiert)
Des Weiteren wird eine Proxyklasse erzeugt, über die eine Instanz des Web Services sowie der Schnittstelle bereitgestellt wird. Die Methode, welche die Schnittstelle liefert, ist vom Namen der Schnittstelle abhängig. Hier ist es die Methode getUtilWSPort(). Über das Schnittstellenobjekt lassen sich dann die Methoden des Web Services aufrufen. Die Klasse selbst ist von der Klasse Service abgeleitet, die sich im Package javax.xml.ws befindet.
1050
Web Service Client erstellen
package de.jse6buch.kap37.client; import java.net.*; import javax.xml.namespace.*; import javax.xml.ws.*; @WebServiceClient(name="UtilWSService", targetNamespace="http://server.kap37.jse6buch.de/", wsdlLocation="http://localhost:8160/utilities?wsdl") public class UtilWSService extends Service { public UtilWSService() { ... } @WebEndpoint(name = "UtilWSPort") public UtilWS getUtilWSPort() { return (UtilWS)super.getPort( new QName("http://server.kap37.jse6buch.de/", "UtilWSPort"), UtilWS.class); } } Listing 37.5: \Beispiele\de\jse6buch\kap37\client\UtilWSService.java (Auszug)
Nachdem nun alle relevanten Klassen und Interfaces vorliegen, kann an die Implementierung des Clients gegangen werden. Da die generierten Dateien noch relativ gut verständlich sind, können sogar kleine Änderungen direkt darin vorgenommen werden, ohne den gesamten Generierungsprozess erneut zu starten.
Beispiel Zum Abschluss wird die Client-Anwendung für den Web Service erstellt. Dazu wird zuerst ein Proxyobjekt erzeugt, über das später auf die Methoden des Web Services zugegriffen werden soll. Über die Methode getUtilWSPort() wird ein Objekt vom Typ des Interfaces UtilWS geholt, über das nun mittels des Proxies die Methoden des Web Services aufgerufen werden. Die gesamte Umwandlung der Parameter in eine SOAPNachricht, das Interpretieren der Nachricht usw. wird durch JAX-WS erledigt.
Java 6
1051
37 – Web Services
package de.jse6buch.kap37.client; import javax.xml.ws.*; public class UtilWSClient { static UtilWS uws; public static void main(String[] args) { UtilWSService uwss = new UtilWSService(); uws = uwss.getUtilWSPort(); System.out.println("10 + 11 = " + uws.addition(10, 11)); System.out.println("Umkehrung von Test ist " + uws.reverse("Test")); } } Listing 37.6: \Beispiele\de\jse6buch\kap37\client\UtilWSClient.java
Hinweis Die Kommunikation mit einem Web Service wurde bisher blockierend durchgeführt, d.h., die Clientanwendung wartete auf die Antwort vom Web Service. Da dies bei längeren Antwortzeiten nicht mehr akzeptabel ist, unterstützt JAX-WS auch die asynchrone Kommunikation. In diesem Fall wird nicht mehr auf die Beendigung eines Web Services gewartet, sondern eine Callback-Routine registriert. Diese wird aufgerufen, wenn das Resultat des Web Services vorliegt.
Hinweis Die Verwendung von JAX-WS mit dem hier vorgestellten Beispiel ist relativ einfach. Wenn Sie allerdings vorhandene Web Services nutzen oder beispielsweise eine asynchrone Kommunikation verwenden wollen, ist noch Handarbeit mit der aktuellen Version notwendig bzw. es treten noch einige Probleme auf. Neben JAX-WS existieren noch andere Frameworks zur Arbeit mit Web Services. Eines davon ist z.B. Apache Axis (http://ws.apache.org/axis/). Mit Axis können Sie Web Services erstellen und auch darauf zugreifen. Interessant ist das mögliche Deployment nach Apache Tomcat, der wesentlich einfacher zu installieren ist als ein umfangreicherer Application Server.
1052
Monitoring, Management und Compiler API 38.1 Einführung Mit dem JDK 6.0 kommen Sie der Java Virtual Machine, d.h. dem Herzstück Javas, noch etwas näher. Das JDK 5.0 wurde bereits um JMX erweitert, jetzt kommt noch das Compiler API hinzu. Außerdem steht ein Interface zum Profiling und Debugging zur Verfügung, das Java Virtual Machine Tool Interface (JVM TI), das ebenfalls schon im JDK 5.0 eingeführt wurde. In diesem Kapitel werden zwei der APIs vorgestellt. Mittels des Compiler APIs können Sie JavaCode über eine API-Schnittstelle übersetzen. Jetzt können Sie dynamisch JavaCode erzeugen, diesen übersetzen und über das Reflection API laden und ausführen. Da bieten sich schon einige interessante Möglichkeiten. JMX und MBeans dienen dagegen dazu, Informationen über Ihre Anwendungen mittels einer zentralen Stelle auszuwerten und sogar steuernd einzugreifen. Benötigt Ihre Anwendung z.B. mehr Threads zur parallelen Ausführung, können Sie diese Anzahl z.B. über eine MBean erhöhen.
38.2 Das Compiler API Über das neue Package javax.tools werden Klassen und Interfaces zum Zugriff auf Java-Tools bereitgestellt. Das Package enthält hauptsächlich Klassen und Interfaces des Compiler APIs (auch Java Compiler Framework), die weiteren Typen dienen mehr oder weniger zu seiner Unterstützung. Durch die gezielte Ansteuerung des Compilers erreichen Sie eine neue, größere Dynamik Ihrer Anwendungen. Aufgrund von Benutzeraktionen lässt sich jetzt beispielsweise on-the-fly Java-Code erzeugen, der danach automatisch kompiliert und ausgeführt werden kann. Compilerfehler lassen sich über das API ermitteln und auswerten. Auch die Dateiverwaltung ist über das API wesentlich komfortabler. Den Einsteig bildet die Klasse ToolProvider, die zwei statische Methoden besitzt, um den Java Compiler und den Class Loader der aktuellen Java Plattform zurückzugeben. Im Folgenden soll nur der Zugriff auf den Java Compiler betrachtet werden. JavaCompiler getSystemJavaCompiler() ClassLoader getSystemToolClassLoader()
Java 6
1053
38 – Monitoring, Management und Compiler API
Der Rückgabewert der Methode getSystemJavaCompiler() ist z.B. null, wenn Sie nicht das Archiv tools.jar mit in den Klassenpfad aufnehmen. In diesem Archiv befindet sich nämlich die »Compiler-Klasse« com.sun.tools.javac.api.JavacTool. Aus diesem Grund sollte der Rückgabewert auf null geprüft werden. Im anderen Fall wird hier der Name der gefundenen Compilerklasse ausgegeben. JavaCompiler jct = ToolProvider.getSystemJavaCompiler(); if(jct == null) System.out.println("null (tools.jar im Klassenpfad ?)"); else { System.out.println(jct.getClass().toString()); ...
Die Methode getTask() der Klasse JavaCompiler erzeugt dann eine so genannte Übersetzungseinheit. Dazu müssen ihr verschiedenste Argumente übergeben werden, welche die Kompilierung später steuern. Das Ergebnis des Aufrufs von getTask() ist ein Objekt vom Typ des Interfaces JavaCompiler.CompilationTask. Das Interface besitzt eine Methode call(), die dann die Übersetzung durchführt. JavaCompiler.CompilationTask ct = jct.getTask(...); ct.call(); // oder kürzer jct.getTask(...).call();
Die Parameter der Methode getTask() muss man erst einmal in Ruhe auf sich wirken lassen, bevor es losgeht. Der Parameter ausgabe kann vom Compiler für Ausgaben genutzt werden. Diese lassen sich aber zur Bearbeitung auch über den Listener-Parameter abgreifen. Der Parameter fileManager kann genutzt werden, um eine anderen Dateimanager zu nutzen. Ein JavaFileManager-Objekt kann aber auch zur Zusammenstellung der Liste der zu übersetzenden Dateien verwendet werden, die als letzter Parameter übergeben werden. Um dem Compiler zusätzliche Parameter zu übergeben, nutzen Sie den Parameter options. Der Parameter classes kann für das Annotation Processing verwendet werden. getTask(Writer ausgabe, JavaFileManager fileManager, DiagnosticListener