Yet Another Java Intro Harald Krottmaier 14. August 2003 Inhaltsverzeichnis 1 Anatomie eines Java-Programms 1 1.1 Ein sehr einfaches Beispiel . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Vom Sourcecode zum Programm . . . . . . . . . . . . . . . . . . 2 1.2.1 Was alles schiefgehen wird . . . . . . . . . . . . . . . . . . 3 1.2.2 Wohin mit den Klassenfiles? . . . . . . . . . . . . . . . . . 4 1.3 Ein einfaches Beispiel . . . . . . . . . . . . . . . . . . . . . . . . 6 1.4 Ein einfaches Applet . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.5 Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.6 Schlüsselwörter in Java . . . . . . . . . . . . . . . . . . . . . . . . 11 2 Datentypen, Variablen und Operatoren 2.1 Datentypen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 2.1.1 Primitive-Datentypen . . . . . . . . . . . . . . . . . . . . 12 boolean-Datentyp . . . . . . . . . . . . . . . . . . . . . . 13 Ganzzahlige-Datentypen (byte, short, int und long) . . 14 char-Datentyp . . . . . . . . . . . . . . . . . . . . . . . . 14 Gleitkomma-Datentypen (float, double) . . . . . . . . . 15 Referenz-Datentypen . . . . . . . . . . . . . . . . . . . . . 16 String und StringBuffer . . . . . . . . . . . . . . . . . 17 Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 Mehrdimensionale Arrays . . . . . . . . . . . . . . . . . . 21 Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.2.1 Deklaration von Variablen . . . . . . . . . . . . . . . . . . 23 2.2.2 Default Werte . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.2.3 Arten und Scope von Variablen . . . . . . . . . . . . . . . 25 Operatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 2.3.1 Unäre Arithmetische Operatoren . . . . . . . . . . . . . . 28 2.3.2 Binäre Arithmetische Operatoren . . . . . . . . . . . . . . 29 2.3.3 Vergleichsoperatoren . . . . . . . . . . . . . . . . . . . . . 30 2.1.2 2.2 2.3 12 I INHALTSVERZEICHNIS II 2.3.4 Logische Verknüpfungsoperatoren . . . . . . . . . . . . . . 30 2.3.5 Bitoperatoren . . . . . . . . . . . . . . . . . . . . . . . . . 31 2.3.6 Shortcut-Zuweisungsoperatoren . . . . . . . . . . . . . . . 32 2.3.7 Weitere Operatoren . . . . . . . . . . . . . . . . . . . . . 32 3 Flusskontrolle 34 3.1 While... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.2 For... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 3.3 If... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37 3.4 Switch... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 3.5 break, continue und return . . . . . . . . . . . . . . . . . . . . 39 4 Klassen in Java 41 4.1 Einige Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 4.2 Definition einer Klasse in Java . . . . . . . . . . . . . . . . . . . 43 4.3 Der Konstruktor einer Klasse . . . . . . . . . . . . . . . . . . . . 45 4.3.1 Konstruktoren in Einfachen Klassen . . . . . . . . . . . . 46 4.3.2 Konstruktoren und Abgeleitete Klassen . . . . . . . . . . 47 ’Destruktor’ einer Klasse . . . . . . . . . . . . . . . . . . . . . . . 50 4.4 4.5 Member-Modifier . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 4.5.1 Access-Modifier . . . . . . . . . . . . . . . . . . . . . . . . 53 4.5.2 Object- und Class-Members . . . . . . . . . . . . . . . . . 56 4.5.3 Variablen-Modifier . . . . . . . . . . . . . . . . . . . . . . 57 4.5.4 Methoden-Modifier . . . . . . . . . . . . . . . . . . . . . . 58 4.6 Initialisierung von Member-Variablen . . . . . . . . . . . . . . . . 58 4.7 Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59 4.8 Inner Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 4.9 Ableitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.10 Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 4.11 Ein kurzer Einblick in Packages . . . . . . . . . . . . . . . . . . . 66 5 Fehlerbehandlung 5.1 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 68 6 Swing 73 6.1 Und wieder ein einfaches Beispiel . . . . . . . . . . . . . . . . . . 74 6.2 Die Model-View-Controller Architektur . . . . . . . . . . . . . . 75 6.3 Container und Komponenten . . . . . . . . . . . . . . . . . . . . 76 6.4 Layoutmanager . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77 6.5 Ein weiteres Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . 80 INHALTSVERZEICHNIS 6.6 III Event-Handling . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 6.6.1 Event-Listener Interface und Adapter . . . . . . . . . . . 83 6.6.2 Event-Listener Methoden . . . . . . . . . . . . . . . . . . 84 6.6.3 Kurzer Überblick über konkrete Events in Swing . . . . . 85 6.7 Was passiert beim Zeichnen eines Fensters? . . . . . . . . . . . . 89 6.8 Swing-Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . 90 6.8.1 Top-Level Container . . . . . . . . . . . . . . . . . . . . . 90 6.8.2 Allgemeine Container . . . . . . . . . . . . . . . . . . . . 91 6.8.3 Kontroll Komponenten . . . . . . . . . . . . . . . . . . . . 92 6.8.4 Info Komponenten . . . . . . . . . . . . . . . . . . . . . . 92 6.8.5 Spezielle Komponenten . . . . . . . . . . . . . . . . . . . 93 7 Threads 94 7.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 7.2 Threads in Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 7.3 Lebenszyklus eines Threads . . . . . . . . . . . . . . . . . . . . . 99 7.4 Einige Methoden in Thread . . . . . . . . . . . . . . . . . . . . . 100 7.5 Stolpersteine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 Literaturverzeichnis 106 Changelog 110 Bevor es losgeht... Wieso diese Seiten? Warum denn schon wieder eine Einführung in Java? Wozu das Ganze? Fragen über Fragen, die man sich stellen kann, wenn man diese Unterlagen in die Hände kriegt. Obwohl es viele Gründe gegen ein solches Minimal-Skriptum gibt (z.B. Arbeitsaufwand, Umfang der hier abgedeckt werden kann etc.), sprechen auch einige Gründe dafür: Zeitmangel der Studenten: Es gibt viel zu tun. Da ich auch einmal studiert habe, weiß ich, dass es ziemlich schwer ist, auf eigene Faust ein neues Gebiet – in diesem Fall die Programmiersprache Java – zu erfassen. Diese Unterlagen sollen hier helfen und den Zeitaufwand für das Erlernen der Sprache reduzieren. Vorwissen der Studenten: Einige von euch haben schon Java Programme geschrieben, einige noch nicht. Diese Unterlagen sollen ein Basiswissen vermitteln, damit man die gestellte Programmieraufgabe in vernünftiger Zeit lösen kann. Fun: Seien wir ehrlich: Es macht auch Spaß, Unterlagen zusammenzustellen :) Wir beginnen mit einigen einführenden, beispielhaften Programmen und erläutern die wichtigsten Konzepte der Programmiersprache. Mit diesem Wissen kann man einfache Aufgaben lösen. Hier wird bewusst immer wieder eine Verbindung zu bekannten Programmiersprachen hergestellt, um einen raschen Lernerfolg zu gewährleisten (im Speziellen zu C++). Danach wird ein Überblick über Datentypen, die in Java zur Verfügung stehen, gegeben und die Unterschiede zur Programmiersprache C++ herausgearbeitet. Für Anregungen habe ich selbstverständlich auch immer ein offenes Ohr. Bitte kontaktiert mich einfach via Email oder verwendet das Diskussionsforum (tugraz.lv.prgpraktikum.skriptum) am Newsserver der TU-Graz. Die aktuellste Version der Unterlagen und die dazu passenden Beispiele ist am Server abgelegt. So. Das sollte zur Motivation reichen. Auf geht’s in die erste Runde! IV Kapitel 1 Anatomie eines Java-Programms Um den Einstieg möglichst einfach zu machen, beginnen wir mit der Analyse eines einfachen Programms und untersuchen im Anschluss etwas komplexere Beispiele. Wie man vom Sourcecode zu einem lauffähigen Programm kommt, wird nach dem ersten Beispiel gezeigt. 1.1 Ein sehr einfaches Beispiel Wer will schon ein “Hello World”-Programm? Wir beginnen mit einem VerySimpleExample.java! Bitte nicht verzweifeln, wenn hier schon einige Begriffe verwendet werden, die erst in den nächsten Kapitel genauer erläutert werden. Für das Verständnis ist ein allgemeines Vorwissen zur Programmierung bei weitem ausreichend. Nun aber zum ersten Beispiel: 1 2 3 4 5 6 7 public c l a s s VerySimpleExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ” This i s a very s i m p l e example . . . ” ) ; } } Intuitiv ist vielleicht schon klar, was dieses Programm macht: Es schreibt die Zeichenkette: This is a very simple example... auf die Konsole (stdout). Betrachten wir nun die Zeilen etwas genauer: Zeile 5: Hier wird eine Methode (println(...);) von einem Objekt (out) aufgerufen. Dieses Objekt wird in einem statischen Objekt (System) gehalten. System wird beim Initialisieren der Java Virtual Machine (JVM) erzeugt. 1 KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 2 Zeile 3: Definition einer Methode main die als Argument ein Feld von Strings nimmt (String[] args). Diese statische Methode (static) gibt keinen Wert zurück (void) und kann auch von anderen Objekten aus aufgerufen werden (public). Zeile 1: Definition einer Klasse (class) VerySimpleExample die von Objekten jeder anderen Klasse aus aufgerufen werden kann (public). Die geschwungenen Klammern ({ bzw. }, curly brace) umschließen jeden Block (Zeile 2 und 7 bzw. 4 und 6) von Anweisungen und stehen laut Styleguide ([PPrakt. Java Coding 2002]) in einer eigenen Zeile. Das macht den Code lesbarer im Vergleich zu den Richtlinien, die Sun ([Sun 1999]) vorschlägt. 1.2 Vom Sourcecode zum Programm Der Sourcecode wird in eine Datei gespeichert, die die Erweiterung ’.java’ hat. Als Dateiname muss der in diesem File definierte Klassenname verwendet werden (VerySimpleExample). Die Groß-/Kleinschreibung muss ebenso beachtet werden, d.h. eine Klasse VerySimpleExample muss in einem File VerySimpleExample.java definiert sein. In Java wird von einem Compiler (javac) Bytecode erzeugt. Dieser ist nicht direkt ausführbar. Der Bytecode wird in Dateien mit der Endung ’.class’ gespeichert. Interpretiert wird der Bytecode von einer sog. Java Virtual Machine (JVM, java). Nicht nur Sun (http://java.sun.com), sondern auch andere Hersteller (z.B. IBM) stellen Implementationen der JVM für verschiedenste Betriebssysteme her. In diesem Zusammenhang soll das Blackdown-Projekt genannt werden, in dem eine Implementation der JVM für Linux realisiert wird (die mitunter besser funktioniert als die Implementation von Sun...). Das Interpretieren eines Bytecodes ist selbstverständlich langsamer als das direkte Ausführen von Maschinencode. Aktuelle Technologien wie z.B. HotSpot versprechen hier wesentliche Performance-Verbesserungen1 . Eine zweite Möglichkeit zur Performancesteigerung ist die direkte Umsetzung des Bytecodes in Maschinencode. Just-in-Time Compiler (sog. JITs) setzen hier die .classFiles in Maschinencode um, der dann vom Prozessor ausgeführt wird. Dadurch ergeben sich üblicherweise Performancesteigerungen. Näheres findet man auf den Seiten von Sun bzw. über Google. Das waren nun aber wirklich viel Fakten! Das Ganze zusammengefasst: • Schreiben des Sourcecodes in einem beliebigen Editor (z.B. emacs). Beachte den Filenamen! • Übersetzen des Sourcecodes in Bytecode (javac VerySimpleExample.java erzeugt VerySimpleExample.class, sofern keine Fehler im Sourcecode sind :)) • Interpretieren des Bytecodes mit einer virtuelle Maschine (java VerySimpleExample). Interpreter für den Bytecode gibt es für die unterschiedlichsten Betriebssysteme (Windows, Solaris, Linux, MacOS...). 1 Wer sich für diese Technologie interessiert, soll bitte den Link verfolgen... Eine Beschreibung würde hier den Rahmen sprengen. KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 1.2.1 3 Was alles schiefgehen wird Betrachten wir nun die häufigsten Fehler, die auftreten können (und auch auftreten werden): Vorsicht Falle: Selbstverständlich muss der Compiler (javac) und auch der Interpreter (java) am Rechner installiert sein. Wer auf der Konsole folgende Rückmeldung >javac VerySimpleExample.java javac: Command not found. bekommt, hat entweder das Programm javac nicht im Suchpfad oder hat den Compiler nicht installiert. Wer mit dem Begriff Suchpfad nichts anfangen kann, der sei auf diverse Einführungshandbücher in das jeweils verwendete Betriebssystem verwiesen. Wenn jemand den Compiler noch nicht installiert hat, der soll doch einmal die Java Seiten von Sun im Internet besuchen. Vorsicht Falle: Der Dateiname des Sourcecodes ist hier – im Unterschied zu anderen Programmiersprachen – wirklich wesentlich. Ein Versuch, die Klasse VerySimpleExample in ein File verysimpleexample.java zu speichern ergibt folgenden Fehler: >javac verysimpleexample.java verysimpleexample.java:1: class VerySimpleExample is public, should be declared in a file named VerySimpleExample.java public class VerySimpleExample ^ 1 error Wenn man mit bestimmten Windows-Versionen arbeitet (z.B. Windows 98, nicht aber Windows 2000...), ist es notwendig, den Filenamen unter Anführungszeichen zu schreiben, damit die Groß-/Kleinbuchstaben im Filenamen nicht verloren gehen! Vorsicht Falle: Wenn das Programm richtig compiliert worden ist, wird der Bytecode in ein File (NameOfTheClass.class) abgelegt. Dieser Bytecode muss mit dem Interpreter ausgeführt werden. Hier ist es wesentlich, dass nur der Klassenname, und nicht der Filename des Bytecodes angegeben wird. Gibt man hier den Namen des Bytecode-Files an, bekommt man folgende Meldung: >java VerySimpleExample.class Exception in thread "main" java.lang.NoClassDefFoundError: VerySimpleExample/class KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 4 Man muss den Namen der Klasse, also VerySimpleExample als Parameter für den Interpreter angegeben. Der richtige Aufruf muss also so erfolgen: >java VerySimpleExample This is a very simple example... Vorsicht Falle: Ein ausführbares Programm in einer Klasse muss eine Methode public static void main(String[] args) implementieren. Diese Methode wird von der JVM gesucht und aufgerufen. Sie entspricht der main() Funktion von C++. Betrachten wir folgendes Beispiel: 1 2 3 4 5 6 7 public c l a s s BadVerySimpleExample { public void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ” I ’m a bad s i m p l e example . . . ” ) ; } } Das File lässt sich selbstverständlich compilieren, aber nicht ausführen! >javac BadVerySimpleExample.java >java BadVerySimpleExample Exception in thread "main" java.lang.NoSuchMethodError: main Hier ist die Fehlermeldung vielleicht etwas verwirrend (eine Methode main wurde ja in Zeile 3 definiert...), aber die Signatur, die verlangt wird, ist nicht vorhanden. Der Code ist kompilierbar (da er ja syntaktisch korrekt ist), aber nicht ausführbar. Ihr könnt es mir glauben (oder auch selbst ausprobieren :)), die JVM verlangt (ohne wenn und aber) die richtige Signatur für main. Diese oben beschriebenen Fehlerquellen sind die häufigsten, die Java spezifisch sind. Syntaxfehler werden (wie in jeder Programmiersprache...) separat mit entsprechenden Fehlermeldungen vom Compiler ausgewiesen. 1.2.2 Wohin mit den Klassenfiles? Diese Frage sollte man sich vor allem stellen, wenn man mit mehreren Klassen arbeitet. Gibt man beim Compilieren keine expliziten Parameter an, so werden die Klassenfiles im selben Verzeichnis abgelegt wie die Sourcecodefiles. Für einzelne Tests sollte das wie geschrieben keine Rolle spielen, bei “richtigen” Programmen sollte man diese Vorgehensweise nicht machen. Hinweis: Was man noch wissen sollte: Sind Klassen voneinander abhängig, findet das der Compiler selbstständig heraus (in C++ ist das nicht so, hier muss explizit im Makefile angegeben werden, welche Objekt-Files voneinander abhängig sind...). Hierbei vergleicht der Compiler die Sourcecodefiles mit den KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 5 Klassenfiles. Haben die Klassenfiles ein jüngeres Datum als die Sourcecodefiles, wird ebenso automatisch neu compiliert. Aus dieser Tatsache ergibt sich aber auch, dass der Compiler nicht nur wissen muss, wo der Sourcecode liegt, sondern auch, wo die Klassenfiles liegen (ansonsten könnte er ja nicht herausfinden, ob das Klassenfile “jünger” als der Sourcecode ist...). Die Sourcecodefiles werden im angegeben Verzeichnis gesucht (bzw. bei Packages, siehe Abschnitt 4.11, in Subverzeichnissen), im Pfad, der im Klassenpfad (siehe später) spezifiziert wurde, oder in den Verzeichnissen, die via sourcepathParameter angegeben werden. Das selbe gilt für den Bytecode-Interpreter: Kann dieser die Klassenfiles nicht finden, wird ein NoClassDefFoundError ausgelöst. >java SomeExample Exception in thread "main" java.lang.NoClassDefFoundError: SomeExample Sieht man eine solche Meldung auf der Konsole, passt der Klassenpfad nicht. Die Suche nach den Klassenfiles erfolgt entweder über: • einen Parameter -cp oder -classpath des Compilers bzw. Interpreters oder über • eine Umgebungsvariable CLASSPATH. Umgebungsvariablen werden abhängig vom Betriebssystem bzw. von der verwendeten Shell unterschiedlich gesetzt. Unter Windows wird die Variable via: set CLASSPATH=.\classes;C:\users\users\hkrott\src\java\classes gesetzt, unter Unix (csh) verwendet man das Shell-Kommando: setenv CLASSPATH ./classes:/usr/users/hkrott/src/java/classes Wenn man bash als Shell verwendet, muss man das Kommando export CLASSPATH=./classes:/usr/users/hkrott/src/java/classes ausführen. Vorsicht Falle: Der Suchpfad kann sinnvollerweise mehrere Verzeichnisse bzw. auch sog. jar-Files (was das ist, werden wir noch hören...) beinhalten. Sinnvollerweise (!@%@!?) gibt es je nach Betriebssystem unterschiedliche Trennzeichen zwischen den Einträgen: Unter Windows muss der Pfad mit einem Strichpunkt (“;”) getrennt werden, unter Unix ist es der Doppelpunkt (“:”)’. Über diese Eigenheit stolpert man gelegentlich... Wir wissen nun, wie man die Suche der Sourcecodefiles (mit -sourcecode bzw. -classpath) und der Klassenfiles steuern kann (via -cp bzw. -classpath und CLASSPATH), aber noch nicht, wie man den Speicherort beim Compilieren (mit javac) festlegt: Diese Aufgabe übernimmt der Parameter -d. Vorsicht Falle: Wenn man den -d Parameter verwendet, sollte man sich sicher sein, dass das angegebene Verzeichnis auch wirklich existiert! Existiert es KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 6 nicht, erhält man viiieeellle Fehlermeldungen, die richtig interpretiert werden müssen... Generell kann man für größere Projekte empfehlen, ein eigenes Verzeichnis mit dem Namen classes anzulegen und die genannten Parameter zu verwenden. Eine mögliche Kommandofolge könnte etwas so aussehen: >cd myproject >mkdir classes >javac -d ./classes -classpath ./classes -sourcepath./src src/*java >java -classpath ./classes MyProjectMainClass Ich habe im obigen Beispiel bewusst nicht den sourcepath-Parameter verwendet, um die Komplexität gering zu halten. Der interessierte Leser kann das ja gerne ausprobieren :) Als Trennzeichen wurde hier jenes für Unix verwendet. Hinweis: Es gäbe noch wesentlich mehr zu den Parametern des Compilers und Interpreters zu schreiben. Für die ersten Schritte sind jedoch die oben genannten (-cp bzw. -classpath und -d) ausreichend. Genauere Informationen sind über den help-Parameter bzw. über die Onlinehilfe abrufbar. Das war also unser erstes Java Programm! Nun noch eine Liste mit Punkten, die wir nicht vergessen sollten: • Es gibt in Java keine Funktionen, sondern nur Methoden! • Bei Programmen muss die Methode main() mit der entsprechenden Signatur (public static void main(String[] args) definiert werden. • Der Sourcecode wird in einen Bytecode übersetzt und von der JVM (Java Virtuellen Maschine) interpretiert. • Sourcedateinname entspricht Klassenname (Groß-/Kleinbuchstaben beachten!) • JVM gibt es für verschiedene Betriebssysteme. • Klassen werden über einen Suchpfad gefunden (-cp und -classpath bzw. CLASSPATH-Umgebungsvariable. Nun geht’s weiter mit einem etwas komplizierteren Beispiel. 1.3 Ein einfaches Beispiel Wenn man die Programmiersprache Java etwas genauer untersucht, dann sieht man, dass bei der Toolsammlung (Software Development Kit) der Hersteller (Sun, IBM, etc.) eine Unmenge an Libraries mitgeliefert werden. In der aktuellen Version vom SDK (Java 2 Platform, v 1.4) sind das ca. 3000 Klassen, welche die unterschiedlichsten Aufgaben übernehmen. Einige der Libraries stehen unmittelbar zur Verfügung, andere wiederum müssen explizit angesprochen oder KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 7 importiert werden. Im folgenden Beispiel verwenden wir Strings, die dem Programm in der Kommandozeile übergeben werden. Strings stehen unmittelbar zur Verfügung und müssen nicht explizit geladen/importiert werden. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public c l a s s SimpleExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { // check number o f arguments i f ( a r g s . l e n g t h == 0) { System . e r r . p r i n t l n ( ”Usage : j a v a SimpleExample ” + ”argument1 [ argument2 . . . ] ” ) ; System . e r r . p r i n t l n ( ” Get some i n f o r m a t i o n from the arguments ” + ” g i v e n to the program . ” ) ; return ; } System . out . p r i n t l n ( ” H e l l o and welcome to the argument p r o c e s s o r ! ” ) ; System . out . p r i n t l n ( ”There a r e ” + a r g s . l e n g t h + ” argument ( s ) on the commandline . ” ) ; for ( int i =0; i<a r g s . l e n g t h ; i ++) { System . out . p r i n t l n ( ”argument ” + ( i +1) + ” has ” + a r g s [ i ] . l e n g t h () + ” c h a r a c t e r s . Uppercase : ” + a r g s [ i ] . toUpperCase ( ) ) ; } System . out . p r i n t l n ( ”That ’ s i t . Bye ! ” ) ; } } Bei der Beschreibung gehen wir nun von oben nach unten. Wir arbeiten hier mit Strings und Arrays (Felder) von Strings und verwenden sogar schon eine for-Schleife :) Zeile 1: Definition der Klasse SimpleExample. Zeile 3: Diese hatten wir schon im ersten Beispiel: Definition der main-Methode der Klasse SimpleExample. Beachte die Signatur! Zeile 6: Ein If-Statement (siehe Abschnitt 3.3) überprüft eine Bedingung. Wenn die Anzahl der Argumente (repräsentiert in einem Array von Strings) gleich 0 ist, dann soll das Programm mit einer Meldung abbrechen. Man kann bei einem Array (siehe Abschnitt 2.1.2) die Anzahl der gespeicherten Elemente über das Property (Eigenschaft des Objekts) length abfragen. Zeile 9: Für Fehlermeldungen sollte man Grundsätzlich stderr verwenden. stderr ist bei jedem Java-Programm via System.err verfügbar und wird beim Starten der JVM erstellt. Zeile 12: Beendet das Programm an dieser Stelle und übergibt die Kontrolle wieder dem Betriebssystem. Im Abschnitt über Fehlerkontrolle (siehe Kapitel 5) werden noch weiter Methoden vorgestellt, wie man mit falschen bzw. fehlerhaften Parametern umgeht. Zeile 14-16: Schreibe auf die Konsole (=System.out). Zeile 17: Für jedes übergebene Argument führe die folgenden Anweisungen aus. In Java wird – gleich wie in C++ – der Zähler für die Array-Elemente von 0 weg gezählt, d.h. das Elemente mit dem Index 0 ist das erste Elemente im Array. Siehe Abschnitt 3.2. KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 8 Zeile 19/20: Hier werden die einzelnen Argumente behandelt. Die Argumente werden als String zur Verfügung gestellt. Man beachte den Ausdruck args[i].length(): Hier wird auf das i-te Element im Array zugegriffen (ist ein String) und auf dieses String-Objekt wird die Methode length() aufgerufen. Im Vergleich zum Array wird hier nicht ein Property, sondern tatsächlich eine Methode aufgerufen! Zeile 20: Damit wir auch mit dem String arbeiten, wird hier der übergebene Wert in Großbuchstaben ausgegeben. Schon hier soll auf eine Eigenschaft von Strings hingewiesen werden: Strings sind immutable (unveränderlich), d.h. der String, der in args[i] abgelegt ist, verändert sich durch den toUpperCase()-Methodenaufruf nicht. Zeile 23: Ende der Methode main. Zeile 24: Ende der Klassendefinition. Hinweis: In C++ ist in argv[0] der Programmname gespeichert. Die Anzahl der Argumente wird via argc festgelegt. In Java gibt es bei main nur einen Parameter (String[] args). Dieses Array ist ein Objekt und hat die Länge als Eigenschaft dieses Arrays (args.length). Das erste Argument wird in args[0] gespeichert. Den Namen der Klasse kann man via this.getClass().getName(); herausfinden. Compiliert wird das Programm (nun kann man schon schreiben “wie gewohnt” :)) mit javac SimpleExample.java. Ein Aufruf könnte so aussehen: >java SimpleExample first second third-parameter "fourth parameter" Hello and welcome to the argument processor! There are 4 argument(s) on the commandline. argument 1 has 5 characters. Uppercase: FIRST argument 2 has 6 characters. Uppercase: SECOND argument 3 has 15 characters. Uppercase: THIRD-PARAMETER argument 4 has 16 characters. Uppercase: FOURTH PARAMETER That’s it. Bye! Parameter werden also einfach durch Leerzeichen getrennt bzw. mit Anführungszeichen übergeben. 1.4 Ein einfaches Applet So, bevor wir nun wirklich mit den in Java zur Verfügung gestellten Datentypen beginnen, noch ein einfaches Applet. Vielleicht ist es euch schon zu Ohren gekommen, dass es in Java Applikationen und Applets gibt. Einfache Applikationen haben wir bereits kennen gelernt. Applets hingegen sind Programme, die innerhalb eines Java-fähigen Webbrowsers verwendet werden können (mehr dazu aber wesentlich später!). Vorab einmal ein ganz einfaches Beispiel: KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 1 2 9 import j a v a . a p p l e t . Applet ; import j a v a . awt . Graphics ; 3 4 5 6 7 8 9 10 public c l a s s SimpleAppletExample extends Applet { public void p a i n t ( Graphics gc ) { gc . drawString ( ” I ’m a very s i m p l e a p p l e t ! ” , 1 0 , 1 0 ) ; } } Zeile 1/2: Wir verwenden hier das import Schlüsselwort um anzudeuten, dass wir weitere Klassen verwenden (in diesem Fall java.applet.Applet und java.awt.Graphics). Man könnte diese zwei Zeilen auch weglassen, müsste dann allerdings den sog. full qualified name von der Applet- bzw. Graphics-Klasse angeben, wenn man sie verwenden will. Z.B., Zeile 4 würde dann nicht mehr “...extends Applet” lauten, sondern “...extends java.applet.Applet”. Das ist natürlich etwas umständlich, deshalb wird man das import Schlüsselwort verwenden. Man kann auch mehrere Klassen aus einem Package mit einem Platzhalter importieren (z.B. import java.applet.*), allerdings sollte man das nicht tun, da man relativ schnell den Überblick verliert, welche Klassen nun wirklich im Programm verwendet werden. Zeile 4-10: Hier wird die Klasse SimpleAppletExample definiert. Die Erweiterung extends Applet zeigt an, dass diese Klasse von der Klasse Applet (genauer java.applet.Applet) abgeleitet wird. Zu diesem Zeitpunkt bitte noch nicht verzweifeln: Klassen und Objekte werden noch im Kapitel 4 genauer erläutert. Zeile 6: Definition der Methode paint, die als Parameter ein Objekt vom Typ Graphics verwendet. Diese Methode wird automatisch nach dem Initialisieren eines Applets aufgerufen (mehr dazu auch hier später...). Zeile 8: Die Methode drawString vom Objekt gc wird mit den drei Parametern (text, x, y) aufgerufen. Auch Applets müssen – selbstverständlich – compiliert und damit in Bytecode übersetzt werden (javac SimpleAppletExample). Der Aufruf des Programms erfolgt allerdings nicht mit java SimpleAppletExample (es wurde hier keine main-Methode definiert!). Wie schon oben bemerkt, werden Applets innerhalb eines “Java enabled Webbrowsers” ausgeführt. Damit der Browser das Applet laden kann, werden in einer HTML-Seite spezielle Tags eingefügt. 1 2 3 4 5 6 7 8 9 10 11 <HTML> <HEAD> <TITLE>Using the SimpleAppletExample</TITLE> </HEAD> <BODY> Now the a p p l e t . . . <APPLET CODE=” SimpleAppletExample . c l a s s ” WIDTH=” 200 ” HEIGHT=”25”> </APPLET> That ’ s i t ! </BODY> </HTML> KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 10 In Zeile 7 sieht man die Anwendung des Applet-Tags (mit einigen Parametern). Wenn man nun diese HTML-Seite mit einem Webbrowser anschaut, sieht man folgendes: Abbildung 1.1: HTML-Seite mit dem Applet Weiter will ich zu diesem Zeitpunkt gar nicht auf Applets eingehen. Diese Zeilen sollen nur den Unterschied zwischen Java-Applets und Java-Applikationen zeigen. Viele Eigenschaften von C++ sind auch in Java vorhanden. Einige Konzepte sind sicherlich zu Beginn ungewohnt (jedes Problem wird mit Klassen beschrieben, die Methode main muss bei ausführbaren Programmen mit einer bestimmten Signatur vorhanden sein...), andere Eigenschaften (z.B. Speichermanagement via GC, Garbage Collector) sowie die umfangreiche mitgelieferte API sind sehr angenehm und erleichtern – zumindest zu Beginn :) – den Umgang mit der Sprache. 1.5 Kommentare In Java gibt es zwei unterschiedliche Arten von Kommentaren: Jene, die “nur” Details im Sourcecode kommentieren, und jene, die zusätzliche für die Dokumentation von Klassen und Methoden verwendet werden. Einfache Kommentare sind gleich wie in C++: 1 // A s i m p l e one−l i n e −comment . . . 2 3 4 5 /∗ another comment over more than one l i n e ∗/ 6 7 8 9 10 /∗ the second kind may n e s t the f i r s t kind // so i t ’ s n i c e f o r debuging purpose to comment out one b l o c k : ) ∗/ Jene Kommentare, die auch für die Dokumentation verwendet werden, müssen selbstverständlich speziell markiert werden. Die Auswertung dieser Kommentare übernimmt ein spezielles Programm. Dieses Programm (javadoc) wird mit dem SDK mitgeliefert2 . 1 2 3 4 5 /∗∗ ∗ A s i m p l e example f o r a documentation− ∗ comment used by <b>javadoc </b>. ∗/ public c l a s s SimpleExample . . . 2 Ein weiteres sehr populäres Programm zum Auswerten von Kommentaren ist Doxygen. Mit Doxygen können auch C++- und C-Programme dokumentiert werden. Näheres ist unter http://www.doxygen.org3 u finden. KAPITEL 1. ANATOMIE EINES JAVA-PROGRAMMS 11 Ein Dokumentationskommentar wird – wie im Beispiel dargestellt – über “/**” eingeleitet. Da javadoc HTML-Seiten aus den Kommentaren erstellen kann, ist es auch möglich, Kommentare mit HTML-Tags zu formatieren. Tags (@tagname) geben einzelnen Kommentaren eine bestimmte Bedeutung. Hier ein Auszug aus den vorhandenen Tags: @author @version @param @return @throws ... Für Referenzen innerhalb von Kommentaren wird das Tag @see verwendet. javadoc bietet noch viele weitere Möglichkeiten. Ein Blick in die Source-Files der Java Platform sollte auf alle Fälle gemacht werden, damit man sieht, wie die Tags angewendet werden sollen ([Javadoc Homepage]). 1.6 Schlüsselwörter in Java In Java 1.4 gibt es folgende 52 Schlüsselwörter, die (selbstverständlich) nicht als Namen für Variablen oder Methoden verwendet werden dürfen. abstract byte class do false for import long package return super throw try assert case const double final goto instanceof native private short switch throws void boolean catch continue else finally if int new protected static synchronized transient volatile break char default extends float implements interface null public strictfp this true while Tabelle 1.1: Schlüsselwörter in Java Die Schlüsselwörter goto und const sind zwar definiert, finden aber in Java 2 keine Verwendung. strictfp ist seit Java 2 ein Schlüsselwort. true, false und null sind reservierte Worte für Werte von boolean-Datentypen bzw. ReferenzDatentypen. Nachdem wir jetzt ein gewisses Gefühl für Java bekommen haben, können wir einzelne Sprachelemente genauer betrachten. Auf geht’s in die zweite Runde... Kapitel 2 Datentypen, Variablen und Operatoren Java ist eine stark typisierte Sprache, d.h. jede Variable muss deklariert werden und hat einen bestimmten Typ. Der Typ der Variable muss bereits zur Compilezeit bekannt sein. In diesem Kapitel beschäftigen wir uns mit den in Java zur Verfügung gestellten Datentypen. Wir werden Variablen deklarieren und Operationen mit ihnen ausführen. 2.1 Datentypen Starten wir mit den Datentypen: Grundsätzlich gibt es zwei Arten von Datentypen: Primitive- und Referenz-Datentypen. Hinweis: Im Vergleich zu C gibt es in Java keine expliziten Zeiger, eigene TypDefinitionen (typedef), Aufzählungen (enum), Record-Typen (struct, union) und Bitfelder. Bei genauerem Hinsehen sieht man, dass man typedef, enum, struct und union und Bitfelder einfach mit Klassen abbilden kann (siehe Kapitel 4), die keine Methoden haben. In dieser Hinsicht ist Java sicher einfacher zu handhaben, da man sich nur ein Schlüsselwort merken muss. Über die Tatsache, dass es keine Zeiger gibt, freuen sich sicher auch viele Entwickler :) 2.1.1 Primitive-Datentypen Diese Kategorie der Datentypen kann man in zwei Typen klassifizieren: boolean und numerische Typen (und diesen wieder in Ganzzahlen- und Gleitkommatypen). Zu jedem Primitiven-Datentypen werden passenden Referenz-Datentypen in Form von Klassen zur Verfügung gestellt, die den jeweiligen Wert kapseln und unterschiedlichste Methoden bereitstellen. Zu int gibt es z.B. einen ReferenzDatentyp mit dem Namen Integer, zu byte einen mit dem Namen Byte usw. (siehe Abschnitt 2.1.2). 12 KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN Typ Größe/Format boolean true/false Ganzzahlen byte 8-bit 2-er Komplement (-128 bis 127) short 16-bit 2-er Komplement (-32768 bis 32767) int 32-bit 2-er Komplement (-2147483648 bis 2147483647) long 64-bit 2-er Komplement (-9223372036854775808 bis 9223372036854775807) char 16-bit Unicode Character (\u0000 bis \uffff, 0 bis 65535) Gleitkommazahlen float 32-bit IEEE 754 double 64-bit IEEE 754 13 Beschreibung Boolscher Wert Byte Ganzzahl Short Ganzzahl Integer Ganzzahl Long Ganzzahl Character Single-precision Gleitkommazahl Double-precision Gleitkommazahl Tabelle 2.1: Wertebereich von Primitiven-Datentypen Wie man mit diesen Datentypen arbeitet, sehen wir etwas später im Abschnitt 2.2. Hinweis: In Java sind die numerischen Typen mit Ausnahme von char im Vergleich zu C++ signed Datentypen. Das Schlüsselwort unsigned gibt es in Java nicht! Hinweis: Für alle Primitiven-Datentypen wird vom Interpreter implizit auch eine String-Repräsentationen zur Verfügung gestellt. Wird ein Primitiver-Datentype im Kontext eines Strings verwendet, so wird die entsprechende StringDarstellung verwendet! Siehe auch Beispiel in Abschnitt 2.3.2. boolean-Datentyp Für den boolean-Datentyp gibt es genau zwei Werte: true und false. Wie man in Tabelle 1.1 sehen kann, sind diese beiden Werte bereits definierte Schlüsselwörter der Sprache. Hinweis: Für Kontrollstrukturen, die wir im nächsten Kapitel noch genauer kennenlernen werden, bedeutet das im Gegensatz zu C/C++, dass beim Entscheidungskriterium wirklich ein boolean-Wert ausgewertet wird. Der geübte C/C++-Guru muss hier also etwas umdenken und nicht if (int_value) {...}, sondern explizit if (int_value != 0 ) {...} schreiben. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 14 Ganzzahlige-Datentypen (byte, short, int und long) Im Vergleich zu C/C++ sind die Ganzzahligen-Datentypen nur als signedDatentypen, d.h. mit Vorzeichen verfügbar. byte, short, int und long unterscheiden sich lediglich im Wertebereich (siehe Tabelle 2.1). Die Werte können, wie man es von anderen Programmiersprachen gewohnt ist, in unterschiedlichen Zahlensystemen interpretiert werden. Generell werden sie im Dezimalen-System interpretiert (z.B. int an_int = 10;). Über bestimmte Schreibweisen (“0x” bzw. “0X” als Prefix) wird der Wert als hexadezimale Zahl interpretiert bzw. “0” (die Zahl “Null”) als Prefix wird der Wert als oktale Zahl interpretiert. Dezimale Zahlen werden als 32-Bit Integer verarbeitet. Wenn ein “l” bzw. “L” unmittelbar einer Zahl folgt, wird die Zahl als long-Wert interpretiert (1234 ist ein Wert vom Typ int, 1234L ein Wert vom Typ long). Damit longWerte fehlerfrei gelesen werden können, wird ein großes-L empfohlen, um eine Verwechslung mit “1” zu vermeiden. Für Umwandlungen von Zahlen in Zeichenketten und umgekehrt, stellen die passenden Referenz-Datentypen statische Methoden (siehe Abschnitt 4.5.2) zur Verfügung (z.B. Integer.toString(int), Integer.parseInt(String) bzw. Byte.parseByte(String)). char-Datentyp Wie man in Tabelle 2.1 sieht, kann man in diesem Datentyp alle UnicodeZeichen (16-Bit) ablegen. Der Wert kann direkt zwischen Apostrophen (z.B. char a_char = ’a’;), über den Latin-1 Zeichencode, oder über den UnicodeWert (z.B. char another_char = ’\u0061’;) definiert werden. Steuerzeichen (z.B. Tabulatoren) sind in Tabelle 2.2 dargestellt. Der zum Primären-Datentyp char passende Referenz-Datentyp Character bietet einige statische Methoden an, die mit einem Zeichen arbeiten können (z.B. Character.isLowerCase(char), Character.toLowerCase(char), etc.). Zeichen \b \t \n \f \r \" \’ \\ \xxx \uxxxx Bedeutung Backspace Tabulator Newline Formfeed (Seitenvorschub) Carriage Return (CR) Double Quote Single Quote Backslash Latin-1 Zeichen, wobei xxx eine oktale Zahl (Basis 8) ist. Unicode Zeichen, wobei xxxx dem hexadezimalen Zeichencode (Basis 16) entspricht. Tabelle 2.2: Steuerzeichen KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 15 Gleitkomma-Datentypen (float, double) Zwei Datentypen stehen in Java für Gleitkomma-Zahlen zur Verfügung: float (32-Bit) und double (64-Bit). Es besteht die Möglichkeit, die Werte auch mit einem Exponenten zu versehen (z.B. 13e3 bzw. 13E3). Beim Datentyp float muss bei der Zuweisung von Werten explizit ein f bzw. F folgen, ansonsten wird das Programm nicht compiliert (“possible loss of precision...”). Werte von Gleitkomma-Datentypen können nicht hexadezimal oder oktal ausgedrückt werden. Jedem Informatiker ist klar, dass Gleitkommazahlen in der Repräsentation im Computer nur eine Annäherung an den echten Wert der Zahl darstellen kann. Variablen vom Typ float (32-Bit) garantieren mindestens 6 signifikante Dezimalstellen, vom Typ double (64-Bit) garantieren mindestens 15 signifikante Dezimalstellen. Interessant ist die Tatsache, dass auch Divisionen durch 0 mit den GleitkommaDatentypen berechnet werden können. Hierbei kommt es zu keiner Fehlermeldung! Der Wert wird in diesem Fall richtigerweise auf “Infinity” gesetzt. Ebenso werden Divisionen durch Unendlich (Ergebnis: “0”) und auch die Division von 0 durch 0 richtig berechnet (Ergebnis: “Not-a-Number”). Wie sich der Leser inzwischen schon vorstellen kann, sind diese Werte in den entsprechenden Referenz-Datentypen Float und Double gespeichert. In Java können Variablen von einem bestimmten Primären-Datentyp, einer Variablen mit einem anderen Primären-Datentyp zugewiesen werden. Ist der Wertebereich der neuen Variable größer, als der der ursprünglichen, erfolgt die Umwandlung automatisch. Es ist also möglich, einer Variablen vom Typ double, den Wert einer Variablen vom Typ float zuzuweisen (widening conversion). Wenn die Umwandlung in die andere Richtung passiert (narrowing conversation), muss diese Konvertierung explizit über sog. casts durchgeführt werden. int a n i n t = 1 3 ; byte a b y t e = ( byte ) a n i n t ; short a s h o r t = ( short ) 1 3 . 6 7 ; Diese Umwandlung passiert gleich wie in C/C++. Bei der Umwandlung in der letzten Zeile (von einem float in einen short) wird einfach der Teil nach dem Komma weggeschnitten. Für Auf-/Abrundungen stehen statische Methoden in der Klasse Math zur Verfügung (z.B. Math.ceil(double), Math.floor(double) etc.). Nun noch ein kurzes Beispiel, das die Maximalwerte der jeweiligen PrimitivenDatentypen ausgibt. Man sieht, dass hier auf die entsprechenden ReferenzDatentypen zurückgegriffen wird, um die Maximalwerte zu setzen. Es wird hier aus Platzgründen aber auch aus Gründen der Motivation nicht auf alle Methoden und Eigenschaften dieser speziellen Referenz-Datentypen eingegangen. Der interessierte Leser hat die Möglichkeit, den Sourcecode (z.B. src/java/lang/ Byte.java) zu inspizieren. 1 2 3 4 5 6 public c l a s s PrimitiveMaxValuesExample { public s t a t i c void main ( S t r i n g a r g s [ ] ) { // boolean boolean a b o o l e a n = true ; KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 16 7 // i n t e g e r s / char byte l a r g e s t b y t e = Byte .MAX VALUE; short l a r g e s t s h o r t = Short .MAX VALUE; int l a r g e s t i n t = I n t e g e r .MAX VALUE; long l a r g e s t l o n g = Long .MAX VALUE; char a c h a r = ’ S ’ ; 8 9 10 11 12 13 14 // r e a l numbers f l o a t l a r g e s t F l o a t = Flo at .MAX VALUE; double l a r g e s t D o u b l e = Double .MAX VALUE; 15 16 17 18 System . out . System . out . System . out . System . out . System . out . 19 20 21 22 23 p r i n t l n ( ”The p r i n t l n ( ”The p r i n t l n ( ”The p r i n t l n ( ”The p r i n t l n ( ”The value of a boolean i s ” + a boolean ) ; l a r g e s t byte v a l u e i s ” + l a r g e s t b y t e ) ; l a r g e s t short value i s ” + l a r g e s t s h o r t ) ; l a r g e s t i n t e g e r value i s ” + l a r g e s t i n t ) ; l a r g e s t long value i s ” + l a r g e s t l o n g ) ; 24 i f ( Character . isUpperCase ( a c h a r ) ) { System . out . p r i n t l n ( ”The c h a r a c t e r ” + a c h a r + ” i s upper c a s e . ” ) ; } else { System . out . p r i n t l n ( ”The c h a r a c t e r ” + a c h a r + ” i s lower c a s e . ” ) ; } 25 26 27 28 29 30 31 32 33 System . out . p r i n t l n ( ”The l a r g e s t f l o a t v a l u e i s ” + l a r g e s t F l o a t ) ; System . out . p r i n t l n ( ”The l a r g e s t double v a l u e i s ” + l a r g e s t D o u b l e ) ; 34 35 } 36 37 } Aufruf auf der Konsole: exttt¿java PrimitiveMaxValuesExample The The The The The The The The value of a_boolean is true largest byte value is 127 largest short value is 32767 largest integer value is 2147483647 largest long value is 9223372036854775807 character S is upper case. largest float value is 3.4028235E38 largest double value is 1.7976931348623157E308 2.1.2 Referenz-Datentypen Will man mehr als ein Zeichen oder eine Zahl in einer Variable speichern, muss man einen Referenz-Datentyp verwenden. Der Wert einer solchen Variable ist eine Referenz (d.h. eine Adresse) zum Speicherbereich, wo ein Objekt abgelegt ist. Der Speicher für diese Variable muss selbstverständlich reserviert werden. Zu den Referenz-Datentypen zählen sowohl Klassen, als auch das Array (=Feld von Werten). Jeder Referenz-Datentyp ist von java.lang.Object abgeleitet und erbt die darin implementierten Methoden (Abschnitt 4.2). Eine besonders oft verwendete Methode ist die toString()-Methode, die implizit aufgerufen wird, sobald das Objekt im Kontext eines Strings verwendet wird. In Java ist es nicht möglich, direkt auf Adressen zuzugreifen (wie z.B. in C oder C++ mit Pointer). Vor allem für Anfänger ist das ein großer Vorteil, da man sich KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 17 nicht um die Freigabe des Speichers kümmern muss. Diese Aufgabe übernimmt der “garbage collector” (GC, ein Modul in der Virtuellen Maschine). Wir werden später (Abschnitt 4.4) noch sehen, welche Methoden einer Klasse zur Verfügung stehen. Zu jedem Primären-Datentyp gibt es – wie schon mehrfach angesprochen – einen passenden (und unveränderlichen, engl. immutable) Referenz-Datentyp der u.a. statische Methoden zur Verfügung stellt. In den folgenden Abschnitten werden zwei Klassen zum Speichern von Zeichenketten betrachten (String und StringBuffer) sowie Felder (Arrays) untersuchen. Das Klassenkonzept von Java wird im nächsten Kapitel genauer erläutert. String und StringBuffer Mit Zeichenketten arbeitet man täglich, daher kommt hier ein kurzer Überblick über zwei Klassen, die mit Zeichenketten arbeiten können. Die Einsatzgebiete für diese beiden Klassen sind aufgrund der Eigenschaften unterschiedlich. Die String-Klasse eignet sich zum Speichern von Zeichenketten, die sich nicht ändern. Der Wert der gespeicherten Zeichenkette kann nicht mehr verändert werden, d.h. Strings sind konstant (man sagt, die Klasse ist bzw. Objekte dieser Klasse sind immutable, also unveränderlich). Für die interne Repräsentation von Strings hat das den Vorteil, dass unterschiedliche String-Objekte, die den gleichen Wert haben, die selben Speicherbereiche verwenden können und damit Speicherplatz gespart werden kann. String-Objekte können entweder explizit mit dem new-Operator oder implizit über doppelte Anführungszeichen (double quotes) erzeugt werden. Im folgenden Beispiel sieht man, wie das in einem praktischen Beispiel umgesetzt werden kann. Bis zur zweiten Zeile verwenden die Variablen a string und another string intern den selben Speicherplatz! Erst in der dritten Zeile wird ein neues String-Objekt erzeugt und der Variablen another string zugewiesen. S t r i n g a s t r i n g = new S t r i n g ( ”some v a l u e o f the s t r i n g ” ) ; S t r i n g a n o t h e r s t r i n g = ”some v a l u e o f the s t r i n g ” ; a n o t h e r s t r i n g = ” changed v a l u e o f the s t r i n g ” ; Vorsicht Falle: Die Tatsache, dass Strings konstant sind, muss man akzeptieren. Ignoriert man diese Tatsache, können implizit viele String-Objekte erzeugt werden, die natürlich auch wieder aus dem Speicher entfernt werden müssen. Das beeinflusst nicht nur das Speichermanagement sondern auch das Laufzeitverhalten (siehe nächstes Beispiel) der Applikation. Will man mit Zeichenketten arbeiten, die dynamisch zusammengesetzt werden, macht es Sinn, die StringBuffer-Klasse zu verwenden. Es werden in diesem Fall weniger Objekte erzeugt und verworfen. Dadurch wird das Laufzeitverhalten des Programms insgesamt besser als bei der Verwendung von StringObjekten. Wie man mit Objekten arbeitet wird noch genauer im folgenden Kapitel behandelt. Ein Beispiel sagt mehr als 1000 Worte. 1 2 3 4 5 public c l a s s StringExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { String a s t r = ” value of a s t r ” ; KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 18 S t r i n g B u f f e r a s t r b u f = new S t r i n g B u f f e r ( ” v a l u e o f a s t r b u f ” ) ; S t r i n g append = ”append t h i s v a l u e . . . ” ; long s t a r t t i m e ; long end time ; f i n a l int LOOP COUNT = 1 0 0 0 ; 6 7 8 9 10 11 // append now to S t r i n g−Object s t a r t t i m e = System . c u r r e n t T i m e M i l l i s ( ) ; for ( int count = 0 ; count < LOOP COUNT; count++) { a s t r += append ; } end time = System . c u r r e n t T i m e M i l l i s ( ) ; System . out . p r i n t l n ( ”Working with S t r i n g−Objects c o s t s ” + ( end time − s t a r t t i m e ) + ” msec . ” ) ; System . out . p r i n t l n ( ” a s t r . l e n g t h ( ) i s : ” + a s t r . l e n g t h ( ) ) ; 12 13 14 15 16 17 18 19 20 21 22 // append now to S t r i n g B u f f e r −Object s t a r t t i m e = System . c u r r e n t T i m e M i l l i s ( ) ; for ( int count = 0 ; count < LOOP COUNT; count++) { a s t r b u f . append ( append ) ; } end time = System . c u r r e n t T i m e M i l l i s ( ) ; System . out . p r i n t l n ( ”Working with S t r i n g B u f f e r −Object c o s t s ” + ( end time − s t a r t t i m e ) + ” msec . ” ) ; System . out . p r i n t l n ( ” a s t r b u f . l e n g t h ( ) i s : ” + a s t r b u f . l e n g t h ( ) ) ; 23 24 25 26 27 28 29 30 31 32 } 33 34 } Aufruf auf der Konsole: exttt¿java StringExample Working with String-Objects costs 598 msec. a_str.length() is: 20014 Working with StringBuffer-Object costs 3 msec. a_str_buf.length() is: 20018 Für die beiden Klassen zur Behandlung von Zeichenketten gibt es selbstverständlich noch wesentlich mehr Methoden als die hier vorgestellte Methode length(). Da wir Klassen und Methoden erst im nächsten Kapitel eingehender behandeln werden, macht es zu diesem Zeitpunkt noch keinen Sinn, näher auf die beiden Klassen einzugehen. Der interessierte Leser hat die Möglichkeit, schon jetzt die online-Dokumentation der beiden Klassen durchzublättern. Neben Zeichenketten werden auch Felder (arrays) in Java speziell behandelt. Arrays Arrays sind Referenz-Datentypen, in denen Werte eines beliebigen Datentyps gespeichert werden, d.h. es können selbstverständlich auch Arrays in einem Array gespeichert werden. Sie sind immer typisiert. Nun einmal zwei Beispiele, wie Arrays deklariert werden können: Einmal ist [] nach dem Variablennamen, und einmal nach dem Typ (siehe auch examples/datentypen/IntArrayExample. java). int i n t a r r a y [ ] ; int [ ] a n o t h e r i n t a r r a y ; Die Länge des Arrays spielt bei der Deklaration keine Rolle. Eine Deklaration einer Variable ist natürlich zu wenig um damit arbeiten zu können. Varia- KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 19 blen müssen auch definiert werden, damit sie verwendet werden können (Abschnitt 2.2)! Wird nun versucht, auf die oben deklarierten Variablen int array bzw. anotherint array zuzugreifen, wird bereits beim Compilieren ein Fehler ausgegeben (variable int array might not have been initialized). Erst bei der Definition wird Speicher für die Elemente reserviert und damit die Länge des Arrays festgelegt. Die Definition erfolgt über den new-Operator. Das nächste Beispiel zeigt Arrays unterschiedlicher Länge vom Typ int: i n t a r r a y = new int [ 5 ] ; a n o t h e r i n t a r r a y = new int [ 2 ] ; Nach dem Erzeugen des Arrays werden die Werte im Array mit dem DefaultWert des Datentyps initialisiert. In unserem Fall (Array mit int Elementen) ist das der Wert 0. Deklaration und Definition können auch in einem Schritt erfolgen: Im folgenden Beispiel in der ersten Zeile das Array mit den Default-Werten initialisiert, in der zweiten Zeile werden den Elementen bereits bei der Definition bestimmte Werte zugewiesen. int [ ] s o m e i n t a r r a y = new int [ 4 ] ; int [ ] s o m e o t h e r i n t a r r a y = { 1 , 2 , 3 } ; Vorsicht Falle: Werden in einem Array Elemente eines Referenz-Datentyps gehalten, so wird lediglich Speicher für die Referenzen (nicht für die Objekte selbst!) reserviert, d.h. die Elemente in dem Array werden auf die DefaultWerte von Referenz-Datentypen (=null) gesetzt. Diesen Umstand sollte man nicht ignorieren (siehe ObjectArrayExample.java). Die Anzahl der Werte (=Länge des Arrays), die in einem Array gespeichert werden können, ist in der Eigenschaft (engl. property) length des Arrays gespeichert. Der Zugriff auf einzelne Elemente erfolgt intuitiv (wie in C++) über die Variable, gefolgt von dem Index in eckigen Klammern. Der Index beginnt wie in C/C++ gewohnt bei 0 und geht bis length-1. int array [1] = 1; int array [2] = 3; int array [3] = 5; for ( int count = 0 ; count < i n t a r r a y . l e n g t h ; count++) { System . out . p r i n t l n ( ” c o n t e n t o f i n t a r r a y at index : ” + i n t a r r a y [ count ] ) ; } Wenn versucht wird, mit einem ungültigen Elementindex (kleiner 0 bzw. größer length-1) auf das Array zuzugreifen, so wird eine java.lang.ArrayIndexOutOfBoundsException-Exception (siehe Kapitel 5) generiert. Vorsicht Falle: Die bei der Definition der Array-Variable festgelegte Größe kann nachträglich nicht verändert werden. Wenn man daher ein Array vergrößern will, muss man ein neues Array mit der gewünschten Größe anlegen und die Werte in dieses neue Array kopieren. Für spezielle Anwendungen stehen natürlich vorgefertigte Klassen zur Verfügung (java.util.Vector etc.) auf die jetzt nicht eingegangen wird. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 20 Sollen die Werte von einem Array in ein anderes kopiert werden, so kann man das entweder selbst implementieren (kopiere Element für Element), oder man verwendet die arraycopy-Methode vom System-Objekt. Wenn man die Signatur dieser Methode in java/lang/System.java betrachtet, sieht man, dass diese Methode native in der JVM ausgeführt wird. Es ergeben sich dadurch Performance-Vorteile. Für Arrays mit primitiven Datentypen ist das Verhalten beim Kopieren offensichtlich: Es werden die Werte der Elemente kopiert. Bei Arrays mit ReferenzDatentypen als Elemente ist das Verhalten auf den ersten Blick vielleicht nicht ganz verständlich: Es werden in diesem Fall nur die Werte der Referenzen kopiert, nicht die Inhalte selbst! Dieses Verhalten sollte im nächsten Kapitel klarer werden. Die Syntax von System.arraycopy: System . arraycopy ( s r c a r r a y , src start index , dest array , dest start index , length ) ; Ist src array und dest array gleich, wird das Feld so kopiert, als ob die Werte von src array zuerst in ein temporäres Objekt kopiert werden, und erst dann über arraycopy verwendet werden. Auch hier soll ein Beispiel die Anwendung von arraycopy zeigen: 1 2 3 4 5 6 7 public c l a s s ObjectArrayExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Object [ ] o b j a r r a y ; Object [ ] a n o t h e r a r r a y = new Object [ 3 ] ; o b j a r r a y = new Object [ 2 ] ; 8 9 10 11 12 13 // s e t v a l u e s o f o b j a r r a y . . . S t r i n g a s t r i n g = new S t r i n g ( ” H e l l o ” ) ; S t r i n g B u f f e r a s t r i n g b u f = new S t r i n g B u f f e r ( ” S t r i n g B u f f e r −Object ” ) ; obj array [0] = a string ; obj array [1] = a string buf ; 14 15 16 17 18 System . out . p r i n t l n ( ” l e n g t h o f o b j a r r a y : ” + o b j a r r a y . l e n g t h ) ; System . out . p r i n t l n ( ” l e n g t h o f a n o t h e r a r r a y : ” + a n o t h e r a r r a y . l e n g t h ) ; System . out . p r i n t l n ( ”Copy now c o n t e n t s o f o b j a r r a y to a n o t h e r a r r a y . . . ” ) ; System . arraycopy ( o b j a r r a y , 0 , a n o t h e r a r r a y , 1 , 2 ) ; 19 20 21 22 23 24 System . out . p r i n t l n ( ” Contents o f o b j a r r a y i s : ” ) ; for ( int count = 0 ; count < o b j a r r a y . l e n g t h ; count++) { System . out . p r i n t ( o b j a r r a y [ count ] ) ; } 25 26 27 28 29 30 System . out . p r i n t l n ( ”\ nContents o f a n o t h e r a r r a y i s : ” ) ; for ( int count = 0 ; count < a n o t h e r a r r a y . l e n g t h ; count++) { System . out . p r i n t ( a n o t h e r a r r a y [ count ] ) ; } 31 32 33 34 System . out . p r i n t l n ( ”\nChange now v a l u e s o f r e f e r e n c e s . . . ” ) ; a s t r i n g = new S t r i n g ( ”Hi ” ) ; a s t r i n g b u f . append ( ” changed ” ) ; 35 36 37 38 39 40 System . out . p r i n t l n ( ” Contents o f o b j a r r a y i s : ” ) ; for ( int count = 0 ; count < o b j a r r a y . l e n g t h ; count++) { System . out . p r i n t ( o b j a r r a y [ count ] ) ; } 41 42 System . out . p r i n t l n ( ”\ nContents o f a n o t h e r a r r a y i s : ” ) ; KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN for ( int count = 0 ; count < a n o t h e r a r r a y . l e n g t h ; count++) { System . out . p r i n t ( a n o t h e r a r r a y [ count ] ) ; } 43 44 45 46 } 47 48 21 } Aufruf auf der Konsole: exttt¿java ObjectArrayExample length of obj_array: 2 length of another_array: 3 Copy now contents of obj_array to another_array... Contents of obj_array is: Hello StringBuffer-Object Contents of another_array is: nullHello StringBuffer-Object Change now values of references... Contents of obj_array is: Hello StringBuffer-Object changed Contents of another_array is: nullHello StringBuffer-Object changed Interessant im obigen Beispiel ist, dass zwar eine Änderung des StringBufferObjekts in Zeile 34 in beiden Feldern sich nach dem Kopieren auswirkt, jedoch die Änderung des String-Objekts keine Änderung in den Arrays bewirkt. Nach den Ausführungen zu String- und StringBuffer-Objekten sollte dieses Verhalten allerdings klar sein, da Strings immutable sind und sich in Zeile 33 nicht der Wert, sondern die Referenz ändert! Zu diesem Zeitpunkt wäre es noch viel zu früh, sich mit weiteren möglichen Fehlern auseinander zusetzen (dest array könnte z.B. zu klein sein, oder null, oder...). Hier verweise ich auf später... Mehrdimensionale Arrays Mehrdimensionale Arrays sind einfach Arrays von Arrays [von...], wobei auch die geschachtelten Arrays unterschiedliche Längen haben können. Im folgenden Beispiel wird ein zweidimensionales Array unterschiedlicher Längen erzeugt und dann dargestellt. 1 2 3 4 5 6 7 8 public c l a s s MultidimensionalArrayExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { int [ ] [ ] i n t a r r a y a r r a y = {{ 1 , 2 , 3 } , { 4, 5, 6, 7, 8 }, { 9 , 10 , 11 , } , { 12 , 13 , 14 }}; 9 10 11 12 13 14 15 for ( int rows =0; rows<i n t a r r a y a r r a y . l e n g t h ; rows++) { for ( int columns =0; columns<i n t a r r a y a r r a y [ rows ] . l e n g t h ; columns++) { System . out . p r i n t l n ( ” i n t a r r a y a r r a y [ ” + rows + ” ] [ ” + columns + ” ] : ” + i n t a r r a y a r r a y [ rows ] [ columns ] ) ; KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN } 16 } 17 } 18 19 22 } Aufruf auf der Konsole: exttt¿java MultidimensionalArrayExample int_array_array[0][0]: int_array_array[0][1]: int_array_array[0][2]: int_array_array[1][0]: int_array_array[1][1]: int_array_array[1][2]: int_array_array[1][3]: int_array_array[1][4]: int_array_array[2][0]: int_array_array[2][1]: int_array_array[2][2]: int_array_array[3][0]: int_array_array[3][1]: int_array_array[3][2]: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Auch hier gilt: Sollen Referenz-Datentypen in einem Array gespeichert werden, so muss wieder Speicher für die eigentlichen Objekte reserviert werden (siehe auch Sun-Tutorial1 ). 2.2 Variablen Die Begriffe Deklaration und Definition haben wir bereits verwendet. Zur Sicherheit kommt noch einmal die Definition der Deklaration und Definition: Deklaration: Hier wird ein Bezeichner (identifier ) bekannt gegeben. Über diesen Bezeichner kann dann auf den deklarierten Bereich zugegriffen werden. Es wird bei der Deklaration kein Speicherbereich für eine ReferenztypVariable reserviert! Bsp.: String a string ; int [ ] a n i n t a r r a y ; Der Bezeichner darf kein Schlüsselwort sein (siehe Abschnitt 1.6). Hier könnte der Compiler nicht feststellen, dass es sich um eine VariablenDeklaration handelt und würde eine Fehlermeldung ausgeben. Definition: Bei einer Definition wird für Variablen Speicher reserviert oder für Methoden eine Implementation zur Verfügung gestellt. Bei Variablen mit Primitive-Datentyp ist die Deklaration gleichzeitig die Definition. Auch für Referenz-Datentypen kann die Deklaration und die Definition in einer Zeile erfolgen. Bsp.: 1 http://java.sun.com/docs/books/tutorial/java/data/arrays.html KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 23 S t r i n g a s t r i n g = new S t r i n g ( ” v a l u e o f the s t r i n g ” ) ; int [ ] a n i n t a r r a y = new int [ 2 5 ] ; Eine Variable entspricht einem Speicherbereich eines bestimmten Typs. Dieser Datentyp wird bei der Deklaration der Variable angegeben. So, nun die Definition von Variablen, wie sie in der Spezifikation steht (mit dem Wissen, dass diese Zeilen hier nicht auf Anhieb verstanden werden können): Primitiv-Datentyp Variablen halten genau den Datentyp, der bei der VariablenDefinition angegeben wurde. Referenz-Datentyp Variablen halten entweder einen null type oder eine Referenz zu einer Variablen mit genau dem Datentyp (oder einem abgeleiteten Datentyp), der bei der Deklaration angegeben wurde. Variablen vom Typ interface halten entweder einen null type oder eine Instanz einer Klasse, die dieses Interface implementiert. Schließlich gibt’s dann noch den array type, der sowohl auf Primitiv- als auch auf Referenz-Datentypen angewendet werden kann: Bei einem Array von Primitiv-Datentypen ist hier entweder eine null-Referenz gespeichert, oder es ist eine Referenz zu einem Array vom entsprechenden Typ gespeichert. Bei Arrays mit Referenz-Datentyp Variablen kann es sich auch um einen abgeleiteten Datentyp handeln ([Gosling et al., 1996]). Wer diese beiden Absätze sofort verstanden hat: Meine Gratulation. Auf den nächsten Seiten sind einige Beispiele mit Variablen und den unterschiedlichsten Datentypen zu finden. Diese Beispiele sollten zum Verständnis beitragen. 2.2.1 Deklaration von Variablen Variablen müssen deklariert werden, bevor sie verwendet werden können. Diese Deklaration schaut generell wie folgt aus: [ f i n a l ] type name ; Implizit ergibt sich je nachdem, wo die Deklaration einer Variablen passiert, ein Gültigkeitsbereich (scope) für die Variable (siehe Abschnitt 2.2.3). final: Eine Variable darf nur einmal initialisiert werden. type: Primitiv- oder Referenz-Datentyp name: Der Name einer Variable kann nicht beliebig sein. Die Länge ist zwar unbeschränkt, aber folgende Bedingungen sind einzuhalten: • Das erste Zeichen darf keine Zahl sein; • Keine Zeichen, die in der Sprache verwendet werden (z.B. “.”, “,”, “+”, etc.) • Der Name darf kein Schlüsselwort sein (if, while, etc. siehe 1.6). • Der Name darf kein in Java reserviertes Wort sein (true, false oder null); • Der Name muss eindeutig im Scope sein, d.h. eine Variable darf nur einmal im Scope deklariert werden. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 24 Für die Vergabe von Namen der Variablen sind je nach Art (member- oder autobzw. lokale-Variable) bestimmte Konventionen einzuhalten (siehe z.B. [PPrakt. Java Coding 2002]). Vorsicht Falle: Es gibt unterschiedliche Meinungen zu final in Java vs. const in C++. Tatsache ist, dass bei final die Variable nur einmal initialisiert werden darf. Für Primitive-Datentypen ist das Verhalten wie man es sich vorstellt: Einmal initialisiert, darf der Wert der Variable nicht mehr verändert werden. Der Compiler gibt entsprechende Fehlermeldungen aus, wenn man es trotzdem versucht. Arbeitet man nun mit Referenz-Datentypen, die Veränderungsmethoden anbieten, dann kann der Wert der Variablen nach dem Initialisieren aber sehr wohl verändert werden. Dieses Verhalten ist auf den ersten Blick vielleicht nicht ganz intuitiv. Betrachtet man nun das const-Schlüsselwort in C++, dann kann man Objekte, die mit dem Schlüsselwort const deklariert wurden, tatsächlich nicht mehr verändern. Bei genauerer Betrachtung sieht man allerdings sofort, dass man jederzeit mit dem const cast-Operator ein Objekt, das ursprünglich konstant deklariert wurde, verändern kann! Eine mögliche Art, ein Objekt konstant sein zu lassen, ist die, dass man keine Veränderungsmethoden für die jeweilige Klasse implementiert und keine direkten Zugriff auf Membervariablen zulässt. Dieser Ansatz wird in Java konsequent verfolgt (siehe z.B. String-Klasse) und sollte auch im eigenen Klassendesign umgesetzt werden. Nun noch ein Beispiel, dass die Verwendung von final bei einem PrimitivenDatentyp demonstriert: 1 2 3 4 5 6 7 public c l a s s PrimitiveFinalExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { f i n a l int AN INT ; f i n a l int ANOTHER INT = 1 0 ; int s o m e i n t ; 8 // the f o l l o w i n g i s a l l o w e d . . . AN INT = 1 0 0 ; // not allowed , because v a r i a b l e i s a l r e a d y i n i t i a l i z e d ANOTHER INT = 1 0 0 ; // a l l o w ed s o m e i n t = AN INT ; // not allowed , because a n i n t i s a l r e a d y i n i t i a l i z e d AN INT = s o m e i n t ; 9 10 11 12 // 13 14 15 16 // } 17 18 } Wenn die Kommentare in Zeile 12 bzw. 16 weggenommen werden, lässt sich das Programm nicht übersetzen: >javac PrimitiveFinalExample.java PrimitiveFinalExample.java:12: cannot assign a value to final variable ANOTHER_INT ANOTHER_INT = 100; ^ 1 error KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 25 bzw. >javac PrimitiveFinalExample.java PrimitiveFinalExample.java:16: variable AN_INT might already have been assigned to AN_INT = some_int; ^ 1 error Ein Beispiel mit einem Referenz-Datentyp ist bei den Übungsbeispielen zu finden (example/datentypen/ReferenceFinalExample.java). 2.2.2 Default Werte Wird einer Variable nicht explizit ein Wert zugewiesen, so wird sie mit einem default-Wert initialisiert (ausgenommen sind hier die lokalen Variablen, siehe Abschnitt 2.2.3). In Tabelle 2.3 wird ein Überblick über die default-Werte unterschiedlicher Datentypen gegeben. Der Referenz-Datentyp wird mit dem reservierten Wort null initialisiert. Typ byte short int long float double char boolean reference type Default Wert (byte)0 (short)0 0 0L 0.0f 0.0d \u0000 false null Tabelle 2.3: Default Werte von Variablen 2.2.3 Arten und Scope von Variablen Je nachdem, in welchem Bereich der jeweiligen Klasse die Variablen definiert werden, findet die Initialisierung unterschiedlich statt und der Gültigkeitsbereich ist unterschiedlich. Grundsätzlich kann an jeder Stelle des Programms eine Variable deklariert werden. In der Java-Spezifikation wird zwischen folgenden 7 Variablenarten unterschieden: Klassenvariable: Variablen, die in einer Klassendefinition static (oder in einer Interfacedefinition mit oder ohne dem static-Schlüsselwort) deklariert werden (mehr dazu etwas später...). Die Variable wird mit einem default-Wert initialisiert, sobald die Klasse das erste Mal geladen wird. Alle Instanzen dieser Klasse teilen sich diese Variable! Die Lebenszeit der Variable (lifetime) endet, sobald das letzte Objekt dieser Klasse vom Speicher entfernt wird (d.h. nicht mehr verwendet wird). KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 26 Hinweis: Wie man den Zugriff auf Klassen- und Instanzvariablen von außen (d.h. von anderen Klassen und auch Packages) beschränken kann, wird noch ausführlich in Abschnitt 4.5 behandelt. Instanzvariable: Variablen, die ohne das Schlüsselwort static in einer Klasse deklariert werden. Jede Instanz der Klasse, also jedes Objekt der Klasse, hat eine private Instanzvariable. Diese Variable wird auf den defaultWert initialisiert, sobald die Instanz erzeugt wird und noch bevor der Konstruktor der Klasse aufgerufen wird. Wenn das Objekt aus dem Speicher gelöscht wird (d.h. nicht mehr referenziert wird), wird auch der Speicherbereich dieser Variable freigegeben und es kann nicht mehr darauf zugegriffen werden (wie sollte man auch, wenn keine Referenz mehr da ist?). Array Elemente: Elemente eines Arrays haben keinen Namen und werden erzeugt und initialisiert, wann immer das entsprechende Array erzeugt wird. Die lifetime endet, wenn es keine Referenz mehr auf das Array gibt. Lokale Variablen: Diese Variablen werden bei der Deklaration der Variablen gültig, und sobald der Block wieder verlassen wird, werden sie wieder ungültig. Es werden hier keine default-Werte gesetzt! Beim Zugriff auf nicht initialisierte lokale Variablen gibt der Compiler eine Fehlermeldung aus. Vorsicht Falle: Für lokale Variablen gibt es keine default-Wert Initialisierung! Methoden Parameter: Sobald Methoden aufgerufen werden, werden diese Parameter-Variablen auf den Wert, mit dem sie aufgerufen werden initialisiert. Gültig sind die Variablen nur innerhalb der Methode. Hinweis: Bei Methodenaufrufen gibt es im Unterschied zu C++ keine default-Werte für Parameter! Vorsicht Falle: In Java werden die Parameter ausschließlich via call-byvalue übergeben. Wenn Referenz-Datentypen verwendet werden, wird der Wert der Referenz (also die Adresse der Variable) übergeben. Konstruktor Parameter: Sobald ein Objekt instantiiert und der Konstruktor der Klasse aufgerufen wird, werden diese Parameter mit dem Wert des übergebenen Parametern initialisiert. Wird der Konstruktor beendet, so verlieren auch die Konstruktor-Parameter ihre Gültigkeit. Exceptionhandler Parameter: Sobald der catch-Block eines try-catch-Blocks verarbeitet wird, werden diese Variablen auf die entsprechenden Werte initialisiert. Der Gültigkeitsbereich endet, wenn der catch-Block verlassen wird. Siehe Kapitel 5. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 27 Da wir Exceptions und Konstruktoren etwas später behandeln werden, vorab einmal ein kleines Programm, das den Scope von Variablen zeigt. 1 2 3 4 5 6 7 public c l a s s ScopeOfVariables { // a s i m p l e c l a s s wide v a r i a b l e shared over a l l i n s t a n c e s o f // t h i s c l a s s s t a t i c int a c l a s s i n t = 1 0 ; // an i n s t a n c e wide v a r i a b l e int a n i n s t a n c e i n t = 1 0 ; 8 // some method with some method parameter , scope o f t h i s // parameters i s r i g h t i n the method ! public void c h a n g e V a r i a b l e s ( int a method int , int a n o t h e r m e t h o d i n t ) { // l o c a l v a r i a b l e s must be i n i t i a l i z e d b e f o r e they can be used int a l o c a l i n t = 1 2 ; // note t h a t the f o l l o w i n g v a r i a b l e i s shared over a l l // i n s t a n c e s a c l a s s i n t += 10; // now the i n s t a n c e i n t e g e r v a r i a b l e System . out . p r i n t l n ( ”−−−−−−−−−−−−−−−−−−−−\n” + ” a method int : ” + a method int + ”\n” + ” a n o t h e r m e t h o d i n t : ” + a n o t h e r m e t h o d i n t + ”\n” + ” a l o c a l i n t : ” + a l o c a l i n t + ”\n” + ” a c l a s s i n t : ” + a c l a s s i n t + ”\n” + ”an instance int : ” + an instance int ); } 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public s t a t i c void main ( S t r i n g [ ] a r g s ) { ScopeOfVariables s c o p e t e s t = new ScopeOfVariables ( ) ; ScopeOfVariables a n o t h e r s c o p e t e s t = new ScopeOfVariables ( ) ; 27 28 29 30 31 s c o p e t e s t . a n i n s t a n c e i n t += 10; s c o p e t e s t . changeVariables ( 1 , 2 ) ; s c o p e t e s t . changeVariables ( 1 , 2 ) ; a n o t h e r sc o p e t e st . changeVariables ( 3 , 4 ) ; 32 33 34 35 } 36 37 } Aufruf auf der Konsole: exttt¿java ScopeOfVariables -------------------a_method_int: 1 another_method_int: 2 a_local_int: 12 a_class_int_: 20 an_instance_int_: 20 -------------------a_method_int: 1 another_method_int: 2 a_local_int: 12 a_class_int_: 30 an_instance_int_: 20 -------------------a_method_int: 3 another_method_int: 4 a_local_int: 12 a_class_int_: 40 an_instance_int_: 10 KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 28 Für ein erstes Verständnis sollte dieses Programm ausreichend sein. Man sollte wirklich den Unterschied zwischen den Arten dieser Variablen verstehen, damit man bei der Softwareentwicklung nicht unerwartete Seiteneffekte produziert. 2.3 Operatoren Der Abschluss dieses Kapitels bilden die Operatoren, die auf Variablen angewendet werden können. Operatoren bilden den Grundstein jeder Programmiersprache. Ein Operator, der einen Operanden verwendet, heißt unärer Operator (z.B. some int--), einer, der zwei Operanden verwendet, binärer Operator (z.B. some int + other int). Es gibt – wie in C++ – auch Operatoren, die mit drei Operanden verwendet werden, nämlich die Kurzform von “if...then...else...”: “op1 ? op2 : op3” (ternärer Operator ). Operatoren werden in einer bestimmten Reihenfolge ausgewertet. Von der Mathematik wird bekannt sein, dass “Punkt-vor-Strich”-Rechnung gilt, d.h. eine Multiplikation wird vor einer Addition ausgeführt. Die Reihenfolge kann über Klammerung der Ausdrücke beeinflusst werden. Auch bei Programmiersprachen wird festgelegt, wann eine Operation ausgeführt wird. Jedem Operator wird eine bestimmte Rangordnung (“R” in der Tabelle, je höher die Rangordnung, desto früher die Auswertung) zugewiesen. Nach dieser Rangordnung werden Ausdrücke ausgewertet. Die Reihenfolge kann über Klammerung beeinflusst werden. Hinweis: Im Vergleich zu C++ gibt es in Java kein Operator-Overloading! Diese Designentscheidung wurde getroffen, um Fehlerquellen für ungeübte Programmierer von vorne herein auszuschalten. Operatoren können über Methoden in Klassen implementiert werden. Statt a += b; kann man eine Methode add() implementieren, die diese Aufgabe übernimmt. Der Aufruf wäre dann a.add(b);. Diese Schreibweise ist man gewohnt und man wird OperatorOverloading gar nicht vermissen... Hinweis: Der obige Hinweis muss noch ergänzt werden: Es gibt für die Entwickler von Java-Programmen kein Operator-Overloading. In Wirklichkeit gibt es nämlich Operator-Overloading auch in Java. Als Beispiel kann die StringKlasse hergenommen werden. Hier wurden sehr wohl die Operatoren “+” und “+=” verwendet, um Zeichenketten zu manipulieren. Nun aber zu den einzelnen Operatoren mit kurzen Erläuterungen. Bei der Beispielsammlung sind einige Tests mit den Operatoren implementiert. 2.3.1 Unäre Arithmetische Operatoren Post-Inkrement (op++) und Post-Dekrement (op--) tragen mit ihren Gegenstücken Pre-Inkrement (++op) und Pre-Dekrement (--op) wohl am meisten zur Verwirrung von unerfahrenen Softwareentwicklern bei. Im folgenden Beispiel KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN R 14 14 15 Operator + ++ Verwendung +op -op op++ 15 -- op-- 14 ++ ++op 14 -- --op 29 Beschreibung op wird zu int Negatives Vorzeichen Post-Inkrement: op wird returniert und erst dann inkrementiert Post-Dekrement: op wird returniert und erst dann dekrementiert Pre-Inkrement: inkrementiere op und returniere danach op Pre-Dekrement: dekrementiere op und returniere danach op Tabelle 2.4: Unäre Arithmetische Operatoren: wird noch einmal gezeigt, wie sich Post-Inkrement und Pre-Dekrement auswirken. // i n i t i a l i z e a n i n t int a n i n t = 5 ; // r e t u r n a n i n t to a n o t h e r i n t and increment a n i n t ; // a n o t h e r i n t == 5; a n i n t == 6; int a n o t h e r i n t = a n i n t++; // dekrement a n i n t and r e t u r n then the v a l u e to s o m e i n t ; // s o m e i n t == 5; a n i n t == 5; int s o m e i n t = −− a n i n t ; Wenn man sich unsicher ist: Bitte unbedingt einige Programme dazu schreiben und wirklich versuchen zu verstehen, worum es hier geht. Diese Konstrukte kommen sehr sehr oft in Programmen vor! 2.3.2 Binäre Arithmetische Operatoren Binäre Arithmetische Operatoren funktionieren so, wie man es erwartet. Interessant ist der +-Operator, der auch für Strings angewendet werden kann. Es kann daher notwendig sein, Ausdrücke mit Klammern zu versehen. // the f o l l o w i n g p r i n t s ” H e l l o ! 1 2 ” System . out . p r i n t l n ( ” H e l l o ! ” + 1 + 2 ) ; // vs . ” H e l l o ! 3 ” System . out . p r i n t l n ( ” H e l l o ! ” + ( 1 + 2 ) ) ; Sollten ganzzahlige Datentypen bei der Division (/) verwendet werden, dann wird bei einer Division durch 0 eine Exception (ArithmeticException) abgesetzt. Für Gleitkomma-Datentypen wird – wie schon in Abschnitt 2.1.1 erläutert – ein spezieller Wert als Ergebnis gesetzt. Hinweis: Der Modulo-Operator (%) funktioniert im Unterschied zu C/C++ nicht nur für Ganzzahlige-Datentypen, sondern auch für Gleitkomma-Datentypen. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN R 11 11 12 12 12 Operator + * / % Verwendung op1+op2 op1-op2 op1*op2 op1/op2 op1%op2 30 Beschreibung Addiert op1 zu op2 Subtrahiert op2 von op1 Multipliziert op1 mit op2 Dividiert op1 durch op2 Berechnet den Rest von op1/op2 Tabelle 2.5: Binäre Arithmetische Operatoren: 2.3.3 Vergleichsoperatoren Bei Kontrollfluss-Steuerungen, die wir im nächsten Kapitel ausführlicher kennenlernen werden, sind Vergleichsoperatoren notwendig. Für Primitive-Datentypen funktionieren die Operatoren wie man es erwartet. Werden unterschiedliche Datentypen miteinander verglichen, wird automatisch in den jeweils größeren Datentyp konvertiert. Bei den speziellen Werten wie sie bei Gleitkomma-Datentypen eingeführt wurden (NaN, etc.) gibt es statische Methoden von Klassen (z.B. Float.isNan())) die Gleichheit bzw. Ungleichheit überprüfen können. Für Referenz-Datentypen, wo man z.B. die Gleichheit von Strings überprüfen will, hat man mit dem == so seine Probleme. Die Werte der Referenzen, also die Adressen, werden miteinander verglichen. Dass diese Adressen für unterschiedliche Objekte, unterschiedliche Werte haben, ist offensichtlich. Hier bietet die String-Klasse entsprechende Methoden (z.B. String.equals()) an. Das Ergebnis von Vergleichsoperatoren ist der Boolean-Wert true oder false. R 9 9 Operator > >= 9 9 < <= 8 8 == != Verwendung Beschreibung op1>op2 op1 ist größer als op2 op1>=op2 op1 ist größer als oder gleich wie op2 op1<op2 op1 ist kleiner als op2 op1<=op2 op1 ist kleiner als oder gleich wie op2 op1==op2 op1 und op2 sind gleich op1!=op2 op1 und op2 sind ungleich Tabelle 2.6: Vergleichsoperatoren 2.3.4 Logische Verknüpfungsoperatoren Sehr oft werden nicht nur einzelne Ausdrücke, sondern ganze Ketten von Ausdrücken miteinander verglichen. Um jetzt diese Teilausdrücke von links nach rechts miteinander zu verbinden, werden Verknüpfungsoperatoren verwendet. Hier kann es passieren, dass nicht der ganze Ausdruck ausgewertet wird: Wenn man z.B. den Ausdruck op1 oder op2 auswerten will, und op1 ist true, dann ist von vorne herein das Ergebnis des gesamten Ausdrucks festgelegt. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN 31 Diese bedingte Auswertung von Ausdrücken hat enorm viele Vorteile. Auf der einen Seite verbessert sich das Laufzeitverhalten, da die Auswertung abgekürzt werden kann, sobald das Ergebnis feststeht. Auf der anderen Seite kann man auch sicher sein, dass der linke Ausdruck sicher ausgewertet wurde. Im folgenden Beispiel kann man sich sicher sein, dass str.length() nur dann ausgewertet wird, wenn str != null ist, d.h. es ist mit keiner NullPointerException zu rechnen (zu Exceptions siehe bitte Abschnitt 5.1). i f ( s t r ! = null && s t r . l e n g t h ( ) > 1 0 ) { ... } R 4 Operator && 7 & 3 || 5 | 6 14 ^ ! Verwendung Beschreibung op1&&op2 op1 und op2 beide true (short eval!) op1&op2 op1 und op2 beide true (full eval!) op1||op2 entweder op1 oder op2 oder beide true (short eval!) op1|op2 entweder op1 oder op2 oder beide true (full eval!) op1^op2 op1 ungleich op2 !op op ist false Tabelle 2.7: Logische Verknüpfungsoperatoren Vorsicht Falle: Man kann es nicht oft genug schreiben: Bitte die Finger weg von & und |! Kein vernünftiger Programmierer geht von einer vollständigen Auswertung bei logischen Verknüpfungen aus! Diese Operatoren sind optisch den Bitoperatoren identisch und sorgen nur für Verwirrung beim Lesen des Programms. 2.3.5 Bitoperatoren Bitoperatoren werden bei Ganzzahlen-Datentypen zum Testen von einzelnen Bits verwendet. Dazu sollte man die binäre Repräsentation einer Zahl verstehen. Vorsicht Falle: Wenn man diese Bitoperatoren anschaut, dann fällt der Operator >>> sofort ins Auge. Hier gibt es keinen Operator ’in die andere Richtung’ (<<<). Wenn ein <<<-Operator auch keinen Sinn macht (das Vorzeichenbit würde ja sowieso “links rausfallen” bzw. wäre das Vorzeichen vom linkesten Bit der Zahl abhängig...), wäre ein solcher Operator aus Gründen der Symmetrie doch wünschenswert. KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN R 10 Operator >> 10 >>> 10 << 7 5 6 14 & | ^ ~ 32 Verwendung Beschreibung op1>>op2 shiftet op1 nach rechts um op2 Bits (Vorzeichenbit wird nicht mitgeshiftet!) op1>>>op2 shiftet op1 nach rechts um op2 Bits (Vorzeichenbit wird mitgeshiftet!) op1<<op2 shiftet op1 nach links um op2 Bits (Vorzeichenbit wird nicht mitgeshiftet!) op1&op2 Bitweises AND von op1 und op2 op1|op2 Bitweises OR von op1 und op2 op1^op2 Bitweises XOR von op1 und op2 ~op Bitweises Komplement von op Tabelle 2.8: Bitoperatoren 2.3.6 Shortcut-Zuweisungsoperatoren Den Zuweisungsoperator (“=”) haben wir nun schon einige Male verwendet. Es wird der gewertete Ausdruck auf der rechten Seite einer Variable zugewiesen. Zusätzlich zum einfachen Zuweisungsoperator gibt es noch 11 weitere Versionen, die implizit Operationen auf den linken Ausdruck ausführen. In der Tabelle sind sämtliche verfügbaren Versionen aufgelistet. Auch hier macht es Sinn, wenn man versteht, was genau passiert. Betrachten wir folgendes Beispiel: a n i n t a r r a y [ count ++] += 4; a n i n t a r r a y [ count ++] = a n i n t a r r a y [ count ++] + 4; In der ersten Zeile wird count nur einmal ausgewertet, während in der zweiten Zeile zweimal count inkrementiert wird! Damit wird in Zeile 2 nicht das Element bei Index count, sondern bei Index (count+1) verändert (der Wert des Elements bei Index count wird mit 4 addiert und bei Index (count+1) abgelegt). Wenn diese Operation innerhalb einer Schleife für alle Elemente ausgeführt wird, wird man sicherlich mit einer ArrayIndexOutOfBoundsException konfrontiert. 2.3.7 Weitere Operatoren In Tabelle 2.10 sind noch weitere Operatoren aufgelistet. Aus anderen Programmiersprachen ist die Abkürzung zum sehr oft verwendeten if...then...else bekannt. op2 und op3 müssen den selben Typ haben. instanceof überprüft, ob ein Objekt von einer bestimmten Klasse instantiiert oder abgeleitet wurde. Für den speziellen null-Wert eines Objekts ergibt instanceof immer false. Objekte einer Klasse werden über den new-Operator instantiiert. Im Kapitel zu Klassen (4) werden wir noch genauer sehen, was hier passiert. In Abschnitt 2.2 haben wir den Operator bereits für Strings und Arrays verwendet. Der castOperator wird ebenso erst in Kapitel 4 beschrieben. Nachdem wir uns jetzt ausführlich mit den Datentypen in Java befasst haben, KAPITEL 2. DATENTYPEN, VARIABLEN UND OPERATOREN R 1 1 1 1 1 1 1 1 1 1 1 Operator += -= *= /= %= &= |= ^= <<= >>= >>>= Verwendung op1 += op2 op1 -= op2 op1 *= op2 op1 /= op2 op1 %= op2 op1 &= op2 op1 |= op2 op1 ^= op2 op1 <<= op2 op1 >>= op2 op1 >>>= op2 33 Beschreibung op1 = op1 + op2 op1 = op1 - op2 op1 = op1 * op2 op1 = op1 / op2 op1 = op1 % op2 op1 = op1 & op2 op1 = op1 | op2 op1 = op1 ^ op2 op1 = op1 << op2 op1 = op1 >> op2 op1 = op1 >>> op2 Tabelle 2.9: Shortcut-Zuweisungsoperatoren R 2 Operator ?: 9 instanceof 13 13 (type) new Verwendung op1 ? op2 : op3 op1 instanceof op2 (type)op1 new op1 Beschreibung if (op1) op2 else op3 true, if op1 is an instance of op2 casts op1 to type creates a new object or array of type op1 Tabelle 2.10: Weitere Operatoren Variablen solchen Typs verwendet und einige Operationen mit diesen Variablen ausgeführt haben, befassen wir uns mit der Flusskontrolle in Programmen. Kapitel 3 Flusskontrolle Da im Prinzip die Flusskontrolle in Java der von anderen Programmiersprachen sehr ähnlich ist, halte ich mich mit den Erklärungen zurück. Bevor es mit den Schleifen losgeht, noch kurz eine Definition von Begriffen, die wir verwenden werden (und die vermutlich intuitiv schon klar sind bzw. aus anderen Programmiersprachen bekannt sind): Ausdruck (expression): Eine Folge von Operatoren, Operanden und/oder Methodenaufrufen. Der Datentyp des zurückgegeben Wertes ist abhängig von den Werten im Ausdruck. Operatoren werden unterschiedlich behandelt und jeweils nach ihrem Rang ausgewertet. Haben Operatoren in einem Ausdruck denselben Rang, so wird der Ausdruck von links nach rechts ausgewertet. Ausdrücke können mit Klammern geschachtelt werden. Damit kann die Reihenfolge der Auswertung beeinflusst werden. Grundsätzlich ist zu raten: lieber eine Klammerung zu viel, als zu-wenig. Als Beispiel: d = a + b / c; d = a + (b / c ) ; // not t h a t easy to read . . . // computes the same v a l u e but i s much // e a s i e r to read . . . Anweisung (statement): Eine Anweisung entspricht (grob gesprochen) einer Programmzeile und wird mit einem Strichpunkt (semicolon, ’;’) abgeschlossen. Es gibt drei Arten von Anweisungen: • Deklarationen und Definitionen: int an_int = 24; • Zuweisungen: an_int = 27; • Kontrollflussanweisungen: Regelt, wie der Programmfluss gesteuert wird (das eigentliche Thema dieses Abschnitts), also if...then...else etc.. Block (block ): Ein Block ist eine Zusammenfassung von Anweisungen und kann überall dort verwendet werden, wo eine einfache Anweisung verwendet werden kann. Ein Block wird mit geschwungenen Klammern (curly brace, { }) gebildet. Innerhalb eines Blocks definierte Variablen sind nur im jeweiligen Block gültig. So, nach diesen kurzen Definitionen, auf zu den Kontrollfluss-Statements! 34 KAPITEL 3. FLUSSKONTROLLE 3.1 35 While... Bei while gibt es die ersten zwei der drei Schleifenvarianten. Einmal: while ( e x p r e s s i o n ) statement und dann noch das Konstrukt: do statement while ( e x p r e s s i o n ) ; Die Unterschiede sind offensichtlich: Soll vor dem Eintritt in die Schleife ein Ausdruck expression ausgewertet werden, so ist die erste Variante zu wählen. Hier kann es passieren, dass die Schleife nicht durchlaufen wird. Soll eine Schleife mindestens einmal durchlaufen werden, bevor ein Ausdruck ausgewertet wird, ist die zweite Variante zu wählen. Der Ausdruck expression muss einen Wert des Typs boolean zurückliefern. 3.2 For... Bei einer for-Schleife wird ein Schleifenzähler zuerst initialisiert, dann wird die Schleifenbedingung (condition) geprüft. Wenn diese Bedingung erfüllt ist, wird der Schleifenblock ausgeführt. Nach jedem Durchlauf der Schleife wird zuerst die step-expression ausgewertet. Bei diesem Ausdruck wird üblicherweise der Schleifenzähler weitergezählt. Danach wird die Schleifenbedingung erneut geprüft. Bei der Initialisierung und in der step-expression dürfen mehrere Anweisungen vorkommen. Diese Anweisungen müssen lediglich durch Beistriche voneinander getrennt werden. for ( i n i t i a l i z a t i o n ; c o n d i t i o n ; step−e x p r e s s i o n ) statement Die Schleifeninitialisierung, -bedingung und die Step-Expression sind optional, wobei die Initialisierung auch außerhalb des for-Statements erfolgen kann. Will man den Scope (Gültigkeitsbereich) des Schleifenzählers so klein als möglich halten (und das sollte man grundsätzlich wollen...), erfolgt die Deklaration und Definition im for-Konstrukt. for ( int c o u nt e r = 0 ; c o un t e r < 3 ; c o unt e r++) { . . . // c o u nt e r v a l i d i n t h i s b l o c k . . . } Im Gegensatz dazu ein Beispiel der Initialisierung außerhalb des Schleifenkopfs: int co u nt e r = 0 ; for ( ; c ou nt e r < 3 ; c o u n t e r++) { . . . // c o u nt e r v a l i d } // c o u nt e r a l s o a v a i l a b l e o u t s i d e the loop−b l o c k . . . System . out . p r i n t l n ( ” c o un t er : ” + c o u n t er ) ; Eine Schleife mit leerer condition mutiert zur Endlosschleife. // some i n f i n i t e l o o p for ( int c o u n t e r ; ; c o u nt e r++) { KAPITEL 3. FLUSSKONTROLLE 36 ... } // another i n f i n i t e l o o p for ( ; ; ) { ... } Abschließend zur for-Schleife noch ein kurzes Beispiel mit unterschiedlichen Initialisierungen der Schleifenvariable. Die dritte Schleife führt mehrere Anweisungen bei der Initialisierung und Step-Expression aus. 1 2 3 4 5 6 7 8 9 10 11 12 13 public c l a s s ForExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { // f i r s t f o r −l o o p . . . for ( int c o u nt e r =0; co u nt e r < 5 ; c o u n te r++) { System . out . p r i n t l n ( ”1 s t : c o u nt e r : ” + c o un t e r ) ; } // the f o l l o w i n g w i l l l e a d to an compiletime e r r o r : // cannot r e s o l v e symbol // System . out . p r i n t l n (” a f t e r 1 s t l o o p : co u n t e r : ” + // c o u nt e r ) ; 14 // second f o r −l o o p . . . int c o u n t e r 2 = 0 ; for ( ; c o u n t e r 2 < 5 ; c o u n t e r 2++) { System . out . p r i n t l n ( ”2nd : c o u n t e r 2 : ” + c o u n t e r 2 ) ; } System . out . p r i n t l n ( ” a f t e r 2 nd l o o p : c o u n t e r 2 : ” + c o u n t e r 2 ) ; 15 16 17 18 19 20 21 22 // t h i r d f o r −l o o p . . . for ( int c o u n t e r 3 1 =0 , c o u n t e r 3 2 = 1 0 ; c o u n t e r 3 1 < 5; c o u n t e r 3 1 ++, c o u n t e r 3 2 −−) { System . out . p r i n t l n ( ”3 rd : c o u n t e r 3 1 : ” + c o u n t e r 3 1 + ” ; counter 3 2 : ” + counter 3 2 ) ; } 23 24 25 26 27 28 29 } 30 31 } Aufruf auf der Konsole: exttt¿java ForExample 1st: counter: 0 1st: counter: 1 1st: counter: 2 1st: counter: 3 1st: counter: 4 2nd: counter_2: 0 2nd: counter_2: 1 2nd: counter_2: 2 2nd: counter_2: 3 2nd: counter_2: 4 after 2nd loop: counter_2: 5 3rd: counter_3_1: 0; counter_3_2: 3rd: counter_3_1: 1; counter_3_2: 3rd: counter_3_1: 2; counter_3_2: 3rd: counter_3_1: 3; counter_3_2: 10 9 8 7 KAPITEL 3. FLUSSKONTROLLE 37 3rd: counter_3_1: 4; counter_3_2: 6 3.3 If... Bei dem if-Statement wird eine Bedingung (condition) ausgewertet. Ist diese Bedingung wahr (true) wird der Zweig ausgeführt, wenn nicht, wird ein eventuell vorhandener else-Zweig ausgeführt. Hinweis: Die Bedingung muss unbedingt einen boolean-Wert zurückliefern (if (an int != 0)). In C/C++ ist das nicht notwendig (if (an int))! Als Beispiel: i f ( condition ) statement−true e l s e // e l s e i s o p t i o n a l . . . statement−f a l s e In Java gibt es wie in C++ das if/else-Statement in Operator-Form. Diese verkürzte Form der Schreibweise ist bei Zuweisungen und Parameterübergaben vorzugsweise zu verwenden. op1 ? op2 : op3 Ist op1 true, wird op2 returniert, ansonsten op3. Im folgenden Beispiel sieht man den Kontrast zwischen Kurzform und ausführlicher Schreibweise. Es ist offensichtlich, dass die Kurzform schneller und einfach zu lesen ist. Im zweiten Teil des Beispiels sieht man eine geschachtelte Form von if/else (die sehr schwer zu lesen ist). 1 2 3 4 5 6 public c l a s s IfExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { int a n i n t = 3 ; int a n o t h e r i n t = 2 ; 7 8 9 System . out . p r i n t l n ( ” a n i n t : ” + a n i n t ) ; System . out . p r i n t l n ( ” a n o t h e r i n t : ” + a n o t h e r i n t ) ; 10 11 12 13 14 15 16 17 18 19 20 21 22 // t h i s i s the i f ( a n i n t > 0) { another int = 1; an int = another } else { an int = another } // . . . o f t h i s an int = ( an int > l o n g form . . . int ; int ; line ; 0 ? another int =1 : another int ) ; 23 24 25 26 27 28 29 30 31 // now some n e s t e d ( unreadable . . . ) int t e s t s c o r e = 7 6 ; char grade ; i f ( t e s t s c o r e >= 90) { grade = ’A ’ ; } else i f / e l s e stat em ents . . . KAPITEL 3. FLUSSKONTROLLE i f ( t e s t s c o r e >= 80) { grade = ’B ’ ; } else i f ( t e s t s c o r e >= 70) { grade = ’C ’ ; } else i f ( t e s t s c o r e >= 60) { grade = ’D ’ ; } else { grade = ’F ’ ; } System . out . p r i n t l n ( ”Grade = ” + grade ) ; 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 } 51 52 38 } Aufruf auf der Konsole: exttt¿java IfExample an_int: 3 another_int: 2 Grade = C 3.4 Switch... Im letzten Beispiel hat man gesehen, dass es notwendig ist, mehrere Fallunterscheidungen elegant zu codieren. If/else-Verschachtelungen sind schwer zu lesen und zu durchschauen: Eine Lösung für Ausdrücke, die ganzzahlige Werte liefern, ist das switch-Statement. switch ( e x p r e s s i o n ) { case c o n s t 1 : statement1 case c o n s t 2 : statement2 default : defaultstatement } Der Ausdruck expression wird ausgewertet und die Verarbeitung beim passenden case-Label weitergeführt. Die Anweisungen nach diesem Label werden danach ausgeführt. Nicht vergessen sollte man eine break Anweisung, nachdem die Anweisungen durchgeführt wurden, ansonsten wird einfach – wie bei Einsprungpunkten üblich – bei der nächsten Anweisung weitergemacht, und das will man vermutlich nicht... 1 2 3 4 5 6 7 public c l a s s SwitchExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { char grade = ’B ’ ; switch ( grade ) { KAPITEL 3. FLUSSKONTROLLE case ’A ’ : System . out . break ; case ’B ’ : System . out . break ; case ’C ’ : System . out . break ; case ’D ’ : System . out . break ; case ’E ’ : System . out . break ; case ’F ’ : System . out . break ; default : System . out . break ; } 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 p r i n t l n ( ”Very good . . . ” ) ; p r i n t l n ( ”Good . . . ” ) ; p r i n t l n ( ” Quite ok . . . ” ) ; p r i n t l n ( ”Ok . . . ” ) ; p r i n t l n ( ”Poor . . . ” ) ; println (” Failed . . . ” ) ; p r i n t l n ( ” I n v a l i d grade . . . ” ) ; } 30 31 39 } Aufruf auf der Konsole: exttt¿java SwitchExample Good... Interessant ist das default-Label: Dieses wird verwendet, wenn kein definiertes Label zum ausgewerteten Ausdruck passt. Eigentlich wäre das letzte break nicht notwendig, weil das switch-Statement sowieso verlassen wird. Es ist allerdings im Sinne der Erweiterbarkeit und der besseren Lesbarkeit sinnvoll, auch das letzte break zu schreiben. Das switch-Statement funktioniert nur mit Expressions, die bei der Auswertung ganzzahlige Werte liefern (char hat ja den Wertebereich 0-65535 und funktioniert aus diesem Grund; siehe Abschnitt 2.1.1). 3.5 break, continue und return Für Verzweigungen innerhalb von Schleifen (for, while) und switch-Statements können break und continue verwendet werden. break: Diese Anweisung beendet die aktuelle Schleife oder die switch-Anweisung, wenn kein Label angegeben wurde. Wird ein Label angegeben, so kann auch eine übergeordnete Schleife, die mit einem Label markiert wurde, beendet werden. Achtung: Es wird die jeweilige Schleife beendet! Der Programmablauf wird nicht beim Label fortgesetzt!! continue: Springt wieder zum Schleifenkopf einer Schleife, d.h. bei einer forund while-Schleife wird die Schleifenbedingung erneut geprüft, eine doSchleife wird ohne weitere Prüfung am Beginn der Schleife fortgesetzt. Wie bei break wird ohne Label immer die aktuelle Schleife fortgesetzt. Mit Label kann eine markierte Schleife am jeweiligen Schleifenkopf fortgesetzt werden. KAPITEL 3. FLUSSKONTROLLE 40 Die beiden Anweisungen können mit und ohne Label aufgerufen werden. Ein Label (“Sprungmarke”) wird wie folgt definiert: t h e l a b e l : statement Vorsicht Falle: Man sieht, dass break und continue mit Labels einem goto sehr Nahe kommen und unbedingt vermieden werden müssen!! (Aus diesem Grund wird hier auf ein ausführliches Beispiel mit Labels verzichtet!). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public c l a s s BreakContinueExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { for ( int count = 0 ; ; count++) { i f ( count > 5) { System . out . p r i n t l n ( ” count > 5 . . . ” ) ; break ; } i f ( count < 100) { System . out . p r i n t l n ( ” c o n t i n u e . . . ” ) ; continue ; } System . out . p r i n t l n ( ” never reached , because o f c o n t i n u e . . . ” ) ; } System . out . p r i n t l n ( ” F i n i s h e d f o r −l o o p ; ( count out o f scope ! ) ” ) ; } } Aufruf auf der Konsole: exttt¿java BreakContinueExample continue... continue... continue... continue... continue... continue... count > 5... Finished for-loop; (count out of scope!) Mit return wird eine Methode beendet. Bei der Signatur einer Methode wird festgelegt, welchen Typ der Return-Wert hat. Wenn eine Methode keinen return-Wert liefert, wird sie als void deklariert. return ; Wird ein Return-Wert von der Methode geliefert, so muss dieser mit dem in der Signatur vereinbarten Typ übereinstimmen. return some value ; Kapitel 4 Klassen in Java In diesem Kapitel werden Eigenschaften und Eigenheiten von Klassen in Java behandelt. Wir beginnen mit einem kurzen Überblick und betrachten danach Java spezifische Details. 4.1 Einige Begriffe Die Begriffe, die nun folgen, sind vermutlich größtenteils schon bekannt. Trotzdem sollen sie hier noch einmal genannt und erläutert werden. Als Beispiele bei den Erklärungen dienen ein Fahrrad und weitere Gegenstände des täglichen Lebens. Klasse (class): Zusammenfassung aller naturgegebenen Möglichkeiten (Methoden) und Eigenschaften (Member-Variablen) gleichartiger Objekte. Bei Fahrrädern kann man z.B. ein Rad wechseln (Methode). Jedes Fahrrad hat eine Lenkstange, einen Sattel, zwei Räder etc. (Member-Variable). Ein Einrad wäre nach dieser Betrachtungsweise kein Fahrrad! Eigenschaften einer Klasse können nicht einfach “weg-definiert” werden!! Ein Fahrrad ist ein Fahrzeug. • Die Beziehung “hat-ein” (HAS-A) wird über eine Member-Variable ausgedrückt. • Die Beziehung “ist-ein” (IS-A) wird über Ableitungen realisiert. Der Zugriff auf einzelne Methoden bzw. einzelne Member-Variablen kann (und soll!) beschränkt werden (siehe Abschnitt 4.5). Objekt: Eine bestimmte Instanz einer Klasse. Z.B. ist das rote Fahrrad des Nachbarn eine Instanz der Klasse Fahrrad. Ein Objekt wird explizit über den new-Operator erzeugt. Hinweis: In Java kann mit dem Schlüsselwort this auf das “eigene” Objekt zugegriffen werden (z.B. wenn man Methoden aufrufen oder MemberVariablen verändern will). 41 KAPITEL 4. KLASSEN IN JAVA 42 Member-Variable: Eigenschaft einer Klasse. Selbstverständlich kann eine Klasse mehrere Eigenschaften haben. Es gibt auch unterschiedliche Arten von Member-Variablen. Wie schon im obigen Beispiel angeschnitten, wäre eine Lenkstange eine Eigenschaft eines Fahrrades und müsste dementsprechend als Member-Variable implementiert werden. Ableitung: Eine Klasse kann von einer anderen Klasse abgeleitet werden, wenn die IS-A-Beziehung zwischen den beiden Klassen gilt. Hinweis: In Java gibt es (leider) nur einfache Ableitungen. Mehrfache Ableitungen müssen über Interfaces umgesetzt werden. Das Fahrrad ist ein Fahrzeug, leitet sich also von der Klasse Fahrzeug ab und erbt alle Eigenschaften und Methoden der Fahrzeug-Klasse. Das Schlüsselwort für die Ableitung ist extends. Interface: Ein Interface beschreibt eine Sammlung von Konstanten- und MethodenDeklarationen von voneinander unabhängigen Klassen. Überlegen wir uns ein Programm zur Inventur von Artikeln. Diesem Programm sollte es egal sein, ob ein Fahrrad oder ein Fernseher inventarisiert werden soll. Beide (Fahrrad und Fernseher) haben eine Inventar-Nummer und stehen irgendwo herum. Für dieses Programm sind nun Inventar-Nummer und Aufstellungsort (und natürlich viele weitere Eigenschaften, die mit der Inventarisierung zu tun haben wie Ankaufdatum, wer hat das Ding angekauft, etc.) interessant. Fahrrad muss daher inventarisierbar sein, damit es vom Programm inventarisiert werden kann. Soll ein Fernseher inventarisiert werden, muss auch dieser inventarisierbar sein. Schlüsselwort für die Definition von Interfaces ist interface. Methode: Eine Funktion, die einer Klasse zugewiesen ist, nennt man Methode. Es gibt statische Methoden sowie Instanz-Methoden. Statische können unabhängig von einer Instanz verwendet werden. Diese haben wir schon für Umwandlungen von Zeichenketten in andere Datentypen kennengelernt (z.B. Integer.parseInt(String);, siehe 2.1.1). Instance methods werden über ein existierendes Objekt aufgerufen und verwenden das Objekt. Die Methode “Reifenwechseln” verwendet ein bestimmtes Fahrrad. Kapselung (encapsulation): Eine Datenstruktur wird in einer Klasse gekapselt und dadurch vor dem Benutzer “versteckt”. Der Benutzer der Klasse kann nur über bestimmte zugängliche Methoden auf einzelne Elemente der Datenstruktur zugreifen. Vorteil: Solange sich an den Zugriffsmethoden nichts ändert, können Änderungen an den internen Details der Klasse vorgenommen werden, ohne dass der Benutzer etwas davon merkt. Für die Kapselung werden die Schlüsselwörter public, private und protected verwendet. Overloading: Von “overloading” spricht man, wenn mehrere Methoden einer Klasse denselben Namen haben, aber mit unterschiedlichen Parametern aufgerufen werden. Overriding: Eine Methode einer abgeleiteten Klasse “overrided” eine Methode mit den selben Namen und Parametern aus der Super-Klasse. Der Name KAPITEL 4. KLASSEN IN JAVA 43 der Methode und die Typen der Parameter werden Signatur einer Methode genannt. Vorsicht Falle: “Override” (“überreiten” bzw. “Vorrang haben vor”) hat absolut nichts mit “overwrite” zu tun (auch wenn die Aussprache dieser beiden Begriffe sich sehr ähnlich ist!). Eine Methode wird in Java nicht überschrieben – sie ist ja noch immer da und kann über das Schlüsselwort super (z.B. super.methode name();) auch aufgerufen werden. JavaScript-Programmierer haben den Begriff “overwrite” vermutlich schon gelesen, sollten ihn aber so schnell wie möglich wieder aus ihrem (Programmier-)Wortschatz streichen! Die oben genannten Begriffe ziehen sich durch die folgenden Abschnitte. In Kürze werden wir die Konzepte in Java an Beispielen genauer betrachten. 4.2 Definition einer Klasse in Java Wie wir schon im ersten Beispiel (siehe Abschnitt 1.1) gesehen haben, wird jede Klasse in einer Datei abgespeichert. Der Name der Datei entspricht dem Klassennamen mit der Erweiterung .java. In Abschnitt 4.1 haben wir geschrieben: [Eine Klasse ist die] “... Zusammenfassung aller naturgegebenen Möglichkeiten (Methoden) und Eigenschaften (Member-Variablen) gleichartiger Objekte ... Eigenschaften einer Klasse können nicht einfach “weg-definiert” werden!”. In Java kann man auch Klassen innerhalb von Klassen definieren. Anwendungsbeispiele für diese Form werden später noch gezeigt. Hinweis: Genauer gesagt darf genau eine public-Klassendefinition in einer Datei stehen. Eine Klassendefinition definiert einen neuen Referenz-Datentyp. Im Gegensatz zu C++ gibt es in Java keine Header-Files, in denen eine Klasse deklariert wird. Dadurch hat man alle notwendigen Informationen einer Klasse in einer Datei zusammengefasst. Nun aber zur Definition einer sog. “top-level class” (d.h. einer Klasse, die nicht innerhalb einer anderen Klasse definiert wurde) mit einigen Erläuterungen: [ public ] [ abstract | f i n a l ] c l a s s [ TheClassName ] [ extends TheBaseClassName ] [ implements SomeInterfaceA [ , SomeInterfaceB ] ] { // h e r e the d e f i n i t i o n o f member v a r i a b l e s and methods . . . } Sichtbarkeit der Klasse (“access modifier”): Hier gibt es zwei Möglichkeiten: public: Die Klasse ist für alle anderen Klassen sichtbar. KAPITEL 4. KLASSEN IN JAVA 44 kein Modifier: Die Klasse ist nur für andere Klassen im selben Package sichtbar. Es ist möglich (aber nicht sinnvoll!), mehrere solcher Klassen in einem File zu speichern. Generell sollte man jede Klasse, egal ob public oder nicht, in ein eigenes Sourcecode-File speichern (auch wenn es hier bei einigen DemoKlassen nicht so gemacht wurde!). Für geschachtelte Klassen (“nested-” oder “inner-classes”, d.h. Klassen, die innerhalb von Klassen definiert werden, siehe Abschnitt 4.8) gibt es noch weitere Schlüsselwörter, die die Sichtbarkeit beeinflussen, nämlich private und protected. Modifier: abstract: Eine Klasse mit diesem Modifier kann nicht instantiiert werden, weil die vollständige Definition auf dieser Abstraktionsebene des Klassendesigns noch nicht bekannt ist. Einer Ableitung steht aber nichts im Weg. Wenn alle Methoden in einer abstrakten Klasse definiert sind, kann ein Entwickler trotzdem eine Ableitung fordern. Sind in der Klasse Methoden als abstract deklariert, ist der Modifier explizit gefordert (ansonsten kommt ein Fehler während des Compilierens). final: Von einer final Klasse kann nicht abgeleitet werden (z.B. java. lang.String, ...). D.h. die Methoden, die in dieser Klasse definiert wurden, können unter keinen Umständen von einem Entwickler verändert werden. Bei einigen Systemklassen (wie schon der angesprochenen String-Klasse) macht dieser Ansatz durchaus Sinn. Im Klassendesign von Libraries muss man generell sehr vorsichtig mit Modifier umgehen. Bei final sind die Auswirkungen besonders deutlich zu sehen. TheClassName: Der Gültigkeitsbereich (scope) des Klassennamen beschränkt sich auf das Package (siehe Abschnitt 4.11), in dem die Klasse definiert wurde. Es ist daher möglich, gleiche Klassennamen in unterschiedlichen Packages zu verwenden, d.h. man könnte ohne weiteres eine Klasse String im Package mypackage definieren, ohne mit java.lang.String in Konkurrenz zu stehen. Will man beide String-Klassen gleichzeitig verwenden, muss bei der Instantiierung mit dem full qualified name gearbeitet werden. Klassen müssen nicht unbedingt einen Namen haben (sog. “anonyme” Klasse). Weitere Schlüsselwörter bei der Klassendefinition: extends: Diese Klasse leitet sich von der Klasse mit dem Namen TheBaseClassName ab. In Java ist nur eine einfache Ableitung möglich! Jede Klasse in Java ist von der Klasse java.lang.Object abgeleitet und erbt daher alle Methoden, die dort definiert sind (u.a. toString(), clone(), finalize()...). Mehr dazu ist in Abschnitt 4.7 bzw. 4.9 zu finden. implements: Diese Klasse implementiert ein oder mehrere Interfaces (siehe 4.10). KAPITEL 4. KLASSEN IN JAVA 45 Hinweis: Die Modifier, die bei der Definition der Klasse stehen, müssen selbstverständlich sinnvoll sein. Widersprüchliche Angaben werden vom Compiler erkannt und dementsprechend nicht in Bytecode übersetzt. Wenn man z.B. eine Klasse sowohl abstract als auch final definiert, wird beim Compilieren ein Fehler gemeldet (“illegal combination of modifiers: abstract and final”). Ähnlich ist das Verhalten des Compilers, wenn falsche Modifiers verwendet werden (z.B. private in einer “top-level” Klasse). Hier meldet der Compiler “modifier private not allowed here”. Für Member einer Klasse (d.h. Variablen, Methoden und auch selbst wieder Klassen...) gibt es die Möglichkeit der Zugriffskontrolle mit public, protected und private, sowie weitere Schlüsselwörter, die in Kürze erläutert werden. Zuvor betrachten wir aber noch die Art und Weise, wie Objekte einer bestimmten Klasse erzeugt werden. 4.3 Der Konstruktor einer Klasse Objekte werden über das Schlüsselwort new erzeugt. Eine Ausnahme gibt es hier bei der String-Klasse (siehe 2.1.2). Durch den Aufruf von obj = new SomeClassName(); wird ein Objekt vom Typ SomeClassName erzeugt. Wie in C++ gibt es auch in Java spezielle Methoden, die für die Initialisierung von Objekten zuständig sind, sog. Konstruktoren (constructors) einer Klasse. Die Signatur eines Konstruktors: [ public | private | protected ] TheClassName ( [ some parameters ] ) [ throws SomeException ] { // h e r e the implementation o f the c o n s t r u c t o r } Der Zugriff auf Konstruktoren wird über sog. Zugriffsmodifier (public, private und protected) gesteuert (siehe Abschnitt 4.5). Durch diese kann die Instantiierung von Objekten dieser Klasse beeinflusst werden. Mit dem Schlüsselwort private kann z.B. verhindert werden, dass “irgendeine” anderen Klasse ein Objekt dieses Typs erzeugt1 . Hinweis: Ein Konstruktor ist – wie in C++ – kein Member einer Klasse, d.h. er wird z.B. nicht von Parent-Klassen geerbt. Anders als bei normalen Methoden (siehe Abschnitt 4.7) darf ein Konstruktor auch nicht mit den folgenden Schlüsselwörten versehen werden: abstract, final, static, synchronized oder native. Da der Konstruktor nicht vererbt wird, machen die Schlüsselwörter abstract und final keinen Sinn. Ebenso wird ein Konstruktor nur bei der Erzeugung einer Instanz aufgerufen (static ist daher ebenso sinnlos...). Der Zugriff auf 1 In diesem Zusammenhang muss man unbedingt das Singleton-Design Pattern nennen ([Gamma et al., 1998]). Bei diesem Ansatz wird garantiert, dass nur eine Instanz einer Klasse erzeugt wird. KAPITEL 4. KLASSEN IN JAVA 46 das Objekt, das gerade instantiiert wird, wird erst nach der Erzeugung gewährt (dieses Verhalten erübrigt das Schlüsselwort synchronized). Schlussendlich wurde noch das Schlüsselwort native für Konstruktoren ausgeschlossen, da dadurch die Implementation der JVM einfacher wurde. Der Name des Konstruktors entspricht dem Klassennamen. Durch overloading können mehrere Konstruktoren in einer Klasse existieren. Ein spezieller Konstruktor – der ohne Parameter – wird default constructor genannt. Konstruktoren werden beim Instantiieren von Objekten aufgerufen, beim Aufruf der Methoden newInstance(...), beim String-Operator + bzw. in anderen Konstruktoren. Er kann nicht als Methode aufgerufen werden (zur Erinnerung: der Konstruktor ist ja kein Member einer Klasse!). Über die throws-Klausel müssen noch Exceptions (siehe Kapitel 5), die während des Aufrufs auftreten können, angegeben werden2 . Im Unterschied zu C++ müssen Exceptions, die während der Ausführung des Konstruktors (bzw. auch einer Methode) auftreten können, bei der Signatur angeführt werden. Will man bei der Erzeugung eines Objekts keine speziellen Initialisierungen vornehmen, so muss man nicht unbedingt einen Konstruktor schreiben. Wenn kein Default-Konstruktor bei einer Klasse definiert ist, wird automatisch einer von der JVM zur Verfügung gestellt. Ein Konstruktor returniert im Unterschied zu Methoden keinen Wert (nicht einmal void!). 4.3.1 Konstruktoren in Einfachen Klassen Beim folgenden Beispiel wurde für die Klasse BaseClass ein Default-Konstruktor implementiert, der beim Erzeugen von Objekten aufgerufen wird. 1 2 3 public c l a s s BaseClass { protected int s o m e i n t ; 4 5 6 7 8 public BaseClass ( ) { System . out . p r i n t l n ( ” D e f a u l t c o n s t r u c t o r o f BaseClass ” ) ; } 9 10 11 12 13 14 public BaseClass ( int s o m e i n t ) { System . out . p r i n t l n ( ”Some o t h e r Constructor o f BaseClass : s o m e i n t : ” + s o m e i n t ) ; some int = some int ; } 15 16 17 18 19 protected void f i n a l i z e ( ) throws Throwable { System . out . p r i n t l n ( ” F i n a l i z e r o f BaseClass ” ) ; } 20 21 22 23 24 25 public int getSomeInt ( ) { System . out . p r i n t l n ( ” getSomeInt ( ) o f BaseClass ” ) ; return s o m e i n t ; } 26 27 28 29 30 public void setSomeInt ( int s o m e i n t ) { System . out . p r i n t l n ( ” setSomeInt ( ) o f BaseClass ” ) ; some int = some int ; 2 Im Abschnitt über Fehlerbehandlung (Kapitel 5) werden wir noch lesen, dass in dieser Klausel nicht alle Typen von Exceptions angegeben werden müssen... KAPITEL 4. KLASSEN IN JAVA 47 } 31 32 public S t r i n g t o S t r i n g ( ) { return ” BaseClass : s o m e i n t : ” + s o m e i n t ; } 33 34 35 36 37 public s t a t i c void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ” Create an o b j e c t o f type BaseClass . . . ” ) ; BaseClass bc1 = new BaseClass ( ) ; System . out . p r i n t l n ( ” Create another o b j e c t o f type BaseClass . . . ” ) ; BaseClass bc2 = new BaseClass ( 2 ) ; } 38 39 40 41 42 43 44 45 } Aufruf auf der Konsole: exttt¿java BaseClass Create an object of type BaseClass... Default constructor of BaseClass Create another object of type BaseClass... Some other Constructor of BaseClass: some_int: 2 Interessant sind die Zeilen 10-14 im obigen Beispiel: Durch overloading wird ein zweiter Konstruktor zur Verfügung gestellt. Innerhalb eines Konstruktors kann in der ersten Zeile der Konstruktor-Definition ein anderer Konstruktor der selben Klasse via this(...) aufgerufen werden. Hätten wir wesentlich mehr Object-Members zu initialisieren, könnten wir im ersten (default) Konstruktor (Zeilen 5-8) den zweiten Konstruktor (via this(init value); aufrufen (vergleiche dazu den Abschnitt über Methoden in 4.7). Hinweis: In Zeile 13 wird einer Member-Variable (some int ) der Wert des übergebenen Parameters zugewiesen. Diese Zeile demonstriert einen (der vielen) Vorteil des verwendeten Codingstandards: Durch das underline-Zeichen am Ende des Variablennamen erkennt man sofort, dass die Zuweisung auf eine Objekt- oder Class-Variable erfolgt. Man muss sich keine Gedanken über den Namen des Parameters (some int) machen, da diesem einfach der underscore (“ ”) “weggenommen” wird. Würde die Signatur des Konstruktors als Parameter eine Variable mit dem selben Namen wie die Klassenvariable (some int ) verwenden (public BaseClass(int some int )), so müsste man bei der Zuweisung, die Klassenvariable expliziter über this ansprechen. Die Zuweisung müsste daher: this.some int = some int ; lauten (this ist die Referenz auf das eigene Objekt). Diese kurze Ausführung soll die Notwendigkeit eines Codingstandards unterstreichen! In Zeile 16 wird noch eine Methode finalize() definiert, die im nächsten Abschnitt noch genauer beleuchtet wird. 4.3.2 Konstruktoren und Abgeleitete Klassen Für einfache Klassen ist das Verhalten also ziemlich einfach und verständlich: Immer wenn ein Objekt von einer Klasse erzeugt wird, wird auch der Konstruk- KAPITEL 4. KLASSEN IN JAVA 48 tor der Klasse aufgerufen. Wie schaut das aber bei komplizierteren Klassen aus, die von anderen Klassen (sog. Super-Klassen) abgeleitet wurden? Betrachten wir ein Beispiel, bei dem eine Klasse UglySubClass von BaseClass abgeleitet wird: 1 2 3 4 5 6 public c l a s s UglySubClass extends BaseClass { public UglySubClass ( ) { System . out . p r i n t l n ( ” D e f a u l t c o n s t r u c t o r o f UglySubClass ” ) ; } 7 public UglySubClass ( int s o m e i n t ) { System . out . p r i n t l n ( ”Some o t h e r Constructor o f UglySubClass ” ) ; setSomeInt ( s o m e i n t ) ; } 8 9 10 11 12 13 protected void f i n a l i z e ( ) throws Throwable { System . out . p r i n t l n ( ” F i n a l i z e r o f UglySubClass ” ) ; } 14 15 16 17 18 public int getSomeInt ( ) { System . out . p r i n t l n ( ” getSomeInt ( ) o f UglySubClass ” ) ; return 1 0 ; } 19 20 21 22 23 24 public s t a t i c void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ” Create an o b j e c t o f type UglySubClass . . . ” ) ; UglySubClass s c 1 = new UglySubClass ( ) ; System . out . p r i n t l n ( ” Create another o b j e c t o f type UglySubClass . . . ” ) ; UglySubClass s c 2 = new UglySubClass ( 2 0 ) ; } 25 26 27 28 29 30 31 32 } Aufruf auf der Konsole: exttt¿java UglySubClass Create an object of type UglySubClass... Default constructor of BaseClass Default constructor of UglySubClass Create another object of type UglySubClass... Default constructor of BaseClass Some other Constructor of UglySubClass setSomeInt() of BaseClass Bei abgeleiteten Klassen wird automatisch im Konstruktor der abgeleiteten Klasse der Default-Konstruktor der Super-Klasse aufgerufen. Innerhalb eines Default-Konstruktors mag dieses Verhalten durchaus erwünscht sein. Im Falle des zweiten Konstruktors (der mit dem Parameter some int) ist dieses Verhalten vermutlich nicht gewünscht! Schauen wir uns die Zeilen 8-12 von UglySubClass.java genauer an: Der Konstruktor hat die selbe Signatur wie der Konstruktor von BaseClass (Parameter ist ein Integer). In Zeile 11 erfolgt eine Zuweisung des Parameters auf die Member-Variable. Dieselbe Zuweisung passiert auch im zweiten Konstruktor von BaseClass. Wieso also diese Zuweisung extra noch einmal schreiben, wenn KAPITEL 4. KLASSEN IN JAVA 49 sie schon in der Basis-Klasse definiert wurde? Es wäre doch toll, wenn man einfach den Konstruktor der Basis-Klasse aufrufen könnte! Bei dieser einfachen Klasse ist dieses Verhalten vielleicht nicht so gravierend, aber wenn man komplexere Klassen anschaut, wo wesentlich mehr Member-Variablen initialisiert werden müssen, ist diese Mehrarbeit unbedingt zu vermeiden. In Java kann man genau an einer Stelle im Konstruktor (und nur dort) einen anderen aufrufen: In der ersten Zeile der Definition des Konstruktors. Die Verwendung von super ist in diesem Fall gleich wie die Verwendung von this in Abschnitt 4.3.1. Eine bessere Implementation (siehe NiceSubClass.java) der Zeile 8-12 müsste also lauten: public NiceSubClass ( int s o m e i n t ) { super ( s o m e i n t ) ; System . out . p r i n t l n ( ”Some o t h e r Constructor o f NiceSubClass ” ) ; } Dadurch ergibt sich der folgende Output: Aufruf auf der Konsole: exttt¿java NiceSubClass Create an object of type NiceSubClass... Default constructor of BaseClass Default constructor of NiceSubClass Create another object of type NiceSubClass... Some other Constructor of BaseClass: some_int: 20 Some other Constructor of NiceSubClass Zusammenfassend kann man sagen, dass die Konstruktoren aller abgeleiteten Klassen der Klassenhierarchie entsprechend von der Basis-Klasse beginnend aufgerufen werden. Dieses Verhalten deckt sich mit dem aus C++ bekannten Mechanismus. Vorsicht Falle: In Java sind alle Methoden virtuell, d.h. eine Methode wird zur Laufzeit in der Ableitungshierarchie gesucht und verwendet. Ausgenommen sind davon Methoden, die mit final markiert sind. Mit dieser Tatsache muss man leben. Als Folge ergibt sich, dass es absolut keinen Sinn macht, in einem Konstruktor Methoden aufzurufen, da man sich nicht sicher sein kann, welche Methode wirklich aufgerufen wird! Es besteht die Möglichkeit, dass auf noch nicht richtig initialisierte Member-Variablen zugegriffen wird! Zur Demonstration dient hier folgende Implementation einer ReallyUglySubClass. Hier ist der Fehler offensichtlich, weil in der Zeile 11 bereits auf einen Member zugegriffen wird, der erst eine Zeile später initialisiert wird. 1 2 3 4 5 6 public c l a s s ReallyUglySubClass extends BaseClass { public ReallyUglySubClass ( ) { System . out . p r i n t l n ( ” D e f a u l t c o n s t r u c t o r o f ReallyUglySubClass ” ) ; } 7 8 public ReallyUglySubClass ( int s o m e i n t ) KAPITEL 4. KLASSEN IN JAVA 50 { 9 System . out . p r i n t l n ( ”Some o t h e r Constructor o f ReallyUglySubClass ” ) ; System . out . p r i n t l n ( ” s o m e i n t i s : ” + getSomeInt ( ) ) ; setSomeInt ( s o m e i n t ) ; 10 11 12 } 13 14 protected void f i n a l i z e ( ) throws Throwable { System . out . p r i n t l n ( ” F i n a l i z e r o f ReallyUglySubClass ” ) ; } 15 16 17 18 19 public S t r i n g t o S t r i n g ( ) { return ” ReallyUglySubClass : s o m e i n t : ” + getSomeInt ( ) ; } 20 21 22 23 24 public s t a t i c void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ” Create an o b j e c t o f type ReallyUglySubClass . . . ” ) ; ReallyUglySubClass s c 1 = new ReallyUglySubClass ( ) ; System . out . p r i n t l n ( ” Create another o b j e c t o f type ReallyUglySubClass . . . ” ) ; ReallyUglySubClass s c 2 = new ReallyUglySubClass ( 2 0 ) ; System . out . p r i n t l n ( ” s c 2 i s : ” + s c 2 . t o S t r i n g ( ) ) ; } 25 26 27 28 29 30 31 32 33 } Aufruf auf der Konsole: exttt¿java ReallyUglySubClass Create an object of type ReallyUglySubClass... Default constructor of BaseClass Default constructor of ReallyUglySubClass Create another object of type ReallyUglySubClass... Default constructor of BaseClass Some other Constructor of ReallyUglySubClass getSomeInt() of BaseClass some_int_ is: 0 setSomeInt() of BaseClass getSomeInt() of BaseClass sc2 is: ReallyUglySubClass: some_int_: 20 4.4 ’Destruktor’ einer Klasse In C++ wird über delete ein Objekt explizit gelöscht und der Destruktor der Klasse wird aufgerufen. Handelt es sich bei der Klasse um eine abgeleitete Klasse, werden außerdem automatisch die Destruktoren der Parent-Klassen aufgerufen. In Java ist die Situation anders. Hier übernimmt der sog. garbage collector (GC) die Speicherverwaltung. Ist ein erzeugtes Objekt nicht mehr referenzierbar (z.B. weil der Gültigkeitsbereich der Variable verlassen wurde), markiert der GC dieses Objekt. Der ursprünglich reservierte Speicher wird aber noch nicht freigegeben. Erst wenn der Speicher wirklich gebraucht wird, entscheidet der GC, welches Objekt entfernt wird. Bevor der Speicher des Objekts freigegeben wird, wird noch der Finalizer (d.h. die Methode finalize, ohne Parameter) KAPITEL 4. KLASSEN IN JAVA 51 aufgerufen. Wann und ob nun der GC von Java das nicht mehr gebrauchte Objekt tatsächlich löscht, steht in den Sternen. Hinweis: Es gibt zwar einen ’Destruktor’ einer Klasse, es wird aber nicht garantiert, dass er überhaupt aufgerufen wird! Betrachtet man die Ausgaben in den letzten Beispielen, so sieht man, dass niemals die Methode finalize() aufgerufen wurde. Im folgenden Beispiel verwenden wir die bereits bekannte Klasse (NiceSubClass): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public c l a s s D e s t r u c t o r T e s t { public s t a t i c void main ( S t r i n g [ ] argv ) { NiceSubClass nsc = new NiceSubClass ( ) ; nsc = null ; System . out . p r i n t l n ( ” j u s t b e f o r e r u n F i n a l i z a t i o n ( ) . . . ” ) ; System . r u n F i n a l i z a t i o n ( ) ; System . out . p r i n t l n ( ” a f t e r r u n F i n a l i z a t i o n ( ) . . . ” ) ; System . out . p r i n t l n ( ” j u s t b e f o r e gc ( ) . . . ” ) ; System . gc ( ) ; System . out . p r i n t l n ( ” a f t e r gc ( ) . . . ” ) ; } } Aufruf auf der Konsole: exttt¿java DestructorTest Default constructor of BaseClass Default constructor of NiceSubClass just before runFinalization()... after runFinalization()... just before gc()... Finalizer of NiceSubClass after gc()... In Zeile 6 wird explizit dem Objekt nsc null zugewiesen und damit unbrauchbar gemacht. Der GC weiss ab diesem Moment, dass auf den ursprünglichen Wert von nsc nicht mehr zugegriffen werden kann. Es werden im Anschluss zwei Methoden aufgerufen, die der JVM anweisen, “wenn notwendig” den garbage collector aufzurufen. Aus der Dokumentation geht hervor, dass hier nicht unbedingt der Finalizer aufgerufen werden muss: public static void runFinalization(): Runs the finalization methods of any objects pending finalization. Calling this method suggests that the Java Virtual Machine expend effort toward running the finalize methods of objects that have been found to be discarded but whose finalize methods have not yet been run. When control returns from the method call, the Java Virtual Machine has made a best effort to complete all outstanding finalizations. Und dann noch: KAPITEL 4. KLASSEN IN JAVA 52 public static void gc(): Runs the garbage collector. Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects in order to make the memory they currently occupy available for quick reuse. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all discarded objects. The call System.gc() is effectively equivalent to the call: Runtime.getRuntime().gc(). Im Unterschied zu C++ wird in Java nicht automatisch die finalize-Methode der Parent-Klasse aufgerufen. Man müsste daher selbstständig den ParentFinalizer (via super.finalize();) als erste Zeile des Finalizers aufrufen. Eine akzeptable Implementation eines Finalizers wäre: protected void f i n a l i z e ( ) throws Throwable { super . f i n a l i z e ( ) ; } Exceptions, die in einer finalize-Methode auftreten, müssen bei der MethodenDefinition angegeben werden. Diese Exceptions werden aber nicht ausgewertet (wie und wo auch?). Die finalize-Methode wird beim Auftreten einer Exception die nicht behandelt wird, einfach verlassen. Mit diesem Wissen bewaffnet, kann man sehr schnell überprüfen, ob man eine existierende Java-Library verwenden soll: Wird finalize implementiert und werden dort Ressourcen freigegeben, ist es sicher nicht sehr gescheit, diese Library zu verwenden. In diesem Fall dürfte Java nicht die “Muttersprache” des Entwicklers sein... Hinweis: Da man nie sicher sein kann, ob und wann der Destruktor für Objekte tatsächlich aufgerufen wird, macht es absolut keinen Sinn, Funktionalität in den Destruktor (=finalize()) zu stecken! Hinweis: In C++ werden Destruktoren dafür verwendet, Ressourcen (z.B. Files, Netzwerkverbindungen etc.) wieder freizugeben. Das ist auch sinnvoll, da man sich in C++ ja sicher sein kann, dass der Destruktor der Klasse auch aufgerufen wird. In Java muss man zur Ressourcenfreigabe separate Methoden schreiben (sinnvolle Namen währen hier z.B. close() oder dispose()), die auch vom Entwickler explizit verwendet werden müssen. Seit Java 1.3 gibt es eine Methode addShutdownHook(someThread) die einen initialisierten, aber noch nicht gestarteten Thread übergibt. Wird die JVM beendet (via System.exit(int), oder System.halt()), so wird dieser someThread gestartet. Allerdings gibt es auch hier die Möglichkeit, dass dieser Thread nicht ausgeführt wird... 4.5 Member-Modifier Zu den Members einer Klasse gehören Variablen, Methoden und selbst wieder Klassen. Alle Member verwenden sog. Zugriffsmodifier (access modifier ), die KAPITEL 4. KLASSEN IN JAVA 53 zur Kapselung von Members verwendet werden. Zunächst betrachten wir diese Access-Modifier, im Anschluss den static-Modifier. Da bei Variablen und Methoden unterschiedliche Modifier zur Anwendung kommen, gibt es dementsprechend eigene Abschnitte dazu. Seit Java 1.4 (genauer: Java Platform 2, Version 1.4 ) gibt es noch den Modifier strictfp, der hier nicht behandelt wird. 4.5.1 Access-Modifier Der Zugriff auf einzelne Elemente einer Klasse (Methoden, Member-Variablen, Konstruktoren) wird über sog. Zugriffsmodifier (access modifiers) gesteuert. Generell sollte der Zugriff auf einzelne Elemente so eingeschränkt wie möglich sein. Ein Einblick in Packages wird später in Abschnitt 4.11 gegeben. Für die Betrachtungen in diesem Abschnitt reicht es, wenn wir wissen, dass Klassen in sog. Packages zusammengefasst werden können. Die folgende Klasse verwenden wir für die Demonstration der Zugriffsmodifier: 1 package t e s t p a c k ; 2 3 4 5 6 7 8 public c l a s s AccessModifierMembersClass { int a p a c k a g e i n t = 1 ; private int a p r i v a t e i n t = 2 ; protected int a p r o t e c t e d i n t = 3 ; public int a p u b l i c i n t = 4 ; 9 public int g e t A P r i v a t e I n t ( ) { return a p r i v a t e i n t ; } 10 11 12 13 14 } Folgende Modifier sind in Java definiert: public: Der Zugriff für sämtliche anderen Klassen ist gestattet. Hier sollte man aufpassen, da wirklich jede Klasse mit einem solchen Member arbeiten kann. private: Zugriff nur in der Klasse, wo das Element definiert wurde. Will man eine falsche Manipulation von außerhalb der Klasse verhindern, macht es Sinn, die Variable private zu markieren. Soll wirklich von Außen auf z.B. eine Variable zugegriffen werden, sollen dafür mit protected markierte Zugriffsmethoden (“getter”) verwendet werden. Der Codingstandard sieht für den Methodennamen einer Zugriffsmethode getNameOfVariable(); vor. Auf den Wert von boolean-Variablen sollten via isNameOfVariable(); zugegriffen werden. default: Wird kein Modifier angegeben, so ist der Zugriff auf Klassen beschränkt, die im selben Package sind. Abgeleitete Klassen außerhalb des Packages haben keinen Zugriff auf diese Elemente! protected: Es dürfen zusätzlich zur eigenen Klasse, auch abgeleitete Klassen und Klassen im selben Package auf das Element zugreifen. KAPITEL 4. KLASSEN IN JAVA 54 Nun folgen die Beispiele. Die entsprechenden Fehler, die bei einem illegalen Zugriff passieren, sind in den Kommentaren notiert: einfache Klasse: 1 import t e s t p a c k ; 2 3 4 5 6 7 class AccessModifierTest1 { public s t a t i c void main ( S t r i n g [ ] a r g s ) { AccessModifierMembersClass obj = new AccessModifierMembersClass ( ) ; 8 // // i l l e g a l a c c e s s : a p a c k a g e i n t i s not p u b l i c i n // t e s t p a c k . AccessModifierMembersClass ; cannot be a c c e s s e d // from o u t s i d e package System . out . p r i n t l n ( obj . a p a c k a g e i n t ) ; // // i l l e g a l a c c e s s : a p r i v a t e i n t has p r i v a t e a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r i v a t e i n t ) ; 9 10 11 12 13 14 15 16 17 // l e g a l a c c e s s System . out . p r i n t l n ( obj . g e t A P r i v a t e I n t ( ) ) ; 18 19 20 // i l l e g a l a c c e s s : a p r o t e c t e d i n t has p r o t e c t e d a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r o t e c t e d i n t ) ; 21 22 23 // 24 // l e g a l a c c e s s , because v a r i a b l e i s p u b l i c ! System . out . p r i n t l n ( obj . a p u b l i c i n t ) ; 25 26 } 27 28 } einfache Klasse, im selben Package: 1 package t e s t p a c k ; 2 3 4 5 6 7 class AccessModifierTest2 { public s t a t i c void main ( S t r i n g [ ] a r g s ) { AccessModifierMembersClass obj = new AccessModifierMembersClass ( ) ; 8 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p a c k a g e i n t ) ; 9 10 11 // i l l e g a l a c c e s s : a p r i v a t e i n t has p r i v a t e a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r i v a t e i n t ) ; 12 13 14 // 15 // l e g a l a c c e s s System . out . p r i n t l n ( obj . g e t A P r i v a t e I n t ( ) ) ; 16 17 18 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p r o t e c t e d i n t ) ; 19 20 21 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p u b l i c i n t ) ; 22 23 } 24 25 } einfache, abgeleitete Klasse: KAPITEL 4. KLASSEN IN JAVA 1 55 import t e s t p a c k ; 2 3 4 5 6 7 c l a s s A c c e s s M o d i f i e r T e s t 3 extends AccessModifierMembersClass { public s t a t i c void main ( S t r i n g [ ] a r g s ) { AccessModifierMembersClass obj = new AccessModifierMembersClass ( ) ; 8 // // i l l e g a l a c c e s s : a p a c k a g e i n t i s not p u b l i c i n // t e s t p a c k . AccessModifierMembersClass ; cannot be a c c e s s e d // from o u t s i d e package System . out . p r i n t l n ( obj . a p a c k a g e i n t ) ; // // i l l e g a l a c c e s s : a p r i v a t e i n t has p r i v a t e a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r i v a t e i n t ) ; 9 10 11 12 13 14 15 16 17 // l e g a l a c c e s s , because method i s p u b l i c ! System . out . p r i n t l n ( obj . g e t A P r i v a t e I n t ( ) ) ; 18 19 20 // i l l e g a l a c c e s s : a p r o t e c t e d i n t has p r o t e c t e d a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r o t e c t e d i n t ) ; 21 22 23 // 24 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p u b l i c i n t ) ; 25 26 } 27 28 } einfache, abgeleitete Klasse, im selben Package: 1 package t e s t p a c k ; 2 3 4 5 6 7 c l a s s A c c e s s M o d i f i e r T e s t 4 extends AccessModifierMembersClass { public s t a t i c void main ( S t r i n g [ ] a r g s ) { AccessModifierMembersClass obj = new AccessModifierMembersClass ( ) ; 8 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p a c k a g e i n t ) ; 9 10 11 // i l l e g a l a c c e s s : a p r i v a t e i n t has p r i v a t e a c c e s s i n // t e s t p a c k . AccessModifierMembersClass System . out . p r i n t l n ( obj . a p r i v a t e i n t ) ; 12 13 14 // 15 // l e g a l a c c e s s , because method i s p u b l i c ! System . out . p r i n t l n ( obj . g e t A P r i v a t e I n t ( ) ) ; 16 17 18 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p r o t e c t e d i n t ) ; 19 20 21 // l e g a l a c c e s s System . out . p r i n t l n ( obj . a p u b l i c i n t ) ; 22 23 } 24 25 } Wie man bei diesen Beispielen sieht, sollte man wissen, wie die Zugriffsmodifier zu verstehen sind. Noch einmal der Hinweis: Grundsätzlich ist der Zugriff auf Variablen von außerhalb der definierten Klasse über Zugriffsfunktionen zu steuern! Ein direkter Zugriff auf Variablen soll nur in der Klasse, in der die Variablen definiert sind, erfolgen! KAPITEL 4. KLASSEN IN JAVA 4.5.2 56 Object- und Class-Members Wie in C++ gibt es auch in Java die Möglichkeit, Variablen und Methoden für alle Objekte einer Klasse (Class-Members) zu definieren bzw. sie für jedes Objekt separat zur Verfügung zu stellen (Object-Member ). Die Unterscheidung, ob ein Member einer Klasse ein Object- oder ein Class-Member ist, erfolgt über den Modifier static. Für Variablen in einer Klasse ist das Verhalten offensichtlich: Sie werden bei der ersten Instantiierung eines Objekts dieses Typs initialisiert. Alle weiteren Instanzen von diesem Typ “teilen” sich diese Variable. Aber nicht nur Variablen, sondern auch Methoden können mit dem Schlüsselwort static markiert werden. Mit solchen Methoden kann man im Prinzip wie mit Funktionen arbeiten. Es ist keine Instanz der Klasse für den Aufruf notwendig. Bereits in Abschnitt 2.1.1 haben wir von statischen Methoden gesprochen, die in Referenz-Datentypen Operationen durchführen (z.B. Integer.parseInt()). Innerhalb dieser Methoden kann man natürlich nicht auf Object-Member zugreifen (hier würde der Compiler einen Fehler ausgeben: “non-static variable name cannot be referenced from a static context”). Das folgende einfache Beispiel hat eine klassenweite Variable (static int num instances ), die die Anzahl der bereits instantiierten Objekte mitzählt. Außerdem gibt es noch eine klassenweite Methode (’static int getNumInstances()’), die diesen Wert returniert. Diese Methode kann auf zwei unterschiedliche Arten aufgerufen werden: Über eine Instanz der Klasse und auch ohne Instanz. Schauen wir uns aber zuerst einmal die Klasse mit den unterschiedlichen Members an: 1 2 3 4 c l a s s ClassObjectMemberExample { s t a t i c int n u m i n s t a n c e s = 0 ; private S t r i n g name ; 5 public ClassObjectMemberExample ( S t r i n g name) { System . out . p r i n t l n ( ” c r e a t e o b j e c t : ” + name + ” . . . ” ) ; name = name ; n u m i n s t a n c e s ++; } 6 7 8 9 10 11 12 public S t r i n g getName ( ) { return name ; } 13 14 15 16 17 public s t a t i c int getNumInstances ( ) { return n u m i n s t a n c e s ; } 18 19 20 21 22 } Hier noch das dazu passende Testprogramm: 1 2 3 4 5 6 7 8 c l a s s ClassObjectMemberTest { public s t a t i c void main ( S t r i n g [ ] a r g s ) { System . out . p r i n t l n ( ”There a r e ” + ClassObjectMemberExample . getNumInstances () + ” i n s t a n c e s o f ClassObjectMemberExample . . . ” ) ; KAPITEL 4. KLASSEN IN JAVA 57 ClassObjectMemberExample o t t o = new ClassObjectMemberExample ( ”Otto” ) ; ClassObjectMemberExample hugo = new ClassObjectMemberExample ( ”Hugo” ) ; 9 10 11 System . out . p r i n t l n ( ” o t t o . getNumInstances : ” + o t t o . getNumInstances () + ” i n s t a n c e s o f ClassObjectMemberExample . . . ” ) ; System . out . p r i n t l n ( ”hugo . getNumInstances : ” + hugo . getNumInstances () + ” i n s t a n c e s o f ClassObjectMemberExample . . . ” ) ; System . out . p r i n t l n ( ” c a l l on c l a s s : ” + ClassObjectMemberExample . getNumInstances () + ” i n s t a n c e s o f ClassObjectMemberExample . . . ” ) ; 12 13 14 15 16 17 18 19 20 21 System . out . p r i n t l n ( ”Name o f o b j e c t o t t o : ” + o t t o . getName ( ) ) ; System . out . p r i n t l n ( ”Name o f o b j e c t hugo : ” + hugo . getName ( ) ) ; 22 23 } 24 25 } Aufruf auf der Konsole: exttt¿java ClassObjectMemberTest There are 0 instances of ClassObjectMemberExample... create object: Otto... create object: Hugo... otto.getNumInstances: 2 instances of ClassObjectMemberExample... hugo.getNumInstances: 2 instances of ClassObjectMemberExample... call on class: 2 instances of ClassObjectMemberExample... Name of object otto: Otto Name of object hugo: Hugo Beachte bitte die unterschiedlichen Zugriffe auf die statische Methode und die Verwendung der Variablen name : Jedes Objekt hat eine eigene Variable name ! 4.5.3 Variablen-Modifier Damit Variablen noch bestimmte Eigenschaften bekommen, können sie noch mit folgenden Modifier versehen werden: final: Die Variable darf nach dem ersten Initialisieren nicht mehr verändert werden. Vorsicht: Leider gibt es in Java keinen const-Modifier. Für Konstanten schlägt Sun Variablen vom Typ static final vor. transient: Zeigt an, dass diese Variable nicht zum persistenten Zustand eines Objekts gehört, d.h. ihr Wert kann jederzeit wieder berechnet werden. Anwendung findet dieser Modifier bei Objekten, die z.B. über ein Netzwerk auf einem anderen Computer übertragen werden müssen. Hier macht es keinen Sinn, diese Variablen auch zu übertragen. volatile: Variable kann von unsynchronized Methoden verwendet werden, daher werden einige Optimierungen vom Compiler nicht durchgeführt (wird sehr selten verwendet...) KAPITEL 4. KLASSEN IN JAVA 4.5.4 58 Methoden-Modifier Auch Methoden können noch mit Modifier versehen werden, um ihre Eigenschaften genauer zu definieren. final: Diese Methode kann nicht mehr in einer abgeleiteten Klasse anders definiert werden. Dadurch erübrigt sich das dynamische Binden von Methoden bei Ableitungen. abstract: Es folgt keine Definition der Methode! Es wird nur darauf hingewiesen, dass diese Methode von einer abgeleiteten Klasse implementiert werden muss. synchronized: Diese Methode kann nicht gleichzeitig von verschiedenen Threads aufgerufen werden. native: Die Definition dieser Methode erfolgt nicht in Java sondern in einer anderen Programmiersprache. Ist vor allem sinnvoll, wenn bestehende Libraries in Java verwendet werden sollen (Stichwort “Java Native Interface”, JNI). 4.6 Initialisierung von Member-Variablen Object- und Class-Variablen werden auf Standardwerte initialisiert. ClassVariablen, wenn die Klasse das erste Mal geladen wird bzw. Object-Variablen bei der Erzeugung von neuen Objekten. Sollen andere Werte als die Standardwerte verwendet werden, kann das in unterschiedlichster Form erfolgen: Object-Variablen: Object-Members sollten im Konstruktor (siehe 4.3) initialisiert werden. Es gibt zwar auch die Möglichkeit, Object-Variablen innerhalb eines Blocks und innerhalb der Klasse zu initialisieren, jedoch sollte man das nicht in dieser Form umsetzen. Die Wartung des Codes ist allerdings durch eine solche Initialisierung extrem schwer. Initialisierungen innerhalb des Konstruktors sind intuitiver. Da ein Konstruktor implizit für alle Objekte von diesem Typ aufgerufen wird, ist es offensichtlich, dass Class-Variablen nicht über den Konstruktor initialisiert werden können. Class-Variablen (static): Für die Initialisierungen von Class-Members kann eine einfache Zuweisung genommen werden: static int num_instances_ = 0; Wenn man sich mit Java beschäftigt, ist sofort klar, wo hier Problem auftreten können: • Die Initialisierung ist auf einfache Zuweisungen beschränkt. • Es können einfache Methoden aufgerufen werden, nicht jedoch Methoden, die Exceptions werfen (checked und runtime exceptions; siehe 5). KAPITEL 4. KLASSEN IN JAVA 59 Ein Ausweg aus dieser Situation bietet ein static-Block, der die ClassVariablen initialisiert: static { // performe h e r e high−s o p h i s t i c a t e d c l a s s −member i n i t i a l i z a t i o n // i n c l u d i n g e r r o r −handling v i a e x c e p t i o n s . . . } Vorsicht Falle: Class-Variablen werden beim ersten Laden der Klasse (z.B. TestClass) im beschriebenen static-Block initialisiert. Nun kann man in diesem static-Block über z.B. Methoden einer anderen Klasse (z.B. StaticInitializerProblem) auf einen nicht initialisierten Wert der Klasse TestClass zugreifen. Man sollte sich dessen bewusst sein, und, wenn möglich, auf diesen static-Block verzichten. 1 2 3 c l a s s T e st C l a s s { s t a t i c int a n i n t ; 4 static { System . out . p r i n t l n ( ” a n i n t not yet i n i t i a l i z e d : ” + S t a t i c I n i t i a l i z e r P r o b l e m . getAnInteger ( ) ) ; an int = 25; } 5 6 7 8 9 10 11 } 12 13 14 15 16 17 18 public c l a s s S t a t i c I n i t i a l i z e r P r o b l e m { s t a t i c int getAnInteger ( ) { return T e st C l a s s . a n i n t ; } 19 public s t a t i c void main ( S t r i n g [ ] a r g s ) { T e s t C l a s s obj = new T e s t C l a s s ( ) ; } 20 21 22 23 24 } Aufruf auf der Konsole: exttt¿java StaticInitializerProblem an_int_ not yet initialized: 0 4.7 Methoden In Java gibt es nur Methoden und keine Funktionen. Will man Funktionen definieren (z.B. berechne die Wurzel einer Zahl...), so müssen diese in Klassen gesammelt werden. Hier macht es Sinn, die Methoden als static zu definieren. Ein Beispiel in der im SDK mitgelieferten API ist die Klasse java.lang.Math: In dieser sind viele mathematische Funktionen gesammelt, die über die Math Klasse aufgerufen werden (z.B. Math.sqrt(25);). Die Definition von Methoden ist ähnlich wie in C++: KAPITEL 4. KLASSEN IN JAVA 60 [ public | private | protected ] return−type methodName ( [ argument−l i s t ] ) [ throws SomeException ] • Informationen zu den method-modifier (public, private und protected bzw. default) sind in Abschnitt 4.5.1 zu finden. • Der return-type gibt an, welches Ergebnis die Methode liefert. Wird kein Wert returniert, so muss void als Typ angegeben werden. • Für den Namen gilt dasselbe wie für Namen von Variablen (siehe Abschnitt 2.2.1). • Die argument-list besteht aus einer definierten, beliebig langen Liste von Argumenten (incl. Typinformation). Argumente jeden Typs (Primitiveund Referenz-Datentypen) werden via call-by-value verwendet. • Der letzte Teil der Signatur (throws...) wird in Kapitel 5 genauer untersucht. Soweit scheint sich also im Vergleich zu anderen Programmiersprachen nichts verändert zu haben. Betrachten wir nun aber einige Spezialitäten in Java. call-by-value: Alle Parameter von Methoden werden immer über ihren Wert übergeben. Für Primitive-Datentypen kann also eine übergebene Variable nicht in der aufrufenden Methode geändert werden. Referenz-Datentypen werden speziell behandelt: Hier wird der Wert der Referenz (d.h. die Adresse) übergeben. In anderen Worten: Es wird bei einem Methodenaufruf nicht das ganze Objekt auf den Stack gelegt, sondern lediglich die Referenz von dem Objekt. Es ist daher möglich, die übergebenen Objekte zu manipulieren, sofern sie nicht immutable (unveränderlich) sind. Schauen wir uns dieses Verhalten bei einem Beispiel an: 1 2 3 4 5 6 7 8 9 10 11 12 public c l a s s CallByValueExample { // change the v a l u e o f some o b j e c t s t a t i c void manipulateParameter ( BaseClass bc , int new value ) { bc . setSomeInt ( new value ) ; } // t h i s does ∗ not ∗ change the v a l u e o f bc o u t s i d e the method . . . s t a t i c void dontManipulateParameter ( BaseClass bc , int new value ) { bc = new BaseClass ( new value ) ; } 13 public s t a t i c void main ( S t r i n g [ ] a r g s ) { BaseClass bc1 = new BaseClass ( 2 ) ; System . out . p r i n t l n ( ” bc1 i s : ” + bc1 ) ; manipulateParameter ( bc1 , 1 0 ) ; System . out . p r i n t l n ( ” bc1 i s now : ” + bc1 ) ; dontManipulateParameter ( bc1 , 2 0 ) ; System . out . p r i n t l n ( ” bc1 i s now : ” + bc1 ) ; 14 15 16 17 18 19 20 21 22 } 23 24 25 } KAPITEL 4. KLASSEN IN JAVA 61 Aufruf auf der Konsole: exttt¿java CallByValueExample Some other Constructor of BaseClass: some_int: 2 bc1 is: BaseClass: some_int_: 2 BaseClass::setSomeInt(...) bc1 is now: BaseClass: some_int_: 10 Some other Constructor of BaseClass: some_int: 20 bc1 is now: BaseClass: some_int_: 10 KEINE Default-Parameter: Eine lästige Einschränkung bei Methoden in Java ist das Fehlen von Default-Parametern. Als Konsequenz muss man entweder • beim Aufruf der Methode alle Parameter angeben und gegebenenfalls mit Default-Werten aufrufen, oder • man überläd eine Methode mit allen möglichen Parametern (overloading). Hier ist es sinnvoll, die Funktionalität in einer Methode (selbstverständlich jener, mit allen Parametern :)) zu implementieren und dann diese in allen anderen Methoden aufzurufen. Als einfaches Beispiel (mit lediglich 2 Parametern) folgt nun ein Auszug aus java.lang.String: public boolean s t a r t s W i t h ( S t r i n g p r e f i x , int t o f f s e t ) { // h e r e the implementation . . . return . . . ; } public boolean s t a r t s W i t h ( S t r i n g p r e f i x ) { return s t a r t s W i t h ( p r e f i x , 0 ) ; } Die erste Definition implementiert die Funktionalität, und die zweite Definition ruft lediglich die erste auf. final für Parameter: In C++ können Parameter, die in der Methode nicht verändert werden dürfen, mit const markiert. Wenn man eine Library verwendet sieht man sofort, welche Parameter nicht verändert werden können. In Java kann man mit dem schon bekannten final Schlüsselwort Parameter versehen, die nur einmal – nämlich beim Aufruf der Methode – initialisiert werden dürfen (seit Java 1.1). Werden allerdings Objekte übergeben, können diese bei vorhandenen Modifier-Methoden verändert werden. automatischer cast bei Parameter: Wird als Parameter einer Methode ein Primitiver-Datentyp übergeben, kann dieser automatisch in einen ’größeren’ Primitiven-Datentyp umgewandelt werden. Will man explizit eine Methode mit einem bestimmten Primitiven-Datentyp aufrufen, muss man den Parameter explizit casten. Zusätzlich gibt es noch Probleme beim Method-Overriding... (siehe Abschnitt 4.9). KAPITEL 4. KLASSEN IN JAVA 62 toString: Wenn eine String-Repräsentation eines Objekts gebraucht wird, dann wird automatisch nach einer Methode mit dem Namen toString gesucht (z.B. System.out.println(obj); bedeutet implizit System.out.println(obj.toString());). Vor allem für Debug-Zwecke ist eine sinnvolle Implementation dieser Methode wichtig. Zuweisung vs. clone(): Bei einer “normalen” Zuweisung von einem Objekt auf ein anderes, wird lediglich die Referenz auf das Ziel-Objekt zugewiesen. Es wird also keine echte Kopie des Objekts angelegt. Soll ein Objekt kopiert werden, wird die Methode clone() als Implementation vorgeschlagen. Diese Methode entspricht dem copy-Konstruktor von C++. Der Aufruf ist so, wie man es sich erwartet: destination obj = source obj.clone();. Vergleich vs. equals(...): Ein “normaler” Vergleich (via ==) betrachtet bei Referenz-Datentypen die Referenzen der verwendeten Objekte. Eine unterschiedliche Referenz sagt aber nicht aus, dass die Inhalte der Objekte verschieden sind! Es kann ja sein, dass das Objekt vorher via clone() mit den gleichen Werten erzeugt wurde! In Java wird vorgeschlagen, die Methode equals(...) für Klassen zu implementieren. Ob ein Objekt gleich wie das andere ist, hängt von der internen Struktur der jeweiligen Klasse ab. Aufruf: obj.equals(an obj);. 4.8 Inner Classes Inner Classes (auch bekannt als “nested-classes”) sind Klassen, die innerhalb von anderen Klassen definiert sind. Wenn die Existenz der inneren Klasse nur im Kontext der äußeren Klasse Sinn macht, sollte man dieses Design verwenden. Die innere Klasse hat vollen Zugriff auf Member der äußeren Klasse (auch wenn diese mit dem Schlüsselwort private gekennzeichnet sind!). Da diese Typen von Klassen wie “normale” Member einer Klasse behandelt werden, können auch die Access-Modifier (Abschnitt 4.5.1) verwendet werden. class EnclosingClass { . . . static class AStaticNestedClass { . . . } class InnerClass { . . . } class // an anonymous c l a s s { . . . } } Wie alle Member von Klassen können auch diese Klassen mit dem Schlüsselwort static markiert sein. static Klassen können wiederum nur auf statische Member zugreifen! Besondere Inner Classes (sog. “Anonyme Klassen”) haben keinen Klassennamen. Via KAPITEL 4. KLASSEN IN JAVA 63 new InterfaceName() { // here the implementation } oder new ClassName( arguments ) { // here the implementation } kann eine Instanz einer solchen Klasse erzeugt werden. Nested-classes sollten vermieden werden, da die Wartbarkeit des Codes leidet. Beim Compilieren wird Bytecode in der Form EnclosingClass$InnerClass. class erzeugt. Anonyme Klassen werden in den Bytecode-File EnclosingClass$1. class übersetzt. Innere Klassen sind schwer zu lesen und sollten wirklich nur für einfachste Klassen (z.B. Event-Handler Adapter) verwendet werden. Aus diesem Grund wird an dieser Stelle nicht weiter auf die Struktur dieser Klassen eingegangen. 4.9 Ableitung Bei den Konstruktoren und Destruktoren haben wir bereits mit Ableitungen gearbeitet und die Reihenfolge der Aufrufe betrachtet. Nun schauen wir uns Eigenheiten bezüglich Methoden und Variablen von Java an. In Java ist – anders als in C++ – nur eine einfache Ableitung möglich. Das Schlüsselwort dazu haben wir schon in Abschnitt 4.2 kennengelernt: extends. Hinweis: Es kann nicht von Klassen abgeleitet werden, die mit final deklariert wurden! Eine abgeleitete Klasse “erbt” von ihrer Super-Klasse Methoden. Nicht final deklarierte Methoden von Basis-Klassen können via overriding neu definiert werden. Methoden in der abgeleiteten Klasse mit der selben Signatur “overriden” die Methoden der Super-Klasse3 . Dazu haben wir schon einige Beispiele gesehen. Wichtig ist, dass alle (nicht final markierten) Methoden virtuell sind, d.h. es wird immer die letzte Implementation in der Ableitungskette gesucht! Auch wenn das Objekt mit einem cast als Typ der Super-Klasse verwendet wird, wird immer die “letzte” Implementation in der Ableitungskette verwendet. Im folgenden Beispiel sieht man dieses Verhalten in Zeile 46/47: Es wird für die Methode getSomeInt die Implementation aus der Klasse SimpleSubClass verwendet und nicht, wie man vielleicht annehmen könnte, die Implementation aus SimpleBaseClass. Will man explizit die Implementation aus einer BasisKlasse verwenden, kann man über das Schlüsselwort super darauf zugreifen. 1 2 3 c l a s s SimpleBaseClass { int s o m e i n t = 1 ; 4 int getSomeInt ( ) { return s o m e i n t ; } 5 6 7 8 9 } 10 11 12 c l a s s SimpleSubClass extends SimpleBaseClass { 3 Bitte “override” nicht mit “overload” verwechseln. Beim “overloading” hat die Methode zwar denselben Namen, aber unterschiedliche Parameter! KAPITEL 4. KLASSEN IN JAVA int s o m e i n t 13 = 2; // shadows s o m e i n t 64 from SimpleBaseClass 14 int getSomeInt ( ) // o v e r r i d e s getSomeInt from SimpleBaseClass { return − s o m e i n t ; } 15 16 17 18 19 int getSuperSomeInt ( ) { return super . getSomeInt ( ) ; } 20 21 22 23 24 } 25 26 27 28 29 30 31 32 public c l a s s DynamicMethodExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { SimpleBaseClass b a s e c l a s s o b j = new SimpleBaseClass ( ) ; SimpleSubClass s u b c l a s s o b j = new SimpleSubClass ( ) ; SimpleBaseClass s u b c l a s s a s b a s e c l a s s o b j = ( SimpleBaseClass ) s u b c l a s s o b j ; 33 System . out . p r i n t l n ( ” b a s e c l a s s o b j . getSomeInt ( ) : ” + b a s e c l a s s o b j . getSomeInt ( ) ) ; System . out . p r i n t l n ( ” b a s e c l a s s o b j . s o m e i n t : ” + baseclass obj . some int ) ; 34 35 36 37 38 System . out . p r i n t l n ( ” s u b c l a s s o b j . getSomeInt ( ) : ” + s u b c l a s s o b j . getSomeInt ( ) ) ; System . out . p r i n t l n ( ” s u b c l a s s o b j . getSuperSomeInt ( ) : ” + s u b c l a s s o b j . getSuperSomeInt ( ) ) ; System . out . p r i n t l n ( ” s u b c l a s s o b j . s o m e i n t : ” + subclass obj . some int ) ; 39 40 41 42 43 44 45 System . out . p r i n t l n ( ” s u b c l a s s a s b a s e c l a s s o b j . getSomeInt ( ) : ” + s u b c l a s s a s b a s e c l a s s o b j . getSomeInt ( ) ) ; System . out . p r i n t l n ( ” s u b c l a s s a s b a s e c l a s s o b j . s o m e i n t : ” + s u b c l a s s a s b a s e c l a s s o b j . some int ) ; 46 47 48 49 } 50 51 } Aufruf auf der Konsole: exttt¿java DynamicMethodExample baseclass_obj.getSomeInt(): 1 baseclass_obj.some_int_: 1 subclass_obj.getSomeInt(): -2 subclass_obj.getSuperSomeInt(): 1 subclass_obj.some_int_: 2 subclass_as_baseclass_obj.getSomeInt(): -2 subclass_as_baseclass_obj.some_int_: 1 Anders ist das Verhalten bei Variablen: Hier sieht man in Zeile 48/49 des vorherigen Beispiels, dass man den Variablen-Wert jener Klasse bekommt, auf den das Objekt “gecastet” wurde. Im Fall von Zeile 48/49 also den Wert aus SimpleBaseClass. Mit super.some var name kann man explizit in abgeleiteten Klassen auf die versteckte Variable der Super-Klasse zugreifen. Vorsicht Falle: Man kann es wirklich nicht oft genug schreiben: Man sollte nie vergessen, dass Methoden virtuell sind! Dadurch wird immer die letzte in der Ableitungskette definierte Implementation einer Methode aufgerufen. Dieses Verhalten kann man nur verhindern, indem man die jeweilige Methode in der Super-Klasse final deklariert. KAPITEL 4. KLASSEN IN JAVA 65 Eigentlich sollten nur jene Methoden in einer Sub-Klasse neu definieren werden, die exakt dieselbe Signatur haben (“overriding”), wie jene in der Basis-Klasse. Alles andere ist overloading. Betrachten wir folgendes Beispiel, wo versucht wird, eine Methode mit float-Parameter zu definieren, die bereits in der BasisKlasse (BaseClass.java) mit einem int-Parameter definiert ist. 1 2 3 4 5 6 7 8 public c l a s s OverloadingExample extends BaseClass { // o v e r l o a d setSomeInt : watch s i g n a t u r e ! ! public void setSomeInt ( f l o a t s o m e f l o a t ) { System . out . p r i n t l n ( ” setSomeInt ( f l o a t ) o f OverloadingExample ” ) ; s o m e i n t = ( int ) s o m e f l o a t ; } 9 public s t a t i c void main ( S t r i n g [ ] a r g s ) { OverloadingExample obj = new OverloadingExample ( ) ; System . out . p r i n t l n ( ” c a l l now setSomeInt with f l o a t . . . ” ) ; obj . setSomeInt ( 1 2 . 0 f ) ; 10 11 12 13 14 15 System . out . p r i n t l n ( ” c a l l now setSomeInt with an i n t . . . ” ) ; // the f o l l o w i n g does ∗ not ∗ work i f c a l l e d without the c a s t ! ! // OverloadingExample . j a v a : 2 1 : R e f e r e n c e to setSomeInt i s // ambiguous . I t i s d e f i n e d i n void setSomeInt ( f l o a t ) and void // setSomeInt ( i n t ) . ( ( BaseClass ) obj ) . setSomeInt ( 1 2 ) ; 16 17 18 19 20 21 } 22 23 } Aufruf auf der Konsole: exttt¿java OverloadingExample Default constructor of BaseClass call now setSomeInt with float... setSomeInt(float) of OverloadingExample call now setSomeInt with an int... setSomeInt() of BaseClass In Zeile 4 wird die Methode setSomeInt via overloading der Klasse hinzugefügt. Interessant ist aber, dass nach der Definition dieser neuen Methode, die Methode setSomeInt(int...) aus der Basis-Klasse nicht mehr ohne expliziten cast des Objekts auf die Basis-Klasse aufgerufen werden kann!!! Die Erklärung dazu ist (relativ :)) einfach: int ist ein Sub-Typ von float: Nun kann der Compiler nicht entscheiden, welche Implementation eingesetzt werden soll, und es ergibt sich die gemeldete Mehrdeutigkeit. 4.10 Interface Wie im letzten Abschnitt erwähnt, gib es in Java nur eine einfache Ableitung. Es kann also nicht von mehreren Klassen abgeleitet werden. Diese Einschränkung ist problematisch, kann aber über sog. Interfaces teilweise umgangen werden. Ein Interface definiert einen Referenz-Datentyp, der einer abstrakten Klasse sehr ähnlich ist. Aber: KAPITEL 4. KLASSEN IN JAVA 66 • Es können nur static final Member-Variablen definiert werden. • Alle Methoden sind implizit abstrakt. • Ein Interface kann nicht instantiiert werden. Definition eines Interfaces: [ public ] interface TheInterfaceName [ extends A n o t h e r I n t e r f a c e ] Implizit sind alle Deklarationen innerhalb eines Interface’s public und abstract. Die Implementation des Interfaces erfolgt in einer Klasse, die via implements TheInterfaceName definiert wurde. Eine Klasse kann mehrere Interfaces implementieren. Sämtliche im Interface deklarierten Methoden sollten in der Klasse definiert werden. Passiert das nicht, muss die Klasse mit abstract als abstrakt definiert werden und eine Sub-Klasse muss die nicht definierten Methoden implementieren. Da alle Methoden abstrakt sind, kann es keine Default-Implementation für Methoden geben. Es kann also passieren, dass bei einem schlechten Klassendesign aufgrund der Einschränkung der einfachen Vererbung Interfaces verwendet werden müssen und dann jede Klasse explizit eine Methode in gleicher Weise implementieren muss. Sollte so etwas passieren: Zurück zum “Zeichentisch” und die Klassenstruktur redesignen. 4.11 Ein kurzer Einblick in Packages Klassen können in sog. Packages gruppiert werden. Gemeinsam mit dem Namen des Packages und dem Klassennamen ergibt sich ein full qualified classname, mit dem diese Klasse identifiziert werden kann. Das Schlüsselwort für die Gruppendefinition ist package. Wenn man die bisherigen Beispiele betrachtet, sieht man, dass hier dieses Schlüsselwort nicht verwendet wurde. Daraus ist ersichtlich, dass es auch “namenlose” Packages geben kann. In Projekten mit vielen Klassen und einzelnen Modulen ist es aber sinnvoll, die einzelnen Module (d.h. zusammengehörige Klassen) in Packages zu organisieren. Betrachten wir eine solche Gruppendefinition in einer Klasse: // some f i l e . . . package some package name ; public c l a s s SomeClassA { . . . } // some o t h e r f i l e . . . package some package name ; public c l a s s SomeClassB { . . . } Die Klasse someClassA hat nun den full qualified name: some package name.SomeClassA. Diese Klasse ist im selben package wie die Klasse some package name.SomeClassB. Will man Packages weiter unterteilen, so kann man Subpackages (getrennt durch ’.’) definieren. Z.B. package some package name . sub package ; public c l a s s SomeOtherClass { . . . } In diesem Fall wäre dann der volle Name der Klasse: some package name.sub package.SomeOtherClass. Es sollte diese Packagestruktur auch in der Verzeichnisstruktur, in der der Sourcecode abgelegt ist, KAPITEL 4. KLASSEN IN JAVA 67 widergespiegelt werden. Für unser Beispiel: Die Files SomeClassA.java und SomeClassB.java sollten in einem Verzeichnis some package name gespeichert werden, Wobei SomeOtherClass.java im Verzeichnis some_package_name/sub_ package abgelegt sein sollte. Diese Struktur ist nicht zwingend, jedoch ratsam, da der Bytecode (.class-Files) automatisch in der entsprechenden Verzeichnishierarchie abgelegt wird. Aus Gründen der Symmetrie ist das wünschenswert. Will man diese in Packages organisierten Klassen verwenden, kann man entweder beim Instantiieren den vollen Klassennamen angeben (obj = new some package name.SomeClassA();) oder das import Statement mit dem full qualified name der Klasse verwenden. import some package name . SomeClassA ; public c l a s s T e s t C l a s s { SomeClassA obj = new SomeClassA ( ) ; } Sollen mehrere Klassen aus einem Package eingebunden werden, so kann man auch import some package name.* (type-import-on-demand ) verwenden. Um den Überblick über die verwendeten Klassen zu behalten, sollte man jedes import-Statement explizit schreiben und auf den “*”, soweit sinnvoll, verzichten. Hinweis: Ein Package weiß nichts von eventuell vorhandenen Subpackages! D.h. ein import some package name.* stellt nur die im Package some_package_ name definierten Klassen zur Verfügung. Hinweis: Wird eine Klasse via import eingebunden, dann kann man Objekte dieser Klasse einfach über obj = new TheNameOfTheClass(); instantiieren. Es ist aber nicht möglich, direkt auf Member der Klasse zuzugreifen! Hinweis: Im Vergleich zu C++ wird in Java nicht bei der Ableitung festgelegt, welcher Zugriff (private, protected oder public) auf die Super-Klasse zugelassen wird. Es ist daher eine Gruppierung von Klassen in Packages absolut notwendig. Von Haus aus werden implizit alle Klassen vom Package java.lang eingebunden (import java.lang.*), d.h. die folgenden Klassen stehen in jedem Programm automatisch zur Verfügung (Java 1.4): AssertionError, Byte, Character, Class, ClassCircularityError, ClassLoader, ClassNotFoundException, Compiler, Double, Error, Exception, ExceptionInInitializerError, Float, IllegalAccessError, IllegalAccessException, IllegalThreadStateException, InheritableThreadLocal, Integer, Long, Math, NoSuchFieldException, Number, Object, Package, Process, Runtime, RuntimeException, RuntimePermission, SecurityException, Short, StackTraceElement, StrictMath, String, StringBuffer, System, ThreadDeath, ThreadLocal, Throwable, UnsupportedOperationException Kapitel 5 Fehlerbehandlung Für die Fehlerbehandlung eignen sich Exceptions außerordentlich gut. Seit Java 1.4 gibt es auch noch sog. Assertions zur Fehlererkennung, die in diesem Abschnitt jedoch nicht behandelt werden. 5.1 Exceptions Exceptions werden verwendet, um Fehler im Programmablauf zu melden. Methoden können in einem solchen Fall selbst darauf reagieren oder diese Exception einfach an den Aufrufenden weitergeben. try { // t r y something where e x c e p t i o n s may appear . . . } catch ( SubException ex ) { // catch some type o f e x c e p t i o n . . . } catch ( Exception ex ) { // catch a l l o t h e r e x c e p t i o n s . . . } finally { // some f i n a l cleanup . . . } Das Konzept ist ähnlich wie in C++: In einem try-Block werden die Operationen abgearbeitet, die eventuell Probleme bereiten könnten. Im Fehlerfall (=Exception wird von einer Methode “geworfen”) wird dieser try-Block verlassen und in den “erstbesten” catch-Block verzweigt. Der “erstbeste” ist jener Block, in dem der Typ der geworfenen Exception behandelt wird. Im Unterschied zu C++ gibt es in Java noch einen optionalen finally-Block, der auf alle Fälle ausgeführt wird, egal ob eine Exception behandelt wird oder nicht. In diesem finally sollten z.B. Files geschlossen werden. Der finallyBlock wird nur dann nicht ausgeführt, wenn zuvor ein System.exit(...); aufgerufen wurde. Vorsicht Falle: Sollte im try-Block ein Wert retourniert werden, muss man 68 KAPITEL 5. FEHLERBEHANDLUNG 69 darauf achten, dass im finally-Block nicht ebenfalls ein Wert retourniert wird, da dieser Wert dann dem Aufrufenden zurückgegeben wird... Ein weiterer Unterschied zu C++ existiert: Während in C++ beliebige Objekte via throw “weitergeworfen” werden können, muss in Java eine Exception vom Typ Throwable sein. In Java gibt es zwei Sub-Klassen von Throwable: Error und Exception. Errors weisen auf einen Fehler innerhalb der JVM hin und sollten unter normalen Umständen nicht vom Entwickler behandelt und verwendet werden. Exceptions können (und sollen!) behandelt werden. Bei den Exceptions gibt es noch eine weitere interessante Sub-Klasse, nämlich RuntimeException (z.B. NullPointerException, IndexOutOfBoundsException, etc.). RuntimeExceptions sollten nicht als Basis-Klasse von eigenen Exceptions dienen, da diese nicht explizit in einer Methode deklariert werden müssen. Betrachten wir ein einfaches Beispiel: 1 2 3 4 5 6 7 8 9 10 c l a s s MyException extends Exception { MyException ( ) { } MyException ( S t r i n g message ) { super ( message ) ; } } • MyException leitet sich von Exception ab und kann daher via throw verwendet werden. Für eine vernünftige Fehlerbehandlung ist es notwendig, eine eigene Exception-Hierarchie zu erzeugen. • Die wohl gängigste Methode bei der Behandlung von Exceptions für Debugzwecke ist die Methode printStackTrace(). Sie gibt den Status des Stacks aus. Diese Methode erleichtert enorm die Fehlersuche (siehe Bildschirmausgabe in ExceptionExample). 1 2 3 4 5 6 7 8 9 10 11 12 13 public c l a s s ExceptionExample { // t h i s method does not handle the e x c e p t i o n and simply // throws i t to the c a l l e r void someMethod ( int index ) throws MyException { System . out . p r i n t l n ( ” In someMethod ( index : ” + index + ” ) ” ) ; i f ( index > 3) { throw new MyException ( ” Index > 3! ” ) ; } } 14 15 16 17 18 19 20 21 22 // handle the e x c e p t i o n void someOtherMethod ( int index ) { System . out . p r i n t l n ( ” In someOtherMethod ( index : ” + index + ” ) ” ) ; try { i f ( index > 3) { KAPITEL 5. FEHLERBEHANDLUNG 70 throw new MyException ( ” Index > 3! ” ) ; 23 } 24 } catch ( MyException ex ) { System . out . p r i n t l n ( ”someOtherMethod : : caught MyException : ” + ex ) ; ex . p r i n t S t a c k T r a c e ( ) ; } catch ( Exception ex ) { System . out . p r i n t l n ( ”someOtherMethod : : caught e x c e p t i o n : ” + ex ) ; ex . p r i n t S t a c k T r a c e ( ) ; } finally { System . out . p r i n t l n ( ”someOtherMethod : : f i n a l l y −Block ” ) ; } 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 } 40 41 public s t a t i c void main ( S t r i n g [ ] argv ) { ExceptionExample e x c e p t i o n e x a m p l e = new ExceptionExample ( ) ; 42 43 44 45 System . out . p r i n t l n ( ” F i r s t t r y . . . ” ) ; try { for ( int index =0; index <6; index++) { e x c e p t i o n e x a m p l e . someMethod ( index ) ; } } catch ( MyException ex ) { System . out . p r i n t l n ( ”main : f i r s t t r y : caught e x c e p t i o n : ” + ex ) ; ex . p r i n t S t a c k T r a c e ( ) ; } catch ( Exception ex ) { System . out . p r i n t l n ( ”main : f i r s t t r y : caught some o t h e r e x c e p t i o n : ” + ex ) ; ex . p r i n t S t a c k T r a c e ( ) ; } finally { System . out . p r i n t l n ( ”main : f i r s t t r y : i n the f i n a l l y b l o c k ” ) ; } 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 System . out . p r i n t l n ( ”Second t r y . . . ” ) ; try { for ( int index =0; index <6; index++) { e x c e p t i o n e x a m p l e . someOtherMethod ( index ) ; } } catch ( Exception ex ) { System . out . p r i n t l n ( ”main : second t r y : caught e x c e p t i o n : ” + ex ) ; ex . p r i n t S t a c k T r a c e ( ) ; } finally { System . out . p r i n t l n ( ”main : second t r y : i n the f i n a l l y b l o c k ” ) ; } 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 } 86 87 } • someMethod erzeugt eine Exception, wenn der übergebene Parameter der Methode größer 3 ist. Bemerkenswert ist Zeile 6: Hier wird definiert, dass diese Methode eine Exeption werfen kann. Im Unterschied zu C++ ist eine Definition absolut notwendig, da sonst das Programm nicht compi- KAPITEL 5. FEHLERBEHANDLUNG 71 liert. Man sieht auf einen Blick, welche Exceptions auftreten können. RuntimeExcpetions müssen in der throws-Anweisung nicht deklariert werden. • someOtherMethod behandelt die auftretenden Exceptions selbst. Interessant ist hier, dass wirklich der finally-Block ausgeführt wird, unabhängig davon, ob eine Exception geworfen wird oder nicht. • In der main-Methode werden die oben definierten Methoden ausgeführt. Man kann beobachten, wie die Exception, die in someMethod geworfen wird, in main behandelt wird und dass danach sofort der try-Block (Zeilen 47-53) verlassen wird. • Die Methode someOtherMethod wird im Unterschied zu someMethod 6mal aufgerufen, da ja die Exception schon in der Methode selbst behandelt wird. Aufruf auf der Konsole: exttt¿java ExceptionExample First try... In someMethod (index: 0) In someMethod (index: 1) In someMethod (index: 2) In someMethod (index: 3) In someMethod (index: 4) main: first try: caught exception: MyException: Index > 3! MyException: Index > 3! at ExceptionExample.someMethod(ExceptionExample.java:11) at ExceptionExample.main(ExceptionExample.java:51) main: first try: in the finally block Second try... In someOtherMethod (index: 0) someOtherMethod::finally-Block In someOtherMethod (index: 1) someOtherMethod::finally-Block In someOtherMethod (index: 2) someOtherMethod::finally-Block In someOtherMethod (index: 3) someOtherMethod::finally-Block In someOtherMethod (index: 4) someOtherMethod::caught MyException: MyException: Index > 3! MyException: Index > 3! at ExceptionExample.someOtherMethod(ExceptionExample.java:23) at ExceptionExample.main(ExceptionExample.java:74) someOtherMethod::finally-Block In someOtherMethod (index: 5) someOtherMethod::caught MyException: MyException: Index > 3! MyException: Index > 3! at ExceptionExample.someOtherMethod(ExceptionExample.java:23) at ExceptionExample.main(ExceptionExample.java:74) KAPITEL 5. FEHLERBEHANDLUNG someOtherMethod::finally-Block main: second try: in the finally block 72 Kapitel 6 Swing Mit dem Swing-Package (javax.swing, mehr als 500 Klassen und Interfaces) kann man recht einfach graphische Benutzeroberflächen für Applikationen zusammenstellen. Da der Umfang dieser Library recht groß ist, beschränkt sich dieses Kapitel auf die Grundlagen. Damit wird es möglich sein, einfachste Programme zu schreiben. Eine genauere Beschreibung und Anwendung der einzelnen Klassen ist in [Java Tutorial 2002] bzw. in [Java SDK Doc 2002] zu finden. Swing gibt es seit Java 1.2 und ist im Package javax.swing zu finden. Es wird generell geraten, Swing für grafische Applikationen zu verwenden und nicht die “alten” AWT (“Abstract Windowing Toolkit”) Libraries. Auf keinen Fall sollten AWT- und Swing-Komponenten gleichzeitig miteinander in einem Programm verwendet werden. Die Java Foundation Classes (JFC) bilden den Grundstock für Benutzeroberflächen. Swing ist ein Bestandteil dieser JFC. Die Teile dieser Klassensammlung sind: Swing Komponenten: Eine Sammlung von Klassen, die die graphische Schnittstelle zwischen Bentuzer und Applikation ermöglicht. Pluggable Look and Feel: Es ist möglich, die Komponenten unterschiedlich darzustellen. Mit der Unterstützung von verschiedenen Look-and-Feels hat der Benutzer einfach die Möglichkeit Java-Applikationen seinen Gewohnheiten entsprechend auszuführen. Accessibility API: Graphische Bentuzeroberflächen sollen auch für alternative Anzeige-/Lesegeräten (z.B. Braille-Zeile) zugänglich sein. Java 2D API: Einfache 2D-Graphiken können mit dieser API erstellt werden. Drag and Drop Support: Drag and Drop zwischen Java- und anderen Applikationen kann mit dieser API implementiert werden. Im Rahmen dieser Veranstaltung betrachten wir aus dem JFC nur einen kleinen Teil der Swing-Klassen. 73 KAPITEL 6. SWING 6.1 74 Und wieder ein einfaches Beispiel Bevor wir uns auf einzelne Klassen stürzen, schauen wir uns das vermutlich einfachste Swing-Programm an. Es ist wichtig, die Konzepte zu verstehen, die in Swing verfolgt werden. Für die Umsetzung kann man auch einen GUI-Builder verwenden1 . 1 import javax . swing . ∗ ; 2 3 4 5 6 7 8 9 10 11 12 13 14 public c l a s s VerySimpleSwingExample { public s t a t i c void main ( S t r i n g [ ] a r g s ) { JFrame a frame = new JFrame ( ”The Frame” ) ; JLabel a l a b e l = new JLabel ( ” I ’m a very s i m p l e swing example . ” ) ; a frame . getContentPane ( ) . add ( a l a b e l ) ; a frame . s e t D e f a u l t C l o s e O p e r a t i o n ( JFrame . EXIT ON CLOSE ) ; a frame . pack ( ) ; a frame . s e t V i s i b l e ( true ) ; } } In diesen wenigen Zeilen werden viele Konzepte (einzelne Container beinhalten irgendwelche Komponenten...) von Swing verwendet. Betrachten wir die einzelnen Zeilen des Programms etwas genauer: import javax.swing.*: Damit die Klassen aus dem Swing-Package direkt im Programm verwendet werden können, muss das Package eingebunden werden. Wenn man noch Events behandeln will (was das ist, erfahren wir in Kürze...), benötigt man zusätzlich noch Klassen aus dem java.awt- bzw. java.awt.event-Package. Viele der Swing-Klassen basieren auf dem Abstract Windowing Toolkit (AWT). import java.awt.*; import java.awt.event.*; import javax.swing.*; new JFrame(...): Ein Frame ist ein Container, der mehrere unterschiedliche Komponenten beinhalten kann und diese auch darstellt. Jede SwingApplikation muss (zumindest) einen sog. top-level container verwenden (siehe JFrame, JDialog, für Applets noch JApplet). Unmittelbar nach dem Erzeugen ist ein Frame ’versteckt’. Man sollte daher nicht auf Zeile 11/12 vergessen :) new JLabel(...): Ein Label ist nun eine solche Komponente, die innerhalb des Frames dargestellt werden soll. Bevor wir das Label verwenden können, muss es selbstverständlich erzeugt werden. In der darauffolgenden Zeile wird dieses Label dem Frame (genauer einer ’Scheibe’ – pane – dieses Frames) hinzugefügt. 1 GUI-Builder sind in einigen IDEs – z.B. JBuilder, Sun ONE Studio (aka Forte) etc. – integriert. Mit diesen Werkzeugen wird es einfacher, einzelne Elemente zu positionieren...). Diese GUI-Builder erzeugen allerdings einen sehr schlechten Code! Man muss der generierten Code auf alle Fälle einem Review unterziehen! KAPITEL 6. SWING 75 a frame.setDefaultCloseOperation(...): Der Name dieser Methode sagt wohl alles... Seit Java 1.3 ist diese Vorgehensweise möglich. Für ältere Versionen muss ein EventListener (siehe später) implementiert werden. a frame.pack(): Letzte Vorbereitungen für die Darstellung (Berechnung der Fenstergröße aufgrund der Komponenten, die im Frame sind). a frame.setVisible(...): Frame wird nun dargestellt. Und so sieht das Ergebnis aus: Abbildung 6.1: Ein einfaches Swing-Beispiel So einfach ist das. Wie man aber schon bei diesem einfachen Programm sieht, ist es notwendig, einige Eigenschaften von Swing zu kennen (z.B. das Frame per default nicht sichtbar sind...), ansonsten sucht man verzweifelt nach dem Fehler. 6.2 Die Model-View-Controller Architektur Im Zusammenhang mit grafischen Oberflächen fällt immer wieder der Begriff MVC-Architektur. In diesem Abschnitt betrachten wir kurz die Eigenschaften dieses Ansatzes. Model : Hier wird die interne Datenrepräsentation einer Komponente (siehe nächster Abschnitt) verstanden. Wenn man ein Texteingabefeld betrachtet, ist das dahinterstehende Modell die Zeichenkette, die eingegeben wurde. Für einen Button, der gedrückt werden kann, ist das Datenmodell der Status, ob er gedrückt wurde oder nicht usw. Wichtig ist hier zu erkennen, dass ein Model auch ohne visueller Komponente existieren kann. View : Das ist die visuelle Komponente, die das Modell nach “außen hin” visualisiert. Auch hier soll auf die Abkopplung zum internen Datenmodell hingewiesen werden. Ob ein Textfeld (JTextField) runde Kanten hat oder nicht, oder ob ein Fenster die Titelzeile zentriert oder linkbündig zeichnet, ist für die interne Repräsentation vollkommen uninteressant2 . Controller : Dieser Teil der Architektur behandelt die Aktionen (events), die mit der Komponente in Verbindung gebracht werden sollen. Was soll z.B. passieren, wenn man einen bestimmten Button drückt, oder wenn man ein Feld ausgefüllt hat? Betrachtet man die einzelnen Aufgaben der Teile, dann sieht man folgenden Zusammenhang: 2 Applikationen, die auf unterschiedlichen Plattformen gestartet werden können, sind hier ein sehr offensichtliches Beispiel für unterschiedliche “Views” von ein und derselben Komponente. KAPITEL 6. SWING 76 • Das Model gibt Informationen an den View zur Darstellung weiter. • Der3 View bestimmt, welche Aktionen an den Controller weitergegeben werden können. • Der Controller ist dafür zuständig, dass das interne Datenmodell wieder auf den aktuellen Stand gebracht wird (das also z.B. die Zeichenkette, die in einem Textfeld geändert wurde, auch im Model aktualisiert wird. Die Komponenten von Swing sind fast nach dieser Architektur aufgebaut. Es wurde eine stärkere Kopplung zwischen dem View- und Controller-Element implementiert, damit es einfacher wird, die Komponenten zu bedienen. Auch für diesen Ansatz gibt’s einen Namen: model-delegate. Jede Komponente in Swing hat ein Model und einensiehe letzte Fußnote... Delegate (genauer einen GUI-Delegate). Das Model ist für die interne Repräsentation zuständig. Der Delegate ist für die Visualisierung der Komponente sowie für die Events, die diese Komponente auslösen kann zuständig. Für den Entwickler bieten sich durch diese Architektur viele Vorteile. Es ist bspw. möglich, unterschiedliche Views an ein Model zu binden, d.h. bestimmte Daten können entweder als Tabelle oder als Diagramm dargestellt werden. Die Daten werden nur an einer Stelle verwaltet (d.h. geändert) und die jeweiligen Views stellen automatisch die Daten richtig dar, ohne, dass man besondere Aktionen setzen muss. Für die ersten Schritte sollte die obige Erklärung ausreichend sein. Durch Beispiele, die in der Folge noch betrachtet werden, wird in wenigen Abschnitten das Konzept von Swing verinnerlicht und es sollte kein Problem mehr sein, bisher unbekannte Komponenten in ein Programm einzusetzen. 6.3 Container und Komponenten Container sind – wie schon angedeutet – Klassen, die einzelne Komponenten halten. Da Container auch Komponenten sind, können Container selbst wieder in Container gesteckt werden. Es gibt eine besondere Gattung von Container, die nicht wieder in Container positioniert werden können: sog. Top-level Container (JFrame, JDialog und JApplet). Versucht man es trotzdem, einen solchen Top-level Container in einen Container zu stecken, wird eine Exception geworfen (“java.lang.IllegalArgumentException: adding a window to a container”). In Abbildung 6.2 ist zu sehen, wie ein JFrame aufgebaut ist: Ein Top-level Container stellt eine spezielle content pane (dt. “Scheibe”) zur Verfügung, die via getContentPane() angefordert werden kann. Auf dieser 3 Der/die/das View. Hier kann der Leser entscheiden, welcher Artikel einzusetzen ist. Je nachdem, wie man view übersetzt: In diesem Zusammenhang sehe ich view als “der Darstellungteil des Modells”, es kann aber “die Darstellungskomponente des Modells”, oder (für die Puristen) einfach “das View-Element” heißen. Alle Germanisten mögen mir verzeih’n. Wichtig für mich ist, dass die Nachricht, die ich weitergeben will, richtig ankommt... KAPITEL 6. SWING 77 Abbildung 6.2: Container mit ContentPane content pane wird der Inhalt (also die einzelnen Komponenten, die diesem Container hinzugefügt wurden) gezeichnet4 . Der einfachste dieser Panes ist JPanel, dann gibt es noch JScrollPane (hier werden automatisch bei Bedarf Scrollbars zur Verfügung gestellt), JTabbedPane (eine Pane als “Karteikarte”) und einige andere. 6.4 Layoutmanager Unser erstes Beispiel war relativ einfach: Wir haben einen frame erzeugt, von diesem die content pane geholt und eine Komponente (ein JLabel) hinzugefügt. Sollen nun mehrere Komponenten in einen Container positioniert werden, muss ein Layoutmanager die Komponenten in die “richtige” Position bringen. Dieser Layoutmanager wird via setLayout(...) einer bestimmten Pane zugewiesen. Einer Komponente kann man via setMinimumSize, setPreferredSize und setMaximumSize (letzteres wird nur vom BoxLayout berücksichtig) in der Größe beeinflussen. Man sollte jedoch wissen, dass nicht alle Größenangaben bei jedem Typ von Komponente berücksichtigt werden...5 Zwischen den einzelnen Komponenten im Container kann über unterschiedlichste Mechanismen ein Freiraum definiert werden. Man kann über den verwendeten Layoutmanager diese Ränder erzwingen, versteckte Komponenten oder spezielle Border-Objekte verwenden. Was ist nun die “richtige Position” eines Elements? Hier entscheidet der Layoutmanager, wo die Elemente am Bildschirm dargestellt werden sollen. Es gibt folgende Layoutmanager in Swing: BorderLayout: Die Bereiche werden in Norden, Süden, Osten, Westen und genau in der Mitte (BorderLayout.NORTH, -.SOUTH, -.EAST, -.WEST bzw. 4 Wenn man versucht, mehrere Komponenten einer content pane hinzuzufügen, wird jeweils nur die letzte gezeichnet. Will man daher mit mehreren Komponenten arbeiten, muss man diese in Gruppen via panes organisieren bzw. einen Layoutmanager verwenden! 5 So wird z.B. die Angabe von setMinimumSize bei einem JLabel ignoriert, eine preferred size wird jedoch richtig ausgewertet... KAPITEL 6. SWING 78 Abbildung 6.3: Unterschiedliche Layoutmanager in Aktion... -.CENTER) unterteilt. Vorsicht: Es darf immer nur eine Komponente in einem Bereich liegen! Container c o nt en t pa ne = getContentPane ( ) ; c o nt e nt pane . setLayout (new BorderLayout ( ) ) ; c o nt e nt pane . add (new JButton ( ”Button 1 ” ) , BorderLayout .NORTH) ; c o nt en t pane . add (new JButton ( ”2” ) , BorderLayout .CENTER) ; ... Man kann via setHgap(...) und setVgap(...) die Ränder (in Pixel) zwischen den Bereichen anpassen bzw. im Konstruktor die Ränder definieren (siehe auch Sun-Tutorial6 ). GridLayout: Hier wird der gesamte Bereich in gleich große Zellen geteilt. Wird im Konstruktor ein Parameter mit 0 übergeben, so wird dieser Wert automatisch (und dynamisch) festgelegt. Im folgenden Beispiel wird ein GridLayout mit einer undefinierten Anzahl von Zeilen und genau zwei Spalten definiert. Container c o nt en t p a ne = getContentPane ( ) ; c o nt e nt pane . setLayout (new GridLayout ( 0 , 2 ) ) ; c o nt en t pane . add (new JButton ( ”Button 1 ” ) ) ; ... Auch in diesem Fall kann noch ein Rand zwischen den einzelnen Zellen definiert werden (entweder über setHgap(...) bzw. setVgap(...) und auch über den Konstruktor). Außerdem kann noch festgelegt werden, in welcher Reihenfolge die Zellen aufgefüllt werden – im bzw. gegen den Uhrzeigersinn (siehe auch Sun-Tutorial7 ). FlowLayout: Bei diesem Layout werden die Komponenten einfach nacheinander visualisiert. Bei Bedarf wird eine neue Zeile eingefügt. Die Ausrichtung der einzelnen Komponenten kann über den Konstruktor bzw. eine Methode setAlignment() festgelegt werden (FlowLayout.LEFT, -.RIGHT, -.CENTER, -.LEADING, -.TRAILING, siehe auch Sun-Tutorial8 ). 6 http://java.sun.com/docs/books/tutorial//uiswing/layout/border.html 7 http://java.sun.com/docs/books/tutorial//uiswing/layout/grid.html 8 http://java.sun.com/docs/books/tutorial//uiswing/layout/flow.html KAPITEL 6. SWING 79 Container c o nt e nt p a ne = getContentPane ( ) ; c o nt e nt pane . setLayout (new FlowLayout ( FlowLayout .RIGHT) ) ; c o nt en t pane . add (new JButton ( ”Button 1 ” ) ) ; ... BoxLayout: Die Komponenten werden in einer einzelnen Zeile oder einer einzelnen Spalte dargestellt, wobei auch hier die Ausrichtung der einzelnen Komponenten festgelegt werden kann (siehe auch Sun-Tutorial9 ). Container co nt e nt pa ne = getContentPane ( ) ; c o nt e nt pane . setLayout (new BoxLayout ( content pane , BoxLayout . Y AXIS ) ) ; addAButton ( ”Button 1 ” , co nt ent pa n e ) ; In der Methode addAButton kann definiert werden, wie das einzelne Element in dem Container ausgerichtet werden soll (siehe Sourcecode von BoxWindow.java). GridBagLayout: Dieser Manager ist wohl der mächtigste der hier vorgestellten. Die Komponenten werden in Zeilen und Spalten positioniert, wobei sie über mehrere Zeilen und Spalten gehen können. Die Zeilen und Spalten können unterschiedliche Höhen und Breiten haben. Ein Beispiel sagt vielleicht mehr als 1000 Worte. Auch in diesem Fall wird absichtlich nicht exakt auf alle Methoden und Parameter dieses Layoutmanagers eingegangen, da dies den Rahmen dieser Einführung bei weitem sprengen würde (siehe auch Sun-Tutorial10 ). 1 2 3 import j a v a . awt . ∗ ; import j a v a . awt . event . ∗ ; import javax . swing . ∗ ; 4 5 6 7 8 public c l a s s GridBagWindow extends JFrame { f i n a l boolean SHOULD FILL = true ; f i n a l boolean SHOULD WEIGHTX = true ; 9 10 11 12 13 14 15 16 17 18 19 20 public GridBagWindow ( ) { JButton button ; Container c o nt ent pa ne = getContentPane ( ) ; GridBagLayout g b l a y o u t = new GridBagLayout ( ) ; GridBagConstraints g b c o n s t r a i n t s = new GridBagConstraints ( ) ; c o nt en t pane . setLayout ( g b l a y o u t ) ; i f ( SHOULD FILL) { g b c o n s t r a i n t s . f i l l = GridBagConstraints .HORIZONTAL; } 21 22 23 24 25 26 27 28 29 30 button = new JButton ( ”Button 1 ” ) ; i f (SHOULD WEIGHTX) { g b c o n s t r a i n t s . weightx = 0 . 5 ; } gb constraints . gridx = 0; gb constraints . gridy = 0; g b l a y o u t . s e t C o n s t r a i n t s ( button , g b c o n s t r a i n t s ) ; co ntent pane . add ( button ) ; 31 32 33 34 35 button = new JButton ( ”2” ) ; gb constraints . gridx = 1; gb constraints . gridy = 0; g b l a y o u t . s e t C o n s t r a i n t s ( button , g b c o n s t r a i n t s ) ; 9 http://java.sun.com/docs/books/tutorial//uiswing/layout/box.html 10 http://java.sun.com/docs/books/tutorial//uiswing/layout/gridbag.html KAPITEL 6. SWING 80 c o nt ent pane . add ( button ) ; 36 37 button = new JButton ( ”Button 3 ” ) ; gb constraints . gridx = 2; gb constraints . gridy = 0; g b l a y o u t . s e t C o n s t r a i n t s ( button , g b c o n s t r a i n t s ) ; co ntent pane . add ( button ) ; 38 39 40 41 42 43 button = new JButton ( ”Long−Named Button 4 ” ) ; g b c o n s t r a i n t s . ipady = 4 0 ; //make t h i s component t a l l g b c o n s t r a i n t s . weightx = 0 . 0 ; g b c o n s t r a i n t s . gridwidth = 3; gb constraints . gridx = 0; gb constraints . gridy = 1; g b l a y o u t . s e t C o n s t r a i n t s ( button , g b c o n s t r a i n t s ) ; co ntent pane . add ( button ) ; 44 45 46 47 48 49 50 51 52 button = new JButton ( ”Button 5 ” ) ; g b c o n s t r a i n t s . ipady = 0 ; // r e s e t to d e f a u l t g b c o n s t r a i n t s . weighty = 1 . 0 ; // r e q u e s t any e x t r a v e r t i c a l space g b c o n s t r a i n t s . anchor = GridBagConstraints .SOUTH; //bottom o f space g b c o n s t r a i n t s . i n s e t s = new I n s e t s ( 1 0 , 0 , 0 , 0 ) ; // top padding gb constraints . gridx = 1; // a l i g n e d with button 2 g b c o n s t r a i n t s . gridwidth = 2 ; // 2 columns wide gb constraints . gridy = 2; // t h i r d row g b l a y o u t . s e t C o n s t r a i n t s ( button , g b c o n s t r a i n t s ) ; co ntent pane . add ( button ) ; 53 54 55 56 57 58 59 60 61 62 63 addWindowListener (new WindowAdapter ( ) { public void windowClosing ( WindowEvent e ) { System . e x i t ( 0 ) ; } }); 64 65 66 67 68 69 70 } 71 72 public s t a t i c void main ( S t r i n g [ ] a r g s ) { GridBagWindow window = new GridBagWindow ( ) ; 73 74 75 76 window . s e t T i t l e ( ”GridBagLayout” ) ; window . pack ( ) ; window . s e t V i s i b l e ( true ) ; 77 78 79 } 80 81 } Fortgeschrittene Programmierer haben auch die Möglichkeit, selbst einen Layoutmanager zu schreiben. Will man in einem Programm die Elemente absolut positionieren, dann muss man den Layoutmanager explizit auf null setzen und die paint()-Methode von Pane überschreiben. Die Absolutpositionierung hat viele Nachteile (z.B. hat die Änderung der Fenstergröße keine Auswirkung auf das Layout...) und sollte nur in Ausnahmefällen verwendet werden. 6.5 Ein weiteres Beispiel In den letzten Beispielen wurde nicht viel getan. Die Interaktion mit dem Benutzer hielt sich in Grenzen. Betrachten wir aus diesem Grund nun ein Beispiel, bei dem der Benutzer das Programm steuern kann. 1 2 3 import j a v a . awt . ∗ ; import j a v a . awt . event . ∗ ; import javax . swing . ∗ ; KAPITEL 6. SWING 81 4 5 6 7 8 public c l a s s SimpleSwingApplication { s t a t i c f i n a l S t r i n g A LABEL PREFIX = ”Number o f button c l i c k s : ” ; int n u m c l i c k s = 0 ; 9 public Component createComponents ( ) { f i n a l JLabel a l a b e l = new JLabel (A LABEL PREFIX + ” 0 JPanel a pane = new JPanel ( ) ; 10 11 12 13 ”); 14 JButton a button = new JButton ( ” I ’m a Swing button ! ” ) ; a button . setMnemonic ( KeyEvent . VK I ) ; a button . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) { public void actionPerformed ( ActionEvent e ) { n u m c l i c k s ++; a l a b e l . setText (A LABEL PREFIX + n u m c l i c k s ) ; } }); 15 16 17 18 19 20 21 22 23 24 25 a a a a 26 27 28 29 pane . s e t B o r d e r ( BorderFactory . createEmptyBorder ( 3 0 , 3 0 , 1 0 , 3 0 ) ) ; pane . setLayout (new GridLayout ( 0 , 1 ) ) ; pane . add ( a button ) ; pane . add ( a l a b e l ) ; 30 return a pane ; 31 } 32 33 public s t a t i c void main ( S t r i n g [ ] a r g s ) { // Create the top−l e v e l c o n t a i n e r and add c o n t e n t s to i t . JFrame a frame = new JFrame ( ” SimpleSwingApplication ” ) ; SimpleSwingApplication app = new SimpleSwingApplication ( ) ; Component c o n t e n t s = app . createComponents ( ) ; a frame . getContentPane ( ) . add ( co nt ents , BorderLayout .CENTER) ; 34 35 36 37 38 39 40 41 a frame . s e t D e f a u l t C l o s e O p e r a t i o n ( JFrame . EXIT ON CLOSE ) ; a frame . pack ( ) ; a frame . s e t V i s i b l e ( true ) ; 42 43 44 } 45 46 } Abbildung 6.4: SimpleSwingApplication in Aktion... Nun einige Kommentare zum Programm: Wie man in der Abbildung sieht, wird ein Button erzeugt und dargestellt. Es wird weiters mitgezählt, wie oft der Button gedrückt worden ist. Das Ergebnis wird in einem Label dargestellt. main(...): In der main-Methode wird ein Frame erzeugt (Zeile 37) und die Komponenten der Applikation (Button und Label) in die Mitte des Containers positioniert (Zeile 39). Danach wird noch definiert, was passieren soll, wenn das Fenster geschlossen wird (setDefaultCloseOperation(...)), KAPITEL 6. SWING 82 das Frame via pack() auf die Visualisierung vorbereitet und schließlich zum Zeichnen freigegeben (setVisible(...)). Danach wird das Fenster mit den Komponenten am Bildschirm von event-handle thread gezeichnet und auf Benutzereingaben gewartet. createComponents(): Diese Methode erzeugt und retourniert die einzelne Komponenten der Applikation und platziert sie auf einem JPanel. Das Label: Im Label unter dem Button wird angezeigt, wie oft dieser gedrückt wurde. Nachdem das Label auch in der anonymen Klasse (in Zeile 17-24) verwendet wird, muss es in Zeile 12 mit dem Modifier final deklariert werden. Macht man das nicht, kommt eine Fehlermeldung: “local variable label is accessed from within inner class; needs to be declared final”. Die Leerzeichen hinter dem Label erzwingen ein breiteres Frame11 . Der Button: Der Button wird wie gewohnt via new JButton(...); erzeugt. Zeile 16 definiert den Shortcut für den Button (Alt-i) bzw. Zeile 17 assoziiert einen sog. ActionListener mit dem Button. Hier sehen wir die Anwendung von Anonymen-Klassen. Diese spezielle Klasse – eine Implementation des ActionListener-Interfaces – wird nur in diesem Zusammenhang verwendet, daher ist dieser Ansatz hier vertretbar. Das ActionListener-Interface verlangt, dass genau eine Methode implementiert werden muss, nämlich die Methode actionPerformed. ActionListener: Der Listener beobachtet alle Actions, die der Button produzieren kann. Wird der Button via Shortcut aktiviert oder mit der linken Maustaste gedrückt, wird die Methode actionPerformed(...) ausgeführt. Über den Methodenparameter e (dieser ist vom Typ ActionEvent) könnte man noch explizit abfragen von welchem Typ der Event ist. Man könnte also unterschiedlich auf Events von Maus und Tastatur reagieren, was man aber hier im speziellen Fall nicht machen will. In der actionPerformed-Methode des Action-Listeners wird die Anzahl der Clicks erhöht (Instanzvariable!) und das Label mit dem neuen Text versehen. Bitte beachte, dass Swing automatisch erkennt, dass das Label neu zu zeichen ist. Dieses Beispiel zeigt, wie einfach es ist, Standardkomponenten (wie in diesem Fall Button und Label) zu verwenden und auftretende Events zu behandeln. Von Komponenten können unterschiedliche Events “gefeuert” werden. 6.6 Event-Handling Einzelne Komponenten und Gruppen von Komponenten können Events (also “Ereignisse”) erzeugen. In diesem Zusammenhang spricht man sehr häufig auch von Events feuern. Damit diese Events nicht einfach im Nirgendwo verschwinden, kann man sog. Event-Listener schreiben, die mit solchen Ereignissen umgehen können. Ein Event-Listener muss zuerst bei der Komponente angemeldet werden, damit er über das Auftreten von Events benachrichtigt wird. 11 Damit braucht man bei mehr als 9-clicks das Frame nicht zu vergrößern. Trickreich, oder? KAPITEL 6. SWING 6.6.1 83 Event-Listener Interface und Adapter Es gibt zwei Methoden, wie man Event-Listener implementieren kann: Entweder man implementiert ein bestimmtes Interface (...Listener.java) oder man leitet von einer bestimmten Klasse ab, die das Interface bereits implementiert hat. Diese Klassen heißen Adapter-Klassen (...Adapter.java). Listener deklarieren Methoden, die in den jeweiligen konkreten Ausprägungen implementiert werden müssen. Einfache Event-Listener mit wenigen Methoden können auf diesen Weg sehr elegant umgesetzt werden. Es muss lediglich das entsprechende Interface (richtig) implementiert werden. Ein Event-Listener kann im Prinzip von jeder Klasse implementiert werden, es muss lediglich ein entsprechendes Interface implementiert bzw. von einem bestimmten Adapter abgeleitet werden. Abhängig vom implementierten Interface werden dem Listener unterschiedliche Events zugestellt und die entsprechenden Event-Listener Methoden aufgerufen. Bei komplizierteren Events arbeitet man besser mit den Adapter-Klassen. AdapterKlassen bieten bereits Default-Implementationen der Methoden. Der konkrete Listener wird einfach vom Adapter abgeleitet und die notwendigen Methoden werden overrided. Ob nun ein Listener-Interface implementiert, oder von einer Adapter-Klasse abgeleitet wird, ist dem Entwickler freigestellt. Wenn man z.B. einen MouseListener betrachtet, sieht man sofort, dass man 5 Methoden implementieren muss: 1 2 3 4 5 6 7 8 9 package j a v a . awt . event ; ... public interface MouseListener extends E v e n t L i s t e n e r { /∗∗ ∗ Invoked when the mouse button has been c l i c k e d ( p r e s s e d ∗ and r e l e a s e d ) on a component . ∗/ public void mouseClicked ( MouseEvent e ) ; 10 /∗∗ ∗ Invoked when a mouse button has been p r e s s e d on a component . ∗/ public void mousePressed ( MouseEvent e ) ; 11 12 13 14 15 /∗∗ ∗ Invoked when a mouse button has been r e l e a s e d on a component . ∗/ public void mouseReleased ( MouseEvent e ) ; 16 17 18 19 20 /∗∗ ∗ Invoked when the mouse e n t e r s a component . ∗/ public void mouseEntered ( MouseEvent e ) ; 21 22 23 24 25 /∗∗ ∗ Invoked when the mouse e x i t s a component . ∗/ public void mouseExited ( MouseEvent e ) ; 26 27 28 29 30 } Will man in einem Programm beispielsweise nur den mousePressed()-Event behandeln, muss man leere Implementationen für die restlichen 4 Methoden zur Verfügung stellen. In einem solchen Fall sollte man den MouseAdapter KAPITEL 6. SWING 84 verwenden und von diesem ableiten. Nun noch zwei kurze Beispiele, die die Implementation des mouseClickedEvents mit Adapter-Klassen skizzieren. Im ersten Beispiel wird eine inner class “MyAdapter” verwendet. Hier wird eine Instanz dieser Klasse dem EventListener (in diesem Fall ist es ein spezieller “Mouse-Event-Listener”) gegeben. Im zweiten Beispiel wird eine anonyme Klasse verwendet, die vom MouseAdapter abgeleitet ist und eine Methode (mouseClicked) overrided. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 //An example o f u s i n g an i n n e r c l a s s . public c l a s s MyClass { ... s o m e o b j e c t . addMouseListener (new MyAdapter ( ) ) ; ... c l a s s MyAdapter extends MouseAdapter { public void mouseClicked ( MouseEvent event ) { . . . // Event ha n dl e r implementation g oes h e r e . . . } } } 15 16 17 18 19 20 21 22 23 24 25 26 27 28 //An example o f u s i n g an anonymous i n n e r c l a s s . public c l a s s MyClass { ... s o m e o b j e c t . addMouseListener (new MouseAdapter ( ) { public void mouseClicked ( MouseEvent event ) { . . . // Event ha n dl e r implementation g oes h e r e . . . } }); ... } Wie so oft bei der Entwicklung von Systemen gibt es hier kein Kochrezept, wie die Events gefangen und behandelt werden müssen. Es bleibt dem Entwickler überlassen, eine sinnvolle Implementation zu schreiben. Vorsicht Falle: Für die Implementation von Event-Listenern werden häufig Anonyme-Klassen verwendet. Vor allem für größere Projekte kann es so sehr schnell unübersichtlich werden. 6.6.2 Event-Listener Methoden Der Code, der ausgeführt werden soll, sobald ein passender Event eintritt (EventListener Methode), wird in einem einzigen Thread (event-dispatching thread ) ausgeführt. Daher werden Events sequenziell abgearbeitet. Dieses Verhalten macht durchaus Sinn! Nehmen wir einen Button als Beispiel: Ein Button wird vom Benutzer gedrückt oder nicht gedrückt. Die Aktion, die “hinter” diesem Button steckt soll demnach auch komplett ausgeführt werden, wenn er gedrückt wurde, oder nicht. Ein Button kann nicht “halb gedrückt” werden... Vorsicht Falle: Die Implementation einer Aktion sollte nicht zu umfangreich KAPITEL 6. SWING 85 sein, da diese Aktionen nur in einem Thread abgearbeitet werden. Nachdem zusätzlich in diesem Thread (event-dispatching thread ) auch noch die Komponenten gezeichnet werden, kann bei langsamer Ausführung der Bildschirm für einige Zeit “einfrieren”. In diesem Fall sollte man die Aktion in einem separaten Thread ausführen und sich selbst um mögliche Synchronisationsprobleme kümmern! Informationen zu einem Event (z.B. welches Objekt hat diesen Event ausgelöst, wann wurde der Event gefeuert, etc.) werden vom System einer Event-Listener Methode als Parameter übergeben (z.B. MouseEvent für Events, die von einer Maus ausgelöst werden; diese Klasse ist – wie alle Event-Klassen – eine SubKlasse von EventObject). In EventObject gibt es die Methode getSource(), die die Quelle des Events retourniert. Da getSource() ein Objekt vom Typ Object returniert, sollte man auf den expliziten Cast zum gewünschten Typ nicht vergessen... 6.6.3 Kurzer Überblick über konkrete Events in Swing Betrachtet man die Events, die innerhalb eines GUI-Systems auftreten können, bemerkt man schnell, dass es unterschiedliche Arten von Events gibt: sog. LowLevel- und Semantic-Events. In die erste Kategorie fallen alle Basis-Events eines Fenstersystems (z.B. die Maus hat sich von x/y auf x1/y1 bewegt und die Maustaste wurde dabei nicht gedrückt...), in die zweite Kategorie fallen Events, die bereits eine konkrete Bedeutung haben (z.B. die linke-Maustaste wurde auf einem Button gedrückt). Es ist offensichtlich, dass unterschiedliche Komponenten, unterschiedliche semantische Events erzeugen können. Es folgt nun ein kurzer Überblick über einige Event-Listener und -Adapter. Beginnen wir mit einer Liste von Listener, die von allen Swing-Komponenten verstanden werden: ComponentListener/Adapter: Dieser Listener wird mit addComponentListener(...) einer Komponente hinzugefügt. Methoden: • void componentShown(ComponentEvent) • void componentHidden(ComponentEvent) • void componentMoved(ComponentEvent) • void componentResized(ComponentEvent) Der Parameter dieser Methoden ist vom Typ ComponentEvent, der eine Methode getComponent() zur Verfügung stellt (siehe auch Sun-Tutorial12 ). FocusListener/Adapter: Mit addFocusListener(...) wird dieser Listener einer Komponente hinzugefügt. Event wird gefeuert, wenn eine Komponente den Fokus für Keyboard-Input erlang bzw. verliert. Methoden: • void focusGained(FocusEvent) • void focusLost(FocusEvent) 12 http://java.sun.com/docs/books/tutorial//uiswing/events/componentlistener. html KAPITEL 6. SWING 86 Der Parameter dieser zwei Methoden ist vom Typ FocusEvent. Via getComponent() bekommt man wieder die Komponente, die diesen Event verursacht hat (siehe auch Sun-Tutorial13 ). KeyListener/Adapter: Wenn Tasten am Keyboard gedrückt werden, werden diesem Listener Events zugestellt, wobei nur jene Komponente diese Events feuert, die den aktuellen Keyboard-Fokus hat. Mit addKeyListener(...) kann dieser Event-Listener einer Komponente hinzugefügt werden. • void keyTyped(KeyEvent) • void keyPressed(KeyEvent) • void keyReleased(KeyEvent) Die wichtigsten Methoden von KeyEvent sind getKeyChar(), getKeyCode() bzw. getKeyText(). Key-Events sind – wie die folgenden Mouse-Events auch – von der Klasse InputEvent abgeleitet. Wenn man diese Events nicht an alle Komponenten weitergeben will, kann man sie auch “verbrauchen” (mit consume()). Von InputEvent werden auch einige interessante Methoden geerbt (getComponent(), isAltDown(), isControlDown(), isMetaDown(), isShiftDown(), siehe auch Sun-Tutorial14 ). MouseListener/Adapter: Kann via addMouseListener(...) einer Komponente hinzugefügt werden. Methoden: • void mouseClicked(MouseEvent) • void mousePressed(MouseEvent) • void mouseReleased(MouseEvent) • void mouseEntered(MouseEvent) • void mouseExited(MouseEvent) Die wichtigsten Methoden von MouseEvent sind getX(), getY(), getClickCount() und getButton(). 1 2 3 import j a v a . awt . ∗ ; import j a v a . awt . event . ∗ ; import javax . swing . ∗ ; 4 5 6 7 8 9 10 public c l a s s MouseEventDemo extends MouseAdapter { public void mousePressed ( MouseEvent event ) { System . out . p r i n t l n ( ” In mousePressed : ” ) ; int m o d i f i e r s = event . g e t M o d i f i e r s ( ) ; 11 12 13 14 15 16 17 18 System . out . p r i n t l n ( ” L e f t button ” + ( ( ( m o d i f i e r s & InputEvent .BUTTON1 MASK) ! = 0 ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; System . out . p r i n t l n ( ” Center button ” + ( ( ( m o d i f i e r s & InputEvent .BUTTON2 MASK) ! = 0 ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; System . out . p r i n t l n ( ” Right button ” + 13 http://java.sun.com/docs/books/tutorial//uiswing/events/focuslistener.html 14 http://java.sun.com/docs/books/tutorial//uiswing/events/keylistener.html KAPITEL 6. SWING 87 ( ( ( m o d i f i e r s & InputEvent .BUTTON3 MASK) ! = 0 ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; System . out . p r i n t l n ( ” S h i f t i s ” + ( event . isShiftDown ( ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; System . out . p r i n t l n ( ” Control i s ” + ( event . isControlDown ( ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; System . out . p r i n t l n ( ” Alt i s ” + ( event . isAltDown ( ) ? ”” : ” not ” ) + ” p r e s s e d ” ) ; 19 20 21 22 23 24 25 26 } 27 28 public s t a t i c void main ( S t r i n g [ ] a r g s ) { JFrame t h e f r a m e = new JFrame ( ”MouseEventDemo” ) ; t h e f r a m e . addMouseListener (new MouseEventDemo ( ) ) ; t h e f r a m e . s e t D e f a u l t C l o s e O p e r a t i o n ( JFrame . EXIT ON CLOSE ) ; the frame . s e t S i z e ( 4 0 0 , 3 0 0 ) ; t h e f r a m e . s e t V i s i b l e ( true ) ; } 29 30 31 32 33 34 35 36 37 } siehe auch Sun-Tutorial15 . MouseMotionListener/Adapter: Kann via addMouseMotionListener(...) einer Komponente hinzugefügt werden. Methoden: • void mouseDragged(MouseEvent) • void mouseMoved(MouseEvent) 1 2 3 import j a v a . awt . ∗ ; import j a v a . awt . event . ∗ ; import javax . swing . ∗ ; 4 5 6 7 8 9 10 11 12 public c l a s s MouseMotionEventDemo implements MouseMotionListener { public void mouseMoved ( MouseEvent event ) { System . out . p r i n t l n ( ” In mouseMoved : ” + event . getX ( ) + ”/” + event . getY ( ) ) ; System . out . p r i n t l n ( event . g e t S o u r c e ( ) ) ; } 13 public void mouseDragged ( MouseEvent event ) { System . out . p r i n t l n ( ” In mouseDragged : ” + event . getX ( ) + ”/” + event . getY ( ) ) ; } 14 15 16 17 18 19 public s t a t i c void main ( S t r i n g [ ] a r g s ) { JFrame t h e f r a m e = new JFrame ( ”MouseMotionEventDemo” ) ; t h e f r a m e . addMouseMotionListener (new MouseMotionEventDemo ( ) ) ; t h e f r a m e . s e t D e f a u l t C l o s e O p e r a t i o n ( JFrame . EXIT ON CLOSE ) ; the frame . s e t S i z e ( 4 0 0 , 3 0 0 ) ; t h e f r a m e . s e t V i s i b l e ( true ) ; } 20 21 22 23 24 25 26 27 28 } (siehe auch Sun-Tutorial16 ). 15 http://java.sun.com/docs/books/tutorial//uiswing/events/mouselistener.html 16 http://java.sun.com/docs/books/tutorial//uiswing/events/mousemotionlistener. html KAPITEL 6. SWING 88 JComponent unterstützt noch weitere Typen von Listenern, die uns im Moment aber weniger interessieren. Alle oben angeführten Listener bzw. Adapter werden von allen Komponenten unterstützt. Speziellere, auf die Komponenten zugeschnittene Event-Listener sind in der nächsten Aufzählung aufgelistet. Das werden jene Event-Listener sein, die in den meisten Applikationen direkt verwendet werden. ActionListener: Benutzer drückt einen Button, auf eine Check-Box, Return in einem Textfeld oder wählt einen Menüpunkt aus. Methode: • void actionPerformed(ActionEvent e): Wird ausgeführt, sobald ein komponentenspezifischer, sinnvoller Event gefeuert wird. Im Falle eines Buttons ist es wichtig zu erfahren, ob er gedrückt wurde oder nicht (und nicht ob jetzt die Maus gerade über den Button läuft...) Wichtige Methoden von ActionEvent sind getActionCommand() (returniert den mit setActionCommand(String) definierten Wert der Komponente) bzw. getModifiers() (returniert den Integerwert von Steuerungstasten – ActionEvent.SHIFT_MASK, -.CTRL_MASK, -.META_MASK und -. ALT_MASK). (siehe auch Sun-Tutorial17 ). WindowListener: Fenster wird vom Benutzer verändert. Methoden: • void windowOpened(WindowEvent) • void windowClosed(WindowEvent) • void windowIconified(WindowEvent) • void windowDeiconified(WindowEvent) • void windowActivated(WindowEvent) • void windowDeactivated(WindowEvent) Die Methode getWindow() returniert von WindowEvent das Window-Objekt, welches den Event auslöste (siehe auch Sun-Tutorial18 ). ChangeListener: Wenn sich der Status einer Komponente ändert (z.B. eines Buttons), wird dieser Listener verständigt. • void stateChanged(ChangeEvent) Im übergebenen ChangeEvent-Objekt gibt es nur die von EventObject geerbte Methode getSource() (siehe auch Sun-Tutorial19 ). ItemListener: Für alle Komponenten, die das ItemSelectable-Interface implementieren (Button, Check-Box, Combo-Box, Radio-Button...). Diese Komponenten verwalten einen Ein/Aus-Zustand von einem oder mehreren Komponenten. Methode: • void itemStateChanged(ItemEvent) 17 http://java.sun.com/docs/books/tutorial//uiswing/events/actionlistener.html 18 http://java.sun.com/docs/books/tutorial//uiswing/events/windowlistener.html 19 http://java.sun.com/docs/books/tutorial//uiswing/events/changelistener.html KAPITEL 6. SWING 89 ItemEvent bietet eine Methode getItem() an, bei der ein komponentenspezifischer Wert vom veränderten Objekt zurückgegebn wird (meist ein String). Die Methode getItemSelectable() retourniert die Komponente, die den Event ausgelöst hat, und mit getStateChange() findet man heraus, ob die Komponente selektiert oder deselektiert wurde (ItemEvent.SELECTED bzw. -.DESELECTED; siehe auch Sun-Tutorial20 ). Es gibt für spezielle Komponenten noch weitere Event-Handler-Typen (z.B. für den JTree gibt es TreeSelectionListener, TreeWillExpandListener und TreeModelListener, für den Fileselector gibt es wieder andere...). Da dieser Abschnitt nur einen Überblick über die Grundelemente von Swing-Komponenten geben soll, wird auf diese speziellen Konstrukte nicht eingegangen. Wie schon bei den einzelnen Event-Handler Typen beschrieben, kann mit der Methode add[SomeTypeOfListener]Listener(...) einer Komponente ein Listener zur Benachrichtigung eines bestimmten Events hinzugefügt werden. Durch mehrfachen Aufruf dieser Methode werden mehrere Listener registriert. Will man z.B. ein Objekt verständigen, wenn ein Button mit der Maus gedrückt wird, schaut der Code wie folgt aus: 1 2 3 JButton t h e b u t t o n = new JButton ( ”Some B u t t o n l a b e l ” ) ; MouseListener t h e l i s t e n e r = new ClassImplementsMouseListener ( ) ; t h e b u t t o n . addMouseListener ( t h e l i s t e n e r ) ; Wird nun der Button gedrückt, wird das Objekt the_listener benachrichtigt. Diese Benachrichtigung erfolgt über den Aufruf der implementierten Methode (in Fall des MouseListener’s ist das mouseClicked). Weiters gibt es noch Methoden der Form get[SomeTypeOfListener]Listener() und remove[SomeTypeOfListener]Listener()21 . Mit diesen Grundlagen sollte es nun möglich sein, einfache, interaktive SwingApplikationen zu entwerfen. Wir betrachten im Folgenden noch einige Spezialitäten von Swing und beantworten einige Fragen. 6.7 Was passiert beim Zeichnen eines Fensters? Komponenten in einem Container werden meist von einem Layoutmanager gezeichnet. Container haben einen Default-Layoutmanager (z.B. JFrame verwendet den BorderLayout-Manager). Normalerweise braucht man sich nicht darum kümmern, wie und wann welche Elemente gezeichnet werden. Treten jedoch bei der Visualisierung Probleme auf, ist es durchaus sinnvoll, wenn man den Vorgang versteht. Nachdem in einen Container Komponenten positioniert wurden, sollte mittels pack() der Container zur Visualisierung vorbereitet werden (es wird z.B. die Größe festgelegt). Mit setVisible(true) wird dann der Container auch sichtbar gemacht und gezeichnet. Komponenten zeichnen sich selbst neu, wenn: • Das Fenster neu aufgebaut wird in dem sie gespeichert sind. 20 http://java.sun.com/docs/books/tutorial//uiswing/events/itemlistener.html 21 Beim Button kann man auch einen ActionListener implementieren... KAPITEL 6. SWING 90 • Die Fenstergröße verändert wird. • Teile des Fensters in den Vordergrund gebracht werden. • Sich der Programmstatus ändert. Wobei nur jene Bereiche neu gezeichnet werden, die von der Änderung betroffen sind. Beispiel: Nimmt man einen Button, der ein bestimmtes Label hat, und verändert man dann dieses Label, so wird nur dieser Button neu gezeichnet, und alle in der Umgebung liegenden Komponenten (die ja von dieser Änderung nicht betroffen sind) werden nicht neu gezeichnet. Elemente unter einer durchsichtigen Komponente werden auch neu gezeichnet, d.h. die Komponenten des Bereichs, der neu gezeichnet werden muss, werden von unten nach oben neu gezeichnet. Werden Komponenten explizit als nicht durchsichtig markiert (setOpaque(true)), werden alle darunterliegenden Komponenten in dem Bereich nicht neu gezeichnet. Werden die Komponenten entgegen aller Norm nicht neu gezeichnet, kann man die Methode repaint() der Komponente explizit aufrufen. Ändert sich die Größe oder Position einer Komponente, sollte man vor repaint() noch revalidate() aufrufen. Das Zeichnen passiert – wie das Abarbeiten eines Events – innerhalb eines bestimmten Threads (und wieder ist es der event-dispatching thread , zu Threads siehe bitte später), d.h., während etwas am Bildschirm gezeichnet wird, werden keine Events abgearbeitet und umgekehrt. Der Zeichenvorgang einer Komponente wird jeweils komplett abgeschlossen (halb gezeichnete Komponenten – z.B. ein halb-gedrückter und halb-nichtgedrückter Knopf – machen keinen Sinn...). Swing verwendet einen sog. double-buffer -Mechanismus, bei dem die Komponenten zuerst versteckt gezeichnet werden und erst danach der Inhalt des fertig gezeichneten Puffers in den sichtbaren Bereich kopiert wird. 6.8 Swing-Komponenten Nun noch eine kurze Zusammenstellung der Komponenten, die in Swing zur Verfügung gestellt werden. Ein interessanterer (visueller) Index ist in SunTutorial22 zusammengestellt. 6.8.1 Top-Level Container Eine Applikation benötigt mindestens einen Top-Level Container, in dem weitere Komponenten plaziert werden können. JFrame: Viele Beispiele, die in diesem Abschnitt vorgestellt wurden, verwenden Frames, daher kommt hier nur ein ganz, ganz kurzes Beispiel, wie ein Frame erzeugt wird: 22 http://java.sun.com/docs/books/tutorial//uiswing/components/components.html KAPITEL 6. SWING 1 2 91 import javax . swing . ∗ ; import j a v a . awt . ∗ ; 3 4 5 6 7 8 public c l a s s SimpleJFrame { public s t a t i c void main ( S t r i n g [ ] a r g s ) { JFrame t h e f r a m e = new JFrame ( ”Some T i t l e o f the Frame” ) ; 9 JLabel t h e l a b e l = new JLabel ( ”Some l a b e l ” ) ; t h e f r a m e . getContentPane ( ) . add ( t h e l a b e l , BorderLayout .CENTER) ; 10 11 12 t h e f r a m e . pack ( ) ; t h e f r a m e . s e t V i s i b l e ( true ) ; 13 14 } 15 16 } JDialog: Einige Klassen stellen Dialoge zur Verfügung. JOptionPane kann unterschiedliche Arten von Dialogen über einfache, statische Methoden erzeugen. Ein Dialog kann auch modal sein, d.h. er blockiert sämtliche Eingaben des Benutzers für darunter liegende Fenster. Im folgenden sind nur zwei einfache Aufrufe skizziert. Weitere Infos sind in Sun-Tutorial23 zu finden. JOptionPane.showMessageDialog(the frame, message,...): Diese Methode öffnet in einem Frame einen Message-Dialog, der über einen Ok-Button verfügt. Weitere Parameter beeinflussen den Titel des Dialogs bzw. das Icon. JOptionPane.showOptionDialog(the frame, message, ...): Hier kann vom Benutzer eine Auswahl getroffen werden. Eine typische Anwendungen für diesen Dialog sind Ja/Nein/Abbruch-Dialoge. Z.B.: 1 Object [ ] o p t i o n s = { ”Yes” , ”No” , ” Cancel ” } ; 2 3 4 5 6 7 8 9 10 int answer = JOptionPane . showOptionDialog ( the frame , // the frame ” Continue the program ?” , // the message ”” , // t i t l e o f d i a l g −frame JOptionPane .YES NO CANCEL OPTION, // o p t i o n type JOptionPane .QUESTION MESSAGE, // type o f d i a l o g , s e l e c t s d e f a u l t i c o n null , // custom i c o n options , // which o p t i o n s options [ 0 ] ) ; // d e f a u l t s e l e c t e d o p t i o n In answer steht nach der Auswahl des Benutzers JOptionPane.YES_OPTION, -.NO_OPTION oder -.CANCEL_OPTION und das Programm kann entsprechend darauf reagieren. ...: JApplet: 6.8.2 Allgemeine Container Diese Art der Container werden meist zur Gruppierung von Komponenten verwendet (siehe auch Sun-Tutorial24 ). 23 http://java.sun.com/docs/books/tutorial//uiswing/components/dialog.html 24 http://java.sun.com/docs/books/tutorial//uiswing/components/intermediate.html KAPITEL 6. SWING 92 JPanel: JScrollPane: JSplitPane: JTabbedPane: JToolBar: JInternalFrame: JLayeredPane: JRootPane: 6.8.3 Kontroll Komponenten Mit diesen Komponenten erfolgt eine Steuerung vom Programm (siehe auch Sun-Tutorial25 ). Die Komponenten können auch deaktiviert werden. JButton: JCheckBox: JRadioBotton: JComboBox: JList: JMenu: JSlider: 6.8.4 Info Komponenten Diese Komponenten werden zur Visualisierung von Werten (Informationsdarstellung) verwendet. JLabel: JProgressBar: JToolTip: 25 http://java.sun.com/docs/books/tutorial//uiswing/components/widgets.html KAPITEL 6. SWING 6.8.5 93 Spezielle Komponenten JColorChooser: JFileChooser: JTable: text.JTextComponent: • JTextArea • JTextField JTree: • JEditorPane Kapitel 7 Threads In Java ist es recht einfach, mehrere Arbeiten gleichzeitig von einem Programm erledigen zu lassen. In diesem Abschnitt betrachten wir das Konzept von Threads1 in Java und zeigen die Probleme und Stolpersteine. In einer Applikation können viele, aber sinnvollerweise nicht beliebig viele Threads verwendet werden. Threads lassen sich in sog. thread-groups und in thread-pools zusammenfassen. Da diese Konzepte bei weitem den Rahmen dieser Unterlagen sprengen würden, sei nur auf die Limitierung hingewiesen. Wie diese Probleme zu lösen sind, wird nicht diskutiert. Wer sich intensiv mit ThreadProgrammierung in Java befasst, kommt nicht um [Oaks and Wong, 1999] herum. Viel Erfahrung ist notwendig, um vernünftige Programme schreiben zu können. 7.1 Motivation Wir haben schon beim Kapitel über Swing gehört, dass Event-Handling Routinen kurz sein sollen bzw. schnell abgearbeitet werden müssen, da diese im selben Thread ausgeführt werden, wie die Zeichenroutinen für die Komponenten. Betrachten wir folgenden Codeausschnitt eines Action-Listeners (Die Methode simulateTask() benötigt in unserem Beispiel 5 Sekunden zur Ausführung): 16 17 18 19 20 21 22 menu item . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) { public void actionPerformed ( ActionEvent event ) { simulateTask ( ) ; } }); Wird nun der Menüpunkt ’Excecute ->Some Task’ ausgewählt, wird die Methode simulateTask() ausgeführt und sämtliche anderen notwendigen Operationen wie z.B. das Verarbeiten weiterer Swing-Events) können nicht abgearbeitet werden. Der Benutzer hat zu Recht das Gefühl, dass die Applikation langsam 1 Thread: 1. Prozess, der innerhalb eines Programms selbstständig ausgeführt werden kann, aber nicht unabhängig von ihm (z.B. die Silbentrennung einer Textverarbeitung). Heutige Prozessoren können mehrere Threads parallel ausführen. 2. Gruppe von News in einer Newsgroup, die zusammengehören. Brockhaus 94 KAPITEL 7. THREADS 95 ist oder mit dem Rechner etwas nicht stimmt. Dieses Verhalten ist natürlich absolut unpassend für interaktive Applikationen. Um das Verhalten der Applikation zu verbessern, muss man die Methode simulateTask() in einem separaten Thread ablaufen lassen. Dieser Ansatz würde die weitere Abarbeitung der Applikation nicht behindern (es passiert ja zugegebenermaßen nicht sehr viel im Action-Listener) Schauen wir uns der Vollständigkeit halber noch die notwendigen Änderungen an (die im Folgenden ausführlicher diskutiert werden): 18 19 20 21 22 23 24 25 26 27 28 29 30 31 menu item . a d d A c t i o n L i s t e n e r (new A c t i o n L i s t e n e r ( ) { public void actionPerformed ( ActionEvent event ) { Thread a c t i o n t h r e a d = new Thread (new Runnable ( ) { public void run ( ) { simulateTask(++num thread ) ; } }); action thread . start ( ) ; } }); Wenn man diese Programme vergleicht, so werden potentielle Benutzer das zweite Programm bei weitem besser akzeptieren als das erste. Genug Motivation also, sich mit threading etwas genauer auseinander zu setzen... Wenn ein Computer nur eine Recheneinheit (=CPU) hat, kann selbstverständlich nur ein Programm zu einem Zeitpunkt ausgeführt werden. Um hier das Gefühl zu vermitteln, dass mehrere Programme “gleichzeitig” ausgeführt werden, arbeitet das Betriebssystem mit einige Tricks (Diese Ausführungen sind sehr, sehr einfach gestaltet und sollen nur einen Überblick geben, wie solche Systeme arbeiten. Ein besserer und ausführlicherer Einblick wird in Büchern zu Betriebssystemen gegeben, siehe z.B. [Silberschatz and Galvin, 1998]). Das Betriebssystem führt eine Liste von Programmen (Prozessen), die ausgeführt werden sollen. Je nach Verfahren wird nun ein Programm, das ausgeführt werden soll, hergenommen und z.B. für einen bestimmten Zeitraum verarbeitet. Nach diesem Zeitraum wird der Status der CPU und der verwendeten Speicherbereiche zwischengespeichert und das nächste Programm ausgewählt und ausgeführt. Welche Programme nun wann für wie lange ausgeführt werden sollen, ist abhängig von den verwendeten Algorithmen und Strategien. Dass hier Probleme auftreten können, liegt auf der Hand (einzelne Programme bekommen vielleicht nie Rechenzeit zugeteilt und vieles andere mehr). Einige – aber sicher nicht alle – Probleme werden wir hier besprechen. In Java ist hier die Situation ähnlich: Threads bekommen Prioritäten (zwischen Thread.MIN PRIORITY und Thread.MAX PRIORITY). Sind Threads im runnableZustand (d.h. die Thread-Objekte sind instantiiert und gestartet) können sie ausgeführt werden. Threads mit höherer Priorität haben bei der Ausführung Vorrang. Der aktuell laufende Thread wird gestoppt, wenn: 1. Ein Thread mit einer höheren Priorität in den runnable-Zustand kommt. 2. Der Thread freiwillig zum Arbeiten aufhört (sleep, yield), auf eine bestimmte Bedingung wartet (wait) bzw. auf Ein-/Ausgabe wartet. KAPITEL 7. THREADS 96 3. Bei Betriebssystemen, die das Zeitscheibenverfahren unterstützen: Wenn der zugeteilte Zeitschlitz aufgebraucht ist. Stehen mehrere CPUs zur Verfügung, werden die Prozesse bzw. Threads auf die zur Verfügung stehenden CPUs aufgeteilt. Diesen Vorgang der Auswahl von Threads auf die CPU nennt man Scheduling (“Ablaufplanung”). Die JVM implementiert das Scheduling preemptive, d.h. sobald ein höher priorisierter Thread als der aktuell laufende im System angemeldet wird, wird der laufende Thread unterbrochen. Vorsicht Falle: Prioritätenbasiertes Scheduling ist die Theorie :) Man sollte sich aber auf keinen Fall darauf verlassen, dass Threads in einer bestimmten Reihenfolge abgearbeitet werden! Es kann auch passieren, dass ein Thread mit niederer Priorität ausgeführt wird, um sog. starvation (“verhungern”) des Threads zu verhindern. 7.2 Threads in Java In Java gibt es zwei Möglichkeiten, Anweisungen in Threads abarbeiten zu lassen, nämlich durch: • Ableiten einer Klasse von java.lang.Thread bzw. durch • Implementieren vom java.lang.Runnable-Interface. Es ist wieder abhängig von der Klassenhierarchie, wie man bei einem gegebenen Problem vorgeht. Man ist aber auf alle Fälle flexibler, wenn man das RunnableInterface implementiert, da man unabhängig von einer vorhandenen Elternklasse Nebenläufigkeit (=gleichzeitige Abarbeitung) erzielen kann2 . Im folgenden Beispiel sieht man, wie diese zwei Typen von Threads gestartet werden können. 1 2 3 4 5 6 7 8 9 10 c l a s s AThread extends Thread { public void run ( ) { for ( int i =0; i <500; i++) { System . out . p r i n t ( ” −− An extended Thread −− ” ) ; } } } 11 12 13 14 15 16 17 18 19 20 21 c l a s s ARunnable implements Runnable { public void run ( ) { for ( int i =0; i <500; i++) { System . out . p r i n t ( ” −− An implemented Runnable −− ” ) ; } } } 22 23 public c l a s s SimpleThreadExample 2 In Java gibt’s ja nur eine einfache Ableitung... KAPITEL 7. THREADS 24 97 { public s t a t i c void main ( S t r i n g [ ] a r g s ) { AThread some thread = new AThread ( ) ; some thread . s t a r t ( ) ; 25 26 27 28 29 ARunnable some runnable = new ARunnable ( ) ; new Thread ( some runnable ) . s t a r t ( ) ; 30 31 } 32 33 } Verwendet man die erste Methode (Ableitung von java.lang.Thread), muss man ein Objekt dieser Klasse erzeugen und die start()-Methode aufrufen (siehe Zeile 27-28). Es wird dann implizit ein Thread gestartet und die Methode run() ausgeführt. Bei der zweiten Art (Implementation von java.lang.Runnable) muss man zuerst ein Objekt vom Typ “Runnable” erzeugen, und dieses Objekt dann dem Thread-Konstruktor als Parameter übergeben. Im gleichen Schritt kann man auch die start()-Methode aufrufen. Man hat die Möglichkeit Threads mit Namen zu versehen. Tut man das nicht, wird automatisch ein Name vom System vergeben. Via setName(String) kann man auch nachträglich einem Thread einen Namen zuweisen. Der Name muss nicht eindeutig im System sein, d.h. es können mehrere Threads ein und denselben Namen haben3 . Ein gestartet Thread wird so lange ausgeführt, bis die run()-Methode verlassen oder eine Exception innerhalb von run() nicht behandelt wird. In Java wird zwischen zwei Arten von Threads unterschieden: User-Threads erledigen Aufgaben, die essentiell für die Applikation sind. Eine Applikation läuft so lange, bis alle User-Threads beendet sind. Beim Starten einer Applikation gibt es genau einen User-Thread, der die main()Methode der zu startenden Klasse aufruft. Daemon-Threads erledigen nicht so wichtige Aufgaben (z.B. garbage collection) die aber notwendig für den Gesamtablauf der Applikation sind. Eine Applikation wird beendet, sobald nur mehr Daemon-Threads in der Appliation laufen. Über die Methode setDeamon(true) kann ein Thread als Daemon-Thread markiert werden, bevor er gestartet wurde4 . Threads teilen sich Ressourcen. Sie besitzen zwar einen “eigenen” Stack und “eigene” lokale Variablen, teilen sich aber Instanz-Variablen. Das bei diesem “Ressource-Sharing” einiges daneben gehen kann ist offensichtlich. Betrachten wir folgendes Beispiel: 1 2 3 public c l a s s BadRunnable implements Runnable { int s h a r e d i n t = 0 ; 4 5 public void run ( ) 3 Nachdem es eine setName(String)-Methode gibt, ist es für den versierten Programmierer offensichtlich, dass auch eine getName()-Methode existieren muss... 4 Wenn der Thread bereits gestartet wurde, wird eine IllegalThreadStateException ausgelöst. KAPITEL 7. THREADS 98 { 6 s h a r e d i n t ++; System . out . p r i n t l n ( Thread . currentThread ( ) . getName () + ” : Value i s now : ” + s h a r e d i n t ) ; 7 8 9 } 10 11 public s t a t i c void main ( S t r i n g [ ] a r g s ) { BadRunnable bad runnable = new BadRunnable ( ) ; Thread t h r e a d 1 = new Thread ( bad runnable ) ; Thread t h r e a d 2 = new Thread ( bad runnable ) ; thread 1 . s t a r t ( ) ; thread 2 . s t a r t ( ) ; } 12 13 14 15 16 17 18 19 20 } Die zwei Threads (thread 1 und thread 2) teilen sich die Variable shared int . Es ist sehr wahrscheinlich, dass auf der Konsole Thread-1: Value is now: 1 Thread-2: Value is now: 2 zu lesen ist. Ebenso ist es wahrscheinlich, dass Thread-2: Value is now: 1 Thread-1: Value is now: 2 oder Thread-1: Value is now: 2 Thread-2: Value is now: 2 auf der Konsole erscheinen. Je nachdem, welcher Thread vorher am Zug ist, ist also das Verhalten der Applikation unterschiedlich. Wenn Blöcke in Threads atomar sein sollen (thread save), so müssen die Anweisungen speziell gekennzeichnet werden. Hierfür wurde das Schlüsselwort synchronized eingeführt. Jede Instanz einer Klasse (i.e. jedes Objekt) besitzt genau einen “Lock”. Will ein Thread in einem Objekt einen synchronized-Block abarbeiten, muss er diesen “Lock” anfordern. Erst wenn der Thread in Besitz dieses “Locks” ist, darf er den Block abarbeiten. Am Ende des synchronisierten Blocks wird der Lock dem Objekt wieder zurückgegeben. Es ergibt sich daraus, dass während ein synchronisierter Block mit einem Objekt ausgeführt wird, kein anderer synchronisierter Block mit diesem Objekt ausgeführt werden kann. Man kann ganze Methoden mit synchronized markieren oder aber wirklich nur die kritischen Bereiche, bei denen Probleme auftreten können, wenn zwei oder mehrere Threads auf ein und dieselbe Variable zugreifen. Jedes Objekt hat einen sog. Monitor (oder “lock”). Will ein Thread A in einen synchronized-Block, so muss er den Monitor des Objekts (auf das synchronisiert wurde) besitzen. Besitzt ein anderer Thread B den Monitor des Objekts, so muss der Thread A so lange warten, bis Thread B den Monitor wieder freigibt (d.h., dass Thread B den synchronized-Block wieder verlassen muss). Es kann nur auf Objekte, und nicht auf Primäre-Datentypen synchronisiert werden. KAPITEL 7. THREADS 99 Generell sollte man bedenken, dass geschachtelte synchronized-Blöcke erhebliche Probleme bringen können und sog. Deadlocks5 auftreten können und lt. Murphy auftreten werden. Als Richtlinie sollte man niemals Methoden innerhalb von synchronisierten Methoden oder Blöcken aufrufen (und wenn man das tut, sollte man genau wissen, was passiert...). Ein einfaches und sehr offensichtliches Beispiel ist in Abschnitt 7.5 zu finden. synchronized methods: Bei den Methoden haben wir bereits das Schlüsselwort synchronized kennengelernt: Solche Methoden dürfen nur von einem Thread zu einem Zeitpunkt mit einem Objekt verwendet werden. Es wird das gesamte Objekt für den weiteren Zugriff gesperrt, bis die synchronisierte Methode wieder verlassen wird! 1 2 3 4 public synchronized void put ( Object t h e o b j e c t ) { ... } Wird diese Methode von einem Thread mit einem Objekt A ausgeführt, wird verhindert, dass ein anderer Thread ebenso diese Methode mit dem Objekt A ausführt. synchronized blocks: Es ist auch möglich auf bestimmte Objekte zu synchronisieren. 1 2 3 4 5 // . . . some code . . . synchronized ( t h e o b j e c t ) { ... } Soweit allgemeine Erläuterungen zum Konzept der Threads in Java. Im Folgenden werden wir untersuchen, wie der Lebenszyklus eines Threads ausschaut, wie die wichtigsten Methoden der Klasse Thread heißen und wie diese arbeiten, sowie welche Stolpersteine bei der Programmierung mit Threads zu beachten sind. 7.3 Lebenszyklus eines Threads Bei der Verwendung von Threads sollte klar sein, dass diese schwerer zu debuggen sind als herkömmliche Programme. Wenn man also nicht genau weiß, was innerhalb eines Threads passiert und welche Variablen von unterschiedlichen Threads geteilt werden, sind hier enorme Probleme zu erwarten. Ein ungeübter Entwickler sollte sich also bewusst sein, was er alles anrichten kann... Einige Stolpersteine sind in Abschnitt 7.5 aufgelistet. Um einen Thread zu starten, brauchen wir – wenig überraschend – zuerst ein Objekt vom Typ Thread. Dieser Thread kann dann mit der start()-Methode gestartet werden. Bei start() werden dem Thread System-Ressourcen (Speicher, Rechenzeit etc.) zur Verfügung gestellt und der Code in der run()-Methode ausgeführt (Status des Threads ist “running”). 5 “Nichts geht mehr”, weil z.B. ein Thread A auf die Freigaben von einem Objekt B wartet, welches von Thread B bereits synchronisiert wurde. Thread B wartet aber auf die Freigabe von Objekt A, welches bereits in Thread A synchronisiert wurde... KAPITEL 7. THREADS 100 Abbildung 7.1: Lebenszyklus eines Threads Vorsicht Falle: Wie wir bereits gesehen haben, ist es extrem leicht, Threads zu erzeugen. Bevor man aber mit Threads arbeitet, sollte man sich bewusst sein, dass das Erzeugen und Starten von Threads eine “teure” Angelegenheit ist (abhängig vom Betriebssystem). Es ist also anzuraten, sich genau zu überlegen, ob ein Thread wirklich erzeugt werden muss (Lösung: thread-pooling). Der Status eines Threads kann in den ’Not Running’-Status übergehen, wenn: • Die sleep(mill_sec)-Methode aufgerufen wird. Es wird also “freiwillig” für eine bestimmte Zeit auf die Ausführung verzichtet. • Explizit auf eine bestimmte Bedingung gewartet wird (wait()). • Auf Ein/Ausgabe gewartet wird. • Freiwillig auf die Ausführung des eigenen Threads verzichtet wird (yield()). Das passiert nur, wenn ein Thread mit gleicher Priorität im RunnableStatus ist. Der Status ändert sich von “Not Runnable” wieder auf “Runnable”, wenn: • Die mill_sec von sleep vergangen sind. • Via notify() bzw. notifyAll() benachrichtigt wird. • Die Ein-/Ausgabe abgeschlossen ist. Schlussendlich muss ein Thread einmal beendet werden. Das passiert, wenn die run()-Methode verlassen wird. Die stop()-Methode darf nicht mehr verwendet werden (stop() ist deprecated!)! Der übliche Ansatz ist, dass man in der run()-Methode eine while-Schleife verwendet (z.B. while (should run ) ...) und die Schleifen-Bedingung ändert, sobald man den Thread beenden will (in diesem Fall also should run = false;). should run muss als Instanzvariable ausgeführt werden, damit andere Methoden der Klasse den Wert ändern können. 7.4 Einige Methoden in Thread Wir haben nun schon einige Methoden der Thread-Klasse kennen gelernt. In diesem kurzen Abschnitt wird eine Auflistung der Methoden nach Kategorien und Aufgaben gegeben. KAPITEL 7. THREADS 101 Ausführung für eine bestimmte Zeit deaktivieren (sleep): Sollen von einem Thread immer wiederkehrende Aufgaben gelöst werden (z.B. zeichnen einer Animation oder crond-ähnliche Aufgaben), so kann der Thread für eine bestimmte Zeit deaktiviert werden. Das Versetzen in den schlafenden Zustand erfolgt über die statische Methode sleep(mill_sec). Z.B. Thread.sleep(1000): der aktuell laufende Thread (current thread ) wird hier für 1000msec. in den “Not Runnable”-Zustand versetzt. Während dieser Unterbrechung werden die gesperrten Objekte nicht freigegeben. Ausführung deaktivieren, bis eine Bedingung zutrifft (wait): Es kann passieren, dass man auf bestimmte Ereignisse warten muss, bis eine Aufgabe fortgesetzt werden kann. Will man z.B. unter anderem eine Liste in Thread A sortieren, so sollte diese zuvor vollständig mit den zu sortierenden Elementen gefüllt sein. Übernimmt diese Aufgabe Thread B, so darf Thread A erst dann weitergeführt werden, wenn Thread B seine Aufgabe erledigt hat. Für diese Art der Kommunikation stehen bei allen Objekten die Methoden wait() und notify() zur Verfühung (ist in java.lang.Object definiert). In Thread A muss daher in etwa der folgende Code stehen: while list_object is not ready wait for list_object Und in Thread B sollte, sobald sich das Listen-Objekt in einem richtigen Zustand befindet, etwa folgender Code stehen: notify list_object Dadurch wird der Thread, der auf das list_object wartet, benachrichtigt, und die Ausführung kann fortgesetzt werden. Ausführung kurzfristig unterbrechen (yield): Ein Thread kann freiwillig seine Ausführung unterbrechen und auch anderen Threads die Möglichkeit zur Ausführung geben (yield()) Namen von Threads: Für Debugging-Zwecke ist es sinnvoll, Threads mit Namen zu versehen (setName(the name)). Man kann dann einfach via getName() auf den Namen zugreifen. Thread beendet? Will man herausfinden, ob ein Thread noch am Leben ist und Operationen ausführt, kann man die Methode isAlive() des Threads verwenden. Diese gibt true zurück, wenn der Thread gestartet, aber noch nicht beendet wurde. Eine weitere Möglichkeit ist die Verwendung von join(). Die Methode join() eines Threads blockiert so lange, bis der Thread nicht mehr “am Leben” ist. Will man maximal eine bestimmte Zeit warten, bis der Thread beendet wird, kann man noch mit join(long timeout) die maximale Wartezeit bekanntgeben. join kann daher als eine Kombination von sleep() und isAlive() angesehen werden. KAPITEL 7. THREADS 102 Vorsicht Falle: Es macht natürlich keine Sinn, diese Methoden (isAlive und join) auf den eigenen Thread aufzurufen. isAlive sollte ja immer true zurückliefern, wenn der Thread gerade läuft, und das wird er wohl, wenn man selbst danach fragen kann... Ein join auf “sich selbst” wird wohl nie returnieren. Prioritäten: Es ist möglich, Threads mit unterschiedlichen Prioritäten zu versehen. Threads mit einer hohen Priorität bekommen mehr Zeit für die Ausführung als jene mit einer niederen Priorität. Mit den Methoden setPriority(...) bzw. int getPriority() kann der entsprechende Parameter für den jeweiligen Thread gesetzt bzw. gelesen werden. 1 2 3 4 /∗∗ ∗ The minimum p r i o r i t y t h a t a thread can have . ∗/ public f i n a l s t a t i c int MIN PRIORITY = 1 ; 5 6 7 8 9 /∗∗ ∗ The d e f a u l t p r i o r i t y t h a t i s a s s i g n e d to a thread . ∗/ public f i n a l s t a t i c int NORM PRIORITY = 5 ; 10 11 12 13 14 /∗∗ ∗ The maximum p r i o r i t y t h a t a thread can have . ∗/ public f i n a l s t a t i c int MAX PRIORITY = 1 0 ; Wird ein Thread erzeugt, bekommt er die selbe Priorität wie der Thread, der ihn erzeugt hat. Gruppen von Threads: Es ist möglich, Gruppen von Threads zur besseren Verwaltung zusammenzufassen. Hier muss bei der Erzeugung des Threads bereits die Gruppe (ThreadGroup) angegeben werden. Wird keine Gruppe angegeben, so wird der Thread einer Default-Gruppe zugeordnet. Wieviele Threads? Man kann herausfinden, wieviele Threads in einer bestimmten Gruppe laufen (activeCount). Hat dieser Thread den Monitor vom Objekt (holdsLock)? Mit syncronized(the_object) kann man verhindern, dass zwei Threads gleichzeitig auf ein Objekt zugreifen. Um zu überprüfen, ob ein Objekt von einem Thread gesperrt wird, gibt es die statische Thread-Methode holdsLock(the_object), die herausfindet, ob der aktuelle Thread das Objekt sperrt. Es gibt noch einige Methoden mehr, die allerdings in diesen Unterlagen nicht behandelt werden. Hier soll lediglich die Idee der Threads verbreitet werden. Wie die optimale (“systemschonende”) Umsetzung ausschaut, kann nicht soooo schnell mitgeteilt werden. Hier benötigt man einiges an Programmierpraxis und Erfahrung... 7.5 Stolpersteine Einige Methoden in der Thread-Klasse sind “gefährlich” und sollten aus diesem Grund nicht verwendet werden. KAPITEL 7. THREADS 103 Vorsicht Falle: Es gibt in der Thread-Klasse einige sog. deprecated Methoden6 , die nicht mehr verwendet werden sollen und früher oder später aus der Sprache herausgenommen werden. Diese Methoden bringen Probleme mit sich und können soweit führen, dass das Programm nicht mehr so funktioniert, wie es sich der Entwickler denkt. Deprecated Methoden sind als solche in der Dokumentation markiert. Im Zusammenhang mit Threads sind folgende Methoden deprecated: stop(), suspend() und resume(). Sobald zwei Dinge auf einmal von einem Programm erledigt werden können, sind Probleme vorprogrammiert. So kann es einfach passieren, dass sich zwei Threads Variablen teilen, die von beiden Threads zur gleichen Zeit verändert werden. Dass hier dann nur mehr Schwachsinn herauskommt, liegt auf der Hand... Vorsicht Falle: Wenn Variablen von mehreren Threads gleichzeitig verändert werden können, muss das verhindert werden! Gleichzeitiger, ausschließlich lesender Zugriff ist selbstverständlich kein Problem! Wie schon angekündigt, ist es sehr wahrscheinlich, dass Deadlocks auftreten, wenn sich Threads gegenseitig Ressourcen reservieren. Ein einfaches Beispiel soll das Wiedergeben. 1 2 3 4 public c l a s s DeadLockExample extends Thread { protected s t a t i c Object a s h a r e d o b j e c t = new Object ( ) ; protected s t a t i c Object a n o t h e r s h a r e d o b j e c t = new Object ( ) ; 5 f i n a l protected s t a t i c S t r i n g FIRST THREAD NAME = ” t h e f i r s t t h r e a d ” ; f i n a l protected s t a t i c S t r i n g SECOND THREAD NAME = ” t h e s e c o n d t h r e a d ” ; 6 7 8 public DeadLockExample ( S t r i n g name) { super (name ) ; } 9 10 11 12 13 public void run ( ) { while ( true ) { i f ( this . getName ( ) . e q u a l s (FIRST THREAD NAME) ) { synchronized ( a s h a r e d o b j e c t ) { System . out . p r i n t l n ( t his . getName () + ” : j u s t locked a shared object . . . ” ) ; synchronized ( a n o t h e r s h a r e d o b j e c t ) { System . out . p r i n t l n ( t h is . getName () + ” : j u s t locked another shared object . . . ” ) ; } System . out . p r i n t l n ( t his . getName () + ” : j u s t unlocked a n o t h e r s h a r e d o b j e c t . . . ” ) ; } System . out . p r i n t l n ( t his . getName () + ” : j u s t unlocked a s h a r e d o b j e c t . . . ” ) ; } else { synchronized ( a n o t h e r s h a r e d o b j e c t ) 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 6 to deprecate: missbilligen, verurteilen, verwerfen KAPITEL 7. THREADS 104 { 38 System . out . p r i n t l n ( t his . getName () + ” : j u s t locked another shared object . . . ” ) ; synchronized ( a s h a r e d o b j e c t ) { System . out . p r i n t l n ( t his . getName () + ” : j u s t locked a shared object . . . ” ) ; } System . out . p r i n t l n ( t his . getName () + ” : j u s t unlocked a s h a r e d o b j e c t . . . ” ) ; 39 40 41 42 43 44 45 46 47 } System . out . p r i n t l n ( t his . getName () + ” : j u s t unlocked a n o t h e r s h a r e d o b j e c t . . . ” ) ; 48 49 50 } try { Thread . s l e e p ( ( int )Math . random ( ) ∗ 1 0 0 0 ) ; } catch ( I n t e r r u p t e d E x c e p t i o n exc ) { exc . p r i n t S t a c k T r a c e ( ) ; } 51 52 53 54 55 56 57 58 59 } 60 } 61 62 public s t a t i c void main ( S t r i n g [ ] a r g s ) { DeadLockExample t h e f i r s t = new DeadLockExample (FIRST THREAD NAME) ; DeadLockExample t h e s e c o n d = new DeadLockExample (SECOND THREAD NAME) ; 63 64 65 66 67 the first . start (); the second . s t a r t ( ) ; 68 69 } 70 71 } In diesem Beispiel wird vom ersten Thread zuerst auf ein Objekt (a shared object ) synchronisiert, danach auf ein anderes Objekt (another shared object ). Für den zweiten Thread ist es genau umgekehrt. Wird dieses Programm ausgeführt, dauert es nicht lange, bis ein Deadlock auftritt... KAPITEL 7. THREADS 105 Literaturverzeichnis [Gamma et al., 1998] Gamma, Helm, Johnson, and Vlissides (1998). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley Longman, Inc. [Gosling et al., 1996] Gosling J., Joy B., Steele G.: The Java Language Spefication, Addison-Wesley (1996). Online: http://java. sun.com/docs/books/jls/index.html [Sun 1999] Sun Code Conventions for the Java Programming Language, April 20, 1999; Online: http://java.sun.com/ docs/codeconv [Java Tutorial 2002] Sun The Java Tutorial, 2002; Online: http://java.sun. com/docs/books/tutorial/ [Java SDK Doc 2002] Sun JavaTM 2 SDK, Standard Edition, Version 1.4, 2002; Online: http://java.sun.com/j2se/1.4/docs/ index.html [Javadoc Homepage] Sun Javadoc Tool Home Page; Online: http://java. sun.com/j2se/javadoc [Oaks and Wong, 1999] Oaks and Wong (1999). Java Threads. O’Reilly Associates, Inc., 2nd edition. [PPrakt. Java Coding 2002] Programmierpraktikum SS2002 Java Coding Convention, 2002; Online: http://courses.iicm. edu/programmierpraktikum/skriptum/java_coding_ convention.pdf [Silberschatz and Galvin, 1998] Silberschatz and Galvin (1998). Operating System Concepts. Addison-Wesley Longman, Inc., 5th edition. 106 Index Übersetzen des Sourcecodes, 2 NaN, 30 %, 29 javax.swing, 73 boolean, 12 numerisch, 12 primitiv, 12 referenz, 16 Deadlocks, 99 default Wert, 25 Default-Parameter, 61 Dekrement post, 28 pre, 28 delete, 50 Destruktor, 50 do, 35 Dokumentationskommentar, 11 Drag and Drop, 73 Abhängigkeit von Klassen, 4 Ableitung, 42, 44, 63 abstract, 58 Access-Modifier, 53 Accessibility API, 73 ActionListener, 88 Allgemeine Container, 91 Anweisung, 34 ArithmeticException, 29 Array, 18 mehrdimensional, 21 Array Element, 26 atomare Operationen, 98 Ausdrücke auswerten, 30 Ausdruck, 34 Einfaches Applet, 8 equals, 62 event-dispatching thread, 90 Event-Handling, 82 Event-Listener Methoden, 84 Exceptionhandler Paramter, 26 Exceptions, 68 expression, 34 Bitoperatoren, 31 Block, 34 BorderLayout, 77 BoxLayout, 79 break, 39 Bytecode, 2 Fehlerbehandlung, 68 final, 24, 57, 58 finalize, 52 FlowLayout, 78 Flusskontrolle, 34 FocusListener/Adapter, 85 for, 35 full vs. short eval, 30 call-by-value, 60 cast, 32 ChangeListener, 88 Class Member, 56 Classpath, 5 Trennzeichen, 5 clone, 62 ComponentListener/Adapter, 85 const cast, 24 Container, 76 continue, 39 garbage collector, 50 GC, 50 GridBagLayout, 79 GridLayout, 78 GUI-Builder, 74 Daemon-Threads, 97 Datentyp, 12 has-a, 41 107 INDEX if, 37 if-operator, 37 Implementation Threads, 96 Info Komponenten, 92 Initialisierung, 58 Inkrement post, 28 pre, 28 inner classes, 62 instanceof, 32 Instanzvariable, 26 Interface, 42, 65 Interpretieren des Bytecodes, 2 is-a, 41 isAlive, 101 ItemListener, 88 Java 2D, 73 Java Foundation Classes (JFC), 73 Java und Windows 95, 3 join, 101 Kapselung, 42 KeyListener/Adapter, 86 Klasse, 41 abstrakt, 44 Definition, 43 Destruktor, 50 final, 44 Konstruktor, 45 Sichtbarkeit, 43 String, 17 StringBuffer, 17 Klassen in Java, 41 Klassenpfad, 5 Klassenvariable, 25 Kommandozeilen-Auswertung, 6 Kommentare, 10 Konstruktor, 45 Konstruktor Parameter, 26 Kontroll Komponenten, 92 Kontrollfluss, 34 label, 40 Layoutmanager, 77 Lebenszyklus Thread, 99 Lokale Variable, 26 main 108 argv vs. args, 8 Methode, 4 Maschinencode, 2 Member-Modifier, 52 Member-Variable, 42 Methode, 42 Methoden, 59 Methoden Parameter, 26 Model-View-Controller, 75 Modulo-Operator, 29 MouseListener/Adapter, 86 MouseMotionListener/Adapter, 87 MVC, 75 native, 58 nested classes, 62 new, 32 NoClassDefFoundError, 5 Object Member, 56 Objekt, 41 Operator, 28 instanceof, 32 new, 32 Overloading, 28 Reihenfolge Auswertung, 28 overloading, 42 overriding, 42 Packages, 66 Panes, 77 Parameter call-by-value, 26 Performancesteigerung, 2 Pluggable Look and Feel, 73 private, 53 protected, 53 public, 53 Referenz-Datentyp, 16 return, 40 Schlüsselwörter, 11 Sourcecode, 2 Spezielle Komponenten, 93 Sprungmarke, 40 statement, 34 super, 43, 49 Swing, 73 switch, 38 synchronized, 58, 98 INDEX System.arraycopy, 20 this, 41 thread-groups, 94 thread-pools, 94 Threads, 94 Top-Level Container, 90 Top-level Container, 76 toString, 62 transient, 57 try...catch, 68 User-Threads, 97 Variable default Wert, 25 Definition, 22 Deklaration, 22 Gültigkeitsbereich, 25 Initialisierung, 25 lokal, 26 Variablen-Modifier, 57 Vergleichsoperatoren, 30 volatile, 57 while, 35 WindowListener, 88 Zeichen von Komponenten, 89 Zeichenketten, 17 Zeiger, 12 Zuweisungsoperator, 32 109 Changelog Aktuelle Version: 14.08.2003. • Änderungen zur Version vom 13.06.2002: Datentypen: Bei den Beispielen diverse Anpassungen im Codingstyle. Swing: Swing und JFC... Swing: Verlinkungen der Beispiele zum Tutorial von Sun. • Änderungen zur Version vom 20.06.2002: Datentypen: Zeilennummern in Text adaptiert. • Änderungen zur Version vom 28.06.2002: Swing / Datenstrukturen: Hier hat Omar Elschatti viele Typos gefunden. Alle Abschnitte: Nina Schmuck hat seeehhhrr viele Typos und Beistrichfehler gefunden ;) • Änderungen zur Version vom 22.07.2002: Index: index-Markups wurden eingesetzt Folien: Verwendung von dino external slides.sty Anatomie: Erweiterungen zu “Vom Sourcecode zum Programm” Datentypen: Vergleiche zu C++; Umstellen einiger Abschnitte Klassen: Mehr Unterschiede zu C++; Umstrukturierung • Änderungen zur Version vom 14.05.2003: Swing: Weitere Hinweise und Kommentare. • Änderungen zur Version vom 22.05.2003: Anatomie: Zusätzlicher Hinweis, was alles schiefgehen kann... Klassen: Korrektur einiger Beispiele bez. Codingstandard. Threads: Umstrukturierungen • Änderungen zur Version vom 2.6.2003: 110 Anatomie: Und wieder: zusätzlicher Hinweis, was alles schiefgehen kann... Datentypen: Fehler bei der Beschreibung von Operatoren korrigiert. Klassen: Zusätzlicher Hinweis zu Packages. Swing: Abschnitt über MVC-Architektur. Korrekturen bei den Beispielen bez. Codingstandard :) (Tnx and Edi!) • Änderungen zur Version vom 2.6.2003: Alle Abschnitte: Edi Haselwanter hat vieeellle Textstellen korrigiert. • Änderungen zur Version vom 12.06.2003 Struktur: Die Links vom Index zur jeweiligen Textstellen haben nicht immer funktioniert. Ivan Maricic hat mir hierzu Hinweise gegeben, sodass diese Links nun funktionieren sollten. Swing, Threads: Edi Haselwanter war wieder fleißig :) 111