Java Tutorial - Wilkening

Transcription

Java Tutorial - Wilkening
Objektorientiertes Programmieren in Java - V. 29
Seite 1 / 409
Objektorientiertes Programmieren
in
Java
Detlef Wilkening
www.wilkening-online.de
© 1997-2016
Version 29
Dieses Java Tutorial darf in unveränderter Form für
Unterrichtszwecke weitergegeben und verwendet werden.
Der Autor freut sich aber über Rückmeldungen zum Einsatz des Tutorials.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 2 / 409
Objektorientiertes Programmieren in Java
1 Organisatorisches ......................................................................................................... 13
2 Java ................................................................................................................................ 14
2.1 Einleitung .................................................................................................................... 15
2.2 Java als Plattform........................................................................................................ 15
2.3 Java Versionen ........................................................................................................... 21
2.4 Technologien .............................................................................................................. 27
2.5 Fazit ............................................................................................................................ 30
3 Mini-Einführung ............................................................................................................. 30
3.1 Applikation .................................................................................................................. 31
3.2 Quelltext ...................................................................................................................... 32
3.3 Ausgabe ...................................................................................................................... 35
3.4 Strings ......................................................................................................................... 36
3.5 Arrays und Kommandozeilen-Argumente .................................................................... 36
3.6 Klassen ....................................................................................................................... 37
3.7 Funktionen .................................................................................................................. 37
3.8 Packages .................................................................................................................... 38
3.9 Exceptions .................................................................................................................. 39
3.10 Eingabe .................................................................................................................. 40
3.11 Konvertierungen ..................................................................................................... 43
4 Praktikum ....................................................................................................................... 43
4.1 Tools ........................................................................................................................... 43
4.2 Source Organisation ................................................................................................... 54
4.3 Benutzung der JDK Entwicklungs-Tools...................................................................... 54
4.4 Projekt Tools ............................................................................................................... 67
4.5 Source-Path und Class-Path ....................................................................................... 68
4.6 Eclipse ........................................................................................................................ 70
4.7 Projekte importieren .................................................................................................... 85
4.8 Aufgaben..................................................................................................................... 90
4.9 Lsg. zu Aufgabe „Hallo Welt“ – Kap. 4.8.1 .................................................................. 92
4.10 Lsg. zu Aufgabe „GUI-Fenster“ – Kap. 4.8.2 .......................................................... 93
4.11 Lsg. zu Aufgabe „Package“ – Kap. 4.8.3 ................................................................ 93
4.12 Lsg. zu Aufgabe „Packages“ – Kap. 4.8.4 .............................................................. 93
4.13 Lsg. zu Aufgabe „Summe“ – Kap. 4.8.5 .................................................................. 94
4.14 Lsg. zu Aufgabe „Ausgabe Verzeichnis“ – Kap. 4.8.6 ............................................. 97
5 Elementare Datentypen und Variablen ......................................................................... 98
5.1 Datentypen .................................................................................................................. 98
5.2 Elementare Datentypen............................................................................................... 99
5.3 Variablen ................................................................................................................... 101
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 3 / 409
5.4 Wert- und Referenz-Semantik ................................................................................... 102
6 Operatoren ................................................................................................................... 105
6.1 Zuweisungs-Operatoren ............................................................................................ 106
6.2 Gleich- und Ungleich-Operatoren .............................................................................. 106
6.3 Geteilt- und Modulo-Operator .................................................................................... 108
6.4 Bitweises AND, OR, XOR ......................................................................................... 110
6.5 Boolsches bzw. bedingtes AND, OR, XOR ............................................................... 110
6.6 Prä- und Post-Inkrement und -Dekrement Operatoren .............................................. 111
6.7 Schiebe-Operatoren .................................................................................................. 111
6.8 Frage-Zeichen Operator ............................................................................................ 112
6.9 Aufgaben................................................................................................................... 112
6.10 Lsg. zu Aufgabe „Inkrement-Operator“ – Kap. 6.9.1 ............................................. 112
7 Kontroll-Strukturen...................................................................................................... 113
7.1 Bedingter-Kontrollfluss – If ........................................................................................ 113
7.2 Mehrfach-Verzweigung – Switch ............................................................................... 117
7.3 For-Schleife ............................................................................................................... 119
7.4 For-Schleife für Container und Arrays ....................................................................... 120
7.5 While-Schleife ........................................................................................................... 120
7.6 Do-Schleife ............................................................................................................... 121
7.7 Break- und Continue-Anweisung ............................................................................... 121
7.8 Gelabelte Sprung-Anweisungen ................................................................................ 122
7.9 Aufgaben................................................................................................................... 123
7.10 Lsg. zu Aufgabe „Schleifen-Varianten“ – Kap. 7.9.1 ............................................. 125
7.11 Lsg. zu Aufgabe „Teilbar?“ – Kap. 7.9.2 ............................................................... 126
7.12 Lsg. zu Aufgabe „Lesbare Zahlen“ – Kap. 7.9.3 ................................................... 127
7.13 Lsg. zu Aufgabe „Zahlen-Liste“ – Kap. 7.9.4......................................................... 128
7.14 Lsg. zu Aufgabe „Mittelwert und Varianz“ – Kap. 7.9.5 ......................................... 131
7.15 Lsg. zu Aufgabe „Multiplikations-Matrix“ – Kap. 7.9.6 ........................................... 132
8 Funktionen ................................................................................................................... 134
8.1 Funktions-Arten ......................................................................................................... 135
8.2 Syntaktischer Aufbau ................................................................................................ 135
8.3 Variablen in Funktionen............................................................................................. 135
8.4 Funktions-Parameter ................................................................................................. 137
8.5 Funktions-Rückgaben ............................................................................................... 138
8.6 Überladen ................................................................................................................. 139
8.7 Rekursion .................................................................................................................. 140
8.8 Aufgaben................................................................................................................... 141
8.9 Lsg. zu Aufgabe „Fakultäts Funktion“ – Kap. 8.8.1.................................................... 143
8.10 Lsg. zu Aufgabe „Primzahl?“ – Kap. 8.8.2 ............................................................ 145
8.11 Lsg. zu „Aufgabe „Schleife rekursiv“ – Kap. 8.8.3 ................................................. 146
8.12 Lsg. zu Aufgabe „Zahlen-Liste 2“ – Kap. 8.8.4...................................................... 146
8.13 Lsg. zu Aufgabe „Quadratwurzel“ – Kap. 8.8.5 ..................................................... 147
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 4 / 409
9 Ausgewählte Bibliotheks-Klassen .............................................................................. 152
9.1 Klasse String ............................................................................................................. 153
9.2 Klassen StringBuilder & StringBuffer ......................................................................... 156
9.3 Container & Iteratoren ............................................................................................... 157
9.4 Wrapper Klassen ...................................................................................................... 176
9.5 Zufalls-Zahlen ........................................................................................................... 178
9.6 Datum und Uhrzeit .................................................................................................... 179
9.7 Datei- und Verzeichnis-Handling ............................................................................... 180
9.8 Aufgaben................................................................................................................... 187
9.9 Lsg. zu Aufgabe „String-Analyse“ – Kap. 9.8.1.......................................................... 192
9.10 Lsg. zu Aufgabe „Hallo <Person>“ – Kap. 9.8.2 .................................................... 196
9.11 Lsg. zu Aufgabe „Hallo <Personen>“ – Kap. 9.8.3 ................................................ 196
9.12 Lsg. zu Aufgabe „Lesbare Zahlen 2“ – Kap. 9.8.4 ................................................ 198
9.13 Lsg. zu Aufgabe „Lottozahlen“ – Kap. 9.8.5 .......................................................... 200
9.14 Lsg. zu Aufgabe „Telefonbuch“ – Kap. 9.8.6 ........................................................ 201
9.15 Lsg. zu Aufgabe „Platten-Platz Verbrauch“ – Kap. 9.8.7 ....................................... 202
9.16 Lsg. zu Aufgabe „Datei-Suche“ – Kap. 9.8.8......................................................... 203
10 Arrays ........................................................................................................................... 204
10.1
10.2
10.3
10.4
10.5
10.6
10.7
10.8
10.9
Deklaration ........................................................................................................... 204
Arrays haben ein Attribut: length........................................................................... 205
Arrays durchlaufen ............................................................................................... 206
Arrays sind Referenzen ........................................................................................ 206
Arrays können mehrdimensional sein ................................................................... 207
Fehlerhafter Index ................................................................................................ 208
Vergleiche ............................................................................................................ 208
Aufgaben .............................................................................................................. 209
Lsg. zu Aufgabe „Lesbare Zahlen 3“ – Kap. 10.8.1............................................... 209
11 Klassen ......................................................................................................................... 210
11.1
11.2
11.3
11.4
11.5
11.6
11.7
11.8
Motivation ............................................................................................................. 210
Klassen-Entwurf ................................................................................................... 211
Beispiel ................................................................................................................. 212
Klassen-Implementierung ..................................................................................... 215
Aufgaben .............................................................................................................. 220
Lsg. zu Aufgabe „Ringzähler“ – Kap. 11.5.1 ......................................................... 223
Lsg. zu Aufgabe „Kontaktdaten 1“ – Kap. 11.5.2 .................................................. 225
Lsg. zu Aufgabe „Kontaktdaten 2“ – Kap. 11.5.3 .................................................. 228
12 Klassen-Details ............................................................................................................ 229
12.1
12.2
12.3
12.4
12.5
Zugriffsbereiche .................................................................................................... 229
Konstruktoren ....................................................................................................... 230
finalize .................................................................................................................. 231
Funktionen ............................................................................................................ 232
Attribute ................................................................................................................ 235
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
12.6
12.7
12.8
12.9
12.10
12.11
Seite 5 / 409
Aufzählungen........................................................................................................ 236
Top-Level-Klassen ................................................................................................ 239
Aufgaben .............................................................................................................. 239
Lsg. zu Aufgabe „Türme von Hanoi“ – Kap. 12.8.1 ............................................... 244
Lsg. zu Aufgabe „Gerüchteküche“ – Kap. 12.8.2 .................................................. 246
Lsg. zu Aufgabe „Tic-Tac-Toe 1“ – Kap. 12.8.3 .................................................... 250
13 Packages ...................................................................................................................... 271
13.1
13.2
13.3
13.4
13.5
13.6
Package-Anweisung ............................................................................................. 271
Klassen in Packages ............................................................................................ 272
Language-Package .............................................................................................. 273
Verschachtelung ................................................................................................... 273
Default-Package ................................................................................................... 273
Verzeichnisse ....................................................................................................... 274
14 Vererbung..................................................................................................................... 274
14.1
14.2
14.3
14.4
14.5
14.6
14.7
14.8
14.9
14.10
14.11
14.12
14.13
14.14
14.15
14.16
14.17
14.18
14.19
14.20
14.21
14.22
14.23
14.24
Vererbungs-Hierarchien........................................................................................ 274
Implementation ..................................................................................................... 275
Schlüsselwort super.............................................................................................. 276
Konstruktoren ....................................................................................................... 276
finalize .................................................................................................................. 277
Überschreiben ...................................................................................................... 277
Ist-ein Beziehung .................................................................................................. 279
Polymorphie.......................................................................................................... 279
abstract................................................................................................................. 280
Casts und instanceof ............................................................................................ 281
Klasse „java.lang.Object“ ...................................................................................... 283
Anwendung – Beispiel „Obstkorb“ ........................................................................ 285
Interfaces.............................................................................................................. 293
Modul-Entkopplung ............................................................................................... 294
Fazit...................................................................................................................... 297
Aufgaben .............................................................................................................. 297
Lsg. zu Aufgabe „Obstkorb“ – Kap. 14.16.1 .......................................................... 299
Lsg. zu Aufgabe „ProgressBar“ – Kap. 14.16.2 .................................................... 299
Lsg. zu Aufgabe „Kontaktdaten 3“ – Kap. 14.16.3 ................................................ 300
Lsg. zu Aufgabe „Kontaktdaten 4“ – Kap. 14.16.4 ................................................ 303
Lsg. zu Aufgabe „Tic-Tac-Toe 2“ – Kap. 14.16.5 .................................................. 303
Lsg. zu Aufgabe „Tic-Tac-Toe 3“ – Kap. 14.16.6 .................................................. 306
Lsg. zu Aufgabe „Tic-Tac-Toe 4“ – Kap. 14.16.7 .................................................. 307
Lsg. zu Aufgabe „Tic-Tac-Toe 5“ – Kap. 14.16.8 .................................................. 308
15 Innere Klassen ............................................................................................................. 308
15.1
15.2
15.3
Eingebettete static Klassen .................................................................................. 309
Eingebettete nicht static Klassen .......................................................................... 310
Eingebettete Interface’s ........................................................................................ 311
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
15.4
15.5
15.6
Seite 6 / 409
Lokale Klassen ..................................................................................................... 311
Anonyme Klassen ................................................................................................. 313
Virtuelle Maschine ................................................................................................ 314
16 GUI Programmierung mit Swing................................................................................. 314
16.1
16.2
16.3
16.4
16.5
16.6
16.7
16.8
16.9
Ein einfaches Fenster ........................................................................................... 316
Ein etwas besseres Fenster.................................................................................. 317
Ein grafisches „Hallo Welt“ ................................................................................... 318
Graphics Objekt .................................................................................................... 320
Klasse „Color“ ....................................................................................................... 321
Änderung der Oberfläche in den Windows-Style .................................................. 322
Aufgaben .............................................................................................................. 324
Lsg. zu Aufgabe „Schwebende Kugel“ – Kap. 16.7.1 ............................................ 325
Lsg. zu Aufgabe „Farb-Fenster“ – Kap. 16.7.2...................................................... 325
17 Philosophie der GUI Programmierung ....................................................................... 325
17.1
17.2
Besitzer des Bildschirms ....................................................................................... 325
Kontrollfluß ........................................................................................................... 326
18 Event-Modelle von Swing............................................................................................ 327
18.1
18.2
18.3
18.4
18.5
18.6
Events und Gruppierungen ................................................................................... 327
Internes Event-Modell ........................................................................................... 328
Externes Event-Modell .......................................................................................... 333
Vergleich der Event-Modelle ................................................................................. 345
Scribble 4 mit Daten-Modell .................................................................................. 345
Aufgaben .............................................................................................................. 348
19 Swing Layouts ............................................................................................................. 348
19.1
19.2
19.3
19.4
19.5
19.6
19.7
Swing Klasse „JButton“......................................................................................... 349
Grundlagen ........................................................................................................... 349
Layouts ................................................................................................................. 350
Verschachtelte Layouts ........................................................................................ 353
Aufgaben .............................................................................................................. 354
Lsg. zu Aufgabe „Layouts 1“ – Kap. 19.5.1 ........................................................... 354
Lsg. zu Aufgabe „Layouts 2“ – Kap. 19.5.2 ........................................................... 354
20 Swing GUI-Elemente.................................................................................................... 354
20.1
20.2
20.3
20.4
20.5
20.6
20.7
20.8
20.9
Labels ................................................................................................................... 354
Text-Felder ........................................................................................................... 355
Buttons ................................................................................................................. 357
Radio-Buttons ....................................................................................................... 358
Check-Boxen ........................................................................................................ 359
Scroll-Bars und Scroll-Panes ................................................................................ 361
Tabellen................................................................................................................ 361
Panels .................................................................................................................. 365
Menüs................................................................................................................... 365
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
20.10
20.11
20.12
20.13
20.14
20.15
Seite 7 / 409
Timer .................................................................................................................... 367
Aufgaben .............................................................................................................. 369
Lsg. zu Aufgabe „Liste“ – Kap. 20.11.1................................................................. 370
Lsg. zu Aufgabe „Scribble 6“ – Kap. 20.11.2 ........................................................ 370
Lsg. zu Aufgabe „TextField“ – Kap. 20.11.3.......................................................... 370
Lsg. zu Aufgabe „Kontaktdaten 5“ – Kap. 20.11.4 ................................................ 370
21 Applets ......................................................................................................................... 370
21.1
21.2
21.3
21.4
21.5
21.6
21.7
21.8
Beispiel ................................................................................................................. 370
Applet HTML-Seite ............................................................................................... 371
Grundlagen ........................................................................................................... 373
Applet-Parameter ................................................................................................. 374
Applets & Applikationen ........................................................................................ 375
Aufgaben .............................................................................................................. 376
Lsg. zu Aufgabe „Statuszeilen-Applet“ – Kap. 21.6.1 ............................................ 376
Lsg. zu Aufgabe „Scribble 7“ – Kap. 21.6.2 .......................................................... 376
22 Exceptions ................................................................................................................... 376
22.1
22.2
22.3
22.4
22.5
22.6
22.7
22.8
22.9
22.10
22.11
22.12
Motivation ............................................................................................................. 376
Realisation ............................................................................................................ 377
Fehler über mehrere Funktions-Ebenen ............................................................... 378
Aufräumarbeiten und „finally“ ................................................................................ 379
Exception-Objekte und Exception-Hierarchie........................................................ 381
Fangen von Basis-Exception-Klassen .................................................................. 382
Mehrere catch-Blöcke ........................................................................................... 383
Unbehandelte Exceptions ..................................................................................... 384
Geprüfte und ungeprüfte Exceptions .................................................................... 385
Verschachtelte try-Blöcke ..................................................................................... 387
Exception-Objekte ................................................................................................ 390
Exception-Sicherheit ............................................................................................. 391
23 Streams ........................................................................................................................ 393
23.1
23.2
23.3
23.4
Einführung ............................................................................................................ 393
Java Streams........................................................................................................ 393
Beispiele ............................................................................................................... 397
Stream-Tokenizer ................................................................................................. 399
24 Reflexion ...................................................................................................................... 400
24.1
24.2
24.3
24.4
24.5
Objekt-Erzeugung über den Klassen-Namen ....................................................... 400
Klasse „Class“ ...................................................................................................... 402
Disassemblieren ................................................................................................... 404
Aufgaben .............................................................................................................. 404
Lsg. zu Aufgabe „Klassen-Inspektor“ – Kap. 24.4.1 .............................................. 404
25 Serialisierung ............................................................................................................... 405
25.1
Interface Serializable ............................................................................................ 405
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
25.2
25.3
25.4
25.5
25.6
25.7
Seite 8 / 409
Rekursion ............................................................................................................. 406
Klassen-Variablen................................................................................................. 408
Modifier „transient“ ................................................................................................ 408
Nicht serialisierbare Basis-Klassen ....................................................................... 409
Aufgaben .............................................................................................................. 409
Lag. zu Aufgabe „Scribble 7“ – Kap. 25.6.1 .......................................................... 409
Abbildungen
Abb. 2-1 : Executable-Abhängigkeit zu einer konkreten Plattform ........................................ 16
Abb. 2-2 : Executables für mehrere Plattformen ................................................................... 16
Abb. 2-3 : Executable für mehrere Plattformen entwickeln ................................................... 17
Abb. 2-4 : Idee einer virtuellen Maschine.............................................................................. 17
Abb. 2-5 : Portabilität mit einer VM und genormtem Byte-Code ........................................... 18
Abb. 3-1 : Oracle Homepage – Einstieg in den Download des JDK...................................... 32
Abb. 4-1 : Oracle Homepage – Einstieg in den Download des JDK...................................... 45
Abb. 4-2 : Oracle Java Download Seite – JDK Auswahl ....................................................... 46
Abb. 4-3 : Oracle JDK Download Seite – Plattform auswählen und downloaden .................. 47
Abb. 4-4 : Oracle Java Download Seite – JDK Dokumentation Auswahl .............................. 48
Abb. 4-5 : Oracle JDK Doku Download Seite – Doku auswählen und downloaden............... 48
Abb. 4-6 : Installiertes JDK – Aufruf der JVM ....................................................................... 49
Abb. 4-7 : Aufruf des Java-Compilers nach gesetzter Path Umgebungs-Variable ................ 49
Abb. 4-8 : Aufruf des Java-Compilers mit vollem Pfad ......................................................... 50
Abb. 4-9 : Entpackte Java Dokumentation ........................................................................... 50
Abb. 4-10 : Java Dokumentations Einstieg „index.html“ ....................................................... 51
Abb. 4-11 : Eclipse Homepage – Wechsel zur Download Seite............................................ 52
Abb. 4-12 : Eclipse Download-Seite – Eclipse IDE for Java Developers ............................... 53
Abb. 4-13 : „Hallo Welt“ Quelltext im Editor (Datei „e:\java\HalloWelt.java“) ......................... 55
Abb. 4-14 : Kommandozeile für das Verzeichnis „e:\java“ .................................................... 56
Abb. 4-15 : Der Java Compiler erzeugt den Byte-Code des „Hallo Welt“ Beispiels .............. 57
Abb. 4-16 : Die virtuelle Java Maschine (JVM) führt das „Hallo Welt“ Beispiel aus ............... 57
Abb. 4-17 : GUI Quelltext im Editor (Datei „e:\java\Gui.java“) ............................................... 58
Abb. 4-18 : Der Java Compiler erzeugt den Byte-Code des GUI Beispiels ........................... 58
Abb. 4-19 : Die virtuelle Java Maschine (JVM) führt das GUI Beispiel aus ........................... 59
Abb. 4-20 : Der Java Compiler erzeugt den Byte-Code aller Klassen im Verzeichnis ........... 59
Abb. 4-21 : Package Quelltext im Editor (Datei „PackageBeispiel.java“) .............................. 60
Abb. 4-22 : Verzeichnis-Struktur für das Package Beispiel................................................... 60
Abb. 4-23 : Der Java Compiler erzeugt den Byte-Code des Package Beispiels ................... 61
Abb. 4-24 : Fehler bei der Ausführung des Package Beispiels ............................................. 61
Abb. 4-25 : Der Java Compiler erzeugt den Byte-Code mit „sourcepath“ Option .................. 62
Abb. 4-26 : Korrekte Ausführung des Package Beispiels ..................................................... 63
Abb. 4-27 : Fehler bei Ausführung außerhalb des Wurzel-Verzeichnisses ........................... 63
Abb. 4-28 : Korrekte Ausführung des Package Beispiels mit Class-Path Option „-cp“.......... 64
Abb. 4-29 : Ausgangs Struktur für Byte-Code eigene Verzeichnisse .................................... 65
Abb. 4-30 : Der Java Compiler erzeugt den Byte-Code mit Trennung des Byte-Codes ........ 65
Abb. 4-31 : Erzeugte Verzeichnis Struktur und Byte-Code ................................................... 66
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 9 / 409
Abb. 4-32 : Korrekte Ausführung des Package Beispiels mit Trennung des Byte-Codes ..... 66
Abb. 4-33 : Kommandozeilen Argument Quelltext im Editor (Datei „e:\java\Args.java“) ........ 67
Abb. 4-34 : Compilation und Ausführung des Kommandozeilen Argumente Beispiels ......... 67
Abb. 4-35 : 2 Klassen in zwei parallelen Packages .............................................................. 69
Abb. 4-36 : Klassen, Dateien, Packages, Verzeichnisse und der Source-Path..................... 69
Abb. 4-37 : Eclipse Workspace Launcher ............................................................................ 71
Abb. 4-38 : Eclipse Begrüßungs Bildschirm.......................................................................... 72
Abb. 4-39 : Eclipse Begrüßungs Bildschirm schließen ......................................................... 72
Abb. 4-40 : Leerer Workspace in der Java Perspektive........................................................ 73
Abb. 4-41 : Neues Projekt anlegen....................................................................................... 74
Abb. 4-42 : Projekt-Wizard ................................................................................................... 75
Abb. 4-43 : Workspace mit neuem „Hallo Welt“ Projekt ....................................................... 76
Abb. 4-44 : Neue Klasse anlegen ......................................................................................... 77
Abb. 4-45 : Klassen-Wizard.................................................................................................. 78
Abb. 4-46 : Warnung bei Nutzung des Default-Packages .................................................... 79
Abb. 4-47 : Warnung bei fehlerhaftem Klassen-Namen ....................................................... 79
Abb. 4-48 : Neue Klasse „HalloWeltAppl“ im Workspace ..................................................... 80
Abb. 4-49 : Fertiger Quelltext im Wizard .............................................................................. 81
Abb. 4-50 : Starten des Programms ..................................................................................... 83
Abb. 4-51 : Ausgabe des Programms .................................................................................. 83
Abb. 4-52 : Eclipse Warnungs Preferences für „serialVersionUID“ ....................................... 85
Abb. 4-53 : Eclipse Warnungs Preferences für „typlose Container“ ...................................... 85
Abb. 4-54 : Ausgepackter Tutorial Workspace auf “E:” ........................................................ 86
Abb. 4-55 : Neuer Eclipse Workspace für die Tutorial Beispiele ........................................... 87
Abb. 4-56 : Kontext-Menü mit Import Eintrag ....................................................................... 87
Abb. 4-57 : Import Wizard Seite 1 ........................................................................................ 88
Abb. 4-58 : Import Wizard Seite 2 ........................................................................................ 89
Abb. 4-59 : Der fertig importierte Eclipse Workspace ........................................................... 90
Abb. 5-1 : Wert-Semantik ................................................................................................... 103
Abb. 5-2 : Referenz-Semantik ............................................................................................ 104
Abb. 9-1 : Ein String-Objekt wird nicht verändert ................................................................ 155
Abb. 9-2 : Ein StringBuilder-Objekt wird verändert ............................................................. 157
Abb. 9-3 : Warnungen des Java-Compilers wegen der Nutzung untypisierter Container.... 162
Abb. 9-4 : Warnungen der Eclipse 3.7.2 wegen der Nutzung untypisierter Container ........ 163
Abb. 9-5 : Vererbungs-Hierarchie der Wrapper-Klassen .................................................... 178
Abb. 10-1 : Referenz-Semantik bei Arrays ......................................................................... 207
Abb. 10-2 : Speicher-Layout eines mehrdimensionalen Arrays .......................................... 207
Abb. 10-3 : Speicher-Layout eines nicht-rechteckigen Arrays ............................................ 208
Abb. 11-1 : Das Schlüsselwort heißt „Abstraktion“ ............................................................. 210
Abb. 11-2 : Objekt-Kapselung und Benutzung über die Schnittstelle .................................. 210
Abb. 11-3 : Klassen und Objekte am Beispiel von Autos .................................................... 211
Abb. 11-4 : Funktionen ohne bzw. mit Objekt-Bezug .......................................................... 217
Abb. 12-1 : Türme von Hanoi ............................................................................................. 240
Abb. 14-1 : Vererbungs-Hierarchie ..................................................................................... 274
Abb. 14-2 : Allgemeine Vererbungs-Hierarchie .................................................................. 274
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 10 / 409
Abb. 14-3 : Vererbungs-Hierarchie des Beispiels ............................................................... 276
Abb. 14-4 : 2 Schicht-Architektur eines Programms ........................................................... 294
Abb. 14-5 : Abhängigkeit in einer 2 Schicht-Architektur ...................................................... 295
Abb. 14-6 : Design der Modul-Entkopplung ........................................................................ 295
Abb. 16-1 : Ein grafisches „Hallo Welt“ Programm ............................................................. 318
Abb. 16-2 : Ein GUI Programm mit bunter Ausgabe im Fenster ......................................... 321
Abb. 16-3 : GUI-Fenster aus Kapitel 19.4 mit Swing-Style ................................................. 322
Abb. 16-4 : GUI-Fenster aus Kapitel 19.4 mit Windows-Style ............................................ 323
Abb. 16-5 : Fenster mit schwebender Kugel ....................................................................... 324
Abb. 17-1 : Aufbau klassischer Programme ....................................................................... 326
Abb. 17-2 : Aufbau Event-gesteuerter Programme............................................................. 326
Abb. 18-1 : Der Fluß eines Events...................................................................................... 328
Abb. 18-2 : Ein einfaches Scribble Zeichen-Programm ...................................................... 331
Abb. 18-3 : Klassen-Diagramm Observer-Pattern .............................................................. 334
Abb. 18-4 : Interaktion Observer-Pattern ............................................................................ 334
Abb. 18-5 Klassen-Diagramm Observer-Pattern in Java ................................................... 334
Abb. 19-1 : Ein Fenster mit einem Button ........................................................................... 349
Abb. 19-2 : Die Content-Pane enthält nur den letzten Button ............................................. 350
Abb. 19-3 : Fenster mit Border-Layout ............................................................................... 351
Abb. 19-4 : Fenster mit Flow-Layout................................................................................... 351
Abb. 19-5 : Fenster mit Grid-Layout ................................................................................... 352
Abb. 19-6 : Fenster mit verschachtelten Layouts ................................................................ 353
Abb. 20-1 : Fenster mit Label ............................................................................................. 355
Abb. 20-2 : Fenster mit Text-Feld und automatischer Längen-Angabe .............................. 356
Abb. 20-3 : Fenster mit Button im initialen Zustand und nach dreimaliger Betätigung ........ 357
Abb. 20-4 : Fenster mit einer Gruppe von Radio-Buttons ................................................... 359
Abb. 20-5 : Fenster mit drei Check-Boxen .......................................................................... 360
Abb. 20-6 : Fenster mit einfacher 4x3 Tabelle .................................................................... 362
Abb. 20-7 : Fenster mit 40x30 Tabelle ohne Scrollbar ........................................................ 362
Abb. 20-8 : Fenster mit 40x30 Tabelle mit Scrollbar ........................................................... 363
Abb. 20-9 : Fenster mit Tabelle, dessen Inhalt aus einem Array stammt ............................ 364
Abb. 20-10 : Fenster mit Tabelle mit Tabellen-Modell ........................................................ 365
Abb. 20-11 : Fenster mit Menü ........................................................................................... 367
Abb. 20-12 : Fenster in grün und in rot – gesteuert von einem Timer ................................. 368
Abb. 22-1 : Ein kleiner aber wichtiger Teil der Exception-Hierarchie in Java ...................... 381
Abb. 22-2 : Klassen-Hierarchie für unsere Beispiel File-Exceptions ................................... 382
Abb. 23-1 : Ein nicht näher spezifizierter Datenstrom ......................................................... 393
Abb. 23-2 : Bsp für ineinander gekapselte Streams ........................................................... 394
Abb. 23-3 : Arbeitsweise der Stream-Kapselung ................................................................ 394
Versions-Historie
Version
Veränderte Kapitel
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 11 / 409
29
April 2016
• In vielen Kapiteln Rechtschreibfehler und kleine Ausdrucksfehler beseitigt.
• Text in hoffentlich allen Kapiteln an die neuen Versionen von Java (JDK 1.8.0_u77)
und Eclipse 4.5.2 (Mars) angepaßt – Screenshots sind noch auf dem alten Stand.
• Viele Kapitel in Kapitel 6 über Operatoren überarbeitet – aber noch nicht fertig.
• Einführendes Kapitel 3.9 über „Exceptions“ überarbeitet und leicht erweitert.
• Einführendes Kapitel 3.10 über „Einlesen von der Kommandozeile“ aktualisiert.
• Alle Quelltexte in den Kapiteln 8-11 mit Syntax-Highlighting versehen.
• Kleine Änderungen in Kapitel 8.
• Mehrere Fehler in den Beispielen von Kapitel 9 beseitigt, außerdem noch mehrfach in
den Beispielen die Typ-Angabe bei Containern vereinfacht.
• In den Musterlösungen zu Kapitel 11 auf typisierte Container umgestellt.
• Kapitel 12.2.1 über Attribut-Initialisierungen erweitert
• Kapitel 12.6 über Enums komplett neu geschrieben
28
April 2015
• In vielen Kapiteln Rechtschreibfehler und kleine Ausdrucksfehler beseitigt.
• Text in hoffentlich allen Kapiteln an die neuen Versionen von Java (JDK 1.8.0_u40),
Eclipse 4.4.2 (Luna) und NetBeans angepaßt.
• Beschränkungen auf alte JDKs entfernt – das Tutorial geht jetzt defaultmäßig immer
von einem JDK 1.8 aus.
• Alle Quelltexte in den Kapiteln 1-7 mit Syntax-Highlighting versehen.
• Viele kleine Erweiterungen, Anpassungen und Vereinfachungen im Kapitel 3.
• Kapitel 4 in vielen Teilen erweitert, so z.B. ein neues Kapitel für den Import von
Projekten in einen Eclipse-Workspace. Außerdem alle Abbildungen erneuert und an
Java 8 und Eclipse 4.4 angepaßt.
• Viele kleine Erweiterungen, Anpassungen und Vereinfachungen im Kapitel 5.
27
Mai 2012
• Text in hoffentlich allen Kapiteln an die neuen Versionen von Java (JDK 1.7.0_u04),
Eclipse 3.7.2 und NetBeans (7.1.2) angepaßt.
• In vielen Kapiteln Rechtschreibfehler und kleine Ausdrucksfehler beseitigt.
• Kapitel 2.3.8 über JDK 1.7 vervollständigt.
• Kleine Änderungen in den Schleifen-Kapiteln 7.3, 7.4 und 7.7.2.
• Kleinere Ergänzungen in Kapitel 8.7 (Rekursion).
• Kapitel 9 (Java-Bibliothek) in allen Kapiteln erweitert und überarbeitet – vor allem
Kapitel 9.1 (Strings), den Anfang von Kapitel 9.3 (Container), Kapitel 9.5
(Zufallszahlen) und Kapitel 9.7 (Datei-System). Außerdem zwei neue Aufgaben 9.8.7
und 9.8.8 hinzugefügt – inkl. den Muster-Lösungen 9.15 und 9.16 (aber leider nur der
reine Quelltext, noch ohne weitere Erklärungen).
• Einige kleinere Änderungen in Kapitel 10 (u.a. neuer For-Schleifen-Typ), aber auch
ein komplett neues Unter-Kapitel 10.3.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 12 / 409
26
Okt. 2011
• Text in vielen Kapiteln (aber noch nicht allen) an die neuen Versionen JDK 1.7.0_00
und Eclipse 3.7.1 angepaßt.
• In vielen Kapiteln Rechtschreibfehler und kleine Ausdrucksfehler beseitigt.
• Ein Unterkapitel 2.3.8 über JDK 1.7 in das Kapitel 2.3 aufgenommen, aber bisher nur
angefangen – noch viel offen.
• Kapitel 7.2 komplett neu geschrieben mit mehr Beispielen und auch JDK 1.7
Erweiterungen.
• Kapitel 8 hat viele kleine Änderungen und Erweiterungen bekommen.
• Kapitel 8 hat zwei neue Aufgaben (Kapitel 8.8.3 und Kapitel 8.8.4) mit Musterlösungen
ohne Erklärungen (Kapitel 8.11 und Kapitel 8.12) bekommen.
• Kapitel 9.3 vollkommen neu geschrieben mit vielen JDK 1.5 und JDK 1.7 Neuheiten
und viel mehr Informationen über die Container-Klassen. Das Kapitel ist aber noch
nicht fertig und enthält noch einige offene Punkte.
• Kapitel 9.7 leider immer noch nicht neu geschrieben, aber immerhin schon Verweise
auf einige „java.nio.file“ Neuigkeiten aus dem JDK 1.7 eingearbeitet.
25
Apr 2011
• Text in vielen Kapiteln an die neuen Versionen JDK 1.6.0_24 und Eclipse 3.6.2
angepaßt.
• In vielen Kapiteln Rechtschreibfehler und kleine Ausdrucksfehler beseitigt.
• Titelblatt eingeführt – mit Copyright und Hinweis auf die freie Verfügbarkeit des
unveränderten Tutorials für Unterrichtszwecke.
• Fußzeile um Link auf meine Homepage erweitert
• Kapitel 1 um Copyright und Hinweis auf die freie Verfügbarkeit des unveränderten
Tutorials für Unterrichtszwecke erweitert.
• Kapitel 2.3.8 ist neu
• Kapitel 16.6 über die Änderung des Swing GUI-Skins zu z.B. einem Windows-Style
wurde neu aufgenommen.
24
• Kapitel 6.3 über die Geteilt- und Modulo-Operatoren ergänzt.
• Fehler im ersten Beispiel in Kapitel 8.6 entfernt.
• Fehler im Durchlauf-Zähler in der Lösung zur rekursiven Quadratwurzel-Näherung in
Kapitel 8.13.2 entfernt.
• Kapitel 9.1.2 über String-Verkettung erweitert.
• Kapitel 9.2 um die Klasse „StringBuilder“ erweitert.
• Sämtliche Lösungen des Kapitels 9 auf StringBuilder, typisierte Container und den
neuen For-Schleifen-Typ umgestellt (Kapitel 9.10, 9.11, 9.12, 9.13 und 9.14).
Außerdem Lösung für das Lottozahlen-Programm (Kapitel 9.13) optimiert.
• Kleine Fehler in den Beispielen in Kapitel 11.4.2 beseitigt.
23
• Text in vielen Kapiteln an die neuen Versionen JDK 1.6.0_22 und Eclipse 3.6.1
angepaßt.
• Kleine Fehler in vielen Kapiteln entfernt.
• Break und Continue Anweisungen mit detailierten Beispielen hinterlegt – Kapitel 7.7.
• Kapitel über echte Enums ergänzt – Kapitel 12.6.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Offene
Punkte
•
•
•
•
•
•
Seite 13 / 409
Thread.sleep aus Kapitel 11.3.1 in Kapitel 9 aufnehmen.
Kapitel 7 ist in manchen Unterkapiteln noch ein bisschen knapp.
Kapitel 18 hat noch einige offene Stellen.
In Kapitel 23 fehlen noch einige Erklärungen.
In Kapitel 25 fehlt der Hinweis auf das „serialVersionUID“ Attribut.
Kapitel 10 über Arrays vor Kapitel 9 ziehen, da im File-System Kapitel ein Array von
Strings vorkommt.
1 Organisatorisches
Thema
• Objektorientiertes Programmieren in Java
• Achtung – die Sprache ist so umfangreich, dass die Vorlesung aus Zeitmangel bei weitem
nicht alle Sprachelemente abdecken kann, und bei den abgedeckten auch viele Details
ausläßt.
• Achtung – die Bibliotheken von Java sind extrem umfangreich – auch hier wird die Vorlesung
nur einzelne kleine Teile vorstellen können.
Voraussetzungen
• Kenntnisse einer beliebigen Programmiersprache.
• Die Vorlesung setzt voraus, dass Sie die Grundlagen der Programmierung kennen. Sie
sollten z.B. wissen was eine Variable, ein Typ oder eine Funktion ist, und warum und wofür
man sie benutzt. Sie sollten die Begriffe Scope (Block), Lebensdauer, Speicher, Stack und
Heap zumindest ungefähr zuordnen können. Über den Vorteil einer konsistenten und
durchgängigen vorstellen Formatierung und Benamung sollte ich kein Wort mehr verlieren
müssen. Ihnen sollte klar sein, warum die Auftrennung in möglichst unabhängige Module
sehr sinnvoll ist. Und zu guter Letzt sollten Sie zumindest in Ansätzen in der Lage sein, ein
Problem in Teil-Probleme zu zerlegen, es zu strukturieren, und einfache Algorithmische
Lösungen zu verstehen und erarbeiten zu können.
Tutorial
• Das Tutorial wurde mal wieder überarbeitet und erweitert. Und trotzdem ist es leider noch
nicht fertig – es enthält also noch immer einige Lücken und wahrscheinlich auch noch
Fehler. Lesen Sie es also mit einer gesunden Skepsis – wie aber eigentlich alles, nicht
wahr?
• Das Tutorial bezieht sich prinzipiell auf die aktuelle Java 8 Plattform mit dem JDK 1.8. Aus
Zeitmangel werden aber viele der „neueren“ Features von Java nicht besprochen. Von daher
laufen viele Beispiele auch mit alten Java-Versionen wie dem JDK 1.5. Trotzdem setze ich
bei allen Beispielen ein JDK 1.8 voraus – da ich zum Teil auch neue Sprach-Features
vorstelle und nutzen werde.
• Die Abbildungen und Beispiele der Kommandozeilen und der IDE in Kapitel 4 beziehen sich
noch auf ältere Versionen von Java – konkret auf das JDK 1.5.0_06. Bzgl. der im Kapitel
vorgestellten Themen gibt es aber keine Unterschiede zu den neusten Versionen, von daher
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 14 / 409
stimmen diese Beschreibungen und Abbildungen auch weiterhin problemlos
• Die Abbildungen der GUI Programme wurden unter Windows XP als auch Windows 7 mit
klassischem GUI Stil sowohl mit dem JDK 1.4.2, JDK 1.5, JDK 1.6, JDK 1.7 als auch dem
JDK 1.8 erstellt. Je nach von Ihnen verwendetem Betriebssystem und/oder JDK kann der
GUI Stil natürlich abweichen – verhalten sollten sich alle Beispiele aber identisch.
• Sollten Sie Fehler finden oder Anregungen haben, so schicken Sie mir bitte eine Mail Adresse: ‘[email protected]’. Bitte haben Sie Verständnis dafür, dass ich nicht jede
Mail beantworten kann.
Praktikum
• Kleine und große Aufgaben in Java lösen
• Näheres siehe in Kapitel 4.
Tools
• Die für die Entwicklung notwendigen Tools werden in Kapitel 4.1 vorgestellt.
• Ihre Benutzung wird in den Kapiteln 4.3 und 4.6 kurz erläutert.
Warum sollte ich Java lernen?
• Im Augenblick die wohl wichtigste und verbreiteste Programmiersprache.
• Relativ einfach zu lernen.
• Im großen Maße plattform-unabhängig und weit verbreitet – siehe Kapitel 2.2.
• Für viele Anwendungsfälle fertige Bibliotheken enthalten.
• Gute Unterstützung an Tools – siehe auch Kapitel 4.1.1 und 4.1.3.
• Gute Unterstützung an Bibliotheken, Literatur, usw.
Literatur und WWW
Es gibt hunderte von Büchern zu Java, Zeitschriften, und zig-Millionen Artikel im Web. Die erste
Anlaufstelle sollte die offizielle Doku von Oracle sein:
• http://www.oracle.com/us/technologies/java/index.html
Ansonsten kann Sie z.B. Onkel „Google“ mit vielen weiteren Links und z.B. Tante „Amazon“ mit
massenhaft Buch-Vorschlägen versorgen – auch wenn Sie natürlich andere Onkels und Tanten
präferieren dürfen.
2 Java
Dieses Kapitel führt in die Philosophie und Historie von Java ein, und stellt wichtige
Schlagwörter und (Marketing-) Begriffe der Java Welt vor. Betrachtet werden z.B. die
Implikationen und Vor- und Nachteile einer virtuellen Maschine, wie Java sie einsetzt oder die
Historie der JDKs. Aber es wird eben auch erklärt, was eine virtuelle Maschine ist, was JDKs
sind, oder was Applets oder Midlets sind.
Alle, die dieses Hintergrund-Wissen nicht interessiert und die heiß auf die Sprache Java an sich
und praktisches Programmieren sind, können direkt zum Kapitel 3 springen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 15 / 409
2.1 Einleitung
Java wurde von James Gosling bei Sun entwickelt. Die erste offizielle Vorstellung von Java war
im März 1995. Java entwickelte sich in den kommenden Jahren – aus verschiedenen Gründen
– zu einer der wichtigsten Programmier-Sprachen auf dem Markt, und ist seit einigen Jahren
die wohl am häufigsten eingesetzteste Programmier-Sprache1 der Welt. Mittlerweile hat Oracle
Sun übernommen – und damit liegt die Weiterentwicklung von Java primär in den Händen von
Oracle.
Bevor wir aber ab Kapitel 3 tiefer in die Sprache Java einsteigen, wollen wir einige andere
Dinge klären, die Java von vielen anderen Sprachen unterscheidet, und zum Teil auch für den
Erfolg von Java verantwortlich sind.
So ist Java mehr als nur die objektorientierte Programmier-Sprache, als die Sie den Namen
Java möglicherweise kennengelernt haben. Mit dem Begriff Java verbindet man auch häufig die
sogenannte virtuelle Maschine „JVM“. Und manchmal wird Java sogar als Plattform – quasi als
eine Art Betriebssystem – bezeichnet. Alle drei Sichtweisen haben ihre Berechtigung – und wir
werden auch gleich sehen, warum.
2.2 Java als Plattform
Um die Aussage „Java als Plattform“ zu verstehen, wollen wir uns etwas detailierter anschauen,
wie „normale“ Compiler-Sprachen arbeiten (Kapitel 2.2.1), und dies dann mit Java vergleichen
(Kapitel 2.2.2).
2.2.1 Compiler-Sprachen
2.2.1.1 Executable
Bei einer Compiler-Sprache wie z.B. C oder C++ schreibt der Programmierer sein Programm in
der entsprechenden Programmiersprache (eben z.B. C oder C++), und ein anderes Programm
(der Compiler) übersetzt dies in direkt ausführbaren Maschinen-Code.
Damit ist klar, dass das eigentlich Executable (d.h. die ausführbare Datei auf Ihrer Festplatte)
Achtung – misstrauen Sie solchen Aussagen wie die häufigste, verbreiteste oder sonstwas
Programmier-Sprache. Erstmal ist die Frage, wie definiert man sowas überhaupt (Anzahl an
Code-Zeilen, Anzahl Programmierer, Anzahl Bibliotheken, ...)? Und letztlich kann keiner
feststellen, wie viele Zeilen Code wirklich weltweit in irgendeiner Sprache geschrieben wurden,
oder wieviele Programmierer oder Firmen eine Sprache einsetzen, oder sonstwas. Es gibt aber
schon einige ganz interessante und halbwegs akzeptabler Statistiken über die Verbreitung und
Relevanz von Programmier-Sprachen. Und hier liegt Java immer in der Spitzengruppe, häufig
an erster Stelle.
1
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 16 / 409
fest an einen Prozessor (bzw. kompatible Prozessoren) gebunden ist, denn nur diese
Prozessoren verstehen diesen Maschinen-Code. Z.B. ein Executable, compiliert für einen
Sparc-Prozessor, läuft nicht auf einem Power-PC Prozessor – auch wenn das gleiche
Betriebssystem vorhanden ist.
Weiterhin hat jedes Betriebssystem ein spezielles Format, in dem ausführbare Datei aufgebaut
sein müssen. Der Compiler muß beim Executable erzeugen natürlich dieses Format
berücksichtigen. Damit ist ein Executable auch ein konkretes Betriebssystem (oder ein
kompatibles) gebunden. Ein Executable, z.B. compiliert für Windows, läuft nicht unter Linux –
auch wenn der gleiche Prozessor vorhanden ist.
Unser Executable ist also an eine Plattform bestehend aus Prozessor und Betriebssystem
gebunden.
Abb. 2-1 : Executable-Abhängigkeit zu einer konkreten Plattform
Natürlich kann man sein Programm auch für eine andere Plattform übersetzen, wenn man denn
einen Compiler für diese Plattform hat, und das Programm plattform-unabhängig geschrieben
ist (siehe Kapitel 2.2.1.2). Aber für jede gewünschte Ziel-Plattform (d.h. jede gewünschte
Kombination aus Prozessor und Betriebssystem) muß ein eigenes Executable erstellt werden.
Abb. 2-2 : Executables für mehrere Plattformen
2.2.1.2 Programmierung
Aber nicht nur der Compiler bestimmt die Ziel-Plattform unserer Programm-Entwicklung. Auch
die Programmierung selber kann hier bestimmte Randbedingungen festlegen. Ein Programm
existiert ja nicht im luftleeren Raum, sondern soll später mit dem Computer und dem Benutzer
interagieren. Dazu greift es z.B. auf das Datei-System zu, und macht Ein- und Ausgaben.
Hier hat der Programmierer drei Möglichkeiten:
1. Er beschränkt sich auf die Sprach- und Bibliotheks-Elemente, die im Umfang seiner
Programmier-Sprache enthalten sind. Unter C und C++ ist dies nur ein extrem kleiner
Bereich, der von den ISO Standards abgedeckt ist – z.B. gehören grafische Oberflächen
oder Netzwerk-Bereiche nicht dazu.
- Der Programmierer ist sehr eingeschränkt in seinen Möglichkeiten.
- Dafür hat er ein Programm geschrieben, das prinzipiell für viele Plattformen compiliert
werden kann.
2. Der Programmierer greift direkt auf das Betriebssystem zu2. Dann hat er alle Möglichkeiten
des Betriebssystems zur Verfügung – er kann also z.B. grafische Oberflächen
implementieren, oder Netzwerk-Funktionalitäten nutzen.
2
Auf das sogenannte Betriebssystem API (Application Programming Interface).
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 17 / 409
- Der Programmier hat alle Möglichkeiten des Betriebssystems.
- Das Programm ist an dieses eine Betriebssystem gebunden, also nicht portabel.
3. Letzlich kann der Programmierer auch fremde (3-party) Bibliotheken nutzen (kommerzielle
oder freie), die betriebssystem-abhängige Funktionalität kapselt, und diese unter mehreren
Betriebssystemen zur Verfügung stellt.
- Der Programmierer muß entsprechende Bibliotheken für seinen Compiler finden, und er
muß sie einsetzen können und dürfen3.
- Das Programm funktioniert dann für alle Plattformen, für die die Bibliothek vorhanden ist.
Abb. 2-3 : Executable für mehrere Plattformen entwickeln
2.2.1.3 Portabiliät
Portabilität ist bei Compiler-Sprachen also ein komplexes Thema, da dort die Hardware, der
Prozessor, das Betriebssystem und alle genutzten 3-party Bibliotheken eine Rolle spielen.
2.2.2 Java Virtual Machine – JVM
Während Compiler-Sprachen also ein Executable erzeugen, dass an eine ganz konkrete
Plattform (Hardware, Prozessor, Betriebssystem) gebunden ist, geht Java einen anderen Weg.
Java compiliert die Programme für eine sogenannte virtuelle Maschine – die „Java Virtual
Machine“ (JVM), d.h. eine Plattform, die es in Silizium gegossen gar nicht gibt4. Statt dessen
wird die virtuelle Maschine in Software geschrieben, und simuliert die Java Plattform auf einer
beliebigen anderen Plattform, z.B. Windows, Linux oder Mac OS-X.
Abb. 2-4 : Idee einer virtuellen Maschine
Damit macht sich Java in einem hohen Maße von den Portabilitäts-Problemen von CompilerSprachen unabhängig. Es gibt nur noch eine Plattform – die Java Virtual Machine – egal auf
welcher echten Plattform das Programm hinterher laufen soll. Solange auf der echten Plattform
eine JVM existiert, können Java Programme dort ablaufen.
Die Bedingungen klingen vielleicht harmlos, sind es aber in der Praxis oft nicht. So haben z.B.
Compiler zum Teil unterschiedliche Bibliotheks-Formate, die sich manchmal sogar zwischen
Compiler-Versionen geändert haben. Oder eine Bibliothek ist zwar für den gesuchten Compiler
vorhanden, aber mit anderen nicht-kompatiblen Optionen gebaut. Sind Sie jetzt von zwei
Bibliotheken, die mit inkompatiblen Optionen erstellt wurden, abhängig, so haben Sie verloren –
selber schon mal erlebt. Oder die Lizenz-Bedingung paßt nicht, oder die Bibliothek kostet
zuviel, oder ist in der Firma nicht validiert, oder oder oder.
4 Okay, es könnte so eine Plattform in Silizium geben. Aber obwohl es schon Versuche in
dieser Richtung gab, gibt es die JVM bis heute nur in Software.
3
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 18 / 409
Daher erzeugt ein Java Compiler auch keinen Maschinen-Code und kein Executable, sondern
sogenannten Byte-Code. Und die Datei bzw. die Dateien, die dabei erzeugt werden, sind
natürlich auch keine Executables, sondern Dateien, die eine JVM zur Ausführung benötigten.
Dieser Ansatz einer virtuellen Maschine hat natürlich einige Konsequenzen – im positiven wie
im negativen Sinne, die wir im folgenden diskutieren wollen.
2.2.2.1 Portabilität
Fangen wir mit einem klaren Plus an – die Portablität. Java Byte-Code ist natürlich extrem
portabel. Er enthält keinerlei Abhängigkeiten zu irgendeiner Hardware, zu irgendeinem
Prozessor oder irgendeinem Betriebssystem. Auch 3-party Bibliotheken machen hier keine
Probleme, da sie selber auch wieder als Java Byte-Code vorliegen. Solange auf der
eigentlichen Plattform eine JVM existiert, kann der Java Byte-Code dort ausgeführt werden.
Und dazu muß der Byte-Code auch nicht neu compiliert werden, denn es gibt keine bzgl. der
Byte-Code Definition keinerlei Unterschiede zwischen den JVMs verschiedener echter
Plattformen5.
Das Ganze geht sogar soweit, dass man Byte-Code von verschiedenen Quellen problemlos
mischen kann, und problemlos woanders laufen lassen kann. Z.B. könnte ein Java Projekt zum
Teil unter Windows mit Eclipse entwickelt werden, ein anderer Teil unter Solaris mit dem JavaCompiler von Sun, und ein dritter Teil unter Linux mit einem beliebigen anderen Compiler. Der
Byte-Code aller drei Teil-Projekte kann einfach zusammengebracht werden, und läuft dann z.B.
auch auf Mac OS-X ohne jede Änderung. Denn der Byte-Code und die Schnittstelle der JVM
sind genau normiert.
Abb. 2-5 : Portabilität mit einer VM und genormtem Byte-Code
Das ist Portablität in einer ganz anderen Dimension im Vergleich zu den herkömmlichen
Compiler-Sprachen.
2.2.2.2 Sandbox
Zusätzlich zu der Portablität ermöglicht der Ansatz einer VM auch noch andere Optionen. Da
der Byte-Code nicht direkt auf der Hardware und dem Prozessor läuft, und er auch nicht direkt
auf das Betriebssystem zugreifen kann, läßt sich ein Java Programm prinzipiell vollständig
kontrollieren. Alle Aktionen des Programms müssen letztlich durch die VM ausgeführt werden.
Wenn die VM aber sicherheits-kritische Aktionen wie z.B. den Zugriff auf das Dateisystem nicht
zuläßt, kann ein Java-Programm hier nichts kaputt machen.
Eine virtuelle Maschine kann also für ein Java-Programm wie ein Sandkasten („Sandbox“) sein,
5
Von den immer vorhandenen Bugs wollen wir mal absehen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 19 / 409
in dem es sich austoben, aber auch keinen Blödsinn anrichten kann. Genau dies beherrscht die
JVM auch. Im Kontext eines Browsers als Applet ausgeführte Java Programme (siehe Kapitel
2.2.2.3 und Kapitel 21) haben eingeschränkte Rechte. Nur über Zertifikate und explizite NutzerBestätigungen können Sie an mehr Rechte kommen – ansonsten müssen sie in ihrem
Sandkasten bleiben.
Hinweis – die Idee des Sandbox funktioniert natürlich nur, wenn die JVM Implementierung
keine Fehler aufweist, durch die die Programme den Sandkasten verlassen können.
2.2.2.3 Browser-Anwendungen
Ideal ist diese Portabilität und das Sandbox-Prinzip natürlich z.B. für Programme aus dem
Internet, die in jedem Browser laufen können sollen. Denken Sie sich eine Aufgabe, für die
einfache Text-Darstellung im Browser nicht reicht – hier wäre es schön, im Browser des
Nutzers ein kleines Programm ablaufen lassen zu können. Dies ist mit Java möglich, da es für
alle halbwegs wichtigen Browser Java-Plugins gibt, d.h. JVMs für den Browser.
Ein Server kann also ein Java-Programm als Byte-Code an einen Browser ausliefern, ohne sich
Gedanken um die Ziel-Plattform machen zu müssen (Portabilität), und Sie als Nutzer können
so ein Programm bedenkenlos in Ihrem Browser ausführen, da es sich nur im Sandkasten der
JVM mit eingeschränkten Rechten bewegen darf6.
So ein Java-Programm, das im Browser läuft, ist übrigens kein ganz normales Java-Programm,
sondern ein sogenanntes Applet, das sich minimal von normalen Java Desktop Programmen
unterscheidet. Im Tutorial lernen wir Applets und ihre Programmierung in Kapitel 21 kennen.
Hinweis – während in den Anfangszeiten von Java gerade die Applets als das Besondere von
Java herausgestellt wurden und ein Grund für den Erfolg von Java waren – sind Applets heute
nur noch sehr selten anzutreffen. Zum einen gibt es mittlerweile viele andere Technologien, die
Java im Browser überflüssig machen, zum anderen hat sich die JVM im Browser als ein echtes
Sicherheitsproblem herauskristallisiert – da die Sandbox nie wirklich sicher war. Die JVM hatte
und hat immer mit schweren Lücken im Sandbox-Prinzip zu kämpfen, wodurch Applets den
Sandkasten verlassen und dann mit den Rechten des Browsers auf dem System agieren
konnten.
2.2.2.4 Portable Umgebungen
Nicht nur Desktops bieten eine große Auswahl an Plattformen, auch portable Geräte wie z.B.
PDAs oder Handies sind eine interessante Umgebung für portable Programme. Sie bieten
vielfältige Hardware und unterschiedliche Betriebssystem – es wäre aber interessant
Programme für alle diese Plattformen schreiben zu können.
Dafür wurde in der Java Familie „JME“ („Java Mobile Edition“) entwickelt. Dies ist eine
6
Wir wollen wieder mal von Bugs in der JVM absehen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 20 / 409
eingeschränkte JVM, die nicht alle Sprach-Features von Java anbietet, und auch bei weitem
nicht alle Bibliotheken von Java unterstützt. Aber JME ist wieder eine genau definierte
Umgebung mit klar abgesteckten Fähigkeiten, die an die Anforderungen von einfachen
portablen Geräten angepaßt sind. Java-Programm für die JME-VM nennen sich Midlets.
Achtung – Java Mobile ist schon einige Jahre alt und adressierte portable Umgebungen, die
nicht besonders leistungsfähig waren – z.B. Handies aus einer Vor-Smartphone-Area. Heutige
portable Geräte wie Smartphones oder Tablets haben die Leistungsfähigkeit von normalen
Desktop Rechnern – hier kann eine fast ganz normale JVM problemlos laufen.
2.2.2.5 Android
Viele heutige portable Geräte laufen unter dem Betriebssystem Android – und die Haus- und
Hofsprache von Android ist Java. Wenn Sie eine App für Android entwickeln wollen, dann
werden Sie dies in den meisten Fällen in Java machen. Natürlich sind auch andere Sprachen
möglich – allen voran C++ – aber die normale Sprache für Android-Apps ist Java.
Das Java von Android ist nicht abgespeckt wie JME, sondern ein ganz normales Java. Nur für
die grafische Oberfläche wird nicht AWT, Swing oder JavaFX (siehe Kapitel 16) eingesetzt,
sondern ein spezielles Android-Spezifisches GUI Framework.
Leider sprengt die Entwicklung von Android Apps unseren Rahmen – obwohl es natürlich „cool“
wäre, ein Programm zu schreiben, das auf vielen Smartphones läuft.
2.2.2.6 Server-Umgebungen
Was für Browser und PDAs recht ist, kann für Server-Infrastrukturen auch passend sein. Auch
dort findet man häufig – historisch bedingt – ein heterogene und gewachsene Infrastruktur von
Plattformen der verschiedensten Art. Früher konnten hierbei Programme nicht einfach von
einem System auf das nächste verschoben werden, um den wechselnden Anforderungen zu
genügen – mit Java ist dies möglich. Solange ein JVM zur Verfügung steht, kann ein Programm
problemlos von einem Rechner auf den anderen verschoben oder kopiert werden.
In Server-Umgebungen ist dies nicht das einzig interessante Kriterium. Wichtig ist z.B. auch die
Unterstützung von Persistenz, Datenbanken, Transaktionen, uvm. Hierfür wurde in die Java
Familie „Java EE“ („Java Enterprise Edition“) aufgenommen. Dies ist eine Erweiterung von Java
um spezielle Bibliotheken und Fähigkeiten (z.B. „Java Enterprise Beans“ als ein KomponentenKonzept für Server-Anwendungen) für eben solche Themen.
Auch andere Server-Themen fanden ihren Niederschlag in der Java Familie. So gibt es in Java
z.B. Servlets – d.h. Java-Programme, die in einem Web-Server ausgeführt werden und
dynamische HTML Seiten bereit stellen können. Oder auch Portlets, die speziell für die
Programmierung von Web-Portalen entwickelt wurden.
Im Tutorial wird gar nicht auf spezielle Server-Elemente von Java eingegangen – dazu fehlt
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 21 / 409
wieder mal die Zeit.
2.2.3 Plattform „Java“
Wie man sieht, stellt Java in Form der JVM wirklich eine Plattform dar – denn sie simuliert
einen Java Prozessor mit einem Java Betriebssystem – dem typischen Verständnis einer
Plattform.
Aber auch mit einem breiter gefassten Verständnis von Plattform trifft der Begriff auf Java zu.
Java adressiert neben den normalen Desktop-Programmen auch Applets, die im Browser
laufen. Dann mit JME Midlets für einfache portable Geräte, und mit speziellen GUI Android
Smartphones. Und für Server-Anwendungen gibt es Java EE mit u.a. Servlets und Portlets.
Dies ist ein breites Spektrum an Einsatzgebieten, was in dieser Vielfalt von kaum einer anderen
Programmier-Sprache abgedeckt wird.
2.3 Java Versionen
Als Java im März 1995 der Öffentlichkeit vorgestellt wurde, war Java noch eine ziemlich kleine
Sprache – und kaum jemand (niemand?) dachte an eine solche Verbreitung, wie sie heute
vorhanden ist. Die initiale Entwicklung von Java hatte auch nur wenig mit den heutigen
Einsatzgebieten zu tun, sondern es ging um eine Software-Plattform für Haushaltsgeräte7. Da
Haushaltsgeräte auch eine sehr hetorogene Plattform darstellen, war die Entwicklung einer
virtuellen Maschine natürlich eine gute Idee8.
Wenn Sie Java nur als Ausführungs-Plattform nutzen wollen – und das wird auf die meisten
Menschen zutreffen – dann benötigen Sie nur die JVM in Form des JRE. Wollen Sie auch mit
Java entwickeln – und das betrifft uns – dann benötigen wir das JDK.
JRE steht hierbei für „Java Runtime Environment“ und ist das Stück Software, was der Nutzer
braucht, um ein Java-Programm (d.h. den Java Byte-Code) laufen zu lassen – also im Prinzip
die virtuelle Maschine.
JDK steht für „Java Development Kit“ und enthält neben dem JRE auch die Tools und
Bibliotheken, die man braucht um Java Programme zu entwickeln. Ein JDK ist also das, was
Sie zum Entwickeln brauchen – siehe auch Kapitel 4.1.1. Häufig wird es auch als Java SDK
(„Software Development Kit“) bezeichnet.
Sie kennen doch sicher auch diese Visionen vom Kühlschrank, der merkt, wenn die Milch alle
ist und beim Händler direkt neue bestellt. Hier hat auch der vielzitierte Satz „Java wurde für
Toaster entwickelt“ seinen wahren Ursprung.
8 Aber glauben Sie jetzt nicht, dass die Idee einer VM eine Java Idee ist. Schon Anfang der
70er Jahre gab es erste VMs mit normiertem Byte-Code, z.B. Eumel. Aber damals war die Zeit
noch nicht reif dafür.
7
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 22 / 409
2.3.1 JDK 1.0
Aus dem Labor kam Java heraus, als Sun eine Konkurrenz zu Microsofts Vorherrschaft auf
dem Desktop suchte, und das Internet sich zu entwickeln began. Der Desktop sollte unwichtig
werden, und statt dessen sollte der Nutzer einfach alles im Browser und dem Internet machen.
Und hierfür erschien eine VM mit ihren Fähigkeiten bzgl. Portabliltät und Sicherheit natürlich
ideal. Also wurde die Idee von Applets entwickelt – die Möglichkeit das damals recht
beschränkte Web um interaktive Möglichkeiten zu erweitern. So kam Java mit dem JRE 1.0
bzw. dem JDK 1.0 in die Welt.
Applets haben aber nicht wirklich die Welt gerettet – oder wie häufig begegnen Ihnen Applets
im Internet? Heutzutage gibt es andere Technologien um Rich-Internet-Seiten zu
programmieren, die sich mehr verbreitet haben, so z.B. DHTML, JavaScript, Ajax oder Flash.
Applets sind die Ausnahme, die man nutzt, wenn man wirklich die Vorteile einer mächtigen
Programmiersprache und der Sandbox im Browser benötigt.
Die Java Bibliothek des JDK 1.0 bestand aus 212 Klassen in 8 Packages. Selbst wenn Ihnen
die Begriffe Klassen und Packages noch nichts sagen – wir lernen sie in Kapitel 11 bzw. 13
kennen – sind die reinen Mengen-Zahlen im Laufe der JDK-Historie ganz interessant. Im
Augenblick reicht uns hier die einfache Vorstellung, dass Klassen stark vereinfacht (und auch
nicht richtig) eine Bündelung von Funktionen darstellen, und Packages ihrerseits wieder
Klassen in Gruppen strukturieren. Interessant ist also vor allem die Zahl der Klassen, da sie
indirekt ungefähr die Zahl der Bibliotheks-Funktionen widerspiegelt.
2.3.2 JDK 1.1
Die nächste Java Version (JDK 1.1) wurde im Februar 1997 freigegeben. Diese Version war ein
erster wichtiger Schritt zu einer ernsthaften Plattform. Der Sprung von Java aus dem Labor in
die reale Welt war mit dem JDK 1.0 relativ schnell erfolgt, und so krankte Java noch an einigen
Kinderkrankheiten. Das JDK 1.1 brachte hier viele Abhilfen, und so wurde neben der Bibliothek
auch die Sprache erweitert. Mit dem JDK 1.1 gab es nun z.B. innere Klassen, das GUI EventModell wurde quasi auf den Kopf gestellt, es gab Änderungen an der Serialisierung und an
Reflection, Unicode-Dateien wurden nun unterstützt, JDBC hielt Einzug, und die JNI
Schnittstelle wurde standardisiert. Und die Bibliothek erweitert sich um fast 300 Klassen.
JDK Version
1.0
1.1
Datum
Packages
Apr. 1995 8
Feb. 1997 23
Klassen
212
504
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
2.3.3 JDK 1.2 – Java 2
Zwischen den JDK Versionen 1.1 und 1.2 gab es viele Zwischen-Versionen, die manche
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 23 / 409
Neuerungen inkrementell einführten. Vor allem die GUI Welt von Java war im Umbruch – über
das eher einfache AWT von JDK 1.0 und JDK 1.1 wurde das schönere und mächtigere Swing
gesetzt. Damit wechselte das primäre Ziel von Java von den Applets zu Desktop-Programmen.
AWT („Abstract Window Toolkit“) war der Teil der Bibliothek des JDK 1.0 und 1.1, welches für
die GUI Programmierung zuständig war. AWT ist auch heute noch Teil von Java, wurde aber
durch Swing erweitert, was eine viel leistungsfähigere GUI Bibliothek darstellt. Nähere
Informationen zu AWT und Swing finden Sie in bzw. ab Kapitel 16.
Eine weitere wichtige Änderung war eine starke Erweiterung der Collections – wichtig, da
Collections das tägliche Brot des Programmierers darstellen, siehe auch Kapitel 9.3.
Aber auch viele andere Bibliotheksteile wurden überarbeitet, erweitert oder ergänzt. So drückte
sich das dann in Zahlen aus:
JDK Version
1.0
1.1
1.2 (Java 2)
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Packages
8
23
59
Klassen
212
504
1520
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
Alles zusammen war Sun Grund genug, von einem ganz neuen besseren Java zu reden, und
gab Java mit dem Namen „Java 2“ die Versions-Nummer 2.
Zusätzlich deutete sich mit dem JDK 1.2 schon ein Wechsel bzgl. der VM Technologie an. Seit
dem JDK 1.3 ist HotSpot die primäre JVM von Sun (siehe Kapitel 2.4.2), im JDK 1.2 war sie
schon enthalten und konnte per Option aktiviert werden.
2.3.4 JDK 1.3
Mit dem JDK 1.3 wurde der Focus von Java auch auf die Server-Infrastrukturen gelenkt, und so
brachte das JDK 1.3 neben HotSpot (siehe Kapitel 2.4.2) viele im Server-Umfeld wichtige neue
und erweiterte Bibliotheksteile mit – z.B. Verteilung via RMI/IIOP und JNDI Namensdienste.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Packages
8
23
59
76
Klassen
212
504
1520
1842
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 24 / 409
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
2.3.5 JDK 1.4
JDK 1.4 im Februar 2002 war eine für Java wichtige Version. Viele Kritiker sagen, dass dies die
erste professionell und produktiv einsetzbare Version war. Auf jeden Fall zeichnete sich das
JDK 1.4 durch weiter verbesserte Performance, einen besseren Garbage-Collector (siehe
Kapitel 2.4.1) und hohe Stabilität aus. Auch heute wird das JDK 1.4 von Sun noch unterstützt,
und wird auch noch von vielen Firmen aus den verschiedensten Gründen eingesetzt.
Natürlich brachte auch das JDK 1.4 wieder viele Bibliotheks Verbesserungen und
Erweiterungen mit sich – schauen Sie sich mal die Zahlen in der folgenden Tabelle an. Sowohl
Desktop-Programme profitierten von neuen Swing Features, als auch Server-Programme von
vielen anderen Neuerungen.
Und mit JDK 1.4 wurde zum ersten Mal seit dem JDK 1.1 auch wieder die Sprache erweitert –
Java bekam Assertions, die aus Zeitmangel in der Vorlesung leider nicht besprochen werden
können.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
1.4
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Feb. 2002
Packages
8
23
59
76
135
Klassen
212
504
1520
1842
2723
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
2.3.6 JDK 1.5 – Java 5
Mit dem JDK 1.5 wurde Java mal wieder umbenannt: „Java 5“ hieß es nun. Obwohl rund 2 ½
Jahre seit dem JDK 1.4 ins Land gegangen waren, vergrößerte sich die Bibliothek kaum. Es
kamen nur 556 neue Klassen hinzu.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
1.4
1.5 (Java 5)
© Detlef Wilkening 1997-2016
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Feb. 2002
Sep. 2004
Packages
8
23
59
76
135
166
Klassen
212
504
1520
1842
2723
3279
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 25 / 409
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
Die größten Änderungen passierten mit der Sprache selber und unter der Haube. Am
offensichtlichsten waren hiervon natürlich die Sprach-Erweiterungen:
• Neue Schleifen-Konstrukte
• Aufzählungs-Typen
• Generics (typisierte Container)
• Annotations
• statische Imports
• etc.
Unter der Haube beherrscht Java seit dem JDK 1.5 z.B. parallele Garbage-Collection – siehe
Kapitel 2.4.1.
Hinweis – aus Zeitmangel werden viele der mit Java 5 eingeführten Sprach-Konstrukte im
Tutorial nicht oder nur sehr knapp besprochen.
2.3.7 JDK 1.6 – Java 6
Im Dezember 2006 beglückte Sun die Java Community mit Java 6 bzw. dem JDK 1.6 – der zur
Zeit aktuellen Version9. Es gab keine Sprach-Änderungen, aber natürlich wurde die Bibliothek
wieder verbessert und erweitert – so wurde u.a. die Integration von Java Programmen in den
Desktop stark verbessert, z.B. durch eine System-Tray Unterstützung. Aber die vielleicht
interessanteste Neuerungen spielte sich woanders ab.
Seit dem JDK 1.6 hat Java eine Script-API und unterstützt damit direkt mehrere andere
Programmierspachen. So gibt es jetzt z.B. JRuby, eine in Java geschriebene Implementierung
der Programmiersprache Ruby auf der JVM mit der Möglichkeit der einfachen Interaktion
zwischen beiden Welten.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
1.4
1.5 (Java 5)
1.6 (Java 6)
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Feb. 2002
Sep. 2004
Dez. 2006
Packages
8
23
59
76
135
166
202
Klassen
212
504
1520
1842
2723
3279
3777
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
9
Stand 13.04.2008
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 26 / 409
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
2.3.8 JDK 1.7
Mittlerweile hat Oracle Sun übernommen, und Java steht nun unter der Regie von Oracle. Dies
hat dazu geführt, dass wir lange auf die nächste Java Version warten mußten. Aber seit August
2011 ist Java 7 (JDK 1.7) da, und seit Mai 2012 ist sie auch für den Produktiveinsatz
freigegeben. Java 7 brachte sowohl Erweiterungen der Sprache, der Bibliothek als auch der
JVM mit sich.
Die Sprache Java wurde um einige kleine Dinge ergänzt, die das Leben in der Praxis einiges
leichter machen. Zum Beispiel können jetzt Switch-Anweisungen für Strings genutzt werden
(Kapitel 7.2), lesbare Integer- und Binär-Literale wurden eingeführt, Exception-Catch-Blöcke
können nun mehrere Exceptions gleichzeitig fangen, Redundante Typ-Informationen bei
Generics sind nicht mehr notwendig (Kapitel 9.3.1), oder die Handhabung von ClosableRessourcen ist nun viel sicherer.
Auch in der Bibliothek gab es eine Menge neue Dinge – besonders hervorzuheben sind sicher
das neue Fork/Join-Framework im Multithreading-Umfeld, die Erweiterungen im New-IO
Package (Kapitel 9.7.2), Unterstützung von Unicode 6, Veränderungen an den Währung-Codes
und Locales, Überarbeitung des XML-Stacks, und der Ausbau der GUI Bibliothek mit z.B.
neuen Designs (Nimbus-Projekt) und transparenten bzw. halb-transparenten Fenstern.
Unter der Haube, daher in der virtuellen Maschine, wurde das Class-Loading Verhalten
verändert, der neue Garbage Collector G1 aktiviert, und die Anbindung an neue ProgrammierSprachen weiter vereinfacht.
Unterm Strich finden sich vielleicht keine großen Erweiterungen in Java 7 wieder, aber doch
viele kleine Dinge die das Programmierer-Leben einfacher und/oder sicherer machen.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
1.4
1.5 (Java 5)
1.6 (Java 6)
1.7 (Java 7)
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Feb. 2002
Sep. 2004
Dez. 2006
Aug. 2011
Packages
8
23
59
76
135
166
202
209
Klassen
212
504
1520
1842
2723
3279
3777
4024
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 27 / 409
2.3.9 JDK 1.8 – Java 8
Am 18. März 2014 ist Java 8 erschienen. Java 8 brachte sowohl Erweiterungen der Sprache,
der Bibliothek als auch der JVM mit sich: in die Sprache wurden Lambdas, funktionale
Interfaces, Default-Implementierungen und Streams mit parallelen Algorithmen eingeführt. Die
Bibliothek bekam eine vollkommen neue Date-Time-API, viele Concurrency Erweiterungen, und
Swing als GUI-Anteil wurde durch Java FX abgelöst. Unterm Strich sind die Erweiterungen in
Java 8 so umfangreich, dass sie höchstens mit dem Sprung auf Java 5 vergleichbar sind.
JDK Version
1.0
1.1
1.2 (Java 2)
1.3
1.4
1.5 (Java 5)
1.6 (Java 6)
1.7 (Java 7)
1.8 (Java 8)
Datum
Apr. 1995
Feb. 1997
Nov. 1998
Mai 2000
Feb. 2002
Sep. 2004
Dez. 2006
Aug. 2011
März 2014
Packages
8
23
59
76
135
166
202
209
217
Klassen
212
504
1520
1842
2723
3279
3777
4024
4240
Achtung – nehmen Sie die Anzahl der Packages und Klassen nicht zu genau. Die Zählungen
stellen Momentaufnahmen dar. Es ist immer wieder passiert, dass neuere Unterversionen des
JDK (z.B. 1.4.2) neue Klassen und Packages eingeführt haben.
Hinweis – aus Zeitmangel werden viele der mit Java 8 eingeführten Sprach-Konstrukte im
Tutorial nicht oder nur sehr knapp besprochen.
2.4 Technologien
Die JVM enthält einige sehr interessante Technologien, die heutzutage die Leistungsfähigkeit
von Java mitbestimmen und daher zum Erfolg von Java beigetragen haben. Zwei dieser
Technologien möchte ich hier detailierter vorstellen, da sie im bisherigen Kapitel schon
mehrfach erwähnt wurden, und sehr aufschlußreich sind.
• Speicherverwaltung und Garbage-Collector
• Performance und Hotspot
2.4.1 Speicherverwaltung und Garbage-Collector
Die Sprache Jave und die JVM unterstützen eine Technik, die Garbage-Collection genannt
wird. Dies meint einfach nur, dass Sie beliebig Objekte erzeugen können, und sich nicht um die
Freigabe des von den Objekten belegten Speichers kümmern müssen. Dies macht dann der
Garbage-Collector – ein Teil der JVM. Er erkennt nicht mehr benötigte Objekte und gibt deren
Speicher wieder frei. Dies entlastet Sie als Programmierer, da Sie sich um dieses Thema nicht
mehr kümmern müssen – und es verhindert den Quell von Fehlern der mit manueller
Speicherverwaltung verbunden ist. Falls Sie jemals z.B. „C“ programmiert haben, dann wissen
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 28 / 409
Sie sicher, dass Zeiger und manuelle Speicherverwaltung eine Büchse der Pandorra sind,
deren Problemen man kaum Herr wird. All das gibt es in Java nicht – das macht die Sprache
einfacher und die Programme fehlerfreier.
Eigentlich könnte man hier aufhören, denn damit ist das Thema „Speicherverwaltung und
Garbage-Collection“ oberflächlich ausreichend beschrieben. Und uns fehlt die Zeit detailiert in
die heutigen Techniken und Algorithmen von Garbage-Collectoren einzusteigen, obwohl es ein
ungeheuer interessantes und faszinierendes Thema ist. Aber ein paar Dinge möchte ich Ihnen
doch noch mit auf den Weg geben – heutige Garbage-Collectoren wie z.B. im JDK 1.8 arbeiten
ungeheuer effizient, d.h. :
• Sie erkennen zuverlässig nicht mehr referenzierte Objekte.
• Sie arbeiten sehr schnell. Programme mit Garbage-Collections können den gleichen
Programmen mit manueller Speicherverwaltung in der Performance überlegen sein.
• Seit dem JDK 1.5 kann ein Großteil der Garbage-Collector Techniken auch parallel zum
normalen Programmfluß stattfinden – daher die JVM nutzt z.B. neuere Multi-Core Maschinen
sehr gut aus.
Einziger Nachteil heutiger Garbage-Collector Techniken ist, dass sie einen gewissen
Speicheroverhead haben, d.h. mehr Speicher verbrauen als im Idealfall notwendig wäre. Bei
reinen Mark&Copy Garbage-Collectoren10 kann dieser Overhead fast die Hälfe des Speichers
ausmachen – aber auch hier sind heutige JVM’s viel effizienter geworden.
2.4.2 Performance und HotSpot
Ein immer wieder heißes Thema in der Diskussion um Java ist die Performance. Es ist ja auch
scheinbar sehr einleuchtend, dass Byte-Code auf einem in Software geschriebenen Prozessor
nicht so schnell ausgeführt werden kann wie Maschinen-Code, der direkt auf einem echten
Prozessor ausgeführt wird. Aber das ist nur der erste Eindruck – mittlerweile gibt es über 40
Jahre Erfahrung und Ideen im Bau von virtuellen Maschinen, und viele dieser Ideen betreffen
der Performance. Die beiden interessantesten Technologien sind hier JIT-Compiling und
HotSpot.
JIT steht für „Just-in-Time“, d.h. ein JIT-Compiler ist ein Just-in-Time Compiler. Damit ist
gemeint, dass die virtuelle Maschine statt einfach den Byte-Code auszuführen diesen bei der
ersten Ausführung in Maschinen-Code übersetzt und dann dieser direkt vom Prozessor
ausgeführt wird. Da dieser Übersetzungs-Vorgang Zeit dauert, wird die Ausführungs-Dauer
beim Übersetzen natürlich stark gebremst – danach steht aber die native ProzessorPerformance zur Verfügung.
Eine Garbage-Collector Technik, die mit zwei Heaps arbeitet, von denen einer immer leer ist
– d.h. sinnlos verbrauchter Speicher darstellt. Dafür ist die Technik sehr schnell, da sie noch
referenzierte Objekte nur markieren („mark“) und dann umkopieren („copy“) muß, und die
Speicherfreigabe der restlichen Objekte in Nullzeit erledigt, da sie sie einfach auf dem alten
Heap vergißt.
10
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 29 / 409
Prinzipiell ließe sich natürlich der gesamte Java Byte-Code in Maschinen-Code übersetzen,
aber es gibt Bereiche, wo es sich nicht lohnt, der Aufwand sehr hoch ist, oder aus anderen
Gründen nicht wirklich gut praktikabel ist. Außerdem darf auch compilierter Byte-Code nicht die
Sicherheits-Prinzipien von Java wie z.B. die Sandbox verletzen.
Auf der anderen Seite gibt es aber auch Bereiche wo sich mit Informationen aus dem Kontext
des Byte-Codes sehr radikale Optimierungen machen liessen – z.B. wer ruft eine Funktion auf,
bzw. mit welchen Parametern wird sie immer aufgerufen. Und hier kommt HotSpot zum Tragen.
HotSpot ist der Name der JVM seit dem JDK 1.3. Bestandteil von HotSpot ist u.a. ein JITCompiler. Das Besondere an HotSpot ist aber, dass hier die VM das Programm zur Laufzeit
beobachtet, und in Abhängigkeit von diesen Beobachtungen weitere Optimierungen durchführt.
Ein offensichtlicher Hintergrund dieser Vorgehensweise ist die Erkennung sogenannter HotSpots, daher von Programm-Teilen, die besonders häufig ausgeführt werden und bei denen
sich die Optimierung besonders lohnt. Aber HotSpot kann z.B. auch dynamische
Optimierungen durchführen, die die Semantik des Codes verändern, aber im konkret
vorliegenden Kontext funktionieren11.
Hinter HotSpot steht mittlerweile eine Menge Erfahrung und Know-How, und die Optimierungen
werden mit jedem JDK verbessert. Mit JIT-Compiling und den weiteren HotSpot Optimierungen
spielt Java heute von der Performance her auf dem Niveau von normalen compilierten
Sprachen wie z.B. C++ mit. Es gibt sogar ernst zu nehmende Stimmen, die die Meinung
vertreten, dass virtuelle Maschinen viel besser optimieren können als jeder Compiler, da ihnen
viel mehr Informationen zur Verfügung stehen kann.
2.4.2.1 Bemerkung
Eigentlich ist hiermit alles Wichtige gesagt. Natürlich ließe sich noch manches zu den
Techniken von VMs, JIT-Compilern und vor allem HotSpot schreiben – aber eine solche
Vertiefung würde den Rahmen des Tutorials bei weitem sprengen. Ich möchte aber noch ein
bisschen zum Thema Performance von Java sagen, da es scheinbar ein extrem emotional
besetztes Thema mit vielen Gerüchten und Halb-Wahrheiten ist.
Viele Kritiker von Java haben bis heute nicht erkannt bzw. erkennen wollen, dass sich die
Performance von Java seit dem JDK 1.0 immens verbessert hat, und viele ihrer Kritiken heute
nicht mehr zutreffend sind. Auf der anderen Seite gibt es auch eine Menge Java-Fanatiker, die
unreflektiert Java für die performanteste Sprache der Welt halten. Und wenn diese aufeinander
treffen, dann knallt es – manchmal kann das sehr unterhaltsam sein.
Ich kann Ihnen nur empfehlen, wenn Sie mal Langeweile haben – suchen Sie sich z.B. eine
C++ Newsgroup und posten Sie etwas polemisch hinein, dass Java viel schneller ist als C++.
Oder posten Sie umgekehrt in eine Java Newsgroup dass C++ viel schneller als Java ist. Dann
lehnen Sie sich zurüch und genießen den Sprachen-Krieg, den Sie angezettelt haben. An
11
Man nennt solche Optimierungen „Closed-World Optimierungen“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 30 / 409
langen verregneten Abenden kann dies viel unterhaltsamer als jeder Film sein.
Und wo liegt die Wahrheit? Nirgendwo oder überall. Ich kann Ihnen problemlos ein Programm
schreiben, wo C++ Java schlägt, und auch umgekehrt. Aber was sagt das schon aus? Nichts!
Es kann schon unglaublich komplex und schwierig sein, „objektiv“ und reproduzierbar die
unterschiedliche Performance zweier unterschiedlicher Implementierungen der gleichen
Sprache in der gleichen Umgebung zu beurteilen. Viel schwieriger ist es schon, verschiedene
Bibliotheken zu vergleichen. Wie schwierig ist es dann wohl, akzeptable Vergleiche zwischen
zwei Sprachen zu bekommen? Und ist das überhaupt sinnvoll? Man entscheidet sich doch nicht
für oder gegen eine Sprache nur aufgrund der Performance, sondern auch aufgrund von
hunderten anderer Kriterien.
Ich kann Ihnen nur empfehlen: solange Sie nicht wirklich richtig Ahnung von der Materie haben
– halten Sie sich aus der Performance Diskussion raus. Java ist heute, gerade auf heutigen
Rechnern, verdammt schnell – aber das gilt auch für viele andere Sprachen. Und wenn Sie
wirklich mal ein Performance-Problem haben, suchen Sie nach besseren Algorithmen oder
anderen Implementierungen bevor Sie die Sprache wechseln. In fast allen Fällen reicht das
aus. Und wenn das nicht reicht, dann haben Sie wahrscheinlich ein Problem, das ein einfacher
Sprachenwechsel auch nicht so einfach beseitigt.
2.5 Fazit
Wie Sie sehen, ist gerade die Plattform Java in Form der JVM ein faszinierender Ansatz, der
viele Probleme z.B. bzgl. Portabilität oder Speicherverwaltung radikal beseitigt. Hinzu kommt
eine relativ einfache Sprache, mit der sich das restliche Tutorial beschäftigt, und eine
wunderbare riesige Bibliothek, die viele Probleme löst. In Kapitel 4.1.3 werden wir dann noch
einige sehr mächtige Tools kennen lernen, die uns beim Programmieren stark unterstützen und
auch noch frei sind. Programmierer-Herz – was willst du mehr?
Ach ja, es sollte Ihnen klar sein, dass dieses Kapitel bei weitem nicht alle Java Wörter
vorgestellt hat. Wundern Sie sich also nicht, wenn Sie mit welchen konfrontiert werden, die hier
fehlen. Das Java Universum ist groß und wächst jeden Tag – es gibt viel zu entdecken...
3 Mini-Einführung
In Java können als ausführbare Einheiten Programme, Applets, Midlets und Servlets erstellt
werden. Wir werden hier hauptsächlich Programme erstellen. Der Unterschied zwischen
Applets und Anwendungen ist nicht sehr groß, so dass dies im Augenblick keine praktische
Bedeutung hat12.
12
Es gibt Unterschiede in der Art des Einsprungs, der Sicherheit, der Bibliotheken und der
Konsole - siehe todo...
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 31 / 409
Dieses Kapitel stellt einige Grundlagen vor, mit denen Sie ruck-zuck konfrontiert werden, um
überhaupt kleine sinnvolle Programme schreiben zu können, und um die Praktikums-Aufgaben
lösen zu können. Von daher brauchen wir sie, und darum werden sie hier gleich erklärt. Auf der
anderen Seite sind viele dieser Dinge „fortgeschrittenen Themen“, die erst im Laufe der Zeit
kommen. Nehmen Sie sie darum hier einfach hin, und wenden Sie sie pragmatisch an, ohne
zuviel darüber nachzudenken. Im Laufe der Zeit klärt sich alles auf.
3.1 Applikation
3.1.1 Beispiel 1
In einer Java Applikation muss es mindestens eine public-Klasse geben, die folgende KlassenFunktion main enthält:
public class Kap_03_01_Bsp_01_HalloWelt {
public static void main(String[] args) {
System.out.println("Hallo Welt");
}
}
Jede öffentliche Klassen-Funktion („public static“) mit dem Namen „main“, dem Rückgabetyp
„void“, und der Parameterliste „String[ ]“ stellt einen Einsprungpunkt in ein Java Programm dar.
Beim Aufruf der JVM geben Sie eine Klasse an, in der die JVM nach einer solchen KlassenFunktion sucht – und wenn sie gefunden wird, beginnt dort das Programm.
Typischerweise gibt es in einem Java-Programm eine Klasse, die die Anwendung repräsentiert,
eine solche „main“ Funktion hat, und den zentralen Einsprungpunkt in die Anwendung darstellt.
Achtung – der Quelltext der Klasse MUSS in einer Datei mit dem Namen der Klasse selber
und der Extension „java“ stehen. Dies fordert die Sprache! Das gleiche Verfahren gibt es auch
noch bei der Verwendung von Packages. Das obige Beispiel muss also in der Datei
„Beispiel.java“ stehen. Warum dies so ist, wird in Kapitel 4.5 erklärt.
Hinweis – u.a. dieses Beispiel wird im Kapitel 4.3 zur Erklärung der Java-Tools eingesetzt. In
Kapitel 4.3.1.1 können sie es im Editor, als Byte-Code und im laufenden Zustand sehen.
3.1.2 Beispiel 2
Um zum einen schon einen Eindruck von der Einfachheit und Leistunsgfähigkeit von Java zu
bekommen, und zum anderen zu testen, ob ihre Java Installation problemlos funktioniert hat,
hier noch ein zweites einfaches Programm.
import javax.swing.JFrame;
public class Kap_03_01_Bsp_02_Gui {
public static void main(String[] args) {
JFrame frame = new JFrame("Mein erstes GUI Fenster");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 32 / 409
frame.setLocation(200, 200);
frame.setSize(300, 100);
frame.setVisible(true);
}
}
Im Gegensatz zum ersten arbeitet es nicht nur auf der Kommandozeile, sondern öffnet
zusätzlich ein einfaches leeres GUI-Fenster mit dem Titel „Mein erstes GUI Fenster“ – siehe
folgende Abbildung:
Abb. 3-1 : einfaches Beispiel GUI-Fenster mit Java Swing
Hinweis – u.a. dieses Beispiel wird im Kapitel 4.3 zur Erklärung der Java-Tools eingesetzt. In
Kapitel 4.3.1.2 können Sie es im Editor, als Byte-Code und im laufenden Zustand sehen.
Hinweis – erzeugte Objekte (hier „frame“) müssen nicht gelöscht werden. Dies macht die JVM
automatisch. Der entsprechende Vorgang nennt sich Garbage-Collection, und wird in Kapitel
11.4.3 noch mal kurz angesprochen. Je nachdem, von welcher Programmiersprache Sie
kommen, ist dies normal oder ungewöhnlich für Sie. Sprachen wie z.B. C# oder Ruby, haben
auch einen Garbage-Collector, und verhalten sich hier wie Java. Kommen Sie dagegen von
Sprachen wie z.B. Pascal, C oder C++, so ist dieses Verhalten ungewöhnlich, da in diesen
Sprachen der Programmierer Speicher explizit wieder freigeben muss (selbst wenn dies oft
nicht direkt zu sehen ist). Java hat hier den Ansatz des Garbage-Collectors gewählt, der zu
weniger Fehlern und mehr Komfort für den Programmierer führt. Übrigens: Sie müssen
erzeugte Objekte in Java nicht nur „nicht explizit“ löschen, Sie können es natürlich auch gar
nicht. Java hat kein Sprachmittel zu Speicherfreigabe – wozu auch?
3.2 Quelltext
3.2.1 Aufbau
Java Quelltext ist formlos bzw. Block-, Anweisungs- und Token-orientiert aufgebaut – d.h. nicht
zeilen- oder einrückungs-orientiert. Sie können also beliebig Leerzeilen, Leerzeichen,
Tabulatoren und Zeilenumbrüche in ihren Quelltext einfügen, solange sie keine Token
auseinander reißen. Token sind quasi die kleinste syntaktische Einheit von Java, z.B. Symbole
(Namen, Bezeichner,...), Schlüsselwörter (class, import, public,...), Operatoren (+, -, +=, &&,
<<<) und Sonderzeichen ({, }, (, ), [,...)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 33 / 409
3.2.2 Literale: Zahlen, Zeichen und Texte
Literale sind Zahlen-, Zeichen- und Text-Konstanten im Quelltext.
• Integer Literale sind normale Zahlen.
• Gleitkomma-Literale werden an dem Dezimal-Punkt erkannt.
• Einzelne Zeichen-Konstanten einfache
• Zeichenketten-Konstanten (Texte) werden in doppelte Hochkommata eingeschlossen. Sie
müssen spätestens am Zeilenende beendet werden.
2
3.14
.23
2.
'c'
"Hallo"
""
//
//
//
//
//
//
//
Integer Konstante
Fliesskomma Konstante
Fliesskomma Konstante
Fliesskomma Konstante
Zeichen-Konstante
Zeichenketten-Konstante
Leere Zeichenketten-Konstante (Leer-String)
3.2.3 Kommentare
Java unterstützt drei Arten von Kommentaren:
1. Bereichs-Kommentare, die mit /* beginnen und mit */ enden, und dabei beliebige Bereiche
überdecken dürfen – eine Schachtelung ist nicht erlaubt.
2. Zeilenend-Kommentare, die mit // beginnen und für den Rest der Zeile gelten.
3. Spezielle Java-Doc Kommentare, die mit /** beginnen, mit */ enden und spezielle Tags
enthalten – eine Schachtelung ist auch hier nicht erlaubt. Mit dem Tool Java-Doc können
aus diesen Kommentaren automatisch Referenz-Dokumente erzeugt werden. Daher
werden diese Kommentare häufig auch „Java-Doc Kommentare“ genannt. Java-Doc
Kommentare werden in der Vorlesung aus Zeitmangel nicht besprochen.
/*
Dies ist ein Kommentar
*/
double d /* Kommentar */ = 3.1;
int i = 5;
// Noch ein Kommentar
Hinweis – die Nutzung von Java-Doc Kommentaren ist sehr zu empfehlen, da auch aktuelle
Entwickluns-Umgebungen wie z.B. Eclipse oder NetBeans (siehe Kapitel 4.1.3) diese direkt
während der Entwicklung auswerten und z.B. in Views oder Tooltips anzeigen.
3.2.4 Symbole
Java unterscheidet bei Symbolen für z.B. Variablen, Funktionen oder Klassen zwischen Großund Kleinschreibung. „Fenster“, „fenster“ und „FENSTER“ sind drei unterschiedliche Symbole.
Erlaubte Symbole bestehen u.a. aus den Buchstaben ‚a’ - ‚z’, ‚A’ - ‚Z’, Ziffern und dem
Underscore. Hierbei darf das Symbol nicht mit einer Ziffer beginnen. Außerdem sind viele
weitere Unicode Zeichen erlaubt, die Buchstaben und Ziffern darstellen. Sie sollten sie in der
Praxis (zumindest in unserem Sprachraum) aber nicht benutzen. Aber wenn Sie wollen, können
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 34 / 409
Sie in Java also japanische Variablen- und chinesische Funktions-Namen vergeben – ich kann
es aber nicht empfehlen.
3.2.5 Konventionen
In Java existiert die Konvention, dass
• Package-Namen klein beginnen und klein geschrieben werden,
• Klassen- und Interface-Namen groß beginnen und kapitalisiert geschrieben werden,
• Funktions- und Variablen-Namen klein beginnen und kapitalisiert geschrieben werden,
• Klassen-Konstanten GROSS mit Underscores geschrieben werden.
Beispiele:
Package-Namen
Klassen- bzw. InterfaceNamen
Funktions-Namen
Variablen-Namen
(auch lokale Konstanten)
Konstanten-Namen
(nur Klassen-Konstanten)
mypackage
metadataauthoring
MyClass
MetaDataManager
AbstractGuiModel
getBackgroundColor
calculateAverage
pidCount
bitLength
DEFAULT_COLOR
START_WIDTH
Selbst wenn sie jetzt noch keine z.B. Packages oder Interfaces kennen, nehmen sie schon mal
zur Kenntnis, daß es für alle Namen in Java Konventionen bzgl. der Schreibweise gibt, an die
sie sich halten sollten.
Damit können wir nun schlussfolgern, dass in der Zeile:
System.out.println("Java");
System eine Klasse, out ein Attribut von System, und println eine Elementfunktion von out sein
muss.
Achtung – in Java sind diese (und andere) Konventionen wichtig, da viele Mechanismen (z.B.
bei Beans13) und Tools darauf basieren. Sie sollten sich also konsequent daran halten. Dies
betrifft nicht nur die Names-Konventionen, sondern auch andere, die wir noch kennen lernen
werden. Viele moderne Entwicklungs-Umgebungen unterstützen Sie durch Warnungen und
Hinweise bei der Einhaltung dieser Konventionen – siehe z.B. Kapitel 4.6.3.
Java-Beans stellen eine Art wiederverwendbare GUI-Komponenten dar. Aus Zeitmangel
werden wir sie in der Vorlesung nicht besprechen können.
13
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 35 / 409
3.3 Ausgabe
In einer Applikation kann man Zeichenketten, Konstanten, Variablen, usw. auf die Konsole
ausgeben. Dafür existiert in der Klasse „System“ der Ausgabestream (PrintStream) „out“, für
den es u.a. die Element-Funktionen „print“ und “println“ gibt.
System.out.print(<ein Argument>);
System.out.println(<ein Argument>);
// Ist ein 'print' inkl. Zeilenumbruch
Beide Elementfunktionen erwarten einen Argument beliebigen Typs und geben diesen auf der
Konsole aus. Die Elementfunktion „println“ erzeugt zusätzlich zur Ausgabe analog zu „print“
noch einen Zeilenumbruch.
int i = 7;
System.out.print("->");
System.out.println(i);
System.out.println("Java");
System.out.println(42);
Ausgabe
->7
Java
42
Den Funktionen können beliebige Argumente übergeben werden. Im folgenden Beispiel sieht
man das z.B. an der Ausgabe von „System.out“ – die Ausgabe ist sicher nur bedingt sinnvoll
und hilfreich und vor allem ist sie nicht immer gleich (die Zahl hinter dem @ kann anders sein)
– aber sie zeigt beispielhaft, dass sich wirklich jedes Argument in einen String wandeln und
dann ausgeben läßt.
System.out.print(7);
System.out.print('c');
System.out.println(78);
System.out.println("Hallo");
System.out.println(System.out);
// <- es laesst sich wirklich alles ausgeben
Mögliche Ausgabe (die genaue Ausgabe von System.out ist nicht definiert)
7c78
Hallo
java.io.PrintStream@1a7bf11
Es können auch mehrere Elemente gleichzeitig ausgegeben werden: wenn ein Element ein
String ist, wird beim Operator + das andere Element immer automatisch in einen String
umgewandelt und danach beide Strings konkateniert (siehe auch Kapitel 9.1.2).
int i = 7;
System.out.println("Java " + 42 + " " + i);
System.out.println(42 + " Java " + i);
// Ausgabe: Java 42 7
// Ausgabe: 42 Java 7
Ausgabe
Java 42 7
42 Java 7
Bemerkung – die Auswertungsreihenfolge des Operators + ist in Java von links nach rechts
definiert. Und auch die Addition zweier Zahlen entspricht der normalen Erwartung.
int i = 4;
System.out.println(i + 42);
System.out.println(4 + 5 + 7);
© Detlef Wilkening 1997-2016
// Ausgabe: 46
// Ausgabe: 16
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 36 / 409
Ausgabe
46
16
Der Aufruf von „System.out.println“ ohne Parameter erzeugt einfach nur einen Zeilenumbruch,
sprich eine Leerzeile.
System.out.println();
// Erzeugt eine Leerzeile
3.4 Strings
Neben vielen anderen Typen kennt Java auch einen Datentyp für Texte (Zeichenketten). Dies
ist der Typ „String“. String ist kein elementarer Datentyp (vergleiche Kapitel 5.2), kann aber
trotzdem einfach benutzt werden.
Strings werden im Detail im Kapitel 9.1 besprochen. Wir führen sie hier ganz kurz und
pragmatisch ein, da sie der Typ der Kommandozeilen-Argumente (siehe Kapitel 3.5) und der
Rückgabe-Typ beim Einlesen einer Zeile von der Kommandozeile (siehe Kapitel 3.10) sind.
Strings können mit Literalen initialisiert werden, und können einander zugewiesen werden. Ihre
Länge bestimmt man mit der Element-Funktion „length“.
String s = "Java";
System.out.println("Laenge von \"" + s + "\" ist " + s.length());
Ausgabe
Laenge von "Java" ist 4
3.5 Arrays und Kommandozeilen-Argumente
In den Übungs-Aufgaben werden häufiger Kommandozeilen-Argumente benutzt. Sie werden
der main-Funktion in einem String-Array übergeben. Um das Array auswerten zu können,
benötigen Sie folgende Informationen:
• Die Größe des Arrays kann über das Attribut length abgefragt werden – Zugriff über den
Punkt-Operator ‚.‘.
• Der Zugriff auf die Elemente des Arrays geschieht über den Index-Operator [] (die eckigen
Klammern) mit Index in den Klammern.
• Arrays sind in Java null-basiert, d.h. z.B. das erste Element hat den Index ‚0‘
Jedes Element des String-Arrays für die Kommandozeilen-Argumente ist ein String (vergleiche
Kapitel 3.4), und kann als solcher direkt genutzt werden, oder auch einer String Variablen
zugewiesen werden.
public class Example {
public static void main(String[] args) {
if (args.length==0) {
System.out.println("Kein Argument");
return;
}
System.out.println(args.length + " Argument(e)");
System.out.println("- 1. Arg: \"" + args[0] + "\"");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 37 / 409
if (args.length>1) {
String arg2 = args[1];
System.out.println("- 2. Arg: \"" + arg2 + "\"");
}
}
}
Detaillierter werden Arrays in Kapitel 10 und Strings in Kapitel 9.1 besprochen.
Hinweis – der Zugriff auf ein nicht-existentes Array-Element (d.h. Index kleiner Null oder Index
zu groß) führt zu einer Exception – siehe Kapitel 3.9 und Kapitel 10.6.
Hinweis – u.a. ein Beispiel, das diesem sehr ähnlich ist, wird im Kapitel 4.3 zur Erklärung der
Java-Tools eingesetzt. In Kapitel 4.3.4 können Sie es im Editor, als Byte-Code und im
laufenden Zustand “sehen“.
3.6 Klassen
Klassen sind das elementare Strukturierungsmittel von Java.
Pro Datei muss genau eine öffentliche Klasse vorhanden sein, die der Datei ihren Namen gibt.
Eine Klasse wird folgendermaßen definiert:
[Modifizierer] class <klassen-name> {
[Elementfunktionen, Attribute,...]
}
Eine Klasse wird öffentlich, indem sie den Modifizierer „public“ bekommt. Eine minimale
öffentliche Klasse sieht also so aus:
public class Klasse {
}
Die Datei, in der die Klasse definiert ist, muss genauso heißen wie die Klasse - abgesehen von
der Dateiextension ‘.java’. Die öffentliche Klasse „Klasse“ muss also in einer Datei
„Klasse.java“ stehen. Warum dies so ist, wird in Kapitel 4.5 erklärt.
Hinweis – liegt eine Klasse in einem oder mehreren Packages, so muss die Datei in einer
Verzeichnisstruktur liegen, die der Package Struktur der Klasse entsprecht.
Hinweis – detaillierte Informationen zu Klassen finden sich in den Kapiteln 11 und 12.
3.7 Funktionen
Jeder ausführbare Code steht immer in einer Funktion14.
Na gut, genau genommen kann Code auch noch in Block-Initialisierern stehen, aber über
solch speziellen Sprachmittel wollen wir hier noch nicht reden.
14
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 38 / 409
Funktionen sind immer Bestandteil einer Klasse.
Syntax:
Modifizierer <Rückgabetyp> <Fkt-Name> ( <Parameterliste> ) {
<Implementierung>
}
Modifizierer – z.B.: public, private, static, final, ...
Eine Parameterliste ist eine durch Komma getrennte Auflistung von Parametern (sie kann auch
leer sein. Ein Parameter besteht aus Typ und Name – Bsp.:
public static void f() { ... }
protected final int doit(StringBuffer sb) { ... }
private static String calcName(String s, int i) { ... }
Aufruf:
<Fkt-Name>( <Argumentliste> );
Bsp:
f();
calcName("", 1);
Rückgaben können beim Funktions-Aufruf ignoriert werden – siehe im Beispiel der Aufruf der
Funktion „calcName“, die einen String zurückgibt – der beim Aufruf aber ignoriert wird.
Achtung
• Die meisten Funktionen in Java sind sogenannte Elementfunktionen. Sie können nur mit
Objektbezug aufgerufen werden.
• Nur Funktionen mit dem Modifier „static“ (sogenannte Klassenfunktionen) können direkt
benutzt werden.
• Solange wir noch nicht tiefer in Klassen eingestiegen sind, werden alle unsere
selbstgeschriebenen Funktionen Klassen-Funktionen sein – daher den Modifier „static“
enthalten. Bitte denken Sie daran – vergessen Sie das „static“ wird ihr Programm in den
meisten Fällen nicht compilieren.
Hinweis – detaillierte Informationen zu Funktionen finden sich in den Kapiteln 8 und 12.4.
3.8 Packages
Es ist sinnvoll, ihre Programme in Module zu ordnen. Das Sprachmittel hierfür sind Packages.
Um eine Klasse in ein Package zu legen, muss die Datei in einem entsprechenden Verzeichnis
(mit Namen des Package) liegen, und die Datei muß als erste Anweisung (d.h. nicht
Kommentar oder Leerzeilen) eine package-Anweisung enthalten. Ein package-Anweisung
beginnt mit dem Schlüsselwort „package“, dann folgt der Package-Name, und das ganze muß
mit einem Semikolon abgeschlossen sein.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 39 / 409
package mypackage;
Bei verschachtelten Packages müssen auch die Verzeichnisse entsprechend verschachtelt
sein. In der Package-Anweisung werden die Package-Namen dann durch Punkte getrennt.
package mypackage.nocheins.innerespackage;
Analog zu Klassen, deren Namen zu Datei-Namen korrespondieren müssen, müssen also
auch die Package-Namen zu Verzeichnissen korrespondieren. Dies verlangt die Sprache, und
sie müssen sich daran halten. Warum dies so ist, wird in Kapitel 4.5 erklärt.
Hier das „Hallo-Welt“ Beispiel aus Kapitel 3.1.1 in leicht modifizierten Versionen:
• Einmal in einem einfachen Package „pack“.
• Und dann in einem verschachtelten Package „aussen“ in „mitte“ in „innen“.
Beispiel 1 – Achtung, die Datei „Beispiel.java“ muss in einem Verzeichnis „pack“ liegen.
package pack;
public class Beispiel {
public static void main(String[] args) {
System.out.println("Hallo Welt");
}
}
Beispiel 2 – Achtung, die Datei „Beispiel.java“ muss in einem Verzeichnis „innen“ liegen, das in
einem Verzeichnis „mitte“ liegen muss, und das wiederum in einem Verzeichnis „aussen“ liegen
muss.
package aussen.mitte.innen;
public class Beispiel {
public static void main(String[] args) {
System.out.println("Hallo Welt");
}
}
Hinweis – detaillierter werden Packages in Kapitel 13 besprochen.
3.9 Exceptions
Es gibt immer Dinge, die können schief gehen – z.B. eine Eingabe oder ein Array-Zugriff.
Solche Probleme werden in Java immer durch Exceptions gemeldet. Da Sie am Anfang sicher
viele Fehler machen werden (jeder Fehler ist gut, denn er zeigt, dass Sie Java angewendet
haben), werden Sie in Java von Anfang an häufig mit Exceptions konfronitert werden. Im
Augenblick reicht uns – neben dem Thema „Checked-Exceptions“ – siehe gleich – hier das
Wissen, dass mit Exceptions Fehler in unserem Programm angezeigt werden. Irgendetwas
haben Sie falsch gemacht, oder ist einfach schief gelaufen. Am Anfang gehen wir erstmal
davon aus, dass alles gut geht – außer wir wollen den Fehlerfall explizit nutzen, wie z.B. in
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 40 / 409
Kapitel 3.11. Daher könnten wir Exceptions eigentlich erstmal ignorieren, aber...
Aber ein Teil der Exceptions können in Java nicht ignoriert werden, nämlich alle CheckedExceptions. Daher: immer wenn eine Funktion ein „Problem“ mit einer solchen CheckedException melden könnte, dann müssen sie sich darum kümmern. Ein Beispiel dafür ist die
Funktion „read“ in der Anweisung „System.in.read()“ im nächsten Kapitel.
Schreiben Sie im Augenblick die problematische(n) Anweisung(en) einfach in einen try-Block
und fügen noch einen leeren catch-Block an. Außerdem merken sie sich, dass der
Programmfluss im Fehlerfall in den catch-Block verzweigt. Beispiele hierfür finden wir gleich in
den Kapiteln 3.10 und 3.11.
System.out.println("vor try");
try {
System.out.println("vor read");
// "read()" kann schief gehen – d.h. koennte eine Exception werfen
System.in.read();
System.out.println("nach read");
} catch (Exception x) {
// Hierhin verzweigt der Programmfluss im Fehlerfall
System.out.println("Fehlerbehandlung");
}
System.out.println("nach try/catch");
Ein zweiter Fall, in dem wir uns auch im Augenblick schon um Exceptions kümmern müssen, ist
wenn Fehler möglich sind, und diese durch Exceptions gemeldet werden. Dies findet sich z.B.
bei Konvertierungen (siehe Kapitel 3.11) oder bei fehlerhaften Array-Zugriffen (siehe Kapitel 3.5
und Kapitel 4.13).
Hinweis – detaillierter werden Exceptions in Kapitel 22 besprochen.
3.10 Eingabe
Die Eingabe von der Kommandozeile war unter Java lange Zeit recht kompliziert, da mehrere
Streams und Reader miteinander verbunden werden mußten, und Checked-Exceptions beteiligt
waren. Prinzipiell ist der Mechanismus mit den Streams und Readern sehr leistungsfähig – nur
für einen Anfänger und eine so alltägliche Aufgabe nicht angemessen – wir werden Streams
und Reader in Kapitel 23 kennen lernen. Mit dem JDK 1.5 konnte das Einlesen mit Hilfe der
Klasse „Scanner“ und der Element-Funktion „nextLine()“ leicht vereinfacht werden. Seit dem
JDK 1.6 kann direkt auf die Kommandozeile zugegriffen und eine Zeile als String eingelesen
werden.
Das folgende Beispiel (für das JDK 1.6) zeigt, wie man eine Zeile als String von der
Kommandozeile einliest und auswertet. Ignorieren Sie erstmal die Dinge, die wir noch nicht
eingeführt haben wie z.B. die Kontrollstrukturen „while“, „if“ und „break“. Der grundsätzliche
Ablauf des Programms sollte auch so klar sein. Es ist ein Echo-Programm, das alle Eingaben
des Nutzers direkt wieder in doppelten Anführungs-Zeichen auf der Kommandozeile ausgibt –
mit der Information wieviele Zeichen eingelesen wurden.
public class Chapter0310Ex01 {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 41 / 409
public static void main(String[] args) {
System.out.println("Echo-Programm - JDK 1.6");
while (true) {
System.out.print("> ");
String in = System.console().readLine();
if (in.length() == 0) {
break;
}
System.out.println(" \"" + in + "\" - " + in.length() + " Zeichen");
}
System.out.println("Programm-Ende");
}
}
mögliche Ausgabe
Echo-Programm - JDK 1.6
> Hallo
"Hallo" - 5 Zeichen
> Ich lerne jetzt Java
"Ich lerne jetzt Java" - 20 Zeichen
>
Programm-Ende
Nehmen Sie das Beispiel erstmal so hin, falls nicht alles klar ist – im Laufe der Vorlesung
werden sich die einzelnen Fragen dazu aufklären. Und wenn sie Eingabe machen müssen,
benutzen sie das Beispiel ganz pragmatisch als Vorlage und passen es im Rahmen ihrer
Kenntnisse und Bedürfnisse an. Die jeweils eingelesene Zeile findet sich innerhalb der WhileSchleife im String „in“, und kann dann von ihnen genutzt werden.
Achtung – das obige Beispiel funktioniert problemlos auf der Konsole, aber nicht in der
Eclipse. Die Eclipse startet ihr Java-Programm im Hintergrund mit einer Umgebung ohne echter
Konsole. Der Aufruf von „console()“ liefert dort „null“ (siehe Kapitel todo) zurück und erzeugt
dann eine Null-Pointer-Exception. Dies ist ein bekannter Bug in der Eclipse, der auch in Version
4.5.2 (Mars Update 2) noch existiert.
• https://bugs.eclipse.org/bugs/show_bug.cgi?id=122429
• http://stackoverflow.com/questions/104254/java-io-console-support-in-eclipse-ide
Darum sind alle Beispiele in diesem Tutorial noch mit dem folgenden JDK 1.1 Mechanismus
umgesetzt.
Aus historischen Gründen – und da Ihnen dieser Code in der Praxis noch häufig begegnen
könnte (z.B. hier im Tutorial, da ich die Eclipse benutze) – hier auch die Beispiele aus ganz
alten Zeiten (ab JDK 1.1) und für das JDK 1.5. Die Programme machen genau das Gleiche wie
das erste Beispiel – sind nur viel mehr Code und schwerer zu verstehen.
Das Beispiel für das JDK 1.1 zeichnet sich dadurch aus, dass
• es Streams & Reader benutzt (siehe Kapitel 23), und
• eine Checked-Exception abfangen muss, die von „reader.readLine()” geworfen werden
könnte.
import java.io.InputStreamReader;
import java.io.BufferedReader;
public class Chapter0310Ex02 {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 42 / 409
public static void main(String[] args) {
try {
System.out.println("Echo-Programm - JDK 1.1");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
while (true) {
System.out.print("> ");
String in = reader.readLine();
if (in.length()==0) {
break;
}
System.out.println(" \"" + in + "\" - " + in.length() + " Zeichen");
}
} catch (Exception x) {
System.out.println("Unerwarteter Fehler");
}
System.out.println("Programm-Ende");
}
}
Hinweis – da Einlesen schief gehen kann sind hier Exceptions möglich, und hier sind diese
Checked-Exceptions und müssen daher abgefangen werden – vergleiche Kapitel 3.9. Sie
können ja mal den try-catch-Block weg lassen – dann wird der Compiler einen Compiler-Fehler
melden.
Die dritte Implementierung unseres Echo-Programms basiert auf dem JDK 1.5 und nutzt einen
„Scanner“. Da Scanner geschlossen werden müssen und unser Beispiel 100% korrekt sein soll,
müssen wir auch die Fälle berücksichtigen, wo in unserem Programm etwas schief geht und
dieses Problem durch eine Exception gemeldet wird (vergleiche Kapitel 3.9). Dadurch gewinnen
wir wieder etwas Exception-Handling, diesmal in Form eines Try/Finally-Blocks:
import java.util.Scanner;
public class Chapter0310Ex03 {
public static void main(String[] args) {
System.out.println("Echo-Programm - JDK 1.5");
Scanner sc = new Scanner(System.in);
try {
while (true) {
System.out.print("> ");
String in = sc.nextLine();
if (in.length() == 0) {
break;
}
System.out.println(" \"" + in + "\" - " + in.length() + " Zeichen");
}
} finally {
sc.close();
}
System.out.println("Programm-Ende");
}
}
Sie sehen, nutzen Sie moderne JDKs und bleiben beim Code des ersten Beispiels für das
Einlesen von der Kommandozeile.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 43 / 409
3.11 Konvertierungen
In den Übungs-Aufgaben werden ab und zu Konvertierungen von Strings zu Ganz- oder
Gleitkomma-Zahlen benötigt. Diese Konvertierungen können über Klassen-Funktionen der
Klassen „Integer“ und „Double“ vorgenommen werden. Achtung – die Konvertierungen können
natürlich schief gehen – in diesem Fall wirft die Parse-Funktion eine „NumberFormatException“
Exception. Auch dies ist also ein Beispiel dafür, dass Sie sich in Java immer wieder um
Exceptions kümmern müssen – auch wenn wir sie noch gar nicht richtig kennen.
public class Example {
public static void main(String[] args) {
if (args.length==0) {
System.out.println("Kein Argument");
return;
}
System.out.println("Arg: " + args[0]);
try {
int i = Integer.parseInt(args[0]);
System.out.println("i: " + i);
} catch (NumberFormatException x) {
System.out.println(args[0] + " laesst sich nicht in int wandeln");
}
try {
double d = Double.parseDouble(args[0]);
System.out.println("d: " + d);
} catch (NumberFormatException x) {
System.out.println(args[0] + " laesst sich nicht in double wandeln");
}
}
}
Hinweis – die Klassen „Integer“ und „Double“ sind quasi die Objekt-Analogien zu den
elementaren Typen „int“ und „double“. Sie werden z.B. als Wrapper-Klassen für Container
benutzt, und sie bieten allgemeine Hilfs-Funktionen wie z.B. „parseInt“ für den jeweiligen Typ
an. Näheres zu diesen Wrapper-Klassen finden Sie in Kapitel 9.4.
4 Praktikum
4.1 Tools
Für die Entwicklung von Java-Applikationen bzw. Applets werden zwingend ein Editor, ein Java
Compiler und eine virtuelle Java-Maschine (JVM – Java Virtual Machine) benötigt. Es gibt viele
weitere Tools, die das Entwickler-Leben erleichtern können, die aber nicht zwingend notwendig
sind – z.B. Debugger, Test-Werkzeuge, Versions-Verwaltungen, Differ, und und und.
Heutzutage wird ein Java Programm aber typischerweise in einer IDE (Integrierte Entwicklungs
Umgebung) wie Eclipse, NetBeans oder IntelliJ entwickelt. Auch wir werden im Praktikum mit
einer IDE (konkret „Eclipse“) arbeiten – siehe Kapitel 4.1.3 und 4.6. Trotzdem sind auch die
Kommandozeilen-Tools in der Praxis sehr wichtig, und sie fördern ein Verständnis für die
Mechanismen, die in einer IDE unter der Haube ablaufen. Daher wollen wir uns vor den IDEs
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 44 / 409
kurz die JDK Entwicklungs-Tools anschauen – siehe Kapitel 4.1.1 und 4.3.
4.1.1 Das JDK und die Tools „java“ und „javac“
Das JDK (siehe auch Kapitel 2.3) von Oracle enthält neben der eigentlichen Java VM auch
viele Tools zur Programmierung mit Java. Da viele diese Tools reine KommandozeilenWerkzeuge sind, ist ihre Benutzung – gerade für den Anfänger – nicht besonders einfach und
komfortabel. Dafür sind sie kostenlos u.a. im Internet erhältlich, und können sowohl privat als
auch komerziell genutzt werden.
Die einfachste Lösung zur Entwicklung von Java-Programmen wäre also ein beliebiger Editor
mit den Kommandozeilen-Werkzeugen „javac“ und „java“ von Oracle.
• Der Java-Compiler „javac“.
Mit diesem Programm wird aus dem oder den Java-Quelltexten der Java Byte-Code erzeugt.
Java Byte-Code hat immer die Extension „.class“.
• Die Java virtuelle Maschine („JVM“) „java“.
Mit diesem Programm wird der Java Byte-Code ausgeführt.
Sie benötigen z.B. das aktuelle Java SDK: J8SE SDK (Java 8 Standard Edition – Software
Development Kit) – die aktuelle Version ist 1.8.0_u77, d.h. JDK 1.8 Update 77 (Stand Anfang
April 2016). Es empfiehlt sich auf jeden Fall auch die „J2SE 8.0 Documentation“ mit
herunterzuladen. Achtung – die Screenshots beziehen sich noch auf Version 1.8.0_u40.
Homepage bzw. Download-Adresse
• http://www.oracle.com/de/index.html
• http://www.oracle.com/de/technologies/java/index.html
• http://www.oracle.com/technetwork/java/javase/downloads/index.html
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 45 / 409
Abb. 4-1 : Oracle Homepage – Einstieg in den Download des JDK
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 46 / 409
Abb. 4-2 : Oracle Java Download Seite – JDK Auswahl
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 47 / 409
Abb. 4-3 : Oracle JDK Download Seite – Plattform auswählen und downloaden
Zusätzlich zum aktuellen JDK sollten Sie immer auch die aktuelle Java-Dokumentation auf der
Oracle Java Download Seite (weiter unten) herunterladen. Und wenn Sie mit JavaFX arbeiten –
siehe Kapitel 16 – dann auch die JavaFX Dokumentation.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 48 / 409
Abb. 4-4 : Oracle Java Download Seite – JDK Dokumentation Auswahl
Abb. 4-5 : Oracle JDK Doku Download Seite – Doku auswählen und downloaden
Hinweis – auf der Oracle Download Seite können Sie übrigens auch in einem immer die jeweils
aktuelle NetBeans IDE mit herunterladen – siehe auch Kapitel 4.1.3.2.
Nach der Installation des JDK steht Ihnen die JVM direkt z.B. auf der Kommandozeile zur
Verfügung und kann mit „java“ aufgerufen werden. Mit der Option „-version“ z.B. gibt die JVM
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 49 / 409
nur ihre Version aus und beendet sich dann direkt. Die Ausführung von echten Java
Programmen auf der Kommandozeile lernen wir in Kapitel 4.3.
Abb. 4-6 : Installiertes JDK – Aufruf der JVM
Wollen Sie auch den Java-Compiler „javac“ und die anderen JDK-Tools direkt nutzen können,
so müssen Sie z.B. unter Windows in der System-Steuerung den Pfad zu dem Bin-Verzeichnis
Ihrer JDK-Installation der Umgebungs-Variablen „path“ hinzufügen. Bei einer „normalen“
Installation ist dies unter einer 64 Bit Windows Version: „C:\Program
Files\Java\jdk1.8.0_77\bin“. Wenn Sie dies gemacht haben, dann können Sie auch den JavaCompiler „javac“ direkt auf der Kommandozeile aufrufen – z.B. auch mit der Option „-version“.
Abb. 4-7 : Aufruf des Java-Compilers nach gesetzter Path Umgebungs-Variable
Haben Sie das JDK Bin-Verzeichnis nicht in den Pfad aufgenommen, dann müssen Sie beim
Aufruf den kompletten Pfad-Namen inkl. Extension mitangeben:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 50 / 409
Abb. 4-8 : Aufruf des Java-Compilers mit vollem Pfad
Werden Sie hauptsächlich oder vielleicht ausschließlich mit einer IDE arbeiten, so lohnt sich
das Setzen der Umgebungs-Variablen nicht. Arbeiten Sie häufig auf der Kommando-Zeile, so
rentiert sich das Setzen sehr schnell.
Hinweis – die Nutzung der JDK Tools ist in Kapitel 4.3 beschrieben.
Die hoffentlich mit heruntergeladene Java-Dokumentation müssen Sie nur entpacken. Darin
finden Sie mit „index.html“ den Einstiegs-Punkt in die sehr umfangreiche Dokumentation:
• Die Java Dokumentation „jdk-8u77-docs-all.zip“ entpackt sich nach „docs“
• Die JavaFX Dokumentation „javafx-8u77-apidocs.zip entpackt sich nach „api“
Abb. 4-9 : Entpackte Java Dokumentation
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 51 / 409
Abb. 4-10 : Java Dokumentations Einstieg „index.html“
4.1.2 Editor ConTEXT oder Notepad
Für das Arbeiten ohne IDE – was in der Praxis immer wieder sinnvoll ist – empfiehlt sich ein
guter Editor. Während die typische Linux Distribution meist einige sehr mächtige Editoren
enthält, bringt Windows von sich aus nur den Editor „Notepad“ mit, der leider nicht
empfehlenswert ist. Zum Glück gibt es für Windows viele freie Editoren – ganz gut leben kann
man z.B. mit:
• ConTEXT
http://www.contexteditor.org/index.php
• Notepad++
http://notepad-plus-plus.org/
• jedit
http://www.jedit.org/
Aber zweifelsfrei gibt es viele weitere empfehlenswerte Editoren unter Windows.
4.1.3 IDEs
Natürlich gibt es für die Java Entwicklung auch integrierte grafische Entwicklungs-Umgebungen
(IDEs15), die alle notwendigen Werkzeuge (Editor, Compiler, JVM) und viele weitere Tools und
Hilfen unter einem Dach zusammenfassen. Die Benutzung dieser Umgebungen ermöglicht ein
weitaus schnelleres und komfortableres Arbeiten, da viele Arbeiten automatisiert sind bzw. in
einer grafischen Umgebung erfolgen können. Ein Teil dieser IDEs sind als „Personal Version“
für den privaten Gebrauch kostenlos erhältlich. Andere sind komplett frei, d.h. auch für den
komerziellen Gebrauch – darunter fallen z.B. die IDEs Eclipse von IBM oder Net-Beans von
Oracle, die beide sehr empfehlenswert sind.
15
Integrated Development Environment
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 52 / 409
4.1.3.1 Eclipse
Eclipse etabliert sich im Augenblick immer mehr als die zentrale Open-Source Umgebung für
die Java Entwicklung. Dies liegt u.a. daran, dass allein IBM über 160 Entwickler16 für die
(Weiter-) Entwicklung von Eclipse einsetzt, und sie trotzdem kostenlos zur Verfügung stellt.
Außerdem gibt es eine große Eclipse Community17, die viele Erweiterungen (Plugins) und Hilfen
bereit stellt. Unterm Strich ist Eclipse mittlerweile ein kleines Monster, eine IDE mit einer
unglaublichen Vielzahl an Möglichkeiten und Features.
Wir werden Eclipse im Praktikum benutzen – die aktuelle Version ist 4.5.2 Mars (Stand Anfang
April 2016). Achtung – die Screenshots beziehen sich noch auf die ältere Eclipse Version 4.4.2
Luna.
Homepage bzw. Download-Adresse
• http://www.eclipse.org
• http://www.eclipse.org/downloads/
Achtung – die Eclipse IDE gibt es in mehreren Varianten. Für den Anwendungsfall der
Vorlesung, d.h. für die Entwicklung von Java Applets und Java Applikationen für den Desktop
ist die „Eclipse IDE for Java Developers“ die richtige Wahl.
Abb. 4-11 : Eclipse Homepage – Wechsel zur Download Seite
Darunter so berühmte Leute wie Erich Gamma, den Autor des Buchs „Entwurfsmuster“ –
siehe Kapitel todo.
17 Dies äußert sich in Deutschland z.B. dadurch, dass es eine eigene Eclipse Zeitschrift gibt,
oder auch Konferenzen zum Thema Eclipse.
16
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 53 / 409
Abb. 4-12 : Eclipse Download-Seite – Eclipse IDE for Java Developers
Nach der Installation von Eclipse müssen Sie die heruntergeladene Zip-Datei nur auspacken. In
dem Ziel-Verzeichnis finden Sie dann die Datei „eclipse.exe“, mit der Sie die Eclipse starten
können. Die Nutzung der Eclipse ist in Kapitel 4.6 beschrieben.
Hinweis – Eclipse selber ist in Java geschrieben. Sie benötigen also ein installiertes JRE auf
Ihrem Rechner. Da Sie ja mit Java entwickeln wollen, sollten Sie also das aktuelle JDK
installiert haben – und das enthält auch das jeweilige JRE.
4.1.3.2 NetBeans
Natürlich hat auch Eclipse seine Schwächen, und andere IDEs und Tools ihre Stärken. Die
Vorlesung möchte keine Werbung für Eclipse sein. Schauen Sie sich ruhig die Alternativen gut
an, und entscheiden Sie nach Ihren eigenen Vorstellungen und Wünschen. Eine vergleichbare
Alternative mit manchen Vor- aber auch Nachteilen ist sicher NetBeans von Oracle – URL
siehe Kapitel 4.1.1.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 54 / 409
Hinweis – NetBeans selber ist in Java geschrieben. Sie benötigen also ein installiertes JRE auf
Ihrem Rechner. Da Sie ja mit Java entwickeln wollen, sollten Sie also das aktuelle JDK
installiert haben – und das enthält auch das jeweilige JRE.
4.2 Source Organisation
Bevor wir unsere ersten Programme übersetzen und laufen lassen, noch einmal die folgenden
sehr wichtigen Hinweise:
• Die gesamte programmierte Logik in einem Java Programm spielt sich in Klassen ab.
• Jeder Java Quelltext muss genau eine public Klasse (oder Interface) enthalten.
• Der Name der Klasse muss dem Namen der Quelldatei entsprechen (inkl. Groß- und
Kleinschreibung). Die Datei muss zusätzlich die Extension „.java“ haben.
- Beispiel: Die public Klasse „Pop3Protocol“ muss zwingend in einer Datei
„Pop3Protocol.java“ stehen.
• Liegt die Klasse in einem Package, so muss die Datei in einem Verzeichnis mit dem
entsprechenden Package Namen liegen (auch hier gilt natürlich wieder Gross- und
Kleinschreibung).
- Beispiel: Liegt die public Klasse „TestCase“ in einem Package „applicationlogic“, so muss
die Datei „TestCase.java“ zwingend in einem Verzeichnis „applicationlogic“ liegen.
- Beispiel: Liegt die public Klasse „SeqNode“ in den Packages „db“ und „implementation“
(d.h. das Package „db“ enthält das Package „implementation“), so muss die Datei
„SeqNode.java“ in einem Verzeichnis „implementation“ liegen, und dieses wiederum in
einem Verzeichnis „db“.
• Der Grund für die Korrelation von Klassen- und Datei-Namen, bzw. von Package- und
Verzeichnis-Namen wird in Kapitel 4.5 erklärt.
Die Korrelation von Klassen- zu Datei-Namen, und Package- zu Verzeichnis-Namen wurde
zwar schon in den Kapiteln 3.1 und 3.8 erwähnt, aber die Erfahrung zeigt, dass dies ein ganz
typischer Anfänger-Fehler ist. Darum sollte es hier noch mal erwähnt sein. Passen Sie also
darauf auf. In den letzten Jahren waren sicher die Hälfte der Probleme in den ersten
Praktikumswochen auf diesen Fehler zurückzuführen.
4.3 Benutzung der JDK Entwicklungs-Tools
Für die Beispiele hier wird davon ausgegangen, dass unter Windows gearbeitet wird, das
aktuelle JDK 1.8 installiert ist, und für z.B. den Java-Compiler das JDK Bin-Verzeichnis in die
Path Umgebungs-Variable eingetragen wurde – siehe Kapitel 4.1.1.
4.3.1 Java mit Klassen ohne Packages
Liegen die Java Klassen in keinem Package, so ist die Benutzung der Kommandozeilen-Tools
sehr einfach. Schauen wir uns die die Schritte zur Verarbeitung und Ausführung des „Hallo-
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 55 / 409
Welt“ Beispiels aus Kapitel 3.1.1 an.
Achtung – Klassen, die in keinem Package liegen, sollten die absolute Ausnahme sein. Sie
sind eine schnelle Lösung für kleine Test- oder Beispiel-Programme – von daher werden Sie
solche viel in diesem Tutorial finden. Für mehr sollte man sie aber nicht nutzen. Später – wenn
Sie reale Programme schreiben – sollten Sie für alle ihre Programme eine sinnvolle DateiOrganisation anlegen, die sich dann in den Packages und Verzeichnissen widerspiegelt.
4.3.1.1 „Hallo Welt“ Beispiel
Erstellen Sie mit einem Editor den Quelltext des „Hallo-Welt“ Java Programms aus Kapitel 3.1.1
– diesmal aber mit dem Klassen-Namen „HalloWelt“, und speichern Sie ihn unter dem Namen
„HalloWelt.java“ ab. Bei mir liegt die Datei z.B. direkt unter „E:\java“.
Abb. 4-13 : „Hallo Welt“ Quelltext im Editor (Datei „e:\java\HalloWelt.java“)
Achtung – passen Sie auf, dass die Klasse genauso wie die Datei heißt (bis auf die zusätzliche
Extension „.java“) – die Sprache Java fordert dies, siehe Kapitel 3.6.
Öffnen Sie jetzt eine Kommandozeile für dieses Verzeichnis.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 56 / 409
Abb. 4-14 : Kommandozeile für das Verzeichnis „e:\java“
Rufen Sie den Java Compiler „javac“ mit Angabe der zu übersetzenden Datei auf. In
Abhängigkeit von ihrer Konfiguration müssen Sie entweder den Pfad zum Compiler mit
angeben, oder können ihn weglassen – siehe Kapitel 4.1.1. Bei mir ist der Pfad zu den JDK
Entwicklungs-Tools in der Pfad-Umgebungsvariable enthalten – ich brauche den Pfad also
nicht anzugeben:
> javac HalloWelt.java
Der Java Compiler compiliert die angegebene Datei und erzeugt den entsprechenden ByteCode (Datei „HalloWelt.class“) im aktuellen Verzeichnis.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 57 / 409
Abb. 4-15 : Der Java Compiler erzeugt den Byte-Code des „Hallo Welt“ Beispiels
Diesen müssen wir jetzt in der virtuellen Java Maschine (JVM) ausführen. Dazu rufen wir die
virtuelle Maschine „java“ auf, und übergeben den vollständigen Namen der Klasse18.
> java HalloWelt
Abb. 4-16 : Die virtuelle Java Maschine (JVM) führt das „Hallo Welt“ Beispiel aus
Nicht den Namen der Class-Datei oder den Namen der Quelltext-Datei, sondern wirklich den
vollständigen Namen der Klasse. Dies impliziert, dass bei Klassen die in Packages liegen
(siehe Kapitel 4.3.2.2) natürlich auch alle Packages mit angegeben werden müssen.
18
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 58 / 409
4.3.1.2 GUI Beispiel
Vergleichbar zum „Hallo-Welt“ Beispiel läuft Handling des GUI Beispiels aus Kapitel 3.1.2 ab.
Abb. 4-17 : GUI Quelltext im Editor (Datei „e:\java\Gui.java“)
Abb. 4-18 : Der Java Compiler erzeugt den Byte-Code des GUI Beispiels
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 59 / 409
Abb. 4-19 : Die virtuelle Java Maschine (JVM) führt das GUI Beispiel aus
4.3.1.3 Mehrere Klassen gleichzeitig compilieren
Wollen Sie mehrere Dateien auf einmal übersetzen, so können Sie den Wildcard „*“ im DateiNamen benutzen. Löschen Sie z.B. die beiden gerade erzeugten Class-Dateien, und
compilieren Sie unsere beiden Beispiel-Quelltexte „HalloWelt.java“ und „Gui.java“ in einem
Rutsch neu:
> javac *.java
Abb. 4-20 : Der Java Compiler erzeugt den Byte-Code aller Klassen im Verzeichnis
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 60 / 409
4.3.2 Java mit Klassen in Packages
Liegen die Klassen in Packages, so wird das ganze etwas aufwändiger, aber nicht wirklich
kompliziert. Um das zu lernen, schreiben wir ein Beispiel ähnlich dem „Hallo-Welt“ von eben,
nur dass die Klasse jetzt in den Packages „aussen.mitte.innen“ liegt. Natürlich legen wir die
Datei korrekt in die Verzeichnis-Struktur „aussen\mitte\innen“19.
Abb. 4-21 : Package Quelltext im Editor (Datei „PackageBeispiel.java“)
Damit das Beispiel auch später noch nutzbar ist, wenn wir die Source- und Class-Path
Optionen erklären (siehe Kapitel 4.3.2.3 und Kapitel 4.5), legen wir die Verzeichnis-Struktur
„aussen\mitte\innen“ nicht direkt in „e:\java“ an, sondern „betten“ sie noch zusätzlich in ein
Zwischen-Verzeichnis „Sourcen“ ein. So sieht unsere Struktur dann aus:
Abb. 4-22 : Verzeichnis-Struktur für das Package Beispiel
Erinnern sie sich? Packages müssen mit den Verzeichnissen korrespondieren – siehe Kapitel
3.8 und todo.
19
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 61 / 409
4.3.2.1 Einfache, aber falsche Vorgehensweise
Wenn Sie jetzt einfach die Vorgehensweise von eben übertragen, erleben Sie eine kleine
Überraschung. Aber warum nicht – probieren wir es einfach mal aus:
• Wir wechseln mit der Konsole in das Verzeichnis „innen“
• Wir rufen den Java Compiler auf
• Und wir starten das Programm...
Abb. 4-23 : Der Java Compiler erzeugt den Byte-Code des Package Beispiels
Wie wir sehen, compilierte der Compiler den Quelltext ganz problemlos, und der Byte-Code
liegt damit vor. Also starten wir das Programm mal:
Abb. 4-24 : Fehler bei der Ausführung des Package Beispiels
Was ist das? Unser Programm startet nicht – statt dessen meldet uns die JVM einen Fehler.
Um das Problem vollständig zu verstehen, fehlt uns noch etwas Wissen. Im Augenblick soll die
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 62 / 409
Bemerkung reichen, dass wir versucht haben, den Compiler und vor allem die JVM zu
betrügen. Beim Starten des Programms erzählen wir der JVM von einer Klasse „Beispiel“ ohne
Packages, die aber in Packages liegt.
Belassen wir es dabei – keine Details, die kommen später – lernen wir lieber, wie man es richtig
macht. Aber vorher löschen Sie den fehlerhaft erzeugten Byte-Code „PackageBeispiel.class“,
bevor er uns noch mehr Ärger macht.
4.3.2.2 So ist es richtig
Statt in das Verzeichnis mit dem Java Quelltext zu wechseln, müssen Sie bei der Übersetzung
das Wurzel-Verzeichnis für die Quelltexte angeben. Das Wurzel-Verzeichnis ist quasi das
Verzeichnis, das das äußere Package enthält, bei uns also „e:\java\Sourcen“.
Wechsel wir also zurück in unser Java-Verzeichnis. Wären wir dort also einfach mit unserer
Konsole geblieben, hätten wir uns viel vergebene Arbeit sparen können. Zurück in „e:\java“
werfen wir den Compiler an, und geben jetzt beim Compiler-Aufruf zwei weitere Optionen an:
• Die Option „-sourcepath“ gibt das (oder die) Wurzel-Verzeichnis Ihrer zu übersetzenden
Klassen an – in unserem Beispiel „Sourcen“ als relative Pfad-Angabe oder „e:\java\Sourcen“
als absolute Pfad-Angabe.
• Die zu übersetzende Java-Datei (oder die Java-Dateien) müssen wir jetzt natürlich inkl. Pfad
angeben.
> javac -sourcepath Sourcen Sourcen\aussen\mitte\innen\PackageBeispiel.java
Abb. 4-25 : Der Java Compiler erzeugt den Byte-Code mit „sourcepath“ Option
Hinweise:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 63 / 409
• Natürlich kann man auch hierbei Wildcards benutzen, um alle Dateien gleichzeitig zu
compilieren.
• Stehen Sie während des Compilierens im Wurzel-Verzeichnis Ihres Programms, so geben
Sie als Soure-Path einfach nur den Punkt „.“ an, der ja für das aktuelle Verzeichnis steht.
Für die Ausführung wechseln wir nun in das Wurzel-Verzeichnis unseres Programms, d.h. in
„e:\java\Sourcen“. Wir werden gleich noch lernen, wie man das Programm aus einem
beliebigen Verzeichnis ausführt – aber erstmal einfach:
Beim Aufruf der JVM muss der vollständige Klassen-Name der Main-Klasse mitgegeben
werden. Das war auch eben schon so, aber eben lag die Klasse nicht in einem Package – von
daher war der einfache Klassen-Name auch gleichzeitig der vollständige Klassen-Name. Jetzt,
mit Packages, ist der vollständige Klassen-Name der Klassen-Name inkl. Package-Namen –
getrennt durch Punkte, d.h.: „aussen.mitte.innen.PackageBeispiel“.
> cd Sourcen
> java aussen.mitte.innen.PackageBeispiel
Abb. 4-26 : Korrekte Ausführung des Package Beispiels
4.3.2.3 Und was, wenn man nicht im Wurzel-Verzeichnis steht?
Lassen Sie uns wieder zurück in unser Java-Verzeichnis „e:\java“ wechseln. Nun stehen wir
nicht mehr im Wurzel-Verzeichnis unseres Programms – wie starten wir es denn nun? Wenn
wir hier die JVM mit der Main-Klasse aufrufen bekommen wir nur einen Fehler:
Abb. 4-27 : Fehler bei Ausführung außerhalb des Wurzel-Verzeichnisses
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 64 / 409
Das war auch zu erwarten. Woher soll die JVM wissen, wo unser Programm auf der Platte liegt.
Wir stehen quasi in einem beliebigen Verzeichnis – und in einem beliebigen anderen
Verzeichnis liegt unser Programm.
In diesem Fall, muß man – analog zur Option „-sourcepath“ beimm Compiler – nun der JVM
das Wurzel-Verzeichnis angeben. Nur heißt die Option hier „-classpath“ oder kurz „-cp“, da sich
die JVM nicht mehr für die Sourcen interessiert, sondern statt dessen den Klassen-Pfad für den
Byte-Code kennen muss.
> java -cp Sourcen aussen.mitte.innen.PackageBeispiel
Abb. 4-28 : Korrekte Ausführung des Package Beispiels mit Class-Path Option „-cp“
Achtung – genauso wie die Sourcen definiert heißen und an genau definierten Stellen (relativ
zum Source-Wurzel-Verzeichnis) liegen müssen, so muß auch der Byte-Code genau definiert
heißen und relativ zu einer Klassen-Pfad-Wurzel genau definiert liegen. Also benennen Sie
niemals Byte-Code um bzw. verschieben Sie ihn auch nicht, bzw. nur sehr bewußt.
4.3.3 Trennung von Sourcen und Byte-Code
Nun ist die Vermischung von Source-Code und Byte-Code im gleichen Verzeichnis keine gute
Idee – oder sagen wir allgemein: die Vermischung von User-erzeugten und automatisch
generierten Dateien. Schön wäre es, wenn der Java-Compiler den Byte-Code in ein extra
Verzeichnis legen würde. Dies kann mit der Option „-directory“ oder kurz „-d“ erreicht werden.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 65 / 409
Abb. 4-29 : Ausgangs Struktur für Byte-Code eigene Verzeichnisse
Achtung, - das Ausgabe Verzeichnis (hier „output“) muss existieren.
Beispielhaftes Compilieren aus dem Verzeichnis „e:\java“ heraus.
> javac -sourcepath sourcen -d output sourcen\aussen\mitte\innen\*.java
Abb. 4-30 : Der Java Compiler erzeugt den Byte-Code mit Trennung des Byte-Codes
Der Compiler erzeugt nun beim Compilieren nicht nur den Byte-Code, sondern auch die den
Packages bzw. dem Source-Path entsprechende Verzeichnis-Struktur. D.h. im AusgabeVerzeichnis liegt nicht einfach die Byte-Code Datei „PackageBeispiel.class“. Statt dessen hat
der Compiler auch die Verzeichnisse „aussen“, „mitte“ und „innen“ angelegt, und erst im
Verzeichnis „innen“ liegt der Byte-Code.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 66 / 409
Abb. 4-31 : Erzeugte Verzeichnis Struktur und Byte-Code
Denken Sie jetzt beim Starten des Programms nur daran, dass Sie den Class-Path (hier z.B.
„output“), und nicht den Source-Path (hier z.B. „sourcen“) angeben müssen. Die Sourcen und
der Byte-Code liegen jetzt ja in zwei unterschiedlichen Verzeichnis-Strukturen.
> java -cp output aussen.mitte.innen.PackageBeispiel
Abb. 4-32 : Korrekte Ausführung des Package Beispiels mit Trennung des Byte-Codes
Hinweis – wenn Sie so arbeiten, d.h. mit der Trennung des Source-Codes vom Byte-Code,
dann müssen Sie immer nur Ihr Source-Verzeichnis sichern, und haben damit immer alles im
Griff. Der Byte-Code läßt sich ja jederzeit wieder herstellen.
4.3.4 Kommandozeilen-Argumente
Sie können an ein Java Programm Argumente übergeben, indem Sie diese als letzte
Argumente – d.h. nach dem Klassen-Namen – an die JVM übergeben. Unser Beispiel ist der
Einfachheit halber ohne Packages, und lehnt sich an das Beispiel aus Kapitel 3.5 an. Achtung,
das Beispiel soll mit zwei Kommandozeilen-Argument aufgerufen werden:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 67 / 409
Abb. 4-33 : Kommandozeilen Argument Quelltext im Editor (Datei „e:\java\Args.java“)
> javac Args.java
> java Args
> java Args Hallo
> java Args Hallo Java
Abb. 4-34 : Compilation und Ausführung des Kommandozeilen Argumente Beispiels
4.4 Projekt Tools
Nehmen wir mal an, Sie entwickeln ein großes Projekt, mit vielen Packages und noch mehr
Klassen. Das neben Quelltexten auch noch viele andere Dinge wie z.B. Icons, Bilder, Videos
und lokalisierte Textdateien enthält. Und wo es nicht nur Class-Dateien, sondern auch andere
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 68 / 409
Dinge erzeugt werden, wie z.B. Jar-Dateien – und sei es nur aus Gründen der Testbarkeit.
Dann wird diese Vorgehensweise – jedes Package händisch auf der Kommandozeile zu
übersetzen – viel zu arbeits- und fehlerintensiv.
Eine Lösung ist natürlich die Verwendung einer IDE wie z.B. Eclipse, in der alle diese Dinge mit
einem hohen Grad an Automation erledigt werden können. Aber möglicherweise wollen oder
können Sie keine IDE verwenden. Oder – und das spiegelt die Praxis in typischen Projekten
eher wider – Sie müssen neben der IDE auch noch eine Kommandozeilen-Lösung
unterstützen20.
In diesem Fall ist das typische Werkzeug im Java Umfeld „Ant“. Die fleissige Ameise Ant ist ein
in Java geschriebenes Open-Source Tool21, mit dem komplette Builds beschrieben und
automatisiert durchgeführt werden können – solange auf dem Build-System eine JVM zur
Verfügung steht. Für Details und weitere Informationen sei hier auf das Internet verwiesen –
siehe z.B. http://jakarta.apache.org/ant.
Ein zweites im Java Umfeld sehr verbreitetes Build-Tool ist Maven (http://maven.apache.org/ ),
aber es existieren noch viele weitere Build-Tools.
Zusätzlich empfehle ich Ihnen neben einem Build-Tool auf jeden Fall eine Versions-Verwaltung
für Ihre Quelltexte – VCS – Version-Control-Systems. Leider sprengen detailierte Informationen
zu VCS den Rahmen des Java Tutorials, lesen Sie z.B. in Wikipedia nach:
http://de.wikipedia.org/wiki/Versionsverwaltung. Folgende Versions-Verwaltungen sind die wohl
zur Zeit verbreitesten Systeme:
• CVS
http://savannah.nongnu.org/projects/cvs
• Subversion – SVN
http://subversion.apache.org/
• Mercurial
http://mercurial.selenic.com/
• Git
http://git-scm.com/
• Perforce
http://www.perforce.com/
4.5 Source-Path und Class-Path
4.5.1 Source-Path
Was sollen eigentlich beim Compilieren und Ausführen diese komischen Source- und ClassZ.B. für automatische Builds jede Nacht, oder für Builds auf zentralen Servern für die keine
IDE vorhanden ist.
21 Ant ist ein Teil des Jakarta Projekts.
20
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 69 / 409
Path Angaben. Schauen wir uns dafür mal den Source-Path beim Compilieren an, und nehmen
wir an, wir haben in einem Projekt die zwei Klassen „Class1“ und „Class2“ in zwei parallel
liegenden Packages „pack1“ und „pack2“.
pack1
pack2
Class1
Class2
Abb. 4-35 : 2 Klassen in zwei parallelen Packages
Selbst wenn wir noch nicht viel über Klassen und Packages wissen, sollte uns klar sein, dass
es passieren kann (und auch soll) dass eine Klasse die andere nutzt.
package pack1;
public class Class1 {
private pack2.Class2 cl;
}
// Class1 nutzt Class2 aus dem Package pack2
Klar sollte uns auch sein, dass der Compiler überprüft, ob es die Klasse z.B. überhaupt gibt, ob
„Class1“ sie benutzen darf, oder ob die Benutzung korrekt erfolgt (z.B. entsprechende
Funktionen in der Klasse vorhanden sind)22.
Woher weiss der Compiler aber nun, wo die Klasse „pack2.Class2“ zu finden ist, um diese
Dinge zu überprüfen? Hier kommt drei Dinge ins Spiel:
1) Eine Klasse heißt wie die Datei, in der sie steht.
2) Ein Package heißt wie das Verzeichnis, das es darstellt.
3) Der Source-Path zeigt auf die Wurzel(n) der Package-Struktur.
pack1
Class-Path == d:\source
d:\
source
pack1
Class1.java
pack2
Class2.java
Class1
pack2
Class2
Abb. 4-36 : Klassen, Dateien, Packages, Verzeichnisse und der Source-Path
Der Compiler findet im Quelltext also die Referenzierung der Klasse „pack2.Class2“ und weiß
In Java werden solche Dinge zum Compile-Zeitpunkt vom Compiler gecheckt. Damit können
Fehler schon zur Compile-Zeit gefunden werden, die ansonsten zu Fehlern zur Laufzeit führen.
Es gibt Sprachen, die dies nicht so machen – und die Fehler für den Benutzer aufsparen.
22
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 70 / 409
sofort folgende Dinge:
1) Das Package heißt „pack2“ – also muss es im Source-Path Verzeichnis als der Wurzel aller
Sourcen ein Verzeichnis mit Namen „pack2“ geben.
2) Die Klasse heißt „Class2“, also muss es in diesem Verzeichnis eine Datei „Class2.java“
geben, die die Klasse „Class2“ enthält und detailiert definiert.
Mit diesem Wissen kann er also die Definition der Klasse „Class2“ finden, sie einlesen, und alle
notwendigen Überprüfungen durchführen.
Hinweis – in Wirklichkeit kann da noch ein bisschen mehr passieren, da z.B. der Source-Path
mehrere Verzeichnisse umfassen kann, oder der Compiler auch mit Class-Dateien und JarFiles umgehen kann. Aber auch da passiert im Prinzip genau das gleiche wie hier beschrieben.
Der Source-Path hilft dem Compiler also referenzierte Klassen zu finden, um seinen CompileJob mit möglichst vielen Überprüfung durchführen zu können.
4.5.2 Class-Path
Und was ist mit dem Class-Path? Das ist genau das gleiche für die virtuelle Maschine. Der
Class-Path beschreibt, wo der Byte-Code der Klassen zu finden ist. D.h. referenziert er die
Wurzel-Verzeichnisse für die Class-Dateien. Und daher müssen auch die Class-Dateien in
korrespondierenden Verzeichnissen abgelegt werden, und haben auch den Namen der Klasse
als Datei-Namen.
Wird kein Class-Path angeben, wird das aktuelle Verzeichnis als Class-Path angenommen.
Folgende Aufrufe sind also identisch:
> java KlassenName
> java –cp . KlassenName
Der Class-Path kann aber auch über die Environment Variable „CLASSPATH“ einen Default
bekommen, der dann statt des aktuellen Verzeichnisses genommen wird. In so einem Fall
wären die beiden obigen Aufrufe nicht identisch, und der Class-Path müßte auch bei ClassDateien im aktuellen Verzeichnis angegeben werden.
4.6 Eclipse
Viel einfacher wird die Java Entwicklung mit einer IDE (Integrated Development Environment),
da dort alle Tools wie Editor, Compiler, JVM und viele weitere unter einer grafischen Oberfläche
zusammengeführt sind. Besonders komfortabel wird die Entwicklung, wenn ein wahres Monster
von IDE wie z.B. Eclipse (siehe Kapitel 4.1.3) genutzt wird. Aber – seien Sie gewarnt – mit dem
Komfort kommt Hand-in-Hand auch eine gewisse Komplexität. Denn Eclipse kann super-viel,
und hat auch seine eigene Benutzungs-Philosophie.
Dieses Kapitel soll eine einfache kleine Einführung in Eclipse sein. Ihnen sollte aber klar sein,
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 71 / 409
dass es nur die rudimentärtsten Features von Eclipse erklären kann23.
Achtung – alle Screen-Shots in diesem Eclipse-Einführungs-Kapitel sind unter Microsoft
Windows 7 im klassischen Design entstanden. Je nach dem von Ihnen verwendeten
Betriebssystem und Design kann das Aussehen bei Ihnen von den Screen-Shots abweichen.
Hinzu kommt noch, dass sich auch das Erscheinungsbild der Eclipse selber und aller
Perspektiven in einem hohen Maße angepaßen und individualisieren werden. Es kann also
ohne weiteres sein, dass Ihre konkrete Eclipse-Installation bei Ihnen ganz anders aussieht.
4.6.1 Workspaces
Nach dem Start von Eclipse müssen Sie als erstes einen Workspace auswählen bzw. neu
anlegen. In Eclipse sind Workspaces für die globale Strukturierung von Projekten da – d.h. in
ihnen können mehrere Projekte zusammengefaßt werden24. Ein Workspace entspricht einem
Verzeichnis, in dem nachher alle Teile des Workspaces liegen. Wenn Sie einen neuen
Workspace anlegen wollen, müssen Sie im Workspace-Launcher daher nur ein
entsprechendes Verzeichnis angeben – hier im Beispiel „e:\java\bsp-workspace“.
Abb. 4-37 : Eclipse Workspace Launcher
Eclipse startet dann mit seinem Begrüßungs-Bildschirm, den wir jetzt ignorieren wollen – d.h.
wir schließen ihn einfach. Später können Sie über den Begrüßungs-Bildschirm z.B. Tutorials
und Beispiele aus dem Internet öffnen.
Es gibt dicke Bücher nur zum Thema Eclipse – darin finden sie alles wichtige,
In der Praxis ist dies sehr normal, dass ein Projekt aus mehreren kleinen Projekten besteht.
Man macht dies, um mehr Übersichtlichkeit zu erreichen.
23
24
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 72 / 409
Abb. 4-38 : Eclipse Begrüßungs Bildschirm
Abb. 4-39 : Eclipse Begrüßungs Bildschirm schließen
Danach sehen wir unseren neuen (noch leeren) Workspace in der Java Perspektive.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 73 / 409
Abb. 4-40 : Leerer Workspace in der Java Perspektive
4.6.2 Projekt
Um in der Eclipse ein Java Programm zu schreiben – und sei es noch so klein – muß ein JavaProjekt vorhanden sein. Dies muss also als erstes angelegt werden. Möglich ist dies über viele
alternative Wege, z.B. über das Menü mit „File => New => Java Project...“, über die Toolbar
oder über das Kontext-Menü im Package-Explorer auf der linken Seite, den man im folgenden
Screen-Shot sieht:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 74 / 409
Abb. 4-41 : Neues Projekt anlegen
Es erscheint ein Wizard, der Sie durch das Anlegen eines neues Projekts führt. Auf der ersten
Seite müssen Sie dem Projekt einen Namen geben, hier im Beispiel „Hallo Welt“, da wir ein
Hallo-Welt Programm implementieren wollen.
Man kann hier auf der ersten Seite und der folgende Seite des Wizards noch weitere
Voreinstellungen vornehmen – aber sie interessieren uns zur Zeit nicht, da wir nur ein ganz
einfaches Java-Projekt anlegen wollen. Also beenden Sie den Wizard mit „Finish“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 75 / 409
Abb. 4-42 : Projekt-Wizard
Nach erfolgreichem Beenden des Projekt-Wizards sehen Sie folgende Workspace Ansicht mit
unserem „Hallo Welt“ Projekt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 76 / 409
Abb. 4-43 : Workspace mit neuem „Hallo Welt“ Projekt
4.6.3 Klasse
Als nächstes müssen wir eine Klasse erzeugen, denn Klassen sind ja das Strukturierungsmittel
von Java, und ohne eine Klasse läuft gar nichts – siehe Kapitel 3.6. Dafür haben wir wieder
mehrere Möglichkeiten, z.B. über das File-Menü, mit dem Toolbar-Button (siehe rote
Markierung im folgenden Screen-Shot) – oder mit dem Kontext-Menü auf dem Source-Folder
des Projekts mit „New => Class“, wie im Screen-Shot zu sehen:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 77 / 409
Abb. 4-44 : Neue Klasse anlegen
Dann öffnet sich der Klassen-Wizard der Eclipse. Primär muß hier der Name der Klasse und
das Package angegeben werden:
• In unserem Beispiel nenne ich die Klasse „HelloWorldAppl“ – siehe zweiter roter Kasten im
folgenden Screen-Shot.
• Außerdem lege ich die Klasse in das Package „examplepackage“ – siehe erster roter Kasten
im folgenden Screen-Shot – denn in realen Programmen sollten alle Klassen Packages
zugeordnet sein, vergleiche Kapitel 3.8 und Kapitel 13.5.
• Eine weiteres Feature des Klassen-Wizards, das wir direkt nutzen wollen, betrifft die
Möglichkeit sich automatisch eine Main-Funktion generieren zu lassen. Das wollen wir
nutzen, und selektieren die entsprechende Checkbox daher – siehe dritter roter Kasten
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 78 / 409
Abb. 4-45 : Klassen-Wizard
Die Namen „HelloWorldAppl“ und „examplepackage“ entsprechen den Java-Konventionen, die
wir in Kapitel 3.2.5 kennen gelernt haben:
• Klassen-Namen beginnen groß und werden kapitalisiert geschrieben
• Package-Namen werden durchgängig klein geschrieben
Auch bei der Einhaltung solcher Konventionen unterstützt uns die Eclipse. Im oberen Bereich
des Wizards wird eine Warnung eingeblendet, wenn wir solche oder andere Konventionen
verletzen. Im folgenden Screen-Shot z.B. sehen wir eine Warnung, wenn wir kein Package, d.h.
das sogenannte Default-Package, verwenden:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 79 / 409
Abb. 4-46 : Warnung bei Nutzung des Default-Packages
Oder im folgenden Beispiel sehen wir eine Warnung, da die Konvention für Klassen-Namen
verletzt wurde – hier z.B. beginnt der Klassen-Name mit einem Kleinbuchstaben:
Abb. 4-47 : Warnung bei fehlerhaftem Klassen-Namen
Alle weiteren Features des Klassen-Wizards ignorieren wir erstmal. Zum einen sagen uns viele
noch nichts, zum anderen müssen Sie ja noch was zum Spielen und Ausprobieren haben.
Mit „Finish“ beenden Sie den Wizard, und Eclipse generiert nach Ihren Angaben:
• Ein Package, wenn angeben, und natürlich inkl. Verzeichnis im Workspace Verzeichnis auf
der Festplatte.
• Eine Klasse in der korrekten Datei, abgelegt im richtigen Package (Verzeichnis).
• Und eine Main-Funktion in der Klasse.
So sieht es das alles dann im Workspace aus.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 80 / 409
Abb. 4-48 : Neue Klasse „HalloWeltAppl“ im Workspace
Ich will auch hier gar nicht auf alle Features von Eclipse eingehen – genau genommen nicht
mal einen Bruchteil – aber ein paar Dinge möchte ich erklären. Den Rest überlasse ich Ihrem
Forschergeist.
• In der Mitte ist der sogenannte Editor-Bereich, in dem die Quelltexte angezeigt und von
Ihnen editiert werden können. Der Editor unterstützt sie mit einer Unmenge von Features, so
z.B. Code-Completion, automatischen Builds, Quick-Fixes, Mark-Occurrences, Folding,
Syntax-Highligthing, Refactoring, uvm. Viel Spaß beim Forschen und Ausprobieren.
• Links im Package-Explorer haben Sie eine logische Sicht auf Ihren Workspace und Ihre
Projekte. Hier finden Sie alle Projekte, Package, Klassen, Libraries und all die anderen
Dinge, die in einem typischen Projekt vorkommen, wieder.
• Unten finden Sie mehrere Views, von denen der Problems-View vorne liegt. In ihm werden
die Compiler-Fehler und –Warnungen des Codes angezeigt. Im Hintergrund liegen Views,
die die Java-Doc bzw. die Deklaration des jeweils selektierten Elements anzeigen. In diesem
Bereich wird sich später auch der Consolen-View öffnen.
• Rechts finden sich mehrere Views untereinander. Da der Screen-Shot so klein ist, sind sie
kaum zu erkennen und so auch nicht zu verwenden. Ich hoffe, Sie haben einen größeren
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 81 / 409
Bildschirm:
- Ganz oben findet sich der Task-Views, der Aufgaben verwalten kann.
- Darunter liegt der sogenannte Outline-View, der eine logische Sicht der aktuellen Datei
ermöglicht. D.h. hier finden Sie alle Imports, Klassen, Konstruktoren, Funktionen und
Attribute wieder. Per Doppelklick können Sie sie direkt anspringen.
Dies ist das Default-Aussehen der Eclipse 4.4 – Sie können es aber in großem Umfang ändern
und an Ihre Bedürfnisse und Vorlieben anpassen.
All diese Features sollen uns aber nicht davon abhalten zu programmieren – darum geht es uns
ja gerade. Und dazu gehört natürlich auch unser Hallo-Welt Programm fertig zu
programmieren. All die Features helfen Ihnen hier zwar – aber denken und tippen müssen Sie
immer noch selber. Ich habe es mal gemacht – und das Ergebnis sehen Sie in der nächsten
Abbildung – unsere Main-Funktion enthält nun eine Anweisung, um „Hallo Welt“ auszugeben:
Abb. 4-49 : Fertiger Quelltext im Wizard
4.6.4 Start und Ab...
Jetzt muss unser ultimatives Programm nur noch laufen, und wir können ans Verkaufen und
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 82 / 409
Geld verdienen gehen25. Es gibt viele Möglichkeiten unser Programm zu starten. Und manche
davon sind kompliziert, da man viele Einfluss-Möglichkeiten hat (z.B. könnte man dem
Programm mal ein anderes JRE unterschieben, um seine Kompatibilität nach unten oder oben
zu checken).
Mit einer der einfachsten Arten das Programm zu starten ist die Klasse mit der Main-Funktion
im Package-Explorer zu selektieren, und dann über das Kontext-Menü „Run As => Java
Application“ das Programm direkt von hier zu starten.
Falls Sie diese Vision glauben, sind sie a) sehr leichtgläubig und haben b) das Tutorial nicht
ordentlich gelesen. Denn in Kapitel 1 steht extra, dass man nicht alles glauben soll was
geschrieben steht – erst recht nicht, wenn es hier im Tutorial steht ;-).
25
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 83 / 409
Abb. 4-50 : Starten des Programms
Unser Programm ist ein reines Kommandozeilen-Programm ohne GUI. Eclipse öffnet jetzt
keine Kommandozeile, sondern stellt einen Consolen-View zur Verfügung, in den das
Programm seine Ausgaben (und später auch Eingaben) macht.
Abb. 4-51 : Ausgabe des Programms
4.6.5 Warnungen
Der Java-Compiler in der Eclipse wird in der Default-Einstellung bei einigen Beispielen aus dem
Tutorial Warnungen erzeugen. Im Prinzip sind diese Warnungen gut und sehr hilfreich, und wir
sollten Code schreiben der keine Warnungen produziert. Aber bei unseren Beispielen machen
wir aber ab und zu Dinge absichtlich falsch, um etwas zu lernen – dann sind uns diese
Warnungen egal.
Wenn Ihr Workspace aber viele Projekte enthält, in denen – aus welchen Gründen auch immer
– zum Teil solche Warnungen auftauchen, dann können eine Menge Warnungen
zusammenkommen. In einem solchen Fall gehen dann schnell wichtige Warnungen
gegenüber den „fehlerhaften“ Warnungen unter und fallen nicht auf. In einem solchen Fall sollte
man die „fehlerhaften“ Warnungen deaktivieren. Sie haben hierzu zwei Möglichkeiten:
• Sie können die Warnung selektiv im Quelltext mit Annotations (siehe Kapitel todo)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 84 / 409
deaktivieren. Dies ist die meistens zweckmäßige Variante, wenn die Warnung an sich
sinnvoll ist – und nur an dieser einen Stelle nicht.
• Oder Sie deaktivieren die Warnung global in der Eclipse in den Eclipse Preferences – siehe
nächste Screenshots. Damit wird die Warnung natürlich nirgendswo mehr erzeugt, und
möglicherweise verhindern Sie damit wichtige Hinweise des Compilers auf fehlerhaften
Code.
Hier zwei Beispiele von Code-Features, die Warnungen erzeugen und in den Beispielen des
Tutorials auftauchen können:
• Das Schreiben serialisierbarer Klassen ohne „serialVersionUID“ Attribute – siehe Kapitel
todo
• Die Verwendung typloser Container – siehe Kapitel todo
Möchten Sie diese Warnungen unterdrücken, so empfehlen sich selektive Annotations (siehe
oben). Alternativ können Sie die Warnungen in den Eclipse Preferences einfach abstellen. Die
Eclipse Preferences erreichen Sie über das Menü „Window => Preferences...“. Unter „Java =>
Compiler => Errors/Warnings“ finden Sie die Details zu den Eclipse/Java Warnungen und
Fehlern – hier ändern Sie dann die Einstellungen von „Warning“ auf „Ignore“ – und schon
haben Sie Ihre Ruhe.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 85 / 409
Abb. 4-52 : Eclipse Warnungs Preferences für „serialVersionUID“
Abb. 4-53 : Eclipse Warnungs Preferences für „typlose Container“
4.6.6 Eclipse
Okay, damit ist unser Kurz-Trip durch die Eclipse fast beendet. Wir haben kurz die wichtigsten
Features für die erste Entwicklung kennen gelernt, wie Workspaces, Projekte, Klassen und den
Consolen-View. Im nächsten Kapitel todo werden wir noch sehen, wie man fertige Projekt und
Einstellungen importiert. Alles weitere kommt im Laufe der Zeit von alleine
Aber vergessen Sie nicht bei all dem Spielen in und mit Eclipse. Eclipse ist nur ein Werkzeug,
eine IDE – das Kern-Thema der Vorlesung ist und bleibt Java.
4.7 Projekte importieren
Damit Sie nicht alle Beispiele abtippen müssen, finden Sie auf meiner Homepage unter
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 86 / 409
http://www.wilkening-online.de/tutorial-java.html nicht nur das jeweils neuste Java-Tutorial,
sondern auch viele der Beispiele und Musterlösungen des Tutorials als Eclipse Workspace.
Laden Sie die entsprechende 7z Datei „JavaTutorialWorkspace.7z“ herunter und packen Sie
sie aus. Ich habe das mal beispielhaft direkt unter „E:\“ gemacht.
Abb. 4-54 : Ausgepackter Tutorial Workspace auf “E:\”
Dann starten Sie die Eclipse und legen einen neuen Workspace an – ich habe z.B.
„e:\java\tutorial-wsp“ genommen. Natürlich müssen Sie keinen neuen Eclipse Workspace
anlegen, sondern können die Beispiele auch in einen vorhandenen Eclipse Workspace
übernehmen – ich habe hier aus Gründen der Übersichtlichkeit einen Neuen gewählt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 87 / 409
Abb. 4-55 : Neuer Eclipse Workspace für die Tutorial Beispiele
Öffnen Sie im Package-Explorer das Kontext-Menü (z.B. mit der rechten Maustaste) und
wählen Sie den Menü-Eintrag „Import“ aus.
Abb. 4-56 : Kontext-Menü mit Import Eintrag
Dann öffnet sich der Import Wizard. Wählen Sie unter „General“ den Eintrag „Existing Projects
into Workspace“ aus und drücken dann den Button „Next“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 88 / 409
Abb. 4-57 : Import Wizard Seite 1
Damit wechseln Sie zur Seite 2 des Import Wizards:
• Im oberen Bereich geben Sie an, wo der ausgepackte Workspace liegt – in meinem Fall
„E:\JavaTutorialWorkspace“
• Nach Eingabe des Verzeichnisses werden im mittleren Projekt Bereich alle Eclipse-Projekte
aufgelistet, die die Eclipse in dem angegebenen Verzeichnis gefunden hat. Hier können Sie
selektiv die Projekte auswählen, die Sie importieren möchten – defaultmäßig sind hier alle
Projekte selektiert.
• Wenn Sie die Quelltexte in Ihren neuen Workspace kopieren möchten, dann müssen Sie
noch die Checkbox bei „Copy projects into workspace“ setzen. Falls Sie dies nicht machen,
verlinkt die Eclipse Ihre neuen Projekte in Ihrem neuen Workspace mit den Dateien im
Tutorial-Verzeichnis. Dann dürfen Sie das Verzeichnis natürlich nicht löschen oder
anderweitig verändern. Darum kopiere ich die Dateien hier lieber in meinen neuen
Workspace. Je nach Workflow und Anforderung kann natürlich auch das Verlinken in der
Praxis sehr sinnvoll und hilfreich sein.
• Danach müssen Sie nur noch den Button „Finish“ drücken.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 89 / 409
Abb. 4-58 : Import Wizard Seite 2
Damit stehen Ihnen dann alle Beispiele und Musterlösungen in Ihrem Workspace zur
Verfügung. Viel Spaß beim Ausführen, Analysieren, Verstehen und Verändern.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 90 / 409
Abb. 4-59 : Der fertig importierte Eclipse Workspace
4.8 Aufgaben
Der Sinn dieser ersten Aufgaben besteht primär darin, sich mit den Entwicklungs-Tools in Form
von Editor, Java-Compiler „javac“, JVM „java“, und Ihrer ausgewählten IDE wie z.B. Eclipse
oder NetBeans vertraut zu machen. Sekundär werden damit natürlich auch die ersten SprachElemente geübt.
Achten Sie bitte auch bei diesen ersten Programm schon auf sinnvolle Namen und eine
vernünftige Einrückung. Und das gilt natürlich erst Recht für alle weiteren Aufgaben.
4.8.1 Aufgabe „Hallo Welt“
Schreiben Sie eine erste Klasse ohne Package mit Main-Funktion, in der „Hallo Welt“ auf der
Kommandozeile ausgegeben wird.
1) Entwickeln und testen Sie das Programm mit einem Editor, dem Java Compiler „javac“, und
mit der JVM „java“ in einer Kommandozeile.
2) Entwickeln und testen Sie das Programm in Ihrer IDE wie z.B. Eclipse oder NetBeans.
Lösung siehe Kapitel 4.9.
4.8.2 Aufgabe „GUI-Fenster“
Schreiben Sie eine Klasse ohne Package mit Main-Funktion, in der ein GUI Fenster geöffnet
wird.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 91 / 409
1) Entwickeln und testen Sie das Programm mit einem Editor, dem Java Compiler „javac“, und
mit der JVM „java“ in einer Kommandozeile.
2) Entwickeln und testen Sie das Programm in Ihrer IDE wie z.B. Eclipse oder NetBeans.
Lösung siehe Kapitel 4.10.
4.8.3 Aufgabe „Package“
Schreiben Sie eine Klasse in einem Package mit Main-Funktion, in der etwas passendes auf
der Kommandozeile ausgegeben wird.
1) Entwickeln und testen Sie das Programm mit einem Editor, dem Java Compiler „javac“, und
mit der JVM „java“ in einer Kommandozeile.
2) Entwickeln und testen Sie das Programm in Ihrer IDE wie z.B. Eclipse oder NetBeans.
Lösung siehe Kapitel 4.11.
4.8.4 Aufgabe „Packages“
Schreiben Sie eine Klasse in einem verschachteltem Package (d.h. das Package liegt wieder
in einem Package) mit Main-Funktion, in der etwas passendes auf der Kommandozeile
ausgegeben wird.
1) Entwickeln und testen Sie das Programm mit einem Editor, dem Java Compiler „javac“, und
mit der JVM „java“ in einer Kommandozeile.
2) Entwickeln und testen Sie das Programm in Ihrer IDE wie z.B. Eclipse oder NetBeans.
Lösung siehe Kapitel 4.12.
4.8.5 Aufgabe „Summe“
Schreiben Sie ein Programm, dass zwei Gleitkomma-Zahlen von der Kommandozeile einliest
und ihre Summe ausgibt. Fangen sie fehlerhafte Eingaben ab, und geben sie im Falle eines
Fehlers eine Meldung aus.
1) Entwickeln und testen Sie das Programm mit einem Editor, dem Java Compiler „javac“, und
mit der JVM „java“ in einer Kommandozeile.
2) Entwickeln und testen Sie das Programm in Ihrer IDE wie z.B. Eclipse oder NetBeans.
Dieses Programm hat – bezogen auf unser aktuelles Wissen – einen Knackpunkt: das Einlesen
von Kommandozeile (siehe Kapitel 3.10) ist relativ aufwändig und setzt eine Menge
Pragmatismus voraus26. Etwas einfacher wäre die Benutzung von KommandozeilenArgumenten (siehe Kapitel 3.5).
Pragmatismus meint hier: ich übernehme den Code und passe ihn an meine Bedürfnisse an,
ohne ihn auch nur annähernd zu verstehen.
26
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 92 / 409
Nun werden viele der noch folgenden Programme so viel Benutzer-Interaktion erfordern, dass
Kommandozeilen-Argumente keine Lösung darstellen, aber für diese Aufgabe (und noch einige
folgende) wäre dies eine akzeptable Lösung. Und später haben wir ja mehr Erfahrung und
mehr Wissen – bis dahin erscheint uns die Eingabe vielleicht gar nicht mehr so mystisch.
Nachteilig an der Kommandozeilen-Argument Lösung ist aber, dass die Änderung der
Kommandozeilen-Argumente in Eclipse aufwändiger ist. Für das Entwickeln und Testen in
Eclipse wäre das Einlesen von Kommandozeile viel angenehmer und flexibler.
Von daher schlage ich folgenden Kompromiß vor:
• Für den ersten Wurf mit dem Java-Compiler „javac“ und der JVM „java“ implementieren sie
die Aufgabe mit Kommandozeilen-Argumenten.
• Und im zweiten Wurf mit Eclipse – und dann ja auch mit viel mehr Erfahrung – lesen Sie die
Summanden von der Kommandozeile ein.
Lösung siehe Kapitel 4.13.
4.8.6 Aufgabe „Ausgabe Verzeichnis“
Compilieren Sie die Programme der fünf vorherigen Aufgaben mit dem Java Compiler „javac“
so, dass der Byte-Code in einem eigenen Verzeichnis liegt. Starten Sie die Programme danach
mit der JVM „java“ in einer Kommandozeile.
Denken Sie sich einen sinnvollen Ausgabe-Pfad Namen aus.
Lösung siehe Kapitel 4.14.
4.9 Lsg. zu Aufgabe „Hallo Welt“ – Kap. 4.8.1
Diese Aufgabe – wie eigentlich fast alle in diesem Kapitel – ist von der Java Seite her sehr
einfach, da der Quelltext z.B. in den Kapiteln 3.1.1 oder 4.3.1.1 oder 4.6.3 fertig vorliegt. Der
Vollständigkeit halber sei hier aber noch mal aufgeführt.
public class Kap0410LsgHalloWelt {
public static void main(String[] args) {
System.out.println("Hallo Welt");
}
}
Für den ersten Teil der Aufgabe, müssen nur noch der Java-Compiler „javac“ und die JVM
„java“, wie in Kapitel 4.3.1.1 beschrieben, benutzt werden.
Die Benutzung der IDE Eclipse wird in Kapitel 4.6 beschrieben. Dies auszuführen ist dann der
zweite Teil der Aufgabe.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 93 / 409
4.10 Lsg. zu Aufgabe „GUI-Fenster“ – Kap. 4.8.2
Auch diese Aufgabe ist ein Übernehmen des fertigen Quelltextes aus z.B. Kapitel 3.1.2, und ein
Anwenden der Tools „javac“ und „java“ wie in Kapitel 4.3.1.2 beschrieben, bzw. von Eclipse
(siehe Kapitel 4.6).
import javax.swing.JFrame;
public class Kap0411LsgGui {
public static void main(String[] args) {
JFrame frame = new JFrame("Mein erstes GUI Fenster");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(200, 200);
frame.setSize(300, 100);
frame.setVisible(true);
}
}
4.11 Lsg. zu Aufgabe „Package“ – Kap. 4.8.3
Von der Seite der Programmierung ist diese Aufgabe im Prinzip wieder ein „Hallo Welt“.
Einziger Unterschied ist, dass die Klasse in einem Package stehen soll, und daher der Quelltext
mit einer entsprechenden Package-Anweisung beginnen muss – siehe Kapitel 3.8. Er könnte
also z.B. so aussehen:
package mypackage;
public class Appl {
public static void main(String[] args) {
System.out.println("Ausgabe aus einer Klasse in einem Package");
}
}
Die eigentliche Schwierigkeit der Aufgabe liegt im Bereich der Daten-Organisation und der
Tools:
• Bei Verwendung von der Tools „javac“ und „java“ müssen Sie dafür sorgen, dass der
Quelltext in einem entsprechenden Verzeichnis liegt. Und Sie müssen das Compilieren und
Starten des Programms entsprechend Kapitel 4.3.2.2 durchführen.
• Mit Eclipse ist das ganze viel einfacher. Bei der Erzeugung der Klasse mit dem KlassenWizard (siehe Kapitel 4.6.3) müssen Sie einfach im Package-Feld den gewünschten
Package-Namen angeben: alles andere erledigt Eclipse für Sie automatisch richtig.
4.12 Lsg. zu Aufgabe „Packages“ – Kap. 4.8.4
Diese Aufgabe unterscheidet sich von Aufgabe 4.8.3 (Lösung Kapitel 4.11) nur durch drei
verschachtelte statt einem einzigen Package. Der Quelltext ist also bis auf die angepaßte
Package Anweisung und die angepaßte Ausgabe identisch zu Kapitel 4.11.
package aussen.mitte.innen;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 94 / 409
public class Appl {
public static void main(String[] args) {
System.out.println("Ausgabe aus einer Klasse in drei Packages");
}
}
Und was ist sonst zu tun?
• Mit den Kommandozeilen-Tools ist händisch für die richtige Verzeichnis-Struktur zu sorgen.
Danach wieder die Benutzung vergleichbar zur Lösung 4.11.
• In Eclipse müssen im Klassen-Wizard nur alle Packages angegeben werden – den Rest
erledigt Eclipse für uns ☺.
4.13 Lsg. zu Aufgabe „Summe“ – Kap. 4.8.5
4.13.1 Mit Kommandozeilen Argumenten 1
Die erste Lösung basiert auf der Auswertung der Kommandozeilen Argumente, statt die Zahlen
von der Kommandozeile interaktiv einzulesen.
Dieses Programm läßt sich mit etwas Pragmatismus bei der Anwendung der Sprach-Elemente
aus den Kapiteln 3.5 („Arrays und Kommandozeilen-Argumente“), 3.9 („Exceptions“) und 3.11
(„Konvertierungen“) recht einfach implementieren.
Für die Addition benötigen wir zwei Summanden, d.h. müssen auch zwei Kommandozeilen
Argumente übergeben werden. Ist dies nicht der Fall, geben wir eine Fehlermeldung aus, und
beenden das Programm.
public static void main(String[] args) {
if (args.length!=2) {
System.out.println("Das Programm erwartet zwei Zahlen als Eingabe");
return;
}
...
}
Dann müssen die Operanden, die ja als Strings vorliegen, in Double Werte gewandelt werden.
Da dies schief gehen kann – der Benutzer könnte ja z.B. Texte statt Zahlen übergeben, könnte
die Wandlungs-Funktion „Double.parseDouble“ eine Exception werfen. Diese fangen wir ab,
melden dann einen Fehler, und beenden das Programm.
double d1;
try {
d1 = Double.parseDouble(args[0]);
} catch (Exception x) {
System.out.println("Der erste Parameter ist keine Gleitkomma-Zahl");
return;
}
Und da wir zwei Summanden haben, benötigen wir den Code zweimal in fast identischer
Weise. Unterschiede finden sich in der Double-Variablen „d1“ bzw. „d2“, dem Kommandozeilen
Argument „args[0]“ bzw. „args[1]“ und der Fehlermeldung.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 95 / 409
Am Schluss müssen die zwei Zahlen nur noch addiert werden, und die Summe muss mit einer
Meldung ausgegeben werden.
double sum = d1 + d2;
System.out.println(d1 + " + " + d2 + " = " + sum);
Alles zusammen ergibt dann folgendes Programm:
public class Appl {
public static void main(String[] args) {
if (args.length!=2) {
System.out.println("Das Programm erwartet zwei Zahlen als Eingabe");
return;
}
double d1;
try {
d1 = Double.parseDouble(args[0]);
} catch (Exception x) {
System.out.println("Der erste Parameter ist keine Gleitkomma-Zahl");
return;
}
double d2;
try {
d2 = Double.parseDouble(args[1]);
} catch (Exception x) {
System.out.println("Der zweite Parameter ist keine Gleitkomma-Zahl");
return;
}
double sum = d1 + d2;
System.out.println(d1 + " + " + d2 + " = " + sum);
}
}
4.13.2 Mit Kommandozeilen Argumenten 2
Wenn man nicht so viel Werte auf super-detailierte Fehler-Meldungen legt, kann man Lösung 1
noch vereinfachen. In Kapitel 3.9 wurde schon erwähnt, dass Probleme in Java durch
Exceptions gemeldet werden – und dazu gehört auch der Zugriff auf nicht existente ArrayElemente (Kapitel 3.5).
Mit dieser Erkenntnis kann man sich das Leben (das Programm) vereinfachen, und die erste
Fehlerabfrage auf die Existenz der beiden Kommandozeilen Argumente sparen. Man greift
beim Konvertieren einfach blind ins Array, und existiert das Element nicht, so wird auch dies
durch eine Exception gemeldet. Wir sollten vielleicht nur die Fehler-Meldung etwas anpassen,
und schon sieht die Konvertierung so aus:
double d1;
try {
d1 = Double.parseDouble(args[0]);
} catch (Exception x) {
System.out.println("Probleme mit dem ersten Parameter");
return;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 96 / 409
Und das ganze Programm ist im Prinzip nur eine Wiederholung dieses Blocks mit Ausgabe der
Summe und dem notwendigen Drum-Herum (Klasse mit Main-Funktion).
public class Appl {
public static void main(String[] args) {
double d1;
try {
d1 = Double.parseDouble(args[0]);
} catch (Exception x) {
System.out.println("Probleme mit dem ersten Parameter");
return;
}
double d2;
try {
d2 = Double.parseDouble(args[1]);
} catch (Exception x) {
System.out.println("Probleme mit dem zweiten Parameter");
return;
}
double sum = d1 + d2;
System.out.println(d1 + " + " + d2 + " = " + sum);
}
}
4.13.3 Einlesen von Kommandozeile
Auch die Version mit dem interaktiven Einlesen von Kommandozeile ist nicht wirklich
schwieriger. Zuerst muss – wie in Kapitel 3.10 beschrieben – die Tastatur „System.in“ in einem
„InputStreamReader“ und einem „BufferedReader“ gewrappt werden.
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
Achtung - vergessen Sie die beiden Import-Anweisungen nicht.
Statt jetzt die Kommandozeilen Argumente auszuwerten, müssen Sie einfach nur eine Zeile als
String von der Kommandozeile einlesen:
• Am besten vorher mit einer sinnvollen Aufforderung für den Benutzer.
• Da das Einlesen schief gehen kann, kann „reader.readLine()“ eine Exception werfen. Da
dies eine Checked-Exception ist, muß diese Zeile in einem Try-Catch-Block stehen. Aber
den haben wir ja schon wegen der Konvertierung „String-zu-Double“.
• Da jetzt sowohl die Eingabe als auch die Konvertierung in ein und dem gleich Try-CatchBlock stehen, kann nicht mehr unterschieden werden, ob das Problem beim Einlesen
entstanden ist, oder bei der Konvertierung. Unsere Fehlermeldung sollte also recht
allgemein gehalten sein.27
double d1;
try {
System.out.print("Bitte geben Sie den ersten Summanden ein: ");
String in = reader.readLine();
Wer gerne bessere Fehlermeldungen hätte, muss die Eingabe und die Konvertierung einzeln
mit einem Try-Catch-Block wrappen, und kann dann dediziert reagieren.
27
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 97 / 409
d1 = Double.parseDouble(in);
} catch (Exception x) {
System.out.println("Probleme mit der Eingabe");
return;
}
Das ganze zweimal, und dann wie gehabt die Summe bilden und ausgeben. Dann noch das
Rum-Herum von Klasse und Main-Funktion, und fertig ist das Programm mit Einlesen von der
Kommandozeile. Es hat doch gar nicht weg getan, oder?
import java.io.InputStreamReader;
import java.io.BufferedReader;
public class Appl {
public static void main(String[] args) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
double d1;
try {
System.out.print("Bitte geben Sie den ersten Summanden ein: ");
String in = reader.readLine();
d1 = Double.parseDouble(in);
} catch (Exception x) {
System.out.println("Probleme mit der Eingabe");
return;
}
double d2;
try {
System.out.print("Bitte geben Sie den zweiten Summanden ein: ");
String in = reader.readLine();
d2 = Double.parseDouble(in);
} catch (Exception x) {
System.out.println("Probleme mit der Eingabe");
return;
}
double sum = d1 + d2;
System.out.println(d1 + " + " + d2 + " = " + sum);
}
}
4.14 Lsg. zu Aufgabe „Ausgabe Verzeichnis“ – Kap. 4.8.6
Zu dieser Lösung gibt es nicht viel zu schreiben, da alle Quelltexte schon in den LösungsKapiteln zuvor entwickelt worden sind. Hier sollen ja nur beim Compilieren mit dem JavaCompiler „javac“ der Byte-Code, d.h. die Class-Dateien, in ein extra Ausgabe Verzeichnis
generiert werden.
Typische Namen für Ausgabe-Verzeichnisse sind:
• output - wegen Ausgabe
• class - wegen Class-Dateien
• bin - wegen binärem Code (Byte-Code)
Aber auch andere Namen, die Assoziationen zu Ausgabe, Byte-Code oder Class-Dateien
erzeugen, sind okay.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 98 / 409
Ansonsten gilt nur: das in Kapitel 4.3.3 erklärte anwenden - und fertig ist die Aufgabe.
5 Elementare Datentypen und Variablen
5.1 Datentypen
5.1.1 Datentypen
Java ist eine statisch typisierte Sprache, d.h. jedes Objekt, Variable, Konstante, Literal, usw.
hat einen eindeutigen Typ, der schon zur Compilezeit feststeht.
Java unterscheidet zwischen elementaren und benutzerdefinierten Datentypen.
• Elementare Datentypen sind fest in die Sprache eingebaut – siehe Kapitel 5.2.
• Benutzerdefinierte Datentypen sind Klassen, Enums bzw. Interfaces – siehe Kapitel 11,
Kapitel 12.6 und Kapitel 14.13.
Java unterscheidet zwischen statischen und dynamischen Typen:
• Der statische Typ ist der Typ, den der Compiler sieht.
• Der dynamische Typ ist der wahre Typ des Objekts zur Laufzeit.
Einen Unterschied zwischen statischen und dynamischen Typ kann es bei Objekten im Kontext
von Vererbung geben. Diese wird uns ab Kapitel 14 begegnen.
5.1.2 Typ-Umwandlungen
In Java können sogenannte Typ-Umwandlungen (auch Casts bzw. Type-Casts genannt)
vorgenommen werden. Dies ist notwendig wenn ein Quelltyp nicht zum Zieltyp passt, z.B. bei
einer Zuweisung oder einem Funktionsaufruf.
Eine Typ-Umwandlung wird durch die Angabe des Zieltyps in Klammern vor dem Quellausdruck
vorgenommen.
byte b = (byte) 'A';
Hinweise:
• Sie sollten sich bemühen, Code zu schreiben, der keine oder nur wenige TypUmwandlungen benötigt.
• In Java sind nur Typ-Umwandlungen erlaubt, die syntaktisch Sinn machen.
• Typ-Umwandlungen gibt es nur im Kontext der elementaren Datentypen und innerhalb von
Vererbungs-Hierachien – siehe Kapitel 14.10.
• Typ-Umwandlungen zwischen elementaren Datentypen werden benötigt, wenn der
Quellausdruck nicht ohne Datenverlust in den Zieltyp paßt – dann müssen Sie diese
Wandlung explizit anfordern.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 99 / 409
• Typ-Umwandlungen innerhalb von Vererbungs-Hierachien können zur Laufzeit schief gehen,
sie werfen dann eine Exception – siehe Kapitel 14.10.
5.2 Elementare Datentypen
Folgende elementaren Datentypen sind in Java enthalten:
Name
void
char
boolean
byte
short
int
long
float
double
Grösse / Byte
2
1 Bit
1
2
4
8
4
8
Default
\u0000
false
0
0
0
0
0.0
0.0
Minimum
\u0000
-128
-32768
-2147483648
-9223372036854775808
± 1.40239846 E - 45
± 4.94065645841246544 E - 324
Maximum
\uFFFF
127
32767
2147483647
9223372036854775807
± 3.40282347 E + 38
± 1.79769313486231570 E + 308
5.2.1 void
void ist ein Dummy-Typ, um bei Funktionen anzuzeigen, dass sie nichts zurückgeben.
void fct() {
}
// Diese Funktion gibt nichts zurueck
5.2.2 char
Ein char enthält ein Zeichen im Unicode Zeichensatz – d.h. 2 Byte Grösse.
• Zeichenkonstanten werden in einfache Hochkommata gesetzt.
• Als Zeichenkonsten sind normalen Zeichen, Escape-Sequenzen (z. B. ‘\n‘ für einen
Zeilenumbruch oder ‘\t’ für einen Tabulator) und Unicode Sequenzen (die Angabe muss
hexa-dezimal erfolgen, z. B. '\u03a3') erlaubt.
char c1 = 'A';
char c2 = '\n';
char c3 = '\u03a3';
// Zeichen A
// Zeilenumbruch
// Griechisches Summenzeichen (dezimal 931)
Ein char kann ohne Typ-Cast einem int, long, float und double zugewiesen werden. Ein char
kann mit explizitem Typ-Cast einem byte und short zugewiesen werden – denken Sie aber an
den möglichen Datenverlust!
byte b = (byte) 'A';
long l = 'A';
Umgekehrt kann einem char nur mit Typ-Cast ein integraler- oder Fliesskomma-Wert
zugewiesen werden – denken Sie aber an den möglichen Datenverlust!
char c1 = (char) 42;
char c2 = (char) 3.14;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 100 / 409
Achtung – vielleicht haben Sie sich gewundert, dass ein char und ein short nicht zuweisungskompatibel sind, sondern ein Typ-Cast nötig ist, wo doch beide Typen ein Grösse von 2 Byte
haben. Aber bedenken Sie, dass ein char den Wertebereich von ‘\u0000’ bis ‘\uFFFF’ umfasst,
d.h. keine negativen Zahlen aufweist, während ein short den Wertebereich von ‘-32768’ bis
‘+32767’ umfasst.
5.2.3 boolean
Der Typ „boolean“ ist der Typ für Wahrheitswerte, daher wenn ein Zustand nur falsch („false“)
oder richtig (true) sein kann. Wir benutzen ihn auch häufig als Kriterium in den Java
Konstrollstrukturen – wie z.B. If-Anweisungen oder Schleifen, siehe Kapitel 7.
Ein boolean Wert kann nur „true“ oder „false“ enthalten28.
boolean flag = true;
boolean okay = false;
Ein boolean Wert kann keiner Variablen eines anderen Typs zugewiesen werden, auch nicht
durch Typ-Cast’s.
int i1 = true;
int i2 = (int)true;
// Compiler-Error
// Compiler-Error
Hinweis – diese Idee der Zuweisung eines Boolean an einen Int, wie auch die Idee im
folgenden Beispiel, mag Sie vielleicht etwas verwundern. Wenn ja, dann ist das gut – und dann
vergessen Sie dies direkt wieder. Aber es gibt Programmier-Sprachen, bei denen so etwas
möglich ist – und für all die, die solche Sprachen kennen: in Java geht dies nicht.
Auch umgekehrt kann kein Wert eines anderen Typs einer boolean Variable zugewiesen
werden, oder als boolean Wert benutzt werden, auch nicht durch Typ-Cast’s.
boolean b = (boolean)1;
int i=0;
if (i) {
// Compiler-Error
// Compiler-Error
5.2.4 Integrale Typen
Als integrale Typen stehen byte, short, int und long zur Verfügung. Zahlen-Literale sind
normalerweise immer int, können aber auch einem byte oder short zugewiesen werden, wenn
sie den entsprechenden Zahlenbereich nicht überschreiten. long-Zahlen-Literale werden durch
ein ‘l‘ oder ‘L‘ hinter der Zahl deklariert.
byte b = 13;
short s = 42;
int i = 1234;
long l = 123456789012345L;
28
true und false sind Schlüsselwörter
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 101 / 409
In Richtung der größeren Typen kann problemlos eine Zuweisung statt finden. In Richtung der
kleineren nur über einen expliziten Typ-Cast - denken sie aber an den möglichen Datenverlust!
int i = 17;
short s = (short)i;
long l = i;
Jeder integrale Typ kann problemlos jedem Fliesskomma Typ zugewiesen werden – sie können
dabei aber Genauigkeit verlieren!
long l = 123456789012345L
float f = l;
double d = l;
5.2.5 Fliesskomma Typen
Als Fliesskomma Typen stehen float und double zur Verfügung. Fliesskomma-Literale
unterscheiden sich von den integralen Zahlen-Literalen dadurch, dass sie einen Punkt
enthalten. Defaultmässig ist ein Fliesskomma-Literal immer vom Typ double, durch ein
nachgestelltes ‘f‘ oder ‘F‘ wird es zu einem float Literal.
float f = 3.14F;
double d = 3.14;
Ein float-Wert kann problemlos einem double zugewiesen werden, umgekehrt geht dies nur mit
einem expliziten Typ-Cast - denken sie aber an den möglichen Datenverlust!
float f1 = 3.14F;
double d1 = 3.14;
float f2 = (float)d1;
double d2 = f1;
5.3 Variablen
Variablen-Definiton
Syntax:
[Modifizierer] <Typ> <Name> [ = <Initialisierer>] ;
Bsp:
public final int i;
private String name = "Albert Einstein";
double d = 3.1415;
String s;
Variablen können nur in Klassen und in Funktionen definiert werden.
• Variablen in Klassen sind entweder Felder (Attribute oder Klassen-Variablen) – siehe Kapitel
11.4.6 und 12.5.
• Variablen in Funktionen sind lokale Variablen – siehe Kapitel 8.3.
Variablen werden einfach über ihren Namen angesprochen, und können z.B. ausgewertet,
verglichen und neu gesetzt werden.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 102 / 409
public class Beispiel {
public static void main(String[] args) {
String name = "Albert Einstein";
System.out.println("Mein Name ist " + name);
}
}
Mehr über Variablen findet sich in den Kapiteln 5.4, 8.3, 8.4, 11.4.6 und 12.5.
5.4 Wert- und Referenz-Semantik
Variablen unterliegen je nach Typ einer unterschiedlichen Semantik.
• Variablen eines elementaren Datentyps (siehe Kapitel 5.2) unterliegen einer sogenannten
Wert-Semantik.
• Variablen eines beliebigen anderen Datentyps unterliegen der Referenz-Semantik.
5.4.1 Wert-Semantik
Eine Variable hat Wert-Semantik, wenn sie den zu speichernden Wert direkt enthält, und alle
Aktionen auf der Variablen direkt auf dem Wert statt finden.
Nehmen wir z.B. eine Int-Variable „v1“, die mit dem Wert „42“ initialisiert wird.
int v1 = 42;
Für diese Variable wird irgendwo im Speicher ein 4 Byte großer Bereich reserviert, der den
Wert „42“ enthält, und der über den Namen „v1“ angesprochen werden kann.
Wird jetzt eine zweite Int-Variable „v2“ definiert, die mit der Variablen „v1“ initialisiert wird, so
bekommt „v2“ eine Kopie des Wertes von „v1“. Jede Variable enthält ihren eigenen Wert.
int v1 = 42;
int v2 = v1;
System.out.println(v1);
System.out.println(v2);
// 42
// 42
Das jede Variable ihren eigenen Wert enthält wird dann deutlich, wenn wir den Wert einer
Variablen ändern – die andere behält ihren alten Wert bei.
int v1 = 42;
int v2 = v1;
System.out.println(v1);
System.out.println(v2);
// 42
// 42
v1 = 23;
System.out.println(v1);
System.out.println(v2);
© Detlef Wilkening 1997-2016
// 23 <- veraendert
// 42 <- nicht veraendert
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
1.
Seite 103 / 409
2.
v1:
42
3.
v1:
42
v1:
23
v2:
42
v2:
42
Abb. 5-1 : Wert-Semantik
Werte-Semantik liegt aber nicht nur bei einer Initialisierung oder einer Zuweisung vor, sondern
z.B. auch bei Vergleichen (siehe auch Kapitel 6.2) und Funktions-Aufrufen (siehe Kapitel 8.4).
Hier ein Beispiel mit zwei Vergleichen.
int v1 = 42;
int v2 = 42;
if (v1==v2) {
System.out.println("v1 und v2 sind gleich");
}
if (v1==42) {
System.out.println("v1 ist gleich 42");
}
Ausgabe
v1 und v2 sind gleich
v1 ist gleich 42
In beiden Fällen wird der Wert von „v1“ (d.h. die „42“) mit dem Wert des zweiten Operanden
verglichen.
Diese Wert-Semantik gilt für alle Variablen eines elementaren Datentyps (d.h. char, boolean,
byte, short, int, long, float oder double), unabhängig davon ob die Variable eine lokale Variable,
ein Parameter, eine Objekt- oder Klassen-Variable ist.
5.4.2 Referenz-Semantik
Abgesehen von den elementaren Datentypen werden alle Objekte (auch Arrays) in Java mit
einer Referenz-Semantik angesprochen:
• Eine Variable auf einen nicht-elementaren Datentyp enthält nicht das Objekt selber,
sondern nur eine Referenz auf das Objekt, man spricht auch von Referenz-Variablen.
• Eine Referenz-Variable wird mit null initialisiert29.
• Vor der Nutzung einer Referenz-Variablen muss ihr ein Objekt zugewiesen werden.
• ‚Normale‘ Referenz-Variablen-Zuweisungen (Operator =) kopieren das Objekt nicht,
sondern erzeugen nur eine weitere Referenz auf das Objekt.
29
null ist in Java ein Schlüsselwort und ist der Defaultwert von Referenz-Variablen. Er hat
nichts mit dem Wert ‘0‘ der elementaren Datentypen zu tun.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
•
•
•
Seite 104 / 409
Bei Funktionsaufrufen unterliegen natürlich auch die Parameter und der Rückgabewert von
nicht-elementaren Datentypen der Referenz-Semantik. Z. B. können also die OriginalObjekte von der aufgerufenen Funktion problemlos verändert werden.
Der Zugriff auf Attribute und Funktionen von Objekten erfolgt über die Referenz-Variable
und den Punkt-Operator.
Der Vergleich zweier Referenz-Variablen vergleicht nicht die Werte der referenzierten
Objekte (Wertgleichheit, bzw. tiefer Vergleich), sondern nur die Gleichheit des
referenzierten Objekts (d.h. Identität, bzw. flacher Vergleich). Achtung – bei Strings kann es
hier ein unerwartetes Verhalten geben – siehe unten bzw. Kapitel 6.2.
StringBuffer buffer1 = null;
if (buffer1==null)
System.out.println("buffer1 ist noch null");
else
System.out.println("buffer1 ist: " + buffer1);
// (1)
buffer1 = new StringBuffer("Hallo");
if (buffer1==null)
System.out.println("buffer1 ist noch null");
else
System.out.println("buffer1 ist: " + buffer1);
// (2)
StringBuffer buffer2 = buffer1;
System.out.println("buffer2 ist: " + buffer2);
// (3)
buffer1.append(" Welt");
System.out.println("buffer1 ist: " + buffer1);
System.out.println("buffer2 ist: " + buffer2);
// (4)
Ausgabe
buffer1
buffer1
buffer2
buffer1
buffer2
ist noch null
ist: Hallo
ist: Hallo
ist: Hallo Welt
ist: Hallo Welt
1.
buffer1:
null
2.
StringBuffer
buffer1:
=>
3.
"Hallo"
StringBuffer
buffer1:
=>
buffer2:
=>
4.
"Hallo"
StringBuffer
buffer1:
=>
buffer2:
=>
"Hallo Welt"
Abb. 5-2 : Referenz-Semantik
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 105 / 409
Achtung – auch Strings sind Objekte und daher führt der Vergleichs-Operator „==“ nur einen
Identitätsvergleich durch, und keinen Wert-Vergleich. Dies ist ein gern gemachter Fehler, der
um so kritischer ist, da der Code manchmal sogar funktioniert – siehe Kapitel 6.2.
Wird versucht über eine Referenz-Variable mit dem Wert „null“ auf das referenzierte Objekt
zuzugreifen (das ja nicht da ist, da ja nichts (null) referenziert wird), so wird dieser Fehler javatypisch mit einer Exception („java.lang.NullPointerException“) gemeldet.
String s = null;
s.length();
// Wirft NullPointerException
6 Operatoren
Folgende Operatoren stellt Java zur Verfügung.
Vorr.
1
Operator
.
[]
(args)
++, --
2
++, -+, -
Operandentyp(en)
Objekt
Array, Integer
Funktion
arithmetisch
As.
L
L
L
L
Operation
Zugriff auf Objekt-Element
Zugriff auf Array-Element
Funktionsaufruf
Postinkrement, -dekrement (unär)
~
arithmetisch
arithmetisch
integral
R
R
R
Präinkrement, -dekrement (unär)
unäres Plus, unäres Minus
bitweises Komplement (unär)
!
boolean
R
logisches Komplement (unär) (logisches Not)
3
new
(Typ)
Klasse
beliebig
R
R
Objekterzeugung
Typumwandlung
4
*, /, %
arithmetisch
L
Multiplikation, Division, Rest
5
+, +
arithmetisch
String
L
L
Addition, Subtraktion
String-Verkettung
6
<<
>>
>>>
integral
integral
integral
L
L
L
Links-Shift
Rechts-Shift mit Vorzeichen-Erweiterung
Rechts-Shift mit Null-Erweiterung
7
<, <=
>, >=
instanceof
arithmetisch
arithmetisch
Typ
L
L
L
kleiner, kleiner-gleich
grösser, grösser-gleich
Typvergleich
8
==
!=
==
!=
einfach
einfach
Objekt
Objekt
L
L
L
L
gleich (identische Werte)
ungleich (unterschiedliche Werte)
gleich (identisches Objekt)
ungleich (unterschiedliche Objekte)
9
&
&
integral
boolean
L
L
bitweises AND
boolsches AND (alle Operanden werden ausgewertet)
10
^
integral
L
bitweises XOR
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 106 / 409
^
boolean
L
boolsches XOR (alle Operanden werden ausgewertet)
11
|
|
integral
boolean
L
L
bitweises OR
boolsches OR (alle Operanden werden ausgewertet)
12
&&
boolean
L
bedingtes AND (Abbruch bei feststehendem Ergebnis)
13
||
boolean
L
bedingtes OR (Abbruch bei feststehendem Ergebnis)
14
?:
boolean, beliebig
R
bedingter (ternärer) Operator
R
R
Zuweisung
Zuweisung mit Operation
15
=
beliebig
*=, /=, %=, beliebig
+=, -=, <<=,
>>=, >>>=,
&=, ^=, |=
• Die meisten dieser Operatoren sollten intuitiv klar sein, da Sie Programmier-Erfahrung
mitbringen.
• Ein paar Operatoren sind nicht zwingend intuitiv in ihrer Benutzung und Semantik, so dass
sie im Weiteren besprochen werden.
• Ein weiterer Teil der Operatoren macht erst im späteren Verlauf der Vorlesung Sinn, da uns
hier noch das entsprechende notwendige Sprachverständnis fehlt. Sie werden in den
jeweiligen Themen-Kapiteln vorgestellt, z.B. der Operator „instanceof“ in Kapitel 14.10.
6.1 Zuweisungs-Operatoren
Der normale Zuweisungs-Operator ‘=’ weist den Wert des rechten Ausdrucks der Variablen auf
der linken Seite zu.
Zusätzlich gibt es in Java noch Zuweisungs-Operatoren mit Operationen. Die Anweisung ‘erg
#= arg;’ entspricht dabei immer der Anweisung ‘erg = erg # (arg);’30.
int i = 7;
int j = 4;
i += 3*j;
System.out.println(i);
// entspricht:
// Ausgabe: 19
i = i + (3*j);
6.2 Gleich- und Ungleich-Operatoren
Bei den elementaren Datentypen vergleichen die Operatoren ‘==’ und ‘!=’ den Wert, während
bei Objekten die Identität (gleiches bzw. verschiedenes Objekt) verglichen wird – siehe auch
z.B. Kapitel 5.4.
Fangen wir mit einem Beispiel an, wo wir sehen, dass bei int’s mit den Werten gearbeitet wird:
int i = 7;
int j = 7;
Durch die angegebene implizite Klammerung um ‘arg’ gibt es niemals Probleme mit den
Operator-Prioritäten - es wird immer Gesamt-‘arg’ auf ‘erg’ angewandt.
30
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
if (i==j)
System.out.println("Gleich");
else
System.out.println("Ungleich");
j = 8;
if (i==j)
System.out.println("Gleich");
else
System.out.println("Ungleich");
Seite 107 / 409
// Ausgabe: Gleich
// Ausgabe: Ungleich
Ganz anders ist das Verhalten bei Objekten, wo mit „==“ bzw. „!=“ die Identität verglichen wird:
StringBuilder sb1 = new StringBuilder("sb");
StringBuilder sb2 = new StringBuilder("sb");
if (sb1==sb2)
System.out.println("Gleich");
else
System.out.println("Ungleich");
// Ausgabe: Ungleich
sb2 = new StringBuilder("xxx");
if (sb1==sb2)
System.out.println("Gleich");
else
System.out.println("Ungleich");
// Ausgabe: Ungleich
sb2 = sb1;
if (sb1==sb2)
System.out.println("Gleich");
else
System.out.println("Ungleich");
// Ausgabe: Gleich
Achtung – dies ist ein gern gemachter Fehler. Wert-Vergleiche auf Objekten müssen meist mit
der Element-Funktion „equals“ gemacht werden, da „==“ nur die Identität vergleicht. Leider muß
man hier sehr vorsichtig sein, da es Klassen gibt bei denen auch „equals“ nur die Identität
vergleicht und nicht den Wert.
StringBuilder sb1 = new StringBuilder("sb");
StringBuilder sb2 = new StringBuilder("sb");
if (sb1.equals(sb2))
System.out.println("Gleich");
else
System.out.println("Ungleich");
sb2 = new StringBuilder("xxx");
if (sb1.equals(sb2))
System.out.println("Gleich");
else
System.out.println("Ungleich");
sb2 = sb1;
if (sb1.equals(sb2))
System.out.println("Gleich");
else
System.out.println("Ungleich");
// Ausgabe: Gleich
// Ausgabe: Ungleich
// Ausgabe: Gleich
Achtung – ganz gefährlich wird es bei Strings. In Kapitel 9.1 werden wir lernen, dass Strings
„immutable“, d.h. unveränderlich, sind. Daher kann die JVM gleiche Strings ohne Gefahr
zusammenlegen, um z.B. Speicher zu sparen. Daher können einzelne – eigentlich nicht
identische Objekte – identisch sein. Es kann also sein, dass Sie in Ihrem Programm bei Strings
fehlerhafterweise „==“ statt „equals“ verwenden, und es erstmal läuft. Gefährlich ist dies
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 108 / 409
deshalb, weil dieses Verhalten nicht zugesichert ist, sondern sich in anderen JVMs wieder
ändern kann.
String s1 = "Hallo";
String s2 = "Hallo";
// Kann vielleicht auf das gleiche Objekt zeigen
if (s1==s2) {
System.out.println("Identisch ");
}
else {
System.out.println("NICHT identisch ");
}
// Identitaets-Abfrage
if (s1.equals(s2)) {
System.out.println("Gleicher Wert");
}
// Defenitiv Wert-Vergleich
mögliche Ausgabe
NICHT identisch
Gleicher Wert
6.3 Geteilt- und Modulo-Operator
Während die normalen mathematischen Operatoren (plus, minus und mal) in ihrem Verhalten
ziemlich intuitiv sind, ist dies beim Geteilt- und Modulo-Operator vielleicht nicht so.
6.3.1 Geteilt-Operator
Im Prinzip arbeitet auch der Geteilt-Operator ganz intuitiv, außer man wendet ihn auf integrale
Typen (byte, short, int und long) an und erwartet dann ein Fließkomma-Ergebnis (z.B. double).
Das funktioniert so nicht – die Division bleibt im Bereich der Typen der Operanden, und das
betrifft auch das Ergebnis.
Teilen Sie Integer durch Integer, so ist das Ergebnis wieder ein Integer – selbst wenn das
Ergebnis damit nicht exakt ist. Das Ergebnis ist nur der ganz-teilige Anteil der Division, den
Rest kann man mit dem Modulo-Operator bestimmen – siehe Kapitel 6.3.2.
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
Ausgabe
10 / 1
10 / 2
10 / 3
10 / 4
10 / 5
10 / 6
10 / 7
10 / 8
10 / 9
->
->
->
->
->
->
->
->
->
/ 1 -> "
/ 2 -> "
/ 3 -> "
/ 4 -> "
/ 5 -> "
/ 6 -> "
/ 7 -> "
/ 8 -> "
/ 9 -> "
/ 10 -> "
/ 11 -> "
/ 12 -> "
+
+
+
+
+
+
+
+
+
+
+
+
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
/
/
/
/
/
/
/
/
/
/
/
/
1));
2));
3));
4));
5));
6));
7));
8));
9));
10));
11));
12));
//
//
//
//
//
//
//
//
//
//
//
//
-> 10
-> 5
-> 3
-> 2
-> 2
-> 1
-> 1
-> 1
-> 1
-> 1
-> 0
-> 0
10
5
3
2
2
1
1
1
1
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 109 / 409
10 / 10 -> 1
10 / 11 -> 0
10 / 12 -> 0
Wie dividiert aber nun zwei Integer-Zahlen so, daß man das korrekte Fließkomma-Zahlen
Ergebnis erhält? Ganz einfach: man muß vor der Division mindestens einen der beiden
Operanden in den gewünschten Fließkomma-Zahlen-Typ („float“ oder „double“) konvertieren.
final int divisor = 7;
final int divident = 2;
int res1 = divisor / divident;
double res2 = (double) divisor / divident;
double res3 = divisor / (double) divident;
double res4 = (double) divisor / (double) divident;
System.out.println("res1
System.out.println("res2
System.out.println("res3
System.out.println("res4
Ausgabe
res1 ->
res2 ->
res3 ->
res4 ->
->
->
->
->
"
"
"
"
+
+
+
+
res1);
res2);
res3);
res4);
//
//
//
//
->
->
->
->
3
3,5
3,5
3,5
3
3.5
3.5
3.5
Hinweis – für Konvertierungen zwischen elementaren Datentypen siehe Kapitel 5.1.2.
6.3.2 Modulo-Operator
Der Modulo-Operator kann nur auf integrale Typen angewendet werden, und liefert dann den
Rest der Integer-Division – siehe Kapitel 6.3.1.
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
System.out.println("10
Ausgabe
10 % 1
10 % 2
10 % 3
10 % 4
10 % 5
10 % 6
10 % 7
10 % 8
10 % 9
10 % 10
10 % 11
10 % 12
->
->
->
->
->
->
->
->
->
->
->
->
% 1 -> "
% 2 -> "
% 3 -> "
% 4 -> "
% 5 -> "
% 6 -> "
% 7 -> "
% 8 -> "
% 9 -> "
% 10 -> "
% 11 -> "
% 12 -> "
+
+
+
+
+
+
+
+
+
+
+
+
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
(10
%
%
%
%
%
%
%
%
%
%
%
%
1));
2));
3));
4));
5));
6));
7));
8));
9));
10));
11));
12));
//
//
//
//
//
//
//
//
//
//
//
//
-> 0
-> 0
-> 1
-> 2
-> 0
-> 4
-> 3
-> 2
-> 1
-> 0
-> 10
-> 10
0
0
1
2
0
4
3
2
1
0
10
10
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 110 / 409
6.4 Bitweises AND, OR, XOR
Überarbeiten todo
Die Operatoren ‘&’, ‘|’ und ‘^’ arbeiten sowohl mit integralen als auch mit boolschen Datentypen
zusammen.
Bei integralen Datentypen werden die interne Darstellung bitweise miteinander verknüpft.
AND
Op. 1
0
0
1
1
Op.2
0
1
0
1
OR
Erg.
0
0
0
1
Op. 1
0
0
1
1
Op.2
0
1
0
1
XOR
Erg.
0
1
1
1
Op. 1
0
0
1
1
Op.2
0
1
0
1
Erg.
0
1
1
0
6.5 Boolsches bzw. bedingtes AND, OR, XOR
Bei boolschen Operanden werden die Operanden logisch miteinander verknüpft, wobei immer
alle Operanden ausgewertet werden.
Bei den bedingten Operatoren ‘&&’ und ‘||’ werden die Operanden auch logisch miteinander
verknüpft, aber die Auswertung wird abgebrochen wird, sobald das Ergebnis feststeht. Es kann
daher passieren, dass nicht alle Operanden ausgewertet werden.
public static boolean fct(int i) {
System.out.println("fct(" + i + ')');
return true;
}
public static void main(String[] args) {
boolean erg, b = true;
erg = b & fct(1);
erg = b | fct(2);
erg = b ^ fct(3);
erg = b && fct(4);
erg = b || fct(5);
b = false;
erg = b & fct(6);
erg = b | fct(7);
erg = b ^ fct(8);
erg = b && fct(9);
erg = b || fct(10);
}
Ausgabe
fct(1)
fct(2)
fct(3)
fct(4)
fct(6)
fct(7)
fct(8)
fct(10)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 111 / 409
6.6 Prä- und Post-Inkrement und -Dekrement Operatoren
Den Operator ‘++’ gibt es in Prä- und Postfix-Notation. Er ist für alle arithmetischen Typen
definiert und entspricht einer Erhöhung um ‘1’. Analog entspricht der ‘—‘ Operator einer
Erniedrigung um ‘1’.
int i = 7;
i++;
System.out.println(i);
++i;
System.out.println(i);
i--;
System.out.println(i);
--i;
System.out.println(i);
// Ausgabe: 8
// Ausgabe: 9
// Ausgabe: 8
// Ausgabe: 7
Im obigen Beispiel ist die Unterscheidung in Prä- und Postfix-Notation egal, aber in anderen
Zusammenhängen ist sie es nicht.
Diese Operatoren können direkt in arithmetischen Ausdrücken benutzt werden - in so einem
Fall entscheidet die Notation, ob die Variable zuerst ausgewertet und dann verändert wird, oder
umgekehrt:
• Präfix-Notation ‘++x’ bzw. ‘–-x’
1. verändern von ‘x’
2. auswerten von ‘x’
• Postfix-Notation ‘x++’ bzw. ‘x--’
1. auswerten von ‘x’
2. verändern von ‘x’
int i = 7;
int j = ++i;
System.out.println(i + " " + j);
j = i--;
System.out.println(i + " " + j);
// Ausgabe: 8 8
// Ausgabe: 7 8
Im Gegensatz zu einigen anderen Programmiersprachen wie z.B. C und C++ ist auch bei
Verwendung mehrere dieser Operatoren in einer Anweisung das Ergebnis exakt definiert, da es
in Java eine eindeutige Abarbeitung von Ausdrücken gibt: zuerst werden bei einer Anweisung
die Operanden von links nach rechts ausgewertet, dann wird der Rest der Anweisung
durchgeführt – mit der Ausnahme von Operanden bei bedingtem Und und Oder!
int i = 2;
int j = ++i + ++i * ++i;
System.out.println(i);
// entspricht 3+4*5
// Ausgabe: 23
6.7 Schiebe-Operatoren
Die Schiebe-Operatoren ‘<<’, ‘>>’ und ‘>>>’ wirken nur auf integrale Datentypen.
Der Operator ‘<<’ schiebt die Bits des integralen Operanden um n-Bits nach links, d.h. er
bewirkt bei allen Zahlen (negativ und positiv) eine Multiplikation mit 2.
int i = 3;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
int j = -5;
System.out.println((i<<1) + " " + (j<<2));
Seite 112 / 409
// Ausgabe: 6 -20
Der Operator ‘>>’ schiebt die Zahl um n-Bits nach rechts mit Vorzeichen-Erweiterung. D.h. das
Vorzeichen der Zahl bleibt erhalten. Dagegen füllt der Operator ‘>>>’ die Zahl immer mit NullBits auf - er betrachtet die Zahl quasi immer als vorzeichenlos.
int i = 5;
int j = -5;
int k = -5;
System.out.println((i>>1)+" "+(j>>1)+" "+(k>>>1));
// Ausgabe: 2 -3 2147483645
6.8 Frage-Zeichen Operator
todo
6.9 Aufgaben
6.9.1 Aufgabe „Inkrement-Operator“
Schreiben Sie ein kleines Programm, das den Unterschied im Verhalten der Prä- und PostInkrement Operatoren („++“) aus Kapitel 6.6 verdeutlicht, d.h. wo ein Vertauschen der
Operatoren zu einem anderen Ergebnis führt.
Lösung siehe Kapitel 6.10.
6.10 Lsg. zu Aufgabe „Inkrement-Operator“ – Kap. 6.9.1
Um das unterschiedliche Verhalten der Operatoren für Prä- und Post-Inkrement zu sehen, ist
es wichtig zu verstehen, dass die Addition um „1“ nicht die einzige Aktion der Anweisung ist.
Die beiden folgenden Zeilen unterscheiden sich semantisch (d.h. in ihrer Wirkung) nicht, da hier
die Addition die einzige veränderne Aktion in der Anweisung ist.
++i;
i++;
// Diese beiden Zeilen haben so fuer sich die gleiche Wirkung,
// d.h. hat ein Vertauschen der Operatoren hat hier keine Auswirkung
Um den Unterschied zu sehen, muss man den Wert des „++“ Ausdrucks in der Anweisung
auswerten. Dazu führen wir eine zweite Variable ein, der wir den „++“ Ausdruck zuweisen.
j = ++i;
j = i++;
Jetzt müssen wir nur noch für gleiche Startwerte sorgen, und das Ergebnis ausgeben. Hier am
Beispiel des Prä-Inkrement-Operators:
int i = 4;
int j = 0;
System.out.println("Prae-Inkrement");
j = ++i;
System.out.println("j = ++i");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 113 / 409
System.out.println("=> i: 4 -> " + i);
System.out.println("=> j: 0 -> " + j);
Jetzt das gleiche noch für den Post-Inkrement-Operator, und dann das ganze in eine Klasse
und eine Main-Funktion – schon ist das Programm fertig:
public class Appl {
public static void main(String[] args) {
int i = 4;
int j = 0;
System.out.println("Prae-Inkrement");
j = ++i;
System.out.println("j = ++i");
System.out.println("=> i: 4 -> " + i);
System.out.println("=> j: 0 -> " + j);
i = 4;
j = 0;
System.out.println("\nPost-Inkrement");
j = i++;
System.out.println("j = i++");
System.out.println("=> i: 4 -> " + i);
System.out.println("=> j: 0 -> " + j);
}
}
Ausgabe
Prae-Inkrement
j = ++i
=> i: 4 -> 5
=> j: 0 -> 5
Post-Inkrement
j = i++
=> i: 4 -> 5
=> j: 0 -> 4
An der Ausgabe sieht man sehr schön den Unterschied:
• Beim Prä-Inkrement wird zuerst das „i“ erhöht (von 4 auf 5), und dann der neue Wert von „i“
(5) der Variablen „j“ zugewiesen. Daher haben beide Variablen nach der Anweisung den
Wert „5“.
• Beim Post-Inkrement wird zuerst der bisherige Wert (4) des „i“ genommen und als Ergebnis
des Ausdrucks benutzt, d.h. hier dem „j“ zugewiesen. „j“ hat also den Wert „4“. Erst danach
wird „i“ erhöht, und bekommt damit den Wert „5“. Diese Erhöhung hat keinen Einfluß mehr
auf den Rest dieser Anweisung.
7 Kontroll-Strukturen
7.1 Bedingter-Kontrollfluss – If
Die wichtigste Kontroll-Struktur ist die If-Anweisung. Sie ermöglicht einen bedingten Sprung,
d.h. in Abhängigkeit von einem Boolschen-Ausdruck unterschiedliche Pfade im Programm zu
durchlaufen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 114 / 409
7.1.1 Einfachste Form
Die einfachste Form der If-Anweisung sieht folgendermaßen aus:
Syntax:
if (boolean-ausdruck)
true-anweisung
Sie beginnt mit dem Schlüsselwort „if“ – dann folgt in runden Klammern ein Ausdruck, der vom
Typ „boolean“ sein muss. Abgeschlossen wird sie durch eine Anweisung. Diese Anweisung
wird nur ausgeführt, wenn der Ausdruck zu „true“ ausgewertet wurde. Im Falle von „false“ wird
die Anweisung übersprungen.
int n = 5;
if (n<7)
System.out.println("n<7");
if (n>20)
System.out.println("n>20");
if (n!=6)
System.out.println("n!=6");
Ausgabe
n<7
n!=6
7.1.2 Blöcke für mehrere Anweisungen
Soll im If-Zweig mehr als eine Anweisung ausgeführt werden, so muß mit den geschweiften
Klammern ein Block gebildet werden. Ein solcher Block steht dabei syntaktisch für eine
einzelne Anweisung.
int n = 5;
if (n<7) {
System.out.println("n<7");
System.out.println("n ist " + n);
System.out.println("Und weiter geht es...");
}
Ausgabe
n<7
n ist 5
Und weiter geht es...
Hinweis – dies gilt genauso für Schleifen oder andere Konstrukte, die normalerweise nur auf
eine Anweisung wirken.
7.1.3 If-Else Anweisung
Für den Fall, dass neben dem True-Fall auch beim False-Fall Aktionen ausgeführt werden
sollen, kann auf die Anweisung (bzw. dem Block) des True-Falls das Schlüsselwort „else“ mit
einer Anweisung für den False-Fall folgen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 115 / 409
Syntax:
if (boolean-ausdruck)
true-anweisung
else
false-anweisung
Beispiel:
int n = 5;
if (n<7)
System.out.println("n<7");
else
System.out.println("n>=7");
if (n>20)
System.out.println("n>20");
else
System.out.println("n<=20");
Ausgabe
n<7
n<=20
Hinweis – auf für den False-Zweig gilt natürlich: Wenn mehr als eine Anweisung notwendig ist,
dann muß ein Block genutzt werden – siehe Kapitel 7.1.2.
7.1.4 If-Anweisung ohne True-Zweig
Es gibt keine Syntax, um eine If-Anweisung nur mit False-Zweig ohne True-Zweig zu
implementieren. Ist dies gewünscht, so gibt es zwei Implementierungs-Möglichkeiten:
• True-Zweig mit leerer Anweisung
• Bool-Ausdruck in der If-Anweisung umformulieren
True-Zweig mit leerer Anweisung
In Java sind leere Anweisungen erlaubt – sie bestehen aus einem einzelnen Semikolon. Damit
läßt sich der True-Zweig einer If-Anweisung sehr schnell abhandeln.
int n = 10;
if (n<7);
// <= dieses Semikolen ist der leere True-Zweig
else
System.out.println("n>=7");
Ausgabe
n>=7
Achtung – so ein leerer True-Zweig ist nicht besonders gut lesbar. Und so ein einzelnes
Semikolon ist schnell überlesen bzw. verwirrend – von daher ist die Lösung nicht zu empfehlen.
Formulieren Sie besser den Bool-Ausdruck um.
Bool-Ausdruck in der If-Anweisung umformulieren
Formulieren Sie den boolschen Ausdruck so um, dass er statt „true“ „false“ liefert und
umgekehrt. Wenn sich der Ausdruck nicht umformulieren läßt, bzw. die Umformulierung
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 116 / 409
aufwändig und fehlerträchtig ist – dann nutzen Sie einfach den boolschen Not-Operator „!“ um
den boolschen Ausdruck zu negieren.
int n = 10;
if (n>=7)
System.out.println("n>=7");
// Ausdruck umformuliert
if (!(n<7))
System.out.println("n>=7");
// Ausdruck negiert mit !
Ausgabe
n>=7
n>=7
7.1.5 Mehrfach-Verzweigungen mit If-ElseIf
Nicht immer gibt es nur einen True-Fall bzw. einen True- und einen False-Fall. Häufig hat man
eine Art Auflistung von verschiedenen Fällen, d.h. eine Mehrfach-Verzweigung. In diesem
Fällen nimmt man verschachtelte If-Anweisungen, die auch als If-ElseIf-Anweisung bezeichnet
wird.
Im Prinzip ist eine If-ElseIf-Anweisung nichts neues – hier wird nur der False-Zweig durch eine
If-Anweisung dargestellt. Der Unterschied ist eine eigenständige Einrückungs-Konvention, bei
der das „if“ direkt auf das „else“ folgt.
Syntax:
if (boolean-ausdruck)
true-anweisung
else if (boolean-ausdruck)
true-2-anweisung
else
false-anweisung
Beispiel:
int age = 27;
if (age<10)
System.out.println("Kind im Alter von " + age);
else if (age<18)
System.out.println("Jugendliche(r) im Alter von " + age);
else if (age<30)
System.out.println("Junge(r) Erwachsene(r) im Alter von " + age);
else if (age<70)
System.out.println("Erwachsene(r) im Alter von " + age);
else
System.out.println("Alte(r) Frau/Mann im Alter von " + age);
Ausgabe
Junge(r) Erwachsene(r) im Alter von 27
Hinweis – auch hier ist der Else-Zweig natürlich optional.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 117 / 409
7.2 Mehrfach-Verzweigung – Switch
Für spezielle Mehrfach-Verzweigungen in Abhängigkeit von einer integralen Variable gibt es in
Java die Switch-Anweisung. Sie funktioniert für die Typen „char“, „byte“, „short“ und „int“, d.h.
Typen mit integralen Werten mit max. 4 Byte Größe, und zusätzlich seit dem JDK 1.5 mit
Enums (siehe Kapitel todo) und seit dem JDK 1.7 auch mit Strings.
Außerdem kann der integrale Ausdruck nur auf Gleichheit zu Compile-Zeit Konstanten des
entsprechenden Typs getestet werden. Wenn man mit diesen Einschränkungen leben kann, ist
eine Switch-Anweisung einer If-Else-If Mehrfach-Verzweigung vorziehen.
Syntax
switch (ausdruck) {
case constant1:
anweisung; ...
[break;]
case constant2:
anweisung; ...
[break;]
...
default:
anweisung; ...
[break;]
}
• ausdruck muss vom Typ „char“, „byte“, „short“, „int“, „Enum“ (seit JDK 1.5) oder „String“
(seit JDK 1.7) sein.
• constant muss eine Compile-Zeit-Konstante vom entsprechenden Typ sein.
public class Appl {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
switch (i) {
case 0:
System.out.println("Null");
break;
case 1:
System.out.println("Eins");
break;
case 3:
System.out.println("Drei");
break;
default:
System.out.println("Unbekannt");
}
}
}
}
Ausgabe
Null
Eins
Unbekannt
Drei
Unbekannt
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 118 / 409
Erst die „break“ Anweisung beendet einen Case-Block. Ist kein „break“ vorhanden, so läuft der
Programmfluss in den nächsten Case-Block hinein – auch in den Default-Block. Auch im
Default-Block darf das „break“ stehen – hat hier aber keine Bedeutung – siehe Beispiel:
for (int i=0; i<7; i++) {
switch (i) {
case 1:
System.out.println("i ist
break;
case 2:
case 3:
System.out.println("i ist
case 4:
System.out.println("Fluss
break;
default:
System.out.println("i ist
break;
}
}
1");
2 oder 3");
kommt aus 2/3 oder i ist 4");
weder 1,2,3 oder 4");
// Nicht notwendig
Ausgabe
i ist weder 1,2,3 oder 4
i ist 1
i ist 2 oder 3
Fluss kommt aus 2/3 oder i ist 4
i ist 2 oder 3
Fluss kommt aus 2/3 oder i ist 4
Fluss kommt aus 2/3 oder i ist 4
i ist weder 1,2,3 oder 4
i ist weder 1,2,3 oder 4
Seit JDK 1.5 kann man die Switch-Anweisung auch für Enums (siehe Kapitel 12.6) verwenden.
Achtung, das Beispiel enthält mehrere Sprachmittel (u.a. die Enums), die erst in späteren
Kapiteln eingeführt werden:
public class Appl {
private enum Color {
RED, GREEN, BLUE
};
private static void switchIt(Color c) {
switch (c) {
case RED:
System.out.println("rot");
break;
case GREEN:
System.out.println("gruen");
break;
case BLUE:
System.out.println("blau");
break;
}
}
public static void main(String[] args) {
switchIt(Color.RED);
switchIt(Color.GREEN);
switchIt(Color.BLUE);
}
}
Ausgabe
rot
gruen
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 119 / 409
blau
Und seit JDK 1.7 funktioniert die Switch-Anweisungs auch mit Strings.
public class Appl {
private static void doStringSwitch(String s) {
switch (s) {
case "Text 1":
System.out.println("Text 1 gefunden");
break;
case "Text 2":
System.out.println("Text 2 gefunden");
break;
default:
System.out.println("Unbekannter Text");
break;
}
}
public static void main(String[] args) {
doStringSwitch("Text 1");
doStringSwitch("Text 2");
doStringSwitch("Text 3");
}
}
Ausgabe
Text 1 gefunden
Text 2 gefunden
Unbekannter Text
Hinweis – der Java Compiler wandelt die Switch-Anweisungen mit Strings in eine Art „HashTable“ mit Sprunganweisung um. Daher wird die Switch-Anweisungen bei vielen Case-Fällen
wahrscheinlich performanter sein als If-Else-If-Else-Anweisungen.
7.3 For-Schleife
Syntax
for (init; test; update)
anweisung
Beispiel
for (int i=0; i<8; i++) {
System.out.print(i);
}
Ausgabe
01234567
Hinweis – werden im Initialisierungs-Teil ein oder mehrere Variablen definiert, so sind diese
nur im Scope der Schleife bekannt.
for (int i=0; i<8; i++) {
System.out.print(i);
}
System.out.print(i);
© Detlef Wilkening 1997-2016
// Definition von i gilt nur fuer die Schleife
// Compiler-Fehler – Variable i ist unbekannt
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 120 / 409
Hinweis – mit dem JDK 1.5 wurde ein weiterer For-Schleifentyp für Container und Arrays
eingeführt – dieser wird im nächsten Kapitel 7.4 beschrieben.
7.4 For-Schleife für Container und Arrays
In Java 5 (JDK 1.5) wurde ein neuer For-Schleifen-Typ für Container (siehe Kapitel 9.3) und
Arrays (siehe Kapitel 10.3) eingeführt.
Achtung – die folgenden beiden Programme können Sie hier noch nicht verstehen, da in ihnen
sowohl Arrays als auch typisierte Container vorkommen. Da es aber hier im Kapitel u.a. um
Schleifen geht, habe ich diesen Schleifen-Typ hier auch beschrieben – kommen Sie später
nochmal zurück zu diesem Kapitel.
Neuer Schleifentyp für Arrays:
int[] a = new int[]{ 1, 2, 3, 5, 7, 11 };
for (int n : a) {
System.out.print("" + n + ' ');
}
Ausgabe
1 2 3 5 7 11
Neuer Schleifentyp für Container:
ArrayList<String> al = new ArrayList<String>();
al.add("eins");
al.add("zwei");
al.add("drei");
for (String s : al) {
System.out.println("-> " + s);
}
Ausgabe
-> eins
-> zwei
-> drei
7.5 While-Schleife
Syntax
while (boolan-ausdruck)
anweisung
Die While-Schleife wird solange wiederholt, wie der ausdruck true ist.
Beispiel
int i = 0;
while (i<3) {
System.out.print(i + " ");
i++;
}
Ausgabe
0 1 2
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 121 / 409
7.6 Do-Schleife
Syntax
do
anweisung
while (boolean-ausdruck);
Die Do-Schleife wird solange wiederholt, wie der Ausdruck true ist.
Beispiel
int i = 0;
do {
System.out.print(i + " ");
i++;
} while (i<3);
Ausgabe
0 1 2
Im Gegensatz zur While-Schleife wird die Do-Schleife immer mindestens einmal durchlaufen.
7.7 Break- und Continue-Anweisung
7.7.1 Break
Das Schlüsselwort „break“ in einer beliebigen Schleife sorgt für einen sofortigen Abbruch der
Schleife.
for (int i=0; i<20; i++) {
System.out.print(i + " ");
if (i>=5) break;
}
System.out.print("Schleifenende");
Ausgabe
0 1 2 3 4 5 Schleifenende
Dies gilt nur für die innerste Schleife, falls Sie mehrere Schleifen ineinander verschachtelt
haben.
for (int i=0; i<2; i++) {
for (int j=0; j<20; j++) {
System.out.print(j + " ");
if (j>=5) break;
}
System.out.print("Ende-von-j-Schleife\n");
}
System.out.print("Schleifenende");
Ausgabe
0 1 2 3 4 5 Ende-von-j-Schleife
0 1 2 3 4 5 Ende-von-j-Schleife
Schleifenende
7.7.2 Continue
Das Schlüsselwort „continue“ in einer Schleife sorgt für einen sofortigen Sprung an den
Schleifen-Kopf – wobei auch bei der Do-Schleife die Test-Bedingung zum Schleifen-Kopf zählt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 122 / 409
Achtung – continue wirkt bei For-Schleifen anders als bei While- und Do-Schleifen, da sich bei
diesen Schleifen die Wirkung des Schleifenkopfes unterscheidet:
• Bei einer For-Schleife wird die Update-Ausdruck ausgeführt, und dann der Test-Ausdruck
ausgewertet – mit einem möglichem Abbruch der Schleife.
• Bei einer While- oder einer Do-Schleife wird nur der Test-Ausdruck ausgewertet – mit einem
möglichem Abbruch der Schleife.
System.out.println("For");
for (int i = 0; i < 7; i++) {
System.out.print(i + " ");
if (i == 3) {
System.out.print(" ** ");
i += 2;
continue;
}
System.out.print(i + " - ");
}
System.out.println("\nWhile");
int i = 0;
while (i < 7) {
System.out.print(i + " ");
if (i == 3) {
System.out.print(" ** ");
i += 2;
continue;
}
System.out.print(i + " - ");
i++;
}
System.out.println("\nDo");
i = 0;
do {
System.out.print(i + " ");
if (i == 3) {
System.out.print(" ** ");
i += 2;
continue;
}
System.out.print(i + " - ");
i++;
} while (i < 7);
Ausgabe
For
0 0 - 1 1 - 2 2 - 3
While
0 0 - 1 1 - 2 2 - 3
Do
0 0 - 1 1 - 2 2 - 3
** 6 6 ** 5 5 - 6 6 ** 5 5 - 6 6 -
Hinweis – auch „continue“ wirkt nur für die innerste Schleife. Wollen Sie zum Schleifenkopf
einer äußeren Schleife springen, so müssen Sie eine gelabelte Sprung-Anweisung nutzen –
siehe nächstes Kapitel.
7.8 Gelabelte Sprung-Anweisungen
Gelabelte Sprung-Anweisungen ermöglichen ein break oder continue über mehrere SchleifenEbenen hinweg.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 123 / 409
Syntax
break label;
continue label;
Dazu muß eine Schleife mit einem Label versehen werden – dies geschieht mit dem LabelNamen und einem folgenden Doppelpunkt. Wird jetzt innerhalb einer solchen Schleife eine
gelabelte Sprunganweisung ausgeführt, so bezieht sie sich auf die Schleife mit dem passenden
Label, auch wenn diese nicht die innerste ist.
Beispiel
outerLoop: for (int i=0; i<5; i++) {
for (int j=0; j<3; j++) {
System.out.print(i + " " + j + " / ");
if (i+j > 3) break outerLoop;
}
}
Ausgabe
0 0 / 0 1 / 0 2 / 1 0 / 1 1 / 1 2 / 2 0 / 2 1 / 2 2 /
Hinweis – eine gelabelte Sprunganweisung darf natürlich nur innerhalb einer Schleife mit
einem passenden Label stehen. Man kann mit ihnen also nicht in eine ganz andere Schleife
springen.
7.9 Aufgaben
7.9.1 Aufgabe „Schleifen-Varianten“
Schreiben Sie ein Programm, dass dreimal hintereinander die Zahlen von 1 bis 5 ausgibt. Für
die erste „1..5“ Ausgabe soll eine For-Schleife benutzt werden, für die zweite eine WhileSchleife, und für die dritte eine Do-Schleife. Jede „1..5“ Ausgabe soll in einer eigenen Zeile
stehen, und die Zahlen sollen mit einem Leerzeichen untereinander getrennt sein.
Ausgabe
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
Lösung siehe Kapitel 7.10.
7.9.2 Aufgabe „Teilbar?“
Schreiben Sie ein Programm, dass hintereinander die Zahlen von 1 bis 10 und jeweils eine
Meldung, ob die Zahl durch 2 bzw. durch 3 teilbar ist, ausgibt. Benutzen Sie, um die Zahlen zu
durchlaufen, eine For-Schleife.
Mögliche Ausgabe:
1 nicht-durch-2-teilbar nicht-durch-3-teilbar
2 durch-2-teilbar nicht-durch-3-teilbar
3 nicht-durch-2-teilbar durch-3-teilbar
4 durch-2-teilbar nicht-durch-3-teilbar
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
5 nicht-durch-2-teilbar
...
Seite 124 / 409
nicht-durch-3-teilbar
Lösung siehe Kapitel 7.11.
7.9.3 Aufgabe „Lesbare Zahlen“
Schreiben Sie ein Programm, dass hintereinander die Zahlen von 1 bis 5 als lesbaren Text
ausgibt. Benutzen Sie, um die Zahlen zu durchlaufen, eine For-Schleife. Jede Zahl-Ausgabe
soll in einer eigenen Zeile stehen.
Mögliche Ausgabe:
eins
zwei
drei
vier
fuenf
Vergeichen Sie zu dieser Aufgabe auch die Aufgaben 9.8.4 (Lösung in Kapitel 9.12) und 10.8.1
(Lösung in Kapitel 10.9).
Lösung siehe Kapitel 7.12.
7.9.4 Aufgabe „Zahlen-Liste“
Schreiben Sie ein Programm, dass hintereinander die Zahlen von 1 bis 5 ausgibt. Die Zahlen
sollen dabei durch einen Bindestrich ‚-‚ getrennt werden, und die gesamte Zahlenreihe soll von
runden Klammern umgeben sein. Benutzen Sie, um die Zahlen zu durchlaufen, eine ForSchleife.
Ausgabe:
(1-2-3-4-5)
Lösung siehe Kapitel 7.13.
7.9.5 Aufgabe „Mittelwert und Varianz“
Schreiben Sie ein Programm, das den Mittelwert und die Varianz von Gleitkomma-Zahlen
berechnet. Der Benutzer gibt die Werte auf der Kommandozeile an. Der Mittelwert ist die
Summe der Werte geteilt durch die Anzahl. Die Varianz ist die Summe der Quadrate der Werte
geteilt durch die um eins reduzierte Anzahl. Fangen sie fehlerhafte Eingaben wieder sinnvoll
ab. Denken Sie sich ein sinnvolles Abbruchkriterium aus, d.h. was muss der Benutzer
eingeben, wenn er fertig mit der Eingabe ist.
Um auch dieses Programm erstmal etwas einfacher zu gestalten, könnten Sie – ähnlich zu
Aufgabe 4.8.5 – eine Version schreiben, die die Werte als Kommandozeilen-Argumente
übergeben bekommt statt Sie einzulesen.
Lösung siehe Kapitel 7.14.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 125 / 409
7.9.6 Aufgabe „Multiplikations-Matrix“
Schreiben Sie ein Programm, das zwei positive Integer-Zahlen einliest, und für diese (von bis
inkl.) das Ein-mal-eins als Matrix ausgibt.
Hinweise:
• Fangen Sie fehlerhafte Eingaben ab. Diesen Hinweis gibt es hier jetzt zum letzten Mal,
danach wird er als selbstverständlich vorausgesetzt.
• Wie in Aufgabe 4.8.5 und Aufgabe 7.9.5 ist es für den Einstieg möglich, zuerst eine Version
mit Kommandozeilen-Argumenten zu schreiben.
Beispiel:
Zahl1: 2
Zahl2: 4
| 2 3 4
---+-----------2 | 4 6 8
3 | 6 9 12
4 | 8 12 16
Hinweis – für eine schönere Formatierung fehlt uns leider noch das Wissen.
Lösung siehe Kapitel 7.15.
7.10 Lsg. zu Aufgabe „Schleifen-Varianten“ – Kap. 7.9.1
Diese Aufgabe ist relativ einfach, da sie nur einfach die drei Schleifen-Typen aus den Kapiteln
7.3, 7.5 und 7.6 implementieren müssen. Letzlich ist das ja auch der Sinn der Aufgabe, einmal
mit den drei Schleifen-Typen von Java vertraut zu werden.
Für einen Java Anfänger hat die Aufgabe vielleicht trotzdem zwei kleine Knackpunkte:
• Die Ausgabe der Zahl müssen Sie mit „System.out.print“ machen (z.B. Zeile *), damit kein
Zeilenumbruch erzeugt wird. Hinter jedem Schleifen-Typ muss dann ein Zeilenumbruch
erzeugt werden – z.B. mit „System.out.println()“ (z.B. Zeile **).
• Für alle drei Schleifen wird eine Zählvariable benötigt. Hier müssen Sie mit der Sichtbarkeit
der Variablen (siehe u.a. Kapitel 5.3) aufpassen. Für die While- und die Do-Schleife
benötigen Sie eine Zähl-Variable, die außerhalb der Schleife definiert ist (Zeile ***). Damit ist
klar, dass Sie sie für die jeweilig zweite Schleife nicht mehr definieren müssen, sondern nur
den Start-Wert neu setzen müssen (Zeile ****).
Die For-Schleife ist hier eine Besonderheit. Da der Schleifenkopf zur Sichtbarkeit der
Schleife gehört, können (und sollten Sie im Normalfall auch) Sie die Zähl-Variable dort
definieren. Dann steht sie aber für die anderen beiden Schleifen-Typen nicht mehr zur
Wiederverwendung zur Verfügung. Anders als das Beispiel könnten Sie hier also auch eine
Zähl-Variable wie in Zeile (***) definieren, und sie auch für die For-Schleife
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 126 / 409
wiederverwenden.
Etwas anderes passiert, wenn Sie die For-Schleife nicht als ersten, sondern als zweiten oder
dritten Schleifen-Typ verwenden. In diesem Fall können Sie in der For-Schleife keine ZählVariable mehr definieren – zumindest nicht mit dem gleichen Namen – da Java keine
mehrfachen Variablen-Namen innerhalb einer Funktion zuläßt (siehe Kapitel 8.3).
public class Appl {
public static void main(String[] args) {
for (int i=1; i<6; i++) {
System.out.print(i + " ");
}
System.out.println();
// (*****)
// (*)
// (**)
int i = 1;
while (i<6) {
System.out.print(i++ + " ");
}
System.out.println();
// (***)
i = 1;
do {
System.out.print(i++ + " ");
} while (i<6);
System.out.println();
// (****)
}
}
Ausgabe
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
7.11 Lsg. zu Aufgabe „Teilbar?“ – Kap. 7.9.2
Die Aufgabe besteht aus zwei Teilen.
• Zuerst müssen natürlich die Zahlen von 1 bis 10 durchlaufen werden.
• Danach muss für jede Zahl geprüft werden, ob sie durch 2 bzw. 3 teilbar ist, und dann eine
entsprechende Meldung ausgegeben werden.
Teil 1 sollten sie mittlerweile problemlos implementieren können, da sie hierfür nur eine
einfache Zählschleife, also eine For-Schleife benötigen.
for (int i=1; i<=10; i++) {
...
}
Teil 2 setzt die richtige Idee voraus, wie man die Teilbarkeit testen kann. Die Lösung ist hier der
Modulo-Operator „%“ – siehe Kapitel 6. Er gibt den Rest der Integer-Division zurück, und die ist
„0“, wenn der erste Operand durch den zweiten Operanden teilbar ist. Mit einer entsprechenden
Meldung sieht der Test auf Teilbarkeit durch 2 also z.B. so aus:
if (i%2!=0) {
System.out.print("nicht-");
}
System.out.print("durch-2-teilbar");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 127 / 409
Damit ist alles vorhanden – jetzt müssen die Teile nur noch zusammengefügt werden:
• Klasse mit Main-Funktion
• For-Schleife mit Ausgabe
• Test auf Teilbarkeit auf „2“ und „3“
Hier das gesamte Programm:
public class Appl {
public static void main(String[] args) {
for (int i=1; i<=10; i++) {
System.out.print(i + " ");
if (i%2!=0) {
System.out.print("nicht-");
}
System.out.print("durch-2-teilbar
");
if (i%3!=0) {
System.out.print("nicht-");
}
System.out.println("durch-3-teilbar");
}
}
}
Ausgabe:
1 nicht-durch-2-teilbar nicht-durch-3-teilbar
2 durch-2-teilbar nicht-durch-3-teilbar
3 nicht-durch-2-teilbar durch-3-teilbar
4 durch-2-teilbar nicht-durch-3-teilbar
5 nicht-durch-2-teilbar nicht-durch-3-teilbar
6 durch-2-teilbar durch-3-teilbar
7 nicht-durch-2-teilbar nicht-durch-3-teilbar
8 durch-2-teilbar nicht-durch-3-teilbar
9 nicht-durch-2-teilbar durch-3-teilbar
10 durch-2-teilbar nicht-durch-3-teilbar
7.12 Lsg. zu Aufgabe „Lesbare Zahlen“ – Kap. 7.9.3
Klar sollte sein: auch hier wird wieder eine Klasse mit Main-Funktion und einer For-Schleife
benötigt. Bleibt nur die Frage offen: wie unterscheidet man auf die verschiedenen Zahlenwerte?
Die Lösung ist ganz einfach: das Problem ist typisch für eine Switch-Anweisung (siehe Kapitel
7.2), die eine Unterscheidung für integrale Werte darstellt. Damit kann das Programm direkt
hingeschrieben werden:
public class Appl {
public static void main(String[] args) {
for (int i=1; i<=5; i++) {
switch (i) {
case 1:
System.out.println("eins");
break;
case 2:
System.out.println("zwei");
break;
case 3:
System.out.println("drei");
break;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 128 / 409
case 4:
System.out.println("vier");
break;
case 5:
System.out.println("fuenf");
break;
}
}
}
}
Hinweis – noch effizienter läßt sich das Programm mit Containern implementieren, die wir in
Kapitel 9.3 kennen lernen werden. Dort wird sich das Problem auch noch mal als neue Aufgabe
9.8.4 wiederholen – und in Kapitel 9.12 findet sich dann die Lösungen mit Containern.
7.13 Lsg. zu Aufgabe „Zahlen-Liste“ – Kap. 7.9.4
Im ersten Augenblick sieht die Aufgabe ganz einfach aus: eine Klasse mit Main-Funktion und
For-Schleife mit Zahlen-Ausgabe und die Klammern-Ausgabe vor und nach der For-Schleife –
quasi analog zur Aufgabe 7.9.1 mit der Lösung 7.10.
// Achtung – fehlerhafte Loesung
public static void main(String[] args) {
System.out.print('(');
for (int i=1; i<=5; i++) {
System.out.print(i + '-');
}
System.out.println(')');
}
Ausgabe:
(1-2-3-4-5-)
Leider ist diese Lösung falsch, denn hinter der letzten Zahl (hier „5“) folgt noch ein Bindestrich.
Das Problem ist ein ganz typisches Alltags-Problem der Programmierung: beim ersten oder
letzten Durchlauf einer Schleife müssen Dinge anders gehandelt werden. Schauen wir uns die
drei typischen Lösungen an:
• Bedingte Ausführung in der Schleife
• Schleife um einen Durchlauf kürzen
• Schleife mit Abbruch in der Mitte
7.13.1 Lösung 1: Bedingte Ausführung in der Schleife
Der erste Ansatz ist einfach, in der Schleife mit einer If-Anweisung den letzten Durchlauf
gesondert zu behandeln, z.B. so:
for (int i=1; i<=5; i++) {
System.out.print(i);
if (i!=5) {
System.out.print('-');
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 129 / 409
Frage: was gefällt ihnen an diesem Quelltext nicht? Ich hoffe, dass ihnen sofort unangenehm
aufgestoßen ist, dass die Konstante „5“ zweimal im Quelltext auftaucht. Bei Änderungen
müssen also zwei Stellen im Quelltext geändert werden. Nun sind die beiden Änderungen nicht
viel Arbeit – das Problem ist in der Praxis, dass eine Stelle vergessen wird. Ein ganz wichtiger
Grundsatz beim Programmieren ist, dass eine Änderungswunsch an der Software (hier: „Lass
die Liste bis 8 gehen“) nur zu Änderungen an einer Stelle im Code führen sollte. Statt des
Literals „5“ sollte also lieber eine Konstante benutzt werden. Da wir Konstanten noch nicht
kennen, wäre die hier angemessene Lösung eine Variable. Um aus einer Variablen eine
Konstante zu machen, muss man aber nur den Modifier „final“ verwenden (siehe Kapitel 8.3) –
das können wir hier ruhig schon direkt benutzen.
final int limit = 5;
for (int i=1; i<=limit; i++) {
System.out.print(i);
if (i!=limit) {
System.out.print('-');
}
Ansonsten kann man die Lösung gut nutzen, und ist mit Klasse, Main-Funktion und der
Ausgabe der Klammern schnell fertig:
public class Appl {
public static void main(String[] args) {
final int limit = 5;
System.out.print('(');
for (int i=1; i<=limit; i++) {
System.out.print(i);
if (i!=limit) {
System.out.print('-');
}
}
System.out.println(')');
}
}
Nachteil dieser Lösung ist, dass die Schleife durch die If-Anweisung etwas komplizierter und
langsamer ist.
7.13.2 Lösung 2: Schleife um einen Durchlauf kürzen
Da hier die Anzahl an Durchläufen festliegt, kann man die Schleife einfach um einen Durchlauf
kürzen, und die spezielle Behandlung der letzten Zahl hinter der Schleife ganz für sich machen:
public class Appl {
public static void main(String[] args) {
final int limit = 5;
System.out.print('(');
for (int i=1; i<=limit-1; i++) {
System.out.print(i + '-');
}
System.out.print(limit);
System.out.println(')');
// <- hier steht jetzt "limit-1"
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 130 / 409
}
Der Vorteil der Lösung ist, dass die Schleife ohne If-Anweisung einfacher und performanter ist.
Leider gibt es Situationen, in denen diese Lösung nicht anwendbar ist, z.B. wenn die SchleifenAbbruch-Bedingung erst innerhalb der Schleife berechnet werden kann.
Außerdem ist hier nachteilig, dass wir genau genommen eine Code-Verdopplung haben. Es
fällt in unserem kleinen Beispiel gar nicht so richtig auf, aber die Ausgabe der Zahl findet sich
jetzt in und nach der Schleife. Im Prinzip sieht unser Programm jetzt ja so aus:
Schleife {
tue-a
tue-b
}
tue-a
In einem echten Programm könnte „tue-a“ viel komplizierter Code sein, der dann doppelt
vorhanden wäre. Damit haben wir wieder ein potentielles Problem im Änderungsfall (Fehler,
Wartung, Refactoring, Anforderungen). Also wenn man diese Lösung wäht, dann sollte man
„tue-a“ auf jeden Fall in eine extra Funktion auslagern.
7.13.3 Lösung 3: Schleife mit Abbruch in der Mitte
Eine andere Lösung wäre die Schleife mittendrin zu verlassen:
Schleife {
tue-a
Abbruch-wenn-noetig
tue-b
}
Mit diesem Muster läßt sich die Code-Verdopplung beseitigen, und diese Lösung funktioniert
auch dann wenn die Abbruch-Bedingung erst in der Schleife berechnet werden kann. Leider
enthält auch sie noch die If-Anweisung, und ist daher nicht ganz so einfach und schnell wie die
zweite Lösung.
public class Appl {
public static void main(String[] args) {
final int limit = 5;
System.out.print('(');
for (int i=1; ; i++) {
System.out.print(i);
if (i==limit) {
break;
}
System.out.print('-');
}
System.out.println(')');
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 131 / 409
7.14 Lsg. zu Aufgabe „Mittelwert und Varianz“ – Kap. 7.9.5
Erklärung folgt (hoffentlich) später – todo...
public class Appl {
public static void main(String[] args) {
if (args.length==0) {
System.out.println("Keine Werte => keine Berechnungen");
return;
}
if (args.length==1) {
try {
double d = Double.parseDouble(args[0]);
System.out.println("Nur ein Wert");
System.out.println("=> Mittelwert: " + d);
System.out.println("=> Varianz: ---");
} catch (Exception x) {
System.out.println("Fehlerhafter Parameter");
}
return;
}
int count = args.length;
double sum = 0.0;
double squares = 0.0;
for (int i=0; i<count; i++) {
try {
double d = Double.parseDouble(args[i]);
sum += d;
squares += d*d;
} catch (Exception x) {
System.out.println(i+1 + "-ter Parameter \"" + args[i] +
"\" fehlerhaft");
return;
}
}
System.out.println(count + " Werte");
System.out.println("=> Mittelwert: " + (sum/count));
System.out.println("=> Varianz: " + (squares/(count-1)));
}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static void main(String[] args) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
int count = 0;
double sum = 0.0;
double squares = 0.0;
while (true) {
try {
System.out.print("Wert: ");
String in = reader.readLine();
if (in.length()==0) break;
double d = Double.parseDouble(in);
count++;
sum += d;
squares += d*d;
} catch (Exception x) {
System.out.println("Fehlerhafte Eingabe, bitte wiederholen");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 132 / 409
}
}
if (count==0) {
System.out.println("Keine Werte => keine Berechnungen");
return;
}
if (count==1) {
System.out.println("Nur ein Wert");
System.out.println("=> Mittelwert: " + sum);
System.out.println("=> Varianz: ---");
return;
}
System.out.println(count + " Werte");
System.out.println("=> Mittelwert: " + (sum/count));
System.out.println("=> Varianz: " + (squares/(count-1)));
}
}
7.15 Lsg. zu Aufgabe „Multiplikations-Matrix“ – Kap. 7.9.6
Erklärung folgt (hoffentlich) später – todo...
public class Appl {
public static void main(String[] args) {
if (args.length!=2) {
System.out.println("Das Programm erwartet zwei Int-Argumente");
return;
}
int v1;
try {
v1 = Integer.parseInt(args[0]);
} catch (NumberFormatException x) {
System.out.println("Argument 1 ist kein Int-Wert");
return;
}
int v2;
try {
v2 = Integer.parseInt(args[1]);
} catch (NumberFormatException x) {
System.out.println("Argument 2 ist kein Int-Wert");
return;
}
if (v1>v2) {
int temp = v1;
v1 = v2;
v2 = temp;
}
// Zahlenbereich auf akzeptable Groesse checken
if (v2-v1>=10) {
System.out.println("Zu grosse Matrix");
return;
}
// Und Ausgabe...
System.out.print("
|");
for (int i=v1; i<=v2; i++) {
System.out.print(" " + i);
}
System.out.println();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 133 / 409
System.out.print("---+");
for (int i=v1; i<=v2; i++) {
System.out.print("---");;
}
System.out.println('-');
for (int i=v1; i<=v2; i++) {
System.out.print(' ' + i + " |");
for (int j=v1; j<=v2; j++) {
System.out.print(" " + i*j);
}
System.out.println();
}
}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static void main(String[] args) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
int v1;
try {
System.out.print("Untere Grenze: ");
String in = reader.readLine();
v1 = Integer.parseInt(in);
} catch (NumberFormatException x) {
System.out.println("Fehlerhafte Eingabe");
return;
}
int v2;
try {
System.out.print("Obere Grenze: ");
String in = reader.readLine();
v2 = Integer.parseInt(in);
} catch (NumberFormatException x) {
System.out.println("Fehlerhafte Eingabe");
return;
}
if (v1>v2) {
int temp = v1;
v1 = v2;
v2 = temp;
}
// Zahlenbereich auf akzeptable Groesse checken
if (v2-v1>=10) {
System.out.println("Zu grosse Matrix");
return;
}
// Und Ausgabe...
System.out.print("
|");
for (int i=v1; i<=v2; i++) {
System.out.print(" " + i);
}
System.out.println();
System.out.print("---+");
for (int i=v1; i<=v2; i++) {
System.out.print("---");;
}
System.out.println('-');
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 134 / 409
for (int i=v1; i<=v2; i++) {
System.out.print(' ' + i + " |");
for (int j=v1; j<=v2; j++) {
System.out.print(" " + i*j);
}
System.out.println();
}
}
}
8 Funktionen
In Java gibt es keine freien (globalen) Funktionen – alle Funktionen sind immer einer Klasse
zugeordnet. Dies gilt auch für die Main-Funktionen.
Syntax:
[Modifizierer] <Rückgabetyp> <Fkt-Name> ( [Parameterliste] ) {
<Implementierung>
}
• Modifizierer – z.B.: public, private, static, final, ...
• Eine Parameterliste ist eine durch Komma getrennte Auflistung von Parametern (sie kann
auch leer sein. Ein Parameter besteht aus Typ und Name.
Bsp:
public static void f() { ... }
protected final int doit(StringBuffer sb) { ... }
private static String calcName(String s, int i) { ... }
Aufruf:
<Fkt-Name>( <Argumentliste> );
Bsp:
f();
calcName("", 1);
Rückgaben können ignoriert werden – siehe Bsp „calcName“.
Bemerkung – fast jeder ausführbare Code steht immer in einer Funktion. Die einzigen
Ausnahmen sind Initialisierungs-Blöcke, die aber im Prinzip auch nur sehr spezielle Funktionen
sind.
Bemerkung – Funktions-Namen werden in Java per Konvention klein begonnen, und dann
kapitalisiert fortgesetzt – vergleiche z.B. den Namen „calcName“. Siehe auch Kapitel 3.2.5.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 135 / 409
8.1 Funktions-Arten
Achtung – in Java gibt es zwei Arten von Funktionen:
• Element-Funktionen
• Klassen-Funktionen
Klassen-Funktionen sind Funktionen, die mit dem Modifizierer „static“ definiert sind – wie z.B.
die Funktion „main“ in unseren Beispielen. Nur Klassen-Funktionen können direkt aufgerufen
werden. Element-Funktion benötigen einen Objekt-Bezug, und werden dann mit dem Objekt
und dem Punkt-Operator „.“ aufgerufen.
Für Objekte z.B. vom Typ „String“ benutzen wir die Element-Funktionen einfach. Im Detail
kennen lernen werden wir sie erst mit der Einführung von Klassen und Objekten in Kapitel todo.
Solange sollten Sie in ihren Beispielen nur Klassen-Funktionen definieren, d.h. jede
selbstgeschriebene Funktion sollte den Modifizierer „static“ bekommen.
public class Appl {
public static void sf() { ... }
public void f() { ... }
public static void main(String[] args) {
sf();
f();
String s = "Hallo";
s.length();
}
// Aufruf geht, da sf static ist
// Compiler-Fehler, da Element-Fkt
// Okay – Aufruf von Element-Fkt
//
mit Objekt-Bezug
}
8.2 Syntaktischer Aufbau
Eine Java Funktion besteht aus einer Folge von Anweisungen. Eine Anweisung ist:
• eine Instruktion (wird durch Semikolon beendet – z.B. Zuweisung, Definition.,
Funktionsaufruf,...),
• eine Kontrollstruktur (if, while, switch,...), oder
• ein Block.
Ein Block ist eine Folge von Anweisungen, die durch die geschweiften Klammern
eingeschlossen sind – ein Block ist immer auch ein Scope, d.h. ein Sichtbarkeitsbereich von
Variablen (siehe Kapitel 8.3).
8.3 Variablen in Funktionen
Variablen können an (fast) jeder beliebigen Stelle in Funktionen definiert werden. Dabei werden
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 136 / 409
sogenannte lokale Variablen erzeugt, die lokal zur Funktion sind31.
void fct() {
int var = 23;
System.out.println(var);
}
Ein einmal vergebener Name darf im gleichen Block nicht ein zweites Mal benutzt werden –
ansonsten wäre der Name ja nicht eindeutig.
void fct() {
int var = 23;
System.out.println(var);
int var = 42;
}
// Compiler-Fehler – var schon definiert
Wird eine Variable innerhalb eines Blocks in einer Funktion definiert, so ist sie auch nur
innerhalb dieses Blocks sichtbar. Ein Block bildet also einen Sichtbarkeitsbereich.
void fct() {
while (true) {
int var = 87;
System.out.println(var);
break;
}
System.out.println(var);
}
// Okay – var ist sichtbar
// Compiler-Fehler – var hier nicht sichtbar
Eine Besonderheit sind hierbei Variablen-Definitionen im Initialisierungs-Teil einer For-Schleife.
Obwohl sie nicht in einem speziellen Block definiert sind, sind diese Variablen nur in der
Schleife bekannt – siehe Kapitel 7.3.
Ist eine lokale Variabel sichtbar (d.h. sie befinden sich innerhalb des definierenden Scopes in
der Funktion), so darf auch in verschachtelten Blöcken keine weitere Variable des gleichen
Names definiert werden.32
int n = 4;
{
int n = 3;
}
// Compiler-Fehler – Variable n ist schon definiert.
8.3.1 Modifier „final“ für lokale Variablen
Der einzig erlaubte Modifizierer für Variablen in Funktionen ist der Modifizierer „final“. Wird er
angegeben, so kann die Variable nicht verändert werden.
final int n = 3;
n = 4;
// Compiler-Fehler – Variable darf nicht geaendert werden
final StringBuffer sb = new StringBuffer("StringBuffer");
Genau genommen sind die Variablen nicht nur lokal zur Funktion, sondern sogar lokal zum
Funktions-Aufruf. Lokale Variablen werden für jeden Funktionsaufruf beim Durchlaufen der
Definition neu erzeugt, d.h. lokale Variablen sind wiedereinsprungsfest (reentrant). Wichtig ist
dies für Rekursion (Kapitel 8.7) und Multithreading (nicht Thema des Tutorials).
32 Dies ist in Sprachen wie z.B. C oder C++ anders.
31
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 137 / 409
sb = new StringBuffer("Error");
// Compiler-Fehler – Variable ist final
sb.append(" das geht aber");
// Okay, final schuetzt nur die Variable
//
nicht das referenzierte Objekt
Achtung – bei Referenz-Variablen bezieht sich „final“ nur auf die Referenz, nicht auf das
referenzierte Objekt – siehe auch das vorherige Beispiel.
8.4 Funktions-Parameter
Funktions-Parameter sind eigentlich ganz normale lokale Variablen, die beim Funktions-Aufruf
mit den Argumenten des Aufrufers initialisiert werden. Damit unterliegen sie natürlich auch der
Wert- bzw. der Referenz-Semantik (siehe Kapitel 5.4), und das ist für Funktions-Parameter
interessant.
Alle elementaren Datentypen werden per Wert-Semantik, d.h. mit einer echten Kopie,
übergeben. Änderungen innerhalb der Funktion haben nur Einfluss auf die Kopie in der
Funktion, und betreffen das Original beim Aufrufer nicht.
void f1(int n1) {
System.out.println("=>
System.out.println("
n1 += 12;
System.out.println("
System.out.println("<=
}
f1");
" + n1);
" + n1);
f1");
int n = 23;
System.out.println(n);
f1(n);
System.out.println(n);
Ausgabe
23
=> f1
23
35
<= f1
23
Anders ist dies dagegen bei Referenz-Variablen, d.h. bei allen Variablen, deren Typen keine
elementaren Datentypen sind. In diesem Fall sind auch Funktions-Parameter ReferenzVariablen, und sie enthalten nur eine Referenz auf das eigentliche Objekt. Eine Funktion kann
also das referenzierte Objekt des Aufrufers verändern!
void f1(StringBuffer sb1)
System.out.println("=>
System.out.println("
sb1.append(" Welt");
System.out.println("
System.out.println("<=
}
{
f1");
" + sb1);
" + sb1);
f1");
StringBuffer sb = new StringBuffer("Hallo");
System.out.println(sb);
f1(sb);
System.out.println(sb);
Ausgabe
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 138 / 409
Hallo
=> f1
Hallo
Hallo Welt
<= f1
Hallo Welt
Achtung – Sie können dieses Verhalten nicht beeinflussen33. Die Übergabe-Semantik des
Funktions-Parameters liegt automatisch mit dem Typ des Parameters fest.
8.5 Funktions-Rückgaben
Hat eine Funktion den Rückgabe-Typ „void“ – siehe auch Kapitel 5.2.1 – so gibt sie nichts
zurück34.
void f() {
}
Soll sie dagegen etwas zurückgeben, so muss der entsprechende Rückgabe-Typ in der
Funktions-Definition angeben werden. Hat eine Funktion einen von „void“ verschiedenen
Rückgabe-Typ, so muss sie mit einer Return-Anweisung beendet werden, die den RückgabeWert definiert. Der Rückgabe-Wert in der Return-Anweisung muss natürlich zum RückgabeTyp der Funktion passen.
int f1() {
return 12;
}
String f2() {
return "Java";
}
StringBuffer f3() {
StringBuffer sb = new StringBuffer("Text");
return sb;
}
List<String> f4() {
return null;
}
Eine Funktion kann beliebig viele Ausgänge haben. Jede Return-Anweisung beendet die
Funktion instantan, und gibt den ausgewerteten Ausdruck der Return-Anweisung zurück.
int sgn(int n) {
if (n<0) {
return –1;
}
return n==0 ? 0 : 1;
}
System.out.println("-6
System.out.println("-1
System.out.println(" 0
System.out.println(" 1
System.out.println(" 3
33
34
=>
=>
=>
=>
=>
"
"
"
"
"
+
+
+
+
+
sgn(-6));
sgn(-1));
sgn( 0));
sgn( 1));
sgn( 3));
Im Gegensatz zu z.B. C++.
In manchen Sprachen werden solche Funktionen auch Prozeduren genannt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 139 / 409
Ausgabe
-6 => -1
-1 => -1
0 => 0
1 => 1
3 => 1
Eine Void-Funktion, d.h. eine Funktion ohne Rückgabe, kann jederzeit mit einer leeren ReturnAnweisung beendet werden.
void f(int i) {
if (i<12) {
return;
}
System.out.println(i);
}
// Leere Return-Anweisung
Funktionen können jeden beliebigen Typ zurückgeben, sowohl elementare als auch
benutzerdefinierte Typen. Hierbei unterliegen natürlich auch die Funktions-Rückgaben der
entsprechenden Wert- bzw. Referenz-Semantik.
Funktions-Rückgaben können direkt benutzt werden, d.h. werden Objekte wie z.B. Strings
zurückgegeben, so können für sie direkt Element-Funktionen aufgerufen werden. Man nennt
dies auch Funktions-Verkettung.
String fct() {
return "Java";
}
int len1 = fct().length();
System.out.println(len1);
// Aufruf der Funktion 'length()' fuer den
// zurueckgegebenen String
String s = fct();
int len2 = s.length();
System.out.println(len2);
// So haette man es auch machen koennen
// Mit lokaler Zwischen-Variable 's'
Ausgabe
4
4
8.6 Überladen
Funktionen können überladen werden, d.h. der Funktions-Name darf mehrfach benutzt werden,
solange sich die Funktionen durch ihre Parameter-Typen unterscheiden, d.h. der Compiler den
Aufruf eindeutig zuordnen kann. Möglich ist dabei auch die Variation der Anzahl an Parametern
– auch dort ist die Funktion exakt erkennbar. Der Rückgabetyp trägt nicht zur Unterscheidung
bei.
public class A {
public static void f() {
System.out.println("---");
}
public static void f(int arg) {
System.out.println("int: " + arg);
}
public static void f(String arg) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 140 / 409
System.out.println("String: " + arg);
}
public static void f(int arg1, String arg2) {
System.out.println("int, String: " + arg1 + ", " + arg2);
}
public static void main(String[] args) {
f();
f(8);
f("Willy");
f("42");
// Ist natuerlich auch ein String, kein 'int'
f(3, "Java");
}
}
Ausgabe
--int: 8
String: Willy
String: 42
int, String: 3, Java
Hinweis – das Feature „Überladen“ funktioniert sowohl mit Klassen-Funktionen (Kapitel
12.4.4), mit Element-Funktionen (Kapitel 11.4.4), als auch mit Konstruktoren (Kapitel 11.4.7).
8.7 Rekursion
Wenn eine Funktion im gleichen Thread mehrfach ineinander (d.h. nicht nacheinander,
sondern gleichzeitig parallel) aufgerufen wird, nennt man das „Rekursion“ bzw. „rekursive
Programmierung“.
Eine Funktion muss sich dabei nicht zwangsläufig selber aufrufen, sondern dies kann auch
über mehrere Zwischenstationen passieren, z.B. „f() => g() => h() => f()“. Solche Fälle sind
häufig gar nicht mehr sofort zu erkennen, von daher passiert dies in der Praxis häufiger, als
man vielleicht im ersten Augenblick denkt.
Es gibt aber auch viele Probleme, die sich rekursiv viel viel leichter programmieren lassen als
ohne Rekursion. Hier ein Beispiel, das man in der Praxis sicher nicht rekursiv lösen würde – die
Summe aller Zahlen von 1 bis n.
public static long sum(long arg) {
if (arg<=1) {
// Operator <= statt == um die Funktion bei
fehlerhaften
return 1;
// Argumenten (arg<1) sauber zu beenden.
}
long erg = arg + sum(arg-1);
return erg;
}
Hierbei wird quasi direkt die mathematische Definition umgesetzt:
sum(1) := 1
sum(n) := n + sum(n-1)
Natürlich läßt sich die Summe der Zahlen von 1 bis n direkt über die Gaußsche-Summen-
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 141 / 409
Formel „n*(n+1)/2“ berechnen – was hier auch viel sinnvoller wäre – aber es geht eben auch
rekursiv.
Hinweis – es läßt sich übrigens beweisen, dass jedes Problem was iterativ gelöst werden kann
(d.h. mit einer Schleife) sich auch rekursiv lösen läßt, und umgekehrt.
Praxis – in den aller-meisten Fällen sind die iterativen Lösungen schneller und benötigen
weniger Speicher – sie sind daher vorzuziehen. Ein Schleifen-Durchlauf ist schnell und effizient,
während ein Funktions-Aufruf doch relativ teuer ist (bezogen auf die Performance).
In so manchen Fällen ist die rekursive Lösung aber ein 5-Zeiler, während die iterative Lösung
Tage harter Arbeit sein kann und hinterher aus vielen Quelltext-Zeilen besteht – Beispiele für
Lösungen, die rekursive erstaunlich einfach sind, finden sich z.B. in Kapitel 9.7.1 („Rekursive
Datei-Suche“) und in der Musterlösung 12.9 zur Aufgabe 12.8.1 („Türme von Hanoi“).
In einigen der folgenden Aufgaben werden sowohl iterative als auch rekursive Lösung verlangt.
Die Lösungen bieten also Diskussions-Material für das Thema Performance und
Implementierbarkeit.
8.8 Aufgaben
8.8.1 Aufgabe „Fakultäts Funktion“
Schreiben Sie zwei Versionen der Fakultäts-Funktion. Die Fakultät ist das Produkt der
natürlichen Zahlen von 1 bis n. Mathematisch sieht die Definition folgendermaßen aus:
• fak(0) :== 1
• fak(n) :== n * fak(n-1)
Version 1 soll das Ergebnis iterativ berechnen – d.h. mit einer Schleife.
Version 2 soll das Ergebnis rekursiv berechnen – d.h. durch Aufruf von sich selber.
Welche Version würden Sie in der Praxis warum vorziehen?
Schreiben Sie zusätzlich ein kleines Haupt-Programm, dass beide Funktionen für einige IntWerte benutzt. Achtung – bedenken Sie, dass die Fakultät sehr schnell wächst, und schon
„14!“ nicht mehr in einen vorzeichenbehafteten 32-Bit Integer-Wert paßt, und „21!“ auch eine
Long-Wert sprengt.
Lösung siehe Kapitel 8.9.
8.8.2 Aufgabe „Primzahl?“
Schreiben Sie eine Funktion, die eine positive Integer-Zahl erwartet und zurückgibt ob die Zahl
eine Primzahl ist. Schreiben Sie ein Programm, dass diese Funktion nutzt. Das Programm soll
sowohl mit einem Kommandozeilen-Argument umgehen können, als auch interaktiv mit dem
Benutzer die Zahl einlesen können. Hierbei gilt folgende Strategie:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 142 / 409
• Sind Kommandozeilen Argumente vorhanden, so werden Sie alle für den Primzahl-Check
genommen.
• Ist kein Kommandozeilen Argument vorhanden, so wird eine Zahl interaktiv vom Benutzer
erfragt.
Lösung siehe Kapitel 8.10.
8.8.3 Aufgabe „Schleife rekursiv“
Nachdem Sie nun Funktionen und Rekursion kennen gelernt haben, schreiben Sie die
einfachen Zahlen-Schleife aus Aufgabe 7.9.1 mit einer rekursiven Funktion statt mit einer
Schleife.
Ausgabe
1 2 3 4 5
Lösung siehe Kapitel todo.
8.8.4 Aufgabe „Zahlen-Liste 2“
Und auch die Zahlen-Liste aus Aufgabe 7.9.4 läßt sich nun mit Funktionen und Rekursion neu
schreiben.
Ausgabe:
(1-2-3-4-5)
Lösung siehe Kapitel todo.
8.8.5 Aufgabe „Quadratwurzel“
Schreiben Sie ein Programm, das die Quadratwurzel einer Fließkomma-Zahl größer-gleich 1.0
berechnet. Benutzen Sie hierbei nur die Grundrechenarten und Vergleiche, indem Sie sich der
Lösung durch Intervall-Schachtelung Schritt für Schritt annähern.
Das Konzept der Intervall-Schachtelung arbeitet folgendermaßen:
• Beginnen Sie mit zwei Zahlen, von denen Sie ausgehen können, dass sie kleiner-gleich bzw.
größer-gleich der Quadratwurzel sind.
• Berechnen Sie den Mittelwert der beiden Zahlen, und quadrieren Sie diesen.
• Weicht der quadrierte Mittelwert um weniger als einen Toleranzwert epsilon (z.B. 1.0e-5) von
der Eingabe-Zahl ab, so verwenden Sie den Mittelwert als Ergebnis der Berechnung. D.h.
Sie haben die Quadratwurzel gefunden.
• Ist das Quadrat des Mittelwertes größer als die Eingabe-Zahl, so verwenden Sie das untere
Intervall für die nächste Iteration.
• Ist das Quadrat des Mittelwertes kleiner als die Eingabe-Zahl, so verwenden Sie das obere
Intervall für die nächste Iteration.
• Führen Sie die Iteration so lange durch, bis sich der quadrierte Mittelwert bis auf den
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 143 / 409
Toleranzwert epsilon an die Eingabe-Zahl angenähert hat.
Geben Sie am Anfang der Berechnung den Toleranzwert epsilon aus.
Geben Sie vor jeder Iteration die Nummer der Iteration und das aktuelle Intervall aus.
Und geben Sie am Ende natürlich auch die Wurzel aus.
Mögliche Ein- und Ausgabe
Wurzel-Berechnung fuer Zahlen groesser-gleich 1.0
Zahl: 4
Berechne Wurzel aus 4 mit einem Epsilon von 0.0001
- Durchlauf 1: 1 < sqrt(4) < 4
- Durchlauf 2: 1 < sqrt(4) < 2.5
- Durchlauf 3: 1.75 < sqrt(4) < 2.5
- Durchlauf 4: 1.75 < sqrt(4) < 2.125
- Durchlauf 5: 1.9375 < sqrt(4) < 2.125
- Durchlauf 6: 1.9375 < sqrt(4) < 2.03125
- Durchlauf 7: 1.98438 < sqrt(4) < 2.03125
- Durchlauf 8: 1.98438 < sqrt(4) < 2.00781
- Durchlauf 9: 1.99609 < sqrt(4) < 2.00781
- Durchlauf 10: 1.99609 < sqrt(4) < 2.00195
- Durchlauf 11: 1.99902 < sqrt(4) < 2.00195
- Durchlauf 12: 1.99902 < sqrt(4) < 2.00049
- Durchlauf 13: 1.99976 < sqrt(4) < 2.00049
- Durchlauf 14: 1.99976 < sqrt(4) < 2.00012
- Durchlauf 15: 1.99994 < sqrt(4) < 2.00012
- Durchlauf 16: 1.99994 < sqrt(4) < 2.00003
Wurzel: 1.99998
Gibt es mehrere Arten, wie sie diese Funktion implementieren können? Wenn ja, dann machen
Sie das auch.
Lösung siehe Kapitel 8.13.
8.9 Lsg. zu Aufgabe „Fakultäts Funktion“ – Kap. 8.8.1
8.9.1 Iterative Lösung
Die iterative Berechnung der Fakultät, d.h. mit einer Schleife, läß sich im Prinzip direkt runter
implementieren. Man muss ja nur alle Zahlen von „0“ bis „n“ inkl. aufmultiplizieren, und dass
Ergebnis zurückgeben.
public static long facIterative(int n) {
long res = 1;
for (int i=2; i<=n; i++) {
res *= i;
}
return res;
}
Hinweis – da die Fakultät von „0“ und „1“ selber „1“ sind, braucht die Schleife erst ab „2“ los
zulaufen. Falls die Funktion mit dem Argument „0“ oder „1“ aufgerufen wird, ist dies auch kein
Problem, da eine For-Schleife kopfgesteuert ist, d.h. auch kein Mal durchlaufen werden kann.
Hinweis – passen Sie auf, dass Sie die Schleife bis „n“ inkl. laufen lassen, d.h. die AbbruchBedingung ist „i<=n“ mit dem Operator „kleiner-gleich“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 144 / 409
8.9.2 Rekursive Lösung
Auch die rekusive Berechnung ist nicht weiter schwierig, da man hier quasi direkt die
mathematische Definition abschreiben kann:
• fak(0) :== 1
• fak(1) :== 1
• fak(n) :== n * fak(n-1)
public static long facRecursive(int n) {
if (n<=1) {
return 1;
}
return n * facRecursive (n-1);
}
8.9.3 Main-Funktion
Die Aufgabe fordert auch eine kleine Main-Funktion. Hier eine, die beide Funktionen parallel
nutzt, und den Benutzer ihre Ergebnisse vergleichen läßt.
public static void main(String[] args) {
for (int i=0; i<=20; i++) {
long fi = facIterative(i);
long fr = facRecursive(i);
System.out.println(i + "! => " + fi + "
}
}
" + fr);
Ausgabe
0! => 1 1
1! => 1 1
2! => 2 2
3! => 6 6
4! => 24 24
5! => 120 120
6! => 720 720
7! => 5040 5040
8! => 40320 40320
9! => 362880 362880
10! => 3628800 3628800
11! => 39916800 39916800
12! => 479001600 479001600
13! => 6227020800 6227020800
14! => 87178291200 87178291200
15! => 1307674368000 1307674368000
16! => 20922789888000 20922789888000
17! => 355687428096000 355687428096000
18! => 6402373705728000 6402373705728000
19! => 121645100408832000 121645100408832000
20! => 2432902008176640000 2432902008176640000
Und welche Lösung sollte man in der Praxis benutzen? Hier ist sicher die iterative Lösung
vorzuziehen, die vollkommen unproblematisch zu implementieren ist, und vor allem bzgl.
Performance vorzuziehen ist, da ein Schleifen-Durchlauf im Normall-Fall einiges billiger ist als
ein Funktions-Aufruf.
Achtung – keine der Lösung kümmert sich wirklich gut um das Problem: „Was passiert, wenn
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 145 / 409
die Funktion mit einer negativen Zahl aufgerufen wird?“ Beide Lösungen geben dann „1“
zurück, was zwar ein sauber definiertes Verhalten ist, aber auch sicher nicht das richtige
Ergebnis. Die typische Java-Lösung, wie wir sie z.B. schon aus den Kapiteln 3.5, 3.10 oder
3.11 kennen, ist die Benutzung von Exceptions – siehe Kapitel todo. Da wir Exceptions noch
nicht kennen, ignorieren wir dieses Problem hier erstmal (sollten es aber nicht vergessen).
8.9.4 Gesamter Quelltext
Hier noch mal der ganze Quelltext inkl. Klasse.
public class Appl {
public static long facIterative(int n) {
long res = 1;
for (int i=2; i<=n; i++) {
res *= i;
}
return res;
}
public static long facRecursive(int n) {
if (n<=1) {
return 1;
}
return n*facRecursive(n-1);
}
public static void main(String[] args) {
for (int i=0; i<=20; i++) {
long fi = facIterative(i);
long fr = facRecursive(i);
System.out.println(i + "! => " + fi + "
}
}
" + fr);
}
8.10 Lsg. zu Aufgabe „Primzahl?“ – Kap. 8.8.2
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static boolean prim(int n) {
for (int i=2; i<=Math.sqrt(n); i++) {
if (n%i==0) {
return false;
}
}
return true;
}
public static void check(String s) {
try {
int n = Integer.parseInt(s);
if (n<0) {
System.out.println("Negative Zahl: " + n + " - no check");
} else {
System.out.println(n + " prim? -> " + prim(n));
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 146 / 409
}
} catch (Exception x) {
System.out.println("Keine Zahl: \"" + s + "\" - no check");
}
}
public static void main(String[] args) {
if (args.length>0) {
for (int i=0; i<args.length; i++) {
check(args[i]);
}
return;
}
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
try {
System.out.print("Bitte geben Sie die zu checkende Zahl ein: ");
String in = reader.readLine();
check(in);
} catch (Exception x) {
System.out.println("Probleme beim Einlesen");
}
}
}
8.11 Lsg. zu „Aufgabe „Schleife rekursiv“ – Kap. 8.8.3
Erklärung folgt (hoffentlich) später – todo...
public class Appl {
public static void loop(int n, int limit) {
System.out.print(n + " ");
if (n == limit) {
return;
}
loop(n + 1, limit);
}
public static void main(String[] args) {
loop(1, 5);
}
}
8.12 Lsg. zu Aufgabe „Zahlen-Liste 2“ – Kap. 8.8.4
Erklärung folgt (hoffentlich) später – todo...
public class Appl {
public static void loop(int n, int limit) {
System.out.print('(');
internalLoop(n, limit);
System.out.print(')');
}
public static void internalLoop(int n, int limit) {
if (n == limit) {
System.out.print(n);
return;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 147 / 409
System.out.print(n + "-");
internalLoop(n + 1, limit);
}
public static void main(String[] args) {
loop(1, 5);
}
}
8.13 Lsg. zu Aufgabe „Quadratwurzel“ – Kap. 8.8.5
8.13.1 Lösung 1 - Iterativ
Fangen wir mit den einfachen Dingen an:
• Zuerst muss ein String von der Kommando-Zeile eingelesen werden.
• Der muss in eine Fließkomma-Zahl gewandelt werden.
• Diese Fließkomman-Zahl soll größer-gleich 1.0 sein.
• Macht eine dieser Bereiche Probleme, beenden wir das Programm mit einer FehlerMeldung.
• Die eigentliche Wurzel-Berechnung lagern wir in die Funktion „squareRoot“ aus.
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
try {
System.out.print("Bitte geben Sie eine Zahl >=1 ein: ");
String in = reader.readLine();
try {
double d = Double.parseDouble(in);
if (d<1.0) {
System.out.println("Zahl: " + d + " zu klein");
} else {
squareRoot(d);
}
} catch (Exception x) {
System.out.println("Keine Zahl: \"" + s + "\"");
}
} catch (Exception x) {
System.out.println("Probleme beim Einlesen");
}
Ich hoffe, Sie sehen es wie ich: der Code sieht kompliziert aus. Man könnte natürlich die
verschachtelten Try-Catch-Blöcke zu einem zusammenfassen, aber dann würden die FehlerMeldungen nicht mehr dediziert für das Problem sein35. Besser ist, wir trennen den Code durch
Benutzung von Funktionen.
public static void main(String[] args) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
try {
System.out.print("Bitte geben Sie eine Zahl >=1 ein: ");
String in = reader.readLine();
calc(in);
} catch (Exception x) {
System.out.println("Probleme beim Einlesen");
Okay, ich gebe zu: das könnte man hier trotzdem hinbekommen, da unterschiedliche
Exceptions geworfen werden. Aber das lernen wir ja erst viel später in Kapitel todo.
35
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 148 / 409
}
}
public static void calc(String s) {
try {
double d = Double.parseDouble(s);
if (d<1.0) {
System.out.println("Zahl: " + d + " zu klein");
} else {
squareRoot(d);
}
} catch (Exception x) {
System.out.println("Keine Zahl: \"" + s + "\"");
}
}
Das ganz hat jetzt außerdem den Vorteil, dass wir unser Programm problemlos komfortabler
gestalten können – viel komfortabler als die Aufgabe das von uns erwartet. Aber wir machen ja
gerne Sternchen-Aufgaben, da das mit Java so einfach ist und so viel Spaß macht.
Schön wäre es, dass Programm sowohl mit einem Kommandozeilen-Argument als auch mit
einer interaktiven Eingabe von der Kommandozeile benutzen zu können. Die Idee ist, wenn der
Benutzer ein Kommandozeilen-Argument mitgibt, dann wird von diesem die Wurzel berechnet.
Ansonsten wird die Eingabe interaktiv von der Konsole eingelesen.
Dies ist jetzt einfach zu implementieren, da wir nur die Kommandozeilen-Argumente auswerten
müssen, und im Falle von einem vorhandenen dieses an die Calc-Funktion weiterreichen
müssen36.
public static void main(String[] args) {
if (args.length==1) {
calc(args[0]);
return;
}
...
}
Damit bleibt nur noch die eigentliche Funktion „squareRoot“ zur Berechnung der Wurzel zur
Implementierung übrig:
• Zuerst wird ein Toleranzwert epsilon benötigt, der die mindestens notwendige Näherung an
die wahre Lösung beschreibt. Hierfür bietet sich natürlich eine Konstante (siehe Kapitel 8.3)
an. Noch besser wäre sicher eine Klassen-Konstante, aber die lernen wir erst in Kapitel
12.5.2 kennen.
• Und es muss eine Start-Ausgabe geben.
Alles zusammen kann dann z.B. so aussehen:
public static double squareRoot(double v) {
final double epsilon = 0.0001;
System.out.println("Berechne Wurzel aus " + v +
Wenn sie noch mehr an Sternchen-Aufgaben interessiert sind, dann sorgen sie doch einfach
dafür, dass wenn mehrere Kommandozeilen-Argumente übergeben werden, für alle die Wurzel
berechnet wird.
36
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 149 / 409
" mit einem Epsilon von " + epsilon);
...
}
Für die Intervallschachtelung benötigen wir zwei Zahlen, bei denen die eine auf jeden Fall
kleiner-gleich der Lösung ist, während die andere größer-gleich der Lösung sein muss. Die
Aufgabe ist zufälligerweise so gestellt, dass der Eingabe-Wert „v“ größer-gleich 1.0 sein muss,
d.h. die Lösung wird immer auch größer-gleich 1.0 sein, und immer kleiner-gleich der EingabeZahl „v“. Als untere und obere Grenze bieten sich also diese Werte an. Die Berechnung des
Mittelwerts „average“ der beiden Grenzwerte, und seine Quadratur „av2“ sind dann kein
Problem mehr.
double
double
double
double
lowerLimit = 1.0;
upper_Limit = v;
average = (lower_limit + upperLimit) / 2;
av2 = average*average;
Die Ausgabe vor jeder Iteration verschieben wir erstmal auf später – wir dürfen sie nur nicht
vergessen.
Wenn der quadrierte Mittelwert „av2“ den Eingabe-Wert bis auf den Toleranzwert epsilon
erreicht hat, dann ist der Mittelwert die Lösung. Ansonsten muss die Iteration fortgesetzt
werden. Dies klingt nach einer Schleife mit entsprechender Abbruch-Bedingung.
Da bleibt höchstens noch die Frage, ob der Mittelwert „average“ bei dem die Eingabe „v“ auf
genau epsilon erreicht wird, auch schon eine Lösung ist oder nicht. Letztlich also die Frage, ob
die Abbruch-Bedingung „>=“ und „<=“ oder nur „>“ und „<“ enthält. Die Aufgaben-Stellung ist
hier nicht ganz exakt, ich habe mir für erstes entschieden. Die Schleife sieht also
folgendermaßen aus.
while ((av2-epsilon>=v) || (av2+epsilon<=v)) {
...
}
Für den Fall, dass die Lösung noch nicht erreicht wurde, muss das Intervall – wie in der
Aufgabe beschrieben – neu gewählt werden
while ((av2-epsilon>=v) || (av2+epsilon<=v)) {
if (v<av2) {
upperLimit = average;
} else {
lowerLimit = average;
}
...
}
Und dann muss für das neu gewählte Intervall der Mittelwert und sein Quadrat neu berechnet
werden.
while ((av2-epsilon>=v) || (av2+epsilon<=v)) {
if (v<av2) {
upperLimit = average;
} else {
lowerLimit = average;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 150 / 409
}
average = (lowerLimit+upperLimit)/2;
av2 = average*average;
}
Im Prinzip ist die Aufgabe jetzt schon fertig. Wir sehen hier aber mal wieder eine Verletzung
des DRY-Prinzips37, da die beiden Berechnungen nun zweimal im Quelltext vorkommen: initial
vor der Schleife, und am Ende der Schleife. Dies liegt daran, dass hier wieder mal eine Schleife
mit Ausgang in der Mitte benötigt wird. Also ändern wir dies um.
double lowerLimit = 1.0;
double upper_Limit = v;
double average;
for (int count=0;;) {
average = (lower_limit+upperLimit)/2;
double av2 = average*average;
if ((av2-epsilon<v) && (av2+epsilon>v)) break;
if (v<av2) {
upperLimit = average;
continue;
}
lowerLimit = average;
}
Jetzt fehlt nur noch die Ausgabe während jeder Iteration, und am Ende die Ausgabe der
Lösung.
for (int count=0;;) {
System.out.println("- Durchlauf " + ++count + ": " + lowerLimit +
" < sqrt(" + v + ") < " + upperLimit);
...
}
System.out.println("Wurzel: " + average);
Alles zusammen sieht dann z.B. so aus:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
//-----------------------------------------------------------------------public static void main(String[] args) {
if (args.length>0) {
calc(args[0]);
return;
}
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
try {
System.out.print("Bitte geben Sie eine Zahl >=1 ein: ");
String in = reader.readLine();
calc(in);
} catch (Exception x) {
System.out.println("Probleme beim Einlesen");
}
}
//-----------------------------------------------------------------------public static void calc(String s) {
37
DRY – don’t repeat yourself.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 151 / 409
try {
double d = Double.parseDouble(s);
if (d<1.0) {
System.out.println("Zahl: " + d + " zu klein");
} else {
squareRoot(d);
}
} catch (Exception x) {
System.out.println("Keine Zahl: \"" + s + "\"");
}
}
//-----------------------------------------------------------------------public static double squareRoot(double v) {
final double epsilon = 0.0001;
System.out.println("Berechne Wurzel aus " + v +
" mit einem Epsilon von " + epsilon);
double lowerLimit = 1.0;
double upper_Limit = v;
double average;
for (int count=0;;) {
System.out.println("- Durchlauf " + ++count + ": " + lowerLimit +
" < sqrt(" + v + ") < " + upperLimit);
average = (lower_limit+upperLimit)/2;
double av2 = average*average;
if ((av2-epsilon<v) && (av2+epsilon>v)) break;
if (v<av2) {
upperLimit = average;
continue;
}
lowerLimit = average;
}
System.out.println("Wurzel: " + average);
return average;
}
}
8.13.2 Lösung 2 - Rekursiv
Natürlich läßt sich auch die Quadratwurzel Berechnung mit Intervall-Schachtelung rekursiv
berechnen. Genau genommen zwingt sich der rekursive Algorithmus bei der Überlegung, wie
man die Funktion implementiert, geradezu auf.
Denn was macht man bei der Intervall-Schachtelung? Man teilt das Intervall, in dem sich die
Lösung befinden muss, in zwei gleich große Teile, und untersucht in welchem Teil die Lösung
liegen muss. Für diesen Teil macht man wieder das gleiche, d.h. wendet die IntervallSchachtelung auf diesen Teil an. Usw. Sie sehen, das klingt stark rekursiv.
Und die Abbruch Bedingung ist natürlich, wenn die Lösung bis auf epsilon erreicht wurde.
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
//------------------------------------------------------------------------public static void main(String[] args) {
if (args.length>0) {
calc(args[0]);
return;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 152 / 409
}
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
try {
System.out.print("Bitte geben Sie eine Zahl >=1 ein: ");
String in = reader.readLine();
calc(in);
} catch (Exception x) {
System.out.println("Probleme beim Einlesen");
}
}
//------------------------------------------------------------------------public static void calc(String s) {
try {
double d = Double.parseDouble(s);
if (d<1.0) {
System.out.println("Zahl: " + d + " zu kleine - keine Berechnung");
} else {
squareRoot(d);
}
} catch (Exception x) {
System.out.println("Keine Zahl: \"" + s + "\" - keine Berechnung");
}
}
//------------------------------------------------------------------------public static double squareRoot(double v) {
System.out.println("Berechne Wurzel aus " + v + " mit einem Epsilon von
0.0001");
double sqrt = squareRoot(v, 1.0, v, 1);
System.out.println("Wurzel: " + sqrt);
return sqrt;
}
//------------------------------------------------------------------------public static double squareRoot(
double v, double lowerLimit, double upperLimit, int count) {
final double epsilon = 0.0001;
System.out.println("- Durchlauf " + count + ": " + lowerLimit +
" < sqrt(" + v + ") < " + upperLimit);
double average = (lowerLimit+upperLimit)/2;
double av2 = average*average;
if ((av2-epsilon<v) && (av2+epsilon>v)) {
return average;
}
if (v<av2) {
return squareRoot(v, lowerLimit, average, count+1);
}
return squareRoot(v, average, upperLimit, count+1);
}
}
9 Ausgewählte Bibliotheks-Klassen
Die Java Bibliothek (JDK 1.8) umfaßt ~217 Packages mit ~4240 Klassen – siehe Kapitel 2. Für
viele Probleme stehen daher fertige Klassen zur Verfügung. Ein paar ganz allgemeine und sehr
zentrale Klassen möchte ich hier ganz kurz vorstellen, damit wir sie in späteren Beispielen bzw.
dem Praktikum benutzen können. Aber machen Sie sich klar, es sind nur ganz wenige aus dem
großen Angebot der Java-Welt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 153 / 409
9.1 Klasse String
Mit der Klasse String werden konstante Unicode Zeichenketten repräsentiert. Ja, Sie haben
richtig gelesen: „konstante Zeichenketten“. Strings können aus Performance-Gründen nicht
verändert werden. Wollen Sie Zeichenketten modifizieren, so müssen Sie die Klassen
StringBuilder oder StringBuffer (siehe Kapitel 9.2) verwenden.
Hinweise:
• Man nennt Strings, wie auch andere unveränderbare Objekte, auch „immutable“ Objekte.
• Strings können einfach so benutzt werden, und benötigen kein „import“. Sie kommen aus
dem Package „java.lang“, das immer automatisch zur Verfügung steht – Kapitel 13.3.
9.1.1 Initialisierung
Da Zeichenketten in der Praxis sehr häufig vorkommen, sind in der Sprache Java einige
Unterstützungen für Strings integriert, die über eine normale ‚Klasse‘ hinaus gehen.
So können Strings neben dem expliziten Erstellen mit „new“ auch einfach mit Zeichenketten
erzeugt werden38.
public class Kap_09_01_Bsp_01_Strings {
public static void main(String[] args) {
String s1 = new String("Java"); // So muessen in Java eigentlich alle
System.out.println(s1);
// Objekte angelegt werden: mit "new"
String s2 = "Hallo Welt";
System.out.println(s2);
// Aber bei Strings geht es auch ohne
// "new" - Nettigkeit der Sprache
s1 = "Viel einfacher so";
System.out.println(s1);
// Und das geht auch bei Zuweisungen
s1 = new String("Aufwaendig");
System.out.println(s1);
// Die normale Art fuer Nicht-Strings
}
}
Ausgabe
Java
Hallo Welt
Viel einfacher so
Aufwaendig
9.1.2 Operator +
Für die Klasse String ist in der Sprache Java der Operator + definiert. Auf zwei Strings
angewandt ist das Ergebnis ein neuer String, der aus den beiden konkatinierten Operanden
besteht.
public class Kap_09_01_Bsp_02_StringAddition1 {
Im Gegensatz zu Arrays (siehe Kapitel 10.1) geht dies jederzeit, und nicht nur bei der
Variablen-Definitionen.
38
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 154 / 409
public static void main(String[] args) {
String s1 = "Hallo";
String s2 = " Welt";
String s = s1 + s2;
System.out.println(s);
}
}
Ausgabe
Hallo Welt
Wie schon kurz in Kapitel 3.4 angesprochen: ist nur einer der Operanden ein String, so wird der
andere implizit in einen String umgewandelt. Wir sehen dies im folgenden Quelltext:
• in der Zeile (*) für einige elementare Datentypen („boolean“, „int“ und „long“),
• in der Zeile (**) nochmal expliziter für ein „int“, und
• in der Zeile (****) für ein AWT-Label. Wir werden GUI-Label noch in Kapitel 20.1 näher
kennen lernen – hier steht es einfach für ein komplexes Objekt. Als Beispiel, dass wirklich
jedes Objekt beim Operator + in einen String gewandelt wird.
import java.awt.Label;
public class Kap_09_01_Bsp_03_StringAddition2 {
public static void main(String[] args) {
String s = "Variablen: ";
boolean b = true;
int i = 12345;
long l = 123456789012345L;
s = s + b + "=" + false + " - i=" + i + " - l=" + l;
System.out.println(s);
int n = 42;
String s1 = "" + n;
String s2 = "" + Integer.toString(n);
System.out.println(s1);
System.out.println(s2);
Label label = new Label("Beschriftung");
s1 = "" + label;
s2 = label.toString();
System.out.println(s1);
System.out.println(s2);
// Zeile (*)
// Zeile (**)
// Zeile (***)
// Zeile (****)
// Zeile (*****)
}
}
Ausgabe
Variablen: true=false - i=12345 - l=123456789012345
42
42
java.awt.Label[label0,0,0,0x0,invalid,align=left,text=Beschriftung]
java.awt.Label[label0,0,0,0x0,invalid,align=left,text=Beschriftung]
Alternativ zu den impliziten Wandlungen beim Operator + in z.B. Zeile (**) und (****) sind auch
die mehr expliziten Wandlungen wie hier in den Zeilen (***) und (*****) möglich.
• Für alle elementaren Datentypen existieren Standard-Umwandlungen, die sich auch in den
jeweiligen Wrapper-Klassen wiederfinden – siehe Kapitel 3.11 und 9.4.
• Für alle anderen Typen kann die Elementfunktion „toString()“ benutzt werden, die für alle
Klassen definiert ist – siehe auch Kapitel 14.11.1.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 155 / 409
Bitte beachten Sie: auch wenn im Quelltext des letzten Beispiels „s=...“ steht, so wird nicht der
String verändert. Es ändert sich nur der Wert der Referenz-Variablen „s“, die nach der
Zuweisung auf ein anderes String-Objekt verweist. Das originale String-Objekt mit dem Wert
„Variablen: “ wird nicht verändert, sondern jetzt nur nicht mehr referenziert.
String
String s = "abc"
s = s + "def"
s:
=>
verändert
s:
unverändert
"abc"
String
String
"abc"
"def"
=>
String
"abcdef"
Abb. 9-1 : Ein String-Objekt wird nicht verändert
Hinweis – die Addition von Strings mit dem Plus-Operator ist nicht sehr effizient. Wie Sie an
der obigen Abbildung sehen können, müssen bei dieser Operation neue Objekte angelegt und
die Zeichen kopiert werden – was relativ viel Zeit kostet. Nutzen Sie aufgrund der Performance
Vorteile bei String-Verkettungen die Klassen „StringBuilder“ und „StringBuffer“, siehe Kapitel
9.2.
9.1.3 Referenz-Semantik und Konstante Zeichenketten
Da String eine Klasse und kein elementarer Datentyp ist, unterliegen String-Variablen natürlich
der Referenz-Semantik (siehe Kapitel 5.4.2).
Und die Klasse String repräsentiert konstante Zeichenketten. D.h. es gibt keine Möglichkeit
einen String zu verändern. Alle Operationen auf Strings lassen diese unangetastet, bzw. geben
einen neuen zurück – der Originalstring bleibt immer unverändert, String-Objekte sind
immutable.
Damit entfällt das typische Problem der Referenzsemantik, nämlich dass ein Objekt von
woanders einfach geändert wird. Strings sind dagegen resistent, und darum an vielen Stellen
unproblematischer – vergleiche z.B. Kapitel todo oder auch Kapitel todo.
Achtung – umgekehrt gibt es bei Strings dadurch auch eine Falle: der Vergleichs-Operator
„==“ führt auch bei Strings nur einen Identitäts-Vergleich durch, und keinen Wert-Vergleich.
Leider verhält sich der Operator „==“ bei Strings aber manchmal so, als würde er einen WertVergleich durchführen – siehe auch Kapitel 6.2. Vergleichen Sie Strings immer mit „equals“,
auch wenn der Operator „==“ scheinbar funktioniert.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 156 / 409
9.1.4 Schnittstelle
Die Klasse String ist in java.lang definiert. Sie hat keine public Attribute, aber viele ElementFunktionen – hier eine kleine Auswahl:
String()
Konstruktor: erzeugt einen leeren String.
String(char[ ])
Konstruktor: erzeugt einen String aus einem char-Array.
String(StringBuffer)
Konstruktor: erzeugt einen String aus einem String-Buffer.
boolean equals(String)
Gibt zurück, ob die beiden Strings wert-gleich sind.
boolean equalsIgnoreCase
(String anotherString)
Vergleicht zwei String ohne Berücksichtigung von Groß- und KleinSchreibung.
char charAt(int)
Gibt das Zeichen an der spezifizierten Position zurück.
int length()
Gibt die Anzahl an Zeichen des Strings zurück, d.h. seine Länge.
String trim()
Gibt einen neuen String zurück, der dem alten ohne führende und
folgende Whitespaces (nicht sichtbare Zeichen wie z.B.
Leerzeichen oder Tabulator) entspricht.
boolean startsWith(String)
Gibt zurück, ob der String mit dem übergebenen beginnt.
boolean endsWith(String)
Gibt zurück, ob der String mit dem übergebenen aufhört.
String substring
(int beginIndex, int endIndex)
Gibt eine Teil-String als neuen String zurück. Der Teil-String
beginnt beim Zeichen mit dem Index „beginIndex“ inkl. und endet
beim Zeichen mit dem Index „endIndex“ exkl. Bei fehlerhaften
Indices wird eine Exception geworfen.
Die komplette Schnittstelle der Klasse „String“ finden Sie in der offiziellen Java ReferenzDokumentation, die Sie bei Oracle herunterladen können – siehe Kapitel 4.1.1.
9.2 Klassen StringBuilder & StringBuffer
Die Klassen StringBuilder und StringBuffer repräsentieren Unicode Zeichenketten, die
verändert werden dürfen. Beide Klassen enthalten viele Funktionen zur Modifikation von
Texten, z.B. die Element-Funktion „append“, die einen Text an das bestehende StringBuilderoder StringBuffer-Objekt anfügen.
public class Kap_09_02_Bsp_01_StringBuilderUndBuffer {
public static void main(String[] args) {
StringBuilder sb1 = new StringBuilder("1: Append");
sb1.append(" mit");
sb1.append(" StringBuilder");
System.out.println(sb1);
StringBuffer sb2 = new StringBuffer("2: Append");
sb2.append(" mit");
sb2.append(" StringBuffer");
System.out.println(sb2);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 157 / 409
}
}
Ausgabe
1: Append mit StringBuilder
2: Append mit StringBuffer
Im Gegensatz zur String-Addition wird hierbei wirklich das Objekt verändert und kein Neues
erzeugt. Dafür müssen StringBuilder- und StringBuffer-Objekte ganz normal explizit mit „new“
erzeugt werden.
Abb. 9-2 : Ein StringBuilder-Objekt wird verändert
Die Klasse „StringBuffer“ existiert seit dem JDK 1.0, während die Klasse „StringBuilder“ erst mit
dem JDK 1.5 eingeführt. Beide Klassen haben dieselbe Schnittstelle. Im Gegensatz zu
StringBuffer ist StringBuilder aber nicht multi-threading fest, dafür aber wesentlich schneller.
Sie sollten also in single-threaded Anwendungen, oder in MT-unkritischen Situation (wie z.B.
String-Manipulation innerhalb einer Funktion mit einer lokalen Variable) die Klasse
„StringBuilder“ gegenüber „StringBuffer“ bevorzugen. Benötigen Sie dagegen eine MT feste
Klasse, so führt kein Weg an „StringBuffer“ vorbei – auch wenn dies mit Performance-Einbußen
verbunden ist.
Hinweis – auch die Klassen StringBuilder und StringBuffer können einfach so benutzt werden,
und benötigen kein „import“. Vergleichbar zur Klasse String kommen sie aus dem Package
„java.lang“, das immer automatisch zur Verfügung steht – siehe Kapitel 13.3.
9.3 Container & Iteratoren
Container sind Objekte, die andere Objekte aufnehmen können, diese verwalten, und dabei
automatisch passend wachsen. Es gibt nicht den einen Container, der für alle Zwecke optimal
geeignet ist, sondern jeder Container hat seine Vor- und Nachteile. Je nach Anwendung bzw.
Anforderung ist mal der Eine, mal der Andere besser geeignet. Nähere Informationen hierzu
finden Sie in Kapitel todo, und ausführlicher in vielen Büchern über „Algorithmen und
Datenstrukturen“.
Seit der ersten Java Version (JDK 1.0) sind einige grundlegende Container Bestandteil der
Java Klassen-Bibliothek. Mit der Version Java 2 (JDK 1.2) wurden die Container stark
überarbeitet und erweitert, und dann mit jeder JDK Version weiter verbessert. So ist heute ein
sehr ordentliches Container-Framework Bestandteil der Java Bibliothek.
9.3.1 Einführung in Container & Iteratoren
Ein typischer Container ist die „ArrayList“. Sie wird – wie jedes Objekt in Java – mit „new“
erzeugt und kann dann einfach genutzt werden. In spitzen Klammern geben wir den Typ der
Elemente an, die in der ArrayList gespeichert werden sollen. Mit diesen spitzen Klammern
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 158 / 409
nutzen wir die seit JDK 1.5 vorhandenen Generics, um den Container typsicher zu machen.
Im folgenden Beispiel wird eine ArrayList für Strings angelegt und mit 3 Strings gefüllt. Die
aktuelle Anzahl an Elementen im Container kann man dann mit „size()“ abfragen:
import java.util.ArrayList;
public class Kap_09_03_Bsp_01_ContainerEinstieg {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<String>();
al.add("Hallo");
al.add("Java");
al.add("Kurs");
System.out.println("Array-List enthaelt " + al.size() + " Elemente");
}
}
Ausgabe
Array-List enthaelt 3 Elemente
Die Typisierung mit den spitzen Klammern bewirkt, dass keine Objekte falschen Typs in den
Container eingefügt werden können39.
ArrayList<String> l = new ArrayList<String>();
l.add("Java");
// Okay, "Java" ist ein String
l.add(123);
// Compiler-Fehler, kein String
l.add(new StringBuilder());
// Compiler-Fehler, auch kein String
Seit JDK 1.7 kann man sich die Erzeugung eines typisierten Objektes erleichtern – man
benötigt die Angabe des Element-Typs in den spitzen Klammern nicht mehr, da der Compiler
diesen aus dem Variablen-Typ ermitteln kann.
import java.util.ArrayList;
public class Kap_09_03_Bsp_02_ContainerEinstiegJdk17 {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<>();
al.add("JDK 1.7");
System.out.println("Array-List enthaelt " + al.size() + " Element");
al = new ArrayList<>();
System.out.println("Array-List enthaelt " + al.size() + " Elemente");
}
}
Ausgabe
Array-List enthaelt 1 Elemente
Array-List enthaelt 0 Elemente
Aber wie läuft man jetzt über einen Container und gibt z.B. alle Element im Container aus? Für
eine ArrayList könnte man dies noch mit einer normalen For-Schleife mit Index-Zähler und dem
wahlfreiem Zugriff auf die Array-List (Element-Funktion „get(index)“) umsetzen:
import java.util.ArrayList;
Hier wird das in Java 5 (JDK 1.5) eingeführte Sprachmittel „Generics“ benutzt. Dieses JavaTutorial behandelt Generics leider nicht.
39
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 159 / 409
public class Kap_09_03_Bsp_03_ContainerSchleife {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<>();
al.add("Hallo");
al.add("Java");
al.add("Interessierte");
// Achtung – so laeuft man eigentlich nie ueber einen Container
for (int i = 0; i < al.size(); i++) {
String s = al.get(i);
System.out.print(s + " ");
}
}
}
Ausgabe
Hallo Java Interessierte
Für das normale Laufen über einen Container sollte man nie den wahlfreien Zugriff mit
„get(index)“ einsetzen, denn:
• Es gibt Container, bei denen der wahlfreie Zugriff sehr langsam ist – z.B. die LinkedList,
siehe Kapitel 9.3.5.
• Es gibt viele Container, die prinzip-bedingt gar keinen wahlfreien Zugriff unterstützen
können, und daher auch nicht enthalten – z.B. eine HashMap, siehe Kapitel 9.3.9.
• Außerdem gibt es für einfachere Lösungen für dieses Problem.
An die Stelle des wahlfreien Zugriffs treten dann die neue For-Schleife oder die Iteratoren40.
Beginnen wir mit der neuen For-Schleife.
Hinweis – die Nutzung des wahlfreien Zugriffs sollte nur dann genutzt werden, wenn man
diesen explizit durch den Algorithmus benötigt. Dann ist er natürlich sehr sinnvoll. Aber Sie
sollten dann auch einen Container wählen, der den wahlfreien Zugriff performant unterstützt,
wie z.B. die ArrayList – siehe Kapitel 9.3.4.
9.3.1.1 Neue For-Schleife für Container
Mit JDK 1.5 (Java 5) wurde ein neuer For-Schleifentyp eingeführt (siehe auch Kapitel 7.4) – der
seit dem die normale Variante ist. Hierbei muß nur noch eine Element-Lauf-Variable mit Typ,
und nach einem Doppelpunkt der Container angegeben werden – den Rest macht der Compiler
automatisch – siehe Beispiel.
import java.util.ArrayList;
public class Kap_09_03_Bsp_04_NeueForSchleife {
public static void main(String[] args) {
ArrayList<String> l = new ArrayList<>();
l.add("Java");
l.add("mit");
l.add("neuer");
l.add("JDK 1.5");
Für eine ausführliche Diskussion von Iteratoren siehe das Buch „Entwurfsmuster“ von u.a.
Erich Gamma.
40
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 160 / 409
l.add("For-Schleife");
for (String s : l) {
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Java mit neuer JDK 1.5 For-Schleife
Diese Schleife kann eigentlich nur eins: einfach über den Container laufen – Element für
Element. Mehr nicht, aber auch nicht weniger – und das ist der normale Anwendungsfall für
Container mit Schleifen, der 95 % Fall. Und den beherrscht sie einfach, schnell und zuverlässig.
Und darum nehmen wir diese Schleife auch immer für diesen Use-Case.
Die einzigen zusätzlichen Möglichkeiten der Container-For-Schleife sind die Nutzung der
Sprung-Anweisungen „break“ und „continue“ – siehe Kapitel 7.4 – die die Schleife vorzeitig
verlassen oder direkt zum nächsten Element gehen können. Hier ein Beispiel mit der neuen
For-Schleife mit „break“ und „continue“:
import java.util.ArrayList;
public class Appl {
public static void main(String[] args) {
ArrayList<String> l = new ArrayList<String>();
l.add("a");
l.add("b");
l.add("c");
l.add("d");
l.add("e");
l.add("f");
for (String s : l) {
System.out.print(s);
if (s.equals("b")) {
continue;
}
System.out.print("-");
if (s.equals("e")) {
break;
}
}
System.out.println();
// Zeile (*)
// Zeile (**)
}
}
Ausgabe
a-bc-d-e-
Nach der Ausgabe von „b “ wird kein Bindestrich ausgegeben, da das „continue“ in Zeile (*)
zuschlägt, und nach Ausgabe von „e-“ wird die Schleife aufgrund von „break“ in Zeile (**)
abgebrochen.
9.3.1.2 Container & Iteratoren
Ein Iterator ist die Abstraktion eines Objekts mit dem über eine Objekt-Menge gelaufen (iteriert)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 161 / 409
werden kann. Ein Iterator zeigt auf ein Objekt, kann auf das nächste gesetzt werden, und weiß,
ob es noch weitere Objekte in der Menge gibt.
Jeder Java Container hat eine Element-Funktion „iterator()“, die einen Iterator über den
Container zurückgibt. Mit der Element-Funktion „hasNext()“ kann am Iterator abgefragt werden,
ob noch weitere Elemente vorliegen. Die Element-Funktion „next()“ geht vor das nächste
Element und gibt dabei das aktuelle Element zurück:
import java.util.ArrayList;
import java.util.Iterator;
public class Appl {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<>();
al.add("Lasst");
al.add("uns");
al.add("Java");
al.add("lernen");
// Iterator mit While-Schleife
Iterator<String> it1 = al.iterator();
while (it1.hasNext()) {
String s = it1.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator mit For-Schleife
for (Iterator<String> it2 = al.iterator(); it2.hasNext(); ) {
String s = it2.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Lasst uns Java lernen
Lasst uns Java lernen
In der Praxis trifft man Iteratoren sowohl mit While- als auch mit For-Schleife – darum enthält
das obige Beispiel beide Varianten. Sie sind gleichwertig und es ist daher reine
Geschmackssache, welche Variante Sie bevorzugen.
9.3.2 Untypisierte Container
Alle bisherigen Beispiel arbeiten mit typisierten Containern, d.h. Containern, deren Element-Typ
mit spitzen Klammern angegeben ist41. Diese Typisierung hat zwei Vorteile:
• Sie können nur Elemente passendes Typs in den Container einfügen – Objekte mit falschem
Typ erzeugen einen Compiler-Fehler (siehe Beispiel weiter oben).
• Holen Sie Element aus dem Container, so ist der Typ bekannt und spezielle
Konvertierungen sind nicht notwendig.
Hier wird das in Java 5 (JDK 1.5) eingeführte Sprachmittel „Generics“ benutzt. Dieses JavaTutorial behandelt Generics leider nicht.
41
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 162 / 409
In der Praxis begegnet uns noch sehr häufig Code mit untypisierten Containern, da dies über 9
Jahre Stand der Java Technik war:
• Viele Schnittstellen sind in dieser Ära entstanden, setzen daher auf untypisierten Container
auf, und sind noch heute aktuell.
• Auch heute noch viel alter Java Code exisitiert und genutzt wird.
Von daher sollten Sie auch die Verwendung von untypisierten Containern kennen – in neuem
Code sollten Sie aber nur typisierte Container verwenden. Das folgende Beispiel zeigt wieder
die schon bekannte Nutzung der ArrayList, diesmal aber ohne Typisierung:
import java.util.ArrayList;
import java.util.Iterator;
public class Kap_09_03_Bsp_07_UntypisierteContainer {
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add("Untypisierter");
al.add("Container");
al.add("-");
al.add("ohne");
al.add("Generics");
Iterator it = al.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// (*)
// (**)
// (***)
}
}
Ausgabe
Untypisierter Container - ohne Generics
Bevor wir den obigen Code besprechen, erstmal ein wichtiger Hinweis: der obige Code erzeugt
sowohl im Java-Compiler „javac“ auf der Kommando-Zeile, als auch in den heutigen IDE's wie
der Eclipse Warnungen. Die beiden folgenden Abbildungen zeigen dies:
Abb. 9-3 : Warnungen des Java-Compilers wegen der Nutzung untypisierter Container
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 163 / 409
Abb. 9-4 : Warnungen der Eclipse 3.7.2 wegen der Nutzung untypisierter Container
Das freut mich – sowohl der Java-Compiler als auch die Eclipse sind meiner Meinung: Nutzen
Sie keine untypisierten Container.
Wenn Sie untypisierte Container aber doch nutzen müssen, dann unterdrücken Sie diese
Warnungen, damit die interessanten Warnungen nicht verdeckt werden. Dies können Sie z.B.
mit der seit Java 5 (JDK 1.5) vorhandenen Annotation „@SuppressWarnings“ machen42. Die
Eclipse kann sie automatisch als Quick-Fix für Sie einfügen. Der neue Quelltext sieht dann so
aus43:
import java.util.ArrayList;
import java.util.Iterator;
public class Kap_09_03_Bsp_07_UntypisierteContainer {
Annotations sind ein weiteres neues Sprachmittel von Java 5 (JDK 1.5). Auch dies wird in
diesem Java-Tutorial leider nicht besprochen.
43 So (mit Annotation) finden Sie den Quelltext auch unter den fertigen Code-Beispielen auf
meiner Homepage http://www.wilkening-online.de.
42
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 164 / 409
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add("Untypisierter");
al.add("Container");
al.add("-");
al.add("ohne");
al.add("Generics");
// (*)
Iterator it = al.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// (**)
// (***)
}
}
Ausgabe
Untypisierter Container - ohne Generics
Sowohl der Container als auch der Iterator werden hier ohne Element-Typ angegeben – siehe
Zeile (*) und (**). Prinzipiell sieht der Code dadurch einfacher aus, aber er ist nur unsicherer:
• Mit „add“ können jetzt beliebige Objekte dem Container hinzugefügt werden – nicht nur
Strings.
• Und die Iterator-Funktion „next“ (Zeile (***)) gibt das Element jetzt nur als „Object“ (siehe
Kapitel 14.11) zurück – und dies muß nun explizit von uns gecastet werden (Angabe des
Zieltyps in runden Klammern vor dem Funktions-Aufruf). Enthält der Container andere
Elemente, so wird diese Zeile zur Laufzeit schief gehen und eine Class-Cast Exception
werfen.
Das folgende Beispiel zeigt das typische Problem untypisierter Container. Das Problem ist,
dass der Fehler nicht vom Compiler erkannt wird, sondern erst zur Laufzeit auftritt – und wenn
Sie Pech haben, erst beim Kunden.
import java.util.ArrayList;
import java.util.Iterator;
public class Kap_09_03_Bsp_08_LaufzeitFehler {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add("Untypisierter");
al.add("Container");
al.add("-");
al.add(new StringBuilder());
// Achtung - kein String
al.add("Generics");
Iterator it = al.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// Laufzeit-Fehler beim
// vierten Durchlauf
}
}
Ausgabe
Untypisierter Container –
Exception in thread "main" java.lang.ClassCastException:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 165 / 409
java.lang.StringBuilder cannot be cast to java.lang.String
at Kap_09_03_Bsp_08_LaufzeitFehler.main
(Kap_09_03_Bsp_08_LaufzeitFehler.java:17)
Hinweis – zur Übung werden alle Container in den folgenden Container-Kapiteln sowohl
typisiert als auch untypisiert vorgestellt – bevorzugen Sie aber, wann immer möglich, die
typisierte Variante. In den restlichen Beispielen und Musterlösungen des Java-Tutorials
kommen daher dann auch nur noch die typisierten Varianten vor.
9.3.3 Container
Alle Container können hier nicht annähernd vorgestellt werden – dazu gibt zuviele in der Java
Bibliothek. Statt dessen beschränken wir uns hier auf die wichtigsten Container:
• java.util.ArrayList – Dynamisches Array (Kapitel 9.3.4)
• java.util.LinkedList – doppelt verkettete Liste (Kapitel 9.3.5)
• java.util.TreeSet – Sortierte Menge (Kapitel 9.3.6)
• java.util.HashSet – Unsortierte gehashte Menge (Kapitel 9.3.7)
• java.util.TreeMap – Sortierter assoziativer Container (Kapitel 9.3.8)
• java.util.HashMap – Unsortierter gehashter assoziativer Container (Kapitel 9.3.9)
Alle Container sind intern typlose Container, d.h. sie können alle Arten von Typen, die nicht
elementar sind, aufnehmen – d.h. alle Objekte. Dies hat manchmal Vorteile, führt aber in der
Praxis häufig zu Typ-Fehlern, die erst zur Laufzeit auffallen (vergleiche vorhergehendes Kapitel
9.3.2). Daher nutzen Sie bitte immer die typisierte Variante. In den folgenden Kapiteln werden
die Container der Vollständigkeit halber aber sowohl typisiert als auch untypisiert vorgestellt44.
Hinweis – um die Klassen ArrayList, TreeMap, Iterator, usw. benutzen zu können, müssen die
entsprechenden „import“ Anweisungen am Anfang des Quelltextes (nach der packageAnweisung und vor der Klassen-Definition) stehen – siehe Beispiele. Weitere Informationen zu
Packages und Imports finden Sie im Kapitel über Packages – siehe Kapitel 13.
Achtung – alle Java Container können nur Objekte aufnehmen, und keine elementaren
Datentypen. Einfache Bool-, Zeichen-, Integer- oder Fließkomma-Werte können Sie also nicht
direkt in Java Containern speichern. Sie müssen solche Werte in spezielle Wrapper-Klassen
einschliessen – siehe Kapitel 9.4. Seit dem JDK 1.5 geschieht dieses Wrappen automatisch,
dieses Java Feature nennt sich „Auto-Boxing“ – siehe Kapitel 9.4.1. Lassen Sie sich aber nicht
täuschen – Java Container können weiterhin keine elementaren Datentypen aufnehmen. Der
Effekt hat nichts an der internen Vorgehensweise und Implementierung geändert.
9.3.4 ArrayList
Die ArrayList ein dynamisches Array, d.h. ein Container bei dem die Elemente direkt
Während das Java-Tutorial genügend Raum für die typisierten Container hat, wird das
Sprachmittel der Generics im Java-Tutorial leider nicht eingeführt.
44
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 166 / 409
hintereinander im Speicher liegen und problemlos am Ende angefügt werden können.
Ausserdem ist er ein Beispiel für einen sequentiellen Container. Das ist ein Container, in dem
die Objekte sequentiell hintereinander liegen (z.B. in der Reihenfolge des Einfügens) und der
Container hierbei keinerlei Einfluss auf die Reihenfolge der Objekte nimmt.
todo
import java.util.ArrayList;
import java.util.Iterator;
public class Appl {
public static void main(String[] args) {
ArrayList<String> al = new ArrayList<>();
al.add("Hallo");
al.add("wunderbarer");
al.add("Java");
al.add("Kurs");
System.out.println("Der Container enthaelt " + al.size() + " Objekte");
// Neue JDK 1.5 For-Schleife
for (String s : al) {
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<String> it = al.iterator(); it.hasNext();) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<String> it = al.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt
Hallo wunderbarer Java
Hallo wunderbarer Java
Hallo wunderbarer Java
4 Objekte
Kurs
Kurs
Kurs
import java.util.ArrayList;
import java.util.Iterator;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add("Hallo");
al.add("wunderbarer");
al.add("Java");
al.add("Kurs");
System.out.println("Der Container enthaelt " + al.size() + " Objekte");
// Neue JDK 1.5 For-Schleife
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 167 / 409
for (Object o : al) {
String s = (String) o;
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator it = al.iterator(); it.hasNext();) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator it = al.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt
Hallo wunderbarer Java
Hallo wunderbarer Java
Hallo wunderbarer Java
4 Objekte
Kurs
Kurs
Kurs
9.3.5 LinkedList
todo
import java.util.Iterator;
import java.util.LinkedList;
public class Appl {
public static void main(String[] args) {
LinkedList<String> ll = new LinkedList<>();
ll.add("Hallo");
ll.add("wunderbarer");
ll.add("Java");
ll.add("Kurs");
System.out.println("Der Container enthaelt " + ll.size() + " Objekte");
// Neue JDK 1.5 For-Schleife
for (String s : ll) {
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<String> it = ll.iterator(); it.hasNext();) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<String> it = ll.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 168 / 409
}
Ausgabe
Der Container enthaelt
Hallo wunderbarer Java
Hallo wunderbarer Java
Hallo wunderbarer Java
4 Objekte
Kurs
Kurs
Kurs
import java.util.Iterator;
import java.util.LinkedList;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
LinkedList ll = new LinkedList();
ll.add("Hallo");
ll.add("wunderbarer");
ll.add("Java");
ll.add("Kurs");
System.out.println("Der Container enthaelt " + ll.size() + " Objekte");
// Neue JDK 1.5 For-Schleife
for (Object o : ll) {
String s = (String) o;
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator it = ll.iterator(); it.hasNext();) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator it = ll.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt
Hallo wunderbarer Java
Hallo wunderbarer Java
Hallo wunderbarer Java
4 Objekte
Kurs
Kurs
Kurs
9.3.6 TreeSet
todo
import java.util.Iterator;
import java.util.TreeSet;
public class Appl {
public static void main(String[] args) {
TreeSet<String> ts = new TreeSet< >();
ts.add("Detlef");
ts.add("Edgar");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 169 / 409
ts.add("Silke");
ts.add("Martina");
ts.add("Aaron");
System.out.println("Der Container enthaelt " + ts.size() + " Objekte");
boolean exists = ts.contains("Detlef");
System.out.println("Detlef ist im TreeSet vorhanden: " + exists);
exists = ts.contains("Hans");
System.out.println("Hans ist im TreeSet vorhanden: " + exists);
// Neue JDK 1.5 For-Schleife
for (String s : ts) {
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<String> it = ts.iterator(); it.hasNext();) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<String> it = ts.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt 5 Objekte
Detlef ist im TreeSet vorhanden: true
Hans ist im TreeSet vorhanden: false
Aaron Detlef Edgar Martina Silke
Aaron Detlef Edgar Martina Silke
Aaron Detlef Edgar Martina Silke
import java.util.Iterator;
import java.util.TreeSet;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
TreeSet ts = new TreeSet();
ts.add("Detlef");
ts.add("Edgar");
ts.add("Silke");
ts.add("Martina");
ts.add("Aaron");
System.out.println("Der Container enthaelt " + ts.size() + " Objekte");
boolean exists = ts.contains("Detlef");
System.out.println("Detlef ist im TreeSet vorhanden: " + exists);
exists = ts.contains("Hans");
System.out.println("Hans ist im TreeSet vorhanden: " + exists);
// Neue JDK 1.5 For-Schleife
for (Object o : ts) {
String s = (String) o;
System.out.print(s + " ");
}
System.out.println();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 170 / 409
// Iterator-Loesung mit For-Schleife
for (Iterator it = ts.iterator(); it.hasNext();) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator it = ts.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt 5 Objekte
Detlef ist im TreeSet vorhanden: true
Hans ist im TreeSet vorhanden: false
Aaron Detlef Edgar Martina Silke
Aaron Detlef Edgar Martina Silke
Aaron Detlef Edgar Martina Silke
9.3.7 HashSet
todo
import java.util.HashSet;
import java.util.Iterator;
public class Appl {
public static void main(String[] args) {
HashSet<String> hs = new HashSet<>();
hs.add("Detlef");
hs.add("Edgar");
hs.add("Silke");
hs.add("Martina");
hs.add("Aaron");
System.out.println("Der Container enthaelt " + hs.size() + " Objekte");
boolean exists = hs.contains("Detlef");
System.out.println("Detlef ist im HashSet vorhanden: " + exists);
exists = hs.contains("Hans");
System.out.println("Hans ist im HashSet vorhanden: " + exists);
// Neue JDK 1.5 For-Schleife
for (String s : hs) {
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<String> it = hs.iterator(); it.hasNext();) {
String s = it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<String> it = hs.iterator();
while (it.hasNext()) {
String s = it.next();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 171 / 409
System.out.print(s + " ");
}
System.out.println();
}
}
Mögliche Ausgabe (Die Reihenfolge im Hash-Container ist undefiniert)
Der Container enthaelt 5 Objekte
Detlef ist im HashSet vorhanden: true
Hans ist im HashSet vorhanden: false
Aaron Martina Edgar Silke Detlef
Aaron Martina Edgar Silke Detlef
Aaron Martina Edgar Silke Detlef
import java.util.HashSet;
import java.util.Iterator;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
HashSet hs = new HashSet();
hs.add("Detlef");
hs.add("Edgar");
hs.add("Silke");
hs.add("Martina");
hs.add("Aaron");
System.out.println("Der Container enthaelt " + hs.size() + " Objekte");
boolean exists = hs.contains("Detlef");
System.out.println("Detlef ist im HashSet vorhanden: " + exists);
exists = hs.contains("Hans");
System.out.println("Hans ist im HashSet vorhanden: " + exists);
// Neue JDK 1.5 For-Schleife
for (Object o : hs) {
String s = (String) o;
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator it = hs.iterator(); it.hasNext();) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator it = hs.iterator();
while (it.hasNext()) {
String s = (String) it.next();
System.out.print(s + " ");
}
System.out.println();
}
}
Mögliche Ausgabe (Die Reihenfolge im Hash-Container ist undefiniert)
Der Container enthaelt 5 Objekte
Detlef ist im HashSet vorhanden: true
Hans ist im HashSet vorhanden: false
Aaron Martina Edgar Silke Detlef
Aaron Martina Edgar Silke Detlef
Aaron Martina Edgar Silke Detlef
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 172 / 409
9.3.8 TreeMap
todo
Bei einem assoziativen Container werden Schlüssel/Wert Paare gespeichert, d.h. dass
eigentliche Objekt wird über einen Schlüssel referenziert. Ein Beispiel dafür ist der
ausgewogene binäre rot-schwarz Baum „TreeMap“, der außerdem noch implizit die Werte nach
dem Schlüssel sortiert.
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.TreeMap;
public class Appl {
public static void main(String[] args) {
TreeMap<String, String> tm = new TreeMap<>();
tm.put("Detlef", "1234");
tm.put("Edgar", "5678");
tm.put("Silke", "248");
tm.put("Martina", "999");
tm.put("Aaron", "646");
System.out.println("Der Container enthaelt " + tm.size() + " Objekte");
String s = tm.get("Edgar");
if (s != null)
System.out.println("Edgar hat die Nummer: " + s);
else
System.out.println("Edgar ist nicht in der TreeMap");
s = tm.get("Hans");
if (s != null)
System.out.println("Hans hat die Nummer: " + s);
else
System.out.println("Hans ist nicht in der TreeMap");
// Neue JDK 1.5 For-Schleife
for (Entry<String, String> e : tm.entrySet()) {
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<Entry<String, String>> it = tm.entrySet().iterator();
it.hasNext();) {
Entry<String, String> e = it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<Entry<String, String>> it = tm.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> e = it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 173 / 409
Ausgabe
Der Container enthaelt 5 Objekte
Edgar hat die Nummer: 5678
Hans ist nicht in der TreeMap
Aaron => 646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
Aaron => 646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
Aaron => 646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.TreeMap;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
TreeMap tm = new TreeMap();
tm.put("Detlef", "1234");
tm.put("Edgar", "5678");
tm.put("Silke", "248");
tm.put("Martina", "999");
tm.put("Aaron", "646");
System.out.println("Der Container enthaelt " + tm.size() + " Objekte");
String s = (String) tm.get("Edgar");
if (s != null)
System.out.println("Edgar hat die Nummer: " + s);
else
System.out.println("Edgar ist nicht in der TreeMap");
s = (String) tm.get("Hans");
if (s != null)
System.out.println("Hans hat die Nummer: " + s);
else
System.out.println("Hans ist nicht in der TreeMap");
// Neue JDK 1.5 For-Schleife
for (Object o : tm.entrySet()) {
Entry<String, String> e = (Entry<String, String>) o;
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator it = tm.entrySet().iterator(); it.hasNext();) {
Entry<String, String> e = (Entry<String, String>) it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<Entry<String, String>> it = tm.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> e = it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
}
}
Ausgabe
Der Container enthaelt 5 Objekte
Edgar hat die Nummer: 5678
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Hans ist
Aaron =>
Aaron =>
Aaron =>
Seite 174 / 409
nicht in der TreeMap
646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
646, Detlef => 1234, Edgar => 5678, Martina => 999, Silke => 248,
9.3.9 HashMap
todo
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
public class Appl {
public static void main(String[] args) {
HashMap<String, String> hm = new HashMap<>();
hm.put("Detlef", "1234");
hm.put("Edgar", "5678");
hm.put("Silke", "248");
hm.put("Martina", "999");
hm.put("Aaron", "646");
System.out.println("Der Container enthaelt " + hm.size() + " Objekte");
String s = hm.get("Edgar");
if (s != null)
System.out.println("Edgar hat die Nummer: " + s);
else
System.out.println("Edgar ist nicht in der HashMap");
s = hm.get("Hans");
if (s != null)
System.out.println("Hans hat die Nummer: " + s);
else
System.out.println("Hans ist nicht in der HashMap");
// Neue JDK 1.5 For-Schleife
for (Entry<String, String> e : hm.entrySet()) {
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator<Entry<String, String>> it = hm.entrySet().iterator();
it.hasNext();) {
Entry<String, String> e = it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator<Entry<String, String>> it = hm.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> e = it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
}
}
Mögliche Ausgabe (Die Reihenfolge im Hash-Container ist undefiniert)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 175 / 409
Der Container enthaelt 5 Objekte
Edgar hat die Nummer: 5678
Hans ist nicht in der HashMap
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
public class Appl {
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
HashMap hm = new HashMap();
hm.put("Detlef", "1234");
hm.put("Edgar", "5678");
hm.put("Silke", "248");
hm.put("Martina", "999");
hm.put("Aaron", "646");
System.out.println("Der Container enthaelt " + hm.size() + " Objekte");
String s = (String) hm.get("Edgar");
if (s != null)
System.out.println("Edgar hat die Nummer: " + s);
else
System.out.println("Edgar ist nicht in der HashMap");
s = (String) hm.get("Hans");
if (s != null)
System.out.println("Hans hat die Nummer: " + s);
else
System.out.println("Hans ist nicht in der HashMap");
// Neue JDK 1.5 For-Schleife
for (Object o : hm.entrySet()) {
Entry<String, String> e = (Entry<String, String>) o;
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit For-Schleife
for (Iterator it = hm.entrySet().iterator(); it.hasNext();) {
Entry<String, String> e = (Entry<String, String>) it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
// Iterator-Loesung mit While-Schleife
Iterator it = hm.entrySet().iterator();
while (it.hasNext()) {
Entry<String, String> e = (Entry<String, String>) it.next();
String key = e.getKey();
String val = e.getValue();
System.out.print(key + " => " + val + ", ");
}
System.out.println();
}
}
Mögliche Ausgabe (Die Reihenfolge im Hash-Container ist undefiniert)
Der Container enthaelt 5 Objekte
Edgar hat die Nummer: 5678
Hans ist nicht in der HashMap
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 176 / 409
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
Aaron => 646, Martina => 999, Edgar => 5678, Silke => 248, Detlef => 1234,
9.3.10 Container-Vergleich
todo
9.4 Wrapper Klassen
In Java gibt es für alle elementaren Datentypen (inkl. „void“) sogenannte Wrapper-Klassen. Sie
haben drei Aufgaben:
• Sie dienen als Wrapper-Klassen für z.B. das Container-Framework – siehe Kapitel 9.3.3 und
das folgende Kapitel 9.4.1. Hinweis – die Wrapper-Objekte sind wie Strings unveränderbar
(„immutable“), d.h. die Werte in den Objekten lassen sich nicht ändern.
• Sie sammeln zu dem Typ gehörige allgemeine Funktionen in Form von Klassen-Funktionen.
Z.B. die Funktionen zum Konvertieren, wie „parseInt“ – siehe Kapitel 3.11.
• Sie werden bei Reflexion als Meta-Klassen eingesetzt – siehe Kapitel 24. Dies erklärt auch
die Wrapper-Klasse „Void“, die sonst keinen Sinn machen würde.
Elementarer Datentyp
boolean
char
byte
short
int
long
float
double
void
Wrapper-Klasse
Boolean
Character
Byte
Short
Integer
Long
Float
Double
Void
Bis auf die Wrapper-Klassen für „char“ und „int“ haben sie den gleichen Namen wie der
jeweilige elementare Datentyp den sie kapseln, beginnen aber groß45.
9.4.1 Auto-Boxing
In Kapitel 9.3.3 habe ich geschrieben, dass die Container-Klassen keine elementaren DatenTypen aufnehmen können, sondern nur Objekte. Das läßt uns keine Ruhe, und darum stellen
wir diese Aussage hier auf die Probe:
import java.util.ArrayList;
import java.util.Iterator;
public class Kap_09_04_Bsp_01_AutoBoxing {
Warum heissen die Wrapper Klassen für „char“ und „int“ nicht wie die elementaren
Datentypen, sondern anders? Ich weiß es nicht, vermute aber historische Gründe.
45
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 177 / 409
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void main(String[] args) {
ArrayList al = new ArrayList();
al.add(1);
// "1" ist ein "int"
al.add(6);
// "6" ist ein "int"
al.add(23);
// "23" ist ein "int"
al.add(42);
// "42" ist ein "int"
System.out.println("Die Liste enthaelt " + al.size() + " ints");
Iterator it = al.iterator();
while (it.hasNext()) {
int n = (int) it.next();
System.out.print(n + " ");
}
// Cast in "int"
}
}
Ausgabe
Die Liste enthaelt 4 ints
1 6 23 42
Funktioniert doch! Was erzählt der denn da? Nun, wenn Sie Kapitel 9.3.3 vollständig in Ruhe
lesen, dann finden Sie noch eine weitere Aussage: „Elementare Datentypen müssen in
speziellen Wrapper-Klassen (siehe Kapitel 9.4) gewrappt werden. Seit dem JDK 1.5 kann
dieses Wrappen implizit geschehen (Auto-Boxing) – dies hat aber nichts an der internen
Vorgehensweise und Implementierung geändert“.
Hinweis – das Beispiel arbeitet mit einem untypisierten Container, da eine Typisierung auf „int“
zu einem Compiler-Fehler geführt hätte, und wir mit der korrekten Typisierung auf „Integer“ die
„Überraschung“ viel kleiner gewesen wäre.
Intern passiert also das im folgenden Beispiel explizit programmierte – nur das Sie es seit dem
JDK 1.5 nicht sehen, da der Compiler es automatisch für Sie macht. Aber Sie dürfen es
natürlich immer noch selber machen, bzw. bei einem älteren JDK (vor 1.5) müssen Sie es
sogar.
import java.util.ArrayList;
import java.util.Iterator;
public class Appl {
public static void main(String[] args) {
ArrayList<Integer> al = new ArrayList<>();
al.add(Integer.valueOf(1));
al.add(Integer.valueOf(6));
al.add(Integer.valueOf(23));
al.add(Integer.valueOf(42));
System.out.println("Die Liste enthaelt " + al.size() + " Integer");
Iterator<Integer> it = al.iterator();
while (it.hasNext()) {
Integer i = (Integer) it.next();
int n = i.intValue();
System.out.print(n + " ");
}
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 178 / 409
Ausgabe
Die Liste enthaelt 4 ints
1 6 23 42
• Beim Einfügen in den Container wird der elementare Datentyp (hier „int“) in eine WrapperKlasse (hier „Integer“) gewrappt („geboxt“). Beim Auto-Boxing geschieht dies automatisch.
• Möglicherweise haben Sie beim Einfügen „new Integer(x)“ statt „Integer.valueOf(x)“ erwartet
– immerhin müssen alle Objekte mit „new“ erzeugt werden. Im Prinzip würde „new
Integer(x)“ hier auch funktionieren, aber die Klassen-Funktion „valueOf“ gibt auch ein
entsprechendes Java Integer-Objekt zurück – kann aber schneller sein. Für genaue Details
schauen Sie bitte in die Java Referenz-Dokumentation.
• Beim Wandeln des Integer-Objekts in einen „int“ muß die Element-Funktion „intValue"
genutzt werden. Beim Auto-Boxing geschieht auch dies automatisch.
• Die ArrayList ist in diesem Beispiel auf „Integer“ typisiert – das ist die passende Typisierung
um via Auto-Boxing „int“s aufzunehmen.
Intern bleibt es aber so, dass Java Container keine elementaren Datentypen aufnehmen
können. Aber seit dem JDK 1.5 hilft uns die Sprache hier ganz automatisch.
9.4.2 Hierarchie
Alle numerischen Wrapper Klassen sind von der Klasse „java.lang.Number“ abgeleitet.
Abb. 9-5 : Vererbungs-Hierarchie der Wrapper-Klassen
9.5 Zufalls-Zahlen
Um Zufalls-Zahlen zu erzeugen gibt es im Package „java.util“ die Klasse „Random“. Man
erzeugt sich ein Objekt der Klasse „Random“ und kann sich von diesem Objekt Zufalls-Zahlen
erzeugen lassen. Es existieren Element-Funktionen zur Erzeugung von Zufalls-Zahlen für die
wichtigsten Typen („boolean“, „int“, „long“, „float“ und „double“) und einige weitere
Anwendungsfälle wie z.B. Byte-Arrays.
Die einfachste Element-Funktion ist „nextInt“, die einen Int-Paramter bekommt und dann
Zufalls-Zahlen zwischen „0“ (inkl.) und dem übergebenden Argument (exkl.) erzeugt. Im
folgenden Beispiel also Zahlen von „0-9“.
import java.util.Random;
public class Kap_09_05_Bsp_01_ZufallsZahlen {
public static void main(String[] args) {
Random rnd = new Random();
for (int i=0; i<20; i++) {
System.out.print(rnd.nextInt(10) + " ");
}
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 179 / 409
mögliche Ausgabe
2 2 5 3 7 0 3 6 4 5 0 7 4 0 3 8 8 7 6 8
Hinweis – vergessen Sie nicht den notwendigen Import von „java.util.Random“.
Benötigen Sie Zufalls-Zahlen in einem anderen Bereich (d.h. nicht von „0“ (inkl.) und dem
übergebenden Argument (exkl.)), dann verschieben Sie die Zahlen einfach durch eine Addition
oder Subtraktion. Um z.B. einen Würfel mit den Zahlen von „1-6“ zu simulieren, benötigen Sie
Zahlen von „0-5“ die Sie durch eine Addition mit „1“ in den gewünschten Bereich verschieben.
import java.util.Random;
public class Kap_09_05_Bsp_02_Wuerfel {
public static void main(String[] args) {
Random rnd = new Random();
for (int i = 0; i < 10; i++) {
System.out.print(rnd.nextInt(6) + 1 + " ");
}
}
}
mögliche Ausgabe
3 4 1 6 5 3 3 2 6 2
Hinweise:
• Benötigt man reproduzierbare Zufalls-Zahlen, so kann man ein Random-Objekt auch mit
einem Seed-Parameter konstruieren, oder bei einem vorhandenen Random-Objekt den
Seed mit der Funktion „setSeed“ setzen.
• Double-Zufalls-Zahlen kann man sich auch mit der Klassen-Funktion „random“ der Klasse
„java.lang.Math“ erzeugen lassen. Die genauen Unterschiede zwischen dieser Funktion und
„nextDouble“ aus „Random“ finden Sie in der Java Referenz-Dokumentation.
9.6 Datum und Uhrzeit
Für Datum und Uhrzeit Funktionen stehen u.a. folgende Klassen zur Verfügung:
• java.util.Date
• java.util.Calendar
• java.text.DateFormat
• java.text.SimpleDateFormat
Wird einfach ein Date Objekt erzeugt ("new Date()“), so enthält dieses Objekt das aktuelle
Datum und die aktuelle Zeit. Die Klassen „DateFormat“ und „SimpleDateFormat“ bieten
Methoden zur Formatierung der Wandlung des Datums in einen String.
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class Kap_09_06_Bsp_01_Datum {
public static void main(String[] args) {
Date now = new Date();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 180 / 409
DateFormat df = DateFormat.getDateInstance();
System.out.println(df.format(now));
df = DateFormat.getDateInstance(DateFormat.FULL);
System.out.println(df.format(now));
df = DateFormat.getTimeInstance();
System.out.println(df.format(now));
df = DateFormat.getTimeInstance(DateFormat.FULL);
System.out.println(df.format(now));
df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
System.out.println(df.format(now));
df = new SimpleDateFormat("d.M.yyyy");
System.out.println(df.format(now));
}
}
mögliche Ausgabe
06.05.2012
Sonntag, 6. Mai 2012
01:05:39
01:05 Uhr MESZ
Sonntag, 6. Mai 2012 01:05 Uhr MESZ
6.5.2012
• Die Definition eines Ausgabe-Formats mit „DateFormat“ arbeitet defaultmäßig mit der
aktuellen Länderkennung - dies läßt sich natürlich ändern.
• „DateFormat“ kann auch Strings parsen – Element-Funktion „parse“.
• Mit der Klasse „Calendar“ kann Datums- und Uhrzeit-Arithmetik mit Date-Objekten
durchgeführt werden, und es können viele weitere Informationen gewonnen werden.
• Achtung – viele Funktion in „Date“ sind „deprecated“ und durch Funktionen der Klasse
„Calendar“ ersetzt worden46.
Hinweis – mit dem JDK 1.8 ist eine neue Date-Time Bibliothek in Java eingeführt worden.
Diese ist viel durchdachter und leistungsfähiger als die alte hier kurz angesprochene Bibliothek.
In realen Projekten sollten Sie besser diese nutzen.
9.7 Datei- und Verzeichnis-Handling
Die Klasse „java.io.File“ repräsentiert eine Datei oder ein Verzeichnis, und sie bietet viele
Funktionen um Dateien und Verzeichnisse zu manipulieren oder Informationen über sie zu
erlangen. Hier ein kleiner Auszug aus der Schnittstelle der Klasse „File“:
File(String fullName)
Erzeugt ein File Objekt für das Element des Datei-Systems mit
dem entsprechenden Namen. Der Name ist vollständig inkl.
Pfad.
Deprecated Klassen oder Funktionen sind veraltet bzw. weisen Probleme auf, und sollten
daher nicht mehr benutzt werden. Normalerweise bietet Java in diesen Fällen neue und
bessere Elemente an.
46
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
File(String path, String name)
Seite 181 / 409
Erzeugt ein File Objekt für das Element des Datei-Systems mit
dem entsprechenden Namen. Der Name wird hierbei getrennt in
Pfad und eigentlichem Namen übergeben.
Der Vorteil ist hier, dass der Benutzer sich nicht um den Trenner
im String kümmern muß, der ja plattform-spezifisch ist.
boolean exists()
Gibt zurück, ob die Datei oder das Verzeichnis existiert.
String getAbsolutePath()
Gibt den Namen (inkl. vollständigem absolutem Pfad) des
Verzeichnisses oder der Datei zurück.
boolean isDirectory()
Gibt zurück, ob das File-Objekt ein Verzeichnis referenziert.
boolean isFile()
Gibt zurück, ob das File-Objekt eine Datei referenziert.
long length()
Gibt bei einer Datei die Größe der referenzierten Datei zurück.
String[ ] list()
Gibt bei einem Verzeichnis die Namen der Elemente im
Verzeichnis (Verzeichnisse und Dateien) als Array von Strings
zurück.
Als erstes Beispiel ein kleines Programm, dass überprüft ob der auf der Kommandozeile
übergebene Name im Datei-System existiert, und ob es eine Datei oder ein Verzeichnis ist.
Wenn es eine Datei ist, so wird noch die Größe der Datei ausgegeben.
import java.io.File;
public class Kap_09_07_Bsp_01_File {
public static void main(String[] args) {
if (args.length!=1) {
System.out.println("Fehler - es wird ein Datei-Name erwartet");
return;
}
String name = args[0];
File file = new File(name);
if (!file.exists()) {
System.out.println("Element \"" + name + "\" existiert nicht");
return;
}
if (file.isFile()) {
String len = "" + file.length();
System.out.println("\"" + name + "\" hat " + len + " Bytes");
return;
}
if (file.isDirectory()) {
System.out.println("\"" + name + "\" ist ein Verzeichnis");
return;
}
System.out.println("Element \"" + name + "\" -> unbekannten Typ");
}
}
Mögliche Ausgabe
>java Appl
Fehler - es wird ein Datei-Name erwartet
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 182 / 409
Mögliche Ausgabe
>java Appl java
Element "java" existiert nicht
Mögliche Ausgabe
>java Appl Kap_09_07_Bsp_01_File.class
"Kap_09_07_Bsp_01_File.class" hat 1414 Bytes
Mögliche Ausgabe
>java Appl .
"." ist ein Verzeichnis
Hinweise
• Für die Verwendung von plattform-unabhängigen Datei-Namen wird in Java die URI
Konvention gewählt, für die es eine eigene Klasse „java.net.URI“ gibt.
• Lesen und Schreiben von Dateien geschieht in Java mit Streams, die mit einem File-Objekt
verbunden sind – siehe Kapitel 23.
9.7.1 Rekursive Datei-Suche
Ein typisches Problem, dass wir nun lösen können, ist die tiefe Suche im Dateisystem z.B. nach
einer Datei mit einem bestimmten Namen. Bevor Sie sich an dieses Unternehmen wagen,
empfehle ich Ihnen, sich sinnvolle Testdaten zu erzeugen – sprich eine kleine Beispiel
Verzeichnis-Struktur. Ich habe es leider erlebt, dass Anfänger ihr Programm zum Test auf ihr
Root-Dateisystem wie z.B. „C:\“ losgelassen haben, und sich dann gewundert haben dass ihr
Programm ewig läuft, Fehler meldet oder scheinbar einfach nur den Rechner lahm legt. Ist
doch auch kein Wunder, oder? Bei unseren heutigen Plattengrößen gibt es da ziemlich viele
Verzeichnisse zu durchsuchen und Datei-Namen zu vergleichen, dass der Rechner gut belastet
ist. Und mit ziemlicher Sicherheit trifft das Programm unterwegs auf Verzeichnisse, für die es
keine Rechte hat – und schon regnet es Exceptions, und damit können wir noch gar nicht gut
umgehen (das lernen wir ja erst in Kapitel 22).
Lange Rede, kurzer Sinn – machen Sie sich eine kleine Test-Daten-Struktur auf Ihrer Platte.
Ich habe das gemacht, unter „C:\JavaDateiSystemBeispiele“ – und so sieht sie bei mir aus.
Suchen wollen wir später die drei Dateien mit dem Namen „find.txt“.
Test-Verzeichnis-Struktur
C:\
└
JavaDateiSystemBeispiele
└
verzeichnis1
│ └
verzeichnis3
│ │ └
verzeichnis4
│ │ │ └
find.txt
│ │ │ └
search.dat
│ │ │ └
test.txt
│ │ └
test.txt
│ └
verzeichnis5
│ │ └
find.txt
│ │ └
test.txt
│ └
daten.dat
│ └
test.txt
└
verzeichnis2
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 183 / 409
│ └
find.txt
│ └
test.txt
└
search.dat
└
test.txt
Hinweis – bitte ignorieren Sie im Augenblick die blau markierten Dateien „search.dat“ und
„daten.dat“ – wir benötigen Sie später in den Aufgaben 9.8.7 und 9.8.8.
Damit können wir nun loslegen. Der erste Schritt ist einfach. Wir laufen einfach über das SuchVerzeichnis und geben alles aus, was wir finden – getrennt nach Verzeichnissen und Dateien.
Einzige Besonderheit: wir geben den Namen vollständig aus, und setzen ihn nicht selber
zusammen, sondern lassen ihn uns vom File-Objekt geben – Zeile (*). Hintergrund hierfür sind
die unterschiedlichen Datei-Trenner unter z.B. Windows (Backslash „\“) und Linux (Slash „/“),
um die wir uns sonst selbst kümmern müßten.
import java.io.File;
public class Kap_09_07_Bsp_02_FileSucheRekursivStep1 {
public static void main(String[] args) {
File path = new File("C:\\JavaDateiSystemBeispiele");
String[] filenames = path.list();
for (String filename : filenames) {
File file = new File(path, filename);
String fullfilename = file.getAbsolutePath();
if (file.isDirectory()) {
System.out.println("Verz.: " + fullfilename);
} else if (file.isFile()) {
System.out.println("Datei: " + fullfilename);
}
}
}
// (*)
}
Mögliche Ausgabe (andere Reihenfolge möglich – ist nicht definiert)
Datei: C:\JavaDateiSystemBeispiele\search.dat
Datei: C:\JavaDateiSystemBeispiele\test.txt
Verz.: C:\JavaDateiSystemBeispiele\verzeichnis1
Verz.: C:\JavaDateiSystemBeispiele\verzeichnis2
Der nächste Schritt ist minimal – wir vergleichen den gefundenen Datei-Namen mit dem
Namen, den wir suchen – und damit wir was finden, suchen wir erstmal nach „test.txt“. Wenn
die Datei stimmt, dann geben wir sie mit komplettem Pfad aus. Und bei Verzeichnissen machen
wir erstmal nix.
import java.io.File;
public class Kap_09_07_Bsp_03_FileSucheRekursivStep2 {
public static void main(String[] args) {
String searchfile = "test.txt";
File path = new File("C:\\JavaDateiSystemBeispiele");
String[] filenames = path.list();
for (String filename : filenames) {
File file = new File(path, filename);
String fullfilename = file.getAbsolutePath();
if (file.isDirectory()) {
// Im Augenblick machen wir hier noch nichts
} else if (file.isFile()) {
if (file.getName().equals(searchfile)) {
System.out.println("-> " + fullfilename);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 184 / 409
}
}
}
}
}
Ausgabe
-> C:\JavaDateiSystemBeispiele\test.txt
Kommen wir nun zum Fall, dass das Verzeichnis selber wieder ein Verzeichnis enthält. In
diesem Fall müssen wir nur für dieses Verzeichnis das Gleiche machen: alle Elemente
durchlaufen, Dateien vergleichen, bei Verzeichnissen wieder das Gleiche. Das klingt nach
Rekursion, wie wir sie in Kapitel 8.7 kennen gelernt haben. Wichtig – dazu müssen wir die
Funktionalität in eine Funktion auslagern – sonst können wir sie nicht rekursiv aufrufen. Und
mehr machen wir in diesem Schritt auch nicht.
import java.io.File;
public class Kap_09_07_Bsp_04_FileSucheRekursivStep3 {
public static void main(String[] args) {
String searchpath = "C:\\JavaDateiSystemBeispiele";
String searchfile = "test.txt";
searchFile(searchpath, searchfile);
}
public static void searchFile(String searchpath, String searchfile) {
File path = new File(searchpath);
String[] filenames = path.list();
for (String filename : filenames) {
File file = new File(path, filename);
String fullfilename = file.getAbsolutePath();
if (file.isDirectory()) {
// Im Augenblick machen wir hier noch nichts
} else if (file.isFile()) {
if (file.getName().equals(searchfile)) {
System.out.println("-> " + fullfilename);
}
}
}
}
}
Ausgabe
-> C:\JavaDateiSystemBeispiele\test.txt
Nachdem unsere Verzeichnis-Durchforste-Funktionalität nun in einer Funktion ist, können wir
sie jetzt auch aufrufen – und diesmal auch mit dem richtigen Datei-Namen.
import java.io.File;
public class Kap_09_07_Bsp_05_FileSucheRekursivStep4 {
public static void main(String[] args) {
String searchpath = "C:\\JavaDateiSystemBeispiele";
String searchfile = "find.txt";
searchFile(searchpath, searchfile);
}
public static void searchFile(String searchpath, String searchfile) {
File path = new File(searchpath);
String[] filenames = path.list();
for (String filename : filenames) {
File file = new File(path, filename);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 185 / 409
String fullfilename = file.getAbsolutePath();
if (file.isDirectory()) {
searchFile(fullfilename, searchfile);
} else if (file.isFile()) {
if (file.getName().equals(searchfile)) {
System.out.println("-> " + fullfilename);
}
}
}
}
}
Mögliche Ausgabe (andere Reihenfolge möglich – ist nicht definiert)
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis2\find.txt
Wow – alles funktioniert. So einfach und elegant ist Rekursion. Damit unser Quelltext noch
etwas mehr nach richtigem Programm aussieht, integrieren wir noch ein paar Dinge:
• Programm-Name
• Rückmeldung, wo gesucht wird
• Rückmeldung, wonach gesucht wird
• Und eine Sicherheits-Abfrage, falls jemand „searchFile“ nicht mit einem Verzeichnis als
ersten Parameter aufruft.
import java.io.File;
public class Kap_09_07_Bsp_06_FileSucheRekursiv {
public static void main(String[] args) {
System.out.println("Rekursive Datei-Suche mit java.io.File");
String searchpath = "C:\\JavaDateiSystemBeispiele";
String searchfile = "find.txt";
System.out.println("Suche in: " + searchpath);
System.out.println("Nach: " + searchfile);
searchFile(searchpath, searchfile);
}
public static void searchFile(String searchpath, String searchfile) {
File path = new File(searchpath);
if (!path.isDirectory()) {
System.out.println("Fehler - kein Verzeichnis");
return;
}
String[] filenames = path.list();
for (String filename : filenames) {
File file = new File(searchpath, filename);
String fullfilename = file.getAbsolutePath();
if (file.isDirectory()) {
searchFile(fullfilename, searchfile);
} else if (file.isFile()) {
if (searchfile.equals(filename)) {
System.out.println("-> " + fullfilename);
}
}
}
}
}
Mögliche Ausgabe (andere Reihenfolge möglich – ist nicht definiert)
Rekursive Datei-Suche mit java.io.File
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 186 / 409
Suche in: c:\aaa
Nach: find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt
-> C:\\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis2\find.txt
Hinweise:
• In den Aufgaben 9.8.7 und 9.8.8 werden wir dieses Programm zur rekursiven Datei-Suche
um weitere Fähigkeiten ergänzen.
• Im nächsten Kapitel wird die rekursive Datei-Suche mit dem JDK 1.7 Package „java.nio.file“
implementiert.
9.7.2 Rekursive Datei-Suche mit „java.nio.file“
Seit dem JDK 1.4 gibt es das Package New-IO „java.nio“ in Java. In Java 7 wurde es um das
Unter-Package „java.nio.file“ ergänzt. Hierdrin sind viele Klassen mit stark erweiterten
Möglichkeiten gegenüber „java.io.file“ enthalten – über Kopieren von Dateien und
Verzeichnissen bis hin zu Themen wie User-Rechte auf Datei-Systeme. So leistungsfähig das
neue „java.nio.file“ Package auch ist – die Nutzung ist für Einsteiger oft schwer und komplex.
Von daher nutzen wir hier im Java-Tutorial nur die File-Klasse – später sollten Sie sich aber
ruhig mal an „java.nio.file“ erinnern, falls Sie aufwändigere Aufgaben im Kontext Datei-System
lösen müssen.
Als Beispiel, aber ohne jede Erklärung, nochmal die rekursive Datei-Suche – nun aber mit
„java.nio.file“.
import
import
import
import
import
import
java.io.File;
java.io.IOException;
java.nio.file.DirectoryStream;
java.nio.file.Files;
java.nio.file.Path;
java.nio.file.Paths;
public class Kap_09_07_Bsp_03_FileSucheRekursivMitNiofile {
public static void main(String[] args) {
System.out.println("Rekursive Datei-Suche mit java.nio.file");
String searchpath = "C:\\JavaDateiSystemBeispiele";
String searchfile = "find.txt";
System.out.println("Suche in: " + searchpath);
System.out.println("Nach: " + searchfile);
try {
Path path = Paths.get(searchpath);
searchFile(path, searchfile);
} catch (IOException e) {
System.out.println("Fehler: " + e.getMessage());
}
}
public static void searchFile(Path path, String searchfile)
throws IOException {
DirectoryStream<Path> dirStream = Files.newDirectoryStream(path);
for (Path entry : dirStream) {
File file = entry.toFile();
if (file.isDirectory()) {
searchFile(entry, searchfile);
} else if (file.isFile()) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 187 / 409
if (file.getName().equals(searchfile)) {
System.out.println("-> " + file.getAbsolutePath());
}
}
}
}
}
Ausgabe (unter Annahme der obigen Datei-Struktur)
Rekursive Datei-Suche mit java.nio.file
Suche in: c:\aaa
Nach: find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis2\find.txt
9.8 Aufgaben
9.8.1 Aufgabe „String-Analyse“
Schreiben Sie ein Programm das eine Zeile einliest, und ausgibt wie oft welches Zeichen in der
Zeile vorkommt. Erzeugen Sie eine Ausgabe der Zeichen in der Reihenfolge ihres ersten
Vorkommens im Eingabetext – weitere Vorkommen der Zeichen werden bei der Ausgabe
ignoriert.
Mögliche Ein- und Ausgabe:
Eingabe: Hallo Welt: 123332
'H': 1
'a': 1
'l': 3
'o': 1
' ': 2
'W': 1
'e': 1
't': 1
':': 1
'1': 1
'2': 2
'3': 3
Implementieren Sie zwei Lösungen:
• Die erste Lösung soll ohne Container auskommen und nur mit den Klassen String,
StringBuffer und/oder StringBuilder implementiert werden.
• Im Gegensatz zur ersten Lösung sollen Sie hier Container nutzen – und erreichen damit
(hoffentlich) eine viel einfachere Lösung.
Lösung siehe Kapitel 9.9.
9.8.2 Aufgabe „Hallo <Person>“
Schreiben Sie ein Programm, dass einen kompletten Namen einliest, und diesen Namen mit
Sternchen und Leerzeichen umrandet als Anrede ausgibt. Bsp: der eingebene Name ist „Detlef
Wilkening“, dann soll folgende Ausgabe erzeugt werden:
Mögliche Ein- und Ausgabe:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 188 / 409
Name: Detlef Wilkening
***************************
*
*
* Hallo Detlef Wilkening! *
*
*
***************************
Lösung siehe Kapitel 9.10.
9.8.3 Aufgabe „Hallo <Personen>“
Schreiben Sie ein Programm ähnlich Aufgabe 9.8.2, dass hier aber beliebig viele komplette
Namen einliest. Wird eine leere Eingabe gemacht, so gelten damit alle Namen als eingegeben.
Danach sollen alles diese Namen mit Anrede und umrandet mit Sternchen und Leerzeichen
ausgegeben werden – die Reihenfolge soll hierbei der Eingabe entsprechen. Die Umrandung
soll sich am längsten Namen orientieren. Bsp: die eingegebenen Namen seien „Max“,
„Johannes Wolfgang“ und „Werner“, dann soll folgende Ausgabe erzeugt werden:
Mögliche Ein- und Ausgabe:
Name: Max
Name: Johannes Wolfgang
Name: Werner
Name:
****************************
*
*
* Hallo Max!
*
* Hallo Johannes Wolfgang! *
* Hallo Werner!
*
*
*
****************************
Lösung siehe Kapitel 9.11.
9.8.4 Aufgabe „Lesbare Zahlen 2“
Eine ähnliche Aufgabe wie 7.9.3: schreiben Sie ein Programm, dass Benutzer-Eingaben von
Ziffern von 1 bis 9 als lesbaren Text ausgibt. Die Eingabe einer beliebigen anderen Zahl oder
von etwas anderem sollen das Programm beenden.
Bitte geben Sie eine Ziffer ein: 2
-> zwei
Bitte geben Sie eine Ziffer ein: 7
-> sieben
Bitte geben Sie eine Ziffer ein: x
Ende
Als Unterschied zu Aufgabe 7.9.3 sollen Sie hier keine Kontrollstruktur (switch oder if) zur
Abbildung der Zahlen auf Texte verwenden, sondern einen Container. Machen Sie sich klar,
dass diese Lösung kürzer, performanter, und auch noch besser lesbar ist.
Die gleiche Aufgabe finden Sie noch mal in Kap. 10.8.1 – dort soll sie mit Arrays gelöst werden,
was in diesem Fall noch ein bisschen effizienter ist.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 189 / 409
Lösung siehe Kapitel 9.12.
9.8.5 Aufgabe „Lottozahlen“
Schreiben Sie ein Lotto-Programm, d.h. ein Programm dass sechs Zufallszahlen zwischen 1
und 49 inkl. sortiert ausgibt. Aber Achtung – keine Zufallszahl darf mehrfach vorkommen.
Lösung siehe Kapitel 9.13.
9.8.6 Aufgabe „Telefonbuch“
Schreiben Sie ein erstes ganz ganz einfaches Telefonbuch47.
Das Telefonbuch soll Namen und Telefonnummern speichern – am Anfang begnügen wir uns
mit einer Nummer pro Name, und es soll keine Name doppelt vorkommen (d.h. jeder Name im
Telefonbuch ist ein Unikat). Die Telefon-Nummern sind keine Zahlen, sondern Texte, damit z.B.
Leerzeichen oder ein Slash „/“ für eine lesbarere Formatierung möglich sind.
Die Einträge im Telefonbuch sollen im Augenblick noch fest im Programm stehen, d.h. es
können im Augenblick keine Einträge hinzugefügt, gelöscht, oder editiert werden.
Der Benutzer kann entweder einen der Befehle „end“ bzw. „list“ oder einen Namen eingeben.
• Bei „end“ soll das Programm beendet werden.
• Bei „list“ sollen alle Einträge im Telefonbuch (zuerst Name, dann Nummer, eine Zeile pro
Eintrag – sortiert nach Namen (nicht lexikalisch sortiert, sondern nach Zeichenkodierung)
ausgegeben werden.
• Alle anderen Benutzer-Eingaben werden als Namen interpretiert, und im Telefonbuch
gesucht. Wird der Name gefunden, so wird die Nummer ausgegeben. Wird der Name nicht
gefunden, so wird eine entsprechende Meldung ausgegeben. Achtung – der Name muss
exakt übereinstimmen, und es werden auch Groß- und Klein-Schreibung unterschieden.
Nach der Bearbeitung des Befehls „list“ oder eines Namens kann der Benutzer die nächste
Eingabe machen.
Mögliche Ein- und Ausgabe
Telefonbuch
Eingabe: list
6
-
Eintraege:
Bernd => 0555 / 55 55 55
Detlef => 0123 / 12 21 1
Dietmar => 0321 / 888 222 99
Edgar => 0111 / 11 11 1
Edmund => 0222 / 33 22 11
Karl => 0111 / 11 22 33
Wir werden dieses Telefonbuch in weiteren Aufgaben mit mehr Wissen zu einer kleinen
Kontaktdaten Verwaltung ausbauen, aber hier und jetzt fangen wir erstmal klein an. Für später
siehe die Aufgaben 11.7, 11.8, 14.19, 14.16.4, todo oder todo.
47
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 190 / 409
Eingabe: Max
Name "Max" ist nicht im Telefonbuch vorhanden.
Eingabe: Detlef
Detlef => 0123 / 12 21 1
Eingabe: end
Programmende
Lösung siehe Kapitel 9.14.
9.8.7 Aufgabe „Platten-Platz Verbrauch“
In Kapitel 9.7.1 haben wir eine einfache Form der rekursiven Datei-Suche kennengelernt. Nun
sollen Sie das Programm etwas erweitern.
• Ihr Programm soll berechnen, wieviel Platten-Platz alle Dateien eines Typs (einer Extension)
innerhalb eines Verzeichnisses verbrauchen.
• Zusätzlich soll der Benutzer das Verzeichnis und die Such-Extension (ohne „.“) auf der
Kommando-Zeile eingeben können.
• Die Verzeichnis-Eingabe soll sich so oft wiederholen, bis der Benutzer ein gültiges
Verzeichnis eingegeben hat.
• Wird keine Extension eingegeben, wird der Platten-Platz Verbrauch aller Dateien im
Verzeichnis berechnet.
• Vergleichbar zur rekursiven Datei-Suche in Kapitel 9.7.1 ignorieren wir Probleme, die sich
aus User-Rechten und fehlenden Zugriffs-Rechten resultieren.
Beispiele, unter der Berücksichtigung der Datei-System-Struktur aus Kapitel 9.7.1, und
entsprechender Datei-Größen:
Mögliche Ein- und Ausgabe
Platten-Platz Verbrauch
Geben Sie bitte das Verzeichnis ein: C:\JavaDateiSystemBeispiele
Geben Sie bitte die Extension ein: dat
Gesamtverbrauch: 3472 Bytes
Mögliche Ein- und Ausgabe
Platten-Platz Verbrauch
Geben Sie bitte das Verzeichnis ein: C:\JavaDateiSystemBeispiele
Geben Sie bitte die Extension ein: txt
Gesamtverbrauch: 5683 Bytes
Mögliche Ein- und Ausgabe
Platten-Platz Verbrauch
Geben Sie bitte das Verzeichnis ein: C:\JavaDateiSystemBeispiele
Geben Sie bitte die Extension ein:
Gesamtverbrauch: 9155 Bytes
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 191 / 409
Mögliche Ein- und Ausgabe
Platten-Platz Verbrauch
Geben Sie bitte das Verzeichnis ein: kein-verzeichnis
"kein-verzeichnis" ist kein Verzeichnis
Geben Sie bitte das Verzeichnis ein: immer noch nicht
"immer noch nicht" ist kein Verzeichnis
Geben Sie bitte das Verzeichnis ein: C:\JavaDateiSystemBeispiele
Geben Sie bitte die Extension ein: dat
Gesamtverbrauch: 3472 Bytes
Lösung siehe Kapitel 9.15.
9.8.8 Aufgabe „Datei-Suche“
Die nächste Aufgabe erweitert die einfache Datei-Suche aus Kapitel 9.7.1.
• Ihr Programm soll alle Dateien innerhalb eines Verzeichnisses suchen, die einem von
mehreren Namen entsprechen.
• Der Benutzer soll das Such-Verzeichnis und die Such-Datei-Namen auf der KommandoZeile eingeben können.
• Die Such-Verzeichnis-Eingabe soll sich so oft wiederholen, bis der Benutzer ein gültiges
Verzeichnis eingegeben hat.
• Die Such-Datei-Namen-Eingabe soll sich so oft wiederholen, bis der Benutzer einen
Leerstring eingibt. Alle Namen bis dahin gelten als zu Suchen.
• Die gefundenen Dateien mit ihren Pfaden sollen sortiert nach den Datei-Namen ausgeben
werden. Jeder Ausgabe-Block soll mit dem Datei-Namen beginnen, und danach eine
eingerückte Aufzählung der Verzeichnisse (inkl. Datei-Namen).
• Wurde keine Datei zu einem Datei-Such-Namen gefunden, so wird nur der Datei-SuchName ausgegeben.
• Vergleichbar zur rekursiven Datei-Suche in Kapitel 9.7.1 ignorieren wir Probleme, die sich
aus User-Rechten und fehlenden Zugriffs-Rechten resultieren.
Beispiele, unter der Berücksichtigung der Datei-System-Struktur aus Kapitel 9.7.1:
Mögliche Ein- und Ausgabe
Datei-Suche
----------Geben
Geben
Geben
Geben
Sie
Sie
Sie
Sie
bitte
bitte
bitte
bitte
das
die
die
die
Such-Verzeichnis ein: C:\JavaDateiSystemBeispiele
Such-Namen ein: find.txt
Such-Namen ein: search.dat
Such-Namen ein:
find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis5\find.txt
-> C:\JavaDateiSystemBeispiele\verzeichnis2\find.txt
search.dat
-> C:\JavaDateiSystemBeispiele\search.dat
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\search.dat
Mögliche Ein- und Ausgabe
Datei-Suche
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 192 / 409
----------Geben Sie bitte das Such-Verzeichnis ein: kein Verzeichnis
"kein Verzeichnis" ist kein Verzeichnis
Geben
Geben
Geben
Geben
Geben
Sie
Sie
Sie
Sie
Sie
bitte
bitte
bitte
bitte
bitte
das
die
die
die
die
Such-Verzeichnis ein: C:\JavaDateiSystemBeispiele
Such-Namen ein: search.dat
Such-Namen ein: search.txt
Such-Namen ein: search.png
Such-Namen ein:
search.dat
-> C:\JavaDateiSystemBeispiele\search.dat
-> C:\JavaDateiSystemBeispiele\verzeichnis1\verzeichnis3\verzeichnis4\search.dat
search.png
search.txt
Lösung siehe Kapitel 9.16.
9.9 Lsg. zu Aufgabe „String-Analyse“ – Kap. 9.8.1
9.9.1 Erste Lösung mit String, StringBuffer bzw. StringBuilder
Als erstes müssen wir einen String von der Kommandozeile einlesen. Da dies – dank Reader
und Exceptions – kein Einzeiler ist, lagern wir dies in eine Funktion aus. Im Fehlerfall gibt die
Funktion einen Leerstring zurück – damit können wir im Augenblick gut leben. So könnte die
Funktion aussehen48.
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
Dann kommt die eigentlich Analyse des Strings. Dazu müssen wir über den String laufen, und
jeden Buchstaben auf die Anzahl der Vorkommen überprüfen – der Pseudocode sieht im ersten
Schritt also so aus. Achtung, dieser Algorithmus ist noch fehlerhaft.
// Vorsicht - noch fehlerhafter Algorithmus
for-each zeichen in string
laufe ueber string und zaehle anzahl dieses zeichens
zeichen und anzahl ausgeben
In Java-Code könnte das so aussehen:
// Vorsicht - noch fehlerhafter Algorithmus
for (int i=0; i<in.length(); i++) {
char c = in.charAt(i);
int count = 0;
Die Funktion ist übrigens so gut, daß wir sie in den nächsten Aufgaben auch immer wieder
ohne lange Erklärung wiederbenutzen werden.
48
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 193 / 409
for (int j=0; j<in.length(); j++) {
if (in.charAt(j)==c) {
count++;
}
}
System.out.println("'" + c + "': " + count);
}
Ausgabe bei in = "aba"
'a': 2
'b': 1
'a': 2
Das Problem ist, dass wir jetzt die mehrfach vorkommenden Zeichen auch mehrfach zählen
und ausgeben – und das ist nicht gewollt.
Wir müssen also die Zeichen markieren, die wir schon gezählt haben. Da wir diese Lösung
ohne Container implementieren wollen, müssen wir im String vermerken, wann ein Zeichen
schon behandelt worden ist. Dazu brauchen wir ein Zeichen, dass im String nicht auftauchen
kann – also als eindeutiges „Schon-Behandelt-Zeichen“ dienen kann.
Im Rahmen unserer Aufgabe gibt es ein solches Zeichen: der Zeilenumbruch ‚\n‘. Dieses
Zeichen kann nicht im String vorhanden sein, da wir den String von der Tastatur einlesen, und
die Return-Taste die Eingabe abschliesst.
Damit wir auf dieses Marker-Zeichen gut lesbar im Code einsetzen können, benutzen wir es
nicht direkt im Quelltext, sondern nutzen eine lokale Konstante, d.h. eine lokale Variable mit
dem Modifier „final“ – siehe Kapitel 8.3 und auch Kapitel 7.13.1. Damit wird aus unserem Code
von oben:
final char ok = '\n';
for (int i=0; i<in.length(); i++) {
char c = in.charAt(i);
if (c==ok) continue;
// (*)
int count = 0;
for (int j=0; j<in.length(); j++) {
if (in.charAt(j)==c) {
count++;
in.setCharAt(j, ok);
// (**)
}
}
System.out.println("'" + c + "': " + count);
}
Neben der lokalen Konstanten hat sich der Code an zwei Stellen geändert:
• Ist das zu analysierende Zeichen unser Marker-Zeichen, so dürfen wir nicht analysieren, und
setzen die Schleife direkt fort – Zeile (*).
• Finden wir während der Analyse im String das zu suchende Zeichen, dann müssen wir es
durch das Marker-Zeichen ersetzen – Zeile (**).
Damit funktioniert unser Programm, und wir könnten uns zufrieden der nächsten Aufgabe
widmen. Aber so ganz optimal ist unser Programm noch nicht. Sobald wir ein neu zu zählendes
Zeichen finden, wissen wir ja, dass es im String vorher nicht vorhanden sein kann – ansonsten
hätten wir es ja schon gezählt. Also brauchen wir den Anfang des Strings für das Zählen nicht
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 194 / 409
zu berücksichtigen. Wenn wir mit dem Zählwert „count=1“ starten, brauchen wir erst mit dem
Zeichen „j+1“ für das Zählen zu starten. Da eine For-Schleife kopfgesteuert ist, und daher auch
0-mal durchlaufen werden kann (siehe Kapitel 7.3), funktioniert dies auch problemlos für das
letzte Zeichen der Schleife.
final char ok = '\n';
for (int i=0; i<in.length(); i++) {
char c = in.charAt(i);
if (c==ok) continue;
int count = 1;
for (int j=i+1; j<in.length(); j++) {
if (in.charAt(j)==c) {
count++;
in.setCharAt(j, ok);
}
}
System.out.println("'" + c + "': " + count);
}
Aber seien Sie sich darüber im klaren – auch mit unserer Optimierung ist dies kein schneller
Algorithmus. Im Prinzip muss für jedes Zeichen im String der String einmal vollständig
durchlaufen werden. Man nennt dies einen quadratischen Algorithmus, da er eine quadratische
Zeit-Komplexität hat. Eine Verdopplung der String-Länge führt zu einer Vervierfachung der
notwendigen Zeit, eine Verdreifachung der String-Länge zu einer Verneunfachung der
notwendigen Zeit. Besser wird die Lösung mit Container im nächsten Kapitel sein.
Mit Klasse und Main-Funktion ergibt das alles zusammen folgenden Code:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
final char ok = '\n';
System.out.print("Eingabe: ");
StringBuilder in = new StringBuilder(readLine());
for (int i=0; i<in.length(); i++) {
char c = in.charAt(i);
if (c==ok) continue;
int count = 1;
for (int j=i+1; j<in.length(); j++) {
if (in.charAt(j)==c) {
count++;
in.setCharAt(j, ok);
}
}
System.out.println("'" + c + "': " + count);
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 195 / 409
}
Ausgabe
Eingabe: Hallo Waldilal
'H': 1
'a': 3
'l': 5
'o': 1
' ': 1
'W': 1
'd': 1
'i': 1
9.9.2 Zweite Lösung mit Container
Erklärung folgt (hoffentlich) später – todo...
import
import
import
import
import
java.io.BufferedReader;
java.io.InputStreamReader;
java.util.ArrayList;
java.util.Iterator;
java.util.TreeMap;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
System.out.print("Eingabe: ");
String in = readLine();
ArrayList<Character> order = new ArrayList<>();
TreeMap<Character, Integer> map = new TreeMap<>();
for (int i = 0; i < in.length(); i++) {
char c = in.charAt(i);
Integer count = map.get(c);
if (count == null) {
map.put(c, 1);
order.add(c);
} else {
map.put(c, ++count);
}
}
for (char c : order) {
int count = map.get(c);
System.out.println("'" + c + "': " + count);
}
}
}
Ausgabe
Eingabe: Hallo Waldilal
'H': 1
'a': 3
'l': 5
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
'o':
' ':
'W':
'd':
'i':
Seite 196 / 409
1
1
1
1
1
9.10 Lsg. zu Aufgabe „Hallo <Person>“ – Kap. 9.8.2
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static String string(int n, char c) {
StringBuilder erg = new StringBuilder(n);
for (int i=0; i<n; i++) {
erg.append(c);
}
return erg.toString();
}
public static void main(String[] args) {
System.out.print("Bitte geben Sie Ihren Namen ein: ");
String name = readLine();
int len = name.length() + 11;
System.out.println(string(len, '*'));
System.out.println('*' + string(len-2, ' ') + '*');
System.out.println("* Hallo " + name + "! *");
System.out.println('*' + string(len-2, ' ') + '*');
System.out.println(string(len, '*'));
}
}
Ausgabe
Bitte geben Sie Ihren Namen ein: Detlef Wilkening
***************************
*
*
* Hallo Detlef Wilkening! *
*
*
***************************
9.11 Lsg. zu Aufgabe „Hallo <Personen>“ – Kap. 9.8.3
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 197 / 409
import java.util.Iterator;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static String string(int n, char c) {
StringBuilder erg = new StringBuilder(n);
for (int i=0; i<n; i++) {
erg.append(c);
}
return erg.toString();
}
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
int maxLen = 0;
while (true) {
System.out.print("Bitte geben Sie die Namen ein: ");
String name = readLine();
if (name.length()==0) break;
list.add(name);
if (name.length()>maxLen) {
maxLen = name.length();
}
}
int len = maxLen + 11;
String stars = string(len, '*');
String spaces = string(len - 2, ' ');
System.out.println(stars);
System.out.println('*' + spaces + '*');
for (String s : list) {
int slen = s.length();
String rest = string(maxLen - slen + 1, ' ');
System.out.println("* Hallo " + s + '!' + rest + '*');
}
System.out.println('*' + spaces + '*');
System.out.println(stars);
}
}
Ausgabe
Bitte geben Sie die Namen
Bitte geben Sie die Namen
Bitte geben Sie die Namen
Bitte geben Sie die Namen
***********************
*
*
* Hallo Max!
*
* Hallo Wolf-Guenter! *
* Hallo Tassilo!
*
*
*
***********************
ein: Max
ein: Wolf-Guenter
ein: Tassilo
ein:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 198 / 409
9.12 Lsg. zu Aufgabe „Lesbare Zahlen 2“ – Kap. 9.8.4
9.12.1 Lösung 1: Doch mit Switch-Anweisung
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
while (true) {
System.out.print("Bitte geben Sie eine Ziffer ein: ");
String in = readLine();
int number;
try {
number = Integer.parseInt(in);
if (number<1 || number>9) break;
} catch (Exception x) {
break;
}
switch (number) {
case 1:
System.out.println("->
break;
case 2:
System.out.println("->
break;
case 3:
System.out.println("->
break;
case 4:
System.out.println("->
break;
case 5:
System.out.println("->
break;
case 6:
System.out.println("->
break;
case 7:
System.out.println("->
break;
case 8:
System.out.println("->
break;
case 9:
System.out.println("->
break;
}
eins");
zwei");
drei");
vier");
fuenf");
sechs");
sieben");
acht");
neun");
}
System.out.println("Ende");
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 199 / 409
}
9.12.2 Lösung 2: Mit TreeMap
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
// HashMap ist sehr gut, aber nicht optimal – noch besser ist Loesung 3
HashMap<Integer, String> numbers = new HashMap<>();
numbers.put(0, "null");
numbers.put(1, "eins");
numbers.put(2, "zwei");
numbers.put(3, "drei");
numbers.put(4, "vier");
numbers.put(5, "fuenf");
numbers.put(6, "sechs");
numbers.put(7, "sieben");
numbers.put(8, "acht");
numbers.put(9, "neun");
while (true) {
System.out.print("Bitte geben Sie eine Ziffer ein: ");
String in = readLine();
int number;
try {
number = Integer.parseInt(in);
if (number < 1 || number > 9) {
break;
}
} catch (Exception x) {
break;
}
System.out.println(numbers.get(number));
}
}
}
9.12.3 Lösung 3: Mit ArrayList
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 200 / 409
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
final ArrayList<String> numbers = new ArrayList<>();
numbers.add("null");
numbers.add("eins");
numbers.add("zwei");
numbers.add("drei");
numbers.add("vier");
numbers.add("fuenf");
numbers.add("sechs");
numbers.add("sieben");
numbers.add("acht");
numbers.add("neun");
while (true) {
System.out.print("Bitte geben Sie eine Ziffer ein: ");
String in = readLine();
int number;
try {
number = Integer.parseInt(in);
if (number < 0 || number > 9) {
break;
}
} catch (Exception x) {
break;
}
System.out.println(numbers.get(number));
}
}
}
9.13 Lsg. zu Aufgabe „Lottozahlen“ – Kap. 9.8.5
Erklärung folgt (hoffentlich) später – todo...
import java.util.Iterator;
import java.util.Random;
import java.util.TreeSet;
public class Appl {
public static void main(String[] args) {
Random rnd = new Random();
TreeSet<Integer> numbers = new TreeSet<>();
while (numbers.size() < 6) {
numbers.add(rnd.nextInt(49) + 1);
}
for (Integer i : numbers) {
System.out.print(i + " ");
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 201 / 409
}
Mögliche Ausgabe
7 13 28 33 34 44
Sehr sehr wichtiger Hinweis – wenn Sie mit den Zahlen dieses Programms Lotto spielen und
gewinnen, dann müssen Sie mich mit 40% am Gewinn beteiligen. Benachrichtigen Sie mich
bitte per Mail „[email protected]“.
9.14 Lsg. zu Aufgabe „Telefonbuch“ – Kap. 9.8.6
Erklärung folgt (hoffentlich) später – todo...
import
import
import
import
java.io.BufferedReader;
java.io.InputStreamReader;
java.util.Map.Entry;
java.util.TreeMap;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
System.out.println("Telefonbuch");
TreeMap<String, String> items = new TreeMap<>();
items.put("Detlef", "0123 / 12 21 1");
items.put("Edgar", "0111 / 11 11 1");
items.put("Bernd", "0555 / 55 55 55");
items.put("Edmund", "0222 / 33 22 11");
items.put("Karl", "0111 / 11 22 33");
items.put("Dietmar", "0321 / 888 222 99");
while (true) {
System.out.print("\nEingabe: ");
String in = readLine();
System.out.println();
if (in.equals("end"))
break;
if (in.equals("list")) {
System.out.println(items.size() + " Eintraege:");
for (Entry<String, String> e : items.entrySet()) {
System.out.println("- " + e.getKey() + " : " + e.getValue());
}
continue;
}
String number = items.get(in);
if (number == null) {
System.out.println(
"Name \"" + in + "\" ist nicht im Telefonbuch vorhanden.");
continue;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 202 / 409
System.out.println(in + " => " + number);
}
System.out.println("Programmende");
}
}
9.15 Lsg. zu Aufgabe „Platten-Platz Verbrauch“ – Kap. 9.8.7
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
public class Kap_09_15_Lsg_PlattenPlatzVerbrauch {
public static void main(String[] args) {
System.out.println("Platten-Platz Verbrauch\n");
File dir = readDirectory();
String ext = readSearchExtension();
long size = calcFileSizes(dir, ext);
System.out.println("\nGesamtverbrauch: " + size + " Bytes");
}
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static File readDirectory() {
while (true) {
System.out.print("Geben Sie bitte das Verzeichnis ein: ");
String in = readLine();
File dir = new File(in);
if (dir.isDirectory()) {
return dir;
}
System.out.println("\"" + dir + "\" ist kein Verzeichnis\n");
}
}
public static String readSearchExtension() {
System.out.print("Geben Sie bitte die Extension ein: ");
String in = readLine();
if (in.isEmpty()) {
return in;
}
return "." + in;
}
public static long calcFileSizes(File dir, String extension) {
long res = 0;
String[] filenames = dir.list();
for (String filename : filenames) {
File file = new File(dir, filename);
if (file.isDirectory()) {
res += calcFileSizes(file, extension);
} else if (file.isFile()) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 203 / 409
if (file.getName().endsWith(extension)) {
res += file.length();
}
}
}
return res;
}
}
9.16 Lsg. zu Aufgabe „Datei-Suche“ – Kap. 9.8.8
Erklärung folgt (hoffentlich) später – todo...
import
import
import
import
import
import
java.io.BufferedReader;
java.io.File;
java.io.InputStreamReader;
java.util.ArrayList;
java.util.Map.Entry;
java.util.TreeMap;
public class Kap_09_16_Lsg_DateiSuche {
public static void main(String[] args) {
System.out.println("Datei-Suche\n-----------\n");
File dir = readDirectory();
TreeMap<String, ArrayList<String>> files = readSearchFileNames();
searchFile(dir, files);
System.out.println();
for (Entry<String, ArrayList<String>> entry : files.entrySet()) {
System.out.println(entry.getKey());
for (String ffn : entry.getValue()) {
System.out.println("-> " + ffn);
}
}
}
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
} catch (Exception x) {
}
return in;
}
public static File readDirectory() {
while (true) {
System.out.print("Geben Sie bitte das Such-Verzeichnis ein: ");
String in = readLine();
File dir = new File(in);
if (dir.isDirectory()) {
return dir;
}
System.out.println("\"" + dir + "\" ist kein Verzeichnis\n");
}
}
public static TreeMap<String, ArrayList<String>> readSearchFileNames() {
TreeMap<String, ArrayList<String>> res = new TreeMap<>();
while (true) {
System.out.print("Geben Sie bitte die Such-Namen ein: ");
String in = readLine();
if (in.isEmpty()) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 204 / 409
return res;
}
res.put(in, new ArrayList<String>());
}
}
public static void
searchFile(File dir, TreeMap<String, ArrayList<String>> files) {
String[] filenames = dir.list();
for (String filename : filenames) {
File file = new File(dir, filename);
if (file.isDirectory()) {
searchFile(file, files);
} else if (file.isFile()) {
findFile(file, files);
}
}
}
public static void
findFile(File file, TreeMap<String, ArrayList<String>> files) {
for (Entry<String, ArrayList<String>> entry : files.entrySet()) {
if (file.getName().equals(entry.getKey())) {
entry.getValue().add(file.getAbsolutePath());
}
}
}
}
10 Arrays
Arrays sind schon kurz in Kapitel 3.5 eingeführt worden. Hier wollen wir sie jetzt detailierter
besprechen.
•
•
•
•
•
•
•
Arrays sind in Java Objekte, d.h. sie stellen keinen „elementaren“ Daten-Typ dar.
Array-Variablen sind Referenz-Variablen (siehe 5.4.2).
Auf die Array-Elemente kann sowohl lesend als auch schreibend mit dem Operator [int]
zugegriffen werden.
Array-Zugriffe mit dem Operator [int] sind 0-basiert.
Zugriffe auf nicht existente Elemente lösen die Exception „IndexOutOfBoundsException“ aus
– siehe Kapitel 10.6.
Arrays haben ein Attribut „length”, dass die Anzahl an Elementen im Array enthält.
Arrays sind von der Klasse „Object“ abgeleitet – siehe Kapitel 14.11.
10.1 Deklaration
Arrays können in Java auf zwei Arten deklariert werden: Die eckigen Klammern, die ein Array
als solches identifizieren, können sowohl am Typ als als auch am Bezeichner stehen. Die
typische Syntax in Java ist die erste Variante.
int[] a1;
int a2[];
// Typische Java Syntax
// Auch moeglich, sieht man aber selten
Initialisierung
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 205 / 409
Es gibt zwei Arten, Arrays zu initialisieren:
1. Literale Initailisierung
Bei der literalen Initialisierung wird hinter der Deklaration in geschweiften Klammern eine Liste
der Initialwerte aufgeführt. Der Compiler erzeugt implizit ein Array entsprechender Größe und
weist die Werte dem Array zu.
Es darf dabei keine Größe in den eckigen Klammern der Variablen-Deklaration angegeben
werden, auch nicht die Richtige.
int[] a = { 1, 2, 3, 4 };
boolean[] b = { true, false, true };
double[2] d = { 1.4, 3.1 };
// Compiler-Error – Groessenangabe nicht
erlaubt
Das Array a bekommt hier automatisch die Größe 4, das Array b die Größe 3 zugewiesen.
Ausserdem werden die Arrays mit den aufgeführten Werten initialisiert.
Die literale Initialisierung darf nur bei der Deklaration einer Array-Variablen benutzt werden. Für
spätere Neubesetzung muss die dynamische Initialisierung benutzt werden.
int[] a = { 1, 2, 3, 4 };
...
a = { 1, 2, 6, 8 };
// Compiler-Error
// Hier ist nur die dynamische Initialisierung
erlaubt
2. Dynamische Initialisierung
Bei der dynamischen Initialisierung legt man selber das Array mit new, Typname und
gewünschter Größe in eckigen Klammern an – das Array wird dabei mit den Defaultwerten des
Typ‘s initialisiert.
int[] a = new int[6];
boolean[] b = new boolean[2];
Hierbei wird mit new ein entsprechender Speicherplatz reserviert (a - Größe 6 / b - Größe 2)
und die Array-Elemente werden mit den Default-Werten der jeweiligen Datentypen gefüllt.
Auch bei der dynamischen Initialisierung können die Initialisierungswerte in Form einer Liste
angegeben werden. Dabei darf keine Größenangabe in den eckigen Klammern stehen und die
Liste muss direkt hinter den eckigen Klammern folgen. Die Größe des Arrays bestimmt der
Compiler implizit aus der Anzahl an Werten in der Liste.
int[] a = new int[] { 2, 5, 9 };
boolean[] b = new boolean[2] { true, false }; // Compiler-Error - wegen
Groessenangabe
10.2 Arrays haben ein Attribut: length
Array-Objekte haben keine Funktion und genau ein Attribut, das nur lesenden Zugriff erlaubt:
‚length‘ liefert die Anzahl an Elementen im Array zurück.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 206 / 409
int[] a = { 1, 2, 3, 4 };
System.out.println(a.length);
Ausgabe
4
10.3 Arrays durchlaufen
Seit Java 5 (JDK 1.5) gibt es einen neuen Schleifen-Typ für Container und Arrays, der schon in
Kapitel 7.4 kurz vorgestellt wurde. Wollen Sie einfach nur über das gesamte Array laufen, dann
nutzen Sie diesen Typ. Alternativ können Sie natürlich ein normale For-Schleife mit SchleifenZähler nutzen.
int[] a = { 1, 2, 3, 4 };
System.out.println("Neue Schleife: ");
for (int n : a) {
System.out.print(n + " ");
}
System.out.println();
System.out.println("Alte Schleife: ");
for (int i=0; i<a.length; i++) {
System.out.print(a[i] + " ");
}
System.out.println();
Ausgabe
Neue Schleife: 1 2 3 4
Alte Schleife: 1 2 3 4
10.4 Arrays sind Referenzen
Auch Arrays sind keine elementaren Datentypen, d.h. unterliegen auch Array-Variablen der
Referenz-Semantik.
int[] a = { 1, 2, 3, 4 };
int[] b = a;
a[2] = 42;
for (int i : a) {
System.out.print(i + "
}
System.out.println();
for (int i : b) {
System.out.print(i + "
}
System.out.println();
Ausgabe
1 2 42
1 2 42
");
");
4
4
Im Beispiel zeigen beide Array-Variablen auf das gleiche Array. Der schreibende Zugriff auf das
dritte Element von a (a[2] = 42;) ändert damit auch das Array b.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 207 / 409
1.
Array int[]
a:
=>
b:
=>
1
2.
2
3
4
Array int[]
a:
=>
b:
=>
1
2
42
4
Abb. 10-1 : Referenz-Semantik bei Arrays
10.5 Arrays können mehrdimensional sein
Natürlich können Arrays auch mehrdimensional sein. Sie sind kein spezieller Datentyp, sondern
werden als Array-von-Arrays implementiert. Die Deklaration der Variablen enthält dann für jede
Dimension ein Klammen-Paar, die Initialisierungsliste besteht dann aus einer Liste von Listen.
Der Zugriff erfolgt über eine entsprechende Anzahl von Index-Operatoren [].
int[][] a = { {1,2}, {3, 4}, {5,6} };
for (int[] aa : a) {
for (int i : aa) {
System.out.print(i + " ");
}
System.out.println();
}
Ausgabe
1 2
3 4
5 6
Achtung – seien Sie sich darüber im klaren, dass Java die Elemente eines mehrdimensionales
Arrays nicht bündig in einem Block im Speicher ablegt, sondern es ein echtes Array-aus-Arrays
ist.
Array: int[][]
4
Array: int[]
3
>
2
Array: int[]
Array: int[]
1
>
=>
>
a:
5
6
Abb. 10-2 : Speicher-Layout eines mehrdimensionalen Arrays
Arrays müssen nicht unbedingt rechteckig sein
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 208 / 409
Aus dem oben gesagten folgt natürlich auch, dass Arrays nicht unbedingt rechteckig sein
müssen.
int[][] a = { {1}, {2, 3}, {4, 5, 6, 7} };
for (int[] aa : a) {
for (int i : aa) {
System.out.print(i + " ");
}
System.out.println();
}
Ausgabe
1
2 3
4 5 6 7
Array: int[][]
3
Array: int[]
2
>
Array: int[]
Array: int[]
1
>
=>
>
a:
4
5
6
7
Abb. 10-3 : Speicher-Layout eines nicht-rechteckigen Arrays
10.6 Fehlerhafter Index
Der Zugriff auf ein nicht existentes Objekt führt java-typisch zu einer Exception
(„java.lang.IndexOutOfBoundsException“). Zugriffe mit fehlerhaftem Index führen also nicht zu
potentiellen Problemen wie in manchen anderen Programmier-Sprachen49, sondern werden
sauber gemeldet.
int[] array = { 1, 2 };
array[0] = 0;
// Okay
array[1] = 0;
// Okay
array[2] = 0;
// Laufzeit-Fehler => wirft Exception
array[-1] = 0;
// Laufzeit-Fehler => wirft Exception
10.7 Vergleiche
Werden zwei Arrays mit dem Operator „==“ verglichen, so wird nur die Identität verglichen, da
es sich um Referenz-Objekte handelt – siehe Kapitel 5.4.2. Auch die Benutzung der von
„Object“ geerbten Element-Funktion „equals“ ändert daran nichts, da das Default-Verhalten der
Vergleich auf Identität ist – siehe Kapitel todo – und die Elementfunktion für Arrays nicht
überschrieben worden ist.
49
Wie z.B. C oder C++.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 209 / 409
Für Werte-Vergleiche auf Arrays muss die Funktion „java.util.Arrays.equals“ benutzt werden.
10.8 Aufgaben
10.8.1 Aufgabe „Lesbare Zahlen 3“
Die gleiche Aufgabe wie 9.8.4: schreiben Sie ein Programm, dass Benutzer-Eingaben von
Zahlen von 1 bis 9 als lesbaren Text ausgibt. Die Eingabe einer beliebigen anderen Zahl oder
eine Fehleingabe soll das Programm beenden.
Bitte geben Sie eine Zahl ein: 2
-> zwei
Bitte geben Sie eine Zahl ein: 7
-> sieben
Bitte geben Sie eine Zahl ein: x
-> Ende
Als Unterschied zu Aufgabe 9.8.4 sollen Sie hier keinen Container, sondern ein Array
verwenden. Machen Sie sich klar, dass diese Lösung vielleicht noch effizienter ist.
Lösung siehe Kapitel 10.9.
10.9 Lsg. zu Aufgabe „Lesbare Zahlen 3“ – Kap. 10.8.1
Erklärung folgt (hoffentlich) später – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static String readLine() {
String in = "";
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
in = reader.readLine();
}
catch (Exception x) {
}
return in;
}
public static void main(String[] args) {
final String[] numbers = {
"eins", "zwei", "drei", "vier", "fuenf",
"sechs", "sieben", "acht", "neun"
};
while (true) {
System.out.print("Bitte geben Sie eine Zahl ein: ");
String in = readLine();
int number;
try {
number = Integer.parseInt(in);
if (number<1 || number>9) break;
} catch (Exception x) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 210 / 409
break;
}
System.out.println("-> " + numbers[number-1]);
}
System.out.print("-> Ende");
}
}
11 Klassen
11.1 Motivation
Um nicht-triviale Probleme in den Griff zu bekommen, muss man die Probleme in kleine
überschaubare Einheiten zerlegen (Stichwort „teile und herrsche“), die möglichst unabhängig
von einander sein sollten (Stichwort „Entkopplung“). Das heißt, man muss in seinem Programm
Abstraktionen einziehen, die Komplexität verbergen (am besten so kapseln, dass gar keine
Zugriff auf die Interna möglich ist – Stichwort „Information-Hiding“) und auf eine einfache Art
Funktionalität zur Verfügung stellen. Ein Beispiel dafür sind z.B. die Container-Klassen von
Java, die die gesamten für die Speicherung und Verwaltung von Objekten notwendigen Daten,
Techniken und Mechanismen verbergen.
In unserem normalen Leben arbeiten wir doch auch so. Wenn sie z.B. ein Haus bauen, dann
werden weder sie, noch der Architekt, und wohl auch nicht der Maurer hierbei die
Eigenschaften von Elementarteilchen wie Quarks oder Bosonen berücksichtigen. Jeder arbeitet
mit den Abstraktions-Ebenen, die für sein Problem angepaßt sind. Der eine denkt halt in
Elementarteilchen, dem nächsten passen Atom-Kerne und Elekronenhüllen. Wieder andere
denken in Molekülen, noch andere z.B. in Werkstoffeigenschaften. Ein Ingenieur wechselt dann
sich in die Sichtweise von – tief unten – Schrauben und Zahnrädern. Weiter oben wird dann
z.B. in Getriebe oder Motoren gedacht. Usw, usw. . Jeder arbeitet halt mit den AbstraktionsEbenen, die für sein Problem angepaßt sind. Und alles darunter interessiert ihn nicht, bzw. wird
aktiv versucht auszublenden, damit das Problem überschaubar bleibt.
Abb. 11-1 : Das Schlüsselwort heißt „Abstraktion“
Einer der Hauptgedanken der Objekt-Orientierung ist daher die Idee, abgeschlossene
gekapselte Einheiten programmieren und anbieten zu können, bei denen der Benutzer sich
keine Gedanken mehr über das Innenleben machen muss, sondern sie einfach über ihre
Schnittstelle benutzt.
Abb. 11-2 : Objekt-Kapselung und Benutzung über die Schnittstelle
Damit ist das Ziel klar – es muß möglich sein:
• abgeschlossene gekapselte Einheiten zu definieren,
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 211 / 409
• diesen Einheiten eine Schnittstelle zu geben,
• sie sollte einfach zu implementieren sein, und
• einfach zu benutzen sein.
Dies wird in Java – wie in allen OO Sprachen – über Klassen und Objekte erreicht. Hierbei sind
• Klassen eine allgemeine Beschreibung einer bestimmten Art Objekte. Eine Klasse Auto
beschreibt ganz allgemein alle Autos, sowohl meine alte Karre, einen neues Familienauto,
als auch das Cabrio meines Kollegen. Allen diesen Autos ist gemein, dass sie Autos sind,
und sich durch gemeinsame Fähigkeiten und Merkmale wie Grösse, Leistung, Verbrauch,
Farbe, usw. auszeichnen.
• Objekte dagegen sind ein konkretes Exemplar (Instanz) einer Klasse. Daher meine alte
Karre ist ein konkretes Exemplar der Klasse Auto.
Abb. 11-3 : Klassen und Objekte am Beispiel von Autos
11.2 Klassen-Entwurf
Bevor sie eine eigene Klasse entwerfen, sollten sie sich immer einige Gedanken über den Sinn,
die Repräsentation und die Implementation der Klasse machen.
1. Überlegung - Art der Objekte:
• Was für Objekte soll die Klasse repräsentieren?
2. Überlegung - Schnittstelle:
• Was soll mit den Objekten der Klasse machbar sein?
• Wie sollen sich die Objekte verhalten?
• Welche Schnittstelle benötigt die Klasse dafür?
3. Überlegung - Vererbung:
• Gibt es Klassen, die eine vergleichbare Schnittstelle haben?
• Gibt es Klassen, die ähnliche Funktionalität aufweist?
• Gibt es Klassen, die diese Objekte in allgemeinerer Form repräsentieren?
• Kann eine abstrakte Schnittstelle konkrete Klassen verbergen?
Diese Frage können wir uns erst stellen, wenn wir Vererbung kennen – siehe Kapitel todo. Ich
habe sie hier aber aufgeführt, damit sie hier alle relevanten Fragen auf einen Schlag sehen.
4. Überlegung - Implementierung:
• Was soll mit den Objekten der Klasse machbar sein?
• Wie sollen sich die Objekte verhalten?
• Wie implementiere ich diese Funktionalitäten?
• Welchen inneren Aufbau wähle ich dafür?
Bemerkung – die zweite und vierte Überlegung beruhen auf den gleichen Fragen, aber einmal
aus der Sicht eines Benutzers der Klassen, und das andere Mal aus der Sicht des
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 212 / 409
Implementierers gestellt.
Bemerkung – in einem größeren Kontext betrachtet würde man die Überlegungen 1 bis 3 als
Analyse und die Überlegungen 2 bis 4 als Design betrachten.
11.3 Beispiel
11.3.1 Wurm-Programm
Vielleicht kennen sie kleine Gimmick-Programme, die z.B. gerne als Bildschirmschoner
eingesetzt werden. Hier konkret geht es um ein Programm, dass einen Wurm über den
Bildschirm krabbeln läßt. Dabei soll der Wurm, der den Bildschirm rechts verläßt links wieder
hinein krabbeln, und umgekehrt. Verläßt der Wurm den Bildschirm oben, so soll er unten
wieder hinein krabbeln, und umgekehrt. Für den Wurm bildet der Bildschirm also eine
unendliche Fläche, bei der die jeweils gegenüberliegenden Ränder aufeinander abgebildet
werden.
Da wir zur Zeit noch kein GUI programmieren können, und auch sonst noch ziemlich viel JavaWerkzeug fehlt, beschränken wir uns hier mit einem ganz kleinen und sehr stark abgespeckten
Wurm Programm:
• Statt mit einem GUI Fenster arbeiten wir nur mit der Konsole.
• Statt eines Wurms wird hier nur ein Zeichen gezeichnet – der Stern ‚*‘.
• Statt der Bildschirmbreite setzen wir eine feste Breite von 40 Zeichen an.
• Statt mit einer x/y-Koordinate wird hier nur x variiert.
• Statt den alten Wurm zu löschen, geben wir eine neue Zeile aus.
Übrig bleibt also nur ein Programm mit ungefähr folgender Ausgabe:
*
*
*
*
...
*
*
*
*
*
...
Alles in allem ist das neue Programm nicht mehr sehr eindrucksvoll, aber es hat noch
genügend Probleme für den Anfang:
• Um die Ausgabe einzurücken, definieren wir eine Hilfs-Funktion „spaces“, die einen
StringBuffer der entsprechenden Größe zurückgibt. Die Funktion ist mit ihrer Schleife nicht
besonders elegant, aber manchmal muss man so programmieren.
• Damit die Ausgabe nicht nur so dahin-fliegt, müssen wir das Programm verlangsamen. Nach
jeder Ausgabe legen wir es daher für 30 ms schlafen. Auch hierfür gibt es eine extra
Funktion „sleep“, die für uns den „Thread.sleep“ Aufruf kapselt, und den notwendigen
try/catch-Block übernimmt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 213 / 409
• Statt einem festen Programm-Ende oder z.B. einer Tastatur-Abfrage aufs Programm-Ende
arbeiten wir mit einer Endlos-Schleife. Das Programm muss daher auf der Konsole mit
„Strg+C“ oder in der IDE abgebrochen werden.
public class Appl {
public static StringBuffer spaces(int len) {
StringBuffer sb = new StringBuffer(len);
for (int i=0; i<len; i++) {
sb.append(' ');
}
return sb;
}
public static void sleep() {
try {
Thread.sleep(30);
} catch (Exception x) {
}
}
public static void main(String[] args) {
System.out.println("Ein Stern laeuft ueber den Bildschirm");
int pos = 0;
while (true) {
System.out.println(spaces(pos) + "*");
sleep();
++pos;
if (pos>40) {
pos=0;
}
}
}
}
Das eigentliche Programm reduziert sich so auf eine Initalisierung und eine Endlos-Schleife mit
6 Zeilen:
int pos = 0;
while (true) {
System.out.println(spaces(pos)+"*");
sleep();
++pos;
if (pos>40) {
pos=0;
}
}
// (*)
// (**)
// (***)
An welcher Stelle könnte dieses Programm durch eine weitere Abstraktion vereinfacht werden?
Immerhin die Hälfte des Programms beschäftigt sich mit dem Positions-Zähler „pos“. Statt
eines „int“ Elements wird hier ja ein sogenannter Ring-Zähler benötigt, und der größte Teil der
Programmlogik beschäftigt sich jetzt mit dem Problem einen Ring-Zähler zu simulieren.
Ausserdem ist nicht schön, dass sich die zwei wichtigsten Parameter des Ring-Zählers an
mehreren Stellen wiederfinden: Start- und End-Wert (*), (**) und (***).
11.3.2 Überlegung 1
Damit ist Überlegung 1 „Art der Objekte“ beantwortet: wir wollen einen Ring-Zähler
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 214 / 409
implementieren, d.h. eine Art Integer-Variable die bei Erreichen eines bestimmten Wertes
wieder auf einen Startwert springt.
Bemerkung – mancher wird nun denken: „Was soll das?“ Die Schleife ist doch übersichtlich,
das Problem klar, die Lösung einfach – wozu hier Aufwand investieren? Aber diese Einstellung
greift zu kurz, denn:
• Nicht immer sind alle Code-Vorkommen eines Ring-Zählers so klar und nah beieinander
lokalisiert. Nicht selten finden sich diese Stellen in realen Programmen verteilt in mehreren
Stellen wieder. Und in solchen Fällen ist dem Leser nicht immer klar, warum hier solcher
Code steht. Wir haben es hier halt mit einem sehr einfachen Beispiel-Programm zu tun.
• Und selbst wenn der Code und die Lösung einfach sind: man kann auch einfache Sachen
vergessen oder falsch machen.
• Ein fertiger Ring-Zähler – wir reden hier jetzt von der zweiten Verwendung der Klasse, nicht
von ihrer ersten – ist ein fertiger Ring-Zähler. Er ist getestet und im Einsatz und läuft – es ist
ein fertiger Ring-Zähler. Man kann ihn nehmen und benutzen. Das nennt man
Wiederverwendung und sollte ein Ziel jeder Software-Entwicklung sein.
• Und warum müssen Abstraktions-Ebenen einen großen Abstand haben, und die Einheiten
dazwischen kompliziert damit sie sich lohnen? Sind sie groß und kompliziert, so sind sie
wahrscheinlich auch fehlerhaft. Sind sie groß und kompliziert, so passen sie sicher nicht
beim nächsten Problem.
• Und bedenken sie – es ist unser erstes Beispiel. Das ändert nichts daran, dass ein RingZähler als wiederverwendbare Einheit (Klasse) Sinn macht. Aber es gibt sicher auch noch
komplexere Klassen.
11.3.3 Überlegung 2
Die Überlegung 2 zielt dann auf die Schnittstelle der Klasse:
• Was soll mit den Objekten der Klasse machbar sein?
• Wie sollen sich die Objekte verhalten?
• Welche Schnittstelle benötigt die Klasse dafür?
Hier ist die Lösung einfach:
• Bei der Erzeugung des Ring-Zählers müssen Start- und Grenzwert definiert werden können.
• Der aktuelle Wert muss abfragbar sein.
• Der Wert muss incrementiert werden können, wobei eine Überschreitung des Grenzwerts
implizit wieder zum Startwert führen muss.
Programm-technisch könnte dies aus der Sicht des Benutzers so aussehen:
RingCounter rc = new RingCounter(0, 40);
while (true) {
System.out.println(spaces(rc.get()) + "*");
sleep();
rc.add();
}
6 Zeilen statt 9, und außerdem noch der Vorteil eine if-Anweisung los geworden zu sein. Und
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 215 / 409
jede Kontrollstruktur bringt halt Komplexität und potentielle Fehlerquellen mit sich.
11.3.4 Überlegung 3
Da wir noch keine Vererbung kennen, ist diese Überlegung zur Zeit noch nicht relevant.
11.3.5 Überlegung 4
So bleibt noch die Frage nach der Implementierung, d.h. wie implementiere ich das?
11.4 Klassen-Implementierung
11.4.1 Definition
Zuerst muss eine Klasse definiert werden.
Syntax Kassen-Definition
[Modifizierer] class klassenname {
...
}
Achtung – eine Klassen-Definition hat immer ihre eigene Datei, die genauso wie die Klasse mit
der Extension ‘java’ heissen muss. Dies ist ein gern gemachter Anfänger-Fehler. Wenn sich
ihre Klasse also nicht compilieren lässt - überprüfen sie erstmal die Namen.
Die für Klassen möglichen Modifizierer werden später aufgeführt und erklärt. Im Augenblick
sollten sie hier immer nur ‘public’ verwenden.
Beispiele:
public class RingCounter {
...
}
public class Person {
...
}
Innerhalb einer Klasse können Konstruktoren, Element-Funktionen, Klassen-Funktionen,
Attribute, innere Klassen, und einige weitere Elemente vorhanden sein. Eine Klasse kann aber
auch einfach leer sein.
11.4.2 Benutzung
Ist eine Klasse definiert, so kann sie im gleichen Package einfach unter ihrem Typ-Namen
benutzt werden. Achtung – Variablen der Klasse sind immer Referenz-Variablen. Und erzeugt
werden Objekte der Klasse mit dem „new“ Operator.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 216 / 409
public class A {
}
public class Appl {
public static void main(String[] args) {
A a = new A();
f(a);
a = g();
}
public static void f(A a) {
}
public static A g() {
return new A();
}
}
11.4.3 Garbage Collector
Erzeugte Objekte müssen nicht explizit gelöscht werden. Dies macht die JVM automatisch.
Dafür gibt es im System den sogenannten Garbage-Collector, der automatisch im Hintergrund
alle nicht mehr referenzierten Objekte löscht. Den Vorgang des Einsammelns nicht mehr
benötigter Objekte nennt man Garbage-Collection. Je nach Implementierung in der JVM wird
diese von Zeit zu Zeit aufgerufen, oder läuft kontinuierlich im Hintergrund. Außerdem läßt sich
die GC auch aus dem Programm heraus mit „System.gc()“ aufrufen – angewendet wird dies in
Aufgabe 12.8.1.
11.4.4 Element-Funktionen
Leere Klassen sind meist ziemlich sinnlos. Um mit den Objekten der Klasse agieren zu können,
sollten diese Element-Funktionen haben.
Syntax Funktions-Definition
[Modifizierer] rueckgabetyp funktionsname(parameterliste) {
// Funktions-Rumpf
}
Gegenüber den bisher verwendeten Klassen-Funktionen unterscheiden sich ElementFunktionen durch den fehlenden Modifier „static“, und dass sie nur mit Objekt-Bezug
aufgerufen werden können.
public class A {
public static void f() {
}
public void g() {
}
public static void main(String[] args) {
f();
g();
// Okay, da Klassen-Funktion
// Compiler-Fehler, da Element-Funktion -> Objekt-Bezug
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 217 / 409
noetig
A a = new A();
a.g();
// Okay, g() wird fuer das Objekt a aufgerufen
}
}
Die für Funktionen möglichen Modifizierer werden später aufgeführt und erklärt. Im Augenblick
sollten sie bei Element-Funktionen immer nur ‘public’ verwenden, bei Klassen-Funktionen
„public“ und „static“.
Und wie sieht unser Ring-Zähler jetzt aus?
public class RingCounter {
public void add() {
...
}
// Die Implementierung ist noch offen
public int get() {
...
}
// Die Implementierung ist noch offen
}
11.4.5 Warum Element-Funktionen?
Weshalb gibt es überhaupt Element-Funktionen? Warum nimmt man nicht einfach KlassenFunktionen, und spart sich den Aufwand mit den Objekten?
Man kann beliebig viele Objekte einer Klasse erzeugen kann, die alle voneinander abhängig
sind. Element-Funktionen beziehen sich jeweils nur auf das Objekt, für das sie aufgerufen
werden, d.h. jedes Objekt kann (und hat) seinen eigenen von allen anderen unabhängigen
Status. Klassen-Funktionen beziehen sich auf die Klasse, für die es nur einen Status gibt. Ohne
Objekte und Element-Funktionen wäre sowas nicht möglich.
StringBuilder s1 = new StringBuilder("Hallo Welt");
StringBuilder s2 = new StringBuilder("Java");
System.out.println("Die Laenge von \"" + s1 + "\" ist " + s1.length());
System.out.println("Die Laenge von \"" + s2 + "\" ist " + s2.length());
„s1“ und „s2“ sind zwei unabhängige Objekte, die unterschiedliche Strings repräsentieren. Nur
Funktionen mit Objekt-Bezug können wissen, welches Objekt sie denn nun bearbeiten sollen.
Abb. 11-4 : Funktionen ohne bzw. mit Objekt-Bezug
11.4.6 Attribute
Natürlich hält ein Objekt Daten. Der Ring-Zähler z.B. muss sowohl seinen Start- und Endwert
kennen, als auch seinen aktuellen Wert. Objekt-spezifische Daten – d.h. Variablen in Objekten
– werden Attribute genannt. Element-Funktionen können einfach unter Verwendung des
Attribut-Namens auf die Attribute zugreifen und sie benutzen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 218 / 409
Achtung – in Java wird auch gerne der historisch begründetet Name „Feld“ statt „Attribute“
verwandt.
Syntax Attribut-Definitionen
[Modifizierer] typ name;
Beispiel
public class A {
private int i;
private String s;
public void set(int arg1, String arg2) {
i = arg1;
s = arg2;
}
public void print() {
System.out.println(i + " => " + s);
}
}
public class Appl {
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
a1.set(1, "Hallo");
a2.set(2, "Java");
a1.print();
a2.print();
}
}
Ausgabe
1 => Hallo
2 => Java
Die für Attribute möglichen Modifizierer werden später aufgeführt und erklärt. Im Augenblick
sollten sie bei Attributen immer nur ‘private’ verwenden.
Und wie sieht unser Ring-Zähler jetzt aus?
public class RingCounter {
private int start;
private int limit;
private int value;
public void add() {
if (++value>limit) {
value=start;
}
}
public int get() {
return value;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 219 / 409
}
}
Jetzt bleibt nur noch die Frage offen, wie denn die Attribute sinnvoll initalisiert werden, d.h. mit
den gewünschten Start- und End-Werten? Zur Zeit werden sie als „int“s alle mit „0“ initialisiert.
Hinweis – Attribute werden auch oft Member, Attributes, Element-Variablen, Objekt-Variablen
oder (speziell in Java) auch Felder genannt.
11.4.7 Konstruktoren
Um ein Objekt bei der Erzeugung sauber zu initialisieren, gibt es spezielle Methoden - die
Konstruktoren. Sie haben keinen Rückgabetyp und heissen immer wie die Klasse. Sie werden
automatisch immer beim Erzeugen eines neuen Objektes aufgerufen.
public class Person {
private String forename;
private String surename;
private int age;
public Person(String fn, String sn, int a) {
forename = fn;
surename = sn;
age = a;
}
public void print() {
System.out.println("Name: " + forename + " " + surename);
System.out.println("Alter: " + age);
}
}
Person p = new Person("Detlef", "Wilkening", 16);
p.print();
// Das Alter stimmt nicht
Ausgabe
Name: Detlef Wilkening
Alter: 16
Nur wenn sie keinen Konstruktor definieren, erzeugt der Compiler automatisch einen
sogenannten Standard-Konstruktor, der ohne Argumente aufgerufen werden kann. Der
Konstruktor wird entsprechend denen bei new angegebenen Argumenten ausgewählt.
public class A {
}
Person p = new Person();
nicht
A a = new A();
Konstruktor
// Compiler-Fehler, dieser Konstruktor existiert hier
// Klasse A hat einen automatisch erzeugten Standard-
Beispiele etwas aufbohren – todo…
Und wie sieht unser Ring-Zähler jetzt aus?
public class RingCounter {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 220 / 409
private int start;
private int limit;
private int value;
public RingCounter(int s, int l) {
start = s;
limit = l;
value = s;
}
public void add() {
if (++value>limit) {
value=start;
}
}
public int get() {
return value;
}
}
Nun ist die Klasse vollständig und unser Beispiel funktioniert.
Bemerkung – die Ring-Zähler Klasse ist in diesem Zustand natürlich nicht wirklich in einem
produktiven Zustand. Z.B. kann man sie mit einem End-Wert, der kleiner als der Start-Wert ist,
problemlos aushebeln.
11.5 Aufgaben
11.5.1 Aufgabe „Ringzähler“
Schreiben Sie einen Ringzähler. Ein Ringzähler ist ein Zähler, der von einem Startwert an hoch
(oder runter) zählt (Delta muss nicht 1 sein) und nach Überschreiten des Endwertes wieder
beim Startwert anfängt. Ein solcher Zähler wird in der Praxis häufig gebraucht, z.B. als Index in
ein Array, als Verweis auf ein Spielfeld, usw. Wie gehen Sie mit widersprüchlichen Eingaben
um?
Beim Erzeugen eines Ringzählers sollte der Nutzer folgende Möglichkeiten haben:
• Angabe nur vom End-Wert => Start-Wert ist 0, Delta ist 1
• Angabe von Start- und End-Wert => Delta ist 1
• Angabe von Start- und End-Wert und Delta
Der Ringzähler sollte z.B. mit folgenden Dingen umgehen können – denken Sie sich eine gute
Fehlerbehandlung im Rahmen ihrer Möglichkeiten für problematische Fälle aus:
• End-Wert vor Start-Wert, Delta positiv
• End-Wert nach Start-Wert, Delta negativ
• Delta ist 0
Lösung siehe Kapitel 11.6.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 221 / 409
11.5.2 Aufgabe „Kontaktdaten 1“
Schreiben Sie eine kleine Kontaktdaten-Verwaltung. Folgende Fähigkeiten soll das Programm
in der ersten Version haben:
• Menü für folgende Aufgaben:
- Eingabe neuer Personen
- Ausgabe aller Personen – unsortiert
- Ausgabe aller Personen, deren Vor- oder Nach-Name mit einem bestimmten StringMuster anfängt (Groß- und Kleinschreibung wird nicht unterschieden).
• Das Menü soll der Benutzer durch die Eingabe einzelner Buchstaben (mit Return) anwählen
können.
Personen bestehen aus:
• Vorname
• Nachname
• Telefon (als String)
Mögliche Ein- und Ausgabe:
Bitte
- l :
- n :
- s :
- e :
> l
5
-
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Kontakte:
Detlef Wilkening - Tel: 1234
Dieter Schmidt
Dietmar Mueller - Tel: 456
Edgar Wilkens
Ralf Eberleh - Tel: 555 444
Bitte
- l :
- n :
- s :
- e :
> n
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Geben Sie eine neue Person ein
Vorname: Martina
Nachname: Winkens
Telefon: 98 76 54
Bitte
- l :
- n :
- s :
- e :
> l
6
-
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Kontakte:
Detlef Wilkening - Tel: 1234
Dieter Schmidt
Dietmar Mueller - Tel: 456
Edgar Wilkens
Ralf Eberleh - Tel: 555 444
Martina Winkens - Tel: 98 76 54
Bitte waehlen Sie eine Aktion aus
- l : Liste
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
>
Seite 222 / 409
n : Neu
s : Suchen
e : Ende
s
Geben Sie einen Suchstring ein: e
- Edgar Wilkens
- Ralf Eberleh - Tel: 555 444
Bitte
- l :
- n :
- s :
- e :
> s
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Geben Sie einen Suchstring ein: de
- Detlef Wilkening - Tel: 1234
Bitte
- l :
- n :
- s :
- e :
> s
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Geben Sie einen Suchstring ein: x
- keinen passenden Eintrag gefunden
Bitte
- l :
- n :
- s :
- e :
> e
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Ende
Schauen Sie sich bei der Lösung ruhig noch mal Aufgabe „Telefonbuch“ (Kapitel 9.8.6) an.
Aber Achtung – viele Dinge sind hier anders:
• Es gibt ein Menü
• Die Liste aller Personen ist hier unsortiert
• Personen können eingegeben werden
• Das Suchen funktioniert auch mit Anfangs-Mustern ohne Berücksichtigung von Groß- und
Klein-Schreibung.
• Und vor allem: jetzt kennen sie Klassen. Und da Sie ja wissen, dass wir die Kontaktdaten
Verwaltung später noch aufbohren wollen – sollten Sie sie sauber strukturieren.
Lösung siehe Kapitel 11.7.
11.5.3 Aufgabe „Kontaktdaten 2“
Und damit wir gleich sehen, ob Sie Ihr Programm sauber strukturiert haben, bohren wir es
direkt auf: Eine Person soll neben Vor-, Nachname und Telefon auch noch eine Beschreibung
(String) enthalten.
Mögliche Ein- und Ausgabe:
Bitte waehlen Sie eine Aktion aus
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
>
l
n
s
e
n
:
:
:
:
Seite 223 / 409
Liste
Neu
Suchen
Ende
Geben Sie eine neue Person ein
Vorname: Detlef
Nachname: Wilkening
Telefon: 1234
Beschreibung: Java Dozent
Bitte
- l :
- n :
- s :
- e :
> n
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Geben Sie eine neue Person ein
Vorname: Bernd
Nachname: Mustermann
Telefon:
Beschreibung:
Bitte
- l :
- n :
- s :
- e :
> l
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
2 Kontakte:
- Detlef Wilkening - Tel: 1234 => Java Dozent
- Bernd Mustermann
Bitte
- l :
- n :
- s :
- e :
> e
waehlen Sie eine Aktion aus
Liste
Neu
Suchen
Ende
Ende
Wenn Sie ihr Programm gut designt haben, dann sollte dies mit sehr wenig Änderungen am
Code machbar sein. Und die Änderungen sollten auch nur sehr lokal sein.
Lösung siehe Kapitel 11.8.
11.6 Lsg. zu Aufgabe „Ringzähler“ – Kap. 11.5.1
Erklärung folgt (hoffentlich) später – todo...
Achtung – ich benutze hier schon das Sprachmittel „this“ aus dem Kapitel 12.4.3.
public class RingCounter {
private
private
private
private
int
int
int
int
start;
limit;
delta;
value;
public RingCounter(int limit) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 224 / 409
this(0, limit, 1);
}
public RingCounter(int start, int limit) {
this(start, limit, 1);
}
public RingCounter(int start, int limit, int delta) {
if (delta==0) {
delta = 1;
} else if (delta<0) {
delta = -delta;
}
this.start = start;
this.limit = limit;
this.delta = start<=limit ? delta : -delta;
this.value = start;
}
public int next() {
value += delta;
if ((value>limit && delta>0) || (value<limit && delta<0)) {
value = start;
}
return value;
}
public int get() {
return value;
}
}
public class Appl {
public static void main(String[] args) {
System.out.println("RingCounter: 0..4 / 1");
RingCounter rc = new RingCounter(4);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 2..6 / 1");
rc = new RingCounter(2, 6);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 2..-2 / 1");
rc = new RingCounter(2, -2);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 1..9 / 0");
rc = new RingCounter(1, 9, 0);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 4..40 / 7");
rc = new RingCounter(4, 40, 7);
System.out.print(rc.get() + " ");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 225 / 409
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 20..-5 / 3");
rc = new RingCounter(20, -5, 3);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println('\n');
System.out.println("RingCounter: 0..100 / -18");
rc = new RingCounter(0, 100, -18);
System.out.print(rc.get() + " ");
for (int i=0; i<20; i++) {
System.out.print(rc.next() + " ");
}
System.out.println();
}
}
Ausgabe
RingCounter: 0..4 / 1
0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0
RingCounter: 2..6 / 1
2 3 4 5 6 2 3 4 5 6 2 3 4 5 6 2 3 4 5 6 2
RingCounter: 2..-2 / 1
2 1 0 -1 -2 2 1 0 -1 -2 2 1 0 -1 -2 2 1 0 -1 -2 2
RingCounter: 1..9 / 0
1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3
RingCounter: 4..40 / 7
4 11 18 25 32 39 4 11 18 25 32 39 4 11 18 25 32 39 4 11 18
RingCounter: 20..-5 / 3
20 17 14 11 8 5 2 -1 -4 20 17 14 11 8 5 2 -1 -4 20 17 14
RingCounter: 0..100 / -18
0 18 36 54 72 90 0 18 36 54 72 90 0 18 36 54 72 90 0 18 36
11.7 Lsg. zu Aufgabe „Kontaktdaten 1“ – Kap. 11.5.2
Erklärung folgt (hoffentlich) später – todo...
Achtung – ich benutze hier schon das Sprachmittel „Klassen-Funktionen“ in einer Form, wie wir
sie hier noch nicht kennen (siehe Kapitel 12.4.4) und auch die direkte Attribut-Initialisierung, wie
sie erst in Kapitel 12.2.1 vorgestellt wird.
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Keyboard {
private static InputStreamReader isr = new InputStreamReader(System.in);
private static BufferedReader reader = new BufferedReader(isr);
public static String readString() {
try {
return reader.readLine();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 226 / 409
} catch (Exception x) {
}
return "";
}
}
public class Person {
private String forename = "";
private String surename = "";
private String phone = "";
public boolean find(String pattern) {
int len = pattern.length();
if (forename.length()>=len) {
String begin = forename.substring(0, len);
if (begin.equalsIgnoreCase(pattern)) {
return true;
}
}
if (surename.length()>=len) {
String begin = surename.substring(0, len);
if (begin.equalsIgnoreCase(pattern)) {
return true;
}
}
return false;
}
public void input() {
System.out.print("Vorname: ");
forename = Keyboard.readString();
System.out.print("Nachname: ");
surename = Keyboard.readString();
System.out.print("Telefon: ");
phone = Keyboard.readString();
}
public void print() {
System.out.print("- " + forename + " " + surename);
if (phone.length()!=0) {
System.out.print(" - Tel: " + phone);
}
System.out.println();
}
}
import java.util.ArrayList;
import java.util.Iterator;
public class Contacts {
private ArrayList<Person> contacts = new ArrayList<>();
public int size() {
return contacts.size();
}
public void add(Person p) {
contacts.add(p);
}
public int find(String pattern) {
int count = 0;
Iterator<Person> i = contacts.iterator();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 227 / 409
while (i.hasNext()) {
Person p = i.next();
if (p.find(pattern)) {
p.print();
count++;
}
}
return count;
}
public void print() {
Iterator<Person> i = contacts.iterator();
while (i.hasNext()) {
i.next().print();
}
}
}
public class Appl {
private Contacts cts = new Contacts();
public void run() {
while (true) {
System.out.println("Bitte waehlen Sie eine Aktion aus");
System.out.println("- l : Liste");
System.out.println("- n : Neu");
System.out.println("- s : Suchen");
System.out.println("- e : Ende");
System.out.print("> ");
String in = Keyboard.readString();
System.out.println();
if (in.equalsIgnoreCase("l")) {
print();
} else if (in.equalsIgnoreCase("n")) {
insert();
} else if (in.equalsIgnoreCase("s")) {
find();
} else if (in.equalsIgnoreCase("e")) {
System.out.println("Ende");
return;
}
System.out.println();
}
}
private void print() {
System.out.println(cts.size() + " Kontakte:");
cts.print();
}
private void insert() {
System.out.println("Geben Sie eine neue Person ein");
Person p = new Person();
p.input();
cts.add(p);
}
private void find() {
System.out.print("Geben Sie einen Suchstring ein: ");
String pattern = Keyboard.readString();
int count = cts.find(pattern);
if (count==0) {
System.out.println("- keinen passenden Eintrag gefunden");
}
}
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 228 / 409
}
}
11.8 Lsg. zu Aufgabe „Kontaktdaten 2“ – Kap. 11.5.3
Die neue Kontaktdaten-Verwaltung unterscheidet sich durch die alte nur durch ein weiters
Merkmal der zu verwaltenen Personen. Da alle personen-relevanten Elemente in der PersonenKlasse gekapselt sind, ist nur diese Klasse betroffen. Das komplette restliche Programm ist von
der Änderung nicht betroffen. Daher wird hier auch nur die neue Personen-Klasse besprochen
und gelistet.
Was muß an der Personen-Klasse geändert werden?
• Sie benötigt ein weiteres String-Attribut für die Beschreibung.
• Die Beschreibung muss bei der Personen-Eingabe mit von der Tastatur gelesen werden –
siehe Element-Funktion „input“.
• Und die Beschreibung muß – wenn vorhanden – mit ausgegeben werden – siehe ElementFunktion „print“.
• Das Suchen wird von der Beschreibung nicht beeinflußt – also muß die Element-Funktion
„find“ nicht angepaßt zu werden.
Fertig – das war’s.
public class Person {
private
private
private
private
String
String
String
String
forename = "";
surename = "";
phone = "";
description = "";
public boolean find(String pattern) {
int len = pattern.length();
if (forename.length()>=len) {
String begin = forename.substring(0, len);
if (begin.equalsIgnoreCase(pattern)) {
return true;
}
}
if (surename.length()>=len) {
String begin = surename.substring(0, len);
if (begin.equalsIgnoreCase(pattern)) {
return true;
}
}
return false;
}
public void input() {
System.out.print("Vorname: ");
forename = Keyboard.readString();
System.out.print("Nachname: ");
surename = Keyboard.readString();
System.out.print("Telefon: ");
phone = Keyboard.readString();
System.out.print("Beschreibung: ");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 229 / 409
description = Keyboard.readString();
}
public void print() {
System.out.print("- " + forename + " " + surename);
if (phone.length()!=0) {
System.out.print(" - Tel: " + phone);
}
if (description.length()!=0) {
System.out.print(" => " + description);
}
System.out.println();
}
}
12 Klassen-Details
12.1 Zugriffsbereiche
Java kennt 4 Zugriffs-Modifizierer. Für alle Elemente in einer Klasse haben sie folgende
Bedeutung:
• public
Auf diese Elemente darf jeder von überall her zugreifen.
• protected
Auf diese Elemente darf von innerhalb des gleichen Package und von den abgeleiteten
Klassen her zugegriffen werden. Packages werden in Kapitel 13 eingeführt, Vererbung in
Kapitel 14. D.h. ist dieser Modifizierer für uns zur Zeit ohne Bedeutung.
• --- (kein Zugriffs-Modifizierer entspricht einer Package Sichtbarkeit)
Auf diese Elemente darf von innerhalb des gleichen Package her zugegriffen werden.
Packages werden in Kapitel 13 eingeführt. D.h. ist dieser Modifizierer für uns zur Zeit noch
ohne Bedeutung.
• private
Auf diese Elemente darf nur von innerhalb der Klasse zugegriffen werden (und von inneren
Klassen – siehe Kapitel 15).
public class A {
private void f();
public void g() {
f();
}
// Kein Problem, da innerhalb der Klasse
}
public class B {
public void f(A a) {
a.f();
a.g();
}
// Compiler-Fehler, da private
// Okay, da public
}
Wichtige Bemerkung – es ist sehr sehr sehr empfehlenswert alle Attribute private zu machen,
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 230 / 409
und einen Zugriff von außen – wenn er denn überhaupt notwendig ist – über Funktionen zu
regeln. Dies ist einer der wichtigsten Grundprinzipien jeglicher Software-Entwicklung und der
Objektorientierung: „Information-Hiding“:
• Die interne Implementierung ist vollständig vor dem Benutzer versteckt.
• Die interne Implementierung läßt sich jederzeit ändern.
• Von außen können keine inkonsistenten Objekt-Zustände erzeugt werden.
12.2 Konstruktoren
12.2.1 Initialisierung
Wird ein Objekt erzeugt, so gibt es mehrere Möglichkeiten die Attribute zu initialisieren.
• Man macht nichts: in diesem Fall werden die entsprechenden Attribute entsprechend ihrem
Typ initialisiert.
• Man kann den gewünschten Wert direkt bei der Attribut-Definition angeben.
• Eine Klasse kann Initialisierungs-Blöcke enthalten – dies wird in der Vorlesung nicht
besprochen.
• Oder das Attribut wird im Konstruktor initialisiert.
public class A {
private int i1;
private int i2 = 42;
private int i3;
Ko.
private
private
fuer s2
private
private
Ko.
// Default-Wert 0
// Initial-Wert bei der Attribut-Definition
// Sieht nach Default-Wert aus, Initialisierung dann im
String s1;
String s2 = "Hallo";
// Default-Wert null
// Initial-Wert String "Hallo"
String s3 = new String("Hallo");
String s4;
// Langfassung von s2
// Sieht nach null aus, aber dann
public A(int arg1, String arg2) {
i3 = arg1;
s4 = arg2;
}
}
• Direkte Initialisierungen der Attribute ist vor allem dann sinnvoll, wenn eine Klasse mehrere
Konstuktoren hat, und die Attribute in allen Konstruktoren die gleichen Werte bekommen.
• Das Initialisieren der Attribute in den Konstruktoren ist vor allem dann sinnvoll, wenn die
gesetzten Werte konstruktor-spezifisch sind, d.h. z.B. von den Konstruktor-Parametern
abhängen.
• Das Initialisieren der Attribute in den Initialisierungs-Blöcken – was hier nicht besprochen
wird – ist vor allem dann sinnvoll, wenn das Setzen in allen Konstruktoren identisch, aber zu
komplex für eine direkte Initialisierung ist.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 231 / 409
12.2.2 Konstruktor-Verkettung
Wenn eine Klasse mehrere Konstruktoren hat, so können diese oft aufeinander zurückgeführt
werden, um das Programmieren zu vereinfachen.
public class Person {
private String forename;
private String surname;
private int age;
public Person(String fn, String sn) {
forename = fn;
surname = sn;
}
public Person(String fn, String sn, int a) {
this(fn, sn);
age = a;
}
}
Der Aufruf eines anderen Konstruktors der gleichen Klasse geschieht über den „this“ Zeiger mit
den entsprechenden Parametern und muss in der ersten Zeile des Konstruktors stehen.
12.3 finalize
Analog zu den Konstruktoren gibt es in Java auch eine Klassen-Methode, die beim Zerstören
eines Objektes aufgerufen wird - sie heisst finalize
public class A {
protected void finalize() {
}
}
Achtung – es ist vollkommen undefiniert wann die Funktion „finalize“ aufgerufen wird. Es kann
sogar passieren, dass sie gar nicht aufgerufen wird:
• Zwischen „nicht-mehr-referenziert-werden“ und dem Ende des Programms wird keine GC
mehr ausgelöst.
• Die Verkettung in der Vererbungs-Hierarchie ist unterbrochen – siehe Kapitel todo.
U.a. deshalb wird in der Praxis von der Verwendung der Finalize-Funktion abgeraten. Statt
dessen sollen lieber Weak- oder Phantom-Referenzen benutzt werden – die zu besprechen
sprengt aber mal wieder den Zeit-Rahmen der Vorlesung.
Mit einem kleinen Beispiel-Programm kann man den Aufruf der Funktion gut nachverfolgen:
public class Test {
public Test() {
System.out.println("- Konstruktor");
}
protected void finalize() {
System.out.println("- finalize");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 232 / 409
}
}
System.out.println("Objekt wird erzeugt");
Test t = new Test();
System.out.println("Referenz auf das Objekt wird freigeben");
t = null;
System.out.println("GC wird ausgeloest");
System.gc();
System.out.println("GC wird ausgeloest");
System.gc();
System.out.println("GC wird ausgeloest");
Mögliche Ausgabe
Objekt wird erzeugt
- Konstruktor
Referenz auf das Objekt wird freigeben
GC wird ausgeloest
- finalize
GC wird ausgeloest
GC wird ausgeloest
Die Ausgabe kann bei jeder virtuellen Java-Maschine anders aussehen, da das genaue
Verhalten bei einer GC der JVM freigestellt ist. Manche Implementierungen z.B. geben ein nicht
mehr referenziertes Objekt erst nach der zweiten GC frei, während sie die erste GC nur als
Markierungsphase benutzen.
12.4 Funktionen
12.4.1 Parameter und Rückgaben
Die integrierten Datentypen wie int, double, char,... werden per Wert übergeben („call-by-value“
– cbv) – sie stellen also eine Kopie da - Änderungen in der Funktion verändern den Wert in der
aufrufenden Funktion nicht.
Bei allen anderen Parametern, die ja alle Referenz-Typen sind, werden die Argumente per
Referenz übergeben („call-by-reference“ – cbr). Bei diesen referenzieren die Parameter das
gleiche Objekt wie in der aufrufenden Funktion – d.h. es kann in der Funktion das OriginalObjekt verändert werden.
public class A {
private static void f(int i, StringBuilder s) {
System.out.println("> f(" + i + ", " + s + ")");
i = 12;
s.append(" Welt");
System.out.println("> i: " + i);
System.out.println("> s: " + s);
}
public static void main(String[] args) {
int i = 42;
StringBuilder s = new StringBuilder("Hallo");
f(i, s);
System.out.println("# i: " + i);
System.out.println("# s: " + s);
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 233 / 409
}
Ausgabe
> f(42, Hallo)
> i: 12
> s: Hallo Welt
# i: 42
# s: Hallo Welt
Der Rückgabe-Typ void bei Funktionen gibt an, dass die Funktion nichts zurückgibt.
12.4.2 return
Ist eine Funktion nicht vom Typ void, so muss sie einen zu dem Typ passenden Wert
zurückgeben. Dies geschieht mit der return Anweisung. Die Funktion wird instantan beendet.
public class Person
{
private int age;
public int getAge() {
return alter;
}
}
• In einer Funktion können beliebig viele return Anweisungen vorkommen.
• Der Rückgabewert kann beim Funktions-Aufruf ignoriert werden, d.h. er muss nicht
verwendet oder ausgewertet werden.
12.4.3 this
Innerhalb jeder Element-Funktion ist automatisch das Schlüsselwort „this“ definiert, das eine
Referenz auf das aktuelle Objekt ist, d.h. das für das die Element-Funktion aufgerufen wurde.
Eine Referenz auf das aktuelle Objekt – aus Sicht der Element-Funktion auf sich selber – wird
in der Praxis häufig benötigt, z.B. um „sich-selber“ an andere Element-Funktionen zu
übergeben. Aber auch für profanere Aufgaben wie z.B. zur Aufruf-Verkettung (siehe Beispiel)
wird „this“ eingesetzt.
public class A {
public A myself() {
return this;
}
public void f() {
}
}
A a = new A();
a.myself().f();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 234 / 409
Ein ganz typischer Anwendungsfall in Java für „this“ ist die Verwendung gleicher Namen für
Attribute (d.h. Variablen im Objekt) und Parameter. In einem solchen Fall verdecken die
Parameter die Attribute, da sie im Scope der Funktion liegen, während die Attribute zum Scope
der Klasse gehören. Über das Schlüsselwort „this“ – d.h. über das aktuelle Objekt – können die
Attribute trotz gleich-namiger lokaler Variablen angesprochen werden.
public class RingCounter {
private
private
private
private
int
int
int
int
start;
limit;
delta;
value;
public RingCounter(int start, int limit, int delta) {
this.start = start;
this.limit = limit;
this.delta = delta;
this.value = start;
}
}
Achtung – in Klassen-Funktionen, d.h. Funktionen mit dem Modifierer static, ist „this“ nicht
definiert, da Klassen-Funktionen keinen Objekt-Bezug haben.
12.4.4 Klassen-Funktionen
Klassen-Funktionen – d.h. Funktionen mit dem Modifier „static“ – werden ohne Objekt-Bezug
aufgerufen. Klassen-Funktionen von fremden Klassen müssen mit dem Klassen-Namen
referenziert werden.
public class A {
public static void f() {
System.out.println("A.f");
}
}
public class B {
public static void f() {
System.out.println("B.f");
}
public static void main(String[] args) {
f();
B.f();
A.f();
}
}
Ausgabe
B.f
B.f
A.f
Im Gegensatz zu einer Element-Funktion ist eine Klassen-Funktion nicht einem Objekt,
sondern einer Klasse zugeordnet. Daher können Klassen-Funktionen natürlich nicht auf
Attribute zugreifen, sondern nur auf Klassen-Variablen – siehe Kapitel 12.5.1.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 235 / 409
12.5 Attribute
12.5.1 Klassen-Variablen
So wie es Element- und Klassen-Funktionen gibt, so gibt es auch Element- und KlassenVariablen. Gegenüber Attributen haben Klassen-Variablen zusätzlich den Modifier „static“.
public class A {
private static int i;
private static String s = "Hallo";
}
Im Gegensatz zu einem Attribut ist eine Klassen-Variable nicht einem Objekt, sondern einer
Klasse zugeordnet. Klassen-Variablen können von Element- und von Klassen-Funktionen
genutzt werden.
Klassen-Variablen werden initialisiert, wenn die Klasse in die JVM geladen wird. Für die
Definition der Initialisierung gibt es drei Arten:
• Man macht nichts: in diesem Fall werden die entsprechenden Klassen-Variablen
entsprechend ihrem Typ initialisiert.
• Man kann den gewünschten Wert direkt bei der Klassen-Variablen -Definition angeben.
• Eine Klasse kann statische Initialisierungs-Blöcke enthalten – dies wird in der Vorlesung
nicht besprochen.
12.5.2 Klassen-Konstanten
An vielen Stellen sind beim Programmieren Konstanten – gerade Integer Konstanten – sehr
hilfreich. Eine Konstante ermöglicht es ihnen statt eines Literals50 einen sprechenden Namen zu
benutzen, und den Wert der Konstanten an einer einzigen Stelle zu definieren, egal wie oft und
wo sie die Konstante benutzen.
// Bsp ohne Konstante
double bruttoPrice = 1.19 * nettoPrice;
// (*)
...
System.out.println(value/1.19);
// (**)
// Bsp mit Konstante – Achtung, nur Pseudocode
MWST = 1.19
double bruttoPrice = MWST * nettoPrice;
...
System.out.println(value/ MWST);
50
Literal – siehe auch Kapitel todo.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 236 / 409
Während in Zeile (*) noch recht offensichtlich ist, dass das Literal wohl die Mehrwert- oder
Umsatz-Steuer meint, ist dies in Zeile (**) nur noch durch den Kontext erkennbar. Statt dessen
ist dies im Beispiel mit Konstante in beiden Fällen sofort klar.
Ändert sich die Umsatz-Steuer, so müssen Sie im ersten Beispiel alle Stellen im
Programmcode finden, die ein Literal enthalten, und sie ändern. Und hoffentlich vergessen sie
keine Stelle bzw. ändern keine „19“ um, die nichts mit der Umsatz-Steuer zutun hat. Im zweiten
Beispiel müssen Sie nur die eine Konstanten-Definition ändern, was viel weniger Arbeit ist und
auch weniger fehlerträchtig.
Ein Konstante ist in Java eine normale Element- oder Klassen-Variable, die den Modifier „final“
bekommt. Damit läßt sich die entsprechende Variable nicht mehr ändern – siehe auch Kapitel
8.3.1.
public class A {
public static final int CONST_VAR = 42;
}
Bemerkung – Konstanten sind in Java typischerweise Klassen-Konstanten, d.h. konstante
Klassen-Variablen, da die Konstante normalerweise ja für alle Objekte gleich ist. Also mit
Modifier „static“.
Bemerkung – Klassen-Konstanten sind, obwohl Variablen, häufig „public“ oder „protected“, da
sie meist von überall genutzt werden können sollen. Da sie konstant sind, d.h. nicht geändert
werden können, ist der unbeschränkte Zugriff ja auch kein Problem.
Bemerkung – der Modifier „final“ wirkt bei Referenz-Variablen nur auf die Variable, und nicht
auf das referenzierte Objekt.
Bemerkung – Klassen-Konstanten werden in Java per Konvention in Großbuchstaben
geschrieben, und die einzelnen Wörter durch Underscores getrennt – siehe auch Kapitel 3.2.5.
12.6 Aufzählungen
Aufzählungen werden benötigt, wenn eine Variable nur wenige feste Werte annehmen kann,
die alle zur Entwicklungs-Zeit schon feststehen. Beispiele hierfür sind:
• Ausrichtung von Text in einem Absatz:
- Linksbündig, zentriert, rechtsbündig, blocksatz
• Spielfarben in einem Kartenspiel:
- Karo, Herz, Pik, Kreuz
• Belegung eines Feldes beim Tic-Tac-Toe:
- Unbelegt, weiß, schwarz
• Auflistung der 16 HTML Grund-Farben:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
-
Seite 237 / 409
Schwarz, weiß, rot, grün,…
In einem solchen Fall wünscht man sich als Programmierer einen speziellen Typ, der nur die
jeweiligen festen Werte aufnehmen kann – eine Aufzählung oder Enumeration, die es seit dem
JDK 1.5 in Java gibt.
• Intern werden Enums in Java mit Klassen definiert, d.h. Enums sind eigentlich nur Klassen.
Daher bestimmt wie bei Klassen auch der Enum-Name den Datei-Namen, und in der Datei
steht nur die Enumeration.
• Die Enumeration wird mit dem Schlüsselwort „enum“ eingeleitet, und ist meistens „public“.
• Im einfachsten Fall besteht der Enum nur aus den Aufzählungs-Konstanten, die vergleichbar
zu Klassen-Konstanten sind. Als solche werden sie natürlich groß geschrieben.
• Der Enum (im Beispiel „Color“) kann wie eine Klasse genutzt werden – d.h. z.B. für
Variablen-Definitionen oder in Funktions-Schnittstellen – natürlich mit Referenz-Semantik.
• Java sorgt dafür, dass nur die Aufzählungs-Konstanten in der Enum-Definition existieren.
Man kann im Programm keine weiteren Aufzählungs-Konstanten anlegen. Daher funktioniert
der Vergleich mit „==“ (Identität), und man muß nicht „equals“ benutzen.
public enum Color {
RED,
GREEN,
BLUE
}
public class A {
public static void checkColor(Color c) {
if (c == Color.RED) {
System.out.println("Farbe ist rot");
} else {
System.out.println("Farbe ist nicht rot");
}
}
public static void main(String[] args) {
Color co = Color.GREEN;
checkColor(co);
checkColor(Color.RED);
}
}
Ausgabe
Farbe ist nicht rot
Farbe ist rot
Ein typisches Einsatzgebiet von Enums sind Switch-Anweisungen, in denen sie genutzt werden
können (siehe auch Kapitel 7.2).
Color c = Color.GREEN;
switch (c) {
case RED:
System.out.println("Farbe ist rot");
break;
case GREEN:
System.out.println("Farbe ist gruen");
break;
case BLUE:
System.out.println("Farbe ist blau");
break;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 238 / 409
Ausgabe
Farbe ist gruen
12.6.1 Enums in Klassen
Selbst wenn wir innere Klassen noch nicht kennen (siehe Kapitel 15) – Enums können auch
innerhalb von Klassen als eine Art klassenbezogene Aufzählung definiert werden. Das
Schlüsselwort „static“ ist hierbei optional.
public class A {
public static enum Color1 { RED, GREEN, BLUE };
public enum Color2 { RED, GREEN, BLUE };
}
12.6.2 Enums sind Klassen
Enums sind in Wirklichkeit echte Klassen, und können als solche auch definiert und genutzt
werden. Daher man kann die Enum-Konstanten mit weiteren Informationen (Attributen) und
Element-Funktionen anreichern:
public enum Color {
RED(255,0,0), GREEN(0,255,0), BLUE(0,0,255);
private int r,g,b;
private Color(int r, int g, int b) {
this.r = r;
this.g = g;
this.b = b;
}
public int getRedValue() {
return r;
}
}
Achtung – Konstruktoren von Enumerationen müssen „private“ sein, da nur die Klasse die
Enumerations-Konstanten erzeugen darf.
12.6.3 Weiteres
• Im JDK wurden außerdem noch statische Imports eingeführt, die man u.a. mit Enums nutzen
kann. Aus Zeitmangel werden wir keine statischen Imports besprechen.
• Enums bringen von sich aus einige Funktionen mit sich, wie z.B. die Klassen-Funktionen:
- „values“, die ein Array aller Enum-Konstanten zurückgibt.
- „valueOf“, die die Enum Konstante zum übergebenen String zurückgibt.
for (Color c : Color.values()) {
System.out.println(c);
}
Color c = Color.valueOf("RED");
System.out.println("-> " + c);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 239 / 409
Ausgabe
RED
GREEN
BLUE
-> RED
12.6.4 Historie
Erst mit Java 5.0 (JDK 1.5) sind Aufzählungs-Typen in Java eingeführt worden. Bis dahin
wurden meist Integer-Konstanten für Aufzählungen verwandt, typischerweise so:
public class Text {
public
public
public
public
static
static
static
static
final
final
final
final
int
int
int
int
LEFT = 1;
CENTER = 2;
RIGHT = 3;
BLOCK = 4;
public void setAlignment(int arg) {
...
}
}
Obwohl diese Lösung mit Integer-Konstanten alles andere als gut ist (fehlende Typsicherheit),
sollten Sie sie kennen, denn sie findet sich in vielen altem Code und vor allem auch in den
Schnittstellen der Java Bibliothek, denn diese wurde zum größten Teil schon in den JDKs 1.0
bis 1.4 definiert.
Eine etwas bessere Lösung für Aufzählungs-Typen statt Integern ist ihre Simulation über
spezielle Klassen. Dieses Verfahren ist in vielen Büchern51 und Artikeln (auch bei Oracle selber)
beschrieben – sprengt aber leider den Rahmen des Tutorials. Seit dem JDK 1.5 ist dies eh
nicht mehr notwendig, da jetzt ja echte Aufzählungs-Typen in Java zur Verfügung stehen.
12.7 Top-Level-Klassen
Klassen (und auch Interfaces), die direkt in ein Package eingebettet sind, nennt man auch TopLevel-Klassen (bzw. Top-Level-Interfaces). Von daher sind alle Klassen, die wir bisher kenenn
gelernt haben Top-Level-Klassen.
12.8 Aufgaben
12.8.1 Aufgabe „Türme von Hanoi“
Simulieren Sie die „Türme von Hanoi“.
51
Z.B. in „Effektiv Java“ von todo.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 240 / 409
Abb. 12-1 : Türme von Hanoi
Die „Türme von Hanoi“ ist ein Spiel, bei dem am Anfang alle Scheiben auf einem Stab liegen,
und dann zu einem anderen Stab transportiert werden sollen. Für den Transport gelten zwei
Regeln:
• Es darf immer nur eine Scheibe bewegt werden.
• Es darf immer nur eine kleinere Scheibe auf einer größeren liegen, nie umgekehrt.
Schreiben Sie ein Programm, dass am Anfang die Anzahl an Scheiben einliest, mit denen die
Türme von Hanoi gespielt werden sollen. Danach soll das Spiel mit entsprechender Ausgabe
simuliert werden.
Mögliche Ein- und Ausgabe
Anzahl Scheiben: 3
1.
-||
|
--|-|
|
---|--|
|
*************************
2.
|
|
|
--|-|
|
---|--|
-|*************************
3.
|
|
|
|
|
|
---|--- --|--|*************************
4.
|
|
|
|
-||
---|--- --|-|
*************************
5.
|
|
|
|
-||
|
--|-- ---|--*************************
6.
|
|
-|-
|
|
--|--
|
|
---|---
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 241 / 409
*************************
7.
|
|
|
|
|
--|--||
---|--*************************
8.
|
|
-||
|
--|-|
|
---|--*************************
Lösung siehe Kapitel 12.9.
12.8.2 Aufgabe „Gerüchteküche“
Kleine Dörfer sind Gerüchteküchen erster Klasse. Jeder kennt jeden, und jeder redet über
jeden, bzw. schweigt wissend. All das wollen wir in einem kleinen Programm nachbilden.
Simulieren Sie ein Dorf mit n Einwohnern. Am Anfang kennt nur ein beliebiger Einwohner
(nehmen Sie einfach den ersten) das Gerücht.
Danach simulieren Sie das Geschehen, indem Sie maximal m Durchläufe starten. In jedem
Durchlauf treffen sich zwei zufällige Einwohner des Dorfes und reden miteinander. Was hierbei
passiert, ist davon abhängig, welchen Status bzgl. des Gerüchts die beiden Einwohner haben.
Einwohner 1
unwissend
unwissend
unwissend
erzählend
erzählend
erzählend
schweigsam
schweigsam
schweigsam
Einwohner 2
unwissend
erzählend
schweigsam
unwissend
erzählend
schweigsam
unwissend
erzählend
schweigsam
=>
Einwohner 1
unwissend
erzählend
unwissend
erzählend
schweigsam
schweigsam
schweigsam
schweigsam
schweigsam
Einwohner 2
unwissend
erzählend
schweigsam
erzählend
schweigsam
schweigsam
unwissend
schweigsam
schweigsam
Änderung
+
+
+
+
+
-
Das Programm ist damit im Prinzip beschrieben. Hier noch einige Hinweise:
• Der Benutzer soll am Anfang eingeben, wieviele Bewohner das Dorf hat, und wieviele
Durchläufe maximal gewünscht sind.
• Gibt es stabile Zustände? D.h. Zustände, bei denen es keine Änderung der Stati der
Einwohner mehr gibt? Wenn ja, welche? Erreicht das Programm einen solchen stabilen
Zustand, soll die Simulation mit einer entsprechenden Meldung beendet werden.
• Wird das Programm durch die max. Anzahl an Durchläufen beendet, soll die Simulation
auch hier mit einer entsprechenden Meldung beendet werden.
• Das Programm soll nach einer Status-Änderung folgende Informationen in einer Zeile
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 242 / 409
ausgeben52:
- Nummer des Durchlaufs (n-stellig in Abhängigkeit der max. Durchläufe, rechtsbündig,
führende Leerzeichen)
- Anzahl an unwissenden Einwohnern in der Form „U(_x)“ (n-stellig in Abhängigkeit der
max. Durchläufe, rechtsbündig, führende Underscores)
- Anzahl an erzählenden Einwohnern – Form anlog zu den Unwissenden
- Anzahl an schweigsamen Einwohnern – Form anlog zu den Unwissenden
- Ein Zeichen für jeden Einwohner:
o Unwissend => „_“
o Erzählend => „|“
o Schweigsam => „*“
Eine Simulation könnte also folgendermaßen aussehen:
Mögliche Ein- und Ausgabe:
Geruechtekueche
- Anzahl Personen: 30
- Max Durchlaeufe: 400
0:
3:
5:
6:
8:
9:
19:
26:
36:
41:
45:
47:
62:
109:
116:
136:
139:
159:
182:
U(29)
U(28)
U(27)
U(26)
U(26)
U(25)
U(25)
U(24)
U(23)
U(22)
U(22)
U(22)
U(21)
U(21)
U(21)
U(20)
U(19)
U(19)
U(19)
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
E(
1)
2)
3)
4)
2)
3)
2)
3)
4)
5)
4)
2)
3)
2)
1)
2)
3)
1)
0)
S( 0)
S( 0)
S( 0)
S( 0)
S( 2)
S( 2)
S( 3)
S( 3)
S( 3)
S( 3)
S( 4)
S( 6)
S( 6)
S( 7)
S( 8)
S( 8)
S( 8)
S(10)
S(11)
|_____________________________
|___________________|_________
|___________________|__|______
|_________|_________|__|______
|_________*_________|__*______
|_____|___*_________|__*______
|_____*___*_________|__*______
|_____*___*__|______|__*______
|_____*__|*__|______|__*______
|__|__*__|*__|______|__*______
*__|__*__|*__|______|__*______
*__|__*__|*__*______*__*______
*__|__*__|*__*______*__*___|__
*__|__*__**__*______*__*___|__
*__|__*__**__*______*__*___*__
*__|__*__**__*____|_*__*___*__
*__|__*__**__*___||_*__*___*__
*__|__*__**__*___**_*__*___*__
*__*__*__**__*___**_*__*___*__
Ende wegen stabilem Zustand
Lösung siehe Kapitel 12.10.
12.8.3 Aufgabe „Tic-Tac-Toe 1“
Schreiben Sie ein einfaches Tic-Tac-Toe.
Tic-Tac-Toe ist ein einfaches Spiel, das auf einem 3x3 Brett gespielt wird. Es wird abwechselnd
gezogen – jedesmal legt ein Spieler einen seiner Spielsteine auf ein Brett. Gewonnen hat, wer
zuerst 3 eigene Steine in einer Reihe hat (vertikal, horinzontal bzw. diogonal). Nach spätestens
neun Zügen ist das Spiel vorbei – hat dann keiner der Spieler eine vollständige Reihe geschafft,
ist das Spiel unentschieden.
Hinweis – wird das Spiel von einem Spieler optimal gespielt, kann er nicht verlieren. D.h.
52
Achtung – das Programm ändert nicht nach jedem Durchlauf seinen Status.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 243 / 409
spielen beide Spieler optimal, so wird es immer unentschieden ausgehen.
Fangen Sie das Programm einfach und klein an, und gehen Sie im ersten Schritt sehr
pragmatisch vor. D.h. versuchen Sie das Programm erstmal vollständig (im Sinne von „man
kann spielen“) zum Laufen zu bringen:
• Machen Sie eine einfache pragmatische Eingabe, z.B. indem die Felder von 1..9
durchnummeriert sind, und Sie einfach nur die Feld-Nummer von der Tastatur abholen.
• Machen Sie nach jedem Zug eine einfache Ausgabe in die Konsole, z.B. indem ein Feld mit
„_“ (unbelegt), „X“ (Spieler 1) und „O“ (Spieler 2) ausgegeben wird.
• Legen Sie einfach fest, dass z.B. immer der Mensch anfängt und der Computer der zweite
Spieler ist.
• Während des Spiels ist kein Abbruch möglich.
• Und vor allen Dingen verwenden Sie erstmal nicht zuviel Arbeit auf einen guten
Computerspieler (zum einen würden Sie dann nicht mehr gewinnen können, zum anderen ist
dies eher ein algorithmisches und nicht ein Java Problem). Im einfachsten Fall
programmieren Sie einfach einen Computerspieler, der das erste leere Feld besetzt. Wenn
dann irgendwann alles gut, stabil, schön usw. läuft, dann sollten Sie sich aber ruhig noch an
dieses Problem heranwagen und einen optimalen Computerspieler implementieren.
Mögliche Ein- und Ausgabe:
___
___
___
Bitte machen sie ihren Zug (1..9): 1
Mensch zieht 1 - Feld 1/1
O__
___
___
Computer (naechstes freies Feld) zieht 2 - Feld 1/2
OX_
___
___
Bitte machen sie ihren Zug (1..9): 4
Mensch zieht 4 - Feld 2/1
OX_
O__
___
Computer (naechstes freies Feld) zieht 3 - Feld 1/3
OXX
O__
___
Bitte machen sie ihren Zug (1..9): 7
Mensch zieht 7 - Feld 3/1
OXX
O__
O__
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 244 / 409
Mensch hat gewonnen
Hinweise:
• Lassen Sie sich nicht von dem scheinbar großen Problem einschüchtern. Programmieren
heißt immer auch – egal ob prozedural, funktional, objekt-orientiert,.. – ein Problem in
kleinere Teilprobleme zu zerlegen, bis man diese lösen kann.
• Fangen Sie auch nicht einfach blind an zu Implementieren – dazu ist diese Aufgabe
wahrscheinlich schon zu groß. Setzen Sie sich hin, und überlegen Sie, welche Teile Sie
zuerst warum und wie implementieren. Welche hängen dann davon ab?
• Identifzieren Sie die wichtigsten Konzepte des Spiels (welche sind das?) und packen Sie
diese in Klassen.
• Überlegen Sie, welche Schnittstellen die Klassen benötigen. Welche Klasse hat welche
Aufgaben bzw. Verantwortlichkeiten?
• Es ist eine interessante und wichtige Aufgabe sich zu überlegen, an welcher Stelle welche
Arbeit erledigt wird – die Kriterien dafür sind Klarheit, Erweiterbarkeit, Allgemeinheit,
Wiederverwendbarkeit, usw.
Lösung siehe Kapitel 12.11.
12.9 Lsg. zu Aufgabe „Türme von Hanoi“ – Kap. 12.8.1
Erklärung folgt (hoffentlich) später – todo...
import java.util.ArrayList;
public class Hanoi {
private ArrayList startTower, middleTower, endTower;
private int count;
private int move;
//-------------------------------------------------------------------------public void play(int count) {
this.count = count;
startTower = new ArrayList(count);
middleTower = new ArrayList();
endTower = new ArrayList();
for (int i=count; i>0; --i) {
startTower.add(i);
}
move = 1;
print();
moveTower(0, startTower, endTower, middleTower);
}
//-------------------------------------------------------------------------private void moveTower(
int layer, ArrayList source, ArrayList dest, ArrayList help) {
// Wenn die zu bewegende Ebene die oberste ist,
// dann kann die Scheibe direkt bewegt werden.
if (layer==source.size()-1) {
moveSlice(source, dest);
return;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 245 / 409
}
// Um den gesamten Haufen von Scheiben ab der angegebenen Ebene
// zu bewegen, muss:
// - zuerst der Haufen Scheiben ueber der Ebene zum Hilfs-Turm
//
bewegt werden,
// - dann die Scheibe zum Ziel-Turm bewegt werden, und dann
// - der verschobene Haufen auf die bewegte Scheibe bewegt werden.
int helpLayer = help.size();
moveTower(layer+1, source, help, dest);
moveSlice(source, dest);
moveTower(helpLayer, help, dest, source);
}
//-------------------------------------------------------------------------private void moveSlice(ArrayList source, ArrayList dest) {
// Den Umweg ueber die Klasse Integer kann man sich sparen, aber
// wir kennen Vererbung und die Klasse "Object" noch nicht.
Integer slice = (Integer)source.remove(source.size()-1);
dest.add(slice);
++move;
print();
}
//-------------------------------------------------------------------------private void print() {
System.out.println("\n" + move + '.');
for (int layer=count-1; layer>=0; --layer) {
System.out.print(' ');
printTowerLayer(startTower, layer);
System.out.print(' ');
printTowerLayer(middleTower, layer);
System.out.print(' ');
printTowerLayer(endTower, layer);
System.out.println();
}
System.out.println(createString(6*count+7, '*'));
}
//-------------------------------------------------------------------------private void printTowerLayer(ArrayList tower, int layer) {
int sliceWidth = tower.size()<=layer ? 0 : (Integer)tower.get(layer);
int spaceCount = count - sliceWidth;
String spaces = createString(spaceCount, ' ');
String halfSlice = createString(sliceWidth, '-');
System.out.print(spaces + halfSlice + '|' + halfSlice + spaces);
}
//-------------------------------------------------------------------------private static String createString(int len, char c) {
StringBuffer sb = new StringBuffer(len);
for (int i=0; i<len; i++) {
sb.append(c);
}
return sb.toString();
}
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static void main(String[] args) {
System.out.print("Anzahl Scheiben: ");
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
String in = reader.readLine();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 246 / 409
int count = Integer.parseInt(in);
if (count<1) {
System.out.println("Fehler");
return;
}
Hanoi h = new Hanoi();
h.play(count);
} catch (Exception x) {
System.out.println("Fehler: " + x);
}
}
}
12.10 Lsg. zu Aufgabe „Gerüchteküche“ – Kap. 12.8.2
12.10.1 Vorüberlegungen
Bevor wir überhaupt mit dem Programmieren anfangen, machen wir uns Gedanken zu den
Stati des Dorfes und den stabilen (End-)Zuständen.
• Nicht nach jeder Simulations-Runde muss sich der Status des Dorfes ändern. Es kann z.B.
sein, dass die Simulation zwei Einwohner aufeinander treffen läßt, die beide das Gerücht
nicht kennen – dann kennen sie es natürlich auch hinterher noch nicht. Die AufgabenStellung verlangt, dass nur nach einer Änderung des Dorf-Status eine neue Ausgabe zu
erfolgen hat – dem müssen wir Rechnung tragen.
• Wann muss die Simulation beendet werden?
Wenn die max. Anzahl an Durchläufen erreicht ist.
Und wenn ein stabiler Endzustand erreicht ist, d.h. nichts mehr passiert. Dies ist dadurch
bestimmt, dass es keinen Erzählenden mehr gibt. Achtung – es ist nicht zwingend nötig,
dass alle wissend und schweigsam sind. Auch unwissende53 haben nichts zu erzählen. Also
führen auch nur Unwissende und Schweigsame zu einem stabilen Endzustand.
12.10.2 Haupt-Programm
Eine typische Simulation läßt sich schnell in zwei Teile zerlegen:
• Erstmal muss die zu simulierende Situation aufgebaut werden, d.h. alle Daten müssen
eingelesen und initiiert werden.
• Dann müssen die Duchläufe gestartet werden, bis sie zu beenden sind.
Damit liegt die Struktur des Haupt-Programms fest.
main
Anzahl Personen einlesen
Anzahl max. Durchlaeufe einlesen
Daten initialisieren
Schleife-ueber-max-Duchlaeufe
Naechste Runde der Simulation
Wenn sich was geaendert hat
Ausgabe Status Dorf
Wenn stabiler Zustand, Schleifen-Ende
53
Gibt es die in einem echten Dorf eigentlich?
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 247 / 409
Ausgabe warum Schleifen-Ende
Ein kleine Optimierung kann man noch vornehmen. todo
main
Anzahl Personen einlesen
Anzahl max. Durchlaeufe einlesen
Daten initialisieren
Schleife-ueber-max-Duchlaeufe
Naechste Runde der Simulation
Wenn sich was geaendert hat
Ausgabe Status Dorf
Wenn stabiler Zustand, Schleifen-Ende
Ausgabe warum Schleifen-Ende
Die weiteren Erklärungen folgen (hoffentlich) später – todo...
12.10.3 Zusammenfassung
Alles zusammen ergibt das damit folgendes Programm:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Utils {
private static InputStreamReader isr = new InputStreamReader(System.in);
private static BufferedReader reader = new BufferedReader(isr);
public static String readString() {
try {
return reader.readLine();
} catch (Exception x) {
}
return "";
}
public static int readInt() {
try {
String in = reader.readLine();
return Integer.parseInt(in);
} catch (Exception x) {
}
return 0;
}
public static int digits(int arg) {
if (arg==0) return 1;
return (int)Math.log10(arg)+1;
}
// In java.text stehen verschiedene Format-Klassen zur Verfuegung,
// mit denen diese Aufgabe auch problemlos loesbar waere. Da wir
// diese Klassen aber nicht eingefuehrt haben, machen wie die
// Formatierung haendisch, und lernen dabei noch etwas programmieren.
public static String toString(int width, int value) {
StringBuffer sb = new StringBuffer(width);
int len = digits(value);
for (int i=0; i<width-len; i++) {
sb.append(' ');
}
return sb.toString() + value;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 248 / 409
}
public class Person {
private static final int UNKNOWING = 1;
private static final int TELLING = 2;
private static final int CLOSEMOUTHED = 3;
private int state = UNKNOWING;
public void setTelling() {
state = TELLING;
}
public boolean isUnknowing() {
return state == UNKNOWING;
}
public boolean isTelling() {
return state == TELLING;
}
public boolean isClosemouthed() {
return state == CLOSEMOUTHED;
}
public void print() {
switch (state) {
case UNKNOWING:
System.out.print('_');
break;
case TELLING:
System.out.print('|');
break;
case CLOSEMOUTHED:
System.out.print('*');
break;
}
}
//
//
//
//
//
Diese Funktion waere auch tabellen-gesteuert moeglich.
Dann waere sie performanter. Aber so ist sie vielleicht
besser zu lesen fuer einen Programmier-Anfaenger. Deshalb
auch extra als Kommentar die - weil nicht notwendig - nicht
zugewiesenen Stati.
public static boolean meet(Person p1, Person p2) {
if (p1.isUnknowing() && p2.isUnknowing()) {
// p1.state = UNKNOWING;
// p2.state = UNKNOWING;
return false;
}
if (p1.isUnknowing() && p2.isTelling()) {
p1.state = TELLING;
// p2.state = TELLING;
return true;
}
if (p1.isUnknowing() && p2.isClosemouthed()) {
// p1.state = UNKNOWING;
// p2.state = CLOSEMOUTHED;
return false;
}
if (p1.isTelling() && p2.isUnknowing()) {
// p1.state = TELLING;
p2.state = TELLING;
return true;
}
if (p1.isTelling() && p2.isTelling()) {
p1.state = CLOSEMOUTHED;
p2.state = CLOSEMOUTHED;
return true;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 249 / 409
}
if (p1.isTelling() && p2.isClosemouthed()) {
p1.state = CLOSEMOUTHED;
// p2.state = CLOSEMOUTHED;
return true;
}
if (p1.isClosemouthed() && p2.isUnknowing()) {
// p1.state = CLOSEMOUTHED;
// p2.state = UNKNOWING;
return false;
}
if (p1.isClosemouthed() && p2.isTelling()) {
// p1.state = CLOSEMOUTHED;
p2.state = CLOSEMOUTHED;
return true;
}
// p1.state = CLOSEMOUTHED;
// p2.state = CLOSEMOUTHED;
return false;
}
}
import java.util.Random;
public class Village {
private Random rnd = new Random();
private int width;
private Person[] persons;
public Village(int count) {
width = Utils.digits(count);
persons = new Person[count];
for (int i=0; i<count; i++) {
persons[i] = new Person();
}
persons[0].setTelling();
}
public boolean next() {
int rnd1 = random();
int rnd2 = random();
while (rnd1 == rnd2) {
rnd2 = random();
}
return Person.meet(persons[rnd1], persons[rnd2]);
}
public boolean stableState() {
for (int i=0; i<persons.length; i++) {
if (persons[i].isTelling()) {
return false;
}
}
return true;
}
public void print() {
int unknowingCount = 0;
int tellingCount = 0;
int closemouthedCount = 0;
for (int i=0; i<persons.length; i++) {
Person p = persons[i];
if (p.isUnknowing())
unknowingCount++;
if (p.isTelling())
tellingCount++;
if (p.isClosemouthed())
closemouthedCount++;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 250 / 409
System.out.print(
"U(" + Utils.toString(width, unknowingCount) +
") E(" + Utils.toString(width, tellingCount) +
") S(" + Utils.toString(width, closemouthedCount) + ") ");
for (int i=0; i<persons.length; i++) {
persons[i].print();
}
}
private int random() {
return rnd.nextInt(persons.length);
}
}
public class Appl {
public static void main(String[] args) {
System.out.println("Geruechtekueche\n===============\n");
System.out.print("Anzahl Personen: ");
int count = Utils.readInt();
if (count < 2) {
System.out.println("Fehler");
return;
}
System.out.print("Max Durchlaeufe: ");
int loops = Utils.readInt();
if (loops < 1) {
System.out.println("Fehler");
return;
}
System.out.println();
Village v = new Village(count);
int width = Utils.digits(loops);
printLine(width, 0, v);
for (int i=0; i<loops; i++) {
if (v.next()) {
printLine(width, i + 1, v);
if (v.stableState())
break;
}
}
System.out.println("\nEnde wegen " +
(v.stableState() ? "stabilem Zustand" : "Erreichen max Durchlaeufe"));
}
private static void printLine(int width, int n, Village v) {
System.out.print(Utils.toString(width, n) + ": ");
v.print();
System.out.println();
}
}
12.11 Lsg. zu Aufgabe „Tic-Tac-Toe 1“ – Kap. 12.8.3
Bei Tic-Tac-Toe ist der Programm-Ablauf stark durch die Spiel-Regeln geprägt: es wird
abwechselnd gezogen bis ein Spieler gewonnen hat oder das Spiel unentschieden zu Ende
gegangen ist. Dieser Ablauf legt die Grobstruktur des Programms fest.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 251 / 409
12.11.1 Grobstruktur von „main“
Unter der Maßgabe, dass der Mensch „weiß“ spielt und damit anfängt, läßt sich der Ablauf von
Tic-Tac-Toe folgendermaßen beschreiben:
Endlos-Schleife über:
1. Mensch macht Zug
2. Ausgabe Spielbrett
3. Hat Mensch gewonnen?
Wenn ja, Meldung und Ende
4. Ist Remis?
Wenn ja, Meldung und Ende
5. Computer macht Zug
6. Ausgabe Spielbrett
7. Hat Computer gewonnen?
Wenn ja, Meldung und Ende
8. --- (Remis Abfrage nicht notwendig)
Da das Tic-Tac-Toe Spielfeld 9 Felder hat, kann ein Remis nur nach dem menschlichen Zug
stattfinden, darum wird die Abfrage auf Remis nach dem Computer-Zug (Punkt 8) nicht
benötigt.
Diese Grobstruktur wirft sofort einige Fragen auf:
• Das Spielbrett muss angezeigt werden.
• Jemand muß wissen, wann das Spiel Remis ist.
• Jemand muß wissen, wann ein Spieler das Spiel gewonnen hat.
• Züge müssen berechnet, ausgeführt und verwaltet werden.
Eigentlich lassen sich die Fragen leicht beantworten:
• Für die Berechnung der Züge sind sicher der Mensch (im Programm vertreten durch ein
einfaches Benutzer-Interface) und der Computer zuständig - beide werden in
entsprechenden Klassen gekapselt.
=> Klasse „Human“ und „ComputerNext“
• Es ist sicher nicht Aufgabe eines Spielers, die Züge zu verwalten, Sieg und Remis zu
erkennen, oder das Spielbrett anzuzeigen. Statt dessen bietet sich eine Klasse an die das
Spielbrett darstellt, und über sich (Sieg, Remis, besetzte Felder, usw.) Bescheid weiß.
=> Klasse „Board“
Ohne an dieser Stelle der Programm-Analyse ins Detail zu gehen, lassen sich die ungefähren
Verantwortlichkeiten der Klassen schon darstellen:
12.11.1.1 Klasse „Board“
Verantwortlichkeiten:
• Verwaltung von Brett und der aktuellen Stellung.
• Erkennen von Sieg und Remis.
• Ausgabe auf der Konsole.
• Annahme eines neuen Zugs.
Abhängigkeiten:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 252 / 409
• ---
12.11.1.2 Klasse „Human“
Verantwortlichkeiten:
• Verwaltung seiner Spielfarbe.
In der ersten Version ist dies eigentlich nicht notwendig, da der Mensch hier immer „weiß“
hat. Aber auf Dauer wird das sicher änderbar sein sollen.
• Benutzer-Interaktion für die Eingabe des nächsten Zugs:
1. Zug eingeben (1..9)
2. Korrekte Eingabe? ja => weiter, nein => neue Eingabe (Sprung zu 1.)
3. Erlaubter Zug? (Feld unbesetzt?) ja => weiter, nein => neue Eingabe (Sprung zu 1.)
4. Zug zurück geben
- Für die Überprüfung auf erlaubten Zug (siehe 3.) wird der Element-Funktion von „Human“
das „Board“ als Parameter übergeben.
Abhängigkeiten:
• Klasse „Board“, da für die Interaktion abgefragt werden muß ob der eingegebene Zug
erlaubt ist oder nicht – d.h. das Feld schon besetzt ist oder nicht.
12.11.1.3 Klasse „ComputerNext“
Verantwortlichkeiten:
• Verwaltung seiner Spielfarbe.
In der ersten Version ist dies eigentlich nicht notwendig, da der Computer hier immer
„schwarz“ hat. Aber auf Dauer wird das sicher änderbar sein sollen.
• Berechung des nächsten Zugs. In der ersten Version wird hier einfach nur das nächste leere
Feld zurückgegeben. Um das zu finden, wird der Element-Funktion von „ComputerNext“ das
„Board“ als Parameter übergeben.
Abhängigkeiten:
• Klasse „Board“, da für die Berechnung die aktuelle Stellung benötigt wird, und auch
abgefragt werden muß ob ein Feld schon besetzt ist oder nicht.
12.11.1.4 Klasse „Board“ II
Aus den Abhängigkeiten der Klassen „Human“ und „Computer“ ergibt sich, dass die Klasse
„Board“ Funktionen benötigt, um die aktuelle Stellung zu ermitteln und ob ein Zug erlaubt ist.
Da wir in dieser ersten Version keine wirklich gute Computer-Spiel-Strategie implementieren
wollen, und wir im Augenblick auch nicht überblicken können in welcher Form wir dafür die
aktuelle Stellung benötigen, beschränken wir uns auf die Abfrage ob ein Zug erlaubt ist.
12.11.1.5 Offene Frage
Nach dieser kurzen Analyse bleibt eigentlich nur noch eine Frage offen: „Wer führt den Zug
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 253 / 409
letztlich eigentlich durch?“
Lösungs-Vorschlag 1:
Man könnte das Board als Parameter an die Berechnungs-Funktionen von „Human“ und
„Computer“ geben, und diese geben den nächsten Zug nicht zurück, sondern setzen ihn direkt.
• Direktes Setzen des Zugs geht eigentlich über die Verantwortlichkeiten der Spieler Klassen
hinaus.
• Mit dieser Lösung würde man jede direkte Kontrolle über den Zug abgeben, da man ihn gar
nicht mehr richtig mitbekommt. Z.B. eine grafische Anzeige des Zugs oder vergleichbares
wären nur schwer integrierbar. Bedenken sie, dass sowas nicht zu den Zuständigkeiten von
„Human“ bzw. „ComputerNext“ gehören kann, da diese sonst in einem anderen UI nicht
benutzbar wären.
• Von meinem Gefühl her ist es nicht gut, dass eine Spieler-Klasse direkt am Brett
rummanipuliert. Nun ist ein Stück Software zwar nicht betrügerisch (ausser es ist so
programmiert), aber eine Spieler Klasse sollte nicht direkt am Brett manipulieren.
Lösungs-Vorschlag 2:
Man könnte dem Board die Spieler mitgeben, und das Board ruft die „nächster-Zug“ Funktion
auf, und setzt den zurückgegebenen Zug dann direkt.
• Dies würde Ringabhängigkeiten erzeugen: die Spieler kennen ja das Brett, und jetzt müßte
das Brett auch die Spieler kennen – sehr schlecht.
• Auch hier wäre die Kontrolle über den Zug – z.B. für eine grafische Rückkopplung – weg.
• Und hier bekommt das Brett viel zu viel Verantwortlichkeiten – es ist für das Brett und das
Umfeld zuständig, aber es soll nicht der aktive Part sein, der als zentrale Komponente das
Spiel am Laufen hält (und dazu würde es bei dieser Lösung zumindest ansatzweise werden).
Beide Lösungen – man legt das Setzen in die Spieler bzw. in das Brett – kommen nicht gut
weg. Also lassen wir das Setzen erstmal außerhalb beider Klassen in der Spielschleife liegen.
Damit hat die Haupt-Schleife auch alle Kontrolle über das Geschehen, z.B. für eine
Rückkopplung auf der Kommandozeile – und die wollen wir ja auch haben.
12.11.1.6 Erste Implementierung
Zuerst müssen wir die entsprechenden Objekte von Mensch, Computer und Brett in „main“
anlegen. Da die Spieler schon mal ihre Spielfarbe verwalten sollen, werden diese im
Konstruktor übergeben (Integer-Konstanten aus der Klasse „Board“54). Außerdem wird einmal
initial das Spielbrett ausgegeben.
public class Appl {
public static void main(String[] args) {
Achtung – ein typischer Fall, wo ein echter Aufzählungs-Typ (siehe Kapitel 12.6) besser
wäre. Da wir sie aber aus Zeitmangel nicht eingeführt haben, und sie auch erst seit JDK 1.5
verfügbar sind, lösen wir das Problem auf althergebrachte Weise mit Integer-KlassenKonstanten.
54
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 254 / 409
Board b = new Board();
Human pl1 = new Human(Board.WHITE);
ComputerNext pl2 = new ComputerNext(Board.BLACK);
b.print();
// Endlos-Schleife...
}
}
Als nächstes fügen wir die Schleife von oben hinzu – schauen wir uns nochmal den
Pseudocode an:
Endlos-Schleife über:
1. Mensch macht Zug
2. Ausgabe Spielbrett
3. Hat Mensch gewonnen?
Wenn ja, Meldung und Ende
4. Ist Remis?
Wenn ja, Meldung und Ende
5. Computer macht Zug
6. Ausgabe Spielbrett
7. Hat Computer gewonnen?
Wenn ja, Meldung und Ende
8. --- (Remis Abfrage nicht notwendig)
1. Mensch macht Zug
Nach unserer Überlegung müssen das jetzt drei Anweisungen sein:
• Objekt „Spieler“ gibt Zug zurück.
• Rückkopplung im CUI über den Zug.
• Setzen des Zugs am Spielbrett.
Um den Zug transportieren zu können, definieren wir eine entsprechende Klasse „Move“.
Achtung – wir benutzen hier nicht das Wissen, dass Spieler 1 im Augenblick immer der
Mensch ist, sondern formulieren den Code schon allgemein, um für spätere Erweiterungen
Erfahrungen zu sammeln und offener zu sein.
Move m = pl1.next(b);
System.out.println(pl1.getName() + " zieht " + m + '\n');
b.set(m);
2. Ausgabe Spielbrett
Das kennen wir schon.
b.print();
3. Hat Mensch gewonnen?
Oben hatten wir festgelegt, dass das Brett die Informationen über den Spielstatus hält – damit
ist dieser Teil einfach hin zu schreiben. Wir übergeben aber die Spielfarbe an das Brett, damit
es weiss auf wessen Sieg es checken soll. Wir wissen hier natürlich, dass wenn hier einer
gewonnen hat, es nur der Mensch sein kann da er den letzten Zug gemacht hat. Aber im Sinne
von allgemeiner Formulierung ist es hier sicher besser allgemein zu bleiben. Das gleiche gilt für
die Ausgabe – hier fragen wir lieber allgemein den Spieler nach dem Namen statt ihn fest zu
verdrahten.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 255 / 409
if (b.wins(pl1.getColor())) {
System.out.println(pl1.getName() + " hat gewonnen");
return;
}
4. Ist Remis?
Vergleichbar zu Punkt 3.
if (b.remis()) {
System.out.println("Das Spiel ist remis ausgegangen");
return;
}
5. Computer macht Zug
6. Ausgabe Spielbrett
7. Hat Computer gewonnen?
8. --- (Remis Abfrage nicht notwendig)
Da die Punkte 1. bis 4. allgemein formuliert waren, kann der Code hierfür fast 1:1 kopiert
werden. Unterschiede sind nur, dass statt Spieler 1 Spieler 2 genommen wird, und die Abfrage
auf Remis entfällt.
m = pl2.next(b);
System.out.println(pl2.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
if (b.wins(pl2.getColor())) {
System.out.println(pl2.getName() + " hat gewonnen");
return;
}
Wenn wir jetzt davon ausgehen, dass es für alle benutzten Klassen „Move, Board, ...)
entsprechende Definitionen im gleichen Package gibt, und die Aufzählungs-Konstanten für die
Spielfarbe in Board definiert werden, ergibt sich der ganze Quelltext:
public class Appl {
public static void main(String[] args) {
Board b = new Board();
Human pl1 = new Human(Move.WHITE);
ComputerNext pl2 = new ComputerNext(Move.BLACK);
b.print();
while (true) {
Move m = pl1.next(b);
System.out.println(pl1.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
if (b.wins(pl1.getColor())) {
System.out.println(pl1.getName() + " hat gewonnen");
return;
}
if (b.full()) {
System.out.println("Das Spiel ist remis ausgegangen");
return;
}
m = pl2.next(b);
System.out.println(pl2.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
if (b.wins(pl2.getColor())) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 256 / 409
System.out.println(pl2.getName() + " hat gewonnen");
return;
}
}
}
}
Eine Bemerkung noch zum Schluss: dadurch dass wir die beiden Teile der Schleife für Mensch
und Computer so ähnlich gestaltet haben, haben wir 2 Vorteile gewonnen:
• Wir konnten den zweiten Teil quasi durch copy&paste erzeugen. Copy&Paste ist zwar nicht
so gut wie Code nur einmal wiederverwendbar schreiben (siehe Punkt 2), aber immer noch
viel besser als sich alles noch mal neu auszudenken.
• Weiter nach vorne gedacht, sehen wir in welche Richtung das Zusammenführen der beiden
Teile mal gehen könnte/muss. Auf Dauer kann es einfach nicht sein, dass wir zwei so
ähnliche Teile Code in unserem Tic-Tac-Toe behalten, und die nicht irgendwie
zusammenführen.
12.11.2 Die Rahmen der Klassen
Der nächste Schritt folgt zwangsläufig ohne echtes Nachdenken. Mit unserem „main“ haben wir
festgelegt welche Typen es gibt und wie ihre Schnittstelle aussieht. Das sollten wir erstmal
hinschreiben55.
12.11.2.1 Klasse „Move“
Eine Kleinigkeit muss hier noch überlegt werden, bevor die Rahmen quasi von alleine da sind.
Wie sieht die Klasse „Move“ aus?
Nun, ein Zug besteht letztlich aus zwei Koordinaten mit den Werten 0..2 mit denen das zu
setzende Feld definiert wird, und der Spielfarbe. Von daher würde eine einfache Klasse mit drei
Integer-Attributen (x,y und Farbe) für die Klasse „Move“ reichen.
Bevor wir die Klasse weiter durchdenken, muß ich auf einige Einwände eingehen, die sie
möglicherweise haben:
• Im Benutzer-Interface wird der Mensch einen Zug durch die Zahlen 1..9 eingeben. Ist ein
Zug also nicht einfach nur ein Integer?
Nein! Verfallen sie nie auf die Idee sich von einem Benutzer-Interface ihre interne
Repräsentation der Daten vorgeben zu lassen: die Zahlen 1..9 sind unsere augenblickliche
einfache Lösung zur Abbildung der Felder im Benutzer-Interface. In einem GUI benutzen sie
möglicherweise 9 Buttons um das Spielfeld abzubilden, die der Benutzer zum Setzen dann
direkt anklicken kann. Ist dann ein Zug etwa ein Button?
• Warum die Werte 0..2 und nicht 1..3?
Nun, gleiches Thema. Der Benutzer will vielleicht Koordinaten von 1..3 sehen,
möglicherweise aber auch a..c – keine Ahnung, das ist Sache des Benutzer-Interfaces.
Übrigens – Eclipse kann das fast von alleine für uns machen. Schauen sie sich mal die
Quick-Fixes von Eclipse an.
55
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 257 / 409
Intern arbeiten wir immer – wenn möglich – 0-basiert und mit Integer-Indices, da das
einfach, schnell, unproblematisch ist, und die Sprache und die Bibliotheken das auch überall
so machen.
• Aber warum dann nicht einfach nur einen Integer von 0..8? Das wäre doch einfacher?
Nun, ein Feld auf einem Spielbrett wird nun mal durch zwei Koordinaten beschrieben. Daher
steht zu erwarten, dass wir diese Repräsentation dauernd brauchen werden. Warum eine
andere wählen?
Und außerdem steht uns ja die Möglichkeit offen, auch noch ein zweite Schnittstelle für die
andere Repräsentation anzubieten.
Zurück zu unserer Klasse „Move“. Wir wissen jetzt, dass sie zwei Integer und die Farbe
enthalten soll, und das wir diese sicher abfragen können müssen. Da dies trivial ist,
implementieren wir die Funktionen auch direkt.
public class Move {
private int x;
private int y;
private int color;
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getColor() {
return color;
}
}
Erfüllt „Move“ damit wirklich alle Anforderungen, die aus der Main-Funktion resultieren? Nein!
Wir haben eine Stelle übersehen, da wir sie noch nicht verstehen.
System.out.println(pl1.getName() + " zieht " + m + '\n');
Hier wird das Move-Objekt „m“ einfach so mit Strings verkettet und ausgegeben. Nun gut, wir
haben u.a. in Kapitel 9.1.2 gelernt, dass das immer geht. Aber wir wissen nicht, warum und wie.
Und da wir jetzt dort eingreifen müssen – wir wollen ja unsere spezielle Tic-Tac-Toe Ausgabe
des Zugs haben – müssen wir jetzt wissen, wo wir ran müssen.
In Kapitel 14.11.1 werden wir lernen, dass wir hierfür die Element-Funktion „toString“
implementieren müssen. Dort lernen wir auch, wie das funktioniert, und warum das immer
klappt. Hier soll uns genügen, dass wir „toString“ implementieren müssen (siehe Kapitel
12.11.4.1).
Also sieht der Rahmen von „Move“ jetzt so aus, und ist jetzt wirklich fertig.
public class Move {
private int x;
private int y;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 258 / 409
private int color;
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getColor() {
return color;
}
public String toString() {
return null;
}
}
12.11.2.2 Klasse „Board“
Der erste Wurf (der Rahmen) des Klasse „Board“ fällt jetzt eigentlich einfach so ab. Die beiden
Farb-Konstanten und alle augenblicklich bekannten öffentlichen Elementfunktionen sind ja
durch unser Hauptprogramm vorgegeben – zusätzlich definieren wir noch eine ElementFunktion „valid“, die aufgrund der Klassen „Human“ und „Computer“ – siehe Kapitel 12.11.1.2
und 12.11.1.3 – benötigt wird.
public class Board {
public static final int WHITE = 1;
public static final int BLACK = 2;
public Board() {
}
public void set(Move m) {
}
public boolean valid(Move m) {
return false;
}
public boolean remis() {
return false;
}
public boolean wins(int color) {
return false;
}
public void print() {
}
}
Achtung – wir machen uns hier noch keine Gedanken um die Implementierung von „Board“ –
das kommt später. Wir sorgen erstmal nur dafür dass unser Programm compiliert.
12.11.2.3 Klasse „Human“
Der Rahmen der Klasse „Human“ ergibt sich genauso automatisch.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 259 / 409
public class Human {
public Human(int color) {
}
public Move next(Board b) {
return null;
}
public int getColor() {
return 0;
}
public String getName() {
return null;
}
}
12.11.2.4 Klasse „ComputerNext“
Na ja, und der Rahmen der Klasse „ComputerNext“ ist fast identisch.
public class ComputerNext {
public ComputerNext(int color) {
}
public Move next(Board b) {
return null;
}
public int getColor() {
return 0;
}
public String getName() {
return null;
}
}
12.11.2.5 Ergebnis
Jetzt sollte unser Programm problemlos compilieren. Mehr noch nicht – gestartet macht das
Verhalten des Programms wenig Sinn, da noch massenhaft Implementierungen fehlen.
12.11.3 Implementierung der Klassen
Jetzt geht’s ans implementieren der Klassen. Nur, mit welcher fängt man an?
Immer mit den unabhängigen Klassen, bzw. mit denen deren abhängige Klassen vorhanden
sind. In unserem Fall ist das die Klasse „Move“, da diese von nichts abhängt, die Spielfeld und
die Spieler-Klassen aber von ihr abhängen.
Warum wählt man diese Reihenfolge? Nun, der Grund ist, dass sie so eine Klasse erhalten, die
voll compilierbar, funktionstüchtig, lauffähig und testbar ist. Da sie ja von keiner anderen Klasse
abhängt, bzw. diese Klassen schon implementiert sind, können sie für diese Klasse ein TestProgramm schreiben, es compilieren und laufen lassen und damit die Klasse testen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 260 / 409
Umgekehrt geht das nicht. Würden sie in unserem Beispiel zuerst einen der Spieler entwickeln,
so würde die Klasse zwar compilieren (da wir ja zufälligerweise den Rahmen von „Move“ und
„Board“ schon fertig haben), aber sie könnten sie nicht testen. Damit würden sie in einem halbfertigen Zustand dann doch zu den Klassen „Move“ und „Board“ wechseln müssen, und erst
nachdem diese fertig sind (vollständig fertig, mit testem und allem) könnten sie an der SpielerKlasse weiter machen.
Okay – in der Praxis ist das meist nicht so schwarz/weiß. Sie benötigen für eine Klasse B nicht
die vollständige Klasse A. Und viele Anforderungen an A ergeben sich auch erst, wenn die
Klassen B und C A benutzen – und so wächst A parallel zu B und C. Aber jede dieser kleinen
Iterationen beginnt immer bei A.
12.11.4 Klasse „Move“
Die Klasse „Move“ ist eigentlich schon fertig, da wir mit dem Rahmen (siehe Kapitel 12.11.2.1)
auch direkt fast alle Element-Funktionen implementiert haben. Da sie alle einfache GetterFunktionen sind, war dies auch nicht weiter schwierig. Nur „toString“ fehlt noch.
12.11.4.1 Element-Funktion „Move.toString“
Was noch fehlt ist ein sinnvolle Darstellung eines Zugs auf der Konsole. Dazu müssen wir nur
die Element-Funktion „toString“ entsprechend implementieren, z.B. so:
public String toString() {
return (3*x+y+1) + " - Feld " + (x+1) + '/' + (y+1);
}
Wie das alles funktioniert mit dieser automatischen String-Umwandlung, und warum wir
„toString“ implementieren müssen, und all das – das wird in Kapitel 14.11.1 erklärt.
12.11.4.2 Konstruktoren
Was der Klasse aber sicher noch fehlt, sind ein oder mehrere Konstruktoren. Ohne das wir
wissen, wie Objekte der Klasse später in „Human“, „ComputerNext“ oder sonst-wo erzeugt
werden, können wir schon sagen, dass es zwei mögliche Kandidaten gibt:
• Move(int x, int y, int c)
– x und y gehen von 0..2
Diese Variante enspricht unserer Denkweise mit x/y-Koordinate des Feldes und der
Spielfarbe. Die Indices gehen von 0..2, da wir intern immer 0-basiert arbeiten.
• Move(int field, int c)
– field geht von 0..8
Diese Variante ist ein Zugeständnis an unser erstes einfaches Benutzerinterface. Hier muss
der Wert 1..9 in die Felder 0..2/0..2 umgesetzt werden. Da dies möglicherweise öfter
vorkommt, implementieren wir diese Zerlegung zentral in der Klasse „Move“. Da intern aber
immer 0-basiert gerechnet wird, erwartet die Funktion einen Feld-Index von 0..8.
Da es nicht viel Arbeit ist, implementieren wir profilaktisch beide Konstruktoren. Man könnte mit
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 261 / 409
dieser Arbeit auch problemlos warten, bis sie denn wirklich gebraucht werden. So, im Vorfeld
implementiert, kann es halt passieren, dass die Schnittstelle doch nicht paßt, oder wir gar eine
Funktion implementieren, die wir dann doch nicht brauchen.
public Move(int n, int c) {
x = n/3;
y = n%3;
color = c;
}
public Move(int x, int y, int c) {
this.x = x;
this.y = y;
color = c;
}
12.11.4.3 Zusammenfassung
Damit ergibt sich der komplette Quelltext der Klasse „Move“.
Source „Move.java“
public class Move {
private int x;
private int y;
private int color;
public Move(int n, int c) {
x = n/3;
y = n%3;
color = c;
}
public Move(int x, int y, int c) {
this.x = x;
this.y = y;
color = c;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getColor() {
return color;
}
public String toString() {
return (3*x+y+1) + " - Feld " + (x+1) + '/' + (y+1);
}
}
12.11.5 Klasse „Board“
Nach „Move“ ist die nächst-tiefere Klasse „Board“, d.h. wird die als nächstes implementiert.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 262 / 409
12.11.5.1 Spielfeld-Repräsentation und erweiterte Klassen-Definition von „Board“
Die erste Entscheidung, die zu treffen ist, ist: wie repräsentieren wir das Spielfeld in der
Klasse? Schauen wir uns mal die Anforderungen an das Spielfeld an:
• Wahlfreier Zugriff wird benötigt: zwei Indices definieren das Feld
• Sortierung wird nicht benötigt.
• Spielfeld benötigt keine dynamische Größe.
• Suchen findet nicht statt.
• Inhalte sollten nicht umsortiert werden – das Brett ist und bleibt wie es ist.
Gleichen wir dies mit unserem Java Wissen ab, dann sehen wir das ein normales 2-dim Array
die Anforderungen am Besten erfüllt.
Stellt sich noch die Frage, was wird der Inhalt des Arrays sein, d.h. wie repräsentieren wir im
Programm ein Feld bzw. den Status eines Feldes? Welche Zustände kann ein Feld annehmen?
• Leer
• Weißer Spielstein
• Schwarzer Spielstein
Das klingt nach einer Aufzählung mit Klassen-Konstanten56. Und Konstanten für die Spielfarben
sind schon vorhanden – ergänzen wir noch eine für ein leeres Feld.
Damit sind die Attribute und Klassen-Konstanten von „Board“ klar.
public class Board {
public static final int EMPTY = 1;
public static final int WHITE = 2;
public static final int BLACK = 3;
private int[][] field;
...
}
12.11.5.2 Attribut-Initialisierung und Konstruktor „Board“
Bei der Objekt-Initialisierung muss nur das Spielfeld aufgebaut werden, d.h. ein 3x3 Array, bei
dem alle Felder „Empty“ sind. Es gibt viele Arten dies zu machen, hier mache ich das mit einer
einfachen Attribut-Initialisierung. Der Konstruktor bleibt dadurch leer.
public class Board {
private static final int EMPTY = 1;
private static final int WHITE = 2;
private static final int BLACK = 3;
private int[][] field =
{ EMPTY, EMPTY, EMPTY
{ EMPTY, EMPTY, EMPTY
{ EMPTY, EMPTY, EMPTY
{
},
},
}
Achtung – noch ein Fall, wo ein echter Aufzählungs-Typ (siehe Kapitel 12.6) besser wäre. Da
wir sie aber aus Zeitmangel nicht eingeführt haben, und sie auch erst seit JDK 1.5 verfügbar
sind, lösen wir das Problem auf althergebrachte Weise mit Integer-Klassen-Konstanten.
56
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 263 / 409
};
public Board() {
}
...
}
12.11.5.3 Element-Funktion „Board.set“
Die Elementfunktion „set“ bekommt einen Zug übergeben und setzt ihn.
Hierbei gibt es ein Thema, über das man zumindest kurz nachdenken sollte: Was passiert,
wenn als Zug ein Feld übergeben wird, das schon besetzt ist? Wir definieren hier für den ZugParameter, dass ein solcher Parameter nicht erlaubt ist. An dieser Stelle ist das eine
akzeptable Forderung, da im Rahmen des „main“-Designs die von den Spielern „berechneten“
Züge regelkonform sein müssen.
Trotzdem sollten wir uns darüber im klaren sein, dass ein Programmier-Fehler in den SpielerKlassen hier zu einem Problem führen kann. Solchen Situationen kann man z.B. mit Assertions
begegnen, die wir aber aus Zeitmangel im Rahmen des Tutorials nicht besprechen.
public void set(Move m) {
field[m.getX()][m.getY()] = m.getColor();
}
12.11.5.4 Element-Funktion „Board.valid“
Die Elementfunktion „valid“ soll zurückgeben, ob ein Spielzug erlaubt ist, d.h. das Feld noch
unbesetzt ist.
public boolean valid(Move m) {
return field[m.getX()][m.getY()]==EMPTY;
}
12.11.5.5 Element-Funktion „Board.remis“
Die Element-Funktion „remis“ soll zurückgeben, ob das Spiel remis ausgegangen ist. Die erste
Idee das zu lösen, ist ganz einfach: es müssen alle Felder besetzt sein.
boolean remis() {
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
if (field[x][y]==EMPTY) return false;
}
}
return true;
}
Aber dann denken wir noch mal darüber nach. Diese Funktion gibt auch dann „true“ zurück,
wenn das Spielfeld voll ist, aber ein Spieler gewonnen hat. Hier wird nämlich nicht auf Remis
untersucht, sondern auf „Erzwungendes-Spielende wegen vollem Feld“. Also eigentlich müßten
wir vorher abfragen, ob kein Spieler gewonnen hat – erst dann arbeitet die Funktion korrekt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 264 / 409
boolean remis() {
if (wins(WHITE) || wins(BLACK)) {
return false;
}
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
if (field[x][y]==EMPTY) return false;
}
}
return true;
}
Wenn wir uns jetzt aber unser Haupt-Programm vor Augen führen, dann sehen wir, dass das
nicht das ist, was wir wollten. Uns ging es schon um das erzwungende Spielende, wenn das
Brett voll ist, denn einen möglichen Sieg hatten wir vorher schon abgehandelt.
if (b.wins(pl1.getColor())) {
System.out.println(pl1.getName() + " hat gewonnen");
return;
}
if (b.remis()) {
System.out.println("Das Spiel ist remis ausgegangen");
return;
}
Jetzt haben wir zwei Möglichkeiten:
1. Wir akzeptieren die obige aufwendigere „remis“ Funktion, selbst wenn dort Abfragen
gemacht werden, die in unserem Programm nicht notwendig sind.
2. Oder wir bleiben bei der ersten Variante, benennen die Funktion aber um.
Achtung – kommen Sie bitte nicht auf die Idee, den Namen bei „remis“ zu belassen, aber von
der Funktionalität her „Brett-voll“ zu implementieren – die Funktion macht dann nicht mehr das,
was der Name verspricht. Irgendwann wird sich das rächen. Man muss nur im HauptProgramm die Abfrage-Reihenfolge von „wins“ und „remis“ verändern, und schon hat man den
Salat. Aus Sicht des Haupt-Programms wäre dann noch alles okay – in der Praxis leider nicht.
Ich persönlich habe mich für Möglichkeit 2 entschieden, d.h. ich nehme unsere erste einfachere
Variante der Element-Funktion, und benenne sie in „full“ um. Denn das liefert sie zurück: ob
das Brett voll ist.
public boolean full() {
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
if (field[x][y]==EMPTY) return false;
}
}
return true;
}
Hinweis – wenn ich jetzt doch noch eine Element-Funktion „remis“ benötige, dann würde ich
sie jetzt auf Basis von „wins“ und „full“ ganz einfach implementieren können. So habe ich zwei
Fliegen mit einer Klappe geschlagen: zur Zeit keine überflüssigen Implementierungen, und auf
Dauer einfache verständliche Funktionen. So z.B. könnte „remis“ dann aussehen:
boolean remis() {
if (wins(WHITE) || wins(BLACK)) {
return false;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 265 / 409
}
return full();
}
12.11.5.6 Element-Funktion „Board.wins“
Die Element-Funktion „wins“ soll zurückgeben, ob der Spieler der übergebenen Farbe
gewonnen hat. Möglicherweise gibt es einen tollen Algorithmus um das schnell und kurz zu
checken – wenn ja, so kenne ich ihn nicht. Und ich hatte auch keine Lust mir groß Gedanken
darum zu machen – acht mögliche Gewinnstellungen kann ich auch schnell bruce-force
durchtesten.
public boolean wins(int
if (field[0][0]==c &&
if (field[1][0]==c &&
if (field[2][0]==c &&
if (field[0][0]==c &&
if (field[0][1]==c &&
if (field[0][2]==c &&
if (field[0][0]==c &&
if (field[0][2]==c &&
return false;
}
c) {
field[0][1]==c
field[1][1]==c
field[2][1]==c
field[1][0]==c
field[1][1]==c
field[1][2]==c
field[1][1]==c
field[1][1]==c
&&
&&
&&
&&
&&
&&
&&
&&
field[0][2]==c)
field[1][2]==c)
field[2][2]==c)
field[2][0]==c)
field[2][1]==c)
field[2][2]==c)
field[2][2]==c)
field[2][0]==c)
return
return
return
return
return
return
return
return
true;
true;
true;
true;
true;
true;
true;
true;
12.11.5.7 Element-Funktion „Board.print“
Die Element-Funktion „print“ soll das Spielfeld ausgeben. Kein Problem – zwei geschachtelte
for-Schleifen für das 2-dim Array und darin eine switch-Anweisung für die jeweiligen FeldAusgaben.
public void print() {
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
switch (field[x][y]) {
case EMPTY:
System.out.print('_');
break;
case WHITE:
System.out.print('O');
break;
case BLACK:
System.out.print('X');
break;
}
}
System.out.println();
}
System.out.println();
}
12.11.5.8 Zusammenfassung
Damit ergibt sich der komplette Quelltext der Klasse „Board“.
Source „Board.java“
public class Board {
public static final int EMPTY = 1;
public static final int WHITE = 2;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 266 / 409
public static final int BLACK = 3;
private int[][] field =
{ EMPTY, EMPTY, EMPTY
{ EMPTY, EMPTY, EMPTY
{ EMPTY, EMPTY, EMPTY
};
{
},
},
}
public Board() {
}
public void set(Move m) {
field[m.getX()][m.getY()] = m.getColor();
}
public boolean valid(Move m) {
return field[m.getX()][m.getY()]==EMPTY;
}
public boolean full() {
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
if (field[x][y]==EMPTY) return false;
}
}
return true;
}
public boolean wins(int
if (field[0][0]==c &&
if (field[1][0]==c &&
if (field[2][0]==c &&
if (field[0][0]==c &&
if (field[0][1]==c &&
if (field[0][2]==c &&
if (field[0][0]==c &&
if (field[0][2]==c &&
return false;
}
c) {
field[0][1]==c
field[1][1]==c
field[2][1]==c
field[1][0]==c
field[1][1]==c
field[1][2]==c
field[1][1]==c
field[1][1]==c
&&
&&
&&
&&
&&
&&
&&
&&
field[0][2]==c)
field[1][2]==c)
field[2][2]==c)
field[2][0]==c)
field[2][1]==c)
field[2][2]==c)
field[2][2]==c)
field[2][0]==c)
return
return
return
return
return
return
return
return
true;
true;
true;
true;
true;
true;
true;
true;
public void print() {
for (int x=0; x<3; ++x) {
for (int y=0; y<3; ++y) {
switch (field[x][y]) {
case EMPTY:
System.out.print('_');
break;
case WHITE:
System.out.print('O');
break;
case BLACK:
System.out.print('X');
break;
}
}
System.out.println();
}
System.out.println();
}
}
12.11.6 Klasse „Human“
12.11.6.1 Erweiterte Klassen-Definition von „Human“
Bzgl. der Attribute der Klasse „Human“ fällt erstmal nur auf, dass sie sich die Spielfarbe merken
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 267 / 409
muss.
public class Human {
private int color;
...
}
12.11.6.2 Attribut-Initialisierung und Konstruktor „Human“
Der Konstruktor muss einfach nur das Attribut mit der übergebene Spiel-Farbe initialisieren.
public class Human {
private int color;
public Human(int c) {
color = c;
}
...
}
12.11.6.3 Element-Funktion „Human.getColor“
Die Element-Funktion „getColor“ soll nur die Spiel-Farbe zurückgeben – trivial.
public int getColor() {
return color;
}
12.11.6.4 Element-Funktion „Human.getName“
Die Element-Funktion „getName“ soll den Namen des Spielers zurückgeben – auch trivial.
public String getName() {
return "Mensch";
}
12.11.6.5 Element-Funktion „Human.next“
Die einzige Funktion der Klasse „Human“ in der wirklich was passiert, ist die Element-Funktion
„next“. Sie ist auch relativ kompliziert – was aber nicht an der eigentlichen Funktionalität liegt,
sondern daran dass in ihr Benutzer-Interaktion statt findet – und „Fehlertolerantes Einlesen ist
einfach der pure Spass!“.
Bevor wir anfangen zu implementieren lassen sie uns kurz überlegen, was in „next“ passieren
muss:
1. Ausgabe „Bitte Zug eingeben“
2. Benutzer-Eingabe 1..9
3. Korrekte Eingabe? Wenn nein, Fehlerhinweis und zurück zu 1.
4. Eingabe in Zug wandeln
5. Freies Feld? Wenn nein, Fehlerhinweis und zurück zu 1.
6. Rückgabe des Zugs
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 268 / 409
Man sieht, dass man hier eine Endlos-Schleife benötigt, die im Falle einer richtigen Eingabe
abgebrochen wird.
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Human {
...
public Move next(Board b) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
while (true) {
System.out.print("Bitte machen Sie ihren Zug (1..9): ");
try {
String in = reader.readLine();
int field = Integer.parseInt(in);
if (field<1 || field>9) {
System.out.println("Fehlerhafte Eingabe!\n");
continue;
}
Move m = new Move(field-1, color);
if (b.valid(m)) {
System.out.println();
return m;
}
System.out.println("Feld schon besetzt!\n");
}
catch (Exception x) {
System.out.println("Fehlerhafte Eingabe!\n");
}
}
}
}
12.11.6.6 Zusammenfassung
Damit ergibt sich der komplette Quelltext der Klasse „Human“.
Source „Human.java“
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Human {
private int color;
public Human(int c) {
color = c;
}
public int getColor() {
return color;
}
public String getName() {
return "Mensch";
}
public Move next(Board b) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 269 / 409
while (true) {
System.out.print("Bitte machen Sie ihren Zug (1..9): ");
try {
String in = reader.readLine();
int field = Integer.parseInt(in);
if (field<1 || field>9) {
System.out.println("Fehlerhafte Eingabe!\n");
continue;
}
Move m = new Move(field-1, color);
if (b.valid(m)) {
System.out.println();
return m;
}
System.out.println("Feld schon besetzt!\n");
}
catch (Exception x) {
System.out.println("Fehlerhafte Eingabe!\n");
}
}
}
}
12.11.7 Zwischenstand
Hiermit könnten sie jetzt schon das Tic-Tac-Toe eingeschränkt zum Laufen bringen. Sie
ersetzen im Haupt-Programm einfach den Computer-Spieler durch einen zweiten menschlichen
Spieler, und schon ist der Computer das Spielfeld für zwei Menschen beim Tic-Tac-Toe.
public class Appl {
public static void main(String[] args) {
Board b = new Board();
Human pl1 = new Human(Move.WHITE);
Human pl2 = new Human(Move.BLACK);
// <<<<<<<<<<<<<< Aenderung
b.print();
...
}
}
12.11.8 Klasse „ComputerNext“
Die erste Implementierung eines Computer-Spielers wird ganz einfach ausfallen – der
Computer wird einfach das erste leere Feld nehmen.
Das Attribut „color“ und die Element-Funktionen „ComputerNext.getColor“ und
„ComputerNext.getName“ werden analog zur Klasse „Human“ sein – d.h. können wir hier auf
die Herleitung verzichten.
12.11.8.1 Element-Funktion „ComputerNext.next“
Damit bleibt als einzige Funktion mit etwas Aufwand die Element-Funktion „next“ über. Aber
auch diese ist nicht schwer.
Der Computer soll einfach das erste freie Feld nehmen, dass heisst wir müssen einfach nur die
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 270 / 409
Felder durchprobieren, und den ersten erlaubten Zug zurückgeben. Da wir in der Klasse „Move“
u.a. einen Konstruktor implementiert haben, der mit einem Feld-Index auskommt, können wir
uns hier auf eine Schleife beschränken, statt zwei verschachtelte Schleifen implementieren zu
müssen.
public Move next(Board b) {
for (int i=0; true; i++) {
Move m = new Move(i, color);
if (b.valid(m)) {
return m;
}
}
// <-- keine Rueckgabe noetig, da man hier nicht hinkommen kann
}
Auch hier verlassen wir uns wieder darauf, dass das Spiel in der Gesamtheit sauber und
fehlerfrei programmiert ist. Wir ignorieren das Problem: welchen Zug geben wir zurück, wenn
es kein freies Feld mehr gibt – da dieses in der Praxis nicht vorkommen dürfte.
Da – unter dieser Bedingung – spätestens beim Feld-Index „8“ ein freies Feld gefunden werden
muß, können wir uns die Abbruch-Bedingung sparen. Damit wird die Stelle hinter den Schleifen
nie erreicht werden können. Das erkennt der Compiler, und verlangt daher dort von uns auch
keine Rückgabe – wir würden eh nur sinnloses Zeug dort hinschreiben können.
12.11.8.2 Zusammenfassung
Damit ergibt sich der komplette Quelltext der Klasse „ComputerNext“.
Source „ComputerNext.java“
public class ComputerNext {
private int color;
public ComputerNext(int c) {
color = c;
}
public int getColor() {
return color;
}
public String getName() {
return "Computer (naechstes freies Feld)";
}
public Move next(Board b) {
for (int i=0; true; i++) {
Move m = new Move(i, color);
if (b.valid(m)) {
return m;
}
}
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 271 / 409
12.11.9 Fazit
Das Programm ist fertig. Natürlich gibt es noch eine Menge offener Wünsche – diese ergeben
sich zum größten Teil aber aus der Aufgaben-Stellung: das Programm hat kein GUI, der
Computer spielt ziemlich dumm spielt, die Gegner liegen fest, der Mensch beginnt immer, usw.
Wirklich unschön an dem Programm sind eigentlich nur wenige Dinge:
• Die Schleife im Haupt-Programm enthält zweimal fast den gleichen Code.
• Es gibt keine saubere Trennung von Benutzer-Oberfläche und Programm-Logik.
• Das Programm enthält viele implizite Annahmen – siehe z.B. Kapitel 12.11.5.3.
In den nächsten Kapiteln werden wir die ersten beiden Unschönheiten beseitigen und das TicTac-Toe gleichzeitig um einige Features bereichern. Um angemessen mit impliziten Annahmen
und darauf basierenden Fehlern umgehen zu können, müßten wir uns mit Assertions
auseinander setzen – dies wird leider im Rahmen der Vorlesung aus Zeitmangel nicht gemacht.
Wenn sie sich das gesamte Programm mal anschauen, so sollten sie eins bemerken. Jede
Klasse ist genau für eine Sache zuständig, und die meisten Funktionen sind ziemlich einfach
und erledigen genau eine Aufgabe. Kompliziert und aufwändig sind nur die Funktionen, die
Benutzer I/O machen – vor allem Benutzer-Eingabe. Dies ist nicht ungewöhnlich.
• Durch ein sauberes OO-Design (OOD) erhält man klare überschaubare Abstraktionen, und
die Klassen und Funktionen bleiben einfach, klein und übersichtlich.
• Benutzer I/O dagegen, und hier vor allem die Eingabe ist nicht trivial. Denn Benutzer können
allen möglichen Blödsinn eingeben, d.h. die Eingabe muss fehlertolerant sein und gute
Meldungen liefern. Dies ist nicht imnmer trivial.
13 Packages
Packages sind neben Klassen das zweite Modul-Konzept von Java. Während Klassen
wiederverwendbare Komponenten darstellen und einen Aspekt der realen Welt abbilden, sind
Packages ein reines Strukturierungsmittel, das konzeptionell zusammengehörige Klassen zu
einer Einheit verbindet.
13.1 Package-Anweisung
In Java ist jede Klasse automatisch Bestandteil eines Packages - indem sie eine .java Datei mit
einer Package-Anweisung wie z.B. „package packagename;“ beginnen, geben sie automatisch
das Package an, zu dem die Klasse gehört.
package mypackage;
// Package Anweisung
public class A {
}
// Die Klasse A liegt jetzt im Package „mypackage“
Achtung – eine Package-Anweisung muss immer die erste Anweisung in einem Quelltext sein.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 272 / 409
Daher vorher darf es nur Kommentar- und Leerzeilen geben.
Hinweis – Klassen in Quelltexten ohne Package-Anweisungen liegen im sogenannten DefaultPackage – siehe Kapitel 13.5.
Hinweis – denken sie daran, dass jedes Package auf der Datei-Systeme-Ebene einem
Verzeichnis entsprechen muss – siehe auch Kapitel 3.8, Kapitel 4.2 bzw. Kapitel 4.3.2.
Hinweis – per Konvention sollten Package Namen immer klein beginnen und klein geschrieben
werden, z. B.: „nameeinespackage“ – siehe auch Kapitel 3.2.5.
13.2 Klassen in Packages
Der vollständige Name einer Klasse ist nicht nur der Klassen-Name, sondern beinhaltet auch
den bzw. die Package-Namen, durch den Punkt-Operator getrennt – Beispiel „java.util.Date“.
Dies wird auch der „vollständig-referenzierte“ oder auch der „vollständig-qualifizierte“ Name
genannt.
Um eine Klasse zu benutzen gibt es drei Möglichkeiten:
• Benutzung des vollständig qualifizierten Namens.
• Benutzung einer Import-Anweisung für die Klasse.
• Benutzung einer Import-Anweisung für das gesamte Package der Klasse.
13.2.1 Benutzung des vollständig qualifizierten Namens
Sie können immer jede Klasse über ihren voll referenzierten Namen ansprechen.
java.util.Date d = new java.util.Date();
13.2.2 Benutzung einer Import-Anweisung
Sie können eine Klasse in den Namensraum ihrer Datei importieren. Dafür können sie mit dem
Schlüsselwort „import“ Import-Anweisungen an den Anfang ihrer Datei schreiben. ImportAnweisungen müssen nach der Package-Anweisung stehen, wenn eine solche vorhanden ist.
Aber sie müssen vor jeder Klassen- oder Interface-Definition erfolgen.
Mit import und exakt referenzierter Klasse importieren sie eine Klasse:
import java.util.Date;
...
Date d = new Date();
Alternativ können sie mit einer Import-Anweisung auch alle Symbole eines Package in den
Namensraum ihrer Datei importieren. Hierfür muss statt des Klassen-Namens in der ImportAnweisung ein „*“ angegeben werden.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 273 / 409
import java.util.*;
...
Date d = new Date();
Vector v = new Vector();
13.2.3 Klassen im gleichen Package
Klassen im gleichen Package brauchen weder importiert noch vollständig qualifiziert werden –
sie sind immer automatisch bekannt und können einfach durch Benutzung des KlassenNamens angeprochen werden – siehe z.B. Bsp. todo.
13.3 Language-Package
Z. B. die Klassen String und Object sind Teil des Packages java.lang, trotzdem konnten wir sie
benutzen57 ohne dieses Package importiert zu haben.
Das Package java.lang wird immer automatisch importiert, ohne dass Sie sich darum kümmern
müssen.
13.4 Verschachtelung
Packages können natürlich wieder in einander verschachtelt sein. Wollen Sie z. B. eine Klasse
Model dem Package generator im Package report zuordnen, so müssen sie nur folgende
package-Anweisung am Anfang Ihrer Datei Model.java unterbringen:
package report.generator;
Wollen sie diese Klasse in einem anderen Package nutzen, so haben sie natürlich folgende
drei Möglichkeiten:
report.generator.Model m = new report.generator.Model();
import report.generator.Model;
...
Model m = new Model();
import report.generator.*;
...
Model m = new Model();
13.5 Default-Package
Und was passiert, wenn sie keine Package-Anweisung benutzen? Nichts - auch das ist
korrekter Code.
57
Falls Ihnen nicht klar ist, das Sie Object benutzt haben - denken Sie daran, dass Object
automatisch die Basisklasse einer Klasse ist, wenn keine andere explizit angegeben ist.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 274 / 409
Für kleinere Projekte bzw. schnelle Tests wurde zur Arbeitserleichterung in Java das Feature
eingeführt, dass in diesem Fall alle diese Klasse in einem Default-Package der
Entwicklungsumgebung landen. Sie brauchen auch keinen import-Anweisung anzugeben - das
Default-Package wird immer automatisch importiert.
Dem Java-Compiler ist die physikalische Realisation des Default-Packages übrigens freigestellt
- er braucht nur eins, kann aber auch mehrere Packages anlegen und diese dann über mehrere
Unterverzeichnisse zu verteilen.
13.6 Verzeichnisse
Packages müssen physikalisch auf der Platte durch Unterverzeichnisse abgebildet werden.
Eine Klasse „first.second.third.ClassName“ muss also in einer Datei „ClassName.java“ in den
Verzeichnis-Struktur „first/second/third“ liegen.
Moderne Entwicklungsumgebungen wie z.B. Eclipse können die notwendige VerzeichnisStruktur automatisch im Hintergrund erzeugen, und legen z.B. neue Dateien automatisch richtig
ab.
14 Vererbung
14.1 Vererbungs-Hierarchien
Vererbung bedeutet "ist ein".
Abb. 14-1 : Vererbungs-Hierarchie
Voraussetzung für öffentliche Vererbung ist immer eine ist-ein Beziehung.
Allgemein:
Abb. 14-2 : Allgemeine Vererbungs-Hierarchie
Hierbei ist:
• A Basisklasse (von B und C)
• B ist abgeleitet von A => B ist ein A => alles was für A gilt, gilt auch für B
• C ist abgeleitet von A => C ist ein A => alles was für A gilt, gilt auch für C
In Richtung der abgeleiteten Klassen findet eine Spezialisierung statt:
• B ist eine Spezialisierung von A
• Ein Pferd ist eine Spezialisierung eines Säugetiers
• Alles, was für Säugetiere gilt (z. B. geboren werden, schwanger sein, sterben), gilt auch für
Pferde. All diese Attribute und Funktionen erbt Pferd von Säugetier.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 275 / 409
In Richtung der Basis-Klassen findet eine Generalisierung oder Verallgemeinerung statt
- Basis-Klassen fassen gemeinsame Dinge der abgeleiteten Klassen zusammen:
• A enthält alle Gemeinsamkeiten von B und C
• Vogel enthält alles Vogel-typische, unabhängig, ob es sich um eine Amsel oder eine Möve
handelt.
Hinweise
• Abgeleitete Klassen (z. B. B und C) sind unabhängig voneinander.
• Von einer Klasse können beliebig viele andere Klassen abgeleitet werden.
• Eine Klasse hat immer nur eine Basisklasse (Einfachvererbung58).
Achtung
Unterscheiden sie zwischen ”hat ein” bzw. ”ist implementiert mit” und ”ist ein”.
• Eine Person hat einen Namen, ist aber kein Name => Aggregation, Komposition, ...
• Eine Person ist ein Lebewesen, immer ohne wenn und aber => Vererbung
14.2 Implementation
Wie wird Vererbung in Java implementiert?
Syntax
[Modifier] class klassenname extends Basisklasse { Klassen-Definition }
public class A {
public void afct() {
System.out.println("afct in A");
}
}
public class B extends A {
public void bfct() {
System.out.println("bfct in B");
}
}
// Normales A Verhalten
A a = new A();
a.afct();
// Normales B Verhalten
B b = new B();
b.bfct();
// Aber B ist auch ein A, darum 'kann' es alles, was A 'kann'
b.afct();
// Ausgabe: afct in A
58
Es gibt viele objektorientierte Sprachen, die auch Mehrfachvererbung unterstützen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 276 / 409
Abb. 14-3 : Vererbungs-Hierarchie des Beispiels
An diesem Beispiel sehen sie, dass die Klasse B alle Funktionen von A erbt, d.h. sie ohne
weitere Schreibarbeit zur Verfügung stehen. Jede Änderung in A wirkt sich damit sofort auch
auf B (und natürlich alle weiteren abgeleiteten Klassen) aus.
Und sie sehen, dass die Klasse B das Verhalten (Methoden) von A erbt, d.h. sie ohne weitere
Schreibarbeit zur Verfügung stehen. Jede Änderung in A wirkt sich damit sofort auch auf B
(und natürlich alle weiteren abgeleiteten Klassen) aus.
14.3 Schlüsselwort super
In jeder Element-Funktion steht das Schlüsselwort super zur Verfügung, das immer für den
Objektanteil der Basisklasse des aktuellen Objektes steht59. Beispiel siehe z. B. Konstruktoren.
14.4 Konstruktoren
Konstruktoren werden in Java nicht vererbt. Dies macht auch keinen Sinn, da ein Konstruktor
immer das erzeugte Objekt initialisieren soll - ein geerbter Konstruktor aber nur den
Objektanteil der Basisklasse initialisieren kann. Sie müssen daher für jede Klasse wieder neu
Konstruktoren erstellen.
Die Konstruktoren der abgeleiteten und der Basis-Klasse sind automatisch miteinander
verkettet, d.h. jeder Konstruktor einer abgeleiteten Klasse ruft als erstes defaultmäßig den
Standard-Konstruktor der Basisklasse auf.
public class A {
public A() {
System.out.println("Konstruktor A");
}
}
public class B extends A {
public B() {
System.out.println("Konstruktor B");
}
}
B b = new B();
Ausgabe
Konstruktor A
Konstruktor B
Sie sehen, dass als erstes automatisch der Standard-Konstruktor der Basisklasse aufgerufen
wird. Wollen sie, dass ein anderer Konstruktor für die Basisklasse benutzt wird, können sie
59
Ähnlich wie this, das immer für das komplette aktuelle Objekt steht (siehe Kapitel 12.4.3).
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 277 / 409
diesen am Anfang des Konstruktors der abgeleiteten Klasse mit super angeben.
public class A {
public A(int i) {
System.out.println("Konstruktor A mit " + i);
}
}
public class B extends A {
public B() {
super(11);
System.out.println("Konstruktor B");
}
}
Ausgabe
Konstruktor A mit 11
Konstruktor B
Wenn die Basisklasse keinen Standard-Konstruktor hat, so müssen sie in den abgeleiteten
Konstruktoren mit super einen Konstruktor angeben.
Sie können in einem Konstruktor mit this auch einen anderen Konstruktor der Klasse
anspringen, der dann einen Konstruktor der Basisklasse aufruft.
Achtung – die Verwendung von überschriebenen (s.u.) Element-Funktionen in Konstruktoren
ist gefährlich, da die Oberklassen-Anteile noch nicht konstruiert sind.
14.5 finalize
Finalize Funktionen (siehe Kapitel 12.3) werden in Java nicht automatisch verkettet. Wenn sie
dies erreichen wollen, so müssen sie in der finalize Funktion die finalize Funktion der
Basisklasse selber aufrufen.
public class B extends A {
protected void finalize() {
super.finalize();
}
}
14.6 Überschreiben
Funktionen von Klassen können in abgeleiteten Klassen überschrieben werden, d.h. sie können
für eine abgeleitete Klasse neu definiert werden (gleicher Name, gleiche Parameterliste).
public class A {
public void fct() {
System.out.println("fct in A");
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 278 / 409
public class B extends A {
public void fct() {
System.out.println("fct in B");
}
}
A a = new A();
a.fct();
B b = new B();
b.fct();
// fct in A
// fct in B
Auf die Art und Weise kann eine nicht passende Implementierung einer Basisklasse in einer
abgeleiteten Klasse neu implementiert, d. h. überschrieben werden. Ein Beispiel wäre die
Methode berechneGehalt in einer Klasse Angestellter und in der abgeleiteten Klasse Vertreter.
Ein Vertreter ist sicherlich auch ein Angestellter, d.h. für ihn gelten die Methode getName,
getPersonalNo(),..., aber das Gehalt wird bei Vertretern oft anders berechnet.
Oft ist es so, dass die Basisklassen Implementierung gar nicht so schlecht ist, aber eben nicht
100 % passt. Vielleicht bekommt der Vertreter zusätzlich zu einem Festgehalt nur noch einen
variablen Anteil hinzu – in diesem Fall wäre die Basisklassen Implementierung ja nicht falsch,
sondern eben nur ein Teil der korrekten Implementierung. Darum ist es oft sinnvoll in einer
Neu-Implementierung auf die Basisklassen Implementierung zurückzugreifen. Hierbei gibt es
zwei ‚reine‘ Formen (Korrektur und Filterung), aber natürlich auch beliebige Mischformen.
14.6.1 Korrektur
Falls das Ergebnis der Basisklassen-Elementfunktion nicht 100% passend ist, kann es in einer
abgeleiteten Klasse korrigiert werden – denken sie an das Vertreter Beispiel von oben.
Prinzip
void f() {
super.f();
// Korrektur
}
// expliziter Aufruf der Original-Elementfunktion
// Korrektur des Ergebnisses
14.6.2 Filterung
Falls die Basisklassen-Elementfunktion nicht alle Fälle (korrekt) behandelt, können diese vorher
abgefangen und behandelt werden.
Prinzip
void f() {
// Filterung
super.f();
}
// Filterung mancher Faelle
// expliziter Aufruf der Original-Elementfunktion
Die Filterung bezieht sich häufig auf die Übergabeparameter.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 279 / 409
14.7 Ist-ein Beziehung
Eine Konsequenz aus der Semantik ‘Vererbung ist eine ist-ein Beziehung’ ist, dass einer
Referenz-Variablen der Basisklasse auch ein Objekt einer abgeleiteten Klasse zugewiesen
werden kann.
// Klasse B ist von A abgeleitet
A a1 = new A();
A a2 = new B();
Dies ist ganz im Sinne der Semantik. Ist-ein heisst, dass für ein B Objekt alles gilt, was für ein
A Objekt gilt - und daher ein B Objekt auch das Interface von A unterstützt.
Bemerkung - falls sie das Ganze etwas verwundert, machen sie sich mal von der ganzen
Computerei frei, und betrachten das Ganze mit einem normalen Beispiel: Wenn sie z.B. auf
einen Stuhl zeigen und sagen „das ist ein Stuhl“, dann wird ihnen wohl niemand widersprechen.
Aber auch die Aussage „das ist ein Möbelstück“ wäre ohne Frage richtig. Und genau das
gleiche passiert hier: Die Referenz-Variable „a2“ sagt mit ihrem statischen Typ „A“ das sie ein
Möbelstück referenziert (darauf zeigt), obwohl sie doch in Wirklichkeit einen Stuhl (ein Objekt
vom Typ „B“) referenziert (darauf zeigt). Aber daran ist nichts falsches und unwahres - sie sagt
nur nicht alles. Aber in vielen Kontexten reicht das. Wir sagen zu unserem Besuch auch „Nimm
dir einen Stuhl“, und lassen offen ob er sich in einen Sessel, die gute Coach oder den normalen
Holzstuhl setzen soll. Warum auch? Im Prinzip würde es sogar reichen zu sagen „Nimm doch
bitte Platz“.
Hinweis - man unterscheidet daher auch in den sogenannten statischen und den dynamischen
Typ.
• Der statische Typ ist der Typ der Referenz-Variablen. Diesen Typ sieht der Compiler, da er
ohne wenn und aber zur Compilezeit feststeht und eindeutig bekannt ist - er steht ja als Typ
an der Definition der Referenz-Variablen. Im Beispiel ist dies der Typ “A“ der ReferenzVariablen „a1“ und „a2“.
• Dem gegenüber ist der dynamische Typ der echte Typ des Objekts, auf das verwiesen wird.
Dieser ist zur Compilezeit nicht zwingend bekannt, und muß nicht dem statischen Typ
entsprechen. Im Beispiel ist der dynamische Typ des von „a1“ referenzierten Objekts „A“,
während es bei „a2“ „B“ ist.
14.8 Polymorphie
Überschreiben und ist-ein-Beziehung zusammen ermöglichen ein Feature, das das
Schlüsselkonzept aller OO Sprachen ist: Polymorphie.
public class A {
public void fct() {
System.out.println("fct in A");
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 280 / 409
}
public class B extends A {
public void fct() {
System.out.println("fct in B");
}
}
A a1 = new A();
A a2 = new B();
a1.fct();
// fct in A
a2.fct();
// fct in B - obwohl ueber eine A Referenz-Variab. aufgerufen
In Java wird der Funktionsaufruf erst zur Laufzeit festgelegt60, und zwar in Abhängigkeit vom
echten Typ des Objekts, und nicht in Abhängigkeit vom Typ der Referenz-Variablen. Dieses
Sprachfeature wird mit Polymorphie bezeichnet.
Mit Polymorphie ist gemeint, dass eine Funktion vielgestaltig ist, d. h. in Abhängigkeit vom
Kontext unterschiedlich (angepasst) reagiert. Genau genommen reagiert natürlich nicht eine
Funktion unterschiedlich, sondern es werden unterschiedliche Funktionen aufgerufen, ohne
dass sich der Entwickler um die echten Objekt-Typen und deren verschiedene FunktionenImplementierungen kümmern muss. Dies ermöglicht es ihm, ähnliche Objekte61 gleich zu
behandeln, ohne Details kennen zu müssen (z.B. welche Klassen es gibt, wie sie heissen, wie
sie zu behandeln sind, usw...).
Hinweis – im ersten Augenblick sieht Polymorphie nicht nach was besonderem aus, sondern
eher nur nach einem kleinen Sprachgag – aber dies ist falsch. Es ist das Schlüsselkonzept
der Objektorientierung. Seine wahre Mächtigkeit erkennt man meist erst in praktischen
Einsätzen, von denen in den weiteren Kapiteln viele folgen werden. Ein kleines Beispiel als
Einstimmung folgt gleich – siehe Kapitel 14.12.
14.9 abstract
Es gibt Situation, in denen eine Basisklasse keine sinnvolle Default-Implementierung für eine
Element-Funktion anbieten kann – ein Beispiel hierfür findet sich u.a. im Beispiel-Kapitel 14.12.
In diesem Fall bekommt die entsprechende Element-Funktion den Modifier abstract, was
bedeutet, dass diese Element-Funktion in dieser Klasse nur deklariert, aber nicht implementiert
wird.
Sobald mindestens eine Element-Funktion in einer Klasse abstract ist (und sei es auch durch
Vererbung – siehe Klasse „B“ im Beispiel), muss auch die Klasse den Modifier abstract
bekommen. In einer tieferen abgleiteten Klasse ohne Modifier abstract muss diese ElementDieses Verhalten wird u.a. auch als dynamische Bindung, späte Bindung oder late binding
bezeichnet.
61 Ähnliche Objekte sind Objekte, die eine gemeinsame Basisklasse (oder in Java auch ein
gemeinsames Interface – siehe todo...) haben.
60
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 281 / 409
Funktion jetzt überschrieben und implementiert werden.
public abstract class A {
public abstract void f();
}
public abstract class B extends A {
}
public abstract class C extends B {
public abstract void f();
}
public class D extends C {
public void f() {
System.out.println("Hallo");
}
}
A a = new D();
a.f();
// Ausgabe: Hallo
Eine abstrakte Klasse kann damit automatisch nicht mehr instanziiert werden – was ja auch
keinen Sinn mehr macht, da sie eine Funktion ohne Implementierung anbietet.
public abstract class A {
public abstract void f();
}
A a = new A();
a.f();
// Fehler – A laesst sich nicht instanziieren, da abstract
// Welche Funktion sollte das dann auch sein??
Eine Klasse darf auch dann abstrakt sein, wenn sie keine abstrakten Funktionen hat.
Typischerweise tritt dieser Fall bei Klassen auf, die keine konkreten Objekte beschreiben,
sondern nur allgemeine Beschreibungen für die Gemeinsamkeiten einer „Objekt-Familie“ sind.
14.10 Casts und instanceof
Mit Casts kann man statische Typen in der Vererbungs-Hierarchie verschieben. Dazu wird der
gewünschte Zieltyp in Klammern vor den Quellausdruck geschrieben. Achtung – dies geht nur,
solange die referenzierten Objekte wirklich solche sind. Ansonsten wird eine Exception
geworfen.
Mit dem Operator „instanceof“ kann abgefragt werden, ob ein Objekt von einem bestimmten
Typ ist.
public class A {
}
public class B extends A {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 282 / 409
public void f() {
System.out.println("B.f");
}
}
A var = new A();
if (var instanceof B) {
System.out.println("Variable var referenziert ein B Objekt");
B b = (B)var;
b.f();
}
else {
System.out.println("Variable var referenziert KEIN B Objekt");
}
var = new B();
if (var instanceof B) {
System.out.println("Variable var referenziert ein B Objekt");
B b = (B)var;
b.f();
}
else {
System.out.println("Variable var referenziert KEIN B Objekt");
}
Ausgabe
Variable var referenziert KEIN B Objekt
Variable var referenziert ein B Objekt
B.f
try {
A a = new A();
B b = (B)a;
b.f();
}
catch (Exception x) {
System.out.println(x);
}
Ausgabe
java.lang.ClassCastException
Achtung – Casts innerhalb einer Vererbungs-Hierarchie sind in einer Sprache mit häufig
untypisierten Schnittstellen62 relativ normal (vergleiche hier z.B. die untypisierten Container, die
wir in Kapitel 9.3 kennen gelernt haben63). Trotzdem sollte ihnen klar sein, dass Casts kein
guter Programmierstil sind, und auf das Notwendigste beschränkt sein sollten.
Noch extremer ist dies mit der Verwendung von „instanceof“. Im Normallfall sollte die Kenntnis
der konkreten Typen hinter einem Basis-Klasse oder einem Interface unnötig sein, da dies z.B.
die Erweiterbarkeit und Wiederverwendbarkeit stark einschränkt. Daher sollte die Benutzung
von „instanceof“ der gut begründete Ausnahmefall bleiben.
Also ganz untypisiert sind sie nicht, sondern typisiert auf „Object“ – siehe Kapitel 14.11. Aber
abgesehen von den elementaren Datentypen sind alle Klassen von „Object“ abgeleitet, so dass
die Typisierung hier nicht wirklich viel hilft.
63 Mit dem JDK 1.5 hat Java Generics bekommen, die u.a. typisierte Container ermöglichen.
Damit werden Casts in Java etwas weniger häufig notwendig, was sicher zu einer
Verbesserung der Progamm-Qualität beitragen wird. Aus Zeitgründen werden wir in der
Vorlesung Generics nicht besprechen.
62
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 283 / 409
14.11 Klasse „java.lang.Object“
Alle Klassen in Java sind immer direkt oder indirekt von der Klasse „Object“ abgeleitet. Wenn
sie keine Basisklasse angeben, wird automatisch „Object“ als Basisklasse angesetzt.
Die Klasse „Object“ beinhaltet allgemeine Methoden, die für jede Klasse sinnvoll sind, z.B.
clone()
equals(Object)
finalize()
getClass()
hashCode()
toString()
Erzeugt eine flache Kopie des Objekts. Dazu muss das Objekt das
Interface Cloneable implementieren und diese Funktion überschreiben.
Arrays sind immer kopierbar.
Vergleicht zwei Objekte auf Identität, d.h. Referenzgleichheit. Für
Objektvergleiche, d.h. tiefe Vergleiche bzw. ein spezielles
Vergleichsverhalten muss diese Funktion überschrieben werden.
Die normale finalize Methode
Gibt die ‘Meta-Klasse’ zum Objekt zurück.
Gibt einen Hash-Wert für das Objekt zurück.
Gibt das Objekt in einer Text-Repräsentation zurück. Diese Funktion wird
automatisch z.B. bei Ausgaben auf die Console oder bei Wandlungen in
einen String aufgerufen. Siehe auch Kapitel 14.11.1.
Wenn sie die Funktionen für ihre Klassen anpassen wollen bzw. müssen, d.h. die geerbte
Funktionalität nicht ausreicht, müssen sie sie überschreiben.
Hinweis – die Funktionen „equals“ und „hashCode“ sind nicht ganz unabhängig voneinander.
wenn sie die Equals-Funktion überschreiben, müssen sie auch die HashCode-Funktion
entsprechend überschreiben. Näheres hierzu finden sie z.B. in der offiziellen Java-Doku oder
vielen Büchern. Aus Zeitmangel wird dies in der Vorlesung nicht besprochen.
Achtung – das automatische Erben von „Object“ gilt nicht für Interfaces (Kapitel 14.13),
sondern nur für Klassen. Da „Object“ eine Klasse ist, können Interfaces nicht von ihr erben,
d.h. Klassen haben immer genau eine absolute Basisklasse - das ist „Object“. Interfaces sind
da anders.
14.11.1 Element-Funktion „Object.toString“
Ich möchte hier noch mal besonders auf die Funktion „toString“ hinweisen. Sie wird immer dann
automatisch aufgerufen, wenn eine Wandlung von einem Objekt in einen String notwendig ist.
Dies geschieht:
• Bei der Ausgabe eines Objekts auf der Console mit „System.out.print“ bzw.
„System.out.println“.
• Bei der Verkettung eines Strings mit einem Objekt mit dem Plus-Operator.
Da die Klasse „java.lang.Object“ eine Default-Implementierung anbietet, kann jedes Objekt
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 284 / 409
immer ausgegeben bzw. in einen String gewandelt werden. Achtung – die DefaultImplementierung von Object liefert keinen besonders sinnvolle Text-Repräsentation, wie auch?
public class A {
}
public class Appl {
public static void main(String[] args) {
A a = new A();
System.out.println(a);
String s = "String: " + a;
System.out.println(s);
}
}
mögliche Ausgabe
A@119c082
String: A@119c082
Mit dem Überschreiben der Funktion „toString“ legen sie das Ergebnis der Wandlung fest.
public class A {
public String toString() {
return "Ich bin ein A-Objekt";
}
}
public class Appl {
public static void main(String[] args) {
A a = new A();
System.out.println(a);
String s = "String: " + a;
System.out.println(s);
}
}
Ausgabe
Ich bin ein A-Objekt
String: Ich bin ein A-Objekt
14.11.2 Object als Basistyp
Jede Klasse ist in Java direkt oder indirekt von „java.lang.Object“ abgeleitet. Daher kann jedes
Objekt in Java immer einer Referenz-Variablen vom statischen Typ „Object“ zugewiesen
werden. Beispiele:
Object o1 = new java.util.TreeMap();
Object o2 = new StringBuffer();
Object o3 = new javax.swing.JFrame();
Von daher ist „Object“ der kleinste gemeinsame Nenner aller Objekte – wir lassen die
elementaren Datentypen mal außen vor. Und von daher werden in Java viele Funktionen auf
„Object“ typisiert, zum Beispiel die Funktionen der Container-Klassen aus Kapitel 9.3. Daher
werden in der Praxis häufig Up-Casts in der Klassen-Hierarchie (siehe Kapitel 14.10) benötigt –
siehe auch die Beispiele der Container-Klassen in Kapitel 9.3.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 285 / 409
14.12 Anwendung – Beispiel „Obstkorb“
14.12.1 Aufgabe
Nehmen wir an, sie wollen einen Obstkorb implementieren:
• Ein Obstkorb soll einfach mehrere Früchte verschiedener Obstsorten aufnehmen können.
• Jede Frucht hat einen Namen.
• Auch der Obstkorb hat einen Namen.
• Außerdem soll der Obstkorb einen Konsolen Ausgabe folgender Form haben:
• Name vom Obstkorb
• Anzahl der Früchte im Obstkorb
• Darstellung alle Früchte – alphabetisch sortiert nach dem Namen der Frucht
• Die Darstellung einer Frucht besteht aus Name und Obstsorte.
• Für den Anfang begnügen wir uns mit den zwei Obstsorten „Apfel“ und „Birne“.
Hier eine mögliche Beispiel-Ausgabe eines Obstkorbs mit 5 Früchten:
Gewünschte Ausgabe – wenn denn der Obstkorb fertig wäre...
Ich bin der Obstkorb "Geschenk" und enthalte 5 Fruechte:
- Bauchiger Adler (Birne)
- Dickes Schwein (Apfel)
- Fetter Kohl (Birne)
- Gruener Baum (Apfel)
- Saftiger Schmatz (Apfel)
Vorgehen – um zu sehen, wie uns Vererbung, „ist-ein“-Beziehung und Polymorphie hier helfen,
werden wir das Programm erstmal ohne diese Sprachmittel implementieren, und dann Stück für
Stück Sprachmittel für Sprachmittel nutzen, und dann hoffentlich sehen wie sie uns das
Programmierer-Leben erleichtern.
Bemerkung – wem ein Obstkorb mit Früchten zu abstrakt oder zu gesund ist, der möge sich
statt dessen eine Angestellten-Verwaltungs-Software für Arbeiter und Vertriebler vorstellen,
oder eine Flughafen-Dispostions-Verwaltung für Flugsteige und Tankwagen, oder oder oder...
14.12.2 Lösung 1 – ohne Vererbung und ohne Polymorphie
Zuerst brauchen wir Klassen für Äpfel und Birnen, die den Namen halten und sich selber
entsprechend der Aufgabenstellung darstellen können.
public class Apple {
private String name;
public Apple(String n) {
name = n;
}
public void print() {
System.out.println("- " + name + " (Apfel)");
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 286 / 409
public String getName() {
return name;
}
}
public class Pear {
private String name;
public Pear(String n) {
name = n;
}
public void print() {
System.out.println("- " + name + " (Birne)");
}
public String getName() {
return name;
}
}
Bevor wir zum Obstkorb kommen – dem eigentlichen Knackpunkt des Programms –
implementieren wir die „main“ Funktion – und bekommen damit implizit die SchnittstellenDefinition vom Obstkorb.
public class Appl {
public static void main(String[] args) {
FruitBasket fb = new FruitBasket("Geschenk");
fb.insert(new Apple("Dickes Schwein"));
fb.insert(new Pear("Fetter Kohl"));
fb.insert(new Apple("Saftiger Schmatz"));
fb.insert(new Apple("Gruener Baum"));
fb.insert(new Pear("Bauchiger Adler"));
fb.print();
}
}
Dann brauchen wir den Obstkorb selber, und das wird schwieriger. Da aber die Schnittstelle
aus dem „main“ automatisch heraus fällt, fangen wir damit an:
public class FruitBasket {
private String name;
public FruitBasket(String n) {
name = n;
}
public void insert(Apple apple) {
...
}
public void insert(Pear pear) {
...
}
public void print() {
...
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 287 / 409
}
Jetzt stellt sich die Frage, wie wir Äpfel und Birnen im Obstkorb speichern können – am besten
schon alphabetisch sortiert. Für die dynamische Speicherung von Objekten haben wir in Kapitel
todo mehrere Container kennengelernt. U.a. gab es dabei auch eine Klasse „TreeMap“, die
über einen Schlüssel (hier bei uns der Name) alphabetisch sortiert.
Hinweis – in der Praxis würde man hierfür natürlich niemals einen assoziativen Container (d.h.
einen mit Schlüssel/Wert Paaren) nehmen, sondern einen Container, der die Sortierung
automatisch auf den Objekten selber vornimmt. Den gibt es in Java natürlich auch, z.B. mit der
Klasse „TreeSet“ in „java.util“, aber a) kennen wir ihn nicht, und b) müßten wir für seinen
Gebrauch wissen, wie man unsere Äpfel und Birnen sortierbar bekommt, d.h. wir müßten mit
dem Interfaces „Comparable“ arbeiten oder einen eigenen Comparator implementieren. Für
beides ist es noch etwas früh – von daher nehmen wir hier die schlechtere Lösung mit der
Klasse „TreeMap“.
Da wir aber noch ohne Vererbung und „ist-ein“ Beziehung arbeiten, können wir Äpfel und
Birnen nicht in einer TreeMap speichern64. Also geben wir der Obstkorb-Klasse für jeden
Obsttyp eine eigene TreeMap.
import java.util.TreeMap;
public class FruitBasket {
private String name;
private TreeMap apples = new TreeMap();
private TreeMap pears = new TreeMap();
public FruitBasket(String n) {
name = n;
}
public void insert(Apple apple) {
apples.put(apple.getName(), apple);
}
public void insert(Pear pear) {
pears.put(pear.getName(), pear);
}
public void print() {
int count = apples.size();
count += pears.size();
System.out.println("Ich bin der Obstkorb \"" + name + "\" und enthalte "
+ count + " Fruechte:");
...
}
}
Als Problem bleibt jetzt nur noch die Ausgabe der Früchte – damit sie über alle Füchte
Okay, in Java geht es schon, da beide implizit von java.lang.Object abgeleitet sind, und
TreeMap mit Objects arbeitet. Aber letztlich wäre das dann auch nur die Verwendung von
Vererbung und „ist-ein“ Beziehung – und das wollen wir ja noch nicht.
64
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 288 / 409
alphabetisch ist, müssen beide TreeMaps parallel durchlaufen werden und die jeweils kleinere
Frucht (bezogen auf den Namen) muß ausgegeben werden. Das klingt kompliziert – gerade
wenn man an später mit noch mehr Obstsorten und noch mehr TreeMaps denkt – daher sehe
ich hier von einer Lösung ab und überlasse diese dem Studenten ;-)
14.12.3 Lösung 2 – immer noch ohne Vererbung und ohne Polymorphie
Wenn Lösung 1 bei der Ausgabe zu kompliziert ist, man aber keine Vererbung und
Polymorphie zur Verfügung hat – was macht man dann? Nun, wenn man in Java ein Problem
hat, dann macht man eine Klasse daraus.
Das Problem ist hier, dass wir Äpfel und Birnen gleichzeitig verwalten wollen, dass aber noch
nicht können bzw. hier mehr wollen. Also schreiben wir eine Klasse, die das für uns macht –
und da sie Früchte verwaltet, nennen wir sie „Fruit“.
public class Fruit {
private Apple apple = null;
private Pear pear = null;
public Fruit(Apple a) {
apple = a;
}
public Fruit(Pear p) {
pear = p;
}
public String getName() {
if (apple!=null) {
return apple.getName();
}
return pear.getName();
}
public void print() {
if (apple!=null) {
apple.print();
return;
}
pear.print();
}
}
Die Klassen „Apple“, „Pear“ und „Appl“ sind von dieser Änderung nicht betroffen, nur der
Obstkorb selber, da er jetzt intern diese Hilfsklasse „Fruit“ benutzt.
import java.util.TreeMap;
import java.util.Iterator;
public class FruitBasket {
private String name;
private TreeMap fruits = new TreeMap();
public FruitBasket(String n) {
name = n;
}
public void insert(Apple apple) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 289 / 409
fruits.put(apple.getName(), new Fruit(apple));
}
public void insert(Pear pear) {
fruits.put(pear.getName(), new Fruit(pear));
}
public void print() {
int count = fruits.size();
System.out.println("Ich bin der Obstkorb \"" + name + "\" und enthalte "
+ count + " Fruechte:");
Iterator i = fruits.values().iterator();
while (i.hasNext()) {
Fruit f = (Fruit)i.next();
f.print();
}
}
}
Bevor sie weitergehen zu Lösung 3, schauen sie sich Lösung 2 ruhig mal in Ruhe an. Wir
haben hier einen wichtigen Grundsatz von Java kennen gelernt, und sehen ein großes Problem
bisheriger Programmierung.
Der Grundsatz ist einfach: „Haben sie ein Problem, machen sie eine Klasse draus“. Im
Prinzip ist dies der alte Grundsatz der Software-Entwicklung „Es gibt kein Problem, das sich
nicht durch eine weitere Indirektion kleiner machen läßt“ in neuen Gewändern, nämlich dem
OO-Kleid.
Ein großes Problem unseres kleinen Programms ist die Wartbarkeit: immer wenn wir hier eine
neue Obstsorte einführen, müssen wir neben einer weiteren Klasse in unserem kleinen
Programm schon mehrere Code-Stellen mit ändern:
• Die Klasse „FruitBasket“ braucht eine weitere „insert“ Funktion.
• Und die Klasse „Fruit“ muss quasi überall angepaßt werden.
Und wir haben ja nur ein kleines Programm. Und dass es so relativ wenig lokalisierte Stellen
sind, liegt eigentlich schon daran, dass wir die fast gesamte Logik für die Obstsorten in der
Klasse „Fruit“ gesammelt haben.
In einem wirklich großen ernsthaften Programm würde bei einer klassischen Programmierung –
wie wir sie hier haben – das Ändern (Einfügen, Löschen, Modifizieren) z.B. einer FlughafenRessource oft an tausenden von Stellen Code-Änderungen nach sich ziehen. Ein horrender
Aufwand, bei dem man sich nie wirklich ganz sicher sein kann, alle relevanten Stellen
berücksichtigt zu haben.
14.12.4 Lösung 3 – mit Vererbung, aber noch ohne Polymorphie
Im Prinzip hat sich eine bessere Lösung schon die ganze Zeit aufgedrängt, aber wir haben sie
bewußt nicht eingesetzt. Mit Vererbung und den Konsequenzen über „ist-ein“ Beziehung sind
einige Dinge viel einfacher – wir können jetzt an vielen Stellen Äpfel und Birnen gleich
behandeln.
Die Klasse „Fruit“ bekommt hier einen ganz anderen Zweck: statt fast alle Probleme zu kapseln
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 290 / 409
mutiert sie zu einer sehr einfachen Basisklasse, die – da sie nicht instanziierbar sein soll –
abstrakt ist:
public abstract class Fruit {
}
Am Beispiel des Apfels schauen wir uns den kleinen Unterschied in der Klassen-Definition der
Obstsorten an – er besteht nur aus der Angabe der Basisklasse und dem Schlüsselwort
„extends“.
public class Apple extends Fruit {
private String name;
public Apple(String n) {
name = n;
}
public void print() {
System.out.println("- " + name + " (Apfel)");
}
public String getName() {
return name;
}
}
Und wie sieht der Obstkorb jetzt aus?
import java.util.TreeMap;
import java.util.Iterator;
public class FruitBasket {
private String name;
private TreeMap fruits = new TreeMap();
public FruitBasket(String n) {
name = n;
}
public void insert(Apple apple) {
fruits.put(apple.getName(), apple);
}
public void insert(Pear pear) {
fruits.put(pear.getName(), pear);
}
public void print() {
int count = fruits.size();
System.out.println("Ich bin der Obstkorb \"" + name
+ "\" und enthalte " + count + " Fruechte:");
Iterator i = fruits.values().iterator();
while (i.hasNext()) {
Fruit f = (Fruit) i.next();
if (f instanceof Apple) {
Apple a = (Apple) f;
a.print();
continue;
}
if (f instanceof Pear) {
Pear p = (Pear) f;
p.print();
continue;
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 291 / 409
}
}
}
Unsere TreeMap kann jetzt direkt alle Früchte ohne Wrapper-Klasse aufnehmen – siehe
Element-Funktionen „insert“. Und die Fall-Unterscheidung zwischen Äpfeln und Birnen findet
sich nur noch in der Schleife der Obstkorb-Ausgabe.
14.12.5 Lösung 4 – mit Vererbung, aber immer noch ohne Polymorphie
Als wir Vererbung eingeführt haben, haben wir noch nicht über Polymorphie gesprochen,
sondern statt dessen den Vorteil herausgestellt, dass gemeinsame Funktionen in eine
Basisklasse gelegt werden, und abgeleitete Klassen diese einfach erben. Diesen Vorteil
können wir hier auch nutzen, in dem wir das Namens-Handling von Äpfeln und Birnen in die
Basisklasse „Fruit“ verschieben – hier am Beispiel von „Apple“ verdeutlicht.
public abstract class Fruit {
private String name;
public Fruit(String n) {
name = n;
}
public String getName() {
return name;
}
}
public class Apple extends Fruit {
public Apple(String n) {
super(n);
}
public void print() {
System.out.println("- " + getName() + " (Apfel)");
}
}
Dies hat den Nebeneffekt, dass sich im Obstkorb die beiden „insert“ Element-Funktionen zu
einer zusammenfassen lassen:
public void insert(Fruit fruit) {
fruits.put(fruit.getName(), fruit);
}
14.12.6 Lösung 5 – mit Vererbung und mit Polymorphie
Als unschöne Stelle im Programm bleibt die Ausgabe-Funktion des Obstkorbs über.
public void print() {
int count = fruits.size();
System.out.println("Ich bin der Obstkorb \"" + name
+ "\" und enthalte " + count + " Fruechte:");
Iterator i = fruits.values().iterator();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 292 / 409
while (i.hasNext()) {
Fruit f = (Fruit) i.next();
if (f instanceof Apple) {
Apple a = (Apple) f;
a.print();
continue;
}
if (f instanceof Pear) {
Pear p = (Pear) f;
p.print();
continue;
}
}
}
Sie sieht kompliziert und fehlerträchtig aus, und sie ist wartungsintensiv. Das Problem hier ist,
dass wir Objekte unterschiedlicher Klassen in die Hand bekommen und wir jeweils die
entsprechende Element-Funktion der jeweiligen Klasse benutzen wollen. Aber genau das liefert
uns doch die Polymorphie!
Wir müssen in der Klasse „Fruit“ nur die entsprechende „print“ Funktion zur Verfügung stellen,
und sie dann in den abgeleiteten Klassen überschreiben. Und da wir für „print“ in „Fruit“ keine
sinnvolle Default-Implementierung kennen, machen wir die Funktion „abstract“.
public abstract class Fruit {
private String name;
public Fruit(String n) {
name = n;
}
public String getName() {
return name;
}
public abstract void print();
}
Für die Obst-Klassen „Apple“ und „Pear“ ändert sich gar nichts. Aber die Ausgabe-Funktion des
Obstkorbs ist auf einmal ganz einfach:
public void print() {
int count = fruits.size();
System.out.println("Ich bin der Obstkorb \"" + name
+ "\" und enthalte " + count + " Fruechte:");
Iterator i = fruits.values().iterator();
while (i.hasNext()) {
Fruit f = (Fruit) i.next();
f.print();
}
}
Und sie enthält keine Abhängigkeiten auf die verwendeten Obstsorten mehr – auf einmal ist sie
ganz allgemeingültig.
14.12.7 Erweiterte Aufgabe
Lassen sie sich das mal auf der Zunge zergehen. In unserem ganzen Programm gibt es fast
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 293 / 409
keine Abhängigkeiten mehr auf die verwendeten Obstsorten. Was heißt das für uns, wenn wir
z.B. eine neue Sorte „Bananen“ einführen wollen?
Zuerst brauchen wir in Anlehnung an „Apple“ und „Pear“ eine weitere Klasse „Banana“. Die
neue Klassen ist vollkommen unabhängig vom restlichen Programm, und wir können sie
einfach hinzufügen:
public class Banana extends Fruit {
public Banana(String n) {
super(n);
}
public void print() {
System.out.println("- " + getName() + " (Banane)");
}
}
Und in „main“ benutzen wir sie einfach:
public static void main(String[] args) {
FruitBasket fb = new FruitBasket("Geschenk");
fb.insert(new Apple("Dickes Schwein"));
fb.insert(new Pear("Fetter Kohl"));
fb.insert(new Apple("Saftiger Schmatz"));
fb.insert(new Apple("Gruener Baum"));
fb.insert(new Pear("Bauchiger Adler"));
fb.insert(new Banana("Krumme Wurst"));
fb.insert(new Banana("Langer Lulatsch"));
fb.print();
}
Ausgabe
Ich bin der Obstkorb "Geschenk" und enthalte 7 Fruechte:
- Bauchiger Adler (Birne)
- Dickes Schwein (Apfel)
- Fetter Kohl (Birne)
- Gruener Baum (Apfel)
- Krumme Wurst (Banane)
- Langer Lulatsch (Banane)
- Saftiger Schmatz (Apfel)
Und ansonsten müssen wir nichts machen – das Programm funktioniert einfach!
Unser eigentliches Programm – hier die Klasse „Obstkorb“ – arbeitet nur auf der Basisklasse,
und muß – dank Polymorphie – die konkreten Klassen nicht kennen, und ist daher vollkommen
unabhängig. Das Programm läßt sich so sehr leicht verändern oder erweitern.
14.13 Interfaces
Interfaces sind spezielle Java-Klassen:
• Sie sind quasi eine auf die Spitze getriebene abstrakte Klasse.
• Sie werden mit dem Schlüsselwort interface deklariert.
• Sie können nur abstrakte Funktionen und Klassen-Variablen enthalten,
d.h. sie enthalten weder Implementationen noch Attribute. Häufig enthalten sie nur KlassenKonstanten – siehe Kapitel 12.5.2.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
•
•
•
•
•
•
Seite 294 / 409
Sie haben keine implizite Basisklasse.
Sie müssen keine Funktionen enthalten, d.h. sie dürfen auch leer sein.
Von Interface’s werden Klassen mit dem Schlüsselwort implements abgeleitet.
Eine Klasse kann beliebig viele Interface’s implementieren.
Es können Referenz-Variablen vom Typ des Interfaces definiert werden.
Interfaces benötigen – als quasi spezielle Klassen – ihre eigene Quelltext-Datei, die natürlich
so heißen muß wie das Interface.
public interface Inter {
public void fct();
}
public class Imple implements Inter {
public void fct() {
System.out.println("In fct() von Impl");
}
}
Inter in = new Imple();
in.fct();
Interface’s sind quasi das Java-Sprachmittel für Mehrfachvererbung, wobei eine Klasse nur
über die extends Schiene eine Implementierung erben kann.
Hinweis – in Java ist es möglich, dass ein Interface komplett leer ist. Mit dem Implementieren
eines solchen Interfaces bekommt eine Klasse quasi ein Flag, dass irgendein Verhalten
gewünscht ist, okay ist, nicht sein soll, oder was auch immer. Ein Beispiel hierfür ist das
Interface „Serializable“, mit dem eine Klasse serialisierbar wird – siehe Kapitel todo.
14.14 Modul-Entkopplung
Vererbung und Polymorphie sind auch ein Mittel um Module voneinander zu entkoppeln, d.h.
die Module unabhängig voneinander zu machen. Schauen wir uns hierzu mal ein Beispiel an:
Das Benutzer-Interface unterliegt in der Praxis oft häufigeren Änderungen. Damit Änderungen
im Benutzer-Interface nicht Änderungen im gesamten Programm nach sich ziehen, versucht
man die eigentliche Programm-Logik und das Benutzer-Interface in Schichten aufzuteilen65.
Abb. 14-4 : 2 Schicht-Architektur eines Programms
In einer solchen Architektur gibt es eine klare Abhängigkeits-Beziehung: hier kennt das UI66 die
In der Praxis findet man häufig drei Schichten oder mehr. Bei einer 3 Schicht-Architektur
werden die Schichten für das Benutzer-Interface und die Programm-Logik meist noch um eine
Schicht zur Daten-Haltung ergänzt.
66 UI – User-Interface, d.h. Benutzer-Interface
65
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 295 / 409
BL67, aber umgekehrt nicht! Die BL stellt Dienste zur Verfügung, die vom UI benutzt werden
können. Die BL kennt aber kein konkretes UI, und darf daher auch keine Benutzer-Interaktion
ausführen!
Abb. 14-5 : Abhängigkeit in einer 2 Schicht-Architektur
Was ist aber nun, wenn z.B. die BL während der Arbeit Eingaben benötigt, z.B. einen
Dateinamen, einen Parameter, ein Passwort oder sonstwas? Sie darf ja keine BenutzerInteraktion ausführen, und kann das UI auch nicht dazu auffordern (da sie es nicht kennt)68.
Was macht man dann?
Formulieren wir das Problem etwas anders, vielleicht fällt dann die Lösung leichter: Die BLSchicht darf nicht selber Benutzer-Interaktion machen, d.h. muss sie dies indirekt machen, z.B.
mittels eines Funktions-Aufrufs. Da sie die-UI Schicht nicht kennt, kann sie dort keine Funktion
direkt aufrufen. Sie muss also eine Funktion aufrufen, die in der BL-Schicht bekannt ist, aber in
einer unbekannten UI-Schicht ausgeführt wird.
Sehen sie die Lösung?
• In der BL-Schicht muss eine Funktion bekannt sein, damit die BL-Schicht sie nutzen kann,
aber sie kann in der BL-Schicht nicht implementiert sein.
• In der UI-Schicht muss genau diese Funktion dann implementiert sein, und sie muss quasi
über die BL-Funktion aufrufbar sein.
Das klingt doch wie Vererbung und Polymorphie:
• In der BL-Schicht wird ein Interface benötigt, dass die Parameter-Hol Funktion definiert.
• In der UI-Schicht muss sich eine UI-Klasse von diesem Interface ableiten und die
Parameter-Hol Funktion implementieren.
• Z.B. beim Aufruf der BL-Schicht könnte jetzt das Objekt, das das BL-Interface implementiert,
mitgegeben werden.
Abb. 14-6 : Design der Modul-Entkopplung
Und hier das Ganze als Quelltext:
package bl;
public interface GetUserParameterInterface {
public String get();
}
package bl;
public class BusinessLogic {
BL – Business-Logic, d.h. Geschäfts- oder Programm-Logik
In vielen Programm findet man hier dann häufig Code, der die Schichtung mit ihrer sauberen
Abhängigkeit durchbricht, und alle Ansätze zur Trennung und Modularisierung ad-acta legt.
Dies kann natürlich nicht Sinn der Sache sein.
67
68
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 296 / 409
public String doit(GetUserParameterInterface gupi) {
String parameter = gupi.get();
return "\"" + parameter + "\"";
}
}
package ui;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import bl.BusinessLogic;
import bl.GetUserParameterInterface;
public class UserInteraction implements GetUserParameterInterface {
private BusinessLogic bl = new BusinessLogic();
public void doit() {
System.out.println("Starte Verarbeitung...");
String s = bl.doit(this);
System.out.println("Ergebnis: " + s);
}
public String get() {
try {
System.out.println("Bitte geben sie einen String ein:");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
return reader.readLine();
}
catch (Exception x) {
}
return "";
}
}
import ui.UserInteraction;
public class Appl {
public static void main(String[] args) {
UserInteraction ui = new UserInteraction();
ui.doit();
}
}
mögliche Ausgabe
Starte Verarbeitung...
Bitte geben sie einen String ein:
Hallo Welt
Ergebnis: "Hallo Welt"
Hinweis – um die Trennung zwischen BL- und UI-Schicht sauber darzustellen, sind die
entsprechenden Klassen (bzw. Interfaces) in entsprechende Packages „bl“ und „ui“ eingefügt.
Bemerkung – ein weiteres, vielleicht etwas einsichtigeres Beispiel findet sich in der
Praktikums-Aufgabe 14.16.2.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 297 / 409
14.15 Fazit
Wenn sie Vererbung und Polymorphie einsetzen, dann hat das Hinzufügen oder Entfernen oder
Verändern einer Klasse fast keine Auswirkungen auf ihr eigentliches Programm –
ausgenommen sind die Stellen an denen die Objekte erzeugt werden, aber auch da kann man
sich helfen.
Immer wenn sie es schaffen ihr Programm, ihre Programm-Struktur oder ihre ProgrammArchitektur auf Baisklassen (oder Interfaces) aufzubauen, dann haben sie gewonnen. Sie
haben leichtes Spiel mit Veränderungen, ohne dass sie ihren ganzen Code nach Änderungen
durchforsten müssen.
Vererbung und Polymorphie sind die Schlüsselkonzepte von OO!
14.16 Aufgaben
14.16.1 Aufgabe „Obstkorb“
Implementieren sie das Obstkorb Programm aus Kapitel 14.12.6. Erweitern sie es um eine
Klasse für Zitronen (Englisch „lemon“).
Lösung siehe Kapitel 14.17.
14.16.2 Aufgabe „ProgressBar“
Ein typisches Problem der Art von Kapitel 14.14 („Modul-Entkopplung“) ist eine FortschrittsAnzeige („ProgressBar“). In der BL Schicht findet eine Verarbeitung statt, die längere Zeit
braucht. Damit der Benutzer eine Rückkopplung bekommt, soll eine Fortschritts-Anzeige
aufgeblendet werden. Nur, die BL-Schicht kennt keine UI-Schicht und weiss auch nicht, was für
eine Fortschritts-Anzeige im jeweiligen UI-Kontext sinnvoll ist. Daher bietet sich eine
Entkopplung über ein Interface an.
• Denken sie sich eine einfache Verarbeitung in der BL-Schicht aus, die etwas Zeit kostet.
Z.B. eine einfache Schleife mit einem „Thread.Sleep“ (siehe Kapitel 11.3.1).
• Schreiben sie ein erstes kleines Programm ohne Fortschritts-Anzeige mit BL- und UISchicht.
• Definieren sie ein Interface, mit dem eine Fortschritts-Anzeige betrieben werden kann.
• Implementieren sie eine Fortschritts-Anzeige.
• Integrieren sie Interface und Fortschritts-Anzeige in ihr Programm.
• Implementieren sie eine weitere Fortschritts-Anzeige. Zeigen sie durch einen einfachen
Austausch der Fortschritts-Anzeigen dass die BL-Schicht mit beiden betrieben werden
kann, ohne dass die BL-Schicht betroffen ist (d.h. geändert werden muss).
• Mögliche Fortschritts-Anzeigen auf Konsolen-Ebene wären z.B.:
• Ausgabe, die von „0 %“ bis „100 %“ hochzählt.
• Liniengrafik mit z.B. dem Zeichen „|“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 298 / 409
Lösung siehe Kapitel 14.18.
14.16.3 Aufgabe „Kontaktdaten 3“
Erweitern sie die kleine Kontaktdaten-Verwaltung aus Kapitel 11.5.3. Gegenüber der Aufgabe
11.5.3 soll dieses Programm aber mehrere Arten von Kontakten verwalten können:
• Single – entspricht der Person aus der alten Aufgabe.
Besteht aus:
• Vorname
• Nachname
• Telefon (als String)
• Beschreibung
Suchen über Vor- und Nachname
• Paar
Besteht aus:
• Vorname 1
• Vorname 2
• Nachname (gemeinsamer)
• Telefon (als String)
Suchen über beide Vornamen und den gemeinsamen Nachname
• Firmenkontakt
• Firmenname
• Ansprechpartner
• Vorname
• Nachname
• Position
• Telefon (als String)
Suchen über Firmenname, Vor- und Nachname des Ansprech-Partners
Lösung siehe Kapitel 14.19.
14.16.4 Aufgabe „Kontaktdaten 4“
Trennen sie die Kontaktdaten-Verwaltung aus Kapitel 14.20 in ihre groben Strukturen auf, und
packen sie diese in eigene Packages. Besonders wichtig ist hier die vollständige Trennung
von Benutzer-Ein- und -Ausgabe und der eigentlichen Programm-Logik. Damit können wir
später problemlos eine grafische Oberfläche auf die Programm-Logik aufsetzen.
Lösung siehe Kapitel 14.20.
14.16.5 Aufgabe „Tic-Tac-Toe 2“
Bauen sie das Tic-Tac-Toe Programm aus Kapitel 12.8.3 so um, dass Vererbung und
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 299 / 409
Polymorphie benutzt wird. Welche Klasse bieten sich im Tic-Tac-Toe dafür an? Wo können sie
das Programm dadurch vereinfachen?
Lösung siehe Kapitel 14.21.
14.16.6 Aufgabe „Tic-Tac-Toe 3“
Nach dem Umbau von Kapitel 14.16.5 sollte es möglich sein, dass Tic-Tac-Toe so zu erweitern,
dass der Benutzer am Anfang die Spieler dynamisch festlegen kann. D.h. implementieren sie
eine Abfrage, mit der der Benutzer definiert, ob Spieler 1 bzw. Spieler 2 jeweils ein Mensch
oder der Computer ist.
Lösung siehe Kapitel 14.22.
14.16.7 Aufgabe „Tic-Tac-Toe 4“
Implementieren sie einen weiteren einfachen Computer-Spieler, der einfach das Feld per Zufall
festlegt. Integrieren sie den Spieler in das Programm und die Spieler-Auswahl von Kapitel
14.16.6. Sehen sie, wie einfach die Erweiterung des Spiels um einen weiteren Spieler
geworden ist?
Lösung siehe Kapitel 14.23.
14.16.8 Aufgabe „Tic-Tac-Toe 5“
Trennen sie das Tic-Tac-Toe aus Kapitel 14.16.7 in seine groben Strukturen auf, und packen
sie diese in eigene Packages. Besonders wichtig ist hier die vollständige Trennung von
Benutzer-Ein- und Ausgabe und der eigentlichen Programm-Logik. Immerhin wollen wir ja auch
das Tic-Tac-Toe später noch mit einer grafischen Benutzeroberfläche versehen, aber das Spiel
nicht neuschreiben müssen sondern die gesamte Logik wiederverwenden können.
Lösung siehe Kapitel 14.24.
14.17 Lsg. zu Aufgabe „Obstkorb“ – Kap. 14.16.1
todo...
14.18 Lsg. zu Aufgabe „ProgressBar“ – Kap. 14.16.2
todo...
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 300 / 409
14.19 Lsg. zu Aufgabe „Kontaktdaten 3“ – Kap. 14.16.3
Noch ohne Erläuterungen – todo...
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Keyboard {
private static InputStreamReader isr = new InputStreamReader(System.in);
private static BufferedReader reader = new BufferedReader(isr);
public static String readString() {
try {
return reader.readLine();
}
catch (Exception x) {
}
return "";
}
}
public interface Contact {
public boolean find(String pattern);
public void input();
public void print();
}
public class Single implements Contact {
private String forename = "";
private String surename = "";
private String phone = "";
public boolean find(String pattern) {
return forename.startsWith(pattern) || surename.startsWith(pattern);
}
public void input() {
System.out.print("Vorname: ");
forename = Keyboard.readString();
System.out.print("Nachname: ");
surename = Keyboard.readString();
System.out.print("Telefon: ");
phone = Keyboard.readString();
}
public void print() {
System.out.print("- Single: " + forename + " " + surename);
if (phone.length()!=0) {
System.out.print(" - Tel: " + phone);
}
System.out.println();
}
}
public class Pair implements Contact {
private
private
private
private
String
String
String
String
fame1 = "";
fname2 = "";
surename = "";
phone = "";
public boolean find(String pattern) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 301 / 409
return fname1.startsWith(pattern) || fname2.startsWith(pattern) ||
surename.startsWith(pattern);
}
public void input() {
System.out.print("Vorname 1: ");
fname1 = Keyboard.readString();
System.out.print("Vorname 2: ");
fname2 = Keyboard.readString();
System.out.print("Nachname: ");
surename = Keyboard.readString();
System.out.print("Telefon: ");
phone = Keyboard.readString();
}
public void print() {
System.out.print("- Paar: " + fname1 + " & " + fname2 + " " + surename);
if (phone.length()!=0) {
System.out.print(" - Tel: " + phone);
}
System.out.println();
}
}
public class Company implements Contact {
private
private
private
private
private
String
String
String
String
String
name = "";
fn = "";
sn = "";
position = "";
phone = "";
public boolean find(String p) {
return name.startsWith(p) || fn.startsWith(p) || sn.startsWith(p);
}
public void input() {
System.out.print("Firmen-Name: ");
name = Keyboard.readString();
System.out.println("Ansprechpartner");
System.out.print("- Vorname: ");
fn = Keyboard.readString();
System.out.print("- Nachname: ");
sn = Keyboard.readString();
System.out.print("- Position: ");
position = Keyboard.readString();
System.out.print("- Telefon: ");
phone = Keyboard.readString();
}
public void print() {
System.out.print("- Firma " + name + " - Ap: " + fn + " " + sn);
if (position.length()!=0) {
System.out.print(" - Pos: " + position);
}
if (phone.length()!=0) {
System.out.print(" - Tel: " + phone);
}
System.out.println();
}
}
import java.util.ArrayList;
import java.util.Iterator;
public class Contacts {
private ArrayList contacts = new ArrayList();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 302 / 409
public void add(Person p) {
contacts.add(p);
}
public void find(String pattern) {
Iterator i = contacts.iterator();
while (i.hasNext()) {
Contact c = (Contact)i.next();
if (c.find(pattern)) {
c.print();
}
}
}
public void print() {
System.out.println("" + contacts.size() + " Kontakte:");
Iterator i = contacts.iterator();
while (i.hasNext()) {
Contact c = (Contact)i.next();
c.print();
}
}
}
public class Appl {
private Contacts cts = new Contacts();
public void run() {
while (true) {
System.out.println("Bitte waehlen sie eine Aktion aus");
System.out.println("- l : Liste");
System.out.println("- n : Neu");
System.out.println("- s : Suchen");
System.out.println("- e : Ende");
String in = Keyboard.readString();
System.out.println();
if (in.equalsIgnoreCase("l")) {
print();
}
else if (in.equalsIgnoreCase("n")) {
insert();
}
else if (in.equalsIgnoreCase("s")) {
find();
}
else if (in.equalsIgnoreCase("e")) {
return;
}
System.out.println();
}
}
private void print() {
cts.print();
}
private void insert() {
Contact c;
while (true) {
System.out.println("- s : Single");
System.out.println("- p : Paar");
System.out.println("- f : Firma");
System.out.println("- a : Abbruch");
String in = Keyboard.readString();
System.out.println();
if (in.equalsIgnoreCase("s")) {
System.out.println("<Eingabe Single>");
c = new Single();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 303 / 409
break;
}
else if (in.equalsIgnoreCase("p")) {
System.out.println("<Eingabe Paar>");
c = new Pair();
break;
}
else if (in.equalsIgnoreCase("f")) {
System.out.println("<Eingabe Firma>");
c = new Company();
break;
}
else if (in.equalsIgnoreCase("a")) {
return;
}
}
c.input();
cts.add(c);
}
private void find() {
System.out.print("Geben sie einen Suchstring ein: ");
String pattern = Keyboard.readString();
cts.find(pattern);
}
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
}
14.20 Lsg. zu Aufgabe „Kontaktdaten 4“ – Kap. 14.16.4
todo...
14.21 Lsg. zu Aufgabe „Tic-Tac-Toe 2“ – Kap. 14.16.5
Immer wenn in einer Aufgaben-Stellung von „verschiedenen Arten von irgendwas“ geredet wird,
klingt dies nach Vererbung. Und wenn die „verschiedenen Arten von irgendwas“ noch
gemeinsam verarbeitet werden müssen, dann ist der Einsatz von Polymorphie meist sinnvoll.
14.21.1 Basis-Klasse „Player“
Im Tic-Tac-Toe aus Kapitel todo gibt es mehrere Arten von Spielern – den Menschen und den
Computer, der immer das nächste freie Feld nimmt. Hierfür könnte eine Klassen-Hierarchie
also sinnvoll sein.
Die erste Aktion ist also die Integration einer Basis-Klasse „Player“.
• Da kein Spieler an sich existiert, wird die Basis-Klasse abstrakt gemacht.
• Beide Spieler haben die Verwaltung der Spielfarbe gemeinsam, d.h. wird diese in die BasisKlasse verschoben.
• Zusätzlich werden die gemeinsamen Funktionen „getName“ und „next“ als abstrakte
Funktionen bekannt gemacht.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 304 / 409
public abstract class Player {
private int color;
public Player(int c) {
color = c;
}
public int getColor() {
return color;
}
public abstract String getName();
public abstract Move next(Board b);
}
Hier die neue Klassen-Implementierung von „ComputerNext“. Die Klasse „Human“ ändert sich
analog.
public class ComputerNext extends Player {
public ComputerNext(int c) {
super(c);
}
public String getName() {
return "Computer (naechstes freies Feld)";
}
public Move next(Board b) {
for (int i=0; true; i++) {
Move m = new Move(i, getColor());
if (b.valid(m)) {
return m;
}
}
}
}
14.21.2 Haupt-Schleife vereinfachen
Werden die Spieler denn irgendwo im Programm gemeinsam verarbeitet? Wenn ja, so könnte
dies durch Vererbung und Polymorphie vielleicht vereinfacht werden.
Ja, in der Hauptschleife ist zweimal fast der gleiche Code vorhanden, da beide Spieler identisch
verarbeitet werden müssen, aber dies bislang nicht mit einem Code-Stück möglich war. Hier
noch mal der bisherige Code:
public class Appl {
public static void main(String[] args) {
Board b = new Board();
Human pl1 = new Human(Move.WHITE);
ComputerNext pl2 = new ComputerNext(Move.BLACK);
b.print();
while (true) {
Move m = pl1.next(b);
System.out.println(pl1.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
© Detlef Wilkening 1997-2016
(*)
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 305 / 409
if (b.wins(pl1.getColor())) {
System.out.println(pl1.getName() + " hat gewonnen");
return;
}
if (b.full()) {
System.out.println("Das Spiel ist remis ausgegangen");
return;
}
(**)
m = pl2.next(b);
System.out.println(pl2.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
if (b.wins(pl2.getColor())) {
System.out.println(pl2.getName() + " hat gewonnen");
return;
}
}
}
}
Das Problem war, dass sich der doppelte Code (von (*) bis (**)) nicht in eine Funktion ziehen
liess, da beide Spieler unterschiedlichen Typs waren.
void play(Typ pl) {
Move m = pl.next(b);
...
}
// welcher Typ? Human oder ComputerNext
Nun gibt es eine gemeinsame Basis-Klasse, und damit ist es möglich eine Funktion für beide
Spieler zu schreiben.
• Um das Spielende zu propagieren, gibt die „play“ Funktion noch einen boolean Wert zurück.
• Außerdem bekommt „play“ neben dem Spieler noch das Board übergeben.
• Zusätzlich typisieren wir noch die Spieler „pl1“ und „pl2“ nach „Player“ um – obwoh das hier
noch ziemlich egal ist.
public class Appl {
public static void main(String[] args) {
Board b = new Board();
Player pl1 = new Human(Board.WHITE);
Player pl2 = new ComputerNext(Board.BLACK);
b.print();
while (true) {
if (play(b, pl1)) return;
if (play(b, pl2)) return;
}
}
public static boolean play(Board b, Player pl) {
Move m = pl.next(b);
System.out.println(pl.getName() + " zieht " + m + '\n');
b.set(m);
b.print();
if (b.wins(pl.getColor())) {
System.out.println(pl.getName() + " hat gewonnen");
return true;
}
if (b.full()) {
System.out.println("Das Spiel ist remis ausgegangen");
return true;
}
© Detlef Wilkening 1997-2016
(*)
(**)
(***)
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 306 / 409
return false;
}
}
Und warum funktioniert das?
1) Da die Funktionen „getName“ und „next“ auch in Player definiert sind – wenn auch nur
abstrakt – compilieren die Zeilen (*), (**) und (***).
2) Dank Polymorphie landen die Funktions-Aufrufe in (*), (**) und (***) auch in den jeweiligen
Spieler-Klassen in den richtigen Funktionen.
14.22 Lsg. zu Aufgabe „Tic-Tac-Toe 3“ – Kap. 14.16.6
Das ist jetzt kein wirkliches Problem, da wir in Kapitel 14.21 alle Spieler schon auf „Player“
typisiert haben.
public static void main(String[] args) {
Board b = new Board();
Player pl1 = new Human(Board.WHITE);
Player pl2 = new ComputerNext(Board.BLACK);
...
Schon jetzt könnten wir problemlos z.B. dem Computer „weiß“ und dem Menschen „schwarz“
geben:
public static void main(String[] args) {
Board b = new Board();
Player pl1 = new ComputerNext(Board.WHITE);
Player pl2 = new Human(Board.BLACK);
...
Man muss dies nur noch durch eine Benutzer-Eingabe dynamisch steuern:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Appl {
public static void main(String[] args) {
Board b = new Board();
Player pl1 = choosePlayer("ersten", Board.WHITE);
Player pl2 = choosePlayer("zweiten", Board.BLACK);
b.print();
while (true) {
if (play(b, pl1)) return;
if (play(b, pl2)) return;
}
}
public static Player choosePlayer(String s, int color) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
while (true) {
System.out.println("Bitte waehlen sie den " + s + " Spieler aus:");
System.out.println("- Mensch (M)");
System.out.println("- Computer erstes freies Feld (E)");
System.out.print("> ");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 307 / 409
try {
String in = reader.readLine();
System.out.println();
if (in.compareToIgnoreCase("M") == 0) {
return new Human(color);
}
if (in.compareToIgnoreCase("E") == 0) {
return new ComputerNext(color);
}
}
catch (Exception x) {
}
System.out.println("Fehlerhafte Eingabe!\n");
}
}
public static boolean play(Board b, Player pl) {
...
}
}
14.23 Lsg. zu Aufgabe „Tic-Tac-Toe 4“ – Kap. 14.16.7
14.23.1 Klasse „ComputerRandom“
Zuerst implementieren wir einen weiteren Computer-Spieler, der das zu spielende Feld per
Zufall auswählt. Damit er problemlos ins Programm integriert werden kann, wird er natürlich von
„Player“ abgeleitet.
import java.util.Random;
public class ComputerRandom extends Player {
public ComputerRandom(int c) {
super(c);
}
public String getName() {
return "Computer (Zufall)";
}
public Move next(Board b) {
Random rnd = new Random();
while (true) {
int field = rnd.nextInt(9);
Move m = new Move(field, getColor());
if (b.valid(m)) {
return m;
}
}
}
}
14.23.2 Spieler-Auswahl
Dann muß er noch ins Menü für die Spieler-Auswahl integriert werden:
public static Player choosePlayer(String s, int color) {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
while (true) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 308 / 409
System.out.println("Bitte waehlen sie den " + s + " Spieler aus:");
System.out.println("- Mensch (M)");
System.out.println("- Computer erstes freies Feld (E)");
System.out.println("- Computer Zufall (Z)");
System.out.print("> ");
try {
String in = reader.readLine();
System.out.println();
if (in.compareToIgnoreCase("M") == 0) {
return new Human(color);
}
if (in.compareToIgnoreCase("E") == 0) {
return new ComputerNext(color);
}
if (in.compareToIgnoreCase("Z") == 0) {
return new ComputerRandom(color);
}
}
catch (Exception x) {
}
System.out.println("Fehlerhafte Eingabe!\n");
}
}
14.23.3 Fertig
Und das war es. Dank Vererbung und Polymorphie funktioniert das eigentliche Programm auch
mit einer Klasse, die zum Zeitpunkt der Programm-Erstellung noch gar nicht bekannt war. Das
eigentliche Programm kennt nur Player und bindet die Funktionen dynamisch, d.h. landen die
Funktions-Aufrufe an den richtigen Stellen.
Nur an den Stellen, an denen Objekte erzeugt werden, muß der Code verändert werden. Und
geschickt programmiert, gibt es nur wenige solche Code-Stellen im Programm.
14.24 Lsg. zu Aufgabe „Tic-Tac-Toe 5“ – Kap. 14.16.8
todo...
15 Innere Klassen
Seit Java 1.1 können in Java Klassen und Interfaces ineinander verschachtelt werden. Man
nennt sie innere Klassen, eingebettete Klassen oder auch verschachtelte Klassen. Es gibt
mehrere Sorten von eingebetteten Klassen:
1. Member-Klassen
Eingebettete static Klassen (oder auch „statische Member-Klassen“) sind relativ normale
Klassen, d.h auch Top-Level Klassen, die logisch einer anderen Klasse zugeordnet sind,
und auch auf deren private Elemente zugreifen dürfen.
Eingebettete nicht static Klassen (oder auch „Member-Klassen“) sind sogenannte
Element-Klassen, deren Instanzen immer einer umgebenden Klassen-Instanz zugeordnet
sind.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 309 / 409
Eingebettete Interfaces (oder auch „Member-Interfaces“) sind vergleichbar zu
eingebetteten static Klassen, d.h sie sind Top-Level-Interfaces, die logisch einer anderen
Klasse zugeordnet sind, und auch auf deren private Elemente zugreifen dürfen. Interfaces
sind implizit immer static.
2. Lokale Klassen sind Klassen die innerhalb eines Code-Blocks definiert sind, und auch nur
dort benutzt werden können. Obwohl doch etwas anderes, haben sie viele Eigenschaften
mit eingebetteten nicht static Klassen gemeinsam.
3. Anonyme Klassen sind lokale Klassen ohne Namen.
15.1 Eingebettete static Klassen
Eingebettete static Klassen:
• Benötigen den Modifier „static“.
• Zugriff über Kombination der Klassen-Namen mit trennendem Punkt.
• Verhalten sich wie normale Klassen (Top-Level-Klassen).
• Können beliebig tief geschachtelt werden.
• Sind logisch der (den) umgebenden Klasse(n) zugeordnet.
• Dürfen auch auf private Elemente der umgebenden Klasse(n) zugreifen.
• Haben direkten Zugriff auf static Elemente der umgebenden Klasse(n).
• Die umgebende Klasse hat auch Zugriff auf private Elemente der inneren Klasse.
public class OuterClass {
private String pstr = "Private aeussere Element-Variable";
private static String pocv = "Private aeussere Klassen-Variable";
public OuterClass() {
System.out.println("Konstruktor OuterClass");
System.out.println(picv);
new StaticInnerClass(this);
}
public static class StaticInnerClass {
private static String picv = "Private innere Klassen-Variable";
public StaticInnerClass(OuterClass o) {
System.out.println("Konstruktor StaticInnerClass");
System.out.println(o.pstr);
System.out.println(pocv);
}
}
}
OuterClass o = new OuterClass();
OuterClass.StaticInnerClass i = new OuterClass.StaticInnerClass(o);
Ausgabe
Konstruktor OuterClass
Private Innere Klassen-Variable
Konstruktor StaticInnerClass
Private aeussere Element-Variable
Private aeussere Klassen-Variable
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 310 / 409
Konstruktor StaticInnerClass
Private aeussere Element-Variable
Private aeussere Klassen-Variable
15.2 Eingebettete nicht static Klassen
Eingebettete nicht static Klassen:
• Heissen auch Element-Klassen.
• Sind logisch der umgebenden Klasse zugeordnet.
• Ihre Objekte sind immer mit genau einem Objekt der umgebenden Klasse verbunden.
• Dürfen auch auf private Elemente der umgebenden Klasse zugreifen.
• Dürfen keine static Elemente enthalten.
• Dürfen nicht den Namen einer umgebenden Klasse bzw. Package’s haben.
public class OuterClass {
private String pstr;
public OuterClass(String s) {
System.out.println("Konstruktor OuterClass");
pstr = s;
new InnerClass();
}
public class InnerClass {
public InnerClass() {
System.out.println("Konstruktor InnerClass");
System.out.println(pstr);
}
}
}
OuterClass o = new OuterClass("Call in fct");
Ausgabe
Konstruktor OuterClass
Konstruktor InnerClass
Call in fct
Im obigen Beispiel greift der Konstruktor direkt auf eine Objekt-Variable der umgebenden
Klasse zu, obwohl hier scheinbar gar kein Objektbezug vorhanden ist. Diesen Objektbezug
stellt der Compiler automatisch her. Wird ein Objekt der Elementklasse erzeugt, übergibt der
Compiler automatisch die this Referenz des aktuellen Objekts als Objektbezug. Ausserdem
erzeugt der Compiler automatisch in der Elementklasse immer ein Attribut für die Referenz auf
das zugeordnete umgebenden Klassenobjekt.
Wird versucht, eine Elementklasse in einer anderen Klasse zu erzeugen, so meldet der
Compiler einen Fehler, da das aktuelle Objekt nicht den richtigen Klassentyp hat.
public class A {
public void fct() {
new OuterClass.InnerClass("Call from A");
}
}
© Detlef Wilkening 1997-2016
// Error
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 311 / 409
Um ein Objekt der Elementklasse ausserhalb der umgebenden Klasse erzeugen zu können,
muss man dem Compiler den impliziten Parameter explizit angeben. Dafür wurde in Java eine
spezielle Syntax für den Operator „new“ eingeführt:
OuterClass o = new OuterClass("Call from every-wbere");
o.new InnerClass();
Ausgabe
Konstruktor OuterClass
Konstruktor InnerClass
Call from every-where
Konstruktor InnerClass
Call from every-where
Diese new Operator wird wie eine Elementfunktion für das zuzuordnende Objekt aufgerufen.
Der Klassenname der Elementklasse muss nicht weiter referenziert werden, da automatisch
der Namensraum der umgebenden Klasse benutzt wird.
15.3 Eingebettete Interface’s
Eingebettete Interface’s:
• sind äquivalent zu eingebetteten static Klassen
• sich wie normale Interfaces
• können beliebig tief geschachtelt werden
• sind logisch der (den) umgebenden Klasse(n) zugeordnet
• sind implizit immer static - das Schlüsselwort kann daher weggelassen werden
public class OuterClass {
public static interface InnerInterface {
public void fct();
}
}
public class Concrete implements OuterClass.InnerInterface {
public void fct() {
System.out.println("In Concrete.fct()");
}
}
OuterClass.InnerInterface ii = new Concrete();
ii.fct();
Ausgabe
In Concrete.fct()
15.4 Lokale Klassen
Eine lokale Klasse wird innerhalb eines Code-Blocks definiert, und ist nur innerhalb desselben
bekannt. Im Prinzip ist eine lokale Klasse eine spezielle Version einer eingebetteten Klasse,
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 312 / 409
denn jeder Code-Block existiert in Java innerhalb einer Klasse, und so hat auch eine lokale
Klasse einen Klassen- bzw. Objektbezug. Daher können die Regeln für eingebettete Klassen
erstmal auf lokale Klassen übertragen werden.
• Lokale Klassen sind nur in dem sie definierenden Block bekannt.
• Lokale Klassen können nicht als public, protected, private oder static definiert werden.
• Lokale Klassen dürfen auch auf private Elemente der umgebenden Klasse zugreifen.
• Lokale Klassen dürfen keine static Elemente enthalten.
• Lokale Klassen können auf Variablen, Parameter und Ausnahmen im Sichtbarkeit des
definierenden Blocks zugreifen, wenn dieses als „final“ definiert sind.
• Lokale Klassen können keine Interfaces sein.
• Lokale Klassen dürfen nicht den Namen einer umgebenden Klasse bzw. Package’s haben.
public class Normal {
private String str = "String Attribute in Normal";
public void f {
final int i = 42;
class LocalClass {
public LocalClass() {
System.out.println("Konstruktor LocalClass");
System.out.println(str);
System.out.println(i);
}
}
LocalClass l = new LocalClass();
}
}
Normal n = new Normal();
n.f();
Ausgabe
Konstruktor LocalClass
String Attribute in Normal
42
Hinweis - eine lokale ist gegenüber einer eingebetteten Klasse vorzuziehen, wenn die Klasse
nur in einer Funktion benötigt wird.
15.4.1 Externe Verwendung
Selbst wenn eine lokale Klasse nur innerhalb des sie definierenden Blocks bekannt ist, so
können Objekte der Klasse auch woanders benutzt werden.
public interface Interface {
public void f();
}
public class Normal {
public Interface getInterfaceObject {
class LocalClass implements Interface {
public void f() {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 313 / 409
System.out.println("LocalClass.f()");
}
}
return new LocalClass();
}
}
Normal n = new Normal();
Interface i = n.getInterfaceObject();
i.f();
Ausgabe
LocalClass.f()
15.5 Anonyme Klassen
Anonyme Klassen sind lokale Klassen ohne Namen. Sie können direkt an der Stelle, wo ein
Objekt von ihnen benötigt und erzeugt wird, ohne Namen definiert werden. Im Gegensatz zu
der Definition einer lokalen Klasse ist die Definition einer anonymen Klasse ein Ausdruck, und
kann daher Teil eines größeren Ausdrucks (z.B. eines Funktionsaufrufs) sein.
• Anonyme Klassen haben nur den automatischen Default-Konstruktor.
• Objekte anonymen Klassen, die in einer Element-Funktion erzeugt werden, haben
automatischen Objekt-Bezug zum aktuellen Objekt der umgebenden Klasse – d.h. dem
Objekt, für das die Element-Funktion aufgerufen wurde (dem „this“ der Element-Funktion) –
siehe Beispiel.
• Objekte anonymen Klassen, die in einer Klassen-Funktion erzeugt werden, haben keinen
Objekt-Bezug.
Syntax
new Basisklasse () { Klassendefinition }
public interface Inter {
public void f();
}
public class Appl {
private String s = "Attribute in Appl";
public static void fct(Inter i) {
System.out.println("Appl.fct bekommt Objekt vom Typ Inter uebergeben");
i.f();
}
public static void main(String[] args) {
fct(new Inter() {
public void f() {
System.out.println("- f() von anonymer Klasse");
//System.out.println("- - Attribut-Zugriff: \"" + s + "\""); Fehler
System.out.println("- - kein Attribut-Zugriff");
}
});
Appl appl = new Appl();
appl.doit();
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 314 / 409
public void doit() {
fct(new Inter() {
public void f() {
System.out.println("- f() von anonymer Klasse");
System.out.println("- - Attribut-Zugriff: \"" + s + "\"");
}
});
}
}
Ausgabe
Appl.fct bekommt Objekt vom Typ Inter uebergeben
- f() von anonymer Klasse
- - kein Attribut-Zugriff
Appl.fct bekommt Objekt vom Typ Inter uebergeben
- f() von anonymer Klasse
- - Attribut-Zugriff: "Attribute in Appl"
Hinweis – anonyme Klassen mögen seltsam und wie ein Spezialfall wirken, aber sie sind z.B.
als Implementierung von Listener-Interfaces bzw. -Klassen beim Event-Handling in Swing das
tägliche Brot – siehe z.B. Kapitel todo.
15.6 Virtuelle Maschine
Die virtuellen Java Maschine kennt keine eingebetteten Klassen. Damit sie trotzdem den
erzeugten Byte Code verarbeiten kann, wendet der Compiler einen kleinen Trick an: er erzeugt
statt der eingebetteten Klassen scheinbare Top-Level Klassen, deren Namen sich aus dem
Namen der umgebenden Klasse, einem $ Zeichen und dem Namen der eingebetteten Klasse
ergibt.
public class OuterClass {
public static class StaticInnerClass {
}
}
So finden sich nach einem solchen Code-Stück im entsprechenden Byte-Code Verzeichnis die
Dateien:
• OuterClass.class
• OuterClass$StaticInnerClass.class
wieder. Schauen sie sich das Ausgabe-Verzeichnis ruhig mal an.
16 GUI Programmierung mit Swing
Bei vielen Programmen wird heutzutage eine grafische Bedienoberfläche mit Fenstern, Menüs,
Maus, uwm. erwartet. Für die Programmierung grafischer Oberflächen enthält Java die
Bibliothek „Swing“, die Klassen für Fenster, Buttons und vieles mehr enthält.
Swing kapselt die Unterschiede zwischen den einzelnen Betriebssystemen, so dass eine mit
Java erstellt Anwendung auf allen Plattformen läuft, auf denen eine virtuelle Maschine JVM
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 315 / 409
vorhanden ist. Dies macht Swing u.a. dadurch, dass es das komplette GUI selber zeichnet und
sich nicht auf die nativen Widgets des jeweiligen Betriebssystems abstützt. Dieses Vorgehen
hat mehrere Konsequenzen:
• Vorteile:
o Alle Swing Elemente stehen immer auf jeder JVM zur Verfügung, auch wenn das
zugrunde liegende OS gar keine entsprechenden Widgets kennt.
o Das Look&Feel von Swing Anwendungen kann jederzeit ausgetauscht werden, und dies
sogar zur Laufzeit. So ist z.B. auf einem Windows Rechner ein Motif Look&Feel möglich,
oder umgekehrt – siehe auch Kapitel 16.6.
• Nachteile:
o Bestimmte betriebssystem-spezifische Features sind nicht in Swing vorhanden, da sie nur
bedingt auf andere Plattformen abbildbar sind - z.B. die neuen Windows 7 Elemente69.
Aufgrund der vollständigen Kapselung der unter der JVM liegenden Plattform (OS und
Prozessor) besteht ausser via JNI70 auch keine Möglichkeit solche Features
anzusprechen.
o Swing GUIs sind etwas langsamer und speicherfressender als native Widgets – dies ist
aber in der Praxis meistens nicht relevant.
Die Klassen-Bibliotheken für die grafische Oberfläche haben sich in der Geschichte von Java
mehrmals geändert. Beim Umstieg vom JDK 1.0 zum JDK 1.1 wurde u.a. das Event-Modell
komplett umgekrempelt. Mit dem JDK 1.2 wurden die alten AWT (Abstract Window Toolkit)
Klassen um die neueren viel leistungsfähigeren JFC (Java Foundation Classes) Klassen
erweitert, die ein Bestandteil von Swing sind und die alten AWT Klassen als Basis-Klassen
beinhalten.
Im gesamten Tutorial wird mit den Swing-Klassen gearbeitet und auf die alte AWT
Bibliotheken, soweit sie nicht noch als Basis von Swing relevant sind, gar nicht mehr
eingegangen.
Dieses Kapitel stellt nur eine erste Einführung in die Programmierung von grafischen
Oberflächen dar, indem es das berühmte „Hallo Welt“ implementiert. In den folgenden Kapiteln
werden einzelne Bereiche der Programmierung von grafischen Oberflächen weiter vertieft.
Hinweis – die Swing Packages wurden mit dem JDK 1.2 gegenüber Vorversionen umbenannt.
Wer also mit Quelltexten aus der JDK 1.1.x Phase konfrontiert wird, dem werden veraltete
import Anweisungen begegnen:
• Früher „com.sun.java.swing“
• Jetzt: „javax.swing“
Lange Zeit war mein Standard Beispiel die Unterstützung von Tray-Icons, aber seit dem JDK
1.6 werden nun auch diese unterstützt. Hier sehen Sie damit auch sehr schön, warum die Java
Bibliothek so gross sein muß: da die OS-API nicht direkt zur Verfügung steht, müssen alle
Fähigkeiten des OS in der Java Bibliothek abgebildet werden.
70 JNI (Java Native Interface) ist eine Schnittstelle in Java, mit der C Funktionen angesprochen
werden können.
69
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 316 / 409
Bemerkung – mittlerweile gibt es mehrere alternative GUI-Toolkits für Java, relevant davon
sind aber eigentlich nur folgende beiden:
• Eins davon ist SWT, das im Rahmen des Eclipse Projekts entwickelt wurde. Im Gegensatz
zu Swing baut es auf die native Widgets des jeweiligen Betriebssystems auf. Dadurch ist
das Look&Feel von SWT besser auf das OS abgestimmt und SWT ist auch performanter.
Auf der anderen Seite macht man sich politisch aber von einem Toolkit abhängig, das nicht
fester Bestandteil von Java ist, und eben daher nicht automatisch auf allen Plattformen mit
einer JVM verfügbar ist. Außerdem ist SWT nicht so gut erweiter- und anpaßbar wie Swing.
Dafür bietet es mit dem RCP (Rich-Client-Plattfom) Framework eine mächtige und sehr
hilfreiche Basis für Applikations-Entwicklung.
• Seit neustem exisitiert auch eine Java Anbindung der C++ GUI Klassen-Bibliothek QT, die
z.B. als C++ Version das GUI-Toolkit für den KDE Desktop unter Linux darstellt.
Achtung – die Kapitel über GUIs mit Swing sind natürlich nur eine kleine Einführung. Das
Thema GUI Entwicklung ist viel zu umfangreich, als das es hier umfassend behandelt werden
könnte - allein mit Swing kann man ganze Bücher füllen71. Diese Einführung versucht die
wichtigsten Konzepte von GUI Programmierung und Swing vorzustellen
16.1 Ein einfaches Fenster
Wenn es nur darum geht, ein Fenster auf den Bildschirm zu zaubern, hält sich der Aufwand in
Grenzen – wir haben das Programm schon in Kapitel 3.1.2 kennen gelernt.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
JFrame frame = new JFrame("Mein GUI Fenster 1");
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
In „main“ wird ein Objekt der Swing-Frame Klasse „JFrame“ erzeugt, die für ein einfaches
Fenster darstellt – dem Konstruktor wird der Fenstertitel übergeben. Mit ‘setLocation’ wird der
Startort, mit ‘setSize’ die Grösse, und mit ‘setVisible’ die Sichtbarkeit gesetzt. Fertig.
Aber diese Minimal-Lösung hat mehrere Nachteile:
• Mit Schliessen des Fensters wird nicht mal das Programm beendet - sie können es nur auf
der Kommandozeile mit [Strg]+[C] abschiessen.
• Das JFrame-Klasse liefert nur ein allgemeines Default-Verhalten, das eigentlich nie reicht. In
dem Fenster soll ja schließlich was passieren.
Um mit dem Schliessen des Fensters das Programm zu beenden, muss für das Fenster ein
71
Die es auch gibt – dicke Wälzer nur über Swing.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 317 / 409
entsprechendes Flag gesetzt werden – dies geschieht mit „setDefaultCloseOperation“ und der
Konstanten „JFrame.EXIT_ON_CLOSE“.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
JFrame frame = new JFrame("Mein GUI Fenster 2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
In einem späteren Kapitel über Event-Handling werden wir das Beenden des Programms beim
Schließen des Fensters anders lösen, indem wir uns selber in die Event-Bearbeitung
einhängen. Aber das wird nur ein Beispiel zum Verständnis des Event-Handlings in Swing sein
– der normale Weg sollte der hier beschriebene mit dem Setzen der Default-Close-Operation
sein.
16.2 Ein etwas besseres Fenster
Besser wäre also ein eigenes Fenster, das wir nach unseren Vorstellungen formen
(programmieren) können - wir würden das Default-Verhalten von JFrame aber gerne
beibehalten. Was macht man dann in Java? Natürlich erben!
Hier ein zweites Programm, das zwar nicht mehr kann als das vorherige, aber schon mal die
Vorbereitungen für mehr darstellt.
public class Appl {
public static void main(String[] args) {
MyFrame frame = new MyFrame("Mein eigenes Fenster");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame(String title) {
super(title);
}
}
Im Prinzip ist es das alte Programm – nur wird statt der Swing Klasse „JFrame“ eine eigene
Frame-Klasse „MyFrame“ benutzt, die sich von „JFrame“ ableitet. Da „MyFrame“ keine neue
Funktionalität hinzufügt, verhält sich das neue Programm wie das alte.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 318 / 409
16.3 Ein grafisches „Hallo Welt“
16.3.1 Das normale grafische „Hallo Welt“
Um ein grafisches „Hallo Welt“ zu erhalten, müssen wir im Fenster auch „Hallo Welt“ ausgeben.
Wie macht man das?
• Immer, wenn ein Fenster neu gezeichnet werden muss, wird automatisch die Funktion
„paint“ aufgerufen, die daher für eigene Ausgaben überschrieben werden muss – siehe
weiter unten und Kapitel 17.
• Die Funktion „paint“ bekommt ein „Graphics“ Objekt übergeben72, das u.a. Funktionen für die
Textausgabe in einem Fenster zur Verfügung stellt – siehe Kapitel 16.4.
import java.awt.Graphics;
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame(String title) {
super(title);
}
public void paint(Graphics g) {
super.paint(g);
g.drawString("Hallo Welt", 50, 50);
}
}
Für die Ausgabe überschreiben wir die Funktion „paint“, die an den Koordinaten „50/50“ den
Text „Hallo Welt“ ausgibt - mehr dazu in Kapitel 17 und Kapitel 16.4.
Abb. 16-1 : Ein grafisches „Hallo Welt“ Programm
Achtung - es ist ganz wichtig - nicht nur für die Funktion „paint“ sondern auch für viele weitere
Funktionen, die wir zukünftig kennenlernen werden - dass zuerst die überschriebene Funktion
der Basis-Klasse (hier „paint“) aufgerufen wird. Das Neu-Zeichnen unseres Fensters besteht ja
nicht nur aus der Ausgabe von „Hallo Welt“, sondern noch aus anderen Dingen - und die
geschehen natürlich in den Basisklassen, und sollten nicht verhindert werden. Und man sollte
zuerst die Super-Funktion aufrufen, da sie ansonsten möglicherweise alle unsere Aktionen
wieder zerstört.
Genau genommen wird ein „java.awt.Graphics2D“ übergeben – aber diese Details ignorieren
wir erstmal.
72
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 319 / 409
Hinweis – warum funktioniert das? Hier ist natürlich wieder Vererbung und Polymorphie im
Spiel. Der interne Fenster-Verwaltungs-Mechanismus der JVM kennt unsere Fenster Klasse
„MyFrame“ natürlich nicht, aber er arbeitet auf einer Basisklasse von „MyFrame“. In dieser ist
die Funktion „paint“ definiert, und die wird intern aufgerufen. Wir haben diese Funktion aber
nun überschrieben, und dank Polymorphie landet damit der Aufruf von „paint“ in
„MyFrame.paint“.
16.3.2 Probleme mit dem JDK 1.5 und zum Teil auch mit dem JDK 1.6
Das im vorherigen Kapitel 16.3.1 beschriebene Java-Programm funktioniert mit dem JDK 1.5
und manchen JDK 1.6 Versionen nicht fehlerfrei73 - zumindest in den von mir getesten
Versionen 1.5.0_04 bis 1.5.0_09. Mit allen möglichen alten JDKs, z.B. 1.2, 1.3, 1.4, 1.4.1, 1.4.2
und auch mit einigen JDK 1.6 Versionen funktioniert das Programm korrekt.
Was ist denn hier nun das Problem? Ganz einfach – immer wenn das Fenster in irgendeiner
Form vergrößert wird (d.h. es wird mit der Maus oder der Tastatur größer gezogen, oder es
wird maximiert), dann wird der Inhalt des Fensters nicht neu gezeichnet, d.h. die Paint-Funktion
wird nicht aufgerufen.
In der realen Praxis ist dies kein Problem, da dort eigentlich nie direkt das Haupt-Frame für
Paint-Aktionen genutzt wird, sondern im Haupt-Frame eigentlich immer weitere GUI-Elemente
(z.B. Menüs, Buttons, Panels, usw.) liegen. Und bei denen klappt das alles auch z.B. im JDK
1.5 problemlos.
Problematisch ist der Bug aber für dieses Tutorial und auch andere Lehrbücher. Denn hier
sollen die Beispiele natürlich möglichst einfach sein, weshalb im Frame oft keine weiteren
Elemente liegen – und damit tritt der Fehler auf.
In der Praxis sollten Sie sich dieses Problems bewußt sein – und die Musterlösungen aus dem
Tutorial entsprechend verändern. Eine mögliche Lösung z.B. ist das Frame mit einem eigenen
Panel (siehe Kapitel 20.8) zu füllen, und alle Aktionen statt im Frame im Panel unterzubringen.
Als Beispiel hier das „Hallo-Welt“ Programm in einer Version mit zusätzlichem Panel, die u.a.
auch mit dem JDK 1.5 funktioniert.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
MyFrame frame = new MyFrame("Hallo-Welt Fenster fuer das JDK 1.5");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
Möglicherweise gibt es – wenn Sie dieses Tutorial lesen – eine neuere JDK Version. Und
möglicherweise ist dort der Bug nicht mehr vorhanden.
73
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 320 / 409
}
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame(String title) {
super(title);
getContentPane().add(new MyPanel());
}
}
import java.awt.Graphics;
import javax.swing.JPanel;
public class MyPanel extends JPanel {
public void paint(Graphics g) {
super.paint(g);
g.drawString("Hallo Welt", 50, 50);
}
}
16.4 Graphics Objekt
Die Klasse „java.awt.Graphics“ stellt die Schnittstelle eines Swing Programms für die Ausgabe
auf dem Bildschirm dar. Die Klasse bietet verschiedenste Element-Funktionen an um Pixel,
Linien, Kreise, Rechtecke, Texte, uvm. auf dem Bildschirm auszugeben.
Lassen sie sich von Eclipse einfach mal die Menge an Funktionen anzeigen, bzw. schauen sie
in die Java-Hilfe. Bei vielen Funktionen sagt der Name intuitiv was die Funktion macht, und
auch die Parameter benötigen kaum Erklärung, von daher sollten einfache Anwendungen kein
Problem sein.
Beispiele von Element-Funktionen in „java.awt.Graphics“
Funktion
void drawLine(int x1, int y1, int x2, int y2)
void drawRect(int x, int y, int width, int height)
void fillRect(int x, int y, int width, int height)
void drawOval(int x, int y, int width, int height)
void fillOval(int x, int y, int width, int height)
void setColor(Color c)
void setPaintMode()
void setXORMode(Color c1)
Beschreibung
Zeichnet eine Linie
Zeichnet ein Rechteck
Zeichnet ein ausgefülltes Rechteck
Zeichnet eine Elipse
Zeichnet eine ausgefüllte Elipse
Setzt die Zeichen-Farbe
Setzt den Zeichen-Mode auf Überschreiben
Setzt den Zeichen-Mode auf XOR zur
übergebenen Farbe
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
public class MyFrame extends JFrame {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 321 / 409
public MyFrame(String title) {
super(title);
setSize(380, 340);
}
public void paint(Graphics g) {
super.paint(g);
g.drawString("Hallo Welt", 20, 60);
g.fillOval(100, 100, 80, 50);
g.setColor(Color.RED);
g.drawRoundRect(40, 200, 300, 40, 20, 20);
g.setColor(Color.BLUE);
g.drawLine(50, 150, 300, 300);
}
}
Abb. 16-2 : Ein GUI Programm mit bunter Ausgabe im Fenster
Hinweis - während des normalen Programm-Ablaufs, z.B. bei einer Benutzer-Interaktion oder
während einer Berechnung, kann es ohne weiteres notwendig sein direkt auf den Bildschirm zu
zeichnen. Auch in diesen Fällen benötigt man ein Graphics Objekt - man kann es sich für jedes
Swing Objekt mit der Funktion „getGraphics()“ holen. Eine Anwendung dafür findet sich z.B. im
Scribble Programm in Kapitel 18.2.4.
Achtung – das Überschreiben der Paint-Funktion des Haupt-Fensters führt mit dem JDK 1.5 zu
Problemen – siehe Kapitel 16.3.2.
16.5 Klasse „Color“
Im letzten Beispiel wurde u.a. die Zeichen-Farbe für das Graphics Objekt auf „rot“ bzw. „blau“
gesetzt - siehe Kapitel 16.4. Für Farben gibt es die Klasse „java.awt.Color“. Sie enthält u.a.
Konstanten für die wichtigsten Farben wie „schwarz – BLACK“, „weiß – WHITE“, „rot – RED“,
„blau – BLUE“, „grün – GREEN“, usw.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 322 / 409
Objekte der Klasse „Color“ können aber z.B. auch durch die gewünschten RGB Werte (rot,
grün, blau) erzeugt werden – die Klasse enthält hierfür verschiedene Konstruktoren.
Color c = new Color(128, 0, 128);
Die Graphics Klasse enthält die Funktion „setColor“ zum Setzen der Zeichen-Farbe.
Alle Swing Klassen haben die Funktionen „setForeground“ und „setBackgroundColor“, um die
Vorder- und Hintergrund-Farbe zu setzen – die Vordergrund-Farbe entspricht der Zeichenfarbe.
16.6 Änderung der Oberfläche in den Windows-Style
Alle GUI Programme in diesem Tutorial werden mit dem normalen Swing-Style erzeugt, d.h. der
Oberflächen-Skin ist Swing. Nehmen wir z.B. das GUI-Fenster aus Kapitel 19.4, das obwohl der
Screenshot unter Windows XP mit klassischem Windows Skin erzeugt wurde – nicht wirklich
nach Windows aussieht:
Abb. 16-3 : GUI-Fenster aus Kapitel 19.4 mit Swing-Style
Möglicherweise möchten Sie aber, dass Ihre Programme unter Windows auch einen WindowsStyle haben. Das können Sie in Swing erreichen, indem Sie in Ihrem Programm den Style
entsprechend setzen. Hier der Quelltext aus Kapitel 19.4 mit verändertem Windows-Style:
import javax.swing.JFrame;
import javax.swing.UIManager;
public class Appl {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
} catch (Exception e) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 323 / 409
// Klappt es wohl nicht - auch egal - dann eben mit Swing-Style...
}
MyFrame frame = new MyFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit verschachtelten Layouts");
setSize(400, 300);
JPanel panel = new JPanel(new GridLayout(3, 2));
panel.add(new JButton("1"));
panel.add(new JButton("2"));
panel.add(new JButton("3"));
panel.add(new JButton("4"));
panel.add(new JButton("5"));
panel.add(new JButton("6"));
Container c = getContentPane();
c.setLayout(new BorderLayout());
c.add(new JButton("North"), BorderLayout.NORTH);
c.add(new JButton("West"), BorderLayout.WEST);
c.add(new JButton("East"), BorderLayout.EAST);
c.add(new JButton("South"), BorderLayout.SOUTH);
c.add(panel, BorderLayout.CENTER);
}
}
Abb. 16-4 : GUI-Fenster aus Kapitel 19.4 mit Windows-Style
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 324 / 409
16.7 Aufgaben
16.7.1 Aufgabe „Schwebende Kugel“
Implementieren sie ein Fenster, dass eine schwebende Kugel wie in der Abb. 16-5 enthält.
Hinweis – es sind wirklich nur die problemlosesen Zeichen-Funktionen aus Kapitel 16.4 benutzt
worden. Es ist einfach ein bisschen geschickt mit der Farbe gespielt worden.
Abb. 16-5 : Fenster mit schwebender Kugel
Lösung siehe Kapitel 16.8.
16.7.2 Aufgabe „Farb-Fenster“
Implementieren sie ein Fenster, dessen Inhalt viele kleine Quadrate sind, die jeweils eine
andere Farbe haben. Überlegen sie, wie sie die drei Farb-Dimensionen „RGB“ auf eine zweidimensionale Fläche abbilden, bzw. wie die den Farb-Raum sinnvoll reduzieren.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 325 / 409
Lösung siehe Kapitel 16.9.
16.8 Lsg. zu Aufgabe „Schwebende Kugel“ – Kap. 16.7.1
todo...
16.9 Lsg. zu Aufgabe „Farb-Fenster“ – Kap. 16.7.2
todo...
17 Philosophie der GUI Programmierung
Bevor wir weiter gehen, müssen einige Dinge über die Philosophie von GUI Programmierung
gesagt werden. Diese Dinge sind ganz unabhängig von Swing oder Java, sondern gelten wohl
für alle GUI Bibliotheken.
• Der Bildschirm gehört nicht dem Programm oder einem Fenster, sondern dem
Betriebssystem.
• Der Kontrollfluß wird vom GUI vorgegeben und ist stark interaktiv.
17.1 Besitzer des Bildschirms
Im Gegensatz zu alten DOS oder CPM Zeiten unterstützen heute alle modernen
Betriebssysteme die Möglichkeit mehrere Programme gleichzeitig auszuführen74. Wenn dann
diese Programme noch GUI Anwendung sind, wie die meisten Anwendungen heute, hat das
einige Konsequenzen für den Zugriff auf den Bildschirm:
• Ein Programm kann nicht einfach beliebig auf den Bildschirm schreiben, da es nicht
selbstverständlich ist, dass der Bildschirm ihm gehört, und nicht ein anderes Programm dort
seine Ausgaben macht.
• Ein Programm kann seinen Ausgabe-Zustand nicht auf dem Bildschirm abfragen, da
mittlerweile ein anderes Programm dort seine Ausgaben gemacht haben kann.
• Ein Programm muss immer in der Lage sein, seine Darstellung zu erneuern, da der
Benutzer die Fenster beliebig nach vorne, hinten, seitwärts und sonstwas verschieben kann,
d.h. ein bislang nicht sichtbarer Teil des Fensters oben liegt und neu gezeichnet werden
muss - siehe z.B. Kapitel 18.2.4 und Kapitel todo.
Die letzten beiden Punkte bedingen, dass ein Programm immer ein komplettes Modell75 seiner
Ausgabe enthalten muss. Damit kann ein Programm dann jederzeit seine Darstellung auf dem
Bildschirm erneuern, wenn es dazu aufgefordert wird. Dies geschieht via OS, JVM und PaintDas heisst, sie sind multitasking fähig.
Mit Modell sind hier alle Daten und deren Beziehungen gemeint, die das Programm
visualisiert.
74
75
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 326 / 409
Funktion.
17.2 Kontrollfluß
Klassische Programme wurden oft nach dem EVA Prinzip76 aufgebaut - hierbei ist der
Programmfluss relativ einfach - ein Schritt folgt nach dem anderen.
Abb. 17-1 : Aufbau klassischer Programme
Auch komplexere klassische Programme haben eine klare Kontrollflußsteuerung in ihrer
Programmlogik. Ein Beispiel dafür ist unser „Tic-Tac-Toe“, dessen „main“ den Kontrollfluß klar
vorgibt – siehe Kapitel 12.11.1.
In einem modernen grafischen Benutzerinterface werden viel höhere Anforderungen an ein
Programm gestellt. Der Benutzer kann beliebige Aktionen durch die Tasten, Menü, Buttons,...
auswählen, er kann das Fenster vergrössern, verkleinern, in den Hintergrund legen bzw. nach
vorne holen. Andere Programme können die Umgebung verändern, der Benutzer kann neue
Einstellungen vornehmen - und auf all das muss das Programm jederzeit sauber und
kontrolliert reagieren.
Damit dies mit einem halbwegs vernünftigen und überschaubaren Programmieraufwand
möglich ist, hat sich das Programmiermodell von EVA hin zu einem Event-gesteuertem Modell
verändert. Hierbei liegt die Kontrolle nicht mehr primär bei dem Programm, sondern beim
Betriebssystem, das bei jedem Ereigniss (Event) eine entsprechende Funktion des Programms
aufruft – sogenannte „Callback-Funktionen“.
Abb. 17-2 : Aufbau Event-gesteuerter Programme
Dieses event-gesteuerte Programmiermodell hat sich in der Praxis sehr bewährt, und findet
sich natürlich auch in Java wieder. An jedes mögliche Ereignis kann der Programmierer eine
Funktion ankoppeln, die bei ihrem Auftritt automatisch ausgeführt wird.
Das ganze ist am Anfang recht ungewohnt, und es sind viele Fragen zu klären:
• Wie kann auf Events reagiert werden, d.h. wie wird eine Callback-Funktion in Java
implementiert?
• Wie können Events verhindert oder modifiziert werden können?
• Wie werden Events bei mehreren aufeinanderliegenden grafischen Elementen propagiert?
• Können Events programmgesteuert ausgelöst werden?
• Welche Elemente können Events auslösen, und welche darauf reagieren?
• Wie können Events und die dahinter liegende Benutzerlogik von den Daten und der
Programmlogik getrennt werden?
76
Eingabe, Verarbeitung, Ausgabe
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 327 / 409
Diese und weitere Fragen müssen von einem Event-Modell beantwortet werden - und dieses
Modell gibt damit den Rahmen - oder auch die Philosophie - vor, in dem ein Programmierer ein
Programm Design erschaffen und implementieren kann und muss.
Hinweis – damit haben wir natürlich ein Problem mit unserer Tic-Tac-Toe Lösung aus Kapitel
14.24 bzw. auch den darauf aufbauenden Lösungen. In dieser Lösung liegt die Steuerung des
Kontrollflußes klar auf der Seite der Spiele-Logik, während uns das GUI Programmier-Modell
diese Option nicht läßt. In einem späteren Kapitel werden wir mögliche Lösungen dieses
Konflikts diskutieren.
18 Event-Modelle von Swing
Swing kennt zwei Event-Modelle:
• das interne Event-Modell – siehe Kapitel 18.2, und
• das externe Event-Modell – siehe Kapitel 18.3.
Achtung – nicht Java kennt zwei Event-Modelle, sondern Swing. Die Sprache stellt AusdrucksElemente zur Verfügung, in deren Rahmen Bibliotheken und Programme entwickelt werden
können. Wie eine Bibliothek (wie z.B. Swing) dann ihr Funktionalität dem Benutzer zur
Verfügung stellt – d.h. wie sie benutzt wird – ist eine Frage der Bibliothek und nicht der
Sprache. Die Sprache gibt den Rahmen vor indem sie sich bewegen kann.
Hinweis – in Java, genau genommen in den GUI Bibliotheken von Java, hat sich die
Philosophie des Event-Handlings, d.h. das Event-Modell, mehrmals geändert. Die EventModelle der JDKs 1.0, 1.1 und 1.2 unterscheiden sich zum Teil sehr stark voneinander. Dies
spiegelt sich weniger in der Sprache wieder77, sondern in erster Linie in den sehr
unterschiedlichen GUI Bibliotheken der JDK Versionen.
18.1 Events und Gruppierungen
Ein unerfahrener Programmierer mag sich die Frage stellen: „Welche Events gibt es
überhaupt?“
Grundsätzlich ist ein Event eine elementare Aktion des Benutzers, die eine Reaktion des
Programms zur Folge haben kann. Dazu gehören z.B.:
• Jede Art der Tastatur-Betätigung.
• Jede Maus-Aktion wie Linksklick, Rechtsklick, Linksdoppelklick, Maus bewegen, usw.
Da das Event-Modell eine Eigenschaft einer Bibliothek ist, sollte es sich eigentlich in keiner
Weise in der Sprache wiederspiegeln. Aber das JDK 1.1 ist u.a. um die inneren Klassen –
siehe Kapitel 15 – erweitert worden, um das neue externe Event-Modell eleganter
programmieren zu können.
77
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 328 / 409
• Fenster-Aktionen wie Fenster wird bewegt, vergrößert, verkleinert, minimiert, maximiert,
geschlossen, usw.
• Fokus-Aktionen, d.h. ein Element verliert bzw. bekommt den Fokus.
• uvm.
Die exakt vorhandenen Events sind vom jeweiligen grafischen Element abhängig, das den
Fokus hat. Z.B. hat ein Baum-Element sicher Events für das Selektieren von Einträgen, für das
Auf- und Zuklappen von Teilbäumen, uvm. Diese Events machen aber z.B. für ein einfaches
Fenster gar keinen Sinn.
Da alle grafische Elemente viele Events haben, sind diese in Swing gruppiert. Gerade
komplexere Elemente wie Tabellen und Bäume wären sonst sehr unübersichtlich. So gibt es
z.B. bei den meisten GUI Elementen drei Gruppen von Maus-Events:
• Mouse
• MouseMotion
• MouseWheel
Hinweis – es kann auch programm-interne Events geben, z.B. Timer-Events – siehe Kapitel
20.10 – aber die meisten Events werden durch externe Aktionen des Benutzers angestoßen,
wie Tastatur- oder Maus-Aktionen. Um die Sache einfach zu halten, gehen wir im folgenden
davon aus, dass alle Events von außen angestoßen werden. Ist dies nicht der Fall, durchläuft
das Event nur nicht soviele Stufen – die grundsätzliche Abarbeitung in Swing ist aber immer
gleich.
18.2 Internes Event-Modell
18.2.1 Der Fluß eines Events
Jedes Event, das der Benutzer auslöst – z.B. das Klicken mit der linken Maus-Taste in ein
Fenster – durchläuft mehrere Stufen:
• Das Betriebssystem bildet dabei die unterste Ebene. Es interagiert mit Hilfe von Treibern mit
der Hardware, und wird von jeder Aktion unterrichtet. Das BS interpretiert die Aktion, und
leitet sie gegebenenfalls an das aktive Programm weiter – in unserem Fall die JVM.
• Die JVM kennt das aktuelle Fenster – genau genommen das grafische Element, das den
Fokus hält – und ruft eine entsprechende Event-Behandlungs-Funktion auf.
• Das aktive grafische Element hat sich beim Erzeugen automatisch bei der JVM registriert –
dies passiert automatisch in den Konstruktoren der Basisklassen - und hat die
entsprechende Event-Behandlungs-Funktion möglicherweise überschrieben. So erfährt es
von dem Event und kann darauf reagieren.
• Hierbei wird zuerst die Funktion „processEvent“ aufgerufen, die das Event dann auf die
jeweiligen Event-Gruppen „process“ Funktionen verteilt - siehe Kapitel 18.2.2.
Abb. 18-1 : Der Fluß eines Events
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 329 / 409
18.2.2 Swings „process“ Funktionen und Event-Klassen
Für jede Gruppe gibt es in den Swing Klassen „process“ Funktionen, die bei den jeweiligen
Events aufgerufen werden. Hier ein Teil der „process“ Funktionen der Fenster Klasse
„JFrame“78:
• protected void processWindowEvent(WindowEvent e)
• protected void processWindowFocusEvent(WindowEvent e)
• protected void processWindowStateEvent(WindowEvent e)
• protected void processKeyEvent(KeyEvent e)
• protected void processMouseEvent(MouseEvent e)
• protected void processMouseMotionEvent(MouseEvent e)
• protected void processMouseWheelEvent(MouseWheelEvent e)
Alle diese Funktionen sind „protected“, da sie nicht von außen aufgerufen werden sollen, aber
natürlich zum Überschreiben gedacht sind, d.h. in den abgeleiteten Klassen ansprechbar sein
müssen.
Alle „process“ Funktionen bekommen ein Event-Objekt als Parameter, das detailierte
Informationen zum jeweiligen Event enthält. Welche Informationen das jeweils sind, ist vom
jeweiligen abhängig.
Die Namen der Event-Klassen entsprechen häufig dem dem der jeweiligen „process“ Funktion
ohne Präfix „process“, d.h. die Funktion „processKeyEvent“ z.B. bekommt einen Parameter
vom Typ „KeyEvent“.
Die Event-Klassen sind in den verschiedensten Packages definiert. Aber die grundlegenden
Event-Klassen wie „WindowEvent“, „KeyEvent“ oder „MouseEvent“ kommen aus
„java.awt.event“.
Achtung – im Normallfall sollte eine überschriebene „process“ Funktion zuerst die
überschriebene Basis-Klassen Funktion aufrufen. Geschieht dies nicht, schneiden sie die
geerbte Event-Behandlung vom Event-Fluß ab – d.h. sie entfällt komplett. Im Normallfall
werden sie dies nicht erreichen wollen, ganz im Gegenteil.
Achtung - ein Teil der „process“ Funktionen - wie z.B. „processMouseMotionEvent“ - müssen
explizit aktiviert werden. Der Grund ist, dass manche dieser Events sehr häufig passieren
können - z.B. Bewegungen der Maus. Je nach Event-Gruppe, Plattform und Anwendung
könnte diese aufwändige Event-Verarbeitung zu einer spürbaren PerformanceVerschlechterung führen. Auf modernen Hardware-Plattformen sollte dies zwar nicht der Fall
sein, aber beim Design von Swing wurde hier auf „Nummer Sicher“ gegangen.
„process“ Funktionen können explizit mit der Funktion „enableEvents“ und einer passenden
Die Klasse „JFrame“ hat - obwohl aus Sicht des GUIs eine recht einfache Klasse - immerhin
schon 14 „process“ Funktionen (Stand JDK 1.4.2).
78
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 330 / 409
Bitmaske an- und ausgestellt werden - siehe z.B. Kapitel 18.2.4. Implizit werden sie auch mit
der Registrierung eines entsprechenden Listener-Objekts aktiviert - siehe Kapitel 18.3.2.
18.2.3 Beispiel „WindowClose“
In Kapitel 16.1 haben wir gelernt wie man ein erreicht, dass das Schließen eines „JFrame“
automatisch auch das Programm beendet. Selbst wenn die dort dargestellt Methode sicher für
die meisten Fälle ausreichend und sinnvoll ist, wollen wir hier als Beispiel dies mal selber
implementieren.
Dazu muß man wissen, dass das Schließen eines Fensters unter die normalen Window-Events
fällt.
• Daraus folgt, dass wir in unserer eigenen Frame-Klasse die Funktion „processWindowEvent“
überschreiben müssen.
• Dann wollen wir auf den Event-Typ „WindowEvent.WINDOW_CLOSING“ reagieren, der mit
„getID()“ abgefragt werden kann.
• In diesem Fall wollen wir das Programm direkt beenden - dies geht mit „System.exit(0)“.
• Und auch hier gilt - wie in Kapitel 18.2.2 und Kapitel 16.3 erklärt - dass natürlich zuerst die
Basis-Klassen Funktion „processWindowEvent“ aufgerufen werden sollte.
Alles zusammen ergibt das dann folgendes Programm:
public class Appl {
public static void main(String[] args) {
MyFrame frame = new MyFrame("Fenster mit eigenem Programm-Ende");
// Nicht mehr notwendig - darum kuemmern wir uns jetzt selbst
// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame(String title) {
super(title);
}
protected void processWindowEvent(WindowEvent e) {
super.processWindowEvent(e);
if (e.getID() == WindowEvent.WINDOW_CLOSING) {
System.exit(0);
}
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 331 / 409
18.2.4 Beispiel „Scribble“
Um das interne Event-Modell näher zu verstehen, wollen wir eine erste Version eines „Scribble“
Programms mit Hilfe des internen Event-Modells implementieren.
Scribble ist quasi ein extrem einfaches Zeichen-Programm, bei dem sie mit der Maus Kurven
und Linien in das Fenster zeichnen können.
• Mit dem Drücken einer beliebigen Maus-Taste beginnt das Zeichnen.
• Solange die Maus-Taste gedrückt ist, wird entsprechend den Maus-Bewegungen
gezeichnet.
• Mit dem Loslassen der Maus-Taste endet die Zeichen-Aktion.
Abb. 18-2 : Ein einfaches Scribble Zeichen-Programm
Als erstes müssen wir uns überlegen, wie das Programm prinzipiell arbeiten soll:
• Die gezeichneten Striche sind Geraden zwischen den Punkten, die für die Maus-Bewegung
als Event gemeldet werden.
• Für den ersten Strich benötigen wir also die x/y Koordinate der Maus beim Maus-Klick und
nach der ersten Maus-Bewegung. Die x/y Koordinaten der Maus beim Maus-Klick müssen
also gespeichert werden - hierzu werden zwei Attribute „lastX“ und „lastY“ in der FensterKlasse definiert.
• Für alle weiteren Striche werden die x/y Koordinaten zwischen Ende des letzten Strichs und
der jeweils nächsten Maus-Bewegung benötigt - auch hier benutzen wir zur Speicherung die
Attribute „lastX“ und „lastY“.
• Das Loslassen der Maus-Taste ist kein relevantes Event, da es an einer Maus-Position
passiert, die schon als Maus-Bewegung gemeldet wurde.
• Damit auch nur ein einzelner Maus-Klick gezeichnet wird, muss noch der erste Punkt explizit
gezeichnet werden.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 332 / 409
Folgende Events sind also für Scribble wichtig:
• Eine beliebige Maus-Taste wurde gedrückt - dies fällt unter die normalen Maus-Events, d.h.
die Funktion „processMouseEvent“ muss überschrieben werden. Das relevante Event ist
das mit der ID „MouseEvent.MOUSE_PRESSED“ - die x/y Koordinaten können mit „getX“
und „getY“ erfragt werden.
• Das Ziehen mit gedrückter Maus-Taste muss verfolgt werden - dies ist eine Drag-Aktion und
fällt unter die Maus-Motion-Events, d.h. die Funktion „processMouseMotionEvent“ muss
überschrieben werden. Das relevante Event ist hier das mit der ID
„MouseEvent.MOUSE_DRAGGED“.
Und so ergibt sich folgender Quelltext für unser „Scribble 1“. Denken sie daran, dass beide
Event-Gruppen explizit mit „enableEvents“ aktiviert werden müssen - siehe auch Kapitel 18.2.2.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
JFrame frame = new ScribbleFrame("Scribble 1 : internes Event-Modell");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ScribbleFrame extends JFrame {
private int lastX;
private int lastY;
public ScribbleFrame(String title) {
super(title);
enableEvents(
AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
}
protected void processMouseEvent(MouseEvent e) {
super.processMouseEvent(e);
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
onMousePressed(e);
}
}
protected void processMouseMotionEvent(MouseEvent e) {
super.processMouseMotionEvent(e);
if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
onMouseDragged(e);
}
}
private void onMousePressed(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, lastX, lastY);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 333 / 409
}
private void onMouseDragged(MouseEvent e) {
int x = e.getX();
int y = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, x, y);
lastX = x;
lastY = y;
}
}
Hiemit haben sie ein ganz einfaches Scribble programmiert, das aber genau unter der in
Kapitel 17.1 beschriebenen Problematik leidet: „Der Bildschirminhalt kann und wird auf
Anforderung nicht neu gezeichnet, d.h. er geht unter Umständen verloren.“
Probieren sie es aus: Minimieren sie z.B. das Fenster, schieben sie es in den Hintergrund, oder
verschieben sie es zum Teil aus dem Bildschirmbereich, und stellen sie es dann wieder her.
Der Bildschirm ist dann leer. Das Fenster muss neu gezeichnet werden, und zum Teil wird es
das auch, denn um Rahmen, Titelleiste, usw. kümmern sich die Swing Basis-Klassen. Aber
unser Inhalt fehlt, da wir uns nicht darum kümmern.
Wir werden dieses Problem in Kapitel 18.5 lösen, aber vorher schauen wir uns das externe
Event-Modell an.
18.3 Externes Event-Modell
Das externe Event-Modell baut auf das sogenannte Observer Pattern auf. Von daher macht es
Sinn, das Oberver-Pattern kurz für sich zu besprechen.
„Design Pattern“ bzw. Entwurfsmuster79 stellen mehr oder weniger fest umrissene DesignEntwürfe für konkrete Problem-Situationen dar. Sie enthalten keine exakte Code-Vorgabe,
sondern sie erklären das konzeptionelle Klassen-Gerüst und die Verteilung der Aufgaben, um
ein Problem zu lösen. Normalerweise finden sie d.h. keinen fertigen Quelltext, der nur noch
abgetippt werden muß, wie z. B. bei Algorithmen, sondern sie müssen selbständig den Entwurf
in passenden Code für ihr Projekt überführen.
18.3.1 Observer Pattern
18.3.1.1 Problem
Ein häufiges Problem ist, dass parallel mehrere (oftt unterschiedliche) Sichten auf einen DatenBestand existieren. Wird der Daten-Bestand nun über eine der Sichten geändert, sollen sich
alle anderen mit aktualisieren.
Entwurfsmuster sind ein wichtiges Thema in der Software-Entwicklung. Die Bibel der
Entwurfsmuster ist das sehr empfehlenswerte Buch „Entwurfsmuster“ von Gamma, Helm,
Vlissides und Johnson.
79
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 334 / 409
Ein sehr großes Beispiel wäre eine Daten-Visualisierung, bei der die Daten sowohl in Form
einer Tabelle als auch in Form verschiedener Grafiken angezeigt werden. Werden nun die
Daten in einer Sicht manipuliert, so sollen sich die anderen Sichten parallel anpassen
Ein anderes – vielleicht aktuelleres Beispiel – ist Eclipse. Änderungen im Quelltext werden
simultan im Package-Explorer, im Outline und in anderen Sichten angezeigt.
Das grundsätzliche Problem „ich-bin-interessiert-an-Änderungen-der-Daten“ ist ein StandardProblem der Software-Entwicklung und exisitiert in allen Größen-Ordnungen. Aber wie löst man
es verständlich, wartbar, übersichtlich, erweiterbar, wiederverwendbar, usw. in einer objektorientierten Sprache? Die Antwort ist das Observer-Pattern.
18.3.1.2 Lösung
•
•
•
•
Allgemeine abstrakte Basis-Klasse für die Daten
Allgemeines Interfaces für die Sichten
Kopplung nur zwischen der Basis-Klasse und dem Interface
Benachrichtigungs-Mechanismus wird nur einmal in der Daten-Basis-Klasse implementiert,
die konkreten Daten erben ihn.
Abb. 18-3 : Klassen-Diagramm Observer-Pattern
Abb. 18-4 : Interaktion Observer-Pattern
18.3.1.3 Betrachtung der Abhängigkeiten der Klassen
todo...
18.3.1.4 Umsetzung in Java
todo...
Abb. 18-5 Klassen-Diagramm Observer-Pattern in Java
import java.util.ArrayList;
import java.util.Iterator;
public class Observable {
private ArrayList observers = new ArrayList();
public void addObserver(Observer obs) {
observers.add(obs);
}
public void notifyObservers() {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 335 / 409
Iterator i = observers.iterator();
while (i.hasNext()) {
Observer obs = (Observer)i.next();
obs.update();
}
}
}
public class Data extends Observable {
private String date = "";
public void setDate(String arg) {
date=arg;
super.notifyObservers();
}
public String getDate() {
return date;
}
}
public interface Observer {
public void update();
}
import java.io.*;
public class View1 implements Observer {
private Data data;
public View1(Data d) {
data = d;
data.addObserver(this);
}
public void update() {
System.out.println("View 1 Daten \"" + data.getDate() + '"');
}
public void editData() {
System.out.print("View 1 - bitte geben sie neue Daten ein:\n> ");
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
String in = reader.readLine();
data.setDate(in);
}
catch (Exception x) {
}
}
}
import java.io.*;
public class View2 implements Observer {
private Data data;
public View2(Data d) {
data = d;
data.addObserver(this);
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 336 / 409
public void update() {
System.out.println("View 2 Daten \"" + data.getDate() + '"');
}
public void editData() {
System.out.print("View 2 - bitte geben sie neue Daten ein:\n> ");
try {
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
String in = reader.readLine();
data.setDate(in);
}
catch (Exception x) {
}
}
}
public class Appl {
public static void main(String[] args) {
Data data = new Data();
View1 view1 = new View1(data);
View2 view2 = new View2(data);
data.setDate("Aus main");
view1.editData();
view2.editData();
}
}
mögliche Ausgabe
View 1 Daten "Aus main"
View 2 Daten "Aus main"
View 1 - bitte geben sie neue Daten ein:
> Dateneingabe via View1
View 1 Daten "Dateneingabe via View1"
View 2 Daten "Dateneingabe via View1"
View 2 - bitte geben sie neue Daten ein:
> Und was aus View 2
View 1 Daten "Und was aus View 2"
View 2 Daten "Und was aus View 2"
Hinweis – da das Oberver-Pattern ein Standard-Problem in der Software-Entwicklung ist, gibt
es schon seit dem JDK 1.0 in der Java Bibliothek die Klasse „java.util.Observer“ und das
Interface „java.util.Observable“. Für viele Probleme sind diese Klassen allemal ausreichend.
18.3.2 Listener-Interfaces
Das externe Event-Modell ist im Prinzip die konsequente Anwendung des Observer Patterns
auf Events. Jedes Event wird quasi als Daten-Änderung eines Observables betrachtet und an
alle registrierten Observer gesendet.
Für jede Event-Gruppe – siehe Kapitel todo – gibt es ein entsprechendes Listener-Interface
(das entspricht quasi dem Observer-Interface im Observer-Pattern). Und für jedes Event der
Event-Gruppe gibt es eine abstrakte Funktion, die überschrieben werden muss (sie
entsprechen quasi jeweils der „update“ Funktion des Observer-Interfaces.
Hier beispielhaft die Definitionen der Listener-Interfaces für die Maus- und Maus-Motion-Events
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 337 / 409
aus dem Package „java.awt.event“:
package java.awt.event;
public interface MouseListener implements EventListener {
public
public
public
public
public
void
void
void
void
void
mouseClicked(MouseEvent e);
mousePressed(MouseEvent e);
mouseReleased(MouseEvent e);
mouseEntered(MouseEvent e);
mouseExited(MouseEvent e);
}
package java.awt.event;
public interface MouseMotionListener implements EventListener {
public void mouseDragged(MouseEvent e);
public void mouseMoved(MouseEvent e);
}
Jedes GUI-Element, dass eine Event-Gruppe unterstützt, bietet entsprechende Funktionen an,
um Listener beim GUI-Element zu registrieren und wieder abzumelden. So hat die Klasse
„JFrame“ u.a. die Funktionen:
• public void addMouseListener(MouseListener l)
• public void removeMouseListener(MouseListener l)
• public void addMouseMotionListener(MouseMotionListener l)
• public void removeMouseMotionListener(MouseMotionListener l)
Hinweis – die Listener-Interfaces heißen immer wie die Event-Gruppen mit Postfix „Listener“.
Die zugehörigen „add“ und „remove“ Funktionen heißen immer wie die Listener-Interfaces mit
Präfix „add“ bzw. „remove“.
Wir könnten unser Scribble also auch folgendermassen implementieren: todo...
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ScribbleFrame
extends JFrame implements MouseListener, MouseMotionListener {
private int lastX;
private int lastY;
public ScribbleFrame(String title) {
super(title);
addMouseListener(this);
addMouseMotionListener(this);
}
private void onMousePressed(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, lastX, lastY);
}
private void onMouseDragged(MouseEvent e) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 338 / 409
int x = e.getX();
int y = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, x, y);
lastX = x;
lastY = y;
}
public void mouseClicked(MouseEvent e) {
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
onMousePressed(e);
}
public void mouseReleased(MouseEvent e) {
}
public void mouseDragged(MouseEvent e) {
onMouseDragged(e);
}
public void mouseMoved(MouseEvent e) {
}
}
18.3.3 Listener-Adapter-Klassen
Die notwendige Implementierung aller Funktionen der Listener-Interfaces ist häufig nervig,
denn in der Praxis werden viele Funktionen leer überschrieben, da das Event für die
Anwendung nicht wichig ist – siehe z.B. Quelltext „Scribble2“ in Kapitel 18.3.2.
Eine Alternative ist hier die Verwendung der Listener-Adapter-Klassen statt der ListenerInterfaces. Die Adapter-Klassen implementieren alle Funktionen des jeweiligen Interfaces mit
leeren Funktionen.
Hier beispielhaft die Definitionen der Listener-Adapter-Klassen für die Maus- und Maus-MotionEvents aus dem Package „java.awt.event“:
package java.awt.event;
public class MouseAdapter implements MouseListener {
public
public
public
public
public
void
void
void
void
void
mouseClicked(MouseEvent e) {}
mousePressed(MouseEvent e) {}
mouseReleased(MouseEvent e) {}
mouseEntered(MouseEvent e) {}
mouseExited(MouseEvent e) {}
}
package java.awt.event;
public class MouseMotionAdapter implements MouseMotionListener {
public void mouseDragged(MouseEvent e) {}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 339 / 409
public void mouseMoved(MouseEvent e) {}
}
Das es überhaupt Interfaces gibt liegt daran, dass Java keine Mehrfach-Vererbung von Klassen
kennt, und daher z.B. das „Scribble2“ sich nicht von „JFrame“, „MouseAdapter“ und
„MouseMotionAdapter“ ableiten kann sondern nur von einer Klasse – siehe Kapitel todo.
Dem gegenüber kann eine Klasse beliebig viele Interfaces implementieren, und kann so
mehrere Aufgaben übernehmen – siehe auch Kapitel todo.
Hinweis – die Listener-Adapter-Klassen heißen immer wie die Event-Gruppen mit Postfix
„Adapter“.
18.3.4 Innere Klassen
Das „Scribble2“ in Kapitel todo ist erstmal kein wirklicher Fortschritt gegenüber dem „Scribble1“
in Kapitel todo – eher im Gegenteil, da auch die unbenutzten Interface-Funktionen leer
implementiert werden müssen.
Aber das externe Event-Modell hat gegenüber dem internen einen wichtigen Vorteil:
• Im internen Event-Modell muss man immer eine eigene Klasse schreiben, die sich von der
normalen Klasse ableitet und die entsprechende „process“ Funktion überschreibt. Eine
solche eigene abgeleitete Klasse ist aber spezifisch für das aktuelle Problem und könnte
daher nicht wiederverwendet werden. Daher benötigt man dann viele abgeleitete Klassen
mit sehr ähnlichem Code, was sehr unschön ist.
• Im externen Event-Modell dagegen können sie einen Observer beim zu beobachtenden
Element registrieren, ohne das sie dieses anpassen müßten.
Um diesen Unterschied zwischen den beiden Event-Modellen noch mal klar zu machen,
implementieren wir mit beiden Strategien die gleiche Aufgabe:
• Ausgabe der Maus-Koordinaten auf der Console bei Drücken einer beliebigen Maus-Taste in
einem Fenster.
• D.h. konkret soll beim Drücken einer beliebigen Maus-Taste in einem Fenster die möglichst
„private“ Element-Funktion „out“ der Klasse „Appl“ mit den x- und y-Koordinaten der Maus
aufgerufen werden.
• Die Element-Funktion „out“ der Klasse „Appl“ soll die Koordinaten dann mit einer Meldung
auf der Console ausgeben.
Bei der Aufgabe geht es darum, dass ein Event (hier Maus-Taste drücken) eines GUI-Elements
(hier das Fenster) für ein Objekt einer anderen Klasse (hier „Appl“) eine Aktion auslöst (d.h.
eine Element-Funktion aufruft). Dies ist eine Standard-Aufgabe. Denken sie z.B. an einen
Button in einem Fenster. Wird der Button betätigt soll im Fenster irgendwas passieren. Die
eigentliche Aktion ist also sicher nicht Bestandteil des Buttons, sondern des Fensters. Das
Fenster in unserer Aufgabe entspricht hier also dem Button, das „Appl“ Objekt dem Fenster,
und die „out“ Funktion der Aktion. Und da die Aktion ein Teil des Verhaltens des Gesamt© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 340 / 409
Moduls ist, sollte „out“ möglichst „private“ sein.
Hinweis – die Aufgabe ist absichtlich so gewählt, da sie mit unserem aktuellen Wissen
problemlos implementierbar ist, denn wir kennen die Fenster-Klasse „JFrame“ und die MausEvents, aber z.B. noch keine Buttons.
18.3.4.1 Implementierung mit dem internen Event-Modell
Um die Aufgabe mit dem internen Event-Modell zu lösen, muss:
• eine eigene Fenster-Klasse von „JFrame“ abgeleitet werden,
• die Maus-Events im Konstruktor enabled werden, und
• die entsprechende „process“ Funktion überladen werden.
Außerdem muß die Element-Funktion „out“ des „Appl“ Objekts aufgerufen werden können dazu muß das „Appl“ in unserer Fenster-Klasse bekannt sein - d.h. implementieren wir ein
entsprechendes Attribut „appl“ und setzen dies im Konstruktor, der nun natürlich auch einen
solchen Parameter benötigt. Die Funktion „out“ kann hierbei leider nicht „private“ sein.
Der Rest ist Pflicht, und sollte kein Problem mehr sein.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
private void run() {
MyFrame frame = new MyFrame(this);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
public void out(int x, int y) {
System.out.println("Maus-Pressed an " + x + "/" + y);
}
}
import java.awt.AWTEvent;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
public class MyFrame extends JFrame {
private Appl appl;
public MyFrame(Appl a) {
super("Maus-Pressed Koordinaten auf Console via internem Event-Modell");
appl = a;
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
protected void processMouseEvent(MouseEvent e) {
super.processMouseEvent(e);
if (e.getID() == MouseEvent.MOUSE_PRESSED) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 341 / 409
appl.out(e.getX(), e.getY());
}
}
}
18.3.4.2 Implementierung mit dem externen Event-Modell
Die Implementierung mit dem externen Event-Modell ist hier ganz einfach.
• Da „Appl“ keine Basis-Klasse hat80, können wir „Appl“ direkt von „MouseAdapter“ ableiten,
und sind nicht auf das Interface „MouseListener“ angewiesen. Darum brauchen wir auch nur
die „process“ Funktion überladen, die uns interessieren – siehe Kapitel todo.
• Da wir das externe Event-Modell benutzen und keine speziellen Fenster-Fähigkeiten
brauchen, können wir direkt die Fenster-Klasse „JFrame“ nehmen.
• Jetzt können wir die „out“ Funktion auch „private“ machen – siehe Aufgaben-Stellung.
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
public class Appl extends MouseAdapter {
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
private void run() {
JFrame frame = new JFrame("M-P Ko auf Console via externem Event-Modell");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.addMouseListener(this);
frame.setVisible(true);
}
public void mousePressed(MouseEvent e) {
out(e.getX(), e.getY());
}
private void out(int x, int y) {
System.out.println("Maus-Pressed an " + x + "/" + y);
}
}
Die Implementierung mit dem externen Event-Modell ist hier so einfach da:
• „Appl“ keine Basis-Klasse hat.
• Wir nur für ein GUI-Element „mousePressed“ überschreiben müssen.
18.3.4.3 Implementierung mit einer non-static Member-Klasse
Was wäre aber, wenn es zwei Fenster gäbe, für die die „MousePressed“ Events überwacht
werden müssen? Das ginge natürlich auch, aber dann müßte die Funktion „mousePressed“
Jedenfalls keine explizite von uns. Natürlich ist „Appl“ implizit von „java.lang.Object“
abgeleitet.
80
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 342 / 409
unterscheiden, welches Fenster das Event abgetriggert hat. Prinzipiell funktioniert das, da die
Event-Klassen (hier „MouseEvent“) u.a. auch Informationen über das Source-Element enthalten
– die Frage, die sich stellt, ist: „Will man das?“
Übertragen auf ein Fenster mit vielen Buttons hieße das z.B., dass die „mousePressed“
Funktion alle Buttons kennen und unterscheiden können muß. Das ergibt weder schönen noch
wirklich wartbaren Code. Besser wäre es sicher, pro Button eine eigene kleine unabhängige
„process“ Funktion zu haben.
Das Problem ist doch eigentlich, dass man folgendes braucht:
• Eine von „MouseListener“ abgeleitete „kleine“ Klasse.
• Am besten eine spezielle Klasse ohne explizite Basis-Klasse, damit man „MouseAdapter“
benutzen kann, und sie sich nur um die Event-Gruppe kümmern muss.
• Ein Objekt dieser Klasse muss mit dem Objekt der übergeordneten Klasse verbunden sein –
in unserem Beispiel z.B. mit dem „Appl“ Objekt, oder mit dem Fenster-Objekt im Button
Beispiel.
• Und es wäre schön, wenn sie auf die „private“ Elemente des übergeordneten Objekts
zugreifen könnte – z.B. auf „private“ Funktionen – um das Modul-Konzept der
übergeordneten Klasse nicht aufzubrechen.
Erinnern sie sich? So was gibt es, und nennt sich „eingebette nicht-static Klasse“, „innere nichtstatic Klasse“, „Member-Klasse“ oder auch „Element-Klasse“ – siehe Kapitel 15.2.
Schreiben wir unser Beispiel mal auf eine Member-Klasse um.
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
public class Appl {
class MouseEventTarget extends MouseAdapter {
public void mousePressed(MouseEvent e) {
out(e.getX(), e.getY());
}
}
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
private void run() {
JFrame frame = new JFrame("M-P Koor. auf Console mit Member-Klasse");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.addMouseListener(new MouseEventTarget());
frame.setVisible(true);
}
private void out(int x, int y) {
System.out.println("Maus-Pressed an " + x + "/" + y);
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 343 / 409
18.3.4.4 Implementierung mit einer lokalen Klasse
Da die Klasse nur an einer Stelle benötigt wird, können wir sie auch zu einer lokalen Klasse
machen – siehe Kapitel 15.4.
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
private void run() {
JFrame frame = new JFrame("M-P Koor. auf Console mit lokaler Klasse");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
class MouseEventTarget extends MouseAdapter {
public void mousePressed(MouseEvent e) {
out(e.getX(), e.getY());
}
}
frame.addMouseListener(new MouseEventTarget());
frame.setVisible(true);
}
private void out(int x, int y) {
System.out.println("Maus-Pressed an " + x + "/" + y);
}
}
18.3.4.5 Implementierung mit einer anonymen Klasse
Und da auch nur ein Objekt der Klasse benötigt wird, kann man daraus auch eine anonyme
Klasse machen – siehe Kapitel 15.5.
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
Appl appl = new Appl();
appl.run();
}
private void run() {
JFrame frame = new JFrame("M-P Koor. auf Console mit anonymer Klasse");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
out(e.getX(), e.getY());
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 344 / 409
}
});
frame.setVisible(true);
}
private void out(int x, int y) {
System.out.println("Maus-Pressed an " + x + "/" + y);
}
}
Die Verwendung einer anonymen Klasse – abgeleitet von einer Event-Adapter-Klasse –
als Event-Listener-Klasse ist das normale Verfahren zum Event-Handling in Swing.
18.3.5 Scribble 3 mit anonymen Listener-Klassen
Lassen sie uns jetzt unser Scribble mit dem externen Event-Modell und anonymen ListenerKlassen implementieren.
import javax.swing.JFrame;
public class Appl {
public static void main(String[] args) {
JFrame frame = new ScribbleFrame("Scrib 3 mit anonymen Listener-Klassen");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(20, 20);
frame.setSize(600, 400);
frame.setVisible(true);
}
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ScribbleFrame extends JFrame {
private int lastX;
private int lastY;
public ScribbleFrame(String title) {
super(title);
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
onMousePressed(e);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
onMouseDragged(e);
}
});
}
private void onMousePressed(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, lastX, lastY);
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 345 / 409
private void onMouseDragged(MouseEvent e) {
int x = e.getX();
int y = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, x, y);
lastX = x;
lastY = y;
}
}
18.4 Vergleich der Event-Modelle
Wann nimmt man nun welches Event-Modell?
• Im Normallfall ist das externe Event-Modell vorzuziehen. Im Zusammenspiel mit anonymen
Listener-Klassen ist es einfach und unproblematisch zu verwenden.
• Bei der Entwicklung eigener bzw. spezialisierter GUI-Elemente kann es Sinn machen, sich in
die interne Event-Verarbeitung einzuklinken. Auf dieser Ebene hat man mehr viel Einfluss
und Möglichkeiten, der bei der Entwicklung eigener Elemente notwendig sein kann. Aber
man kann auch viel kaputt machen, da ein Eingriff in die interne Event-Verarbeitung eine
Operation am offenen Herzen des Elements ist. Man sollte daher wissen, was man macht
und was man will. Ansonsten sollte man besser die Finger davon lassen.
18.5 Scribble 4 mit Daten-Modell
Bleibt zum Schluß noch eine Sache zu tun: die versprochene Implementierung81 eines
Scribbles, das auch nach dem Neu-Zeichnen sein Bild darstellt – siehe Kapitel 17.1.
Im Prinzip ist das ganz einfach: wir müssen uns das gezeichnete Bild merken, und es auf Abruf
(d.h. in der Funktion „paint“ – siehe Kapitel 16.3) zeichnen. Dieses Problem zwingt uns dazu,
uns Gedanken zu machen, was denn unser Scribble-Bild eigentlich ist bzw. wie es aufgebaut
ist:
• Jede x/y-Koordinate ist ein Punkt.
• Mit der Maus zeichnet der Benutzer Linien-Züge, d.h. eine Aneinander-Reihung (oder
Verkettung) von Linien – sogenannte „Polygone“. Hierbei ist der Extrem-Fall zu beachten,
dass ein Polygon auch nur aus einem einzelnen Punkt bestehen kann. Ein Polygon ist also
eine Reihe von beliebig vielen, aber mindestens einem Punkt.
• Von diesen Polygonen kann das Scribble beliebig viele enthalten.
Aus dieser Problem-Analyse ergeben sich zwangsläufig folgende notwendige Klassen:
• Eine Klasse für Punkte, die x/y- Koordinaten aufnehmen kann. Hiermit sind wir schnell fertig,
da es im Package „java.awt“ eine einfache Klasse „Point“ für Punkte gibt.
• Eine Klasse „Polygon“ für Polygone, die beliebig viele Punkte aufnehmen kann. Um
mindestens einen Punkt zu garantieren, wird der erste Punkt direkt im Konstruktor
übergeben.
81
Siehe Ende Kapitel 18.2.4.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 346 / 409
• Und eine Klasse „PaintModel“ für das gesamte Bild, die beliebig viele Polygone aufnehmen
kann. Die Klasse heißt „PaintModel“ und nicht „Picture“, da man in der SoftwareEntwicklung häufig von Modellen redet, wenn strukturierte Daten gemeint sind. Z.B. werden
wir bei Swing-Tabellen noch sogenannte „TableModel‘s“ kennen lernen – siehe Kapitel
todo.
Damit wäre Schritt 1 unserer Klassen-Entwicklungs-Überlegungen – siehe Kapitel 11.2 –
erledigt.
1. Überlegung - Art der Objekte
2. Überlegung - Schnittstelle
3. Überlegung - Vererbung
4. Überlegung - Implementierung
Weitere Überlegungen – todo...
import
import
import
import
java.awt.Graphics;
java.awt.Point;
java.util.ArrayList;
java.util.Iterator;
public class Polygon {
private ArrayList points = new ArrayList();
// Ersten Punkt doppelt einfuegen, damit mindestens
// zwei Punkte da sind - siehe Funktion 'paint'.
public Polygon(Point p) {
points.add(p);
points.add(p);
}
public void addPoint(Point p) {
points.add(p);
}
// Diese Funktion setzt voraus, dass mindestens zwei Punkte
// vorhanden sind - der Konstruktor garantiert dies.
// - waere kein Punkt da
=> Exception in Zeile (*)
// - waere nur ein Punkt da => kein Zeichnen, da die Schleife
//
nie betreten wird.
public void paint(Graphics g) {
Iterator it = points.iterator();
Point start = (Point) it.next();
// (*)
while (it.hasNext()) {
Point end = (Point) it.next();
g.drawLine(start.x, start.y, end.x, end.y);
start = end;
}
}
}
import
import
import
import
java.awt.Graphics;
java.awt.Point;
java.util.ArrayList;
java.util.Iterator;
public class PaintModel {
private ArrayList polygons = new ArrayList();
private Polygon actuellPolygon;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 347 / 409
public void addPolygon(Point p) {
actuellPolygon = new Polygon(p);
polygons.add(actuellPolygon);
}
public void addPoint(Point p) {
actuellPolygon.addPoint(p);
}
public void paint(Graphics g) {
Iterator it = polygons.iterator();
while (it.hasNext()) {
Polygon poly = (Polygon) it.next();
poly.paint(g);
}
}
}
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ScribbleFrame extends JFrame {
private int lastX;
private int lastY;
private PaintModel model = new PaintModel();
public ScribbleFrame(String title) {
super(title);
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
onMousePressed(e);
}
});
addMouseMotionListener(new MouseMotionAdapter() {
public void mouseDragged(MouseEvent e) {
onMouseDragged(e);
}
});
}
private void onMousePressed(MouseEvent e) {
lastX = e.getX();
lastY = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, lastX, lastY);
model.addPolygon(new Point(lastX, lastY));
}
private void onMouseDragged(MouseEvent e) {
int x = e.getX();
int y = e.getY();
Graphics g = getGraphics();
g.drawLine(lastX, lastY, x, y);
lastX = x;
lastY = y;
model.addPoint(new Point(x, y));
}
public void paint(Graphics g) {
super.paint(g);
model.paint(g);
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 348 / 409
18.6 Aufgaben
18.6.1 Aufgabe „MainFrame“
Schreiben sie eine eigene Frame-Klasse „MainFrame“, die von „JFrame“ abgeleitet ist, aber
beim Schliessen des Fensters immer automatisch das Programm beendet.
• Welche Möglichkeiten haben sie diese Aufgabe zu lösen?
• Welche Vor- und Nachteile haben die einzelnen Lösungen?
• Welche würden sie bevorzugen?
18.6.2 Aufgabe „Scribble 5 zeichnet Rechtecke“
Implementieren sie mit dem externen Event-Modell ein Rechteck-Scribble, dass statt LinienZüge Rechtecke zeichnet.
• Entwickeln sie zuerst eine Version ohne Modell, und konzentrieren sie sich auf die EventBehandlung und die eigentliche Zeichen-Aufgabe.
• Danach entwickeln sie ein passendes Modell, und integrieren es in ihr Rechteck-Scribble.
19 Swing Layouts
Für viele GUI-Elemente gibt es in Swing natürlich fertige Klassen, z.B. für verschiedene Arten
von Buttons, für Labels, Eingabe-Felder, Listen, Tabellen, Bäume, uvm. Ein Teil dieser Klassen
besprechen wir in Kapitel 20. Bevor wir diese Elemente besprechen, sollten wir aber wissen,
wie man sie in ein Fenster einfügt und positioniert.
Das ganze hat was von der Henne/Ei-Problematik. Man kann keine Elemente in ein Fenster
einfügen, wenn man keine Layouts kennt. Aber wie soll man Layouts erklären, wenn man keine
Elemente zum Einfügen hat.
Wir lösen das ganz pragmatisch – in Kapitel 19.1 führen wir ein einfaches GUI-Element ein,
einen Button. Danach besprechen wir die Layouts, und nutzen erstmal für alle Beispiele nur
Buttons.
Aber warum gibt es überhaupt Layouts? Man könnte doch einfach die GUI Elemente
pixelgenau positionieren und fertig!? Dieses Vorgehen hat in der Praxis viele Probleme, da z.B.
Schriftgrößen sehr unterschiedlich sind, und damit eine pixelgenaue Postionierung nicht
sinnvoll ist. Außerdem können pixelgenaue Positionierungen z.B. sich nicht von alleine an
wechselnde Fenster-Größen anpassen. Darum gibt es in Swing Layouts, die die Ausrichtung,
Grösse und Position selbständig angleichen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 349 / 409
19.1 Swing Klasse „JButton“
Für einen normalen Button gibt es in Swing die Klasse „JButton“ im Package „javax.swing“. Um
einen Button mit Text auf der Schaltfäche zu erzeugten, muß der Konstruktor „JButton(String
text)“ benutzt werden. Weitere Informationen zur Klasse „JButton“ finden sie in Kapitel 20.3.
Ein GUI-Element wird in das Fenster mit „getContentPane().add“ eingefügt.
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Button");
getContentPane().add(new JButton("Hallo"));
}
}
Dieser Code erzeugt ein Fenster, dass komplett mit einem Button ausgefüllt ist. Und der Button
passt sich automatisch der Größe des Fensters an. Probieren sie es aus!
Abb. 19-1 : Ein Fenster mit einem Button
19.2 Grundlagen
Ein „JFrame“ Fenster unterteilt sich in den sogenannten Client-Bereich und den Rest. Der Rest
sind die Titelzeile, die Rahmen, möglicherweise Menü und Statusleiste, usw. Der innere
Bereich steht dem Programm zur Verfügung und nennt sich Client-Bereich.
Für die Verwaltung des Client-Bereichs enthält ein „JFrame“ Fenster ein sogenantes „ContentPane“ Objekt - vom Typ her ist es ein „java.awt.Container“.
• Zugreifen kann man auf die Content-Pane mit der Funktion „getContentPane()“.
• In diese Content-Pane können GUI-Elemente mit „add“ eingefügt werden.
• Ohne Layout verwaltet die Content-Pane nur das zuletzt eingefügte Element. Im folgenden
Programm z.B. enthält das Fenster nur den Button mit dem Text „3“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 350 / 409
Abb. 19-2 : Die Content-Pane enthält nur den letzten Button
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Nur der letzte Button \"3\" gewinnt");
getContentPane().add(new JButton("1"));
getContentPane().add(new JButton("2"));
getContentPane().add(new JButton("3"));
}
}
Hinweis – GUI-Elemente im Sinne von Swing sind alle Objekte, die von „java.awt.Component“
abgeleitet sind.
19.3 Layouts
Layouts sind Klassen, die mehrere GUI Elemente verwalten können, und diese aneinander
positionieren. Folgende Layouts sind u.a. Teil der Swing Bibliothek:
• Border-Layout
• Flow-Layout
• Grid-Layout
Swing enthält noch viel mehr Layout-Klassen, aber diese drei sind mit Abstand am einfachsten
zu benutzen und decken schon viele Anwendungs-Fälle ab. Aus Zeitmangel werden wir uns
daher auf diese drei beschränken.
19.3.1 Border-Layout
Das Border-Layout teilt sich in einen großen mittleren Bereich (CENTER) und vier SeitenBereiche (NORTH, SOUTH, WEST und EAST) auf. Beim Hinzufügen eines Elements muss der
Ziel-Bereich angegeben werden – dafür existieren entsprechende Konstanten in der Klasse
„BorderLayout“.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 351 / 409
Abb. 19-3 : Fenster mit Border-Layout
import java.awt.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit BorderLayout");
setSize(300, 200);
Container c = getContentPane();
c.setLayout(new BorderLayout());
c.add(new JButton("North"), BorderLayout.NORTH);
c.add(new JButton("West"), BorderLayout.WEST);
c.add(new JButton("East"), BorderLayout.EAST);
c.add(new JButton("South"), BorderLayout.SOUTH);
c.add(new JButton("Center"), BorderLayout.CENTER);
}
}
Beim Border-Layout müssen nicht alle Bereiche gesetzt werden. Sind welche unbesetzt, dann
werden die anderen einfach vergrößert – bis der Client-Bereich abgedeckt ist.
19.3.2 Flow-Layout
Das Flow-Layout ordnet Komponenten zeilenweise von links nach rechts und von oben nach
unten an, wobei es preferredSize für jede Komponente verwendet. Es werden so viele
Komponenten wie möglich in eine Zeile gesetzt, bevor eine neue Zeile begonnen wird.
Abb. 19-4 : Fenster mit Flow-Layout
import java.awt.*;
import javax.swing.*;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 352 / 409
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit FlowLayout");
setSize(320, 170);
Container c = getContentPane();
c.setLayout(new FlowLayout());
c.add(new JButton("Schalter 1"));
c.add(new JButton("Schalter 2"));
c.add(new JButton("Schalter 3"));
c.add(new JButton("Schalter 4"));
c.add(new JButton("S 5"));
c.add(new JButton("S 6"));
c.add(new JButton("Ein sehr sehr sehr langer Schalter 7"));
c.add(new JButton("Schalter 8"));
}
}
19.3.3 Grid-Layout
Das Grid-Layout fügt Komponenten in ein Gitter von Zellen, bestehend aus Zeilen und
Spalten, ein. Es vergrössert die Komponenten auf den in der Zelle verfügbaren Platz.
Jede Zelle hat dieselbe Grösse. Das Gitter ist einheitlich. Wenn Sie die Grösse eines
Grid-Layout-Containers verändern, vergrössert Grid-Layout die Zellen im Rahmen des
für den Container verfügbaren Platzes auf das grösstmögliche Mass.
Abb. 19-5 : Fenster mit Grid-Layout
import java.awt.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit GridLayout");
setSize(320, 170);
Container c = getContentPane();
c.setLayout(new GridLayout(2, 3));
c.add(new JButton("1"));
c.add(new JButton("2"));
c.add(new JButton("3"));
c.add(new JButton("4"));
c.add(new JButton("5"));
c.add(new JButton("6"));
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 353 / 409
19.4 Verschachtelte Layouts
Um Komponenten „tiefer zu layouten“, können Panels als Komponenten in ein Layout
eingeführt werden. Panels können ihrerseits wieder ein Layout haben und Komponenten
aufnehmen. Panels sind Objekte vom Typ „javax.swing.JPanel“ und auch ganz normale GUI
Elemente – siehe auch Kapitel 20.8.
Das folgende Beispiel zeigt ein Fenster mit Border-Layout, bei dem im Center-Bereich ein GridLayout integriert ist.
Abb. 19-6 : Fenster mit verschachtelten Layouts
import java.awt.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit verschachtelten Layouts");
setSize(400, 300);
JPanel panel = new JPanel(new GridLayout(3, 2));
panel.add(new JButton("1"));
panel.add(new JButton("2"));
panel.add(new JButton("3"));
panel.add(new JButton("4"));
panel.add(new JButton("5"));
panel.add(new JButton("6"));
Container c = getContentPane();
c.setLayout(new BorderLayout());
c.add(new JButton("North"), BorderLayout.NORTH);
c.add(new JButton("West"), BorderLayout.WEST);
c.add(new JButton("East"), BorderLayout.EAST);
c.add(new JButton("South"), BorderLayout.SOUTH);
c.add(panel, BorderLayout.CENTER);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 354 / 409
}
}
Hinweis – in Kapitel 16.6 finden Sie dieses Programm auch mit Windows-Style.
19.5 Aufgaben
19.5.1 Aufgabe „Layouts 1“
Entwickeln sie ein Fenster, dass oben zwei Reihen mit je 6 Buttons enthält, und den Rest des
Bildschirms „frei“ läßt.
Lösung siehe Kapitel 19.6.
19.5.2 Aufgabe „Layouts 2“
Entwickeln sie ein Fenster, dass an den Seiten je 5 Buttons unter einander enthält, und im
restlichen Mittelbereich einen einzelnen Button.
Lösung siehe Kapitel 19.7.
19.6 Lsg. zu Aufgabe „Layouts 1“ – Kap. 19.5.1
todo...
19.7 Lsg. zu Aufgabe „Layouts 2“ – Kap. 19.5.2
todo...
20 Swing GUI-Elemente
In diesem Kapitel werden kurz einige Swing GUI-Elemente vorgestellt. In Swing gibt es
natürlich noch viel mehr fertige GUI-Elemente – hier können nur ein paar kleine wichtige GUIElemente vorgestellt werden. Und vor allem können all diese GUI-Elemente viel mehr, als hier
beschrieben wird. Die Kapitel stellen nur kurze Einführungen in die wichtigsten GrundFunktionalitäten der GUI-Elemente dar.
20.1 Labels
Die Klasse „javax.swing.JLabel“ ist eine Klasse für einfache Labels (Beschriftungen).
Wichtige Element-Funktionen:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Konstruktor JLabel ()
Konstruktor JLabel (String text)
void setText(String text)
String getText()
void setEnabled(boolean b)
Seite 355 / 409
Erzeugt ein einfaches Label.
Erzeugt ein Label mit Text.
Setzt den Text des Labels neu.
Gibt den Text des Labels zurück.
Aktiviert bzw. deaktiviert das Label.
Das Beispiel ist ein einfaches Fenster mit Label.
Abb. 20-1 : Fenster mit Label
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Label");
getContentPane().add(new JLabel("Ich bin ein Label"));
}
}
20.2 Text-Felder
Die Klasse „javax.swing.JTextField“ ist eine Klasse für Text-Eingabe-Felder – auch Edit-Felder
genannt.
Wichtige Element-Funktionen:
Konstruktor JTextField()
Konstruktor JTextField (String text)
void setText(String text)
String getText()
void setEnabled(boolean b)
Erzeugt ein leeres Text-Feld.
Erzeugt ein Text-Feld mit Text.
Setzt den Text neu.
Gibt den Text zurück.
Aktiviert bzw. deaktiviert das Text-Feld.
Wichtige Events:
• Caret
• Key
Name:
Beschreibung:
Listener-Interface:
Eine Funktion:
Adapter-Klasse:
Event-Klasse:
Caret
Wird aktiviert, wenn das Caret bewegt wird.
javax.swing.event.CaretListener
void caretUpdate(CaretEvent e)
--javax.swing.event.CaretEvent
Name:
Key
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Beschreibung:
Listener-Interface:
Drei Funktionen:
Adapter-Klasse:
Event-Klasse:
Seite 356 / 409
Wird aktiviert, wenn die Tastatur benutzt wird.
java.awt.event.KeyListener
void keyPressed(KeyEvent e)
void keyReleased(KeyEvent e)
void keyTyped(KeyEvent e)
java.awt.event.KeyAdapter
java.awt.event.KeyEvent
Das Beispiel zeigt automatisch bei jeder Änderung des eingegebenen Textes im unteren Label
die Anzahl von Zeichen des Textes an.
Abb. 20-2 : Fenster mit Text-Feld und automatischer Längen-Angabe
import
import
import
import
java.awt.*;
java.awt.event.*;
javax.swing.*;
javax.swing.event.*;
public class MyFrame extends JFrame {
private JTextField field = new JTextField();
private JLabel output = new JLabel();
public MyFrame() {
super("Fenster mit Text-Feld");
onTextChanged();
field.addCaretListener(new CaretListener() {
public void caretUpdate(CaretEvent e) {
onTextChanged();
}
});
field.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) {
onTextChanged();
}
});
Container c = getContentPane();
c.setLayout(new GridLayout(3, 1));
c.add(new JLabel("Bitte geben sie Text ein:"));
c.add(field);
c.add(output);
}
private void onTextChanged() {
String text = field.getText();
output.setText("Der Text ist " + text.length() + " Zeichen lang.");
}
}
Hinweis – es werden sowohl „Update-Caret“ als auch „Key-Typed“ Events überwacht, da je
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 357 / 409
nach Benutzung von „Entf“, „Backspace“, dem Clipboard, usw. unterschiedliche Events
getriggert werden, und daher die Überwachung eines Events nicht ausreicht.
20.3 Buttons
Die Klasse „javax.swing.JButton“ ist eine Klasse für normale Buttons, auch „Push-Buttons“
genannt.
Wichtige Element-Funktionen:
Konstruktor JButton()
Konstruktor JButton(String text)
void setText(String text)
String getText()
void setEnabled(boolean b)
Erzeugt einen einfachen Button mit leerer Schaltfläche.
Erzeugt einen Button mit Text auf der Schaltfläche.
Setzt den Text der Schaltfäche neu.
Gibt den Text der Schaltfäche zurück.
Aktiviert bzw. deaktiviert den Button.
Wichtige Events:
• Action
Name:
Beschreibung:
Listener-Interface:
Eine Funktion:
Adapter-Klasse:
Event-Klasse:
Action
Wird aktiviert, wenn der Button betätigt wird.
java.awt.ActionListener
void actionPerformed(ActionEvent e)
--java.awt.ActionEvent
Das Beispiel ist ein Fenster mit einem Button „Klick mich“. Wird der Button angeklickt, so
ändert sich der Text auf „Aua“, und bei jedem weiteren Klick wird ein weiteres „aua“
hinzugefügt.
Abb. 20-3 : Fenster mit Button im initialen Zustand und nach dreimaliger Betätigung
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
private JButton button;
public MyFrame() {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 358 / 409
super("Fenster mit Button");
button = new JButton("Klick mich");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
onButtonClick();
}
});
getContentPane().add(button);
}
private void onButtonClick() {
if (button.getText().equals("Klick mich")) {
button.setText("Aua");
}
else {
button.setText(button.getText() + ", aua");
}
}
}
20.4 Radio-Buttons
Die Klasse „javax.swing.JRadioButton“ ist eine Klasse für Radio-Buttons, d.h. Buttons die
gruppiert auftreten, und von denen nur einer pro Gruppe selektiert sein kann.
Default-mässig ist erstmal jeder Radio-Button seine eigene Gruppe, d.h. alle Radio-Buttons
schalten unabhängig voneinander an und aus. Sollen mehrere Radio-Buttons einander
automatisch deselektieren, so müssen sie einer gemeinsamen Button-Gruppe zugeordnet
werden – hierfür gibt es in Swing die Klasse „javax.swing.ButtonGroup“. Mit „add“ können
Radio-Button einer Gruppe hinzugefügt werden.
Wichtige Element-Funktionen:
Konstruktor JRadioButton()
Konstruktor JRadioButton(String text)
Ko. JRadioButton(String text, boolean b)
void setText(String text)
String getText()
void setEnabled(boolean b)
void setSelected(boolean b)
boolean isSelected()
Erzeugt einen unselektierten Radio-Button ohne Text.
Erzeugt einen unselektierten Radio-Button mit Text.
Erzeugt einen Radio-Button mit Text und entsprechender
Selektion.
Setzt den Text neu.
Gibt den Text zurück.
Aktiviert bzw. deaktiviert den Radio-Button.
Selektiert bzw. deselektiert den Radio-Button.
Gibt zurück, ob der Radio-Button selektiert ist.
Wichtige Events:
• Action – siehe Kapitel 20.3
Das Beispiel zeigt ein Fenster mit vier Radio-Buttons, die einer Gruppe zugeordnet sind.
Ausserdem ist am unteren Rand ein Label vorhanden, das den jeweils selektieren Radio-Button
explizit angibt – hierfür ist im Beispiel für jeden Radio-Button ein „ActionListener“ gesetzt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 359 / 409
Abb. 20-4 : Fenster mit einer Gruppe von Radio-Buttons
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
private JRadioButton[] buttons = new JRadioButton[] {
new JRadioButton("Eins", true),
new JRadioButton("Zwei"),
new JRadioButton("Drei"),
new JRadioButton("Vier"),
};
private ButtonGroup group = new ButtonGroup();
private JLabel output = new JLabel();
public MyFrame() {
super("Fenster mit Radio-Buttons");
onSelectionChanged(buttons[0]);
Container c = getContentPane();
c.setLayout(new GridLayout(buttons.length+1, 1));
for (int i=0; i<buttons.length; i++) {
buttons[i].addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
onSelectionChanged((JRadioButton)e.getSource());
}
});
group.add(buttons[i]);
c.add(buttons[i]);
}
c.add(output);
}
private void onSelectionChanged(JRadioButton button) {
output.setText("Radio-But. \"" + button.getText() + "\" ist selektiert.");
}
}
Achtung – diese Gruppierung via „ButtonGroup“ ist eine rein logische Gruppierung, und hat
überhaupt nichts mit der Anordnung der Radio-Buttons im Fenster zu tun.
20.5 Check-Boxen
Check-Boxes sind spezielle Buttons, die meistens zwei Stati haben – diese werden im GUI
durch Boxen mit Check-Häkchen dargestellt. In Swing werden sie durch die Klasse
„javax.swing.JCheckBox“ repräsentiert.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 360 / 409
Wichtige Element-Funktionen:
Konstruktor JCheckBox()
Konstruktor JCheckBox (String text)
Kon. JCheckBox (String text, boolean b)
void setText(String text)
String getText()
void setEnabled(boolean b)
void setSelected(boolean b)
boolean isSelected()
Erzeugt eine unselektierte Check-Box ohne Text.
Erzeugt eine unselektierte Check-Box mit Text.
Erzeugt eine Check-Box mit Text und entsprechender
Selektion.
Setzt den Text neu.
Gibt den Text zurück.
Aktiviert bzw. deaktiviert die Check-Box.
Selektiert bzw. deselektiert die Check-Box.
Gibt zurück, ob die Check-Box selektiert ist.
Wichtige Events:
• Action – siehe Kapitel 20.3
Das Beispiel zeigt ein Fenster mit drei Check-Boxen. Werden sie selektiert bzw. deselektiert, so
wird automatisch ihre Vordergrund-Farbe auf „grün“ bzw. „rot“ gesetzt.
Abb. 20-5 : Fenster mit drei Check-Boxen
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
private JCheckBox[] buttons = new JCheckBox[] {
new JCheckBox("Eins", true),
new JCheckBox("Zwei"),
new JCheckBox("Drei")
};
public MyFrame() {
super("Fenster mit Check-Boxen");
onSelectionChanged(buttons[0]);
Container c = getContentPane();
c.setLayout(new GridLayout(buttons.length, 1));
for (int i=0; i<buttons.length; i++) {
buttons[i].addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
onSelectionChanged((JCheckBox)e.getSource());
}
});
onSelectionChanged(buttons[i]);
c.add(buttons[i]);
}
}
private void onSelectionChanged(JCheckBox button) {
button.setForeground(button.isSelected() ? Color.GREEN : Color.RED);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 361 / 409
}
}
20.6 Scroll-Bars und Scroll-Panes
Möglicherweise ist dem ein oder anderen aufgefallen, dass unsere Fenster noch ein kleines
Problem haben – macht man sie zu klein, so verschwinden Elemente. Die Layouts haben
Grenzen, sobald ein Element so klein würde, dass es nicht mehr sinnvoll darstellbar wäre.
Möchte man in einem solchen Fall Scroll-Bars bekommen, so muss man sich nicht selber
darum kümmern, sondern muss eine sogenannte Scroll-Pane zwischen Container und den
enthaltenden Elementen legen. Im Extremfall ist dies sogar für ein einzelnes GUI-Element
nötig, z.B. Tabellen – siehe Kapitel 20.7.2.
Ein Scroll-Pane ist eine Fläche, die Scrollbars zur Verfügung stellt – in Swing dargestellt durch
die Klasse „javax.swing.JScrollPane“ – ein Beispiel findet sich in Kapitel 20.7.2. Sowohl für die
horizontale als auch für die vertikale Richtung können die Scrollbars auf verschiedene Arten
unabhängig voneinander immer, nie, automatisch auf- und ausgeblendet werden – siehe auch
das Interface „ScrollPaneConstants“.
Bemerkung – hier sieht man wieder eine ganz zentrale OO-Philosophie, die sich natürlich auch
in Swing wiederfindet: Baue keine „ich-kann-alles“ Klassen, sondern zerlege das Problem in
viele kleine Komponenten, die man überschreiben und/oder wiederverwenden kann.
20.7 Tabellen
Die Swing Tabellen-Klasse „javax.swing.JTable“ ist sehr leistungsfähig. Man kann quasi alles
beeinflussen – bis hin zum Erscheinungs-Bild und den Editier-Möglichkeiten einer einzelnen
Zelle. Für alle eigenständigen Aufgaben gibt es eigene Interfaces und Klassen, von denen man
sich ableiten kann, und damit die Default-Einstellungen überschreiben kann.
Leider sind die Tabellen dadurch nicht immer einfach in ihrer Programmierung – und ein
wirklich umfassender Einstieg wäre fast ein eigenes Buch. Von daher beschränkt sich dieses
Kapitel auf die wesentlichen Themen.
20.7.1 Eine einfache Tabelle
Um eine einfache Tabelle zu erstellen, braucht es nicht viel. Man erzeugt ein Objekt der Klasse
„javax.swing.JTable“ und übergibt dem Konstruktor die Anzahl an Zeilen und Spalten – im
Beispiel 4 Zeilen und 3 Spalten.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 362 / 409
Abb. 20-6 : Fenster mit einfacher 4x3 Tabelle
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Tabelle 1");
JTable table = new JTable(4, 3);
setContentPane(table);
}
}
20.7.2 Tabellen mit Scrollbars
Leider hat eine solche Tabelle ein Problem, das man erst bei einer größeren Anzahl an Zeilen
bzw. Spalten sieht – darum hier eine Tabelle mit 40 Zeilen und 30 Spalten. Wie man sieht, hat
die Tabelle keine Scrollbars.
Abb. 20-7 : Fenster mit 40x30 Tabelle ohne Scrollbar
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Tabelle 2");
JTable table = new JTable(40, 30);
setContentPane(table);
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 363 / 409
Wie wir in Kapitel 20.6 gelernt haben, müssen wir ein Scroll-Pane einsetzen. Wir machen dies
hier in einer ganz primitiven Variante, ohne die Scrollbars hier wirklich optimal zu unterstützen.
Abb. 20-8 : Fenster mit 40x30 Tabelle mit Scrollbar
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Tabelle 3");
JTable table = new JTable(40, 30);
JScrollPane scrollPane = new JScrollPane(table);
setContentPane(scrollPane);
}
}
20.7.3 Tabellen-Inhalt aus Arrays
Als wichtigstes fehlt aber noch der Inhalt der Tabellen – bislang sind sie leer und damit nicht
besonders hilfreich.
Um Inhalte in eine Tabelle anzuzeigen, muss ein Tabellen-Modell erstellt werden – siehe
Kapitel 20.7.4. Da dies für einfache Tabellen-Anzeigen sehr aufwändig ist, gibt es zwei „Hilfs“
Konstruktoren in „JTable“, die Arrays bzw. den Java-Container „java.util.Vector“ erwarten. Im
Hintergrund baut das „JTable“ aus dem Array oder demContainer ein Default-Tabellen-Modell
auf – siehe „javax.swing.table.DefaultTableModel“.
Im Beispiel bekommt „JTable“ 2 Arrays übergeben:
• Parameter 1 ist ein zwei-dimensionales Array, das den Inhalt der Tabelle enthält – typisiert
ist er auf „Object[ ][ ]“. Da sich jedes Objekt immer in einen String wandeln läßt, kann dieses
Array immer angezeigt werden.
• Parameter 2 ist ein ein-dimensionales Array, das die Spalten-Überschriften enthält. Auch es
ist auf „Object[ ]“ typisiert.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 364 / 409
Abb. 20-9 : Fenster mit Tabelle, dessen Inhalt aus einem Array stammt
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Tabelle 4");
String[][] values = {{"1","2"}, {"3","4"}, {"5","6"}, {"7","8"}};
String[] titles = { "Col 1", "Col 2"};
JTable table = new JTable(values, titles);
setContentPane(new JScrollPane(table));
}
}
20.7.4 Tabelle mit Tabellen-Modell
Für komplexere Inhalte, Interaktion, und weitreichende Einflußmöglichkeiten muss der Tabelle
ein Tabellen-Modell mitgegeben werden – d.h. das Objekt muss das Interface
„javax.swing.table.TableModel“ implementieren. In diesem Interface ist die minimale
Schnittstelle zwischen Tabellen-Anzeige und Tabellen-Daten beschrieben, so z.B. Funktionen
für die Anzahl an Zeilen und Spalten, für die Inhalte, und einiges mehr.
In der Praxis werden nicht alle diese Beeinflussungs-Möglichkeiten benötigt – d.h. gibt es die
abstrakte Klasse „javax.swing.table.AbstractTableModel“, die für einige Funktionen DefaultImplementierungen anbietet. So müssen nur noch minimal drei Funktionen überschrieben
werden.
Das folgende Beispiel nutzt genau diese Klasse, leitet sich von „AbstractTableModel“ ab, und
überschreibt den minimalen Satz an notwendigen Funktionen.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 365 / 409
Abb. 20-10 : Fenster mit Tabelle mit Tabellen-Modell
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Tabelle 5");
JTable table = new JTable(new MyTableModel());
setContentPane(new JScrollPane(table));
}
}
import javax.swing.table.*;
class MyTableModel extends AbstractTableModel {
public int getRowCount() {
return 15;
}
public int getColumnCount() {
return 3;
}
public Object getValueAt(int arg0, int arg1) {
return "" + arg0 + " / " + arg1;
}
}
20.8 Panels
Auch die in Kapitel 19.4 vorgestellten Panels „javax.swing.JPanel“ sind ganz normale GuiElemente. Häufig werden sie einfach als eine Art GUI-Container in Verbindung mit Layouts
genutzt.
Aber sie können auch als einfache GUI-Elemente benutzt werden, die z.B. eigene Zeichnungen
enthalten – natürlich via Ableiten und Überschreiben der „paint“ Funktion.
20.9 Menüs
Um an ein „JFrame“ Fenster ein Menü anzuhängen, müssen drei Klassen benutzt werden:
• „javax.swing.JMenuBar“ repräsentiert das komplette Menü, d.h. die Menü-Zeile im Fenster.
• „javax.swing.JMenu“ repräsentiert einzelne Menüs, die Items, Seperatoren und Sub-Menüs
enthalten können. Sub-Menüs sind wiederrum nur ganz normale Menüs – sie lassen sich
halt verschachten.
• „javax.swing.JMenuItem“ sind einzelne Menü-Einträge, die vom Benutzer angewählt werden
können.
Wichtige Element-Funktionen von „JMenuBar“
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Konstruktor JMenuBar()
void add(JMenu m)
Seite 366 / 409
Erzeugt eine leere Menü-Zeile.
Fügt das Menü ans Ende der Menü-Zeile an.
Wichtige Element-Funktionen von „JMenu“
Konstruktor JMenu(String text)
void add(JMenuItem item)
void add(String text)
void addSeparator()
void setText(String text)
String getText()
void setEnabled(boolean b)
Erzeugt ein leeres Menü mit dem übergebenen Text.
Fügt den Menü-Eintrag ans Ende des Menüs an. Hier
dürfen auch „JMenu“ Objekte übergeben werden, da diese
von „JMenuItem“ abgeleitet sind.
Fügt einen Menü-Eintrag mit dem übergebenen Text ans
Ende des Menüs an.
Kurzform für „add(new MenuItem(text))“.
Fügt einen Separator ans Ende des Menüs an.
Setzt den Text des Menüs neu.
Gibt den Text des Menüs zurück.
Aktiviert bzw. deaktiviert das Menü.
Wichtige Element-Funktionen von „JMenuItem“
Konstruktor JMenuItem(String text)
void setEnabled(boolean b)
Erzeugt einen Menü-Eintrag mit dem übergebenen Text.
Aktiviert bzw. deaktiviert den Menü-Eintrag
Wichtige Events von „JMenuItem:
• Action – siehe Kapitel 20.3
Um ein Menü ohne spezielle Features zu erstellen, muß man also:
• eine Menü-Zeile erstellen,
• mindestens ein Menü an die Menü-Zeile anhängen,
• Menü-Einträge, Separatoren und Unter-Menüs an das Menü anhängen, und
• die Menü-Einträge mit Action-Listener versehen.
Hinweis – um Menü-Einträge selektiert, d.h. mit einem Häkchen versehen, darzustellen muss
statt eines „JMenuItem“ ein Objekt der abgeiteten Klasse „JCheckBoxMenuItem“ genommen
werden. Mit „void setSelected(boolean)“ kann die Selektion gesetzt, und mit „boolean
isSelected()“ abgefragt werden.
Das Beispiel ist ein „JFrame“ Fenster mit einer Menü-Zeile, die zwei Menüs enthält. Im Bild ist
das erste Menü zu sehen – es besteht aus zwei Einträgen, einem Separator und einem SubMenü. Das Sub-Menü enthält drei Einträge, wobei der erste deaktiviert und der dritte selektiert
ist. Das zweite Menü „Menü-2“ enthält nur einen Eintrag, an dem aber ein Action-Listener
hängt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 367 / 409
Abb. 20-11 : Fenster mit Menü
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
super("Fenster mit Menü");
JMenuItem subItem1 = new JMenuItem("Sub-Item 1");
JMenuItem subItem2 = new JMenuItem("Sub-Item 2");
JMenuItem subItem3 = new JCheckBoxMenuItem("Sub-Item 3");
subItem1.setEnabled(false);
subItem3.setSelected(true);
JMenu submenu = new JMenu("Sub-Menü");
submenu.add(subItem1);
submenu.add(subItem2);
submenu.add(subItem3);
JMenu menu1 = new JMenu("Menü-1");
menu1.add("Item 1");
menu1.add("Item 2");
menu1.addSeparator();
menu1.add(submenu);
JMenu menu2 = new JMenu("Menü-2");
JMenuItem item3 = new JMenuItem("Item 3");
item3.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
setTitle("Item 3 wurde betätigt");
}
});
menu2.add(item3);
JMenuBar menubar = new JMenuBar();
menubar.add(menu1);
menubar.add(menu2);
setJMenuBar(menubar);
}
}
20.10 Timer
Für periodisch wiederkehrende Aufgaben gibt es in Swing eine Timer-Klasse
„javax.swing.Timer“. Gestartet ruft sie in regelmäßigen Zeit-Intervallen – diese können in MilliSekunden definiert werden – die gesetzten Action-Listener auf.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 368 / 409
Hinweis –bei positiven Intervall-Werten muss der Timer explizit gestartet werden – siehe
Tabelle und Beispiel.
Wichtige Element-Funktionen:
Konstr. Timer(int delay, ActionListener l)
void start()
void stop()
Erzeugt einen Timer mit dem Zeit-Intervall „delay“ in MilliSekunden und dem übergebenen Action-Listener.
Startet den Timer.
Stoppt den Timer.
Wichtige Events:
• Action – siehe Kapitel 20.3
Das Beispiel erzeugt ein Fenster, dass jede halbe Sekunde die Farbe von „grün“ nach „rot“ und
wieder zurück wechselt. Außerdem wird die aktuelle Farbe in der Titelzeile des Fensters
angezeigt.
Abb. 20-12 : Fenster in grün und in rot – gesteuert von einem Timer
import java.awt.Color;
import java.awt.event.*;
import javax.swing.*;
public class MyFrame extends JFrame {
public MyFrame() {
onTimer();
Timer timer = new Timer(500, new ActionListener() {
public void actionPerformed(ActionEvent e) {
onTimer();
}
});
timer.start();
}
private void onTimer() {
if (getContentPane().getBackground() == Color.GREEN) {
getContentPane().setBackground(Color.RED);
setTitle("Fenster in \"rot\" mit Timer");
}
else {
getContentPane().setBackground(Color.GREEN);
setTitle("Fenster in \"grün\" mit Timer");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 369 / 409
}
}
}
20.11 Aufgaben
20.11.1 Aufgabe „Liste“
Programmieren sie ein Fenster mit einer Liste – Swing Klasse „JList“ – mit Scrollbars und
Listen-Model. Die Benutzung der Listen-Klasse ist quasi analog zur Tabellen-Klasse, nur sind
Liste und Listen-Modell einfacher, da eine Liste nur eine Spalte hat.
Lösung siehe Kapitel 20.12.
20.11.2 Aufgabe „Scribble 6“
Implementieren sie zuerst ein Scribble, das sowohl Linienzüge als auch Rechtecke zeichnen
kann. Setzen sie für die Auswahl Buttons oder ein Menü ein. Implementieren sie das Scribble in
mehreren Schritten82:
• Erste Version ohne Modell, mit dem Haupt-Augenmerk auf die Auswahl und die Integration
von „Scribble 3“ – siehe Kapitel 18.3.5 – und „Scribble 5“ – siehe Kapitel 18.6.2.
• In der zweiten Version müssen sie ein Modell entwickeln, das sowohl Linien-Züge als auch
Rechtecke aufnehmen kann. Nehmen sie das Model vom „Sribble 4“ – siehe Kapitel 18.5 –
erweitern es um Rechtecke, und integrieren es. Denken sie schon nach vorne – auf Dauer
werden noch andere Grund-Elemente (Ellipsen, Texte,...) gezeichnet werden müssen. Am
besten entwickeln sie also ein Modell, das offen ist für neue Grund-Elemente und mit
verschiedenen Arten von Grund-Elementen umgehen kann.
• Wenn sie eine gute offene Lösung für das Modell gefunden haben, ist die Integration einer
weiteren Zeichen-Figur kein Problem. Also lassen sie ihr Scribble noch Ellipsen zeichnen
können – natürlich zusätzlich zu den Polygonen und Rechtecken, und natürlich auch mit
Model. An dem Aufwand, den sie treiben müssen, um Ellipsen zu integrieren können sie
erkennen, wie gut ihr Programm-Design ist.
Lösung siehe Kapitel 20.13.
20.11.3 Aufgabe „TextField“
Entwickeln sie ein eigenes Text-Feld Element, dass gegenüber dem Original-Element
Ein guter Entwickler zeichnet sich nicht dadurch aus, dass er direkt am Computer die größten
und tollsten Programme entwickelt, sondern dadurch, dass er gute Angewohnheiten hat, die ihn
gut machen. Dazu gehört eben auch ein Problem in mehrere Stufen zu zerlegen und diese
nach und nach abzuarbeiten. Aber auch so Dinge wie: Coding-Styles, Unit-Tests, Assertions,
Refactoring, Weiterbildung, Reflektion, und einiges mehr.
82
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 370 / 409
„JTextField“ noch zusätzlich ein externes Event für die Änderung des Inhalts hat, d.h. einen
Change-Listener.
Lösung siehe Kapitel 20.14.
20.11.4 Aufgabe „Kontaktdaten 5“
Implementieren sie ein GUI für die „Kontaktdaten-Verwaltung 4“ aus Kapitel todo bzw. Kapitel.
Lösung siehe Kapitel 20.15.
20.12 Lsg. zu Aufgabe „Liste“ – Kap. 20.11.1
todo...
20.13 Lsg. zu Aufgabe „Scribble 6“ – Kap. 20.11.2
todo...
20.14 Lsg. zu Aufgabe „TextField“ – Kap. 20.11.3
todo...
20.15 Lsg. zu Aufgabe „Kontaktdaten 5“ – Kap. 20.11.4
todo...
21 Applets
Applets sind kleine Programme, die in eine HTML Seite eingebettet sind und von einem
Browser ausgeführt werden.
Achtung – gleich vorweg ein paar Hinweise zur praktischen Seite der Entwicklung von Applets:
• Der verwendete Browser muss natürlich java-fähig sein.
• Es muss ein entsprechendes Java-Plugin (passendes JDK) für den Browser installiert sein.
• Es gibt immer wieder Ärger mit den Browser-Caches. D.h. die Browser merken nicht dass
sie eine neue Class-Datei erzeugt haben, und benutzen die aus dem Cache.
21.1 Beispiel
Sie benötigen die einbettende HTML Seite und die „class“ Datei des Applets im gleichen
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 371 / 409
Verzeichnis (wenn die Applet Klasse nicht in einem Package liegt).
<html>
<head>
<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<title>
Mein erstes Applet
</title>
</head>
<body>
<applet
codebase = "."
code = "MyApplet.class"
weidth = 400
hight = 300
>
</applet>
</body>
</html>
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MyApplet extends JApplet {
public MyApplet() {
final JButton button = new JButton("Klick mich bitte vorsichtig an");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
button.setText("Aua, das tat weh...");
}
});
setContentPane(button);
}
}
21.2 Applet HTML-Seite
Ein Applet kann nie für sich alleine funktionieren, sondern braucht immer eine HTML Seite, in
die es eingebettet ist. Dies soll und kann kein HTML Lehrgang sein, darum wird eine kleine
Beispiel Seite kurz besprochen - für mehr Details lesen sie bitte ein entsprechendes HTML
Buch.
<html>
<head>
<meta HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<title>
HTML-Testseite
</title>
</head>
<body>
Mein tolles Applet erscheint in einem Java-f&auml;higen Browser.<BR>
<applet
CODEBASE = "."
CODE
= "packageName.MeineAppletKlasse.class"
NAME
= "TestApplet"
WIDTH
= 400
HEIGHT
= 300
HSPACE
= 0
VSPACE
= 0
ALIGN
= middle
>
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 372 / 409
Text für den Fall, dass der Browser keine Applets anzeigen kannn
</applet>
</body>
</html>
Hinweis – aufgrund der Plattform-unabhängigkeit von HTML sollten alle „nicht-normalen“
Zeichen codiert sein, d.h. das „&auml;“ statt des einfachen „ä“. Auch diese Dinge sollten sie
genauer in einem HTML Buch nachlesen.
Für Java Applets wurde in HTML das Tag „applet“ aufgenommen, das mit <applet> begonnen
und mit </applet> beendet wird. Der zwischen den Tags stehende Text wird vom Browser
ausgegeben, wenn er nicht Applet-fähig ist.
Zum Applet-Start-Tag gehören Parameter, die das Applet näher spezifizieren:
Parameter
optional
Bedeutung
CODE
Gibt den Namen des Applets an:
• Gross-/Kleinschreibung muss beachtet werden.
• die Extension .class sollte angegeben werden.
• die Klassendatei muss im aktuellen Verzeichnis liegen
WIDTH
Die für das Applet zur Verfügung stehende Breite
HEIGHT
Die für das Applet zur Verfügung stehende Höhe
CODEBASE
x
Alternative Verzeichnisse (durch Kommata getrennt) für die
Klassendateien
ARCHIVE
x
Angabe eines jar-Archivs für die Klassendateien und die
Ressourcen
OBJECT
x
Datei mit dem serialisierten Inhalt des Applets
ALT
x
Alternativer Text für nicht Appletfähige Browser
NAME
x
Eindeutiger Name für das Applet. Wichtig bei der Verwendung
mehrerer miteinander kommunizierender Applets auf einer Seite
ALIGN
x
Vertikale Anordnung des Applets - einer der folgenden Werte
(left, right, top, texttop, middle, absmiddle, baseline, bottom,
absbottom)
VSPACE
x
Rand über und unter dem Applet
HSPACE
x
Rand rechts und links vom Applet
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 373 / 409
21.3 Grundlagen
21.3.1 Klassen-Hierarchie „Applet“ bzw. „JApplet“
Ein Applet muss zwingend von „java.applet.Applet“ abgeleitet sein. Wird Swing benutzt
Ableitungs-Hierarchie:
Object - Component - Container - Panel - Applet - JApplet
21.3.2 Default-Konstruktor
Ein Browser erzeugt ein Applet immer mit dem Default-Konstruktor. In diesem sollten sie bei
Applets aber ausnahmsweise keine Initialisierung vornehmen. Für die Initialisierung steht in
Applets die Funktion „init“ zur Verfügung – siehe Kapitel 21.3.3.1.
21.3.3 Wichtige Applet-Funktionen
Es gibt verschiedene Applet-Funktionen, die der Kommunikation zwischen Browser und Applet
dienen und in einer eigenen Applet-Klasse überschrieben werden können.
21.3.3.1 Funktion „public void init()“
Nach der Erstellung des Applets wird für das Applet die Funktion „init“ vom Browser aufgerufen,
in der Sie das Applet initailisieren sollten. Die Funktion wird genau einmal für das Applet nach
der Erzeugung aufgerufen.
21.3.3.2 Funktion „public void start()“
Um die Ausführung des Applets zu starten ruft der Browser die Funktion „start“ auf.
Achtung - diese Funktion kann mehrfach aufgerufen werden. Z. B. bei einem Seitenwechsel
muss der Browser das Applet nicht zerstören, sondern kann es cachen. Wird die Seite wieder
aufgerufen, muss der Browser das Applet daher nicht neu erzeugen und ruft dann natürlich
auch nicht mehr die Funktion „init“ auf, sondern direkt wieder „start“.
21.3.3.3 Funktion „public void stop()“
Die Funktion „stop“ ruft der Browser auf, wenn das Applet gestoppt werden soll.
Achtung - wie schon start, so kann auch stop mehrfach aufgerufen werden, da der Browser
nur die Ausführung des Applets unterbricht, aber das Applet nicht zerstört, z. B. beim Verlassen
der HTML Seite mit cachen des Applets. Geben sie also niemals Ressourcen, die beim Starten
wieder benötigt werden, in dieser Funktion frei.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 374 / 409
21.3.3.4 funktion „public void destroy()“
Erst die Funktion „destroy“ zeigt an, dass das Applet zerstört wird. Auch diese Funktion wird nur
einmal (wie init) am Ende der Lebensdauer des Applets vom Browser aufgerufen.
Achtung - für die Freigabe von Ressourcen benutzen sie immer destroy und nicht den
Destruktor finalize, da dieser nicht zwingend aufgerufen wird.
21.3.3.5 Funktion „public void showStatus(String msg)“
Mit der funktion „showStatus“ können sie Ausgaben in der Statuszeile des Browsers
vornehmen.
showStatus("Hallo Welt in der Statuszeile");
21.3.3.6 Funktion „public String getAppletInfo()“
Mit dieser funktion können sie Informationen über Ihr Applet zur Verfügung stellen, die der
Browser abrufen und anzeigen kann.
public String getAppletInfo() {
return "Mein Super-Applet Version 0.001";
}
21.4 Applet-Parameter
Neben den Parametern für das Applet-Tag selber können dem Applet auch noch Appletspezifische Parameter übergeben werden. Diese werden mit dem optionalen HTML-Tag
PARAM im Applet-Bereich angegeben - PARAM wiederum hat die Parameter name und value.
<APPLET
CODEBASE
CODE
NAME
WIDTH
HEIGHT
HSPACE
VSPACE
ALIGN
>
<PARAM
<PARAM
</APPLET>
=
=
=
=
=
=
=
=
"."
"packageName.meineAppletKlasse.class"
"TestApplet"
400
300
0
0
middle
NAME = "speed" VALUE = "10">
NAME = "rate" VALUE = "2">
Damit das Applet die Parameter aus der HTML Seite erfragen kann, gibt es die Funktion public
String getParameter(String name), die den Namen des Parameters enthält und den Wert des
Parameters als String zurückgibt.
Für nähere Informationen über die Parameter kann das Applet die Funktion public String[ ][ ]
getParameterInfo() überschreiben, die ein zweidimensionales String-Array zurückgibt - eine
Zeile pro Parameter, den das Applet erwartet. Eine Zeile besteht aus drei Einträgen, die den
Namen des Parameters, den Typ und eine Beschreibung enthalten. Die Einträge sollten für
Menschen lesbar und verständlich sein, da der Browser diese Informationen nicht interpretiert,
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 375 / 409
sondern sie nur dem Benutzer zugänglich macht.
public String[][] getParameterInfo() {
String pinfo[][] =
{
{"speed", "int", "Schnelligkeit"},
{"rate", "int", "Rate"},
};
}
21.5 Applets & Applikationen
Von einer „stand-alone“ Applikation unterscheidet sich ein Applet durch mehrere Dinge:
Applet
„stand-alone“ Applikation
Es muss eine spezielle Applet-Klasse geben, Es wird keine spezielle Applikations-Klasse
die direkt oder indirekt83 von der Klasse
zwingend vorausgesetzt. Ein „main“ kann in
„java.applet.Applet“ abgeleitet sein muss.
jeder Klasse stehen. Wenn eine spezielle
Applikations-Klasse vorhanden ist, so kann
sie von einer beliebigen Klasse abgeleitet
sein.
Start, indem der Browser die Haupt-Klasse
Start in der eindeutigen static Funktion main.
des Applets instanziiert (Default-Konstruktor)
und die Funktion init und start aufruft.
Darf nicht auf Dateien auf dem lokalen
Rechner zugreifen und darf keine Prozesse
auf diesem starten84.
Keine Sicherheitsbeschränkungen bzgl. des
lokalen Systems
Arbeitet immer grafik- und
ereignissorientiert85.
Kann auch Texteingabe und -ausgabe auf
der Kommandozeile vornehmen.
Hinweis – es ist relativ leicht Java-Sourcen zu schreiben, die sowohl als Applet als auch als
„stand-alone“ Anwendung laufen.
83
Wenn Sie ein Applet basierend auf den Swing Klassen erstellen, so benutzen Sie als
Basisklasse „javax.swing.JApplet“, die wiederum von „java.applet.Applet“ abgeleitet ist.
84 Genau genommen gibt es ein sehr detailiertes Sicherheits-Konzept, über das der Benutzer
einem Applet beliebige Rechte geben kann. Defaultmäßig hat ein Applet aber keine Rechte auf
dem Rechner des Benutzers.
85
In der Entwicklungsumgebung kann die Kommandozeile problemlos zum Debuggen benutzt
werden. Aber in manchen Browsern existiert keine Kommandozeile, so dass Textein- und ausgabe keinen Sinn machen - viele Browser stellen ein Ausgabefenster für Textausgaben zur
Verfügung, dass der Benutzer aber extra öffnen muss.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 376 / 409
21.6 Aufgaben
21.6.1 Aufgabe „Statuszeilen-Applet“
Implementieren sie ein Applet, bestehend aus einem Text-Eingabe-Feld und einem Button. Bei
Betätigung des Buttons soll der Inhalt des Text-Feldes in der Statuszeile des Browsers
ausgegeben werden.
Lösung siehe Kapitel 21.7.
21.6.2 Aufgabe „Scribble 7“
Modifizieren sie das „Scribble 6“ – siehe Kapitel 20.11.2 – so, dass es auch als Applet läuft.
Lösung siehe Kapitel 21.8.
21.7 Lsg. zu Aufgabe „Statuszeilen-Applet“ – Kap. 21.6.1
todo...
21.8 Lsg. zu Aufgabe „Scribble 7“ – Kap. 21.6.2
todo...
22 Exceptions
Exceptions sind ein Sprachkonzept zur Behandlung von unnormalen Programm-Status, z.B.
von Fehlern.
22.1 Motivation
Die beiden grössten Probleme der konventionellen Fehlerbehandlung (Rückgabe von ErrorCode’s, Fehlerparameter, globale Variablen, Fehlerfunktionen,...) sind, dass sich im Code
normaler Code und Fehlerbehandlungscode logisch vermischen, und die Abfrage auf Fehler
von der Sorgfalt des Programmierers abhängig ist.
Das Ergebnis sind Quelltexte folgender Art – Achtung, Pseudocode:
back = fkt1();
if (back==ERROR) {
// behandle Fehler
}
back = fkt2();
if (back==ERROR) {
// behandle Fehler
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
fkt3();
int i = fkt4();
if (i<0) {
back = fkt5().fkt6();
if (back==ERROR) {
// behandle Fehler
}
else {
back = fkt7();
if (back==ERROR) {
// behandle Fehler
}
}
}
Seite 377 / 409
// Fehlerbehandlung vergessen
// Behandlung vergessen oder keine Fehler moeglich?
// Behandlung vergessen oder keine Fehler moeglich?
Schöner wäre aber:
// Normaler Code
fkt1();
fkt2();
fkt3();
int i = fkt4();
if (i<0) {
fkt5().fkt6();
}
else {
fkt7();
}
// Fehlerbehandlung
if (ERROR) {
// behandle Fehler
}
Aus diesen Gründen heraus entstand das Konzept der Exceptions.
22.2 Realisation
Tritt ein Fehler auf, so wird ein Exception-Objekt geworfen – dies darf nur „logisch innerhalb86“
eines try-Blocks passieren. Wird ein Exception-Objekt geworfen, wird instantan in die
entsprechende Fehler-Behandlung verzweigt.
public static void main(String[] args) {
for (int i=0; i<2; i++) {
try {
System.out.println("try-Block Anfang mit i=" + i);
if (i==1) {
throw new RuntimeException();
}
System.out.println("try-Block Ende");
}
catch (RuntimeException x) {
System.out.println("Fehler-Objekt wurde gefangen");
}
}
}
(*)
(**)
Ausgabe
try-Block Anfang mit i=0
86
„logisch innerhalb“ meint hier aus Sicht des Programm-Verlaufs – siehe auch Kapitel 22.3.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 378 / 409
try-Block Ende
try-Block Anfang mit i=1
Fehler-Objekt wurde gefangen
An der Ausgabe sieht man sehr schön, dass bei „i==0“ die Schleife normal durchlaufen un der
catch-Block ignoriert wird. Bei „i==1“ dagegen wird direkt nach dem „throw“ die Abarbeitung des
try-Blocks unterbrochen und in den catch-Block verzweigt.
Man kann also sagen:
• Der try-Block ist der Bereich, der den normalen Code ohne Fehlerbehandlung enthält.
• Tritt im normalen Programm-Verlauf ein Fehler auf, so meldet die entsprechende Stelle
diesen indem sie mit „throw“ ein Fehler-Objekt wirft – im Beispiel vom Typ
„RuntimeException“ in Zeile (*).
• Im Falle eines Fehlers, d.h. eines „throw’s“, wird der normale Programm-Verlauf automatisch
unterbrochen und sofort zur entsprechenden Fehler-Behandlung verzweigt – im Beispiel der
catch-Block (**).
• Ein catch-Block stellt den Fehler-Behandlungs-Code zur Verfügung.
• Nach Verlassen des catch-Blocks gilt der Fehler als behandelt.
Die Idee hierbei ist, dass es nach Auftreten eines Fehler einfach keinen Sinn mehr macht den
normalen Programm-Verlauf zu verfolgen. Würde der Fehler ignoriert und der normale
Programm-Fluß weiter abgearbeitet werden, so kann dies nicht gut sein sondern statt dessen
gefährliche Folgen haben. Denken sie z.B. ganz extrem, dass die Kühlung eines Kraftwerks
nicht aktiviert werden konnte. Wenn dieser Fehler ignoriert werden würde, und die Generatoren
danach einfach weiter hoch gefahren werden würde, so wäre das sicher nicht das Beste.
Hinweis – zu einem try-Block gehört immer mindestens ein catch-Block oder ein ein finallyBlock – siehe auch Kapitel 22.4.
Hinweis – in der Praxis arbeitet man eher selten mit der Klasse „RuntimeException“ als
Exception-Klasse – siehe Zeile (*) und (**). Im Hinblick auf die nächsten Grundlagen ist die
Klasse „RuntimeException“ aber erstmal einfacher, da sie uns Exception-Spezifikationen (siehe
Kapitel todo) erspart. Welche Klassen für das Exception-Handling zur Verfügung stehen, und
welche man wann benutzt, wird in Kapitel todo erklärt.
22.3 Fehler über mehrere Funktions-Ebenen
Das Exception-Handling ist nicht auf eine Funktion festgelegt, sondern kann über eine beliebige
Menge von Funktions-Aufrufen hinweg erfolgen.
Das folgende Beispiel ist im Prinzip das Gleiche wie das Vorherige. Nur steht das „if“ mit dem
„throw“ nicht direkt im try-Block, sondern wird über 2 Funktions-Aufrufe („f1“ und „f“)
angeprochen. Die Funktionen enthalten zusätzlich noch Ausgaben, um den Programm-Verlauf
gut verfolgen zu können.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 379 / 409
private static void f2(int i) {
System.out.println("
-> f2");
if (i==1) {
throw new RuntimeException();
}
System.out.println("
<- f2");
}
private static void f1(int i) {
System.out.println("
-> f1");
f2(i);
System.out.println("
<- f1");
}
public static void main(String[] args) {
for (int i=0; i<2; i++) {
try {
System.out.println("try-Block Anfang mit i=" + i);
f1(i);
System.out.println("try-Block Ende");
}
catch (RuntimeException x) {
System.out.println("Fehler-Objekt wurde gefangen");
}
}
}
Ausgabe
try-Block Anfang mit i=0
-> f1
-> f2
<- f2
<- f1
try-Block Ende
try-Block Anfang mit i=1
-> f1
-> f2
Fehler-Objekt wurde gefangen
Wie man an den Ausgaben deutlich sieht, arbeitet das Exception-Handling auch über mehrere
Funktions-Ebenen hinweg.
• Das throw steht weiterhin logisch in einem try-Block. Innerhalb der Funktion „f2“ ist zwar kein
try-Block vorhanden, aber im logischen Sinne steht „f2“ in „f1“ und „f1“ im try-Block von
„main“.
• Nach werfen des Exception-Objekts wird auch hier automatisch und sofort in den FehlerBehandlungs Block verzweigt. Man sagt, dass eine Exception automatisch den FunktionsStack abbaut. Es wird kein Code mehr im normalen Programm-Verlauf ausgeführt – auf
keiner Ebene der Funktionen, bis der Fehler als bearbeitet gilt.
22.4 Aufräumarbeiten und „finally“
Müssen nach einem Code-Abschnitt auf jeden Fall irgendwelche Aufräumarbeiten ausgeführt
werden – z.B. das Schließen von Dateien oder Netzwerkverbindungen – so gibt es dafür einen
finally-Block.
Ein finally-Block gehört immer zu einem try-Block. Egal wie der try-Block verlassen wird
(normaler Programmfluß, return im try-Block, Exception), der Code im finally Block wird immer
ausgeführt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 380 / 409
Das folgende Beispiel ist bis auf zwei Änderungen das Gleiche wie das Vorherige:
• In der Funktion „f1“ ist der Aufruf von „f2“ in einen try-Block gelegt, und der zugehörige
finally-Block enthält die Ausgabe zum Verlassen der Funktion.
• Der try-Block in „main“ hat einen finally-Block mit einer zusätzlichen Ausgabe bekommen.
private static void f2(int i) {
System.out.println("
-> f2");
if (i==1) {
throw new RuntimeException();
}
System.out.println("
<- f2");
}
private static void f1(int i) {
System.out.println("
-> f1");
try {
f2(i);
}
finally {
System.out.println("
<- f1");
}
}
public static void main(String[] args) {
for (int i=0; i<2; i++) {
try {
System.out.println("try-Block Anfang mit i=" + i);
f1(i);
System.out.println("try-Block Ende");
}
catch (RuntimeException x) {
System.out.println("Fehler-Objekt wurde gefangen");
}
finally {
System.out.println("finally-Block in main");
}
}
}
Ausgabe
try-Block Anfang mit i=0
-> f1
-> f2
<- f2
<- f1
try-Block Ende
finally-Block in main
try-Block Anfang mit i=1
-> f1
-> f2
<- f1
Fehler-Objekt wurde gefangen
finally-Block in main
An den Ausgaben kann man gut verfolgen, dass der finally-Block immer ausgeführt wird, wenn
der zugehörige try-Block verlassen wird.
Hinweise zu Syntax:
• Zu einem try-Block gehört mindestens ein catch-Block oder ein finally-Block.
• D.h. ist auch ein try-Block nur mit finally-Block erlaubt – siehe z.B. Funktion „f1“ im Beispiel.
• Gehören zu einem try-Block catch-Blöcke und ein finally-Block, so folgt der finally-Block dem
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 381 / 409
letzten catch-Block, d.h. er steht am Ende des kompletten try-catch-finally-Konstrukts.
Siehe auch „main“ im Beispiel.
• Es darf nur einen finally-Block pro try-Block geben.
22.5 Exception-Objekte und Exception-Hierarchie
22.5.1 Exception-Objekte
Es können nur Objekte der Klasse „java.lang.Throwable„ oder einer von ihr abgeleiteten
Klasse Exception-Objekte sein, d.h. in einer throw-Anweisung geworfen werden.
throw new String("Error");
throw new Exception();
// Fehler, String ist kein Throwable
// Okay, Exception ist ein Throwable
Der Sinn einer konkreten Exception Klasse ist, dass sie einen ganz fest umrissenden Fehler
spezifiziert. So gibt es z.B. in Java die „java.lang.NullPointerException“, die geworfen wird,
wenn auf eine Null-Referenz zugegriffen wird. Oder z.B. beim Zugriff auf nicht existente ArrayElemente wird eine „java.lang.ArrayIndexOutOfBoundsException“ geworfen.
String s = null;
s.toString();
// erzeugt eine NullPointerException
int[] a = { 0, 1, 2 };
a[5] = 5;
// erzeugt eine ArrayIndexOutOfBoundsException
22.5.2 Exception-Hierarchie
In der Klassen-Bibliothek von Java existieren viele Exception-Klassen, d.h. Klassen die direkt
oder indirekt von „Throwable“ abgeleitet sind. Von besonderer Bedeutung sind dabei die
folgenden:
java.lang.Throwable
java.lang.Error
java.lang.Exception
java.lang.
RuntimeException
Abb. 22-1 : Ein kleiner aber wichtiger Teil der Exception-Hierarchie in Java
Die Klasse „java.lang.Error“ und ihre Unterklassen sind für interne Probleme der virtuellen
Maschine (z. B. beim Linken, Speicherüberlauf,...) reserviert. Sie müssen diese Exceptions
nicht behandeln und sollten es auch nicht, da sie im Normalfall für den Programmierer nicht
behandelbar sind. Sie sollten auch keine Unterklassen von „java.lang.Error“ bilden, da dies im
Normallfall keinen Sinn macht.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 382 / 409
Die Klasse „java.lang.Exception“ ist die direkte oder indirekte Baisklasse für die normalen
Exceptions. Auch sie sollten diese Klasse im Normallfall als direkte oder indirekte Baisklasse
für ihre eigenen Exception-Klassen nutzen.
Die Klasse „java.lang.RuntimeException“ ist besonders, da sie und ihre abgeleiteten Klassen
ungeprüfte Exceptions ermöglichen – siehe Kapitel 22.9.
22.6 Fangen von Basis-Exception-Klassen
Nicht immer will man aber ganz genau wissen, was für ein Fehler aufgetreten ist. Dann ist es
möglich ganze Gruppen von Exceptions über ihre gemeinsame Basis-Klasse abzuhandeln.
Eine Exception-Basis-Klasse faßt daher eine Menge von thematisch ähnlichen oder zusammen
gehörigen Fehlern zusammen.
Bei einer Datei-Schittstelle könnte es z.B. Exceptions der folgenden Art geben:
• FileDoesNotExistException
• FileNotReadableException
• FileNotWritableException
• ...
Hier würde es sich anbieten, die Exception-Klassen von einer gemeinsamen Basis-Klasse
„FileException“ abzuleiten.
Abb. 22-2 : Klassen-Hierarchie für unsere Beispiel File-Exceptions
Will man nun gar nicht genau wissen, welche File-Exception geworfen wurde, sondern nur dass
überhaupt ein Datei-Fehler aufgetreten ist, so kann man im catch-Block einfach die BasisKlasse abfangen.
public class FileException extends RuntimeException {
}
public class FileDoesNotExistException extends FileException {
}
public class FileNotReadableException extends FileException {
}
public class FileNotWritableException extends FileException {
}
public static void main(String[] args) {
for (int i=0; i<4; i++) {
try {
System.out.println("try-Block mit i=" + i);
if (i==0) {
throw new FileDoesNotExistException();
}
else if (i==1) {
throw new FileNotReadableException();
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 383 / 409
else if (i==2) {
throw new FileNotWritableException();
}
System.out.println("try-Block Ende");
}
catch (FileException x) {
System.out.println("FileException wurde gefangen");
}
}
}
Ausgabe
try-Block mit i=0
FileException wurde gefangen
try-Block mit i=1
FileException wurde gefangen
try-Block mit i=2
FileException wurde gefangen
try-Block mit i=3
try-Block Ende
Wie man sieht, wird in bei allen geworfenen Exception-Objekten der catch-Block der BasisKlasse zur Fehler-Behandlung genutzt.
22.7 Mehrere catch-Blöcke
Was ist aber nun, wenn ich ein paar konkrete Fehler abfangen und bearbeiten will, andere aber
ganz allgemein behandeln möchte?
In den vorherigen Kapiteln wurde schon zwischen den Zeilen erwähnt, dass es mehrere catchBlöcke pro try-Block geben kann.
• Jeder catch-Block ist für eine Exception-Klasse zuständig.
• Wird ein Exception-Objekt geworfen, so werden die catch-Blöcke von oben nach unten
abgesucht, ob das Exception-Objekt zu dem Typ des catch-Parameters paßt. Wenn ja, so
wird in den catch-Block verzweigt.
• Daher darf auch kein allgemeiner catch-Block vor einem spezielleren stehen. Wäre z.B. der
catch-Block für „FileException“ der erste, und erst danach würde der für
„FileDoesNotExistException“ folgen, so würde der zweite catch-Block nie zum Zuge
kommen. Denn jede „FileDoesNotExistException“ würde schon vom „FileException“ catchBlock abgefangen werden.
• Nach normaler Abarbeitung eines catch-Blocks gilt der Fehler als behandelt, und es wird –
möglicherweise nach Bearbeitung des optionalen try-Blocks – nach dem letzten catch-Block
fortgefahren.
public static void main(String[] args) {
for (int i=0; i<4; i++) {
try {
System.out.println("try-Block mit i=" + i);
if (i==0) {
throw new FileDoesNotExistException();
}
else if (i==1) {
throw new FileNotReadableException();
}
else if (i==2) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 384 / 409
throw new FileNotWritableException();
}
System.out.println("try-Block Ende");
}
catch (FileDoesNotExistException x) {
System.out.println("FileDoesNotExistException wurde gefangen");
}
catch (FileNotReadableException x) {
System.out.println("FileNotReadableException wurde gefangen");
}
catch (FileException x) {
System.out.println("FileException wurde gefangen");
}
}
}
Ausgabe
try-Block mit i=0
FileDoesNotExistException wurde gefangen
try-Block mit i=1
FileNotReadableException wurde gefangen
try-Block mit i=2
FileException wurde gefangen
try-Block mit i=3
try-Block Ende
22.8 Unbehandelte Exceptions
Wird ein Exception-Objekt geworfen, für das kein passender catch-Block, so wird der
Funktions-Stack Funktion für Funktion abgebaut (siehe Kapitel 22.3) bis zur „main“ Funktion. Ist
auch hier kein passender catch-Block vorhanden, so wird das Programm automatisch beendet.
private static void f2() {
System.out.println(" - f2");
System.out.println("
=> throw ohne catch");
throw new RuntimeException();
}
private static void f1() {
System.out.println("- f1");
f2();
}
public static void main(String[] args) {
System.out.println("main");
f1();
}
mögliche Ausgabe
main
- f1
- f2
=> throw ohne catch
java.lang.RuntimeException
at Appl.f2(Appl.java:6)
at Appl.f1(Appl.java:11)
at Appl.main(Appl.java:16)
Exception in thread "main"
Durch dieses Vorgehen wird verhindert, dass ein Fehler-Zustand unbehandelt bleibt, aber das
Programm weiter läuft – siehe auch Kapitel todo.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 385 / 409
22.9 Geprüfte und ungeprüfte Exceptions
In Java wird zwischen geprüften und ungeprüften Exceptions unterschieden – sie heißen auch
checked bzw. unchecked exceptions.
Für geprüfte Exceptions kann der Compiler zur Compile-Zeit feststellen, ob sie vollständig
behandelt werden, d.h. es immer einen passenden catch-Block im Quelltext gibt. Für
ungeprüfte Exceptions gilt dies nicht.
• Ungeprüfte Exceptions sind Exceptions, bei denen Objekte geworfen werden, die direkt oder
indirekt von „java.lang.Error“ bzw. „java.lang.RuntimeException“ abgeleitet sind.
• Alle anderen Exception-Objekte sind geprüfte Exceptions.
Welche Konsequenzen hat aber nun die Verwendung von geprüften Exceptions? Der Code ist
nur dann korrekt, d.h. wird vom Compiler compiliert, wenn das Werfen einer geprüften
Exception in der Funktions selber abgefangen wird, oder mit einer Exception-Spezifikation nach
außen gemeldet wird.
public void f() {
throw new Exception();
}
// Compiler-Fehler, unbehandelte Exception
Lösung 1 – Abfangen der Exception in der Funktion selber
public void f() {
try {
throw new Exception();
}
catch (Exception x) {
}
}
// Exception wird abgefangen
Lösung 2 – Funktion mit Exception-Spezifikation
public void f() throws Exception {
throw new Exception();
// Exception wird nach aussen gemeldet
}
Ein Exception-Spezifikation wird hinter die Paramter-Liste der Funktion geschrieben, und
besteht aus dem Schlüsselwort „throws“ und einer Auflistung der möglichen Exceptions. Wird in
der Exception-Spezifikation eine Klasse angegeben, so werden damit alle Objekte vom Typ der
Basis-Klasse bzw. einer von ihr abgeleiteten Klassen als mögliche Exception-Objekte erlaubt.
Ist keine Exception-Spezifikation vorhanden, so darf keine geprüfte Exception geworfen
werden.
Beispiele:
- void f() throws Exception
- void g() throws FileDoesNotExistException, FileNotReadableException
- void h()
• void f() throws Exception
Diese Funktion darf alle Exceptions werfen, deren Typ „java.lang.Exception“ ist bzw. deren
Typen direkt oder indirekt von „java.lang.Exception“ abgeleitet sind. Und natürlich
ungeprüfte Exceptions.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 386 / 409
• void g() throws FileDoesNotExistException, FileNotReadableException
Diese Funktion darf alle Exceptions werfen, deren Typ „FileDoesNotExistException“ oder
„FileNotReadableException“ ist bzw. von einer dieser Klassen abgeleitet ist. Und natürlich
ungeprüfte Exceptions.
• void h()
Diese Funktion darf keine geprüften Exceptions werfen, sondern nur ungeprüfte.
Achtung – die Exception-Spezifikation bezieht sich nur auf geprüfte Exceptions. Jede Funktion
kann jederzeit ungeprüfte Exceptions werfen.
Durch die Exception-Spezifikation weiß der Compiler, ob eine Funktion eine geprüfte Exception
werfen könnte oder nicht. Der Funktions-Aufruf einer Funktion, die eine geprüfte Exception
werfen könnte, muss seinerseits wieder entweder durch einen try-catch-Block oder eine
Exception-Spezifikation gesichert sein.
public void f1() throws Exception {
throw new Exception();
}
public void f2() {
f1();
}
// Compiler-Fehler, unbehandelte Exception
Lösung 1 – Abfangen der Exception in der Funktion selber
public void f1() throws Exception {
throw new Exception();
}
public void f2() {
try {
f1();
}
catch (Exception x)
}
}
// Exception wird abgefangen
{
Lösung 2 – Funktion mit Exception-Spezifikation
public void f1() throws Exception {
throw new Exception();
}
public void f2() throws Exception {
f1();
// Exception wird nach aussen gemeldet
}
Die Exception-Spezifikationen von Java sind nicht unumstritten:
Ein klarer Vorteil ist, dass - zumindest die geprüften Exceptions - im Code behandelt werden
müssen, da der Compiler dies prüft. Für die geprüften Exceptions können daher zur Laufzeit
keine unangenehmen Überraschungen auftreten.
In der Praxis findet man aber häufig try-Blöcke mit leeren catch-Blöcken, d.h. ohne wirkliche
Fehlerbehandlung. Diese Blöcke stehen dann nur da, um den Compiler zu befriedigen, nicht
aus wirklicher Überzeugung. Hier im Tutorial z.B. finden sich einige Beispiele mit leeren catch© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 387 / 409
Blöcken - zum Teil da noch keine Exceptions bekannt waren, und zum Teil da das Beispiel um
andere Sprach-Features ging und der Code nicht zu sehr aufgebläht werden sollte.
Exception-Spezifikation blähen die Funktions-Signatur häufig um technische Fehler auf, die auf
unteren Ebenen nicht behandelt werden können. Z.B. Netzwerk- oder Datenbank-Probleme
sind typische Beispiele. Die entsprechenden Exceptions sind geprüfte Exceptions. Da sie auf
unteren Ebenen nicht sinnvoll behandelt werden können, müssen sie nach oben durchgereicht
werden, und das heißt wiederrum lange Exception-Spezifikationen ohne wirklichen Vorteil denn das eine Datenbank-Operation schief gehen kann, sollte auch so jedem klar sein.
In der Praxis geht daher mittlerweile die Tendenz zu ungeprüften Exceptions. So gibt es z.B. im
Serverbereich das immer beliebtere Framework „Spring“, das u.a. mit dem Hauptvorteil wirbt,
dass es die geprüften JDBC (Datenbank) Exceptions automatisch in ungeprüfte überführt.
Die größte Gefahr ist aber das fehlende Bewußtsein vieler Programmierer für ungeprüfte
Exceptions. Aufgrund der immer wieder vorkommenden Compiler-Fehler wegen nicht
behandelter geprüfter Exceptions entsteht bei vielen Programmieren das Gefühl, sobald der
Compiler nicht mehr nervt habe man sich um alle möglichen Probleme gekümmert. Dabei wird
dann vergessen, dass es viele - sehr viele - ungeprüfte Exceptions gibt, die mögicherweise
nicht bedacht worden sind. Hierbei sollte auch bedacht werden, dass viele sehr elementare
Exceptions wie z.B. die „NullPointerException“ oder auch die
„ArrayIndexOutOfBoundsException“ zu den ungeprüften Exceptions gehören. Und sehr
kritische Fehler, wie z.B. fehlender Speicher, werden auch über ungeprüfte Exceptions
gemeldet für die dann auch noch die Empfehlung ausgesprochen wird „sich nicht drum zu
kümmern, da man in Java dann eh nichts mehr machen kann“ - siehe Kapitel todo. Ein weiterer
wichtiger Verweis ist auch das Kapitel todo über das Thema „Exception-Sicherheit“.
22.10 Verschachtelte try-Blöcke
Natürlich kann in einem try-Block wieder ein try-Block verkommen – direkt innerhalb der
Funktion (Bsp. 1) oder auch indirekt durch Funktions-Aufrüfe (Bsp. 2).
public static void main(String[] args) {
try {
System.out.println("Aeusserer try-Block");
try {
System.out.println("Innerer try-Block");
throw new Exception();
}
catch (Exception x) {
System.out.println("Innerer catch-Block");
}
System.out.println("Aeusserer try-Block Ende");
}
catch (Exception x) {
System.out.println("Aeusserer catch-Block");
}
}
private static void f() {
try {
System.out.println("Innerer try-Block");
throw new Exception();
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 388 / 409
}
catch (Exception x) {
System.out.println("Innerer catch-Block");
}
}
public static void main(String[] args) {
try {
System.out.println("Aeusserer try-Block");
f();
System.out.println("Aeusserer try-Block Ende");
}
catch (Exception x) {
System.out.println("Aeusserer catch-Block");
}
}
Ausgabe (in beiden Beispielen identisch)
Aeusserer try-Block
Innerer try-Block
Innerer catch-Block
Aeusserer try-Block Ende
In der Praxis ist gerade die zweite Variante nicht ungewöhnlich, da man häufig nicht weiß was
in den aufgerufenden Funktionen genau passiert. Und das intern irgendwas schief gehen
könnte, ist recht normal.
Drei Anwendungen sind hierbei besonders interessant:
• Erneutes Auswerfen einer Exception
• Auswerfen einer anderen Exception
• Unbehandelte Exceptions im inneren Block
22.10.1 Erneutes Auswerfen einer Exception
Eine gefangene Exception kann natürlich jederzeit im catch-Block wieder geworfen werden.
Dies passiert, wenn man auf einen Fehler oder eine Fehler-Gruppe dediziert reagieren möchte,
den Fehler aber nicht endgültig behandeln kann – ihn also weitermelden muß.
public static void main(String[] args) {
try {
System.out.println("Aeusserer try-Block");
try {
System.out.println("Innerer try-Block");
throw new Exception();
}
catch (Exception x) {
System.out.println("Innerer catch-Block");
throw x;
}
// System.out.println("Aeusserer try-Block Ende");
}
catch (Exception x) {
System.out.println("Aeusserer catch-Block");
}
}
unreachable code
Ausgabe
Aeusserer try-Block
Innerer try-Block
Innerer catch-Block
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 389 / 409
Aeusserer catch-Block
Hinweis – mit Verlassen des catch-Blocks gilt die primäre Exception als behandelt. Die neu
geworfene wurde aber noch nicht gefangen – und gilt daher nun als unbehandelt. Hierbei ist es
unwichtig, dass das neu geworfene Exception-Objekt identisch zum alten ist.
22.10.2 Auswerfen einer anderen Exception
Statt der Original Exception kann auch eine beliebige andere Excepton geworfen werden. Dies
wird häufig gemacht, wenn die Original Exception nicht nach aussen bekannt gemacht werden
soll, da sie z.B. aus einer externen Bibliothek kommt.
public static void main(String[] args) {
try {
System.out.println("Aeusserer try-Block");
try {
System.out.println("Innerer try-Block");
throw new Exception();
}
catch (Exception x) {
System.out.println("Innerer catch-Block");
throw new RuntimeException();
}
// System.out.println("Aeusserer try-Block Ende");
}
catch (Exception x) {
System.out.println("Aeusserer catch-Block");
}
}
unreachable code
Ausgabe
Aeusserer try-Block
Innerer try-Block
Innerer catch-Block
Aeusserer catch-Block
Hinweis – man nennt dies auch Exception-Mapping.
Hinweis – mit Verlassen des catch-Blocks gilt die primäre Exception als behandelt. Die neu
geworfene wurde aber noch nicht gefangen – und gilt daher nun als unbehandelt.
22.10.3 Unbehandelte Exceptions im inneren Block
Es ist nicht notwendig, dass die catch-Blöcke eines try-Blocks alle möglichen Exceptions
fangen. Ist kein passender catch-Block vorhanden, wird der Stack weiter abgebaut bis ein trymit einem passenden catch-Block gefunden wird.
public static void main(String[] args) {
try {
System.out.println("Aeusserer try-Block Start");
try {
System.out.println("Innerer try-Block Start");
System.out.println("Werfe Exception");
throw new Exception();
}
catch (ClassCastException x) {
System.out.println("Innerer catch-Block: ClassCastException gefangen");
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 390 / 409
}
System.out.println("Aeusserer try-Block Ende");
}
catch (Exception x) {
System.out.println("Aeusserer catch-Block: Exception gefangen");
}
}
Ausgabe
Aeusserer try-Block Start
Innerer try-Block Start
Werfe Exception
Aeusserer catch-Block: Exception gefangen
Dies ist ein ganz normaler Fall. Sie müssen nicht an jeder Stelle alle möglichen Exceptions
abfangen, nur weil sie geworfen werden könnten. Sondern an den Stellen, an denen sie
bestimmte Fehler behandeln, sie melden, neu aufsetzen oder sonst was können – also sinnvoll
auf diese Fehler reagieren können – an denen fangen sie sie ab. Ansonsten ignorieren sie sie –
im Rahmen der Exception-Sicherheit, siehe Kapitel 22.12.
22.11 Exception-Objekte
Ein Exception sollte detailierte Informationen über den Fehler enthalten, der zu ihrem Werfen
führte. Dann können Meldungen ausgeben werden, die helfen den Fehler zu finden und zu
beheben. Da sie prinzipiell für jeden Fehlertyp eine eigene Exception-Klasse entwerfen können,
stehen ihnen die kompletten Möglichkeiten einer Klasse zur Verfügung um diese Informationen
zu transportieren.
Unabhängig davon erbt jedes Exception Objekt, da es direkt oder indirekt von „Throwable“
abgeleitet ist, mehrere Funktionen – und damit implizit Funktionalitäten. Zwei davon möchte ich
hier vorstellen:
• public String getMessage()
Gibt einen String zurück, der die Exception näher beschreibt.
• public void printStackTrace()
Gibt die aktuelle Exception und den Funktions-Stack zum Zeitpunkt ihrer Erzeugung auf der
Error-Ausgabe aus.
public class Appl {
private static void f3() {
throw new RuntimeException("Ich bin ein Fehler-Objekt");
}
private static void f2() {
f3();
}
private static void f1() {
f2();
}
public static void main(String[] args) {
try {
f1();
}
catch (Throwable t) {
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 391 / 409
System.out.println("Throwable gefangen");
System.out.println("Obj: \"" + t + '"');
System.out.println("Msg: \"" + t.getMessage() + '"');
System.out.println();
t.printStackTrace();
}
}
}
Ausgabe
Throwable gefangen
obj: "java.lang.RuntimeException: Ich bin ein Fehler-Objekt"
msg: "Ich bin ein Fehler-Objekt"
java.lang.RuntimeException: Ich bin ein Fehler-Objekt
at Appl.f3(Appl.java:4)
at Appl.f2(Appl.java:8)
at Appl.f1(Appl.java:12)
at Appl.main(Appl.java:17)
Hinweis – diesmal ist das Beispiel der vollständige Quelltext, damit sie sehen das im StackTrace Dateiname und Zeilennummern der Funktions-Aufrufe enthalten sind.
22.12 Exception-Sicherheit
So einfach und schlüssig Exceptions im ersten Augenblick auch wirken - es ist schwer, wirklich
Exception-sicheren Code zu schreiben. Exception-sicherer Code ist Code, der im Falle einer
Exception keinen undefinierten Status zurückläßt, sondern die Objekte immer in einem
sauberen Zustand hinterläßt. Hierbei werden drei Level unterschieden:
Level 1 – Starke-Garantie
Wenn eine Exception geworfen wird, bleibt der Status des Programms im logischen Sinne
unverändert. Diese Garantie impliziert eine globale Commit/Rollback Strategie, die auch dafür
sorgt, dass z.B. Referenzen und Iteratoren nach einer Exception noch korrekt sind.
Level 2 – Mindest-Garantie
Wenn eine Exception geworfen wird, so entstehen keine Ressourcen-Löcher (z.B. nicht
geschlossene Dateien oder Netzwerk-Verbindungen), und alle Objekte bleiben konsistent und
benutzbar. Dies ist die Mindest-Garantie, die gegeben sein muss, damit ein Programm
funktionsfähig bleibt.
Level 3 – keine Garantie
Wenn eine Exception geworfen wird, so entstehen Ressourcen-Löcher bzw. nicht benutzbare
oder inkonsistente Objekte.
Level 3 darf nie passieren, denn dann haben sie ein potentielles Problem in ihrem Programm.
Level 1 wäre der Ideal-Zustand, der aber häufig nicht erreichbar ist, aber angesprebt werden
sollte.
Hier ein kleines Beispiel – Exception-sicher oder nicht?
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 392 / 409
public class Person {
private String name;
private int age;
private String phone;
public Person(String n, String a, String p) {
name = n;
age = Integer.parseInt(a);
phone = p;
}
public String toString() {
return name + " (Alter: " + age + ") - Telefon: " + phone;
}
public void init(String n, String a, String p) {
name = n;
age = Integer.parseInt(a);
phone = p;
}
}
Natürlich nicht! Schauen wir uns z.B. folgende Benutzung an:
public static void main(String[] args) {
Person prs = new Person("Detlef", "23", "0241 / 123 456");
System.out.println("Person: " + prs);
try {
prs.init("Bernd", "--", "012 / 345 67");
}
catch (Exception x) {
}
System.out.println("Person: " + prs);
}
Ausgabe
Person: Detlef (Alter: 23) - Telefon: 0241/ 123 456
Person: Bernd (Alter: 23) - Telefon: 0241/ 123 456
Wie sie an der Ausgabe sehen, existiert nach der Exception ein Personen-Objekt, das einen
inkonsistenten Zustand hat. Denn eine Person „Bernd“ mit Alter „23“ und der Telefon-Nr.
„0241/123456“ gibt es nicht.
Das Problem ist hier die „init“ Funktion in der Klasse „Person“. Mitten in der Veränderung der
Attribute der Klasse kann eine Exception geworfen werden – in diesem Augenblick ist ein Teil
der Attribute verändert, während ein anderer Teil noch den alten Wert hat. Das darf nicht
passieren!
Die Lösung ist entweder:
• Erst alle Veränderungen lokal machen, und sie dann in einem Rutsch Exception-sicher
ausführen. Oder
• Alle Veränderungen Stück für Stück ausführen, aber im Falle einer Exception die schon
gemachten rückgängig zu machen.
public void init(String n, String a, String p) {
int ag = Integer.parseInt(a);
name = n;
age = ag;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 393 / 409
phone = p;
}
Das Problem ist, dass an sehr vielen Stellen Exceptions geworfen werden können – man aber
nicht daran denkt, da man sie im normalen Code nicht sieht. Und da viele Exceptions keine
geprüften Exceptions sind, macht einen der Compiler auch nicht darauf aufmerksam.
23 Streams
23.1 Einführung
Ein allgemeines Konzept in der Software-Entwicklung sind die sogenannte Streams. Ein
Stream ist eigentlich nicht mehr als ein Datenstrom, der die Daten von irgendwoher bezieht und
irgendwohin liefert. Außerdem können Streams möglicherweise manipuliert werden (z.B.
geleert werden).
Abb. 23-1 : Ein nicht näher spezifizierter Datenstrom
In der Praxis sind zwei Stream-Varianten besonders wichtig:
• Byte-Streams, d.h. Streams, bei denen der Datenstrom aus einfachen Bytes ohne jede
Struktur besteht. Byte-Streams werden in Java durch Input-Streams bzw. Output-Streams
repräsentiert – siehe Kapitel todo.
• Zeichen-Streams, d.h. Streams, bei denen der Datenstrom aus Zeichen besteht. Da Java
intern mit Unicode UTF-16 arbeitet, sind hier im Normallfall 2 Byte große Zeichen mit UTF16 Codierung gemeint. Zeichen-Streams werden in Java durch Reader und Writer
repräsentiert – siehe Kapitel todo.
Bemerkung – da Streams sehr allgemein sind, werden sie oft als Grundlage für ein
Kommunikation-Konzept zwischen zwei beliebigen „Partnern“ genommen, z.B. zwischen
Prozessen, Threads, zwei Netzwerk-Teilnehmern, einem Datei-System und einem Programm,
usw.
23.2 Java Streams
23.2.1 Architektur
In Java stehen 4 Stream-Hierarchien zur Verfügung:
• Eingabe Byte-Streams – abstrakte Basis-Klasse: „java.io.InputStream“
• Ausgabe Byte-Streams – abstrakte Basis-Klasse: „java.io.OutputStream“
• Eingabe Zeichen-Streams – abstrakte Basis-Klasse: „java.io.Reader“
• Ausgabe Zeichen-Streams – abstrakte Basis-Klasse: „java.io.Writer“
Warum gibt es überhaupt 4 Stream-Hierarchien, und nicht einfach 4 Stream-Klassen?
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 394 / 409
• Nun, im einfachsten Fall transportieren Streams ihre Daten unverändert von der Quelle zum
Ziel. Aber es gibt auch Streams, die die Daten unterwegs manipulieren – z.B. puffern,
filtern, zählen, zippen bzw. unzippen, Hash-Werte bestimmen, und vieles mehr.
• Außerdem sind die Quellen und Ziele je nach Anforderung unterschiedlich. In einem Fall
wird z.B. aus einer Datei gelesen und in einen String geschrieben. In einem anderen Fall
aus einem Byte-Array gelesen und in eine Datei geschrieben.
Statt einer Klasse, die „alles“ anbietet, wurde in Java die bessere und offenere Architektur einer
Klassen-Hierarchie gewählt.
• Es gibt eine abstrakte Basis-Klasse, z.B. „InputStream“
• Pro Verhalten gibt es eine konkrete abgeleitete Klasse, z.B. „BufferedInputStream“.
• Viele Streams können einen anderen gleichartigen Stream kapseln.
Welche Vorteile hat eine solche Architektur?
• Kleine, übersichtliche und gekapselte Komponenten
Jede einzelne Klasse ist nur für ein Verhalten da, und kann so klein und übersichtlich – auch bzgl.
ihrer Schnittstelle – gehalten werden.
• Erweiterbarkeit und Offenheit
Implementiert keine Stream-Klasse ein gewünschtes Verhalten, so kann es jederzeit selber
implementiert und in die Stream-Verarbeitung integriert werden.
• Kombinierbarkeit
Da viele Streams ineinander gekapselt werden können, können die einzelnen Transformationen in
beliebigen Kombinationen und Reihenfolgen aufeinander angewandt werden.
• Erweiterbarkeit, Offenheit und Kombinierbarkeit
Es können alle Quellen mit allen Zielen kombiniert werden, und es können eigene Quellen bzw. Ziele
in die Stream-Verarbeitung integriert werden.
Gerade die Voraussetzung „Streams fast beliebig ineinander zu kapseln“ erlaubt eine wahre
Vielfalt an Lösungen. So könnte z.B. ein Input-Stream eine Datei kapseln. Um diesen gepuffert
auszulesen, wird er in einem gepuffert-Input-Stream gekapselt. Ist die Datei verschlüsselt,
würde man diesen in einem Entschlüsselungs-Stream kapseln. Und wenn der eigentliche Inhalt
dann noch gezippt ist, dann wird noch ein Unzip-Input-Stream um diesen herum gelegt.
Abb. 23-2 : Bsp für ineinander gekapselte Streams
Hinweis – funktionieren tut dies intern natürlich wieder mit Vererbung und Polymorphie. Da
z.B. alle Input-Streams von einer Basis-Klasse abgeleitet sind, können sie alle über diese
gehandelt werden. Da sie die Schnittstelle der Basis-Klasse überschrieben haben, werden die
Aufrufe via Polymorphie transparent an die jeweiligen Implementierungen weitergereicht.
Abb. 23-3 : Arbeitsweise der Stream-Kapselung
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 395 / 409
23.2.2 Byte-Streams und Zeichen-Streams
Der allgemeinere Stream-Typ ist natürlich der Byte-Stream, da intern alle Daten aus Bytes
aufgebaut sind. Um aus einem Byte-Stream Zeichen zu lesen, existiert daher die von „Reader“
abgeleitete Klasse „java.io.InputStreamReader“, die einen Input-Stream in einem Reader
kapselt. Umgekehrt gibt es die von „Writer“ abgeleitete Klasse „java.io.OutputStreamWriter“,
die einen Output-Stream in einem Writer kapselt.
Ein Beispiel hierfür findet sich z.B. in Kapitel 23.3.1.
23.2.3 Stream-Hierarchien
Viele spezielle Stream-Fähigkeiten (d.h. konkrete Stream-Klassen) finden sich in allen 4
Stream-Hierarchien. Zum einen werden natürlich für die meisten Eingabe-Streams auch die
entsprechenden Gegenstücke auf der Ausgabe-Seite benötigt, bzw. umgekehrt. Zum anderen
sind viele Fähigkeiten unabhängig vom Daten-Typ (d.h. Byte oder Zeichen), und sind daher in
beiden Hierarchien sinnvoll.
Stellvertretend für alle Stream-Hierarchien wird hier die Eingabe Byte-Stream Hierarchie
detailierter betrachtet. Für die anderen Streams wird auf die Java-Hilfe verwiesen.
java.lang.Object
java.io.InputStream (abstract) – siehe auch Kapitel todo
javax.sound.sampled.AudioInputStream
java.io.ByteArrayInputStream – siehe auch Kapitel todo
java.io.FileInputStream – siehe auch Kapitel todo
java.io.FilterInputStream
java.io.BufferedInputStream – siehe auch Kapitel todo
java.util.zip.CheckedInputStream
javax.crypto.CipherInputStream
java.io.DataInputStream – siehe auch Kapitel todo
java.security.DigestInputStream
java.util.zip.InflaterInputStream
java.util.zip.GZIPInputStream
java.util.zip.ZipInputStream
java.util.jar.JarInputStream
java.io.LineNumberInputStream (deprecated)
javax.swing.ProgressMonitorInputStream
java.io.PushbackInputStream – siehe auch Kapitel todo
org.omg.CORBA.portable.InputStream
java.io.ObjectInputStream – siehe auch Kapitel todo
java.io.PipedInputStream
java.io.SequenceInputStream – siehe auch Kapitel todo
java.io.StringBufferInputStream (deprecated)
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 396 / 409
23.2.3.1 Klasse „java.io.InputStream“
Basis-Klasse für alle byte-orientierten Input-Streams
• public int available() throws IOException
• public void close() throws IOException
• public abstract int read() throws IOException
• public int read(byte[ ] b) throws IOException
23.2.3.2 Klasse „java.io.ByteArrayInputStream“
Hiermit kann ein Byte-Array mit einem Input-Stream verbunden werden.
byte[] array = new byte[10];
ByteArrayInputStream in = new ByteArrayInputStream(array);
23.2.3.3 Klasse „java.io.FileInputStream“
Ist ein Stream, der mit einem File verbunden wird.
• public FileInputStream(File file) throws FileNotFoundException
• public FileInputStream(String name) throws FileNotFoundException
Ein Beispiel hierfür finden sie z.B. in Kapitel 23.3.2.
23.2.3.4 Klasse „java.io.BufferedInputStream“
Implementiert einen gepufferten Input-Stream
• public BufferedInputStream(InputStream in)
Ein Beispiel hierfür finden sie z.B. in Kapitel 23.3.2 und Kapitel 25.
23.2.3.5 Klasse „java.io.DataInputStream“
Mit diesem Stream können die elementaren Datentypen aus einem zugrunde liegenden
InputStream auf maschinenunabhängge Weise gelesen werden.
• public DataInputStream(InputStream in)
• public final int readInt() throws IOException
23.2.3.6 Klasse „java.io.PushbackInputStream“
Implementiert einen Stream mit Puffer, in den Daten zurückgeschrieben werden können.
• public PushbackInputStream(InputStream in)
• public void unread(byte[ ] b) throws IOException
23.2.3.7 Klasse „java.io.ObjectInputStream“
Implementiert einen Stream, aus dem komplette Objekte heraus gelesen werden können.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 397 / 409
• public ObjectInputStream(InputStream in) throws IOException, ...
• public final Object readObject() throws OptionalDataException, ClassNotFoundException
Beispiele hierfür finden sich im Kapitel über Serialisierung – siehe Kapitel 25.
23.2.3.8 Klasse „java.io.SequenceInputStream“
Kann Input-Streams sequenziell koppeln.
• public SequenceInputStream(InputStream s1, InputStream s2)
Ein Beispiel hierfür finden sie z.B. in Kapitel 23.3.4.
23.3 Beispiele
23.3.1 Beispiel „Tastatur-Eingabe“
Beispiel aus Kapitel 3.10. Erläuterung – todo...
public static void main(String[] args) {
try {
System.out.println("Echo-Programm");
InputStreamReader isr = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(isr);
while (true) {
System.out.print("> ");
String in = reader.readLine();
if (in.length()==0) {
break;
}
System.out.println(" \"" + in + "\" - " + in.length() + " Zeichen");
}
}
catch (Exception x) {
}
System.out.println("Programm-Ende");
}
mögliche Ausgabe
Echo-Programm
> Hallo
"Hallo" - 5 Zeichen
> Ich lerne jetzt Java
"Ich lerne jetzt Java" - 20 Zeichen
>
Programm-Ende
23.3.2 Beispiel „Datei lesen“
Erläuterung – todo...
public static void main(String[] args) {
try {
System.out.println("Lese Datei \"Testdaten.txt\" - Version 1");
File file = new File("Testdaten.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 398 / 409
BufferedReader reader = new BufferedReader(isr);
while (true) {
String in = reader.readLine();
if (in==null) break;
System.out.println("> " + in);
}
System.out.println("Datei-Ende");
}
catch (Exception x) {
System.out.println("Probleme beim Lesen der Datei");
}
}
mögliche Ausgabe
Lese Datei "Testdaten.txt"
> Hallo Welt,
>
> diese Zeilen kommen aus der Datei.
Datei-Ende
Eine alternative kürzere Variante arbeitet mit der Klasse „java.io.FileReader“, die intern ein FileObjekt erzeugt, dieses in einen File-Input-Stream und dann in einem Input-Stream-Reader
kapselt.
public static void main(String[] args) {
try {
System.out.println("Lese Datei \"Testdaten.txt\" - Version 2");
FileReader fr = new FileReader("Testdaten.txt");
BufferedReader reader = new BufferedReader(fr);
while (true) {
String in = reader.readLine();
if (in==null) break;
System.out.println("> " + in);
}
System.out.println("Datei-Ende");
}
catch (Exception x) {
System.out.println("Probleme beim Lesen der Datei");
}
}
23.3.3 Beispiel „Lesen mit Zeilen-Nummer“
Erläuterung – todo...
public static void main(String[] args) {
try {
System.out.println("Echo-Programm mit Zeilen-Nummern");
InputStreamReader isr = new InputStreamReader(System.in);
LineNumberReader reader = new LineNumberReader(isr);
while (true) {
System.out.print("> ");
String in = reader.readLine();
if (in.length() == 0) {
break;
}
System.out.print(reader.getLineNumber() + ":");
System.out.println(" \"" + in + "\" - " + in.length() + " Zeichen");
}
}
catch (Exception x) {
}
}
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 399 / 409
mögliche Ausgabe
Echo-Programm mit Zeilen-Nummern
> Hallo Welt,
1: "Hallo Welt," - 11 Zeichen
> ich lerne jetzt Java.
2: "ich lerne jetzt Java." - 21 Zeichen
> Bald schon kann ich tolle Sachen schreiben.
3: "Bald schon kann ich tolle Sachen schreiben." - 43 Zeichen
> Bis bald...
4: "Bis bald..." - 11 Zeichen
>
23.3.4 Beispiel „Sequenzielle Kopplung“
Erläuterung – todo...
public static void main(String[] args) {
try {
System.out.println("Lesen aus zwei gekoppelten Strings:");
String s1 = "Zeile 1\n\nUnd dies ist ";
String s2 = "die Zeile 3\nZeile 4";
// StringBufferInputStream ist zwar deprecated, aber die Klasse
// ermoeglicht das einfachste Beispiel - darum doch.
StringBufferInputStream sbis1 = new StringBufferInputStream(s1);
StringBufferInputStream sbis2 = new StringBufferInputStream(s2);
SequenceInputStream sis = new SequenceInputStream(sbis1, sbis2);
InputStreamReader isr = new InputStreamReader(sis);
LineNumberReader reader = new LineNumberReader(isr);
while (true) {
String in = reader.readLine();
if (in==null) {
break;
}
System.out.println(reader.getLineNumber() + ": \"" + in + "\"");
}
}
catch (Exception x) {
}
}
Ausgabe
Lesen aus zwei gekoppelten Strings:
1: "Zeile 1"
2: ""
3: "Und dies ist die Zeile 3"
4: "Zeile 4"
23.4 Stream-Tokenizer
Hilfs-Klasse um Zeichen-Ströme in Token zu zerlegen: „java.io.StreamTokenizer“.
• public StreamTokenizer(Reader r)
• public int ttype
• public static final int TT_EOF
• public static final int TT_EOL
• public static final int TT_NUMBER
• public static final int TT_WORD
• public String sval
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 400 / 409
• public double nval
• public int nextToken() throws IOException
public static void main(String[] args) {
try {
String str = "3 mal 3 ist ach..., aeh ... 9!";
StringReader sr = new StringReader(str);
StreamTokenizer st = new StreamTokenizer(sr);
while (st.nextToken() != StreamTokenizer.TT_EOF) {
switch (st.ttype) {
case StreamTokenizer.TT_EOL:
System.out.println("End of Line");
break;
case StreamTokenizer.TT_NUMBER:
System.out.println("Zahl: " + st.nval);
break;
case StreamTokenizer.TT_WORD:
System.out.println("Wort: " + st.sval);
break;
}
}
}
catch (Exception x) {
}
}
Ausgabe
Zahl: 3.0
Wort: mal
Zahl: 3.0
Wort: ist
Wort: ach...
Wort: aeh
Zahl: 0.0
Zahl: 0.0
Zahl: 0.0
Zahl: 9.0
Der Case-Fall „StreamTokenizer.TT_EOL“ wird hier eigentlich nicht benötigt, da defaultmässig
im StringTokenizer ein EOL ein Whitespace ist.
24 Reflexion
Java bietet ein Sprachmittel, mit dem zur Laufzeit Informationen über beliebige Objekte
abgefragt und genutzt werden können. Dazu existiert in Java die Klasse „java.lang.Class“, die
eine Art Meta-Ebene über den normalen Klassen darstellt, d.h. sie enthält Informationen über
die Klassen selber. Mit solchen Informationen kann man anders programmieren, als man es
von ‘normalen’ Compiler-Sprachen gewöhnt ist.
24.1 Objekt-Erzeugung über den Klassen-Namen
Eine der Möglichkeiten die sich durch solche Meta-Informationen bietet, ist die Erstellung von
Objekten einer Klasse zur Laufzeit nur unter Angabe des Klassen-Namens.
Bitte verstehen Sie diesen Satz richtig. Es geht nicht um die normale Erzeugung eines Objekts
mit „new“ in der Art:
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 401 / 409
String s = new String();
sondern um eine Erzeugung, bei der der Klassen-Name zur Compile-Zeit noch vollkommen
unbestimmt ist.
Object o1 = ClassCreator.create("KlassenName");
Object o2 = ClassCreator.create(string);
Hierbei erzeugt die Funktion „create“ der Klasse „ClassCreator“ zur Laufzeit aus dem String ein
Objekt der Klasse und liefert dieses zurück. Der String kann z. B. auch aus einer BenutzerEingabe oder einer Datenbank87 stammen, d.h. er ist zur Compile-Zeit vollkommen unbestimmt.
Die Klasse „ClassCreator“ und die Funktion „create“ sind hierbei willkürlich gewählt – sie
existieren so in Java nicht. Es ging dabei erstmal nur darum, den obigen Code einfach zu
halten, und den Unterschied zu „new“ zu verdeutlichen. Aber wir können problemlos eine
Klasse erstellen, die sich entsprechend verhält.
public class ClassCreator {
public static Object create(String name) {
try {
Class descriptor = Class.forName(name);
return descriptor.newInstance();
}
catch (Throwable x) {
System.out.println("Probleme in ClassCreator.create(String)");
}
return null;
}
}
Und so wird sie dann benutzt:
StringBuffer s = (StringBuffer)ClassCreator.create("java.lang.StringBuffer");
s.append("Java ganz meta-maessig");
System.out.println(s);
Ausgabe
Java ganz meta-maessig
„Object forName(String)“ ist eine Klassen-Funktion der Klasse „Class“, die einen String - den
vollständig referenzierten Klassen-Namen - erwartet, und dann einen Klassen-Descriptor vom
Typ „Class“ zurückgibt. Dieses Objekt der Klasse „Class“ enthält eine vollständige
Beschreibung der angegebenen Klasse. Mit diesem Descriptor kann z.B. mit „newInstance()“
ein neues Objekt der Klasse erzeugt werden88.
Achtung – die Klasse des so erzeugten Objekts muss einen Standard-Konstruktor haben, da
die JVM keine Parameter herbeizaubern kann.
Der Mechanismus der Serialisierung (siehe Kapitel todo...) arbeitet genau mit diesem Trick.
88 Da ClassCreator.create ein Object zurückgibt, muss das Ergebnis natürlich gecastet
werden, damit es benutzt werden kann.
87
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 402 / 409
Folgende Probleme können auftauchen:
String
Leerstring "", bzw. Unerlaubter Name
Unreferenzierter Klassen-Name
Nicht public Klasse in fremdem Package
Klasse mit privatem Standard-Konstruktor
Klasse ohne Standard Konstruktor
Abstrakte Klasse bzw. Interface
Exception
IllegalArgumentException
ClassNotFoundException
IllegalAccessException
IllegalAccessException
NoSuchMethodError
IllegalArgumentException
24.2 Klasse „Class“
Für jede Klasse in der virtuellen Maschine wird ein „Class“ Objekt angelegt. Mit der Funktion
‘Class getClass()’ der Klasse „Object“ kann man daher direkt an die Meta-Informationen zu
einem Objekt gelangen.89
String s = "Hallo";
Class c = s.getClass();
24.2.1 Element-Funktionen
Die Klasse „Class“ enthält Element-Funktionen, mit der Information über die Klasse des
zugehörigen Class-Objekts zur Laufzeit ermittelt werden können. Hier eine kleine Auswahl:
Funktion
String getName()
Constructor[ ] getConstructors()
Constructor[ ] getDeclaredConstructors()
Method[ ] getMethods()
Method[ ] getDeclaredMethods()
Field[ ] getFields()
Field[ ] getDeclaredFields()
boolean isArray()
boolean isInterface()
boolean isPrimitive()
Rückgabewert
voll referenziert Name der Klasse
Array aller public Konstruktoren
Array aller Konstruktoren
Array aller public Methoden
Array aller Methoden
Array aller public Attribute
Array aller Attribute
ob das Objekt ein Array ist
ob das Objekt ein Interface ist
ob das Objekt ein elementarer Datentyp ist
Hinweis – alle diese Klassen (z.B. „Constructor“, „Method“,...) und viele weitere kommen aus
dem Package „java.lang.reflect“.
Und hier ein kleines einfaches Beispiel:
class Reflect {
private int privateInt;
Da in Java jede Klasse immer direkt oder indirekt von Object abgeleitet ist, ist diese Funktion
immer vorhanden.
89
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 403 / 409
protected boolean protectedBoolean;
public double publicDouble;
public Reflect() {}
public Reflect(int i) {}
private void privateFct() {}
protected void protectedFct() {}
public void publicFct() {}
public int fct1() { return 0; }
public void fct1(int i) {}
public void fct1(int i, String s) {}
}
public static void main(String[] args) {
Reflect r = new Reflect();
Class c = r.getClass();
System.out.println("Allgemeine Klasseninformationen");
System.out.println(" " + c.getName());
System.out.println("Konstruktoren");
Constructor[] con = c.getConstructors();
for (int i=0; i<con.length; i++) {
System.out.println(" " + con[i]);
}
System.out.println("Methoden");
Method[] m = c.getMethods();
for (int i=0; i<m.length; i++) {
System.out.println(" " + m[i]);
}
System.out.println("Attribute");
Field[] f = c.getDeclaredFields();
for (int i=0; i<f.length; i++) {
System.out.println(" " + f[i]);
}
}
Ausgabe
Allgemeine Klasseninformationen
Reflect
Konstruktoren
public Reflect(int)
public Reflect()
Methoden
public void Reflect.publicFct()
public void Reflect.fct1(int)
public void Reflect.fct1(int,java.lang.String)
public int Reflect.fct1()
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final void java.lang.Object.wait(long,int) throws
java.lang.InterruptedException
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
public java.lang.String java.lang.Object.toString()
Attribute
private int Reflect.privateInt
protected boolean Reflect.protectedBoolean
public double Reflect.publicDouble
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 404 / 409
Die Parameter-Namen z.B. lassen sich übrigens zur Laufzeit nicht mehr feststellen, da diese
Informationen nicht zur Klasse gehören, und vom Compiler nirgendwo hinterlegt werden.
24.3 Disassemblieren
Bei dem Thema Reflexion liegt die Frage nahe, ob jemand bestehenden Java Byte-Code
decompilieren bzw. disassemblieren kann, und damit an das Know-How des Entwicklers / der
Firma kommen kann, der / die diesen Code geschrieben hat.
Die komplette Programmstruktur liegt immer im Java Byte-Code vor, sonst könnte das
Reflexion API nicht funktionieren. Ausserdem kann der Byte Code in hohen Masse wieder in
relativ lesbaren Quellcode zurückgewandelt werden, denn:
• Die Abbildung von Anweisungen auf Code ist relativ eindeutig.
• Es gibt in Java keinen Präprozessor.
• OO Funktion sind meist sehr klein (90% weniger als 20 Zeilen)
• Der Compiler kann nur wenige Optimierungen machen.
Nur die Namen der Parameter und der lokalen Variablen fehlen.
Ein relativ einfacher Disassembler liegt dem Java SDK bei – es ist das Kommandozeilen-Tool
„javap“. Es gibt viele viel leistungsfähigerer Disassembler – einen relativ guten Ruf hat das Tool
„Jad“ – siehe http://www.kpdus.com/jad.html.
Es gibt daher Tools – sogenannte Obfuscator – die sämtliche Symbol-Definitionen und deren
Benutzungen mit unleserlichen Namen ersetzen. Dies funktioniert aber nur, solange keine
Reflection eingesetzt wird.
24.4 Aufgaben
24.4.1 Aufgabe „Klassen-Inspektor“
Schreiben sie ein kleines grafisches Tool, einen Klassen-Inspektor. Hierbei kann der Benutzer
einen vollständig referenzierten Klassen-Namen eingeben, und erhält danach eine Tabelle der:
- (public) Konstruktoren
- (public) Funktionen
- (public) Attribute
- usw.
Lösung siehe Kapitel 24.5.
24.5 Lsg. zu Aufgabe „Klassen-Inspektor“ – Kap. 24.4.1
todo...
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 405 / 409
25 Serialisierung
Der Mechanismus der Serialisierung und der Deserialisierung erlaubt es den Status von
Objekten in einen Stream zu schreiben, bzw. ihn daraus zu lesen, um diesen z.B. nach einem
Programmlauf für den nächsten zu sichern90. Oder auch um Objekt-Stati von einem Rechner
auf den anderen zu transportieren.
Für diesen Mechanismus sind in Java folgende Elemente vorhanden:
• Interface „java.io.Serializable“
• Klasse „java.io.ObjectOutputStream“
• Klasse „java.io.ObjectInputStream“
• Interface „java.io.Externalizable“
• Interface „java.io.ObjectOutput“
• Interface „java.io.ObjectInput“
Genau genommen werden die Interface’s Externalizable, ObjectOutput und ObjectInput in
vielen Fällen gar nicht für eine Serialisierung bzw. Deserialisierung benötigt, da das DefaultVerhalten von Java für die meisten Zwecke ausreicht. Über die Implementierung dieser
Interface’s kann das Default-Verhalten der Serialisierung verändert und angepaßt werden.
Da Serialisierung und Deserialisierung Ein- bzw. Ausgabe-Operationen sind, die fehlschlagen
können, können alle Methoden eine IOException bzw. davon abgeleitete Exception-Klassen
werfen. Manche Funktionen werfen unter Umständen auch noch andere Exceptions. Dies sind
geprüfte Exceptions – bei der Benutzung werden also immer entsprechende try/catch Blöcke
oder Exception-Spezifikationen notwendig sein.
25.1 Interface Serializable
Mit dem Serializable Interface bietet Java einen leistungsstarken Default Mechanismus für
Serialisierung und Deserialisierung, der in vielen Fällen ausreicht. Damit eine Klasse serialisiert
bzw. deserialisiert werden kann, muss sie nur vom Interface Serializable abgeleitet werden. Da
Serializable keine Funktion deklariert, muss in der Serializable-Klasse nichts weiter getan
werden.
import java.io.*;
public class SerialClass implements Serializable {
private String s;
private int i;
public SerialClass(String s, int i) {
this.s = s;
this.i = i;
}
public void print() {
System.out.println("Objekt von SerialClass mit: " + s + " - " + i);
90
Dies wird oft auch mit dem Begriff ‘Persistenz’ belegt.
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 406 / 409
}
}
System.out.println("Schreiben");
try {
SerialClass obj1 = new SerialClass("Objekt 1", 42);
SerialClass obj2 = new SerialClass("Objekt 2", 23);
obj1.print();
obj2.print();
FileOutputStream file = new FileOutputStream("objekte.dat");
ObjectOutputStream oos = new ObjectOutputStream(file);
oos.writeObject(obj1);
oos.writeObject(obj2);
oos.flush();
file.close();
}
catch (Exception x) {
System.out.println("Fehler beim Schreiben: " + x);
}
Ausgabe
Schreiben
Objekt von SerialClass mit: Objekt 1 - 42
Objekt von SerialClass mit: Objekt 2 - 23
In diesem Beispiel werden zwei Objekte der Klasse „SerialClass“ in die Datei "objekte.dat"
serialisiert (abgespeichert).
Um die beiden Objekte wieder einzulesen ist genauso viel Aufwand nötig:
System.out.println("Lesen");
try {
FileInputStream file = new FileInputStream("objekte.dat");
ObjectInputStream ois = new ObjectInputStream(file);
SerialClass obj1 = (SerialClass) ois.readObject();
SerialClass obj2 = (SerialClass) ois.readObject();
file.close();
obj1.print();
obj2.print();
}
catch (Exception x) {
System.out.println("Fehler beim Lesen: " + x);
}
Ausgabe
Lesen
Objekt von SerialClass mit: Objekt 1 - 42
Objekt von SerialClass mit: Objekt 2 - 23
Hinweis – intern arbeitet dies natürlich mit Reflection, über das alle notwendigen Informationen
für die JVM zur Verfügung stehen.
25.2 Rekursion
Die Serialisierung ist nicht auf die primäre Klassen-Ebene beschränkt, sondern läuft rekursiv
durch alle abhängigen Objekte und speichert den gesamten Objekt-Graphen, der zur
vollständigen Wiederherstellung nötig ist.
public class SerialClass1 implements Serializable {
private String s;
private int i;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 407 / 409
private SerialClass2 sc2;
public SerialClass1(String s, int i) {
this.s = s;
this.i = i;
sc2 = new SerialClass2(s);
}
public void print() {
System.out.println("Objekt von SerialClass1 mit: " + s + " - " + i);
System.out.println("- Unter-Objekt: " + sc2);
}
}
public class SerialClass2 implements Serializable {
private String s;
public SerialClass2(String s) {
this.s = s;
}
public String toString() {
return "SerialClass2 mit \"" + s + "\"";
}
}
Ausgabe in Anlehnung an das Bsp aus Kapitel 25.1.
Schreiben
Objekt von SerialClass1 mit: Objekt 1 - 42
- Unter-Objekt: SerialClass2 mit "Objekt 1"
Objekt von SerialClass1 mit: Objekt 2 - 23
- Unter-Objekt: SerialClass2 mit "Objekt 2"
Lesen
Objekt von SerialClass1 mit:
- Unter-Objekt: SerialClass2
Objekt von SerialClass1 mit:
- Unter-Objekt: SerialClass2
Objekt 1 - 42
mit "Objekt 1"
Objekt 2 - 23
mit "Objekt 2"
Bei einer solchen Rekursion müssen auch alle abhängigen Klassen serialisierbar sein. Ist eine
Klasse dies nicht, so werden beim Speichern bzw. beim Lesen entsprechende Exceptions
geworfen – siehe Beispiel.
public class SerialClass implements Serializable {
private String s;
private int i;
private NonSerialClass nsc;
public SerialClass(String s, int i) {
this.s = s;
this.i = i;
nsc = new NonSerialClass(s);
}
public void print() {
System.out.println("Objekt von SerialClass mit: " + s + " - " + i);
System.out.println("- Unter-Objekt: " + nsc);
}
}
public class NonSerialClass {
private String s;
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 408 / 409
public NonSerialClass(String s) {
this.s = s;
}
public String toString() {
return "NonSerialClass mit \"" + s + "\"";
}
}
Ausgabe in Anlehnung an das Bsp aus Kapitel 25.1.
Schreiben
Objekt von SerialClass mit: Objekt 1 - 42
- Unter-Objekt: NonSerialClass mit "Objekt 1"
Objekt von SerialClass mit: Objekt 2 - 23
- Unter-Objekt: NonSerialClass mit "Objekt 2"
Fehler beim Schreiben: java.io.NotSerializableException: NonSerialClass
Lesen
Fehler beim Lesen: java.io.WriteAbortedException: writing aborted;
java.io.NotSerializableException: NonSerialClass
25.3 Klassen-Variablen
Klassen-Variablen, d.h. static Attribute, werden nicht serialisiert und auch nicht deserialisiert, da
sie der Klasse und nicht einem einzelnen Objekt gehören.
25.4 Modifier „transient“
Für Attribute gibt es den Modifier transient, der dafür sorgt, dass dieses Attribut nicht serialisert
bzw. deserialisiert wird.
Hier wird im Beispiel aus Kapitel 25.1 die Klasse „SerialClass“ durch die fast identische Klasse
„SerialClassTransient“ ersetzt – einziger Unterschied ist das das „int“ Attribut den Modifier
„transient“ bekommen hat.
public class SerialClassTransient implements Serializable {
private String s;
private transient int i;
public SerialClassTransient(String s, int i) {
this.s = s;
this.i = i;
}
public void print() {
System.out.println("Objekt von SerialClassTransi. mit: " + s + " - " + i);
}
}
Ausgabe nach der Deserialisierung in Anlehnung an das Bsp aus Kapitel 25.1.
Schreiben
Objekt von SerialClassTransient mit: Objekt 1 - 42
Objekt von SerialClassTransient mit: Objekt 2 – 23
Lesen
Objekt von SerialClassTransient mit: Objekt 1 - 0
Objekt von SerialClassTransient mit: Objekt 2 - 0
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de
Objektorientiertes Programmieren in Java - V. 29
Seite 409 / 409
Das „int“ Attribut „i“ wird nicht serialisiert und beim Deserialisieren auf den Defaultwert des Typs
(hier bei int ‘0’) gesetzt.
25.5 Nicht serialisierbare Basis-Klassen
Hat die serialisierbare Klasse eine nicht serialisierbar Basis-Klasse, so muss diese einen
Standard-Konstruktor besitzen, der bei der Deserialisierung zur Konstruktion des BasisKlassen-Anteils aufgerufen wird.
Ist kein Standard-Konstruktor in der Basis-Klasse vorhanden, und wird versucht eine solche
Klasse zu deserialisieren, so wird die Exception „java.io.InvalidClassException“ mit dem Fehler
„NoSuchMethodError“ ausgelöst.
25.6 Aufgaben
25.6.1 Aufgabe „Scribble 7“
Erweitern sie das „Scribble 6“ um die Funktionalität das Bild zu speichern und zu laden.
Lösung siehe Kapitel 25.7.
25.7 Lag. zu Aufgabe „Scribble 7“ – Kap. 25.6.1
todo...
© Detlef Wilkening 1997-2016
http://www.wilkening-online.de

Similar documents