Go To Internetprogrammierung . 3827318254, 9783827318251 [PDF]


154 16 10MB

German Pages 867 Year 2002

Report DMCA / Copyright

DOWNLOAD PDF FILE

Table of contents :
Go To Java......Page 3
2.6 Variablen......Page 5
3.3 Klassen II......Page 6
5.8 java.util.Arrays......Page 7
8.2 Die Klasse java.io.File......Page 8
10.4 Ersetzungen (Substitutions)......Page 9
13.4 SQL-Crash-Kurs......Page 10
14.5 Farben......Page 11
Sun Microsystems Inc. Binary Code License Agreement......Page 12
16.5 Constrained Properties......Page 13
Vorwort......Page 15
Geleitwort von Guido Krüger......Page 19
1.1 Übersicht......Page 21
1.3.1 Class-Datei......Page 24
1.3.4 Compile-Time......Page 25
1.3.7 Threads......Page 26
1.3.8 JDK, JRE, J2SDK, J2SE, J2EE......Page 27
1.3.11 JAR-Datei......Page 28
1.3.13 Packages......Page 29
1.4 JDK und JRE......Page 32
1.4.1 Installation des JDK von SUN......Page 33
1.5 Mein erstes Java-Programm......Page 36
1.6.1 Dokumentations-Kommentare......Page 41
2.2 Identifier......Page 49
2.3 Kommentare......Page 50
2.4.1 Der Datentyp boolean......Page 51
2.4.2 Der Datentyp char......Page 52
2.4.4 Numerische Datentypen......Page 53
2.5 Programmblöcke......Page 58
6.1 Deklaration von Variablen......Page 60
6.2 Geltungsbereich (Scope)......Page 62
2.7.1 Zuweisung einer Referenz......Page 67
2.8 Arrays......Page 69
2.8.1 Deklaration von Arrays......Page 70
2.8.2 Initialisierung von Arrays......Page 71
2.8.4 Das Attribut length......Page 72
2.9 Casting......Page 73
2.10 Operatoren......Page 77
2.10.1 Arithmetische Operatoren......Page 80
2.10.2 Logische Operatoren......Page 81
2.10.3 Bitweise Operatoren......Page 82
2.10.4 Vergleichs-Operatoren......Page 83
2.10.5 Zuweisungs-Operatoren......Page 84
2.10.6 Sonstige Operatoren......Page 85
2.11 Expressions......Page 87
2.12.2 Deklarations-Statements......Page 88
2.12.3 Kontroll-Statements......Page 89
2.13 Methoden......Page 102
2.13.1 Der Datentyp von Methoden......Page 103
2.13.2 Die Parameterliste von Methoden......Page 104
3.1 Klassen I......Page 107
3.1.2 Attribute......Page 109
3.1.3 Methoden......Page 112
3.1.4 Konstruktoren......Page 120
3.1.6 Anwendungsbeispiele......Page 122
3.1.7 Zusammenfassung......Page 129
3.2 OOP-Konzepte......Page 130
3.2.1 Abstraktion......Page 131
3.2.2 Kapselung......Page 132
3.2.3 Vererbung......Page 134
3.2.4 Polymorphie......Page 140
3.3.2 Konstruktoren......Page 148
3.3.3 Statische Initialisierung......Page 153
3.3.4 Finale Klassen......Page 154
3.3.5 Abstrakte Klassen......Page 155
3.3.7 Modifier......Page 157
3.3.8 Initialisierung......Page 160
3.4 Interfaces......Page 161
3.4.1 Vererbung von Interfaces......Page 167
3. 4.2 Tagging Interfaces......Page 168
3.5.1 Objekte kopieren......Page 169
3.5.2 Inner Classes......Page 177
3.5.3 Anonyme Klassen......Page 180
3.5.4 Wrapper-Klassen......Page 181
4 Exceptions......Page 187
4.1.1 try/catch-Block......Page 189
4.1.2 finally......Page 194
4.2 Die Klasse java.lang.Exception......Page 195
4.3.1 throw-Anweisung......Page 196
4.3.2 throws-Klausel......Page 197
4.4 Selbst definierte Exceptions......Page 199
5.1 java.lang.String......Page 201
5.1.1 Konstruktoren von java.lang.String......Page 203
5.1.2 Methoden von java.lang.String......Page 205
5.2.1 Konstruktoren von java.lang.StringBuffer......Page 218
5.2.2 Methoden von java.lang.StringBuffer......Page 219
5.3 java.util.Properties......Page 224
5.3.2 Methoden von java.util.Properties......Page 228
5.4.1 Konstruktoren von java.util.StringTokenizer......Page 230
5.4.2 Methoden von java.util.StringTokenizer......Page 232
5.5.1 Attribute von java.lang.System......Page 235
5.5.2 Methoden von java.lang.System......Page 238
5.6 java.lang.Runtime......Page 240
5.7 java.lang.Math......Page 241
5.8 java.util.Arrays......Page 243
5.9 java.util.Random......Page 246
5.10 java.util.Date......Page 247
5.11 java.util.TimeZone......Page 248
5.12.1 Konstruktoren von java.util.GregorianCalendar......Page 251
5.12.4 Calendar-Felder......Page 252
5.12.5 Calendar-Berechnungen......Page 254
5.12.6 Schaltjahre......Page 255
6.1 java.util.Enumeration......Page 257
6.2.1 Konstruktoren von java.util.Hashtable......Page 258
6.2.2 Methoden von java.util.Hashtable......Page 259
6.3.1 Konstruktoren von java.util.Vector......Page 261
6.3.2 Methoden von java.util.Vector......Page 262
6.4 java.util.Stack......Page 265
6.5 java.util.BitSet......Page 266
6.5.1 Konstruktoren von BitSet......Page 267
6.5.2 Methoden von BitSet......Page 268
7.1 Das Collection-Framework......Page 271
7.2 Collection- Interfaces......Page 273
7.2.1 Iteratoren......Page 274
7.2.2 Das Interface java.util.Collection......Page 275
7.2.3 Das Interface java.util.List......Page 276
7.2.4 Das Interface java.util.Set......Page 279
7.2.5 Das Interface java.util.Map......Page 282
7.3 Collection- Implementierungen......Page 286
7.3.1 List-Implementierungen......Page 287
7.3.2 Set-Implementierungen......Page 297
7.3.3 Map-Implementierungen......Page 303
8.1 Dateien zeilenweise lesen......Page 309
8.2 Die Klasse java.io.File......Page 311
8.2.1 Konstruktoren von java.io.File......Page 312
8.2.3 Wichtige Methoden von java.io.File......Page 313
8.3 Streams......Page 317
8.3.1 Byte-Streams......Page 318
8.3.2 Character-Streams......Page 336
8.3.3 Funktionale Übersicht der Streams......Page 341
8.4 Formatierte Ausgabe......Page 342
8.5.1 Konstruktoren von java.io.RandomAccessFile......Page 343
8.5.2 Methoden von java.io.RandomAccessFile......Page 344
8.6 Datei-Sperren (File Locking)......Page 349
9.2 Locales......Page 353
9.2.1 Konstruktoren von java.utilLocale......Page 355
9.3 Resource Bundles......Page 356
9.4.1 Zahlen- und Währungs-Formatierung......Page 362
9.4.2 Datums- und Tageszeit-Formatierung......Page 366
9.4.3 Zusammengesetzte Texte......Page 370
10.1.2 Pattern......Page 371
10.1.3 Regulärer Ausdruck......Page 372
10.1.4 Pattern Matching......Page 374
10.2 Das Package java.util.regex......Page 375
10.2.1 Grundlagen einer Suche mit regex......Page 378
10.2.2 Methoden von java.util.regex.Pattern......Page 384
10.2.3 Methoden von java.util.regex.Matcher......Page 389
10.3 Reguläre Ausdrücke......Page 394
10.3.1 Meta-Zeichen......Page 395
10.3.2 Quantifier......Page 402
10.3.3 Zeichenklassen......Page 411
10.3.4 Capturing groups......Page 423
10.3.5 Minimal Matching......Page 426
10.3.6 Possessive Matching......Page 427
10.3.8 Dynamische Flags......Page 428
10.3.9 Lookahead Assertions......Page 429
10.4 Ersetzungen (Substitutions)......Page 430
11 Multi-Threading......Page 433
1.1.1 Konstruktoren von java.lang.Thread......Page 434
11.1.2 Starten von Threads......Page 435
11.2 Freigeben der CPU......Page 442
11.3 Threads beenden......Page 443
11.4 Thread-Prioritäten......Page 445
11.5.1 Synchronisierte Methoden......Page 449
11.5.2 Synchronisierte Objekte......Page 451
11.5.3 Deadlocks......Page 453
11.5.4 wait und notify......Page 456
11.7 Thread-Gruppen......Page 467
11.8 Pipes......Page 470
12.1 Client-/Server-Kommunikation......Page 477
12.1.2 Sockets......Page 478
12.1.3 Ein einfacher Netzwerk-Server......Page 479
12.1.4 Server-Sockets, Client-Sockets......Page 486
12.2.1 Beispiel: ein Ressourcen-Server......Page 491
13.1 JDBC-Konzepte......Page 503
3.1.1 JDBC-Treiber-Typen......Page 506
13.2.1 JAR-Datei für JDBC-Treiber......Page 507
13.2.4 Treiber-Klasse laden......Page 508
13.3.1 Datenbank-URI......Page 510
13.3.3 Properties......Page 511
13.4 SQL-Crash-Kurs......Page 514
13.4.1 SQL Client-Programme......Page 515
13.4.2 Tabellen (Tables)......Page 520
13.4.3 Das INSERT-Statement......Page 531
13.4.4 Das DELETE-Statement......Page 533
13.4.5 Das UPDATE-Statement......Page 536
13.4.6 Das SELECT-Statement......Page 537
13.4.7 Joins......Page 539
13.4.8 Commit und Rollback......Page 544
13.5.1 Nicht-SELECT-Statements......Page 545
13.5.2 SELECT-Statements......Page 555
13.6 Veränderbare ResultSets......Page 558
13.6.1 Datensätze ändern......Page 559
13.6.2 Datensätze löschen......Page 561
3.6.3 Datensätze einfügen......Page 562
14 Grafik-Programmierung......Page 565
14.1.1 Aufbau eines Swing-Fensters......Page 567
14.1.2 Öffnen eines Swing-Fensters......Page 571
14.1.3 Schließen eines Swing-Fensters......Page 575
14.2 Panes und Panels......Page 580
14.3.1 Events......Page 583
14.3.2 Event Listener implementieren und registrieren......Page 586
14.3.3 Adapter-Klassen......Page 594
14.3.4 Event Listeners und java.beans.EventHandler......Page 598
14.4 Swing-Komponenten......Page 602
14.4.1 Labels......Page 603
14.4.2 Buttons......Page 610
14.4.3 Toggle Buttons......Page 618
14.4.4 Check Boxes......Page 621
14.4.5 Radio Buttons......Page 623
14.4.6 Textfelder......Page 626
14.4.7 Mehrzeilige Textfelder......Page 638
14.4.8 Scroll Panes......Page 645
14.4.9 Lists......Page 648
14.4.10 Combo Boxes......Page 663
14.4.11 Progress Bars......Page 674
14.4.12 Progress Monitors......Page 681
14.4.13 Sliders......Page 686
14.4.14 Spinners......Page 694
14.4.15 Tables......Page 695
14.4.16 Trees......Page 703
14.4.17 File Choosers......Page 710
14.5 Farben......Page 714
14.6 Fonts......Page 720
14.6.5 Konstruktoren von java.awt.Font......Page 721
14.6.6 Wichtige Methoden von java.awt.Font......Page 722
14.6.7 Font-Metrik......Page 723
4.7.1 Rahmen erzeugen......Page 726
14.7.2 Rahmen hinzufügen......Page 729
14.8 Cursors......Page 730
4.9 Layout-Manager......Page 734
14.9.2 Flow-Layout......Page 735
14.9.3 Box-Layout......Page 740
14.9.4 Grid-Layout......Page 743
14.9.5 Border-Layout......Page 744
14.9.6 Card-Layout......Page 745
14.9.7 GridBag-Layout......Page 749
14.10 Menüs......Page 759
14.10.2 Menü -Einträge erzeugen......Page 760
14.10.3 Untermenüs erzeugen......Page 763
14.10.4 Menü -Listener registrieren......Page 764
14.10.5 Popup-Menüs......Page 766
14.11 Tool Bars......Page 768
14.11.1 Konstruktoren von javax.swing.JToolBar......Page 769
14.11.2 Platzierung von Tool Bars......Page 770
14.11.3 Elemente in Tool Bars platzieren......Page 771
14.12 Dialoge......Page 772
14.12.1 Modale Dialoge......Page 773
14.12.2 Nicht modale Dialoge......Page 786
14.13 Spezielle Panes......Page 790
14.13.1 Layered Pane......Page 791
14.13.2 Tabbed Pane......Page 800
14.13.3 Split Pane......Page 804
A......Page 809
B......Page 810
C......Page 811
D......Page 812
E......Page 813
F - G......Page 814
H......Page 815
I - J......Page 816
K......Page 818
M......Page 819
N......Page 820
P......Page 821
Q - R......Page 822
S......Page 823
U......Page 825
V - Y......Page 826
Z......Page 827
Sun Microsystems, Inc. Binary Code License Agreement......Page 829
15.2 Unterschiede zu Applikationen......Page 834
15.3 Voraussetzungen für Applets......Page 835
15.4.2 Laden von Applets......Page 836
15.5 appletviewer......Page 840
15.6 Applets und Sicherheit......Page 841
15.6.1 Policy-Dateien......Page 842
15.6.2 Signieren von Applets......Page 845
15.7 Das Applet-API......Page 848
15.8 Der AppletContext......Page 851
15.10 Umwandeln von Programmen in Applets......Page 852
16.2 Merkmale von Java Beans......Page 854
16.3 Bean-Serialisierung......Page 856
16.4 Bound Properties......Page 860
16.5 Constrained Properties......Page 863
Ins Internet: Weitere Infos zum Buch, Downloads, etc.......Page 0
© Copyright......Page 867
Papiere empfehlen

Go To Internetprogrammierung .
 3827318254, 9783827318251 [PDF]

  • 0 0 0
  • Gefällt Ihnen dieses papier und der download? Sie können Ihre eigene PDF-Datei in wenigen Minuten kostenlos online veröffentlichen! Anmelden
Datei wird geladen, bitte warten...
Zitiervorschau

Java

Die Reihe Go To Die zuverlässigen Führer auf dem Weg zum Profi Folgende Titel sind bereits erschienen: Jürgen Bayer, Go To Internetprogrammierung 1264 S., ISBN 3-8273-1825-4 Detlef Drews, Heinz Schwab, Go To Visual Basic .NET, 888 S., ISBN 3-8273-1830-0 Stefan Wille Go To Java Server Pages 600 Seiten, ISBN 3-8273-1892-0 Mechtild Käufer Go To JavaScript 624 Seiten, ISBN 3-8273-1916-1 Armin Hanisch Go To C# 534 Seiten, ISBN 3-8273-1932-3 Drews, Kaddik, Schwab Go To Visual Basic 6.0 768 Seiten, ISBN 3-8273-1376-7 Andreas Bohne, Guido Lang Go To Delphi 6 960 Seiten, ISBN 3-8273-1774-6 André Willms Go To C++-Programmierung 768 Seiten, ISBN 3-8273-1495-X Herold, Klar, Klar Go To Objektorientierung 744 Seiten, ISBN 3-8273-1651-0 Daryl Harms, Kenneth McDonald Go To Python 640 Seiten, ISBN 3-8273-1800-9 Peter Loos Go to COM 672 Seiten, ISBN 3-8273-1678-2 Guido Krüger Go To Java 2, 2. Auflage 1.224 Seiten, ISBN 3-8273-1710-X Dirk Abels Go To C++ Builder 5.0 432 Seiten, ISBN 3-8273-1713-4 Michael Hernandez, John Viescas Go To SQL 512 Seiten, ISBN 3-8273-1772-X

Helmut Patay

Java

An imprint of Pearson Education München • Boston • San Francisco • Harlow, England

Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über abrufbar.

Die Informationen in diesem Buch 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, die in diesem Buch erwähnt werden, sind gleichzeitig auch eingetragene Warenzeichen oder sollten als solche betrachtet werden. Umwelthinweis: Dieses Buch wurde auf chlorfrei gebleichtem Papier gedruckt.

10 9 8 7 6 5 4 3 2 1 05 04 03 ISBN 3-8273-2072-0 © 2003 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 Einbandgestaltung: Barbara Thoben, Köln Lektorat: Frank Eller, [email protected] Korrektorat: Simone Meißner, Fürstenfeldbruck Herstellung: Monika Weiher, [email protected]

Inhaltsverzeichnis

Inhaltsverzeichnis Vorwort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Geleitwort von Guido Krüger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 1

Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 1.1 1.2 1.3

1.4 1.5 1.6 2

Übersicht Java-Versionen Grundbegriffe 1.3.1 Class-Datei 1.3.2 Interpreter 1.3.3 Compiler 1.3.4 Compile-Time 1.3.5 Runtime 1.3.6 Exception 1.3.7 Threads 1.3.8 JDK, JRE, J2SDK, J2SE, J2EE 1.3.9 Virtual Machine (VM) 1.3.10 Klassen 1.3.11 JAR-Datei 1.3.12 CLASSPATH 1.3.13 Packages JDK und JRE 1.4.1 Installation des JDK von SUN Mein erstes Java-Programm Kommentare in Java 1.6.1 Dokumentations-Kommentare

21 24 24 24 25 25 25 26 26 26 27 28 28 28 29 29 32 33 36 41 41

Sprach-Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 2.1 2.2 2.3 2.4

2.5 2.6

Zeichensatz in Java-Programmen Identifier Kommentare Datentypen 2.4.1 Der Datentyp boolean 2.4.2 Der Datentyp char 2.4.3 Der Datentyp String 2.4.4 Numerische Datentypen Programmblöcke Variablen 2.6.1 Deklaration von Variablen 2.6.2 Geltungsbereich (Scope)

49 49 50 51 51 52 53 53 58 60 60 62

Inhaltsverzeichnis

2.7 2.8

2.9 2.10

2.11 2.12

2.13

Referenzen 2.7.1 Zuweisung einer Referenz Arrays 2.8.1 Deklaration von Arrays 2.8.2 Initialisierung von Arrays 2.8.3 Zugriff auf Array-Elemente 2.8.4 Das Attribut length 2.8.5 Mehrdimensionale Arrays Casting Operatoren 2.10.1 Arithmetische Operatoren 2.10.2 Logische Operatoren 2.10.3 Bitweise Operatoren 2.10.4 Vergleichs-Operatoren 2.10.5 Zuweisungs-Operatoren 2.10.6 Sonstige Operatoren 2.10.7 Operator-Prioritäten Expressions Statements 2.12.1 Ausdrucks-Statements 2.12.2 Deklarations-Statements 2.12.3 Kontroll-Statements Methoden 2.13.1 Der Datentyp von Methoden 2.13.2 Die Parameterliste von Methoden

67 67 69 70 71 72 72 73 73 77 80 81 82 83 84 85 87 87 88 88 88 89 102 103 104

Objektorientierte Programmierung (OOP) . . . . . . . . . . . . . . . . . . . . . . . 107 3.1

3.2

3.3

Klassen I 3.1.1 this 3.1.2 Attribute 3.1.3 Methoden 3.1.4 Konstruktoren 3.1.5 Destruktoren 3.1.6 Anwendungsbeispiele 3.1.7 Zusammenfassung OOP-Konzepte 3.2.1 Abstraktion 3.2.2 Kapselung 3.2.3 Vererbung 3.2.4 Polymorphie Klassen II 3.3.1 Klassen-Deklaration 3.3.2 Konstruktoren

107 109 109 112 120 122 122 129 130 131 132 134 140 148 148 148

Inhaltsverzeichnis

3.4

3.5

4

153 154 155 157 157 160 161 167 168 169 169 177 180 181

Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 4.1

4.2 4.3

4.4 5

3.3.3 Statische Initialisierung 3.3.4 Finale Klassen 3.3.5 Abstrakte Klassen 3.3.6 Finale Attribute 3.3.7 Modifier 3.3.8 Initialisierung Interfaces 3.4.1 Vererbung von Interfaces 3.4.2 Tagging Interfaces Klassen III 3.5.1 Objekte kopieren 3.5.2 Inner Classes 3.5.3 Anonyme Klassen 3.5.4 Wrapper-Klassen

Behandlung von Exceptions 4.1.1 try/catch-Block 4.1.2 finally Die Klasse java.lang.Exception Weitergabe von Exceptions 4.3.1 throw-Anweisung 4.3.2 throws-Klausel Selbst definierte Exceptions

189 189 194 195 196 196 197 199

Wichtige Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 5.1

5.2

5.3

5.4

5.5

5.6 5.7 5.8

java.lang.String 5.1.1 Konstruktoren von java.lang.String 5.1.2 Methoden von java.lang.String java.lang.StringBuffer 5.2.1 Konstruktoren von java.lang.StringBuffer 5.2.2 Methoden von java.lang.StringBuffer java.util.Properties 5.3.1 Konstruktoren von java.util.Properties 5.3.2 Methoden von java.util.Properties java.util.StringTokenizer 5.4.1 Konstruktoren von java.util.StringTokenizer 5.4.2 Methoden von java.util.StringTokenizer java.lang.System 5.5.1 Attribute von java.lang.System 5.5.2 Methoden von java.lang.System java.lang.Runtime java.lang.Math java.util.Arrays

201 203 205 218 218 219 224 228 228 230 230 232 235 235 238 240 241 243

Inhaltsverzeichnis

5.9 5.10 5.11 5.12

java.util.Random java.util.Date java.util.TimeZone java.util.Calendar 5.12.1 Konstruktoren von java.util.GregorianCalendar 5.12.2 Umwandlung von Date in Calendar 5.12.3 Umwandlung von Calendar in Date 5.12.4 Calendar-Felder 5.12.5 Calendar-Berechnungen 5.12.6 Schaltjahre

246 247 248 251 251 252 252 252 254 255

Collections I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257 6.1 6.2

6.3

6.4 6.5

java.util.Enumeration java.util.Hashtable 6.2.1 Konstruktoren von java.util.Hashtable 6.2.2 Methoden von java.util.Hashtable java.util.Vector 6.3.1 Konstruktoren von java.util.Vector 6.3.2 Methoden von java.util.Vector java.util.Stack java.util.BitSet 6.5.1 Konstruktoren von BitSet 6.5.2 Methoden von BitSet

257 258 258 259 261 261 262 265 266 267 268

Collections II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271 7.1 7.2

7.3

Das Collection-Framework Collection-Interfaces 7.2.1 Iteratoren 7.2.2 Das Interface java.util.Collection 7.2.3 Das Interface java.util.List 7.2.4 Das Interface java.util.Set 7.2.5 Das Interface java.util.Map Collection-Implementierungen 7.3.1 List-Implementierungen 7.3.2 Set-Implementierungen 7.3.3 Map-Implementierungen

271 273 274 275 276 279 282 286 287 297 303

Ein-/Ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 8.1 8.2

Dateien zeilenweise lesen Die Klasse java.io.File 8.2.1 Konstruktoren von java.io.File 8.2.2 Attribute von java.io.File 8.2.3 Wichtige Methoden von java.io.File

309 311 312 313 313

Inhaltsverzeichnis

8.3

8.4 8.5

8.6 9

317 318 336 341 342 343 343 344 349

Internationalisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 9.1 9.2 9.3 9.4

10

Streams 8.3.1 Byte-Streams 8.3.2 Character-Streams 8.3.3 Funktionale Übersicht der Streams Formatierte Ausgabe java.io.RandomAccessFile 8.5.1 Konstruktoren von java.io.RandomAccessFile 8.5.2 Methoden von java.io.RandomAccessFile Datei-Sperren (File Locking)

Localization Locales 9.2.1 Konstruktoren von java.utilLocale Resource Bundles Formatierung 9.4.1 Zahlen- und Währungs-Formatierung 9.4.2 Datums- und Tageszeit-Formatierung 9.4.3 Zusammengesetzte Texte

353 353 355 356 362 362 366 370

Pattern Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371 10.1

10.2

10.3

10.4

Begriffs-Erklärungen 10.1.1 Semantik 10.1.2 Pattern 10.1.3 Regulärer Ausdruck 10.1.4 Pattern Matching Das Package java.util.regex 10.2.1 Grundlagen einer Suche mit regex 10.2.2 Methoden von java.util.regex.Pattern 10.2.3 Methoden von java.util.regex.Matcher Reguläre Ausdrücke 10.3.1 Meta-Zeichen 10.3.2 Quantifier 10.3.3 Zeichenklassen 10.3.4 Capturing groups 10.3.5 Minimal Matching 10.3.6 Possessive Matching 10.3.7 Non-capturing groups 10.3.8 Dynamische Flags 10.3.9 Lookahead Assertions 10.3.10 Lookbehind Assertions Ersetzungen (Substitutions)

371 371 371 372 374 375 378 384 389 394 395 402 411 423 426 427 428 428 429 430 430

Inhaltsverzeichnis

Multi-Threading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 433 11.1

11.2 11.3 11.4 11.5

11.6 11.7 11.8

Threads erzeugen und starten 11.1.1 Konstruktoren von java.lang.Thread 11.1.2 Starten von Threads Freigeben der CPU Threads beenden Thread-Prioritäten Synchronisation 11.5.1 Synchronisierte Methoden 11.5.2 Synchronisierte Objekte 11.5.3 Deadlocks 11.5.4 wait und notify Daemon-Threads Thread-Gruppen Pipes

434 434 435 442 443 445 449 449 451 453 456 467 467 470

Netzwerk-Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 477 12.1

12.2

Client-/Server-Kommunikation 12.1.1 Ports 12.1.2 Sockets 12.1.3 Ein einfacher Netzwerk-Server 12.1.4 Server-Sockets, Client-Sockets Multi-Threaded Server 12.2.1 Beispiel: ein Ressourcen-Server

477 478 478 479 486 491 491

JDBC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503 13.1 13.2

13.3

13.4

JDBC-Konzepte 13.1.1 JDBC-Treiber-Typen JDBC-Treiber einbinden 13.2.1 JAR-Datei für JDBC-Treiber 13.2.2 Package java.sql importieren 13.2.3 SQL-Erweiterungen in javax.sql 13.2.4 Treiber-Klasse laden Verbindungsaufbau mit JDBC 13.3.1 Datenbank-URI 13.3.2 User und Kennwort 13.3.3 Properties SQL-Crash-Kurs 13.4.1 SQL Client-Programme 13.4.2 Tabellen (Tables) 13.4.3 Das INSERT-Statement 13.4.4 Das DELETE-Statement

503 506 507 507 508 508 508 510 510 511 511 514 515 520 531 533

Inhaltsverzeichnis

13.5

13.6

14

13.4.5 Das UPDATE-Statement 13.4.6 Das SELECT-Statement 13.4.7 Joins 13.4.8 Commit und Rollback SQL-Statements mit JDBC ausführen 13.5.1 Nicht-SELECT-Statements 13.5.2 SELECT-Statements Veränderbare ResultSets 13.6.1 Datensätze ändern 13.6.2 Datensätze löschen 13.6.3 Datensätze einfügen

536 537 539 544 545 545 555 558 559 561 562

Grafik-Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 565 14.1

14.2 14.3

14.4

14.5

GUI-Programme 14.1.1 Aufbau eines Swing-Fensters 14.1.2 Öffnen eines Swing-Fensters 14.1.3 Schließen eines Swing-Fensters Panes und Panels Event-Handling 14.3.1 Events 14.3.2 Event Listener implementieren und registrieren 14.3.3 Adapter-Klassen 14.3.4 Event Listeners und java.beans.EventHandler Swing-Komponenten 14.4.1 Labels 14.4.2 Buttons 14.4.3 Toggle Buttons 14.4.4 Check Boxes 14.4.5 Radio Buttons 14.4.6 Textfelder 14.4.7 Mehrzeilige Textfelder 14.4.8 Scroll Panes 14.4.9 Lists 14.4.10 Combo Boxes 14.4.11 Progress Bars 14.4.12 Progress Monitors 14.4.13 Sliders 14.4.14 Spinners 14.4.15 Tables 14.4.16 Trees 14.4.17 File Choosers Farben

567 567 571 575 580 583 583 586 594 598 602 603 610 618 621 623 626 638 645 648 663 674 681 686 694 695 703 710 714

Inhaltsverzeichnis

14.6

14.7

14.8 14.9

14.10

14.11

14.12

14.13

Fonts 14.6.1 Font-Familie 14.6.2 Font-Stil 14.6.3 Font-Face 14.6.4 Font-Größe 14.6.5 Konstruktoren von java.awt.Font 14.6.6 Wichtige Methoden von java.awt.Font 14.6.7 Font-Metrik Rahmen 14.7.1 Rahmen erzeugen 14.7.2 Rahmen hinzufügen Cursors Layout-Manager 14.9.1 Layout-Manager zuordnen 14.9.2 Flow-Layout 14.9.3 Box-Layout 14.9.4 Grid-Layout 14.9.5 Border-Layout 14.9.6 Card-Layout 14.9.7 GridBag-Layout Menüs 14.10.1 Menüs erzeugen 14.10.2 Menü-Einträge erzeugen 14.10.3 Untermenüs erzeugen 14.10.4 Menü-Listener registrieren 14.10.5 Popup-Menüs Tool Bars 14.11.1 Konstruktoren von javax.swing.JToolBar 14.11.2 Platzierung von Tool Bars 14.11.3 Elemente in Tool Bars platzieren Dialoge 14.12.1 Modale Dialoge 14.12.2 Nicht modale Dialoge Spezielle Panes 14.13.1 Layered Pane 14.13.2 Tabbed Pane 14.13.3 Split Pane

720 721 721 721 721 721 722 723 726 726 729 730 734 735 735 740 743 744 745 749 759 760 760 763 764 766 768 769 770 771 772 773 786 790 791 800 804

Stichwortverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 809 Sun Microsystems Inc. Binary Code License Agreement . . . . . . . . . . . . . 829

Bonuskapitel auf der Begleit-CD

Bonuskapitel auf der Begleit-CD Inhaltsverzeichnis 15

Applets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 15.1 15.2 15.3 15.4

Was sind Applets? Unterschiede zu Applikationen Voraussetzungen für Applets Struktur von Applets 15.4.1 Klassen-Diagramm 15.4.2 Laden von Applets 15.5 appletviewer 15.6 Applets und Sicherheit 15.6.1 Policy-Dateien 15.6.2 Signieren von Applets 15.7 Das Applet-API 15.8 Der AppletContext 15.9 Kommunikation zwischen Applets 15.10 Umwandeln von Programmen in Applets 16

1 1 2 3 3 3 7 8 9 12 15 18 19 19

Java Beans . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 16.1 16.2 16.3 16.4 16.5

Einsatz von Java Beans Merkmale von Java Beans Bean-Serialisierung Bound Properties Constrained Properties

21 21 23 27 30

Vorwort

Vorwort Einer der Gründe, warum ich mich für Java als Programmiersprache interessierte, war die Aussicht, endlich ein Programm mit grafischer Oberfläche erstellen zu können, ohne in den tiefsten Ebenen der X11-Bibliothek herumstochern zu müssen. Bis dahin hatte ich zwar bereits einige Programme mit X11 in C erstellt, die Frustration wuchs jedoch mit jeder Programmzeile, musste man doch tonnenweise Programmcode schreiben, nur um ein simples Fenster auf den Bildschirm zu zaubern. Zu diesem Zeitpunkt versuchte ich mich nebenbei auch in Perl/Tk, das ebenfalls versprach, GUI-Programme erstellen zu können. Allerdings musste man dafür zusätzlich eine weitere Programmiersprache erlernen, und die Vorbereitungen für das erste GUISkript waren nicht ohne Hemmschwellen zu bewältigen. Deshalb war ich froh, als mit Java erstmals eine weithin akzeptierte Hochsprache angeboten wurde, in der Grafik-Programmierung vollständig integriert enthalten war. Es ist schon verblüffend zu sehen, wie schnell man damit ansprechende GUI-Programme erstellen kann. Als ich mich im Zuge der Einarbeitung auch mit NetzwerkProgrammierung, Datenbank- und LDAP-Anbindungen beschäftigte, lernte ich Java als vielseitige Programmiersprache kennen und schätzen. Besonders angetan war ich von der eleganten Thread-Programmierung, bei der man einen Prozess in viele einzelne Teile aufspalten kann, die zwar parallel unabhängig voneinander wie eigenständige Prozesse ablaufen, aber dennoch auf gemeinsame Daten zugreifen können. Das umständliche Programmieren mit Shared Memory und Semaphoren entfällt also. Bis zur Java-Version 1.4 fehlte allerdings die Unterstützung von regulären Ausdrücken, mit denen ich bei Perl meine wahre Freude hatte, stellen sie doch die Grundlage jeglicher Textbearbeitung dar. Obwohl Pattern Matching mit regulären Ausdrücken seit der Version 1.4 als eigenes Package enthalten ist, fehlt mir doch ein wenig die vollständige Integration in den Sprachumfang, so wie das bei Perl der Fall ist, aber nun gut, man kann nicht alles haben. Durch das Neu-Design der Java-Collections in der Version 1.2, mit denen eine flexible Datenverarbeitung beliebig komplexer Gebilde möglich ist, wurde ein weiterer Meilenstein in Java gesetzt, der die Sprache universell einsetzbar macht. Und mit Hilfe von JDBC und JNDI können diese Daten elegant in beliebigen Repositories gespeichert werden (meist sind dies Datenbanken oder LDAP-Server). Zu guter Letzt möchte ich die Java-Beans nicht unerwähnt lassen, die als Bausteine mit wohl definiertem API die Entwicklung größerer Projekte wesentlich übersichtlicher machen.

Vorwort

r denjenigen, der Java nicht oder nur entfernt kennt, ist gerade die Universalität der rache der größte Stolperstein, wird man doch schier erschlagen von Klassen. Für n Anfänger ist das größte Problem, sich im Urwald der API-Dokumentation recht zu finden und zu ergründen, wie er nun einen konkreten Anwendungsfall plementieren kann. Allein schon die Tatsache, dass man selbst für ein Hello Worldogramm eine Klasse schreiben muss, ist für manchen das K.O.-Kriterium. Auch für mich war dies eine schwierige Phase. Fängt man zu Beginn doch meist mit vermeinth einfachen Beispielen wie dem Lesen von Dateien an. In Java kann dies jedoch hr schnell zum Knockout führen, denn scheinbar ist hier nichts umständlicher als s zeilenweise Lesen von Dateien! erade aus diesen Gründen entstand dieses Buch. Es soll einerseits so viel Information eten wie möglich, andererseits möchte ich nicht, dass der Leser sich in den zahlreien Tiefen von Java verliert. Im Vordergrund steht immer die Praxis, die ich in zahleichen Beispielen näher bringen will. Sie sollen möglichst schnell die wichtigsten pekte der Programmiersprache lernen, ohne durch zu viele Details gelangweilt er verwirrt zu werden. Deshalb konzentriere ich mich in diesem Buch auf diejenin Themen, die einem als Programmierer als Erstes auf den Nägeln brennen. Da in a ohne Objekte nichts geht, nimmt natürlich das Kapitel über Objektorientierte ogrammierung eine gewichtige Stellung ein. Aber auch das Thema Grafikprogramerung ist ein zentraler Punkt, der in diesem Buch sogar die meisten Seiten verhlungen hat, obwohl ich bewusst auf Gebiete verzichtet habe, die in die Richtung n Spiele-Programmierung und Multimedia gehen. Als Ausgleich dafür erhalten Sie sführliche Informationen über Events und GUI-Komponenten in Swing. enn Sie vorhaben, Java-Programme mit Anschluss an relationale Datenbanken zu ogrammieren, liegen Sie mit diesem Buch genau richtig, denn im Buch enthalten ein Crash-Kurs in SQL, mit dem Sie wichtige Tipps erhalten, was man in SQL zu achten hat (und natürlich, was man besser lassen sollte). Wo immer möglich, gebe h Ihnen in diesem Kapitel Auskunft über die Unterschiede in MySQL und Oracle, n in meinen Augen wichtigsten Vertretern der Datenbank-Hersteller. Sie wollen netzwerkfähige Serverprogramme erstellen? Auch dafür steht ein eigenes pitel zur Verfügung. Wie schreibt man ein Programm, das für einen internationalen Auftritt gerüstet ist? Welche Hilfsmittel gibt es für die Formatierung von Datumsanben? Wie erstellt man ein Programm, das über Textdateien angepasst werden kann, ne den Sourcecode neu übersetzen zu müssen?

Vorwort

Für all diese Fragen, und solche, die mir im Moment nicht einfallen, werden Sie Antworten in diesem Buch finden. In der Hoffnung, Ihnen mit dem Buch geholfen zu haben, verbleibe ich Hochachtungsvoll Helmut Patay

Geleitwort von Guido Krüger

Geleitwort von Guido Krüger Niemals zuvor hat eine Programmiersprache so unterschiedliche Fähigkeiten miteinander kombiniert. Niemals zuvor gab es objektorientierte Grafik- und Netzwerkfähigkeiten in einer so umfassenden und leicht zugänglichen Klassenbibliothek, auf einem so einfach zu erlernenden, aber doch flexiblen Sprachkern. Niemals zuvor konnten Programme so leicht auf unterschiedliche Plattformen portiert werden. Niemals zuvor gab es Applets, Servlets, Doclets; niemals zuvor wurde so viel Wert auf Sprachsicherheit gelegt. Und niemals zuvor gab es so viele freie Softwareprojekte, so viele große, verteilte Anwendungen und ein so hohes Abstraktionsniveau in der Software-Entwicklung wie in den letzten Jahren, seit der Einführung von Java. Nur schwer kann man sich der Magie entziehen, die eine Konferenz vom Schlage der »Java One« auslöst. Dabei ist es ganz unerheblich, ob man an die »Keynotes« denkt, bei der 20.000 Java-Begeisterte 15 Meter unter den Straßen von San Francisco den Worten einiger »Java-Gurus« lauschen. Oder ob ein paar tausend Entwickler freiwillig Schlange stehen, um zur Teilnahme an einem Programmierwettbewerb einen vernetzten PDA zum Sonderpreis zu erstehen. Oder ob in einem völlig überfüllten Hörsaal 1500 statt der erlaubten 1000 Teilnehmer gebannt den Ausführungen des »Doktors« lauschen, um wenigstens ein kleines Quäntchen von der unendlichen Weisheit der großen Vordenker mit nach Hause zu nehmen. Vorbei sind die Zeiten, in denen Software-Standards in elitären und langweiligen (im wahrsten Sinne des Wortes) Gremien beschlossen oder von monopolistischen Einzelunternehmen in den Markt gedrückt wurden. Vorbei sind die Zeiten, in denen Programmiersprachen entweder einfach (exklusiv-)oder mächtig waren. In denen sie entweder performant oder sicher waren. In denen sie entweder für kleine oder für große, für kaufmännische oder für technische, für akademische oder für praktische Probleme geeignet waren. Vorbei sind auch die Zeiten, in denen zum x-ten Mal die eigene Doppelt-Verkettete-Lineare-Liste oder Festkomma-Arithmetik-Bibliothek gebastelt wurde. Vorbei sind die Zeiten ohne Unit-Testing, ohne Multithreading, ohne Datenbankanbindung und ohne grafische Oberflächen. Vorbei sind auch die Zeiten des (statischen) Linkens. Ist das alles nur Java zu verdanken? Nein, sicher nicht. Viele der genannten Technologien gab es schon vorher. Nur eben nicht im Paket. Etliches davon findet sich in anderen Programmiersprachen wieder. Nur eben nicht portabel. Java war eine Art Katalysator. Eine Initialzündung. Java bildete das Fundament zur Vereinigung einer Reihe

Geleitwort von Guido Krüger

n Technologien, die noch Mitte der 90er Jahre als unvereinbar galten. Java war die ogrammiersprache, die die portable, objektorientierte Programmierung wirklich onfähig gemacht hat – nicht Simula, nicht SmallTalk, nicht C++. Und schon gar nicht C#. Vor allem aber hat es Java geschafft, das Interesse eines großen Teils der ltweiten Programmiergemeinde anzuziehen und so einen gewaltigen kollektiven rtschrittsschub ausgelöst. Das war vielleicht sein wichtigster Verdienst. Das vorliegende Buch ist das neue »Go To Java 2« (welches vor einiger Zeit verlagsern in »Handbuch der Java-Programmierung« umbenannt wurde). Es trägt den Namen eines ehemaligen Bestsellers und schickt sich an, dem Leser die Grundlagen r Programmiersprache Java zu vermitteln. Viele wichtige Techniken werden hier rmittelt, und das Buch setzt interessante Akzente. Ich wünsche dem neuen »Go To Java« und seinem Autor, Helmut Patay, dass es ein profilierter und erfolgreicher Nachlger wird. Den Lesern wünsche ich viel Spaß beim Lesen und viel Erfolg mit Java. Guido Krüger, im April 2003

Einführung

1

Einführung

Mit diesem einleitenden Kapitel möchte ich Ihnen einen Einstieg in Java bieten, der Sie mit den Grundlagen der Programmiersprache vertraut macht, ohne dass der Überblick dabei verloren geht. Am Ende des Kapitels sollten Sie in der Lage sein, Java auf einem Windows- oder Unix-Rechner zu installieren, Programme zu übersetzen und zu starten. Auch grundlegende Begriffe werden Sie hier verstehen lernen.

1.1

Übersicht

In der Software-Entwicklung beginnt das Alphabet mit dem Buchstaben C. Als die Programmiersprache bekannt wurde, waren sich alle einig, dass es die beste Sprache aller Zeiten war, weil sie dem Programmierer alle denkbaren Möglichkeiten bot, indem er den Speicher direkt bearbeiten, Zeiger auf beliebige Adressen im RAM setzen und andere hoch performante Dinge tun konnte. Allerdings stellte sich diese Flexibilität bald als gravierender Nachteil heraus, weil Programme oft ohne Vorwarnung einfach abstürzten und vor allem unter Windows häufig dazu führten, dass man den ganzen Rechner neu starten musste. Außerdem hat C den Nachteil, dass die Sprache nicht objektorientiert, sondern prozedural ist, deswegen hat man eine neue Sprache erfunden, die zwar immer noch alle möglichen direkten Speicher-Operationen zuließ, aber vom Konzept her objektorientiert war. Da die neue Sprache aus C hervorging, aber nach Meinung des Erfinders besser war, nannte man sie schlicht C++. Wie der erfahrene Programmierer weiß, bedeutet das Anhängen von ++ an eine Variable, dass deren Wert nach Gebrauch erhöht wird, böse Zungen behaupten deshalb, man hätte die neue Sprache besser ++C nennen sollen, denn wer will schon etwas Altes benutzen, das hinterher an Wert gewinnt. Besser sei doch, erst den Wert zu erhöhen und dann den neuen Wert zu verwenden. Es gibt zwar mittlerweile Tonnen von Bibliotheken für C und C++, die dem Programmierer dabei helfen, nicht immer das Rad neu erfinden zu müssen, dennoch kranken beide Sprachen zum einen daran, dass sie sehr fehleranfällig sind (weil der Programmierer alles darf), zum anderen daran, dass man Seiten von Code schreiben muss, um eine Kleinigkeit zu programmieren. Noch viel gravierender ist, dass die erzeugten Programme nicht portabel sind, d.h. ein C- oder C++-Programm läuft entweder auf Windows oder auf Solaris oder auf einem MAC oder auf Linux oder ..., die Liste ist mittlerweile recht lang. Wenn man nicht die Quell-Datei zur Verfügung hat (in welcher der Source-Code des Programms steht), die man für die gewünschte Plattform neu übersetzen kann, dann steht man im Regen.

Einführung

Schon bald nach der Erfindung von C++ hat sich die Riege der Programmier-Gurus von überzeugt, dass diese Sprache nicht das Gelbe vom Ei ist, und hat einige andere Sprachen entwickelt (meist Hoch-Sprachen), die vor allem die Plattform-Unabhängigeit und die Objektorientierung in den Vordergrund stellen. Fast ausnahmslos sind dies Interpreter-Sprachen, die, anders als bei C oder C++, keinen direkt ausführbaren Maschinencode über einen Compiler erzeugen, sondern immer einen Runtime-Intereter für die Ausführung eines Programms benötigen, der den Programmcode zeichenoder byteweise liest und die übersetzten Binär-Anweisungen anschließend ausführt. lgemein werden diese Hoch-Sprachen als Skript-Sprachen bezeichnet, ein Proamm ist also kein direkt ausführbares Programm mehr, sondern ein Skript, das von einem anderen Programm (dem Interpreter) ausgeführt wird. Bedeutende Beispiele hierfür sind Sprachen wie Perl oder Python, die sich sehr großer Beliebtheit erfreuen. i den genannten Beispielen trifft allerdings dasselbe zu wie beim guten alten ASIC, der Interpreter muss immer den Source-Code interpretieren, um ein Skript sführen zu können. Dies bedeutet im Allgemeinen, dass die Skripts speziell bei hleifen langsamer sind als Binär-Programme. Allerdings muss angemerkt werden, ss in der Praxis dieser Geschwindigkeits-Verlust meist nicht relevant ist im Vergleich r Zeitersparnis bei der Implementierung. An dieser Stelle kommt Java (nicht zu verwechseln mit JavaScript, das nicht das Geringste mit Java zu tun hat) gerade recht. Es versucht, die Vorteile der Objektorientierten Programmierung sowie die Plattform-Unabhängigkeit von Skript-Sprachen und (heute ganz wichtig) Skalierbarkeit, außerdem eingebaute Netzwerkfähigkeit, Sicherheit, grafische Oberflächen, schlicht alles was gut ist, in einer Sprache zu vereinen. m die schlechte Performance der bisherigen Skript-Sprachen zu erhöhen, hat man ch einen Zwischenschritt einfallen lassen: Der Quell-Code von Java wird nicht in ein direkt ausführbares Binär-Programm vom Compiler übersetzt (das wiederum cht portabel wäre), sondern es wird ein so genannter Byte-Code erzeugt, für dessen Abarbeitung weniger Zeit benötigt wird, der aber dennoch Plattform-unabhängig ist. Deswegen ist ein Java-Programm in der Regel schneller als ein Perl oder Python-Proamm. Wie bei anderen Skript-Sprachen auch benötigt man allerdings immer mindest eine Runtime-Umgebung der Programmiersprache (JRE), um ein Javaogramm laufen zu lassen (das ist der Preis der Plattform-Unabhängigkeit). a ist in vielen Dingen sehr restriktiv, d.h. ein C- oder C++-Programmierer muss ch gewaltig umstellen, wenn er Java lernen möchte. Auf der anderen Seite verhindert ese restriktive Haltung, dass man beim Programmieren Fehler einbaut, die oft sehr schlecht zu finden sind (Memory-Leaks, Zeiger, die ins Leere zeigen usw.).

Einführung

Auch hat man bei Java von Anfang an daran gedacht, dass ein Programm nicht nur lokal auf dem eigenen Rechner läuft, sondern über einen Browser gestartet wird, außerdem ist bei Java die grafische Oberfläche (GUI) bereits mit eingebaut (Wer schon einmal ein Programm mit grafischer Oberfläche in Perl und Tk geschrieben hat, weiß ein Lied davon zu singen.). Einen Netzwerk-Server zu implementieren ist in Java eine Leichtigkeit, bei anderen Programmiersprachen bedeutet dies oft stundenlanges Programmieren. Allerdings muss hinzugefügt werden, dass es wohl keine andere Programmiersprache gibt, die so großzügig mit den System-Ressourcen umgeht wie Java, auch hat man bis einschließlich Java-Version 1.3 vergessen, reguläre Ausdrücke zu unterstützen (wer schon einmal in Perl trotz der schweren Verständlichkeit mit regulären Ausdrücken gearbeitet hat, der wird als Programmierer nicht mehr ohne sie leben wollen). Seit der Version 1.4 sind reguläre Ausdrücke in Java zwar unterstützt, aber leider nicht von Anfang an mit in die Sprache selbst integriert (wie das z.B. bei Strings der Fall ist). Gott sei Dank wird alles von Tag zu Tag (oder sagen wir von Jahr zu Jahr) besser, allgemein kann man sagen, dass Java eine Programmiersprache ist, die vom heutigen Stand der Technik aus gesehen eine der modernsten ist, die es gibt. Als erste Programmiersprache hat sie z.B. für jeden Datentyp, unabhängig davon, auf welcher Plattform ein Programm läuft, eine feste Speicher-Größe festgelegt (Integer-Werte sind z.B. immer 32-Bit lang), d.h. ein Programmierer muss sich nicht mehr den Kopf zerbrechen, warum das Programm auf Maschine A läuft und auf Maschine B nicht. Außerdem ist Java grundsätzlich multi-threaded, d.h. ein Java-Programm kann parallel in mehreren Instanzen gleichzeitig laufen, ohne dass für jede Instanz ein neuer Prozess gestartet werden muss. Mit der Multi-Thread-Technik kommt ein weiterer Aspekt zu Tage, der in der herkömmlichen Multi-Prozess-Technik häufig die Köpfe der Programmierer rauchen ließ, das ist die gemeinsame Nutzung eines Daten-Bereichs von unterschiedlichen Threads. In der bisherigen Multi-Prozess-Technik musste man Handstände machen, um dieses Feature ordentlich zu programmieren (Shared-Memory, Semaphoren), und allzu oft gab es hier gravierende Fehler, die nicht einfach zu entdecken waren. Mit der Thread-Technik ist in dieser Beziehung Gott sei Dank alles einfacher geworden, und Java bietet genügend Mittel, um beim gleichzeitigen Zugriff auf eine Ressource von unterschiedlichen Threads für die notwendige Daten-Konsistenz zu sorgen. Threads sind zudem wesentlich schneller, weil das Betriebssystem bei einem Wechsel von Threads nicht so viel Verwaltungsarbeit leisten muss wie bei Prozess-Wechseln.

Einführung

Durch die strikte Objektorientierung in der Sprache und der restriktiven Handhang (so gibt es z.B. keine mehrfache Vererbung, was in C++ gebräuchlich, aber umständlich zu programmieren und zudem verwirrend ist) wird Java auch straight forrd, d.h. man hat relativ einfache Regeln, an die man sich halten muss, auf der anden Seite kann man mit diesen Regeln sehr effizient programmieren.

2

Java-Versionen

undsätzlich unterscheidet man zwischen Version 1 und Version 2, letztere wird ch Java-2-Plattform genannt und kennzeichnet alle Versionen ab 1.2.2 bis zur heute ktuellen Version 1.4. le Versionen vor 1.2.2 kann man getrost als Spielwiese betrachten, die Entwickler n Java haben hier Erfahrungen gesammelt, die dann ab der Version 1.2.2 umgesetzt urden. Viele Bugs wurden beim Wechsel von Version 1.x auf die Java-2-Plattform hoben, Features wurden konsistenter implementiert, es kam das Paket Swing hinzu, s man für Grafik-Programmierung benutzt, um eine einheitliche grafische Oberfläe zu gestalten. Mit der Version 1.3 wurde unter anderem JNDI (Java Naming and Directory Intere) integriert, das eine einheitliche Schnittstelle für die Benutzung von Serveressourcen wie Datenbanken und LDAP-Server verwendet. Wiederum gab es viele Bugfixes, auch wurde die Performance speziell von Swing deutlich gesteigert. n Version 1.4, die gleichzeitig die heute aktuelle Java-Version darstellt, kamen als ghlight reguläre Ausdrücke hinzu, die man für Such- und Ersetzungs-Operationen Zeichenketten benötigt.

3

Grundbegriffe

Damit Sie bei den vielen Begriffen, die Ihnen gleich um die Ohren fliegen werden, cht völlig im Regen stehen, möchte ich im Folgenden die wichtigsten kurz erläutern. 3.1

Class-Datei

i Java wird der Source-Code in Dateien mit der Endung .java abgelegt. Werden a-Programme vom Compiler übersetzt, dann erzeugt dieser kein Binär-Programm, ndern portablen Byte-Code, den er in einer Class-Datei ablegt, deren Endung .class Aus MyTest.java wird nach dem Übersetzen also MyTest.class.

Einführung

1.3.2

Interpreter

Jeder, der schon einmal den Windows-Befehl dir oder das Unix-Pendant ls aufgerufen hat, kennt den Begriff Interpreter, auch wenn er sich dessen vielleicht nicht bewusst ist. Damit solche Befehle des Betriebssystems aufgerufen und ausgeführt werden können, benötigt man einen so genannten Kommandozeilen-Interpreter, den man gemeinhin auch als SHELL bezeichnet. Immer dann, wenn Sie in Windows die DOS-Box öffnen (die auch als Eingabe-Aufforderung bezeichnet wird), starten Sie eine SHELL, die nichts anderes tut, als auf Eingaben über die Tastatur zu warten. Gibt man ein Kommando ein, dann interpretiert die SHELL das erste Wort der eingegebenen Zeile als Programm, das sie anschließend ausführt. Alle weiteren Worte der Eingabezeile werden als Argumente bzw. Parameter für das aufzurufende Programm verwendet, die das Verhalten des Programms steuern. Etwas Ähnliches macht der Java-Interpreter, er liest ein Java-Programm (genauer gesagt, eine Class-Datei), interpretiert die darin enthaltenen Anweisungen (neudeutsch: Statements) und übersetzt diese Anweisungen dann in binären Maschinencode, der direkt von der CPU des Rechners ausgeführt werden kann. Der Java-Interpreter ist Bestandteil des JRE (Erläuterung siehe unten) und manifestiert sich im Programm namens java, das im Unterverzeichnis jre/bin der Java-Distribution abgelegt ist (zusätzlich gibt es eine Kopie unter bin). Landläufig wird der Java-Interpreter als Virtual Machine bezeichnet. 1.3.3

Compiler

Ein Compiler stellt in Java die Vorstufe des Interpreters dar, d.h. er übersetzt den Source-Code von Java-Programmen in einen speziellen Byte-Code, der anschließend vom Interpreter gelesen und ausgeführt werden kann. Im Gegensatz zum Interpreter, der ein Java-Programm tatsächlich ausführt, werden vom Compiler die im Programm enthaltenen Anweisungen nur geprüft und übersetzt, ohne sie auszuführen. Der Java-Compiler ist Bestandteil des JDK (Erläuterung siehe unten) und heißt javac. Das Programm ist im Unterverzeichnis bin der Java-Distribution abgelegt. 1.3.4

Compile-Time

Der Begriff Compile-Time wird ins Deutsche übersetzt mit Übersetzungszeit und bedeutet im Prinzip nichts anderes als diejenige Zeit, in welcher der Compiler ein Java-Programm kompiliert (es wird in den so genannten Byte-Code übersetzt, das

Einführung

gebnis speichert der Compiler in einer Class-Datei ab). Treten bei dieser Übersetng Fehler auf, dann bricht der Compiler den Vorgang mit einem so genannten mpile-Time-Error ab. 3.5

Runtime

Unter dem Begriff Runtime (zu Deutsch Laufzeit) versteht man die Zeitspanne, wähnd der die Virtual Machine von Java eine vom Compiler erzeugte Class-Datei interetiert und ausführt. Tritt dabei ein Fehler auf, dann nennt man diesen Runtime-Error, der immer mit einer so genannten Exception (zu Deutsch Ausnahme) verbunden ist. 3.6

Exception

Der Begriff Exception wird ins Deutsche übersetzt mit Ausnahme und tritt zur Laufzeit eines Java-Programms auf, wenn irgendein Fehler passiert, zum Beispiel, weil man eine Division durch Null durchführt, oder wenn man versucht, eine Datei zu öffnen, die gar nicht vorhanden ist. Man kann auch im Programm selbst Exceptions auslösen, jedem Fall beendet sich das Programm mit einer entsprechenden Meldung, wenn eine Exception auftritt, die im Programmcode nicht abgefangen wird. 3.7

Threads

le gängigen Betriebssysteme sind heutzutage Multi-Prozess-Systeme, bei denen sich ehrere Programme die System-Ressourcen wie Hauptspeicher und CPU teilen und rallel nebeneinander gleichzeitig ablaufen können. Eine einzelne Instanz eines launden Programms entspricht dabei einem Prozess. Damit kein Chaos im System entsteht, wenn viele Prozesse nebeneinander ablaufen, sorgt das Betriebssystem dafür, dass zu einem Zeitpunkt immer nur ein einziger Prozess aktiv sein kann, er hat dann sozusagen Exklusiv-Recht über den gesamten Rechner. Durch ein Zeitscheiben-Schema wird dafür gesorgt, dass jeder Prozess nur einen bestimmten Zeitraum lang aktiv ist, nach dessen Ablauf er von einem anderen Prozess in der Warteschlange abgelöst wird. Dieser Mechanismus erfordert natürlich einen relativ hohen Verwaltungsaufwand seitens des Betriebssystems, weil alle gespeicherten ProzessDaten ausgelagert und der neue Prozess geladen werden müssen. Diesen Umschalt-Vorgang von einem Prozess zum nächsten nennt man neudeutsch Process-Switching. Ich glaube, man kann sich gut vorstellen, was im Betriebssystem alles ablaufen muss, nn Hunderte von Prozessen verwaltet und ständig umgeschaltet werden müssen, r dafür zuständige Teil des Kernels (Betriebssystem-Kern) hat einiges zu tun, weil die

Einführung

gesamte Prozess-Umgebung gesichert werden muss, bevor der nächste Prozess geladen werden kann. Hier kommen Threads ins Spiel. Threads sind nichts anderes als abgespeckte Prozesse. Mehr noch, Threads sind Kinder eines einzigen Prozesses, sie erben vom Eltern-Prozess die gesamte Umgebung, laufen aber dennoch unabhängig voneinander, so als wären sie eigenständige Prozesse. Im Speziellen sind die einzelnen Threads eines Prozesses in der Lage, auf gemeinsame Hauptspeicher-Daten zuzugreifen (und können somit Variablen und Methoden benutzen, die nur einmal definiert sind). Dies bringt einen ungeheuren Vorteil mit sich, weil sich damit umständliche Programmierung mit Shared-Memory und Semaphoren erübrigt. Für diejenigen, denen diese Begriffe nichts sagen: Macht nichts, weil Sie in Java so gut wie nie damit zu tun haben werden. Diejenigen, die bereits mit Themen wie Semaphoren zu tun hatten, werden sicherlich begeistert sein, dass man diese nicht mehr benötigt. Der Vorteil des Zugriffs mehrerer Threads auf gemeinsame Daten birgt natürlich auch Gefahren, wenn diese nicht nur gelesen, sondern auch beschrieben werden. In diesem Fall muss der Programmierer die Daten synchronisieren, um sicherzustellen, dass immer nur ein Thread zu einem Zeitpunkt die Daten beschreiben kann. Wir werden uns weiter unten noch ausführlich mit Threads und der Synchronisation beschäftigen. 1.3.8

JDK, JRE, J2SDK, J2SE, J2EE

Hinter diesen Buzzwords (zu Deutsch Schlagworte), die immer dann durch die Gegend fliegen, wenn die Leute von Applikationen oder schlicht von Programmen sprechen, verbirgt sich in Wahrheit nicht viel mehr als eine Java-Umgebung der Programme. Das JDK (Java Development Kit) ist dabei die Entwicklungsumgebung und enthält alle benötigten Programme, um Java-Programme zu übersetzen und ablaufen zu lassen, während das JRE (Java Runtime Environment) nur diejenigen Dateien enthält, die für das Starten von bereits übersetzten Java-Programmen benötigt werden, beim JRE fehlt also der Compiler mit seinen Dateien. Man muss auf einem Rechner mindestens das JRE installiert haben, um ein übersetztes Java-Programm ablaufen lassen zu können. J2SDK (Java-2 Software Development Kit) bedeutet nichts anderes als ein JDK der Java-2-Plattform, die Java-Version ist hier mindestens 1.2.2. J2SE ist die Abkürzung von Java-2 Standard Edition und bedeutet nichts anderes als das JDK ab Version 1.2.

Einführung

Unter dem Begriff J2EE (Java-2 Enterprise Edition) versteht man ein Paket basierend f dem J2SDK, welches Erweiterungen wie zum Beispiel Java-Enterprise-Beans, rvlets oder auch Java-Server-Pages (JSP) enthält. 3.9

Virtual Machine (VM)

e Virtual Machine, kurz VM genannt, ist nichts anderes als ein laufender Java-Intereter. Im einfachsten Fall arbeitet die VM nur ein einzelnes Java-Programm ab. esonders bei Web-Applikationen hat die VM aber mehr als nur ein einziges Proamm zu betreuen, dort verwaltet die VM viele Servlets oder Java-Server-Pages. 3.10

Klassen

Im Kapitel über objektorientierte Programmierung werden wir detailliert auf Klassen ngehen. An dieser Stelle möchte ich den Begriff nur kurz erläutern. In Java ist jedes ogramm und jedes Programmmodul eine Klasse. Wenn Sie zum Beispiel Strings rarbeiten, dann arbeiten Sie mit einer Klasse, lesen Sie in Java Daten aus einer Datei, nn verwenden Sie eine Klasse. Immer und überall stoßen Sie auf Klassen. ne Klasse definiert ein Objekt, das dem Programmierer bestimmte Eigenschaften ttribute) und Funktionalitäten (Methoden) zur Verfügung stellt. Der dafür notwendige Programmcode wird in einer Datei mit der Endung .java abgespeichert und mit m Compiler übersetzt, der das resultierende Java-Programm dann in einer Datei mit r Endung .class ablegt. Diese so genannte Class-Datei kann dann mit dem Java-Intereter (Virtual Machine) ausgeführt werden. 3.11

JAR-Datei

Unter dem Begriff JAR (Java ARchive) muss man sich eine Datei ähnlich wie eine P-Datei oder auch eine komprimierte TAR-Datei vorstellen. Sie enthält in der Regel Java-Bibliotheken in Form von Class-Dateien, die vom Dateisystem hierarisch gegliedert in eine einzelne Datei kopiert werden. Java liefert hierzu auch das tsprechende Tool in Form des Programms jar, das im gleichen Verzeichnis liegt wie r Java-Compiler. Es arbeitet genauso wie das tar-Programm, verwendet aber grundtzlich einen Komprimierungs-Algorithmus, der kompatibel zum ZIP-Format ist. R-Dateien werden hauptsächlich deshalb benutzt, weil es einfacher ist, Java-Klassen einer einzelnen Datei von einem Rechner auf einen anderen zu übertragen als Hunderte von Dateien mit ihren Verzeichnissen. Auch sieht die Festplatte einfach sser aufgeräumt aus, wenn man ein paar JAR-Dateien in einem Verzeichnis hat stelle von vielen Dateien in vielen Verzeichnissen.

Einführung

1.3.12

CLASSPATH

Der Begriff CLASSPATH ist in Java immer wieder der Grund für rauchende Entwicklerköpfe. Es handelt sich hier um eine Umgebungsvariable der Virtual Machine, die dazu benutzt wird, alle in einem Java-Programm verwendeten Klassen im Dateisystem zu finden. Wenn Sie nur die mit dem JDK bzw. JRE ausgelieferten Java-Klassen benutzen, brauchen Sie die Umgebungsvariable überhaupt nicht. Meist verwendet man jedoch Zusatz-Klassen, die nicht in der Standard-Distribution der JRE enthalten sind (z.B. wenn Sie selbst eigene Klassen programmiert haben). Damit der Java-Interpreter solche Klassen ebenfalls findet, müssen diese in der Umgebungsvariablen CLASSPATH angegeben sein. Der Mechanismus ist genau derselbe wie bei der Umgebungsvariablen PATH, die von der SHELL benutzt wird, um Programme im Dateisystem zu finden. Für alle, die nichts mit dem Begriff SHELL anfangen können: Eine SHELL ist ein Kommandozeilen-Interpreter, unter Windows ist das die DOS-Box, unter Unix die Bourne-, Korne-, C-SHELL oder die bash-SHELL. Alle SHELLs haben eines gemeinsam: Sie warten auf Eingaben des Benutzers von der Tastatur und führen nach einer Eingabe Programme aus. Nicht mehr und nicht weniger. Hinweis Vor Version 1.2 von Java musste man die Umgebungsvariable CLASSPATH auch dann explizit setzen, wenn man nur die in der Standard-Distribution der JRE enthaltenen Klassen benutzte. Bei der Installation des JDK wurde die Umgebungsvariable deshalb automatisch so gesetzt, dass die Virtual Machine die mitgelieferten Klassen finden konnte. Das hat aber immer zu größerer Verwirrung geführt (vor allem in Windows), wenn man als Anwender die Variable mit einem eigenen Wert belegt hat. Aus diesem Grund sind die Pfade aller Klassen, die in der JDK-Distribution enthalten sind, seit Version 1.2 unabhängig von der Umgebungsvariablen CLASSPATH vordefiniert, die Variable wird also nur dann benötigt, wenn man selbst geschriebene oder vom Internet heruntergeladene bzw. Applikations-spezifische Klassen verwendet, die nicht in der Standard-Distribution enthalten sind. 1.3.13

Packages

Bei der großen Zahl an Java-Klassen, die man im Internet findet, kann man sich gut vorstellen, dass es namensgleiche Klassen gibt. So werden zum Beispiel die Klassen für Datenbank-Treiber grundsätzlich Driver genannt. Um Konflikten bei der Namensgebung vorzubeugen, haben sich die Konstrukteure von Java den Begriff Package ein-

Einführung

allen lassen, mit dessen Hilfe man mehrere zusammengehörende Klassen in Gruppen einteilen kann, die wie das Dateisystem hierarchisch aufgebaut sind. Nur ist das ennzeichen der einzelnen Pakete nicht der Slash /, sondern der Punkt. Ich möchte Ihnen diese hierarchische Struktur anhand eines Beispiels veranschaulien. Alle wichtigen und sehr häufig benutzten Java-Klassen sind im Package java.lang zusammengefasst, während man populäre Hilfs-Klassen im Package java.util ablegt, so gibt es darin zum Beispiel die Klasse Date, mit deren Hilfe Datums-Manipulationen möglich sind. Ein weiteres Package der Standard-Distribun ist java.sql, dort liegt unter anderem ebenfalls eine Klasse Date. e beiden gleichnamigen Klassen Date haben aber nichts gemeinsam, es handelt sich um völlig verschiedene Dinge. Sie können die Struktur der Java-Packages aus der JDK-Distribution erkennen, wenn e die mitgelieferte Datei src.jar (Unix) bzw. src.zip (Windows) auspacken, die alle a-Standard-Klassen im Quell-Code enthält. Es empfiehlt sich aber, vorher ein Unterverzeichnis src anzulegen und das Archiv dort auszupacken. Wie man beim Vereich der Package-Namen und der Verzeichnisse im Dateisystem erkennt, entspricht ein Punkt im Package-Namen einem Verzeichnis-Trenner im Dateisystem, d.h. mit dem neuen Punkt kommt eine Verzeichnis-Ebene hinzu. t werden Packages umgekehrt gegliedert wie Internet-Domains. So kann man hand des Package-Namens schnell erkennen, von wem eine Klasse entwickelt wurde, außerdem ist diese Notation in Firmen üblich, die allesamt eine Internetomain besitzen. Ein Beispiel hierfür ist das Package com.sun.security.auth. Liest man den Package-Namen rückwärts, dann erkennt man die Internet-Domain sun.com. Achtung Alle Package-Namen müssen aus Kleinbuchstaben bestehen, das edeutet, dass die entsprechenden Verzeichnis-Namen im Dateisystem ebenfalls ur Kleinbuchstaben enthalten dürfen. Sonderzeichen und Leerzeichen dürfen nicht in Package-Namen vorkommen. enn man eine Java-Klasse entwickelt, die zu einem Package gehören soll, dann icht es nicht aus, die Quell-Datei einfach in das entsprechende Verzeichnis zu stellen. Man muss diese Zuordnung zusätzlich im Quell-Code selbst durchführen, indem an an den Anfang der Datei die package-Direktive stellt, die aus dem reservierten Wort package gefolgt vom voll qualifizierten Namen des Packages besteht. Hier ein ispiel für die Klasse Vector :

Einführung

// Die package-Direktive muss am Anfang der // Java-Datei stehen package java.util; ... public class Vector { ... }

Möchte man nun in seinem Programm Klassen verwenden, die zu einem bestimmten Package gehören, dann müssen diese Klassen entweder mit ihrem voll qualifizierten Namen einschließlich des Package-Namens angegeben oder vor ihrem Gebrauch mit Hilfe der import-Direktive geladen werden. Hier hat man zwei Möglichkeiten. Entweder man gibt explizit jede Klasse mit einer eigenen import-Direktive an, oder man verwendet den Asterisk *, um alle Klassen eines Packages zu laden. Eine Ausnahme bilden alle Klassen des Packages java.lang. Diese sind so häufig benutzt und werden als so wichtig angesehen, dass sie ohne Angabe des PackageNamens direkt benutzt werden können. Klassen ohne explizite Package-Zuordnung (die Java-Dateien enthalten keine package-Direktive) wandern in ein spezielles Default-Package ohne Namen.

Ich glaube, hierzu sind ein paar Beispiele angebracht: // selbst implementierte Klasse Cookie im // Unterverzeichnis de/hemu/util // Der Pfadname der Datei lautet // de/hemu/util/Cookie.java package de.hemu.util; // Benutzung der Klasse Cookie in einem Java-Programm // voll qualifizierte Angabe de.hemu.util.Cookie c = new de.hemu.util.Cookie(); // dasselbe in abkürzender Schreibweise import de.hemu.util.Cookie; Cookie c = new Cookie();

Einführung

Hinweis Werden alle Klassen eines Packages mit dem Asterisk geladen, dann bedeutet dies keine Performance-Einbuße, weil die Klassen dynamisch on-demand on der Virtual Machine geladen werden, und zwar nur die benötigten Klassen.

Achtung Vorsicht ist dann beim Import mit dem Asterisk geboten, wenn es durch die abkürzende Schreibweise zu Namenskonflikten kommen kann. In diesem Fall müssen die gleich lautenden Klassen voll qualifiziert inklusive PackageNamen angegeben werden:

import java.util.*; import java.sql.*; ... // so geht es nicht, weil der Compiler nicht weiß, // ob die Klasse Date aus java.util oder aus // java.sql gemeint ist Date d = new Date(); // voll qualifizierte Angabe, die funktioniert java.util.Date d = new java.Util.Date;

Den Asterisk darf man nur am Ende der Package-Hierarchie angeben, folgender Code ist also fehlerhaft: import java.*.*;

4

JDK und JRE

ie ich weiter oben bereits erwähnt habe, benötigen Sie auf jedem Rechner mindesns eine Runtime-Umgebung (JRE), um Java-Programme ausführen zu können. ejenigen, die selbst Java-Programme entwickeln wollen, brauchen das JDK, weil r darin der Java-Compiler enthalten ist. eide Komponenten können Sie aus dem Internet von verschiedenen Herstellern runterladen. Die wichtigsten Adressen sind hier SUN, IBM und Blackdown. Ich öchte mich hier auf die Distribution von SUN beschränken, speziell für Linux sei er angemerkt, dass sie eventuell die Blackdown-Distribution benötigen, weil mane Java-Programme mit X11-Oberfläche bei der SUN-Distribution ein seltsames rhalten zeigen, bis hin zum Absturz des Window-Managers von KDE.

Einführung

1.4.1

Installation des JDK von SUN

Das JDK von SUN besteht aus zwei Teilen, der eigentlichen Entwicklungs-Umgebung sowie der dazugehörigen Dokumentation. Sie beginnen am besten, indem Sie im Browser den URI http://java.sun.com/products aufrufen. Wer mit dem Begriff URI nichts anfangen kann: URI (Uniform Resource Identifier) ist nahezu dasselbe wie URL (Uniform Resource Locator) und stellt die Adresse eines Web-Dokuments dar. Klicken Sie dort auf Java 2 Platform, Standard Edition (J2SE), Sie landen auf der Einstiegsseite für das JDK der Java-2-Plattform. Auf dieser Seite finden Sie Links für den Download des JDK und der dazugehörenden Dokumentation. Derzeit aktuell ist die JDK-Version 1.4. Achtung Vor dem Herunterladen des JDK und der Dokumentation sollten Sie sich vergewissern, dass genügend Platz auf der Festplatte vorhanden ist. Allein für die beiden Downloads müssen Sie mit ca. 70 MB rechnen. Wenn das Ganze dann ausgepackt ist, kommen weitere 220 MB hinzu. Auf der Festplatte müssen Sie also mindestens Platz für 350 MB haben. Anschließend können Sie entweder das JDK oder die JRE herunterladen, Sie müssen sich nur noch für die gewünschte Ziel-Plattform entscheiden. Hinweis

Wenn Sie das JDK herunterladen, ist die JRE bereits darin enthalten.

Der Download der Dokumentation erfolgt als ZIP-Archiv, das Sie am besten unterhalb des Verzeichnisses Ihrer anschließenden JDK-Installation auspacken. Windows-Installation

Wie üblich gestaltet sich die Installation des JDK unter Windows recht einfach. Nach dem Herunterladen der exe-Datei für das JDK müssen Sie nur die Datei im DateiManager doppelt anklicken, um sie aufzurufen. Alles weitere erfolgt menügesteuert. Hinweis Ich empfehle Ihnen, als Verzeichnisnamen für die Installation des JDK C:\jdk oder D:\jdk zu wählen (Der Laufwerks-Buchstabe kann je nach Platten-Einteilung natürlich auch anders lauten.).

Einführung

Nach der Installation des JDK (und ggf. der Dokumentation) sollten Sie die Unterrzeichnisse bin und jre/bin Ihrer Installation mit in den Suchpfad aufnehmen, indem e die Umgebungsvariable PATH um diese beiden Verzeichnisse erweitern. Am einhsten geht das über die Systemsteuerung, wenn Sie dort auf das Menü SYSTEM und schließend unter dem Menü ERWEITERT auf UMGEBUNGSVARIABLEN klicken. Dort können Sie die System-Variable PATH erweitern. Hinweis Wenn Sie die Umgebungsvariable PATH über die Systemsteuerung ndern, müssen Sie sich anschließend vom System abmelden und wieder anmelden, damit die Änderung wirksam ist. ternativ ist es auch möglich, in einer DOS-Box die Umgebungsvariable mit dem T-Befehl zu erweitern. Es sei allerdings angemerkt, dass diese Einstellung nur für ese eine DOS-Box gültig ist. Hierzu ein Beispiel (unter der Annahme, dass das JDK ter C:\jdk installiert ist): C:\>set PATH=%PATH%;C:\jdk\bin;C:\jdk\jre\bin

berprüfen können Sie Ihre Installation, indem Sie anschließend den Java-Interpreter s einer DOS-Box heraus wie folgt aufrufen: C:\>java -fullversion java full version "1.4.0_01-b03"

enn Sie statt der hier gezeigten Ausgabe eine Fehlermeldung erhalten, dann haben Sie die Umgebungsvariable PATH nicht richtig erweitert. nix-Installation (Linux)

Unter Linux laden Sie in der Regel eine rpm-Datei herunter. Dieser Datei-Typ ist ne so genannte Redhat-Package-Manager-Datei und kann mit Hilfe des Betriebsstem-Kommandos rpm installiert werden. Hinweis Bevor Sie eine rpm-Datei installieren, müssen Sie sich unter der UnixKennung root angemeldet haben, da hierfür besondere Rechte benötigt werden.

Einführung

Meist ist nach einer Linux-Installation das JDK oder zumindest die JRE bereits im System vorhanden, es muss also durch die neue Version des JDK ersetzt werden. Zu diesem Zweck melden Sie sich als Erstes als root-Benutzer am System an. Anschließend überprüfen Sie, ob Java bereits installiert ist: root:~> rpm -qa | grep -i java java2-1.3.1-27

Die gezeigte Ausgabe kann je nach System variieren. Wird gar nichts ausgegeben, dann bedeutet dies, dass Java nicht installiert ist. In diesem Fall können Sie das JDK mit folgendem Kommando sofort installieren (Ich habe die Datei j2sdk-1_4_0_ 01-linux-i586-rpm.bin in das HOME-Verzeichnis des Benutzers root heruntergeladen): root:~> chmod +x j2sdk-1_4_0_01-linux-i586-rpm.bin root:~> ./j2sdk-1_4_0_01-linux-i586-rpm.bin

Zunächst wird mit dem chmod-Kommando die Datei ausführbar gemacht, anschließend aufgerufen. Daraufhin erhält man die Lizenz-Bestimmungen, die man mit yes quittieren muss. Die einzige Aktion ist das Auspacken der Datei j2sdk-1_4_0_ 01-linux-i586-rpm im aktuellen Verzeichnis. Diese können Sie nun mit dem folgenden Kommando installieren: root:~> rpm -i j2sdk-1_4_0_01-linux-i586-rpm

Das JDK wird jetzt unter /usr/java/ j2sdk1.4.0_01 installiert. Da es sich eingebürgert hat, dass Java unter /usr/lib installiert ist, sollte man nun einen symbolischen Link anlegen: root:~>cd /usr/lib root:/usr/lib> ln -s ../java/j2sdk1.4.0_01 java

Wenn Sie auch die Java-Dokumentation heruntergeladen hatten, können Sie diese mit dem unzip-Kommando installieren: root:~> cd /usr/lib/java root:/usr/lib/java> unzip ~/j2sdk-1_4_0-doc.zip

Einführung

amit wird die gesamte Dokumentation im Unterverzeichnis docs der JDK-Installan ausgepackt. s Letztes sollten Sie noch überprüfen, ob die Umgebungsvariable PATH die Verzeichse /usr/lib/java/bin und /usr/lib/java/jre/bin enthält: root:/usr/lib/java> which java /usr/lib/java/bin/java

enn keine Ausgabe erfolgt, dann müssen Sie die bin-Verzeichnisse von Java /usr/lib/ a/bin sowie /usr/lib/java/jre/bin in der Variablen PATH eintragen, indem Sie in der Datei .profile bzw. .bashrc oder .bash_profile des jeweiligen Benutzers (oder in /etc/profile r alle Benutzer) folgenden Eintrag machen: PATH=$PATH:/usr/lib/java/bin:/usr/lib/java/jre/bin; export PATH

enn Sie nun den Java-Interpreter mit dem Schalter -fullversion aufrufen, sollten Sie lgende Ausgabe erhalten (Die exakte Ausgabe hängt natürlich von dem installierten K ab): root:/usr/lib/java> java -fullversion java full version "1.4.0_01-b03" root:/usr/lib/java>

, nun sind Sie gerüstet für das Weitere.

5

Mein erstes Java-Programm

r alle Ungeduldigen unter Ihnen wollen wir uns ansehen, was grundsätzlich an beit nötig ist, um ein Programm in Java zu erstellen und auszuführen. s Erstes benötigen Sie natürlich einen ordentlichen Editor, mit dem Sie den QuellCode schreiben können. Hier gibt es heute unzählige Varianten, angefangen beim vi unter Unix oder dem Notepad unter Windows bis hin zu den komplett integrierten Systemen größerer Entwicklungswerkzeuge für Java, bei denen man gar nicht mehr merkt, dass man in einem Editor arbeitet (ein populärer Vertreter dieser Gattung ist

Einführung

JBuilder von Borland). Was Sie letztlich zum Erstellen Ihrer Sourcen benutzen, hängt ganz von Ihren Vorlieben ab, jedoch gibt es ein paar grundsätzliche Dinge, die ein Editor beherrschen sollte, wenn Sie den Spaß am Programmieren nicht verlieren wollen. Bei allen ernsthaften Programmierern hat sich eingebürgert, dass die EinrückungsTiefe der einzelnen Programmebenen nicht ein TAB mit 8 Zeichen ist, sondern aus 4 Blanks (Leerzeichen) besteht. Der verwendete Editor sollte also in der Lage sein, 4 Leerzeichen einzufügen, wenn Sie auf die (ÿ_)-Taste drücken. Viel Arbeit erspart auch das Feature, dass man beliebig viele Zeilen gemeinsam um eine Ebene weiter ein- oder ausrücken kann. Da man auch in Java sehr häufig mehrere Klammern öffnet und schließt, seien es runde, eckige oder geschweifte Klammern, erleichtert es die Arbeit ungemein, wenn der Editor beim Eintippen einer schließenden Klammer kurzzeitig die dazu passende öffnende Klammer aufblinken lässt oder in irgendeiner anderen Form hervorhebt. Diese Hervorhebung sollte auch bei bereits geschriebenem Programmcode möglich sein. Glauben Sie mir, eines der lästigsten Dinge ist die Suche nach fehlenden oder überschüssigen Klammern im Source-Code. Das Nonplusultra wäre natürlich ein ordentliches Syntax-Highlighting, bei dem im Source-Code reservierte Wörter, Funktionsnamen, Variablen-Namen, Konstanten und Kommentare farblich gekennzeichnet werden. Dieses Feature ist beileibe kein Luxus, es hilft vielmehr, Fehler zu vermeiden. So weit zum Editor. Sehen wir uns nun den Quell-Code eines einfachen Beispielprogramms an: 01 02 03 04 05 06 07 08 09 10 11 12 13

// HabeDieEhre.java /* Das erste Java-Programm. Wir werden bald noch weitere hinzufügen. */ public class HabeDieEhre { public static void main( String[] args ) { System.out.println( "Habe die Ehre" ); System.exit( 0 ); } }

Listing 1.1: HabeDieEhre.java

Einführung

Das Erste, was Ihnen beim Lesen sicherlich aufgefallen sein wird, ist meine bayerische Herkunft. Es ist doch langweilig, immer nur »Hello, world« auszugeben. eben dieser eher nebensächlichen Information kann man unter anderem heraussen, dass Java zwei verschiedene Varianten von Kommentaren erlaubt (Eine dritte rden wir noch kennen lernen, wenn es um die Dokumentation von Java-Code eht.). Der doppelte Slash in Zeile 01 leitet einen einzeiligen Kommentar ein, der h bis zum Ende der Zeile erstreckt, während die Zeichenketten /* und */ den ginn bzw. das Ende eines beliebig langen Kommentars kennzeichnen, der sich auch er mehrere Zeilen erstrecken kann (hier in den Zeilen 03 bis 06). Ab Zeile 08 tritt das ein, was ich bereits angedroht hatte: Es wird eine Klasse definiert. as das ist, werden wir im Kapitel über objektorientierte Programmierung noch zur Genüge kennen lernen. den Zeilen 09 bis 12 steht der eigentliche Programmcode. Er macht nichts wirklich nnvolles, es wird lediglich der String »Habe die Ehre« ausgegeben, gefolgt von einem Zeilenvorschub. Anschließend beendet sich das Programm mit dem Status 0. An dieser Stelle möchte ich Sie kurz mit dem exit-Status von Programmen vertraut machen: Jedes Programm wird über eine SHELL aufgerufen und übergibt nach dem Abarbeiten der letzten Anweisung (englisch Statement) dieser SHELL einen numerihen Status. Damit hat man die Möglichkeit, festzustellen, ob ein Programm ordngsgemäß gelaufen ist oder einen Fehler hatte. undsätzlich gilt: Beendet sich ein Programm mit dem Status 0, dann ist alles OK, i jedem anderen exit-Status liegt ein Fehler vor. In Windows allerdings kann man t diesem Mechanismus nicht viel anfangen, er ist vielmehr in Unix von Interesse. Der Programmcode des Beispiels gibt auch Auskunft über die Art und Weise, wie an den Quell-Code schreibt, dies möchte ich hier ausdrücklich betonen. Sie sehen m Beispiel, dass die Einrückungs-Tiefe von Programmblöcken 4 Blanks ist, dass erzeichen um Parameter von Methoden (das sind im Prinzip nichts anderes als nktionsaufrufe) verwendet werden und dass vor einem Semikolon (das ist der ichpunkt als Abschluss einer Anweisung, neudeutsch auch Statement genannt) kein erzeichen steht. All dies und weitere Hinweise für guten Programmier-Stil finden Sie übrigens im Styleguide für Java, den ich für Sie auf der CD-ROM in der Datei chdaten\AnhangA/java-style.pdf abgelegt habe. Nun müssen wir den Programmcode der Datei HabeDieEhre.java mit dem Java-Comer übersetzen, dies geschieht mit dem Kommando:

Einführung

javac HabeDieEhre.java

Denken Sie bitte daran, dass das Verzeichnis bin der JDK-Installation im Suchpfad (Umgebungsvariable PATH) enthalten sein muss, sonst findet die SHELL den JavaCompiler nicht. Der Aufruf des Java-Compilers bewirkt, dass dieser unser Java-Programm zunächst einliest und einer Syntax-Prüfung unterzieht, diesen Vorgang nennt man Parsen. Wenn bei dieser Prüfung keine Fehler auftreten, wandelt er den Source-Code in den so genannten Byte-Code um, der in einer Class-Datei gespeichert wird, deren Name sich aus der Zeichenkette HabeDieEhre und der Endung .class zusammensetzt, es wird also eine Datei namens HabeDieEhre.class erzeugt. Erst nachdem eine Class-Datei vorhanden ist, kann man mit Hilfe des Java-Interpreters das Programm aufrufen (Achtung, es wird nur der Klassen-Name ohne die DateiEndung .class angegeben): java HabeDieEhre

Das Kommando gibt, wie nicht anders zu erwarten, die Zeichenkette Habe die Ehre

aus. Doch bereits bei diesem sehr einfachen Beispiel gibt es Stolpersteine. WindowsAnwender neigen dazu, keine Unterscheidung zwischen Groß- und Kleinbuchstaben zu machen, weil auch das Betriebssystem so ausgelegt ist. In Windows gibt es keinen Unterschied zwischen den beiden Dateinamen HabeDieEhre.java und habedieehre.java. Der Source-Code lässt sich also auch so in Byte-Code übersetzen: javac habedieehre.java

So weit zum Compiler. Der Interpreter jedoch ist da weitaus penibler. Versucht man, das Programm mit dem Aufruf

Einführung

java habedieehre

starten, verweigert er mit folgender Fehlermeldung den Dienst: Exception in thread "main" java.lang.NoClassDefFoundError: habedieehre

as liegt daran, dass (in Windows) zwar beim Datei-Namen keine Unterscheidung zwischen Groß- und Kleinschreibung gemacht wird, sehr wohl aber beim KlassenNamen, der im Programmcode definiert ist. Wir haben unserer Klasse aber den Namen HabeDieEhre gegeben, nicht habedieehre. Deshalb verweigert der Interpreter den Dienst. Dasselbe Problem handeln wir uns ein, wenn wir manuell die Umgebungsvariable CLASSPATH verändern und dabei vergessen, das aktuelle Verzeichnis mit aufzunehmen, er ein Beispiel (Windows): C:\temp>set CLASSPATH=C:\myClasses;C:\yourClasses C:\temp>java habedieehre Exception in thread "main" java.lang.NoClassDefFoundError: HabeDieEhre

Machen wir es richtig, funktioniert die Sache: C:\temp>set CLASSPATH=C:\myClasses;C:\yourClasses;. C:\temp>java habedieehre Habe die Ehre

Hinweis Der Java-Interpreter sucht standardmäßig auch im aktuellen Verzeichnis nach Class-Dateien, allerdings nur dann, wenn die Umgebungsvariable CLASSPATH nicht gesetzt ist. Der Klassen-Name, der als Argument beim Aufruf des Java-Interpreters angegeben wird, muss exakt übereinstimmen mit dem im Programmcode verwendeten KlassenNamen, auch hinsichtlich Groß- und Kleinschreibung.

Einführung

1.6

Kommentare in Java

Wie wir in unserem kleinen Beispiel-Programm bereits gesehen haben, bietet Java mehrere Möglichkeiten, so genannte Inline-Dokumentation in Form von Kommentaren zu verwenden. Kurze Kommentare können am Ende einer Zeile oder als einzelne Kommentar-Zeilen mit der Zeichenkette // angegeben werden: // Das ist ein einzeiliger Kommentar. String prefix = "hallo"; // Kommentar am Ende der Zeile

Will man einen längeren Kommentar einfügen, der sich auch über mehrere Zeilen erstrecken kann, muss man diesen durch die Zeichenkette /* beginnen und durch die Zeichenkette */ beenden: /* Ab hier folgt ein mehrzeiliger Kommentar Er kann beliebig lang sein. */ String suffix = ".txt";

1.6.1

Dokumentations-Kommentare

Wenn man statt der einleitenden Zeichenkette /* die Zeichenfolge /** verwendet (einziger Unterschied ist, dass statt einem Asterisk nun zwei geschrieben werden), dann handelt es sich um einen Dokumentations-Kommentar, der vom Programm javadoc interpretiert wird. Dieses Programm ist Bestandteil des JDK und erzeugt standardmäßig eine HTML-Dokumentation Ihrer Java-Programme. Sie können jedoch beliebige Arten von Dokumentation erstellen lassen, denn javadoc kann über so genannte Doclets erweitert werden. Wenn Sie mehr über diese Erweiterungs-Möglichkeit erfahren wollen, können Sie im Verzeichnis docs/tooldocs Ihrer JDK-Installation die Datei index.html mit einem Browser öffnen und die entsprechenden Seiten lesen. Ich möchte mich in diesem Buch auf HTML-Dokumentation beschränken. Sehen wir uns kurz den Gebrauch von javadoc anhand unseres Beispiel-Programms an. Zunächst rufen wir die Online-Hilfe von javadoc auf:

Einführung

Abbildung 1.1: Kommandozeilen-Argumente von javadoc

ie wir sehen, bietet javadoc eine Vielzahl von Optionen, mit denen man das Verhaln anpassen kann. Die meisten von Ihnen werden jedoch selten davon Gebrauch machen müssen. Ich persönlich verwende oft den Schalter -d in Verbindung mit einem selbst gewählten Verzeichnis. Der Grund dafür ist, dass javadoc nicht nur eine einzige HTML-Datei erzeugt, sondern es entsteht eine Reihe von HTML-Dateien, die standardmäßig im aktuellen Verzeichnis angelegt werden. Sehen wir uns an, was adoc aus unserem Java-Programm HabeDieEhre.java macht:

Einführung

Abbildung 1.2: Aufruf von javadoc ohne Schalter -d

Sie sehen, es werden insgesamt 12 zusätzliche Dateien im aktuellen Verzeichnis angelegt. Aus diesem Grund habe ich vorher ein Unterverzeichnis docs angelegt, aus dem heraus der Aufruf von javadoc erfolgt. Da unser Java-Programm eine Ebene höher abgelegt ist, muss man das übergeordnete Verzeichnis vor den Datei-Namen stellen (../). Wer sich jetzt wundert, warum ich auf einem Windows-PC den Slash als Verzeichnis-Trenner verwende: Alle Java-Programme verstehen den Slash als VerzeichnisTrenner, egal, welches Betriebssystem verwendet wird. Der String ../HabeDieEhre.java ist ein Kommandozeilen-Argument für das Java-Programm javadoc und wird nicht von der DOS-SHELL gelesen. Ich hätte javadoc mit dem Schalter -d docs auch im selben Verzeichnis aufrufen können, in dem unser Programm steht. In diesem Fall würde das Verzeichnis automatisch angelegt, wenn es noch nicht existiert, und die erzeugten Dateien dort abgelegt. Wenn Sie die Datei index.html mit einem Browser öffnen, sehen Sie dieselbe Struktur wie bei der Standard-Dokumentation von Java.

Einführung

Nun wollen wir anhand unseres kleinen Beispiel-Programms die grundlegenden öglichkeiten kennen lernen, die für die Inline-Dokumentation von Java-Programmen zur Verfügung stehen. Sie werden hier nicht alle Details serviert bekommen, ben Sie aber einmal die Konzepte verstanden, dann ist es mit der Beschreibung im Unterverzeichnis docs/tooldocs ein Leichtes, alle unterstützten Tags zu erkunden. Unter m Begriff Tag versteht man im Übrigen nichts anderes als einen Kennzeichner, das ein Text mit besonderer Bedeutung, der für Auszeichnungs-Zwecke benutzt wird. em das immer noch nichts sagt, wird den Begriff gleich verstehen, wenn ich unser ispiel-Programm mit Inline-Dokumentation versehe. Hier unser verändertes Programm, das ich in der Datei HabeDieEhre1.java abgespeiert habe:

Abbildung 1.3: Beispiel-Programm mit Java-Dokumentation

Hinweis Damit das Tag @author in die HTML-Dokumentation mit aufgenommen wird, muss beim Aufruf von javadoc der Schalter -author angegeben werden. Gleiches gilt für das Tag @version, hier heißt der Schalter -version. ie wir sehen, dürfen am Beginn jeder Kommentar-Zeile ein oder mehrere Asteriskichen gefolgt von optionalen Leerzeichen stehen. Sie werden von javadoc entfernt. Mit Ausnahme der Übersichts-Dokumentation, die vor der Klassen-Definition steht, nimmt der erste Kommentar eine Sonderstellung ein, wenn er mit einem Punkt abgeschlossen wird. javadoc fügt nach dem ersten Punkt einen HTML-Zeilenvorschub ein

Einführung

und zeigt den ersten Satz in der Übersichts-Beschreibung an. Ansonsten gilt: Will man Auszeichnungen im Text, zum Beispiel, um einen Zeilenvorschub zu erzwingen, dann muss das entsprechende HTML-Tag angegeben sein. Die HTML-Seite der erzeugten Java-Dokumentation hat immer das gleiche Layout: Ganz oben steht eine Navigation, dann folgt eine Übersicht für die Klasse, in welcher der Kommentar aufgenommen wird, der im Java-Programm vor der Klassen-Definition steht. Daraufhin erzeugt javadoc für alle Bestandteile der Klasse eine Kurzfassung der Dokumentation (Bestandteile von Klassen sind Konstruktor, Methoden und Attribute, die auch Felder oder englisch fields genannt werden). Diese jetzt vielleicht noch unbekannten Begriffe werden wir im Kapitel über OOP (Objektorientierte Programmierung) detailliert erläutern. Im Anschluss an die Übersicht kommt eine komplette Beschreibung aller einzelnen Klassen-Bestandteile. In dieser Dokumentation ist im Gegensatz zur Übersicht abgesehen vom ersten Satz (durch einen Punkt abgeschlossen) auch der Rest des Kommentars enthalten. javadoc kennt eine Reihe von speziellen Tags, die mit dem At-Zeichen @ beginnen. Text, der nach einem solchen Tag folgt, wird gesondert behandelt. Im Folgenden möchte ich die wichtigsten Tags kurz beschreiben:  @author text

Dieses Tag kann mehrfach vorkommen und dient dazu, dass man einen oder mehrere Autoren für das Java-Programm oder einen Teil davon nennt. javadoc fügt in die HTML-Dokumentation den hervorgehobenen Text »Author:« ein, dem eine Komma-separierte Liste der genannten Autoren folgt, wenn mehrere @authorTags vorkommen. Man kann auch mehrere Autoren als text verwenden und so eine eigene Auszeichnung für die Trennung der einzelnen Autoren erzwingen.  @version text

Das @version-Tag kann nur ein einziges Mal vorkommen und muss im Kommentar für die Klasse (bzw. das Interface) stehen, es kann also nicht für Methoden oder Attribute verwendet werden. text ist beliebiger Text. Was ein Interface ist, werden wir im Kapitel OOP lernen.  @param paramName[ text]

Das @param-Tag wird für die Beschreibung der Argumente von Methoden verwendet. Damit legt man das API für den Aufruf der Methode fest und teilt dem Programmierer, der unsere Methode benutzen möchte, mit, wie er die Methode aufrufen muss. paramName ist der Name des formalen Methoden-Parameters, text beschreibt den Parameter näher.

Einführung

@return text

Mit dem @return-Tag beschreibt man, welche Art von return-Stati eine Methode hat und was die einzelnen Werte bedeuten. text kann beliebiger Text sein. @since text

Mit dem @since-Tag teilt man meist die Programmversion mit, seit der die beschriebene Methode oder das Attribut vorhanden ist. text kann beliebiger Text sein. @throws exception text

Über das @throws-Tag gibt man Auskunft, welche Exceptions eine Methode auslösen kann. Exceptions, die mit dem Wort Ausnahmen ins Deutsche übersetzt werden, lernen wir weiter unten noch im Detail kennen. Das @throws-Tag kann beliebig oft angegeben sein. exception ist der Klassen-Name für die Exception. javadoc fügt den kompletten Klassen-Namen ein, wenn die Exception nicht vollständig angegeben ist. Mehr zur Hierarchie von Klassen lernen wir weiter unten. text beschreibt die Gründe für das Auslösen der Exception genauer. @see "text"

Mit dieser Variante des @see-Tags fügt javadoc den hervorgehobenen Text »See Also:« am Ende der Beschreibung für die Klasse, die Methode oder das Attribut ein und nach einem Zeilenvorschub text. Sind mehrere solcher @see-Tags vorhanden, dann werden die einzelnen Texte durch Komma getrennt angegeben. @see label

Mit dieser Variante, die ebenfalls mehrfach vorkommen kann, wird der Text label als Hypertext-Link auf URI dargestellt, optional kann auch ein HTML-Anker anchor angegeben sein, gekennzeichnet durch das vorangestellte Hash-Zeichen #. Der Text wird wie bei der ersten Variante von @see in den »See Also«-Bereich aufgenommen. @see [package.][class][#member[ label]]

Diese dritte Variante des @see-Tags ist die wohl am häufigsten benutzte. Mit ihr kann man im »See Also«-Bereich Hypertext-Links auf Klassen, Methoden und Attribute einfügen. Allerdings sei angemerkt, dass in der JDK-Version 1.4.0 zwar der Text richtig aufgenommen wird, aber keine Hypertext-Links erscheinen, was auf einen Bug zurückzuführen ist. Wie man an der Definition sieht, sind so ziemlich alle Bestandteile optional, können also auch wegfallen. Fehlt das label, dann setzt javadoc den nach dem @see-Tag stehenden Text ein, wobei eine gewisse Intelligenz an den Tag gelegt wird. javadoc sucht in den Java-Klassen des JDK (und zusätzlich in allen Klassen, die über den

Einführung

Kommandozeilen-Parameter -classpath angegeben sind) nach den angegebenen Klassen des @see-Tags, um das Label entweder in absoluter Schreibweise oder teilweise abgekürzt zu erzeugen, wenn es nicht angegeben ist. Ich glaube, hier sind ein paar Beispiele sicherlich nicht fehl am Platze: /** * @see * @see * @see * @see */

#DEBUG #init( int ) init String#equals java.lang.String#equals( Object ) equals

Das erste Beispiel erzeugt einen Eintrag unter »See Also« mit dem Text DEBUG, der sich auf ein Klassen- oder ein Methoden-Attribut innerhalb derselben Programmdatei bezieht. Da kein Label angegeben ist, setzt javadoc automatisch denselben String ein wie nach dem Hash-Zeichen angegeben. Das zweite Beispiel bezieht sich auf die im selben Programm implementierte Methode init(), die ein int-Argument erwartet. Hier ist jedoch der String init als Label angegeben, der im Hypertext-Link angezeigt werden soll. Das dritte Beispiel verweist auf die equals()-Methode der Klasse String. Da ich keinen Package-Namen für die Klasse angegeben habe, sucht javadoc in seinem Klassenpfad nach dieser Klasse (und wird bei java.lang fündig). Im letzten Beispiel wird der gleiche Verweis benutzt, jedoch in ausführlicher Schreibweise und mit einem expliziten Label, das im Hypertext-Link angezeigt werden soll.  {@link [package.]class[#member] label}

Dieses Tag ist identisch mit @see, jedoch wird der Hypertext-Link nicht in der See Also-Sektion angezeigt, sondern dort, wo das @link-Tag steht. Außerdem muss es durch geschweifte Klammern umrahmt werden. Will man im Label selbst eine geschlossene geschweifte Klammer benutzen, dann muss man diese durch die HTML-Notation } darstellen.  @deprecated text

Mit dem @deprecated-Tag kennzeichnet man eine Methode, Variable oder auch eine ganze Klasse oder ein Interface, um anzuzeigen, dass sie nicht mehr benutzt werden sollte, weil sie überholt und durch etwas anderes ersetzt worden ist. javadoc setzt den Text hinter dem Tag automatisch in eine deprecated-Überschrift und hebt

Einführung

ihn besonders hervor. Auch hier wird nur der erste Satz von text in der Übersicht angezeigt. Deshalb sollte man vor dem ersten Punkt von text mindestens angeben, seit welcher Programmversion die betreffende Programmstelle deprecated ist, wodurch sie ersetzt wurde, sowie einen Hypertext-Link auf den Ersatz. Manch einer wird sich nun vielleicht fragen, warum ich hier so lang und breit über adoc und Kommentare im Source-Code spreche. Nun, nichts passiert ohne Grund, d in diesem Fall ist der Grund die über Jahre hinweg gemachte Erfahrung, dass ein ogramm zwar relativ schnell implementiert ist, meist jedoch fällt eine saubere Dokumentation des Programms der Faulheit oder der Zeitnot zum Opfer. Und rade bei Java wäre dies schade, bietet sich doch hier die Möglichkeit, Dokumentaon direkt am Ort des Geschehens zu erstellen, mehr noch, Sie können Ihr API ereits fertig stellen, ohne den eigentlichen Programmcode geschrieben zu haben. aher mein Rat: Nutzen Sie das Feature Inline-Dokumentation mit javadoc reichlich.

Sprach-Grundlagen

2

Sprach-Grundlagen

In diesem Kapitel möchte ich Sie mit den Grundlagen von Java vertraut machen, die für die weiteren Kapitel benötigt werden. Sie sollten zumindest die Grundlagen der Programmierung als Basis-Wissen mitbringen.

2.1

Zeichensatz in Java-Programmen

Die meisten Programmiersprachen erlauben im Quell-Code nur einen begrenzten Zeichensatz, meist ISO-8859-1. Java geht hier einen Schritt weiter, denn es ist grundsätzlich Unicode-basiert. Dieser Zeichensatz kodiert jedes Zeichen mit 2 Bytes und umfasst damit wesentlich mehr Zeichen als zum Beispiel der ASCII-Zeichensatz, bei dem nur 7 Bits verwendet werden. Dabei ist die Codierung der ersten 256 Bytes kompatibel mit dem ISO-8859-1 Zeichensatz. Die Darstellung eines Unicode-Zeichens in Java erfolgt durch Voranstellen von \u vor den hexadezimalen Zeichen-Code. Ein Beispiel: ... String myVar = "Üund\u03A9 erlaubt";

Mit Ausnahme von \u03A9 sind alle Zeichen des Programmcodes mit dem ISO-8859-1 Code kompatibel und können daher direkt eingegeben werden. Der Unicode \u03A9 bedeutet das griechische Omega-Zeichen O und kann nicht über die Tastatur eingegeben werden, da es nicht mit einem, sondern mit zwei Bytes kodiert wird. Hinweis Der Unicode-Zeichensatz wird auch bei Strings und einzelnen Zeichen vom Typ char verwendet. Selbst die Namen von Variablen und Methoden sowie von Klassen können als Unicode-Zeichen angegeben sein (davon möchte ich jedoch abraten).

2.2

Identifier

Unter dem Begriff Identifier versteht man den Bezeichner bzw. den Namen einer Variablen, einer Methode oder einer Klasse. In Java muss ein Identifier mit einem Unterstrich oder einem Unicode-Zeichen beginnen, der einen Buchstaben repräsentiert (eine Ziffer am Anfang ist also nicht

Sprach-Grundlagen

aubt). Alle weiteren Zeichen eines Identifiers können beliebige Unicode-Zeichen mit Ausnahme von White Space sein (das sind alle Zeichen, die auf einem Ausdruck f Papier weiß bleiben, wie zum Beispiel das Leerzeichen). e Länge von Identifiern ist in Java unbegrenzt. Die Zeichen sind case-sensitive, zwihen Klein- und Großschreibung wird also unterschieden. n selbst definierter Identifier darf kein reserviertes Wort sein (auch die Zeichenketn true, false und null sind verboten, obwohl sie nicht zu den reservierten Wörtern hören).

3

Kommentare

Java unterstützt drei verschiedene Arten von Kommentaren: // Das ist ein einzeiliger Kommentar, der sich // bis zum Ende der Zeile erstreckt. /* Kommentar, der sich über mehrere Zeilen erstrecken kann. */ /** * Spezieller Kommentar für Inline-Dokumentation. * Man kennzeichnet ihn dadurch, dass zwei Asterisk* Zeichen nach dem Slash folgen. * Er wird vom Programm javadoc interpretiert, mit * dessen Hilfe man HTML-Dokumentation aus der * Inline-Doku erstellen kann. */

undsätzlich dürfen gleiche Kommentar-Arten nicht geschachtelt werden, eine ischung davon jedoch schon: /* Mischung aus verschiedenen Kommentaren. // einzeiliger Kommentar Und wieder normaler mehrzeiliger Kommentar. */

Sprach-Grundlagen

2.4

Datentypen

Java unterscheidet grundsätzlich zwei verschiedene Arten von Datentypen: Einfache Daten (primitive Datentypen) und Objekte, die als komplexe Daten in Klassen definiert werden. Objekte werden im nächsten Kapitel ausführlich behandelt, deshalb wollen wir uns hier auf die primitiven Datentypen beschränken. Datentyp

SpeicherBedarf

Wertebereich

Default-Wert

byte

1

-27 .. 27 – 1

0

short

2

-215 .. 215 – 1

0

int

4

-231 .. 231 – 1

0

long

8

-263 .. 263 – 1

0

float

4

±1.4E-45 .. ±3.4028235E38

0.0

double

8

±4.9E-324 .. ±1.7976931348623157E308

0.0

boolean

1

false .. true

false

char

2

\u0000 .. \uFFFF

\u0000

Tabelle 2.1: Übersicht der primitiven Datentypen

Hinweis Wenn Sie die Tabelle genau betrachten, stellen Sie fest, dass sowohl Speicherbedarf als auch Wertebereich nicht von der verwendeten CPU abhängen. Java ist wohl die einzige Sprache, die für jeden Datentyp einen konstanten Wertebereich vorgibt, egal, ob in dem Computer eine 32-Bit CPU oder einer 1024-Bit CPU eingebaut ist. Ein int-Wert ist also immer 4 Byte breit und hat auf jedem Computer der Welt denselben Wertebereich. Des Weiteren ist bemerkenswert, dass es in Java keinen vorzeichenlosen Datentyp für Zahlen gibt. Sehen wir uns nun die einzelnen primitiven Datentypen etwas näher an. 2.4.1

Der Datentyp boolean

Dieser Datentyp ist schnell erklärt, denn er kennt nur zwei Werte: false oder true. Beide Werte sind in Java zwar keine reservierten Wörter, können aber bei Zuweisungen wie solche verwendet werden. Da ich in diesem Buch voraussetze, dass Sie wis-

Sprach-Grundlagen

n, was logisch unwahr bzw. wahr bedeutet, möchte ich nicht näher auf die boolesche gebra eingehen. 4.2

Der Datentyp char

ie eingangs bereits erwähnt haben die Designer von Java von Anfang an Unicode als sis für alle Zeichen und Zeichenketten gewählt, ein einzelnes Zeichen vom Datenp char wird also immer mit 2 Bytes kodiert. Als einziger der primitiven Datentypen, t denen Zahlen verarbeitet werden können, besitzt ein char-Wert kein Vorzeichen. char-Literale (das sind Zeichen, die als Konstanten im Programmcode stehen), müs-

n in einfache Quotes gesetzt werden. Beispiel: char c = 'A'; // falsch wäre char c = "A"; // doppelte Quotes sind Strings vorbehalten

ie in allen Programmiersprachen üblich, unterstützt Java einige Zeichen mit besonrer Bedeutung (Escape-Sequenzen), die in der folgenden Tabelle aufgeführt sind: Escape-Sequenz

Bedeutung

\n

Zeilenvorschub (Unix-Zeilenende)

\r

Wagenrücklauf (MAC-Zeilenende)

\t

Tab

\b

Backspace

\f

Formfeed

\"

Doppeltes Quote

\'

Einfaches Quote

\\

Backslash

nnn \uxxxx

Oktal kodiertes Zeichen Unicode-Zeichen

elle 2.2: Übersicht der Escape-Sequenzen in Zeichen-Konstanten

Sprach-Grundlagen

Verwendet man oktal kodierte Zeichen, dann ist der größte Zeichen-Code 377. Es müssen zwar keine führenden Nullen angegeben werden, ich rate aber trotzdem dazu, damit zeigt man dem Leser deutlicher, was gemeint ist: char c = '\060'; // die Ziffer '0' // es ginge auch so: char c = '\60';

Achtung Vorsicht ist bei Unicode-Zeichen geboten. Der Java-Compiler ersetzt die Kodierung \uxxxx, bevor der Programmcode interpretiert wird. Ich möchte Ihnen die Auswirkung an einem Beispiel zeigen:

// Wir char c char c char c char c

wollen ein einfaches Quote literal verwenden = '''; // Fehler = '\''; // OK = '\047'; // OK = '\u0027'; // Fehler

Ich glaube, der Fehler im ersten Beispiel ist einleuchtend. Dass jedoch auch bei der letzten Variante ein Compiler-Fehler auftritt, ist nicht sofort ersichtlich. Hier muss man wissen, dass der Compiler als Erstes alle Unicode-Zeichen-Codes durch deren Zeichen-Äquivalent ersetzt, danach erst wird der Programmcode interpretiert. Nach der Ersetzung von \u0027 haben wir aber wie bei der ersten Variante wiederum drei Quotes im Programm stehen. Bei der oktalen Kodierung erhalten wir jedoch keinen Fehler, weil die Umwandlung des Zeichen-Codes hier erst nach dem Interpreter-Lauf erfolgt. 2.4.3

Der Datentyp String

Im Gegensatz zu den hier besprochenen primitiven Datentypen sind Zeichenketten (Strings) in Java Objekte. Strings werden ausführlich im Kapitel über wichtige Klassen besprochen. 2.4.4

Numerische Datentypen

Insgesamt bietet Java vier Integer- und zwei Fließkomma-Datentypen an. Allen gemeinsam ist die Tatsache, dass es in Java keine unsigned-Zahlen gibt (Zahlen ohne Vorzeichen).

Sprach-Grundlagen

Hinweis Java ist sehr konsequent, was die Portabilität von Zahlen angeht. Egal, welche CPU in Ihrem Rechner eingebaut ist, hat jeder numerische Datentyp einen fest vorgegebenen Wertebereich (unabhängig davon, ob Sie eine 8-, 16-, 32der 64-Bit CPU Ihr Eigen nennen). h möchte Sie hier nicht mit den Details der Zahlen-Darstellungen langweilen, vielmehr versuche ich, praktische Hinweise zu geben, die Ihnen beim Programmieren helfen können. Deshalb fällt die Übersicht der numerischen Datentypen relativ kurz aus: te

n byte-Wert nimmt im Hauptspeicher genau 1 Byte in Anspruch und hat aufgrund s immer vorhandenen Vorzeichens einen Wertebereich von -128 bis +127. ort short-Werte belegen im Hauptspeicher genau 2 Bytes und können Zahlen im Werte-

ereich von -32768 bis +32767 erreichen. t int-Werte sind genau 4 Bytes lang und können somit Werte im Bereich von

147483648 bis +2147483647 annehmen. Wenn Sie im Programm numerische erale verwenden, so werden diese standardmäßig als int-Werte behandelt: System.out.println( "Ergebnis: " + ( 3 + 5 ) ); // Sowohl 3 als auch 5 werden zunächst in int-Werte // umgewandelt, bevor die Summe gebildet (ebenfalls // int-Wert) und ausgegeben wird.

ng long-Werte sind genau 8 Bytes lang und können Werte im Bereich von 223372036854775808 bis +9223372036854775807 erreichen. long-Literale wern im Programm durch das Zeichen »L« oder »l« gekennzeichnet (ich empfehle ein

«, da es besser lesbar ist): long lg = 36578899999999L;

Ganzzahlige Literale können in den drei Zahlensystemen Oktal, Dezimal oder Hexazimal durch Voranstellen einer führenden »0« (Oktal-System) bzw. von »0x« (Hexa-

Sprach-Grundlagen

dezimal-System) angegeben sein. Ohne Präfix nimmt der Compiler an, dass es sich um eine Zahl im Dezimal-System handelt. Hier einige Beispiele: 17 // Dezimal-Zahl 0x7f // Hex-Zahl 037 // Oktal-Zahl 0x123456789012L // long-Hex-Zahl

float float-Werte belegen genau 4 Bytes im Hauptspeicher und sind Fest- oder Fließkomma-Zahlen. Literale float-Zahlen können durch den Buchstaben »F« bzw. »f« explizit als solche gekennzeichnet werden (fehlt die explizite Kennzeichnung, dann fasst der Compiler die Literale als double-Werte auf). Sie dürfen sowohl als Festkomma-Literale als auch mit einem Exponenten zur Basis 10 im Programm angegeben sein. Als Dezimalpunkt muss das Zeichen ».« verwendet werden, das Komma ist nicht erlaubt. Allerdings ist die erreichbare Genauigkeit recht gering, so dass sie eher selten benutzt werden. Der Wertebereich geht von ±1.4E-45 bis ±3.4028235E38.

Ein paar Beispiele für float-Literale: 3.14F -3.20E9f 17.4e-18F .37F 7F // Diese Notation ist zwar erlaubt, ich empfehle // aber stattdessen 7.0F // da man hier sofort sieht, dass es sich nicht // um eine Hex-Zahl handelt.

double double-Werte belegen genau 8 Bytes im Hauptspeicher und können daher den Wertebereich von ±4.9E-324 bis ±1.7976931348623157E308 durchlaufen. Literale Fest- oder Gleitkomma-Zahlen ohne eine explizite Typ-Angabe am Ende werden standardmäßig als double-Werte interpretiert. Allerdings kann man auch double-Literale explizit als solche durch Anhängen von »D« bzw. »d« kennzeichnen. Auch hierzu einige Beispiele:

Sprach-Grundlagen

-13.4 1.17E-130 -9.88888e30 -17E200d .0005e-10D 1D // OK, ich rate aber davon ab und empfehle 1.0

em die Wertebereiche der primitiven numerischen Datentypen nicht ausreichen, nn auf die im JDK enthaltenen Klassen java.math.BigInteger und java.math.BigDecimal zurückgreifen, die beliebig große Zahlen verarbeiten können (allerdings auf osten der Performance). ordefinierte numerische Konstanten

Das JDK bietet mit den so genannten Wrapper-Klassen für die primitiven Datentypen ter anderem einige Konstanten an, von denen ich Ihnen hier die wichtigsten vorllen möchte: Byte.MIN_VALUE, Byte.MAX_VALUE Short.MIN_VALUE, Short.MAX_VALUE Integer.MIN_VALUE, Integer.MAX_VALUE Long.MIN_VALUE, Long.MAX_VALUE Float.MIN_VALUE, Float.MAX_VALUE Double.MIN_VALUE, Double.MAX_VALUE Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY Float.NaN, Double.NaN

e Wrapper-Klassen Byte, Short, Integer und Long sowie Float und Double sind im pitel über Objektorientierte Programmierung beschrieben. Ich glaube, die Konstanten nd einigermaßen selbst erklärend. Auf der CD-ROM finden Sie in der Datei MinMax.java ein Beispiel für die Konstanten der Wrapper-Klassen. klärungsbedürftig könnte vielleicht die Konstante NaN sein, die in den Wrapperassen für Fließkomma-Zahlen definiert ist. Die Abkürzung NaN steht für Not-a-Number und wird dann zurückgegeben, wenn die Division 0.0/0.0 ausgeführt rd. Auch die Konstanten für die negative bzw. positive Unendlichkeit möchte ich

Sprach-Grundlagen

kurz erklären. Sie werden bei der Division -0.0/0.0 bzw. +0.0/0.0 zurückgegeben. Allerdings ist die Benutzung dieser speziellen Konstanten nicht ganz so einfach. Hierzu folgendes Beispiel: double d = 0.0 / 0.0; // Division durch 0 ergibt NaN if ( d == Double.NaN ) System.out.println( "Division durch Null" ); else System.out.println( "OK" );

Die Preisfrage lautet: »Was wird ausgegeben?« Ich denke, jeder Politiker, der »Division durch Null« zu seinem Wahlversprechen erklärt hätte, würde gewählt werden, oder? Tja, meist wird man nach der Wahl enttäuscht. So auch hier, denn der Programmcode gibt brav »OK« aus. Jetzt gehen wir noch einen Schritt weiter in der Verwirrung und lassen folgenden Programmcode laufen: System.out.println( "0 / 0 = " + 0 / 0 );

Als Ergebnis bekommen wir eine saftige RuntimeException in Form einer ArithmeticException (Wer mit diesen Begriffen nichts anfangen kann, sei auf das Kapitel Exceptions verwiesen, hier soll genügen, dass es sich dabei um Laufzeitfehler handelt, die von der Virtual Machine ausgelöst werden). Probieren Sie es aus, es stimmt. Damit nicht genug, wenn wir den Programmcode geringfügig ändern und einen der beiden Operanden der Division zu einem double-Literal machen, erhalten wir keine Fehlermeldung mehr: System.out.println( "0 / 0 = " + 0 / 0.0 );

Stattdessen gibt die Methode println() nun den Text "0 / 0 = NaN" aus. Ziemlich verwirrend, was? Nun wollen wir die Sache nach und nach aufklären:

Sprach-Grundlagen

Achtung Eine Division durch Null mit Integer-Werten führt zu einer Fehlermeldung mit Abbruch des Programms, wenn der Fehler nicht abgefangen wird, bei Gleitkomma-Werten jedoch tritt kein Fehler auf. Stattdessen enthält das Ergebnis der fehlerhaften Division je nach Wert und Vorzeichen des Zählers entweder NaN (Zähler ist 0.0), POSITIVE_INFINITY (Zähler ist positiv und größer als 0.0) oder NEGATIVE_INFINITY (Zähler ist negativ und sein absoluter Wert ist größer als 0.0). i Gleitkomma-Operationen muss das Ergebnis mit den Methoden Double.isNaN() w. Double.isInfinite() überprüft werden, etwa so wie im folgenden Beispiel: ... double x = 0.0; double y = 0.0; double d = x / y; if ( Double.isNaN( d ) || Double.isInfinite() ) { // Division durch Null

Der direkte Vergleich des Ergebnisses z.B. mit der Konstanten NaN führt jedoch nicht m gewünschten Ergebnis: ... if ( d == Double.NaN ) { // Der Vergleich ist false und kann somit nicht // verwendet werden.

5

Programmblöcke

n Java-Programm besteht nicht nur aus einem Block Programmcode, sondern thält vielmehr unterschiedliche Arten von Blöcken, die teilweise voneinander hängen. So setzt sich ein einfaches Hauptprogramm in Java mindestens aus zwei ogrammblöcken zusammen: public class MyProg { public static void main( String[] args ) { System.out.println( "Griaß Gott" ); }

Sprach-Grundlagen

Der Beispielcode enthält zwei Blöcke, nämlich der Klasse MyProg und einer darin befindlichen Methode (für alte C-Hasen: Funktion) main. Jeder Programmblock wird in Java durch ein Pärchen geschweifter Klammern umrahmt, die dessen Grenzen kennzeichnen. Normalerweise haben diese Programmblöcke einen Namen und eine vorbestimmte Bedeutung (MyProg ist durch das Schlüsselwort class eindeutig eine Java-Klasse, während main aufgrund der runden Klammern eindeutig als Methode identifiziert ist). Es ist sogar möglich, willkürlich Programmblöcke ohne einen Namen festzulegen, solche Blöcke nennt man dann anonyme Programmblöcke: { int i = 5; { int j = 6; } }

Vielleicht wird sich jetzt der eine oder andere fragen, wofür dieses komplizierte Gebilde notwendig ist. Sie werden weiter unten mehr dazu erfahren, wenn ich über Geltungsbereiche spreche. Hier soll genügen, dass jeder Programmblock, auch ein anonymer Block, einen neuen Geltungsbereich beginnt (und mit der schließenden geschweiften Klammer wieder beendet). Programmblöcke findet man in einem Java-Programm sehr häufig, z.B.: if ( i < 5 ) { // Programmblock System.out.println( "i < 5" ); } while ( i-- < 5 ) { // Programmblock System.out.println( i ); } for ( int i = 0; i < 5; i++ ) { // Programmblock }

Sprach-Grundlagen

6

Variablen

Java hat grundsätzlich jede Variable einen festen Datentyp, der bereits zur Compileit überprüft wird (Ausnahmen gibt es jedoch, wie wir bei der Besprechung von rays noch sehen werden). Das bedeutet, dass man bei einer Variablen-Deklaration im Programm immer den Datentyp mit angeben muss, der sich später nicht mehr ndern lässt. Dieser Datentyp muss entweder der Name eines der primitiven Typen n oder ein Klassen-Name. Primitive Variablen Eine primitive Variable hat als Datentyp einen der insgesamt 8 einfachen Typen byte, short, int, long, float, double oder boolean. Variablen dieses Typs werden deshalb primitiv genannt, weil der darin gespeicherte Wert direkt im Hauptspeicher als einfacher Wert abgelegt wird. Referenz-Variablen Eine Referenz-Variable enthält keinen primitiven Datenwert, sondern die Speicheradresse, wo ein Objekt als komplexes Gebilde im Hauptspeicher abgelegt ist. Solche Variablen entsprechen in etwa den Zeigern, die man aus anderen Programmiersprachen kennt. Der Name einer Variablen kann grundsätzlich aus allen alphanumerischen Unicodeichen bestehen (Ausnahme: White Space-Zeichen sind verboten). Er muss mit einem Buchstaben (auch das griechische Omega ist ein Buchstabe) oder einem Unterstrich beginnen und darf beliebig lang sein. 6.1

Deklaration von Variablen

vor eine Variable benutzt werden kann, muss sie dem Compiler durch eine Deklation bekannt gemacht werden. Wie bereits erwähnt, muss man dabei immer den Datentyp der Variablen angeben. Bereits bei der Deklaration einer Variablen kann diese initialisiert werden. Dies ist wie gesagt eine Kann-Regel, die Initialisierung ist o nicht zwingend erforderlich. Der Compiler führt eine automatische Initialisieng durch, wenn man dies bei der Deklaration nicht selbst tut. Handelt es sich um eine Referenz-Variable, dann sollte man es sich angewöhnen, diese immer bei der Deklaration zu initialisieren. Hierzu ein paar Beispiele: int i; // nur Deklaration, der Compiler führt eine // automatische Initialisierung mit 0 durch.

Sprach-Grundlagen

double d = 1.5; // explizite Initialisierung durch eine // Deklaration einschließlich Wert// Wert-Zuweisung. double g; // nur Deklaration, der Compiler initialisiert // die Variable mit dem Wert 0.0 long l; // Deklaration mit anschließender l = 78999900012L; // Zuweisung eines Werts. double d1 = d + i; // Eine Variable kann auch mit einem // Ausdruck initialisiert werden. boolean b; // nur Deklaration, der Compiler // initialisiert die Variable automatisch // mit dem Wert false. String s; // Deklaration einer Referenz-Variablen ohne // Initialisierung, der Compiler führt keine // automatische Initialisierung durch. String s1 = null; // // // //

besser, wir initialisieren die Referenz-Variable explizit mit dem Sonder-Wert null (d.h. s1 enthält keine Referenz auf ein Objekt).

String s2 = s1; // s2 enthält eine gültige Referenz // auf s1 (obwohl diese wiederum keine // gültige Referenz besitzt).

Wie bereits erwähnt, muss eine Variable deklariert werden, bevor man sie benutzt. Wo sich die Deklaration im Programmcode befindet, bleibt dem Programmierer überlassen, sie muss nur vor deren erstem Gebrauch stehen. Damit bleibt einem also erspart, dass man alle benutzten Variablen grundsätzlich am Anfang des Programmteils (z.B. des Hauptprogramms) deklarieren muss. Namenskonvention für Variablen

Nahezu für jede Programmiersprache existiert ein so genannter Style Guide, in welchem nachzulesen ist, welche Regeln man beim Schreiben von Programmcode einhalten sollte. Leider hält sich nur ein geringer Prozentsatz aller Entwickler an diese Konventionen. Ich möchte Sie ausdrücklich bitten, sich den auf der CD-ROM enthaltenen Style Guide nicht nur durchzulesen, sondern die dort beschriebenen Vorga-

Sprach-Grundlagen

n auch einzuhalten. Auch bezüglich Variablen-Namen gibt es eine Konvention, die sagt, dass der Name einer normalen Variablen mit einem Kleinbuchstaben beginn soll. Besteht der Name aus mehreren logischen Wort-Teilen, dann ist jeweils der ste Buchstabe eines logischen Teils großzuschreiben. Unterstriche für diese Trenng soll man nicht verwenden. Eine Ausnahme von dieser Regel gilt für Konstann, deren Name grundsätzlich komplett aus Großbuchstaben bestehen soll. Hier dürn Unterstriche verwendet werden. ispiele für Variablen-Namen: String thisIsAString; int loopCount; // schlechter Stil int loop_count; // ebenfalls gegen die Regeln, da erster Buchstabe groß int LoopCount; // Konstanten-Definition public final String VERSION = "1.1"; public final int MAX_COUNT = 1000;

6.2

Geltungsbereich (Scope)

ariablen besitzen eine Lebensdauer, d.h. sie sind nur innerhalb eines bestimmten reichs gültig. Diesen Bereich nennt man Geltungsbereich oder neudeutsch Scope. ne Variable beginnt mit ihrer Deklaration zu leben und ist so lange gültig, bis der eltungsbereich, in dem sie deklariert wurde, verlassen wird. a teilt in punkto Geltungsbereich die Variablen in drei verschiedene Arten ein: Lokale Variablen Variablen, die in einer Methode (so nennt man in Java eine Funktion) deklariert sind, werden als lokale Variablen bezeichnet. Sie gelten nur innerhalb dieser Methode und leben daher nur für die Dauer des Methodenaufrufs. Außerhalb der Methode sind sie völlig unsichtbar.

Sprach-Grundlagen



Klassen-Variablen Eine Variable, die statisch in einer Java-Klasse deklariert wird (wie das funktioniert, lernen wir im Kapitel über Objektorientierte Programmierung), ist von dem Moment an gültig, wo die Klasse von der Virtual Machine geladen wird. Ihr Geltungsbereich endet erst dann, wenn die Klasse wieder verworfen (entladen) wird.



Instanz-Variablen Variablen, die in einer Java-Klasse ohne das reservierte Wort static deklariert werden, sind so genannte Instanz-Variablen, auch Member-Variablen genannt. Solche Variablen beginnen ihr Leben damit, dass eine Instanz der Klasse erzeugt wird (ein Objekt der Klasse wird instanziert). Mit jedem neu erzeugten Objekt der Klasse wird eine eigenständige Kopie aller in der Klasse deklarierten Instanz-Variablen angelegt, die so lange gültig bleibt, bis das Objekt, zu dem sie gehört, vom Garbage Collector aus dem Hauptspeicher entfernt wird. Wer sich jetzt schwer tut mit den Begriffen, der sollte im Kapitel Objektorientierte Programmierung nachlesen.

Hinweis Neben den hier vorgestellten Variablen gibt es noch die so genannten formalen Parameter-Variablen, die bei der Deklaration von Java-Methoden angegeben sind. Auf diese Art werde ich im Abschnitt über Methoden zu sprechen kommen. Verdecken von Variablen

Unter diesem etwas seltsamen Ausdruck versteht man die Deklaration einer Variablen, die unter demselben Namen in einem weiter außen liegenden Programmblock bereits deklariert wurde. Ich demonstriere dies am besten anhand eines (nicht erlaubten) Beispiels: int i = 5; for ( int i = 1; i > b, der die Bits des Operanden a um so viele Stellen nach rechts schiebt, wie im zweiten Operanden b angegeben ist. Ist a negativ (das höchstwertige Bit von a ist gesetzt), dann wird mit jeder Schiebe-Operation ein 1-Bit von links nachgeschoben.

>>>

Rechts-Shift (ohne Vorzeichen)

wie vorher, jedoch wird mit jeder Schiebe-Operation ein 0-Bit von links nachgeschoben.

b.

>>>=

Rechts-Shift-Zuweisung ohne Vorzeichen

a >>>= b weist a den Wert a >>> b zu und evaluiert a >>> b.

elle 2.7: Zuweisungs-Operatoren

Sprach-Grundlagen

2.10.6

Sonstige Operatoren

Operator

Name

Bedeutung

( params )

Liste

Komma-separierte Liste von Parametern

( type )

Casting

Typ-Konvertierung

?:

Konditional

Abkürzung für if-else

[]

Array

Kennzeichen für ein Array

.

Punkt

Trennung von Packages und KlassenMethoden bzw. Attributen

New

new

Instanzierung neuer Objekte

Instanceof

instanceof

Klassen-Abfrage

Tabelle 2.8: Sonstige Operatoren

Einige Operatoren fallen ein wenig aus der Reihe, deshalb wurden sie in einer eigenen Gruppe zusammengefasst. An dieser Stelle möchte ich nur zwei vorstellen, die Beschreibung der anderen finden Sie in den Kapiteln der jeweiligen Themengebiete. Der Konditional-Operator ?:

Weiter unten lernen wir die Kontroll-Anweisung if-else kennen. Für einfache Abfragen wurde eine Abkürzung entwickelt, die häufig eingesetzt wird, der Fragezeichen-Operator, auch Konditional-Operator genannt. Hier die Syntax des Operators: expr ? trueExpr : falseExpr

Dies ist der einzige Operator mit drei Operanden. Links vom Fragezeichen muss ein Ausdruck expr stehen, der einen boolean-Wert zurückliefert (evaluiert). Wenn expr true evaluiert, dann wird der Ausdruck trueExpr links vom Doppelpunkt evaluiert, ansonsten der Ausdruck falseExpr rechts vom Doppelpunkt. Ich glaube, hier tut ein Beispiel gut: int i; ... // Lösung mit einer einfachen if-else-Abfrage int sign;

Sprach-Grundlagen

if ( i >= 0 ) sign = 1; else sign = -1; // Lösung mit ?: int sign = ( i >= 0 ) ? 1 : -1;

ie man deutlich sieht, ist der Code mit dem Konditional-Operator wesentlich kürer. Genau das war auch im Sinne des Erfinders. er Punkt-Operator .

n Operator, den man gar nicht als solchen wahrnimmt, ist der Punkt-Operator ».«. ir haben ihn bereits viele Male in den Beispielen verwendet, ohne ein Wort darüber verlieren. Auch hier möchte ich nicht viel Zeit damit verschwenden. Der PunktOperator wird benutzt, um den Variablen-Namen einer Referenz-Variablen auf ein Objekt vom Namen der aufzurufenden Methode oder des Attributs zu trennen. Bei atischen Methoden und Attributen trennt er den Klassen-Namen vom Methodenw. Attribut-Namen. Hierzu einige Beispiele: // Aufruf der Methode println(), die in der Klasse // System mit dem Attribut Objekt out zur Verfügung // gestellt wird. System.out.println(); | | |---------- Instanz-Methode println() | |-------------- Attribut out der Klasse System |--------------------- Klassen-Name // Wir speichern den Inhalt der lokalen Variablen // attr (rechts vom Gleichheitszeichen) in der // Instanz-Variablen attr ab, auf die wir über die // vordefinierte Variable this zugreifen. this.attr = attr; | |------------ Attribut attr der Variablen this |----------------- spezielle Instanz-Variable

er instanceof-Operator

Mit dem unären Operator instanceof kann man den Datentyp einer Referenz-Varile dynamisch zur Laufzeit abfragen. Ein Beispiel hierfür finden Sie im Kapitel Objektorientierte Programmierung.

Sprach-Grundlagen

2.10.7

Operator-Prioritäten

Gruppe

Operatoren

Postfix-Operatoren

[], ., ( params ), expr++, expr--

Unäre Operatoren

++expr, --expr, +expr, -expr, ~, !

Spezielle Objekt-Operatoren

new, ( type )

Multiplikative Operatoren

*, /, %

Additive Operatoren

+, -

Shift-Operatoren

, >>>

Vergleichs-Operatoren

, =, instanceof ==, !=

Bitweises AND

&

Bitweises EXOR

^

Bitweises OR

|

Logisches AND

&&

Logisches EXOR

^

Logisches OR

||

Konditional

?:

Zuweisung

=, +=, -=, *=, /=, %=, &=, ^=, |=, =, >>>=

Tabelle 2.9: Operator-Prioritäten

2.11 Expressions Wenn Sie Konstanten und Variablen mit Operatoren verknüpfen oder Methoden aufrufen, dann erhalten Sie einen Ausdruck, den man im Englischen Expression nennt. Solche Expressions bilden eine Programmeinheit. Mehrere solcher Einheiten wiederum gruppieren sich zu Anweisungen (Statements), die dann als ausführbarer Programmcode in der CPU abgearbeitet werden. Definition einer Expression

Unter einer Expression versteht man eine Serie von Konstanten, Variablen, Operatoren und Methoden-Aufrufen, die einen einfachen Rückgabe-Wert evaluiert.

Sprach-Grundlagen

ispiele für Expressions:

3 + 4 i = j + k Integer.parseInt( "134" )

12 Statements enn man eine oder mehrere Expressions mit einem Semikolon abschließt, dann hält man eine Anweisung, die im Englischen als Statement bezeichnet wird. Ein atement ist die kleinste Programmeinheit, die in der Regel als Ganzes von der CPU einem Stück ausgeführt wird. Man unterscheidet folgende Arten von Statements: 2.12.1

Ausdrucks-Statements

ese Art von Statements, die man im Englischen als Expression Statements bezeichnet, steht aus Expressions der folgenden Art: Zuweisungs-Ausdrücke Auto-Increment oder Auto-Decrement-Ausdrücke Methoden-Aufrufe Instanzierungen von Objekten Daneben gibt es noch den Sonderfall einer leeren Anweisung, die nur aus einem mikolon besteht. Man benötigt dieses Statement in seltenen Fällen, wenn die rach-Syntax eine Anweisung vorschreibt, das Programm aber gar nichts tun soll. er einige Beispiele für Expression-Statements: i = 5; i++; System.out.println( i );

12.2

Deklarations-Statements

mer dann, wenn Sie eine Variable deklarieren (und ggf. initialisieren), haben Sie ein Deklarations-Statement verwendet. Hier ein paar Beispiele:

Sprach-Grundlagen

int i; double d; String s = new String( "bla" );

Das letzte Deklarations-Statement ist übrigens gleichzeitig auch ein Ausdruck-Statement. 2.12.3

Kontroll-Statements

Kontroll-Statements dienen der Steuerung des Programmflusses. Die folgende Tabelle gibt Ihnen einen Überblick der in Java erhältlichen Kontroll-Statements: Statement-Typ

verfügbare Statements

Abfragen

if-else, switch-case

Exceptions

try-catch-finally, throw

Schleifen

for, while, do-while

Sprünge

break, continue, label:, return

Tabelle 2.10: Kontroll-Statements in Java

Bevor wir nun zu den einzelnen Kontroll-Statements kommen, möchte ich Ihnen die allgemeine Definition eines Statements nicht vorenthalten: Definition eines Statements // einfaches Statement // Alle Angaben in eckigen Klammern sind optional und // können daher auch entfallen. expr[ expr ...]; // zusammengesetztes Statement (compound statement) // Alle Angaben in eckigen Klammern sind optional und // können daher auch entfallen. { statement[ statement ...] }

Sprach-Grundlagen

ie wir sehen, gibt es zwei verschiedene Arten von Statements, einfache, die nur aus einer Reihe von Ausdrücken (Expressions) bestehen, die mit einem Semikolon abgeschlossen werden, und zusammengesetzte (compound statements), die mit geschweiften ammern gekennzeichnet werden und eine beliebige Anzahl von Statements enthaln können (die ihrerseits wiederum zusammengesetzte Statements sein dürfen). Zu Deutsch bedeutet dies: Ein einfaches Statement ist eine einzelne Anweisung, geschlossen durch ein Semikolon. Ein zusammengesetztes Statement besteht aus ehreren Einzel-Statements, die in geschweifte Klammern gesetzt werden. else

Das if-else-Statement ist eines der am häufigsten benutzten Mittel, um zur Laufzeit ogrammverzweigungen durchzuführen. Sehen wir uns die Syntax an (Angaben in kigen Klammern sind optional und können daher auch entfallen): if ( expr ) statement1[ else statement2]

Der Ausdruck expr wird zunächst evaluiert und muss einen boolean-Wert zurücklien. Ist das Ergebnis true, dann wird statement1 ausgeführt. Falls nicht, dann kann man im else-Zweig der Abfrage Programmcode hinterlegen, der dann ausgeführt wird, wenn die if-Abfrage false evaluiert hat. weit zur unverdaulichen Theorie. Wollen wir nun das Ganze anhand von Beispielen mit Leben füllen: ... int i = 3; int k = 4; if ( i == k ) System.out.println( "Gleichheit" ); if ( i < k ) System.out.println( "i < k" ); if ( i > k ) System.out.println( "i > k" ); // dasselbe, aber anders programmiert if ( i == k ) { System.out.println( "Gleichheit" ); else { if ( i < k ) {

Sprach-Grundlagen

System.out.println( "i < k" ); } else { System.out.println( "i > k" ); } }

Wie immer führen viele Wege nach Rom. Ich persönlich verwende grundsätzlich keine einfachen Statements in if-else-Abfragen, sondern zusammengesetzte Statements mit geschweiften Klammern (Ausnahmen können in diesem Buch durchaus vorkommen, um Platz zu sparen). Der Grund ist ganz einfach. Meist fügt man zu einem späteren Zeitpunkt weitere Statements im if-Block oder im else-Block hinzu. In diesem Fall muss man jedoch ein zusammengesetztes Statement mit geschweiften Klammern bilden, da dies bei mehreren Statements notwendig ist. Häufig vergisst man dies aber, und schon hat man sich selbst eine Falle gestellt: // Ursprünglicher Code if ( i > 10 ) j = 1; // Veränderter Code if ( i > 10 ) j = 1; System.out.println( "i > 10" );

Wir haben im Laufe der Zeit ein Debug-Statement hinzugefügt und damit den ifBlock um ein Statement erweitert. Dieses wird aber nicht nur dann ausgeführt, wenn die Variable i größer als 10 ist, sondern immer, unabhängig vom aktuellen Wert von i. Damit die Sache funktioniert, müssen wir geschweifte Klammern setzen: // Ursprünglicher Code if ( i > 10 ) { j = 1; } // Veränderter Code if ( i > 10 ) { j = 1; System.out.println( "i > 10" ); }

Sprach-Grundlagen

itch-case

ssen Sie mich es deutlich ausdrücken: Ich halte nicht viel vom switch-Statement. Es bt wohl nur sehr seltene Fälle, in denen man dieses Statement wirklich benötigt. Als Fingerzeig gilt: Braucht man es öfter, dann hat man falsch programmiert. Aber sei’s um, hier die Syntax (wie immer sind alle Angaben in eckigen Klammern optional d können daher entfallen): switch ( expr ) { case constant1: statement ...[ break;] case constant2: ...[ break;] ...[ default: statement ...]

Der Ausdruck expr wird nacheinander mit den Konstanten in den case-Zeilen verglien. Ist expr mit einer Konstanten identisch, dann wird der Programmcode dieses case-Blocks (ausnahmsweise dürfen hier die geschweiften Klammern fehlen) ausgehrt. Am Ende jedes case-Blocks kann man optional mit der break-Anweisung dafür rgen, dass das switch-Statement verlassen wird. Fehlt die break-Anweisung, dann hrt die Virtual Machine mit dem nächsten case-Block fort. Ebenfalls optional kann die switch-Anweisung einen default-Block enthalten, der in jedem Fall ausgeführt wird, wenn vorher nicht mit break das switch-Statement verlassen wurde. Aus der Sicht der Programmlogik ist die switch-Anweisung nichts anderes als eine schachtelte if-Abfrage. Sie hat allerdings einen gravierenden Nachteil, denn nach m reservierten Wort case dürfen nur Konstanten stehen, keine Variablen. Zudem üssen die Konstanten und der Ausdruck expr, der nach switch steht, kompatibel ueinander sein. Damit Sie sehen, was ich von diesem Statement halte, verzichte ich er auf ein Beispiel.

Sprach-Grundlagen

for-Schleife

Ein sehr häufig benutztes Vehikel in der Programmierung sind Schleifen, in denen Programmcode mehr als nur einmal ausgeführt werden kann. Ein wichtiger Vertreter dieser Gattung ist die for-Schleife, die meist dann verwendet wird, wenn die Anzahl der Schleifen-Durchläufe vorher bekannt ist (obwohl sehr viele Programmierer sie für Endlos-Schleifen benutzen). Hier ist die Syntax der for-Schleife (Angaben in eckigen Klammern sind optional und können entfallen): for ( [init]; [condition]; [postProcess] ) statement

Eine for-Schleife besteht aus einem Kopf-Teil und einem Rumpf-Teil. Im SchleifenRumpf steht der Programmcode, der x-mal ausgeführt werden soll, er kann beliebig viele zusammengesetzte Statements enthalten. Der Schleifen-Kopf besteht aus dem reservierten Wort for und drei weiteren Bestandteilen, die durch runde Klammern umrahmt sind. Der init-Teil wird vor dem Eintritt in den ersten Schleifen-Durchlauf einmalig ausgeführt und dient meist der Initialisierung von Schleifen-Variablen. Es dürfen mehrere Variablen initialisiert werden, jeweils durch ein Komma getrennt. Allerdings müssen alle Variablen im Datentyp zueinander kompatibel sein. condition ist ein Ausdruck, der einen boolean-Wert zurückliefern muss und eine Abfrage darstellt, ob der Programmcode im Schleifen-Rumpf ausgeführt werden soll. condition wird jedes Mal ausgeführt, bevor ein neuer Schleifen-Durchlauf beginnt. Ergibt die Abfrage false, dann wird die for-Schleife beendet. Der postProcess-Teil schließlich wird jedes Mal am Ende eines Schleifen-Durchlaufs ausgeführt, also immer nach dem letzten Statement des Schleifen-Rumpfs. Er kann auch leer sein. Man darf mehrere Ausdrücke durch Komma getrennt angeben. Ich glaube, ein paar Beispiele für die for-Schleife tun gut (Das in manchen Beispielen verwendete break-Statement lernen wir gleich kennen.): // for-Schleife, die 10-mal durchlaufen wird for ( int i = 0; i < 10; i++ ) { System.out.println( "i = " + i ); } // dasselbe, diesmal jedoch ist der Initialisierungsteil // leer. int i = 0;

Sprach-Grundlagen

for ( ; i < 10; i++ ) { System.out.println( "i = " + i );

// Im nächsten Beispiel wird der Schleifen-Zähler // im Schleifen-Rumpf erhöht. for ( int i = 0; i < 10; ) { System.out.println( "i = " + i ); i += 2;

// Nun lassen wir den Abfrage-Teil weg und verlassen // die Schleife aus dem Schleifen-Rumpf heraus. for ( int i = 0; ; i++ ) { if ( i >= 10 ) break; System.out.println( "i = " + i );

// Typische Endlos-Schleife for ( ; ; ; ) { // nicht vergessen: Die Schleife muss per // Programmlogik aus dem Schleifen-Rumpf heraus // abgebrochen werden, da man sonst tatsächlich // eine endlos ablaufende Schleife gebaut hat. if ( ... ) break;

// Ausgabe eines zweidimensionalen Arrays mit // geschachtelten for-Schleifen int[][] ar = new int[ 5 ][ 5 ]; for ( int i = 0; i < ar.length; i++ ) { for ( int j = 0; j < ar[ i ].length; j++ ) { System.out.println( "ar[ " + i + " ][ " + j + " ] = " + ar[ i ][ j ] ); }

// Verwendung mehrerer Schleifen-Variablen for ( int i = 0, j = i + 1; i < 5; i++, j += 2 ) { System.out.println( "i = " + i + ", j = " + j );

Sprach-Grundlagen

// // // //

Bei der Initialisierung der Schleifen-Variablen darf der Datentyp nur einmal angegeben werden, alle verwendeten Variablen müssen also denselben Datentyp haben.

while-Schleife

Neben der for-Schleife wird häufig die while-Schleife benutzt, und zwar immer dann, wenn die Anzahl der Schleifen-Durchläufe nicht von vornherein bekannt ist und sich das Abbruch-Kriterium somit erst im Schleifen-Rumpf ergibt. Die whileSchleife hat folgende Syntax: while ( expr ) statement

expr dient demselben Zweck wie bei der for-Schleife, nämlich vor jedem Eintritt in den Schleifen-Rumpf zu prüfen, ob dieser überhaupt ausgeführt werden soll. Der Schleifen-Rumpf selbst besteht wiederum aus beliebig vielen zusammengesetzten Statements (oder auch aus einem einzelnen einfachen Statement). Zur Veranschaulichung wieder ein paar Beispiele: int i = 10; while ( i > 0 ) { System.out.println( "i = " + i ); i--; } // typische Endlos-Schleife int i = 0; while ( true ) { // nicht vergessen: Irgendwann muss die Schleife // aus dem Schleifen-Rumpf heraus beendet werden. if ( i == 10 ) break; System.out.println( "i = " + i ); i++; }

Ein wesentlicher Unterschied zur for-Schleife ist, dass ein Schleifen-Zähler (wenn vorhanden) im Schleifen-Rumpf explizit erhöht (oder verringert) werden muss, da dies nicht im Schleifen-Kopf möglich ist.

Sprach-Grundlagen

-Schleife

e do-Schleife arbeitet fast genauso wie die while-Schleife, jedoch wird nicht vor ntritt in den Schleifen-Rumpf überprüft, ob dieser ausgeführt werden soll, sondern m Ende des Schleifen-Rumpfs (nach der letzten Anweisung im Schleifen-Rumpf). as hat zur Folge, dass bei der do-Schleife die Statements des Schleifen-Rumpfs minstens einmal ausgeführt werden. Syntax der do-Schleife: do statement while ( expr );

iederum besteht der Schleifen-Rumpf meist aus zusammengesetzten Anweisungen, e in geschweifte Klammern gesetzt werden. Achtung Das Semikolon am Ende der do-Schleife ist kein Tippfehler meinereits, sondern zwingend vorgeschrieben. eak und continue

ie wir in den Beispielen für Schleifen bereits gesehen haben, ist es manchmal nötig, us dem Schleifen-Rumpf heraus die Entscheidung zu treffen, ob die Schleife beendet erden oder sofort der nächste Schleifen-Durchlauf begonnen werden soll. Hierfür llt Java zwei Statements zur Verfügung: break und continue. e Bedeutung der reservierten Wörter ist eindeutig, break führt zum sofortigen bbruch einer Schleife, continue zum sofortigen Beenden des aktuellen SchleifenRumpfs und zum Eintritt in den nächsten Durchlauf, ohne dass die folgenden Anweingen des Schleifen-Rumpfs ausgeführt werden. Vorher wird bei for-Schleifen türlich der Programmcode der PostProcess-Ausdrücke und die Abbruch-Bedingung sgeführt. Das break-Statement haben wir ja bereits bei den Endlos-Schleifen kenn gelernt. Hier ein Beispiel für das continue-Statement: // Ausgabe aller ungeraden Zahlen for ( int i = 0; i < 10; i++ ) { // nächsten Schleifen-Durchlauf beginnen, wenn // der Zähler eine gerade Zahl enthält if ( ( i % 2 ) == 0 ) continue; System.out.println( "i = " + i );

Sprach-Grundlagen

Achtung Die Statements break und continue dürfen nur innerhalb von Schleifen verwendet werden. Beide Statements wirken sich ohne weitere Angaben nur auf die den Schleifen-Rumpf umgebende Schleife aus. Schleifen-Labels

Bei geschachtelten Schleifen möchte man des Öfteren eine weiter außen liegende Schleife beenden, obwohl man sich in einer inneren Schleife befindet: // Die folgende geschachtelte Schleife soll beendet // werden, wenn das Produkt von i und j größer als 50 // ist. for ( int i = 0; i < 10; i++ ) for ( int j = i + 1; j < 10; j++ ) { if ( ( i * j ) > 50 ) break; }

Wenn wir den Programmcode ausführen, erzielen wir leider nicht das gewünschte Ergebnis, denn die break-Anweisung führt nur zum Abbruch der inneren Schleife, wir wollen aber die äußere Schleife beenden. Zu diesem Zweck bietet Java die Möglichkeit, eine Schleife mit einem Namen, dem so genannten Label, zu versehen. Damit kann man gezielt eine bestimmte Schleife entweder beenden (break) oder mit deren nächstem Schleifen-Durchlauf fortfahren (continue). Die Syntax eines Labels ist wie folgt: label: Schleife

Um die so gekennzeichnete Schleife anzusprechen, gibt man nach dem break- bzw. continue-Statement deren Name label an: loop1: for ( int i = 0; i < 10; i++ ) { for ( int j = i + 1; j < 10; j++ ) { if ( ( i * j ) > 50 ) break loop1; } }

Sprach-Grundlagen

return

e return-Anweisung gehört ebenfalls zu den Kontroll-Statements und wird in Methon benutzt, um die aktuelle Methode sofort zu verlassen, womit der Programmfluss eder zum aufrufenden Programmcode zurückkehrt. Es sind zwei Varianten möglich: return; return expr;

e erste Variante wird in Methoden vom Typ void verwendet, während bei der zwein Variante dem aufrufenden Programmteil ein Rückgabewert expr übergeben wird. eser muss kompatibel zum Datentyp der Methode sein. Mehr hierzu sehen Sie im Abschnitt über Methoden. sert

Das Kontroll-Statement assert wurde mit dem JDK 1.4 neu eingeführt. Man benutzt , um ein Programm mit einem harten Fehler abzubrechen, wenn bestimmte Annahmen über den Zustand von Variablen oder Programmzuständen nicht erfüllt sind. Hinweis Falls Sie noch ein Neuling in Java sind, können Sie die Beschreibung des assert-Statements zunächst überspringen, denn für das Verständnis der Anweiung sind einige Dinge Voraussetzung, die erst in den folgenden Kapiteln erläutert werden. e Syntax des assert-Statements ist wie folgt: assert expr; assert expr : msg;

pr muss ein Ausdruck sein, der einen boolean-Wert evaluiert. Solange expr einen true-Wert evaluiert, passiert überhaupt nichts, das Programm verhält sich so, als wäre s assert-Statement gar nicht vorhanden. In dem Moment, wo expr einen falseWert evaluiert, löst die Virtual Machine einen AssertionError aus. Das ist eine besonre Ausnahme, die nicht von der Klasse java.lang.Exception abgeleitet wird, ndern von java.lang.Error. der zweiten Variante kann man mit msg einen Ausdruck angeben (meist ein String), r Zusatz-Informationen über den Fehler enthält. Aus technischer Sicht wird msg im onstruktor von java.lang.AssertionError übergeben.

Sprach-Grundlagen

Wenn Sie sich den Text genau durchgelesen haben, stellen Sie vielleicht fest, dass man dasselbe Verhalten auch mit normalen Abfragen und Exceptions erreichen kann. Assertions besitzen jedoch den Vorteil, dass sie zur Laufzeit über ein Kommandozeilen-Argument der Virtual Machine ein- oder ausgeschaltet werden können. Standardmäßig sind sie abgeschaltet. In diesem Fall werden die assert-Statements von der Virtual Machine überhaupt nicht beachtet. Auch läuft das Programm damit etwas schneller, als hätte man statt Assertions normale Abfragen eingebaut. Damit Sie auch in der Praxis etwas mit dem Begriff Assertions anfangen können, möchte ich die Wirkungsweise anhand eines Beispiels verdeutlichen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

import java.util.*; public class AssertTest { public static void main( String[] args ) { MyClass cl = new MyClass(); for ( int i = 0; i < 10; i++ ) { String s = "Item " + i; if ( i == 9 ) { s = null; } cl.addItem( s ); } } } class MyClass { private Vector items = new Vector(); private int maxItems = 50; private int nItems = 0; public void addItem( Object o ) { assert nItems < maxItems : "Speicher voll"; assert o != null : "NULL object"; items.addElement( o ); nItems++; } }

Sprach-Grundlagen

Das Hauptprogramm benutzt eine einfache Klasse MyClass, um Objekte in einem eicher abzulegen. Hierzu habe ich eine Methode addItem() implementiert. Sie verndet das assert-Statement, um sicherzustellen, dass die maximale Anzahl von Elementen nicht überschritten werden kann (Zeile 23) und kein null-Objekt aufgenommen wird (Zeile 24). In der abgebildeten Version des Programms wird ein ufzeitfehler aufgrund eines null-Objekts ausgelöst. Wenn Sie in Zeile 19 den Wert r die maximale Anzahl von Elementen auf einen Wert ändern, der kleiner als 10 ist, halten Sie eine Assertion mit dem Text "Speicher voll". amit Sie das Programm übersetzen können, benötigen Sie in jedem Fall mindestens die Version 1.4.0 des JDK und müssen dem Compiler mit dem Kommandozeilengument »-source 1.4« mitteilen, dass er Assertions unterstützen soll. Tun Sie das nicht, dann ernten Sie eine Fehlermeldung. Hier die Kommandozeile für den Comer-Lauf: C:\temp>javac -source 1.4 AssertTest.java

Damit Assertions zur Laufzeit unterstützt werden, müssen sie dies der Virtual Machine über den Schalter »-ea« mitteilen: C:\temp>java -ea AssertTest

obieren Sie sowohl die Variante mit als auch ohne Schalter »-ea« aus und vergleien Sie die Resultate. Man hätte alternativ auch normale Exceptions verwenden können: 01 import java.util.*; 02 03 public class AssertTest1 { 04 public static void main( String[] args ) { 05 MyClass cl = new MyClass(); 06 for ( int i = 0; i < 10; i++ ) { 07 String s = "Item " + i; 08 if ( i == 9 ) { 09 s = null; 10 } 11

Sprach-Grundlagen

12 try { 13 cl.addItem( s ); 14 } catch ( Exception ex ) { 15 System.err.println( ex ); 16 System.exit( 1 ); 17 } 18 } 19 } 20 } 21 22 class MyClass { 23 private Vector items = new Vector(); 24 private int maxItems = 50; 25 private int nItems = 0; 26 27 public void addItem( Object o ) throws Exception 28 { 29 if ( nItems >= maxItems ) 30 throw new Exception( "Speicher voll" ); 31 if ( o == null ) 32 throw new Exception( "NULL object" ); 33 items.addElement( o ); 34 nItems++; 35 } 36 }

Der große Unterschied zum Beispiel mit Assertions besteht darin, dass der Programmcode für die Überprüfung immer ausgeführt wird und nicht abgeschaltet werden kann. Achtung Assertions sind aus anderen Programmiersprachen bekannte Konstrukte, die harte Laufzeitfehler produzieren, wenn sich aufgrund von Programmier-Fehlern inkonsistente Programmzustände ergeben. Auch in der JDK-Version 1.0 war dieses Feature vorgesehen, wurde jedoch aus zeitlichen Gründen zurückgestellt und erst mit der Version 1.4 ins JDK integriert. Das bedeutet natürlich, dass auf jedem Rechner, auf dem Java-Programme mit Assertions laufen sollen, mindestens das JDK 1.4 installiert sein muss. Außerdem sind Assertions standardmäßig ausgeschaltet. Um sie zu aktivieren, muss die Virtual Machine mit dem Kommandozeilen-Argument »-ea« gestartet werden (die LangForm dieses Schalters ist »-enableassertions«).

Sprach-Grundlagen

uch dem Compiler muss explizit über das Kommandozeilen-Argument »-source 1.4« gesagt werden, dass er Assertions berücksichtigen soll. Vergisst man dies, dann ntet man einen Compiler-Fehler, der auf höfliche Art mitteilt, dass assert seit Veron 1.4 ein reserviertes Wort ist. Da es erfahrungsgemäß weit mehr als ein Jahr dauert, bis sich eine neue Java-Version abliert hat, sollte man Assertions derzeit gezielt nur dann einsetzen, wenn klar ist, ss dieses Feature auch auf allen benutzten Rechnern unterstützt wird.

13 Methoden Ich möchte hier nicht in die Details von Funktionen einsteigen, da ich davon ausgehe, ss Sie wissen, was eine Funktion oder was der Unterschied zwischen formalen Paraetern und lokalen Variablen ist. Vielmehr will ich Ihnen mit diesem Abschnitt zein, worauf man beim Entwurf und bei der Implementierung von Methoden (so ißen Funktionen in Java) achten muss. n paar grundlegende Dinge sollten aber zum gemeinsamen Verständnis nicht fehlen, deshalb zunächst zur allgemeinen Deklaration einer Methode (alle Angaben in kigen Klammern sind optional und können entfallen): perms][ static][ final] type name([ params ])[ throws exList] Methoden-Rumpf[ return expr;]

ms kann, falls vorhanden, einer der drei folgenden Strings sein: public, private er protected. Was die einzelnen Strings bedeuten, werde ich im Kapitel Objektoritierte Programmierung beschreiben. Ebenso die Bedeutung der reservierten Wörter static und final. Das Schlüsselwort throws ist im Kapitel Exceptions näher erläutert. er von Bedeutung sind type, mit dem man den Datentyp (und damit den returnlue) der Methode festlegt, sowie params, mit dem die formalen Parameter der Methode als Komma-separierte Liste angegeben werden.

Sprach-Grundlagen

2.13.1

Der Datentyp von Methoden

In Java muss jede Methode mit einem festgelegten Datentyp deklariert werden, der auch den return-Wert bestimmt. Dieser muss entweder einer der acht primitiven Datentypen sein oder der Klassen-Name eines Objekts, das die Methode zurückgibt. Hat eine Methode keinen return-Value, der an den Aufrufer übergeben wird, dann muss der spezielle Datentyp void angegeben werden. In jedem Fall muss der Rückgabe-Wert, den die Methode an den aufrufenden Programmcode übergibt, kompatibel zu dem in der Deklaration angegebenen sein. Falls der Datentyp void ist, gibt die Methode gar keinen return-Value zurück, sondern beendet sich einfach dadurch, dass entweder das letzte Statement des Methoden-Rumpfs ausgeführt wurde, oder durch ein leeres return-Statement ohne Argument. Hierzu ein paar Beispiele: // Methode vom Typ int // Es muss zwingend ein return-Statement vorhanden // sein, mit dem ebenfalls ein int-Wert an den // Aufrufer zurückgegeben wird. public int sum( int a, int b ) { return a + b; } // Methode, die keinen return-Value besitzt, sondern // einfach irgendetwas tut. public void pr( String msg ) { System.out.println( msg ); } // dasselbe, aber diesmal wird ein return-Statement // verwendet, um den Programmcode zu vereinfachen. // Normalerweise hätte man eine geschachtelte // if-Abfrage implementieren müssen, wenn man // der Maxime folgt, dass eine Methode grundsätzlich // nur einen Eingang und nur einen Ausgang haben soll. public void countNumbers( int num ) { if ( num < 0 ) return; if ( num < 10 ) { ones++; return; } if ( num < 100 ) { tens++;

Sprach-Grundlagen

return; } if ( num < 1000 ) { hundreds++; return; }

// Methode vom Typ String, die eine Instanz-Variable // mit dem Argument erweitert und dann zurückgibt. // Das reservierte Wort this wird benötigt, um den // formalen Parameter msg vom Variablen-Namen des // Instanz-Attributs unterscheiden zu können. protected String doLog( String msg ) { this.msg += msg; return this.msg;

// Beispiel, bei dem zwar mit dem return-Statement // ein anderer Datentyp zurückgegeben wird, als // im Datentyp der Deklaration angegeben ist, dieser // aber aufgrund der Kompatibilität vom Compiler // akzeptiert wird, weil die Klasse Vector das // Interface List implementiert und somit kompatibel // zu diesem Datentyp ist. public List add( int[] ar ) { Vector v = new Vector(); for ( int i = 0; i < ar.length; i++ ) { v.addElement( new Integer( ar[ i ] ) ); } return v;

13.2

Die Parameterliste von Methoden

Java werden alle Aufruf-Parameter von Methoden als Kopie übergeben (call-byue), da es keine Zeiger-Variablen oder Adress-Operatoren wie zum Beispiel in C bt. Was passiert aber nun, wenn einer Methode ein Objekt übergeben wird? Nun, jedes Objekt wird über eine Referenz-Variable angesprochen, die man beim Aufruf einer Methode als Parameter ganz normal wie andere Variablen auch als Kopie ergibt. Die Referenz-Variable enthält jedoch nicht das Objekt selbst, sondern nur

Sprach-Grundlagen

dessen Adresse im Hauptspeicher. Also wird der Methode beim Aufruf eine Kopie dieser HauptSpeicheradresse übergeben. In der Methode kann man deshalb den Wert der Referenz-Variablen selbst nicht ändern, die übergebene Referenz-Variable behält also in jedem Fall ihren ursprünglichen Wert. Das Objekt, auf das die Referenz-Variable zeigt, kann aber vom Programmcode der Methode geändert werden, obwohl die Referenz-Variable selbst als Kopie an die Methode übergeben wurde. Hat Ihnen das weitergeholfen? Wenn nicht, hier ein Beispiel: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Datei ArgTest01.java // Das Programm zeigt, dass man auf der Hut sein // sollte, wenn man Objekte als Parameter von // Methoden-Aufrufen benutzt. public class ArgTest01 { public static void main( String[] args ) { int[] ar = { 1, 2, 3, }; myMethod( ar ); printArray( ar ); } public static void myMethod( int[] ar ) { ar[ 0 ] = 10; ar = null; } public static void printArray( int[] ar ) { if ( ar == null ) { System.out.println( "null" ); return; } for ( int i = 0; i < ar.length; i++ ) { System.out.println( "ar[ " + i + " ] = " + ar[ i ] ); } } }

Sprach-Grundlagen

Im Hauptprogramm wird zunächst ein Array aus int-Werten definiert, das wir schließend als Argument für den Methoden-Aufruf benutzen. Nachdem sich die Methode beendet hat, geben wir den Inhalt unseres Arrays aus. der Methode myMethod() ändern wir das erste Element des übergebenen Arrays, nach löschen wir das Array, indem wir die Referenz auf null setzen. enn wir das Programm ausführen, dann erhalten wir folgende Ausgabe: ar[ 0 ] = 10 ar[ 1 ] = 2 ar[ 2 ] = 3

e Ausgabe beweist: Die Methode ist zwar in der Lage, das über die Referenz-Varile übergebene Objekt zu ändern (nicht vergessen: Arrays sind Objekte). Die Refenz-Variable selbst bleibt jedoch unangetastet, weil die Methode ja nur eine Kopie r Variablen erhält. Um auch dies zu ermöglichen, müsste man eine Referenz auf die Referenz übergeben, und das ist in Java aufgrund des fehlenden Adress-Operators nicht möglich.

Objektorientierte Programmierung (OOP)

3

Objektorientierte Programmierung (OOP)

Mit diesem Kapitel möchte ich Sie in die Geheimnisse der objektorientierten Programmierung (kurz OOP genannt) einweihen. Java ist vollständig objektorientiert, deshalb können Sie kein einziges Programm schreiben, ohne die Konzepte von OOP verstanden zu haben (obwohl manche die Copy/Paste-Methode bevorzugen, bei der man nichts verstehen muss, wenn man weiß, an welchen Stellen was zu ändern ist).

3.1

Klassen I

In Java sind Klassen das Vehikel, mit dem Programme zum Laufen gebracht werden. Selbst in einem sehr einfachen Programm, das nur aus ein paar Anweisungen besteht, muss zwingend eine Klassen-Definition stehen. An dieser Stelle möchte ich Ihnen den Unterschied zwischen Klassen auf der einen Seite und Objekten bzw. Instanzen auf der anderen Seite aufzeigen. OOP-Anfänger tun sich häufig schwer, diese Dinge auseinander zu halten. Eine Klasse beschreibt die Eigenschaften und das Verhalten einer bestimmten Gattung von Objekten. So könnte man zum Beispiel die Gattung Mensch dadurch beschreiben, dass sie die Eigenschaft besitzt, auf zwei Beinen zu gehen. Eines der Verhaltensmuster von Menschen ist die Sprache, denn diese ist selbst im Zeitalter von Email und Videospielen immer noch die typische Methode der Unterhaltung zwischen einzelnen Objekten der Klasse Mensch. Das Attribut Bewegungsart und die Aktion bzw. die Methode Sprechen sind spezifisch für die Klasse Mensch und gelten gleichermaßen für alle Objekte dieser Gattung. Dagegen unterscheiden sich die einzelnen Menschen durchaus darin, wie sie auf ihren zwei Beinen gehen oder wie sie sprechen. Jedem Individuum der Klasse (man spricht hier von Instanz) wird bereits durch die Geburt mit auf den Weg gegeben, wie seine Art zu sprechen oder seine Gangart sein wird. Den Zeitpunkt der Geburt eines menschlichen Individuums, bei der ein neues Objekt der Klasse Mensch ins Leben gerufen wird, nennt man in der objektorientierten Welt Instanzierung. Wie man an der heutigen Umweltbelastung durch die vielen Objekte Mensch sieht, verbraucht jede Instanz eines Klassen-Objekts System-Ressourcen, bei Programmen ist dies im Wesentlichen Hauptspeicher.

Objektorientierte Programmierung (OOP)

Das Mitgeben der individuellen Eigenschaften eines einzelnen Objekts übernimmt r so genannte Konstruktor (zu Deutsch Zusammenbauer). Er besorgt alle notwendigen essourcen und sorgt dafür, dass die neue Instanz der Klasse einmalig ist. ne Klasse ist sozusagen der Bauplan, in dem steht, wie ein einzelnes Objekt zusammengebaut werden muss, welche Eigenschaften es haben soll und wie es sich gegener der Außenwelt verhält. Wenn ein Objekt gemäß Bauplan erstellt worden ist, hat man eine Instanz der Klasse vor sich. Java werden Klassen mit dem reservierten Wort class definiert, dem der Name der asse und der Klassen-Rumpf in geschweiften Klammern folgen: // einfachste Form einer Klassen-Definition // Dateiname: MyClass.java class MyClass { // Klassen-Rumpf

ir werden weiter unten sehen, dass es mehrere Varianten der Klassen-Deklaration bt, hier habe ich nur die einfachste Form beschrieben. Der Klassen-Rumpf in schweiften Klammern enthält den Programmcode für die Klasse, also unter anderem tribute in Form von Variablen sowie Methoden (so heißen Funktionen in OOP). Hinweis In Java muss der Dateiname der Java-Datei, in welcher der Programmcode für eine Klasse abgespeichert wird, exakt so lauten wie der Klassenname, die Datei-Endung muss .java sein. Dabei wird von Java zwischen Groß- und Kleinschreibung unterschieden (case-sensitive). Die Klasse MyClass muss also in der Datei MyClass.java abgespeichert werden. assennamen müssen mit einem Buchstaben beginnen und dürfen im Weiteren nur uchstaben, den Unterstrich und Ziffern enthalten. Umlaute oder sonstige Sonderchen im Klassennamen sind nicht erlaubt. er erste Buchstabe des Klassennamens sollte unbedingt ein Großbuchstabe sein. steht der Klassenname aus mehreren logischen Wortteilen, dann sollte jeder neue ortteil mit einem Großbuchstaben beginnen (keine Unterstriche als Trennzeichen r die einzelnen Wortbestandteile verwenden). Der Name MyClass ist also wesentlich sser als My_class.

Objektorientierte Programmierung (OOP)

Im Folgenden möchte ich kurz auf die einzelnen Bestandteile im Rumpf einer Klasse eingehen. 3.1.1

this

Mit dem reservierten Wort this stellt Java dem Programmierer eine vordefinierte Referenz-Variable auf das aktuelle Objekt zur Verfügung, mit der man auf alle Daten der Instanz zugreifen kann: public class MyClass { // Instanz-Attribut String title = null; // Konstruktor public MyClass( String title ) { this.title = title; } }

3.1.2

Attribute

Attribute (englisch attributes) stellen die Eigenschaften eines Objekts oder einer Klasse dar und werden unter anderem auch Felder (englisch fields) genannt. Die AttributWerte sind in Variablen abgespeichert. Klassen-Attribute

Klassen-Attribute, auch statische Attribute genannt, repräsentieren Eigenschaften der ganzen Klasse und nicht eines einzelnen Objekts. Wenn man für die Klasse Mensch zum Beispiel die Anzahl der Arme als Attribut aufnehmen würde, müsste man eine Variable deklarieren (nennen wir sie armCount), die für alle Instanzen der Klasse nur einmal definiert ist und nicht in jedem einzelnen Objekt erneut angelegt wird, wenn die Instanz vom Konstruktor ins Leben gerufen wird. In Java werden statische Klassen-Variablen dadurch gekennzeichnet, dass man vor den Datentyp der Variablen das reservierte Wort static setzt. Beispiel für ein statisches Klassen-Attribut: class MyClass { static String className = "MyClass"; }

Objektorientierte Programmierung (OOP)

ill man ein Klassen-Attribut lesen oder schreiben, dann adressiert man die dafür rgesehene Variable, indem man vor den Variablennamen den Klassennamen und nen Punkt als Trennzeichen stellt: // Lesen eines Klassen-Attributs String name = MyClass.className; // Setzen eines Klassen-Attributs MyClass.className = "MyClass";

Hinweis Auf Klassen-Attribute kann man auch dann zugreifen, wenn noch kein Objekt der Klasse instanziert worden ist. Instanz-Attribute

Gegensatz zu statischen Attributen, die für alle Instanzen einer Klasse nur einmal im Hauptspeicher angelegt werden, sind Instanz-Attribute nicht der gesamten Klasse, ndern individuell jedem einzelnen Objekt zugeordnet (durch so genannte Memr-Variablen, auch Instanz-Variablen genannt). Für die Klasse Mensch könnte man m Beispiel die Armlänge als Instanz-Attribut definieren, das im Gegensatz zum assen-Attribut armCount von Mensch zu Mensch unterschiedlich ist. enn der Konstruktor eine neue Instanz der Klasse erzeugt, dann werden alle Memr-Variablen, in denen die Instanz-Attribute abgespeichert sind, extra für dieses eine Objekt individuell im Hauptspeicher angelegt. ispiel für ein Instanz-Attribut: class YourClass { // Member-Variable, die der Compiler für jedes neue // Objekt individuell anlegt. // Werden zum Beispiel 20 Instanzen der Klasse // erzeugt, dann gibt es diese Variable ebenso // 20-mal. int width = 10;

r Variablennamen von Attributen gilt dasselbe wie bei Klassennamen, nur sollten mit einem Kleinbuchstaben beginnen. Ausnahme: Hat das Attribut den Charakter ner Konstanten, dann sollte der Variablenname nur aus Großbuchstaben bestehen.

Objektorientierte Programmierung (OOP)

Der lesende bzw. schreibende Zugriff auf Instanz-Attribute erfolgt ähnlich wie bei Klassen-Attributen, nur wird in der Regel vor den Punkt als Trennzeichen nicht der Klassenname gestellt, sondern der Name einer Referenz-Variablen auf ein instanziertes Objekt der Klasse (allerdings ist auch die erste Variante möglich). Im Programmcode des Klassen-Rumpfs kann man die vordefinierte Variable this verwenden, wenn man auf Instanz-Variablen des aktuellen Objekts zugreifen möchte: // Klasse MyClass class MyClass { ... // statische Klassen-Attribute static String VERSION = "1.0"; static String title = "MyClass"; // Instanz-Attribute int width = 10; int height = 5; // Zugreifen auf Member-Variablen mit this int dim = this.width * this.height; } // Hauptprogramm public class MyClassTest { public static void main( String[] args ) { MyClass cl = new MyClass(); // Zugreifen auf Member-Variablen mit einer // Instanz-Variablen cl.width = 150; // Zugreifen auf Klassen-Variablen String version = MyClass.VERSION; // es geht auch über die Objekt-Referenz String title = cl.title; } }

Wie wir im Beispielcode des Hauptprogramms sehen, kann man auf statische KlassenAttribute sowohl durch Angabe des Klassennamens als auch über eine Objekt-Referenz zugreifen.

Objektorientierte Programmierung (OOP)

Hinweis Auf Instanz-Attribute kann man nur über Objekt-Referenzen zugreifen, es muss also vorher ein individuelles Objekt instanziert worden sein. 1.3

Methoden

sher haben wir nur die Eigenschaften einer Klasse bzw. einer Instanz betrachtet, die Variablen implementiert werden. Das Verhalten einer Klasse bzw. einer spezifischen Instanz einer Klasse drückt sich durch Aktionen aus, die mit einem Objekt der mehreren Objekten) irgendetwas tun. Aktionen werden üblicherweise in Form n Funktionen implementiert, die man bei OOP Methoden nennt. ie schon bei den Attributen gezeigt, gibt es zwei verschiedene Arten von Methon: solche, die im Kontext der Klasse statisch definiert sind (Klassen-Methoden), und jene, die im Kontext einer Instanz der Klasse definiert sind (Instanz-Methoden). Klassen-Methoden

assen-Methoden werden im Kontext der Klasse ohne Bezug auf ein individuelles Objekt der Klasse mit dem vorangestellten reservierten Wort static definiert. Diese Methoden können direkt über den Klassennamen aufgerufen werden, ohne dass ein Objekt instanziert werden muss. Allerdings kann man damit natürlich nicht auf Instanztribute zugreifen, die ja an ein individuelles Objekt der Klasse gebunden sind. ispiel für eine statische Klassen-Methode: class MyClass { // Klassen-Attribute static String title = "MyClass"; // Instanz-Attribute int width = 10; static String getTitle() { return title; } // Die folgende Methode führt zu einem Fehler, weil // sie als statische Klassen-Methode definiert ist, // aber auf eine Instanz-Variable zugreift, die // nicht im Klassen-Kontext, sondern im Instanz// Kontext definiert ist. static int getWidth() {

Objektorientierte Programmierung (OOP)

return width; } }

Instanz-Methoden

Instanz-Methoden werden immer im Kontext einer individuellen Instanz einer Klasse definiert, sie können also nur über eine Objekt-Referenz aufgerufen werden. InstanzMethoden können sowohl auf Instanz-Attribute als auch auf statische Klassen-Attribute zugreifen. Beispiel für eine Instanz-Methode: class MyClass { // Klassen-Attribute static String title = "MyClass"; // Instanz-Attribute int width = 10; // Instanz-Methode int getWidth() { return width; // es ginge natürlich auch so: // return this.width; } }

Was die Namenskonventionen für Methoden angeht, gilt das bereits für Attribute Gesagte. Zusätzlich gibt es hier ein paar Einschränkungen bzw. besondere Regeln zu beachten: 

Ist der Methodenname identisch mit dem Klassennamen, dann handelt es sich zwangsweise um einen Konstruktor, der eine spezielle Methode ist und daher eine eigene Syntax in der Deklaration besitzt. Wir werden Konstruktoren im nächsten Abschnitt kennen lernen.



Wählt man als Methodenname die Zeichenkette toString, dann interpretiert der Java-Compiler die Methode mit einem Hauch Automagic, was bedeutet, dass solche Methoden eine spezielle Bedeutung haben und damit besondere Unterstützung durch den Compiler erfahren, ähnlich wie Konstruktoren. Wir werden auch darauf noch zurückkommen.

Objektorientierte Programmierung (OOP)

Methoden, die nichts anderes tun als lesend oder schreibend auf Attribute zuzugreifen und so die Attribute nach außen kapseln, haben eine spezielle Namenskonvention, auf die ich gleich eingehen werde. Accessor-Methoden

ese Art von Methoden werden zur Kapselung von Attributen verwendet und geben r den aktuellen Wert eines bestimmten Attributs zurück. Was Kapselung bedeutet, erden wir lernen, wenn ich die Konzepte von OOP erläutere. cessor-Methoden werden auch als Getter-Methoden oder kurz Getter bezeichnet. Sie ben insofern eine spezielle Namenskonvention, als der Methodenname in gewisser eise mit dem Namen der Instanz-Variablen für das zu lesende Attribut verheiratet . Man hängt einfach vor den Variablennamen für das Attribut die Zeichenkette get d schreibt den ersten Buchstaben des Variablennamens groß, schon hat man den ethodennamen. Dazu ein Beispiel (die Bedeutung von private und public ist zwar rmutlich zu erahnen, ich werde diese Begriffe aber weiter unten noch ausführlicher handeln): class User { private String login = null; private String pwd = null; // Accessor-Methoden public String getLogin() { return login; } public String getPwd() { return pwd; }

tribute, die einen booleschen Wert enthalten, werden wiederum gesondert behanlt, hier wird entweder das Präfix is oder has verwendet, wobei der hintere Teil des ethodennamens nicht immer exakt dem Variablennamen für das Attribut entspricht: class User { ... // Instanz-Attribute mit einem booleschen Wert private boolean enabled = true;

Objektorientierte Programmierung (OOP)

private boolean errorFlag = false; // Accessor-Methoden public boolean isEnabled() { return this.enabled; } public boolean isDisabled() { return ! this.enabled; // es ginge auch // return ! isEnabled(); } public boolean hasErrors() { return this.errorFlag; } }

Mutator-Methoden

Diese Art von Methoden werden wie Accessor-Methoden zur Kapselung von Attributen verwendet und setzen den Wert eines bestimmten Instanz-Attributs. Was Kapselung bedeutet, werden wir lernen, wenn ich die Konzepte von OOP erläutere. Mutator-Methoden werden auch Setter-Methoden oder kurz Setter genannt. Wie schon bei den Accessor-Methoden gezeigt, besitzen auch Mutator-Methoden eine spezielle Namenskonvention, nur hier wird als Präfix für den Methodennamen die Zeichenkette set verwendet. Attribute mit booleschen Werten werden auch hier gesondert behandelt. Hierzu ein paar Beispiele: class User { private String login = null; private String pwd = null; // Instanz-Attribute mit einem booleschen Wert private boolean enabled = true; private boolean errorFlag = false; // Mutator-Methoden für nicht-boolean Attribute public void setLogin( String login ) { this.login = login;

Objektorientierte Programmierung (OOP)

} public void setPwd( String pwd ) { this.pwd = pwd; } // Mutator-Methoden für boolean Attribute public void setEnabled( boolean flag ) { this.enabled = flag; } // Die Methode enable() ist keine reine // Mutator-Methode, sondern eher eine normale // Aktions-Methode, auch wenn in diesem Beispiel // die einzige Aktion darin besteht, ein Instanz// Attribut zu verändern. public void enable() { this.enabled = true; } public void setError () { this.errorFlag = true; } public void setError ( boolean flag ) { this.errorFlag = flag; } // Auch die Methode cleanErrors() ist keine echte // Mutator-Methode, sondern eine normale Aktions// Methode. public void cleanErrors () { this.errorFlag = false; }

toString()-Methode

ie ich bereits erwähnt habe, verhält sich Java bei Methoden, die den Namen toString gen, anders als bei normalen Methoden, toString() ist ein wenig automagic, weil hier tomatisch etwas Besonderes passiert, ohne dass der Programmierer es merkt. Wie wir im nächsten Kapitel sehen werden, dient der Operator + bei Strings der Verttung von einzelnen Zeichenketten:

Objektorientierte Programmierung (OOP)

"abc" + " def" // ergibt "abc def"

Wird ein Objekt vom Typ String als Parameter von print() oder println() verwendet, dann wird die Zeichenkette, die in der Objekt-Referenz abgelegt ist, ausgegeben (das ist bereits magisch, wie wir sehen werden, wenn ich die String-Klasse erläutere): System.out.println( myString ) // gibt den Inhalt der // Variablen myString aus

So richtig automagic wird es aber, wenn wir in einer String-Verkettung oder der Methode println() ein selbst definiertes Objekt verwenden: class MyClass { private String title = "MyClass"; } // Hauptprogramm ... MyClass obj = new MyClass(); System.out.println( obj ); ... String s = "Objekt: " + obj;

Sowohl die Methode println() als auch der Verkettungs-Operator + erwarten als Argument bzw. Operand eine Zeichenkette. Dies ist aber im Falle der Referenz-Variablen obj nicht der Fall, denn hier handelt es sich nicht um eine Zeichenkette, sondern um ein Objekt bzw. eine Instanz einer Klasse, und das ist kein einfacher Datentyp (englisch primitive data type), sondern ein komplexes Gebilde. Was soll nun der Compiler damit anfangen? Soll er den Klassennamen oder den Inhalt der Instanz-Variablen title anstelle der Objekt-Referenz einsetzen oder vielleicht eine Fehlermeldung produzieren? Nun, er tut nichts dergleichen. Immer dann, wenn der Compiler auf das Problem stößt, statt einer erwarteten Zeichenkette ein Objekt vor sich zu haben, dann versucht er zunächst, die Methode toString() des Objekts aufzurufen und deren return-Wert

Objektorientierte Programmierung (OOP)

verwenden (damit ist klar, welchen Datentyp die Methode haben muss, nämlich String). Findet er keine Methode mit diesem Namen, dann setzt er einfach den Klas-

nnamen und die HauptSpeicheradresse des Objekts ein. ill man also zum Beispiel für Debug-Zwecke Objekte einschließlich der aktuellen erte ihrer Attribute ausgeben, dann muss man eine Instanz-Methode mit dem Namen toString implementieren, die alle Daten in einem String aufsammelt und dien dann zurückgibt. Sehen wir uns dazu ein Beispiel an: class MyClass { private String title = "MyClass"; public String toString() { return "title = '" + this.title + "'"; } public static void main( String[] args ) { MyClass cl = new MyClass(); System.out.println( cl ); }

enn wir das Programm übersetzen und ausführen (Datei-Name muss MyClass.java in), dann erhalten wir als Ausgabe: title = 'MyClass'

Das ist also exakt das, was die Methode toString() zurückliefert. Ohne die Methode toString() hätten wir wie gesagt nur den Klassennamen und die HauptSpeicherresse in der Ausgabe gesehen. ain()-Methode

enso wie die toString()-Methode erfährt die statische Methode main() eine sondere Sprach-Unterstützung in Java. Jede Klasse, in der diese Methode impleentiert wird, kann von der Virtual Machine ausgeführt werden, im MethodenRumpf von main() steht also das Hauptprogramm:

Objektorientierte Programmierung (OOP)

class MyClass { .... // Hauptprogramm public static void main( String[] args ) { // Methoden-Rumpf mit dem Hauptprogramm ... System.exit( 0 ); } }

Die Methode hat ein Array von Strings als Parameter, das die KommandozeilenArgumente enthält. Interessanterweise ist die Methode mit dem Datentyp void deklariert, sie hat also keinen Rückgabewert. Dieser wird wie im Beispiel gezeigt über den Aufruf der Methode exit() der Klasse System an die SHELL zurückgegeben, in der die Virtual Machine gestartet wurde. Mit dem Beenden der Methode (entweder durch return oder den exit()-Aufruf oder weil der Interpreter das letzte Statement des Methoden-Rumpfs abgearbeitet hat) wird auch die Virtual Machine beendet, die das Hauptprogramm abarbeitet. Methoden-Signatur

Jede Methode in Java, auch die im folgenden Abschnitt beschriebenen Konstruktoren, besitzen eine so genannte Signatur, mit der eine Methode eindeutig identifiziert werden kann. Dies hat insbesondere bei polymorphen Methoden eine besondere Gewichtung (auf Polymorphie werde ich in der Beschreibung der Konzepte von OOP eingehen). Die Signatur einer Methode setzt sich zusammen aus dem Methodennamen und der Parameter-Liste. Hier ein paar Beispiele: class TestClass { | Signatur | public TestClass() { ... } | Signatur | public TestClass( int width ) { ... } |Signatur | public String getTitle() { ... }

Objektorientierte Programmierung (OOP)

| Signatur | public String getTitle( String locale ) { ... } | Signatur | public void setDim( int width, int height ) { .. }

ir werden wie gesagt später noch einmal auf Signaturen zurückkommen, wenn es um Polymorphie geht. Hinweis Der Datentyp einer Methode ist nicht Bestandteil der Signatur. Das ist uch nicht notwendig, um eine Methode eindeutig zu bestimmen, weil es in Java nicht erlaubt ist, in einer Klasse (oder im Vererbungsbaum mehrerer Klassen, den wir noch kennen lernen werden) zwei Methoden mit derselben Signatur, aber unterschiedlichen Datentypen zu implementieren. 1.4

Konstruktoren

ie eingangs bereits erwähnt, ist einiges zu tun, um eine individuelle Instanz einer Klasse Leben zu rufen, es müssen Dinge wie Hauptspeicher-Reservierung und Initialisieng durchgeführt werden. Diese Arbeiten übernimmt der so genannte Konstruktor. Ich öchte hier nicht auf die Details der verschiedenen Variations-Möglichkeiten von Konuktoren eingehen, das werde ich weiter unten noch erledigen. Hier sollen Sie nur die undlagen kennen lernen, damit Sie im Groben wissen, was ein Konstruktor ist. Der Konstruktor ist dadurch gekennzeichnet, dass in seiner Methoden-Deklaration r Datentyp fehlt (der Datentyp ist praktisch die Klasse), und der Methodenname identisch ist mit dem Klassennamen. Fehlen in der Konstruktor-Deklaration die formalen Funktions-Parameter, dann spricht man von einem Default-Konstruktor, der ederum eine spezielle Bedeutung hat, wie wir später sehen werden. Natürlich können in der Konstruktor-Deklaration auch formale Funktions-Parameter gegeben sein, die man meist für die Initialisierung von Instanz-Attributen verwendet. er ein paar Beispiele für Konstruktoren: class User { // Instanz-Attribute String login = null;

Objektorientierte Programmierung (OOP)

String pwd = null; // Default-Konstruktor public User() { this.login = ""; this.pwd = ""; } // Konstruktor, mit dem der Login-Name // initialisiert wird. public User( String login ) { this.login = login; } // Konstruktor, mit dem sowohl der Login-Name als // auch das Kennwort initialisiert werden. public User( String login, String pwd ) { this.login = login; this.pwd = pwd; } }

Erzeugt werden neue Instanzen einer Klasse mit dem reservierten Wort new, dem der Konstruktor-Name und in runden Klammern die Parameter für den Konstruktor folgen: class MyClass { public static void main( String[] args ) { // Instanzierung eines neuen Objekts // der Klasse. Es wird der Default-Konstruktor // aufgerufen. MyClass cl = new MyClass(); } }

Der Aufruf des Konstruktors mit dem reservierten Wort new liefert eine Objekt-Referenz auf die neu angelegte Instanz zurück. Wer den Begriff Referenz nicht kennt, sei auf später vertröstet.

Objektorientierte Programmierung (OOP)

Hinweis Wenn man in einer Klasse keinen Konstruktor definiert hat, dann fügt er Compiler automatisch einen Default-Konstruktor ohne Argumente in den bersetzten Byte-Code ein, da beim Erzeugen einer neuen Klassen-Instanz immer ein Konstruktor aufgerufen wird. at man mindestens einen Konstruktor mit Parametern in einer Klasse definiert und rwendet eine beliebige andere Klasse den Default-Konstruktor unserer Klasse, dann uss man zwingend auch einen Default-Konstruktor implementieren, da sich der Compiler sonst weigert, das Programm zu übersetzen. 1.5

Destruktoren

er andere objektorientierte Programmiersprachen kennt, erwartet jetzt sicherlich die Beschreibung von Destruktoren. Tja, voll daneben, kann ich nur sagen, denn in a gibt es keine Destruktoren (obwohl man sie in gewisser Weise doch implementieen kann). Destruktoren dienen dazu, nicht mehr benötigten Speicher wieder freizuben, das ist immer dann der Fall, wenn ein Objekt keine Referenz mehr aufweist d damit tot ist. In Java wird das Aufräumen toter Objekte automatisch im Hinterund durch den so genannten Garbage Collector durchgeführt, man muss sich also nicht selbst darum kümmern. Angemerkt sei, dass es in manchen Fällen doch notndig wird, selbst für Aufräumarbeiten zu sorgen. Meist hat man aber etwas falsch ogrammiert, wenn man an diesem Punkt angelangt ist. 1.6

Anwendungsbeispiele

zt wollen wir uns die Zusammenhänge an einem konkreten Beispiel in Form eines einfachen Java-Programms ansehen, das eine ebenso einfache Klasse benutzt: der Java-Programmcode, der in einer Datei mit der Endung .java abgespeichert ist, uss mindestens eine Klasse enthalten. Will man ein ausführbares Programm schrein, dann muss man die spezielle statische Methode main() in die Klasse aufnehmen, die den Code des Hauptprogramms enthält. Fehlt diese Methode, dann kann die asse nur von anderen Programmen benutzt werden (diese müssen dann die statische Methode main() implementieren). Zur Demonstration erstellen wir nun zwei Java-Dateien: IUser.java, in der wir eine einfache Klasse implementieren, und IUserTest.java, in der wir unser Hauptprogramm terbringen. Die Klasse IUser definiert ganz allgemein einen Internet-User mit zwei tributen (Login-Name und Kennwort) sowie ein paar Methoden, um auf die Attrite zugreifen zu können:

Objektorientierte Programmierung (OOP)

01 // Datei IUser.java 02 class IUser { 03 // Attribute 04 private String login = null; 05 private String pwd = null; 06 07 // Getter-Methoden 08 public String getLogin() { 09 return this.login; 10 } 11 12 public String getPwd() { 13 return this.pwd; 14 } 15 16 // Setter-Methoden 17 public void setLogin( String login ) { 18 this.login = login; 19 } 20 21 public void setPwd( String pwd ) { 22 this.pwd = pwd; 23 } 24 }

Hinweis Der Ausdruck null wird bei Objekten verwendet, um das absolute Nichts darzustellen. Eine Objekt-Variable, der man den Wert null zuweist, enthält keinerlei Referenz, sie zeigt also ins Leere (obwohl sie durch die Deklaration einem bestimmten Objekt-Typ bzw. einer bestimmten Klasse zugeordnet ist). Die Variablen-Deklaration in Zeile 04 besagt nicht etwa, dass wir eine String-Variable deklarieren und ihr einen leeren String zuweisen, sondern wir deklarieren die Variable login vom Typ String, die ins Leere zeigt, es wird also zu diesem Zeitpunkt noch kein Speicherplatz für den Variablen-Wert reserviert. An mehreren Stellen unserer Beispiel-Klasse sehen Sie das Wort this, dias wir weiter oben bereits kurz kennen gelernt hatten. Java stellt innerhalb des Klassen-Rumpfs den Methoden eine spezielle vordefinierte Variable mit dem Namen this zur Verfügung. Sie ist eine Referenz auf die aktuelle Instanz der Klasse und kann im Konstruktor sowie in allen Instanz-Methoden benutzt werden. Der Ausdruck

Objektorientierte Programmierung (OOP)

this.login

eift also auf die Variable login des aktuellen Objekts der Klasse IUser zu. Diese plizite Angabe ist zwar nicht unbedingt notwendig, da man innerhalb von InstanzMethoden auch direkt über den Variablennamen auf die Attribute zugreifen kann, wie zum Beispiel so: public String getPwd() { return pwd;

ber zum einen wird der Programmcode mit der expliziten Angabe this besser lesr, zum anderen haben wir manchmal keine andere Wahl, wie zum Beispiel bei: 17 18 19

public void setLogin( String login ) { this.login = login; }

Hier gibt es bereits den formalen Methoden-Parameter mit dem Namen login. Im ethoden-Rumpf können wir nun nicht einfach schreiben: 18

login = login;

i diesem Code wüsste der Compiler nicht, dass wir mit dem login auf der linken ite der Zuweisung unsere Instanz-Variable meinen. Manch einer wird jetzt einwern: »Dann lass uns doch einfach den formalen Parameter anders nennen!«. Und wirkh, er hat Recht, diese Maßnahme beseitigt den Namenskonflikt: public void setLogin( String newLogin ) { login = newLogin; }

e explizite Verwendung von this erhöht jedoch die Lesbarkeit des Programmcodes, il man sofort sieht, welche Variable gemeint ist.

Objektorientierte Programmierung (OOP)

Und hier unser Hauptprogramm (Datei IUserTest.java), in dem wir die Klasse IUser benutzen: 01 class IUserTest { 02 // Mit der statischen Methode main wird das 03 // Hauptprogramm gekennzeichnet 04 public static void main( String[] args ) { 05 IUser u = new IUser(); 06 u.setLogin( "hans.dampf" ); 07 u.setPwd( "verysecret" ); 08 09 System.out.println( 10 "Login-Name = '" + u.getLogin() + 11 "', Kennwort = '" + u.getPwd() + "'" 12 ); 13 14 System.exit( 0 ); 15 } 16 }

In Zeile 05 instanzieren wir ein Objekt der Klasse IUser und weisen der Variablen u eine Referenz auf das neu erstellte Objekt zu. Das geschieht durch das reservierte Wort new, dem ein Konstruktor folgt. In unserem Fall handelt es sich um einen Default-Konstruktor ohne Parameter, deshalb die runden Klammern ohne Argumente. In den Zeilen 06 sowie 07 benutzen wir zwei Instanz-Methoden, um die Attribute des Objekts mit Werten zu versorgen. An dieser Stelle möchte ich noch einmal kurz auf die Bedeutung des Punkts zu sprechen kommen: 06

u.setLogin( "hans.dampf" );

Die Programmzeile liest sich wie folgt: »Rufe die Methode setLogin()der Instanz auf, deren Referenz in der Variablen u gespeichert ist, und übergebe einen konstanten String als Argument!«. Der Punkt dient in Java also als Trennzeichen für einzelne Komponenten, ähnlich wie der Slash / als Trenner für Verzeichnisse. Diese PunktNotation wird auch bei Packages verwendet. Die Punkt-Notation ist auch gut zu erkennen in den Zeilen 09 und 14, wo ich vordefinierte Referenzen bzw. Methoden der Klasse System verwende. Diese Klasse wird im Kapitel Wichtige Klassen besprochen.

Objektorientierte Programmierung (OOP)

Hinweis Natürlich kann man die main()-Methode und damit das Hauptproramm auch direkt in die Klasse IUser stellen, in diesem Fall benötigen wir nur ine Datei. Es ist durchaus üblich, jede Klasse mit einer main()-Methode zu versehen, auch wenn die Klasse gar nicht für den direkten Aufruf aus der SHELL heraus edacht ist. Dies ist vor allem für Debug-Zwecke während der Programmentwickung von Vorteil, denn man kann in der main()-Methode den Test-Code für die Klassen-Methoden unterbringen. weit zur Theorie. Sehen wir uns jetzt die Praxis an und lassen unser Testprogramm laufen:

Abbildung 3.1: Übersetzen und Starten des Programms IUserTest

Der aufmerksame Leser wird vielleicht bemerkt haben, dass ich zwar unser Hauptproramm, nicht aber unsere Klassen-Datei kompiliere. Das haben wir dem intelligenten Compiler javac zu verdanken, der beim Bearbeiten der Zeile 05 unseres Hauptproamms erkennt, dass er die Datei IUser.class laden muss. Dabei überprüft er auch, ob vorher die Java-Datei IUser.java übersetzen muss, was zum Beispiel der Fall ist, nn die Class-Datei noch nicht existiert oder ein älteres Datum hat als die JavaDatei. Dieser intelligente Vorgang ist rekursiv, damit werden automatisch alle verwenten Klassen wenn nötig neu kompiliert. Achtung Damit der Java-Compiler und auch die Virtual Machine alle benötigten Dateien finden, sollten beide Java-Dateien im selben Verzeichnis stehen. Falls dies nicht der Fall ist, muss man über die Umgebungs-Variable CLASSPATH den Suchpfad erweitern. Mehr über den CLASSPATH finden Sie in Kapitel 1. vor wir den ersten Teil des Buchs Klassen zuschlagen, möchte ich Ihnen noch einal die Methode toString() zeigen, die in Java etwas Besonderes darstellt. Zunächst ndern wir unser Hauptprogramm leicht ab und speichern die neue Datei unter dem Namen IUserTest1.java:

Objektorientierte Programmierung (OOP)

01 class IUserTest1 { 02 public static void main( String[] args ) { 03 IUser u = new IUser(); 04 u.setLogin( "hans.dampf" ); 05 u.setPwd( "verysecret" ); 06 07 System.out.println( "Objekt = " + u ); 08 09 System.exit( 0 ); 10 } 11 }

Wie wir in Zeile 07 sehen, gebe ich nun nicht mehr die einzelnen Attribute aus, sondern übergebe der Methode println() jetzt die Referenz-Variable der Instanz. Sehen wir uns an, was dabei herauskommt:

Abbildung 3.2: Modifiziertes Programm IUserTest1

Nun gibt die Methode println() den Klassennamen und die HauptSpeicheradresse der Referenz-Variablen aus. Tja, damit können wir noch nicht viel anfangen, bis auf die Erkenntnis, dass wir zumindest keinen Laufzeitfehler erhalten haben. Jetzt lassen Sie mich die Datei IUser.java ändern, indem ich eine neue Methode toString() implementiere. Den neuen Programmcode speichern wir in der Datei IUser1.java ab: // Datei IUser1.java ... // toString-Methode public String toString() { return "Instanz = \n\tLogin-Name = '" + this.getLogin() + "'\n\tKennwort = '" + this.getPwd() + "'"; }

Objektorientierte Programmierung (OOP)

ie wir beim Vergleich mit der ersten Version sehen, hat sich bis auf die neue ethode nichts geändert (nicht ganz richtig, wir müssen den Klassennamen ändern, il wir den Programmcode ja in einer anderen Datei speichern). Nun, was macht die Methode toString()? Nicht besonders viel, sie baut einen String sammen, der den aktuellen Zustand der Objekt-Attribute widerspiegelt. Bevor wir her darauf eingehen, wollen wir noch schnell das Hauptprogramm so ändern, dass nun den neuen Klassennamen verwendet, anschließend übersetzen wir das Ganze d lassen das Programm noch einmal laufen. Zunächst die Änderung des Hauptproamms, die wir in der Datei IUserTest2.java abspeichern: 01 class IUserTest2 { 02 public static void main( String[] args ) { 03 IUser1 u = new IUser1(); 04 u.setLogin( "hans.dampf" ); 05 u.setPwd( "verysecret" ); 06 07 System.out.println( "Objekt = " + u ); 08 09 System.exit( 0 ); 10 } 11 }

s auf den neuen Namen der Klasse in Zeile 03 (Vorsicht, man muss hier an zwei ellen ändern) ist das Hauptprogramm gleich geblieben. Und hier das Ergebnis:

Abbildung 3.3: IUser mit toString()-Methode

as passiert hier? Es sieht ganz so aus, aus würde die Virtual Machine jetzt die neu nzugekommene Methode toString() aufrufen, wenn im Hauptprogramm die Zeile 07

System.out.println( "Objekt = " + u );

Objektorientierte Programmierung (OOP)

abgearbeitet wird. Der Hintergrund ist folgender: Durch die String-Verkettung mit dem Operator + wandelt die Virtual Machine unser Objekt, dessen Adresse in der ReferenzVariablen u abgelegt ist, implizit in einen String um. Technisch ist das in Java so gelöst, dass sie die Methode toString() der Instanz aufruft, dessen return-Wert dann an den zweiten Operanden des Operators angehängt wird, der links vom Operator steht. Wir sehen also, dass der Methodenname toString irgendwie in Java vordefiniert ist. Sie sollten sich also merken: »Implementiere in jeder selbst geschriebenen Klasse immer eine toString()-Methode!«. Sie kann im Übrigen auch hervorragend für Debug-Ausgaben während der Test-Phase von Programmen benutzt werden. 3.1.7

Zusammenfassung

Wir haben jetzt einige grundlegende Dinge von Klassen kennen gelernt. Da ich Sie nicht bereits im ersten Abschnitt verzweifeln lassen möchte, habe ich hier aber nur an der Oberfläche gekratzt und Basis-Wissen vermittelt, das Sie brauchen, um anhand einfacher Beispiele das Klassen-Konzept von OOP zu verstehen. Wir werden das Thema Klassen weiter unten noch einmal vertiefen. Bevor ich nun auf die weiteren Konzepte von OOP eingehe, möchte ich das soeben Gelernte noch einmal kurz zusammenfassen: 

Klassen sind der Grundstein für jedes Java-Programm. Sie enthalten Variablen, in denen die Attribute der Klassen-Objekte, auch Felder genannt, gespeichert sind. Aktionen der Objekte werden in Funktionen implementiert, die man bei OOP als Methoden bezeichnet.



Man unterscheidet mehrere Arten von Methoden. Als Erstes wären da die so genannten Accessor- und Mutator-Methoden (auch Getter bzw. Setter genannt). Sie dienen dazu, die Werte der Variablen für die Attribute zu lesen (Accessor) bzw. mit einem neuen Wert zu belegen (Mutator). Eine weitere spezielle Art von InstanzMethode ist toString(), die den aktuellen Zustand aller Attribute als String zurückliefert. Neben dem Konstruktor, den wir gesondert behandeln werden, gibt es weitere Methoden, die bestimmte Aktionen für die Instanz durchführen, wie zum Beispiel die Daten der Instanz in einer Datenbank zu speichern.



Jede Methode hat eine Signatur, die sich aus dem Methodennamen und der Parameter-Liste ergibt und die Methode so eindeutig identifiziert.



Es gibt sowohl Instanz-Attribute und -Methoden, die im Kontext von instanzierten Objekten einer Klasse definiert sind (und damit immer über eine ObjektReferenz aufgerufen werden müssen), als auch statische Attribute und Methoden, die keine Zuordnung zu einer Instanz besitzen, sondern im Kontext einer Klasse definiert sind. Solche Attribute bzw. Methoden sind also so lange gültig, wie die

Objektorientierte Programmierung (OOP)

Klasse geladen ist, in der sie definiert wurden. Instanz-Attribute und -Methoden hingegen sind nur verfügbar, nachdem ein Objekt instanziert wurde, und verlieren ihre Gültigkeit, wenn die Instanz über den so genannten Garbage Collector (das ist die Müllabfuhr für tote Objekte) aus dem System verschwindet. Konstruktoren erledigen die Arbeit der Speicher-Reservierung und der Initialisierung bei der Instanzierung neuer Objekte. Man kann beliebig viele Konstruktoren implementieren. Der Default-Konstruktor ohne Parameter nimmt eine Sonderstellung ein, da er implizit immer aufgerufen wird, wenn kein Konstruktor implementiert worden ist. In Java gibt es keinen Destruktor.

2

OOP-Konzepte

Nachdem Sie nun schon eine Menge Grundlagen über Klassen kennen gelernt haben, öchte ich Ihnen die wichtigsten Konzepte von OOP näher bringen. s die Programmiersprache C ihren Siegeszug in der Computerwelt hielt, kannte man nur den Begriff der prozeduralen Programmierung. Für die Datenverarbeitung hrieb man einen Satz von Funktionen, von denen jede einzelne irgendetwas mit n Daten tat. Die Daten selbst waren nichts anderes als eine Struktur, die man beim ufruf der Funktionen als Argument übergab. er wird bereits ein wichtiger Nachteil der prozeduralen Programmierung erkennr. Änderte man die Datenstruktur, dann zog dies meist einen Rattenschwanz an npassungen in allen Funktionen nach sich, weil der Programmcode jeder einzelnen nktion die interne Datenstruktur kennen muss, um sie verarbeiten zu können. enn man nun einen weiteren Datensatz zu verarbeiten hatte, dessen Struktur ähnch, aber nicht gleich der ersten war, dann musste man neue Funktionen schreiben, die mit der ähnlichen Struktur umgehen konnten. Man duplizierte also bereits eschriebenen Code und änderte diesen anschließend so ab, dass er auf die neue Datenstruktur abgestimmt war. er objektorientierte Ansatz geht einen völlig neuen Weg, indem nicht die Funktion, sondern die Daten selbst im Mittelpunkt des Interesses stehen. Sie sind nun keine einfache Struktur mehr, die man als Argument einer Funktion übergibt, sondern bende Objekte, die bestimmte Eigenschaften besitzen und verschiedene Methoden bieten, mit denen sie sich sozusagen selbst verarbeiten.

Objektorientierte Programmierung (OOP)

Das Gedankengebäude der objektorientierten Programmierung besteht im Wesentlichen aus vier Grundmauern, in denen sich das Konzept von OOP wiederfindet: 3.2.1

Abstraktion

Will man ein konkretes Problem mit Hilfe objektorientierter Programmierung lösen, dann muss man in einem ersten Schritt zunächst alle beteiligten Komponenten abstrahieren, indem ihre Eigenschaften und mögliche Aktionen definiert werden. Ich möchte Ihnen diesen Prozess anhand eines Beispiels näher bringen: Wir haben einen Bausatz für ein Spielzeug-Auto gekauft. Zu Hause angekommen können wir natürlich nicht anders als sofort das Paket zu öffnen. Was finden wir im Karton? Na ja, zunächst einmal eine Unmenge an Plastiktüten, in denen sich die Einzelteile befinden. Viel wichtiger für die meisten wird aber das Papier sein, auf dem das Wort Bauanleitung steht. Bei der Vielzahl an Einzelteilen wären wir sonst vermutlich hoffnungslos überfordert. Doch dank des Bauplans haben wir das Unmögliche möglich gemacht, und schon nach einer Stunde steht ein feuerroter Ferrari mit batteriebetriebenem Motor und Fernsteuerung vor uns (Na ja, ein paar kleine Schrauben sind wie gewöhnlich übrig geblieben, aber wenn man an das Weltraum-Teleskop Hubble denkt, befinden wir uns damit in bester Gesellschaft). Und zur allgemeinen Überraschung der Familie funktioniert die Fernsteuerung auch. Sogar das Schalten in den zweiten Gang klappt, und unser Ferrari braust durchs Wohnzimmer! Warum erzähle ich Ihnen eine Geschichte über Kinder-Spielzeug, wo es doch eigentlich um die epochale Erfindung OOP geht? Nun, das Beispiel eignet sich sehr gut, das komplizierte und eher trockene Thema ein bisschen anschaulicher zu gestalten. Der Ferrari in unserem Wohnzimmer ist nichts anderes als ein individuelles Objekt einer bestimmten Klasse von Spielzeug-Autos. Die Bauanleitung ist gleichbedeutend mit der Klasse (man könnte auch Art oder Gattung dazu sagen). Eine Klasse beschreibt die Eigenschaften (Attribute) und das Verhalten (Methoden) der einzelnen Klassen-Objekte, die man auch als Instanzen der Klasse bezeichnet. Im Bauplan stehen zunächst nur Anweisungen für den Zusammenbau eines individuellen Objekts, die für alle Instanzen gleich sind, weil sie ja alle zur selben Klasse gehören. Aber dadurch, dass es die Autos in verschiedenen Farben gibt (zugegeben, Ferrari macht da vielleicht eine Ausnahme) oder Sie das Nummernschild des Autos selbst mit Filzstift beschriften, wird durch den Zusammenbauer (das ist der deutsche Ausdruck für Konstruktor) eine ganz individuelle Instanz der Klasse ins Leben gerufen.

Objektorientierte Programmierung (OOP)

Der Schritt der Abstraktion besteht also daraus, die Eigenschaften und das Verhalten n Objekten so zu verallgemeinern, dass daraus ein Bauplan in Form einer Klasse tsteht, der für alle gleichartigen Objekte gilt. 2.2

Kapselung

n wesentlicher Beitrag für bessere Wartbarkeit von objektorientiert implementiern Programmen ist die Kapselung von Attributen und Methoden. Für Außenstende, die eine Klasse und Instanzen dieser Klasse benutzen, steht in der Regel ein ohl definierter Satz von Methoden zur Verfügung, mit deren Hilfe Eigenschaften eines Objekts geändert oder bestimmte Aktionen gestartet werden können. Diese von ßen nutzbaren Methoden werden in einer API-Dokumentation genau beschrieben PI steht für Application Programming Interface). ie die Methoden intern arbeiten, ist nach außen unsichtbar und wird in den Methoden der Klasse gekapselt. Dies hat den Vorteil, dass man den Programmcode in einer Klasse ändern kann, ohne dass alle Programme, die unsere Klasse benutzen, enfalls angepasst werden müssen. Den Anwender einer Klasse sollte ohnehin nicht eressieren, wie eine Klassen-Methode ihre Arbeit macht, wichtig ist nur, dass sie nau das tut, was im API beschrieben ist. ir haben die Kapselung der Attribute bereits in unserem Beispiel weiter oben gesen, als wir so genannte Accessor- und Mutator-Methoden implementiert hatten, die für n lesenden bzw. schreibenden Zugriff auf die Instanz-Variablen verantwortlich sind. Das folgende Schaubild demonstriert die Kapselung: 



 





 

   

Abbildung 3.4: Kapselung von Attributen

Objektorientierte Programmierung (OOP)

In Abbildung 3.4 ist sowohl die Kapselung der beiden Attribute a und b als auch der Direktzugriff auf das Attribut c dargestellt. Während die ersten beiden Attribute sowohl für den lesenden als auch den schreibenden Zugriff Accessor- und MutatorMethoden besitzen, kann von außen direkt auf das Attribut c zugegriffen werden (dies setzt voraus, dass die Variablen a und b mit dem reservierten Wort private deklariert werden, c aber mit public). Ich möchte Ihnen nun zeigen, dass die Member-Variablen login und pwd wirklich von der Außenwelt vor direktem Zugriff geschützt und damit gekapselt sind. Schreiben wir unser letztes Testprogramm IUserTest2.java ein bisschen um und speichern den folgenden Programmcode in der Datei IUserTest3.java ab: 01 // Dieses Programm lässt sich nicht übersetzen 02 class IUserTest3 { 03 public static void main( String[] args ) { 04 IUser1 u = new IUser1(); 05 u.login = "hans.dampf"; 06 u.pwd = "verysecret"; 07 08 System.out.println( "Objekt = " + u ); 09 10 System.exit( 0 ); 11 } 12 }

Wie wir beim Vergleich der Zeilen 05 und 06 sehen, verwende ich jetzt nicht die Mutator-Methoden, sondern versuche nun, die Instanz-Variablen direkt zu beschreiben. Das Ergebnis sieht so aus:

Abbildung 3.5: Direkte Verwendung privater Instanz-Variablen

Bereits der Compiler verweigert seinen Dienst. Woran das liegt, werden wir im Einzelnen später sehen. Die Kapselung der Instanz-Variablen ist also vollständig, weil es zum einen von außen unmöglich ist, direkt auf die Variablen zuzugreifen, zum ande-

Objektorientierte Programmierung (OOP)

en die Regeln beim lesenden oder schreibenden Zugriff durch die Getter- oder Setr-Methoden über das API der Methoden genau definiert sind. Die internen Verarbeitungsschritte jedoch sind dem Benutzer der Klasse verborgen. könnte man zum Beispiel in setPwd() noch eine Längen-Prüfung einbauen, um zu währleisten, dass das Kennwort mindestens aus 6 Zeichen besteht. Das API würde h dadurch aber nicht ändern (na ja, fast nicht, denn man müsste zumindest den Typ r Methode ändern oder auf andere Weise dem Benutzer der Methode mitteilen, ob die Operation erfolgreich war oder misslungen ist. Wie das funktioniert, werden wir ahren, wenn ich Exceptions bespreche). 2.3

Vererbung

n sehr wichtiges Konzept in OOP ist die Möglichkeit der Vererbung (englisch: heritance), mit deren Hilfe man bereits erstellten Programmcode wieder verwenden d sich damit eine Menge Schreibarbeit sparen kann. Damit Sie den Begriff Vererbung verstehen lernen, müssen Sie zunächst folgende age beantworten können: »Was haben ein VW Polo und ein Porsche 911 gemeinsam?«. Ich hätte alternativ die Frage: »Wie bringt man ein Fahrrad und einen 40-Tonr unter einen Hut?« Nun, eine der möglichen Antworten auf die erste Frage wäre »Beides sind Autos«. e zweite Frage könnte man mit »Sowohl das Fahrrad als auch der LKW ist ein ansportmittel« beantworten. h glaube, Sie sehen schon, worauf dieses Frage- und Antwort-Spiel hinausläuft. Wir chen nach Überbegriffen bzw. nach einer Klassifizierung mehrerer unterschiedlier Unterklassen, die bestimmte Merkmale gemeinsam haben. Dieser Vorgang der Zusammenfassung kann mehrstufig sein, und am Ende landen wir immer bei einem hierarchischen Baum, ähnlich wie beim Abstammungsbaum der Menschen. kann man zum Beispiel den VW und den Porsche zusammenfassend als PKW zeichnen, den LKW und den PKW wiederum als KFZ, KFZ und Fahrrad zum Beiiel als Landfahrzeug, Schiffe, Flugzeuge und Landfahrzeuge als Transportmittel. Je ch Feinheit der Unterteilung (Klassifizierung) ergeben sich mehr oder weniger ele Überklassen in der hierarchischen Baum-Struktur:

Objektorientierte Programmierung (OOP)

 

 

 

  

 







  

 



Abbildung 3.6: Hierarchische Baum-Struktur von Transportmitteln

Wenn man sich das Bild der Klassen-Hierarchie genauer ansieht, fällt auf, dass mit jeder Ebene in der Hierarchie die Spezialisierung zunimmt. Während die oberste Klasse Transportmittel noch sehr allgemein ist, unterscheidet man in der zweiten Ebene zwischen Land- und Luftfahrzeugen (der Einfachheit halber habe ich im Bild die Wasserfahrzeuge weggelassen). In der dritten Ebene erfolgt eine weitere Differenzierung usw. Das kann man so weit treiben, bis man zum Beispiel bei einem VW Polo angekommen ist. Manch einer wird den Klassen-Baum vielleicht anders strukturieren als ich in meinem Beispiel. Bei der Vererbung gilt der Satz »Viele Wege führen nach Rom«. Grundsätzlich sollte man den Vererbungsbaum so aufbauen, dass er zum einen die tatsächlichen Verhältnisse möglichst exakt wiedergibt und auf der anderen Seite den Gesichtspunkt der Wieder-Verwendbarkeit von Klassen optimal ausnutzt. Wenn man den Klassen-Baum von unten nach oben betrachtet, dann wird deutlich, dass jede Unterklasse (auch Kind-Klasse genannt) die Eigenschaften (Attribute) und das Verhalten (Methoden) ihrer jeweiligen Überklasse (auch Eltern-Klasse genannt) erbt. So wird zum Beispiel die Eigenschaft eines KFZ, einen Motor zu besitzen, an die darunter liegenden Klassen LKW, PKW und KRAD weitervererbt, denn alle sind motorisiert. Von der Klasse Straßenfahrzeuge erben sie die Eigenschaft, (in der Regel) auf Straßen zu fahren, von Landfahrzeuge, dass sie sich zu Lande bewegen (auch hier gibt es Ausnahmen), außerdem sind sie Transportmittel.

Objektorientierte Programmierung (OOP)

Durch geschicktes Anordnen der Klassen im Hierarchie-Baum kann man sehr viele Methoden in den Eltern-Klassen implementieren (hier zum Beispiel in KFZ). Alle nd-Klassen erben dann automatisch die Funktionalitäten und man muss nur die ezifischen Merkmale der Kind-Klasse programmieren. h werde mich im zweiten Abschnitt über Klassen noch tief schürfend mit Vererbung schäftigen, hier möchte ich kurz das Prinzip veranschaulichen, wie man in Java Unterklassen bildet: 01 02 03 04 05 06 07 08 09 10 11 01 02 03 04 05 06 07 08 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

// Datei Craft.java // oberste Klasse für Transportmittel class Craft { // max. Ladekapazität in kg int maxCharge = 200; // Getter-Methode für die Ladekapazität int getMaxCharge() { return maxCharge; } } // Datei Aircraft.java // Klasse Aircraft, die aus Craft abgeleitet wird // und Attribute sowie Methoden von Craft erbt. class Aircraft extends Craft { public Aircraft() { maxCharge = 5000; } } // Datei CraftTest.java // Hauptprogramm class CraftTest { public static void main( String[] args ) { // Instanzierung eines Objekts der Klasse // Craft Craft cr = new Craft(); System.out.println( "maximale Kapazität von Craft = " + cr.getMaxCharge() ); // Instanzierung eines Objekts der Klasse // Aircraft Aircraft ac = new Aircraft();

Objektorientierte Programmierung (OOP)

16 17 18 19 20 21 }

System.out.println( "maximale Kapazität von Aircraft = " + ac.getMaxCharge() ); }

Übersetzen und Ausführen des Hauptprogramms liefert (die beiden benutzten Klassen Craft und Aircraft werden vom Compiler automatisch übersetzt):

Abbildung 3.7: Beispiel von Klassen-Vererbung

Für alle, die sich ob der scheinbar neuen Schreibung des Wortes Kapazität nun verwundert fragen, ob sie in der Schule etwas verpasst haben, sei angemerkt, dass der Zeichensatz in einer DOS-Box ein anderer ist als in einem Windows-Programm. Wenn Sie die Ausgabe des Programms nicht am Bildschirm angezeigt, sondern in eine Datei umgelenkt hätten, dann würden Sie in einem Windows-Editor wirklich ein »ä« sehen. In Unix haben Sie das Problem nicht. Um das zu beweisen (und Ihnen zu zeigen, dass Java wirklich Plattform-unabhängig ist), kopieren wir nun die kompilierten ClassDateien auf einen Linux-Rechner:

Objektorientierte Programmierung (OOP)

nd nun lassen wir das Programm noch einmal laufen, diesmal unter Linux:

Abbildung 3.9: CraftTest unter Linux

um einen fällt angenehm auf, dass Umlaute in Linux richtig dargestellt werden. Außerdem müssen wir die Source-Dateien nicht kompilieren, sondern können in der rtual Machine direkt die Class-Dateien vom Windows-Rechner benutzen. Zurück zum eigentlichen Thema Vererbung. Ich glaube, der Source-Code der Klasse Craft ist nach den oben gezeigten Beispielen verständlich. Etwas Neues sehen wir in r Klassen-Deklaration von Aircraft.java: 04 class Aircraft extends Craft {

ie wir sehen, kann man mit dem reservierten Wort extends eine Klasse sozusagen weitern. Die Deklaration von Aircraft besagt, dass sie aus der Über-Klasse Craft geleitet wird und von dieser alle Variablen und Methoden vererbt bekommt (wir rden weiter unten sehen, dass man die Vererbung auch einschränken kann). Hinweis Java unterstützt, anders als C++, keine Mehrfach-Vererbung, das edeutet, dass in Java eine beliebige Klasse nur eine einzige Überklasse besitzen kann. Diese Einschränkung ist zwar manchmal hinderlich, kann aber mit Hilfe von Interfaces, die wir weiter unten besprechen, elegant umgangen werden. Nach dem eservierten Wort extends darf also nur eine einzige Klasse angegeben sein. Im Konstruktor der Klasse Aircraft verwenden wir die Instanz-Variable maxCharge, ne sie definiert zu haben: 05 06 07

public Aircraft() { maxCharge = 5000; }

Objektorientierte Programmierung (OOP)

Die interessante Zeile ist Zeile 06, in der wir die Variable maxCharge mit dem Wert 5000 belegen. Durch die Vererbung wird die Variable der Klasse Craft verwendet, und wir können sie in der abgeleiteten Klasse Aircraft ohne Deklaration oder Neu-Definition direkt benutzen. Dem aufmerksamen Leser wird vielleicht ins Auge gestochen sein, dass ich die Variable maxCharge nicht mit dem reservierten Wort private deklariert habe (zu dessen Bedeutung wir im zweiten Abschnitt über Klassen kommen werden). Sie können es versuchsweise mit private übersetzen, allerdings setzt es dann eine Fehlermeldung des Compilers. Wie gesagt, das Warum kommt später. Ich habe jetzt häufig die Wörter Überklasse und Unterklasse benutzt, um damit die Ebene in der Klassenhierarchie auszudrücken. Meist jedoch werden andere Begriffe verwendet, die ich im Folgenden kurz erläutern möchte: 

Base Class Mit diesem Begriff wird die oberste Klasse in der Hierarchie bezeichnet. Im Deutschen bezeichnet man sie als Basisklasse. Darunter versteht man diejenigen Klassen, die nicht explizit mit dem reservierten Wort extends deklariert werden. Wir werden noch sehen, dass jede Java-Klasse als oberste Base Class die Klasse Object besitzt, die vordefiniert ist. Und zwar auch dann, wenn es sich nicht um

eine abgeleitete Klasse handelt. 

Parent Class Parent Class ist ein Synonym für Eltern-Klasse, man bezeichnet damit diejenige Klasse, die in der Hierarchie eine Ebene (oder auch mehrere Ebenen) weiter oben angesiedelt ist. So ist in unserem Beispiel Craft die direkte Eltern-Klasse von Aircraft, Object wäre aber ebenso eine Eltern-Klasse von Aircraft.



Super Class Super Class ist ein Synonym für Parent Class.



Child Class Child Class, zu Deutsch auch Kind-Klasse, ist das Gegenteil von Parent Class und kennzeichnet eine abgeleitete Klasse, die in der Hierarchie eine Ebene (oder auch mehrere Ebenen) weiter unten angesiedelt ist. In unserem Beispiel wäre also Aircraft die direkte Kind-Klasse von Craft und eine Kind-Klasse von Object.



Derived Class Derived Class, zu Deutsch abgeleitete Klasse, ist ein Synonym für Child Class.

Objektorientierte Programmierung (OOP)

.2.4

Polymorphie

g verwoben mit der Klassen-Vererbung ist der Begriff Polymorphie (auch Polymorismus genannt). Java kann eine Variable zu verschiedenen Zeitpunkten unterschiedliche Objekte thalten, solange sichergestellt ist, dass der Variablen-Typ eine Eltern-Klasse (oder ein Interface) des aufzunehmenden Objekts ist. Diesen Aspekt von OOP nennt man lymorphie. So kann zum Beispiel eine Variable vom Typ Craft definiert werden, der man anschließend ein instanziertes Objekt der Klasse Aircraft zuweist. ber Vorsicht! Umgekehrt funktioniert die Sache nicht. Es ist also nicht möglich, eine Variable vom Typ Aircraft zu definieren und anschließend ein Objekt der Klasse Craft darin abzuspeichern. ie immer ist es das Beste, das Ganze mit etwas Praxis zu würzen. Dazu erweitern ir unsere Klasse Craft um die Methode toString(), deren Bedeutung wir ja bereits iter oben schon kennen gelernt haben: 01 // Datei Craft1.java 02 class Craft1 { 03 int maxCharge = 200; 04 05 // Getter-Methode für die Ladekapazität 06 int getMaxCharge() { 07 return maxCharge; 08 } 09 10 public String toString() { 11 String cln = "Craft1"; 12 return "Objekt " + cln + ":\n\t" + 13 "maxCharge = '" + maxCharge + "'"; 14 } 15 }

vor wir uns Gedanken über den Programmcode machen, ändern wir unser Hauptogramm so ab, dass es nun Craft1 benutzt, und sehen uns das Ergebnis an:

Objektorientierte Programmierung (OOP)

Hauptprogramm (Datei CraftTest1.java): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

// Datei CraftTest1.java // Hauptprogramm class CraftTest1 { public static void main( String[] args ) { // Instanzierung eines Objekts der Klasse // Craft1 Craft1 cr = new Craft1(); System.out.println( cr ); // Instanzierung eines Objekts der Klasse // Aircraft1 cr = new Aircraft1(); System.out.println( cr ); } }

Und hier das Ergebnis nach dem Übersetzen:

Abbildung 3.10: CraftTest1 mit toString()-Methode

Und nun zum Programmcode. Nehmen wir uns zuerst Craft1.java zur Brust: Das Einzige, was sich hier gegenüber der vorherigen Version geändert hat, ist die neue Methode toString(). Viel zu erklären gibt es da nicht, sie gibt einen String zurück, in dem der aktuelle Zustand der Instanz-Variablen und der Klassenname stehen. Wie weiter oben bereits angesprochen, wird die Funktion toString() implizit durch die Methode println() aufgerufen, im Fach-Jargon heißt das Automagic, weil die Methode auf magische Weise automatisch verwendet wird. Interessanter wird es im Hauptprogramm CraftTest1.java. Wir verwenden jetzt nämlich nicht mehr zwei verschiedene Variablen für die Objekt-Referenzen, sondern nur noch eine einzige, die vom Typ Craft1 ist. Wie wir sehen, kann man darin durchaus

Objektorientierte Programmierung (OOP)

ne Referenz auf ein Objekt der Klasse Aircraft1 abspeichern, ohne vom Compiler straft zu werden. Sie können übrigens die umgekehrte Variante selbst versuchen, indem Sie als Typ die Klasse Aircraft1 angeben. Es wird nicht funktionieren, weil ein Objekt der Klasse Craft1 nicht abwärts-kompatibel zu Aircraft1 ist, umgekehrt jedoch gehört ein Objekt der Klasse Aircraft1 durchaus auch zur Klasse Craft1. Haben Sie sich eigentlich nicht gewundert, warum die Methode toString() aus Craft1 ch dann aufgerufen wird, wenn die Variable cr eine Referenz auf Aircraft1 enthält? Dort ist die Methode nämlich gar nicht definiert. Nun, die Lösung des Rätsels heißt Polyrphie. Die Virtual Machine versucht zunächst, die Methode in der aktuellen Klasse fzurufen. Ist sie dort nicht definiert (was bei unserem Beispiel der Fall ist), dann wird e Suche in der Hierarchie des Klassen-Baums nach oben fortgesetzt, beginnend bei der ekten Eltern-Klasse des Objekts. In unserem Beispiel wird sie bereits hier fündig. Ich möchte es noch einmal betonen: Die Virtual Machine sucht zur Laufzeit des Proamms nach der Methode, nicht etwa der Compiler während der Übersetzungszeit. Das könnte er in den meisten Fällen auch gar nicht, weil zur Übersetzungszeit des ogramms oft nicht feststeht, welche Objekt-Referenz in der Variablen cr gespeiert ist. Auch hierzu ein kleines Beispiel: // Array mit 10 Elementen vom Typ Craft definieren. // zu diesem Zeitpunkt wird nur Speicherplatz für // die Array-Elemente besorgt, es werden jedoch noch // keine Instanzen erzeugt. Craft[] crs = new Craft[ 10 ]; for ( int i = 0; i < 10; i++ ) { // in den Array-Elementen mit geradzahligem Index // werden Instanzen von Craft abgelegt, // in den ungeradzahligen Elementen Instanzen // von Aircraft if ( ( i % 2 ) == 0 ) { crs[ i ] = new Craft(); } else { crs[ i ] = new Aircraft(); }

An diesem Beispiel ist deutlich zu sehen, dass erst zur Laufzeit des Programms bekannt , was für ein Objekt sich in welchem Array-Element befindet. Für die Ungeduldi-

Objektorientierte Programmierung (OOP)

gen: Arrays werde ich im Kapitel Wichtige Klassen besprechen. Hier sollten Sie jedoch wissen, dass der Ausdruck new Craft[ 10 ];

nicht etwa 10 Objekte der Klasse Craft anlegt, sondern nur 10 Elemente eines Arrays mit dem Datentyp Craft. Das ist ein gewaltiger Unterschied, denn es findet keine Instanzierung von Objekten statt, lediglich der Speicherplatz für 10 Objekt-Referenzen wird reserviert. Das dynamische Suchen nach der passenden Methode durch die Virtual Machine nennt man neudeutsch Late Binding, zu Deutsch etwa späte Zuordnung. Wenn wir uns die Ausgabe des Programms CraftTest1.java noch einmal genauer ansehen, fällt dem einen oder anderen vielleicht ein kleiner Schönheitsfehler auf. Es wird zweimal derselbe Klassenname Craft1 ausgegeben, obwohl wir im zweiten Fall eine Referenz auf eine Instanz der Klasse Aircraft1 verwenden. Das liegt ganz einfach daran, dass wir in der toString()-Methode von Craft1, die ja jedes Mal benutzt wird, einen konstanten String verwenden: 11

String cln = "Craft1";

Viel eleganter wäre es doch, könnten wir dynamisch zur Laufzeit den passenden Klassennamen irgendwoher bekommen. Na ja, blättern wir halt ein wenig im Java-API. Bereits nach spätestens einer halben Stunde sollten wir fündig werden. In der Klasse Object, deren Attribute und Methoden ja an alle weiteren Klassen automagically weiter vererbt werden, ist die Methode getClass() implementiert. Sie liefert ein Objekt der Klasse Class zurück, die wie Object zu den vordefinierten Klassen von Java gehört. In der Instanz von Class wiederum finden wir die Methode getName(), die genau das liefert, was wir suchen: Den Namen unserer eigenen Klasse. Jetzt können wir die Datei Craft1.java wie folgt ändern und in Craft2.java abspeichern (Der Kürze wegen habe ich nur die beiden geänderten Zeilen aufgeführt): 02 class Craft2 { ... 11 String cln = getClass().getName();

Objektorientierte Programmierung (OOP)

Zusätzlich müssen wir natürlich die Kind-Klasse Aircraft1 und unser Hauptprogramm passen, ich habe die Änderungen in Aircraft2.java und CraftTest2.java gespeichert: 01 02 03 04 05 06 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

// Datei Aircraft2.java class Aircraft2 extends Craft2 { public Aircraft2() { maxCharge = 5000; } } // Datei CraftTest2.java // Hauptprogramm class CraftTest2 { public static void main( String[] args ) { // Instanzierung eines Objekts der Klasse // Craft2 Craft2 cr = new Craft2(); System.out.println( cr ); // Instanzierung eines Objekts der Klasse // Aircraft2 cr = new Aircraft2(); System.out.println( cr ); } }

hen wir uns das Ergebnis an:

Abbildung 3.11: AircraftTest2 mit dynamischem Klassennamen

un wird der Name der Klasse für die aktuelle Instanz dynamisch zur Laufzeit ausgeeben und nicht mehr fest verdrahtet. sher haben wir nur an der Oberfläche von Polymorphie gekratzt, das soll sich nun ndern, wenn wir mit Begriffen wie Overloading und Overriding jonglieren.

Objektorientierte Programmierung (OOP)

Overloading

Unter dem Begriff Overloading (ins Deutsche übersetzt mit Überladen) versteht man die Deklaration mehrerer Methoden, die denselben Namen, aber unterschiedliche Argumente (und damit verschiedene Signaturen) haben. Die gleichnamigen Methoden können entweder in ein und derselben Klasse oder in unterschiedlichen Klassen innerhalb eines Strangs des Vererbungsbaums definiert sein, meist sind die Methoden jedoch gemeinsam in einer Klasse implementiert. Overloading ist in Java der Ersatz für variable Funktions-Parameter, die es zum Beispiel in der Programmiersprache Perl gibt. In unseren Beispiel-Klassen verwenden wir die Variable maxCharge, in der das zulässige Gesamtgewicht in der Einheit Kilogramm gespeichert ist. Ich will nun zunächst eine Mutator-Methode für dieses Attribut implementieren, mit der man den Wert der Variablen auch von außen gezielt verändern kann: public void setMaxCharge( int maxCharge ) { this.maxCharge = maxCharge; }

So weit, so gut. Nun möchte ich das API aber erweitern, so dass man das Gesamtgewicht in unterschiedlichen Einheiten angeben kann. Wir benötigen dafür also ein weiteres Argument, mit dem der Aufrufer die gewünschte Einheit mitteilt: public void setMaxCharge( int maxCharge, String unit ) { if ( unit.equals( "kg" ) ) { this.maxCharge = maxCharge; return; } if ( unit.equals( "g" ) ) { this.maxCharge = maxCharge / 1000; return; } }

In unserem kleinen Programm fehlen weitere Einheiten, ich möchte hier jedoch nur das Prinzip zeigen.

Objektorientierte Programmierung (OOP)

uch wird im Beispiel eine ungültige Angabe für die Einheit der Einfachheit halber ignoriert. Wir werden später sehen, wie man es richtig macht, wenn wir zu den ceptions kommen. orauf es ankommt, ist die Tatsache, dass wir in der Klasse Craft nun zwei Methoden t demselben Namen, aber unterschiedlichen Argumenten haben (Wer kann die age beantworten, warum ich die Methoden gerade in Craft, und nicht in einer nd-Klasse implementiert habe?). Beide Methoden dienen dem Setzen der Variablen maxCharge, jedoch mit einem unterschiedlichen Verhalten. Das ist Overloading. Overriding

Der wesentlich interessantere Aspekt von Polymorphie ist unter dem Begriff Overing bekannt, den man im Deutschen als Überschreiben bezeichnet. Im Gegensatz zu Overloading, wo nur der Methodenname identisch war, ist bei Overriding die gesamte gnatur der implementierten Methoden gleich. Die Signatur einer Methode umfasst wohl den Namen als auch die Anzahl und Typen der Argumente einer Methode. i Overriding befinden sich die einzelnen Methoden mit gleichem Namen und gleien Parametern immer in verschiedenen Klassen des Hierarchie-Baums. Ich möchte es an einem Beispiel verdeutlichen. Flugzeuge werden unter anderem nach ihrer Spannweite der Tragflächen bemessen. Dieses Merkmal gibt es bei anderen Transportmitteln nicht. Wir wollen nun dieses Attribut zur Klasse Aircraft hinzufügen (Datei Aircraft3.java) und die toString()-Methode von Craft2 in Aircraft3 erneut implemenren, damit das zusätzliche Attribut ausgegeben wird: 01 // Datei Aircraft3.java 02 class Aircraft3 extends Craft2 { 03 // Spannweite in cm 04 private int wingLength = 1000; 05 06 public Aircraft3() { 07 maxCharge = 5000; 08 } 09 10 public String toString() { 11 return super.toString() + "\n\t" + 12 "Spannweite = '" + wingLength + "' cm"; 13 } 14 }

Objektorientierte Programmierung (OOP)

Damit wir das Ganze testen können, passen wir CraftTest2.java an und speichern die Änderungen in CraftTest3.java ab: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15

// Datei CraftTest3.java // Hauptprogramm class CraftTest3 { public static void main( String[] args ) { // Instanzierung eines Objekts der Klasse // Craft2 Craft2 cr = new Craft2(); System.out.println( cr ); // Instanzierung eines Objekts der Klasse // Aircraft3 cr = new Aircraft3(); System.out.println( cr ); } }

Das Programm präsentiert sich nun wie folgt:

Abbildung 3.12: Overriding in einer Kind-Klasse

Der geänderte Programmcode ist bis auf eine einzige Zeile in Aircraft3.java klar, diese sehen wir uns näher an: 11

return super.toString() + "\n\t" +

Das Wörtchen super ist ein reserviertes Wort in Java und enthält eine Referenz auf die Instanz der direkten Eltern-Klasse des aktuellen Objekts (vergleiche hierzu auch this, in der eine Referenz auf die Instanz der aktuellen Klasse abgespeichert ist). In unserem Fall zeigt super also auf die Instanz von Craft2. In besagter Zeile 11 tun wir folglich nichts anderes, als die gleichnamige Methode toString() der Eltern-Klasse aufzurufen. Das Ergebnis kennen wir, beide Ausgaben werden aneinander gehängt.

Objektorientierte Programmierung (OOP)

elche Methode aufgerufen wird, hängt vom Objekt selbst ab und wird dynamisch r Laufzeit entschieden. Enthält die Variable cr eine Referenz auf Craft2, dann wird die toString()-Methode dieser Klasse verwendet, im Falle von Aircraft3 ist es die eichnamige Methode in der Kind-Klasse (die dann wiederum toString() aus der tern-Klasse aufruft). viel im Moment zum Thema Polymorphie. Wenden wir uns nun wieder den Klasn zu.

3

Klassen II

ir haben bisher einige Grundlagen der Klassen in OOP kennen gelernt, die für die zeigten Beispiel ausreichend waren. Um tiefer in die Materie einzusteigen, bedarf es lerdings ein wenig mehr. 3.1

Klassen-Deklaration

ir haben bisher nur die einfachste Form der Klassen-Deklaration benutzt. Meist llt man vor das reservierte Wort class noch den Modifier public: public class MyClass { ...

er Unterschied zwischen beiden Varianten ist, dass Klassen, die mit dem Modifier public deklariert sind, von allen Programmen bzw. Klassen benutzt werden können,

ährend man ohne diesen Modifier die Klasse nur dann verwenden kann, wenn der nutzende Programmcode zum selben Package gehört (Klassen ohne diesen Modir gehören zum so genannten Default-Package). Was Packages sind, haben wir in pitel 1 bereits gelernt. Hinweis Wenn eine Java-Datei mehrere Klassen enthält, darf nur eine davon mit dem Modifier public deklariert werden. 3.2

Konstruktoren

ie wir bereits wissen, dient ein Konstruktor dazu, eine neue Klassen-Instanz in rm eines Objekts ins Leben zu rufen und gegebenenfalls Attribute zu initialisieren. chnisch gesehen ist ein Konstruktor nichts anderes als eine Instanz-Methode wie

Objektorientierte Programmierung (OOP)

jede andere auch, allerdings sind Konstruktoren ähnlich wie die toString()-Methode mit einem Hauch Automagic behaftet und haben ganz spezielle Eigenschaften, die für andere Methoden nicht gelten. So werden Konstruktoren zum Beispiel nicht an Kind-Klassen vererbt. Aufgerufen wird ein Konstruktor ebenfalls nicht wie eine normale Methode, sondern mit dem reservierten Wort new und dem Namen der Klasse, gefolgt von einem Paar runder Klammern, in denen, falls vorhanden, Parameter übergeben werden können. Da Konstruktoren Methoden sind, können sie mit beliebigen Argumenten deklariert werden. Ein Konstruktor ohne Parameter ist wiederum etwas Besonderes und wird als Default-Konstruktor bezeichnet. Default-Konstruktor

Der Default-Konstruktor wird beim Instanzieren neuer Objekte immer dann aufgerufen, wenn 

kein Konstruktor in der Klasse implementiert ist.



der Aufruf des Default-Konstruktors direkt angegeben ist.



in einer Kind-Klasse ein Konstruktor mit Parametern aufgerufen wird und es in der Eltern-Klasse keinen Konstruktor mit derselben Parameter-Liste gibt.

Ich glaube, hier sind ein paar Beispiele angebracht: // Klasse TestClass // Sie enthält keinerlei Konstruktor // Der Default-Konstruktor wird automatisch // mit new aufgerufen (der Compiler setzt ihn // für uns ein). class TestClass { // Attribute int a = 5; int b = 2; public String toString() { return "TestClass:\n\t" + "a = " + a + ", b = " + b; } } // Hauptprogramm

Objektorientierte Programmierung (OOP)

... // Deklaration einer Referenz-Variablen vom Typ // TestClass. In der Deklaration ist gleichzeitig // die Instanzierung eines neuen Objekts enthalten. // Dabei wird der Default-Konstruktor aufgerufen. TestClass cl = new TestClass(); System.out.println( cl );

un dasselbe, diesmal implementieren wir explizit einen Default-Konstruktor: // Klasse TestClass mit Default-Konstruktor class TestClass { // Attribute int a = 5; int b = 2; public TestClass() { a = 10; b = 6; } public String toString() { return "TestClass:\n\t" + "a = " + a + ", b = " + b; }

Und hier ein Beispiel mit einer abgeleiteten Klasse: // Basis-Klasse class Parent { protected int a = 1; public Parent() {}

// Kind-Klasse class Child extends Parent { private int b = 2; public Child( int a, int b ) { super.a = a;

Objektorientierte Programmierung (OOP)

this.b = b; } }

In der Kind-Klasse Child haben wir einen Konstruktor mit zwei Argumenten implementiert, jedoch keinen Default-Konstruktor. Dem aufmerksamen Leser wird vermutlich in Erinnerung sein, dass man das eigentlich nicht darf. Solange man in Programmen, die unsere Klasse benutzen, keinen Default-Konstruktor verwendet, ist das kein Problem. Man sollte es sich aber angewöhnen, im Falle mehrerer Konstruktoren immer einen Default-Konstruktor zu implementieren. Achtung Wenn in einer Kind-Klasse ein Konstruktor aufgerufen wird, dann versucht der Compiler zunächst, den Konstruktor in der direkten Eltern-Klasse mit derselben Signatur aufzurufen. Existiert dieser nicht, dann wird der Default-Konstruktor der Eltern-Klasse aufgerufen. Ist aber in der Eltern-Klasse kein DefaultKonstruktor implementiert, wohl aber Konstruktoren mit Parametern, dann liefert der Compiler eine Fehlermeldung. Expliziter Konstruktor-Aufruf

Häufig werden mehrere Konstruktoren in einer Klasse implementiert, um so verschiedene Varianten der Initialisierung von Attributen zu ermöglichen: 01 // Datei ConTest.java 02 class ConTest { 03 private String title = null; 04 private int version = 1; 05 06 public ConTest( String title ) { 07 this.title = title; 08 } 09 10 public ConTest( String title, int version ) { 11 this.title = title; 12 this.version = version; 13 } 14 15 public static void main( String[] args ) { 16 ConTest t = new ConTest( "hallo" ); 17 } 18 }

Objektorientierte Programmierung (OOP)

er Programmcode im Beispiel hat den Schwachpunkt, dass Code dupliziert rkommt, und zwar in Zeile 11, wo wir das Attribut title setzen, was ja bereits im rüber liegenden Konstruktor in Zeile 07 erledigt wird. Besser ist es, wenn wir im teren Konstruktor den darüber definierten Konstruktor aufrufen und so das Duplit des Codes vermeiden: 01 // Datei ConTest.java 02 class ConTest { 03 private String title = null; 04 private int version = 1; 05 06 public ConTest( String title ) { 07 this.title = title; 08 } 09 10 public ConTest( String title, int version ) { 11 this( title ); 12 this.version = version; 13 } 14 15 public static void main( String[] args ) { 16 ConTest t = new ConTest( "hallo" ); 17 } 18 }

Durch die Änderung wird der Programmcode leichter zu warten. Achtung Wenn man Konstruktoren explizit in anderen Konstruktoren aufruft, ann muss dieser Aufruf die erste Anweisung sein, sonst meldet der Compiler inen Fehler. Würde man also zum Beispiel zuerst das Attribut version setzen und dann den Konstruktor aufrufen, hätte man ein Problem. ufruf von Konstruktoren der Eltern-Klasse

Wie bereits erwähnt, wird bei abgeleiteten Klassen automatisch derjenige Konstrukr der darüber liegenden Eltern-Klasse aufgerufen, der dieselbe Signatur hat (bzw. r Default-Konstruktor, falls in der Eltern-Klasse kein passender Konstruktor gefunn wird). Dieser Vorgang ist bei Klassen-Bäumen mit mehreren Ebenen rekursiv. an kann aber auch gezielt den Konstruktor der Eltern-Klasse aufrufen, und dies olgt, wen wundert’s, mit der speziellen Referenz super:

Objektorientierte Programmierung (OOP)

class CClass extends PClass { ... // expliziter Aufruf des übergeordneten Konstruktors public CClass( int a, int b ) { super( a ); ... } }

Auch hier muss, wie schon vorher, der Konstruktor-Aufruf die erste Anweisung sein. Es ist übrigens nicht möglich, eine Ebene der Vererbung zu überspringen: // Dieser Code funktioniert nicht class CClass extends PClass { ... public CClass( int a, int b ) { super.super( a ); ... } }

3.3.3

Statische Initialisierung

Wir haben bisher den Konstruktor für die Initialisierung von Instanzen kennen gelernt. Was aber ist mit Klassen? Auch diese können (statische) Attribute haben, die eventuell initialisiert werden müssen, wenn die Klasse geladen wird. Nun, hierfür gibt es so genannte statische Initialisierungsblöcke, das sind Programmteile, die in geschweifte Klammern gestellt und mit dem Präfix static versehen werden: // statische Initialisierung einer Klasse public class MyClass { public static String title; public static String version; static { title = "MyTitle"; }

Objektorientierte Programmierung (OOP)

static { version = "1.0"; }

rwendet werden statische Initialisierungs-Blöcke dann, wenn keine gemeinsame eklaration und Initialisierung von Attributen möglich ist, weil es sich um mehrere Anweisungen handelt. In unserem Beispiel hätten wir keine statischen Initialisiengs-Blöcke benötigt, da die Initialisierung der Attribute jeweils nur aus einer einzin Anweisung besteht. Wie wir im Beispiel sehen, dürfen beliebig viele Initialisiengs-Blöcke vorhanden sein, sie werden in der Reihenfolge abgearbeitet, wie sie im ogrammtext stehen (Ausnahme: finale Attribute, da es sich hier um Konstanten ndelt, diese werden bereits zur Übersetzungszeit initialisiert). atische Initialisierungs-Blöcke werden nur ein einziges Mal ausgeführt, wenn die asse von der Virtual Machine geladen wird. Achtung Statische Initialisierungs-Blöcke können nur für statische KlassenVariablen verwendet werden, nicht aber für Member-Variablen von Objekten, die ja immer individuellen Instanzen der Klasse und nicht der Klasse selbst zugeordnet sind. 3.4

Finale Klassen

enn Sie eine einfache Klasse implementieren, bei der absehbar ist, dass diese nicht geleitet wird und somit keine Kind-Klasse hat, können Sie in der Klassen-Deklaran vor den Klassennamen das reservierte Wort final setzen: // eine Klasse, die keine Vererbung zulässt final class NoChildrenAllowedClass { private String title = "My title"; public final void setString( String title ) { this.title = title; } public String getTitle() { return this.title; }

Objektorientierte Programmierung (OOP)

Wie wir bei der Deklaration der Methode setString() sehen, können auch Methoden als final erklärt werden. Solche Methoden können von Kind-Klassen nicht überschrieben werden. Obwohl die zweite Methode (getTitle()) nicht als final deklariert wurde, ist sie es dennoch, weil die Klasse selbst final ist. Der Compiler würde diese Methode so übersetzen, dass sie inline wird. Diesen Begriff zeige ich Ihnen am besten anhand eines Beispiels: // Hauptprogramm NoChildrenAllowedClass c = new NoChildrenAllowedClass(); String title = c.getTitle(); // Die letzte Programmzeile wird vom Compiler // optimiert, so als hätten wir geschrieben: String title = c.title;

Es erfolgt also gar kein Methoden-Aufruf mehr, sondern die eigentlich als privat deklarierte Variable title wird direkt gelesen. Damit erreichen wir eine Geschwindigkeitssteigerung des Programms. Jeder, der versucht, eine finale Klasse abzuleiten, wird eine Fehlermeldung des Compilers ernten. Der Vorteil von solchen Klassen ist, dass damit die Polymorphie abgeschaltet wird, es muss also nicht mehr zur Laufzeit entschieden werden, welche Methode aufgerufen werden soll. Der Compiler kann in diesem Fall Code erzeugen, der wesentlich performanter ist, manche Methoden können sogar zu inline-Methoden werden (siehe obiges Beispiel), finale Klassen erhöhen also die Geschwindigkeit von Java-Programmen. Ein typischer Vertreter für eine finale Klasse ist die String-Klasse von Java. Hinweis Wird eine Klasse als final deklariert, dann sind automatisch alle darin enthaltenen Methoden ebenfalls final (und können damit nicht überschrieben werden). 3.3.5

Abstrakte Klassen

Im Gegensatz zu finalen Klassen, wo man keine Vererbung zulässt, will man bei abstrakten Klassen genau diese Vererbung erzwingen, indem vor den Klassennamen das reservierte Wort abstract geschrieben wird. Der Compiler verhindert daraufhin mit einer entsprechenden Fehlermeldung, dass Objekte der abstrakten Klasse instanziert werden:

Objektorientierte Programmierung (OOP)

// Deklaration einer abstrakten Klasse abstract class AClass { private String title = "A title"; public abstract String getTitle(); public boolean hasTitle() { return ( title.length() == 0 ) ? false : true; }

// Hauptprogramm // Die nächste Programmzeile liefert eine Fehlermeldung // des Compilers AClass cl = new AClass();

rwendet werden abstrakte Klassen dann, wenn man mehrere ähnliche Klassen plementiert hat und bei einer Design-Prüfung feststellt, dass es eine Reihe von ethoden gibt, die denselben Namen tragen und gleiche Parameter besitzen, in den einzelnen Klassen aber unterschiedlich implementiert sind. solchen Fällen macht es Sinn, die Gemeinsamkeiten zu extrahieren, eine abstrakte tern-Klasse zu bilden und dort diese Methoden zu deklarieren, ohne sie gleich zu plementieren (siehe getTitle() im Beispiel). Mit diesem Verfahren wird das Klasn-Design klarer und der Grundgedanke von OOP umgesetzt. Abstrakte Klassen versprechen sozusagen Funktionalitäten, die dann in konkreten nd-Klassen (englisch concrete classes) umgesetzt (implementiert) werden müssen. ie man an der zweiten Methode hasTitle() im Beispiel sieht, kann man normale d abstrakte Methoden durchaus mischen, es dürfen in einer abstrakten Klasse also ch vollständig implementierte Methoden vorhanden sein. Hinweis Bei finalen Klassen werden alle darin enthaltenen Methoden automatisch ebenfalls final. Abstrakte Klassen verhalten sich umgekehrt: Wird mindestens ine der darin enthaltenen Methoden als abstrakt deklariert, dann ist die Klasse selbst automatisch ebenfalls abstrakt und muss auch als solche mit dem reservierten Wort abstract deklariert werden.

Objektorientierte Programmierung (OOP)

3.3.6

Finale Attribute

Es ist auch möglich, Member-Variablen von Objekten bzw. statische Klassen-Variablen als final zu deklarieren. In diesem Fall dürfen solche Variablen nur ein einziges Mal initialisiert und anschließend nicht mehr verändert werden. Finale Attribute sind also im Prinzip Konstanten. Hierzu ein Beispiel: class MyClass { // Deklaration und Initialisierung erfolgen in // einer Anweisung public static final String version = "1.0"; // Deklaration und Initialisierung sind getrennt public final String title; public MyClass() { this.title = "MyClass"; } }

3.3.7

Modifier

In den bisher gezeigten Beispielen habe ich des Öfteren die reservierten Wörter public oder private verwendet, ohne näher darauf einzugehen. Das will ich nun nachholen. Modifier werden verwendet, um den Scope sowie die Zugriffsrechte von Attributen oder Methoden zu beeinflussen. Den Begriff Scope kann man in diesem Zusammenhang mit Sichtbarkeit übersetzen. Man kann sowohl bei Klassen- als auch bei Variablen- und Methoden-Deklarationen Modifier verwenden (obwohl für Klassen meist nur der Modifier public benutzt wird). Ist kein Modifier angegeben, dann hat das deklarierte Element Package-Scope. Was wir uns darunter vorzustellen haben, erfahren Sie im nächsten Abschnitt. Modifier stehen immer am Beginn einer Deklaration. Hier ein paar Anschauungsbeispiele: // Modifier bei Klassen public class PClass { ... } public final class FClass { ... }

Objektorientierte Programmierung (OOP)

public abstract class AClass { ... } // Modifier bei Methoden public PClass() { ... } // Konstruktor public String getTitle() { ... } public final int getWidth() { ... } public abstract void getThing(); public static void main( String[] args ) { ... } // Modifier bei Variablen public int width = 10; public static String title = "MyTitle"; public final String version = "1.0";

hen wir uns nun die noch nicht besprochenen Modifier im Einzelnen an: public

eser Modifier wird immer dann angegeben, wenn die Klasse bzw. die Methode der das Attribut globalen Scope haben soll und somit auch Programmcode sichtbar der nicht in derselben Klasse steht. Speziell bei Attributen bedeutet dies, dass die gehörige Variable von jedem Programmcode sowohl direkt gelesen als auch geänrt werden kann, deshalb sollte man diesen Modifier bei Attributen recht sparsam rwenden, weil sonst der Grundgedanke der Kapselung von OOP außer Kraft setzt wird. rwendet man den Modifier bei Klassen, dann ist die Klasse nicht nur vom Proammcode innerhalb desselben Packages (dazu kommen wir noch) sichtbar, sondern erall. Hinweis Innerhalb einer Java-Datei darf nur eine einzige Klasse den Modifier public besitzen. protected

eser Modifier wird hauptsächlich für Methoden und Attribute in Eltern-Klassen rwendet. Damit sind die so deklarierten Elemente nur innerhalb der definierenden asse und allen daraus abgeleiteten Kind-Klassen sichtbar. Bei Attributen bedeutet dies, dass alle abgeleiteten Kind-Klassen diese sowohl lesen als auch verändern dürfen. Auch in Klassen, die innerhalb derselben Java-Datei stehen wie die Klasse, in welcher s protected-Element steht, können darauf zugreifen.

Objektorientierte Programmierung (OOP)

private

Dieser Modifier wird wohl am häufigsten für Instanz-Attribute verwendet, um diese vor Zugriff zu schützen. Solche Elemente sind nur innerhalb der definierenden Klasse sichtbar (und in den Klassen, die in derselben Java-Datei stehen). Achtung Instanz-Attribute, die mit dem Modifier private deklariert sind, können auch von Methoden einer anderen Instanz gelesen und geändert werden, vorausgesetzt, es handelt sich dabei um Objekte derselben Klasse. Ich glaube, das zuletzt Gesagte bedarf eines kleinen Beispiels: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 01 02 03 04 05 06 07 08 09

// Datei AClass.java class AClass { private int a = 1; public AClass( int a ) { this.a = a; } public void setA( AClass cl, int a ) { cl.a = a; } public String toString() { return "" + a; } } // Datei ProtectedClassTest.java class ProtectedClassTest { public static void main( String[] args ) { AClass cl1 = new AClass( 5 ); AClass cl2 = new AClass( 3 ); cl2.setA( cl1, 10 ); System.out.println( "a = " + cl1 ); } }

In Zeile 03 der Klasse AClass definieren wir ein privates Instanz-Attribut, es kann also nur von Methoden innerhalb derselben Klasse verändert werden.

Objektorientierte Programmierung (OOP)

Zeile 09 ist die Methode setA() implementiert, die als erstes Argument eine stanz-Referenz auf ein Objekt derselben Klasse übergeben bekommt und über diese Objekt-Referenz den Wert der privaten Variable a dieses Objekts ändert. Wenn ir das Programm übersetzen und ausführen, erhalten wir keine Fehlermeldung, sonrn die Ausgabe a = 10

s beweist, dass der Wert des privaten Attributs geändert wurde. Hinweis In Zeile 14 der Class-Datei habe ich nicht den Wert der Variablen a direkt zurückgegeben, sondern eine String-Verkettung über den Operator + mit inem leeren String. Der Grund ist, dass die Variable nicht vom Typ String, sondern int ist. Der return-Wert der Methode muss aber ein String sein. Nun kann man entweder den Variablen-Wert explizit in eine Zeichenkette umwandeln oder ie String-Verkettung verwenden, bei welcher der Compiler alle Nicht-StringWerte automatisch in Strings umwandelt. transient

Mit dem Schlüsselwort transient, das vor einem der Modifier public, private oder protected steht, kennzeichnet man eine Variable, die bei einer Objekt-Serialisierung nicht berücksichtigt werden soll. Im Kapitel Java-Beans sehen Sie hierzu ein Beispiel. latile

as Schlüsselwort volatile teilt dem Compiler mit, dass er für die so deklarierte Varie keine Optimierungen durchführen soll. Es steht vor einem der Modifier public, private oder protected. 3.8

Initialisierung

Zum Abschluss möchte ich die Reihenfolge erläutern, in welcher die Initialisierung n Objekten abläuft: Falls die Klasse noch nicht geladen wurde, wird dies getan, dabei werden alle statischen Initialisierungs-Blöcke in der Reihenfolge ausgeführt, wie sie im Programmcode stehen.

Objektorientierte Programmierung (OOP)

2.

Im aktiven Konstruktor werden als Erstes automatisch alle passenden Konstruktoren der Eltern-Klassen der Reihe nach aufgerufen (von unten nach oben im Vererbungsbaum).

3.

Alle Member-Variablen (Instanz-Variablen) der einzelnen Klassen werden nach Durchlaufen des jeweiligen Konstruktors initialisiert.

4.

Der Programmcode im Konstruktor des zu instanzierenden Objekts wird ausgeführt.

3.4

Interfaces

Kommen wir nun zu einem etwas schwer verdaulichen Thema, den Interfaces. Als Erstes möchte ich Ihnen erklären, warum Interfaces überhaupt benötigt werden. Nehmen wir an, Sie implementieren Programmcode für ein Applet. Dafür steht eine Klasse zur Verfügung, die Sie ableiten, d.h. Sie schreiben eine Kind-Klasse von Applet (oder JApplet). So weit, so gut. Nun sollen die Objekte der Klasse aber mehrere Dinge gleichzeitig tun, das macht man in der Regel mit Threads (zu denen wir weiter unten noch ausgiebig kommen werden). Auch dafür gibt es eine vorgefertigte Klasse Thread, deren Grundgerüst man in Kind-Klassen erweitern kann. Schon aber haben wir ein Problem, denn unsere Kind-Klasse besitzt bereits eine Eltern-Klasse, nämlich Applet. Und da es in Java keine Mehrfach-Vererbung gibt, können wir nicht noch eine weitere Eltern-Klasse hinzufügen. Gott sei Dank haben die Designer von Java daran gedacht und das Problem in Form von Interfaces elegant gelöst: Interfaces ähneln in gewisser Weise abstrakten Klassen, nur besitzen Interfaces keinen Konstruktor. Alle darin enthaltenen Attribute und Methoden sind automatisch abstract und public. Deklariert werden Interfaces mit dem reservierten Wort interface. Der zweite Grund, warum Interfaces benötigt werden, liegt in der Verallgemeinerung. Sie bieten nämlich die Möglichkeit, eine gemeinsame Schnittstelle (API) für unterschiedliche Klassen zu definieren. Dadurch werden alle Klassen, welche ein gemeinsames Interface implementieren, zueinander kompatibel. Am besten, ich erkläre die Sache anhand eines Beispiels. Alle Ebenen (also Gebilde mit zweidimensionaler räumlicher Ausdehnung) haben eine bestimmte Grundfläche, die sich bei rechteckigen Flächen immer mit derselben Formel berechnen lässt (Länge x Breite).

Objektorientierte Programmierung (OOP)

zt nehmen wir einmal zwei konkrete Ebenen, und zwar einen Tisch und einen rkplatz. Diese beiden Flächen haben nichts miteinander zu tun, und wenn wir uns ide als Klassen denken, ist es auch nicht möglich, einen Vererbungsbaum für die iden Klassen zu erstellen. nd trotzdem haben sie eines gemeinsam: die Grundfläche. Jetzt könnte man auf die erwegene Idee kommen und die Grundfläche als Eltern-Klasse sowohl von Tisch als ch von Parkplatz zu definieren. Zwar haben beide die Eigenschaften einer Fläche, nnoch gehört ein Tisch nicht zur Gattung Fläche, weil er dreidimensional ist. ie gesagt, die Lösung des Problems ist ein Interface, das wir nun beispielhaft implemeneren wollen (wobei wir der Einfachheit halber als feste Maß-Einheit mm annehmen): 01 // Datei Area.java 02 public interface Area { 03 // Deklaration einer Methode, welche die 04 // Grundfläche in Quadratmeter zurückgibt 05 public double qm(); 06 }

Das Interface ist sehr einfach, es wird nur eine einzige Methode deklariert. Wohl merkt: Die Methode wird nur deklariert, nicht aber implementiert, das Interface rspricht lediglich, dass alle Klassen, die das Interface benutzen, alle darin deklarieren Methoden auch wirklich implementieren. vor wir nun die Beispiel-Klassen programmieren, müssen wir noch wissen, wie man ein Interface in einer Klasse benutzt. Dafür stellt Java das reservierte Wort implements zur Verfügung, das ebenso wie extends nach dem Klassennamen folgt und eine mmaseparierte Liste von Interfaces enthält, die von der Klasse implementiert wern. Sie haben richtig gelesen: Im Gegensatz zu extends, wo nur eine Eltern-Klasse ngegeben werden darf, kann man nach implements beliebig viele Interfaces auffühn. Sehen wir uns ein paar Beispiele an: class AClass implements Comparable { ... } class BClass extends Applet implements Comparable, Runnable

Objektorientierte Programmierung (OOP)

Alle Klassen, die ein Interface implementieren, verhalten sich praktisch wie die Pseudo-Objekte, die durch das Interface definiert werden, unser Tisch verhält sich also wie eine Fläche, ebenso unser Parkplatz. Dasselbe gilt für das zweite Interface Comparable, das ebenso nur eine einzige Methode deklariert, nämlich compareTo(). Sie wird verwendet, um Objekte bezüglich ihrer Größe zu vergleichen: // Interface Comparable public interface Comparable { public int compareTo( Object o ); }

Jeder, der diese Methode implementiert, muss eine negative ganze Zahl zurückgeben, wenn das aktuelle Objekt kleiner als das durch den Parameter angegebene ist, eine positive ganze Zahl im umgekehrten Fall, und 0, wenn beide Objekte gleich groß sind. Wir wollen nun die beiden Klassen Desk und Parking programmieren: 01 // Datei Desk.java 02 class Desk implements Area, Comparable { 03 // Radius in mm; 04 private int r = 1000; 05 06 // Konstruktor mit Radius als Parameter 07 public Desk( int r ) { 08 this.r = r; 09 } 10 11 // Die vom Interface Area versprochene Methode 12 // qm() 13 // Da die interne Einheit des Radius in mm ist, 14 // muss erst in m umgerechnet werden. 15 // Für die Berechnung der Kreisfläche benutzen 16 // wir die Konstante PI aus dem Package 17 // java.lang.Math 18 public double qm() { 19 // Da sich bei der Umrechnung von mm in m 20 // Kommastellen ergeben können, müssen wir 21 // eine Gleitkomma-Variable verwenden 22 double myR = ( double ) ( this.r / 1000 ); 23 return myR * myR * Math.PI; 24 } 25

Objektorientierte Programmierung (OOP)

26 // Die vom Interface Comparable versprochene 27 // Methode compareTo() wird benutzt, um Objekte 28 // des gleichen Typs vergleichen zu können. 29 // Das sind entweder Objekte verwandter Klassen 30 // oder Objekte, welche dasselbe Interface 31 // implementieren. Wichtig ist nur, dass alle 32 // zu vergleichenden Objekte die Methode qm() 33 // zur Verfügung stellen. Die Methode vergleicht 34 // das aktuelle Objekt mit dem als Parameter 35 // angegebenen. 36 public int compareTo( Object o ) { 37 if ( o instanceof Area ) { 38 Area a = ( Area ) o; 39 if ( qm() < a.qm() ) return -1; 40 if ( qm() > a.qm() ) return 1; 41 return 0; 42 } 43 44 return 0; 45 } 46 } 01 // Datei Parking.java 02 class Parking implements Area, Comparable { 03 private int len = 5000; 04 private int width = 2000; 05 06 // Konstruktor mit Länge und Breite als Parameter 07 public Parking( int len, int width ) { 08 this.len = len; 09 this.width = width; 10 } 11 12 // Die vom Interface Area versprochene Methode 13 // qm() 14 public double qm() { 15 double l = ( double ) ( this.len / 1000 ); 16 double w = ( double ) ( this.width / 1000 ); 17 return l * w; 18 } 19 20 // Die vom Interface Comparable versprochene 21 // Methode compareTo() 22 public int compareTo( Object o ) { 23 if ( o instanceof Area ) {

Objektorientierte Programmierung (OOP)

24 25 26 27 28 29 30 31 32 }

Area a = ( Area ) o; if ( qm() < a.qm() ) return -1; if ( qm() > a.qm() ) return 1; return 0; } return 0; }

Jetzt fehlt noch ein Hauptprogramm, mit dem wir das Ganze testen können. Hierzu erstellen wir die Datei AreaTest.java. 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// // // // // //

Datei AreaTest.java Wir erzeugen in einem Array etliche Objekte, die zur Hälfte aus Tischen und zur anderen Hälfte aus Parkplätzen bestehen. Anschließend sortieren wir das Array entsprechend der Fläche der einzelnen Elemente.

// Wir verwenden als Zufallsgenerator die Klasse // java.util.Random. Diese müssen wir dazu in der // import-Direktive angeben. Der Einfachheit halber // geben wir einen Asterisk an, d.h. damit können // wir jede Klasse aus dem Package java.util // verwenden. Geladen werden aber nur die // im Programm verwendeten Klassen. import java.util.*; class AreaTest { public static void main( String[] args ) { // Anzahl der Elemente im Array int cnt = 10; // Zufallsgenerator erzeugen Random rd = new Random(); // Unser Array ist vom Typ Area, da es // verschiedene Elemente enthält. Beide // Typen sind aber kompatibel mit Area. Area[] entries = new Area[ cnt ]; for ( int i = 0; i < cnt; i++ ) { // Elemente mit geradem Index sind Tische

Objektorientierte Programmierung (OOP)

31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 }

// Element mit ungeradem Index Parkplätze if ( ( i % 2 ) == 0 ) { // Wir erzeugen einen zufälligen // Tisch-Radius und instanzieren // ein Tisch-Objekt. int r = rd.nextInt( 30 ) * 100; if ( r == 0 ) r = 1000; entries[ i ] = new Desk( r ); } else { int l = rd.nextInt( 500 ) * 1000; if ( l == 0 ) l = 10000; int w = rd.nextInt( 400 ) * 1000; if ( w == 0 ) w = 5000; entries[ i ] = new Parking( l, w ); } } // Sortieren des Arrays nach Flächengröße Arrays.sort( ( Object[] ) entries ); // Ausgeben des sortierten Arrays for ( int i = 0; i < entries.length; i++ ) { System.out.println( "entry " + i + " = " + entries[ i ].qm() ); } }

enn wir das Programm übersetzen und ausführen, erhalten wir folgende Ausgabe:

Abbildung 3.13: Interface-Beispiel AreaTest

Objektorientierte Programmierung (OOP)

Zum Sortieren der Elemente unseres Arrays habe ich die Hilfsklasse java.util.Arrays verwendet, die mehrere Sortier-Methoden zur Verfügung stellt. Eine davon erwartet als Parameter ein Array vom Typ Object und verwendet unter anderem die Methode compareTo() zum Sortieren. Bemerkenswert in Zeile 50 ist, dass unser Array, das wir der Methode sort() als Parameter übergeben, verändert wird, obwohl in Java normalerweise Methoden-Argumente als Kopie (call-by-value) und nicht als Referenz (call-by-reference) übergeben werden. In Java sind Arrays jedoch als Objekte implementiert, somit können die Elemente eines übergebenen Arrays verändert werden (nicht aber die Array-Referenz selbst). 3.4.1

Vererbung von Interfaces

Erstellt man einen Vererbungsbaum mehrerer abgeleiteter Klassen, dann werden nicht nur Methoden und Attribute, sondern auch Interfaces an die Kind-Klassen weiter vererbt. Hierzu ein kleines Beispiel, in dem wir die Klasse Parking ableiten und eine Kind-Klasse für Tiefgaragen implementieren: 01 // Datei SubParking.java 02 class SubParking extends Parking { 03 int height = 0; 04 05 public SubParking( 06 int len, int width, int height 07 ) { 08 // Aufruf des Konstruktors der Eltern-Klasse 09 super( len, width ); 10 this.height = height; 11 } 12 13 public static void main( String[] args ) { 14 SubParking p = 15 new SubParking( 100000, 50000, 3000 ); 16 System.out.println( "Flaeche = " + 17 ( ( Area ) p ).qm() 18 ); 19 } 20 }

Objektorientierte Programmierung (OOP)

ie wir sehen, kann das Interface Area in der Kind-Klasse direkt benutzt werden. Das insofern nichts Besonderes, als Methoden der Eltern-Klasse ja sowieso an Kindassen weitervererbt werden. Interessanter ist hier schon der Aspekt, dass Objekte r Kind-Klasse nun ebenso kompatibel zum Typ des Interfaces werden, wie in Zeile gezeigt ist. Hinweis Interfaces können auch als Container für Konstanten benutzt werden, der gar keine Methoden enthält, sondern nur Variablen, die als Konstanten definiert sind.

// Interface als Container für Konstanten public interface AppConst { public static final String VERSION = "1.0"; public static final String TITLE = "My App";

... // Klasse, die das Interface benutzt class MyClass implements AppConst { public static void main( String[] args ) { System.out.println( "Version = " + TITLE ); }

ätte man die Konstanten in einer eigenen Klasse untergebracht, müsste man mehr hreibaufwand betreiben, da bei jedem Variablennamen auch der Klassenname angeben werden muss (z.B. AppConst.VERSION). Allerdings sei darauf hingewiesen, dass i der abkürzenden Schreibweise mit Benutzung von Interfaces darauf geachtet wern muss, dass keine Namenskonflikte entstehen (weil z.B. der Identifier VERSION reits in einer anderen Klasse definiert wurde). 4.2

Tagging Interfaces

aben Sie sich schon die Mühe gemacht, die Datei src.jar der JDK-Distribution auspacken und ein bisschen im Source-Code der Standard-Klassen zu schmökern? Wenn nicht, möchte ich Ihnen ein nettes Interface zeigen:

Objektorientierte Programmierung (OOP)

// Datei java/lang/Cloneable.java package java.lang; public interface Cloneable {}

Toll, was? Jetzt fragen Sie sich sicherlich, was das eigentlich soll. Tja, die Frage ist berechtigt. Das Einzige, wofür das Interface gut ist, ist die Möglichkeit, dass man Objekte mit Hilfe des instanceof-Operators überprüfen kann, ob sie dieses Interface implementieren (ein Beispiel dafür haben wir bereits im vorherigen Programmbeispiel gesehen). Man nennt solche Interfaces auch Tagging Interfaces. Mit dieser Art von Interfaces wird die Tatsache ausgenutzt, dass eine Klasse, welche Interfaces implementiert, automatisch kompatibel zum Typ aller implementierten Interfaces wird, man kann einem Objekt damit also sozusagen mehrere Datentypen zuordnen. Ein Beispiel sehen wir weiter unten, wenn es darum geht, Objekte zu kopieren.

3.5

Klassen III

In diesem Abschnitt möchte ich Ihnen zeigen, wie man Objekte kopiert und was unter Inner Classes sowie Anonymous Classes zu verstehen ist. 3.5.1

Objekte kopieren

Objekte werden in Java immer in Form von Referenz-Variablen weitergereicht, es gilt also das call-by-reference-Prinzip (obwohl die Referenz-Variablen selbst als Kopie übergeben werden). Das gilt unter anderem auch für Zuweisungen, weil Java keine Überladung von Operatoren wie bei C++ kennt: 01 // Datei ObjAssign.java 02 class ObjAssign { 03 public static void main( String[] args ) { 04 // Objekt der Klasse Obj instanzieren 05 Obj o1 = new Obj( 1 ); 06 07 // Zuweisung des Objekts an eine neue 08 // Referenz-Variable 09 Obj o2 = o1; 10 11 // Ändern des Attributs von o2 12 o2.a = 2;

Objektorientierte Programmierung (OOP)

13 14 // Ausgabe beider Objekte 15 System.out.println( "o1 = " + o1 ); 16 System.out.println( "o2 = " + o2 ); 17 18 } 19 } 20 21 class Obj { 22 public int a = 0; 23 24 public Obj( int a ) { 25 this.a = a; 26 } 27 28 public String toString() { 29 return "a = " + a; 30 } 31 }

gezeigten Beispiel wird nicht etwa eine Kopie des Objekts an die Variable o2 überben, sondern eine Kopie der Referenz auf das Objekt. Das bedeutet, die Variable o2 gt nach der Zuweisung auf dasselbe Objekt wie o1. Ändern wir nun den Wert des tributs über die Variable o2, dann erfolgt diese Änderung im Original-Objekt, auf s die Variable o1 zeigt. println() gibt also für beide Variablen den Wert 2 aus. Durch Übersetzen und Ausführen des Programms lässt sich der Sachverhalt beweisen:

Abbildung 3.14: Zuweisung einer Objekt-Referenz und ihre Folgen

äufig möchte man aber eine Kopie des gesamten Objekts. Hierzu muss man die Methode clone() benutzen, die in der Klasse Object definiert ist. Allerdings sei angemerkt, dass dieses Thema seine Tücken hat (die man durch Objekt-Serialisierung umgehen kann).

Objektorientierte Programmierung (OOP)

Wenn wir die Methode clone() benutzen, die in der Klasse Object deklariert ist, können wir durch Overriding der Methode eine echte Kopie unseres Objekts erstellen (Im Programmcode wird ein noch unbekanntes Sprach-Konstrukt in Form eines try/ catch-Blocks und einer Exception verwendet, wir werden diese Begriffe im Kapitel Exceptions noch kennen lernen): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

// Datei ObjCopy.java class ObjCopy { public static void main( String[] args ) { // Objekt instanzieren Obj o1 = new Obj( 1 ); // Testen, ob die Klasse Obj Cloning // unterstützt if ( ! ( o1 instanceof Cloneable ) ) { System.err.println( "kein Clonen moeglich" ); System.exit( 1 ); } // Neues Objekt als Kopie des ersten // Objekts erzeugen Obj o2 = ( Obj ) o1.clone(); // Attribut im zweiten Objekt ändern o2.a = 2; // Ausgabe, die beweist, dass wir // tatsächlich eine echte Kopie // erhalten haben System.out.println( "o1 = " + o1.a ); System.out.println( "o2 = " + o2.a ); } } // Unsere Test-Klasse, die das Interface Cloneable // implementiert und damit bekannt macht, dass // die Methode clone() verwendet werden kann class Obj implements Cloneable { public int a = 0; public Obj( int a ) {

Objektorientierte Programmierung (OOP)

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 }

this.a = a; } // Die protected-Methode aus der Klasse Object // wird von uns als public-Methode neu // implementiert (Overriding) public Object clone() { // Die Methode clone() aus Object kann eine // Exception auslösen, die wir mit einem // try/catch-Block abfangen müssen. // Eigentlich ist das nicht nötig, weil wir // wissen, dass keine Exception auftreten // kann, da wir das Clonen ja selbst // implementieren. Es muss jedoch den Java// Regeln Genüge getan werden. try { // Aufrufen der gleichnamigen Methode // aus Object, wir erhalten eine // bitweise Kopie zurück. Das // Objekt muss über Casting in den // Typ Obj umgewandelt werden. Obj n = ( Obj ) super.clone(); return n; } catch( CloneNotSupportedException ex ) { return null; } }

esmal erhalten wir eine echte Kopie des ursprünglichen Objekts, wie die Ausgabe weist:

Abbildung 3.15: Objekte kopieren mit clone()

Ich möchte noch einmal kurz auf die Zeilen 59 und 60 eingehen:

Objektorientierte Programmierung (OOP)

59 60

Obj n = ( Obj ) super.clone(); return n;

Da die clone()-Methode von Object eine Instanz von Object zurückliefert, die Kopie aber vom Typ Obj ist, müssen wir in Zeile 59 Casting verwenden. In Zeile 60 hingegen benötigen wir kein Casting, weil die Klasse Object eine Eltern-Klasse von Obj ist. Um Ihnen nun die versteckten Tücken zu zeigen, erweitern wir die Klasse Obj um ein weiteres Attribut, das selbst wieder eine Objekt-Referenz auf eine weitere Klasse ist: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Datei ObjCopy1.java class ObjCopy1 { public static void main( String[] args ) { // Objekt instanzieren Obj o1 = new Obj( 1 ); // Testen, ob die Klasse Obj Cloning // unterstützt if ( ! ( o1 instanceof Cloneable ) ) { System.err.println( "kein Clonen moeglich" ); System.exit( 1 ); } // Neues Objekt als Kopie des ersten // Objekts erzeugen Obj o2 = ( Obj ) o1.clone(); // Attribute im zweiten Objekt ändern o2.a = 2; o2.o.s = "wohin"; // Ausgabe, die beweisen soll, dass wir // tatsächlich eine echte Kopie // erhalten haben System.out.println( "o1 = " + o1 ); System.out.println( "o2 = " + o2 ); } } // Unsere Test-Klasse, die das Interface Cloneable

Objektorientierte Programmierung (OOP)

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71

// implementiert und damit bekannt macht, dass // die Methode clone() verwendet werden kann class Obj implements Cloneable { public int a = 0; public SubObj o = null; public Obj( int a ) { this.a = a; this.o = new SubObj( "Wie und warum" ); } // Die protected-Methode aus der Klasse Object // wird von uns als public-Methode neu // implementiert (Overriding) public Object clone() { try { Obj n = ( Obj ) super.clone(); return n; } catch( CloneNotSupportedException ex ) { return null; } } public String toString() { return "a = " + a + ", o = " + o; } } class SubObj { public String s = null; public SubObj( String s ) { this.s = s; } public String toString() { return "s = " + this.s; } }

er einzige Unterschied zu vorher ist das zusätzliche Attribut mit der neuen Klasse SubObj. Wenn wir das Programm ausführen, erhalten wir jedoch nicht die gewünschte

Ausgabe:

Objektorientierte Programmierung (OOP)

Abbildung 3.16: Objekte mit Referenz-Attributen kopieren

Es sieht wohl so aus, als zeigte die Variable für das Attribut o beide Male auf dasselbe Objekt. Dies liegt daran, dass wir mit clone() eine Binär-Kopie des ursprünglichen Objekts erhalten, was bedeutet, dass die Variable o des kopierten Objekts exakt dieselbe Referenz enthält wie die des Original-Objekts. Die Lösung ist, wie manche sicher bereits vermuten, bei allen Referenz-Attributen ebenfalls die Methode clone() zu benutzen: 01 // Datei ObjCopy2.java 02 class ObjCopy2 { 03 public static void main( String[] args ) { 04 // Objekt instanzieren 05 Obj o1 = new Obj( 1 ); 06 07 // Testen, ob die Klasse Obj Cloning 08 // unterstützt 09 if ( ! ( o1 instanceof Cloneable ) ) { 10 System.err.println( 11 "kein Clonen moeglich" 12 ); 13 System.exit( 1 ); 14 } 15 16 // Neues Objekt als Kopie des ersten 17 // Objekts erzeugen 18 Obj o2 = ( Obj ) o1.clone(); 19 20 // Attribute im zweiten Objekt ändern 21 o2.a = 2; 22 o2.o.s = "wohin"; 23 24 // Ausgabe, die beweist, dass wir 25 // tatsächlich eine echte Kopie 26 // erhalten haben 27 System.out.println( "o1 = " + o1 ); 28 System.out.println( "o2 = " + o2 );

Objektorientierte Programmierung (OOP)

29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

} } // Unsere Test-Klasse, die das Interface Cloneable // implementiert und damit bekannt macht, dass // die Methode clone() verwendet werden kann class Obj implements Cloneable { public int a = 0; public SubObj o = null; public Obj( int a ) { this.a = a; this.o = new SubObj( "Wie und warum" ); } // Die protected-Methode aus der Klasse Object // wird von uns als public-Methode neu // implementiert (Overriding) public Object clone() { try { Obj n = ( Obj ) super.clone(); n.o = ( SubObj ) this.o.clone(); return n; } catch( CloneNotSupportedException ex ) { return null; } } public String toString() { return "a = " + a + ", o = " + o; } } class SubObj implements Cloneable { public String s = null; public SubObj( String s ) { this.s = s; } public Object clone() { try { SubObj n = ( SubObj ) super.clone(); return n;

Objektorientierte Programmierung (OOP)

73 74 75 76 77 78 79 80 81 }

} catch( CloneNotSupportedException ex ) { return null; } } public String toString() { return "s = " + this.s; }

Nun funktioniert die Sache so, wie wir uns das vorstellen:

Abbildung 3.17: Objekte mit Referenzen richtig kopieren

3.5.2

Inner Classes

Wir haben bisher Attribute und Methoden als Bestandteile von Klassen kennen gelernt. Java bietet jedoch auch die Möglichkeit, dass man Klassen innerhalb von Klassen definiert, diese bezeichnet man als innere bzw. lokale Klassen, auf Neudeutsch heißen diese dann Inner Classes. Meist werden solche Klassen anonym benutzt, es handelt sich dann um namenlose Klassen (anonymous classes), die wir im nächsten Abschnitt kennen lernen werden. Deshalb möchte ich Ihnen nur kurz die Highlights von Inner Classes anhand eines Beispiels zeigen: 01 02 03 04 05 06 07 08 09 10 11

// Datei InnerClassTest.java // Innerhalb der Klasse wird noch einmal eine Klasse // als lokale Klasse implementiert. class InnerClassTest { private String title = null; // Konstruktor der äußeren Klasse public InnerClassTest( String title ) { this.title = title; }

Objektorientierte Programmierung (OOP)

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55

// Methode für das Erzeugen einer Instanz der // inneren Klasse. Dies ist die einzige // Möglichkeit, ein Objekt der inneren Klasse // anzulegen. public Inner getInnerInstance( String title ) { return new Inner( title ); } // Die äußere Klasse kann auf Attribute und // Methoden der inneren Klasse nur über die // Objekt-Referenz zugreifen, die vorher von // getInnerInstance() erzeugt worden ist. public void setInnerVersion( Inner obj, String version ) { obj.version = version; } public String toString() { return "toString aussen\n\t" + "title = " + this.title; } // Innere Klasse, die nur mit dem Aufruf von // getInnerInstance() der äußeren Klasse // instanziert werden kann. private class Inner { private String title = null; private String version = "1.0"; // Konstruktor der inneren Klasse // Jede Methode der inneren Klasse kann // auf alle Attribute und Methoden der // äußeren Klasse zugreifen, auch wenn diese // als privat deklariert sind. private Inner( String title ) { this.title = title; InnerClassTest.this.title = "Outer"; } public String toString() { return "toString innen\n\t" + InnerClassTest.this +

Objektorientierte Programmierung (OOP)

56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 }

"\n\ttitle = " + this.title + "\n\tversion = " + this.version; } } public static void main( String[] args ) { InnerClassTest outer = new InnerClassTest( "Aussenklasse" ); System.out.println( outer ); Inner inner = outer.getInnerInstance( "Innenklasse" ); outer.setInnerVersion( inner, "2.0" ); System.out.println( inner ); }

Hinweis Die gesamte Unterstützung von Inner Classes erfolgt durch den Compiler, die Virtual Machine hingegen hat von Inner Classes keinerlei Ahnung. Wenn Sie das Programm übersetzen, stellen Sie fest, dass der Compiler nicht nur eine Class-Datei, sondern gleich mehrere erzeugt. Neben der üblichen Class-Datei entsteht für jede innere Klasse eine Class-Datei, deren Name sich aus dem Namen der äußeren und der inneren Klasse zusammensetzt, zwischen beiden steht ein DollarZeichen. Zusätzlich wird für jede innere Klasse noch eine Class-Datei angelegt, deren Name mit dem der äußeren Klasse beginnt, gefolgt von einem Dollar-Zeichen und einer laufenden Nummer. Die zusätzlichen Class-Dateien werden benötigt, damit die Virtual Machine (in Unkenntnis innerer Klassen) alle Klassen findet. Von besonderer Bedeutung ist die Tatsache, dass alle Methoden der inneren Klasse auf alle Instanz-Variablen und Methoden zugreifen kann, selbst wenn diese privat sind. Dies gilt sogar für Identifier (Namen von Variablen, Methoden etc.), die sowohl in der inneren als auch der äußeren Klasse gleich sind, hier kann man aus der inneren Klasse auf gleichnamige Variablen der äußeren Klasse zugreifen, indem man vor den Variablennamen den Klassennamen der äußeren Klasse, gefolgt vom reservierten Wort this, stellt:

Objektorientierte Programmierung (OOP)

50

InnerClassTest.this.title = "Outer";

Hinweis Da innere Klassen wie Instanz-Elemente behandelt werden, ist die Verwendung von allen üblichen Modifiern ebenso möglich. Auch kann man wie bei normalen Klassen sowohl abstrakte als auch finale innere Klassen bilden. Sogar statische Klassen sind möglich, allerdings haben die darin definierten Methoden dann einen Zugriff auf Instanz-Elemente der äußeren Klasse. nere Klassen dürfen sogar innerhalb von Funktionen der äußeren Klasse definiert erden, man spricht dann von lokalen Klassen. Allerdings gilt hier die Einschränng, dass solche Klassen nur Zugriff auf äußere Elemente haben, die mit dem reservierten Wort final deklariert sind. 5.3

Anonyme Klassen

Man kann das Feature der inneren Klassen noch weiter treiben, bis man bei Klassen ndet, die gar keinen Namen mehr besitzen, solche Klassen werden dann als anonyme assen (englisch anonymous classes) bezeichnet. Hauptsächlich benutzt werden anome Klassen im Zusammenhang mit AWT- und Swing-Programmierung, die wir eiter unten noch besprechen werden. ei anonymen Klassen erfolgt die Instanzierung zusammen mit der Implementierung s Klassen-Rumpfs in einer Anweisung: // Syntax für eine anonyme Klasse new interface() { ... // Klassen-Rumpf

// oder (Angaben in eckigen Klammern sind optional) new class([ args ]) { ... // Klassen-Rumpf

i der ersten Variante wird eine anonyme Klasse instanziert und definiert, die ein terface implementiert (Anstelle von interface müssen Sie sich den Namen eines terfaces denken.).

Objektorientierte Programmierung (OOP)

Im zweiten Fall handelt es sich um eine anonyme Klasse, welche die angegebene Eltern-Klasse class erweitert und somit eine Kind-Klasse davon wird. args sind die Parameter, welche dem Konstruktor der Eltern-Klasse class übergeben werden. Anonyme Klassen haben eine Reihe von besonderen Eigenschaften, die ich im Folgenden kurz aufzählen möchte: 

Anonyme Klassen haben keinen Namen und können daher auch keinen Konstruktor besitzen. Die übergebenen Argumente bei der Instanzierung durch new rufen vielmehr den entsprechenden Konstruktor der Eltern-Klasse class auf.



Da anonyme Klassen immer lokal sind, weil sie innerhalb von Methoden einer äußeren Klasse definiert werden, können sie nur auf Variablen und Methoden der äußeren Klasse zugreifen, die mit dem reservierten Wort final deklariert wurden.



Man sollte anonyme Klassen nur dann verwenden, wenn der Programmcode der Klasse einige wenige Methoden enthält und überschaubar ist.



Erkennen kann man anonyme Klassen daran, dass nach der schließenden runden Klammer des new-Aufrufs kein Semikolon, sondern eine öffnende geschweifte Klammer folgt.

Im Moment möchte ich es dabei belassen, da anonyme Klassen fast ausschließlich im Zusammenhang mit Events für Grafik-basierte Programme verwendet werden. Im Kapitel Grafik-Programmierung werden Sie etliche Anschauungs-Beispiele sehen. 3.5.4

Wrapper-Klassen

In Java läuft ohne Klassen (fast) gar nichts, wie wir bisher gesehen haben. Das Wörtchen fast bezieht sich auf die im Sprachumfang eingebauten einfachen Datentypen, die man im Englischen als primitive data types bezeichnet. Dazu gehören unter anderem ganze und gebrochene Zahlen (byte, short, int, long, float, double), boolesche Werte (boolean) oder auch der Datentyp char. Beim Datentyp String jedoch wurde eine Ausnahme gemacht, hier handelt es sich um Objekte (mit ein bisschen Automagic gewürzt). Wrapper-Klassen dienen keinem anderen Zweck als dem, diese primitiven Datentypen zu Klassen zu machen, mit deren Hilfe man Objekte instanzieren kann, es wird also eine Objekt-Hülle um den einfachen Datenwert gewickelt. Vielleicht fragen Sie sich jetzt, warum das Ganze?

Objektorientierte Programmierung (OOP)

Nun, der unbedarfte Professor würde jetzt vielleicht antworten: »Damit ist Java in volGänze objektorientiert!«, aber das ist natürlich kein Grund. Vielmehr hat das anze einen praktischen Hintergrund. Angenommen, Sie wollen den Fall unterscheiden, ob bei einem numerischen Datenbank-Feld eine beliebige Zahl oder nichts (NULL) abgespeichert ist. Sie müssen das Ganze in einem primitiven Datentyp für hlen abspeichern. Was speichern Sie für NULL ab? Die Zahl 0 geht nicht, es hanlt sich ja um einen gültigen numerischen Wert. Mit Objekten haben Sie aber die öglichkeit, über den Pseudo-Wert null das Vorhandensein eines Wertes zu prüfen. Oder Sie bearbeiten ein HTML-Formular in einem Servlet. Wie unterscheiden Sie t einem primitiven Datentyp, ob ein Formularfeld vorhanden ist oder nicht? Sie wollen Daten in einer Instanz der Klasse Vector abspeichern. Die dafür vorgesene Methode erwartet eine Instanz der Klasse Object als Parameter. Mit primitiven Datentypen stehen Sie hier ganz schön im Regen. e sehen schon, es gibt eine Reihe von Gründen, sich den Luxus von Overhead bei imitiven Datentypen zu leisten. Zudem bieten die Wrapper-Klassen einen weiteren rteil: Man kann Konstanten wie zum Beispiel die größte darstellbare Zahl in den assen unterbringen. e Namen der Wrapper-Klassen kann man sich leicht merken, fast immer sind sie identisch mit den Namen der primitiven Datentypen, nur dass der erste Buchstabe oßgeschrieben wird: Primitiver Datentyp

Wrapper-Klasse

byte

Byte

short

Short

Int

Integer

long

Long

float

Float

double

Double

boolean

Boolean

char

Character

void

Void

elle 3.1: Übersicht der Wrapper-Klassen

Objektorientierte Programmierung (OOP)

Instanzierung von Wrapper-Klassen

Fast immer kann man Objekte von Wrapper-Klassen instanzieren, indem man dem Konstruktor entweder den primitiven Datenwert oder einen String als Parameter übergibt. Hier ist die vollständige Liste der Konstruktoren: Wrapper-Klasse

Konstruktoren

Byte

Byte( byte value ) Byte( String s )

Short

Short( short value ) Short( String s )

Integer

Integer( int value ) Integer( String s )

Long

Long( long value ) Long( String s )

Float

Float( float value ) Float( double value ) Float( String s )

Double

Double( double value ) Double( String s )

Boolean

Boolean( boolean value ) Boolean( String s )

Character

Character( char value )

Tabelle 3.2: Konstruktoren der Wrapper-Klassen

Hinweis Verwendet man bei der Klasse Boolean den Konstruktor mit einem String als Parameter, dann wird nur dann ein Objekt mit dem Datenwert true angelegt, wenn als Stringwert true angegeben ist, wobei nicht zwischen Groß- und Kleinschreibung unterschieden wird. In allen anderen Fällen wird ein Objekt mit dem Datenwert false instanziert. Mit Ausnahme der Klasse Boolean können alle Konstruktoren mit String als Parameter eine Exception auslösen, die wir im nächsten Kapitel kennen lernen. Hier habe ich sie der Einfachheit halber in der Deklaration des Konstruktors weggelassen.

Objektorientierte Programmierung (OOP)

Primitiven Datenwert ermitteln

eben einigen zusätzlichen Methoden sind in allen Wrapper-Klassen Accessorethoden definiert, die den primitiven Datenwert des aktuellen Objekts zurücklien: Wrapper-Klasse

Accessor-Methoden

Byte

byte byteValue()

Short

short shortValue()

Integer

int intValue()

Long

long longValue()

Float

float floatValue()

Double

double doubleValue()

Boolean

boolean booleanValue()

Character

char charValue()

belle 3.3: Accessor-Methoden der Wrapper-Klassen

le numerischen Wrapper-Klassen haben zudem Methoden, mit denen man den tuellen primitiven Datenwert als übergeordneten oder auch untergeordneten Datentyp erhalten kann, so sind zum Beispiel in der Klasse Short die AccessorMethoden byteValue(), intValue(), longValue(), floatValue() und doubleValue() plementiert. ill man ganz auf die Instanzierung eines Objekts für einen primitiven Datenwert rzichten, so kann man die statischen Methoden der numerischen Wrapper-Klassen nutzen, wenn der primitive Datenwert als String-Variable angegeben ist (oder als String-Konstante vorliegt). Die dafür vorgesehenen Methoden sind bei den ganzzahgen numerischen Wrapper-Klassen mehrfach vorhanden, um den angegebenen ing für verschiedene Zahlensysteme zurückgeben zu können: Wrapper-Klasse

statische Accessor-Methode

Byte

static byte parseByte( String s ) static byte parseByte( String s, int r )

Short

static short parseShort( String s ) static short parseShort( String s, int r )

elle 3.4: Statische Methoden für den primitiven Wert mit Wrapper-Klassen

Objektorientierte Programmierung (OOP)

Wrapper-Klasse

statische Accessor-Methode

Integer

static int parseInt( String s ) static int parseInt( String s, int r )

Long

static long parseLong( String s ) static long parseLong( String s, int r )

Float

static float parseFloat( String s )

Double

static double parseDouble( String s )

Tabelle 3.4: Statische Methoden für den primitiven Wert mit Wrapper-Klassen (Forts.)

Der Parameter int r bedeutet ein Zahlensystem zur Basis r. Gibt man für den Parameter zum Beispiel die Zahl 8 an, so wird der Wert des Strings als Oktalzahl interpretiert. Auch hier können, wie bereits vorher gezeigt, alle angegebenen Methoden Exceptions auslösen, wenn die Umwandlung in einen gültigen primitiven Datenwert nicht möglich ist. Der Einfachheit halber sind die Exceptions in der Übersicht nicht angegeben. Übergabe mit call-by-reference

Die Wrapper-Klassen eignen sich nicht dafür, dass sie Methoden übergeben werden, in denen dann der primitive Datenwert geändert wird (was sich auf den Originalwert auswirken würde, da Objekte immer als Referenz, nicht als Kopie an Methoden übergeben werden). Der Grund ist ganz einfach: Die Wrapper-Klassen stellen keine Mutator-Methoden zur Verfügung, man bezeichnet sie deshalb auch als immutable. Diese Einschränkung kann man dadurch umgehen, dass man keine Instanz einer Wrapper-Klasse übergibt, sondern den primitiven Datenwert in einem Array speichert und das Array übergibt. Arrays sind Objekte und werden deshalb ebenfalls als Referenz übergeben, somit ist es möglich, dass die aufgerufene Methode den Original-Wert ändert. Die zweite Lösung ist, dass man sich dafür selbst eine eigene Wrapper-Klasse schreibt. Einige Wrapper-Klassen bieten eine Vielzahl verschiedener zusätzlicher Methoden an, die Sie am besten mit dem Browser im Java-API des JDK erkunden. Daneben sind dort auch wichtige Konstanten definiert, die im Kapitel Sprach-Grundlagen beschrieben werden.

Exceptions

4

Exceptions

Dieses Kapitel befasst sich eingehend mit Laufzeit-Fehlern, die man auch als Ausnahmen oder neudeutsch als Exceptions bezeichnet. Es gibt grundsätzlich zwei Möglichkeiten für das Auslösen von Exceptions: 

Die Virtual Machine erkennt einen Laufzeit-Fehler des Programms, zum Beispiel, weil eine illegale Division durch Null ausgeführt werden soll.



Der Programmierer löst in seinem Programmcode gezielt eine Exception aus, weil zum Beispiel eine Konfigurationsdatei, die vom Programm gelesen werden muss, nicht vorhanden ist.

Wenn eine Exception ausgelöst wird, ohne dass im Programm besondere Vorkehrungen getroffen wurden, dann bricht die Virtual Machine das Programm ab. Bei Hauptprogrammen bedeutet dies das sofortige Ende des Programms. Man kann eine Exception auch weiter reichen und dem übergeordneten Programmcode überlassen, ob und wie auf die Ausnahme reagiert werden soll (wie das funktioniert, werden wir bald sehen). Sehen wir uns einen typischen Fall an, so wie er tagtäglich passieren kann: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20

// Datei ExceptionTest01.java // Das Programm führt eine Division durch Null // durch, was zu einem Laufzeit-Fehler führt. // Die Virtual Machine löst dadurch eine Exception // aus, die zum Beenden des Programms führt. public class ExceptionTest01 { private static int a = 1; private static int b = 0; private static void f1() { f2(); } private static void f2() { f3(); } private static void f3() { double c = a / b; }

Exceptions

21 22 23 24 25 }

public static void main( String[] args ) { f1(); }

ing 4.1: Programm mit einem unbehandelten Laufzeit-Fehler (Forts.)

enn wir das Programm übersetzen und ausführen, erhalten wir folgende Ausgabe:

Abbildung 4.1: Auswirkung einer Exception ohne Maßnahmen im Programm

as wir in der Ausgabe sehen, ist ein so genannter Stack-Trace. Der Fehler tritt nicht Hauptprogramm, sondern in der Methode f3() auf, die von f2() aufgerufen urde. Diese wiederum wurde von f1() und diese zu guter Letzt aus dem Hauptproramm heraus aufgerufen. Genau diese Programm-Hierarchie sehen wir im ausgegenen Stack-Trace, das innerste Programm-Teil, in dem die Exception auftrat, steht erster Stelle, danach folgen alle Methoden der Programm-Hierarchie, bis am Ende die Zeile des Hauptprogramms ausgegeben wird, in welcher der tödliche Aufruf steht. komplexen Programmen kann sich die Ausgabe des Stack-Trace über mehrere ldschirm-Seiten hinweg erstrecken. Als hilfreich erweist sich der Stack-Trace, weil nicht nur angibt, wo der Fehler passiert ist, sondern auch in welcher Zeile des Proamms und vor allem, weil die komplette Programm-Hierarchie ausgegeben wird. as ist insbesondere dann wichtig, wenn eine Methode von vielen anderen Klassen nutzt wird. In solchen Fällen würde es kaum weiterhelfen, wüsste man nur die ethode, in welcher der Fehler auftrat. Der Stack-Trace gibt genaue Auskunft darür, wer wann wen in welcher Reihenfolge aufgerufen hat. Java sind Exceptions als Klassen hierarchisch in einem Vererbungsbaum definiert, r unterhalb von Object mit der Eltern-Klasse Throwable als Vater aller Java-Fehler ginnt. Diese Klasse hat unter anderem die Kind-Klasse java.lang.Exception. Mit dieser und den daraus abgeleiteten Klassen werden wir uns im Folgenden näher schäftigen.

Exceptions

Hinweis Eine zweite Kind-Klasse von Throwable, nämlich Error, wird immer dann instanziert, wenn ein schwerwiegender Fehler in der Virtual Machine aufgetreten ist. Auf solche Fehler sollte man im Programm nicht reagieren, sondern den eingebauten Fehler-Mechanismus von Java seine Arbeit tun lassen, das Programm sollte in einem solchen Fall immer mit einer Fehlermeldung der Virtual Machine abgebrochen werden.

4.1

Behandlung von Exceptions

In Java gilt der Grundsatz: »Exceptions müssen entweder behandelt oder weitergereicht werden!«, was im Original »catch or throw!« heißt. Sehen wir uns nun an, wie man im Programmcode Exceptions behandelt. 4.1.1

try/catch-Block

Das reservierte Wort try kann mit Versuche übersetzt werden, catch bedeutet Fange ab. Programmcode, bei dem nicht sicher ist, ob eine Exception auftreten kann oder nicht, bzw. in dem Methoden aufgerufen werden, die laut API Exceptions auslösen können, müssen in einen try/catch-Block gestellt werden, wenn man die Laufzeit-Fehler selbst behandeln möchte. Definition des try/catch-Blocks // einfacher try/catch-Block try { // gefährliche Anweisungen } catch ( exceptionClass name ) {[ catchCode ]} // erweiterter try/catch-Block mit finally try { // gefährliche Anweisungen } catch ( exceptionClass name ) {[ catchCode ]} finally { finallyCode }

Alle Anweisungen, die zu einer Exception führen können, werden in den durch die geschweiften Klammern gekennzeichneten Block gestellt, der nach dem reservierten Wort try folgt. Es können beliebig viele Anweisungen in diesem Block stehen.

Exceptions

Nach dem try-Block folgt mindestens ein catch-Block, in dem ein bestimmter Typ n Exceptions durch den catchCode abgefangen werden kann, dieser steht wiederum in geschweiften Klammern und ist optional, d.h. man kann auch ein Paar leerer ammern angeben, hierzu weiter unten mehr. exceptionClass ist der Klassen-Name rjenigen Exception, die man abfangen möchte, name ist ein Identifier für eine Varile, in welcher die Referenz auf die Exception abgespeichert wird. vor Sie nun verwirrt im Wald stehen, hier ein Anschauungsbeispiel: Benutzung des try/catch-Blocks int a = 1; int b = 0; double c; try { c = a / b; } catch ( ArithmeticException ex ) { System.out.println( "Fehler " + ex ); System.exit( 1 );

ist ein von mir willkürlich gewählter Variablen-Name. Die Variable enthält bei Auftreten einer Exception eine Referenz auf die Instanz der Exception. In den meisn Büchern wird der Buchstabe e als Variablen-Name verwendet. Ich bevorzuge jedoch ex, weil wir später, wenn es um das Thema Grafik-Programmierung geht, ch ein weiteres Element mit Namen Event kennen lernen werden, das ich dann mit abkürze. e Virtual Machine bricht die Abarbeitung der Anweisungen im try-Block an der elle ab, wo ein Runtime-Fehler eine Exception auslöst. Alle weiteren Anweisungen s try-Blocks werden nicht mehr ausgeführt, sondern der Programmfluss wird enteder in dem zur Exception passenden catch-Block fortgeführt oder das Programm rd ganz abgebrochen, wenn die aufgetretene Exception nicht abgefangen wird. es ist vor allem bei der NullPointerException der Fall, die speziell bei Methoden ftreten kann, welche Objekt-Instanzen zurückgeben. Falls in der Methode ein Fehauftritt, kann es des Öfteren vorkommen, dass eine so genannte RuntimeException sgelöst wird (dazu gehört auch die NullPointerException), die nicht im API der Methode beschrieben ist (und die man aufgrund dessen auch nicht abfangen kann).

Exceptions

Ich habe die Variable c außerhalb des try-Blocks deklariert, und zwar aus gutem Grund: Würde man die Variable innerhalb des try-Blocks deklarieren, dann hätte sie nur innerhalb dieses try-Blocks Gültigkeit, selbst im catch-Block wäre sie nicht definiert. Der folgende Vorgang ist übrigens der wohl häufigste Fehler neben Tippfehlern: Sie schreiben ein paar Anweisungen. Beim Übersetzen stellen Sie durch Fehlermeldungen fest, dass Sie Exceptions abfangen müssen. Also rücken Sie den betreffenden Programmcode um eine Ebene weiter ein und packen einen try/catch-Block drum herum. Schon haben Sie sich in fast allen Fällen einen anderen Fehler eingefangen, der wiederum zu einer Fehlermeldung des Compilers führt. Dadurch, dass der gesamte Code nun in dem try-Block steht, sind alle Variablen, die Sie deklariert hatten, nun außerhalb des try-Blocks nicht mehr definiert. In diesem Fall hilft es sehr, wenn Sie Deklaration und Initialisierung von Variablen trennen. Wie ich bereits erwähnt habe, kann man beliebig viele catch-Blöcke definieren, hier ein Beispiel: String sa = "1"; int b = 0; double c; try { int a = Integer.parseInt( sa ); c = a / b; } catch ( ArithmeticException ex ) { System.out.println( "Fehler " + ex ); System.exit( 1 ); } catch ( NumberFormatException ex ) { System.out.println( "Variable sa ist keine Zahl" ); System.exit( 1 ); }

Wie alle anderen Klassen auch sind Exceptions abwärts-kompatibel, Sie können also eine Eltern-Klasse statt der tatsächlichen Kind-Klasse verwenden. Faule Programmierer würden dieses Feature vielleicht so nutzen:

Exceptions

String sa = "1"; int b = 0; double c; try { int a = Integer.parseInt( sa ); c = a / b; } catch ( Exception ex ) { System.out.println( "Fehler " + ex ); System.exit( 1 );

Da die Klasse Exception Basisklasse aller weiteren Exceptions ist, kann man sich hreibarbeit sparen und nur diese Eltern-Klasse in einem einzigen catch-Block fangen. Davon kann ich jedoch nur abraten. Bei Auftreten eines Fehlers sollte mögchst viel Detail-Information an den Aufrufer zurückgegeben werden, die man durch ehrere catch-Blöcke besser weiterreichen kann als mit nur einem einzigen catchock. gibt noch eine andere Art von Faulheit, die noch schlimmer ist: Manche Programerer schreiben den gesamten Code einer Methode in einen einzigen try/catchock und fangen nur die oberste Eltern-Klasse Exception ab: public void myMethod() { try { // Der gesamte Methoden-Rumpf steht in einem // einzigen try/catch-Block } catch ( Exception ex ) { // Abfang-Code }

Sie können sich vielleicht vorstellen, wie wenig hilfreich diese Art der Programmieng ist, wenn sich der Programmcode im try/catch-Block über fünf Bildschirmiten erstreckt und irgendwo ein Fehler auftritt. Gibt man mehrere catch-Blöcke an, die aufgrund des Vererbungsbaums zur ausgelösn Exception passen, dann wird der erste catch-Block ausgeführt, der im Programmde nach dem try-Block gefunden wird:

Exceptions

// try-Block, in dem versucht wird eine Datei zu lesen try { // Datei öffnen } catch ( IOException ex ) { ... } catch ( Exception ex ) { ... }

Wenn eine IOException ausgelöst wurde, dann führt die Virtual Machine den Programmcode im catch-Block für IOException aus. Handelt es sich aber zum Beispiel um eine NullPointerException, dann verzweigt das Programm in den catch-Block für Exception, weil kein Block für die NullPointerException vorhanden ist, Exception aber eine Eltern-Klasse von NullPointerException und somit kompatibel zur ausgelösten Exception ist. Hinweis Werden mehrere Exceptions in catch-Blöcken abgefangen, die zueinander kompatibel sind, weil sie im Vererbungsbaum übereinander liegen, dann ist es ratsam, zuerst die unterste Kind-Exception anzugeben und die allgemeinste Exception am Ende. Würde man die Reihenfolge vertauschen, indem man als Erstes die oberste Eltern-Exception angibt, dann wird in jedem Fall dieser catchBlock aufgerufen, während alle weiteren passenden Blöcke niemals benutzt werden. Vorsicht ist geboten bei geschachtelten try/catch-Blöcken: try { ... } catch ( Exception ex ) { ... try { ... // Der Variablen-Name ex wurde bereits im // äußeren catch-Block vergeben, das Programm // lässt sich nicht übersetzen. } catch ( Exception ex ) { ... } }

Exceptions

Obwohl man try/catch-Blöcke beliebig tief schachteln darf, muss man hier bei der Namensvergabe der Exceptions aufpassen. In unserem Beispiel würde sich der Comer weigern, den Code zu übersetzen, weil der Variablen-Name ex des inneren catch-Blocks bereits im außen liegenden catch-Block verwendet ist. In Java ist es veroten, Variablen-Namen im Programmcode zu verwenden, die bereits in einem umgebenden Programmblock deklariert wurden (Ausnahme: lokale Variablen in ethoden von Klassen). Sind die catch-Blöcke jedoch in derselben Ebene, dann darf man gleiche Identifier für die Exceptions verwenden. Hinweis Wenn man eine Exception mit Hilfe von catch-Blöcken abfängt, heißt das nicht, dass anschließend die Methode oder der Programm-Teil abgebrochen wird, im Gegenteil, indem man eine Exception abfängt, verhindert man genau dieen Abbruch. Was man dann im catch-Block nach einem Fehler tut, ist dem Prorammierer überlassen. So werden häufig Zugriffe auf eventuell belegte Ressourcen wie zum Beispiel Ports in Schleifen programmiert, die eine bestimmte maximale Anzahl von Versuchen starten, eine Verbindung herzustellen. Ist der Port belegt, ann setzt es zwar eine Exception, das muss aber noch lange nicht heißen, dass ein ehler aufgetreten ist, denn es kann ja sein, dass der Port kurzfristig von einem andeen Programm belegt ist. Man wartet in einem solchen Fall im catch-Block für eine kurze Zeit, erhöht den Zähler für die Anzahl der gestarteten Versuche und startet ann einen neuen Versuch. Erst wenn die vorgegebene maximale Anzahl von Versuhen erreicht ist, löst man selbst eine Exception aus oder beendet das Programm.

Hinweis In der Regel müssen alle möglichen Exceptions in einem oder in mehreren catch-Blöcken abgefangen werden. Eine Ausnahme von dieser Regel bilden die Runtime-Exceptions. Vorsicht, dieser Begriff ist nicht zu verwechseln mit Runtime-Fehler. Letzteres ist ein Fehler, der zur Laufzeit auftritt und eine Exception auslöst. Runtime-Exceptions sind alle Kind-Klassen von RuntimeException. Diese müssen weder in Methoden-Deklarationen angegeben noch in catch-Blöcken abgefangen werden (obwohl man es dennoch tun kann). 1.2

finally

n try/catch-Block kann einen so genannten finally-Block enthalten, der optional t und damit auch entfallen darf: // erweiterter try/catch-Block mit finally try {

Exceptions

// gefährliche Anweisungen } catch ( exceptionClass name ) {[ catchCode ]} finally { finallyCode }

Der Programmcode im finally-Block wird in jedem Fall durchlaufen, wenn der tryBlock verlassen wird, egal aus welchem Grund. Das ist also der geeignete Platz für Aufräumarbeiten, in dem man z.B. das Schließen von Dateien oder Datenbank-Verbindungen unterbringt. Da ein Beispiel für den finally-Block zu diesem Zeitpunkt zu viele Unbekannte ins Spiel bringen würde, möchte ich Sie hier auf spätere Kapitel vertrösten, wo ich dieses Thema noch einmal aufgreifen werde. Spätestens bei Datenbank-Programmierung mit JDBC werden Sie den Aha-Effekt erleben.

4.2

Die Klasse java.lang.Exception

Exceptions sind in Java ganz normale Klassen und besitzen wie diese Konstruktoren und Methoden für die Verarbeitung von Fehler-Objekten. In der folgenden Tabelle sind die Konstruktoren der Klasse Exception dargestellt: Konstruktor

seit

Beschreibung

Exception()

1.0

Default-Konstruktor. Legt ein leeres Objekt ohne Detail-Information an.

Exception( String s )

1.0

Legt ein neues Objekt mit dem angegebenen Text für die Detail-Information an.

Exception( String s, Throwable t )

1.4

Zusätzlich zur textuellen Information kann ein Objekt vom Typ Throwable angegeben werden, das z.B. die ursprüngliche Exception enthalten kann.

Exception( Throwable t )

1.4

Wie vorher, nur fehlt die Angabe der textuellen Information.

Tabelle 4.1: Konstruktoren der Klasse Exception

Meist wird die zweite Konstruktor-Variante benutzt. Seit der Java-Version 1.4 kann zusätzlich ein Exception-Objekt im Konstruktor angegeben werden (zum Beispiel die im Programmcode abgefangene Exception). Dieses Feature kann unter anderem dazu genutzt werden, eine eigene Verarbeitung des Laufzeit-Fehlers durchzuführen.

Exceptions

e Klasse Exception bietet einige weitere Methoden für die Verarbeitung von ceptions an, die hauptsächlich einer erweiterten, selbst implementierten Ausgabe n Laufzeit-Fehlern dienen. Im Rahmen dieses Buches genügt es allerdings zu wisen, dass nicht abgefangene Exceptions grundsätzlich in der Standard-Fehlerausgabe s Systems landen. Man kann auch die Methode printStackTrace() der Klasse Exception aufrufen, die explizit eine Ausgabe des Stack-Trace auf STDERR durchhrt.

3

Weitergabe von Exceptions

Der zweite Teil der Java-Regel »catch or throw!« besagt, dass man für den Fall, wo an einen Laufzeit-Fehler nicht selbst bearbeiten möchte oder kann, diesen an die chste Instanz weiterleiten muss. Damit wir den Satz verstehen, müssen wir erst wisn, was die nächste Instanz ist. Nun, aus der Sicht eines beliebigen Programmcodes die nächste Instanz der umgebende Programmcode. Das kann bei einer Schleife der ßerhalb dieser Schleife liegende Code sein, von der Warte des Methoden-Rumpfs s gesehen ist der aufrufende Programmcode die nächste Instanz. Das kann sich bis m Hauptprogramm fortpflanzen. Die Code-Hierarchie sieht man auch anhand des ack-Trace in Abbildung 4.1. ne Exception kann also von innen nach außen durchgereicht werden, bis man im auptprogramm angelangt ist. Wird die Exception dort nicht abgefangen, dann endet die Virtual Machine das Programm. 3.1

throw-Anweisung

Das Auslösen einer Exception ist eigentlich ganz einfach. Java stellt für diesen Zweck t dem reservierten Wort throw (ins Deutsche mit werfen übersetzt, in Bayern werden ceptions grundsätzlich gschmissn) eine eigene Anweisung zur Verfügung: throw exceptionInstance;

eptionInstance kann entweder ein neu instanziertes Objekt einer der vielen Excepn-Klassen von Java oder ein vorhandenes Objekt sein. Auch selbst definierte ceptions können geworfen werden. Hier ein gebräuchliches Beispiel für das Aussen einer Exception:

Exceptions

// Auslösen einer neuen Exception aufgrund eines // Laufzeit-Fehlers beim Verarbeiten einer Zahl try { ... } catch ( NumberFormatException ex ) { throw new Exception( "falsche Zahl" ); } // Weitergabe der ursprünglichen Exception try { ... } catch ( NumberFormatException ex ) { throw new Exception( "Fehler " + ex ); } // die Weitergabe funktioniert auch so: try { ... } catch ( NumberFormatException ex ) { // Code für die eigene Verarbeitung // z.B. Log-Meldungen schreiben ... // ursprüngliche Exception selbst noch einmal // werfen throw ex; }

Wird in einem catch-Block selbst eine Exception ausgelöst (geworfen), dann bricht die Virtual Machine an dieser Stelle die Verarbeitung des Programms ab und sucht wie üblich im umgebenden Programmcode nach catch-Blöcken bis hinauf zum Hauptprogramm. Das Auslösen einer neuen Exception in einem catch-Block wird häufig dazu benutzt, um eine angepasste Fehlerbehandlung zu implementieren. 4.3.2

throws-Klausel

Vorsicht: Verwechseln Sie das reservierte Wort throws nicht mit dem vorher beschriebenen throw! Während man mit throw explizit eine Exception auslöst, teilt man mit throws dem Compiler bei der Deklaration von Methoden mit, dass sie bestimmte Exceptions werfen (was wiederum mit throw geschieht).

Exceptions

m besten, ich zeige Ihnen die Zusammenhänge wie immer anhand eines Beispiels. Zuerst möchte ich demonstrieren, wie es nicht geht: 01 02 03 04 05 06 07

// Datei ExceptionTest02.java // Diese Datei lässt sich nicht übersetzen. public class ExceptionTest02 { public static void main( String[] args ) { throw new Exception( "bla" ); } }

Der Compiler beschwert sich beim Versuch, die Datei zu übersetzen, mit der Fehlereldung, dass die Ausnahme Exception deklariert werden muss. Hier also ein funkonierendes Beispiel: 01 // Datei ExceptionTest03.java 02 public class ExceptionTest03 { 03 public static void main( String[] args ) 04 throws Exception 05 { 06 throw new Exception( "bla" ); 07 } 08 }

le Exceptions, die von einer Methode mit throw ausgelöst werden können, müssen mit dem reservierten Wort throws vor dem Methoden-Rumpf deklariert sein. Man nn beliebig viele Exceptions angeben, als Trennzeichen muss das Komma verwent werden. Eine Ausnahme bilden RuntimeException und alle daraus abgeleiteten nd-Klassen: 01 // Datei ExceptionTest04.java 02 public class ExceptionTest04 { 03 public static void main( String[] args ) { 04 throw new NullPointerException( "bla" ); 05 } 06 }

Da die Klasse NullPointerException von RuntimeException abgeleitet ist, muss sie nicht in der Methoden-Deklaration angegeben sein.

Exceptions

4.4

Selbst definierte Exceptions

Da Exceptions ganz normale Klassen sind, kann man auch eigene Exceptions als Kind-Klassen der Standard-Klassen ableiten und so applikationsspezifische LaufzeitFehler definieren. Hier ein Beispiel dazu: 01 02 03 04 05 06 07

// Datei MyException.java // Eigene Exception-Klasse public class MyException extends Exception { public MyException( String s ) { super( s ); } }

Und hier ein Java-Programm, welches die neu definierte Exception benutzt: 01 02 03 04 05 06 07 08 09 10 11

// Datei ExceptionTest05.java // Die Klasse benutzt eine selbst implementierte // Exception, die als Kind-Klasse von Exception // abgeleitet ist. public class ExceptionTest05 { public static void main( String[] args ) throws MyException { throw new MyException( "bla" ); } }

Wichtige Klassen

5

Wichtige Klassen

5.1

java.lang.String

Strings werden auch Zeichenketten genannt und manifestieren sich in Java in der Klasse java.lang.String, d.h. es handelt sich hierbei um Objekte. Wie man bereits am Package-Namen vermuten kann, sind Strings integraler Bestandteil von Java und erfahren besondere Unterstützung durch den Compiler. Zum einen sind Zeichenketten bei Java ganz normale Objekte, die durch Instanzieren einer Klasse entstehen, zum anderen werden sie ein bisschen behandelt wie die primitiven Datentypen. Meist fällt einem die Tatsache, dass Strings in Java als eine Kette von Unicode-Zeichen verarbeitet werden, gar nicht auf, manchmal jedoch ist es gut, diesen Umstand zu kennen. Da Strings als eine Reihe von char-Elementen gespeichert sind (die ebenfalls in Unicode vorliegen), benötigen Strings doppelt so viel Speicherplatz wie einfache byte-Ketten im ASCII-Format, weil zur Darstellung von Unicode-Zeichen immer zwei Bytes verwendet werden. Wie ich bereits erwähnt habe, bietet Java bei der Verarbeitung von Strings eine besondere Sprach-Unterstützung, was bei normalen Klassen nicht der Fall ist. So kann man ein Objekt der Klasse String beispielsweise ohne das reservierte Wort new instanzieren: String myString = "hallo";

Auch die Verkettung von Strings ist direkt in die Sprache über den Operator + möglich: String s = "Habe " + "die Ehre"; // in s steht nun "Habe die Ehre"

Natürlich funktioniert auch die Kombination aus Zuweisungs- und VerkettungsOperator: String s = "Habe"; s += " die"; s += " Ehre"; // s enthält nun "Habe die Ehre";

Wichtige Klassen

Hinweis Verwendet man String-Literale im Source-Code, dann müssen die Zeichenketten-Konstanten in doppelte Anführungszeichen gesetzt werden. Einfache Quotes sind den char-Konstanten vorbehalten. e Leichtigkeit, mit der diese Operationen programmiert werden können, verführt erdings auch häufig dazu, dass man sein Hirn nicht mehr bemüht, was vor allem nn unangenehme Folgen haben kann, wenn Geschwindigkeit und Speicher-Effienz im Vordergrund stehen. Aus dem Beispielcode wird nicht auf den ersten Blick sichtlich, dass der Compiler bzw. die Virtual Machine eine Menge zu tun hat, so erden in diesen drei Programmzeilen eine ganze Reihe von Objekten instanziert d Daten im Hauptspeicher hin- und herkopiert. All diese Operationen sind relativ itintensiv, wie wir bald feststellen werden. ings sind immutable, das bedeutet, dass man den Wert eines einmal erzeugten ing-Objekts nachträglich nicht mehr ändern kann. Sie werden in der Klasse String ine einzige Methode finden, mit der man den aktuellen Wert eines Strings ändern nn. Wie bitte?«, werden jetzt manche sagen, »im vorangegangenen Beispiel haben wir ch eine String-Verkettung mit nur einer Instanz durchgeführt!« a, weit gefehlt, kann ich da nur sagen. Der Compiler versteckt nämlich die Tatsae, dass er zunächst ein temporäres String-Objekt erzeugt und dieses anschließend in r Referenz-Variablen s speichert. vor wir nun zu den Methoden der Klasse String kommen, werfen wir noch einen rzen Blick auf einen weiteren Aspekt der besonderen Sprach-Unterstützung, die r für Strings gilt, und zwar die automatische Konvertierung primitiver Datentypen einen String-Wert: 01 // Datei StringTest01.java 02 public class StringTest01 { 03 public static void main( String[] args ) { 04 int i = 3; 05 int j = 4; 06 07 // Der Wert der int-Variablen i wird 08 // automatisch in einen String umgewandelt 09 System.out.println( i );

Wichtige Klassen

10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 }

5.1.1

// Vorsicht: Hier wird zuerst eine Addition // durchgeführt, anschließend das Ergebnis // in einen String umgewandelt und // ausgegeben. System.out.println( i + j ); // Hier stellt der Compiler fest, dass keine // Umwandlung möglich ist, da die erste // Addition vielmehr eine String-Verkettung // darstellt. Es wird also der Wert von i // an den leeren String angehängt, // anschließend der Wert von j an das // Ergebnis der vorherigen Operation. // Das Resultat ist also "34" System.out.println( "" + i + j ); }

Konstruktoren von java.lang.String

Neben der speziellen Instanzierung von Strings mit Zeichenketten-Konstanten gibt es eine Reihe von Konstruktoren, mit denen String-Objekte erzeugt werden können. Die wichtigsten möchte ich Ihnen hier zeigen:  String()

Default-Konstruktor  String( String s )

Instanziert ein neues String-Objekt durch Kopie eines bereits vorhandenen  String( char[] c )

Aus einem char-Array wird ein String-Objekt erzeugt.  String( byte[] b )

Aus einem byte-Array wird ein String-Objekt erzeugt, es wird die Default-Einstellung für den Zeichensatz verwendet.  String( byte[] b, String charSet )

Wie vorher, nur wird hier explizit der zu verwendende Zeichensatz mit dem zweiten Argument angegeben.

Wichtige Klassen

String( StringBuffer sb )

Das String-Objekt wird mit dem Inhalt eines Objekts der Klasse StringBuffer erzeugt. Hinweis Da Strings wie normale Objekte behandelt werden, muss sich der Prorammierer nicht um die Speicher-Verwaltung kümmern. Diese Tatsache macht sich besonders für Kenner von C positiv bemerkbar. le Konstruktoren, bei denen ein Character-Set (zu Deutsch Zeichensatz) angegeben rden kann, lösen eventuell die Exception java.io.UnsupportedEncodingException s, die abgefangen werden muss: // Datei StringTest02.java import java.io.*; public class StringTest02 { public static void main( String[] args ) { // byte-Array instanzieren byte[] b = new byte[] { 0x41, 0x42, }; String s = null; // Der Konstruktor kann die Exception // java.io.UnsupportedEncodingException // auslösen, deshalb benötigen wir einen // try/catch-Block. try { s = new String( b, "ISO-8859-1" ); } catch ( UnsupportedEncodingException ex ) { System.err.println( "Zeichensatz falsch" ); System.exit( 1 ); } System.out.println( "String = " + s ); }

ehr zu Zeichensätzen erfahren Sie, wenn Sie im Browser bei der Beschreibung des onstruktors im API des JDK den entsprechenden Links folgen.

Wichtige Klassen

Im Beispiel habe ich eine Besonderheit von Arrays genutzt: Man kann Arrays bei der Deklaration mit Konstanten initialisieren, die in geschweifte Klammern gestellt werden. In diesem Fall muss die Anzahl der Elemente des Arrays nicht in den eckigen Klammern angegeben werden, sondern der Compiler ermittelt diese selbst aufgrund der Anzahl von Elementen im folgenden Block, der durch geschweifte Klammern gekennzeichnet ist. Vergessen Sie bitte nicht, dass nach der schließenden geschweiften Klammer ein Semikolon stehen muss, da es sich um eine Zuweisung handelt. Definition eines String-Arrays

Hier noch einmal ein Beispiel für die Initialisierung eines String-Arrays mit Konstanten: // Initialisierung eines String-Arrays String[] sarray = { "a", "b", "c", }; // Man String String string

kann statt Konstanten auch Variablen verwenden s1 = "a"; s2 = "b"; sarray = { s1, s2, };

Hinweis Bei der Initialisierung von Arrays mit einer Liste aus Konstanten kann auch nach dem letzten Element der Liste ein Komma angegeben werden. Dieses Feature sollte man immer nutzen, da es hilft, Fehler zu vermeiden (z.B. wenn Sie später die Liste erweitern). 5.1.2

Methoden von java.lang.String

Wie bereits erwähnt, bietet die String-Klasse eine Reihe von Methoden an, die das Verarbeiten von Strings erleichtern. Im Folgenden möchte ich auf die wichtigsten Methoden eingehen: Länge eines Strings ermitteln

Mit Hilfe der Methode length() lässt sich die Anzahl der Zeichen in einem String ermitteln: int length()

Wichtige Klassen

e Methode length() gibt nicht etwa die Anzahl von Bytes der Zeichenkette rück, sondern die Anzahl der Zeichen. Das ist in Java ein Unterschied, da alle ings im Unicode-Format gespeichert sind, das für jedes Zeichen zwei Bytes verndet. Achtung Verwechseln Sie bitte die String-Methode length() nicht mit dem Attribut length von Arrays. Oft vergisst man die runden Klammern, es gibt aber ein Attribut length in der String-Klasse.

// Beispiel für die Methode length() String s = "abcde"; for ( int i = 0; i < s.length(); i++ ) { // Extrahieren eines einzelnen Zeichens an der // angegebenen Position i. System.out.println( s.charAt( i ) );

System.out.println( "Der String ist " + s.length() + " lang" );

e einzelnen Zeichen werden in einem String als Array abgelegt. Das erste Zeichen t in diesem Array den Index 0, das letzte Zeichen den Index length() - 1. Extrahieren von Zeichen

Um ein einzelnes Zeichen aus einem String zu extrahieren, dessen Position bekannt kann man die Methode charAt() verwenden: char charAt( int ind )

ispiele für charAt(): // Extrahieren eines einzelnen Zeichens aus einem // String mit charAt() String s = "hallo"; char c = s.charAt( 2 );

Wichtige Klassen

// umgekehrte Ausgabe eines Strings for ( int i = s.length() - 1; i >= 0; i-- ) { System.out.print( s.charAt( i ) ); } System.out.println();

Seltsamerweise müssen wir bei der Methode charAt() die mögliche Exception java.lang.IndexOutOfBoundsException nicht abfangen. Warum? Die Antwort: Es handelt sich hier um eine Kind-Klasse von RuntimeException. Wie bereits im vorherigen Kapitel erwähnt, müssen solche Exceptions nicht abgefangen werden. Wohl gemerkt, das ist eine Kann-Regel, keine Muss-Regel. Will man eventuell mehrere Zeichen aus einem String extrahieren, dann hat man folgende Methoden zur Auswahl: Die Methode substring() hat den Datentyp String und bietet die Möglichkeit, TeilBereiche des Strings zu extrahieren: String substring( int fromInd ) String substring( int fromInd, int endInd )

In der ersten Variante wird der gesamte Rest des Strings ab dem angegebenen StartIndex extrahiert. Bei der zweiten Variante wird ab fromInd bis ausschließlich endInd ein Teil-Bereich gebildet. Beispiele für substring(): // Extrahieren eines Teil-Strings // 012345678 String s = "abcdefghi"; String ts = s.substring( s.length() - 3 ); // in ts wird ab Index 6 (s.length() - 1) der Rest // von s kopiert. // ts enthält also "ghi"

Wichtige Klassen

Mit der Methode substring (int beginInd, int endInd) kann man einen beliebigen il-String extrahieren: // 01234567890123456 String s = "abcdefghijklmnopq"; // Kopieren der ersten 5 Zeichen String ts = s.substring( 0, 5 );

werden Zeichen von einschließlich beginInd bis ausschließlich endInd extrahiert. e Methode subSequence( int beginInd, int endInd ) ist seit der Version 1.4 des K implementiert und funktioniert ähnlich wie substring(), nur liefert sie den Datentyp java.lang.CharSequence zurück. Dabei handelt es sich um ein Interface, das bergreifend über mehrere Klassen ein paar Methoden deklariert. Damit kann man f Objekte der Klassen String, java.lang.StringBuffer und java.nio.CharBuffer dieselben Methoden anwenden, ohne eine Objekt-Umwandlung durchführen zu üssen, weil alle Klassen die Methoden des Interfaces implementieren. hite Space entfernen

Mit der Methode trim() erhält man einen String zurück, der dem ursprünglichen eicht, jedoch werden am Beginn und am Ende alle White Space-Zeichen entfernt. e klassischen White Space-Zeichen sind \n, \r, \t, ' ', \f . Allerdings werden mit dier Methode auch alle Control-Zeichen am Beginn und Ende des Strings entfernt. Dazu gehören alle Zeichen, deren Zeichen-Code kleiner als 0x20 ist. String trim()

er Original-String wird dabei nicht verändert. Hier ein Beispiel für trim(): // String mit White Space-Zeichen am Anfang und Ende String s = "\n\t \f\u0001 Stringwert \t\r\n"; String s1 = s.trim(); // s1 enthält "Stringwert";

Wichtige Klassen

Strings umwandeln

Die Klasse String bietet für die Umwandlung der Zeichen in Groß- bzw. Kleinbuchstaben die Methoden toUpperCase() und toLowerCase() an: String toUpperCase() String toLowerCase()

Beispiel für toUpperCase() und toLowerCase(): String s = "Habe die Ehre"; // Umwandlung in Großbuchstaben String s1 = s.toUpperCase(); // s1 enthält "HABE DIE EHRE" // Umwandlung in Kleinbuchstaben String s2 = s.toLowerCase(); // s2 enthält "habe die ehre"

Wie immer bei immutable Objekten gilt, dass nicht der Original-String verändert, sondern eine veränderte Kopie als neues Objekt zurückgeliefert wird. Suchen in Strings

Für das Suchen von Zeichenketten in Strings stellt die Klasse String ebenfalls eine Reihe von Methoden zur Verfügung. indexOf() sowie lastIndexOf() suchen nach der angegebenen Zeichenkette bzw. nach dem angegebenen Zeichen und geben den Index des ersten Zeichens eines Treffers im zu durchsuchenden String als int-Wert zurück, Wird kein Treffer gefunden, dann geben beide Methoden -1 zurück. startsWith() bzw. endsWith() prüfen, ob der String mit der angegebenen Zeichenkette beginnt oder endet. Diese Methoden geben einen boolean-Wert zurück.

Alle Methoden unterscheiden zwischen Groß- und Kleinschreibung, sie sind also casesensitive: int indexOf( int ch ) int indexOf( int ch, int fromInd ) int indexOf( String s )

Wichtige Klassen

int indexOf( String s, int fromInd ) int int int int

lastIndexOf( lastIndexOf( lastIndexOf( lastIndexOf(

int ch ) int ch, int fromInd ) String s ) String s, int fromInd )

boolean startsWith( String prefix ) boolean startsWith( String prefix, int fromInd ) boolean endsWith( String suffix )

er ein paar Beispiele für die Suche in Strings String s = "Das ist das Haus vom Nikolaus"; // 01234567890123456789012345678 // 1.: indexOf( String ) // erstes Auffinden der Zeichenkette "das" int i = s.indexOf( "das" ); // i enthält 8, das ist der Index des ersten Zeichens // im Treffer der Suche ("d" von "das") // Suche, die keinen Treffer erzielt i = s.indexOf( "bla" ); // i enthält -1 // Suche nach einem einzelnen Zeichen i = s.indexOf( 'a' ); // i enthält 1, weil das erste 'a' den Index 1 hat. // Obwohl ein Argument vom Typ int erwartet wird, // darf man ein char-Argument angeben, da der Compiler // eine automatische Typ-Konvertierung durchführt. // Man hätte natürlich auch eine Variable verwenden // können: char ch = 'a'; i = s.indexOf( ch ); // Suche in einer Schleife, um die Anzahl der // Treffer im String zu ermitteln int cnt = 0; for ( int pos = 0; pos >= 0; cnt++ ) { pos = s.indexOf( "s", pos ); // Start-Index für die nächste Suche setzen, damit

Wichtige Klassen

// keine Endlos-Schleife entsteht. if ( pos >= 0 ) { pos += "s".length(); } } // 2.: indexOf( String, int ) // dito, diesmal mit einer Variante von indexOf() // Die Suche beginnt ab dem angegebenen Index i = s.indexOf( "aus", 14 ); // i enthält 26 ("a" von "aus" in "Nikolaus") // statt eines Strings als Suchmuster kann auch // ein Zeichen im int-Format angegeben sein. // 3.: indexOf( int ) i = s.indexOf( 0x20 ); // i enthält 3, das ist der Index des ersten Blanks // 4.: indexOf( int, int ) i = s.indexOf( 0x20, 4 ); // i enthält 7 (Index des zweiten Blanks) // // // // //

Es gibt keine Beschränkung für den Start-Index. Ist dieser negativ, dann wird dasselbe Resultat erzielt, als hätte man 0 angegeben. Ist er größer als der Index des letzten Zeichens, dann wird immer -1 zurückgeliefert.

// // // // // // // //

Für alle gezeigten Varianten gibt es eine entsprechende Methode lastIndexOf(), die nicht von links nach rechts sucht, sondern beginnend beim letzten Zeichen des Strings nach links sucht. 1.: lastIndexOf( String ) 2.: lastIndexOf( String, int ) 3.: lastIndexOf( int ) 4.: lastIndexOf( int, int )

// // // // //

Es gibt keine Beschränkung für den Start-Index. Ist dieser negativ, dann wird immer -1 zurückgeliefert. Ist er größer als der Index des letzten Zeichens, dann wird dasselbe Resultat erzielt, als hätte man den Index des letzten Zeichens angegeben.

boolean flag = s.startsWith( "D" );

Wichtige Klassen

// flag ist true flag = s.startsWith( "d" ); // flag ist false flag = s.endsWith( "s" ); // flag ist true flag = s.endsWith( "S" ); // flag ist false

rings vergleichen

Meist möchte man zwei Strings direkt auf Gleichheit überprüfen, zu diesem Zweck llt die Klasse String die beiden Methoden equals() und equalsIgnoreCase() zur rfügung: boolean equals( Object o ) boolean equalsIgnoreCase( String s )

ispiele für equals() und equalsIgnoreCase(): String s1 = "hallo"; String s2 = "Hallo"; // 1.: equals boolean flag = s1.equals( s2 ); // flag ist false, weil der Vergleich case-sensitive // ist. // es geht auch anders herum flag = s2.equals( s1 ); // oder mit einer if-Abfrage if ( s1.equals( s2 ) ) { flag = true; else { flag = false; // man kann natürlich auch mit einem String-Literal // vergleichen if ( s1.equals( "hallo" ) ) { flag = true;

Wichtige Klassen

} // 2.: equalsIgnoreCase flag = s1.equalsIgnoreCase( s2 ); // flag ist true, weil der Vergleich case-insensitive // ist.

Interessanterweise erwartet equals() eine Instanz der Klasse Object als Argument, während equalsIgnoreCase() einen String als Parameter besitzt. Dieser Umstand besitzt jedoch keinerlei Bedeutung, beide Methoden der String-Klasse dürfen nur auf Strings angewendet werden. Zwei weitere Methoden, compareTo() und compareToIgnoreCase(), werden meist für die Sortierung von Strings verwendet. Beide Methoden liefern entweder eine negative Zahl, 0 oder eine positive Zahl zurück, je nachdem, ob das String-Objekt, über welches die Methode aufgerufen wurde, lexikalisch kleiner, gleich oder größer als das Argument ist: int compareTo( Object o ) int compareTo( String s ) int compareToIgnoreCase( String s )

Beispiele für compareTo() und compareToIgnoreCase(): String s1 = "als"; String s2 = "Alster"; // 1.: compareTo int comp = s1.compareTo( s2 ); // comp enthält eine positive Zahl, da der Vergleich // case-sensitive ist, "als" kommt lexikalisch nach // "Alster". // 2.: compareToIgnoreCase comp = s1.compareTo( s2 ); // comp enthält eine negative Zahl, da der Vergleich // case-insensitive ist, "ALS" kommt lexikalisch vor // "ALSTER".

Wichtige Klassen

hiffbruch erleidet man bei Strings mit Umlauten: String s1 = "Über"; String s2 = "Ulme"; int comp = s1.compareTo( s2 ); // comp sollte eigentlich negativ sein, weil // "Über" lexikalisch vor "Ulme" kommt. // Der Vergleich verwendet jedoch keine Locale // und liefert deshalb eine positive Zahl.

ir werden später noch sehen, wie man diese Unschönheit abstellt, wenn wir über die Klassen Arrays und Collator sprechen. Mit der Methode contentEquals() kann man einen String mit einem Objekt der asse StringBuffer vergleichen, die wir als Nächstes besprechen werden: boolean contentEquals( StringBuffer sb )

ispiel für contentEquals(): String s = "hallo"; StringBuffer sb = new StringBuffer( "Hallo" ); boolean flag = s.contentEquals( sb ); // flag ist false, da die Strings case-sensitive // verglichen werden und somit unterschiedlich sind.

Mit der Methode regionMatches() können zwei Teil-Strings miteinander verglichen rden: boolean regionMatches( int off1, String s2, int off2, int len

boolean regionMatches( boolean case, int off1, String s2, int off2, int len

Wichtige Klassen

Die Methode vergleicht Teil-Strings des aktuellen Objekts und des als zweiten Parameter angegebenen String-Objekts s2 miteinander. Für den Vergleich wird jeweils ein Teil-Bereich der Strings mit der angegebenen Länge len verwendet. Der Start-Index des zu vergleichenden Bereichs beginnt im aktuellen Objekt bei Index off1, beim angegebenen Objekt bei Index off2. Mit der zweiten Variante kann man angeben, ob der Vergleich case-sensitive sein soll oder nicht. Ist case true, dann erfolgt ein case-sensitiveVergleich, ansonsten wird nicht zwischen Groß- und Kleinschreibung unterschieden. Beispiel für regionMatches(): String s1 = "Das ist das Haus vom Nikolaus"; // 01234567890123456789012345678 String s2 = "das Haus sieht so schön aus"; // 012345678901234567890123456 boolean flag = s1.regionMatches( 8, // Offset in s1 s2, 0, // Offset in s2 8 // Länge für den Vergleich ); // flag ist true, weil "das Haus" von String s1 // ab Offset 8 // mit "das Haus" von String s2 ab Offset 0 miteinander // verglichen werden. // Der Vergleich ist case-sensitive. // Hätte man als Länge den Wert 10 verwendet, wäre // das Ergebnis false, weil "das Haus v" mit // "das Haus s" miteinander verglichen worden wären.

Zeichen in Strings ersetzen

Will man in einem String ein einzelnes Zeichen durch ein anderes ersetzen, verwendet man die Methode replace(): String replace( char oldC, char newC )

Beispiel für replace(): // Alle Vorkommnisse eines Zeichens ersetzen String s = "7 + 7 + 7 + 7";

Wichtige Klassen

String s1 = s.replace( '7', '3' ); // s1 enthält "3 + 3 + 3 + 3"

Konvertierung von Strings in primitive Datentypen

Mit Hilfe von getBytes() und getChars() kann ein String in ein Array eines primitien Datentyps umgewandelt werden: byte[] getBytes() byte[] getBytes( String charset ) void getChars( int srcBegin, int srcEnd, char[] dst, int dstBegin

arset muss ein String für einen unterstützten Zeichen-Code sein, z.B. "ISO-8859-1". End ist der Index nach dem letzten zu kopierenden Zeichen. ispiele für getBytes() und getChars(): String s = "12345"; byte[] b1 = s.getBytes(); // b1 enthält nicht etwa die Dezimalzahlen // { 1, 2, 3, 4, 5, }, sondern vielmehr den // Zeichencode der Ziffern // { 49, 50, 51, 52, 53, } (dezimal) char[] ca = new char[ 3 ]; s.getChars( 0, 3, ca, 0 ); // ca enthält die ersten drei Zeichen des Strings

e Wrapper-Klassen für die primitiven Datentypen bieten statische Methoden an, die einen String in den gewünschten Datentyp umwandeln: static static static static static static

long Long.parseLong( String s ) long Long.parseLong( String s, int radix ) int Integer.parseInt( String s ) int Integer.parseInt( String s, int radix ) short Short.parseShort( String s ) short Short.parseShort( String s, int radix )

Wichtige Klassen

static static static static

byte Byte.parseByte( String s ) byte Byte.parseByte( String s, int radix ) float Float.parseFloat( String s ) double Double.parseDouble( String s )

Alle aufgeführten Methoden können eine NumberFormatException auslösen, wenn die Umwandlung unmöglich ist. Konvertierung von primitiven Datentypen in Strings

Die String-Klasse bietet einige statische Methoden für die Konvertierung primitiver Datentypen in Strings an: static String valueOf( static String valueOf( static String valueOf( static String valueOf( char[] carray, int ) static String valueOf( static String valueOf( static String valueOf( static String valueOf( static String valueOf(

boolean b ) char c ) char[] carray ) fromInd, int len double d ) float f ) int i ) long l ) Object o )

Da diese Methoden statisch sind, muss man keine Instanz der String-Klasse erzeugen, um sie aufrufen zu können. Hier ein paar Beispiele: boolean b = true; String s = String.valueOf( b ); // s enthält "true" char[] ca = new char[] { 'a', 'b', 'c' }; s = String.valueOf( ca ); // s enthält "abc" Integer intObj = new Integer( 0x41 ); s = String.valueOf( intObj ); // s enthält "65", nicht etwa "A"

Wichtige Klassen

it der Version 1.4 des JDK sind in der String-Klasse einige weitere Methoden für ttern Matching und reguläre Ausdrücke hinzugekommen. Wir werden diese Methoden im Kapitel Pattern Matching besprechen.

2

java.lang.StringBuffer

Zusätzlich zur Klasse String haben die Java-Designer eine weitere Klasse zur Bearbeing von Zeichenketten vorgesehen: StringBuffer. Sie bietet zwar nicht so viele ethoden an wie String, ist jedoch wesentlich performanter und sollte immer dann rwendet werden, wenn man den Inhalt eines Strings ändern möchte. Dies ist ja, wie r gelernt haben, bei Objekten der String-Klasse unmöglich, da diese immutable nveränderbar) sind. StringBuffer-Objekte zeichnen sich dadurch aus, dass ihre Kapazität dynamisch ver-

ndert werden kann, ohne dass dadurch eine neue Instanz erzeugt werden muss. hen wir uns zunächst die Konstruktoren von StringBuffer an: 2.1

Konstruktoren von java.lang.StringBuffer

StringBuffer()

Default-Konstruktor. Initial wird eine Kapazität von 16 Zeichen reserviert. StringBuffer( int len )

Mit diesem Konstruktor kann die initiale Kapazität beim Erzeugen einer neuen Instanz festgelegt werden. StringBuffer( String s )

Beim Erzeugen einer neuen Instanz mit diesem Konstruktor wird der Inhalt des angegebenen Strings in den Puffer kopiert. Die initiale Kapazität ist genauso groß wie die Anzahl der Zeichen des Strings. er Vorteil gegenüber der String-Klasse ist, dass beim Hinzufügen von Zeichen eine neue Instanz angelegt und die Zeichen kopiert werden müssen, sondern nur uer Speicherplatz belegt wird, falls die Kapazität des Puffers erschöpft ist. Auf der D-ROM finden Sie in der Datei StringBufferTest01.java ein Beispiel, mit dem der rformance-Gewinn gegenüber Strings deutlich wird. Wenn wir das Programm bersetzen und ausführen, ergibt sich ein ziemlich eindeutiges Bild (Achtung: Es daut ca. drei Minuten, bis das Programm zu Ende ist, Sie müssen sich also in Geduld ben, drücken Sie nicht (STRG)-(C), weil Sie glauben, das Programm sei abgestürzt):

Wichtige Klassen

Abbildung 5.1: Performance-Vergleich zwischen String und StringBuffer

Zunächst fällt der krasse Geschwindigkeits-Unterschied auf. Mehr als zwei Minuten dauert die Verarbeitung für das Objekt der Klasse String, bei Verwendung des StringBuffer-Objekts sind wir bereits nach 20 Millisekunden fertig. Ziemlich deutlich, was? »Und was lernen wir daraus?«: So oft wie möglich StringBuffer verwenden! Bevor Sie sich wundern, warum die Kapazität des StringBuffer-Objekts am Ende des Tests mehr als 50000 Zeichen ist (es werden im Laufe des Tests genau 50000 Zeichen zu je 2 Byte Speicherplatz im Objekt gespeichert), hier die Auflösung: Initial besitzt der Puffer eine Kapazität von 16 Zeichen. Nach dem Einstellen von 16 Zeichen ist der Puffer voll und es wird ein neuer Puffer angelegt, der die doppelte Größe des vorherigen hat (plus 2 Zeichen für die Puffer-Verwaltung). Die im bisherigen Puffer enthaltenen Zeichen werden in den neuen Puffer kopiert. So geht es weiter, bis der Puffer schließlich die Größe von 73726 Zeichen hat. Ich glaube, dieses kleine Beispiel hat deutlich gemacht, dass sich der Einsatz von StringBuffer-Objekten in vielen Fällen lohnt. Sehen wir uns nun einige Methoden der Klasse an: 5.2.2

Methoden von java.lang.StringBuffer

Zusätzlich zu den bereits von String-Objekten her bekannten Methoden (indexOf() etc.) stellt die Klasse StringBuffer Methoden für das Anhängen, Einfügen sowie Löschen von Zeichen zur Verfügung (Erinnern wir uns: Im Gegensatz zu StringObjekten kann der Inhalt von StringBuffer-Objekten verändert werden.). Im Folgenden möchte ich Ihnen eine Liste der verfügbaren Methoden zeigen: Einfügen und Löschen von Zeichen StringBuffer append( boolean b ) StringBuffer append( char c ) StringBuffer append( char[] ca )

Wichtige Klassen

StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer

append( append( append( append( append( append( append( append(

char[] ca, int off, int len ) double d ) float f ) int i ) long l ) Object o ) String s ) StringBuffer sb )

StringBuffer StringBuffer StringBuffer StringBuffer int off,

insert( int off, boolean b ) insert( int off, char c ) insert( int off, char[] ca ) insert( char[] ca, int off1, int len

StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer StringBuffer

insert( insert( insert( insert( insert( insert(

int int int int int int

off, off, off, off, off, off,

double d ) float f ) int i ) long l ) Object o ) String s )

StringBuffer delete( int start, int end ) StringBuffer deleteCharAt( int ind )

Ich glaube, die meisten der gezeigten Methoden sind selbst erklärend und können als ausaufgabe von Ihnen alleine gemeistert werden. Von meiner Seite nur ein paar Anmerkungen: Die Methode append() hängt grundsätzlich Zeichen am Ende des Puffers an. Im Falle eines char-Arrays kann man den Start-Index und die Anzahl der zu kopierenden Zeichen als Parameter angeben. Wird der Methode eine Instanz der Klasse Object übergeben, dann führt die Virtual Machine die Methode toString() des Objekts aus und hängt das Ergebnis am Ende des Puffers an. Das erste Argument der insert()-Methoden ist der Index des Puffers, an dem die Zeichen eingefügt werden. Ist der Index z.B. 0, dann werden die neuen Zeichen am Beginn des Puffers eingefügt. Will man etwas am Ende anhängen, darf man nicht den Index des letzten Zeichens im Puffer angeben, sondern die Länge des Puffers (obj.length()).

Wichtige Klassen



Das erste Argument der delete()-Methode gibt den Start-Index an, ab dem Zeichen aus dem Puffer entfernt werden sollen. Das zweite Argument gibt nicht den Index des letzten zu entfernenden Zeichens an, sondern den Index des darauf folgenden Zeichens. delete( 0, 2 ) entfernt also die ersten 2 Zeichen. Ist das zweite Argument größer als der Index des letzten Zeichens, dann werden Zeichen vom angegebenen Start-Index bis zum Ende des Puffers entfernt.



Speziell die Methoden insert() und delete() bzw. deleteCharAt() können Runtime-Exceptions auslösen, wenn die angegebenen Indizes keinen Sinn ergeben.

Methoden für Kapazität und Länge des Puffers

Verwechseln Sie bitte nicht Kapazität (Methode capacity()) und Länge eines StringBuffer-Objekts (Methode length()). Die Länge entspricht der Gesamtzahl aller Zeichen, die aktuell im Objekt gespeichert sind. Die Kapazität jedoch enthält die aktuelle Puffergröße des Objekts, diese ist immer größer oder gleich der Anzahl enthaltener Zeichen. Werden neue Zeichen in den Puffer aufgenommen, die nicht mehr in den aktuellen Puffer hineinpassen, dann wird ein neuer Puffer angelegt, der ungefähr doppelt so groß ist wie der bisherige, der Puffer-Inhalt in den neuen Puffer kopiert, die neuen Zeichen angehängt und die Referenz-Variable auf den neuen Puffer gesetzt. Mit der Methode setLength() kann man die Anzahl der Zeichen explizit setzen. Die Methode ensureCapacity() kann verwendet werden, um die Kapazität eines bereits instanzierten Objekts zu erhöhen. Sehen wir uns zunächst die Definitionen der Methoden an: int capacity() void ensureCapacity( int min ) int length() void setLength( int newLen )

Ich denke, hier ist ein kleines Beispiel angebracht: // StringBuffer mit einer initialen Kapazität von // 2 Zeichen erzeugen. StringBuffer sb = new StringBuffer( 2 ); // 3 Zeichen hinzufügen

Wichtige Klassen

sb.append( "123" ); // Da der Puffer initial nur Platz für 2 Zeichen hat, // wird er vergrößert auf ( 2 + 1 ) * 2 = 6. // Nach der Operation ist die Kapazität 6, die Länge 3. // capacity() = 6, length() = 3 // Jetzt hängen wir weitere 4 Zeichen an. sb.append( "4567" ); // Wiederum geht der Platz im Puffer aus, denn die // aktuelle Kapazität beträgt nur 6 Zeichen, insgesamt // sind es nun aber 7 Zeichen. // Also wird die Kapazität erneut vergrößert // ( 6 + 1 ) * 2 = 14 // capacity() = 14, length() = 7 // Nun setzen wir explizit die Länge auf 5 sb.setLength( 5 ); // Die Kapazität wird nicht verändert, der Puffer jedoch // wird abgeschnitten, so dass nur noch die ersten // 5 Zeichen enthalten sind // capacity() = 14, length() = 5 // Jetzt setzen wir die Länge neu sb.setLength( 20 ); // Die ersten 5 Zeichen bleiben erhalten, alle // darauf folgenden 15 Zeichen werden mit // \u0000 belegt. // Zunächst wird die neue Kapazität errechnet // ( 14 + 1 ) * 2 = 30 // Da dieser Wert größer ist als die neue Länge // (20), wird die Kapazität auf 30 geändert // capacity() = 30, length() = 20 // Wir ändern die Länge neu, diesmal so, dass sich // ein größerer Wert ergibt als ( 30 + 1 ) * 2 sb.setLength( 65 ); // Nun wird dieser Wert für die neue Kapazität // verwendet // capacity() = 65, length() = 65 // Wir erhöhen die Kapazität, ohne dass Speicher // reserviert wird. sb.ensureCapacity( 70 ); // Die Kapazität wird hier nicht auf 70 erhöht,

Wichtige Klassen

// // // //

sondern nach der Formel ( 65 + 1 ) * 2 neu gesetzt, weil die angegebene Kapazität kleiner ist als 132. capacity() = 132, length() = 65

// Dasselbe noch mal, doch jetzt ist das Argument // größer als der berechnete Wert 266 sb.ensureCapacity( 300 ); // capacity() = 300, length() = 65

Ersetzen von Zeichen

Im Gegensatz zur Klasse String, wo man nur ein einzelnes Zeichen ändern konnte (und dabei wurde eine komplett neue Instanz der Klasse erzeugt), ist es mit der StringBuffer-Methode replace() möglich, einen ganzen Bereich des Puffers zu ändern: StringBuffer replace( int start, int end, String s )

Zunächst werden alle Zeichen einschließlich des Indizes start bis ausschließlich des Indizes end entfernt, anschließend werden die Zeichen des angegebenen Strings s stattdessen eingefügt. Liegt der Index end außerhalb des Puffers, dann werden alle Zeichen von Index start bis zum Ende des Puffers entfernt. Liegt aber der Index start außerhalb des Puffers oder hinter end, dann setzt es eine StringIndexOutOfBoundsException. Puffer-Inhalt umkehren

Mit der Methode reverse() kann man die Reihenfolge der Zeichen im Puffer umkehren: StringBuffer reverse()

Aus "123" wird also "321". Einzelnes Zeichen ersetzen

Ein beliebiges Zeichen kann man mit der Methode setCharAt() ersetzen: void setCharAt( int ind, char c )

Wichtige Klassen

ie bei allen Methoden, bei denen man einen Index angeben kann, wird eine IndexOutOfBoundsException ausgelöst, wenn der Index keinen Sinn ergibt.

Umwandlung in einen String

ie bei fast allen Klassen ist auch bei StringBuffer die Methode toString() implementiert. Sie wird übrigens vom Compiler dazu verwendet, um den Operator + zu berladen: String s = "1" + "a"; // Diese Zuweisung wird vom Compiler übersetzt als String s = new StringBuffer().append( 1 ).append( "a" ).toString();

3

java.util.Properties

einigen Beispielen haben wir beim Aufruf des Programms Kommandozeilengumente angegeben, um das Verhalten des Programms von außen steuern zu nnen. Der Grund dafür ist klar: Wenn wir zum Beispiel die Anzahl von SchleifenDurchläufen eines Test-Programms ändern, dann wollen wir nicht jedes Mal das a-Programm neu kompilieren. Die Anpassung von Programmparametern ist eichbedeutend mit dem Einstellen bestimmter Eigenschaften (englisch Properties). undsätzlich sollten alle Programmparameter, die sich ändern können (wenn man B. das Programm auf unterschiedlichen Rechnern mit verschiedenen Datei-Systemen installieren möchte), nicht hart verdrahtet im Source-Code stehen, sondern auslagert werden (z.B. in eine Konfigurations-Datei oder eine Datenbank). Man nnte natürlich das Programm auch nur über Kommandozeilen-Argumente beim Aufruf der Virtual Machine anpassen, häufig ist die Zahl der einstellbaren Parameter groß, dass dafür Kommandozeilen-Argumente nicht mehr in Frage kommen. Anhand eines kleinen Beispiels möchte ich Ihnen zeigen, warum man Properties benötigt: 01 02 03 04 05 06 07

// Datei PropertiesTest01.java // Das Programm liest einen Geldbetrag ein // (als Kommandozeilen-Argument) // und berechnet den Brutto-Preis mit MwSt. public class PropertiesTest01 { // MwSt-Satz 16%

Wichtige Klassen

08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 }

private static double mwst = 0.16; // Währungs-Einheit private static String currency = "EUR"; public // // if

static void main( String[] args ) { Das erste Kommandozeilen-Argument muss eine Gleit- oder Fest-Kommazahl sein. ( args.length == 0 ) { usage(); System.exit( 1 );

} // Der Netto-Preis wird als Kommandozeilen// Argument angegeben. // Wir rechnen diesen Betrag in die kleinste // Währungs-Einheit um. double netPrice = Double.parseDouble( args[ 0 ] ) * 100; // Berechnung des Faktors inklusive MwSt. double fac = 1.0 + mwst; // Berechnung des Brutto-Preises double mwstPrice = Math.round( netPrice * fac ) / 100.0; // Ausgabe der Währung und des Brutto// Preises System.out.println( "Brutto = " + currency + " " + mwstPrice ); } private static void usage() { System.err.println( "Aufruf: PropertiesTest01 " ); }

Zur Erläuterung: Das Programm liest einen Betrag in Euro ein (z.B. 14.6) und berechnet daraus den Brutto-Preis inklusive Mehrwertsteuer. Die Formel dafür lautet allgemein:

Wichtige Klassen

brutto = netto * ( 1 + ( MwSt / 100 ) )

Da wir den Mehrwertsteuer-Satz (hier 16%) bereits durch 100 geteilt haben, reduziert h die Berechnung des Faktors, mit dem wir den Netto-Preis multiplizieren müssen, auf 27

double fac = 1.0 + mwst;

Den eingegebenen Wert in Euro rechnen wir in die kleinste Währungseinheit Cent um, damit wird die notwendige Rundung des Brutto-Preises auf ganze Cent einfaer. Deshalb müssen wir in der Zeile 30

Math.round( netPrice * fac ) / 100.0;

ch dem Runden den Brutto-Preis wieder durch 100 teilen, um Euro zu erhalten. wohl den Mehrwertsteuer-Satz als auch die Währung haben wir fest verdrahtet im ogrammcode definiert. Was aber, wenn sich der Mehrwertsteuer-Satz ändert? Wir e wissen, wie schnell so etwas gehen kann. Nun, in diesem Fall bleibt uns nichts deres übrig, als das Programm zu ändern und erneut zu kompilieren. Jetzt stellen Sie sich vor, unser Programm sei ein winziger Bestandteil einer großen Applikation, die auf CD-ROM in großer Zahl verkauft wird. Immense Kosten für die Herstellung d den Vertrieb der neuen CD-ROMs wären die Folge. Ich denke, jetzt ist allen klar, dass es so in einer modernen Programmier-Technik nicht funktionieren kann. Java bietet mit der Klasse Properties eine elegante Methode, anpassbare Programmeigenschaften in eine Textdatei auszulagern. So könn sogar mehrere Programme oder Programmteile in ihrem Verhalten angepasst wern, ohne dass man den Java-Code ändern muss. Der Aufbau einer Properties-Datei in der einfachsten Form ist denkbar simpel: Jede einstellbare Eigenschaft besteht aus einem Key (neudeutsch für Schlüssel) und einem ue (neudeutsch für Wert), die durch ein Gleichheitszeichen oder durch einen Dopelpunkt getrennt werden. Pro Zeile kann man eine Eigenschaft angeben, z.B.: title = Programmüberschrift rootDir = /tmp/myDir timeout : 100 s logFile:out.log

Wichtige Klassen

Achtung In keinem Fall dürfen am Ende einer Zeile Leerzeichen stehen, da sie sonst Bestandteil des Value sind und fast ausnahmslos zu Fehlern führen, die mit einem normalen Text-Editor nicht sofort zu erkennen sind. Leider fügen sehr viele Editoren unter Windows Leerzeichen am Ende der Zeile unbemerkt ein. Folgende allgemeinen Regeln gelten für Properties-Dateien: 

Als Zeilen-Ende werden die Zeichen \n, \r\n und \r erkannt.



Alle Zeilen, die leer sind oder deren erstes Nicht-Leerzeichen entweder # oder ! ist, werden ignoriert (Kommentar-Zeilen).



Alle Zeichen nach dem ersten Nicht-Leerzeichen bis ausschließlich des Gleichheitszeichens oder des Doppelpunkts werden als Key interpretiert (Leerzeichen zwischen Key und Gleichheitszeichen bzw. Doppelpunkt sind nicht Bestandteil des Keys).



Sonderzeichen wie Leerzeichen, TAB-Zeichen, Gleichheitszeichen oder Doppelpunkt können literal als Bestandteil des Keys durch Voranstellen eines Backslash angegeben werden (Dieses Feature sollte man jedoch nicht nutzen.).



Alle Leerzeichen nach dem Gleichheitszeichen oder Doppelpunkt werden ignoriert und der Rest der Zeile (inklusive etwaiger Leerzeichen am Ende einer Zeile) wird als Value interpretiert.



Sonderzeichen wie \n, \r, \r\n, \t, \\, \", \', \uxxxx (Unicode) können literal (also mit Backslash) im Value angegeben sein und werden in das jeweilige Sonderzeichen konvertiert (aus \u0041 wird das Zeichen A).



Der Value kann sich über mehrere Zeilen erstrecken, in diesem Fall muss durch einen Backslash am Ende der Zeile die Fortsetzung gekennzeichnet werden. Der Backslash sowie alle führenden Leerzeichen der nächsten Zeile werden nicht in den Value mit aufgenommen.



Erscheint derselbe Key mehrfach, dann überschreibt die letzte Definition des Keys alle vorangegangenen Definitionen dieses Keys.



Alle Properties-Dateien sollten die Endung .properties haben.

Beispiele für Properties: # Kommentar ! ebenfalls Kommentar # Die nächste Zeile ist eine Leerzeile und

Wichtige Klassen

# wird ebenfalls ignoriert # Key mit Blanks und Doppelpunkt im Key selbst key\ mit\ Leerzeichen\ und\ Doppelpunkt\: = value # Value mit Fortsetzung # der Value enthält den String "a, b, c" continuedKey = a, \ b, \ c # Mehrfach-Key (am Ende enthält der Key den # String "last" als Value) errorKey = first errorKey : middle errorKey=last

, nachdem wir nun im Groben wissen, was sich hinter dem Begriff Property vergt, sehen wir uns die Klasse Properties näher an. 3.1

Konstruktoren von java.util.Properties

e Klasse bietet neben dem Default-Konstruktor Properties() noch einen zweiten n, bei dem man ein bereits instanziertes Objekt derselben Klasse übergeben kann: Properties() Properties( Properties default )

Das default-Objekt wird verwendet, falls ein Property-Key in der Properties-Datei nicht gefunden werden kann. 3.2

Methoden von java.util.Properties

sen von Properties

Mit Hilfe der Methode load() kann der Inhalt einer Properties-Datei gelesen werden: void load( InputStream is ) throws IOException

Wichtige Klassen

Die Methode load() erwartet als Argument ein Objekt der Klasse InputStream. Diese jedoch bietet keinen Konstruktor mit einem Pfad-Namen als Argument an. Deshalb muss man stattdessen ein FileInputStream-Objekt instanzieren. Auf der CD-ROM finden Sie in der Datei PropertiesTest02.java ein Beispiel, das im aktuellen Verzeichnis eine Properties-Datei namens myProps.properties einliest. Lesen eines Property-Werts

Um den Wert eines bestimmten Property-Keys zu lesen, verwendet man die Methode getProperty(): String getProperty( String key ) String getProperty( String key, String defaultValue )

Diese Methode sucht nach dem durch key angegebenen Property und liefert den zugehörigen Value als String zurück. Optional kann ein defaultValue angegeben sein, der verwendet wird, wenn kein Property mit dem Namen key gefunden wurde. Die Methode liefert null zurück, wenn kein Property mit dem angegebenen Namen gefunden werden konnte und kein Default-Value angegeben war. Alle Keys lesen

Manchmal benötigt man eine Liste aller im Properties-Objekt abgelegten Keys, dafür kann die Methode propertyNames() benutzt werden. Sie liefert eine Instanz des Interfaces Enumeration zurück, die wir im nächsten Kapitel näher kennen lernen werden. Anschließend kann man über die einzelnen Elemente der Liste iterieren und die zugehörigen Values auslesen. Die Klasse Properties bietet zudem weitere Methoden an, auf deren Beschreibung ich allerdings hier verzichten möchte, da die beiden gezeigten Methoden in den meisten Fällen ausreichend sind. Ein interessanter Aspekt ergibt sich, wenn man zum Laden von Properties nicht einen normalen InputStream verwendet, der aus einer Datei liest, sondern dafür eine httpVerbindung zu einem Web-Server benutzt. Damit kann man verschiedene Properties zentral auf einem Rechner verwalten.

Wichtige Klassen

Hinweis Interessanterweise bietet die Klasse Properties keine Unterstützung für Multi-Value Properties, darunter versteht man Property-Definitionen, die mehrach mit demselben Key, aber mit unterschiedlichen Werten vorkommen. Hierfür muss man sich etwas anderes einfallen lassen (zum Beispiel unter Verwendung der Klasse StringTokenizer, die wir gleich kennen lernen werden).

4

java.util.StringTokenizer

i der Beschreibung der Klasse Properties haben wir ein Manko gesehen, das es uns nicht ermöglicht, Multi-Value Properties zu lesen. Die folgende Properties-Datei hrt folglich zu Überraschungen: title = Test title = Mein Test

wird nicht etwa ein Array ausgelesen, das zwei Elemente besitzt, sondern die letzte Definition von title in der Properties-Datei überschreibt alle vorangegangenen, wir halten also "Mein Test". Wir können den Fehler umgehen, indem wir die Einzelrte von title mit einem selbst gewählten speziellen Trennzeichen in eine Definition schreiben: title = Test::Mein Test

enn wir nun das Property title auslesen, müssen wir selbst dafür sorgen, dass aus den Value angegebenen Werten ein Multi-Value-Array wird. Bei dieser Aufgabe untertzt uns die Klasse StringTokenizer, mit der wir einen String in einzelne Teile, so nannte Tokens, zerlegen können. 4.1

Konstruktoren von java.util.StringTokenizer

sgesamt stehen drei verschiedene Konstruktoren zur Verfügung: StringTokenizer( String s ) StringTokenizer( String s, String delim ) StringTokenizer( String s, String delim, boolean incDelim

Wichtige Klassen

Während man bei den letzten beiden Varianten im Konstruktor zusätzlich zum String s, der in einzelne Bestandteile (Tokens) zerlegt werden soll, ein Trennzeichen delim (genauer gesagt eine Zeichenklasse aller Trennzeichen) angeben kann, wird bei der ersten Variante eine Default-Zeichenklasse als Trennzeichen verwendet. Diese besteht aus den White Space-Zeichen ' ', '\t', '\n', '\r', '\f'. Im Klartext bedeutet dies, dass jedes einzelne dieser Zeichen zu einer Trennung führt: "a b\tc\nd" wird also zu "a", "b", "c", "d"

Um es noch einmal klarzumachen: Gibt man zum Beispiel als Trennzeichen den String "ernstl" an, dann führen alle in diesem String enthaltenen Zeichen unabhängig voneinander zu einer Worttrennung, nicht der gesamte String. Die angegebenen Trennzeichen werden case-sensitive bewertet, d.h. zwischen Groß- und Kleinschreibung wird unterschieden. Bei der letzten Konstruktor-Variante kann man als dritten Parameter ein Flag incDelim übergeben. Ist dieses Flag true, dann werden auch die Trennzeichen selbst in die Liste der Tokens mit aufgenommen, ansonsten werden sie zwar für die Trennung in einzelne Tokens verwendet, anschließend aber verworfen. Hier ein paar Beispiele für die Benutzung der Konstruktoren: String s = "in Minga stäht a Hofbreihaus"; StringTokenizer stk = new StringTokenizer( s ); // stk enthält die Tokens "in", "Minga", "stäht", // "a", "Hofbreihaus". stk = new StringTokenizer( s, "H" ); // stk enthält die Tokens "in Minga stäht a ", // "ofbreihaus". stk = new StringTokenizer( s, "hH" ); // stk enthält die Tokens "in Minga stä", "t a ", // "ofbrei", "aus". stk = new StringTokenizer( s, " ", true ); // stk enthält die Tokens "in", " ", "Minga", " ", // "stäht", " ", "a", " ", "Hofbreihaus".

Wichtige Klassen

4.2

Methoden von java.util.StringTokenizer

Abfrage-Methoden für die Tokens

e Klasse bietet drei Methoden an, um die Anzahl der im Objekt enthaltenen okens zu ermitteln bzw. abzufragen, ob es weitere Tokens gibt: int countTokens() boolean hasMoreElements() boolean hasMoreTokens()

e Methode countTokens() liefert die aktuelle Anzahl von Tokens im Objekt zurück. e sollte vor der Schleife aufgerufen werden, in der die einzelnen Tokens gelesen rden, da die Anzahl mit jedem gelesenen Token verringert wird. e Methoden hasMoreElements() sowie hasMoreTokens() bewirken dasselbe und lien true zurück, wenn noch weitere Tokens vorhanden sind, andernfalls false. Methoden zum Auslesen von Tokens

u guter Letzt sehen wir uns nun die Methoden an, mit denen die einzelnen Tokens ch dem Aufspalten eines Strings ausgelesen werden können: String nextToken() String nextToken( String newDelim ) Object nextElement()

e wohl am häufigsten benutzte Methode nextToken() ist in der ersten Variante zeigt. Falls weitere Tokens vorhanden sind, liefert die Methode das nächste Element r Liste als String zurück. Dabei wird die Anzahl der verbleibenden Tokens verrinrt, dies wirkt sich auf einen anschließenden Aufruf der Methode countTokens() aus. Mit der zweiten Variante kann man explizit eine neue Zeichen-Klasse für das Aufspaln des Strings angeben. Diese Methode wird aber selten benutzt. Die letzte Variante nextElement() schließlich ist hauptsächlich deshalb vorhanden, weil die Klasse das terface Enumeration implementiert. , nun wollen wir die Klasse in Aktion treten lassen, indem wir unser Programm zur rechnung des Brutto-Preises erweitern, das wir bei der Beschreibung der Klasse Properties verwendet haben. Die Erweiterung besteht zum einen darin, dass wir nun

Wichtige Klassen

den Mehrwertsteuer-Satz nicht hart verdrahtet im Programm stehen haben, sondern in eine Properties-Datei auslagern, außerdem wollen wir nicht nur einen, sondern mehrere Steuersätze als Multi-Value Property unterstützen: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

// Datei PropertiesTest03.java // // // // // //

Das Programm liest einen Geldbetrag ein (als Kommandozeilen-Argument) und berechnet den Brutto-Preis mit MwSt. Die Mehrwertsteuer-Sätze und die Währung werden aus der Properties-Datei mwst.properties gelesen.

import java.io.*; import java.util.*; public class PropertiesTest03 { public static void main( String[] args ) { // Das erste Kommandozeilen-Argument muss // eine Gleit- oder Fest-Kommazahl sein. if ( args.length == 0 ) { usage(); System.exit( 1 ); } // Einlesen der Properties String path = "mwst.properties"; Properties props = new Properties(); try { props.load( new FileInputStream( path ) ); } catch ( IOException ex ) { System.err.println( "Fehler beim Lesen der Datei " + path + ", ex = " + ex ); System.exit( 1 ); } // Der Netto-Preis wird als Kommandozeilen// Argument angegeben // Wir rechnen diesen Betrag in die kleinste

Wichtige Klassen

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

// Währungs-Einheit um. double netPrice = Double.parseDouble( args[ 0 ] ) * 100; // Property "mwst" lesen. Falls es nicht // vorhanden ist, wird 16% verwendet. String mwstStr = props.getProperty( "mwst", "0.16" ); // Property "currency" für die Währung // lesen. Falls es nicht vorhanden ist, // wird "EUR" verwendet. String currency = props.getProperty( "currency", "EUR" ); // StringTokenizer mit ":" als Trennzeichen // instanzieren StringTokenizer stok = new StringTokenizer( mwstStr, ":" ); // Schleife über alle Mehrwertsteuer-Sätze. while ( stok.hasMoreTokens() ) { double mwst = Double.parseDouble( stok.nextToken() ); // Berechnung des Faktors inklusive MwSt. double fac = 1.0 + mwst; // Berechnung des Brutto-Preises double mwstPrice = Math.round( netPrice * fac ) / 100.0; // Ausgabe der Währung und des Brutto// Preises System.out.println( "Brutto = " + currency + " " + mwstPrice ); } } private static void usage() { System.err.println( "Aufruf: PropertiesTest03 "

Wichtige Klassen

84 85 86 }

5.5

); }

java.lang.System

Die Klasse System kann nicht instanziert werden und enthält ausschließlich statische Attribute und Methoden, die häufig benötigt werden. 5.5.1

Attribute von java.lang.System

Wir haben in den bisherigen Beispielen häufig Anweisungen der Form System.out.println( ... );

gesehen, ohne uns großartig Gedanken darüber zu machen. Nun, jetzt ist der Zeitpunkt gekommen, dies nachzuholen. Jedes Programm erhält vom Betriebssystem drei vordefinierte Objekte (zwei für die Ausgabe, eins für die Eingabe). Diese werden in der Klasse System als statische Attribute zur Verfügung gestellt. Das Attribut out ist ein Objekt der Klasse io.PrintStream und wird für Ausgaben eines Programms auf den Standard-Ausgabe-Kanal (STDOUT) benutzt. Für Ausgaben auf den Standard-Fehler-Kanal (STDERR) kann das Attribut err verwendet werden, das ebenso wie out eine Instanz der Klasse PrintStream darstellt. Diese Klasse stellt als die wichtigsten Vertreter die Methoden print() und println() zur Verfügung, mit deren Hilfe Ausgaben im Programm erfolgen. Den Gebrauch dieser Methoden haben wir bereits in den Beispielen gesehen. Hier die Definition der Attribute: static PrintStream out static PrintStream err static InputStream in

Standardmäßig landen Ausgaben über das Attribut err im selben Ausgabe-Kanal wie solche über das Attribut out, d.h. Standard-Fehler-Ausgabe und Standard-Ausgabe sind miteinander verbunden. Beide Kanäle können jedoch getrennt voneinander

Wichtige Klassen

im Aufruf des Programms durch spezielle Argumente in der Kommandozeile mgelenkt werden. Hier ein Beispiel, wie man dies in Unix bewerkstelligt: // Ausführen des Programms MyProg mit Umlenkung aller // Ausgaben in die Datei /tmp/out.txt // Sowohl Ausgaben auf STDOUT als auch auf STDERR werden // in die Datei umgelenkt. java MyProg > /tmp/out.txt // Dasselbe, diesmal werden die Kanäle für Standard// Ausgabe und Standard-Fehler-Ausgabe separat // umgelenkt. java MyProg >/tmp/out.txt 2>/tmp/err.txt // Im nächsten Beispiel wird die Fehler-Ausgabe nach // /dev/null umgelenkt, d.h. unterdrückt. java MyProg 2>/dev/null // Und hier noch ein Beispiel, wie man unter UNIX // eine Vereinigung von STDERR und STDOUT erzwingt. // Beide Ausgabe-Kanäle werden in die Datei // /tmp/out.txt umgeleitet. java MyProg > /tmp/out.txt 2>&1

er noch ein Beispiel-Programm, wie Sie Eingaben über das Attribut in (Eingabenal STDIN) lesen können. Es enthält auch Anweisungen für Programmausgaben: 01 // Datei StdinTest.java 02 03 import java.io.*; 04 05 public class StdinTest { 06 // Da beim Lesen von der Standard-Eingabe 07 // Exceptions ausgelöst werden können, 08 // müssen diese entweder mit einem 09 // try/catch-Block abgefangen werden, 10 // oder man nimmt die abzufangenden 11 // Exceptions in die Methoden-Deklaration auf. 12 public static void main( String[] args ) 13 throws IOException 14 { 15 // Lesepuffer

Wichtige Klassen

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 }

byte[] buf = new byte[ 4096 ]; // Ausgabe des Programm-Prompts // (Eingabe-Aufforderung) System.out.print( "> " ); // Endlos-Schleife, die beim Eingeben // des Zeichens 'q' abgebrochen wird. while ( true ) { // Eine Zeile von Standard-Eingabe lesen int len = System.in.read( buf ); // Umwandeln des byte-Arrays in einen // String. String line = new String( buf, 0, len ); // Entfernen des Zeilenvorschubs am // Ende der Zeile, er ist ebenfalls // Bestandteil der Eingabe-Daten line = line.trim(); // // // // if

Wenn als einziges Zeichen ein 'q' eingegeben wird, wollen wir das Programm beenden, indem die EndlosSchleife verlassen wird. ( line.equals( "q" ) ) { break;

} // Ausgabe der eingegebenen Zeile und // des Programm-Prompts. System.out.print( "'" + line + "'\n> " ); } System.exit( 0 ); }

Achtung Vorsicht ist bei langen Eingaben geboten: Der Eingabe-Puffer ist auf 4096 Bytes begrenzt. Gibt man eine längere Eingabe-Zeile ein, dann erfolgt keine Exception, sondern die zu lange Eingabe-Zeile wird derart in mehrere Teile zerlegt, dass kein Einzelteil größer als 4096 Bytes ist.

Wichtige Klassen

5.2

Methoden von java.lang.System

Aktuelle Zeit lesen

Mit der Methode currentTimeMillis() kann man die aktuelle Zeit der System-Uhr slesen: static long currentTimeMillis()

ese Methode wird meist für das Erzeugen von Zeitstempeln im Programm und zur ufzeit-Ermittlung von Programmteilen benutzt. Sie liefert die Anzahl von Millikunden zurück, die seit dem 1.1.1970 00:00 Uhr GMT vergangen sind. Ein Beiiel hierzu finden Sie bei der Beschreibung der Klasse StringBuffer. Beenden der Virtual Machine

uch die Methode exit() haben wir in einigen Beispielen bereits benutzt. Sie beent die Virtual Machine, in deren Kontext der Programmcode abläuft. Bei Hauptproammen bedeutet dies das normale Programmende. Definiert ist die Methode wie lgt: static void exit( int status )

Das Argument muss eine ganze Zahl sein und gibt dem Aufrufer der Virtual Machine nen numerischen Return-Status zurück. Bei Hauptprogrammen gilt die Regel: Bei folg muss ein Programm den Return-Status 0 zurückgeben, bei einem Fehler endeinen anderen Wert (meist ist dies der Wert 1). mleiten der Ein-/Ausgabe-Kanäle

i der Beschreibung der Attribute für die Standard-Eingabe und Standard-Ausgabe n Programmen haben wir gesehen, wie man beim Aufrufen eines Programms die näle umleiten kann (zum Beispiel in eine Datei). Die Klasse System bietet für denben Zweck statische Methoden an, mit deren Hilfe die Kanäle vom Programm lbst umgelenkt werden können. An dieser Stelle möchte ich Ihnen nur die Deklaran der Methoden zeigen: static void setOut( PrintStream os ) static void setErr( PrintStream os ) static void setIn( InputStream is )

Wichtige Klassen

Arrays kopieren

Die Klasse System stellt mit der Methode arraycopy() eine Möglichkeit zur Verfügung, effizient und schnell Arrays zu kopieren: static void arraycopy( Object src, int srcPos, Object dest, int destPos, int length )

Die Methode kopiert aus dem Array src Elemente ab dem angegebenen Start-Index srcPos in das Array dest ab dem Index destPos. Es werden length Elemente kopiert. Sie kann verschiedene Exceptions auslösen, diese lesen Sie am besten im API der Klasse System nach, da die vielen Möglichkeiten den Rahmen dieses Buches sprengen würden. Garbage Collector explizit aufrufen

Normalerweise läuft der Garbage Collector (das ist die Müllabfuhr für nicht mehr benötigten Speicher in der Virtual Machine) als eigenständiger Thread im Hintergrund und sorgt automatisch dafür, dass tote Objekte aufgeräumt werden. In bestimmten Fällen (zum Beispiel, wenn sehr viele Threads gleichzeitig in einer Virtual Machine parallel laufen und damit der Garbage Collector zu langsam wird) kann es jedoch nötig sein, explizit den Garbage Collector aufzurufen. An dieser Stelle möchte ich Ihnen nur die Deklaration für den Aufruf zeigen: static void gc()

Eigenschaften der Runtime-Umgebung lesen

Mit Hilfe der Methoden getProperties() bzw. getProperty() kann man aktuelle Einstellungen der Java Runtime-Umgebung lesen: +

static Properties getProperties() static String getProperty( String key ) static String getProperty( String key, String default )

Wichtige Klassen

Auch hierzu ein kleines Beispiel: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19

// Datei SystemTest01.java // Das Programm gibt alle aktuellen // Einstellungen der Virtual Machine aus. import java.util.*; public class SystemTest01 { public static void main( String[] args ) { Properties props = System.getProperties(); Enumeration en = props.propertyNames(); while ( en.hasMoreElements() ) { String name = ( String ) en.nextElement(); String val = props.getProperty( name ); System.out.println( name + " = '" + val + "'" ); } } }

stem-Properties setzen

us der Kommandozeile heraus kann man System-Properties mit dem Schalter -D zielt setzen: java -Dprop1=v1 -Dprop2=v2 -Dprop3="hi there" SystemTest

obieren Sie es aus, in der Ausgabe-Liste der System-Properties erscheinen alle über die Kommandozeile angegebenen Properties.

6

java.lang.Runtime

der Klasse Runtime sind einige nützliche Methoden enthalten, von denen ich hier lerdings nur ein paar ausgesuchte vorstellen möchte: void long long long

gc() freeMemory() maxMemory() totalMemory()

Wichtige Klassen

Ein Programm, das die Methoden von Runtime verwenden möchte, muss sich erst eine Instanz der Klasse besorgen: Runtime rt = Runtime.getRuntime();

Die Methode gc() zwingt die Virtual Machine, den Garbage Collector auch dann in Gang zu setzen, wenn er gerade der Meinung ist, nichts aufräumen zu müssen. freeMemory() gibt den freien Hauptspeicher der Virtual Machine in Bytes zurück. maxMemory() liefert den gesamten Hauptspeicher in Bytes zurück, welcher der Virtual Machine zur Verfügung steht. Mit totalMemory() erhält man den gesamten derzeit

von der Virtual Machine reservierten Hauptspeicher in Bytes. Auf der CD-ROM finden Sie in der Datei RuntimeTest01.java ein Beispiel für die Benutzung der Klasse Runtime. Wenn Sie das Programm aufrufen, werden Sie feststellen, dass der Wert für den freien Hauptspeicher nach dem Aufruf des Garbage Collectors größer ist als vorher. Probieren Sie das Programm einmal mit verschiedenen Kommandozeilen-Argumenten für die Virtual Machine, z.B.: java RuntimeTest01 -Xmx256m -Xms64m

In diesem Beispiel wird die Virtual Machine mit einem initialen Hauptspeicher von 65 MByte (Schalter -Xms) gestartet. Insgesamt stellt man der Virtual Machine 256 MByte zur Verfügung. Hinweis

5.7

Die Methode maxMemory() gibt es erst seit Version 1.4 des JDK.

java.lang.Math

Für alle, die trigonometrische und andere mathematische Methoden benötigen, ist die Klasse Math genau das Richtige. Nebenbei bemerkt, daneben existiert die Klasse java.lang.StrictMath, die ebenso einige der auch in Math implementierten Methoden enthält. StrictMath richtet sich allerdings genau nach den publizierten Algorithmen der mathematischen Funktionen, was zu einer deutlich geringeren Performance führt. Wem es also egal ist, wie die Methoden ihre Arbeit tun, der sollte die Klasse Math benutzen. Ich möchte Sie hier nicht mit den Methoden langweilen, Sie können sich die Beschreibungen in der API-Dokumentation des JDK durchlesen. An dieser Stelle möchte ich Ihnen nur den Geschwindigkeits-Unterschied zwischen zwei gleichen Methoden aus Math und aus StrictMath zeigen:

Wichtige Klassen

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

// Datei MathTest01.java // Das Programm verwendet die Methode sqrt() // einmal von Math, ein weiteres Mal von StrictMath // und gibt die jeweils benötigte Zeit aus. public class MathTest01 { public static void main( String[] args ) { long start = System.currentTimeMillis(); for ( int i = 0; i < 1000000; i++ ) { double j = Math.sqrt( ( double ) i ); } long end = System.currentTimeMillis(); System.out.println( "Math = " + ( end - start ) + " ms" ); start = System.currentTimeMillis(); for ( int i = 0; i < 1000000; i++ ) { double j = StrictMath.sqrt( ( double ) i ); } end = System.currentTimeMillis(); System.out.println( "StrictMath = " + ( end - start ) + " ms" ); } }

enn Sie das Programm übersetzen und ausführen, werden Sie in etwa folgende gebnisse erhalten:

Abbildung 5.2: Unterschied zwischen Math und StrictMath

Wichtige Klassen

5.8

java.util.Arrays

Diese Klasse Arrays bietet einige nützliche Methoden für Arrays jeglicher Art an, von denen ich hier ein paar ausgesuchte zeigen möchte: static List asList( Object[] a ) static void fill( byte[] a, byte val ) static void fill( byte[] a, int startInd, int endInd, byte val ) static boolean equals( byte[] a1, byte[] a2 ) static void sort( byte[] a ) static void sort( byte[] a, int startInd, int endInd ) static void sort( Object[] a, Comparator c )

Hinweis Die Methoden fill(), equals() und sort() gibt es mehrfach, jeweils für die unterschiedlichen primitiven Datentypen. Der Einfachheit halber habe ich nur die Methoden für den Datentyp byte aufgeführt. Eine Ausnahme bildet die letzte sort()Methode, mit der man über einen Comparator die Sortierung anpassen kann. Die Methode asList() wandelt das angegebene Array in ein Objekt der Klasse java.util.List um. Hier ist Vorsicht geboten, weil die einzelnen Elemente der Liste Referenzen auf das ursprüngliche Array sind. Dies bedeutet, dass sich Änderungen von Elementen in der Liste auf die Elemente des Original-Arrays auswirken. Auch hat die Liste eine feste Länge, das Hinzufügen oder Löschen von List-Elementen ist nicht möglich. Ein Beispiel hierzu: 01 02 03 04 05 06 07 08 09 10 11

// Datei ArraysTest01.java // Das Programm zeigt, wie man ein Array in eine // Liste umwandeln kann. Es wird auch deutlich, // dass nicht alle Methoden des Interface List // nach der Umwandlung möglich sind. import java.util.*; public class ArraysTest01 { public static void main( String[] args ) { // Integer Array definieren Integer[] i1 = {

Wichtige Klassen

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 }

new Integer( 7 ), new Integer( 3 ), }; // Array in eine Liste umwandeln List l = Arrays.asList( ( Object[] ) i1 ); // Anzahl der List-Elemente ermitteln int nitems = l.size(); System.out.println( nitems + " items" ); // Eine Änderung eines List-Elements wirkt // sich auf das Original-Array aus!!!!!!! l.set( 0, new Integer( 5 ) ); for ( int i = 0; i < i1.length; i++ ) { System.out.println( "i[ " + i + " ] = " + i1[ i ] ); } // Das Entfernen von Elementen ist nicht // gestattet, weil die Liste eine feste // Größe besitzt. try { l.remove( 0 ); } catch ( Exception ex ) { System.out.println( "Loeschen geht nicht" ); } // Auch Hinzufügen von Elementen ist // nicht erlaubt. try { l.add( new Integer( 10 ) ); } catch ( Exception ex ) { System.out.println( "Addieren geht nicht" ); } }

e Methode equals() vergleicht alle Elemente zweier Arrays miteinander und gibt r dann true zurück, wenn wirklich alle Elemente gleich sind.

Wichtige Klassen

Mit der Methode fill() kann man alle Elemente oder Teilbereiche eines Arrays mit demselben Wert füllen. Zum Sortieren von Arrays stehen ebenfalls einige Methoden zur Verfügung, wobei die meisten nach der Art des angegebenen Arrays sortieren (so werden zum Beispiel numerische Arrays unter Beachtung des Vorzeichens aufsteigend sortiert). Interessant ist die sort()-Methode, bei der man einen Comparator als Argument angeben kann. Damit ist es möglich, String-Arrays unter Einbeziehung von Umlauten in verschiedenen Sprach-Varianten richtig zu sortieren. Hier ein Beispiel, das die Klasse java.text.RuleBasedCollator dafür verwendet: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Datei ArraysTest03.java // Das Programm demonstriert, wie man Strings // sortieren kann, die zum Beispiel deutsche // Umlaute enthalten. import java.text.*; import java.util.*; public class ArraysTest03 { public static void main( String[] args ) { String s1 = "Über"; String s2 = "Ulme"; String[] sa = { s1, s2 }; // Sortierung ohne besondere Vorkehrungen // Umlaute werden falsch bewertet und führen // nicht zum gewünschten Ergebnis. Arrays.sort( sa ); System.out.println( "normale Sortierung" ); for ( int i = 0; i < sa.length; i++ ) { System.out.println( "\tsa[ " + i + " ] = " + sa[ i ] ); } // Sortierung mit dem Interface Comparator. // Mit einem RuleBasedCollator-Objekt, // welches das Interface implementiert, // kann man Strings in beliebigen // Locales (Sprachvarianten) richtig // sortieren. RuleBasedCollator c = ( RuleBasedCollator ) Collator.getInstance(

Wichtige Klassen

33 34 35 36 37 38 39 40 41 42 43 44 45 46 }

9

new Locale( "de", "DE", "" ) ); Arrays.sort( sa, c ); System.out.println( "Sortierung mit Comparator" ); for ( int i = 0; i < sa.length; i++ ) { System.out.println( "\tsa[ " + i + " ] = " + sa[ i ] ); } }

java.util.Random

ie der Name bereits vermuten lässt, wird die Klasse Random zur Erzeugung von Zufallszahlen benutzt. Im Gegensatz zur Methode random() der Klasse Math können mit auch ganzzahlige Zufallszahlen generiert werden. Random besitzt zwei Konstruktoren: Random() Random( long seed )

ei der zweiten Variante kann eine Zahl als Argument angegeben werden (seed), mit lcher der Zufallsgenerator initialisiert wird. Damit ist es möglich, selbst auf unterschiedlichen Rechnern immer genau dieselbe Zahlenreihe zu erzeugen, denn Comuter-basierte Zufallszahlen sind nicht wirklich zufällig. Verwendet man den Defaultonstruktor, dann initialisiert sich der Zufallsgenerator selbst. Unter den Methoden, die von der Klasse zur Verfügung gestellt werden, möchte ich hnen die wohl am häufigsten benötigte zeigen: int nextInt( int n )

e Methode nextInt() liefert ganze Zufallszahlen zwischen 0 und (n-1). Auf der CD-ROM habe ich in der Datei RandomTest01.java ein Programm für Sie, das mit Hilfe von Random und nextInt() Lottozahlen erzeugt.

Wichtige Klassen

5.10 java.util.Date Die Klasse Date muss man eigentlich zu den verunglückten Entwürfen in Java zählen, nachdem man das API des JDK überflogen hat. Bis auf wenige Ausnahmen sind alle Methoden als deprecated gekennzeichnet, was bedeutet, dass sie nicht mehr benutzt werden sollten. Der Grund dafür liegt in der Komplexität der Datums-Berechnung, die man beim ersten Entwurf unterschätzt hatte. Trotzdem wird die Date-Klasse auch heute noch von anderen Klassen verwendet, z.B. von java.util.Calendar und java.util.TimeZone. Deshalb möchte ich Ihnen hier nur die beiden Konstruktoren zeigen, die meist benutzt werden: Date() Date( long ms )

Der Default-Konstruktor erzeugt eine Instanz der Date-Klasse mit dem aktuellen Zeitstempel auf dem Rechner. Das Argument ms beim zweiten Konstruktor ist die Anzahl der Millisekunden, die seit dem 1.1.1970 00:00 Uhr GMT vergangen sind. Doch Vorsicht: Date d = new Date( 0L );

ergibt meist nicht den 1.1.1970 00:00 Uhr, sondern das Ergebnis hängt von der Zeitzone ab, in welcher der Rechner liegt, auf dem der Programmcode aufgerufen wurde. In Deutschland ist das Ergebnis zum Beispiel 1.1.1970 01:00 Uhr, weil unsere Zeitzone um +1 Stunde vor GMT liegt. Für die Umwandlung eines Date-Objekts in ein Calendar-Objekt und umgekehrt können die Methoden getTime() und setTime() der Calendar-Klasse benutzt werden. Achtung Vorsicht ist geboten, wenn Sie sowohl alle Klassen aus java.util als auch aus java.sql importieren, denn in beiden Packages ist eine Klasse mit dem Namen Date vorhanden. In diesem Fall müssen Sie den gewünschten Klassennamen voll qualifiziert angeben: java.util.Date d = new java.util.Date();

Wichtige Klassen

11 java.util.TimeZone er mit Datum und Zeit spielen möchte, kommt an Zeitzonen nicht vorbei. Um es rauszuschicken: Manipulationen der Zeit unter Berücksichtigung von Zeitzonen nn sehr schnell zu einer zeitraubenden Sache werden, dies liegt vor allem daran, dass ne Zeitzone nicht nur einen bestimmten Offset von GMT (Greenwich Mean Time, ch UTC genannt) besitzt, sondern unter Umständen auch eine Sommerzeit, und die ist teilweise sehr unterschiedlich. Nicht nur, dass einige Zeitzonen gar keine Umschaltung auf Sommerzeit kennen, auch der Zeitpunkt und die Anzahl Minuten d sehr verschieden. So gibt es beispielsweise Staaten, die jährlich aufs Neue entheiden, ob und wann auf Sommerzeit bzw. Winterzeit umgeschaltet wird. In den meisten europäischen Staaten ist das Ganze noch relativ einfach, weil dort gemeinsam u festgesetzten Zeitpunkten eine Umschaltung um genau eine Stunde erfolgt. e Klasse java.util.TimeZone ist abstrakt und kann daher selbst nicht instanziert erden. Mit Hilfe der statischen Methode getDefault()kann man sich jedoch ein Objekt für die auf dem Rechner gültige Zeitzone besorgen. Wollen Sie eine der vielen vordefinierten Zeitzonen benutzen, so verwenden Sie am besten die ebenfalls stache Methode getTimeZone(): static TimeZone getTimeZone( String ID )

e ID kann auch mit ihrer Abkürzung (z.B. CET) angegeben sein, dies aber nur aus ünden der Rückwärts-Kompatibilität zum JDK1.1, Sie sollten immer die Langrm für die Zeitzonen-ID verwenden. Falls eine ungültige ID angegeben wurde, fert die Methode ein Objekt für die GMT-Zeitzone zurück. Die ID wird übrigens se-insensitive interpretiert. Zunächst haben wir natürlich das Problem, was für eine ID wir für welche Zeitzone rwenden müssen. Hier hilft die statische Methode getAvailableIDs(), welche die s aller vordefinierten Zeitzonen als String-Array zurückliefert. Die Methode exisrt auch in einer Variante, bei der man den Offset von GMT in Millisekunden angen kann, es werden dann nur die IDs derjenigen Zeitzonen zurückgegeben, deren fset von GMT dem angegebenen Wert entsprechen: String[] getAvailableIDs( int rawOffset )

Das Argument rawOffset ist der gewünschte GMT-Offset ohne Sommerzeit-Berückhtigung (in Deutschland beträgt er also immer 3600000). Bei Zeitzonen, in denen

Wichtige Klassen

sich der GMT-Versatz irgendwann geändert hat, bezieht sich rawOffset auf den heute gültigen Offset. So wurde in Honolulu zum Beispiel die GMT-Verschiebung im Jahre 1947 von -10:30 Stunden auf -10:00 Stunden geändert, der rawOffset für diese Zeitzone ist also -10:00 Stunden. Diese Besonderheit wird beim Vergleich der beiden Methoden getRawOffset() und getOffset(): int getOffset( long date ) abstract int getRawOffset()

Wenn Sie z.B. ein TimeZone-Objekt für die ID Pacific/Honolulu instanzieren und sich den GMT-Offset für das Jahr 1946 sowie für 1947 ausgeben lassen, dann ergibt sich genau der 30-Minuten-Unterschied, um die sich der GMT-Versatz geändert hat (die Klasse GregorianCalendar wird im nächsten Abschnitt beschrieben): // String für die ID der Zeitzone String tzStr = "Pacific/Honolulu"; // Zeitzonen-Objekt instanzieren TimeZone tz = TimeZone.getTimeZone( tzStr ); // Calendar-Objekt für die Zeitzone instanzieren GregorianCalendar cal = new GregorianCalendar( tz ); // Jahr auf 1946 setzen und GMT-Offset ermitteln cal.set( Calendar.YEAR, 1946 ); int oldOff = tz.getOffset( cal.getTimeInMillis() ); // dasselbe für das Jahr 1948 cal.set( Calendar.YEAR, 1948 ); int newOff = tz.getOffset( cal.getTimeInMillis() ); // Ausgabe beider GMT-Offsets in Stunden // dabei muss man einen double-Wert benutzen, damit // die Nachkomma-Stellen ausgegeben werden. System.out.println( "alter Offset = " + ( oldOff / 3600000.0 )+ ", neuer Offset = " + ( newOff / 3600000.0 ) );

Es wird ausgegeben: alter Offset = -10.5 neuer Offset = -10.0

Wichtige Klassen

Um den Namen einer Zeitzone auszugeben, verwendet man die Methode getDisplayName(), die in verschiedenen Varianten angeboten wird:

// Ausgabe der Lang-Form ohne Sommerzeit-Angabe in der // Default-Locale. String getDisplayName() // Hier kann man mit daylight festlegen, ob der // Sommerzeit-String ausgegeben werden soll oder nicht. // Mit style legt man fest, ob die Lang- oder Kurz// Form verwendet werden soll (mit den Konstanten // TimeZone.LONG bzw. TIMEZONE.SHORT). String getDisplayName( boolean daylight, int style ) // Hier kann man zusätzlich angeben, für welche // Locale der Zeitzonen-Name formatiert werden soll. String getDisplayName( boolean daylight, int style, Locale locale

// Wie die Variante ohne Argumente, jedoch kann man // die Locale für die Ausgabe-Formatierung wählen. String getDisplayName( Locale locale )

Um festzustellen, ob die verwendete Zeitzone Sommerzeit-Umstellung besitzt oder b man sich gerade in der Sommer- oder Winterzeit befindet, können die Methoden useDaylightTime() bzw. inDaylightTime() benutzt werden: abstract boolean useDaylightTime() abstract boolean inDaylightTime( Date date )

Mit der Methode getDSTSavings() kann man die Anzahl der Millisekunden lesen, um lche die Uhr bei der Umschaltung zwischen Sommer- und Winterzeit vor- bzw. rückgestellt wird. Besitzt die verwendete Zeitzone keine Sommerzeit, dann liefert e Methode den Wert 0 zurück: int getDSTSavings()

Wichtige Klassen

Will man eine beliebig selbst definierte Zeitzone instanzieren, so muss die konkrete Kind-Klasse java.util.SimpleTimeZone verwendet werden. Da man meist mit den im System vordefinierten Zonen auskommt, möchte ich hier auf eine Beschreibung dieser Klasse verzichten.

5.12 java.util.Calendar Mit der Klasse Calendar wurden zwei gravierende Nachteile der Klasse Date behoben. Diese berücksichtigt zum einen keine Zeitzonen, zum anderen kennt sie keine verschiedenen Kalender-Ausprägungen. Calendar ist eine abstrakte Basisklasse und ihre Methoden und Attribute finden vor allem in der daraus abgeleiteten Klasse GregorianCalendar Anwendung, die ebenfalls im Package java.util enthalten ist. Der Gregorianische Kalender ist weltweit der am häufigsten eingesetzte und derzeit die einzige konkrete Implementierung des JDK.

5.12.1

Konstruktoren von java.util.GregorianCalendar

Leider besitzt die Klasse keinen Konstruktor, bei dem man ein Date-Objekt angeben kann. Hier die zur Verfügung gestellten Konstruktoren GregorianCalendar() GregorianCalendar( int year, int month, int date GregorianCalendar( int year, int month, int date, int hour, int ) GregorianCalendar( int year, int month, int date, int hour, int int second ) GregorianCalendar( Locale locale ) GregorianCalendar( TimeZone tz ) GregorianCalendar( TimeZone zone, Locale aLocale

) minute

minute,

)

Hinweis Das Argument month muss im Bereich von 0 bis 11 liegen, da der Januar nicht die Nummer 1, sondern 0 hat. Das Argument date bezeichnet den Monats-Tag, der bei 1 beginnt und je nach Monat maximal 28, 29, 30 oder 31 sein kann.

Wichtige Klassen

12.2

Umwandlung von Date in Calendar

ie man an den zur Verfügung gestellten Konstruktoren sieht, gibt es keine direkte öglichkeit, ein Date-Objekt in ein Calendar-Objekt umzuwandeln. Deshalb instanrt man zunächst ein GregorianCalendar-Objekt mit der gewünschten Zeitzone und cale auf Basis der aktuellen Zeit. Anschließend setzt man mit setTime() die Zeit: Date d = ... GregorianCalendar cal = new GregorianCalendar( TimeZone.getTimeZone( ... ), new Locale( ... ) ); cal.setTime( d );

12.3

Umwandlung von Calendar in Date

mgekehrt kann man mit der Methode getTime() ein Calendar-Objekt in ein Datebjekt umwandeln: GregorianCalendar cal = new GregorianCalendar( TimeZone.getTimeZone( ... ), new Locale( ... ) ); Date d = cal.getTime();

12.4

Calendar-Felder

e Klasse Calendar stellt viele Attribute (Felder) zur Verfügung, die man einzeln mit r Methode get() lesen oder mit set() setzen bzw. bei add() oder roll() angeben nn. Sie identifizieren jeweils ein bestimmtes Attribut numerisch. Attribut

Bedeutung

ZONE_OFFSET

Feld-Nummer für Zeitzonen-Offset von GMT in Millisekunden ohne Berücksichtigung einer Sommerzeit.

DST_OFFSET

Feld-Nummer für die Anzahl der Millisekunden, um die sich die Sommerzeit von der Winterzeit unterscheidet. Für Deutschland beträgt der Wert zum Beispiel 3600000. Allerdings ist dieses Feld während der Winterzeit immer 0.

elle 5.1: Übersicht der Calendar-Attribute

Wichtige Klassen

Attribut

Bedeutung

ERA

Feld-Nummer für die Identifizierung vor bzw. nach Christi Geburt (BD, AD). Dieses Feld ist abhängig von der CalendarImplementierung.

YEAR

Feld-Nummer für das Jahr

MONTH

Feld-Nummer für den Monat. Der Wert 0 gilt für Januar, 11 für Dezember.

JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER

Feld-Nummern für die symbolischen Monats-Konstanten (APRIL hat z.B. den Wert 3)

WEEK_OF_MONTH

Feld-Nummer für die Woche im Monat (1 bis 5)

WEEK_OF_YEAR

Feld-Nummer für die Woche im Jahr (1 bis 52)

DATE,

Feld-Nummer für den Tag im Monat (1 bis 31)

DAY_OF_MONTH DAY_OF_WEEK

Feld-Nummer für den Wochentag (1 bis 7). Mit der Methode getFirstDayOfWeek() kann man den Wert für den ersten Tag der Woche herausfinden (z.B. SUNDAY für USA, MONDAY für Frankreich).

DAY_OF_WEEK_IN_MONTH

Feld-Nummer für die Woche im Monat bezogen auf den Monatstag. Vom 1. bis zum 7. eines Monats hat dieses Feld den Wert 1. Vom 8. bis zum 14. hat es den Wert 2 usw.

DAY_OF_YEAR

Feld-Nummer für den Tag im Jahr (1 bis 366)

SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY

Feld-Nummern für die symbolischen Wochentage (TUESDAY hat z.B. den Wert 3)

HOUR,

Feld-Nummern für die Tages-Stunden (HOUR gilt für 12-StundenAnzeige inkl. AM/PM, HOUR_OF_DAY für 24-Stunden-Anzeige)

HOUR_OF_DAY AM_PM, AM, PM

AM_PM ist die Feld-Nummer, mit der das Feld für die Unterscheidung zwischen Vormittag (AM) und Nachmittag (PM) gelesen bzw.

gesetzt werden kann. MINUTE

Feld-Nummer für die Minute

SECOND

Feld-Nummer für die Sekunde

MILLISECOND

Feld-Nummer für die Millisekunde innerhalb der Sekunde

Tabelle 5.1: Übersicht der Calendar-Attribute (Forts.)

Wichtige Klassen

Achtung Wenn Sie mit set() Calendar-Felder setzen, wird nicht die gesamte Struktur neu berechnet. So bleibt zum Beispiel der Wochentag gleich, wenn Sie die Zeit um einen Monat in die Zukunft schieben. Um eine Neu-Berechnung durchführen zu lassen, müssen Sie die Methode add() verwenden. 12.5

Calendar-Berechnungen

Mit Hilfe der Methoden set() sowie add() kann man elegant Datums-Berechnungen rchführen. // Calendar-Objekt mit aktuellem Datum erzeugen. GregorianCalendar cal = new GregorianCalendar(); // Datum ausgeben System.out.println( cal.getTime() ); // Das Datum auf Februar 2003 umstellen cal.add( Calendar.MONTH, 2 ); // Datum ausgeben System.out.println( cal.getTime() );

Das Ergebnis (das aktuelle Datum war 31.12.2002 20:32:19): Tue Dec 31 20:32:19 CET 2002 Fri Feb 28 20:32:19 CET 2003

Nach der Neu-Berechnung der Datums-Felder wird das Datum richtig ausgegeben, mlich der letzte Tag des Montas Februar 2003. Hätten wir mit set() den Monat d das Jahr geändert, dann wäre das Ergebnis der 3. März. Hinweis Anstelle von add() kann auch roll() verwendet werden. Diese lässt jedoch die nächsthöhere Datumseinheit unverändert. Wenn Sie also zum Beispiel zum Datum 31.07.2002 7 Monate hinzuzählen, dann ergibt das den 28. Februar, llerdings nicht 2003, sondern 2002. Während der Monat angepasst wurde (einschließlich der Bedingung, dass der letzte Tag im Februar der 28. oder der 29. ist), leibt die nächstgrößere Einheit (das Jahr) unverändert.

Wichtige Klassen

5.12.6

Schaltjahre

Um festzustellen, ob das aktuelle Datum in einem Schaltjahr liegt, kann man die Methode isLeapYear() der Klasse GregorianCalendar benutzen: boolean isLeapYear()

Collections I

6

Collections I

Hinter dem Begriff Collection, den man etwa mit Sammlung ins Deutsche übersetzen kann, verbirgt sich eigentlich nichts Besonderes, obwohl viele Programmierer dennoch auf Kriegsfuß damit stehen. Im Prinzip stellt eine Collection nichts anderes dar als einen Container für Daten, die nicht unbedingt alle vom selben Typ sein müssen. Der Container enthält einzelne Elemente in Form einer Liste, auf die man entweder direkt zugreifen kann (wahlfreier Zugriff) oder wie bei einem normalen Array in einer Schleife nacheinander (sequentieller Zugriff) bearbeiten muss. Kompliziert wird das Ganze durch die vielen Klassen und Interfaces, die Java für die Bearbeitung solcher Daten zur Verfügung stellt. In diesem Kapitel möchte ich mit Ihnen den Nebel der Collections lüften. Vor Version 1.2 von Java war die Welt der Collections noch weitaus aufgeräumter, denn dort gab es eine überschaubare Anzahl von Klassen, die ich nun vorstellen möchte. Ein Interface wird uns bei allen Klassen dieses Kapitels begleiten, deshalb möchte ich zu Beginn kurz darauf eingehen:

6.1

java.util.Enumeration

Sehr häufig liest man Elemente aus Collections sequentiell aus, dafür wird fast immer das Interface Enumeration verwendet. Es deklariert zwei Methoden: boolean hasMoreElements() Object nextElement()

Ich glaube, beide Methoden sind selbst erklärend: hasMoreElements() prüft, ob weitere Elemente in der Liste vorhanden sind, nextElement() gibt das nächste Element der Liste als Instanz der Klasse Object zurück. Um also mit dem gelesenen Objekt etwas anfangen zu können, muss es mit dem richtigen Cast beim Auslesen aus der Liste versehen werden. Wir werden beide Methoden in Beispielen näher kennen lernen.

Collections I

2

java.util.Hashtable

n Objekt der Klasse Hashtable wird verwendet, wenn man beliebige Objekte in einem Container unsortiert speichern möchte. Jedem Element wird dabei ein Name geben, der als Key bezeichnet wird. Unter diesem Key kann man anschließend auf s gespeicherte Objekt zugreifen. Die zu speichernden Daten werden im Value des ements abgelegt, jedes Element einer Hashtable besteht also immer aus einem Key/ ue-Paar. Ich möchte noch einmal betonen, dass auf die Elemente einer Hashtable undsätzlich nur über den Key zugegriffen werden kann, nicht über einen fortlaunden Index oder Ähnliches. ei der Speicherung von Elementen wird ein so genannter Hash-Algorithmus verndet, der wesentlich zur Erhöhung der Geschwindigkeit beim Auslesen von Elementen beiträgt. Dieser Algorithmus indiziert die Keys der gespeicherten Elemente d erzeugt für jeden einzelnen Key einen eindeutigen Hash-Wert, der dann für das Auffinden des zugehörigen Elements verwendet wird. Hinweis Weder die Keys noch die Values einer Hashtable dürfen null sein. Alle Keys einer Hashtable sind eindeutig, es kann also keine Elemente mit gleichem Key eben. Wird ein neues Element angelegt, dessen Key bereits in der Hashtable vorhanden ist, so überschreibt die Operation den Value des bereits existierenden Elements. hen wir uns nun die wichtigsten Konstruktoren der Klasse Hashtable an: 2.1

Konstruktoren von java.util.Hashtable

Hashtable() Hashtable( int initCapacity ) Hashtable( int initCapacity, float loadFactor )

er Default-Konstruktor legt eine Objekt-Instanz mit Standard-Einstellungen für initCapacity (11) und loadFactor (0.75) an. Mit den anderen beiden Konstruktoren

nn man die Standard-Einstellungen anpassen. Doch was bedeutet nun Kapazität d LoadFactor? Nun, unter Kapazität versteht man die Anzahl von Elementen, für e Hauptspeicher reserviert ist, der LoadFactor gibt den Prozentsatz des Füllgrads an, dem neuer Hauptspeicher reserviert werden muss (Für die angegebenen Defaulterte muss also ab dem neunten Element neuer Hauptspeicher reserviert werden).

Collections I

Hierzu wird die Methode rehash() aufgerufen, die uns als Benutzer der Klasse allerdings nicht weiter interessiert. Im Folgenden möchte ich Ihnen die wichtigsten Methoden der Klasse Hashtable zeigen: 6.2.2

Methoden von java.util.Hashtable

Hinweis Die Keys von Hashtables werden als Instanzen vom Typ Object interpretiert. Meist jedoch verbirgt sich dahinter nichts anderes als ein String. Hinzufügen von Elementen Object put( Object key, Object value )

Die Methode put() fügt das durch key identifizierte Objekt hinzu. Falls zu diesem Zeitpunkt bereits ein Element mit demselben Key existiert, wird der alte Value zurückgegeben und durch value überschrieben, ansonsten liefert die Methode den Wert null zurück. Hierzu ein Beispiel: ... Hashtable ht = new Hashtable(); ht.put( "id", new Integer( 1 ) ); ht.put( "title", "a string" );

Es werden zwei unterschiedliche Objekte in die Hashtable aufgenommen, sie können anschließend mit den Keys "id" bzw. "title" wieder gelesen werden. Lesen eines Elements

Mit der Methode get() kann ein Element durch Angabe des Keys ausgelesen werden: ... String title = ( String ) ht.get( "title" ); Integer idObj = ( Integer ) ht.get( "id" );

Collections I

ie die Beispiele zeigen, muss der zurückgelieferte Value für den angegebenen Key mmer mit einem Cast auf den eigentlichen Datentyp versehen werden, da die Methode get() das Objekt grundsätzlich als Referenz auf die Klasse Object zurückliefert. Abfrage-Methoden

r diverse Abfragen stehen folgende Methoden zur Verfügung: boolean containsKey( Object key ) boolean isEmpty() int size()

Mit der Methode containsKey() kann abgefragt werden, ob ein Element mit dem ngegebenen Key vorhanden ist oder nicht. Die Methode isEmpty() liefert true rück, wenn die Hashtable leer ist, andernfalls false. Schließlich kann man mit size() abfragen, wie viele Elemente sich in der Hashtable befinden. ste aller Keys lesen

e Methode keys() liefert eine unsortierte Liste aller Keys der Hashtable zurück: Enumeration keys()

Das folgende Beispiel-Programm gibt die Elemente einer Hashtable aus: // Datei HashtableTest01.java import java.util.*; public class HashtableTest01 { public static void main( String[] args ) { Hashtable ht = new Hashtable(); ht.put( "id", new Integer( 1 ) ); ht.put( "title", "my title" ); Enumeration en = ht.keys(); while ( en.hasMoreElements() ) { String key = ( String ) en.nextElement(); Object val = ht.get( key ); System.out.println( key + " = " + val ); } }

Collections I

Löschen von Elementen

Mit der Methode remove() kann ein einzelnes Element durch Angabe des Key aus der Hashtable entfernt werden. Falls ein Element mit dem angegebenen Key existiert, liefert die Methode den Value des Elements zurück, ansonsten wird null zurückgegeben. Die Methode clear() löscht alle Elemente: Object remove( Object key ) void clear()

Hinweis Die Klasse Hashtable ist im Gegensatz zu den seit Version 1.2 des JDK neu hinzugekommenen Collection-Klassen synchronisiert und damit thread-safe. Was dies im Einzelnen bedeutet, werden wir im Kapitel über Threads lernen. Hier soll uns genügen, dass synchronisierte Klassen bzw. Methoden generell langsamer sind als nicht synchronisierte.

6.3

java.util.Vector

Eine alte Weggefährtin von Hashtable ist die Klasse Vector, die ähnlich wie ein Array mehrere Elemente als lineare Liste speichert, d.h. man greift auf die Elemente der Liste über einen numerischen Index zu. Im Gegensatz zu Arrays können in einem Vector jedoch beliebige Objekte gespeichert werden, während bei Arrays alle Elemente denselben Datentyp haben müssen. Zudem kann sich die Größe eines Vector zur Laufzeit ändern, auch das ist bei Arrays nicht möglich. Hinweis Die Klasse Vector ist synchronisiert und damit relativ langsam. Will man sehr effizient und schnell sein, dann sollte man auf die Klasse java.util. ArrayList zurückgreifen, die Bestandteil der seit Version 1.2 des JDK neu hinzugekommenen Collection-Klassen und nicht synchronisiert ist. 6.3.1

Konstruktoren von java.util.Vector

Ähnlich wie bei der Klasse Hashtable gibt es auch bei Vector unterschiedliche Konstruktoren, mit denen die Performance durch Erhöhen der Kapazität verbessert werden kann: Vector() Vector( int initCapacity ) Vector( int initCapacity, int capacityInc )

Collections I

Der Parameter initCapacity gibt an, für wie viele Elemente Hauptspeicher reserviert rden soll. Mit capacityInc kann man einstellen, um wie viele Elemente die Kapazität höht werden soll, wenn diese beim Hinzufügen eines neuen Elements erschöpft ist. e Default-Werte sind 10 für initCapacity und 0 für capacityInc. 3.2

Methoden von java.util.Vector

Im Folgenden möchte ich Ihnen die wichtigsten Methoden der Klasse Vector zeigen. r nahezu alle Anwendungsfälle gibt es seit JDK 1.2 verschiedene Varianten für die t JDK 1.2 eingeführten neuen Implementierungen der Collections. Achtung Während die Funktionsweise gleichartiger Methoden aus JDK 1.0 und .2 gleich ist, gilt dies leider nicht für die Parameter-Liste der Methoden. So erwartet die Methode insertElementAt() zum Beispiel als erstes Argument das einzufügende Objekt, während die Methode add() zuerst den Index haben möchte. nzufügen von Elementen

e Methode addElement() dient dem Hinzufügen eines neuen Elements am Ende r Liste, mit insertElementAt() kann ein neues Element an beliebiger Stelle der Liste eingefügt werden. Alternativ kann die Methode add() (JKD 1.2) für dieselben Zwee benutzt werden: void addElement( Object o ) void insertElementAt( Object o, int index ) boolean add( Object o ) void add( int index, Object o )

Das erste Element in der Liste eines Vector-Objekts besitzt den Index 0. Die Methode insertElementAt() fügt das neue Element an der durch index angegebenen Position ein. Alle Elemente, deren Index größer oder gleich index ist, werden beim Einfügen um eine Position nach hinten geschoben. Gibt man einen Index an, der entweder negativ oder größer als die Anzahl der Elemente ist, dann erntet man eine ArrayIndexOutOfBoundsException. e boolean-Methode add() liefert immer den Wert true zurück.

Collections I

Ändern von bestehenden Elementen

Mit der Methode setElementAt() können Elemente durch Angabe eines Index direkt geändert werden. Alternativ kann die Methode set() für denselben Zweck benutzt werden, sie wurde mit JDK 1.2 eingeführt: void setElementAt( Object o, int index ) Object set( int index, Object o )

Wie bei insertElementAt() kann man ebenfalls eine Exception als Lohn ernten, wenn der Index ungültig ist (negativ oder hinter dem letzten Element). Die Methode set() liefert das alte Objekt zurück, das durch das angegebene ersetzt wurde. Anzahl der Elemente lesen

Um die aktuelle Anzahl von Elementen eines Vector-Objekts zu lesen, verwendet man die Methode size(): int size()

Wenn man nur prüfen möchte, ob überhaupt ein Element vorhanden ist, kann die Methode isEmpty() benutzt werden: boolean isEmpty()

Einzelne Elemente lesen

Für das Auslesen des ersten bzw. letzten Elements stehen die Methoden firstElement() bzw. lastElement() zur Verfügung. Will man ein Element an einer bestimmten Position der Liste lesen, benutzt man hierzu die Methode elementAt(): Object elementAt( int index ) Object firstElement() Object lastElement()

Wie immer, wenn ein Index als Parameter angegeben werden kann, ist eine ArrayIndexOutOfBoundsException die Antwort auf ungültige Indizes. Im Gegensatz dazu liefern die Methoden firstElement() bzw. lastElement() die Exception NoSuchElementException, wenn die Liste gar keine Elemente enthält.

Collections I

le Elemente lesen

Mit der Methode elements() bekommt man eine Enumeration zurück, welche dielbe Reihenfolge wie die ursprüngliche Liste hat (Index 0 enthält das erste Element). e Methode toArray() wandelt das Vector-Objekt in ein Array mit fester Größe um. beiden Fällen erhält man eine Kopie der Liste, d.h. bei Änderungen bleibt die sprüngliche Liste des Vector-Objekts unverändert: Enumeration elements() Object[] toArray() Object[] toArray( Object[] a )

er Unterschied zwischen den beiden Varianten von toArray() ist, dass mit einem ray als Parameter dessen Datentyp für das zurückgelieferte Array festgelegt werden nn. Hierzu ein Beispiel: ... Vector v = new Vector(); v.addElement( new Integer( 1 ) ); // Das folgende Statement funktioniert nicht Integer[] intA = ( Integer[] ) v.toArray(); // macht man es aber so, dann ist die Sache OK Integer[] intA = ( Integer[] ) v.toArray( new Integer( 1 ) );

emente löschen

Zum Löschen der gesamten Liste stehen die Methoden clear() (JDK 1.2) und removeAllElements() (JDK 1.0) zur Verfügung. Will man einzelne Elemente entfernen, nn benutzt man entweder removeElementAt() (JDK 1.0) oder remove() (JDK 1.2): void clear() void removeAllElements() void removeElementAt( int index ) Object remove( int index ) boolean remove( Object o )

ährend die Methode removeElementAt() als void definiert ist, liefert remove() mit nem Index als Argument das gelöschte Element zurück. Beide Methoden werfen eine ArrayIndexOutOfBoundException, falls ein ungültiger Index angegeben ist. Dane-

Collections I

ben existiert noch die JDK 1.2-Version von remove(), bei der man ein Element statt eines Index angibt. Sie liefert true zurück, wenn das angegebene Element vorhanden war. Hinweis Neben den hier besprochenen Methoden bietet die Klasse eine Reihe weiterer Methoden an, mit denen man zum Beispiel eine Suche in der Liste ausführen kann oder die ein Objekt anstelle des Index als Parameter akzeptieren. Ich möchte damit jedoch nicht den Rahmen dieses Buches sprengen. Dem Wissbegierigen sei deshalb die API-Dokumentation empfohlen.

6.4

java.util.Stack

Jeder Programmierer kennt den Begriff eines Stack. Es handelt sich dabei um einen speziellen Speicher, der bei Funktionsaufrufen für die Zwischenspeicherung von Register-Inhalten und für die Funktions-Argumente verwendet wird. Dabei gilt das LIFO-Prinzip (Last In First Out), d.h. Daten, die zuletzt in den Stack aufgenommen sind, werden als erste wieder ausgelesen. Stack ist eine Kind-Klasse von Vector und erweitert diese um Stack-spezifische Methoden, die ich hier kurz zusammenfassen möchte:

// Konstruktor Stack() // Objekt im Stack ablegen Object push( Object o ) // Objekt aus dem Stack holen Object pop() // Objekt lesen, ohne es zu entfernen Object peek() // Prüfen, ob der Stack leer ist boolean empty() // Nach einem Objekt suchen int search( Object o )

Collections I

Wenn man mit pop() oder peek() versucht, ein Element in einem leeren Stack zu en, dann setzt es eine EmptyStackException. e Methode search() sucht, beginnend beim ersten Element, nach dem angegeben Objekt und gibt die Position des gefundenen Elements zurück, wobei das erste ement nicht den Index 0 hat, sondern beim Stack beginnt die erste Position bei dex 1. Existiert das gesuchte Objekt nicht im Stack, liefert die Methode den Wert zurück.

5

java.util.BitSet

e Klasse BitSet bietet die Möglichkeit, logische Zustände in Mengenform zu verariten. Um Ihnen gleich ein anschauliches Beispiel anzubieten, möchte ich zu den atei-Rechten im Unix-Dateisystem greifen. Wenn Sie zum Beispiel auf einem Linux-Rechner das Kommando ls aufrufen, erhalten Sie auch Informationen über die Zugriffsrechte jeder Datei und jedes Verzeichnisses: cd / ls -l $ ls -l insgesamt 124 drwxr-xr-x 2 root drwxr-xr-x 2 root ...

root root

4096 Aug 16 12:37 bin 4096 Jul 26 15:25 boot

e Ausgabe ist nur als Beispiel gedacht, auf Ihrem Rechner werden Sie unter Umständen eine andere Liste erhalten. Wichtig in diesem Zusammenhang ist das erste ld in der Ausgabe, es enthält zusätzlich zum Typ der Datei (hier deutet das »d« am nfang auf Verzeichnisse hin) auch die Zugriffsrechte: rwxr-xr-x

r Nicht-Eingeweihte stellt sich natürlich die Frage: »Was bedeutet rwx?«. Nun, in nix gibt es drei verschiedene Rechte: Leserecht, Schreibrecht und das Recht, eine Datei ausführen zu können (bei Verzeichnissen bedeutet das letzte Recht, dass man Dateien im Verzeichnis lesen darf). Jedes einzelne dieser Rechte wird für drei verschiedene Nutzer vergeben: den Eigentümer der Datei (User), die Nutzer, die in der-

Collections I

selben Gruppe sind wie der Gruppe (Group), welcher die Datei zugeordnet ist, sowie alle anderen Nutzer, die nicht zu einer der ersten Kategorie gehören (Other). Der String rwxr-xr-x

bedeutet also: Der Eigentümer besitzt alle Rechte (Leserecht (r), Schreibrecht (w) und Recht zur Ausführung (x)). Gruppenmitglieder sowie alle anderen Nutzer besitzen zwar Leserecht und das Recht zur Ausführung der Datei, aber kein Schreibrecht (statt des Zeichens »w« steht hier »-«). Man kann sich den String für die Rechte als Bitfolge vorstellen: Das Minuszeichen bedeutet »kein Recht«, alles andere bedeutet »Recht vorhanden«. Wenn ich nun den Rechte-String als Folge von Bits aufschreibe, erhalten wir die Bit-Reihe: |U| |G| |O| 111 101 101

Das, was Sie hier sehen, ist nichts anderes als ein BitSet, bestehend aus 9 Bits. Wollen wir nun die Klasse BitSet näher betrachten. 6.5.1

Konstruktoren von BitSet

Zusätzlich zum Default-Konstruktor stellt die Klasse einen weiteren Konstruktor zur Verfügung, mit dem man die Anzahl der Bits einstellen kann: BitSet() BitSet( int nbits )

Die Mindest-Anzahl von Bits beträgt 64 Bits, gibt man mit nbits eine größere Anzahl von Bits an, dann wird immer auf ein Vielfaches von 64 aufgerundet. Natürlich kann man auch mit weniger Bits arbeiten, die Mindestbreite des Bitfeldes ist aber immer 64. Alle Bits eines neu instanzierten Objekts der Klasse BitSet sind gelöscht und damit im Zustand false.

Collections I

5.2

Methoden von BitSet

Setzen von Bits

e Methode set() wird verwendet, wenn man einzelne oder mehrere Bits gezielt tzen möchte: void set( int index ) void set( int fromInd, int toInd ) void set( int index, boolean val )

ie man an der letzten Variante von set() erkennt, kann man damit auch Bits chen, indem man sie gezielt auf false setzt. Bei der zweiten Variante werden alle ts ab einschließlich Index fromInd bis ausschließlich Index toInd gesetzt. Zudem kann eine IndexOutOfBoundsException ausgelöst werden, wenn man einen ungültigen Index gibt. schen von Bits

e Methode clear() dient dem Löschen von Bits: void clear() void clear( int index ) void clear(int fromInd, int toInd )

r fromInd und toInd gilt dasselbe wie beim Setzen von Bits. vertieren von Bits

Mit der Methode flip() können einzelne oder auch mehrere Bits invertiert werden: void flip( int index ) void flip(int fromInd, int toInd )

r fromInd und toInd gilt dasselbe wie beim Setzen von Bits. Abfragen von Bits

um Abfragen eines einzelnen Bits nimmt man die Methode get(). Mit ihr können ch mehrere Bits als Instanz der Klasse BitSet gelesen werden:

Collections I

boolean get( int index ) BitSet get( int fromInd, int toInd )

Für fromInd und toInd gilt dasselbe wie beim Setzen von Bits. Mengen-Operationen

Zum Verknüpfen zweier BitSets dienen die Methoden and() (logische UND-Verknüpfung), or() (logische ODER-Verknüpfung), xor() (logische EXOR-Verknüpfung sowie andNot() (invertierte logische UND-Verknüpfung): void void void void

and( BitSet bs ) or( BitSet bs ) xor( BitSet bs ) andNot( BitSet bs )

Hier sind ein paar Beispiele angebracht: ... // Instanzierung eines BitSet-Objekts mit 3 Bits BitSet bs = new BitSet( 3 ); // Setzen des ersten und des dritten Bits bs.set( 0 ); bs.set( 2 ); // bs enthält nun 101 // Erstellen einer Kopie mit clone() Bitset sav = ( BitSet ) bs.clone(); // Zweites BitSet instanzieren BitSet bs1 = new BitSet( 3 ); // Setzen des zweiten und des dritten Bits bs1.set( 1 ); bs1.set( 2 ); // bs1 enthält nun 011 // UND-Verknüpfung 101 AND 011 bs.and( bs1 ); // bs enthält nun 001

Collections I

// bs // // // // // //

alten Zustand wiederherstellen = ( BitSet ) sav.clone(); Eine Zuweisung wie z.B. bs = sav; würde nicht funktionieren, da bs anschließend eine Referenz auf sav wäre und keine Kopie. Bei einer Mengen-Operation würden wir also das BitSet sav verändern.

// ODER-Verknüpfung 101 OR 011 bs.or( bs1 ); // bs enthält nun 111 // alten Zustand wiederherstellen bs = ( BitSet ) sav.clone(); // EXOR-Verknüpfung 101 EXOR 011 bs.xor( bs1 ); // bs enthält nun 110 // ANDNOT-Verknüpfung 101 ANDNOT 011 = 101 AND 100 bs.andNot( bs1 ); // bs enthält nun 100

verse Methoden

Mit Hilfe der Methode cardinality() kann man feststellen, wie viele Bits im Zustand true sind: int cardinality()

e Methode isEmpty() liefert false zurück, wenn kein Bit gesetzt ist: boolean isEmpty()

eben den hier gezeigten Methoden bietet die Klasse BitSet noch weitere Methoden , die Sie am besten im API des JDK nachlesen können.

Collections II

7

Collections II

Dieses Kapitel beschäftigt sich mit den zusätzlichen Interfaces und Klassen, die im Zuge des Neu-Designs der Java-Collections seit dem JDK 1.2 hinzugekommen sind. Die Klassen aus dem vorangegangenen Kapitel, die ursprünglich das Collection-API von Java darstellten, besitzen einige Schwächen, die mit dem JDK 1.2 und dem damit einhergehenden neuen Design durch die neuen Interfaces und Klassen behoben wurden. Zudem hat man die »alten« Klassen erweitert, damit sie dem neuen CollectionAPI gerecht wurden. Größtes Manko im 1.0-Design der Collections ist wohl die schlechte Performance, weil nahezu alle Methoden synchronized und damit thread-safe sind. Was dies bedeutet, werden wir im Kapitel Multi-Threading erfahren, hier soll genügen, dass der Nachteil von synchronisierten Methoden darin besteht, dass sie grundsätzlich langsamer sind als nicht synchronisierte. Den 1.0-Klassen wird aber auch nachgesagt, dass sie zu wenig vielseitig gestaltet und vom Design zu wenig durchdacht sind. Kurzum, mit der Einführung des JDK 1.2 hat man noch einmal gut über Collections nachgedacht, und dabei herausgekommen sind etliche neue Interfaces und Klassen, die wir unter dem Oberbegriff Collection-Framework nun genauer betrachten wollen.

7.1

Das Collection-Framework

Unter dem Begriff Framework versteht man in der Programmierung einen Satz an Funktionen, die als Basis für die eigentliche Entwicklung von Programmcode dienen, indem sie eine einheitliche Schnittstelle sowie eine bestimmte Grund-Funktionalität zur Verfügung stellen. Alle Interfaces und Klassen des Collection-Framework sind im Package java.util untergebracht. Achtung Alle Methoden des Collection-Frameworks im JDK 1.2 sind generell nicht synchronisiert. Bei gleichzeitigem schreibenden Zugriff auf Daten der Collections durch mehrere Threads muss der Entwickler selbst für die Synchronisation sorgen.

Collections II

Das Collection-Framework von Java besteht im Wesentlichen aus drei Dingen: Interfaces Die neuen Interfaces des JDK 1.2 sind hierarchisch aufgebaut und definieren sozusagen das API für abstrakte Datentypen. Implementierungen Das JDK 1.2 stellt eine Sammlung abstrakter und konkreter Klassen zur Verfügung, die allesamt die Methoden der neuen Collection-Interfaces implementieren. Damit stellen sie praktisch wieder verwendbare Datenstrukturen dar. Algorithmen Die neuen Collection-Klassen des JDK 1.2 enthalten unter anderem polymorphe Methoden für die Suche und die Sortierung der abstrakten Daten. Dadurch, dass die Algorithmen polymorph und Bestandteil des API der Collection-Interfaces sind, können sie auf verschiedenste konkrete Ausprägungen von abstrakten Daten einer Collection angewandt werden, sie stellen sozusagen wieder verwendbare Funktionen dar. Das Collection-Framework des JDK 1.2 besteht aus drei Grund-Typen: Lists Listen sind geordnete Mengen von beliebigen Objekten, auf die sowohl wahlfrei über einen Index als auch sequentiell über einen Iterator zugegriffen werden kann. Die Klasse Vector ist ein typischer Vertreter für eine Liste. Sets Sets sind ungeordnete Mengen von Objekten, die im Gegensatz zu Listen keine Objekt-Duplikate enthalten dürfen. Je nach Implementierung gelten weitere Einschränkungen, so erlauben manche Set-Implementierungen keine null-Werte als Elemente. Andere Implementierungen wiederum garantieren eine bestimmte Sortier-Reihenfolge. Sets sind also nicht immer unsortiert. Auf einzelne Elemente von Sets kann nur sequentiell über einen Iterator zugegriffen werden. Maps Maps sind Mengen, deren Elemente immer aus Key/Value-Paaren bestehen (siehe Klasse Hashtable). Manche Map-Implementierungen garantieren eine bestimmte Sortier-Reihenfolge, andere wiederum besitzen keinerlei vorgegebene Sortierung der Elemente. Maps bieten die Möglichkeit, die Daten in Views anderer Collection-Typen umzuwandeln. Meist greift man auf die Daten über die Keys zu, wie bei der Klasse Hashtable ist es aber auch möglich, nur die Values zu extrahieren.

Collections II

Achtung Grundsätzlich muss man Vorsicht walten lassen, wenn Teilmengen aus einer Collection entnommen werden oder man beim Iterieren über die Elemente Änderungen in der Collection oder den Elementen selbst durchführt. Hier gelten strenge Regeln, die bei Nicht-Beachtung immer zu einer Exception führen.

7.2

Collection-Interfaces

Wie wir bereits aus dem Kapitel OOP gelernt haben, dienen Interfaces unter anderem dazu, eine gemeinsame Schnittstelle (API) für unterschiedliche Klassen anzubieten. Dies vereinheitlicht die Verarbeitung der Daten über Klassen-Grenzen hinweg. Für die Java-Collections werden zu diesem Zweck folgende Interfaces zur Verfügung gestellt:  



 

  

  

Abbildung 7.1: Interface-Hierarchie der Java-Collections

Aus dem Bild ist zum einen erkennbar, dass wir es mit einer recht aufgeräumten und übersichtlichen Hierarchie zu tun haben, andererseits gibt es zwei voneinander unabhängige Interface-Bäume für unterschiedliche Anforderungen. Kompliziert wird es erst, wenn wir auf die Klassen zu sprechen kommen, welche die gezeigten Interfaces implementieren, denn damit steigt die Anzahl der einsetzbaren Varianten ziemlich an, und so mancher kann dabei den Überblick verlieren. Gott sei Dank kann man sich aber anhand der Namensgebung für die einzelnen Klassen orientieren, die am Ende immer den Namen des implementierten Interfaces besitzt, an den Anfang des Klassen-Namens wird der Typ der Implementierung gesetzt. Ein Beispiel hierfür sind LinkedList und HashSet. Die Klasse LinkedList implementiert das List-Interface und verwendet für die Speicherung der Daten eine verkettete Liste, während HashSet das Interface Set implementiert und für die Daten eine HashMap benutzt. Zunächst wollen wir uns den Collection-Interfaces zuwenden. Wie bereits bei den alten Collections aus dem vorangegangenen Kapitel gibt es auch hier ein eigenes Interface für den sequentiellen Zugriff auf die Daten:

Collections II

2.1

Iteratoren

us den alten Collections kennen Sie den Begriff Enumeration, hinter dem sich ein nterface verbirgt, mit dessen Methoden man die einzelnen Elemente einer Hashtable der eines Vektors durchlaufen kann. Mit dem Neu-Design der Collections hat man s Interface java.util.Iterator geschaffen, das bei allen Collection-Klassen ab dem K 1.2 verwendet wird. Dahinter verbirgt sich eigentlich nichts wesentlich Neues, man hat sich nur dem allgemeinen Sprachgebrauch angepasst. Für Programmierer deutet dies, dass sie sich nun mindestens zwei neue Methodennamen merken müsn. Listen besitzen zusätzlich einen speziellen ListIterator, der weitere Funktionaliten anbietet. Hier die in Iterator deklarierten Methoden: Methode

Beschreibung

boolean hasNext()

Gibt true zurück, wenn mindestens noch ein Element vorhanden ist, das mit next() ausgelesen wird. Andernfalls liefert die Methode false zurück.

Object next()

Liefert das nächste Element zurück. Der Aufrufer muss das zurückgelieferte Objekt in den passenden Datentyp mit Casting umwandeln. Wenn kein Element mehr verfügbar ist, löst die Methode eine NoSuchElementException aus.

void remove()

Löscht das mit next() zuletzt gelesene Element aus der Collection. Die Methode darf nur direkt nach einem Call von next() einmal aufgerufen werden. Vor jedem erneuten Aufruf muss ebenfalls vorher remove() aufgerufen werden. Diese Methode ist, wie alle ändernden Methoden von Collections, optional. Sie muss zwar implementiert werden, wenn man dieses Feature aber in seiner Collection-Klasse nicht anbieten möchte, kann man beim Aufruf der Methode eine UnsupportedOperationException werfen.

elle 7.1: Methoden des Interfaces java.util.Iterator

n augenfälliger Unterschied zur Enumeration besteht in der Möglichkeit, mit remove() Elemente auch über einen Iterator löschen zu können. Achtung Vorsicht beim Löschen von Elementen! Wenn eine Instanz für einen Iterator erzeugt worden ist, dann besteht die einzige Möglichkeit, Elemente aus der zugehörigen Collection zu löschen, im Aufruf von remove() des Iterators. Tut man dies direkt über die Methoden des Collection-Objekts, dann sind die Ergebnisse undefiniert. Wir werden diese Problematik näher besprechen, wenn ich auf die Collection-Klassen eingehe.

Collections II

7.2.2

Das Interface java.util.Collection

Das Interface Collection in der obersten Ebene der Collection-Interfaces enthält alle Methoden-Deklarationen, die den darunter liegenden abgeleiteten Interfaces gemeinsam sind, und trägt so zu einem einheitlichen und klaren Design der InterfaceHierarchie bei. Wie bei Klassen-Hierarchien kann es auch als oberster Datentyp beim Casting für die unterschiedlichen konkreten Instanzen der Collection-Klassen verwendet werden. Für denjenigen, der die Collection-Klassen benutzt, bleibt also das API immer gleich, egal, welche konkrete Implementierung verwendet wird. Optionale Methoden

Tja, dies war die Beschreibung der Theorie. In der Praxis sieht das Ganze leider etwas anders aus. Nicht jede Implementierung einer Collection muss das volle Spektrum der Methoden unterstützen, die im Interface Collection deklariert sind. Deshalb hat man diese Methoden in der Spezifikation der Java-Collections als optional definiert (dabei handelt es sich um alle Methoden, mit denen die Elemente einer Collection verändert werden können). Klassen, die das Interface Collection implementieren, müssen zwar aufgrund der Java-Regeln auch alle darin deklarierten Methoden implementieren, können aber eine UnsupportedOperationException auslösen, falls die Funktionalität für die konkrete Implementierung nicht unterstützt ist. In der folgenden Tabelle möchte ich Ihnen einen Überblick über alle Methoden des Interface Collection geben. Methode

optional

boolean add( Object o )

ja

boolean addAll( Collection c )

ja

void clear()

ja

boolean contains( Object o )

-/-

boolean containsAll( Collection c )

-/-

boolean equals( Object o )

-/-

int hashCode()

-/-

boolean isEmpty()

-/-

Iterator iterator()

-/-

boolean remove( Object o )

ja

Tabelle 7.2: Methoden von java.util.Collection

Collections II

Methode

optional

boolean removeAll( Collection c )

ja

boolean retainAll( Collection c )

ja

int size()

-/-

Object[] toArray()

-/-

Object[] toArray( Object[] a )

-/-

elle 7.2: Methoden von java.util.Collection (Forts.)

ele der aufgeführten Methoden sind bereits vom Namen her selbst erklärend, ihre nktionsweise sollten wir bereits von der Klasse Hashtable kennen. Diejenigen ethoden, deren Namen auf All enden, verarbeiten nicht nur ein einzelnes Element, ndern mehrere, die im Argument, das vom Typ Collection ist, übergeben werden. werden zum Beispiel mit der Methode removeAll() alle Elemente gelöscht, die in r als Argument übergebenen Collection vorhanden sind, während die Methode retainAll() alle Elemente löscht, die nicht in der angegebenen Collection existieren. containsAll() liefert dann true zurück, wenn alle in der angegebenen Collection rhandenen Elemente existieren. le Methoden, die zu einer Veränderung der Collection führen können (zum Beiiel add() oder remove()), geben einen boolean-Wert zurück, der true ist, wenn urch den Methoden-Aufruf eine Veränderung stattgefunden hat, andernfalls liefern false zurück. r den sequentiellen Zugriff auf die einzelnen Elemente der Collection ist die Methode iterator() vorhanden, die ähnlich wie das Interface Enumeration arbeitet. 2.3

Das Interface java.util.List

Das List Interface wurde für geordnete Collections geschaffen, bei denen die einzeln Elemente ähnlich wie bei der Klasse Vector mit einem Index versehen werden, r für das erste Element bei 0 beginnt. Es dürfen beliebig viele identische Elemente thalten sein. Manche Implementierungen des Interfaces erlauben Elemente mit m Wert null, andere wiederum nicht. Zusätzlich zu den vererbten Methoden aus Collection bietet dieses Interface Methoden den sequentiellen oder wahlfreien Zugriff über den Index der Elemente sowie Methoden für die Suche nach Elementen an. Was die Verarbeitungsgeschwindigkeit

Collections II

betrifft, hängt diese stark von der konkreten Implementierung ab. Bei manchen Implementierungen ist der sequentielle Zugriff auf die Elemente schneller, bei anderen wiederum sollte man den wahlfreien Zugriff über den Index wählen. Wir werden die Unterschiede kennen lernen, wenn wir zu den konkreten Implementierungen der Interfaces kommen. Hier zunächst ein Überblick über die Methoden des List-Interfaces: Methode

optional

boolean add( Object o )

ja

void add( int ind, Object o )

ja

boolean addAll( Collection c )

ja

boolean addAll( int ind, Collection c )

ja

void clear()

ja

boolean contains( Object o )

-/-

boolean containsAll( Collection c )

-/-

boolean equals( Object o )

-/-

Object get( int ind )

-/-

int hashCode()

-/-

int indexOf( Object o )

-/-

boolean isEmpty()

-/-

Iterator iterator()

-/-

int lastIndexOf( Object o )

-/-

ListIterator listIterator()

-/-

ListIterator listIterator( int ind )

-/-

Object remove( int ind )

ja

boolean removeAll( Collection c )

ja

boolean retainAll( Collection c )

ja

Object set( int ind, Object o )

ja

int size()

-/-

Tabelle 7.3: Methoden von java.util.List

Collections II

Methode ist subList( int fromInd, int toInd )

optional

-/-

Object[] toArray()

-/-

Object[] toArray( Object[] a )

-/-

elle 7.3: Methoden von java.util.List (Forts.)

r die Suche nach Objekten stellt das Interface die Methoden indexOf() sowie lastIndexOf() zur Verfügung, diese arbeiten ähnlich wie die gleichnamigen Methoden der String-Klasse. Hier heißt es allerdings aufpassen, denn in den meisten Implementiengen ist die Suche eine zeitraubende Angelegenheit. emente können nicht nur am Ende der Liste neu hinzugefügt werden, sondern rch Angabe eines Index an beliebiger Stelle, für das Ändern von bestehenden Eleenten verwendet man die Methode set(). Mit get() kann man wahlfrei auf Elemente zugreifen, für den sequentiellen Zugriff sten die Methoden iterator() und listIterator() zur Verfügung. Die beiden Iteraton arbeiten ähnlich wie das Interface Enumeration, bieten aber weiter reichende Funkonalitäten an, so kann man damit zum Beispiel auch Elemente löschen oder hinzufügen. ie die Iteratoren im Einzelnen funktionieren, werden wir weiter unten noch sehen. Mit der Methode subList() kann eine Teilmenge der Liste extrahiert werden. Sie thält alle Elemente ab einschließlich fromInd bis ausschließlich toInd. Diese Methode mit Vorsicht zu genießen, da die zurückgelieferte Liste eine Referenz auf die Elemente der Original-Liste ist, d.h. Änderungen wirken sich beidseitig aus. Damit veretet sich automatisch, dass man zum Beispiel Elemente in der Original-Liste hinzugt oder löscht, nachdem eine Teilmenge extrahiert wurde. va.util.ListIterator

eziell für List-Objekte steht das Interface ListIterator zur Verfügung, das den reits bekannten Iterator erweitert, so dass man die Liste in beiden Richtungen urchlaufen kann:

Collections II

Methode

optional

void add( Object o )

ja

boolean hasNext()

-/-

boolean hasPrevious()

-/-

Object next()

-/-

int nextIndex()

-/-

Object previous()

-/-

int previousIndex()

-/-

void remove()

ja

void set( Object o )

ja

Tabelle 7.4: Methoden von java.util.ListIterator

Die Methoden remove() und set() beziehen sich jeweils auf dasjenige Objekt, welches durch einen vorangegangenen Aufruf von next() oder previous() zurückgeliefert wurde. Die Indizes der Elemente liegen immer zwischen den einzelnen Elementen (es gibt also kein aktuelles Element, das man über den Index ansprechen kann). nextIndex() liefert den Index für das Element, das beim nächsten next()-Aufruf zurückgegeben wird, während previousIndex() den Index für das Element liefert, welches beim nächsten Aufruf von previous() zurückgegeben wird. Wenn kein Element in der Vorwärts-Richtung vorhanden ist, liefert nextIndex() die Länge der Liste zurück, in Rückwärts-Richtung wird von previousIndex() im selben Fall -1 zurückgegeben. Die Methode remove() darf nur nach einem vorhergehenden Aufruf von next() oder previous() verwendet werden (und dann auch nur einmal, vor jedem erneuten Call von remove() muss entweder next() oder previous() aufgerufen werden). Dasselbe gilt für set(). 7.2.4

Das Interface java.util.Set

Das Interface Set ähnelt der bereits bekannten Hashtable, es können also keine identischen Elemente in der Collection vorhanden sein (im Gegensatz zur Hashtable werden bei Set jedoch keine Keys für den Zugriff auf die Elemente verwendet). Interes-

Collections II

santerweise findet sich im Interface keine Methode für den direkten Zugriff auf ein stimmtes Element, sondern nur ein Iterator, mit dem die Elemente in einer Schleife cheinander gelesen werden können. Dabei hat man keinen Einfluss auf die Reihenge der Elemente. Achtung Größte Vorsicht ist geboten, wenn die Elemente einer Set-Collection mutable und damit veränderbar sind. Wird ein Element nach dem Hinzufügen derart geändert, dass dadurch die equals()-Methode einen anderen Wert zurückliefert als vorher, so gilt die gesamte Collection als undefiniert und darf somit nicht mehr benutzt werden. bersicht der angebotenen Methoden des Interfaces Set: Methode

optional

boolean add( Object o )

ja

boolean addAll( Collection c )

ja

void clear()

ja

boolean contains( Object o )

-/-

boolean containsAll( Collection c )

-/-

boolean equals( Object o )

-/-

int hashCode()

-/-

boolean isEmpty()

-/-

terator iterator()

-/-

boolean remove( Object o )

ja

boolean removeAll( Collection c )

ja

boolean retainAll( Collection c )

ja

int size()

-/-

Object[] toArray()

-/-

Object[] toArray( Object[] a )

-/-

elle 7.5: Methoden von java.util.Set

Collections II

java.util.SortedSet

Dieses aus Set abgeleitete Interface garantiert, dass die Elemente der Collection aufsteigend sortiert sind. Die Objekte der Collection müssen zumindest das Interface Comparable implementieren, man spricht dann von natural sorting, was bedeutet, dass die Elemente in der ihrer Klasse entsprechenden natürlichen Reihenfolge sortiert werden (Zahlen sind zum Beispiel aufgrund ihres vorzeichenbehafteten Werts aufsteigend sortiert). Es ist aber auch eine selbst definierte Reihenfolge denkbar, in diesem Fall müssen die Objekte der Collection das Interface Comparator implementieren, das wir bereits bei der Beschreibung der Klasse Arrays kennen gelernt haben. Dort haben wir Strings unter Berücksichtigung der deutschen Umlaute mit Einbeziehung der Locale sortiert, indem wir eine Kind-Klasse von java.text.Collator benutzten. Will man eine selbst definierte Reihenfolge für die Sortierung implementieren, dann ändert man häufig auch die equals()-Methode. In diesem Fall muss auch die Methode hashCode() überschrieben werden, um sicherzustellen, dass Elemente, die in der Reihenfolge identisch sind, auch gleiche Hash-Codes besitzen. Eine weitere Voraussetzung für die Objekte der Collection ist, dass sie mutually comparable sind, das heißt nichts anderes, als dass sie über die Methode compareTo() miteinander vergleichbar sind. Andernfalls muss die Vergleichsmethode eine ClassCastException werfen. Übersicht der zusätzlichen Methoden von SortedSet:  Comparator comparator()

Die Methode liefert den Comparator zurück, der für die Sortierung im Falle einer natürlichen Sortier-Reihenfolge (natural sorting) verwendet wird, oder null, falls ein Comparator für die Sortierung benutzt wird.  Object first()

Diese Methode liefert das gemäß der verwendeten Sortierung erste Element zurück.  SortedSet headSet( Object o )

Die Methode liefert eine Teilmenge der Collection zurück, deren Elemente in der Reihenfolge alle vor dem angegebenen Element kommen.  Object last()

Diese Methode liefert das gemäß der verwendeten Sortierung letzte Element zurück.

Collections II

SortedSet subSet( Object from, Object to )

Die Methode gibt eine Teilmenge der Collection zurück, die alle Elemente einschließlich from bis ausschließlich to enthält. SortedSet tailSet( Object o )

Mit dieser Methode erhält man eine Teilmenge der Collection, deren Elemente in der Reihenfolge alle nach dem angegebenen Element kommen. 2.5

Das Interface java.util.Map

Gegensatz zu den bisher vorgestellten Collection-Interfaces ist Map nicht aus Collection abgeleitet, sondern ein eigenständiges Interface. Es ersetzt die abstrakte asse java.util.Dictionary, die mit dem JDK 1.2 als obsolete gebrandmarkt wurde. ese Klasse sollte also nicht mehr verwendet werden. Map verhält sich ähnlich wie die Klasse Hashtable, indem die Elemente als Key/Value-

aar gespeichert werden, wobei keine Key-Duplikate vorkommen können (Beim nfügen eines Elements, dessen Schlüssel bereits existiert, wird das bestehende Element überschrieben). Je nach Implementierung sind null-Werte erlaubt oder auch nicht. Da das Interface zum Collection-Design gehört, bietet es eine Methode an, lche die Elemente als so genannten Collection-View zurückliefert, damit können e Methoden aus dem Interface Collection auf die Elemente angewandt werden. Achtung Vorsicht ist geboten, wenn die Elemente einer Map mutable (veränderar) sind. Wird ein Element so geändert, dass die Methoden equals() oder hashCode() andere Werte als vorher zurückgeben, dann ist die Map unbrauchbar, weil das Verhalten dieser Methoden dann nicht mehr definiert ist. bersicht der von Map deklarierten Methoden: Methode

optional

void clear()

ja

boolean containsKey( Object key )

-/-

boolean containsValue( Object value )

-/-

Set entrySet()

-/-

elle 7.6: Methoden von java.util.Map

Collections II

Methode

optional

boolean equals( Object o )

-/-

Object get( Object key )

-/-

int hashCode()

-/-

boolean isEmpty()

-/-

Set keySet()

-/-

Object put( Object key, Object value )

ja

void putAll( Map m )

ja

Object remove( Object key )

ja

int size()

-/-

Collection values()

-/-

Tabelle 7.6: Methoden von java.util.Map (Forts.)

Hinweis Die Methode get() liefert den Wert null sowohl dann zurück, wenn kein Element mit dem angegebenen Key existiert, als auch für den Fall, dass der Value des Elements mit dem angegebenen Key null ist. Hier kann mit der Methode containsKey() überprüft werden, ob ein Element gefunden wurde oder nicht. Wie wir aus der Tabelle ersehen, besitzt das Interface Map keine Iteratoren, mit denen die einzelnen Elemente durchlaufen werden können. Stattdessen bietet es Methoden für unterschiedliche Collection-Views an. Sie verwenden dann die Iteratoren des Views, um auf die Daten sequentiell zuzugreifen:  Set entrySet()

Diese Methode liefert eine Menge von Key/Value-Paaren als Set zurück. Der Zugriff auf die einzelnen Elemente gestaltet sich allerdings etwas kompliziert, denn die einzige Möglichkeit, auf Elemente vom Typ Set zuzugreifen, besteht in Form eines Iterators, den man von der Methode iterator() bekommt. In der Schleife, mit der man über die einzelnen Elemente der Collection läuft, muss das jeweilige Element auf den Typ Map.Entry gecastet werden, der als lokales Interface von Map definiert ist. Nun ist man endlich am Ziel und kann mit den Methoden

Collections II

getKey() und getValue() den Key bzw. den Wert des Elements lesen. Hört sich

wirklich kompliziert an, was? Nun, wir werden dafür ein Beispiel sehen, wenn ich zu den konkreten Implementierungen der Collection-Interfaces komme. Es wird aber noch schwieriger, wenn man weiß, dass die Elemente als Referenz auf die Original-Map zurückgeliefert werden und das Interface Map.Entry zusätzlich die Methode setValue() besitzt, mit der man ein Element ändern kann. Diese Änderung wirkt sich also auf die ursprüngliche Map-Collection aus. Es wird noch unübersichtlicher, wenn man zum Beispiel ein Element in der Original-Map löscht, nachdem der Iterator instanziert wurde, dann nämlich ist das Verhalten völlig undefiniert. Will man unbedingt Elemente ändern oder löschen, dann darf man dies nur mit der remove()-Methode des Iterators oder der setValue()-Methode der Map.Entry-Instanz. Zu guter Letzt sei angemerkt, dass die Map.Entry-Objekte nur für die Lebensdauer des Iterators gültig sind. So, nun wissen wir, dass die Methode entrySet() mit größter Vorsicht zu genießen ist. Set keySet()

Diese Methode liefert eine Collection vom Typ Set zurück, die alle Keys der Map enthält. Wir werden bei den Beispielen sehen, wofür man diese Methode verwenden kann. Auch hier gilt: Die zurückgegebene Collection ist eine Referenz auf die Original-Map, Änderungen wirken sich also gegenseitig aus. Falls die Map verändert wird, nachdem ein Iterator der zurückgelieferten Collection erzeugt worden ist, dann sind die Ergebnisse undefiniert. Collection values()

Diese Methode liefert eine Collection vom Typ Collection zurück, die alle Values der Map enthält. Wie bei keySet() erhält man keine Kopie der Map, sondern eine Referenz auf die Values der Original-Map, Änderungen wirken sich also gegenseitig aus. Auch hier ist das Verhalten undefiniert, wenn man nach dem Erzeugen eines Iterators die Original-Map verändert. va.util.SortedMap

eses aus java.util.Map abgeleitete Interface wird für sortierte Elemente verwendet ergleiche hierzu auch SortedSet). Alle Elemente müssen das Interface Comparable implementieren (oder vom Interface Comparator akzeptiert werden). Zusätzlich ist fordert, dass alle Elemente die compareTo()-Methode in Einklang mit equals() plementieren, da die Map sonst unterschiedliche Ergebnisse liefern kann, je nachm, wie sie verwendet wird (beim Vergleich zweier gleicher Elemente mit compareTo() muss 0 zurückgeliefert werden).

Collections II

Methode

optional

Comparator comparator()

-/-

Object firstKey()

-/-

SortedMap headMap( Object to )

-/-

Object lastKey()

-/-

sortedMap subMap( Object from, Object to )

-/-

sortedMap tailMap( Object from )

-/-

Tabelle 7.7: Methoden von java.util.SortedMap  comparator()

Diese Methode liefert entweder den Komparator zurück, der für die Map definiert wurde, oder null, falls die natürliche Sortierung der Elemente benutzt wird.  firstKey()

Diese Methode liefert das erste Element gemäß der Sortierung zurück. Wenn die Map leer ist, wird eine NoSuchElementException ausgelöst.  headMap( Object to )

Diese Methode liefert eine Teilmenge der Map zurück, deren Keys kleiner sind als der mit to angegebene und damit in der Sortier-Reihenfolge vor diesem stehen. Gibt man einen nicht vorhandenen Key als Argument an, dann wird eine IllegalArgumentException ausgelöst. Ist to null, und die Implementierung der Map lässt keine null-Werte in Keys zu, dann wird eine NullPointerException geworfen. Will man eine Teilmenge, die auch das angegebene Element enthält, kann man sich bei einigen Datentypen mit einem Trick behelfen. Hier die Vorgehensweise bei Strings: headMap( to + "\0" ) Vorsicht! Die Elemente der zurückgelieferten Teilmenge sind Referenzen auf die Elemente der Original-Map, Änderungen wirken sich also gegenseitig aus.  lastKey()

Diese Methode liefert das letzte Element gemäß der Sortierung zurück. Wenn die Map leer ist, wird eine NoSuchElementException ausgelöst.

Collections II

subMap( Object from, Object to )

Diese Methode liefert eine Teilmenge zurück, in der alle Elemente einschließlich from und ausschließlich to enthalten sind. Will man auch to in der Teilmenge haben, muss man zum selben Trick wie bei headMap() greifen. Genauso kann man mit subMap( from + "\0", to ) erreichen, dass weder from noch to enthalten sind. Gibt man einen nicht vorhandenen Key bei einem der Argumente an, dann wird eine IllegalArgumentException ausgelöst. Ist from oder to null, und die Implementierung der Map lässt keine null-Werte in Keys zu, dann wird eine NullPointerException geworfen. Vorsicht! Die Elemente der zurückgelieferten Teilmenge sind Referenzen auf die Elemente der Original-Map, Änderungen wirken sich also gegenseitig aus. tailMap( Object from )

Diese Methode liefert eine Teilmenge der Map zurück, deren Keys größer als oder gleich dem mit from angegebenen sind und damit in der Sortier-Reihenfolge ab diesem stehen. Gibt man einen nicht vorhandenen Key als Argument an, dann wird eine IllegalArgumentException ausgelöst. Ist from null, und die Implementierung der Map lässt keine null-Werte in Keys zu, dann wird eine NullPointerException geworfen. Will man eine Teilmenge, die das angegebene Element nicht enthält, kann man sich bei einigen Datentypen mit einem Trick behelfen. Hier die Vorgehensweise bei Strings: tailMap( from + "\0" ) Vorsicht! Die Elemente der zurückgelieferten Teilmenge sind Referenzen auf die Elemente der Original-Map, Änderungen wirken sich also gegenseitig aus.

3

Collection-Implementierungen

Nachdem wir nun eingehend die Collection-Interfaces besprochen haben, wenden r uns nun den Implementierungen zu. Zunächst möchte ich Ihnen einen Überblick er die vorhandenen Klassen geben: ie das Bild zeigt, sind alle konkreten Implementierungen aus abstrakten Klassen geleitet. Dies hat den Vorteil, dass man nicht alle Methoden neu implementieren uss, wenn man eine eigene Collection-Klasse erstellt. In diesem Buch möchte ich ch aber auf die vorhandenen konkreten Klassen beschränken.

Collections II



    

 

  

    

    

 

 

   

  



 

 

   

  

   

Abbildung 7.2: Übersicht der Collection-Implementierungen

7.3.1

List-Implementierungen

In Abbildung 7.2 sehen wir einen alten Bekannten wieder. Die Klasse Vector wurde erweitert, so dass sie nun auch das Interface List implementiert. Ansonsten besteht der Unterschied zur Klasse ArrayList im Wesentlichen darin, dass die Methoden von Vector synchronisiert und damit langsamer sind als die von ArrayList. Bei der Wahl zwischen ArrayList und LinkedList sollte man berücksichtigen, welche Listen bearbeitet werden. LinkedList implementiert die Liste als doppelt verkettete Liste. Das bedeutet bei kleineren Listen, die mehrheitlich nicht sequentiell lesend benutzt werden, eine Performance-Einbuße gegenüber ArrayList. Auf der anderen Seite ist sie bei großen Listen schneller, bei denen häufig Elemente eingefügt oder gelöscht werden. Werden Listen von mehreren Threads gleichzeitig schreibend benutzt, sollten Sie auf Vector zurückgreifen, da diese Klasse die Synchronisation automatisch liefert. Sie haben die Wahl. java.util.ArrayList

Auf der CD-ROM finden Sie ein Beispiel für die Benutzung der Klasse ArrayList in der Datei ArrayListDemo01.java. Es werden die gängigen List-Operationen durchgeführt (Anfügen neuer Elemente am Ende der Liste, Einfügen von Elementen an beliebigen Positionen, Löschen von Elementen). Das Beispiel-Programm führt auch

Collections II

eitmessungen der einzelnen Operationen durch. Es zeigt sich, dass ArrayList die eignete Klasse ist, um bei indizierten Elementen mit wahlfreiem oder sequentiellem ugriff eine gute Performance bei niedrigem Ressourcen-Verbrauch zu erreichen. enso wird deutlich, dass Mengen-Operationen sehr viel Zeit beanspruchen: ... Random rnd = new Random(); ArrayList lst1 = new ArrayList(); ArrayList lst2 = new ArrayList(); // Füllen beider Listen mit Zufalls-Zahlen lst1.add( new Integer( rnd.nextInt( 1000000 ) ); ... // Löschen aller Elemente aus lst1, die in lst2 // enthalten sind lst1.removeAll( lst2 );

zeigt sich, dass die Lösch-Operation weit mehr als 100-mal langsamer ist, als würde an dieselbe Anzahl von Elementen direkt durch Angabe des Index löschen, wie zum ispiel mit folgendem Code: ... Random rnd = new Random(); int[] delInds = new int[ addCount ]; for ( int i = 0; i < delInds.length; i++ ) { delInds[ i ] = rnd.nextInt( lst1.size() );

for ( int i = 0; i < delInds.length; i++ ) { int ind = delInds[ i ]; if ( ind >= lst1.size() ) ind = lst1.size() - 1; lst1.remove( ind );

lerdings sind generell alle Operationen von ArrayList in der momentanen Implementierung des JDK 1.4 immer noch schneller als die entsprechenden Operationen n LinkedList, zudem ist der Speicherbedarf geringer. Auch hierfür sehen Sie ein ispiel, wenn ich zur LinkedList-Implementierung komme.

Collections II

Konstruktoren von java.util.ArrayList  ArrayList()

Default-Konstruktor, der eine leere Liste mit einer Speicher-Reservierung von 10 Elementen anlegt  ArrayList( int initialCapacity )

Wie Default-Konstruktor, jedoch kann man die Anzahl der Elemente, für die Speicher reserviert wird, explizit angeben. Ist das Argument negativ, setzt es eine IllegalArgumentException.  ArrayList( Collection c )

Legt eine Liste an, die initial mit der Kopie von c gefüllt wird. Die Reihenfolge der Elemente entspricht dabei der Reihenfolge, in welcher sie vom Iterator der Collection c zurückgegeben werden. Die initiale Kapazität beträgt 110% von c. Ist das Argument null, so wird eine NullPointerException geworfen. Methoden von java.util.ArrayList  boolean add( Object o )

Das angegebene Objekt wird ans Ende der Liste angehängt. Diese Methode gibt immer true zurück (gemäß Collection-Konvention). Auch null-Werte sind erlaubt.  void add( int index, Object o )

Fügt das angegebene Objekt an Position index ein. Falls an dieser oder nachfolgenden Positionen bereits Elemente vorhanden sind, werden sie alle um eine Position weiter nach hinten versetzt. Das erste Element hat den Index 0, das letzte ist an Position list.size()-1 zu finden. Diese Methode wirft eine IndexOutOfBoundsException, wenn ein ungültiger Index angegeben wird (kleiner als 0 oder größer als size()).  boolean addAll( Collection c )

Fügt alle Elemente der angegebenen Collection in der Reihenfolge am Ende an, die durch den Iterator von c vorgegeben ist. Wenn c null ist, wird eine NullPointerException geworfen. Während der Operation darf die angegebene Collection c nicht verändert werden, da sonst die Liste einen undefinierten Zustand hat. Aus diesem Grund darf das Argument in keinem Fall identisch mit der Instanz sein, über welche die Methode aufgerufen wird (l.addAll(l) geht nicht). Die Methode liefert true zurück, wenn die Liste durch den Aufruf der Methode verändert wurde, ansonsten false.

Collections II

boolean addAll( int index, Collection c )

Diese Methode arbeitet praktisch wie eine Verknüpfung der beiden Methoden add( int index, Object o ) und addAll( Collection c ). void clear()

Nach dem Aufruf dieser Methode enthält die Liste keine Elemente mehr. Object clone()

Diese Methode liefert eine Kopie der Liste. Die Elemente der Liste werden jedoch nicht kopiert, sondern als Referenz übergeben (es werden nur die Referenzen kopiert). boolean contains( Object o )

Diese Methode liefert true zurück, wenn die Liste das angegebene Element enthält, ansonsten false. Der Test funktioniert auch dann, wenn das Argument null ist. Andernfalls wird er mit der Methode equals() durchgeführt. boolean containsAll( Collection c )

Diese Methode wird von AbstractCollection geerbt und liefert true zurück, wenn die Liste alle Elemente der angegebenen Collection enthält, andernfalls false. void ensureCapacity( int capacity )

Falls die angegebene Kapazität größer als die aktuelle ist, wird die Speicher-Reservierung entsprechend angepasst. boolean equals( Object o )

Diese Methode (sie wird von AbstractList geerbt) liefert nur dann true zurück, wenn das Argument o ebenfalls eine Liste ist und alle Elemente beider Listen in derselben Reihenfolge gleich sind (dies bedeutet, dass false zurückgeliefert wird, wenn die angegebene Liste zwar genau dieselben Elemente besitzt, diese aber in unterschiedlicher Reihenfolge). Object get( int index )

Liefert das Objekt mit dem angegebenen Index zurück. Wenn index negativ oder größer bzw. gleich list.size() ist, dann wirft die Methode eine IndexOutOfBoundsException. int hashCode()

Diese Methode wird von AbstractList geerbt und gibt den numerischen Wert zurück, der als interner Key für das Speichern von Elementen verwendet wird. Bei einer Änderung der Liste (Hinzufügen/Entfernen von Elementen) ändert sich jeweils der Hash Code.

Collections II

 int indexOf( Object o )

Diese Methode liefert die Position des ersten Elements der Liste, der gleich dem angegebenen Objekt ist (Die Gleichheits-Prüfung erfolgt mit equals()). Wenn kein passendes Element gefunden werden kann, liefert die Methode den Wert -1 zurück.  boolean isEmpty()

Diese Methode liefert true zurück, wenn die Liste leer ist und somit keine Elemente enthält, ansonsten false.  Iterator iterator()

Diese Methode wird von AbstractList geerbt und liefert einen Iterator für die Liste zurück. Da die Klasse ArrayList die Iterator-Methode remove(int) überschreibt, darf sie benutzt werden, ohne eine Exception zu verursachen. Wenn jedoch gleichzeitig von verschiedenen Programmstellen oder auch durch mehrfach vorhandene Referenzen auf dieselbe Liste eine Änderung der Liste erfolgt, setzt es in jedem Fall eine ConcurrentModificationException.  int lastIndexOf( Object o )

Wie indexOf(), jedoch wird im Erfolgsfall der Index des letzten Elements zurückgegeben, das identisch ist mit o. Interessanterweise findet sich in der Klasse keine Methode indexOf(Object o, int start), diese Funktionalität muss man selbst implementieren.  ListIterator listIterator()

Diese Methode liefert einen List Iterator zurück, mit dem die Liste in beiden Richtungen Element für Element durchlaufen werden kann. Bezüglich gleichzeitiger Änderungen von verschiedenen Threads oder mit verschiedenen Referenzen gilt dasselbe wie bei der Methode iterator().  ListIterator listIterator( int start )

Wie listIterator(), jedoch kann man den Start-Index angeben, der für den Aufruf von next() bzw. previous() verwendet wird.  Object remove( int index )

Diese Methode entfernt das Element an der angegebenen Position. Wenn index negativ oder größer/gleich list.size() ist, setzt es eine IndexOutOfBoundsException.  boolean remove( Object o )

Diese Methode ist von AbstractCollection geerbt. Sie funktioniert ähnlich wie die vorangegangene, erwartet aber als Argument ein Objekt, das mittels eines Ite-

Collections II

rators in der Liste gesucht wird. Das erste Element der Liste, welches identisch ist mit dem angegebenen Objekt, wird aus der Liste entfernt. Die Methode liefert true zurück, wenn ein Element entfernt wurde, ansonsten false. boolean removeAll( Collection c )

Die Methode wird von AbstractCollection geerbt. Es werden alle Elemente entfernt, die in der angegebenen Collection c enthalten sind. Falls in der Liste mehrere identische Objekte vorhanden sind, werden sie im Gegensatz zur Methode remove(Object o) alle gelöscht. Falls null übergeben wird, wirft die Methode eine NullPointerException. Hat sich nach dem Aufruf der Methode die Liste geändert, dann liefert sie true zurück, ansonsten false. boolean retainAll( Collection c )

Diese Methode wird von AbstractCollection geerbt und ist das Gegenteil von removeAll(). Es werden alle Elemente der Liste gelöscht, die nicht in der Collection c enthalten sind. Falls null übergeben wird, wirft die Methode eine NullPointerException. Hat sich nach dem Aufruf der Methode die Liste geändert, dann liefert sie true zurück, ansonsten false. Object set( int index, Object o )

Diese Methode ersetzt das Element an der angegebenen Position durch das übergebene Objekt. Falls index negativ oder größer/gleich list.size() ist, setzt es eine IndexOutOfBoundsException. Die Methode liefert das ersetzte Objekt zurück. int size()

Diese Methode liefert die Anzahl der Elemente in der Liste zurück. List subList( int from, int to )

Diese Methode liefert eine Teil-Liste zurück, die alle Elemente der ursprünglichen Liste einschließlich Start-Position from und ausschließlich Ende-Postion to enthält. Sind from und to identisch, dann ist die zurückgelieferte Liste leer. Vorsicht! Die Liste enthält Referenzen auf die ursprüngliche Liste, deshalb wirken sich Änderungen beidseitig aus. Grundsätzlich gilt: Wird die Liste in irgendeiner Art und Weise direkt verändert, so dass sich deren Größe oder das Verhalten beim Iterieren ändert, dann ist die Teil-Liste in einem undefinierten Zustand. Man kann diese Methode für Bereichs-Operationen verwenden, hier ein Beispiel für das Löschen eines Bereichs: list.subList( from, to ).clear();

Beispiel für eine Bereichssuche: list.subList( from, to ).indexOf( searchObj );

Collections II

 Object[] toArray()

Diese Methode liefert ein Array aus Objekten zurück, das die Elemente der Liste in derselben Reihenfolge enthält, wie man sie beim Iterieren erhalten würde.  Object[] toArray( Object[] targetType )

Diese Methode funktioniert wie die vorangegangene ohne Argument, man kann jedoch den Typ des zurückgelieferten Arrays bestimmen, er ist nämlich derselbe wie das Argument. Ist das übergebene Array groß genug, um alle List-Elemente aufzunehmen, so werden die List-Elemente zusätzlich im übergebenen Array gespeichert. Hat das übergebene Array mehr Elemente als die Liste, dann wird das Array-Element, welches direkt auf das letzte List-Element folgt, mit null belegt (nur dann sinnvoll auswertbar, wenn keine null-Werte in der Liste verwendet werden). In allen Fällen, in denen die List-Elemente im übergebenen Argument gespeichert werden, ist der Return-Wert der Methode eine Referenz auf das übergebene Array. Nur wenn das übergebene Array kleiner ist als die Liste, wird ein neues Array erzeugt und zurückgegeben. Die Methode wirft eine ArrayStoreException, falls die Klasse des angegebenen Arrays keine Eltern-Klasse aller Elemente der Liste ist. Das angegebene Array darf nicht null sein! Ich glaube, hier ist ein Beispiel nötig: ArrayList al = new ArrayList(); al.add( "1" ); a1.add( "2" ); String[] s1 = new String[ 0 ]; String[] s2 = { "a", "b", }; String[] s3 = { "c", "d", "e", "f", }; String r = ( String[] ) ( al.toArray( s1 ) ); // r enthält alle Elemente der Liste al, s1 bleibt // leer r = ( String[] ) ( al.toArray( s2 ) ); // Die Elemente werden in das Array kopiert, weil // genügend Platz ist. // r ist in diesem Fall eine Referenz auf s2! r = ( String[] ) ( al.toArray( s3 ) ); // Die Elemente werden wie vorher in das Array kopiert, // und das dritte Element von s3 wird mit null belegt. // r ist wiederum eine Referenz auf s3. // s3 enthält nun { "1", "2", null, "f" }

Collections II

void trimToSize()

Der Aufruf dieser Methode führt dazu, dass die Kapazität der Liste genauso groß wird wie die aktuelle Länge. Man kann sie für Speicher-Minimierungen verwenden. va.util.LinkedList

Der Vorteil einer LinkedList-Implementierung gegenüber einer ArrayList ist, dass man damit automatisch Stacks, Queues und ähnliche Gebilde ohne Mehr-Aufwand Programmierung leicht implementieren kann, da bereits einige notwendige ethoden eingebaut sind (so gibt es für die Operationen Lesen und Löschen die öglichkeit, auf das erste bzw. das letzte Element zuzugreifen). lerdings sei angemerkt, dass in der gegenwärtigen Implementierung des JDK 1.4 mtliche Operationen wesentlich langsamer sind als bei ArrayList-Objekten. Zudem rbraucht ein LinkedList-Objekt mehr Speicher als ein vergleichbares ArrayListObjekt. Auf der CD-ROM finden Sie ein Beispiel für die Benutzung von LinkedList-Objekten in der Datei LinkedListDemo01.java. Darin sind auch Zeitmessungen thalten. Es werden dieselben Operationen durchgeführt wie bei ArrayList (Datei rayListDemo01.java). enn Sie die Zeit-Werte vergleichen, werden Sie die Langsamkeit von LinkedList utlich bemerken. Schrauben Sie die Anzahl der List-Elemente auch noch in Gröenordnungen von mehr als einer Million Elemente, dann kann es durchaus sein, dass eine OutOfMemoryException der Virtual Machine ausgelöst wird (ArrayList mit derseln Anzahl von Elementen dagegen läuft noch). Hinweis Wenn Sie OutOfMemoryException-Fehler erhalten, können Sie beim Aufruf der Virtual Machine mit dem Schalter -Xmx256m zum Beispiel dafür sorgen, dass der Virtual Machine mehr Speicher zur Verfügung steht (im Beispiel sind das 256MByte), eine Exception wird dann nicht mehr so schnell ausgelöst. onstruktoren von java.util.LinkedList LinkedList()

Default-Konstruktor, der eine leere Liste anlegt LinkedList( Collection c )

Wie Default-Konstruktor, jedoch werden alle Elemente der angegebenen Collection c in die Liste übernommen. Die Reihenfolge ist dieselbe, wie sie der Iterator von c vorgibt.

Collections II

Methoden von java.util.LinkedList  boolean add( Object o )

Siehe gleichnamige Methode von ArrayList  void add( int index, Object o )

Siehe gleichnamige Methode von ArrayList  boolean addAll( Collection c )

Siehe gleichnamige Methode von ArrayList  boolean addAll( int index, Collection c )

Siehe gleichnamige Methode von ArrayList  void addFirst( Object o )

Hängt das angegebene Objekt am Beginn der Liste ein.  void addLast( Object o )

Fügt das angegebene Objekt am Ende der Liste an.  void clear()

Löscht die Liste.  Object clone()

Siehe gleichnamige Methode von ArrayList  boolean contains( Object o )

Siehe gleichnamige Methode von ArrayList  boolean containsAll( Collection c )

Siehe gleichnamige Methode von ArrayList  boolean equals( Object o )

Siehe gleichnamige Methode von ArrayList  Object get( int index )

Siehe gleichnamige Methode von ArrayList  Object getFirst()

Liefert das erste Element der Liste zurück.  Object getLast()

Liefert das letzte Element der Liste zurück.

Collections II

int hashCode()

Siehe gleichnamige Methode von ArrayList int indexOf( Object o )

Siehe gleichnamige Methode von ArrayList boolean isEmpty()

Siehe gleichnamige Methode von ArrayList Iterator iterator()

Siehe gleichnamige Methode von ArrayList int lastIndexOf( Object o )

Siehe gleichnamige Methode von ArrayList ListIterator listIterator()

Siehe gleichnamige Methode von ArrayList ListIterator listIterator( int index )

Siehe gleichnamige Methode von ArrayList Object remove( int index )

Siehe gleichnamige Methode von ArrayList boolean remove( Object o )

Siehe gleichnamige Methode von ArrayList boolean removeAll( Collection c )

Siehe gleichnamige Methode von ArrayList Object removeFirst()

Löscht das erste Element der Liste. Alle weiteren Elemente rutschen um eine Position nach vorne. Diese Operation ist wesentlich schneller als dieselbe von ArrayList. Wenn die Liste leer ist, wirft die Methode eine NoSuchElementException. Object removeLast()

Löscht das letzte Element der Liste. Alle weiteren Elemente rutschen um eine Position nach vorne. Diese Operation ist wesentlich schneller als dieselbe von ArrayList. Wenn die Liste leer ist, wirft die Methode eine NoSuchElementException. boolean retainAll( Collection c )

Siehe gleichnamige Methode von ArrayList.

Collections II

 Object set( int index, Object o )

Siehe gleichnamige Methode von ArrayList.  int size()

Siehe gleichnamige Methode von ArrayList.  List subList( int from, int to )

Siehe gleichnamige Methode von ArrayList.  Object[] toArray()

Siehe gleichnamige Methode von ArrayList.  Object[] toArray( Object[] targetType )

Siehe gleichnamige Methode von ArrayList. 7.3.2

Set-Implementierungen

java.util.HashSet

Diese Klasse implementiert das Interface Set und verwendet intern eine HashMapInstanz für die Speicherung der Elemente. Die Reihenfolge der Elemente ist weder fest vorgegeben noch konstant. null-Elemente sind erlaubt. Jedes Objekt kann in einem HashSet nur ein einziges Mal ohne Duplikate vorkommen. Bezüglich der Performance gelten die Regeln analog zur Klasse Hashtable, ebenso die initialen Einstellungen für den Load Factor und die Kapazität. Hinweis Sowohl der Load Factor als auch die Kapazität von HashSet können nur einmalig im Konstruktor gesetzt werden, anschließend kann man beide Werte nicht mehr verändern. Auf der CD-ROM finden Sie in der Datei HashSetDemo01.java ein Beispiel, das ähnliche Operationen durchführt wie die bereits besprochenen List-Implementierungen. Auch Zeitmessungen sind darin enthalten. In der Gegenüberstellung zeigt sich, dass Einfüge-Operationen bei HashSet geringfügig mehr Zeit in Anspruch nehmen als bei List-Implementierungen. Dies liegt daran, dass Hash Sets keine Duplikate enthalten können. Werden jedoch Bulk-Operationen wie zum Beispiel removeAll() durchgeführt, so hat die Klasse HashSet eindeutig um Längen die Nase vorn. Bedingt durch das Set-Interface kann man jedoch nicht über einen Index auf einzelne Elemente des Hash Sets zugreifen, sondern nur über einen Iterator.

Collections II

onstruktoren von java.util.HashSet HashSet()

Default-Konstruktor. Er legt ein leeres Set an. Die initiale Kapazität ist 16 und der Load Factor ist 0.75. HashSet( Collection c )

Bei diesem Konstruktor kann man eine Collection angeben, deren Elemente in das Set aufgenommen werden. Die Kapazität wird so groß eingestellt, dass alle Elemente Platz finden, der Load Factor ist 0.75. HashSet( int initialCapacity )

Wie Default-Konstruktor, jedoch kann man die initiale Kapazität einstellen. HashSet( int initialCap, float loadFactor )

Bei diesem Konstruktor können sowohl die initiale Kapazität als auch der Load Factor eingestellt werden. Methoden von java.util.HashSet boolean add( Object o )

Nimmt das angegebene Objekt in das Set auf, falls es nicht bereits existiert. Die Methode liefert true zurück, wenn das Element aufgenommen wurde, andernfalls false. boolean addAll( Collection c )

Diese Methode wird von AbstractCollection geerbt. Siehe gleichnamige Methode von ArrayList. void clear()

Leert das Set. Object clone()

Gibt eine einfache Kopie des Sets zurück. Vorsicht! Die darin befindlichen Objekte werden nicht kopiert, sondern nur deren Referenzen, es gilt also das callby-reference-Prinzip (Änderungen im kopierten Set wirken sich auf das OriginalSet aus und umgekehrt). boolean contains( Object o )

Liefert true zurück, wenn das angegebene Objekt im Set enthalten ist, andernfalls false.

Collections II

 boolean containsAll( Collection c )

Diese Methode wird von AbstractCollection geerbt. Siehe gleichnamige Methode von ArrayList.  boolean equals( Object o )

Diese Methode wird von AbstractSet geerbt. Sie liefert true zurück, wenn das angegebene Objekt mit dem aktuellen Set identisch ist, andernfalls false. Es wird nicht nur die Gleichheit der Objekt-Referenzen verglichen, sondern beide Sets werden hinsichtlich gleicher Größe und identischer Set-Elemente geprüft.  int hashCode()

Diese Methode wird von AbstractSet geerbt und funktioniert wie die gleichnamige Methode von AbstractList.  boolean isEmpty()

Diese Methode liefert true zurück, wenn das Set leer ist, andernfalls false.  Iterator iterator()

Liefert einen Iterator für das Set zurück, mit dem auf die Elemente sequentiell (ohne bestimmte Reihenfolge) zugegriffen werden kann.  boolean remove( Object o )

Entfernt das angegebene Objekt aus dem Set, falls es vorhanden ist. Die Methode liefert true zurück, falls ein Element entfernt wurde, andernfalls false.  boolean removeAll( Collection c )

Diese Methode wird von AbstractSet geerbt und entfernt alle Elemente des Sets, die in der angegebenen Collection c vorhanden sind.  boolean retainAll( Collection c )

Diese Methode wird von AbstractCollection geerbt und löscht alle Elemente des Sets, die nicht in der angegebenen Collection c vorhanden sind.  int size()

Diese Methode gibt die Anzahl der Elemente im Set zurück.  Object[] toArray()

Siehe gleichnamige Methode von ArrayList.  Object[] toArray( Object[] targetType )

Siehe gleichnamige Methode von ArrayList.

Collections II

va.util.LinkedHashSet

ese Klasse verbindet eine LinkedList mit einem HashSet. Dadurch ist die Reihenlge der Elemente konstant und ändert sich nicht, so wie das bei HashSet der Fall ist. e Klasse findet vor allem dann Anwendung, wenn man eine beliebige Collection pieren möchte, ohne die Reihenfolge der Elemente zu verändern. LinkedHashSet bietet dieselben Konstruktoren und Methoden wie HashSet an und

gt keine weiteren Funktionalitäten hinzu (außer dass die Reihenfolge der Elemente nstant ist). Auf der CD-ROM ist ein Beispiel für die Verwendung der Klasse LinkedHashSet in der Datei LinkedHashSetDemo01.java enthalten. va.util.TreeSet

ese Klasse wird immer dann verwendet, wenn man die Elemente einer Collection rtiert abspeichern möchte. Die Sortierung erfolgt entweder über das Comparableterface, das von jedem Element des Sets implementiert werden muss, oder durch nen Comparator, den man im Konstruktor angeben kann. Aufgrund der Sortierung die Performance bei TreeSet-Objekten zum Teil wesentlich geringer als bei HashSet- oder LinkedHashSet-Objekten. Auch für diese Klasse habe ich ein Beispiel auf r CD-ROM für Sie in der Datei TreeSetDemo01.java. Konstruktoren von java.util.TreeSet TreeSet()

Default-Konstruktor. Er erzeugt ein leeres Set. Alle Elemente werden in ihrer natürlichen Weise sortiert und müssen das Interface Comparable implementieren. Es ist aufgrund dieser Einschränkung zum Beispiel nicht möglich, Integer- und String-Objekte gemischt in das Set aufzunehmen, weil diese nicht gegenseitig vergleichbar sind (mutually comparable). TreeSet( Collection c )

Wie Default-Konstruktor, jedoch kann das Set initial mit Daten einer Collection gefüttert werden. Der Konstruktor wirft eine NullPointerException, wenn die Collection c null ist. TreeSet( Comparator comp )

Bei diesem Konstruktor kann man einen Comparator angeben, der die Sortierung der Elemente übernimmt. Standardmäßig wird die abstrakte Klasse java.text. Collator und die daraus abgeleitete konkrete Kind-Klasse java.text.RuleBasedCollator mit Java ausgeliefert, die beide das Interface java.util.Comparator implementieren. Auf diese Weise kann man eigene Comparatoren entwickeln, um beliebige Sortierungen durchzuführen.

Collections II

 TreeSet( SortedSet set )

Bei diesem Konstruktor kann man anstelle einer Collection eine bereits sortierte Liste angeben, ansonsten ist er identisch mit TreeSet( Collection c ) Methoden von java.util.TreeSet  boolean add( Object o )

Diese Methode arbeitet wie die gleichnamigen Methoden der bisher besprochenen Klassen, jedoch liefert sie nur dann true zurück, wenn das angegebene Element vorher noch nicht existiert hat, andernfalls wird das Element nicht in das Set aufgenommen, und die Methode gibt false zurück. Wenn das aufzunehmende Element nicht mit den bereits existierenden Elementen verglichen werden kann (weil zum Beispiel die Datentypen nicht vergleichbar sind oder das Objekt das Interface Comparable nicht implementiert bzw. vom verwendeten Comparator nicht sortiert werden kann), dann wirft die Methode eine ClassCastException.  boolean addAll( Collection c )

Wie vorher, jedoch werden alle Elemente der Collection c aufgenommen und dabei gleichzeitig sortiert. Auch hier gilt: Sind mehrere identische Elemente in der Collection enthalten, dann wird von diesen jeweils nur das erste Element in das Set aufgenommen. Eine NullPointerException wird geworfen, wenn das Argument null ist. Sind die aufzunehmenden Elemente nicht vergleichbar (siehe auch add( Object o )), dann löst die Methode eine ClassCastException aus.  void clear()

Leert das Set.  Object clone()

Gibt eine einfache Kopie des Sets zurück. Vorsicht! Die darin befindlichen Objekte werden nicht kopiert, sondern nur deren Referenzen, es gilt also das callby-reference-Prinzip (Änderungen im kopierten Set wirken sich auf das OriginalSet aus und umgekehrt).  Comparator comparator()

Diese Methode liefert entweder eine Referenz auf den verwendeten Comparator oder den Wert null zurück, wenn kein Comparator benutzt wird.  boolean contains( Object o )

Liefert true zurück, wenn das angegebene Objekt im Set enthalten ist, andernfalls false. Die Methode wirft eine ClassCastException, wenn das angegebene Objekt nicht mit den Elementen des Sets vergleichbar ist.

Collections II

boolean containsAll( Collection c )

Siehe gleichnamige Methode von ArrayList. boolean equals( Object o )

Diese Methode wird von AbstractSet geerbt. Sie liefert true zurück, wenn das angegebene Objekt mit dem aktuellen Set identisch ist, andernfalls false. Es wird nicht nur die Gleichheit der Objekt-Referenzen verglichen, sondern beide Sets werden hinsichtlich gleicher Größe und identischer Set-Elemente geprüft. Object first()

Liefert das erste Objekt des Sets zurück. int hashCode()

Diese Methode wird von AbstractSet geerbt und funktioniert wie die gleichnamige Methode von AbstractList. SortedSet headSet( Object to )

Siehe Methode headMap() von SortedMap. Zusätzlich kann diese Methode eine ClassCastException werfen, wenn das angegebene Objekt nicht mit den Elementen des Sets verglichen werden kann. boolean isEmpty()

Diese Methode liefert true zurück, wenn das Set leer ist, andernfalls false. Iterator iterator()

Liefert einen Iterator für das Set zurück, mit dem man sequentiell auf die sortierten Elemente zugreifen kann. Object last()

Liefert das letzte Element des Sets zurück. boolean remove( Object o )

Entfernt das angegebene Objekt aus dem Set. Die Methode liefert true zurück, wenn das Objekt vorhanden war, andernfalls false. Kann das Objekt nicht mit den Elementen des Sets verglichen werden, dann wirft die Methode eine ClassCastException. boolean removeAll( Collection c )

Diese Methode wird von AbstractSet geerbt und entfernt alle Elemente des Sets, die in der angegebenen Collection c vorhanden sind. boolean retainAll( Collection c )

Diese Methode wird von AbstractCollection geerbt und löscht alle Elemente des Sets, die nicht in der angegebenen Collection c vorhanden sind.

Collections II

 int size()

Diese Methode liefert die Anzahl der im Set vorhandenen Elemente zurück.  SortedSet subSet( Object from, Object to )

Siehe Methode subMap() von SortedMap. Zusätzlich kann eine ClassCastException geworfen werden, wenn eines der beiden angegebenen Objekte nicht mit den Elementen des Sets vergleichbar ist.  SortedSet tailSet( Object from )

Siehe Methode tailMap() von SortedMap. Zusätzlich kann eine ClassCastException geworfen werden, wenn das angegebene Objekt nicht mit den Elementen des Sets vergleichbar ist.  Object[] toArray()

Siehe gleichnamige Methode von ArrayList  Object[] toArray( Object[] targetType )

Siehe gleichnamige Methode von ArrayList 7.3.3

Map-Implementierungen

Alle Map-Implementierungen bieten einen Set-View der Elemente über die Methode entrySet() an. Details über diese Methode finden Sie in Kapitel 0. java.util.HashMap

Diese Implementierung des Map-Interfaces ist im Prinzip identisch mit der Klasse Hashtable, jedoch ist sie nicht synchronisiert. Im Gegensatz zu einer Hashtable erlaubt HashMap, dass sowohl Keys als auch Values von Elementen null sind. Da die Methoden dieser Klasse von der Funktionalität identisch sind mit denen der Klasse Hashtable, sei auf deren Dokumentation verwiesen. java.util.LinkedHashMap

Diese Klasse ist vergleichbar mit LinkedHashSet, sie funktioniert genauso wie HashMap, sorgt aber für eine konstante Reihenfolge der Elemente. java.util.TreeMap

Diese Klasse ähnelt TreeSet, nur dass anstelle des SortedSet-Interfaces von TreeMap das Interface SortedMap implementiert wird. Aus der Sicht des Programmierers besteht der wesentliche Unterschied zwischen den Interfaces darin, dass beim SortedMap-

Collections II

terface mit Keys jongliert wird (Die Elemente bestehen aus Key/Value-Paaren). eshalb gibt es bei TreeMap keine add-Methoden, vielmehr werden neue Elemente mit put() hinzugefügt. onstruktoren von java.util.TreeMap TreeMap()

Default-Konstruktor. Er erzeugt eine leere Map. Die Sortierung der Elemente erfolgt mit dem Comparable-Interface, deswegen müssen alle Keys der aufzunehmenden Elemente dieses Interface implementieren, außerdem darf beim Vergleich zweier Keys keine ClassCastException geworfen werden. TreeMap( Comparator comp )

Bei diesem Konstruktor kann man einen Comparator angeben, der die Sortierung der Keys durchführt. Auch hier gilt, dass alle Keys vergleichbar sein müssen, es darf also keine ClassCastException ausgelöst werden. TreeMap( Map m )

Wie Default-Konstruktor, jedoch kann man die TreeMap initial mit einer Map füttern. Man erzeugt somit eine Map, welche dieselben Elemente enthält, jedoch sind deren Keys aufsteigend sortiert. TreeMap( SortedMap m )

Wie vorher, jedoch erzeugt man eine Kopie einer bereits sortierten Map. Es wird der Comparator der übergebenen SortedMap auch für die TreeMap verwendet. Methoden von java.util.TreeMap void clear()

Leert die Map. Object clone()

Gibt eine einfache Kopie des Sets zurück. Vorsicht! Die darin befindlichen Objekte werden nicht kopiert, sondern nur deren Referenzen, es gilt also das callby-reference-Prinzip (Änderungen in der kopierten Map wirken sich auf die Original-Map aus und umgekehrt). Comparator comparator()

Diese Methode liefert entweder eine Referenz auf den verwendeten Comparator oder den Wert null zurück, wenn kein Comparator benutzt wird.

Collections II

 boolean containsKey( Object key )

Liefert true zurück, wenn die Map ein Element mit dem angegebenen Key enthält, andernfalls false. Wenn das angegebene Objekt nicht mit den Keys der Map verglichen werden kann, wirft die Methode eine ClassCastException.  boolean containsValue( Object value )

Wie containsKey(), jedoch kann man hier feststellen, ob ein bestimmter Value existiert.  Set entrySet()

Liefert einen Set-View der Map. Jedes Element des Views wird zu einem Objekt vom Typ Map.Entry. Dieses Interface bietet folgende Methoden: Methoden von Map.Entry boolean equals( Object o ) Object getKey() Object getValue() int hashCode() Object setValue( Object value )

Tabelle 7.8: Methoden des Interfaces Map.Entry

Wichtig: Die Map.Entry-Elemente sind nur innerhalb eines Iterators des zurückgelieferten Sets definiert. Auch sind die Elemente keine Kopien, sondern Referenzen auf die Original-Map. Änderungen dürfen deshalb nur über den Iterator des zurückgelieferten Sets oder die setValue()-Methode von Map.Entry durchgeführt werden.  boolean equals( Object o )

Diese Methode wird von AbstractMap geerbt. Sie liefert true zurück, wenn das angegebene Objekt mit der aktuellen Map identisch ist, andernfalls false. Es wird nicht nur die Gleichheit der Objekt-Referenzen verglichen, sondern beide Maps werden hinsichtlich gleicher Größe und identischer Map-Elemente geprüft.  Object firstKey()

Diese Methode liefert den ersten Key der sortierten Map zurück. Wenn die Map leer ist, wirft die Methode eine NoSuchElementException.

Collections II

Object get( Object key )

Diese Methode liefert den Value des angegebenen Keys zurück. Hier ist Vorsicht geboten, wenn man null-Keys benutzt, in diesem Fall sollte man mit Hilfe von containsKey() überprüfen, ob das Element mit dem angegebenen Key existiert. Sonst kann man nicht entscheiden, ob beim Return-Wert null das Element nicht existiert oder der Key vorhanden, aber ebenfalls null ist. int hashCode()

Diese Methode wird von AbstractMap geerbt. Sie iteriert über das von entrySet() zurückgelieferte Set, ruft für jedes Element dessen hashCode()-Funktion auf und addiert die Werte. SortedMap headMap( Object toKey )

Siehe gleichnamige Methode des Interfaces SortedMap. boolean isEmpty()

Diese Methode wird von AbstractMap geerbt und liefert true zurück, wenn die Map leer ist, andernfalls false. Set keySet()

Diese Methode liefert einen Set-View der sortierten Map-Keys (ohne Values) zurück. Mit Hilfe des Set-Iterators kann man auf die Keys anschließend sequentiell zugreifen. Änderungen sollten nur über den Iterator oder die Set-Methoden clear(), remove(), removeAll() und retainAll() durchgeführt werden. Das Hinzufügen neuer Elemente über das zurückgelieferte Set ist nicht unterstützt. Object lastKey()

Diese Methode liefert den letzten Key der sortierten Map zurück. Wenn die Map leer ist, wirft die Methode eine NoSuchElementException. Object put( Object key, Object value )

Nimmt das Objekt value unter dem angegebenen Key in die Map auf. Wenn der Key bereits existiert, dann ersetzt die Methode den betreffenden Value durch den neuen Wert. Diese Methode kann sowohl eine NullPointerException als auch eine ClassCastException werfen. Letztere wird dann ausgelöst, wenn der angegebene Key nicht mit den bereits existierenden verglichen werden kann. Die NullPointerException tritt auf, wenn die Sortierung auf Basis von Comparable erfolgt und der angegebene Key null ist oder wenn der verwendete Comparator keine nullKeys unterstützt. Die Methode liefert den alten Value zurück. Dieser kann auch null sein.

Collections II

 void putAll( Map m )

Diese Methode übernimmt alle Elemente der angegebenen Map in die aktuelle TreeMap. Alle Values der TreeMap, bei denen die Keys identisch mit denen der angegebenen Map sind, werden durch die Map-Values ersetzt. Auch diese Methode kann sowohl eine ClassCastException als auch eine NullPointerException auslösen (siehe oben).  Object remove( Object key )

Entfernt das Element mit dem angegebenen Key aus der Map. Bezüglich ReturnWert und möglicher Exceptions siehe Methode put().  int size()

Diese Methode liefert die Anzahl der Key/Value-Paare der Map zurück.  SortedMap subMap( Object frKey, Object toKey )

Siehe gleichnamige Methode des Interfaces SortedMap.  SortedMap tailMap( Object fromKey )

Siehe gleichnamige Methode des Interfaces SortedMap.  Collection values()

Diese Methode liefert einen Collection-View der Map-Values zurück. Bezüglich der Abhängigkeiten siehe Methode keySet(). java.util.IdentityHashMap

Diese Klasse verwendet beim Vergleich der Elemente die Objekt-Referenzen und nicht die Objekt-Werte. Sie verstößt absichtlich gegen die allgemeinen CollectionRegeln und wird nur sehr selten benötigt. Aus diesem Grund möchte ich hier nicht näher darauf eingehen. java.util.WeakHashMap

Auf diese Klasse möchte ich hier nicht näher eingehen, da sie evtl. unerwartete Ergebnisse liefern kann. Die Existenz der Elemente hängt stark von der Arbeitsweise des Garbage Collectors ab, der Elemente automatisch löschen kann, wenn keine Referenzen mehr auf ihren Key bestehen.

Ein-/Ausgabe

8

Ein-/Ausgabe

Wer bereits Erfahrungen hinsichtlich File I/O mit anderen Programmiersprachenn gemacht hat, wird angesichts der dafür vorgesehenen Java-Klassen zutiefst erschrecken. Für den Einsteiger ist es nahezu unmöglich, auf Anhieb den Inhalt einer Datei zeilenweise auszulesen, ohne Myriaden von API-Seiten zu lesen. Zum einen ist gerade dieses Kapitel in Java eine der Kehrseiten der Programmiersprachen, mit denen sich die Erfinder von Java zu Anfang nicht gerade mit Ruhm bekleckert haben, zum anderen hat man versucht, möglichst generische Klassen für I/O zu implementieren (»Eier legende Wollmilchsäue«). Allerdings muss man fairerweise hinzufügen, dass Java eine der wenigen Programmiersprachen ist, die Unicode-basierte Strings verwendet (und damit ein Zeichen eine Länge von 16 Bit hat). Das Thema I/O wird dadurch wesentlich komplexer. Java bietet aber diverse Möglichkeiten, Daten in jeder Form zu lesen oder zu schreiben. Die einfachsten Mittel haben wir bereits mehrfach in Programmbeispielen gesehen, die Klasse System stellt drei vordefinierte Streams für Standard-Eingabe (System.in), Standard-Ausgabe (System.out) sowie Standard-Fehler-Ausgabe (System.err) zur Verfügung, die man im Programm ohne vorherige Maßnahmen direkt verwenden kann. Daneben gibt es aber eine große Anzahl weiterer Streams für die verschiedensten Bedürfnisse. Wir werden Streams weiter unten genauer betrachten. Hinweis

Alle Klassen für die Ein-/Ausgabe sind im Package java.io abgelegt.

Bevor ich näher auf das Design und die Klassen der Ein-/Ausgabe in Java eingehe, möchte ich Ihnen zeigen, wie man Text-Dateien zeilenweise liest, denn damit fängt man als Neuling einer Programmiersprachen meist an.

8.1

Dateien zeilenweise lesen

Wir wollen eine Datei zeilenweise lesen. Dazu schauen wir als Erstes in das Java-API, wo eine Klasse namens java.io.File beschrieben ist. Doch leider wird man hier nicht fündig, weil diese Klasse keinerlei Methoden zum Lesen oder Schreiben von Dateien anbietet (mehr zu dieser Klasse weiter unten). Als Nächstes versuchen wir es bei java.io.FileInputStream, aber auch hier Fehlanzeige, es gibt dort zwar einige Methoden zum Lesen, aber keine, die zeilenweise einliest.

Ein-/Ausgabe

was weiter unten im API ist die Klasse java.io.FileReader zu finden, probieren wir dort einmal, doch leider finden wir auch dort nichts. Nun kommt man ohne Intuition nicht weiter. Meine Intuition sagt mir, ich solle es mal mit der Klasse java.io.BufferedReader versuchen, et voilà, schon haben wir, was r brauchen, die Klasse besitzt eine Methode readLine(). Zwar bietet diese Klasse inen Konstruktor an, dem man den Pfadnamen der zu lesenden Datei übergibt, er immerhin ist in der Beschreibung der Klasse eine Anleitung zu finden, wie man e zum Lesen einer Datei benutzen kann (den Source-Code finden Sie auf der CDROM in der Datei ReadFileLines.java): 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

// Datei ReadFileLines.java import java.io.*; // Das Programm gibt den Inhalt einer Datei // aus, deren Pfad-Name man in der Kommandozeile // als Argument angibt. public class ReadFileLines { public static void main( String[] args ) { if ( args.length != 1 ) { System.out.println( "usage: program " ); System.exit( 1 ); } BufferedReader in = null; String line = null; // Nahezu alle I/O-Operationen können // Exceptions auslösen, die wir behandeln // müssen. try { // Da der Konstruktor von BufferedReader // kein Argument für den Pfad-Namen // anbietet, müssen wir dies mit dem // Konstruktor von FileReader // erledigen. BufferedReader benötigen // wir, weil nur dort eine Methode // readLine() implementiert ist. in = new BufferedReader( new FileReader( args[ 0 ] ) );

Ein-/Ausgabe

33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 }

while ( ( line = in.readLine() ) != null ) { System.out.println( line ); } } catch ( FileNotFoundException ex ) { System.out.println( "Datei nicht gefunden, Grund " + ex ); System.exit( 1 ); } catch ( IOException ex ) { System.out.println( "IO exception, Grund " + ex ); System.exit( 1 ); } finally { if ( in != null ) { try { in.close(); } catch ( Exception ignored ) {} } } System.exit( 0 ); }

Dieses Beispiel greift dem Geschehen vor, ich möchte jedoch dem Hilfesuchenden eine Möglichkeit in die Hand geben, Text-Dateien auszulesen, ohne dass er vorher zig Seiten Dokumentation lesen muss.

8.2

Die Klasse java.io.File

Wer glaubt, die Klasse java.io.File diene dazu, Inhalte von Dateien zu verarbeiten, irrt gewaltig. Sie wird vielmehr benutzt, um auf Meta-Daten von Dateien zuzugreifen. Eine Datei kann in diesem Zusammenhang nicht nur eine normale Art von Datei (plain file) sein, sondern auch ein Verzeichnis (directory). Hinweis An dieser Stelle sei angemerkt, dass Windows ein anderes Trennzeichen für Pfad-Namen verwendet als Unix:

Ein-/Ausgabe

# Windows-Pfad C:\tmp\t.html # Unix-Pfad /tmp/t.html

e Windows-Variante ist vor allem deshalb unschön, weil der Backslash in Java eine sondere Bedeutung besitzt. Im Java-Code können Sie jedoch immer den Slash ("/") Trennzeichen benutzen, gleichgültig, in welchem Betriebssystem das Programm uft. Die Virtual Machine sorgt automatisch dafür, dass immer das richtige Trennzeien verwendet wird. ternativ kann man im Java-Programm zur Laufzeit das aktuelle Trennzeichen mit em statischen Attribut File.separator auslesen. 2.1

Konstruktoren von java.io.File

e File-Klasse bietet folgende Konstruktoren an: File( String path )

Dieser Konstruktor erzeugt ein Objekt für den angegebenen Pfad-Namen. Das Objekt wird auch dann erfolgreich instanziert, wenn Sie einen Fantasie-Pfad angeben, der überhaupt nicht im Dateisystem vorkommen kann. Sie können jedoch mit Hilfe der Methode exists() überprüfen, ob der Pfad auf eine vorhandene Datei oder ein existierendes Verzeichnis zeigt oder nicht. File( URI uri )

Wie vorher, jedoch wird kein Dateisystem-Pfad angegeben, sondern ein URI (z.B. file:///tmp/t.html). File( String parent, String child )

Bei diesem Konstruktor geben Sie den Pfad in Form zweier Bestandteile an, wobei parent das übergeordnete Verzeichnis für child ist. parent kann auch null sein, in diesem Fall verhält sich der Konstruktor so, als hätte man ihn mit nur einem Argument aufgerufen. Ist parent leer, dann wird ein Betriebssystem-abhängiges Default-Verzeichnis als parent verwendet. Wenn child ein absoluter Pfad-Name ist, dann wird dieser erst in einen relativen Pfad umgewandelt. File( File parent, String child )

Wie vorher, jedoch wird parent als File-Objekt und nicht als String angegeben.

Ein-/Ausgabe

8.2.2

Attribute von java.io.File

Die Klasse bietet mit den Attributen separator und separatorChar die Möglichkeit an, zur Laufzeit eines Programms das Betriebssystem-abhängige Trennzeichen für Pfad-Namen zu lesen ("/" in Unix, "\" in Windows): String separator char separatorChar

Mit den Attributen pathSeparator bzw. pathSeparatorChar kann man dynamisch zur Programmlaufzeit feststellen, welches Trennzeichen für Pfade in Umgebungs-Variablen benutzt wird (":" in Unix, ";" in Windows): String pathSeparator char pathSeparatorChar

8.2.3

Wichtige Methoden von java.io.File

Mit der Methode exists() können Sie feststellen, ob das Objekt im Dateisystem existiert oder nicht: boolean exists()

Um zu überprüfen, ob Sie Lese- bzw. Schreibrechte für das Dateisystem-Objekt haben, benutzen Sie canRead() bzw. canWrite(): boolean canRead() boolean canWrite()

Die Methode length() liefert die Dateigröße in Bytes zurück. Wenn Sie diese Methode bei Verzeichnissen benutzen, dann liefert sie in Windows die Länge 0 zurück, in Unix wird die tatsächliche Verzeichnis-Größe zurückgegeben. Bei symbolischen Links wird nicht die Länge des Link-Namens, sondern die Dateigröße des Original-Objekts zurückgegeben (der Link wird aufgelöst). Mit isDirectory() können Sie prüfen, ob das Dateisystem-Objekt ein Verzeichnis ist oder nicht (isFile() liefert das Gegenteil). Java bietet leider keine genauere Unterscheidung an, z.B. ob es sich um einen symbolischen Link oder eine Geräte-Datei etc.

Ein-/Ausgabe

ndelt. isHidden() liefert in Windows true zurück, wenn das Dateisystem-Objekt als rsteckt markiert wurde, in Unix wird true zurückgegeben, wenn der Datei- bzw. rzeichnis-Name mit einem Punkt beginnt: boolean isFile() boolean isDirectory() boolean isHidden() lastModified() liefert den Zeitstempel der letzten Datei-Änderung in Millisekunden

it dem 1.1.1970 00:00 Uhr GMT zurück. Leider fehlen entsprechende Methoden r den Zeitpunkt der Objekt-Erzeugung bzw. des letzten Lese-Zugriffs. Mit setLastModified() kann man den Zeitstempel explizit neu setzen: long lastModified() void setLastModified( long timestamp )

Sie können ein Verzeichnis mit mkdir() anlegen, mkdirs() arbeitet rekursiv und legt entuell nicht vorhandene übergeordnete Verzeichnisse automatisch an: boolean mkdir() boolean mkdirs()

Mit renameTo() können Sie sowohl Dateien als auch Verzeichnisse umbenennen: boolean renameTo( File new )

e Methode liefert nur dann einen true-Wert zurück, wenn die Operation erfolgch war. Prüfen Sie deshalb immer den return-Wert. Mit delete() können Sie eine Datei löschen. Die Methode kann auch bei Verzeichsen verwendet werden, allerdings nur dann, wenn diese leer sind. Bei Erfolg liefert e Methode einen true-Wert zurück, andernfalls false: boolean delete()

Ein-/Ausgabe

Wenn es sich bei dem File-Objekt um ein Verzeichnis handelt, können Sie die Namen der darin befindlichen Dateien und Verzeichnisse mit list() oder listFiles() lesen: String[] list() String[] list( FilenameFilter filter ) File[] listFiles() File[] listFiles( FileFilter filter ) File[] listFiles( FilenameFilter filter ) list() und listFiles() unterscheiden sich zunächst dadurch, dass list() ein String-

Array der im Verzeichnis enthaltenen Datei- und Verzeichnis-Namen zurückgibt, während listFiles() ein Array aus File-Objekten zurückliefert. Hinweis Das aktuelle Verzeichnis ».« sowie das übergeordnete Verzeichnis »..« sind nicht im Array enthalten. Beide Methoden akzeptieren einen Filter als Argument, mit dem man bestimmte Verzeichnis-Einträge filtern kann, diese sind dann nicht in dem zurückgelieferten Array enthalten. Sowohl das Interface FileFilter als auch FilenameFilter deklarieren die Methode accept(), in welcher der Filter implementiert werden muss. Sie wird für jeden Verzeichnis-Eintrag aufgerufen. Gibt sie einen true-Wert zurück, dann wird der Eintrag ins Array mit aufgenommen. Während die Methode von FileFilter ein File-Objekt als Argument erwartet, werden der Methode accept() von FilenameFilter zwei Argumente übergeben (ein File-Objekt für das Verzeichnis, ein StringObjekt für den Namen des Eintrags). Wenn das File-Objekt, über welches eine der list-Methoden aufgerufen wird, kein Verzeichnis ist, dann liefern die Methoden null zurück. Die Elemente des zurückgelieferten Arrays sind nicht sortiert. Ich glaube, ein Beispiel wäre hier angebracht: import java.io.*; # alle Dateien und Unterverzeichnisse des aktuellen # Directories lesen. File f = new File( "." );

Ein-/Ausgabe

String[] entries = f.list(); # wenn das aktuelle Verzeichnis z.B. die Dateien # "a.html", "b.txt" sowie das Unterverzeichnis # "html" enthält, dann wird ein Array mit # drei Elementen zurückgeliefert # ( "a.html", "b.txt" "html" ) # dito, jedoch mit einem Filter, der nur diejenigen # Einträge in das Array aufnimmt, deren Name mit # "a" beginnen. entries = f.list( new FilenameFilter() { public boolean accept( File dir, String name ) { if ( name.toLowerCase().startsWith( "a" ) ) { return true; } return false; } );

Oft benötigt man Temporär-Dateien, die nur für kurze Zeit verwendet und anschlieend gelöscht werden. Hierzu bietet die File-Klasse die Methode createTempFile() zwei Varianten an: static File createTempFile( String prefix, String suffix

static File createTempFile( String prefix, String suffix, File dir

i der ersten Variante wird ein Default-Verzeichnis für die Temporär-Datei benutzt, t der zweiten Variante kann man explizit ein Verzeichnis angeben, in dem die Temrär-Datei angelegt wird. prefix muss mindestens drei Zeichen enthalten. suffix nn null sein, in diesem Fall wird der String ».tmp« verwendet. er einige weitere hilfreiche Methoden:

Ein-/Ausgabe

String getName() String getPath() String getAbsolutePath() File getAbsoluteFile() String getParent() File getParentFile()

Die Methode getName() liefert den letzten Pfad-Anteil zurück und funktioniert demnach ähnlich wie das Programm basename in Unix (Vorsicht, wenn man diese Methode für die Elemente der logischen Laufwerke benutzt, liefert getName() einen leeren String zurück, hier sollte man getPath() verwenden). getParent() und getParentFile() liefern null zurück, es sei denn, man hat einen

Konstruktor verwendet, bei dem das Eltern-Verzeichnis als erstes Argument anzugeben ist. Speziell für Windows-Programmierer interessant dürfte die Methode listRoots() sein, sie liefert ein File-Array zurück, das alle logischen Laufwerke enthält: static File[] listRoots()

Auf meinem PC liefert die Methode zum Beispiel folgende Laufwerke (die Ausgabe erfolgt mit getPath()): A:\ C:\ D:\

8.3

Streams

In den Anfangszeiten von C war alles noch einfach: Dateien bestanden aus Bytes, die entweder Binärdaten oder Zeichen im ASCII-Code enthielten. Später wurde das Netzwerk erfunden und plötzlich gab es eine neue Art von Ein-/Ausgabe über so genannte Sockets. Damit nicht genug, es kamen die äußerst hilfreichen Pipes hinzu, welche wiederum einen anderen Mechanismus für den Datenverkehr benötigen. Man musste sehr genau wissen, woher man Daten einliest oder wohin man sie schreibt. Damit es für den Programmierer nicht allzu leicht wird, erfanden die Prozessor-Hersteller natürlich unterschiedliche Arten der Abspeicherungsart von Daten. Während

Ein-/Ausgabe

Motorola Zahlen, die größer waren als 8 Bit, immer im Big Endian-Format (Big End mes first, 0x1234 wird als 0x1234 gespeichert) speicherte, hat Intel genau den umgeehrten Weg gewählt und legt alle Daten im Little Endian-Format (Little End comes st, 0x1234 wird als 0x3412 gespeichert) ab. Da diese Formate nicht kompatibel sind, usste man genau aufpassen, wenn man Zahlen aus Dateien oder aus dem Netzwerk en wollte. Java sind die Dinge Gott sei Dank etwas besser standardisiert: Man hat sich generell r das Big Endian-Format entschieden (aber wer Zahlen aus Dateien lesen will, die nicht durch Java-Code generiert worden sind, hat natürlich nach wie vor dieselben obleme). Außerdem wird jede I/O in Java über so genannte Streams abgewickelt, e dem Programmierer eine einheitliche Schnittstelle anbieten, er muss sich also nicht um lästige Details kümmern, egal, ob die Daten File-basiert, Socket-basiert oder pe-basiert sind. Ein Stream ist nichts anderes als irgendein Strom von Daten, den man lesen oder schreiben kann. n großer Vorteil von Streams ist die Kompatibilität der unterschiedlichen Streamsten. So kann man zum Beispiel einen Data-Stream, der für das Übertragen von einchen Datentypen zuständig ist, einem Pipe-Stream zuordnen, den man für die ommunikation zweier Threads benötigt. Für den Programmierer ändert sich durch nichts, er muss lediglich Daten in den Stream schreiben bzw. daraus lesen, al, um welche Art von Stream es sich dabei handelt. Mehrere Streams können auch zu einem einzigen Stream zusammengefasst werden, ne dass sich dadurch etwas für den Programmierer ändert. nige Streams haben wir bereits kennen gelernt, dies sind die vordefinierten Klassen r Standard-I/O (System.in, System.out, System.err). Java gibt es zurzeit 8-Bit-Streams (Byte-Streams) und 16-Bit-Streams (Charactereams). 3.1

Byte-Streams

r die byteweise Verarbeitung von Eingabe- und Ausgabe-Daten stellt Java die Basisassen java.io.InputStream und java.io.OutputStream zur Verfügung, welche die urzel einer ganzen Vererbungs-Kette von Klassen für spezialisierte Ein-/Ausgabeperationen darstellen.

Ein-/Ausgabe

Klassen-Diagramm der Ausgabe-Streams   

      

                                                     !               

Abbildung 8.1: Klassen-Diagramm der Ausgabe-Streams

Klassen-Diagramm der Eingabe-Streams

Alle Klassen der Byte-Streams werden für die byteweise Verarbeitung von BinärDaten verwendet, sie eignen sich also nicht für Text-Zeichen, da Strings und einzelne Zeichen (Characters) in Java grundsätzlich mit je 2 Bytes kodiert sind. Für die TextVerarbeitung sollte man auf Reader- bzw. Writer-Klassen zurückgreifen, die wir im nächsten Abschnitt kennen lernen.

Ein-/Ausgabe

  !  "!    "!    "!    "!    "!      "!      "!    "!        "!    "! "!   $!   %!  "!   "!  

   "!    "!    # ! "!  

Abbildung 8.2: Klassen-Diagramm der Eingabe-Streams

Methoden von java.io.OutputStream

e abstrakte Basis-Klasse OutputStream deklariert bzw. implementiert zusätzlich zum efault-Konstruktor folgende Methoden: void close()

Diese Methode schließt den Ausgabe-Stream und gibt damit alle belegten Ressourcen frei. Die Methode ist in der Klasse OutputStream funktionslos.

Ein-/Ausgabe

 void flush()

Mit dieser Methode werden alle zwischengespeicherten Daten zum AusgabeMedium geschrieben. Die Methode ist in der Klasse OutputStream funktionslos.  void write( byte[] b )

Diese Methode schreibt den Inhalt des Arrays b zum Ausgabe-Kanal.  void write( byte[] b, int off, int len )

Diese Methode arbeitet wie die vorhergehende, jedoch kann man sowohl den Offset innerhalb des Arrays angeben, ab dem die Ausgabe erfolgen soll, als auch die Anzahl der auszugebenden Bytes.  abstract void write( int b )

Diese Methode schreibt den Inhalt der 8 niederwertigen Bits von b zum AusgabeKanal. Alle höherwertigen Bits von b werden ignoriert. Methoden von java.io.InputStream

Die abstrakte Basis-Klasse InputStream deklariert bzw. implementiert zusätzlich zum Default-Konstruktor folgende Methoden:  int available()

Diese Methode liefert die Anzahl von Bytes zurück, die momentan verfügbar sind, ohne den lesenden Prozess beim Aufruf von read() oder skip() zu blockieren. In der Klasse InputStream liefert diese Methode immer den Wert 0 zurück.  void close()

Diese Methode schließt den Eingabe-Stream und gibt die dadurch belegten System-Ressourcen frei. In der Klasse InputStream ist diese Methode funktionslos.  void mark( int limit )

Mit dieser Methode (die nicht von allen Streams unterstützt wird, siehe hierzu auch die Beschreibung von markSupported()) kann man die aktuelle Position des Lese-Zeigers im Eingabe-Stream markieren. Ruft man später die Methode reset() auf, dann wird der Zeiger auf die markierte Position gesetzt. Damit ist es möglich, dieselben Bytes mehrmals zu lesen. Mit dem Argument limit kann man die Anzahl der Bytes begrenzen, die für eine erneute Lese-Operation zwischengespeichert werden. Wird nach dem Markieren limit durch Lese- oder Skip-Aufrufe überschritten, dann ist die gesetzte Marke ungültig. In der Klasse InputStream ist diese Methode funktionslos.

Ein-/Ausgabe

boolean markSupported()

Diese Methode liefert nur dann true zurück, wenn das Setzen von Marken durch mark() sowie das Zurücksetzen des Lese-Zeigers auf Marken mit reset() vom Eingabe-Stream unterstützt wird. In der Klasse InputStream gibt diese Methode immer false zurück. abstract int read()

Diese Methode muss von konkreten Kind-Klassen implementiert werden und liefert das nächste Byte des Eingabe-Streams als int-Wert im Bereich 0 bis 255 zurück. int read( byte[] buf )

Diese Methode liest so viele Bytes wie möglich vom Eingabe-Stream und speichert die einzelnen Bytes in buf ab. Falls keine Daten verfügbar sind, blockiert die Methode den Aufrufer so lange, bis Daten für die Lese-Operation vorhanden sind. Es wird die aktuelle Anzahl von gelesenen Bytes zurückgegeben. Falls keine Daten verfügbar sind, ohne dass die Lese-Operation blockiert wird, liefert die Methode den Wert -1 zurück. int read( byte[] buf, int off, int len )

Diese Methode funktioniert ähnlich wie die vorhergehende, jedoch kann man den Offset in buf , ab dem die gelesenen Bytes gespeichert werden, und die Anzahl der zu lesenden Bytes angeben. Kann das erste Byte aus irgendeinem Grund (außer der Zeiger befindet sich am Ende des Eingabe-Streams) nicht gelesen werden, dann löst die Methode eine IOException aus. Bei allen darauf folgenden Bytes jedoch wird keine Exception ausgelöst, vielmehr verhält sich die Methode dann so, als sei das Ende des Eingabe-Streams erreicht worden. void reset()

Mit dieser Methode kann man den Zeiger für den Eingabe-Strom auf diejenige Position zurücksetzen, die man vorher mit mark() markiert hatte. reset() ist stark von der Implementierung in den konkreten Kind-Klassen abhängig, deshalb wirft sich in der abstrakten Klasse immer eine IOException. Diese wird ebenfalls ausgelöst, wenn vorher kein Aufruf von mark() erfolgt ist oder die maximale Anzahl von zwischengespeicherten Bytes der Markierung durch eine Lese- oder Skip-Operation überschritten wurde. Die Methode ist in der Klasse InputStream funktionslos und wirft immer eine IOException. Sie muss also in Kind-Klassen überschrieben werden. long skip( long n )

Diese Methode führt dazu, dass man eine bestimmte Anzahl von Bytes im Eingabe-Stream überspringen kann, ohne sie lesen zu müssen. Die Methode ist zwar

Ein-/Ausgabe

in InputStream implementiert, sollte in konkreten Kind-Klassen jedoch durch eine performante Implementierung überschrieben werden. Sie liefert die Anzahl von Bytes zurück, die tatsächlich übersprungen wurden. Konstruktoren von java.io.FileOutputStream

Die konkrete Klasse FileOutputStream ist spezialisiert für Ausgaben in Dateien und hat folgende Konstruktoren:  FileOutputStream( String path )

Dieser Konstruktor erzeugt eine Objekt-Instanz, indem er versucht, die angegebene Datei zum Schreiben zu öffnen (alle Daten einer eventuell bereits existierenden Datei gehen dabei verloren). Kann die Datei aus irgendeinem Grund nicht zum Schreiben geöffnet werden (z.B. weil sich hinter dem Pfad-Namen ein bereits existierendes Verzeichnis anstelle einer normalen Datei versteckt), dann wirft die Methode eine FileNotFoundException. Ist ein Security-Manager installiert, der die Operation nicht zulässt, dann wirft diese Methode eine SecurityException.  FileOutputStream( String path, boolean append )

Wie vorher, jedoch kann man mit einem true-Wert von append angeben, dass der Inhalt einer bereits existierenden Datei nicht gelöscht werden soll, sondern die zu schreibenden Daten am Ende der Datei angehängt werden sollen.  FileOutputStream( File file )

Anstelle des Pfad-Namens wird ein File-Objekt übergeben, ansonsten verhält sich diese Methode wie die mit einem String-Argument.  FileOutputStream( File file, boolean append )

Wie vorher, jedoch kann man mit einem true-Wert von append angeben, dass der Inhalt einer bereits existierenden Datei nicht gelöscht werden soll, sondern die zu schreibenden Daten am Ende der Datei angehängt werden sollen.  FileOutputStream( FileDescriptor fdObject )

Diese Methode hat als Argument anstelle eines Pfad-Namens oder eines FileObjekts ein FileDescriptor-Objekt einer bereits geöffneten Datei. Methoden von java.io.FileOutputStream  void close()

Konkrete Implementierung der gleichnamigen, aber funktionslosen Methode von OutputStream.

Ein-/Ausgabe

FileChannel getChannel()

Diese Methode liefert ein FileChannel-Objekt für die Datei zurück. Die Position des Schreib-Zeigers für den FileChannel ist dieselbe wie für das FileOutputStreamObjekt. Die Klasse java.nio.channels.FileChannel wurde neu mit dem JDK 1.4 eingeführt und wird unter anderem für Dateisperren benutzt. FileDescriptor getFD()

Diese Methode liefert ein eindeutiges FileDescriptor-Objekt für die geöffnete Datei zurück. Die Klasse java.io.FileDescriptor bietet die zwei Methoden sync() und valid() an, mit denen man alle in Puffern zwischengespeicherten Daten zum Ausgabe-Medium schreiben kann (die Methode sync() wird erst dann beendet, wenn wirklich alle Daten geschrieben wurden) sowie prüfen kann, ob das Ausgabe-Medium noch gültig ist. void write( byte[] b )

Die Methode schreibt den Inhalt von b zum Ausgabe-Stream. void write( byte[] b, int off, int len )

Wie vorher, jedoch kann man mit off angeben, ab welchem Index des Arrays die Daten geschrieben werden. Mit len gibt man die Anzahl der zu schreibenden Bytes an. void write( int b )

Diese Methode ist die konkrete Implementierung der gleichnamigen Methode der Klasse OutputStream. Es werden nur die 8 niederwertigen Bits verwendet, alle höherwertigen Bits werden ignoriert. Damit lassen sich also nur Zahlen bis 255 verarbeiten. s Beispiel möchte ich Ihnen ein (nicht funktionierendes) Programm zeigen, das ber die Klasse FileOutputStream versucht, int-Werte in eine Datei zu schreiben (Sie den das Programm auf der CD-ROM in der Datei FileOutputStreamDemo01.java): 01 02 03 04 05 06 07

// Datei FileOutputStreamDemo01.java import java.io.*; // Das Programm schreibt die int-Werte 100-500 // in die Datei "bla.txt". public class FileOutputStreamDemo01 {

Ein-/Ausgabe

08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 }

public static void main( String[] args ) { // Datei-Name für die zu schreibende Datei. String path = "bla.txt"; // Ausgabe-Stream FileOutputStream os = null; // Da die Operationen Exceptions auslösen // können, müssen sie in einen try/catch// Block. try { os = new FileOutputStream( new File( path ) ); for ( int i = 100; i < 500; i++ ) { os.write( i ); } } catch ( IOException ex ) { System.err.println( "Exception " + ex ); System.exit( 1 ); } finally { if ( os != null ) { try { os.close(); } catch ( IOException ign ) {} } } }

Wenn wir das Programm starten, schreibt es zwar 400 Bytes in die Datei, jedoch tritt ab der Zahl 256 ein Überlauf ein und es wird stattdessen die Binär-Zahl 0 in die Datei geschrieben. Dies liegt ganz einfach daran, dass man in einem Byte nur Zahlen von 0 bis 255 speichern kann. java.io.FileInputStream

Die Klasse FileInputStream enthält im Wesentlichen die passenden Gegenstücke der write-Methoden aus FileOutputStream, deshalb sei an dieser Stelle auf das Java-API verweisen.

Ein-/Ausgabe

va.io.DataOutputStream

enn Sie nicht nur Bytes, sondern beliebige einfache Java-Datentypen in einem te-Stream verarbeiten wollen, dann können Sie die Klasse DataOutputStream benutn, die von FilterOutputStream abgeleitet ist. Sie bietet neben dem Konstruktor DataOutputStream( OutputStream os )

lgende zusätzlichen Methoden an: int size()

Diese Methode liefert die Anzahl von Bytes zurück, die bisher zum AusgabeStream geschrieben wurden. void writeBoolean( boolean b )

Diese Methode schreibt den Wert von b kodiert in 1 Byte zum Ausgabe-Stream. void writeByte( int b )

Diese Methode schreibt die 8 niederwertigen Bits von b zum Ausgabe-Stream. void writeBytes( String s )

Diese Methode schreibt den Inhalt von s als Byte-Folge zum Ausgabe-Stream. Vorsicht: Dabei wird das höherwertige Byte der Unicode-Zeichen verworfen! Wenn Sie auch das höherwertige Byte verarbeiten wollen, sollten Sie stattdessen die Methode writeChars() benutzen. void writeChar( char c )

Diese Methode schreibt den Inhalt von c als 2-Byte-Folge zum Ausgabe-Stream (höherwertiges Byte zuerst). void writeChars( String s )

Diese Methode schreibt den Inhalt von s als Abfolge von jeweils 2 Bytes pro Zeichen zum Ausgabe-Stream (höherwertiges Byte zuerst). void writeDouble( double d )

Diese Methode konvertiert den Inhalt von d zunächst in einen long-Wert (mit Hilfe der Methode Double.doubleToLongBits()). Anschließend werden diese 8 Bytes zum Ausgabe-Stream geschrieben (höchstwertiges Byte zuerst). void writeFloat( float f )

Diese Methode konvertiert den Inhalt von d zunächst in einen int-Wert (mit Hilfe der Methode Float.floatToIntBits()). Anschließend werden diese 4 Bytes zum Ausgabe-Stream geschrieben (höchstwertiges Byte zuerst).

Ein-/Ausgabe

 void writeInt( int i )

Diese Methode schreibt den Inhalt von i als 4-Byte-Folge zum Ausgabe-Stream (höchstwertiges Byte zuerst).  void writeLong( long l )

Diese Methode schreibt den Inhalt von l als 8-Byte-Folge zum Ausgabe-Stream (höchstwertiges Byte zuerst).  void writeShort( short sh )

Diese Methode schreibt den Inhalt von sh als 2-Byte-Folge zum Ausgabe-Stream (höchstwertiges Byte zuerst).  void writeUTF( String s )

Diese Methode schreibt zunächst die Länge des Strings als short-Wert zum Ausgabe-Stream (damit ist klar, dass der String maximal 65535 Zeichen enthalten kann). Anschließend wird jedes Zeichen des Strings als UTF-8-Kode geschrieben. Wie viele Bytes tatsächlich geschrieben werden, hängt davon ab, welche Zeichen der String enthält. Maximal kann dies die dreifache Menge an Bytes bezogen auf die Anzahl von String-Zeichen sein. java.io.DataInputStream

Die Klasse DataInputStream ist das Gegenstück zu DataOutputStream und bietet demnach anstelle der write-Methoden passende read-Methoden an. Aus diesem Grund möchte ich an dieser Stelle auf die API-Dokumentation verweisen. java.io.PipedInputStream, java.io.PipedOutputStream

Diese Klassen können für die Kommunikation zweier Threads über so genannte Pipes verwendet werden. Eine Pipe kann man sich als Röhre vorstellen, in welche ein Thread hineinschreibt, der andere Thread liest die Daten am anderen Ende der Röhre aus. Bei Pipes müssen die Daten also nirgendwo zwischengespeichert werden. Sie fügen der Eltern-Klasse nur wenige Zusatz-Funktionalitäten hinzu, deshalb sei an dieser Stelle auf das Beispiel im Kapitel Threads verwiesen. java.io.SequenceInputStream

Diese spezielle InputStream-Klasse kann zum Einlesen von Daten aus mehreren Streams verwendet werden. Sie bietet hierzu folgende Konstruktoren an: SequenceInputStream( Enumeration en ) SequenceInputStream( InputStream is1, InputStream is2 )

Ein-/Ausgabe

ei der ersten Variante gibt man eine Enumeration aus mehreren Streams an, deren ufzeit-Klasse vom Typ InputStream sein muss (natürlich sind auch daraus abgeleitete assen erlaubt). Die zweite Variante verwendet man dann, wenn nicht mehr als zwei eams zusammengeschaltet werden sollen. n read-Aufruf liest so lange vom ersten Eingabe-Strom, bis dieser den End-Of-FileZustand (EOF) erreicht hat, anschließend werden die Daten vom nächsten Eingabeom gelesen, bis am Ende auch der letzte Eingabe-Strom End-Of-File-Zustand hat. va.io.ObjectInputStream, java.io.ObjectOutputStream

Mit diesen Klassen können alle Objekte, welche das Interface java.io.Serializable er java.io.Externalizable-Interface implementieren, in einen Datenstrom aus tes verwandelt und so mit ObjectOutputStream zum Beispiel in einer Datei abgeeichert werden. Umgekehrt kann man alle so abgespeicherten Objekte mit ObjectInputStream wieder in den Hauptspeicher laden und so zum Leben erwecken. Im pitel Java-Beans auf der CD finden Sie ein Beispiel für die Benutzung der Klassen. va.util.zip.CheckedInputStream, java.util.zip.CheckedOutputStream

enn man Daten über Streams verarbeiten muss, die nur eine unsichere Übertragung eten und häufiger ausfallen, kann man hierfür die Klassen CheckedOutputStream für s Schreiben sowie CheckedInputStream für das Lesen der Daten verwenden, die ide im Package java.util.zip enthalten sind. Sie verwenden ein Objekt vom Typ java.util.zip.Checksum, das mit jeder Schreib- bzw. Lese-Operation verändert wird d nach der Datenübertragung eine Prüfsumme über alle Bytes enthält. War die Datenübertragung fehlerfrei, dann müssen die Prüfsummen des Eingabe- und des Ausgabe-Streams gleich sein. ide Klassen besitzen einen Konstruktor, dem als zweites Argument eine Instanz des terfaces Checksum übergeben wird: CheckedOutputStream( OutputStream os, Checksum chk ) CheckedInputStream( InputStream is, Checksum chk )

Das Interface Checksum wiederum besitzt im JDK 1.4 zwei konkrete Default-Impleentierungen, dies sind die Klassen java.util.zip.Adler32 und java.util.zip. CRC32. Sie werden wie folgt benutzt: // Default-Konstruktoren Adler32()

Ein-/Ausgabe

CRC32() // Vom normalen Programmierer benutzte Methoden long getValue()

In den meisten Publikationen finden Sie ein einfaches Beispiel für die Verwendung der beiden Klassen, das ich jedoch nicht für ausreichend halte, deshalb möchte ich an dieser Stelle etwas dazu beisteuern, Ihnen eine praxisgerechte Anwendung zu zeigen. Das folgende Programm (Datei CheckedStreamDemo01.java) erzeugt zwei Threads und verbindet sie mit einem PipedOutputStream bzw. PipedInputStream, denen sozusagen Checked-Streams übergestülpt werden, damit die Daten per Prüfsumme auf ihre Konsistenz hin getestet werden können: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

// Datei CheckedStreamDemo01.java import java.io.*; import java.util.*; import java.util.zip.*; public class CheckedStreamDemo01 { public static void main( String[] args ) { CheckedOutputStream cos = null; CheckedInputStream cis = null; // OutputStream für die Pipe anlegen. PipedOutputStream pos = new PipedOutputStream(); MyWriter wr = null; MyReader rd = null; try { // OutputStream mit Prüfsumme anlegen. cos = new CheckedOutputStream( pos, new Adler32() ); // OutputStream mit Prüfsumme und // dem vorher erzeugten // PipedOutputStream anlegen. cis = new CheckedInputStream(

Ein-/Ausgabe

29 new PipedInputStream( pos ), 30 new Adler32() 31 ); 32 33 // Threads für die Ein-/Ausgabe 34 // erzeugen und starten. 35 wr = new MyWriter( cos ); 36 rd = new MyReader( cis ); 37 wr.start(); 38 rd.start(); 39 } catch ( Exception ex ) { 40 System.err.println( 41 "Exception " + ex 42 ); 43 System.exit( 1 ); 44 } 45 46 // In einer Warte-Schleife auf die 47 // Beendigung aller Threads warten. 48 while ( true ) { 49 if ( 50 ( ! wr.isAlive() ) && 51 ( ! rd.isAlive() ) 52 ) 53 break; 54 try { 55 Thread.sleep( 1000 ); 56 } catch ( InterruptedException ign ) {} 57 } 58 59 // Ein-/Ausgabe-Streams schließen. 60 try { 61 cos.close(); 62 cis.close(); 63 } catch ( Exception ign ) {} 64 65 System.exit( 0 ); 66 } 67 } 68 69 // Klasse für Ausgabe-Thread. 70 class MyWriter extends Thread { 71 private CheckedOutputStream os = null; 72

Ein-/Ausgabe

73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 }

public MyWriter( CheckedOutputStream os ) { this.os = os; } public void start() { try { Random rnd = new Random(); // Wir erzeugen minimal 100 und max. // 200 zufällige Bytes. int cnt = rnd.nextInt( 101 ) + 100; // Die soeben zufällig ermittelte Anzahl // von Ausgabe-Bytes schreiben wir als // Erstes zum Empfänger-Thread. os.write( cnt ); // Nun schreiben wir zufällige Bytes. for ( int i = 0; i < cnt; i++ ) { int n = rnd.nextInt( 256 ); os.write( n ); } // Die jetzt aktuelle Prüfsumme wird // in Bytes umgewandelt (höchstwertiges // Byte zuerst) und ebenfalls zum // Empfänger gesendet. long cs = os.getChecksum().getValue(); byte[] b = new byte[ 4 ]; for ( int i = 0; i < b.length; i++ ) { b[ 3 - i ] = ( byte ) ( cs & 0xff ); cs >>= 8; } os.write( b ); } catch ( IOException ex ) { System.err.println( "Writer: Exception " + ex ); } }

Ein-/Ausgabe

117 118 // Klasse für Eingabe-Thread. 119 class MyReader extends Thread { 120 private CheckedInputStream is = null; 121 122 public MyReader( 123 CheckedInputStream is 124 ) { 125 this.is = is; 126 } 127 128 public void start() { 129 try { 130 // Im ersten Byte, das wir lesen, 131 // steht die Anzahl der folgenden 132 // Bytes. 133 int len = is.read(); 134 135 // Wir lesen alle folgenden Bytes 136 // mit Ausnahme der Prüfsumme, 137 // die anschließend vom Sender kommt. 138 for ( int i = 0; i < len; i++ ) { 139 int b = is.read(); 140 } 141 142 // aktuellen Wert der Prüfsumme 143 // zwischenspeichern 144 long cs = is.getChecksum().getValue(); 145 146 // Nun lesen wir die Sender-Prüfsumme 147 // in ein Byte-Array ein und wandeln 148 // diesen Wert in einen long-Wert um. 149 byte[] b = new byte[ 4 ]; 150 is.read( b ); 151 long rdCs = 0; 152 for ( int i = 0; i < b.length; i++ ) { 153 rdCs |= b[ i ] & 0xff; 154 if ( i < ( b.length - 1 ) ) 155 rdCs = 0 ) { System.err.println(

Internationalisierung

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 }

"Locale " + locs[ i ].toString() + " geht nicht" ); continue; } char[] c = ns.toCharArray(); if ( c[ 0 ] > 0x7f ) { System.err.println( "Locale " + locs[ i ].toString() + " geht auch nicht" ); continue; } pw.println( locs[ i ].toString() + ": " + ns + " " + cs + " --- " + ncs ); } if ( pw != null ) { try { pw.close(); } catch ( Exception ign ) {} } }

Das Programm holt sich in Zeile 09 alle Locales über die statische Methode NumberFormat.getAvailableLocales(). Die Ausgabe der Daten erfolgt in die Datei a.txt. In einer Schleife über alle gefundenen Locales besorgt es sich für jede Locale je eine Instanz zur sprachspezifischen Formatierung der Zahl und der zugehörigen Währung. Zusätzlich wird die Anzahl der Nachkomma-Stellen mit setMaximumFractionDigits() auf zwei begrenzt (standardmäßig ist dieser Parameter auf drei Nachkomma-Stellen eingestellt). In Zeile 37 wird die Zahl nun sprachspezifisch in einen String umgewandelt.

Internationalisierung

den Zeilen 39 bis 41 holt sich das Programm das Symbol für die Währung und ndelt mit format() die Zahl inklusive des Währungszeichens in einen String um. In n Zeilen 43 bis 62 werden diejenigen Locales gefiltert, die nicht ordentlich dargellt werden können (Sie können die Zeilen versuchshalber auskommentieren). Am de wird die gesamte Information für jede Locale ausgegeben, hier ein Extrakt:

Abbildung 9.2: Sprachspezifische Ausgabe von Zahlen und Währungen

Mit den konkreten Java-Klassen java.text.DecimalFormat sowie java.text.DecimalFormatSymbols haben Sie weitere Möglichkeiten, das Aussehen der Zahlen individuell anzupassen. Da dies jedoch dem Sinn von Locales widerspricht, möchte ich nicht näher darauf eingehen. Hinweis

9.4.2

Datums- und Tageszeit-Formatierung

Um Datums- und Tageszeit-Angaben sprachspezifisch zu formatieren, können Sie die asse java.text.DateFormat verwenden, die ähnlich wie NumberFormat aufgebaut ist. Auch sie ist abstrakt und man kann sowohl für Datum als auch für Tageszeit eine konete Instanz anfordern: // Formatierung nur für Datum mit Default-Locale und // Default-Formatierung (DateFormat.DEFAULT) static DateFormat getDateInstance() // Wie vorher, jedoch kann man eine der Konstanten

Internationalisierung

// FULL, LONG, MEDIUM oder SHORT aus DateFormat angeben. static DateFormat getDateInstance( int style ) // Wie vorher, jedoch mit expliziter Angabe der Locale static DateFormat getDateInstance( int style, Locale loc ) // Dasselbe in Grün, jedoch erhält man eine Formatierung // für die Tageszeit static DateFormat getTimeInstance() static DateFormat getTimeInstance( int style ) static DateFormat getTimeInstance( int sty, Locale loc ) // Mit den folgenden Methoden erhält man Formatierungen // sowohl für Datum als auch für Tageszeit. static DateFormat getDateTimeInstance() static DateFormat getDateTimeInstance( int dateStyle, int timeStyle ) static DateFormat getDateTimeInstance( int dateStyle, int timeStyle, Locale loc )

Der Parameter style (bzw. dateStyle und timeStyle) muss eine der in DateFormat definierten Konstanten DEFAULT, FULL, LONG, MEDIUM oder SHORT sein und bestimmt die Art der Ausgabe. Mit der Methode format() wandelt man ein Datum bzw. eine Uhrzeit in den sprachspezifischen String um: ... Date now = new Date(); Locale loc = Locale.getDefault(); DateFormat df = DateFormat.getDateInstance( DateFormat.LONG, loc ); DateFormat tf = DateFormat.getTimeInstance( DateFormat.SHORT, loc ); String dateString = df.format( now ); String timeString = tf.format( now );

Internationalisierung

Auch hierfür habe ich ein Beispielprogramm, das diverse Möglichkeiten für alle definierten Locales ausgibt, Sie finden es auf der CD-ROM in der Datei DateTime ormatDemo01.java. Hier ein Auszug des Programms, wenn man die Ausgabe in eine Datei umlenkt und mit einem Editor öffnet:

Abbildung 9.3: Sprachspezifische Ausgabe von Datum und Tageszeit

va.text.SimpleDateFormat

äufig möchte man das Datum bzw. die Tageszeit selbst formatieren, dabei aber eine rachspezifische Ausgabe zum Beispiel für Monat oder Wochentag benutzen. In dien Fällen ist die aus DateFormat abgeleitete konkrete Klasse java.text.SimpleDateFormat eine Hilfe. Meist verwendet man hierzu folgenden Konstruktor: SimpleDateFormat( String format, Locale loc )

Der Parameter format gibt an, welche Bestandteile des Datums bzw. der Tageszeit wie sgegeben werden sollen (die Formatierung kann mit der Methode format() aus der tern-Klasse DateFormat erfolgen). Er kann sowohl normalen literalen Text als auch

Internationalisierung

Zeichen mit besonderer Bedeutung enthalten. Grundsätzlich gilt die Regel, dass alle Buchstaben sowohl in Groß- als auch in Kleinschreibung reserviert sind und das Format eines Datum-Bestandteils vorgeben. Will man literalen Text mit in die Ausgabe aufnehmen, dann muss dieser in einfache Quotes gesetzt werden ("EM" steht für Wochentag und Monat, während "'EM'" literal ohne besondere Bedeutung ist, da es in einfache Quotes gesetzt ist). Die folgende Tabelle gibt Ihnen eine Übersicht, wie format zu verwenden ist. Zeichen

Bedeutung

Beispiele

G

Ära (vor/nach Christus)

AD, n. Chr.

Y

Jahr

03 (y, yy, yyy), 2003 (yyyy), 02003 (yyyyy)

M

Monat

1 (M), 01 (MM), Jan (MMM), Januar (MMMM)

W

Jahres-Woche

2 (w), 02 (ww)

W

Monats-Woche

3 (W), 03 (WW)

D

Jahres-Tag

1 (D), 01 (DD), 001 (DDD)

D

Monats-Tag

1 (d), 01 (dd)

F

numerischer Wochen-Tag im Monat

1 (z.B. erster Mittwoch im Januar 2003), 2 (z.B. zweiter Mittwoch im Januar 2003)

E

Wochen-Tag als String

Mo, Mon (E, EE, EEE), Montag, Monday (EEEE)

A

Kennzeichen für Vor-/Nachmittag

AM,PM

H

Stunde (0-23)

8 (H), 08 (HH)

K

wie H, jedoch von 1 – 24

K

Stunde (0-11)

H

wie K, jedoch von 1 – 10

M

Minuten (0-59)

9 (m), 09 (mm)

S

Sekunden (0-59)

3 (s), 03 (ss)

S

Millisekunden (0-999)

117 (S), 033 (SSS)

Z

Zeitzone

CET

Z

Zeitzone

+0100

Tabelle 9.2: Formatierungs-Symbole von SimpleDateFormat

7 (K), 07 (KK)

Internationalisierung

er ein Beispiel: String format = "yyyy M MMM MMMM dd E EEEE"; Date dt = new Date(); SimpleDateFormat df = new SimpleDateFormat( format, Locale.getDefault() ); System.out.println( df.format( dt ) );

i einer deutschen Locale wird der String "2002 12 Dez Dezember 23 Mo Montag" ausgeben, während dasselbe Datum in Frankreich zum Beispiel als "2002 12 déc. cembre 23 lun. lundi" erscheint. e Klasse DateFormat bietet über die gezeigten Möglichkeiten hinausgehende Mutar-Methoden (Setters) an, mit denen sich das Format beliebig ändern lässt, im Rahen dieses Buches möchte ich mich jedoch auf die Grund-Funktionalitäten schränken. 9.4.3

Zusammengesetzte Texte

Mit Hilfe der Klasse java.text.MessageFormat und Resource Bundles lassen sich auch rachspezifische Texte in Dateien oder Klassen auslagern, die zusätzlich zu statischem xt auch dynamischen Inhalt besitzen, der zur Laufzeit des Programms durch aktue Werte ersetzt wird. Die Komplexität dieser Möglichkeit würde jedoch den Rahen dieses Buches sprengen, deshalb möchte ich Sie an dieser Stelle nur auf diese nktionalität aufmerksam machen.

Pattern Matching

10 Pattern Matching Dieses Kapitel macht Sie vertraut mit den Begriffen Pattern Matching und Reguläre Ausdrücke. Ich werde behutsam in das Thema eintauchen und versuchen, das komplexe Gebiet so einfach und verständlich wie möglich näher zu bringen. Am Ende des Kapitels werden Sie dennoch in der Lage sein, auch schwierige reguläre Ausdrücke zu verstehen und effizienten Programmcode für die Text-Bearbeitung selbst zu erstellen.

10.1 Begriffs-Erklärungen 10.1.1

Semantik

Wenn in einem deutschen Text das Zeichen »Ø« vorkommt, dann handelt es sich wohl um einen Syntax-Fehler, weil dieses Zeichen in der deutschen Sprache nicht vorkommen kann (Auch wenn fleißige Asterix-Leser das Gegenteil behaupten, was ich ihnen beim besten Willen nicht verübeln kann, weil ich zu den fleißigsten Asterix-Lesern überhaupt zähle.). Wenn jemand das Wort »weil« mehrfach hintereinander schreibt, dann ist das kein Syntax-Fehler, denn das Wort ist laut Duden durchaus erlaubt. Aber auf der anderen Seite macht der Satz »weil weil weil weil« nicht wirklich Sinn, oder? Das ist nämlich ein Semantik-Fehler, denn nach dem Wort »weil« darf dasselbe Wort nicht noch einmal stehen. Will man zum Beispiel die Syntax des Source-Codes eines Java-Programms überprüfen, liegt die Hauptaufgabe darin, nach unerlaubten Zeichen zu suchen, das ist noch recht einfach. Die Semantik zu prüfen ist jedoch wesentlich schwieriger, weil man nicht nur nach unerlaubten Zeichen, sondern nach (durchaus gültigen) Zeichen am falschen Ort suchen muss. Wie wir sehen, sind Syntax-Fehler wesentlich leichter zu erkennen als Semantik-Fehler, da Semantik immer komplexer ist als reine Syntax. 10.1.2

Pattern

Die Frage »Was ist ein Pattern?« ist schnell erklärt: Ein Pattern ist ein Muster. Toll, was? Aber ich will den geneigten Leser nicht verzweifeln lassen. Muster ist die wörtliche Übersetzung von Pattern und kann alles Mögliche bedeuten. In unserem Fall ist damit

Pattern Matching

r Begriff Suchmuster gemeint. Im einfachsten Fall kann dies ein einzelnes Zeichen in, das in einer Zeichenkette gesucht wird. Wenn wir zum Beispiel nach dem Zeien »e« in der Zeichenkette »Der Apfel fällt nicht weit vom Baum« suchen, dann ist s Zeichen »e« das Pattern. was umständlicher wird ein Pattern, wenn es reguläre Ausdrücke enthält. Auch hier ein kleines Beispiel zur Einführung: Ich möchte eine Zeichenkette daraufhin überprüfen, ob sie mit einem Punkt endet. diesem Fall reicht es nicht aus, nach einem Punkt zu suchen, denn dieser kann ja berall in der Zeichenkette vorkommen, am Anfang, mittendrin oder auch am Ende. Wir müssen einen Weg finden, das Suchmuster (unser Pattern) so zu schreiben, dass die Virtual Machine weiß: Wir suchen einen Punkt am Ende der Zeichenkette, nicht endwo. Wie sagt man das der Virtual Machine? Na ja, in Form von regulären Ausdrücken. Und damit wären wir schon beim nächsn erklärungsbedürftigen Begriff: 0.1.3

Regulärer Ausdruck

n regulärer Ausdruck (englisch: regular expression) bringt Semantik in das Suchmus(englisch: Search Pattern oder kurz Pattern). Bleiben wir der Einfachheit halber bei nserem Beispiel von vorhin: Wir suchen nach einem Punkt am Ende einer Zeichentte. Zu diesem Zweck haben die Erfinder der regulären Ausdrücke (God bless em) spezielle Sonderzeichen erfunden, mit denen unter anderem das Ende eines ings identifiziert werden kann. Bei regulären Ausdrücken wurde das Dollarzeichen dafür auserkoren. Wann immer man am Ende eines Patterns ein Dollarzeichen fint, heißt dies: Ende der zu durchsuchenden Zeichenkette. Und nun zu unserer ursprünglichen Aufgabe: »Suche nach einem Punkt am Ende der eichenkette«. Ganz intuitiv würde ich das folgende Pattern (noch einmal zur Aufischung: Pattern heißt Suchmuster) vorschlagen:

Das sieht doch schon nicht schlecht aus: Das Pattern besteht aus einem Punkt und nem Dollarzeichen, welches das Ende der zu durchsuchenden Zeichenkette markiert. Also: »Suche nach einem Punkt am Ende der Zeichenkette!«. ider ist die Sache nicht ganz so einfach, denn der Punkt wurde ebenfalls als Sonderchen für Patterns ausgesucht und hat somit eine besondere Bedeutung (für Wiss-

Pattern Matching

begierige: Er steht für ein beliebiges Zeichen). Jetzt stellt sich die Frage, wie gibt man der Virtual Machine an, dass sie nach einem Punkt am Ende und nicht nach irgendeinem beliebigen Zeichen am Ende des Strings suchen soll? Die Antwort ist, wie so oft, der Backslash: Wenn wir einen Backslash vor ein Zeichen setzen, das als Sonderzeichen fungiert, dann verliert es seine besondere Bedeutung und wird zu einem ganz normalen Zeichen. Also schreiben wir unser Pattern noch mal: \.$

Und schon haben wir unser erstes Pattern geschrieben! Achtung Wenn Sie in einem Java-Programm den Backslash zur Entwertung eines Sonderzeichens verwenden, muss der Backslash zweimal geschrieben (und damit entwertet) werden, da ein Backslash vom Compiler wiederum als Sonderzeichen behandelt wird:

// falsch String pattern = "\.$"; // richtig String pattern = "\\.$";

Ich möchte ja kein Schwarzseher sein, aber ganz ehrlich: Von hier bis zur völligen Verinnerlichung von regulären Ausdrücken wird noch eine (hoffentlich nicht zu lange) Zeit vergehen. Als abschreckendes Beispiel wollen wir eines der eingangs erwähnten Probleme noch einmal aufgreifen: »Wie kann ich feststellen, ob eine vorgegebene Email-Adresse gültig ist oder nicht?« In Worten ausgedrückt ist es gar nicht so schwer: Eine gültige Email-Adresse muss das At-Zeichen @ genau einmal enthalten. Umlaute und sonstige Sonderzeichen dürfen nicht vorhanden sein, mit Ausnahme der Zeichen . (Punkt) und – (Bindestrich). Nach dem At-Zeichen @ muss eine Internet-Domain in der Form abc.def stehen, es kann aber auch länger sein, zum Beispiel ab.cd.ef.gh. Ein Punkt muss hier mindestens vorhanden sein, sonst ist die Internet-Domain nicht gültig (de geht also nicht). Zum Schluss sei noch angemerkt, dass Email-Adressen case-insensitive sind, abc ist also dasselbe wie Abc, ABC oder aBc.

Pattern Matching

a ja, scheint wohl doch nicht so einfach zu sein. Wenn ich nun gnadenlos den reguren Ausdruck dafür zeige, sehen Sie gleich, warum: ^[A-Za-z\d_.-]+\@([A-Za-z_\d.-]+\.)+[A-Za-z]+$

Im Moment wissen wir nur, dass ein Dollarzeichen am Ende des Patterns das Ende des durchsuchenden Strings bedeutet, die Zeichenkette \. einen literalen Punkt (also rklich das Zeichen .). ie wir deutlich sehen, müssen noch viele Dinge gelernt werden, um den Rest zu erstehen. 0.1.4

Pattern Matching

e Frage »Was bitte schön ist Pattern Matching?« kann ich gut nachvollziehen. Für hr viele Programmierer, gerade für Einsteiger, ist dieses Thema auf lange Zeit ein uch mit sieben Siegeln. Manchen Entwicklern werden sich die Tiefen des Pattern Matching wohl nie eröffnen. Das liegt daran, dass es zum einen sehr kompliziert ist, m anderen besitzt selbst ein relativ einfach aufgebautes Pattern sehr kryptische Zeien wie zum Beispiel ^(.+).*?a{3}$

h selbst habe einige Zeit gebraucht, bis ich so weit war, dieses schwierige Thema zu rstehen und mein Wissen einigermaßen verständlich an andere weiterzugeben. Vor lem liegt die Hemmschwelle, sich mit dem Gebiet überhaupt ernsthaft zu beschäftigen, ativ hoch, weil man durch die vielen Sonderzeichen erst einmal abgeschreckt wird. Deshalb möchte ich kurz darauf eingehen, warum sich einige schlaue Leute auf die ahnsinnstat eingelassen haben, Pattern Matching in Verbindung mit regulären Ausücken (neudeutsch: regular expressions) in die Welt zu setzen: ner der vielen Gründe für das Entstehen von Pattern Matching ist wohl die Frage: Wie kann ich feststellen, ob eine vorgegebene Email-Adresse gültig ist oder nicht?« r Amerikaner könnte der Grund auch sein: »Wie kann ich möglichst einfach alle nderzeichen, sprich alle Zeichen, die nicht dem Zeichensatz ASCII mit 7 Bit entrechen, aus Texten entfernen?« Oder für Mathematiker: »Wie kann ich überprüfen, ob in einem String eine gültige stkomma-Zahl steht oder nicht?«

Pattern Matching

Man könnte die Liste der Entstehungsgründe von Pattern Matching beliebig lange fortführen. Die Komplexität von Pattern Matching kommt hauptsächlich dadurch zustande, dass den Zeichenketten eine bestimmte Semantik zugrunde liegt, die wie die Semantik einer Sprache eben sehr komplex ist. Allein dies begründet, warum es bis heute kein wirklich brauchbares Programm gibt, um Texte in eine andere Sprache zu übersetzen. Was bedeutet Matching?

Der Begriff Matching kann ins Deutsche übersetzt werden mit Auffinden oder Übereinstimmung. Wenn wir zum Beispiel nach einem »e« in der Zeichenkette »Der« suchen, dann haben wir einen Match (man könnte auch Treffer sagen), weil das »e« im Wort »Der« vorkommt. Suchen wir das gleiche Zeichen aber in der Zeichenkette »Das«, dann erzeugt die Suche keinen Match, weil »e« eben nicht in »Das« vorkommt. Hätten wir das Zeichen »E« in der Zeichenkette »Der« gesucht, dann hängt das Ergebnis der Suche davon ab, ob wir wirklich nach dem großen »E« suchen oder ob uns Groß- und Kleinschreibung nicht interessiert. Wir können beim Matching angeben, ob die Suche case-sensitive sein soll oder nicht. Unter Pattern Matching versteht man also das Suchen in Zeichenketten, wobei das Suchmuster allgemein als Pattern bezeichnet wird und neben normalen Zeichen auch reguläre Ausdrücke enthalten kann. Hinweis Pattern Matching kann in einer erweiterten Form auch zum Ersetzen von Zeichenketten (Substitution) verwendet werden.

10.2 Das Package java.util.regex Hinweis Eigentlich müssten wir erst die Grundlagen der regulären Ausdrücke behandeln. Da ich mit Ihnen jedoch keinen Kurs in Trockenschwimmen veranstalten möchte, sondern gerade beim so komplexen Thema reguläre Ausdrücke nicht auf praktische Beispiele verzichten kann, bespreche ich zuerst die Klassen und Methoden, die Java für Pattern Matching zur Verfügung stellt. Sollten Sie an der einen oder anderen Stelle Verständnisprobleme haben, wenn ich bei dieser Beschreibung auf einen regulären Ausdruck stoße, sollten Sie dennoch nicht verzweifeln, die Erklärung folgt, dessen können Sie sicher sein.

Pattern Matching

it Version 1.4 des JDK ist das Package java.util.regex Bestandteil der Java-Distribun. Es enthält im Wesentlichen die zwei Klassen Pattern und Matcher, die wir in diem Kapitel ausführlich behandeln werden. Ich möchte es aber ruhig angehen und Ihnen er nur einen kleinen Ausschnitt davon zeigen, schließlich kennen Sie im Moment nur nen kleinen Bruchteil der zur Verfügung stehenden regulären Ausdrücke. ttern.matches

e Klasse Pattern stellt eine so genannte Convenience-Methode zur Verfügung, mit er auf einfache Art und Weise eine Text-Suche durchgeführt werden kann. Generell zeichnet man Methoden, die einem die meiste Arbeit abnehmen (und deswegen eist auch weniger Freiheiten bieten) als Convenience-Methoden. Hier ist die Deklaion der Methode matches(): static boolean matches( String regex, CharSequence str )

Das Argument regex enthält das Suchmuster, in str ist die Zeichenkette gespeichert, die rchsucht werden soll. Wie wir am Schlüsselwort static sehen, kann die Methode ekt aufgerufen werden, man muss also kein Klassen-Objekt instanzieren. Hinweis Das Interface CharSequence ist übrigens ebenfalls neu mit JDK 1.4 hinzugekommen. Es fasst einige Methoden der Klassen String, StringBuffer und CharBuffer zusammen, d.h. Sie können hier auch einen String als Argument angeben. e Methode matches() liefert einen true-Wert zurück, wenn ein Match erzielt worn ist, ansonsten wird false zurückgegeben. Ein paar kleine Beispiele sollen den ebel lichten (und nebenbei auch zeigen, dass diese Methode nichts für wirkliche nner der Materie ist): String regex = "das"; String str = "der die das"; boolean flag = Pattern.matches( regex, str ); System.out.println( "flag = " + flag ); // flag ist false

ir durchsuchen den String "der die das" nach dem Suchmuster "das". Eigenartigereise liefert die Methode matches() einen false-Wert zurück, obwohl das Suchmuster rkommt. Nun, die Anwort ist: Das Pattern muss im gesamten String, der durchsucht wird, einen Treffer erzielen. Bei unserem Beispiel-Pattern heißt das, es wird nur dann

Pattern Matching

ein Treffer gefunden, wenn der String identisch mit der Zeichenkette "das" ist ("das ist" würde keinen Treffer erzielen). Dies ist im folgenden Beispiel der Fall: str = "das"; flag = Pattern.matches( regex, str ); System.out.println( "flag = " + flag ); // flag ist true

Hier sind Pattern und zu durchsuchende Zeichenkette identisch, die Forderung, dass unser Pattern für den gesamten String einen Treffer erzielt. Nun dasselbe, jedoch schreiben wir den ersten Buchstaben der zu durchsuchenden Zeichenkette groß: str = "Das"; flag = Pattern.matches( regex, str ); System.out.println( "flag = " + flag ); // flag ist false

Auch hier wieder kein Treffer. Pattern Matching ist nämlich grundsätzlich case-sensitive, es sei denn, man stellt dieses Verhalten um (was jedoch bei der statischen Methode matches() nicht möglich ist). Jetzt zeige ich Ihnen noch eine weitere Variante, die zu einem Treffer führt: regex = ".*das"; str = "der die das"; flag = Pattern.matches( regex, str ); System.out.println( "flag = " + flag ); // flag ist true

Beim Pattern habe ich einen Punkt (.) gefolgt von einem Asterisk (*) vor unser konstantes Suchmuster "das" gestellt. An dieser Stelle sei nur angemerkt, dass diese Kombination von Meta-Zeichen (so heißen Zeichen mit einer besonderen Bedeutung) einen Match für eine beliebige Anzahl von Zeichen erzeugt. Egal, wie lange der zu durchsuchende String ist, es wird immer dann ein Match erzeugt, wenn er mit "das" endet (es wird auch dann ein Treffer erzielt, wenn der String nur aus "das" besteht). So viel zur statischen Convenience-Methode matches(). Spätestens dann, wenn Sie ein bisschen vertrauter mit Pattern Matching und regulären Ausdrücken sind, werden Sie mir zustimmen, dass man die Methode nicht gerade als großen Wurf bezeichnen

Pattern Matching

0.2.1

Grundlagen einer Suche mit regex

vor wir in den eigentlichen Teil dieses Kapitels einsteigen (reguläre Ausdrücke), öchte ich Ihnen zeigen, wie man grundsätzlich mit den beiden Klassen des Packages regex umgeht. Eine Suche umfasst folgende Schritte: Regulären Ausdruck kompilieren mit Pattern.compile() Matcher-Instanz erzeugen mit matcher() Such-Methode aufrufen, z.B. find() ttern kompilieren

vor man ein Suchmuster, das zwar nicht immer, aber in der Regel aus regulären Ausücken besteht, verwenden kann, muss man es zunächst mit der statischen Methode Pattern.compile() kompilieren lassen. Erst nach dieser Kompilierung steht ein Pattern in Form einer Instanz der Klasse Pattern zur Verfügung. Dieses erzeugte Objekt wird benötigt, um mit der Methode matcher() eine Matcher-Instanz der Klasse Matcher zu erzeugen. Erst über dieses Objekt können wir eine Suche durchführen. Hört sich recht rwirrend an, ist aber ganz einfach, wenn man einmal das Schema begriffen hat. Deshalb lassen Sie mich das Ganze anhand eines einfachen Beispiels erläutern: // Wir erzeugen aus dem Suchstring "das" ein Objekt der // Klasse Pattern, indem wir die Methode compile() mit // unserem Suchstring als Argument aufrufen. Pattern pat = Pattern.compile( "das" );

e Methode compile() ist noch in einer weiteren Variante verfügbar, bei der man das rhalten des Pattern Matching steuern kann. Zum Beispiel ist es damit möglich, die che case-insensitive durchzuführen, also ohne Unterscheidung zwischen Großd Kleinschreibung: Pattern compile( String regex, int flags )

r flags kann man eine oder mehrere Konstanten der Klasse Pattern angeben, die mit dem bitweisen Operator | verknüpft werden. An dieser Stelle interessiert uns nur s Flag, mit dem man die Suche case-insensitive machen kann. Hier noch einmal der Aufruf von compile(), diesmal mit Flag:

Pattern Matching

Pattern pat = Pattern.compile( "das", Pattern.CASE_INSENSITIVE );

Hinweis Die Methode compile() kann unter Umständen eine Exception vom Typ java.util.regex.PatternSyntaxException auslösen, und zwar dann, wenn man in seinem regulären Ausdruck Syntax-Fehler hat. Da sie aus der Klasse RuntimeException abgeleitet ist, muss sie nicht mit einem try/catch-Block abgefangen werden (ich empfehle es jedoch). Matcher-Instanz erzeugen

Nachdem man ein Pattern aus dem regulären Ausdruck kompiliert hat, muss man eine Instanz von Matcher erzeugen. Dies erfolgt durch den Aufruf der Methode matcher(), die ebenfalls der Klasse Pattern angehört. Hier die Syntax von matcher(): Matcher matcher( CharSequence input )

Sehen wir uns auch hier ein einfaches Beispiel an, welches das bereits kompilierte Pattern von vorhin benutzt: // unser zu durchsuchender String String input = "der die das"; // Matcher-Instanz erzeugen Matcher m = pat.matcher( input );

Suche durchführen

Die Klasse Matcher bietet einige Methoden für die Suche an, ich möchte Ihnen hier die einfachste zeigen, sie heißt find(). Die Syntax ist ganz einfach: boolean find()

Traumhaft einfach, oder? Die Erklärung ist ebenso einfach: Die Methode sucht im Input-String nach der jeweils nächsten Zeichenkette, die das Pattern erfüllt. Wird ein

Pattern Matching

effer gefunden, liefert die Methode true zurück, andernfalls false. Die Suche folgt auf Basis der Standard-Einstellungen des Patterns bzw. den durch Flags geänrten Einstellungen im Aufruf der compile()-Methode. Beim allerersten Aufruf von find() beginnt die Suche am Anfang des Input-Strings, ruft man die Methode nach nem Treffer noch einmal auf, wird bei demjenigen Zeichen innerhalb des Inputings mit der Suche begonnen, das nach dem vorher gefundenen Treffer folgt. Hm, wenn ich es mir recht überlege, war diese Erklärung wohl doch nicht so einfach. un, dem kann mit einem anschaulichen Beispiel abgeholfen werden. Damit Sie den berblick behalten, füge ich die bisherigen Code-Zeilen hinzu: Pattern pat = Pattern.compile( "das" ); Matcher m = pat.matcher( "der die das" ); boolean found = m.find(); System.out.println( "Es wurde " + ( found ? "" : "k" ) + "ein Treffer gefunden" );

wird (wie nicht anders zu erwarten) ausgegeben, dass ein Treffer gefunden wurde. eich noch ein Beispiel, bei dem die Suche case-insensitive ist: Pattern pat = Pattern.compile( "das", Pattern.CASE_INSENSITIVE ); Matcher m = pat.matcher( "Der die DaS" ); boolean found = m.find(); System.out.println( "Es wurde " + ( found ? "" : "k" ) + "ein Treffer gefunden" );

Auch hier wird ein Treffer erzielt, obwohl Groß- und Kleinbuchstaben bunt gemischt rkommen. Versuchen Sie die vorherige Variante mit dem gemischten Input-String, nd Sie werden keinen Treffer erzielen. u guter Letzt ein Beispiel mit einer Schleife, bei dem die Anzahl der Zeichen "e" in einem String ermittelt wird. Hier wird demonstriert, wie find() bei mehrmaligem ufruf funktioniert. Außerdem verwende ich zwei weitere Methoden der Klasse

Pattern Matching

Matcher, mit denen die Treffer-Positionen innerhalb des Input-Strings ermittelt werden können. Sie finden das Programm auf der CD-ROM in der Datei Regex001.java:

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

// Datei Regex001.java import java.util.*; import java.util.regex.*; // Das Programm sucht nach allen Vorkommnissen des // Zeichens 'e' (Groß-/Kleinschreibung egal) im // ersten Kommandozeilen-Argument. // Wenn Treffer gefunden werden, so gibt es für // jeden einzelnen Match Zusatz-Informationen // über die Anfangs- und Ende-Positionen des // jeweiligen Treffers aus. public class Regex001 { public static void main( String[] args ) { if ( args.length != 1 ) { usage(); System.exit( 1 ); } String input = args[ 0 ]; // Hier schenke ich mir das Abfangen der // Exception der Einfachheit halber. Pattern pat = Pattern.compile( "e", Pattern.CASE_INSENSITIVE ); Matcher m = pat.matcher( input ); // Zähler für die Anzahl von 'e's int cnt = 0; // Vektoren für die Start-/Ende-Positionen // der einzelnen Treffer. Vector starts = new Vector(); Vector ends = new Vector(); // // // //

Such-Schleife, die so lange durchlaufen wird, bis kein Treffer mehr gefunden werden kann. Beim ersten Aufruf beginnt die Suche bei Position 0 im Input-String.

Pattern Matching

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85

while ( m.find() ) { // Start-Position des Treffers im // Input-String ermitteln. int start = m.start(); // Position des ersten Zeichens nach // dem Treffer ermitteln. int end = m.end(); // Positionen den Vektoren hinzufügen. starts.add( new Integer( start ) ); ends.add( new Integer( end ) ); // Zähler erhöhen. cnt++; } // Ausgabe der Gesamt-Zahl von Treffern. System.out.println( "Das Pattern 'e' erzielte insgesamt " + cnt + " Treffer\n" ); // // // // // if

Damit man die Zusatz-Informationen (Positionen innerhalb des Input-Strings) besser bewerten kann, gebe ich über dem Input-String die Positionen der einzelnen Zeichen aus. ( cnt > 0 ) { int len = input.length(); for ( int i = 0; i < len; i++ ) { System.out.print( i % 10 ); } System.out.println( "\n" + input + "\n" );

} // Wurden Treffer erzielt, dann hier // für jeden einzelnen Match Zusatz-Info // ausgeben. for ( int i = 0; i < cnt; i++ ) { System.out.println(

Pattern Matching

86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 }

"Treffer " + ( i + 1 ) + ": " + "Start = " + starts.elementAt( i ) + ", Ende = " + ends.elementAt( i ) ); } System.exit( 0 ); } static void usage() { System.err.println( "usage: Regex001 " ); }

Listing 10.1: Beispiel für eine Suche mit find()

Die folgenden Zeilen zeigen die Bildschirm-Ausgabe eines beispielhaften Programmaufrufs: java Regex001 "Ueber sieben Bruecken musst du gehen" Das Pattern 'e' erzielte insgesamt 8 Treffer 012345678901234567890123456789012345 Ueber sieben Bruecken musst du gehen Treffer Treffer Treffer Treffer Treffer Treffer Treffer Treffer

1: 2: 3: 4: 5: 6: 7: 8:

Start Start Start Start Start Start Start Start

= = = = = = = =

1, Ende = 2 3, Ende = 4 8, Ende = 9 10, Ende = 11 16, Ende = 17 19, Ende = 20 32, Ende = 33 34, Ende = 35

C:\buecher\java\Kapitel05\code>

Wichtig ist, dass man das Kommandozeilen-Argument in doppelte Anführungszeichen stellen muss, wenn der eingegebene String Leerzeichen enthält. Das Programm gibt zunächst die Gesamtzahl der gefundenen Treffer aus. Der besseren Übersicht halber wird anschließend der eingegebene String samt Positions-Nummern für die ein-

Pattern Matching

lnen Zeichen angezeigt. Darunter sieht man Detail-Informationen für jeden Tref, es wird sowohl die Start-Position des Treffers als auch die Position desjenigen ichens ausgegeben, bei dem die nächste Suche startet. 0.2.2

Methoden von java.util.regex.Pattern

er noch einmal eine Zusammenstellung der am häufigsten benutzten Methoden r Klasse Pattern. Sie dient als Referenz und enthält damit auch Beschreibungen, ren Bedeutung wir erst nach der Behandlung von regulären Ausdrücken verstehen: va.util.regex.Pattern.compile()

ese statische Methode kompiliert den angegebenen regulären Ausdruck in ein chmuster (Pattern) und gibt eine Instanz der Klasse Pattern zurück. Wenn der Ausuck Syntax-Fehler enthält, dann löst die Methode eine Exception der Klasse java.util.regex.PatternSyntaxException aus: static Pattern compile( String regex ) throws PatternSyntaxException static Pattern compile( String regex, int flags ) throws PatternSyntaxException

e erste Variante der Methode haben wir bereits kennen gelernt. Bei der zweiten riante kann man ein oder mehrere Flags angeben, mit denen man das Verhalten der atching-Operation beeinflussen kann. Hierzu verwenden Sie die Konstanten aus r Klasse Pattern. Wenn Sie mehr als ein Flag angeben wollen, dann müssen diese rch eine bitweise ODER-Funktion verknüpft sein. Die folgende Tabelle gibt eine bersicht der unterstützten Flags:

Pattern Matching

Flag

Bedeutung

CASE_SENSITIVE

Die Suche wird so eingestellt, dass sie case-insensitive ist, es wird also nicht zwischen Groß- und Kleinschreibung unterschieden. Dieses Flag entspricht der Matching-Option i von Perl. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?i).

DOT_ALL

Die Suche wird so eingestellt, dass das Meta-Zeichen . auch einen Treffer für das Zeilenende-Zeichen erzeugt, wenn der Input-String aus mehreren Zeilen besteht. Standardmäßig matcht der Punkt kein Zeilenende-Zeichen. Was ein ZeilenendeZeichen ist, bestimmt das Betriebssystem. Windows benutzt die Zeichen \r\n, Unix das Zeichen \n, und beim Mac ist es \r. Dieses Flag entspricht der Matching-Option s von Perl. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?s).

MULTILINE

Standardmäßig matchen die Meta-Zeichen ^ und $ den Beginn bzw. das Ende des Input-Strings. Mit dem Flag erzeugen diese Meta-Zeichen auch vor bzw. nach jedem Zeilenende-Zeichen einen Match. Dieses Flag entspricht der MatchingOption m von Perl. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?m).

COMMENTS

Normalerweise werden White Space-Zeichen (Leerzeichen, Tabs und Zeilenende-Zeichen) in regulären Ausdrücken als ganz normale Zeichen behandelt. Mit diesem Flag werden White Space-Zeichen vom Pattern-Compiler ignoriert, außerdem kann man Kommentare in den regulären Ausdruck einfließen lassen, die durch das Zeichen # gekennzeichnet sind und sich bis zum Ende der Zeile erstrecken. Auch diese Kommentare werden bei Verwendung dieses Flags ignoriert. Dieses Flag entspricht der Matching-Option x von Perl. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?x).

UNIX_LINES

Normalerweise bestimmt das verwendete Betriebssystem, woraus ein ZeilenendeZeichen besteht (siehe hierzu auch die Beschreibung des Flags DOT_ALL). Man kann bei der Matching-Operation aber auch erzwingen, dass grundsätzlich das Unix-Zeilenende-Zeichen benutzt wird, indem man dieses Flag angibt. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?d).

UNICODE_CASE

Wenn man die Suche case-insensitive einstellt, dann bezieht sich diese Einstellung standardmäßig nur auf die Zeichen des US-ASCII-Code, gilt also somit nicht für deutsche Umlaute. Das kann man mit diesem Flag ändern. Allerdings ist davon die Performance negativ betroffen. Dieselbe Einstellung erreicht man auch mit der im regulären Ausdruck eingebetteten Sequenz (?u).

Tabelle 10.1: Flags für Pattern.compile()

Pattern Matching

Flag

Bedeutung

CANON_EQ

Dieses Flag bezieht sich auf zusammengesetzte Zeichen wie zum Beispiel Vokale mit Akzenten. Diese bestehen aus zwei Zeichen und besitzen ein Äquivalent aus nur einem Zeichen. So entsteht zum Beispiel das Zeichen å durch die Zusammenfassung von a und ° (Grad-Zeichen), die übereinander geschoben werden. Der kanonische Zeichen-Code dieses Zeichens ist a\u030A, das Äquivalent dafür lautet ?. Mit diesem Flag erzielt der kanonische Zeichen-Code in einem regulären Ausdruck einen Match für ein Fragezeichen ?. Die Verwendung dieses Flag hat negative Auswirkung auf die Performance.

elle 10.1: Flags für Pattern.compile() (Forts.)

Hinweis Man kann kompilierte reguläre Ausdrücke auch zur Verbesserung der erformance verwenden, wenn sie auf mehrere Input-Strings angewendet werden sollen. In solchen Fällen muss der Ausdruck nur einmal kompiliert werden und kann anschließend beliebig oft für Matching-Operationen auf unterschiedliche nput-Strings angewendet werden. va.util.regex.Pattern.matcher()

achdem man einen regulären Ausdruck mit Hilfe der statischen Methode compile() ein Pattern kompiliert hat, ruft man die Instanz-Methode matcher() des zurückgeferten Objekts auf, um eine Instanz der Klasse Matcher zu erhalten, mit der schließend eine Matching-Operation durchgeführt werden kann. Die Methode hat gende Deklaration: Matcher matches( CharSequence input )

Das Argument input muss den zu durchsuchenden Input-String enthalten und ist m Typ CharSequence. Dieses Interface fasst einige Methoden der Klassen String, StringBuffer und CharBuffer zusammen und macht diese Klassen kompatibel zueinder. matcher() liefert eine Instanz der Klasse Matcher zurück, deren Methoden im chsten Abschnitt beschrieben werden. va.util.regex.Pattern.split()

e Methode split() kann als erweiterter StringTokenizer verwendet werden, der mit regulären Ausdrücken arbeitet. Sie hat folgende Deklarationen:

Pattern Matching

String[] split( CharSequence input ) String[] split( CharSequence input, int limit )

Die erste Variante ist gleichbedeutend mit: split( input, 0 ) split() verwendet das aktuelle Pattern als Trenner, um den angegebenen InputString aufzusplitten, und gibt die Einzelteile als String-Array zurück. Die Treffer sind nicht Bestandteil des Arrays. Ich glaube, um die volle Blütenpracht dieser Methode zu erkennen, braucht es ein kleines Beispiel:

import java.util.regex.*; ... String regex = "e"; String input = "Ein gut Regex braucht Weile"; Pattern pat = null; try { pat = Pattern.compile( regex ); } catch ( PatternSyntaxException ex ) { System.err.println( "Fehler in regex" ); System.exit( 1 ); } String[] result = pat.split( input ); // // // // // //

Hier noch einmal der Input-String mit Kennzeichnungen für alle 'e's (das erste "E" zählt nicht, da die Matching-Operation case-sensitive ist und wir nach kleingeschriebenen "e"s suchen. | | | | Ein gut Regex braucht Weile

// // // // //

Und hier das Ergebnis in result result[ 0 ] = "Ein gut R" result[ 1 ] = "g" result[ 2 ] = "x braucht W" result[ 3 ] = "il"

Pattern Matching

Ich glaube, das Ergebnis ist ziemlich einleuchtend. Dasselbe Resultat hätten wir auch mit einem StringTokenizer erzielt. Diejenigen, die sich nun die Frage nach der Exisnz-Berechtigung für reguläre Ausdrücke stellen, möchte ich beruhigen. Erstens abe ich in diesem Beispiel eigentlich gar keinen regulären Ausdruck eingesetzt, sonrn in Ermangelung von Kenntnissen nur einen konstanten String. Spätestens dann, nn Sie ein bisschen in die Thematik der regulären Ausdrücke hineingeschnuppert ben, werden Sie wohl nur noch sehr selten einen StringTokenizer benutzen wollen. enn wir den regulären Ausdruck "e" mit dem Flag CASE_INSENSITIVE kompilieren, nn erhalten wir ein etwas anderes Resultat: ... pat = Pattern.compile( regex, Pattern.CASE_INSENSITIVE ); ... // // // // // //

Das neue Ergebnis ist: result[ 0 ] = "" result[ 1 ] = "in gut R" result[ 2 ] = "g" result[ 3 ] = "x braucht W" result[ 4 ] = "il"

Nun hat das String-Array ein Element mehr, weil auch das erste, großgeschriebene " einen Treffer erzielt und somit mit zur Aufspaltung des Input-Strings verwendet rd. Da jedoch vor diesem Zeichen nichts steht, ist das erste Element des Arrays leer. Dem aufmerksamen Leser wird nun vielleicht auffallen, warum zwar das erste "E" in nen linken und einen rechten Teil aufgespalten wird, nicht aber das "e" am Ende des ings. Nun, dieses Verhalten ist die Standard-Einstellung. Wir werden gleich sehen, wie man das ändert. Hinweis Findet die Methode split() einen Treffer an Position 0 des InputStrings, dann erzeugt es ein leeres Element im Index 0 des zurückgelieferten Arrays. Es wird jedoch kein leeres Element am Ende des Arrays angehängt, wenn ein Treffer am Ende des Input-Strings erzielt wurde. Dieses Standard-Verhalten ann mit dem zweiten Argument von split() verändert werden.

Pattern Matching

Die folgende Tabelle erläutert die Bedeutung des zweiten Arguments von split(): Limit

Bedeutung

0

Alle Treffer mit Ausnahme des Matches am Ende des Input-Strings für das Pattern führen zu einer Aufsplittung in einen linken und einen rechten Teil. Das ErgebnisArray kann je nach Pattern und Input-String beliebig viele Elemente enthalten. Ist der reguläre Ausdruck z.B. "-", und der Input-String z.B. "-1-2-", dann enthält das Ergebnis-Array insgesamt 3 Elemente ("", "1", "2").

>0

Wie vorher, jedoch wird das Ergebnis-Array auf limit-Elemente beschränkt. Nur (limit - 1) Treffer führen zur Aufsplittung, und im letzten Array-Element steht der Rest des Input-Strings, der aufgrund der Mengen-Begrenzung nicht mehr aufgesplittet worden ist. Ist der reguläre Ausdruck z.B. "-", limit=3 und der Input-String z.B. "---1-2-3-", dann enthält das Ergebnis-Array insgesamt 3 Elemente ("", "", "-1-2-3"). Mit limit=5 erhält man ("", "", "", "1", "2-3-" ).

dir *.txt // Unix-SHELL /home/gast $ ls -l *.txt

Die beiden dargestellten Kommandos geben eine Liste aller Dateien des aktuellen Verzeichnisses aus, deren Dateiname mit ".txt" endet. Wichtigster Bestandteil des Kommandos ist der Asterisk (*), den Sie vor dem Punkt angeben, denn dabei handelt es sich um ein spezielles Meta-Zeichen (Meta-Zeichen sind allgemein Zeichen mit besonderer Bedeutung). In den gezeigten Beispielen wird das Kommando von einer SHELL (Kommandozeilen-Interpreter) gelesen und interpretiert, das Sonderzeichen * hat dabei die spezielle Bedeutung »x beliebige Zeichen«, wobei das x für eine ganze Zahl zwischen 0 und unendlich steht. Jede Datei, deren Name mit ".txt" endet, erfüllt die Such-Bedingung (auch eine Datei, deren Dateiname tatsächlich nur aus den vier Zeichen ".txt" besteht). Der String "*.txt" ist ein regulärer Ausdruck, der sich aus einer konstanten Zeichenkette (".txt") und einem so genannten Meta-Zeichen zusammensetzt. Natürlich kann man nicht davon ausgehen, dass ein Asterisk in der SHELL dieselbe SonderBedeutung hat wie bei Pattern Matching in Java (das übrigens aus dem von Perl hervorgegangen ist). Wollen wir uns nun also den Meta-Zeichen in regulären Ausdrücken zuwenden: 10.3.1

Meta-Zeichen

Wie bereits erwähnt, besteht ein regulärer Ausdruck in der Regel nicht nur aus konstantem Text (das wäre ja auch ziemlich langweilig), sondern enthält als Garnierung zusätzlich Meta-Zeichen, die den Ausdruck erst zu einem regulären Ausdruck machen und dem Pattern Matching eine gewaltige Fülle an Möglichkeiten gibt. In einem regulären Ausdruck werden die folgenden Zeichen als Meta-Zeichen mit besonderer Bedeutung interpretiert: ^$.\*+?{}()[]|

So viele sind es eigentlich gar nicht. Nun ja, ein paar kommen noch hinzu, die nur in Zusammenhang mit einem der hier aufgeführten Meta-Zeichen ebenfalls eine besondere Bedeutung gewinnen, ansonsten aber normale Zeichen sind. Aber alles zu seiner Zeit.

Pattern Matching

Hinweis Für alle Meta-Zeichen gilt: Will man literal nach einem Zeichen uchen, das eine besondere Bedeutung besitzt, dann muss man es entwerten, indem ein Backslash vor das Meta-Zeichen gestellt wird. ngen wir mit den einfachen Meta-Zeichen an: ^ Meta-Zeichen

Das Meta-Zeichen ^ (Caret) kennzeichnet standardmäßig den Beginn des Inputings. Wird der reguläre Ausdruck mit dem Flag MULTILINE kompiliert, dann erzeugt uch nach einem Zeilenende-Zeichen einen Treffer. Sehen wir uns hierzu ein paar ispiele an: // Input-Strings, der zweite enthält ein Zeilenende// Zeichen. String is1 = "Das ist eine Zeile"; String is2 = "2 Zeilen\ndas geht auch"; // Regulärer Ausdruck, wir suchen nach der Zeichenkette // "das", die je nach Einstellung entweder ganz am // Anfang des Input-Strings oder direkt nach einem // Zeilenende-Zeichen stehen muss String regex = "^das"; // Pattern mit dem Flag CASE_INSENSITIVE kompilieren Pattern pat = null; try { pat = Pattern.compile( regex, Pattern.CASE_INSENSITIVE ); } catch ( PatternSyntaxException ex ) { System.err.println( "Fehler im Pattern" ); System.exit( 1 );

// Suche auf Basis des regulären Ausdrucks im String is1 Matcher m = pat.matcher( is1 ); if ( m.find() ) { System.out.println( "Treffer" ); else { System.out.println( " kein Treffer" );

Pattern Matching

} // Das Ergebnis ist ein Treffer, weil String is1 // ganz zu Beginn des Strings "Das" enthält und die // Suche case-insensitive war. // nun suchen wir mit demselben Pattern in is2 m.reset( is2 ); if ( m.find() ) { System.out.println( "Treffer" ); } else { System.out.println( " kein Treffer" ); } // // // //

Hier wird kein Treffer gefunden, weil zwar die Zeichenkette "das" vorkommt, aber nicht am Beginn des Input-Strings, sondern nach einem ZeilenendeZeichen.

// Nun kompilieren wir den regulären Ausdruck mit dem // Flag Pattern.MULTILINE und suchen noch einmal // im String is2. Jetzt benötigen wir übrigens keinen // try/catch-Block, da wir aufgrund der ersten // Kompilierung bereits sicher sein können, dass unser // Pattern in Ordnung ist. Beachten Sie, dass ich die // beiden Flags mit dem "|"-Operator bitweise verknüpfe. pat = Pattern.compile( regex, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE ); m = pat.matcher( is2 ); if ( m.find() ) { System.out.println( "Treffer" ); } else { System.out.println( " kein Treffer" ); } // Jetzt wird die Zeichenkette "das" nach dem // Zeilenende-Zeichen gefunden.

Pattern Matching

Hinweis Bemerkenswert ist die Tatsache, dass, obwohl unser String is2 nur ein Unix-Zeilenende-Zeichen enthält, auch auf einem Windows-PC ein Treffer erzielt wird. Dies liegt daran, dass Java intern dasselbe Zeilenende-Zeichen verwendet wie Unix. $ Meta-Zeichen

Das Meta-Zeichen $ verhält sich ebenso wie ^, kennzeichnet jedoch nicht den eginn des Input-Strings, sondern das Ende. Kompiliert man ein Pattern, das $ entlt, mit dem Flag Pattern.MULTILINE, dann wird auch ein Treffer direkt vor einem ilenende-Zeichen erzielt, nicht nur am Ende des Input-Strings. Auch hierzu wier ein kleines Beispiel: String String String String

regex is1 = is2 = is3 =

= "gut"; "Reguläre Ausdrücke sind gut"; "Reguläre Ausdrücke sind gut!"; "Gut\ndass es reguläre Ausdrücke gibt";

Pattern pat = null; try { pat = Pattern.compile( regex, Pattern.CASE_INSENSITIVE ); } catch ( PatternSyntaxException ex ) { System.err.println( "Fehler im Pattern" ); System.exit( 1 );

Matcher m = pat.matcher( is1 ); if ( m.find() ) { System.out.println( "Treffer" ); else { System.out.println( " kein Treffer" ); // Es wird ein Treffer gefunden, weil "gut" ganz // am Ende des Input-Strings steht. m.reset( is2 ); if ( m.find() ) { System.out.println( "Treffer" );

Pattern Matching

else { System.out.println( " kein Treffer" ); } // Es wird kein Treffer gefunden, weil "gut" eben nicht // ganz am Ende des Input-Strings steht, sondern danach // noch ein Ausrufe-Zeichen steht. m.reset( is3 ); if ( m.find() ) { System.out.println( "Treffer" ); } else { System.out.println( " kein Treffer" ); } // Auch hier wird kein Treffer erzielt, weil die // Zeichenkette "gut" nicht ganz am Ende steht. // Nun verwenden wir wieder das Flag MULTILINE. pat = Pattern.compile( regex, Pattern.CASE_INSENSITIVE | Pattern.MULTILINE ); m = pat.matcher( is3 ); if ( m.find() ) { System.out.println( "Treffer" ); } else { System.out.println( " kein Treffer" ); } // Wir erzielen nun einen Treffer, da die Zeichenkette // "gut" direkt vor einem Zeilenende-Zeichen steht. // Interessant ist nun die Kombination aus den beiden // Meta-Zeichen ^ und $: regex = "^gut$"; // Mit diesem regulären Ausdruck suchen wir nach der // Zeichenkette "gut", die ganz am Anfang des Input// Strings (oder direkt nach einem Zeilenende-Zeichen, // je nach Flag-Einstellung bei der Kompilierung) // stehen muss. Mit dem Dollar-Zeichen muss es aber // gleichzeitig entweder ganz am Ende des Input-Strings // oder direkt vor einem Zeilenende-Zeichen stehen, // abhängig davon, ob das Flag MULTILINE verwendet // wird oder nicht. Hier ein paar Beispiele für // verschiedene Fälle:

Pattern Matching

// // // //

is is is is

= = = =

"gut": erzielt immer einen Treffer "\ngut": erzielt nur mit MULTILINE einen Treffer "gut\n": erzielt nur mit MULTILINE einen Treffer "\ngut\n": dito

. (Punkt) Meta-Zeichen

Der Punkt wird als Meta-Zeichen sehr häufig verwendet, vor allem in Verbindung mit Quantifiern, die wir bald kennen lernen werden. Er kennzeichnet ein beliebiges eichen. Ausnahme: Standardmäßig erzielt der Punkt keinen Treffer für Zeilenendeichen, es sei denn, man kompiliert den regulären Ausdruck mit dem Flag Pattern.DOTALL. hen wir uns die Verwendung des Meta-Zeichens Punkt in einem Beispiel an: String regex = ".ein"; // Wir suchen nach der Zeichenkette "ein", vor der genau // ein beliebiges Zeichen stehen muss. Dabei ist es // völlig egal, um was für ein Zeichen es sich dabei // handelt (Ausnahmen sind Zeilenende-Zeichen). // Hier ein paar Input-Strings, welche die Bedingung // erfüllen und einen Treffer erzielen: // "mein", "kein", "fein", "sein", ",ein", " ein", // ".ein", "Rein". // Aber: // "\nein" erfüllt die Bedingung bei normaler // Kompilierung des regulären Ausdrucks nicht. // Nur wenn man das Flag DOTALL bei der Kompilierung // angibt, wird auch hier ein Treffer erzielt. // // // // // // // // // // // //

Interessant: Auch "keiner", "Meines Erachtens", "Gemeinheit" erfüllen das Such-Kriterium, weil wir im regulären Ausdruck ja nicht angegeben haben, wo die Zeichenkette im Input-String stehen soll, sondern nur, dass sie vorhanden sein muss. Hätten wir das Dollar-Zeichen verwendet: String regex = ".ein$"; dann wäre klar: es wird nur dann ein Treffer erzielt, wenn der Input-String mit "ein" endet (bzw. vor einem Zeilenende-Zeichen steht, wenn der reguläre Ausdruck mit dem Flag MULTILINE kompiliert wurde).

Pattern Matching

\ Meta-Zeichen

Der Backslash \ wird in den verschiedensten Variationen für reguläre Ausdrücke verwendet. So zum Beispiel ähnlich wie in Strings, um mit einer Escape-Sequenz spezielle Zeichen zu kodieren: Escape-Sequenz

Bedeutung

\\

Literaler Backslash

\n

Zeilenvorschub (0x0a)

\r

Wagenrücklauf (0x0d)

\t

Tabulator (0x09)

\f

Seitenwechsel (0x0c)

\a

Klingelzeichen (0x07)

\e

Escape (0x1b)

\cx

Control-Zeichen x (z.B. \cm für STRG-M)

\0n, \0nn

Oktal-Code eines Zeichens (0 java WinEvents3 Handler 1 Handler 2

An der Ausgabe ist zu erkennen, dass die Event Source das erzeugte Event nicht paralan alle registrierten Listener schickt, sondern dies passiert nacheinander (sequenzi) in der Reihenfolge der Registrierung. Das Besondere daran ist, dass ein Listener i allen Low-Level-Events die Möglichkeit hat, das Event sozusagen aufzubrauchen, dem er die Methode consume() aufruft, die in der Klasse des Event-Objekts definiert ist. Die anschließend folgenden Empfänger des Events wiederum können mit lfe der Methode isConsumed() abfragen, ob das Event von einem der Vorgänger reits verbraucht wurde und somit nicht mehr von Interesse ist. Damit stehen dem ogrammierer interessante Aspekte zur Verfügung. Anhand eines einfachen Beispiels möchte ich Ihnen diese Variante der Event-Veraritung demonstrieren: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

// Datei SeqEvents.java import java.awt.*; import java.awt.event.*; import javax.swing.*; class SeqEvents { public static void main( String[] args ) { JFrame frm = new JFrame( "Event-Beispiel" ); frm.setSize( 200, 200 ); Container cp = frm.getContentPane(); cp.addMouseListener( new MouseAdapter() { public void mousePressed( MouseEvent ev ) { System.out.println( "x = " + ev.getX() + ", y = " + ev.getY() ); ev.consume(); }

Grafik-Programmierung

25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

public void mouseReleased( MouseEvent ev ) { System.out.println( "x = " + ev.getX() + ", y = " + ev.getY() ); } } ); cp.addMouseListener( new MouseAdapter() { public void mousePressed( MouseEvent ev ) { if ( ev.isConsumed() ) { System.out.println( "Maus Event verbraucht" ); } else { System.out.println( "Maus gedrueckt" ); } } public void mouseReleased( MouseEvent ev ) { if ( ev.isConsumed() ) { System.out.println( "Maus Event verbraucht" ); } else { System.out.println( "Maus losgelassen" ); } } } ); frm.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); }

Grafik-Programmierung

69 70 71 72 73 }

} ); frm.setVisible( true ); }

Das Programm registriert zwei Listener für Mausklicks in der Content Pane des Fenss sowie einen weiteren für das Schließen des Fensters. Der erste Mausklick-Listener berschreibt die Methode mousePressed(), in der die Maus-Position ausgegeben und nach das Event aufgebraucht wird. Ebenso überschrieben wird die Methode mouseReleased(). Nur fehlt hier der Aufruf von consume(). er zweite Listener fragt in seiner mousePressed()-Methode ab, ob das Event von einem Vorgänger aufgebraucht wurde, und gibt je nach Lage der Dinge eine entspreende Meldung aus. Zusätzlich überschreibt er die Methode mouseReleased(), in der e Abfrage ebenfalls durchgeführt wird. Startet man das Programm und klickt mit der Maus irgendwo in das Fenster (wobei es egal ist, welche Maustaste man drückt), so kommt zuerst die Methode mousePressed() des Listeners zum Zuge, der die Position ausgibt und das Event aufbraucht. Anschlieend ist der zweite Listener an der Reihe und gibt in seiner Methode mousePressed() aus, dass das Event bereits aufgebraucht wurde. Lässt man nun die Maustaste wieder los, dann wird die Methode mouseReleased() der beiden Listener aufgerufen, wiederum in derselben Reihenfolge. Diesmal gibt der zweite Listener jedoch eine andere Meldung s vorher aus, weil das Event durch den ersten Listener nicht aufgebraucht worden ist. 4.3.3

Adapter-Klassen

eist implementiert man nicht vollständig ein Listener-Interface, sondern erweitert eine Adapter-Klasse, die alle vom Interface verlangten Methoden leer vordefiniert d so die Regeln für Java-Interfaces einhält. Der Vorteil von Adaptern ist, dass man in seinem Listener immer nur diejenige Methode überschreiben (und damit auch klarieren) muss, für die man sich interessiert. va.awt.event.WindowAdapter

r alle Events, die mit Fenstern zu tun haben, können wir dazu die Adapter-Klasse WindowAdapter verwenden, die zum Package java.awt.event gehört. Die folgende

belle gibt Ihnen einen Überblick der von den Interfaces deklarierten ListenerMethoden:

Grafik-Programmierung

Hinweis Alle Methoden in der Tabelle bekommen als Argument ein Objekt der Klasse java.awt.event.WindowEvent übergeben.

Listener-Methode

Beschreibung

windowActivated

Wird aufgerufen, wenn der Frame bzw. das Fenster zum aktiven Fenster des Desktops wird, es erhält in diesem Fall immer den Input-Fokus. Falls das HauptfensterHauptfenster weitere Kind-Fenster enthält, löst das erste KindFenster ebenfalls dieses Event aus.

windowClosed

Diese Methode wird aufgerufen, nachdem ein Fenster oder ein Frame durch den Aufruf der Methode dispose() geschlossen wurde, wodurch alle System-Ressourcen freigegeben werden.

windowClosing

Wird dann aufgerufen, wenn man den Close-Button in der Titel-Leiste des Fensters betätigt, wodurch der Close-Prozess angestoßen wird.

windowDeactivated

Wird aufgerufen, wenn ein Fenster bzw. ein Frame den Input-Fokus verliert, weil ein anderes Fenster des Desktops zum aktiven Fenster wird.

windowDeiconified

Wird aufgerufen, wenn man ein HauptfensterHauptfenster aus dem minimierten Zustand in den Normal-Zustand bringt.

windowGainedFocus

Wird aufgerufen, wenn der Frame bzw. das Fenster den Input Fokus erhält.

windowIconified

Wird aufgerufen, wenn man ein HauptfensterHauptfenster vom NormalZustand in den minimierten Zustand bringt.

windowLostFocus

Wird aufgerufen, wenn der Frame bzw. das Fenster den Input Fokus verliert.

windowOpened

Wird einmalig dann aufgerufen, wenn ein neues Fenster sichtbar gemacht wird.

windowStateChanged

Wird immer dann aufgerufen, wenn sich der Zustand eines Frames bzw. eines Fensters ändert (z.B. wenn es minimiert, maximiert oder geschlossen wird).

Tabelle 14.1: Methoden von java.awt.event.WindowAdapter

javax.swing.event.MouseInputAdapter

GUI-Programme werden fast ausschließlich mit der Maus (und nebenbei natürlich der Tastatur) bedient. Die Programmaktionen stößt der Anwender an, indem er irgendeine der Maustasten betätigt, wodurch mindestens ein Maus-Event ausgelöst wird. Zusätzlich kann gleichzeitig eine oder mehrere Modifier-Tasten auf dem Keyboard gedrückt werden. Modifier-Tasten sind (ALT), (STRG), (ª_), (ALT_GR) und bei manchen Tastaturen auch die (META)-Taste. Damit lassen sich viele verschiedene Varianten mit nur einer einzigen Maustaste unterscheiden. Das Programm kann im Event Listener nicht nur feststellen, welche Maustaste betätigt wurde, sondern auch, ob eine oder mehrere Modifier-Tasten zusätzlich beteiligt sind. Auch kann unterschieden werden, ob eine Maustaste gedrückt oder losgelassen wurde.

Grafik-Programmierung

Doch damit nicht genug, selbst Maus-Bewegungen über einer Komponente des Fenss kann das Programm registrieren (obwohl dieses Feature nicht unbedingt mit einer hen Performance verträglich ist, weil damit sehr viele Events durch die Gegend schickt werden). Zu guter Letzt können über die Maus-Events auch Drag-Operationen verarbeitet rden. Dabei drückt der Anwender eine Maustaste (press), bewegt die Maus schließend bei gedrückter Maustaste (drag) und lässt die gedrückte Maustaste dann eder los (release). Hinweis Mit JDK 1.4 wurde der Support für Mäuse mit einem Drehrad (Wheel-Mäuse) eingeführt. Um solche Events zu verarbeiten, verwendet man das Interface MouseWheelListener, das zugehörige Event gehört zur Klasse MouseWheelEvent. Zu 99% jedoch wird man in GUI-Programmen auf das Maus-Event clicked reagieren, s ausgelöst wird, wenn eine Maustaste gedrückt und anschließend dieselbe Taste eder losgelassen wird. r die einzelnen Maus-Events kann man natürlich verschiedene Listener implemenren. Einfacher jedoch ist auch hier die Verwendung eines Adapters, der alle benöten Methoden leer implementiert und uns somit Arbeit abnimmt. Ich empfehle nen den MouseInputAdapter, denn er implementiert alle Maus-Interfaces (na ja, fast e, mit Ausnahme des MouseWheelListener, der ist ja auch noch neu, weil er erst mit m JDK 1.4 hinzugekommen ist). Hinweis Der MouseInputAdapter ist im Package javax.swing.event enthalten, nicht in java.awt.event. der folgenden Tabelle der implementierten Methoden von MouseInputAdapter sitzen alle Methoden ein Argument vom Typ MouseEvent. Listener-Methode

Beschreibung

mouseClicked

Methode des Interfaces MouseListener Wird aufgerufen, wenn man eine beliebige Maustaste herunterdrückt und dieselbe Taste wieder loslässt.

elle 14.2: Methoden von javax.swing.event.MouseInputAdapter

Grafik-Programmierung

Listener-Methode

Beschreibung

mouseDragged

Methode des Interfaces MouseMotionListener Diese Methode wird nach dem Drücken einer Maustaste so lange aufgerufen, wie die Taste gedrückt bleibt und die Maus bewegt wird (selbst dann, wenn sich der Mauszeiger bereits außerhalb der GUI-Komponente befindet, innerhalb der das ursprüngliche Klick-Event stattgefunden hat).

mouseEntered

Methode des Interfaces MouseListener Wird an den Grenzen einer GUI-Komponente aufgerufen, wenn die Maus von außen in die Komponente hineinbewegt wird.

mouseExited

Methode des Interfaces MouseListener Wird an den Grenzen einer GUI-Komponente aufgerufen, wenn die Maus von innen aus der Komponente herausbewegt wird.

mouseMoved

Methode des Interfaces MouseMotionListener Wird aufgerufen, wenn die Maus innerhalb der GUI-Komponente bewegt wird.

mousePressed

Methode des Interfaces MouseListener Wird aufgerufen, wenn man eine beliebige Maustaste drückt.

mouseReleased

Methode des Interfaces MouseListener Wird aufgerufen, wenn man eine gedrückte Maustaste wieder loslässt.

Tabelle 14.2: Methoden von javax.swing.event.MouseInputAdapter (Forts.)

java.awt.event.KeyAdapter

Ebenfalls nützlich für die Verarbeitung von Standard-Events ist die Klasse java.awt.event.KeyAdapter. Sie ist vergleichsweise einfach. Hinweis Alle Methoden der Klasse KeyAdapter besitzen ein Argument vom Typ KeyEvent.

Listener-Methode

Beschreibung

keyPressed

Wird aufgerufen, wenn man eine Taste auf dem Keyboard herunterdrückt.

keyReleased

Wird aufgerufen, wenn man eine Taste auf dem Keyboard loslässt.

keyTyped

Wird aufgerufen, wenn man eine Taste auf dem Keyboard herunterdrückt und anschließend loslässt.

Tabelle 14.3: Methoden von java.awt.event.KeyAdapter

Grafik-Programmierung

4.3.4

Event Listeners und java.beans.EventHandler

it dem JDK 1.4 kam die neue Klasse java.beans.EventHandler hinzu, die eine Resurcen-sparende Möglichkeit für die Registrierung von Event Listeners bietet. Hierr verwendet man die Methode create(). Sie liefert in allen Varianten den Datentyp Object zurück, den man auf das gewünschte Listener Interface casten muss. Allerdings i hinzugefügt, dass dieses Modell der Definition von Event Listeners etwas gewöhngsbedürftig und auch nicht in allen Fällen anwendbar ist (so darf die Actionethode des Event Listeners zum Beispiel nur ein einziges Statement enthalten). Hinweis Wie man am Package-Namen java.beans für die Klasse EventHandler sieht, folgt die Implementierung den Richtlinien für Java-Beans, d.h. ein BeanObjekt besitzt Properties, und für den Zugriff auf diese Properties müssen Accesor- und Mutator-Methoden nach dem Schema get und set implementiert werden, wobei der erste Buchstabe von PropertyName großzuschreiben ist.

static Object create( Class listenerInterface, Object target, String action

ese Variante von create() erzeugt eine Listener-Implementierung, in welcher alle Interface deklarierten Methoden mit dem Statement target.action() ohne Arguente ausgestattet werden. Hier ein Beispiel, das beim Drücken auf einen Button die Methode pack() des Fenster-Objekts aufruft: // Wir befinden uns im Konstruktor einer aus JFrame // abgeleiteten Klasse und implementieren einen // ActionListener, der beim Drücken auf den Button // btn die pack()-Methode im Frame-Objekt aufruft. btn.addActionListener( ( ActionListener ) EventHandler.create( ActionListener.class, this, "pack" ) );

Grafik-Programmierung

// Dasselbe mit einer inneren anonymen Klasse ... final JFrame frm = this; btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { frm.pack(); } } ); // // // // // // // //

Da wir in der Methode actionPerformed aus einer inneren Klasse heraus auf das Frame-Objekt zugreifen, müssen wir über eine Hilfs-Variable, die als final deklariert ist, dafür sorgen, dass dem Gesetz Genüge getan ist. Wir hätten z.B. nicht das reservierte Wort this benutzen können, weil wir in der inneren Klasse sind, und dort zeigt this nicht mehr auf das Frame-Objekt.

Bei der nächsten Variante von create() kann man als zusätzliches Argument den Namen einer Property des Event-Objekts angeben, das mit Hilfe der zugehörigen Accessor-Methode gelesen wird. action wiederum ist der Name einer Property im Ziel-Objekt target, das durch die entsprechende Mutator-Methode auf den gelesenen Wert gesetzt werden soll. static Object create( Class listenerInterface, Object target, String action, String eventPropertyName )

Ich glaube, bei so viel staubtrockener Theorie wirkt ein Beispiel wie Balsam. Wir implementieren einen ActionListener, der beim Drücken auf den Button dessen Text mit dem Titel des Fensters überschreibt: // herkömmliche Methode: final JFrame frm = this;

Grafik-Programmierung

btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { btn.setText( frm.getTitle() ); } } ); // neue Methode: btn.addActionListener( ( ActionListener ) EventHandler.create( ActionListener.class, btn, "text", "source.getTopLevelAncestor.title" ) );

h schätze, auch das Beispiel selbst ist erklärungsbedürftig. Das letzte Argument der Methode create() ist der durch Punkt qualifizierte Property-Name für den FensterTitel. Ausgangspunkt ist das Event-Objekt. source ist ein Property dieses Objekts, das auf die Instanz der Event-Quelle zeigt (unser Button). Wir müssen aber den FensterTitel lesen, und der ist nicht im Button-Objekt hinterlegt, sondern im Property title des nster-Objekts. Also müssen wir uns mit getTopLevelAncestor() (das ist eine Methode in der Klasse JComponent, aus welcher ein JButton abgeleitet wird) eine Refeenz auf das Frame-Objekt besorgen. Von diesem Objekt schließlich wird die Methode getTitle() aufgerufen. Der return-Wert wird nun als Argument von setText() aus unserer Button-Instanz verwendet, um den Button mit einem neuen Label zu versorgen. Als Ergebnis steht auf dem Button derselbe String wie in der Titel-Leiste unseres nsters. Der Java Compiler macht aus unserem Programmcode Folgendes: btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { btn.setText( ev.getSource().getTopLevelAncestor().getTitle() );

Grafik-Programmierung

} } );

Wir hätten übrigens statt der Property-Namen text bzw. title auch gleich die Namen der Methoden verwenden können: btn.addActionListener( ( ActionListener ) EventHandler.create( ActionListener.class, btn, "setText", "source.getTopLevelAncestor.getTitle" ) );

Die Virtual Machine ist so schlau, dass sie bemerkt, ob der angegebene String für die Property bereits ein Methodenname ist oder wirklich nur der Property-Name. Nun zur letzten Variante der create()-Methode. Die bisherigen hatten alle im Interface deklarierten Methoden mit demselben Statement ausgerüstet. Für den Fall, dass man je nach Event unterschiedliche Aktionen ausführen möchte, gibt es die create()Methode mit fünf Argumenten, bei der man im letzten Argument den Namen der Handler-Methode im Listener angeben kann, die mit Leben gefüllt werden soll. Alle anderen im Interface deklarierten Methoden sind dann leer. static Object create( Class listenerInterface, Object target, String action, String eventPropertyName, String listenerMethodName )

Auch hier sollte ein Beispiel Licht ins Dunkel bringen. Wir definieren zwei Listener, die auf Maus-Events in der Content Pane reagieren, in der wir einen Button untergebracht haben. Klickt man irgendwo in die Fenster-Fläche, dann soll nach dem Aktivieren der Maustaste unser Button genau an die Stelle verschoben werden, wo der Klick stattgefunden hat. Nach dem Loslassen der Maustaste soll unser Button in die

Grafik-Programmierung

ke obere Ecke der Content Pane geschoben werden. Hier sind die beiden notwengen Listener: // Listener für das Herunterdrücken der Maustaste // Es wird die Methode mousePressed() implementiert, // in der vom Event-Objekt die Methode getPoint() // aufgerufen und der return-Wert als Argument an // die Methode setLocation() des Button-Objekts // übergeben wird. p.addMouseListener( ( MouseListener ) EventHandler.create( MouseListener.class, btn, "location", "point", "mousePressed" ) ); // Listener für das Loslassen der Maustaste // Es wird nur die Methode mouseReleased() // implementiert. // Über das Event-Objekt wird die linke obere // Ecke des Containers, in dem der Button liegt, // ermittelt und dieser Wert als Argument für // die setLocation()-Methode des Button-Objekts // verwendet. p.addMouseListener( ( MouseListener ) EventHandler.create( MouseListener.class, btn, "location", "source.parent.location", "mouseReleased" ) );

14.4 Swing-Komponenten diesem Abschnitt möchte ich Sie Stück für Stück mit den Komponenten vertraut machen, die Swing zur Verfügung stellt. Ein paar davon haben wir ja bereits kurz kenn gelernt, als es um Fenster ging. Beginnen wir mit den einfachen Dingen:

Grafik-Programmierung

14.4.1

Labels

Labels sind einfache GUI-Komponenten, die sowohl Text als auch ein Bild darstellen können, wobei alle Variationsmöglichkeiten gegeben sind (nur Text, nur Bild, Text mit Bild). Swing stellt Labels über die Klasse javax.swing.JLabel zur Verfügung. Bevor wir uns die Klasse näher ansehen, hier ein Beispielprogramm mit fünf verschiedenen Labels. Zunächst zeige ich Ihnen, wie sich das Programm präsentiert:

Abbildung 14.12: Label-Beispielprogramm

Das erste Label ganz links besteht nur aus Text, das zweite nur aus einem Bild (Icon). Label 3 und 4 besitzen sowohl ein Icon als auch Text mit verschiedenen Text-Ausrichtungen. Label 5 schließlich besteht aus HTML-Text, der mit seinen Auszeichnungen interpretiert und angezeigt wird (damit sind alle Auszeichnungs-Arten verfügbar, die HTML bietet, unter anderem auch mehrzeilige Labels). Und nun der dazugehörende Programmcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19

// Datei LabelDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class LabelDemo { public static void main( String[] args ) { LabelFrame frm = new LabelFrame( "Label Demo" ); frm.setVisible( true ); frm.pack(); } } class LabelFrame extends JFrame { public LabelFrame( String title ) {

Grafik-Programmierung

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63

super( title ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); JLabel lbl = new JLabel( "Label 1" ); p.add( lbl ); lbl = new JLabel( new ImageIcon( "label2.png" ) ); p.add( lbl ); lbl = new JLabel( "Label 3", new ImageIcon( "sound.gif" ), SwingConstants.CENTER ); p.add( lbl ); lbl = new JLabel( "Label 4", new ImageIcon( "sound.gif" ), SwingConstants.CENTER ); lbl.setHorizontalTextPosition( SwingConstants.CENTER ); lbl.setVerticalTextPosition( SwingConstants.BOTTOM ); p.add( lbl ); lbl = new JLabel( "Label 5" + "
mit Zeilenvorschub

" +

Grafik-Programmierung

64 65 66 67 68 69 70 71 }

"und farbig" + "" ); p.add( lbl ); cp.add( p ); }

Im Folgenden möchte ich die einzelnen Bestandteile des Programms erläutern. Konstruktoren von javax.swing.JLabel  JLabel()

Default-Konstruktor  JLabel( String text )

Konstruktor, der ein Label nur mit Text erzeugt. Der Text wird im zur Verfügung stehenden Rechteck für das Label vertikal zentriert ausgerichtet. Horizontal ist der Text links ausgerichtet.  JLabel( String text, int align )

Wie vorher, jedoch kann über das zweite Argument (int-Konstanten aus dem Swing-Interface SwingConstants) die horizontale Text-Ausrichtung festgelegt werden. Gültige Werte sind LEFT, CENTER, RIGHT, LEADING oder TRAILING.  JLabel( Icon img )

Konstruktor, der ein Label nur mit Icon erzeugt. Das Icon wird im Label-Rechteck sowohl horizontal als auch vertikal zentriert.  JLabel( Icon img, int align )

Wie vorher, jedoch kann man die horizontale Ausrichtung des Icons mit den Konstanten aus dem Interface SwingConstants festlegen.  JLabel( String text, Icon img, int align )

Konstruktor, der ein Label mit Text und Icon erzeugt. Der Text wird vertikal zentriert ausgerichtet, bezogen auf die Gesamthöhe des Labels. Horizontal steht der Text rechts vom Icon. Mit dem dritten Argument align kann die horizontale Ausrichtung beider Bestandteile (Text und Icon) festgelegt werden. Ausrichtung des Label-Textes

Bei Labels, die sowohl aus Text als auch aus einem Icon bestehen, kann man die horizontale sowie die vertikale Lage des Textes bezogen auf das Icon zusätzlich dynamisch einstellen:

Grafik-Programmierung

// horizontale Ausrichtung void setHorizontalTextPosition( int align ) // align muss eine der folgenden Konstanten aus // javax.swing.SwingConstants sein: // LEADING, LEFT, CENTER, RIGHT, TRAILING // vertikale Ausrichtung void setVerticalTextPosition( int align ) // align muss eine der folgenden Konstanten aus // javax.swing.SwingConstants sein: // TOP, CENTER, BOTTOM

rändern des Label-Textes

Mit der Methode setText() lässt sich dynamisch der Text des Labels verändern: void setText( String text )

amit wir das Ganze vertiefen, hier ein Beispielprogramm, bei dem im 2-Sekundenkt der Text um das Icon herumläuft. Zunächst ein paar Screenshots:

Abbildung 14.13: Label mit umlaufendem Text (Anfangszustand)

Abbildung 14.14: Label mit umlaufendem Text (Text oberhalb)

Abbildung 14.15: Label mit umlaufendem Text (Text rechts oben)

h glaube, Sie haben nun ein Gefühl dafür, wie die Sache weitergeht. Sehen wir uns un den Programmcode an:

Grafik-Programmierung

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

// Datei LabelDemo1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class LabelDemo1 { public static void main( String[] args ) { LabelFrame1 frm = new LabelFrame1( "Label Demo" ); frm.setVisible( true ); for ( int i = 1; i = hors.length ) i = 0; lbl.setHorizontalTextPosition( hors[ i ] ); lbl.setVerticalTextPosition( verts[ i ] ); lbl.setText( texts[ i ] ); lbl.setHorizontalAlignment( SwingConstants.CENTER ); lbl.setVerticalAlignment( SwingConstants.CENTER ); pack(); }

Grafik-Programmierung

Im Hauptprogramm fehlt der sonst übliche Aufruf von pack(), der das Fenster in seiner Größe so dimensioniert, dass alle darin enthaltenen Elemente Platz finden. Dies erledigt die Methode rotateText(). Diese rufen wir aus dem Hauptprogramm in einer Schleife (mit einer Wartezeit von einer Sekunde) auf. Kurz zum Konstruktor unserer Frame-Klasse: Er erzeugt ein Label mit Text und Icon in der Content Pane des Fensters. Die Haupt-Arbeit wird von rotateText() erledigt. Dort sind Arrays für die horizontalen und vertikalen Positions-Konstanten und für die Label-Texte aller möglichen Konstellationen definiert. Als Index wird die Instanz-Variable i verwendet. Ebenso als Instanz-Variable ist das Objekt für das Label deklariert, da wir sie sowohl im Konstruktor als auch in der Methode rotateText() verwenden. Mit jedem Aufruf der Methode wird der Index um 1 erhöht. Ist man am Ende der Arrays angelangt, beginnt der Zähler wieder bei 0. Es werden die horizontale sowie die vertikale Lage des Textes bezogen zum Icon gesetzt und der Text mit dem entsprechenden Wert belegt. Dann wird das Label zentriert und anschließend die Methode pack() aufgerufen, damit das Fenster an die jeweilige Größe des Labels angepasst wird. Übrigens: Zu den Mutator-Methoden gibt es auch die entsprechenden AccessorMethoden: int getHorizontalTextPosition() int getVerticalTextPosition() int getHorizontalAlignment() int getVerticalAlignment() String getText()

Auch das Icon kann dynamisch zur Laufzeit ausgetauscht werden mit: void setIcon( Icon img )

Den Abstand zwischen Text und Icon kann man mit den Methoden void setIconTextGap( int gap ) int getIconTextGap()

Grafik-Programmierung

tzen und lesen. Maß-Einheit hierfür ist Pixel. Zwei weitere Methoden dienen der Zuordnung des Labels zu einer anderen GUI-Komponente: void setLabelFor( Component c ) Component getLabelFor()

ir werden hierfür weiter unten noch ein Beispiel sehen. Hinweis Ein Label kann keine Maus-Events auslösen. Will man über ein Label dennoch eine Aktion starten, muss man die Maus-Events derjenigen GUI-Komponente dafür verwenden, in der das Label aufgenommen worden ist. bels mit anderen GUI-Komponenten verknüpfen

Mit der Methode setLabelFor() ist es möglich, ein Label-Objekt einer anderen GUIomponente zuzuordnen. Was damit gemeint ist, erfahren Sie weiter unten im Abschnitt 0 Textfelder. 4.4.2

Buttons

Buttons werden als GUI-Komponenten sehr häufig benutzt, um bestimmte Aktionen starten. Sie können wahlweise mit Text, Icon oder einer Kombination aus beidem koriert werden. Die Klasse javax.swing.JButton geht aus der Eltern-Klasse AbstractButton hervor, in der die meisten Methoden implementiert sind. Der Grund für ist, dass es noch andere Arten von Buttons gibt (Checkboxes, Radio Buttons), die bis auf kleinere Unterschiede dieselben Methoden besitzen wie JButton. Konstruktoren von javax.swing.JButton JButton()

Default-Konstruktor JButton( String text )

Erzeugt einen Button mit Text JButton( Icon img )

Erzeugt einen Button mit einem Icon JButton( String text, Icon img )

Erzeugt einen Button mit Text und Icon

Grafik-Programmierung

 JButton( Action a )

Erzeugt einen Button, dessen Eigenschaften (inklusive des Action Listeners) aus dem Argument entnommen wird Hinweis Das Interface javax.swing.Action wurde mit JDK 1.3 eingeführt und erlaubt unter anderem eine zentrale Verwaltung der registrierten Action Listeners. Ich glaube, hier muss ich ein kleines Anschauungsbeispiel einfließen lassen: Angenommen, Sie implementieren eine Aktion Datei speichern und registrieren diesen Listener sowohl für einen Menü-Punkt als auch für einen Button im Tool-Bar. Wenn das Programm nun in einem Zustand ist, der diese Aktion nicht erlaubt (weil zum Beispiel nichts zum Speichern vorhanden ist), dann müssen Sie beide GUI-Komponenten deaktivieren. Das Interface Action besitzt jedoch die Methode setEnabled(), mit der man das Action-Objekt sperren und freischalten kann. Dadurch werden alle Komponenten, für die das Listener-Objekt registriert wurde, über die Zustandsänderung benachrichtigt und verändern automatisch ihr Aussehen, so dass man zum Beispiel die gesperrte Aktion direkt erkennen kann, weil sowohl der Menü-Punkt als auch der Tool-Bar Button gedimmt sind. Ähnlich wie bei JLabel hat man auch bei JButton die Möglichkeit, den Text beliebig zum Icon zu positionieren sowie den Button als Ganzes auszurichten: void void void void int int int int

setHorizontalTextPosition( int align ) setVerticalTextPosition( int align ) setHorizontalAlignment( int align ) setVerticalAlignment( int align ) getHorizontalTextPosition() getVerticalTextPosition() getHorizontalAlignment() getVerticalAlignment()

Für das dynamische Austauschen des Button-Textes bzw. des Icons stehen die Methoden void setText( String text ) void setIcon( Icon img )

Grafik-Programmierung

zur Verfügung. Mit der Methode addActionListener() schließlich kann ein Event Handler für den Button definiert werden, der aufgerufen wird, wenn man mit der Maus auf den Button klickt. Buttons können mit der Methode setEnabled() auch deaktiviert sabled) werden. Ruft man diese Methode mit einem false-Wert als Argument auf, dann kann der Button keine Events mehr erzeugen und wird gedimmt dargestellt. vor wir zu weiteren Eigenschaften von Buttons kommen, wollen wir uns ein kleis Beispiel ansehen, das drei Buttons definiert. Mit einem Mausklick auf den mittleen Button erzeugt man einen Piepston, der linke Button deaktiviert den mittleren Button, während der rechte Button ihn wieder zum Leben erweckt. Das Programm äsentiert sich wie folgt:

Abbildung 14.16: Button-Beispielprogramm (Anfangszustand)

bildung 14.17: Button-Beispielprogramm (mittlerer Button deaktiviert)

Der Programmcode für das Beispiel: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16

// Datei ButtonDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ButtonDemo { public static void main( String[] args ) { ButtonFrame frm = new ButtonFrame(); frm.setVisible( true ); frm.pack(); } } class ButtonFrame extends JFrame { public ButtonFrame() {

Grafik-Programmierung

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

super( "Button Demo" ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); final JButton mbtn = new JButton( new ImageIcon( "sound.gif" ) ); mbtn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { Toolkit tk = Toolkit.getDefaultToolkit(); tk.beep(); } } ); JButton btn = new JButton( "mittleren Button deaktivieren", new ImageIcon( "rightArrow.gif" ) ); btn.setHorizontalTextPosition( SwingConstants.LEFT ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) {

Grafik-Programmierung

61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 }

mbtn.doClick(); mbtn.setEnabled( false ); } } ); p.add( btn ); btn = new JButton( "mittleren Button aktivieren", new ImageIcon( "leftArrow.gif" ) ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { mbtn.setEnabled( true ); mbtn.doClick(); } } ); p.add( mbtn ); p.add( btn ); cp.add( p ); }

nen Piepston kann man mit der Methode beep() erklingen lassen, die in der Klasse java.awt.Toolkit definiert ist. Da es sich hierbei um eine abstrakte Klasse handelt, uss man sich zuerst mit Hilfe der statischen Methode getDefaultToolkit() eine nkrete Instanz besorgen. Beispiel habe ich eine weitere Button-Methode benutzt (doClick()), mit der man s dem Programm heraus einen Mausklick auf einen Button simulieren kann. Dabei rd dasselbe Event ausgelöst, als hätte der Anwender tatsächlich mit der Maus auf n Button gedrückt. Dies passiert natürlich nur, solange der betreffende Button nicht aktiviert ist. Sie können es ausprobieren, indem Sie die beiden Zeilen 61 62

mbtn.doClick(); mbtn.setEnabled( false );

Grafik-Programmierung

miteinander vertauschen. Ruft man doClick() auf, nachdem der Button deaktiviert wurde, dann passiert überhaupt nichts. Hinweis Im Konstruktor müssen wir die Variable mbtn für den mittleren Button mit dem reservierten Wort final deklarieren, weil aus den anonymen Klassen der Event Listener darauf zugegriffen wird. Dies schreibt Java vor. Default-Button

Wenn in einem Frame mehrere Buttons vorhanden sind, dann kann man für die Root Pane des Frames einen so genannten Default-Button definieren. Im GUI wird dieser Default-Button durch eine stärkere Umrandung sichtbar gemacht. Drückt der Anwender die (¢)-Taste, dann wird automatisch ein Klick-Event auf diesen Button ausgelöst, so, als hätte man explizit mit der Maus auf den Button geklickt. In jeder Root Pane kann es maximal einen Default-Button geben. Um im Programm einen bestimmten Button zum Default-Button zu erklären, kann man das betreffende Button-Objekt als Argument an die Methode setDefaultButton() von JRootPane übergeben (hier ein Auszug aus dem gleich folgenden Beispiel, bei dem der mittlere Button zum Default-Button erklärt wird): getRootPane().setDefaultButton( mbtn );

Tastatur-Mnemonics

Buttons werden in der Regel durch einen Mausklick aktiviert, es ist aber auch möglich, dass ein Tastatur-Shortcut benutzt wird, um dasselbe Ergebnis zu erzielen. Solche Tastatur-Shortcuts nennt man auch Mnemonics, das sind Kombinationen einer normalen Taste mit einer so genannten Modifier-Taste (üblicherweise wird dafür die (ALT)-Taste verwendet). Java erlaubt fast für alle Swing-Komponenten solche Mnemonics, die man mit der folgenden Methode zuordnet: setMnemonic( int code )

Für Buttons ist die Methode in der Klasse AbstractButton implementiert. Das intArgument der Methode muss eine Konstante aus der Klasse java.awt.event.KeyEvent sein, dort sind für alle möglichen Tasten numerische Konstanten definiert, die alle mit der Zeichenkette VK_ beginnen. Im gleich folgenden Beispiel definiere ich zum Beispiel für den mittleren Button, der einen Piepston erzeugen soll, das folgende Mnemonic:

Grafik-Programmierung

mbtn.setMnemonic( KeyEvent.VK_B );

amit kann man den Button über die Tastatur durch Drücken der Kombination aus LT)-(B) aktivieren. olTips

Unter einem ToolTip versteht man Text, der dann angezeigt wird, wenn man den Mauszeiger längere Zeit über einer GUI-Komponente verweilen lässt. ToolTips wern häufig verwendet, um dem Anwender eine Hilfestellung für die einzelnen Diag-Elemente des Programms zu geben. Erzeugt werden ToolTip-Texte mit der Methode javax.swing.JComponent.setToolTipText(). Hier ein Auszug aus dem vernderten Beispielprogramm, den kompletten Source-Code finden Sie auf der CD-ROM in der Datei ButtonDemo1.java: lbtn.setToolTipText( "Drücken Sie auf diesen Button, " + "um den mittleren Button zu " + "sperren" );

hen wir uns jetzt das Beispielprogramm an, wie es sich uns auf dem Bildschirm darllt, wenn man längere Zeit mit dem Mauszeiger über dem linken Button verweilt:

Abbildung 14.18: Button-Beispielprogramm mit ToolTips und Mnemonics

as Programm arbeitet ähnlich wie das vorangegangene Beispiel, es besitzt drei Butns mit denselben Funktionen wie vorher. Allerdings habe ich diesmal eine andere plementierung gewählt, damit Sie unter anderem die verschiedenen Varianten der tener-Programmierung sehen. Diesmal verwende ich keine anonymen Klassen für ActionListener, sondern eine zentrale Methode actionPerformed(), welche die ents aller drei Buttons verarbeitet. Aus diesem Grund muss die Klasse ButtonFrame1 s ActionListener-Interface implementieren:

Grafik-Programmierung

15 class ButtonFrame1 extends JFrame 16 implements ActionListener

Damit die zentrale Methode actionPerformed() weiß, welche Aktion ausgeführt werden soll (die Methode wird ja durch ein Event eines beliebigen Buttons aufgerufen), muss man den einzelnen Buttons eine Identifikation mitgeben, dafür ist die Methode setActionCommand() zuständig, die in der Klasse AbstractButton implementiert ist: 68 69

mbtn.setActionCommand( "beep" ); mbtn.addActionListener( this );

Zeile 69 registriert die Methode actionPerformed() des Frame-Objekts für Action Events des mittleren Buttons, in Zeile 68 wird dem Event eine eindeutige Identifikation gegeben. In der Event Handler-Methode muss natürlich abgefragt werden, welcher Button das Event ausgelöst hat, dafür verwendet man die Methode getActionCommand(): 110 111 112

if ( ev.getActionCommand().equals( "beep" ) ) {

Da wir nun in einer eigenen Methode auf die einzelnen Button-Objekte zugreifen, müssen wir diese als Instanz-Variablen in der Klasse ButtonDemo1 definieren. Vorher war dies nicht nötig, weil wir dort sowohl die Variablen für die Buttons als auch den Code für die Event Listener innerhalb des Konstruktors stehen hatten. Noch eine Bemerkung zum Fokus von GUI-Komponenten. Eine Komponente kann nur dann Tastatur-Events erhalten, wenn sie den Input-Fokus besitzt. Dies trifft vor allem auf Eingabe-Felder zu. Mit der (ÿ_)-Taste kann man in einem Fenster den Input-Fokus nacheinander auf alle Dialog-Elemente legen, wobei das Element, welches aktuell den Fokus besitzt, besonders gekennzeichnet wird (bei Eingabe-Feldern blinkt zum Beispiel der Cursor). Vergleichen Sie doch einmal die Screenshots beider Programme (Abbildung 14.16 und Abbildung 14.18), ich hoffe, man kann den Unterschied auch ohne Farben sehen. In Abbildung 14.16 hat der linke Button einen leichten Rand um den ButtonText und das Icon herum. Das liegt daran, dass dieser Button den Input-Fokus besitzt. Starten Sie das Programm ButtonDemo einmal und drücken Sie mehrmals die (ÿ_)-

Grafik-Programmierung

ste, Sie werden sehen, dass alle Buttons der Reihe nach den Input-Fokus erhalten. lerdings ist der Fokus bei Buttons nicht so sehr von Belang, weil man sie meist mit r Maus bedient. Deshalb habe ich mit der Zeile 47

lbtn.setFocusPainted( false );

die Kennzeichnung des Buttons abgeschaltet. Rollover-Effekte

der Klasse javax.swing.AbstractButton, aus der unter anderem JButton abgeleitet sind Methoden für Rollover-Effekte implementiert. Darunter versteht man eine ränderung des Aussehens, wenn man mit der Maus über die Komponente fährt. er eine Liste der Methoden: void setRolloverIcon( Icon img ) void setRolloverEnabled( boolean enabled )

amit Sie sehen, wie sich Rollover-Effekte auswirken, habe ich ein kleines Beispiel rbereitet, das Sie auf der CD-ROM in der Datei ButtonDemo2.java finden. Das Programm zeigt einen Button, der statt eines Text-Labels ein Icon besitzt, sowie daneben einen einfachen Text. Wenn Sie nun die Maus über den Button bewegen, ndert sich das Aussehen des Knopfs. Verlassen Sie den Einflussbereich des Buttons, dann wird wieder das ursprüngliche Icon auf dem Button angezeigt. Mit den Methoden void setSelectedIcon( Icon img ) void setPressedIcon( Icon img ) void setRolloverSelectedIcon( Icon img )

nnen Sie zusätzliche Effekte erzielen. Ich glaube, die Namen der Methoden spreen für sich und müssen nicht näher erklärt werden. 4.4.3

Toggle Buttons

n Toggle Button ist ähnlich wie ein normaler Button eine Komponente, mit der an durch Drücken einer Maustaste eine Aktion startet. Er hat jedoch zwei unterhiedliche Zustände (selektiert bzw. nicht selektiert), die in der Darstellung des Buttons

Grafik-Programmierung

sichtbar gemacht werden. Mit jedem Klick auf den Button ändert sich dessen Zustand. Swing stellt Toggle Buttons mit der Klasse javax.swing.JToggleButton zur Verfügung. Zusätzlich zu den von JButton bekannten Konstruktoren kennt die Klasse JToggleButton zwei weitere Konstruktoren, mit denen der initiale Zustand des ButtonObjekts eingestellt werden kann. Dafür fehlen Konstruktoren, mit denen man die horizontale Ausrichtung des Buttons definieren kann. Konstruktoren von javax.swing.JToggleButton  JToggleButton()  JToggleButton( String text )  JToggleButton( Icon img )  JToggleButton( String text, Icon img )  JToggleButton( String text, boolean selected )  JToggleButton( Icon img, boolean selected )  JToggleButton( String t, Icon i, boolean sel )  JToggleButton( Action a )

Die Abfrage des Button-Zustands erfolgt mit der Methode isSelected(). Ich habe für Sie ein kurzes Beispielprogramm parat: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18

// Datei TButtonDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TButtonDemo { public static void main( String[] args ) { TButtonFrame frm = new TButtonFrame(); frm.setVisible( true ); frm.pack(); } } class TButtonFrame extends JFrame { public TButtonFrame() { super( "Toggle Button Demo" );

Grafik-Programmierung

19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 2 43 44 45 46 7 48 49 50 51 52 53 54 }

addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); final JToggleButton btn = new JToggleButton( "Toggle Button" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { if ( btn.isSelected() ) { btn.setText( "selektiert" ); } else { btn.setText( "Nicht selektiert" ); } } } ); cp.add( btn ); }

er ein paar Screenshots für die einzelnen Schritte des Programms:

Abbildung 14.19: ToggleButton Beispielprogramm (Anfangszustand)

Grafik-Programmierung

Abbildung 14.20: ToggleButton Beispielprogramm (einmal gedrückt)

Abbildung 14.21: ToggleButton Beispielprogramm (zweimal gedrückt)

Hinweis Wenn zur Laufzeit dynamisch der Text bzw. das Icon eines Buttons geändert wird, so dass der Button mehr Platz einnimmt als durch das aktuelle Layout vorgegeben, dann muss man die Methode pack() des umgebenden Frames aufrufen, damit dieser seine Komponenten und sich selbst neu layoutet und damit den neuen Größen anpasst. In Abbildung 14.21 sehen Sie, dass der Platz für den neuen Text des Buttons nicht mehr ausreicht. Wenn Sie folgende Zeilen einfügen, dann passt sich das Fenster automatisch den jeweiligen Größenverhältnissen an:

29 Container cp = getContentPane(); // neue Zeile hinzu: 29a final JFrame f = this; ... 46 // neue Zeile hinzu 46a

); f.pack();

Da wir die pack()-Methode in einer anonymen inneren Klasse aufrufen, müssen wir den Gesetzen der Sprache gehorchen und die Variable f für den Frame mit final zu einer Konstanten machen. 14.4.4

Check Boxes

Jeder, der schon einmal ein HTML-Formular ausgefüllt hat, ist mit Sicherheit auf Check Boxes gestoßen (Klasse javax.swing.JCheckBox). Dabei handelt es sich um Toggle Buttons, die zusätzlich zum Text und/oder Icon noch ein grafisches Element enthalten, über das der Zustand des Buttons (selektiert oder nicht selektiert) sichtbar gemacht wird (beim Toggle Button wurde dagegen die Hintergrundfarbe des Buttons geändert, um den jeweiligen Zustand anzuzeigen). Ansonsten sind Check Boxes und

Grafik-Programmierung

s Beispiel ein einfaches Programm, mit dem man die drei Grundfarben mit Check Boxes selektieren kann:

Abbildung 14.22: Check Box-Beispielprogramm (Anfangszustand)

bildung 14.23: Check Box-Beispielprogramm (Grün und Blau selektiert)

Der dazu gehörende Programmcode sieht folgendermaßen aus: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

// Datei CheckBoxDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class CheckBoxDemo { public static void main( String[] args ) { CheckBoxFrame frm = new CheckBoxFrame(); frm.setVisible( true ); frm.pack(); } } class CheckBoxFrame extends JFrame { public CheckBoxFrame() { super( "Check Box Demo" ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } }

Grafik-Programmierung

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 }

14.4.5

); Container cp = getContentPane(); JPanel p = new JPanel(); JCheckBox cbr = new JCheckBox( "Rot" ); p.add( cbr ); JCheckBox cbg = new JCheckBox( "Grün" ); p.add( cbg ); JCheckBox cbb = new JCheckBox( "Blau" ); p.add( cbb ); cp.add( p ); }

Radio Buttons

Radio Buttons (Objekte der Swing-Klasse javax.swing.JRadioButton), ebenfalls sehr geläufige Dialog-Elemente von GUI-Programmen, verhalten sich ähnlich wie Check Boxes, allerdings werden immer mehrere Radio Buttons in einer Gruppe zusammengefasst (javax.swing.ButtonGroup). Aufgrund dieser Gruppierung bilden die so zusammengefassten Buttons eine logische Einheit mit Einfach-Auswahl, d.h. es kann immer nur ein Button in der Gruppe selektiert sein. Klickt man mit der Maus auf einen anderen Button derselben Gruppe, dann wird der vorher selektierte Button de-selektiert, bevor der angeklickte Button ausgewählt wird. Was die Konstruktoren und sonstigen Methoden von Radio Buttons angeht, so sind sie nahezu identisch zu Check Boxes und normalen Buttons. Das Gruppen-Verhalten wird über ein Objekt der Klasse ButtonGroup implementiert. Hier ein kurzer Pseudo-Code, der die einzelnen Schritte verdeutlicht: Button erzeugen ... Button erzeugen

Grafik-Programmierung

Button Gruppe erzeugen group.add( button ); ... group.add( button );

Auch zum Thema Radio Buttons möchte ich Ihnen ein kurzes Beispiel zeigen. Zunächst zum Screenshot:

Abbildung 14.24: Radio Button Beispielprogramm (Anfangszustand)

as Beispiel hat zwei Besonderheiten: Zum einen ist der Button für Rot vorbelegt. es ist zwar nicht zwingend notwendig, ich empfehle aber, auch initial immer einen Button zu selektieren (mit der Methode setSelected()), weil bei Radio Buttons mer genau ein Button ausgewählt ist. Zweitens habe ich die Texte jeweils mit der ntsprechenden Farbe dargestellt, und zwar mit HTML-Tags, die Bestandteil des Butn-Textes sind (bis jetzt haben wir ja den Umgang mit Farben in Swing noch nicht nnen gelernt). Nun aber zum Programmcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17

// Datei RButtonDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class RButtonDemo { public static void main( String[] args ) { RButtonFrame frm = new RButtonFrame(); frm.setVisible( true ); frm.pack(); } } class RButtonFrame extends JFrame { public RButtonFrame() { super( "Radio Button Demo" );

Grafik-Programmierung

18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 }

addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); JRadioButton btnr = new JRadioButton( "" + "Rot" ); p.add( btnr ); btnr.setSelected( true ); JRadioButton btng = new JRadioButton( "" + "Grün" ); p.add( btng ); JRadioButton btnb = new JRadioButton( "" + "Blau" ); p.add( btnb ); ButtonGroup grp = new ButtonGroup(); grp.add( btnr ); grp.add( btng ); grp.add( btnb ); cp.add( p ); }

Die Vorbelegung des Buttons für Rot habe ich in Zeile 36 durchgeführt.

Grafik-Programmierung

Hinweis Wir werden später noch einmal auf Buttons zurückkommen, wenn ich Menüs bespreche. 4.4.6

Textfelder

r Text-Eingaben bietet Swing eine Reihe von Möglichkeiten. Ich möchte mit den nfachsten in Form von Objekten der Klasse javax.swing.JTextField sowie javax.swing.JPasswordField beginnen. Hinweis Auch einfache Textfelder können im Prinzip beliebig komplex werden, wenn sie mit einem Document Model verknüpft sind (ein Document Model kann .B. HTML-Text sein). Meist werden Textfelder aber wirklich nur für plain Text erwendet, deshalb möchte ich mich im Rahmen dieses Buches hierauf beschränken. wohl ein normales Textfeld als auch ein Password-Feld können nur einzeiligen Text fnehmen. Der wohl wichtigste Unterschied zwischen beiden Klassen ist, dass beim assword-Feld die eingegebenen Zeichen nicht im Klartext angezeigt werden. Stattssen wird für jedes eingegebene Zeichen z.B. ein Asterisk dargestellt (das anzuzeinde Zeichen ist einstellbar). onstruktoren für Textfelder und Password-Felder JTextField() JTextField( int columns ) JTextField( String text ) JTextField( String text, int columns ) JTextField( Document doc, String text, int columns ) JPasswordField() JPasswordField( int columns ) JPasswordField( String text ) JPasswordField( String text, int columns ) JPasswordField( Document doc, String text, int columns )

ie Sie sehen, sind die Konstruktoren der beiden Komponenten identisch, lediglich die Darstellung unterscheidet sich. Im folgenden Beispiel zeige ich die Verbindung n Textfeldern und Labels. Hier ein Screenshot:

Grafik-Programmierung

Abbildung 14.25: Textfeld Beispielprogramm (Anfangszustand)

Bei genauem Hinsehen stellen Sie fest, dass die Labels Mnemonics besitzen (auch ToolTips, nur sieht man die im Bild nicht). Ich werde darauf gleich zurückkommen, doch zunächst der Programmcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

// Datei TextFieldDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TextFieldDemo { public static void main( String[] args ) { TextfieldFrame frm = new TextfieldFrame(); frm.setVisible( true ); frm.pack(); } } class TextfieldFrame extends JFrame { public TextfieldFrame() { super( "Textfeld Demo" ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); JLabel lbl = new JLabel( "Login" ); lbl.setDisplayedMnemonic( KeyEvent.VK_L ); lbl.setToolTipText(

Grafik-Programmierung

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 }

"Login-Namen eingeben" ); p.add( lbl ); lbl.setVerticalAlignment( SwingConstants.CENTER ); JTextField tf = new JTextField( 10 ); p.add( tf ); lbl.setLabelFor( tf ); lbl = new JLabel( "Kennwort" ); lbl.setDisplayedMnemonic( KeyEvent.VK_K ); lbl.setToolTipText( "Kennwort eingeben" ); p.add( lbl ); JPasswordField pf = new JPasswordField( 10 ); p.add( pf ); lbl.setLabelFor( pf ); lbl.setVerticalAlignment( SwingConstants.CENTER ); cp.add( p ); }

Ich glaube, der größte Teil des Programms sollte keine Schwierigkeiten bereiten. In ile 43 jedoch kommt etwas Neues: 43

lbl.setLabelFor( tf );

rknüpfen von Labels mit anderen Komponenten

Mit der Methode setLabelFor() kann man ein Label einem beliebigen anderen GUIement vom Typ java.awt.Component logisch zuordnen. Hat man nun zusätzlich ein Mnemonic für das Label vergeben und gibt die Tastenkombination über die Tastatur ein, dann erhält das zugeordnete GUI-Element automatisch den Input-Fokus. Probieren Sie es aus. Initial hat das Textfeld für den Login-Namen den Input-Fokus, kennbar an der blinkenden Textmarke. Wenn Sie nun (ALT)-(K) (Mnemonic für das

Grafik-Programmierung

Kennwort-Label) eingeben, dann bekommt das Kennwort-Textfeld den Input-Fokus, weil wir Label und Textfeld miteinander verbunden haben. Einstellen des Ausgabezeichens bei Password-Feldern

Je nach Look-and-Feel wird ein anderes Zeichen für die Darstellung in Password-Feldern verwendet, meist ist es jedoch der Asterisk *. Mit Hilfe der Methode setEchoChar() kann man jedoch ein beliebiges Zeichen verwenden: void setEchoChar( char c )

Gibt man eine numerische 0 als Argument an, dann verhält sich das Password-Feld genauso wie ein normales Textfeld: pf.setEchoChar( ( char ) 0 );

Textfelder validieren

Eine häufig wiederkehrende Aufgabe stellt sich dem Programmierer bei Textfeldern in den Weg: Die Eingaben durch den Anwender müssen validiert werden, d.h. das Programm muss feststellen, ob der Anwender Unsinn eingegeben hat. Zu diesem Zweck benutzen wir ein ActionEvent, das beim Drücken auf die (¢)-Taste ausgelöst wird, wenn ein Textfeld den Input-Fokus besitzt. Wir wollen unser Beispielprogramm nun so erweitern, dass es sowohl den LoginNamen als auch das Kennwort überprüft. Als Regel für den Login-Namen soll gelten, dass er aus mindestens einem Buchstaben bestehen muss und nur Buchstaben bzw. den Bindestrich enthalten darf (hugo-hein wäre also erlaubt, -hein aber nicht). Zusätzlich darf der Login-Name zwei solcher Namenskomponenten enthalten, die durch einen Punkt getrennt sind (z.B. hugo.hein). Sowohl Groß- als auch Kleinbuchstaben sollen erlaubt sein. Hört sich kompliziert an, was? Natürlich werden wir diese Prüfung mit einem regulären Ausdruck und Pattern Matching elegant und einfach lösen. Die Regeln für das Kennwort machen wir einfacher, hier soll nur überprüft werden, ob die Eingabe mindestens sechs Zeichen enthält. Hat der Anwender ein Feld fehlerhaft ausgefüllt, dann löschen wir den Inhalt des Feldes und geben eine entsprechende Fehlermeldung im Konsolen-Fenster aus.

Grafik-Programmierung

Damit eine Prüfung der Eingaben erfolgen kann, müssen wir für beide Felder einen tion Listener registrieren, der die Checks durchführt. Da sich das Programm nauso präsentiert wie das vorangegangene Beispiel (zusätzlich gibt es Meldungen f der Konsole aus), verzichte ich hier auf einen Screenshot und kommen gleich zum ogrammcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

// Datei TextFieldDemo1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.regex.*; public class TextFieldDemo1 { public static void main( String[] args ) { TextfieldFrame1 frm = new TextfieldFrame1(); frm.setVisible( true ); frm.pack(); } } class TextfieldFrame1 extends JFrame implements ActionListener { public TextfieldFrame1() { super( "Textfeld Demo" ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); JLabel lbl = new JLabel( "Login" ); lbl.setDisplayedMnemonic( KeyEvent.VK_L );

Grafik-Programmierung

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82

lbl.setToolTipText( "Login-Namen eingeben" ); p.add( lbl ); lbl.setVerticalAlignment( SwingConstants.CENTER ); JTextField tf = new JTextField( 10 ); p.add( tf ); lbl.setLabelFor( tf ); tf.addActionListener( this ); tf.setActionCommand( "login" ); lbl = new JLabel( "Kennwort" ); lbl.setDisplayedMnemonic( KeyEvent.VK_K ); lbl.setToolTipText( "Kennwort eingeben" ); p.add( lbl ); JPasswordField pf = new JPasswordField( 10 ); p.add( pf ); lbl.setLabelFor( pf ); lbl.setVerticalAlignment( SwingConstants.CENTER ); pf.addActionListener( this ); pf.setActionCommand( "pwd" ); cp.add( p ); } public void actionPerformed( ActionEvent ev ) { String cmd = ev.getActionCommand(); if ( cmd.equals( "pwd" ) ) { JPasswordField pf = ( JPasswordField ) ev.getSource(); String s = new String( pf.getPassword() ); if ( s.length() < 6 ) {

Grafik-Programmierung

83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 }

System.err.println( "Kennwort zu kurz" ); pf.setText( "" ); } return; } // command ist login String pat = "^[a-z]+(?:-[a-z]+)*" + "(?:\\.[a-z]+(?:-[a-z]+)*)*$"; Pattern p = Pattern.compile( pat, Pattern.CASE_INSENSITIVE ); JTextField tf = ( JTextField ) ev.getSource(); String s = tf.getText(); Matcher m = p.matcher( s ); if ( ! m.matches() ) { System.err.println( "Login ungueltig" ); tf.setText( "" ); } }

Damit das Programm die Eingaben überprüfen kann, muss ein Action Listener impleentiert werden, hierzu habe ich eine entsprechende implements-Klausel in der Deklaration der Klasse TextFieldFrame1 hinzugefügt. Der Event Handler ist in der Methode actionPerformed() implementiert. Zur Registrierung des Listeners rufe ich r jedes der beiden Eingabe-Felder die Methode addActionListener() auf und kennichne mit setActionCommand() das jeweilige Feld: 50 51 ... 67 68

tf.addActionListener( this ); tf.setActionCommand( "login" ); pf.addActionListener( this ); pf.setActionCommand( "pwd" );

Grafik-Programmierung

Im Event Handler muss zunächst mit getActionCommand() festgestellt werden, welches Eingabe-Feld das Event ausgelöst hatte, denn zum einen haben die beiden Felder einen unterschiedlichen Typ (gerechterweise muss ich hinzufügen, dass sie miteinander kompatibel sind, weil JPasswordField von JTextField abgeleitet ist), außerdem ist die Semantik und damit die Art und Weise der Überprüfung verschieden. Das Kennwort wird wie gesagt nur auf eine Minimal-Länge hin überprüft, während der Check für den Login-Namen schon etwas komplizierter ist, was man an dem regulären Ausdruck ^[a-z]+(?:-[a-z]+)*(?:\\.[a-z]+(?:-[a-z]+)*)*$

erkennen kann. Zu Deutsch bedeutet das Pattern: Der Login-Name muss mit einem Buchstaben beginnen. Er darf entweder nur aus Buchstaben oder aus Wortbestandteilen, die durch einen Bindestrich getrennt sind, bestehen. Diese beiden erlaubten Varianten wiederum dürfen öfter vorkommen, jeweils durch einen Punkt getrennt. Zu guter Letzt wird das Pattern Matching case-insensitive gemacht, indem das Argument Pattern.CASE_INSENSITIVE beim Aufruf der compile()-Methode angegeben wird. Einen gravierenden Nachteil besitzt das Programm: Drückt man nicht die (¢)-Taste, sondern die (ÿ_)-Taste, um den Input-Fokus von einem Feld auf das andere zu transferieren, dann unterbleibt die Feld-Prüfung, weil kein ActionEvent ausgelöst wird, sondern ein FocusEvent. Probieren Sie es aus, Sie können auf diese Weise jeden erdenklichen Unsinn eintippen, ohne dass unser Programm Ihnen auf die Schliche kommt. Wir könnten natürlich zusätzlich einen Focus Listener implementieren, ich möchte Ihnen aber eine gänzlich andere Methode zeigen, die von Java seit dem JDK 1.4 zur Verfügung gestellt wird. Validierung mit JFormattedTextField

Mit dem JDK 1.4 wurde ein neues Textfeld entwickelt, das unter anderem die Validierung von Eingaben wesentlich vereinfacht. Es ist aus JTextField abgeleitet und heißt javax.swing.JFormattedTextField. Im Gegensatz zu normalen Textfeldern, die nur Strings ohne besondere Bedeutung (der Text besitzt keine Semantik) speichern können, ist JFormattedTextField in der Lage, beliebige Objekte mit individueller Semantik aufzunehmen. So hat eine Zahl zum Beispiel ein anderes Format als ein Datum und muss entsprechend unterschiedlich validiert werden. Mit JFormattedTextField werden Fokus Events verarbeitet, d.h. dass die Validierung automatisch dann erfolgt, wenn das Eingabe-Feld den Input-Fokus verliert.

Grafik-Programmierung

Ich möchte Sie hier nicht mit den wirklich komplexen internen Abläufen verwirren, die noch dazu nicht besonders gut dokumentiert sind. Allerdings benötigen wir dench einige Informationen, um unsere eigene Semantik überprüfen zu können. Da wäre zum Beispiel der Begriff Formatter, den es zu klären gibt. Die Klasse JFormattedTextField verwendet einen Formatter, um den String-Wert des Textfeldes in ein bjekt umzuwandeln und umgekehrt. Die hierfür benötigten Umwandlungsethoden heißen stringToValue() (Umwandlung des String-Werts in ein Objekt) d valueToString() (Umwandlung des internen Objekts in einen String-Wert). ese Methoden werden gleichzeitig auch für die Überprüfung der Semantik nutzt. r die Formatierung und Validierung von Zahlen und Datums-Werten bietet JFormattedTextField standardmäßig die Klasse NumberFormatter bzw. DateFormatter an. ide Klassen sind definiert im Package javax.swing.text und werden über Umwege s der Klasse AbstractFormatter abgeleitet, die in JFormattedTextField als innere asse implementiert ist. JFormattedTextField bietet einen Konstruktor an, bei dem man den zu verwenden-

n Formatter angeben kann: JFormattedTextField( JFormattedTextField.AbstractFormatter formatter

enn wir nun mit Hilfe von regulären Ausdrücken den Login-Namen mit seiner genen Semantik prüfen wollen, müssen wir hierfür einen neuen Formatter impleentieren, der aus AbstractFormatter abgeleitet ist und die Methode stringToValue() überschreibt. Der passende Name für diese Klasse ist RegexFormatter. Gott sei Dank ist es nicht nötig, das Rad neu zu erfinden, das haben bereits die Entwickler bei SUN für uns erledigt. In dieser Klasse ist die Methode stringToValue() so implemenrt, dass ein Pattern Matching mit dem angegebenen regulären Ausdruck durchgehrt wird. Den Quell-Code für den RegexFormatter finden Sie auf der CD-ROM in der Datei gexFormatter.java, die Klasse ist leider noch nicht in der Standard-Distribution des K enthalten.

Grafik-Programmierung

Der Formatter bietet zusätzlich zum Default-Konstruktor zwei weitere Konstruktoren an, mit denen man den regulären Ausdruck für die Semantik-Prüfung entweder als String oder als Objekt übergeben kann: RegexFormatter( String patternString ) RegexFormatter( Pattern pattern )

Letzteres bietet sich in unserem Fall an, weil die Eingabe case-insensitive überprüft werden soll. Wir müssen den RegexFormatter also wie folgt bedienen: ... import java.util.regex.*; ... String patStr = "^[a-z]+(?:-[a-z]+)*(?:\\.[a-z]+(?:-[a-z]+)*)*$"; Pattern pat = Pattern.compile( patStr, Pattern.CASE_INSENSITIVE );

Das Textfeld wird dann wie folgt erzeugt: JFormattedTextField tf = new JFormattedTextField( new RegexFormatter( pat ) );

So, ich glaube, wir haben nun alle Grundlagen, um uns einem Beispiel widmen zu können. Als Grundlage dient das vorangegangene Programm: 01 02 03 04 05 06 07 08 09 10

// Datei TextFieldDemo2.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.regex.*; public class TextFieldDemo2 { public static void main( String[] args ) {

Grafik-Programmierung

11 TextfieldFrame2 frm = new TextfieldFrame2(); 12 frm.setVisible( true ); 13 frm.pack(); 14 } 15 } 16 17 class TextfieldFrame2 extends JFrame 18 implements ActionListener 19 { 20 public TextfieldFrame2() { 21 super( "Textfeld Demo" ); 22 23 addWindowListener( 24 new WindowAdapter() { 25 public void windowClosing( 26 WindowEvent ev 27 ) { 28 System.exit( 0 ); 29 } 30 } 31 ); 32 33 Container cp = getContentPane(); 34 JPanel p = new JPanel(); 35 36 JLabel lbl = new JLabel( "Login" ); 37 lbl.setDisplayedMnemonic( KeyEvent.VK_L ); 38 lbl.setToolTipText( 39 "Login-Namen eingeben" 40 ); 41 p.add( lbl ); 42 lbl.setVerticalAlignment( 43 SwingConstants.CENTER 44 ); 45 46 String patStr = 47 "^[a-z]+(?:-[a-z]+)*" + 48 "(?:\\.[a-z]+(?:-[a-z]+)*)*$"; 49 Pattern pat = Pattern.compile( 50 patStr, 51 Pattern.CASE_INSENSITIVE 52 ); 53 JFormattedTextField tf = 54 new JFormattedTextField(

Grafik-Programmierung

55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 }

new RegexFormatter( pat ) ); tf.setColumns( 10 ); p.add( tf ); lbl.setLabelFor( tf ); lbl = new JLabel( "Kennwort" ); lbl.setDisplayedMnemonic( KeyEvent.VK_K ); lbl.setToolTipText( "Kennwort eingeben" ); p.add( lbl ); JPasswordField pf = new JPasswordField( 10 ); p.add( pf ); lbl.setLabelFor( pf ); lbl.setVerticalAlignment( SwingConstants.CENTER ); pf.addActionListener( this ); cp.add( p ); } public void actionPerformed( ActionEvent ev ) { JPasswordField pf = ( JPasswordField ) ev.getSource(); String s = new String( pf.getPassword() ); if ( s.length() < 6 ) { System.err.println( "Kennwort zu kurz" ); pf.setText( "" ); } }

Wenn Sie ein bisschen mit dem Textfeld herumspielen, werden Sie feststellen, dass es sich bei Falsch-Eingaben leicht anders verhält als JTextField. Wurde bereits einmal eine gültige Eingabe gemacht und anschließend das Feld noch einmal editiert, so dass sich ein fehlerhafter Login-Name ergibt, dann ersetzt der eingebaute Listener den String durch den vorher gültigen.

Grafik-Programmierung

4.4.7

Mehrzeilige Textfelder

e bisherigen Textfelder haben die Einschränkung, dass keine mehrzeiligen Texte ngegeben werden können. Diesem Manko wollen wir nun mit der Swing-Klasse javax.swing.JTextArea abhelfen. Objekte dieser Klasse können mit einer bestimmten nzahl von Spalten und Zeilen erzeugt werden. m besten, wir sehen uns gleich die Konstruktoren an: Konstruktoren von javax.swing.JTextArea JTextArea()

Default-Konstruktor. Vorsicht: Wenn Sie ein Objekt mit diesem Konstruktor erzeugen, müssen Sie anschließend die Anzahl Spalten und Zeilen auf einen Wert größer 0 setzen, da der Default-Wert für beide Attribute 0 ist, das Textfeld hätte dann eine Ausdehnung von 0 und wäre sozusagen unsichtbar. JTextArea( int rows, int cols )

Konstruktor, bei dem man die Anzahl der Spalten (Breite) und der Zeilen (Höhe) angeben kann. JTextArea( String text )

Konstruktor mit Vorbelegung des Textes. JTextArea( String text, int rows, int cols )

Kombination aus den vorherigen Konstruktoren. JTextArea( Document doc )

Konstruktor, bei dem man das Dokument-Modell angeben kann. JTextArea( Document d, String t, int r, int c )

Luxus-Variante, bei der alle möglichen Initial-Werte angegeben werden können. Hinweis

Alle Swing-basierten Text-Komponenten benutzen das Interface

javax.swing.text.Document für die Verwaltung der Text-Daten. Damit können

erschiedene Dokument-Modelle unterstützt werden, angefangen bei einfachem Text bis hin zu strukturierten Dokumenten wie zum Beispiel HTML oder XML. Zudem ist es möglich, Document Listeners zu registrieren, um auf jegliche Ändeungen im Text zu reagieren. Diese weit reichenden Möglichkeiten übersteigen edoch den Rahmen dieses Buches. Dann wollen wir uns doch gleich ein Beispiel ansehen:

Grafik-Programmierung

01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

// Datei TextAreaDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class TextAreaDemo { public static void main( String[] args ) { TAFrame frm = new TAFrame( "TextArea Demo" ); frm.setVisible( true ); frm.pack(); } } class TAFrame extends JFrame { public TAFrame( String title ) { super( title ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); JTextArea a = new JTextArea(); a.setColumns( 10 ); a.setRows( 3 ); getContentPane().add( a ); } }

Das Programm präsentiert sich wie folgt:

Grafik-Programmierung

Abbildung 14.27: TextArea Beispielprogramm (nach Text-Eingabe)

Das Ergebnis sieht nicht besonders zufrieden stellend aus, denke ich. Nur ein Teil des xtes passt in das Fenster. Wir können natürlich das Fenster vergrößern, aber mit eser Methode stoßen wir spätestens dann an eine Grenze, wenn der Bildschirmrand reicht ist. Ich glaube, jedem ist klar, was fehlt: Scrollbars. Im nächsten Abschnitt werden wir dies Problem in Form einer JScrollPane lösen. Vorher jedoch möchte ich Ihnen zein, wie man einen Listener implementiert, der Events einer Text-Komponente verbeiten kann. Sehen wir uns als Erstes das Interface für den Listener an: Document Listener

Das Interface javax.swing.event.DocumentListener deklariert drei Methoden, um auf nderungen in Text-Komponenten zu reagieren (alle Methoden sind vom Typ void nd besitzen ein Argument vom Typ javax.swing.event.DocumentEvent): Listener-Methode

Beschreibung

changedUpdate

Wird aufgerufen, wenn eine Änderung in der Text-Komponente stattgefunden hat.

insertUpdate

Wird aufgerufen, wenn Text eingefügt wurde.

removeUpdate

Wird aufgerufen, wenn ein Lösch-Vorgang in der Text-Komponente stattgefunden hat.

elle 14.4: DocumentListener-Methoden

s Nächstes wollen wir die Klasse für das DocumentEvent näher betrachten: Document Events

der Klasse javax.swing.event.DocumentEvent sind alle nötigen Methoden definiert, m Informationen über das Ereignis selbst sowie das Document-Objekt zu erhalten (ich be in der Tabelle nur die wichtigsten Methoden aufgenommen):

Grafik-Programmierung

Event-Methode

Beschreibung

Document getDocument()

Liefert das Document-Objekt, das von der Text-Komponente intern benutzt wird, um die Daten zu speichern.

int getLength()

Liefert die Länge der Änderung zurück.

int getOffset()

Liefert den Offset im Document zurück, ab dem die Änderung stattgefunden hat.

Tabelle 14.5: Wichtige Methoden der Klasse DocumentEvent

Achtung Obwohl es über getDocument() möglich wäre, im Listener die Daten der Text-Komponente zu ändern, möchte ich davon abraten. Der Listener soll ja auf Änderungen reagieren und diese nicht selbst verursachen. So, ich glaube, jetzt ist es an der Zeit für ein Beispiel. Wir schreiben ein Programm, das als einziges GUI-Element eine JTextArea enthält, für die wir einen DocumentListener implementieren. Dieser gibt seine Informationen in der Konsole aus. Hier der fertige Programmcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22

// Datei DocListenerDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; public class DocListenerDemo { public static void main( String[] args ) { DLFrame frm = new DLFrame( "TextArea Demo" ); frm.setVisible( true ); frm.pack(); } } class DLFrame extends JFrame { public DLFrame( String title ) { super( title );

Grafik-Programmierung

23 addWindowListener( 24 new WindowAdapter() { 25 public void windowClosing( 26 WindowEvent ev 27 ) { 28 System.exit( 0 ); 29 } 30 } 31 ); 32 33 JTextArea a = new JTextArea(); 34 a.setColumns( 20 ); 35 a.setRows( 6 ); 36 getContentPane().add( a ); 37 a.getDocument().addDocumentListener( 38 new DListener() 39 ); 40 } 41 } 42 43 class DListener implements DocumentListener { 44 public void insertUpdate( DocumentEvent ev ) { 45 doLog( ev, "Insert" ); 46 } 47 public void removeUpdate( DocumentEvent ev ) { 48 doLog( ev, "Delete" ); 49 } 50 public void changedUpdate( DocumentEvent ev ) { 51 doLog( ev, "Change" ); 52 } 53 54 public void doLog( 55 DocumentEvent ev, 56 String action 57 ) { 58 Document doc = 59 ( Document ) ev.getDocument(); 60 System.out.println( 61 action + ": " + ev.getLength() + 62 " Zeichen" 63 ); 64 System.out.println( 65 "Offset: " + ev.getOffset() 66 );

Grafik-Programmierung

67 68 69 70 71 72 }

System.out.println( "Text Laenge: " + doc.getLength() ); System.out.println(); }

In Zeile 37 registrieren wir den Document Listener für die Text-Komponente. Wichtig ist, dass der Listener für das Document-Objekt und nicht für das JTextArea-Objekt registriert wird. Deshalb müssen wir uns mit der Methode getDocument() das Objekt erst von der Text-Komponente besorgen. Im Listener werden die Event-Informationen mit doLog() auf die Konsole ausgegeben. Die erste Ausgabe ist die Anzahl der Zeichen, die eingefügt bzw. gelöscht werden. Dann folgt die Information, wo die Aktion stattgefunden hat. Zum Schluss wird die Gesamtlänge des Textes in der Text-Komponente nach der Aktion ausgegeben (die Methode getLength() wird hier über die Variable doc aufgerufen, nicht über die Event-Variable ev!). Und nun ein paar Screenshots des Programms:

Abbildung 14.28: Document Listener-Beispiel nach Eingabe

Grafik-Programmierung

Abbildung 14.29: Document Listener-Beispiel (Eingabe selektiert)

Abbildung 14.30: Document Listener-Beispiel (Selektion ersetzt)

Grafik-Programmierung

Bemerkenswert ist, dass kein Change-Event ausgelöst wird, wenn wir die Text-Selektion ersetzen, sondern eine Folge von zwei Events, zuerst ein Remove-Event, das sich auf fünf Zeichen bezieht, anschließend folgt ein Insert-Event. Das liegt daran, dass einfache Document-Objekte, die nicht strukturiert sind (HTML-Dokumente sind strukturiert, da sie Tags enthalten, plain-text-Dokumente jedoch sind nicht strukturiert), keine Change-Events auslösen.

Abbildung 14.31: Document Listener-Beispiel (Zeichen gelöscht)

Obwohl ich das Zeichen »h« durch die (æ___)-Taste gelöscht habe, wird als Offset nicht etwa 1 ausgegeben, sondern 0. Es scheint wohl so, dass diese Taste zunächst die Text-Eingabe-Marke um ein Zeichen nach links schiebt und anschließend das rechts von der Marke stehende Zeichen löscht. Richtig! Genau das ist der Fall, es handelt sich also nicht um einen Bug. 14.4.8

Scroll Panes

Als wir im letzten Abschnitt mehrzeilig Textfelder erzeugt haben, fehlte ein wesentliches Merkmal, das Sie unter dem Namen Scrollbar mit Sicherheit kennen. Warum gibt es keine Textfelder mit Scrollbars?

Grafik-Programmierung

Nun, die Antwort liegt in der universellen Verwendung von Scroll Panes. Es gibt ja nicht nur Textfelder, die Scrollbars benutzen. Wir werden im weiteren Verlauf noch nige andere Komponenten kennen lernen, bei denen Scrollbars vonnöten sind. Wenn wir nun für jede GUI-Komponente eine individuelle Scrollbar-Lösung implementiert hätten, müssten wir das Rad immer wieder neu erfinden. a bietet mit der Klasse javax.swing.JScrollPane eine elegante Methode, jeder liebigen GUI-Komponente Scrollbars hinzuzufügen. Ich möchte Ihnen dies am ispiel eines mehrzeiligen Textfelds zeigen: ... JTextArea ta = new JTextArea( 3, 10 ); JScrollPane sp = new JScrollPane( ta ); // es geht auch so: JTextArea ta = new JTextArea( 3, 10 ); JScrollPane sp = new JScrollPane(); sp.setViewportView( ta );

enn wir den Code in unser Beispielprogramm für mehrzeilige Textfelder einbauen (auf der CD-ROM finden Sie das neue Programm in der Datei TASPDemo.java), nn sieht das Fenster zwar zu Beginn genauso aus, gibt man jedoch mehr Text ein, s horizontal oder vertikal zur Verfügung steht, dann erscheinen wie durch Zaubernd Scrollbars, die uns erlauben, an jede beliebige Stelle des Textfelds zu navigieren, ne das Fenster vergrößern zu müssen:

Abbildung 14.32: Text Area Beispielprogramm mit Scroll Pane

Aufbau einer JScrollPane

Links ist die GUI-Komponente dargestellt (dabei muss es sich um eine Swing-Komnente handeln), von welcher ein Ausschnitt (View) in der Scroll Pane dargestellt wird. Dieser Ausschnitt ist in ein Objekt der Klasse javax.swing.JViewport eingebett.

Grafik-Programmierung

      

 





   

   

    

Abbildung 14.33: Aufbau einer JScrollPane mit einem Viewport

Mit je einem Objekt der Klasse javax.swing.JScrollBar kann man den Ausschnitt in der GUI-Komponente horizontal und vertikal hin- und herbewegen. Über dem Viewport sowie links davon kann zusätzlich ein Objekt der Klasse JViewport für die Spalten- bzw. Zeilen-Überschrift eingeblendet werden. Konstruktoren von javax.swing.JScrollPane  JScrollPane()

Default-Konstruktor. Es werden nur dann Scrollbars eingeblendet, wenn die GUIKomponente größer ist als der Viewport, in welchem der Ausschnitt angezeigt wird. Um einen Viewport für die GUI-Komponente zu erzeugen, muss die Methode setViewportView() aufgerufen werden.  JScrollPane( Component view )

Wie Default-Konstruktor, jedoch wird die anzuzeigende GUI-Komponente bereits im Konstruktor angegeben, der Aufruf von setViewportView() erübrigt sich.  JScrollPane( int vsbPolicy, int hsbPolicy )

Wie Default-Konstruktor, jedoch kann für beide Scrollbars die Policy angegeben werden. Die möglichen Werte sind in der folgenden Tabelle dargestellt (Alle Konstanten sind in javax.swing.JScrollPane definiert):

Grafik-Programmierung

Konstante

Bedeutung

VERTICAL_SCROLLBAR_AS_NEEDED

Die vertikale Scrollbar wird nur bei Bedarf eingeblendet, wenn die GUI-Komponente größer als der Viewport ist.

VERTICAL_SCROLLBAR_NEVER

Die vertikale Scrollbar wird nie eingeblendet.

VERTICAL_SCROLLBAR_ALWAYS

Die vertikale Scrollbar wird immer eingeblendet, auch wenn sie nicht benötigt wird.

HORIZONTAL_SCROLLBAR_AS_NEEDED

Die horizontale Scrollbar wird nur bei Bedarf eingeblendet, wenn die GUI-Komponente größer als der Viewport ist.

HORIZONTAL_SCROLLBAR_NEVER

Die horizontale Scrollbar wird nie eingeblendet.

HORIZONTAL_SCROLLBAR_ALWAYS

Die horizontale Scrollbar wird immer eingeblendet, auch wenn sie nicht benötigt wird.

elle 14.6: Konstanten für die Scrollbar-Policy von JScrollPane

JScrollPane( Component v, int vsb, int hsb )

Dieser Konstruktor ist die Verknüpfung aus den vorangegangenen. chtige Methoden von JScrollPane setColumnHeaderView( Component header )

Diese Methode kann verwendet werden, um der Scroll Pane eine Spalten-Überschrift hinzuzufügen. header kann in diesem Fall zum Beispiel ein Objekt vom Typ JLabel sein. setRowHeaderView( Component header )

Wie vorher, jedoch wird eine Zeilen-Überschrift auf der linken Seite hinzugefügt. setViewportView( Component view )

Mit dieser Methode kann eine neue GUI-Komponente in den Viewport geladen werden. Sie wird zum Beispiel benötigt, wenn man den Default-Konstruktor benutzt. setViewportBorder( Border border )

Mit dieser Methode kann man den Viewport mit einem Rahmen versehen. Wie man Rahmen (borders) verwendet, lernen wir weiter unten). 4.4.9

Lists

der von Ihnen kennt vermutlich SELECT-Listen von HTML-Formularen. Eine hnliche GUI-Komponente wird auch von Swing mit der Klasse javax.swing.JList r Verfügung gestellt, allerdings mit wesentlich komplexeren Möglichkeiten. So

Grafik-Programmierung

kann man mit JList beliebige Objekte verwalten, nicht nur Strings. Wie bereits von mehrzeiligen Textfeldern her bekannt, verwendet man auch bei JList-Objekten eine Scroll Pane, um bei längeren Listen mit Hilfe von Scrollbars nach oben oder unten zu navigieren. Sehen wir uns als Erstes die Konstruktoren von JList an: Konstruktoren von javax.swing.JList  JList()

Default-Konstruktor. Will man nach dem Instanzieren über diesen Konstruktor Daten in das List-Objekt aufnehmen, muss die Methode setListData() verwendet werden.  JList( Vector data )

Konstruktor, bei dem ein Vektor als Daten-Array übergeben wird. Die Liste ist immutable und kann anschließend nicht verändert, sondern nur lesend benutzt werden.  JList( Object[] data )

Wie vorher, nur wird bei diesem Konstruktor ein Array aus Objekten übergeben.  JList( ListModel model )

Diesen Konstruktor werden wir erst dann verstehen, wenn wir wissen, was ein ListModel ist. Bis dahin rufen wir den Konstruktor wie folgt auf: JList( new DefaultListModel() )

Bevor wir ans Eingemachte gehen, möchte ich Ihnen an einem Beispiel zeigen, wie man Listen vom Prinzip her benutzt. Nehmen wir dazu eine simple Liste aus Strings:

Abbildung 14.34: JList Beispielprogramm mit einer einfachen String-Liste

Grafik-Programmierung

Das Programmfenster enthält eine Scroll Pane, die als Viewport ein JList-Objekt hat. ie man anhand der Scrollbar sieht, enthält die Liste mehr Elemente, als im Viewport chtbar sind. Einige davon sind selektiert, was man an der andersfarbigen Darstellung erkennen kann. Sehen wir uns nun den Programmcode an: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38

// Datei ListDemo.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ListDemo { public static void main( String[] args ) { ListFrame frm = new ListFrame( "List Demo" ); frm.setVisible( true ); frm.pack(); } } class ListFrame extends JFrame { public ListFrame( String title ) { super( title ); addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent ev ) { System.exit( 0 ); } } ); Container cp = getContentPane(); JPanel p = new JPanel(); String[] s = "Auswahl "Auswahl "Auswahl "Auswahl

{ 1", 3", 5", 7",

"Auswahl "Auswahl "Auswahl "Auswahl

2", 4", 6", 8",

Grafik-Programmierung

39 40 41 42 43 44 45 46 47 48 49 50 51 52 }

"Auswahl "Auswahl "Auswahl "Auswahl "Auswahl "Auswahl

9", "Auswahl 10", 11", "Auswahl 12", 13", "Auswahl 14", 15", "Auswahl 16", 17", "Auswahl 18", 19", "Auswahl 20",

}; JList lst = new JList( s ); JScrollPane sp = new JScrollPane( lst ); p.add( sp ); cp.add( p ); }

Es ist also nicht allzu viel Programmier-Aufwand nötig, um dieses Beispiel umzusetzen. Zunächst definiert man eine Liste aus Strings (auch Vektoren oder andere Objekt-Arrays sind möglich) und übergibt das Array an den Konstruktor von JList. Fertig. Man muss nur noch eine Scroll Pane hinzufügen. Daten-Modelle

Solange man nur readonly-Listen verwendet (der Inhalt der Liste ändert sich nicht), braucht man bis auf wenige Methoden der Klasse JList keine weiteren HintergrundInformationen mehr und kann sofort loslegen. Falls dies bei Ihnen zutrifft, können Sie den Rest dieses Abschnitts überspringen und bei den Klassen-Methoden weiter lesen. Etwas komplizierter wird die Sache, wenn man beabsichtigt, den Inhalt einer Liste zu verändern. Dann nämlich muss man sich mit dem Daten-Modell auseinander setzen, das im Hintergrund für die Speicherung der Daten benutzt wird. Gott sei Dank kann man auch in diesem Fall auf ein vorgefertigtes Modell zurückgreifen, das von Java zur Verfügung gestellt wird. Damit wir begreifen, wofür ein Daten-Modell gut ist, müssen wir uns erst klar darüber sein, welche Aktionen bei Änderungen in der Liste notwendig sind. Zunächst ist festzuhalten, dass zwei getrennte Dinge zusammenspielen: Auf der einen Seite haben wir die interne Verwaltung der Daten, die in einer separaten Klasse erfolgt, auf der anderen Seite müssen diese Daten in der grafischen Oberfläche im GUI-Element JList dargestellt werden.

Grafik-Programmierung

le Interaktionen mit dem Anwender erfolgen also über Methoden der Klasse JList, so m Beispiel das Selektieren von List-Elementen. Änderungen jedoch erfolgen zunächst der Klasse, die für die Daten-Verwaltung verantwortlich ist. Anschließend muss die GUI-Komponente jedoch über die Änderungen informiert werden, damit sie die nzeige entsprechend der Änderung anpasst. Es müssen also Listener implementiert werden, die für die Synchronisation zwischen Daten-Haltung und Daten-Anzeige sorgen. Ich möchte nicht zu tief in die Materie einsteigen, doch zumindest das Defaultaten-Modell will ich Ihnen nicht verschweigen (Sie können darauf aufbauend eigene Daten-Modelle entwickeln). r die Kommunikation zwischen Daten-Modell und JList-Objekten stellt Java das terface javax.swing.ListModel zur Verfügung, das von der abstrakten Klasse javax.swing.AbstractListModel implementiert wird. Daraus abgeleitet steht Ihnen die konkrete Klasse javax.swing.DefaultListModel zur Verfügung, das für viele Anwendungsfälle ausreichend ist und Ihnen viel Programmier-Arbeit erspart. Falls es dennoch nicht genügt, kann man eine eigene Kind-Klasse von AbstractListModel implementieren. Das Interface ListModel deklariert folgende Methoden, die in einer konkreten Klasse plementiert sein müssen: Object getElementAt( int index )

Liefert ein List-Element mit dem angegebenen Index zurück int getSize()

Liefert die Anzahl der List-Elemente zurück void addListDataListener( ListDataListener l )

Hinzufügen eines Listeners für Daten-Änderungen void removeListDataListener(ListDataListener l)

Entfernen eines Listeners e beiden ersten Methoden sind leicht zu verstehen. Bei den anderen jedoch müssen r ein bisschen tiefer in die Thematik einsteigen. Das Interface javax.swing. event.ListDataListener wird benutzt, um auf Events der Klasse javax.swing. event.ListDataEvent zu reagieren, und deklariert folgende Methoden: // Die folgende Methode wird aufgerufen, wenn eine // Änderung in der Datenhaltung erfolgt ist.

Grafik-Programmierung

void contentsChanged( ListDataEvent ev ) // intervalAdded() wird aufgerufen, wenn List-Elemente // innerhalb eines Bereichs in der Datenhaltung // eingefügt wurden. Der Bereich wird durch index0 // und index1 festgelegt (siehe Beschreibung von // ListDataEvent). Beide Index-Nummern sind Bestandteil // des Bereichs. void intervalAdded( ListDataEvent ev ) // wie intervalAdded(), wird jedoch nach dem Entfernen // eines Bereichs aufgerufen. void intervalRemoved( ListDataEvent ev )

Das Event-Objekt, welches den Listener-Methoden übergeben wird, besitzt hauptsächlich die beiden Methoden: int getIndex0() int getIndex1()

Die Methoden können verwendet werden, um den Index für Beginn und Ende eines Bereichs der Liste zu erhalten, der von der Änderung in der Datenhaltung betroffen ist. Kommen wir nun zur abstrakten Klasse AbstractListModel. Diese implementiert folgende Methoden: // Methoden für das Hinzufügen bzw. Entfernen eines // Daten-Listener. void addListDataListener( ListDataListener l ) void removeListDataListener( ListDataListener l ) // alle registrierten Daten-Listeners lesen ListDataListener[] getListDataListeners() // alle registrierten Listener beliebigen Typs // lesen. EventListener[] getListeners( Class listenerType ) // Auslösen eines Events bei einer Änderung protected void fireContentsChanged( Object source, int index0, int index1

Grafik-Programmierung

// Auslösen eines Events nach dem Hinzufügen von // Elementen protected void fireIntervalAdded( Object source, int index0, int index1

// Auslösen eines Events nach dem Löschen von // Elementen protected void fireIntervalRemoved( Object source, int index0, int index1

vor wir anhand der Klasse DefaultListModel die Benutzung von veränderbaren Lisn demonstrieren, wollen wir uns den Standard-Methoden für readonly-Zugriffe wenden. ichtige Methoden von javax.swing.JList int getSelectedIndex()

Diese Methode liefert den Index des ersten selektierten List-Elements zurück. Wenn kein Element ausgewählt ist, liefert sie den Wert -1 zurück. int[] getSelectedIndices()

Diese Methode liefert ein Array mit den Indizes aller selektierten List-Elemente in aufsteigender Reihenfolge zurück. Wenn kein Element selektiert ist, dann wird ein leeres Array zurückgegeben. Object getSelectedValue()

Diese Methode liefert das Objekt des ersten selektierten Elements zurück. Wenn kein Element ausgewählt ist, liefert die Methode null zurück. Object[] getSelectedValues()

Diese Methode liefert ein Array zurück, das alle selektierten List-Elemente als Objekte in aufsteigender Reihenfolge enthält. Wenn kein Element ausgewählt ist, dann wird ein leeres Array zurückgegeben. boolean isSelectedIndex( int index )

Die Methode liefert den Wert true zurück, wenn das Element mit dem angegebenen Index selektiert ist, andernfalls false.

Grafik-Programmierung

 boolean isSelectionEmpty()

Diese Methode liefert den Wert true zurück, wenn kein Element ausgewählt ist, andernfalls gibt sie false zurück.  ListModel getModel()

Liefert das Daten-Modell für die Verwaltung der List-Elemente zurück. Diese Methode wird zum Beispiel benötigt, wenn man die Gesamtzahl der in der Liste enthaltenen Elemente lesen möchte.  void clearSelection()

Diese Methode hebt alle Selektionen in der Liste auf.  void setListData( Object[] data )

Diese Methode initialisiert die Liste neu mit dem angegebenen Array. Vorsicht: Diese Methode sollte nur bei readonly-Listen verwendet werden. Falls Änderungen vorgesehen sind, muss stattdessen das Objekt des Daten-Modells befüllt werden.  void setListData( Vector data )

Wie vorher, jedoch mit einem Vektor als Argument. Vorsicht: Diese Methode sollte nur bei readonly-Listen verwendet werden. Falls Änderungen vorgesehen sind, muss stattdessen das Objekt des Daten-Modells befüllt werden.  void setSelectedIndex( int index )

Selektiert das Element mit dem angegebenen Index.  void setSelectedIndices( int[] indices )

Wie vorher, jedoch mit einem Array als Argument. Alle Elemente der Liste mit den angegebenen Indizes werden selektiert.  void setSelectionMode( int mode )

Mit dieser Methode kann man beeinflussen, ob Mehrfach-Selektionen möglich sind oder nicht. In der folgenden Tabelle sind die möglichen Werte für mode dargestellt (alle aufgeführten Konstanten sind im Swing-Interface javax.swing.ListSelectionModel definiert): Modus

Bedeutung

SINGLE_SELECTION

Nur ein einziges Element kann in der gesamten Liste zu einem Zeitpunkt selektiert sein.

Tabelle 14.7: Selektions-Modi von JList

Grafik-Programmierung

Modus

Bedeutung

MULTIPLE_INTERVAL_SELECTION

Es können beliebige Elemente der Liste selektiert werden.

SINGLE_INTERVAL_SELECTION

Es können zwar mehrere Elemente der Liste selektiert sein, sie müssen aber in einem zusammenhängenden Bereich liegen.

elle 14.7: Selektions-Modi von JList (Forts.)

void setVisibleRowCount( int rows )

Mit dieser Methode kann man die Anzahl von Zeilen der Liste angeben, die im Viewport der Scroll Pane zu sehen sind. Zwei nützliche Methoden, die man als Bestandteil von JList erwartet, fehlen leir: selectAll() und getSize(). Während man die erste Methode tatsächlich per Hand programmieren muss, kann man immerhin die Gesamtzahl der Elemente in r Liste über das Daten-Modell herausbekommen, und zwar mit getModel().getSize(). amit ist auch klar, wie man alle Elemente der Liste selektieren kann: Zuerst besorgt an sich die Anzahl aller Elemente. Dann füllt man ein Array mit fortlaufenden Nummern von 0 bis getModel().getSize() - 1. Dieses Array übergibt man anschlieend der Methode setSelectedIndices(). enn Sie das API von JList durchlesen, werden Sie viele weitere Methoden sehen, die aber weit über den Rahmen dieses Buches hinausgehen würden. Lassen Sie uns shalb nur ein relativ einfaches Beispiel für die Benutzung dieser GUI-Komponente rchspielen. Als Basis nehmen wir hierfür das vorangegangene Programm und weitern es um einige Buttons, mit denen man alle Elemente selektieren, alle Eleente deselektieren sowie die Policy-Einstellung für die Selektion vornehmen kann: 01 02 03 04 05 06 07 08 09 10

// Datei ListDemo1.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ListDemo1 { public static void main( String[] args ) { ListFrame1 frm = new ListFrame1( "List Demo" );

Grafik-Programmierung

11 12 frm.setVisible( true ); 13 frm.pack(); 14 } 15 } 16 17 class ListFrame1 extends JFrame { 18 public ListFrame1( String title ) { 19 super( title ); 20 21 addWindowListener( 22 new WindowAdapter() { 23 public void windowClosing( 24 WindowEvent ev 25 ) { 26 System.exit( 0 ); 27 } 28 } 29 ); 30 31 Container cp = getContentPane(); 32 JPanel p = new JPanel(); 33 34 String[] s = { 35 "Auswahl 1", "Auswahl 2", 36 "Auswahl 3", "Auswahl 4", 37 "Auswahl 5", "Auswahl 6", 38 "Auswahl 7", "Auswahl 8", 39 "Auswahl 9", "Auswahl 10", 40 "Auswahl 11", "Auswahl 12", 41 "Auswahl 13", "Auswahl 14", 42 "Auswahl 15", "Auswahl 16", 43 "Auswahl 17", "Auswahl 18", 44 "Auswahl 19", "Auswahl 20", 45 }; 46 47 final JList lst = new JList( s ); 48 JScrollPane sp = new JScrollPane( lst ); 49 p.add( sp ); 50 51 JButton btn = new JButton( 52 "Alles selektieren" 53 ); 54 btn.addActionListener( 55 new ActionListener() {

Grafik-Programmierung

56 57 58 59 60 61 62 63 64 65 6 67 8 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100

public void actionPerformed( ActionEvent ev ) { ListModel m = lst.getModel(); int size = m.getSize(); int[] is = new int[ size ]; for ( int i = 0; i < size; i++ ) { is[ i ] = i; } lst.setSelectedIndices( is ); } } ); p.add( btn ); btn = new JButton( "Alles deselektieren" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { lst.clearSelection(); } } ); p.add( btn ); btn = new JButton( "Einfach-Selektion" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { lst.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );

Grafik-Programmierung

101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 }

} } ); p.add( btn ); btn = new JButton( "Mehrfach-Selektion" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { lst.setSelectionMode( ListSelectionModel.MULTIPLE_INTERVAL_SELECTION ); } } ); p.add( btn ); btn = new JButton( "Einfache Bereichs-Selektion" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { lst.setSelectionMode( ListSelectionModel.SINGLE_INTERVAL_SELECTION ); } } ); p.add( btn ); cp.add( p ); }

Zunächst sollten Sie das Programm laufen lassen und ein bisschen mit den Buttons herumspielen, um zu sehen, was dabei passiert. Schauen Sie sich anschließend den

Grafik-Programmierung

ogrammcode an und lesen Sie auch die Beschreibung der einzelnen Methoden rch, die im Programm verwendet werden. Nun möchte ich Ihnen zum Abschluss noch ein Listen-Beispiel zeigen, bei dem wohl lesend als auch schreibend auf die Daten zugegriffen wird. Als Daten-Modell rwende ich DefaultListModel. Das Programm stellt jeweils links und rechts zwei ten dar (die linke Liste ist mit Daten gefüllt, die rechte ist zunächst leer). Zwischen iden Listen sind Buttons, mit denen man die Elemente jeweils von links nach rechts nd umgekehrt verschieben kann (Experten werden dieses Feature mit Sicherheit aus versen Anwendungen kennen). er ein Screenshot des Programms direkt nach dem Start:

Abbildung 14.35: JList Beispielprogramm mit DefaultListModel

Ich glaube, die Bedeutung der Buttons sollte klar sein, mit dem linken verschiebt man emente der linken Liste nach rechts, der rechte Button dient der umgekehrten Operation. Nun zum Programmcode: 01 02 03 04 05 06 07 08 09 10 11 12 13 14

// Datei ListDemo2.java import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ListDemo2 { public static void main( String[] args ) { ListFrame2 frm = new ListFrame2( "List Demo" ); frm.setVisible( true ); frm.pack(); }

Grafik-Programmierung

15 } 16 17 class ListFrame2 extends JFrame { 18 public ListFrame2( String title ) { 19 super( title ); 20 21 addWindowListener( 22 new WindowAdapter() { 23 public void windowClosing( 24 WindowEvent ev 25 ) { 26 System.exit( 0 ); 27 } 28 } 29 ); 30 31 Container cp = getContentPane(); 32 JPanel p = new JPanel(); 33 34 String[] s = { 35 "Auswahl 1", "Auswahl 2", "Auswahl 3", 36 "Auswahl 4", "Auswahl 5", "Auswahl 6", 37 "Auswahl 7", "Auswahl 8", "Auswahl 9", 38 "Auswahl 10", "Auswahl 11", 39 "Auswahl 12", "Auswahl 13", 40 "Auswahl 14", "Auswahl 15", 41 "Auswahl 16", "Auswahl 17", 42 "Auswahl 18", "Auswahl 19", 43 }; 44 45 final DefaultListModel m1 = 46 new DefaultListModel(); 47 for ( int i = 0; i < s.length; i++ ) { 48 m1.addElement( s[ i ] ); 49 } 50 final JList lst1 = new JList( m1 ); 51 JScrollPane sp1 = new JScrollPane( lst1 ); 52 53 final DefaultListModel m2 = 54 new DefaultListModel(); 55 final JList lst2 = new JList( m2 ); 56 JScrollPane sp2 = new JScrollPane( lst2 ); 57 58 p.add( sp1 ); 59

Grafik-Programmierung

60 61 62 63 64 65 66 67 68 69 70 71 72 3 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 3 94 95 96 97 98 99 100 101 102 103 }

JButton btn = new JButton( ">>" ); btn.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent ev ) { while ( true ) { int i = lst1.getSelectedIndex(); if ( i < 0 ) break; Object o = m1.remove( i ); m2.addElement( o ); } } } ); p.add( btn ); btn = new JButton( "