100% fanden dieses Dokument nützlich (1 Abstimmung)
4K Ansichten833 Seiten

Das Java 6 Codebook

Hochgeladen von

Adryia
Copyright
© Attribution Non-Commercial (BY-NC)
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
100% fanden dieses Dokument nützlich (1 Abstimmung)
4K Ansichten833 Seiten

Das Java 6 Codebook

Hochgeladen von

Adryia
Copyright
© Attribution Non-Commercial (BY-NC)
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen

Das Java 6 Codebook

Dirk Louis, Peter Müller

Das Java 6 Codebook


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.

Die Informationen in diesem Produkt werden ohne Rücksicht auf einen eventuellen Patentschutz veröffentlicht.
Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zusammenstellung von Texten
und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem können Fehler nicht vollständig ausgeschlossen
werden. Verlag, Herausgeber und Autoren können für fehlerhafte Angaben und deren Folgen weder eine juristische
Verantwortung noch irgendeine Haftung übernehmen.
Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Herausgeber dankbar.

Alle Rechte vorbehalten, auch die der fotomechanischen Wiedergabe und der Speicherung in elektronischen Medien.
Die gewerbliche Nutzung der in diesem Produkt gezeigten Modelle und Arbeiten ist nicht zulässig.

Fast alle Hardware- und Softwarebezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch ver-
wendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen zeitnah zu ermitteln, ob
ein Markenschutz besteht, wird das ® Symbol in diesem Buch nicht verwendet.

Umwelthinweis:
Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt. Die Einschrumpffolie – zum Schutz vor Verschmutzung –
ist aus umweltverträglichem und recyclingfähigem PE-Material.

10 9 8 7 6 5 4 3 2 1

09 08 07

ISBN 978-3-8273-2465-8

© 2007 by Addison-Wesley Verlag,


ein Imprint der Pearson Education Deutschland GmbH,
Martin-Kollar-Straße 10–12, D-81829 München/Germany
Alle Rechte vorbehalten

Korrektorat: Petra Alm


Lektorat: Brigitte Bauer-Schiewek, [email protected]
Herstellung: Elisabeth Prümm, [email protected]
Satz: Kösel, Krugzell (www.KoeselBuch.de)
Umschlaggestaltung: Marco Lindenbeck, webwo GmbH ([email protected])
Druck und Verarbeitung: Kösel, Krugzell (www.KoeselBuch.de)

Printed in Germany
Textgestaltung
Inhaltsverzeichnis

Beispiel für eine zwei-


zeilige Überschrift
Teil I Einführung 13
Über dieses Buch 15

Teil II Rezepte 17
Zahlen und Mathematik 19

Kapiteltext
1 Gerade Zahlen erkennen 19
2 Effizientes Multiplizieren (Dividieren) mit Potenzen von 2 19
3 Primzahlen erzeugen 20
4 Primzahlen erkennen 22
5 Gleitkommazahlen auf n Stellen runden 24
6 Gleitkommazahlen mit definierter Genauigkeit vergleichen 25

Kapiteltext
7 Strings in Zahlen umwandeln 26
8 Zahlen in Strings umwandeln 30
9 Ausgabe: Dezimalzahlen in Exponentialschreibweise 34
10 Ausgabe: Zahlenkolonnen am Dezimalzeichen ausrichten 37
11 Ausgabe in Ein- oder Mehrzahl (Kongruenz) 44
12 Umrechnung zwischen Zahlensystemen 46
13 Zahlen aus Strings extrahieren 49

Kapiteltext
14 Zufallszahlen erzeugen 51
15 Ganzzahlige Zufallszahlen aus einem bestimmten Bereich 53
16 Mehrere, nicht gleiche Zufallszahlen erzeugen (Lottozahlen) 54
17 Trigonometrische Funktionen 56
18 Temperaturwerte umrechnen (Celsius <-> Fahrenheit) 56
19 Fakultät berechnen 57
Kapiteltext

20 Mittelwert berechnen 59
21 Zinseszins berechnen 60
22 Komplexe Zahlen 62
23 Vektoren 72
24 Matrizen 78
25 Gleichungssysteme lösen 90
26 Große Zahlen beliebiger Genauigkeit 91
Kapiteltext

Strings 95
27 In Strings suchen 95
28 In Strings einfügen und ersetzen 98
29 Strings zerlegen 100
30 Strings zusammenfügen 101
Kapiteltext

31 Strings nach den ersten n Zeichen vergleichen 102


6 >> Inhaltsverzeichnis
Textgestaltung

32 Zeichen (Strings) vervielfachen 106


33 Strings an Enden auffüllen (Padding) 107
34 Whitespace am String-Anfang oder -Ende entfernen 110
35 Arrays in Strings umwandeln 111
36 Strings in Arrays umwandeln 113
Beispiel für eine zwei-
zeilige Überschrift

37 Zufällige Strings erzeugen 116


38 Wortstatistik erstellen 118

Datum und Uhrzeit 121


39 Aktuelles Datum abfragen 121
40 Bestimmtes Datum erzeugen 123
41 Datums-/Zeitangaben formatieren 125
Kapiteltext

42 Wochentage oder Monatsnamen auflisten 128


43 Datumseingaben einlesen und auf Gültigkeit prüfen 130
44 Datumswerte vergleichen 131
45 Differenz zwischen zwei Datumswerten berechnen 133
46 Differenz zwischen zwei Datumswerten in Jahren, Tagen
und Stunden berechnen 134
47 Differenz zwischen zwei Datumswerten in Tagen berechnen 140
Kapiteltext

48 Tage zu einem Datum addieren/subtrahieren 141


49 Datum in julianischem Kalender 142
50 Umrechnen zwischen julianischem und
gregorianischem Kalender 143
51 Ostersonntag berechnen 144
52 Deutsche Feiertage berechnen 148
Kapiteltext

53 Ermitteln, welchen Wochentag ein Datum repräsentiert 158


54 Ermitteln, ob ein Tag ein Feiertag ist 159
55 Ermitteln, ob ein Jahr ein Schaltjahr ist 160
56 Alter aus Geburtsdatum berechnen 161
57 Aktuelle Zeit abfragen 163
58 Zeit in bestimmte Zeitzone umrechnen 164
59 Zeitzone erzeugen 165
Kapiteltext

60 Differenz zwischen zwei Uhrzeiten berechnen 168


61 Differenz zwischen zwei Uhrzeiten in Stunden, Minuten,
Sekunden berechnen 169
62 Präzise Zeitmessungen (Laufzeitmessungen) 172
63 Uhrzeit einblenden 174

System 179
Kapiteltext

64 Umgebungsvariablen abfragen 179


65 Betriebssystem und Java-Version bestimmen 180
66 Informationen zum aktuellen Benutzer ermitteln 181
67 Zugesicherte Umgebungsvariablen 182
68 System-Umgebungsinformationen abrufen 183
69 INI-Dateien lesen 184
Kapiteltext
>> Inhaltsverzeichnis 7

Textgestaltung
70 INI-Dateien schreiben 187
71 INI-Dateien im XML-Format schreiben 188
72 Externe Programme ausführen 189
73 Verfügbaren Speicher abfragen 191
74 Speicher für JVM reservieren 192

Beispiel für eine zwei-


zeilige Überschrift
75 DLLs laden 192
76 Programm für eine bestimmte Zeit anhalten 197
77 Timer verwenden 197
78 TimerTasks gesichert regelmäßig ausführen 199
79 Nicht blockierender Timer 200
80 Timer beenden 201
81 Auf die Windows-Registry zugreifen 201

Kapiteltext
82 Abbruch der Virtual Machine erkennen 206
83 Betriebssystem-Signale abfangen 208

Ein- und Ausgabe (IO) 211


84 Auf die Konsole (Standardausgabe) schreiben 211
85 Umlaute auf die Konsole (Standardausgabe) schreiben 212
86 Von der Konsole (Standardeingabe) lesen 214

Kapiteltext
87 Passwörter über die Konsole (Standardeingabe) lesen 216
88 Standardein- und -ausgabe umleiten 216
89 Konsolenanwendungen vorzeitig abbrechen 219
90 Fortschrittsanzeige für Konsolenanwendungen 219
91 Konsolenmenüs 222
92 Automatisch generierte Konsolenmenüs 225

Kapiteltext
93 Konsolenausgaben in Datei umleiten 230
94 Kommandozeilenargumente auswerten 230
95 Leere Verzeichnisse und Dateien anlegen 232
96 Datei- und Verzeichniseigenschaften abfragen 234
97 Temporäre Dateien anlegen 237
98 Verzeichnisinhalt auflisten 238
99 Dateien und Verzeichnisse löschen 239
Kapiteltext

100 Dateien und Verzeichnisse kopieren 241


101 Dateien und Verzeichnisse verschieben/umbenennen 245
102 Textdateien lesen und schreiben 245
103 Textdatei in String einlesen 248
104 Binärdateien lesen und schreiben 249
105 Random Access (wahlfreier Zugriff) 250
Kapiteltext

106 Dateien sperren 256


107 CSV-Dateien einlesen 258
108 CSV-Dateien in XML umwandeln 266
109 ZIP-Archive lesen 269
110 ZIP-Archive erzeugen 272
111 Excel-Dateien schreiben und lesen 274
112 PDF-Dateien erzeugen 278
Kapiteltext
8 >> Inhaltsverzeichnis
Textgestaltung

GUI 287
113 GUI-Grundgerüst 287
114 Fenster (und Dialoge) zentrieren 292
115 Fenstergröße festlegen (und gegebenenfalls fixieren) 295
Beispiel für eine zwei-

116 Minimale Fenstergröße sicherstellen 296


zeilige Überschrift

117 Bilder als Fensterhintergrund 298


118 Komponenten zur Laufzeit instanzieren 300
119 Komponenten und Ereignisbehandlung 303
120 Aus Ereignismethoden auf Fenster und Komponenten zugreifen 310
121 Komponenten in Fenster (Panel) zentrieren 312
122 Komponenten mit Rahmen versehen 315
123 Komponenten mit eigenem Cursor 318
Kapiteltext

124 Komponenten mit Kontextmenü verbinden 319


125 Komponenten den Fokus geben 322
126 Die Fokusreihenfolge festlegen 323
127 Fokustasten ändern 326
128 Eingabefelder mit Return verlassen 329
129 Dialoge mit Return (oder Esc) verlassen 330
Kapiteltext

130 Transparente Schalter und nichttransparente Labels 332


131 Eingabefeld für Währungsangaben (inklusive InputVerifier) 336
132 Eingabefeld für Datumsangaben (inklusive InputVerifier) 342
133 Drag-and-Drop für Labels 347
134 Datei-Drop für JTextArea-Komponenten (eigener TransferHandler) 349
135 Anwendungssymbol einrichten 356
136 Symbole für Symbolleisten 357
Kapiteltext

137 Menüleiste (Symbolleiste) aus Ressourcendatei aufbauen 359


138 Befehle aus Menü und Symbolleiste zur Laufzeit aktivieren
und deaktivieren 370
139 Menü- und Symbolleiste mit Aktionen synchronisieren 371
140 Statusleiste einrichten 377
141 Hinweistexte in Statusleiste 382
Kapiteltext

142 Dateien mit Datei-Dialog (inklusive Filter) öffnen 385


143 Dateien mit Speichern-Dialog speichern 391
144 Unterstützung für die Zwischenablage 398
145 Text drucken 400
146 Editor-Grundgerüst 410
147 Look&Feel ändern 410
148 Systemtray unterstützen 414
Kapiteltext

149 Splash-Screen anzeigen 417


150 Registerreiter mit Schließen-Schaltern (JTabbedPane) 419

Grafik und Multimedia 425


151 Mitte der Zeichenfläche ermitteln 425
152 Zentrierter Text 426
Kapiteltext

153 In den Rahmen einer Komponente zeichnen 428


>> Inhaltsverzeichnis 9

Textgestaltung
154 Zeichnen mit unterschiedlichen Strichstärken und -stilen 431
155 Zeichnen mit Füllmuster und Farbverläufen 433
156 Zeichnen mit Transformationen 436
157 Verfügbare Schriftarten ermitteln 439
158 Dialog zur Schriftartenauswahl 440

Beispiel für eine zwei-


zeilige Überschrift
159 Text mit Schattenwurf zeichnen 443
160 Freihandzeichnungen 445
161 Bilder laden und anzeigen 448
162 Bilder pixelweise bearbeiten (und speichern) 458
163 Bilder drehen 462
164 Bilder spiegeln 464
165 Bilder in Graustufen darstellen 466

Kapiteltext
166 Audiodateien abspielen 467
167 Videodateien abspielen 471
168 Torten-, Balken- und X-Y-Diagramme erstellen 475

Reguläre Ausdrücke und Pattern Matching 481


169 Syntax regulärer Ausdrücke 481
170 Überprüfen auf Existenz 484

Kapiteltext
171 Alle Treffer zurückgeben 486
172 Mit regulären Ausdrücken in Strings ersetzen 487
173 Anhand von regulären Ausdrücken zerlegen 488
174 Auf Zahlen prüfen 490
175 E-Mail-Adressen auf Gültigkeit prüfen 493
176 HTML-Tags entfernen 495

Kapiteltext
177 RegEx für verschiedene Daten 498

Datenbanken 501
178 Datenbankverbindung herstellen 501
179 Connection-Pooling 503
180 SQL-Befehle SELECT, INSERT, UPDATE und DELETE durchführen 505
181 Änderungen im ResultSet vornehmen 508
Kapiteltext

182 PreparedStatements ausführen 509


183 Stored Procedures ausführen 510
184 BLOB- und CLOB-Daten 512
185 Mit Transaktionen arbeiten 515
186 Batch-Ausführung 516
187 Metadaten ermitteln 518
Kapiteltext

188 Datenbankzugriffe vom Applet 521

Netzwerke und E-Mail 525


189 IP-Adressen ermitteln 525
190 Erreichbarkeit überprüfen 526
191 Status aller offenen Verbindungen abfragen 529
192 E-Mail senden mit JavaMail 532
Kapiteltext
10 >> Inhaltsverzeichnis
Textgestaltung

193 E-Mail mit Authentifizierung versenden 535


194 HTML-E-Mail versenden 537
195 E-Mail als multipart/alternative versenden 540
196 E-Mail mit Datei-Anhang versenden 543
197 E-Mails abrufen 545
Beispiel für eine zwei-
zeilige Überschrift

198 Multipart-E-Mails abrufen und verarbeiten 550


199 URI – Textinhalt abrufen 554
200 URI – binären Inhalt abrufen 555
201 Senden von Daten an eine Ressource 557
202 Mini-Webserver 559

XML 565
Kapiteltext

203 Sonderzeichen in XML verwenden 565


204 Kommentare 565
205 Namensräume 566
206 CDATA-Bereiche 567
207 XML parsen mit SAX 567
208 XML parsen mit DOM 571
209 XML-Dokumente validieren 575
Kapiteltext

210 XML-Strukturen mit Programm erzeugen 578


211 XML-Dokument formatiert ausgeben 580
212 XML-Dokument formatiert als Datei speichern 582
213 XML mit XSLT transformieren 584

Internationalisierung 589
Kapiteltext

214 Lokale einstellen 589


215 Standardlokale ändern 592
216 Verfügbare Lokalen ermitteln 593
217 Lokale des Betriebssystems ändern 597
218 Strings vergleichen 599
219 Strings sortieren 600
220 Datumsangaben parsen und formatieren 601
Kapiteltext

221 Zahlen parsen und formatieren 604


222 Währungsangaben parsen und formatieren 605
223 Ressourcendateien anlegen und verwenden 607
224 Ressourcendateien im XML-Format 611
225 Ressourcendateien für verschiedene Lokale erzeugen 614
226 Ressourcendatei für die Lokale des aktuellen Systems laden 616
Kapiteltext

227 Ressourcendatei für eine bestimmte Lokale laden 618

Threads 623
228 Threads verwenden 623
229 Threads ohne Exception beenden 624
230 Eigenschaften des aktuellen Threads 627
231 Ermitteln aller laufenden Threads 628
Kapiteltext
>> Inhaltsverzeichnis 11

Textgestaltung
232 Priorität von Threads 629
233 Verwenden von Thread-Gruppen 631
234 Iterieren über Threads und Thread-Gruppen einer Thread-Gruppe 632
235 Threads in Swing: SwingWorker 635
236 Thread-Synchronisierung mit synchronized (Monitor) 639

Beispiel für eine zwei-


zeilige Überschrift
237 Thread-Synchronisierung mit wait() und notify() 640
238 Thread-Synchronisierung mit Semaphoren 642
239 Thread-Kommunikation via Pipes 644
240 Thread-Pooling 646
241 Thread-globale Daten als Singleton-Instanzen 651

Applets 655

Kapiteltext
242 Grundgerüst 655
243 Parameter von Webseite übernehmen 661
244 Bilder laden und Diashow erstellen 663
245 Sounds laden 669
246 Mit JavaScript auf Applet-Methoden zugreifen 672
247 Datenaustausch zwischen Applets einer Webseite 675
248 Laufschrift (Ticker) 677

Kapiteltext
Objekte, Collections, Design-Pattern 681
249 Objekte in Strings umwandeln – toString() überschreiben 681
250 Objekte kopieren – clone() überschreiben 683
251 Objekte und Hashing – hashCode() überschreiben 689
252 Objekte vergleichen – equals() überschreiben 695

Kapiteltext
253 Objekte vergleichen – Comparable implementieren 699
254 Objekte serialisieren und deserialisieren 705
255 Arrays in Collections umwandeln 710
256 Collections in Arrays umwandeln 711
257 Collections sortieren und durchsuchen 712
258 Collections synchronisieren 716
259 Design-Pattern: Singleton 718
Kapiteltext

260 Design-Pattern: Adapter (Wrapper, Decorator) 721


261 Design-Pattern: Factory-Methoden 729

Sonstiges 733
262 Arrays effizient kopieren 733
263 Arrays vergrößern oder verkleinern 734
Kapiteltext

264 Globale Daten in Java? 735


265 Testprogramme schreiben 738
266 Debug-Stufen definieren 742
267 Code optimieren 745
268 jar-Archive erzeugen 746
269 Programme mit Ant kompilieren 748
270 Ausführbare jar-Dateien mit Ant erstellen 754
Kapiteltext
12 >> Inhaltsverzeichnis
Textgestaltung

271 Reflection: Klasseninformationen abrufen 755


272 Reflection: Klasseninformationen über .class-Datei abrufen 760
273 Reflection: Klassen instanzieren 762
274 Reflection: Methode aufrufen 766
275 Kreditkartenvalidierung 768
Beispiel für eine zwei-
zeilige Überschrift

276 Statistik 770

Teil III Anhang 777


Tabellen 779
Java 779
Kapiteltext

Swing 782
Reguläre Ausdrücke 790
SQL 796
Lokale 797

Die Java-SDK-Tools 799


javac – der Compiler 799
Kapiteltext

java – der Interpreter 801


jar – Archive erstellen 803
javadoc – Dokumentationen erstellen 806
jdb – der Debugger 807
Weitere Tools 809

Stichwortverzeichnis 811
Kapiteltext

Lizenzvereinbarungen 825
Kapiteltext
Kapiteltext
Kapiteltext
Teil I Einführung
Über dieses Buch

Wenn Sie glauben, mit dem vorliegenden Werk ein Buch samt Begleit-CD erstanden zu haben,
befinden Sie sich im Irrtum. Was Sie gerade in der Hand halten, ist in Wahrheit eine CD mit
einem Begleitbuch.
Auf der CD finden Sie – nach Themengebieten geordnet – ungefähr 300 Rezepte mit ready-to-
use Lösungen für die verschiedensten Probleme. Zu jedem Rezept gibt es im Repository den
zugehörigen Quelltext (in Form eines Codefragments, einer Methode oder einer Klasse), den
Sie nur noch in Ihr Programm zu kopieren brauchen.
Wer den Code eines Rezepts im praktischen Einsatz erleben möchte, findet auf der CD zudem
zu fast jedem Rezept ein Beispielprogramm, das die Verwendung des Codes demonstriert.
Und dann gibt es noch das Buch.
Im Buch sind alle Rezepte abgedruckt, beschrieben und mit Hintergrundinformationen erläu-
tert. Es wurde für Programmierer geschrieben, die konkrete Lösungen für typische Probleme
des Programmieralltags suchen oder einfach ihren Fundus an nützlichen Java-Techniken und
-Tricks erweitern wollen. In diesem Sinne ist das Java-Codebook die ideale Ergänzung zu
Ihrem Java-Lehr- oder -Referenzbuch.

Auswahl der Rezepte


Auch wenn dreihundert Rezepte zweifelsohne eine ganz stattliche Sammlung darstellen, so
bilden wir uns nicht ein, damit zu jedem Problem eine passende Lösung angeboten zu haben.
Dazu ist die Java-Programmierung ein zu weites Feld (und selbst das vereinte Wissen zweier
Autoren nicht ausreichend). Wir haben uns aber bemüht, eine gute Mischung aus häufig
benötigten Techniken, interessanten Tricks und praxisbezogenen Designs zu finden, wie sie
zum Standardrepertoire eines jeden fortgeschrittenen Java-Programmierers gehören sollten.
Sollten Sie das eine oder andere unentbehrliche Rezept vermissen, schreiben Sie uns
([email protected]). Auch wenn wir nicht versprechen können, jede Anfrage mit einem
nachgereichten Rezept beantworten zu können, so werden wir zumindest versuchen, Ihnen
mit einem Rat oder Hinweis weiterzuhelfen. Auf jeden Fall aber werden wir ihre Rezeptvor-
schläge bei der nächsten Auflage des Buches berücksichtigen.

Fragen an die Autoren


Trotz aller Sorgfalt lässt es sich bei einem Werk dieses Umfangs erfahrungsgemäß nie ganz
vermeiden, dass sich Tippfehler, irreführende Formulierungen oder gar inhaltliche Fehler ein-
schleichen. Scheuen Sie sich in diesem Fall nicht, uns per E-Mail an [email protected]
eine Nachricht zukommen zu lassen. Auch für Lob, Anregungen oder Themenwünsche sind
wir stets dankbar.
Errata werden auf der Website www.carpelibrum.de veröffentlicht.
Sollten Sie Fragen zu einem bestimmten Rezept haben, wenden Sie sich bitte direkt an den
betreffenden Autor. In den Quelltexten im Ordner Beispiele sind dazu die Namen der Autoren
angegeben. Von Ausnahmen abgesehen gilt aber auch die folgende Zuordnung.
16 >> Über dieses Buch

Kategorien Autor(en)
Zahlen und Mathematik Dirk Louis, [email protected]
Strings
Datum und Uhrzeit
System Peter Müller, [email protected]
Ein- und Ausgabe Peter Müller, [email protected]
GUI Dirk Louis, [email protected]
Grafik und Multimedia
Reguläre Ausdrücke und Pattern Matching Dirk Louis, [email protected]
Datenbanken Peter Müller, [email protected]
Netzwerke und E-Mail Peter Müller, [email protected]
XML
Internationalisierung Dirk Louis, [email protected]
Threads Peter Müller, [email protected]
Applets Dirk Louis, [email protected]
Objekte, Collections, Design-Pattern Dirk Louis, [email protected]
Sonstiges

Kompilieren der Buchbeispiele


Wenn Sie eines der Beispiele von der CD ausführen und testen möchten, gehen Sie wie folgt
vor:
1. Kopieren Sie das Verzeichnis auf Ihre Festplatte.
2. Kompilieren Sie die Quelldateien.
In der Regel genügt es dem Java-Compiler den Namen der Programmdatei mit der main()-
Methode zu übergeben. Meist heißt die Programmdatei Start.java oder Program.java.
javac Start.java
oder
javac Program.java
Bei einigen Programmen müssen Sie externe Bibliotheken in Form von jar-Archiven in
den CLASSPATH aufnehmen oder mit der Option -cp als Parameter an javac übergeben
(javac -cp xyz.jar Program.java).
3. Führen Sie das Programm aus:
java Start
oder
java Program
Bei einigen Programmen müssen Sie externe Bibliotheken in Form von jar-Archiven in
den CLASSPATH aufnehmen oder mit der Option –cp als Parameter an java übergeben
(java -cp xyz.jar Program).
Für Beispielprogramme mit abweichendem Aufruf finden Sie im Verzeichnis des Beispiels eine
Readme-Datei mit passendem Aufruf.
Zahlen und Mathematik

Strings

Datum und Uhrzeit

Teil II Rezepte System

Ein- und Ausgabe (IO)

GUI

Grafik und Multimedia

RegEx

Datenbanken

Netzwerke und E-Mail

XML

International

Threads

Applets

Objekte

Sonstiges
Zahlen
Zahlen und Mathematik

1 Gerade Zahlen erkennen


Wie alle Daten werden auch Integer-Zahlen binär kodiert, jedoch nicht, wie man vielleicht
annehmen könnte, als Zahlen im Binärsystem, sondern als nach dem 2er-Komplement
kodierte Folgen von Nullen und Einsen.
Im 2er-Komplement werden positive Zahlen durch ihre korrespondierenden Binärzahlen dar-
gestellt. Das oberste Bit (MSB = Most Significant Bit) kodiert das Vorzeichen und ist für posi-
tive Zahlen 0. Negative Zahlen haben eine 1 im MSB und ergeben sich, indem man alle Bits
der korrespondierenden positiven Zahlen gleichen Betrags invertiert und +1 addiert. Der Vor-
zug dieser auf den ersten Blick unnötig umständlich anmutenden Kodierung ist, dass die
Rechengesetze trotz Kodierung des Vorzeichens im MSB erhalten bleiben.
Das Wissen um die Art der Kodierung erlaubt einige äußerst effiziente Tricks, beispielsweise
das Erkennen von geraden Zahlen. Es lässt sich leicht nachvollziehen, dass im 2er-Komple-
ment alle geraden Zahlen im untersten Bit eine 0 und alle ungeraden Zahlen eine 1 stehen
haben. Man kann also leicht an dem untersten Bit (LSB = Least Significant Bit) ablesen, ob es
sich bei einer Integer-Zahl um eine gerade oder ungerade Zahl handelt.
Ein einfaches Verfahren ist, eine bitweise AND-Verknüpfung zwischen der zu prüfenden Zahl
und der Zahl 1 durchzuführen. Ist das Ergebnis 0, ist die zu prüfende Zahl gerade.

/**
* Stellt fest, ob die übergebene Zahl gerade oder ungerade ist
*/
public static boolean isEven(long number) {
return ((number & 1l) == 0l) ? true : false;
}

Listing 1: Gerade Zahlen erkennen

2 Effizientes Multiplizieren (Dividieren) mit


Potenzen von 2
Wie im Dezimalsystem die Verschiebung der Ziffern um eine Stelle einer Multiplikation bzw.
Division mit 10 entspricht, so entspricht im Binärsystem die Verschiebung um eine Stelle einer
Multiplikation bzw. Division mit 2. Multiplikationen und Divisionen mit Potenzen von 2
können daher mit Hilfe der bitweisen Shift-Operatoren << und >> besonders effizient durchge-
führt werden.
Um eine Integer-Zahl mit 2n zu multiplizieren, muss man ihre Bits einfach nur um n Positio-
nen nach links verschieben:
20 >> Primzahlen erzeugen
Zahlen

/**
* Multiplizieren mit Potenz von 2
*/
public static long mul(long number, int pos) {
return number << pos;
}

Listing 2: Multiplikation

Um eine Integer-Zahl durch 2n zu dividieren, muss man ihre Bits einfach nur um n Positionen
nach rechts verschieben:

/**
* Dividieren mit Potenz von 2
*/
public static long div(long number, int pos) {
return number >> pos;
}

Listing 3: Division

Beachten Sie, dass bei Shift-Operationen mit << und >> das Vorzeichen erhalten bleibt (anders
als bei einer Verschiebung mit >>>)!
Die Division mit div() erwies sich trotz des Function Overheads durch den Methodenaufruf als
um einiges schneller als die /-Operation.

3 Primzahlen erzeugen
Primzahlen sind Zahlen, die nur durch sich selbst und durch 1 teilbar sind. Dieser schlichten
Definition stehen einige der schwierigsten und auch fruchtbarsten Probleme der Mathematik
gegenüber: Wie viele Primzahlen gibt es? Wie kann man Primzahlen erzeugen? Wie kann man
testen, ob eine gegebene Zahl eine Primzahl ist? Wie kann man eine gegebene Zahl in ihre
Primfaktoren zerlegen?
Während die erste Frage bereits in der Antike von dem griechischen Mathematiker Euklid beant-
wortet werden konnte (es gibt unendlich viele Primzahlen), erwiesen sich die anderen als Inspi-
ration und Herausforderung für Generationen von Mathematikern – und Informatikern. So
spielen Primzahlen beispielsweise bei der Verschlüsselung oder bei der Dimensionierung von
Hashtabellen eine große Rolle. Im ersten Fall ist man an der Generierung großer Primzahlen
interessiert (und nutzt den Umstand, dass sich das Produkt zweier genügend großer Primzahlen
relativ einfach bilden lässt, es aber andererseits unmöglich ist, in angemessener Zeit die Prim-
zahlen aus dem Produkt wieder herauszurechnen). Im zweiten Fall wird berücksichtigt, dass die
meisten Hashtabellen-Implementierungen Hashfunktionen1 verwenden, die besonders effizient
arbeiten, wenn die Kapazität der Hashtabelle eine Primzahl ist.

1. Hashtabellen speichern Daten als Schlüssel/Wert-Paare. Aufgabe der Hashfunktion ist es, aus dem Schlüssel den
Index zu berechnen, unter dem der Wert zu finden ist. Eine gute Hashfunktion liefert für jeden Schlüssel einen eige-
nen Index, der direkt zu dem gesuchten Wert führt. Weniger gute Hashfunktionen liefern für verschiedene Schlüssel
den gleichen Index, so dass hinter diesen Indizes Wertelisten stehen, die noch einmal extra durchsucht werden müs-
sen. In der Java-API werden Hashtabellen beispielsweise durch HashMap, HashSet oder Hashtable implementiert.
>> Zahlen und Mathematik 21

Zahlen
Der wohl bekannteste Algorithmus zur Erzeugung von Primzahlen ist das Sieb des Eratos-
thenes.
1. Schreibe alle Zahlen von 2 bis N auf.
2. Rahme die 2 ein und streiche alle Vielfachen von 2 durch.
3. Wiederhole Schritt 2 für alle n mit n <= sqrt(N), die noch nicht durchgestrichen wurden.
4. Alle eingerahmten oder nicht durchgestrichenen Zahlen sind Primzahlen.
Eine mögliche Implementierung dieses Algorithmus verwendet die nachfolgend definierte
Methode sieve(), die die Primzahlen aus einem durch min und max gegebenen Zahlenbereich
als LinkedList-Container zurückliefert.

import java.util.LinkedList;
...
/**
* Sieb des Eratosthenes
*/
public static LinkedList<Integer> sieve(int min, int max) {
if( (min < 0) || (max < 2) || (min > max) )
return null;

BitSet numbers = new BitSet(max); // anfangs liefern alle Bits false


numbers.set(0); // keine Primzahlen -> auf true setzen
numbers.set(1);

int limit = (int) Math.sqrt(max);

for(int n = 2; n <= limit; ++n)


if(!numbers.get(n))
for(int i = 2*n; i < max; i+=n)
numbers.set(i);

// Primzahlen im gesuchten Bereich zusammenstellen


LinkedList<Integer> prims = new LinkedList<Integer>();

for(int i = min; i < max; ++i)


if(!numbers.get(i))
prims.add(i);

return prims;
}

Listing 4: Primzahlen erzeugen

Die Methode prüft zuerst, ob der angegebene Bereich überhaupt Primzahlen enthält. Dann legt
sie einen BitSet-Container zahlen an, der die Zahlen von 0 bis max repräsentiert. Anfangs sind
die Bits in numbers nicht gesetzt (false), was bedeutet, die Zahlen sind noch nicht ausgestri-
chen. In zwei verschachtelten for-Schleifen werden die Nicht-Primzahlen danach ausgestri-
chen. Zu guter Letzt werden die Primzahlen zwischen min und max in einen LinkedList-
Container übertragen und als Ergebnis zurückgeliefert.
22 >> Primzahlen erkennen
Zahlen

Wenn es Sie interessiert, welche Jahre im 20. Jahrhundert Primjahre waren, können Sie diese
Methode beispielsweise wie folgt aufrufen:
import java.util.LinkedList;
...
// Primzahlen erzeugen
LinkedList<Integer> prims = MoreMath.sieve(1900, 2000);

if(prims == null)
System.out.println("Fehler in Sieb-Aufruf");
else
for(int elem : prims)
System.out.print(" " + elem);
Wenn Sie in einem Programm einen Hashtabellen-Container anlegen wollen, dessen Anfangs-
kapazität sich erst zur Laufzeit ergibt, benötigen Sie allerdings eine Methode, die ihnen genau
eine passende Primzahl zurückliefert. Dies leistet die Methode getPrim(). Sie übergeben der
Methode die gewünschte Mindestanfangskapazität und erhalten die nächsthöhere Primzahl
zurück.

import java.util.LinkedList;
...
/**
* Liefert die nächsthöhere Primzahl zurück
*/
public static int getPrim(int min) {

LinkedList<Integer> l;
int max = min + 20;

do {
l = sieve(min, max);
max += 10;
} while (l.size() == 0);

return l.getFirst();
}

Listing 5: Die kleinste Primzahl größer n berechnen

Die Erzeugung eines HashMap-Containers mit Hilfe von getPrim() könnte wie folgt aussehen:
java.util.HashMap map = new java.util.HashMap(MoreMath.getPrim(min));

4 Primzahlen erkennen
Das Erkennen von Primzahlen ist eine Wissenschaft für sich – und ein Gebiet, auf dem sich
vor kurzem (im Jahre 2001) Erstaunliches getan hat.
Kleinere Zahlen, also Zahlen < 9.223.372.036.854.775.807 (worin Sie unschwer die größte
positive long-Zahl erkennen werden), lassen sich schnell und effizient ermitteln, indem man
prüft, ob sie durch irgendeine kleinere Zahl (ohne Rest) geteilt werden können.
>> Zahlen und Mathematik 23

Zahlen
/**
* Stellt fest, ob eine long-Zahl eine Primzahl ist
*/
public static boolean isPrim(long n) {

if (n <= 1) // Primzahl sind positive Zahlen > 1


return false;

if ((n & 1l) == 0l) // gerade Zahl


return false;

long limit = (long) Math.sqrt(n);

for(long i = 3; i < limit; i+=2)


if(n % i == 0)
return false;

return true;
}

Listing 6: »Kleine« Primzahlen erkennen

Die Methode testet zuerst, ob die zu prüfende Zahl n kleiner als 2 oder gerade ist. Wenn ja, ist
die Methode fertig und liefert false zurück. Hat n die ersten Tests überstanden, geht die
Methode in einer Schleife alle ungeraden Zahlen bis sqrt(n) durch und probiert, ob n durch
die Schleifenvariable i ohne Rest geteilt werden kann. Gibt es einen Teiler, ist n keine Primzahl
und die Methode kehrt sofort mit dem Rückgabewert false zurück. Gibt es keinen Teiler, liefert
die Methode true zurück.
Leider können wegen der Beschränkung des Datentyps long mit dieser Methode nur relativ
kleine Zahlen getestet werden. Um größere Zahlen zu testen, könnte man den obigen Algo-
rithmus für die Klasse BigInteger implementieren. (BigInteger und BigDecimal erlauben die
Arbeit mit beliebig großen (genauen) Integer- bzw. Gleitkommazahlen, siehe Rezept 26.) In der
Praxis ist dieser Weg aber kaum akzeptabel, denn abgesehen davon, dass die BigInteger-
Modulo-Operation recht zeitraubend ist, besitzt der Algorithmus wegen der Modulo-Operation
in der Schleife von vornherein ein ungünstiges Laufzeitverhalten.
Mangels schneller deterministischer Verfahren werden Primzahlen daher häufig mit Hilfe pro-
babilistischer Verfahren überprüft, wie zum Beispiel dem Primtest nach Rabin-Miller.
So verfügt die Klasse BigInteger über eine Methode isProbablePrime(), mit der Sie BigInte-
ger-Zahlen testen können. Liefert die Methode false zurück, handelt es sich definitiv um
keine Primzahl. Liefert die Methode true zurück, liegt die Wahrscheinlichkeit, dass es sich um
eine Primzahl handelt, bei 1 – 1/2n. Den Wert n übergeben Sie der Methode als Argument.
(Die Methode testet intern nach Rabin-Miller und Lucas-Lehmer.)
import java.math.BigInteger;

/**
* Stellt fest, ob eine BigInteger-Zahl eine Primzahl ist
* (erkennt Primzahl mit nahezu 100%-iger Sicherheit (1 - 1/28 = 0,9961))
*/
24 >> Gleitkommazahlen auf n Stellen runden
Zahlen

public static boolean isPrim(BigInteger n) {


return n.isProbablePrime(8);
}
Abschließend sei noch erwähnt, dass es seit 2002 ein von den indischen Mathematikern Agra-
wal, Kayal und Saxena gefundenes deterministisches Polynomialzeitverfahren zum Test auf
Primzahlen gibt.

5 Gleitkommazahlen auf n Stellen runden


Die von Math angebotenen Rundungsmethoden runden – wie das Casting in einen Integer-Typ
– stets bis zu einem Integer-Wert auf oder ab.

Rundungsmethode Beschreibung
Cast () Rundet immer ab.
Math.rint(double x) Rundet mathematisch (Rückgabetyp double).
Math.round(double x) Rundet kaufmännisch (Rückgabetyp long bzw. int).
Math.round(float x)
Math.ceil(double x) Rundet auf die nächste größere ganze Zahl auf (Rückgabetyp double).
Math.floor(double x) Rundet auf die nächste kleinere ganze Zahl ab (Rückgabetyp double).

Tabelle 1: Rundungsmethoden

Möchte man Dezimalzahlen auf Dezimalzahlen mit einer bestimmten Anzahl Nachkommastel-
len runden, bedarf es dazu eigener Methoden: eine zum mathematischen und eine zum kauf-
männischen Runden auf x Stellen.
Das kaufmännische Runden betrachtet lediglich die erste zu rundende Stelle. Ist diese gleich 0,
1, 2, 3 oder 4, wird ab-, ansonsten aufgerundet. Dieses Verfahren ist einfach, führt aber zu
einer gewissen Unausgewogenheit, da mehr Zahlen auf- als abgerundet werden. Das mathe-
matische Runden rundet immer von hinten nach vorne und unterscheidet sich in der Behand-
lung der 5 als zu rundender Zahl:
왘 Folgen auf die 5 noch weitere von 0 verschiedene Ziffern, wird aufgerundet.
왘 Ist die 5 durch Abrundung entstanden, wird aufgerundet. Ist sie durch Aufrundung ent-
standen, wird abgerundet.
왘 Folgen auf die 5 keine weiteren Ziffern, wird so gerundet, dass die vorangehende Ziffer
gerade wird.
Der letzte Punkt führt beispielsweise dazu, dass die Zahl 8.5 von rint() auf 8 abgerundet wird,
während sie beim kaufmännischen Runden mit round() auf 9 aufgerundet wird.
Mit den folgenden Methoden können Sie kaufmännisch bzw. mathematisch auf n Stellen
genau runden.
/**
* Kaufmännisches Runden auf n Stellen
*/
public static double round(double number, int n) {
return (Math.round(number * Math.pow(10,n))) / Math.pow(10,n);
}
>> Zahlen und Mathematik 25

Zahlen
/**
* Mathematisches Runden auf n Stellen
*/
public static double rint(double number, int n) {
return (Math.rint(number * Math.pow(10,n))) / Math.pow(10,n);
}
Die Methoden multiplizieren die zu rundende Zahl mit 10n, um die gewünschte Anzahl Nach-
kommastellen zu erhalten, runden das Ergebnis mit round() bzw. rint() und dividieren das
Ergebnis anschließend durch 10n, um wieder die alte Größenordnung herzustellen.

Abbildung 1: Kaufmännisches und mathematisches Runden auf drei Stellen

6 Gleitkommazahlen mit definierter Genauigkeit


vergleichen
Gleitkommazahlen können zwar sehr große oder sehr kleine Zahlen speichern, jedoch nur mit
begrenzter Genauigkeit. So schränkt der zur Verfügung stehende Speicherplatz den Datentyp
float auf ca. sieben und den Datentyp double auf ungefähr zehn signifikante Stellen ein.
Ergeben sich im Zuge einer Berechnung mit Gleitkommazahlen Zahlen mit mehr signifikanten
Stellen oder fließen Literale mit mehr Stellen ein, so entstehen Rundungsfehler.
Kommt es bei einer Berechnung nicht auf extreme Genauigkeit an, stören die Rundungsfehler
meist nicht weiter. (Wenn Sie beispielsweise die Wohnfläche einer Wohnung berechnen, wird
es nicht darauf ankommen, ob diese 95,45 oder 94,450000001 qm beträgt.)
Gravierende Fehler können allerdings entstehen, wenn man Gleitkommazahlen mit Run-
dungsfehlern vergleicht. So ergibt der Vergleich in dem folgenden Codefragment wegen Run-
dungsfehlern in der Zahlendarstellung nicht die erwartete Ausgabe »gleich Null«.
double number = 12.123456;
number -= 12.0;
number -= 0.123456;

if (number == 0.0)
System.out.println("gleich Null");
Dabei weicht number nur minimal von 0.0 ab! Um mit Rundungsfehlern behaftete Gleitkomma-
zahlen korrekt zu vergleichen, bedarf es daher einer Vergleichsfunktion, die mit einer gewis-
sen Toleranz (epsilon) arbeitet:
26 >> Strings in Zahlen umwandeln
Zahlen

/**
* Gleitkommazahlen mit definierter Genauigkeit vergleichen
*/
public static boolean equals(double a, double b, double eps) {
return Math.abs(a - b) < eps;
}
Mit dieser Methode kann die »Gleichheit« wie gewünscht festgestellt werden:
double number = 12.123456;
number -= 12.0;
number -= 0.123456;

if(MoreMath.equals(number, 0.0, 1e10))


System.out.println("gleich Null");

Apropos Vergleiche und Gleitkommazahlen: Denken Sie daran, dass Sie Vergleiche
Achtung

gegen NaN oder Infinity mit Double.isNaN() bzw. Double.isInfinity() durchführen.

7 Strings in Zahlen umwandeln


Benutzereingaben, die über die Konsole (System.in), über Textkomponenten von GUI-Anwen-
dungen (z.B. JTextField) oder aus Textdateien in eine Anwendung eingelesen werden, sind
immer Strings – selbst wenn diese Strings Zahlen repräsentieren. Um mit den Zahlenwerten
rechnen zu können, müssen die Strings daher zuerst in einen passenden numerischen Typ wie
int oder double umgewandelt werden.
Die Umwandlung besteht grundsätzlich aus zwei Schritten:
왘 Dem Aufruf einer geeigneten Umwandlungsmethode
왘 Der Absicherung der Umwandlung für den Fall, dass der String keine gültige Zahl enthält

Für die Umwandlung selbst gibt es verschiedene Möglichkeiten und Klassen:


왘 Die parse-Methoden der Wrapper-Klassen
Die Wrapper-Klassen zu den elementaren Datentypen (Short, Integer, Double etc.) verfügen
jede über eine passende statische parse-Methode (parseShort(), parseInt(), parseDouble()
etc.), die den ihr übergebenen String in den zugehörigen elementaren Datentyp umwan-
delt. Kann der String nicht umgewandelt werden, wird eine NumberFormatException aus-
gelöst.
Die parse-Methoden der Wrapper-Klassen für die Ganzzahlentypen, Byte, Short, Int und
Long, sind überladen, so dass Sie neben dem umzuwandelnden String auch die Basis des
Zahlensystems angeben können, in dem die Zahl im String niedergeschrieben ist:
parseInt(String s, int base).
try {
number = Integer.parseInt(str);
}
catch(NumberFormatException e) {}
>> Zahlen und Mathematik 27

Zahlen
왘 Die parse()-Methode von DecimalFormat
Die Klasse DecimalFormat wird zwar vorzugsweise zur formatierten Umwandlung von Zah-
len in Strings verwendet (siehe Rezept 8), mit ihrer parse()-Methode kann aber auch der
umgekehrte Weg eingeschlagen werden.
Die parse()-Methode parst die Zeichen im übergebenen String so lange, bis sie auf ein
Zeichen trifft, das sie nicht als Teil der Zahl interpretiert (Buchstabe, Satzzeichen). Aber
Achtung! Das Dezimalzeichen, gemäß der voreingestellten Lokale der Punkt, wird igno-
riert. Die eingeparsten Zeichen werden in eine Zahl umgewandelt und als Long-Objekt
zurückgeliefert. Ist der Zahlenwert zu groß oder wurde zuvor für das DecimalFormat-Objekt
setParseBigDecimal(true) aufgerufen, wird das Ergebnis als Double-Objekt zurückgeliefert.
Kann keine Zahl zurückgeliefert werden, etwa weil der String mit einem Buchstaben
beginnt, wird eine ParseException ausgelöst.
Der Rückgabetyp ist in jedem Fall Number. Mit den Konvertierungsmethoden von Number
(toInt(), toDouble() etc.) kann ein passender elementarer Typ erzeugt werden.
Die parse()-Methode ist überladen. Die von NumberFormat geerbte Version übernimmt
allein den umzuwandelnden String, die in DecimalFormat definierte Version erhält als zwei-
tes Argument eine Positionsangabe vom Typ ParsePosition, die festlegt, ab wo mit dem
Parsen des Strings begonnen werden soll.
import java.text.DecimalFormat;
import java.text.ParseException;

DecimalFormat df = new DecimalFormat();


try {
number = (df.parse(str)).intValue();
}
catch(ParseException e) {}
왘 Die next-Methoden der Klasse Scanner
Mit der Klasse Scanner können Eingaben aus Strings, Dateien, Streams oder auch der Kon-
sole (System.in) eingelesen werden. Die Eingabe wird in Tokens zerlegt (Trennzeichen
(Delimiter) sind standardmäßig alle Whitespace-Zeichen – also Leerzeichen, Tabulatoren,
Zeilenumbrüche).
Die einzelnen Tokens können mit next() als Strings oder mit den nextTyp-Methoden
(nextInt(), nextDouble(), nextBigInteger() etc.) als numerische Typen eingelesen werden.
Im Falle eines Fehlers werden folgende Exceptions ausgelöst: InputMismatchException,
NoSuchElementException und IllegalStateException. Letztere wird ausgelöst, wenn der
Scanner zuvor geschlossen wurde. Die beiden anderen Exceptions können Sie vermeiden,
wenn Sie vorab mit next(), nextInt(), nextDouble() etc. prüfen, ob ein weiteres Token vor-
handen und vom gewünschten Format ist.
import java.util.Scanner;

Scanner scan = new Scanner(str); // new Scanner(System.in), um


// von der Konsole zu lesen
if (scan.hasNextInt())
number = scan.nextInt();
28 >> Strings in Zahlen umwandeln
Zahlen

Methode Beschreibung Absicherung Laufzeit


(für »154«
auf PIII,
2 GHz)
Integer.parseInt() Übernimmt als Argument den NumberFormatException < 1 sec
umzuwandelnden String und
versucht ihn in einen int-Wert
umzuwandeln.
»154« -> 154
»15.4« -> Exception
»15s4« -> Exception
»s154« -> Exception
DecimalFormat.parse() Übernimmt als Argument den ParseException ~ 100 sec
umzuwandelnden String, parst
diesen Zeichen für Zeichen, bis
das Ende oder ein Nicht-Zah-
len-Zeichen erreicht wird, und
liefert das Ergebnis als Long-
Objekt (bzw. Double) zurück.
»154« -> 154
»15.4« -> 154
»15s4« -> 15
»s154« -> Exception
Scanner.nextInt() »154« -> 154 Scanner.hasNextInt() ~ 2500 sec
»15.4« -> hasNextInt() ergibt
false
»15s4« -> hasNextInt() ergibt
false
»s154« -> hasNextInt() ergibt
false

Tabelle 2: Vergleich verschiedener Verfahren zur Umwandlung von Strings in Zahlen

Mit dem folgenden Programm können Sie Verhalten und Laufzeit der verschiedenen Umwand-
lungsmethoden auf Ihrem Rechner prüfen:

import java.util.Scanner;
import java.text.DecimalFormat;
import java.text.ParseException;

public class Start {

public static void main(String[] args) {


System.out.println();

if (args.length != 1) {
System.out.println(" Aufruf: Start <Ganzzahl>");

Listing 7: Vergleich verschiedener Umwandlungsverfahren


>> Zahlen und Mathematik 29

Zahlen
System.exit(0);
}

long start, end; // für die Zeitmessung


int number;
String str = args[0]; // Die umzuwandelnde Zahl als String

// Umwandlung mit parseInt()


number = -1;
start = System.currentTimeMillis();
for(int i = 0; i <= 10000; ++i) {
try {
number = Integer.parseInt(str);
}
catch(NumberFormatException e) {}
}
end = System.currentTimeMillis();
System.out.printf("%15s liefert %d nach %5s sec \n", "parseInt()",
number, (end-start));

// Umwandlung mit DecimalFormat


number = -1;
start = System.currentTimeMillis();
for(int i = 0; i <= 10000; ++i) {
DecimalFormat df = new DecimalFormat();
try {
number = (df.parse(str)).intValue();
}
catch(ParseException e) {}
}
end = System.currentTimeMillis();
System.out.printf("%15s liefert %d nach %5s sec \n", "DecimalFormat",
number, (end-start));

// Umwandlung mit Scanner


number = -1;
start = System.currentTimeMillis();
for(int i = 0; i <= 10000; ++i) {
Scanner scan = new Scanner(str);
if (scan.hasNextInt())
number = scan.nextInt();
}
end = System.currentTimeMillis();
System.out.printf("%15s liefert %d nach %5s sec \n", "Scanner", number,
(end-start));

}
}

Listing 7: Vergleich verschiedener Umwandlungsverfahren (Forts.)


30 >> Zahlen in Strings umwandeln
Zahlen

8 Zahlen in Strings umwandeln


Die Umwandlung von Zahlen in Strings gehört wie ihr Pendant, die Umwandlung von Strings
in Zahlen, zu den elementarsten Programmieraufgaben überhaupt. Java unterstützt den Pro-
grammierer dabei mit drei Varianten:
왘 der auf toString() basierenden, (weitgehend) automatischen Umwandlung (für größtmög-
liche Bequemlichkeit),
왘 der auf NumberFormat und DecimalFormat basierenden, beliebig formatierbaren Umwand-
lung (für größtmögliche Flexibilität)
왘 sowie der von C übernommenen formatierten Ausgabe mit printf(). (printf() eignet sich
nur zur Ausgabe auf die Konsole und wird hier nicht weiter behandelt. Für eine Beschrei-
bung der Methode siehe Lehrbücher zu Java oder die Java-API-Referenz.)

Zahlen in Strings umwandeln mit toString()


Wie Sie wissen, erben alle Java-Klassen von der obersten Basisklasse die Methode toString(),
die eine Stringdarstellung des aktuellen Objekts zurückliefert. Die Implementierung von
Object liefert einen String des Aufbaus klassenname@hashCodeDesObjekts zurück. Abgeleitete
Klassen können die Methode überschreiben, um sinnvollere Stringdarstellungen ihrer Objekte
zurückzugeben. Für die Wrapper-Klassen zu den numerischen Datentypen ist dies geschehen
(siehe Tabelle 3).

toString()-Methode zurückgelieferter String


Integer.toString() Stringdarstellung der Zahl, bestehend aus maximal 32 Ziffern. Negative
Byte.toString() Zahlen beginnen mit einem Minuszeichen.
Short.toString() 123
-9000
Long.toString() Wie für Integer, aber mit maximal 64 Ziffern.
Float.toString() Null wird als 0.0 dargestellt.
Double.toString() Zahlen, deren Betrag zwischen 10-3 und 107 liegt, werden als Zahl mit
Nachkommastellen dargestellt. Es wird immer mindestens eine Nachkom-
mastelle ausgegeben. Der intern verwendete Umwandlungsalgorithmus
kann dazu führen, dass eine abschließende Null ausgegeben wird.
Zahlen außerhalb des Bereichs von 10-3 und 107 werden in Exponential-
schreibweise dargestellt oder als infinity.
Negative Zahlen werden mit Vorzeichen dargestellt.
-333.0 // -333
0.0010 // 0.001
9.9E-4 // 0.00099
Infinity // 1e380 * 10

Tabelle 3: Formate der toString()-Methoden

Bei Ausgaben mit PrintStream.print() und PrintStream.println() oder bei Stringkonkatena-


tionen mit dem +-Operator wird für primitive numerische Daten intern automatisch ein Objekt
der zugehörigen Wrapper-Klasse erzeugt und deren toString()-Methode aufgerufen. Dieser
Trick erlaubt es, Zahlen mühelos auszugeben oder in Strings einzubauen – sofern man sich
mit der Standardformatierung durch die toString()-Methoden zufrieden gibt.
>> Zahlen und Mathematik 31

Zahlen
int number = 12;
System.out.print(number);
System.out.print("Wert der Variablen: " + number);

Zahlen in Strings umwandeln mit NumberFormat und DecimalFormat


Wem die Standardformate von toString() nicht genügen, der kann auf die abstrakte Klasse
NumberFormat und die von ihr abgeleite Klasse DecimalFormat zurückgreifen. Die Klasse
DecimalFormat arbeitet mit Patterns (Mustern). Jedes DecimalFormat-Objekt kapselt intern ein
Pattern, das angibt, wie das Objekt Zahlen formatiert. Die eigentliche Formatierung erfolgt
durch Aufruf der format()-Methode des Objekts. Die zu formatierende Zahl wird als Argument
übergeben, der formatierte String wird als Ergebnis zurückgeliefert.
Die Patterns haben folgenden Aufbau:
Präfixopt Zahlenformat Suffixopt
Präfix und Suffix können neben beliebigen Zeichen, die unverändert ausgegeben werden,
auch die Symbole % (Prozentsymbol), \u2030 (Promillesymbol) und \u00A4 (Währungssym-
bol) enthalten. Die eigentliche Zahl wird gemäß dem mittleren Teil formatiert, der folgende
Symbole enthalten kann:

Symbol Bedeutung
0 obligatorische Ziffer
# optionale Ziffer
. Dezimalzeichen
, Tausenderzeichen
- Minuszeichen
E Exponentialzeichen

Tabelle 4: Symbole für DecimalFormat-Patterns

Integer- und Gleitkommazahlen können nach folgenden Schemata aufgebaut werden:


#,##0

#,##0.00#
Die Vorkommastellen können durch das Tausenderzeichen gruppiert werden. Es ist unnötig,
mehr als ein Tausenderzeichen zu setzen, da bei mehreren Tausenderzeichen die Anzahl der
Stellen pro Gruppe gleich der Anzahl Stellen zwischen dem letzten Tausenderzeichen und dem
Ende des ganzzahligen Teils ist (in obigem Beispiel 3). Im Vorkommateil darf rechts von einer
obligatorischen Ziffer (0) keine optionale Ziffer mehr folgen. Die maximale Anzahl Stellen im
Vorkommateil ist unbegrenzt, optionale Stellen müssen lediglich zum Setzen des Tausender-
zeichens angegeben werden. Im Nachkommateil darf rechts von einer optionalen Ziffer keine
obligatorische Ziffer mehr folgen. Die Zahl der obligatorischen Ziffern entspricht hier der Min-
destzahl an Stellen, die Summe aus obligatorischen und optionalen Ziffern der Maximalzahl an
Stellen. Optional kann sich an beide Formate die Angabe eines Exponenten anschließen (siehe
Rezept 9).
Negative Zahlen werden standardmäßig durch Voranstellung des Minuszeichens gebildet, es
sei denn, es wird dem Pattern für die positiven Zahlen mittels ; ein spezielles Negativ-Pattern
angehängt.
32 >> Zahlen in Strings umwandeln
Zahlen

Landesspezifische Symbole wie Tausenderzeichen, Währungssymbol etc. werden gemäß der


aktuellen Lokale der JVM gesetzt.
import java.text.DecimalFormat;
...

double number = 3344.588;

DecimalFormat df = new DecimalFormat("#,##0.00");


System.out.println(df.format(number)); // Ausgabe: 3,344.59
Statt eigene Formate zu definieren, können Sie sich auch von den statischen Methoden der
Klasse NumberFormat vordefinierte DecimalFormat-Objekte zurückliefern lassen:

NumberFormat-Methode Liefert
getInstance() Format für beliebige Zahlen:
getNumberInstance() #,##0.###
(= Zahl mit mindestens einer Stelle, maximal drei Nachkomma-
stellen und Tausenderzeichen nach je drei Stellen)
getIntegerInstance() Format für Integer-Zahlen:
#,##0
(= Zahl mit mindestens einer Stelle, keine Nachkommastellen und
Tausenderzeichen nach je drei Stellen)
getPercentInstance() Format für Prozentangaben:
#,##0%
(= Zahl mit mindestens einer Stelle, keine Nachkommastellen,
Tausenderzeichen nach je drei Stellen und abschließendem Prozent-
zeichen)
(Achtung! Die zu formatierende Zahl wird automatisch mit 100
multipliziert.)
getCurrencyInstance() Format für Preisangaben:
#,##0.00 ¤
(= Zahl mit mindestens einer Stelle, genau zwei Nachkommastellen,
Tausenderzeichen nach je drei Stellen und abschließendem Leer- und
Währungszeichen)

Tabelle 5: Factory-Methoden der Klasse NumberFormat

import java.text.NumberFormat;
...

double number = 0.3;


NumberFormat nf = NumberFormat.getPercentInstance();
System.out.print(nf.format(number)); // Ausgabe: 30%

number = 12345.6789;
nf = NumberFormat.getNumberInstance();
System.out.print(nf.format(number)); // Ausgabe: 12.345,679
>> Zahlen und Mathematik 33

Zahlen
Formatierungsobjekte anpassen
Die von einem DecimalFormat-Objekt vorgenommene Formatierung kann jederzeit durch Auf-
ruf der entsprechenden set-Methoden angepasst werden.

Methode Beschreibung
void setCurrency(Currency c) Ändert die zu verwendende Währung.
void setDecimalSeparatorAlwaysShown(boolean opt) Wird true übergeben, wird das Dezimalzeichen
auch am Ende von Integer-Zahlen angezeigt.
void setGroupingSize(int n) Anzahl Stellen pro Gruppe.
void setGroupingUsed(boolean opt) Legt fest, ob die Vorkommastellen gruppiert
werden sollen.
void setMaximumFractionDigits(int n) Maximale Anzahl Stellen im Nachkommateil.
void setMaximumIntegerDigits(int n) Maximale Anzahl Stellen im Integer-Teil.
void setMinimumFractionDigits(int n) Minimale Anzahl Stellen im Nachkommateil.
void setMinimumIntegerDigits(int n) Minimale Anzahl Stellen im Integer-Teil.
void setMultiplier(int n) Faktor für Prozent- und Promille-Darstellung.
setNegativePrefix(String new) Setzt Präfixe und Suffixe der Patterns für
setNegativeSuffix(String new) negative bzw. positive Zahlen.
setPositivePrefix(String new)
setPositiveSuffix(String new)

Tabelle 6: Set-Methoden von DecimalFormat (die hervorgehobenen Methoden sind auch für
NumberFormat definiert)

import java.text.DecimalFormat;
import java.text.NumberFormat;
...

double number = 12345.6789;


NumberFormat nf = NumberFormat.getNumberInstance();
nf.setMaximumFractionDigits(2);
nf.setGroupingUsed(false);
if (nf instanceof DecimalFormat)
((DecimalFormat) nf).setPositiveSuffix(" Meter");

System.out.print(nf.format(number)); // Ausgabe: 12345,68 Meter

Eigene, vordefinierte Formate


Wenn Sie an verschiedenen Stellen immer wieder dieselben Formatierungen benötigen, lohnt
sich unter Umständen die Definition eigener vordefinierter Formate, beispielsweise in Form
von static final-Konstanten, die mittels eines static-Blocks konfiguriert werden.
Die folgenden Definitionen gestatten die schnelle Formatierung von Gleitkommazahlen gemäß
US-amerikanischer Gepflogenheiten, mit maximal drei Nachkommastellen und je nach aus-
gewählter Konstante mit oder ohne Gruppierung.
import java.text.NumberFormat;
import java.util.Locale;

public class MoreMath {


34 >> Ausgabe: Dezimalzahlen in Exponentialschreibweise
Zahlen

public static final NumberFormat NFUS;


public static final NumberFormat NFUS_NOGROUP;

static {
NFUS = NumberFormat.getNumberInstance(Locale.US);
NFUS_NOGROUP = NumberFormat.getNumberInstance(Locale.US);
NFUS_NOGROUP.setGroupingUsed(false);
}

// Instanzbildung unterbinden
private MoreMath() { }
}
Aufruf:
public static void main(String args[]) {
...
double number = 12345.6789

System.out.println(MoreMath.NFUS.format(number));
System.out.println(MoreMath.NFUS_NOGROUP.format(number));
}
Ausgabe:
12,345.679
12345.679

Mehr zur landesspezifischen Formatierung in der Kategorie »Internationalisierung«.


H in we i s

9 Ausgabe: Dezimalzahlen in Exponentialschreibweise


Es mag verwundern, aber die Ausgabe von Dezimalzahlen in Exponentialschreibweise stellt in
Java insofern ein Problem dar, als es (derzeit) keine standardmäßige Unterstützung dafür gibt.
Wenn Sie für die Umwandlung einer Dezimalzahl in einen String der toString()-Methode ver-
trauen, sind die Zahlen mal als normale Dezimalbrüche und mal in Exponentialschreibweise
formatiert. Wenn Sie NumberFormat.getNumberInstance() bemühen, erhalten Sie immer einfache
Dezimalbrüche. Eine vordefinierte NumberFormat-Instanz für die Exponentialschreibweise gibt es
nicht. Lediglich printf() bietet Unterstützung für die Formatierung in Exponentialschreibweise
(Konvertierungssymbol %e), doch eignet sich printf() nur für die Konsolenausgabe.
Will man also Dezimalzahlen in Exponentialschreibweise darstellen, muss man für ein geeig-
netes Pattern ein eigenes DecimalFormat-Objekt erzeugen. (Mehr zu DecimalFormat-Patterns in
Rezept 8).

DecimalFormat-Patterns für die Exponentialdarstellung


Patterns für die Exponentialdarstellung bestehen wie jedes DecimalFormat-Pattern aus Präfix,
Zahlenformat und Suffix. Das Zahlenformat hat den Aufbau:
#0.0#E0
>> Zahlen und Mathematik 35

Zahlen
Die Umwandlung eines solchen Patterns in eine formatierte Zahl ist etwas eigentümlich.
Grundsätzlich gilt: Sie geben die maximale und minimale Anzahl Vorkommastellen an und
das DecimalFormat-Objekt berechnet den passenden Exponenten. Aus diesem Grund kann für
den Exponenten auch nur die minimale Anzahl Stellen angegeben werden. Die maximale Zahl
ist unbeschränkt.
Für die Berechnung des Exponenten gibt es zwei Modelle, die über den Aufbau des Vorkom-
mateils ausgewählt werden:
왘 Besteht der Vorkommateil nur aus obligatorischen Stellen (0), berechnet DecimalFormat den
Exponenten so, dass exakt die vorgegebene Zahl Stellen vor dem Komma erreicht wird.

Zahl Pattern String


12.3456 0.#E0 1,2E1
12.3456 000.#E0 123,5E-1
12.3456 000.#E00 123,5E-01

왘 Enthält der Vorkommateil optionale Stellen (#), ist der Exponent stets ein Vielfaches der
Summe an Vorkommastellen.

Zahl Pattern String


12.3456 #.#E0 1,2E1
12.3456 ##0.#E0 12,35E0
123456 ##0.#E0 123,5E3

Die Anzahl signifikanter Stellen in der Mantisse entspricht der Summe aus obligatorischen
Vorkomma- und maximaler Anzahl Nachkommastellen.
Tausenderzeichen sind nicht erlaubt.

Vordefinierte Patterns
Wenn Sie an verschiedenen Stellen immer wieder dieselben Formatierungen benötigen, lohnt
sich unter Umständen die Definition eigener vordefinierter Formate, beispielsweise in Form
von static final-Konstanten, die mittels eines static-Blocks konfiguriert werden.
Die folgenden Definitionen gestatten die schnelle Formatierung von Gleitkommazahlen in
Exponentialschreibweise mit
왘 einer Vorkommastelle und sechs signifikanten Stellen (DFEXP),
왘 sechs signifikanten Stellen und Exponenten, die Vielfache von 3 sind (DFEXP_ENG).

import java.text.DecimalFormat;

public class MoreMath {

public static final DecimalFormat DFEXP;


public static final DecimalFormat DFEXP_ENG;

Listing 8: Vordefinierte DecimalFormat-Objekte für die Exponentialdarstellung


36 >> Ausgabe: Dezimalzahlen in Exponentialschreibweise
Zahlen

static {
DFEXP = new DecimalFormat("0.#####E0");
DFEXP_ENG = new DecimalFormat("##0.#####E0");
}
...
}

Listing 8: Vordefinierte DecimalFormat-Objekte für die Exponentialdarstellung (Forts.)

Eigene Formatierungsmethode
DecimalFormat verwendet zur Kennzeichnung des Exponenten ein großes E und zeigt positive
Exponenten ohne Vorzeichen an. Wer ein kleines E bevorzugt oder den Exponenten stets mit
Vorzeichen dargestellt haben möchte (so wie es printf() tut), muss den von Decimal-
Format.format() zurückgelieferten String manuell weiterverarbeiten.
Die Methode formatExp() kann Ihnen diese Arbeit abnehmen. Sie formatiert die übergebene
Zahl in Exponentialschreibweise mit einer Vorkommastelle. Die maximale Anzahl Nachkom-
mastellen in der Mantisse wird als Argument übergeben. Optional können Sie über Boolesche
Argumente zwischen großem und kleinem E und zwischen Plus- und Minuszeichen oder nur
Minuszeichen vor dem Exponenten wählen.

import java.text.DecimalFormat;

public class MoreMath {


...

/**
* Formatierung als Dezimalzahl in Exponentialschreibweise
*/
public static String formatExp(double number, int maxStellen) {
return MoreMath.formatExp(number, maxStellen, false, false);
}

public static String formatExp(double number, int maxStellen,


boolean smallExp) {
return MoreMath.formatExp(number, maxStellen, smallExp, false);
}

public static String formatExp(double number, int maxDigits,


boolean smallExp, boolean plus) {

// Pattern für Exponentialschreibweise erzeugen


StringBuilder pattern = new StringBuilder("0.#");

if(maxDigits > 1)
pattern.append(MoreString.charNTimes('#',maxDigits-1));

pattern.append("E00");

Listing 9: Dezimalzahlen in Exponentialschreibweise


>> Zahlen und Mathematik 37

Zahlen
// Zahl als String formatieren
String str = (new DecimalFormat(pattern.toString())).format(number);

// Exponentzeichen und/oder Pluszeichen


if (smallExp || (plus && Math.abs(number) >= 1)) {

int pos = str.indexOf('E');


StringBuilder tmp = new StringBuilder(str);

if (smallExp)
tmp.replace(pos, pos+1, "e");

if (plus && Math.abs(number) >= 1)


tmp.insert(pos+1, '+');

return tmp.toString();
} else
return str;
}
}

Listing 9: Dezimalzahlen in Exponentialschreibweise (Forts.)

Die Methode formatExp(double number, int maxStellen, boolean smallExp, boolean plus)
baut zuerst das gewünschte Pattern auf, wobei sie zur Vervielfachung der optionalen Nach-
kommastellen die Methode MoreString.charNTimes() aus Rezept 32 aufruft. Dann erzeugt sie
den gewünschten Formatierer und noch in der gleichen Zeile durch Aufruf der format()-
Methode die Stringdarstellung der Zahl. Für eine Darstellung mit kleinem e wird das große E
im String durch das kleine e ersetzt. Wurde die Darstellung mit Pluszeichen vor dem Exponent
gewünscht und ist der Betrag der Zahl größer oder gleich 1, wird das Pluszeichen hinter dem E
(bzw. e) eingefügt.
Für die Nachbearbeitung des Strings wird dieser in ein StringBuilder-Objekt umgewandelt –
nicht wegen der Unveränderbarkeit von String-Objekten (die dazu führt, dass bei String-
Manipulationen stets Kopien erzeugt werden), sondern wegen der insert()-Methode, die
String fehlt.
Mit dem zugehörigen Testprogramm können Sie das Ergebnis verschiedener Formatierungs-
möglichkeiten vergleichen.

10 Ausgabe: Zahlenkolonnen am Dezimalzeichen


ausrichten
Die meisten Programmierer betrachten die Formatierung der Ausgaben als ein notwendiges
Übel – sicherlich nicht ganz zu Unrecht, denn neben der Berechnung korrekter Ausgaben ist
deren Formatierung natürlich nur zweitrangig. Trotzdem sollte die Formatierung der Ausga-
ben, insbesondere die Präsentation von Ergebnissen, nicht vernachlässigt werden – nicht ein-
mal dann, wenn sich dieses notwendige Übel als unnötig kompliziert erweist, wie
beispielsweise bei der Ausrichtung von Zahlenkolonnen am Dezimalzeichen. In der API-Doku-
mentation zur Java-Klasse NumberFormat findet sich hierzu der Hinweis, dass sich besagtes
Problem durch Übergabe eines FieldPosition-Objekts an die format()-Methode von Number-
38 >> Ausgabe: Zahlenkolonnen am Dezimalzeichen ausrichten
Zahlen

Abbildung 2: Formatierung von Dezimalzahlen

Format (bzw. DecimalFormat) lösen lässt. Wie dies konkret aussieht, untersuchen die beiden fol-
genden Abschnitte.

Ausgaben bei proportionaler Schrift


Als Beispiel betrachten wir das folgende Zahlen-Array:
double[] numbers = { 1230.45, 100, 8.1271 };
Um diese Zahlenkolonne untereinander, ausgerichtet am Dezimalzeichen, ausgeben zu kön-
nen, müssen drei Dinge geschehen:
1. Die Zahlen müssen in Strings umgewandelt werden.
Dies geschieht durch Erzeugung einer passenden NumberFormat- oder DecimalFormat-Instanz
und Übergabe an die Methode format(), siehe Rezept 8.
2. Die gewünschte Position des Dezimalzeichens muss festgelegt werden.
Hier ist zu beachten, dass die gewählte Position nicht zu weit vorne liegt, damit nicht bei
der Ausgabe der Platz für die Vorkommastellen der einzelnen Strings fehlt. Liegen die aus-
zugebenden Strings bereits zu Beginn der Ausgabe komplett vor, empfiehlt es sich, die
Strings mit den Zahlen in einer Schleife zu durchlaufen und sich an dem String zu orien-
tieren, in dem das Dezimalzeichen am weitesten hinten liegt.
3. Den einzelnen Strings müssen so viele Leerzeichen vorangestellt werden, bis ihr Dezimal-
zeichen an der gewünschten Stelle liegt.
Die nachfolgend abgedruckte Methode alignAtDecimal() erledigt alle drei Schritte in einem.
Die Methode übernimmt ein double-Array der auszugebenden Zahlen und einen Formatstring
für DecimalFormat und liefert die für die Ausgabe aufbereiteten Stringdarstellungen als Array
von StringBuffer-Objekten zurück. Für die häufig benötigte Ausgabe von Zahlen mit zwei
Nachkommastellen gibt es eine eigene überladene Version, der Sie nur das Zahlen-Array über-
geben müssen.

import java.text.DecimalFormat;
import java.text.FieldPosition;
...
/**
* Array von Strings am Dezimalzeichen ausrichten

Listing 10: Ausrichtung am Dezimalzeichen bei proportionaler Schrift


>> Zahlen und Mathematik 39

Zahlen
* (Version für proportionale Schrift)
*/
public static StringBuffer[] alignAtDecimal (double[] numbers) {
return alignAtDecimalPoint(numbers, "#,##0.00");
}

public static StringBuffer[] alignAtDecimal (double[] numbers,


String format) {
DecimalFormat df = new DecimalFormat(format);
FieldPosition fpos =
new FieldPosition(DecimalFormat.INTEGER_FIELD);
StringBuffer[] strings = new StringBuffer[numbers.length];
int[] charToDecP = new int[numbers.length];
int maxDist = 0;

// nötige Vorarbeiten
// Strings initisieren, Position des Dezimalpunkts
// feststellen, max. Zahl Vorkommastellen ermitteln
for(int i = 0; i < numbers.length; ++i) {
strings[i] = new StringBuffer("");
df.format(numbers[i], strings[i], fpos);
charToDecP[i] = fpos.getEndIndex();
if (maxDist < charToDecP[i])
maxDist = charToDecP[i];
}

// nötige Anzahl Leerzeichen voranstellen


char[] pad;
for(int i = 0; i < numbers.length; ++i) {
pad = new char[maxDist - charToDecP[i]];
for(int n = 0; n < pad.length; ++n)
pad[n] = ' ';

strings[i].insert(0, pad);
}

return strings;
}

Listing 10: Ausrichtung am Dezimalzeichen bei proportionaler Schrift (Forts.)

Wie findet diese Methode die Position der Dezimalzeichen? Denkbar wäre natürlich, einfach
mit indexOf() nach dem Komma zu suchen. Doch dieser Ansatz funktioniert natürlich nur,
wenn DecimalFormat gemäß einer Lokale formatiert, die das Komma als Dezimalzeichen ver-
wendet. Lauten die Alternativen demnach, entweder eigenen Code zur Unterstützung ver-
schiedener Lokale zu schreiben oder aber eine feste Lokale vorzugeben und damit auf
automatische Adaption an nationale Eigenheiten zu verzichten? Mitnichten. Sie müssen der
format()-Methode lediglich als drittes Argument eine FieldPosition-Instanz übergeben und
können sich dann von diesem die Position des Dezimalzeichens zurückliefern lassen. Für die
alignAtDecimal()-Methode sieht dies so aus, dass diese eingangs ein FieldPosition-Objekt
erzeugt. Dieses liefert Informationen über den ganzzahligen Anteil, zu welchem Zweck die
Konstante INTEGER_FIELD übergeben wird. (Wenn Sie die ebenfalls vordefinierte Konstante
40 >> Ausgabe: Zahlenkolonnen am Dezimalzeichen ausrichten
Zahlen

FRACTION_FIELD übergeben, beziehen sich die Angaben, die die Methoden des FieldPosition-
Objekts zurückliefern, auf den Nachkommaanteil.)
In einer ersten Schleife werden dann die Zahlen mit Hilfe der format()-Methode in Strings
umgewandelt und in StringBuffer-Objekten abgespeichert. Die Position des Dezimalzeichens
wird für jeden String mit Hilfe der FieldPosition-Methode getEndIndex() abgefragt und im
Array charToDecP zwischengespeichert. (Enthält der String kein Dezimalzeichen, wird die Posi-
tion hinter der letzten Ziffer des Vorkommateils zurückgeliefert.) Gleichzeitig wird in maxDist
der größte Abstand von Stringanfang bis Dezimalzeichen festgehalten.
In der anschließenden, zweiten for-Schleife werden die Strings dann so weit vorne mit Leer-
zeichen aufgefüllt, dass in allen Strings das Dezimalzeichen maxDist Positionen hinter dem
Stringanfang liegt.
Der Einsatz der Methode könnte nicht einfacher sein: Sie übergeben ihr das Array der zu for-
matierenden Zahlen und erhalten die fertigen Strings in Form eines StringBuffer-Arrays
zurück:
// aus Start.java
double[] numbers = { 1230.45, 100, 8.1271 };
StringBuffer[] strings;

strings = MoreMath.alignAtDecimal(numbers);
System.out.println();
System.out.println(" Kapital : " + strings[0]);
System.out.println(" Bonus : " + strings[1]);
System.out.println(" Rendite (%) : " + strings[2]);
Sagt Ihnen die vorgegebene Formatierung mit zwei Nachkommastellen nicht zu, übergeben
Sie einfach Ihren eigenen Formatstring, siehe auch Rezept 8.
// aus Start.java
strings = MoreMath.alignAtDecimal(numbers, "#,##0.0######");
System.out.println();
System.out.println(" Kapital : " + strings[0]);
System.out.println(" Bonus : " + strings[1]);
System.out.println(" Rendite (%) : " + strings[2]);

Ausgaben bei nichtproportionaler Schrift


Etwas komplizierter wird es, wenn die Zahlen in nichtproportionaler Schrift in ein Fenster
oder eine Komponente (vorzugsweise eine Canvas- oder JPanel-Instanz) gezeichnet werden
sollen. Da in einer nichtproportionalen Schrift die einzelnen Buchstaben unterschiedliche
Breiten haben, können die Strings mit den Zahlendarstellungen nicht durch Einfügen von
Leerzeichen ausgerichtet werden. Stattdessen muss für jeden String berechnet werden, ab wel-
cher x-Koordinate mit dem Zeichnen des Strings zu beginnen ist, damit sein Dezimalzeichen
in einer Höhe mit den Dezimalzeichen der anderen Strings liegt.
>> Zahlen und Mathematik 41

Zahlen
Abbildung 3: Ausgerichtete Zahlenkolonnen (Formate: »#,##0.00« (Vorgabe der überladenen
alignAtDecimal()-Version), »#,##0.0######« und »#,##0.##«)

Statt die aufbereiteten Stringdarstellungen auf die Konsole auszugeben, können Sie sie
H inwe is

auch in Dateien oder GUI-Komponenten schreiben oder in eine GUI-Komponente, bei-


spielsweise ein JPanel-Feld, zeichnen. Einzige Bedingung: Es wird eine proportionale
Schrift verwendet.
public void paintComponent(Graphics g) {
super.paintComponent(g);

int x = 50;
int y = 50;
FontMetrics fm;

g.setFont(new Font("Courier", Font.PLAIN, 24));


fm = g.getFontMetrics();

strings = MoreMath.alignAtDecimal(numbers, "#,##0.0######");


g.drawString("Kapital : " + strings[0].toString(), x, y );
g.drawString("Bonus : " + strings[1].toString(),
x, y + fm.getHeight());
g.drawString("Rendite (%) : " + strings[2].toString(),
x, y + 2 * fm.getHeight());
}

import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.awt.FontMetrics;

/**
* Array von Strings am Dezimalzeichen ausrichten

Listing 11: Ausrichtung am Dezimalzeichen bei nichtproportionaler Schrift


42 >> Ausgabe: Zahlenkolonnen am Dezimalzeichen ausrichten
Zahlen

* (Version für nicht-proportionale Schrift)


*/
public static StringBuffer[] alignAtDecimal(double[] numbers,
FontMetrics fm,
int[] xOffsets) {
return alignAtDecimal(numbers, "#,##0.00", fm, xOffsets);
}

public static StringBuffer[] alignAtDecimal(double[] numbers,


String format,
FontMetrics fm,
int[] xOffsets) {
DecimalFormat df = new DecimalFormat(format);
FieldPosition fpos =
new FieldPosition(DecimalFormat.INTEGER_FIELD);
StringBuffer[] strings = new StringBuffer[numbers.length];
int[] pixToDecP = new int[numbers.length];
int maxDist = 0;

if(numbers.length != xOffsets.length)
throw new IllegalArgumentException("Fehler in Array-Dimensionen");

// nötige Vorarbeiten
// Strings erzeugen, Position des Dezimalpunkts
// feststellen, Pixelbreite bis Dezimalpunkt ermitteln
for(int i = 0; i < numbers.length; ++i) {
strings[i] = new StringBuffer("");
df.format(numbers[i], strings[i], fpos);

pixToDecP[i] = fm.stringWidth(strings[i].substring(0,
fpos.getEndIndex()));
if (maxDist < pixToDecP[i])
maxDist = pixToDecP[i];
}

// xOffsets berechnen
for(int i = 0; i < numbers.length; ++i) {
xOffsets[i] = maxDist - pixToDecP[i];
}

return strings;
}

Listing 11: Ausrichtung am Dezimalzeichen bei nichtproportionaler Schrift (Forts.)

Für die Ausrichtung von Zahlen in nichtproportionaler Schrift übernimmt die alignAtDeci-
mal()-Methode zwei weitere Argumente:
왘 Zum einen muss sie für jeden formatierten String den Abstand vom Stringanfang bis zum
Dezimalzeichen in Pixeln berechnen. Da sie dies nicht allein leisten kann, übernimmt sie
ein FontMetrics-Objekt, das zuvor für den gewünschten Ausgabefont erzeugt wurde (siehe
>> Zahlen und Mathematik 43

Zahlen
Listing 12). Deren stringWidth()-Methode übergibt sie den Teilstring vom Stringanfang
bis zum Dezimalzeichen und erhält als Ergebnis die Breite in Pixel zurück, die sie im Array
pixToDecP speichert.
왘 Neben den formatierten Strings muss die Methode dem Aufrufer für jeden String den x-
Offset übergeben, um den die Ausgabe verschoben werden muss, damit die Dezimalzeichen
untereinander liegen. Zu diesem Zweck übernimmt die Methode ein int-Array, in dem es
die Offsetwerte abspeichert. Die Offsetwerte selbst werden in der zweiten for-Schleife als
Differenz zwischen der Pixelposition des am weitesten entfernt liegenden Dezimalzeichens
(maxDist) und der Position des Dezimalzeichens im aktuellen String (pixToDecP) berechnet.

Im folgenden Beispiel werden letzten Endes zwei Spalten ausgegeben. Die erste Spalte besteht
aus den Strings des Arrays prefix und wird rechtsbündig ausgegeben. Die zweite Spalte ent-
hält die Zahlen des Arrays numbers, die am Dezimalzeichen ausgerichtet werden sollen. Um
dies zu erreichen, berechnet das Programm die Länge des größten Strings der 1. Spalte (fest-
gehalten in prefixlength) sowie mit Hilfe von alignAtDecimal() die x-Verschiebungen für die
Strings der zweiten Spalte.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class StartGUI extends JFrame {


double[] numbers = { 1230.45, 100, 8.1271 };
String[] prefix = {"Kapital : ","Bonus : ","Rendite (%): "};
StringBuffer[] strings = new StringBuffer[numbers.length];

class MyCanvas extends JPanel {

public void paintComponent(Graphics g) {


super.paintComponent(g);

int x = 50;
int y = 50;
FontMetrics fm;

g.setFont(new Font("Times New Roman", Font.PLAIN, 24));


fm = g.getFontMetrics();
int prefixLength = 0;
for (int i = 0; i < prefix.length; ++i)
if (prefixLength < fm.stringWidth(prefix[i]))
prefixLength = fm.stringWidth(prefix[i]);

int[] xOffsets = new int[numbers.length];


strings = MoreMath.alignAtDecimal(numbers,
"#,##0.0######",
fm, xOffsets);
for (int i = 0; i < strings.length; ++i) {
g.drawString(prefix[i], x, y + i*fm.getHeight() );
g.drawString(strings[i].toString(),
x + prefixLength + xOffsets[i],

Listing 12: Fenster mit ausgerichteten Zahlen (aus StartGUI.java)


44 >> Ausgabe in Ein- oder Mehrzahl (Kongruenz)
Zahlen

y + i*fm.getHeight() );
}
}
} // Ende von MyCanvas
...

Listing 12: Fenster mit ausgerichteten Zahlen (aus StartGUI.java) (Forts.)

Die Strings der ersten Spalte werden einfach mit drawString() an der X-Koordinate x gezeich-
net. Die Strings der zweiten Spalte hingegen werden ausgehend von der Koordinate x zuerst
um prefixlength Pixel (um sich nicht mit der ersten Spalte zu überschneiden) und dann noch
einmal um xOffsets[i] Positionen (damit die Dezimalzeichen untereinander zu liegen kom-
men) nach rechts verschoben.

Abbildung 4: Ausrichtung von Zahlenkolonnen bei nichtproportionaler Schrift

11 Ausgabe in Ein- oder Mehrzahl (Kongruenz)


Kongruenz im sprachwissenschaftlichen Sinne ist die formale Übereinstimmung zusammenge-
hörender Satzglieder, ihre wohl bekannteste Form die Übereinstimmung von attributivem
Adjektiv und Beziehungswort in Kasus, Numerus und Genus wie in »das kleine Haus« oder
»dem kleinen Hund«. Es gibt sprachliche Wendungen, in denen selbst Leute mit gutem Sprach-
gefühl nicht gleich sagen können, ob der Kongruenz Genüge getan wurde (wie z.B. in »Wie
wäre es mit einem Keks oder Törtchen?«), doch dies ist ein Thema für ein anderes Buch.
Als Programmierer leiden wir vielmehr unter einer ganz anderen Form der Kongruenz, einer
Kongruenz im Numerus, die dem Redenden oder Schreibenden eigentlich nie Probleme berei-
tet, sondern eben nur dem Programmierer: der Kongruenz zwischen Zahlwort und Bezie-
hungswort.
Angenommen, Sie arbeiten mit einem Online-Bestellsystem für eine Bäckerei und wollen dem
Kunden zum Abschluss anzeigen, wie viele Brote er bestellt hat. Sie lesen die Anzahl der
bestellten Brote aus einer Variablen countBreads und erzeugen folgenden Ausgabestring: "Sie
haben " + countBreads + " Brote bestellt." Hat der Kunde zwei Brote bestellt, erhält er die
Mitteilung:
Sie haben 2 Brote bestellt.
Hat er kein oder ein Brot bestellt, liest er auf seinem Bildschirm:
Sie haben 0 Brote bestellt.
>> Zahlen und Mathematik 45

Zahlen
oder noch schlimmer:
Sie haben 1 Brote bestellt.
Die meisten Programmierer lösen dieses Problem, indem sie das Substantiv in Ein- und Mehr-
zahl angeben – in diesem Fall also Brot(e) – oder die verschiedenen Fälle durch if-Verzwei-
gungen unterscheiden. Darüber hinaus gibt es in Java aber noch eine eigene Klasse, die
speziell für solche (und noch kompliziertere) Fälle gedacht ist: java.text.ChoiceFormat.
ChoiceFormat-Instanzen bilden eine Gruppe von Zahlenbereichen auf eine Gruppe von Strings
ab. Die Zahlenbereiche werden als ein Array von double-Werten definiert. Angegeben wird
jeweils der erste Wert im Zahlenbereich. So definiert das Array
double[] limits = {0, 1, 2};
die Zahlenbereiche
[0, 1)
[1, 2)
[2, ∞)
Werte, die kleiner als der erste Bereich sind, werden diesem zugesprochen.
Als Pendant zum Bereichsarray muss ein String-Array definiert werden, das ebenso viele
Strings enthält, wie es Bereiche gibt (hier also drei):
String[] outputs = {"Brote", "Brot", "Brote"};
Übergibt man beide Arrays einem ChoiceFormat-Konstruktor, erzeugt man eine Abbildung der
Zahlen aus den angegebenen Bereichen auf die Strings:
ChoiceFormat cf = new ChoiceFormat(limits, outputs);
Der Ausgabestring für unsere Online-Bäckerei lautete damit:
"Sie haben " + countBreads + " " + cf.format(countBreads) + " bestellt.\n"
und würde für 0, 1, 2 und 3 folgende Ausgaben erzeugen:
Sie haben 0 Brote bestellt.
Sie haben 1 Brot bestellt.
Sie haben 2 Brote bestellt.
Sie haben 3 Brote bestellt.

Wäre countBreads eine double-Variable, würde obiger Code leider auch Ausgaben wie
Hi nweis

»Sie haben 0.5 Brote bestellt.« oder »Sie haben 1.5 Brot bestellt.« erzeugen. Um dies zu
korrigieren, könnten Sie die Grenzen als {0, 0.5, 1} festlegen, auf {"Brote", "Brot",
"Brote"} abbilden und in der Ausgabe x.5 als "x 1/2" schreiben, also beispielsweise:
»Sie haben 1 1/2 Brote bestellt.«.

Parameter in Ausgabestrings
Leider ist es nicht möglich, das Argument der format()-Methode in den zurückgelieferten
String einzubauen. Dann bräuchte man nämlich statt
countBreads + " " + cf.format(countBreads)
46 >> Umrechnung zwischen Zahlensystemen
Zahlen

nur noch
cf.format(countBreads)
zu schreiben, und was wichtiger wäre: Man könnte in den Ausgabestrings festlegen, ob für
einen Bereich der Zahlenwert ausgegeben soll. Beispielsweise ließe sich dann das unschöne
»Sie haben 0 Brote bestellt.« durch »Sie haben kein Brot bestellt.« ersetzen.
Die Lösung bringt in diesem Fall die Klasse java.text.MessageFormat:
// ChoiceFormat-Objekt erzeugen, das Zahlenwerte Strings zuordnet
double[] limits = {0, 1, 2};
String[] outputs = {"kein Brot", "ein Brot", "{0} Brote"};
ChoiceFormat cf = new ChoiceFormat(limits, outputs);

// MessageFormat-Objekt erzeugen und mit ChoiceFormat-Objekt verbinden


MessageFormat mf = new MessageFormat(" Sie haben {0} bestellt.\n");
mf.setFormatByArgumentIndex(0, cf);

// Ausgabe
Object[] arguments = {new Integer(number)};
System.console().printf("%s \n", mf.format(arguments)); // zur Verwendung von
// System.console()
// siehe Rezept 85
Sie erzeugen das gewünschte ChoiceFormat-Objekt und fügen mit {} nummerierte Platzhalter
in die Strings ein. (Beachten Sie, dass ChoiceFormat den Platzhalter nicht ersetzt, sondern
unverändert zurückliefert. Dies ist aber genau das, was wir wollen, denn das im nächsten
Schritt erzeugte MessageFormat-Objekt, das intern unser ChoiceFormat-Objekt verwendet, sorgt
für die Ersetzung des Platzhalters.)
Dann erzeugen Sie das MessageFormat-Objekt mit dem Ausgabetext. In diesen Text fügen Sie
einen Platzhalter für den Zahlenwert ein. Da der Zahlenwert von dem soeben erzeugten
ChoiceFormat-Objekt verarbeitet werden soll, registrieren Sie Letzteres mit Hilfe der setFormat-
ByArgument()-Methode als Formatierer für den Platzhalter.
Anschließend müssen Sie nur noch die format()-Methode des MessageFormat-Objekts aufrufen
und ihr die zu formatierende Zahl (allerdings in Form eines einelementigen Object-Arrays)
übergeben.

12 Umrechnung zwischen Zahlensystemen


Der Rechner kennt keine Zahlensysteme, er unterscheidet allein zwischen den binären Kodie-
rungen für Integer- und Gleitkommazahlen. Für diese Kodierungen ist die Hardware ausgelegt,
in diesen Kodierungen finden alle Berechnungen statt. Will ein Programm dem Anwender die
Verarbeitung von Zahlen aus einem bestimmten Zahlensystem erlauben, muss es lediglich
dafür sorgen, dass Zahlen aus dem betreffenden Zahlensystem eingelesen und ausgegeben
werden können. Soweit es die Integer-Zahlen betrifft, ist die hierfür benötigte Funktionalität
bereits in den Java-API-Klassen vorhanden.
>> Zahlen und Mathematik 47

Zahlen
Abbildung 5: Mit ChoiceFormat können Sie (unter anderem) Mengen korrekt in
Ein- oder Mehrzahl angeben.

Einlesen Ausgeben
Zehnersystem Andere Systeme Zehnersystem Andere Systeme
Byte.parseByte Integer.parseInt Byte.toString() Integer.toString(int i, int
(String s) (String s, int Byte.toString(byte n) radix)
Short.parseShort radix) Short.toString() Integer.toBinaryString(int i)
(String s) Short.toString(short n) Integer.toOctalString(int i)
Integer.parseInt Integer.toString() Integer.toHexString(int i)
(String s) Integer.toString(int n)
Long.parseLong Long.toString() System.out.printf()
(String s)
Long.toString(long n)

Tabelle 7: Ein- und Ausgabe für Zahlen verschiedener Zahlensysteme

Mit Hilfe dieser Methoden lässt sich auch leicht ein Hilfsprogramm schreiben, mit dem man
Zahlen zwischen den in der Programmierung am weitesten verbreiteten Zahlensystemen (2, 8,
10 und 16) umrechnen kann:

public class Start {

public static void main(String args[]) {

Listing 13: Programm zur Umrechnung zwischen Zahlensystemen


48 >> Umrechnung zwischen Zahlensystemen
Zahlen

System.out.println();

if (args.length != 3) {
System.out.println(" Aufruf: Start <Ganzzahl> "
+ "<Orgin. Basis: 2, 8, 10, 16> "
+ "<Zielbasis: 2, 8, 10, 16>");
System.exit(0);
}

try {
int number = 0;
int srcRadix = Integer.parseInt(args[1]);
int tarRadix = Integer.parseInt(args[2]);

if ( ! (srcRadix == 2 || srcRadix == 8 || srcRadix == 10


|| srcRadix == 16 || tarRadix == 2 || tarRadix == 8
|| tarRadix == 10 || tarRadix == 16) ) {
System.out.println(" Ungueltige Basis");
System.exit(0);
}

number = Integer.parseInt(args[0], srcRadix);

System.out.println(" Basis \t Zahl");


System.out.println(" ----------------------------------");
System.out.println(" " + srcRadix + " \t\t " + args[0]);
System.out.println(" " + tarRadix + " \t\t "
+ Integer.toString(number, tarRadix));
}
catch (NumberFormatException e) {
System.err.println(" Ungueltiges Argument");
}

}
}

Listing 13: Programm zur Umrechnung zwischen Zahlensystemen (Forts.)

Abbildung 6: Zahlensystemrechner; Aufruf mit <Ganzzahl> <Originalbasis> <Zielbasis>


>> Zahlen und Mathematik 49

Zahlen
13 Zahlen aus Strings extrahieren
Manchmal sind die Zahlen, die man verarbeiten möchte, in Strings eingebettet. Beispielsweise
könnte die Kundennummer eines Unternehmens aus einem Buchstabencode, einem Zahlen-
code, einem Geburtsdatum im Format TTMMJJ und einem abschließenden einbuchstabigen
Ländercode bestehen:
KDnr-2345-150474a
Wenn Sie aus einem solchen String die Zahlen herausziehen möchten, können Ihnen die im
Rezept 8 vorgestellten Methoden nicht mehr weiterhelfen.
Gleiches gilt, wenn Sie Zahlen aus einem größeren Text, beispielsweise einer Datei, extrahie-
ren müssen. Eine Möglichkeit, dies zu bewerkstelligen, wäre das Einlesen des Textes mit einem
Scanner-Objekt. Dies geht allerdings nur, wenn der Text so in Tokens zerlegt werden kann,
dass die Zahlen als Tokens verfügbar sind. Außerdem ist diese Lösung, obwohl im Einzelfall
sicher gangbar und auch sinnvoll, per se doch recht unflexibel.
Eine recht praktische und flexible Lösung für beide oben angeführten Aufgabenstellungen ist
dagegen das Extrahieren der Zahlen (oder auch anderer Textpassagen) mittels regulärer Aus-
drücke und Pattern Matching. Zur einfacheren Verwendung definieren wir gleich zwei Metho-
den:
ArrayList<String> getPatternsInString(String s, String p)

ArrayList<String> getNumbersInString(String s)
Die Methode getPatternsInString() übernimmt als erstes Argument den String, der durch-
sucht werden soll, und als zweites Argument den regulären Ausdruck (gegeben als String), mit
dem der erste String durchsucht werden soll. Alle gefundenen Vorkommen von Textpassagen,
die durch den regulären Ausdruck beschrieben werden, werden in einer ArrayList<String>-
Collection zurückgeliefert.
Für das Pattern Matching sind in dem Paket java.util.regex die Klassen Pattern und Matcher
definiert. Die Methode getPatternsInString() »kompiliert« zuerst den String mit dem regulä-
ren Ausdruck p mit Hilfe der statischen Pattern-Methode compile() in ein Pattern-Objekt pat.
Als Nächstes wird ein Matcher benötigt, der den String s unter Verwendung des in pat gespei-
cherten regulären Ausdrucks durchsucht. Dieses Matcher-Objekt liefert die Pattern-Methode
matcher(), der als einziges Argument der zu durchsuchende String übergeben wird. Der Mat-
cher enthält nun alle benötigten Informationen (das Pattern und den zu durchsuchenden
String); die Methoden zum Finden der übereinstimmenden Vorkommen stellt die Klasse Mat-
cher selbst zur Verfügung:
왘 Mit boolean matches() kann man prüfen, ob der gesamte String als ein »Match« für den
regulären Ausdruck angesehen werden kann.
왘 Mit boolean find() kann man den String nach übereinstimmenden Vorkommen (»Mat-
ches«) durchsuchen. Der erste Aufruf sucht ab dem Stringanfang, nachfolgende Aufrufe
setzen die Suche hinter dem letzten gefundenen Vorkommen fort.
왘 Mit int start() und int end() bzw. String group() können Sie sich Anfangs- und End-
position bzw. das komplette zuletzt gefundene Vorkommen zurückliefern lassen.

Da die Methode getPatternsInString() einen String nach allen Vorkommen des übergebenen
Musters durchsuchen soll, ruft sie find() in einer while-Schleife auf und speichert die gefun-
denen Übereinstimmungen in einem ArrayList-Container.
50 >> Zahlen aus Strings extrahieren
Zahlen

import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.ArrayList;

/**
* Pattern in einem Text finden und als ArrayList zurückliefern
*/
public static ArrayList<String> getPatternsInString(String s, String p) {
ArrayList<String> matches = new ArrayList<String>(10);

Pattern pat = Pattern.compile(p);


Matcher m = pat.matcher(s);

while(m.find())
matches.add( m.group() );

return matches;
}

Listing 14: Strings nach beliebigen Patterns durchsuchen

Die zweite Methode, getNumbersInString(), liefert im String gefundene Zahlen zurück. Dank
getPatternsInString() fällt die Implementierung von getNumbersInString() nicht mehr son-
derlich schwer: Die Methode ruft einfach getPatternsInString() mit einem regulären Aus-
druck auf, der Zahlen beschreibt:

/**
* Zahlen in einem Text finden und als ArrayList zurückliefern
*/
public static ArrayList<String> getNumbersInString(String s) {
return getPatternsInString(s, "[+-]?\\d+(\\,\\d+)?");
}

Listing 15: Strings nach Zahlen durchsuchen

Der reguläre Ausdruck von getNumbersInString() setzt Zahlen aus drei Teilen zusammen:
einem optionalen Vorzeichen ([+-]?), einer mindestens einelementigen Folge von Ziffern
(\\d+) und optional einem dritten Teil, der mit Komma eingeleitet wird und mit einer mindes-
tens einelementigen Folge von Ziffern ((\\,\\d+)?) endet. Dieser reguläre Ausdruck findet
ganze Zahlen, Gleitkommazahlen mit Komma zum Abtrennen der Nachkommastellen und
ohne Tausenderzeichen, Zahlen mit und ohne Vorzeichen, aber auch Zahlenschrott (beispiels-
weise Teile englisch formatierter Zahlen, die das Komma als Tausenderzeichen verwenden).
Wenn Sie Zahlenschrott ausschließen, gezielt nach englischen Gleitkommazahlen, beliebig
formatierten Zahlen, Hexadezimalzahlen oder irgendwelchen sonstigen Textmustern suchen
wollen, können Sie nach dem Muster von getNumbersInString() eine eigene Methode definie-
ren oder getPatternsInString() aufrufen und den passenden regulären Ausdruck als Argu-
ment übergeben.
>> Zahlen und Mathematik 51

Zahlen
System.out.println("\n Suche nach (deutschen) Zahlen: \n");

ArrayList<String> numbers = MoreMath.getNumbersInString(str);


for (String n : numbers)
System.out.println(n);

System.out.println("\n Suche nach Zahlen und ähnlichen Passagen: \n");

numbers = MoreMath.getPatternsInString(str, "[+-]?[\\d.,]+\\d+|\\d+");


for (String n : numbers)
System.out.println(n);

System.out.println("\n Suche in Kundennummer: \n");

numbers = MoreMath.getPatternsInString("KDnr-2345-150474a", "\\d+");


for (String n : numbers)
System.out.println(n);

Listing 16: Aus Start.java

14 Zufallszahlen erzeugen
Zur Erzeugung von Zufallszahlen gibt es in der Java-Klassenbibliothek den »Zufallszahlenge-
nerator« java.util.Random. Die Arbeit mit dieser Klasse sieht so aus, dass Sie zuerst ein Objekt
der Klasse erzeugen und sich dann durch Aufrufe der entsprechenden next-Methoden der
Klasse Random die gewünschten Zufallswerte zurückliefern lassen.
import java.util.Random;

Random generator = new Random();

double d = generator.nextDouble(); // liefert Wert zwischen [0.0, 1.0)


boolean b = generator.nextBoolean(); // liefert true oder false
long l = generator.nextLong(); // liefert zufälligen long-Wert
int i = generator.nextInt(); // liefert zufälligen int-Wert
i = generator.nextInt(10); // liefert Wert zwischen [0, 1)

// Fünf ganzzahlige Zufallszahlen zwischen 0 und 100 ausgeben


for(int i = 0; i < 5; ++i)
System.out.println(generator.nextInt(100));
Wenn Sie an double-Zufallszahlen aus dem Bereich 0.0 bis 1.0 interessiert sind, brauchen Sie
Random nicht selbst zu instanzieren, sondern können direkt die Math-Methode random() aufrufen:
double d = Math.random();

Die Erzeugung von Zufallszahlen mit Hilfe von Computern ist im Grunde gar nicht zu
H i n we i s

realisieren. Dies liegt daran, dass die Zahlen letzten Endes nicht zufällig gezogen wer-
den, sondern von einem mathematischen Algorithmus errechnet werden. Ein solcher
Algorithmus bildet eine Zahlenfolge, die sich zwangsweise irgendwann wiederholt.
Allerdings sind die Algorithmen, die man zur Erzeugung von Zufallszahlen in Pro-
grammen verwendet, so leistungsfähig, dass man von der Periodizität der erzeugten
Zahlenfolge nichts merkt.
52 >> Zufallszahlen erzeugen
Zahlen

Gaußverteilte Zufallszahlen
Die oben aufgeführten next-Methoden sind so implementiert, dass sie alle Zahlen aus ihrem
Wertebereich mit gleicher Wahrscheinlichkeit zurückliefern. In Natur und Technik hat man es
dagegen häufig mit Größen zu tun, die normalverteilt sind.

Normalverteilung
E x k ur s

Normalverteilte Größen zeichnen sich dadurch aus, dass die möglichen Werte um einen
mittleren Erwartungswert streuen. Der Erwartungswert ist am häufigsten vertreten, die
Wahrscheinlichkeit für andere Werte nimmt kontinuierlich ab, je mehr die Werte vom
Erwartungswert abweichen.

Häufigkeit

s s (Standardabweichung)

x (Mittelwert)
Wert

Abbildung 7: Gaußsche Normalverteilung; die Standardabweichung ist ein Maß dafür,


wie schnell die Wahrscheinlichkeit der Werte bei zunehmender
Abweichung vom Mittelwert abnimmt.

Eine Firma, die Schrauben herstellt, könnte beispielsweise Software für eine Messanlage in
Auftrag geben, die sicherstellen soll, dass die Durchmesser der produzierten Schrauben nor-
malverteilt sind und die Standardabweichung unterhalb eines vorgegebenen Qualitätslimits
liegt. Die zum Testen einer solchen Software benötigten normalverteilten Zufallszahlen liefert
die Random-Methode nextGaussian():
Random generator = new Random();

double d = generator.nextGaussian();
Der Erwartungswert der zurückgelieferten Zufallszahlen ist 0, die Standardabweichung 1.
Durch nachträgliche Skalierung und Addition eines Offsets können beliebige normalverteilte
Zahlen erzeugt werden.

Zufallszahlen zum Testen von Anwendungen


Beim Testen von Anwendungen, die mit Zufallszahlen arbeiten, ergibt sich das Problem, dass
die Anwendung bei jedem neuen Start mit anderen Zufallszahlen arbeitet und daher unter-
schiedliche Ergebnisse produziert. Das Aufspüren von Fehlern ist unter solchen Bedingungen
>> Zahlen und Mathematik 53

Zahlen
natürlich sehr schwierig. (Gleiches gilt, wenn Sie Zufallszahlen als Eingaben zum Testen der
Anwendung benutzen.)
Aus diesem Grunde lassen sich Zufallsgeneratoren in der Regel so einstellen, dass sie auch
reproduzierbare Folgen von Zufallszahlen erzeugen können. Zur Einstellung dient der so
genannte Seed. Jeder Seed erzeugt genau eine vordefinierte Folge von Zufallszahlen. Wenn
Sie also dem Random-Konstruktor einen festen Seed-Wert vorgeben:
Random generator = new Random(3);
erzeugt der zugehörige Zufallsgenerator bei jedem Start der Anwendung die gleiche Folge von
Zufallszahlen und Sie können die Anwendung mit reproduzierbaren Ergebnissen testen.

Wenn Sie den Random-Konstruktor ohne Argument aufrufen (siehe vorangehende


H i n we i s

Abschnitte), wählt er den Seed unter Berücksichtigung der aktuellen Zeit. So wird
gewährleistet, dass bei jedem Aufruf ein individueller Seed und damit eine individuelle
Zahlenfolge erzeugt wird.

15 Ganzzahlige Zufallszahlen aus einem bestimmten


Bereich
Mit Hilfe der Methode Random.nextInt(int n) können Sie sich eine zufällige Integer-Zahl aus
dem Bereich von 0 bis n (exklusive) zurückliefern lassen.
Wenn Sie Integer-Zahlen aus einem beliebigen Bereich benötigen, müssen Sie entweder die
Anzahl Zahlen im Bereich selbst berechnen, als Argument an nextInt() übergeben und zu der
zurückgelieferten Zufallszahl die erste Zahl im gewünschten Bereich hinzuaddieren ...
... oder Sie definieren sich nach dem Muster von Math.random() eine eigene Methode random-
Int(int min, int max), der Sie nur noch die gewünschten Bereichsgrenzen übergeben müssen:

import java.util.Random;

public class MoreMath {


...
// private Instanz des verwendeten Zufallsgenerators
private static Random randomNumberGenerator;

// nur einen Zufallsgenerator verwenden


private static synchronized void initRNG() {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random();
}

/**
* Zufallszahl aus vorgegebenem Bereich zurückliefern
*/
public static int randomInt(int min, int max) {
if (randomNumberGenerator == null)
initRNG();

Listing 17: Ganzzahlige Zufallszahlen aus einem definierten Bereich


54 >> Mehrere, nicht gleiche Zufallszahlen erzeugen (Lottozahlen)
Zahlen

int number = randomNumberGenerator.nextInt( max+1-min );

return min + number;


}
}

Listing 17: Ganzzahlige Zufallszahlen aus einem definierten Bereich (Forts.)

Zwei Dinge sind zu beachten:


왘 Die Methode liefert eine Zahl aus dem Bereich [min, max] zurück, im Gegensatz zu Random.
nextInt(n), das eine Zahl aus [0, n) zurückliefert, ist die Obergrenze also in den Werte-
bereich mit eingeschlossen.
왘 Die Methode muss sicherstellen, dass sie nur beim ersten Aufruf einen Random-Zufallsgene-
rator erzeugt, der bei nachfolgenden Aufrufen verwendet wird. Zu diesem Zweck wurde
für den Generator ein privates statisches Feld definiert. Eine einfache if-Bedingung prüft,
ob der Generator erzeugt werden muss oder schon vorhanden ist.
Ein wenig umständlich erscheint die Auslagerung der if-Abfrage in eine eigene private
Methode, doch dies erlaubt es, die Methode als synchronized zu deklarieren und die Erzeu-
gung des Zufallszahlengenerators so threadsicher zu machen.

16 Mehrere, nicht gleiche Zufallszahlen erzeugen


(Lottozahlen)
Wenn Sie sich von der Methode nextInt(int n) Zufallszahlen aus dem Wertebereich [0, n)
zurückliefern lassen, kann es schnell passieren, dass Sie die eine oder andere Zahl mehrfach
erhalten. Je kleiner der Wertebereich, umso größer die Wahrscheinlichkeit, dass dies passiert.
Sofern Sie also an einmaligen Zufallszahlen, wie sie beispielsweise zur Simulation einer Lotto-
ziehung benötigt werden, interessiert sind, müssen Sie die Dubletten herausfiltern. Eine beson-
ders elegante Möglichkeit dafür dies zu tun, bietet die Collection-Klasse TreeSet. Deren add()-
Methode fügt neue Elemente nämlich nur dann ein, wenn diese noch nicht im TreeSet-Con-
tainer enthalten sind.
Die folgende Methode erzeugt einen TreeSet-Container für size Integer-Zahlen und füllt die-
sen mit Werten aus dem Bereich [min, max].

import java.util.Random;
import java.util.TreeSet;

/**
* Erzeugt size nicht gleiche Zufallszahlen aus Wertebereich von
* min bis max
*/
public static TreeSet<Integer> uniqueRandoms(int size, int min, int max) {
TreeSet<Integer> numbers = new TreeSet<Integer>();
Random generator = new Random();
int n;

Listing 18: Methode zur Erzeugung einmaliger Zufallszahlen


>> Zahlen und Mathematik 55

Zahlen
if (size > max+1-min)
throw new IllegalArgumentException("Gibt nicht genügend " +
"eindeutige Zahlen im Bereich!");

if (size == max+1-min) {
for(int i= min; i <= max; ++i)
numbers.add(i);

} else {
while(numbers.size() != size) {
n = min + generator.nextInt(max+1 - min);

// Zahl einfügen, falls nicht schon vorhanden


numbers.add(n);
}
}

return numbers;
}

Listing 18: Methode zur Erzeugung einmaliger Zufallszahlen (Forts.)

Wenn es in dem spezifizierten Wertebereich nicht genügend Zahlen gibt, um den Container
ohne Dubletten zu füllen, wird eine IllegalArgumentException ausgeworfen.
Wenn der spezifizierte Wertebereich gerade genau so viele Zahlen enthält, wie Zahlen in den
Container eingefügt werden sollen, werden die Zahlen mit Hilfe einer for-Schleife in den Con-
tainer eingefügt. (In diesem Fall kann eigentlich nicht mehr von Zufallszahlen die Rede sein.)
Ist der Wertebereich größer als die gewünschte Anzahl Zufallszahlen, werden die Zahlen
zufällig gezogen, bis der Container die gewünschte Anzahl Elemente enthält. Beachten Sie,
dass wir uns hier nicht die Mühe machen, den Rückgabewert der add()-Methode zu überprü-
fen (true oder false), da die Bedingung der while-Schleife bereits sicherstellt, dass die Zie-
hung nicht vorzeitig beendet wird.
Das folgende Programm zeigt, wie mit Hilfe von uniqueRandoms() die Ziehung der Lottozahlen
(6 aus 49) simuliert werden kann.

import java.util.TreeSet;

public class Start {

public static void main(String args[]) {


System.out.println();

System.out.println("Willkommen zur Ziehung der Lottozahlen!");

TreeSet<Integer> randomNumbers = MoreMath.uniqueRandoms(6, 1, 49);

Listing 19: Lottozahlen


56 >> Trigonometrische Funktionen
Zahlen

for(int elem : randomNumbers)


System.out.println(" " + elem);
}
}

Listing 19: Lottozahlen (Forts.)

Wenn Sie selbst Lotto spielen, bauen Sie das Programm und die Methode uniqueRan-
Tipp

doms() doch so aus, dass häufig getippte Zahlenkombinationen (siehe Fachliteratur zu


Spielsystemen) aussortiert werden. Viel mehr dürfte hinter den Spielsystemen kommer-
zieller Anbieter auch nicht stecken.

17 Trigonometrische Funktionen
Bei Verwendung der trigonometrischen Methoden ist zu beachten, dass diese Methoden als
Parameter stets Werte in Bogenmaß (Radiant) erwarten. Beim Bogenmaß wird der Winkel
nicht in Grad, sondern als Länge des Bogens angegeben, den der Winkel aus dem Einheitskreis
(Gesamtumfang 2 π) ausschneidet: 1 rad = 360º/2 π; 1º = 2 π/360 rad.
360 Grad entsprechen also genau 2 π, 180 Grad entsprechen 1 π, 90 Grad entsprechen 1/2 π.
Wenn Sie ausrechnen wollen, was 32 Grad in Radiant sind, multiplizieren Sie einfach die
Winkelangabe mit 2 * π und teilen Sie das Ganze durch 360 (oder multiplizieren Sie mit π und
teilen Sie durch 180).
bogenlaenge = Math.PI/180 * grad;
Math stellt zur bequemen Umrechnung von Grad in Radiant und umgekehrt die Methoden
toDegrees() und toRadians() zur Verfügung. Beachten Sie aber, dass diese Umrechnung nicht
immer exakt ist. Gehen Sie also beispielsweise nicht davon aus, dass sin(toRadians(180.0))
exakt 0.0 ergibt. (Siehe auch Rezept 6 zum Vergleichen mit definierter Genauigkeit.)

18 Temperaturwerte umrechnen (Celsius <-> Fahrenheit)


Während die Wissenschaft und die meisten Völker dieser Welt die Temperatur mittlerweile in
Grad Celsius messen, ist in den USA immer noch die Einheit Fahrenheit gebräuchlich. Die For-
mel zur Umrechnung von Fahrenheit in Celsius lautet:
c = ( f − 32) * 5 / 9
Aus dieser Formel lassen sich schnell zwei praktische Methoden zur Umrechnung von Fahren-
heit in Celsius und umgekehrt ableiten:
/**
* Umrechnung von Fahrenheit in Celsius
*/
public static double fahrenheit2Celsius(double temp) {
return (temp - 32) * 5.0/9.0;
}

/**
* Umrechnung von Celsius in Fahrenheit
*/
>> Zahlen und Mathematik 57

Zahlen
public static double celsius2fahrenheit(double temp) {
return (temp * 9 / 5.0) + 32;
}

Die Mathematik unterscheidet nicht zwischen 5/9 und 5.0/9.0 – wohl aber der Compi-
A cht un g

ler, der im ersten Fall eine Ganzzahlendivision durchführt, d.h. den Nachkommaanteil
unterschlägt.

Mit dem Programm Start.java zu diesem Rezept können Sie beliebige Temperaturwerte
umrechnen. Geben Sie einfach in der Kommandozeile die Ausgangseinheit (-f für Fahrenheit
oder -c für Celsius) und den umzurechnenden Temperaturwert an.

Abbildung 8: Programm zur Umrechnung zwischen Fahrenheit und Celsius

19 Fakultät berechnen
Mathematisch ist die Fakultät definiert als:
n! = 1, wenn n = 0

n! = 1 * 2 * 3 ... * (n-1) * n, für n = 1, ..


oder rekursiv formuliert:
fac(0) = 1;

fac(n) = n * fac(n-1);
Die Fakultät ist vor allem für die Berechnung von Wahrscheinlichkeiten wichtig. Wenn Sie
beispielsweise sieben Kugeln, nummeriert von 1 bis 7, in einen Behälter geben und dann
nacheinander ziehen, gibt es 7! Möglichkeiten (Permutationen), die Kugeln zu ziehen.

/**
* Fakultät berechnen

Listing 20: Methode zur Berechnung der Fakultät


58 >> Fakultät berechnen
Zahlen

*/
public static double factorial(int n) {
double fac = 1;

if (n < 0)
throw new IllegalArgumentException("Fakultaet ist nur fuer "
+ "positive Zahlen definiert");

if (n < 2)
return fac;

while(n > 1) {
fac *= n;
--n;
}

return fac;
}

Listing 20: Methode zur Berechnung der Fakultät (Forts.)

Die Fakultät ist eine extrem schnell ansteigende Funktion. Bereits für relativ kleine
A c h tun g.

Eingaben wie die Zahl 10 ergibt sich ein sehr hoher Wert (10! = 3.628.800) und 171!
liegt schon außerhalb des Wertebereichs von double!

Mit dem Start-Programm zu diesem Rezept können Sie sich die Fakultäten von 0 bis n ausge-
ben lassen. Übergeben Sie n beim Aufruf in der Konsole und denken Sie daran, dass Sie ab 171
nur noch infinity-Ausgaben ernten.

Abbildung 9: Fakultät
>> Zahlen und Mathematik 59

Zahlen
20 Mittelwert berechnen
Wenn wir den Mittelwert oder Durchschnitt einer Folge von Zahlen berechnen, bilden wir
üblicherweise die Summe der einzelnen Werte und dividieren diese durch die Anzahl der
Werte. In der Mathematik bezeichnet man dies als das arithmetische Mittel und stellt es weite-
ren Mittelwerten gegenüber.

Mittelwert Berechnung
arithmetischer x1 + x2 + ... + xn
x=
n
geometrischer
x = n x1 ⋅ x2 ⋅ ... ⋅ xn

harmonischer n
x=
1 1 1
+ + ... +
x1 x2 xn

quadratischer
x=
1 2
n
(
x1 + x22 + ... + xn2 )

Tabelle 8: Mittelwerte

Die folgenden Methoden zur Berechnung der verschiedenen Mittelwerte wurden durchweg mit
einem double...-Parameter definiert. Als Argument kann den Methoden daher ein double-
Array oder eine beliebig lange Folge von double-Werten übergeben werden.
/**
* Arithmetisches Mittel (Standard für Mittelwertberechnungen)
*/
public static double arithMean(double... values) {
double sum = 0;

for (double d : values)


sum += d;

return sum/values.length;
}

/**
* Geometrisches Mittel
*/
public static double geomMean(double... values) {
double sum = 1;

for (double d : values)


sum *= d;

return Math.pow(sum, 1.0/values.length);


}
60 >> Zinseszins berechnen
Zahlen

/**
* Harmonisches Mittel
*/
public static double harmonMean(double... values) {
double sum = 0;

for (double d : values)


sum += 1.0/d;

return values.length / sum;


}

/**
* Quadratisches Mittel
*/
public static double squareMean(double... values) {
double sum = 0;

for (double d : values)


sum += d*d;

return Math.sqrt(sum/values.length);
}
Mögliche Aufrufe wären:
double[] values = {1, 5, 12.5, 0.5, 3};
MoreMath.arithMean(values);
oder
MoreMath.geomMean(1, 5, 12.5, 0.5, 3)

Die Methode geomMean() liefert NaN zurück, wenn die Summe der Werte negativ ist
Achtung

(wegen Ziehen der n-ten Wurzel).

21 Zinseszins berechnen
Die Grundformel zur Zinseszinsrechnung lautet:

K n = K 0 ⋅ (1 + i )
n

wobei n die Laufzeit in Jahren und i den Jahreszinssatz (p/100) bezeichnet. Kn ist das Endka-
pital, das man erhält, wenn man das Startkapital K0 für n Jahre (oder allgemein Zinsperioden)
zu einem Zinssatz i verzinsen lässt.
Kommen monatliche Raten dazu, erweitert sich die Formel zu:

K n = K 0 ⋅ (1 + i ) + R ⋅
n (1 + i )n − 1
(1 + i )1/12 − 1
>> Zahlen und Mathematik 61

Zahlen
In der Finanzwelt wird aber meist mit der folgenden Variante für vorschüssige Renten gerech-
net:

K n = K 0 ⋅ (1 + i ) + R ⋅
n (1 + i )n − 1 ⋅ (1 + i )1/ 12
(1 + i )1 /12 − 1
Die Methode capitalWithCompoundInterest() berechnet nach obiger Formel das Endkapital
nach n Jahren monatlicher Ratenzahlung und Zinseszinsverzinsung. Als Argumente über-
nimmt die Methode das Startkapital, das 0 sein kann, die Höhe der Raten (installment), den
Zins in Prozent (interest), der nicht 0 sein darf, und die Laufzeit (term).
/**
* Kapitalentwicklung bei monatlicher Ratenzahlung und Zinseszins
*/
public static double capitalWithCompoundInterest(double startCapital,
double installment,
double interest,
int term) {

if(interest == 0.0)
throw new IllegalArgumentException("Zins darf nicht Null sein");

double interestRate = interest/100.0;


double accumulationFactor = 1 + interestRate;
double endCapital = startCapital * Math.pow(accumulationFactor , term)
+ installment * (Math.pow(accumulationFactor , term) - 1)
/ (Math.pow(accumulationFactor ,1/12.0) - 1)
* Math.pow(accumulationFactor , 1/12.0);

return endCapital;
}
Die Höhe der reinen Einzahlungen berechnet paidInCapital():
/**
* Berechnung des eingezahlten Kapitals
*/
public static double paidInCapital(double startCapital,
double installment,
int term) {
double endCapital = startCapital;

for (int n = 1; n <= term; ++n)


endCapital = endCapital + 12*installment;

return endCapital ;
}
Das Start-Programm zu diesem Rezept nutzt obige Methoden zur Implementierung eines Zins-
rechners. Startkapital, monatliche Raten, Verzinsung in Prozent und Laufzeit in Jahren werden
über JTextField-Komponenten abgefragt. Nach Drücken des BERECHNEN-Schalters wird die
jährliche Kapitalentwicklung berechnet und in der JTextArea-Komponente links angezeigt.
62 >> Komplexe Zahlen
Zahlen

Abbildung 10: Zinsrechner

22 Komplexe Zahlen
Komplexe Zahlen gehören zwar nicht unbedingt zum täglichen Handwerkszeug eines Pro-
grammierers, bilden aber ein wichtiges Teilgebiet der Algebra und finden als solches immer
wieder Eingang in die Programmierung, so zum Beispiel bei der Berechnung von Fraktalen.
Komplexe Zahlen haben die Form
z = x + iy
wobei x als Realteil, y als Imaginärteil und i als die imaginäre Einheit bezeichnet wird (mit
i2 = -1). Vereinfacht werden Zahlen oft als Paare aus Real- und Imaginärteil geschrieben:
(x, y).

imaginäre Achse

4 + 2i

1 reelle Achse

Abbildung 11: Komplexe Zahlen in Koordinatendarstellung

Grafisch werden komplexe Zahlen in einem Koordinatensystem dargestellt (Gaußsche Zahlen-


ebene, siehe Abbildung 11).
Statt als Paar aus Real- und Imaginärteil können komplexe Zahlen daher auch als Kombina-
tion aus Radius und Winkel zwischen der Verbindungslinie zum Koordinatenursprung und der
positiven reellen x-Achse angegeben werden (Polarkoordinaten). Der Radius wird dabei übli-
cherweise als Betrag, der Winkel als Argument bezeichnet.
>> Zahlen und Mathematik 63

Zahlen
Rechnen mit komplexen Zahlen

Operation Beschreibung
Betrag Der Betrag einer komplexen Zahl ist die Quadratwurzel aus der
Summe der Komponentenquadrate.
z = x2 + y2

Addition Komplexe Zahlen werden addiert, indem man die Realteile und Ima-
ginärteile addiert.
(x, y) + (x', y') = (x + x', y + y')
Subtraktion Komplexe Zahlen werden subtrahiert, indem man die Realteile und
Imaginärteile voneinander subtrahiert.
(x, y) – (x', y') = (x – x', y – y')
Die Subtraktion entspricht der Addition der Negativen (-z = -x -yi)
Vervielfachung Vervielfachung ist die Multiplikation mit einer reellen Zahl.
3 * (x, y) = (3*x. 3*y)
Multiplikation Die Multiplikation zweier komplexer Zahlen ist gegeben durch:
(x, y) * (x', y') = (xx'-yy', xy' + yx')
Division Die Division z/z' ist gleich der Multiplikation mit der Inversen z*z-1.
Die Inverse einer komplexen Zahl ist definiert als:
x y
z −1 = 2 − i
x + y2 x2 + y2

Tabelle 9: Rechenoperationen für komplexe Zahlen

Die Klasse Complex


Die Klasse Complex definiert neben verschiedenen Konstruktoren Methoden für die Berechnung
von Betrag, Negativer, Konjugierter und Inverser sowie Methoden für die Grundrechenarten
Addition, Subtraktion, Vervielfachung und Multiplikation. Die Division kann durch Multipli-
kation mit der Inversen berechnet werden. Die Methoden für die Grundrechenarten sind durch
statische Versionen überladen. Die nichtstatischen Methoden verändern das aktuelle Objekt,
die statischen Methoden liefern das Ergebnis der Operation als neues Complex-Objekt zurück.
Zur Unterstützung der Polarkoordinatendarstellung gibt es einen Konstruktor, der eine kom-
plexe Zahl aus Radius (Betrag) und Winkel (Argument) berechnet, sowie Get-Methoden, die
Radius und Winkel eines gegebenen Complex-Objekts zurückliefern.
64 >> Komplexe Zahlen
Zahlen

Methode Beschreibung
Complex() Konstruktoren.
Complex(double real, double imag) Der Standardkonstruktor erzeugt eine komplexe Zahl,
Complex(double r, double phi, byte polar) deren Real- und Imaginärteil 0.0 ist.
Der zweite Konstruktor erzeugt eine komplexe Zahl mit
den übergebenen Werten für Real- und Imaginärteil.
Der dritte Konstruktor rechnet die übergebenen Werte
für Radius und Winkel in Real- und Imaginärteil um
und erzeugt das zugehörige Complex-Objekt. Um diesen
Konstruktor von dem zweiten Konstruktor unterschei-
den zu können, ist ein drittes Argument notwendig,
dem Sie einfach die Konstante Complex.POLAR überge-
ben.
double getReal() Liefert den Realteil der aktuellen komplexen Zahl
zurück.
void setReal(double real) Weist dem Realteil der aktuellen komplexen Zahl einen
Wert zu.
double getImag() Liefert den Imaginärteil der aktuellen komplexen Zahl
zurück.
void setImag(double real) Weist dem Imaginärteil der aktuellen komplexen Zahl
einen Wert zu.
double getR () Liefert den Radius (Betrag) der aktuellen komplexen
Zahl zurück. (Polarkoodinatendarstellung)
double getPhi () Liefert den Winkel (Argument) der aktuellen komple-
xen Zahl zurück. (Polarkoodinatendarstellung)
void add(Complex a) Addiert die übergebene komplexe Zahl zur aktuellen
public static Complex add(Complex a, komplexen Zahl.
Complex b) Die statische Version addiert die beiden übergebenen
komplexen Zahlen und liefert das Ergebnis zurück.
void add(double s) Addiert die übergebene reelle Zahl zur aktuellen kom-
public static Complex add(Complex a, plexen Zahl.
double s) Die statische Version addiert die reelle Zahl s zur über-
gebenen komplexen Zahl a und liefert das Ergebnis
zurück.
void subtract(Complex a) Subtrahiert die übergebene komplexe Zahl von der
public static Complex subtract(Complex a, aktuellen komplexen Zahl.
Complex b) Die statische Version subtrahiert die zweite übergebene
komplexe Zahl von der ersten und liefert das Ergebnis
zurück.
void subtract(double s) Subtrahiert die übergebene reelle Zahl von der aktuel-
public static Complex subtract(Complex a, len komplexen Zahl.
double s) Die statische Version subtrahiert die reelle Zahl s von
der übergebenen komplexen Zahl a und liefert das
Ergebnis zurück.

Tabelle 10: Methoden der Klasse Complex


>> Zahlen und Mathematik 65

Zahlen
Methode Beschreibung
void times(double s) Multipliziert die aktuelle komplexe Zahl mit der über-
public static Complex times(Complex a, gebenen reellen Zahl s.
double s) Die statische Version multipliziert die übergebene kom-
plexe Zahl a mit der reellen Zahl s und liefert das
Ergebnis zurück.
void multiply(Complex a) Multipliziert die aktuelle komplexe Zahl mit der über-
public static Complex multiply(Complex a, gebenen komplexen Zahl.
Complex b) Die statische Version multipliziert die beiden übergebe-
nen komplexen Zahlen und liefert das Ergebnis zurück.
void negate() Negiert die aktuelle komplexe Zahl (-x, -yi).
double abs() Liefert den Betrag der komplexen Zahl zurück.
Complex conjugate() Liefert die konjugiert komplexe Zahl (x, -y) zur
aktuellen komplexen Zahl zurück.
Complex inverse() Liefert die Inverse zur aktuellen komplexen Zahl
zurück.
Object clone() Erzeugt eine Kopie der aktuellen komplexen Zahl.
Zur Überschreibung der clone()-Methode siehe auch
Rezept 250.
boolean equals(Object obj) Liefert true zurück, wenn das übergebene Objekt vom
static boolean equals(Complex a, Complex b, Typ Complex ist und Real- und Imaginärteil die gleichen
double eps) Werte wie die aktuelle komplexe Zahl besitzen. Zur
Überschreibung der equals()-Methode siehe auch
Rezept 252.
Die statische Version erlaubt für den Vergleich die
Angabe einer Genauigkeit eps. Die Real- bzw. Imagi-
närteile der beiden komplexen Zahlen werden dann als
»gleich« angesehen, wenn ihre Differenz kleiner eps ist.
int hashCode() Liefert einen Hashcode für die aktuelle komplexe Zahl
zurück.
String toString() Liefert eine String-Darstellung der komplexen Zahl
zurück:
x + yi

Tabelle 10: Methoden der Klasse Complex (Forts.)

/**
* Klasse für komplexe Zahlen
*/
public class Complex implements Cloneable {
public final static byte POLAR = 1;

private double real = 0.0; // Realteil


private double imag = 0.0; // Imaginärteil

/*** Konstruktoren ***/

Listing 21: Complex.java


66 >> Komplexe Zahlen
Zahlen

public Complex() {
this.real = 0.0;
this.imag = 0.0;
}
public Complex(double real, double imag) {
this.real = real;
this.imag = imag;
}
public Complex(double r, double phi, byte polar) {
this.real = r * Math.cos(phi);
this.imag = r * Math.sin(phi);
}

/*** Get- und Set-Methoden ***/

public double getReal() {


return real;
}
public void setReal(double real) {
this.real = real;
}

public double getImag() {


return imag;
}
public void setImag(double imag) {
this.imag = imag;
}

public double getR() {


return this.abs();
}
public double getPhi() {
return Math.atan2(this.imag, this.real);
}

/*** Rechenoperationen ***/

// Addition this += a
public void add(Complex a ) {
this.real += a.real;
this.imag += a.imag;
}
// Addition c = a + b
public static Complex add(Complex a, Complex b) {
Complex c = new Complex();
c.real = a.real + b.real;
c.imag = a.imag + b.imag;

Listing 21: Complex.java (Forts.)


>> Zahlen und Mathematik 67

Zahlen
return c;
}

// Addition einer Gleitkommazahl


public void add(double s) {
this.real += s;
}
// Addition einer Gleitkommazahl
public static Complex add(Complex a, double s) {
Complex c = new Complex();
c.real = a.real + s;
c.imag = a.imag;
return c;
}

// Subtraktion this -= a
public void subtract(Complex a ) {
this.real -= a.real;
this.imag -= a.imag;
}
// Subtraktion c = a - b
public static Complex subtract(Complex a, Complex b) {
Complex c = new Complex();
c.real = a.real - b.real;
c.imag = a.imag - b.imag;
return c;
}

// Subtraktion einer Gleitkommazahl


public void subtract(double s) {
this.real -= s;
}
// Subtraktion einer Gleitkommazahl
public static Complex subtract(Complex a, double s) {
Complex c = new Complex();
c.real = a.real - s;
c.imag = a.imag;
return c;
}

// Vervielfachung durch Multiplikation mit Gleitkommazahl


public void times(double s) {
this.real *= s;
this.imag *= s;
}
public static Complex times(Complex a, double s){
double r, i;

r = a.real * s;
i = a.imag * s;
return new Complex(r, i);

Listing 21: Complex.java (Forts.)


68 >> Komplexe Zahlen
Zahlen

// Multiplikation this *= b
public void multiply(Complex b){
double r, i;

r = (this.real * b.real) - (this.imag * b.imag);


i = (this.real * b.imag) + (this.imag * b.real);

this.real = r;
this.imag = i;
}

// Multiplikation c = a * b
public static Complex multiply(Complex a, Complex b){
double r, i;

r = (a.real * b.real) - (a.imag * b.imag);


i = (a.real * b.imag) + (a.imag * b.real);
return new Complex(r, i);
}

/*** Sonstige Operationen ***/

// Negation
public void negate() {
this.real *= -1;
this.imag *= -1;
}

// Betrag
public double abs() {
return Math.sqrt(real*real + imag*imag);
}

// Konjugierte
public Complex conjugate() {
return new Complex(this.real, -this.imag);
}

// Inverse
public Complex inverse() {
double r, i;

r = this.real / ((this.real * this.real) + (this.imag * this.imag));


i = -this.imag / ((this.real * this.real) + (this.imag * this.imag));

return new Complex(r, i);

Listing 21: Complex.java (Forts.)


>> Zahlen und Mathematik 69

Zahlen
}

/*** Überschriebene Object-Methoden ***/

public Object clone() {


try {
Complex c = (Complex) super.clone();
c.real = this.real;
c.imag = this.imag;
return c;
} catch (CloneNotSupportedException e) {
// sollte nicht vorkommen
throw new InternalError();
}
}

public boolean equals(Object obj) {


if (obj instanceof Complex) {
Complex tmp = (Complex) obj;
// wenn beide NaN, dann als gleich ansehen
if ( (Double.isNaN(this.real) || Double.isNaN(this.imag))
&& (Double.isNaN(tmp.real) || Double.isNaN(tmp.imag)) )
return true;

if ( (this.real == tmp.real) && (this.imag == tmp.imag) )


return true;
else
return false;
}
return false;
}

public static boolean equals(Complex a, Complex b, double eps) {


if (a.equals(b))
return true;
else {
if( (Math.abs(a.real - b.real) < eps)
&&(Math.abs(a.imag - b.imag) < eps) )
return true;
else
return false;
}
}

public int hashCode() {


long bits = Double.doubleToLongBits(this.real);
bits ^= Double.doubleToLongBits(this.imag) * 31;
return (((int) bits) ^ ((int) (bits >> 32)));
}

public String toString() {

Listing 21: Complex.java (Forts.)


70 >> Komplexe Zahlen
Zahlen

String sign = " + ";


if(this.imag < 0.0)
sign = " - ";

java.text.DecimalFormat df = new java.text.DecimalFormat("#,##0.##");


String str_rt = df.format(this.real);
String str_it = df.format(Math.abs(this.imag));

return str_rt + sign + str_it + "i";


}
}

Listing 21: Complex.java (Forts.)

Das Programm aus Listing 22 demonstriert den Einsatz der Klasse Complex anhand der Berech-
nung einer Julia-Menge. Die Berechnung der Julia-Menge erfolgt der Einfachheit halber direkt
in paintComponent(), auch wenn dies gegen den Grundsatz verstößt, in Ereignisbehandlungs-
code zeitaufwendige Berechnungen durchzuführen. Die Folge ist, dass die Benutzerschnitt-
stelle für die Dauer der Julia-Mengen-Berechnung lahm gelegt wird, was uns hier aber nicht
weiter stören soll. (Korrekt wäre die Auslagerung der Berechnung in einen eigenen Thread,
siehe Kategorie »Threads«.)

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Start extends JFrame {

class MyCanvas extends JPanel {

public void paintComponent(Graphics g) {


super.paintComponent(g);

Complex c = new Complex(-0.012, 0.74);

for(int i = 0; i < getWidth(); ++i)


for(int j = 0; j < getHeight(); ++j) {
Complex x = new Complex(0.0001*i, 0.0001*j);
for(int n = 0; n < 100; ++n) {
if (x.abs() > 100.0)
break;
x.multiply(x);
x.add(c);
}
if (x.abs() < 1.0) {
g.setColor(new Color(0, 0, 255));
g.fillRect(i, j, 1, 1);
} else {
g.setColor(new Color((int)x.abs()%250, 255, 255));

Listing 22: Fraktalberechnung mit Hilfe komplexer Zahlen


>> Zahlen und Mathematik 71

Zahlen
g.fillRect(i, j, 1, 1);
}
}
}
}

public Start() {
setTitle("Julia-Menge");

getContentPane().add(new MyCanvas(), BorderLayout.CENTER);

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

public static void main(String args[]) {


// Fenster erzeugen und anzeigen
Start mw = new Start();
mw.setSize(500,350);
mw.setResizable(false);
mw.setLocation(200,300);
mw.setVisible(true);
}

Listing 22: Fraktalberechnung mit Hilfe komplexer Zahlen (Forts.)

Abbildung 12: Julia-Menge


72 >> Vektoren
Zahlen

23 Vektoren
Vektoren finden in der Programmierung vielfache Anwendung – beispielsweise zur Repräsen-
tation von Koordinaten, für Berechnungen mit gerichteten, physikalischen Größen wie
Geschwindigkeit oder Beschleunigung und natürlich im Bereich der dreidimensionalen Com-
putergrafik. So können – um ein einfaches Beispiel zu geben – Punkte in der Ebene P(5; 12)
oder im Raum (5; 12; -1) als zwei- bzw. dreidimensionale Ortsvektoren (d.h. mit Beginn im
Ursprung des Koordinatensystems) repräsentiert werden.

⎛5⎞
⎛5⎞ ⎜ ⎟
p = ⎜⎜ ⎟⎟, bzw. p = ⎜ 12 ⎟
⎝12 ⎠ ⎜ − 1⎟
⎝ ⎠

Der Abstand zwischen zwei Punkten P und Q ist dann gleich der Länge des Vektors, der vom
einen Punkt zum anderen führt.

PQ = q − p

Rechnen mit Vektoren

Operation Beschreibung
Länge Die Länge (oder der Betrag) eines Vektors ist die Quadratwurzel aus
der Summe der Komponentenquadrate. (Für zweidimensionale Vekto-
ren lässt sich dies leicht aus dem Satz des Pythagoras ableiten.)

v = (1*1) + (3 * 3) , für v = (1; 3)

Addition Vektoren werden addiert, indem man ihre einzelnen Komponenten


addiert.

⎛1⎞ ⎛ 5⎞ ⎛1 + 5 ⎞ ⎛ 6⎞
⎜⎜ ⎟⎟ + ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟
⎝ 3⎠ ⎝ 0 ⎠ ⎝ 3 + 0 ⎠ ⎝ 3 ⎠

Das Ergebnis ist ein Vektor, der vom Anfang des ersten Vektors zum
Ende des zweiten Vektors weist.
Subtraktion Vektoren werden subtrahiert, indem man ihre einzelnen Komponen-
ten subtrahiert.
⎛1⎞ ⎛ 5 ⎞ ⎛ 1 − 5 ⎞ ⎛ − 4 ⎞
⎜⎜ 3 ⎟⎟ − ⎜⎜ 0 ⎟⎟ = ⎜⎜ 3 − 0 ⎟⎟ = ⎜⎜ 3 ⎟⎟
⎝ ⎠ ⎝ ⎠ ⎝ ⎠ ⎝ ⎠
Für zwei Ortsvektoren p und q erhält man den Vektor, der von P nach
Q führt, indem man p von q subtrahiert.
Vervielfachung Vervielfachung ist die Multiplikation mit einem skalaren Faktor.
⎛ 10 ⎞ ⎛ 2 *10 ⎞ ⎛ 20 ⎞
2 ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟
⎝ − 7 ⎠ ⎝ 2 * −7 ⎠ ⎝ − 14 ⎠
Durch die Vervielfachung wird lediglich die Länge, nicht die Richtung
des Vektors verändert. Einer »Division« entspricht die Multiplikation
mit einem Faktor zwischen 0 und 1.

Tabelle 11: Vektoroperationen


>> Zahlen und Mathematik 73

Zahlen
Operation Beschreibung
Skalarprodukt Das Skalarprodukt (englisch »dot product«) ist das Produkt aus den
Längen (Beträgen) zweier Vektoren multipliziert mit dem Kosinus des
Winkels zwischen den Vektoren.

v ⋅ w = v w cos α

Für zwei- und dreidimensionale Vektoren kann es als die Summe der
Komponentenprodukte berechnet werden:

⎛1⎞ ⎛ 5 ⎞
⎜⎜ ⎟⎟ ⋅ ⎜⎜ ⎟⎟ = 1* 5 + 3 * 0 = 5
⎝ 3⎠ ⎝ 0 ⎠
Das Skalarprodukt ist ein skalarer Wert.
Stehen die beiden Vektoren senkrecht zueinander, ist das Skalar-
produkt gleich null.
Vektorprodukt Das Vektorprodukt (englisch »cross product«) ist das Produkt aus den
Längen (Beträgen) zweier Vektoren multipliziert mit dem Sinus des
Winkels zwischen den Vektoren.

v × w = v w sin α

Für dreidimensionale Vektoren kann es wie folgt aus den Komponen-


ten berechnet werden:

⎛ α 1 ⎞ ⎛ β1 ⎞ ⎛ α 2 β 3 − α 3 β 2 ⎞
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜ α 2 ⎟ × ⎜ β 2 ⎟ = ⎜ α 3 β1 − α 1 β 3 ⎟
⎜α ⎟ ⎜ β ⎟ ⎜ α β − α β ⎟
⎝ 3⎠ ⎝ 3⎠ ⎝ 1 2 2 1 ⎠

Das Vektorprodukt zweier Vektoren v und w ist ein Vektor, der senk-
recht zu v und w steht. Die drei Vektoren bilden ein Rechtssystem
(Drei-Finger-Regel). Der Betrag des Vektorprodukts ist gleich dem
Flächeninhalt des von v und w aufgespannten Parallelogramms.
In der 3D-Grafikprogrammierung kann das Vektorprodukt zur
Berechnung von Oberflächennormalen verwendet werden.

Tabelle 11: Vektoroperationen (Forts.)

Die Klasse Vector3D


Die Klasse Vector3D ist für die Programmierung mit dreidimensionalen Vektoren ausgelegt.
Vektoren können als Objekte der Klasse erzeugt und bearbeitet werden. Für die Grundrechen-
arten (Addition, Subtraktion und Vervielfachung) gibt es zudem statische Methoden, die das
Ergebnis der Operation als neuen Vektor zurückliefern. In Anwendungen, die nicht übermäßig
zeitkritisch sind, kann man die Klasse auch für zweidimensionale Vektoren verwenden, indem
man die dritte Dimension (Feld z) auf null setzt. (Achtung! Das Vektorprodukt liefert stets
einen Vektor, der zur Ebene der Ausgangsvektoren senkrecht steht.) Tabelle 12 stellt Ihnen die
Methoden der Klasse vor.
74 >> Vektoren
Zahlen

Methode Beschreibung
Vector3D() Konstruktoren.
Vector3D(double x, double y, double z) Der Standardkonstruktor erzeugt einen Vektor,
dessen x,y,z-Felder auf 0.0 gesetzt sind.
Der zweite Konstruktor weist den Feldern die
übergebenen Werte zu.
void add(Vector3D v) Addiert den übergebenen Vektor zum aktuellen
static Vector3D add(Vector3D v1, Vector3D v2) Vektor.
Die statische Version addiert die beiden über-
gebenen Vektoren und liefert das Ergebnis als
neuen Vektor zurück.
double angle(Vector3D v) Berechnet den Winkel zwischen dem aktuellen
und dem übergebenen Vektor.
Der Winkel wird in Bogenmaß zurückgeliefert
(und kann beispielsweise mit Math.toDegrees()
in Grad umgerechnet werden).
Object clone() Erzeugt eine Kopie des aktuellen Vektors.
Zur Überschreibung der clone()-Methode siehe
auch Rezept 250.
Vector3D crossProduct(Vector3D v) Berechnet das Vektorprodukt aus dem aktuel-
len und dem übergebenen Vektor.
double dotProduct(Vector3D v) Berechnet das Skalarprodukt aus dem aktuel-
len und dem übergebenen Vektor.
boolean equals(Object obj) Liefert true zurück, wenn das übergebene
Objekt vom Typ Vector3D ist und die Felder x, y
und z die gleichen Werte wie im aktuellen Vek-
tor haben.
Zur Überschreibung der equals()-Methode
siehe auch Rezept 252.
double length() Berechnet die Länge des Vektors.
void scale(double s) Skaliert den aktuellen Vektor um den Faktor s.
static Vector3D scale(Vector3D v, double s) Skaliert den übergebenen Vektor um den Fak-
tor s und liefert das Ergebnis als neuen Vektor
zurück.
void subtract(Vector3D v) Subtrahiert den übergebenen Vektor vom aktu-
static Vector3D subtract(Vector3D v1, Vector3D v2) ellen Vektor.
Die statische Version subtrahiert den zweiten
vom ersten Vektor und liefert das Ergebnis als
neuen Vektor zurück.
String toString() Liefert eine String-Darstellung des Vektors
zurück:
(x; y; z)

Tabelle 12: Methoden der Klasse Vector3D


>> Zahlen und Mathematik 75

Zahlen
/**
* Klasse für dreidimensionale Vektoren
*
*/
public class Vector3D implements Cloneable {
public double x;
public double y;
public double z;

// Konstruktoren
public Vector3D() {
x = 0;
y = 0;
z = 0;
}
public Vector3D(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}

// Addition
public void add(Vector3D v) {
x += v.x;
y += v.y;
z += v.z;
}
public static Vector3D add(Vector3D v1, Vector3D v2) {
return new Vector3D(v1.x+v2.x, v1.y+v2.y, v1.z+v2.z);
}

// Subtraktion
public void subtract(Vector3D v) {
x -= v.x;
y -= v.y;
z -= v.z;
}
public static Vector3D subtract(Vector3D v1, Vector3D v2) {
return new Vector3D(v1.x-v2.x, v1.y-v2.y, v1.z-v2.z);
}

// Skalierung (Multiplikation mit Skalar)


public void scale(double s) {
x *= s;
y *= s;
z *= s;
}
public static Vector3D scale(Vector3D v, double s) {
return new Vector3D(v.x*s, v.y*s, v.z*s);
}

Listing 23: Vector3D.java


76 >> Vektoren
Zahlen

// Skalarprodukt
public double dotProduct(Vector3D v) {
return x*v.x + y*v.y + z*v.z;
}

// Vektorprodukt
public Vector3D crossProduct(Vector3D v) {
return new Vector3D(y*v.z - z*v.y,
z*v.x - x*v.z,
x*v.y - y*v.x);
}

// Winkel zwischen Vektoren ( arccos(Skalarprodukt/(LängeV1 * LängeV2)) )


public double angle(Vector3D v) {
return Math.acos( (x*v.x + y*v.y + z*v.z) /
Math.sqrt( (x*x + y*y + z*z) *
(v.x*v.x + v.y*v.y + v.z*v.z)));
}

// Länge
public double length() {
return Math.sqrt(x*x + y*y + z*z);
}

// Umwandlung in String
public String toString() {
return "(" + x + "; " + y + "; " + z + ")";
}

// Kopieren
public Object clone() {
try {
Vector3D v = (Vector3D) super.clone();
v.x = x;
v.y = y;
v.z = z;
return v;
} catch (CloneNotSupportedException e) {
// sollte nicht vorkommen
throw new InternalError();
}
}

// Vergleichen
public boolean equals(Object obj) {
if (obj instanceof Vector3D) {
if ( x == ((Vector3D) obj).x
&& y == ((Vector3D) obj).y
&& z == ((Vector3D) obj).z)
return true;
}

Listing 23: Vector3D.java (Forts.)


>> Zahlen und Mathematik 77

Zahlen
return false;
}
}

Listing 23: Vector3D.java (Forts.)

Das Programm aus Listing Listing 24: demonstriert den Einsatz der Klasse Vector3D anhand
eines geometrischen Problems. Mittels Vektoren wird ausgehend von den Punktkoordinaten
eines Dreiecks der Flächeninhalt berechnet.

public class Start {

public static void main(String args[]) {


System.out.println();

System.out.println(" Flaecheninhalt eines Dreiecks berechnen");


System.out.println();

System.out.println(" Gegeben: Dreieck zwischen Punkten: ");


System.out.println("\t A (2; 3; 0)");
System.out.println("\t B (2.5; 5; 0)");
System.out.println("\t C (7; 4; 0)");

// Punktvektoren
Vector3D a = new Vector3D(2, 3, 0);
Vector3D b = new Vector3D(2.5, 5, 0);
Vector3D c = new Vector3D(7, 4, 0);

// Kantenvektoren
Vector3D ab = Vector3D.subtract(b, a);
Vector3D ac = Vector3D.subtract(c, a);

double cross = (ab.crossProduct(ac)).length();

double area = 0.5 * cross;

// Für MoreMath.rint siehe Rezept 5


System.out.println("\n Berechnete Flaeche: " +
MoreMath.rint(area, 2));
}
}

Listing 24: Testprogramm: Berechnung eines Flächeninhalts mit Vektoren


78 >> Matrizen
Zahlen

Abbildung 13: Ausgabe des Testprogramms

24 Matrizen
In der Mathematik ist eine Matrix ein rechteckiges Zahlenschema. Als (m,n)-Matrix oder
Matrix der Ordnung m × n bezeichnet man eine Anordnung aus m Zeilen und n Spalten:

⎛α α 12 α 13 ⎞
A = ⎜⎜ 11 ⎟ (Beispiel für eine (2, 3)-Matrix)
α
⎝ 21 α 22 α 23 ⎟⎠
Matrizen können lineare Abbildungen repräsentieren (eine (m,n)-Matrix entspricht einer line-
aren Abbildung vom Vektorraum Vn nach Vm) oder auch lineare Gleichungssysteme. In der
Programmierung werden Matrizen vor allem zur Lösung linearer Gleichungssysteme sowie für
Vektortransformationen in 3D-Grafikanwendungen eingesetzt.

Rechnen mit Matrizen

Operation Beschreibung
Addition Matrizen werden addiert, indem man ihre einzelnen Komponenten
addiert.
⎛ α 11 α12 ⎞ ⎛ β11 β12 ⎞ ⎛ α11 + β11 α 12 + β12 ⎞
⎜⎜ ⎟⎟ + ⎜⎜ ⎟=⎜ ⎟
⎝ α 21 α 22 ⎠ ⎝ β 21 β 22 ⎟⎠ ⎜⎝ α 21 + β 21 α 22 + β 22 ⎟⎠
Zwei Matrizen, die addiert werden, müssen der gleichen Ordnung
angehören.
Subtraktion Matrizen werden subtrahiert, indem man ihre einzelnen Komponen-
ten subtrahiert.
⎛ α 11 α 12 ⎞ ⎛ β11 β12 ⎞ ⎛ α 11 − β11 α 12 − β12 ⎞
⎜⎜ ⎟⎟ − ⎜⎜ ⎟=⎜ ⎟
⎝ α 21 α 22 ⎠ ⎝ β 21 β 22 ⎟⎠ ⎜⎝ α 21 − β 21 α 22 − β 22 ⎟⎠
Zwei Matrizen, die subtrahiert werden, müssen der gleichen Ordnung
angehören.
Vervielfachung Vervielfachung ist die Multiplikation mit einem skalaren Faktor.
⎛α α 12 ⎞ ⎛ kα11 kα 12 ⎞
k ⎜⎜ 11 ⎟⎟ = ⎜⎜ ⎟
⎝ 21 α 22 ⎠ ⎝ kα 21
α kα 22 ⎟⎠

Tabelle 13: Matrixoperationen


>> Zahlen und Mathematik 79

Zahlen
Operation Beschreibung
Multiplikation Bei der Matrizenmultiplikation C = A*B ergeben sich die Elemente der
Ergebnismatrix C durch Aufsummierung der Produkte aus den Ele-
menten einer Zeile von A mit den Elementen einer Spalte von B:
Spalten von A
cij = ∑a
k =1
ik kjb

Eine Multiplikation ist nur möglich, wenn die Anzahl von Spalten
von A gleich der Anzahl Zeilen von B ist. Das Ergebnis aus der Multi-
plikation einer (m,n)-Matrix A mit einer (n,r)-Matrix B ist eine (m,r)-
Matrix.
⎛ α 11 α 12 ⎞ ⎛ β11 ⎞ ⎛ (α 11 * β11 ) + (α 12 * β 21 ) ⎞
⎜⎜ ⎟⎟ * ⎜⎜ ⎟⎟ = ⎜⎜ ⎟⎟
⎝ α 21 α 22 ⎠ ⎝ β 21 ⎠ ⎝ (α 21 * β11 ) + (α 22 * β 21 ) ⎠

Tabelle 13: Matrixoperationen (Forts.)

Die Klasse Matrix


Die Klasse Matrix ist für die Programmierung mit Matrizen beliebiger Ordnung ausgelegt. Sie
unterstützt neben den Grundrechenarten auch die Berechnung der Transponierten, der Inver-
tierten und der Determinanten.
Ist die Matrix quadratisch und repräsentiert sie ein lineares Gleichungssystem, können Sie die-
ses mit der Methode solve() lösen. Zur Lösung des Gleichungssystems wie auch zur Berech-
nung der Inversen und der Determinanten wird intern eine LR-Zerlegung der Ausgangsmatrix
berechnet, die durch ein Objekt der Hilfsklasse LUMatrix repräsentiert wird. Die LR-Zerlegung
liefert die Methode luDecomp(), die auch direkt aufgerufen werden kann.
Eine spezielle Unterstützung für Vektortransformationen, wie sie für 3D-Grafikanwendungen
benötigt werden, bietet die Klasse nicht. Die grundlegenden Operationen, von der Addition
von Transformationen über die Anwendung auf Vektoren durch Matrizenmultiplikation bis
hin zur Berechnung der Inversen, um Transformationen rückgängig machen zu können, sind
zwar allesamt mit der Klasse durchführbar, dürften aber für die meisten Anwendungen zu viel
Laufzeit beanspruchen. (Für professionelle Grafikanwendungen sollten Sie auf eine Implemen-
tierung zurückgreifen, die für (4,4)-Matrizen optimiert ist, siehe beispielsweise Java 3D.)

Methode Beschreibung
Matrix(int m, int n) Konstruktoren.
Matrix(int m, int n, double s) Erzeugt wird jeweils eine (m,n)-Matrix (n Zeilen, m
Matrix(int m, int n, double[][] elems) Spalten). Die Elemente der Matrix werden je nach Kon-
struktor mit 0.0, mit s oder mit den Werten aus dem
zweidimensionalen Array elems initialisiert.
Negative Zeilen- oder Spaltendimensionen führen zur
Auslösung einer NegativeArraySizeException.
Wird zur Initialisierung ein Array übergeben, müssen
dessen Dimensionen mit m und n übereinstimmen.
Ansonsten wird eine IllegalArgumentException ausge-
löst.

Tabelle 14: Methoden der Klasse Matrix


80 >> Matrizen
Zahlen

Methode Beschreibung
void add(Matrix B) Addiert die übergebene Matrix zur aktuellen Matrix.
static Matrix add(Matrix A, Matrix B) Die statische Version addiert die beiden übergebenen
Matrizen und liefert das Ergebnis als neue Matrix
zurück.
Gehören die Matrizen unterschiedlichen (n,m)-Ordnun-
gen an, wird eine IllegalArgumentException ausgelöst.
Object clone() Erzeugt eine Kopie der Matrix.
Zur Überschreibung der clone()-Methode siehe auch
Rezept 250.
boolean equals(Object obj) Liefert true zurück, wenn das übergebene Objekt vom
Typ Matrix ist und die Elemente die gleichen Werte wie
die Elemente der aktuellen Matrix haben.
Zur Überschreibung der equals()-Methode siehe auch
Rezept 252.
double det() Liefert die Determinante der aktuellen Matrix zurück.
Für nichtquadratische oder singuläre Matrizen wird eine
IllegalArgumentException ausgelöst.
double get(int i, int j) Liefert den Wert des Elements in Zeile i, Spalte j zurück.
double[][] getArray() Liefert die Elemente der Matrix als zweidimensionales
Array zurück.
int getColumnDim() Liefert die Anzahl der Spalten (n).
static Matrix getIdentity(int n, int m) Erzeugt eine Identitätsmatrix mit m Zeilen und n Spal-
ten.
In einer Identitätsmatrix haben alle Diagonalelemente
den Wert 1.0, während die restlichen Elemente gleich
null sind.
Bei der 3D-Grafikprogrammierung kann die Identität als
Ausgangspunkt zur Erzeugung von Translations- und
Skalierungsmatrizen verwendet werden.
int getRowDim() Liefert die Anzahl der Zeilen (m).
Matrix inverse() Liefert die Inverse der aktuellen Matrix zurück. Existiert
die Inverse, gilt
A*A-1 = I
Wenn die aktuelle Matrix nicht quadratisch oder singu-
lär ist, wird eine IllegalArgumentException ausgelöst.

Tabelle 14: Methoden der Klasse Matrix (Forts.)


>> Zahlen und Mathematik 81

Zahlen
Methode Beschreibung
LUMatrix luDecomp() Liefert die LR-Zerlegung der aktuellen Matrix als Objekt
der Hilfsklasse LUMatrix zurück. Die Zerlegung, die der
Konstruktor von LUMatrix vornimmt, erfolgt nach dem
Verfahren von Crout.
Die LUMatrix hat den folgenden Aufbau:

⎛ u11 u12 u13 ... ⎞


⎜ ⎟
⎜l u 22 u 23 ... ⎟
LU = ⎜ 21
l31 l32 u33 ... ⎟
⎜ ⎟
⎜ ... ... ... ... ⎟⎠

Die u(i,j)-Elemente bilden die obere Dreiecksmatrix U,
die l(i,j)-Elemente die untere Dreiecksmatrix L. Zur L-
Matrix gehören zudem noch die l(i,i)-Elemente, die alle
1 sind und daher nicht extra in LU abgespeichert sind.
Das Produkt aus L*U liefert nicht direkt die Ausgangs-
matrix A, sondern eine Permutation von A. Die Permu-
tationen sind in den privaten Feldern von LUMatrix
gespeichert und werden bei der Rückwärtssubstitution
(LUMatrix.luBacksolve()) berücksichtigt.
Für nichtquadratische oder singuläre Matrizen wird eine
IllegalArgumentException ausgelöst.
Matrix multiply(Matrix B) Multipliziert die aktuelle Matrix mit der übergebenen
Matrix und liefert das Ergebnis als neue Matrix zurück:
Res = Akt * B
Wenn die Spaltendimension der aktuellen Matrix nicht
gleich der Zeilendimension der übergebenen Matrix ist,
wird eine IllegalArgumentException ausgelöst.
void print() Gibt die Matrix auf die Konsole (System.out) aus (vor-
nehmlich zum Debuggen und Testen gedacht).
void set(int i, int j, double s) Weist dem Element in Zeile i, Spalte j den Wert s zu.
double[] solve(double[] bvec) Löst das Gleichungssystem, dessen Koeffizienten durch
die aktuelle (quadratische) Matrix repräsentiert werden,
für den Vektor B (gegeben als Argument bvec). Das
zurückgelieferte Array ist der Lösungsvektor X, so dass
gilt:
A*X = B
Wenn die aktuelle Matrix nicht quadratisch oder singu-
lär ist, wird eine IllegalArgumentException ausgelöst.
(Siehe auch Rezept 25)
void subtract(Matrix B) Subtrahiert die übergebene Matrix von der aktuellen
static Matrix subtract(Matrix A, Matrix B) Matrix.
Die statische Version subtrahiert die zweite von der ers-
ten Matrix und liefert das Ergebnis als neue Matrix
zurück.
Gehören die Matrizen unterschiedlichen (m,n)-Ordnun-
gen an, wird eine IllegalArgumentException ausgelöst.

Tabelle 14: Methoden der Klasse Matrix (Forts.)


82 >> Matrizen
Zahlen

Methode Beschreibung
void times(double s) Multipliziert die Elemente der aktuellen bzw. der über-
static Matrix times(Matrix A, double s) gebenen Matrix mit dem Faktor s.
void transpose() Transponiert die Matrix bzw. liefert die Transponierte
static Matrix transpose(Matrix A) zur übergebenen Matrix zurück.
Die Transponierte ergibt sich durch Spiegelung der Ele-
mente an der Diagonalen, sprich durch paarweise Ver-
tauschung der Elemente a(ij) mit a(ji).
Wenn die zu transponierende Matrix nicht quadratisch
ist, wird eine RuntimeException bzw. IllegalArgumentEx-
ception ausgelöst.

Tabelle 14: Methoden der Klasse Matrix (Forts.)

/**
* Klasse für Matrizen
*
* @author Dirk Louis
*/
import java.text.DecimalFormat;

class Matrix implements Cloneable {

private double[][] elems = null; // Zum Speichern der Elemente


private int m; // Zeilen-Dimension
private int n; // Spalten-Dimension

// Leere Matrix (mit 0.0 gefüllt)


public Matrix(int m, int n) {
this.m = m;
this.n = n;
elems = new double[m][n];
}

// Konstante Matrix (mit s gefüllt)


public Matrix(int m, int n, double s) {
this.m = m;
this.n = n;
elems = new double[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
elems[i][j] = s;
}

// Matrix (mit Werten aus zweidimensionalem Array gefüllt)


public Matrix(int m, int n, double[][] elems) {
// Dimension des Arrays mit m und n vergleichen
if (m != elems.length) // Anzahl Zeilen gleich m?
throw new IllegalArgumentException("Fehler in Zeilendimension");

Listing 25: Matrix.java


>> Zahlen und Mathematik 83

Zahlen
for (int i = 0; i < m; ++i) // für alle Zeilen die Anzahl Spalten
if (n != elems[i].length) // gleich m?
throw new IllegalArgumentException("Fehler in Spaltendimension");

this.m = m;
this.n = n;
this.elems = elems;
}

// Addition THIS = THIS + B


public void add(Matrix B) {
if (this.m != B.m || this.n != B.n)
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

double[][] addElems = B.getArray();


for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
elems[i][j] += addElems[i][j];
}

// Addition C = A + B
public static Matrix add(Matrix A, Matrix B) {
int m = A.getRowDim();
int n = A.getColumnDim();

if (m != B.getRowDim() || n != B.getColumnDim() )
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

double[][] newElems = new double[m][n];


double[][] elemsA = A.getArray();
double[][] elemsB = B.getArray();

for (int i = 0; i < m; i++)


for (int j = 0; j < n; j++)
newElems[i][j] = elemsA[i][j] + elemsB[i][j];

return new Matrix(m, n, newElems);


}

// Subtraktion THIS = THIS - B


public void subtract(Matrix B) {
if (this.m != B.m || this.n != B.n)
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

double[][] subtractElems = B.getArray();


for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
elems[i][j] -= subtractElems[i][j];
}

Listing 25: Matrix.java (Forts.)


84 >> Matrizen
Zahlen

// Subtraktion C = A - B
public static Matrix subtract(Matrix A, Matrix B) {
int m = A.getRowDim();
int n = A.getColumnDim();

if (m != B.getRowDim() || n != B.getColumnDim() )
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

double[][] newElems = new double[m][n];


double[][] elemsA = A.getArray();
double[][] elemsB = B.getArray();

for (int i = 0; i < m; i++)


for (int j = 0; j < n; j++)
newElems[i][j] = elemsA[i][j] - elemsB[i][j];

return new Matrix(m, n, newElems);


}

// Vervielfachung THIS = THIS * s


public void times(double s) {
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
elems[i][j] *= s;
}

// Vervielfachung C = B * s
public static Matrix times(Matrix B, double s) {
int m = B.getRowDim();
int n = B.getColumnDim();
double[][] newElems = new double[m][n];
double[][] elemsB = B.getArray();

for (int i = 0; i < m; i++)


for (int j = 0; j < n; j++)
newElems[i][j] = elemsB[i][j] * s;

return new Matrix(m, n, newElems);


}

// Multiplikation C = THIS * B
public Matrix multiply(Matrix B) {
if (this.n != B.getRowDim())
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

int m = this.getRowDim();
int n = B.getColumnDim();
double[][] newElems = new double[m][n];
double[][] elemsB = B.getArray();
double sum = 0;

Listing 25: Matrix.java (Forts.)


>> Zahlen und Mathematik 85

Zahlen
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sum = 0;
for (int k = 0; k < this.getColumnDim(); k++)
sum += this.elems[i][k] * elemsB[k][j];
newElems[i][j] = sum;
}
}

return new Matrix(m, n, newElems);


}

// Transponieren THIS = THIS^T [a(ij) -> a(ji)]


public void transpose() {
if (this.n != this.m)
throw new RuntimeException("Matrix-Dimensionen passen nicht.");

double[][] transelems = new double[m][n];


for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
transelems[j][i] = this.elems[i][j];

this.elems = transelems;
}

// Transponieren C = THIS^T [a(ij) -> c(ji)]


public static Matrix transpose(Matrix A) {
int m = A.getRowDim();
int n = A.getColumnDim();

if (m != n)
throw new IllegalArgumentException("Matrix-Dim. passen nicht.");

double[][] transelems = new double[m][n];


double[][] elemsA = A.getArray();
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
transelems[j][i] = elemsA[i][j];

return new Matrix(n, m, transelems);


}

// Identitätsmatrix C = mxn-I
public static Matrix getIdentity(int m, int n) {
double[][] idelems = new double[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
idelems[i][j] = (i == j ? 1.0 : 0.0);

return new Matrix(m, n, idelems);

Listing 25: Matrix.java (Forts.)


86 >> Matrizen
Zahlen

// LR-Zerlegung
public LUMatrix luDecomp() {
return new LUMatrix(this);
}

// Gleichungssystem lösen
public double[] solve(double[] bvec) {
LUMatrix LU = luDecomp();
double[] xvec = LU.luBacksolve(bvec);

return xvec;
}

// Inverse THIS^-1, so dass THIS*THIS^-1 = I


public Matrix inverse() {
double[] col = new double[n];
double[] xvec = new double[n];
double[][] invElems = new double[n][n];

LUMatrix LU = luDecomp();

for (int j=0; j < n; j++) {


for(int i=0; i< n; i++)
col[i] = 0.0;

col[j] = 1.0;
xvec = LU.luBacksolve(col);

for(int i=0; i< n; i++)


invElems[i][j] = xvec[i];
}

return new Matrix(n, n, invElems);


}

// Determinante
public double det() {

LUMatrix LU = luDecomp();

double d = LU.getD();

for (int i=0; i < n; i++)


d *= LU.elems[i][i];

Listing 25: Matrix.java (Forts.)


>> Zahlen und Mathematik 87

Zahlen
return d;
}

// Get/Set-Methoden
public double[][] getArray() {
return elems;
}

public int getRowDim() {


return m;
}
public int getColumnDim() {
return n;
}

public double get(int i, int j) {


return elems[i][j];
}
public void set(int i, int j, double s) {
if( (i >= 0 && i < m) && (j >= 0 && j < n) )
elems[i][j] = s;
}

// Kopieren
public Object clone() {
try {
Matrix B = (Matrix) super.clone();
B.elems = new double[m][n];
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
B.elems[i][j] = this.elems[i][j];

return B;
} catch (CloneNotSupportedException e) {
// sollte nicht vorkommen
throw new InternalError();
}
}

// Vergleichen
public boolean equals(Object obj) {
if (obj instanceof Matrix) {
Matrix B = (Matrix) obj;
if (B.getRowDim() == m && B.getColumnDim() == n) {
for (int i = 0; i < m; i++)
for (int j = 0; j < n; j++)
if(B.get(i, j) != this.elems[i][j])
return false;
return true;
}

Listing 25: Matrix.java (Forts.)


88 >> Matrizen
Zahlen

return false;
}
return false;
}

// Ausgeben auf Konsole


public void print() {
DecimalFormat df = new DecimalFormat("#,##0.###");
int maxLength = 0;

for (int i = 0; i < m; ++i)


for (int j = 0; j < n; ++j)
maxLength = (maxLength >= df.format(elems[i][j]).length())
? maxLength : df.format(elems[i][j]).length();

for (int i = 0; i < m; ++i) {


System.out.print (" [ ");
for (int j = 0; j < n; ++j)
System.out.printf(MoreString.strpad(df.format(elems[i][j]),
maxLength) + " " );
System.out.println ("]");
}
}
}

Listing 25: Matrix.java (Forts.)

Das Programm aus Listing 26 demonstriert die Programmierung mit Objekten der Klasse
Matrix anhand einer Matrixmultiplikation und der Berechnung einer Inversen.

public class Start {

public static void main(String args[]) {

System.out.println("\n /*** Matrixmultiplikation ***/ \n");

// Matrizen aus Arrays erzeugen


double[][] elemsA = { { 2, 4, -3},
{ 1, 0, 6} };
Matrix A = new Matrix(2, 3, elemsA);

double[][] elemsB = { {1},


{2},
{6} };
Matrix B = new Matrix(3, 1, elemsB);

A.print();
System.out.println("\n multipliziert mit \n");
B.print();
System.out.println("\n ergibt: \n");

Listing 26: Rechnen mit Matrizen


>> Zahlen und Mathematik 89

Zahlen
// Matrizen multiplizieren
Matrix C = A.multiply(B);

C.print();

System.out.println("\n\n /*** Inverse ***/ \n");

System.out.println(" Inverse von: \n");

double[][] elemsD = { { 1, 0 },
{ 1, 2} };
Matrix D = new Matrix(2, 2, elemsD);
D.print();

System.out.println("\n ist: \n");

// Inverse berechnen
C = D.inverse();
C.print();

System.out.println();
}
}

Listing 26: Rechnen mit Matrizen (Forts.)

Abbildung 14: Matrixmultiplikation und Berechnung der Inversen


90 >> Gleichungssysteme lösen
Zahlen

25 Gleichungssysteme lösen
Mit Hilfe der Methode solve() der im vorangehenden Rezept beschriebenen Klasse Matrix
können Sie lineare Gleichungssysteme lösen, die sich durch quadratische Matrizen repräsen-
tieren lassen. Intern wird dabei ein Objekt der Hilfsklasse LUMatrix erzeugt, welches die LU-
Zerlegung der aktuellen Matrix repräsentiert. Die Klasse selbst ist hier nicht abgedruckt, steht
aber – wie alle anderen zu diesem Buch gehörenden Klassen – als Quelltext auf der Buch-CD
zur Verfügung. Der Code der Klasse ist ausführlich kommentiert.
Das Programm aus Listing 26 demonstriert, wie mit Hilfe der Klasse Matrix ein lineares Glei-
chungssystem gelöst werden kann.

public class Start {

public static void main(String args[]) {


System.out.println();

System.out.println(" /*** Lineare Gleichungssysteme ***/ ");


System.out.println("\n");

System.out.println(" Gesucht werden x, y und z, sodass:\n");


System.out.println("\t x + 5y - z = 1");
System.out.println("\t -3x + y - z = 1");
System.out.println("\t 3x + y + z = -3");
System.out.println("\n");

// Koeffizientenmatrix erzeugen
double[][] elems = { { 1, 5, -1},
{ -3, 1, -1},
{ 3, 1, 1} };
Matrix A = new Matrix(3, 3, elems);

double[] bvec = { 1, 1, -3 };

System.out.println("\n Koeffizientenmatrix: ");


A.print();
System.out.println();

// Gleichungssystem lösen
double[] loesung = A.solve(bvec);

System.out.println("\n Gefundene Loesung: ");


System.out.println(" x = " + loesung[0]);
System.out.println(" y = " + loesung[1]);
System.out.println(" z = " + loesung[2]);

System.out.println();
}
}

Listing 27: Testprogramm: Lösung eines linearen Gleichungssystems


>> Zahlen und Mathematik 91

Zahlen
Abbildung 15: Lösung eines linearen Gleichungssystems durch Zerlegung der
Koeffizientenmatrix

26 Große Zahlen beliebiger Genauigkeit


Alle elementaren numerischen Typen arbeiten aufgrund ihrer festen Größe im Speicher mit
begrenzter Genauigkeit (wobei die beiden Gleitkommatypen den größeren Integer-Typen sogar
bezüglich der Anzahl signifikanter Stellen unterlegen sind).
Die Lage ist allerdings bei weitem nicht so tragisch, wie es sich anhört: Die Integer-Typen
arbeiten in ihrem (für long durchaus beachtlichen) Wertebereich absolut exakt und die Genau-
igkeit des Datentyps double ist meist mehr als zufrieden stellend. Trotzdem gibt es natürlich
Situationen, wo Wertebereich und Genauigkeit dieser Datentypen nicht ausreichen, etwa bei
quantenphysikalischen Berechnungen oder in der Finanzmathematik, wo manchmal schon
geringfügige Rundungs- oder Darstellungsfehler durch Multiplikation mit großen Faktoren zu
extremen Abweichungen führen.
Für solche Fälle stellt Ihnen die Java-Bibliothek die Klassen BigInteger und BigDecimal aus
dem Paket java.math zur Verfügung.
Beide Klassen
왘 arbeiten mit unveränderbaren Objekten, die Integer- (BigInteger) oder Gleitkommazahlen
(BigDecimal) beliebiger Genauigkeit kapseln.
왘 definieren neben anderen Konstruktoren auch solche, die als Argument die String-Darstel-
lung der zu kapselnden Zahl erwarten. (So lassen sich die zu erzeugenden Instanzen mit
Zahlen initialisieren, die nicht mehr als Literale numerischer Datentypen geschrieben wer-
den können.)
왘 BigInteger(String zahl)
왘 BigDecimal(String zahl)
왘 definieren numerische Methoden für die vier Grundrechenarten.
Beachten Sie, dass alle diese Methoden das Ergebnis der Operation als neues Objekt
zurückliefern (da einmal erzeugte Big-Objekte wie gesagt unveränderlich sind):
92 >> Große Zahlen beliebiger Genauigkeit
Zahlen

왘 BigInteger add(BigInteger wert)


왘 BigInteger subtract(BigInteger wert)
왘 BigInteger multiply(BigInteger wert)
왘 BigInteger divide(BigInteger wert)
왘 BigInteger pow(int exponent)
왘 definieren verschiedene weitere Methoden zur Umwandlung in oder aus elementaren
Datentypen:
왘 long longValue()
왘 double doubleValue()
왘 static BigInteger valueOf(long wert)
왘 definieren Vergleichsmethoden und verschiedene weitere nützliche Methoden (siehe API-
Dokumentation)
왘 boolean equals(Object o)
왘 int compareTo(BigInteger wert)

Ein Anfangskapital von 200 € soll für 9 Jahre bei einer vierteljährlichen Verzinsung von
0,25% p.Q. angelegt werden.
Das folgende Programm berechnet das Endkapital gemäß der Zinseszins-Formel Kn = K0
(1 + p/100)n in den drei Datentypen float, double und BigDecimal.

import java.math.BigDecimal;

public class Start {

/*
* Zinseszins-Berechnung mit float-Werten
*/
static float compoundInterest(float startCapital,
float interestRate, int term) {
return startCapital *
(float) Math.pow(1.0 + interestRate, term);
}

/*
* Zinseszins-Berechnung mit double-Werten
*/
static double compoundInterest(double startCapital,
double interestRate, int term) {
return startCapital * Math.pow(1.0 + interestRate, term);
}

/*
* Zinseszins-Berechnung mit BigDecimal-Werten
*/

Listing 28: Rechnen mit BigDecimal


>> Zahlen und Mathematik 93

Zahlen
static BigDecimal compoundInterest(BigDecimal startCapital,
BigDecimal interestRate, int term) {
interestRate = interestRate.add(new BigDecimal(1.0));

BigDecimal factor = new BigDecimal(0);


factor = factor.add(interestRate);

for (int i = 1; i < term ; ++i)


factor = factor.multiply(interestRate);

return startCapital.multiply(factor);
}

public static void main(String args[]) {


System.out.println();

System.out.println(compoundInterest(200.0f, 0.025f, 36));


System.out.println(compoundInterest(200.0, 0.025, 36));
System.out.println(compoundInterest(new BigDecimal("200.0"),
new BigDecimal("0.025"),36).doubleValue());
}
}

Listing 28: Rechnen mit BigDecimal (Forts.)

Ausgabe:
486.50708
486.5070631435786
486.50706314358007

Für die Ausgabe des BigDecimal-Werts ist die Umwandlung in double notwendig, da
Achtung

toString() eine Stringdarstellung des BigDecimal-Objekts liefert, aus der der tatsächli-
che Wert praktisch nicht herauszulesen ist. Den mit der Umwandlung einhergehenden
Genauigkeitsverlust müssen wir also in Kauf nehmen. Er ist aber nicht so tragisch. Ent-
scheidend ist, dass die Formel (insbesondere die Potenz!), mit erhöhter Genauigkeit
berechnet wurde.
Strings
Strings

27 In Strings suchen
Da Strings nicht sortiert sind, werden sie grundsätzlich sequentiell (von vorn nach hinten oder
umgekehrt von hinten nach vorn) durchsucht: Trotzdem lassen sich drei alternative Suchver-
fahren unterscheiden, nämlich die Suche nach
왘 einzelnen Zeichen,
왘 Teilstrings,
왘 Mustern (regulären Ausdrücken).

Grundsätzlich gilt, dass die aufgeführten Suchverfahren von oben nach unten immer leis-
tungsfähiger, aber auch immer teurer werden.

Suchen nach einzelnen Zeichen


Mit den String-Methoden indexOf() und lastIndexOf() können Sie nach einzelnen Zeichen in
einem String suchen.
Als Argument übergeben Sie das zu suchende Zeichen und optional die Position, ab der
gesucht werden soll. Als Ergebnis erhalten Sie die Position des nächsten gefundenen Vorkom-
mens, wobei die Methode indexOf() den String von vorn nach hinten und die Methode last-
IndexOf() von hinten nach vorn durchsucht.
Die wichtigsten Einsatzmöglichkeiten sind
왘 die Suche nach dem ersten Vorkommen eines Zeichens:
// Erstes Vorkommen von 'y' in String text
int pos = text.indexOf('y');
왘 die Suche nach dem letzten Vorkommen eines Zeichens:
// Letztes Vorkommen von 'y' in String text
int pos = text.lastIndexOf('y');
왘 die Suche nach allen Vorkommen eines Zeichens:
// Alle Vorkommen von 'y' in String text
int found = 0;
while ((found = text.indexOf('y', found)) != -1 ) {
// hier Vorkommen an Position end verarbeiten
...
++found;
}

Suchen nach Teilstrings


Ebenso wichtig wie die Suche nach Zeichen ist die Suche nach Teilstrings in einem String. Doch
leider gibt es in der String-Klasse derzeit keine Methode, mit der man einen String nach den
Vorkommen eines Teilstrings durchsuchen könnte. Mit Hilfe der String-Methoden indexOf()
und substring() ist eine solche Methode aber schnell implementiert:
96 >> In Strings suchen
Strings

/**
* String nach Teilstrings durchsuchen
*/
public static int indexOfString(String text, String searched) {
return indexOfString(text, searched, 0);
}

public static int indexOfString(String text, String searched, int pos) {


String tmp;
int lenText = text.length();
int lenSearched = searched.length();
int found = pos;

// Nach Anfangsbuchstaben suchen


while ((found = text.indexOf(searched.charAt(0), found)) != -1 ) {

// Wenn String noch groß genug, Teilstring herauskopieren


// und mit dem gesuchten String vergleichen
if (found + lenSearched <= lenText) {
tmp = text.substring(found, found + lenSearched);

if (tmp.equals(searched))
break; // Vorkommen gefunden
}
++found;
}

return found;
}

Listing 29: Methoden zum Suchen nach Strings in Strings

Die erste der beiden Methoden übernimmt als Argumente den zu durchsuchenden String und
den zu suchenden String und beginnt mit der Suche am Anfang des Strings, d.h., sie ruft ein-
fach die zweite Methode mit der Startposition 0 auf. Diese zweite Methode übernimmt als
zusätzliches Argument besagte Positionsangabe und durchsucht dann ab dieser Position den
String text nach dem nächsten Vorkommen von searched. Sie geht dabei so vor, dass sie
zuerst mit indexOf() nach dem Anfangsbuchstaben von searched sucht. Wurde ein Vorkom-
men dieses Buchstabens gefunden, kopiert die Methode ab seiner Position aus text einen
String heraus, der ebenso groß ist wie der gesuchte String (sofern die Länge von text dies
zulässt), und vergleicht diesen mit searched. Stimmen beide Strings überein, wird die Suche
abgebrochen und die gefundene Position zurückgeliefert. Wird kein Vorkommen von searched
gefunden, liefert die Methode -1 zurück.
Eingesetzt wird die Methode so wie indexOf():
// Erstes Vorkommen von "John Maynard" in String text
int pos = MoreString.indexOfString(text, "John Maynard");

// Alle Vorkommen von "John Maynard" in String text


found = 0;
>> Strings 97

Strings
while((found = MoreString.indexOfString(text, "John Maynard", found)) != -1) {
// hier Vorkommen an Position end verarbeiten
...
++found;
}

Suchen nach Mustern


Mit Hilfe der RegEx-Unterstützung von Java können Sie auch nach Vorkommen eines Musters
suchen.
1. Zuerst definieren Sie das Muster, nach dem gesucht werden soll.
Beispielsweise könnten Sie einen deutschen Text mit folgendem Muster nach Substantiven
durchsuchen:
"[A-ZÄÖÜ][a-zA-ZäöüßÄÖÜ]+"
Dieses Muster beschreibt ein Wort, das mit einem Großbuchstaben beginnt, dem beliebig
viele Kleinbuchstaben folgen.
2. Dann kompilieren Sie das Muster in ein Pattern-Objekt und besorgen sich für das Pattern-
Objekt und den zu durchsuchenden String einen Matcher.
import java.util.regex.Pattern;
import java.util.regex.Matcher;
...

Pattern pat = Pattern.compile("[A-ZÄÖÜ][a-zA-ZäöüßÄÖÜ]+");


Matcher m = pat.matcher(text);
3. Schließlich lassen Sie die Matcher-Methode find() das nächste Vorkommen suchen.
Achtung! Die Methode find() setzt die Suche automatisch immer an der Position fort, an
der die letzte find()-Suche beendet wurde. Um die Suche wieder am Anfang zu beginnen,
rufen Sie reset() auf. Wenn Sie die Suche an einer bestimmten Position starten wollen,
übergeben Sie find() als zweites Argument die gewünschte Position.
Das letzte gefundene Vorkommen wird intern vom Matcher-Objekt gespeichert und kann
durch Aufruf der Methode group() abgefragt werden.
// Erstes Vorkommen suchen
if (m.find())
System.console().printf("Erstes Vorkommen: %s\n",
m.group());

// Alle Vorkommen suchen, gegebenenfalls nach m.reset();


while (m.find()) {
System.console().printf("%s\n", m.group());
}

Die String-Methode matches() ist nicht zum Durchsuchen von Strings geeignet. Sie
A c h t u ng

prüft lediglich, ob der aktuelle String einem übergebenen Muster entspricht!

Im Start-Programm zu diesem Rezept werden die hier vorgestellten Suchverfahren alle noch
einmal eingesetzt und demonstriert.
98 >> In Strings einfügen und ersetzen
Strings

28 In Strings einfügen und ersetzen


왘 Wenn Sie alle Vorkommen eines Zeichens durch ein anderes Zeichen ersetzen wollen,
rufen Sie die String-Methode
String replace(char oldChar, char newChar)
auf. Der resultierende String wird als Ergebnis zurückgeliefert.
왘 Wenn Sie alle Vorkommen eines Teilstrings durch einen anderen Teilstring ersetzen
wollen, rufen Sie die String-Methode
String replaceAll(String regex, String replacement)
auf. Als erstes Argument übergeben Sie einfach den zu ersetzenden Teilstring. Der resultie-
rende String wird als Ergebnis zurückgeliefert.
왘 Wenn Sie alle Vorkommen eines Musters durch einen anderen Teilstring ersetzen wol-
len, rufen Sie die String-Methode
String replaceAll(String regex, String replacement)
auf und übergeben Sie das Muster als erstes Argument. Der resultierende String wird als
Ergebnis zurückgeliefert.
왘 Wenn Sie die Zeichen von start bis einschließlich end-1 durch einen anderen Teilstring
ersetzen wollen, wandeln Sie den String in ein StringBuilder-Objekt um und rufen Sie die
Methode
StringBuilder replace(int start, int end, String str)
auf. Da StringBuilder-Objekte nicht wie String-Objekte immutable sind, bearbeitet die
StringBuilder-Methode direkt das aktuelle Objekt. Den zugehörigen String können Sie
sich durch Aufruf von toString() zurückliefern lassen:
StringBuilder tmp = new StringBuilder(text);
tmp.replace(0, 10, " ");
text = tmp.toString();
왘 Wenn Sie Zeichen oder Strings an einer bestimmten Position in den String einfügen
wollen, wandeln Sie den String in ein StringBuilder-Objekt um und rufen Sie eine der
überladenen Versionen von
StringBuilder insert(int offset, char c)
StringBuilder insert(int offset, String str)
StringBuilder insert(int offset, boolean b)
StringBuilder insert(int offset, int i)
...
auf. Den zugehörigen String können Sie sich durch Aufruf von toString() zurückliefern
lassen.

Das Start-Programm zu diesem Rezept demonstriert die Ersetzung mit der replace()-Methode
von StringBuilder und der replaceAll()-Methode von String. (Hinweis: Das Programm ist
trotz der Erwähnung eines bekannten Politikers nicht als politischer Kommentar gedacht, son-
dern spielt lediglich – in Anlehnung an den »Lotsen« Bismarck – mit der Vorstellung vom
Kanzler als Steuermann.)
>> Strings 99

Strings
public class Start {

public static void main(String args[]) {

String text = "John Maynard!\n"


+ "\"Wer ist John Maynard?\"\n"
+ "\"John Maynard war unser Steuermann,\n"
+ "Aus hielt er, bis er das Ufer gewann,\n"
+ "Er hat uns gerettet, er traegt die Kron,\n"
+ "Er starb fuer uns, unsre Liebe sein Lohn.\n"
+ "John Maynard.\"\n";
int pos;
int found;

// Originaltext ausgeben
System.console().printf("\n%s", text);

// Zeichen von 156 bis einschließlich 160 ersetzen


StringBuilder tmp = new StringBuilder(text);
tmp.replace(156,161,"tat es");
text = tmp.toString();

// "John Maynard" durch "Gerhard Schroeder" ersetzen


text = text.replaceAll("John Maynard", "Gerhard Schroeder");

// Bearbeiteten Text ausgeben


System.console().printf("\n%s", text);

}
}

Listing 30: Ersetzen in Strings

Abbildung 16: Textfälschung mittels replace()


100 >> Strings zerlegen
Strings

29 Strings zerlegen
Zum Zerlegen von Strings steht die String-Methode split() zur Verfügung.
String[] split(String regex)
Der split()-Methode liegt die Vorstellung zugrunde, dass der zu zerlegende Text aus mehre-
ren informationstragenden Passagen besteht, die durch spezielle Zeichen oder Zeichenfolgen
getrennt sind. Ein gutes Beispiel ist ein String, der mehrere Zahlenwerte enthält, die durch
Semikolon getrennt sind:
String data = "1;-234;5623;-90";
Die Zahlen sind in diesem Fall die eigentlich interessierenden Passagen, die extrahiert werden
sollen. Die Semikolons dienen lediglich als Trennzeichen und sollen bei der Extraktion ver-
worfen werden (d.h., sie sollen nicht mehr als Teil der zurückgelieferten Strings auftauchen).
Für solche Fälle ist split() ideal. Sie übergeben einfach das Trennzeichen (in Form eines
Strings aus einem Zeichen) und erhalten die zwischen den Trennzeichen stehenden Textpassa-
gen als String-Array zurück.
String[] buf = data.split(";");
Selbstverständlich können Sie auch Strings aus mehreren Zeichen als Trennmarkierung über-
geben. Da der übergebene String als regulärer Ausdruck interpretiert wird, können Sie sogar
durch Definition einer passenden Zeichenklasse nach mehreren alternativen Trennmarkierun-
gen suchen lassen:
String[] buf = data.split("[;/]"); // erkennt Semikolon und
// Schrägstrich als Trennmarkierung

Abbildung 17: Beispiel für die Zerlegung eines Textes in einzelne Wörter

oder beliebig komplexe Trennmarkierungen definieren:


>> Strings 101

Strings
// aus Start.java
// Text, der mit Zeilenumbrüchen und zusätzlichen Leerzeichen formatiert wurde
String text = "Sei getreu bis in den Tod,\n so will ich dir die Krone des "
+ "Lebens geben.";

String[] words = text.split("\\s+"); // Beliebige Folgen von Whitespace


// als Trennmarkierung erkennen

Listing 31: Strings mit split() zerlegen

30 Strings zusammenfügen
Dass Strings mit Hilfe des +-Operators oder der String-Methode concat() aneinander gehängt
werden können (Konkatenation), ist allgemein bekannt.
Zu beachten ist allerdings, dass diese Operationen, wie im Übrigen sämtliche Manipulationen
von String-Objekten, vergleichsweise kostspielig sind, da String-Objekte immutable, sprich
unveränderlich, sind. Alle String-Operationen, die den ursprünglichen String verändern wür-
den, werden daher nicht auf den Originalstrings, sondern einer Kopie ausgeführt!
Wenn Sie also – um ein konkretes Beispiel zu geben – an einen bestehenden String einen
anderen String anhängen, werden die Zeichen des zweiten Strings nicht einfach an das letzte
Zeichen des ersten Strings angefügt, sondern es wird ein ganz neues String-Objekt erzeugt, in
welches die Zeichen der beiden aneinander zu hängenden Strings kopiert werden.
Solange die entsprechenden String-Manipulationen nur gelegentlich durchgeführt werden, ist
der Overhead, der sich durch die Anfertigung der Kopie ergibt, verschmerzbar. Sobald aber auf
einen String mehrere manipulierende Operationen nacheinander ausgeführt werden, stellt sich
die Frage nach einer ressourcenschonenderen Vorgehensweise.
Java definiert zu diesem Zweck die Klassen StringBuilder und StringBuffer. Diese stellen
Methoden zur Verfügung, die direkt auf dem aktuellen Objekt (letzten Endes also der Zeichen-
kette, die dem String zugrunde liegt) operieren und mit deren Hilfe Konkatenations-, Einfüge-
und Ersetzungsoperationen effizient durchgeführt werden können.

String partOne = "Der Grund, warum wir uns über die Welt täuschen, ";
String partTwo = "liegt sehr oft darin, ";
String partThree = "dass wir uns über uns selbst täuschen.";

// StringBuilder-Objekt auf der Grundlage von partOne erzeugen


StringBuilder text = new StringBuilder(partOne);

// Text in StringBuilder-Objekt bearbeiten


text.append(partTwo);
text.append(partThree);

// String-Builder-Objekt in String verwandeln


String s = text.toString();

Listing 32: String-Konkatenation mit StringBuilder


102 >> Strings nach den ersten n Zeichen vergleichen
Strings

Wenn Sie einen String in ein StringBuilder-Objekt verwandeln wollen, um es effizienter bear-
beiten zu können, dürfen Sie nicht vergessen, die Kosten für die Umwandlung in StringBuil-
der und wieder zurück in String in Ihre Kosten-Nutzen-Analyse mit einzubeziehen.
Denken Sie aber auch daran, dass der Geschwindigkeitsvorteil von StringBuilder.append()
nicht nur darin besteht, dass kein neues String-Objekt erzeugt werden muss. Mindestens
ebenso wichtig ist, dass die Zeichen des Ausgangsstrings (gemeint ist der String, an den ange-
hängt wird) nicht mehr kopiert werden müssen. Je länger der Ausgangsstring ist, umso mehr
Zeit wird also eingespart.
Mit dem Start-Programm zu diesem Rezept können Sie messen, wie viel Zeit 10.000 String-
Konkatenationen mit dem String-Operator + und mit der StringBuilder-Methode append()
benötigen.

StringBuilder oder StringBuffer?


E x ku r s

Die Klassen StringBuilder und StringBuffer verfügen über praktisch identische Kon-
struktoren und Methoden und erlauben direkte Operationen auf den ihnen zugrunde
liegenden Strings. Die Klasse StringBuffer wurde bereits in Java 1.2 eingeführt und ist
synchronisiert, d.h., sie eignet sich für Implementierungen, in denen mehrere Threads
gleichzeitig auf einen String zugreifen.
So vorteilhaft es ist, eine synchronisierte Klasse für String-Manipulationen zur Verfü-
gung zu haben, so ärgerlich ist es, in den 90% der Fälle, wo Strings nur innerhalb eines
Threads benutzt werden, den unnötigen Ballast der Synchronisierung mitzuschleppen.
Aus diesem Grund wurde in Java 5 StringBuilder eingeführt – als nichtsynchronisier-
tes Pendant zu StringBuffer.
Sofern Sie also nicht mit einem älteren JDK (vor Version 1.5) arbeiten oder Strings
benötigen, auf die von mehreren Threads aus zugegriffen wird, sollten Sie stets String-
Builder verwenden. Im Übrigen kann Code, der mit StringBuilder arbeitet, ohne große
Mühen nachträglich auf StringBuffer umgestellt werden: Sie müssen lediglich alle
Vorkommen von StringBuilder durch StringBuffer ersetzen. (Gilt natürlich ebenso für
die umgekehrte Richtung.)

31 Strings nach den ersten n Zeichen vergleichen


Die Java-String-Klassen bieten relativ wenig Unterstützung zur bequemen Textverarbeitung.
Nicht, dass elementare Funktionalität fehlen würde; was fehlt, sind Convenience-Methoden,
wie man sie von stärker textorientierten Programmiersprachen kennt und die einem die eine
oder andere Aufgabe vereinfachen würden. Die folgenden Rezepte sollen helfen, diese Lücke
zu schließen.
Für String-Vergleiche gibt es in Java auf der einen Seite die String-Methoden compareTo() und
compareToIgnoreCase(), die nach dem Unicode der Zeichen vergleichen, und auf der anderen
Seite die Collator-Methode compare() für Vergleiche gemäß einer Lokale (siehe Rezept 205).
Diese Methoden vergleichen immer ganze Strings.
Wenn Sie lediglich die Anfänge zweier Strings vergleichen wollen, müssen Sie die zu verglei-
chenden String-Teile als Teilstrings aus den Originalstrings herausziehen (substring()-
Methode). Das folgende Listing demonstriert dies und kapselt den Code gleichzeitig in eine
Methode.
>> Strings 103

Strings
/**
* Strings nach ersten n Zeichen vergleichen
* Rückgabewert ist kleiner, gleich oder größer Null
*/
public static int compareN(String s1, String s2, int n) {
if (n < 1)
throw new IllegalArgumentException();

if (s1 == null || s2 == null)


throw new IllegalArgumentException();

// Kürzen, wenn String mehr als n Zeichen enthält


if (s1.length() > n)
s1 = s1.substring(0, n);
if (s2.length() > n)
s2 = s2.substring(0, n);

// Die Stringanfänge vergleichen


return s1.compareTo(s2);
}

Listing 33: Strings nach den ersten n Zeichen vergleichen

Die Methode compareN() ruft für den eigentlichen Vergleich die String-Methode compareTo()
auf. Folglich übernimmt sie auch die compareTo()-Semantik: Der Rückgabewert ist kleiner,
gleich oder größer null, je nachdem, ob der erste String kleiner, gleich oder größer als der
zweite String ist. Verglichen wird nach dem Unicode und der zurückgelieferte Zahlenwert gibt
die Differenz zwischen den Unicode-Werten des ersten unterschiedlichen Zeichenpaars an
(bzw. der Stringlängen, falls die Zeichenpaare alle gleich sind).
Sollen die Strings lexikografisch verglichen werden, muss der Vergleich mit Hilfe von Collator.
compare() durchgeführt werden:

/**
* Strings nach ersten n Zeichen gemäß Lokale vergleichen
* Rückgabewert ist -1, 0 oder 1
*/
public static int compareN(String s1, String s2, int n, Locale loc) {
if (n < 1)
throw new IllegalArgumentException();

if (s1 == null || s2 == null)


throw new IllegalArgumentException();

// Kürzen, wenn String mehr als n Zeichen enthält


if (s1.length() > n)
s1 = s1.substring(0, n);
if (s2.length() > n)
s2 = s2.substring(0, n);

Listing 34: Strings lexikografisch nach den ersten n Zeichen vergleichen


104 >> Strings nach den ersten n Zeichen vergleichen
Strings

Collator coll = Collator.getInstance(loc);


return coll.compare(s1, s2);
}

Listing 34: Strings lexikografisch nach den ersten n Zeichen vergleichen (Forts.)

In der Datei MoreString.java zu diesem Rezept sind noch zwei weitere Methoden defi-
H in we is

niert, mit denen Strings ohne Berücksichtigung der Groß-/Kleinschreibung verglichen


werden können:
static int compareNIgnoreCase(String s1, String s2, int n, Locale loc)

static int compareNIgnoreCase(String s1, String s2, int n)

Das Start-Programm zu diesem Rezept liest über die Befehlszeile zwei Strings und die Anzahl
der zu vergleichenden Zeichen ein. Dann vergleicht das Programm die beiden Strings auf vier
verschiedene Weisen:
왘 gemäß Unicode, mit Berücksichtigung der Groß-/Kleinschreibung.
왘 gemäß Unicode, ohne Berücksichtigung der Groß-/Kleinschreibung.
왘 gemäß der deutschen Lokale, mit Berücksichtigung der Groß-/Kleinschreibung.
왘 gemäß der deutschen Lokale, ohne Berücksichtigung der Groß-/Kleinschreibung.

import java.util.Locale;

public class Start {

public static void main(String args[]) {


System.out.println();

if (args.length != 3) {
System.out.println(" Aufruf: Start <String> <String> <Ganzzahl>");
System.exit(0);
}

try {
int n = Integer.parseInt(args[2]);

System.out.println("\n Vergleich nach Unicode ");


int erg = MoreString.compareN(args[0], args[1], n);

if(erg < 0) {
System.out.println(" String 1 ist kleiner");
} else if(erg == 0) {
System.out.println(" Strings sind gleich");
} else {
System.out.println(" String 1 ist groesser");
}

Listing 35: Testprogramm für String-Vergleiche


>> Strings 105

Strings
...

System.out.println("\n Vergleich nach Lokale");


erg = MoreString.compareN(args[0], args[1], n,
new Locale("de", "DE"));

switch(erg) {
case -1: System.out.println(" String 1 ist kleiner");
break;
case 0: System.out.println(" Strings sind gleich");
break;
case 1: System.out.println(" String 1 ist groesser");
break;
}

...

}
catch (NumberFormatException e) {
System.err.println(" Ungueltiges Argument");
}
}
}

Listing 35: Testprogramm für String-Vergleiche (Forts.)

Abbildung 18: Ob ein String größer oder kleiner als ein anderer String ist, hängt vor
allem von der Vergleichsmethode ab!

Die in diesem Rezept vorgestellten Methoden sind, man muss es so hart sagen, alles
T ip p

andere als effizient implementiert: Die Rückführung auf vorhandene API-Klassen sorgt
für eine saubere Implementierung, bedeutet aber zusätzlichen Function Overhead, und
die Manipulation der tatsächlich ja unveränderbaren String-Objekte führt im Hinter-
grund zu versteckten Kopieraktionen. Wesentlich effizienter wäre es, die Strings in
char-Arrays zu verwandeln (String-Methode toCharArray()) und diese selbst Zeichen
für Zeichen zu vergleichen. Der Aufwand lohnt sich aber nur, wenn Sie wirklich exzes-
siven Gebrauch von diesen Methoden machen wollen.
106 >> Zeichen (Strings) vervielfachen
Strings

32 Zeichen (Strings) vervielfachen


Strings zu vervielfachen oder ein Zeichen n Mal in einen String einzufügen, ist nicht schwer.
Das Aufsetzen der nötigen Schleifen, eventuell auch die Umwandlung in StringBuilder-
Objekte, ist allerdings lästig und stört die Lesbarkeit des Textverarbeitungscodes. Conve-
nience-Methoden, die Zeichen bzw. Strings vervielfachen und als String zurückliefern, können
hier Abhilfe schaffen.
Die Methode charNTimes() erzeugt einen String, der aus n Zeichen c besteht.

import java.util.Arrays;

/**
* Zeichen vervielfältigen
*/
public static String charNTimes(char c, int n) {
if (n > 0) {
char[] tmp = new char[n];

Arrays.fill(tmp, c);

return new String(tmp);


} else
return "";
}

Listing 36: String aus n gleichen Zeichen erzeugen

Beachten Sie, dass die Methode nicht einfach ein leeres String-Objekt erzeugt und diesem mit
Hilfe des +-Operators n Mal das Zeichen c anhängt. Diese Vorgehensweise würde wegen der
Unveränderbarkeit von String-Objekten dazu führen, dass n+1 String-Objekte erzeugt wür-
den. Stattdessen wird ein char-Array passender Größe angelegt und mit dem Zeichen c gefüllt.
Anschließend wird das Array in einen String umgewandelt und als Ergebnis zurückgeliefert.

Zur Erinnerung: String-Objekte sind immutable, d.h. unveränderbar. Jegliche Ände-


A c h t u ng

rung an einem String-Objekt, sei es durch den +-Operator oder eine der String-Metho-
den führt dazu, dass ein neues String-Objekt mit den gewünschten Änderungen
erzeugt wird.

Die Schwestermethode heißt strNTimes() und erzeugt einen String, der aus n Kopien des
Strings s besteht.

/**
* String vervielfältigen
*/
public static String strNTimes(String s, int n) {
StringBuilder tmp = new StringBuilder();

Listing 37: String aus n Kopien einer Zeichenfolge erzeugen


>> Strings 107

Strings
for(int i = 1; i <= n; ++i)
tmp.append(s);

return tmp.toString();
}

Listing 37: String aus n Kopien einer Zeichenfolge erzeugen (Forts.)

Diese Methode arbeitet intern mit einem StringBuilder-Objekt, um die Mehrfacherzeugung


von String-Objekten zu vermeiden.

StringBuilder- und StringBuffer-Objekte erlauben die direkte Manipulation von


H i nwe i s

Strings, d.h., die Methoden dieser Klassen operieren auf dem aktuellen Objekt und ver-
ändern dessen Zeichenfolge (statt wie im Fall von String ein neues Objekt zu erzeu-
gen). Die Klassen StringBuilder und StringBuffer besitzen identische Methoden. Die
StringBuilder-Methoden sind in der Ausführung allerdings schneller, weil sie im
Gegensatz zu den StringBuffer-Methoden nicht threadsicher sind.

Die zurückgelieferten Strings können Sie in andere Strings einbauen oder direkt ausgeben.

public class Start {

public static void main(String args[]) {


System.out.println();

System.out.println(" /" + MoreString.charNTimes('*', 40));


System.out.println();
System.out.println("\t Zeichen und ");
System.out.println("\t Zeichenfolgen vervielfachen ");
System.out.println();
System.out.println(" " + MoreString.strNTimes("*-", 20) + "/");

}
}

Listing 38: Testprogramm zu charNTimes() und strNTimes()

33 Strings an Enden auffüllen (Padding)


Die Klasse String definiert eine Methode trim() zum Entfernen von Whitespace-Zeichen an
den Enden eines Strings, aber keine Methode, um Strings an den Enden bis zu einer
gewünschten Länge aufzufüllen.
Die statische Methode MoreString.strpad() übernimmt einen String, füllt ihn bis auf die
gewünschte Zeichenlänge auf und liefert den resultierenden String zurück. Das Füllzeichen ist
frei wählbar. Der Parameter end bestimmt, ob am Anfang oder Ende des Strings aufgefüllt
wird. Als Argumente können ihm die vordefinierten Konstanten MoreString.PADDING_LEFT und
MoreString.PADDING_RIGHT übergeben werden. Der letzte Parameter cut legt fest, wie vorzuge-
hen ist, wenn die gewünschte Länge kleiner als die Originallänge des Strings ist. Ist cut gleich
true, wird der String am Ende gekürzt, ansonsten wird der Originalstring zurückgeliefert.
108 >> Strings an Enden auffüllen (Padding)
Strings

Abbildung 19: Ausgabe vervielfachter Zeichen und Zeichenfolgen

public class MoreString {


public static final short PADDING_LEFT = 0;
public static final short PADDING_RIGHT = 1;

...

/**
* Padding (Auffüllen) für String
*/
public static String strpad(String s, int length, char c, short end,
boolean cut) {
if(length < 1 || s.length() == length)
return s;

if(s.length() > length)


if (cut) // String verkleinern
return s.substring(0, length);
else // String unverändert zurückgeben
return s;

// Differenz berechnen // String vergrößern


int diff = length - s.length();

char[] pad = new char[diff];


for(int i = 0; i < pad.length; ++i)
pad[i] = c;

if(end == MoreString.PADDING_LEFT)
return new String(pad) + s;
else
return s + new String(pad);
}
...

Listing 39: String auf gewünschte Länge auffüllen


>> Strings 109

Strings
Häufig müssen Strings linksseitig mit Leerzeichen aufgefüllt werden. Für diese spezielle Auf-
gabe gibt es eine überladene Version der Methode:
public static String strpad(String s, int length) {

return MoreString.strpad(s, length,' ', MoreString.PADDING_LEFT);


}
Listing 40 demonstriert den Einsatz der Methode.

public class Start {

public static void main(String args[]) {


System.out.println();
String s = "Text";

System.out.println(" Padding rechts mit . auf Laengen 3, 5, 7 und 9");


System.out.println();
System.out.println(MoreString.strpad(s, 3,'.',MoreString.PADDING_RIGHT));
System.out.println(MoreString.strpad(s, 5,'.',MoreString.PADDING_RIGHT));
System.out.println(MoreString.strpad(s, 7,'.',MoreString.PADDING_RIGHT));
System.out.println(MoreString.strpad(s, 9,'.',MoreString.PADDING_RIGHT));
System.out.println();

System.out.println(" Padding links mit Leerzeichen auf Laengen 3, 5, 7 "


+ "und 9");
System.out.println();
System.out.println(MoreString.strpad(s, 3));
System.out.println(MoreString.strpad(s, 5));
System.out.println(MoreString.strpad(s, 7));
System.out.println(MoreString.strpad(s, 9));

System.out.println();
}
}

Listing 40: Testprogramm zu strpad()

Abbildung 20: String-Padding


110 >> Whitespace am String-Anfang oder -Ende entfernen
Strings

34 Whitespace am String-Anfang oder -Ende entfernen


Zu den Standardaufgaben der String-Verarbeitung gehört auch das Entfernen von Whitespace
(Leerzeichen, Zeilenumbruch, Tabulatoren etc.). Die String-Klasse stellt zu diesem Zweck die
Methode trim() zur Verfügung – allerdings mit dem kleinen Wermutstropfen, dass diese
immer von beiden Seiten, Stringanfang wie -ende, den Whitespace abschneidet. Um Ihnen die
Wahlmöglichkeit wiederzugeben, die trim() verweigert, erhalten Sie hier zwei Methoden
ltrim() und rtrim(), mit denen Sie Whitespace gezielt vom String-Anfang (ltrim()) bzw.
String-Ende (rtrim()) entfernen können.

/**
* Whitespace vom Stringanfang entfernen
*/
public static String ltrim(String s) {
int len = s.length();
int i = 0;
char[] chars = s.toCharArray();

// Index i vorrücken, bis Nicht-Whitespace-Zeichen


// (Unicode > Unicode von ' ') oder Stringende erreicht
while ((i < len) && (chars[i] <= ' ')) {
++i;
}

// gekürzten String zurückliefern


return (i > 0) ? s.substring(i, len) : s;
}

/**
* Whitespace vom Stringende entfernen
*/
public static String rtrim(String s) {
int len = s.length();
char[] chars = s.toCharArray();

// Länge len verkürzen, bis Nicht-Whitespace-Zeichen


// (Unicode > Unicode von ' ') oder Stringanfang erreicht
while ((len > 0) && (chars[len - 1] <= ' ')) {
--len;
}

// gekürzten String zurückliefern


return (len < s.length()) ? s.substring(0, len) : s;
}

Listing 41: Methoden zur links- bzw. rechtsseitigen Entfernung von Whitespace

Beide Methoden lassen sich von der String-Methode toCharArray() das Zeichenarray zurück-
liefern, das dem übergebenen String zugrunde liegt. Dieses Array gehen die Methoden in einer
while-Schleife Zeichen für Zeichen durch: ltrim() vom Anfang und rtrim() vom Ende ausge-
hend. Die Schleife wird so lange fortgesetzt, wie der Unicode-Wert der vorgefundenen Zeichen
>> Strings 111

Strings
kleiner oder gleich dem Unicode-Wert des Leerzeichens ist (dies schließt Whitespace und nicht
druckbare Sonderzeichen aus). Anschließend wird je nachdem, ob Whitespace gefunden wurde
oder nicht, ein vom Whitespace befreiter Teilstring oder der Originalstring zurückgeliefert.
Um einen String mit Hilfe dieser Methoden am Anfang oder Ende von Whitespace zu befreien,
übergeben Sie den String einfach als Argument an die jeweilige Methode und nehmen den
bearbeiteten String als return-Wert entgegen:
String trimmed = MoreString.ltrim(str);

Abbildung 21: Effekt der Methoden ltrim() und rtrim()

35 Arrays in Strings umwandeln


Als Java-Programmierer denkt man bei der Umwandlung von Objekten in Strings natürlich
zuerst an die Methode toString().

toString()
Für Arrays liefert toString() allerdings nur den Klassennamen und den Hashcode, wie in der
Notimplementierung von Object festgelegt.
int[] ints = { 1, -312, 45, 55, -9, 7005};
System.out.println(ints); // ruft intern ints.tostring() auf
Ausgabe:
[I@187c6c7
Angesichts der Tatsache, dass man sich von der Ausgabe eines Arrays in der Regel verspricht,
dass die einzelnen Array-Elemente in der Reihenfolge, in der sie im Array gespeichert sind, in
Strings umgewandelt und aneinander gereiht werden, ist die Performance von toString() ent-
täuschend. Andererseits ist bekannt, dass die Array-Unterstützung von Java nicht in den
Array-Objekten selbst, sondern in der Utility-Klasse Arrays implementiert ist. Und richtig, es
gibt auch eine Arrays-Methode toString().

Arrays.toString()
Die Arrays-Methode toString() übernimmt als Argument ein beliebiges Array und liefert als
Ergebnis einen String mit den Elementen des Arrays zurück. Genauer gesagt: Die Elemente im
Array werden einzeln in Strings umgewandelt, durch Komma und Leerzeichen getrennt anein-
ander gehängt, schließlich in eckige Klammern gefasst und zurückgeliefert:
int[] ints = { 1, -312, 45, 55, -9, 7005};
System.out.println(Arrays.toString(ints));
Ausgabe:
[1, -312, 45, 55, -9, 7005]
112 >> Arrays in Strings umwandeln
Strings

Enthält das Array als Elemente weitere Arrays, werden diese allerdings wiederum nur durch
Klassenname und Hashcode repräsentiert (siehe oben). Sollen Unterarrays ebenfalls durch
Auflistung ihrer Elemente dargestellt werden, rufen Sie die Arrays-Methode deepToString()
auf.
Auch wenn die Array-Methode toString() der Vorstellung einer praxisgerechten Array-to-
String-Methode schon sehr nahe kommt, lässt sie sich noch weiter verbessern. Schön wäre
zum Beispiel, wenn der Programmierer selbst bestimmen könnte, durch welche Zeichen oder
Zeichenfolge die einzelnen Array-Elemente getrennt werden sollen. Und auf die Klammerung
der Array-Elemente könnte man gut verzichten.

MoreString.toString()
Die statische Methode MoreString.toString() ist weitgehend identisch zu Arrays.toString(), nur
dass der erzeugte String nicht in Klammern gefasst wird und der Programmierer selbst bestimmen
kann, durch welche Zeichenfolge die einzelnen Array-Elemente getrennt werden sollen (zweites
Argument: separator). Ich stelle hier nur die Implementierungen für int[]- und Object[]-Arrays
vor. Für Arrays mit Elementen anderer primitiver Datentypen (boolean, char, long, double etc.)
müssen nach dem gleichen Muster eigene überladene Versionen geschrieben werden:

/**
* int[]-Arrays in Strings umwandeln
*/
public static String toString(int[] a, String separator) {
if (a == null)
return "null";
if (a.length == 0)
return "";

StringBuilder buf = new StringBuilder();


buf.append(a[0]);

for (int i = 1; i < a.length; i++) {


buf.append(separator);
buf.append(a[i]);
}

return buf.toString();
}

/**
* Object[]-Arrays in Strings umwandeln
*/
public static String toString(Object[] a, String separator) {
if (a == null)
return "null";
if (a.length == 0)
return "";

StringBuilder buf = new StringBuilder();

Listing 42: Arrays in Strings verwandeln


>> Strings 113

Strings
buf.append(a[0].toString());

for (int i = 1; i < a.length; i++) {


buf.append(separator);
buf.append(a[i].toString());
}

return buf.toString();
}

Listing 42: Arrays in Strings verwandeln (Forts.)

Mit Hilfe dieser Methoden ist es ein Leichtes, die Elemente eines Arrays wahlweise durch Leer-
zeichen, Kommata, Semikolons oder auch Zeilenumbrüche getrennt in einen String zu ver-
wandeln:
int[] ints = { 1, -312, 45, 55, -9, 7005};
String intStr;

intsStr = MoreString.toString(ints, ", ");


intsStr = MoreString.toString(ints, "\t");
intsStr = MoreString.toString(ints, "\n");

Abbildung 22: Vergleich der verschiedenen Array-to-String-Methoden

36 Strings in Arrays umwandeln


Ebenso wie es möglich ist, Array-Elemente in Strings zu verwandeln und zu einem einzigen
String zusammenzufassen, ist es natürlich auch denkbar, einen String in Teilstrings zu zer-
legen, in einen passenden Datentyp umzuwandeln und als Array zu verwalten. Mögliche
Anwendungen wären zum Beispiel die Zerlegung eines Textes in ein Array von Wörtern oder
die Extraktion von Zahlen aus einem String.
114 >> Strings in Arrays umwandeln
Strings

1. Als Erstes zerlegen Sie den String in Teilstrings. Dies geschieht am effizientesten mit der
split()-Methode (siehe Rezept 29).
Als Argument übergeben Sie einen regulären Ausdruck (in Form eines Strings), der angibt,
an welchen Textstellen der String aufgebrochen werden soll. Im einfachsten Fall besteht
dieser reguläre Ausdruck aus einem oder mehreren Zeichen respektive Zeichenfolgen, die
als Trennzeichen zwischen den informationstragenden Teilstrings stehen. Die Teilstrings
werden als String-Array zurückgeliefert.
String[] buf = aString.split(" "); // Leerzeichen als
// Trennzeichen

String[] substrings = aString.split("\\s+"); // Whitespace als


// Trennzeichen
Wenn es darum ginge, den String in ein Array von Teilstrings zu zerlegen, ist die Arbeit an
diesem Punkt bereits getan (von einer eventuell erforderlichen Nachbearbeitung der Teilstrings
einmal abgesehen). Ansonsten:
2. Wandeln Sie das String-Array in ein Array von Elementen des gewünschten Zieltyps um.
Das Start-Programm zu diesem Rezept demonstriert die Umwandlung in Arrays an zwei Bei-
spielen. Im ersten Fall wird ein Text in Wörter zerlegt (mit Whitespace als Trennzeichen), im
zweiten Fall wird ein String, der durch Tabulatoren getrennte Zahlenwerte enthält, zerlegt und
in ein Array von int-Werten umgewandelt:

public class Start {

public static void main(String args[]) {


String paul_ernst = "Die Narren reden am liebsten von der Weisheit, "
+ "die Schurken von der Tugend.";
String data = "1\t-234\t5623\t-90";

System.out.println("\n\n Text in Woerter-Array zerlegen:\n");


System.out.println(" \"" + paul_ernst + "\"\n\n");

// String in Array verwandeln


String[] words = paul_ernst.split("\\s+");

// Ausgabe der Array-Elemente


System.out.println(" Array nach split(\"\\\\s+\") :\n");
for (String s : words)
System.out.println("\t" + s);

System.out.println("\n\n String mit int-Daten in Zahlen zerlegen:\n");


System.out.println(" \"" + data + "\"\n\n");

// String in Array verwandeln

Listing 43: Demo-Programm zur Umwandlung von Strings in Arrays


>> Strings 115

Strings
String[] buf = data.split("\t");
int[] numbers = new int[buf.length];
try {
for (int i = 0; i < numbers.length; ++i)
numbers[i] = Integer.parseInt(buf[i]);

} catch (NumberFormatException e) {
System.err.println(" Fehler bei Umwandlung in Integer");
}

// Ausgabe der Array-Elemente


System.out.println(" Array nach split(\"\\t\"):\n");
for (Integer i : numbers)
System.out.println("\t" + i);

}
}

Listing 43: Demo-Programm zur Umwandlung von Strings in Arrays (Forts.)

Abbildung 23: Umwandlung von Strings in Arrays


116 >> Zufällige Strings erzeugen
Strings

37 Zufällige Strings erzeugen


Für Testzwecke ist es oft hilfreich, eine große Anzahl an unterschiedlichen, zufällig zusammen-
gesetzten Zeichenketten zur Verfügung zu haben. Als Java-Programmierer haben Sie natürlich
die besten Möglichkeiten, solche Zufallsstrings zu erzeugen. Für den Zufall sorgt dabei die
Klasse java.util.Random, die man zur Generierung von gleichverteilten Zufallszahlen verwen-
den kann, beispielsweise aus dem Bereich von 65 bis 122, in dem die ASCII-Codes der Groß- und
Kleinbuchstaben liegen. Aus einem ASCII-Wert kann dann einfach per Cast das entsprechende
Zeichen erzeugt werden. Das folgende Beispiel zeigt eine mögliche Implementierung zur Erzeu-
gung von solchen Zufallsstrings mit einer wählbaren Mindest- und Maximallänge:

/**
* Klasse zur Erzeugung zufälliger Zeichenketten aus dem Bereich a-Z
*/
import java.util.*;

class RandomStrings {

/**
* Erzeugt zufällige Zeichenketten aus dem Bereich a-Z
*
* @param num Anzahl zu generierender Zeichenketten
* @param min minimale Länge pro Zeichenkette
* @param max maximale Länge pro Zeichenkette
* @return ArrayList<String> mit Zufallsstrings
*/
public static ArrayList<String> createRandomStrings(int num,
int min, int max) {
Random randGen = new Random(System.currentTimeMillis());
ArrayList<String> result = new ArrayList<String>();

if(min > max || num <= 0) // nichts zu tun


return result;

for(int i = 1; i <= num; i++) {


int length;

// Länge des nächsten Strings zufällig wählen


if(min == max)
length = min;
else
length = randGen.nextInt(max + 1 - min) + min;

StringBuilder curStr = new StringBuilder();


int counter = 0;

while(counter < length) {


// Bereichsgrenzen: von A = 65 bis z = 122
// Bereich 91-96 sind Sonderzeichen -> überspringen
int value = randGen.nextInt(122 + 1 -65) + 65;

Listing 44: RandomStrings.java – ein String-Generator


>> Strings 117

Strings
if(value >= 91 && value <= 96)
continue;
else
counter++;

char z = (char) value;


curStr.append(z);
}

result.add(curStr.toString());
}

return result;
}
}

Listing 44: RandomStrings.java – ein String-Generator (Forts.)

Das Start-Programm zu diesem Rezept benutzt den String-Generator zur Erzeugung von zehn
Strings mit vier bis fünfzehn Buchstaben.

public class Start {

public static void main(String[] args) {

// Zehn Strings mit vier bis fünfzehn Zeichen erzeugen


ArrayList<String> strings;
strings = RandomStrings.createRandomStrings(10, 4, 15);

for(String str : strings)


System.out.println(str);
}
}

Listing 45: Erzeugung zufälliger Zeichenketten

Abbildung 24: Zufällige Strings der Länge 4 bis 15


118 >> Wortstatistik erstellen
Strings

38 Wortstatistik erstellen
Das Erstellen einer Wortstatistik, d.h. das Erkennen der Wörter inklusive ihrer Häufigkeit, lässt
sich mit Java sehr elegant realisieren. Die Kombination zweier Klassen stellt alles Nötige
bereit:
왘 Mit java.util.StringTokenizer wird der Text in die interessierenden Wörter zerlegt.
Bequemerweise kann man der Klasse auch einen String mit allen Zeichen mitgeben, die als
Trennzeichen interpretiert werden sollen. Dadurch kann man beispielsweise neben dem
üblichen Leerzeichen auch andere Satzzeichen beim Lesen überspringen.
왘 Alle gefundenen Wörter werden in einer Hashtabelle (z.B. java.util.HashMap) abgespei-
chert, zusammen mit ihrer aktuellen Häufigkeit. Wenn ein Wort bereits vorhanden ist, wird
lediglich der alte Zählerwert um eins erhöht.

/**
* Klasse zur Erstellung von Wortstatistiken
*/
import java.util.*;
import java.io.*;

class WordStatistics {

/**
* Erstellt eine Wortstatistik; Rückgabe ist eine Auflistung aller
* vorkommenden Wörter und ihrer Häufigkeit; Satzzeichen und gängige
* Sonderzeichen werden ignoriert.
*
* @param text zu analysierender Text
* @return HashMap<String, Integer> mit Wörtern und ihren Häufigkeiten
*/
public static HashMap<String, Integer> countWords(String text) {
StringTokenizer st = new StringTokenizer(text,
"\n\" -+,&%$§.;:?!(){}[]");
HashMap<String, Integer> wordTable = new HashMap<String, Integer>();

while(st.hasMoreTokens()) {
String word = st.nextToken();
Integer num = wordTable.get(word);

if(num == null) {
// bisher noch nicht vorhanden -> neu einfügen mit Zählwert = 1
num = new Integer(1);
wordTable.put(word, num);
}
else {
// Wort bereits vorhanden -> Zähler erhöhen
int numValue = num.intValue() + 1;
num = new Integer(numValue);
wordTable.put(word, num);
}

Listing 46: WordStatistics.java


>> Strings 119

Strings
}

return wordTable;
}
}

Listing 46: WordStatistics.java (Forts.)

Das Start-Programm zu diesem Rezept demonstriert den Aufruf.

public class Start {

public static void main(String[] args) {

if(args.length != 1) {
System.out.println("Aufruf: <Dateiname>");
System.exit(0);
}

// Datei einlesen
StringBuilder text = new StringBuilder();

try {
BufferedReader reader = new BufferedReader(
new FileReader(args[0]));
String line;

while((line = reader.readLine()) != null)


text.append(line + "\n");

} catch(Exception e) {
e.printStackTrace();
}

// Statistik erstellen
HashMap<String, Integer> statistic;
statistic = WordStatistics.countWords(text.toString());

// Statistik ausgeben
System.console().printf("\n Anzahl unterschiedlicher Wörter: %d\n",
statistic.size());
Set<String> wordSet = statistic.keySet();
Iterator<String> it = wordSet.iterator();

while(it.hasNext()) {
String word = it.next();
int num = statistic.get(word).intValue();
System.console().printf(" %s : %d \n", word, num); }

Listing 47: Erstellen einer Wortstatistik


120 >> Wortstatistik erstellen
Strings

}
}
}

Listing 47: Erstellen einer Wortstatistik (Forts.)

Abbildung 25: Ausgabe von Worthäufigkeiten

Die Gesamtzahl an unterschiedlichen Wörtern kann man wie oben gezeigt über die
H in we is

Methode size() der Hashtabelle ermitteln. Falls man allerdings die gesamte Anzahl an
Wörtern (inklusive Wiederholungen) benötigt, bietet StringTokenizer die Methode
countTokens() an:
StringTokenizer st = new StringTokenizer(meinText, "\n\" -+,&%$§.;:?!(){}[]");
System.out.println("Gesamtzahl Wörter: " + st.countTokens());
Datum und Uhrzeit

39 Aktuelles Datum abfragen

Datum und Uhrzeit


Der einfachste und schnellste Weg, das aktuelle Datum abzufragen, besteht darin, ein Objekt
der Klasse Date zu erzeugen:
import java.util.Date;

Date today = new Date();


System.out.println(today);
Ausgabe:
Thu Mar 31 10:54:31 CEST 2005
Wenn Sie dem Konstruktor keine Argumente übergeben, ermittelt er die aktuelle Systemzeit
als Anzahl Millisekunden, die seit dem 01.01.1970 00:00:00 Uhr, GMT, vergangen sind, und
speichert diese in dem Date-Objekt. Wenn Sie das Date-Objekt mit println() ausgeben (oder in
einen String einbauen), wird seine toString()-Methode aufgerufen, die aus der Anzahl Milli-
sekunden das Datum berechnet. Und genau hier liegt das Problem der Date-Klasse.
Die Date-Klasse arbeitet nämlich intern mit dem gregorianischen Kalender. Dieser ist zwar weit
verbreitet und astronomisch korrekt, jedoch bei weitem nicht der einzige Kalender. Bereits im
JDK 1.1 wurden der Klasse Date daher die abstrakte Klasse Calendar und die von Calendar
abgeleitete Klasse GregorianCalendar an die Seite gestellt. Die Idee dahinter:
왘 Neben GregorianCalendar können weitere Klasse für andere Kalender implementiert wer-
den.
왘 Von der statischen Methode Calendar.getInstance() kann sich der Programmierer automa-
tisch den passenden Kalender zur Lokale des aktuellen Systems zurückliefern lassen.

Leider gibt es derzeit nur drei vordefinierte Kalenderklassen, die von getInstance() zurückge-
liefert werden: sun.util.BuddhistCalendar für die Thai-Lokale (th_TH), JapaneseImperial-
Calendar (für ja_JP) und GregorianCalendar für alle anderen Lokalen.
Trotzdem sollten Sie den Empfehlungen von Sun folgen und die Klasse Date nur dann verwen-
den, wenn Sie an der reinen Systemzeit interessiert sind oder die chronologische Reihenfolge
verschiedener Zeiten prüfen wollen. Wenn Sie explizit mit Datumswerten programmieren
müssen, verwenden Sie Calendar oder GregorianCalendar.
// Aktuelles Datum mit Calendar abfragen
import java.util.Calendar;

Calendar calendar = Calendar.getInstance();


// verwende zur Ausgabe System.console() anstelle von System.out, um evt.
// enthaltene Umlaute korrekt auszugeben (siehe Rezept 85)
System.console().printf("%s\n",
java.text.DateFormat.getDateTimeInstance().format(calendar.getTime()) );
Der Aufruf Calendar.getInstance() liefert ein Objekt einer Calendar-Klasse zurück (derzeit für
nahezu alle Lokalen eine GregorianCalendar-Instanz, siehe oben). Das Calendar-Objekt reprä-
sentiert die aktuelle Zeit (Datum und Uhrzeit) gemäß der auf dem System eingestellten Lokale
und Zeitzone.
122 >> Aktuelles Datum abfragen

Die Felder (Jahr, Monat, Stunde ...) eines Calendar-Objekts können mit Hilfe der get-/set-
Methoden der Klasse abgefragt bzw. gesetzt werden.

Methode Beschreibung
Zum Abfragen der verschiedenen Feldwerte. Die Felder
Datum und Uhrzeit

int get(int field)


werden durch folgende Konstanten ausgewählt:
AM_PM // AM (Vormittag) oder PM (Nachmittag)
DATE // entspricht DAY_OF_MONTH
DAY_OF_MONTH // Tag im Monat, beginnend mit 1
DAY_OF_WEEK // Tag in Woche (1 (SUNDAY) - 7 (SATURDAY))
DAY_OF_WEEK_IN_MONTH // 7-Tage-Abschnitt in Monat,
beginnend mit 1
DAY_OF_YEAR // Tag im Jahr, beginnend mit 1
DST_OFFSET // Sommerzeitverschiebung in Millisekunden
ERA // vor oder nach Christus
HOUR // Stunde vor oder nach Mittag (0 - 11)
HOUR_OF_DAY // Stunde (0 - 23)
MILLISECOND // Millisekunden (0-999)
MINUTE // Minuten (0-59)
MONTH // Monat, beginnend mit JANUARY
SECOND // Sekunde (0-59)
WEEK_OF_MONTH // Woche in Monat, beginnend mit 0
WEEK_OF_YEAR // Woche in Jahr, beginnend mit 1
YEAR // Jahr
ZONE_OFFSET // Verschiebung für Zeitzone in Millisekun-
den
Date getTime() Liefert das Datum als Date-Objekt zurück.
long getTimeInMillis() Liefert das Datum als Millisekunden seit/bis zum
01.01.1970 00:00:00 Uhr, GMT zurück.
void set(int field, int value) Setzt den angegebenen Feldwert. Zur Bezeichnung der
Felder siehe get().
void set(int year, int month, int date) Setzt Jahr, Monat (0-11) und Tag (1-31). Optional können
void set(int year, int month, int date, auch noch Stunde (0-23), Minute und Sekunde angegeben
int hourOfDay, int minute) werden.
void set(int year, int month, int date,
int hourOfDay, int minute,
int second)
void setTime(Date d) Setzt das Datum gemäß dem übergebenen Date-Objekt.
void setTimeInMillis(long millis) Setzt das Datum gemäß der übergebenen Anzahl Milli-
sekunden seit/bis zum 01.01.1970 00:00:00 Uhr, GMT.

Tabelle 15: Get-/Set-Methoden zum Abfragen und Setzen der Datumsfelder der Calendar-
Klasse

Die Klasse Date enthält ebenfalls Methoden zum Abfragen und Setzen der einzelnen
A c h tun g

Datums- und Zeitfelder. Diese sind jedoch als »deprecated« eingestuft, von ihrem
Gebrauch wird abgeraten.
>> Datum und Uhrzeit 123

40 Bestimmtes Datum erzeugen


Es gibt verschiedene Wege, ein Objekt für ein bestimmtes Datum zu erzeugen.
Handelt es sich um ein Datum im gregorianischen Kalender, können Sie direkt ein Objekt der
Klasse GregorianCalendar erzeugen und dem Konstruktor Jahr, Monat (0–11) und Tag (1–31)

Datum und Uhrzeit


übergeben:
import java.util.GregorianCalendar;
...
Calendar birthday = new GregorianCalendar(1964, 4, 20);
Andere Kalender werden – mit Ausnahme des buddhistischen, des imperialistischen japani-
schen und des julianischen Kalenders (siehe unten) – derzeit nicht unterstützt.
Wenn Sie den Kalender nicht vorgeben, sondern gemäß den Ländereinstellungen des aktuellen
Systems auswählen möchten, lassen Sie sich von Calendar.getInstance() ein Objekt des loka-
len Kalenders zurückliefern und ändern das von diesem Objekt repräsentierte Datum durch
Setzen der Felder für Jahr, Monat und Tag:
import java.util.Calendar;
...
Calendar birthday = Calendar.getInstance();
birthday.set(1964, 4, 20);
Auf einem System, das für die Thai-Lokale (th_TH) konfiguriert ist, liefert getInstance() eine
Instanz von sun.util.BuddhistCalendar, für die Lokale ja_JP eine Instanz von JapaneseImperi-
alCalendar und für alle anderen Lokalen eine Instanz von GregorianCalendar.

Für Daten vor dem 15. Oktober 1582 berechnet die Klasse GregorianCalendar das
A c h t u ng

Datum nach dem julianischen Kalender. Dies ist sinnvoll, da an diesem Tag – der dem
5. Oktober 1582 im julianischen Kalender entspricht – der gregorianische Kalender
erstmals eingeführt wurde (in Spanien und Portugal). Andere Länder folgten nach und
nach. In England und Amerika begann der gregorianische Kalender beispielsweise mit
dem 14. September 1752. Wenn Sie die Lebensdaten englischer bzw. amerikanischer
Persönlichkeiten oder Daten aus der englischen bzw. amerikanischen Geschichte, die
vor der Einführung des gregorianischen Kalender liegen, historisch korrekt darstellen
möchten, müssen Sie das Datum der Einführung mit Hilfe der Methode setGregorian-
Change(Date) umstellen.
GregorianCalendar change = new GregorianCalendar(1752, 8, 14, 1, 0, 0);
((GregorianCalendar) birthday).setGregorianChange(change.getTime());
Wenn Sie Interesse halber beliebige Daten nach dem gregorianischen Kalender berech-
nen möchten, rufen Sie setGregorianChange(Date(Long.MIN_VALUE)) auf. Wenn Sie
beliebige Daten nach dem julianischen Kalender berechnen wollen, rufen Sie setGrego-
rianChange(Date(Long.MAX_VALUE)) auf.

Der Vollständigkeit halber sei erwähnt, dass es auch die Möglichkeit gibt, ein Date-Objekt
durch Angabe von Jahr (abzgl. 1900), Monat (0-11) und Tag (1-31) zu erzeugen:
Date birthday1 = new Date(64, 4, 20);
Vom Gebrauch dieses Konstruktors wird allerdings abgeraten, er ist als deprecated markiert.
124 >> Bestimmtes Datum erzeugen

Der gregorianische Kalender und die Klasse GregorianCalendar


E x k ur s

Vor der Einführung des gregorianischen Kalenders im Jahre 1582 durch Papst Gregor
XIII. galt in Europa der julianische Kalender. Der julianische Kalender, von dem ägyp-
tischen Astronomen Sosigenes ausgearbeitet und von Julius Cäsar im Jahre 46 v. Chr.
Datum und Uhrzeit

in Kraft gesetzt, war ein reiner Sonnenkalender, d.h., er richtete sich nicht nach den
Mondphasen, sondern nach der Länge des mittleren Sonnenjahres, die Sosigenes zu
365,25 Tagen berechnete. Der julianische Kalender übernahm die zwölf römischen
Monate, korrigierte aber deren Längen auf die noch heute gültige Anzahl Tage, so dass
das Jahr fortan 365 Tage enthielt. Um die Differenz zum »angenommenen« Sonnenjahr
auszugleichen, wurde alle vier Jahre ein Schaltjahr eingelegt.
Tatsächlich ist das mittlere Sonnenjahr aber nur 365,2422 Tage lang (tropisches Jahr).
Der julianische Kalender hinkte seiner Zeit also immer weiter hinterher, bis im Jahre
1582 das Primar-Äquinoktium auf den 11. statt den 21. März fiel.
Um die Differenz auszugleichen, verfügte Papst Gregor XIII. im Jahr 1582, dass in die-
sem Jahr auf den 4. Oktober der 15. Oktober folgen sollte. Gleichzeitig wurde der
gregorianische Kalender eingeführt, der sich vom julianischen Kalender in der Berech-
nung der Schaltjahre unterscheidet. Während der julianische Kalender alle vier Jahre
ein Schaltjahr einlegte, sind im gregorianischen Kalender alle Jahrhundertjahre, die
nicht durch 400 teilbar sind, keine Schaltjahre. Durch diese verbesserte Schaltregel ist
ein Jahr im gregorianischen Kalender durchschnittlich 365,2425 Tage lang, was dem
tatsächlichen mittleren Wert von 365,2422 Tagen (tropisches Jahr) sehr nahe kommt.
(Erst nach 3000 Jahren wird sich die Abweichung zu einem Tag addieren.)
Spanien, Portugal und Teile Italiens führten den gregorianischen Kalender wie vom
Papst vorgesehen in der Nacht vom 4. auf den 5./15. Oktober ein. Die meisten katholi-
schen Länder folgten in den nächsten Jahren, während die protestantischen Länder den
Kalender aus Opposition zum Papst zunächst ablehnten. Die orthodoxen Länder Ost-
europas führten den gregorianischen Kalender gar erst im 20. Jahrhundert ein.

Land Einführung
Spanien, Portugal, Teile Italiens 04./15. Oktober 1582
Frankreich 09./20. Dezember 1582
Bayern 05./16. Oktober 1583
Hzm. Preußen 22. August/02. September 1612
England, Amerika 02./14. 1752
Schweden 17. Februar/1. März 1753
Russland 31. Januar/14. Februar 1918
Griech.-Orthodoxe Kirche 10./24. März 1924
Türkei 1927

Tabelle 16: Einführung des gregorianischen Kalenders


>> Datum und Uhrzeit 125

Die Klasse GregorianCalendar implementiert eine Hybridform aus gregorianischem und


julianischem Kalender. Anhand des Datums der Einführung des gregorianischen Kalen-
ders interpretiert sie Datumswerte entweder als Daten im gregorianischen oder juliani-
schen Kalender (siehe Hinweis weiter oben). Das Datum der Einführung kann mit Hilfe

Datum und Uhrzeit


der Methode setGregorianChange(Date d) angepasst werden.
Das Datum, das eine GregorianCalendar-Instanz repräsentiert, kann durch Angabe der
Datumsfelder (Jahr, Monat, Tag ...), als Date-Objekt oder als Anzahl Millisekunden seit/
bis zum 01.01.1970 00:00:00 Uhr, GMT, festgelegt und umgekehrt auch als Werte der
Datumsfelder, Date-Objekt oder Anzahl Millisekunden abgefragt werden. Für die kor-
rekte Umrechnung zwischen Datumsfeldern und Anzahl Millisekunden sorgen dabei
die von Calendar geerbten und in GregorianCalendar überschriebenen protected-
Methoden computeFields() und computeTime().

41 Datums-/Zeitangaben formatieren
Zur Formatierung von Datums- und Zeitangaben gibt es drei Wege zunehmender Komplexität,
aber auch wachsender Gestaltungsfreiheit:
왘 toString()
왘 DateFormat-Stile
왘 SimpleDateFormat-Muster

Formatierung mit toString()


Die einfachste Form der Umwandlung einer Datums-/Zeitangabe in einen String bietet die
toString()-Methode. Ist die Datums-/Zeitangabe in ein Date-Objekt verpackt, erhält man auf
diese Weise einen String aus (engl.) Wochentagskürzel, (engl.) Monatskürzel, Tag im Monat,
Uhrzeit, Zeitzone und Jahr:
Thu Mar 31 10:54:31 CEST 2005
Wer Gleiches von der toString()-Methode der Klasse Calendar erwartet, sieht sich allerdings
getäuscht. Die Methode ist rein zum Debuggen gedacht und packt in den zurückgelieferten
String alle verfügbaren Informationen über den aktuellen Zustand des Objekts. Um dennoch
einen vernünftigen Datums-/Zeit-String zu erhalten, müssen Sie sich die im Calendar-Objekt
gespeicherte Zeit als Date-Objekt zurückliefern lassen und dessen toString()-Methode auf-
rufen:
Calendar calendar = Calendar.getInstance();
Date today = calendar.getTime();
System.out.println(date);
Ausgabe:
Thu Mar 31 10:54:31 CEST 2005
126 >> Datums-/Zeitangaben formatieren

Formatierung mit DateFormat-Stilen


Die Klasse DateFormat definiert vier vordefinierte Stile zur Formatierung von Datum und Uhr-
zeit: SHORT, MEDIUM (= DEFAULT), LONG und FULL.
Die Klasse DateFormat selbst ist abstrakt, definiert aber verschiedene statische Factory-Metho-
Datum und Uhrzeit

den, die passende Objekte abgeleiteter Klassen (derzeit nur SimpleDateFormat) zur Formatie-
rung von Datum, Uhrzeit oder der Kombination aus Datum und Uhrzeit zurückliefern.
Die Formatierung mit DateFormat besteht daher aus zwei Schritten:
1. Sie rufen die gewünschte Factory-Methode auf und lassen sich ein Formatierer-Objekt
zurückliefern.
import java.text.DateFormat;

// Formatierer für reine Datumsangaben im SHORT-Stil


DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
2. Sie übergeben die Datums-/Zeitangabe als Date-Objekt an die format()-Methode des For-
matierers und erhalten den formatierten String zurück.
Calendar calendar = Calendar.getInstance();
String str = df.format(calendar.getTime()));
Die Klasse DateFormat definiert vier Factory-Methoden, die gemäß der auf dem System einge-
stellten Lokale formatieren:
getInstance() // Formatierer für Datum und Uhrzeit im SHORT-Stil
getDateInstance() // Formatierer für Datum im DEFAULT-Stil (= MEDIUM)
getTimeInstance() // Formatierer für Uhrzeit im DEFAULT-Stil (= MEDIUM)
getDateTimeInstance() // Formatierer für Datum und Uhrzeit
// im DEFAULT-Stil (= MEDIUM)
Die letzten drei Methoden sind zweifach überladen, so dass Sie einen anderen Stil bzw. Stil
und Lokale vorgeben können, beispielsweise:
getDateInstance(int stil)
getDateInstance(int stil, Locale loc)
gibt eine Übersicht über die Formatierung durch die verschiedenen Stile.

Stil Formatierung (Lokale de_DE)


Datum
SHORT 31.03.05
MEDIUM (= DEFAULT) 31.03.2005
LONG 31. März 2005
FULL Donnerstag, 31. März 2005
Uhrzeit
SHORT 19:51
MEDIUM (= DEFAULT) 19:51:14
LONG 19:51:14 CEST
FULL 19.51 Uhr CEST

Tabelle 17: DateFormat-Stile


>> Datum und Uhrzeit 127

Zur landesspezifischen Formatierung mit Lokalen siehe auch Rezepte in Kategorie


H i nwe i s

»Internationalisierung«.

Datum und Uhrzeit


Formatierung mit SimpleDateFormat-Mustern
Wer mit den vordefinierten DateFormat-Formatstilen nicht zufrieden ist, kann sich mit Hilfe
der abgeleiteten Klasse SimpleDateFormat einen individuellen Stil definieren. SimpleDateFormat
besitzt einen Konstruktor
SimpleDateFormat(String format, Locale loc)
der neben der Angabe der Lokale auch einen Formatstring erwartet. Dieser String enthält feste
datums- und zeitrelevante Formatanweisungen und darf beliebig durch weitere Zeichenfolgen,
die in ' ' eingeschlossen sind, unterbrochen sein.
SimpleDateFormat df = new SimpleDateFormat("'Heute ist der 'dd'. 'MMMM");
Dieser Aufruf erzeugt eine Ausgabe der Art:
"Heute ist der 12. Juni".
Die wichtigsten Formatanweisungen für Datum und Zeit lauten:

Format Beschreibung
G »v. Chr.« oder »n. Chr«
(in englischsprachigen Lokalen »BC« oder »AD«)
yy Jahr, zweistellig
yyyy Jahr, vierstellig
M, MM Monat, einstellig (soweit möglich) bzw. immer zweistellig (01, 02 ...)
MMM Monat als 3-Buchstaben-Kurzform
MMMM voller Monatsname
w, ww Woche im Jahr, einstellig (soweit möglich) bzw. immer zweistellig (01, 02 ...)
W Woche im Monat
D, DD, DDD Tag im Jahr, einstellig bzw. zweistellig (soweit möglich) oder immer dreistellig (001,
002 ...)
d, dd Tag im Monat, einstellig (soweit möglich) bzw. immer zweistellig (01, 02 ...)
E Wochentag-Kürzel: »Mo«, »Di«, »Mi«, »Do«, »Fr«, »Sa«, »So«
(für englischsprachige Lokale werden dreibuchstabige Kürzel verwendet)
EEEE Wochentag (ausgeschrieben)
a »AM« oder »PM«
H, HH Stunde (0-23), einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)
h, hh Stunde (1-12), einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)
K, KK Stunde (1-24), einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)
k, kk Stunde (1-12), einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)

Tabelle 18: SimpleDateFormat-Formatanweisungen


128 >> Wochentage oder Monatsnamen auflisten

Format Beschreibung
m, mm Minuten, einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)
s, ss Sekunden, einstellig (soweit möglich) bzw. immer zweistellig (00, 01 ...)
Millisekunden, einstellig bzw. zweistellig (soweit möglich) oder immer dreistellig (001,
Datum und Uhrzeit

S, SS, SSS
002 ...)
z Zeitzone
Z Zeitzone (gemäß RFC 822)

Tabelle 18: SimpleDateFormat-Formatanweisungen (Forts.)

42 Wochentage oder Monatsnamen auflisten


Manchmal ist es nötig, die Namen der Wochentage oder Monate aufzulisten – beispielsweise
um sie über ein Listenfeld zur Auswahl anzubieten, sie in eine Tabelle einzubauen oder Ähn-
liches. Die Strings mit den Namen der Wochentage oder Monate können Sie selbst aufsetzen …
oder sich von einer passenden Java-Klasse zurückliefern lassen. Letztere Vorgehensweise pro-
duziert in der Regel weniger Code und gestattet Ihnen zudem die Wochentage und Monate in
der Sprache des aktuellen Systems anzuzeigen.
Bleibt noch zu klären, von welcher Klasse Sie sich die Namen der Wochentage und Monate
zurückliefern lassen. Im Paket java.text gibt es eine Klasse namens DateFormatSymbols, die
alle wichtigen Bestandteile von Datums- oder Zeitangaben in lokalisierter Form zurückliefert.
Zwar empfiehlt die API-Dokumentation diese Klasse nicht direkt zu verwenden, doch gilt dies
vornehmlich für die Formatierung von Datums- bzw. Uhrzeitstrings. (Für diese Aufgabe
bedient man sich besser eines DateFormat-Objekts, siehe Rezept 41, welches dann intern mit
DateFormatSymbols arbeitet.) Für die Abfrage der lokalisierten Wochentags- und Monatsnamen
gibt es hingegen kaum etwas Besseres als die DateFormatSymbols-Methoden getWeekdays() und
getMonths():
String[] getWeekdays() // liefert die Wochentagsnamen
String[] getShortWeekdays() // liefert die Kurzformen der Wochentagsnamen
String[] getMonths() // liefert die Monatsnamen
String[] getShortMonths() // liefert die Kurzformen der Monatsnamen

String-Arrays der Monats- oder Wochentagsnamen anlegen


Der Einsatz der Methoden ist denkbar einfach. Zuerst erzeugen Sie ein DateFormatSymbols-
Objekt, dann rufen Sie eine der Methoden auf und erhalten ein String-Array mit den Namen
der Wochentage von Sonntag bis Samstag bzw. der Monatsnamen von Januar bis Dezember
zurück.
// Wochentage in der Sprache des Systems
DateFormatSymbols dfs = new DateFormatSymbols();
String[] weekdayNames = dfs.getWeekdays();
for(String n : weekdayNames)
System.console().printf("\t%s%n", n);
>> Datum und Uhrzeit 129

Die Sprache, in der die Namen zurückgeliefert werden, legen Sie bei der Instanzierung des
DateFormatSymbols-Objekts fest. Wenn Sie den Konstruktor wie oben ohne Argument aufrufen,
wird die Sprache des aktuellen Systems verwendet.1 Wenn Sie dem Konstruktor eine Lokale
übergeben, wird die Sprache dieser Lokale verwendet.

Datum und Uhrzeit


// Monate in französisch
DateFormatSymbols dfs = new DateFormatSymbols(new Locale("fr", "FR"));
String[] monthNames = dfs.getMonths();

Wenn Sie zu einem bestimmten Datum den lokalisierten Monats- oder Wochentagsna-
H in we is

men abfragen möchten, können Sie seit Java 6 dazu die Calendar-Methode getDisplay-
Name() verwenden:
String weekdayName = calendar.getDisplayName(Calendar.DAY_OF_WEEK,
Calendar.LONG,
Locale.getDefault());
String monthName = calendar.getDisplayName(Calendar.MONTH,
Calendar.LONG,
new Locale("no", "NO"));
Von der Schwestermethode getDisplayNames() können Sie sich eine Map<String, Inte-
ger>-Collection der Monats- oder Wochentagsnamen zurückliefern lassen. Allerdings
verwendet diese Collection die Namen als Schlüssel und da sie zudem ungeordnet ist,
eignet sie sich nicht zum Aufbau von Listenfeldern oder ähnlichen geordneten Auflis-
tungen der Wochentage oder Monate.

Listenfelder mit Monats- oder Wochentagsnamen


Der Code zum Aufbau eines Listenfelds mit Wochentagsnamen könnte wie folgt aussehen:

// Listenfeld anlegen und mit Wochentagsnamen füllen


DateFormatSymbols dfs = new DateFormatSymbols();
JList weekdayList = new JList(dfs.getWeekdays());

// Listenfeld konfigurieren
weekdayList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
weekdayList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
JList list = (JList) e.getSource();
String s = (String) list.getSelectedValue();

if (s != null) {
lb3.setText("Ausgew. Wochentag: " + s);
}
}
});

// Listenfeld mit Bildlaufleiste ausstatten


JScrollPane sp1 = new JScrollPane(weekdayList);

Listing 48: Listenfeld mit Wochentagsnamen

1. Um exakt zu sein: Es wird die Sprache der Standardlokale verwendet. Die Standardlokale spiegelt allerdings die Sys-
temkonfiguration wider, es sei denn, Sie hätten sie zuvor umgestellt, siehe Rezept 215.
130 >> Datumseingaben einlesen und auf Gültigkeit prüfen

sp1.setBounds(new Rectangle(30, 40, 200, 100));

// JScrollPane mit Listenfeld in Formular einfügen


getContentPane().add(sp1);
Datum und Uhrzeit

Listing 48: Listenfeld mit Wochentagsnamen (Forts.)

Listenfelder mit den Monatsnamen können Sie analog erzeugen. Sie müssen nur zum Füllen
des Listenfelds die DateFormatSymbols-Methode getMonths() aufrufen.

43 Datumseingaben einlesen und auf Gültigkeit prüfen


Zum Einlesen von Datumseingaben benutzt man am besten eine der parse()-Methoden von
DateFormat:
Date parse(String source)
Date parse(String source, ParsePosition pos)
So wie die format()-Methode von DateFormat ein Date-Objekt anhand der eingestellten Lokale
und dem ausgewählten Pattern in einen String formatiert, analysieren die parse()-Methoden
einen gegebenen String, ob er ein Datum enthält, das Lokale und Muster entspricht. Wenn ja,
liefern sie das Datum als Date-Objekt zurück. Enthält der übergebene String keine passende
Datumsangabe, löst die erste Version eine ParseException aus. Die zweite Version, welche ab
der Position pos sucht, liefert null zurück.
Der folgende Code liest deutsche Datumseingaben im MEDIUM-Format (TT.MM.JJJJ) ein und
gibt sie zur Kontrolle im FULL-Format aus. Für Ein- und Ausgabe werden daher unterschied-
liche DateFormat-Instanzen (parser und formatter) erzeugt:
Date date = null;
DateFormat parser =
DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.GERMANY);
DateFormat formatter =
DateFormat.getDateInstance(DateFormat.FULL, Locale.GERMANY);

try {
// Datum aus Kommandozeile einlesen
date = parser.parse(args[0]);

// Datum auf Konsole ausgeben (verwendet System.console() anstelle von


// System.out, um evt. enthaltene Umlaute korrekt auszugeben
// (siehe Rezept 85)
System.console().printf("\n %s \n", formatter.format(date));

} catch(ParseException e) {
System.err.println(" Kein gueltiges Datum (TT.MM.JJJJ)");
}
Eine Beschreibung der vordefinierten DateFormat-Formate sowie der Definition eigener For-
mate mit SimpleDateFormat finden Sie in Rezept 41. Ein Beispiel für das Einlesen von Datums-
werten mit eigenen SimpleDateFormaten finden Sie im Start-Programm zu diesem Rezept.
>> Datum und Uhrzeit 131

Beim Parsen spielt es grundsätzlich keine Rolle, wie oft ein bedeutungstragender Buch-
A c h tun g

stabe, etwa das M für Monatsangaben, wiederholt wird. Während »MM« bei der Forma-
tierung eine zweistellige Ausgabe erzwingt, sind für den Parser »M« und »MM« gleich,
d.h., er liest die Anzahl der Monate – egal aus wie vielen Ziffern die Angabe besteht.

Datum und Uhrzeit


(Tatsächlich können sogar Werte wie 35 oder 123 übergeben werden. Der überzählige
Betrag wird in die nächsthöhere Einheit, für Monate also Jahre, umgerechnet. Wenn
Sie dieses Verhalten unterbinden wollen, rufen Sie setLenient(false) auf.)
Eine Ausnahme bilden die Jahresangaben. Zweistellige Jahresangaben werden beim
Parsen als Kürzel für vierstellige Jahresangaben angesehen und so ergänzt, dass das
sich ergebende Datum nicht mehr als 80 Jahre vor und nicht weiter als 20 Jahre hinter
dem aktuellen Datum liegt. Angenommen, das Programm wird am 05. April 2005 aus-
geführt. Die Eingabe 05.04.24 wird dann als 5. April 2024 geparst. Auch die Eingabe
05.04.25 wird noch ins 21. Jahrhundert verlegt, während die Eingabe 06.04.25 bereits
als 6. April 1925 interpretiert wird.
Jahresangaben aus einem oder mehr als zwei Buchstaben (»y«, »yyyy«) werden immer
unverändert übernommen.

Abbildung 26: Einlesen von Datumseingaben im DateFormat.MEDIUM-Format für die


deutsche Lokale. Der dritte Aufruf demonstriert, wie zu große Werte
in die nächsthöhere Einheit umgerechnet werden. Der vierte Aufruf
zeigt, wie Datumsangaben im angelsächsischen Format abgewiesen werden.

44 Datumswerte vergleichen
Um festzustellen, ob zwei Datumswerte (gegeben als Date- oder Calendar-Objekt) gleich sind,
brauchen Sie nur die Methode equals() aufzurufen:
// gegeben Date t1 und t2
if (t1.equals(t2))
System.out.println("gleiche Datumswerte");
Um zu prüfen, ob ein Datum zeitlich vor oder nach einem zweiten Datum liegt, stehen Ihnen
neben compareTo() die speziellen Methoden before() und after() zur Verfügung:
// gegeben Calendar t1 und t2
if (t1.after(t2) )
System.out.println("\t t1 liegt nach t2");
132 >> Datumswerte vergleichen

Date-/Calendar-Methode Beschreibung
boolean equals(Object) Liefert true, wenn der aktuelle Datumswert und das übergebene
Datum identisch sind.
Bei Date ist dies der Fall, wenn beide Objekte vom Typ Date sind
Datum und Uhrzeit

und auf derselben Anzahl Millisekunden basieren.


Bei Calendar ist dies der Fall, wenn beide Objekte vom Typ Calen-
dar sind, auf derselben Anzahl Millisekunden basieren und
bestimmte Calendar-Charakteristika sowie die Zeitzone überein-
stimmen.
int compareTo(Date/Object) Liefert -1, 0 oder 1 zurück, je nachdem, ob der aktuelle Datums-
wert kleiner, gleich oder größer dem übergebenen Wert ist.
boolean before(Date/Object) Liefert true, wenn der aktuelle Datumswert zeitlich vor dem über-
gebenen Datum liegt.
boolean after(Date/Object) Liefert true, wenn der aktuelle Datumswert zeitlich nach dem
übergebenen Datum liegt.

Tabelle 19: Vergleichsmethoden für Datumswerte

Vergleiche unter Ausschluss der Uhrzeit


Die vordefinierten Vergleichsmethoden der Klasse Date und Calendar basieren allesamt auf der
Anzahl Millisekunden seit dem 01.01.1970 00:00:00 Uhr, GMT. Sie sind also nicht geeignet,
wenn Sie feststellen möchten, ob zwei Datumswerte denselben Tag (ohne Berücksichtigung
der Uhrzeit) bezeichnen:
// Datumsobjekt für den 5. April 2005, 12 Uhr
Calendar t1 = Calendar.getInstance();
t1.set(2005, 3, 5, 12, 0, 0);

// Datumsobjekt für den 5. April 2005, 13 Uhr


Calendar t2 = Calendar.getInstance();

System.out.println("Vergleich mit equals() : " + t1.equals(t2)); // false


System.out.println("Vergleich mit compareTo(): " + t1.compareTo(t2)); // -1
Um Datumswerte ohne Berücksichtigung der Uhrzeit vergleichen zu können, bedarf es dem-
nach eigener Hilfsmethoden:
/**
* Prüft, ob zwei Calendar-Objekte den gleichen Tag im Kalender bezeichnen
*/
public static boolean equalDays(Calendar t1, Calendar t2) {
return (t1.get(Calendar.YEAR) == t2.get(Calendar.YEAR))
&& (t1.get(Calendar.MONTH) == t2.get(Calendar.MONTH))
&& (t1.get(Calendar.DAY_OF_MONTH) == t2.get(Calendar.DAY_OF_MONTH));
}
Die Methode equalDays() prüft paarweise, ob Jahr, Monat und Tag der beiden Calendar-
Objekte übereinstimmen. Wenn ja, liefert sie true zurück.
/**
* Prüft, ob zwei Calendar-Objekte den gleichen Tag bezeichnen
*/
public static int compareDays(Calendar t1, Calendar t2) {
>> Datum und Uhrzeit 133

Calendar clone1 = (Calendar) t1;


clone1.set(t1.get(Calendar.YEAR), t1.get(Calendar.MONTH),
t1.get(Calendar.DATE), 0, 0, 0);
clone1.clear(Calendar.MILLISECOND);

Datum und Uhrzeit


Calendar clone2 = (Calendar) t2;
clone2.set(t2.get(Calendar.YEAR), t2.get(Calendar.MONTH),
t2.get(Calendar.DATE), 0, 0, 0);
clone2.clear(Calendar.MILLISECOND);

return clone1.compareTo(clone2);
}
Die Methode compareDays() nutzt einen anderen Ansatz als equalDays(). Sie legt Kopien der
übergebenen Calendar-Objekte an und setzt für diese die Werte der Stunden, Minuten, Sekun-
den (set()-Aufruf) sowie Millisekunden (clear()-Aufruf) auf 0. Dann vergleicht sie die Klone
mit Calendar.compareTo() und liefert das Ergebnis zurück.

45 Differenz zwischen zwei Datumswerten berechnen


Wie Sie die Differenz zwischen zwei Datumswerten berechnen, hängt vor allem davon ab,
wozu Sie die Differenz benötigen und was Sie daraus ablesen wollen. Geht es lediglich darum,
ein Maß für den zeitlichen Abstand zwischen zwei Datumswerten zu erhalten, genügt es, sich
die Datumswerte als Anzahl Millisekunden, die seit dem 01.01.1970 00:00:00 Uhr, GMT, ver-
gangen sind, zurückliefern zu lassen und voneinander zu subtrahieren:
long diff = Math.abs( date1.getTimeInMillis() - date2.getTimeInMillis() );

Wenn Sie mit Calendar-Objekten arbeiten, erhalten Sie die Anzahl Millisekunden von
H i n we i s

der Methode getTimeInMillis(). Für Date-Objekte rufen Sie stattdessen getTime() auf.

Uhrzeit ausschalten
Datumswerte, ob sie nun durch ein Objekt der Klasse Date oder Calendar repräsentiert werden,
schließen immer auch eine Uhrzeit ein. Wenn Sie Datumsdifferenzen ohne Berücksichtigung
der Uhrzeit berechnen wollen, müssen Sie die Uhrzeit für alle Datumswerte auf einen gemein-
samen Wert setzen.
Wenn Sie ein neues GregorianCalendar-Objekt für ein bestimmtes Datum setzen und nur die
Daten für Jahr, Monat und Tag angeben, werden die Uhrzeitfelder automatisch auf 0 gesetzt.
Um Ihre Intention deutlicher im Quelltext widerzuspiegeln, können Sie die Felder für Stunden,
Minuten und Sekunden aber auch explizit auf 0 setzen:
GregorianCalendar date1 = new GregorianCalendar(2002, 5, 1);
GregorianCalendar date2 = new GregorianCalendar(2002, 5, 1, 0, 0, 0);
Für bestehende Calendar-Objekte können Sie die Uhrzeitfelder mit Hilfe von set() oder
clear() auf 0 setzen:
// Aktuelles Datum
Calendar today = Calendar.getInstance();
134 >> Differenz zwischen zwei Datumswerten in Jahren, Tagen und Stunden berechnen

// Stunden, Minuten und Sekunden auf 0 setzen


today.set(today.get(Calendar.YEAR), today.get(Calendar.MONTH),
today.get(Calendar.DATE), 0, 0, 0);
// Millisekunden auf 0 setzen
today.clear(Calendar.MILLISECOND);
Datum und Uhrzeit

Eine Beschreibung der verschiedenen Datums- und Uhrzeitfelder finden Sie in Tabelle 15 aus
Rezept 40.

46 Differenz zwischen zwei Datumswerten in Jahren,


Tagen und Stunden berechnen
Weit komplizierter ist es, die Differenz zwischen zwei Datumswerten aufgeschlüsselt in Jahre,
Tage, Stunden etc. anzugeben. Daran sind vor allem zwei Umstände Schuld:
왘 Die Sommerzeit.
Wenn zwei Datumswerte verglichen werden, von denen einer innerhalb und der andere
außerhalb der Sommerzeit liegt, führt die Sommerzeitverschiebung zu eventuell uner-
wünschten Differenzberechnungen.
In Deutschland beginnt die Sommerzeit am 27. März. Um zwei Uhr nachts wird die Uhr um
1 Stunde vorgestellt. Die Folge: Zwischen dem 27. März 00:00 Uhr und dem 28. März
00:00 Uhr liegen tatsächlich nur 23 Stunden. Trotzdem entspricht dies kalendarisch einem
vollen Tag! Wie also sollte ein Programm diese Differenz anzeigen: als 23 h oder als 1 d?
왘 Die unterschiedlichen Längen der Monate und Jahre.
Wenn Sie eine Differenz in Jahren und/oder Monaten ausdrücken möchten, stehen Sie vor
der Entscheidung, ob Sie mit festen Längen rechnen wollen (1 Jahr = 365 Tage, 1 Monat =
30 Tage oder auch 1 Jahr = 365,25 Tage, 1 Monat = 30,4 Tage) oder ob Sie die exakten
Längen berücksichtigen.

Die Klasse TimeSpan dient sowohl der Repräsentation als auch der Berechnung von Datumsdif-
ferenzen. In ihren private-Feldern speichert sie die Differenz zwischen zwei Datumswerten
sowohl in Sekunden (diff) als auch ausgedrückt als Kombination aus Jahren, Tagen, Stunden,
Minuten und Sekunden.
TimeSpan-Objekte können auf zweierlei Weise erzeugt werden:
왘 indem Sie den public-Konstruktor aufrufen und die Differenz selbst als Kombination aus
Jahren, Tagen, Stunden, Minuten und Sekunden übergeben:
TimeSpan ts = new TimeSpan(0, 1, 2, 0, 0);
왘 indem Sie die Methode getInstance() aufrufen und dieser zwei GregorianCalendar-Objekte
übergeben, sowie boolesche Werte, die der Methode mitteilen, ob bei der Berechnung der
Differenz auf Sommerzeit und Schaltjahre zu achten ist:
GregorianCalendar time1 = new GregorianCalendar(2005, 2, 26);
GregorianCalendar time2 = new GregorianCalendar(2005, 2, 27);
TimeSpan ts = TimeSpan.getInstance(time1, time2, true, true);

Die Differenz, die ein TimeSpan-Objekt repräsentiert, können Sie auf zweierlei Weise abfragen:
왘 Mit Hilfe der get-Methoden (getYears(), getDays() etc.) lassen Sie sich die Werte der zu-
gehörigen Felder zurückliefern und erhalten so die Kombination aus Jahren, Tagen, Stun-
>> Datum und Uhrzeit 135

den, Minuten und Sekunden, aus denen sich die Differenz zusammensetzt. (Die Methode
toString() liefert auf diese Weise die Differenz als String zurück – nur dass sie natürlich
direkt auf die Feldwerte zugreift.)
왘 Mittels der in-Methoden (inYears(), inWeeks() etc.) können Sie sich die Differenz ausge-

Datum und Uhrzeit


drückt in ganzzahligen Werten einer einzelnen Einheit (also beispielsweise in Jahren oder
Tagen) zurückliefern lassen.

Der Quelltext der Klasse TimeSpan sieht folgendermaßen aus:

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
* Klasse zur Repräsentation und Berechnung von Zeitabständen
* zwischen zwei Datumsangaben
*/
public class TimeSpan {
private int years;
private int days;
private int hours;
private int minutes;
private int seconds;
private long diff;

// public Konstruktor
public TimeSpan(int years, int days, int hours,
int minutes, int seconds) {
this.years = years;
this.days = days;
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
diff = seconds + 60*minutes + 60*60*hours
+ 24*60*60*days + 365*24*60*60*years;
}

// protected Konstruktor, wird von getInstance() verwendet


protected TimeSpan(int years, int days, int hours,
int minutes, int seconds, long diff) {
this.years = years;
this.days = days;
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
this.diff = diff;
}

// Erzeugt aus zwei GregorianCalendar-Objekten


// ein TimeSpan-Objekt

Listing 49: Die Klasse TimeSpan


136 >> Differenz zwischen zwei Datumswerten in Jahren, Tagen und Stunden berechnen

public static TimeSpan getInstance(GregorianCalendar t1,


GregorianCalendar t2,
boolean summer, boolean leap) {
// siehe unten
}
Datum und Uhrzeit

public int getYears() { return years; }


public int getDays() { return days; }
public int getHours() { return hours; }
public int getMinutes() { return minutes; }
public int getSeconds() { return seconds; }

public int inYears() { return (int) (diff / (60 * 60 * 24 * 365)); }


public int inWeeks() { return (int) (diff / (60 * 60 * 24 * 7)); }
public int inDays() { return (int) (diff / (60 * 60 * 24)); }
public int inHours() { return (int) (diff / (60 * 60)); }
public int inMinutes() { return (int) (diff / 60); }
public int inSeconds() { return (int) (diff); }

public String toString() {


StringBuilder s = new StringBuilder("");

if(years > 0) s.append(years + " j ");


if(days > 0) s.append(days + " t ");
if(hours > 0) s.append(hours + " std ");
if(minutes > 0) s.append(minutes + " min ");
if(seconds > 0) s.append(seconds + " sec ");

if (s.toString().equals("") )
s.append("Kein Zeitunterschied");

return s.toString();
}
}

Listing 49: Die Klasse TimeSpan (Forts.)

Am interessantesten ist zweifelsohne die Methode getInstance(), die die Differenz zwischen
zwei GregorianCalendar-Objekten berechnet, indem Sie die Differenz aus den Millisekunden-
Werten (zurückgeliefert von getTimeInMillis()) berechnet, diesen Wert durch Division mit
1000 in Sekunden umrechnet und dann durch sukzessive Modulo-Berechnung und Division in
Sekunden, Minuten, Stunden, Tage und Jahr zerlegt. Am Beispiel der Berechnung des Sekun-
denanteils möchte ich dies kurz erläutern:
Nachdem die Methode die Differenz durch 1000 dividiert hat, speichert sie den sich ergeben-
den Wert in der lokalen long-Variable diff, die somit anfangs die Differenz in Sekunden ent-
hält.
diff = (last.getTimeInMillis() - first.getTimeInMillis())/1000;
Rechnet man diff%60 (Modulo = Rest der Division durch 60), erhält man den Sekundenanteil:
int seconds = (int) (diff%60);
>> Datum und Uhrzeit 137

Anschließend wird diff durch 60 dividiert und das Ergebnis zurück in diff gespeichert.
diff /= 60;
Jetzt speichert diff die Differenz in ganzen Minuten (ohne den Rest Sekunden).

Datum und Uhrzeit


public static TimeSpan getInstance(GregorianCalendar t1,
GregorianCalendar t2,
boolean summer, boolean leap) {
GregorianCalendar first, last;
TimeZone tz;
long diff, save;

// Immer frühere Zeit von späteren Zeit abziehen


if (t1.getTimeInMillis() > t2.getTimeInMillis()) {
last = t1;
first = t2;
} else {
last = t2;
first = t1;
}

// Differenz in Sekunden
diff = (last.getTimeInMillis() - first.getTimeInMillis())/1000;

if (summer) { // Sommerzeit ausgleichen


tz = first.getTimeZone();
if( !(tz.inDaylightTime(first.getTime()))
&& (tz.inDaylightTime(last.getTime())) )
diff += tz.getDSTSavings()/1000;
if( (tz.inDaylightTime(first.getTime()))
&& !(tz.inDaylightTime(last.getTime())) )
diff -= tz.getDSTSavings()/1000;
}

save = diff;

// Sekunden, Minuten und Stunden berechnen


int seconds = (int) (diff%60); diff /= 60;
int minutes = (int) (diff%60); diff /= 60;
int hours = (int) (diff%24); diff /= 24;

// Jahre und Tage berechnen


int days = 0;
int years = 0;

if (leap) { // Schaltjahre ausgleichen


int startYear = 0, endYear = 0;
int leapDays = 0; // Schalttage in Zeitraum
int subtractLeapDays = 0; // abzuziehende Schalttage
// (da in Jahren enthalten)

Listing 50: Quelltext der Methode TimeSpan.getInstance()


138 >> Differenz zwischen zwei Datumswerten in Jahren, Tagen und Stunden berechnen

if( (first.get(Calendar.MONTH) < 1)


|| ( (first.get(Calendar.MONTH) == 1)
&& (first.get(Calendar.DAY_OF_MONTH) < 29)))
startYear = first.get(Calendar.YEAR);
Datum und Uhrzeit

else
startYear = first.get(Calendar.YEAR)+1;

if( (last.get(Calendar.MONTH) > 1)


|| ( (last.get(Calendar.MONTH) == 1)
&& (last.get(Calendar.DAY_OF_MONTH) == 29)))
endYear = last.get(Calendar.YEAR);
else
endYear = last.get(Calendar.YEAR)-1;

for(int i = startYear; i <= endYear; ++i)


if (first.isLeapYear(i))
++leapDays;

// Jahre berechnen
years = (int) ((diff-leapDays)/365);

// in Jahren enthaltene Schalttage


subtractLeapDays = (years+3)/4;
if (subtractLeapDays > leapDays)
subtractLeapDays = leapDays;

// Tage berechnen
days = (int) (diff - ((years*365) + subtractLeapDays));

} else {
days = (int) (diff%365);
years = (int) (diff/365);
}

return new TimeSpan(years, days, hours, minutes, seconds, (int) save);


}

Listing 50: Quelltext der Methode TimeSpan.getInstance() (Forts.)

Was zum Verständnis der getInstance()-Methode noch fehlt, ist die Berücksichtigung von
Sommerzeit und Schaltjahren.
Wird für den Parameter summer der Wert true übergeben, prüft die Methode, ob einer der
Datumswerte (aber nicht beide) in die Sommerzeit der aktuellen Zeitzone fallen. Dazu lässt sie
sich von der Calendar-Methode getTimeZone() ein TimeZone-Objekt zurückliefern, das die Zeit-
zone des Kalenders präsentiert, und übergibt nacheinander dessen inDaylightTime()-Methode
die zu kontrollierenden Datumswerte:
if (summer) { // Sommerzeit ausgleichen
tz = first.getTimeZone();
if( !(tz.inDaylightTime(first.getTime()))
&& (tz.inDaylightTime(last.getTime())) )
>> Datum und Uhrzeit 139

diff += tz.getDSTSavings()/1000;
if( (tz.inDaylightTime(first.getTime()))
&& !(tz.inDaylightTime(last.getTime())) )
diff -= tz.getDSTSavings()/1000;
}

Datum und Uhrzeit


Fällt tatsächlich einer der Datumswerte in die Sommerzeit und der andere nicht, gleicht die
Methode die Sommerzeitverschiebung aus, indem sie sich von der getDSTSavings()-Methode
des TimeZone-Objekts die Verschiebung in Millisekunden zurückliefern lässt und diesen Wert,
geteilt durch 1000, auf diff hinzuaddiert oder von diff abzieht. Die Differenz zwischen dem
27. März 00:00 Uhr und dem 28. März 00:00 Uhr wird dann beispielsweise als 1 Tag und nicht
als 23 Stunden berechnet.
Wird für den Parameter leap der Wert true übergeben, berücksichtigt die Methode in Zeitdiffe-
renzen, die sich über mehrere Jahre erstrecken, Schalttage. Schaltjahre, die in der Differenz
komplett enthalten sind, werden demnach als 366 Jahre angerechnet. So wird die Differenz
zwischen 01.02.2004 und dem 01.03.2004 zu 29 Tagen berechnet und die Differenz zwischen
dem 01.02.2004 und dem 01.02.2005 als genau 1 Jahr. In den meisten Fällen führt diese
Berechnung zu Ergebnissen, die man erwartet, sie zeitigt aber auch Merkwürdigkeiten. So
werden beispielsweise die Differenzen zwischen dem 28.02.2004 und dem 28.02.2005 zum
einen 29.02.2004 und dem 28.02.2005 zum anderen beide zu 1 Jahr berechnet.
Wenn Sie für den Parameter leap den Wert false übergeben, wird das Jahr immer als 365 Tage
aufgefasst.
Das Start-Programm zu diesem Rezept liest über die Befehlszeile zwei deutsche Datumsanga-
ben im Format TT.MM.JJJJ ein und berechnet die Differenz unter Berücksichtigung von Som-
merzeit und Schaltjahren.

import java.util.GregorianCalendar;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Locale;

public class Start {

public static void main(String args[]) {


DateFormat parser = DateFormat.getDateInstance(DateFormat.MEDIUM,
Locale.GERMANY);
GregorianCalendar time1 = new GregorianCalendar();
GregorianCalendar time2 = new GregorianCalendar();
TimeSpan ts;
System.out.println();

if (args.length != 2) {
System.out.println(" Aufruf: Start <Datum: TT.MM.JJJJ> "
+ "<Datum: TT.MM.JJJJ>");
System.exit(0);
}

try {

Listing 51: Differenz zwischen zwei Datumswerten berechnen


140 >> Differenz zwischen zwei Datumswerten in Tagen berechnen

time1.setTime(parser.parse(args[0]));
time2.setTime(parser.parse(args[1]));
ts = TimeSpan.getInstance(time1, time2, true, true);
System.out.println(" Differenz: " + ts);
Datum und Uhrzeit

} catch(ParseException e) {
System.err.println("\n Kein gueltiges Datum (TT.MM.JJJJ)");
}
}
}

Listing 51: Differenz zwischen zwei Datumswerten berechnen (Forts.)

Abbildung 27: Beispielaufrufe

47 Differenz zwischen zwei Datumswerten in Tagen


berechnen
Ein Tag besteht stets aus 24 Stunden, 24*60 Minuten, 24*60*60 Sekunden oder 24*60*60*1000
Millisekunden. Was liegt also näher, als die Differenz zwischen zwei Datumswerten zu berech-
nen, indem man die in den Calendar-Objekten gespeicherte Anzahl Millisekunden seit dem
01.01.1970 00:00:00 Uhr, GMT, abfragt, voneinander abzieht, durch 1000 und weiter noch
durch 24*60*60 dividiert?
// Vereinfachter Ansatz:
long diff = Math.abs((time2.getTimeInMillis()-time1.getTimeInMillis())/1000);
long diffInDays = diff/(60*60*24);
Das Problem an dieser Methode ist, dass die Sommerzeit nicht berücksichtigt wird. Stünde in
obigem Code time1 für den 27. März 00:00 Uhr und time2 für den 28. März 00:00 Uhr, wäre
diff lediglich gleich 23*60*60 und diffInDays ergäbe 0.
Um korrekte Ergebnisse zu erhalten, können Sie entweder die Zeitzone des Calendar-Objekts
auf eine TimeZone-Instanz umstellen, die keine Sommerzeit kennt (und zwar bevor in dem
Objekt die gewünschte Zeit gespeichert wird), die Sommerzeitverschiebung manuell korrigie-
ren (siehe Quelltext zu getInstance() aus Rezept 46) oder sich der in Rezept 46 definierten
TimeSpan-Klasse bedienen. In letzterem Fall müssen die beiden Datumswerte als Gregorian-
>> Datum und Uhrzeit 141

Calendar-Objekte vorliegen. Diese übergeben Sie dann an die Methode getInstance(), wobei
Sie als drittes Argument unbedingt true übergeben, damit die Sommerzeit berücksichtigt wird.
(Das vierte Argument, das die Berücksichtigung der Schalttage bei der Berechnung der Jahre
steuert, ist für die Differenz in Tagen unerheblich.)

Datum und Uhrzeit


import java.util.GregorianCalendar;

// gegeben GregorianCalendar time1 und time2


TimeSpan ts = TimeSpan.getInstance(time1, time2, true, true);
System.out.println("Differenz: " + ts.inDays());
Das Start-Programm zu diesem Rezept berechnet auf diese Weise die Differenz in Tagen zwi-
schen zwei Datumseingaben, die über die Befehlszeile entgegengenommen werden.

Die Realität ist leider oftmals komplizierter, als wir Programmierer es uns wünschen.
A c h t u ng

Tatsächlich ist in UTC (Coordinated Universal Time) nicht jeder Tag 24*60*60 Sekunden
lang. Alle ein oder zwei Jahre wird am Ende des 31. Dezember oder 30 Juni eine
Schaltsekunde eingefügt, so dass der Tag 24*60*60+1 Sekunden lang ist. Diese Korrek-
tur gleicht die auf einer Atomuhr basierende UTC-Zeit an die UT-Zeit (GMT) an, die auf
der Erdumdrehung beruht.

48 Tage zu einem Datum addieren/subtrahieren


In Sprachen, die die Überladung von Operatoren unterstützen, erwarten Programmieranfänger
häufig, dass man das Datum, welches in einem Objekt einer Datumsklasse gekapselt ist, mit
Hilfe überladener Operatoren inkrementieren oder um eine bestimmte Zahl Tage erhöhen
kann:
++date; // kein Java!
date = date + 3; // kein Java!
Nicht selten sehen sich die Adepten dann getäuscht, weil die zugrunde liegende Implementie-
rung nicht die Anzahl Tage, sondern die Anzahl Millisekunden, auf denen das Datum basiert,
erhöhen.
Nun, in Java gibt es keine überladenen Operatoren und obiger Fallstrick bleibt uns erspart.
Wie aber kann man in Java Tage zu einem bestehenden Datum hinzuaddieren oder davon
abziehen?
Wie Sie mittlerweile wissen, werden Datumswerte in Calendar-Objekten sowohl als Anzahl
Millisekunden als auch in Form von Datums- und Uhrzeitfeldern (Jahr, Monat, Wochentag,
Stunde etc.) gespeichert. Diese Felder können mit Hilfe der get-/set-Methoden der Klasse (siehe
Tabelle 15) abgefragt und gesetzt werden.
Eine Möglichkeit, Tage zu einem Datum zu addieren oder von einem Datum abzuziehen,
ist daher, set() für das Feld Calendar.DAY_OF_MONTH aufzurufen und diesem den alten Wert
(= get(Calendar.DAY_OF_MONTH)) plus der zu addierenden Anzahl Tage (negativer Wert für
Subtraktion) zu übergeben.
date.set(Calendar.DAY_OF_MONTH, date.get(Calendar.DAY_OF_MONTH) + days);
Einfacher noch geht es mit Hilfe der add()-Methode, der Sie nur noch das Feld und die zu
addierende Anzahl Tage (negativer Wert für Subtraktion) übergeben müssen:
date.add(Calendar.DAY_OF_MONTH, days);
142 >> Datum in julianischem Kalender

Kommt es zu einem Über- oder Unterlauf (die berechnete Anzahl Tage ist größer als die Tage
im aktuellen Monat bzw. kleiner als 1), passt add() die nächstgrößere Einheit an (für Tage also
das MONTH-Feld). Die set()-Methode passt die nächsthöhere Einheit nur dann an, wenn das
private-Feld lenient auf true steht (Standardwert, Einstellung über setLenient()). Ansonsten
wird eine Exception ausgelöst.
Datum und Uhrzeit

Mit der roll()-Methode schließlich können Sie den Wert eines Feldes ändern, ohne dass bei
Über- oder Unterlauf das nächsthöhere Feld angepasst wird.
date.roll(Calendar.DAY_OF_MONTH, days);

Methode Arbeitsweise
set(int field, int value) Anpassung der übergeordneten Einheit, wenn lenient = true,
ansonsten Auslösen einer Exception
add(int field, int value) Anpassung der übergeordneten Einheit
roll(int field, int value) Keine Anpassung der übergeordneten Einheit

Tabelle 20: Methoden zum Erhöhen bzw. Vermindern von Datumsfeldern

Das Start-Programm demonstriert die Arbeit von add() und roll(). Datum und die hinzuzu-
addierende Anzahl Tage werden als Argumente über die Befehlszeile übergeben.

Abbildung 28: Addieren und Subtrahieren von Tagen

49 Datum in julianischem Kalender


Sie benötigen ein Calendar-Objekt, welches den 27.04.2005 im julianischen Kalender reprä-
sentiert?
In diesem Fall reicht es nicht, einfach dem GregorianCalendar-Konstruktor Jahr, Monat (-1)
und Tag zu übergeben, da die Hybridimplementierung der Klasse GregorianCalendar standard-
mäßig Daten nach dem 15. Oktober 1582 als Daten im gregorianischen Kalender interpretiert
(vergleiche Rezept 40).
Stattdessen müssen Sie
1. ein neues GregorianCalendar-Objekt erzeugen:
GregorianCalendar jul = new GregorianCalendar();
>> Datum und Uhrzeit 143

2. dessen GregorianChange-Datum auf Date(Long.MAX_VALUE) einstellen:


jul.setGregorianChange(new Date(Long.MAX_VALUE));
3. Jahr, Monat und Tag für das Objekt setzen:
jul.set(2005, 3, 27);

Datum und Uhrzeit


Das neue Objekt repräsentiert nun das gewünschte Datum im julianischen Kalender. (Der
intern berechnete Millisekundenwert gibt also an, wie viele Sekunden das Datum vom
01.01.1970 00:00:00 Uhr, GMT, entfernt liegt.)

Wenn Sie das Datum mittels einer DateFormat-Instanz in einen String umwandeln
Achtung

möchten, müssen Sie beachten, dass die DateFormat-Instanz standardmäßig mit einer
GregorianCalendar-Instanz arbeitet, die für Datumswerte nach Oktober 1582 mit dem
gregorianischen Kalender arbeitet. Um das korrekte julianische Datum zu erhalten,
müssen Sie dem Formatierer eine Calendar-Instanz zuweisen, die für alle Datumswerte
nach dem julianischen Kalender rechnet, beispielsweise also jul:
DateFormat dfJul = DateFormat.getDateInstance(DateFormat.FULL);
dfJul.setCalendar(jul);
System.out.println(dfJul.format(jul.getTime()));

50 Umrechnen zwischen julianischem und


gregorianischem Kalender
Um ein Datum im julianischen Kalender in das zugehörige Datum im gregorianischen Kalen-
der umzuwandeln (so dass beide Daten gleich viele Millisekunden vom 01.01.1970 00:00:00
Uhr, GMT, entfernt liegen), gehen Sie am besten wie folgt vor:
1. Erzeugen Sie ein neues GregorianCalendar-Objekt:
GregorianCalendar gc = new GregorianCalendar();
2. Stellen Sie dessen GregorianChange-Datum auf Date(Long.MIN_VALUE) ein:
gc.setGregorianChange(new Date(Long.MAX_VALUE));
3. Setzen sie die interne Millisekundenzeit des Objekts auf die Anzahl Millisekunden des juli-
anischen Datums:
gc.setTimeInMillis(c.getTimeInMillis());
Wenn Sie ein Datum im gregorianischen Kalender in das zugehörige Datum im julianischen
Kalender umwandeln möchten, gehen Sie analog vor, nur dass Sie setGregorianChange() den
Date(Long.MAX_VALUE) Wert übergeben.

/**
* Gregorianisches Datum in julianisches Datum unwandeln
*/
public static GregorianCalendar gregorianToJulian(GregorianCalendar c) {

GregorianCalendar gc = new GregorianCalendar();


gc.setGregorianChange(new Date(Long.MAX_VALUE));

Listing 52: Methoden zur Umwandlung von Datumswerten zwischen julianischem und
gregorianischem Kalender
144 >> Ostersonntag berechnen

gc.setTimeInMillis(c.getTimeInMillis());

return gc;
}
Datum und Uhrzeit

/**
* Julianisches Datum in gregorianisches Datum unwandeln
*/
public static GregorianCalendar julianToGregorian(GregorianCalendar c) {

GregorianCalendar gc = new GregorianCalendar();


gc.setGregorianChange(new Date(Long.MIN_VALUE));
gc.setTimeInMillis(c.getTimeInMillis());

return gc;
}

Listing 52: Methoden zur Umwandlung von Datumswerten zwischen julianischem und
gregorianischem Kalender (Forts.)

Wenn Sie Datumswerte mittels einer DateFormat-Instanz in einen String umwandeln:


A cht un g

GregorianCalendar date = new GregorianCalendar();


DateFormat df = DateFormat.getDateInstance();
String s = df.format(date.getTime());
müssen Sie beachten, dass die DateFormat-Instanz das übergebene Datum als Date-Objekt
übernimmt und mittels einer eigenen Calendar-Instanz in Jahr, Monat etc. umrechnet.
Wenn Sie mit DateFormat Datumswerte umwandeln, für die Sie das GregorianChange-
Datum umgestellt haben, müssen Sie daher auch für das Calendar-Objekt der DateFor-
mat-Instanz das GregorianChange-Datum umstellen – oder es einfach durch das Grego-
rianCalendar-Objekt des Datums ersetzen:
GregorianCalendar jul = new GregorianCalendar();
jul.setGregorianChange(new Date(Long.MAX_VALUE));
DateFormat dfJul = DateFormat.getDateInstance(DateFormat.FULL);
dfJul.setCalendar(jul);

Das Start-Programm zu diesem Rezept liest ein Datum über die Befehlszeile ein und interpre-
tiert es einmal als gregorianisches und einmal als julianisches Datum, welche jeweils in ihre
julianische bzw. gregorianische Entsprechung umgerechnet werden.

51 Ostersonntag berechnen
Der Ostersonntag ist der Tag, an dem die Christen die Auferstehung Jesu Christi feiern. Gleich-
zeitig kennzeichnet er das Ende des österlichen Festkreises, der mit dem Aschermittwoch
beginnt.
Für den Programmierer ist der Ostersonntag insofern von zentraler Bedeutung, als er den
Referenzpunkt für die Berechnung der österlichen Feiertage darstellt: Aschermittwoch, Grün-
donnerstag, Karfreitag, Ostermontag, Christi Himmelfahrt, Pfingsten.
>> Datum und Uhrzeit 145

Der Ostertermin richtet sich nach dem jüdischen Pessachfest und wurde auf dem Konzil von
Nicäa 325 festgelegt als:
»Der 1. Sonntag, der dem ersten Pessach-Vollmond folgt.« – was auf der Nördlichen Halb-
kugel dem ersten Vollmond nach der Frühlings-Tag-und-Nachtgleiche entspricht.

Datum und Uhrzeit


Wegen dieses Bezugs auf den Vollmond ist die Berechnung des Ostersonntags recht kompli-
ziert. Traditionell erfolgte die Berechnung mit Hilfe des Mondkalenders und der goldenen Zahl
(die laufende Nummer eines Jahres im Mondzyklus). Heute gibt es eine Vielzahl von Algorith-
men zur Berechnung des Ostersonntags. Die bekanntesten sind die Algorithmen von Carl
Friedrich Gauß, Mallen und Oudin. Auf Letzterem basiert auch der in diesem Rezept imple-
mentierte Algorithmus:

import java.util.GregorianCalendar;

/**
* Datum des Ostersonntags im gregorianischen Kalender berechnen
*/
public static GregorianCalendar eastern(int year) {
int c = year/100;
int n = year - 19 * (year/19);
int k = (c - 17)/25;

int l1 = c - c/4 - (c-k)/3 + 19*n + 15;


int l2 = l1 - 30*(l1/30);
int l3 = l2 - (l2/28)*(1 - (l2/28) * (29/(l2+1)) * ((21-n)/11));

int a1 = year + year/4 + l3 + 2 - c + c/4;


int a2 = a1 - 7 * (a1/7);
int l = l3 - a2;

int month = 3 + (l + 40)/44;


int day = l + 28 - 31*(month/4);

return new GregorianCalendar(year, month-1, day);


}

Listing 53: Berechnung des Ostersonntags im gregorianischen Kalender

Wenn Sie historische Ostertermine berechnen, müssen Sie bedenken, dass der grego-
Achtung

rianische Kalender erst im Oktober 1582, in vielen Ländern sogar noch später, siehe
Tabelle 16 in Rezept 41, eingeführt wurde.
Wenn Sie zukünftige Ostertermine berechnen, müssen Sie bedenken, dass diese nur
nach geltender Konvention gültig sind. Bestrebungen, die Berechnung des Ostertermins
zu vereinfachen und das Datum auf einen bestimmten Sonntag festzuschreiben, gibt es
schon seit längerem. Bisher konnten die Kirchen diesbezüglich allerdings zu keiner
Einigung kommen.
146 >> Ostersonntag berechnen

Ostern in der orthodoxen Kirche


Vor der Einführung des gregorianischen Kalenders galt der julianische Kalender, nach dem
folglich auch Ostern berechnet wurde. Die meisten Länder stellten mit der Übernahme des gre-
gorianischen Kalenders auch die Berechnung des Ostersonntags auf den gregorianischen
Kalender um. Nicht so die orthodoxen Kirchen. Sie hingen nicht nur lange dem julianischen
Datum und Uhrzeit

Kalender an, siehe Tabelle 16 in Rezept 41, sondern behielten diesen für die Berechnung des
Ostersonntags sogar noch bis heute bei.
Die folgende Methode berechnet den Ostersonntag nach dem julianischen Kalender.

import java.util.Date;
import java.util.GregorianCalendar;

/**
* Datum des Ostersonntags im julianischen Kalender berechnen
*/
public static GregorianCalendar easternJulian(int year) {
int month, day;
int a = year%19;
int b = year%4;
int c = year%7;

int d = (19 * a + 15) % 30;


int e = (2*b + 4*c + 6*d + 6)%7;

if ((d+e) < 10) {


month = 3;
day = 22+d+e;
} else {
month = 4;
day = d+e-9;
}

GregorianCalendar gc = new GregorianCalendar();


gc.setGregorianChange(new Date(Long.MAX_VALUE));
gc.set(year, month-1, day, 0, 0, 0);

return gc;
}

Listing 54: Berechnung des Ostersonntags im julianischen Kalender

Beachten Sie, dass GregorianChange-Datum für das zurückgelieferte Calendar-Objekt auf


Date(Long.MAX_VALUE) gesetzt wurde, d.h., das Calendar-Objekt berechnet den Millisekunden-
wert, der dem übergebenen Datum entspricht, nach dem julianischen Kalender (siehe auch
Rezept 49).
Wenn Sie Jahr, Monat und Tag des Ostersonntags im julianischen Kalender aus dem zurück-
gelieferten Calendar-Objekt auslesen möchten, brauchen Sie daher nur die entsprechenden
Felder abzufragen, beispielsweise:
>> Datum und Uhrzeit 147

GregorianCalendar easternJ = MoreDate.easternJulian(year);


System.out.println(" " + easternJ.get(Calendar.YEAR)
+ " " + easternJ.get(Calendar.MONTH)
+ " " + easternJ.get(Calendar.DAY_OF_MONTH));
Wenn Sie das Datum des Ostersonntags im Julianischen Kalender mittels einer DateFormat-

Datum und Uhrzeit


Instanz in einen String umwandeln möchten, müssen Sie beachten, dass die DateFormat-
Instanz standardmäßig mit einer GregorianCalendar-Instanz arbeitet, die für Datumswerte
nach Oktober 1582 den gregorianischen Kalender zugrunde legt. Um das korrekte julianische
Datum zu erhalten, müssen Sie dem Formatierer eine Calendar-Instanz zuweisen, die für alle
Datumswerte nach dem julianischen Kalender rechnet, beispielsweise also das von More-
Date.easternJulian() zurückgelieferte Objekt:
easternJ = MoreDate.easternJulian(year);
df.setCalendar(easternJ);
System.console().printf("Orthod. Ostersonntag (Julian.): %s\n",
f.format(easternJ.getTime()));
Sicherlich wird es Sie aber auch interessieren, welchem Datum in unserem Kalender der ortho-
doxe Ostersonntag entspricht.
Dazu brauchen Sie easternJ nur mittels einer DateFormat-Instanz zu formatieren, deren Calendar-
Objekt nicht umgestellt wurde:
easternJ = MoreDate.easternJulian(year);
System.console().printf("Orthod. Ostersonntag (Gregor.): %s\n",
df.format(easternJ.getTime()));
Oder Sie erzeugen eine neue GregorianCalendar-Instanz und weisen dieser die Anzahl Millise-
kunden von easternJ zu. Dann können Sie das Datum auch durch Abfragen der Datumsfelder
auslesen:
GregorianCalendar gc = new GregorianCalendar();
gc.setTimeInMillis(easternJ.getTimeInMillis());
System.out.println(" " + gc.get(Calendar.YEAR) + " " + gc.get(Calendar.MONTH)
+ " " + gc.get(Calendar.DAY_OF_MONTH));
Das Start-Programm zu diesem Rezept demonstriert die Verwendung von MoreDate.eastern()
und MoreDate.easternJulian(). Das Programm nimmt über die Befehlszeile eine Jahreszahl
entgegen und gibt dazu das Datum des Ostersonntags aus.

import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.text.DateFormat;

public class Start {

public static void main(String args[]) {


GregorianCalendar eastern, easternJ;
DateFormat df = DateFormat.getDateInstance(DateFormat.FULL);
int year = 0;
System.out.println();

Listing 55: Berechnung des Ostersonntags


148 >> Deutsche Feiertage berechnen

if (args.length != 1) {
System.out.println(" Aufruf: Start <Jahreszahl>");
System.exit(0);
}
Datum und Uhrzeit

try {
year = Integer.parseInt(args[0]);

// Ostersonntag berechnen
eastern = MoreDate.eastern(year);
System.console().printf(" Ostersonntag: %s\n",
df.format(eastern.getTime()));

// Griech-orthodoxen Ostersonntag berechnen


// (julianischer Kalender)
easternJ = MoreDate.easternJulian(year);
df.setCalendar(easternJ);
System.console().printf("Orthod. Ostersonntag (julian.): %s\n",
df.format(easternJ.getTime()));
df.setCalendar(eastern);
System.console().printf("Orthod. Ostersonntag (gregor.): %s\n",
df.format(easternJ.getTime())); }
catch (NumberFormatException e) {
System.err.println(" Ungueltiges Argument");
}
}
}

Listing 55: Berechnung des Ostersonntags (Forts.)

Abbildung 29: Ostersonntage der Jahre 2006, 2007 und 2008

52 Deutsche Feiertage berechnen


Gäbe es nur feste Feiertage, wäre deren Berechnung ganz einfach – ja, eigentlich gäbe es gar nichts
mehr zu berechnen, denn Sie müssten lediglich für jeden Feiertag ein GregorianCalendar-Objekt
erzeugen und dem Konstruktor Jahr, Monat (0-11) und Tag des Feiertagsdatum übergeben.
>> Datum und Uhrzeit 149

Fakt ist aber, dass ungefähr die Hälfte aller Feiertage beweglich sind. Da wären zum einen die
große Gruppe der Feiertage, die von Osten abhängen, dann die Gruppe der Feiertage, die von
Weihnachten abhängen, und schließlich noch der Muttertag.
Letzterer ist im Übrigen kein echter Feiertag, aber wir wollen in diesem Rezept auch die Tage

Datum und Uhrzeit


berücksichtigen, denen eine besondere Bedeutung zukommt, auch wenn es sich nicht um
gesetzliche Feiertage handelt.

Feiertag abhängig von Datum


Neujahr – 01. Januar
Heilige drei Könige* – 06. Januar
Rosenmontag Ostersonntag Ostersonntag – 48 Tage
Fastnacht Ostersonntag Ostersonntag – 47 Tage
Aschermittwoch Ostersonntag Ostersonntag – 46 Tage
Valentinstag – 14. Februar
Gründonnerstag Ostersonntag Ostersonntag – 3 Tage
Karfreitag Ostersonntag Ostersonntag – 2 Tage
Ostersonntag Pessach-Vollmond 1. Sonntag, der dem ersten Pes-
sach-Vollmond folgt (siehe
Rezept 51)
Ostermontag Ostersonntag Ostersonntag + 1 Tag
Maifeiertag – 1. Mai
Himmelfahrt Ostersonntag Ostersonntag + 39 Tage
Muttertag 1. Mai 2. Sonntag im Mai
Pfingstsonntag Ostersonntag Ostersonntag + 49 Tage
Pfingstmontag Ostersonntag Ostersonntag + 50 Tage
Fronleichnam* Ostersonntag Ostersonntag + 60 Tage
Mariä Himmelfahrt* – 15. September
Tag der deutschen Einheit – 3. Oktober
Reformationstag* – 31. Oktober
Allerheiligen* – 1. November
Allerseelen – 2. November
Nikolaus – 6. Dezember
Sankt Martinstag – 11. November
Volkstrauertag Heiligabend Sonntag vor Totensonntag
Buß- und Bettag* Heiligabend Mittwoch vor Totensonntag
Totensonntag Heiligabend 7 Tage vor 1. Advent
1. Advent Heiligabend 7 Tage vor 2. Advent
2. Advent Heiligabend 7 Tage vor 3. Advent
3. Advent Heiligabend 7 Tage vor 4. Advent

Tabelle 21: Deutsche Feiertage (gesetzliche Feiertage sind farbig hervorgehoben, regionale
Feiertage sind mit * gekennzeichnet)
150 >> Deutsche Feiertage berechnen

Feiertag abhängig von Datum


4. Advent Heiligabend Sonntag vor Heiligabend
Heiligabend – 24. Dezember
1. Weihnachtstag – 25. Dezember
Datum und Uhrzeit

2. Weihnachtstag – 26. Dezember


Silvester – 31. Dezember

Tabelle 21: Deutsche Feiertage (gesetzliche Feiertage sind farbig hervorgehoben, regionale
Feiertage sind mit * gekennzeichnet) (Forts.)

Wie Sie der Tabelle entnehmen können, bereitet die Berechnung der Osterfeiertage, insbeson-
dere die Berechnung des Ostersonntags, die größte Schwierigkeit. Doch glücklicherweise
haben wir dieses Problem bereits im Rezept 51 gelöst. Die Berechnung der Feiertage reduziert
sich damit weitgehend auf die Erzeugung und Verwaltung der Feiertagsdaten. Der hier präsen-
tierte Ansatz basiert auf zwei Klassen:
왘 einer Klasse CalendarDay, deren Objekte die einzelnen Feiertage repräsentieren, und
왘 einer Klasse Holidays, die für ein gegebenes Jahr alle Feiertage berechnet und in einer
Vector-Collection speichert.

Die Klasse CalendarDay


Die Klasse CalendarDay speichert zu jedem Feiertag den Namen, das Datum (als Anzahl Milli-
sekunden), einen optionalen Kommentar, ob es sich um einen gesetzlichen nationalen Feiertag
handelt oder ob es ein regionaler Feiertag ist.

/**
* Klasse zum Speichern von Kalenderinformationen zu Kalendertagen
*/
public class CalendarDay {

private String name;


private long time;
private boolean holiday;
private boolean nationwide;
private String comment;

public CalendarDay(String name, long time, boolean holiday,


boolean nationwide, String comment) {
this.name = name;
this.time= time;
this.holiday = holiday;
this.nationwide = nationwide;
this.comment = comment;
}

public String getName() {


return name;

Listing 56: Die Klasse CalendarDay


>> Datum und Uhrzeit 151

public long getTime() {


return time;
}

Datum und Uhrzeit


public boolean getHoliday() {
return holiday;
}

public boolean getNationwide() {


return nationwide;
}

public String getComment() {


return comment;
}
}

Listing 56: Die Klasse CalendarDay (Forts.)

Die Klasse Holidays


Die Klasse berechnet und verwaltet die Feiertage eines gegebenen Jahres.
Das Jahr übergeben Sie als int-Wert dem Konstruktor, der daraufhin berechnet, auf welche
Datumswerte die Feiertage fallen, und für jeden Feiertag ein CalendarDay-Objekt erzeugt. Die
CalendarDay-Objekte werden zusammen in einer Vector-Collection gespeichert.

Die Methode eastern(), die vom Konstruktor zur Berechnung des Ostersonntags ver-
H i n we i s

wendet wird, ist in Holidays definiert und identisch zu der Methode aus Rezept 51.

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Vector;

/**
* Berechnet Feiertage eines Jahres und speichert die gewonnenen
* Informationen in einer Vector-Collection von CalendarDay-Objekten
*/
public class Holidays {
Vector<CalendarDay> days = new Vector<CalendarDay>(34);

public Holidays(int year) {


// Ostern vorab berechnen
GregorianCalendar eastern = eastern(year);
GregorianCalendar tmp;

Listing 57: Aus Holidays.java


152 >> Deutsche Feiertage berechnen

int day;

days.add(new CalendarDay("Neujahr",
(new GregorianCalendar(year,0,1)).getTimeInMillis(),
true, true, ""));
Datum und Uhrzeit

days.add(new CalendarDay("Heilige drei Könige",


(new GregorianCalendar(year,0,6)).getTimeInMillis(),
false, false, "in Baden-Würt., Bayern und Sachsen-A."));
tmp = (GregorianCalendar) eastern.clone();
tmp.add(Calendar.DAY_OF_MONTH, -48);
days.add(new CalendarDay("Rosenmontag",
tmp.getTimeInMillis(),
false, false, ""));
tmp.add(Calendar.DAY_OF_MONTH, +1);
days.add(new CalendarDay("Fastnacht",
tmp.getTimeInMillis(),
false, false, ""));
tmp.add(Calendar.DAY_OF_MONTH, +1);
days.add(new CalendarDay("Aschermittwoch",
tmp.getTimeInMillis(),
false, false, ""));
days.add(new CalendarDay("Valentinstag",
(new GregorianCalendar(year,1,14)).getTimeInMillis(),
false, false, ""));
tmp = (GregorianCalendar) eastern.clone();
tmp.add(Calendar.DAY_OF_MONTH, -3);
days.add(new CalendarDay("Gründonnerstag",
tmp.getTimeInMillis(),
false, false, ""));
tmp.add(Calendar.DAY_OF_MONTH, +1);
days.add(new CalendarDay("Karfreitag",
tmp.getTimeInMillis(),
true, true, ""));
days.add(new CalendarDay("Ostersonntag",
eastern.getTimeInMillis(),
true, true, ""));
tmp = (GregorianCalendar) eastern.clone();
tmp.add(Calendar.DAY_OF_MONTH, +1);
days.add(new CalendarDay("Ostermontag",
tmp.getTimeInMillis(),
true, true, ""));
days.add(new CalendarDay("Maifeiertag",
(new GregorianCalendar(year,4,1)).getTimeInMillis(),
true, true, ""));
tmp = (GregorianCalendar) eastern.clone();
tmp.add(Calendar.DAY_OF_MONTH, +39);
days.add(new CalendarDay("Himmelfahrt",
tmp.getTimeInMillis(),
true, true, ""));

// Muttertag = 2. Sonntag in Mai

Listing 57: Aus Holidays.java (Forts.)


>> Datum und Uhrzeit 153

GregorianCalendar firstMay = new GregorianCalendar(year, 4, 1);


day = firstMay.get(Calendar.DAY_OF_WEEK);
if (day == Calendar.SUNDAY)
day = 1 + 7;
else

Datum und Uhrzeit


day = 1 + (8-day) + 7;
days.add(new CalendarDay("Muttertag",
(new GregorianCalendar(year,4,day)).getTimeInMillis(),
false, false, ""));

tmp = (GregorianCalendar) eastern.clone();


tmp.add(Calendar.DAY_OF_MONTH, +49);
days.add(new CalendarDay("Pfingstsonntag",
tmp.getTimeInMillis(),
true, true, ""));
tmp.add(Calendar.DAY_OF_MONTH, +1);
days.add(new CalendarDay("Pfingstmontag",
tmp.getTimeInMillis(),
true, true, ""));
tmp.add(Calendar.DAY_OF_MONTH, +10);
days.add(new CalendarDay("Fronleichnam",
tmp.getTimeInMillis(),
true, false, "in Baden-Würt., Bayern, Hessen, NRW, "
+ "Rheinl.-Pfalz, Saarland, Sachsen (z.T.) "
+ "und Thüringen (z.T.)"));
days.add(new CalendarDay("Maria Himmelfahrt",
(new GregorianCalendar(year,7,15)).getTimeInMillis(),
false, false, "in Saarland und kathol. Gemeinden "
+ "von Bayern"));
days.add(new CalendarDay("Tag der Einheit",
(new GregorianCalendar(year,9,3)).getTimeInMillis(),
true, true, ""));
days.add(new CalendarDay("Reformationstag",
(new GregorianCalendar(year,9,31)).getTimeInMillis(),
true, false, "in Brandenburg, Meckl.-Vorp., Sachsen, "
+ "Sachsen-A. und Thüringen"));
days.add(new CalendarDay("Allerheiligen",
(new GregorianCalendar(year,10,1)).getTimeInMillis(),
true, false, "in Baden-Würt., Bayern, NRW, "
+ "Rheinl.-Pfalz und Saarland"));
days.add(new CalendarDay("Allerseelen",
(new GregorianCalendar(year,10,2)).getTimeInMillis(),
false, false, ""));
days.add(new CalendarDay("Martinstag",
(new GregorianCalendar(year,10,11)).getTimeInMillis(),
false, false, ""));

// ab hier nicht mehr chronologisch

// 4. Advent = 1. Sonntag vor 1. Weihnachtstag


GregorianCalendar advent = new GregorianCalendar(year, 11, 25);

Listing 57: Aus Holidays.java (Forts.)


154 >> Deutsche Feiertage berechnen

if (advent.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY)
advent.add(Calendar.DAY_OF_MONTH, -7);
else
advent.add(Calendar.DAY_OF_MONTH,
-advent.get(Calendar.DAY_OF_WEEK)+1);
Datum und Uhrzeit

days.add(new CalendarDay("4. Advent",


advent.getTimeInMillis(),
false, false, ""));

// 3. Advent = Eine Woche vor 4. Advent


advent.add(Calendar.DAY_OF_MONTH, -7);
days.add(new CalendarDay("3. Advent",
advent.getTimeInMillis(),
false, false, ""));
// 2. Advent = Eine Woche vor 3. Advent
advent.add(Calendar.DAY_OF_MONTH, -7);
days.add(new CalendarDay("2. Advent",
advent.getTimeInMillis(),
false, false, ""));
// 1. Advent = Eine Woche vor 2. Advent
advent.add(Calendar.DAY_OF_MONTH, -7);
days.add(new CalendarDay("1. Advent",
advent.getTimeInMillis(),
false, false, ""));

// Totensonntag = Sonntag vor 1. Advent


tmp = (GregorianCalendar) advent.clone();
tmp.add(Calendar.DAY_OF_MONTH, -7);
days.add(new CalendarDay("Totensonntag",
tmp.getTimeInMillis(),
false, false, ""));

// Volkstrauertag = Sonntag vor Totensonntag


tmp.add(Calendar.DAY_OF_MONTH, -7);
days.add(new CalendarDay("Volkstrauertag",
tmp.getTimeInMillis(),
false, false, ""));

// Buß- und Bettag = Mittwoch vor Totensonntag


day = tmp.get(Calendar.DAY_OF_WEEK);
if (day == Calendar.WEDNESDAY)
day = -(4+day);
else
day = (4-day);
tmp.add(Calendar.DAY_OF_MONTH, day);
days.add(new CalendarDay("Buß- und Bettag",
tmp.getTimeInMillis(),
false, false, "Sachsen"));

days.add(new CalendarDay("Nikolaus",
(new GregorianCalendar(year,11,6)).getTimeInMillis(),

Listing 57: Aus Holidays.java (Forts.)


>> Datum und Uhrzeit 155

false, false, ""));


days.add(new CalendarDay("Heiligabend",
(new GregorianCalendar(year,11,24)).getTimeInMillis(),
false, false, ""));
days.add(new CalendarDay("1. Weihnachtstag",

Datum und Uhrzeit


(new GregorianCalendar(year,11,25)).getTimeInMillis(),
true, true, ""));
days.add(new CalendarDay("2. Weihnachtstag",
(new GregorianCalendar(year,11,26)).getTimeInMillis(),
true, true, ""));
days.add(new CalendarDay("Silvester",
(new GregorianCalendar(year,11,31)).getTimeInMillis(),
false, false, ""));
}
...

Listing 57: Aus Holidays.java (Forts.)

Damit man mit der Klasse Holidays auch vernünftig arbeiten kann, definiert sie verschiedene
Methoden, mit denen der Benutzer Informationen über die Feiertage einholen kann:
왘 CalendarDay searchDay(String name)

Sucht zu einem gegebenen Feiertagsnamen (beispielsweise »Allerheiligen« das zugehörige


CalendarDay-Objekt und liefert es zurück. Alternative Namen werden zum Teil berücksichtigt.
Wurde kein passendes CalendarDay-Objekt gefunden, liefert die Methode null zurück.
왘 CalendarDay getDay(GregorianCalendar date)
Liefert zu einem gegebenen Datum das zugehörige CalendarDay-Objekt zurück bzw. null, wenn
kein passendes Objekt gefunden wurde.
왘 boolean isNationalHoliday(GregorianCalendar date)
Liefert true zurück, wenn auf das übergebene Datum ein gesetzlicher, nationaler Feiertag fällt.
왘 boolean isRegionalHoliday(GregorianCalendar date)
Liefert true zurück, wenn auf das übergebene Datum ein gesetzlicher, regionaler Feiertag fällt.

...
// Liefert das CalendarDay-Objekt zu einem Feiertag
public CalendarDay searchDay(String name) {
// Alternative Namen berücksichtigen
if (name.equals("Heilige drei Koenige"))
name = "Heilige drei Könige";
if (name.equals("Gruendonnerstag"))
name = "Gründonnerstag";
if (name.equals("Tag der Arbeit"))
name = "Maifeiertag";
if (name.equals("Christi Himmelfahrt"))
name = "Himmelfahrt";

Listing 58: Die Klasse Holidays


156 >> Deutsche Feiertage berechnen

if (name.equals("Vatertag"))
name = "Himmelfahrt";
if (name.equals("Tag der deutschen Einheit"))
name = "Tag der Einheit";
if (name.equals("Sankt Martin"))
Datum und Uhrzeit

name = "Martinstag";
if (name.equals("Vierter Advent"))
name = "4. Advent";
if (name.equals("Dritter Advent"))
name = "3. Advent";
if (name.equals("Zweiter Advent"))
name = "2. Advent";
if (name.equals("Erster Advent"))
name = "1. Advent";
if (name.equals("Buss- und Bettag"))
name = "Buß- und Bettag";
if (name.equals("Bettag"))
name = "Buß- und Bettag";
if (name.equals("Weihnachtsabend"))
name = "Heiligabend";
if (name.equals("Erster Weihnachtstag"))
name = "1. Weihnachtstag";
if (name.equals("Zweiter Weihnachtstag"))
name = "2. Weihnachtstag";

for(CalendarDay d : days) {
if (name.equals(d.getName()) )
return d;
}

return null;
}

// Liefert das CalendarDay-Objekt zu einem Kalenderdatum


public CalendarDay getDay(GregorianCalendar date) {
for(CalendarDay d : days) {
if( d.getTime() == date.getTimeInMillis() )
return d;
}

return null;
}

// Stellt fest, ob das angegebene Datum auf einen gesetzlichen, nationalen


// Feiertag fällt
public boolean isNationalHoliday(GregorianCalendar date) {
for(CalendarDay d : days) {
if( d.getTime() == date.getTimeInMillis()
&& d.getHoliday() && d.getNationwide() )
return true;

Listing 58: Die Klasse Holidays (Forts.)


>> Datum und Uhrzeit 157

return false;
}

Datum und Uhrzeit


// Stellt fest, ob das angegebene Datum auf einen gesetzlichen, regionalen
// Feiertag fällt
public boolean isRegionalHoliday(GregorianCalendar date) {
for(CalendarDay d : days) {
if( d.getTime() == date.getTimeInMillis()
&& d.getHoliday() && !d.getNationwide() )
return true;
}

return false;
}

public static GregorianCalendar eastern(int year) {


// siehe Rezept 51
}
}

Listing 58: Die Klasse Holidays (Forts.)

Zu diesem Rezept gibt es zwei Start-Programme. Beide nehmen die Jahreszahl, für die sie ein
Holidays-Objekt erzeugen, über die Befehlszeile entgegen.
왘 Mit Start1 können Sie die Feiertage eines Jahres auf die Konsole ausgeben oder in eine
Datei umleiten:
java Start1 > Feiertage.txt
왘 Mit Start2 können Sie abfragen, auf welches Datum ein bestimmter Feiertag im übergebe-
nen Jahr fällt. Der Name des Feiertags wird vom Programm abgefragt.2

Abbildung 30: Mit Start2 können Sie sich Feiertage in beliebigen Jahren2 berechnen lassen.

2. Immer vorausgesetzt, die entsprechenden Feiertage gibt es in dem betreffenden Jahr und an ihrer Berechnung hat
sich nichts geändert. (Denken Sie beispielsweise daran, dass es Bestrebungen gibt, das Osterdatum festzuschreiben.)
158 >> Ermitteln, welchen Wochentag ein Datum repräsentiert

53 Ermitteln, welchen Wochentag ein Datum repräsentiert


Welchem Wochentag ein Datum entspricht, ist im DAY_OF_WEEK-Feld des Calendar-Objekts
gespeichert. Für Instanzen von GregorianCalendar enthält dieses Feld eine der Konstanten SUN-
DAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY oder SATURDAY.
Datum und Uhrzeit

Den Wert des Felds können Sie für ein bestehendes Calendar-Objekt date wie folgt abfragen:
int day = date.get(Calendar.DAY_OF_WEEK);
Für die Umwandlung der DAY_OF_WEEK-Konstanten in Strings (»Sonntag«, »Montag« etc.) ist es
am einfachsten, ein Array der Wochentagsnamen zu definieren und den von get(Calendar.
DAY_OF_WEEK) zurückgelieferten String als Index in dieses Array zu verwenden. Sie müssen aller-
dings beachten, dass die DAY_OF_WEEK-Konstanten den Zahlen von 1 (SUNDAY) bis 7 (SATURDAY)
entsprechen, während Arrays mit 0 beginnend indiziert werden.
Das Start-Programm zu diesem Rezept, welches den Wochentag zu einem beliebigen Datum
ermittelt, demonstriert diese Technik:

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Locale;

public class Start {

public static void main(String args[]) {


DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM,
Locale.GERMANY);
GregorianCalendar date = new GregorianCalendar();
int day;
String[] weekdayNames = { "SONNTAG", "MONTAG", "DIENSTAG", "MITTWOCH",
"DONNERSTAG", "FREITAG", "SAMSTAG" };

System.out.println();

if (args.length != 1) {
System.out.println(" Aufruf: Start <Datum: TT.MM.JJJJ>");
System.exit(0);
}

try {
date.setTime(df.parse(args[0]));

day = date.get(Calendar.DAY_OF_WEEK);

System.out.println(" Wochentag: " + weekdayNames[day-1]);

} catch(ParseException e) {
System.err.println("\n Kein gueltiges Datum (TT.MM.JJJJ)");
}

Listing 59: Programm zur Berechnung des Wochentags


>> Datum und Uhrzeit 159

}
}

Listing 59: Programm zur Berechnung des Wochentags (Forts.)

Datum und Uhrzeit


Abbildung 31: Kann ein Maifeiertag schlechter liegen?

54 Ermitteln, ob ein Tag ein Feiertag ist


Mit Hilfe der Klasse Holidays aus Rezept 52 können Sie schnell prüfen, ob es sich bei einem
bestimmten Tag im Jahr um einen Feiertag handelt.
1. Zuerst erzeugen Sie für das gewünschte Jahr ein Holidays-Objekt.
Holidays holidays = new Holidays(2005);
2. Dann erzeugen Sie ein GregorianCalendar-Objekt für das zu untersuchende Datum.
GregorianCalendar date = new GregorianCalendar(2005, 3, 23);
3. Schließlich prüfen Sie mit Hilfe der entsprechenden Methoden des Holidays-Objekts, ob es
sich um einen Feiertag handelt.
Sie können dabei beispielsweise so vorgehen, dass Sie zuerst durch Aufruf von isNational-
Holiday() prüfen, ob es sich um einen nationalen gesetzlichen Feiertag handelt. Wenn
nicht, können Sie mit isRegionalHoliday() prüfen, ob es ein regionaler gesetzlicher Feier-
tag ist. Trifft auch dies nicht zu, können Sie mit getDay() prüfen, ob der Tag überhaupt als
besonderer Tag in dem holidays-Objekt gespeichert ist:
if(holidays.isNationalHoliday(date)) {
System.out.println("\t Nationaler Feiertag ");
} else if(holidays.isRegionalHoliday(date)) {
System.out.println("\t Regionaler Feiertag " );
} else if (holidays.getDay(date) != null) {
System.out.println("\t Besonderer Tag " );
} else
System.out.println("\t Kein Feiertag ");
Bezeichnet ein Datum einen Tag, der im Holidays-Objekt gespeichert ist, können Sie sich mit
getDay() die Referenz auf das zugehörige CalendarDay-Objekt zurückliefern lassen und die für
den Tag gespeicherten Informationen abfragen.
160 >> Ermitteln, ob ein Jahr ein Schaltjahr ist
Datum und Uhrzeit

Abbildung 32: Ermitteln, ob ein Tag ein Feiertag ist

55 Ermitteln, ob ein Jahr ein Schaltjahr ist


Ob ein gegebenes Jahr im gregorianischen Kalender ein Schaltjahr ist, lässt sich bequem mit
Hilfe der Methode isLeapYear() feststellen. Leider ist die Methode nicht statisch, so dass Sie
zum Aufruf ein GregorianCalendar-Objekt benötigen. Dieses muss aber nicht das zu prüfende
Jahr repräsentieren, die Jahreszahl wird vielmehr als Argument an den int-Parameter über-
geben.
GregorianCalendar date = new GregorianCalendar();
int year = 2005;

date.isLeapYear(year);
Mit dem Start-Programm zu diesem Rezept können Sie prüfen, ob ein Jahr im gregorianischen
Kalender ein Schaltjahr ist.

Abbildung 33: 1900 ist kein Schaltjahr, weil es durch 100 teilbar ist. 2000 ist ein Schaltjahr,
obwohl es durch 100 teilbar ist, weil es ein Vielfaches von 400 darstellt.
>> Datum und Uhrzeit 161

56 Alter aus Geburtsdatum berechnen


Die Berechnung des Alters, sei es nun das Alter eines Kunden, einer Ware oder einer beliebi-
gen Sache (wie z.B. Erfindungen), ist eine recht häufige Aufgabe. Wenn lediglich das Jahr der
»Geburt« bekannt ist, ist diese Aufgabe auch relativ schnell durch Differenzbildung der Jahres-

Datum und Uhrzeit


zahlen erledigt.
Liegt jedoch das komplette Geburtsdatum vor und ist dieses mit einem zweiten Datum, bei-
spielsweise dem aktuellen Datum, zu vergleichen, müssen Sie beachten, dass das Alter unter
Umständen um 1 geringer ist als die Differenz der Jahreszahlen – dann nämlich, wenn das
Vergleichsdatum in seinem Jahr weiter vorne liegt als das Geburtsdatum im Geburtsjahr. Die
Methode age() berücksichtigt dies:
/**
* Berechnet, welches Alter eine Person, die am birthdate
* geboren wurde, am otherDate hat
*/
public static int age(Calendar birthdate, Calendar otherDate) {
int age = 0;

// anderes Datum liegt vor Geburtsdatum


if (otherDate.before(birthdate))
return -1;

// Jahresunterschied berechnen
age = otherDate.get(Calendar.YEAR) - birthdate.get(Calendar.YEAR);

// Prüfen, ob Tag in otherDate vor Tag in birthdate liegt. Wenn ja,


// Alter um 1 Jahr vermindern
if ( (otherDate.get(Calendar.MONTH) < birthdate.get(Calendar.MONTH))
||(otherDate.get(Calendar.MONTH) == birthdate.get(Calendar.MONTH)
&& otherDate.get(Calendar.DAY_OF_MONTH) <
birthdate.get(Calendar.DAY_OF_MONTH)))
--age;

return age;
}
Vielleicht wundert es Sie, dass die Methode so scheinbar umständlich prüft, ob der Monat im
Vergleichsjahr kleiner als der Monat im Geburtsjahr ist, oder, falls die Monate gleich sind, der
Tag im Monat des Vergleichsjahrs kleiner dem Tag im Monat des Geburtsjahrs ist. Könnte man
nicht einfach das Feld DAY_OF_YEAR für beide Daten abfragen und vergleichen?
Die Antwort ist nein, weil dann Schalttage das Ergebnis verfälschen können. Konkret: Für das
Geburtsdatum 01.03.1955 und ein Vergleichsdatum 29.02.2004 würde get(Calendar.DAY_OF_
YEAR) in beiden Fällen 60 zurückliefern. Das berechnete Alter wäre daher fälschlicherweise 50
statt 49.
Mit dem Start-Programm zu diesem Rezept können Sie berechnen, wie alt eine Person oder ein
Gegenstand heute ist. Das Geburtsdatum wird im Programmverlauf abgefragt, das Vergleichs-
datum ist das aktuelle Datum. Beachten Sie auch die Formatierung der Ausgabe mit Choice-
Format, siehe Rezept 11.
162 >> Alter aus Geburtsdatum berechnen

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.text.DateFormat;
import java.text.ChoiceFormat;
import java.text.ParseException;
Datum und Uhrzeit

import java.util.Locale;
import java.util.Scanner;

public class Start {

public static void main(String args[]) {


DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM,
Locale.GERMANY);
GregorianCalendar date = new GregorianCalendar();
Scanner sc = new Scanner(System.in);
int age = 0;

try {
System.out.print("\n Geben Sie Ihr Geburtsdatum im Format "
+ " TT.MM.JJJJ ein: ");
String input = sc.next();

date.setTime(df.parse(input));

// Vergleich mit aktuellem Datum


age = MoreDate.age(date, Calendar.getInstance());

if (age < 0) {
System.out.println("\n Sie sind noch nicht geboren");
} else {
double[] limits = {0, 1, 2};
String[] outputs = {"Jahre", "Jahr", "Jahre"};
ChoiceFormat cf = new ChoiceFormat(limits, outputs);

System.out.println("\n Sie sind " + age + " "


+ cf.format(age) + " alt");
}

} catch(ParseException e) {
System.err.println("\n Kein gueltiges Datum (TT.MM.JJJJ)");
}
}
}

Listing 60: Start.java – Programm zur Altersberechnung


>> Datum und Uhrzeit 163

57 Aktuelle Zeit abfragen


Der einfachste und schnellste Weg, die aktuelle Zeit abzufragen, besteht darin, ein Objekt der
Klasse Date zu erzeugen:
import java.util.Date;

Datum und Uhrzeit


Date today = new Date();
System.out.println(today);
Ausgabe:
Thu Mar 31 10:54:31 CEST 2005
Wenn Sie lediglich die Zeit ausgeben möchten, lassen Sie sich von DateFormat.getTime-
Instance() ein entsprechendes Formatierer-Objekt zurückliefern und übergeben Sie das Date-
Objekt dessen format()-Methode. Als Ergebnis erhalten Sie einen formatierten Uhrzeit-String
zurück.
String s = DateFormat.getTimeInstance().format(today);
System.out.println( s ); // Ausgabe: 10:54:31

Mehr zur Formatierung mit DateFormat, siehe Rezept 41.


H in we is

Sofern Sie die Uhrzeit nicht nur ausgeben oder bestenfalls noch mit anderen Uhrzeiten des
gleichen Tags vergleichen möchten, sollten Sie die Uhrzeit durch ein Calendar-Objekt (siehe
auch Rezept 39) repräsentieren.
왘 Sie können sich mit getInstance() ein Calendar-Objekt zurückliefern lassen, welches die
aktuelle Zeit (natürlich inklusive Datum) repräsentiert:
Calendar calendar = Calendar.getInstance();
왘 Sie können ein Calendar-Objekt erzeugen und auf eine beliebige Zeit setzen:
Calendar calendar = Calendar.getInstance();
왘 Sie können die Zeit aus einem Date-Objekt an ein Calendar-Objekt übergeben:
Calendar calendar = Calendar.getInstance();
calendar.set(calendar.get(Calendar.YEAR), // Datum
calendar.get(Calendar.MONTH), // beibehalten
calendar.get(Calendar.DATE),
12, 30, 1); // Uhrzeit setzen
왘 Sie können ein GregorianCalender-Objekt für eine bestimmte Uhrzeit erzeugen:
GregorianCalendar gCal =
// year, m, d, h, min, sec
new GregorianCalendar(2005, 4, 20, 12, 30, 1);
164 >> Zeit in bestimmte Zeitzone umrechnen

Um die in einem Calendar-Objekt gespeicherte Uhrzeit auszugeben, können Sie entwe-


H in we is

der die Werte für die einzelnen Uhrzeit-Felder mittels der zugehörigen get-Methoden
abfragen (siehe Tabelle 15 aus Rezept 40) und in einen String/Stream schreiben oder
sie wandeln die Feldwerte durch Aufruf von getTime() in ein Date-Objekt um und
Datum und Uhrzeit

übergeben dieses an die format()-Methode einer DateFormat-Instanz:


String s = DateFormat.getTimeInstance().format(calendar.getTime()) );

58 Zeit in bestimmte Zeitzone umrechnen


Wenn Sie mit Hilfe von Date oder Calendar die aktuelle Zeit abfragen (siehe Rezept 57), wird
das Objekt mit der Anzahl Millisekunden initialisiert, die seit dem 01.01.1970 00:00:00 Uhr,
GMT, vergangen sind. Wenn Sie diese Zeitangabe in einen formatierten String umwandeln
lassen (mittels DateFormat oder SimpleDateFormat, siehe Rezept 41), wird die Anzahl Millise-
kunden gemäß dem gültigen Kalender und gemäß der auf dem aktuellen System eingestellten
Zeitzone in Datums- und Zeitfelder (Jahr, Monat, Tag, Stunde, Minute etc.) umgerechnet.

Formatierer auf Zeitzone umstellen


Wenn Sie die Zeit dagegen in die Zeit einer anderen Zeitzone umrechnen lassen möchten,
gehen Sie wie folgt vor:
1. Erzeugen Sie ein TimeZone-Objekt für die gewünschte Zeitzone.
2. Registrieren Sie das TimeZone-Objekt beim Formatierer.
3. Wandeln Sie die Zeitangabe mit Hilfe des Formatierers in einen String um.
Um beispielsweise zu berechnen, wie viel Uhr es aktuell in Los Angeles ist, würden Sie schrei-
ben:
// Formatierer
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.FULL,
DateFormat.FULL);

// Aktuelles Datum
Date today = new Date();

// 1. Zeitzone erzeugen
TimeZone tz = TimeZone.getTimeZone("America/Los_Angeles");

// 2. Zeitzone beim Formatierer registrieren


df.setTimeZone(tz);

// 3. Umrechnung (und Ausgabe) in Zeitzone für Los Angeles (Amerika)


System.out.println(" America/Los Angeles: " + df.format(today));

Wenn die Uhrzeit in Form eines Calendar-Objekts vorliegt, gehen Sie analog vor. Sie
Hi nwei s

müssen lediglich daran denken, die Daten des Calendar-Objekts als Date-Objekt an die
format()-Methode zu übergeben: df.format(calObj.getTime()).
>> Datum und Uhrzeit 165

Calendar auf Zeitzone umstellen


Sie können auch das Calendar-Objekt selbst auf eine andere Zeitzone umstellen. In diesem Fall
übergeben Sie das TimeZone-Objekt, welches die Zeitzone repräsentiert, mittels setTimeZone()
an das Calendar-Objekt:

Datum und Uhrzeit


Calendar calendar = Calendar.getInstance();
...
TimeZone tz = TimeZone.getTimeZone("America/Los_Angeles");
calendar.setTimeZone(tz);
Die get-Methoden des Calendar-Objekts – wie z.B. calendar.get(calendar.HOUR_OF_DAY),
calendar.get(calendar.DST_OFFSET), siehe Tabelle 15 aus Rezept 40 – liefern daraufhin die der
Zeitzone entsprechenden Werte (inklusive Zeitverschiebung und Berücksichtigung der Som-
merzeit) zurück.

Die Zeit, die ein Calendar-Objekt repräsentiert, wird intern als Anzahl Millisekunden,
Achtung

die seit dem 01.01.1970 00:00:00 Uhr, GMT vergangen sind, gespeichert. Dieser Wert
wird durch die Umstellung auf eine Zeitzone nicht verändert. Es ändern sich lediglich
die Datumsfeldwerte, wie Stunden, Minuten etc., die intern aus der Anzahl Millisekun-
den unter Berücksichtigung der Zeitzone berechnet werden. Vergessen Sie dies nie, vor
allem nicht bei der Formatierung der Zeitwerte mittels DateFormat. Wenn Sie sich näm-
lich mit getTime() ein Date-Objekt zurückliefern lassen, das Sie der format()-Methode
von DateFormat übergeben können, wird dieses Date-Objekt auf der Grundlage der
intern gespeicherten Anzahl Millisekunden erzeugt. Soll DateFormat die Uhrzeit in der
Zeitzone des Calendar-Objekts formatieren, müssen Sie die Zeitzone des Calendar-
Objekts zuvor beim Formatierer registrieren:
df.setTimeZone(calendar.getTimeZone());

59 Zeitzone erzeugen
Zeitzonen werden in Java durch Objekte vom Typ der Klasse TimeZone repräsentiert. Da Time-
Zone selbst abstrakt ist, lassen Sie sich TimeZone-Objekte von der statischen Methode getTime-
Zone() zurückliefern, der Sie als Argument den ID-String der gewünschten Zeitzone übergeben:
TimeZone tz = TimeZone.getTimeZone("Europe/Berlin");

ID Zeitzone entspricht GMT


Pacific/Samoa Samoa Normalzeit GMT-11:00
US/Hawaii Hawaii Normalzeit GMT-10:00
US/Alaska Alaska Normalzeit GMT-09:00
US/Pacific, Canada/Pacific, Pazifische Normalzeit GMT-08:00
America/Los_Angeles
US/Mountain, Canada/Mountain, Rocky Mountains Normalzeit GMT-07:00
America/Denver
US/Central, America/Chicago, America/ Zentrale Normalzeit GMT-06:00
Mexico_City

Tabelle 22: Zeitzonen


166 >> Zeitzone erzeugen

ID Zeitzone entspricht GMT


US/Eastern, Canada/Eastern, Östliche Normalzeit GMT-05:00
America/New_York
Canada/Atlantic, Atlantic/Bermuda Atlantik Normalzeit GMT-04:00
Datum und Uhrzeit

America/Buenos_Aires Argentinische Zeit GMT-03:00


Atlantic/South_Georgia South Georgia Normalzeit GMT-02:00
Atlantic/Azores Azoren Zeit GMT-01:00
Europe/Dublin, Europe/London, Greenwich Zeit GMT-00:00
Africa/Dakar Koordinierte Universalzeit
Etc/UTC
Europe/Berlin, Etc/GMT-1 Zentraleuropäische Zeit GMT+01:00
Europe/Kiev Osteuropäische Zeit GMT+02:00
Africa/Cairo Zentralafrikanische Zeit
Asia/Jerusalem Israelische Zeit
Europe/Moscow Moskauer Normalzeit GMT+03:00
Asia/Baghdad Arabische Normalzeit
Asia/Dubai Golf Normalzeit GMT+04:00
Indian/Maledives Maledivische Normalzeit GMT+05:00
Asia/Colombo Sri Lanka Zeit GMT+06:00
Asia/Bangkok Indochina Zeit GMT+07:00
Asia/Shanghai Chinesische Normalzeit GMT+08:00
Asia/Tokyo Japanische Normalzeit GMT+09:00
Australia/Canberra Östliche Normalzeit GMT+10:00
Pacific/Guadalcanal Salomoninseln Zeit GMT+11:00
Pacific/Majuro Marshallinseln Zeit GMT+12:00

Tabelle 22: Zeitzonen (Forts.)

Die weit verbreiteten dreibuchstabigen Zeitzonen-Abkürzungen wie ETC, PST, CET, die
H i n we i s

aus Gründen der Abwärtskompatibilität noch unterstützt werden, sind nicht eindeutig
und sollten daher möglichst nicht mehr verwendet werden.

Verfügbare Zeitzonen abfragen


Die Übergabe einer korrekten ID ist aber noch keine Garantie, dass die zur Erstellung des Time-
Zone-Objekts benötigten Informationen auf dem aktuellen System vorhanden sind. Dazu müs-
sen Sie sich mit TimeZone.getAvailableIDs() ein String-Array mit den IDs der auf dem System
verfügbaren Zeitzonen zurückliefern lassen und prüfen, ob die gewünschte ID darin vertreten
ist.
TimeZone tz = null;
String ids[] = TimeZone.getAvailableIDs();
for (int i = 0; i < ids.length; ++i)
>> Datum und Uhrzeit 167

if (ids[i].equals(searchedID))
tz = TimeZone.getTimeZone(ids[i]);
Wenn Sie TimeZone.getTimeZone() eine ungültige ID übergeben, erhalten Sie die Greenwich-
Zeitzone (»GMT«) zurück.

Datum und Uhrzeit


Eigene Zeitzonen erzeugen
Eigene Zeitzonen erzeugen Sie am einfachsten, indem Sie TimeZone.getTimeZone() als ID einen
String der Form »GMT-hh:mm« bzw. »GMT+hh:mm« übergeben, wobei hh:mm die Zeitver-
schiebung in Stunden und Minuten angibt.
TimeZone tz = TimeZone.getTimeZone("GMT-01:00");
Allerdings berücksichtigen die erzeugten TimeZone-Objekte dann keine Sommerzeit. Dazu
müssen Sie nämlich explizit ein Objekt der Klasse SimpleTimeZone erzeugen und deren Kon-
struktor, neben der frei wählbaren ID für die neue Zeitzone, auch noch die Informationen für
Beginn und Ende der Sommerzeit übergeben.
Die im Folgenden abgedruckte Methode MoreDate.getTimeZone() verfolgt eine zweigleisige
Strategie. Zuerst prüft sie, ob die angegebene ID in der Liste der verfügbaren IDs zu finden ist.
Wenn ja, erzeugt sie direkt anhand der ID das gewünschte TimeZone-Objekt. Bis hierher unter-
scheidet sich die Methode noch nicht von einem direkten TimeZone.getTimeZone()-Aufruf.
Sollte die Methode allerdings feststellen, dass es zu der ID keine passenden Zeitzonen-Infor-
mationen gibt, liefert sie nicht die GMZ-Zeitzone zurück, sondern zieht die ebenfalls als Argu-
mente übergebenen Informationen zu Zeitverschiebung und Sommerzeit hinzu und erzeugt
ein eigenes SimpleTimeZone-Objekt.
/**
* Hilfsmethode zum Erzeugen einer Zeitzone (TimeZone-Objekt)
*/
public static TimeZone getTimeZone(String id, int rawOffset,
int startMonth, int startDay,
int startDayOfWeek, int startTime,
int endMonth, int endDay,
int endDayOfWeek, int endTime,
int dstSavings) {
TimeZone tz = null;

// Ist gewünschte Zeitzone verfügbar?


String ids[] = TimeZone.getAvailableIDs();
for (int i = 0; i < ids.length; ++i)
if (ids[i].equals(id))
tz = TimeZone.getTimeZone(ids[i]);

if(tz == null) // Eigene Zeitzone konstruieren


tz = new SimpleTimeZone(rawOffset, id, startMonth, startDay,
startDayOfWeek, startTime, endMonth,
endDay, endDayOfWeek, endTime,
dstSavings);

return tz;
}
Das Start-Programm zu diesem Rezept zeigt den Aufruf von MoreDate.getTimeZone(), um sich
ein TimeZone-Objekt für »America/Los_Angeles« zurückliefern zu lassen:
168 >> Differenz zwischen zwei Uhrzeiten berechnen

// aus Start.java
SimpleDateFormat sdf = new SimpleDateFormat("dd. MMMM yyyy, HH:mm");
Calendar calendar = Calendar.getInstance();
TimeZone tz;
Datum und Uhrzeit

tz = MoreDate.getTimeZone("America/Los_Angeles", -28800000,
Calendar.APRIL, 1, -Calendar.SUNDAY, 7200000,
Calendar.OCTOBER, -1, Calendar.SUNDAY, 7200000,
3600000);
sdf.setTimeZone(tz);
System.console().printf("\t%s\n", sdf.format(calendar.getTime()));
Ausgabe:
04. April 2005, 05:10

Wenn Sie die Uhrzeit mit Angabe der Zeitzonen ausgeben:


Achtung

SimpleDateFormat sdf = new SimpleDateFormat("dd. MMMM yyyy, HH:mm z");


kann es passieren, dass für selbst definierte Zeitzonen (ID nicht in der Liste der verfügbaren
IDs vorhanden) eine falsche Zeitzone angezeigt wird. Dies liegt daran, dass SimpleDate-
Format in diesem Fall in die Berechnung der »Zeitzone« auch die Sommerzeitverschiebung
mit einbezieht.

60 Differenz zwischen zwei Uhrzeiten berechnen


Die Differenz zwischen zwei Uhrzeiten zu berechnen, ist grundsätzlich recht einfach: Sie
lassen sich die beiden Zeiten als Anzahl Millisekunden seit dem 01.01.1970 00:00:00 Uhr,
GMT, zurückgeben, bilden durch Subtraktion die Differenz und rechnen das Ergebnis in die
gewünschte Einheit um:
import java.util.GregorianCalendar;

GregorianCalendar time1 = new GregorianCalendar(2005, 4, 1, 22, 30, 0);


GregorianCalendar time2 = new GregorianCalendar(2005, 4, 2, 7, 30, 0);

long diff = time2.getTimeInMillis() - time1.getTimeInMillis();

// Differenz in Millisekunden : diff


// Differenz in Sekunden : diff/1000
// Differenz in Minuten : diff/(60*1000)
// Differenz in Stunden : diff/(60*60*1000)
Das obige Verfahren berechnet letzten Endes aber keine Differenz zwischen Uhrzeiten, sondern
Differenzen zwischen Zeiten (inklusive Datum). Das heißt, für time1 = 01.05.2005 22:30 Uhr
und time2 = 03.05.2005 7:30 Uhr würde die Berechnung 33 Stunden (bzw. 1980 Minuten)
ergeben. Dies kann, muss aber nicht im Sinne des Programmierers liegen.
Wenn Sie nach obigem Verfahren den zeitlichen Abstand zwischen zwei reinen Uhrzeiten (bei-
spielsweise von 07:00 zu 14:00 oder von 14:00 zu 05:00 am nächsten Tag) so berechnen wol-
len, wie man ihn am Zifferblatt einer Uhr ablesen würde, müssen Sie darauf achten, die
Datumsanteile beim Erzeugen der GregorianCalendar-Objekte korrekt zu setzen – oder Sie
erweitern den Algorithmus, so dass er gegebenenfalls selbsttätig den Datumsteil anpasst.
>> Datum und Uhrzeit 169

Differenz ohne Berücksichtung des Tages


Der folgende Algorithmus vergleicht die reinen Uhrzeiten.
왘 Liegt die Uhrzeit von time1 zeitlich vor der Uhrzeit von time2, wird die Differenz von time1
zu time2 berechnet. Beispiel:

Datum und Uhrzeit


Für time1 = 07:30 Uhr und time2 = 22:30 Uhr werden 15 Stunden (bzw. 900 Minuten)
berechnet.
왘 Liegt die Uhrzeit von time1 zeitlich nach der Uhrzeit von time2, wird die Differenz von
time1 zu time2 am nächsten Tag berechnet. Beispiel:
Für time1 = 22:30 Uhr und time2 = 07:30 Uhr werden 9 Stunden (bzw. 540 Minuten)
berechnet.
import java.util.Calendar;
import java.util.GregorianCalendar;

GregorianCalendar time1 = new GregorianCalendar(2005, 1, 1, 22, 30, 0);


GregorianCalendar time2 = new GregorianCalendar(2005, 1, 3, 7, 30, 0);

// time1 kopieren und Datumsanteil an time2 angleichen


GregorianCalendar clone1 = (GregorianCalendar) time1.clone();
clone1.set(time2.get(Calendar.YEAR), time2.get(Calendar.MONTH),
time2.get(Calendar.DAY_OF_MONTH));

// liegt die Uhrzeit von clone1 hinter time2, erhöhe Tag von time2
if (clone1.after(time2))
time2.add(Calendar.DAY_OF_MONTH, 1);

diff = time2.getTimeInMillis() - clone1.getTimeInMillis();

// Differenz in Millisekunden : diff


// Differenz in Sekunden : diff/1000
// Differenz in Minuten : diff/(60*1000)
// Differenz in Stunden : diff/(60*60*1000)

61 Differenz zwischen zwei Uhrzeiten in Stunden,


Minuten, Sekunden berechnen
Um die Differenz zwischen zwei Uhrzeiten in eine Kombination aus Stunden, Minuten und
Sekunden umzurechnen, berechnen Sie zuerst die Differenz in Sekunden (diff). Dann rechnen
Sie diff Modulo 60 und erhalten den Sekundenanteil. Diesen ziehen Sie von der Gesamtzahl
ab (wozu Sie am einfachsten die Ganzzahldivision diff/60 durchführen). Analog rechnen Sie
den Minutenanteil heraus und behalten die Stunden übrig.
Die statische Methode getInstance() der nachfolgend definierten Klasse TimeDiff tut genau
dies. Sie übernimmt als Argumente die beiden Datumswerte (in Form von Calendar-Objekten)
sowie ein optionales boolesches Argument, über das sie steuern können, ob die reine Uhrzeit-
differenz ohne Berücksichtigung des Datumsanteils (true) oder die Differenz zwischen den
vollständigen Datumsangaben (false) berechnet wird. Als Ergebnis liefert die Methode ein
Objekt ihrer eigenen Klasse zurück, in dessen public-Feldern die Werte für Stunden, Minuten
und Sekunden gespeichert sind.
170 >> Differenz zwischen zwei Uhrzeiten in Stunden, Minuten, Sekunden berechnen

import java.util.Calendar;

/**
* Klasse zur Repräsentation und Berechnung von Zeitabständen
* zwischen zwei Uhrzeiten
Datum und Uhrzeit

*
*/
public class TimeDiff {

public int hours;


public int minutes;
public int seconds;

// Berechnet die Zeit zwischen zwei Uhrzeiten, gegeben als


// Calendar-Objekte (berücksichtigt ganzes Datum)
public static TimeDiff getInstance(Calendar t1, Calendar t2) {
return getInstance(t1, t2, false);
}

// Berechnet die Zeit zwischen zwei Uhrzeiten, gegeben als


// Calendar-Objekte (wenn onlyClock true, wird nur Differenz zwischen
// Tageszeiten berechnet)
public static TimeDiff getInstance(Calendar t1, Calendar t2,
boolean onlyClock) {
Calendar clone1 = (Calendar) t1.clone();
long diff;

// reine Uhrzeit, Datumsanteil eliminieren, vgl. Rezept 60


if (onlyClock) {
clone1.set(t2.get(Calendar.YEAR), t2.get(Calendar.MONTH),
t2.get(Calendar.DAY_OF_MONTH));
if (clone1.after(t2))
t2.add(Calendar.DAY_OF_MONTH, 1);
}

diff = Math.abs(t2.getTimeInMillis() - clone1.getTimeInMillis())/1000;

TimeDiff td = new TimeDiff();

// Sekunden, Minuten und Stunden berechnen


td.seconds = (int) (diff%60); diff /= 60;
td.minutes = (int) (diff%60); diff /= 60;
td.hours = (int) diff;

return td;
}
}

Listing 61: Die Klasse TimeDiff


>> Datum und Uhrzeit 171

Wenn Sie für den dritten Parameter false übergeben oder einfach die überladene Version mit
nur zwei Parametern aufrufen, repräsentiert das zurückgelieferte Objekt die Differenz in Stun-
den, Minuten, Sekunden vom ersten Datum zum zweiten.
Wenn Sie für den dritten Parameter true übergeben, repräsentiert das zurückgelieferte Objekt

Datum und Uhrzeit


die Stunden, Minuten, Sekunden von der Uhrzeit des ersten Calendar-Objekts bis zur Uhrzeit
des zweiten Calendar-Objekts – so wie die Zeitdifferenz auf dem Zifferblatt einer Uhr abzule-
sen ist:
왘 Für time1 = 07:30 Uhr und time2 = 22:30 Uhr werden 15 Stunden (bzw. 900 Minuten)
berechnet.
왘 Für time1 = 22:30 Uhr und time2 = 07:30 Uhr werden 9 Stunden (bzw. 540 Minuten)
berechnet

Das Start-Programm zu diesem Rezept demonstriert die Verwendung:

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.text.DateFormat;

public class Start {

public static void main(String args[]) {


DateFormat dfDateTime = DateFormat.getDateTimeInstance();
TimeDiff td;
System.out.println();

GregorianCalendar time1 =
new GregorianCalendar(2005, 4, 1, 22, 30, 0);
GregorianCalendar time2 =
new GregorianCalendar(2005, 4, 3, 7, 30, 0);
System.out.println(" Zeit 1 : " + dfDateTime.format(time1.getTime()));
System.out.println(" Zeit 2 : " + dfDateTime.format(time2.getTime()));

// Berücksichtigt Datum
System.out.println("\n Differenz zw. Uhrzeiten (mit Datum)\n");

td = TimeDiff.getInstance(time1, time2);
System.out.println(" " + td.hours + " h "
+ td.minutes + " min " + td.seconds + " sec");

// Reine Uhrzeit
System.out.println("\n\n Differenz zw. Uhrzeiten (ohne Datum)\n");

td = TimeDiff.getInstance(time1, time2, true);


System.out.println(" " + td.hours + " h "
+ td.minutes + " min " + td.seconds + " sec");
}
}

Listing 62: Einsatz der Klasse TimeDiff


172 >> Präzise Zeitmessungen (Laufzeitmessungen)
Datum und Uhrzeit

Abbildung 34: Berechnung von Uhrzeitdifferenzen in Stunden, Minuten, Sekunden

62 Präzise Zeitmessungen (Laufzeitmessungen)


Für Zeitmessungen definiert die Klasse System die statischen Methoden currentTimeMillis()
und nanoTime(). Beide Methoden werden in gleicher Weise eingesetzt und liefern Zeitwerte in
Millisekunden (10-3 sec) bzw. Nanosekunden (10-9 sec). In der Praxis werden Sie wegen der
größeren Genauigkeit in der Regel die ab JDK-Version 1.5 verfügbare Methode nanoTime()
vorziehen.
Zeitmessungen haben typischerweise folgendes Muster:
// 1. Zeitmessung beginnen (Startzeit abfragen)
long start = System.nanoTime();

// Code, dessen Laufzeit gemessen wird


Thread.sleep(50000);

// 2. Zeitmessung beenden (Endzeit abfragen)


long end = System.nanoTime();

// 3. Zeitmessung auswerten (Differenz bilden und ausgeben)


long diff = end-start;
System.out.println(" Laufzeit: " + diff);

Laufzeitmessungen
Wenn Sie Laufzeitmessungen durchführen, um die Performance eines Algorithmus oder einer
Methode zu testen, beachten Sie folgende Punkte:
왘 Zugriffe auf Konsole, Dateisystem, Internet etc. sollten möglichst vermieden werden.
Derartige Zugriffe sind oft sehr zeitaufwendig. Wenn Sie einen Algorithmus testen, der
Daten aus einer Datei oder Datenbank verarbeitet, messen Sie den Algorithmus unbedingt
erst ab dem Zeitpunkt, da die Daten bereits eingelesen sind. Ansonsten kann es passieren,
dass das Einlesen der Daten weit mehr Zeit benötigt als deren Verarbeitung und Sie folg-
lich nicht die Effizienz Ihres Algorithmus, sondern die der Einleseoperation messen.
왘 Benutzeraktionen sollten ebenfalls vermieden werden.
Sie wollen ja nicht die Reaktionszeit des Benutzers messen, sondern Ihren Code.
>> Datum und Uhrzeit 173

currentTimeMillis() und nanoTime()


E x k ur s

Die Methode currentTimeMillis() gibt es bereits seit dem JDK 1.0. Sie greift, ebenso wie
Date() oder Calendar.getInstance() die aktuelle Systemzeit in Millisekunden seit dem
01.01.1970 00:00:00 Uhr, GMT, ab. (Tatsächlich rufen Date() und Calendar.getIns-

Datum und Uhrzeit


tance() intern System.currentTimeMillis() auf.) Die Genauigkeit von Zeitmessungen
mittels currentTimeMillis() ist daher von vornherein auf die Größenordnung von Milli-
sekunden beschränkt. Sie verschlechtert sich weiter, wenn der Systemzeitgeber, der die
Uhrzeit liefert, in noch längeren Intervallen (etwa alle 10 Millisekunden) aktualisiert
wird.
Die Methode currentTimeMillis() eignet sich daher nur für Messungen von Operatio-
nen, die länger als nur einige Millisekunden andauern (Schreiben in eine Datei, Zugriff
auf Datenbanken oder Internet, Messung der Zeit, die ein Benutzer für die Bearbeitung
eines Dialogfelds oder Ähnliches benötigt).
Die Methode nanoTime() gibt es erst seit dem JDK 1.5. Sie fragt die Zeit von dem genau-
estens verfügbaren Systemzeitgeber ab. (Die meisten Rechner besitzen mittlerweile Sys-
temzeitgeber, die im Bereich von Nanosekunden aktualisiert werden.) Die von diesen
Systemzeitgebern zurückgelieferte Anzahl Nanosekunden muss sich allerdings nicht auf
eine feste Zeit beziehen und kann/sollte daher nicht als Zeit/Datum interpretiert werden.
(Versuchen Sie also nicht, den Rückgabewert von nanoTime() in Millisekunden umzu-
rechnen und zum Setzen eines Date- oder Calendar-Objekts zu verwenden.)
Die Methode nanoTime() ist die Methode der Wahl für Performance-Messungen.

왘 Führen Sie wiederholte Messungen durch.


Verlassen Sie sich nie auf eine Messung. Wiederholen Sie die Messungen, beispielsweise in
einer Schleife, und bilden Sie den Mittelwert.
왘 Verwenden Sie stets gleiche Ausgangsdaten.
Wenn Sie verschiedene Algorithmen/Methoden miteinander vergleichen, achten Sie darauf,
dass die Tests unter denselben Bedingungen und mit denselben Ausgangsdaten durchge-
führt werden.

Vorsicht Jitter! Viele Java-Interpreter fallen unter die Kategorie der Just-In-Time-Com-
A c htu n g

piler, insofern als sie Codeblöcke wie z.B. Methoden bei der ersten Ausführung von
Bytecode in Maschinencode umwandeln, speichern und bei der nächsten Ausführung
dann den bereits vorliegenden Maschinencode ausführen. In diesem Fall sollten Sie
eine zu beurteilende Methode unbedingt mehrfach ausführen und die erste Laufzeit-
messung verwerfen.

Nanosekunden in Stunden, Minuten, Sekunden, Millisekunden


und Nanosekunden umrechnen
Laufzeitunterschiede, die in Nanosekunden ausgegeben werden, können vom Menschen meist
nur schwer miteinander verglichen und in ihrer tatsächlichen Größenordnung erfasst werden.
Es bietet sich daher an, die in Nanosekunden berechnete Differenz vor der Ausgabe in eine
Kombination höherer Einheiten umzurechnen.
174 >> Uhrzeit einblenden

/**
* Klasse zum Umrechnen von Nanosekunden in Stunden, Minuten...
*
*/
public class TimeDiff {
Datum und Uhrzeit

public int hours;


public int minutes;
public int seconds;
public int millis;
public int nanos;

public static TimeDiff getInstance(long time) {


TimeDiff td = new TimeDiff();

td.nanos = (int) (time%1000000); time /= 1000000;


td.millis = (int) (time%1000); time /= 1000;
td.seconds = (int) (time%60); time/= 60;
td.minutes = (int) (time%60); time/= 60;
td.hours = (int) time;

return td;
}
}

Listing 63: Die Klasse TimeDiff zerlegt eine Nanosekunden-Angabe in höhere Einheiten.

Aufruf:
// 3. Zeitmessung auswerten (Differenz bilden und ausgeben)
long diff = end-start;
td = TimeDiff.getInstance(diff);
System.out.println(" Laufzeit: " + td.hours + " h "
+ td.minutes + " min " + td.seconds + " sec "
+ td.millis + " milli " + td.nanos + " nano");

63 Uhrzeit einblenden
In den bisherigen Rezepten ging es mehr oder weniger immer darum, die Zeit einmalig abzufra-
gen und irgendwie weiterzuverarbeiten. Wie aber sieht es aus, wenn die Uhrzeit als digitale Zeit-
anzeige in die Oberfläche einer GUI-Anwendung oder eines Applets eingeblendet werden soll?
Zur Erzeugung einer Uhr müssen Sie die Zeit kontinuierlich abfragen und ausgeben. In diesem
Rezept erfolgen das Abfragen und das Anzeigen der Zeit weitgehend getrennt.
왘 Für das Abfragen ist eine Klasse ClockThread verantwortlich, die, wie der Name schon ver-
rät, von Thread abgeleitet ist und einen eigenständigen Thread repräsentiert.
왘 Die Anzeige der Uhr kann in einer beliebigen Swing-Komponente (zurückgehend auf die
Basisklasse JComponent) erfolgen.

Um die Verbindung zwischen ClockThread und Swing-Komponente herzustellen, übernimmt


der ClockThread-Konstruktor eine Referenz auf die Komponente. Als Dank fordert er die Kom-
ponente nach jeder Aktualisierung der Uhrzeit auf, sich neu zu zeichnen.
>> Datum und Uhrzeit 175

import java.util.Date;
import java.text.DateFormat;
import javax.swing.JComponent;

/**

Datum und Uhrzeit


* Thread-Klasse, die aktuelle Uhrzeit in Komponenten einblendet
*
*/
public class ClockThread extends Thread {

private static String time;


private DateFormat df = DateFormat.getTimeInstance();
private JComponent c;

public ClockThread(JComponent c) {
this.c = c;
this.start();
}

public void run() {


while(isInterrupted() == false) {

// Uhrzeit aktualisieren
ClockThread.time = df.format(new Date());

// Komponente zum Neuzeichnen auffordern


c.repaint();

// eine Sekunde schlafen


try {
sleep(1000);
}
catch(InterruptedException e) {
return;
}
}
}

public static String getTime() {


return time;
}
}

Listing 64: Die Klasse ClockThread

Der Konstruktor von ClockThread speichert die Referenz auf die Anzeige-Komponente und
startet den Thread, woraufhin intern dessen run()-Methode gestartet wird (mehr zu Threads in
der Kategorie »Threads«). Die run()-Methode enthält eine einzige große while-Schleife, die so
lange durchlaufen wird, wie der Thread ausgeführt wird. In der Schleife wird die aktuelle Zeit
abgefragt, formatiert und im statischen Feld time gespeichert, von wo sie die Anzeige-Kompo-
nente mit Hilfe der public getTime()-Methode auslesen kann.
176 >> Uhrzeit einblenden

Das Start-Programm zu diesem Rezept demonstriert, wie die Uhrzeit mit Hilfe von ClockThread
in einem JPanel, hier die ContentPane des Fensters, angezeigt werden kann.

import java.awt.*;
import java.awt.event.*;
Datum und Uhrzeit

import javax.swing.*;

public class Start extends JFrame {

class ClockPanel extends JPanel {


public void paintComponent(Graphics g) {
super.paintComponent(g);

// Uhrzeit einblenden
g.setFont(new Font("Arial", Font.PLAIN, 18));
g.setColor(Color.blue);
g.drawString(ClockThread.getTime(), 15, 30);
}
}

private ClockThread ct;


private ClockPanel display;

public Start() {
setTitle("Fenster mit Uhrzeit");
display = new ClockPanel();
getContentPane().add(display, BorderLayout.CENTER);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

// Thread für Uhrzeit erzeugen und starten


ct = new ClockThread(display);
}

public static void main(String args[]) {


// Fenster erzeugen und anzeigen
Start mw = new Start();
mw.setSize(500,350);
mw.setLocation(200,300);
mw.setVisible(true);
}
}

Listing 65: GUI-Programm mit Uhreinblendung

Dem Fenster fällt die Aufgabe zu, den Uhrzeit-Thread in Gang zu setzen und mit der Anzeige-
Komponente zu verbinden. Beides geschieht im Konstruktor des Fensters bei der Erzeugung
des ClockThread-Objekts.
>> Datum und Uhrzeit 177

Für die Anzeige-Komponente muss eine eigene Klasse (ClockPanel) abgeleitet werden. Nur so
ist es möglich, die paintComponent()-Methode zu überschreiben und den Code zum Einblenden
der Uhrzeit aufzunehmen.

Datum und Uhrzeit


Abbildung 35: GUI-Programm mit eingeblendeter Uhrzeit in JPanel
System

64 Umgebungsvariablen abfragen
Die java.lang.System-Klasse stellt über die Methode getProperties() eine elegante Möglich-

System
keit zum Abrufen von Umgebungsinformationen zur Verfügung. Diese Informationen können
durchlaufen werden, da sie in Form einer java.util.Properties-Instanz vorliegen.
Um die Systemvariablen durchlaufen zu können, wird im folgenden Beispiel der lokalen Vari-
ablen env eine Referenz auf die von System.getProperties zurückgelieferte Properties-Instanz
zugewiesen. Mit Hilfe von env.keys() lassen sich dann alle Schlüssel in Form einer Enumera-
tor-Instanz auslesen. Diese kann per while-Schleife durchlaufen werden.
Innerhalb der Schleife kann der aktuelle Schlüssel mit Hilfe der Methode nextElement() der
Enumerator-Instanz ermittelt werden. Deren Rückgabe liegt allerdings in Form einer Object-
Instanz vor, die deshalb noch in einen String gecastet werden muss, bevor sie weiterverwendet
werden kann.
Jetzt lässt sich der Wert der so ermittelten Systemvariablen auslesen. Dazu wird die Methode
getProperty() der Properties-Instanz genutzt, der als Parameter der Schlüssel übergeben wird.

import java.io.PrintStream;
import java.util.Enumeration;
import java.util.Properties;

public class EnvInfo {

/**
* Abfrage und Ausgabe von Umgebungsvariablen
*/
public static void enumerate() {

// Properties einlesen
Properties env = System.getProperties();

// Schlüssel auslesen
Enumeration keys = env.keys();

// Standardausgabe referenzieren
PrintStream out = System.out;

// Schlüssel durchlaufen
while(keys.hasMoreElements()) {

// Aktuellen Schlüssel auslesen


String key = (String)keys.nextElement();

// Wert auslesen

Listing 66: Ausgabe von Umgebungsinformationen


180 >> Betriebssystem und Java-Version bestimmen

String value = env.getProperty(key);

// Daten ausgeben
out.println(String.format("%s = %s", key, value));
}
}
System

Listing 66: Ausgabe von Umgebungsinformationen (Forts.)

Wenn Sie den Wert eines bestimmten Schlüssels eruieren wollen, müssen Sie nicht den
H in we is

Umweg über System.getProperties().getProperty() gehen, sondern können dies direkt


via System.getProperty() erledigen.

Abbildung 36: Ausgabe der Umgebungsinformationen

65 Betriebssystem und Java-Version bestimmen


Die Bestimmung von Betriebssystem und verwendeter Java-Version erfolgt mit Hilfe der
Schlüssel os.name und os.version. Mit dem Schlüssel java.version können die Versionsinfor-
mationen von Java ausgelesen werden:

public class Start {

public static void main(String[] args) {

// Betriebssystem-Name auslesen
System.out.println(

Listing 67: Ermitteln von Betriebssystem- und Java-Versionsinformationen


>> System 181

String.format("OS: %s",
System.getProperty("os.name")));

// Betriebssystem-Version auslesen
System.out.println(
String.format("Version: %s",

System
System.getProperty("os.version")));

// Java-Version auslesen
System.out.println(
String.format("Java-Version: %s",
System.getProperty("java.version")));
}
}

Listing 67: Ermitteln von Betriebssystem- und Java-Versionsinformationen (Forts.)

Abbildung 37: Ausgabe von Java- und Betriebssystem-Version

66 Informationen zum aktuellen Benutzer ermitteln


Die Java-Runtime stellt einige Informationen zum aktuellen Benutzer zur Verfügung. Diese
können per System.getProperty() unter Verwendung der folgenden Schlüssel abgerufen wer-
den:

Schlüssel Garantiert Beschreibung


user.country nein Kürzel des Landes, das der Nutzer in den Systemeinstellungen ange-
geben hat – beispielsweise DE für Deutschland oder AT für Österreich
user.dir ja Aktuelles Arbeitsverzeichnis
user.variant nein Verwendete Variante der Länder- und Spracheinstellungen
user.home ja Home-Verzeichnis des Nutzers (bei Windows beispielsweise der
Ordner »Eigene Dateien«)
user.timezone nein Verwendete Zeitzone
user.name ja Anmeldename des Nutzers
user.language nein Kürzel der Sprache, die der Nutzer aktiviert hat – beispielsweise de
für Deutsch oder en für Englisch

Tabelle 23: Schlüssel für den Abruf von Benutzerinformationen


182 >> Zugesicherte Umgebungsvariablen

Die nicht garantierten Elemente sind nicht auf jedem System vorhanden. Die drei Schlüssel
user.dir, user.home und user.name werden aber in jedem Fall einen Wert zurückgeben, da sie
zu den zugesicherten Systeminformationen gehören.

67 Zugesicherte Umgebungsvariablen
Java stellt eine große Anzahl Umgebungsvariablen bereit, die über System.getProperty()
System

abgerufen werden können. Nicht alle dieser Variablen sind auf jedem System verfügbar, aber
Java sichert die Existenz zumindest einiger Umgebungsvariablen zu:

Schlüssel Beschreibung
java.version Java-Version
java.vendor Anbieter
java.vendor.url Anbieter-Homepage
java.home Installationsverzeichnis
java.vm.specification.version Version der JVM-Spezifikation
java.vm.specification.vendor Anbieter der JVM-Spezifikation
java.vm.specification.name Name der JVM-Spezifikation
java.vm.version JVM-Version
java.vm.vendor JVM-Anbieter
java.vm.name JVM-Name
java.specification.version JRE-Version
java.specification.vendor JRE-Anbieter
java.specification.name JRE-Spezifikation
java.class.version Java Class Format-Version
java.class.path Klassenpfad
java.library.path Pfade, die durchsucht werden, wenn Java-Libraries geladen
werden sollen
java.io.tmpdir Temporäres Verzeichnis
java.compiler Compiler-Name
java.ext.dirs Pfade, die durchsucht werden, wenn Java-Extensions geladen
werden sollen
os.name Name des Betriebssystems
os.arch Prozessor-Architektur
os.version Version des Betriebssystems
file.separator Trenner zwischen Pfaden und Verzeichnissen
path.separator Trenner zwischen mehreren Pfaden
line.separator Zeilenumbruch-Zeichenfolge (»\n« bei Unix, »\r\n« bei Windows)
user.name Anmeldename des aktuellen Benutzers
user.home Home-Verzeichnis des aktuellen Benutzers
user.dir Aktuelles Arbeitsverzeichnis

Tabelle 24: Zugesicherte Systemvariablen


>> System 183

Interessant für den produktiven Einsatz dürften die Informationen zum Betriebssystem, zum
Benutzer, zu den Pfaden und möglicherweise auch die Java-Version sein. Andere Informatio-
nen, etwa zur JRE- oder JVM-Version, werden in der Praxis nicht allzu häufig benötigt.

Wenn Sie andere Java-Umgebungsvariablen als die zugesicherten verwenden wollen,


A cht un g

sollten Sie die Existenz eines Werts, den Sie mit Hilfe von System.getProperty() ermit-
teln, immer hinterfragen und auf null prüfen, bevor Sie ihn verwenden:

System
String value = System.getProperty(key);
if(null != value && value.length() > 0) {
System.out.println(String.format("Wert von %s: %s", key, value));
}

68 System-Umgebungsinformationen abrufen
Seit Java 5 besteht die Möglichkeit, auf die Umgebungsvariablen des Betriebssystems zuzu-
greifen. So kann beispielsweise das Home-Verzeichnis des aktuell angemeldeten Benutzers
ausgelesen oder die PATH-Angabe interpretiert werden.
Das Auslesen dieser Informationen geschieht mit Hilfe einer java.util.Map-Collection, die für
Schlüssel und Werte nur Strings zulässt und der mit System.getenv() eine Referenz auf die
Systemvariablen zugewiesen wird. Mittels java.util.Iterator können die Schlüssel durchlau-
fen werden. Die Methode get() der Map-Instanz env erlaubt unter Übergabe des Schlüssels als
Parameter den Abruf des referenzierten Werts:

import java.util.Map;
import java.util.Iterator;

public class SystemInfo {

/**
* Abfrage und Ausgabe von Systemvariablen
*/
public static void enumerate() {

// Systemvariablen in Map<String, String> einlesen


Map<String, String>env = System.getenv();

// Iterator erzeugen, um die Schlüssel durchlaufen


// zu können
Iterator<String> keys = env.keySet().iterator();

// Iteratur durchlaufen
while(keys.hasNext()) {
// Schlüssel abrufen
String key = keys.next();

// Wert abrufen
String value = env.get(key);

Listing 68: Ausgabe aller Umgebungsvariablen des Betriebssystems


184 >> INI-Dateien lesen

// Schlüssel und Wert ausgeben


System.out.println(String.format("%s = %s", key, value));
}
}
}
System

Listing 68: Ausgabe aller Umgebungsvariablen des Betriebssystems (Forts.)

69 INI-Dateien lesen
Zum Lesen und Schreiben von Konfigurationsdaten und Benutzereinstellungen kann die
java.util.Properties-Klasse verwendet werden. Dabei handelt es sich um eine nicht Generics-
fähige Ableitung der java.util.Hashtable-Klasse, die ihrerseits weitestgehend der java.util.
Hashmap-Klasse entspricht.
Die Properties-Klasse verwaltet Name/Wert-Paare und bietet besondere Methoden zum Laden
und Speichern der in ihr enthaltenen Werte, wodurch sie sich besonders für die Sicherung von
Anwendungseinstellungen in Form von INI-Dateien oder XML eignet.

Erzeugen mit Standardwerten bzw. ohne Standardwerte


Properties-Instanzen können mit dem Standardkonstruktor erzeugt werden:
java.util.Properties props = new java.util.Properties();
Ein überladener Konstruktor erlaubt es, eine bereits existierende Properties-Instanz für die
Definition von Standardwerten zu verwenden:
java.util.Properties props = new java.util.Properties(defaults);

Zuweisen und Abrufen von Werten


Die Zuweisung von Werten geschieht mit Hilfe der Methode setProperty(), die als Parameter
zwei String-Werte entgegennimmt. Der erste Parameter dient dabei als Schlüssel, der zweite
Parameter stellt den Wert dar:
props.setProperty("Name", "Mueller");
props.setProperty("FirstName", "Paul");
props.setProperty("City", "Musterstadt");
Zum Abrufen der gespeicherten Werte verwenden Sie getProperty(). Als Parameter übergeben
Sie den Schlüssel:
System.out.println(String.format("Name: %s",
props.getProperty("Name")));
System.out.println(String.format("First name: %s",
props.getProperty("FirstName")));
System.out.println(String.format("City: %s",
props.getProperty("City")));
Einer zweiten, überladenen Form kann als zweiter Parameter ein Default-Wert übergeben wer-
den, der zurückgeliefert wird, falls der Schlüssel nicht existiert:
System.out.println(String.format("Country: %s",
props.getProperty("Country", "Germany")));
>> System 185

Gespeicherte Properties laden


Das Laden einer INI-Datei erfolgt mit Hilfe eines InputStreams. Dieser wird als Parameter der
load()-Methode einer zuvor erzeugten Properties-Instanz übergeben:

import java.util.Properties;
import java.io.FileInputStream;

System
import java.io.IOException;

public class Start {

/**
* Lädt die in der angegebenen Datei gespeicherten Parameter
*/
private static Properties load(String filename) {

// Properties-Instanz erzeugen
Properties props = new Properties();

try {
// FileInputStream-Instanz zum Laden der Daten
FileInputStream in = new FileInputStream(filename);

// Daten laden
props.load(in);

// Aufräumen
in.close();

} catch (IOException e) {
// Eventuell aufgetretene Ausnahmen abfangen
}

// Ergebnis zurückgeben
return props;
}

public static void main(String[] args) {

// Daten laden
Properties props = load("app.ini");

// ...und ausgeben
System.out.println(
String.format("Name: %s", props.getProperty("Name")));
System.out.println(
String.format("First name: %s", props.getProperty("FirstName")));
System.out.println(
String.format("City: %s", props.getProperty("City")));
}
}

Listing 69: Laden von Properties


186 >> INI-Dateien lesen

Gespeicherte Properties im XML-Format laden


Analog zum Laden von in Textform vorliegenden Einstellungen gestaltet sich das Laden der
Daten aus einer XML-Datei. Einziger Unterschied ist die verwendete Methode: Statt load()
wird hier loadFromXML() verwendet. Dieser Methode wird eine java.io.InputStream-Instanz als
Parameter übergeben, mit deren Hilfe die Daten geladen werden:
System

import java.util.Properties;
import java.io.FileInputStream;
import java.io.IOException;

public class Start {

/**
* Lädt die in der angegebenen XML-Datei gespeicherten Parameter
*/
private static Properties load(String filename) {

// Properties-Instanz erzeugen
Properties props = new Properties();

try {
// FileInputStream-Instanz zum Laden der Daten
FileInputStream in = new FileInputStream(filename);

// Daten laden
props.loadFromXML(in);

} catch (IOException e) {
// Eventuell aufgetretene Ausnahmen abfangen

} finally {
// Aufräumen
in.close();
}

// Ergebnis zurückgeben
return props;
}

public static void main(String[] args) {

// Daten laden
Properties props = load("app.xml");

// ...und ausgeben
System.out.println(
String.format("Name: %s", props.getProperty("Name")));
System.out.println(
String.format("First name: %s", props.getProperty("FirstName")));
System.out.println(

Listing 70: Laden von als XML vorliegenden Daten


>> System 187

String.format("City: %s", props.getProperty("City")));


}
}

Listing 70: Laden von als XML vorliegenden Daten (Forts.)

System
70 INI-Dateien schreiben
Zum Speichern einer Properties-Instanz kann deren Methode store() verwendet werden.
Dabei kann eine IOException auftreten, weshalb das Speichern in einen try-catch-Block
gefasst werden sollte:

import java.util.Properties;
import java.io.FileOutputStream;
import java.io.IOException;

public class Start {

/**
* Speichert eine Properties-Datei unter dem angegebenen Namen
*/
public static void save(
Properties props, String path, String comment) {
try {
// FileOutputStream-Instanz zum Speichern instanzieren
FileOutputStream fos = new FileOutputStream(path);

// Speichern
props.store(fos, comment);

// Aufräumen
fos.close();
} catch (IOException ignored) {}
}

public static void main(String[] args) {


// Properties-Instanz erzeugen
Properties props = new Properties();

// Werte setzen
props.setProperty("Name", "Mustermann");
props.setProperty("FirstName", "Hans");
props.setProperty("City", "Musterstadt");

// Speichern
save(props, "data.ini", "Saved data");
}
}

Listing 71: Speichern einer Properties-Instanz


188 >> INI-Dateien im XML-Format schreiben

Der zweite Parameter der überladenen Methode store() dient der Speicherung eines Kommen-
tars – hier könnte beispielsweise auch ein Datum ausgegeben werden.

71 INI-Dateien im XML-Format schreiben


Das Speichern von INI-Dateien im XML-Format erfolgt analog zum Speichern im Textformat,
jedoch wird statt der Methode store() die Methode storeToXML() verwendet. Auch hier kann
System

es zu einer IOException kommen (etwa wenn auf die Datei nicht schreibend zugegriffen wer-
den konnte), die mit Hilfe eines try-catch-Blocks abgefangen werden sollte:

import java.util.Properties;
import java.io.FileOutputStream;
import java.io.IOException;

public class Start {

/**
* Speichert eine Properties-Datei unter dem angegebenen Namen
* als XML
*/
public static void saveAsXml(
Properties props, String path, String comment) {
try {
// FileOutputStream-Instanz zum Speichern instanzieren
FileOutputStream fos = new FileOutputStream(path);

// Speichern
props.storeToXML(fos, comment);

// Aufräumen
fos.close();
} catch (IOException ignored) {}
}

public static void main(String[] args) {


// Properties-Instanz erzeugen
Properties props = new Properties();

// Werte setzen
props.setProperty("Name", "Mustermann");
props.setProperty("FirstName", "Hans");
props.setProperty("City", "Musterstadt");

// Als XML Speichern


saveAsXml(props, "data.xml", "Saved data");
}
}

Listing 72: Speichern einer INI-Datei als XML


>> System 189

72 Externe Programme ausführen


Externe Prozesse werden mit Hilfe der Methode exec() der java.lang.Runtime-Klasse gestartet.
Deren Rückgabe ist eine Instanz der java.lang.Process-Klasse.

Das Starten von externen Prozessen ist sehr stark plattformabhängig und verstößt
A cht un g

somit gegen einen der Java-Grundsätze: »Write once, run anywhere« ist beim Ausfüh-

System
ren externer Prozesse nicht mehr gegeben.

Bei der Ausführung von Prozessen kann es zu IOExceptions kommen, falls Rechte aus dem
aktuellen Kontext heraus fehlen, das angegebene Programm nicht gefunden werden konnte
oder sonstige Fehler bei dessen Ausführung aufgetreten sind.
Der Rückgabecode eines Prozesses lässt sich mit Hilfe der Methode exitValue() abrufen. Alter-
nativ – und das wird in der Praxis häufiger vorkommen – kann auf die Beendigung eines Pro-
zesses mit waitFor() gewartet werden.
Die Ausgabe kann über die Methode getInputStream() in Form einer java.io.InputStream-
Instanz abgerufen werden. Sinnvollerweise wird dies in einer java.io.BufferedInputStream-
Instanz gekapselt. Das eigentliche Auslesen erfolgt mit Hilfe einer java.io.BufferedReader-
Instanz, die per zugrunde liegendem java.io.InputStreamReader die im BufferedInputStream
vorliegenden Daten verarbeitet. Auf diese Weise kann auch gleich sichergestellt werden, dass
Umlaute korrekt eingelesen werden: Der Konstruktor des InputStreamReaders erlaubt zu die-
sem und anderen Zwecken die Angabe des zu verwendenden Zeichensatzes.

import java.io.*;

public class Ping {

/**
* Pingt die angegebene Adresse an
*/
public static String ping(String address) {
StringBuffer result = new StringBuffer();
BufferedReader rdr = null;
PrintWriter out = null;

// Runtime-Instanz erzeugen
Runtime r = Runtime.getRuntime();

try {
// Prozess erzeugen
// Syntax für Unix-Systeme: "ping -c 4 -i 1 <Adresse>"
// Syntax für Windows-Systeme: "ping <Adresse>"
Process p = r.exec(String.format("ping %s", address));

// Warten, bis der Prozess abgeschlossen ist


p.waitFor();

Listing 73: Ping auf einen externen Host


190 >> Externe Programme ausführen

// BufferedReader erzeugen, der die Daten einliest


// Die Angabe der CodePage ist auf Windows-Systemen
// nötig, um Umlaute korrekt verarbeiten können
rdr = new BufferedReader(
new InputStreamReader(new BufferedInputStream(
p.getInputStream()), "cp850"));
System

// Daten einlesen
String line = null;
while(null != (line = rdr.readLine())) {
if(line.length() > 0) {
result.append(line + "\r\n");
}
}
} catch (IOException e) {
// IOException abfangen
e.printStackTrace();
} catch (InterruptedException e) {
// Ausführung wurde unterbrochen
e.printStackTrace();
} finally {
try {
// Aufräumen
rdr.close();
} catch (IOException e) {}
}

return result.toString();
}
}

Listing 73: Ping auf einen externen Host (Forts.)

Für die Ausgabe der zurückgelieferten Prozessdaten auf die Konsole bietet sich die printf()-
Methode der Klasse Console an. Dann müssen Sie sich um eventuell enthaltene Umlaute keine
Sorgen machen.

import java.io.*;

public class Start {

public static void main(String[] args) {


// Anzupingende Adresse ist erster Parameter
String address = "java.sun.com";
if(args != null && args.length > 0) {
address = args[0];
}

// Pingen

Listing 74: Ausführen eines externen Prozesses und Ausgeben der vom Prozess
zurückgelieferten Daten
>> System 191

String output = Ping.ping(address);

// Prozessdaten ausgeben
System.console().printf("%s%n", output);
}
}

System
Listing 74: Ausführen eines externen Prozesses und Ausgeben der vom Prozess
zurückgelieferten Daten (Forts.)

Beim Ausführen der Klasse können Sie als Parameter einen Server-Namen oder eine IP-
Adresse übergeben.

Abbildung 38: Ausführen eines Pings aus Java heraus

73 Verfügbaren Speicher abfragen


Mit Hilfe der java.lang.Runtime-Klasse lässt sich ermitteln, wie viel Speicher der aktuellen
Java-Instanz zur Verfügung steht. Diese Information liefert die Methode freeMemory() einer
Runtime-Instanz. Die Instanz kann über die statische Methode Runtime.getRuntime() referen-
ziert werden:

public class Start {

public static void main(String[] args) {


// Runtime-Instanz referenzieren
Runtime r = Runtime.getRuntime();

// Freien Speicher auslesen


long mem = r.freeMemory();

// In KBytes umrechnen
double kBytes = ((mem / 1024) * 100) / 100;

// In MBytes umrechnen

Listing 75: Ermitteln des freien Speichers einer Java-Instanz


192 >> Speicher für JVM reservieren

double mBytes = ((kBytes / 1024) * 100) /100;

// Ausgeben
System.out.println(
String.format(
"Diese Java-Instanz hat %d Byte "
System

+ "(=%g KByte bzw. %g MByte) freien Speicher.",


mem, kBytes, mBytes));
}
}

Listing 75: Ermitteln des freien Speichers einer Java-Instanz (Forts.)

Die ebenfalls verfügbaren Methoden maxMemory() und totalMemory() können genutzt werden,
um den maximal verfügbaren Speicher und die Gesamtmenge an Speicher der Java-Anwen-
dung auszuwerten.

74 Speicher für JVM reservieren


Die Reservierung von Speicher für die JVM erfolgt beim Aufruf des Java-Interpreters unter
Angabe der Parameter -Xms und -Xmx.
Die Parameter haben dabei folgende Bedeutung:

Parameter Bedeutung
-Xms<Größe> Anfänglicher Speicher für die Ausführung der Anwendung. Der Parameter
<Größe> kann dabei in Byte, Kilobyte oder Megabyte angegeben werden:
-Xms1024: anfänglicher Speicher von 1 Kbyte
-Xms100k: anfänglicher Speicher von 100 Kbyte
-Xms32m: anfänglicher Speicher von 32 Mbyte
Der Standardwert von -Xms ist plattformabhängig und beträgt je nach
Systemumgebung und Java-Version 1-2 Mbyte.
-Xmx<Größe> Maximaler Speicher für die Ausführung der Anwendung:
-Xmx2048: maximaler Speicher von 2 Kbyte
-Xmx300k: maximaler Speicher von 300 Kbyte
-Xmx128m: maximaler Speicher von 128 Mbyte
Der Standardwert beträgt je nach Systemumgebung und Java-Version zwi-
schen 16 und 64 Mbyte.

Tabelle 25: Kommandozeilen-Parameter für die Reservierung von Speicher

75 DLLs laden
Das Laden und Ausführen externer Bibliotheken erfolgt via JNI (Java Native Interface). Dabei
wird die externe Bibliothek mit Hilfe von loadLibrary() in eine Java-Klasse eingebunden, ihre
Methoden werden aus Sicht der Klasse wie gewöhnliche Instanz-Methoden verwendet.
>> System 193

Das Laden und Verwenden externer Bibliotheken (auf Windows-Systemen meist als
A c h tun g

DLLs vorliegend) sollte mit Bedacht vorgenommen werden, denn es hebt die Plattform-
und Systemunabhängigkeit von Java auf.

Die grundsätzliche Vorgehensweise für den Einsatz von JNI sieht so aus:

System
왘 Erstellen einer Java-Klasse, die die zu implementierenden Funktionen definiert und die
externe Bibliothek lädt
왘 Kompilieren der Java-Klasse
왘 Erzeugen einer C/C++-Header-Datei, die die zu implementierenden Funktionen für C- oder
C++-Programme definiert
왘 Implementieren der Funktionen in C oder C++
왘 Bereitstellen der externen Bibliothek
Das Laden und Verwenden der externen Bibliothek geschieht mit Hilfe eines statischen Blocks
und unter Verwendung des Schlüsselworts native. Die Angabe der Dateiendung der externen
Bibliothek unterbleibt dabei, so dass hier eine gewisse Portabilität gewahrt bleibt:

public class Start {

// Laden der externen Bibliothek


static {
System.loadLibrary("HelloWorld");
}

// Deklaration der Methode in der externen Bibliothek


public native String sayHello();

public static void main(String[] args) {


// Neue Instanz erzeugen
Start instance = new Start();

// Externe Methode ausführen


System.out.println(instance.sayHello());
}
}

Listing 76: Laden und Verwenden einer externen Bibliothek

Nach dem Kompilieren der Klasse kann eine C/C++-Header-Datei erzeugt werden, in der die zu
implementierende JNI-Methode definiert ist. Dies geschieht unter Verwendung des Hilfspro-
gramms javah, das sich im /bin-Verzeichnis der JDK-Installation befindet.
Der Aufruf von javah sieht zur Generierung einer Datei HelloWorld.h wie folgt aus:
javah -jni -classpath "%CLASSPATH%;." -o HelloWorld.h Start
Die so erzeugte Header-Datei kann nun verwendet werden, um eine externe Bibliothek zu
erstellen. Diese wird in der Regel meist nur eine Wrapper-Funktion haben und somit den
194 >> DLLs laden

Zugriff auf andere Bibliotheken oder Systemfunktionen erlauben. Innerhalb der Header-Datei
ist eine Funktion definiert, die implementiert werden muss:
JNIEXPORT jstring JNICALL Java_Start_sayHello(JNIEnv *, jobject);
Am Beispiel eines Visual C++-Projekts soll aufgezeigt werden, wie die Umsetzung stattfinden
kann. Analog kann auch bei Verwendung eines anderen Entwicklungstools und eines anderen
Compilers vorgegangen werden.
System

Zunächst soll ein neues C++-Projekt im Visual Studio .NET angelegt werden. Dieses Projekt ist
vom Typ Windows-32-Applikation. In den Projekteigenschaften muss als Ausgabetyp »Dyna-
mische Bibliothek (.dll)« festgelegt werden.
Nach dem Erzeugen des Projekts müssen unter EXTRAS/OPTIONEN/PROJEKTE/VC++-VERZEICH-
NISSE die Ordner %JAVA-HOME%/include und %JAVA_HOME%/include/win32 für Include-
Dateien hinzugefügt werden:

Abbildung 39: Hinzufügen der Java-Include-Verzeichnisse zum Projekt

In den Projekteigenschaften müssen unter dem Punkt ALLGEMEIN folgende Einstellungen vor-
genommen werden:
왘 Verwendung von ATL: Dynamische Verknüpfung zu ATL
왘 Zeichensatz: Unicode

Unter dem Punkt LINKER muss die Bibliothek %JAVA_HOME%/lib/jawt.lib hinzugefügt werden.
Die Wrapper-Klasse HelloWorld kapselt den Zugriff auf die eigentlich verwendete Klasse Say-
Hello, die folgende Header-Definition besitzt:

class SayHello {
public:

Listing 77: SayHello.h


>> System 195

SayHello(void);
~SayHello(void);
char* execute(void);
};

Listing 77: SayHello.h (Forts.)

System
Die Implementierung ist in diesem Fall trivial:

#include "StdAfx.h"
#include ".\sayhello.h"

// Konstruktor
SayHello::SayHello(void) {}

// Destruktor
SayHello::~SayHello(void) {}

// Implementierung von execute


char* SayHello::execute() {
return "Hello world from C++!";
}

Listing 78: SayHello.cpp

Die Wrapper-Klasse HelloWorld.cpp muss die von Java generierte Header-Datei HelloWorld.h
referenzieren und die dort definierte Methode Java_Start_sayHello() implementieren:

// HelloWorld.cpp : Wrapper-Klasse, wird von Java aufgerufen

// Includes
#include "stdafx.h"
#include "HelloWorld.h"
#include "SayHello.h"
#include <win32\jawt_md.h>

// Default-Einstiegspunkt
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved) {
return TRUE;
}

// Implementierung der Java-Methode


JNIEXPORT jstring JNICALL Java_Start_sayHello (
JNIEnv *env, jobject obj) {

// Instanz erstellen
SayHello *instance = new SayHello();

Listing 79: JNI-Wrapper-Implementierung HelloWorld.cpp


196 >> DLLs laden

// Rückgabe abrufen
const char* tmp = instance->execute();

// Größe bestimmen
size_t size = strlen(tmp);
System

// jchar-Array erzeugen
jchar* jc = new jchar[size];

// Speicher reservieren
memset(jc, 0, sizeof(jchar) * size);

// Kopieren
for(size_t i = 0; i < size; i++) {
jc[i] = tmp[i];
}

// Rückgabe erzeugen
jstring result = (jstring)env->NewString(jc, jsize(size));

// Aufräumen
delete [] jc;

// Zurückgeben
return result;
}

Listing 79: JNI-Wrapper-Implementierung HelloWorld.cpp (Forts.)

JNI definiert einige Datentypen, die die C-/C++-Gegenstücke zu den Java-Datentypen darstel-
len. C-/C++-Datentypen müssen stets aus und in die JNI-Datentypen gecastet werden, da sich
sowohl Größe als auch Kodierung der repräsentierten Java-Datentypen deutlich von ihren
C++-Pendants unterscheiden. Die hier praktizierte Rückgabe von Zeichenketten erfordert bei-
spielsweise, dass einzelne Zeichen in ihre JNI-jchar-Pendants gecastet werden müssen.

Achten Sie darauf, nicht mehr benötigte Ressourcen wieder freizugeben, um keine
A c h t u ng

Speicherlöcher zu erzeugen.

Nach dem Kompilieren kann die Bibliothek verwendet werden. Dabei muss sie sich innerhalb
eines durch die Umgebungsvariable PATH definierten Pfads befinden, um gefunden zu werden:

Abbildung 40: Verwenden einer C++-Methode aus Java heraus


>> System 197

76 Programm für eine bestimmte Zeit anhalten


Mit Hilfe der statischen Methode sleep() der Klasse java.lang.Thread können Sie den aktuel-
len Thread für die als Parameter angegebene Zeit in Millisekunden anhalten. So kann dafür
gesorgt werden, dass das System insbesondere bei lang laufenden Schleifen die Möglichkeit
erhält, andere anstehende Aufgaben abzuarbeiten.
Während ein Thread per Thread.sleep() pausiert, kann es vorkommen, dass er beendet oder

System
sonstwie unterbrochen wird. In diesem Fall wird eine InterruptedException geworfen, die auf-
gefangen oder deklariert werden muss:

import java.util.Date;

public class Start {

public static void main(String[] args) {


// Aktuelle Uhrzeit ausgeben
System.out.println(
String.format("Current time: %s", new Date().toString()));

// Thread pausieren
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}

// Aktuelle Uhrzeit ausgeben


System.out.println(
String.format("Current time: %s", new Date().toString()));
}
}

Listing 80: Pausieren eines Threads per Thread.sleep()

77 Timer verwenden
Mit Hilfe der java.util.Timer-Klasse können Aufgaben wiederholt ausgeführt werden. Klas-
sen, die regelmäßig eingebunden werden sollen, müssen von der Basisklasse java.util.Timer-
Task erben und deren run()-Methode überschreiben:

import java.util.TimerTask;
import java.util.Calendar;
import java.text.DateFormat;

public class SimpleTimerTask extends TimerTask {

private boolean running = false;

/**

Listing 81: Die Klasse SimpleTimerTask definiert einen per Timer auszuführenden Task.
198 >> Timer verwenden

* Wird vom Timer regelmäßig aufgerufen und ausgeführt


*/
public void run() {
String date = DateFormat.getTimeInstance().format(
Calendar.getInstance().getTime());
System

String message = running ?


"%s: Still running (%s)" : "%s: Started! (%s)";

// Ausgeben einer Nachricht mit der aktuellen Uhrzeit


System.out.println(String.format(
message, "SimpleTimerTask", date));

running = true;
}
}

Listing 81: Die Klasse SimpleTimerTask definiert einen per Timer auszuführenden Task. (Forts.)

Das Einbinden eines TimerTask geschieht über eine neue Timer-Instanz, deren schedule()-
Methode eine Instanz der TimerTask-Ableitung als Parameter übergeben wird. Weiterhin kann
angegeben werden, wann oder mit welcher Startverzögerung und in welchem Abstand die
Ausführung stattfinden soll. Die Angabe von Startverzögerung und Ausführungsintervall
erfolgt dabei stets in Millisekunden:

import java.util.Timer;

public class Start {

public static void main(String[] args) {


// TimerTask instanzieren
SimpleTimerTask task = new SimpleTimerTask();

// Timer instanzieren
Timer timer = new Timer();

// Task planen
timer.schedule(task, 0, 5000);
}
}

Listing 82: Ausführen eines Timers

Beachten Sie, dass die Ausführung des Programms so lange fortgesetzt wird, wie der
A c h t u ng

Timer aktiv ist. Nach dem Planen des Tasks kann allerdings mit anderen Aufgaben
fortgefahren werden – der Timer verhält sich wie ein eigenständiger Thread, was er
intern auch ist.
>> System 199

System
Abbildung 41: Ausführung des Timers

Neben der hier gezeigten Variante existieren noch weitere Überladungen der schedule()-
Methode:

Überladung Beschreibung
schedule(TimerTask task, Date time) Führt den Task zur angegebenen Zeit aus. Es findet
keine Wiederholung der Ausführung statt.
schedule(TimerTask task, Date firstTime, Führt den Task zur angegebenen Zeit aus und wartet
long period) vor einer erneuten Ausführung die in period angege-
bene Zeitspanne in Millisekunden ab.
schedule(TimerTask task, long delay) Führt den Task nach der in Millisekunden angegebe-
nen Zeitspanne aus. Es findet keine Wiederholung der
Ausführung statt.

Tabelle 26: Weitere Überladungen der schedule()-Methode

78 TimerTasks gesichert regelmäßig ausführen


Wenn TimerTasks zwingend in bestimmten Abständen ausgeführt werden müssen (etwa, wenn
exakt alle 60 Minuten ein Stunden-Signal ertönen soll), kann dies mit Hilfe der Methode
scheduleAtFixedRate() erreicht werden. Diese Methode fängt Verzögerungen, wie sie etwa
durch den GarbageCollector entstehen können, ab und sorgt für eine Ausführung des Timer-
Tasks basierend auf der Systemuhrzeit – die natürlich ihrerseits genau sein sollte.
Die Verwendung von scheduleAtFixedRate() unterscheidet sich nicht wesentlich von der der
schedule()-Methode. Es existieren hier lediglich zwei Überladungen, die die Angabe einer
Startverzögerung oder einer Startzeit sowie eines Ausführungsintervalls in Millisekunden
erlauben:
200 >> Nicht blockierender Timer

import java.util.Timer;

public class Start {

public static void main(String[] args) {


// TimerTask instanzieren
System

SimpleTimerTask task = new SimpleTimerTask();

// Timer instanzieren
Timer timer = new Timer();

// Task planen
timer.scheduleAtFixedRate(task, 0, 5000);
}
}

Listing 83: Ausführen eines Timers, der exakt alle fünf Sekunden läuft

79 Nicht blockierender Timer


Standardmäßig sind Timer blockierend: Sie verhindern, dass die Anwendung, in der sie laufen,
beendet werden kann, solange der Timer noch aktiv ist. Dies kann zu unerwünschten Zustän-
den führen. Um einen Timer automatisch zu beenden, sobald die Anwendung beendet werden
soll, muss er als Dämon-Timer ausgeführt werden. Dies kann durch Übergabe des Werts true
an den Konstruktor der Timer-Instanz erreicht werden:

import java.util.Timer;

public class Start {

public static void main(String[] args) {


// TimerTask instanzieren
SimpleTimerTask task = new SimpleTimerTask();

// Timer als Dämon instanzieren


Timer timer = new Timer(true);

// Task planen
timer.scheduleAtFixedRate(task, 0, 1000);

// Anwendung nach zehn Sekunden beenden


try {
Thread.sleep(10000);
} catch (InterruptedException e) {}
}
}

Listing 84: Dämon-Timer


>> System 201

80 Timer beenden
Timer beenden sich in der Regel, wenn die letzte Referenz auf den Timer entfernt und alle aus-
stehenden Aufgaben ausgeführt worden sind. Dies kann je nach Programmierung einige Zeit
dauern oder bei endlos laufenden Timern schier unmöglich sein.
Aus diesem Grund verfügt die Klasse Timer über die Methode cancel(), die alle noch anstehen-
den und nicht bereits ausführenden Aufgaben abbricht und den Timer beendet:

System
import java.util.Timer;

public class Start {

public static void main(String[] args) {


// TimerTask instanzieren
SimpleTimerTask task = new SimpleTimerTask();

// Timer instanzieren
Timer timer = new Timer();

// Task planen
timer.scheduleAtFixedRate(task, 0, 1000);

// Anwendung zehn Sekunden pausieren


try {
Thread.sleep(10000);
} catch (InterruptedException e) {}

// Timer beenden
timer.cancel();
}
}

Listing 85: Beenden eines Timers über seine cancel()-Methode

81 Auf die Windows-Registry zugreifen


Die berüchtigte Windows-Registry ist eine simple, dateibasierte Datenbank zur Registrierung
von anwendungsspezifischen Werten. Aus einem Java-Programm heraus hat man zwei Mög-
lichkeiten, darauf zuzugreifen:
왘 Das Paket java.util.prefs bietet Klassen und Methoden zum Setzen und Lesen von Ein-
trägen in einem Teilbaum der Registry. Der volle Zugriff auf die Registry ist hiermit aller-
dings nicht möglich. Dafür funktioniert dieser Ansatz auch unter Unix/Linux (wobei eine
XML-Datei erzeugt wird, die als Registry-Ersatz dient).
왘 Einsatz der Windows API für den Zugriff auf die entsprechenden Registry-Funktionen.
Dies erfordert den Einsatz des Java Native Interface (JNI).

Das Paket java.util.prefs


In diesem Paket bietet die Klasse Preferences die statischen Methoden userNode() und system-
Node(), welche die speziellen Registry-Schlüssel HKEY_CURRENT_USER\Software\JavaSoft\Prefs
bzw. HKEY_LOCAL_MACHINE\Software\JavaSoft\Prefs repräsentieren. Unterhalb dieser Einträge
202 >> Auf die Windows-Registry zugreifen

kann ein Java-Programm beliebige eigene Knoten durch Aufruf der Methode node() generie-
ren und diesen dann Schlüssel mit Werten zuweisen bzw. lesen. Dies erfolgt wie bei einer
Hashtabelle mit den Methoden put() und get(). Für spezielle Datentypen wie boolean oder int
stehen auch besondere putXXX()-Methoden bereit, z.B. putBoolean().

import java.util.prefs.*;
System

public class Start {


public static void main(String[] args) {
try {
// Knoten anlegen im Teilbaum HKEY_CURRENT_USER
Preferences userPrefs = Preferences.userRoot().node("/carpelibrum");

// mehrere Schlüssel mit Werten erzeugen


userPrefs.putBoolean("online",true);
userPrefs.put("Name", "Peter");
userPrefs.putInt("Anzahl", 5);

// Alle Schlüssel wieder auslesen


String[] keys = userPrefs.keys();

for(int i = 0; i < keys.length; i++)


System.out.println(keys[i] + " : " + userPrefs.get(keys[i], ""));

} catch(Exception e) {
e.printStackTrace();
}
}
}

Listing 86: Zugriff auf Java-spezifische Registry-Einträge

Der Einsatz des Pakets java.util.prefs erlaubt wie bereits erwähnt nur das Ablegen oder Aus-
lesen von Informationen, die von einem Java-Programm stammen. Es ist nicht möglich,
andere Bereiche der Windows-Registry zu lesen oder zu verändern.

Aufruf der Windows Registry API


Den vollen Zugriff auf die Registry erhält man nur durch Einsatz der Windows API, was bei
einem Java-Programm per JNI erfolgen kann. Glücklicherweise existiert eine sehr brauchbare
OpenSource-Bibliothek namens jRegistryKey, die bereits eine fertige JNI-Lösung bereitstellt, so
dass man nicht gezwungen ist, mit C-Code herumzuhantieren. Laden Sie hierzu von http://
sourceforge.net/projects/jregistrykey/ das Binary-Package jRegistryKey-bin.x.y.z.zip (aktuelle
Version 1.4.3) und gegebenenfalls die Dokumentation herunter. Das ZIP-Archiv enthält zwei
wichtige Dateien:
왘 jRegistryKey.jar: Extrahieren Sie diese Datei und nehmen Sie sie in den CLASSPATH Ihrer
Java-Anwendung auf.
왘 jRegistryKey.dll: Extrahieren Sie diese Datei und kopieren Sie sie in ein Verzeichnis, das in
der PATH-Umgebungsvariable definiert ist, oder in das Verzeichnis, in dem Sie Ihre Java-
Anwendung aufrufen werden.
>> System 203

Die Bibliothek stellt zwei zentrale Klassen bereit: RegistryKey repräsentiert einen Schlüssel
und RegistryValue steht für einen Schlüsselwert. Hierbei gibt es verschiedene Datentypen, die
als Konstanten definiert sind, u.a. ValueType.REG_SZ (null-terminated String = endet mit dem
Zeichen '\0'), ValueType.REG_BINARY (Binärdaten) und ValueType.REG_DWORD (32 Bit Integer).
Das Setzen bzw. Lesen von Registry-Werten erfolgt mit Hilfe der RegistryKey-Methoden set-
Value() bzw. getValue().

System
Das folgende Beispiel demonstriert den Einsatz dieser Klassen:

/**
* Klasse für den Zugriff auf die Windows-Registry via JNI
*/
import ca.beq.util.win32.registry.*;
import java.util.*;

class WindowsRegistry {

/**
* Erzeugt einen neuen Schlüssel
*
* @param root Schlüssel-Wurzel, z.B. RootKey.HKEY_CURRENT_USER
* @param name voller Pfad des Schlüssels
* (z.B. "Software\\Carpelibrum\\ProgData")
* @return RegistryKey-Objekt ode null bei Fehler
* (z.B. schon vorhanden)
*/
public RegistryKey createKey(RootKey root, String name) {
try {
RegistryKey key = new RegistryKey(root, name);
key.create();
return key;

} catch(Exception e) {
e.printStackTrace();
return null;
}
}

/**
* Erzeugt einen neuen Unterschlüssel
* @param root Schlüssel-Wurzel, z.B. RootKey.HKEY_CURRENT_USER
* @param parent voller Pfad des Vaterschlüssels
* (z.B."Software\\Carpelibrum")
* Vaterschlüssel muss existieren
* @param name Name des Unterschlüssels (z.B. "ProgData")
* @return RegistryKey-Objekt oder null bei Fehler
*/
public RegistryKey createSubKey(RootKey root,String parent,String name) {
try {
RegistryKey key = new RegistryKey(root, parent);
RegistryKey sub = key.createSubkey(name);

Listing 87: WindowsRegistry.java


204 >> Auf die Windows-Registry zugreifen

return sub;

} catch(Exception e){
e.printStackTrace();
return null;
}
System

/**
* Löscht einen Schlüssel
*/
public boolean deleteKey(RootKey root, String name) {
try {
RegistryKey key = new RegistryKey(root, name);
key.delete();
return true;

} catch(Exception e) {
e.printStackTrace();
return false;
}
}

/**
* Liefert den gewünschten Schlüssel
*/
public RegistryKey getKey(RootKey root, String name) {
RegistryKey result = null;

try {
result = new RegistryKey(root, name);

if(result.exists() == false)
result = null;

} catch(Exception e) {
e.printStackTrace();
}

return result;
}

/**
* Liefert alle Unterschlüssel zu einem Schlüssel
*/
public ArrayList<RegistryKey> getSubkeys(RootKey root, String name) {
ArrayList<RegistryKey> result = new ArrayList<RegistryKey>();

try {
RegistryKey key = new RegistryKey(root, name);

Listing 87: WindowsRegistry.java (Forts.)


>> System 205

if(key.hasSubkeys()) {
Iterator it = key.subkeys();

while(it.hasNext()) {
RegistryKey rk = (RegistryKey) it.next();
result.add(rk);

System
}
}

} catch(Exception e) {
e.printStackTrace();
}

return result;
}
}

Listing 87: WindowsRegistry.java (Forts.)

Das Start-Programm zu diesem Rezept demonstriert den Zugriff auf die Windows Registry. Es
liest alle Unterschlüssel von HKEY_CURRENT_USER\Software aus und trägt einen eigenen Schlüssel
ein.

import ca.beq.util.win32.registry.*;
import java.util.*;

public class Start {

public static void main(String[] args) {

try {
WindowsRegistry registry = new WindowsRegistry();

// Alle Unterschlüssel von HKEY_CURRENT_USER\Software ausgeben


ArrayList<RegistryKey> subs =
registry.getSubkeys(RootKey.HKEY_CURRENT_USER, "Software");

System.out.println("\nInhalt von HKEY_CURRENT_USER\\Software :");


for(RegistryKey k : subs)
System.out.println(k.getName());

// Schlüssel HKEY_CURRENT_USER\Software\Carpelibrum anlegen falls


// noch nicht vorhanden
RegistryKey key = registry.getKey(RootKey.HKEY_CURRENT_USER,
"Software\\Carpelibrum");

if(key == null)
key = registry.createKey(RootKey.HKEY_CURRENT_USER,
"Software\\Carpelibrum");

Listing 88: Zugriff auf Windows-Registry via JNI


206 >> Abbruch der Virtual Machine erkennen

// Werte setzen
RegistryValue name = new RegistryValue("Name", "Mustermann");
RegistryValue anzahl = new RegistryValue("Anzahl", 5);
key.setValue(name);
key.setValue(anzahl);
System

// Werte wieder auslesen


if(key.hasValues()) {
System.out.println("Werte von " + key.getName());
Iterator it = key.values();

while(it.hasNext()) {
RegistryValue value = (RegistryValue) it.next();
System.out.println(value.getName() + " : " +
value.getStringValue());
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

Listing 88: Zugriff auf Windows-Registry via JNI (Forts.)

Abbildung 42: Auflistung von Registry-Einträgen

82 Abbruch der Virtual Machine erkennen


Da ein Java-Programm immer innerhalb einer Virtual Machine (VM) läuft, führt das Beenden
der VM auch zum Abwürgen des Programms. Dies ist oft unschön, da hierdurch einem Pro-
gramm keine Gelegenheit mehr bleibt, interessante Daten wie bisherige Resultate oder Log-
Informationen auf die Festplatte zu sichern. Seit Java 1.3 gibt es glücklicherweise einen so
genannten ShutdownHook-Mechanismus, der teilweise Abhilfe schafft.
Ein ShutdownHook ist ein Thread, der initialisiert und laufbereit ist, aber während der norma-
len Programmausführung nicht aktiv ist. Erst wenn das Programm beendet wird, greift der
>> System 207

Hook und wird als Letztes ausgeführt, und zwar sowohl bei einem regulären Programmende
als auch einem vorzeitig erzwungenen Ende durch Beenden der VM über die Tastenkombina-
tion (Strg)+(C).

Das Beenden per Taskmanager unter Windows wird leider nicht erkannt. Unter Unix/
H in we is

Linux werden alle Beendigungen erkannt, die das Signal SIGINT auslösen.

System
Der Einsatz des Hook-Mechanismus ist sehr einfach. Man definiert eine eigene Thread-Klasse,
die man mit Hilfe der Methode Runtime.addShutdownHook(Thread t) registriert. Das war es
schon. Falls man nur für bestimmte Programmphasen einen Hook als Rückversicherung haben
möchte, kann man jederzeit mit Runtime.removeShutdownHook(Thread t) den Hook wieder
abmelden. Dies ist beispielsweise praktisch, falls der Hook nur bei einem vorzeitigen Abbruch
abgearbeitet werden soll, aber nicht bei einem regulären Programmende. In diesem Fall sollte
die letzte Anweisung der Aufruf von removeShutdownHook() sein.

/**
* Modellklasse für ShutdownHook zum Abfangen von STRG-C
*/
class ShutdownHook extends Thread {
private Start parent;

public ShutdownHook(Start t) {
parent = t;
}

public void run() {


System.out.println("Programm wurde abgebrochen");
System.out.println("Letzte Zwischensumme: " + parent.getValue());
}
}

Listing 89: ShutdownHook.java

Das Start-Programm zu diesem Rezept addiert in einer for-Schleife Integer-Zahlen. Im Falle


eines vorzeitigen Abbruchs mit (Strg)+(C) wird der ShutdownHook ausgeführt, der den letz-
ten Zwischenwert ausgibt.

/**
* Aufrufbeispiel: STRG-C führt zur Ausgabe des letzten Wertes
*/
public class Start {
private int value = 0;

public void doWork() {


ShutdownHook hook = new ShutdownHook(this);
Runtime rt = Runtime.getRuntime();

Listing 90: Abbruch der Virtual Machine erkennen


208 >> Betriebssystem-Signale abfangen

rt.addShutdownHook(hook);

for(int i = 0 ; i < 50; i++) {


value = value + i;

try {
System

Thread.sleep(500);

} catch(Exception e) {
}
}

System.out.println("Endsumme: " + value);


rt.removeShutdownHook(hook);
}

public int getValue() {


return value;
}

public static void main(String[] args) {


Start s = new Start();
s.doWork();
}
}

Listing 90: Abbruch der Virtual Machine erkennen (Forts.)

Abbildung 43: Abbruch der VM erkennen

83 Betriebssystem-Signale abfangen
Ein Betriebssystem kann jedem laufenden Programm Signale senden. Eines der wichtigsten
Signale ist SIGINT, das einen Programmabbruch signalisiert, wie er auf Windows-Rechnern
beispielsweise ausgelöst wird, wenn innerhalb eines Konsolenfensters die Tastenkombination
(Strg)+(C) gedrückt wird (woraufhin die Java Virtual Machine und damit auch das aus-
geführte Java-Programm abgebrochen werden). Rezept 82 zeigte, wie Sie mit Hilfe eines so
genannten Shutdown-Hooks noch Aufräumarbeiten oder Speicheraktionen etc. durchführen,
bevor das Programm zwangsweise beendet wird. Was aber, wenn das Programm einfach wei-
terarbeiten und das Betriebssystem-Signal ignorieren soll? Dann hilft auch kein Shutdown-
Hook.
>> System 209

Als Lösung bietet Sun zwei inoffizielle Klassen an: sun.misc.Signal sowie sun.misc.Signal-
Handler. Diese Klassen erscheinen in keiner Dokumentation, sie sind offiziell nicht vorhanden,
und es gibt daher keine Garantie dafür, dass sie in zukünftigen Java-Versionen weiterhin vor-
handen sein werden. Eine entsprechende Warnung wird beim Kompilieren ausgegeben. Das
folgende Beispiel demonstriert das Abfangen des Signals SIGINT:
import sun.misc.Signal;

System
import sun.misc.SignalHandler;

public class Start {


public static void main(String[] args) {

// Signal SIGINT ignorieren


Signal.handle(new Signal("INT"), new SignalHandler () {
public void handle(Signal sig) {
System.err.println("SIGINT wird ignoriert. Mache weiter...");
System.err.flush();
}
});

int counter = 0;

while(counter <= 10) {


try {
Thread.sleep(2000);
System.out.println(counter++);

} catch(Exception e) {
}
}
}
}

Listing 91: Signale abfangen

Zur Einrichtung einer Signalbehandlung rufen Sie die statische Methode Signal.handle() auf,
die zwei Parameter erwartet:
왘 Eine Instanz der Klasse Signal, der Sie den Namen des zu fangenden Signals übergeben.
Der Name ist dabei der Betriebssystem-typische Signalname ohne »SIG«, also z.B. INT für
SIGINT.
왘 Eine Instanz der Klasse SignalHandler mit einer Implementierung der Methode handle().
210 >> Betriebssystem-Signale abfangen
System

Abbildung 44: Programm ignoriert Signal SIGINT


Ein- und Ausgabe (IO)

84 Auf die Konsole (Standardausgabe) schreiben


Die normale Konsolenausgabe erfolgt über den Ausgabestream System.out und die allseits
bekannten Methoden print() bzw. println(), z.B.
System.out.println("Hallo Leute!");
System.out.println("Wert von x:" + x); // x sei eine Variable

Formatierte Ausgabe

Ein- und Ausgabe


Der große Nachteil dieser Methoden ist die mangelnde Formatierfähigkeit, was insbesondere
bei der Ausgabe von Gleitkommazahlen sehr unschön ist. (Eine double-Zahl wie 3.141592654
wird exakt so ausgegeben; eine Beschränkung auf beispielsweise zwei Nachkomma-Stellen ist
nicht möglich.) Glücklicherweise bietet Java mittlerweile die Methode printf() an, der man
als ersten Parameter den eigentlichen Ausgabetext – ergänzt um spezielle Formatplatzhalter %
für die auszugebenden Variablenwerte – und anschließend die Variablen übergibt. So gibt der
folgende Code den Wert von pi mit einer Vorkomma- und drei Nachkommastellen aus:
double pi = 3.141592654;
System.out.printf("Die Zahl %1.3f nennt man PI.", pi);
Die Syntax für eine Formatangabe ist:
%[Index$][Flags][Breite][.Nachkomma]Typ
Angaben in [] sind dabei optional1, so dass die einfachste Formatanweisung %Typ lautet.
Breite gibt die Anzahl an auszugebenden Zeichen an. Der Typ definiert die Art der Daten; zur
Verfügung stehen:

Typ Beschreibung
c Darstellung als Unicode-Zeichen
d Dezimal: Integer zur Basis 10
x Hexadezimal: Integer zur Basis 16
f Gleitkommazahl
s String
t Zeit/Datum; auf t folgt ein weiteres Zeichen:
H (Stunde), M (Minute), S (Sekunde), d (Tag), m (Monat), Y (Jahr), D (Datum als Tag-
Monat-Jahr)
% Darstellung des Prozentzeichens

Tabelle 27: Typspezifizierer für printf()

Die wichtigsten Werte für Flags sind: ^ (Umwandlung in Großbuchstaben), + (Vorzeichen


immer ausgeben), 0 (Auffüllen der Breite mit Nullen).

1. Die [] selbst werden nicht angegeben!


212 >> Umlaute auf die Konsole (Standardausgabe) schreiben

import java.util.Date;

public class Start {

public static void main(String[] args) {

int index = 4;
float f= 3.75f;
String txt = "Wahlanteil";
Date dt = new java.util.Date();

System.out.println();
Ein- und Ausgabe

System.out.printf("Nr. %02d %s %2.1f %% Zeit %4$tH:%4$tM:%4$tS \n",


index, txt, f, dt);
}
}

Listing 92: Datenausgabe mit printf()

Abbildung 45: Ausgabe des Start-Programms

85 Umlaute auf die Konsole (Standardausgabe) schreiben


Wenn Sie Java-Strings via System.out auf die Windows-Konsole ausgeben, werden die 16-Bit-
Codes der einzelnen Zeichen auf je 8-Bit zurechtgestutzt, ohne dass dabei allerdings eine kor-
rekte Umkodierung in den 8-Bit-OEM-Zeichensatz der Konsole stattfinden würde. Das traurige
Ergebnis: Die deutschen Umlaute sowie etliche weitere Umlaute und Sonderzeichen, die die
Konsole prinzipiell anzeigen könnte, gehen verloren.
Um die Umlaute dennoch korrekt auszugeben, müssen Sie
왘 entweder auf das in Java 6 neu eingeführte Console-Objekt zurückgreifen
왘 oder die Zeichenkodierung explizit vorgeben.
>> Ein- und Ausgabe (IO) 213

Umlaute über Console ausgeben


Zur formatierten Ausgabe von Strings definiert die Klasse Console eine Methode printf(), die
wie die gleichnamige Methode von System.out arbeitet (siehe Rezept 84) – nur eben mit dem
Unterschied, dass die Zeichen in den OEM-Zeichensatz der Konsole umkodiert werden.
Die Klasse Console instanzieren Sie nicht selbst. Wenn Ihr Java-Code im Kontext einer Java
Virtual Machine-Instanz ausgeführt wird, die mit einem Konsolenfenster verbunden ist,
erzeugt die JVM automatisch intern ein Console-Objekt, welches das Konsolenfenster reprä-
sentiert. Über die statische Console-Methode console() können Sie sich eine Referenz auf die-
ses Objekt zurückliefern lassen.
import java.io.Console;

Ein- und Ausgabe


public class Start {

public static void main(String[] args) {

// Zugriff auf das Console-Objekt


Console cons = System.console();

// Ausgabe
if (cons != null) {
cons.printf("\n");
cons.printf(" Ausgabe der Umlaute mit Console \n");
cons.printf(" ä, ö, ü, ß \n");
}
}
}

Listing 93: Ausgabe von Umlauten auf die Konsole

Für vereinzelte Ausgaben lohnt es sich nicht, den Verweis auf das Console-Objekt in
T ip p

einer eigenen Variablen zu speichern. Hängen Sie in solchen Fällen den printf()-Auf-
ruf einfach an den System.console()-Aufruf an:
System.console().printf("\n Ausgabe der Umlaute mit Console \n");
System.console().printf(" ä, ö, ü, ß \n");

Umlaute über PrintStream ausgeben


Hinter System.out verbirgt sich ein PrintStream-Objekt, das mit der Konsole als Ausgabegerät
verbunden ist und die Standard-Zeichenkodierung verwendet. Wenn Sie eigene PrintStream-
Objekte erzeugen, können Sie diese mit beliebigen Ausgabestreams verbinden und auch die
Zeichenkodierung frei (soweit verfügbar) wählen.

import java.io.*;

public class Start {

public static void main(String[] args) {

PrintStream out;
214 >> Von der Konsole (Standardeingabe) lesen

try {
out = new PrintStream(System.out, true, "Cp850");
} catch (UnsupportedEncodingException e) {
out = System.out;
}

out.printf("\n");
out.printf(" Ausgabe der Umlaute mit PrintStream \n");
out.printf(" ä, ö, ü, ß \n");
}
}

Dem PrintStream-Konstruktor werden drei Argumente übergeben:


Ein- und Ausgabe

왘 ein OutputStream-Objekt, das das Ziel der Ausgabe vorgibt (hier die Konsole)
왘 true, damit die Ausgaben sofort ausgeführt werden (andernfalls werden die Ausgaben
gepuffert, und Sie müssen die Methode flush() aufrufen)
왘 den Namen der gewünschten Zeichenkodierung (hier "Cp850" für die DOS-Codepage 850)

Vor Java 6 war dies der übliche Weg, um Umlaute auf die Konsole auszugeben.
H inwe is

86 Von der Konsole (Standardeingabe) lesen


Konsolenanwendungen bedienen sich zum Einlesen von Daten über die Tastatur traditionell
des Eingabestreams System.in, um den dann meist ein BufferedReader-Objekt aufgebaut wird.
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

try {
System.out.print(" Geben Sie Ihren Namen ein: ");
name = in.readLine();
System.out.print(" Geben Sie Ihr Alter ein: ");
age = Integer.parseInt( in.readLine() );

System.out.printf(" %s (Alter: %d) \n", name, age);


} catch (IOException e) {}

Für ausführlichere Informationen zur Umwandlung von Stringeingaben in Zahlen


Hi nwei s

siehe Rezept 87.


>> Ein- und Ausgabe (IO) 215

Umlaute, die über die Tastatur eingelesen wurden, werden bei Ausgabe auf die Konsole
H in we is

mit System.out korrekt angezeigt. Probleme gibt es allerdings, wenn Sie die eingelese-
nen Strings in eine Datei schreiben oder in eine grafische Benutzeroberfläche ein-
bauen. Dann sollten Sie entweder auf Console umsteigen (siehe unten) oder den
InputStreamReader mit passender Zeichenkodierung erzeugen.

Einlesen mit Console


Seit Java 6 können Sie auch das vordefinierte Console-Objekt zum Einlesen verwenden.
Console cons = System.console();

cons.printf(" Geben Sie Ihren Namen ein: ");

Ein- und Ausgabe


name = cons.readLine();
cons.printf(" Geben Sie Ihr Alter ein: ");
age = Integer.parseInt( cons.readLine() );

cons.printf (" %s (Alter: %d) \n", name, age);


Die auffälligste Veränderung gegenüber der BufferedReader-Konstruktion ist das Wegfallen
der geschachtelten Konstruktoraufrufe und der Exception-Behandlung, wodurch der Code
übersichtlicher wird. Weniger offensichtlich, aber möglicherweise noch interessanter ist, dass
eingelesene Strings, die Umlaute enthalten, problemlos in GUI-Oberflächen eingebaut oder
über das Console-Objekt wieder auf die Konsole ausgegeben werden können. Schließlich kön-
nen Sie den Text zur Eingabeaufforderung direkt an readLine() übergeben:
Console cons = System.console();

name = cons.readLine(" Geben Sie Ihren Namen ein: ");


...

Einlesen mit Scanner


Zum Einlesen und Parsen können Sie sich auch der Klasse Scanner bedienen:
Scanner sc = new Scanner(System.in);

System.out.print(" Geben Sie Ihren Namen ein: ");


name = sc.nextLine();
System.out.print(" Geben Sie Ihr Alter ein: ");
age = sc.nextInt();
sc.nextLine();

System.out.printf(" %s (Alter: %d) \n", name, age);

Umlaute, die über die Tastatur eingelesen wurden, werden bei Ausgabe auf die Konsole
H in we is

mit System.out korrekt angezeigt. Probleme gibt es allerdings, wenn Sie die eingelese-
nen Strings in eine Datei schreiben oder in eine grafische Benutzeroberfläche ein-
bauen. Dann sollten Sie das Scanner-Objekt auf der Basis des internen Readers des
Console-Objekts erzeugen:
Scanner sc = new Scanner(System.console().reader());
Die Ausgabe auf die Konsole muss dann ebenfalls über das Console-Objekt erfolgen.
216 >> Passwörter über die Konsole (Standardeingabe) lesen

87 Passwörter über die Konsole (Standardeingabe) lesen


Das Einlesen von Passwörtern oder anderweitigen sensiblen Daten über die Konsole war in
Java früher ein großes Problem, weil jeder Umstehende die Eingaben mitlesen konnte. Dank
der Console-Methode readPassword() gehören diese Probleme der Vergangenheit an.

import java.io.*;

public class Start {


private final static String PASSWORT = "Sesam";

public static void main(String[] args) {


String name;
Ein- und Ausgabe

char passwort[];

Console cons = System.console();


cons.printf("\n");

cons.printf(" Benutzername eingeben: ");


name = cons.readLine();
cons.printf(" Passwort eingeben: ");
passwort = cons.readPassword();

if (PASSWORT.equals(new String(passwort)))
cons.printf(" %s, Sie sind angemeldet! \n", name);
else
cons.printf(" Anmeldung fehlgeschlagen! \n");
}
}

Listing 94: Geheime Daten in Konsolenanwendungen einlesen

Ausgabe:
Benutzername eingeben: Dirk
Passwort eingeben:
Dirk, Sie sind angemeldet!

88 Standardein- und -ausgabe umleiten


Die Standardstreams System.out, System.in und System.err sind per Voreinstellung mit der
Konsole verbunden. Doch diese Einstellung ist nicht unabänderlich. Mit Hilfe passender
Methoden der Klasse System können sie umgeleitet werden.

Methode Beschreibung
static setIn(InputStream stream) Setzt System.in auf den Eingabestream stream.
static setErr(PrintStream stream) Setzt System.err auf den Ausgabestream stream.
static setOut(PrintStream stream) Setzt System.out auf den Ausgabestream stream.

Tabelle 28: Methoden in System für die Umleitung der Standardein-/-ausgabe


>> Ein- und Ausgabe (IO) 217

Die nachfolgend definierte Klasse TextAreaPrintStream ist beispielsweise geeignet, um die


Standardausgabe in eine JTextArea umzuleiten. Die Klasse muss zu diesem Zweck von Print-
Stream abgeleitet werden – passend zum Argument der setOut()-Methode. Die Referenz auf
die JTextArea übernimmt die Klasse als Konstruktorargument.

import javax.swing.*;
import java.io.*;

/*
* Klasse zur Umleitung der Standardausgabe in eine JTextArea
*/
public class TextAreaPrintStream extends PrintStream {

Ein- und Ausgabe


public TextAreaPrintStream(JTextArea ta) {
super(new TextAreaOutputStream(ta));
}
}

// Hilfsklasse, die OutputStream für JTextArea erzeugt


class TextAreaOutputStream extends OutputStream {
private JTextArea ta;

public TextAreaOutputStream(JTextArea ta) {


this.ta = ta;
}

public void write(int b) {


char c = (char) b;
ta.append(String.valueOf(c));
}
}

Listing 95: TextAreaPrintStream.java – PrintStream-Klasse zur Umleitung der Standardausgabe


in eine JTextArea

Ein kleines Problem ist, dass die Basisklasse PrintStream nur Konstruktoren definiert, die ein
File-Objekt, einen Dateinamen oder einen OutputStream als Argument erwarten. Die Klasse
TextAreaPrintStream löst dieses Problem, indem sie den Basisklassenkonstruktor mit dem Out-
putStream-Argument aufruft – allerdings mit einer abgeleiteten OutputStream-Klasse, deren
Konstruktor die Referenz auf die JTextArea-Instanz übergeben werden kann. In dieser Output-
Stream-Klasse wird dann die write()-Methode überschrieben, die die an die Standardausgabe
geschickten Zeichencodes in die JTextArea schreibt.

Zur Erinnerung: Der Konstruktor einer abgeleiteten Klasse ruft als erste Anweisung
H in we i s

immer einen Konstruktor der Basisklasse auf. Ist ein entsprechender super-Aufruf im
Quelltext des Konstruktors nicht vorgesehen, erweitert der Java-Compiler den Kon-
struktorcode automatisch um den Aufruf eines Standardkonstruktors (Konstruktor
ohne Parameter) der Basisklasse.
218 >> Standardein- und -ausgabe umleiten

Das Programm zu diesem Rezept ist ein GUI-Programm, dessen ContentPane mittels einer
JSplitPane-Instanz in zwei Bereiche unterteilt ist:
왘 einem Arbeitsbereich mit zwei JButton-Instanzen, die beim Drücken einen Text an die
Standardausgabe schicken,
왘 einen Logging-Bereich mit der JTextArea-Komponente, in die die Ausgaben umgeleitet
werden.

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
Ein- und Ausgabe

public class Start extends JFrame {

public Start() {

// Hauptfenster konfigurieren
setTitle("Umlenken der Standardausgabe");

// Panel mit zwei Schaltern


JPanel p = new JPanel();
JButton btn1 = new JButton("A ausgeben");
btn1.setFont(new Font("Dialog", Font.PLAIN, 24));
btn1.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(" A");
}
});
JButton btn2 = new JButton("B ausgeben");
btn2.setFont(new Font("Dialog", Font.PLAIN, 24));
btn2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println(" B");
}
});
p.add(btn1);
p.add(btn2);

// JTextArea zum Protokollieren der Schalterklicks


JScrollPane scrollpane = new JScrollPane();
JTextArea logpane = new JTextArea();
scrollpane.getViewport().add(logpane, null);

// Schalter-Panel und JTextArea in SplitPane einfügen


JSplitPane splitpane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
p, scrollpane);
getContentPane().add(splitpane, BorderLayout.CENTER);

Listing 96: Start.java – Umlenken der Standardausgabe in eine JTextArea


>> Ein- und Ausgabe (IO) 219

// Standardausgabe auf JTextArea umlenken


TextAreaPrintStream out = new TextAreaPrintStream(logpane);
System.setOut(out);

setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

public static void main(String args[]) {


Start frame = new Start();
frame.setSize(500,300);
frame.setLocation(300,300);

Ein- und Ausgabe


frame.setVisible(true);
}
}

Listing 96: Start.java – Umlenken der Standardausgabe in eine JTextArea (Forts.)

Abbildung 46: JTextArea mit umgelenkten println()-Ausgaben

89 Konsolenanwendungen vorzeitig abbrechen


Konsolenanwendungen, die sich aufgehängt haben oder deren Ende Sie nicht mehr abwarten
möchten, können auf den meisten Betriebssystemen durch Drücken der Tastenkombination
(Strg) +(C)

abgebrochen werden.

90 Fortschrittsanzeige für Konsolenanwendungen


Auch Konsolenanwendungen führen hin und wieder länger andauernde Berechnungen durch,
ohne dass irgendwelche Ergebnisse auf der Konsole angezeigt werden. Ungeduldige Anwender
kann dies dazu verleiten, das Programm – in der falschen Annahme, es sei bereits abgestürzt –
abzubrechen. Um dem vorzubeugen, sollten Sie das Programm in regelmäßigen Abständen
Lebenszeichen ausgeben lassen.
220 >> Fortschrittsanzeige für Konsolenanwendungen

Es gibt unzählige Wege, eine Fortschrittsanzeige zu implementieren. Entscheidend ist, eine


passende Form und einen geeigneten Zeitabstand zwischen den einzelnen Lebenszeichen
(respektive Aktualisierungen der Fortschrittsanzeige) zu finden:
왘 Die Lebenszeichen sollten den Anwender unaufdringlich informieren.
Lebenszeichen, die vom Anwender bestätigt werden müssen, scheiden in 99% der Fälle
ganz aus. Gleiches gilt für die Ausgabe von akustischen Signalen. (Gegen die Verbindung
des Endes der Berechnung mit einem akustischen Signal ist jedoch nichts einzuwenden.)
왘 Andere Ausgaben sollten durch die Fortschrittsanzeige möglichst wenig gestört werden.
Bei Ausgabe von Lebenszeichen auf die Konsolen sollten Sie vor allem darauf achten, dass
die Konsole nicht unnötig weit nach unten gescrollt wird.
Ein- und Ausgabe

Gut geeignet ist die periodische Ausgabe eines einzelnen Zeichens, beispielsweise eines
Punkts, ohne Leerzeichen oder Zeilenumbrüche:
System.out.print(".");
왘 Die Lebenszeichen sollten nicht zu schnell aufeinander folgen, aber auch nicht zu lange
auf sich warten lassen.
왘 Die Anzahl der Lebenszeichen sollte größer als 4 sein, aber nicht zu groß werden. (Droht
die Konsole mit Lebenszeichen überschwemmt zu werden, so setzen Sie lieber den Zeitab-
stand zwischen den Lebenszeichen herauf.)
왘 Informieren Sie den Anwender vorab, dass nun mit einer längeren Wartezeit zu rechnen
ist.

Nachdem Sie Form und Frequenz der Lebenszeichen ungefähr festgelegt haben, müssen Sie
überlegen, wie Sie für eine periodische Ausgabe der Lebenszeichen sorgen.

Fortschrittsanzeigen mit Schleifen


Wenn die Berechnung, deren Fortschreiten Sie durch Lebenszeichen verdeutlichen wollen,
eine große äußere Schleife durchläuft, bietet es sich an, die Lebenszeichen innerhalb dieser
Schleife auszugeben:

// 1. Zeitaufwendige Berechnung ankündigen


System.out.print(" Bitte warten");

for(int i = 0; i < 12; ++i) {


// tue so, als würde intensiv gerechnet
Thread.sleep(400);

// 2. Lebenszeichen periodisch ausgeben


System.out.print(".");
}
System.out.println();

// 3. Ergebnis anzeigen
System.out.println("\n Berechnung beendet.\n");

Listing 97: Aus Start.java – Lebenszeichen mit Schleife


>> Ein- und Ausgabe (IO) 221

Bei dieser Form entspricht die Anzahl der Lebenszeichen den Durchläufen der Schleife. Wird
die Schleife sehr oft durchlaufen, setzen Sie die Anzahl der Lebenszeichen herab, indem Sie
nur bei jedem zweiten, dritten ... Schleifendurchgang Lebenszeichen ausgeben lassen:
if( i%2)
System.out.print(".");
Wird die Schleife zu selten durchlaufen, müssen Sie mehrere Lebenszeichen über die Schleife
verteilen (eventuell gibt es innere Schleifen, die sich besser zur Ausgabe der Lebenszeichen
eignen).

Fortschrittsanzeigen mit Timern


Durch Timer gesteuerte Fortschrittsanzeigen sind aufwändiger zu implementieren, haben aber

Ein- und Ausgabe


den Vorzug, dass die Lebenszeichen in exakt festgelegten Zeitintervallen ausgegeben werden
können.
Zuerst definieren Sie eine TimerTask-Klasse, in deren run()-Methode Sie ein Lebenszeichen
ausgeben lassen, beispielsweise:

import java.util.TimerTask;

/**
* TimerTask-Klasse für Konsolen-Fortschrittsanzeige
*/
class ShowProgressTimer extends TimerTask {

public void run() {


System.out.print(".");
}
}

Listing 98: Einfache TimerTask-Klasse für Konsolen-Fortschrittsanzeigen

Danach wechseln Sie zum Code der Berechnung. Vor dem Start der Berechnung geben Sie eine
Vorankündigung aus, erzeugen ein Timer-Objekt und übergeben diesem eine Instanz der
TimerTask-Klasse, eine anfängliche Verzögerung und die Dauer des Zeitintervalls (in Milli-
sekunden).
Anschließend folgt die eigentliche Berechnung, während im Hintergrund der Thread des
Timers ausgeführt und in den festgelegten periodischen Abständen die run()-Methode des
TimerTask-Objekts ausgeführt wird.
Nach Abschluss der Berechnung beenden Sie den Timer.

int aMethod() throws InterruptedException {


// Zeitaufwändige Berechnung ankündigen
System.out.print(" Bitte warten");

// Zeitgeber für Fortschrittsanzeige starten und alle 400 ms


// ausführen lassen.
Timer timer = new Timer();

Listing 99: Aus Start.java – Lebenszeichen mit Timer


222 >> Konsolenmenüs

timer.schedule(new ShowProgressTimer(), 0, 400);

// tue so, als würde intensiv gerechnet


Thread.sleep(12*1000);

// Zeitgeber für Fortschrittsanzeige beenden


timer.cancel();
System.out.println();

// Ergebnis zurückliefern
return 42;
}
Ein- und Ausgabe

Listing 99: Aus Start.java – Lebenszeichen mit Timer (Forts.)

Abbildung 47: Fortschrittsanzeigen in Konsolenanwendungen

91 Konsolenmenüs
Bei Menüs denken die meisten Anwender an Menüs von GUI-Anwendungen. Doch auch Kon-
solenanwendungen können einen Leistungsumfang erreichen, der eine menügesteuerte Pro-
grammführung opportun macht.
Die Implementierung eines Konsolenmenüs besteht aus drei Schritten:
1. Anzeigen des Menüs
Die Ausgabe erfolgt zeilenweise mit println()-Aufrufen. Zu jedem Befehl muss ein Code
angegeben werden, über den der Anwender den Befehl auswählen kann. Gut geeignet sind
hierfür Zeichen, Integer-Werte oder Aufzählungskonstanten, da diese in Schritt 3 mittels
einer switch-Verzeigung ausgewertet werden können.
System.console().printf(" Erster Menübefehl <a> \n");
System.console().printf(" Zweiter Menübefehl <b> \n");
...
2. Abfragen der Benutzerauswahl
Der Anwender wird aufgefordert, den Code für einen Befehl einzugeben. Ungültige Einga-
ben, soweit sie nicht in Schritt 3 vom default-Block der switch-Anweisung abgefangen
werden (beispielsweise Strings aus mehr als einem Zeichen), müssen aussortiert werden.
>> Ein- und Ausgabe (IO) 223

3. Abarbeiten des Menübefehls


Typischerweise in Form einer switch-Verzweigung.

Grundstruktur
Soll das Menü nicht nur einmalig zu Beginn des Programms angezeigt werden, gehen Sie so
vor, dass Sie die obigen drei Schritte in eine do-while-Schleife fassen und das Menü um einen
Menübefehl zum Verlassen des Programms erweitern. Wird dieser Menübefehl ausgewählt,
wird die do-while-Schleife und damit das Programm beendet.

import java.util.Scanner;

public class Start {

Ein- und Ausgabe


public static void main(String[] args) {

final char NO_OPTION = '_';

Scanner scan = new Scanner(System.console().reader());


String input;
char option = NO_OPTION;

// Schleife, in der Menue wiederholt angezeigt und Befehle abgearbeitet


// werden, bis Befehl zum Beenden des Programms ausgewählt und die
// Schleife verlassen wird

do {
// 1. Menu anzeigen

System.console().printf("\n");
System.console().printf(" ******************************** \n");
System.console().printf(" Menü \n");
System.console().printf("\n");
System.console().printf(" Erster Menübefehl <a> \n");
System.console().printf(" Zweiter Menübefehl <b> \n");
System.console().printf(" Dritter Menübefehl <c> \n");
System.console().printf(" Programm beenden <q> \n");
System.out.print("\n Ihre Eingabe : ");

// 2. Eingabe lesen
option = NO_OPTION;

input = scan.nextLine();
if(input.length() == 1) // einzelnes Zeichen in Eingabe
option = input.charAt(0);

System.out.println("\n");

Listing 100: Konsolenanwendung mit Menü


224 >> Konsolenmenüs

// 3. Menübefehl abarbeiten

switch(option) {
case 'a': System.console().printf(" Menübefehl a \n");
break;
case 'b': System.console().printf(" Menübefehl b \n");
break;
case 'c': System.console().printf(" Menübefehl c \n");
break;
case 'q': System.console().printf(" Programm wird beendet \n");
break;
default: System.console().printf(" Falsche Eingabe \n");
}
Ein- und Ausgabe

System.out.println("\n");

// 4. Warten, bis Anwender fortfahren will

System.console().printf(" <Enter> drücken zum Fortfahren \n");


scan.nextLine();
} while(option != 'q');
}
}

Listing 100: Konsolenanwendung mit Menü (Forts.)

Schritt 2 liest eine Eingabe von der Tastatur ein. Besteht die Eingabe aus einem einzelnen Zeichen,
wird sie in option gespeichert und so an die switch-Anweisung weitergegeben. Hat der Anwender
aus Versehen mehrere Tasten gedrückt, wird der Standardwert NO_OPTION weitergereicht. (NO_
OPTION wurde zu Beginn der main()-Methode mit einem Zeichen initialisiert, das keinem Menü-
befehl entspricht. NO_OPTION wird daher vom default-Block der switch-Anweisung behandelt.)

Statt in der Schleifenbedingung zu prüfen, ob die Schleife weiter auszuführen ist


T ip p

(while(option != 'q')), können Sie die Schleife auch mit einer Label-Sprunganweisung
aus der switch-Verzweigung heraus verlassen:
quit: while(true) {
...
switch(option) {
...
case 'q': System.console().printf(" Programm wird beendet \n");
break quit;
default: System.console().printf(" Falsche Eingabe \n");
}
}
>> Ein- und Ausgabe (IO) 225

Groß- und Kleinschreibung unterstützen


Wenn Sie im Buchstabencode zu den Menübefehlen nicht zwischen Groß- und Kleinschrei-
bung unterscheiden, können Sie die switch-Verzweigung nutzen, um Groß- und Kleinbuchsta-
ben elegant auf die gleichen Menübefehle abzubilden:
switch(option) {
case 'A':
case 'a': System.console().printf(" Menübefehl a \n");
break;
case 'B':
case 'b': System.console().printf(" Menübefehl b \n");
break;
case 'C':

Ein- und Ausgabe


case 'c': System.console().printf(" Menübefehl c \n");
break;
case 'Q': option = 'q';
case 'q': System.console().printf(" Programm wird beendet \n");
break;
default: System.console().printf(" Falsche Eingabe \n");
}

Die Anweisung option = 'q' ist nötig, damit die do-while(option != 'q') auch bei Ein-
Achtung

gabe von Q beendet wird. Wird die Schleife wie im vorangehenden Absatz beschrieben
mit einer Sprunganweisung verlassen, kann die Anweisung entfallen.

92 Automatisch generierte Konsolenmenüs


Mit der Klasse ConsoleMenu, die in diesem Rezept vorgestellt wird, können Sie Konsolenmenüs
auf der Basis von Textdateien erstellen. Die Arbeit zur Implementierung eines Konsolenmenüs
reduziert sich damit auf die Bearbeitung der Textdatei und das Aufsetzen der switch-Verzwei-
gung zur Behandlung der verschiedenen Menübefehle. Die Titel der Menübefehle können
jederzeit in der Textdatei geändert werden, ohne dass die Java-Quelldatei neu kompiliert wer-
den muss (beispielsweise zur Lokalisierung des Programms).
Die Textdatei mit den Menübefehlen besitzt folgendes Format:
왘 Jede Zeile repräsentiert einen Menübefehl.
왘 Jede Zeile beginnt mit dem Zeichen, das später zum Aufruf des Menübefehls einzugeben
ist. Danach folgt ein Semikolon und anschließend der Titel des Menübefehls.
왘 Der Titel darf kein Semikolon enthalten.
왘 Zwischen Codezeichen, Semikolon und Titel dürfen keine anderen Zeichen (auch kein
Whitespace) stehen.

a;Erster Menübefehl
b;Zweiter Menübefehl
c;Dritter Menübefehl
q;Programm beenden

Listing 101: Beispiel für eine Menütextdatei


226 >> Automatisch generierte Konsolenmenüs

Die Klasse ConsoleMenu erzeugt aus dieser Datei das folgende Menü:
********************************
Menue

Erster Menübefehl...........<a>
Zweiter Menübefehl..........<b>
Dritter Menübefehl..........<c>
Programm beenden.............<q>

Ihre Eingabe : q
Das Füllzeichen zwischen Befehlstitel und -code (hier der Punkt .) wird als Argument an den
Konstruktor von ConsoleMenu übergeben. Die Menüüberschrift und die Eingabeaufforderung
Ein- und Ausgabe

können in abgeleiteten Klassen durch Überschreibung der Methoden printHeader() bzw.


printPrompt() angepasst werden.
Das Einlesen des Menüs geschieht vollständig im Konstruktor, dem zu diesem Zweck der Name
der Textdatei und das zu verwendende Füllzeichen übergeben werden. Der Konstruktor liest
in einer while-Schleife die Zeilen der Textdatei, extrahiert daraus die Informationen für die
Menübefehle und speichert diese in einem Objekt der Hilfsklasse MenuElem. Die MenuElem-
Objekte wiederum werden in einer Vector-Collection verwaltet.
Während des Einlesens bestimmt der Konstruktor zusätzlich die Zeichenlänge des größten Titels
(maxLength) sowie den numerischen Code des »größten« verwendeten Menübefehlszeichens. Im
Anschluss an die while-Schleife füllt der Konstruktor alle Titel bis auf maxLength+10 Zeichen mit
dem übergebenen Füllzeichen auf (damit die Menübefehlszeichen später rechtsbündig unterein-
ander ausgegeben werden). Der numerische Code wird benötigt, um die Konstante NO_OPTION
sicher mit einem Zeichen initialisieren zu können, das mit keinem Menübefehl verbunden ist.
Der Konstruktor übernimmt alle nötigen Dateioperationen. Werden dabei Exceptions ausge-
löst, werden diese an den Aufrufer weitergegeben. Erkennt der Konstruktor Fehler im Dateifor-
mat, löst er eine Exception der selbst definierten Klasse ParseMenuException aus.
import java.io.FileReader;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Vector;
import java.util.Scanner;

/**
* Klasse zum Aufbau von Konsolenmenüs
*/
public class ConsoleMenu {

private class MenuElem { // Hilfsklasse für Menüelemente


private char code;
private String title;

MenuElem(char code, String title) {


this.code = code;
this.title = title;
}
}
>> Ein- und Ausgabe (IO) 227

public final char NO_OPTION; // nicht belegtes Zeichen


private Vector<MenuElem> menu = new Vector<MenuElem>(7); // Vektor mit
// Menübefehlen

public ConsoleMenu(String filename, char paddChar)


throws IOException, ParseMenuException {

// Textdatei mit Menü öffnen


BufferedReader in = new BufferedReader(new FileReader(filename));

// Für jede Zeile Menübefehlinformationen auslesen, in MenuElem-Objekt


// speichern und in Vector ablegen

Ein- und Ausgabe


String line;
int maxCode = 0;
int maxLength = 0;
char code;
String title;

// Menübefehle einlesen
while( (line = in.readLine()) != null) {

// kurz prüfen, ob Zeile korrekt aufgebaut ist


if( line.charAt(1) != ';'
|| line.length() < 3
|| line.indexOf(';', 2) != -1) {

menu.clear();
in.close();
throw new ParseMenuException("Fehler beim Parsen der " +
" Menuedatei");
}

code = line.charAt(0);
title = line.substring(2);

// Größte Titellänge festhalten


maxLength = (title.length() > maxLength)
? title.length() : maxLength;

// Größten Zeichencode festhalten


maxCode = (Character.getNumericValue(code) > maxCode)
? Character.getNumericValue(code) : maxCode;

menu.add(new MenuElem(code, title));


}

// Alle Menütitel auf gleiche Länge plus 10 Füllzeichen bringen


int diff;

for(MenuElem e : menu) {
diff = (maxLength + 10) - e.title.length();
char[] pad = new char[diff];
for(int i = 0; i < pad.length; ++i)
228 >> Automatisch generierte Konsolenmenüs

pad[i] = paddChar;

e.title += new String(pad);


}

// Zeichen bestimmen, das mit keinem Menübefehl verbunden ist


NO_OPTION = (char) (100+maxCode);

in.close();
}

protected void printHeader() {


System.console().printf("\n");
Ein- und Ausgabe

System.console().printf(" ******************************** \n");


System.console().printf(" Menü \n");
System.console().printf("\n");
}
protected void printPrompt() {
System.out.println();
System.out.print(" Ihre Eingabe : ");
}

public void printMenu() {


printHeader();

for(MenuElem e : menu)
System.console().printf(" %s<%s> \n", e.title, e.code);

printPrompt();
}

public char getUserOption() {


String input;
char option = NO_OPTION;
Scanner scan = new Scanner(System.console().reader());

input = scan.nextLine();
if(input.length() == 1) // einzelnes Zeichen in Eingabe
option = input.charAt(0);

System.out.println("\n");
return option;
}
}
Für die Ausgabe des Menüs müssen Sie lediglich die Methode printMenu() aufrufen. Die Ein-
gabe des Anwenders können Sie selbst einlesen oder bequem mit getUserOption() abfragen,
siehe Listing 102.

public class Start {

public static void main(String args[]) {

Listing 102: Verwendung der Klasse ConsoleMenu in einem Konsolenprogramm


>> Ein- und Ausgabe (IO) 229

char option;
ConsoleMenu menu;

System.out.println();

try {
menu = new ConsoleMenu("Menu.txt", '.');

quit: while(true) {

menu.printMenu();
option = menu.getUserOption();

Ein- und Ausgabe


switch(option) {
case 'A':
case 'a': System.console().printf(" Menübefehl a \n");
break;
case 'B':
case 'b': System.console().printf(" Menübefehl b \n");
break;
case 'C':
case 'c': System.console().printf(" Menübefehl c \n");
break;
case 'Q': option = 'q';
case 'q': System.console().printf(" Programm wird beendet \n");
break quit;
default: System.console().printf(" Falsche Eingabe \n");
}

System.out.println("\n");

System.console().printf(" <Enter> drücken zum Fortfahren \n");


scan.nextLine();
} // Ende while

} catch(Exception e) {
System.err.println("Fehler: " + e.getMessage());
}
}
}

Listing 102: Verwendung der Klasse ConsoleMenu in einem Konsolenprogramm (Forts.)


230 >> Konsolenausgaben in Datei umleiten
Ein- und Ausgabe

Abbildung 48: Konsolenmenü

93 Konsolenausgaben in Datei umleiten


Ausgaben, die zur Konsole geschickt werden, lassen sich auf den meisten Betriebssystemen
durch Piping in Dateien umleiten.
Auf diese Weise können die Ausgaben auf bequeme Weise dauerhaft gespeichert werden, was
etliche Vorteile bringt: Sie können die Ausgaben mit den Ergebnissen späterer Programm-
sitzungen vergleichen. Die Ausgaben lassen sich mit anderen Programmen elektronisch wei-
terverarbeiten. Sie können umfangreiche Ausgaben in einen Editor laden und mit dessen
Suchfunktion durchgehen.
Unter Windows leiten Sie die Ausgaben mit > in eine Textdatei um.
java ProgrammName > Output.txt
Unter Linux bieten die meistens Shells gleich mehrere Symbole für die Umleitung von Konso-
lenausgaben in Dateien an. bash, csh und tcsh unterstützen beispielsweise
왘 > die Umleitung durch Überschreiben,
왘 >> die Umleitung durch Anhängen,
왘 >| die erzwungene Umleitung.

94 Kommandozeilenargumente auswerten
Konsolenanwendungen besitzen die erfreuliche Eigenschaft, dass man ihnen beim Aufruf
Argumente mitgeben kann – beispielsweise Optionen, die das Verhalten des Programms steu-
ern, oder zu verarbeitende Daten. Der Java-Interpreter übergibt diese Argumente beim Pro-
grammstart an den args-Array-Parameter der main()-Methode. In der main()-Methode können
Sie die im Array gespeicherten Kommandozeilenargumente abfragen und auswerten.
Das Programm zu diesem Rezept erwartet auf der Kommandozeile drei Argumente: eine Zahl,
ein Operatorsymbol und eine zweite Zahl. Es prüft vorab, ob der Anwender beim Aufruf die
korrekte Anzahl Argumente übergeben hat.
>> Ein- und Ausgabe (IO) 231

Bei einer abweichenden Anzahl weist das Programm den Anwender auf die korrekte Aufruf-
syntax hin und beendet sich selbst.
Stimmt die Anzahl der Argumente, wandelt das Programm die Argumente in die passenden
Datentypen um (Kommandozeilenargumente sind immer Strings) und berechnet, sofern die
Typumwandlung nicht zur Auslösung einer NumberFormatException geführt hat, die gewünschte
Operation.

public class Start {

public static void main(String args[]) {


System.out.println();

Ein- und Ausgabe


// Prüfen, ob korrekte Anzahl Kommandozeilenargumente vorhanden
if (args.length != 3) {
System.out.println("Falsche Anzahl Argumente in Kommandozeile");
System.out.println("Aufruf: java Start "
+ "Zahl Operator Zahl <Return>\n");
System.exit(0);
}

try {

// Die Kommandozeilenargumente umwandeln


double zahl1 = Double.parseDouble(args[0]);
char operator = args[1].charAt(0);
double zahl2 = Double.parseDouble(args[2]);

System.out.print("\n " + zahl1 + " " + operator + " " + zahl2);

// Befehl bearbeiten
switch(operator) {
case '+': System.out.println(" = " + (zahl1 + zahl2));
break;
case '-': System.out.println(" = " + (zahl1 - zahl2));
break;
case 'X':
case 'x':
case '*': System.out.println(" = " + (zahl1 * zahl2));
break;
case ':':
case '/': System.out.println(" = " + (zahl1 / zahl2));
break;
default: System.out.println("Operator nicht bekannt");
break;
}

} catch (NumberFormatException e) {
System.err.println(" Ungueltiges Argument");
}
}
}

Listing 103: Start.java – Verarbeitung von Kommandozeilenargumenten


232 >> Leere Verzeichnisse und Dateien anlegen

Abbildung 49: Übergabe von Aufrufargumenten an ein Konsolenprogramm


Ein- und Ausgabe

Auf der Windows-Konsole führen Aufrufe mit * möglicherweise dazu, dass der
H i n we i s

Befehlszeileninterpreter von Windows eine falsche Zahl an Argumenten übergibt.


Setzen Sie * dann in Anführungszeichen: »*«.

95 Leere Verzeichnisse und Dateien anlegen


Neues, leeres Verzeichnis anlegen
Das Anlegen von leeren Verzeichnissen erfolgt einfach durch Aufruf der Methode
File.mkdir(), z.B.
import java.io.*;

File f = new File(".\\meinVerzeichnis");


boolean status = f.mkdir();
Hier lauert allerdings eine kleine Falle: Wenn man einen Pfad angibt, der Verzeichnisse ent-
hält, die es selbst noch nicht gibt, z.B.
File f = new File(".\\neuesVerzeichnis\\neuesUnterverzeichnis");
boolean status = f.mkdir(); // liefert false!
... scheitert der Aufruf. In solchen Fällen kann man auf die wenig bekannte Methode mkdirs()
zurückgreifen, die alle Verzeichnisse auf dem angegebenen Pfad erzeugt, falls sie noch nicht
vorhanden sind:
boolean status = f.mkdirs(); // liefert nun true

Neue, leere Datei anlegen


Das Anlegen einer leeren Datei war in den Anfangszeiten von Java recht umständlich, da man
mit Hilfe einer Ausgabeklasse wie FileOutputStream eine explizite write()-Operation durch-
führen musste, die null Bytes schrieb, gefolgt vom Schließen des Ausgabestreams mit close().
Mittlerweile geht dies aber deutlich einfacher mit der File-Methode createNewFile():
File f = new File(".\\temp\\leereDatei.txt");
boolean status = f.createNewFile();
Falls man hierbei einen vollen Pfadnamen (wie im Beispiel) angibt, müssen allerdings die ent-
sprechenden Verzeichnisse bereits existieren. Es kann daher recht praktisch sein, eine eigene
Methode createNewFile() zu definieren, die zuvor sicherstellt, dass der Pfad an sich existiert:
>> Ein- und Ausgabe (IO) 233

import java.io.*;

class FileUtil {

/**
* Erzeugt eine neue leere Datei; Pfad wird ggf. erzeugt
*
* @param name relativer oder absoluter Dateiname
* @return true bei Erfolg, sonst false (Datei existert schon
* oder keine Schreibrechte)
*/
public static boolean createNewFile(String name) {

Ein- und Ausgabe


boolean result;
File f = new File(name);

try {
// zuerst mal so probieren
result = f.createNewFile();

} catch(Exception e) {
result = false;
}

try {
if(result == false) {
// sicherstellen, dass Pfad existiert und nochmal probieren
int pos = name.lastIndexOf(File.separatorChar);

if(pos >= 0) {
String path = name.substring(0,pos);
File p = new File(path);
result = p.mkdirs();

if(result)
result = f.createNewFile();
}
}

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}
}

Listing 104: Methode zum sicheren Anlegen neuer Dateien (aus FileUtil.java)
234 >> Datei- und Verzeichniseigenschaften abfragen

Das Start-Programm zu diesem Rezept

public class Start {

public static void main(String[] args) {

boolean result = FileUtil.createNewFile(".\\temp\\test.txt");

if(result)
System.out.println("Leere Datei angelegt!");
else
System.out.println("Datei konnte nicht erzeugt werden!");
}
Ein- und Ausgabe

Listing 105: Neue Datei anlegen

96 Datei- und Verzeichniseigenschaften abfragen


Neben einigen offensichtlichen Eigenschaften wie Dateinamen lassen sich über die Klasse
java.io.File weitere interessante Eigenschaften ermitteln. Das nachfolgende Codeschnipsel
zeigt ein Beispiel für das Ermitteln häufig benötigter Informationen wie Dateigröße, Datei-
namen (absolut und relativ), Wurzel des Dateipfads und Zugriffsrechte:

import java.io.*;
import java.util.Date;

/**
* Klasse zur Ermittlung von Datei-/Verzeichniseigenschaften
*/
class FileInfo {
private String fileName;
private File file;

public FileInfo(String name) {


fileName = name;

try {
file = new File(name);

} catch(Exception e) {
e.printStackTrace();
}
}

// Liefert true, wenn die Datei existiert


public boolean exists() {
try {
return file.exists();

Listing 106: FileInfo.java – Klasse zur Abfrage von Datei-/Verzeichniseigenschaften


>> Ein- und Ausgabe (IO) 235

} catch(Exception e) {
e.printStackTrace();
return false;
}
}

// Liefert den vollen Dateinamen inkl. Pfad oder null bei Fehler
public String getAbsoluteName() {
try {
return file.getCanonicalPath();

} catch(Exception e) {
e.printStackTrace();

Ein- und Ausgabe


return null;
}
}

// Liefert den Dateinamen ohne Pfad oder null bei Fehler


public String getName() {
if(file != null)
return file.getName();
else
return null;
}

// Dateigröße in Bytes oder -1 bei Fehler


public long getSize() {
long result = -1;

if(file != null)
result = file.length();

return result;
}

// Liefert das Wurzelverzeichnis (z.B. d:\) für die aktuelle Datei


// oder null bei Fehler
public File getRoot() {
try {
File[] roots = File.listRoots();

for(File f : roots) {
String path = f.getCanonicalPath();

if(getAbsoluteName().startsWith(path))
return f;
else
continue;
}

Listing 106: FileInfo.java – Klasse zur Abfrage von Datei-/Verzeichniseigenschaften (Forts.)


236 >> Datei- und Verzeichniseigenschaften abfragen

} catch(Exception e) {
e.printStackTrace();
}

// nichts gefunden -> Fehler


return null;
}

// Liefert das Vaterverzeichnis oder null bei Fehler


public File getParent() {
if(file != null)
return file.getParentFile();
else
Ein- und Ausgabe

return null;
}

// Liefert die Zugriffsrechte als "r" (lesen) oder "rw" (


// lesen und schreiben) oder "" (gar keine Rechte)
public String getAccessRights() {
if(file != null) {
if(file.canWrite())
return "rw";
else if(file.canRead())
return "r";
else
return "";

} else
return null;
}

// Liefert das Datum der letzten Änderung oder null bei Fehler
public Date getLastModified() {
if(file != null) {
long time = file.lastModified();
return (new Date(time));

} else
return null;

// Liefert true, wenn es ein Verzeichnis ist


public boolean isDirectory() {
if(file != null && file.isDirectory())
return true;
else
return false;

}
}

Listing 106: FileInfo.java – Klasse zur Abfrage von Datei-/Verzeichniseigenschaften (Forts.)


>> Ein- und Ausgabe (IO) 237

Das Start-Programm demonstriert Aufruf und Verwendung.

public class Start {

public static void main(String[] args) {

FileInfo fi = new FileInfo("test.txt");


System.out.println("Zugriffsrechte: " + fi.getAccessRights());
}
}

Listing 107: Dateieigenschaften ermitteln

Ein- und Ausgabe


97 Temporäre Dateien anlegen
Temporäre Dateien, also Dateien, die nur vorübergehend während der Programmausführung
benötigt werden, lassen sich natürlich als ganz normale Dateien (beispielsweise wie in Rezept
95 gezeigt) anlegen und einsetzen. Die Java-Bibliothek bietet jedoch in der Klasse
java.io.File spezielle Methoden an, mit denen sich der Einsatz von temporären Dateien etwas
vereinfachen lässt. Mit createTempFile(String prefix, String suffix) lassen sich beliebig
viele Dateien im Standard-Temp-Verzeichnis erzeugen2. Jede erzeugte Datei fängt dabei mit
dem übergebenen String prefix an, gefolgt von einer automatisch erzeugten, fortlaufenden
Zahl und dem übergebenen String suffix als Dateiendung. Da temporäre Dateien nach Pro-
grammende nicht mehr benötigt werden, kann man sogar mit Hilfe der Methode deleteOn
Exit() vorab festlegen, dass diese Dateien beim Beenden der Virtual Machine automatisch
gelöscht werden und kein unnötiger Datenmüll zurückbleibt:

import java.io.*;

public class Start {

public static void main(String[] args) {

try {
// eine Datei im Standard-Temp Verzeichnis erzeugen
File tmp1 = File.createTempFile("daten_",".txt");
tmp1.deleteOnExit();

// die andere Datei im aktuellen Verzeichnis erzeugen


File tmpDir = new File(".");
File tmp2 = File.createTempFile("daten_",".txt",tmpDir);
tmp2.deleteOnExit();

// Dateien verwenden
System.out.println(tmp1.getCanonicalPath());
System.out.println(tmp2.getCanonicalPath());

Listing 108: Temporäre Datei erzeugen

2. Unter Windows ist dies meist c:\Dokumente und Einstellungen\Username\Lokale Einstellungen\Temp.


238 >> Verzeichnisinhalt auflisten

} catch(Exception e) {
e.printStackTrace();
}
}
}

Listing 108: Temporäre Datei erzeugen (Forts.)

98 Verzeichnisinhalt auflisten
Ein häufiges Problem ist das Durchlaufen aller Dateien und Unterverzeichnisse von einem
gegebenen Wurzelverzeichnis aus. Hierfür eignet sich ein rekursiver Ansatz, bei dem eine
Ein- und Ausgabe

Methode listAllFiles() als Parameter ein Verzeichnis erhält, alle darin enthaltenen Dateien
und Verzeichnisse auflistet und diese dann der Reihe nach durchgeht. Bei einer Datei wird der
Name gespeichert, bei einem Verzeichnis ruft sich die Methode selbst mit diesem Verzeichnis
als Argument auf.

import java.io.*;
import java.util.*;

class FileUtil {

/**
* Auflistung aller Dateien/Verzeichnisse in einem Startverzeichnis
* und in allen Unterzeichnissen
*
* @param rootDir File-Objekt des Startverzeichnisses
* @param includeDirNames Flag, ob auch reine Verzeichnisse als separater
* Eintrag erscheinen (true/false)
* @return ArrayList<File> mit allen File-Objekten
*/
public static ArrayList<File> listAllFiles(File rootDir,
boolean includeDirNames) {
ArrayList<File> result = new ArrayList<File>();

try {
File[] fileList = rootDir.listFiles();

for(int i = 0; i < fileList.length; i++) {


if(fileList[i].isDirectory() == true) {
if(includeDirNames)
result.add(fileList[i]);

result.addAll(listAllFiles(fileList[i],includeDirNames));
}
else
result.add(fileList[i]);
}

Listing 109: Methode zur rekursiven Auflistung von Verzeichnisinhalten


>> Ein- und Ausgabe (IO) 239

} catch(Exception e) {
e.printStackTrace();
}

return result;
}
}

Listing 109: Methode zur rekursiven Auflistung von Verzeichnisinhalten (Forts.)

Das Start-Programm demonstriert den Gebrauch.

Ein- und Ausgabe


import java.io.*;
import java.util.*;

public class Start {

public static void main(String[] args) {

File root = new File(".");


ArrayList<File> files = FileUtil.listAllFiles(root, false);

try {
for(File f : files)
System.out.println(f.getCanonicalPath());

} catch(Exception e) {
e.printStackTrace();
}
}
}

Listing 110: Verzeichnisinhalt auflisten

99 Dateien und Verzeichnisse löschen


Zum Löschen einer Datei bzw. eines Verzeichnisses kann man die aus der Klasse File bekannte
Methode delete() verwenden, z.B.
File f = new File(".\\temp\\test.txt");
boolean st = f.delete();

if(st == true)
System.out.println("Datei geloescht");
Voraussetzung für ein erfolgreiches Löschen ist eine Schreibberechtigung auf die gewünschte
Datei für den Benutzer, unter dessen Kennung das Java-Programm ausgeführt wird. Bei Ver-
zeichnissen kommt eine weitere, oft lästige Bedingung hinzu: Das zu löschende Verzeichnis
muss leer sein! In der Praxis ist dies natürlich meist nicht der Fall und man muss erst dafür
240 >> Dateien und Verzeichnisse löschen

sorgen, dass alle enthaltenen Dateien und Unterverzeichnisse gelöscht worden sind. Hierzu
kann man den rekursiven Ansatz aus Rezept 98 einsetzen:

import java.io.*;

class FileUtil {

/** Löscht die übergebene Datei oder Verzeichnis


* (auch wenn es nicht leer ist)
*
* @param Zu löschende Datei/Verzeichnis
* @return true bei vollständigem Löschen, sonst false
*/
Ein- und Ausgabe

public static boolean deleteFile(File startFile) {


if(startFile == null)
return true;

boolean statusRecursive = true;

if(startFile.isDirectory() == true) { // rekursiv den Inhalt löschen


try {
File[] fileList = startFile.listFiles();

for(int i = 0; i < fileList.length; i++) {


boolean st = deleteFile(fileList[i]);

if(st == false)
statusRecursive = false;
}
} catch(Exception e) {
e.printStackTrace();
statusRecursive = false;
}
}

// Datei/Verzeichnis löschen
boolean status = startFile.delete();

return (status && statusRecursive);


}
}

Listing 111: Methode zum rekursiven Löschen von Verzeichnissen

Das Start-Programm demonstriert den Aufruf:

public class Start {

public static void main(String[] args) {

Listing 112: Datei/Verzeichnis löschen


>> Ein- und Ausgabe (IO) 241

File f = new File(".\\testdir");


boolean result = deleteFile(f);

if(result)
System.out.println("Datei/Verzeichnis geloescht");
else
System.out.println("Konnte nicht loeschen!");
}
}

Listing 112: Datei/Verzeichnis löschen (Forts.)

Ein- und Ausgabe


100 Dateien und Verzeichnisse kopieren
Für das Kopieren von Dateien und Verzeichnissen gibt es keine direkte Java-Methode, so dass
man hier selbst programmieren muss. Beim Kopieren einer Datei spielt es übrigens keine Rolle,
ob es sich um Binärdaten oder Text handelt, d.h., man kann immer mit einer Instanz von
FileInputStream zum Lesen und FileOutputStream zum Schreiben arbeiten. Die höchste
Kopiergeschwindigkeit erhält man, wenn auf Betriebssystemebene mit direktem Kanaltransfer
gearbeitet wird. Diese Funktionalität wird durch die Methode transferTo() der Klasse FileIn-
putStream ermöglicht.
Die nachfolgende Klasse FileCopy zeigt eine mögliche Implementierung zum Kopieren von
Dateien (Methode copyFile() oder ganzen Verzeichnissen (copyTree()) inklusive Unterver-
zeichnissen:

import java.util.*;
import java.io.*;
import java.nio.channels.*;

class FileCopy {

/**
* Ausgabe aller Datei-/Verzeichnisnamen in einem Startverzeichnis und in
* allen Unterzeichnissen
*
* @param rootDir File-Objekt des Startverzeichnisses
* @param includeDirNames Flag, ob auch Verzeichnisnamen als separater
* Eintrag erscheinen (true/false)
* @return ArrayList<File> mit allen File-Objekten
*/
public static ArrayList<File> listAllFiles(File rootDir,
boolean includeDirNames) {
ArrayList<File> result = new ArrayList<File>();

try {
File[] fileList = rootDir.listFiles();

for(int i = 0; i < fileList.length; i++) {

Listing 113: Methoden zum Kopieren von Dateien und Verzeichnissen


242 >> Dateien und Verzeichnisse kopieren

if(fileList[i].isDirectory() == true) {
if(includeDirNames)
result.add(fileList[i]);

result.addAll(listAllFiles(fileList[i],includeDirNames));
}
else
result.add(fileList[i]);
}

} catch(Exception e) {
e.printStackTrace();
}
Ein- und Ausgabe

return result;
}

/**
* Kopieren einer Datei/Verzeichnisses; eine vorhandene Zieldatei
* wird überschrieben
*
* @param sourceDir Name des zu kopierenden Verzeichnisses
* @param targetRoot Name des Zielverzeichnisses, in das hineinkopiert
* werden soll (muss existieren)
* @return true bei Erfolg, ansonsten false
*/
public static boolean copyTree(String sourceDir, String targetRoot) {
boolean result;

try {
File source = new File(sourceDir);
File root = new File(targetRoot);

if(source.exists() == false || source.isDirectory() == false)


return false;

if(root.exists() == false || root.isDirectory() == false)


return false;

// sicherstellen, dass Unterverzeichnis vorhanden ist


String targetRootName = root.getCanonicalPath() + File.separator +
source.getName();
File target = new File(targetRootName);

if(target.exists() == false) {
boolean st = target.mkdir();

if(st == false)
return false;
}

Listing 113: Methoden zum Kopieren von Dateien und Verzeichnissen (Forts.)
>> Ein- und Ausgabe (IO) 243

// Auflistung aller zu kopierenden Dateien


ArrayList<File> fileNames = listAllFiles(source, true);
result = true;

for(File f : fileNames) {
String fullName = f.getCanonicalPath();
int pos = fullName.indexOf(sourceDir);
String subName = fullName.substring(pos + sourceDir.length()+1);
String targetName = targetRootName + subName;

if(f.isDirectory()) {
// Unterverzeichnis ggf. anlegen
File t = new File(targetName);

Ein- und Ausgabe


if(t.exists() == false) {
boolean st = t.mkdir();

if(st == false)
result = false;
}

continue;
}

boolean st = copyFile(f.getCanonicalPath(), targetName);

if(st == false)
result = false;
}

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}

/**
* Kopieren einer Datei; eine vorhandene Zieldatei
* wird überschrieben
*
* @param sourceFile Name der Quelldatei
* @param targetFile Name der Zieldatei
* @return true bei Erfolg, ansonsten false
*/
public static boolean copyFile(String sourceFile, String targetFile) {
boolean result;

try {
// Eingabedatei öffnen

Listing 113: Methoden zum Kopieren von Dateien und Verzeichnissen (Forts.)
244 >> Dateien und Verzeichnisse kopieren

FileInputStream inputFile= new FileInputStream(sourceFile);


FileChannel input= inputFile.getChannel();

// Zieldatei öffnen
FileOutputStream outputFile = new FileOutputStream(targetFile);
FileChannel output= outputFile.getChannel();

// die Länge der zu kopierenden Datei


long num = input.size();

// kopieren
input.transferTo(0,num,output);
Ein- und Ausgabe

input.close();
output.close();
result = true;

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}
}

Listing 113: Methoden zum Kopieren von Dateien und Verzeichnissen (Forts.)

Das Start-Programm demonstriert den Aufruf. Denken Sie daran, Quell- und Zielverzeichnis
vor dem Aufruf anzulegen (bzw. die Pfade für sourceDir und targetRootDir anzupassen).

public class Start {

public static void main(String[] args) {

String sourceDir = "c:\\temp\\MyDir";


String targetRootDir ="c:\\UserDirs";
boolean result = FileCopy.copyTree(sourceDir, targetRootDir);

if(result)
System.out.println("Verzeichnis kopiert!");
else
System.out.println("Fehler beim Kopieren!");
}
}

Listing 114: Datei/Verzeichnis kopieren


>> Ein- und Ausgabe (IO) 245

101 Dateien und Verzeichnisse verschieben/umbenennen


Das Verschieben von Dateien und Verzeichnissen ist wesentlich einfacher als das Kopieren, da
technisch gesehen nur der Name geändert werden muss. Hierfür bietet die Klasse java.io.File
bereits alles, was man braucht, in Form einer Methode renameTo():

import java.io.*;

public class Start {

public static void main(String[] args) {

try {

Ein- und Ausgabe


// ein Verzeichnis umbenennen
File file = new File("c:\\temp\\appconfig");
File newFile = new File("c:\\temp\\basics");

boolean status = file.renameTo(newFile);


System.out.println("Verschieben erfolgreich: " + status);

} catch(Exception e) {
e.printStackTrace();
}
}
}

Listing 115: Datei/Verzeichnis verschieben

Beim Einsatz von renameTo() sollten Sie Folgendes beachten:


왘 Die Schreibrechte auf dem Dateisystem müssen vorhanden sein.
왘 Die Zieldatei/das Verzeichnis darf noch nicht existieren.
왘 Das Verschieben über Partitionen hinweg ist unter Windows nicht möglich (z.B. c:\test.txt
nach d:\test.txt).

102 Textdateien lesen und schreiben


Beim Einlesen von Dateien muss man prinzipiell unterscheiden, ob es sich um Binärdaten oder
Textdaten handelt. Aus technischer Sicht besteht der Unterschied darin, dass bei einer Binär-
datei die einzelnen Bytes ohne weitere Interpretation in den Speicher geladen werden, wäh-
rend bei Textdaten ein oder mehrere aufeinander folgende Bytes zu einem Textzeichen (je
nach verwendeter Zeichenkodierung) zusammengefasst werden. Für das korrekte Verarbeiten
von Textdateien ist es daher wichtig zu wissen, in welcher Zeichenkodierung die Datei
ursprünglich geschrieben worden ist. Das folgende Beispiel liest eine Datei in der gewünschten
Kodierung als String ein bzw. schreibt einen String als Textdatei:
246 >> Textdateien lesen und schreiben

/**

Listing 116: Methoden zum Lesen und Schreiben von Textdateien beliebiger Zeichenkodierung

*
* @author Peter Müller
*/
import java.util.*;
import java.io.*;

class FileUtil {
Ein- und Ausgabe

/**
* Lädt eine Textdatei mit der angegebenen Zeichenkodierung
*
* @param fileName Name der Datei
* @param charSet Name der Zeichenkodierung, z.B. UTF-8 oder ISO-8859-1;
* bei Angabe von null wird die Default-Kodierung der
* Virtual Machine genommen
* @return String-Objekt mit eingelesenem Text oder null bei
* Fehler
*/
public static String readTextFile(String fileName, String charSet) {
String result = null;

try {
InputStreamReader reader;

if(charSet != null)
reader = new InputStreamReader(new FileInputStream(fileName),
charSet);
else
reader = new InputStreamReader(new FileInputStream(fileName));

BufferedReader in = new BufferedReader(reader);


StringBuilder buffer = new StringBuilder();
int c;

while((c = in.read()) >= 0) {


buffer.append((char) c);
}

in.close();
return buffer.toString();

} catch(Exception e) {
e.printStackTrace();
result = null;
}

return result;
}
>> Ein- und Ausgabe (IO) 247

/**
* Schreibt einen String als Textdatei in der angegebenen Zeichenkodierung.
*
* @param data Zu schreibender String
* @param fileName Dateiname
* @param charSet Zeichenkodierung (oder null für
* Default-Zeichenkodierung der VM)
* @return true bei Erfolg
*/
public static boolean writeTextFile(String data, String fileName,
String charSet) {
boolean result = true;

Ein- und Ausgabe


try {
OutputStreamWriter writer;

if(charSet != null)
writer = new OutputStreamWriter(new FileOutputStream(fileName),
charSet);
else
writer = new OutputStreamWriter(new FileOutputStream(fileName));

BufferedWriter out = new BufferedWriter(writer);


out.write(data, 0, data.length());
out.close();

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}
}

Listing 116: Methoden zum Lesen und Schreiben von Textdateien beliebiger Zeichenkodierung
(Forts.)

Das Start-Programm demonstriert den Aufruf.

public class Start {

public static void main(String[] args) {

String text = FileUtil.readTextFile(".\\john_maynard.txt",


"ISO-8859-1");
boolean status = FileUtil.writeTextFile(text,
".\\john_maynard_utf8.txt",
"UTF-8");

Listing 117: Textdatei lesen/schreiben


248 >> Textdatei in String einlesen

}
}

Listing 117: Textdatei lesen/schreiben (Forts.)

103 Textdatei in String einlesen


Und gleich noch ein Rezept, mit dem Sie den Inhalt einer ASCII- oder ANSI-Textdatei in einen
String einlesen können. In String-Form kann der Text dann beispielsweise mit den Methoden
der Klasse String bearbeitet, mit regulären Ausdrücken durchsucht oder in eine Textkompo-
nente (beispielsweise JTextArea) kopiert werden.
Ein- und Ausgabe

Die beiden Methoden


String file2String(String filename)
String file2String(FileReader in)
lesen den Inhalt der Datei und liefern ihn als String zurück. Die Methoden wurden überladen,
damit Sie sie sowohl mit einem Dateinamen als auch mit einem FileReader-Objekt aufrufen
können. Die erste Version spart Ihnen die Mühe, ein eigenes FileReader-Objekt zu erzeugen.
import java.io.FileReader;
import java.io.IOException;

public static String file2String(String filename) throws IOException {

// Versuche Datei zu öffnen - Löst FileNotFoundException aus,


// wenn Datei nicht existiert, ein Verzeichnis ist oder nicht gelesen
// kann
FileReader in = new FileReader(filename);

// Dateiinhalt in String lesen


String str = file2String(in);

// Stream schließen und String zurückliefern


in.close();

return str;
}

public static String file2String(FileReader in) throws IOException {


StringBuilder str = new StringBuilder();

int countBytes = 0;
char[] bytesRead = new char[512];

while( (countBytes = in.read(bytesRead)) > 0)


str.append(bytesRead, 0, countBytes);

return str.toString();
}
>> Ein- und Ausgabe (IO) 249

Um das Lesen des Dateiinhalts möglichst effizient zu gestalten, werden die Zeichen nicht ein-
zeln mit read(), sondern in 512-Byteblöcken mit read(char[]) eingelesen. Außerdem werden
die Zeichen nicht direkt an ein String-Objekt angehängt (etwa mit + oder concat()), sondern in
einem StringBuffer gespeichert. Der Grund ist Ihnen sicherlich bekannt: Strings sind in Java
immutable (unveränderlich), d.h., beim Konkatenieren oder anderen String-Manipulationen
werden immer neue Strings angelegt und der Inhalt des alten Strings wird samt Änderungen
in den neuen String kopiert. StringBuffer- und StringBuilder-Objekte sind dagegen mutable
(veränderlich) und werden direkt bearbeitet.
StringBuffer und StringBuilder sind nahezu wie Zwillinge, nur dass StringBuilder schneller
in der Ausführung ist, weil nicht threadsicher. Da gleichzeitige Zugriffe aus verschiedenen
Threads auf die lokale Variable str nicht gegeben sind, haben wir für die obige Implementie-
rung StringBuilder gewählt.

Ein- und Ausgabe


104 Binärdateien lesen und schreiben
Der Umgang mit Binärdaten ist eigentlich sehr einfach, da man sich hier im Gegensatz zu
Textdaten keinerlei Gedanken über Zeichenkodierungen machen muss. Zur Eingabe bietet sich
BufferedInputStream an, für die Ausgabe empfiehlt sich BufferedOutputStream.

import java.io.*;

class FileUtil {

/**
* Liest eine Binärdatei in ein byte-Array ein
*
* @param fileName Zu lesende Binärdatei
* @return byte[] oder null bei Misserfolg
*/
public static byte[] readBinaryFile(String fileName) {
byte[] result = null;

try {
BufferedInputStream input;
input = new BufferedInputStream(new FileInputStream(fileName));
int num = input.available();
result = new byte[num];
input.read(result, 0, num);
input.close();

} catch(Exception e) {
e.printStackTrace();
result = null;
}

return result;
}

/**

Listing 118: Methoden zum Lesen und Schreiben von Binärdateien


250 >> Random Access (wahlfreier Zugriff)

* Schreibt ein byte-Array als Binärdatei;


* eine vorhandene Datei wird überschrieben
*
* @param data Zu schreibende Binärdaten
* @param fileName Dateiname
* @return true bei Erfolg
*/
public static boolean writeBinaryFile(byte[] data, String fileName) {
boolean result = true;

try {
BufferedOutputStream output;
output = new BufferedOutputStream(new FileOutputStream(fileName));
Ein- und Ausgabe

output.write(data, 0, data.length);
output.close();

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}
}

Listing 118: Methoden zum Lesen und Schreiben von Binärdateien (Forts.)

Das Start-Programm demonstriert den Aufruf.

public class Start {

public static void main(String[] args) {

byte[] data = FileUtil.readBinaryFile(".\\windows_konsole.pdf");


FileUtil.writeBinaryFile(data, ".\\kopie.pdf");
}
}

Listing 119: Binärdateien lesen/schreiben

105 Random Access (wahlfreier Zugriff)


Beim so genannten wahlfreien Zugriff (Random Access) kann man in einer Datei den Schreib-/
Lesezeiger beliebig positionieren, um dann an dieser Position zu lesen oder zu schreiben. Man
kann sich das am besten so vorstellen, dass die Datei ein byte-Array im Hauptspeicher ist und
man auf jede Indexposition direkt zugreifen kann.
Unterstützt wird der Random Access durch die Klasse java.io.RandomAccessFile. Sie arbeitet
recht low-level, d.h., der Programmierer muss exakt wissen, wo er den Schreib-/Lesezeiger
positioniert, wie viele Bytes ab dieser Position gelesen oder geschrieben werden sollen und
was mit den Daten dann passieren soll. Dies betrifft insbesondere Textzeichen, die ggf. in die
>> Ein- und Ausgabe (IO) 251

richtige Zeichenkodierung umgewandelt werden müssen. Es existiert in RandomAccessFile zwar


auch eine auf den ersten Blick brauchbare Methode readLine() zum zeilenweisen Einlesen
von Textdateien. Diese ist aber erstens ungepuffert und liest somit sehr langsam und liefert
zudem lediglich für normale ASCII-Zeichen korrekte Zeichen zurück. Bei anderen Zeichen-
kodierungen (z.B. UTF-8) muss man byteweise einlesen und die Konvertierung selbst durch-
führen3. Das folgende Beispiel zeigt daher eine Klasse zur Durchführung von wahlfreiem
Dateizugriff mit einigen verbesserten Methoden, z.B. writeString() zum Schreiben einer Zei-
chenkette oder readLine() zum Lesen einer ganzen Zeile.

import java.io.*;

/**

Ein- und Ausgabe


* Klasse für den wahlfreien Zugriff auf eine Datei
*/
class RandomAccess {
private RandomAccessFile file;
private String fileName;
private final short MAX_LINE_LENGTH = 4096;

public RandomAccess(String name) {


fileName = name;
}

/**
* Datei für wahlfreien Zugriff öffnen
*
* @param mode Modus "r" = lesen,
* "rw" = lesen und schreiben
* @return true bei Erfolg, sonst false
*/
public boolean open(String mode) {
boolean result = true;

try {
file = new RandomAccessFile(fileName, mode);

} catch(Exception e) {
e.printStackTrace();
result = false;
}

return result;
}

/**
* Datei schließen

Listing 120: RandomAccess.java – eine Klasse für den Zugriff auf beliebige Positionen in einer
Textdatei

3. RandomAccessFile hat zwar auch eine Methode readUTF(), die leider ein java-spezifisches UTF-Format erwartet,
das nicht UTF-8-kompatibel ist, und somit in der Regel nicht brauchbar ist.
252 >> Random Access (wahlfreier Zugriff)

*
* @return true bei Erfolg, sonst false
*/
public boolean close() {
try {
file.close();
return true;

} catch(Exception e) {
e.printStackTrace();
return false;
}
}
Ein- und Ausgabe

/**
* Liefert die aktuelle Größe der Datei in Bytes oder -1 bei Fehler
*
* @return Anzahl Bytes
*/
public long getLength() {
try {
return file.length();

} catch(Exception e) {
e.printStackTrace();
return -1;
}
}

/**
* Liefert den aktuellen Wert des Dateizeigers
*
* @return long-Wert mit Position oder -1 bei Fehler
*/
public long getFilePointer() {
try {
return file.getFilePointer();

} catch(Exception e) {
e.printStackTrace();
return -1;
}
}

/**
* Hängt die übergebenen Bytes an das Ende der Datei
*/
public void append(byte[] data) {

Listing 120: RandomAccess.java – eine Klasse für den Zugriff auf beliebige Positionen in einer
Textdatei (Forts.)
>> Ein- und Ausgabe (IO) 253

try {
file.seek(file.length());
file.write(data);

} catch(Exception e) {
e.printStackTrace();
}
}

/**
* Hängt den String in der gewünschten Kodierung an
*/

Ein- und Ausgabe


public void appendString(String str, String encoding) {
try {
byte[] byteData = str.getBytes(encoding);
append(byteData);

} catch(Exception e) {
e.printStackTrace();
}
}

/**
* Liest die angegebene Anzahl Bytes ein und liefert sie als Array zurück
*
* @param startPos Position, ab der gelesen werden soll
* @param num Anzahl einzulesender Bytes
* @return byte[] mit eingelesenen Daten oder null bei Fehler
*/
public byte[] read(long startPos, int num) {
try {
file.seek(startPos);
byte[] data = new byte[num];
int actual = file.read(data, 0, num);

if(actual < num) {


// das Array kleiner machen, da weniger als gewünscht gelesen
// worden ist
byte[] tmp = new byte[actual];

for(int i = 0; i < actual; i++)


tmp[i] = data[i];

data = tmp;
}

return data;

Listing 120: RandomAccess.java – eine Klasse für den Zugriff auf beliebige Positionen in einer
Textdatei (Forts.)
254 >> Random Access (wahlfreier Zugriff)

} catch(Exception e) {
e.printStackTrace();
return null;
}

/**
* Schreibt die übergebenen Bytes in die Datei
*
* @param data Position, ab der geschrieben werden soll
* @param startPos Array mit zu schreibenden Daten
Ein- und Ausgabe

* @return true bei Erfolg, sonst false


*/
public boolean write(byte[] data, long startPos) {
try {
file.seek(startPos);
file.write(data, 0, data.length);
return true;

} catch(Exception e) {
e.printStackTrace();
return false;
}

/**
* Schreibt den übergebenen String in der gewünschten Zeichenkodierung
* als Bytefolge
*
* @param data zu schreibender String
* @param encoding Name der Zeichenkodierung
* @param startPos Startposition, ab der geschrieben werden soll
* @return true bei Erfolg, sonst false
*/
pu