Java Schritt für Schritt
Arbeitsbuch mit eLearning-Kurs
0115
2024
978-3-8385-6177-6
978-3-8252-6177-1
UTB
Marcus Deininger
Thomas Kessel
10.36198/9783838561776
Die Programmiersprache Java von Anfang bis Ende durchzuarbeiten und zu erlernen scheint für viele Studierende eine große Hürde zu sein. Nicht mit diesem Arbeitsbuch. Es führt Schritt für Schritt und leicht verständlich in die Programmiersprache ein.
Das Buch umfasst 14 Kapitel: Einführung in Java; Variablen, Datentypen, Operatoren; Kontrollstrukturen; Felder / Arrays; Methoden; Sichtbarkeit / Gültigkeit; Objektorientierte Konzepte; Ausnahmen / Exceptions; Zeichenketten / Strings; Lineare Datenstrukturen; Datenströme / Streams; Datenbanken mit Java; Graphische Benutzeroberflächen mit Swing: Einführung; komplexere Oberflächen.
Zahlreiche Übersichten und Zusammenfassungen erleichtern das Verständnis. Zudem wird zum Buch ein eLearning-Kurs angeboten.
<?page no="0"?> Marcus Deininger Thomas Kessel Java Schritt für Schritt 3. Auflage <?page no="1"?> utb 4432 Eine Arbeitsgemeinschaft der Verlage Brill | Schöningh - Fink · Paderborn Brill | Vandenhoeck & Ruprecht · Göttingen - Böhlau · Wien · Köln Verlag Barbara Budrich · Opladen · Toronto facultas · Wien Haupt Verlag · Bern Verlag Julius Klinkhardt · Bad Heilbrunn Mohr Siebeck · Tübingen Narr Francke Attempto Verlag - expert verlag · Tübingen Psychiatrie Verlag · Köln Ernst Reinhardt Verlag · München transcript Verlag · Bielefeld Verlag Eugen Ulmer · Stuttgart UVK Verlag · München Waxmann · Münster · New York wbv Publikation · Bielefeld Wochenschau Verlag · Frankfurt am Main <?page no="2"?> Prof. Dr. Marcus Deininger ist Professor für Informatik an der Hochschule für Technik Stuttgart mit dem Schwerpunkt Software Engineering. Prof. Dr. Thomas Kessel lehrt Wirtschaftsinformatik an der Dualen Hochschule Baden- Württemberg in Stuttgart. <?page no="3"?> Marcus Deininger / Thomas Kessel Java Schritt für Schritt Arbeitsbuch mit eLearning-Kurs 3., überarbeitete Auflage UVK Verlag · München <?page no="4"?> Umschlagabbildung: © iStockphoto da-kuk Bibliografische Information der Deutschen Nationalbibliothek Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http: / / dnb.dnb.de abrufbar. 3., überarbeitete Auflage 2024 2., überarbeitete Auflage 2018 1. Auflage 2016 DOI: https: / / doi.org/ 10.36198/ 9783838561776 © UVK Verlag 2024 - ein Unternehmen der Narr Francke Attempto Verlag GmbH + Co. KG Dischingerweg 5, D-72070 Tübingen Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Das gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Alle Informationen in diesem Buch wurden mit großer Sorgfalt erstellt. Fehler können dennoch nicht völlig ausgeschlossen werden. Weder Verlag noch Autor: innen oder Herausgeber: innen übernehmen deshalb eine Gewährleistung für die Korrektheit des Inhaltes und haften nicht für fehlerhafte Angaben und deren Folgen. Diese Publikation enthält gegebenenfalls Links zu externen Inhalten Dritter, auf die weder Verlag noch Autor: innen oder Herausgeber: innen Einfluss haben. Für die Inhalte der verlinkten Seiten sind stets die jeweiligen Anbieter oder Betreibenden der Seiten verantwortlich. Internet: www.narr.de eMail: info@narr.de Einbandgestaltung: siegel konzeption ⅼ gestaltung CPI books GmbH, Leck utb-Nr. 4432 ISBN 978-3-8252-6177-1 (Print) ISBN 978-3-8385-6177-6 (ePDF) ISBN 978-3-8463-6177-1 (ePUB) <?page no="5"?> Vorwort Java gehört zu den populärsten Programmiersprachen weltweit und wird sowohl in Theorie als auch in der Praxis intensiv eingesetzt. In Firmen wird Java sowohl für große geschäftskritische unternehmensweite Informationssysteme als auch für die Programmierung von technischen Anwendungen verwendet. In den letzten Jahren wird Java bevorzugt als erste Programmiersprache in Schule, Ausbildung und Studium unterrichtet, da sie auf einfachen, verständlichen und überzeugenden Konzepten aufgebaut ist. In dem vorliegenden Arbeitsbuch werden Schritt für Schritt die grundlegenden Sprachelemente eingeführt, die Konzepte objektorientierter Programmierung in Java erläutert und die Nutzung relevanter Klassenbibliotheken (z.B. in Datenstrukturen oder bei der Ein- und Ausgabe) vorgestellt. Zuerst werden die verfügbaren einfachen Datentypen und die Deklaration von Variablen beschrieben. Anschließend werden die Kontrollstrukturen wie z.B. die Sequenz, die Auswahl und die verschiedenen Schleifen eingeführt und an Beispielen erläutert. Abgerundet wird dies durch eine kompakte Darstellung der Felder oder Arrays. Methoden erlauben es bequem Anweisungen zusammenfassen und diese bequem aufzurufen. Die Diskussion der Konzepte Sichtbarkeit und Gültigkeit, sowie des Geheimnisprinzips und der Zugriffsmodifier bereiten den Einstieg in die objektorientierte Programmierung vor. Die zentralen Begriffe Klassen, Objekte, abstrakte Klassen/ Methoden und Schnittstellen stehen im Mittelpunkt und führen zur Vererbung und zum Polymorphismus. Die Frage der Fehlerbehandlung wird später im Kapitel der Exceptions behandelt. Die Umsetzung der vorhergehenden Konzepte kann sehr schön in den betreffenden Kapiteln über Zeichenketten (Strings), lineare Datenstrukturen (Collections), den Datenströmen (Streams), Datenbanken und der graphischen Benutzeroberfläche anhand von zahlreichen Codebeispielen illustriert, erklärt und eingeübt werden. <?page no="6"?> Zu diesem Buch gibt es einen ergänzenden eLearning-Kurs aus 70 Fragen . Mithilfe des Kurses können Sie online überprüfen, inwieweit Sie die Themen des Buches verinnerlicht haben. Gleichzeitig festigt die Wiederholung in Quiz-Form den Lernstoff. Der eLearning-Kurs kann Ihnen dabei helfen, sich gezielt auf Prüfungssituationen vorzubereiten. Der eLearning-Kurs ist eng mit vorliegendem Buch verknüpft. Sie finden im Folgenden zu den wichtigen Kapiteln QR-Codes, die Sie direkt zum dazugehörigen Fragenkomplex bringen. Andersherum erhalten Sie innerhalb des eLearning-Kurses am Ende eines Fragendurchlaufs neben der Auswertung der Lernstandskontrolle auch konkrete Hinweise, wo Sie das Thema bei Bedarf genauer nachlesen bzw. vertiefen können. Diese enge Verzahnung von Buch und eLearning-Kurs soll Ihnen dabei helfen, unkompliziert zwischen den Medien zu wechseln, und unterstützt so einen gezielten Lernfortschritt. Die Autoren haben zu diesem Lehrbuch einen vorlesungbegleitenden Foliensatz zur Verfügung gestellt. Er ist unter https: / / files.narr.digital/ 9783825261771/ Zusatzmaterial.zip downloadbar. <?page no="7"?> Inhaltsübersicht Vorwort ................................................................................................................... 5 Schritt 1: Einführung in Java ................................................................................. 13 Schritt 2: Variablen, Datentypen, Operatoren........................................................ 23 Schritt 3: Kontrollstrukturen.................................................................................. 35 Schritt 4: Felder / Arrays ....................................................................................... 45 Schritt 5: Methoden .............................................................................................. 51 Schritt 6: Sichtbarkeit / Gültigkeit ......................................................................... 59 Schritt 7: Objektorientierte Konzepte .................................................................... 69 Schritt 8: Ausnahmen / Exceptions ....................................................................... 83 Schritt 9: Zeichenketten / Strings ......................................................................... 91 Schritt 10: Lineare Datenstrukturen .................................................................... 101 Schritt 11: Datenströme / Streams...................................................................... 125 Schritt 12: Datenbanken mit Java ....................................................................... 149 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung ................... 163 Schritt 14: Graphische Benutzeroberflächen mit Swing: komplexere Oberflächen .............................................................................................. 175 Stichwortverzeichnis............................................................................................ 197 <?page no="9"?> Inhaltsverzeichnis Vorwort ..................................................................................................................................................5 Schritt 1: Einführung in Java ...........................................................................13 Historie................................................................................................................................. 15 1.2 Begriffe ................................................................................................................................. 15 1.3 Besonderheiten von Java.............................................................................................. 16 1.4 Konventionen und Notationen .................................................................................. 20 1.5 Das erste Java-Programm ............................................................................................ 21 Schritt 2: Variablen, Datentypen, Operatoren .............................................. 23 2.1 Datentypen ......................................................................................................................... 25 2.2 Operatoren ......................................................................................................................... 31 Schritt 3: Kontrollstrukturen ...........................................................................35 3.1 Anweisungen ..................................................................................................................... 37 3.2 Sequenz ................................................................................................................................ 38 3.3 Auswahl ............................................................................................................................... 39 3.4 Schleifen / Wiederholungen....................................................................................... 41 Schritt 4: Felder / Arrays.................................................................................. 45 4.1 Werte in Arrays anordnen........................................................................................... 47 Schritt 5: Methoden.......................................................................................... 51 5.1 Anweisungen in Methoden zusammenfassen ....................................................53 Schritt 6: Sichtbarkeit / Gültigkeit.................................................................. 59 6.1 Java-Komponenten ......................................................................................................... 61 6.2 Das Geheimnisprinzip und Zugriffsmodifier.......................................................63 6.3 Qualifikation und Import ............................................................................................. 65 6.4 Gültige und sichtbare Elemente ................................................................................ 67 6.5 Innere Elemente............................................................................................................... 68 <?page no="10"?> 10 Inhaltsverzeichnis 7.1 Klassen und Objekte .......................................................................................................71 7.2 Erweiterung / Vererbung.............................................................................................74 7.3 Abstrakte Klassen und Methoden ............................................................................75 7.4 Schnittstellen / Interfaces............................................................................................76 7.5 Aufzählungstypen / Enumerations..........................................................................78 7.6 Polymorphismus ..............................................................................................................80 7.7 Best Practices der objektorientierten Programmierung ...............................81 Schritt 8: Ausnahmen / Exceptions ................................................................ 83 8.1 Ausnahmen auslösen und behandeln .....................................................................85 Schritt 9: Zeichenketten / Strings .................................................................. 91 9.1 Die Klassen String und StringBuilder .....................................................................93 9.2 Erzeugung von Strings...................................................................................................93 9.3 Vergleich von Strings .....................................................................................................94 9.4 Extraktion von Zeichen oder Teilstrings...............................................................95 9.5 Umwandeln von Strings................................................................................................96 9.6 Umwandlung von elementaren Datentypen in Strings...................................97 9.7 Verarbeitung von Zeichenketten mit der Klasse StringBuilder..................98 Schritt 10: Lineare Datenstrukturen............................................................ 101 10.1 Überblick .......................................................................................................................... 103 10.2 Typisierung von Collections.....................................................................................106 10.3 Das Interface Collection .............................................................................................107 10.4 Die Liste / List ................................................................................................................110 10.5 Die Menge / Set..............................................................................................................113 10.6 Die Schlange / Queue ..................................................................................................115 10.7 Der Keller / Stapel / Stack ........................................................................................119 10.8 Die Assoziationsliste / Map......................................................................................120 Schritt 11: Datenströme / Streams .............................................................. 125 11.1 Datenquellen und -senken........................................................................................127 11.2 Daten- und Stream-Arten ..........................................................................................127 Schritt 7: Objektorientierte Konzepte ............................................................69 <?page no="11"?> Inhaltsverzeichnis 11 11.3 Lesen und Schreiben von Strömen in Java ........................................................ 128 11.4 Lesen und Schreiben von Byte-Strömen............................................................ 130 11.5 Lesen und Schreiben von Textdateien ................................................................ 134 11.6 Lesen und Schreiben von Java-Daten .................................................................. 140 11.7 Objekte speichern und lesen ................................................................................... 144 Schritt 12: Datenbanken mit Java .................................................................149 12.1 Java und Datenbanken ............................................................................................... 151 12.2 Relationale Datenbanken und SQL ....................................................................... 152 12.3 Datenbankzugriff mit JDBC ...................................................................................... 158 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung......163 13.1 Benutzeroberflächen .................................................................................................. 165 13.2 Aufbau von Swing-Oberflächen.............................................................................. 166 13.3 Einfache Widgets .......................................................................................................... 168 13.4 Interaktion mit Widgets ............................................................................................ 171 Schritt 14: Graphische Benutzeroberflächen mit Swing: komplexere Oberflächen ..........................................................................................175 14.1 Komplexere Oberflächen .......................................................................................... 177 14.2 Übersicht über das Anwendungsbeispiel .......................................................... 179 14.3 MVC: Trennung von Oberfläche und Anwendung ......................................... 180 14.4 Weitere Widgets: Auswahllisten ........................................................................... 182 14.5 Layout-Manager ............................................................................................................ 187 14.6 Strukturierung der Oberfläche ............................................................................... 191 14.7 Weitere Widgets............................................................................................................ 193 Stichwortverzeichnis .......................................................................................197 <?page no="13"?> Schritt 1: Einführung in Java <?page no="14"?> Lernhinweise und Prüfungstipps Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1229 Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um die Programmiersprache Java, die wichtigsten Begriffe, die Besonderheiten von Java, die üblichen Notationen und Konventionen in Java, die notwendigen Werkzeuge zur Softwareentwicklung sowie um das erste „Hallo Welt“-Programm in Java. Welche Schlagwörter lerne ich kennen? Java Software Development Kit (SDK), Java Development Kit (JDK) Java Runtime Environment (JRE) Plattformunabhängigkeit Objektorientierung Einfachheit Netzwerkfähigkeit Sicherheit Offenheit Java Standard Edition (JSE) Java Enterprise Edition (JEE) Java Embedded Integrierte Entwicklungsumgebung (IDE) Eclipse, NetBeans Wofür benötige ich dieses Wissen? Dieses Wissen wird benötigt, um (1) die zentralen Java-Begriffe zu verstehen, (2) Java als Programmiersprache (besser) einordnen zu können, (3) die Ausrichtungen der einzelnen Java-Editionen zu erkennen, und (4) die Vorteile der erforderlichen Werkzeuge, wie z.B. die integrierte Entwicklungsumgebung, zu sehen. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gewünscht, die Besonderheiten von Java zu erklären und die einzelnen Java-Begriffe oder -Editionen zu erläutern. <?page no="15"?> 1.2 Begriffe 15 Historie Java wurde 1995 von einem Team von Entwicklern um James Gosling und Billy Joy im Auftrag der Firma SUN Microsystems entworfen und umgesetzt. Im Jahr 2010 wurde SUN Microsystems von Oracle übernommen. Im Zug dieser Transaktion wurden auch alle intellektuellen Rechte an Java, z.B. die Patente, Lizenzen und Urheberrechte, an Oracle übertragen. Die Entwicklung von Java wurde stark durch andere Programmiersprachen, wie z.B. C++ und Smalltalk, beeinflusst. Java selbst hat wiederum zu einer Vielzahl von neuen Programmiersprachen, wie z.B. Groovy, AspectJ, Clojure oder Scala, hervorgebracht, die die zugrundeliegende Architektur, z.B. die virtuelle Java-Maschine, übernehmen oder einzelne Aspekte vertiefen. Die Weiterentwicklung von Java erfolgt im Rahmen des Java Community Processes (JCP), an dem sich viele renommierte IT-Hersteller, IT-Dienstleister und Softwareunternehmen beteiligen. Trotz der Einbindung anderer Unternehmen bleibt Oracle jedoch die bestimmende Kraft bei der Weiterentwicklung von Java. Vorschläge zu einer neuen Version einer bereits existierenden oder einer innovativen, erstmaligen Technologie werden in Form Java Specification Requests (JSR) definiert, der den jeweils aktuellen Stand der Diskussion definiert (einsehbar auf www.jcp.org). Die daraus resultierenden technischen Spezifikationen der verschiedenen Java-Technologien können auch auf den einschlägigen Webseiten der Firma Oracle eingesehen werden. 1.2 Begriffe Das Java Software Development Kit (Java SDK oder auch manchmal als JDK bezeichnet) ist die technische Voraussetzung, um in Java programmieren zu können. Es ist über die Webseiten von Oracle für verschiedene Hardware-Plattformen (z.B. Windows, Linux, MacOS) kostenlos verfügbar, es existieren darüber hinaus aber auch noch weitere Implementierungen von anderen Herstellern. Das Java SDK umfasst zum einen den Compiler, der den Java-Quellcode in einen plattformübergreifenden Zwischencode (Bytecode) übersetzt, und zum anderen die Java- Laufzeitumgebung ( Java Runtime Environment , JRE ) inklusive der zahlreichen Klassenbibliotheken und dem Interpreter, der den Zwischencode in die jeweilige Zielplattform überführt. Java Development Kit (JDK) wird oft synonym zu Java SDK verwendet. <?page no="16"?> 16 Schritt 1: Einführung in Java Aus diesem Ansatz ergeben sich zwei wesentliche Vorteile: erstens, ein Anwender kann Zwischencode problemlos von einer Hardware- Plattform auf eine andere übertragen, wo er dort dann interpretiert wird, und zweitens, ein Hersteller muss für eine neue Hardware-Plattform nur eine entsprechende Java-Laufzeitumgebung bereitstellen, was weit weniger aufwändig ist, als das gesamte Java SDK zu portieren. Der Quelltext eines Java-Programms könnte prinzipiell zwar mit Hilfe eines Texteditors erstellen werden, aber dies wäre nur wenig sinnvoll, da weitere unterstützende Funktionen, wie z.B. die Ausführung oder das Debugging des Java- Codes, über die Kommandozeile aufgerufen werden müssten. Aus diesem Grund gibt es integrierte Entwicklungsumgebungen („Integrated Development Environments“, IDE), wie z.B. Eclipse oder NetBeans, die all diese Funktionalitäten in einer gemeinsamen Benutzeroberfläche bündeln und als kostenlose Versionen verfügbar sind. 1.3 Besonderheiten von Java Die wesentlichen Differenzierungsfaktoren von Java zum Zeitpunkt seines Entwurfs waren: Plattformunabhängigkeit Objektorientierung Einfachheit Netzwerkfähigkeit Sicherheit Offenheit/ Open Source Plattformunabhängigkeit bedeutet, dass der Java-Quellcode in den Zwischencode übersetzt wird und dann von jeder Java-Laufzeitumgebung, unabhängig von der Betriebssystem-Plattform, ausgeführt werden kann. Die Java-Laufzeitumgebung ist (immer) eine Voraussetzung für die Ausführung von Java-Programmen und stellt eine Abstraktion von der konkreten Hardware dar. Insbesondere die plattformunabhängige Verfügbarkeit einer grafischen Benutzeroberfläche war ein wichtiges Alleinstellungsmerkmal bei der Markteinführung von Java. Grafische Benutzeroberflächen mit Java werden in Schritt 13 eingeführt. <?page no="17"?> 1.3 Besonderheiten von Java 17 Objektorientierung basiert darauf, dass Klassen definiert werden, von denen Instanzen oder Objekte erzeugt werden. Das Programm besteht dann in dem Versenden von Nachrichten zwischen den einzelnen Objekten (bzw. dem Aufruf von Methoden laut der Java-Terminologie). Das Prinzip der Objektorientierung war zum damaligen Zeitpunkt bereits in einigen Programmiersprachen, z.B. Smalltalk, C++ oder Objective-C umgesetzt, aber erst durch Java erlangte die konsequente und durchgängige objektorientierte Programmierung die nötige Verbreitung und Unterstützung. Typische Mechanismen von objektorientierten Programmiersprachen sind Klassen, die Vererbung und der Polymorphismus. In Java wird das objektorientierte Paradigma ein wenig abgemildert, wenn es um die einfachen Datentypen geht, die nicht als Klassen implementiert sind. Die Konzepte der objektorientierten Programmierung werden in Schritt 7 betrachtet. Unter Einfachheit versteht man bei einer Programmiersprache, dass die zugrundeliegenden Konstrukte übersichtlich, verständlich und leicht zu erlernen sind, so dass bereits mit wenigen Schlüsselwörtern und -konzepten eine große Ausdrucksfähigkeit erreicht wird. Die Einfachheit von Java kommt in zwei Aspekten zum Tragen: zum einen orientiert sich die Syntax von Java an C und zum zweiten besteht Java aus einem „Sprachkern“ und Klassenbibliotheken. Das Besondere ist hierbei, dass der Sprachkern nur wenige, grundlegende Sprachkonstrukte und Schlüsselwörter enthält, so dass er leicht erlernbar und verständlich ist. Die Auslagerung vieler Funktionalitäten in die Klassenbibliotheken erlaubt eine große Flexibilität und kommt auch dadurch zum Ausdruck, dass es drei Versionen (Standard, Enterprise und Embedded) gibt, die sich in erster Linie aufgrund der Klassenbibliotheken unterscheiden. Der Sprachkern von Java wird in den Schritten 2 bis 4 vorgestellt. Eine Programmiersprache gilt als netzwerkfähig , wenn die grundlegenden Funktionalitäten zum Aufbau und zur Beendigung einer Netzwerkverbindung integriert sind, z.B. über eine entsprechende Standard-Klassenbibliothek. Die Netzwerkfähigkeit war von Anfang an in Java integriert und motivierte deshalb den Einsatz von Java in verteilten Anwendungen und bei der sich damals erst entwickelnden Web-Programmierung. Aufbauend auf diesen grundlegen- <?page no="18"?> 18 Schritt 1: Einführung in Java den Features entwickelte sich sehr schnell die Java Enterprise Edition (JEE), die insbesondere für verteilte Systeme und große, geschäftskritische Anwendungen in Unternehmen ausgelegt ist. Aufgrund der einfachen Erweiterbarkeit von Java kamen in den vergangenen Jahren immer mehr Klassen und Funktionen zur Netzwerkprogrammierung hinzu, die einfach in die bisherigen Klassenbibliotheken integriert oder in separate ausgelagert werden konnten. Die Sicherheit einer Programmiersprache hängt von unterschiedlichen Aspekten ab: von einer zuverlässigen Speicherverwaltung, über die Typprüfung aller Variablen bis hin zu einer konsequenten Fehlerbehandlung, der Validierung des erzeugten Codes oder die eventuellen Einschränkungen bei der Ausführung der Java-Anwendung. Zahlreiche Sicherheitsmechanismen sind von vorneherein in Java eingebunden. Das automatische Speichermanagement erlaubt es, nicht mehr benötigte Speicherbereiche nach Gebrauch wieder frei zu geben und so „memory leaks“ zu vermeiden. Memory leaks sind Speicherbereiche, die für die Nutzung eines Programms reserviert sind, die aber anschließend nicht mehr frei gegeben werden. Alle Variablen müssen deklariert, typisiert und initialisiert werden, bevor sie verwendet werden dürfen. Dies reduziert deutlich die Gefahr von falschen oder unvollständigen Werten. Die integrierte Fehlerbehandlung dank der Exceptions erlaubt es, auch auf Störungen des Programmablaufs zur Laufzeit entsprechend zu reagieren und diese sogar ggf. zu beheben. Die Ausnahmebehandlung wird in Schritt 11 diskutiert. Die Sicherheit beschränkt sich dabei nicht nur auf die Programmierung, sondern sie kann sich insbesondere auf die Ausführung des Java-Codes erstrecken. Zur Abwehr von Schadsoftware kann überprüft werden, ob der Code selbst verändert wurde, oder es können sogar Sicherheitsregeln definiert werden, die die Ausführungs- und Zugriffsrechte der Anwendung einschränken, um so zu verhindern, dass ein potentieller Angreifer weitergehende Rechte erwirbt. Als Offenheit wird die Veröffentlichung aller relevanten Schnittstellen, Formate und Technologien bezeichnet. Unter Open Source versteht man in der Regel, dass der Quellcode öffentlich verfügbar gemacht wird, unter einer entsprechenden offenen Lizenz steht, also z.B. verändert, kopiert und weitergegeben werden kann, und sich eine Community, eventuell in Partnerschaft mit kommerziellen Firmen, um die Weiterentwicklung kümmert. <?page no="19"?> 1.3 Besonderheiten von Java 19 Ein Großteil der Java-Technologien wurden unter Open Source-Lizenzen gestellt und steht somit den Entwicklern zur Verfügung. Außerdem wurden die betreffenden Spezifikationen und Schnittstellen ebenfalls dokumentiert, so dass diese von Dritten implementiert oder problemlos genutzt werden können. Java ist also nicht primär ein Produkt, sondern die technische Spezifikation einer Technologie, die dann von Herstellern umgesetzt werden kann. Neben den obigen technologischen Gründen gab es aber auch zahlreiche wirtschaftliche Gründe für den Erfolg Javas. Die Plattformunabhängigkeit hat z.B. zur Konsequenz, dass sich die Kosten für die Entwicklung, den Test und die Wartung von Java-Programmen (im Vergleich zu den herkömmlichen Programmiersprachen) erheblich reduzierten. Software-Anbieter müssen so nämlich nur noch eine Version programmieren, statt für jede Plattform jeweils eine unterschiedliche Version zu entwickeln. Im Lauf der Zeit entstand um Java ein Ökosystem von Werkzeuganbietern, Dienstleistern, Entwicklern usw., die sich so gegenseitig unterstützten und gemeinsam ein enormes Know-how aufbauen konnten. Insbesondere dank der Vielfalt von Entwicklungswerkzeugen und -umgebungen wurde die Weiterentwicklung von Java deutlich beschleunigt. Es gibt drei Editionen von Java, die unterschiedliche Ziele verfolgen und sich im Wesentlichen durch die Klassenbibliotheken unterscheiden: Java Standard Edition (JSE): bietet grundlegende Funktionalitäten (inkl. grafischer Benutzeroberfläche, Datenbank, Netzwerk, Dateisystem usw.) an. Java Enterprise Edition (JEE): ausgelegt für die Entwicklung unternehmenskritischer Anwendungen, es basiert auf der JSE und erweitert diese um weitere Technologien z.B. für verteilte Komponenten, Web-/ Internet-Programmierung. Java Embedded: angepasst für den Einsatz in eingebetteten Systemen. Außerdem gibt es noch diverse Anpassungen von Java für den Einsatz in mobilen Systemen (z.B. in Android). Entsprechend existieren auch verschiedene Kategorien von Java- Anwendungen, d.h. für die Editionen gibt es auch einen zugehörigen Anwendungstyp: Klassische Desktop-Anwendung: sie ist in der Regel für den Stand-alone-Betrieb auf einem PC konzipiert und bietet dem Benutzer eine grafische Benutzeroberfläche an. Unternehmensanwendung: mittels eines Clients (z.B. ein Browser) wird über das Netzwerk auf einen Applikationsserver zugegriffen, auf dem verschiedene Technologien, wie z.B. Java Servlets, Java Server Pages, Enterprise Java Beans, laufen. Eine Unternehmensanwendung zeichnet sich weiterhin dadurch aus, <?page no="20"?> 20 Schritt 1: Einführung in Java dass sie eine Datenbank und mehrere verteilte Komponenten besitzt, sowie für eine größere Zahl von Benutzern ausgelegt ist. Eingebettete Anwendung: sie läuft innerhalb des umgebenden technischen Systems ab und ist in der Regel für die Außenwelt bzw. für den Benutzer nicht oder nur teilweise sichtbar Mobile App: Web-Anwendungen können für die mobile Plattform angepasst und als hybride Applikation ausgeliefert werden oder sie werden direkt in Java, für das passende mobile Gerät geschrieben. 1.4 Konventionen und Notationen Im Laufe der Zeit haben sich gewisse Notationen und Konventionen für die Java- Programmierung entwickelt, die auch hier angewendet werden. Im Folgenden geht es um drei Aspekte: Die Darstellung von Java-Code im vorliegenden Buch: - Programmcode wird in der Schriftart Consolas dargestellt. - Schlü sselwö rter werden fett dargestellt. - Platzhalter fü r konkrete Namen, Schlü sselwö rter oder Codeteile werden kursiv dargestellt. - Mehrfache Wiederholungen werden durch einen Stern * am Ende dargestellt. - Optionen werden durch eckige Klammern [ ] dargestellt. - Alternativen werden durch einen Strich | abgetrennt. - Analoge Fortsetzungen oder Auslassungen werden durch … dargestellt. Die üblichen Konventionen für die Benennung von Variablen, Methoden und Klassen in Java: - Die Bezeichnung einer Klasse beginnt mit einem Großbuchstaben. - Die Namen einer Variablen und einer Methode beginnen mit einem Kleinbuchstaben. - Die Bezeichner dü rfen nicht mit einer Ziffer starten und dü rfen keine Sonder- und Leerzeichen enthalten. - Der Name einer Konstante wird vollstä ndig in Großbuchstaben geschrieben. - Bei zusammengesetzten Benennungen beginnt jedes Wort mit einem Großbuchstaben. Weitergehende Standards zur Formatierung von Java Code und Systematik bei der Benennung von Bezeichnern: <?page no="21"?> 1.5 Das erste Java-Programm 21 - Hierzu gibt es eine Vielzahl von Konzepten. Die daraus resultierenden Richtlinien sind in Form von Coding Style Guides oder Lehrbü chern dokumentiert und kö nnen mittlerweile teilweise durch spezifische Werkzeuge automatisch ü berprü ft werden. 1.5 Das erste Java-Programm In diesem Abschnitt geht es darum, die ersten Schritte in Java zu unternehmen. Dabei sollten zunächst die folgenden Hinweise beachtet werden: Das Erlernen einer Programmiersprache erfordert auch praktische Übungen und Aufgaben, die gelöst werden müssen, vergleichbar dem Vorgehen beim Lernen einer Fremdsprache, einer Sportart oder eines Musikinstruments. Für die Ausführung des folgenden Programms sind das Java SDK und eine integrierte Entwicklungsumgebung - z.B. Eclipse oder NetBeans - eine Voraussetzung, deshalb sollte man sich zuerst mit der Nutzung der Entwicklungsumgebung vertraut machen. Das Vorgehen zur Erstellung des Java-Programms bei Nutzung einer Entwicklungsumgebung 1 sieht prinzipiell wie folgt aus: [1] Anlegen eines Java-Projekts. [2] Erzeugung einer Java-Klassendatei → Eintragung des Klassennamens und die Generierung der main-Methode. [3] Editieren der Java-Klasse. [4] UÜ bersetzen der Klasse. [5] Ausfü hren der Klasse. Das zentrale Element von Java ist die Klasse. Eine Klasse hat die Form public class Klassenname { Anweisungen } und muss in einer Datei gleichen Namens mit der Endung .java abgelegt sein. Dies bedeutet zum Beispiel, dass die Klasse HalloWelt in der gleichnamigen Datei HalloWelt.java gespeichert sein muss. Die Methode, mit der ein Java-Programm gestartet wird, ist die Methode namens main , die immer die Form public static void main(String[] args) haben muss. 1 Die Erstellung des Java-Programms ist prinzipiell auch mit einem Texteditor möglich. In diesem Fall müssen alle Dateien des Projekts manuell erzeugt werden. <?page no="22"?> 22 Schritt 1: Einführung in Java Ein einfaches Java-Programm, das den Text „Hallo Welt“ am Bildschirm ausgibt, hat die folgende Form: Listing 1: HalloWelt -Programm [1] Die Klasse HalloWelt wird definiert. [2] Die Methode main wird festgelegt, eventuelle Eingaben werden als Parameter args in ein Array ü bernommen. [3] Der Text „Hallo Welt“ wird auf der Standardausgabe, d.h. dem Bildschirm ausgegeben. Die folgenden Schritte werden zur Ausführung des Programms durchgeführt: Abb. 1: Übersetzung und Ausführung des Quellcodes von HalloWelt. java Der Quellcode liegt in der Datei HalloWelt.java vor. Er wird durch den Befehl javac HalloWelt.java (der den Compiler des Java SDKs aufruft) in den Bytecode HalloWelt.class übersetzt. Diese ist dann auf allen Plattformen lauffähig ist, auf denen eine JRE verfügbar ist. Anschließend wird der Bytecode interpretiert, ausgeführt und die Ausgabe „Hallo Welt“ erscheint als Resultat auf dem Bildschirm. In einer IDE werden diese Schritte innerhalb der Entwicklungsumgebung durchgeführt. public class HalloWelt { public static void main(String[] args) { System.out.println("Hallo Welt"); } } 1 2 3 HalloWelt.java HalloWelt.class Quellcode Bytecode Ausgabe: Hallo Welt javac HalloWelt.java java HalloWelt Compiler: Interpreter: <?page no="23"?> Schritt 2: Variablen, Datentypen, Operatoren <?page no="24"?> 24 Schritt 2: Variablen, Datentypen, Operatoren Lernhinweise und Prüfungstipps Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1230 Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um Variablen, Datentypen und Operatoren und wie diese deklariert und verwendet werden. Welche Schlagwörter lerne ich kennen? Variable Datentyp Operator Deklaration Initialisierung Wertzuweisung Konvertierung von Datentypen Wofür benötige ich dieses Wissen? Variablen speichern Werte zur weiteren Verarbeitung. Datentypen definieren die Wertebereiche einer Variablen und die möglichen Operationen, so dass die Gültigkeit der Werte geprüft werden kann. Operatoren erlauben beispielsweise die Veränderung einer Variablen oder die Berechnung eines neuen Wertes durch die Verknüpfung von Variablen oder Werten. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig nach (1) der Deklaration und Initialisierung von Variablen, (2) den verschiedenen Datentypen und Operatoren und (3) der Konvertierung von Datentypen gefragt. Im Rahmen von Aufgaben müssen die Variablen deklariert und initialisiert sowie Operatoren benutzt werden. <?page no="25"?> 25 2.1 Datentypen 2.1 Datentypen Datentypen stellen die grundlegenden (Daten-)Bausteine einer Programmiersprache dar, denn es können nur die Werte und Daten gespeichert und verarbeitet werden, für die es auch entsprechende Datentypen gibt. Ein Datentyp beschreibt eine Reihe von ähnlichen Werten und die darauf agierenden Operationen, z.B. die ganzen Zahlen von -128 bis 127 ( byte ), alle Zeichen des Zeichensatzes ( char ) oder die booleschen Werte true und false ( boolean ), sowie die entsprechenden Operationen (arithmetische für die Zahlen und logische für die booleschen Werte). Ein Datentyp definiert die Art der Daten, die verfügbaren Operationen und den möglichen Wertebereich der Variable; dies bedeutet, dass die Variable nur Werte innerhalb dieses Spektrums annehmen kann. Man unterscheidet in Java zwei Kategorien von Datentypen: einfache Datentypen und Referenztypen. Einfache Datentypen , wie byte, short, int, long, float, double, char oder boolean , zeichnen sich dadurch aus, dass sie direkt den Wert enthalten. Referenztypen , die z.B. durch ein Array oder eine Klasse definiert werden, enthalten den Verweis auf eine Speicherstelle, wo eine komplexe Datenstruktur vorliegt, die sich aus mehreren einfachen Datentypen oder Referenztypen zusammensetzt. 2 Einfache Datentypen haben einen fest vorgegebenen Speicherplatz. Sie sind in Java vordefiniert und können auch nicht ergänzt werden. Es gibt die folgenden einfachen Datentypen: 2 Im Gegensatz zu C oder C++ erlaubt Java keinen direkten Zugriff auf Speicheradressen - nur Zuweisungen und Test auf Identität sind möglich. Damit umgeht Java eine große Fehlerquelle. <?page no="26"?> 26 Schritt 2: Variablen, Datentypen, Operatoren Datentyp Datenart Wertebereiche Operationen byte ganze Zahlen -128 … +127 Mathematische Operationen: +, -, *, / , % (Divisionsrest), … short -32768 … +32767 int -2 31 … +2 31 -1 long -2 63 … +2 63 -1 float Fließkommazahlen -3.4*10 38 … +3.4*10 38 Mathematische Operationen: +, -, *, / , … double -1.7*10 308 … +1.7*10 308 char Zeichen Alle Unicode-Zeichen Mathematische Operationen, da Buchstaben intern als Zahlen codiert sind boolean logische Werte (wahr, falsch) true , false Logische Operationen && (und), || (oder), ! (nicht) Tab. 1: Einfache Datentypen in Java Die Datentypen byte bis long beschreiben ganze Zahlen, float und double Gleitkommazahlen, während char ein Zeichen und boolean die (booleschen) Werte true und false beschreibt. Abb. 2: Übersicht der Datentypen Die einfachen Datentypen sind durch die Java-Sprachdefinition vorgegeben und können nicht erweitert werden. Die Referenztypen sind durch die Klassenbibliotheken teilweise vorgegeben, ein Programmierer kann aber auch eigene Refe- Zahlen byte short int long float double Datentypen einfache Datentypen Referenztypen Array Klasse, z.B. String Zeichen char Wahrheitswerte boolean <?page no="27"?> 2.1 Datentypen 27 renztypen über die Einführung von Klassen definieren und verwenden. Der Datentyp für Zeichenketten heißt z.B. String und ist ein Referenztyp, d.h. er bezeichnet eine existierende Java-Klasse. Eine Variable wird deklariert durch einen Datentyp und initialisiert durch eine Wertzuweisung. Eine Wertzuweisung erfolgt durch den Operator = , wobei die Variable links und der Wert rechts davon steht. Variablen benötigen für eine gültige Deklaration einen Datentyp, erst dann kann anschließend die Wertzuweisung erfolgen. Die erste Wertzuweisung heißt Initialisierung und ist bei lokalen Variablen vor der Verwendung zwingend erforderlich. Die Kombination von Datentyp und Variable ist nicht nur bei lokalen Variablen, sondern auch bei der Definition von Parametern einer Methode und der Deklaration von Klassen- oder Instanzvariablen anzutreffen. Eine Variable kann mit einem Behälter verglichen werden, in dem genau ein Wert eines bestimmten Datentyps abgelegt wird. Variablen erlauben es, Daten im Hauptspeicher eines Programms abzulegen, zu lesen oder zu verändern. Dank des Namens kann auf den Wert direkt zugegriffen werden. Die Deklaration einer Variablen erfolgt in der Form: Datentyp Variablenname; Die Wertzuweisung geschieht durch den Zuweisungsoperator = gefolgt von dem entsprechenden Wert, also wie folgt: Variablenname = Wert; Im folgenden Beispiel beginnen alle Variablen mit Kleinbuchstaben, dies entspricht der allgemeinen Java-Konvention. Listing 2: Berechnung der Summe aus zweien Variablen [1] Die Variable a wird deklariert und mit dem Wert 5 initialisiert. [2] Die Variable b wird deklariert und mit dem Wert 10 initialisiert. [3] Anschließend wird die Summe vom Wert der beiden Variablen berechnet. Die Berechnungsvorschrift ist aber unabhä ngig vom Wert der Variablen formuliert. int a = 5; int b = 10; int summe = a + b; 1 2 3 <?page no="28"?> 28 Schritt 2: Variablen, Datentypen, Operatoren Im Listing oben sind a und b lokale Variablen, die in einem Anweisungsblock definiert werden. Solche Variablen müssen immer erst initalisiert werden, bevor sie verwendet werden. Diese Regel wurde deshalb eingeführt, um sicherzustellen, dass eine Variable immer einen definierten Wert hat. Eine Besonderheit sind symbolische Konstanten, die sich syntaktisch von Variablen dadurch unterscheiden, dass das Schlüsselwort final vorangestellt wird und der Wert nach der ersten Zuweisung (während der Laufzeit des Programms) nicht mehr geändert werden kann. Konvention ist, diese Konstanten vollständig groß zu schreiben. Bei der Nutzung von Gleitkommazahlen muss aber beachtet werden, dass Java automatisch als Standardvermutung annimmt, dass alle Gleitkommazahlen vom Datentyp double sind, außer wenn sie explizit als float -Datentyp gekennzeichnet sind. Dies geschieht durch ein nachgestelltes f oder F an die entsprechende Zahl, z.B. 3.3f oder 9.9F sind (Gleitkomma-)Zahlen vom Typ float . Analog muss eine ganze Zahl durch ein nachgestelltes l oder L als zum Datentyp long zugehörig gekennzeichnet werden. Ansonsten wird die ganze Zahl als int bewertet. Zeichen ( char ) werden durch das Zeichen selbst, umschlossen von einfachen Hochkommas ( ' ), dargestellt. Beispiele für die Deklaration und die Initialisierung von Variablen finden sich im anschließenden Listing: Listing 3: Deklaration und Initialisierung von Variablen [1] Die Variable i wird fü r den Datentyp int , d.h. als ganze Zahl, deklariert. [2] Der Variablen i wird der Wert 10 zugewiesen. [3] Die Variable x wird als double deklariert, d.h. als Gleitkommazahl, und mit dem Wert 3.14 initialisiert. [4] Die Variable c wird als char , d.h. als Zeichen, deklariert. [5] Der booleschen Variable b wird direkt der Wert true zugewiesen. [6] Die Konstante PI wird mit dem Wert 3.14 initialisiert. int i; i = 10; double x = 3.14; char c = 'A'; boolean b = true; final double PI = 3.14; 2 3 4 1 5 6 <?page no="29"?> 2.1 Datentypen 29 In den bisherigen Fällen wurden den Variablen nur korrekte Werte zugewiesen, d.h. die Werte sind alle mit den Datentypen der Variablen kompatibel gewesen. Die Typisierung der Daten wurde aber eingeführt, um falsche Zuweisungen von Werten zu erkennen und konsequenterweise zu verhindern. Bei Wertzuweisungen können zwei Aspekte überprüft werden: (1) ist der Wert vom selben Datentyp wie die Variable und (2) liegt der Wert innerhalb des erlaubten Bereichs des Datentyps. Zum Beispiel kann einer Variablen vom Datentyp byte kein boolescher Wert (z.B. true oder false ) oder eine Gleitkommazahl zugewiesen werden, denn die Variable erlaubt nur ganze Zahlen. Mögliche problematische Fälle bei Wertzuweisungen (die vom Compiler zurückgewiesen würden) finden sich im folgenden Code: Listing 4: Fehlerhafte Wertzuweisungen bei Variablen [1] Einer Variablen vom Datentyp byte , d.h. von der Datenart der ganzen Zahlen, darf keiner boolescher Wert, d.h. true , zugewiesen werden. [2] Der Wertebereich einer Variablen vom Datentyp byte ist von -128 bis +127 beschrä nkt, deshalb ist die Zuweisung der Zahl 300 nicht mö glich. [3] Die Gleitkommazahl 3.14 mü sste als float gekennzeichnet werden, indem ein f oder F nachgestellt wird. [4] Einer Variablen vom Datentyp char darf keine Zahl zugewiesen werden. [5] Der Wert einer Konstante darf zur Laufzeit nicht verä ndert werden. Die Datentypen für Zahlen können, wie in Abbildung 3 dargestellt, in einer aufsteigenden Reihenfolge, vom kleinsten zum größten Wertebereich, angeordnet werden: Abb. 3: Typvergrößerung bei einfachen Datentypen byte short long int float double char byte d = true; byte d = 300; float y = 3.14; char c = 99; final double PI = 3.14; PI = 2.2; 2 3 4 1 5 <?page no="30"?> 30 Schritt 2: Variablen, Datentypen, Operatoren Der Datentyp char erscheint a priori nicht zu in dieses Schema zu passen, aber da er direkt in int umgewandelt werden kann, kann er als „Seiteneinsteiger“ platziert werden. Bei der Konvertierung von Datentypen können prinzipiell zwei Fälle unterschieden werden: Typvergrößerung: von einem kleinen zu einem großen Datentyp Typverkleinerung: von einem großen zu einem kleinen Datentyp Konvertierungen (1) von oder zu boolean hin und (2) zwischen einfachen Datentypen und Referenztypen sind nicht möglich. Der erste Fall, die Typvergrößerung, kann automatisch von der JVM vorgenommen werden, ohne dass der Programmierer eingreifen muss, denn hier geht keine Informationen verloren und jeder Wert kann problemlos von dem größeren Datentyp dargestellt und verarbeitet werden. Listing 5: Typvergrößerung bei Variablen [1], [2] Es erfolgt eine Vergrö ßerung vom Datentyp byte zu int . [3], [4] Hier wird von float auf double erweitert. [5], [6] Die Vergrö ßerung findet von char auf int statt. Der zweite Fall, die Typverkleinerung, kann zu einem Informationsverlust führen, wenn zum Beispiel eine Gleitkommazahl in eine ganze Zahl überführt wird oder eine Zahl außerhalb des darstellbaren Wertbereichs des kleineren Datentyps liegt. Aus diesem Grund wird eine explizite Bestätigung des Programmierers durch einen sogenannten „cast“ eingefordert. Die Syntax sieht wie folgt aus: Variable_1 = (Datentyp) Variable_2 byte d = 111; int i = d ; float y = 3.14F; double z = y ; char c = 'A'; int j = c; 2 3 4 1 5 6 <?page no="31"?> 2.2 Operatoren 31 Dies bedeutet, dass der Wert von Variable_2 explizit in den Datentyp von Variable _ 1 konvertiert wird. Listing 6: Typverkleinerung bei einer Variablen [1], [2] Der Wert 3.14 wird von double in int überführt, so dass der Wert 3.14 seine Nachkommastellen (.14) verliert und j nur noch den Wert 3 enthält. 2.2 Operatoren Die Operatoren lassen sich in die folgenden wesentlichen Kategorien einteilen: Inkrement/ Dekrement ( ++ , -- ) arithmetische Operatoren ( + , - , * , / , % ) Zuweisungsoperatoren ( = , += , -= , *= , / = , %= ) logische Operatoren ( &, && , | , || , ! ) Vergleiche ( == , ! = , <= , >= ) Inkrement/ Dekrement Bezeichnung Symbol Beispiel Erläuterung Präinkrement ++ ++i i = i + 1, i wird zuerst erhöht und danach wird i evaluiert Postinkrement ++ i++ i = i + 1, i wird evaluiert und erst danach wird i erhöht Prädekrement -- --i i = i - 1, i wird zuerst reduziert und danach wird i evaluiert Postdekrement -i-i = i - 1, i wird evaluiert und danach wird i reduziert Tab. 2: Inkrement/ Dekrement in Java Falls ++i oder i++ in einer Schleife oder allein stehend auftauchen, dann führt die Verwendung des Prä- oder Postinkrements zu keinem Unterschied. Der Unterschied zwischen Prä- und Postinkrement ist subtil und wird durch die folgenden Anweisungen deutlich. double z = 3.14; int j = (int) z; 2 1 <?page no="32"?> 32 Schritt 2: Variablen, Datentypen, Operatoren Listing 7: Unterschiedliches Verhalten bei Prä- und Postinkrement [1] s ist gleich 3, da sich s als Summe von a und b++ ergibt, wobei b mit dem Wert 2 in die Summe eingeht und erst danach (auf 3) erhö ht wird. [2] Hier ist s gleich 4, da c zuerst (auf 3) erhö ht wird und dieser Wert auch in der Summe berü cksichtig wird. Arithmetische Operatoren Bezeichnung Symbol Beispiel Erläuterung Addition + a+b Summe von a und b Subtraktion a-b Differenz von a und b Multiplikation * a*b Produkt von a und b Division / a/ b Quotient von a und b Restwert % a%b a modulo b Tab. 3: Arithmetische Operatoren in Java Der Restwertoperator berechnet den „ganzzahligen Rest”, der bei der Modulo- Berechnung anfällt. Z.B.: der Ausdruck 9 % 2 ergibt 1, da 9 : 2 = 4 Rest 1 ist. Zuweisungsoperatoren Bezeichnung Symbol Beispiel Erläuterung (einfache) Zuweisung 3 = a=5 der Variablen a wird der Wert 5 zugewiesen Additionszuweisung += a+=b entspricht a = a + b Subtraktionszuweisung -= a-=b entspricht a = a b Multiplikationszuweisung *= a*=b entspricht a = a * b Divisionszuweisung / = a/ =b entspricht a = a / b Modulozuweisung %= a%=b entspricht a = a % b Tab. 4: Zuweisungsoperatoren in Java 3 Nur für C-Programmierer ist es natürlich, dass der Zuweisungsoperator durch ein einfaches Gleichheitszeichen ( = ) und der Gleichheitstest durch ein doppeltes Gleichheitszeichen ( == ) umgesetzt sind, deshalb führt dies leicht zu Verwechslungen. int a, b, c, s; a = 1; b = 2; c = 2; s = a + (b++); s = a + (++c); 2 1 <?page no="33"?> 2.2 Operatoren 33 Die oben aufgeführten Additions-, Subtraktions-, Multiplikations- und Divisionszuweisungen kombinieren jeweils den entsprechenden arithmetischen Operator mit dem einfachen Zuweisungsoperator. Logische Operatoren Bezeichnung Symbol Beispiel Erläuterung logisches UND & a & b wahr, falls a und b wahr sind - es werden alle Teile des Ausdrucks ausgewertet. „Kurzschluss“- UND && a && b wahr, falls a und b wahr sind - Evaluierung bricht ab, wenn a falsch ist. logisches ODER | a | b wahr, falls a oder b wahr sind - es werden alle Teile des Ausdrucks ausgewertet. „Kurzschluss“- ODER || a || b wahr, falls a oder b wahr sind - Evaluierung bricht ab, wenn a wahr ist. Negation ! ! a wahr, falls a falsch ist Tab. 5: Logische Operatoren in Java Die Operatoren für die logischen Funktion UND, ODER oder NICHT sind klar (und selbsterklärend). Die Kurzschlussoperatoren sind effizient, denn sie nutzen die Reihenfolge und die Wahrheitswerte der Operanden bei der von links nach rechts gehenden Berechnung aus. Falls sich bei a && b herausstellt, dass a false ist, dann muss b nicht mehr berücksichtigt werden, da das Endergebnis schon false ist. Umgekehrt muss bei a || b , b nicht berücksichtigt werden, wenn a true ist, denn dann ist das Ergebnis auch true . Relationale Vergleichsoperatoren Bezeichnung Symbol Beispiel Erläuterung gleich == a == b Test, ob a gleich b ist ungleich ! = a ! = b Test, ob a ungleich b ist kleiner < a < b Test, ob a kleiner als b ist größer > a > b Test, ob a größer als b ist kleiner-gleich <= a <= b Test, ob a kleiner oder gleich b ist größer-gleich >= a >= b Test, ob a größer oder gleich b ist Tab. 6: Relationale Operatoren in Java <?page no="34"?> 34 Schritt 2: Variablen, Datentypen, Operatoren Alle relationalen Vergleichsoperatoren benötigen zwei Operanden, d.h. zwei Werte, und sie liefern einen booleschen Rückgabewert. Außer booleschen Werten können alle anderen einfachen Typen (auch gemischt) auf größer oder kleiner geprüft werden. Bei Zeichen wird bei dem Vergleich dabei der zugrunde liegende Code verwendet (siehe Schritt 11). <?page no="35"?> Schritt 3: Kontrollstrukturen <?page no="36"?> 36 Schritt 3: Kontrollstrukturen Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? Kontrollstrukturen sind die Konstrukte einer Programmiersprache, die den Programmablauf beschreiben; sie verbinden und steuern dabei die einzelnen Anweisungen. Sequenz, Iteration und Auswahl sind die typischen Kontrollstrukturen. Zudem geht es auch um den Aufbau einfacher Anweisungen. Welche Schlagwörter lerne ich kennen? Iteration Sequenz Auswahl for-Schleife while-Schleife if-else switch-case einfache Anweisung leere Anweisung Block Wofür benötige ich dieses Wissen? Selbst kleine Java-Programme benötigen in der Regel alle drei Kontrollstrukturen, denn sie sind die Basis für jegliche Programmierung. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? Die Beherrschung der Kontrollstrukturen ist die Voraussetzung, um Java- Anwendungen schreiben zu können. Die wichtigsten Konstrukte sind dabei die for - und die while -Schleife sowie die if-else -Auswahl. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1231 <?page no="37"?> 3.1 Anweisungen 37 3.1 Anweisungen Im vorliegenden Kapitel geht es zum einen darum, wie aus den einzelnen Operatoren komplexere Anweisungen gebildet werden und wie der Ablauf des Programms gestaltet werden kann. Die zentralen Begriffe sind hierbei die Anweisungen und die Kontrollstrukturen , d.h. sie sind die grundlegenden Sprachelemente, um die Berechnungen durchzuführen und den Verlauf des Programms zu steuern. Abb. 4: Übersicht der Anweisungen und Kontrollstrukturen Der obigen Übersicht kann man entnehmen, dass sich die elementaren Anweisungen aus den Ausdrucksanweisungen und den leeren Anweisungen zusammensetzen. Während die leere Anweisung trivial ist (sie besteht in Java nur aus dem Semikolon), ist die Ausdrucksanweisung („expression“) in der Regel eine Kombination von Variablen und den in Schritt 2 erläuterten Operatoren sowie Methodenaufrufen mit Rückgabewerten (siehe Schritt 5). Diese Ausdrucks-Anweisungen können aus einem arithmetischen, relationalen, logischen, Inkrement-, Dekrement-oder Zuweisungsoperator oder einer beliebigen (syntaktischen korrekten) Kombination dieser bestehen. Typische Beispiele für Ausdrucks-Anweisungen sind: Anweisungen Elementare Anweisungen Kontrollstrukturen Auswahl Schleife Methoden- Aufruf Sequenz Ausdrucks- Anweisung Leere Anweisung <?page no="38"?> 38 Schritt 3: Kontrollstrukturen i++ j *=i u = x && y && z Als Kontrollstrukturen bezeichnet man die Sprachelemente einer Programmiersprache, die den Ablauf eines Programms steuern. In einer Programmiersprache gibt es normalerweise vier Kategorien von Kontrollstrukturen: Sequenz Iteration Auswahl Methodenaufruf Eine Sequenz ist die Ausführung von Anweisungen, die nacheinander erfolgen. Eine Iteration (oder Schleife) ist die wiederholte Durchführung von Anweisungen. Eine Auswahl ist die bedingte Verzweigung innerhalb des Programmablaufs. Ein Methodenaufruf ist der Aufruf einer Methode mit aktuell besetzten Parametern (siehe Schritt 5). 4 In Java werden natürlich alle vier Konstrukte unterstützt, wobei es sowohl für die Schleifen als auch für die Auswahl unterschiedliche Ausprägungen gibt. 3.2 Sequenz Die Sequenz wird nicht durch eine besondere Anweisung oder ein spezielles Schlüsselwort umgesetzt, sondern einfach dadurch, dass die Anweisungen nach- oder untereinander geschrieben werden. Konsequenterweise werden diese Anweisungen dann genau in dieser Reihenfolge auch ausgeführt. Eine Reihe von Anweisungen wird in der Regel in einem Block zusammengefasst, der durch geschweifte Klammern definiert ist. 4 Im Gegensatz zu Methodenaufrufen in Anweisungen sind hier auch Aufrufe ohne Rückgabe möglich - ein evtl. Rückgabeergebnis wird hier ignoriert. <?page no="39"?> 3.3 Auswahl 39 Ein einfaches Beispiel wird hier aufgeführt: Listing 8: Beispiel zweier sequentieller Anweisungen Zuerst wird die Zeile [1] und danach die Zeile [2] durchlaufen, da durch den Quellcode eine explizite Reihenfolge festgelegt wurde. 3.3 Auswahl Für die Auswahl gibt es in Java zwei Alternativen: die if-else -Verzweigung die switch case- Verzweigung Die if-else-Verzweigung basiert auf einer booleschen Bedingung und bietet zwei Äste an: der erste, wenn die Bedingung wahr ist, und der optionale zweite für den sonstigen Fall. Es handelt sich hier also um eine Ein- oder Zweifach-Verzweigung. Die switch-case-Verzweigung ist hingegen als Mehrfach-Verzweigung ausgelegt. Abhängig von einem ganzzahligen Ausgangswert, dem Vergleich mit einem Zeichen oder einem String 5 , wird der entsprechende Ast angesprungen. Eine wichtige Besonderheit ist dabei, dass jeder Ast mit einem break- Befehl abgeschlossen werden muss, ansonsten werden die nachfolgenden Äste ebenfalls ausgeführt. Der syntaktische Aufbau der if-else -Verzweigung ist wie folgt: if(Boolesche_Bedingung) Anweisung_1; else Anweisung_2; Bei der if-else -Verzweigung wird geprüft, ob die angegebene boolesche Bedingung logisch wahr oder falsch ist. Wenn die Bedingung wahr ist, wird die Anweisung_1 ausgeführt, ansonsten wird die Anweisung_2 ausgeführt. Der 5 Seit der Java Version 7. { System.out.println("Dies ist eine Anweisung"); System.out.println("Dies ist eine weitere Anweisung"); } 2 1 <?page no="40"?> 40 Schritt 3: Kontrollstrukturen else -Zweig ist dabei optional und führt, sofern er entfällt, zu einer Einfach-Verzweigung. Falls mehrere Anweisungen - statt nur einer einzelnen - ausgeführt werden sollten, dann müssten diese in geschweiften Klammern gesetzt werden, so dass ein Block von Javacode entsteht. if(Boolesche_Bedingung){ Anweisung_11; Anweisung_12; … }else{ Anweisung_21; Anweisung_22; } Es empfiehlt sich, möglichst diese Variante mit den geschweiften Klammern zu verwenden, so dass eine oder mehrere Anweisungen eingefügt werden können. Die Mehrfachverzweigung ist an den beiden Schlüsselwörtern case und switch zu erkennen. Die Syntax der switch-case -Abfrage ist: switch(Ausdruck){ case Alternative_1: Anweisung_11; Anweisung_12; …; break; case Alternative_2: Anweisung_21; Anweisung_22; …; break; … default: Anweisungen; } Der Ausdruck hinter switch darf nur vom Typ char , byte , short , int oder String (seit Java Version 7) sein. Konsequenterweise dürfen die Äste der switch -Verzweigung lediglich konstante Werte vom Typ char , byte , short , und int sowie vom Typ String sein. Die Anweisungen der case -Äste sollten mit break; beendet werden (ohne weitere Bedingungsprüfung), da ansonsten die Anweisungsblöcke der nachfolgenden Äste ebenfalls ausgeführt werden. Der default -Zweig ist optional. Er wird ausgeführt, wenn keine der vorherigen Alternativen ausgeführt wurde. <?page no="41"?> 3.4 Schleifen / Wiederholungen 41 3.4 Schleifen / Wiederholungen Für die Kategorie der Schleifen (auch „Iterationen“ genannt) gibt es drei Ausprägungen: die for -Schleife: bietet sich insbesondere an, wenn bereits zu Beginn die Anzahl der Durchläufe bekannt ist, die while -Schleife: eine „Kopf“-Schleife, bei der zuerst vor Beginn des Schleifenrumpfes geprüft wird, ob die Bedingung wahr ist und nur in diesem Fall wird der Schleifenrumpf ausgeführt, die do-while -Schleife: eine „Fuß“-Schleife, denn hier wird erst am Ende des Schleifenrumpfs geprüft, ob die Bedingung wahr ist und in diesem Fall wird die Schleife nochmals durchlaufen. Der prinzipielle Aufbau einer for -Schleife sieht wie folgt aus: for(Initialisierung; Bedingung; Update) Anweisung; Auch hier empfiehlt es sich den Schleifenrumpf in geschweifte Klammern zu setzen, um so sicherzustellen, dass die Schleife auch mehrere Anweisungen ausführt. for(Initialisierung; Bedingung; Update) { Anweisung_1; Anweisung_2; Anweisung_3; … } Bei der Initialisierung handelt es sich in der Regel um den Schleifenzähler, der ggf. deklariert und auf einen Wert gesetzt wird, um dann in die Bedingung einzufließen. Beim Update wird der Schleifenzähler erhöht oder reduziert. Wenn die Schleifenbedingung als wahr ausgewertet wird, werden die Anweisungen des Schleifenrumpfs und eine Anpassung des Schleifenzählers (typischerweise eine Erhöhung oder Reduzierung um 1) ausgeführt, im negativen Fall wird jedoch die Schleife verlassen. Es ist darauf hinzuweisen, dass der Schleifenrumpf nur aus einer Anweisung besteht oder ansonsten aus einem Block von Anweisungen, definiert durch geschweifte Klammern. <?page no="42"?> 42 Schritt 3: Kontrollstrukturen Beispiel für eine for -Schleife: Listing 9: Beispiel einer for-Schleife [1] Der Schleifenzä hler i wird deklariert und mit dem Wert 1 belegt. [2] Vor der Durchfü hrung der Schleife wird geprü ft, ob die Bedingung fü r die Fortsetzung noch gegeben ist (i < 5). [3] Der Schleifenzä hler wird erhö ht, nachdem der Schleifenrumpf durchlaufen wurde. [4] Die Schleife wird ausgefü hrt und die Ausgabe erfolgt unter Berü cksichtigung des aktuellen Stands des Schleifenzä hlers am Bildschirm. Eine typische Anwendung der for -Schleife ist die Durchwanderung eines Arrays, deshalb gibt es hierfür eine spezielle, erweiterte Variante der for -Schleife für Arrays (die for-each -Schleife): for (Datentyp Variable : Arrayname 6 ){ Anweisungen; … } In Kapitel 4 finden sich für beide Varianten der for -Schleife zur Array-Durchwanderung Beispiele. Die while - und die do -Schleife bieten im Gegensatz zur for -Schleife nur die Möglichkeit, die Abbruchbedingung zu prüfen. Eventelle Schleifenparameter und deren jeweilige Änderungen muss ein Programmierer an anderer Stelle (z.B. vor der Schleife und im Rumpf) vorsehen. Der prinzipielle Aufbau der while -Schleife sieht wie folgt aus: while(Boolesche_Bedingung){ Anweisung_1; Anweisung_2; … } 6 Statt eines Arrays kann auch eine Collection verwendet werden, die auf dem Interface Collection basierenden Datenstrukturen werden in Schritt 10 erläutert. 2 3 1 for (int i = 1; i < 5; i++){ System.out.println("Durchlauf: " + i); } 4 <?page no="43"?> 3.4 Schleifen / Wiederholungen 43 Der Einfachheit halber wird der Schleifenrumpf durch geschweifte Klammern definiert, so dass mehrere Anweisungen ausgeführt werden können. Analog zur for -Schleife testet die while -Schleife (als „Kopf-Schleife“) zuerst , ob die Bedingung erfüllt ist, und erst danach werden die Anweisung(en) des Schleifenrumpfs ausgeführt. Listing 10: Beispiel einer while-Schleife [1] Der Schleifenzä hler i wird deklariert und mit dem Wert 1 belegt. [2] Es wird geprü ft, ob die Schleifenbedingung (noch) erfü llt ist. [3] Falls ja, wird im Schleifenrumpf der Text, inklusive des Werts des Schleifenzä hlers, ausgegeben. [4] Der Schleifenzä hler wird um 1 erhö ht. Im Gegensatz zu den vorhergehenden Schleifen ( for -Schleife, while -Schleife), testet die do-while -Schleife erst am Ende, ob die Bedingung zum weiteren Durchlauf der Schleife noch erfüllt ist. Deshalb wird sie auch „Fuß-Schleife“ genannt, sie ist syntaktisch wie folgt strukturiert: do Anweisung while(Boolesche_Bedingung) Die do-while -Schleife wird mindestens einmal ausgeführt, der Rumpf der while -Schleife wird ggfs. gar nicht ausgeführt. Beispiel für eine do-while -Schleife: Listing 11: Beispiel einer do-while-Schleife [1] Der Schleifenzä hler i wird deklariert und mit dem Wert 1 belegt. int i = 1; while (i < 5){ System.out.println("Durchlauf: " + i); i++; } 2 3 4 1 int i = 1; do{ System.out.println("Durchlauf: " + i); i++; }while (i < 5); 2 3 4 1 <?page no="44"?> 44 Schritt 3: Kontrollstrukturen [2] Ausgabe des Texts, inklusive dem Wert des Schleifenzä hlers. [3] Hochzä hlen des Schleifenzä hlers. [4] Letztlich wird geprü ft, ob die Schleifenbedingung erfü llt ist. <?page no="45"?> Schritt 4: Felder / Arrays <?page no="46"?> 46 Schritt 4: Felder / Arrays Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? Ein Array (oder Feld) ist die wichtigste Datenstruktur, um mehrere Daten desselben Typs hintereinander abzuspeichern. Die Deklaration sowie die Initialisierung eines Arrays und der anschließende Zugriff über den Index werden erläutert. Ein Array kann dabei ein- oder mehrdimensional sein. Welche Schlagwörter lerne ich kennen? Array Feld Zugriff Index, Position eindimensional mehrdimensional Wofür benötige ich dieses Wissen? Ein Array wird verwendet, um größere Datenmengen desselben Typs abzuspeichern. Aus diesem Grund sind die Kenntnisse über Arrays für alle Entwickler relevant. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? Arrays sind Referenztypen und unterscheiden sich damit von den einfachen Datentypen. Ein Array kann man sich als lange Liste von Werten vorstellen, auf die man über den Index direkt zugreifen kann. Typische Fragen sind, mit welcher Zahl die Nummerierung der Positionen beim Array beginnt und wie der Zugriff auf das Array erfolgt. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1232 <?page no="47"?> 4.1 Werte in Arrays anordnen 47 4.1 Werte in Arrays anordnen Ein Array (oder Feld) stellt eine Anordnung von Werten dar, die alle vom selben Datentyp sein müssen und die über einen Index identifiziert werden können. Im einfachsten Fall, einem eindimensionalen Array, ist das Array eine sequentielle Anordnung der Werte. Der Index entspricht dabei der Position des Werts im Array; er beginnt bei 0 und wird in Einerschritten hochgezählt. 7 Das folgende Beispiel erläutert den Aufbau des Arrays. Die Werte sind vom Datentyp int . Index 0 1 2 3 4 Wert 2 4 6 8 10 Tab. 7: Eindimensionales Array mit 5 Werten Im Beispiel handelt es sich um die Speicherung der Werte 2, 4, 6, 8 und 10 in den Spalten bzw. Positionen [0], [1], [2], [3] und [4]. Ein zweidimensionales Array ist vergleichbar einer Tabelle bestehend aus Zeilen und Spalten. Zeile/ Spalte 0 1 2 3 4 0 0 1 2 3 4 1 5 6 7 8 9 2 10 11 12 13 14 3 15 16 17 18 19 Tab. 8: Zweidimensionales Array mit 4 5 Werten Im vorliegenden Beispiel handelt es sich um die Speicherung der Werte 0 bis 19 in den entsprechenden Positionen [0][0], [0][1], …, [3][4] verteilt über die 4 Zeilen und 5 Spalten. Der Zugriff auf die einzelnen Werte eines Arrays erfolgt über deren eindeutige Positionierung, dem Index. Bei einem eindimensionalen Array ist es der Index (oder die Position) innerhalb der linearen Anordnung. Bei einem zweidimensionalen Array sind zwei Indizes notwendig, einer für die Zeile und ein anderer für die Spalte. 7 Dies bedeutet, dass das erste Element den Index 0 hat und das letzte Element immer einen Index hat, der um eins kleiner ist als die Anzahl der Elemente im Array. <?page no="48"?> 48 Schritt 4: Felder / Arrays Bei einem mehrdimensionalen Array mit n Dimensionen werden folgerichtig n Indizes benötigt, um die eindeutige Position jedes Wertes im Array festzulegen. Ein Array ist eine Anordnung von Werten desselben Datentyps. Der Zugriff erfolgt über die eindeutige Indizierung eines Wertes. Bei einem eindimensionalen Array ist es ein Index, bei einem zweidimensionalen Array sind zwei Indizes und bei einem n-dimensionalen Array sind n Indizes notwendig. Die Anzahl der Indizes entspricht also den Dimensionen des Arrays . Ein Array ist (unabhängig von der Dimension) ein Referenztyp, d.h. die Variable eines Arrays enthält nur die Referenz auf den Speicherbereich, in dem die einzelnen Werte des Arrays gespeichert sind. 8 Man unterscheidet zwischen: der Arrayvariable, die benannt ist und dem Array selbst, das keinen Namen hat. Die Deklaration eines eindimensionalen Arrays ist wie folgt: Datentyp[] Variablenname Die Initialisierung des eindimensionalen Arrays erfolgt durch: new Datentyp[Länge] oder durch eine explizite Aufzählung, z.B. {Wert_1, Wert_2, …, Wert_n} Üblicherweise wird die Deklaration mit der Initialisierung in einer gemeinsamen Anweisung zusammengefasst: Datentyp[] Variablenname = new Datentyp [Länge]; 9 Bei einem Array der Dimension n müssen die rechteckigen Klammern [ ] (auf beiden Seiten) n-mal aufgeführt werden. Ein n-dimensionales Array wird deklariert und initialisiert: Datentyp[] … [] Variablenname = new Datentyp [Länge_1] … [Länge_n]; 10 8 Im Gegensatz dazu enthalten Variablen eines einfachen Datentyps den Wert direkt. 9 Java erlaubt auch den alternativen syntaktischen Ausdruck Variablenname[] , der aber nicht empfohlen wird. 10 Bei mehreren Dimensionen können die letzten Initialisierungen auch unvollständig sein. <?page no="49"?> 4.1 Werte in Arrays anordnen 49 Auf die einzelnen Elemente des Arrays kann über den Index bzw. die Indizes zugegriffen werden. Bei der Verwendung eines Indizes, der über die Grenzen des Arrays hinausgeht, wird eine IndexOutOfBoundsException 11 geworfen. Der häufigste Fall ist ein eindimensionales Array, bei dem der Zugriff durch einen Index erfolgt, der durch zwei rechteckige Klammern umschlossen wird: Variablenname[Index] Bei einem n-dimensionalen Array sind n Indizes erforderlich: Variablenname [Index_1] ... [Index_n] Wenn ein Array deklariert wird, lässt sich die Größe des Arrays aus der entsprechenden Zahl ablesen. Allerdings gibt es auch Fälle, in denen zum Beispiel ein Array als Parameter übergeben wird und in dem die Größe zur Laufzeit bestimmt werden muss. In diesem Fall kann auf das Attribut length eines Arrays zurückgegriffen werden. Die Größe eines Arrays kann mit dem Attribut length ermittelt werden, in der Form Arrayvariable.length . Die Größen eines zweidimensionalen Felds lassen sich durch Arrayvariable.length (erste Dimension) und Arrayvariable[0].length (zweite Dimension) ermitteln. Listing 12: Deklaration und Initialisierung eines zweidimensionalen Arrays bestehend aus ganzen Zahlen [1] Deklaration und Initialisierung der Konstante ZEILEN und SPALTEN . [2] Deklaration und Initialisierung eines zweidimensionalen Felds. [3] AÜ ußere for -Schleife, die die Zeilen durchlä uft. 11 Die Aufgabe von Exceptions wird in Kapitel 8 erläutert. final int ZEILEN = 10; final int SPALTEN = 20; int[][] tabelle = new int[ZEILEN][SPALTEN]; for(int i = 0; i < tabelle.length; i++) { for(int k = 0; k < tabelle[i].length; k++){ tabelle[i][k] = i * k; } } 1 2 3 4 5 <?page no="50"?> 50 Schritt 4: Felder / Arrays [4] Innere for -Schleife, die die Spalten durchlä uft - es wird dabei jeweils die Spaltenzahl der aktuellen Zeile ermittelt. [5] Zuweisung des Produkts aus Zeile und Spalte an den jeweiligen Tabellenwert. Alternativ kann für Arrays auch die in Kapitel 2 bereits vorgestellte for-each - Variante genutzt werden. Dabei werden die einzelnen Elemente des Arrays nacheinander durchlaufen und automatisch einer Variablen zugewiesen (die zum Datentyp des Arrays kompatibel sein muss). Listing 13: Durchwanderung eines Arrays mit der for-each -Schleife [1] Das Array vom Typ int wird deklariert und initialisiert durch die explizite Aufzä hlung aller Elemente. [2] Die Variable i vom Datentyp int wird deklariert und der Bezug zum Array feld wird hergestellt. [3] Im Schleifenrumpf werden automatisch alle Elemente des Arrays (von 0 bis zum Ende des Arrays) durchlaufen der Variable i zugewiesen. Beim ersten Durchlauf enthä lt n also den Wert des Arrays an der Position 0, beim zweiten Durchlauf den von der Position 1 usw. int[] feld = {1, 2, 3, 4, 5, 6, 7, 8, 9}; for(int n : feld ){ System.out.println("Element: " + n); } 2 3 1 <?page no="51"?> Schritt 5: Methoden <?page no="52"?> 52 Schritt 5: Methoden Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um Methoden, ihren Aufbau und ihren Aufruf. Welche Schlagwörter lerne ich kennen? Methode Signatur Modifier formale und aktuelle Parameter Rekursion Wofür benötige ich dieses Wissen? Methoden sind ein Element der Programmierung, um Abläufe zusammen zu fassen und mehrfach aufrufen zu können. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, die Signatur einer Methode zu bestimmen und aktuelle und formale Parameter zu unterscheiden. Im Rahmen von Programmieraufgaben müssen Methoden deklariert werden. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1233 <?page no="53"?> 5.1 Anweisungen in Methoden zusammenfassen 53 5.1 Anweisungen in Methoden zusammenfassen Methoden stellen ein wesentliches Konzept aller Programmiersprachen dar. Methoden fassen Anweisungen unter einem symbolischen Methodennamen zusammen. Die Methode kann unter diesem Namen aufgerufen werden und führt dann die enthaltenen Anweisungen aus. Konkrete Methoden werden in der Form definiert: Modifier* (Rückgabetyp | void) Methodenname (Parameterliste){ Methodenkörper } Abstrakte Methoden werden in der Form definiert Modifier* (Rückgabetyp | void) Methodenname (Parameterliste); Dabei muss einer der Modifier abstract sein, static , private und final sind als Modifier (siehe unten) hier ausgeschlossen. Als Signatur einer Methode werden der Methodenname und die Parametertypen (in der Reihenfolge der Deklaration) bezeichnet. Modifier, Ergebnistyp und Parameternamen gehören nicht zur Signatur. Innerhalb einer Klasse dürfen nur Methoden unterschiedlicher Signatur vorkommen. Methoden können überladen werden: eine Klasse kann mehrere Methoden mit gleichem Namen und unterschiedlichen Parametertypen besitzen (also mit unterschiedlichen Signaturen). Methoden können durch ihre Parameter unterschiedliche Werte erhalten. Formale Parameter sind symbolische Namen, die zur Deklaration der Methode genutzt werden. Aktuelle Parameter sind die tatsächlichen Werte, die während der Ausführung genutzt werden. Die formalen Parameter werden in der Parameterliste zusammen mit ihrem Typ in der Form Datentyp Parameter, … deklariert. Die Parameterliste kann auch leer sein. Aktuelle Parameter werden in der Reihenfolge der Deklaration an die formalen Parameter gebunden. Der Methodenkörper ist eine Sequenz von Java-Anweisungen. <?page no="54"?> 54 Schritt 5: Methoden Methoden können einen Rückgabetyp besitzen. In diesem Fall muss die Methode einen Wert vom entsprechenden Typ zurückgeben. Dem Rückgabewert wird das Schlüsselwort return vorangestellt. Mit return wird die Ausführung der Methode beendet und die Kontrolle an den aufrufenden Programmteil zurückgegeben. Gibt die Methode kein Ergebnis zurück, hat sie den Rückgabetyp void . In diesem Fall ist kein return notwendig, aber auch return ohne Rückgabewert ist möglich. Modifier legen den Zugriff auf die Methode oder weitere Eigenschaften der Methode fest. Es wird zwischen Zugriffs- und Nicht-Zugriffsmodifiern unterschieden. Zugriffsmodifier legen die Sichtbarkeit der Elemente fest und definieren den Grad, in dem andere Programmteile auf Programmteile (in diesem Fall Methoden) zugreifen können. Eine ausführliche Diskussion der Sichtbarkeit und der Wirkung der Zugriffsmodifier findet sich in Kapitel 6. Die Zugriffsmodifier für Methoden sind: public : Die Methode kann aus allen anderen Klassen aufgerufen werden. protected : Die Methode kann aus allen anderen Klassen innerhalb desselben Pakets und aus Unterklassen der die Methode enthaltenden Klasse aufgerufen werden. default (auch „ friendly “ oder „ package “): Die Methode kann aus allen anderen Klassen innerhalb desselben Pakets aufgerufen werden. Dieser Modifier hat kein Schlüsselwort, sondern wird durch das Fehlen der anderen Zugriffsmodifier angezeigt. private : Die Methode kann nur innerhalb derselben Klasse aufgerufen werden. Die Nicht-Zugriffsmodifier sind: static : Die Methode ist der Klasse zugeordnet. Falls der Modifier fehlt, ist die Methode dem Objekt zugeordnet (eine Erläuterung von Objekten findet sich in Kapitel 7). final : Die Methode kann nicht von einer Unterklasse überschrieben werden. Fehlt der Modifier, kann sie überschrieben werden. abstract : Abstrakte Methoden haben keinen Methodenkörper („Implementierung“) sondern nur eine Signatur und werden mit einem Semikolon abgeschlossen. Abstrakte Methoden können nicht ausgeführt werden, sondern müssen von Unterklassen dieser Klasse implementiert werden (→ Kapitel 7). Fehlt der Modifier, ist die Methode konkret und muss einen Körper besitzen. Sie kann dann auch ausgeführt werden. <?page no="55"?> 5.1 Anweisungen in Methoden zusammenfassen 55 Als Beispiel für dieses und die folgenden beiden Kapitel soll ein Ausschnitt aus einem Programm zur Autovermietung verwendet werden. Anwender dieses Programms können (über die Kommandozeile) Autos ausleihen und wieder zurückgeben. Die folgende Methode der Autovermietung sucht im Feld ausgeliehen die Position eines freien Autos (Listing 14): Listing 14: Die Methode freiesAutoSuchen [1] Der Methodenname freiesAutoSuchen . [2] Zugriffsmodifier public . [3] Nicht-Zugriffsmodifier static . [4] Die Methode muss ein Resultat vom Typ int liefern. [5] Die Methode hat keine formalen Parameter. [6] Die Methode durchwandert alle Werte des Felds bis zur Anzahl und prü ft, ob das das Feld ausgeliehen an der Stelle i true oder false ist. [7] Falls an der Stelle i false eingetragen ist, ergibt ! ausgeliehen [i] den Wert true . In diesem Fall wird der gefundene Index i als Resultat zurü ck gegeben. Die Methode endet dann an dieser Stelle. [8] Falls an allen Stellen true eingetragen war, wird die Schleife ohne return durchlaufen. Am Ende wird die zu Beginn der Klasse festgelegte Konstante UNDEFINIERT (mit dem Wert -1) als Resultat zurü ckgegeben. Die folgende Methode der Autovermietung entleiht ein Auto (Listing 15): public static int freiesAutoSuchen() { for (int i = 0; i < anzahl; i++) if (! ausgeliehen[i]) return i; return UNDEFINERT; } 6 7 8 public class Autovermietung { …public static String[] modelle = new String[MAX]; public static String[] kennzeichen = new String[MAX]; public static int[] kreditkarten = new int[MAX]; … public static String ausleihen(int kreditkarte) { int i = freiesAutoSuchen(); if (i == UNDEFINERT) { System.out.println("Kein freies Auto gefunden."); return null; } ausgeliehen[i] = true; 2 3 4 1 5 6 7 8 <?page no="56"?> 56 Schritt 5: Methoden Listing 15: Die Methode ausleihen [1] Der Methodenname ausleihen . [2] Zugriffsmodifier public . [3] Nicht-Zugriffsmodifier static . [4] Die Methode muss ein Resultat vom Typ String liefern. [5] Die Methode hat den formalen Parameter kreditkarte. [6] Es wird zunä chst der Index eines freien Autos gesucht. [7] Falls kein freies Auto gefunden wurde (das Ergebnis war dann die zuvor definierte Konstante UNDEFINIERT ), wird die Methode an dieser Stelle mit dem Rü ckgabewert null beendet. null ist ein von Java vordefinierter Wert der „kein Objekt“ kennzeichnet. [8] Das Auto wird als „ausgeliehen“ gekennzeichnet, die Kreditkartennummer wird gespeichert. [9] Meldung am Bildschirm. [10] Rü ckgabe des Kennzeichens des ausgeliehenen Fahrzeugs. [11] Aufruf der Methode: der aktuelle Parameter 12345678 (der eine Kreditkartennummer reprä sentieren soll) wird an den formalen Parameter kennzeichen der Methode ausleihen gebunden. Methoden können sich selbst aufrufen - in diesem Fall spricht man von Rekursion (Listing 16). public class Autovermietung { … public static int freiesAutoSuchen(int i) { if(i == anzahl) return UNDEFINERT; else if (! ausgeliehen[i]) return i; 1 2 3 4 kreditkarten[i] = kreditkarte; System.out.println(modelle[i] + " (" + kennzeichen[i] + ")" + " wurde ausgeliehen"); return kennzeichen[i]; }…public static void main(String[] args) { …String kennzeichen = ausleihen(12345678); } } 9 10 11 <?page no="57"?> 5.1 Anweisungen in Methoden zusammenfassen 57 Listing 16: Rekursive Implementierung von freiesAutoSuchen [1] Der Methodenname ausleihen . [2] Die Methode hat den formalen Parameter i . [3] Abbruchbedingung fü r die Rekursion: es wurden alle gü ltigen Werte durchsucht und nichts gefunden. In diesem Fall wird UNDEFINIERT zurü ckgegeben. [4] Abbruchbedingung fü r die Rekursion: es wurden ein nicht ausgeliehener Wagen gefunden. In diesem Fall wird der Index zurü ckgegeben. [5] Fortsetzung der Rekursion: Die Methode ruft sich selbst mit geä ndertem Parameter auf. [6] Aufruf der rekursiven Methode: Startwert ist der aktuelle Parameter 0 - der erste Index des Feldes. else return freiesAutoSuchen(i + 1); } public static String ausleihen(int kreditkarte) { int i = freiesAutoSuchen(0); … } 5 6 <?page no="59"?> Schritt 6: Sichtbarkeit / Gültigkeit <?page no="60"?> 60 Schritt 6: Sichtbarkeit / Gültigkeit Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um die Sichtbarkeit und Gültigkeit von Methoden und Attributen. Welche Schlagwörter lerne ich kennen? Sichtbarkeit Gültigkeit Zugriffsmodifier Klasse Interface Aufzählungstypen Paket Information Hiding Wofür benötige ich dieses Wissen? Die Kontrolle über Sichtbarkeit erlaubt, öffentliche Schnittstellen für Klassen, Interfaces und Aufzählungstypen zu definieren. Damit ist es möglich, ein System in möglichst unabhängige Komponenten zu zerlegen. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, die verschiedenen Zugriffsmodifier zu unterscheiden und den Unterschied zwischen Sichtbarkeit und Gültigkeit zu erläutern oder anzuwenden. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1234 <?page no="61"?> 6.1 Java-Komponenten 61 6.1 Java-Komponenten Ein Programmsystem wird in Teile oder Komponenten zerlegt, um diese Komponenten möglichst unabhängig behandeln zu können. In dieser Situation bekommt das Thema „Sichtbarkeit und Gültigkeit“ eine zentrale Rolle. Komponenten in Java sind Klassen, Interfaces (→ Schritt 7) und Pakete. Klassen sind die zentrale Komponente in Java (und allen objektorientierten Sprachen). Ein Klasse besteht aus Attributen und Methoden und ggfs. weiteren, inneren Klassen, Interfaces und Aufzählungstypen. Im Folgenden werden die Sichtbarkeit und Gültigkeit zunächst nur für äußere Klassen, Interfaces und Aufzählungstypen betrachtet. Innere Elemente stellen einen Sonderfall dar, der im letzten Abschnitt behandelt wird. Interfaces sind Komponenten, die ausschließlich nicht-statische, abstrakte Methodendefinitionen und evtl. konstante Attribute enthalten können. Aufzählungstypen sind Komponenten, die neben Attributen und Methoden insbesondere Konstanten definieren. Klassen, Interfaces und Aufzählungstypen werden in Paketen zusammengefasst; Pakete können wiederum Teil anderer Pakete sein. Innerhalb von Klassen, Interfaces und Aufzählungstypen kann mit Hilfe der Zugriffsmodifier die Sichtbarkeit von Methoden und Attributen für andere Komponenten festgelegt werden. Dabei kann festgelegt werden, ob Methoden oder Attribute nur innerhalb einer Klasse oder Aufzählungstyps, nur innerhalb des gleichen Pakets, nur von Unterklassen einer Klasse oder generell verwendet werden können. Klassen werden in der folgenden Form definiert: Listing 17: Allgemeine Form einer Klassendefiniton package paketname(.paketname)*; (Modifier)* class Klassenname{ Klassenkörper } 3 4 5 6 <?page no="62"?> 62 Schritt 6: Sichtbarkeit / Gültigkeit [1] Schlü sselwort package zeigt die Paketdefiniton an. Die Paketdefinition ist optional - falls sie fehlt, wird die Klasse im „default“-Paket abgelegt. Eine Klasse kann immer nur einem Paket zugeordnet sein, d.h. es kann hö chsten eine Paketdefinition geben. [2] Liste von Paketnamen, getrennt durch Punkte und mit einem Semikolon beendet. Die Namen geben (von links nach rechts) die Pakethierarchie an. Der erste Name links ist das ä ußerste Paket, die folgenden Pakete sind jeweils im vorigen Paket enthalten. Pakete werden auf Betriebssystemebene als Verzeichnisse umgesetzt - der Paketname muss dabei genau dem Verzeichnisnamen entsprechen. Paketnamen sind ü blicherweise in Kleinbuchstaben. Enthä lt ein Paket ein anderes Paket, so ist es auch im entsprechenden Verzeichnis. Enthä lt ein Paket eine Klasse, so ist die Klassendatei auch im entsprechenden Verzeichnis des Pakets. [3] Liste von Modifiern. Wie bei den Methoden und Attributen sind hier Zugriffs- und Nicht-Zugriffsmodifier mö glich. [4] Schlü sselwort class zeigt eine Klassendefinition an. [5] Name der Klasse. Klassennamen beginnen ü blicherweise mit einem Großbuchstaben. [6] Kö rper der Klasse: Methoden und Attribute. Interfaces oder Aufzählungstypen werden analog mit dem Schlüsselwort interface oder enum definiert. Dabei gelten die folgenden Besonderheiten: Interfaces dürfen nur abstrakte Methoden mit public-Sichtbarkeit enthalten. Der public - und der abstract -Modifier können dabei weggelassen werden und werden dann implizit ergänzt. Ab Java 8 ist für Interfaces der static -Modifier und der (neue) default - Modifier zugelassen. Der default -Modifier erzwingt einen Methodenkörper. 12 Aufzählungstypen können zusätzlich eine Liste von Konstanten (vom Typ des Aufzählungstyps) enthalten, die aber keine explizite Typdeklaration mehr benötigen. 12 Achtung: Der default -Modifier ist nicht mit der „default“-Sichtbarkeit zu verwechseln. Der default -Modifier ist ein - seit Java 8 neues - Schlüsselwort, die „default“-Sichtbarkeit wird durch das Fehlen eines Zugriffsmodifiers angezeigt. <?page no="63"?> 6.2 Das Geheimnisprinzip und Zugriffsmodifier 63 6.2 Das Geheimnisprinzip und Zugriffsmodifier Das zentrale Prinzip, um die Unabhängigkeit zwischen Komponenten zu erreichen, ist das Geheimnisprinzip („Information Hiding“). Dieses Prinzip bezieht sich im Wesentlichen auf die Zerlegung in Klassen und Pakete. Die zentralen Ideen sind: Komponenten stellen nur die Informationen, die tatsächlich nötig sind, zur Verfügung - alle übrigen Informationen werden verborgen. Auf die verborgenen Informationen darf von außerhalb nur über Methoden zugegriffen werden - der direkte Zugriff auf Variablen von außerhalb ist verboten. Java kontrolliert den Zugriff (und erlaubt damit das Geheimnisprinzip) mit Hilfe der Zugriffsmodifier public , private , protected und „default“. „default“ ist kein eigenes Schlüsselwort (im Gegensatz zum default -Modifier bei Interfaces), sondern ist dann festgelegt, wenn keines der anderen drei Schlüsselwörter verwendet wird. Diese Modifier können auf Variablen, Methoden oder (eingeschränkt) auf Klassen angewandt werden. Zugriffsmodifier bei Klassendefinitionen: public class A{…} : Die Klasse kann von allen anderen Klassen verwendet werden. class A{…} : (kein Zugriffsmodifier, „default“). Die Klasse kann nur von Klassen desselben Pakets verwendet werden. Zugriffsmodifier bei Methodendefinitionen innerhalb einer Klasse z.B. für eine Methode modifier void m(){…} oder ein Attribut modifier int x : modifier Die Methode oder das Attribut kann von Objekten 13 der Klasse selbst … public … und allen anderen Objekten verwendet werden. protected … und von Objekten aller Klassen im selben Paket und aller Unterklassen der Klasse verwendet werden. (kein Zugriffsmodifier, „default“) … und von Objekten aller Klassen im selben Paket verwendet werden. private … und keinen weiteren Objekten verwendet werden. 14 Tab. 9: Nutzung von Modifiern 13 Siehe Abschnitt: „Klassen und Objekte“. 14 Die bedeutet auch, dass ein Objekt auf die privaten Attribute eines anderen Objekts zugreifen kann, falls es ein Exemplar derselben Klasse wie das Objekt selbst ist. <?page no="64"?> 64 Schritt 6: Sichtbarkeit / Gültigkeit Das Geheimnisprinzip wird mit Hilfe der Zugriffsmodifier folgendermaßen umgesetzt: Attribute sind immer private . Methoden können je nach Verwendung private (Hilfsmethoden), public (öffentliche Methoden) oder protected (auf Pakete oder Unterklassen beschränkte Methoden) sein. Der „default“-Zugriff wird eher selten genutzt. Listing 18 zeigt, wie in der Autovermietung Informationen verborgen werden. Listing 18: Verbergen von Informationen in der Autovermietung [1] Definition eines Pakets vermietung . In diesem Paket liegt die Klasse Autovermietung. [2] Deklaration von privaten Daten. Diese Daten sind nur innerhalb der Klasse Autovermietung nutzbar. [3] Deklaration einer privaten (Hilfs-)Methode. Diese Methode kann nur innerhalb der Klasse Autovermietung genutzt werden. [4] Die Methode greift auf die privaten Daten der Klasse zu. [5] Deklaration einer ö ffentlichen Methode ausleihen . Diese Methode stellt die ö ffentliche Schnittstelle der Klasse dar. [6] Die Methode greift auf die privaten Daten der Klasse zu. Im folgenden Abschnitt wird gezeigt, wie auf die öffentlichen Elemente durch eine Anwenderklasse zugegriffen wird. package vermietung; public class Autovermietung { …private static String[] kennzeichen = new String[MAX]; private static boolean[] ausgeliehen = new boolean[MAX]; private static int anzahl = 0; …private static int freiesAutoSuchen() { for(int i = 0; i < anzahl; i++) if(! ausgeliehen[i]) return i; return UNDEFINERT; } public static String ausleihen(int kreditkarte){ int i = freiesAutoSuchen(); …return kennzeichen[i]; }… } 1 2 3 4 5 6 <?page no="65"?> 6.3 Qualifikation und Import 65 6.3 Qualifikation und Import Elemente aus anderen Klassen können nicht direkt verwendet werden - sie müssen über eine Qualifikation identifiziert werden. Eine Qualifikation besteht aus den Paketnamen, dem Klassennamen und schließlich dem Elementnamen, jeweils getrennt durch einen Punkt, also: (Paketname.)*Klassenname.Elementname Listing 19 bis Listing 21 zeigen, wie die öffentliche Schnittstelle aus Listing 18 von einer Anwenderklasse genutzt werden kann. Listing 19: Qualifizierte Nutzung der Autovermietung [1] Definition eines Pakets kunden . In diesem Paket liegt die Klasse Interaktion . Klassen aus diesem Paket kö nnen aus anderen Paketen nur deren ö ffentliche ( public ) Elemente nutzen. [2] Qualifikation der Methode ausleihen aus Klasse Autovermietung im Paket vermietung . Die Qualifikation kann durch einen Import ersetzt werden - in diesem Fall werden zu Beginn der Klasse (nach der Paketdeklaration) durch den Import die Klassen angegeben, von denen Elemente verwendet werden sollen. In diesem Fall kann die Qualifikation durch die Paketnamen weg gelassen werden, also: import (Paketname.)+Klassenname ; package kunden; public class Interaktion{ public static void main(String[] args) { …String kennzeichen = vermietung.Autovermietung.ausleihen(12345678); … } } 1 2 package interaktion; import vermietung.Autovermietung; public class Interaktion{ 1 2 <?page no="66"?> 66 Schritt 6: Sichtbarkeit / Gültigkeit Listing 20: Impor t der Autovermietung [1] Definition eines Pakets interaktion . [2] Import der Klasse Autovermietung aus dem Paket vermietung . [3] Qualifikation der Methode ausleihen aus Klasse Autovermietung . Eine Qualifikation des Pakets ist wegen des Imports nicht mehr notwendig. Eine Besonderheit stellen Wildcards (Symbol * ) dar. Sie erlauben den generellen Import aller öffentlichen Elemente einer Komponente. Da das Geheimnisprinzip aber fordert, nur die benötigten Elemente zu importieren, ist die Nutzung der Wildcard nicht empfohlen. Die benötigten Elemente sollten explizit importiert werden. Eine erlaubte Ausnahme ist der statische Import: In diesem Fall stehen alle statischen Methoden oder Attribute ohne weitere Qualifikation zur Verfügung. Dies ist vor allem für Aufzählungstypen sinnvoll (→ Schritt 7). Listing 21: Statischer Import der Autovermietung [1] Definition eines Pakets interaktion . [2] Statischer Import aller statischen Elemente der Klasse Autovermietung aus dem Paket vermietung . [3] Nutzung der Methode ausleihen aus Klasse Autovermietung . Eine Qualifikation des Pakets ist wegen des statischen Imports nicht mehr notwendig. package interaktion; import static vermietung.Autovermietung.*; public class Interaktion { public static void main(String[] args) { String kennzeichen = ausleihen(12345678); System.out.println(kennzeichen); } } 1 2 3 public static void main(String[] args) { …String kennzeichen = Autovermietung.ausleihen(12345678); … } } 3 <?page no="67"?> 6.4 Gültige und sichtbare Elemente 67 6.4 Gültige und sichtbare Elemente Informationen, die von einer Komponente verwendet werden können, bezeichnet man als (für diese Komponente) gültig. Wenn sie darüber hinaus direkt (d.h. ohne Qualifikation) verwendet werden können, bezeichnet man sie als sichtbar. Eine Besonderheit stellen überdeckte Elemente dar: das sind Elemente, die denselben Namen haben z.B. eine Variable in einer Klasse und in einer Methode (z.B. ein Parameter). In diesem Fall ist in der Methode der Parameter sichtbar, die Variable der Klasse ist aber für die Methode nur gültig, aber nicht sichtbar. Um auf diese Variable zuzugreifen, muss sie qualifiziert werden. Bei statischen Elementen findet das über den Klassennamen statt, bei Objekten findet das über das Schlüsselwort this statt (→ Schritt 7). Im folgenden Beispiel wird eine überdeckte Variable durch Qualifikation sichtbar gemacht. Listing 22: Zugriff auf ein n i cht-sichtbares Element [1] Deklaration der Variablen kreditkarte , die fü r das Objekt gü ltig ist. [2] Deklaration des Parameters kreditkarte , der fü r die Methode setKreditkarte gü ltig ist. [3] Qualifikation der (in dieser Methode ü berdeckten) Variablen [1] mit Hilfe von this . [4] Die qualifizierte Variable, die damit wieder verwendbar ist. [5] Der lokale Parameter kennzeichen . package vermietung; public class Auto { …private int kreditkarte; …public void setKreditkarte(int kreditkarte) { this.kreditkarte = kreditkarte; }… } 1 2 3 4 5 <?page no="68"?> 68 Schritt 6: Sichtbarkeit / Gültigkeit 6.5 Innere Elemente Wie zu Beginn erwähnt, bietet Java die Möglichkeit, innere Komponenten zu erstellen. D.h. Klassen, Methoden, Interfaces und Aufzählungstypen können wiederum Klassen, Interfaces oder Aufzählungstypen enthalten. Es ist allerdings nicht möglich, innere Pakete oder Methoden zu erstellen (letztere indirekt über eine innere Klasse in einer Methode, die eine Methode definiert). Für diese besonderen Fälle gelten weitere Sichtbarkeitsregeln: Äußere Elemente können dabei immer auf Attribute oder Methoden ihrer inneren Elemente zugreifen - unabhängig von deren Zugriffsmodifier. Innere Klassen, Interfaces oder Aufzählungstypen können zusätzlich die Zugriffsmodifier protected oder private besitzen. In diesen Fällen ist die Verwendung für andere Klassen ausserhalb der eigenen Klasse entsprechend eingeschränkt. <?page no="69"?> Schritt 7: Objektorientierte Konzepte <?page no="70"?> 70 Schritt 7: Objektorientierte Konzepte Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um die zentralen Ideen der objektorientierten Programmierung. Welche Schlagwörter lerne ich kennen? Klasse Objekt Konstruktor Interface Enumeration Vererbung Polymorphismus Delegation Wofür benötige ich dieses Wissen? Die objektorientierten Konzepte bilden die Grundlage der objektorientierten Programmierung. Alle „echten“ Java-Programme bauen auf diesen Konzepten auf. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, Klasse und Objekt zu unterscheiden. Üblicherweise müssen Klassen, Interfaces und Aufzählungstypen erstellt und erweitert werden. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1235 <?page no="71"?> 7.1 Klassen und Objekte 71 Gegenüber der herkömmlichen, prozeduralen Programmierung, die die Methoden in den Mittelpunkt stellt, stehen bei der objektorientierten Programmierung die Daten (die „Objekte“) im Mittelpunkt. Die objektorientierte Programmierung setzt konsequent auf das Geheimnisprinzip (→ Schritt 6). 7.1 Klassen und Objekte Zentrale Konzepte der objektorientierten Programmierung sind die Klasse , in der Daten definiert werden, und das Objekt , das auf Basis der Klassendefinition erzeugt wird. Eine Klasse hat zwei Aspekte: statische und nicht-statische Eigenschaften. Statische Eigenschaften einer Klasse: Die Klasse kann eigene Daten und Methoden besitzen - dies sind die statischen Eigenschaften einer Klasse. Sie werden mit dem static -Modifier versehen und üblicherweise Klassenvariablen und Klassenmethoden genannt. Klassenvariablen und -methoden sind für die Klasse selbst und alle ihre Objekte gültig. Eine Klassenvariable existiert pro Klasse ein einziges Mal und hat deshalb nur einen einzigen Wert. Nicht-statische Eigenschaften einer Klasse: Die Klasse ist ein „Bauplan“ für Objekte (auch „Instanzen“ oder „Exemplare“). Die Klasse beschreibt dann die Daten und das Verhalten der zukünftigen Objekte - dies sind die nicht-statischen („non-static“) Eigenschaften einer Klasse. Diese Eigenschaften werden ohne static -Modifier definiert und als Instanzvariablen und Instanzmethoden bezeichnet. Instanzvariablen und -methoden sind nur für die Objekte der Klasse gültig - nicht aber für die Klasse. Pro Objekt wird jeweils eine eigene Instanzvariable angelegt. Eine besondere Instanzmethode (mit abweichender Syntax) ist der Konstruktor: Ein Konstruktor kann ausschließlich bei der Erzeugung eines Objekts verwendet werden. <?page no="72"?> 72 Schritt 7: Objektorientierte Konzepte Der Konstruktor muss denselben Namen wie die zugehörige Klasse haben und besitzt keinen Rückgabetyp, kann aber Parameter besitzen. Der Konstruktor dient zur Initialisierung des gerade erzeugten Objekts. Der Konstruktor kann überladen werden: Es kann mehrere Konstruktoren mit unterschiedlichen Parametern geben. Java stellt automatisch für jede Klasse einen parameterlosen Standardkonstruktor zur Verfügung - wird jedoch ein eigener Konstruktor realisiert, steht der Standardkonstruktor nicht mehr zur Verfügung. Listing 23 zeigt ein Beispiel für eine Klasse mit statischen und nicht-statischen Elementen. Listing 23: Beispiel für Klassen und Instanzelemente [1] Konstante (finale Klassenvariable). [2] Klassenvariable nummer . [3] Instanzvariablen modell und kennzeichen . [4] Konstruktor fü r ein Auto-Objekt. [5] Bei der Initialisierung eines Autos wird ein neues Kennzeichen erzeugt. Da die Klassenvariable nummer nur einmal vorliegt, zä hlt jeder Konstruktoraufruf diese Variable hoch. [6] Instanzmethode eines Auto-Objekts. Ein neues Objekt wird mit Hilfe des Ausdrucks new Konstruktorname (Parameter) erzeugt. Der new -Operator erzeugt das Objekt, der Konstruktor initialisiert es. Das neu erzeugte Objekt ist vom Typ seiner Klasse. Es kann deshalb einer Variablen vom Typ der Klasse zugewiesen werden. public class Auto { private static final String KENNZEICHEN_TEXT = "S-AV"; private static int nummer = 1000; private String modell; private String kennzeichen; …public Auto(String modell, double miete) { this.modell = modell; this.kennzeichen = KENNZEICHEN_TEXT + " " + nummer; nummer++; … } public boolean hatKennzeichen(String kennzeichen){ return this.kennzeichen.equalsIgnoreCase(kennzeichen); }… } 1 2 3 4 5 6 <?page no="73"?> 7.1 Klassen und Objekte 73 Listing 24: Erzeugung eines neuen Objekts [1] Deklaration einer Variablen auto vom Typ Auto. [2] Erzeugung eines neuen Autoobjekts mit dem new-Operator. [3] Initialisierung des neuen Autoobjekts durch den Konstruktor. [4] Nutzung des neu erzeugten Autoobjekts. Methoden, die ausschließlich zum Lesen oder Schreiben von Variablen dienen, werden Getter oder Setter genannt. Sie haben typischerweise die folgende Form: Listing 25: Getter und Setter [1] Deklaration einer (Instanz-)Variablen namens kosten vom Typ double . [2] Deklaration eines Getters: der Methodenname ist getVariablenname . Rü ckgabetyp ist der Typ der Variablen, die Methode besitzt keine Parameter. [3] Rü ckgabe des Variablenwerts. [4] Deklaration eines Setters: der Methodenname ist setVariablenname . Rü ckgabetyp ist void . [5] Die Methode besitzt einen Parameter, der wie die Variable heißt und denselben Typ hat. public class Autovermietung { public void neuesAuto(String modell, double miete){ … Auto auto = new Auto(modell, miete); autos[anzahl] = auto; anzahl++; … }… } 1 2 3 4 public class Auto { …private double kosten; … public double getKosten() { return kosten; } public void setKosten(double kosten) { this.kosten = kosten; }… } 1 2 3 4 5 6 <?page no="74"?> 74 Schritt 7: Objektorientierte Konzepte [6] Zuweisung des Parameters an die Variable. Da die Variable durch den Parameter verdeckt ist (→ Schritt 6), muss sie mit this qualifiziert werden. this beschreibt dabei das gesamte aktuelle Objekt. 7.2 Erweiterung / Vererbung Klassen können durch andere Klassen erweitert werden. Dieser Mechanismus wird üblicherweise Vererbung genannt. Die erbende oder erweiterte Klasse wird Unterklasse genannt, die vererbende oder erweiterte Klasse wird Oberklasse genannt. In Java wird Vererbung durch das Schlüsselwort extends angezeigt. Vererbung wird in der Form definiert: Modifier class Klassenname extends Oberklassenname { Klassenkörper } Im Rahmen der Vererbung ist der default-Modifier nicht sinnvoll und wird im Folgenden nicht mehr betrachtet. Die Vererbung in Java hat die folgenden Merkmale: Eine Unterklasse kann immer nur von einer Klasse erben. Man spricht deshalb von Einfachvererbung . Eine Unterklasse erbt alle Instanzvariablen und alle nicht-privaten Instanzmethoden der Oberklasse. Klassenvariablen, -methoden und Konstruktoren werden nicht vererbt. Unterklassen können neue Daten und Methoden hinzufügen oder geerbte Methoden überschreiben oder redefinieren, aber keine Elemente entfernen („Monotonie der Vererbung“). Eine Unterklasse kann nur auf geerbte public - oder protected -Daten direkt zugreifen. private -Daten werden für die Unterklasse zwar angelegt, können aber nicht direkt von der Unterklasse verwendet werden. In diesem Fall benötigt die Unterklasse einen (geerbten) public -/ protected -Getter von der Oberklasse. public -/ protected -Methoden der Oberklasse können durch Methoden mit der gleichen Signatur in der Unterklasse überschrieben werden (Redefinition, „Overriding“). Auf die ursprüngliche Methode der Oberklasse kann mit super.Methodenname(Parameter) zugegriffen werden. Konstruktoren werden nicht vererbt, der Konstruktor der Oberklasse kann mit super(Parameter) aufgerufen werden - dieser Aufruf muss als erste Anweisung im Konstruktor stehen. <?page no="75"?> 7.3 Abstrakte Klassen und Methoden 75 Die oberste Klasse ist Object . Falls extends in der Klassendefinition weggelassen wird, erbt die Klasse automatisch von Object . Listing 26: Ober- und Unterklassen [1] Definition der Klasse Auto . [2] Definition der Methode ausleihen . [3] Definition der Klasse Lkw ; diese Klasse ist eine Unterklasse der Klasse Auto. [4] Die Methode ausleihen wird in der Unterklasse redefiniert. [5] Mit super wird zunä chst die ursprü ngliche Implementierung ausgefü hrt. [6] Im Anschluss werden zusä tzlich fü r den LKW spezifische Aktionen ausgefü hrt. 7.3 Abstrakte Klassen und Methoden Abstrakte Klassen dienen dazu, Gemeinsamkeiten für spätere Unterklassen zusammen zufassen. Solche Gemeinsamkeiten können Attribute, Methoden oder abstrakte Methoden sein. Abstrakte Klassen oder Methoden werden mit dem Schlüsselwort abstract gekennzeichnet. public class Auto { …protected boolean ausleihen(int geplant, int kreditkarte) { if(ausgeliehen) return false; this.kreditkarte = kreditkarte; ausgeliehen = true; return true; }… } public class Lkw extends Auto { … protected boolean ausleihen(int geplant, int kreditkartenNr) { if(! super.ausleihen(geplant, kreditkartenNr)) return false; this.vergangen = 0; this.geplant = geplant; return true; }… } 1 2 3 4 5 6 <?page no="76"?> 76 Schritt 7: Objektorientierte Konzepte Von abstrakten Klassen können keine Instanzen erzeugt werden. Abstrakte Methoden besitzen keinen Methodenkörper - nur die Signatur und den Rückgabetyp. Abstrakte Klassen werden durch den Modifier abstract gekennzeichnet und in der folgenden Form definiert: Zugriffsmodifier abstract class Klassenname { Klassenkörper } . Abstrakte Klassen dienen zur Darstellung allgemeiner Sachverhalte, die z.T. noch unvollständig sein können und die dann durch Unterklassen konkretisiert und vervollständigt werden. Abstrakte Methoden werden durch den Modifier abstract gekennzeichnet und in der folgenden Form definiert: Zugriffsmodifier abstract (Rückgabetyp | void) Methodenname (Parameterliste); Abstrakte Methoden können nur in abstrakten Klassen definiert werden. Ist die Unterklasse einer solchen Klasse nicht abstrakt, müssen alle in der Oberklasse definierten, abstrakten Methoden in dieser Klasse implementiert werden, d.h. es müssen Methoden mit der durch die abstrakte Klasse vorgegebenen Signatur und Rückgabetyp programmiert werden. Listing 27: Abstrakte Klassen und Methoden [1] Definition einer abstrakten Klasse. [2] Definition einer abstrakten Methode. 7.4 Schnittstellen / Interfaces Interfaces sind Komponenten, die ausschließlich abstrakte Methoden definieren. Sie werden durch das Schlüsselwort interface gekennzeichnet und in der folgenden Form definiert: Zugriffsmodifier interface { Methodendeklarationen } . Das Schlüsselwort abstract für die definierten Methoden ist optional und wird üblicherweise weggelassen. Anders als Klassen können Interfaces von mehreren anderen Interfaces erben. Seit Java 8 ist es möglich, auch konkrete Implementierung im Interface anzulegen - diese werden mit dem Modifier default gekennzeichnet. public abstract class Auto { …protected abstract boolean istEin(int gruppe); … } 2 <?page no="77"?> 77 Klassen können ein Interface implementieren. Anders als bei der Vererbung können Klassen auch mehrere Interfaces implementieren. Dies wird durch das Schlüsselwort implements angezeigt. In diesem Fall muss die implementierende Klasse alle Methoden mit der durch das Interface vorgegebenen Signatur und Rückgabetyp implementieren. Listing 28: Interface und implementierende Klasse [1] Definition des Interfaces Buchbar [2] Definition von zwei abstrakten Methoden [3] Definition der Klasse WiFi . [4] Diese Klasse implementiert das Interface Buchbar . [5] Dazu muss sie die im Interface definierten Methoden realisieren. Interfaces können, wie Klassen auch, als Typ zur Variablen- oder Parameterdeklaration verwendet werden. Objekte einer Klasse, die ein Interface implementieren, können auch einer Variablen vom Typ des Interfaces zugewiesen werden - in diesem Fall sind für die Variable nur die im Interface definierten Methoden gültig. Interfaces dienen vor allem dazu, auf der einen Seite eine Implementierungsvorgabe zu machen (durch die abstrakten Methoden) und auf der anderen Seite bereits Programmteile zu schreiben, die Objekte von diesem Interface-Typ nutzen. public interface Buchbar { public float zurueckgeben(); public void einTagVergeht(); } public class WiFi implements Buchbar{ private static final float KOSTEN = 2; private float kosten = 0; public float zurueckgeben() { float kosten = this.kosten; this.kosten = 0; return kosten; } public void einTagVergeht() { kosten = kosten + KOSTEN; }… } 1 2 3 4 5 7.4 Schnittstellen / Interfaces <?page no="78"?> 78 Schritt 7: Objektorientierte Konzepte Dieses Vorgehen wird oft „Design by Contract“ genannt: das Interface stellt einen Vertrag dar, gegen den programmiert wird. 7.5 Aufzählungstypen / Enumerations Seit der Version 1.5 besteht in Java die Möglichkeit, Aufzählungstypen („Enumerations“) zu definieren. Ein Aufzählungstyp ist eine Liste von Konstanten. Technisch sind Aufzählungstypen eine Klasse (nämlich eine Unterklasse der vordefinierten Klasse java.lang.Enum ) - allerdings haben sie eine an ihre Aufgabe angepasste Syntax. In der Programmierung besteht oft Bedarf an Datentypen mit einer eigenen Wertemenge, meist um bestimmte Zustände oder bestimmte Eigenschaften zu kennzeichnen. Für die Autovermietung wäre das z.B. der Fahrzeugtyp: PKW/ Kombi, LKW, Premium-Modell. In der Programmierung werden diese Werte üblicherweise zugwiesen und abgefragt, um z.B. festzustellen, ob ein Auto vom Typ PKW/ Kombi ist. In C-ähnlichen Sprachen (wie auch Java) hat es sich eingebürgert, dass solche Werte als numerische Konstanten (vom Typ int ) definiert werden, also z.B.: Listing 29: Definition eines Wertetyps [1] Definition eines Wertetyps mit Hilfe von numerischen Konstanten [2] Variable, die diese Konstanten annehmen kann. [3] Methode, die die Variable auswertet. Indem man symbolische Konstanten (im Gegensatz zu direkten numerischen Konstanten) nutzt, wird das Programm lesbarer, nichtsdestotrotz ist es anfällig für Fehler und Missbrauch: Grundsätzlich könnten der Variablen gruppe public class Auto { public static final int PKW_KOMBI = 0; public static final int LKW = 1; public static final int PREMIUM = 2; …private String kennzeichen; private int gruppe; …public String toString(){ String grStr; switch(gruppe){ case PKW_KOMBI: grStr = "[PKW/ Kombi]"; break; case LKW: grStr = "[LKW]"; break; case PREMIUM: grStr = "[Premium-Modell]"; break; default : grStr = "<unbekannte Gruppe>"; break; }return modell + " " + grStr+ " (" + kennzeichen + ")"; } } 1 2 3 <?page no="79"?> 7..5 Aufzählungstypen / Enumeration 79 beliebige Zahlen zugewiesen werden, was zu undefiniertem Verhalten führen kann, oder man kann versucht sein, in den Zahlen Eigenschaften zu codieren, die dann nur wieder dekodiert werden können, wenn die entsprechende Information zur Dekodierung verfügbar ist. (In unserem Beispiel könnte das z.B. der Mietpreis für eine bestimmte Fahrzeuggruppe sein - diese Information müsste mindestens als Kommentar hinterlegt sein.) Aufzählungstypen vermeiden diese Probleme: Sie definieren für jeden Wertebereich einen eigenen Typ, der nicht mit anderen Typen gemischt werden kann. Zusätzlich können für die Werte weitere Attribute definiert werden. Im einfachsten Fall wird mit Hilfe der Deklaration public enum Aufzählungstyp{ WERT1, WERT2, … } ein Typ mit einer Reihe von Werten definiert. Es ist nun möglich, Variablen von dem Aufzählungstyp zu deklarieren und dieser Variablen einen der im Aufzählungstyp definierten Werte zuzuweisen. Die Wertenamen eines Aufzählungstyps werden gemäß Java-Konvention groß geschrieben, da es sich um Konstanten handelt. Listing 30: Definition ei n es Aufzählungstyps [1] Definition eines Aufzä hlungstyps mit drei Werten. [2] Variable, die diese Werte annehmen kann. Aufzählungstypen können - wie normale Java-Klassen - mit beliebigen Attributen und Methoden versehen werden. Die einzelnen Werte eines Aufzählungstyps sind technisch gesehen konstante Objekte von der Klasse des Aufzählungstyps. Mit den Attributen können diesen Konstanten zusätzliche Werte mitgegeben werden. Belegt werden die Attribute üblicherweise mit einem privaten Konstruktor, der nicht mehr explizit erscheint, sondern bei der Definition der Werte implizit durch eine Parameterliste aufgerufen wird. Listing 31 zeigt, wie der Aufzählungstyp für eine Gruppe um ein Attribut für eine Standardmiete ergänzt werden kann. public enum Gruppe { PKW_KOMBI, LKW, PREMIUM } public class Auto { …private Gruppe gruppe; … } 1 2 <?page no="80"?> 80 Schritt 7: Objektorientierte Konzepte Listing 31: Aufzählungstyp mit Attributen [1] Definition der Werte: als Parameter werden die Werte angegeben, die dem Konstruktor zur Initialisierung ü bergeben werden - die Parameter mü ssen mit der Signatur des Konstruktors ü bereinstimmen. Die Werteliste muss in diesem Fall mit einem ; abgeschlossen werden. [2] Das Attribut der Konstanten. [3] Konstruktor, der das Attribut setzt. Der Konstruktor eines Aufzä hlungstyps ist immer private . Er wird implizit bei der Deklaration aufgerufen. [4] Getter zum Zugriff auf den Attributwert, z.B . LKW.getStandard Miete(); 7.6 Polymorphismus Polymorphismus in objektorientierten Programmiersprachen bedeutet, dass ein Objekt nicht unbedingt unter seinem eigenen Typ auftreten muss, sondern auch als ein anderer Typ auftreten kann. In Java kann das der Typ eines implementierten Interfaces oder einer Oberklasse sein. In diesen Fällen wird das Objekt einer Variablen zugewiesen oder an einen Parameter gebunden, der nicht gleich dem Objekttyp ist, sondern eine Oberklasse oder ein implementiertes Interface des Objekts ist. public enum Gruppe { PKW_KOMBI(29.99), LKW(19.99), PREMIUM(99.99); private double standardMiete; private Gruppe(double miete) { this.standardMiete = miete; } public double getStandardMiete() { return standardMiete; } } 1 2 3 4 public abstract class Auto { … private Buchbar[] zubehoer = new Buchbar[MAX]; … protected void einTagVergeht(){ if(ausgeliehen){ 1 <?page no="81"?> 7.7 Best Practices der objektorientierten Programmierung 81 Listing 32: Polymorphismus [1] Definition eines Felds vom Typ Buchbar . Dieses Feld kann alle Objekte aufnehmen, deren Klasse das Interface Buchbar implementieren, z.B. die Klasse WiFi (s.o.). [2] Das Interface garantiert, dass die implementierenden Klassen die Methode einTagvergeht zur Verfü gung stellt - sie kann deshalb hier aufgerufen werden, ohne das konkrete Objekt zu kennen. Polymorphismus erlaubt die Programmierung eines allgemeinen Verhaltens (z.B. auf Basis eines Interfaces), das dann von konkreten oder implementierenden Klassen genutzt wird - diese Klassen müssen zur Zeit der Formulierung des allgemeinen Verhaltens noch nicht bekannt sein. 7.7 Best Practices der objektorientierten Programmierung Objektorientierte Programmierung bietet ein hohes Potenzial, wiederverwendbare und leicht wartbare Programme zu erstellen. Um das zu erreichen, sollten die folgenden Best Practices beachtet werden: Geheimnisprinzip umsetzen : Daten sollten immer private sein. Der direkte Zugriff auf Daten sollte auf keinen Fall möglich sein, der Zugriff über Getter und Setter sollten soweit wie möglich vermieden werden - ein Objekt sollte vor allem fachliche Funktionen zur Verfügung stellen. Fachlich arbeiten: Klassen und Objekte sollten immer logisch/ fachlich zusammengehörige Elemente zusammenfassen. Klassen, die nur Getter und Setter zur Verfügung stellen, sind reine Datencontainer ohne fachliche Funktion. Konsequente Delegation: Funktionen sollten immer dort ausgeführt werden, wo sie auch fachlich angesiedelt sind - ggfs. müssen aufrufende Objekte die Aufrufe delegieren. Generalisierung/ Spezialisierung : Unterklassen, die gebildet werden, um auf die Daten oder Methoden der Oberklasse geschickt zuzugreifen, sind technisch möglich, sollten aber vermieden werden. Unterklassen sollten immer fachlich/ logisch spezieller als ihre Oberklassen sein. for(int i = 0; i < zubehoer.length; i++) if(zubehoer[i] ! = null) zubehoer[i].einTagVergeht(); … } }… } 2 <?page no="82"?> 82 Schritt 7: Objektorientierte Konzepte Verwendung genereller Eigenschaften : Sind Unterklassen eine Spezialisierung der Oberklassen, sollte es möglich sein, auf den direkten Zugriff von Daten der Oberklassen zu verzichten - dies sollten die fachlichen Methoden der Oberklasse machen. Unterklassen sollten dann die fachlichen Methoden der Oberklasse redefinieren, die ursprüngliche Methode mit super aufrufen und dann den speziellen Teil anschließen. <?page no="83"?> Schritt 8: Ausnahmen / Exceptions <?page no="84"?> 84 Schritt 8: Ausnahmen / Exceptions Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? Es gibt zwei Kategorien von Problemen: schwere Fehler, Errors, die nicht behoben werden können, und einfache Fehler, Exceptions, die von einer Anwendung behandelt und behoben werden können. Welche Schlagwörter lerne ich kennen? Error Exception try-and-catch finally Weitergabe (throws) Fehlerbehandlung Fehlermeldung Wofür benötige ich dieses Wissen? Jede Anwendung sollte auftretende Exceptions behandeln, um zuverlässig und stabil ablaufen zu können. Die Kenntnisse über die Ursachen und die Hierarchie der Exceptions sind notwendig für eine effiziente Fehlerbehandlung. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? Errors und Exceptions müssen unterschieden werden. Die beiden Prinzipien der Fehlerbehandlung (1) try-and-catch und (2) throws werden normalerweise abgefragt. Beim try-and-catch-Ansatz müssen die Bedingungen der catch-Klausel vom Speziellen zum Allgemeinen gehen. Eine Fehlerbehandlung ist prinzipiell nach dem gleichen Schema aufgebaut und kann auf die vorgegebenen Methoden der entsprechenden Klassen zugreifen. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1236 <?page no="85"?> 8.1 Ausnahmen auslösen und behandeln 85 8.1 Ausnahmen auslösen und behandeln Programmierfehler und die sich daraus häufig ergebenden Abstürze der Anwendung oder sogar des Betriebssystems hat sicherlich jeder Benutzer schon einmal erlebt. Bestimmte Fehler lassen sich dabei durch eine „defensive Programmierung“ durchaus vermeiden, indem z.B. vor dem Objektzugriff geprüft wird, ob das Objekt bereits initialisiert ist oder ob die übergegebenen Parameterwerte gültig sind. Abgesehen von diesen vermeidbaren Fehlern gibt es jedoch auch eine ganze Reihe von Fehlern, die nicht im Voraus abgefangen werden können. Es handelt sich hierbei z.B. um Fehleingaben durch Benutzer, Probleme mit dem Dateisystem oder dem Netzwerk und den Ausfall von Hardware. Für diese Kategorie von Fehlern ist die in Java bereits integrierte Fehlerbehandlung (in Form der Exceptions) geeignet. Aus diesem Grund müssen systematisch alle möglichen Fehlerfälle berücksichtigt werden, da in der Regel auftretende unbehandelte Fehler zum Absturz der Anwendung führen. In Java stellt die Klasse Throwable die Basis für die beiden Klassen Error und Exception dar, die wiederum alle weiteren konkreten Fehlerklassen ableiten. Throwable kann dabei Informationen über die Fehlerursache, die Fehlermitteilung usw. enthalten. Es gibt also zwei Kategorien von Fehlerklassen: Ein Error gehört zur Klasse der schwerwiegenden Fehler, die zum Abbruch der Anwendung führen, da sie prinzipiell nicht behandelbar sind. Eine Exception gehört zur Klasse der behebbaren Fehler, die entweder gelöst oder weitergeleitet werden können. Die Exception bietet als Objekt in der Regel Methoden an, die Informationen über die Ursache, den aktuellen Stand und die Fehlermeldung liefern, die dann von der Fehlerbehandlung ausgewertet werden können. Es gibt bereits eine große Zahl von verfügbaren Exception-Klassen, die alle wesentlichen Aspekte abdecken und sich von diesen beiden Hauptklassen Error und Exception ableiten. Zusätzlich können für anwendungsspezifische Anforderungen noch eigene Exception-Klassen definiert werden. Typische Fälle von Errors sind zum Beispiel: OutOfMemoryError tritt auf, falls die JVM nicht mehr über ausreichend Hauptspeicher verfügt. <?page no="86"?> 86 Schritt 8: Ausnahmen / Exceptions StackOverflowError : bedeutet, dass ein Überlauf des Stacks vorliegt, weil z.B. zu viele Methoden rekursiv aufgerufen wurden. InternalError : interner Fehler der JVM ist aufgetreten. UnknowError : unbekannter JVM-Fehler wurde ausgelöst. Bekannte Beispiele für Exceptions sind: NullPointerException : das Objekt, auf das man versucht zu zugreifen ist nicht initialisiert, es ist also null . IndexOutOfBoundsException : beim Zugriff auf ein Array wird ein Index verwendet, der außerhalb des gültigen Wertebereichs liegt. NumberFormatException : tritt beim Parsen einer Zeichenkette auf, falls diese kein gültiges Zahlenformat darstellt. IOException : fehlerhafter Zugriff auf die Ein- oder Ausgabe, z.B. weil eine Datei nicht existiert. Bei den Exceptions wird weiterhin noch zwischen geprüften („checked“) und ungeprüften („unchecked“) Exceptions unterschieden. Falls eine geprüfte Exception in einem Codeblock geworfen werden könnte, so muss diese entweder deklariert (per throws ) oder aufgefangen (per try-and-catch ) werden. Ungeprüfte Exceptions hingegen können behandelt (oder deklariert werden) - müssen aber nicht. In dieser Hinsicht ist die Klasse RuntimeException wichtig, denn sie und alle daraus abgeleiteten Klassen gehören zu den ungeprüften Exceptions, alle sonstigen zu den geprüften Exceptions. Sie wird direkt von der Klasse Exception abgeleitet und umfasst eine Vielzahl von weiteren Exceptions, denen alle Java-Entwickler während ihrer Programmierung schon begegnet sind, wie z.B. die Null- PointerException oder die IndexOutOfBoundsException . Grundsätzlich sind alle ungeprüften Exceptions durch defensive Programmierung vermeidbar. Tritt eine solche RuntimeException auf, liegt deshalb immer ein Programmierfehler vor. Dagegen liegt der Fehler bei einer geprüften Ausnahme immer in der Umgebung, die nicht oder nur schwer zu kontrollieren ist. Für die Behandlung von Exceptions gibt es prinzipiell immer die beiden Alternativen, die sich aber gegenseitig ausschließen: [1] den try-catch-Block , der die Exception auffängt und bearbeitet , oder [2] das throws -Konstrukt, das die Exception entgegen nimmt und dann weiterleitet . <?page no="87"?> Im ersten Ansatz wird die Exception behandelt, indem alle vorhandenen Fehlerbehandlungszweige mit der vorliegenden Exception verglichen werden und beim ersten passenden Vergleich die zugehörigen Anweisungen ausgeführt werden. Im Regelfall liegt hierbei eine Hierarchie der Fehlerbehandlungszweige vor, mit zuerst den spezifischen Exception-Klassen und dann den allgemeineren, von denen die spezifischen abgeleitet sind. Es wird dann - vom Spezifischen zum Allgemeinen - die Fehlerbehandlung ausgeführt, die als erste zum tatsächlichen Fehler passt. Im zweiten Ansatz werden die unbehandelten Exceptions weitergeleitet, ohne sicherzustellen, dass sie später behoben werden. Im schlimmsten Fall kann die Exception also beliebig oft weitergereicht werden, ohne aufgefangen und behandelt zu werden, was automatisch zum Abbruch der Anwendung führen würde. Im Folgenden wird der erste Ansatz vertieft, bei dem die Exceptions aufgefangen und behandelt werden. Der try-catch-Block besteht aus drei Teilen, von denen die beiden ersten verpflichtend sind: try : alle Anweisungen, bei denen prinzipiell die Exceptions aufgefangen werden sollen, müssen sich innerhalb der geschweiften Klammern des try-Blocks befinden. catch : alle aufzufangenden Exceptions müssen explizit aufgeführt werden. finally : dieses Konstrukt ist optional; falls es vorhanden ist, wird es in jedem Fall ausgeführt, selbst dann, wenn keine Exception geworfen wird. Syntaktisch ist das try-catch-Konstrukt folgendermaßen aufgebaut: Listing 33: Syntaktischer Aufbau des try-and-catch-Blocks try{ <Anweisungen, die zur Ausnahme führen können> }catch(Ausnahmeklasse1 e1){ <Anweisungen, zur Behandlung der ersten Ausnahme> }catch(Ausnahmeklasse2 e2){ <Anweisungen, zur Behandlung der zweiten Ausnahme> }…finally{ <Anweisungen, zum Abschluss; im Erfolgs- und Fehlerfall> } 1 2 3 4 5 6 7 8.1 Ausnahmen auslösen und behandeln 87 <?page no="88"?> 88 Schritt 8: Ausnahmen / Exceptions Grundsätzlich hat ein try-catch-Block folgenden Aufbau/ Ablauf: [1] Das Schlü sselwort try umschließt (mit einer ö ffnenden und spä ter mit einer schließenden Klammer) [2] einen Java-Codeblock, dessen Anweisungen zur Laufzeit auf Exceptions geprü ft werden. [3] Fü r jedes catch wird eine Exception-Klasse angegeben, die mit der geworfenen Exception verglichen wird. Im Fall einer UÜ bereinstimmung mit der ersten catch -Regel [4] werden die Anweisungen zur ersten Fehlerbehandlung ausgefü hrt, [5] ansonsten wird die geworfene Exception mit der zweiten catch -Regel verglichen, und im positiven Fall [6] werden die Anweisungen zur zweiten Fehlerbehandlung ausgefü hrt. [7] finally ist optional und markiert den Codeblock, der in jedem Fall durchgefü hrt wird, selbst wenn keine Exception ausgelö st wurde. Das folgende Listing zeigt die Verwendung an einem konkreten Beispiel, indem eine Zeichenkette nach einem Integer-Wert geparst wird. Die bereits verfügbare Zeichenkette s kann dabei aus einer Datei oder einer Benutzerschnittstelle ausgelesen worden sein. Listing 34: Parsen eines ganzzahligen Werts in der Zeichenkette [1] try definiert den zu ü berwachenden Java-Code, d.h. Zeile [2] [2] es wird versucht, die Zeichenkette in einen Integer-Wert zu ü berfü hren, indem ein ganzzahliger Wert geparst wird; [3] die catch -Regel greift, falls eine NumberFormatException geworfen wird [4] dann wird der aktuelle Programmstand (als Stack-Trace) ausgegeben. [5] Diese catch -Regel greift, falls eine allgemeine Exception (z.B. durch weitere Anweisungen in [2]) ausgelö st wurde. [6] Hier findet dann die zugehö rige Fehlerbehandlung statt. String s = …; try { int i = Integer.parseInt(s); / / weitere Anweisungen }catch(NumberFormatException e){ e.printStackTrace(); / / hier z.B. erneutes Einlesen eines Zeichens }catch(Exception e){ / / mögliche weitere Fehlerbehandlung } 1 2 3 4 5 6 <?page no="89"?> Jedes Exception-Objekt bietet eine Reihe von Methoden an, die kurz beschrieben werden: toString() : Ausgabe des Exception-Objekts als Zeichenkette getCause() : Zugriff auf die Fehlerursache getMessage() : Ausgabe der Fehlermeldung printStackTrace() : Ausgabe des aktuellen Programmablaufs und -status Im Fall, dass eine Exception nicht behandelt, sondern „nur“ delegiert wird, ist dies im Kopf einer Methode mittels des Schlüsselworts throws , gefolgt von der oder den Ausnahmeklassen, zu deklarieren. In Abwandlung des vorhergehenden Beispiels kann die NumberFormat- Exception in der folgenden Methode verarbeiten deklariert werden, wenn der (bisherige) Code nicht von einem try-catch-Block umgeben wird. Listing 35: Delegation einer Ausnahme Neben der Nutzung von vorhandenen Exception-Klassen ist es natürlich auch möglich, eigene Klassen zu definieren, die von den bereits verfügbaren Klassen Exception und RuntimeException abgeleitet werden. Best Practices bei der Nutzung von Exceptions: RuntimeExceptions sollten nicht abgefangen, sondern durch defensive Programmierung vermieden werden. Exceptions sollten lokal (d.h. dort, wo sie auftreten) und im Kontext behandelt werden. Die Weiterleitung der Exception sollte eine Ausnahme bleiben, da diese ansonsten sehr leicht zu (längeren) Ketten von Weiterleitungen der Exceptions führen und somit eine sinnvolle Behandlung kaum noch möglich ist. 8.1 Ausnahmen auslösen und behandeln 89 <?page no="91"?> Schritt 9: Zeichenketten / Strings <?page no="92"?> 92 Schritt 9: Zeichenketten / Strings Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? Das Wesentliche über Zeichenketten und die entsprechenden Klassen String und StringBuilder . Es geht darum, Zeichenketten zu erzeugen, zu vergleichen und daraus einzelne Zeichen oder Teilzeichenketten zu extrahieren, zu löschen oder einzufügen. Welche Schlagwörter lerne ich kennen? Zeichenkette String StringBuilder Extraktion von Zeichen Umwandlung von/ in String Löschen von Zeichen Einfügen von Zeichen Wofür benötige ich dieses Wissen? Die Klasse String gehört zu den wichtigsten und am häufigsten verwendeten Klassen in Java, deshalb ist die Kenntnis dieses Datentyps eine wichtige Voraussetzung für die Arbeit mit Java. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? Die Unterschiede zwischen den Klassen String und StringBuilder , sowie die Anwendung der unterschiedlichen Methoden zum Vergleich und zur Umwandlung von Zeichenketten. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1237 <?page no="93"?> 9.2 Erzeugung von Strings 93 Die Klasse String ist kein einfacher Datentyp, sondern eine Java- Klasse für die Bearbeitung von Zeichenketten. Sie gehört sicherlich zu den am häufigsten benutzten Klassen und ist somit unumgänglich für die Programmierung. Außerdem lassen sich an dieser Klasse bereits die einfachen Konzepte zum Aufruf von Methoden studieren und einüben. Die Klasse StringBuilder ergänzt String , da sie über komplementäre Methoden verfügt, die in String nicht zu finden sind. 9.1 Die Klassen String und StringBuilder Die Klasse String enthält nur fixe, d.h. nicht zu verändernde Zeichenketten, während die Klasse StringBuilder variable, d.h. veränderbare Zeichenketten umfasst. Aus der unterschiedlichen Ausrichtung der beiden Klassen String und String- Builder ergibt sich auch die differenzierte Ausstattung mit Methoden. Die Klasse String verfügt nur über Methoden, die den Inhalt der Zeichenketten nicht verändern, während StringBuilder hingegen nur verändernde Methoden hat. Die Klasse String verfügt über Methoden: zur Extraktion von Zeichen und Teilzeichenketten zum Vergleich von Zeichenketten zur Umwandlung von einfachen Datentypen in Zeichenketten (die Rückumwandlung erfolgt mittels Wrapper-Klassen) Die Klasse StringBuilder umfasst Methoden: zur Löschung von Zeichen und Teilzeichenketten zum Einfügen von Zeichen und Zeichenketten In den folgenden Abschnitten werden die Klassen String und StringBuilder vorgestellt und erörtert. 9.2 Erzeugung von Strings Der erste Schritt besteht in der Erzeugung einer Zeichenkette, was am einfachsten dadurch geschieht, indem der Text in doppelte Anführungszeichen gesetzt und einer Variablen zugewiesen wird. Listing 36: Erzeugung einer Zeichenkette String s = "Hallo Welt"; <?page no="94"?> 94 Schritt 9: Zeichenketten / Strings Eine leere Zeichenkette (dargestellt durch "" ) ist zu unterscheiden von einer nicht-initialisierten Variablen, die den Wert null hat. Ebenso ist die Zeichenkette, die nur aus einem Leerzeichen besteht (dargestellt durch " " ), unterschiedlich zur leeren Zeichenkette ( "" ). 9.3 Vergleich von Strings Der Vergleich von Zeichenketten gehört zu den entscheidenden Methoden der Klasse String . Eine Besonderheit ist hierbei, dass es hierfür mehrere verfügbare Methoden gibt, die jeweils unterschiedliche Voraussetzungen und Ziele haben und dementsprechend auch andersgeartete Ergebnisse liefern. Folgende Arten des Vergleichs sind möglich: == Prüfung der Identität der (Objekte der) Zeichenketten, equals(s) Prüfung der Inhalte der Zeichenketten, equalsIgnoreCase(s) Prüfung der Inhalte der Zeichenketten, unabhängig von der Klein- und Großschreibung, compareTo(s) Prüfung, ob die Inhalte der Zeichenkette (lexikalisch) kleiner, gleich oder größer als eine andere ist. Die ersten drei Fälle liefern dabei einen Booleschen Rückgabewert, während der letzte Vergleich als Rückgabewert einen negativen ganzzahligen Wert, Null oder einen positiven ganzzahligen Wert ergibt. Listing 37 zeigt Beispiele für Anwendung der vier Vergleichsoperationen. Listing 37: Anwendung der vier Vergleichsoperationen [1] Beim Vergleich mit == wird geprü ft, ob die beiden Referenzen auf die Objekte, d.h. die beiden Speicheradressen, die auf die Zeichenketten verweisen, dieselben sind. Diese Methode geht ü ber die ü bliche inhaltliche Gleichheit hinaus, denn zwei Zeichenketten kö nnten zwar inhaltlich gleich sein, aber an unterschiedlichen Speicheradressen liegen, und sie wä ren in diesem Fall unterschiedlich. String s1, s2; boolean ergebnis; int vergleich; …ergebnis = (s1 == s2); ergebnis = s1.equals(s2); ergebnis = s1.equalsIgnoreCase(s2); vergleich = s1.compareTo(s2); 1 2 3 4 <?page no="95"?> 9.4 Extraktion von Zeichen oder Teilstrings 95 [2] Beim Vergleich mit equals() wird die ü bliche inhaltliche Gleichheit geprü ft, d.h. die Inhalte der beiden Zeichenketten mü ssen identisch sein, um gleich zu sein. Im Unterschied zum ersten Fall wä ren sie aber selbst dann noch gleich, selbst wenn sie an zwei unterschiedlichen Speicheradressen liegen. [3] Beim Vergleich mit equalsIgnorecase() wird die inhaltliche Gleichheit abgemildert, denn die Zeichenketten kö nnen in Klein- und Großschreibung voneinander abweichen und trotzdem noch gleich sein. [4] Beim Vergleich mit compareTo() werden zwei Zeichenketten lexikalisch miteinander verglichen. Dies bedeutet, dass das erste Zeichen der ersten Zeichenkette mit dem ersten Zeichen der zweiten Zeichenkette verglichen wird. Ist es kleiner, wird eine negative Zahl zurü ckgegeben, ist es grö ßer, wird eine positive Zahl zurü ckgegeben, und wenn beide Zeichen gleich sind, wird das nä chste Zeichen untersucht usw. Falls beide Zeichenketten inhaltlich gleich sind, ist der Rü ckgabewert 0. In der Übersicht des lexikalischen Vergleichs ergibt sich somit: s1.compareTo(s2) Rückgabewert Beispiel kleiner negativ (z.B. -1) s1= ″ 12 ″ , s2= ″ AB ″ größer positiv (z.B. 1) s1= ″ Ab ″ , s2= ″ AB ″ gleich 0 s1= ″ AB ″ , s2= ″ AB ″ Tab. 10: Lexikalischer Vergleich zweier Zeichenketten Der lexikalische Vergleich basiert auf der (An-)Ordnung der Zeichen innerhalb des Unicodebzw. ASCII-Zeichensatzes. Dies hat zur Konsequenz, dass die nichtsichtbaren Zeichen am kleinsten sind, die Zahlen kleiner als die Buchstaben sind, die Großbuchstaben kleiner als die Kleinbuchstaben sind. 9.4 Extraktion von Zeichen oder Teilstrings Häufig geht es aber nur darum, ein Zeichen oder eine Teilzeichenkette an einer bestimmten Position innerhalb der Zeichenkette zu extrahieren. Dabei muss beachtet werden, dass (wie bei Arrays auch) die Zählung der Zeichen bei 0 beginnt. Die folgenden Methoden übernehmen diese Aufgaben: substring(i,j): String gibt die Zeichenkette von der Position i bis zur Position j-1 zurück (und nicht, wie vielleicht erwartet, bis j). <?page no="96"?> 96 Schritt 9: Zeichenketten / Strings substring(i): String gibt die Zeichenkette von der Position i bis zum Ende der Zeichenkette zurück. Die Methode verhält sich erwartungskonform und liefert die Zeichen von der Position i bis zum Ende. charAt(i): char gibt das Zeichen an der Position i zurück. Listing 38: Extrak t ion eines Zeichens und einer Teilzeichenkette [1] Das Zeichen an der Position 1 wird ermittelt. [2] Die Zeichenkette von der Position von 0 bis 4 wird zurü ckgegeben - der zweite Index muss dazu um eins hö her sein, also 5. [3] Die Zeichenkette ab der Position 6 bis zum Ende wird zurü ckgegeben. 9.5 Umwandeln von Strings Eine lästige, aber häufige Aufgabe ist die Entfernung von Leerzeichen zu Beginn und am Ende des Wortes oder des Satzes. Hierfür bietet sich die Methode trim an, die das automatisch erledigt und eine bereinigte Version der Zeichenkette als Rückgabewert liefert (das ursprüngliche Original wird also nicht verändert). Listing 39: Bereinigung von führenden oder folgenden Leerzeichen bei einer Zeichenkette Zwei weitere nützliche Methoden sind toLowerCase und toUpperCase , die alle Buchstaben der Zeichenkette in die Klein- oder Großschreibung überführt. Listing 40: Umwandlung einer Zeichenkette in Klein- und Großbuchstaben [1] Umwandlung in Kleinbuchstaben mittels toLowerCase() . [2] Umwandlung in Großbuchstaben mittels toUpperCase() . String s = " Hallo Welt "; System.out.println(s.trim()); / / → "Hallo Welt" String s = "Hallo Welt"; System.out.println(s.toLowerCase()); / / → "hallo welt" System.out.println(s.toUpperCase()); / / → "HALLO WELT" 1 2 String s = "Hallo Welt"; System.out.println(s.charAt(1)); / / → 'a' System.out.println(s.substring(0, 5)); / / → "Hallo" System.out.println(s.substring(6)); / / → "Welt" 1 2 3 <?page no="97"?> 9.6 Umwandlung von elementaren Datentypen in Strings 97 Eine letzte nützliche Methode ist split(String delimiter): String[] . Diese Methode, angewandt auf einen String, teilt den String überall dort auf, wo der delimiter auftritt. Ergebnis ist ein Array mit den entstandenen Teilstrings. 9.6 Umwandlung von elementaren Datentypen in Strings Die Umwandlung eines einfachen Datentyps, z.B. einer Zahl in die entsprechende Zeichenkette, ist eine typische Aufgabe und wird von der statischen Methode valueOf der Klasse String für alle einfachen Datentypen übernommen, da sie eine mehrfach überladene Methode ist. Für die umgekehrte Richtung, d.h. die Umwandlung des Inhalts einer Zeichenkette in den entsprechenden einfachen Datentyp gibt es jeweils eine Methode der zugehörigen Wrapper-Klasse. Die Wrapper-Klasse existiert für jeden einfachen Datentyp und verfügt über eine statische parse -Methode, welche die Umwandlung vornimmt. Eine Übersicht der Wrapper-Klassen und der entsprechenden parse-Methoden findet sich im folgenden Absatz. Zur Systematik der Benennung sei noch kurz darauf hingewiesen, dass die Wrapper-Klasse in der Regel so wie der einfache Datentyp heißt (Ausnahme: Integer), aber - gemäß der Java-Konvention - mit einem Großbuchstaben beginnt und der Name der parse-Methode aus parse und dem entsprechenden Datentyp besteht. Einfacher Datentyp Aufruf der Wrapper-Klasse mit parse- Methode für einen String s boolean Boolean.parseBoolean(s) byte Byte.parseByte(s) short Short.parseShort(s) int Integer.parseInt(s) long Long.parseLong(s) float Float.parseFloat(s) double Double.parseDouble(s) Tab. 11: Übersicht Wrapper-Klassen und parse-Methoden <?page no="98"?> 98 Schritt 9: Zeichenketten / Strings Listing 41: Umwandlung von einem elementaren Datentyp in einen String (und umgekehrt) [1] Umwandlung einer ganzen Zahl in einen String. [2] Umwandlung eines Booleschen Wertes in einen String. [3] Umwandlung eines Zeichens in einen String. [4] Umwandlung eines Strings in eine ganze Zahl. 9.7 Verarbeitung von Zeichenketten mit der Klasse StringBuilder Für die Erzeugung eines StringBuilder -Objekts muss bereits eine Zeichenkette vorliegen, die dann in einen StringBuilder umgewandelt wird. Ein String- Builder -Objekt wird wie üblich durch den new -Operator und einem Konstruktor mit dem String erzeugt. Der ursprüngliche String wird kopiert und bleibt selbst unverändert, z.B.: Listing 42: Erzeugung eines StringBuilder -Objekts In der Regel müssen die Inhalte eines StringBuilders wieder in den Typ String umgewandelt werden, um weiter im Programm verwendet zu werden, da die meisten anderen Methoden nur Variablen der Klasse String bearbeiten können. Hierfür wird die bekannte toString() -Methode verwendet. Listing 43: Rüc k gabe des Inhalts eines StringsBuilders-Objekts als String StringBuilder sb = new StringBuilder("Hallo Welt"); String iStr = String.valueOf(1); String bStr = String.valueOf(true); String cStr = String.valueOf('a'); try{String s ="1234"; int i = Integer.parseInt(s); } catch (NumberFormatException e){ System.out.println("Fehler aufgetreten"); } 1 2 3 4 StringBuilder sb = new StringBuilder("Hallo Welt"); String s = sb.toString(); System.out.println(s); / / → "Hallo Welt" <?page no="99"?> 9.7 Verarbeitung von Zeichenketten mit der Klasse StringBuilder 99 Die beiden wichtigsten Funktionen der Klasse StringBuilder sind das gezielte Löschen und Einfügen einzelner Zeichen oder einer Teilzeichenkette aus oder in der vorliegenden Zeichenkette. Für die Löschung von Zeichen sind die beiden folgenden Methoden verantwortlich: deleteCharAt(i) : löscht ein Zeichen an der Position i delete(i, j) : löscht alle Zeichen von der Position i bis j (inklusive) Listing 44: Lösch u ng einzelner oder mehrerer Zeichen Beim Einfügen von Zeichen sind zwei Methoden zu unterscheiden: append (s): das Einfügen eines Strings oder elementaren Datentyps am Ende der Zeichenkette insert (i, s): das Einfügen eines Strings oder elementaren Datentyps an der Position i innerhalb der Zeichenkette Listing 45: Einfügung einzelner Zeichen (Fortsetzung von Listing 44) Beide Methoden erlauben dabei sowohl die Einfügung einfacher Datentypen als auch ganzer Zeichenketten. StringBuilder sb = new StringBuilder("Hallo Welt"); sb.deleteCharAt(5); System.out.println(sb.toString()); / / → "HalloWelt" sb.delete(0,5); System.out.println(sb.toString()); / / → "Welt" y p ( g()) sb.append("! "); System.out.println(sb.toString()); / / → "Welt! " sb.insert(0, "Hallo "); System.out.println(sb.toString()); / / → "Hallo Welt! " <?page no="101"?> Schritt 10: Lineare Datenstrukturen <?page no="102"?> 102 Schritt 10: Lineare Datenstrukturen Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? Java bietet einfach zu nutzende Klassen für die Umsetzung linearer Datenstrukturen, z.B. Listen, Schlangen oder Stacks, an, die für große Datenmengen effizient implementiert sind und bereits über viele nützliche Methoden verfügen. Welche Schlagwörter lerne ich kennen? Collection Iterator Liste, List, ArrayList, LinkedList Menge, Set, HashSet Schlange, Queue, Deque Keller, Stapel, Stack Assoziationsliste, Map, HashMap, TreeMap Wofür benötige ich dieses Wissen? Anwendungen benötigen flexible und leistungsfähige Datenstrukturen, um die zu bearbeitenden Daten abzuspeichern. Aus diesem Grund ist es sehr wichtig, die verfügbaren Java-Klassen zu kennen und anwenden zu können. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? Typische Prüfungsfragen zielen oft auf die Unterschiede zwischen einzelnen Datenstrukturen ab und fokussieren sich auf die praxisrelevanten Methoden. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1238 <?page no="103"?> 10.1 Überblick 103 Lineare Datenstrukturen zeichnen sich durch ihre sequentielle Anordnung aus und sie erlauben es, Daten und Objekte zu speichern, auszulesen, einzufügen, zu löschen und zu verwalten. Bislang wurden nur Arrays als eine mögliche Form einer linearen Datenstruktur besprochen, wobei diese jedoch eine Reihe von Nachteilen hat: Die Größe des Arrays wird zu Beginn festgelegt und kann anschließend, d.h. zur Laufzeit des Programms, nicht mehr geändert werden. Das Einfügen oder das Löschen von Elementen innerhalb des Arrays ist vergleichsweise aufwändig, weil alle Arrayelemente verschoben werden müssen und aus Leistungsgründen typischerweise Elemente am Ende des Arrays eingefügt oder gelöscht werden. Das Array verfügt - abgesehen vom direkten Zugriff - leider über keine weiteren Funktionen, wie z.B. die Sortierung oder die Suche nach einzelnen Werten. Jede weitere Funktionalität muss also selbst implementiert, getestet und gewartet werden, statt kostengünstig auf Standardklassen zurückzugreifen. Aus den oben genannten Gründen gibt es bessere Alternativen in Java, die in den folgenden Abschnitten vorgestellt werden. Hierzu werden zuerst ein Gesamtbild und die Grundlagen der Datenstrukturen vermittelt. Anschließend werden für jede einzelne Datenstruktur die passenden Klassen und Interfaces in Java im Detail vorgestellt und jeweils anhand eines Beispiels erörtert. 10.1 Überblick Java unterstützt die folgenden Datenstrukturen, indem es hierfür passende Klassen und Interfaces anbietet: Liste (List) Menge (Set) Schlange (Queue) Keller (Stack) Assoziationsliste (Map) <?page no="104"?> 104 Schritt 10: Lineare Datenstrukturen Die linearen Datenstrukturen werden durch eine Reihe von Interfaces, abstrakten und konkreten Klassen, in Form des Java Collection Frameworks implementiert. Die prinzipielle Struktur wird in der folgenden Abbildung dargestellt. Abb. 5: Konzept des Java Collection Frameworks [1] Die grundlegenden Schnittstellen einer Datenstruktur werden in einem Interface definiert. Die Namensgebung der Interfaces wie z.B. Collection , List , Queue , Map oder Set zeigt, dass zu jeder Datenstruktur auch ein Interface passt. [2] Die abstrakten Klassen, z.B. AbstractCollection , AbstractList , AbstractQueue , implementieren allgemeine Funktionen der jeweiligen Interfaces. [3] Konkrete Klassen, z.B. ArrayList oder LinkedList , die von den abstrakten Klassen abgeleitet werden, sind die eigentliche Implementierungen der jeweiligen Collection. Die wesentlichen differenzierenden Eigenschaften der einzelnen Datenstrukturen sind, abgesehen von den Klassen und Interfaces, der Zugriffsort, die Bewahrung der Eingabereihenfolge und das einfache oder mehrfache Vorkommen der Elemente. Unter Zugriffsort ist die Position der Elemente der Datenstrukturen zu verstehen, die für das Einfügen, Löschen und Lesen von Elementen verfügbar ist. Zum Beispiel kann bei einer Liste auf jede Position zugegriffen werden, beim Keller ist nur das Ende verwendbar. Die Bewahrung der Eingabereihenfolge bedeutet, dass die Datenstruktur explizit die Elemente in derselben Ordnung beibehält, wie sie eingegeben wurden. Dies ist z.B. bei der Liste der Fall, bei einer Menge hingegen nicht. Schnittstelle Abstrakte Klasse Klasse Legende: Klasse A implementiert Interface IF 2 3 1 Klasse K ist abgeleitet von Oberklasse A A IF A K <?page no="105"?> 10.1 Überblick 105 Die wesentlichen Eigenschaften der zu diskutierenden Datenstrukturen werden in der folgenden Tabelle kurz zusammengefasst. Name Interface Klasse Zugriff Bewahrung der Eingabereihenfolge Collection Collection sequentiell nein Menge Set HashSet , TreeSet sequentiell nein Liste List ArrayList , LinkedList Indizierung (überall) ja Schlange Queue , Deque LinkedList am Kopf, Ende ja Keller Deque LinkedList Ende umgedreht bei Ausgabe Assoziationsliste Map HashMap , TreeMap indirekt nein Tab. 12: Übersicht der Datenstrukturen 15 Vom grundlegenden Interface Collection werden z.B. die Interfaces List, Set, Deque, Queue abgeleitet (d.h. es ist ein Super-Interface). Es hat kein direktes Pendant in Form einer Klasse, es definiert einen sequentiellen Zugriff auf die gespeicherten Elemente und es sichert nicht notwendigerweise die Eingabereihenfolge zu. Die Liste benutzt das Interface List, sie wird von den Klassen ArrayList, LinkedList implementiert und sie unterscheidet sich von der Collection dadurch, dass sie Positionen hat, so dass (1) jedes Element indiziert wird, somit direkt zugreifbar ist, und (2) die Eingabereihenfolge beibehalten wird. Die Liste bietet die größten Freiheiten an, nämlich den direkten Zugriff auf alle Positionen. Die Schlange hat zwei Interfaces: Queue und Deque ( D ouble E nded Que ue), für die einfache und die doppelte Schlange, die beide von der Klasse LinkedList implementiert werden. Die einfache Schlange, beschrieben durch Queue , bietet das Lesen und Löschen nur am Kopf und das Einfügen nur am Ende an. Bei der doppelten Schlange, Deque , sind Lesen, Löschen und Einfügen sowohl am Kopf als auch am Ende möglich. 15 Die in Java existierenden Klassen Vector und Stack waren ursprünglich als Implementierung der Liste und des Kellers gedacht. Sie sind inzwischen nicht mehr relevant und verbleiben nur aus Kompatibilitätsgründen in der Java Standard Edition. <?page no="106"?> 106 Schritt 10: Lineare Datenstrukturen Der Keller wiederum stellt eine Einschränkung der Schlange, dar, indem nur auf das Ende zugegriffen wird und sich die Reihenfolge der eingegebenen Elemente umdreht. Die Assoziationsliste basiert nicht auf der Collection . Sie wird durch das Interface Map beschrieben und von den Klassen HashMap und TreeMap implementiert, wobei ersteres unsortiert und letzteres sortiert ist. In beiden Fällen erfolgt der Zugriff durch die der Assoziation (bestehend aus Schlüssel und Wert) zugrundeliegenden Hashfunktion, so dass nur ein indirekter Zugriff vorliegt. Die Eingabereihenfolge wird im ersten Fall nicht bewahrt und im zweiten Fall bewusst verändert, da die Elemente sortiert werden (müssen). 10.2 Typisierung von Collections Ein wesentlicher Punkt bei der Definition einer Datenstruktur ist die Festlegung des Typs (analog zu der Typisierung von Arrays). Bei der Deklaration einer Java- Klasse, die eine Datenstruktur implementiert, muss der Typ der Elemente angegeben werden. Die Instanz einer Datenstruktur wird typisiert, indem bei der Deklaration und der Initialisierung, der Typ, eingerahmt durch ein spitzes Klammernpaar, angegeben wird. In dem nachfolgenden Beispiel wird durch den Ausdruck <String> festgelegt, dass sowohl das Interface als auch die zugehörige Instanz der Klasse LinkedList vom Typ String sind: List<String> meineListe = new LinkedList<>() 16 . Die Typisierung der Elemente einer Datenstruktur hat den großen Vorteil, dass sowohl beim Einfügen als auch beim Lesen der Typ zur Übersetzungszeit geprüft wird. Dies hat zur Folge, dass kein Casting notwendig ist, wenn das Ergebnis des Lesezugriffs einer Variablen vom selben Typ zugewiesen wird, und dass auch Instanzen von abgeleiteten Klassen zugewiesen werden können, da sie auch die Eigenschaften und Methoden der deklarierten Klasse haben. Diese werden dann aber immer nur als Elemente der deklarierten Klasse behandelt. Die Typisierung zieht sich durch alle Collection-Interfaces und -Klassen und wird allgemein durch das Suffix <T> oder <E> dargestellt, wobei T oder E für einen beliebigen Referenztyp steht, z.B . List<E> . Collections können nur Referenz- 16 Ab Java Version 7 kann der Diamond-Operator „ <> “ verwendet werden, so dass es nicht mehr nötig ist, den Typ nochmals auf der rechten Seite zu wiederholen. <?page no="107"?> 10.3 Das Interface Collection 107 typen enthalten. Möchte man einen elementaren Datentyp, wie z.B. byte , short , int , festlegen, greift man auf die entsprechenden Wrapper-Klassen, wie z.B. Byte , Short , Integer , zurück. In dem obigen Beispiel würde man also <Integer> verwenden, wenn man möchte, dass die Elemente vom Typ int sind. 17 In dem obigen Beispiel wird die Variable meineListe als Variable vom Typ des Interfaces List definiert (auf der linken Seite steht der Ausdruck List <String> meineListe ). Auf der rechten Seite muss dann die konkrete Implementierung angegeben werden (im Beispiel new LinkedList<>() ) . Somit wird sichergestellt, dass die Variable meineListe nur die Methoden des Interfaces List nutzt. 10.3 Das Interface Collection Das Interface Collection steht im Mittelpunkt des Java Collection Frameworks, denn es bildet die Basis für viele weitere Interfaces und Klassen, wie z.B. List , Queue . Anschließend erfolgt eine Zweiteilung: zum einen werden aus Collection weitere Interfaces, Subinterfaces, z.B. List , Queue , Set , abgeleitet, und zum anderen, werden abstrakte Klassen, wie z.B. AbstractCollection , Abstract List , AbstractQueue , definiert, die die obigen Interfaces implementieren. Auf der untersten Ebene werden die konkreten Klassen aufgeführt, die von den abstrakten Klassen abgeleitet sind und die die entsprechenden Interfaces implementieren. Die bekanntesten Klassen sind hierbei ArrayList , LinkedList , HashMap , TreeMap , HashSet und TreeSet . In diesem Abschnitt wird das Interface Collection vorgestellt, die davon abgeleiteten Interfaces und deren implementierende Klassen werden in den nachfolgenden Abschnitten vorgestellt. Das Interface Collection<T> erweitert das Interface Iterable<T> . Aus diesem Grund werden zunächst noch die Interfaces Iterable und Iterator (das immer mit Iterable zusammen auftritt) erläutert. Erst diese Schnittstellen erlauben es, eine Anordnung von Elementen zu durchlaufen, ohne dass deren Positionen bekannt sind. Die Aufgabe des Interfaces Iterable<T> ist es, den Iterator eines bestimmten Typs T zu liefern und mit Hilfe dieses Iterators die Collection zu durchlaufen und so auf die einzelnen Elemente zuzugreifen. T ist dabei der Typ, der den die 17 Die automatische Umwandlung der elementaren Datentypen in die entsprechende Wrapper- Klasse (und umgekehrt), z.B. von int nach Integer erfolgt durch das Autoboxing. <?page no="108"?> 108 Schritt 10: Lineare Datenstrukturen Collection speichert. Ein Iterator implementiert dabei das Interface Iterator . Tabelle 13 gibt einen Überblick über die definierten Methoden der beiden Interfaces. Methodendefinition Erläuterung Interface Iterable<T> iterator() : Iterator<T> Liefert einen Iterator. Interface Iterator<T> hasNext() : boolean Prüft, ob es noch ein weiteres Element gibt und liefert in diesem Fall den Wert true , sonst false . next() : T Gibt das nächste Element der Collection zurück. remove() Löscht das aktuelle Element. Tab. 13: Übersicht der Methoden der Interfaces Iterable und Iterator Der Iterator muss nicht direkt genutzt werden - seine Existenz erlaubt aber die Verwendung der erweiterten for-Schleife (→ Schritt 3). Ein Iterator kann aber auch direkt verwendet werden, wie das folgende Listing 46 zeigt: Listing 46: Schleife zur Nutzung eines Iterators [1] Zunä chst wird eine Collection c auf Strings definiert (in diesem Fall mit Hilfe der Klasse ArrayList (→ s.u.). [2] Mit Hilfe der Methode iterator() wird der (vordefinierte) Iterator diese Collection erhalten und der Variablen it zugewiesen. [3] Vor Eintritt in die while -Schleife wird geprü ft, ob es noch ein weiteres, noch nicht besuchtes Element in c gibt. [4] Falls ja, geht der Iterator zum nä chsten Element weiter und dieses wird dann ausgegeben. Die vier wichtigsten Funktionalitäten einer Datenstruktur sind (1) das Einfügen, (2) das Entfernen und (3) das Lesen von Elementen und (4) die Prüfung auf Collection<String> c = new HashSet<>(); / / Elemente zur Collection hinzufügen …Iterator<String> it = c.iterator(); while(it.hasNext()) { System.out.println(it.next()); } 1 2 3 4 <?page no="109"?> 10.3 Das Interface Collection 109 Vorhandensein, was durch die Methoden add(…) , remove(…) , iterator() und contains(…) umgesetzt wird. Zusätzlich sind die folgenden Methoden möglich: Das Einfügen von einzelnen oder mehreren Elementen kann über add() bzw. addAll() erfolgen. Das Entfernen aller Elemente aus der Collection kann über clear (), die gezielte Löschung einzelner Elemente bzw. mehrerer Elemente hingegen wird durch remove() bzw. removeAll() ausgeführt. Der lesende Zugriff auf die Elemente der Collection wird durch die Methode iterator() ermöglicht, die das Interface Iterator implementiert und so die Durchquerung der Collection erlaubt. Die Prüfung, ob ein Element bzw. mehrere Elemente schon in der Collection vorhanden sind, wird durch die Methoden contains() bzw. containsAll() ausgeführt. Tabelle 14 gibt eine Übersicht der Methoden der Schnittstelle Collection : Methode Erläuterung add(E e) : boolean Einfügen eines einzelnen Elements addAll(Collection <E> c) : boolean Einfügen einer ganzen Collection clear() Löschen der gesamten Collection contains(Object o) : boolean Prüfung, ob das Objekt bereits vorhanden ist containsAll(Collection<E> c) : boolean Prüfung, ob die Collection von Objekten bereits vorhanden ist isEmpty() : boolean Prüfung, ob die Collection leer ist iterator() : Iterator<E> Erzeugt einen Iterator, der es erlaubt die Collection zu durchlaufen. remove(Object o) : boolean Entfernen eines vorhandenen Objekts removeAll(Collection <E> c) : boolean Entfernen aller Objekte aus der Collection, die als Parameter übergeben werden size() : int Anzahl der Elemente der Collection toArray(T[] a) : T[] Die Elemente der Collection werden in einem Array (das dem Typ des übergebenen Arrays entspricht) zurückgegeben. Passen sie in das übergebene Array wird das verwendet, ansonsten wird ein neues erzeugt. Tab. 14: Übersicht der Methoden des Interfaces Collection <?page no="110"?> 110 Schritt 10: Lineare Datenstrukturen Die Schnittstelle Collection garantiert die Bewahrung der Eingabereihenfolge nicht, d.h. es kann nicht davon ausgegangen werden, dass diese Sequenz der eingegebenen Elemente so bewahrt bleibt. Auf die Elemente kann nur mittels eines Iterators zugegriffen werden. Erst das Unterinterface List definiert die Methoden, die eine Indizierung erlauben und garantiert damit eine feste Reihenfolge. 10.4 Die Liste / List Die lineare Datenstruktur Liste basiert auf der Idee, dass alle Elemente hintereinander angeordnet sind und jedes über eine eindeutige Position, den Index, verfügt. Über diesen Index können Werte gelesen oder geschrieben werden (vergleichbar einem Array). Wie bei Arrays beginnt auch hier die Indizierung bei 0. Die Datenstruktur Liste zeichnet sich dadurch aus, dass die Elemente in einer sequentiellen Reihenfolge angeordnet sind und mittels Index ein direkter Zugriff auf jedes Element möglich ist. Elemente können an jeder Position gelesen, geschrieben, gelöscht oder hinzugefügt werden. Die Reihenfolge der Elemente wird bewahrt. Der Zusammenhang zwischen den Interfaces, abstrakten und konkreten Klassen für die Datenstruktur Liste ist wie folgt: Abb. 6: Ableitungen der Klassen und Interfaces für die Datenstruktur Liste 18 18 In der Java-Klassenbibliothek werden List und AbstractCollection zusätzlich explizit von Iterable abgeleitet - dies ist nicht notwendig, da beide von Collection abgeleitet werden und Schnittstellen Collection List Abstrakte Klassen AbstractCollection Klassen ArrayList LinkedList Iterable AbstractList AbstractSequentialList <?page no="111"?> 10.4 Die Liste / List 111 Das Interface List wird von dem Interface Collection abgeleitet. Von der abstrakten Klasse AbstractCollection wird die abstrakte Klasse AbstractList abgeleitet. Von diese die konkrete Klasse ArrayList . Von der AbstractList wird die AbstractSequentialList und schließlich die LinkedList abgeleitet. Neben den bereits definierten Methoden des Interfaces Collection bietet List die folgenden, zusätzlichen Methoden, die in der folgenden Tabelle, einer Übersicht von List , kurz zusammengefasst sind: Methode Erläuterung add(int index, E element) : boolean Hinzufügen eines Objekts an der Position index addAll(int index, Collection <? extends E> c) Hinzufügen einer Sammlung von Elementen an der Position index 19 get(int index) : E Rückgabe des Objekts an der Position index indexOf(E element) : int Rückgabe der (ersten) Position des Elements. remove(int index) : E Entfernen des Objekts an der Position index set(int index, E element) : E Ersetzen des Objekts an der Position index subList(int fromIndex, int toIndex) : List <E> Rückgabe einer Liste von den Positionen fromIndex bis toIndex Tab. 15: Zusätzliche Methoden des Interfaces List Das Interface List ergänzt die Collection um Methoden, die dank der Indizierung den Zugriff auf eine Position erlauben, dabei kann es sich um einen lesenden, schreibenden oder löschenden Zugriff handeln. List bietet deshalb die bereits von Collection bekannten Methoden zum Einfügen ( add() , addAll() ) und Löschen ( remove() ) an, diese sind jedoch um den Parameter index erweitert, der festlegt, wo die Elemente eingefügt bzw. gelöscht werden sollen. indirekt damit auch von Iterable abgeleitet werden. Wir verzichten deshalb auf die Darstellung dieser zusätzlichen Beziehungen in dieser und den folgenden Abbildungen. 19 Der Ausdruck <? extends E> bedeutet, dass die Objekte der Collection von der Klasse E oder davon abgeleiteten Klassen sein können. <?page no="112"?> 112 Schritt 10: Lineare Datenstrukturen Die Methode get(index) ist neu, denn sie erlaubt den Zugriff des Elements an der Position index. Außerdem kann mittels set(index) an der Position index das aktuelle Element durch ein neues Element ersetzt werden. Weiterhin ist es möglich, sich eine Teilliste durch sublist (fromIndexto Index) berechnen zu lassen, die von den Positionen fromIndex bis toIndex die Elemente zusammenstellt. Die abstrakte Klasse AbstractList implementiert das Interface List und leitet daraus die beiden konkreten Klassen ArrayList und LinkedList ab. Diese unterscheiden sich also nicht im Verhalten der Methoden des Interfaces List , sondern in der Form der Implementierung: bei ArrayList liegt ein Array, bei Linked- List eine verzweigte Liste zugrunde, im Umfang der Schnittstellen: LinkedList implementiert neben List noch weitere Interfaces, z.B. Queue oder Deque (dies sind die Interfaces für eine einfache bzw. doppelte Schlange). Java bietet mit ArrayList und LinkedList zwei konkrete Klassen an, basierend auf den Interfaces List und Collection . Es handelt sich also um zwei unterschiedliche Implementierungen der Datenstruktur Liste. Das folgende Beispiel verdeutlicht die Nutzung der verschiedenen Methoden einer List . Listing 47: Eine Liste basierend auf LinkedList import java.util.LinkedList; import java.util.List; public class ListenVerwendung { public static void main(String[] args) { List<String> liste = new LinkedList<>(); / / Aufbau der Liste liste.add("a"); liste.add("b"); liste.add("c"); System.out.println(liste); / / → [a, b, c] / / Zugriff auf die Elemente der Liste System.out.println(liste.get(1)); / / → b System.out.println(liste.contains("b")); / / → true System.out.println(liste.indexOf("c")); / / → 2 liste.remove("c"); / / Löschen eines Elements liste.clear(); / / Löschen der Liste System.out.println(liste.isEmpty()); / / → true } } 1 2 3 4 5 6 7 8 9 <?page no="113"?> 10.5 Die Menge / Set 113 Eine Variable liste vom Typ List wird deklariert und mittels new wird eine Instanz von LinkedList erzeugt. Die Liste wird aufgebaut, indem die add -Methode mehrfach aufgerufen wird. Die Liste wird ausgegeben: [a, b, c] (die implizit aufgerufene toString -Methode der Liste führt zu dieser Formatierung) Der Zugriff auf die Position 1 erfolgt mittels get(1) . Das Vorhandensein des Elements b wird mittels contains("b") geprüft. Die Position des Elements c wird über indexOf("c") ermittelt. Anschließend wird c aus der Liste mittels remove("c") entfernt. Abschließend wird die Liste durch clear() vollständig gelöscht. Mit isEmpty() wird schließlich geprüft, ob sie leer ist. 10.5 Die Menge / Set Die Datenstruktur der Menge ist aus der Mathematik bekannt. Sie gehört ebenfalls zu den linearen Datenstrukturen. Im Unterschied zur Liste ist kein direkter Zugriff über die Position des Elements möglich und es wird vorausgesetzt, dass jedes Element nur einmal vorkommt. Aufgrund der fehlenden Indizierung der Elemente der Menge müssen zur Bearbeitung alle Elemente durchlaufen werden. Ansonsten sind die üblichen Methoden zum Einfügen und Löschen von Elementen sowie zum Test auf das Vorhandensein verfügbar. Abb. 7: Ableitungen der Klassen und Interfaces für die Datenstruktur Menge Schnittstellen Collection Set Abstrakte Klassen AbstractCollection Klassen HashSet Iterable AbstractSet TreeSet SortedSet <?page no="114"?> 114 Schritt 10: Lineare Datenstrukturen Die lineare Datenstruktur Set setzt voraus, dass (1) jedes Element nur einmal existiert und (2) es existiert keine Indizierung des Elements, d.h. es existieren keine festen Positionen für Elemente. Die Datenstruktur Menge basiert in Java auf dem Interface Set , das von den Interfaces Collection und Iterable abgeleitet ist, das in der abstrakten Klasse AbstractSet und der konkreten Klasse HashSet umgesetzt wird. Die einzige Erweiterung zwischen dem Interface Set zur Collection besteht darin, dass im Set noch zusätzlich die Methode retainAll definiert wird: Methode Erläuterung retainAll(Collection<? > c): boolean Entfernung der Elemente, die nicht in der Collection c aufgeführt sind Tab. 16: Zusätzliche Methode des Interfaces Set Das folgende Beispiel zeigt die Nutzung des Interfaces Set . Dabei wird als Implementierung die Klasse HashSet verwendet: Listing 48: Nutzung der Klasse HashSet import java.util.HashSet; import java.util.Iterator; import java.util.Set; public class SetVerwendung { public static void main(String[] args) { Set<String> menge = new HashSet<>(); menge.add("a"); menge.add("b"); menge.add("c"); menge.add("a"); Iterator<String> it = menge.iterator(); while(it.hasNext()) { String s = it.next() System.out.println(s); } for(String s : menge){ System.out.println(s); } System.out.println(menge.contains("b")); / / → true menge.remove("c"); / / Löschen eines Elements menge.clear(); / / Löschen der Menge } } 1 2 3 4 5 6 7 8 <?page no="115"?> 10.6 Die Schlange / Queue 115 [1] Die Variable menge wird durch die Klasse HashSet instantiiert und fü r das Interface Set deklariert. [2] Die Elemente a, b und c werden der Menge hinzugefü gt. [3] Es wird versucht, a erneut hinzuzufü gen - fü r ein Set ist dies nicht mö glich. [4] Anschließend wird die Menge menge per Iterator durchlaufen. und jedes Element wird angezeigt. Zuerst wird der Iterator fü r menge von der Klasse HashSet erzeugt. Bevor die while -Schleife betreten wird, wird geprü ft ob menge noch ein weiteres Element besitzt. Falls ja, geht der Iterator zum nä chsten Element weiter, das dann ausgegeben wird. [5] Alternativ wird die Menge mit einer (ä quivalenten) for-each -Schleife durchwandert. [6] Es wird geprü ft, ob b in der Menge vorhanden ist. [7] Das Element c wird aus der Menge entfernt. [8] Abschließend wird die vollstä ndige Menge geleert. 10.6 Die Schlange / Queue Die nächste Datenstruktur ist die einfache Schlange. Typische Beispiele für diese Datenstruktur sind die Drucker- oder Warteschlange eines Betriebssystems. Im Unterschied zur Liste kann bei der Datenstruktur Schlange nur auf die erste und letzte Position zugegriffen werden. Die Datenstruktur einfache Schlange zeichnet sich dadurch aus, dass es sich um eine lineare Datenstruktur handelt, bei der nur das Element an der ersten Position (d.h. am Anfang) gelesen oder gelöscht werden darf und ein neues Element nur an der letzten Position (d.h. am Ende) hinzugefügt werden darf. Abb. 8: Ableitungen der Klassen und Interfaces für die Datenstruktur Schlange Schnittstellen Collection Queue Abstrakte Klassen AbstractCollection Klassen LinkedList Iterable Deque AbstractSequentialList <?page no="116"?> 116 Schritt 10: Lineare Datenstrukturen Die Datenstruktur einfache Schlange basiert in Java auf dem Interface Queue , das vom Interface Collection abgeleitet ist, das in der abstrakten Klasse AbstractQueue und der konkreten Klasse LinkedList umgesetzt wird. Neben den bereits definierten Methoden des Interfaces Collection bietet Queue die folgenden zusätzlichen Methoden, die in der folgenden Tabelle kurz zusammengefasst sind: Methode Erläuterung add(E element) : boolean Hinzufügen des Elements am Ende die Schlange remove() : E Lesen und Entfernen des Elements am Anfang der Schlange element() : E Lesen des Elements am Anfang der Schlange Tab. 17: Zusätzliche Methoden des Interfaces Queue Die Zugriffe beschränken sich also auf die erste und letzte Position, den Anfang und das Ende der Schlange. Die oben aufgeführten Methoden werfen im Fehlerfall (z.B. beim Lesen aus einer leeren Schlange) eine Exception. Alternativ gibt es weitere Methoden, die in diesem Fall true oder null zurückgeben. Das folgende Beispiel verdeutlicht die Umsetzung der einfachen Schlange basierend auf der Klasse LinkedList . Listing 49: Eine einfache Schlange, basierend auf LinkedList import java.util.LinkedList; import java.util.Queue; public class QueueVerwendung { public static void main(String[] args) { Queue<String> schlange = new LinkedList<>(); / / Aufbau der Schlange schlange.add("a"); schlange.add("b"); schlange.add("c"); System.out.println(schlange); / / → [a, b, c] / / Zugriff auf die Elemente der Schlange System.out.println(schlange.element()); / / → a String s = schlange.remove(); System.out.println(s); / / a System.out.println(schlange); / / → [b, c] } } 1 2 3 4 5 6 <?page no="117"?> 10.6 Die Schlange / Queue 117 [1] Die Variable schlange wird erzeugt [2] Die Schlange wird ü ber die Aufrufe der add -Methode aufgebaut. [3] Die Schlange wird ausgegeben: [a, b, c] [4] Der Anfang der Schlange wird ü ber element() angezeigt. [5] Anschließend wird der Anfang mittels remove() gelö scht, zurü ck geliefert und das Ergebnis angezeigt. [6] Die Schlange wird erneut ausgegeben und liefert [b, c] Neben der einfachen Schlange gibt es als Weiterentwicklung auch die doppelte Schlange, bei der wahlweise am Anfang oder am Ende Elemente gelesen, gelöscht oder hinzugefügt werden. Der Entwickler ist selbst für die Beibehaltung der inhaltlichen Integrität der Daten verantwortlich. Die Datenstruktur doppelte Schlange zeichnet sich dadurch aus, dass es sich um eine lineare Datenstruktur handelt, bei der sowohl das Element an der ersten Position (d.h. am Anfang) oder an der letzten Position (d.h. am Ende) gelesen, gelöscht oder ein neues Element hinzugefügt werden darf. Der Unterschied besteht darin, dass am Anfang zusätzlich Elemente hinzugefügt werden können, am Ende zusätzlich Elemente gelesen oder gelöscht werden können. Dies bedeutet, dass bei einer doppelten Schlange also sowohl am Anfang als auch am Ende dieselben Methoden ausgeführt werden können, d.h. zum Lesen, Hinzufügen und Löschen von Elementen. Die Datenstruktur doppelte Schlange basiert in Java auf dem Interface Deque (= D ouble E nded Que ue), das vom Interface Collection abgeleitet ist und der konkreten Klasse LinkedList umgesetzt wird. Eine Übersicht der Methoden von Deque findet sich in der folgenden Tabelle (wobei hier nur die zusätzlichen Methoden zum Interface Queue aufgeführt werden): Methode Erläuterung addFirst(E element) : boolean Hinzufügen des Elements am Anfang der Schlange removeFirst() : E Entfernen des Anfangs der Schlange getFirst() : E Lesen des Anfangs der Schlange <?page no="118"?> 118 Schritt 10: Lineare Datenstrukturen addLast(E element) : boolean Hinzufügen des Elements am Ende der Schlange removeLast() : E Entfernen des Elements am Ende der Schlange getLast() : E Lesen des Elements am Ende der Schlange Tab. 18: Zusätzliche Methoden des Interfaces Deque für die doppelte Schlange Das folgende Beispiel zeigt die Umsetzung der doppelten Schlange basierend auf der Klasse LinkedList . Listing 50: Eine doppelte Schlange, basierend auf LinkedList [1] Die Variable schlange wird als eine Instanz der Klasse LinkedList definiert und auf das Interface Deque festgelegt. [2] Die Zeichenketten a, b und c werden hinzugefü gt. [3] Die Schlange wird ausgegeben. [4] Das Element am Anfang wird gelesen. [5] Das Element am Ende wird gelesen. [6] Das Element am Anfang wird entfernt; das Element und die verbleibende Schlange werden ausgegeben. [7] Das Element am Ende wird gelö scht; die verbleibende Schlange und das Element werden ausgegeben. import java.util.Deque; import java.util.LinkedList; public class DequeVerwendung { public static void main(String[] args) { Deque<String> schlange = new LinkedList<>(); / / Aufbau der Schlange schlange.addFirst("a"); schlange.addLast("b"); schlange.addLast("c"); System.out.println(schlange); / / → [a, b, c] / / Zugriff auf die Elemente der Schlange System.out.println(schlange.getFirst()); / / → a System.out.println(schlange.getLast()); / / → c String s1 = schlange.removeFirst(); System.out.println(s1 + " " + schlange); / / → a [b, c] String s2 = schlange.removeLast(); System.out.println(schlange + " " + s2); / / → [b] c } } 1 2 3 4 5 6 7 <?page no="119"?> 10.7 Der Keller / Stapel / Stack 119 10.7 Der Keller / Stapel / Stack Die lineare Datenstruktur Stack (auch Keller oder auch Stapel) schränkt den Zugriff auf die erste Position ein. Erlaubte Operationen sind dabei nur das Hinzufügen, das Lesen und das Löschen eines Elements am Anfang. Ein Stack ist dabei durch das LIFO-Prinzip gekennzeichnet, wobei LIFO für „last in, first out“ steht. Dies bedeutet, dass das zuletzt hinzugefügte Element als erstes wieder ausgelesen wird. Interpretiert man den Stack als Stapel, der von unten nach oben wächst, so spricht man oft auch von der „Spitze des Stapels“, die gelesen oder verändert wird. Die lineare Datenstruktur Stack erlaubt nur den Zugriff auf das letzte Element und sie invertiert bei der Ausgabe die Reihenfolge der Elemente. Interessanterweise gibt es eine Vielzahl von Anwendungen des Stacks, z.B. die Programmiersprache Forth, die „Umgekehrte Polnische Notation“ (UPN) bei Taschenrechnern und die Implementierung der Speicherung der Aufrufe von Methoden. Das Java-Interface Deque (das im vorigen Abschnitt „Die Schlange / Queue“ vorgestellt wurde) definiert zusätzlich einen Stack mit den üblichen Zugriffsmethoden: Einfügen, Lesen und Löschen des Elements. Eine Übersicht der Methoden des Interfaces Deque , die für die Datenstruktur Stack relevant sind, wird hier tabellarisch aufgelistet. Methode Erläuterung push(E element) : void Hinzufügen des Elements am Anfang („Spitze“) des Stacks (entspricht der Methode addFirst(…) ). pop() : E Entfernen des Elements vom Anfang des Stacks (entspricht removeFirst() ). peek() : E Lesen des Elements am Anfang des Stacks, ohne es zu löschen (entspricht der Methode getFirst() ). Tab. 19: Zusätzliche Methoden des Interfaces Deque für den Stack Wie bei der doppelten Schlange auch, ist der Anwender der Datenstruktur verantwortlich, die Struktur konsistent zu verwenden und nur die für den Stack vorgesehenen Methoden zu verwenden. Das folgende Listing zeigt ein Beispiel für die Nutzung eines Stacks. <?page no="120"?> 120 Schritt 10: Lineare Datenstrukturen Listing 51: Nutzung des Interfaces Deque für den Stack [1] Erzeugung und Initialisierung der Variablen keller vom Typ Deque . [2] Die Elemente a, b und c werden am Anfang des Stacks hinzugefü gt. [3] Der Anfang des Stacks wird ausgegeben und gelö scht. [4] Der Anfang des Stacks zurü ck geliefert. [5] Der verbleibende Stack wird ausgegeben. 10.8 Die Assoziationsliste / Map Die Assoziationsliste unterscheidet sich von den vorhergehenden Datenstrukturen Liste und Schlange dadurch, dass nicht die Position der Elemente im Vordergrund steht, sondern die Assoziation zwischen Schlüssel und Wert. Aus diesem Grund ist Map unabhängig vom bisher genutzten Collection-Interface. Das Ziel der Assoziationsliste ist, schnell über den Schlüssel auf den zugehörigen Wert zugreifen zu können. Die Position des Schlüssel-Wert-Paares ist dabei nicht bekannt. Ansonsten gelten die üblichen Operationen, wie das Einfügen und das Löschen von Elementen, nur mit dem Unterschied, dass sich diese auf Schlüssel- Wert-Paare und nicht auf einzelne Werte beziehen. Eine andere Konsequenz ist, dass die Position des Schlüssel-Wert-Paares nicht vom Benutzer beeinflusst werden kann, da diese von der Assoziationsliste eigenständig verwaltet wird. Die lineare Datenstruktur Assoziationsliste besteht aus Paaren von Schlüsseln und Werten. Der Zugriff erfolgt über den Schlüssel auf den Wert. import java.util.Deque; import java.util.LinkedList; public class StackVerwendung { public static void main(String[] args) { Deque<String> keller = new LinkedList<>(); / / Aufbau des Stacks keller.push("a"); keller.push("b"); keller.push("c"); / / Anzeigen und Entfernen der Stackspitze String s = keller.pop(); System.out.println(s); / / → c / / Zugriff auf die Stackspitze System.out.println(keller.peek()); / / → b System.out.println(keller); / / → [b, a] } } 1 2 3 4 5 <?page no="121"?> 10.8 Die Assoziationsliste / Map 121 Ein typisches Anwendungsbeispiel für eine Assoziationsliste wäre ein deutschfranzösisches Wörterbuch, bei dem die deutschen Wörter die Schlüssel und die französischen Übersetzungen die Werte darstellen. Die Positionen der einzelnen Paare innerhalb der Assoziationsliste werden dabei nicht angezeigt. Abb. 9: Ableitungen der Klassen und Interfaces für die Datenstruktur Map Die Datenstruktur Assoziationsliste basiert in Java auf dem Interface Map<K,V> , wobei K für den Schlüsseltyp („Keys“) und V für den Wertetyp („Values“) stehen). Das Map - Interface wird von der abstrakten Klasse AbstractMap und und den konkreten Klassen HashMap und TreeMap umgesetzt. In Java gibt es die zwei Klassen für die Implementierung der Assoziationsliste: HashMap und TreeMap . Bei der Hash Map werden die Schlüssel unsortiert verwaltet. In einer Tree Map werden die Schlüssel sortiert (z.B. numerisch oder lexikographisch) verwaltet - dies ist relevant, wenn die Schlüssel durchwandert werden. Das Interface Map bietet Methoden an, die in der folgenden Tabelle zusammengefasst sind: Methode Erläuterung put(K key, V value): V Ein Paar (key, value) bestehend aus Schlüssel und Wert wird in die Map eingefügt. putAll(Map<? extends K,? extends V> m) Eine Map kann in eine bereits existierende Map eingefügt werden. keySet(): Set<K> Liefert ein Set der Schlüssel zurück. values(): Collection<V> Liefert eine Collection der Werte zurück. Schnittstellen Map SortedMap Abstrakte Klassen Klassen HashMap AbstractMap TreeMap <?page no="122"?> 122 Schritt 10: Lineare Datenstrukturen get(Object key) : V Liefert den Wert zum Schlüssel. containsKey(Object key) : boolean Prüft, ob der Schlüssel in den Schlüsseln der Map enthalten ist. containsValue(Object value) : boolean Prüft, ob der Wert in den Werten der Map enthalten ist. remove(Object key): V Entfernt das Objekt mit dem übergebenen Schlüssel. Ergebnis ist das entfernte Objekt oder null , falls kein Schlüssel vorlag. clear() Leeren der gesamten Map. size() : int Anzahl der Paare der Map. isEmpty() : boolean Prüfung, ob die Map leer ist. Tab. 20: Methoden des Interfaces Map Das folgende Beispiel zeigt die Benutzung anhand der Klasse HashMap : Listing 52: Eine Assoziationsliste basierend auf HashMap import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; public class MapVerwendung { public static void main(String[] args) { Map<String,String> woerter = new HashMap<>(); woerter.put("Brot", "pain"); woerter.put("Tag","jour"); woerter.put("Wort", "mot"); String s = woerter.get("Tag"); System.out.println(s); / / → jour Set<String> keys = woerter.keySet(); System.out.println(keys); / / → [Wort, Brot, Tag] Collection<String> values = woerter.values(); System.out.println(values); / / → [mot, pain, jour] System.out.println(woerter.containsKey("Wort")); / / → true System.out.println(woerter.containsValue("jour")); / / → true woerter.remove("Tag"); System.out.println(woerter.values()); / / → [mot, pain] } } 1 2 3 4 5 6 7 8 <?page no="123"?> 10.8 Die Assoziationsliste / Map 123 [1] Die Variable woerter wird vom Typ Map deklariert und mit einer HashMap instanziiert. [2] Die Schlü ssel-Wert-Paare ("Brot" , "pain") , ("Tag" , "jour") und ("Wort" , "mot") werden zur Map hinzugefü gt. [3] UÜ ber den Schlü ssel "Tag" wird auf den Wert "jour" zugegriffen. [4] Die Schlü ssel werden als Set zurü ckgegeben. [5] Alle Werte werden als Collection zurü ckgegeben. [6] Das Vorhandensein des Schlü ssels "Wort" wird geprü ft. [7] Das Vorhandensein des Werts "jour" wird geprü ft. [8] Das Paar mit dem Schlü ssel "Tag" wird entfernt. Die Klasse TreeMap bietet einige zusätzliche Methoden, die auf der Sortierung der Schlüssel-Wert-Paare beruhen. So kann zum Beispiel auf den ersten oder letzten Schlüssel zugegriffen werden oder es kann ein Schlüssel angegeben werden und dann nach den unmittelbar vorhergehenden oder nachfolgenden Schlüsseln gesucht werden. Die hinzugekommenen Methoden profitieren also explizit von der Sortierung der Elemente. Das Interface SortedMap<K,V> wird von Map<K,V> abgeleitet und durch die Klasse TreeMap umgesetzt. Die tabellarische Übersicht fasst die wichtigsten zu Map zusätzlichen Methoden der SortedMap zusammen: Methode Erläuterung firstKey() : K Gibt den ersten Schlüssel zurück. lastKey () : K Gibt den letzten Schlüssel zurück. Tab. 21: Zusätzliche Methoden des Interfaces SortedMap <?page no="125"?> Schritt 11: Datenströme / Streams <?page no="126"?> 126 Schritt 11: Datenströme / Streams Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um unterschiedliche Datenarten und die passenden Klassen und Methoden zur Ein- und Ausgabe in Dateien. Welche Schlagwörter lerne ich kennen? Stream Datei Quelle Senke Buffer Reader Writer Serialisierung Wofür benötige ich dieses Wissen? Die Ein- und Ausgabe von Daten ist notwendig für die Kommunikation mit der Umgebung. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, Daten in eine Datei zu schreiben oder Daten von der Eingabe zu lesen. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1239 <?page no="127"?> 11.2 Daten- und Stream-Arten 127 Eine wichtige Aufgabe bei der Programmierung ist die Ein- und Ausgabe von Daten. Grundsätzlich wird zwischen grafischer und zeichenbasierter Ein- und Ausgabe unterschieden. Wir werden hier zunächst die grundlegendere, zeichenbasierte Kommunikation betrachten. 11.1 Datenquellen und -senken Daten werden in Java als Ströme („Streams“) betrachtet, die Zeichen transportieren. Eingabeströme („Input-Streams“) lesen Daten von einer Quelle. Quellen können dabei Dateien (mit dauerhaft gespeicherten Daten), Eingabegeräte (z.B. die Tastatur), andere Programme oder entfernte Server sein. Ausgabeströme („Output-Streams“) schreiben Dateien in eine Senke. Senken können ebenfalls Dateien, andere Programme, Ausgabegeräte (z.B. der Bildschirm) oder entfernte Clients ein. 11.2 Daten- und Stream-Arten Grundsätzlich wird in der Informatik zwischen Binärdaten und Textdaten unterschieden 20 . Textdaten können mit einem Texteditor betrachtet und bearbeitet werden, Binärdaten enthalten überwiegend Zeichen, die nicht direkt im Texteditor verarbeitet werden können, weil sie als unsichtbare Zeichen oder besondere Symbole dargestellt werden. Die übliche Speichereinheit für beide Arten ist ein Byte (acht Bits). Für Binärdaten ist der Inhalt der Bytes durch die Art der Anwendung bestimmt (so kann bei einer Videodatei das Byte ein Teil des Bildes, bei einer Audiodatei das Byte einen Teil des Tons darstellen). Für Textdateien gibt es eine Reihe normierter Zeichencodierungen. Die wichtigsten sind: ANSI: Umfasst 256 Zeichen; jedes Zeichen kann mit einem Byte dargestellt werden. Die erste Hälfte der Zeichen entspricht dem ASCII- und Unicode-Zeichensatz. Unicode: Versucht jedem Zeichen aller Sprachen einen Code zuzuordnen. Im Augenblick umfasst der Standard 1 114 112 Zeichen. Ursprünglich war man davon ausgegangen, dass zwei Bytes genügen, dies ist aber nicht mehr der Fall. Aus diesem Grund gib es verschiedene Untermengen, die die wichtigsten Zeichen umfassen. Die wichtigsten Unterstandards dabei sind UTF-16 und UTF-8. 20 Siehe auch „Fit für die Prüfung: Informatik“, utb, 2015 <?page no="128"?> 128 Schritt 11: Datenströme / Streams UTF-16: Untermenge von Unicode, das die wichtigsten Unicode-Zeichen enthält. Die Zeichen werden mit zwei Bytes dargestellt. Dies ist das Format, das von Java verwendet wird . UTF-8: Unicode-Codierung mit flexibler Byte-Größe pro Zeichen (von ein bis sechs Bytes). Dies erlaubt eine effizientere Speicherung. Java sieht grundsätzlich vier verschiedene Stream-Arten für unterschiedliche Aufgaben vor: Byte-, Zeichen-, Daten- und Objekt-Streams. Tabelle 22 zeigt die wesentlichen Merkmale dieser Ströme. Strom Leseeinheit Anwendung Byte-Strom 1 Byte (8 Bits) Direkte Verarbeitung der Dateidaten, z.B. bei Audio- oder Videodateien. Zeichenstrom („Character Stream“) 2 Bytes (16 Bits) im Unicode-Format Verarbeitung von Textdokumenten (die Eingangsdaten des Textdokuments können dabei in einem der zuvor vorgestellten Formate vorliegen) Datenstrom (basierend auf Byte- Strom) Primitive Java- Typen: boolean , byte , short , int , long , float , double und der Referenztyp String Verarbeitung von Programmdaten. Objektstrom (basierend auf Byte- Strom) serialisierbare 21 Java-Objekte Verarbeitung von Programmzuständen. Tab. 22: Stream-Arten in Java 11.3 Lesen und Schreiben von Strömen in Java Alle Klassen zum Lesen und Schreiben von Strömen in Java befinden sich im Paket java.io . Um das Lesen oder Schreiben zu optimieren können Puffer (englisch „Buffer“) eingesetzt werden. Statt Daten sofort weiterzuleiten, speichert der 21 Die Java-Klasse muss in diesem Fall das Marker-Interface Serializable implementieren (→ Abschnitt: „Objekte speichern und lesen“) <?page no="129"?> 11.3 Lesen und Schreiben von Strömen in Java 129 Puffer die Daten zwischen (die Daten werden „gepuffert“) und leitet sie in Gruppen weiter. Das Betriebssystem kann die Puffer zur Leistungssteigerung nutzen. Zusätzlich können weitere Filter eingesetzt werden, um die gelesenen oder zu schreibenden Daten vor- oder nachzuverarbeiten (z.B. ganze Java-Objekte codieren). Das Muster zum Lesen und Schreiben von Strömen ist immer dasselbe und wird im Folgenden allgemein erläutert; die nachfolgenden Abschnitte zeigen dann die beispielhafte Anwendung. Typisches Muster zum Lesen von Daten Import der notwendigen Klassen aus java.io Die folgenden Anweisungen sollten in einem try -Block stehen, da sie alle eine IOException auslösen können. Öffnen der Quelle durch FileInputStream oder FileReader (abhängig vom Stream-Typ) Pufferung des geöffneten Streams durch einen Buffered InputStream oder BufferedReader Evtl. weitere Filter: DataInputStream für Datenströme oder Object- InputStream für Objektströme Lesen der Elemente vom Strom; abhängig vom Stromtyp muss auf unterschiedliche Art und Weise auf das Stromende reagiert werden. In einem abschließenden finally -Block wird die Quelle wieder geschlossen. Abbildung 10 zeigt die Abfolge der verwendeten Elemente: Abb. 10: Abfolge von Datenquelle, InputStream oder Reader, Puffer und Filter Analog werden Daten geschrieben. InputStream / Reader Puffer Daten-Strom Daten-Strom gruppierter Daten-Strom Datenquelle Daten im Programm Filter gefilterter Daten-Strom <?page no="130"?> 130 Schritt 11: Datenströme / Streams Typisches Muster zum Schreiben von Daten Import der notwendigen Klassen aus java.io Die folgenden Anweisungen sollten in einem try -Block stehen, da sie alle eine IOException auslösen können. Öffnen der Quelle durch FileOutputStream oder FileWriter (abhängig vom Stream-Typ) Pufferung des geöffneten Streams durch einen Buffered Output Stream oder BufferedWriter Evtl. weitere Filter: DataOutputStream für Datenströme oder Object OutputStream für Objektströme Schreiben der Elemente in den Strom. In einem abschließenden finally -Block wird die Quelle wieder geschlossen. Abbildung 11 zeigt die Abfolge der Elemente. Abb. 11: Abfolge von Filter, Puffer, OutputStream oder Writer und Datensenke 11.4 Lesen und Schreiben von Byte-Strömen Das folgende Listing 53 zeigt die notwendigen Codeteile, um eine Byte-Datei zu lesen und zu verarbeiten (in diesem Fall am Bildschirm auszugeben). Das Muster für das Lesen und Schreiben ist für alle Datenarten im Prinzip dasselbe und wird deshalb hier sehr ausführlich erläutert. In den folgenden Listings werden wir dann nur noch auf die wesentlichen Unterschiede eingehen. import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class BytesLesen { public static void main(String[] args) { 1 OutputStream / Writer Puffer Daten-Strom gruppierter Daten-Strom gefilterter Daten-Strom Datensenke Daten im Programm Filter Daten-Strom <?page no="131"?> 11.4 Lesen und Schreiben von Byte-Strömen 131 Listing 53: Eine Byte-Datei lesen und verarbeiten [1] Import der notwendigen Klassen aus dem Paket java.io . [2] Name der Datei, die gelesen werden soll ( bytes.dat ). Ein Name ohne weitere Pfadangaben verweist auf eine Datei auf derselben Ebene des Java-Projekts. 22 [3] Definition des eigentlichen Streams und des Puffers ( buffer ). Beide kö nnen als InputStream definiert werden, da sowohl FileInputStream und BufferedInputStream Unterklassen von InputStream sind. [4] Mit der Klasse FileInputStream kann ein Eingabestrom auf eine Byte-Datei geö ffnet werden. Falls die Datei nicht existiert oder nicht geö ffnet werden kann, wird eine FileNotFoundException (die eine Unterklasse der IOException ist) geworfen (siehe [10]). [5] Definition eines Puffers auf den Byte-Strom. Das Prinzip ist (auch bei den spä teren Varianten) immer dasselbe: Der Konstruktor akzeptiert als Argument einen (allgemeinen) InputStream , der verarbeitet werden soll. Dadurch kö nnen beliebige (z.B. gefilterte) InputStreams genutzt werden. [6] Lesen des ersten Bytes. Alle InputStreams besitzen eine Methode read(): int , mit der ein Byte vom Strom gelesen wird. Falls beim Lesen ein 22 Java erlaubt außerdem absolute und relative Adressierung. Eine Adressierung unabhängig von Betriebssystemeigenschaften ist allerdings sehr komplex und wird hier nicht weiter betrachtet. String datei = "bytes.dat"; InputStream stream = null; InputStream buffer = null; try { stream = new FileInputStream(datei); buffer = new BufferedInputStream(stream); int b = buffer.read(); while (b ! = -1) { System.out.println(b); / / Byte verarbeiten b = buffer.read(); } } catch (IOException e) { e.printStackTrace(); } finally { if (buffer ! = null) try { buffer.close(); / / schließt auch "stream" } catch (IOException e) {} } } } 2 3 4 5 6 7 8 9 10 11 <?page no="132"?> 132 Schritt 11: Datenströme / Streams Fehler auftritt, wird eine IOException geworfen (siehe [10]). Da die Methode einen Wert zwischen -1 und 255 liefert, ist der Java-Typ byte nicht geeignet (er kann nur Werte von -128 bis 127 aufnehmen). [7] Falls das Ende der Datei erreicht ist, liefert die Methode read() den Wert -1. Ansonsten wird der Wert des gelesenen Bytes (ein Wert zwischen 0 und 255) zurü ckgegeben. [8] Verarbeitung des gelesenen Bytes - hier wird lediglich der Wert am Bildschirm ausgegeben. [9] Lesen des nä chsten Bytes. [10] Abfangen einer IOException . Diese Ausnahme wird geworfen, falls beim OÜ ffnen der Datei oder beim Lesen ein Fehler auftritt (siehe [4], [6] und [9]); z.B. wenn die Datei wä hrend des Lesens gelö scht wird. [11] Nachdem alle Bytes gelesen sind, muss die Datei mit der Methode close() geschlossen werden, da ansonsten die Datei evtl. gesperrt bleibt. UÜ blicherweise sollte diese Methode in einen finally -Block gefasst werden, um sicher zu stellen, dass die Datei auch im Fehlerfall wieder geschlossen wird. Das Schließen des Puffers fü hrt auch dazu, dass der enthaltene Stream geschlossen wird. Allerdings mü ssen dabei die folgenden Mö glichkeiten beachtet werden: Der Puffer kö nnte durch einen vorherigen Fehler nicht erzeugt worden sein (also null sein) und die close -Operation selbst kann eine IOException auslö sen. Der erste Fall wird durch das if -Statement abgefangen, der zweite Fall kann durch ein weiteres try-catch -Statement (innerhalb des finally -Statements) abgefangen werden. Listing 54 zeigt ein Programm, das die Bytes 00 bis FF (0 bis 255) in eine Datei schreibt. Der Ablauf ist analog zum Lesen der Datei. import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class BytesSchreiben { public static void main(String[] args) { String datei = "bytes.dat"; OutputStream stream = null; OutputStream buffer = null; try { stream = new FileOutputStream(datei); buffer = new BufferedOutputStream(stream); 1 2 3 4 5 <?page no="133"?> 11.4 Lesen und Schreiben von Byte-Strömen 133 Listing 54: Eine Byte-Datei schreiben [1] Import der notwendigen Klassen aus dem Paket java.io . [2] Name der Datei, die geschrieben werden soll ( bytes.dat ). [3] Definition des eigentlichen Streams und des Puffers ( buffer ). Beide kö nnen als OutputStream definiert werden, da sowohl FileOutputStream und BufferedOutputStream Unterklassen von OutputStream sind. [4] Mit der Klasse FileOutputStream kann ein Ausgabestrom fü r eine Byte- Datei geö ffnet werden. Falls die Datei nicht geö ffnet werden kann, wird eine FileNotFoundException geworfen (Unterklasse von IOException , siehe [8]). [5] Definition eines Puffers auf den Byte-Strom. [6] Als Beispiel sollen hier alle 256 mö glichen, unterschiedlichen Bytes geschrieben werden. [7] Schreiben eines Bytes. Alle OutputStreams besitzen eine Methode write(b int) , mit der ein Byte in den Strom geschrieben wird. Falls beim Schreiben ein Fehler auftritt, wird eine IOException geworfen (siehe [8]). [8] Abfangen einer IOException (siehe [4] und [7]). Nachdem alle Bytes geschrieben sind, muss die Datei mit der Methode close() geschlossen werden. Diese Methode leert außerdem den Puffer („flush“) - der Puffer verfügt zusätzlich über eine explizite Methode flush() , die aber nicht aufgerufen werden muss. Tabelle 23 und 24 enthalten die wesentlichen Java-Klassen und -Methoden zum Lesen und Schreiben von Byte-Strömen. for (int b = 0; b <= 255; b++) / / Bytes verarbeiten buffer.write(b); } catch (IOException e) { e.printStackTrace(); } finally { if (buffer ! = null) try { buffer.close(); / / schließt auch "stream" } catch (IOException e) {} } } } 6 7 8 9 <?page no="134"?> 134 Schritt 11: Datenströme / Streams Oberklasse InputStream Unterklasse zum Dateizugriff FileInputStream Konstruktor zum Öffnen der Byte-Quelle stream = new FileInputStream(String filename) Unterklasse zur Pufferung BufferedInputStream Konstruktor zur Pufferung buffer = new BufferedInputStream(InputStream stream) Lesemethode buffer.read(): int throws IOException liefert ein Byte zwischen 0 und 255 Dateiende buffer.read() liefert -1 Datei schließen buffer.close() schließt alle beteiligten Streams Tab. 23: Klassen und Methoden zum Lesen eines Byte-Stroms Oberklasse OutputStream Unterklasse zum Dateizugriff FileOutputStream Konstruktor zum Öffnen der Byte-Senke stream = new FileOutputStream(String filename) Unterklasse zur Pufferung BufferedOutputStream Konstruktor zur Pufferung buffer = new BufferedOutputStream(OutputStream stream) Schreibmethode buffer.write(): int throws IOException schreibt ein Byte zwischen 0 und 255 Datei schließen buffer.close() schließt alle beteiligten Streams Tab. 24: Klassen und Methoden zum Schreiben eines Byte-Stroms 11.5 Lesen und Schreiben von Textdateien Textdateien sind Zeichen-(„Character“)-Ströme. Der Aufbau ist derselbe wie beim Lesen und Schreiben der Byte-Ströme (voriger Abschnitt). Allerdings werden statt der dort verwendeten Klassen InputStream und OutputStream die Klassen Reader und Writer (und ihre Unterklassen) verwendet. <?page no="135"?> 11.5 Lesen und Schreiben von Textdateien 135 Das folgende Listing 55 zeigt einen typischen Code zum Lesen einer Textdatei. Gegenüber den vorigen Beispielen wurden hier der Import mit einer Wildcard vereinfacht und die Exceptions nicht mehr gefangen, sondern weitergereicht (beides ist für reale Programme nicht empfohlen, erlaubt hier aber kürzere Listings). Listing 55: Eine Textdatei lesen [1] Import der notwendigen Klassen aus dem Paket java.io . [2] Weitergabe der IO-Ausnahme, die durch die Anweisung [5], [7], [8] oder [9] ausgelö st werden kö nnen. (Die zuvor verwendete FileNotFoundException ist eine Unterklasse der IOException und muss deshalb hier nicht explizit aufgezä hlt werden.) [3] Name der Datei, die gelesen werden soll ( zeichen.text ). [4] Definition des Readers und des Puffers. Der Reader kann als Variable vom Typ Reader definiert werden, da der FileReader eine Unterklasse von Reader ist. Der Puffer muss als BufferedReader definiert werden. Der BufferedReader ist zwar eine Unterklasse von Reader , die zum Lesen notwendige Methode readLine() wird aber nur von dieser Unterklasse definiert. import java.io.*; public class ZeichenLesen { public static void main(String[] args) throws IOException { String datei = "zeichen.txt"; Reader reader = null; BufferedReader buffer = null; try { reader = new FileReader(datei); buffer = new BufferedReader(reader); String str = buffer.readLine(); / / Text verarbeiten while(str ! = null){ System.out.println(str); str = buffer.readLine(); } } finally { if (buffer ! = null) buffer.close(); / / schließt auch "reader" } } } 1 2 3 4 5 6 7 8 9 <?page no="136"?> 136 Schritt 11: Datenströme / Streams [5] Mit der Klasse FileReader kann eine Textdatei geö ffnet werden. Falls die Datei nicht existiert oder nicht geö ffnet werden kann, wird eine FileNot- FoundException geworfen (siehe [2]). [6] Definition eines Puffers auf den Zeichenstrom. Wie zuvor auch akzeptiert der Konstruktor als Argument einen (allgemeinen) Reader , der verarbeitet werden soll. Dadurch kö nnen beliebige (z.B. gefilterte) Reader genutzt werden. [7] Statt eines einzelnen Zeichens erlaubt der BufferedReader mit der Methode readLine(): String , eine ganze Zeile aus der Datei zu lesen. Falls das Ende der Datei erreicht ist, liefert die Methode den Wert null . Falls beim Lesen ein Fehler auftritt, wird eine IOException geworfen (siehe [2]). [8] Verarbeitung der gelesenen Zeile - hier wird lediglich die Zeile am Bildschirm ausgegeben, und Lesen der nä chsten Zeile. [9] Nachdem alle Zeilen gelesen sind, muss die Datei mit der Methode close() geschlossen werden. Wie zuvor wird ü ber den finally -Block sichergestellt, dass diese Methode auf jeden Fall ausgefü hrt wird. Listing 56 zeigt das analoge Listing zur Ausgabe eines Textes in eine Datei. Listing 56: Eine Textdatei schreiben import java.io.*; public class ZeichenSchreiben { public static void main(String[] args) throws IOException { String datei = "zeichen.txt"; Writer writer = null; BufferedWriter buffer = null; try { writer = new FileWriter(datei); buffer = new BufferedWriter(writer); for (char c = 'A', i = 1; c <= 'z'; c++, i++) { buffer.write(c); / / Text verarbeiten if (i % 20 == 0) buffer.newLine(); } } finally { if (buffer ! = null) buffer.close(); / / schließt auch "writer" } } } 1 2 3 4 5 6 7 8 9 <?page no="137"?> 11.5 Lesen und Schreiben von Textdateien 137 [1] Import der notwendigen Klassen aus dem Paket java.io . [2] Weitergabe der IO-Ausnahme, die durch die Anweisung [5], [7], [8] oder [9] ausgelö st werden kö nnen. [3] Name der Datei, die geschrieben werden soll ( zeichen.text ). [4] Definition des Writers und des Puffers. Der Writer kann als Variable vom Typ Writer definiert werden, da der FileWriter eine Unterklasse von Writer ist. Der Puffer muss als BufferedWriter definiert werden. Der BufferedWriter ist zwar eine Unterklasse von Writer , die zum Schreiben notwendige Methode newLine() wird aber nur von dieser Unterklasse definiert. [5] Mit der Klasse FileWriter kann eine Textdatei geö ffnet werden. Falls die Datei nicht geö ffnet werden kann, wird eine FileNotFoundException geworfen (siehe [2]). [6] Definition eines Puffers auf den Zeichenstrom. Wie zuvor auch, akzeptiert der Konstruktor als Argument einen (allgemeinen) Writer , der verarbeitet werden soll. Dadurch kö nnen beliebige (z.B. gefilterte) Writer genutzt werden. [7] In diesem Beispiel sollen die Buchstaben von A bis z in die Datei geschrieben werden. Nach jeweils 20 Zeichen soll ein Zeilenumbruch eingefü gt werden. Die Methode write(int c) schreibt das Zeichen c in die Datei 23 . Zusä tzlich stehen noch weitere Methoden zur Verfü gung, die z.B. einen ganzen String schreiben kö nnen. Der Puffer ist fü r die Gruppierung und effiziente UÜ bertragung der Zeichen zustä ndig. Falls beim Schreiben ein Fehler auftritt, wird eine IOException geworfen (siehe [2]). [8] Zusä tzlich erlaubt der Puffer die Methode newLine() , mit der ein Zeilenumbruch eingefü gt wird (alternativ hä tte man auch write ('\n') schreiben kö nnen). [9] Nachdem alle Zeilen gelesen sind, muss die Datei mit der Methode close() geschlossen werden. close() leert auch hier den Puffer. Eine Besonderheit ist das Lesen von der Tastatur in Java. Während die Ausgabe am Bildschirm mit Hilfe der Methoden System.out.println(…) bequem geht, ist das Lesen von der Tastatur eher umständlich und folgt dem zuvor beschriebenen Muster (im Prinzip ist dieser umständliche Weg analog auch für die Ausgabe möglich, wird aber natürlich nicht genutzt). Listing 57 zeigt, wie eine Textzeile von der Tastatur gelesen und am Bildschirm wiederholt wird. 23 char besteht aus zwei, int besteht aus vier Bytes. Aus diesem Grund ist char kompatibel zu int , kann also ohne Casting einem int zugewiesen werden. Da int aber alle vier Bytes verwendet, muss es gecastet werden, wenn es einem char zugwiesen wird. <?page no="138"?> 138 Schritt 11: Datenströme / Streams Listing 57: Eine Zeile von der Tastatur lesen [1] Import der notwendigen Klassen. [2] Java definiert bereits einen Tatstatur-Eingabestrom ( Input Stream ) namens System.in . [3] System.in ist ein Byte-orientierter Strom, wä hrend Textdaten in Java Unicode-basiert sein mü ssen. Die Klasse InputStreamReader schlä gt die Brü cke von dem Bytezum zeichenorientierten Strom. Dazu wird dem Konstruktor der InputStream ü bergeben. Ergebnis ist ein Reader, der diesen Strom konvertiert. [4] Wie zuvor wird der Reader nun mit einem Puffer ( BufferedReader ) versehen. [5] Nun kann, analog zum Lesen einer Textdatei, eine Zeile von der Tastatur gelesen werden. Lesen erzwingt eine Ausnahmebehandlung, da Lesen per se eine unsichere Tä tigkeit ist. So kann z.B. durch technische Probleme der Lesevorgang unterbrochen werden. [6] Diese mö gliche Ausnahme wird aufgefangen. Gelesen werden zunächst immer Strings. Um Strings weiter zu verarbeiten, sind vor allem die folgenden Methoden nützlich (siehe auch Kapitel 9 „Zeichenketten/ Strings“): Die Methode split(String delimiter) der Klasse String liefert ein Array mit einzelnen Strings, die mit Hilfe des Strings delimiter getrennt werden. So liefert z.B. "abc ; def ; 123" . split("; ") ein Array mit den Einzelstrings "abc", "def" und "123" . Die statische Methode parseInt(String str) der Klasse Integer wandelt den als Parameter übergebenen String in einen int -Wert um. Falls der String nicht umgewandelt werden kann, wird eine NumberFormatException geworfen). import java.io.*; public class TatstaturEingabe { public static void main(String[] args) { InputStream stream = System.in; Reader reader = new InputStreamReader(stream); BufferedReader buffer = new BufferedReader(reader); try { String s = buffer.readLine(); System.out.println(s); } catch (IOException e) { e.printStackTrace(); } } } 1 2 3 4 5 6 <?page no="139"?> 11.5 Lesen und Schreiben von Textdateien 139 Analog stehen für die Klassen Long , Double , Float und Boolean die Methoden parseLong , parseDouble , parseFloat und parseBoolean zur Verfügung. Tabelle 25 und 26 enthalten die wesentlichen Java-Klassen und -Methoden zum Lesen und Schreiben von Zeichenströmen. Oberklasse Reader Unterklasse zum Dateizugriff FileReader Konstruktor zum Öffnen der Zeichenquelle reader = new FileReader(String filename) Unterklasse zur Pufferung BufferedReader Konstruktor zur Pufferung buffer = new BufferedReader (Reader reader) Lesemethode buffer.readln(): String throws IOException liefert eine Zeile. Dateiende buffer.readln() liefert null Datei schließen buffer.close() schließt alle beteiligten Streams Tab. 25: Klassen und Methoden zum Lesen eines Zeichenstroms Oberklasse Writer Unterklasse zum Dateizugriff FileWriter Konstruktor zum Öffnen der Zeichensenke writer = new FileWriter(Stringfilename) Unterklasse zur Pufferung BufferedWriter Konstruktor zur Pufferung buffer = new BufferedWriter (Writer writer) Schreibmethoden buffer.write(String st) throws IOException schreibt eine Zeile. buffer.newLine() schreibt einen Zeilenumbruch. Datei schließen buffer.close() schließt alle beteiligten Streams Tab. 26: Klassen und Methoden zum Schreiben eines Zeichenstroms <?page no="140"?> 140 Schritt 11: Datenströme / Streams 11.6 Lesen und Schreiben von Java-Daten Um Daten aus einem Java-Programm direkt zu speichern und wieder zu lesen, sind die bisher betrachteten Byte- und Textströme nur unzureichend geeignet. Prinzipiell könnten Daten direkt als Bytes oder umgewandelt in Strings geschrieben und gelesen werden. Allerdings ist dieser Weg sehr aufwändig. Java erlaubt mit Datenströmen die direkte Verarbeitung von primitiven Typen: boolean , byte , short , int , long , float , double und dem Referenztyp String . Im Folgenden sollen (einfache) Buchdaten durch ein Programm geschrieben und wieder gelesen werden. Ein Buch wird durch die folgende Klasse beschrieben (Listing 58). Listing 58: Datentyp zur Buchbeschreibung Das folgende Programm (Listing 59) erzeugt zunächst eine Reihe von Datensätzen und schreibt dann die einzelnen Daten in eine Datei. Gegenüber den bisherigen Beispielen wurde hier (um das Beispiel übersichtlicher zu gestalten) auf den finally -Block verzichtet. public class Buch { private String titel; private double preis; public Buch(String titel, double preis) { super(); this.titel = titel; this.preis = preis; } public String getTitel() { return titel; } public double getPreis() { return preis; } public String toString() { return titel + ", " + preis + "€"; } } 1 <?page no="141"?> 11.6 Lesen und Schreiben von Java-Daten 141 Listing 59: Schreiben der Programmdaten [1] Import der notwendigen Klassen aus dem Paket java.io und Weitergabe aller IO-Ausnahmen, die durch die Anweisung [3], [4] oder [5] ausgelö st werden kö nnen. [2] Erzeugung einer Reihe von Beispieldatensä tzen, die gespeichert werden sollen. [3] Definition der OutputStreams, die zum Schreiben in die Datei daten.dat genutzt werden sollen. Zunä chst wird (wie zuvor) ein gepufferter Byte- Strom definiert. Zusä tzlich wird an den Byte-Strom ein DataOutputStream „angeschlossen“. Der DataOutputStream hat fü r alle primitiven Datentypen und fü r String geeignete Schreibmethoden. [4] Schreiben der Daten. In diesem Fall wird zunä chst der Name (als UTF-Codierung) und der Preis des Artikels geschrieben. Analog gibt es fü r die ü brigen primitiven Datentypen die Methoden write(boolean b) , write(int i) usw. [5] Nachdem alle Daten geschrieben sind, muss die Datei mit der Methode close() geschlossen werden. close() leert auch hier den Puffer. Listing 60 zeigt, wie die Daten wieder eingelesen werden. Dabei liegt es in der Verantwortung des Programmierers, dass die Struktur der Daten beachtet wird (in diesem Fall eine Abfolge von String und double ). import java.io.*; public class DatenSchreiben { public static void main(String[] args) throws IOException { Buch[] buecher = { new Buch("Java Lerntafel", 7.99), new Buch("Systemanalyse und -entwurf mit UML", 7.99), new Buch("Java Lernbuch", 12.99), new Buch("Wirtschaftsinformatik Wissensordner", 49.99) }; String datei = "daten.dat"; OutputStream stream = new FileOutputStream(datei); OutputStream buffer = new BufferedOutputStream(stream); DataOutputStream out = new DataOutputStream(buffer); for (int i = 0; i < buecher.length; i ++) { out.writeUTF(buecher[i].getTitel()); out.writeDouble(buecher[i].getPreis()); } out.close(); } } 1 2 3 4 5 <?page no="142"?> 142 Schritt 11: Datenströme / Streams Listing 60: Lesen der Programmdaten [1] Import der notwendigen Klassen aus dem Paket java.io und Weitergabe aller IO-Ausnahmen, die durch die Anweisung [3], [4] oder [6] ausgelö st werden kö nnen. [2] Erzeugung einer Datenstruktur, die die eingelesenen Programmdaten aufnehmen soll. [3] Definition der InputStreams, die zum Lesen aus der Datei daten.dat genutzt werden sollen. Zunä chst wird (wie zuvor) ein gepufferter Byte-Strom definiert. Zusä tzlich wird an den Byte-Strom ein DataInputStream „angeschlossen“. Der DataInputStream hat fü r alle primitiven Datentypen und fü r String geeignete Schreibmethoden. [4] Lesen der Daten. In diesem Fall werden zunä chst der Name (als UTF-Codierung) und dann der Preis des Artikels gelesen. Analog gibt es fü r die ü brigen primitiven Datentypen die Methoden readBoolean(): boolean , read- Int(): int usw. [5] Im Unterschied zu zuvor wird das Dateiende nicht durch einen besonderen Wert angezeigt, sondern es wird beim Lesevorgang eine EOFException („End of File“) ausgelö st. Das Listing zeigt das typische Code-Muster, um alle import java.io.*; import java.util.*; public class DatenLesen { public static void main(String[] args) throws IOException { List<Buch> buecher = new ArrayList<>(); String datei = "daten.dat"; InputStream stream = new FileInputStream(datei); InputStream buffer = new BufferedInputStream(stream); DataInputStream in = new DataInputStream(buffer); try { while (true) { String titel = in.readUTF(); double preis = in.readDouble(); buecher.add(new Buch(titel, preis)); } } catch (EOFException e) {} in.close(); for (Buch buch : buecher) System.out.println(buch); } } 1 2 3 4 5 6 7 <?page no="143"?> 11.6 Lesen und Schreiben von Java-Daten 143 Daten zu lesen: in einer Schleife ohne explizite Abbruchbedingung ( while(true) ) wird solange gelesen, bis eine EOFException ausgelö st wird. Wird die Exception ausgelö st, wird sie ohne weitere Meldung gefangen und das Programm fortgefü hrt. [6] Nachdem alle Daten geschrieben sind, muss die Datei mit der Methode close() geschlossen werden. [7] Am Ende werden die gelesenen Daten am Bildschirm ausgegeben. Die Verwendung der EOFException zur Anzeige des Dateiendes ist nicht ganz befriedigend. Eigentlich sollten Exceptions für Fehlerfälle reserviert sein. Dass das Dateiende erreicht ist, ist aber kein eigentlicher Fehler. Alternative Lösungen wären die folgenden: Speicherung der Anzahl der folgenden Datensätze zu Beginn (mit der Methode writeInt() ). Dies setzt aber voraus, dass die Anzahl der Datensätze zu Beginn bekannt ist. Speicherung einer speziellen Markierung als Kennzeichen für den Datensatz. Z.B. einen leeren String, einen besonderen Text (z.B. "<Ende>" , null ist nicht erlaubt) oder eine reservierte Zahl (z.B. -1). Dieser Wert kann dann beim Einlesen abgefragt werden. Dies setzt aber voraus, dass dieser Wert nicht an anderer Stelle in den Daten verwendet wird. Tabelle 27 und 28 enthalten die zusätzlich notwendigen Java-Klassen und -Methoden zum Lesen und Schreiben von Datenströmen. Oberklasse InputStream Unterklasse zum Lesen der Daten DataInputStream Konstruktor zum Lesen der Daten in = new DataInputStream(InputStream stream) Lesemethoden in.readInt(): int, in.readDouble(): double, in.readString(): String, … throws IOException liefert den entsprechenden Java- Typ Dateiende in.read() wirft eine EOFException Tab. 27: Zusätzliche Klassen und Methoden zum Lesen eines Datenstroms <?page no="144"?> 144 Schritt 11: Datenströme / Streams Oberklasse OutputStream Unterklasse zum Schreiben von Daten DataOutputStream Konstruktor zum Schreiben von Daten out = new DataOutputStream(OutputStream stream) Schreibmethoden out.write(int i), out.write(double d), out.write(String s), … throws IOException schreibt den entsprechenden Java-Typ Tab. 28: Zusätzliche Klassen und Methoden zum Schreiben eines Datenstroms 11.7 Objekte speichern und lesen Mit den zuvor vorgestellten Datenströmen können die primitiven Daten und Strings eines Objekts gespeichert und (mit dem Wissen um die Struktur des Objekts) die Objekte wieder rekonstruiert werden. Objektströme gehen noch einen Schritt weiter und erlauben ganze Objekte oder auch Objektgeflechte zu speichern und zu lesen. Man spricht hier von „Serialisierung“. Um ein Objekt zu speichern, muss es zunächst „serialisierbar“ gemacht werden. Dazu muss das Objekt (und alle seine enthaltenen Objekte) das Interface java.io.Serializable implementieren. Dieses Interface definiert keine Methoden, sondern zeigt nur an, dass Objekte dieser Klasse serialisiert werden können. Man spricht hier von einem Marker-Interface. Standardmäßig implementieren nahezu alle vordefinierten Java-Klassen dieses Interface, also insbesondere String , alle Collection -Klassen und Arrays. Listing 61 zeigt das Buchobjekt aus Listing 58, das vollständig gespeichert werden soll und deshalb nun das Interface Serializable implementiert. Listing 61: Das Objekt, das gespeichert werden soll [1] Import und Implementierung des Interfaces Serializable . Der ü brige Code bleibt unverä ndert. import java.io.Serializable; public class Buch implements Serializable{ / / Code wie zuvor private String titel; private double preis; … } 1 <?page no="145"?> 11.7 Objekte speichern und lesen 145 Listing 62 zeigt nun, wie die Objekte vom Typ Buch mit Hilfe von DataOutputStream gespeichert werden. Erklärung der einzelnen Schritte in Listing 62: [1] Import der notwendigen Klassen aus dem Paket java.io und Weitergabe aller IO-Ausnahmen, die durch die Anweisung [3], [4] oder [5] ausgelö st werden kö nnen. [2] Erzeugung einer Reihe von Beispieldatensä tzen, die gespeichert werden sollen. [3] Definition der OutputStreams, die zum Schreiben in die Datei objekte.dat genutzt werden sollen. Zunä chst wird (wie zuvor) ein gepufferter Byte- Strom definiert. Zusä tzlich wird an den Byte-Strom ein ObjectOutputStream „angeschlossen“. [4] Schreiben der Objekte. Der ObjectOutputStream bietet dafü r die Methode writeObject(Object o) an, mit der alle serialisierbaren Objekte geschrieben werden kö nnen. [5] Nachdem alle Daten geschrieben sind, muss die Datei mit der Methode close() geschlossen werden. close() leert auch hier den Puffer. Listing 62: Speicherung von ganzen Objekten Listing 63 zeigt, wie die Daten wieder eingelesen werden. Dabei liegt es auch hier in der Verantwortung des Programmierers, dass die Struktur der Daten beachtet wird (in diesem Fall die Typen der gespeicherten Objekte). Erklärung der einzelnen Schritte: import java.io.*; public class ObjekteSchreiben { public static void main(String[] args) throws IOException { Buch[] buecher = { new Buch("Java Lerntafel", 7.99), new Buch("Systemanalyse und -entwurf mit UML", 7.99), new Buch("Java Lernbuch", 12.99), new Buch("Wirtschaftsinformatik Wissensordner", 49.99) }; String datei = "objekte.dat"; OutputStream stream = new FileOutputStream(datei); OutputStream buffer = new BufferedOutputStream(stream); ObjectOutputStream out = new ObjectOutputStream(buffer); for (int i = 0; i < buecher.length; i ++) out.writeObject(buecher[i]); out.close(); } } 1 2 3 4 5 <?page no="146"?> 146 Schritt 11: Datenströme / Streams [1] Import der notwendigen Klassen aus dem Paket java.io und Weitergabe aller IO-Ausnahmen, die durch die Anweisung [3], [4] oder [5] ausgelö st werden kö nnen. Zusä tzlich besteht die Gefahr, dass Anweisung [4] eine ClassNot- FoundException auslö st - dies ist der Fall, wenn ein Objekt gelesen wird, zu dem in der aktuellen Umgebung keine passende Klasse vorliegt. [2] Erzeugung einer Datenstruktur, die die eingelesenen Programmdaten aufnehmen soll. [3] Definition der InputStreams, die zum Lesen aus der Datei objekte.dat genutzt werden sollen. Zunä chst wird (wie zuvor) ein gepufferter Byte-Strom definiert. Zusä tzlich wird an den Byte-Strom ein ObjectInput Stream „angeschlossen“. [4] Lesen der Objekte. Der ObjectInputStream bietet dazu die Methode readObject(): Object an. Das gelesene Objekt muss noch in den Zieltyp gecastet werden. [5] Wie zuvor auch, wird am Ende des Lesevorgangs eine EOFException („End of File“) ausgelö st, auf die beim Lesen reagiert werden muss. [6] Nachdem alle Daten geschrieben sind, muss die Datei mit der Methode close() geschlossen werden. Am Ende werden die gelesenen Daten am Bildschirm ausgegeben. Listing 63: Lesen von gespeicherten Objekten import java.io.*; import java.util.*; public class ObjekteLesen { public static void main(String[] args) throws IOException, ClassNotFoundException List<Buch> buecher = new ArrayList<>(); String datei = "objekte.dat"; InputStream stream = new FileInputStream(datei); InputStream buffer = new BufferedInputStream(stream); ObjectInputStream in = new ObjectInputStream(buffer); try { while (true) { Buch buch = (Buch) in.readObject(); buecher.add(new Buch(titel, preis)); } } catch (EOFException e) {} in.close(); for (Buch buch : buecher) System.out.println(buch); } } 1 2 3 4 5 6 7 <?page no="147"?> 11.7 Objekte speichern und lesen 147 Ähnlich wie zuvor ist auch hier die Verwendung der EOFException zur Anzeige des Dateiendes nicht befriedigend. Alternative Lösungen für Objektströme wären die folgenden: Speicherung der gesamten Collection, statt der einzelnen Objekte. In diesem Fall muss natürlich auch wieder eine ganze Collection gelesen werden. Dazu ist aber nur ein einziger Lesebefehl notwendig (also keine Abfrage auf das Dateiende). Speicherung des Werts null als letztes Element. Dadurch kann beim Einlesen eine while -Schleife genutzt werden, die auf null prüft. Allerdings muss dann sichergestellt sein, dass nicht schon vorher null -Werte geschrieben werden. Tabelle 29 und 30 enthalten die zusätzlich notwendigen Java-Klassen und -Methoden zum Lesen und Schreiben von Datenströmen. Oberklasse InputStream Unterklasse zum Lesen von Objekten ObjectInputStream Konstruktor zum Lesen von Objekten in = new ObjectInputStream(InputStream stream) Lesemethoden in.readObject(): Object … throws IOException liefert ein Java-Objekt (das noch gecastet werden muss) Dateiende in.read() wirft eine EOFException Tab. 29: Zusätzliche Klassen und Methoden zum Lesen eines Objektstroms Oberklasse OutputStream Unterklasse zum Schreiben von Objekten ObjectOutputStream Konstruktor zum Schreiben von Objekten out = new ObjectOutputStream(OutputStream stream) Lesemethoden out.write(Object o) … throws IOException schreibt ein Java-Objekt (das serialisierbar sein muss) Tab. 30: Zusätzliche Klassen und Methoden zum Schreiben eines Objektstroms <?page no="149"?> Schritt 12: Datenbanken mit Java <?page no="150"?> 150 Schritt 12: Datenbanken mit Java Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel wird ein erster Blick auf die Verwendung von Datenbanken mit Hilfe von Java geworfen. Welche Schlagwörter lerne ich kennen? Datenbank SQL JDBC ACID-Prinzip DDL DML Wofür benötige ich dieses Wissen? Datenbanken sind ein zentrales Element von großen Anwendungen. Sie dienen zur sicheren und dauerhaften Speicherung von Daten. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, das ACID-Prinzip und die zentralen SQL-Befehle zu erläutern. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1240 <?page no="151"?> 12.1 Java und Datenbanken 151 Datenbanken werden bei großen Anwendungen zur sicheren und dauerhaften Speicherung der Daten genutzt. In diesem Kapitel wollen wir eine allererste Einführung in das Thema Datenbanken geben. Dabei sollen die wichtigsten Ideen und ersten Ansätze zur programmatischen Anbindung von relationalen Datenbanken an Java vorgestellt werden. Sie ersetzen auf keinen Fall eine tiefere Auseinandersetzung mit dem Thema; insbesondere der Entwurf von Datenbanktabellen wird hier ausgeklammert. 12.1 Java und Datenbanken Im vorigen Kapitel wurde gezeigt, wie Daten direkt aus dem Programm gespeichert und gelesen werden konnten. Dies hat den Nachteil, dass die Daten nur über dieses Java-Programm verarbeitet werden können und der Programmierer allein die Verantwortung über die gespeicherten Daten hat. Dem gegenüber haben Datenbanksysteme den Vorteil, dass sie unabhängig vom eigentlichen Programm die Daten speichern und zugreifbar machen, so dass sie auch von anderen Programmen verwendet werden können. Bekannte Datenbanksysteme sind z.B. Oracle, MySQL, IBM DB2, Microsoft SQL Server oder SAP-DB - im Rahmen dieser Einführung wird die mit Java ausgelieferte Datenbank Derby genutzt. Der Datenzugriff wird bei den meisten heute kommerziell genutzten Datenbanken in der Sprache SQL (Structured Query Language) durchgeführt, die sehr einfach erlernbar ist. Datenbanksysteme übernehmen außerdem die Verantwortung für die gespeicherten Daten. Sie erfüllen dabei das ACID-Prinzip: Atomicity : Zusammengehörige Vorgänge werden ganz oder gar nicht wirksam (auch „Transaktionsprinzip“). Consistency : eine konsistente Datenbank ist nach einer Operation wieder konsistent (auch „Integrität“). Isolation : keine Beeinflussung bei gleichzeitigen Zugriffen, jedes Programm sieht stets konsistente Daten. Durability : die Daten werden dauerhaft (nach Programmende) gespeichert. Zusätzlich unterstützen Datenbanksysteme die Sicherheit : Für Teile der Daten können definierte Zugriffsrechte gesetzt werden. Java kann kein SQL und benötigt deshalb eine Zwischenschicht, Java Database Connect (kurz JDBC), die es erlaubt, von Java aus SQL-Befehle an das Datenbanksystem zu schicken. Üblicherweise wird für jede Datenbank eine passende JDBC- Schnittstelle mitgeliefert. <?page no="152"?> 152 Schritt 12: Datenbanken mit Java Die allgemeinen Zugriffsklassen und -methoden auf SQL-Datenbanken sind im Paket java.sql gesammelt. Dabei werden die SQL-Befehle, die in einem Programm abgesetzt werden sollen, als String an JDBC übergeben und von dort aus auf der Datenbank ausgeführt. Eine SQL-Abfrage liefert üblicherweise eine Reihe von Datensätzen zurück. Diese Ergebnismenge wird durch JDBC in ein Java-Objekt umgesetzt, das dann im Programm verarbeitet werden kann. Abbildung 12 zeigt diesen Ablauf. Abb. 12: Kommunikation zwischen Java-Programm und SQL-Datenbank 12.2 Relationale Datenbanken und SQL Die zentrale Idee von relationalen Datenbanken ist, Daten in Tabellen zu speichern - diese Tabellen können als Relationen interpretiert werden. Jede Zeile (Tupel) in einer Tabelle ist ein Datensatz (Record). Die Datentypen einer Zeile - also die Art der Werte und die möglichen Operationen - sind durch das Datenbankschema festgelegt. Die Typen werden spaltenweise festgelegt. SQL sieht u.a. die folgenden Datentypen mit den Entsprechungen in Java vor: CHAR entspricht char VARCHAR(n) entspricht einem String der maximalen Länge n FLOAT entspricht double INTEGER entspricht int BIGINT entspricht long Datenbank Java-Programm JDBC java.sql Ergebnismenge der Anfrage Die SQL-Statements werden als String formuliert und an JDBC übergeben SQL-Statements werden ausgeführt Ergebnismenge in Form eines Java-Objekts <?page no="153"?> 12.2 Relationale Datenbanken und SQL 153 Der spezielle Wert NULL kann überall als „undefiniert“ eingesetzt und abgefragt werden. Abbildung 13 und 14 zeigen zwei (bereits befüllte) Tabellen. Abb. 13: Die Tabelle BUCH Die Tabelle BUCH soll für unterschiedliche Bücher die ISBN-Nummer, den Titel und den Preis des Buches aufnehmen. Die Tabelle hat folgende Elemente: [1] Die Namen der Spalten ( ISBN , TITEL und PREIS ) [2] Ein Datensatz: Das Buch „Systemanalyse und -entwurf mit UML“, zum Preis von 7,99€ mit der ISBN-Nr. 9783825242091. [3] Die Spalte ISBN vom Typ BIGINT . [4] Die Spalte TITEL vom Typ VARCHAR(256) , d.h. es sind Strings mit maximal 256 Zeichen zugelassen. [5] Die Spalte PREIS vom Typ FLOAT . Abb. 14: Die Tabelle AUTOR Da jedes Buch auch mehrere Autoren besitzen kann, ist es nicht möglich, die Autoren einzeln in der BUCH -Tabelle zu hinterlegen. Aus diesem Grund wird eine zweite Tabelle AUTOR eingeführt, die pro Zeile, einen Autor und die ISBN-Nummer des Buches enthält. Über diese (eindeutige) Nummer ist es möglich, die zum PREIS TITEL ISBN 7.99 Java Lerntafel 9783825239497 7.99 Systemanalyse und -entwurf mit UML 9783825242091 12.99 Java Lernbuch 9783825244323 49.99 Wirtschaftsinformatik Wissensordner 9783825244132 … 1 2 3 4 5 1 2 3 4 ISBN NAME 9783825239497 Marcus Deininger 9783825239497 Thomas Kessel 9783825244132 Marcus Deininger 9783825244132 Thomas Kessel 9783825244132 Marcus Vogt <?page no="154"?> 154 Schritt 12: Datenbanken mit Java Buch zugehörigen Autoren zu suchen und umgekehrt alle Bücher eines Autors zu suchen. Die Tabelle hat folgende Elemente: [1] Die Namen der Spalten ( NAME und ISBN ) [2] Ein Datensatz: Der Autor „Marcus Vogt“ ist (Co-)Autor des Buchs mit der ISBN-Nr. 9783825244132. [3] Die Spalte NAME vom Typ VARCHAR(30) . [4] Die Spalte ISBN vom Typ BIGINT . Die Tabellen können mit Hilfe der Sprache SQL (Structured Query Language) manipuliert werden. Jede Datenbank hat üblicherweise eine Kommandozeileneingabe, in der die Befehle direkt eingegeben und die Ergebnisse angezeigt werden können, oder die Interaktion erfolgt - wie zuvor erläutert - über die JDBC- Schnittstelle. Es gibt zwei Gruppen von SQL-Befehlen : DDL (Data Definition Language) legt die Struktur der Daten fest: CREATE : legt eine Tabelle an DROP : löscht eine Tabelle DML (Data Manipulation Language) legt die Inhalte der Daten fest: INSERT : fügt einen Datensatz ein SELECT : ermittelt eine Menge von Datensätzen UPDATE : verändert eine Menge von Datensätzen DELETE : löscht eine Menge von Datensätzen SQL kennt keine Kontrollstrukturen wie while, if, … Die folgenden SQL-Beispiele illustrieren, wie die zuvor gezeigten Tabellen erzeugt und abgefragt werden können. 24 Definition von Tabellen mit SQL, z.B. für die Tabellen BUCH und AUTOR (Listing 64): Listing 64: Erzeugung der Tabellen BUCH und AUTOR 24 SQL unterscheidet nicht zwischen Groß- und Kleinschreibung. Wir werden im Folgenden (zur besseren Unterscheidung von Java) alle SQL-Befehle groß schreiben. CREATE TABLE BUCH (ISBN BIGINT NOT NULL, TITEL VARCHAR(256), PREIS FLOAT, PRIMARY KEY (ISBN)); CREATE TABLE AUTOR (NAME VARCHAR(30) ISBN BIGINT); 5 6 7 <?page no="155"?> 12.2 Relationale Datenbanken und SQL 155 [1] Die Schlü sselwö rter CREATE TABLE leiten den Befehl ein. [2] Dann folgt der Name der Tabelle: BUCH . [3] In Klammern folgen die Spaltendefinitionen, zuerst der Name der Spalte ( ISBN ), dann der Typ ( BIGINT ). [4] Zusä tzlich kann verlangt werden, dass ein Wert nicht NULL sein darf. Der Versuch, doch NULL einzutragen wü rde nun zu einem Fehler fü hren. [5] Weitere Spalten TITEL und PREIS - jeweils getrennt durch ein Komma. [6] Eine Spalte kann durch die Schlü sselwö rter PRIMARY KEY als Primä rschlü ssel gekennzeichnet werden. Dies bedeutet, dass der Wert eindeutig sein muss, es also keine Datensä tze mit gleicher ISBN-Nummer geben darf. Dadurch ist es mö glich, den Datensatz mit Hilfe dieser Nummer zu identifizieren. Der Befehl wird in der Kommandozeile ü blicherweise mit einem Strichpunkt abgeschlossen - fü r JDBC ist er spä ter nicht notwendig. [7] Analog wird die Tabelle AUTOR erzeugt. Diese Tabelle besitzt keinen Primä rschlü ssel, da sowohl die Namen als auch die ISBN-Nummern mehrfach vorkommen kö nnen. Löschen von Tabellen mit SQL, z.B. die Tabellen BUCH und AUTOR (Listing 65): Listing 65: Löschen der Tabellen BUCH und AUTOR [1] Die Schlü sselwö rter DROP TABLE leiten den Befehl ein. Dann folgt der Name der Tabelle, die gelö scht werden soll, in diesem Fall BUCH . Mit der Tabelle werden auch alle enthaltenen Daten gelö scht. [2] Analog kann die Tabelle AUTOR gelö scht werden. Die Inhalte von Tabellen werden mit den Befehlen SELECT (Suchen), DELETE (Löschen), UPDATE (Änderung) und INSERT (Einfügen) gesucht oder geändert. SEL- ECT , DELETE und UPDATE betreffen immer Mengen von Datensätzen. Die Menge der betroffenen Datensätzen wird durch eine optionale WHERE -Klausel eingeschränkt. Dabei gibt es innerhalb einer Tabelle keine von außen sichtbare Reihenfolge der Datensätze. INSERT betrifft immer nur einen einzigen Datensatz. Bricht einer der Befehle mit einem Fehler ab, findet keine Änderung der Datenbank statt, sie wird auf den Zustand vor dem Befehl zurückgesetzt, d.h. er wird entweder ganz oder gar nicht ausgeführt. Damit ist immer sichergestellt, dass die Datenbank konsistent bleibt. Dieses Verhalten wird als Transaktionssicherheit bezeichnet. DROP TABLE BUCH; DROP TABLE AUTOR; 1 2 <?page no="156"?> 156 Schritt 12: Datenbanken mit Java Alle Änderungen, die auf der Datenbank vorgenommen werden, werden nicht sofort ausgeführt, sondern erst nach Bestätigung mit dem Befehl COMMIT . Änderungen (bis zum vorigen COMMIT ) können mit dem Befehl ROLLBACK annulliert werden. Eine Ausnahme ist JDBC: es verfügt über einen Autocommit , alle Änderungen werden sofort durchgeführt. Einfügen eines Datensatzes; z.B. in BUCH und AUTOR (vgl. Listing 66). Listing 66: Eintrag in die Tabellen BUCH und AUTOR [1] Die Schlü sselwö rter INSERT INTO leiten den Befehl ein. [2] Dann folgt der Name der Tabelle: BUCH . [3] In Klammern folgen die Spalten, die befü llt werden sollen ( ISBN , TITEL , PREIS ). [4] Das Schlü sselwort VALUES leitet die Werte ein. [5] In Klammern folgen dann die Werte, die eingetragen werden sollen - sie werden in der Reihenfolge, wie zuvor angegeben, geschrieben. Strings kö nnen in doppelten oder einfachen Hochkommas gesetzt werden - um die Befehle spä ter in Java nutzen zu kö nnen, empfehlen sich einfache Hochkommas. [6] Analog wird die Tabelle AUTOR mit einem Datensatz befü llt. Der sicherlich wichtigste SQL-Befehl ist der SELECT -Befehl. Er erlaubt, aus der Datenbank eine Menge von Datensätzen abzufragen. Die allgemeine Form ist: SELECT spalte_1, spalte_2, … spalte_n FROM tabellenname WHERE bedingung SELECT liefert die Werte der angegebenen Spalten (in dieser Reichenfolge) aus der angegebenen Tabelle. Wobei die Werte der in der WHERE -Klausel definierten Bedingung entsprechen. Sollen alle Werte eines Datensatzes geliefert werden, kann statt den einzelnen Spalten die Wildcard * geschrieben werden. Die WHERE -Klausel kann weggelassen werden - in diesem Fall werden alle Datensätze zurückgeliefert. FROM erlaubt auch, mehrere Tabellennamen (getrennt durch Kommas) anzugeben. In diesem Fall sollten die Namen von einem Variablennamen gefolgt wer- INSERT INTO BUCH (ISBN, TITEL, PREIS) VALUES(9783825244132, 'Wirtschaftsinformatik Wissensordner', 49.99); INSERT INTO AUTOR (NAME, ISBN) VALUES('Marcus Vogt', 9783825244132); 5 6 4 <?page no="157"?> 12.2 Relationale Datenbanken und SQL 157 den, z.B. FROM AUTOR a, BUCH b . Diese Variable kann dann in der WHERE -Klausel zur Qualifizierung genutzt werden. Zur Formulierung der WHERE -Klausel stehen (u.a.) die folgenden Möglichkeiten zur Verfügung: elementare Bedingungen mit NOT , AND oder OR Vergleiche mit < , <= , = , <> (ungleich), >= und > LIKE für String-Muster. Das Sonderzeichen % in String-Muster passt auf beliebig lange String-Teile, das Sonderzeichen _ passt auf beliebige Zeichen. '%Java%' repräsentiert z.B. alle Strings, die das Wort „Java“ enthalten. IS NULL oder IS NOT NULL dient zur Abfrage auf NULL Werte unterschiedlicher Tabellen werden mit zuvor definierten Variablen qualifiziert Im Folgenden werden einige Beispiele zum SELECT -Befehl gezeigt: Alle Datensätze aus der Tabelle BUCH ermitteln: SELECT * FROM BUCH ; Alle Datensätze aus der Tabelle BUCH ermitteln, die 7,99€ oder weniger kosten: SELECT * FROM BUCH WHERE PREIS <= 7.99; Alle Datensätze aus der Tabelle BUCH ermitteln, die Java im Titel haben: SELECT * FROM BUCH WHERE TITEL LIKE '%Java%'; Alle Datensätze aus der Tabelle BUCH ermitteln, die Java im Titel haben und 7,99€ oder weniger kosten: SELECT * FROM BUCH WHERE TITEL LIKE '%Java%' AND PREIS <= 7.99; Preis und Titel der Bücher ermitteln, die „Marcus Vogt“ (mit-)geschrieben hat: SELECT TITEL, PREIS FROM AUTOR a, BUCH b WHERE a.NAME = 'Marcus Vogt' AND a.ISBN = b.ISBN; Dieser Befehl ist besonders interessant, da er zwei Tabellen bearbeitet. Dabei stellt die Bedingung a.ISBN = b.ISBN sicher, dass aus der Tabelle BUCH nur die ISBN-Nummern gewählt werden, die im Datensatz von „Marcus Vogt“ vorkommen. Der UPDATE -Befehl erlaubt, eine Menge von Datensätzen in der Datenbank zu aktualisieren. Die allgemeine Form ist: <?page no="158"?> 158 Schritt 12: Datenbanken mit Java UPDATE tabellenname SET spalte_1=wert_1, spalte_2=wert_2, … spalte_n=wert_n WHERE bedingung Dabei müssen nur Spalten angegeben werden, die auch aktualisiert werden sollen. Zur Aktualisierung dürfen auch die alten Werte auf der rechten Seite der Zuweisung genutzt werden. Das folgende Beispiel zeigt, wie alle Buchpreise unter 20€ um einen Euro erhöht werden: UPDATE BUCH SET PREIS = PREIS + 1 WHERE PREIS < 20; Der DELETE -Befehl erlaubt, eine Menge von Datensätzen aus der Datenbank zu löschen. Die allgemeine Form ist: DELETE FROM tabellenname WHERE bedingung Achtung: DELETE ohne WHERE löscht den Inhalt einer ganzen Tabelle ohne Vorwarnung! Das folgende Beispiel löscht alle Nicht-Java-Bücher aus der BUCH -Tabelle: DELETE FROM BUCH WHERE TITEL NOT LIKE '%Java%' 12.3 Datenbankzugriff mit JDBC 25 JDBC definiert ein Paket von Klassen für den dynamischen Zugriff auf Datenbanken aus Java heraus. Die Klassen für den Datenbankzugriff mit JDBC liegen in den Paketen java.sql . Die wesentlichen Aktionen für den Programmierer sind dabei: Verbindung zur Datenbank herstellen (mit den Klassen DriverManager und Connection ) Eine SQL-Anweisung als String formulieren und ausführen (mit den Klasse Statement ) Eine Ergebnismenge verarbeiten (mit den Klassen ResultSet und ResultSetMetaData ) Fehler behandeln (mit der Klasse SQLException ) 25 Die folgenden Beispiele beziehen sich auf die Derby-Datenbank - für andere Datenbanken ist das Vorgehen aber analog. <?page no="159"?> 12.3 Datenbankzugriff mit JDBC 159 Um einen Datenbankzugriff mit JDBC durchzuführen, muss bei der Ausführung der Treiber der Datenbank im Klassenpfad der Anwendung liegen. Der Treiber für JDBC ist eine jar-Datei, die mit der Datenbank ausgeliefert wird. Im Fall von Derby findet sich die Datei derby.jar im JDK-Ordner unter dem Pfad db/ lib/ . Üblicherweise lässt sich eine jar-Datei in einer IDE einfach in ein Projekt einbinden - die Hilfe der IDE gibt dazu normalerweise unter dem Stichwort „buildpath“ oder „classpath“ Auskunft. Um das Programm direkt in der Konsole zu starten, ist (unter Windows) der folgende Aufruf notwendig (ggf. muss der Pfad des Derby-Treibers angepasst werden - Achtung: der Abschluss mit ; . ist wichtig! ): java -classpath "C: \jdk1.8.0\db\lib\derby.jar; ." DBProgramm Listing 67 zeigt, wie von Java aus mit Hilfe der zuvor beschriebenen SQL-Anweisungen eine Datenbank erstellt, geöffnet und befüllt wird. Listing 67: Programmatische Erzeugung und Befüllung der Tabellen BUCH und AUTOR import java.sql.*; public class DBInitialisieren { public static void main(String[] args) throws SQLException { Connection conn = DriverManager .getConnection("jdbc: derby: daten/ db; create=true"); Statement stmt = conn.createStatement(); stmt.executeUpdate("CREATE TABLE BUCH (" + "ISBN BIGINT NOT NULL, " + "TITEL VARCHAR(256), " + "PREIS FLOAT, " + "PRIMARY KEY (ISBN))"); stmt.executeUpdate("CREATE TABLE AUTOR (" + "NAME VARCHAR(30), " + "ISBN BIGINT)"); stmt.executeUpdate("INSERT INTO BUCH (ISBN, TITEL, PREIS)" + "VALUES (9783825239497, 'Java Lerntafel', 7.99)"); stmt.executeUpdate("INSERT INTO BUCH (ISBN, TITEL, PREIS)" + "VALUES (9783825244132, 'Wirtschaftsinformatik " + "Wissensordner', 49.99)"); / / weitere Datensätze stmt.executeUpdate("INSERT INTO AUTOR (NAME, ISBN)" + "VALUES ('Marcus Vogt', 9783825244132)"); stmt.close(); conn.close(); System.out.println("Datenbank initalisiert"); } } 1 2 3 4 5 6 7 <?page no="160"?> 160 Schritt 12: Datenbanken mit Java [1] Import der notwendigen Klassen - um Platz zu sparen, wird hier die Wildcard zum Import verwendet. [2] Alle SQL-Anweisungen kö nnen eine SQL-Exception auslö sen - um Platz zu sparen, wird der Fehler hier weitergereicht. [3] Aufbau der Verbindung zur Datenbank. Der dazu notwendige String "jdbc: derby: db/ daten; create=true" besteht aus den folgenden Teilen: - Das Protokoll ( jdbc ): Es gibt an, dass die Verbindung mit Hilfe von JDBC stattfinden soll. - Der Datenbanktyp ( derby ): Er gibt an, dass es sich dabei um eine Derby- Datenbank handelt. Auf Basis dieses Eintrags wird die entsprechende Derby-Treiberklasse zur Laufzeit geladen. 26 - Der Datenbankpfad ( daten/ db ): Er gibt an, wo die Datenbank liegt (im Verzeichnis daten ) und wie sie heißt ( db ). Es ist auch mö glich, einen absoluten Pfadnamen anzugeben. - Weitere (durch Semikolon abgetrennte) Optionen: in diesem Fall die Option create=true , die anzeigt, dass die Datenbank, falls sie noch nicht existiert, angelegt werden soll. Weitere Optionen sind z.B. Benutzername und Passwort, falls die Datenbank passwortgeschü tzt ist. [4] Erzeugung eines Statement-Objekts fü r die verbundene Datenbank. [5] Anlegen der Tabellen BUCH und AUTOR mit den zuvor beschriebenen Eigenschaften. Der SQL-Befehl wird als String der Methode executeUpdate ü bergeben. Die Methode executeUpdate(String) fü hrt Datenbankä nderungen durch, d.h. sie sollte fü r CREATE -, INSERT -, UPDATE -, DELETE - und DROP -Befehle genutzt werden. Derby ist standardmä ßig im Autocommit-Modus, d.h. wenn der Befehl erfolgreich durchgefü hrt ist, fü hrt Java automatisch ein Commit aus. [6] Eintrag der Datensä tze in die Tabellen. [7] Schließen des Statements und der Verbindung. Damit werden die Ressourcen wieder frei gegeben. Listing 68 zeigt, wie in einem Java-Programm mit Hilfe der zuvor beschriebenen SQL-Anweisungen die Datenbank abgefragt wird. 26 Bis zur Java-Version 6 musste diese Klasse explizit geladen werden. Ab Version 7 geschieht dies automatisch. <?page no="161"?> 12.3 Datenbankzugriff mit JDBC 161 Listing 68: Abfrage der Tabellen BUCH und AUTOR [1] Import der notwendigen Klassen. [2] Weiterreichen der SQL-Exceptions. [3] Aufbau der Verbindung zur Datenbank. Der dazu notwendige String "jdbc: derby: db/ daten; create=false" unterscheidet sich zu Listing 68, indem er eine fehlende Datenbank nicht erzeugt - in diesem Fall wü rde der Verbindungsaufbau fehlschlagen. [4] Erzeugung eines Statement-Objekts fü r die verbundene Datenbank. [5] Abfrage aller Datensä tze der Tabelle BUCH . Der SQL-Befehl wird als String der Methode executeQuery ü bergeben. Die Methode executeQuery (String) fü hrt Datenbankabfragen aus, d.h. sie sollte fü r SELECT -Befehle genutzt werden. Der Befehl liefert ein ResultSet -Objekt mit den gefundenen Datensä tzen zurü ck. import java.sql.*; public class DBAbfragen { public static void main(String[] args) throws SQLException { Connection conn = DriverManager .getConnection("jdbc: derby: daten/ db; create=false"); Statement stmt = conn.createStatement(); System.out.println("Alle Bücher abfragen"); ResultSet rs = stmt.executeQuery("SELECT * FROM BUCH"); while (rs.next()) { long isbn = rs.getLong("ISBN"); String titel = rs.getString("TITEL"); double preis = rs.getDouble ("PREIS"); System.out.println("\t" + isbn + " " + titel + ", " + preis + "€"); } System.out.println("\nBücher von 'Marcus Vogt' abfragen"); rs = stmt.executeQuery("SELECT TITEL, PREIS FROM Autor a, BUCH b" + " WHERE a.NAME = 'Marcus Vogt' AND a.ISBN = b.ISBN"); while (rs.next()) { String titel = rs.getString(1); double preis = rs.getDouble (2); System.out.println("\t" + titel + ", " + preis + "€"); }/ / weitere Abfragen stmt.close(); conn.close(); } } 1 2 3 4 5 6 7 8 <?page no="162"?> 162 Schritt 12: Datenbanken mit Java [6] Das ResultSet -Objekt kann mit Hilfe der Methode next(): boolean bearbeitet werden: next() liefert true , falls es einen weiteren Datensatz gibt, sonst false . - Gibt es einen weiteren Datensatz, kann auf die einzelnen Werte des Datensatzes mit Hilfe geeigneter Getter zugegriffen werden. Die Getter haben die Form getInt(String): int , getString(String): String , usw. Als Parameter wird der Name der Spalte angegeben. Es ist in der Verantwortung des Programmierers, den fü r die Spalte passenden Getter auszuwä hlen. [7] Alternativ kö nnen Getter auch die Form getString(int): String , … haben. Der int -Parameter gibt die Position des gesuchten Werts in der Suchanfrage (oder Tabelle bei *). Achtung: Im Gegensatz zu Java beginnt die Zä hlung bei 1. [8] Schließen des Statements und der Verbindung. Üblicherweise repräsentieren Tabellen die Daten von Objekten. Das Lesen und Schreiben von Tabellen geht damit meist einher mit dem Erzeugen von Objekten aus den gelesenen Daten oder Extrahieren der Daten aus Objekten, um sie zu schreiben. <?page no="163"?> Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung <?page no="164"?> 164 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um grafische Ein- und Ausgabe von Daten mit Hilfe der Swing-Klassenbibliothek. Welche Schlagwörter lerne ich kennen? GUI AWT Swing Widget Listener Wofür benötige ich dieses Wissen? Grafische Benutzeroberflächen sind notwendig für die Kommunikation mit dem Benutzer. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, das Konzept von ereignisbasierter Programmierung zu erläutern. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1241 <?page no="165"?> 13.1 Benutzeroberflächen 165 Im Kapitel 11 „Datenströme/ Streams“ wurde die zeichenorientierte Ein- und Ausgabe vorgestellt. Moderne Systeme verfügen üblicherweise über eine grafische Benutzeroberfläche. Für solche Benutzeroberflächen gibt es eine Reihe von Bibliotheken. Im Folgenden wird hier die Swing-Bibliothek genutzt, die mit der Java-Standard-Edition ausgeliefert wird und die es erlaubt, moderne Oberflächen zu implementieren. Die Swing-Bibliothek ist mit etwa 300 Klassen und Interfaces sehr umfangreich und kann hier nicht abgedeckt werden. In diesem Teil werden die grundlegenden Elemente der Interaktion vorgestellt. Damit ist es möglich, einfache Fenster mit Schaltflächen, Ein- und Ausgabeelementen zu erstellen und auf Ereignisse zu reagieren. Im Kapitel 14 werden komplexere Oberflächen vorgestellt. 13.1 Benutzeroberflächen Die in Kapitel 11 gezeigten Streams erlauben es, Daten über die Konsole ein- und auszugeben. Dabei findet die Eingabe über Tastatur und Kommandozeile statt, das System reagiert nur auf (Enter) und Eingaben werden erst nach Drücken von verarbeitet. Demgegenüber stehen graphische Benutzeroberflächen (Graphical User Interfaces „GUI“) mit einer Ereignissteuerung. Dabei können alle Eingabegeräte (Tastatur, Maus, ggfs. weitere Geräte) genutzt werden, die Oberflächen können unmittelbar auf eine Eingabe reagieren und es besteht die freie Wahl der Eingabefelder durch Mausklick. Die GUI-Klassen in Java sind in zwei Stufen historisch gewachsen. Seit Java 1.1 gibt es den Abstract Window Toolkit ( AWT ). Das Prinzip von AWT ist es, die Widgets (Dialogelemente) des Betriebssystems direkt zu nutzen. Dies führt aber dazu, dass nur der kleinste gemeinsame Umfang aller Betriebssystemelemente genutzt werden kann. Die Programme sind dadurch zwar schnell und portabel, die Menge der Widgets ist aber eingeschränkt und AWT wirkt „altmodisch“. Seit Java 1.2 gibt es Swing , das z.T. auf den AWT-Klassen aufbaut. Swing implementiert alle Widgets in Java selbst, dadurch hat man die volle Kontrolle über das Aussehen und Verhalten. Swing-Klassen erkennt man typischerweise an dem Präfix „J“ (z.B. JButton ). Swing ist sehr flexibel, hat aber den Nachteil, dass es deutlich mehr Rechenleistung als AWT benötigt - was aber bei heutigen Prozessorleistungen nicht mehr ins Gewicht fällt. Darüber hinaus gibt es noch zahleiche weitere Oberflächenbibliotheken für Java, z.B. SWT (www.eclipse.org/ swt/ ) oder JFace (wiki.eclipse.org/ JFace). Auf Basis der Oberflächenbibliotheken gibt es Interface-Builder (z.B. Matisse von Netbeans). Mit einem Interface-Builder kann die grafische Oberfläche „gezeichnet“ und Code an vorgesehenen Stellen hinzugefügt werden. Dies erlaubt eine <?page no="166"?> 166 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung zunächst einfache Erstellung von Oberflächen, allerdings kann der daraus erzeugte Code meist nur mit dem Erstellungswerkzeug gewartet werden und enthält oft ineffiziente und umständliche Code-Teile. Wir empfehlen deshalb, Interface-Builder nur für Prototypen zu nutzen und die eigentliche Benutzeroberfläche „von Hand“ zu erstellen. 13.2 Aufbau von Swing-Oberflächen Die Swing-Klassen finden sich im Paket javax.swing und seinen Unterpaketen. Evtl. noch benötigte AWT-Klassen finden sich im Paket java.awt und seinen Unterpaketen. Um eine grafische Benutzeroberfläche zu erstellen, muss eine eigene Klasse für die Oberfläche erstellt werden. Diese Klasse ist üblicherweise eine Unterklasse von JFrame , ist die aktive Komponente und enthält die main -Methode, zeigt Daten an und erlaubt, Daten zu manipulieren. Klassen mit Daten und Geschäftslogik sind meist passive Komponenten, die von der Oberflächenklasse angestoßen werden. Eine Oberflächenklasse wird als Unterklasse von JFrame definiert. Die Klasse enthält meist die main -Methode, die die Oberfläche startet. Im Konstruktor werden die Widgets initialisiert, das Layout der Widgets festgelegt, die Interaktion zwischen den Widgets definiert, und das Gesamtlayout des Fensters festgelegt. Listing 69 zeigt eine einfache Oberflächenklasse, die ein Fenster öffnet. import java.awt.Dimension; import javax.swing.JFrame; public class Oberflaeche extends JFrame{ private static final int WIDTH = 300, HEIGHT = 200; public Oberflaeche(){ / / hier ... / / Widgets definieren / / Widget-Layout festlegen / / Widget-Interaktion definieren 1 2 3 4 5 <?page no="167"?> 13.2 Aufbau von Swing-Oberflächen 167 Listing 69: Aufbau einer Oberflächenklasse Die wesentlichen Elemente sind dabei: [1] Import der notwendigen AWT- und Swing-Klassen. [2] Oberflä chenklasse als Unterklasse von JFrame . [3] Definition der Breite und Hö he des Fensters in Pixeln. [4] Konstruktor der Oberflä chenklasse, der alle Interaktionselemente zusammensetzt. [5] Hier werden spä ter die einzelnen Widgets definiert und platziert. [6] Definition des Gesamtlayouts und -verhaltens bestehend aus : - Titel des Fensters: setTitle("Oberfläche") ; alternativ kann der Titel auch mit Hilfe von super("Oberfläche") zu Beginn gesetzt werden. - Grö ße des Fensters (in Pixeln) this.setPreferredSize(…) - Standardplatzierung beim OÜ ffnen gemä ß der Strategie des Betriebssystems: this.setLocationByPlatform(true) - Beenden der Applikation beim Schließen (Drü cken des „Schließen-Kreuzes“): this.setDefaultCloseOperation(EXIT_ON_CLOSE) - fehlt dieser Befehl, wird das Fenster nur unsichtbar, lä uft aber noch im Hintergrund. - Layout realisieren: this.pack() - erst dadurch werden die zuvor definierten Layouteinstellungen umgesetzt. Fehlt dieser Befehl, erscheint zunä chst nur die Titelleiste. - Fenster sichtbar machen: this.setVisible(true) - fehlt dieser Befehl, bleibt das Fenster nur unsichtbar, lä uft aber im Hintergrund. [7] main -Methode, die die Oberflä che erzeugt und damit das Fenster ö ffnet. g this.setTitle("Oberfläche"); this.setPreferredSize(new Dimension(WIDTH, HEIGHT)); this.setLocationByPlatform(true); this.setDefaultCloseOperation(EXIT_ON_CLOSE); this.pack(); this.setVisible(true); } public static void main(String[] args){ new Oberflaeche(); } } 6 7 <?page no="168"?> 168 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung Abb. 15: Fenster der Oberfläche aus Listing 69 13.3 Einfache Widgets Das Beispiel aus Listing 69 ist bisher ein leerer Rahmen, der noch keine Widgets enthält. Um Widgets darstellen zu können, müssen sie in einem Swing-Container platziert werden. Der gefüllte Container wird dann dem JFrame übergeben, das ihn dann (mit seinem Inhalt) darstellen kann. Dieser darzustellende Container wird Content Pane genannt. Die Methode zum Setzen ist setContentPane (Container) , wobei als Parameter alle Swing-Elemente möglich sind (da alle eine Unterklasse von Container darstellen). Der Standardcontainer von Swing ist die Klasse JPanel. JPanel kann beliebige weitere Container-Objekte (also alle Swing-Elemente) aufnehmen. JPanel kann folgendermaßen genutzt werden: Erzeugung: JPanel p = new JPanel() ; Hinzufügen eines Swing-Elements: p.add(Container); - dabei sind als Parameter alle Swing-Elemente möglich (auch weitere JPanels ) Die Elemente werden standardmäßig von links nach rechts, und von oben nach unten verteilt, andere Strategien werden später vorgestellt. Die wichtigsten Darstellungselemente zur Anzeige von Text und der Entgegennahme von Reaktionen sind JLabel (eine Beschriftung), JTextField und JTextArea (Textein-/ -ausgabeelemente) und JButton (eine Schaltfläche). Diese Elemente haben die folgenden Eigenschaften: Die Beschriftung JLabel dient dazu, nicht-editierbare Texte („Beschriftungen“) anzuzeigen. - Erzeugung: JLabel l = new JLabel ("Beschriftung"); Textfeld JTextField zur Eingabe und Darstellung einer Textzeile. - Erzeugung JTextField f = new JTextField(int columns) ; dabei gibt <?page no="169"?> 13.3 Einfache Widgets 169 der Parameter die Anzahl der Spalten ( ≈ Buchstaben) an. Falls kein Wert angegeben wird, passt sich das Feld dem verfü gbaren Platz an. - Setzen des Texts: f.setText("Ein Text") ; Lesen des Texts: String s = f.getText() ; - Weitere Methoden, um Verhalten festlegen: z.B. keine Eingabe zulassen f.setEditable(false) ; Standard ist true . Die Textkomponente JTextArea - Zur Eingabe und Darstellung mehrerer Textzeilen. - Erzeugung: new JTextArea(int rows, int columns) ; dabei geben die Parameter die Zeilen und Spalten an. Falls kein Wert angegeben wird, passt sich das Feld dem verfü gbaren Platz an. - Zentrale Methoden sind: setText(String) - einen Text setzen, append(String) - einen Text anhä ngen, getText(): String - den gesamten Text zurü ckgeben, getSelectedText(): String - den selektierten Text zurü ckgeben . Schaltfläche JButton - Definiert eine Schaltflä che, die gedrü ckt werden kann (wie auf einen „Druck“ reagiert werden kann, wird im nä chsten Abschnitt gezeigt). - Erzeugung: JButton b = new JButton("Drücken") ; der Parameter legt die Beschriftung der Schaltflä che fest. - Verhalten festlegen: f.setEnabled(false) ; false bedeutet, dass die Schaltflä che deaktiviert ist; Standard: true Listing 70 zeigt auf Basis des vorgestellten Rahmens eine grafische Benutzeroberfläche, die eine Beschriftung, Textfelder und einen Button enthält. Startet man die Klasse, erscheint das folgende Fenster (Abbildung 16), in dem Texte eingegeben werden können und die Schaltfläche gedrückt werden kann (allerdings noch ohne Effekt). Abb. 16: Gestartete Benutzeroberfläche <?page no="170"?> 170 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung Listing 70: Eine grafische Oberfläche mit Label, Textfeldern und Schaltfläche Das Programm besteht aus folgenden Elementen: [1] Import der notwendigen Klassen [2] Definition der Fenstermaße. [3] Erzeugung eines Standardcontainers zur Aufnahme der Widgets. [4] Erzeugung der Beschriftung, Textfelder und Schaltflä che: eine Beschriftung mit Titel „Eingabe“. ein einzeiliges Textfeld der Grö ße 20. import java.awt.Dimension; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.JTextField; public class Oberflaeche extends JFrame{ private static final int WIDTH = 300, HEIGHT = 200; public Oberflaeche(){ JPanel p = new JPanel(); JLabel l = new JLabel("Eingabe"); JTextField f = new JTextField(20); JTextArea a = new JTextArea(3, 25); JButton b = new JButton("Drücken"); f.setText("Beispieltext"); a.setText("Beispieltext\nüber mehrere\nZeilen"); p.add(l); p.add(f); p. add(a); p.add(b); this.setContentPane(p); this.setTitle("Oberfläche mit Widgets"); this.setPreferredSize(new Dimension(WIDTH, HEIGHT)); this.setLocationByPlatform(true); this.setDefaultCloseOperation(EXIT_ON_CLOSE); this.pack(); this.setVisible(true); } public static void main(String[] args){ new Oberflaeche(); } } 1 2 3 4 5 6 7 8 <?page no="171"?> 13.4 Interaktion mit Widgets 171 ein Textfeld mit drei Zeilen und 25 Spalten ein Button mit dem Namen „Drü cken“ [5] Initialisierung der Textfelder mit einem Text. [6] Hinzufü gen der Widgets zum Standardcontainer. [7] Eintragen des Standardcontainers als Content-Pane des Fensters. [8] Definition der ü brigen Fenstereigenschaften (siehe Listing 69) und Start der Oberflä che. 13.4 Interaktion mit Widgets Wird die Schaltfläche im vorigen Programm gedrückt, hat das noch keinen Effekt. Dazu muss eine Interaktion zwischen den Komponenten eingeführt werden. Interaktionen in graphischen Benutzeroberflächen sind ereignisgesteuert . Eine Aktion (z.B. Mausklick, Tastatureingabe) an einer Komponente löst ein Ereignis aus. Damit eine Komponente auf ein Ereignis reagieren kann, muss ein Listener -Objekt erzeugt werden, das an die Komponente gebunden wird. Das Listener-Objekt implementiert in seinen Methoden die Reaktionen auf die Ereignisse und ändert z.B. andere Komponenten, was wiederum dort Ereignisse auslösen kann. Java sieht für verschiedene Interaktionsformen je nach Ereignis unterschiedliche Listener vor. So „hört“ z.B. ein ActionListener auf das Drücken der Schaltfläche. 27 Listener in Java sind zunächst Interfaces, d.h. wenn man einen Listener erstellen möchte, muss man das zugehörige Interface implementieren. Der zur Schaltfläche zugehörige Listener ist das Interface ActionListener , das die Methode actionPerformed(ActionEvent e) vorschreibt. In dieser Methode kann die Reaktion auf den Druck der Schaltfläche definiert werden. Das Listener- Objekt wird mit der Methode addActionListener(ActionListener) zu der Schaltfläche hinzugefügt. So wäre z.B. ein ActionListener, der auf einen Button- Druck reagiert, folgendermaßen zu realisieren (Listing 71): 27 Weitere Listener werden in Kapitel 14 vorgestellt. <?page no="172"?> 172 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung Listing 71: Definition eines ActionListeners [1] Import der notwendigen Klassen und Interfaces. [2] Die neue Listener-Klasse ButtonListener muss das Interface ActionListener implementieren. [3] Eine JTextArea wird als private Instanzvariable ü ber den Konstruktor gesetzt - dieses Textfeld soll von dem Listener nach dem Drü cken bearbeitet werden. [4] ActionListener verlangt die Implementierung der Methode actionPerformed(ActionEvent) . [5] Hier wird nun ein Text an die JTextArea angehä ngt. import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JTextArea; public class ButtonListener implements ActionListener { private JTextArea area; public ButtonListener(JTextArea area) { this.area = area; } public void actionPerformed(ActionEvent e) { area.append("\nButton gedrückt"); } } import java.awt.*; import javax.swing.*; public class Oberflaeche extends JFrame{ …public Oberflaeche(){ JPanel p = new JPanel(); …JButton b = new JButton("Drücken"); …b.addActionListener(new ButtonListener(a)); p.add(l); p.add(f); p. add(a); p.add(b); this.setContentPane(p); … }… } 1 2 3 4 6 5 <?page no="173"?> 13.4 Interaktion mit Widgets 173 [6] Zusä tzlich zum vorigen Listing 70 wird ein ButtonListener -Objekt (mit der JTextArea als Parameter) erzeugt und mit der Methode add- ActionListener(…) an die Schaltflä che gebunden. Damit wird die Methode actionPerformed(…) des Listeners jedes Mal ausgefü hrt, wenn die Schaltflä che gedrü ckt wird. Abb. 17: Das Fenster, nachdem einmal der Button gedrückt worden ist In dem Beispiel greift der Listener auf die JTextArea zu. Dazu muss sie als Parameter übergeben werden. Da Listener oft nur wenige Codezeilen, aber häufigen Zugriff auf andere Widgets benötigen ist diese Vorgehensweise sehr umständlich. Für diesen Fall ist es in Java deshalb möglich, implementierende Instanzen (und auch Unterklassen) direkt bei der Erzeugung der Variablen als „anonyme Klassen“ zu definieren. Dies geschieht dadurch, dass man ein Objekt von diesem Interface mit dem new -Operator erzeugt und dabei mit einem Klassenkörper versieht. In diesem Klassenkörper müssen die für das Interface definierten Methoden implementiert werden, dabei kann aber auf die Variablen der Umgebung unmittelbar zugegriffen werden. Das vorige Beispiel kann als anonyme Klasse folgendermaßen formuliert werden (Listing 72): import java.awt.*; import javax.swing.*; public class Oberflaeche extends JFrame{ …public Oberflaeche(){ JPanel p = new JPanel(); …JTextArea a = new JTextArea(3, 25); JButton b = new JButton("Drücken"); …p.add(l); p.add(f); p. add(a); p.add(b); <?page no="174"?> 174 Schritt 13: Graphische Benutzeroberflächen mit Swing: Einführung Listing 72: Ein anonymer ActionListener Die wesentlichen Elemente sind dabei: [1] Der ActionListener wird mit new ActionListener(){…} direkt als anonyme Klasse erzeugt und fü r die Schaltflä che gesetzt. [2] Wie zuvor muss die Methode actionPerformed innerhalb des Kö rpers der anonymen Klasse implementiert werden. [3] Dies erlaubt nun auf einfache Art und Weise den direkten Zugriff auf die anderen Variablen und - in diesem Fall - die AÜ nderung der JTextArea . Der Nachteil für diesen Ansatz ist, dass jede weitere Nutzung erneut definiert werden muss. Trotz dieses Nachteils halten wir diesen Ansatz für den, der bei der Erstellung einer Oberfläche zuerst verfolgt werden sollte, da er - in Kombination mit der in Kapitel 14 vorgeschlagenen Codeorganisation - den übersichtlichsten Code produziert. 28 28 In der Literatur findet sich auch oft der Ansatz, die Oberflächenklasse selbst gleichzeitig zur implementierenden Klasse zu machen. Davon raten wir ab, da es Interaktion und Darstellung zu sehr verwischt und nur schwer zu testen ist. ( ) ( ) ( ) ( ) b.addActionListener(new ActionListener(){ public void actionPerformed(ActionEvent e) { a.append("\nButton gedrückt"); } }); this.setContentPane(p); … }… } 1 2 3 <?page no="175"?> Schritt 14: Graphische Benutzeroberflächen mit Swing: komplexere Oberflächen <?page no="176"?> 176 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Lernhinweise und Prüfungstipps Was erwartet mich in diesem Kapitel? In diesem Kapitel geht es um weitere Konzepte und Möglichkeiten bei der grafischen Ein- und Ausgabe von Daten mit Hilfe der Swing-Klassenbibliothek. Welche Schlagwörter lerne ich kennen? Model-View-Controller (MVC) Layout-Manager Wofür benötige ich dieses Wissen? Die Konstruktion von grafischen Benutzeroberflächen mit Hilfe des MVC- Konzepts ist notwendig, um eine bessere Wartbarkeit zu erzielen. Der Einsatz von Layout-Managern erlaubt die einfache Anordnung von Oberflächenelementen. Welchen Prüfungstipp kann ich aus diesem Abschnitt ziehen? In Prüfungen wird häufig gefordert, das MVC-Konzept zu erläutern oder die verschiedenen Layout-Manager abzugrenzen. Die Lernfragen zu diesem Kapitel finden Sie unter: https: / / narr.kwaest.io/ s/ 1242 <?page no="177"?> 14.1 Komplexere Oberflächen 177 Aufbauend auf dem vorigen Kapitel wird an einem umfangreichen Beispiel gezeigt, wie eine komplexere Oberfläche für eine Anwendung erstellt werden kann. Anhand dieses Beispiels werden die folgenden Aspekte vorgestellt: Das Model-View-Controller-Konzept , das eine Trennung von Geschäftslogik und Oberfläche vorsieht. Weitere Widgets, die es erlauben, auch komplexere Interaktionen zu realisieren. Layout-Manager, die es erlauben, die Platzierung von Widgets zu kontrollieren. 14.1 Komplexere Oberflächen Im vorigen Abschnitt konnten alle Oberflächenelemente direkt im Konstruktor definiert und initialisiert werden. Dies wird selbst bei einfachen Oberflächen schnell unübersichtlich. Aus diesem Grund schlagen wir vor, unterschiedliche Aktionen in Hilfsmethoden zu gruppieren, um so mehr Überblick zu behalten. Eine Oberflächenklasse wird als Unterklasse JFrame definiert. Der Konstruktor sollte folgendermaßen aufgebaut werden: [1] Instanzierung der benötigten Daten und Widgets. [2] Initiale Belegung der Widgets in einer Methode init Widgets() . [3] Festlegung des Widgets-Layouts in einer Methode widgetLayout (): JPanel . [4] Festlegung der Widgets-Interaktionen in einer Methode widgetInter action() . [5] Definition des grundlegenden Layouts der Oberflächenklasse in einer Methode frameLayout() . Listing 73 zeigt den typischen Aufbau einer (unvollständigen) Oberflächenklasse. import java.awt. …; import java.swing.…; public class Oberflaeche extends JFrame { private static final String TITLE = "Oberfläche"; private static final int HEIGHT = …, WIDTH = …; private Daten daten; private J… widget1, widget2, …; 1 2 3 <?page no="178"?> 178 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Listing 73: Aufbau einer Oberflächenklasse Die wesentlichen Elemente sind dabei: [1] Import der notwendigen Klassen. [2] Oberflä chenklasse als Unterklasse von JFrame . [3] Definition des Fenstertitels und der Hö he und Breite des Fensters in Pixeln. [4] Deklaration der Daten und Widgets. [5] Konstruktor der Oberflä chenklasse, der alle Interaktionselemente zusammensetzt. public Oberflaeche() { daten = new Daten(…); widget1 = new J…(…); initWidgets(); JPanel content = widgetLayout(); widgetInteraction(); setContentPane(content); frameLayout(); } private void initWidgets() { / / Initialisierung der Oberflächenelemente. } private JPanel widgetLayout() { JPanel panel = new JPanel(); / / Platzierung der Oberflächenelemente / / auf dem panel. return panel; } private void widgetInteraction() { / / Erzeugung der Listener und Festlegung / / der Interaktionen. } private void frameLayout() { this.setTitle(TITLE); this.setPreferredSize(new Dimension(WIDTH, HEIGHT)); this.setLocationByPlatform(true); this.setDefaultCloseOperation(EXIT_ON_CLOSE); this.pack(); this.setVisible(true); } public static void main(String[] args) { new Oberflaeche(); } } 45 789 10 7 8 9 10 →→ → → 6 <?page no="179"?> 14.2 Übersicht über das Anwendungsbeispiel 179 Abb. 18: Skizze der geplanten Oberfläche [6] Erzeugung der Daten und Widgets. [7] Hilfsmethode initWidgets() initialisiert die Widgets, z.B. Vorbelegung von Textfeldern. [8] Erzeugung des Widget-Layouts durch die Hilfsmethode widgetLayout() . Diese Methode liefert ein JPanel -Objekt zurü ck, das die gesamte Fensteroberflä che definiert. Dieses Objekt wird mit der Methode setContent- Pane(…) gesetzt. [9] Erzeugung der Widget-Interaktion durch die Hilfsmethode widgetInteraction() . Hier werden die Listener definiert. [10] Definition des Gesamtlayouts in der Hilfsmethode frame Layout() - Titel des Fenster: this.setTitle(…) - Grö ße des Fensters (in Pixeln) this.setPreferredSize(…) - Standardplatzierung beim OÜ ffnen: this.setLocationByPlatform(true) - Applikation beim Schließen beenden: this.setDefaultCloseOperation(EXIT_ON_CLOSE) - Zuvor definierte Layout-Elemente platzieren: this.pack() - Fenster sichtbar machen: this.setVisible(true) 14.2 Übersicht über das Anwendungsbeispiel Als etwas komplexeres Anwendungsbeispiel sollen Bücher verwaltet werden. Abbildung 18 zeigt eine Skizze der geplanten Oberfläche. Die Anwendung soll die folgenden Funktionen realisieren: [1] Buchtitel sollen in einer Liste dargestellt werden. [2] Ein Titel soll ausgewä hlt werden kö nnen. [3] Ist ein Titel ausgewä hlt, sollen Details zum ausgewä hlten Buch angezeigt werden. [4] Ein ausgewä hlter Titel soll auch aus der Liste entfernt werden kö nnen. Deininger, Marcus; Kessel, Thomas: Fit für die Prüfung: Java ISBN 9783825239497, 7,99f€ Fit für die Prüfung: Linux Fit für die Prüfung: UML Fit für die Prüfung: BPMN Wirtschaftsinformatik st… Fit für die Prüfung: Java Fit für die Prüfung: Wirt… Brückenkurs Java Fit für die Prüfung: Java Entfernen 1 2 3 4 <?page no="180"?> 180 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Die Buchdaten werden im Beispiel im Programm erzeugt - es ist aber leicht vorstellbar, dass die Daten aus einer Datenbank, wie in Kapitel 12 gezeigt, kommen können. 14.3 MVC: Trennung von Oberfläche und Anwendung } Listing 74: Die Klasse Buch Der Katalog repräsentiert eine Sammlung von Büchern (Listing 75) Die Buchdaten, die auf der Oberfläche dargestellt werden sollen, werden in einer Klasse Buch und Katalog verwaltet. Ein Objekt der Klasse Buch repräsentiert ein einzelnes Buch (Listing 74) mit den Daten der Autoren, dem Titel, der ISBN- Nummer und dem Preis. public class Buch { private String autoren; private String titel; private long isbn; private double preis; public Buch(String autoren, String titel, long isbn, double preis) { this.autoren = autoren; this.titel = titel; this.isbn = isbn; this.preis = preis; } public String toString() { return titel; } public String getReferenz(){ return autoren + ": \n" + titel + "\nISBN " + isbn + ", " + preis + "€"; } import java.util.ArrayList; import java.util.List; public class Katalog { private List<Buch> buecher = new ArrayList<>(); 1 <?page no="181"?> d Anwendung 181 } Listing 75: Die Klasse Katalog Katalog besitzt dabei die folgenden Aspekte: [1] Die Bü cher werden in einer ArrayList gesammelt. [2] Mit der Methode einfuegen(Buch) kann ein Buch in die Sammlung eingetragen werden. [3] Mit der Methode entfernen(Buch) kann ein Buch aus der Sammlung gelö scht werden. [4] Die Methode getBuecher() liefert die Bü cher der Sammlung zurü ck - fü r die grafische Benutzeroberflä che muss dies als Array sein. [5] Die Klassenmethode getBeispiel() liefert einen Katalog mit einer Reihe von Beispielbü chern zurü ck - in einem „echten“ Programm kä men diese Daten von der Datenbank. Man kann erkennen, dass Katalog und Buch unabhängig von Darstellung und Interaktion sind - denn sie enthalten ausschließlich Geschäftslogik, aber keiner- 14.3 MVC: Trennung von Oberfläche un public void einfuegen(Buch buch){ buecher.add(buch); } public void entfernen(Buch buch){ buecher.remove(buch); } public Buch[] getBuecher(){ Buch[] buecher = new Buch[this.buecher.size()]; for (int i = 0; i < buecher.length; i++) buecher[i] = this.buecher.get(i); return buecher; } public static Katalog getBeispiel(){ Katalog katalog = new Katalog(); katalog.einfuegen( new Buch("Kessel, Thomas; Vogt, Marcus", "Fit für die Prüfung: Wirtschaftsinformatik", 9783825244309L, 12.99)); katalog.einfuegen( new Buch("Deininger, Marcus; Kessel, Thomas", "Fit für die Prüfung: Java", 9783825244323L, 12.99)); / / weitere Bücher return katalog; } 2 3 4 5 <?page no="182"?> 182 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen lei AWT- oder Swing-Elemente. Dies hat den Vorteil, dass dieselben fachlichen Klassen und Daten von unterschiedlichen Benutzeroberflächen auf verschiedene Arten präsentiert werden (z.B. auf PC oder dem Smartphone, in deutschem oder amerikanischem Format usw.) Diese strikte Trennung und Unabhängigkeit der Geschäftslogik und Datenmodell von der Darstellung sollte immer eingehalten werden. Eine Vermischung führt zu unsicherem, undurchsichtigem und schwer wartbarem Code. Die Trennung von Geschäftslogik und Darstellung wird auch das MVC-Prinzip (Model-View-Controller) genannt. Die Daten/ die Logik der Anwendung sind das Model , das keine Darstellungselemente kennt. Die Darstellung (z.B. JFrame ) ist die View , die die Daten und Logik verwendet. Die Listener und ihre Verarbeitungslogik sind die Controller , die die Interaktionselemente anstoßen. Die Anwendung des MVC-Prinzips bedeutet, dass wir im Folgenden die Oberfläche und die Listener auf Basis der bestehenden Geschäftsklassen und ihrer Schnittstellen entwickeln können. 14.4 Weitere Widgets: Auswahllisten Im ersten Schritt sollen nun die Bücher des Katalogs in einer Liste dargestellt werden. Dazu nutzen wir die Liste ( JList ) und den zugehörigen Listener ( List- SelectionListener ). Die JList erlaubt die Einfach-/ Mehrfach-Auswahl aus einer Liste aus Objekten gleichen Typs. Sie hat die folgenden Eigenschaften: Erzeugung mit einem Typparameter (wie z.B. bei der ArrayList ), der sicher stellt, dass nur Elemente des angegebenen Typs eingetragen werden können: new JList<Typ>() oder new JList<Typ>(Typ[]) - in diesem Fall wird die Liste mit den Elementen des ü bergebenen Arrays vorbelegt. Die Objekte werden mit Hilfe ihrer toString -Reprä sentation dargestellt. Wichtige Methoden: setListData(Typ[]) - Belegt die Liste mit den Elementen des ü bergebenen Arrays getSelectedValue(): Typ - Liefert das aktuell selektierte Objekt. Ist kein Objekt selektiert, wird null zurü ckgegeben. <?page no="183"?> 14.4 Weitere Widgets: Auswahllisten 183 addListSelectionListener (ListSelectionListener) - Fü gt einen ListSelectionListener hinzu. setSelectedValue(Object, boolean) - Selektiert das ü bergebene Objekt (wenn es in der Liste ist); wenn true mitgegeben wird, scrollt die Liste automatisch zum selektierten Objekt. Der passende Listener ist durch das Interface ListSelectionListener vorgegeben. Für implementierende Klassen gilt Folgendes: Es muss die Methode valueChanged(Event) implementiert werden. Diese Methode wird ausgelöst wird, wenn ein Objekt selektiert oder deselektiert wird. Nutzt man den Listener, muss man beachten, dass die Methode valueChanged(Event) bei einer Selektion immer mehrmals ausgelöst wird. Grund dafür ist, dass nach dem ersten Auslösen, noch Anpassungen stattfinden können. Aus diesem Grund muss abgefragt werden, ob die Liste bereit ist. Dies kann von dem Event mit der Methode getValueIsAdjusting(): boolean abgefragt werden. Erst wenn die Methode false liefert, sind alle Anpassungen fertig und die Aktion kann ausgeführt werden. Listing 76 zeigt das Programm zur Darstellung der Katalogdaten. import java.awt.*; import javax.swing.*; public class Oberflaeche extends JFrame { private static final String TITLE = "Bücher"; private static final int HEIGHT = 250, WIDTH = 350; private Katalog katalog; private JList<Buch> liste; public Oberflaeche() { katalog = Katalog.getBeispiel(); liste = new JList<>(); initWidgets(); JPanel content = widgetLayout(); widgetInteraction(); setContentPane(content); frameLayout(); } private void initWidgets() { liste.setListData(katalog.getBuecher()); } 1 2 3 4 <?page no="184"?> 184 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Listing 76: D arstellung der Daten in einer Liste Die Struktur des Programms entspricht dem Listing 73, neu sind die folgenden Elemente: [1] Definition der Instanzvariablen fü r Modell ( katalog ) und Widget ( liste ). [2] Initialisierung der Modelldaten (siehe Listing 75) [3] Erzeugung des JList -Objekts. [4] Belegung der Liste mit den Bü chern des Katalogs. [5] Einfü gen der Liste in den Standardcontainer. Der Start des Programms liefert das folgende Fenster (Abbildung 19). Die einzelnen Bücher werden pro Zeile mit Hilfe ihrer toString -Methode dargestellt und können mit der Maus selektiert werden (ohne dass zunächst etwas passiert). private JPanel widgetLayout() { JPanel panel = new JPanel(); panel.add(liste); return panel; } private void widgetInteraction() { / / noch keine Interaktion } private void frameLayout() { / / wie vorher … } public static void main(String[] args) { new Oberflaeche(); } } 5 Abb. 19: Ein Fenster mit einer Liste der Bücher <?page no="185"?> … } Listing 77: Definition einer Oberfläche mit weiteren Widgets Der erste Teil des Listings (Listing 77) definiert das Modell und das Layout der Elemente. [1] Zusä tzlich wird eine Modellvariable auswahl definiert. Diese Variable wird benö tigt, um das aktuell selektierte Buch zu referenzieren. Falls kein Buch selektiert ist, soll sie null enthalten. [2] Definition der zusä tzlichen Text- und Button-Widgets. [3] Initialisierung der Modelldaten (siehe Listing 75) 14.4 Weitere Widgets: Auswahllisten 185 Im zweiten Schritt soll nun ein Textfeld hinzugefügt werden, das die vollständige Referenz für ein selektiertes Buch anzeigt, und eine Schaltfläche, die ein selektiertes Buch löschen kann. Listing 77 zeigt dabei die Definition des Layouts, Listing 78 die Definition der Interaktion. import … public class Oberflaeche extends JFrame { …private Katalog katalog; private Buch auswahl; private JList<Buch> liste; private JTextArea text; private JButton entfernen; public Oberflaeche() { katalog = Katalog.getBeispiel(); liste = new JList<>(); text = new JTextArea(); entfernen = new JButton("Entfernen"); … } private void initWidgets() { liste.setListData(katalog.getBuecher()); auswahl = null; } private JPanel widgetLayout() { JPanel panel = new JPanel(); panel.add(liste); panel.add(text); panel.add(new JLabel("Bücher verwalten")); panel.add(entfernen); return panel; } 1 2 3 4 5 6 <?page no="186"?> 186 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen [4] Erzeugung des JList -Objekts, der JTextArea und des Entfernen-Buttons. [5] Belegung der Liste mit den Bü chern, des Katalogs. Da zu Beginn kein Buch selektiert wird, wird auswahl auf null gesetzt. [6] Einfü gen aller Elemente in den Standardcontainer. Zusä tzlich wird ein Label „Bü cher verwalten“ eingefü gt - dies ist nur ein Text ohne weitere Interaktion und kann deshalb lokal definiert werden. … } Listing 78: Definition der Interaktion [1] Definition der Variablen (wie zuvor). [2] Definition und Zuordnung des ListSelectionListener fü r die Liste in Form einer anonymen Klasse. Der zweite Teil des Listings (Listing 78) definiert nun die Interaktion der Elemente. import … public class Oberflaeche extends JFrame { …private Katalog katalog; private Buch auswahl; liste.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { if(e.getValueIsAdjusting()) return; auswahl = liste.getSelectedValue(); if(auswahl == null) text.setText(""); else text.setText(auswahl.getReferenz()); } }); entfernen.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { if(auswahl ! = null){ katalog.entfernen(auswahl); 7 liste.setListData(katalog.getBuecher()); } } }); } 1 private JList<Buch> liste; private JTextArea text; private JButton entfernen; … private void widgetInteraction() { 2 3 6 4 5 <?page no="187"?> 14.5 Layout-Manager 187 [3] Mit der Methode getValueIsAdjusting() muss zunä chst geprü ft werden, ob die Auswahl abgeschlossen ist. Falls die Selektion noch nicht abgeschlossen ist, soll die Aktion abgebrochen werden. [4] Ist die Auswahl abgeschlossen, kann mit getSelectedValue() das selektierte Objekt ermittelt werden. Falls nichts selektiert ist, liefert die Methode null . Das selektierte Objekt wird in der Variablen auswahl gespeichert. [5] Abhä ngig davon, ob nichts selektiert ist, wird das Textfeld geleert oder mit der Referenz des selektierten Objekts befü llt. [6] Definition und Zuordnung des ActionListener fü r den Button. [7] Falls ein Element ausgewä hlt worden war, wird es aus dem Katalog entfernt. Die Liste wird mit dem jetzt reduzierten Buchbestand neu gefü llt. Dies lö st ein neues Listenereignis aus, das wiederum den ListSelectionListener mit dem zuvor beschriebenen Verhalten anstö ßt. Der Start des Programms liefert das folgende Fenster (Abbildung 20). Die Details zu einem selektierten Buch werden nun dargestellt, ein selektiertes Buch kann gelöscht werden. Abb. 20: Ein Fenster mit einer Liste 14.5 Layout-Manager Während die Interaktion nun so wie zu Beginn spezifiziert ist, ist das Layout der Komponenten noch nicht zufriedenstellend. Das bisherige Layout erlaubte, Elemente mit add(…) von links nach rechts und oben nach unten zu platzieren. Die <?page no="188"?> 188 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Elemente verschieben sich mit dem Ändern der Fenstergröße - das Fenster versucht, alle Elemente immer im verfügbaren Platz in der angelegten Reihenfolge unterzubringen. Layout-Manager erlauben, dieses Verhalten zu ändern und zu kontrollieren. Java besitzt mehrere Layout-Manager. Wir werden die folgenden (wichtigsten) betrachten: FlowLayout : Standardverhalten der Komponenten, wie zuvor beschrieben. BorderLayout : Einteilung in einen Hauptbereich in der Mitte und vier Hilfsbereiche an den Rändern. Die enthaltenden Komponenten werden proportional gestreckt. GridLayout : Einteilung in ein frei bestimmbares Raster. Alle Widgets werden gleichmäßig verteilt. Die Komponenten werden üblicherweise mit Hilfe von JPanels (mit unterschiedlichen Layouts) ineinander geschachtelt. Abbildung 21 zeigt ein solches typisches Layout. Abb. 21: Eine Oberfläche mit unterschiedlichen Layouts Das BorderLayout teilt das Fenster in fünf Bereiche ein, die einzeln angesprochen werden können (Abbildung 22). Die Mitte („CENTER“) enthält üblicherweise die wesentlichen Darstellungselemente, die Randbereiche unterstützende Elemente. Unten („SOUTH“) werden oft Schaltflächen oder andere Steuerelemente angebracht. JPanel mit BorderLayout JList JButton JPanel mit FlowLayout JPanel mit GridLayout JTextArea <?page no="189"?> 14.5 Layout-Manager 189 Abb. 22: Bereiche des BorderLayout Das BorderLayout wird für eine Komponente (meist ein JPanel ) folgendermaßen gesetzt: JPanel p = new JPanel(); p.addLayoutManager(new BorderLayout()); oder direkt JPanel p = new JPanel(new BorderLayout()); Widgets können dann folgendermaßen platziert werden: p.add(widget1, BorderLayout.CENTER); p.add(widget2, BorderLayout.SOUTH); Das GridLayout teilt das Fenster in gleich große Zeilen und Spalten - add befüllt die Elemente von links nach rechts und oben nach unten. Abbildung 23 zeigt ein GridLayout mit drei Zeilen und zwei Spalten. Abb. 23: Ein 3 x 2- GridLayout Oben („NORTH“) Unten („SOUTH“) Links („WEST“) Rechts („EAST“) Mitte („CENTER“) Hauptfenster oft Buttonleiste 3x2 Grid <?page no="190"?> 190 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Das GridLayout wird für eine Komponente (meist ein JPanel ) folgendermaßen gesetzt: JPanel p = new JPanel(); p.addLayoutManager(new GridLayout(3, 2)); oder direkt JPanel p = new JPanel(new GridLayout(3, 2)); Widgets können dann folgendermaßen platziert werden: p.add(widget1); p.add(widget2); } Listing 79: Angepasstes Layout Listing 79 zeigt die modifizierte Methode widgetLayout() mit den folgenden Elementen (die übrigen Teile bleiben gegenüber der vorigen Fassung unverändert): [1] Festlegung eines BorderLayout fü r das Gesamt-Panel. [2] Definition eines Hilfs-Panels inhalt mit GridLayout - eine Zeile, zwei Spalten. Dabei werden die Zellen von links nach rechts und oben nach unten befüllt. Falls nicht genügend Zellen vorhanden sind, werden sie ergänzt. Alle Komponenten werden dabei auf die gleiche Größe skaliert. Mit diesen Layout-Managern lässt sich die Anwendung nun folgendermaßen verbessern (Listing 79). … public class Oberflaeche extends JFrame { … private JPanel widgetLayout() { JPanel panel = new JPanel(new BorderLayout()); JPanel inhalt = new JPanel(new GridLayout(1, 2)); inhalt.add(liste); inhalt.add(text); panel.add(inhalt, BorderLayout.CENTER); JPanel steuerung = new JPanel(); steuerung.add(new JLabel("Bücher verwalten")); steuerung.add(entfernen); panel.add(steuerung, BorderLayout.SOUTH); return panel; } … 1 2 3 4 5 6 6 <?page no="191"?> 14.6 Strukturierung der Oberfläche 191 [3] Eintrag der Liste als erstes (und damit links) und Eintrag des Textfelds als zweites (und damit rechts); die beiden Elemente werden auf die gleiche Grö ße skaliert. [4] Eintrag des Hilfs-Panels in die Mitte des Gesamt-Panels. [5] Definition eines weiteren Hilfs-Panels steuerung . Falls kein Layout-Manager angegeben wird, wird es standardmä ßig mit FlowLayout versehen. [6] Eintrag eines Labels und einer Schaltflä che auf steuerung -Panel. [7] Eintrag des steuerung -Hilfs-Panels am unteren Rand der Gesamt-Panels. Dieses Layout führt nun zu folgender Benutzeroberfläche (Abbildung 24): Abb. 24: Benutzeroberfläche mit neuem Layout 14.6 Strukturierung der Oberfläche Bisher stehen die Elemente noch ohne Abgrenzung nebeneinander. Zur besseren Übersichtlichkeit lassen sich Elemente mit zusätzlichen Scrollbars und Rahmen versehen. Um Listen oder Text scrollen zu können, werden sie in eine JScrollPane eingebettet. Die JScrollPane ist wiederum eine Komponente, so dass sie direkt in die bestehenden Layouts eingesetzt werden kann. Sie wird folgendermaßen genutzt: JScrollPane sp = new JScrollPane(komponente); andereKomponente.add(sp); Eine komponente wird von einer Scrollpane „eingepackt“ und in dieser Form einer anderen Komponente zugeordnet. Die Jscroll Pane fügt bei Bedarf (wenn ein Element nicht vollständig gezeigt werden kann) eine vertikale oder horizontale Scrollbar ein. Elemente werden mit einem Rahmen versehen, indem ein Border -Objekt hinzugefügt wird. Das folgende Beispiel zeigt, wie eine komponente mit einem schwarzen Rahmen mit einem Titel links oben versehen werden kann: South Center Zelle 1|1 Zelle 1|2 <?page no="192"?> 192 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Border line = BorderFactory.createLineBorder(Color.BLACK); TitledBorder border = BorderFactory.createTitledBorder(line, "Titel"); border.setTitleJustification(TitledBorder.LEFT); komponente.setBorder(border); } Listing 80: Einfügen einer Scrollbar und eines Rahmens [1] Die Liste wird zusä tzlich in eine JScrollPane eingebettet und mit Scroll- Bars versehen. [2] Um das Textfeld wird ein Rahmen mit dem Titel „Referenz“ gezeichnet. Mit diesen Änderungen ist das Beispiel vollständig umgesetzt. Das Fenster erscheint nun folgendermaßen (Abbildung 25): …public class Oberflaeche extends JFrame { … private JPanel widgetLayout() { JPanel panel = new JPanel(new BorderLayout()); JPanel inhalt = new JPanel(new GridLayout(1, 2)); inhalt.add(new JScrollPane(liste)); Border line = BorderFactory.createLineBorder(Color.BLACK); TitledBorder border = BorderFactory.createTitledBorder(line, "Referenz"); border.setTitleJustification(TitledBorder.LEFT); text.setBorder(border); inhalt.add(text); panel.add(inhalt, BorderLayout.CENTER); JPanel steuerung = new JPanel(); steuerung.add(new JLabel("Bücher verwalten")); steuerung.add(entfernen); panel.add(steuerung, BorderLayout.SOUTH); return panel; } … 1 2 <?page no="193"?> 14.7 Weitere Widgets 193 Abb. 25: Fertige Oberfläche mit Scrollbar, Rahmen und Titel 14.7 Weitere Widgets Nachdem in den vorigen Abschnitten das vollständige Beispiel entwickelt wurde, werden in diesem Abschnitt noch weitere Widgets vorgestellt, die für grafische Benutzeroberflächen oft benötigt werden: die Combo-Box ( JComboBox ), Kontrollfeld ( JCheckBox ) und Optionsfeld ( JRadioButton ): Die Combo-Box ist eine Kombination aus Liste und Schaltfläche: eine Auswahl aus der Liste entspricht einem Schaltflächendruck. Das Kontrollfeld simuliert das Ankreuzen auf einem Formular. Das Optionsfeld simuliert das Umschalten an einem Gerät. Abbildung 26 zeigt eine Oberfläche mit den drei Elementen. Abb. 26: Oberfläche mit Combo-Box, Check-Boxen und Radio-Buttons. Rahmen mit Titel Scrollbar <?page no="194"?> 194 Schritt 14: Benutzeroberflächen mit Swing: komplexere Oberflächen Verwendung der Combo-Box Erzeugung (analog zur JList ): jcb = new JComboBox<Type>() oder jcb = new JComboBox<Type>(Type[]) Elemente zur Liste hinzufügen: jcb.addItem(Type) Auswahl abfragen (oft wird eine mögliche leere Auswahl als spezielles Element am Anfang eingetragen): jcb.getSelectedItem(): Object 29 (nicht Type ) jcb.getSelectedIndex(): int (Position des Elements) Der zugehörige Listener ist der ActionListener (siehe JButton ), der ausgelöst wird, wenn ein Element selektiert wird. Verwendung des Kontrollfelds Erzeugung cb = new JCheckBox("Titel") Methoden zur Statusabfrage cb.isSelected(): boolean liefert true , falls das Kontrollfeld ausgewä hlt ist. cb.setSelected(boolean) setzt oder lö scht die Auswahl des Kontrollfelds. Listener ist der ItemListener , der ausgelöst wird, wenn das Kontrollfeld selektiert oder deselktiert wird. Hinweis: normalerweise sollte ein „Check“ keine Aktion auslösen. Der Anwender soll lediglich einen Zustand setzen. Wenn eine Aktion geschehen soll, sollte eine Schaltfläche oder eine Combo-Box verwendet werden. Verwendung des Optionsfelds Erzeugung cb = new JRadioButton("Titel") Methoden zur Statusabfrage (analog JCheckBox ) cb.isSelected(): boolean cb.setSelected(boolean) 29 Im Gegensatz zur JList können (trotz Typ-Parameter) auch andere beliebige Objekte vorkommen, das Ergebnis muss deshalb (mit Risiko) gecastet werden! <?page no="195"?> 14.7 Weitere Widgets 195 Listener ist der ChangeListener , der ausgelöst wird, wenn eine Option geändert wird. Optionsfelder sollen immer nur eine Option erlauben. Um dies umzusetzen, muss ein ButtonGroup -Objekt erzeugt werden. Diesem Objekt werden alle zusammengehörigen Optionsfelder mit der Methode add(RadioButton) zugeordnet. <?page no="197"?> Stichwortverzeichnis A abstract 54 AbstractMap 121 abstrakte Klasse 75 abstrakte Methode 75 ACID-Prinzip 151 aktueller Parameter 53 arithmetische Operatoren 32 Array 48 ArrayList 111 Assoziationsliste 120 Aufzählungstyp 62, 78 Auswahl 38 AWT 165 B Behandlung 86 Block 39 boolean 25 Buffer 128 byte 25 C char 25 Collection 107 D Datei 127 Datenbank 151 Datentyp 25 DDL 154 default-Modifier 54 Delegation 81 Deque 117 DML 154 double 25 do-while-Schleife 41 E Eclipse 16 eindimensionales Array 47 einfache Datentypen 25 Einfachheit 17 Einfügen von Zeichen 98 Enumeration 78 Error 85 Exception 85 Extraktion von Zeichen 95 F Fehlermeldung 89 Feld 47 final 54 finally 87 float 25 formaler Parameter 53 for-Schleife 41 friendly-Modifier 54 G Geheimnisprinzip 64 GUI 165 Gültigkeit 66 <?page no="198"?> 198 Stichwortverzeichnis H Hallo Welt 22 HashSet 114 I IDE 16 if-else-Verzweigung 39 Index 48 Information Hiding 63 int 25 integrierte Entwicklungsumgebung 16 Interface 62, 76 Iteration 38, 41 Iterator 107 J Java 15 Java Embedded 19 Java Enterprise Edition 19 Java Software Development Kit 15 Java Standard Edition 19 JDBC 158 JDK 15 JEE 19 JSE 19 K Keller 119 Klasse 61, 71 Konstante 28 Konstruktor 71 Kontrollstrukturen 38 L Layout-Manager 189 leere Anweisung 37 LinkedList 111 List 111 Liste 110 Listener 171 Logische Operatoren 33 long 25 Löschen von Zeichen 98 M Map 121 mehrdimensionales Array 48 Menge 113 Methode 53 Methodenkörper 53 Model-View-Controller 183 Modifier 54 N NetBeans 16 Netzwerkfähigkeit 17 Nicht-Zugriffsmodifier 54 O Oberklasse 74 Objekt 71 Objektorientierung 17 Offenheit 18 Open Source 18 Operatoren 31 <?page no="199"?> P Package-Modifier 54 Paket 61 Parameter 53 Plattformunabhängigkeit 16 Polymorphismus 80 Position 48 private 54 protected 54 public 54 Puffer 128 Q Qualifikation 65 Quelle 127 Queue 116 R Reader 135 Referenztypen 25 Rekursion 56 relationale Vergleichsoperatoren 33 Rückgabewert 54 S Schlange 115 Schleifen 41 SDK 15 Senke 127 Sequenz 38 Serialisierung 144 Stichwortverzeichnis 199 Set 114 short 25 Sicherheit 18 Sichtbarkeit 67 Signatur 53 SQL 152 Stack 119 Stapel 119 static 54 Stream 127 String 93 StringBuilder 93 Swing 165 switch-case-Verzweigung 39 T throws 86 TreeMap 121 try-catch 86 Typvergrößerung 30 Typverkleinerung 30 U Umwandlung in einfachen Datentyp 97 Umwandlung nach String 97 Umwandlung von Strings 96 Unterklasse 74 V Variable 27 Vererbung 74 <?page no="200"?> 200 Stichwortverzeichnis W Wertzuweisung 27 while-Schleife 41 Widget 165 Writer 137 Z Zeichenkette 93 Zugriff 49 Zugriffsmodifier 54, 63 zweidimensionales Array 47 <?page no="201"?> Die Programmiersprache Java von Anfang bis Ende durchzuarbeiten und zu erlernen scheint für viele Studierende eine große Hürde zu sein. Nicht mit diesem Arbeitsbuch. Es führt Schritt für Schritt und leicht verständlich in die Programmiersprache ein. Das Buch umfasst 14 Kapitel: Einführung in Java; Variablen, Datentypen, Operatoren; Kontrollstrukturen; Felder / Arrays; Methoden; Sichtbarkeit / Gültigkeit; Objektorientierte Konzepte; Ausnahmen / Exceptions; Zeichenketten / Strings; Lineare Datenstrukturen; Datenströme / Streams; Datenbanken mit Java; Graphische Benutzeroberflächen mit Swing: Einführung; komplexere Oberflächen. Zahlreiche Übersichten und Zusammenfassungen erleichtern das Verständnis. Zudem wird zum Buch ein eLearning-Kurs angeboten. utb+ Das Lehrwerk mit dem digitalen Plus Informatik | Wirtschaftsinformatik ISBN 978-3-8252-6177-1 Dies ist ein utb-Band aus dem UVK Verlag. utb ist eine Kooperation von Verlagen mit einem gemeinsamen Ziel: Lehr- und Lernmedien für das erfolgreiche Studium zu veröffentlichen. utb.de QR-Code für mehr Infos und Bewertungen zu diesem Titel