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