PROGRAMMIEREN MIT JAVA Unterrichtsbegleitendes Skript „Habe Mut, den eigenen Verstand zu gebrauchen.“ Kant C. Endreß 08/2006 1. Grundlagen der Programmierung 1. Grundlagen der Programmierung Lernziele ☺ Die Begriffe Algorithmus, Programm, Programmiersprache, Compiler und Interpreter kennen. ☺ Wissen, welche grundlegenden Schritte bei der Software-Entwicklung durchlaufen werden und den Begriff Software-Lifecycle kennen. ☺ Die prinzipiellen Verarbeitungsmodelle kompilierter und interpretierter Programmiersprachen kennen. ☺ Algorithmen mit Struktogrammen und Programmablaufplänen beschreiben. Computer und ihre Anwendungen sind aus unserem täglichen Leben sowohl innerhalb der Arbeitswelt als auch im privaten Bereich nicht mehr wegzudenken. Fast überall werden heutzutage Daten verarbeitet und Geräte gesteuert. Ziel dieses Kapitels ist es, Sie mit den grundlegenden Begriffen der Informatik vertraut zu machen. 1.1 Grundbegriffe Computer Als Computer bezeichnet man ein technisches Gerät, das schnell und meist zuverlässig rechnen sowie Daten und Informationen automatisch verarbeiten und speichern kann. Im Unterschied zu einem normalen Automaten, wie z.B. einem Geld- oder Getränkeautomaten, der nur festgelegte Aktionen ausführt, können wir einem Computer die Vorschriften, nach denen er arbeiten soll, stets neu vorgeben. Algorithmus Arbeitsvorschriften oder Handlungsanweisungen wie beispielsweise die Regeln für Kreditberechnungen einer Bank oder die Anleitung zur Steuerung von Signalanlagen einer Modelleisenbahn bezeichnet man in der Informatik mit dem Begriff Algorithmus. Ein Algorithmus ist eine Verfahrensvorschrift zur Lösung eines Problems. Die Bezeichnung Algorithmus wurde abgeleitet vom Namen des arabischen Mathematikers Muhammad Ibn Musa Al Chwarismi aus dem 9. Jahrhundert, der als einer der ersten systematische Lösungsvorschriften für die Lösung von quadratischen Gleichungen in einem Buch zusammenfasste. Die sehr ausgereifte Theorie der Algorithmen gehört zu den wichtigsten Grundlagen der heutigen Informatik. Ein Algorithmus muss … ausführbar sein. Folgende Vorschrift ist z.B. nicht ausführbar: „Wenn man in 14 Tagen Zahnschmerzen bekommt, dann ist bereits heute mit dem Zahnarzt ein Termin auszumachen, um die Wartezeit zu verkürzen“ C. Endreß 1 / 17 11/2006 1. Grundlagen der Programmierung eindeutig beschrieben sein. Die Abfolge der einzelnen Verarbeitungsschritte muss eindeutig festgelegt sein. Programmiersprachen wurden dazu entwickelt, eine eindeutige Beschreibung sicherzustellen. Ein Algorithmus kann aber auch in jedem beliebigen anderen Formalismus dargestellt werden, der die eindeutige Interpretation der Verarbeitungsschritte sicherstellt. endlich sein. D.h. er muss nach endlich vielen Abarbeitungsschritten ein Ergebnis liefern und anhalten (terminieren). allgemein sein. Ein Algorithmus löst ein allgemeines Problem oder eine allgemeine Problemklasse und nicht nur ein spezielles Problem. Die Wahl eines einzelnen, aktuell zu lösenden Problems aus dieser Klasse erfolgt durch Parameter. Unter dem Gesichtspunkt der „Wiederverwendbarkeit“ von Programmen ist es besonders wichtig, mit Algorithmen möglichst allgemeine Problemklassen zu lösen. 20 cm c 10 cm b 30 cm a Spezielles Verfahren: Die Oberfläche des Quaders mit den Kantenlängen 30 cm, 10 cm und 20 cm beträgt: Allgemeines Verfahren: Die Oberfläche des Quaders mit den Kantenlängen a, b und c berechnet sich nach der Formel: O = 2ab + 2bc + 2ac 2 * 30 cm * 10 cm + 2 * 10 cm * 20 cm + 2 * 30 cm * 20 cm = 2200 cm² Programme Um dem Computer einen Algorithmus in einer präzisen Form mitzuteilen, muss man den Algorithmus als Programm formulieren. Programme bestehen aus speziellen Handlungsanweisungen, die ein Computer „verstehen“ kann. Programme sind daher konkreter und eingeschränkter als Algorithmen. Sie werden durch eine Programmiersprache beschrieben und enthalten für die Daten eine bestimmte Darstellung. Computersystem Der Computer mit seinen Programmen wird häufig auch als Computersystem bezeichnet. Generell gesehen setzt sich ein Computersystem zusammen aus den materiellen Teilen, der sogenannten Hardware, und den immateriellen Teilen, der sogenannten Software. Unter dem Begriff Software versteht man neben den Programmen auch zugehörige Daten und Dokumentationen. Man unterscheidet Systemsoftware und Anwendungssoftware. Zur erst genannten Gruppe zählt man überlicherweise das Betriebssystem, Compiler, Datenbanken, Kommunikationsprogramme und spezielle Dienstprogramme. Anwendungssoftware wie beispielsweise Textverarbeitungs- oder Zeichenprogramme dient dem Anwender zum Lösen von Aufgaben. C. Endreß 2 / 17 11/2006 1. Grundlagen der Programmierung Informatik Im Laufe der Entwicklung der Computer- und Softwaretechnik von den Kindertagen der ersten Rechner mit Transistortechnik und integrierten Schaltkreisen in Großrechnern über die ersten Mikroprozessoren bis zu den heutigen leistungsfähigen PCs hat sich mit der Informatik eine eigenständige Wissenschaftsdisziplin entwickelt, die sich mit der theoretischen Analyse und Konzeption sowie der konkreten Realisierung von Computersystemen befasst. Programmiersprache Ein Programm ist ein Algorithmus, der in einer Programmiersprache formuliert ist. Diese Sprache erlaubt es den Anwendern bzw. Programmieren, mit dem Computer zu „sprechen“ und diesem Anweisungen zu geben. Dieses „Sprechen“ kann auf unterschiedliche Weise erfolgen. Sprich mit mir! Programmiersprachen dienen der Kommunikation Mensch – Maschine und zeichnen sich durch einen exakten Formalismus aus, der gekennzeichnet ist durch Syntax (Rechtschreibung und Grammatik) und Semantik (Bedeutung) Es gibt zum Beispiel Sprachen, die der Prozessor des Computers direkt „versteht“. Man nennt diese Sprachen Maschinensprachen oder Assemblersprachen. Programme in solchen Sprachen kann der Prozessor direkt ausführen. Allerdings ist man mit solchen maschinennahen Sprachen stark abhängig vom Prozessortyp und muss den Algorithmus dem jeweiligen Prozessor anpassen. Alternativ gibt es sogenannte benutzernahe bzw. problemnahe Programmiersprachen, die man als höhere Programmiersprachen oder auch problemorientierte Programmiersprachen bezeichnet (z.B. Pascal, Delphi, C/C++, Java). Diese Sprachen ermöglichen problemnahe, prozessorunabhängige Programmierung. Man benötigt allerdings einen Übersetzer in Form einer speziellen Software, der die Anweisungen der höheren Programmiersprache in Maschinensprache überführt. Maschinensprache 01110110 11110110 Assembler 01101101 LD RG1 23 11011100 Frühe problemorientierte MOV RG7 RG2 00101100 Programmiersprache ADD RG2 RG1 01001011 ADD RG2 RG7 10 PRINT “HALLO“ 10011010 LD RG5 Java 2035 SET A = 7 DIV RG2 RG4 30 GOSUB F1 public class HelloWorld { MOV RG5 40 RG7 PRINT “WELT“ public static void main(String[] args) { 50 GOTO 130 System.out.println(“Hallo Welt!”); } } C. Endreß 3 / 17 11/2006 1. Grundlagen der Programmierung Programmieren Unter Programmieren versteht man eine Tätigkeit, bei der unter Verwendung einer gegebenen Programmiersprache ein gestelltes Problem gelöst wird. Programmieren heißt also nicht einfach nur, ein Programm einzutippen. Normalerweise ist eine ganze Reihe von Arbeitsschritten nötig, bis ein gestelltes Problem zufrieden stellend mit dem Computer gelöst ist. Die Bezeichnung Programmieren umfasst daher meist eine ganze Kette von Arbeitsvorgängen, beginnend bei der Analyse des Problems und endend bei der Kontrolle oder Interpretation der Ergebnisse. Oft stellt dabei die Analyse den aufwändigsten Teil dar. Bei der Problemanalyse oder auch Modellierung müssen wir meistens ein umgangssprachlich formuliertes Problem analysieren und so aufbereiten (modellieren), dass sich die einzelnen Teilprobleme leicht und übersichtlich programmieren lassen. Dazu erstellen wir eine algorithmische Beschreibung für die Lösung des Problems bzw. seiner Teilprobleme. Man fasst diese Beschreibungen auch unter dem Begriff Entwurf zusammen. Anschließend wird der Entwurf auf einem Computersystem implementiert, d.h. realisiert und umgesetzt. Dabei nennt man das Übertragen der algorithmischen Beschreibung in eine Programmiersprache und Eingeben des Programms in den Computer auch Kodierung. Programme, die zum ersten Mal kodiert werden, sind fast niemals fehlerfrei. Man unterscheidet sogenannte Syntaxfehler, die durch einfache Schreibfehler oder falschen Einsatz von Sprachelementen entstehen, und sogenannte Semantikfehler, die bei falschem logischem Aufbau des Programms auftreten. Während Syntaxfehler bereits bei der Kodierung vom Compiler erkannt werden, zeigen sich Semantikfehler erst beim Ablauf des Programms. Auch insgesamt gesehen kann ein Programm eine falsche Struktur haben, was meistens auf eine fehlerhafte Problemanalyse zurückzuführen ist. Selbst nach umfangreichen Tests müssen „richtig“ erscheinende Programme nicht zwangsweise logisch korrekt sein. Erfahrungsgemäß enthält ein hoher Prozentsatz aller technisch-wissenschaftlichen Programme auch nach umfassenden Tests noch Fehler, die nur durch hohen Aufwand oder durch Zufall entdeckt werden. Bedenken Sie beim Programmieren, dass Programme nicht nur vom Autor, sondern besonders bei industriellen Software-Entwicklungen von vielen Personen gelesen werden. Deshalb gilt: Eine Dokumentation der Programme ist unerlässlich. Das Testen, Warten und Pflegen von Software wird durch gute Dokumentationen erheblich erleichtert. Das reduziert Kosten! C. Endreß 4 / 17 11/2006 1. Grundlagen der Programmierung Der Software-Lifecycle Analyse Ziel: exakte, eindeutige und vollständige Beschreibung des zu lösenden Problems Ausdrucksmittel: Umgangssprache, Prozessdiagramme, UML Unabhängig von technischen Systemen Was soll gelöst werden? Wartung Sichert reibungslosen Betrieb eines Programms im Dauereinsatz Entwurf Software lebt länger als Hardware Wie soll es gelöst werden? Ziel: Bauplan des Programms, Algorithmische Beschreibung Testen Ausdrucksmittel: Struktogramme, Programmablaufpläne, UML, Pseudocode, Umgangssprache Ziel: korrektes, robustes Programm Implementierung sichert nur Grundfunktion Systematisches Testen ist schwierig Implementierung Ziel: Funktionsfähiges Programm Ausdrucksmittel: Programmiersprache C. Endreß 5 / 17 11/2006 1. Grundlagen der Programmierung Verarbeitungsmodelle Grundsätzlich gibt es für die Überführung von Programmen, die in problemorientierten Programmiersprachen formuliert wurden, zwei verschiedene Verarbeitungsmodelle. Compiler Ein Übersetzer, der problemorientierte Programme in maschinennahe Programme übersetzt, heißt Compiler. Der Compiler übersetzt das gesamte Quell-Programm (geschrieben in der Programmiersprache) in ein Ziel-Programm bzw. eine ausführbare Datei (executable). Während der Übersetzung prüft der Compiler, ob die Syntax (Rechtschreibung) des Programms fehlerfrei ist. => Hohe Effizienz Das Zielprogramm kann beliebig oft ausgeführt werden ohne erneutes Kompilieren. Jede Plattform erfordert einen eigenen Compiler, mit dem das Zielprogramm zu übersetzen ist. Programmiersprachen sind oft nicht plattformunabhängig definiert. Traditionelle Compilersprachen: Pascal, C, C++ Beispiel: Programmausführung eines C++-Programms Quell-Programm in C++ Compiler für C++ und Windows Compiler für C++ und Linux Compiler für C++ und MacOS Ziel-Programm für Windows Ziel-Programm für Linux Ziel-Programm für MacOS Legende: Teilprodukt Tätigkeit C. Endreß Windows Linux 6 / 17 MacOS 11/2006 1. Grundlagen der Programmierung Interpreter Der Interpreter analysiert das Quell-Programm Anweisung für Anweisung und führt jede Anweisung sofort aus, bevor er die folgende Anweisung analysiert. Ein Interpreter ermöglicht die Plattformunabhängigkeit von Programmen. Interpreter haben eine schlechtere Performance als Compiler. Populäre interpretierte Sprache: Java Beispiel: Programmausführung eines Java-Programms Quell-Programm in Java Compiler für Java Plattformunabhängiger Byte-Code Virtuelle Maschine für Windows Virtuelle Maschine für Linux Virtuelle Maschine für MacOS Windows Linux MacOS Legende: Teilprodukt Tätigkeit Die Programmiersprache Java wurde mit dem Ziel entwickelt, plattformunabhängig zu sein. Ein Compiler übersetzt das Quell-Programm in sogenannten Java-Bytecode, der zwar plattformunabhängig ist, jedoch nicht unmittelbar ausgeführt werden kann. Erst der Java-Interpreter analysiert den erzeugten Byte-Code und führt ihn schrittweise aus. Der Interpreter selbst wird vom Prozessor ausgeführt und verdeckt die Eigenschaften des jeweiligen Prozessortyps, wodurch sich eine höhere „Abstraktionsschicht“ bietet. Da man sich diese Abstraktionsschicht als einen gedachten Prozessor vorstellen kann, spricht man von einer virtuellen Maschine (virtual machine), abgekürzt VM. C. Endreß 7 / 17 11/2006 1. Grundlagen der Programmierung 1.2 Kontrollstrukturen Kontrollstrukturen steuern den Ablauf eines Algorithmus. Kontrollstrukturen sollen es ermöglichen, Problemlösungen in natürlicher, problemangepasster Form zu beschreiben, so beschaffen sein, dass sich die Problemstruktur im Algorithmus widerspiegelt, leicht lesbar und verständlich sein, eine leichte Zuordnung zwischen statischem Algorithmustext und dynamischem Algorithmuszustand erlauben, mit minimalen Konzepten ein breites Anwendungsspektrum abdecken, Korrektheitsbeweise von Algorithmen erleichtern. In den 70er Jahren hat sich herausgestellt, dass man mit den folgenden vier Strukturen auskommt, um den Ablauf eines Algorithmus bzw. Programms zu steuern: Sequenz „Mach was!“ Auswahl „Mach was, falls …“ Wiederholung „Mach was wieder und wieder“ Aufruf anderer Algorithmen „Mach jenes …“ Notationsformen Die Darstellung dieser vier Kontrollstrukturen kann in verschiedenen Notationen angegeben werden: Die Struktogramm-Notation beruht auf einem Vorschlag von Nassi und Shneiderman, daher auch Nassi-Shneiderman-Diagramm genannt, und ermöglicht eine grafische Darstellung von Kontrollstrukturen. Die Notation ist in DIN 66261 genormt. Die Pseudo-Code-Notation ist eine textuelle, semiformale Darstellungsform in Anlehnung an problemorientierte Programmiersprachen. Während für die Kontrollstrukturen die Syntax und die Wortsymbole von Programmiersprachen verwendet werden (z.B. if, switch, while), werden für die Anweisungen entweder verbale Formulierungen oder mehr oder weniger programmiersprachliche Notationen benutzt. Der im Folgenden benutzte Pseudo-Code orientiert sich an der Programmiersprache Java. Die Programmablaufplan-Notation (PAP) benutzt grafische Symbole, die durch Linien miteinander verbunden sind, um Kontrollstrukturen zu beschreiben. PAPs bzw. Flussdiagramme sind seit 1969 in Gebrauch und genormt in DIN 66001. C. Endreß 8 / 17 11/2006 1. Grundlagen der Programmierung 1.2.1 Sequenz Erfordert eine Problemlösung, dass mehrere Anweisungen hintereinander auszuführen sind, formuliert man eine Sequenz bzw. eine Aneinanderreihung. Bei der Sequenz erfolgt die Abarbeitung von oben nach unten. Sequenz allgemein Erläuterung Ein beliebig groß gewähltes Viereck wird nach jeder Anweisung mit einer horizontalen Linie abgeschlossen Anweisung 1 Struktogramm Pseudo-Code Anweisung 2 Die einzelnen Anweisungen werden durch Semikolon voneinander getrennt. Anweisung 1; Anweisung 2; Anweisung 1 PAP Einfache Anweisungen werden durch Rechtecke dargestellt, die wiederum durch Ablauflinien verbunden werden. Anweisung 2 Beispiel: Sequenz zur Berechnung des Mehrwertsteuerbetrags sequentielle Abfolge der Verarbeitungsschritte Eingabe Nettopreis Multipliziere Nettopreis mit 16 % Gib Ergebnis aus 1.2.2 Auswahl Sollen Anweisungen in Abhängigkeit von bestimmten Bedingungen ausgeführt werden, dann verwendet man das Konzept der Auswahl bzw. Verzweigung. Es gibt drei verschiedene Auswahl-Konzepte, die jeweils für bestimmte Problemlösungen geeignet sind: einseitige Auswahl zweiseitige Auswahl Mehrfachauswahl C. Endreß 9 / 17 11/2006 1. Grundlagen der Programmierung Auswahl (ein- und zweiseitig) allgemein Erläuterung Ausdruck ja Struktogramm nein Ja-Anweisung(en) Pseudo-Code Nein-Anweisung(en) Ist der Ausdruck wahr, werden die Ja-Anweisungen ausgeführt, sonst werden die NeinAnweisungen ausgeführt. if ( Ausdruck ) Ja-Anweisung; else Nein-Anweisung; Das Ergebnis des Ausdrucks muss von Typ boolean sein. Ausdruck ja Ja-Anweisung PAP Bei der einseitigen Auswahl fehlt der Alternativzweig. nein Nein-Anweisung Beispiel: Ein EDV-Großhändler gewährt auf optische PC-Mäuse Mengenrabatt von 5 % ab einer Bestellmenge von 100 Stück. Diese Rabattregelung können wir mit einem Struktogramm oder einem PAP folgendermaßen darstellen: Bestellmenge < 100 ja Rabatt = 0 Bestellmenge < 100 nein nein ja Rabatt = 5 % Berechne Rabattbetrag Rabatt = 0 Rabatt = 5 % Berechne Rabattbetrag C. Endreß 10 / 17 11/2006 1. Grundlagen der Programmierung Muss zwischen mehr als zwei Möglichkeiten gewählt werden, wird die Mehrfachauswahl verwendet. Mehrfachauswahl allgemein Erläuterung Ausdruck Fall 1 Fall 2 Fall 3 default Struktogramm Anw. 1 Pseudo-Code Anw. 2 AusnahmeAnweisungen Anw. 3 switch ( Ausdruck ) { case konstanterAusdruck1; Anweisung(en); break; case konstanterAusdruck2; Anweisung(en); break; ... default: Anweisung; } Anweisung(en); Der Ausdruck dient als Selektor zum Auswählen der einzelnen Fälle. Ist ein entsprechender Fall nicht aufgeführt, wird die Anweisung hinter default ausgeführt. Ausdruck PAP Fall 1 Fall 2 Fall 3 Fall 4 Anw. 1 Anw. 2 Anw. 3 Anw. 4 Beispiel: Ein Hardwarehändler gewährt seinen Kunden Treue-Rabatt. Dazu teilt er die Kunden in Kategorien ein, die unterschiedliche Rabatte erhalten. Kategorie C. Endreß default 1 2 3 Rabatt = 5 % Rabatt = 10 % Rabatt = 15 % 11 / 17 Rabatt = 0 11/2006 1. Grundlagen der Programmierung 1.2.3 Wiederholung Um eine oder mehrere Anweisungen in Abhängigkeit von einer Bedingung zu wiederholen oder um eine gegebene Anzahl von Wiederholungen zu durchlaufen, verwendet man das Konzept der Wiederholung bzw. Schleife. Drei Wiederholungskonstrukte werden unterschieden: Kopfgesteuerte Schleife: (Wiederholung mit Abfrage vor jedem Wiederholungsdurchlauf) Ist der Ausdruck im Schleifenkopf wahr, wird der Schleifenkörper durchlaufen. Andernfalls wird mit der ersten Anweisung hinter dem Wiederholungsblock fortgefahren. Vor jeder weiteren Wiederholung wird der Ausdruck erneut ausgewertet. Fußgesteuerte Schleife: (Wiederholung mit Abfrage nach jedem Wiederholungsdurchlauf) Der Wiederholungsblock wird auf jeden Fall einmal durchlaufen. Weitere Wiederholungen erfolgen nur, wenn der Ausdruck im Schleifenfuß wahr ist. Andernfalls wird mit der ersten Anweisung hinter dem Schleifenfuß fortgefahren. Zählschleife: (Wiederholung mit fester Wiederholungsanzahl) Liegt bei einem Problem die Anzahl der Wiederholungen von Anfang an fest, dann wird die so genannte Zählschleife bzw. Laufanweisung verwendet. Die Anzahl der Wiederholungen wird durch eine Zählvariable mitgezählt und die Bedingung so gewählt, dass nach der geforderten Wiederholungsanzahl der Abbruch erfolgt. Wiederholung allgemein Solange Ausdruck Wiederholungsanweisung(en) Wiederholungsanweisung(en) Struktogramm Solange Ausdruck for ( Startausdruck, Endausdruck, Schrittweite ) Wiederholungsanweisung(en) do { Fußgesteuerte Schleife: Wiederholung mit Abfrage nach jedem Wiederholungsdurchlauf Zählschleife: Wiederholung mit fester Wiederholungszahl Fußgesteuerte Schleife Wiederholungsanweisungen; } while ( Ausdruck ); for (Startaudruck, Endeausdruck, Schrittweite) { Wiederholungsanweisungen; } C. Endreß Kopfgesteuerte Schleife: Wiederholung mit Abfrage vor jedem Wiederholungsdurchlauf Kopfgesteuerte Schleife while ( Ausdruck ) { Wiederholungsanweisungen; } Pseudo-Code Erläuterung 12 / 17 Zählschleife 11/2006 1. Grundlagen der Programmierung Die PAP-Notation besitzt keine eigenen Darstellungsformen für Schleifenkonstrukte. Wiederholungsstrukturen müssen mit Auswahlsymbolen formuliert werden, wie es das folgende Beispiel zeigt. Beispiel: Es sollen alle geraden Zahlen zwischen 1 und 20 ausgegeben werden. Setze Zahl auf 0 Setze Zahl auf 0 Solange Zahl < 20 Erhöhe Zahl um 2 Zahl < 20 Gib Zahl aus nein ja Erhöhe Zahl um 2 Gib Zahl aus 1.2.4 Aufruf anderer Algorithmen Soll in einem Algorithmus ein anderer Algorithmus angewendet werden, dann geschieht dies durch einen Aufruf. Ein Aufruf erfolgt durch Angabe des Algorithmusnamens, gefolgt von der Liste der aktuellen Parameter. Nach Ausführung des aufgerufenen Algorithmus wird der rufende Algorithmus hinter der Aufrufstelle fortgesetzt. Ein Algorithmus kann sich auch selbst aufrufen (rekursiver Aufruf). Aufruf allgemein Erläuterung Anweisung 1 Struktogramm Operationsname (aktuelle Parameter) Nach Ausführung der aufgerufenen Operation wird der Ablauf hinter der Aufrufstelle fortgesetzt Anweisung 2 Pseudo-Code Anweisung 1; Operationsname ( aktuelle Parameter ); Anweisung 2; Ein Aufruf erfolgt durch Angabe des Operationsnamens gefolgt von der Liste der aktuellen Parameter. Anweisung 1 PAP Operationsname (aktuelle Parameter) Anweisung 2 C. Endreß 13 / 17 11/2006 1. Grundlagen der Programmierung Beispiel: Der Bruttopreis einer Ware ergibt sich aus dem Nettopreis zuzüglich der Mehrwertsteuer. Eingabe Nettopreis BerechneMehrwertSteuer(Nettopreis) Bruttopreis = Nettopreis + Mehrwertsteuer Ausgabe Bruttopreis Schachtelung von Kontrollstrukturen Komplexe Abläufe können durch Schachtelung von Kontrollstrukturen beschrieben werden. Innerhalb von Wiederholungsanweisungen können weitere Wiederholungsanweisungen oder/und Auswahlanweisungen stehen. Im Prinzip kann man die Kontrollstrukturen in beliebiger Kombination beliebig tief ineinander schachteln. 1.2.5 Strukturierte Programmierung Man hat nachgewiesen, dass alle Kontrollflüsse durch eine Auswahlkonstruktion und eine Wiederholungskonstruktion beschrieben werden können. Die vorgestellten Kontrollstrukturen besitzen jeweils genau einen Eingang und einen Ausgang. Zwischen dem Eingang und dem Ausgang gilt das Lokalitätsprinzip, d.h. der Kontrollfluss verlässt den durch Eingang und Ausgang definierten Kontrollflussbereich nicht. Struktogramme Vorteile: Optimale grafische Darstellung von linearen Kontrollstrukturen, da es nicht möglich ist, Sprünge darzustellen. Auswahl wird in ablaufadäquater Form dargestellt, d.h., die Alternativen werden vertikal angeordnet, während in textuellen Darstellungsformen eine horizontale Anordnung erfolgt. Kann leicht zerlegt und zusammengesetzt werden (Modularisierung) Der verfügbare Platz ermöglicht die Wahl aussagekräftiger Namen. Der Vereinbarungsteil kann am Anfang in einem eigenen Kasten beschrieben werden. Programmablaufplan Vorteile: Viele Freiheiten bzgl. Kontrollfluss Nachteile: Für grundlegende Kontrollstrukturen wie Mehrfachauswahl und Wiederholungen gibt es keine eigenen Symbole. Schachtelungsstrukturen sind kaum erkennbar. Schlechte Darstellung von linearen Kontrollstrukturen aufgrund zu großer Freiheiten. C. Endreß 14 / 17 11/2006 1. Grundlagen der Programmierung 1.2.6 Erstes Beispiel Problemstellung: Schlage einen Nagel in die Wand und hänge ein Bild daran auf! Verbale Formulierung des Algorithmus: Schritt Anweisung 1 Hammer in die Hand nehmen. 2 Nagel mit der anderen Hand auf dem Zielpunkt festhalten. 3 Wiederhole die folgenden Anweisungen solange, bis der Nagel eingeschlagen ist oder Du genug hast. { 4 Mit dem Hammer auf den Nagel schlagen. 5 Falls Nagel nicht getroffen { 6 Einige laute Worte sprechen. 7 Hammer weg werfen. 8 Finger desinfizieren und verbinden. } } 9 Falls der Nagel eingeschlagen ist, Bild aufhängen. 10 Sonst jemanden rufen, der sich mit Bilder Aufhängen auskennt. An dieser Stelle ist es Zeit, dass Sie mal wieder aktiv werden. Nehmen Sie Papier und Bleistift zur Hand, und erstellen Sie für den formulierten Algorithmus einen Programmablaufplan und ein Struktogramm! C. Endreß 15 / 17 11/2006 1. Grundlagen der Programmierung 1.2.7 Zweites Beispiel Problemstellung: Es ist ein Algorithmus zur Berechnung der Summe der natürlichen Zahlen von 1 bis n zu formulieren: 1+2+3+...+n=? Die Zahl n wird fest vorgegeben. Seit Carl Friedrich Gauß in seiner Grundschulzeit einmal die natürlichen Zahlen von 1 bis 100 addieren musste, kennt die Mathematik eine geschlossene Formel für diese Aufgabenstellung: Summe = n * (n + 1) / 2 Carl Friedrich Gauß Lösungsansatz Zuerst n festlegen. Summe mit 0 festlegen. Summe durch fortlaufendes Aufaddieren erhöhen. Mit einem Zähler, der von 1 bis n mitläuft, die Additionen kontrollieren Summe ausgeben Verbale Formulierung (für n = 5) Schritt Anweisung 1 Setze n auf den Wert 5. 2 Setze Summe auf den Wert 0. 3 Setze Zähler auf den Wert 1. 4 Wiederhole, solange der Zähler kleiner oder gleich n ist { 5 Addiere den Wert des Zählers zur Summe. 6 Erhöhe den Zähler um 1. } 7 C. Endreß Gib den Wert der Summe aus. 16 / 17 11/2006 1. Grundlagen der Programmierung Schreibtischtest Schritt n Summe Zähler Zähler <= n Schritt n Summe Zähler Zähler <= n 1 5 ? ? ? 5 5 6 3 wahr 2 5 0 ? ? 6 5 6 4 wahr 3 5 0 1 wahr 4 5 6 4 wahr 4 5 0 1 wahr 5 5 10 4 wahr 5 5 1 1 wahr 6 5 10 5 wahr 6 5 1 2 wahr 4 5 10 5 wahr 4 5 1 2 wahr 5 5 15 5 wahr 5 5 3 2 wahr 6 5 15 6 falsch 6 5 3 3 wahr 4 5 15 6 falsch 4 5 3 3 wahr 7 5 15 6 falsch C. Endreß 17 / 17 11/2006 2. Was ist Java? 2. Was ist Java? Lernziele ☺ Das Verarbeitungsmodell von Java kennen. ☺ Die Entstehungsgeschichte von Java kennen lernen. ☺ Wissen, welche besonderen Merkmale die Sprache auszeichnen. Quizfrage Was ist Java? A: Tropische Südseeinsel B: Amerikanische Umgangssprache für „Kaffee“ C: Objektorientierte Programmiersprache D: Die Frau von Bill Gates Lösung: A, B, C 2.1 Wie Java funktioniert Nehmen wir an, Sie möchten eine Anwendung (z.B. eine interaktive Partyeinladung) schreiben, die auf den beliebigen Geräten, die Ihre Freunde besitzen, funktionieren soll. ss Party { public cla ic void public stat args) { g[] main(Strin .println(” System.out ”); Tim Party bei Quellcode für interaktive Partyeinladung l cia _0 pe () ad kes od ect alo o th bj 0 inv Me g.O 1 < 1 lan # ava. J Party b ei Tim Quelle Compiler Code Erstellen Sie ein Quelldokument. Verwenden Sie als Programmiersprache Java. Lassen Sie Ihren Quellcode durch einen Quellcode-Compiler laufen. Der Compiler prüft, ob Fehler vorhanden sind und kompiliert erst, wenn er sicher ist, dass alles richtig läuft. Der Compiler erstellt ein neues Dokument, das in Java-Bytecode kodiert ist. Jedes Gerät, das Java ausführen kann, kann diese Datei interpretieren bzw. in etwas übersetzen, das es ausführen kann. Der kompilierte Bytecode ist plattformunabhängig. C. Endreß Party bei Tim Method Party() 0 aload_0 1 invokespecial # 1 <Method java.lang.Object()> 4 return Method void … 1/6 Party bei Tim Virtuelle Maschinen Ihre Freunde haben keine physische Java-Maschine, sondern alle haben eine virtuelle (in Software implementierte) JavaMaschine, die innerhalb Ihrer elektronischen Geräte läuft. Die virtuelle Maschine liest den Bytecode und führt ihn aus. 07/2006 2. Was ist Java? 2.2 Was Sie in Java machen Sie geben eine Quellcode-Datei ein, kompilieren diese mit dem javac-Compiler und führen den kompilierten Bytecode anschließend auf einer virtuellen Java-Maschine aus. import java.awt.* import java.awt.event; class Party{ public void erstelleEinladung(){ Frame f = new Frame(); Label l = new Label(“Party bei Tim”); Button a = new Button(“Sicher doch”); Button b = new Button(“Ohne mich”); Panel p = new Panel(); p.add(l); p.add(a); p.add(b); } // mehr Code } Quelle Geben Sie Ihren Quellcode ein. Dazu reicht ein einfacher Texteditor. Speichern Sie den Quellcode unter Party.java. Compiler Kompilieren Sie die Datei Party.java, indem Sie auf der Kommandokonsole mit dem Befehl javac die Compiler-Anwendung aufrufen. Wenn der Compiler keinen Fehler im Quellcode findet, generiert er die Bytecode-Datei Party.class. Method Party() 0 aload_0 1 invokespecial #1 <Method java.lang.Object()> 4 return Method void erstelleEinladung() 0 new #2 <Class.java.awt.Frame> 3 dup 4 invokespecial #3 <Method java.awt.Frame> Code Kompilierter Code Party.class. Virtuelle Maschinen Führen Sie das Programm aus, indem Sie die Java Virtual Maschine (JVM) mit der Datei Party.class starten. Die JVM übersetzt die Datei in etwas, das die zugrunde liegende Plattform versteht, und führt das Programm aus. Hinweis: Diese Darstellung soll keine Anleitung für Ihr erstes Programm sein, sondern Ihnen nur ein Gefühl für die Verarbeitung von Java-Programmen vermitteln. C. Endreß 2/6 07/2006 2. Was ist Java? Klassen in der Java-Standardbibliothek 2.3 Historisches zu Java 3500 3000 „Duke“ das Java-Maskottchen 2500 2000 1500 1000 500 0 Java 1.02 1991 C. Endreß Java 1.1 250 Klassen 500 Klassen Java 2 (Version 1.2 – 1.4) Java 5.0 (Version 1.5 und höher) Langsam Etwas schneller 2300 Klassen Hübscher Name, hübsches Logo. Spaß beim Arbeiten, viele Fehler. Applets sind die große Sache. Mehr Fähigkeiten, freundlicher. Beginnt, sehr beliebt zu werden. Besserer GUI-Code Viel schneller 3500 Klassen Kann (manchmal) mit nativer Geschwindigkeit laufen. Ernsthaft, mächtig. Kommt in drei Varianten: Micro Edition (J2ME), Standard Edition (J2SE) und Enterprise Edition (J2EE). Wird zur Sprache der Wahl für neue EnterpriseAnwendungen (insbesondere webbasierte) und mobile Anwendungen. Mehr Power. Erleichtert die Entwicklung Neben dem Hinzufügen von mehr als 1000 Klassen werden in Java 5.0 größere Änderungen an der Sprache selbst vorgenommen, um das Programmieren zu vereinfachen. Außerdem werden neue Features hinzugefügt, die in anderen Sprachen beliebt sind. Alles begann mit einer E-Mail, in der der 25-jährige SUN-Programmierer Patrick Naughton die Firmen- und Produktpolitik seines Arbeitgebers anprangerte. Die Produkte seien zu akademisch, die Benutzung zu kompliziert. Die Klagen wurden von den SUN-Bossen Scott McNealy und Bill Joy erhört. Naughton begann, mit James Gosling und Mike Sheridan in einem neuen Projekt einen Prototyp zur Steuerung und Integration von elektronischen Konsumgeräten (Toaster, Videorekorder, Fernseher etc.) zu entwickeln. 3/6 07/2006 2. Was ist Java? 1992 Naughton und sein Team bauten ein Gerät, das zur Fernbedienung unterschiedlicher elektronischer Konsumgeräte eingesetzt werden konnte. Dieser Prototyp („*7“ StarSeven) glich heutigen Palm-Computern. Er verfügte über ein Betriebssystem (Green-OS) einen Portabler Interpreter (Oak) ein graphisches Subsystem, das eine tastaturlose graphische Oberfläche realisierte eine Drahtlose Datenübertragung 1994 *7 wurde zwar zur Serienreife entwickelt, die Vermarktung scheiterte jedoch. Zu dieser Zeit erkannte Bill Joy das Potential des rasant wachsenden World-Wide-Webs und die Bedeutung einer plattformunabhängigen Programmiersprache, mit der neben aktuellen Inhalten auch Programme transportiert und ohne Installations- und Portierungsaufwand auf einem beliebigen Zielrechner ausgeführt werden können. Daraufhin entwickelten Naughton und Gosling mit der Programmiersprache Oak im Herbst 1994 die erste Version des Browsers WebRunner, der neben der Darstellung von HTMLSeiten auch kleine Java-Programme (Applets) aus dem WWW laden und innerhalb des Browserfensters ausführen konnte. 1995 Oak wurde unter dem Namen HotJava stabilisiert und weiterentwickelt. Der Durchbruch für HotJava kam jedoch erst, als die Firma Netscape, die mit dem Navigator den führenden Browser besaß, sich entschied, die Java-Technologie von SUN zu lizenzieren. Diese Ankündigung von Netscape am 23. Mai 1995 wird von SUN als offizielle Geburtsstunde von Java gesehen. 1996 SUN gab JDK 1.0 (Java Development Kit) frei. 1999 JDK 1.2 wurde auf den Markt gebracht und in Java 2 Platform umbenannt. Wichtigste Neuerungen: Swing Toolset, Java 2D API, Drag-and-Drop API 2002 JDK 1.4 bringt Performance-Verbesserungen, neue Bibliotheken, XML-Unterstützung 2004 JDK 1.5 bringt weitere Verbesserungen und Ergänzungen wie u.a. generische Klassen und Enumerations. Sun ändert die Bezeichnung von JDK 1.5 in J2SE 5.0. 2.4 Java-Programme Applications Applications sind Anwendungen, die selbständig auf einem Computer laufen. Voraussetzung: Eine Java-Laufzeit-Umgebung (JRE = Java-Runtime-Environment) ist auf dem System installiert. Applets Applets sind Programme, die über das Internet von einem Web-Server geladen und in einem WebBrowser ausgeführt werden. Das Sicherheitskonzept der Java-Applets verhindert, dass Applets auf lokale Daten des Systems, auf das sie geladen werden, zugreifen. Voraussetzung: JRE im Browser oder externes JRE durch Java-PlugIn vorhanden. C. Endreß 4/6 07/2006 2. Was ist Java? 2.5 Eigenschaften von Java Einfach Objektorientiert Plattformunabhängig Robust Interpretiert Nebenläufig (unterstützt multi-threading) Verteilt Sicher Dynamisch 2.5.1 Java ist einfach Zu großen Teilen wurde Java von C / C++ abgeleitet. Viele potentielle Fehlerquellen von C & C++ wurden jedoch entfernt. So gibt es in Java z.B. keine Zeiger. Java kennt nur 8 elementare Datentypen. Es gibt nur 50 reservierte Wörter. Java verfügt über ein Memory Management und eine Garbage Collection: Nicht mehr referenzierte Objekt werden automatisch durch die Garbage Collection aus dem Speicher entfernt. Das vermeidet memory leaks. Man bekommt insgesamt eine bessere Performance durch effizientes Memory Management. Java verfügt über eine integrierte Synchronisation von Threads. 2.5.2 Java ist objektorientiert Java Programme bestehen aus Klassen. Jedes Programm besitzt mindestens eine Klasse. Java verfügt über den Mechanismus der Datenkapselung (durch Klassen und Zugriffsbeschränkungen private, public, protected ). Polymorphismus (Abstrakte Klassen und Methoden) ist möglich. Vererbung ist möglich. Dynamic binding ist möglich. 2.5.3 Java ist plattformunabhängig Java ist plattformunabhängig. Der Java Compiler erzeugt Byte Code und nicht Maschinencode. Java Programme können auf jeder Plattform ausgeführt werden, die eine Virtuelle Maschine besitzt. Java-Programme sind portierbar auf jede Plattform, für die eine JVM existiert. Datentypen sind unabhängig von der Implementierung der JVM. C. Endreß 5/6 07/2006 2. Was ist Java? 2.5.4 Java ist robust Folgende Gründe machen Java robust: Strikte Typüberprüfung zur Übersetzungs- und Laufzeit Automatisches Memory Management Automatische Prüfung von Zugriffen auf Arrays und Strings 2.5.5 Java ist sicher Speicheranforderungen und Referenzen werden von der JVM verwaltet. Entwickler haben keinen direkten Zugriff auf den Speicher (keine Pointer). Es wird eine Überprüfung durchgeführt, ob der Byte-Code Zeiger verändert Zugriffsbeschränkungen verletzt Der Byte-Code-Verifier stellt sicher, dass kein Stack-Überlauf oder -Unterlauf stattfindet und die Typen sämtlicher Parameter korrekt sind. 2.5.6 Java ist nebenläufig Mit Nebenläufigkeit bezeichnet man die Fähigkeit eines Systems, zwei oder mehr Vorgänge gleichzeitig oder quasi-gleichzeitig ausführen zu können. Ein Thread ist ein eigenständiges Programmfragment, das parallel zu anderen Threads laufen kann. Ein Thread ähnelt damit einem Prozess, arbeitet aber auf einer feineren Ebene. Während ein Prozess das Instrument zur Ausführung eines kompletten Programms ist, können innerhalb dieses Prozesses mehrere Threads parallel laufen. Der Laufzeit-Overhead zur Erzeugung und Verwaltung eines Threads ist relativ gering und kann in den meisten Programmen vernachlässigt werden. Ein wichtiger Unterschied zwischen Threads und Prozessen ist, dass sich alle Threads eines Programms einen gemeinsamen Adressraum teilen, also auf dieselben Variablen zugreifen, während die Adressräume unterschiedlicher Prozesse streng voneinander getrennt sind. Java hat Threads direkt in der Sprache integriert. Die Klasse Thread enthält Methoden zum Starten, Stoppen und Verwalten von Threads. Die Synchronisation mehrerer Threads ist möglich. Java Laufzeitbibliotheken sind threadsicher. 2.5.7 Java ist dynamisch Klassen können zur Laufzeit geladen werden, weil die Programme interpretiert werden. Anwendungen können auf Änderungen ihrer Laufzeitumgebung reagieren, ohne dass der Code verändert und neu übersetzt werden muss. C. Endreß 6/6 07/2006 3. Aufbau von Anwendungen 3. Aufbau von Anwendungen Lernziele ☺ Wissen, aus welchen Bausteinen ein Java-Programm aufgebaut wird. ☺ Wissen, welche Namenskonventionen in Java bestehen. ☺ Wissen, was Packages sind und wie sie organisiert werden. ☺ Wissen, wie Java-Programme kompiliert und ausgeführt werden. 3.1 Aller Anfang ist schwer Diese sprichwörtliche Erkenntnis gilt naturgemäß auch für das Erlernen einer Programmiersprache. Die Ursache bestimmter Anlaufschwierigkeiten liegt möglicherweise darin begründet, dass selbst eine noch so einfach gehaltene Einführung in das Programmieren nicht ohne ein gewisses Mindestmaß an Formalismus auskommt, um bestimmte Sachverhalte korrekt wiederzugeben. Einsteiger werden dadurch leicht abgeschreckt. Das Erlernen einer Programmiersprache unterscheidet sich im Grunde nicht vom Erlernen einer Fremdsprache wie Englisch oder Französisch. Es gibt eine gewisse Grammatik, die festgelegt ist durch den Wortschatz, die Syntax und die Semantik. Nach den Regeln der Grammatik werden Sätze gebildet. Für diese Sätze werden Vokabeln benötigt. Mit Hilfe der erlernten Worte und der Grammatik der Sprache formen wir Sätze und Texte. In einer Programmiersprache funktioniert das Ganze auf die gleiche Art und Weise. Wir werden lernen, nach gewissen Regeln mit dem Computer zu „sprechen“, d.h. dem Computer verständlich zu machen, was er für uns tun soll. Dazu müssen wir uns zuerst mit gewissen Grundelementen der Programmiersprache vertraut machen. Es ist daher nicht zu vermeiden, sich mit bestimmten formalen Aspekten der Sprache zu beschäftigen. 3.2 Struktur eines Java-Programms Packen Sie eine Klasse in eine Quelldatei. Klassen sind die grundlegenden Ausführungseinheiten in Java. Sie enthalten neben den Methoden auch die Daten, auf denen die Methoden operieren. Grundsätzlich bilden Klassen die Basis der objektorientierten Programmierung. Klasse Methode 1 Anweisung Packen Sie Methoden in eine Klasse Methoden bilden funktionale Einheiten. Sie sind in Klassen definiert. Packen Sie Anweisungen in eine Methode. Anweisungen bilden den Kern eines Java-Programms. C. Endreß Quelldatei 1/9 Methode 2 Anweisung1 Anweisung2 10/2008 3. Aufbau von Anwendungen Was kommt in eine Quelldatei? Quelldateien besitzen die Endung .java. Eine Quelldatei enthält eine Klassendefinition. Obwohl es möglich ist, mehrere Klassen in eine Quelldatei zu schreiben, sollten Sie dieses vermeiden. public class Hund { Die Klassendefinition muss von einem Paar geschweifter Klammern eingefasst werden. Der Name der Quelldatei muss mit dem Namen der Klasse identisch sein. Java ist „case-sensitive“, d.h. Sie müssen Groß- und Kleinschreibung beachten. Klasse } public class Hund { Was kommt in eine Klasse? void bellen(){ Eine Klasse hat eine oder mehrere Methoden, die innerhalb der Klasse deklariert werden müssen. Die Methodendeklaration wird ebenfalls in geschweifte Klammern gefasst. Die Methode bellen() sollte Anweisungen dazu enthalten, wie ein Hund bellen soll. } } Was kommt in eine Methode? Methode public class Hund { In die geschweiften Klammern der Methode schreiben Sie die Anweisungen, die in der Methode ausgeführt werden sollen. void bellen(){ Anweisung1; Anweisung2; } } Anweisungen Die Anatomie einer Klasse Wenn die JVM gestartet wird, sucht sie nach der Klasse, die Sie beim Aufruf der JVM angegeben haben. Anschließend sucht die JVM in dieser Klasse nach einer Methode mit der Bezeichnung main(), die genau so aussehen muss: public static void main(String[] args) { // hier kommt Ihr Quellcode hinein } Mit dieser Methode beginnt die Programmausführung, und die virtuelle Maschine arbeitet alle Anweisungen zwischen den geschweiften Klammern der Methode main() ab. Jede Java-Anwendung muss mindestens eine Klasse besitzen. Eine Klasse der Anwendung muss eine Methode main() in der oben dargestellten Weise enthalten. C. Endreß 2/9 10/2008 3. Aufbau von Anwendungen Das ist eine Klasse public, damit jeder darauf zugreifen kann Die öffnende geschweifte Klammer der Klasse Der Name der Klasse public class MeineErsteAnwendung { (Das kommt später) Der Rückgabetyp void bedeutet, dass es keinen Rückgabewert gibt Dieser Methode muss ein Array von Strings übergeben werden, das „args“ genannt wird. (Spielt noch keine Rolle) public static void main (String[] args) { Der Name der Methode System.out.println (“Java macht Spaß”) ; Sagt, dass etwas auf der Standardausgabe ausgegeben werden soll Die öffnende geschweifte Klammer der Methode Jede Anweisung MUSS mit einem Semikolon enden! Die Zeichenkette, die Sie ausgeben möchten } Die schließende geschweifte Klammer der Methode } Die schließende geschweifte Klammer der Klasse 3.3 Das erste Java-Programm Die Problemstellung Wir schreiben eine einfache Konsolenanwendung zur Mehrwertsteuerberechnung. Das Programm soll zu einem Nettobetrag für den festen Mehrwertsteuersatz von 19 % die Mehrwertsteuer sowie den zugehörigen Bruttobetrag errechnen und ausgeben. Der Lösungsansatz Sie erinnern sich noch an den Software-Lifecycle? Nach der Analyse unseres zugegebenermaßen kleinen Problems wollen wir einen Algorithmus zur Lösung entwerfen. Als Ausdrucksmittel zur Formulierung von Algorithmen verwendet man in der Informatik i.d.R. Diagramme wie Programmablaufpläne (PAP) oder Struktogramme. Mit Diagrammen lassen sich Abläufe besser verdeutlichen als durch umgangssprachliche Beschreibungen. In der folgenden Abbildung wird der Lösungsansatz mit beiden Diagrammtypen dargestellt. C. Endreß 3/9 10/2008 3. Aufbau von Anwendungen Programmablaufplan (PAP) Struktogramm Eingabe: Nettobetrag Eingabe: Nettobetrag Berechne Mehrwertsteuer Berechne Mehrwertsteuer Berechne Bruttobetrag Ausgabe: MwSt und Bruttobetrag Berechne Bruttobetrag Ausgabe: MwSt und Bruttobetrag Der Quelltext Wir erstellen ein Verzeichnis mit der Bezeichnung mehrwertsteuer und speichern die Quelltextdatei MehrwertSteuerBerechnung.java in diesem Verzeichnis. 1 package mehrwertsteuer; 2 import support.Console; 3 4 public class MehrwertSteuerBerechnung { public static void main(String[] args) throws Exception { // Variablendeklaration double mwst, netto, brutto; 5 6 // Konstante final int MWSTSATZ = 19; 7 8 // Eingabe Console.print("Nettobetrag: "); netto = Console.readDouble(); 9 // Verarbeitung mwst = netto * MWSTSATZ / 100; brutto = netto + mwst; 10 // Ausgabe Console.println("Mehrwertsteuer: " + mwst); Console.println("Bruttobetrag : " + brutto); } } Erläuterungen 1 Die Anweisung package mehrwertsteuer; gibt an, zu welchem Paket die Klasse gehört. Die package-Anweisung muss stets die erste Anweisung des Quellcodes sein. Üblicherweise fasst man in Java die Klassen eines Programms zur besseren Strukturierung in Pakten zusammen. In diesem Beispiel gehört die Klasse MehrwertSteuerBerechnung zu dem Paket mehrwertsteuer. Das bedeutet, die Quelldatei muss in einem Verzeichnis namens mehrwertsteuer gespeichert werden. C. Endreß 4/9 10/2008 3. Aufbau von Anwendungen 2 Eine Klasse muss im Java-Quellcode über den vollqualifizierenden Klassennamen angesprochen werden. Dieser setzt sich zusammen aus dem Namen des Packages, in dem sich die Klasse befindet, und dem Namen der Klasse selbst. Allerdings führt diese vollqualifizierende Bezeichnung sehr schnell zu sehr langen Ausdrücken. Diesen Aufwand kann man reduzieren, indem man einzelne Klassen oder ganze Packages mit der Anweisung import einbindet. import-Anweisungen müssen nach der packageAnweisung am Anfang der Datei aufgelistet werden. Dadurch ist die Angabe des Package-Namens bei einem Klassenaufruf nicht mehr nötig. In diesem Beispielprogramm wird die Klasse Console aus dem Package support eingebunden, so dass wir im Quellcode auf den vollqualifizierenden Klassennamen support.Console verzichten und uns auf den einfachen Klassennamen beschränken können. Die Klasse Console stellt Methoden zur einfachen Ein- und Ausgabe in Konsolenanwendungen zur Verfügung (siehe 7 bis 9). Sie ist nicht Bestandteil des JDK von Sun, sondern ist ein externes Paket, das uns die Handhabung von Ein- und Ausgaben im Vergleich zu den Bordmitteln von Java erheblich erleichtert. 3 Das Schlüsselwort class gefolgt vom Namen der Klasse leitet die Definition einer Klasse ein. Die Klasse, die die Methode main() enthält, muss öffentlich sichtbar sein, was durch den Sichtbarkeitsmodifzierer public festgelegt wird. 4 Der Interpreter der virtuellen Maschine beginnt die Ausführung eines Java-Programms immer mit der Methode main(), die folgendermaßen deklariert sein muss: public static void main(String[] args) Fehlt diese Methode in Ihrem Programm, so kann die virtuelle Maschine das Programm nicht ausführen. Die Ergänzung throws Exception ist in unserem Beispiel erforderlich, weil die Methode readDouble() aus der Klasse Console auf fehlerhafte Eingaben mit einer sogenannten Exception (Ausnahme) reagiert. Java besteht darauf, dass jemand die Verantwortung für die Behandlung der Exception übernimmt. Durch den Befehl throws Exception teilen wir dem Programm mit, dass wir uns nicht um die Ausnahme kümmern werden, sondern dass wir die Verantwortung für die Ausnahmebehandlung auf eine übergeordnete Ebene „werfen“. In diesem Fall ist das die virtuelle Maschine. Auf Exception-Handling werden wir zu einem späteren Zeitpunkt eingehen. 5 Aus der Mathematik oder Physik wissen wir, dass Berechnungen mit Formeln ausgeführt werden, die Formelgrößen oder Variablen enthalten. Auch in Programmen arbeiten wir mit Variablen. Bevor wir jedoch eine Variable in einem Programm verwenden können, müssen wir die Variable deklarieren, d.h. dem Programm den Namen der Variablen und die Art der zu speichernden Daten mitteilen. In dieser Zeile deklarieren wir die Variablen mwst, netto und brutto. Die Variablen sollen mit Dezimalzahlen arbeiten. Der entsprechende Datentyp in Java heißt double. Da die drei Variablen von demselben Datentyp sind, können wir sie in einer Anweisung durch Komma getrennt deklarieren. Unsere drei Variablen sind lokale Variablen der main()-Methode, weil wir sie in der Methode main() deklarieren. 6 Den Mehrwertsteuersatz wollen wir in unserem Programm einmal festgelegen und nicht mehr verändern. Wir deklarieren ihn deshalb als Konstante. Einer Konstanten müssen wir bei der Deklaration ein Wert zuweisen, der im Gegensatz zum Wert einer Variablen während des Programmablaufs nicht mehr veränderbar ist. Das Schlüsselwort final, das vor dem Datentyp angegeben wird, deklariert MWSTSATZ als Konstante. Als Datentyp für MWSTSATZ verwenden wir den Ganzzahltyp int. 7 Vor der ersten Benutzereingabe sollten wir dem Benutzer durch eine kurze Anweisung mitteilen, welche Eingabe das Programm erwartet. Die Methode print() der Klasse Console gibt einen Text oder eine Zeichenkette im Konsolenfenster aus. Die auszugebende Zeichenkette muss als Parameter in die Klammern hinter dem Methodennamen eingetragen werden. Man sagt auch, der Parameter wird an die Methode übergeben. Zeichenketten werden in doppelte Hochkommata gesetzt (“Nettobetrag: “). 8 Die Methode readDouble() der Klasse Console liest eine Fließkommazahl des Typs double von der Konsole ein. Der eingelesene Wert wird der Variablen netto zugewiesen. C. Endreß 5/9 10/2008 3. Aufbau von Anwendungen 9 Diese Anweisung ist eine Zuweisung und keine Gleichung. Das Gleichheitszeichen symbolisiert den Zuweisungsoperator, der in der Programmierung nicht dieselbe Bedeutung besitzt wie ein mathematisches Gleichheitszeichen. Arithmetische Ausdrücke dürfen nur auf der rechten Seite des Zuweisungsoperators stehen. Es wird zuerst der arithmetische Ausdruck rechts vom Zuweisungsoperator berechnet (hier: netto * MWSTSATZ / 100). Anschließend wird das Ergebnis der Variablen links vom Zuweisungsoperator (hier: mwst) zugewiesen. 10 Nach der Berechnung der Variablenwerte für mwst und brutto wollen wir unsere Ergebnisse dem Benutzer mitteilen. Die Methode println() der Klasse Console gibt eine Zeichenkette aus, die mit Hilfe des Verkettungsoperators (+) aus der Zeichenkette “Mehrwertsteuer: “ und dem Wert der Variablen mwst gebildet wird. Am Ende der Zeile erfolgt ein Zeilenvorschub, so dass die nächste Ausgabeanweisung den Text in eine neue Zeile schreibt. 3.4 Bezeichner Ein Bezeichner ist eine Folge von Zeichen, die Variablen, Methoden, Konstanten, Klassen und Packages benennt. Bezeichner … können in Java beliebig lang sein. müssen mit einem Buchstaben (‚a’ bis ‚z’ oder ‚A’ bis ‚Z’) beginnen und können dann beliebig fortgesetzt werden (ohne Leerzeichen). Java unterscheidet Groß- und Kleinschreibung! Aus historischen Gründen sind zwar auch noch der Unterstrich (‚_’) oder das Dollarzeichen (‚$’) als Anfangszeichen erlaubt, jedoch entspricht dieses nicht mehr der Konvention. dürfen kein Java-Schlüsselwort sein. müssen in ihrem Gültigkeitsbereich eindeutig sein. 3.5 Namenskonventionen In Java gibt es für die Vergabe von Namen bestimmte Konventionen, die das Verständnis der Quelltexte erleichtern und daher eingehalten werden sollten. Als Bezeichner sollten grundsätzlich aussagekräftige Namen gewählt werden, die die Aufgabe oder Funktion des bezeichneten Elements beschreiben. Namen für Konvention Beispiele Variablen beginnen mit Kleinbuchstaben. Bei zusammengesetzten Wörtern wird der erste Buchstabe jedes folgenden Wortes groß geschrieben. meinGehalt, kundenNummer, mehrWertSteuer Methoden wie Variablennamen, oft ist die erste Silbe ein Verb. print, readDouble, berechneZinsen Klassen beginnen mit einem Großbuchstaben. Bei zusammengesetzten Wörtern wird der erste Buchstabe jedes folgenden Wortes auch groß geschrieben. String, HelloWorld, MehrWertSteuerBerechnung Packages bestehen ausschließlich aus Kleinbuchstaben. Mehrteilige Paketnamen werden durch Punkte separiert. mehrwertsteuer, java.lang, com.borland.jbuilder.ide Konstanten bestehen ausschließlich aus Großbuchstaben. Mehrteilige Konstantennamen können durch Unterstriche separiert werden. C. Endreß 6/9 MWSTSATZ, MAX_ANZAHL 10/2008 3. Aufbau von Anwendungen 3.6 Packages Java organisiert Quelltextdateien (.java) und übersetzte Klassen (.class) in sogenannten Packages. Jede Klasse gehört zu genau einem Package. Der Zugriff auf eine Klasse erfolgt immer über den Package-Namen. Package-Namen bestehen in Java aus einem oder mehreren Wörtern, die durch einen Punkt getrennt sind. Jedes Wort entspricht dabei einem Unterverzeichnis im Verzeichnispfad der gewünschten Klassendatei. Eine Klasse aus dem Paket java.awt.image befindet sich also im Verzeichnis java\awt\image. Die Klassenbibliothek des JDK 1.4 enthält in über 130 Packages mehrere tausend Klassen. Der Umfang des aktuellen JDK 1.5 ist dem gegenüber noch gewachsen. Wichtige Standard-Pakete java.lang Standard-Funktionalitäten (fundamentales Paket, das immer automatisch eingebunden wird) java.math Klassen mit mathematische Methoden java.util Datenstrukturen, Tools und Hilfsklassen javax.swing Swing-Klassen für GUI Wichtige externe Klassen, die das Programmieren auf der Konsolenebene erleichtern. Externe Pakete support.Console Ein- und Ausgabefunktionen für Konsolenanwendungen support.NumberFormat Formatierungs-Methoden für die numerischen Datentypen double und int zur Verfügung. Archive Grundsätzlich müssen sich in Java alle Klassen in Packages, also in einer Verzeichnisstruktur, befinden. Die Verwaltung vieler kleiner Klassen und Pakete innerhalb einer Verzeichnisstruktur hat mehrere Nachteile. Deshalb bietet Java die Möglichkeit, Klassen und Pakete in Archivdateien zusammenzufassen. Ein JavaArchiv ist eine Datei, die mehrere Dateien umfasst und die Endung .jar hat. Vorteile: platzsparend, übersichtlich, schneller Zugriff 3.7 Java-Basics: Punkt für Punkt Eine Java-Datei ist die kleinste Einheit Programm-Code, die der Java-Compiler kompilieren kann. Eine Java-Datei besteht aus: 1. einer optionalen package-Anweisung 2. null oder mehr import-Anweisungen 3. einer (oder mehr) Klassendefinition(en) Anweisungen enden mit einem Semikolon; Codeblöcke werden durch ein Paar geschweifte Klammern definiert {} C. Endreß 7/9 10/2008 3. Aufbau von Anwendungen Das Gleichheitszeichen (=) ist der Zuweisungsoperator und beschreibt in Java keine mathematische Gleichung. Eine Java-Datei darf maximal eine public deklarierte Klasse enthalten. Der Name der Datei muss mit dem Namen der Klasse identisch sein und die Namenserweiterung .java haben. Es ist gute Programmierpraxis, nur eine Klasse pro Java-Datei zu definieren. Begründung: Der Compiler javac erzeugt für jede Klasse, die in einer Quelltextdatei definiert ist, eine eigene Bytecode-Datei (.class) mit dem Namen der Klasse. Eine Bytecode-Datei (.class) hat denselben Namen wie die Klasse, die sie definiert. Eine Java-Anwendung benötigt eine Klasse, mit einer main()-Methode: public static void main(String[] args) Diese main()-Methode ist für den Java-Interpreter der Einstiegspunkt zur Programmausführung. 3.8 Anwendungen kompilieren und ausführen 3.8.1 Der Java-Compiler javac Mit dem Java-Compiler javac werden Java-Quelltextdateien in Byte-Code übersetzt. Der Compiler wird von der Kommandozeile mit folgendem Befehl aufgerufen: javac [Optionen] [Dateinamen] [@Dateiliste] Parameter Bedeutung Optionen -classpath : Klassenpfad zum Suchen benutzerdefinierter Klassen; mehrere Pfade sind bei Windows durch Semikolon zu trennen Dateinamen Namen der Quelltextdateien, die zu übersetzen sind (durch Leerzeichen getrennt) Alle Dateien des Verzeichnisse übersetzen: *.java @Dateiliste Textdatei, die eine Liste der zu übersetzenden Namen enthält Der Compiler erzeugt für jede definierte Klasse des Quelltextes eine Klassendatei (.class). Gibt der Compiler keine Meldung aus, wurde das Programm erfolgreich übersetzt. 3.8.2 Der Java-Interpreter java Der Java-Interpreter kann über zwei Kommandos aufgerufen werden: java [Optionen] Klassendatei [Argumente] führt Anwendungen aus, öffnet bei graphischen Anwendungen zusätzlich ein Konsolenfenster javaw [Optionen] Klassendatei [Argumente] führt unter Windows graphische Anwendungen aus, ohne ein Konsolenfenster zu öffnen C. Endreß 8/9 10/2008 3. Aufbau von Anwendungen Der Name der Klassendatei muss ohne die Endung .class angegeben werden. Argumente werden direkt an die Anwendung übergeben und von der main()-Methode im String-Array args entgegengenommen. Optionen: -classpath (Klassenpfad der benutzerdefinierten Klassen, die das Programm benötigt) -jar C. Endreß (Startet eine Anwendung, die sich in einem Archiv befindet.) 9/9 10/2008 4. Variablen und Ausdrücke 4. Variablen und Ausdrücke Lernziele ☺ Wissen, was Variablen und Konstanten sind, und wie man sie verwendet. ☺ Wissen, welche elementaren Datentypen Java besitzt. ☺ Wissen, welche Operatoren es in Java gibt. ☺ Wissen, wie Ausdrücke zusammengesetzt und ausgewertet werden. Wie bekomme ich Daten in meine Programme? Computerprogramme werden zur elektronischen Verarbeitung von Daten eingesetzt. Im ersten Programmbeispiel des vorangegangenen Kapitels haben wir ein kleines Programm zur Berechnung der Mehrwertsteuer geschrieben. Die Daten, mit denen unser Programm gearbeitet hat, waren der Nettopreis, die Mehrwertsteuer usw. Diese Werte wurden in Variablen gespeichert. Nähere Erklärungen zum Umgang mit Variablen in Java und zu deren Verarbeitung liefert dieses Kapitel. 4.1 Variablen Variablen dienen dazu, Daten im Hauptspeicher eines Programms abzulegen, zu lesen, zu verändern. Schreibender Zugriff auf eine Variable: Es wird eine Information bzw. ein Wert in der Variablen abgelegt. Vorher vorhandene Werte werden überschrieben, d.h. gelöscht. Lesender Zugriff auf eine Variable: Es wird der Wert der Variablen gelesen. Der gespeicherte Wert bleibt dabei unverändert. Beim Lesen wird nur eine Kopie des gespeicherten Wertes erzeugt, die dann weiter verarbeitet wird. Eine Variable ist ein Datenelement eines bestimmten Datentyps, das durch einen Bezeichner (Namen) identifiziert wird. Der Wert einer Variablen kann während des Programmablaufs verändert werden (variabel = veränderbar). In Java gilt: Variablen müssen einen Typ haben. Variablen müssen einen Namen haben. Außerdem hat jede Variable einen Wert und ist in einem Speicherbereich abgelegt. C. Endreß 1 / 13 double nettoBetrag; Typ Name 10/2008 4. Variablen und Ausdrücke Java kümmert sich um Typen. Die Typüberprüfungen werden zur Compile-Zeit vorgenommen. => Java ist typsicher. Variablen müssen vor ihrer ersten Verwendung deklariert werden, d.h. dem Compiler bekannt gemacht werden. Vor der ersten lesenden Verwendung müssen Variablen initialisiert werden, d.h. sie müssen einen definierten Wert erhalten. Wenn möglich sollte eine Initialisierung bereits bei der Deklaration erfolgen. “Einen doppelten Espresso bitte!” Wenn Sie über Variablen nachdenken, denken Sie an Becher - bei Java denken Sie natürlich an KaffeeBecher. Eine Variable ist einfach ein Becher. Ein Behälter. Die Variable enthält etwas. Sie hat eine Größe und einen Typ. In diesem Kapitel interessieren uns zunächst nur die Behälter für die elementaren Typen. Später kommen wir dann noch zu den Bechern, die Referenzen und Objekte enthalten. Auch wenn diese Becher-Analogie zunächst simpel scheinen mag, bietet Sie uns ein vertrautes Mittel, die Dinge in einer einfachen Weise zu betrachten, auch wenn die Zusammenhänge später komplexer werden. Die Becher für elementare Datentypen gleichen den Bechern, die Sie in einem Café bekommen. Es gibt z.B. bei Starbucks Bechergrößen wie „Short“, „Tall“ oder „Grande“ Auch elementare Typen in Java kommen in unterschiedlichen Größen vor, die jeweils unterschiedliche Namen haben. Für Ihre Variablen benötigen Sie also lediglich die passende Bechergröße. Java besitzt vier Behälter für ganzzahlige Datentypen. Statt einer Bestellung wie „Ich nehme einen Tall Hot Chocolate“ sagen Sie dem Compiler einfach „Ich nehme eine int-Variable mit dem Wert 90“. Allerdings müssen Sie in Java Ihrem Becher noch einen Namen geben. Sie sagen also „Ich nehme eine int-Variable mit den Wert 90. Gib der int short byte Variablen den Namen gewicht.“ long Im Quellcode würden Sie diese „Bestellung“ so formulieren: int gewicht = 90; Jede Variable eines elementaren Datentyps belegt eine festgelegte Anzahl von Bits (die Bechergröße) im Arbeitsspeicher des Rechners. Die Größen (in Bit) der 8 elementaren Datentypen von Java sehen Sie hier: byte 8 short 16 int 32 Ganze Zahlen C. Endreß long 64 float 32 double 64 boolean 8 char 16 Fließkommazahlen 2 / 13 10/2008 4. Variablen und Ausdrücke Der Datentyp einer Variablen legt fest, welche Werte die Variable annehmen kann und welche Operationen auf diesen Werten ausgeführt werden können. Elementare Datentypen Anwendung Typname Länge Wertebereich Logik boolean 1 Byte true, false Zeichen char 2 Byte Alle Unicode-Zeichen Ganze Zahlen byte 1 Byte -27 . . . 27 – 1 short 2 Byte -215 . . . 215 – 1 int 4 Byte -231 . . . 231 – 1 long 8 Byte -263 . . . 263 – 1 float 4 Byte +/- 3.4 * 1038 double 8 Byte +/- 1.8 * 10308 Fließkommazahlen Logischer Typ Mit boolean besitzt Java einen eigenen logischen Datentyp und beseitigt damit eine Schwäche von C/C++. Dort konnten Integer-Typen auch für logische Ausdrücke „missbraucht“ werden. In Java muss der Typ boolean dort verwendet werden, wo ein logischer Operand erforderlich ist. Zeichentyp char-Literale werden grundsätzlich in einfache Hochkommata gesetzt: ’a’ String-Literale stehen in doppelten Hochkommata: “Hallo“ Java stellt eine Reihe von Standard-Escape-Sequenzen zur Verfügung, die zur Darstellung von Sonderzeichen verwendet werden können: Zeichen Bedeutung \b Rückschritt (Backspace) \t Horizontaler Tabulator \n Zeilenvorschub (Newline) \f Seitenumbruch (Formfeed) \r Wagenrücklauf (Carriage return) \” Doppeltes Anführungszeichen \\ Backslash Ganze Zahlen Alle ganzzahligen Typen sind in Java vorzeichenbehaftet. Ihre Länge ist auf allen Plattformen gleich. C. Endreß 3 / 13 10/2008 4. Variablen und Ausdrücke Fließkommazahlen Fließkommazahlen werden in Java mit einem Dezimalpunkt geschrieben: 4.678 Die Fließkommatypen in Java setzen sich nach IEEE-754 folgendermaßen zusammen: Vorzeichen-Bit (1 Bit) Mantisse (23 Bit bei float, 52 Bit bei double) Exponent (8 Bit bei float, 15 Bit bei double) Neben den numerischen Literalen gibt es noch einige symbolische Literale in den Klassen Float und Double des Pakets java.lang: Name Bedeutung MAX_VALUE Größter darstellbarer positiver Wert MIN_VALUE Kleinster darstellbarer positiver Wert NaN Not-A-Number NEGATIVE_INFINITY Negativ unendlich POSITIV_INFINITY Positiv unendlich Zeichenketten Zeichenketten-Literale stehen in doppelten Hochkommata: “Hallo“ Zeichenketten werden in Java durch Objekte die Klasse String dargestellt. Die Klasse String ist die wichtigste Datenstruktur für alle Aufgaben, die etwas mit Ein- und Ausgabe oder der Verarbeitung von Zeichen zu tun haben. Sie bietet viele wichtige Methoden zum Verarbeiten von Zeichenketten (z.B. + Operator zur Verkettung). 4.1.1 Variablennamen Variablennamen müssen mit einem Buchstaben beginnen (‘A‘ bis ‘Z‘, ‘a‘ bis ‘z‘) und dürfen dann weitere Buchstaben oder Ziffern enthalten. Variablennamen beginnen nach der Java-Sprachkonvention mit einem Kleinbuchstaben: meinGehalt. Java unterscheidet Groß- und Kleinschreibung. D.h. die Namen zahl, Zahl und zAHL bezeichnen drei unterschiedliche Variablen. Variablennamen dürfen kein von Java reserviertes Schlüsselwort sein. 4.1.2 Deklaration und Initialisierung Bei der Deklaration müssen Datentyp und Name der Variable angegeben werden. Die Deklaration wird wie alle Anweisungen in Java durch ein Semikolon abgeschlossen. Mehrere Variablen gleichen Typs können in einer Liste durch Kommata getrennt in einer Anweisung deklariert werden. Variablendeklarationen dürfen an einer beliebigen Stelle im Programm-Code erscheinen. Mit Hilfe des Zuweisungsoperators (=) können Variablen bei der Deklaration mit einem Wert initialisiert werden. C. Endreß 4 / 13 10/2008 4. Variablen und Ausdrücke Deklaration: Datentyp VariablenName; Initialisierung: Datentyp VariablenName = InitialWert; int anzahl; anzahl = 24; boolean machtSpass; machtSpass = true; double radius = 5.517; double laenge, breite, hoehe; char zeichen = ‘A’; boolean lichtAn = true; lichtAn = machtSpass; float pi = 3.141f; Dezimalzahlen werden in Java mit einem Dezimalpunkt geschrieben! Achten Sie auf das >>f<<. Bei einem float-Wert brauchen Sie das, weil Java sonst denkt, dass alles mit Dezimalpunkt ein double ist. Sie wollen doch sicher nichts verschütten … Achten sie darauf, dass der Wert in die Variable passt. Einen großen Wert können Sie nicht in einen kleinen Becher stecken – jedenfalls nicht, ohne etwas zu verschütten. Der Compiler versucht, das zu verhindern. Wenn er aus Ihrem Code erkennen kann, dass ein Wert nicht in den Behälter (Becher/Variable) passt, gibt er eine Fehlermeldung aus und erzeugt keinen Bytecode zu Ihrem fehlerhaften Quellcode. Sie können beispielsweise nicht den Wert aus einer intVariablen in einen Behälter mit byte-Größe stopfen. int x = 24; byte b = x; Das funktioniert nicht! Sie fragen jetzt vielleicht, warum das nicht funktioniert. Schließlich ist der Wert von x nur 24, und 24 ist definitiv klein genug, um in ein byte gestopft zu werden. Sie wissen das! Der Compiler kümmert sich aber nur darum, dass Sie versuchen etwas Großes in einen zu kleinen Behälter zu stopfen und dabei eventuell etwas verschütten. Sie dürfen nicht erwarten, dass der Compiler weiß, welchen Wert x hat, selbst wenn Sie den Wert in Ihrem Code ausgeschrieben haben. Einer Variablen können Sie auf mehreren Wegen einen Wert zuweisen. C. Endreß Einen literalen Wert mit dem Zuweisungsoperator (=) zuweisen: x = 12; istGut = true; Einer Variablen den Wert einer anderen Variablen zuordnen: x = y; In einem Ausdruck Variablen verknüpfen: x = y + 47; 5 / 13 10/2008 4. Variablen und Ausdrücke 4.1.3 Hintergründiges zu Variablen Nachdem wir weiter oben eine Variablen-Becher-Analogie zur leichteren Anschauung im Umgang mit Variablen eingeführt haben, folgt nun ein Einblick in die Hintergründe der Variablenverarbeitung eines JavaProgramms. Sie wissen natürlich, dass Java keine Becher mit Namen versieht und Werte hineinpakt. Der sogenannte Memorymanager von Java legt die Variablen in den Speicherzellen des Arbeitsspeichers ab. Die jeweils ein Byte breiten Speicherzellen sind mit physikalischen Adressen versehen. Ein Java-Programm reserviert Speicherzellen, die den Namen der Variablen als symbolische Adresse erhalten. Über diese symbolische Adresse wird dann schreibend oder lesend auf den Inhalt der Speicherzelle zugegriffen. Wie wir wissen, ist Java eine streng typisierte Sprache. Daher muss prinzipiell der Datentyp des Initialwertes mit dem Datentyp der Variablen übereinstimmen. int anzahl = 24; double radius = 5.517; char letter = ’A’; 1004 5.517 radius letter double letter char Wir haben gelernt, dass der Compiler nichts Großes in kleine Becher füllt, weil er nichts verschütten will. Umgekehrt ist es jedoch möglich, den Inhalt kleiner Becher in große Becher umzufüllen, wie das folgende Beispiel zeigt: ’A’ hoch Typkonvertierungen 1012 int 4.1.4 24 anzahl ’A’ int radius Physikalische Adresse 1000 5.517 24 anzahl Symbolische Adresse int hoch = 2347; long sehrHoch; sehrHoch = hoch; sehr Hoch In bestimmten Fällen nimmt der Java-Compiler erweiternde Typkonvertierungen vor (implizites Type-Casting). Dabei wird der Grundsatz verfolgt, dass ein einfacher Datentyp in einen umfassenderen Datentyp konvertiert wird. Auf diese Weise werden keine Informationen verloren. byte short int long float long double char C. Endreß 6 / 13 10/2008 4. Variablen und Ausdrücke Die Umwandlung vom Typ int oder long nach float oder vom Typ long nach double kann jedoch zu einem Genauigkeitsverlust führen. Der Compiler konvertiert automatisch … bei einer Zuweisung, wenn der Typ der Variablen mit dem Typ des zugewiesenen Ausdrucks nicht identisch ist. bei der Auswertung eines arithmetischen Ausdrucks, wenn Operanden unterschiedlich typisiert sind. beim Aufruf einer Methode, falls die Typen der aktuellen Parameter nicht mit den formalen Parametern der Methode übereinstimmen. 4.2 Konstanten Eine Konstante ist ein Datenelement, dem nur einmal im Programmverlauf ein Wert zugewiesen wird. Die Initialisierung muss bei der Deklaration erfolgen. Man kann nach ihrer Initialisierung nur noch lesend auf eine Konstante zugreifen. Zur Deklaration einer Konstanten wird das Schlüsselwort final verwendet. Syntax: final Datentyp KonstantenName = Wert; final double PI = 3.141; final char DOLLAR = ‘$’; final int SPIELER = 11; Der Wert einer Konstanten ist nicht veränderbar (konstant). In Java ist es Konvention, die Namen von Konstanten ausschließlich in Großbuchstaben zu schreiben (z.B. MWST, PI, MAX_WERT, ANZAHL_TAGE) 4.3 Ausdrücke Ein Ausdruck ist eine Verarbeitungsvorschrift zur Ermittlung eines Wertes. Ein Ausdruck besteht aus Operanden und Operatoren. Jeder Operand kann selbst wieder ein Ausdruck sein. Operator: Gibt an, was gemacht wird. Operand: Gibt an, womit etwas gemacht wird. Das Ergebnis eines Ausdrucks nennt man Rückgabewert. Operatoren Rückgabewert 4 + 5 * 3 => 19 Operanden C. Endreß 7 / 13 10/2008 4. Variablen und Ausdrücke Der Typ des Rückgabewertes hängt vom Typ der Operanden ab. Ausdrücke sind die kleinsten ausführbaren Einheiten eines Java-Programms. Sie dienen dazu Variablen einen Wert zuzuweisen, numerische Berechnungen anzustellen oder logische Bedingungen zu formulieren. In Java gibt es folgende Arten von Operatoren: Arithmetische Operatoren (Berechnungen) Relationale Operatoren (Vergleiche) Logische Operatoren (Verknüpfung boolescher Werte) Zuweisungsoperatoren 4.3.1 Arithmetische Operatoren Arithmetische Operatoren erwarten einen numerischen Operanden und liefern einen numerischen Rückgabewert. Werden Operanden unterschiedlichen Typs in einem arithmetischen Ausdruck verwendet, konvertiert der Compiler die Operanden automatisch in den umfassenderen Datentyp. 12 + 5.3 12.0 + 5.3 = 18.3 Ergebnis von Typ double Typ-Konvertierung von int nach double Operator Beispiel Bedeutung + a+b addiert a und b - a–b subtrahiert b von a * a*b multipliziert a mit b / a/b dividiert a durch b - -a % a%b ++ a++ anwendbar auf Zahlen, char negatives Vorzeichen a modulo b, d.h. es ergibt den Rest der Ganz- und Division a / b Fließkommazahlen Inkrement: erhöht a um 1 ++a -- a-- Dekrement: verringert a um 1 Variablen --a Vorsicht bei der Verwendung von Inkrement- und Dekrement-Operatoren in Zuweisungen: Präfix: a = ++b Postfix: a = b++ C. Endreß erst wird b um 1 erhöht, dann wird a das Ergebnis zugewiesen. erst wird a der Wert von b zugewiesen, dann wird b um 1 erhöht. 8 / 13 10/2008 4. Variablen und Ausdrücke 4.3.2 Relationale Operatoren Relationale Operatoren vergleichen Ausdrücke miteinander und erzeugen einen boolschen Rückgabewert (true oder false) Operator == Beispiel Bedeutung a == b a gleich b: Ergibt true, wenn a gleich b ist. Bei Referenztypen ergibt sich true, wenn beide Werte auf dasselbe Objekt zeigen. a ungleich b: Ergibt true, wenn a nicht gleich b ist. != a != b < a<b <= a <= b > a>b >= a >= b 4.3.3 anwendbar auf Zahlen, boolean und Referenztypen Bei Referenztypen ergibt sich true, wenn beide Werte nicht auf dasselbe Objekt zeigen. a kleiner b: Ergibt true, wenn a kleiner b ist. a kleiner oder gleich b: Ergibt true, wenn a kleiner oder gleich b ist. a größer b: Ergibt true, wenn a größer b ist. Zahlen a größer oder gleich b: Ergibt true, wenn a größer oder gleich b ist. Logische Operatoren Logische Operatoren dienen dazu, Werte vom Typ boolean miteinander zu vergleichen. Der Rückgabewert ist ebenfalls vom Typ boolean. Operator Beispiel Bedeutung anwendbar auf logisches NICHT: macht aus true false und umgekehrt ! !a && a && b logisches UND: ergibt nur true, wenn sowohl Operand a als auch b true sind, sonst false || a || b logisches ODER: ergibt true, wenn mindestens einer der beiden Operanden a oder b true ist ^ a^b logisches Exklusiv-ODER: ergibt true, wenn beide Operanden einen unterschiedlichen Wahrheitswert haben boolean Die Operatoren && und || führen zu einer sogenannten Short-Circuit-Evaluation des logischen Ausdrucks, d.h. ein weiter rechts stehender Teilausdruck wird nur noch dann ausgewertet, wenn er für das Ergebnis des Gesamtausdrucks noch von Bedeutung ist. Die logischen Operatoren & (UND) und | (ODER) führen die Auswertung ohne Short-Circuit-Evaluation durch. C. Endreß 9 / 13 10/2008 4. Variablen und Ausdrücke 4.3.4 Zuweisungsoperatoren Zuweisungsoperatoren sind anwendbar auf alle Datentypen. Die Datentypen der Operanden müssen kompatibel sein, d.h. die Typen sind gleich oder ineinander konvertierbar. Operator 4.3.5 Beispiel Bedeutung Einfache Zuweisung: weist a den Wert von b zu und liefert b als Rückgabewert. = a=b += a += b a=a+b -= a -= b a=a-b *= a *= b a=a*b /= a /= b a=a/b %= a %= b a=a%b Sonstige Operatoren String-Verkettung Der Operator „+“ arbeitet bei Zeichenketten als Verkettungsoperator. Wenn wenigstens einer der beiden Operatoren a + b ein String ist, wird der gesamte Ausdruck als String-Verkettung ausgeführt. Hierbei wird gegebenenfalls der Nicht-String-Operand in einen String konvertiert und anschließend mit dem anderen Operand verkettet. Verkettungsoperator Das folgende Beispiel double liter = 9.5; System.out.println(″Ihr Benzinverbrauch beträgt ″ + liter + ″ Liter pro 100 km.″); erzeugt die Bildschirmausgabe: Ihr Benzinverbrauch beträgt 9.5 Liter pro 100 km. Der Wert von liter wird implizit in einen String umgewandelt Type-Cast-Operator Mit Hilfe des Type-Cast-Operators können Typumwandlungen explizit vorgenommen werden. Der Compiler akzeptiert es, wenn Sie ihm durch einen Cast-Operator mitteilen, dass Sie den Inhalt eines größeren Bechers in einen kleineren umfüllen möchten. Dass Sie dabei Inhalte verschütten, liegt dann in Ihrer Verantwortung. int i = 13; byte b = (byte) i; i = (int) 13.678; C. Endreß Erzwingt die Konvertierung des int-Wertes in byte Erzwingt die Konvertierung des double-Literals in den int-Wert 13 10 / 13 10/2008 4. Variablen und Ausdrücke 4.3.6 Prioritäten Bei Ausdrücken, die mehrere Operatoren beinhalten, legen die Vorrangregeln der Operatoren die Reihenfolge der Auswertung fest. Diese Vorrangregeln bezeichnet man als Prioritäten. In Java gelten folgende Vorrangregeln: Gruppe Operator Bezeichnung anwendbar auf 1 () Klammern 2 ++ Inkrement -- Dekrement - negatives Vorzeichen ! Logisches NICHT L S = Strings Type-Cast A V = Variablen 3 (type) 4 * Multiplikation / Division % Modulo + Additon - Subtraktion + String-Verkettung < Kleiner 5 6 <= > 5 7 8 A Abkürzungen E = elementare Typen N == Gleich != Ungleich && Logisches UND || Logisches ODER = Zuweisung N = numerische Typen N, N S, A N, N Größer Größer gleich L = logische Typen N, N Kleiner gleich >= A = alle E, E L, L V, A += Additionszuweisung -= Subtraktionszuweisung *= Multiplikationszuweisung /= Divisionszuweisung %= Modulozuweisung V, N Operatoren gleicher Priorität werden von links nach rechts ausgewertet. Die Reihenfolge der Auswertung kann durch das Setzen von Klammern () verändert werden. Ausdrücke in Klammern werden zuerst ausgewertet. C. Endreß 11 / 13 10/2008 4. Variablen und Ausdrücke Bei arithmetischen Ausdrücken gilt wie in der Mathematik: Punkt-vor-Strich-Rechnung. Beispiel: Aus der Mathematik ist das Polynom zweiten Grades bekannt: y = a x2 + b x + c. Die Berechnung von y erfolgt in Java über eine Zuweisung. y = a * x * x + b * x + c; Reihenfolge der Operatoren-Auswertung Sind die Variablen mit den Werten a = 2, b = 3, c = 7, x = 5 initialisiert, ergibt sich die Auswertung y = 2 * 5 * 5 + 3 * 5 + 7; y = 10 * 5 + 3 * 5 + 7; y = 50 + 3 * 5 + 7; y = 50 + y = 65 y = 4.4 15 + 7; + 7; 72; Anweisungen Eine Anweisung ist eine Ausführungseinheit, d.h ein vollständiger Befehl an den Computer. Anweisungen verändern den Zustand von Programmen - z.B. durch Berechnungen von Variablen. Ein Programm besteht aus einer Folge von Anweisungen, die in einer bestimmten Reihenfolge ausgeführt werden. Ausdrücke, die Berechnungen anstellen, oder Befehle, die bestimmte Abläufe bewirken, werden in Anweisungen formuliert. Anweisungen werden in Java mit Semikolon (;) abgeschlossen. y = x + 7 / a; System.out.println(“Java bereitet mir Kopfschmerzen!“); Mehrere Anweisungen werden in Code-Blöcken zusammengefasst. Die Blöcke werden durch ein Klammerpaar { } eingeschlossen. Anfang des Code-Blocks { Anweisung1; Anweisung2; ... } Ende des Code-Blocks C. Endreß 12 / 13 10/2008 4. Variablen und Ausdrücke 4.5 Kommentare Kommentare dienen dazu, Notizen, Bemerkungen oder Erläuterungen direkt in den Quelltext aufzunehmen. Einzeiliger Kommentar // // Kommentar bis zum Zeilenende Mehrzeiliger Kommentar /* */ /* ... Kommentar über mehrere Zeilen */ Dokumentationskommentar /** */ /** Dokumentation über mehrere Zeilen, kann mit javadoc aus der Quelle extrahiert und in ein HTMLDokument geschrieben werden */ Die Laufzeit oder Ausführung des Programms wird durch Kommentare nicht beeinflusst, weil der Compiler Kommentare nicht übersetzt und in das Programm einbindet. Spitzen Sie Ihren Bleistift! Nachdem Sie nun einiges über Variablen, Konstanten und Ausdrücke erfahren haben, versuchen Sie herauszufinden, welche der folgenden Anweisungen zulässig sind und welche nicht! 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. C. Endreß int x = 34.5; boolean boo = x; int g = 17; int y = g; y = y + 10.5; short s; s = y; byte b; byte v = b; short n = 12; v = n; byte k = 128; char a = ’Hallo’; String z = ″Java″; z = z + 5.0; final int K = 10; g++; K++; z = g + K + 100; 13 / 13 10/2008 5. Kontrollstrukturen 5. Kontrollstrukturen Lernziele ☺ Wissen, was Kontrollstrukturen sind und wie sie verwendet werden. ☺ Die Struktogramm-/PAP-Notation von Kontrollstrukturen kennen und anwenden. ☺ Die Java-Syntax der Kontrollstrukturen Sequenz, Auswahl und Wiederholung kennen. ☺ Kontrollstrukturen aus Struktogramm-/PAP-Notation und Java-Syntax ineinander überführen können. ☺ Kriterien zur Auswahl geeigneter Kontrollstrukturen kennen. In der Programmiertechnik unterscheidet man einfache Anweisungen und Steueranweisungen oder Kontrollstrukturen. Kontrollstrukturen steuern den Ablauf eines Algorithmus. Eine problemadäquate Umsetzung von Problemlösungen in Kontrollstrukturen wird durch folgende vier semantisch unterschiedliche Kontrollstrukturen ermöglicht: Sequenz, Auswahl, Wiederholung, Aufruf anderer Algorithmen. 5.1 Sequenz Man formuliert eine Sequenz bzw. eine Aneinanderreihung, wenn mehrere Anweisungen hintereinander auszuführen sind. Bei der Sequenz erfolgt die Abarbeitung von oben nach unten. Der Anweisungsblock Anweisungen, die nacheinander ausgeführt werden, fasst man in einem Block zusammen. Ein Block gilt als eine einzelne Anweisung und kann überall dort verwendet werden, wo syntaktisch eine einzelne elementare Anweisung erlaubt wäre. Ein Block darf auch Anweisungen zur Deklaration lokaler Variablen enthalten. Diese sind nur innerhalb des Blocks gültig und sichtbar. Syntax { Anweisung1; Anweisung2; ... } C. Endreß 1 / 10 09/2006 5. Kontrollstrukturen Lokale Variablen dürfen sich nicht gegenseitig verdecken. Es ist also nicht erlaubt, eine bereits deklarierte Variable x in einem tiefer geschachtelten Block erneut zu deklarieren. Das Verdecken von Klassen- oder Instanzvariablen dagegen ist zulässig (this-Operator schafft dabei Klarheit). 5.2 Auswahlstrukturen Auswahlstrukturen dienen dazu, bestimmte Programmteile nur beim Eintreten vorgegebener Bedingungen auszuführen. 5.2.1 Ein-/zweiseitige Auswahl if-Anweisung Syntax if( Ausdruck ) Anweisung1; else Anweisung2; Ist der Ausdruck wahr, so wird Anweisung1 ausgeführt. Sonst wird Anweisung2 ausgeführt. Die else-Alternative ist optional. Bei der einseitigen Auswahl fehlt sie. Ist das Ergebnis von Ausdruck falsch, wird in diesem Fall mit der ersten Anweisung nach Anweisung1 fortgefahren. Anstelle einer einzelnen Anweisung kann auch eine Folge von Anweisungen in einem Code-Block angegeben werden, der von geschweiften Klammern { } eingefasst wird. Struktogramm ja Programmablaufplan Ausdruck Anweisung1 nein ja Anweisung2 Anweisung1 Ausdruck nein Anweisung2 Beispiel: Einseitige Auswahl Ein Hardware-Händler liefert frei Haus, wenn die Bestellmenge bei mindestens 10 PC-Mäusen liegt. Bei einer geringeren Bestellmenge berechnet er eine Transportpauschale von 10,00 €. C. Endreß 2 / 10 09/2006 5. Kontrollstrukturen Java Struktogramm Transportpauschale = 0 transportPauschale = 0; Anzahl < 10 ja if( anzahl < 10 ) transportPauschale = 10.0; nein Transportpauschale = 10 PAP Transportpauschale = 0 nein Anzahl < 10 ja Transportpauschale = 10 Beispiel: Zweiseitige Auswahl Wegen eines Jubiläums bietet der Hardware-Großhändler Sonderkonditionen beim Kauf von PC-Mäusen: Bei Abnahme von weniger als 12 Mäusen gewährt er einen Rabatt von 3 %, bei größeren Abnahmemengen beträgt der Rabatt 5 %. Java Struktogramm Anzahl < 12 if( anzahl < 12 ) rabatt = 0.03; else rabatt = 0.05; nein ja Rabatt = 3 % Rabatt = 5 % PAP ja Rabatt = 3 % C. Endreß 3 / 10 Anzahl < 12 nein Rabatt = 5 % 09/2006 5. Kontrollstrukturen 5.2.2 Mehrseitige Auswahl Bei der mehrseitigen Auswahl werden mehrere Auswahlstrukturen ineinander geschachtelt. Für die einzelnen Auswahlstrukturen gelten die Erläuterungen zur ein- und zweiseitigen Auswahl. Bei der Schachtelung von Strukturen ist die Verwendung von geschweiften Klammern und das Einrücken der einzelnen Kontrollblöcke besonders wichtig, um den Überblick über die Struktur zu behalten. Beispiel: Mehrseitige Auswahl Ein Krawattenversand-Händler führt während der heißen Sommermonate neue Rabattkonditionen für sein Krawattensortiment ein, um seinen Umsatz zu steigern. Er wendet folgende Rabattstaffel an: Bei einem Bestellwert von weniger als 100 € gewährt er 10 % Rabatt, bei einem Bestellwert von 100 € bis 500 € beträgt der Rabatt 15 %, in allen anderen Fällen liegt der Rabatt bei 20 %. Java Struktogramm if( warenWert < 100 ) rabattSatz = 10; else { if( warenWert <= 500 ) rabattSatz = 15; else rabattSatz = 20; } rabatt = warenWert * rabattSatz / 100; Warenwert < 100 ja nein Rabattsatz = 10 Warenwert <= 500 ja Rabattsatz = 15 nein Rabattsatz = 20 Rabatt = Warenwert * Rabattsatz / 100 Wichtig: Bei einer Schachtelung von if-else-Konstrukten bezieht sich else immer auf das vorangegangene if. Fehlerhafte Einrückung sorgt hier für Missverständnisse: if( ausdruck1 ) if( ausdruck2 ) anweisung1; else anweisung2; if( ausdruck1 ) if( ausdruck2 ) anweisung1; else anweisung2; C. Endreß Die korrekte Einrückung sieht so aus: 4 / 10 09/2006 5. Kontrollstrukturen 5.2.3 Mehrfachauswahl Bei der Mehrfachauswahl oder Fallunterscheidung ist, abhängig vom Ergebnis eines zu prüfenden Ausdrucks, genau ein Zweig mit Anweisungen auszuführen. Die Mehrfachauswahl erhöht vor allem die Übersichtlichkeit, wenn viele Alternativen zur Auswahl stehen. switch-Anweisung Syntax switch( Ausdruck ) { case Kontante1: Anweisung(en); break; case Kontante2: Anweisung(en); break; case Kontante3: Anweisung(en); break; ... default: Anweisung; } Der Ausdruck dient als Selektor zum Auswählen der einzelnen Fälle. Der Selektor muss eine Variable oder ein Ausdruck eines ganzzahligen Typs (byte, short, char oder int) sein. Es wird die Sprungmarke (case) angesprungen, deren Konstante mit dem Wert des Selektors übereinstimmt. Die optionale default-Marke wird angesprungen, wenn keine passende Sprungmarke gefunden wird. Die optionale break-Anweisung beendet die Ausführung der switch-Struktur und verhindert so, dass die Anweisungen der folgenden case-Marken ebenfalls ausgeführt werden. Struktogramm PAP Ausdruck Konstante1 Konstante2 Konstante3 Anweisung C. Endreß Anweisung Anweisung Ausdruck default Anweisung 5 / 10 Fall 1 Fall 2 Fall 3 Fall 4 Anw. 1 Anw. 2 Anw. 3 Anw. 4 09/2006 5. Kontrollstrukturen Beispiel: Mehrfachauswahl Der Krawattenversender aus dem vorangegangenen Beispiel erweitert sein Rabattmodell. Er gewährt seinen Kunden einen Treuerabatt. Hierfür hat er alle Kunden in Kategorien eingeteilt und gewährt folgende Rabattsätze: Kategorie Rabattsatz 1 10 % 2 12 % 3 15 % 4 20 % 5 30 % andere 0% Das Problem ist auch hier mit der Schachtelung mehrerer Auswahlstrukturen lösbar. Bei sehr vielen Alternativen wird die mehrseitige Auswahl jedoch schnell unübersichtlich. Deshalb wird man in solchen Fällen als Kontrollstruktur die Mehrfachauswahl verwenden. Java switch( kategorie ) { case 1: rabattSatz = 10; case 2: rabattSatz = 12; case 3: rabattSatz = 15; case 4: rabattSatz = 20; case 5: rabattSatz = 30; default: rabattSatz = 0; } C. Endreß Struktogramm Kategorie =1 break; break; break; break; break; =2 =3 =4 =5 sonst Rabatt- Rabatt- Rabatt- Rabatt- Rabatt- Rabattsatz satz satz satz satz satz = 10% = 12% = 15% = 20% = 30% = 0% 6 / 10 09/2006 5. Kontrollstrukturen 5.3 Wiederholungsstrukturen Wenn es nötig ist, bestimmte Programmteile mehrfach auszuführen, verwendet man Wiederholungsstrukturen - sogenannte Schleifen oder Iterationen. Eine Schleife besteht aus einer Anweisungsfolge, die mehrfach durchlaufen werden kann, und einer Schleifenbedingung. Lineare Struktur Kopfgesteuerte Schleife Fußgesteuerte Schleife Anweisung1 Schleifenbedingung Anweisung1 Anweisung2 Anweisung1 Anweisung2 Anweisung1 Anweisung2 Schleifenbedingung ... Die Schleifenbedingung enthält die Kontrollinformationen für die Anzahl der Schleifendurchläufe. Bei sogenannten geschlossenen Schleifen steht die Anzahl der Schleifendurchläufe bereits vor das Schleifenausführung fest. Man nennt diese Schleifen daher auch Zählschleifen. Bei sogenannten offene Schleifen steht die Anzahl der Schleifendurchläufe beim Eintritt in die Schleife noch nicht fest. Eine fußgesteuerte Schleife wird mindestens einmal durchlaufen. Weitere Wiederholungen werden durch die Schleifenbedingung am Ende des Schleifenkörpers gesteuert. Die kopfgesteuerte Schleife muss nicht zwingend durchlaufen werden. Die Schleifenbedingung im Schleifenkopf steuert die Zahl der Wiederholungen. C. Endreß 7 / 10 09/2006 5. Kontrollstrukturen 5.3.1 Die Zählschleife for-Schleife Syntax for(Initialisierung; Schleifenbedingung; Wiederholungsausdruck){ Anweisung(en); } Der Ausdruck Initialisierung wird nur einmal beim Start der Schleife aufgerufen. Üblicherweise wird hier die Zählvariable der Schleife initialisiert und auch deklariert. Fehlt der Initialisierungsteil, wird keine Initialisierung im Kopf der Schleife durchgeführt. Die Schleifenbedingung wird vor jedem Schleifendurchlauf ausgewertet. Ist das Ergebnis true, wird der Anweisungsblock der Schleife ausgeführt. Fehlt die Schleifenbedingung, so setzt der Compiler an dieser Stelle die Konstante true ein. Im Wiederholungsausdruck können Sie ein oder mehrere Dinge angeben, die bei jedem Schleifendurchlauf ausgeführt werden sollen (z.B. Zählvariable inkrementieren). Der Wiederholungsausdruck wird am Ende jedes Schleifendurchlaufs ausgeführt. Anschließend wird erneut die Schleifenbedingung geprüft. Struktogramm for ( Initialisierung; Schleifenbedingung; Wiederholungsausdruck ) Anweisung(en) 100 Mal wiederholen for(int i = 0; i < 100; i++){ } In normalem Deutsch bedeutet das: „100 Mal wiederholen.“ Wie der Compiler es sieht: Erstelle eine int-Variable i und setze sie auf 0. Wiederhole, solange i < 100 ist. Addiere am Ende jedes Schleifendurchlaufs 1 zu i hinzu. C. Endreß 8 / 10 09/2006 5. Kontrollstrukturen Beispiel: Zählschleife Eine Bank bietet ihren Kunden „Wachstumssparen“ zu folgende Konditionen an: Das Anlagekapital wird mit einem Zinssatz von 4 % pro Jahr verzinst. Die jährlich anfallenden Zinsen werden dem Kapital zugeschlagen und im Folgejahr mit verzinst (Zinseszinseffekt). Die Laufzeit der Anlage beträgt 5 Jahre. Java Struktogramm for ( i = 1; i <= 5; i++ ) for( int i = 1; i <= 5; i++ ) { zinsen = kapital * zinsSatz / 100; kapital = kapital + zinsen; } 5.3.2 zinsen = kapital * zinsSatz / 100 kapital = kapital + zinsen Die kopfgesteuerte Schleife while-Schleife Syntax Struktogramm Solange Ausdruck wahr while( Ausdruck ){ Anweisung(en); } Anweisung(en) Wenn der Ausdruck, der vom Typ boolean sein muss, wahr ist, wird die Anweisung(en) im Schleifenrumpf ausgeführt. Am Ende des Schleifenkörpers wird Ausdruck erneut geprüft. Ist das Ergebnis false, wird der Schleifenkörper nicht mehr wiederholt, und die Programmausführung wird hinter dem Schleifenkörper fortgesetzt. Beispiel: Kopfgesteuerte Schleife Ein Anlagekapital von 10000 € wird jährlich mit 5 % Zinsen verzinst. Nach wie viel Jahren hat sich das Kapital verdoppelt, wenn die jährlichen Zinsen dem Kapital zugeschlagen werden? C. Endreß 9 / 10 09/2006 5. Kontrollstrukturen Java Struktogramm endkapital = kapital * 2 endkapital = 2 * kapital; laufzeit = 0; while( kapital < endkapital ) { zinsen = kapital * zinsSatz / 100; kapital = kapital + zinsen; laufzeit = laufzeit + 1; } 5.3.3 laufzeit = 0 Solange ( kapital < endkapital ) zinsen = kapital * zinsSatz / 100 kapital = kapital + zinsen laufzeit = laufzeit + 1 Die Fußgesteuerte Schleife do-while-Schleife Syntax Struktogramm Anweisung(en) do { Anweisung(en); } while( Ausdruck ); Solange Ausdruck wahr Die do-while-Schleife wird mindestens einmal durchlaufen. Wenn die Auswertung des Ausdrucks in der while-Anweisung das Ergebnis true liefert, wird der Schleifenkörper erneut durchlaufen. Ist das Ergebnis false, wird die Programmausführung hinter der while-Anweisung fortgesetzt. Der Codeblock im Schleifenrumpf kann eine oder mehrere Anweisungen enthalten. Beispiel: Fußgesteuerte Schleife Die Problemstellung aus dem Beispiel der kopfgesteuerten Schleife kann auch mit einer fußgesteuerten Schleife gelöst werden. Java Struktogramm endkapital = kapital * 2 endkapital = 2 * kapital; laufzeit = 0; do { zinsen = kapital * zinsSatz / 100; kapital = kapital + zinsen; laufzeit = laufzeit + 1; } while( kapital < endkapital ); C. Endreß laufzeit = 0 zinsen = kapital * zinsSatz / 100 kapital = kapital + zinsen laufzeit = laufzeit + 1 Solange ( kapital < endkapital ) 10 / 10 09/2006 6. Felder, Enumerationen und Zeichenketten 6. Felder, Enumerationen und Zeichenketten Lernziele ☺ Die Unterschiede zwischen den Java-Konzepten für einfache Datentypen und Referenzdatentypen erklären können. ☺ Das Java-Konzept für Felder als Beispiel für Referenzdatentypen erklären, darstellen und anwenden können. ☺ Das Java-Konzept für Aufzählungstypen (Enumerationen) erklären und anwenden können. ☺ Das Java-Konzept für Zeichenketten (Klasse String) als weiteres Beispiel für Referenzdatentypen erklären, darstellen und anwenden können. 6.1 Einfache Datentypen und Referenzen Bei einfachen Datentypen wird über den Namen der Variable direkt der Bereich des Speichers angesprochen, in dem der Datenwert der Variable abgelegt ist. Der Name der Variablen stellt damit die symbolische Adresse einer Speicherzelle dar. Variable primZahl preis 131 295.90 6.2 Referenzdatentypen Mit Hilfe von Referenzdatentypen können wir aus einfachen Datentypen „eigene“ Datentypen erzeugen. Solche selbst definierten Typen sind erforderlich, wenn kompliziertere Anwendungen effizient programmiert werden sollen. In Java gibt es prinzipiell zwei Arten von Referenzdatentypen: Felder und Klassen. Im Gegensatz zu einfachen Datentypen kann man mit Referenzdatentypen die eigentlichen Werte von Variablen nicht direkt, sondern nur indirekt über eine Referenz bearbeiten. Der Arbeitsspeicher eines Rechners ist in Speicherzellen eingeteilt, wobei jede Speicherzelle eine numerische Adresse besitzt. Das folgende Bild soll den prinzipiellen Speicheraufbau verdeutlichen. In der Speicherzelle mit der Adresse 1100 ist der ganzzahlige Wert 131 abgelegt. Dieser Variablen ist die Bezeichnung primZahl als symbolische Adresse zugeordnet, über die direkt auf den Inhalt der Speicherzelle mit der Adresse 1100 zugegriffen wird. C. Endreß 1 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Adresse im Speicher Typ des Inhalts ... Symbolische Adresse primZahl Inhalt der Speicherzelle 1100 131 ganzzahliger Wert ... nachName 1200 1600 Referenz ... 1600 Meier Zeichenkette ... Die Speicherzelle mit der Adresse 1200, der die symbolische Adresse nachName zu geordnet ist, enthält den Wert 1600. Im Unterschied zu einem einfachen Datentyp wie der Variablen primZahl wird der Wert 1600 nicht als numerischer Wert interpretiert, sondern als Adresse. D.h. der Inhalt der Variablen nachName ist ein Verweis - eine Referenz - auf eine andere Speicherzelle. Das eigentliche „Objekt“ befindet sich erst in der Speicherzelle mit der referenzierten Adresse. In diesem Beispiel enthält die Speicherzelle mit der Adresse 1600 die Zeichenkette Meier. Die Speicherzelle mit der Adresse 1600 besitzt keine symbolische Adresse. Auf ihren Inhalt kann nur indirekt über Referenzen zugegriffen werden. Variablen von Referenzdatentypen enthalten Referenzen, d.h. Verweise auf Speicheradressen, an denen die eigentlichen Daten-Objekte abgelegt sind. Objekt r1 Referenzen r2 Daten variable Objekt Daten Referenz Es können auch mehrere Referenzen auf dasselbe Objekt verweisen. C. Endreß 2 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Referenzdatentypen auf Gleichheit prüfen: Der Gleichheitsoperator (==) prüft nur die Gleichheit der Speicheradressen, auf die die Referenzen verweisen. Die inhaltliche Gleichheit der referenzierten Objekte muss mit einer Methode (z.B. equals()) geprüft werden. variable1 Referenzen == Objekte equals() variable2 6.3 Felder 6.3.1 Einführendes Beispiel: Terminkalender Wir wollen einen kleinen Terminkalender programmieren. Unser Programm soll für jede Stunde des Tages einen Texteintrag ermöglichen. Montag 00:00 12:00 01:00 13:00 Gleich ist die Schule aus 02:00 14:00 Mittagessen bei McD. 03:00 15:00 Siesta 04:00 16:00 05:00 Weckerklingeln 17:00 Hausaufgaben für Programmierung 06:00 Heftiges Weckerklingeln 18:00 Biergarten 07:00 Aufstehen 19:00 08:00 Doppelstunde Programmierung 20:00 09:00 21:00 10:00 22:00 11:00 23:00 Schlafen Der Einfachheit halber realisieren wir das Programm zunächst nur für einen Wochentag, z.B. Montag. Das Programm soll Einträge über ein Menü aufnehmen und eine komplette Tagesseite auf dem Bildschirm darstellen können. Eine Realisierung mit den Mitteln, die uns bisher zur Verfügung stehen, erstreckt sich über mehrere Seiten und könnte folgendermaßen aussehen: C. Endreß 3 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten /* Programm: UmstaendlicherKalender Umstaendlicher Terminkalender fuer 24 Stundeneintraege eines Kalendertags */ import java.io.IOException; import support.Console; public class UmstaendlicherKalender { 1 2 3 public static void main(String[] args) throws IOException { // Variabledeklaration // Fuer jede Stunde eine Variable String termin_00 = ""; String termin_01 = ""; String termin_02 = ""; String termin_03 = ""; String termin_04 = ""; String termin_05 = ""; String termin_06 = ""; String termin_07 = ""; String termin_08 = ""; String termin_09 = ""; String termin_10 = ""; String termin_11 = ""; String termin_12 = ""; String termin_13 = ""; String termin_14 = ""; String termin_15 = ""; String termin_16 = ""; String termin_17 = ""; String termin_18 = ""; String termin_19 = ""; String termin_20 = ""; String termin_21 = ""; String termin_22 = ""; String termin_23 = ""; // Das Hauptprogramm in einer Schleife boolean fertig = false; while (!fertig) { // Zuerst ein Bildschirmmenue Console.println("\n1 = Neuer Eintrag"); Console.println("2 = Termine ausgeben"); Console.println("3 = Programm beenden"); Console.print("Ihre Wahl ->"); int auswahl = Console.readInt(); // Fallunterscheidung der Auswahl switch (auswahl) { case 1: // Termine eingeben Console.print("Welche Uhrzeit? ->"); int uhrzeit = Console.readInt(); if (uhrzeit < 0 || uhrzeit > 23) { Console.println("Eingabefehler!"); break; } 4 Console.print("Termin ->"); String eintrag = Console.readln(); // Termin einordnen switch (uhrzeit) { C. Endreß 4 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten case 0: termin_00 break; case 1: termin_01 break; case 2: termin_02 break; case 3: termin_03 break; case 4: termin_04 break; case 5: termin_05 break; case 6: termin_06 break; case 7: termin_07 break; case 8: termin_08 break; case 9: termin_09 break; case 10: termin_10 break; case 11: termin_11 break; case 12: termin_12 break; case 13: termin_13 break; case 14: termin_14 break; case 15: termin_15 break; case 16: termin_16 break; case 17: termin_17 break; case 18: termin_18 break; case 19: termin_19 break; case 20: termin_20 break; C. Endreß = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; = eintrag; 5 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten case 21: termin_21 = eintrag; break; case 22: termin_22 = eintrag; break; case 23: termin_23 = eintrag; break; } break; case 2: // Termine ausgeben Console.println(" 0 Uhr: " + termin_00); Console.println(" 1 Uhr: " + termin_01); Console.println(" 2 Uhr: " + termin_02); Console.println(" 3 Uhr: " + termin_03); Console.println(" 4 Uhr: " + termin_04); Console.println(" 5 Uhr: " + termin_05); Console.println(" 6 Uhr: " + termin_06); Console.println(" 7 Uhr: " + termin_07); Console.println(" 8 Uhr: " + termin_08); Console.println(" 9 Uhr: " + termin_09); Console.println("10 Uhr: " + termin_10); Console.println("11 Uhr: " + termin_11); Console.println("12 Uhr: " + termin_12); Console.println("13 Uhr: " + termin_13); Console.println("14 Uhr: " + termin_14); Console.println("15 Uhr: " + termin_15); Console.println("16 Uhr: " + termin_16); Console.println("17 Uhr: " + termin_17); Console.println("18 Uhr: " + termin_18); Console.println("19 Uhr: " + termin_19); Console.println("20 Uhr: " + termin_20); Console.println("21 Uhr: " + termin_21); Console.println("22 Uhr: " + termin_22); Console.println("23 Uhr: " + termin_23); break; case 3: // Programm beenden fertig = true; break; default: // Falsche Zahl eingegeben Console.println("Eingabefehler!"); } 5 } } } Erläuterungen 1 Für jede Stunde des Tages wird eine Variable vom Typ String deklariert, die eine Zeichenkette – in diesem Fall den Termineintrag - aufnehmen kann. Alleine hierfür sind 24 Deklarationen erforderlich. 2 In einem kleinen Konsolenmenü wird dem Benutzer die Auswahl zwischen einer Termineintragung, der Kalenderausgabe und dem Programmende angeboten. Die Auswahl erfolgt numerisch und wird in der ganzzahligen Variablen auswahl gespeichert. 3 Zum Auswerten der Menüauswahl wird der Wert von auswahl einer Fallunterscheidung unterzogen. 4 Zum Eintragen oder Verändern eines Termins müssen Uhrzeit (uhrzeit) und Text (eintrag) eingegeben werden. Um herauszufinden, in welcher Terminvariablen der Eintrag zu speichern C. Endreß 6 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten ist, muss die Variable uhrzeit einer aufwendigen Fallunterscheidung unterzogen werden. Es sind 24 Fälle zu unterscheiden. Bei einer Kalenderausgabe sind ebenfalls wieder 24 Fälle zu unterscheiden, da die Terminvariablen einzeln angesprochen werden müssen. 5 Wir haben also dreimal 24 Zeilen, d.h. 72 Zeilen allein darauf verwendet Texteinträge für einen Kalendertag zu verwalten. Wenn wir nun von 365 Tagen pro Jahr ausgehen und unseren Kalender 5 Jahre lang verwenden wollen, erhalten nach der gegebenen Lösungsvariante eine Programmlänge von weit über 130.000 Programmzeilen! Viel Spaß beim Programmieren! Wenn wir das Problem näher analysieren, stellen wir fest, dass unsere Terminvariablen termin_00 bis termin_23 alle vom Typ String sind und ähnliche Inhalte – nämlich Termine - repräsentieren. Auch die Bezeichner der Variablen unterscheiden sich nur durch eine nachstehende Ziffer, d.h. durch einen Index. Wenn es also eine Möglichkeit gäbe, verschiedene Variablen nur durch ihren Index anzusprechen, könnten wir uns damit die aufwendigen switch-Konstrukte zur Fallunterscheidung ersparen. Diese Möglichkeit eröffnen uns die sogenannten Felder oder Arrays. Mit ihrer Hilfe können wir Werte wie in einer Tabelle anlegen und über einen Index Zugriff auf die Werte erhalten. 6.3.2 Ein bisschen Theorie zu Feldern (Arrays) In der Praxis tritt oft das Problem auf, dass viele Variablen vom gleichen Typ deklariert werden müssen, die auch inhaltlich eine identische Bedeutung haben wie z.B. double messWert1, double messWert2, ... Aus diesem Grund enthalten fast alle Programmiersprachen einen Konstruktionsmechanismus, der es erlaubt, typenidentische Variablen zu einer Einheit zusammenzufassen. Eine solche Typenkonstruktion bezeichnet man als Feld oder Array. Ohne Feld: messWert1 messWert2 messWert3 Elemente (alle vom gleichen Typ) Mit Feld: messReihe Index 0 1 2 Ein Feld/Array besteht aus Elementen bzw. Komponenten desselben Typs. Auf die einzelnen Elemente des Felds wird über Indizes zugegriffen. Im Gegensatz zu anderen Programmiersprachen sind Felder in Java Objekte, d.h. Feld-Variablen sind Referenzen, Felder besitzen Methoden und Instanzvariablen, Felder werden zur Laufzeit erzeugt. C. Endreß 7 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Die Feld-Variable ist eine Referenz auf ein Feld-Objekt. Referenzen sind Zeiger auf Objekte. Sie enthalten keine Daten, sondern nur die Speicheradresse des Objekts, auf das sie deuten. Es können mehrere Referenzen auf ein Objekt deuten. Referenz Feld-Variable Objekt Feld-Objekt Element[0] Element[1] Element[n-1] Das Feld-Objekt ist eine Einheit, welche die Feld-Elemente enthält. Das Feld-Objekt wird einer Feld-Variablen zugeordnet. Felder in Java sind semidynamisch. Ihre Größe kann zur Laufzeit festgelegt, danach aber nicht mehr verändert werden. 6.3.3 Felder erzeugen Um ein Feld in Java zu erzeugen, sind zwei Schritte nötig: Deklaration einer Feld-Variablen: int[] feld; feld null boolean[] c; double[] messReihe; Wahlweise können die eckigen Klammern auch hinter den Variablennamen geschrieben werden, aber das ist ein Tribut an die Kompatibilität zu C/C++ und sollte in neuen Java-Programmen vermieden werden. Zum Zeitpunkt der Deklaration wird noch nicht festgelegt, wie viele Elemente das Feld besitzen soll. Diese Festlegung erfolgt erst bei seiner Initialisierung mit Hilfe des new-Operators. Festlegen der Feldgröße: feld = new int[4]; feld 0 messReihe = new double[10]; 0 Alle Elemente des Feldes werden bei der Erzeugung mit new automatisch initialisiert. Die Initialwerte hängen vom Typ des Feldes ab: C. Endreß 0 c = new boolean[7]; Datentyp Default-Wert boolean false ganzzahlige Typen 0 Fließkommatypen 0.0 char ’\u0’ Objekte null 8 / 23 0 10/2008 6. Felder, Enumerationen und Zeichenketten Mit dem new-Operator erzeugte Arrays werden häufig in Zählschleifen initialisiert. for(int i = 0; i < 4; i++){ feld[ i ] = i + 1; } feld 1 feld[0] 2 feld[1] 3 feld[2] 4 feld[3] Alternativ zur Verwendung des new -Operators kann ein Feld auch literal initialisiert werden. Dazu werden die Feld-Elemente mit einer Initialisierungsliste erzeugt. int [] feld2 = { 1, 12, 4, 108}; boolean[] feld3 = { true, true, false }; String[] feld4 = {“Hallo,”, “ich bin”, “ein Array”}; 1 feld2 true feld3 Hallo, feld4 12 true ich bin 4 false ein Array 108 Die Größe des Feldes ergibt sich hierbei aus der Anzahl der zugewiesenen Elemente. Anders als bei der expliziten Initialisierung mit new, muss in diesem Fall die Initialisierung direkt bei der Deklaration erfolgen. Deklaration und Erzeugung eines Feldes können auch in einer Anweisung vollzogen werden: int[] feld = new int[4]; boolean[] c = new boolean[7]; double[] messReihe = new double[10]; 6.3.4 Felder verwenden Bei der Erzeugung eines Feldes von n Elementen werden die einzelnen Elemente von 0 aufsteigend bis n – 1 durchnummeriert. Der Zugriff auf ein Element erfolgt über den Feldnamen und den numerischen Index des Elements, der nach dem Feldnamen in eckige Klammern geschrieben wird. Der Zugriff kann sowohl schreibend als auch lesend erfolgen. x[0] = 17; // weist dem ersten Element des Feldes x den Wert 17 // zu. x[3] = 25; // weist dem vierten Element des Feldes x den Wert 25 // zu. x[4] = x[0] + x[3]; // weist dem fünften Element des Feldes x // Summe der Elemente x[0] und x[3] zu. die Der Feld-Index muss vom Typ int sein. Aufgrund der automatischen Typkonvertierungen, die der Compiler vornimmt, sind auch short, byte und char-Werte erlaubt. Fließkomma- oder booleanWerte sind als Indexwert nicht zulässig. C. Endreß 9 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Obwohl long auch ein Ganzzahltyp ist, ist dieser Typ ebenfalls nicht als Index zulässig. Ein Feld mit 263 – 1 Elementen sprengt jeden Arbeitsspeicher – auch wenn die kleinsten Datentypen wie boolean oder byte verwendet werden, die jeweils nur 1 Byte groß sind. Jedes Feld hat eine Instanzvariable length, welche die Anzahl seiner Elemente angibt. int[] temperaturen = {18, 23, 27, 32, 25}; int anzahlElemente = temperaturen.length; // ergibt 5 18 temperaturen 23 27 length = 5 32 25 Häufig verwendet man for-Schleifen, um über die Elemente eines Feldes zu iterieren. for(int i = 0; i < temperaturen.length; i++){ summe = summe + temperaturen[i]; } durchSchnitt = durchSchnitt / temperaturen.length; Indexwerte werden vom Laufzeitsystem auf Einhaltung der Feld-Grenzen geprüft. Die Indexwerte müssen größer oder gleich 0 und kleiner als length sein. Andernfalls löst der Interpreter eine ArrayIndexOutOfBoundsException aus. 6.3.5 Erweiterte for-Schleife Mit Java 5 wurde speziell für Felder die sogenannte erweiterte for-Schleife eingeführt. for(int temperatur : temperaturen){ summe = summe + temperatur; } durchSchnitt = durchSchnitt / temperaturen.length; Der Iterator wird beim Start der Schleife mit dem ersten Element des Feldes initialisiert. Mit Beginn jedes weiteren Durchlaufs wird der Iterator auf das jeweils folgende Element gesetzt. Auf diese Weise läuft die Schleife selbständig über alle Elemente des Feldes. Explizite Zählvariablen oder Schleifenbedingungen gibt es bei dieser vereinfachten Schleife nicht. C. Endreß 10 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Beispiel: Umsatzbilanz einer Filiale Die Filiale Nord der Firma Cash & Co hat im vergangenen Jahr folgende Umsätze erzielt: 1. Quartal 2. Quartal 3. Quartal 4. Quartal 65000 € 56000 € 54000 € 52000 € Problemstellung: Es sollen der Gesamtumsatz des Jahres und der durchschnittliche Quartalsumsatz berechnet werden. Lösungsansatz: Die einzelnen Umsatzzahlen werden in einem Array gespeichert. Anschließend erfolgen die geforderten Berechnungen. Java-Quellcode /* Programm: Umsatzbilanz Demonstriert die Handhabung eines eindimensionalen Arrays */ import java.io.IOException; import support.Console; public class UmsatzBilanz { public static void main(String[] args) throws IOException { // Deklaration der Variablen double[] umsaetze ; umsaetze = new double[4]; double gesamtUmsatz = 0; double quartalsDurchschnitt = 0; 1 2 3 4 // Einlesen der Quartalsumsätze for(int i = 0; i < umsaetze.length; i++){ Console.print((i + 1) + ". Quartal -> "); umsaetze[i] = Console.readDouble(); } 5 6 // Summe und Durchschnitt berechnen for(double quartalsUmsatz : umsaetze){ gesamtUmsatz = gesamtUmsatz + quartalsUmsaetz; } 7 quartalsDurchschnitt = gesamtUmsatz / umsaetze.length; // Ausgabe der Ergebnisse Console.println("Gesamtumsatz: " + gesamtUmsatz); Console.println("Quartalsdurchschnitt: " + quartalsDurchschnitt); } } Konsolen-Ausgabe 1. Quartal -> 65000 2. Quartal -> 56000 3. Quartal -> 54000 4. Quartal -> 52000 Gesamtumsatz: 227000.0 Quartalsdurchschnitt: 56750.0 C. Endreß 11 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Erläuterungen 1 Es wird eine Feld-Variable mit der Bezeichnung umsaetze deklariert für ein Feld mit Elementen des Typs double. Ein Feld wird jedoch noch nicht angelegt. Die Variable umsaetze referenziert daher noch keinen Speicherbereich, sondern wird zunächst mit dem Wert null initialisiert. umsaetze null Feld-Variable 2 Der Operator new legt ein Feld mit 4 Elementen des Typs double im Arbeitsspeicher an. Java initialisiert die Elemente des Feldes mit sogenannten default-Werten, die abhängig sind vom Datentyp der Elemente. Der default-Wert für Variablen des Typs double ist 0.0. Die Zuweisung initialisiert die Variable umsaetze mit einer Referenz auf das erzeugte Array. umsaetze null Feld-Variable 0.0 umsaetze[0] 0.0 umsaetze[1] 0.0 umsaetze[2] 0.0 umsaetze[3] Feld-Objekt Die Deklaration (Zeile 1) und die Initialisierung (Zeile 2) des Feldes können auch in einer Anweisung erfolgen: double[] umsaetze = new double[4]; 3 Wir benötigen eine Variable, in der wir den Gesamtumsatz speichern können. Daher deklarieren wir eine Variable gesamtUmsatz vom Typ double und weisen ihr explizit den Wert 0.0 zu. Dieses müssen wir tun, da die Variable gesamtUmsatz eine lokale Variable ist. Lokale Variablen werden in Java nicht automatisch mit default-Werten initialisiert. 4 Auch für die Berechnung des Quartalsdurchschnitts benötigen wir eine double-Variable. Deklaration und Initialisierung erfolgen analog zu Zeile 3. 5 Die Initialisierung des Feldes erfolgt in einer Zählschleife elementweise. Die Zählvariable i wird als Index benutzt, um die einzelnen Array-Elemente anzusprechen. Beim Durchlaufen des Feldes muss sichergestellt werden, dass der Feldindex nicht über die Feldgrenzen hinausläuft, denn ein Feldelement umsaetze[4] gibt es nicht. Beim der Verwendung eines Positionsindex, der außerhalb der Feldgrenzen liegt, kommt es zu einer ArrayIndexOutOfBoundsException. Deshalb durchlaufen wir die for-Schleife nur solange die Bedingung i < umsaetze.length erfüllt ist, d.h. solange i < 4 ist. C. Endreß 12 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten 6 Dem Feld werden die Umsatzwerte elementweise durch eine Konsoleneingabe des Benutzers zugewiesen. umsaetze Feld-Variable 65000 umsaetze[0] 56000 umsaetze[1] 54000 umsaetze[2] 52000 umsaetze[3] Feld-Objekt 7 6.3.6 Zur Berechung des Gesamtumsatzes müssen die Werte der einzelnen Feldelemente addiert werden. Dazu wird mit einer vereinfachten for-Schleife über alle Feldelemente iteriert. Der Iterator quartalsUmsatz wird beim Start der Schleife mit dem ersten Element des Feldes umsaetze initialisiert. Mit Beginn jedes weiteren Durchlaufs wird der Iterator auf das jeweils folgende Element gesetzt. Auf diese Weise läuft die Schleife selbständig über alle Elemente des Feldes umsaetze, deren jeweiliger Wert zum gesamtUmsatz addiert wird. Felder kopieren Felder sind Objekte und können deshalb nicht mit dem Zuweisungsoperator (=) kopiert werden, wie es bei Variablen einfacher Datentypen möglich ist. int[] a = {10, 20, 30}; int[] b; b = a; a Vorsicht! Zwei Referenzen auf dasselbe Array-Objekt a[0] = 111; 20 b 30 a 111 20 b[2] = 321; summe = a[0] + a[2]; 10 // ergibt 432 b 321 a 10 Felder können kopiert werden durch elementweises Kopieren (Schleifen erforderlich) oder die Methode clone() 20 30 int[] a = {10, 20, 30}; int[] b; b b = (int[]) a.clone(); 10 20 30 C. Endreß 13 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten 6.4 Mehrdimensionale Felder Alle Felder sind in Java grundsätzlich eindimensional. Die Elemente eines Feldes können ihrerseits Felder sein, was das Schachteln von Feldern ermöglicht. Mehrdimensionale Felder lassen sich durch geschachtelte Arrays darstellen. Geschachtelte Felder werden durch mehrere Klammerpaare deklariert. Beispiel: Umsatzbilanz einer Firma Wir erweitern das vorangegangene Beispiel. Die Firma Cash & Co besitzt drei Filialen, die im vergangenen Jahr folgende Umsätze erzielt haben: Filiale 1. Quartal 2. Quartal 3. Quartal 4. Quartal Nord 65000 € 56000 € 54000 € 52000 € Mitte 70000 € 65000 € 62000 € 65000 € Süd 75000 € 76000 € 84000 € 82000 € Problemstellung: Unser Ziel ist es, die Umsatztabelle ist in strukturierter Form, d.h. in einem zweidimensionalen Feld im Programmspeicher abzubilden, um verschiedene rechnerische Auswertungen durchzuführen. So sollen beispielsweise die Jahresumsätze für jede Filiale sowie die Firmenumsätze pro Quartal berechnet werden. Lösungsansatz: Die Umsatztabelle weist eine zweidimensionale Struktur auf. Die Umsätze einer Filiale sind in einer Zeile abgelegt. Jede Spaltenposition in der Zeile entspricht einem Quartal. Für eine einzige Filiale können die Umsätze in einem eindimensionalen Array gespeichert werden, so wie wir es bereits im vorangegangenen Beispiel ausgeführt haben. Für die drei angegebenen Filialen ergibt dieses Vorgehen die folgenden drei eindimensionalen Felder: double[] filialeNord = { 65000, 56000, 54000, 52000 }; double[] filialeMitte = { 70000, 65000, 62000, 60000 }; double[] filialeSued = { 80000, 75000, 67000, 70000 }; filialeNord 65000 56000 54000 52000 filialeMitte 70000 65000 62000 60000 filialeSued 80000 75000 67000 70000 Die drei Filial-Arrays werden nun zu einer Tabelle in Form eines zweidimensionalen Feldes zusammengefasst. Dabei machen wir uns den Grundsatz zunutze, dass Elemente eines eindimensionalen Feldes wiederum Felder sein können. double[][] umsaetze = { filialeNord, filialeMitte, filialeSued }; umsaetze filialeNord filialeMitte filialeSued C. Endreß 14 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Das Feld umsaetze ist somit ein Feld, das aus Feldern besteht. Die Vektoren filialeNord, filialeMitte und filialeSued der Filialen stellen die Zeilen des neuen zweidimensionalen Feldes dar, das eine Dimension von 3 x 4 hat, d.h. 3 Zeilen und 4 Spalten. Man bezeichnet diese Struktur auch als 3 x 4 Matrix. Die Zeilen (horizontale Dimension) enthalten die Umsätze einer Filiale. Die Spalten (vertikale Dimension) enthalten die Umsätze eines Quartals. Die Position eines einzelnen Feldelementes wird durch den Zeilen- und den Spaltenindex festgelegt, die in eckigen Klammern hinter dem Namen des Arrays angegeben werden. So bezeichnet z.B. umsaetze[1][3] das Element in der zweiten Zeile und der vierten Spalte mit dem Wert 60000. Die Zählung der Positionsnummern beginnt mit Null. Die Deklaration des Feldes umsaetze kann in Java auch mit einer Initialisierungsliste erfolgen. umsaetze umsaetze[0][0] umsaetze[0][1] umsaetze[0][2] umsaetze[0][3] 65000 56000 54000 52000 umsaetze[1][0] umsaetze[1][1] umsaetze[1][2] umsaetze[1][3] 70000 65000 62000 60000 umsaetze[2][0] umsaetze[2][1] umsaetze[2][2] umsaetze[2][3] 80000 75000 67000 70000 umsaetze[0] umsaetze[1] umsaetze[2] umsaetze[i].length = 4 umsaetze.length = 3 Zeilenindex i = 0, 1, 2 double[][] umsaetze = { { 65000, 56000, 54000, 52000 }, { 70000, 65000, 62000, 60000 }, { 80000, 75000, 67000, 70000 } }; Java-Quellcode /* Programm: UmsatzTabelle Demonstriert die Handhabung eines zweidimensionalen Arrays */ import support.Console; 1 public class UmsatzTabelle { public static void main(String[] args) { // Deklaration des Feldes double[][] umsaetze = { { 65000, 56000, 54000, 52000 }, { 70000, 65000, 62000, 60000 }, { 80000, 75000, 67000, 70000 } }; // Ausgabe der Tabelle C. Endreß 15 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten 2 3 4 for (int zeile = 0; zeile < umsaetze.length; zeile++) { for (int spalte = 0; spalte < umsaetze[zeile].length; spalte++) { Console.print(umsaetze[zeile][spalte] + "\t\t"); } Console.println(); } 5 // Ausgabe mit erweiterter for-Schleife for (double[] filialUmsatz : umsaetze) { for (double quartalsUmsatz : filialUmsatz) { Console.print(quartalsUmsatz + "\t\t"); } Console.println(); } 6 7 } } Konsolen-Ausgabe 65000.0 70000.0 80000.0 56000.0 65000.0 75000.0 54000.0 62000.0 67000.0 52000.0 60000.0 70000.0 Erläuterungen 1 Deklaration eines zweidimensionalen Feldes mittels Initialisierungsliste: Die 3 Unterlisten mit jeweils 4 Elementen bilden die Zeilen des Arrays umsaetze. 2 Um ein zweidimensionales Feld in tabellarischer Form auf der Konsole auszugeben, sind zwei geschachtelte Schleifen erforderlich. Die Zählvariable zeile der äußeren Schleife gibt den Zeilenindex der auszugebenden Zeile vor. Der Zeilenindex beginnt bei 0 (erste Zeile). Die Schleife wird durchlaufen, solange der Wert der Zählvariablen zeile kleiner ist als die Anzahl der Zeilen des Arrays. Diese Anzahl ist in der Eigenschaft length des Arrays umsaetze abgelegt. Daher lautet die Schleifenbedingung: zeile < umsaetze.length 3 Die innere Zählschleife hat eine Zählvariable spalte, den Spaltenindex symbolisiert und über alle Spaltenpositionen der ausgewählten Zeile läuft. Der Spaltenindex beginnt bei 0 (erste Spalte). Die Länge der auszugebenden Zeile wird über die Eigenschaft length des gewählten Zeilenvektors umsaetze[zeile] erfragt. Daher lautet die Schleifenbedingung: spalte < umsaetze[zeile].length 4 Auf die Elemente des Feldes wird über die Angabe des Feldnamens und der Positionsnummern der Zeile und Spalte zugegriffen: umsaetze[zeile][spalte] Die zur besseren Formatierung werden in der Ausgabeanweisung nach dem Umsatzwert jeweils zwei Tabulator-Sequenzen ("\t\t") eingefügt. 5 Nach der Ausgabe einer Zeilen wird ein Zeilenvorschub ausgeführt. 6 Das zweidimensionale Feld umsaetze besteht aus zeilenweise angeordneten eindimensionalen Feldern. Der Iterator, mit dem die einzelnen Zeilen angesprochen werden können, muss daher eine Feldvariable sein (double[] filialUmsatz). Mit diesem Iterator durchläuft die äußere Schleife die Zeilen der Matrix. 7 Die innere Schleife iteriert mit der double-Variablen quartalsUmsatz über alle Elemente des Zeilenfeldes filialUmsatz. C. Endreß 16 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Um ein mehrdimensionales Feld zu erstellen, muss wie bereits vom Eindimensionalen bekannt eine FeldVariable deklariert werden. Bei der Deklaration der Feld-Variablen ist für jede Dimension des Feldes ein eckiges Klammerpaar hinter dem Typ der Elemente anzugeben. Beispiel: Erzeugung eines Arrays mit 3 x 2 Elementen vom Typ int int[][] matrix; // Deklaration eines 2-dimensionalen Feldes matrix = new int[3][5]; // Initialisierung des Array-Objekts mit 3 Zeilen // und 5 Spalten Deklaration und Initialisierung können bei mehrdimensionalen Feldern auch in einer Anweisung erfolgen. int[][] matrix = new int[3][5]; Geschachtelte Felder können auch über geschachtelte Initialisierungslisten erzeugt werden. int[][] matrix = { {1, -1, 2}, {2, 0, 1}, {1, 0, 0} }; Feld 1 -1 2 2 0 1 1 0 0 Unterfelder Die Unterfelder müssen nicht gleich groß sein: int[][] pascal = { {1}, {1, 1}, {1, 2, 1} }; Feld 1 1 1 1 2 Unterfelder 1 Länge des ersten Unterfeldes: pascal[0].length = 1 Länge des zweiten Unterfeldes: pascal[1].length = 2 Länge des dritten Unterfeldes: pascal[2].length = 3 Bei geschachtelten Feldern muss mindestens die Elementzahl der obersten Schachtelungsebene angegeben werden. Bei tieferen Ebenen ist die Angabe optional. Wird eine Längenangabe in einer tieferen Ebene gemacht, dann müssen alle darüber liegenden Ebenen ebenfalls festgelegt werden. Lücken dürfen bei der Festlegung nicht entstehen. Außerdem müssen bei der Anwendung des newOperators so viele Ebenen wie in der Deklaration angegeben werden. double[][][] dreiDimensionen1, dreiDimensionen2, dreiDimensionen3; C. Endreß dreiDimensionen1 = new double[2][3][]; // erlaubt dreiDimensionen2 = new double[2][][4]; // Lücke nicht erlaubt dreiDimensionen3 = new double[5][]; // eine Ebene fehlt, // nicht erlaubt 17 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Der Arrayindex wird in Java grundsätzlich von 0 an gezählt. Bei geschachtelten Feldern muss der Index für jede Dimension in einem eigenen Klammernpaar stehen. int[][] matrix = new int [5][5]; int x = matrix[3][4]; 6.5 Enumerationen Eigene Konstanten können Sie erstellen, indem Sie Variablen als static final deklarieren. Wenn Sie jedoch einen ganzen Satz von konstanten Werten erzeugen wollen, die die einzigen gültigen Werte für eine Variable repräsentieren, benötigen Sie einen sogenannten Aufzählungstyp. Aufzählungstypen bezeichnet man auch als Enumerationen. In Java werden Enumeratioen mit dem Schlüsselwort enum definiert. Java-Quellcode /* Programm: DemoEnum Demonstriert die Handhabung einer einfachen Aufzählung*/ public class DemoEnum { enum Ampel {ROT, GELB, GRUEN}; public static void main(String[] args) { Ampel signal = Ampel.ROT; for(Ampel eineLampe : Ampel.values()){ System.out.println("Lampe: " + eineLampe); } if(signal == Ampel.ROT){ System.out.println("STOP! Rote Ampel!"); } } } Konsolen-Ausgabe Lampe: ROT Lampe: GELB Lampe: GRUEN STOP! Rote Ampel! Sie können Enumerationen in bedingten Anweisungen wie if- und switch-Anweisungen verwenden. Enum-Instanzen können mit == oder der Methode equals() verglichen werden. Mit einer erweiterten for-Schleife kann man eine Aufzählung durchlaufen. Die Methode values() liefert ein Feld mit allen Werten der Aufzählung. Die Reihenfolge der Feldelemente entspricht der Reihenfolge der Initialisierungsliste. C. Endreß 18 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten Wenn Sie eine Enumeration erstellen, erzeugen Sie eine neue Klasse und erweitern implizit die Klasse java.lang.Enum. Sie können eine Enumeration als eigenständige Klasse in einer eigenen Quelldatei deklarieren oder als Member einer anderen Klasse wie im Beispiel die enum Ampel. Sie können Enumerationen Konstruktoren, Methoden, Variablen und einen sogenannten konstantenspezifischen Klassenbody zuweisen. /* Programm: DemoEnumComplex Das Programm demonstriert die Deklaration einer Enumeration in einer eigenständigen Klasse. */ public enum Namen { JERRY("Lead-Gitarre") { public String singt() { return "heiser"; } }, BOBBY("Schlagzeug") { public String singt() { return "gar nicht"; } }, PHIL("Bass"); private String instrument; Namen(String instrument) { this.instrument = instrument; } public String getInstrument() { return instrument; } public String singt() { return "gelegentlich"; } } public class DemoEnumComplex { public static void main(String[] args) { for (Namen name : Namen.values()) { System.out.print(name + " spielt " + name.getInstrument()); System.out.println(" und singt " + name.singt()); } } } Konsolen-Ausgabe JERRY spielt Lead-Gitarre und singt heiser BOBBY spielt Schlagzeug und singt gar nicht PHIL spielt Bass und singt gelegentlich C. Endreß 19 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten 6.6 Zeichenketten: Die Klasse String Zeichenketten werden in Java nicht mittels eines einfachen Datentypen, sondern in Form eines Referenzdatentypen durch die Klasse String dargestellt. (Package: java.lang.String) Die Klasse String bietet Methoden zum Erzeugen von Zeichenketten, zur Extraktion von Teilstrings, zum Vergleich mit anderen Strings, und zur Erzeugung von Strings aus primitiven Typen Strings sind Objekte. String-Variablen sind (wie Array-Variablen auch) Referenzenvariablen. Ein Vergleich der Referenzvariablen mit dem Gleichheitsoperator (==) vergleicht lediglich die Referenzen nicht aber den Inhalt der String-Objekt. Zum physischen Kopieren von Zeichenketten benötigt man geeignete Methoden. Es reicht nicht aus, die Referenzvariable eine Strings einer anderen String-Variablen zuzuweisen, da hierbei nur die Referenzen kopiert werden. Zeichenketten sind keine Zeichen-Felder. 6.6.1 Strings erzeugen Zeichenketten-Objekte können auf zwei Arten erzeugt werden: Vereinfachte Syntax (Literale Initialisierung) String text = “Java“; Konstruktoren text String text = new String( “Java“ ); Erzeugt einen neuen String aus einem String-Literal. J String-Variable (Referenz) a v String-Objekt String neuerText = new String( alterText ); Erzeugt einen neuen String durch Duplizierung eines vorhandenen Strings. char[] zeichenfeld = {’J’‚ ’a’, ’v’, ’a’}; String text = new String( zeichenfeld ); Erzeugt einen String aus einem vorhandenen Zeichen-Array. C. Endreß 20 / 23 10/2008 a 6. Felder, Enumerationen und Zeichenketten 6.6.2 Strings verwenden Zeichenketten verbinden: Konkatenation Zeichenketten können mit dem + Operator aneinandergehängt werden. String text = “Java“; String neuerText = text + “ ist eine Insel.”; Länge einer Zeichenkette public int length() Liefert die Länge einer Zeichenkette. Wenn das String-Objekt leer ist, wird der Wert 0 zurückgegeben. String text = “Java“; int laenge = text.length(); // laenge = 4 Vergleichen von Zeichenketten public boolean equals(Object einObjekt) Die Methode equals() liefert true, wenn das aktuelle String-Objekt und einObjekt gleich sind. Diese Vergleichsmethode steht übrigens für alle Objekttypen, die von der Klasse Object abgeleitet wurden zur Verfügung, muss allerdings für selbst definierte Klassen auch selbst implementiert werden, da die Basisklasse Object nicht wissen kann, wie Objekte selbst definierter Klassen auf Gelichheit zu prüfen sind. String meinName = “Karl“; String deinName = “Otto“; if(meinName.equals(deinName)) Console.println(“Die Namen sind gleich!“); else Console.println(“Die Namen sind nicht gleich!“); Ein Vergleich von String-Variablen mit dem Gleichheitsoperator == vergleicht nicht die Inhalte der Zeichenketten, sondern lediglich die Referenzen, also die Adressen, auf die sich die String-Variablen beziehen. if(meinName == deinName) Console.println(“Die Referenzen zeigen auf dasselbe String-Objekt“); meinName String-Variablen (Referenzen) deinName C. Endreß K a r l String-Objekte == O 21 / 23 t t equals() o 10/2008 6. Felder, Enumerationen und Zeichenketten Wichtige Methoden der Klasse String: Methode Beschreibung charAt() holt ein Zeichen aus dem String heraus compareTo() lexikalischer Vergleich zweier Strings equals() equalsIgnoreCase() prüfen auf Gleichheit startsWith() endsWith() vergleichen den Anfang bzw. das Ende eines Strings mit einem angegebenen Wert indexOf() lastIndexOf() suchen in einem String vorwärts bzw. rückwärts nach einem angegebenen Zeichen oder Teilstring substring() gibt einen Teilstring aus einem String zurück replace() erzeugt eine neue Kopie aus dem String, in der ein Zeichen durch ein anderes ersetzt wird toUpperCase() toLowerCase() konvertieren die Groß-/Kleinschreibung eines Strings trim() entfernt Leerraum am Anfang und Ende length() gibt die Länge des Strings zurück valueOf() Statische Methode, die diverse primitive Datentypen in Strings konvertiert String-Objekte sind unveränderlich! Es gibt keine Methode, um den Inhalt zu verändern. Die obigen Methoden, die einen String zurückgeben, modifizieren nicht den übergebenen String, sondern erzeugen stattdessen eine modifizierte Kopie. Die Klasse StringBuffer stellt Zeichenketten zur Verfügung, die veränderbar sind. Umwandlung einer Zahl in einen String int zahl = 12; String zahlAlsText; zahlAlsText = String.valueOf(zahl); Umwandlung eines Strings in eine Zahl int zahl; String text = "123"; zahl = Integer.parseInt(text); In Java gibt es Hüllklassen (wrapper classes) für einfache Datentypen. Diese Klassen heißen gleich oder ähnlich wie die zugehörigen Typen. Sie beginnen aber wie bei Klassennamen üblich mit Großbuchstaben: z.B. die Klasse Integer für den Typ int. Die Hüllklassen stellen Operationen und Attribute bereit, die dem elementaren Typ zugeordnet werden, wie z.B. Zeichenkettenumwandlungen. Seit der J2SE 5.0 gibt es einen Mechanismus, der das automatische Ein- und Auspacken von primitiven Typen in und aus Wrapper-Klassen unterstützt. Dieses als Autoboxing bzw. Autounboxing ("automatisches Ein- und Auspacken") bezeichnete Verfahren sorgt dafür, dass an vielen Stellen automatisch zwischen primitiven Typen und Wrapperobjekten konvertiert wird. Erwartet eine Methode C. Endreß 22 / 23 10/2008 6. Felder, Enumerationen und Zeichenketten beispielsweise einen Integer-Wert als Argument, kann außer einem Integer auch direkt ein int übergeben werden. Dieser wird dann automatisch in einen gleichwertigen Integer konvertiert. Auch in umgekehrter Richtung funktioniert das, etwa wenn in einem arithmetischen Ausdruck ein double erwartet, aber ein Double übergeben wird. Damit kann ab J2SE 5 die Umwandlung eines Strings in eine Zahl auch folgendermaßen ablaufen: int zahl; String text = "123"; zahl = Integer.valueOf(text); C. Endreß 23 / 23 10/2008 7. Funktionen 7. Funktionen Lernziele ☺ Wissen, wie Programme durch das Verwenden von Funktionen strukturiert werden und welche Vorteile dieses Strukturierungsprinzip besitzt. ☺ Die Parameterübergabemechanismen erläutern können. ☺ Den Gültigkeitsbereich von Parametern und Variablen kennen. ☺ Methoden schreiben und den geeigneten Parameterübergabemechanismus anwenden können. ☺ Überladene Methoden bei geeigneten Problemlösungen verwenden können. ☺ Einfache rekursive Methoden schreiben können. 7.1 Divide et impera! Eine Funktion ist ein abgeschlossener Programmteil, der aus einer oder mehreren Anweisungen besteht, und einen eigenen Namen besitzt. Unter diesem Namen kann die Funktion von einem Programmteil aufgerufen werden. Eine Funktion erledigt eine abgeschlossene Teilaufgabe. Vorteile des Funktionskonzepts: Zerlegung komplexer Problemstellungen in einfachere Teilprobleme Übersichtlichkeit des Quellcodes durch Strukturierung Mehrfachverwendung und Wiederverwendbarkeit von Softwarekomponenten Einfachere Programmpflege, bessere Wartbarkeit Funktionen stellen Dienstleistungen zur Verfügung. Zum Anwenden einer Funktion muss bekannt sein, „was“ die Funktion macht. „Wie“ die Funktion ihre Aufgabe ausführt (Algorithmen und Variablen), ist für den Anwender uninteressant. (Funktionale Abstraktion) Beim Aufruf einer Funktion wird ein Speicherbereich reserviert, in dem die lokalen Variablen der Funktion abgelegt werden. Nach Beendigung der Funktion kehrt das Programm wieder an die Aufrufstelle zurück und setzt die Programmausführung fort. Funktionen können durch Parameter Werte übergeben werden. In Funktionen können lokale Variablen deklariert werden, die unabhängig von anderen Funktionen sind. Allgemein bezeichnet man Funktionen, die Bestandteile von Klassen sind, als Methoden. Da JavaProgramme immer aus Klassen bestehen, spricht man in Java nur von Methoden. C. Endreß 1/9 05/2007 7. Funktionen 7.2 Methoden Eine Methode muss deklariert werden. In Java muss die Deklaration immer in einer Klasse erfolgen. Methoden Syntax Modifier Typ MethodenName([Parameterliste]) { Anweisung(en); [ return rueckgabeWert; ] } Der Modifier gibt die Sichtbarkeit der Methode an. private : Methode ist nur innerhalb der eigenen Klasse aufrufbar public : Methode kann auch von anderen Klassen aufgerufen werden protected : gilt im Zusammenhang mit Vererbung, dazu später mehr Typ gibt den Datentyp des Rückgabewertes an. Mit return gibt die Methode einen Wert an den Aufrufer zurück. Wenn die Methode kein Ergebnis zurückgibt, ist der Typ void zu verwenden. Die Parameterliste ist optional. Mehrere Parameter werden durch Komma getrennt. Für jeden Parameter sind der Typ und der Name des Parameters anzugeben. Die Parameter in der Deklaration bezeichnet man als formale Parameter. Die Werte, die beim Methodenaufruf an die Methode übergeben werden, nennt man aktuelle Parameter. Beispiel Sichtbarkeit Rückgabetyp Methodenname formaler Parameter public double berechneQuadrat( double x ) { return x * x; } C. Endreß 2/9 05/2007 7. Funktionen 7.2.1 Aufruf von Methoden Eine Methode wird über ihren Namen aufgerufen. Auf den Namen folgt in Klammern die Liste der aktuellen Parameter. Beispiel: import support.Console; public class MethodenAufruf { public static double quadrat( double x ) { return x * x; } 1 2 public static void main(String[] args) { double y; y = quadrat( 2.5 ); Console.print(“2.5 hoch 2 ist “ + y); } 3 } 1 Deklaration der Methode quadrat. Das Schlüsselwort static deklariert die Methode als sogenannte statische Methode, damit wir sie in der Methode main einfach mit dem Methodennamen aufrufen können. Nähere Erläuterungen zu statischen Methoden erfolgen weiter unten. 2 Die Methode gibt ein Ergebnis mit der return-Anweisung an den Aufrufer zurück. 3 Aufruf der Methode: Der aktuelle Parameter hat den Wert 2.5. Dieser wird an die Methode übergeben. Der Rückgabewert der Methode quadrat wird der Variable y zugewiesen. 7.2.2 Datenaustausch zwischen Methoden Der Aufrufer einer Methode tauscht Daten mit der Methode über eine „Schnittstelle“ aus, die im Methodenkopf durch die Festlegung der Parameterliste und des Rückgabetyps definiert wird. Zinsertrag = berechneZins(Kapital, Zinssatz, Anlagedauer); Schnittstelle Methodenaufruf Zinsertrag Methode Kapital double k Zinsatz double p Anlagedauer int T int int berechneZins zinsen Rückgabewert C. Endreß 3/9 05/2007 7. Funktionen Als Seiteneffekt ist bezeichnet man einen Datenaustausch, der an der Schnittstelle vorbeiläuft. Das kann z.B. bei der Verwendung von globalen Variablen passieren. Seiteneffekte sind unbedingt zu vermeiden, da sie das Prinzip der funktionalen Abstraktion („was“ vom „wie“ trennen) untergraben und die Modularisierung von Softwarekomponenten verhindern. 7.2.3 Gültigkeitsbereich von Variablen Alle Variablen, die in einer Methode deklariert werden, sind lokale Variablen und existieren nur innerhalb der Methode. Der Speicherplatz für lokale Variablen wird erst angelegt, wenn die Methode ausgeführt wird. Wenn die Methode beendet ist, wird der Speicherplatz wieder freigegeben und kann für andere Zwecke verwendet werden. D.h. die Speicherinhalte sind nach dem Ende der Methode nicht mehr verfügbar. Auch die formalen Parameter des Methodenkopfs sind lokale Variablen der Methode. import support.Console; public class MehrWertSteuer { public static double mwst( double netto, double mwstSatz){ double steuer; steuer = netto * mwstSatz / 100; return steuer; } 1 2 3 4 public static void main(String[] args) { double netto = 200.0; double brutto = 0.0; brutto = netto + mwst( netto, 16.0 ); Console.println("Netto : " + netto); Console.println("Brutto: " + brutto); } 5 6 7 8 9 } Ein Ablaufprotokoll verdeutlich den Programmablauf: Zeile C. Endreß main() netto lokale Variable in mwst(), verdeckt gleichnamige Variable aus main() brutto 5 200.0 6 200.0 0.0 7 200.0 0.0 netto mwstSatz 1 --- --- 200.0 16.0 2 --- --- 200.0 16.0 3 --- --- 200.0 16.0 32.0 4 --- --- 200.0 16.0 32.0 8 200.0 232.0 mwst() 4/9 steuer 05/2007 7. Funktionen 7.2.4 Parameterübergabe Welcher aktuelle Parameter welchem formalen Parameter zugeordnet wird, wird in Java durch die Positionszuordnung festgelegt. Beim Methodenaufruf werden die akutellen Parameter in der Reihenfolge angegeben, die durch die Liste der formalen Parameter bei der Methodendeklaration vorgegeben ist. Der erste aktuelle Parameter wird dem ersten formalen Parameter zugeordnet, der zweite aktuelle Parameter dem zweitem formalen Parameter usw. Die Anzahl der aktuellen und formalen Parameter müssen immer übereinstimmen. public static void main(String[] args) { ... netto = 200.0; brutto = netto + mwst( netto, 16.0 ); ... } aktuelle Parameter 200.0 32.0 16.0 public static double mwst(double betrag, double mwstSatz){ double steuer; steuer = betrag * mwstSatz / 100; formale Parameter return steuer; } Grundsätzlich unterscheidet man zwei Arten der Parameterübergabe: die Übergabe per Wert (call by value ) die Übergabe per Referenz (call by reference) call by value In Java werden einfache Datentypen per Wert übergeben, d.h. der formale Parameter wird mit einer Kopie des aktuellen Parameters initialisiert. Veränderungen am Wert des formalen Parameters bleiben lokal beschränkt und wirken nicht auf den Eingabeparameter des aufrufenden Programmteils zurück. netto 200.0 Kopie betrag 200.0 In der Liste der aktuellen Parameter kann auch ein beliebiger Ausdruck stehen, der dem Datentyp des formalen Parameters entspricht (z.B. ein arithmetischer Ausdruck wie 7.5 + 14.8). Dieser wird zunächst ausgewertet und das Ergebnis dann an den formalen Parameter der Methode übergeben. C. Endreß 5/9 05/2007 7. Funktionen call by reference Objekte werden in Java per Referenz übergeben. Da Objekte sehr groß sein können (z.B. Arrays), ist es nicht sinnvoll, ihre Werte in eine Methode zu kopieren. Es wird lediglich eine Kopie der ObjektVariable, die in Java immer eine Referenz ist, an den formalen Parameter übergeben. Alle Änderungen, die in einer Methode vorgenommen werden, werden direkt am Originalobjekt vorgenommen und nicht an einer Kopie. Das Kopieren von Referenzen spart Speicherplatz und Kopierzeit. public class CallByReference { 1 public static double summe( double[] a ) { double sum = 0.0; for( int i = 0; i < a.length; i++) sum = sum + a[i]; return sum; } public static void main(String[] args) { double[] umsatz; ... gesamtUmsatz = summe( umsatz ); ... } 2 3 } 1 Der formale Parameter der Methode summe ist die Array-Variable a. Eine Array-Variable ist immer eine Referenz auf ein Array. Außerdem ist die Variable a eine lokale Variable der Methode summe(). 2 Als Array-Variable ist umsatz ebenfalls eine Referenz auf ein Feld. 3 Der aktuelle Parameter enthält eine Referenz (also eine Speicheradresse) auf das Array umsatz. Dieser Referenzwert wird an den formalen Parameter der Methode summe als übergeben. Der formale Parameter ist die Variable a, die durch die Parameterübergabe zu einer zweiten Referenz auf das bestehende Array wird. Allerdings existiert a nur als lokale Variable in der Methode summe() und wird nach Ausführung der Methode wieder aus dem Arbeitsspeicher gelöscht. Array-Objekt umsatz Kopie der Referenz a C. Endreß 6/9 05/2007 7. Funktionen 7.2.5 Überladen von Methoden Die Signatur einer Methode besteht aus dem Methodennamen sowie der Anzahl und der Reihenfolge der Typen der formalen Parameter. Der Ergebnistyp gehört nicht zur Signatur. public static double berechneZins(double k, double p, int T) Signatur Bei einem Methodenaufruf wird zunächst die Signatur der Methode bestimmt und anschließend die passende Methode gefunden. Falls in einer Klasse zwei oder mehr Methoden dieselbe Signatur besitzen, gibt der Compiler Fehlermeldungen aus, weil die Methoden nicht mehr unterscheidbar sind. Besitzen zwei oder mehr Funktionen derselben Klasse denselben Namen, aber sonst unterschiedliche Signaturen, so bezeichnet man den Funktion als überladen. Vorteil des Überladens: Man muss keine unterschiedlichen Methodennamen wählen, wenn Methoden sich lediglich in ihren Parametern unterscheiden aber sonst die gleiche Aufgabe erledigen. 1 public class MethodenUeberladen { public static double summe( double[] a ) { double sum = 0.0; for( int i = 0; i < a.length; i++) sum = sum + a[i]; return sum; } public static int summe( int[] a ) { int sum = 0.0; for( int i = 0; i < a.length; i++) sum = sum + a[i]; return sum; } 2 public static void main(String[] args) { double[] umsatz; int[] arbeitsTage; ... jahresArbeitsZeit = summe( arbeitsTage ); jahresUmsatz = summe( umsatz ); ... } 3 4 } 1 Deklaration der ersten Methode summe mit einen Parameter vom Typ double[]. 2 Deklaration der ersten Methode summe mit einen Parameter vom Typ int[]. 3 Passend zur Signatur des Aufrufs wird die Methode in Zeile 2 aufgerufen. 4 Passend zur Signatur des Aufrufs wird die Methode in Zeile 1 aufgerufen. C. Endreß 7/9 05/2007 7. Funktionen 7.2.6 Statische Methoden Methoden mit dem Attribut static (statisch) sind nicht an die Existenz eines konkreten Objekts gebunden, sondern existieren vom Laden der Klasse bis zum Beenden des Programms. Statische Methoden können aufgerufen werden, ohne ein Objekt der Klasse zu verwenden, in der die Methode deklariert wurde. Methoden, die nicht statisch sind, können nur über zugehörige Objekte aufgerufen werden. einige Beispiele: statische Methoden nicht statische Methoden public static void main(String[] args) String text = “Hallo”; text = text.toUpperCase(); int zahl = Console.readInt(); char zeichen = text.charAt(4); double x = Math.sqrt(6.25); Console.print(“Ich bin static”); 7.2.7 Rekursion Wenn eine Anweisung innerhalb einer Methode die Methode selbst wieder aufruft, bezeichnet man die Methode als rekursiv. Mit rekursiven Algorithmen können Problem auf sehr elegante Weise gelöst werden. Da allerdings bei jedem erneuten Aufruf der Funktion ein eigener Namensraum für die Funktion und ihre lokalen Daten angelegt wird, ist die Performance von rekursiven Algorithmen schlechter als bei iterativen Lösungen. Beispiel: Fakultät Die Fakultät einer natürlichen Zahl ist folgendermaßen definiert: 5 ! = 5 ⋅ 4 ⋅ 3 ⋅ 2 ⋅ 1 = 120 n ! = n ⋅ (n − 1) ⋅ (n − 2) ⋅ ... ⋅ 2 ⋅ 1 n != n ⋅ (n − 1)! Rekursive Formulierung: 1 , falls ⎧ n! = ⎨ ⎩ n ⋅ (n − 1)! , falls n=1 n>1 Rekursive Funktionen bestehen aus einem Teil, der das Problem verkleinert und einen rekursiven Aufruf ausführt sowie einem Teil, der ein Ergebnis zurückliefert (Basisfall, Abbruchbedingung). C. Endreß 8/9 05/2007 7. Funktionen Der Quellcode einer rekursiven Fakultätsfunktion könnte so aussehen: public static int fakultaet( int n ) { if( n > 1 ) return n * fakultaet( n – 1 ); else return 1; } Zur Berechnung von 5! ergibt sich der folgende Rekursionsablauf: rekursiver Abstieg rekursiver Aufstieg 5! 5! 5! = 5 * 24 = 120 5 * 4! 5 * 4! 4! = 4 * 6 = 24 4 * 3! 4 * 3! 3! = 3 * 2 = 6 3 * 2! 3 * 2! 2! = 2 * 1 = 2 2 * 1! 2 * 1! 1! = 1 1 Basisfall 1 Solange das Argument n > 1 ist, wird die Funktion fakultaet mit einem verkleinerten Argument (n-1) erneut aufgerufen. Erst wenn der Basisfall n = 1 erreicht wird, wird ein Wert (hier 1) zurückgegeben. C. Endreß 9/9 05/2007 8. Objekte und Klassen – Teil 1 8. Objekte und Klassen – Teil 1 Lernziele ☺ Die Begriffe Klasse, Objekt, Attribut, Operation und Botschaft anhand von Beispielen erklären können. ☺ UML-Notation zur Darstellung von Klassen anwenden. 8.1 Einführung Objektorientierte Programmierung erlaubt eine natürliche Modellierung vieler Problemstellungen. Ein Objekt ist ein tatsächlich existierendes „Ding“ aus der Anwendungswelt des Programms. Beispiel: Girokonto einzahlen 4711 auszahlen Kontostand Objekte haben bestimmte Eigenschaften, ein bestimmtes Verhalten, eine Identität. Denke in Begriffen des Problems! Man muss unterscheiden zwischen der Beschreibung von Objekten und den Objekten selbst: Die Beschreibung eines Objekts entspricht einem Bauplan (Klasse). Mit einem Bauplan können beliebig viele Objekte erstellt werden (Exemplare, Instanzen). Vorteile der objektorientierten Programmierung (OOP): Objektorientierung wird nicht nur in der Programmierung, sondern auch als Prinzip in der Analyse und dem Entwurf von Software eingesetzt. Daraus ergibt sich eine Software-Entwicklung „aus einem Guss“. Modularisierung des Software-Projekts Wiederverwendbarkeit von Software-Komponenten C. Endreß 1 / 12 08/2006 8. Objekte und Klassen – Teil 1 8.2 Klassen Beispiel Für eine Bank-Software soll eine Kontoverwaltung entwickelt werden. Zunächst sollen nur Girokonten verwaltet werden. In einem ersten Schritt werden die Anforderungen an die Software in einem Pflichtenheft zusammengefasst: /1/ Zu einem Girokonto werden eine Kontonummer und der aktuelle Kontostand gespeichert. /2/ Wird ein Girokonto angelegt, so wird diesem eine Kontonummer zugewiesen. /3/ Es werden Ein- und Auszahlungen auf dem Girokonto vorgenommen. /4/ Alle Daten des Kontos können einzeln gelesen werden. Modellierung des Girokontos Ein Girokonto hat einen Zustand, der durch seine Attribute (z.B. seinen Kontostand und seine Kontonummer) beschrieben wird. Ein Girokonto hat ein Verhalten, das durch seine Operationen (z.B. einzahlen und auszahlen) beschrieben wird. Um eine Operation aufzurufen, wird eine Botschaft an das betreffende Objekt gesendet. Diese Botschaft aktiviert eine Operation gleichen Namens. Die gewünschten Ausgabedaten werden an den Sender der Botschaft zurückgegeben. Empfang einer Botschaft einzahlen auszahlen kontoNr kontoStand getKontoNr Empfang einer Botschaft C. Endreß getKonto Stand Antwort auf eine Botschaft 2 / 12 08/2006 8. Objekte und Klassen – Teil 1 Definitionen Attribute sind Eigenschaften eines Objekts. Sie beschreiben die Daten, die von Objekten einer Klasse angenommen werden können. Attribute sind also ... Datenelemente, die durch einen Namen bezeichnet werden, einen bestimmten Typ haben und verschiedene Werte speichern können. Alle Objekte einer Klasse besitzen dieselben Attribute, die sich im allgemeinen jedoch in ihren Werten unterscheiden. Operationen oder Methoden beschreiben das Verhalten eines Objekts. Sie sind Funktionen, die in der Klasse definiert sind. Jede Operation kann auf alle Attribute eines Objekts dieser Klasse zugreifen. Operationen kommunizieren mit der Umwelt über Ein- und Ausgabeparameter. Das Senden einer Botschaft oder Nachricht an ein Objekt bedeutet den Aufruf der gleichnamigen Operation. Mit einem Objekt können nur die Aktionen ausgeführt werden, die es nach außen zur Verfügung stellt (Schnittstelle). Ein Objekt reagiert nur auf Botschaften, die es „versteht“. Eine Botschaft besteht aus 3 Teilen: Empfänger-Objekt Name der Methode Parameter der Methoden Ein Objekt kann mit einem Rückgabewert auf eine Botschaft antworten. Methoden mit Rückgabewerten werden üblicherweise zum Abfragen der aktuellen Attributwerte verwendet (z.B. getKontoStand()) Eine Klasse ist ein Bauplan für Objekte. Sie definiert die Attribute und Methoden, die alle Objekte besitzen, die von dieser Klasse erzeugt werden. Jede Klasse besitzt einen Namen (z.B. „GiroKonto“). Jede Klasse besitzt einen Mechanismus, um Objekte zu erzeugen. Dieser Mechanismus wird durch eine oder mehrere (überladene) Methoden realisiert, deren Name gleich dem Klassennamen ist. Man nennt diese Methoden Konstruktoren. Die Attribute sind in der Regel private deklariert. Damit sind sie außerhalb der Klasse nicht sichtbar und können nur über Zugriffsmethoden angesprochen werden, welche die Klasse selbst zur Verfügung stellt. Dieses Prinzip der Kapselung ist eine wichtige Eigenschaft objektorientierter Programmiersprachen. In der objektorientierten Programmierung verwendet man zur Modellierung eine graphische Notationsform namens UML (Unified Modelling Language). C. Endreß 3 / 12 08/2006 8. Objekte und Klassen – Teil 1 UML-Klassen-Diagramm GiroKonto - kontoStand: double Klassenname Attribute - kontoNr: int + GiroKonto() Java-Klassendeklaration public class GiroKonto { //-------------- Attribute -------------private double kontoStand; private int kontoNr; Methoden //-------------- Methoden --------------// Konstruktor public GiroKonto(int kontoNr) { this.kontoNr = kontoNr; kontoStand = 0.0; } + einzahlen() + auszahlen() + getKontoNr() + getKontoStand() public void einzahlen(double betrag) { kontoStand = kontoStand + betrag; } public double auszahlen(double betrag) { kontoStand = kontoStand - betrag; return betrag; } public int getKontoNr() { return kontoNr; } public double getKontoStand() { return kontoStand; } } 8.3 Objekte Objekte der OOP sollen Objekte der realen Welt abbilden. Reale Objekte können konkret (z.B. Fahrrad, Girokonto) oder abstrakt (z.B. Menge der ganzen Zahlen) sein. Wie reale Objekte haben Software-Objekte eine Identität -> jedes Objekt kann eindeutig identifiziert werden einen Zustand -> jedes Objekt hat charakteristische Merkmale (Attribute) ein Verhalten -> jedes Objekt kann in bestimmter Weise handeln (Methoden) Objekte sind Instanzen von Klassen. Objekte verhalten sich, wie die Klasse es vorgibt. C. Endreß 4 / 12 08/2006 8. Objekte und Klassen – Teil 1 Objekte interagieren durch Nachrichtenaustausch. Objekt A schickt eine Botschaft an Objekt B. Objekt B reagiert darauf in geeigneter Weise. Objekte sind die Bausteine für Programme. Ein Objekt ist eine Software-Einheit, die aus Daten (Attribute) und Funktionalität (Methoden) besteht. Die Klassendeklaration legt die Attribute und Methoden der zugehörigen Objekte fest. Sie legt aber nicht die Attributwerte fest. Um ein Objekt von einer Klasse anzulegen, muss eine Objektvariable vom Typ der Klasse deklariert werden. Mit Hilfe des new-Operators wird dieser Variable ein neu erzeugtes Objekt zugewiesen. Ein Objekt wird nie direkt angesprochen. Objektvariablen sind Referenzen auf Objekte. Ein Objekt kann viele Referenzen besitzen. Beispiel: /* Programm: Kontoverwaltung Demonstriert die Handhabung der Klasse GiroKonto */ import support.Console; public class KontoVerwaltung { public static void main(String[] args) { GiroKonto erstesKonto; GiroKonto zweitesKonto; 1 2 erstesKonto = new GiroKonto(4711); zweitesKonto = new GiroKonto(4712); 3 erstesKonto.einzahlen(1000); zweitesKonto.einzahlen(2000); 4 5 double betrag = erstesKonto.auszahlen(500); zweitesKonto.einzahlen(betrag); 6 Console.println("Konto " + erstesKonto.getKontoNr() + ": Kontostand = " + erstesKonto.getKontoStand()); Console.println("Konto " + zweitesKonto.getKontoNr() + ": Kontostand = " + zweitesKonto.getKontoStand()); } } Konsolen-Ausgabe Konto 4711: Kontostand = 500.0 Konto 4712: Kontostand = 2500.0 Erläuterungen 1 C. Endreß Deklaration von zwei Objekt-Variablen (Referenzen) der Klasse GiroKonto. 5 / 12 08/2006 8. Objekte und Klassen – Teil 1 2 Erzeugung zweier Objekte durch den Aufruf des Konstruktors der Klasse GiroKonto in Verbindung mit dem new-Operator. Der Konstruktor erhält als Parameter die Kontonummer des zu erzeugenden Kontos. Die Variablen erstesKonto und zweitesKonto sind Referenzen auf die erzeugten GiroKonto-Objekte. Referenz Objekt erstesKonto kontoNr 4711 kontoStand 0.0 kontoNr zweitesKonto 4712 kontoStand 0.0 3 Aufruf der Methode einzahlen() für beide GiroKonto-Objekte. Methoden werden in Java über den Namen der Objekt-Variable gefolgt vom Punkt-Operator und dem Methodennamen aufgerufen. 4 Aufruf der Methode auszahlen() für das Objekt erstesKonto. Die Methode liefert den Auszahlungsbetrag als Rückgabewert, der der Variable betrag zugewiesen wird. 5 Der Betrag wird mit der Methode einzahlen() auf das Objekt zweitesKonto eingezahlt. 6 Die beiden Ausgabeanweisungen geben mit Hilfe der jeweiligen get-Methoden die Attributwerte der beiden GiroKonto-Objekte aus. Objekt-Diagramm Die UML stellt mit Objekt-Diagrammen eine graphische Möglichkeit zur Darstellung von Objekten zur Verfügung. Ein Objekt-Diagramm beschreibt Objekte, Attributwerte und Verbindungen zwischen Objekten zu einem bestimmten Zeitpunkt des Programms. UML-Objekt-Diagramm Objektname Attribute einObjekt:Klasse attribut1 = Wert1 attribut2 = Wert2 :Klasse einObjekt C. Endreß Klassenname Vollständige Darstellung Im unteren Feld werden die im Kontext relevanten Attribute angegeben. Die Attributtypen sind bereits bei der Klasse deklariert. Sie müssen deshalb hier nicht mehr angegeben werden. Verkürzte Darstellungen Wenn die Klasse aus dem Kontext ersichtlich ist, genügt auch der unterstrichene Objektname. 6 / 12 08/2006 8. Objekte und Klassen – Teil 1 Beispiel: Objekt-Diagramm Im vorangegangenen Programm KontoVerwaltung.java wurden zwei Objekte erzeugt, deren Zustand am Programmende mit folgendem Objekt-Diagramm beschrieben werden kann. erstesKonto:GiroKonto zweitesKonto:GiroKonto kontoNr = 4711 kontoStand = 500.0 kontoNr = 4712 kontoStand = 2500.0 Verbindungen zwischen Objekten werden mit einfachen Verbindungslinien zwischen den ObjektSymbolen dargestellt. Konstruktoren Ein Konstruktor ist eine spezielle Methode, die automatisch bei der Erzeugung eines Objekts aufgerufen wird. Konstruktoren erhalten den Namen der Klasse, zu der sie gehören. Konstruktoren haben keinen Rückgabewert – auch nicht void. Konstruktoren sind üblicherweise nach außen sichtbar: public Ein Konstruktor kann nicht wie eine Funktion aufgerufen werden. Konstruktoren dürfen eine beliebige Anzahl von Parametern haben und können überladen werden. Die Parameter dienen zum Initialisieren der Attribute. Wenn ein Konstruktor keine Parameter besitzt, werden die Attribute des erzeugten Objekts mit Standardwerten vorbelegt. Ein Konstruktor ohne Parameter heißt default-Konstruktor. Der default-Konstruktor wird implizit vom Compiler generiert, falls in einer Klasse kein Konstruktor definiert wurde. Wird jedoch ein beliebiger Konstruktor definiert, erzeugt der Compiler keinen default-Konstruktor. Beim Ausführen der new-Anweisung wählt der Compiler anhand der aktuellen Parameterliste den passenden Konstruktor aus und ruft ihn mit den angegebnen Argumenten auf. Beispiel: public class GiroKonto { . . . // parameterloser Konstruktor public GiroKonto(){ kontoStand = 0.0; } public GiroKonto(int kontoNr) { this.kontoNr = kontoNr; kontoStand = 0.0; } } C. Endreß 7 / 12 08/2006 8. Objekte und Klassen – Teil 1 Löschen von Objekten In Java muss sich der Programmierer nicht um das Löschen von Objekten kümmern. In unregelmäßigen Abständen findet automatisch eine Speicherbereinigung (garbage collection) statt. Es werden alle Objekte automatisch gelöscht, auf die keine Referenzen mehr zeigen. Beispiel: public class KontoVerwaltung { public static void main(String[] args) { GiroKonto erstesKonto = new GiroKonto(4711); . . . erstesKonto = null; // explizit zum Löschen freigegeben } Zuweisungsoperator In Java ist jede Variable, die ein Exemplar einer Klasse bezeichnet, eine Referenz auf ein Objekt der Klasse. Eine Zuweisung kopiert keine Objekte! Sie kopiert nur die Referenz darauf. Um eine Kopie eines Objekts zu erhalten, muss man eigene Konstruktoren einführen oder mit sogenannten Clones arbeiten. Objekte als Parameter Bei der Übergabe von Objekten als Parameter von Methoden werden Referenzen weitergegeben. Diesen Parameterübergabemechanismus nennt man call by reference. Im Gegensatz zu call by value werden nun alle Änderungen am Originalobjekt und nicht an einer Kopie des Objekts vorgenommen. Beispiel: import support.Console; public class KontoVerwaltung { 1 static void print(GiroKonto einKonto){ Console.println("Konto " + einKonto.getKontoNr() + ": Kontostand = " + einKonto.getKontoStand()); } 2 public static void main(String[] args) { GiroKonto erstesKonto; GiroKonto zweitesKonto; 3 4 erstesKonto = new GiroKonto(4711); zweitesKonto = erstesKonto; 5 erstesKonto.einzahlen(1200); zweitesKonto.einzahlen(500); 6 print(erstesKonto); print(zweitesKonto); C. Endreß 8 / 12 08/2006 8. Objekte und Klassen – Teil 1 } } Konsolen-Ausgabe Konto 4711: Kontostand = 1700.0 Konto 4711: Kontostand = 1700.0 Erläuterungen 1 Deklaration der Methode print() mit dem Referenz-Parameter einKonto. Die Methode gibt die Kontonummer und den Kontostand eines GiroKonto-Objekts auf der Konsole aus. 2 Deklarieren von zwei Objekt-Variablen der Klasse GiroKonto. 3 Erzeugung eines Girokonto-Objekts durch Konstruktoraufruf. 4 Der Referenz-Variablen zweitesKonto wird eine Referenz auf das Girokonto-Objekt erstesKonto zugewiesen. Beide Referenz-Variablen verweisen damit auf dasselbe Objekt. GiroKontoerstesKonto Objekt Kopie der Referenz zweitesKonto 5 Aufruf der Methode einzahlen() für die Objekte erstesKonto und zweitesKonto. Beide Methodenaufrufe wirken auf dasselbe Objekt, wie die Ausgabe der Attribute in 6 zeigt. 6 Ausgabe der aktuellen Attributwerte des GiroKonto-Objekts. 8.4 Felder von Objekten Frage: Wie können wir unser Beispiel zur Kontoverwaltung so modifizieren, dass wir eine größere Anzahl von Konto-Objekten verwalten können? Antwort: Wir speichern die Konto-Objekte in einem Array. Felder können in Java nicht nur für einfache Datentypen, sondern auch für Referenzdatentypen aufgebaut werden. Als Datentyp der Feldelemente wird die Klasse der zu speichernden Objekte angegeben: Klasse[] arrayName = new Klasse[anzahl]; Beispiel: Kontoverwaltung für 20 Girokonten import support.Console; public class KontoVerwaltung { 1 C. Endreß public static void main(String[] args) { GiroKonto[] konten = new GiroKonto[20]; 9 / 12 08/2006 8. Objekte und Klassen – Teil 1 2 3 for(int i = 0; i < konten.length; i++){ konten[i] = new GiroKonto(4000 + i); } 4 konten[0].einzahlen(1200); konten[1].einzahlen(500); konten[19].einzahlen(7500); 5 printKonto(konten[0]); Console.println("\nKONTOLISTE:"); printAlleKonten(konten); 6 } 7 public static void printKonto(GiroKonto einKonto){ Console.println("Konto " + einKonto.getKontoNr() + ": Kontostand = " + einKonto.getKontoStand()); } 8 public static void printAlleKonten(GiroKonto[] alleKonten){ for(int i = 0; i < alleKonten.length; i++){ printKonto(alleKonten[i]); } } } Konsolen-Ausgabe Konto 4000: Kontostand = 1200.0 KONTOLISTE: Konto 4000: Konto 4001: Konto 4002: . . . Konto 4019: Kontostand = 1200.0 Kontostand = 500.0 Kontostand = 0.0 Kontostand = 7500.0 Erläuterungen 1 Deklaration einer Feldvariablen mit der Bezeichnung konten, die auf die Feldelemente konten[0] bis konten[19] verweist. Die Feldkomponenten sind Referenzen auf Objekte der Klasse GiroKonto. Diese Referenzen zeigen anfangs „nirgendwohin“, d.h. sie referenzieren kein spezifisches Objekt, sondern die sogenannte Null-Referenz, die in Java mit null bezeichnet wird. Um in unserem Programm tatsächlich mit 20 GiroKonto-Objekten arbeiten zu können, müssen wir diese Objekte erst erzeugen. konten 2 C. Endreß null konten[0] null konten[1] null konten[19] Zur Erzeugung der 20 GiroKonto-Objekte verwenden wir eine Zählschleife, die über alle Feldelemente iteriert. Die Zählvariable i beginnt mit 0, dem Index des ersten Feldelements. Die Schleife wird durchlaufen, solange die Zählvariable kleiner als die Anzahl der Feldelemente ist. 10 / 12 08/2006 8. Objekte und Klassen – Teil 1 3 Innerhalb der Schleife initialisieren wir jede Feldkomponente mit einem eigenen GiroKontoObjekt. Der Konstruktor erhält die Kontonummer (hier: 4000 + i) als Parameter und initialisiert mit diesem Wert das Attribut kontoNr. Das Attribut kontoStand wird auf den Wert 0.0 gesetzt. GiroKonto-Objekte konten 4000 kontoNr 0.0 kontoStand konten[0] konten[1] 4001 kontoNr 0.0 kontoStand konten[19] 4019 kontoNr 0.0 kontoStand 4 Die folgenden Anweisungen führen Einzahlungen auf den Objekten konten[0], konten[1] und konten[19] aus. 5 Die Methode printKonto() wird aufgerufen, um die Attribute eines GiroKonto-Objekts auszugeben. In diesem Fall werden die Attributwerte des ersten Array-Elements ausgegeben. 6 Wenn die Daten aller Objekte des GiroKonto-Feldes ausgegeben werden sollen, ist es sinnvoll hierfür eine eigene Methode zu schreiben. 8.5 Klassenvariablen, Klassenmethoden Die Deklaration von Klassenvariablen und Klassenmethoden erfolgt mit dem Schlüsselwort static. Deshalb bezeichnet man Klassenvariablen auch als statische Variablen. // Klassenvariable public static double wechselKurs; // Klassenmethode public static void setWechselKurs(double tagesKurs){...} Eine statische Variable oder statische Methode gehört zur Klasse selbst und nicht zu den Objekten der Klasse. Es müssen keine Objekte für den Zugriff erzeugt werden (z.B. Math.sqrt() oder Console.println()). Innerhalb einer static-Methode darf nur auf Elemente zugegriffen werden, die ebenfalls static sind, da beim Aufruf einer Klassenmethode nicht davon ausgegangen werden kann, dass von dieser Klasse bereits Objekte erzeugt worden sind. Klassenmethoden gelten implizit als final und können nicht überschrieben werden. Klassenvariablen existieren nur einmal im Speicher. Sie haben für alle Instanzen der Klasse denselben Wert. C. Endreß 11 / 12 USDollar wechselKurs ... setWechselKurs() ... 08/2006 8. Objekte und Klassen – Teil 1 Instanzvariablen existieren für jedes Objekt. Sie können für jede Instanz individuelle Werte annehmen. Klassenvariablen und Klassenmethoden werden im UML-Klassendiagramm unterstrichen dargestellt. 8.6 Zusammenfassung Die objektorientierte Programmierung basiert auf einer Reihe von Konzepten, von denen folgende behandelt wurden. Ein Objekt (auch Exemplar oder Instanz genannt) ist ein individuelles Exemplar von Dingen, Personen oder Begriffen. Es besitzt eine Objekt-Identität. Objekte werden durch Konstruktoren erzeugt und im Arbeitsspeicher auf dem sogenannten heap gespeichert. In Java sind Objektvariablen Referenzen auf Objekte. Die Sprache verfügt über eine Speicherbereinigung (garbage collection), die Objekte automatisch löscht, wenn keine Referenz mehr auf sie vorhanden ist. Attribute beschreiben die Eigenschaften eines Objekts. Attributwerte sind die aktuellen Werte, die die Attribute besitzen. Operationen (auch Methoden genannt) beschreiben das Verhalten eines Objekts, d.h. die Dienstleistungen die es seiner Umwelt oder sich selbst zur Verfügung stellt. Operationen kommunizieren mit der Umwelt über Ein-/Ausgabeparameter. Klassen sind der Bauplan für Objekte. Sie fassen Objekte mit gleichen Attributen und Operationen zu einer Einheit zusammen. Das Geheimnisprinzip fordert, dass auf Attributwerte nur über Operationen der Klasse zugegriffen werden kann. Durch Botschaften kommunizieren Objekte und Klassen untereinander. Die UML (Unified Modelling Language) ist eine graphische Notationsform zur Darstellung von Klassen und Objekten. In einem Pflichtenheft werden die Anforderungen an eine Software festgelegt und folgendermaßen durchnummeriert: /1/ Anforderung 1 /2/ Anforderung 2 … /n/ Anforderung n Anhand der Anforderungen ist ein Klassendiagramm in UML-Notation zu erstellen, wobei zunächst nur sogenannte Fachkonzept-Klassen modelliert werden, die den fachlichen Teil der Anforderungen beschreiben. C. Endreß 12 / 12 08/2006 9. Objekte und Klassen – Teil 2 9. Objekte und Klassen – Teil 2 Lernziele ☺ Den Begriff Kapselung erklären können. ☺ Die Begriffe Oberklassen, Unterklasse, Vererbung und Polymorphie anhand von Beispielen erklären können. ☺ Die Konzepte Vererbung, Überschreiben und Polymorphie in Java-Programmen einsetzen können. ☺ UML-Notation zur Darstellung von Klassendiagrammen anwenden. 9.1 Grundprinzipien der OOP Kapselung Vererbung Polymorphie 9.1.1 Kapselung Der Zustand eines Objekts wird durch seine Attributwerte beschrieben. Es ist ein Prinzip der OOP, die Details der Implementierung vor der Außenwelt zu verbergen. Nach außen ist nur die „Schnittstelle“ des Objekts sichtbar. Sie wird gebildet durch Operationen, die für andere Objekte nutzbar sind. Die Komplexität der Benutzung eines Objekts wird durch die Schnittstelle vereinfacht. Um die Operationen zu verwenden, muss man nicht deren interne Implementierung kennen. (Um ein Auto fahren zu können, muss man nicht über die technischen Details des Motors Bescheid wissen.) Kapselung bedeutet, dass Attribute und Implementierung eines Objekts nicht nach außen sichtbar sind. Der Zugriff auf Eigenschaften (Attribute) und Funktionalität (Methoden) von Objekten erfolgt nur über die Schnittstelle des Objekts. Ziele der Kapselung Verbergen komplizierter Einzelheiten Klare Definition der Schnittstelle Vermeiden von Seiteneffekten Unterstützung von Aufgabenteilung und Modularisierung C. Endreß 1 / 12 09/2005 9. Objekte und Klassen – Teil 2 Verwendung eines Objekts wird unabhängig von seiner Implementierung. => Implementierung austauschbar Jede Klasse legt die Zugriffsrechte auf ihre Methoden und Attribute selbst fest. Dabei gilt: Methoden, die nur Hilfsfunktionen für andere Methoden erfüllen, sollten private oder protected sein. Attribute sollten nicht public sein, um unkontrolliertes Ändern zu vermeiden und Datenkonsistenz zu gewährleisten. Für Zugriffe auf Attribute sollten set- und get-Methoden zur Verfügung gestellt werden. Beispiel: UML GiroKonto private - kontoStand: double - kontoNr: int … + getKontoStand() public + setKontoStand() … 9.1.2 Java public class GiroKonto { // Attribute private int kontoNr; private double kontoStand; . . . // Zugriffsmethoden public double getKontoStand() { return kontoStand; } public void setKontoStand(double kontoStand){ this.kontoStand = kontoStand; } . . . } Vererbung Beispiel: Die Kontoverwaltung für Girokonten soll nun auf die Verwaltung von Sparkonten und Festgeldkonten ausgeweitet werden. Das Pflichtenheft wird zu diesem Zweck um folgende Anforderungen erweitert: /1/ Zu einem Girokonto sollen neben der Kontonummer und dem aktuellen Kontostand auch ein Dispositionslimit, ein Sollzins und ein Habenzins gespeichert werden. /2/ Sparkonten werden mit einem Habenzins verzinst. Sie müssen stets eine Mindesteinlage aufweisen. /3/ Festgeldkonten haben eine bestimmte Laufzeit und einen Zinssatz. /4/ Spar- und Festgeldkonten haben eine Kündigungsfrist. /5/ Wird ein Konto angelegt, wird ihm eine Kontonummer zugeordnet. /6/ Es müssen Ein- und Auszahlungen auf den Konten vorgenommen werden können. /7/ Von einem Girokonto sollen Überweisungen an andere Konten durchgeführt werden können. /8/ Alle Daten müssen einzeln gelesen werden können. C. Endreß 2 / 12 09/2005 9. Objekte und Klassen – Teil 2 Modellierung der Konten Die drei Kontoarten zeigen ähnliche Eigenschaften und ähnliches Verhalten. GiroKonto kontoNr kontoStand habenZins sollZins dispo GiroKonto() einzahlen() auszahlen() ueberweisen() getKontoNr() getKontoStand() getHabenZins() getSollZins() getDispo() SparKonto FestGeldKonto kontoNr kontoStand habenZins mindestEinlage kontoNr kontoStand habenZins laufZeit SparKonto() einzahlen() auszahlen() kuendigen() getKontoNr() getKontoStand() getHabenZins() getMindestEinlage() FestGeldKonto() einzahlen() auszahlen() kuendigen() getKontoNr() getKontoStand() getHabenZins() getLaufzeit() Gemeinsame Eigenschaften und Methoden werden in eine Oberklasse Konto ausgelagert. Es ergibt sich ein erweitertes Klassenmodell: Generalisierung Konto Oberklasse Spezialisierung kontoNr kontoStand habenZins Konto() einzahlen() auszahlen() getKontoNr() getKontoStand() getHabenZins() Vererbung GiroKonto SparKonto FestGeldKonto sollZins dispo mindestEinlage laufZeit GiroKonto() auszahlen() ueberweisen() getSollZins() SparKonto() auszahlen() kuendigen() getMindestEinlage() FestGeldKonto() kuendigen() getLaufzeit() Unterklasse Vererbung bezeichnet die Weitergabe von Eigenschaften (Attributen) und Operationen einer Klasse an eine andere Klasse. C. Endreß 3 / 12 09/2005 9. Objekte und Klassen – Teil 2 Vererbung ist ein wichtiger Mechanismus der objektorientierten Programmierung. Sie ermöglicht das Ableiten neuer Klassen aus bestehenden Klassen, indem Attribute und Methoden an neue Klassen „vererbt“ werden. Die abgeleitete Klasse (Unterklasse, Subklasse) erbt alle Attribute und Methoden der zugeordneten Oberklasse (Basisklasse, Superklasse). Die Klasse Konto ist also die Oberklasse der Klassen GiroKonto, SparKonto und FestGeldKonto. Diese Klassen sind Unterklassen der Klasse Konto. Erhält eine Unterklasse zusätzliche Attribute und Methoden, stellt sie eine Spezialisierung der Oberklasse dar. Ein Objekt einer Unterklasse kann überall dort verwendet werden, wo ein Objekt der Oberklasse erlaubt ist. Sinnvoll ist das Konzept der Vererbung immer dann, wenn die Oberklasse eine Generalisierung der Unterklasse darstellt bzw. die Unterklasse eine Spezialisierung der Oberklasse darstellt. Vorteile der Vererbung Aufbauend auf bestehenden Klassen können mit wenig Aufwand neue Klassen erstellt werden. Die Änderbarkeit wird unterstützt. So wirkt sich beispielsweise die Änderung eines Attributs oder einer Methode automatisch auf alle Unterklassen der Klassenhierarchie aus. Das Konzept der Vererbung widerspricht allerdings dem Geheimnisprinzip (Kapselung), das besagt, dass keine Klasse die Attribute einer anderen Klasse sieht. Vererbung in Java Konstuktoren werden nicht vererbt. Konstruktoren der abgeleiteten Klassen müssen neu definiert werden! Konstruktoren der Unterklassen rufen implizit den default-Konstruktor der Oberklasse auf. Wurde kein default-Konstruktor in der Oberklasse definiert, gibt es einen Compiler-Fehler. Das ist der Fall, wenn in der Oberklasse lediglich parametrisierte Konstruktoren angegeben wurden und daher ein parameterloser default-Konstruktor nicht automatisch erzeugt wurde. Ein parametrisierter Konstruktor der Oberklasse wird mit dem Schlüsselwort super aus einem Unterklassen-Konstruktor heraus aufgerufen. super muss die erste Anweisung im Unterklassenkonstruktor sein und wird verwendet wie eine normale Methode. Es ist auch erlaubt, mit Hilfe der this-Methode einen anderen Konstruktor der eigenen Klasse aufzurufen. Modifizierer für den Zugriff auf Attribute und Methoden public Zugriff für alle Klassen, keine Einschränkungen protected Zugriff nur für die Klasse selbst und für Unterklassen private Zugriff nur für die Klasse, in der die Methode oder das Attribut deklariert ist - Zugriff für die Klasse selbst und alle Klassen des Packages In Java kann durch das Schlüsselwort final verhindert werden, dass von einer Klasse Unterklassen abgeleitet werden. Dieses ist sinnvoll, wenn verhindert werden soll, dass vererbte Operationen C. Endreß 4 / 12 09/2005 9. Objekte und Klassen – Teil 2 redefiniert werden. Beispielsweise erlauben die Klassen Math und String keine Unterklassen. Überschreiben von Methoden Methoden der Unterklasse überschreiben bzw. redefinieren gleichnamige, geerbte Methoden der Oberklasse. Im Beispiel Kontoverwaltung wird die Methode auszahlen() der Basisklasse Konto in den Unterklassen überschrieben. In der Klasse SparKonto muss nach der Auszahlung noch eine mindestEinlage auf dem Konto verbleiben. Ein GiroKonto kann dagegen bis zu einem Limit (dispo) überzogen werden. Beispiel: Das Klassenmodell der Kontoverwaltung kann in Java folgendermaßen implementiert werden: /* Programm: Kontoverwaltung Fachkonzept-Klasse Konto, Oberklasse der Fachkonzept-Klassen GiroKonto SparKonto FestGeldKonto */ 1 public class Konto { //---------- Attribute ------------protected int kontoNr; protected double kontoStand; protected double habenZins = 1.5; // gilt für alle Konten //---------- Methoden -------------// Konstruktoren public Konto(int kontoNr) { this.kontoNr = kontoNr; kontoStand = 0; } public Konto(int kontoNr, double kontoStand) { this.kontoNr = kontoNr; this.kontoStand = kontoStand; } public void einzahlen(double betrag) { if(betrag > 0.0){ kontoStand = kontoStand + betrag; } } public boolean auszahlen(double betrag) { if(kontoStand >= betrag){ kontoStand = kontoStand - betrag; return true; } else return false; } public int getKontoNr() { return kontoNr; } public double getKontoStand() { return kontoStand; } public double getHabenZins() { return habenZins; } 2 3 4 5 } C. Endreß 5 / 12 09/2005 9. Objekte und Klassen – Teil 2 Erläuterungen 1 Attribute werden als protected deklariert, damit sie auch von den Unterklassen aus sichtbar sind. 2 Deklaration von zwei überladenen Konstruktoren (unterscheiden sich in ihrer Signatur) 3 Die Methode einzahlen() wird vererbt und ist gültig für alle Konten. 4 Die Methode auszahlen() ist hier als Standardlösung implementiert. Sie wird in den Unterklassen gemäß den jeweiligen Anforderungen überschrieben. 5 Die Zugriffsmethoden sind gültig für alle Konten. /* Programm: Kontoverwaltung Fachkonzept-Klasse GiroKonto, abgeleitet von der Klasse Konto */ public class GiroKonto extends Konto { //------------- Attribute ----------------private double dispo; private double sollZins = 13.0; //gilt für alle Girokonten 1 2 //------------- Methoden -----------------// Konstruktor public GiroKonto(int kontoNr, double dispo) { super(kontoNr); this.dispo = dispo; } 3 4 public boolean auszahlen(double betrag) { if (kontoStand + dispo >= betrag) { kontoStand = kontoStand - betrag; return true; } else return false; } 5 public boolean ueberweisen(double betrag, Konto empfaenger) { if (auszahlen(betrag) == true) { empfaenger.einzahlen(betrag); return true; } else return false; } 6 public double getSollZins() { return sollZins; } public double getDispo() { return dispo; } } Erläuterungen 1 Die Klasse GiroKonto ist eine Unterklasse der Klasse Konto. Diese Vererbungsbeziehung wird durch das Schlüsselwort extends festgelegt. 2 Deklaration der Attribute, die für die Klasse GiroKonto neu hinzukommen (Spezialisierung). Das Attribut sollZins wird mit 13.0 initialisiert. C. Endreß 6 / 12 09/2005 9. Objekte und Klassen – Teil 2 3 Deklaration des Konstruktors der Klasse GiroKonto. In der Anweisung super(kontoNr) wird der Konstruktor der Oberklasse Konto aufgerufen und der Parameter kontoNr übergeben. Die superAnweisung muss die erste Anweisung eines Konstruktors sein. 4 Die Methode auszahlen() in GiroKonto überschreibt die gleichnamige Methode der Oberklasse Konto. Die Implementierung ist den Erfordernissen eines Girokontos angepasst. 5 Die Methode ueberweisen() ermöglicht die Überweisung eines Geldbetrags (betrag) auf ein Konto (empfaenger). Hier wird von der Möglichkeit Gebrauch gemacht, Objekte als Parameter zu übergeben. Das Konto-Objekt empfaenger kann dabei sowohl ein Objekt der Basisklasse Konto als auch ein Objekt der drei Unterklassen GiroKonto, SparKonto oder FestGeldKonto sein. 6 Zugriffsmethoden für die Attribute, die neu zur Klasse GiroKonto hinzugekommen sind. /* Programm : Kontoverwaltung Testrahmen für die Klasse GiroKonto */ import support.Console; public class KontoVerwaltung { public static void main(String[] args) { // Deklaration der Objekt-Variablen GiroKonto erstesKonto; GiroKonto zweitesKonto; 1 2 // zwei Girokonten anlegen erstesKonto = new GiroKonto(4711, 500); zweitesKonto = new GiroKonto(4712, 500); 3 // Einzahlung und Überweisung erstesKonto.einzahlen(1200); 4 if( erstesKonto.ueberweisen(500, zweitesKonto) ) Console.println("Überweisung ausgeführt"); else Console.println("Überweisung nicht ausgeführt"); // Ausgabe der aktuellen Kontostände Console.println("Konto " + erstesKonto.getKontoNr() + ": Kontostand = " + erstesKonto.getKontoStand()); Console.println("Konto " + zweitesKonto.getKontoNr() + ": Kontostand = " + zweitesKonto.getKontoStand()); } } Konsolenausgabe Überweisung ausgeführt Konto 4711: Kontostand = 700.0 Konto 4712: Kontostand = 500.0 Erläuterungen 1 Deklaration von zwei Referenz-Variablen der Klasse GiroKonto. 2 Erzeugung von zwei Objekten der Klasse GiroKonto durch Aufruf der entsprechenden Konstruktoren. C. Endreß 7 / 12 09/2005 9. Objekte und Klassen – Teil 2 3 Aufruf der Methode einzahlen() für das Objekt erstesKonto. 4 Aufruf der Methode ueberweisen() für das Objekt erstesKonto. Die Methode erhält als Parameter den Überweisungsbetrag und das GiroKonto-Objekt zweitesKonto, auf das der Betrag überwiesen werden soll. Der boolsche Rückgabewert der Methode wird in einer ifAnweisung ausgewertet. 9.1.3 Polymorphie Der Begriff Polymorphie kommt aus dem Griechischen und bedeutet Vielgestaltigkeit. Die gleiche Botschaft kann an Objekte unterschiedlicher Klassen gesendet werden. Die Empfängerobjekte reagieren darauf in ihrer eigenen, klassenspezifischen Weise. So löst z.B. die Botschaft auszahlen() bei Objekten der Klassen GiroKonto oder SparKonto einen Auszahlvorgang aus, der jedoch in den beiden Klassen unterschiedlich gestaltet ist. Polymorphie ist die Verfügbarkeit mehrerer Definitionen einer Methode, aus denen erst zum Zeitpunkt der Ausführung eine geeignete Definition ausgewählt wird. /* Programm: Kontoverwaltung Testrahmen für polymorphe Methode auszahlen() */ import support.Console; public class KontoVerwaltungPolymorph { static void print(Konto einKonto){ Console.println("Konto " + einKonto.getKontoNr() + ": Kontostand = " + einKonto.getKontoStand()); } public static void main(String[] args) { // Deklaration der Objekt-Variablen Konto erstesKonto; Konto zweitesKonto; 1 // Konten anlegen erstesKonto = new GiroKonto(4711, 500); zweitesKonto = new SparKonto(1234); 2 3 // Einzahlung erstesKonto.einzahlen(1200); zweitesKonto.einzahlen(2000); 4 // Aufruf einer polymorphen Methode erstesKonto.auszahlen(1500); zweitesKonto.auszahlen(2000); 5 6 // Ausgabe der aktuellen Kontostände print(erstesKonto); print(zweitesKonto); } } Konsolenausgabe Konto 4711: Kontostand = -300.0 Konto 1234: Kontostand = 2000.0 C. Endreß 8 / 12 09/2005 9. Objekte und Klassen – Teil 2 Erläuterungen 1 Deklaration von Objekt-Variablen der Klasse Konto. 2 Erzeugung eines Objekts der Klasse GiroKonto durch Aufruf des entsprechenden Konstruktors. Eine Objekt-Variable der Klasse Konto kann auch auf ein Objekt einer Unterklasse von Konto zeigen. 3 Erzeugung eines Objekts der Klasse SparKonto durch Aufruf des entsprechenden Konstruktors. Eine Objekt-Variable der Klasse Konto kann auch auf ein Objekt einer Unterklasse von Konto zeigen. 4 Aufruf der von der Oberklasse Konto geerbten Methode einzahlen(). 5 Die Methode auszahlen() ist polymorph. Sie ist in der Oberklasse Konto in einer Variante implementiert, die durch spezialisierte Definitionen in den Unterklassen GiroKonto und SparKonto redefiniert wird. Die Auszahlungsanweisung des Girokontos kann aufgrund des vorhandenen Dispolimits ausgeführt werden. Der Auszahlvorgang beim Sparkonto fordert eine verbleibende Mindesteinlage, die jedoch in Zeile 6 nicht zurückbleibt, so dass die Auszahlung nicht erfolgt. Oberklasse Konto public boolean auszahlen(double betrag) { if(kontoStand >= betrag){ kontoStand = kontoStand - betrag; return true; } else return false; } Unterklasse GiroKonto public boolean auszahlen(double betrag) { if (kontoStand + dispo >= betrag) { kontoStand = kontoStand - betrag; return true; } else return false; } Unterklasse SparKonto public boolean auszahlen(double betrag) { if(kontoStand - mindestEinlage >= betrag){ kontoStand = kontoStand - betrag; return true; } else return false; } Eine Referenz-Variable kann zur Laufzeit eines Programms auf verschiedene Objekte verweisen. Somit kann auch erst zur Laufzeit des Programms die Operation festgelegt werden, die zum jeweiligen Objekt passt. Wird bereits beim Compilieren festgelegt welche Methode aufgerufen wird, spricht man von einer statischen Bindung (early binding). Wird dagegen erst während der Laufzeit die aufzurufende Methode festgelegt, nennt man dieses dynamische Bindung (late binding). C. Endreß 9 / 12 09/2005 9. Objekte und Klassen – Teil 2 9.1.4 Abstrakte Klassen und Methoden Häufig ergibt sich bei der Modellierung von Klassen folgende Situation: Alle Unterklassen einer Basisklasse haben eine gleichnamige Methode mit unterschiedlicher Implementierung. Beispiel: auszahlen(double betrag) GiroKonto: beliebige Auszahlung bis Limit SparKonto: Mindesteinlage nötig FestGeldKonto: Auszahlung erst nach Ende der Laufzeit Lösung: abstrakte Methode in der Oberklasse. Eine abstrakte Methode ist eine Methode, die nicht realisiert ist. Die abstrakte Methode der Oberklasse gibt nur die Signatur der Methode an, nicht aber ihre Realisierung. Die Methodendeklaration verwendet das Schlüsselwort abstract. Abstrakte Methoden müssen in den Unterklassen redefiniert werden. Sobald eine Klasse eine abstrakte Methode enthält, ist die ganze Klasse abstrakt. Von einer abstrakten Klasse können keine Objekte erzeugt werden. Es können nur Objekte zu nicht abstrakten Unterklassen erzeugt werden. Abstrakte Methoden müssen in den Unterklassen implementiert werden, es sei denn, die Unterklassen sind ihrerseits wieder abstrakte Klassen. Eine abstrakte Klasse ist eine Klasse, von der keine Exemplare erzeugt werden können. Meist enthalten abstrakte Klassen abstrakte Methoden, die auf dem Abstraktionsniveau der Klassen nicht formuliert werden können und nur eine gemeinsame Schnittstelle für alle Unterklassen definieren. Abstrakte Klassen, Redefinition und Polymorphie erlauben die Konstruktion flexibler Strukturen von Klassen, in denen (abstrakte) Oberklassen Protokolle festlegen, die von allen Unterklassen eingehalten werden müssen. Dabei ist jedes Objekt Experte für sein eigenes Handeln, da es seine Methoden aus der Klasse ableitet, von der es erzeugt wurde. Beispiel: Der Oberklasse Konto werden Informationen (Attribute und Methoden) hinzugefügt. So wird die Beschreibung verfeinert (GiroKonto bzw. SparKonto) Die Klasse Konto sollte als abstrakte Klasse modelliert werden, da es in der Realität keine „allgemeinen“ Konten gibt, sondern nur Spezialisierungen wie Giro- und Sparkonten. GiroKonto und SparKonto sind konkrete Klassen, da es Objekte ihrer Art gibt. In der UML werden abstrakte Klassen und Methoden durch kursive Schrift gekennzeichnet. Alternativ können sie im Namensfeld der Klasse mit {abstract} gekennzeichnet sein. C. Endreß 10 / 12 Konto kontoNr kontoStand habenZins Konto() einzahlen() auszahlen() getKontoNr() getKontoStand() getHabenZins() 09/2005 9. Objekte und Klassen – Teil 2 Beispiel: Die abstrakte Klasse Konto wird in Java folgendermaßen implementiert: /* Programm: Kontoverwaltung Fachkonzept-Klasse Konto, abstrakte Oberklasse der Klassen GiroKonto SparKonto FestGeldKonto */ public abstract class Konto { 1 //------------ Attribute ----------------protected int kontoNr; protected double kontoStand; protected double habenZins = 1.5; //------------ Methoden -----------------// Konstruktor public Konto(int kontoNr) { this.kontoNr = kontoNr; kontoStand = 0; } public void einzahlen(double betrag) { if(betrag > 0.0) kontoStand = kontoStand + betrag; } 2 public abstract boolean auszahlen(double betrag); public int getKontoNr() { return kontoNr; } public double getKontoStand() { return kontoStand; } public double getHabenZins() { return habenZins; } } Erläuterungen 1 Durch das Schlüsselwort abstract wird die Klasse Konto als abstrakte Klasse deklariert. Damit ist diese Klasse nicht mehr instanzierbar. 2 Die Methode auszahlen() wird abstract deklariert. Ein Anweisungsblock ist damit nicht mehr möglich. Die Methode muss in den Unterklassen redefiniert werden. C. Endreß 11 / 12 09/2005 9. Objekte und Klassen – Teil 2 9.2 Zusammenfassung Klassen fassen Objekte mit gleichen Attributen und Methoden zu einer Einheit zusammen. Nach dem Geheimnisprinzip kapselt die Klasse Attribute und eventuell Methoden, die nicht nach außen sichtbar sein sollen. Auf diese als private bzw. protected deklarierten Attribute kann nur innerhalb ihrer Klasse zugegriffen werden. Für den Zugriff von außen sind public deklarierte set- und get-Methoden erforderlich. Durch die Vererbung werden Attribute und Operationen an alle Unterklassen einer Oberklasse weitergegeben. Es entsteht eine Klassenhierarchie, die je nach Blickrichtung eine Generalisierungsbzw. Spezialisierungshierarchie darstellt. Geerbte Attribute werden durch gleichnamige Attribute der Unterklasse verborgen, geerbte Methoden werden durch gleichnamige Methoden überschrieben bzw. redefiniert. In Java kann durch das Schlüsselwort super auf verborgene Attribute bzw. überschriebene Methoden zugegriffen werden. Von abstrakten Klassen können keine Objekte erzeugt werden. Abstrakte Methoden müssen in den Unterklassen definiert werden. Polymorphie erlaubt es, gleiche Botschaften an Objekte unterschiedlicher Klassen zu senden. Schreibt man eine Methode, die einen Parameter vom Typ der Oberklasse enthält, kann während der Laufzeit dort auch ein Objekt einer Unterklasse eingesetzt werden. Erst beim Aufruf der Methode steht fest, auf welches Objekt welcher Klasse die Methode angewendet werden soll. C. Endreß 12 / 12 09/2005 10. Datenstrukturen 10. Datenstrukturen Lernziele ☺ Die wichtigsten Datenstrukturen der Collections-API kennen. ☺ Die Unterschiede zwischen den Strukturen Feldern, Listen, Maps und Sets kennen und geeignete Strukturen anwenden können. ☺ Sortier- und Suchfunktionen der Collections-API kennen und anwenden. 10.1 Wenn Felder nicht genug sind In den vergangenen Kapiteln haben wir gleichartige Daten in Feldern, die wir mit Hilfe des new-Operators in einer bestimmten Größe erzeugt haben, gepackt und im Arbeitsspeicher abgelegt. Dieses Verfahren ist gut geeignet, wenn man die Anzahl der zu speichernden Elemente beim Anlegen des Feldes kennt. Ist diese Anzahl jedoch nicht bekannt oder muss die Kapazität des Feldes im Laufe der Zeit vergrößert oder verkleinert werden, erweisen sich Felder als unflexibel und problematisch. Problem: Ein Benutzer gibt eine Liste von Namen über die Tastatur ein. Anfangs weiß er noch nicht, um wie viele Namen es sich handelt. Wie groß soll das Feld für die Namensliste werden? Lösung: Anstelle eines klassischen Feldes, das nach dem Initialisieren in seiner Größe festgelegt ist, benötigen wir eine flexible Datenstruktur, die trotzdem die Funktionalität von Feldern bietet. Die Firma Sun hat zu diesem Zweck eine Vielzahl von sogenannten Collection-Klassen vordefiniert. Diese Collections sind eine Sammlung von Klassen, die eine beliebige Anzahl von Objekten speichern können. Jede dieser Klassen besitzt eine Methode toArray(), mit der man die gesammelten Objekte jederzeit in ein Feld umwandeln kann. Für die Namensliste verwenden wir die Klasse ArrayList. Diese Klasse besitzt eine Methode add, mit sich Objekte an Ende der Liste anhängen lassen, eine Methode get, mit der sich beliebige Objekte der Liste auslesen, eine Methode size, die die Anzahl der Objekte in der Liste angibt. ArrayList<String> namensListe = new ArrayList<String>(); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String name = br.readLine(); namensListe.add(name); for(int i = 0; i < namensListe.size(); i++){ System.out.println(namensListe.get(i)); } C. Endreß 1 / 11 10/2008 10. Datenstrukturen 10.2 Collections Collections haben gegenüber einfachen Feldern den Vorteil, dass die Anzahl der Elemente, die man in ihnen speichern möchte, nicht von vornherein feststehen muss. Sie sind in ihrer Größe flexibel. Collections verfügen über zusätzliche Methoden, die den Umgang mit ihnen sehr komfortabel gestalten. Spezielle Collections garantieren, dass ein bestimmtes Element nicht zweimal in der Menge aufgenommen wird. Es gibt sogenannte Iteratoren, mit denen man sich auf sehr bequeme Weise durch die Werte in einer Collections arbeiten kann. 10.2.1 Drei wichtige Strukturen: List, Set, Map In der Collections-API finden wir die drei grundlegenden Interfaces List, Set und Map. Eine ArrayList ist z.B. eine List. LIST (wenn es auf die Reihenfolge ankommt) Collections, die die Indexposition kennen. Lists wissen, wo etwas in der Liste steht. Dasselbe Objekt kann durch mehrere Elemente referenziert werden. 0 1 2 3 List SET (wenn es auf die Einzigartigkeit ankommt) Collections, die nichts mehrfach enthalten dürfen. Sets wissen, ob etwas schon in der Sammlung vorkommt. Es kann nie mehr als ein Element dasselbe Objekt referenzieren, und es können auch nicht zwei Objekte referenziert werden, die als gleich gelten. Set C. Endreß 2 / 11 10/2008 10. Datenstrukturen MAP (wenn etwas anhand eines Schlüssels gefunden werden soll) Collections, die Schlüssel/Wert-Paare verwenden. Maps wissen, welcher Wert mit einem bestimmten Schlüssel verknüpft ist. Es können zwei verschiedene Schlüssel vorkommen, die denselben Wert referenzieren. Die Schlüssel selbst können nicht mehrmals vorkommen. Normalerweise werden als Schlüssel StringNamen benutzt, so dass Sie beispielsweise Name/WertEigenschaftslisten erstellen können. Ein Schlüssel darf jedoch jedes beliebige Objekt sein. 10.2.2 “Ball1“ “Ball2“ “Fisch“ “Auto“ Map Auszug aus der Collcetions-API «interface» Collection «interface» Set «interface» List «interface» SortedSet TreeSet LinkedHashSet HashSet ArrayList LinkedList Vector «interface» Map «interface» SortedMap TreeMap C. Endreß HashMap LinkedHashMap 3 / 11 Hashtable 10/2008 10. Datenstrukturen 10.3 ArrayList Eine ArrayList ist eine Liste von Elementen, die man im Prinzip wie ein Feld handhaben kann. Nur die Syntax sieht etwas anders aus. Einige wichtige Methoden der Klasse ArrayList: ArrayList add(Object elem) Fügt der Liste das angegebene Objekt elem hinzu. get(int index) Liefert das Objekt zurück, das sich aktuell an der Position index befindet. size() Liefert die Anzahl der Elemente zurück, die sich aktuell in der Liste befinden. remove(int index) Entfernt das Objekt an der Position index aus der Liste. remove(Object elem) Entfernt das Objekt elem, wenn es sich in der Liste befindet. contains(Object elem) Liefert true zurück, wenn das Objekt in der Liste vorhanden ist, sonst false. isEmpty() Liefert true zurück, wenn die Liste keine Elemente enthält, sonst false. indexOf(Object elem) Liefert entweder den Index für das übergebene Objekt oder -1 zurück. Bei einer ArrayList arbeiten Sie mit einem Objekt der Klasse ArrayList. Sie rufen die Methoden des ArrayList-Objekts wie bei anderen Objekten auch mit dem Punktoperator auf. ArrayLists sind genau wie Felder nullbasiert, d.h. das erste Element befindet sich an der Position 0 und das letzte Element an der Position size() – 1. C. Endreß 4 / 11 10/2008 10. Datenstrukturen Ein paar Dinge, die Sie mit einer ArrayList machen können: Eine erstellen ArrayList<Ball> meineListe = new ArrayList<Ball>(); Etwas reinstecken Ball b = new Ball(); meineListe.add(b); b Noch etwas reinstecken Ball x = new Ball(); meineListe.add(x); b x Ermitteln, wie viele Dinge in der Liste drin sind int anzahlDerBälle = meineListe.size(); Herausfinden, ob sie etwas enthält boolean istDrin = meineListe.contains(x); Herausfinden, wo sich etwas befindet int index = meineListe.indexOf(x); Ermitteln, ob sie leer ist boolean istLeer = meineListe.isEmpty(); Etwas daraus entfernen meineListe.remove(b); C. Endreß x 5 / 11 10/2008 10. Datenstrukturen Ein Vergleich zwischen ArrayList und einem gewöhnlichen Feld (Array) 1 Ein einfaches Feld muss seine Größe bereits kennen, wenn es erzeugt wird. Bei einer ArrayList erzeugen Sie einfach ein Objekt der Klasse ArrayList. Die ArrayList muss nie wissen, wie groß sie sein soll, weil sie automatisch wächst und schrumpft, wenn hinzugefügt bzw. entfernt werden. String[] einFeld = new String[5]; ArrayList<String> eineListe = new ArrayList<String>(); 2 Wenn Sie ein Objekt in ein gewöhnliches Feld stecken möchten, müssen Sie es einem bestimmten Ort zuweisen. Der Positionsindex muss zwischen 0 und der Feldlänge minus 1 liegen. einFeld[4] = ″Hallo″; Wenn der Index außerhalb der Arraygrenzen liegt, gibt es zur Laufzeit eine Exception der Art ArrayIndexOutOfBoundsException. Bei einer ArrayList brauchen Sie sich um Feldgrenzen keine Gedanken zu machen. Verwenden Sie einfach die add()-Methode, um ein neues Element der Liste hinzuzufügen. eineListe.add(″Hallo″); 3 Felder verwenden in Java die Array-Syntax, die in Java an keiner anderen Stelle verwendet wird. ArrayLists sind ganz gewöhnliche Java-Objekte. Es gibt also keine besondere Syntax. einFeld[2] 4 ArrayLists sind seit Java 5.0 parametrisiert. Vor Java 5.0 gab es keine Möglichkeit, den Typ der Dinge zu deklarieren, die in einer ArrayList gespeichert werden. ArrayLists waren als heterogene Sammlungen von Objekten. Aber seit Java 5.0 können wir den Typ der Objekte, die in der ArrayList abgelegt werden, festlegen. ArrayList<String> C. Endreß 6 / 11 10/2008 10. Datenstrukturen 10.4 Höhere Typsicherheit durch Generics Bevor es Generics gab, d.h. vor Java 5.0, scherte sich der Compiler nicht darum, was in eine Collection gesteckt wurde. Alle Collection-Implementierungen waren so deklariert, dass sie den Typ Object enthielten. Man konnte einfach alles in einer ArrayList ablegen. Der Compiler hinderte Sie nicht daran, einen Hund in eine Liste von Enten zu stecken. Ohne Generics Objekte werden als Fußball-, Hund-, Fisch- oder AutoReferenzen hineingesteckt … Bevor es Generics gab, hatte man keine Möglichkeit, den Typ einer ArrayList zu deklarieren, daher wurden ihrer Methode add() Argumente vom Typ Object übergeben. ArrayList Object Object Object Object … und kommen als Referenz vom Typ Object wieder heraus. Mit Generics Objekte werden ausschließlich als Referenzen auf FischObjekte hineingesteckt … Mit Generics können Sie nur Fisch-Objekte in die ArrayList<Fisch> stecken. Deshalb kommen die Objekte auch als FischReferenzen wieder heraus. Sie brauchen sich keine Sorgen zu machen, dass irgendjemand einen Volkswagen in die ArrayList hineinsteckt oder dass das, was Sie herausholen, vielleicht nicht auf eine Fisch-Referenz gecastet werden kann. ArrayList <Fisch> … und kommen als Referenz vom Typ Fisch wieder heraus. C. Endreß 7 / 11 10/2008 10. Datenstrukturen Mit Generics können Sie typsichere Collections erzeugen, wodurch einige Probleme schon zur Kompilierzeit abgefangen werden und nicht erst zur Laufzeit auftreten. Ohne Generics durften Sie mit Genehmigung des Compilers einen Kürbis in eine ArrayList stecken, die eigentlich nur Objekte vom Typ Katze enthalten sollte. 10.4.1 Verwendung von generischen Klassen Da ArrayList unsere meistgenutzte generische Klasse ist, werfen wir einen Blick in die ArrayListDokumentation der Java API. Für andere Collection-Klassen gilt dasselbe. Die beiden wichtigen Stellen, die man sich bei einer generischen Klasse ansehen muss, sind: 1. die Deklaration der Klasse und 2. die Deklaration der Methoden, mit denen Sie Elemente hinzufügen können. Die ArrayList-Dokumentation der Java-API verstehen (Oder: Was sich wirklich hinter <E> verbirgt) public class ArrayList<E> extends AbstractList<E> implements List<E> ...{ public boolean add(E e) // mehr Code } C. Endreß 8 / 11 10/2008 10. Datenstrukturen Verwendung von Typparametern bei ArrayList Dieser Code ArrayList<Auto> meinFuhrpark = new ArrayList<Auto>() bedeutet, dass ArrayList public class ArrayList<E> extends AbstractList<E> ... { public boolean add(E e) // mehr Code } vom Compiler behandelt wird als public class ArrayList<Auto> extends AbstractList<Auto> ... { public boolean add(Auto e) // mehr Code } Mit anderen Worten, das „E“ wird durch den tatsächlichen Typ (der auch als Typparameter bezeichnet wird) ersetzt, den Sie bei der Erzeugung der ArrayList benutzen. Deshalb lässt Sie die ArrayList-Methode add() nichts anderes hinzufügen als Objekte eines Referenztyps, der mit dem Typ von „E“ kompatibel ist. Wenn Sie also eine ArrayList<Auto> erzeugen, wird aus der Methode add() plötzlich eine Methode add(Auto e). C. Endreß 9 / 11 10/2008 10. Datenstrukturen 10.4.2 Verwendung von generischen Methoden Bei einer generischen Klasse enthält die Klassendeklaration einen Typparameter. Bei einer generischen Methode wird bei der Deklaration ein Typparameter in der Signatur angegeben. 1 Verwendung eines in der Klassendeklaration definierten Typparameters public class ArrayList<E> extends AbstractList<E> ... { } public boolean add(E e) ... Wenn Sie einen Typparameter für die Klasse deklarieren, können Sie diesen Typ einfach überall dort einsetzen, wo Sie als Typ eine wirkliche Klasse oder ein wirkliches Interface benutzt hätten. Der im Methodenargument deklarierte Typ wird dann letztendlich durch den Typ ersetzt, den Sie beim Instanzieren der Klasse verwenden. 2 Verwendung eines NICHT in der Klassendeklaration definierten Typparameters public <T extends Haustier> void etwasMachen(ArrayList<T> liste) Wenn die Klasse selbst keiner Typparameter verwendet, können Sie trotzdem noch einen für eine Methode angeben, indem Sie ihn an einer sehr vor dem ungewöhnlichen Stelle deklarieren: Rückgabetyp. In dieser Methode kann „T“ beispielsweise „jeder Typ von Haustier“ sein. Wenn Sie eine Liste von HaustierObjekten etwas machen soll, warum sagen Sie das nicht einfach? Warum schreiben Sie nicht einfach etwasMachen(ArrayList<Haustier> liste) Die Antwort ist einfach . . . C. Endreß 10 / 11 10/2008 10. Datenstrukturen Das hier public <T extends Haustier> void etwasMachen(ArrayList<T> liste) ist nicht dasselbe wie public void etwasMachen(ArrayList<Haustier> liste) Beide Formulierungen sind zulässig, aber sie haben unterschiedliche Bedeutungen. Die erste Variante mit <T extends Haustier> bedeutet, dass jede ArrayList zulässig ist, die mit der Klasse Haustier oder einer Unterklasse von Haustier wie z.B. Hund oder Katze deklariert wurde. Deshalb können Sie die obere Methode mit ArrayList<Hund> oder ArrayList<Katze> oder ArrayList<Haustier> aufrufen. Die zweite Variante mit ArrayList<Haustier> bedeutet, dass ausschließlich eine ArrayList<Haustier> als Parameter zulässig ist. Eine ArrayList<Hund> oder eine ArrayList<Katze> dürfen nicht an die Methode übergeben werden, auch wenn die Klassen Hund und Katze Unterklassen von Haustier sind. Die ArrayList<Haustier> darf allerdings Objekte der Haustier-Unterklassen wie z.B. Hund und Katze enthalten. Jedes Element der ArrayList<Haustier> wird aber als Haustier betrachtet, d.h. es können für diese Elemente nur Methoden der Basisklasse Haustier aufgerufen werden. Um spezielle Hund- oder Katze-Methoden zu verwenden, müssen Sie die Elemente in die jeweiligen Unterklassen casten. 10.5 Weitere Collections im Überblick TreeSet Bewahrt die Elemente in sortierter Reihenfolge auf und verhindert ein doppeltes Vorkommen. HashMap Ermöglicht die Speicherung und den Zugriff auf Elemente in Form von Name/Wert-Paaren. LinkedList Für eine schnellere Verarbeitung beim Einfügen oder Löschen von Elementen in der Mitte einer Collection. (In der Praxis werden Sie aber auch hierfür in der Regel eine ArrayList einsetzen.) HashSet Verhindert doppelte Vorkommen von Elementen in der Collection und kann ein gegebenes Element in der Collection schnell finden. LinkedHashMap Wie eine normale HashMap, kann sich jedoch die Reihenfolge merken, in der Elemente (Name/WertPaar) eingefügt wurden, oder kann so konfiguriert werden, dass sie sich die Reihenfolge merkt, in der zuletzt auf die Elemente zugegriffen wurde. C. Endreß 11 / 11 10/2008 11. Objekte und Klassen – Assoziationen 11. Objekte und Klassen – Assoziationen Lernziele ☺ Erklären können, was eine Assoziation ist. ☺ Erklären können, was Aggregation und Komposition sind. ☺ UML-Notation für Assoziationen anwenden können. ☺ Assoziationen in Java realisieren können. 11.1 Zuerst die Theorie: Assoziationen Eine Assoziation modelliert Verbindungen zwischen Objekten einer oder mehrerer Klassen. Sie ist auch zwischen Objekten derselben Klasse zulässig. Eine Assoziation beschreibt stets Beziehungen zwischen Objekten, nicht zwischen Klassen. Es ist jedoch üblich, von einer Assoziation zwischen Klassen zu sprechen, obwohl streng genommen die Objekte dieser Klassen gemeint sind. Eine reflexive Assoziation besteht zwischen Objekten derselben Klasse. Beispiel: Kontoverwaltung Wir betrachten die Kontoverwaltung einer Bank. Al Capone eröffnet am 04.04.2004 ein Geschäftskonto mit der Kontonummer 4711. Er wird dadurch zum Kunden der Bank. Einen Monat später eröffnet er bei der gleichen Bank ein privates Konto, das die Kontonummer 9876 erhält. Es besteht eine Verbindung zwischen dem Kontobesitzer Al Capone und den Konten mit den Nummer 4711 und 9876. Assoziationen zwischen Kunde und Konto: Objektdiagramm :Kunde :Konto Name = Al Capone Kontonr = 4711 Art = Geschäft Eröffnung = 04.04.04 :Konto Kontonr = 9876 Art = Privat Eröffnung = 10.05.04 Klassendiagramm Kunde Name C. Endreß 1 * besitzt 1 / 16 Konto Kontonr Art Eröffnung 10/2008 11. Objekte und Klassen – Assoziationen Für die Klassen Kunde und Konto gilt im betrachteten Modell: Jeder Kunde kann mehrere Konten besitzen. Jedes Konto gehört zu genau einem Kunden. Die Menge aller Verbindungen bezeichnet man als Assoziationen zwischen den Objekten der Klassen Kunde und Konto. Assoziationen sind in der Systemanalyse bidirektional. (D.h. der Kunde kennt seine Konten und jedes Konto kennt seinen Kunden.) Assoziationen werden durch eine Linie zwischen Klassen beschrieben. An jedem Ende der Linie muss die Wertigkeit bzw. Multiplizität angegeben werden. Die Multiplizität spezifiziert wie viele Objekte ein bestimmtes Objekt kennen kann. Man unterscheidet Kann- und Muss-Assoziationen. Eine Kann-Beziehung hat als Untergrenze die Kardinalität 0. Eine Muss-Beziehung hat als Untergrenze die Kardinalität 1 oder größer. Darstellung von Assoziationen und Multiplizitäten in UML-Notation: Klasse A Assoziationsname Rolle A Anzahl der Assoziationen: Klasse A Klasse A Klasse A Klasse A Klasse A Klasse A Rolle B Assoziationsname Klasse B Zwischen einem beliebigen Objekt und einem Objekt der Klasse A gibt es: 1 genau eine Beziehung (Muss-Beziehung) * viele Beziehungen (Null, eine oder mehrere) (Kann-Beziehung) 0..1 Null oder eine Beziehung (Kann-Beziehung) 1..* eine oder mehrere Beziehungen (Muss-Beziehung) 1, 3, 5 eine, drei oder fünf Beziehungen (Muss-Beziehung) 0..5 null bis fünf Beziehungen (Kann-Beziehung) Bezogen auf das obige Beispiel ergibt sich folgende Interpretation: Die Kann-Beziehung von Kunde zu Konto (*) bedeutet, dass es Bankkunden geben kann, die kein Konto besitzen. Die Muss-Beziehung von Konto zu Kunde (1) bedeutet, dass ein Konto nicht auf mehrere Namen laufen kann. Ein neues Konto darf nur für einen existierenden Kunden eingerichtet werden. muss Kunde kann 1 * besitzt Jedes Konto muss zu genau einem Kunden gehören C. Endreß Konto Jeder Kunde kann Null, ein oder mehrere Konten besitzen 2 / 16 10/2008 11. Objekte und Klassen – Assoziationen Wird dagegen auch die Assoziation von Kunde zu Konto als Muss-Assoziation (1..*) modelliert, so darf es keine Kunden geben, die kein Konto besitzen. Wird das letzte Konto eines Kunden gelöscht, muss auch der Kunde gelöscht werden. Wird ein Kunde im System gelöscht, werden auch alle seine Konten gelöscht. muss muss 1 Kunde 1..* besitzt Konto Assoziationen können benannt werden. Der Name beschreibt i.a. nur eine Richtung. Der Rollenname beschreibt die Bedeutung der Klasse in der Assoziation. Er wird jeweils an ein Ende der Assoziation geschrieben. Die geschickte Wahl der Rollennamen kann zur Verständlichkeit des Modells mehr beitragen als der Name der Assoziation. Beispiel: Der Bankkunde kann zum einen als Kontoinhaber auftreten (Muss-Beziehung von Konto zu Kunde). Er kann aber auch als Kontoberechtigter auftreten (Kann-Beziehung von Konto zu Kunde). Kunde 1 Name Konto 1..* Kontonr Art 1..* Eröffnung Kontoinhaber * Kontoberechtigter Abgeleitete Assoziation Eine Assoziation heißt abgeleitet (derived association), wenn die gleichen Abhängigkeiten bereits durch andere Assoziationen beschrieben werden. Sie fügt keine neuen Informationen zum Modell hinzu und ist daher redundant. Eine abgeleitete Assoziation wird durch das Präfix „/“ vor dem Assoziationsnamen oder Rollennamen gekennzeichnet. Wie das folgende Klassendiagramm zeigt, gibt es einen „direkten Weg“ von Professor zu Studenten und einen „Umweg“ über die Vorlesung. Professor liest 1 * Vorlesung * hört * * / ist Hörer von C. Endreß 3 / 16 * Student 10/2008 11. Objekte und Klassen – Assoziationen 11.2 ... dann die Praxis: Assoziationen in Java Die Programmierung einer Assoziation hängt davon ab, ob die Obergrenze der Multiplizitäten maximal 1 oder größer 1 ist, die Assoziation bidirektional oder unidirektional ist. Unidirektionale Verbindungen bestehen nur in einer Richtung. Beispielsweise kennt ein Objekt der Klasse Kunde seine Konto-Objekte. Ein Konto-Objekt kennt aber nicht das Kunden-Objekt, mit dem es verbunden ist. Unidirektionale Assoziationen werden in der UML durch eine Pfeilspitze gekennzeichnet. Bei einer bidirektionalen Assoziation kennen sich alle verbundenen Objekte gegenseitig. Dies ist bei Fachkonzept-Klassen (auf Analyseebene) in der Regel der Fall. Bei einer unidirektionalen Assoziation muss nur eine Verbindungsrichtung verwaltet werden, während bei der bidirektionalen Assoziation beide Verbindungsrichtungen verwaltet werden müssen. Übliche Operationen zum Verwalten von Verbindungen zu Objekten einer Klasse XY: 11.2.1 setXY Anlegen einer Verbindung getXY Aufrufen einer Verbindung removeXY Löschen einer Verbindung Unidirektionale Assoziationen Beispiel: Kontoverwaltung Ein Bankkunde besitzt ein Konto. Das Klassendiagramm zeigt eine unidirektionale 1:1-Assoziation. D.h. der Kunde kennt in seiner Rolle als Kontobesitzer sein Konto, das Konto kennt seinen Besitzer jedoch nicht. Kunde - name : String - vorName : String Konto 1 besitzt 1 Kontobesitzer + Kunde( name : String, vorName : String ) - kontoNr : int - kontoStand : double = 0 + Konto( kontoNr : int ) + einzahlen( betrag : double ) : void + auszahlen( betrag : double) : boolean Es ergibt sich die folgende Realisierung in Java-Quellcode: /* Beispiel: Kontoverwaltung Klasse: Kunde Demonstriert die Handhabung einer unidirektionalen 1:1-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ public class Kunde { // Attribute private String name; private String vorName; 1 // Verbindungsvariable zu einen Objekt der Klasse Konto private Konto meinKonto; // Konstruktor public Kunde(String name, String vorName) { C. Endreß 4 / 16 10/2008 11. Objekte und Klassen – Assoziationen this.name = name; this.vorName = vorName; this.meinKonto = null; 2 } // Verwaltungsmethoden für die Assoziation // Verbindung vom Kunden zum Konto-Objekt aufbauen public void setKonto(Konto einKonto) { this.meinKonto = einKonto; } 3 // Aufrufen der Verbindung für den Zugriff auf das Konto public Konto getKonto() { return meinKonto; } 4 // Löschen der Verbindung zum Konto public void removeKonto(){ meinKonto = null; } 5 // Get-Methoden für die Attribute public String getName() { return name; } public String getVorName() { return vorName; } } Erläuterungen 1 Das Attribut meinKonto ist eine Referenzvariable der Klasse Konto. Mit dieser Referenzvariable wird die Verbindung zwischen einem Kunden-Objekt und einem Konto-Objekt hergestellt. Java weist Attributen, die nicht explizit im Quellcode mit einem Initialwert belegt werden, defaultWerte zu. Für Referenzvariablen ist der default-Wert null. :Kunde name vorName meinKonto = null Verbindungsvariable 2 Im Konstruktor des Kunden wird die Verbindungsvariable meinKonto zunächst noch einmal bewusst mit null initialisiert. Es besteht noch keine Verbindung des Kunden zu einem Konto. Das Anlegen der Verbindung zu einem Konto-Objekt wird mit der Operation setKonto() hergestellt. 3 Die Methode setKonto() stellt die Assoziation zwischen dem Objekt der Klasse Kunde und einem Konto-Objekt her. Dazu wird der Methode setKonto() das Konto-Objekt übergeben, zu dem eine Verbindung aufgebaut werden soll. Das Attribut meinKonto des Kunden-Objekts wird in setKonto() mit der Referenz auf den übergebenen Parameter einKonto initialisiert. Damit wird die unidirektionale Verbindung zwischen einem Kunden und dem zugehörigen Konto erstellt. 4 Die Methode getKonto() ruft die Verbindung zum assoziierten Konto-Objekt auf, indem die Verbindungsvariable meinKonto zurückgegeben wird. C. Endreß 5 / 16 10/2008 11. Objekte und Klassen – Assoziationen Die Methode removeKonto() löscht die Verbindung zum Konto, indem das Attribut meinKonto, das auf das Konto-Objekt zeigt, auf null gesetzt wird. Das dereferenzierte Konto-Objekt wird dann vom Garbage Collector aus dem Programmspeicher gelöscht. 5 Die Assoziation zwischen Kunde und Konto ist unidirektional. D.h. der Kunde kennt in seiner Rolle als Kontobesitzer sein Konto, das Konto kennt seinen Besitzer jedoch nicht. Die Klasse Konto enthält kein Referenz-Attribut auf ein Kunden-Objekt. /* Beispiel: Kontoverwaltung Klasse: Konto Demonstriert die Handhabung einer unidirektionalen 1:1-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ public class Konto { // Attribute private int kontoNr; private double kontoStand; // Konstruktor public Konto(int kontoNr) { this.kontoNr = kontoNr; kontoStand = 0; } // Methoden public void einzahlen(double betrag) { if (betrag > 0) { kontoStand += betrag; } } public boolean auszahlen(double betrag) { if (kontoStand - betrag >= 0) { kontoStand -= betrag; return true; } else return false; } public int getKontoNr(){ return kontoNr; } public double getKontoStand(){ return kontoStand; } } In der folgenden Anwendung wird ein Kunde angelegt, für den anschließend ein Konto eröffnet wird. /* Beispiel: Kontoverwaltung Klasse: Kontoverwaltung Demonstriert die Handhabung einer unidirektionalen 1:1-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ import support.Console; public class Kontoverwaltung { public static void main(String[] args) { 1 C. Endreß Kunde einKunde = new Kunde("Capone", "Al"); 6 / 16 10/2008 11. Objekte und Klassen – Assoziationen 2 // Konto erzeugen und Assoziation zum Kontobesitzer herstellen einKunde.setKonto(new Konto(4711)); 3 // Zugriff auf das Konto-Objekt über die Assoziation einKunde.getKonto().einzahlen(2000); 4 Console.println("Kontobesitzer: " + einKunde.getName() + ", " + einKunde.getVorName()); Console.println("Kontonummer: " + einKunde.getKonto().getKontoNr()); Console.println("Kontostand : " + einKunde.getKonto().getKontoStand() + " €"); // Konto löschen einKunde.removeKonto(); 5 } } Konsolenausgabe Kontobesitzer: Capone, Al Kontonummer: 4711 Kontostand : 2000.0 € Erläuterungen 1 Anlegen des Kunden-Objekts einKunde. 2 Mit dem new-Operator wird ein anonymes Konto-Objekt erzeugt, das mit der Kontonummer 4711 initialisiert und als Parameter an die Instanz-Methode setKonto() des Objekts einKunde übergeben wird. Wie bereits oben erläutert, stellt die Methode setKonto() die Verbindung zwischen dem Objekt des Kontobesitzers und dem Konto-Objekt her, indem die Verbindungsvariable meinKonto mit dem entsprechenden Konto-Objekt initialisiert wird. einKunde :Konto name = Capone vorName = Al kontoNr = 4711 kontoStand = 0 meinKonto = Verbindungsvariable 3 Das Kunden-Objekt einKunde ruft über die Methode getKonto() die Verbindung zum assoziierten Konto-Objekt auf und kann dadurch auf die Instanz-Methoden des Kontos zugreifen. Mit der Methode einzahlen() wird ein Betrag von 2000 € auf das Konto eingezahlt. 4 Ausgeben der Attribute des Objekts einKunde und im Folgenden der Attribute des assoziierten Konto-Objekts. 5 Die Methode einKunde.removeKonto() löscht die Assoziation zwischen dem Objekt einKunde und seinem Konto-Objekt, indem die Verbindungsvariable meinKonto wieder auf null gesetzt wird. Damit ist das Konto-Objekt nicht mehr referenziert und wird vom Garbage Collector der virtuellen Maschine aus dem Programmspeicher gelöscht. Bei unidirektionalen Assoziationen wie im gezeigten Fall enthält nur eine Klasse Referenz-Attribute und Verwaltungsoperationen für die Verbindung. C. Endreß 7 / 16 10/2008 11. Objekte und Klassen – Assoziationen 11.2.2 Bidirektionale Assoziationen Bei bidirektionalen Assoziationen sind in beiden beteiligten Klassen Referenz-Attribute und Verwaltungsoperationen für die Verbindungen vorzusehen. Beispiel: Kontoverwaltung a) 1:1-Assoziation Wir erweitern das vorangegangene Beispiel zu einer bidirektionalen Verbindung zwischen Kunde und Konto. Der Kontobesitzer kennt sein Konto. Außerdem kennt nun auch das Konto seinen Kontobesitzer. Das Klassendiagramm zeigt jetzt eine bidirektionale 1:1-Assoziation. Kunde - name : String - vorName : String Konto 1 besitzt Kontobesitzer + Kunde( name : String, vorName : String ) 1 - kontoNr : int - kontoStand : double = 0 + Konto( kontoNr : int ) + einzahlen( betrag : double ) : void + auszahlen( betrag : double) : boolean Die Klasse Kunde kann aus dem letzten Beispiel übernommen werden, da sich für den Kunden nichts ändert. Die Klasse Konto muss die Verbindung zum Kunden erweitert werden: /* Beispiel: Kontoverwaltung Klasse: Konto Demonstriert die Handhabung einer bidirektionalen 1:1-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ public class Konto { // Attribute private int kontoNr; private double kontoStand; 1 2 3 4 5 C. Endreß // Verbindungsvariable für die Assoziation zu einem Kunden private Kunde kontoBesitzer; // Konstruktor public Konto(int kontoNr, Kunde kontoBesitzer) { this.kontoNr = kontoNr; this.kontoStand = 0; this.setKontoBesitzer(kontoBesitzer); } // Verwaltungsmethoden für die Assoziation // Verbindungen vom Konto zum Kontobesitzer aufbauen public void setKontoBesitzer(Kunde kontoBesitzer){ this.kontoBesitzer = kontoBesitzer; this.kontoBesitzer.setKonto(this); } // Verbindung zum Kontobesitzer aufrufen public Kunde getKontoBesitzer(){ return kontoBesitzer; } // Verbindungen zwischen Konto und Kontobesitzer löschen public void removeKontoBesitzer(){ 8 / 16 10/2008 11. Objekte und Klassen – Assoziationen this.kontoBesitzer.removeKonto(); this.kontoBesitzer = null; } // Get-Methoden public int getKontoNr(){ return kontoNr; } public double getKontoStand(){ return kontoStand; } // Instanzmethoden public void einzahlen(double betrag) { if (betrag > 0) { kontoStand += betrag; } } public boolean auszahlen(double betrag) { if (kontoStand - betrag >= 0) { kontoStand -= betrag; return true; } else return false; } } Erläuterungen 1 Deklaration der Verbindungsvariable kontoBesitzer: Die Verbindungsvariable kontoBesitzer soll eine Assoziation zum Kontobesitzer, einem Objekt der Klasse Kunde, realisieren. Die Variable muss deshalb von der Klasse Kunde sein. 2 Dem Konstruktor des Konto-Objekts wird der Kontobesitzer als Parameter übergeben. Damit kann sofort bei der Erzeugung des Konto-Objekts die Verbindung zum Kontobesitzer hergestellt werden, indem die Verbindungsvariable kontoBesitzer an die Methode setKontoBesitzer() übergebenen wird. 3 Die Methode setKontoBesitzer() baut die bidirektionale Verbindung zwischen dem KundenObjekt und dem Konto-Objekt auf. Zuerst wird die Referenzvariable this.kontoBesitzer mit dem übergebenen Kunden-Objekt initialisiert, so dass das Konto nun über einen Link zum Kontobesitzer verfügt. Anschließend wird mit diesem Link mit der Botschaft setKonto() an den Kontobesitzer die Verbindung vom Kunden zum Konto zu initialisiert. Als Parameter übergibt sich das Konto selbst: setKonto(this). 2. Rückverbindung vom Kunden zum Konto setzen: this.kontoBesitzer.setKonto(this) Kunde Konto name vorName kontoNr kontoStand meinKonto kontoBesitzer 1. Verbindung vom Konto zum Kunden setzen: this.kontoBesitzer = kontoBesitzer C. Endreß 9 / 16 10/2008 11. Objekte und Klassen – Assoziationen 4 getKontoBesitzer() ruft die Verbindung zum Kontobesitzer auf, so dass das Konto auf seinen Besitzer zugreifen kann. 5 Die beidseitige Verbindung zwischen dem Konto und dem Kontobesitzer wird in der Methode removeKontoBesitzer() gelöscht, indem zuerst die Verbindung vom Kontobesitzer zum und dann die Konto ausgelöst wird (this.kontoBesitzer.removeKonto()) Verbindungsvariable this.kontoBesitzer wieder auf null gesetzt wird. /* Beispiel: Kontoverwaltung Klasse: Kontoverwaltung Demonstriert die Handhabung einer bidirektionalen 1:1-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ import support.Console; public class Kontoverwaltung { public static void main(String[] args) { 1 // Ein Kunden-Objekt anlegen Kunde einKunde = new Kunde("Capone", "Al"); 2 // Konto-Objekt erzeugen und den Kontobesitzer übergeben Konto einKonto = new Konto(4711, einKunde); 3 // Zugriff auf das Konto-Objekt über den Kontobesitzer einKunde.getKonto().einzahlen(2000); Console.println("Assoziation Kunde -> Konto: "); Console.println("Kontobesitzer: " + einKunde.getName() + ", " + einKunde.getVorName()); Console.println("Kontonummer: " + einKunde.getKonto().getKontoNr()); Console.println("Kontostand : " + einKunde.getKonto().getKontoStand() + " €"); // Zugriff auf den Kontobesitzer über das Konto-Objekt Console.println("\nAssoziation Konto -> Kunde: "); Console.println("Konto: " + einKonto.getKontoNr()); Console.println("Kontobesitzer: " + einKonto.getKontoBesitzer().getName()+ ", " + einKonto.getKontoBesitzer().getVorName()); 4 } } Konsolenausgabe Assoziation Kunde -> Konto: Kontobesitzer: Capone, Al Kontonummer: 4711 Kontostand : 2000.0 € Assoziation Konto -> Kunde: Konto: 4711 Kontobesitzer: Capone, Al C. Endreß 10 / 16 10/2008 11. Objekte und Klassen – Assoziationen Erläuterungen 1 Anlegen des Kunden-Objekts einKunde. 2 Anlegen des Objekts einKonto: Dem Konstruktor der Klasse Konto werden die Kontonummer (4711) und ein Objekt der Klasse Kunde (einKunde) übergeben. Im Konstruktor von Konto wird dann das Attribut kontoNr mit 4711 initialisiert. Außerdem wird im Konto-Konstruktor die Verbindung des neuen Konto-Objekts einKonto zum Kunden-Objekt einKunde erzeugt und anschließend sofort die Rückverbindung vom Kunden-Objekt einKunde zum Konto-Objekt einKonto hergestellt (siehe oben Erläuterungen zur Klasse Konto). 2. Rückverbindung vom Kunden zum Konto: this.kontoBesitzer.setKonto(this) einKunde:Kunde einKonto:Konto name = Capone vorName = Al kontoNr = 4711 kontoStand = 0 meinKonto = einKonto kontoBesitzer = einKunde 1. Verbindung von Konto zum Kunden: this.kontoBesitzer = kontoBesitzer 3 Die Klasse Kunde stellt die Methode getKonto() bereit, um die Verbindung zum assoziierten Konto-Objekt aufzurufen. Mit Hilfe von einKunde.getKonto() kann dann auf die verschiedenen Instanz-Methoden des verbundenen Kontos zugegriffen werden. 4 In der umgekehrten Richtung ruft einKonto.getKunde() die Verbindung vom Objekt einKonto zum assoziierten Kunden-Objekt auf. b) 1:*-Assoziation Wir gehen noch einen Schritt weiter und erweitern das vorangegangene Beispiel zu einer bidirektionalen 1:*Assoziation zwischen Kunde und Konto. Ein Kontobesitzer kann beliebig viele Konten besitzen. Jedes Konto hat nur einen Besitzer und kennt diesen. Klassendiagramm Kunde - name : String - vorName : String Konto 1 besitzt Kontobesitzer + Kunde( name : String, vorName : String ) 0..* - kontoNr : int - kontoStand : double = 0 + Konto( kontoNr : int ) + einzahlen( betrag : double ) : void + auszahlen( betrag : double) : boolean Die folgenden Auszüge aus dem Java-Quellcode enthalten den Programmcode, der für die 1:*-Assoziation relevant ist. Anstelle einer einzelnen Referenzvariable vom Typ Konto benötigt der Kunde nun eine Möglichkeit, beliebig viele Referenzvariablen auf Konto-Objekte zu verwalten. Dazu kann man auf die Collections von Java zurückgreifen und z.B. ein Objekt der Klasse ArrayList verwenden. C. Endreß 11 / 16 10/2008 11. Objekte und Klassen – Assoziationen /* Programm: Kontoverwaltung Klasse: KontoVerwaltung.java Demonstriert die Handhabung einer bidirektionalen 1:*-Assoziation zwischen der Klasse Kunde und der Klasse Konto */ */ package kontoverwaltung; import support.*; public class KontoVerwaltung { public static void main(String[] args) { Kunde ersterKunde = new Kunde("Capone", "Al"); ersterKunde.addKonto(new Konto(2020)); ersterKunde.addKonto(new Konto(5050)); // Einzahlung ersterKunde.getKonto(2020).einzahlen(1000); // Auszahlung ersterKunde.getKonto(2020).auszahlen(300); // weiterer Code } } /* Programm: Kontoverwaltung Klasse: Kunde.java */ package kontoverwaltung; public class Kunde { 1 private ArrayList<Konto> kundenKonten = new ArrayList<Konto>(); // weiterer Code 2 public void addKonto(Konto neuesKonto){ neuesKonto.setKontoBesitzer(this); this.kundenKonten.add(neuesKonto); } 3 public void removeKonto(Konto wegMitDemKonto){ if(this.kundenKonten.contains(wegMitDemKonto)){ wegMitDemKonto.setKontoBesitzer(null); this.kundenKonten.remove(wegMitDemKonto); } } 4 public Konto getKonto(int kontoNr) { for (Konto konto : kundenKonten) { if (konto.kontoNr == kontoNr) { return konto; } } return null; } } C. Endreß 12 / 16 10/2008 11. Objekte und Klassen – Assoziationen Erläuterungen 1 Die 0..*-Assoziation zwischen Kunde und Konten wird mit einer ArrayList realisiert, da diese eine flexible Kapazität besitzt. 2 Die Methode addKonto() baut die beidseitige Beziehung auf. Zuerst wird beim neuen Konto die Referenzvariable für den Kontobesitzer initialisiert. Dann wird das neue Konto in die ArrayList der Konten aufgenommen. 3 Die Methode removeKonto() löscht die bidirektionale Verbindung zwischen dem Kunden- und dem Konto-Objekt wegMitDemKonto. Wenn wegMitDemKonto ein Element der Kontoliste ist, wird mit setKontoBesitzer(null) die Verbindung vom Konto zum Kunden gelöscht. Anschließend wird das Konto-Objekt aus der Liste entfernt. 4 Mit einer erweiterten for-Schleife wird über die Konten der ArrayList kundenKonten iteriert. Das Objekt konto ist der Iterator mit dem die ArrayList durchlaufen wird. /* Programm: Kontoverwaltung Klasse: Konto.java */ package kontoverwaltung; public class Konto { ... // Konstruktor public Konto(int kontoNr) { this.kontoNr = kontoNr; this.kontoStand = 0; this.kontoBesitzer = null; } // Verwaltungsmethoden für die Assoziation // Verbindungen vom Konto zum Kontobesitzer aufbauen public void setKontoBesitzer(Kunde kontoBesitzer){ this.kontoBesitzer = kontoBesitzer; } 1 // Verbindungen zwischen Konto und Kontobesitzer löschen public void removeKontoBesitzer(){ this.kontoBesitzer.removeKonto(this); this.kontoBesitzer = null; } 2 } Erläuterungen 1 Die Methode setKontoBesitzer() wird vom Kundenobjekt in dessen Instanz-Methode addKonto() aufgerufen. 2 Die Methode removeKontoBesitzer() löscht beide Referenzen: Zuerst die Referenz vom Kontobesitzer zu dessen Konto (this.kontoBesitzer.removeKonto(this)) und dann die Referenz des Kontos auf seinen Kontobesitzer (this.kontoBesitzer = null). C. Endreß 13 / 16 10/2008 11. Objekte und Klassen – Assoziationen 11.2.3 Spezielle Assoziationen Die UML kennt außer der einfachen Assoziation noch zwei weitere, speziellere Arten: Aggregation und Komposition 11.2.4 Aggregation Eine Aggregation liegt vor, wenn zwischen den Objekten der beteiligten Klassen (kurz: den beteiligten Klassen) eine Rangordnung gilt, die sich durch „ist Teil von“ bzw. „besteht aus“ beschreiben lässt. In einer Aggregationsbeziehung zwischen zwei Klassen muss genau ein Ende der Beziehung die Aggregatklasse (das Ganze) sein und das andere Ende für die Einzelteile stehen. Das bedeutet: Wenn B Teil von A ist, dann darf A nicht Teil von B sein. Aggregationen sind gewöhnlich 1-zu-viele-Beziehungen. Es besteht keine existentielle Abhängigkeit zwischen der Aggregatklasse und der Teilklasse. Das Ganze kann gelöscht werden und seine Einzelteil bleiben erhalten. Bei einer Aggregation kennzeichnet eine weiße bzw. transparente Raute die Aggregatklasse, also das Ganze. Alle anderen Angaben (Kardinalitäten, Namen, Rollen, etc.) werden analog zur Assoziation angegeben. Beispiele: Aggregation Ein Pkw hat üblicherweise vier Räder und eventuell eine Ersatzrad. Jedes Rad gehört genau zu einem Pkw. 1 Pkw 4..5 Aggregatklasse Rad Teilklasse Einem Web-Auftritt können mehrere Web-Seiten zugeordnet sein. Jede Web-Seite kann in mehreren WebAuftritten referenziert werden. Der Web-Auftritt kann gelöscht werden, ohne dass die referenzierten WebSites gelöscht werden müssen. * Web-Auftritt 1..* Aggregatklasse Web-Site Teilklasse Eine Abteilung umfasst mehrere Mitarbeiter. Wird die Abteilung aufgelöst, so werden die Mitarbeiter jedoch nicht zwingend entlassen. 1 Abteilung Aggregatklasse 1..* Mitarbeiter Teilklasse Achtung Placebo! Assoziation und Aggregation sind semantisch gleichwertig. Sie sind im resultierenden Programmcode nicht unbedingt zu unterscheiden. Falls Sie unsicher sind, welche Variante die richtige ist, machen Sie sich klar, dass es streng genommen sowieso keinen Unterschied gibt. Die Aggregation gibt einen wichtigen Hinweis auf die höhere Bindung zwischen den an der Aggregationsbeziehung beteiligten Klassen, was Klassenmodelle verständlicher macht. Im Zweifelsfall nehmen Sie die einfachere Variante, also die Assoziation. C. Endreß 14 / 16 10/2008 11. Objekte und Klassen – Assoziationen 11.2.5 Komposition Eine Komposition ist eine strenge Form der Aggregation. Auch hier muss eine „ist Teil von“-Beziehung vorliegen. Die Teile sind vom Ganzen existenzabhängig. Jedes Objekt der Teilklasse kann – zu einem Zeitpunkt – nur Komponente eines einzigen Objekts der Aggregatklasse sein, d.h. die bei der Aggregatklasse angetragene Multiplizität darf nicht größer als eins sein. Ein Teil darf jedoch – zu einem anderen Zeitpunkt – auch einem anderen Ganzen zugeordnet werden. Es besteht eine existentielle Abhängigkeit zwischen der Aggregatklasse und der Teilklasse. Wird das Ganze gelöscht, dann werden automatisch seine Teile gelöscht. Ein Teil darf jedoch zuvor explizit entfernt werden. Falls eine variable Kardinalität bei den Teilen angegeben ist (z.B.: 1..*), heißt dies, sie müssen nicht gemeinsam mit dem Aggregat erzeugt werden, sondern können auch später entstehen. Die dynamische Semantik des Ganzen gilt auch für seine Teile. Wird beispielsweise das Ganze kopiert, so werden auch seine Teile kopiert. Bei einer Komposition kennzeichnet eine schwarze bzw. gefüllte Raute das Ganze. Alle anderen Angaben (Kardinalitäten, Namen, Rollen, Restriktionen, etc.) werden analog zur Assoziation angegeben. Beispiele: Komposition Eine Rechnung enthält mindestens eine Rechnungsposition. Eine Rechnungsposition gehört zu genau einer Rechnung. Die Rechnungspositionen sind existenzabhängig von der Rechnung. Sobald die Rechnung gelöscht wird, werden auch alle Rechnungspositionen in ihr ebenfalls gelöscht. Die Klasse Rechnung übernimmt bestimmte Aufgaben für die Gesamtheit. So wird sie beispielsweise Operationen wie anzahlPositionen() oder summeBilden() enthalten. 1 Rechnung 1..* Aggregatklasse Rechnungsposition Teilklasse Einem Sparkonto können jeweils mehrere Kontobewegungen zugeordnet sein. Es kann allerdings jede Kontobewegung nur einem Sparkonto zugeordnet sein. Wird das Sparkonto-Objekt kopiert, dann werden auch alle ihm zugeordneten Kontobewegungen kopiert. Wird das Konto gelöscht, werden auch die zugeordneten Kontobewegungen gelöscht. 1 Sparkonto * Aggregatklasse Kontobewegung Teilklasse Ein Verzeichnis kann mehrere Dateien enthalten, wobei jede Datei nur im einem Verzeichnis enthalten sein kann. Wird das Verzeichnis kopiert, werden auch alle darin enthaltenen Dateien kopiert. Wird das Verzeichnis gelöscht, werden auch alle darin enthaltenen Dateien gelöscht. 1 Verzeichnis Aggregatklasse C. Endreß 15 / 16 * Datei Teilklasse 10/2008 11. Objekte und Klassen – Assoziationen 11.3 Zusammenfassung Eine Assoziation modelliert Verbindungen zwischen Objekten einer oder mehrerer Klassen. Diese Verbindungen können nur in einer Richtung (unidirektional) oder in wechselseitiger Richtung (bidirektional)bestehen. Assoziationen zwischen Objekten werden in Java mit Referenz-Variablen realisiert. Sonderfälle der Assoziation sind die Aggregation und die Komposition. Eine Aggregationen modelliert eine Ist-Teil-von-Beziehung. Eine Komposition ist eine strenge Form der Aggregation. Es besteht eine existentielle Abhängigkeit zwischen dem Ganzen (Aggregatklasse) und seinen Teilen. Jedes Teil kann zu einem Zeitpunkt nur zu einem Ganzen gehören. Durch Kardinalitäten wird die Wertigkeit von Assoziationen, Aggregationen und Kompositionen spezifiziert. Zusätzlich kann durch eine Rolle die Funktion des Objekts in einer Assoziation, Aggregation oder Komposition festgelegt werden. C. Endreß 16 / 16 10/2008 12. Objekte und Klassen – Dynamische Abläufe 12. Objekte und Klassen – Dynamische Abläufe Lernziele ☺ Dynamische Abläufe mit den UML-Notationen Sequenzdiagramm und Kollaborationsdiagramm beschreiben können. 12.1 Dynamische Abläufe Objektdiagramme, die wir bisher kennen gelernt haben, zeigen nur eine Momentaufnahme des Systems zu einem bestimmten Zeitpunkt. Sie eignen sich deshalb nur bedingt zur graphischen Darstellung dynamischer Abläufe. Die UML verfügt zu diesem Zweck über geeignetere Notationen: Kollaborationsdiagramme und Sequenzdiagramme 12.1.1 Kollaborationsdiagramm Ein Kollaborationsdiagramm stellt den Botschaftenfluss zwischen Objekten dar. Es sieht dem Objektdiagramm, in dem Objekte und ihre Verbindungen beschrieben werden, relativ ähnlich. Ein Kollaborationsdiagramm erweitert das Objektdiagramm um Botschaften. Es zeigt diejenigen Objekte, die für die Ausführung einer bestimmten Operation relevant sind. Objekte, die während der Ausführung neu erzeugt werden, sind mit {new}, Objekte, die während der Ausführung gelöscht werden, mit {destroyed} gekennzeichnet. Objekte, die während der Ausführung sowohl erzeugt als auch wieder gelöscht werden, sind {transient}. Als Auslöser einer Operation kann ein Akteur – in der Regel der Benutzer – eingetragen werden, dargestellt als „Strichmännchen“. An jede Verbindung (link) kann eine Botschaft in Form eines Pfeils, einer laufenden Nummer und dem Operationsnamen angetragen werden. Die Reihenfolge und Verschachtelung der Operationen wird durch eine hierarchische Nummerierung angegeben. 1:nachricht1() :Klasse1 {transient} 2:nachricht2() :Klasse2 {new} 3:nachricht4() © C. Endreß :Klasse4 {new} 1/7 2.1:nachricht7() :Klasse3 {new} wird während der Ausführung neu erzeugt {{transient} wird während der Ausführung erzeugt und wieder gelöscht {destroyed} wird während der Ausführung gelöscht 10/2008 12. Objekte und Klassen – Dynamische Abläufe 12.1.2 Sequenzdiagramme Ein Sequenzdiagramm dient zur schematischen Veranschaulichung von zeitbasierten Vorgängen. Sequenzdiagramme erlauben eine genaue zeitliche Darstellung von Abläufen – allerdings unter Verzicht auf Attributangaben und Verbindungen zwischen Objekten. In der Systemanalyse werden Sequenzdiagramme verwendet, um Abläufe so präzise zu beschreiben, dass deren fachliche Korrektheit diskutiert werden kann. Im Systementwurf werden Sequenzdiagramme für eine detaillierte Spezifikation der Operationsabläufe verwendet und enthalten dann alle beteiligten Operationen. Kennzeichnend für diese Darstellungsform des Sequenzdiagramms ist eine Zeitachse, die vertikal von oben nach unten führt. Objekte, die Botschaften austauschen, werden durch gestrichelte vertikale Geraden dargestellt (sog. Objektlinien oder Lebenslinien). Jede Linie repräsentiert die Existenz eines Objekts während einer bestimmten Zeit. Eine Objektlinie beginnt nach dem Erzeugen des Objekts und endet mit dem Löschen des Objekts. Existiert ein Objekt während der gesamten Ausführungszeit, dann ist die Linie von oben nach unten durchgezogen. Am oberen Ende der Linie wird ein Objektsymbol gezeichnet. Wird ein Objekt erst im Laufe der Ausführung erzeugt, dann zeigt eine Botschaft auf dieses Objektsymbol. Das Löschen des Objekts wird durch ein großes „X“ markiert. Die Reihenfolge der Objekte ist beliebig. Sie soll so gewählt werden, dass ein möglichst übersichtliches Diagramm entsteht. Die erste vertikale Linie bildet in vielen Sequenzdiagrammen einen Akteur – in der Regel der Benutzer – dargestellt als »Strichmännchen«. In das Sequenzdiagramm werden die Botschaften eingetragen, die zum Aktivieren der Operationen dienen. Jede Botschaft wird als gerichtete Kante (mit gefüllter Pfeilspitze) vom Sender zum Empfänger gezeichnet. Der Pfeil wird mit dem Namen der aktivierten Operation beschriftet. Eine aktive Operation wird durch ein schmales Rechteck (Steuerungsfokus) auf der Objektlinie angezeigt. Nach dem Beenden der Operation zeigt eine gestrichelte Linie mit offener Pfeilspitze, dass der Kontrollfluss zur aufrufenden Operation zurückgeht. Auf diese gestrichelte Linie kann verzichtet werden. Die UML erlaubt die Angabe von Bedingungen und Wiederholungen im Sequenzdiagramm. Die Bedingung (condition) wird in eckigen Klammern angegeben: [Bedingung] Operation() Die aufgeführte Operation wird dann aufgerufen, wenn die Bedingung erfüllt ist. Wiederholungen (iterations) können spezifiziert werden durch: Operation() oder * [Bedingung] Operation() Wenn keine Wiederholungen in ein Diagramm eingetragen werden, so bedeutet dies in der UML, dass die Anzahl der Wiederholungen unspezifiziert ist. Die Bedingung wird in der Systemanalyse in der Regel umgangssprachlich formuliert. © C. Endreß 2/7 10/2008 12. Objekte und Klassen – Dynamische Abläufe bereits existierende Objekte Akteur einObjekt :Klasse1 Bedingung: [bedingung] operation() :Klasse2 Wiederholung: * operation() * [bedingung] operation() Objekt wird erzeugt nachricht1() Klasse3() neuesObjekt: Klasse3 nachricht5() Auf Antwortpfeile kann auch verzichtet werden. nachricht3() Objekt schickt Botschaften an sich selbst nachricht2() Steuerungsfokus Objekt wird gelöscht Lebenslinie Beispiel: Kontoverwaltung Ein Bankkunde kann beliebig viele Sparkonten besitzen. Für jedes Konto wird ein individueller Habenzins festgelegt. Außerdem besitzt jedes Konto eine eindeutige Kontonummer. Ein Kunde kann Beträge einzahlen und abheben. Desweiteren werden Zinsen gutgeschrieben. Um die Zinsen zu berechnen, muss für jede Kontobewegung das Datum und der Betrag notiert werden. Die Gutschrift der Zinsen erfolgt bei den Sparkonten jährlich. Ein Kunde kann jedes seiner Konten wieder auflösen. Klassendiagramm Das statische Konzept der Problemlösung wird durch folgendes UML-Klassendiagramm dargestellt. Kunde Name Vorname SparKonto 1 0..* Kontonummer KontoBewegung 1 Habenzins / Kontostand 1..* Betrag Datum einzahlen() auszahlen() gutschreibenZinsen() © C. Endreß 3/7 10/2008 12. Objekte und Klassen – Dynamische Abläufe Objektdiagramm Das Objektdiagramm liefert eine Momentaufnahme des Systems, da es den Zustand nur zu einem bestimmten Zeitpunkt beschreibt. Der Kunde Al Capone hat ein Sparkonto mit der Kontonummer 1111 eröffnet und am 04.04.2004 eine erste und bisher einzige Einzahlung über 1000 € vorgenommen. Am 10.04.2004 folgte eine Auszahlung über 300 €. Weitere Kontobewegungen gab bis dahin nicht. ersterKunde:Kunde Name = ”Capone“ Vorname = “Al“ :SparKonto :KontoBewegung Kontonummer = 1111 Habenzins = 1.5 / Kontostand = 700 € Betrag = 1000 € Datum = 04.04.2004 :KontoBewegung Betrag = -300 € Datum = 10.04.2004 Kollaborationsdiagramm Der Ablauf einer Auszahlung für ein Sparkonto des Kunden-Objekts ersterKunde wird mit folgendem Objektdiagramm dargestellt: 1 : getLinkKonto() Kontoverwaltung ersterKunde : Kunde 2 : auszahlen(betrag) 2.1.1 : KontoBewegung() : SparKonto : KontoBewegung {new} 2.1: [kontoStand > betrag] addKontoBewegung(betrag) Die Reihenfolge der Operationen wird durch die Nummerierung festgelegt. Die Methode setLinkKontoBewegung wird im Rahmen der Methode auszahlen aufgerufen – erkennbar an der Nummer 2.1. Außerdem ist die Ausführung der Methode setLinkKontoBewegung von der Bedingung kontoStand > betrag abhängig, was durch die eckigen Klammern gekennzeichnet ist. Der Konstruktoraufruf KontoBewegung() ist wiederum Bestandteil der Methode setLinkKontoBewegung. © C. Endreß 4/7 10/2008 12. Objekte und Klassen – Dynamische Abläufe Sequenzdiagramm Das folgende Sequenzdiagramm modelliert den Vorgang einer Auszahlung für das Kunden-Objekt ersterKunde. Kontoverwaltung ersterKunde : Kunde getLinkKonto() : SparKonto auszahlen(betrag) [kontoStand > betrag] addKontoBewegung() KontoBewegung() : KontoBewegung Als Akteur tritt in diesem Fall die Klasse Kontoverwaltung auf, die an das Objekt ersterKunde die Botschaft getLinkKonto sendet. Das Objekt ersterKunde baut eine Verbindung zu einem Sparkonto auf und sendet die Botschaft auszahlen an das SparKonto-Objekt. Wenn der aktuelle Kontostand größer als der Auszahlungsbetrag ist, wird die Auszahlung ausgeführt, indem mit der Methode setLinkKontoBewegung ein neues Objekt der Klasse KontoBewegung mit den Daten der Transaktion generiert wird. Die Verbindung zwischen dem SparKonto und dem neuen KontoBewegung-Objekt wird mit der Methode setLinkKontoBewegung erzeugt. Implementierung in Java Die folgenden Auszüge aus dem Java-Quellcode enthalten den Programmcode, der für die modellierte Auszahlung relevant ist. /* Programm: Kontoverwaltung Klasse: KontoVerwaltung.java */ package kontoverwaltung; import support.*; import java.util.GregorianCalendar; public class KontoVerwaltung { public static void main(String[] args) { Kunde ersterKunde = new Kunde("Capone", "Al"); ersterKunde.addSparKonto(new SparKonto(1111)); // Einzahlung ersterKunde.getLinkKonto(1111).einzahlen(1000); // Auszahlung © C. Endreß 5/7 10/2008 12. Objekte und Klassen – Dynamische Abläufe ersterKunde.getLinkKonto(1111).auszahlen(300); druckeKontoAuszug(ersterKunde.getLinkKonto(1111)); } public static void druckeKontoAuszug(Konto konto) { ... } } /* Programm: Kontoverwaltung Klasse: Kunde.java */ package kontoverwaltung; public class Kunde { ... 1 private ArrayList<SparKonto> kundenKonten = new ArrayList<SparKonto>(); ... public void addSparKonto(SparKonto neuesKonto){ this.kundenKonten.add(neuesKonto); } public SparKonto getLinkKonto(int kontoNr) { for (SparKonto konto : kundenKonten) { if (konto.kontoNr == kontoNr) { return konto; } } return null; } 2 } Erläuterungen 1 Die 0..*-Assoziation zwischen Kunde und Sparkonten wird mit einer ArrayList realisiert, die diese eine flexible Kapazität besitzt. 2 Mit einer erweiterten for-Schleife wird über die Sparkonten der ArrayList kundenKonten iteriert. Das Objekt konto ist der Iterator mit dem die ArrayList durchlaufen wird. /* Programm: Kontoverwaltung Klasse: SparKonto.java */ package kontoverwaltung; public class SparKonto { ... 1 private ArrayList<KontoBewegung> transAktionen = new ArrayList<KontoBewegung>(); ... © C. Endreß 6/7 10/2008 12. Objekte und Klassen – Dynamische Abläufe public boolean auszahlen(double betrag) { if (this.getKontoStand() – betrag >= 0) { this.addKontoBewegung(-betrag)); return true; } else return false; } 2 3 private void addKontoBewegung(double betrag) { this.transaktionen.add(new KontoBewegung(betrag)); } } Erläuterungen 1 Deklaration einer ArrayList für Objekte der Klasse Kontobewegung. 2 Eine Auszahlung erfolgt nur, wenn der Kontostand größer als der Auszahlungsbetrag ist. Es wird dann ein mit der Methode addKontoBewegung() ein neues KontoBewegung-Objekt der ArrayList transAktionen hinzugefügt. Um die Kontobewegung als Auszahlung zu kennzeichnen, erhält der Betrag ein negatives Vorzeichen, indem die Variable betrag negiert wird. Der Kontostand ist nach der vorangegangenen Modellierung (siehe Klassendiagramm) ein abgeleitetes Attribut. Er wird stets aktuell in der Methode getKontoStand() aus den Kontobewegungen errechnet. 3 Die Methode addKontoBewegung() ist mit der Sichtbarkeit private versehen. Dadurch wird sichergestellt, dass eine Kontobewegung nur innerhalb der der Klasse SparKonto im Rahmen der Methoden einzahlen() und auszahlen() erzeugt werden kann. /* Programm: Kontoverwaltung Klasse: KontoBewegung.java */ package kontoverwaltung; import java.util.GregorianCalendar; public class KontoBewegung { private double betrag; private GregorianCalendar datum; public KontoBewegung(double betrag) { this.betrag = betrag; // speichert das aktuelle Systemdatum datum = new GregorianCalendar(); } ... } © C. Endreß 7/7 10/2008 13. Objekte und Klassen – Interfaces 13. Objekte und Klassen – Interfaces Lernziele ☺ Syntax und Semantik von Java-Interfaces anhand von Beispielen erklären können. 13.1 Interfaces In der objektorientierten Software-Entwicklung gibt es neben Klassen noch Schnittstellen. Der Begriff wird nicht einheitlich verwendet. In der Regel definieren Schnittstellen Dienstleistungen für Anwender, d.h. für aufrufende Klassen, ohne etwas über die Implementierung der Dienstleistung festzulegen. Es werden Operationssignaturen bereitgestellt, die das „Was“ aber nicht das „Wie“ festlegen. Eine Schnittstelle besteht also im Allgemeinen nur aus Operationssignaturen, d.h. sie besitzt keine Operationsrümpfe und keine Attribute. Schnittstellen werden in Java Interfaces genannt. Ein Interface beschreibt Funktionalitäten aber stellt keinerlei Implementierung der Funktionalität zur Verfügung (funktionale Abstraktion). Ein Interface ist ähnlich wie eine Klasse aufgebaut, enthält jedoch weder Attribute noch Methodenimplementierungen. Es werden nur die Signaturen (Methodenkopf mit Parameterliste) der Methoden angegeben, und es dürfen Konstanten deklariert werden. Sollen die Funktionalitäten, die ein Interface beschreibt, von einer Klasse zur Verfügung gestellt werden, muss diese das Interface implementieren, d.h. die Funktionen „ausformulieren“. 13.1.1 Interfaces definieren Regeln für die Definition eines Interface: In Java wird ein Interface mit dem Schlüsselwort interface deklariert. Alle Methoden müssen mit dem Modifikator public deklariert werden. Die Methoden dürfen keinen Anweisungsblock (Rumpf) enthalten. Es dürfen nur unveränderbare Klassenattribute (Konstanten) deklariert werden, die unmittelbar mit einem Wert zu initialisieren sind. Diese Klassenattribute sind implizit public, final und static. Ein Interface hat keine Konstruktoren, weil es nicht instanziiert werden kann. 13.1.2 Interfaces verwenden Eine Klasse kann beliebig viele Interfaces implementieren. Die Implementierung erfolgt mit dem Schlüsselwort implements. Auf diese Weise kann in Java eine Form von Mehrfachvererbung realisiert werden. Jede Klasse, die ein Interface implementiert, muss alle Methoden des Interface implementieren. Diese Methoden müssen in der Klasse als public deklariert werden. Außerdem muss die Signatur der C. Endreß 1/8 02/2006 13. Objekte und Klassen – Interfaces implementierenden Methode exakt mit der Signatur übereinstimmen, die in der Interface-Definition spezifiziert ist. Implementiert eine Klasse die Methoden eines Interface nicht vollständig, so muss diese Klasse als abstrakte Klasse deklariert werden. Es ist erlaubt und üblich, dass Klassen, die Interfaces implementieren, eigene zusätzliche Operationen definieren. Analog zu Referenz-Variablen auf Objekte können auch Referenz-Variablen von Interface-Typen deklariert werden. Eine Interface-Referenz kennt nur die Operationen, die im Interface deklariert sind. Wenn eine Klasse ein Interface implementiert, können Objekte dieser Klasse an Variablen des InterfaceTyps zugewiesen werden. In einem Interface können Konstanten definiert werden, die implizit sowohl static als auch final deklariert sind. Jede Klasse, die das Interface implementiert, erbt diese Konstanten und kann sie verwenden, als wären sie direkt in der Klasse definiert. Es ist nicht notwendig, den Namen des Interface voranzustellen oder irgendeine Art von Implementation der Konstanten zu schreiben. Nützlich ist ein Interface immer dann, wenn Eigenschaften einer Klasse beschrieben werden sollen, die nicht direkt in seiner normalen Vererbungshierarchie abgebildet werden können. Beispiel Die bereits bekannte Kontoverwaltung soll zu einer Verwaltung für Kapitalanlagen erweitert werden. Zur Vereinfachung betrachten wir in diesem Beispiel nur Sparkonten und Sparbriefe. Da ein Sparbrief kein Konto im üblichen Sinn ist, wird die Klasse SparBrief nicht von Konto abgeleitet. Die neuen Anforderungen werden in einem Pflichtenheft formuliert: /1/ Neben Sparkonten sollen als neue Anlageart auch Sparbriefe verwaltet werden. /2/ Sparbriefe haben einen Zinssatz, einen Nennwert und eine Laufzeit. Sie besitzen außerdem eine Vertragsnummer. Alle Attribute werden bei Vertragsabschluss festgelegt und sind nicht änderbar. /3/ Ein- und Auszahlungen sind bei Sparbriefen nicht möglich. /4/ Die Zinsen für Sparbriefe und Sparkonten werden jährlich berechnet. /5/ Kapitaleinkünfte sind steuerpflichtig. Auf Zinserträge von Sparbriefen und Sparkonten wird eine 30prozentige Kapitalertragssteuer erhoben. Es sind Sparerfreibeträge zu berücksichtigen. /6/ Alle Daten müssen einzeln gelesen werden können. C. Endreß 2/8 02/2006 13. Objekte und Klassen – Interfaces Das Pflichtenheft fordert für Objekte beider Klassen die Berechnung von Zinsen und Steuern. Da die beiden Klassen nicht in einer Vererbungshierarchie stehen, definieren wir ein Interface KapitalAnlage, das die gemeinsamen Funktionalitäten berechneZinsen und berechneSteuern beschreibt und die Kapitalertragssteuer als Konstante deklariert. Die Klassen SparKonto und SparBrief implementieren das Interface. Das folgende UML-Diagramm zeigt die Klassenhierarchie. «interface» KapitalAnlage Konto # kontoNr: int + KAPITALERTRAGSSTEUER: double # kontoStand: double # habenZins: double + berechneZinsen() + berechneSteuern() + Konto() + einzahlen() + auszahlen() + getKontoNr() + getKontoStand() SparBrief + getHabenZins() - zinsSatz: double - nennWert: double - laufzeit: int - vertragsNr: int SparKonto - freiStellung: double + SparBrief() + berechneZinsen() - mindestEinlage: double - freiStellung: double + berechneSteuern() + SparKonto() + getLaufzeit() + auszahlen() + getNennWert() + berechneZinsen() + getVertragsNr() + berechneSteuern() + getZinsSatz() + getMindestEinlage() + getFreiStellung() + getFreiStellung() + setFreiStellung() + setFreiStellung() Java: /* Interface KapitalAnlage: Demonstriert die Definition eines Java-Interfaces mit einem Klassenattribut und zwei Methoden-Signaturen */ public interface KapitalAnlage { // Klassenattribut double KAPITALERTRAGSSTEUER = 30.0; 1 2 // Deklaration der Methoden public double berechneZinsen(int tage); public double berechneSteuern(int tage); 3 } Erläuterungen 1 Durch das Schlüsselwort interface wird KapitalAnlage als Java-Interface deklariert. 2 Deklaration eines Klassenattributs. Alle Klassen, die das Interface KapitalAnlage implementieren, erben das Attribut. Das Attribut wird implizit zu einer Konstanten, auch wenn das Schlüsselwort final, mit dem Konstanten üblicherweise deklariert werden, nicht explizit verwendet wird. Der C. Endreß 3/8 02/2006 13. Objekte und Klassen – Interfaces Wert von KAPITALERTRAGSSTEUER kann daher von den implementierenden Klassen nicht mehr verändert werden. 3 Bei der Methodendeklaration wird kein Methodenrumpf (auch nicht { }) angegeben, sondern nur ein abschließendes Semikolon. /* Klasse SparKonto: SparKonto ist eine Unterklasse von Konto und implementiert das Interface KapitalAnlage. Das Listing zeigt nur die Ergänzungen des Quellcodes gegenüber dem bekannten Quellcode von SparKonto. */ public class SparKonto extends Konto implements KapitalAnlage { 1 2 private double freiStellung; . . . 3 public double berechneZinsen(int tage) { double zinsen = 0.0; double zinsSatz = getHabenZins(); double kapital = getKontoStand(); if (tage > 0) zinsen = kapital * (zinsSatz / 100) * tage / 360; return zinsen; } 4 public double berechneSteuern(int tage) { double steuer = 0.0; double freiBetrag = getFreiStellung(); double zinsen = berechneZinsen(tage); if (zinsen > freiBetrag) steuer = (zinsen - freiBetrag) * KAPITALERTRAGSSTEUER / 100; 5 return steuer; } 6 public double getFreiStellung() { return freiStellung; } 7 public void setFreiStellung(double betrag) { freiStellung = betrag; } . . . } Erläuterungen 1 Die Klasse SparKonto wird als Unterklasse von Konto deklariert (extends Konto) und implementiert das Interface KapitalAnlage (implements KapitalAnlage). Die Klasse SparKonto erbt somit die Attribute und Methoden der Oberklasse Konto sowie die Konstanten und Methoden des Interface KapitalAnlage. Die Methoden des Interface sind abstrakt und müssen von der Unterklasse SparKonto ebenso implementiert werden wie die abstrakten Methoden der Oberklasse Konto. 2 Deklaration des Attributs für den Sparerfreibetrag. C. Endreß 4/8 02/2006 13. Objekte und Klassen – Interfaces 3 Die Methode berechneZinsen() wurde im Interface KapitalAnlage deklariert und wird an dieser Stelle von der Klasse SparKonto implementiert. Der Modifizierer muss public sein. Die Methoden-Signatur der implementierenden Methode muss mit der Methoden-Signatur des Interfaces übereinstimmen. 4 Die Methode berechneSteuern() wurde im Interface KapitalAnlage deklariert und wird an dieser Stelle von der Klasse SparKonto implementiert. Es gelten die bekannten Regeln. 5 Die Methode berechneSteuern() kann direkt auf die Konstante KAPITALERTRAGSSTEUER zugreifen, weil SparKonto diese Konstante vom Interface KapitalAnlage geerbt hat. 6 Methode zum Lesen des Attributwerts von freiStellung. 7 Methode zum Schreiben des Attributwerts von freiStellung. 1 2 3 4 /* Klasse SparBrief: SparBrief implementiert das Interface KapitalAnlage. */ public class SparBrief implements KapitalAnlage { // Attribute private double zinsSatz; private double nennWert; private int laufzeit; private int vertragsNr; private double freiStellung; // Konstruktor public SparBrief(int vertragsNr, double zinsSatz, double betrag, int jahre) { this.vertragsNr = vertragsNr; this.zinsSatz = zinsSatz; nennWert = betrag; laufzeit = jahre * 360; freiStellung = 0.0; } public double berechneZinsen(int tage) { double zinsen = 0.0; double zinsSatz = getZinsSatz(); double kapital = getNennWert(); if (tage > 0 && tage <= laufzeit) zinsen = kapital * (zinsSatz / 100) * tage / 360; return zinsen; } 5 public double berechneSteuern(int tage) { double steuer = 0.0; double freiBetrag = getFreiStellung(); double zinsen = berechneZinsen(tage); if (zinsen > freiBetrag) steuer = (zinsen - freiBetrag) * KAPITALERTRAGSSTEUER / 100; 6 return steuer; } 7 C. Endreß public int getLaufzeit() { return laufzeit; } 5/8 02/2006 13. Objekte und Klassen – Interfaces public double getNennWert() { return nennWert; } public int getVertragsNr() { return vertragsNr; } public double getZinsSatz() { return zinsSatz; } public double getFreiStellung() { return freiStellung; } public void setFreiStellung(double betrag) { freiStellung = betrag; } } Erläuterungen 1 Die Klasse SparBrief implementiert das Interface KapitalAnlage (implements KapitalAnlage). Sie erbt damit alle Konstanten und Methoden des Interface. Da die Methoden des Interface abstrakt sind, muss die Klasse SparBrief diese implementieren. 2 Deklaration der Attribute 3 Gemäß Anforderung des Pflichtenhefts (/2/) werden vom Konstruktor die Attribute initialisiert. setMethoden sind in der Klasse nicht vorgesehen, da die Attributwerte während der Laufzeit des Sparbriefs laut Pflichtenheft (/2/) nicht änderbar sein sollen. 4 Die Methode berechneZinsen() wurde im Interface KapitalAnlage deklariert und wird an dieser Stelle von der Klasse SparBrief implementiert. Der Modifizierer muss public sein. Die Methoden-Signatur der implementierenden Methode muss mit der Methoden-Signatur des Interfaces übereinstimmen. 5 Die Methode berechneSteuern() wurde im Interface KapitalAnlage deklariert und wird an dieser Stelle von der Klasse SparBrief implementiert. Es gelten die bekannten Regeln. 6 Die Methode berechneSteuern() kann direkt auf die Konstante KAPITALERTRAGSSTEUER zugreifen, weil SparBrief diese Konstante vom Interface KapitalAnlage geerbt hat. 7 Deklarationen der get-Methoden zum Lesen der Attributwerte. /* Programm: Testrahmen für die Klassen SparKonto und SparBrief */ public class Anwendung { 1 2 3 4 C. Endreß static void druckeZinsen(KapitalAnlage anlage, int tage){ Console.println("Zeitraum: " + tage + " Tage"); Console.println("Zinsen : " + anlage.berechneZinsen(tage)); } public static void main(String[] args) { // Deklaration der Objekt-Variablen SparKonto einSparKonto = new SparKonto(4711); einSparKonto.einzahlen(5000); 6/8 02/2006 13. Objekte und Klassen – Interfaces 5 6 SparBrief einSparBrief = new SparBrief(1234, 4.0, 5000, 4); einSparBrief.setFreiStellung(1000); Console.println("Sparkonto #" + einSparKonto.getKontoNr()); druckeZinsen(einSparKonto, 360); Console.println("Kapitalertragssteuer: " + einSparKonto.berechneSteuern(360)); Console.println("\nSparbrief #" + einSparBrief.getVertragsNr()); druckeZinsen(einSparBrief, 360); Console.println("Kapitalertragssteuer: " + einSparBrief.berechneSteuern(360)); 7 8 9 10 } } Konsolenausgabe Sparkonto #4711 Zeitraum: 360 Tage Zinsen : 75.0 Kapitalertragssteuer: 22.5 Sparbrief #1234 Zeitraum: 360 Tage Zinsen : 200.0 Kapitalertragssteuer: 0.0 Erläuterungen 1 Deklaration Methode druckeZinsen() zur Ausgabe des Zinsertrags. Der Parameter anlage der Methode ist eine Variable vom Typ des Interface KapitalAnlage. Diesem Parameter dürfen Objekte aller Klasse übergeben werden, die das Interface KapitalAnlage implementierten – in diesem Fall die Klassen SparBrief und SparKonto. Von welcher Klasse das übergebene Objekt ist, entscheidet sich erst zur Laufzeit (dynamisches Binden). 2 Aufruf der polymorphen Methode berechneZinsen(). Alle Klassen, die KapitalAnlage implementieren, müssen die Methoden berechneZinsen() implementieren. Die Art der Implementierung hängt von der Klasse (Polymorphie). 3 Deklaration der Referenz-Variable einSparKonto und Erzeugung eines Objekts der Klasse SparKonto. 4 Aufruf der Instanz-Methode einzahlen(). 5 Deklaration der Referenz-Variable einSparBrief und Erzeugung eines Objekts der Klasse SparBrief. 6 Aufruf der Instanz-Methode setFreiStellung(). 7 In der Methode druckeZinsen() wird dem Parameter anlage vom Interface-Typ KapitalAnlage das Objekt einSparKonto der Klasse SparKonto übergeben. Dies ist nur möglich, weil die Klasse SparKonto das Interface KapitalAnlage implementiert und damit alle ihre Objekte kompatibel zu Interface-Variablen von KapitalAnlage sind. 8 Aufruf der polymorphen Methode berechneSteuern(), die vom Interface KapitalAnlage deklariert und in der Klasse SparKonto implementiert wird. 9 In der Methode druckeZinsen() wird dem Parameter anlage vom Interface-Typ KapitalAnlage das Objekt einSparBrief der Klasse SparBrief übergeben. Dies ist nur möglich, weil die Klasse SparBrief das Interface KapitalAnlage implementiert und damit alle ihre Objekte kompatibel zu Interface-Variablen von KapitalAnlage sind. 10 Aufruf der polymorphen Methode berechneSteuern(), die vom Interface KapitalAnlage deklariert und in der Klasse SparBrief implementiert wird. C. Endreß 7/8 02/2006 13. Objekte und Klassen – Interfaces 13.1.3 Interfaces und Abstrakte Klassen Ein Interface enthält ausschließlich abstrakte Methoden und stellt damit eine Sonderform einer abstrakten Klasse dar. Eine abstrakte Klasse muss hingegen nicht vollständig abstrakt sein. Sie kann Teile einer Implementierung enthalten, von der erbende Klassen Gebrauch machen können. In Java kann eine Unterklasse nur von einer Oberklasse erben (Einfachvererbung). Allerdings kann eine Klasse beliebig viele Interfaces implementieren, wodurch eine Art Mehrfachvererbung realisiert werden kann. Ein wichtiger Unterschied zwischen Interfaces und abstrakten Klassen hängt mit der Kompatibilität zusammen. Wird ein Interface als Bestandteil einer öffentlichen Schnittstelle definiert und später eine neue Methode zu diesem Interface hinzugefügt, so werden alle Klassen funktionsunfähig, die frühere Versionen dieses Interface implementiert haben, es sei denn, Sie ergänzen in allen implementierenden Klassen die neue Methodenimplementierung. Bei einer abstrakten Klasse ist es dagegen möglich nichtabstrakte Methoden hinzuzufügen, ohne existierende abgeleitete Klassen modifizieren zu müssen. 13.2 Zusammenfassung Das Schnittstellen-Konzept von Java erlaubt es, Konstanten und abstrakte Methoden zu einem Interface (Schnittstelle) zu bündeln. Eine Klasse kann mit dem Schlüsselwort implements beliebig viele Schnittstellen implementieren. Die Klasse erbt dabei alle Konstanten und abstrakten Operationen des Interface. Variablen können vom Typ eines Interface sein. In diesem Fall können sie auf Objekte verweisen, die dieses Interface oder ein daraus abgeleitetes Interface implementieren. Interface-Variablen können ihrerseits Variablen zugewiesen werden, die zu Klassen gehören, die das Interface implementieren. C. Endreß 8/8 02/2006 14. Graphische Oberflächen 14. Graphische Oberflächen Lernziele ☺ Die Konzepte graphischer Oberflächen in Java verstehen . ☺ Das Konzept der Ereignisbehandlung verstehen und anwenden können. ☺ Grundelemente graphischer Oberflächen kennen. ☺ Graphische Benutzungsoberflächen mit Swing erstellen können. 14.1 Einführung Die graphischen Benutzungsoberflächen, über die ein Benutzer mit einer Anwendungssoftware interagiert und kommuniziert, bezeichnet man als Graphical User Interface, abgekürzt GUI. Java stellt für die GUI-Programmierung entsprechende Klassen zur Verfügung, die seit dem JDK 1.2 unter dem Oberbegriff Java Foundation Classes (JFC) zusammen gefasst werden und in mehrere Pakete aufgeteilt sind: AWT (Abstract Windowing Toolkit): Diese Klassenbibliothek heißt „abstrakt”, weil sich der Programmierer keine Gedanken darüber machen muss, wie die Elemente der Oberfläche, z.B. Eingabefelder und Druckknöpfe auf einem konkreten System umgesetzt werden. Alle Interaktionselemente werden vom darunter liegenden Betriebssystem zur Verfügung gestellt. Man nennt diese Vorgehensweise Peer-Ansatz, weil die AWT-Komponenten alle auszuführenden Aktionen an plattformspezifische GUI-Objekte, Peers genannt, weiterreichen. Komponenten, die solche Peer-Objekte benötigen, werden als heavyweight (schwergewichtig) bezeichnet. Sie sehen auf unterschiedlichen Betriebssystemen wie z.B. Windows oder Linux auch unterschiedlich aus. Infolge dieses Prinzips kann das AWT nur diejenigen GUI-Funktionalitäten bereitstellen, die auf allen unterstützen Plattformen verfügbar sind. Swing Die Nachteile der AWT-Klassen haben dafür gesorgt, dass mit der Entwicklung der Swing-Klassen ein anderer Weg eingeschlagen wurde. Fast alle Swing-Komponenten sind vollständig in Java geschrieben und werden deshalb als lightweight (leichtgewichtig) bezeichnet. Nur wenige Komponenten benutzen noch in kleinem Ausmaß plattformspezifische GUI-Objekte. Form und Funktion der Komponenten sind somit nicht mehr an das Betriebssystem gebunden, auf dem das Programm ausgeführt wird. Die Oberfläche kann plattformunabhängig vollständig selbst gestaltet und auch noch zur Laufzeit des Programms im look and feel verändert werden. Swing bietet wesentlich mehr Möglichkeiten zur Oberflächengestaltung und ist damit flexibler und effizienter als AWT. © C. Endreß 1 / 33 02/2006 14. Graphische Oberflächen 14.2 Ein erstes Beispiel In einem ersten Beispiel soll mit Swing zunächst ein leeres Fenster mit der Titelzeile „Mein erstes Swing-Fenster“ erzeugt werden. 1 /* Programm: FensterOhneInhalt.java * Erzeugt ein einfaches Swing-Fenster auf dem Bildschirm */ import javax.swing.*; public class FensterOhneInhalt { public static void main(String[] args) { // Fenster-Objekt erzeugen JFrame fenster = new JFrame(); 2 // Attribute des Fensters setzen fenster.setTitle("Mein erstes Swing-Fenster"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 3 4 5 6 } } Erläuterungen 1 Importieren des swing-Pakets, das die swing-Klassen für die graphischen Oberflächen enthält. 2 Die Klasse JFrame ist die wichtigste Hauptfensterklasse in Swing. Mit Hilfe des Konstruktors der Klasse JFrame wird ein leeres Anwendungsfenster namens fenster generiert. 3 Das JFrame-Objekt fenster besitzt verschiedene Attribute, die mit seinen set-Methoden angepasst werden. Die Methode setTitle legt den Text fest, der in der Rahmenleiste des Fensters als Fenster-Titel angezeigt wird. 4 Die Methode setSize bestimmt die Breite (300 Pixel) und die Höhe (150 Pixel) des Fensters. 5 Der Status des Fenster wird mit der Methode setVisible auf „sichtbar“ gesetzt, um das Fenster auf dem Bildschirm erscheinen zu lassen. Standardmäßig sind neu erzeugte Fenster der Klasse JFrame nicht sichtbar. 6 Die Instanzmethode setDefaultCloseOperation des JFrame-Objekts legt fest, wie das Fenster auf Betätigen des Schließen-Symbols bzw. der Tastenkombination Alt-F4 reagieren soll. Durch Angabe der Konstanten EXIT_ON_CLOSE wird mit dem Schließen des Fenster auch das Programm beendet. In älteren Programmen kann anstelle der Methode setDefaultCloseOperation ein sogenannter WindowsListener als Routine zur Ereignisbehandlung verwendet werden, der alle Aktionen ausführt, die mit dem Schließen des Fensters verbunden sind. Allerdings wird der Quellcode durch den WindowsListener nur unnötig aufgebläht, wenn mit dem Schließen des Fensters lediglich das Programm beendet wird. Falls jedoch mit dem Beenden des Programms weitere Aktionen wie beispielsweise das Speichern von Daten verbunden sind, wird eine Ereignisbehandlung unumgänglich. (Dazu später mehr) © C. Endreß 2 / 33 02/2006 14. Graphische Oberflächen fenster.addWindowListener( new WindowAdapter() { public void windowClosing( WindowEvent e ) { System.exit(0); } }); Wenn wir das Programm ausführen, öffnet sich das Windows-Fenster, das sich in der gewohnten Weise mit der Maus bewegen, vergrößern und verkleinern lässt. Beendet wird das Programm über die bekannten oder Tastenkombination Alt-F4 ). Mechanismen des Betriebssystems (Schließen-Symbol Warum wird das Programm nicht, so wie wir es von Konsolenprogrammen kennen, unmittelbar nach Ausführung der letzten Anweisung der main-Methode beendet? Durch das Erzeugen des JFrame-Objekts wird ein zusätzlicher Programmfluss für das Fenster gestartet, der parallel zum Programmfluss der main-Methode abgearbeitet wird. Einen solchen parallelen Programmfluss bezeichnet man als Thread (deutsch: Faden). Ein Programm kann aus vielen Threads bestehen und ist erst dann beendet, wenn alle Threads beendet sind. Das Programm FensterOhneInhalt terminiert also erst, wenn der Thread, der für die Fensterdarstellung zuständig ist, beendet ist. Wir wollen nun noch eine kleine Veränderung an der Klasse FensterOhneInhalt vornehmen, so dass diese für zukünftige Erweiterungen geeignet ist. Die neue Variante sieht dann folgendermaßen aus: /* Programm: FensterOhneInhalt.java * Erzeugt ein einfaches Swing-Fenster auf dem Bildschirm */ import javax.swing.*; 1 2 public class FensterOhneInhalt extends JFrame { // Konstruktor public FensterOhneInhalt(){ // Hier werden später Komponenten hinzugefügt } public static void main(String[] args) { // Fenster-Objekt erzeugen FensterOhneInhalt fenster = new FensterOhneInhalt(); 3 // Attribute des Fensters setzen fenster.setTitle("Mein erstes Swing-Fenster"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Die Klasse FensterOhneInhalt erbt jetzt von JFrame. Wir definieren auf diese Weise unsere eigene Fenster-Klasse. 2 Die Klasse ist mit einem Konstruktor ausgestattet, der zunächst noch einen leeren Rumpf aufweist. Dort werden später die Komponenten des Fensters eingefügt. 3 In der main-Methode arbeiten wir nicht mehr mit einem JFrame-Objekt, sondern mit einem Objekt der selbstdefinierten Klasse FensterOhneInhalt. Grundsätzlich könnte die mainMethode auch in einer anderen Klasse definiert sein. Der Einfachheit halber ist sie noch in die Klasse FensterOhneInhalt gepackt. © C. Endreß 3 / 33 02/2006 14. Graphische Oberflächen 14.3 Grundsätzliches zum Aufbau graphischer Oberflächen Der Aufbau einer graphischen Benutzungsoberfläche erfolgt nach einem hierarchischen Baukastenprinzip. Aus einer vorgegebenen Menge sogenannter Komponenten wählt man Bausteine für die Oberfläche aus. Diese Bausteine werden in Container-Komponenten angeordnet, mit denen ein Basis-Container wie z.B. ein Fenster bestückt wird. In der Java-Klassenbibliothek finden sich alle benötigten Klassen zum Aufbau einer graphischen Oberfläche: Grundkomponenten: einfache Oberflächenelemente wie z.B. Beschriftungen (Labels), Knöpfe (Buttons), Auswahlfelder oder Klapptafeln. Container: Komponenten, die selbst wieder Komponenten enthalten können. Aufbau von Swing-Fenstern Ein bedeutender Unterschied zwischen AWT- und Swing-Fenstern besteht in ihrer Komponentenstruktur. Während die Komponenten eines AWT-Fensters direkt auf dem Fenster platziert werden, besitzt ein Swing-Hauptfenster eine einzige Hauptkomponente, die alle anderen Komponenten aufnimmt. Die Hauptkomponente eines Swing-Fensters wird als RootPane (Wurzelfeld, Basisfeld) bezeichnet und ist vom Typ JRootPane. Eine RootPane enthält folgende Komponenten: eine normalerweise unsichtbare GlassPane, eine sichtbare LayeredPane mit einem optionalen Menübalken (MenuBar) und einem Inhaltsfeld (ContentPane) MenuBar ContentPane GlassPane LayeredPane Beim Anlegen eines Fensters wird die RootPane mit den darin enthaltenen Schichten GlassPane, LayeredPane und ContentPane automatisch erzeugt. Die Menüleiste bleibt standardmäßig leer. Alle Komponenten des Anwendungsfensters werden zur ContentPane, dem Inhaltsfeld, hinzugefügt. Auf die Bestandteile eines Swing-Fensters kann mit den folgenden Operationen zugegriffen werden: public JLayeredPane getLayeredPane() liefert die LayeredPane, die von der RootPane benutzt wird public JMenuBar getJMenuBar() liefert den Menübalken der LayeredPane public Container getContentPane() liefert die das Inhaltsfeld public Component getGlassPane() liefert die aktuelle GlassPane © C. Endreß 4 / 33 02/2006 14. Graphische Oberflächen 14.4 Ein Fenster mit Text In folgenden Beispiel wird ein leeres Fenster mit einem kleinen Text gefüllt. Dazu verwenden wird die Swing-Komponente JLabel, die einen Schriftzug oder ein Icon enthalten kann. 1 2 3 4 5 6 7 /* Programm: FensterMitText.java * Erzeugt ein einfaches Swing-Fenster mit Text-Label */ import java.awt.*; import javax.swing.*; public class FensterMitText extends JFrame { Container c; JLabel beschriftung; // Konstruktor public FensterMitText(){ c = getContentPane(); c.setLayout(new FlowLayout()); beschriftung = new JLabel("Label-Text im Fenster"); c.add(beschriftung); } 8 public static void main(String[] args) { // Fenster-Objekt erzeugen FensterMitText fenster = new FensterMitText(); // Attribute des Fensters setzen fenster.setTitle("Fenster mit Text-Label"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Die Klasse Container, die für die ContentPane des Fensters benötigt wird, ist eine Klasse aus dem Package java.awt, das deshalb importiert wird. 2 Zunächst vereinbaren wir in der Klasse FensterMitText zwei Instanzvariablen, die im Konstruktor initialisiert werden. Die Variable c vom Typ Container benötigen wir, um eine Referenz auf den Container (in diesem Fall die ContentPane) unseres Fenster-Objekts zu speichern. Denn wir dürfen, wie oben bereits erwähnt, keine Komponenten direkt einem JFrame-Objekt hinzufügen, sondern müssen diese in das Inhaltsfeld des Fensters einfügen. 3 Die Variable beschriftung ist eine Referenz auf ein Objekt von Typ JLabel. Wir benötigen sie, um den Text darzustellen. 4 Die Referenz auf den Container des Fensters wird mit der Methode getContentPane festgelegt. Das Fenster erbt diese Methode von der Klasse JFrame. 5 Mit der Methode setLayout des Container-Objekts legen wir das Layout des Containers fest. Das Layout FlowLayout legt ein „fließendes“ Layout fest, das es dem Inhaltsfeld erlaubt, die Komponenten abhängig von der aktuellen Größe des Fensters fließend anzuordnen. © C. Endreß 5 / 33 02/2006 14. Graphische Oberflächen 6 Erzeugen des JLabel-Objekts beschriftung mit dem Textinhalt, der dem Konstruktor übergeben wird. 7 Die Komponente beschriftung muss der ContentPane des Fensters hinzugefügt werden. Dieses erfolgt, indem wir der Instanzmethode add des Containers c die Objekt-Variable beschriftung als Parameter übergeben. 8 Die main-Methode ist gegenüber dem Beispiel FensterOhneInhalt nahezu unverändert – abgesehen von der Erzeugung des Fensterobjekts, das jetzt die Klasse FensterMitText verwendet. 14.5 AWT- und Swing-Klassenbibliothek im Kurzüberblick Die folgende Abbildung stellt auszugsweise die Hierarchie der wichtigsten AWT- und Swing-Klassen dar. Die Namen aller Swing-Klassen beginnen mit einem J. Component Container Panel Windows Applet Dialog Frame JDialog JFrame JApplet JWindows Top-Level-Container JComponent JLabel JList JComboBox JPanel weitere SwingKomponenten An oberster Stelle steht die abstrakte Klasse Component, die als Oberklasse aller AWT- und SwingKlassen Basismethoden zur Verfügung stellt, die allen graphischen Komponenten gemeinsam sind. Von Component abgeleitet ist die Klasse Container, die Basisklasse für alle Container-Klassen ist. © C. Endreß 6 / 33 02/2006 14. Graphische Oberflächen Als Basis einer graphischen Benutzungsoberfläche mit Swing verwendet man einen sogenannten TopLevel-Container, der alle weiteren Komponenten und Untercontainer der Oberfläche aufnimmt. Top-Level-Container der Swing-Klassen JFrame Standardfenster mit Rahmen, Titelleiste und optionalem Menübalken JWindow Fenster ohne Rahmen, Titelleiste und Menübalken JDialog Modale und nichtmodale Dialoge, die auch als Unterfenster verwendet werden können JApplet Container für Swing-Element in einem Applet Diese 4 Top-Level-Container sind im Gegensatz zu allen anderen Swing-Klassen nicht leichtgewichtig, sondern müssen durch das jeweilige Betriebssystem dargestellt werden. Alle leichtgewichtigen Swing-Komponenten (JLabel, JButton usw.) sind Unterklassen der abstrakten Klasse JComponent. Warnung! Niemals AWT- und Swing-Klassen in einem Fenster mischen, da dies zu unvorhersehbaren Effekten führen kann! 14.6 Layout-Manager Layout-Manager legen die Anordnung der verschiedenen Komponenten in einem Container fest. Dabei verteilen Layout-Manager den Gesamtplatz der Container-Fläche abhängig von den eingepflegten Komponenten, wobei je nach Layout teilweise Zwischenraum eingefügt wird oder Komponenten in ihrer Größe angepasst bzw. gar nicht dargestellt werden. Jeder AWT- und Swing-Container besitzt einen voreingestellten Layout-Manager. Die Verwendung eines Layout-Managers ist nicht zwingend erforderlich. Wenn Sie dem Layout-Manager den Wert null zuweisen, können Komponenten durch die Angabe exakter Größen und Positionen in Containern angeordnet werden. Dazu können Methoden wie setSize und setLocation verwendet werden. Allerdings lassen sich solche Oberflächen schlechter portieren und anpassen. Die am häufigsten verwendeten Layout-Manager stellt die folgende Tabelle vor: Layout-Manager BorderLayout © C. Endreß Beschreibung Die Containerfläche wird in die fünf Gebiete „North“, „South“, „East“, „West“ und „Center“ eingeteilt. In jedes dieser Gebiete kann eine Komponente eingefügt werden. Die Größe der Komponenten im Norden und Süden wird durch ihre Höhe, die der Komponenten im Westen und Osten durch ihre Breite bestimmt. Die Größe des zentralen Gebiets kann je nach Größe des Containers variieren. (DefaultLayout der Klasse JFrame) 7 / 33 North West Center East South 02/2006 14. Graphische Oberflächen FlowLayout Die Komponenten werden „fließend“ von links nach rechts in Zeilen angeordnet. Die Zeilen werden von oben nach unten gefüllt und können linksbündig, zentriert (standardmäßig) und rechtsbündig angeordnet werden. Nummer 1 Nummer 2 Nummer 3 Nummer 4 Nummer 5 Nummer 6 GridLayout null Erzeugt alle Komponeten in gleicher Größe und ordnet sie in einem Gitter mit angegebenen Ausmaßen an. Die Anzahl der Zeilen und Spalten werden beim Konstruktoraufruf des Layout-Manager festgelegt. Nr.1 Nr. 2 Nr. 3 Nr. 4 Nr. 5 Nr. 6 Nr. 7 Nr. 8 Nr. 9 Nr. 10 Die Komponenten werden nicht vom Layout-Manager verwaltet. Position und Größe muss für jede Komponente explizit mit geeigneten Methoden wie setLocation, setSize oder setBounds gesetzt werden. 14.7 Einige Grundkomponenten Alle Grundkomponenten sind Unterklassen der abstrakten Klasse JComponent, die am Anfang der SwingKomponenten-Hierarchie steht. Aufgrund der Mächtigkeit der Swing-Bibliothek können an dieser Stelle nur einige ausgewählte Komponenten besprochen werden. Die folgende Tabelle liefert einen kurzen Überblick über wichtige Grundkomponenten. Schaltflächen und Optionsschalter JButton Eine Schaltfläche bzw. ein Taster auf dem Text, ein Bild oder beides angezeigt wird. JToggleButton Ein Schalter auf dem Text, ein Bild oder beides angezeigt wird. Der Schalter kann an- oder ausgeschaltet bzw. selektiert oder nicht selektiert sein. Unterklassen sind JCheckBox und JRadioButton. JCheckBox Ein Schalter für die Auswahl/Anzeige von Optionen, der sich nicht gegenseitig ausschließen (Mehrfachoptionen). JRadioButton Ein Schalter für die Auswahl/Anzeige von Optionen, die sich gegenseitig ausschließen (Einfachoptionen). Textanzeige und Textbearbeitung JLabel Eine einfache Komponente, um Text, ein Bild oder beides anzuzeigen. JTextField Eine Komponente zur Anzeige, Eingabe und Bearbeitung einer einzelnen Textzeile. JTextArea Eine Komponente zur Anzeige, Eingabe und Bearbeitung von mehrzeiligem Text. Einfache Dialoge JOptionPane © C. Endreß Eine komplexe Komponente, mit der einfache und häufig benötigte Dialoge angezeigt werden können. 8 / 33 02/2006 14. Graphische Oberflächen Auswahllisten JComboBox Eine Kombination aus einem Texteingabefeld und einer Auswahlliste. Der Benutzer kann einen Wert eingeben oder aus einer Liste auswählen. JList Zeigt eine Auswahlliste an. Die Elemente sind üblicherweise Zeichenketten oder Bilder. Mehrfachauswahl von Elementen ist möglich. Einfache Container Ein Container, in den andere Komponenten platziert werden können. Wird meistens mit einem geeigneten Layout-Manager verwendet. JPanel 14.7.1 Die Klasse JLabel Ein Label kann zur Darstellung von Text und Bildern verwendet werden, wobei auch beides kombiniert werden kann. Für Text und Bild kann die horizontale und vertikale Ausrichtung innerhalb des Labels festgelegt werden. Für Text ist die standardmäßige horizontale Ausrichtung „linksbündig“, für Bilder ist sie „zentriert“. Vertikal wird standardmäßig jeweils „zentriert“ ausgerichtet. Es stehen verschiedene Konstruktoren zur Verfügung, denen sowohl Text als auch Bilder oder ein Bild und Text übergeben werden können. Bild hinzufügen: Einem Label kann man ein Bildobjekt hinzufügen, das das Interface Icon implementiert. In der Regel geschieht das durch ein Objekt der Klasse ImageIcon. Der Konstruktor public ImageIcon (String fileName) erzeugt ein ImageIcon-Objekt aus dem Bild der Datei fileName. Beispiel: Das Fenster vom Typ JFrame erhält ein Label mit Bild und zugehörigem Text. /* Programm: FensterMitLabel.java * Erzeugt ein einfaches Swing-Fenster mit einem * Label, das Text und ein Icon enthält */ import java.awt.*; import javax.swing.*; 1 2 public class FensterMitLabel extends JFrame { private Container c; private JLabel einLabel; 3 // Konstruktor public FensterMitLabel(){ c = getContentPane(); c.setLayout(new BorderLayout()); // Bildobjekt erzeugen © C. Endreß 9 / 33 02/2006 14. Graphische Oberflächen 4 Icon bild = new ImageIcon("duke_flip.gif"); // Label mit Text und Bild beschriften einLabel = new JLabel("Let's swing!",bild, JLabel.CENTER); einLabel.setHorizontalTextPosition(JLabel.CENTER); einLabel.setVerticalTextPosition(JLabel.BOTTOM); c.add(einLabel); 5 6 7 } public static void main(String[] args) { // Erzeuge ein Fenster-Objekt FensterMitLabel fenster = new FensterMitLabel(); // Attribute des Fensters setzen fenster.setTitle("Label mit Bild und Text"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Deklaration des Containers für die ContentPane des Fensters. 2 Deklaration des Label-Objekts einLabel vom Typ JLabel. 3 Der Layout-Manager für den Container wird auf BorderLayout gesetzt. 4 Mit dem Konstruktoraufruf der Klasse ImageIcon wird ein Bildobjekt vom Typ Icon erzeugt. Der Dateiname der Bilddatei wird als String angegeben. Dabei ist zu beachten: "duke_flip.gif" Die Bilddatei liegt in demselben Verzeichnis wie das Programm. Funktioniert nicht aus JBuilder und Eclipse heraus, sondern nur über das Konsolenkommando java "oberflaechen/duke_flip.gif" Programmdatei und Bilddatei liegen im Package oberflaechen. Funktioniert nicht aus bei JBuilder und Eclipse heraus, sondern nur über das Konsolenkommando java "D:/DukeIcons/duke_flip.gif" absolute Pfadangabe der Bilddatei, Slashes (/) verwenden Icon bild = new ImageIcon(getClass().getResource("/oberflaechen/duke_flip.gif")); Programmdatei und Bilddatei liegen im Package oberflaechen. Funktioniert auch aus JBuilder und Eclipse heraus. 5 Das Label wird mit Text und Icon initialisiert, wobei die horizontale Ausrichtung mit der Konstanten JLabel.CENTER auf zentriert gesetzt wird. 6 Die beiden folgenden Anweisungen positionieren den Text zentriert unter dem Bild. 7 Das Label-Objekt wird der ContentPane des Fensters hinzugefügt. © C. Endreß 10 / 33 02/2006 14. Graphische Oberflächen 14.7.2 Schaltflächen mit JButton und JToggleButton Beispiel Taster 1 ist ein Objekt der Klasse JButton. Schalter 1 und Schalter 2 sind Objekte der Klasse JToggleButton. Schalter 2 ist bereits zum Programmstart selektiert, steht also auf „an“. Der kleine bläuliche Rahmen innerhalb von Taster 1 zeigt an, dass dieser Taster gerade den Fokus besitzt. Er kann daher auch ohne Maus durch Drücken der Leertaste betätigt werden. Mit der Tabulator-Taste kann der Fokus an den nächsten Schalter weitergegeben werden. Beim Betätigen von Taster 1 verändert sich dessen Darstellung. Der Hintergrund wird dunkelgrau eingefärbt. Nach dem Loslassen ist Taster 1 wieder deaktiviert. Schalter 1 verändert seine Darstellung, sobald er gedrückt bzw. selektiert wird. Dieser Zustand beleibt erhalten, wenn der Schalter losgelassen wird. Erneutes Betätigen von Schalter 1 stellt den ursprünglichen Zustand wieder her. /* Programm: FensterMitButtons.java * Erzeugt ein einfaches Swing-Fenster mit * einem Taster und zwei Schaltern */ import java.awt.*; import javax.swing.*; 1 public class FensterMitButtons extends JFrame { private Container c; private JButton button1; private JToggleButton toBtn1, toBtn2; // Konstruktor public FensterMitButtons(){ c = getContentPane(); c.setLayout(new FlowLayout()); 2 button1 = new JButton("Taster 1"); toBtn1 = new JToggleButton("Schalter 1"); toBtn2 = new JToggleButton("Schalter 2"); 3 button1.setFont(new Font("SansSerif", Font.ITALIC, 24)); toBtn1.setFont(new Font("SansSerif", Font.ITALIC, 24)); toBtn2.setFont(new Font("SansSerif", Font.ITALIC, 24)); © C. Endreß 11 / 33 02/2006 14. Graphische Oberflächen 4 toBtn2.setSelected(true); 5 c.add(button1); c.add(toBtn1); c.add(toBtn2); } public static void main(String[] args) { FensterMitButtons fenster = new FensterMitButtons(); fenster.setTitle("Taster und Schalter"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Deklaration der Objektvariablen der Schaltflächen (ein Button, zwei Toggle-Buttons) 2 Initialisieren der Schaltflächen: Die Beschriftungen der Schaltflächen wird dem Konstruktor als Parameter übergeben, kann aber auch nachträglich mit der Instanz-Methode setText zugefügt werden. 3 Die Beschriftung der Schaltflächen-Objekte wird hier in ihren Eigenschaften verändert. 4 Der Toggle-Button toBtn2 wird vorselektiert. Dadurch ist toBtn2 bereits beim Programmstart „angeschaltet“. 5 Die Komponenten werden der ContentPane hinzugefügt. 14.7.3 Optionsschalter JCheckBox Mit Checkboxes kann eine Auswahl für Mehrfachoptionen realisiert werden. D.h. es können mehrere Optionen gleichzeitig selektiert werden. Die Selektion einer Checkbox wird durch ein Häkchen angezeigt. /* Programm: CheckBoxes.java * Erzeugt ein einfaches Swing-Fenster mit * drei Checkboxes */ import java.awt.*; import javax.swing.*; 1 public class CheckBoxes extends JFrame { private Container c; private JCheckBox cbKursiv, cbFett, cbUnterstrichen; // Konstruktor public CheckBoxes() { c = getContentPane(); c.setLayout(new FlowLayout()); 2 © C. Endreß cbKursiv = new JCheckBox("Kursiv"); cbFett = new JCheckBox("Fett"); cbUnterstrichen = new JCheckBox("Unterstrichen"); 12 / 33 02/2006 14. Graphische Oberflächen 3 cbFett.setSelected(true); cbUnterstrichen.setSelected(true); 4 c.add(cbFett); c.add(cbKursiv); c.add(cbUnterstrichen); } public static void main(String[] args) { CheckBoxes fenster = new CheckBoxes(); fenster.setTitle("CheckBoxes"); fenster.setSize(300, 100); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Deklaration von drei JCheckBox-Objekten 2 Initialisierung der drei JCheckBox-Objekte: Die Beschriftung der Checkbox wird dem Konstruktor als Parameter übergeben. 3 Die Checkboxes cbFett und cbUnterstrichen werden mit der Methode setSelected bereits zum Programmstart selektiert. Mit der Methode isSelected kann abgefragt werden, ob ein JCheckBox-Objekt selektiert ist. (Rückgabewert: true = selektiert, false = nicht selektiert). 4 Die JCheckBox-Objekte werden der ContentPane hinzugefügt. JRadioButton Radiobuttons können ähnlich wie Checkboxes die Zustände „selektiert“ und „nicht selektiert“ annehmen. Der „selektiert“-Zustand wird durch einen Punkt gekennzeichnet. Normalerweise werden JRadiobutton-Objekte mit einem ButtonGroup-Objekt zu einer Gruppe zusammengefasst, in der immer höchstens ein Radiobutton aktiviert sein kann (Einfachoption). ButtonGroup-Objekte können auch zur Gruppierung von JToggleButton-Objekten eingesetzt werden. /* Programm: RadioButtons.java * Erzeugt ein einfaches Swing-Fenster mit * drei RadioButtons, die in einer ButtonGroup gruppiert werden. */ import java.awt.*; import javax.swing.*; 1 2 public class RadioButtons extends JFrame { private Container c; private JRadioButton rbFestGeld, rbSpar, rbGiro; private ButtonGroup bgKontoAuswahl; // Konstruktor public RadioButtons() { c = getContentPane(); c.setLayout(new FlowLayout()); © C. Endreß 13 / 33 02/2006 14. Graphische Oberflächen 3 rbGiro = new JRadioButton("Girokonto"); rbSpar = new JRadioButton("Sparkonto"); rbFestGeld = new JRadioButton("Festgeldkonto"); 4 rbGiro.setSelected(true); 5 6 bgKontoAuswahl = new ButtonGroup(); bgKontoAuswahl.add(rbGiro); bgKontoAuswahl.add(rbSpar); bgKontoAuswahl.add(rbFestGeld); 7 c.add(rbGiro); c.add(rbSpar); c.add(rbFestGeld); } public static void main(String[] args) { RadioButtons fenster = new RadioButtons(); fenster.setTitle("RadioButtons"); fenster.setSize(300, 100); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Deklaration der JRadioButton-Objekte 2 Deklaration des ButtonGroup-Objekts bgKontoAuswahl, in dem die drei JRadioButtonObjekte später zusammengefasst werden. 3 Initialisierung der drei JRadioButton-Objekte: Dem JRadioButton-Konstruktor wird die Beschriftung des jeweiligen JRadioButton-Objekts als Parameter übergeben. Die Beschriftung kann auch nachträglich mit der Methode setText eingerichtet werden. 4 Das JRadioButton-Objekt rbGiro wird selektiert. Der Selektionszustand eines JRadioButton-Objekts kann wie bei JCheckBox-Objekten mit der Methode isSelected abgefragt werden. 5 Das ButtonGroup-Objekt bgKontoAuswahl wird initialisiert. 6 Die JRadioButton-Objekte werden der ButtonGroup hinzugefügt. Die Zusammenfassung zu einer ButtonGroup hat nur logische Bedeutung und wirkt sich nicht auf die graphische Darstellung und Anordnung der Radiobuttons auf der Oberfläche aus. 7 Die JRadioButton-Objekte müssen einzeln dem Oberflächen-Container hinzugefügt werden. 14.7.4 Klassen für die Ein-/Ausgabe von Texten Java stellt verschiedene Klassen zur Eingabe und Ausgabe von Texten bereit. In der abstrakten Klasse JTextComponent, von der alle Textkomponenten erben, werden die Basismethoden definiert. getText() liefert den kompletten Text der Komponente getSelectedText() liefert den gerade markierten Text der Komponente setText(String s) setzt den Text der Komponente auf den Inhalt s setEditable(boolean b) b = true => Modus „editierbar” b = false => Modus „nicht editierbar“ © C. Endreß 14 / 33 02/2006 14. Graphische Oberflächen Die Methode getText() liefert den Text der Komponente als String zurück. Zum Verarbeiten von Zahlenwerten ist es erforderlich den String mittels sog. Wrapper-Klassen (Integer, Double usw.) in den gewünschten numerischen Datentyp (int, double usw.) umzuwandeln. Umwandlung einer Zeichenkette aus einem JTextField-Objekt in eine ganze Zahl: int ganzeZahl = (Integer.valueOf(eingabe.getText())).intValue(); 1 2 3 3 :Integer 2 :String 1 eingabe:JTextField 12 wert = 12 “12“ text = “12“ analog erfolgt die Umwandlung einer Zeichenkette in eine Fließkommazahl: double dezimalZahl = (Double.valueOf(eingabe.getText())).doubleValue(); Umwandlung einer Zahl in eine Zeichenkette: String intZahlAlsText = String.valueOf(ganzeZahl); String doubleZahlAlsText = String.valueOf(doubleZahl); Mit Objekten der Klassen JTextField und JPasswordField können einzeilige Texte verarbeitet werden. Während ein JTextField-Objekt die Textzeile lesbar darstellt, wird diese in einem JPasswordField-Objekt unlesbar mittels einer entsprechenden Anzahl von Ersatz-Zeichen, sogenannten „Echo-Zeichen“, dargestellt. setHorizantolAlignment(int alignment) setzt die horizontale Ausrichtung Objekte der Klasse JTextArea dienen zur Verarbeitung mehrzeiliger Texte. getLineCount() liefert die Anzahl der Zeilen setLineWrap(boolean wrap) wrap = true => aktiviert Zeilenumbruch wrap = false => deaktiviert Zeilenumbruch setWrapStyleWord(boolean b) b = true => aktiviert wortweisen Zeilenumbruch b = false => deaktiviert wortweisen Zeilenumbruch Falls in einem JTextArea-Objekt so viel Text eingegeben wird, dass die vorgegebene Größe des Objekts zur Darstellung des Texts nicht ausreicht, wird der überschüssige Text nicht mehr angezeigt. Um den angezeigten Ausschnitt zu verschieben, bettet man die JTextAreaKomponente in eine JScrollPane-Komponente ein, die einen Schieberegler bereitstellt. © C. Endreß 15 / 33 02/2006 14. Graphische Oberflächen Daneben gibt es noch die Klassen JEditorPane und JTextPane, die auch formatierte Texte wie z.B. HTML-Dokumente verarbeiten können. 14.7.5 Oberflächen strukturieren Die Klasse JPanel gehört zur Gruppe der Container. Sie kann selbst Komponenten enthalten und dient hauptsächlich der Strukturierung von Oberflächen. Als Layout-Manager ist FlowLayout voreingestellt. Beispiel Das Beispiel zeigt ein Fenster, dessen Oberfläche mit drei Panel strukturiert wurde, die im Norden, Süden und Zentrum des JFrames platziert wurden. Die Panels werden randlos dargestellt und sind daher nicht erkennbar. Die blauen Rahmen sind nachträglich eingefügt und deuten nur die Positionen der Panels an. Mit der Methode setBorder kann für Panels genauso wie für andere Komponenten auch ein Rand eingestellt werden. Panel 1 (FlowLayout) Panel 2 (FlowLayout) Panel 3 (GridLayout) /* Programm: FensterMitPanles.java * Erzeugt ein Swing-Fenster, das mit * drei Panels strukturiert wird. */ import java.awt.*; import javax.swing.*; 1 public class FensterMitPanels extends JFrame { private Container c; private JPanel jp1, jp2, jp3; // Konstruktor public FensterMitPanels() { c = getContentPane(); 2 jp1 = new JPanel(); jp2 = new JPanel(); jp3 = new JPanel(new GridLayout(2, 3)); 3 // Vier Tasten in Panel 1 einfuegen for (int i = 1; i <= 4; i++) jp1.add(new JButton("Taste " + i)); 4 5 // Bildobjekt erzeugen Icon bild = new ImageIcon(getClass().getResource("/oberflaechen/duke_flip.gif")); // Bild dreimal in Panel 2 einfuegen © C. Endreß 16 / 33 02/2006 14. Graphische Oberflächen 6 for (int i = 0; i < 3; i++) jp2.add(new JLabel(bild)); // 6 Checkboxes in Panel 3 setzen for (int i = 1; i <= 6; i++) jp3.add(new JCheckBox("Auswahl " + i)); 7 8 c.add(jp1, BorderLayout.NORTH); c.add(jp2, BorderLayout.CENTER); c.add(jp3, BorderLayout.SOUTH); } public static void main(String[] args) { FensterMitPanels fenster = new FensterMitPanels(); fenster.setTitle("Fenster mit Panels"); fenster.setSize(350, 200); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Drei JPanel-Objekte deklarieren. 2 Initialisieren der JPanel-Objekte. Das Standardlayout ist FlowLayout. 3 Das JPanel-Objekt jp3 erhält ein GridLayout mit 2 Zeilen und 3 Spalten. In diesem Panel werden später 6 Checkboxen untergebracht. 4 4 Schaltflächen von Typ JButton werden dem Panel jp1 hinzugefügt. Wegen des FlowLayout der JPanel-Komponente erscheinen die Buttons in einer Reihe. 5 Erzeugen eines Icon-Objekts für die Labels des zweiten Panels. 6 Drei JLabel-Objekte werden jeweils mit dem Icon-Objekt bild erzeugt und dem Panel jp2 zugefügt. 7 Für Panel jp3 haben wir ein GridLayout mit 2 Zeilen und 3 Spalten eingestellt. Die 6 JCheckBox-Objekte werden zeilenweise in das Gitter dieses Panels gesetzt. 8 Nachdem die visuellen Komponenten den drei JPanel-Objekten zugefügt wurden, müssen die Panels noch auf der ContentPane des Fensters angeordnet werden. Die jeweiligen Positionen werden der add-Methode als Parameter (z.B. BorderLayout.NORTH) übergeben. 14.8 Ereignisverarbeitung 14.8.1 Einführung Programme mit graphischer Benutzungsoberfläche bedient ein Benutzer durch Tastatureingaben und Mausklicks. Diese Benutzeraktivitäten lösen Ereignisse aus, die vom jeweiligen Betriebssystem bzw. GUISystem an das Programm weitergegeben werden. Das Programm wird dabei über alle Arten von Ereignissen und Zustandsänderungen informiert und reagiert in seinem Ablauf darauf. Beispiele für mögliche Ereignisse bzw. Zustandsänderungen: Mausklicks, Mausbewegungen Tastatureingaben Veränderungen an Größe oder Lage bzw. Schließen von Fenstern © C. Endreß 17 / 33 02/2006 14. Graphische Oberflächen Timerintervalle Ankunft von Datenpaketen in einem Netzwerk usw. Programme mit einer graphischen Benutzungsoberfläche sind ereignisgesteuert. Der Programmablauf wird von Ereignissen und Zustandsänderungen bestimmt, auf die das Programm reagiert. 14.8.2 Ereignisverarbeitung in Java Zuerst die Theorie ... Ereignisübermittlung in der „realen Welt“: Ereignis Empfänger Quelle In Java: Ereignisquelle (Event-Source): ein Objekt, das ein Ereignis auslöst (z.B. ein Button) Ereignis (Event): ein Objekt einer Ereignis-Klasse (z.B. ein Mausklick) Ereignisempfänger/-abhörer (Event-Listener): ein Objekt, das auf Ereignisse reagiert (z.B. eine bestimmte Aktion startet) Delegation Event Model: Ereignisquellen lösen Ereignisse aus. Die Ereignisabhörer werden von der Ereignisquelle über eingetretene Ereignisse informiert. Damit ein Empfänger tatsächlich Ereignisse von einer Ereignisquelle empfangen kann, muss er zuvor bei dieser registriert werden. Eine Ereignisquelle kann i.d.R. an eine beliebige Anzahl von Ereignisabhörern Ereignisse senden. Ein Ereignisabhörer kann bei beliebig vielen Ereignisquellen angemeldet sein. Ereignisquelle 1 Ereignisquelle 2 Ereignis Ereignis Empfänger 1 Empfänger 2 Ereignis Ereignisquelle 3 © C. Endreß Ereignis 18 / 33 Empfänger 3 02/2006 14. Graphische Oberflächen Beispiel: Button und zugehöriger Ereignisabhörer ActionEvent Button actionPerformed Ereignisquelle Ereignis (Nachricht) ActionListener Ereignisabhörer ... dann die Praxis Beispiel: Farbwechsel In einer minimalistischen graphischen Oberfläche, die nur einen Knopf enthält, soll sich durch Drücken des Knopfs die Hintergrundfarbe des Fensters zufällig verändern. Lösungsschritte: Gestalten der Oberfläche: Ein Fenster, das von der Klasse JFrame abgeleitet wird und ein JButton-Objekt enthält. Ereignisverarbeitung: Die Ereignisquelle ist ein Button (JButton-Objekt). Durch Drücken des Buttons wird ein Ereignis ausgelöst. Um auf dieses Ereignis reagieren zu können, müssen wir einen Ereignisempfänger erzeugen und diesen bei der Ereignis-Quelle (hier der Button) anmelden. Für die Erzeugung eines Ereignisempfängers benötigen wir eine Klasse, die über alle erforderlichen Eigenschaften verfügt. Dazu müssen wir wissen, auf welche Art von Ereignis der Empfänger ansprechen soll. Im Fall eines gedrückten Buttons handelt es sich um ein Action-Ereignis, d.h. um ein Objekt der Klasse ActionEvent. Wir müssen also eine Empfängerklasse schreiben, deren Objekte wissen, was beim Empfang eines ActionEvent-Ereignisses zu tun ist. Um dies zu gewährleisten, müssen wir uns an gewisse „Regeln“ halten, die in einem Interface festgelegt sind. Für ActionEvent-Ereignisse ist das Interface ActionListener zuständig, das deshalb in unserer Empfänger-Klasse implementiert werden muss. © C. Endreß 19 / 33 02/2006 14. Graphische Oberflächen Das ActionListener-Interface „weiß“, dass für die Bearbeitung eines ActionEvent-Objekts automatisch die Methode actionPerformed aufgerufen wird. Diese Methode müssen wir in unserer Empfänger-Klasse in der von uns gewünschten Weise implementieren. D.h. wir werden im MethodenRumpf ausformulieren, dass bei einem Druck auf den Button die Hintergrundfarbe des Containers zufällig verändert wird. Das ActionListener-Interface enthält im übrigen keine weiteren Methoden. 1 /* Programm: Farbwechsel.java * Erzeugt ein Swing-Fenster mit einem Button. Die * Hintergrundfarbe des Fensters wechselt bei jedem * Betätigen des Buttons auf eine zufällige Farbe. */ import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Farbwechsel extends JFrame{ Container c; JButton button; // Konstruktor public Farbwechsel(){ c = getContentPane(); 2 3 // Button erzeugen und dem Container hinzufügen button = new JButton("Hintergrundfarbe wechseln"); c.add(button, BorderLayout.NORTH); 4 5 // Listener-Objekt erzeugen und beim Button anmelden ButtonListener bListener = new ButtonListener(); button.addActionListener(bListener); } // Innere Klasse ButtonListener class ButtonListener implements ActionListener { public void actionPerformed(ActionEvent e){ // RGB-Farbe des Containerhintergrunds zufaellig aendern float zufallR = (float) Math.random(); float zufallG = (float) Math.random(); float zufallB = (float) Math.random(); Color farbe = new Color(zufallR, zufallG, zufallB); c.setBackground(farbe); // Zugriff auf c moeglich, da // ButtonListener innere Klasse } } 6 7 8 9 public static void main(String[] args) { Farbwechsel fenster = new Farbwechsel(); fenster.setTitle("Farbwechsel"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Im Paket java.awt.event befinden sich alle Ereignis-Klassen. 2 Erzeugen eines JButton-Objekts mit der Beschriftung „Hintergrundfarbe wechseln“. 3 Das JButton-Objekt button wird dem Inhaltsfeld des Fensters zugefügt und im oberen Fensterteil positioniert. © C. Endreß 20 / 33 02/2006 14. Graphische Oberflächen 4 Erzeugen eines Ereignisempfänger-Objekts mit Namen bListener, das auf Button-Ereignisse reagiert. Die Ereignisempfänger-Klasse ButtonListener wird weiter unten deklariert. 5 Das Empfänger-Objekt bListener wird mit der Methode addActionListener bei der Ereignisquelle button registriert. Die Methode addActionListener wird von der Klasse JButton bereit gestellt und erwartet einen Parameter vom Typ ActionListener, d.h. ein Objekt einer Klasse, die das Interface ActionListener implementiert. 6 Deklarieren der Ereignisempfänger-Klasse ButtonListener als innere Klasse der Klasse Farbwechsel. Damit Objekte der Klasse ButtonListener auf ActionEvents, die ein JButton-Objekt (in diesem Fall das Objekt button) auslöst, reagieren können, muss die Klasse das Interface ActionListener implementieren. Beim Drücken von button wird die Botschaft actionPerformed an das Objekt bListener gesendet. Der Compiler erzeugt nicht nur für die Klasse Farbwechsel, sondern auch für deren innere Klasse ButtonListener eine Bytecode-Datei, die er mit dem Namen Farbwechsel$ButtonListener.class versieht. 7 Es werden drei Zufallszahlen mit float-Werten zwischen 0 und 1 erzeugt. Die Methode Math.random() liefert double-Werte, so dass die Ergebnisse nach float gecastet werden müssen. 8 Objekte der Klasse Color legen ihre Farbe durch Anteile an Rot, Grün und Blau fest. Man spricht daher auch vom RGB-Farbmodell. Die Rot-, Grün- und Blau-Anteile werden jeweils als int-Werte im Bereich 0 bis 255 oder alternativ als float-Werte im Bereich 0.0 bis 1.0 angegeben. Das Objekt farbe wird mit Zufallswerten initialisiert. 9 Die Hintergrundfarbe des Container c wird auf farbe gesetzt. In der Klasse ButtonListener kann auf c zugegriffen werden, weil ButtonListener eine innere Klasse der Klasse Farbwechsel ist und damit Zugriff auf die Objekte der Klasse Farbwechsel hat. 14.8.3 Programmiervarianten für die Ereignisverarbeitung Es gibt verschiedene Möglichkeiten, das in Java verwendete Modell der Ereignisverarbeitung programmiertechnisch umzusetzen. Man kann dabei prinzipiell 4 Varianten unterscheiden: Die Listener-Klasse wird als innere Klasse realisiert. Die Listener-Klasse wird als anonyme Klasse realisiert. Die Container-Klasse wird selbst zur Listener-Klasse. Die Listener-Klasse wird als separate Klasse realisiert. Diese Varianten werden im folgenden am Beispiel Farbwechsel erläutert. Variante 1: Listener-Klasse als innere Klasse Innere Klassen werden in die Klasse eingeschachtelt, die die Ereignisquelle enthält, und haben Zugriff auf alle Attribute und Operationen ihrer umgebenden Klasse. © C. Endreß 21 / 33 02/2006 14. Graphische Oberflächen In dem vorangegangenen Beispiel sind wir nach diesem Verfahren vorgegangen. Die Klasse Farbwechsel enthält eine innere Klasse ButtonListener, die durch die Implementierung der Schnittstelle ActionListener zur Listener-Klasse wird. Variante 2: Listener-Klasse als anonyme Klasse Anonyme Klassen sind eine Spezialform von inneren Klassen. Sie können quasi an jeder Stelle definiert werden – sogar innerhalb von Methodenaufrufen oder in Wertzuweisungen. Anonyme Klassen zeichnen sich dadurch aus, dass sie keinen eigenen Klassennamen besitzen. Man definiert sie nach folgendem Schema: new <NameDerSuperklasseOderDesInterfaces>() { // Anweisungsblock } Die Definition steht in direktem Zusammenhang mit dem Konstruktoraufruf und definiert eine Klasse, die ein Interface implementiert oder eine Klasse erweitert. Anschließend wird die Definition wieder vergessen. Anonyme Klassen werden hauptsächlich für die Definition von „Wegwerfklassen“ verwendet, also von Klassen, die nur ein einziges Mal im gesamten Programm verwendet werden. Wenn sich die Ereignisverarbeitung so einfach gestaltet wie in unserem Farbwechsel-Programm, in dem lediglich ein einziges Listener-Objekt benötigt wird, kann man darauf verzichten, die Listener-Klasse explizit mit einem Namen zu versehen. Stattdessen erzeugt man das Listener-Objekt mit einer anonymen Klasse. Dabei wird erst unmittelbar beim Erzeugen des Objekts die Struktur der anonymen Klasse festgelegt. Man gibt hinter dem new-Operator den Namen der Superklasse, von der die anonyme Klasse erben soll, oder den Namen des Interfaces, das die anonyme Klasse implementieren soll, an. Mit dieser Technik verkürzt sich das Beispielprogramm: /* Programm: Farbwechsel.java * Listener-Klasse als anonyme Klasse */ import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Farbwechsel extends JFrame{ Container c; JButton button; // Konstruktor public Farbwechsel(){ c = getContentPane(); // Button erzeugen und dem Container hinzufügen button = new JButton("Hintergrundfarbe wechseln"); c.add(button, BorderLayout.NORTH); 1 © C. Endreß // Listener-Objekt erzeugen und beim Button anmelden ActionListener btnListener = new ActionListener() { public void actionPerformed(ActionEvent e){ // Hintergrundfarbe der Containers zufaellig aendern float zufallR = (float) Math.random(); float zufallG = (float) Math.random(); float zufallB = (float) Math.random(); 22 / 33 02/2006 14. Graphische Oberflächen Color farbe = new Color(zufallR, zufallG, zufallB); c.setBackground(farbe); } }; // Ende der anonymen Klassendefinition 2 button.addActionListener(btnListener); } public static void main(String[] args) { Farbwechsel fenster = new Farbwechsel(); fenster.setTitle("Farbwechsel"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Definieren einer anonymen Klasse, die das Interface ActionListener implementiert, bei gleichzeitigem Erzeugen des Listener-Objekt bListener. 2 Listener-Objekt bListener bei der Ereignisquelle button registrieren. Für die anonyme Klasse erzeugt der Compiler eine Bytecode-Datei mit dem Dateinamen Farbwechsel$1.class. Anonyme Klassen werden lediglich nummeriert. Die Zugehörigkeit zur Klasse Farbwechsel wird durch den ersten Teil des Namens deutlich. Variante 3: Container-Klasse als Listener-Klasse Die zuletzt beschriebene Variante lässt sich weiter verkürzen, indem man sogar darauf verzichtet, mittels einer inneren oder anonymen Klasse ein eigenes Listener-Objekt zu erzeugen. Stattdessen verwendet man das Objekt, in dem man sich zu Laufzeit des Programms befindet (also das Objekt der Klasse JFrame), als Listener-Objekt. Dafür muss die Fenster-Klasse selbst das entsprechende Listener-Interface implementieren. /* Programm: Farbwechsel_V3.java * Container-Klasse als Listener-Klasse */ import java.awt.*; import java.awt.event.*; import javax.swing.*; 1 public class Farbwechsel_V3 extends JFrame implements ActionListener{ Container c; JButton button; // Konstruktor public Farbwechsel_V3(){ c = getContentPane(); // Button erzeugen und dem Container hinzufügen button = new JButton("Hintergrundfarbe wechseln"); c.add(button, BorderLayout.NORTH); // eigenes Objekt beim Button als Listener anmelden button.addActionListener(this); 2 } 3 © C. Endreß public void actionPerformed(ActionEvent e){ // Hintergrundfarbe der Containers zufaellig aendern float zufallR = (float) Math.random(); 23 / 33 02/2006 14. Graphische Oberflächen float zufallG = (float) Math.random(); float zufallB = (float) Math.random(); Color farbe = new Color(zufallR, zufallG, zufallB); c.setBackground(farbe); } public static void main(String[] args) { Farbwechsel fenster = new Farbwechsel(); fenster.setTitle("Farbwechsel"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Die Klasse Farbwechsel erbt wie gehabt von JFrame und implementier das Interface ActionListener. Dadurch müssen wir in die Klasse Farbwechsel die Instanzmethode actionPerformed aufnehmen. 2 Das Fenster-Objekt der Klasse Farbwechsel kann nun zur Laufzeit selbst die Rolle des Listeners übernehmen. Daher genügt es, mit der Methode addActionListener einfach die this-Referenz (die Referenz auf das eigene Objekt) bei dem Button-Objekt registrieren zu lassen. 3 Die Methode actionPerformed muss als Instanz-Methode in der Klasse Farbwechsel definiert werden, da Farbwechsel das entsprechende Listener-Interface implementiert. Der Methodenrumpf von actionPerformed unterscheidet sich nicht von den vorhergehenden Beispielen. Variante 4: Listener-Klasse als separate Klasse Diese vierte Variante ermöglicht eine strikte Trennung zwischen der graphischen Oberfläche und der Ereignisverarbeitung. Man lagert die Listener-Klasse vollständig in eine eigenständige Klasse aus. Dabei ist jedoch zu beachten, dass die Listener-Klasse je nach Aufgabenstellung einen Zugriff auf die Ereignis-Quelle, ihren Container oder andere Objekte benötigt. Dieses kann realisiert werden, indem man dem ListenerObjekt die entsprechenden Informationen bzw. Referenzen bereits bei seiner Erzeugung übergibt. Für diese Aufgabe muss natürlich ein spezieller, parametrisierter Konstruktor programmiert werden. In unserem Farbwechsel-Beispiel ergäbe sich für die Listener-Klasse ButtonListener.java folgende Implementierung: /* ButtonListener.java * Eigenstaendige Listener-Klasse */ import java.awt.*; import java.awt.event.*; 1 public class ButtonListener implements ActionListener { Container c; // Referenz auf den zu beeinflussenden Container 2 // Konstruktor public ButtonListener(Container c){ this.c = c; } public void actionPerformed(ActionEvent e){ // Hintergrundfarbe der Containers zufaellig aendern © C. Endreß 24 / 33 02/2006 14. Graphische Oberflächen float zufallR = (float) Math.random(); float zufallG = (float) Math.random(); float zufallB = (float) Math.random(); Color farbe = new Color(zufallR, zufallG, zufallB); c.setBackground(farbe); } } Erläuterungen 1 Wir benötigen einen Zugriff auf den Container, der den auslösenden Button enthält. Dazu wird die Referenz-Variable c vom Typ Container deklariert. 2 Die Referenz-Variable c des Listeners wird mit dem zu beeinflussenden Container initialisiert. /* Programm: Farbwechsel_V4.java * Listener-Klasse als separate Klasse ausführen */ import java.awt.*; import javax.swing.*; public class Farbwechsel_V4 extends JFrame{ Container c; JButton button; // Konstruktor public Farbwechsel_V4(){ c = getContentPane(); // Button erzeugen und dem Container hinzufügen button = new JButton("Hintergrundfarbe wechseln"); c.add(button, BorderLayout.NORTH); // Listener-Objekt erzeugen und beim Button anmelden ButtonListener btnListener = new ButtonListener(c); button.addActionListener(btnListener); 1 2 } public static void main(String[] args) { Farbwechsel_V4 fenster = new Farbwechsel_V4(); fenster.setTitle("Farbwechsel"); fenster.setSize(300, 150); fenster.setVisible(true); fenster.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } } Erläuterungen 1 Beim Erzeugen des Listener-Objekts btnListener muss dem Konstruktor ButtonListener() eine Referenz auf den Container c des Fensters übergeben werden. 2 Das Listener-Objekt btnListener wird bei der Ereignisquelle button mit der Methode addActionListener angemeldet. © C. Endreß 25 / 33 02/2006 14. Graphische Oberflächen Tabelle: Varianten der Ereignisverarbeitung Listener-Klasse als ... Bewertung innere Klasse + Geeignet für kleine Programme + Die Listener-Klasse kann von Adapter-Klassen abgeleitet werden. anonyme Klasse + Geeignet für kleine Programme – Nur ein einziges Listener-Objekt der Listener-Klasse möglich. Container-Klasse + Einfach zu implementieren, da keine weiteren Klassen erforderlich sind. – Keine Trennung zwischen Oberfläche und Ereignisverarbeitung. – Für jeden Ereignistyp ist ein passendes Listener-Interface zu implementieren. Das führt schnell zu vielen leeren Methoden Rümpfen und unübersichtlichen Programmen. Adapter-Klassen können nicht verwendet werden, weil Mehrfachvererbung in Java nicht möglich ist. separate Klasse + Aufgrund der Trennung zwischen Oberflächengestaltung und Ereignisverarbeitung gut geeignet für komplexe Programme. 14.8.4 Listener-Interfaces und Adapter-Klassen Elementare Ereignisse (Low-Level-Ereignisse) sind Ereignisse die z.B. durch Maus oder Taststur ausgelöst werden. Sie werden in der Klasse ComponentEvent und deren Unterklassen behandelt. Semantische Ereignisse sind nicht an ein bestimmtes Interaktionselement gebunden. Das Ereignis ActionEvent kann z.B. von vielen Komponenten ausgelöst werden. Alle Interfaces, die zur Implementierung von Ereignisempfänger-Klassen genutzt werden, sind Subinterfaces von EventListener. Grundsätzlich gibt es zu jeder Ereignis-Klasse ein Interface XxxEvent XxxListener Listener-Interfaces für semantische Ereignisse enthalten lediglich eine einzige zu implementierende Methode. Z.B. besitzt das Interface ActionListener nur die Methode actionPerformed. Listener-Interfaces zu Low-Level-Ereignissen umfassen mehrere Methoden, wie z.B. die Interfaces MouseListener oder WindowListener. Bei der Implementierung von Interfaces müssen grundsätzlich alle Methoden des Interfaces implementiert werden. Das kann lästig werden, wenn das Interface mehrere Methoden besitzt und man für eine graphische Oberfläche lediglich eine der Methoden benötigt. Zur Vereinfachung dieses Problems gibt es sogenannte Adapter-Klassen. Eine Adapter-Klasse ist eine abstrakte Klasse, die das entsprechende Interface implementiert und alle Methoden mit leeren Rümpfen versieht. Eine Adapter-Klasse kann verwendet werden, wenn aus einem Interface nur ein Teil der Methoden benötigt wird. Eine selbstgeschriebene Empfänger-Klasse kann von der Adapter-Klasse erben und redefiniert nur die benötigten Methoden. Man muss dann nicht mehr alle Methoden des Interfaces implementieren. © C. Endreß 26 / 33 02/2006 14. Graphische Oberflächen Anstelle von class EigenerListener implements XxxListener { . . . } schreibt man class EigenerListener extends XxxAdapter { . . . } Interface XxxListener => zugehörige Adapter-Klasse XxxAdapter Zu jedem Low-Level-Ereignis stellt das Paket java.awt.event eine passende Adapter-Klasse zur Verfügung. Beispiel: CloseToggleButtons.java Die Oberfläche des Beispielprogramms enthält zwei Schalter in Form von JToggleButton-Objekten sowie ein Label, das den Anwender darüber informiert, dass das Fenster nur geschlossen werden kann, wenn beide Schalter aktiviert sind. Sind nicht beide Schalter aktiviert und der Anwender versucht, das Fenster zu schließen, erhält er in einem modalen Dialogfenster eine Mitteilung. Dieses Dialogfenster muss erst mit dem OK-Button geschlossen werden, bevor der Benutzer im Anwendungsfenster fortfahren kann. /* Programm: CloseToggleButtons.java * Erzeugt ein Swing-Fenster mit zwei Toggle-Buttons, * die beide zum Schliessen des Fensters aktiviert sein muessen */ import java.awt.*; import java.awt.event.*; import javax.swing.*; public class CloseToggleButtons extends JFrame { Container c; // Container dieses Frames JLabel label; JToggleButton tBtn1, tBtn2; // Konstruktor public CloseToggleButtons() { c = getContentPane(); c.setLayout(new FlowLayout()); // Erzeuge die Label- und Button-Objekte label = new JLabel("Zum Schliessen des Fensters beide Schalter aktivieren!"); tBtn1 = new JToggleButton("Schalter 1"); tBtn2 = new JToggleButton("Schalter 2"); © C. Endreß 27 / 33 02/2006 14. Graphische Oberflächen // Fuege die Komponenten dem Frame hinzu c.add(label); c.add(tBtn1); c.add(tBtn2); // Registriere WindowListener beim Fenster addWindowListener(new ClosingListener()); 1 } // Innere Listener-Klasse public class ClosingListener extends WindowAdapter { public void windowClosing(WindowEvent event) { if (tBtn1.isSelected() && tBtn2.isSelected()) { event.getWindow().dispose(); System.exit(0); } else JOptionPane.showMessageDialog( c, "Vor dem Schliessen erst beide Schalter aktivieren!"); } } 2 3 4 5 6 7 public static void main(String[] args) { CloseToggleButtons fenster = new CloseToggleButtons(); fenster.setTitle("CloseToggleButtons"); fenster.setSize(400, 100); fenster.setVisible(true); // Setze das Verhalten des Frames beim Schliessen auf "Nichtstun" fenster.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); 8 } } Erläuterungen 1 Der WindowListener wird beim Fenster angemeldet. 2 Deklarieren der inneren Klasse ClosingListener: Die Klasse ClosingListener muss eine Empfänger-Klasse für WindowEvents sein. Bisher haben wir in unseren Empfänger-Klassen stets das entsprechende Listener-Interface (hier: WindowListener) implementiert. Von den 7 Methoden des Interfaces WindowListener benötigen wir in diesem Programm nur die Methode windowClosing. Um nicht auch noch die übrigen 6 Methoden definieren zu müssen, implementieren wir nicht das Listener-Interface WindowListener, sondern leiten unsere Empfänger-Klasse ClosingListener von der zugehörigen Adapter-Klasse WindowAdapter ab. 3 Die Methode windowClosing wird aufgerufen, wenn das Fenster geschlossen werden soll. Der Anwender löst dieses Ereignis mit einem Klick auf den Schließen-Knopf des Fensters oder die Tastenkombination Alt-F4 aus. Das Fenster soll jedoch erst geschlossen werden können, wenn beide Schalter aktiviert sind. Diese Voraussetzung ist in der Methode windowClosing zu prüfen. 4 Die Auswahlzustände der beiden JToggleButton-Objekte werden mit der Methode isSelected abgefragt. Die Methode liefert das Ergebnis true, wenn ein JToggleButtonObjekt selektiert ist. 5 Über das Ereignis-Objekt event wird mit der Methode getWindow zunächst die Referenz auf das Fenster ermittelt, das das Ereignis ausgelöst hat. Danach wird das Fenster durch seine Instanzmethode dispose zerstört. 6 Das Programm wird mit der Methode System.exit beendet. © C. Endreß 28 / 33 02/2006 14. Graphische Oberflächen 7 Sind nicht beide Schalter aktiviert, erzeugen wir mit der Klasse JOptionPane ein modales Dialogfenster, das nur eine Mitteilung mit einen OK-Button präsentiert. 8 Damit das Schließen des Fensters auch in der oben gewünschten Weise abläuft setzen wir die Standardeinstellung beim Schließen des Fensters nicht mehr auf „Exit“ sondern auf „Nichtstuns“. Die eingestellte Standardtätigkeit des Fenster wird immer nach dem Ausführen des WindowListeners durchgeführt. Da bereits der Listener das Fenster schließt, muss dieses hier nicht mehr erfolgen. 14.8.5 Empfänger-Registrierung bei Ereignis-Quellen Grundsätzlich muss jedes Empfänger-Objekt bei der Ereignis-Quelle registriert werden, von der es Ereignisse empfangen soll. Implementiertes Listener-Interfaces : XxxListener Empfänger-Objekt anmelden addXxxListener(Empfänger-Objekt) Empfänger-Objekt abmelden removeXxxListener(Empfänger-Objekt) Was passiert bei der Registrierung? Jede Ereignis-Quelle, die von der Basisklasse der Swing-Komponenten JComponent abgeleitet wird, enthält eine Instanzvariable listenerList vom Typ EventListenerList. Durch den Aufruf einer entsprechenden Registrierungsanweisung (addXxxListener) wird eine Referenz auf das übergebene Empfänger-Objekt in die listenerList eingetragen. Die Referenz ist vom Typ des Empfänger-Objekts (z.B. ActionListener, MouseListener etc.). Wenn ein Ereignis auftritt, wird dieses von der Ereignis-Quelle nur an Listener-Objekte weitergeleitet, die in der Listenerliste mit dem passenden Referenztyp eingetragen sind. So werden ActionEvents nur an ActionListener weitergeleitet, MouseEvents nur an MouseListener usw. button1 button1Listener JButton-Objekt, enthält eine Instanzvariable listenerList vom Typ EventListenerList. Ereignis-Abhörer für button1, der das Interface ActionListener implementiert und die Methode actionPerformed definiert. listenerList public void actionPerformed(ActionEvent e) { // Ereignisbehandlung … } ... Diese Referenz wird erzeugt durch die Anweisung button1.addActionListener( button1Listener ); © C. Endreß 29 / 33 02/2006 14. Graphische Oberflächen 14.8.6 Vorgehensweise Um eine problemgerechte Ereignisverarbeitung in Java zu programmieren, bietet sich folgende Vorgehensweise an: Ereignisverarbeitung in Java programmieren 1. Überlegen, von welcher Ereignisquelle Ereignisse abgehört werden sollen. 2. Feststellen, welche Ereignisklassen diese Ereignisquelle erzeugt. (s.a. Tabellen zu elementaren und semantischen Ereignissen) 3. Pro Ereignisklasse eine Empfänger-Klasse für den Ereignistyp schreiben z.B. class AktionsAbhoerer a) Wenn semantisches Ereignis, dann entsprechendes Interface implementieren z.B. class AktionsAbhoerer implements ActionListener und alle Interface-Methoden definieren z.B. public void actionPerformed(ActionEvent event){…} b) Wenn elementares Ereignis, dann eventuell entsprechende Adapter-Klasse erweitern z.B. class MausAbhoerer extends MouseAdapter und nur die benötigten Methoden definieren. 4. Pro Empfänger-Klasse ein Empfänger-Objekt erzeugen z.B. AktionsAbhoerer einAbhoerer = new ActionsAbhoerer() 5. Pro Ereignisquelle entsprechendes Empfänger-Objekt registrieren z.B. einButton.addActionListener(einAbhoerer) © C. Endreß 30 / 33 02/2006 14. Graphische Oberflächen Tabelle: Elementare Ereignisse (Low-Level-Ereignisse) und ihre Verarbeitung Ereignisquellen Ereignisklasse Component ComponentEvent Schnittstelle und ihre Methoden Adapterklasse Registrierung ComponentListener ComponentAdapter addComponentListener componentHidden componentMoved componentResized componentShown FocusEvent FocusListener focusGained focusLost KeyEvent KeyListener keyPressed keyReleased keyTyped MouseEvent MouseListener mouseClicked mouseEntered mouseExited mousePressed mouseReleased MouseMotionEvent MouseMotionListener mouseDragged mouseMoved Container ContainerEvent ContainerListener componentAdded componentRemoved © C. Endreß 31 / 33 Eine Komponente wurde unsichtbar geschaltet. Die Position einer Komponente wurde verändert. Die Größe einer Komponente wurde verändert. Eine Komponente wurde sichtbar geschaltet. FocusAdapter addFocusListener Eine Komponente hat den Tastatur-Fokus erhalten. Eine Komponente hat der Tastatur-Fokus verloren. KeyAdapter addKeyListener Eine Taste wurde gedrückt. Eine Taste wurde losgelassen. Eine Taste wurde gedrückt und wieder losgelassen. MouseAdapter addMouseListener Die Maustaste wurde geklickt (gedrückt und wieder losgelassen). Die Maus wurde in eine Komponente bewegt. Die Maus aus einer Komponente heraus bewegt wurde. Die Maustaste wurde gedrückt. Die Maustaste wurde losgelassen. MouseMotionAdapter addMouseMotionListener Die Maus wurde mit gedrückter Maustaste bewegt. Die Maus wurde ohne gedrückte Maustaste bewegt. ContainerAdapter Eine Komponente wurde einem Container hinzugefügt. Eine Komponente wurde aus einem Container entfernt. 02/2006 addContainerListener 14. Graphische Oberflächen JDialog, JFrame WindowEvent WindowListener windowActivated windowClosed windowClosing windowDeactivated windowDeiconfied windowIconfied windowOpened WindowFocusListener windowGaindedFocus windowLostFocus WindowStateListener windowStateChanged © C. Endreß 32 / 33 WindowAdapter addWindowListener Das Fenster wurde aktiviert. Das Fenster wurde geschlossen. Das Fenster soll geschlossen werden. Das Fenster wurde deaktiviert. Das Fenster wurde wieder hergestellt. Das Fenster wurde minimiert. Das Fenster wurde das erste Mal sichtbar. WindowAdapter addWindowFocusListener Das Fenster hat den Focus erhalten. Das Fenster hat den Focus verloren. WindowAdapter Der Zustand des Fensters hat sich geändert. 02/2006 addWindowStateListener 14. Graphische Oberflächen Tabelle: Semantische Ereignisse und ihre Verarbeitung Ereignisquellen Ereignisklasse Schnittstelle und ihre Methoden Registrierung JButton, JComboBox, JMenuItem, JTextField, JToggleButton, JList, JCheckBox, JRadioButton ActionEvent ActionListener actionPerformed addActionListener JScrollBar AdjustmentEvent JButton, JList, JMenuItem, JTextField, JToggleButton, JCheckBox, JRadioButton, JSlider ChangeEvent JCheckBox, JRadioButton, JMenuItem. JComboBox, JToggleButton ItemEvent JTextField, JTextArea TextEvent JTextField, JTextArea JList, JTable JMenu © C. Endreß CaretEvent ListSelectionEvent MenuEvent Eine Aktion wurde ausgeführt. AdjustmentListener adjustmentValueChanged addAdjustmentListener Der Wert wurde geändert. ChangeListener stateChanged Der Zustand der Ereignis-Quelle wurde verändert. ItemListener itemStateChanged Der Zustand der Ereignis-Quelle wurde verändert. TextListener textValueChanged Der Text wurde verändert addChangeListener addItemListener addTextListener CaretListener caretUpdate Die Cursor-Position wurde aktualisiert. ListSelectionListener valueChanged Die Auswahl der Listeneinträge wurde verändert. MenuListener menuCanceld menuDeselected menuSelected 33 / 33 addCaretListener addListSelectionListener addMenuListener Ein Menüpunkt wurde verworfen. Ein Menüpunkt wurde deselektiert. Ein Menüpunkt wurde selektiert. 02/2006 15. Serialisierung 15. Serialisierung Lernziele ☺ Die Begriffe Serialisierung und Deserialsierung erklären können. ☺ Serialisierung und Deserialisierung einfacher Objektstrukturen in Java realisieren können. 15.1 Begriffsbestimmung Objekte, die in einem Programm erzeugt werden, sind nur in dessen Speicherbereich verfügbar, solange das Programm läuft. Sollen diese Objekte in einer Datei gespeichert oder über eine Netzwerkverbindung transportiert werden, so müssen die Objekte in ein geeignetes Format konvertiert werden. Dazu wandelt man den Zustand eines Objekts, der durch die Werte seiner Instanzvariablen festgelegt ist, in eine systemunabhängige Binärdarstellung um, die über einen Datenstrom transportiert werden kann. Diesen Vorgang nennt man Serialisierung. Definition Serialisierung ist die Umwandlung eines Objekts in eine systemunabhängige Binärdarstellung zum Transportieren des Objekts über einen Datenstrom. Das Rekonstruieren von Objekten aus serialisierten Daten bezeichnet man als Deserialisierung. 15.2 Serialisierung Mit der Klasse ObjectOutputStream können Datenströme zur Serialisierung von Objekten erzeugt werden. public ObjectOutputStream(OutputStream out) An den Konstruktor wird ein OutputStream-Objekt (Byte-Strom) übergeben, das als Ziel der Ausgabe dient. Zum Schreiben der serialisierten Daten in eine Datei verwendet man ein FileOutputStreamObjekt. Die Klasse FileOutputStream ist eine Unterklasse von OutputStream. Die Klasse ObjectOutputStream besitzt Methoden, um elementare Datentypen zu serialisieren. Zum Serialisieren von Objekten verfügt die Klasse ObjectOutputStream über die Methode public final void writeObject(Object obj) Objekte können nur serialisiert werden, wenn sie das Interface Serializable implementieren. Dabei handelt es sich um ein Interface, das weder Methoden nach Variablen enthält. Es reicht daher aus, eine Klasse lediglich mit implements Serializable zu erweitern. Methoden sind nicht zu implementieren. Beim Aufruf der Methode writeObject werden nur Instanzvariablen des übergebenen Objekts serialisiert. Klassenvariablen werden von der Serialisierung ausgenommen, da sie nicht zum Objekt, sondern zur Klasse gehören. Will man auch bestimmte Instanzvariablen von der Serialisierung ausnehmen, müssen diese in der Klassendefinition durch das Schlüsselwort transient markiert werden. C. Endreß 1/4 02/2006 15. Serialisierung Beispiel: import java.io.*; 1 2 public class Datensatz implements Serializable { public int nr; // Nummer des Datensatzes public double wert; // Wert des Datensatzes public String kom; // Kommentar 3 public Datensatz (int nr, double wert, String kom) { // Konstruktor this.nr = nr; this.wert = wert; this.kom = kom; } 4 public String toString() { // Erzeugung einer String-Darstellung return "Nr. " + nr + ": " + wert + " (" + kom + ")"; } } Erläuterungen 1 Um Objekte der Klasse Datensatz serialisieren zu können, muss die Klasse das Interface Serializable implementieren. Serializable enthält weder Methoden noch Variablen, die in der implementierenden Klasse definiert werden müssten. 2 Mit der Sichtbarkeit public verstoßen die Deklarationen der Attribute gegen das Prinzip der Datenkapselung der objekt-orientierten Programmierung. Die Veröffentlichung der Attribute dient hier lediglich der Verkürzung des Quellcodes, da auf get- und set-Methoden verzichtet werden kann. Zur Nachahmung sei sie jedoch nicht empfohlen. 3 Dem parametrisierten Konstruktor werden die Attributwerte der Instanzvariablen übergeben. 4 Die Methode toString liefert die textuelle Repräsentation der Datensatz-Objekte. Das folgende Listing demonstriert das Serialisieren von Objekten der Klasse Datensatz in der Datei MeineDaten.dat: import java.io.*; 1 2 3 4 public class ObjectWrite { public static void main(String[] summand) { try { String dateiname = "MeineDaten.dat"; FileOutputStream datAus = new FileOutputStream(dateiname); ObjectOutputStream oAus = new ObjectOutputStream(datAus); 5 int anzahl = 2; Datensatz a = new Datensatz(99, 56, "Coca Cola"); Datensatz b = new Datensatz(111, 1234.79, "Fahrrad"); 6 7 8 oAus.writeInt(anzahl); // Anzahl der Datensätze oAus.writeObject(a); // Datensatz 1 oAus.writeObject(b); // Datensatz 2 9 oAus.close(); System.out.println(anzahl + " Datensaetze in die Datei " + dateiname + " geschrieben"); System.out.println(a); System.out.println(b); } C. Endreß 2/4 02/2006 15. Serialisierung catch (Exception e) { System.out.println("Fehler beim Schreiben: " + e); } } } Erläuterungen 1 Die Konstruktoren der OutputStreams sowie die Methode writeObject werfen IOExceptions aus. Deshalb werden die Anweisungen in einen try-Block gesetzt. Die folgende catch-Anweisung fängt geworfene Exceptions auf und zeigt deren Fehlermeldungen an. 2 Die Stringvariable enthält den Dateinamen. 3 Das FileOutputStream-Objekt datAus wird mit der Datei MeineDaten.dat initialisiert. Der Stream stellt eine Verbindung zur Zieldatei her, über die Daten in die Datei geschrieben werden können. 4 Das ObjectOutputStream-Objekt oAus wird mit dem FileOutputStream-Objekt datAus initialisiert. Dadurch entsteht eine Verbindung von oAus über datAus in die Datei. Der ObjectOutputStream serialisiert die Objekte, der FileOutputStream überträgt die serialisierten Binärdaten in die Datei. 5 Es werden zwei Datensatz-Objekte a und b erzeugt. 6 Die Anzahl der zu serialisierenden Datensätze wird in die Datei geschrieben. 7 Die Methode writeObject serialisiert das Datensatz-Objekt a über den ObjectOutputStream und schreibt es in die Datei. 8 Die Methode writeObject serialisiert das Datensatz-Objekt b über den ObjectOutputStream und schreibt es in die Datei. 9 Die Methode close schließt den OutputStream und beendet die Verbindung zur Datei. 15.3 Deserialisierung Um Datenströme zu deserialisieren, verwendet man die Klasse ObjectInputStream: public ObjectInputStream(InputStream in) An den Konstruktor wird ein InputStream-Objekt (Byte-Strom) übergeben, über das Daten von einer Quelle eingelesen werden. Zum Lesen von Daten aus einer Datei verwendet man ein FileInputStream-Objekt. Die Klasse FileInputStream ist eine Unterklasse von InputStream. Die Methode public final Object readObject() liest ein Objekt aus dem Datenstrom und liefert es als Ergebnis zurück. Beispiel: Mit dem folgenden Programm werden Objekte, die im vorangegangenen Beispiel serialisiert wurden, aus einer Datei ausgelesen und rekonstruiert. Import java.io.*; 1 2 3 4 C. Endreß public class ObjectRead { public static void main (String[] summand) { try { String dateiname = "MeineDaten.dat"; FileInputStream datEin = new FileInputStream(dateiname); ObjectInputStream oEin = new ObjectInputStream(datEin); 3/4 02/2006 15. Serialisierung 5 int anzahl = oEin.readInt(); System.out.println("Die Datei " + dateiname + " enthaelt " + anzahl + " Datensaetze"); for (int i=1; i<=anzahl; i++) { Datensatz gelesen = (Datensatz) oEin.readObject(); System.out.println(gelesen); } oEin.close(); 6 7 } catch (Exception e) { System.out.println("Fehler beim Lesen: " + e); } } } Erläuterungen 1 Die Konstruktoren der InputStreams sowie die Methode readObject werfen IOExceptions aus. Deshalb werden die Anweisungen in einen try-Block gesetzt. Die folgende catch-Anweisung fängt geworfene Exceptions auf und zeigt deren Fehlermeldungen an. 2 Die Stringvariable enthält den Dateinamen. 3 Das FileInputStream-Objekt datEin wird mit der Datei MeineDaten.dat initialisiert. Der InputStream stellt eine Verbindung zur Quelldatei her, von der die Daten gelesen werden sollen. 4 Das ObjectInputStream-Objekt oEin wird mit dem FileInputStream-Objekt datEin initialisiert. Dadurch entsteht eine Verbindung von der Quelldatei über datEin zu oEin. Der ObjectInputStream deserialisiert die binären Daten, die der FileInputStream aus der Quelldatei liest, und rekonstruiert Objekte. 5 Als erste Information wird die Anzahl der serialisierten Objekte ausgelesen. 6 Es wird ein Objekt der Klasse Datensatz erzeugt. Die Methode readObject liest die serialisierten Binärdaten über den ObjectInputStream oEin aus der Datei und rekonstruiert ein Objekt. Dieses deserialisierte Objekt ist von der Basisklasse Object. Für eine Zuweisung an das Datensatz-Objekt gelesen muss ein Typ-Cast nach Datensatz durchgeführt werden. 7 Nachdem alle Objekte eingelesen sind, wird der Eingabestream geschlossen. C. Endreß 4/4 02/2006 16. Suchen und Sortieren 16. Suchen und Sortieren Lernziele ☺ Algorithmen für lineare und binäre Suche erklären und anwenden können. ☺ Die Begriffe Komplexität und Ordnung eines Algorithmus kennen. ☺ Algorithmen für die Sortierverfahren Bubble-Sort, Insertion-Sort und Quick-Sort erklären können. 16.1 Suchverfahren Das Suchen von Informationen in Informationsbeständen gehört zu den häufigsten Aufgaben, die mit Computersystemen ausgeführt werden. Oft ist die gesuchte Information eindeutig durch einen Schlüssel identifizierbar. Schlüssel sind in der Regel positive ganze Zahlen (z.B. Artikelnummer, Kontonummer etc.) oder alphabetische Schlüssel (z.B. Nachname, Firmenname usw.). Von dem Schlüssel gibt es meist eine Referenz auf die eigentlichen Informationen. Die Suchverfahren können Kategorien zugeordnet werden: Elementare Suchverfahren: Es werden nur Vergleichsoperationen zwischen den Schlüsseln ausgeführt. Schlüssel-Transformationen (Hash-Verfahren): Aus dem Suchschlüssel wird mit arithmetischen Operationen direkt die Adresse von Datensätzen berechnet. Suchen in Texten: Suchen eines Musters in einer Zeichenkette. 16.1.1 Lineare Suche Die lineare bzw. sequentielle Suche wird immer dann angewendet, wenn die Schlüssel in ungeordneter Folge in einer Liste abgelegt sind. Lineare Suche: Die Elemente der Liste werden der Reihe nach mit dem Suchschlüssel verglichen, bis das passende Element gefunden wird oder das Ende der Liste erreicht ist. Beispiel: Feld: a[ ] Feldlänge: n =14 Suchschlüssel: k = 57 Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 27 2 13 6 8 16 5 59 47 33 57 19 29 4 lineare Suche a[ i ] Suchschlüssel gefunden an Position i = 10 C. Endreß 1 / 14 10/2008 16. Suchen und Sortieren Als Ergebnis der Suche kann entweder die Position des gesuchten Elements zurückgegeben werden oder auch nur die Information ob das gesuchte Element in der Liste enthalten ist. Das Struktogramm zeigt eine Funktion, die bei erfolgreicher Suche die Schlüsselposition zurückgibt, deren Wert zwischen 0 und (n – 1) liegt. Ist das gesuchte Element nicht im Feld enthalten, liefert die Funktion den Wert n zurück. lineareSuche( int a[ ], int n, int k ) int i = 0 while( i < n && k != a[ i ] ) i=i+1 return i Aufwandsbetrachtung Unter der Komplexität eines Algorithmus versteht man den zu seiner Durchführung erforderlichen Aufwand an Betriebsmitteln wie Rechenzeit und Speicherplatz. Die asymptotische Zeitkomplexität wird als Ordnung bezeichnet und durch ein großes O ausgedrückt: O(n). Wenn ein Algorithmus Eingabedaten des Umfangs n verarbeitet, und der Zeitaufwand linear ist, besitzt der Algorithmus die Ordnung n. Seine Zeitkomplexität ist dann proportional n. Für die lineare Suche in einer Liste gilt: Enthält die Liste n Elemente, dann werden im schlechtesten Fall n Schlüsselvergleiche für eine erfolgreiche Suche benötigt. Nimmt man an, dass die Verteilung der Schlüssel gleichwahrscheinlich ist, dann ist zu erwarten, dass für eine erfolgreiche Suche im Mittel 1 ⋅ n n ∑i = i=1 n +1 2 Schlüsselvergleiche durchzuführen sind. Lineare Suche: 16.1.2 O(n) = n Binäre Suche Liegen die Schlüssel geordnet in einer linearen Liste vor, kann eine binäre Suche durchgeführt werden. In einem sortierten Feld kann man deutlich schneller suchen. Binäre Suche: Man vergleicht zunächst den Suchschlüssel mit dem Element in der Mitte des Feldes. Ist der Suchschlüssel kleiner als das mittlere Element, setzt man die Suche im unteren Teilfeld fort. Ist der Suchschlüssel größer als das mittlere Element, wird die Suche im oberen Teilfeld fortgeführt. Im ausgewählten Teilfeld vergleicht man nun den Suchschlüssel wieder mit dem mittleren Element des Teilfeldes. Gegebenenfalls muss das Teilfeld erneut halbiert und die Suche nach dem beschrieben Verfahren fortgesetzt werden, bis das gesuchte Element gefunden ist oder nur noch ein Element übrig bleibt. C. Endreß 2 / 14 10/2008 16. Suchen und Sortieren Beispiel 1: Suchschlüssel im Feld vorhanden Feld: a[ ] Feldlänge: n =14 Suchschlüssel: k = 8 Teilfeldgrenzen: low, high Teilfeldmitte: m Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 2 5 6 8 13 16 19 27 29 33 47 52 57 59 2 5 6 8 13 16 low = 0, high = 5 m=2 8 13 16 low = 3, high = 5 m=4 low = 0, high = 13 m=6 low = 3, high = 3 m=3 8 Suchschlüssel gefunden an Position i = 3 Beispiel 2: Suchschlüssel ist nicht im Feld vorhanden Feld: a[ ] Feldlänge: n =14 Suchschlüssel: k = 54 Teilfeldgrenzen: low, high Teilfeldmitte: m Index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 2 5 6 8 13 16 19 27 29 33 47 52 57 59 low = 0, high = 13 m=6 27 29 33 47 52 57 59 low = 7, high = 13 m = 10 52 57 59 low = 11, high = 13 m = 12 52 low = 11, high = 11 m = 11 Suchschlüssel nicht gefunden C. Endreß 3 / 14 10/2008 16. Suchen und Sortieren Der Suchalgorithmus kann sowohl rekursiv als auch iterativ formuliert werden. Die folgenden Struktogramme zeigen mögliche Formulierungen einer rekursiven sowie einer iterativen Funktion binSuche. int binSuche( int a[ ], int k, int low, int high ) int m = (low + high) / 2 low <= high ja ja k = a[ m ] return m nein nein k < a[ m ] ja return binSuche(a, k, low, m - 1) return -1 nein return binSuche(a, k, m + 1, high) Rekursive Formulierung der binären Suche int binSuche( int a[ ], int n, int k ) int m int low = 0 int high = n - 1 while( low <= high) m = ( low + high ) / 2 k = a[ m ] ja return m nein k > a[ m ] ja low = m + 1 nein high = m - 1 return -1 Iterative Formulierung der binären Suche Aufwandsbetrachtung Entscheidend für den Aufwand ist die Anzahl der notwendigen Halbierungen, die natürlich von der Anzahl der Feldelemente abhängt. Im schlechtesten Fall sind so viele Halbierungen vorzunehmen, bis nur noch ein Element zu betrachten ist. Der Aufwand für eine binäre Suche ist durch die maximale Anzahl der Halbierungen des Feldes begrenzt. C. Endreß Anzahl der Halbierungen 0 1 2 3 Länge des zu betrachtenden Teilfeldes n n/2 n/4 n/8 4 / 14 4 5 6 7 k n/16 n/32 n/64 n/128 n/2k 10/2008 16. Suchen und Sortieren Da die Mindestgröße des zu betrachtenden Teilfeldes 1 sein muss, ergibt sich die folgende Ungleichung: n 2k ≥ 1. Löst man diese Ungleichung durch Logarithmieren nach k auf, ergibt sich für die Anzahl der Halbierungen: k ≤ log2 (n) Das gesuchte Element ist also nach maximal log2(n) Halbierungsschritten gefunden. Binäre Suche: O(n) = log2 (n) 16.2 Sortieren In vielen Anwendungen muss eine Folge von Objekten in eine bestimmte Reihefolge gebracht werden. Als Beispiel sei hier auf ein Telefonverzeichnis hingewiesen, in dem die Einträge alphabetisch nach Namen zu sortieren sind. Bei kaufmännisch-administrativen Anwendungen werden beispielsweise 25 % der Computerzeit für Sortiervorgänge benötigt. Daher wurde intensiv nach effizienten Sortieralgorithmen gesucht, die sich nach folgenden Kriterien klassifizieren lassen: Zeitverhalten Interne vs. externe Sortierung, d.h. passen die zu sortierenden Schlüssel alle in den Arbeitsspeicher oder nicht. Arbeitsspeicherverbrauch, d.h. wird zusätzlicher Speicherplatz außer dem Platz für die Schlüssel benötigt. Stabiles vs. instabiles Verhalten, d.h. die Reihenfolge von Elementen mit gleichem Sortierschlüssel wird während des Sortierens nicht vertauscht. Stabilität ist oft erwünscht, wenn die Elemente bereits nach einem zweitrangigen Schlüssel geordnet sind (z.B. Name, Vorname) Sensibilität bezogen auf die Eingabeverteilung, d.h. verändert sich das Zeitverhalten, wenn die Eingabefolge bereits sortiert oder vollständig unsortiert ist. Allgemeines vs. spezielles Verhalten, d.h. wird nur eine lineare Ordnung auf der Menge der Schlüssel vorausgesetzt oder müssen die Schlüssel von spezieller Gestalt sein. Anhand dieser Kriterien ist für ein gegebenes Sortierproblem eine geeignete Auswahl zu treffen. Das Hauptkriterium ist natürlich das Zeitverhalten. Dazu gilt folgender Satz: Kein Sortierverfahren kommt mit weniger als n log2( n ) Vergleichen zwischen Schlüsseln aus. C. Endreß 5 / 14 10/2008 16. Suchen und Sortieren Die folgende Abbildung gibt einen Überblick über bekannte Sortierverfahren, die nach ihrem Zeitverhalten klassifiziert sind. Sortierverfahren Für kleine n (n < 500) Für große n (n >= 500) Elementare Verfahren Schnelle Verfahren 2 Schlüsselvergleiche: O(n) = n (für zufällig sortierte Folgen) Schlüsselvergleiche: O(n) = n log2(n) Sortieren durch Auswahl Mischsortieren Sortieren durch Austauschen Quicksort Sortieren durch Einfügen Sortieren mit einer Halde (selection sort) (mergesort) (bubblesort, exchange sort) (insertion sort) 16.2.1 (Heapsort) Bubble-Sort Bubble-Sort: Durchlaufe das Feld in aufsteigender Richtung. Betrachte dabei immer zwei benachbarte Elemente. Wenn die zwei benachbarten Elemente nicht in aufsteigender Ordnung sind, vertausche sie. Nach dem ersten Durchlauf ist auf jeden Fall das größte Element am Ende des Feldes. Wiederhole den obigen Verfahrensschritt so lange, bis das Feld vollständig sortiert ist. Dabei muss jeweils das letzte Element des vorherigen Durchlaufs nicht mehr betrachtet werden, da es schon seine endgültige Position gefunden hat. Beispiel: C. Endreß 0 1 2 3 4 320 178 86 207 59 178 320 86 207 59 178 86 320 207 59 178 86 207 320 59 178 86 207 59 320 6 / 14 1. Durchgang 10/2008 16. Suchen und Sortieren 178 86 207 59 320 86 178 207 59 320 2. Durchgang 86 178 207 59 320 86 178 59 207 320 86 178 59 207 320 86 178 59 207 320 86 59 178 207 320 86 59 178 207 320 3. Durchgang 4. Durchgang 59 86 178 207 320 Der Name Bubble-Sort rührt daher, dass man das Wandern des größten Elements ganz nach rechts mit dem Aufsteigen von Luftblasen vergleichen kann. Der Algorithmus kann folgendermaßen mit einem Struktogramm formuliert werden: void bubbleSort( int a[ ], int n ) int temp for( int i = n - 1; i > 0; i-- ) for( int k = 0; k < i; k++ ) a[ k ] > a[ k + 1 ] nein ja temp = a[ k ] a[ k ] = a[ k + 1 ] a[ k + 1 ] = temp Bubble-Sort C. Endreß 7 / 14 10/2008 16. Suchen und Sortieren Aufwandsbetrachtung Strukturell besteht der Bubble-Sort-Algorithmus aus einem Kern, der in zwei Schleifen eingebettet ist. Die Laufzeit des Kerns hängt natürlich davon ab, ob eine Vertauschung durchzuführen ist oder nicht. Im günstigsten Fall (best case) ist das Feld bereits sortiert und es findet keine Vertauschung von Elementen statt. Im ungünstigsten Fall (worst case) erfolgt jedoch bei jedem Kerndurchlauf eine Vertauschung. Wenn wir eine zufällige Verteilung der Daten annehmen, können wir davon ausgehen, dass in etwa der Hälfte aller Kerndurchläufe eine Vertauschung durchzuführen ist. Der Kern wird also bei zufälliger Verteilung eine mittlere Laufzeit haben, die etwa die Hälfte der zur Vertauschung von zwei Elementen benötigten Rechenzeit ausmacht. Der Wert dieser Rechenzeit ist natürlich abhängig von der Plattform, auf der der Algorithmus abläuft, sowie von dem Datentyp der Elemente. Strings oder Objekte benötigen eine wesentlich längere Rechenzeit zur Vertauschung als elementare Datentypen. Unter der oben gemachten Annahme einer mittleren Rechenzeit für den Algorithmuskern lässt sich die Komplexität des Algorithmus nach folgender Überlegung berechnen: Beim ersten Durchlauf durch das Feld läuft die Laufvariable k der inneren Schleife von 0 bis n-2. Die innere Schleife wird also n-1 mal durchlaufen. Beim zweiten Durchlauf wird die innere Schleife nur noch n-2 mal durchlaufen. Beim dritten Durchlauf sind es nur noch n-3 Läufe durch die innere Schleife. Beim letzten Durchlauf wird die innere Schleife schließlich nur noch einmal durchlaufen. Daraus ergibt sich für die Summe S der Läufe durch den Kern: n −1 S = (n − 1) + (n − 2) + (n − 3) + . . . + 3 + 2 + 1 = ∑i i =1 Zur Berechnung der Summe verdoppeln wir beide Seiten der Gleichung: 2 ⋅ S = (n − 1) + (n − 2) + (n − 3) + K + 1 + 2 + 3 + 2 + + 1 3 + K + (n − 3) + (n − 2) + (n − 1) dass der 2 ⋅ S = (n − 1) ⋅ n S= (n − 1) ⋅ n 2 Aus der oben hergeleiteten Laufzeitverhalten aufweist. Bubble Sort: 16.2.2 Formel folgt, Bubble-Sort-Algorithmus ein quadratisches O(n) = n2 Insertion-Sort Insertion-Sort ist ein Sortierverfahren, das so arbeitet wie wir Spielkarten auf der Hand sortieren. Insertion-Sort: Die erste Karte ganz links ist sortiert. Wir nehmen die zweite Karte und stecken sie, je nach Größe, vor oder hinter die erste Karte. Damit sind die beiden ersten Karten relativ zueinander sortiert. Wir nehmen die dritte Karte und schieben sie solange nach links, bis wir an die Stelle kommen, an der sie passt. Dort stecken wir sie hinein. Für alle weiteren Karten verfahren wir der Reihe nach ebenso. C. Endreß 8 / 14 10/2008 16. Suchen und Sortieren In einem Array funktioniert das natürlich nicht ganz so leicht wie in einem Kartenspiel, da wir nicht einfach ein von rechts kommendes Element dazwischen schieben können. Dazu müssen zunächst alle übersprungenen Elemente nach rechts aufrücken, um für das einzusetzende Element Platz zu machen. 0 1 2 3 4 320 178 86 207 101 unsortiertes Feld 86 207 101 1. Durchlauf 207 101 2. Durchlauf 101 3. Durchlauf 320 178 178 320 86 86 178 320 207 86 178 207 4. Durchlauf 320 101 86 101 178 207 320 sortiertes Feld void insertionSort( int a [ ], int n ) int element, i, j for( i = 1; i < n; i++ ) element = a[ i ] j=i while( j > 0 && element < a[ j - 1 ] ) a[ j ] = a[ j - 1 ] j=j-1 a[ j ] = element Insertion-Sort C. Endreß 9 / 14 10/2008 16. Suchen und Sortieren Aufwandsbetrachtung Beim Insertion-Sort-Verfahren haben wir zwei ineinander verschachtelte Schleifen zu analysieren. Hierbei wird jedoch die innere Schleife über eine zusätzliche Bedingung (element < a[ j – 1 ]) gesteuert. Bei zufällig verteilten Daten können wir davon ausgehen, dass diese Bedingung im Mittel bei der Hälfte des zu durchlaufenden Indexbereichs erfüllt ist, die Schleife also im Durchschnitt auf halber Strecke abgebrochen werden kann. Bezeichnen wir die Laufzeit der inneren Schleife mit cins2 und die Laufzeit der äußeren Schleife mit cins1, so ergibt sich die folgende Formel für das Laufzeitverhalten von Insertion-Sort: t(n) = t(n) = n −1 ⎛ ∑ i/2 ⎞ ⎟ ⎜c + ⎜ ins1 i=1 ⎝ ∑c n −1 i ⎞ ⋅ c ins2 ⎟ 2 ⎠ ⎛ ∑ ⎜⎝ c i =1 ins1 + j =1 t(n) = c ins1(n − 1) + ins 2 ⎟ c ins 2 ⋅ 2 t(n) = c ins1(n − 1) + c ins2 ⋅ ⎠ n −1 ∑i i=1 n ⋅ (n − 1) 4 Auch hier haben wir wieder asymptotisch quadratisches Verhalten. Insertion-Sort: 16.2.3 O(n) = n2 Quicksort Das Quicksort-Verfahren wurde 1960 von C.A.R. Hoare erfunden ist eines der schnellsten Sortierverfahren für Felder, die weitgehend unsortiert sind. Der Grundgedanke von Quicksort verfolgt die Divide-and-Conquer-Strategie: eine Folge wird durch Zerlegen in kleinere Teilfolgen sortiert. Quicksort: Zu Beginn wird in dem zu sortierenden Feld ein als Pivotelement (Angelpunkt) bezeichnetes Element beliebig gewählt. In einem Durchlauf werden dann die Feldelemente so miteinander vertauscht, dass am Ende des Durchlaufs das Pivotelement das Feld in ein linkes Teilfeld und ein rechtes Teilfeld zerlegt. Alle Elemente des linken Teilfeldes sind kleiner oder gleich dem Pivotelement, die Elemente des rechten Teilfeldes sind größer oder gleich dem Pivotelement. Damit hat das Pivotelement seine endgültige Position in dem Feld eingenommen. Mit den beiden Teilfeldern wird nun in gleicher Weise verfahren, bis ein Teilfeld nur noch ein oder gar kein Element mehr enthält und somit sortiert ist. C. Endreß 10 / 14 10/2008 16. Suchen und Sortieren Beispiel: Das Feld a[ ] mit 10 Elementen ist zu sortieren. In der Folge wird das Element in der Feldmitte (a[m] = 60) als Pivotelement für eine Aufteilung der Folge in zwei Teilfolgen ausgewählt. 0 1 2 3 4 5 6 7 8 9 76 7 58 88 60 41 82 77 49 86 Als nächstes wird die Folge von links durchsucht, bis ein Element gefunden wird, das größer als das Pivotelement oder das Pivotelement selbst ist. Das ist bereits bei a[0] = 76 der Fall. Von rechts wird die Folge durchsucht, bis ein Element gefunden wird, das kleiner als das Pivotelement oder das Pivotelement selbst ist. Die Suche hält bei a[8] = 49 an. Die Elemente a[0] und a[8] werden anschließend vertauscht. 0 1 2 3 4 5 6 7 8 9 76 7 58 88 60 41 82 77 49 86 Suche von rechts Suche von links Elemente vertauschen Die Suche wird von links mit dem Index 1 und von rechts mit dem Index 7 fortgesetzt. Die nächsten Elemente, die vertauscht werden sind im linken Teilfeld a[3] = 88 und im rechten Teilfeld a[5] = 41. 0 1 2 3 4 5 6 7 8 9 49 7 58 88 60 41 82 77 76 86 Der linke Suchindex wird auf 4 gesetzt. Der rechte Index erhält ebenfalls den Wert 4. Die Suchbereiche überschneiden sich damit und der Durchlauf wird beendet. Das Pivotelement hat seinen endgültigen Platz erreicht. Im linken Teilfeld befinden sich nur noch Werte, die kleiner als der Pivot a[4] sind. Die Werte im rechten Teilfeld sind größer als der Pivot. 0 1 2 3 4 5 6 7 8 9 49 7 58 41 60 88 82 77 76 86 Überschneidung beim Durchsuchen Die beiden Teilfelder werden nun rekursiv nach demselben Verfahren sortiert. Sortierung des linken Teilfelds a[0] . . . a[3]: Rekursionsebene 1 C. Endreß 0 1 2 3 49 7 58 41 0 1 2 3 7 49 58 41 Als Pivot wird a[1] festgelegt. Die Suche von links ergibt, dass a[0] größer als das Pivotelement ist. Der rechte Index läuft bis zum Pivotelement, da kein Element im rechten Feld kleiner als der Pivot ist. a[0] und der Pivot a[1] werden getauscht. Die Suchbereiche überschneiden sich, das Pivotelement hat seine endgültige Position im Feld bei a[0] erreicht. Das linke Teilfeld ist leer. Das rechte Teilfeld a[1]...a[3] wird in der nächsten Rekursionsebene bearbeitet 11 / 14 10/2008 16. Suchen und Sortieren Rekursionsebene 2 Rekursionsebene 3 1 2 3 49 58 41 1 2 3 49 41 58 1 2 49 41 1 2 41 49 Das Pivotelement wird aus der Feldmitte (a[2]) gewählt. Das Element a[3] des rechten Feldes wird mit dem Pivot vertauscht, da es kleiner ist. Es kommt wieder zu einer Überschneidung der Suchbereiche von links und rechts. Der Durchlauf wird abgebrochen. Das linke Teilfeld a[1]...a[2] wird in der nächsten Rekursionsebene sortiert. Das Element a[1] wird als Pivot gewählt. Die Elemente werden vertauscht. Da das linke Teilfeld nur ein Element enthält, ist es sortiert. Das rechte Teilfeld ist leer. Damit ist die gesamte Teilfolge a[0]...a[3] links des ersten Pivotelements a[4] sortiert. Die Sortierung des rechten Teilfeldes a[5] . . . a[9] erfolgt analog: Rekursionsebene 1 Rekursionsebene 2 C. Endreß 5 6 7 8 9 88 82 77 76 86 5 6 7 8 9 76 82 77 88 86 5 6 7 8 9 76 77 82 88 86 7 8 9 82 88 86 7 8 9 82 86 88 Die Suchbereichüberschneidung beendet den Durchlauf. Das linke Teilfeld besteht nur noch aus einem Element (a[5]) und ist deshalb sortiert. Das Element a[6] befindet sich ebenfalls an seiner endgültigen Position. Das rechte Teilfeld a[7]...a[9] wird in der nächsten Rekursionsebene sortiert. Als Pivot wird wieder die Feldmitte gewählt (a[8]) Das Pivotelement hat seinen Platz erreicht. Die linke Teilfolge wird wieder rekursiv sortiert. 12 / 14 10/2008 16. Suchen und Sortieren Rekursionsebene 3 7 8 82 86 a[7] wird als Pivot gewählt. Die Suchbereiche überschneiden sich. Die Elemente sind sortiert Fertig sortierte Folge: 0 1 2 3 4 5 6 7 8 9 7 41 49 58 60 76 77 82 86 88 Der Quicksort-Algorithmus arbeitet rekursiv: void quickSort( int UnterGrenze, int OberGrenze, int a[ ] ) int links = UnterGrenze int rechts = OberGrenze int pivot = a[ (links + rechts) / 2 ] while ( a[ links ] < pivot ) links = links + 1 while ( pivot < a[ rechts ] ) rechts = rechts - 1 links <= rechts ja nein vertausche( a[ links ], a[ rechts ] ) links = links + 1 rechts = rechts - 1 while ( links <= rechts ) UnterGrenze < rechts ja nein quickSort ( UnterGrenze, rechts, a ) links < OberGrenze ja nein quickSort( links, OberGrenze, a ) Quicksort Aufwandsbetrachtung Wenn wir eine zentrierte Lage des Pivots unterstellen, erfolgt eine fortlaufende Halbierung der Teilfelder, so dass das Feld mit log2 n Halbierungen sortiert ist. Um jedes Element mit dem Pivot zu vergleichen, sind n–1 Vergleiche und im Mittel n/2 Vertauschungen notwendig. Daraus ergibt sich im Mittel ein Aufwand von Quicksort: C. Endreß O(n) = n * log2 n 13 / 14 10/2008 16. Suchen und Sortieren 16.3 Vergleichen von Zeichenketten Such- und Sortierverfahren werden häufig auf textuelle Daten angewendet wie z.B. Namen oder Buchtitel. Es ist dann erforderlich, solche Zeichenketten mit einander zu vergleichen. Zeichenketten sind in Java Objekte der Klasse String. Zum Vergleich von String-Objekten können die logischen Operatoren <, >, ==, != nicht verwendet werden. Stattdessen stellt die Klasse String Instanzmethoden zur Verfügung, mit welchen Zeichenketten verglichen werden können. Methode compareTo(String s) compareToIgnoreCase(String s) Beschreibung Lexikalischer Vergleich zweier Strings: Bei einem lexikalischen Vergleich werden die Zeichen paarweise von links nach rechts verglichen. Tritt ein Unterschied auf oder ist einer der Strings beendet, wird das Ergebnis ermittelt. Ist das aktuelle String-Objekt kleiner als s, wird ein negativer Wert zurückgegeben. Ist es größer, wird ein positiver Wert zurückgegeben. Bei Gleichheit liefert die Methode den Rückgabewert 0. equals(String s) equalsIgnoreCase(String s) Prüfen auf Gleichheit: Die Methoden liefern true, wenn das aktuelle String-Objekt und der Parameter s inhaltlich gleich sind. Beispiel: String name1 = “Adam“; String name2 = “Eva“; name1.compareTo(name2) liefert -1, weil “Adam“ im Lexikon vor “Eva“ steht. C. Endreß 14 / 14 10/2008