Yet Another Java Introduction (YAJI)

Werbung
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
Herunterladen