back to top   1 Einführung in Java

 

back to top   1.1 Was ist Java?

 

back to top   1.2 Entstehungsgeschichte

 

Das alte und neue Javalogo sowie Duke das Maskottchen

back to top   1.3 Die Java-Plattform

 

Prinzipiell kann, wie erwähnt, eine Java-Applikation auf jeder beliebigen Plattform zur Ausführung gebracht werden --- unabhängig davon welches Gerät die notwendige virtuelle Maschine implementiert.
So existieren Visionen und erste Umsetzungen von JVMs für Elektrogeräte wie Kaffeemaschinen, Kühlschränke, Radios etc.
Zur Erinnerung: die Intention der ursprünglichen Java-Enwicklung zielte nicht auf Desktop Rechner oder gar das Internet.

Aufbau der Java Plattform

Die Abbildung schematisiert den Aufbau der Java-Plattform. Von der konkreten Plattform der physischen Hardware (Prozessor, etc. und des darauf ausgeführten Betriebsystems) wird durch die plattformspezifische virtuelle Maschine abstrahiert.
In diese eingebettet ist das Java-Application Programmers Interface (Abk. API) --- die sog. Klassenbibliothek. Sie ist vollständig in Java realisiert, damit ist sie bereits als plattformunabhängiges Java-Programm ausgelegt. Das API liefert die wesentlichen Grundprimitven zur Erstellung leistungsfähiger Programme.
Auf dieser virtuellen Plattform läuft das (Benutzer) Java-Programm --- in Form des Bytecodes --- ab.


Hinweis: Beachten Sie die Unterscheidung zwischen Java-Plattform und Ausführungsplattform (=Betriebsystem und Hardware) einer Java-Applikation

Zur Applikationserstellung mit der Java-Plattform bietet SUN drei verschiedene Ausbaustufen (Editionen) an, die in der nachfolgenden Abbildung zusammengestellt sind.

Java-Editionen

Diese Editionen basieren alle auf demselben Java-Sprachkern, verfügen jedoch über verschiedene Standard-APIs, die auf die jeweils adressierte Problemstellung zugeschnitten sind. Im Einzelnen sind dies:

Wir werden uns nachfolgend auf die Betrachtung des Java-Sprachkernes, sowie ausgewählter APIs beschränken, überwiegend in allen drei Editionen, jedoch mindestens in J2EE und J2SE, zur Verfügung stehen.

Das Sun Java Development Kit (JDK)

Architektur des Java SDK (JDK) von SUN

Das JDK stellt eine Referenzimplementierung der kompletten Java-Plattform zur Verfügung.
Seine Hauptbestandteile sind:

Die Rückübersetzung des Compilates HelloWorld.class mit javap -c HelloWorld liefert:

(1)Compiled from HelloWorld.java
(2)public class HelloWorld extends java.lang.Object {
(3)    public HelloWorld();
(4)    public static void main(java.lang.String[]);
(5)}
(6)
(7)Method HelloWorld()
(8)   0 aload_0
(9)   1 invokespecial #1 <Method java.lang.Object()>
(10)   4 return
(11)
(12)Method void main(java.lang.String[])
(13)   0 getstatic #2 <Field java.io.PrintStream out>
(14)   3 ldc #3 <String "Hello World!">
(15)   5 invokevirtual #4 <Method void println(java.lang.String)>
(16)   8 return

Beispiel 1: Decompilierung mit javap

Anmerkung: Zum Investitionsschutz kommerziell erstellter Java-Software sind Applikationen verfügbar, die den generierten Bytecode so nachbearbeiten, daß eine Decompilierung deutlich erschwert wird, oder die Ausgabe unbrauchbar wird. Die Ausführbarkeit des Bytecodes wird hierdurch nicht beeinträchtigt.

Zusätzlich ist ein eigenständiger Java-Interpeter, das sog. Java Runtime Environment (Abk. JRE) verfügbar. Sein Minimalxaufruf lautet jre example. Diese Applikation ist nicht Bestandteil der Standard Java-Plattform (JDK v1.3) und ist separat kostenlos über die Sun Java-Homepage beziehbar.

back to top   1.4 Vom Quellcode zum lauffähigen Programm

 

Die Programmentwicklung vollzieht sich im klassischen edit-build-run-Zyklus. Als Besonderheit generiert der Java-Compiler je eine Bytecode-Datei (Dateiextension class) für jede zugreifbare Klasse innerhalb der Quelldatei.
Die entstehenden Class-Dateien werden durch die Laufzeitumgebung interpretativ ausgeführt.

Der Compilierungsvorgang

Im Beispiel enthält die Quellcodedatei HelloWorld.java die beiden Klassendateien HelloWorld und SayHello.
Diejenige Klasse, welche die Main-Methode beinhaltet muß identisch der sie enthaltenden Quelldatei benannt sein -- im Beispiel HelloWorld.
Mit javac HelloWorld.java wird die Datei übersetzt, und die beiden Class-Dateien HelloWorld.class und SayHello.class erzeugt.
Die Programmausführung erfolgt durch Absetzen von java HelloWorld auf der Kommandozeile. (Achtung! Keine Dateiextension angeben!)
Der Aufruf java SayHello führt hingegen wegen des Fehlens der Main-Methode in der Klasse SayHello zur Fehlermeldung Exception in thread "main" java.lang.NoSuchMethodError: main.

back to top   2. Syntax und Semantik der Programmiersprache Java

 

back to top   2.1 C, C++, C# und Java

 

Wie bereits in der Einführung angedeutet wurde Java nahe an der Syntax der verbreiteten hybrid objektorientiert-prozeduralen Sprache C++ entwickelt. Jedoch mit der Einschränkung, deren mitunter krypischen Syntax deutlich zu vereinfachen. Überdies wurde auf wesentliche dort anzutreffende Sprachmerkmale verzichtet.

Die Java Language Specification führt bereits im Vorwort aus, daß sich die Java-Sprachentwickler zwar in einer Vielzahl von Punkten an C und C++ orientierten, jedoch der Sprachaufbau stark von diesen Beiden Vätern abweicht. Aus praktischer Motivation heraus wurde beim Sprachdesign auf die Einführung neuer und ungetesteter Sprachelemente verzichtet.
Wie erwähnt ist Java als streng typisierte Sprache ausgelegt. Daher können die meisten Typfehler bereits zur Übersetzungszeit erkannt werden. Technisch gesehen meint strenge Typisierung (auch statische Typisierung), daß der Typ einer Variable während der Programmausführung nicht verändert werden kann. Durch statische Typanalyse währen des Compilierungsvorganges können Typfehler erkannt werden. Durch polymorphe Aufrufe kann es jedoch auch während der Programmausführung zu Typfehlern kommen, da hierbei der dynamische Typbindung zum Einsatz kommt.

Anders als C und C++ verfügt Java über keinerlei systemnahe Konstrukte. Direkte Hardwarezugriffe und Systemprogrammierung im Allgemeinen kann daher mit nicht realisiert werden.
Die Sprachväter begründen dies mit der Transparenz einer high-level-Sprache, die keinerlei Rückschlüsse auf die darunterliegende Hardware zulassen sollte.

Anders als C/C++ verfügt Java über automatische Speicherbereingung (engl. garbage collection), welche die fehlerträchtigen Speicheroperationen (insbesodnere free und delete) obsolet werden läßt. Konsequenterweise läßt Java die Allokation beliebiger Speicherbereiche nicht zu. Nicht mehr benötigte Speicherplätze (Variablen) können zwar durch den Programmierer (durch die explizite Zuweisung von NULL) als nicht mehr benötigt gekennzeichnet werden, Freigabefunktionen stellt die Sprache jedoch nicht zur Verfügung.

Bekannte, und berüchtigte, Fehlerquellen wie Arrayzugriffe ohne vorherige Indexprüfung, variabel lange Parameterlisten oder Zeiger nebst Zeigerarithmetik existieren nicht, und verleihen dem Sprachentwurf dadurch zusätzliche Sicherheit.
Der Verzicht auf (explizite) Zeiger zieht die Behandlung aller Methodenparameter als Werte (call-by-value) nach sich.
Ebenso wurde auf Umgebungseigenschaften wie den Präprozessor und Includedateien vollständig verzichtet wurde. die Strukturierung des Java-Quellcodes kann über ein eigenes Paketkonzept erfolgen.

Nützlichkeiten wie die Lockerung des Variablendefinitionszwanges am Blockanfang wurden beibehalten. Selbiges gilt für die Aufhebung der Trennung zwischen Deklaration und Definition bei einfachen Variablen, wie sie bereits in anderen Sprachen verwirklicht ist. Für Objekte existiert diese Trennung -- sinnvollerweise -- weiterhin fort.

Hinsichtlich objektorientierter Konzepte geht Java deutlich über C++ hinaus. Dergestalt, daß weder klassenlos Programme entwickelt werden können, noch Structs und Unions als Zwitter zwischen Variablen und Klassen implementiert sind. Die Prämisse strengerer Objektorientierung erklärt auch das Verbot globaler Variablen und Methoden. Allerdings sind die primitiven skalaren Datentypen (wie int, char, etc.) nicht als Objekte realisisiert.
Analog C++ können Methodennamen überladen werden (Ad-hoc Polymorphie). Zur Eindeutigkeitsidentifikation wird die Signatur (gebildet aus Methodennamen und übergabeparametern) herangezogen. Die überladung von Operatoren, wie in C++ möglicht, ist nicht vorgesehen.
Der in C++ realisierte Vererbungsmechanismus wurde unter der Einschränkung übernommen, Mehrfachvererbung zu verbieten. Um trotz dieser Restriktion sinnvolle Anwendungsentwicklung betreiben zu können wurde Java als zusätzliches Sprach-Konzept die Schnittstelle (engl. Interface) hinzugefügt. Hiervon können eine Klasse beliebig viele implementiert werden.
Im Gegensatz zu C++ erben Klassen ohne ausdrücklich angegebene Elternklasse automatisch per Vorgabe von java.lang.Object.

Parametrische Polymorphie in Form von Templates, wie in C++ anzutreffen, ist in Java erst ab der Sprachversion 1.5 realisiert.

Von C++ wurde die Fehlerbehandlung in Form von Ausnahmen (engl. exceptions) übernommen. Hierdurch können Fehlerereignisse zentralisiert behandelt werden. Zusätzlich wird der Kontrollfluß von Fehlerbehandlungscode bereinigt und dadurch übersichtlicher.

Offensichtlich ist die übernahme des Kommentierungsstils von C und C++. So können alle Kommentarkonstrukte wie in den vorgenannten Sprachen üblich eingesetzt werden.
Zusätzlich wird ein separater Kommentarstil (/**...*/) für Quellen automatisierter Dokumentation angeboten. Zwischen den so abgegrenzten Regionen können vorgegebene Marken plaziert werden; diese werden beispielsweise durch das JDK-Werkzeug javadoc verarbeitet.

Augenfälligstes abgrenzendes Merkmal von Java gegenüber C/C++ ist es, daß kein plattformspezifischer Maschinencode als Resultat des Compilierungsvorganges erzeugt wird, sondern binärer Bytecode genannte Zwischenrepräsentation. Diese wird durch die Java Virtual Machine zur Ausführungzeit interpretativ abgearbeitet.
Anmerkung: Bytecode ist nicht Java-spezifisch, sondern kann auch durch andere Sprachen erzeugt werden, was mit unter auch geschicht.

back to top   Microsofts C#

 

Die durch Microsoft entwickelte Sprache C# (sprich: C sharp) weißt einige interessante Parallelen zu Java auf, führt jedoch auch neue Konzepte ein.
Jenseits der Einordnung in die Komponentenarchitektur der .NET-Plattform, welche nur schwer mit der der Java-Plattform (insbesondere der J2EE-Plattform) verglichen werden kann, stellt C# jedoch eine vollwertige -- sehr stark an Java orientierte -- Programmiersprache dar. Bereits das, zu erwartende, Standardbeispiel der HelloWorld-Applikation läßt die enge Verwandschaft, abzulesen an der Syntax deutlich werden (Vergleiche HelloWorld in Java, in in C#).
Im Gegensatz zu C# ist Java auf der Programmierebene nicht rein objektorientiert wie beispielsweise SmallTalk. So treten auch hier die primitiven Datentypen als nicht-objektartige Werte auf.

Ebenso wie in Java ist ausschließlich Einfachvererung zugelassen, ergänzt wird diese um Schnittstellen. Auch C# erlaubt es einer Klasse mehrere Schnittstellen zu implementieren. Die Syntax unterscheidet jedoch nicht mehr explizit zwischen Schnittstellenimplementierung und Erben von einer Klasse (siehe Beispiel). Unverändert zu Java wird auch jede Klasse, die über keine Elternklasse verfügt automatisch als Subklasse von object eingeordnet.
Blattknoten einer Vererbungshierarchie (d.h. Klassen für die durch den Programmierer vorgegeben wird, sie sollen nicht weiter durch Ableitung spezialisiert werden) können mit dem Schlüsselwort sealed -- in Java: final -- gekennzeichnet werden.

Zusätzlich zu Klassen beinhaltet C#, wieder, den Sprachmechanismus der Struktur (struct). Hierüber wird die Differenzierung zwischen Wert- und Referenztypen realisiert. Während Klassen, Java-konform Referenztypen bezeichnen, übernehmen Strukturen die der Werttypen. Desweiteren werden Structs durch den Compiler als Bestandteile umgebender Objekte übersetzt. Durch diese Charakteristika unterscheiden sich C#-Structs stark von den dortigen Klassenstrukturen, weshalb sie auch nicht analog zu deren Schema verwirklicht sind. Strukturen können weder erben noch vererbt werden, lediglich die Möglichkeit Schnittstellen zu implementieren bleibt erhalten.

Deutlich komfortabler als in Java fällt jedoch der Umgang mit Referenz- und Werttypen aus. Das in C# angebotene boxing und unboxing übertrifft deutlich die Benutzungsfreundlichkeit der Java-Wrapperklassen (Beispiel). Dieser Mißstand wurde jedoch in der Javasprachversion 1.5 behoben, die dieses Konzept ebenfalls einführt.

Erstmals reichert C# auch den Sprachumfang C/C++-basierter Sprachen um neue syntaktische Konstrukte an. Hierunter fallen beispielsweise Schlüsselworte zur Definition von Delegationsobjekten, die als solche explizit im Programmcode deklariert werden können.

Wie Java compiliert auch C# in eine Zwischenrepräsentation. Allerdings mit dem Unterschied, daß diese nicht interpretativ abgearbeitet wird, sondern erneut in maschinenspezifisches natives Format übersetzt wird.

Die Tabelle stellt einige Sprachmerkmale von C++, Java und C# gegenüber
(Tabelle in Anlehnung an: Eisenecker, U.: Dissonanz oder Wohlklang, in: iX 9/2000, Hannover, 2000, p. 48-51)

Sprachmerkmal
C++
Java
C#
Automatische Speicherbereinigung
(garbage collection)
nicht unterstützt
unterstützt
unterstützt
Coercion
(Typumwandlungspolymorphie)
unterstützt
unterstützt
unterstützt
Globale Methoden
unterstützt
nicht unterstützt
nicht unterstützt
Inklusionspolymorphie
unterstützt
unterstützt
unterstützt
Operatoroverloading
unterstützt
nicht unterstützt
unterstützt
Parametrische Polymorphie
(Templates)
unterstützt
Referenztypen
unterstützt
unterstützt
unterstützt
Überladungspolymorphie
unterstützt
unterstützt
unterstützt
Werttypen
unterstützt
unterstützt
unterstützt
unified type system
Zeiger
unterstützt
nicht unterstützt
unterstützt

(1) Ab Version 1.5 für Klassen der Collection API im Sprachumfang enthalten.
(2) Die Aufnahme in die Sprache ist für die Nachfolgeversion des .NET-Framworks 1.1 geplant.
Einen Ausblick auf die geplanten Sprachmerkmale liefert das Projekt CLRGEN von Microsoft Research.

back to top   2.2 Grundstrukturen

 

back to top   2.2.1 Programmaufbau

 

Bereits am HelloWorld-Beispiel wird der wesentliche Aufbau einer Javaquellcodedatei sichtbar.

(1)public class Minimal {
(2)   public static void main(String[] args) {
(3)      //..
(4)   } //main()
(5)} //class Minimal

Beispiel 2: Minimales Java-Programm

Zunächst erfolgt die Definition einer Klasse; im Beispiel Minimal.
Vor dem Schlüsselwort class ist die Sichtbarkeit auf public festgelegt, was eine allgemeine Sichtbar- und Zugrifbarkeit der Klasse bewirkt.
Im Gegensatz zu C++ ist jedoch die Klassendefinition zwingend erforderlich! Es ist nicht möglich Javaquellcode, der keine Klassendefinition enthält, zu übersetzen. Hingegen ist es ohne weiteres möglich gewöhnliche C-Programme mit einem C++-Compiler zu übersetzen.

Darüberhinaus ist es zwingend vorgeschrieben die Quellcodedatei identisch zur public deklarierten Klasse zu benennen.

Eine weitere Besonderheit gegenüber C++ stellt die Plazierung der main-Funktion dar. Während diese in C++, selbst bei der Verwendung von Klassen (siehe Beispiel (HelloWorld.cpp)), außerhalb jeder Klasse angeschrieben werden muß, um automatisch beim Programmstart ausgeführt zu werden, erzwingt Java die main-Methode innerhalb einer public deklarierten Klasse.

Die Signatur der main-Methode ist mit public static void main(String[] args) fixiert. Davon Abweichende Spezifikationen sowohl in Rückgabewert als auch Parameterliste, können zwar in Bytecode übersetzt werden, jedoch wird diese abweichende Main-Methode nicht mehr automatisch durch das Laufzeitsystem aufgerufen.
Auch hier zeigt sich Java deutlich restriktiver als C/C++, die beide beliebige Rückgabetypen und -- in Grenzen -- leicht variierende Parameterlisten zulassen.

Abschließend: Innerhalb der main-Methode der public deklarierten Klasse kann der auszuführende Code angegeben werden.

back to top   2.2.2 Einfache Datentypen

 

Wie herkömmliche prozedurale Programmiersprachen auch stellt Java primitive Datentypen zur Verfügung. Im Gegensatz zu manchen etablierten objektorientierten Programmiersprachen (z.B. SmallTalk) sind diese Datentypen in Java intern nicht als Objekte realisiert.

Das Java-Typsystem

Im Einzelnen werden angeboten:

Datentyp
Erklärung
Wertebereich
Wahrheitswert
true oder false
Einfaches Zeichen aus dem 16-Bit Unicode Zeichensatz
8-bittige vorzeichenbehaftete Ganzzahl
-27 bis 27-1
-128 <= byte <= 127
16-bittige vorzeichenbehaftete Ganzzahl
-215 bis 215-1
-32768 <= short <= 32767
32-bittige vorzeichenbehaftete Ganzzahl
-231 bis 231-1
-2147483648 <= int <= 2147483647
64-bittige vorzeichenbehaftete Ganzzahl
-263 bis 263-1
-9223372036854775808 <= long <= 9223372036854775807
32-bittige Gleitkommazahl (nach IEEE 754-1985)
Größtmögliche positive Zahl: 3.40282347e+38f
Kleinstmögliche positive Zahl: 1.40239846e-45f
64-bittige Gleitkommazahl (nach IEEE 754-1985)
Größtmögliche positive Zahl: 1.79769313486231570e+308
Kleinstmögliche positive Zahl: 4.94065645841246544e-324
Object
Objektwertiger Datentyp. Jedes Objekt hat (implizit) diesen Typ.

Siehe Java Language Specification

Anmerkungen:

(1)public class ConstFormats {
(2)	public static void main(String[] args) {
(3)		int i = 052;		//octal
(4)		System.out.println("i as decimal = "+i);
(5)
(6)		i = 0x2a;			//hexadecimal
(7)		System.out.println("i as decimal = "+i);
(8)
(9)		long l = 0x2aL;	//hexadecimal long
(10)		System.out.println("l as decimal = "+l);
(11)
(12)		float f = 0x2af;	//hexadecimal float
(13)		System.out.println("f as decimal = "+f);
(14)	} //main()
(15)} //class ConstFormats

Beispiel 3: Verschiedene Deklarationen und Wertebelegungen   ConstFormats.java

Das Programm liefert folgende Ausgabe:

i as decimal = 42
i as decimal = 42
l as decimal = 42
f as decimal = 687.0

VORSICHT! Die hexadezimale Definition des Floatwertes liefert nicht -- wie vielleicht intuitiv zu erwarten -- 42 als ganzzahligen Anteil, sondern die Gleitkommainterpretation des hexadezimal spezifizierten Bitmusters.

Als streng typisierte Sprache muß jede Variable in Java mit einem Typ deklariert werden, ungetypte Variablen existieren nicht.
Nach der Deklaration in einem Programm wird der Variableninhalt auf einen resiervierten, für den Programmierer nicht zugänglichen Wert undefined gesetzt. Hierdurch kann der Compiler lesende Referenzierungen vor Wertdefinition zur Übersetzungszeit erkennen.
Die Deklaration geschieht, angelehnt an C/C++ durch Angabe des Typs gefolgt durch den Variablennamen. Optional kann dieses Statement durch die Festlegung eines Vorgabewertes ergänzt werden.

(1)public class NotInitialized {
(2)	public static void main (String[] args) {
(3)		int i;
(4)		System.out.println("i= "+i); //i is not initialized yet
(5)	} //main()
(6)} //class notInitialized

Beispiel 4: Nicht initialisierte Referenzierung   NotInitialized.java

Liefert beim Übersetzen die Fehlermeldung:

NotInitialized.java:6: variable i might not have been initialized
                System.out.println("i= "+i); //i is not initialized yet
                                         ^
1 error
(1)public class CharArithmetic {
(2)	public static void main (String[] args) {
(3)		char c = 'a';
(4)
(5)		System.out.println("c = "+c);
(6)		c++;
(7)		System.out.println("c = "+c);
(8)
(9)		int i = c;
(10)
(11)		System.out.println("i = "+i);
(12)		System.out.println("i = "+ (char) i);
(13)	} //main()
(14)} //class CharArithmetic

Beispiel 5: Verwendung von char als numerischer Typ   CharArithmetic.java

Das Programm liefert bei der Ausführung folgende Ausgabe:

c = a
c = b
i = 98
i = b

back to top   2.2.3 Operatoren

 

Java bietet 37 verschiedene Operatoren an, die ihrer Semantik nach mit denen von C/C++ weitestgehend identisch sind (siehe Language Specification).

Je nach Typ auf dem Operator definiert ist, wird unterschieden zwischen: Integralen-, Fließkomma-, Boole'schen- und objektwertigen-Operatoren.

back to top   Operatoren auf integralen Typen (siehe Java API Specification)

 

Diese built-in Operatoren nehmen keinerlei Fehlerprüfung oder -meldung vor. Lediglich die beiden Integerdivisionsoperatoren / und % werfen im Fehlerfalle eine ArithmeticException-Ausnahme falls der Divisor gleich Null ist (siehe Java Language Specification bzw. siehe Java Language Specification).

back to top   Operatoren auf Fließkommatypen (siehe Java Language Specification)

 

Auf diesen Typen sind die auch auf integralen Typen zugelassenen arithmetischen-, numerischen Vergleichs-, Inkrement- und Dekrement-Operatoren verfügbar. Ferner existiert der numerische Cast (siehe Java API Specification).

Auch die Fließkommaoperatoren lösen keine Exceptions aus. überlauf wird als positiv Unendlich, Unterlauf entsprechend als negativ Unendlich dargestellt. Liefert die Operation kein mathematisch interpretierbares Resultat, wird der Wert auf NaN gesetzt. In der Konsequenz resultiert auch NaN falls ein so gesetzter Operand erneut verknüpft wird. Dies gilt nicht für die Gleichheitsoperation.

Anmerkung: Identisch zu ANSI C/C++ wurde der unäre Operator + lediglich aus Symmetriegründen zum unären eingeführt.

back to top   Operationen auf Boole'schen Typen

 

Auf Boole'schen Typen sind alle relationalen und logischen Operatoren zugelassen.
Als Operand des Konditionaloperators können neben Boole'schen Werten auch Integralwerte angegeben werden. Diese werden gemäß C-Konvention zu true konvertiert sofern sie ungleich Null sind, andernfalls zu false.

back to top   Operatoren auf objektwertigen Typen

 

back to top   Schlussbemerkungen

 

back to top   Operatorpräzedenz

 

Es gelten folgende Operatorpräzedenzen (geordnet von oben (entspricht höchster Präzedenz) nach unten (niedrigster Präzedenz)):

Operator
Symbol
Postfix-Operatoren
[] . (params) expr++ expr--
unäre Operatoren
++expr --expr +expr -expr ~ !
Erzeugung oder Typumwandlung
new (type )expr
Multiplikationsoperatoren
* / %
Additionsoperatoren
+ -
Verschiebeoperatoren
<< >> >>>
Vergleichsoperatoren
< > <= >= instanceof
Gleichheitsoperatoren
== !=
Bitoperator Und
&
Bitoperator exklusives Oder
^
Bitoperator inklusives Oder
|
logisches Und
&&
logisches Oder
||
Konditionaloperator
? :
Zuweisungsoperatoren
= += -= *= /= %= >>= <<= >>>= &= ^= |=

back to top   Automatische Typkonversion

 

Die automatische Typkonversion wird durch den Compiler bzw. das Laufzeitsystem immer dann angewandt, wenn nicht alle Operanden typgleich sind. Dies ist beispielsweise bei der Multiplikation einer Int-Zahl mit einem Fließkommawert der Fall.
Die automatische Typumwandlung ist im Wesentlichen identisch zur in C/C++ realisierten ausgelegt.

byte
char
short
int
long
float
double
boolean
byte
int
int
int
int
long
float
double
nicht unterstützt
char
int
int
int
long
float
double
nicht unterstützt
short
int
int
long
float
double
nicht unterstützt
int
int
long
float
double
nicht unterstützt
long
long
float
double
nicht unterstützt
float
float
double
nicht unterstützt
double
double
nicht unterstützt
boolean
boolean

Die Tabelle stellt die automatischen Typkonversionen zur Festlegung des Ausdruckstyps dar. Da alle Operatoren kommutativ sind, ist die Tabelle symmetrisch zur Hauptdiagonale (nur die obere Dreiecksmatrix ist aus übersichtlichkeitsgründen ausgefüllt).
Alle Typkonversionen werden statisch und operatorunabhängig durchgeführt, d.h. mögliche Typfehler werden zur Compilezeit erkannt und gemeldet.

back to top   2.3 Kontrollstrukturen

 

back to top   2.3.1 Selektion und Mehrfachselektion -- das if und case-Statement

 

Das if-Statement ist in Java analog der aus C/C++ bekannten Semantik und Syntax realisiert (Siehe Language Specification):

IfThenStatement:
   if ( Expression ) Statement

IfThenElseStatement:
   if ( Expression ) StatementNoShortIf else Statement

IfThenElseStatementNoShortIf:
   if ( Expression ) StatementNoShortIf else StatementNoShortIf
(1)public class IfTest {
(2)	public static void main(String[] args) {
(3)		int i = 42;
(4)
(5)		if (1==1)
(6)			if (i < 50)
(7)				i++;
(8)			else
(9)				i--;
(10)	} //main()
(11)} //end class IfTest

Beispiel 6: If-Statement mit dangling else   IfTest.java

Leider wurde in Java die Chance zur Ausmerzung des lästigen und fehlerträchtigen dangling else-Problems nicht genutzt. Daher wird -- konform zu C/C++ -- ein else immer dem letzten vorhergehenden if zugeordnet.
Strenger im Vergleich zu C/C++ ist hingegen die Typisierung der Expression als Bedingung innerhalb der Verzweigung gefaßt. Hier ist zwingend der Typ boolean erforderlich.

Auch das switch-Statement ist identisch zum C/C++-Analogon realisiert.
Ebenso wie dort müssen die Alternativzweige mit einem expliziten break abgeschlossen werden.
Innerhalb der case-Verzweigungen sind nur konstante Ausdrücke der Typen char, byte, short oder int zugelassen. (siehe Java Language Specification).

Syntax:

SwitchStatement:
   switch ( Expression ) SwitchBlock

SwitchBlock:
   { SwitchBlockStatementGroupsoptional SwitchLabelsoptional }

SwitchBlockStatementGroups:
   SwitchBlockStatementGroup
   SwitchBlockStatementGroups SwitchBlockStatementGroup

SwitchBlockStatementGroup:
   SwitchLabels BlockStatements

SwitchLabels:
   SwitchLabel
   SwitchLabels SwitchLabel

SwitchLabel:
   case ConstantExpression :
   default :

Beispiel eines switch-Statements:

(1)public class SwitchTest {
(2)	public static void main(String[] args) 	{
(3)		int i = 42;
(4)
(5)		here:
(6)		switch (i) {
(7)			case 0:	i++;
(8)						break here;
(9)			case 1:
(10)			case 2:	i--;
(11)						break;
(12)			default:
(13)						i *= 10;
(14)		} //switch
(15)	} //main()
(16)} //class SwitchTest

Beispiel 7: Switch-Statement   SwitchTest.java

Anmerkungen:

back to top   2.3.2 Iteration -- for-, do-while-Schleifen

 

Java bietet die bereits in C/C++ eingeführten Schleifenkonstrukte

an.

Die for-Struktur besteht aus drei optionalen Komponenten: Initialisierung(en, Fortsetzungsbedingung(en) und Wertaktualisierung(en) (auch Reinitialisierung(en)).
Ebenso wie in C/C++ sind diese durch Semikola voneinander abgetrennt.
Im Initialisierungs- und Fortsetzungsteil sind mehrere, durch Komma separierte, Ausdrücke zuglassen. Im Fortsetzungsbedingungsteil hingegen nicht! Hier müssen mehrere Bedingungen durch logisches Und (&&) verbunden werden.

(1)public class ForTest {
(2)	public static void main(String[] args) {
(3)		for (int i=1, j=-2; i <= 10 && j <=0 ; i+=2, j++)
(4)			System.out.println("i = "+i+"  j = "+j);
(5)	} //main()
(6)} //class ForTest

Beispiel 8: Eine for-Schleife   ForTest.java

Alle Schleifen können vorzeitig, d.h. trotz gültiger Fortsetzungsbedingung(en) wahlfrei mit der break-Anweisung verlassen werden.
Generell versucht die break-Anweisung hinter dem nächstliegenden (d.h. innsteren erreichbaren) schließenden Schleifenkonstrukt fortzusetzen. Der Einsatz dieser Anweisung außerhalb der beschriebenen Strukturen wird per übersetzungsfehler verhindert.

Zusätzlich können Sprungziele durch Marken explizit benannt werden. Diese labels werden durch einen eineindeutigen Namen abgeschlossen von einem Doppelpunkt symbolisiert.
Es sind jedoch nur „Rückwärtssprünge“ möglich. (siehe Java Language Specification)

(1)public class BreakTest {
(2)	public static void main(String[] args) {
(3)		myLabel:
(4)		for (int i=1; i<=100; i++) {
(5)			for(int j=1; j<=100; j++) {
(6)				if (i*j >= 42) {
(7)					System.out.println("exiting loop");
(8)					break myLabel;
(9)				} //if
(10)			} //for
(11)		}//for
(12)		System.out.println("continuing...");
(13)	} //main()
(14)} //class BreakTest

Beispiel 9: Verlassen einer Schleife mit break   BreakTest.java

Im Gegensatz zu vielen prozeduralen Programmiersprachen verfügt Java über kein goto-Statement welche beliebige Sprünge erlauben würde.
Allerdings scheint beim Sprachdesign durchaus an diese Möglichkeit gedacht worden zu sein. Findet sich doch goto in der Aufzählung der reservierten Schlüsselworte (siehe Java Language Specification) und im Index (siehe Java Language Specification).
Die Referenzimplementierung des Javacompiliers von SUN nutzt das Schlüsselwort lediglich um den illegalen Beginn eines Ausdrucks zum Übersetzungszeitpunkt anzuzeigen.

Zum vorzeitigen Rücksprung aus dem Schleifenkörper ist das das continue-Statement definiert.
Nach seiner Ausführung erfolgt unverzüglich die Auswertung der Fortsetzungsbedingung, unter Auslassung aller folgenden Anweisungen des aktuellen Blocks.
Analog der break-Anweisung kann die Schachtelungstiefe in der fortgefahren werden soll durch Angabe eines Labels gesteuert werden. (siehe Java Language Specification)

Kopf- und Fußgesteuerte Schleifen, werden mit der while ... do-Konstruktion analog zu C/C++ realisiert.
Auch hier wird der Typ der Bedingung bereits zur Übersetzungszeit auf boolean geprüft.

Häufige Anwendungsform von Schleifen ist die Traversierung einer Objektmenge.
Das nachfolgende Beispiel zeigt die „klassische“ Vorgehensweise zur Ausgabe aller Elemente einer Aufzählung. Hierzu wird zunächst Elementanzahl ermittelt und anschließend in einer for-Schleife, beginnend mit der kleinsten Indexnummer (0) bis zur ermittelten Obergrenze inkrementiert. An jeder Indexposition wird das unter dieser Ordnungsnummer abgelegte Element ausgegeben.
Der Vorgehensweise unterliegt den beiden Grundannahmen, daß die Werte einerseits kontinuierlich (d.h. keine Indexposition ist unbesetzt) abgelegt sind. Und zweitens, daß die Wertemenge ab der Indexposition 0 aufsteigend abgelegt ist. Beide Annahmen können für die durch die Java-Standardklasse Vector realisierte Aufzählung als erfüllt angenommen werden.

(1)import java.util.Vector;
(2)
(3)public class NaiveIteration {
(4)	public static void main(String[] args) {
(5)		Vector v = new Vector();
(6)		v.add(new String("Berta"));
(7)		v.add(new String("Anna"));
(8)		v.add(new String("Cäsar"));
(9)
(10) 		for (int i=0; i<v.size(); i++) {
(11)      	System.out.println(v.get(i));
(12)     	} //for
(13)   } //main()
(14)} //class NaiveIteration

Beispiel 10: Naive Traversierung einer Objektmenge   NaiveIteration.java

Die Lösung leistet zwar das gewünschte, jedoch wirkt die Schleifenkonstruktion unnötig komplex. Überdies zwingt sie den Programmierer Daten abzuspeichern (in Form des Schleifenzählers i sowie der implizit angeforderten unbenannten Speicherstelle zur Ablage der Elementanzahl), die nur zum Zweck der Mengentraversierung benötigt werden.

Um die Umsetzung dieser häufig auftretenden Standardsituation zu vereinfachen bietet die Standard-API die Schnittstelle Iterator an. Sie entbindent den Programmierer von der expliziten Ermittlung der Elementanzahl, sowie der indexgebundenen Traversierung.

Das nachfolgende Beispiel zeigt die Integration des Iterator-basierten Ansatzes für die bekannte Mengentraversierungsaufgabe

(1)import java.util.Iterator;
(2)import java.util.Vector;
(3)
(4)public class IteratorTest {
(5)	public static void main(String[] args) {
(6)		Vector v = new Vector();
(7)		v.add(new String("Berta"));
(8)		v.add(new String("Anna"));
(9)		v.add(new String("Cäsar"));
(10)
(11) 		for (Iterator i = v.iterator() ; i.hasNext() ;) {
(12)      	System.out.println(i.next());
(13)     	} //for
(14)   } //main()
(15)} //class IteratorTest

Beispiel 11: Iterator-basierte Traversierung einer Objektmenge   IteratorTest.java

Die Lösung enthebt den Programmierer zwar vom Aufwand die Elementanzahl explizit zu ermitteln und einem Speicherplatz zuzuweisen, jedoch wird mit dem Iterator-Objekt immernoch benannter Speicher (in Form der Variable e) angefordert, der ausschließlich der schleifeninternen Logik dient.

Erweiterte Schleifen-Syntax

Zur Behebung des Übelstandes der unnötigen expliziten Informationsermittlung im vorigen Beispiel existiert seit Java Version 1.5 eine abkürzenden Schreibweise für Mengentraversierungen.

(1)import java.util.Enumeration;
(2)import java.util.Vector;
(3)
(4)public class NewIteration {
(5)	public static void main(String[] args) {
(6)		Vector v = new Vector();
(7)		v.add(new String("Berta"));
(8)		v.add(new String("Anna"));
(9)		v.add(new String("Cäsar"));
(10)
(11) 		for (Object o : v) {
(12)      	System.out.println( o );
(13)     	} //for
(14)   } //main()
(15)} //class NewIteration

Beispiel 12: Traversierung einer Objektmenge   NewIteration.java

Das Beispiel zeigt die alternative Schreibweise, welche keine unötigen, d.h. im Schleifenrumpf nicht benötigten, Daten ermittelt. Der Ausdruck innerhalb der for-Klammen typisiert die Elemente der Menge v als Object und weist sie temporär (konkret: für jeden Schleifendurchlauf einen Wert) der Variable o zu.

Die Entnahme aus der Objektmenge erfolgt unter Nutzung des allgemeinen Typs Object anstatt direkt auf den konkreten Typ String zurückzugreifen. Diese Asymmetrie erklärt sich aus Nutzung der Klasse Vector ohne Verwendung der in der Programmiersprache vorhandenen Generizitätsmechanismen. Nähere Ausführungen zu den damit verbundenen Möglichkeiten finden sich im Kapitel Parametrische Polymorphie/Generics.

back to top   2.3.3 Ausnahmen und ihre Behandlung -- Exception Handling

 

Die Ausnahmebehandlung von Java ist konzeptionell und syntaktisch eng and das Pendant des ANSI-C++-Standards angelehnt. (siehe Java Language Specification).

Drei generelle Vorteile ergeben sich durch Verwendung von Ausnahmen:

Die generelle Syntax kann wie folgt beschrieben werden:

try {
   //statements which may throw an exception
   //... or create and throw an exception object manually
} catch (exceptionFoo e) {
   //handling of exception of type exceptionFoo
   //exception object is named e
} catch (exceptionBar e) {
   //handling of exception of type exceptionBar
   //exception object is named e
} catch (Exception e) {
   //handling all exceptions not handled by specialized handlers yet
   //exception object is named e
} finally {
   //executed regardless the execution state of the previous try block
}//finally
(1)public class ExceptionHandlingTest1 {
(2)	public static void main(String[] args)	{
(3)		for (int i=2; i>=0; i--)
(4)			System.out.println(42/i);
(5)	} //main()
(6)} //class ExceptionHandlingTest1

Beispiel 13: Exception auslösender Code   ExceptionHandlingTest1.java

Das Beispiel illustriert das Auftreten einer java.lang.ArithmeticException, verursacht durch die Division durch Null.

Alle in einen try-Block eingeschlossenen Anweisungen werden „überwacht“ ausgeführt. Das bedeutet, es erfolgt kein Programmabbruch beim Auftreten einer Ausnahme, sondern der direkte Ansprung eines exception handlers.
Exception Handler werden durch catch-Blöcke implementiert. Der Typ der zu behandelden Exception wird als übergabeparameter angegeben. Das Laufzeitsystem sorgt für Auswahl der korrekten (d.h. zuständigen) Behandlungsroutine.

Alle Ausnahmen erben von der Klasse Exception. Hierdurch kann auch ein Vorgabe-Exception-Handler installiert werden. Das folgende Beispiel zeigt diesen im Anschluß an die Behandlungsroutine der ArithmeticException Ausnahme. Eine übersicht der in der Java-API definierten Ausnahme findet sich in: (siehe Java API Specification)
Der Ausdruck catch (Exception e) übernimmt hierbei die Rolle des aus C++ bekannten catch(...).

(1)public class ExceptionHandlingTest2 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			for (int i=2; i>=0; i--)
(5)				System.out.println(42/i);
(6)		} catch (java.lang.ArithmeticException e) {
(7)			System.out.println("an arithmetic exception was thrown -- aborting program");
(8)			System.out.println("Exception's message "+e.getMessage() );
(9)			System.out.println("Stack trace: ");
(10)			e.printStackTrace();
(11)
(12)		}
(13)		catch (Exception e) {
(14)			System.out.println("an exception was thrown -- aborting program");
(15)		} finally {
(16)			System.out.println("finished try block");
(17)		} //finally
(18)	} //main()
(19)} //ExceptionhandlingTest2

Beispiel 14: Ausnahmebehandlung   ExceptionHandlingTest2.java

Das Programm liefert die Ausgabe:

$java ExceptionHandlingTest1
21
42
an arithmetic exception was thrown
finished try block

Der optional angebbare finally-Block wird immer ausgeführt, unabhängig davon ob der von try umschlossene Anweisungsteil erfolgreich (d.h. fehlerfrei) oder durch Exception (oder auch sonstige Sprungmechanismen wie break) verlassen wurde. Er bietet die Gelegenheit nach erfolgter Ausnahmebehandlung „Aufräumarbeiten“, wie das Schließen möglicherweise noch geöffneter Dateien, durchzuführen.

Nützliche Zusatzinformationen über die Art des aufgetretenen Ausnahmeereignisses können durch die Methoden getMessage() und printStackTrace() abgefragt werden. Beide Methoden sind von java.lang.Throwable, der Superklasse von java.lang.Exception, ererbt und stehen daher auf allen Ausnahmeobjekten zur Verfügung.

Ausnahmen die als Subklassen von RuntimeException realisiert sind müssen nicht explizit deklariert oder aufgefangen werden, da sie von Instruktionen der virtuellen Maschine erzeugt werden. Dies stellt einen Widerspruch zur erhobenen Forderung dar, daß alle Ausnahmen, die Subklassen von Throwable sind, explizit zu deklarieren und aufzufangen sind!
Eine korrekte Deklaration wäre jedoch unter praktischen Gesichtspunkten nicht praktikabel, da beispielweise ein Fehler des Typs InternalError potentiell durch jede Methode erzeugt werden kann.
Abgesehen davon verhalten sich jedoch Laufzeit-Ausnahmen jedoch wie „normale“ Exceptions. Begründet durch Abwesenheit einer expliziten Deklaration sieht man auch -- außer in raren und begründeten Ausnahmefällen -- vom Auffangen und Behandeln durch den Programmierer ab.

Erzeugen eigener Ausnahmeereignisse
Neben durch das Laufzeitsystem generierten Ausnahmeereignissen können auch innerhalb des Programmcodes gezielt Exceptions durch den Anwender generiert werden. Hierfür existiert das throw-Statement.

(1)public class OwnException1 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			System.out.println("throwing an arithmetic exception...");
(5)			throw new ArithmeticException();
(6)			//never gets here
(7)		} //try
(8)		catch (Exception e) {
(9)			System.out.println(e.toString() + " exception caught");
(10)		} //catch
(11)	} //main()
(12)} //class OwnException1	

Beispiel 15: Anwenderdefiniert ausgelöste Ausnahme   OwnException1.java

Das Codebeispiel zeigt das manuelle Erzeugen einer ArithmeticException.
Anmerkung: Nach throw angegebene Anweisungen können niemals erreicht werden, und führen bereits zum Übersetzungszeitpunkt zu einer entsprechenden Fehlermeldung.

Das bestehende vorgegebene System der Exceptions kann durch den Programmierer jederzeit um eigendefinierte Ausnahmen erweitert werden. Voraussetzung hierfür ist die Definition einer neuen Ausnahmeklasse; dies geschieht (im einfachsten Falle) durch Erben von java.lang.Exception.
Das nachfolgende Beispiel illustriert dies:

(1)public class OwnException2 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			System.out.println("throwing myException...");
(5)			throw new myException();
(6)		} catch (Exception e) {
(7)			System.out.println(e.toString() + " exception caught");
(8)		} //catch
(9)	} //main()
(10)} //class OwnException2
(11)
(12)class myException extends Exception {
(13)} //class myException

Beispiel 16: Eigendefinierte Ausnahme   OwnException2.java

Für alle Ausnahmen die nicht lokal behandelt werden, kann durch Angabe der throws-Liste die Ausnahmenbehandlung an den Aufrufer weitergereicht werden.

(1)public class OwnException3 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			exceptionProne();
(5)		} catch (myException myE) {
(6)			System.out.println("Exception of type myException caught!");
(7)		} //catch
(8)	} //main()
(9)
(10)	public static void exceptionProne() throws myException {
(11)		throw new myException();
(12)	} //exceptionProne()
(13)} //class OwnException3
(14)
(15)class myException extends Exception {
(16)} //class myException

Beispiel 17: Propagierung der Ausnahmebehandlung   OwnException3.java

Im Beispiel wird die eigendefinierte Ausnahme myException nicht innerhalb der Methode exceptionProne behandelt, sondern an den Aufrufer -- in diesem Beispiel die main-Methode -- zur Behandlung weitergereicht.
Der übersetzter prüft hier das Vorhandensein eines try-catch-Blockes innerhalb von main ab.
Das folgende Beispiel zeigt eine Erweiterung des vorhergehenden, dergestalt, daß auch die main-Methode mit einem throws versehen ist. In diesem Fall wird das Ausnahmeereignis an das Laufzeitsystem weitergereicht; welches in der Konsequenz die Ausführung terminiert.

Generell gilt in Java die catch or throw-Regel, die besagt, daß ein Ausnahmeereignis entweder aufgefangen und behandelt (catch) oder weitergereicht (throw) werden muß.

(1)public class OwnException4 {
(2)	public static void main(String[] args) throws myException 	{
(3)		throw new myException();
(4)	} //main()
(5)} //class OwnException4
(6)
(7)class myException extends Exception {
(8)} //class myException

Beispiel 18: Propagierung eines Ausnahmeereignisses an das Laufzeitsystem   OwnException4.java

Auch die Kombination der beiden vorgestellten Ansätze ist möglich ...
Im abschließenden Codebeispiel wird neben der lokalen Ausnahmebehandlung (catch-Block innerhalb exceptionProne() auch eine Behandlung innerhalb der aufrufenden Methode (main) durchgeführt. Hierzu wird das aufgefangene Ausnahmeereignis innerhalb der Behandlungsroutine erneut mittels throw ausgelößt.

(1)public class OwnException5 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			exceptionProne();
(5)		} catch (myException e) {
(6)			System.out.println("exception myException catched within main method");
(7)		} //catch
(8)	} //main()
(9)
(10)	public static void exceptionProne() throws myException {
(11)		try {
(12)			throw new myException();
(13)		} catch (myException e) {
(14)			System.out.println("exception myException catched within method exceptionProne");
(15)			System.out.println("re-throwing...");
(16)			throw e;
(17)		} //catch
(18)	} //exceptionProne()
(19)} //class OwnException5
(20)
(21)class myException extends Exception {
(22)} //class myException

Beispiel 19: Behandlung und Weiterreichung einer Ausnahme   OwnException5.java

back to top   Zwangsbedingungen

 

Zur Steigerung der Qualität des entstehenden Systems gestattet Java die Definition von Zwangsbedingungen (Assertion), deren Einhaltung während der Ausführung geprüft werden kann. Ist eine solche Bedingung nicht erfüllt, so erfolgt ein Programmabbruch.

Im Unterschied zur Möglichkeit durch Selektionsausdrücke die Rückgabewerte von Methodenaufrufen auszuwerten oder durch Ausnahmebehandlung gezielt und benutzerdefiniert auf Fehlersituationen zu reagieren bieten die Zwangsbedingungen sowohl eine gegenüber den beiden genannten Ansätzen signifikant kompaktifizierte Syntax an, die gleichzeitig weniger Freiheitsgrade in der Behandlung der Fehlersituation bietet.
Zusätzlich kann die Prüfung von Zwangsbedingungen zur Laufzeit statisch durch einen Schalter der Ausführungsumgebung gesteuert werden. Auf dieser Basis eignen sie sich gut für die Formulierung verschiedenster Konsistenzprüfungen, die später im Produktivbetrieb zur Steigerung der Ausführungsgeschwindigkeit deaktiviert werden können.

Die allgemeine Syntax einer Zwangsbedingung lautet:

assert Boole'scher-Ausdruck (: Ausdruck)opt

Generell werden Zwangsbedingungen durch das Schlüsselwort assert eingeleitet. Darauf folgt ein Boole'scher Ausdruck, der erfüllt sein muß. Liefert die Auswertung dieses Ausdrucks den Wahrheitswert false, so erfolgt der Programmabbruch. Ist zusätzlich, nach dem separierenden Doppelpunkt, ein Ausdruck angegeben, so wird dieser zur Konstruktion eines AssertionError-Objekts herangezogen.

Aus den zugelassenen Parametertypen eines solchen Objekts ergeben sich die möglichen angebbaren Ausdruckstypen als: boolean, char, double, float, int, long und Object.

Das nachfolgende Beispiel zeigt den Einsatz einer einfachen Zwangsbedingung, deren Fehlschlag ohne anwenderdefinierte Konstruktion AssertionError-Objekts behandelt wird:

(1)public class AssTest1 {
(2)	public static void main(String[] args) {
(3)		Object o = null;
(4)		//...
(5)		assert o != null;
(6)	} //main()
(7)} //class AssTest1	

Beispiel 20: Einfache Zwangsbedingung   AssTest1.java

Das Beispiel deklariert eine Variable o als Ausprägung der Klasse Object und initialisiert diese mit null. Im späteren Verlauf der Ausführung soll geprüft werden, ob zwischenzeitlich eine Initialisierung erfolgt ist. Ist dies nicht der Fall, so ist eine Programmfortsetzung nicht sinnvoll. Diese Prüfung, verbunden mit der impliziten Forderung den Ablauf bei ihrer Nichterfüllung zu terminieren, ist als Zwangsbedingung realisiert.

Zur Verwendung der Zwangsbedingungen ist die Übersetzung mindestens konform zur Sprachversion 1.4 notwendig. Aktiviert wird diese Quellcodeinterpretation durch Übergabe des Wertes „1.4“, oder höher, als Parameter des Übersetzerschalters -source. Wird ein Wert kleiner als 1.4 gewählt, so wird das Schlüsselwort assert nicht als solches interpretiert, sondern kann als Identifier zur Benennung von Klassen, Attributen oder Methoden benutzt werden.
Im selben Sinne ist es ebenso zwingend erforderlich die Prüfung von Zwangsbedingung innerhalb der Laufzeitumgebung durch den Schalter -enableassertions zu aktivieren. Andernfalls werden alle assert-Anweisungen ungeprüft ignoriert. Aus dieser Forderung ergibt sich, daß mindestens Version 1.4 der Laufzeitumgebung benötigt wird um die Interpretation von Zwangsbedingungen zu aktivieren.

Die Ausführung des Beispiels liefert daher, bei aktiviertem Schalter die Ausgabe:

Exception in thread "main" java.lang.AssertionError
        at AssTest1.main(AssTest1.java:5)

Zur Einschränkung der Zwangsbedingungsauswertung gestattet die Laufzeitumgebung die Spezifikation derjenigen Klassen, für die diese Bedingungen ausgewertet werden sollen. Hierzu werden nach dem Schlüsselwort enableassertions, abgetrennt durch einen Doppelpunkt, die Namen der zu berücksichtigenden Klassen angegeben werden. Für das obenstehende Beispiel sind daher die Aufrufe java -enableassertions AssTest1 und java -enableassertions:AssTest1 AssTest1 äquivalent.

Ebenfalls durch Kommandozeilenschalter kann die (De-)Aktivierung der Prüfung der in den Standard-API-Klassen formulierten Zwangsbedingungen gesteuert werden. Hierfür stehen die Schalter -enablesystemassertions bzw. -disablesystemassertions. Die Möglichkeiten der klassengenauen Steuerung stehen jedoch in diesem Falle nicht zur Verfügung.

Die Möglichkeit der Übergabe von Parametern an das automatisiert durch das Laufzeitsystem erzeugte AssertionError-Objekt bietet sich insbesondere zur Dokumentation der Abbruchursache an. So zeigt das nachfolgende Beispiel prüft durch Aufruf der Methode exists der Klasse File ob die Datei test im aktuellen Dateisystemkatalog angelegt ist. Ist dies nicht erfüllt, so wird eine festgelegte Zeichenkette dem Konstruktor von AssertionError übergeben. Die Zeichenkette kann, als Ausprägung der Klasse String zur Konstruktion eines Fehlerobjektes verwendet werden, da sie gemäß der Typrestriktion eine gültige Instanz von Object repräsentiert.
Gleichzeitig kann die Methode exists als Ausdruck nach dem assert-Schlüsselwort angegeben werden, da die Ausführung der Methode einen Rückgabewert vom Typ boolean liefert und damit der Gesamtausdruck als Wahrheitswert ausgewertet werden kann.

(1)import java.io.File;
(2)
(3)public class AssTest2 {
(4)	public static void main(String[] args) {
(5)		//...
(6)		assert (new File("test")).exists() : "File 'test' does not exist";
(7)	} //main()
(8)} //class AssTest2	

Beispiel 21: Zwangsbedingung mit anwenderdefinierter Fehlerobjekt   AssTest2.java

Zwar sollte nach Nichterfüllung einer Zwangsbedingung die Applikationsausführung beendet werden, wie es auch im Standardfalle durch das Laufzeitsystem geschieht. Jedoch kann dieses Verhalten mit Mitteln des Ausnahmebehandlung unterbunden werden. Hierzu muß eine Zwangsbedingungsauswertung in einen try-Block einbettet werden. Findet sich im zugeordneten catch-Bereich eine Klausel für den AssertionError so wird diese ausgeführt und anschließend die Programmverarbeitung normal fortgesetzt. Das nachfolgende Beispiel zeigt diese Anwendung.

(1)import java.io.File;
(2)
(3)public class AssTest3 {
(4)	public static void main(String[] args) {
(5)		//...
(6)		try {
(7)			assert false : "this assertion fails definitely";
(8)		} catch (AssertionError ae) {
(9)			System.out.println("caught assertion error");
(10)		} //catch
(11)		System.out.println("continuing after error ");
(12)	} //main()
(13)} //class AssTest3

Beispiel 22: Abfangen einer fehlschlagenden Zwangsbedingung   AssTest3.java

Das im Beispiel gezeigte Verhalten ist zwar syntaktisch korrekt und wird auch im angestrebten Sinne ausgeführt, sollte jedoch nur mit Bedacht angewandt werden, da es die Semantik einer Zwangsbedingung aufweicht.
Insgesamt empfiehlt sich die Verwendung von Zwangsbedingungen lediglich für eine engumrissene Klasse von Fehlern, wie Nachbedingungen von Methoden, die einen internen Systemzustand dokumentieren, der durch keine Modifikation in einen konsistenten Zustand überführt werden könnte, er eine Ausführungsfortsetzung sinnvoll erscheinen läßt. Insbesondere solche, die durch Anwenderinteraktion begründet sind (wie fehlerhafte Parameterübergabe, etc.) sollten durch Ausnahmen behandelt werden.

back to top   2.4 Von komplexen zu objektorientierten Datenstrukturen

 

back to top   2.4.1 Arrays

 

Wie fast alle prozeduralen Programmiersprachen unterstützt auch Java die Zusammenfassung beliebiger gleichartiger Elemente zu geordneten, nicht zwingend dupplikatfreien, Mengen; die sog. Feldern, Arrays oder Vektoren.

Arrays in Java sind nicht Bestandteil des built-in Typsystems, sondern bereits als Klasse innerhalb der Java API realisiert.
Wie in anderen Programmiersprachen üblich stellen Javaarrays einen zusammenhängen kontinuierlichen Speicherbereich dar.

Bereits innerhalb der Standardsignatur der main-Methode wird ein Array verwendet:
public static void main(String[] args).

Einige Charakteristika:

Syntax:

ArrayCreationExpression:
   new PrimitiveType DimExprs Dimsoptional
   new TypeName DimExprs Dimsoptional
   new PrimitiveType Dimsoptional ArrayInitializer
   new TypeName Dims ArrayInitializer

DimExprs:
   DimExpr
   DimExprs DimExpr

DimExpr:
   [ Expression ]

Dims:
   [ ]
   Dims [ ]

(1)public class Array1 {
(2)	public static void main(String[] args) {
(3)		int 			firstArray[] 	= new int[10];
(4)		int[] 		secondArray 	= new int[10];
(5)		double[]		thirdArray		= {3.14, 2+5, 42.0};
(6)
(7)		System.out.println("length of firstArray="+ firstArray.length );
(8)		System.out.println("length of secondArray="+ secondArray.length );
(9)		System.out.println("length or thirdArray="+ thirdArray.length );
(10)		for (int i=0; i<thirdArray.length; i++)
(11)			System.out.println("thirdArray["+i+"]="+thirdArray[i]);
(12)
(13)		System.out.println("lenght of anonymous array="+ (new byte[] {1,2,3}).length);
(14)	} //main()
(15)} //class Array1

Beispiel 23: Arrayerzeugung und Initialisierung   Array1.java

Der Code aus Beispiel 16 definiert zunächst zwei int-Array der Größe 10. Die beiden Syntaxvarianten sind (wie aus C/C++ bekannt) äquivalent.
thirdArray wird bereits während der Definition mit double-Werten initialisiert. Die Größe muß nicht explizit angegeben werden, die errechnet sich automatisch aus der Anzahl der angegeben Ausdrücke.
Die letzte Definition eines Arrays im Beispiel erfolgt anonym. Der so erzeugte Array steht nach Verlassen des System.out.println-Ausdrucks nicht mehr zur Verfügung.

Arrays deren Elemente eigendefinierte Typen sind werden entsprechend mit eigenerTyp[] arrayName ... definiert.

Mehrdimensionale Arrays ...
werden nicht explizit unterstützt, sondern als Arrays von Arrays behandelt.

(1)public class Array2 {
(2)	public static void main(String[] args) {
(3)		int[][] multiDimensional = new int[2][3];
(4)
(5)		int dimR = multiDimensional.length;
(6)		int dimC = multiDimensional[0].length;
(7)
(8)		System.out.println("Lenght of multiDimensional="+ dimR);
(9)		System.out.println("Lenght of multiDimensional[0]="+dimC );
(10)
(11)		for(int i=0; i<dimR; i++)
(12)			for(int j=0; j<dimC; j++)
(13)				multiDimensional[i][j] = i*dimC + j+1;
(14)
(15)		System.out.println("Content of multiDimensionl:");
(16)		for(int i=0; i<dimR; i++) {
(17)			for(int j=0; j<dimC; j++) {
(18)				System.out.print(multiDimensional[i][j]+ " ");
(19)			} //for
(20)			System.out.println();
(21)		} //for
(22)	} //main()
(23)} //class Array2

Beispiel 24: Mehrdimensionale Arrays   Array2.java

Bildschirmausgabe:

$java Array2
Lenght of multiDimensional=2
Lenght of multiDimensional[0]=3
Content of multiDimensionl:
1 2 3
4 5 6

Die Angabe mehrer Dimensionsgrößen bei der Definition stellt eine Kurzform für die verschachtelte Variante dar. So ist das nachfolgende Codefragment äquivalent zum vorhergehenden Beispiel:

(1)public class Array3 {
(2)	public static void main(String[] args) {
(3)		int[][] multiDimensional = new int[2][];
(4)		for (int i=0; i<multiDimensional.length; i++)
(5)			multiDimensional[i] = new int[3];
(6)
(7)		int dimR = multiDimensional.length;
(8)		int dimC = multiDimensional[0].length;
(9)
(10)		System.out.println("Lenght of multiDimensional="+ dimR);
(11)		System.out.println("Lenght of multiDimensional[0]="+dimC );
(12)
(13)		for(int i=0; i<dimR; i++)
(14)			for(int j=0; j<dimC; j++)
(15)				multiDimensional[i][j] = i*dimC + j+1;
(16)
(17)		System.out.println("Content of multiDimensionl:");
(18)		for(int i=0; i<dimR; i++) {
(19)			for(int j=0; j<dimC; j++) {
(20)				System.out.print(multiDimensional[i][j]+ " ");
(21)			} //for
(22)			System.out.println();
(23)		} //for
(24)	} //main()
(25)} //class Array3

Beispiel 25: verschachtelte mehrdimensionale Arraydefinition   Array3.java

Die Definition
int[][] multiDimensional = { {1,2,3}, {100,200,300} };
Definiert einen Array der zwei Array, jeweils der Länge 3, enthält, und initialisiert diese mit den angegebenen Werten.
Unterscheiden sich die beiden implizit spezifizierten Arrays in der Größe, so definiert die Anzahl des ersten angegebenen Arrays die Elementzahl (Zeilenlänge) für alle folgenden (Zeileneinträge).

Duplizieren von Arrays:
Für die häufig benötigte Anwendung den vollständigen Inhalt eines Arrays in einen zweiten Array gleicher Dimension(en) zu kopieren exisitert die clone-Methode.

(1)public class Array4 {
(2)	public static void main(String[] args) {
(3)		int[] ia = {1,2,3,4,5};
(4)		int[] ib = new int[ia.length];
(5)
(6)		System.out.println("ia==ib = "+  (ia==ib));
(7)
(8)		System.out.println("content of ia:");
(9)		for(int i=0; i<ia.length; i++)
(10)			System.out.print(ia[i]+" ");
(11)		System.out.println("");
(12)
(13)		System.out.println("content of ib:");
(14)		for(int i=0; i<ib.length; i++)
(15)			System.out.print(ib[i]+" ");
(16)		System.out.println("");
(17)
(18)
(19)		ib = (int[]) ia.clone();
(20)		System.out.println("content of ib after cloning:");
(21)		for(int i=0; i<ib.length; i++)
(22)			System.out.print(ib[i]+" ");
(23)		System.out.println("");
(24)
(25)		System.out.println("ia==ib = "+  (ia==ib));
(26)
(27)		System.out.println("Setting ia=ib...");
(28)		ia=ib;
(29)		System.out.println("ia==ib = "+  (ia==ib));
(30)	} //main()
(31)} //class Array4

Beispiel 26: Duplizierung eines Arrayinhaltes mit der clone-Methode   Array4.java

Bildschirmausgabe:

$java Array4
ia==ib = false
content of ia:
1 2 3 4 5
content of ib:
0 0 0 0 0
content of ib after cloning:
1 2 3 4 5
ia==ib = false

Das Beispiel definiert zunächst den Array ia per expliziter Initialisierung. Ein zweiter Array, ib wird mit derselben Länge wie ia definiert.
Wie zu erwarten sind die Arrayobjekte verschieden, d.h. sie belegen unterschiedliche Speicherplätze.
Die clone-Methode dupliziert den Inhalt des Arrays ia und weist in ib zu.
In den letzten Zeilen wird der Variable ia der Wert von ib zugewiesen. Mithin der Array ia durch ib überschrieben. Das ursprüngliche ia kann damit nicht mehr durch den Programmierer referenziert werden, und wird für den Garbage Collector als freizugeben markiert.

Die ArrayStoreException wird ausgeworfen, sobald ein Typkonflikt beim Einfügen eines Arrayelementes zur Laufzeit auftritt; statisch erkennbare Typkonflikte werden bereits durch den übersetzer gemeldet.

(1)public class Array5 {
(2)	public static void main(String[] args) {
(3)		Test2[] ta = new Test2[5];
(4)		Test1[] tb = ta;
(5)
(6)		try {
(7)			tb[0] = new Test1();
(8)		} //try
(9)		catch (ArrayStoreException e) {
(10)			System.out.println(e);
(11)		} //catch
(12)	} //main()
(13)} //class Array5
(14)
(15)class Test1 {
(16)} //class Test1
(17)
(18)class Test2 extends Test1 {
(19)} //class Test2

Beispiel 27: Typkonflikt beim Einfügen, der zur Auslösung einer ArrayStoreException führt   Array5.java

Obwohl der Typ der Arraykomponenten von tb als test1 deklariert war, führt die Zuweisung innerhalb des try-Blockes zu einer ArrayStoreException.
Ursache: Durch die Initialisierung mit Elementen des Typs Test2, der eine Spezialisierung von Test1 bildet, wird auch der erwartete Inhaltstyp von Test1 verändert (konkret: spezialisiert).

back to top   2.4.2 Klassen

 

Klassen und Objekte stellen das namensgebende Hauptabstraktionsmittel in der objektorienterten Programmierung dar.
Prinzipiell lassen sich aus Sicht der OO-Programmierung drei Arten von Sprachen unterscheiden:

Gemäß dieser Einordnung ist Java als hybride objektorientierte Sprache anzusehen. Dies rührt hauptsächlich von der Umsetzung des build-in Typsystems als einfache Werte -- anstatt first class objects -- her.
In der Praxis zieht dies jedoch zumeist keine allzugrossen Einschränkungen nach sich.

Die Verwendung von Klassen ist in Java, im scharfen Gegensatz zu C++, zwingend. So ist es nicht möglich ein Programm ohne zumindest eine (Haupt-)Klasse zu schreiben.

Jede Klasse bildet einen abgeschlossenen Sichtbarkeitsbereich (auch: Scope) für die darin definierten Attribute und Methoden.

Bestandteile einer Klasse

Eine Java-Klasse besteht aus den in der Abbildung dargestellten Teilen:
Klassendeklaration: Sie benennt die Klasse eindeutig, und legt die allgemeinen Charakteristika fest. Das Aussehen der Deklaration kann wie folgt beschrieben werden:

Syntaxelement
Semantik
public
Die Klasse ist allgemein sichtbar.
Vorgabegemäß kann eine Klasse nur von Klassen desselben Packages genutzt werden. Durch das Schlüsselwort public wir sie auch außerhalb des umgebenden Packages sicht- und zugreifbar.
Die namensgebende Klasse einer Java-Quellcode-Datei, die auch die main-Methode enthalten kann, muß zwingend als public erklärt sein.
abstract
es können keine Objekte dieser Klasse erzeugt werden.
Abstrakte Klassen dienen zur Strukturierung des Entwurfs, um gemeinsame Merkmale verschiedener Klassen zentralisiert ausdrücken zu können.
final
von dieser Klasse kann nicht geerbt werden.
Die Entscheidung eine Klasse als final zu deklarieren ist designgetrieben. In der Verwendung der Klasse ergeben sich keine Unterschiede. Jedoch können so Vererbungsäste als abgeschlossen gekennzeichnet werden, um auszudrücken, daß an dieser Stelle keine Weiterentwicklung erfolgen soll. Das Schlüsselwort verhindert einen entsprechenden Versuch durch einen Fehler zum Übersetzungszeitpunkt.
Beispiele aus der Java-API: java.io.FileDescriptor, die Wrappertypen: Boolean, Integer, etc.
class ClassName
Name der Klasse
extends ClassName
Die Klasse erbt von der Klasse ClassName
In Java ist nur einfache Vererbung zugelassen, daher kann an dieser Stelle auch maximal eine Superklasse spezifiziert werden.
implements InterfaceList
Die Klasse implementiert die in der InterfaceList aufgeführten Schnittstellen.
Die Schnittstellennamen werden durch Kommata voneinander abgetrenn.
{
   ClassBody
}
Ausprogrammierter Klassenrumpf.

Ein umfangreicheres Beispiel:

UML-Klassendiagramm
(1)public class Student extends Person implements NamedEntity {
(2)	private String matrikelNo;
(3)
(4)	public Student(String matrikelNo) {
(5)		this.matrikelNo = matrikelNo;
(6)	} //constructor
(7)
(8)	public String getMatrikelNo() {
(9)		return matrikelNo;
(10)	} //getMatrikelNo;
(11)
(12)	public boolean setMatrikelNo(String matrikelNo) {
(13)		if (matrikelNo.compareTo("")==0) {
(14)			this.matrikelNo = matrikelNo;
(15)			return true;
(16)		} else
(17)			return false;
(18)	} //setMatrikelNo()
(19)
(20)	public String getName() {
(21)		return name;
(22)	} //getName()
(23)
(24)	public boolean setName(String newName) {
(25)		if (newName.compareTo("")!=0) {
(26)			name = newName;
(27)			return true;
(28)		} else
(29)			return false;
(30)	} //setName()
(31)
(32)	public String toString() {
(33)		return ("Name: "+this.getName()+"\nMatrikelnummer: "+this.getMatrikelNo() );
(34)	} //toString()
(35)
(36)	public static void main(String[] args) {
(37)		Student mario = new Student("0793022");
(38)		System.out.println( mario.getMatrikelNo() );
(39)		System.out.println("mario.toString() returns:\n"+mario.toString() );
(40)		mario.setName("Mario Jeckle");
(41)		System.out.println("mario.toString() returns:\n"+mario.toString() );
(42)	} //main()
(43)} //class Student
(44)
(45)class Person {
(46)	String	name;
(47)} //class Person
(48)
(49)interface NamedEntity {
(50)	String getName();
(51)	boolean setName(String newName);
(52)} //interface NamedEntity

Beispiel 28: Beispiel einer ausprogrammierten Klasse   Student.java

Bildschirmausgabe:

0793022
mario.toString() returns:
Name: null
Matrikelnummer: 0793022
mario.toString() returns:
Name: Mario Jeckle
Matrikelnummer: 0793022

Innere Klassen

Seit Java v1.1 besteht auch die Möglichkeit Klassen innerhalb von Klassen zu definieren.
Merkmale:

(Naive) Erweiterung des vorhergehenden Beispiels: Die Matrikelnummer ist als eigenständige Klasse innerhalb von Student2 realisiert:

(1)public class Student2 extends Person implements NamedEntity {
(2)	public Student2(String newMatrikelNo) {
(3)		matrikelNo = new MatrikelNo(newMatrikelNo);
(4)	} //constructor
(5)
(6)	class MatrikelNo {
(7)		private String	matrikelNo;
(8)
(9)		public MatrikelNo(String newMatrikelNo) {
(10)			matrikelNo = newMatrikelNo;
(11)			if (this.checkMatrikelNo() != true)
(12)				System.out.println("illegal MatrikelNo");
(13)		} //constructor
(14)		public boolean checkMatrikelNo() {
(15)			return( this.matrikelNo.length() == 7 ? true : false);
(16)		} //end checkMatrikelNo()
(17)
(18)		public String getMatrikelNo() {
(19)			return matrikelNo;
(20)		} //getMatrikelNo()
(21)
(22)		public boolean setMatrikelNo(String newMatrikelNo) {
(23)			matrikelNo = newMatrikelNo;
(24)			if (this.checkMatrikelNo() == true)
(25)				return true;
(26)			else
(27)				return false;
(28)		} //setMatrikelNo()
(29)	} //MatrikelNo;
(30)
(31)
(32)	private MatrikelNo matrikelNo;
(33)
(34)	public String getMatrikelNo() {
(35)		return matrikelNo.getMatrikelNo();
(36)	} //getMatrikelNo;
(37)
(38)	public boolean setMatrikelNo(String matrikelNo) {
(39)		return true;
(40)	} //setMatrikelNo()
(41)
(42)	public String getName() {
(43)		return name;
(44)	} //getName()
(45)
(46)	public boolean setName(String newName) {
(47)		if (newName.compareTo("")!=0) {
(48)			name = newName;
(49)			return true;
(50)		} else
(51)			return false;
(52)	} //setName()
(53)
(54)	public String toString() {
(55)		return ("Name: "+this.getName()+"\nMatrikelnummer: "+this.getMatrikelNo() );
(56)	} //toString()
(57)
(58)	public static void main(String[] args) {
(59)		Student2 mario = new Student2("0793022");
(60)		System.out.println( mario.getMatrikelNo() );
(61)
(62)		Student2 hans = new Student2("1234567X");
(63)	} //main()
(64)} //class Student2
(65)
(66)class Person {
(67)	String	name;
(68)} //class Person
(69)
(70)interface NamedEntity {
(71)	String getName();
(72)	boolean setName(String newName);
(73)} //interface NamedEntity

Beispiel 29: Realisierung der Matrikelnummer als innere Klasse   Student2.java

Anonyme Klassen:
Als weitere Besonderheit besteht die Möglichkeit die eingebettete Klasse anonym zu definieren. (siehe Java Language Specification)
Syntaktisch folgt die gesamte Klassendefinition auf eine mit new eingeleitete Objekterzeugung.
Unbenannte Klassen verfügen über keinen Konstruktor, da dieser konsequenterweise auch namenlos sein müßte.

Syntax:

new BaseClass (Parameters) {
   //inner class methods and data
};

Hinweis: Man beachte das (zwingende) Semikolon am Ende des Ausdrucks!

(1)public class Anonymous {
(2)	private TestClass test() {
(3)		return new TestClass() {
(4)			public void hello() {
(5)				System.out.println("overridden test!");
(6)			} //hello()
(7)		}; //class TestClass
(8)	} //test()
(9)
(10)	public static void main(String[] args) {
(11)		TestClass myRV;
(12)		myRV = (new Anonymous()).test();
(13)		myRV.hello();
(14)
(15)		System.out.println( myRV instanceof TestClass);
(16)		System.out.println( myRV.getClass().getName() );
(17)	} //end main()
(18)} //class Anonymous
(19)
(20)class TestClass {
(21)	public void hello() {
(22)		System.out.println("original");
(23)	} //hello()
(24)} //class TestClass

Beispiel 30: Anonyme innere Klasse   Anonymous.java

Bildschirmausgabe:

$java Anonymous
overridden test!
true
Anonymous$1

In der durch die Methode test retournierten Ausprägung der Klasse TestClass ist die Methode hello überschrieben. Da jede innere anonyme Klasse eine bestehende Klasse aus Ausgangsbasis besitzen muß kann die zurückgegebene Ausprägung einer Speicherzelle dieses Typs zugewiesen werden. Entsprechend lifert der instanceof-Operator auch true für den Typtest zurück.
Durch die Redefinition der Methode hello wird innerhalb von main nicht die ursprüngliche, sondern die überschreibende der anonymen Klasse aufgerufen.
Die letzte Programmzeile in main bildet einen Vorgriff auf die noch zu behandelnde Reflection API, welche die Gewinnung von Modellinformationen zur Laufzeit erlaubt. Konkret ermittelt die abgebildete Zeile den Namen der Klasse des in myRV gespeicherten Objekts. Java setzt für innere Klassen einen Namen aus der umgebenden Klasse und dem Namen der inneren Klasse, abgetrennt durch das Dollarsymbol, zusammen. Anonyme Klassen werden aufsteigend nummeriert. Daher im Beispiel der Surrogatname Anonymous$1 für die erste anonyme Klasse innerhalb der Klasse anonymous.

Im Grunde genommen handelt es sich bei anonymen inneren Klassen de facto um eine besondere Art der Vererbung, welche bereits bekannte Methoden überschreibt. Das überladen existierender Methoden, sowie die Erweiterung der Superklasse um zusätzliche Attribute oder Methoden ist nicht möglich.

Vorwärtsverweis: Ein reales Beispiel für die Nutzung dieses Sprachmechanismus wird in Kapitel 3 vorgestellt.
Im Kontext des in Kapitel 3.2.7 vorgestellten Abstract Windowing Toolkit (AWT) ergeben sich gute reale Anwendungsfälle für anonyme innere Klassen.

Abschlußbemerkung:
Die weiteren Spielarten innerer Klassendefinitionen werden wegen der geringen praktischen Relevanz -- außerhalb des AWT --, und tendenziellen Unübersichtlichkeit des entstehenden Entwurfs nicht diskuiert. Eine gute Referenz findet sich im für Ausbildungszwecke (als HTML) kostenfrei verfügbaren Buch GoTo Java2.

Mit strictfp existiert seit Java v1.2 ein neues Schlüsselwort zur Modifikation des Klassenverhaltens. Es deaktiviert die erweiterte Gleitpunktdarstellung nach IEEE 754-1985. In diesem Modus werden float-Datentypen statt mit 32 mit 43-Bit, bzw. double-Datentypen mit 79 statt 64-Bit behandelt.
Durch die bessere Nutzung der vorhandenen Zielhardware ergibt sich neben Performancegewinnen auch eine erhöhte Berechnungsgenauigkeit. Als Resultat kann derselbe Code auf verschiedenen realen Maschinen, je nach Auslegung der Gleitkommaarithmetik, zu verschiedenen Berechnungsergebnissen führen.
Durch Angabe von strictfp wird somit wieder portables Verhalten der Fließkommaoperationen erzwungen.

Das Beispiel (nach Kazuyuki SHUDO) zeigt die Verwendung dieses Schlüsselwortes:

(1)class StrictfpTest {
(2)  private static double defaultDmul(double a, double b) {
(3)    return a * b;
(4)  }
(5)
(6)  private static strictfp double strictDmul(double a, double b) {
(7)    return a * b;
(8)  }
(9)
(10)  private static double defaultDdiv(double a, double b) {
(11)    return a / b;
(12)  }
(13)
(14)  private static strictfp double strictDdiv(double a, double b) {
(15)    return a / b;
(16)  }
(17)
(18)  public static void main(String[] args) {
(19)    double a, b, c;
(20)
(21)    /* multiplication */
(22)    a = Double.longBitsToDouble(0x0008008000000000L);
(23)    b = Double.longBitsToDouble(0x3ff0000000000001L);
(24)
(25)    System.out.println(a + " (0x0008008000000000)");
(26)    System.out.println("  * " + b + " (0x3ff0000000000001)");
(27)
(28)    c = defaultDmul(a, b);
(29)    System.out.println("default : " + c +
(30)		" (0x" + Long.toHexString(Double.doubleToLongBits(c)) + ")");
(31)
(32)    c = strictDmul(a, b);
(33)    System.out.println("strictfp: " + c +
(34)		" (0x" + Long.toHexString(Double.doubleToLongBits(c)) + ")");
(35)
(36)    System.out.println();
(37)
(38)    /* division */
(39)    a = Double.longBitsToDouble(0x000fffffffffffffL);
(40)    b = Double.longBitsToDouble(0x3fefffffffffffffL);
(41)
(42)    System.out.println(a + " (0x000fffffffffffff)");
(43)    System.out.println("  / " + b + " (0x3fefffffffffffff)");
(44)
(45)    c = defaultDdiv(a, b);
(46)    System.out.println("default : " + c +
(47)		" (0x" + Long.toHexString(Double.doubleToLongBits(c)) + ")");
(48)
(49)    c = strictDdiv(a, b);
(50)    System.out.println("strictfp: " + c +
(51)		" (0x" + Long.toHexString(Double.doubleToLongBits(c)) + ")");
(52)  }
(53)}

Beispiel 31: Verwendung des Schlüsselwortes strictfp   StrictfpTest.java

Die Unterstützung durch die virtuelle Maschine vorausgesetzt, liefert die Ausführung des Programms folgende Ausgabe:

1.112808544714844E-308 (0x0008008000000000) * 1.0000000000000002 (0x3ff0000000000001)
default : 1.112808544714844E-308 (0x8008000000000)
strictfp: 1.1128085447148447E-308 (0x8008000000001)

2.225073858507201E-308 (0x000fffffffffffff) / 0.9999999999999999 (0x3fefffffffffffff)
default : 2.2250738585072014E-308 (0x10000000000000)
strictfp: 2.225073858507201E-308 (0xfffffffffffff)

SUNs Referenzimplementierung der virtuellen Maschine unterstützen ab Version 1.3 auf den Intelplattformen Windows und Linux die korrekte Umsetzung des erweiterten Fließkommaformates.

Objekterzeugung:
Die Erzeung konkreter Ausprägungen (auch: Instanzen oder Objekte) einer Klasse geschieht üblicherweise durch den new-Operator.
Die Syntax lautet: Type Variable = new Type(Parameters)

Wird ein Objekt nicht mehr durch den Programmierer referenziert, so wird es durch den Garbage Collector aus dem Speicher entfernt. Dies kann u. U. auch erst verzögert geschehen, da der Garbage Collector als eigener asynchroner Thread der virtuellen Maschine realisiert ist. (Der Aufruf System.gc() schlägt der virtuellen Maschine vor den Garbage Collector bei Gelegenheit aufzurufen.)

Weiterführende Informationen zum Thema innere Klassen.

back to top   2.4.3 Attribute

 

Attribute stellen in der objektorientierten Programmierung den üblichen -- und bei strenger Betrachtung den einzigen -- Weg zur Ablage dynamischer Zustandsinformation eines Objekts dar. (Selbstverständlich können auch Variablen innerhalb von Methoden beliebige Informationen aufnehmen. Jedoch sind diese Inhalte im Allgemeinen nach Verlassen des Gültigkeitsbereichs verloren).

Abbildung 2 stellt die Plazierung der Attribute in UML-Notation dar.

Vereinfachte Syntax einer Attributdefinition:

(public,
protected,
private,
static,
final,
transient,
volatile)zero or more
Identifier
[]optional
(= Initialization)optional

Die Syntaxkomponenten im Einzelnen:

Syntaxelement
Semantik
public
Das Attribut ist allgemein für alle anderen Klassen sichtbar.
protected
Das Attribut ist ausschließlich für die definierende Klasse sowie diejenigen sichtbar die von aktuellen erben, sowie alle Klassen desselben Packages.
private
Das Attribut ist nur innerhalb der deklarierenden Klasse sichtbar.
static
Gültigkeitsbereich des Attributs ist die gesamte Klasse. D.h. alle Objekte dieser Klasse teilen sich dasselbe Attribut (= derselbe Speicherplatz). Ein so deklariertes Attribut hat in allen Ausprägungen denselben Wert; änderungen finden automatisch synchronisiert in allen Objekten statt.
Ohne Angabe dieses Schlüsselwortes ist der Scope auf die Objektebene festgelegt.
Das Attribut ist konstant, und kann nach seiner (zwingend anzugebenden!) Initialisierung nicht mehr verändert werden.
So ausgezeichnete Attribute sind nicht Teil des persistenten Objektzustandes, und werden bei einer Ablage auf Sekundärspeicher (File, Datenbank, etc.) ignoriert.
volatile
Kennzeichnet ein Attribut als bei Optimierungen nicht zu berücksichtigen.
Die Semantik und Anwendung ist ähnlich zur aus C/C++ bekannten Mimik.
(1)public class Attributes {
(2)	int 		testAttribute1;
(3)	public	short		testAttriubte2 = 99;
(4)	private 	long 		testAttribute3 = 5L;
(5)	protected boolean	testAttribute4;
(6)	volatile	private char testAttribute5;
(7)	public transient long testAttribute6 = 0x42;
(8)
(9)	static final double pi = 3.141592654;
(10)
(11)	TestClass testAttributeC1;
(12)	TestClass testAttributeC2 = new TestClass();
(13)} //class Attributes
(14)
(15)class TestClass {
(16)} //class TestClass

Beispiel 32: Einige Attributdefinitionen   Attributes.java

Abschlußbemerkungen:

back to top   2.4.4 Operationen und Methoden

 

Operationen und Methoden bilden das dynamische Verhalten eines Objekts ab. Während der Begriff Operation nur die Signatur, bestehend aus Rückgabetyp, Operationsnamen und der Parameterliste, bezeichnet, deckt Methode auch die programmiersprachliche Umsetzung ab.
Java erlaubt ausschließlich die Definition von Methoden innerhalb von Klassen. Globale Funktionen sind ebenso wie globale Variablen nicht möglich!

Vereinfachte Syntax einer Operation:

(public,
protected,
private,
abstract,
static,
final,
synchronized,
native,
strictfp
)zero or more
ResultType
Identifier
(ParameterList)
(throws ExceptionList)optional

Die Syntaxkomponenten im Einzelnen:

Syntaxelement
Semantik
public
Die Methode ist allgemein für alle anderen Klassen sichtbar.
protected
Die Methode ist ausschließlich für Klassen sichtbar die von der aktuellen erben, sowie alle Klassen desselben Packages.
private
Die Methode ist nur innerhalb der deklarierenden Klasse sichtbar.
abstract
Definiert analog zum Schlüsselwort auf Klassenebene, daß eine so gekennzeichnete Operation keine Implementierung bereitstellt; mithin keine Methode implementiert.
static
Gültigkeitsbereich dieser Methode ist die Klasse. In der Anwendung bedeutet dies, daß eine so deklarierte Methode sowohl auf der Klasse selbst, als auch einem Objekt dieser Klasse aufgerufen werden kann.
Beispiel
Die Methode darf nicht in einer erbenden Klasse überschrieben werden.
synchronized
Auf einer so deklarierten Methode wird durch das Laufzeitsysteme ein wechselseitiger Ausschluß realisiert.
Das bedeutet, daß sich nur jeweils ein Thread innerhalb einer Methode des Objektes befinden darf.
native
Eine so gekennzeichnete Methode ist nicht in Java realisiert, sondern wird als nativer Code einer anderen Programmiersprache (z.B. C/C++) realisiert.
Das JDK bietet für die beiden genannten Sprachen Unterstützung in Form automatisch generierter Headerdateien an.
strictfp
Deaktiviert Nutzung plattformspezifischer Gleitkommahardware.
analog strictfp auf Klassenebene

Aufgrund der strengen Typisierung der Programmiersprache ist jede Javaoperation über ihren Rückgabewert typisiert. Als Rückgabetypen stehen alle primitiven built-in Datentypen, alle Klassen und Schnittstellen, sowie der explizite Nichttypvoid zur Verfügung.
Wie aus C/C++ bekannt geben void-Methoden keine Werte an den Aufrufer zurück, und können daher nicht innerhalb von Ausdrücken verwendet werden.
Ebenso analog der bekannten Mimik wird die explizite Rückgabe eines Wertes durch das Schlüsselwort return eingeleitet. Wie in (neuen) C/C++-übersetzern üblich, prüft auch Java die Existenz einer erreichbaren typkompatiblen Return-Anweisung innerhalb des Methodenrumpfes.

Die auf den Operationsnamen folgende Parameterliste setzt sich aus einer Folge von Typen und Parameternamen zusammen, kann jedoch auch leer sein.
Verursacht durch das Fehlen expliziter Referenztypen (wie Zeiger) werden alle Parameter, die primitve build-in Typen sind by value und alle objektartigen Parameter per Vorgabe by reference übergeben.
Hinweis: Es existiert kein Mechanismus dieses Verhalten zu überschreiben oder abzuändern! Lediglich für die by reference Interpretation der Primitivtypen ist mit den Wrapper Typen eine Standardmethodik vorgesehen.

Genaugenommen kommt auch für Objekte, die als Parameter einer Methode verwendet werden, eine Wertübergabe zum Einsatz. Jedoch wird in diesem Falle nicht der Wert des Objektes übergeben, d.h. es wird keine Kopie des Objektes erzeugt und an die aufzurufende Methode übergeben. Vielmehr wird eine Kopie der Referenz auf das Objekt übergeben.
Dieser Umstand führt dazu, daß auch objektwertige Parameter innerhalb eines Methodenrumpfes nicht direkt modifiziert werden können, sondern hierfür auf Methoden des zu verändernden Objekts zurückgegriffen werden muß.
Mehr Informationen hierzu.

Die Signatur einer Javamethode wird aus Operationsname und der Parameterliste, unter Berücksichtigung der Parameterreihenfolge, gebildet.
Sichtbarkeitsattribute, ebenso wie der Rückgabetyp und weitere Eigenschaften gehen nicht in die Signaturbildung ein.

Innerhalb des Methodenrumpfs sind alle aus 2.3 bekannten Kontrollstrukturen zugelassen. Darüberhinaus kann an beliebiger Stelle die Deklaration lokaler Variablen erfolgen. Im Gegensatz zu C/C++ ist die Lebensdauer lokaler Variablen strikt an die des umgebenden Blocks gebunden. Daher steht das Schlüsselwort static für die Variablendefinition innerhalb von Methoden nicht zur verfügung.

Der Aufruf von Methoden erfolgt in der bekannten Punktnotation, die in allgemeiner Form als oder Objekt.Methode(Parameterliste) oder entsprechend Klasse.Methode(Parameterliste) bei Klassenmethoden beschrieben werden kann.

Eine Sonderrolle spielt das Schlüsselwort this im Methodenrumpf. Es steht als impliziter Parameter in allen nicht-statischen Methoden zur Verfügung. this referenziert immer das aktuelle Objekt. Die lokale Variable this ist dabei immer von Typ final und erlaubt keine Neuzuweisungen.
Üblicherweise ist die Verwendung von this nicht explizit erforderlich. Zur Behebung von Namenskonflikten zwischen Übergabeparametern und lokalen Variablen leistet es jedoch wertvolle Dienste. Weitere Anwendung findet das Schlüsselwort zur Aufruf von Konstruktoren.

(1)public class ThisDemo {
(2)	private int i = 42;
(3)
(4)	public void setI(int i) {
(5)		this.i = i;
(6)	} //setI()
(7)
(8)	public int getI() {
(9)		return this.i;
(10)	} //getI()
(11)
(12)	public static void main(String[] args) {
(13)		ThisDemo td = new ThisDemo();
(14)		System.out.println("value of i="+td.getI() );
(15)		td.setI(45);
(16)		System.out.println("value of i="+td.getI() );
(17)	} //main()
(18)} //class ThisDemo

Beispiel 33: Zugriff auf Objekteigenschaften mit this   ThisDemo.java

Bildschirmausgabe:

$java ThisDemo
value of i=42
value of i=45

Das Beispiel zeigt die Verwendung der Eigenobjektreferenz this innerhalb der Methoden setI und getI.
Während die Referenzierung von i innerhalb getI auch ohne vorstelltes explizites this eindeutig auflösbar ist, muß in setI das Schlüsselwort zwingend angegeben werden um dem Namenskonflikt zwischen dem Parameter i und dem gleichnamigen Attribut aufzulösen.

Analog zu den Klassenattributen werden durch das Schlüsselwort staticKlassenmethoden definiert.
Auf sie kann unabhängig von der Existenz konkreter Objekte der Klasse zugegriffen werden.
Aufgrund ihrer instanzenunabhängigen Natur besitzen statische Methoden keine this Referenz, da ein so zu referenzierendes Objekt nicht exisitiert.

(1)public class StaticMethodTest {
(2)	public static void helloWorld() {
(3)		System.out.println("hello world");
(4)	} //helloWorld()
(5)
(6)	public static void main(String[] args) {
(7)		StaticMethodTest.helloWorld();
(8)		(new StaticMethodTest()).helloWorld();
(9)	} //main()
(10)} //class StaticMethodTest

Beispiel 34: Statische Methoden   StaticMethodTest.java

Besondere Methoden:
Konstruktoren
Identisch benannt zur beherbergenden Klasse

Syntaktisch ähneln die Konstruktoren den „normalen“ Operationsdeklarationen. Jedoch mit dem Unterschied, daß kein Rückgabetyp spezifiert werden kann.
Der Konstruktorenaufruf wird automatisch durch den übersetzer plaziert.
Existiert kein expliziter Konstruktor, so wird während des übersetzungsvorganges ein unparametrisierter Vorgabekonstruktor erzeugt. Dieser enthält keinerlei eigene Funktionalität, sondern ruft nur den entsprechenden parameterlosen Konstruktor der Superklasse auf. Verfügt eine Klasse hingegen ausschließlich über parametrisierte Konstruktoren, so wird kein unparametrisierter Defaultkonstruktor angelegt.
Aufrufe unter den verschiedenen Konstruktoren können durch das bekannte Schlüsselwort this vorgenommen werden. Hierbei werden die einzelnen Konstruktoren wie normale Methoden behandelt. Der Aufruf erfolgt durch thisgefolgt von den Konstruktorenparametern in runden Klammern. Der kaskadierende Konstruktorenaufruf muß zwingend als erstes Statement des Anweisungsblockes plaziert werden.
Zwar verfügt die Konstruktormethode über keinen expliziten Rückgabetyp, wird aber intern als void-Typisiert behandelt.

(1)public class Construct {
(2)	public Construct() {
(3)		System.out.println("object created using explicitly stated parameterless Constructur");
(4)	} //constructor
(5)
(6)	public Construct(int i) {
(7)		System.out.println("object Constructed by parametrized Constructor, parameter i="+i);
(8)	} //constructor
(9)
(10)	public static void main(String args[]) {
(11)		new Construct();
(12)		new Construct(1);
(13)		new TestClass1();
(14)	} //main()
(15)} //class Construct
(16)
(17)class TestClass1 extends TestClass2 {
(18)	//nothing!
(19)} //TestClass1
(20)
(21)class TestClass2 {
(22)	TestClass2() {
(23)		this(1);
(24)		System.out.println("Constructor of TestClass2 called");
(25)	} //constructor
(26)
(27)	TestClass2(int i) {
(28)		this ((short) 2,(byte) 3);
(29)		System.out.println("Constructor of TestClass2 called paramter i="+i);
(30)	} //constructor
(31)
(32)	TestClass2(short s, byte b) {
(33)		System.out.println("Constructor of TestClass2 called paramter s="+s+" parameter b="+b);
(34)	} //constructor
(35)} //class TestClass2

Beispiel 35: Verschiedene Konstruktoren   Construct.java

Nicht-öffentliche Konstruktoren und Fabriken:
Häufig anzutreffen sind Klassen, die zwar einen explizit definierten Konstruktor bieten, diesen jedoch private deklarieren. In der Konsequenz ist eine Objekterzeugung per new nicht möglich.
Zumeist wird dieser Ansatz angewandt, wenn die Objekterzeugung aufwendig ist und nicht dem Anwender überlassen werden kann oder soll.
Um dennoch Objekte der betreffenden Klasse erzeugen zu können wird eine statische Fabrik-Methode eingeführt, welche die Objekterzeugung übernimmt und implizit new aufruft.

(1)public class Construct2 {
(2)	private Construct2() {
(3)		System.out.println("hello world");
(4)	} //constructor
(5)
(6)	public static void main(String[] args) {
(7)		new Construct2();
(8)		/* new OtherClass(); would fail since constructor is declared to be private */
(9)		OtherClass oc = OtherClass.OtherClassFactory(); //object creation using simple factory approach
(10)	} //main()
(11)} //class Construct2
(12)
(13)class OtherClass {
(14)	public static OtherClass OtherClassFactory() {
(15)		return new OtherClass();
(16)	} //constructor
(17)
(18)	private OtherClass() {
(19)		System.out.println("other class created");
(20)	} //constructor
(21)} //class OtherClass

Beispiel 36: Nicht-öffentliche Konstruktoren und Fabrikmethoden zur Objekterzeugung   Construct2.java

Bildschirmausgabe:

$java Construct2
hello world
other class created

Der Konstruktorenaufruf innerhalb der Klasse construct2 kann bei Absetzen des new ausgeführt werden, da aus der Klasse heraus (konkret: innerhalb einer der statischen Methode main) ein Objekt dieser Klasse erzeugt wird -- der private Konstruktor ist somit zugreifbar.
Hingegen ist die Erzeugung eines Objekts von otherClass per new nicht möglich, da der dort definierte private Konstruktor nicht aus construct2 heraus referenzierbar ist. Der entsprechende Fehler wird bereits zum Übersetzungszeitpunkt erkannt.
Die Erzeugung von Objekten der Klasse otherClass ist ausschließlich über die statische Methode otherClassFactory möglich. Sie ruft intern den privaten Konstruktor auf, der an dieser Stelle sicht- und zugreifbar ist.
(siehe Java API Specification, siehe Java Language Specification)

Ein Beispiel für eine Klasse, die weder über eine Factorymethode, noch über öffentliche Konstruktoren verfügt ist die Standard-API-Klasse Void.
Ihre Implementierung ist in der API wie folgt festgelegt:

public final
class Void {
    public static final Class TYPE = Class.getPrimitiveClass("void");

    private Void() {}
}

Noch vor dem Konstruktor werden die statischen Initialisierungen aufgerufen. Aufgrund der Reihenfolge in der Methodenabarbeitung existiert zum Ablaufzeitpunkt der statischen Initialisierungen das zu erzeugende Objekt noch nicht; der static-Block wird quasi zum Ladezeitpunkt der Klasse ausgeführt. Daher können zu diesem Zeitpunkt keine Zugriffe auf nichtstatische Speicherobjekte (Methoden und Attribute) erfolgen. Zugriffe auf statische Attribute und Methoden sind jedoch möglich.
Der Initialisierungsblock darf nicht durch Unterbrechungsanweisungen wie break oder return vorzeitig verlassen werden. Hierunter fallen auch Exceptions, die zum vorzeitigen Verlassen des Anweisungsblockes führen würden.

(1)public class StaticInit {
(2)	static int i=42;
(3)
(4)	static {
(5)		System.out.println("value of i="+i);
(6)		helloWorld();
(7)		i = 43;
(8)	} //static
(9)
(10)	public static void helloWorld() {
(11)		System.out.println("hello world");
(12)	} //helloWorld()
(13)
(14)	public static void main(String[] args) {
(15)		StaticInit myObj = new StaticInit();
(16)	} //main()
(17)
(18)	public StaticInit() {
(19)		System.out.println("value of i="+i);
(20)	} //constructor
(21)} //class StaticInit

Beispiel 37: Statische Initialisierung   StaticInit.java

Bildschirmausgabe:

$java StaticInit
value of i=42
hello world
value of i=43

Das Beispiel zeigt zunächst den Zugriff auf das statische Attribut (Klassenattribut) i direkt nach dessen Definition und Initialisierung. Zu diesem Zeitpunkt ist der Initialisierungswert 42 gesetzt.
Der Aufruf von helloWorld demonstriert die Möglichkeit vor der Objekterzeugung statische Methoden auszuführen.
Nach Abarbeitung der statischen Initialisierung wird der Konstruktor bearbeitet. In ihm steht der Wert von i nur noch in der durch die statische Initialisierung modifizierten Form zur Verfügung.

Ausführungsreihenfolge der konstruierenden Codesequenzen:
(als Pseudocode):

for-each (Superclass s) {
   s.AttributeInitialization()
   s.StaticBlock()
} //for-each
for-each (Superclass s) {
   s.Constructor()
}

Beginnend mit der hierachiehöchsten Superklasse werden zunächst die Initialisierungen der Attribute, im Anschluß daran (falls vorhanden) der static-Block dieser Klasse, abgearbeitet.
In einem zweiten Durchlauf über die Klassenhierarchie werden die Konstruktoren der Superklassen in derselben Reihenfolge zur Ausführung gebracht.

(1)public class SuperClassConstruct extends Class2 {
(2)	static int i=6;
(3)
(4)	static {
(5)		System.out.println("i="+i--);
(6)		System.out.println("static initialization of class SuperClassConstruct");
(7)		System.out.println("i="+i);
(8)	} //static
(9)
(10)	public SuperClassConstruct() {
(11)		System.out.println("object of class SuperClassConstruct constructed");
(12)	} //constructor
(13)
(14)	public static void main(String[] args) {
(15)		new SuperClassConstruct();
(16)	} //main()
(17)} //class SuperClassConstruct
(18)
(19)class Class2 extends Class1 {
(20)	static int i=4;
(21)
(22)	static {
(23)		System.out.println("i="+i--);
(24)		System.out.println("static initialization of class Class2");
(25)		System.out.println("i="+i);
(26)	} //static
(27)
(28)	public Class2() {
(29)		System.out.println("object of class Class2 constructed");
(30)	} //constructor
(31)} //class Class2
(32)
(33)class Class1 {
(34)	static int i=2;
(35)	static {
(36)		System.out.println("i="+i--);
(37)		System.out.println("static initialization of class Class1");
(38)		System.out.println("i="+i);
(39)	} //static
(40)
(41)	public Class1() {
(42)		System.out.println("object of class Class1 constructed");
(43)	} //constructor
(44)} //class Class2

Beispiel 38: Initialisierungsreihenfolge   SuperClassConstruct.java

Bildschirmausgabe:

$java SuperClassConstruct
i=2
static initialization of class class1
i=1
i=4
static initialization of class class2
i=3
i=6
static initialization of class superClassConstruct
i=5
object of class class1 constructed
object of class class2 constructed
object of class superClassConstruct constructed

Destruktoren --finalize
Zwar können in Java, anders als in C++, Objekte nicht durch Destruktorenaufruf zerstört werden, sondern nur duch Null-Zuweisung als nicht mehr benötigt markiert werden, Destruktoren werden jedoch weiterhin explizit zur Verfügung gestellt.
Destruktoren werden nicht durch den Anwender aufgerufen, sondern implizit erst zum Zerstörungszeitpunkt eines Objekts. Genaugenommen ist die Ausführung des Destruktors nicht garantiert. Terminiert die Applikation vor dem Ablauf des Garbage Collectors, so werden die Objekte implizit durch das Betriebssystem aus dem Speicher entfernt, ohne das die Java-Laufzeitumgebung zunächst die finalize-Methoden aufruft.
Die Sichtbarkeitseinschränkung von finalize muß mindestens protected sein. Dies rührt von der impliziten Überschreibung der von Object ererbten Methode finalize her. (vgl. java.lang.Object:finalize)

(1)public class DestruktorTest {
(2)	public static void main(String[] args) {
(3)		Test t1 = new Test();
(4)		t1 = null;
(5)		System.gc();
(6)	} //main()
(7)} //class DestruktorTest
(8)
(9)class Test {
(10)	public void finalize() 	{
(11)		System.out.println("destructor of class Test called");
(12)	} //finalize()
(13)} //class Test

Beispiel 39: Destruktorenaufruf durch Garbage Collector   DestruktorTest.java

Im Beispiel wird das Objekt t1 zunächst durch Nullsetzung zum Löschen markiert, und im anschließenden Garbage Collector-Aufruf gelöscht. Während des Entfernens aus dem Speicher wird die darstellte Nachricht am Bildschirm ausgegeben.

Hauptmethode -- main
Pro Java-Applikation kann maximal eine Methode der Signatur main(String[]) existieren. Sie wird beim Startvorgang innerhalb der öffentlichen Klasse gesucht, die namensgebend für die Klassendatei ist.
Applets verfügen hingegen über keine automatisch aufgerufene main-Methode, sondern werden durch init() Initialisiert und durch das im Anschluß darauf aufgerufene start() ausgeführt.

Die Methode toString
ist auf der Superklasse Object aller Javaklassen definiert. Sie wird von allen Klassen der Standard-API implementiert. Durch sie wird immer eine Zeichenkettenrepräsentation des aktuellen Objekts zurückgegeben.
Diese Methode wird standardmäßig bei der Ausgabe per System.out.println aufgerufen.

Zum Abschluß: Sichtbarkeit von Attributen und Methoden im Überblick:

definierende Klasse
Abgeleitete Klasse
Package
Alle anderen
default
(keine explizite Festlegung)
unterstützt
public
unterstützt
unterstützt
unterstützt
unterstützt
private
unterstützt
protected
unterstützt
unterstützt
Zugriff auf Attribute und
Methoden der Subklasse
unterstützt

Anmerkung: Der Zugriff auf als protected deklarierte Attribute und Methoden ist auch über Objektreferenzen möglich, die denselben Typ haben wie die definierende Klasse.

back to top   2.4.5 Aufzählungstypen

 

Ab Version 1.5 führt Java mit dem Schlüsselwort enum einen eigenständigen und expliziten Mechanismus zur Definition von Aufzählungstypen ein.

Konzeptionell sind Aufzählungstypen identisch zu Variablen, die innerhalb des Rumpfes einer Methode deklarierten werden oder Variablen und Attributen einer Klasse gleichgestellt. Aus diesem Grunde ähnelt die Definitionssyntax auch den bekannten Darstellungsformen:

Vereinfachte Syntax der Enum-Definition:

(public,
protected,
private)one of
staticopt
finalopt
enum
Identifier
{value}

Im einfachsten Anwendungsfall besteht die Definition eines Aufzählungstypen aus der durch Kommata voneinander separierten vollständigen Aufzählung aller Werte.
Das Beispiel zeigt die Bildung des Aufzählungstyps season, der durch die zulässigen Werte winter, spring, summer und fall konstituiert wird.

(1)public class EnumTest1 {
(2)	public static void main(String[] args) {
(3)		enum season { winter, spring, summer, fall; }
(4)
(5)		season s1 = season.winter;
(6)
(7)		if (s1 == season.winter)
(8)			System.out.println("It's "+s1);
(9)
(10)		season s2 = season.winter;
(11)	} //main()
(12)} //class EnumTest1

Beispiel 40: Definition eines einfachen Aufzählungstyps   EnumTest1.java

Die Verwendung von Aufzählungstypen entspricht der von primitiven Typen. Aus diesem Grund ist keine Anforderung von Speicherplatz durch das new-Schlüsselwort notwendig. Der Übersetzer verhindert sogar aktiv die Instanziierung eines Aufzählungstyps via new und bricht beim Versuch mit der Fehlermeldung enum types may not be instantiated ab.
Im Gegensatz zu Ausprägungen der Primivtypen besitzen Aufzählungsinstanzen jedoch keinen Vorgabewert mit dem sie standardmäßig initialisiert werden. Stattdessen führt die Verwendung einer nichtinitialisierten Ausprägung eines Aufzählungstypen zu einem Übersetzungsfehler (Fehlermeldung: variable ... might not have been initialized).

Ansonsten bietet die Umsetzung die für Primitivtypen bekannten Eigenschaften. So liefert die Ausgabe diejenige Zeichenkette (d.h. den Wert) mit dem Aufzählungsinstanz belegt wurde.
Ebenfalls identisch zu den Primitivtypen besitzen Aufzählungsinstanzen keine Identität. Dies äußert sich darin, daß zwei mit demselben Wert belegte Aufzählungen als identisch betrachtet werden.

Nicht identisch sind hingegen Ausprägungen verschiedener Aufzählungstypen, die vermeintlich denselben Wert enthalten, d.h. in deren zur Definition verwendeten Werteliste sich lexikalisch dieselben Einträge finden.
So würde die nachfolgende Zuweisung bereits durch den Übersetzer (mit der Fehlermeldung incompatible types) abgelehnt

enum seasonE { winter, spring, summer, fall; };
enum seasonG { winter, frühling, sommer, herbst; };
seasonE s = seasonG.winter;

Dasselbe gilt auch für den Versuch des Vergleichs der Inhalte zweiter Aufzählungsausprägungen, wie sie durch das nachstehende Codefragment versucht wird.

enum seasonE { winter, spring, summer, fall; };
enum seasonG { winter, frühling, sommer, herbst; };
seasonE s1 = seasonE.winter;
seasonG s2 = seasonG.winter;
if (s1==s2) ...

Auch in diesem Fall wird bereits zum Übersetzungszeitpunkt durch die Fehlermeldung incomparable types: seasonG and seasonE auf den Fehler hingewiesen.

In Erweiterung der bisher vorgestellten Syntax lassen sich Aufzählungstypen sogar „klassenartig“ ausbauen. Diese Erweiterung erlaubt es die Elemente des Aufzählungstypen wahlfrei an selbstdefinierte Eigenschaften zu binden. Konzeptionell werden diese Eigenschaften dabei als Attribute des Aufzählungstypen aufgefaßt, die durch einen durch den Programmierer bereitzustellenden Konstruktor zugewiesen werden. Der Konstuktorenaufruf erfolgt dabei automatisch durch das Laufzeitsystem zum Definitionszeitpunkt eines Aufzählungstypen für alle konstituierenden Inhaltselemente. Das nachfolgende Beispiel zeigt eine Verwendung des erweiterten Konzepts:

(1)public class EnumTest2 {
(2)	public enum Coin {
(3)		penny(1), nickel(5), dime(10), quarter(25);
(4)    	private int value;
(5)		Coin(int value) {
(6)			System.out.println("creating: "+value);
(7)			this.value = value;
(8)		} //constructor
(9)    	public int value() {
(10)    		return value;
(11)    	} //value()
(12)	} //enum Coin
(13)
(14)	public static void main(String[] args) {
(15)		Coin c1 = Coin.dime;
(16)		System.out.println( c1.value() );
(17)		} //main
(18)} //class EnumTest2

Beispiel 41: Definition eines klassenartigen Aufzählungstyps   EnumTest2.java

Das Beispiel definiert den Typ Coin mit seinen Inhaltstypen penny, nickel, dime und quarter. Den Inhaltstypen wird durch Konstruktoraufruf jeweils ein Inhaltswert zugeordnet. Dieser Wert wird der definierten privaten Variable value zugewiesen.
Durch die Methode value kann der dem jeweiligen Inhaltstyp zugewiesene Wert ausgelesen werden.
Methoden, die den Inhalt der definierten Variable schreiben können zwar definiert werden, jedoch werden Wertänderungen nicht auf die vordefinierten Inhaltstypen synchronisiert.

Die Anzahl der intern mit einem Wert verbundenen Festwerte ist hier bei nicht beschränkt. Das abschließende Beispiel zeigt die Zuweisung von zwei Einzelwerten im Konstruktor:

(1)public class EnumTest3 {
(2)	public enum Time {
(3)		highNoon(12,00), sunSet(20,30), sunRise(5,30), teaTime(17,00);
(4)    	private int hour;
(5)		private int minute;
(6)
(7)		Time(int hour, int minute) {
(8)			this.hour = hour;
(9)			this.minute = minute;
(10)		} //constructor
(11)    	public void printTime() {
(12)    		System.out.print(hour+":"+minute);
(13)    	} //printTime()
(14)	} //enum Time
(15)
(16)	public static void main(String[] args) {
(17)		Time t1 = Time.sunSet;
(18)		System.out.print( "Sunset is at: ");
(19)		t1.printTime();
(20)		} //end main
(21)} //class EnumTest3

Beispiel 42: Definition eines klassenartigen Aufzählungstyps   EnumTest3.java

back to top   2.4.6 Wrapper-Typen

 

Korrespondierend zu jedem primitiven Datentypen in Java gibt es einen Wrapper Typen.
Sie kapselt den zugrundeleigenden Primitivtyp in einer eigenen Klasse, und stellt einige Servicemethoden bereit.

Objekte aller Wrappertypklassen können nur bei ihrer Erzeugung mit Werten versehen werden, die über die gesamte Lebensdauer nicht mehr verändert werden können.

Primitivtyp
Wrapper Typ
void
Dieser Typ ist nicht als Datentyp für Variablen und Attribute verfügbar, sondern kann nur als Rückgabetyp von Operationen spezifiziert werden.
Void
Kann nicht instanziert werden.

Die Abbildung 9 zeigt die Organisation der Wrapperklassen innerhalb der Standard-API im Überblick:

Hierarchie der Wrapperklassen
(1)public class CBRCBV {
(2)	public static void main(String[] args) {
(3)		TestClass testObj = new TestClass();
(4)		int testVar;
(5)		Byte testWrapperType;
(6)
(7)		testObj.setS((short) 42);
(8)		testWrapperType = new Byte((byte) 12);
(9)		testVar = 50;
(10)
(11)		System.out.println("values before method run");
(12)		System.out.println("testVar = "+testVar);
(13)		System.out.println("testWrapperType = "+testWrapperType.byteValue() );
(14)		System.out.println("testObj.s = "+testObj.getS() );
(15)
(16)		aMethod(testVar, testWrapperType, testObj);
(17)
(18)		System.out.println("values after method run");
(19)		System.out.println("testVar = "+testVar);
(20)		System.out.println("testWrapperType = "+testWrapperType.byteValue() );
(21)		System.out.println("testObj.s = "+testObj.getS() );
(22)	} //main()
(23)
(24)	private static void aMethod(int i, Byte b, TestClass tc)	{
(25)		i++;
(26)		b = new Byte((byte) 0);
(27)		tc.setS( (short) (tc.getS()+1) );
(28)
(29)		System.out.println("values within method after modification:");
(30)		System.out.println("testVar = "+i);
(31)		System.out.println("testWrapperType = "+b.byteValue() );
(32)		System.out.println("testObj.s = "+tc.getS() );
(33)	} //aMethod()
(34)} //class CBRCBV
(35)
(36)class TestClass {
(37)	private short s;
(38)
(39)	public short getS() {
(40)		return s;
(41)	} //getS
(42)
(43)	public void setS(short s) {
(44)		this.s = s;
(45)	} //getS()
(46)} //TestClass

Beispiel 43: Verwendung von primitiven, Wrapper und objektwertigen Typen   CBRCBV.java

Bildschirmausgabe:

values before method run
testVar = 50
testWrapperType = 12
testObj.s = 42
values within method after modification:
testVar = 51
testWrapperType = 0
testObj.s = 43
values after method run
testVar = 50
testWrapperType = 12
testObj.s = 43

Im Beispiel werden zunächst Exemplare der drei verschiedenen Typfamilien -- Primitivtyp, Wrapper Typ und Objekt -- erzeugt und mit Werten versehen.
Innerhalb der Methode aMethod, die alle drei Variablen als Übergabeparameter erhält, werden die Inhalte lokal geändert. Während die int-Variable direkt beschrieben werden kann, wird die Änderung des objektwertigen Parameters tc durch eine Methode der Klasse TestClass realisiert. Auf dem Wrapper Typen ist durch die Java-API keine Modifikationsroutine vorgesehen. Daher wird der übergebenen Referenz ein neues Objekt zugewiesen.
Nach Ausführung der Methode aMethod werden die Werte nochmals ausgegeben. Hier zeigt sich, daß auch für Objekte kein echtes Call-by-Reference zur Verfügung steht, sondern lediglich eine Kopie auf die Referenz übergeben wurde. Aus diesem Grunde sind die Änderungen sowohl am übergebenen Primitivtypen als auch an der Variable des Typs Byte verloren.
Als einzige Möglichkeit zur Realisierung von Modifikationen an einem Objekt erweisen sich die Methoden dieses Objekts.

Mehr Information hierzu:

Alle Wrappertypen bieten bestimmte Servicemethoden an, die in der Praxis häufig auftretende Anforderungen erfüllten. Darunter fallen neben der Ausgabe des Wrappertypeninhaltes (d.h. des gekapselten Primtivwertes) auch Umsetzungen der auf Objekten nicht zur Verfügung stehenden Typumwandlungen (casts), sowie Methoden zur Erzeugung der Primitivrepräsentationen aus anderen als den Wrappertypendarstellungen (z.B. aus beliebigen Zeichenketten).

Wichtige und gebräuchliche Servicemethoden, sowie weitere Charakteristika der Wrappertypen:

(1)public class WrapperComparison {
(2)	public static void main(String[] args) {
(3)		Integer i1 = new Integer ( 42 );
(4)		Integer i2 = new Integer ( 42 );
(5)
(6)		System.out.println("i1==i2="+ (i1==i2) );
(7)		System.out.println("i1.equals(i2)="+ (i1.equals(i2) ));
(8)		System.out.println("i1.compareTo(i2)="+ (i1.compareTo(i2)));
(9)	} //main()
(10)} //class WrapperComparison

Beispiel 44: Vergleich von Wrapperobjekten   WrapperComparison.java

Bildschirmausgabe:

i1==i2=false
i1.equals(i2)=true
i1.compareTo(i2)=0

Hinweis:
Obwohl die Semantik der Operation equals für Objekte der Klasse Object und deren Subklassen als größtmögliche unterscheidende Äquivalenzrelation (im Original) festgelegt ist weicht z.B. die Implementierung in der Standard-API-Klasse String von dieser Maßgabe ab. Sie liefert bereits true wenn die beiden Zeichenketten inhaltsgleich sind, jedoch verschiedene Objekte darstellen.

back to top   2.4.7 Boxing/Unboxing

 

Zwar realisiert Java eine grundlegende Trennung zwischen Primitivtypen und Klassen, die Wertausprägungen nicht als Objekte auffaßt. Dennoch wird diese Maßgabe durch das in der Sprachversion 1.5 eingeführte dynamische Umwandlung zwischen primitiven Werten und den zugehörigen Wrapperklassen aufgeweicht. Die Integration dieses als Boxing/Unboxing bezeichnete Verfahren trägt den gesammelten Anwendererfahrungen Rechnung, die eine Vereinfachung der aufwendigen Wrapper-Objekterzeugung bzw. Wertextraktion aus bestehenden Wrapperobjekten fordern. Das namensgebende Begriffspaar bezeichnet hierbei diese beiden Schritte, d.h. Boxing die automatische Wrapper-Objekterzeugung bzw. Unboxing die Wandlung eines objektgekapselten Wertes in einen Primitivwert.

Die automatische Typwandlung wird in Java ausschließlich für Übergabeparameter angeboten, wie das nachfolgende Beispiel zeigt:

(1)public class BUBTest1 {
(2)	public static void main(String[] args) {
(3)		BUBTest1 obj = new BUBTest1();
(4)		obj.boxIt( new Integer(42) ); //nothing new
(5)		obj.boxIt( 42 ); //dynamic boxing
(6)
(7)		obj.unBoxIt( 42 ); //nothing new
(8)		obj.unBoxIt( new Integer(42) ); //dynamic unboxing
(9)	} //main()
(10)	public void boxIt(Integer i) {
(11)		System.out.println("value="+i);
(12)	} //boxIt
(13)	public void unBoxIt(int i) {
(14)		System.out.println("value="+i);
(15)	} //unBoxIt
(16)} //class BUBTest1

Beispiel 45: Dynamisches Boxing/Unboxing   BUBTest1.java

Das Beispiel zeigt neben den jeweils singnaturkonformen Aufrufen auch die Verwendung der dynamischen Typwandlung; für die Methode boxIt die automatische Erzeugung eines innerhalb des Methodenrumpfes referenzierten Wrapper-Objekts bzw. für unBoxIt die Extraktion des im übergebenen Wrapper-Objekt enthaltenen int Primitivwertes.

Der Boxingvorgang kann außer für die Wrapper-Typen auch für Objekte der Klasse Object durchgeführt werden, da diese als Superklasse aller Wrapper-Typen angelegt sind ist diese Konversion stets typkonform. Das nachfolgende Beispiel zeigt die Anwendung:

(1)public class BUBTest2 {
(2)	public static void main(String[] args) {
(3)		BUBTest2 obj = new BUBTest2();
(4)		obj.boxIt( new Integer(42) ); //nothing new
(5)		obj.boxIt( 42 ); //dynamic boxing
(6)
(7)	} //main()
(8)	public void boxIt(Object i) {
(9)		System.out.println("value="+i);
(10)	} //boxIt
(11)} //class BUBTest2

Beispiel 46: Dynamisches Boxing für den Typ Object   BUBTest2.java

Das Beispiel zeigt die bisher schon mögliche Verwendung einer Ausprägung des Typs Integer an einer Stelle, an der eine Ausprägung von Object erwartet wird, was unter Nutzung der Subklassenpolymorphie möglich ist. Ausgehend von diesem Sachstand ist die Realisierung des dynamischen Boxings im Beispiel zu verstehen. Das Auftreten der int-Zahl wird automatisch in eine Ausprägung der Klasse Integer konvertiert, die dann typkonform anstelle der erwarteten Object-Instanz verwendet werden kann.

Die umgekehrte Nutzung, d.h. die Verwendung einer Ausprägung von Object an einer Stelle, an der eine konkrete int-Zahl erwartet wird, ist --- nach Maßgabe der nicht typsicher möglichen Konversion entgegen der Vererbungsrichtung --- nicht möglich.

back to top   2.4.8 Vererbung

 

Wie in objektorientierten Sprachen üblich, untersütützt auch Java das Konzept der Vererbung. Die wesentlichen Hintergründe und Anwendungsgebiete, sowie Designaspekte sind aus früheren Lehrveranstaltungen bekannt, und werden daher an dieser Stelle nicht mehr wiederholt.

Charakteristika:

(1)public class InheritanceDemo {
(2)	public static void main(String[] args) {
(3)		C2 myC2 = new C2();
(4)
(5)		System.out.println("attrib2="+myC2.attrib2);
(6)		System.out.println("inherited attrib1="+myC2.attrib1);
(7)
(8)		C1 myC1 = myC2; //implicit type cast (up cast!)
(9)
(10)		C1 myC11 = new C1();
(11)
(12)		C2 myC22 = (C2) myC11;  //explicit type cast needed (down cast!)
(13)										//will throw a ClassCastException
(14)
(15)	} //main()
(16)} //class InheritanceDemo
(17)
(18)class C1 {
(19)	public int attrib1=1;
(20)} //class C1
(21)
(22)class C2 extends C1 {
(23)	public int attrib2=2;
(24)} //class C2

Beispiel 47: Erzeugung einer ClassCastException   InheritanceDemo.java

Die Umwandlung des C2 Objektes in eines vom Typ C1 erfolgt implizit und automatisch.
Beim Versuch ein Objekt der Klasse c1 in ein Objekt der Klasse C2 umzuwandeln (down cast -- Subklassenobjekt wird in Superklassenobjekt gewandelt) wird eine ClassCastException erzeugt.

(1)public class DynamicBinding {
(2)	public static void main(String[] args) {
(3)		C1 myC1 = new C1();
(4)		myC1.hello();
(5)
(6)		myC1 = new C2();
(7)		myC1.hello();
(8)
(9)		System.out.println("invoking sHello...");
(10)		((C2) myC1).sHello();
(11)	} //main()
(12)} //class DynamicBinding
(13)
(14)class C1 {
(15)	public void hello() {
(16)		System.out.println("Hello from class C1");
(17)	} //hello()
(18)} //class C1
(19)
(20)class C2 extends C1 {
(21)	public void hello() {
(22)		System.out.println("Hello from class C2");
(23)	} //hello()
(24)
(25)	public void sHello() {
(26)		super.hello();
(27)		hello();
(28)	} //superHello()
(29)} //class C2

Beispiel 48: Dynamische Bindung von Methoden   DynamicBinding.java

Bildschirmausgabe:

$java DynamicBinding
Hello from class C1
Hello from class C2
invoking sHello...
Hello from class C1
Hello from class C2

(Relevanter Teil der Ausgabe: oberhalb von invoking sHello) Trotz der Typisierung der Variable myC1 als C1-Ausprägung wird die Methode hello von C2 ausgeführt, wenn der Inhalt der Variable auf ein solches Objekt verweist.

(1)public class ConstDestProp {
(2)	public static void main(String[] args) {
(3)		System.out.println("first creation...");
(4)		new C2();
(5)		System.gc();
(6)
(7)		System.out.println("second creation...");
(8)		new C2(42);
(9)
(10)		System.out.println("third creation...");
(11)		new C2(3.14);
(12)	} //main()
(13)} //class ConstDestProp
(14)
(15)class C1 {
(16)	public C1(int i) {
(17)		System.out.println("constructor of C1 exectued with param i="+i);
(18)	} //C2(int)
(19)
(20)	public C1() {
(21)		System.out.println("constructor of C1 exectued");
(22)	} //C1()
(23)
(24)	public void finalize() {
(25)		System.out.println("destructor of C1 executed");
(26)	} //finalize()
(27)} //class C1
(28)
(29)class C2 extends C1 {
(30)	public C2() {
(31)		System.out.println("constructor of C2 exectued");
(32)	} //constructor
(33)
(34)	public C2(int i) {
(35)		super(i++);
(36)		System.out.println("constructor of C2 exectued with param i="+i);
(37)	} //constructor
(38)
(39)	public C2(double d) {
(40)		System.out.println("constructor of C2 exectued with param d="+d);
(41)	} //constructor
(42)
(43)	public void finalize() {
(44)		System.out.println("destructor of C2 executed");
(45)	} //finalize()
(46)} //class C1

Beispiel 49: Propagierung von Konstruktoren- und Destrukturenaufrufen   ConstDestProp.java

Bildschirmausgabe:

$java ConstDestProp
first creation...
constructor of c1 exectued
constructor of c2 exectued
destructor of c2 executed
second creation...
constructor of c1 exectued with param i=42
constructor of c2 exectued with param i=43
third creation...
constructor of c1 exectued
constructor of c2 exectued with param d=3.14

Zunächst wird ein Objekt der Klasse C2 über den Aufruf des parameterlosen Konstrukturs erzeugt. Vor Ausführung des Konstruktors von C2 wird durch den Übersetzer ein Aufruf an den Superklassenkonstruktor von c1 generiert.
Bei Entfernung des C2-Objektes aus dem Speicher wird dessen Destruktor, nicht jedoch der der Superklasse, aufgerufen.
Die zweite Objekterzeugung nutzt den expliziten parametrisierten Konstruktor C2(int). Innerhalb dieser Methode wird zunächst explizit der Konstruktur identischer Parameterliste der Superklasse explizit per super-Schlüsselwort aufgerufen.
Die dritte Sequenz nutzt den zweiten parametrisierten Konstruktor in C2. In dessen Rumpf ist jedoch kein expliziter Aufruf eines Superklassenkonstruktors plaziert. Daher wird automatisch ein Aufruf an den parameterlosen Konstruktor der Superklasse erzeugt.

back to top   2.4.10 Schnittstellen

 

In C++ nicht explizit präsent. Dort wird die Schnittstellensemantik durch pure virtual classes (abstrakte Klasse die nur abstrakte Methoden besitzt) nachgebildet.
Hinweis: Trotz der teilweise weit gefaßten Interpretation des Begriffes Schnittstelle wurde diese Übersetzung hier für das originalsprachliche Interface verwendet.

Java-Schnittstellen versammeln Operationen, d.h. Methodensignaturen ohne eine Implementierung vorzugeben, sowie Konstante.
Zusätzlich können auch Konstanten definiert werden.

Syntax:

InterfaceDeclaration:
   InterfaceModifiersopt interface Identifier
      ExtendsInterfacesopt InterfaceBody

Auch für Schnittstellen stehen die bekannten Modifier public, protected, private, abstract, static und strictfp zur Verfügung.

Die Sichtbarkeitseinschränkungen sind genauso definiert wie die gleichnamigen Pendants für Klassen. Konsequenterweise muß ein public Interface auch in einer gleichnamigen Quellcodedatei untergebracht werden.
Zwar definiert die Sprachspezifikation (aus Konsistenzgründen zur Definition der Klasse) den Modifier abstract, dieser ist aber für alle Schnittstellen implizit. Die Sprachspezifikation rät sogar explizit von seiner (verwirrenden) Verwendung ab.

Weiterführende Information: Java Language Specification

Charakteristika:

UML-Darstellung des Beispiels
(1)public class Interface1 {
(2)	public static void main(String[] args) {
(3)		TestClass1 tco1 = new TestClass1();
(4)		TestClass2 tco2 = new TestClass2();
(5)		tco1.sayHello();
(6)		tco2.sayHello();
(7)
(8)		if (tco1 instanceof TestClass1)
(9)			System.out.println("tco1 is instance of TestClass1");
(10)
(11)		if (tco1 instanceof PoliteObject)
(12)			System.out.println("tco1 is instance of PoliteObject");
(13)
(14)		PoliteObject po;
(15)		po = tco1;
(16)		po.sayHello();
(17)		po = tco2;
(18)		po.sayHello();
(19)	} //main()
(20)} //class Interface1
(21)
(22)interface PoliteObject {
(23)	public void sayHello();
(24)} //interface PoliteObject
(25)
(26)class TestClass1 implements PoliteObject {
(27)	public void sayHello() {
(28)		System.out.println("Hello World");
(29)	} //sayHello()
(30)} //class TestClass1
(31)
(32)class TestClass2 implements PoliteObject {
(33)	public void sayHello() {
(34)		System.out.println("Guten Tag");
(35)	} //sayHello()
(36)} //class TestClass2

Beispiel 50: Verwendung von Schnittstellen   Interface1.java

Bildschirmausgabe:

$java Interface1
Hello World
Guten Tag
tco1 is instance of TestClass1
tco1 is instance of politeObject
Hello World
Guten Tag

Das Beispiel definiert die Schnittstelle PoliteObject welche die Operation sayHello anbietet. Die beiden Klassen TestClass1 und TestClass2 implementieren jeweils Methoden für die durch die Schnittstelle definierten Operationen.
Der erste Anweisungsblock zeigt die (simple) Ausführung der genannten Methode.
Die darauffolgende Codesequenz hebt den Aspekt der Mehrfachtypisierung (Objekt von Schnittstellen-implementierender Klasse ist sowohl Ausprägung der Klasse selbst (im Beispiel: TestClass1, als auch der Schnittstelle (PoliteObject).
Abschließend wird eine Variable vom Typ der Schnittstelle deklariert, und zunächst mit der Referenz auf ein Objekt der Klasse TestClass1 belegt. Der (statisch typsicher mögliche) Aufruf sayHello ist auch hier möglich. Gleiche Bedingungen herrschen nach der Zuweisung eines Objektes vom Typ TestClass2 an dieselbe Variable.

Einige bekannte Schnittstellenverwendungen:

back to top   2.4.10 Pakete

 

Pakete stellen eine Sammlung von Klassen und Schnittstellen dar, die Zugriffsschutz und Namensraumverwaltung bietet.
siehe Java Language Specification

Aus dieser Definition heraus sind die Java-Pakete mit den aus C/C++ bekannten Header- und Includedateien nur schwer vergleichbar. Anders als in C/C++ werden diese externen Quellcodesequenzen nicht physisch in den zu übersetzenden Strom eingebunden, sondern existieren weiter und unabhängig. Technisch stellen Pakete ein eigenständiges Sprachmittel dar.
Insbesondere dienen sie nicht zur Separierung von Schnittstellendefinition (.h-Datei) und deren Implementierung.

Syntax einer Paketdefinition:
package packageName; am Anfang der Quellcodedatei.
Syntax einer Paketdefinition:
import packageName.subPackage.subSubPackage...className;
Hinweis: Im Gegensatz zur C/C++-Präprozessoranweisung include stellt die import-Definition einen syntaktisch korrekten Javaausdruck dar, der durch ein Semikolon abgeschlossen werden muß.
Paketbenennung: Um Pakete weltweit eindeutig identifizieren und unterscheiden zu können hat sich als Konvention ein an die URL-Notation (IETF RFC 1738) anglehntes Namensschema durchgesetzt. Hierbei werden die URL-Komponenten ausgehend von der Toplevel-Domain von rechts nach links angegeben.
Beispiel: Ein Paket testPackageA im Namensraum myCompany.germany.org würde als org.germany.myCompany.testPackageA abgelegt.

Die einzelnen Hierarchiebene der Paketidentifikation werden im JDK durch Kataloge im Dateisystem nachgebildet in denen die Javadateien des entsprechenden Paketes abgelegt werden müssen. So würde die Datei testPackageA.class im Katalog /com/germany/myCompany plaziert (Durch Substitution der Punkte im vollqualifizierten Klassennamen durch den Verzeichnistrenner / ergibt sich der vollqualifizierte Dateipfad.
Das JDK erfodert es, alle Quellcodedateien einer Paketebene gemeinsam zu übersetzen. Compileraufruf javac File1.java File2.java File3.java ...
Weiterführende Information

Charakteristika:

Hinweise:

Beispiele aus der Java-API:

Beispiel:
Quellcodedateien:

(1)package testPackage;
(2)
(3)public class TestClass {
(4)	public static void sayHello() {
(5)		System.out.println("hello from TestClass contained in testPackage");
(6)	} //sayHello()
(7)} //class TestClass

Beispiel 51: Klasse TestClass innerhalb des Paketes testPackage   TestClass.java

(1)import testPackage.*;
(2)
(3)public class PackageUser {
(4)	public static void main(String[] args) {
(5)		testClass.sayHello();
(6)	} //main()
(7)} //class PackageUser

Beispiel 52: Paketverwendende Datei PackageUser.java   PackageUser.java

Dateiplazierung:
TestClass.java im Verzeichnis testPackage.
packageUser.java im logisch direkt übergeordneten Verzeichnis.

Anmerkung:
Der allgemeine Import aller in testPackage enthaltenen Klassen, Schnittstellen und Pakete könnte auch per import testPackage.TestClass; auf die gewünschte Klasse TestClass eingeschänkt werden.
Ebenso würde der vollqualifizierte Methodenaufruf mit testPackage.TestClass.sayHello(); ohne import-Deklaration auskommen.

Statische Imports

Mit der Überarbeitung zur Sprachversion 1.5 wurde durch die statischen Importe eine abkürzende Schreibweise für importierte statische Methoden etabliert.
Durch die zusätzliche Angabe des Schlüsselwortes static vor der vollqualifizierten Klassenidentifikation stehen als Resultat alle als statisch deklarierten Methoden zum direkten Aufruf, d.h. ohne den Zwang die deklarierende Klasse zu explizieren, zur Verfügung.
Semantisch entspricht die neu geschaffene Importvariante jedoch der bisher vorstellten Mimik. Sie ergänzt diese lediglich um eine Schreibweise die syntaktisch an den Aufruf globaler Funktionen in C/C++ erinnert.
Das nachfolgende Beispiel zeigt die Anwendung beim Aufruf der Methode sin der Standardklasse Math:

(1)import static java.lang.Math.*;
(2)
(3)public class SITest {
(4)	public static void main(String[] atrgs) {
(5)		System.out.println("sin(42)="+ sin(42) );
(6)	} //main()
(7)} //class SITest

Beispiel 53: Statischer Import   SITest.java

back to top   3 Die Java-Plattform

 

back to top   3.1 Die Laufzeitumgebung

 

back to top   3.1.1 Garbage Collection

 

Wie bereits im Einführungskapitel angeschnitten, verfügt die Javalaufzeitumgebung über eine Speicherverwaltung automatischer Speicherbereinigung (engl. garbage collection) des dynamisch verwalteten Heaps.
Generell kann Speicher durch den Programmierer nur konsumiert, nicht jedoch wieder freigegeben werden. (Zur Erinnerung: Die explizite Nullzuweisung an eine Objektvariable markiert den dadurch referenzierten Speicherplatz als nicht mehr benötigt, gibt ihn jedoch nicht direkt frei.)
Speicherbereiche werden in Java durch das Java-Schlüsselwort new, für Java-Objekte, bzw. durch die explizite Definition von primitiven Datentypen, reserviert. Konkret geschieht die Speicherreservierung auf dem Heap ausschließlich durch die Byte-Code-Instruktionen new, newarray, anewarray und multianewarray.
Explizite Freigabemöglichkeiten, wie durch delete in C++, existieren nicht.

Zum Begriff garbage collection: Die Wortwahl impliziert, daß nicht mehr benötigter Speicher als Abfall behandelt wird, der wegzuwerfen ist. Jedoch implementiert die automatische Speicherbereinigung nicht das intuitiv damit verknüpfte Bild des weggebens...
Vielmehr führt die garbage collection ein Speicher Recycling durch, in dessen Verlauf wiederverwendbare Speicherbereiche erkannt, und einer neuen Nutzung zugeführt werden.

Unabhängig von der tatsächlichen Implementierung vollzieht sich die Speicherbereinigung in zwei Schritten:

Als zusätzliche Implementierungsrestriktion tritt unter praktischen Gesichtspunktennoch hinzu, daß der Garbage-Collector-Lauf möglichst wenig (im Idealfalle: keinen) zusätzlichen Speicher beansprucht. Nur so kann das funktionieren auch bei knappem Speicher noch gewährleistet werden.

Für die Aufgabenstellung der automatischen Freispeichergewinnung werden daher sog. mark and sweep-Algorithmen eingesetzt. Die zugrundeliegende Vorgehensweise kann wie folgt beschrieben werden: während seiner periodischen Durchläufe markiert der Speicherbereinigungsprozeß alle erreichbaren Speicherobjekte (mark-Phase). Nach Analyse aller möglichen Zugriffspfade werden die unmarkieren (da unreferenzierten) Speicherobjekte automatisch freigegeben (sweep-Phase).

Während der Mark-Phase werden die Speicherreferenzen verändert, daher wird die Programmausführung zu diesem Zeitpunkt unterbrochen. Als Konsequenz hat die Speicherbereinigung meßbare Auswirkung auf die Ausführungszeit. Die Hauptzeit wird jedoch während der Sweep-Phase verbracht, deren Dauer letztlich von der Größe des zur Verfügung stehenden Arbeitsspeichers abhängt.

Die durch das Mark-and-Sweep-Verfahren bedingte zeitweilige Unterbrechung der Programmausführung ist für interaktive- und Realzeitanwendungen denkbar schlecht geeignet. Daher kommen in der Praxis, so auch in Java, zumeist modifizierte -- aber aufwendigere -- Verfahren zum Einsatz, welche einzelne Speicherobjekte dynamisch sperren. Die Modifikationen setzen an der Erkenntnis an, daß sowohl die Markierungs- als auch die Löschphase inkrementell organisiert sind, d.h. sie betreffen nicht die Gesamtheit der speicherresidenten Objekte. Daher eignen sich beide zu einer kontrolliert parallelen Ausführung, die „lediglich“ dafür Sorge tragen muß, daß die aktuell durch den Garbage Collector im Zugriff befindlichen Speicherstrukturen entsprechend gesperrt sind.

Dieses Verfahren ist der naiven Referenzzählung (separate Tabelle enthält Markierung für jedes Objekt, ob Referenzen darauf existieren) deutlich überlegen, da auch zirkulär verkettete nicht erreichbare Speicherstrukturen als unerreichbar erkannt werden können.

Das nachfolgende Beispiel legt sukzessive verschiedene Speicherstrukturen an, im Folgenden wird der Ablauf des mark and sweep-Algorithmus verdeutlicht:

Anmerkung: Im Vorgriff auf die Behandlung der Collection API verwendet die Implementierung der Klasse node die Klasse HashSet und die Schnittstelle Iterator, auf die in Kapitel 3.2.4 näher eingegangen wird.

(1)import java.util.HashSet;
(2)import java.util.Iterator;
(3)
(4)public class GCTest4 {
(5)	static int m1;
(6)
(7)	void aMethod(Node paramNode) {
(8)		Node m2 = new Node("n1");
(9)		Node tmpNode;
(10)
(11)		tmpNode = m2.createChild("n11");
(12)		tmpNode.createChild("n111");
(13)		tmpNode = m2.createChild("n12");
(14)		tmpNode.createChild("n121");
(15)		tmpNode.createChild("n122");
(16)		tmpNode.createChild("n123");
(17)
(18)		Node m3 = new Node("n2");
(19)		tmpNode = m3.createChild("n21");
(20)		tmpNode.appendChild( m3 );
(21)
(22)		System.out.println("output just for testing reasons...");
(23)		System.out.println("name of parameter Node: "+paramNode.getName() );
(24)		System.out.println("name of Node referenced by m2: "+m2.getName() );
(25)		System.out.println("child's names:");
(26)		m2.getChildsNames();
(27)
(28)		System.out.println("name of Node referenced by m3: "+m3.getName() );
(29)		System.out.println("is n21 child of n2? "+m3.isChild("n21") );
(30)		System.out.println("is n2 child of n21? "+tmpNode.isChild("n2") );
(31)
(32)		tmpNode = new Node("n31");
(33)
(34)		((tmpNode.createChild("n32")).createChild("n33")).appendChild(tmpNode);
(35)
(36)		tmpNode = null;
(37)		System.gc();
(38)	} //aMethod()
(39)
(40)	public static void main(String[] args) {
(41)		(new GCTest4()).aMethod(new Node("n4"));
(42)	} //end main()
(43)} //class GCTest4
(44)
(45)class Node {
(46)	HashSet childs = new HashSet();
(47)	String name;
(48)
(49)	public Node(String name) {
(50)		this.name = name;
(51)		System.out.println("created Node object named "+this.name);
(52)	} //Node(String)
(53)
(54)	public void appendChild(Node child) {
(55)		childs.add(child);
(56)	} //appendChild(Node)
(57)
(58)	public Node createChild(String name) {
(59)		Node newChild = new Node( name );
(60)		childs.add(newChild);
(61)		return newChild;
(62)	} //createChild(String)
(63)
(64)	public String getName() {
(65)		return name;
(66)	} //getName()
(67)
(68)	public boolean isChild(String name) {
(69)		Iterator childIt = childs.iterator();
(70)		while (childIt.hasNext()) {
(71)			if ( ((Node) childIt.next()).getName() == name)
(72)				return true;
(73)		} //while
(74)		return false;
(75)	} //isChild(String)
(76)
(77)	public void getChildsNames() {
(78)		Node child;
(79)		Iterator childIt = childs.iterator();
(80)		while (childIt.hasNext()) {
(81)			child = (Node) childIt.next(); //explicit (down) type cast needed since HashMap contains only Object instances
(82)			System.out.println( child.getName() );
(83)			child.getChildsNames();
(84)		} //while
(85)	} //getChildsNames()
(86)	protected void finalize() {
(87)		System.out.println("Node object named "+name+" freed!");
(88)	} //finalize()
(89)} //class Node

Beispiel 54: verschiedene Speicherstrukturen   GCTest4.java

Bildschirmausgabe:

$java GCTest4
created node object named n4
created node object named n1
created node object named n11
created node object named n111
created node object named n12
created node object named n121
created node object named n122
created node object named n123
created node object named n2
created node object named n21
output just for testing reasons...
name of parameter node: n4
name of node referenced by m2: n1
child's names:
n11
n111
n12
n121
n122
n123
name of node referenced by m3: n2
is n21 child of n2? true
is n2 child of n21? true
created node object named n31
created node object named n32
created node object named n33
node object named n31 freed!
node object named n32 freed!
node object named n33 freed!

Ablauf des mark and sweep-Verfahrens:
Zunächst werden die Referenzen aller im Gültigkeitsbereich der aktuell ausgeführten Methode verfolgt. Im Einzelnen sind dies:

Die Menge dieser Speicherreferenzen wird als root set bezeichnet.

Schematische Darstellung der Speicherstrukturen des Beispielprogramms nach null-Setzung von tmpNode, unmittelbar vor Ablauf der Garbage Collectors

Mark-Phase:
Der Speicherbereinigungsprozeß durchläuft ausgehend von den Elementen des root sets , mit dem Ziel die erreichbaren zu markieren. Die sich zunächst intuitiv aufdrängende Vorgehensweise der rekursiven Baumtraversierung verbietet sich, wenn die Restriktion daß der Speicherbereinigungsprozeß nur minimale eigene Speicheranforderungen stellt berücksichtigt werden soll. (Darüberhinaus ergibt sich noch ein weit schwerwiegenderes Problem bei zyklischen Strukturen, wie wir in der Folge noch sehen werden...).
Daher wird die verzeigerte Speicherstruktur iterativ, unter Abhänderung der Zeigerstruktur, durchlaufen. Da der Rekursionsstack als Gedächtnis des genommenen Weges durch den Baum nicht zur Verfügung steht, werden die Vorwärts-Zeiger sukzessive zu Rückkehr-Zeigern modifizert. Dies vollzieht sich in zwei Schritten. Zunächst wird der referenzierte Knoten ermittelt und zwischengespeichert. Dann wird dieser besucht und markiert; und die Zeigerrichtung invertiert. Ist ein Blattknoten erreicht und markiert, so werden die veränderten Zeigerstrukturen zur Rückkehr benutzt (sie zeigen jetzt auf den jeweils hierarchisch übergeordneten Knoten). Während des Aufsteigens werden die Zeicherstrukturen nochmals invertiert, d.h. wieder in ihren ursprünglichen Zustand (zurück) versetzt.

Schematische Darstellung der Speicherstrukturen des Beispielprogramms nach Besuch und Markierung einiger Knoten

Einen Sonderfall, der bei rekursiver Implementierung aufwendig zu behandeln wäre, stellen zirkuläre Strukturen dar. Hierbei handelt es sich allgemein um Zykel beliebiger Länge. Das bedeutet, nach einer gewissen Anzahl von Speicherknoten existiert ein Verweis (zurück) auf einen bereits traversierten Knoten. Als praktisches Beispiel solcher Strukturen seien zyklisch verkettete Listen angeführt.
Solche Strukturen sind in zweierlei Hinsicht bemerkenswert. Zum Einen, stellen sie hinsichtlich effizienter Traversierung eine Herausforderung dar, zum Anderen, da unabhängige Zykel (siehe n31, n32 und n33 im Beispiel) nicht erreichbare, aber gültig referenzierte Strukturen sind.

Die Graphik zeigt schrittweise die Traversierung und Markierung der einzelnen Knoten eines Zykels der Länge 2.

Traversierung zyklischer Strukturen
  1. Umkehrung der Referenzierungsrichtung zwischen der Anwendervariable m3 (Element des root set) und dem als n2 benannten Speicherobjekt.
    n2 wird markiert.
  2. Weiternavigation, durch Verfolgung der Referenz von n2 zum Speicherobjekt n21.
    Umkehrung der Referenz und Markierung des Knotens n21.
  3. Verfolgung der Referenz von n21 zurück zu n2.
    Neuer Knoten (n2) ist bereits markiert. (Implizit ist ein Zyklus erkannt worden!)
    Damit sind wir zwangsläufig am Ende eines Traversierungsastes angelangt, da auf einen markierten Knoten ausschließlich markierte folgen, sofern die Hierarchie absteigent durchlaufen wird.
  4. Verfolgung der Referenz zurück zum Ursprungsknoten.
    Anschließend: Invertierung der Verzeigerung; stellt Ursprungszustand wieder her.
  5. dto.
  6. dto.

Eine Abwandlung des vorhergehenden Falles zyklischer Strukturen stellt die auf n31, n32 und n33 gebildete Struktur dar.
Obwohl auf alle drei erzeugten Objekte gültige Referenzen existieren (n31 zeigt auf n32, n32 auf n33, das wiederum auf n31 verweist) sind die Objekte nach Neuzuweisung (null-Setzung) an tmpNode allesamt nicht mehr erreichbar.

Sweep-Phase:
Sie dominiert den Speicherbereinigungslauf zeitlich. Während die Markierungen vergleichsweise schnell und effizient -- d.h. unter Vermeidung unnötiger Besuchsschritte -- angebracht werden können, wird in der zweiten Phase der gesamte Heap durchlaufen. Dabei wird jedes Speicherobjekt betrachtet, unabhängig davon ob es markiert ist oder nicht.
Nichtmarkierte Speicherobjekte werden in den Freispeicher eingereiht.

Hinweis: Die implementierungsspezifischen Aussagen beziehen sich auf SUNs Java2 (JDK v1.3) Umsetzung.

Technisch ist der Java Garbage Collector eigenständiger niederpriorer Thread innerhalb der virtuellen Maschine ausgelegt. Situations- und plattformabhängig ist dieser Thread synchron oder asynchron realisiert. Im Falle knappen Speichers, oder expliziter Anforderung durch das Programm, läuft er synchron.
Vor der Freigabe des Speicherplatzes eines Objekts wird dessen Destruktormethode aufgerufen.
Inkrementelle Speicherplatzbereinigung (auf Klassenebene) kann für die virtuelle Maschine der Fa. SUN durch die Kommandozeilenoption -Xincgc erzwungen werden.

Der Garbage Collector kann nicht explizit aufgerufen werden. Jedoch erwirkt der Aufruf System.gc() den Versuch zur Speicherbereinigung. Nach Rückkehr der Methode muß eine Speicherbereinigung nicht zwingend erfolgt sein.
Für Objekte die bereits zur Entfernung aus dem Speicher ausgewählt wurden, jedoch die Abarbeitung ihrer Destruktoren noch aussteht, kann der Destruktlauf mit System.runFinalization() angestoßen werden.

Die Javalaufzeitumgebung von SUN java erlaubt den Eingriff in die Standardgarbagecollection über folgende Kommandozeilenschalter:

(1)public class GCTest1 {
(2)	public static void main(String[] args) {
(3)		System.out.println("memory before: "+Runtime.getRuntime().freeMemory() );
(4)		Test1 t1Obj = new Test1();
(5)		System.out.println("memory after: "+Runtime.getRuntime().freeMemory() );
(6)		t1Obj = null;
(7)		System.gc();
(8)		System.out.println("memory after garbage collection: "+ Runtime.getRuntime().freeMemory() );
(9)	} //main()
(10)} //class GCTest1
(11)
(12)class Test1 {
(13)	double testArray[] = new double[100];
(14)	public void finalize() {
(15)		System.out.println("object of class Test1 freed");
(16)	} //finalize()
(17)} //class Test1

Beispiel 55: Speicherverbrauch vor und nach Garbage Collection   GCTest1.java

Bildschirmausgabe:

$java -verbose:gc GCTest1
memory before: 1814608
memory after: 1809824
[Full GC 216K->112K(1984K), 0.0344306 secs]
object of class test1 freed
memory after garbage collection: 1915920

Zunächst wird der aktuelle freie Speicher innhalb der virtuellen Maschine mittels freeMemory() abgefragt (genaugenommen liefert die Methode eine Schätzung des freien Speichers in Byte). Das erzeugte Objekt initialisiert einen double-Array mit 100 Werten. Hieraus errechnet sich ein minimaler Speicherplatzbedarf von 800 Byte für das Objekt t1Obj. Der im Beispiel ermittelte Wert von 4520 Byte wird durch weitere Effekte wie Verwaltungsstrukturen und sonstige Laufzeitinformation verursacht.
Nach Nullzuweisung und explizitem Aufruf der Garbage Collectors mit System.gc() vergrößert sich der freie Speicher wieder. Er nimmt sogar über den Startwert zu. Dies ist der Freigabe von Speicherobjekten geschuldet, die nicht durch den Anwender, sondern durch die virtuelle Maschine selbst erzeugt wurden. Einen Eindruck der, üblicherweise verdeckt, automatisch geladenen Kompontenten liefert der Kommandozeilenschalter verbose:class der der Java-Ausführungsumgebung übergeben werden kann.

Die Realisierung des Garbage Collectors von SUN erhebt nicht den Anspruch in jedem Falle Speichplatz-optimal vorzugehen, d.h. alle potentiell unreferenzierten Objekte zu sofort entdecken und freizugeben. Dies läßt sich in einer Modifikation des obigen Beispiels zeigen:

(1)public class GCTest2 {
(2)	public static void main(String[] args) {
(3)		System.out.println("memory before: "+Runtime.getRuntime().freeMemory() );
(4)		Test1 t1Obj = new Test1();
(5)		t1Obj = null;
(6)
(7)		for (int i=0; i<10; i++) {
(8)			System.gc();
(9)			System.out.println("memory after garbage collection: "+ Runtime.getRuntime().freeMemory() );
(10)		} //for
(11)	} //main()
(12)} //class GCTest2
(13)
(14)class Test1 {
(15)	double testArray[] = new double[100];
(16)	public void finalize() 	{
(17)		System.out.println("object of class Test1 freed");
(18)	} //finalize()
(19)} //class Test1

Beispiel 56: Mehrmaliger Garbage Collectorlauf   GCTest2.java

Bildschirmausgabe:

$java -verbose:gc gcTest2
memory before: 1814640
[Full GC 216K->112K(1984K), 0.0336478 secs]
object of class test1 freed
memory after garbage collection: 1915920
[Full GC 113K->112K(1984K), 0.0382289 secs]
memory after garbage collection: 1915952
[Full GC 113K->112K(1984K), 0.0306936 secs]
memory after garbage collection: 1915952
[Full GC 113K->111K(1984K), 0.0392094 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0315576 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0305033 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0313825 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0304195 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0319465 secs]
memory after garbage collection: 1917008
[Full GC 112K->111K(1984K), 0.0361018 secs]
memory after garbage collection: 1917008

So wird ein weiterer Speicherblock, trotz der bereits erfolgten Entfernung des Objektes aus dem Speicher (siehe Ausgabe des Destruktors), der Größe 193 Bytes erst durch den vierten Garbage Collector Lauf freigegeben.

Auswirkungen auf die Programmierung:
A priori hat das Vorhandensein eines automatischen Speicherbereinigungsmechanismus keine Auswirkungen auf die Algorithmenentwicklung oder -umsetzung. Jedoch können Maßnahmen zur expliziten Freigabe nicht mehr benötigter Speicherbereiche durch den Anwendungsprogrammierer (oftmals) unterbleiben, da das Laufzeitsystem sich um die Freigabe nicht mehr erreichbarer Speicherzellen kümmert. Dies kann beispielsweise bei der Implementierung verketteter Datenstrukturen (Listen, Bäume, etc.) hilfreich sein.
Die entstehenden Algorithmen sind jedoch dann nicht mehr adaptionsfrei auf Systeme ohne garbage collection übertragbar.

Abschlußbemerkungen:

back to top   3.1.2 Virtuelle Maschine

 

Kern der oft apostrophierten Plattformunabhängigkeit der Programmiersprache Java ist die Generierung eines generischen Zwischenformates -- des Byte-Codes. Dieser wird von einer plattformabhängig implementierten Programmeinheit, der Java Virtual Machine (Abk. JVM) interpretativ zur Ausführung gebracht.
Jede Java-Applikation wird auf einer eigenen virtuellen Maschine zur Ausführung gebracht. Dies garantiert eine größtmögliche Abschottung, mit dem Ziel maximierter Sicherheit, der möglicherweise gleichzeitig auf einer realen Maschine ausgeführten Java-Programme voneinander.
Das Konzept der virtuellen Maschine, die als Programm auf einer realen Hardware abläuft, ermöglicht eine vergleichsweise einfache und schnelle Portierbarkeit auf neue Zielumgebungen, da lediglich die virtuelle Maschine an die veränderte reale Maschine angepaßt werden muß.

Der Gedanke virtueller Maschinen, die generischen Zwischencode -- oftmals auch als P-Code bezeichnet -- ausführen, ist nicht neu. Bereits USCD Pascal, E-BASIC und die verschiedenen SmallTalk-Implementierungen, setzt diesen praktisch um.
Die Realisierung der Befehlsfolgen (Opcodes) innerhalb der virtuellen Maschine von Java ähnelt teilweise frappant der Architektur der für die Züricher Pascal-Implementierung entwickelten (abstrakten) P-Maschine.

Inzwischen steht mit der zAAP (zSeries Application Assist Processor)-Hardware für die IBM-Mainframemaschinen z890 und z990 sogar eine vollständige Hardwareimplementierung der JVM zur Verfügung, welche den Charakter der virtuellen zu einer realen Maschine weiterentwickelt.

Ein Beispiel einer vollständig als Softwar realisierten „klassischen“ virtuellen Maschine ist java, die Bestandteil des Java-Development Toolkits von SUN ist.
Bekannte andere virtuelle Maschinen sind: Kaffe oder auch IBMs Jikes-Implementierung
Wie bereits bekannt wird eine Java-Applikation durch den Aufruf java, gefolgt vom Namen der Startklasse und etwaiger Kommandozeilenparameter ausgeführt. Technisch gesehen bewirkt der Aufruf zunächt die Erzeugung einer neuen Instanz der virtuellen Maschine, auf welcher die Programm-Abarbeitbung mit der main-Methode der Startklasse begonnen wird.
Eine Instanz einer virtuellen Maschine existiert, solange Programmfäden (engl. Threads) (genaugenommen: non-deamon Threads) ausgeführt werden, bzw. die virtuelle Maschine explizit beendet wird (mit dem API-Aufruf System.exit()) oder ein Fehler auftritt.

Architektur der virtuellen Maschine (nach: Venners, B.: Inside the Java 2 Virtual Machine, chap. 5)

Die wesentlichen Bestandteile der JVM sind:

Die Java-Stacks sind in stack frames organisert. Jedem Methodenaufruf ist ein Stack-Frame zugeordnet, der beim Aufruf erzeugt (push), und beim Verlassen (pop) vom Stack entnommen wird.
Innerhalb eines Frames befindet sich

Den für den Anwendungsentwickler offensichtlichsten Bestandteil der virtuellen Maschine, bilden jedoch die JVM-Instruktionen -- die Maschinensprache der JVM.
Der Befehlssatz der JVM umfaßt ausschließlich genau ein Byte lange Opcodes.
Die JVM ist generell stack-orientiert. Dies bedeutet, daß Quell- und Zieloperanden der meisten Operationen werden vom Stack entnommen, und das Ergebnis dort abgelegt. Insbesondere existieren, abgesehen von vier Verwaltungsspeicherplätzen je Ausführungs-Thread, keine virtuellen Prozessorregister, um die Implementierungsanforderungen an die reale Plattform zu minimieren.

Als threadlokale Register stehen zur Verfügung:

Die Adresslänge innerhalb der JVM ist auf vier Byte (32 Bit) fixiert. Hieraus ergibt sich ein (theoretischer) Adressraum von 4 GB.
Die initiale und maximale Ausdehnung des Heaps kann durch die Kommandozeilenschalter Xms bzw. Xmx gesteuert werden (Beispiel: java -Xms350M -Xmx500M HelloWorld führt ein einfaches Hello-World-Beispiel mit einer anfänglichen Speicherausstattung von 350 MB aus, die im Verlaufe des Programmablaufs auf höchstens 500 MB anwachsen kann.)

Das Typsystem der JVM lehnt sich eng an das der Hochsprache an. (Zur Erinnerung: primitive Typen in Java)
Zusätzlich erweitert es die Primitivtypen um einen Adresstypen returnAddress und führt explizite Referenztypen auf die verschiedenen high-level Typen (Klassen, Schnittstellen, Arrays) ein.

Typsystem der virtuellen Maschine

Datentypen der JVM:

Jede Bytecode-Instruktion besteht zunächst aus ihrem Opcode, optional gefolgt von den benötigten Operanden. Diese stehen jedoch nicht für sich, sondern sind eingebettet in den organisatorischen Rahmen der class-Datei, deren Format im Anschluß vorgestellt wird.

Die verschiedenen Maschineninstruktionen lassen sich in Klassen einteilen:

Instruktionen zum Zugriff auf lokale Variablen:

Instruktion
Funktion
Rücksprung aus Subroutine, wird in der Implementierung von finally benutzt
Lädt Referenz von spezifisch indizierter Position auf den Operanden-Stack
Lädt Referenz auf Operanden-Stack (Index im Opcode explizit angegeben)
Speichert auf Operanden Stack liegenden Wert in lokale Variable
Legt Referenz in lokaler Variable auf Operanden-Stack ab (Index wird im Opcode explizit angegeben)
Lädt double-Wert aus lokaler Variable auf den Operanden-Stack
Lädt double-Wert vom Operanden-Stack und legt ihn in lokaler Variable ab
Lädt float-Wert aus lokaler Variable auf den Operanden-Stack
Lädt float-Wert vom Operanden-Stack und legt ihn in lokaler Variable ab
Lädt int auf Operanden-Stack (Index im Opcode explizit angegeben)
Lädt int-Wert aus lokaler Variable auf den Operanden-Stack
Legt int-Wert von spezifisch indizierter Position in lokaler Variable auf Operanden-Stack ab
Lädt int-Wert vom Operanden-Stack und legt ihn in lokaler Variable ab
Lädt long-Wert aus lokaler Variable auf den Operanden-Stack
Lädt long-Wert vom Operanden-Stack und legt ihn in lokaler Variable ab
Inkrementiert lokale Variable um fixe int-Zahl

Instruktionen zur expliziten Modifikation des Operanden-Stacks:

Instruktion
Funktion
Ablegen eines byte-Wertes auf dem Operanden-Stack
Ablegen eines short-Wertes auf dem Operanden-Stack
Entnimmt und verwirft (de facto: löscht) obersten Operanden-Stack-Eintrag.
Entnimmt (de facto: löscht) obersten beiden Operanden-Stack-Einträge.
Tauscht die beiden obersten Operanden-Stack-Einträge aus.
Ablegen eines Elements (referenziert über 16-Bit Index) des runtime constant pools auf dem Operanden-Stack
Ablegen eines Elements (referenziert über 32-Bit Index) des runtime constant pools auf dem Operanden-Stack
Legt null auf dem Operanden-Stack ab.
Legt double Konstante auf dem Operanden-Stack ab.
dconst_0 die Konstante 0.0, bzw. dconst_1 den Wert 1.0.
Legt float Konstante auf dem Operanden-Stack ab.
fconst_0 die Konstante 0.0, bzw. fconst_1 den Wert 1.0.
Legt int-Konstante auf dem Operanden-Stack ab.
Es existieren Opcodes für folgende Konstanten: iconst_m1 -- -1; iconst_0 -- 0; iconst_1 -- 1; iconst_2 -- 2; iconst_3 -- 3; iconst_4 -- 4; iconst_5 --5.
Dupliziert oberstes Element des Operanden-Stacks.
dup_x1 legt das neue Element als zweitunterstes auf dem Stack ab.
dup_x2 legt das neue Element, abhängig vom Stack-Inhalt, als zweit- oder drittunterstes auf dem Stack ab.
Dupliziert die beiden obersten Elemente des Operanden-Stacks.
dup2_x1 legt die neuen Elemente als zweitunterstes und folgendes auf dem Stack ab.
dup2_x2 legt die neuen Elemente, abhängig vom Stack-Inhalt, als zweit- oder drittunterstes und folgendes auf dem Stack ab.
Legt long Konstante auf dem Operanden-Stack ab.
Es existieren Opcodes für folgende Konstanten: iconst_0 -- 0; iconst_1 -- 1.

Instruktion zur Steuerung des Kontrollflußes:

Instruktion
Funktion
Bedingungsloser Sprung innerhalb derselben Methode. Der anzugebende branch offset ist auf 16-Bit fixiert, woraus sich ableiten läßt, daß das Opcodesegment einer Methode niemals (in der JVM-Version 1.3) die Größe von 64KByte überschreiten darf. Näheres zu den Einschränkungen der virtuellen Maschine findet sich in im Abschnitt 4.10 der JVM-Spezifikation, sowie in der Diskussion des Class-File Formats.
Bedingungsloser Sprung innerhalb derselben Methode (mit 32-Bit Offset)
Bedingter Sprung im Falle der Gültigkeit der Bedingung.
Die zu vergleichenden Operanden werden als Referenzen übergeben. Als Bedingungen stehen Gleichheit (eq) und Ungleichheit (ne) zur Verfügung.
Bedingter Sprung im Falle der Gültigkeit der Bedingung.
Die zu vergleichenden Operanden werden als int-Werte übergeben. Als Bedinungen stehen zur Verfügung: Gleichheit (eq), Ungleichheit (ne), Kleiner (lt), Kleiner oder Gleich (le), Größer (gt) und Größer oder Gleich (ge).
Bedingter Sprung, nach Vergleich des obersten Elements des Operanden-Stacks mit Null
Für spezifische Vergleiche stehen folgende Opcodes zur Verfügung: Geleichheit (ifeq), Ungleichheit (ifne), Kleiner (iflt), Kleiner oder Gleich (ifle), Größer (ifgt), Größer oder Gleich (ifge).
Bedingter Sprung -- im Falle der Ungleichheit -- nach Vergleich der auf dem Operanden-Stack befindlichen Referenz mit Null
Bedingter Sprung -- im Falle der Gleichheit -- nach Vergleich der auf dem Operanden-Stack befindlichen Referenz mit Null
Unbedingter Sprung, unter Sicherung der Rücksprungadresse auf dem Operanden-Stack, zu 16-Bit Adresse.
Unbedingter Sprung, unter Sicherung der Rücksprungadresse auf dem Operanden-Stack, zu 32-Bit Adresse.
Zugriff auf Sprungtabelle per Schlüssel und anschließende Verzweigung.
Benutzt zur Implementierung des switch-Konstrukts
Indexbasierter Zugriff auf Sprungtabelle und anschließende Verzweigung.

Instruktionen zur Operation auf Klassen und Objekten:

Instruktion
Funktion
Array-Erzeugung an definierter Stelle im Laufzeit-Konstanten-Pool.
Typkompatibilitätsprüfung (siehe Beispiel)
Prüft ob Objekt gegebenen Typ hat (d.h. Ausprägung der Klasse -- oder einer Subklasse -- ist; das Interface implementiert; Array-Kompatibel ist)
Erzeugt neues Objekt
Hinweis: Die Punktnotation zur Trennung der Pakethierarchien wird hier durch Slash „/“ ersetzt.

Instruktionen zur Methodenausführung:

Instruktion
Funktion
Ruft Instanzenmethode auf; mit besonderer Behandlung bestimmter Umstände.
Hinweis: Diese Instruktion wurde umbenannt, frühere JDK-Versionen benutzen invokeonvirtual.
Ruft statische Klassenmethode auf.
Ruft Instanzenmethode auf.
Ruft Schnittstellenmethode auf.
Retourniert Referenz auf Speicherobjekt nach Methodenausführung.
Retourniert double-Wert nach Methodenausführung.
Retourniert float-Wert nach Methodenausführung.
Retourniert int-Wert nach Methodenausführung.
Retourniert long-Wert nach Methodenausführung.
Retourniert nach Methodenausführung ohne Rückgabewert (void-Methode).

Instruktionen zum Zugriff auf Attribute:

Instruktion
Funktion
Legt Attributinhalt eines Objekts auf Operanden-Stack ab.
Die Klasse des Objekts, auf das der Zugriff erfolgen soll, wird über einen 16-Bit Offset auf dem runtime constant pool addressiert. Auch hier wird wieder die Limitierung der virtuellen Maschine auf 216 Klassen deutlich.
Legt Attributinhalt eines statischen Klassenattributes auf dem Operanden-Stack ab.
Setzt Wert eines Attributs.
Setzt Wert eines statischen Klassenattributes.

Instruktionen zur Operation auf Arrays:

Instruktion
Funktion
Erzeugt einen neuen Array, und definiert die Komponententypen als einen der Primitvtypen.
Die Implementierung von SUN verwendet für den Wahrheitswert je acht Bit. Anderen Umsetzungen ist es explizit Freigestellt hier mit optimierteren Speicherstrukturen zu operieren.
Erzeugt einen neuen Array von Referenztypen zur Aufnahme beliebiger Objekte.
Erzeugt einen mehrdimensionalen Array.
Lädt Referenz von Arrayposition.
Speicher Objekt an spezifischer Arrayposition.
Liefert Elementanzahl (Kardinalität) eines Arrays.
Lädt byte oder boolean aus Arrayposition.
Legt byte oder boolean an Arrayposition ab.
Lädt short aus Arrayposition.
Legt short an Arrayposition ab.
Lädt char aus Arrayposition.
Legt char an Arrayposition ab.
Lädt double aus Arrayposition.
Legt double an Arrayposition ab.
Lädt float aus Arrayposition.
Legt float an Arrayposition ab.
Lädt int aus Arrayposition.
Legt int an Arrayposition ab.
Lädt long aus Arrayposition.
Legt long an Arrayposition ab.

Instruktionen zur Typkonversion:

Instruktion
Funktion
Konvertiert int zu byte.
Konvertiert int zu char.
Konvertiert int zu double.
Konvertiert int zu float.
Konvertiert int zu long.
Konvertiert int zu short.
Konvertiert double zu float.
Konvertiert long zu double.
Konvertiert long zu float.
Konvertiert long zu int.
Konvertiert double zu float.
Konvertiert float zu double.
Konvertiert float zu int.
Konvertiert float zu long.
Konvertiert double zu float.
Konvertiert double zu float.
Konvertiert double zu int.
Konvertiert double zu long.

Instruktionen zur Durchführung arithmetischer Operationen:
Eingangsperanden werden vom Stack entnommen und das Berechnungsergebnis ebenda abgelegt.

Instruktion
Funktion
Addiert zwei double-Werte.
Subtrahiert zwei double-Werte.
Dividiert zwei double-Werte.
Multipliziert zwei double-Werte.
Negiert double-Wert durch Zweierkomplementbildung.
Divisionsrest bei Division zweier double-Werte.
Vergleicht zwei double Werte.
Ist einer der beiden Operanden NaN, so legt dcmpg1, dcmpl hingegen -1 als Ergebnis auf dem Stack ab.
Addiert zwei float-Werte.
Subtrahiert zwei float-Werte.
Multipliziert zwei float-Werte.
Dividiert zwei float-Werte.
Addiert zwei int-Werte.
Subtrahiert zwei int-Werte.
Multipliziert zwei int-Werte.
Dividiert zwei int-Werte.
Boole'sche UND-Verknüpfung zweier int-Werte.
Boole'sche ODER-Verknüpfung zweier int-Werte.
Exklusive Boole'sche ODER-Verknüpfung zweier int-Werte.
Negiert int-Wert durch Zweierkomplementbildung.
Divisionsrest bei Division zweier int-Werte.
Linksshift eines int-Wertes.
Rechtsshift eines int-Wertes.
Rechtsshift eines int-Wertes unter Nulleinfügung und Vorzeichenlösung.
Negiert float-Wert durch Zweierkomplementbildung.
Divisionsrest bei Division zweier float-Werte.
Vergleicht zwei floatWerte.
Ist einer der beiden Operanden NaN, so legt fcmpg1, fcmpl hingegen -1 als Ergebnis auf dem Stack ab.
Addiert zwei long-Werte.
Boole'sche UND-Verknüpfung zweier long-Werte.
Vergleicht zwei longWerte.
Dividiert zwei long-Werte.
Multipliziert zwei long-Werte.
Negiert long-Wert durch Zweierkomplementbildung.
Divisionsrest bei Division zweier long-Werte.
Boole'sche ODER-Verknüpfung zweier long-Werte.
Linksshift eines long-Wertes.
Rechtsshift eines long-Wertes.
Subtrahiert zwei long-Werte.
Rechtsshift eines long-Wertes unter Nulleinfügung und Vorzeichenlösung.
Exklusive Boole'sche ODER-Verknüpfung zweier long-Werte.

Sonstige Instruktionen:

Instruktion
Funktion
Ausnahmebehandlung
Wirft eine Exception.
Der Zugriff auf jedes Objekt wird durch einen Monitor synchronisiert. Die Anweisung sperrt ein Objekt.
Gibt ein gesperrtes Objekt frei.
Sonstige Opcodes
Buchstäblich: no operation. Bewirkt nichts; keine Operatoren, keine Änderungen am Operanden-Stack.

Die beiden Opcodes mit den Ordnungsnummern 254 und 255 (0xfe und 0xff, mnemonic impdep1 und impdep2) sind durch SUN als reserviert gekennzeichnet. Sie können von durch den Hersteller der virtuellen Maschine mit eigendefinierter Funktionalität implementiert werden.
Darüberhinaus ist mit dem Opcode 202 (mnemonic breakpoint) ein Einstiegspunkt für Debugger definiert.

Alle Opcodes sind mit einem Byte codiert. Hieraus ergibt 256 als maximaler Befehlsumfang der virtuellen Maschine. Zur Verringerung der notwendigen verschiedenen Befehle sind nicht alle Opcodes für alle Typen der JVM implementiert. Üblicherweise existieren nur Opcodes für int, float, long und double sowie die Referenzen. Für alle anderen Typen stehen Konvertierungsmöglichkeiten in die genannten zur Verfügung.

Opcode
byte
short
int
long
float
double
char
Referenz
...ipush
bipush
sipush
...const
iconst
lconst
fconst
dconst
aconst
...load
iload
lload
fload
dload
aload
...store
istore
lstore
fstore
dstore
astore
...inc
iinc
...aload
baload
saload
iaload
laload
faload
daload
caload
aaload
...astore
bastore
sastore
iastore
lastore
fastore
dastore
castore
aastore
...add
iadd
ladd
fadd
dadd
...sub
isubb
lsub
fsub
dsub
...mul
imul
lmul
fmul
dmul
...div
idiv
ldiv
fdiv
ddiv
...rem
irem
lrem
frem
drem
...neg
ineg
lneg
fneg
dneg
...shl
ishl
lshl
...shr
ishr
lshr
...ushr
iushr
lushr
...and
iand
land
...or
ior
lor
...xor
ixor
lxor
i2...
i2b
i2s
i2l
i2f
i2d
l2...
l2i
l2f
l2d
f2...
f2i
f2l
f2d
...cmp
lcmp
...cmpl
fcmpl
dcmpl
...cmpg
fcmpg
dcmpg
if_...cmpcond
if_icmpcond
if_acmpcond
...return
ireturn
lreturn
freturn
dreturn
areturn

Treten bei der Ausführung der Opcodes Ausnahmen auf, so werden durch die virtuelle Maschine Laufzeit-Ausnahmeereignisse (engl. runtime exception) generiert. Wie bekannt werden Ausnahmen dieser Kategorie (üblicherweise) nicht aufgefangen und behandelt.
So wird die ClassCastException im Beispiel aus Kapitel 2 durch die versuchte explizite Typumwandlung ausgelöst.
Der erzeugte Bytecode für diese Anweisung lautet:

aload_3
checkcast 2

aload_3 lädt die Referenz auf eine lokale Variable auf den Operanden-Stack. Die lokale Variable 3 entspricht im Beispiel myC11.
checkcast testet ob die auf dem Operanden-Stack befindliche Referenz kompatibel zum übergebenen Typen (hier die 2 als Referenz auf die zweite geladene Klasse; benannt mit C2) ist. Im Falle der Nichtkompatibilität wird durch die virtuelle Maschine eine ClassCastException erzeugt.

Beispiel 1: Einfache arithmetische und Ein-/Ausgabeoperationen
Beispiel 1: Einfache arithmetische und Ein-/Ausgabeoperationen
(1).class public examples/BC1
(2).super java/lang/Object
(3)
(4).method public <init>()V
(5)   aload_0
(6)   invokenonvirtual java/lang/Object/<init>()V
(7)   return
(8).end method
(9)
(10).method public static main([Ljava/lang/String;)V
(11)	.limit locals 5
(12)   .limit stack 10
(13)
(14)	iconst_2
(15)	istore_0
(16)
(17)	bipush 101
(18)	istore_1
(19)
(20)	bipush 99
(21)	istore_2
(22)
(23)	;we will need this twice
(24)	getstatic java/lang/System/out Ljava/io/PrintStream;
(25)	astore_3
(26)
(27)	iload_1
(28)	iload_2
(29)	iadd
(30)	istore_1
(31)
(32)	;convert int to string
(33)	iload_1
(34)	invokestatic java/lang/String/valueOf(I)Ljava/lang/String;
(35)	astore 4
(36)
(37)	;Print a string
(38)	aload_3
(39)	aload 4
(40)	invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
(41)
(42)	iload_1
(43)	iload_0
(44)	idiv
(45)
(46)	;convert int to string
(47)	invokestatic java/lang/String/valueOf(I)Ljava/lang/String;
(48)	astore 4
(49)
(50)	;Print a string
(51)	aload_3
(52)	aload 4
(53)	invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
(54)
(55)	;print a fixed string
(56)	aload_3
(57)	ldc "The End"
(58)	invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
(59)   return
(60).end method
(61)
Download des Beispiels


Der Java-Assemblercode des Beispiels 1 zeigt die Verwendung einiger einfacher arithmetischer und Ein-/Ausgabeoperationen.
Zunächst zeigt das Beispiel den Aufbau einer Java-Assemblerdatei, wie sie vom Übersetzer Jasmin akzeptiert und in ausführbaren Java-Bytecode umgewandelt wird.
So legt die .class-Deklaration zunächst fest, daß es sich um die öffentlich zugängliche (d.h. als public deklarierte) Klasse BC1, im Paket examples handelt.
Die darauf folgende .super-Definition legt die Elternklasse der betrachteten Klasse fest. Im Falle keiner explizit definierten Elternklasse ist dies vorgabegemäß die Standardklasse Class.
Nach den einführenden Deklarationen definiert die Quellcodedatei den statischen Initialisierter.

Die Methode main stellt den Beginn der aktiven Verarbeitung dar. Ihre Signaturdefinition ([Ljava/lang/String;)V läßt die JVM-interne Kurzschreibweise des Typsystems erkennen. So deutet die in den Klammern der Übergabewerte eingeschlossene eckige Klammern an, daß ein Array gleichtypisierter Ausprägungen übergeben wird. Diese Ausprägungen sind alle vom Standardtyp java.lang.String (die paket-separierenden Punkte werden JVM-intern zu Pfadseparatoren aufgelöst). Zusätzlich ist dem Klassenname ein einleitendes L vorangestellt, um auszudrücken, daß es sich nicht um einen Primitivtyp, sondern um eine Sprachkomponente (das „L“ deutet hierbei auf den Begriff language hin) handelt.
Nach der Klammer ist der Rückgabetyp --- im Falle von main vorgabegemäß void --- angegeben. Auch er wird unter Verwendung derselben Abkürzungskonvention dargestellt.

Zu Eingangs der Methode main allozieren die beiden Direktiven .limit zunächst Speicher für die lokalen Variablen (.limit locals) und die Tiefe des methodenintern verwendeten Operandenstacks (.limit stack).

Die (aktive durch den Programmierer gesteuerte) Verarbeitung beginnt im Beispiel mit dem Anweisung iconst_2 welche den ganzzahligen Wert 2 auf dem Operandenstack ablegt. Anschließend wird dieser Wert, mittels der Anweisung istore_0 vom Stack entnommen und in die erste lokale Variable gespeichert.
Mit iconst_2 findet ein besonderer Befehl zur Ablage einer ganzzahligen Konstante auf dem Operanden Stack Verwendung, der es gestattet bestimmte (häufig benötigte) Konstantenablagen in nur genau einem Byte auszudrücken. Durch die JVM-Spezifikation vorgesehen sind hierbei Instruktionen für die Konstanten -1, 0, 1, 2, 3, 4 oder 5. Im Ergebnis ist die Nutzung der abkürzenden Befehlsschweibweise äquivalent zum Einsatz der Instruktion bipush unter Explizierung der abzulegenden Konstante.

Diese äquivalente Form der Belegung einer lokalen Variable zeigt der zweite Anweisungsblock, der die numerische Konstante 101, für die keine abkürzende Schreiweise angeboten wird, auf dem Operandenstack ablegt um sie der zweiten lokalen Variable (mit der Indexnummer 1) zuzuweisen.
In derselben Weise wird für die Initialisierung der dritten lokalen Variablen mit dem Wert 99 verfahren.

Anschließend wird durch getstatic der Dateideskriptor der Standardausgabe (d.h. desjenigen Streams mit dem Wert System/out) gelesen und die zurückgelieferte Adresse in der vierten lokalen Variable (Indexnummer 3) abgelegt.

Der darauffolgende Anweisungsblock zeigt die Umsetzung einer einfachen Ganzzahladdition, die zunächst die beiden zu verknüpfenden Operanden (die Inhalte der lokalen Variablen mit den Indexnummern 1 und 2) auf dem Stack ablegt und anschließend mittels der Ganzzahladdition (iadd) verknüpft.
Das auf dem Stack abgelegte Berechnungsergebnis wird durch istore_1 er zweiten lokalen Variablen zugewiesen.

Der nächste Anweisungsblock bereitet die Ausgabe des Berechnungsergebnisses auf der Standardausgabe vor.
Hierzu plaziert er zunächst den Inhalt der lokalen Variable mit der Indexnummer 1 (d.h. das Berechnungsergebnis des direkt vorhergehenden Schrittes) auf dem Stack.
Anschließend wird eine Standard-API-Methode (die Methode valueOf) aufgerufen, welche den auf dem Stack übergebenen int-Parameter in eine Zeichenkette wandelt und die Referenz darauf als Rückgabewert auf dem Stack plaziert.
Dieser Rückgabewert wird in der fünften lokalen Variable (Indexnummer 4) abgelegt.

Anschließend werden die in zwischenzeitlich den vierten und fünften lokalen Variablen abgelegten Adressen des Ausgabe-Streams und der auszugebenden Zeichenkette geladen und auf dem Operanden-Stack abgelegt.
Durch Aufruf der Standard-Ausgabemethode println mittels invokevirtual wird die referenzierte Zeichenkette auf der Standardausgabe dargestellt.

Der folgende Anweisungsblock demonstriert eine Ganzzahldivision mittels idiv welche Divisior und Dividenden als Operadnen auf dem Stack erwartet und das Berechnungsergebnis ebenda plaziert.

Anschließend wird das (noch auf dem Stack liegende) Berechnungsergebnis direkt weiterverarbeitet und in eine Zeichenkette gewandelt. Hierbei kommt die bereits bekannte Funktion zum Einsatz.
Danach erfolgt wiederum die Ausgabe in der bekannten Form.

Abschließend wird eine fixe Zeichenkette ausgegeben, deren Zeichenkettendarstellung nicht berechnet zu werden braucht. Ihr Wert kann daher direkt aus dem Laufzeitkonstantenpool per ldc geladen werden.
Die übrigen Schritte zur Erzeugung der Ausgabe bleiben indes unverändert.

Nutzung von Methoden:
Bereits bei den einfachen Operationen aus Beispiel 1 zeigt sich, daß die wiederholte Angabe von sehr ähnlichen Instruktionsfolgen nicht zu vermeiden ist. Insbesondere die beiden Konversionen des int-Datentyps als Voraussetzung der zeichenbasierten Ausgabe ist vollständig identisch.
Zur Strukturierung stehen daher auf der Java-Assemblerebene die bereits aus der Java-Hochsprache bekannten Methoden zur Verfügung, wie Beispiel 2 zeigt.

Beispiel 2: Nutzun von Methoden
Beispiel 2: Nutzun von Methoden
(1).class public examples/BC2
(2).super java/lang/Object
(3)
(4).method public <init>()V
(5)   aload_0
(6)   invokenonvirtual java/lang/Object/<init>()V
(7)   return
(8).end method
(9)
(10).method public static printInt(I)V
(11)	.limit locals 2
(12)	.limit stack 2
(13)
(14)	iload_0
(15)	invokestatic java/lang/String/valueOf(I)Ljava/lang/String;
(16)	astore_1
(17)
(18)	getstatic java/lang/System/out Ljava/io/PrintStream;
(19)	aload_1
(20)	invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
(21)	return
(22).end method
(23)
(24).method public static main([Ljava/lang/String;)V
(25)	.limit locals 50
(26)   .limit stack 40
(27)
(28)	iconst_2
(29)	istore_0
(30)
(31)	bipush 101
(32)	istore_1
(33)
(34)	bipush 99
(35)	istore_2
(36)
(37)	;we will need this twice
(38)	getstatic java/lang/System/out Ljava/io/PrintStream;
(39)	astore_3
(40)
(41)	iload_1
(42)	iload_2
(43)	iadd
(44)	istore_1
(45)
(46)	iload_1
(47)	invokestatic examples/BC2/printInt(I)V
(48)
(49)	iload_1
(50)	iload_0
(51)	idiv
(52)
(53)	invokestatic examples/BC2/printInt(I)V
(54)
(55)	;print a fixed string
(56)	aload_3
(57)	ldc "The End"
(58)	invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
(59)   return
(60).end method
Download des Beispiels


Die Funktionalität des Beispieles ist mit der der vorhergend vorgestellten Codesequenz identisch. Jedoch finden sich jetzt die Instruktionsfolgen zur Berechnung der Zeichenkettenrepräsentation einer Ganzzahl und ihrer anschließenden Ausgabe in die Methode printInt ausgelagert.

Diese Methode akzeptiert eine Ausprägung des Primitivtyps int als Übergabe und liefert keinen Rückgabewert.
Die Signatur ist daher dahingehend vereinbart, daß genau eine int-konforme Zahl als Parameter auf dem Stack erwartet wird, d.h. der Aufrufer hat diese vor dem Aufruf dort abzulegen.

Zusätzlich benötigt die Methode selbst zu ihrer Ausführung einige lokale Variablen, die auf dem methodenspezifischen Stack abgelegt werden. Dieser stellt eine Erweiterung des bereits durch den Aufruf verwendeten Operandenstacks dar.

Mit Java steht jedoch keineswegs die einzige Hochsprache zur Erzeugung von Byte-Code-Dateien zur Verfügung. Diese Seite listet eine Vielzahl verschiedener Alternativen.
Beispielsweise erzeugt der Oberon-Compiler von Canterbury für alle Oberon-Module, einschließlich der Systemmodule, Java-Klassen.

Beispiel 3: Die Hello World Applikation als Oberon Programm
Beispiel 3: Die Hello World Applikation als Oberon Programm
MODULE helloworld;

IMPORT Out;

BEGIN
  Out.String( "Hello World" );
  Out.Ln;
END helloworld.
Download des Beispiels


Die erzeugten class-Dateien -- SYSTEM.class, helloworld.class, Out.class, Sys.class -- können auf jeder JVM zur Ausführung gebracht werden. java helloworld liefert das erwartete Ergebnis.



Das Class-File-Format

Ausgangspunkt jeder Programmausführung innerhalb der JVM ist die class-Datei als Eingabe. Sie wird üblicherweise durch durch den Java-Compiler (im JDK: javac erzeugt).

Einige Eigenschaften jedes class-Files:

Die JVM-Spezifikation legt zur Definition der Struktur des class-Files eigene Datentypen fest: u1, u2 und u4 zur Definition vorzeichenloser ein-, zwei- und drei-Bytetypen. Für diese (von der Java-üblichen vorzeichenbehafteten Mimik (abgesehen von char) abweichenden) Datentypen stehen mit readUnsignedByte(), readUnsignedShort() und readInt() entsprechende Lesemethoden zur Verfügung.

The class File Format @ Java Virtual Machine Specification

ClassFile {
      u4 magic;
      u2 minor_version;
      u2 major_version;
      u2 constant_pool_count;
      cp_info constant_pool[constant_pool_count-1];
      u2 access_flags;
      u2 this_class;
      u2 super_class;
      u2 interfaces_count;
      u2 interfaces[interfaces_count];
      u2 fields_count;
      field_info fields[fields_count];
      u2 methods_count;
      method_info methods[methods_count];
      u2 attributes_count;
      attribute_info attributes[attributes_count];
    }

Constant Pool @ Java Virtual Machine Specification

cp_info {
      u1 tag;
      u1 info[];
    }

field_info @ Java Virtual Machine Specification

field_info {
        u2 access_flags;
        u2 name_index;
        u2 descriptor_index;
        u2 attributes_count;
        attribute_info attributes[attributes_count];
           }

method_info @ Java Virtual Machine Specification

method_info {
        u2 access_flags;
        u2 name_index;
        u2 descriptor_index;
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }

attribute_info @ Java Virtual Machine Specification

  attribute_info {
        u2 attribute_name_index;
        u4 attribute_length;
        u1 info[attribute_length];
    }

Aufbau einer class-Datei verdeutlicht an nachfolgendem Beispielquellcode.

Hinweis: Mit Classeditor existiert ein freies Werkzeug zur Inspektion und Modifikation übersetzter Class-Dateien.

Beispiel 4: Java-Quellcode der untersuchten Klassendatei
Beispiel 4: Java-Quellcode der untersuchten Klassendatei
(1)class Act {
(2)	public static void doMathForever() {
(3)		int i=0;
(4)		while (true) {
(5)			i += 1;
(6)			i *= 2;
(7)		} //while
(8)	} //doMathForever()
(9)} //class Act
Download des Beispiels


magic Identifier

Der magic-Identifier ist auf die Bytekombination (in hexadezimaler Darstellung) CA FE BA BE fixiert. Anhand dieser erkennt der Kassenlader der Laufzeitsystems die Datei als ausführbare Java-Bytecode-Datei an.
Ist diese gegenüber dem Vorgabewert modifiziert wird eine java.lang.ClassFormatError Ausnahme im Hauptthread generiert (Bad magic number wird als zusätzliche Nachricht der Ausnahme ausgegeben). siehe JVM-Spezifikation

version identifier

Die beiden Versionskennungen minor und major bilden gemeinsam den Versionsschlüssel der class-Datei in der gängigen Punktnotation. Hierbei gilt: major.minor
Die Klassendatei des Beispiels trägt den Versionsschlüssel 45.3.
Der im SUN JDK enthaltene Java-Compiler erlaubt per Kommandozeilenparameter (target) die JVM-spezifische Steuerung der Codegenerierung. Die den einzelnen Sprachversionen zugeordneten Bytecodeversionen sind in der nachfolgenden Tabelle zusammengestellt.

Java-Version
Bytecode-Version
1.1
45.3
1.2
46.0
1.3
47.0
1.4 (sowie 1.4.1, 1.4.2 und der Prototyp des 1.5-Compilers)
48.0
Zweite Vorabversion des 1.5-Compilers
50.0
1.5 (beta1)
49.0
1.5 (beta2)
49.29

Vorgabegemäß wird durch die Compilerversion 1.5 (ab Beta-Version 2) 49.29 erzeugt.
Trägt eine class-Datei eine durch die JVM nicht unterstützte Versionsnummer, so wird eine java.lang.UnsupportedClassVersionError-Ausnahme generiert.
Jede JVM kann verschiedene class-Datei-Versionen unterstützen, die letzendlich Festlegung welche Versionen durch einzelne Java-Plattform-Releases zu unterstützten sind obliegt jedoch SUN. So unterstützt SUNs JDK v1.0.2 class-Dateien der Versionen 45.0 bis einschließlich 45.3. Die JDK-Generation v1.1.x ab Version 45.0 bis einschließlich 45.65535 und Implementierungen der Java 2 Plattform, Version 1.2, bis einschließlich 46.0. JDK v1.3.0 verarbeitet Klassendateien bis hin zur Versionsnummer 47.0. Zur Verarbeitung von Klassen, welche die in 1.5 eingeführten Generizitätsmechanismen verwenden nicht zwingend eine Ausführungsumgebung dieser Versionsnummer benötigt, da das erzeugte Klassenformat (bisher, da diese Aussagen auf dem Informationsstand der verfügbaren Betaversion basieren) nicht verändert wurde. Die Nutzung des dynamischen Boxing/Unboxings benötigt jedoch eine Ausführungsumgebung mindestens der Version 1.5.
siehe JVM-Spezifikation

constant pool count

Die Bytefolge constant_pool_count enthält die um eins erhöhte Anzahl der Einträge der constant_pool Tabelle.
Im Beispiel ist dies: 17.
siehe JVM-Spezifikation

Der constant pool enthält die symbolische Information über Klassen, Schnittstellen, Objekte und Arrays. Die Elemente des dieser Datenstruktur sind vom Typ cp_info und variieren je nach tag in ihrer Länge. Als Konstantentypen (=Inhalt des Tag-Bytes) sind zugelassen:

Konstantentyp
Wert
CONSTANT_Utf8
1
CONSTANT_Methodref
10
CONSTANT_InterfaceMethodref
11
CONSTANT_NameAndType
12
CONSTANT_Integer
3
CONSTANT_Float
4
CONSTANT_Long
5
CONSTANT_Double
6
CONSTANT_CLASS
7
CONSTANT_String
8
CONSTANT_Fieldref
9

siehe JVM-Spezifikation

erstes Element des constant pools / Referenz auf Superklasse

Konstante vom Typ CONSTANT_class die einen Verweis auf auf das zwölfte Element des Konstantenpools (0x0C) enthält. Zum Lesezeitpunkt der ersten Konstante kann diese Referenz noch nicht aufgelöst und auf Gültigkeit geprüft werden. (Später sehen wir, daß es sich um eine Referenz auf java.lang.Object handelt).
Hierbei handelt es sich immer um die Referenz auf die Superklasse. Auch für die API-Klasse Object selbst findet sich diese Refenz in der Klassendatei, auch wenn Diassemblierungswerkzeuge wie javap diese nicht ausgeben.

zweites Element des constant pools / this Referenz

Konstante vom Typ CONSTANT_class die einen Verweis auf auf das 13. Element des Konstantenpools (0x0D) enthält. An dieser Stelle findet sich der String Act, also der Klassenname der zur Klassendatei gehörigen Klasse selbst. Auch hierbei handelt es sich zunächst um eine nicht auflösbare Vorwärtsreferenz.
Technisch gesehen realisiert sie den this-Verweis.

drittes Element des constant pools / Konstruktoren-Verweis

Konstante vom Typ CONSTANT_Methodref. Die folgenden zwei Bytes (im Beispiel: 0x00 01) bezeichnen das referenzierte Objekt, gefolgt vom Methodenindex (0x00 04). Im Beispiel handelt es sich um das Objekt mit der Referenznummer 1 (=erstes Element des Konstantenpools, die Superklasse Object). Unter der Referenznummer 0x04 wird auf die Methode <init>() verweisen. Da die betrachtete Klasse Act keinen eigenen Konstruktor definiert, wird der der Superklasse aufgerufen.

viertes Element des constant pools / Rückgabetyp des 14. Elements

Konstante vom Typ CONSTANT_NameAndType. Die folgenden beiden Bytes (0x00 0E) verweisen auf die zu beschreibende Position innerhalb des Konstantenpools (im Beispiel: init). Dieser Position wird der and Position 0x10 spezifizierte Typ als Rückgabetyp zugeordnet -- im Beispiel ()V; also void.

fünftes Element des constant pools / Konstante Zeichenkette

Konstante vom Typ CONSTANT_Utf8 leitet einen konstenten Zeichenketten-Ausdruck ein.
Zeichenketten werden generell im Unicode UTF-8 Format abgelegt, wobei jedoch aus Speicherplatzeffizienzgründen für die Zeichen zwischen \u0001 und \u007F nur jeweils ein Byte benötigt werden. Für alle anderen Unicode-Symbole wird der entsprechende 2- bzw. 3-Byte Speicherplatz zur Verfügung gestellt.
Die Konstante wird von der Länge der Zeichenkette (im Beispiel: 13) gefolgt, daher kann auf eine terminierende Null verzichtet werden.
Die Bedeutung dieser -- im Java-Quellcode nicht enthaltenen -- Zeichenkette wird im Kontext des Klassenladevorgangs deutlich.

sechstes Element des constant pools / Methodenname

Konstante vom Typ CONSTANT_Utf8. Sie leitet den in der Klasse Act spezifizierten Methodennamen doMathForever ein.

siebtes Element des constant pools / Zeichenkette Exceptions

Konstante vom Typ CONSTANT_Utf8, die den fixen String Exceptions einleitet.

achtes Element des constant pools / Zeichenkette LineNumberTable

Konstante vom Typ CONSTANT_Utf8, die den fixen String LineNumberTable einleitet.

neuntes Element des constant pools / Zeichenkette SourceFile

Konstante vom Typ CONSTANT_Utf8, die den fixen String SourceFile einleitet.

zehntes Element des constant pools / Zeichenkette LocalVariables

Konstante vom Typ CONSTANT_Utf8, die den fixen String LocalVariables einleitet.

elftes Element des constant pools / Zeichenkette Code

Konstante vom Typ CONSTANT_Utf8, die den fixen String Code einleitet.

zwölftes Element des constant pools / Zeichenkette java.lang.Object

Konstante vom Typ CONSTANT_Utf8, die den String java/lang/Object einleitet. Vom ersten Element des Konstantenpools referenziertes Element. Die in der Java-Hochsprache üblichen Punkte zur Trennung der Pakte, Subpakete und Klassennamen werden in der JVM konsequent (aus historischen Gründen) durch Querstriche ersetzt.

13. Element des constant pools / Zeichenkette Act

Konstante vom Typ CONSTANT_Utf8, die den String Act einleitet. Vom zweiten Element des Konstantenpools referenziertes Element. Name der Klasse.

14. Element des constant pools / Zeichenkette init

Konstante vom Typ CONSTANT_Utf8, die den String <init> einleitet. Methodenname der innerhalb der Superklasse Object. Der Rückgabewert dieser Methode ist im vierten Element des Konstantenpools abgelegt.

15. Element des constant pools / Zeichenkette snipet.java

Konstante vom Typ CONSTANT_Utf8, die den String snipet.java -- den Namen der Quellcodedatei in der sich die Definition der Klasse Act befindet -- einleitet.

16. Element des constant pools / Zeichenkette ()V

Konstante vom Typ CONSTANT_Utf8, die den String ()V einleitet. Methodendeskriptor, der weder Übergabeargumente noch Rückgabetyp besitzt.

Zugriffsflags

Zugriffsflags für die Klasse Act. Der konkrete Code ergibt sich aus der binären ODER-Verknüpfung verschiedener Zugriffsflaggen, die in untenstehender Tabelle wiedergegeben sind.

Flag
Wert
Beschreibung
ACC_PUBLIC
0x0001
public-Deklaration; Zugriffbar von allen anderen Klassen, auch außerhalb des eigenen Pakets
ACC_FINAL
0x0010
final-Deklaration; Verbot der Vererbung
ACC_SUPER
0x0020
Besondere Behandlung der Superklasseninstruktionen bei Aufruf über Opcode invokespecial
ACC_INTERFACE
0x0200
Struktur ist Schnittstelle, keine Klasse
ACC_ABSTRACT
0x0400
Abstrakte Struktur, von der keine Ausprägungen erzeugt werden können
Referenz auf this und super

Referenz in den Konstantenpool, auf die Klasse selbst (this) und die Superklasse (super).

interface_count und fields_count

interface_count: Anzahl der durch die Klasse implementierten Schnittstellen.
fields_count: Anzahl der Klassen- oder Instanzvariablen der Klasse.

methods_count

Anzahl der durch die Klasse implementierten Methoden.
Die Zahl ergibt sich aus der tatsächlich durch den Anwender definierten Anzahl und den impliziten, d.h. durch den Compiler hinzugefügten (z.B. Konstruktor), Methoden.

methods_info

Informationen über die in der Klasse Act definierten Methoden.
Die Zugriffsflaggen (0x00 09) weisen doMathForever() als static und public aus. (Die konkrete Wertebelegung kann untenstehender Aufstellung entnommen werden. Auch in diesem Falle wird der tatsächliche Wert durch Boole'sche ODER-Verknüpfung der Einzelwerte gebildet.)
Der Verweis (0x06) auf das sechste Element des Konstantenpools referenziert den Methodennamen im Klartext. Der zweite Verweis (0x10) auf den Konstantenpool kennzeichnet doMathForever() als parameterlose Methode ohne Rückgabetypen.

Flag
Wert
Beschreibung
ACC_PUBLIC
0x0001
public-Deklaration; Zugriffbar von allen anderen Klassen, auch außerhalb des eigenen Pakets
ACC_PRIVATE
0x0002
Als private deklariert, daher nur innerhalb der definierenden Klasse verwendbar
ACC_PROTECTED
0x0004
protected-Deklaration, Zugriff nur in Subklassen möglich
ACC_STATIC
0x0008
static-Deklaration, keine Auswirkungen auf Sichtbarkeit und Zugriffsrechte
ACC_FINAL
0x0010
final-Deklaration; nach initialer Zuweisung keine Wertänderung möglich
ACC_VOLATILE
0x0040
volatile-Deklaration; keine Berücksichtung in Optimierungsmaßnahmen
ACC_TRANSIENT
0x0080
transient-Deklaration; keine Berücksichtung durch Perisistenzmanager
Attribute der Methode doMathForever()

Die Methode doMathForever() verfügt nur über genau ein Attribut, daher ist der attribute count zu Beginn der Bytesequenz auf 0x00 01 gesetzt. Dieses eine Attribut wird durch Index 11 innerhalb des Konstantenpools referenziert. Dort ist die Zeichenkette Code lokalisiert. Dadurch wird angezeigt, daß die folgenden Bytes die Implementierung dieser Methode beinhalten.
Der abschließende vier-Byte Indikator enthält die Länge der Methodenimplementierung (im Beispiel: 0x30).

maxStack und maxLocals der Methode doMathForever()

maxStack: Maximalhöhe des Operandenstacks die während der Methodenausführung erreicht werden kann. (Im Beispiel: 2; die Opcode-Implementierung der beiden verwendeten arithmetischen Operationen benötigen niemals mehr als zwei Stackpositionen.)
maxLocals: Anzahl der lokalen Variablen. (die verwendete Variable i)

Bytecode und Ausnahmentabelle der Methode doMathForever()

Die code length legt die Anzahl der folgenden Bytecode-Instruktionen fest (im Beispiel: 12), darauf folgen die tatsächlichen Opcodes.

pc    instruction    mnemonic
0     03             iconst_0
1     3B             istore_0
2     840001         iinc 0 1
5     1A             iload_0
6     05             iconst_2
7     68             imul
8     3B             istore_0
9     A7FFF9         goto 2

Die Ausnahmentabelle (Exception Table) enthält die Anzahl der durch die Methode aufgefangenen Ausnahmeereignisse; im Beispiel: 0.

Eigenschaften des Code-Bereichs der Methode doMathForever()

In diesem Bereich werden zusätzliche Charakteristika des bereits definierten Codebereichs hinterlegt, z.B. Debugginginformation.
Im betrachteten Falle ist nur eine Eigenschaft angegeben (attribute_count = 0x01). Diese referenziert das achte Element des Konstantenpools -- die Zeichenkette LineNumberTable. Die beiden abschließenden Attribute bezeichnen die Länge dieser Tabelle (0x12) und die Anzahl der Einträge (0x4).
Die LineNumberTable des Beispiels:

line 4: i = 0;
line 5: while(true) {
line 6: i += 1;
line 7: i *= 2;
Zuordnung zwischen LineNumberTable und der Methode doMathForever()

Diese Datenstruktur stellt die Zuordnung zwischen den Quellcodezeilen und den resultierenden Opcodes her.

LineNumberTable[0]:  iconst_0    istore_0
LineNumberTable[1]:  iinc 0 1
LineNumberTable[2]:  iload_0     iconst_2    imul  istore_0
LineNumberTable[3]:  goto 2
Zugriffsflaggen und Indizes der Klasse Act()

Diese zweite methodenbezogene Struktur gibt Auskunft über den Konstruktor der Klasse Act.
Im ersten Doppelbyte sind die Zugriffsrechte spezifiziert; in diesem Falle sind keine gesonderten Festlegungen getroffen -- es handelt sich um eine einfache Methode.
Die Referenz in den Konstantenpool verweist auf die implementierende Methode (im Beispiel: Position 0x0E, dort findet sich die Methode <init>).
Durch die letzten beiden Bytes wird der Typ des Konstruktors referenziert, im betrachteten Beispiel die Position 0x10 im Konstantenpool, mithin ein parameterloser Konstruktor.

Attribute der Klasse Act()

Der Zähler (ersten beiden Bytes) zu beginn der Struktur zeigt an, daß nur ein Attribut der Klasse Act() folgt. Im Beispielfall handelt es sich dabei um das über den Index 11 (0x0B) angesprochene Element des Konstantenpools, die Zeichenkette Code.
Der abschließend angegebene Längenzähler fixiert die Anzahl der folgenden Bytes.

maxStack und maxLocals der Klasse Act()

Analog der Definition für Methoden, die maximale Höhe des Operandenstacks und die Anzahl der lokalen Variablen.

Bytecode und Ausnahmetabelle der Klasse Act()

Opcode-Implementierung des Konstruktors, sowie die Aufzählung der durch ihn potentiell ausgelösten Ausnahmeereignisse (im keine, daher Anzahl gleich Null).
Die Implementierung in Java-Bytecode:

pc    instruction    mnemonic
0     2A             aload_0
1     B70003         invokeonvirtual #3 <Method java.lang.Object <init> ()V>
4     B1             return
Eigenschaften des Code-Bereichs der Klasse Act()

Anzahl der Eigenschaften im ersten Doppelbyte (im Beispiel: 1). Die spezifische Eigenschaft wird durch Index acht im Konstantenpool (=LineNumberTable) näher definiert.
Diese Tabelle hat die Länge 0x06, mit einem einzigen Eintrag.

Zuordnung zwischen LineNumberTable und der Klasse Act()

Zuordnung der Quellcodezeilennummern zu den resultierenden Opcodes.

Allgemeine Eigenschaften

Am Ende einer Klassendatei kann eine beliebige Menge allgemeiner Attribute angeben werden.
für das Beispiel wurde durch den Compiler genau ein Attribut erzeugt, ein Verweis auf die Quelldatei (Konstantenpool-Index 0x09). Auf diese Information folgt ein Verweis auf den Namen der Quellcodedatei (Konstantenpool-Index 0x15).

Schlußbemerkungen:

Implementationsansätze für die Execution Engine der JVM

Graphik aus: Raner, M.: Blick unter die Motorehaube

Die interpretative Ausführung einer class-Datei mit der virtuellen Java-Maschine ist jedoch keineswegs zwingend, auch wenn sie das derzeit am häufigsten anzutreffende Vorgehen verkörpert. Bereits in der Standardedition des Java Development Toolkits von SUN wird seit Version 1.2 ein just in time compiler mitgeliefert, der transparent in die Ausführungsumgebung integriert ist. Er durchbricht die befehlweise interpretative Abarbeitung und greift den bei der Abarbeitung dynamisch durch den Interpretationsprozeß entstehenden plattformspezifischen Maschinencode ab und puffert ihn zwischen. Bei jeder erneuten Ausführung derselben Bytecodesequenz wird nun dieser bereits übersetzte Code ausgeführt. Dieses Vorgehen ist in den verbreiteten JVM-Implementierungen der Internet-Browser von Netscape und Microsoft verwirklicht. Ebenso bieten fast alle verfügbaren Java-Entwicklungsumgebungen diese Laufzeit-optimierende Komponente an. Der dadurch erzielte Geschwindigkeitsvorteil bewegt sich, je nach Struktur der Anwendung, zwischen zehn und 50 Prozent.
Den größten Gewschwindigkeitszuwachs verspricht man sich jedoch von der vollständigen Realisierung der virtuellen Maschine in Hardware; damit wird sie de facto zur realen Maschine. Hierzu liegen jedoch noch keine Ergebnisse vor, welche die der derzeitigen Implementierung auf handelsüblichen Prozessoren signifikant überträfen.
Eine andere Sichweise nutzt das Bytecodeformat welches eine Zwischenrepräsentation darstellt nicht zur Interpretation mit dem Ziele der direkten Ausführung, sondern als Eingabeformat eines weiteren Übersetzungsschrittes, der üblicherweise plattformabhängigen nativen Code erzeugt. Für C++ existieren bereits Umsetzungen, die Bytecode in übersetzungsfähigen C++-Quellcode transformieren. Eine Spielart hiervon bildet das diskutierte Werkzeug javap dessen Ausgabeformat, nach einigen Umformumgen, direkt als Eingabe weiterer Übersetzer akzeptiert wird.

Web-Referenzen 1: Weiterführende Links
Web-Referenzen 1: Weiterführende Links




3.2 Die Java API und weiterführende Themen

3.2.1 Ein-/Ausgabe -- Streams

Zur Erinnerung: bisher behandelte Möglichkeiten zur Ein- und Ausgabe:
Bisher wurden ausschließlich Bildschirm-Ausgaben, und diese ausschließlich mit der statischen Methode System.out.println, erzeugt werden. Der einzige uns bisher bekannte Mechanismus zur Eingabebehandlung war der der Kommandozeilenparameter in der main-Methode.

Wie aus C++ bekannt verfügt auch Java über die objektorientierte Kapselung der Ein- und Ausgabebehandlung in Form von Streams. Hierbei stehen beliebigste Ein- und Ausgabequellen über denselben programmiersprachlichen Mechanismus zur Verfügung, unabhängig davon wie das physische Gerät realisiert ist.

Zentrales Paket für die Ein-/Ausgabebehalung ist java.io. Es enthält neben den wichtigsten Klassen zur Implementierung des E/A-Verhaltens auch verschiedene Schnittstellen, sowie die möglichen Ausnahmeereignisse während der verschiedenen E/A-Operationen.
Gegenüber den aus C++ bekannten Datenströmen tritt bei Java hinzu, daß auch Informationen über Netze mit denselben Mechanismen übertragen werden.

In Java werden generelle zwei Streamtypen unterschieden: Character Streams und Byte Streams. Wie der Name schon andeutet, sind Byte Streams auf die Verarbeitung von beliebigen Byte-artigen Informationseinheiten -- und damit acht Bit große Einheiten -- beschränkt. Diese Mimik stellt insbesondere bei der Verarbeitung von Unicode-Zeichenketten eine große Einschränkung dar, da hierbei nicht gesamte Zeichen-Information (ein Symbol mißt 16 Bit) in einem Zugriff verarbeitet werden kann. Daher wurde mit dem JDK v1.1 zusätzlich das Konzept der Character Streams eingeführt, die generell 16 Bit lange Zeichen bereitstellen.

Als Byte Streams stehen zur Verfügung:
(Einrückungen kennzeichnen Subklassenbeziehungen, Kursivsetzungen abstrakte Klassen)

Als Character Streams stehen zur Verfügung:
(Einrückungen kennzeichnen Subklassenbeziehungen, Kursivsetzungen abstrakte Klassen)

Üblicherweise existieren die Ströme immer symmetrisch, d.h. in gleicher Weise sowohl für Ein- als auch Ausgabe. Dieses Prinzip wird nur für einzelne Klassen durchbrochen, die zwar Eingebeseitig existieren (beispielsweise Lesen mit Zeilennummer), für die jedoch kein expliziter Ausgabemechanismus benötigt wird.

Zusätzlich zu den nach Zugriffsarten klassifizierten Strömen existiert mit der Klasse RandomAccessFile eine Strom über den sowohl lesende als auch schreibende Zugriffe abgewickelt werden können.

Die abstrakten Basisklassen InputStream bzw. Reader und definieren ähnliche Lese- und Zugriffsmethoden, die in allen abgeleiteten Klassen auf den entsprechenden Stromtypen zur Verfügung stehen.
Die aktuell gewünschte Zugriffsart (nur-lesend, nur-schreibend oder beides) wird über einen Parameter des Konstrukturs gesteuert.
Wie durch den Klassennamen bereits angedeutet, existiert dieser Strom ausschließlich für Dateien; eine Netzwerkanwendung ist nicht möglich.

grundlegende Lesemethoden:

grundlegene Schreibmethoden:

Mit den Dateideskriptorenin, out und err stehen die aus C/C++ bekannten drei Standardstörme zur Verfügung.
Diese standardmäßig geöffneten Ströme stehen während der Ausführungzeit jeder Applikation zur Verfügung.

(1)import java.io.FileDescriptor;
(2)import java.io.FileWriter;
(3)import java.io.IOException;
(4)
(5)public class PrintLn {
(6)	public static void main(String[] args) {
(7)		FileWriter fw = null;
(8)		try {
(9)			fw = new FileWriter(FileDescriptor.out);
(10)			for (int i=0; i < args.length; i++)
(11)				fw.write (args[i]+" ");
(12)		} catch (IOException ioe) {
(13)			System.out.println("cannot open stdout!");
(14)		} finally {
(15)			try {
(16)				fw.close();
(17)			} catch (Exception e) {
(18)				//ignore it
(19)			} //catch
(20)		} //finally
(21)	} //main()
(22)} //class PrintLn

Beispiel 57: Ausgabe auf standard out   PrintLn.java

Das Programm öffnet erzeugt ein FileWriter-Objekt mit dem vorgegebenen Dateideskriptor out.
Anschließend werden die Kommandozeilenparameter (allesamt Typ String) mit write ausgegeben.
Alle Methoden der Klasse FileWriter können ein Ausnahmeereigniss vom Typ IOException erzeugen. Daher müssen Operationen auf dem erzeugten Ausgabestrom durch try-Blöcke abgesichert werden.

Wird als Ausgabekanal des FileWriter-Stroms auf eine pysikalische Datei ausgerichtet, so muß lediglich die Konstruktoranweisung zu fw = new FileWriter("myFile.asc") modifiziert werden.
Alle E/A-Klassen interpretieren die Pfadangabe relativ zum aktuellen Verzeichnis. Die Verzeichnisseparatoren variieren plattformabhängig. Verzeichnistrenner und aktueller Katlog können über die system properties ermittelt werden.

Streamerzeugung und mögliche Datenquellen aller (nicht als deprecated gekennzeichneten) Streamtypen:

Strom
Erzeugbar aus
BufferedInputStream
BufferedOutputStream
BufferedReader
BufferedWriter
ByteArrayInputStream
Byte Array
CharArrayReader
Character Array
DataInputStream
DataOutputStream
FileInputStream
File-Objekt
FileDescriptor (nur auf die Standardströme stdin, stdout, stderr andwendbar)
String, der einen gültigen Pfad enthält
FileOutputStream
File-Objekt,
FileDescriptor (nur auf die Standardströme stdin, stdout, stderr andwendbar)
String, der einen gültigen Pfad enthält
FileReader
File-Objekt,
FileDescriptor (nur auf die Standardströme stdin, stdout, stderr andwendbar)
String, der einen gültigen Pfad enthält
FileWriter
File-Objekt,
FileDescriptor (nur auf die Standardströme stdin, stdout, stderr andwendbar)
String, der einen gültigen Pfad enthält
FilterInputStream
FilterOutputStream
FilterReader
InputStreamReader
LineNumberReader
ObjectInputStream
ObjectOutputStream
OutputStreamWriter
PipedInputStream
PipedOutputStream
Der parameterlose Vorgabekonstruktor erzeugt einen unverbundenen Strom.
PipedOutputStream
PipedInputStream
Der parameterlose Vorgabekonstruktor erzeugt einen unverbundenen Strom.
PipedReader
PipedWriter
PrinterWriter
PrintStream
PushbackInputStream
PushbackReader
SequenceInputStream
InputStream
Oder Inhalt eines Objekts dessen Klasse die Enumeration-Schnittstelle implementiert.
StringReader

Der LineNumberReader liefert ein Beispiel eines Stroms, der auf Basis eines anderen Stroms definiert wird. Im Falle des folgenden Beispiels wird ein LineNumberReader ausgehend von einem bestehenden FileReader erzeugt.

Wie bereits im zweiten Kapitel angesprochen unterstützt Java den Unicode-Zeichensatz. Durch ihn wird die plattformübergreifende Darstellung verschiedenster Zeichensätze ermöglicht. Als Erweiterung des klassischen ISO 8859-Teil 1 Zeichensatzes benötigt er jedoch generell 16 Bit zur Darstellung eines Symbols. Zusätzlich ist zu einem Unicode codierten Datenstrom die Codierungsschema, identifiziert durch ein eindeutiges Kürzel, anzugeben um die korrekte Darstellung zu ermöglichen.
Hierzu erlauben die Stream-Klassen InputStreamReader und OutputStreamWriter die explizite Spezifikation der Eingabe- bzw. Ausgabe Encodierung.
Jede Java-Implementierung muß mindestens folgende Code-Formate unterstützen: US-ASCII, ISO-8859-1, UTF-16BE (16-Bit Unicode im big-endian Format), UTF-16LE (dergleichen als little-endian) und UTF-16 (allgemeines 16-Bit Unicodeformat, byte order mark am Beginn des Stroms definiert verwendetes Anordnungsschema).

(1)import java.io.FileDescriptor;
(2)import java.io.FileOutputStream;
(3)import java.io.FileReader;
(4)import java.io.IOException;
(5)import java.io.LineNumberReader;
(6)import java.io.OutputStreamWriter;
(7)
(8)public class UnicodeWriter {
(9)	public static void main(String[] args) {
(10)		LineNumberReader lnr = null;
(11)		OutputStreamWriter osw = null;
(12)
(13)		String encoding, text;
(14)
(15)		try {
(16)			System.out.print("specify encoding:");
(17)			lnr = new LineNumberReader(new FileReader(FileDescriptor.in));
(18)			encoding = lnr.readLine();
(19)
(20)			System.out.print("Specify text to encode:");
(21)			text = lnr.readLine();
(22)
(23)			System.out.print("encoded text:");
(24)
(25)			osw = new OutputStreamWriter((new FileOutputStream(FileDescriptor.out)), encoding);
(26)			osw.write(text);
(27)		} catch (IOException ioe) {
(28)			System.out.println("an IOException occurred\n"+ioe.getMessage() );
(29)		} finally {
(30)			try {
(31)				lnr.close();
(32)				osw.close();
(33)			} catch (Exception e) {
(34)				//ignore it
(35)			} //catch
(36)		} //finally
(37)	} //main()
(38)} //class UnicodeWriter

Beispiel 58: Schreiben in frei wählbarem Ausgabeencoding   UnicodeWriter.java

Das Programm ließt zunächst von der Standardeingabe die gewünschte Encodingdefinition und den auszugebenen Text als Zeichenkette.
Dann wird ein Ausgabestrom auf die Standardausgabe erzeugt, der das zuvor spezifizierte Encoding verwendet. Unterstützt die Java-Implementierung das angegebene Codierungsschema nicht, schlägt die Erzeugung des Ausgabestroms fehl, und es wird ein Ausnahmeereignis erzeugt.
Zum Abschluß wird der Text unter Anwendung des definierten Encodingschemas über den Ausgabestrom ausgegeben.

Beispielinteraktionen:

specify encoding:US-ASCII
Specify text to encode:abcäöüß
encoded text:abc????
specify encoding:UTF8
Specify text to encode:aä
encoded text:aÔÇ×
specify encoding:UTF-16LE
Specify text to encode:test
encoded text:t e s t
(1)import java.io.IOException;
(2)import java.io.FileDescriptor;
(3)import java.io.LineNumberReader;
(4)import java.io.FileReader;
(5)import java.io.FileWriter;
(6)
(7)
(8)public class Type {
(9)	public static void main(String[] args) {
(10)		FileReader fr = null;
(11)		FileWriter fw = null;
(12)		LineNumberReader lnr = null;
(13)		String line;
(14)
(15)		try {
(16)			fr = new FileReader(args[0]);
(17)			lnr = new LineNumberReader (fr);
(18)			fw = new FileWriter(FileDescriptor.out);
(19)
(20)			while ( (line = lnr.readLine()) != null) {
(21)				fw.write( lnr.getLineNumber()+": "+line+"\n" );
(22)			}//while
(23)		} catch (IOException ioe) {
(24)			System.out.println("an IOException occurred");
(25)			System.out.println( ioe.getMessage() );
(26)		} finally {
(27)			try {
(28)				fr.close();
(29)				fw.close();
(30)			} catch (IOException ioe) {
(31)				//ignore it
(32)			} //catch
(33)		} //finally
(34)	} //main()
(35)} //class Type

Beispiel 59: Zeilenweise Ausgabe einer Datei incl. Zeilennummern   Type.java



Serialisierung von Objekten
Durch den Stromtyp ObjectOuputStream können vollständige Objekte geschrieben, und durch ObjectInputStream wieder in den Speicher eingeladen werden.

(1)import java.io.FileInputStream;
(2)import java.io.FileOutputStream;
(3)import java.io.IOException;
(4)import java.io.ObjectInputStream;
(5)import java.io.ObjectOutputStream;
(6)import java.io.Serializable;
(7)import java.util.Calendar;
(8)import java.util.GregorianCalendar;
(9)
(10)public class SerializeData {
(11)	public static void main(String[] args)	{
(12)		//just for determining the current year
(13)		GregorianCalendar gregCal = new GregorianCalendar();
(14)
(15)		Person hans = new Person();
(16)		hans.name = new String("hans");
(17)		hans.yearOfBirth = 1950;
(18)		hans.age = gregCal.get(Calendar.YEAR) - hans.yearOfBirth;
(19)
(20)		System.out.println("object before serialization:\n" + hans.toString() );
(21)
(22)		ObjectOutputStream oos = null;
(23)
(24)		try {
(25)			oos = new ObjectOutputStream( new FileOutputStream("hans") );
(26)			oos.writeObject ( hans );
(27)		} catch (IOException ioe) {
(28)			System.out.println("an IOException occurred");
(29)			System.out.println( ioe.getMessage() );
(30)		} finally {
(31)			try {
(32)				oos.close();
(33)			} catch (IOException ioe) {
(34)				//ignore it
(35)			} //catch
(36)		} //finally
(37)
(38)		gregCal = null;
(39)		hans = null;
(40)
(41)		ObjectInputStream ois = null;
(42)		try {
(43)			Person anotherOne;
(44)			ois = new ObjectInputStream( new FileInputStream("hans") );
(45)			anotherOne = (Person) ois.readObject();
(46)
(47)			System.out.println( "object retrieved from file:\n"+ anotherOne.toString() );
(48)		} catch (IOException ioe) {
(49)			System.out.println("an IOException occurred while reading back object");
(50)		} catch (ClassNotFoundException cnfe) {
(51)			System.out.println("could not find class Person");
(52)		} finally {
(53)			try {
(54)				ois.close();
(55)			} catch (IOException ioe) {
(56)				//ignore it
(57)			} //catch
(58)		} //finally
(59)	} //main()
(60)} //class serializeData
(61)
(62)class Person implements Serializable {
(63)	public int yearOfBirth;
(64)	public String name;
(65)	transient int age;
(66)
(67)	public void finalize() {
(68)		System.out.println("object destroyed");
(69)	} //finalize()
(70)
(71)	public String toString() {
(72)		return("name="+name+"\n"+"yearOfBirth="+yearOfBirth+"\n"+"age="+age);
(73)	}//toString()
(74)} //class Person

Beispiel 60: Serialisieren und Laden eines Objekts   SerializeData.java

Bildschirmausgabe:

object before serialization:
name=hans
yearOfBirth=1950
age=50
object retrieved from file:
name=hans
yearOfBirth=1950
age=0

Das transiente Attributage wird nicht in die Datei übernommen. Die Inhalte aller anderen Attribute werden gesichert, und können rückgelesen werden.
Beim (Wieder-)Einlesen eines Objektes wird versucht dessen Klassendefinition aus der entsprechenden Klassendatei zu laden. Ist dies nicht möglich, so wird ein Ausnahmeereignis vom Typ ClassNotFoundException generiert.

Anmerkung: Der Java-Serialisierungsmechanismus verhindert die mehrfache Ablage desselben Objektes im Bytestrom.



Für die häufig umzusetzende Aufgabe der Eingabeformatprüfung, und anschließenden Klassifizierung in bestimmte Kategorien steht die Klasse StreamTokenizer zur Verfügung.

(1)import java.io.StreamTokenizer;
(2)import java.io.FileReader;
(3)import java.io.FileDescriptor;
(4)import java.io.IOException;
(5)
(6)public class TokenTest {
(7)	public static void main(String[] args) {
(8)		StreamTokenizer st = null;
(9)
(10)		int 	op1=0,
(11)				op2=0;
(12)		try {
(13)			st = new StreamTokenizer(new FileReader(FileDescriptor.in));
(14)
(15)			while(st.nextToken() == StreamTokenizer.TT_NUMBER)
(16)				op1 = (op1*10) +(int) st.nval;
(17)
(18)			while(st.nextToken() == StreamTokenizer.TT_NUMBER)
(19)				op2 = (op2*10) + (int) st.nval;
(20)
(21)			System.out.println(op1+"+"+op2+"="+(op1+op2));
(22)		} catch (IOException ioe) {
(23)			System.out.println("an IOException occurred\n"+ioe.getMessage() );
(24)		} //catch
(25)	} //main()
(26)} //class TokenTest

Beispiel 61: Einfache Addition zweier Zahlen unter Verwendung des StringTokenizers   TokenTest.java

Die beiden Operanden können durch ein beliebiges nicht numerisches Zeichen voneinander abgetrennt werden, abenso kann das Bereichnungsende erklärt werden.



Zum Zugriff auf (G-)ZIP komprimierte Dateien bieten die Klassen ZipInputStream und GZIPInputStream bzw. ZipOutputStream und GZIPOutputStream Lese- bzw. Schreibströme an.

back to top   3.2.2 Threads und Nebenläufigkeit

 

Java bietet mit den sog. Programmfäden engl. Threads die Möglichkeit an, paralle leichtgewichtige Prozesse direkt in der Hochsprache zu definieren und zu kontrollieren. Hierbei werden keine Anforderungen an eine spätere Unterstützung dieses Konzepts durch die tatsächliche physische Hardware gestellt; der gesamte Mechanismus ist rein Hochsprachen-basiert.
Als Bestandteil des automatisch importierten Paketes java.lang stehen Threads in jeder Applikation und jedem Applet ohne zusätzliche Aufwende zur Verfügung.

Weiterführende Informationen: Java Language Specification, chap. 17 und Java JVM Spezifikation, chap. 8.

Inhaltlich unterscheidet sich ein Thread nur in marginalen Modifikationen von einer gewöhlichen Klasse. Hauptunterschied ist die eigenständige aktive und nebenläufige Ausführung von Objekte einer solchen Klasse.

Voraussetzungen zur Erzeugung eines Threads:

Hinweis: Die Methode run() sollte nicht direkt aufgerufen werden! Bei der direkten Ausführung unterbliebe die notwendige Initialisierung; insbesondere wäre der dann „gewöhnliche“ Methodenaufruf nicht ansynchron, und das neue Objekt würde nicht nebenläufig ausgeführt.

(1)public class Threads1 {
(2)	public static void main(String[] args) {
(3)		HelloThread northGerman = new HelloThread( "Moin Moin" );
(4)		HelloThread southGerman = new HelloThread( "Gruess Gott" );
(5)
(6)		northGerman.start();
(7)		southGerman.start();
(8)	} //main()
(9)} //class Threads
(10)
(11)class HelloThread extends Thread {
(12)	protected String greetingText;
(13)
(14)	public HelloThread (String greetingText) {
(15)		this.greetingText = greetingText;
(16)	} //constructor
(17)
(18)	public void run() {
(19)		while (true) {
(20)			try {
(21)				Thread.sleep(500);
(22)			} catch (InterruptedException ie) {
(23)				System.out.println("an InterruptedException occurred\n"+ie.toString()+"\n"+ie.getMessage() );
(24)			} //catch
(25)			System.out.println( greetingText);
(26)		} //while
(27)	} //run()
(28)}//class HelloThread

Beispiel 62: Zwei einfache Threads   Threads1.java

Das Programm gibt die beiden Texte Moin Moin und Gruess Gott jeweils im Wechsel durch separate Threads aus.
Besonders fällt auf, daß die Applikation nach Abarbeiten der letzten Anweisung der main-Methode nicht terminiert.

Java-Programme die (noch) Vordergrund-Threads ausführen terminieren nicht am Ende der main-Methode, sondern führen weiterhin die noch laufenden Threads -- bis zu deren eigenständigem Terminieren oder Abbruch -- aus.
Die zweite Threadklasse bilden die Daemon-Threads. Verfügt ein laufendes Programm ausschließlich über solche Hintergrund-Threads terminiert es am Ende der main-Methode wie gewohnt.
Demnach läßt sich der bisher bekannte Programmtyp als nebenläufige Applikation mit dem Vordergrundthread main betrachten. Terminiert dieser Thread, so terminiert auch die gesamte Applikation.

Hinweis: Programme mit laufenden Vordergrundthreads lassen sich durch Beenden der virtuellen Maschine mittels System.exit(int) terminieren.

Das Beispiel enthält auch bereits zwei der möglichen Status innerhalb des Lebenszyklus eines Threads, nämlich erzeugt (aber noch nicht in Ausführung befindlich, nach Aufruf des Konstruktors) und in Ausführung nach dem Starten durch den Aufruf der Methode run().
Die möglichen Threadzustände können jedoch noch durch weitere Methoden beinflußt werden:

Hinweis:
Die beiden stop-Methoden, sowie die Methoden suspend und und resume sind seit der Version 1.3 als deprecated gekennzeichnet und sollten nicht mehr verwendet werden!
Der Grund liegt in der, mit der durch die API zugesicherten, sofortigen Unterbrechung. Dabei kann die Unterbrechung auch innerhalb eines kritischen Abschnittes erfolgen, wodurch es zu verschiedensten Inkonsistenzen und weiteren Folgeproblemen kommen kann.

Hinzu kommen von java.lang.Object ererbte Methoden:

Thread-Zustände

Die Graphik zeigt die verschiedenen möglichen Zustände eines Threads. Als deprecated gekennzeichnete Methoden sind grau unterlegt.

Das interne Scheduling aller laufenden Threads erfolgt prioritätsgesteuert. Die Prioritäten zugreifbarer (d.h. eigen erzeugter) Threads kann erfragt und gezielt verändert werden.
Die Threadpriorität wird als ganzzahliger Wert angegeben. Plattformspezifisch kann dieser Wert variieren; jedoch kann die konkrete Unter- und Obergrenze über die Konstanten MIN_PRIORITY bzw. MAX_PRIORITY zur Laufzeit erfragt werden. Ohne manuellen Eingriff werden neue Threads mit der durch NORM_PRIORITY definierten Priorität erzeugt.

Methoden zur Beeinflussung der Thread-Priorisierung:

Aus Gründen der vereinfachten Verwaltung können Threads in Gruppen (API-Klasse ThreadGroup) zusammengefaßt und damit gebündelt beeinflußt werden.
Operationen zur Zustandsänderung die auf einzelnen Threads wirken, können auch auf Thread-Gruppen angewendet werden.
Die Verwaltung von solchen Thread-Bündeln ist in der API-Klasse ThreadGroup zusammengefaßt; die Gruppenzugehörigkeit eines spezifischen Threads kann durch getThreadGroup() ermittelt werden.

Daemon-Threads:
wie bereits angerissen gibt es neben den „normalen“ Anwenderthreads auch die Klasse der Daemon-Threads.
Ihr Hauptunterscheidungsmerkmal zu den Anwenderthreads ist das Charakteristikum, daß die JVM auch terminiert, wenn sich Threads dieser Kategorie in Ausführung befinden. Dies prädestiniert sie für Hintergrundaufgaben wie Verwaltungs- oder Überwachungstätigkeiten.
Der Typ eines Threads kann vor dessen Ausführungsbeginn durch den start()-Aufruf mittels der Methode setDaemon(boolean) festgelegt, und zur Laufzeit mit isDaemon() jederzeit während der Ausführung erfragt, werden.

(1)public class IsPrime {
(2)	public static void main(String[] args) {
(3)		PrimitivePrimeTest ppt;
(4)		StatusInformation myInfo = new StatusInformation();
(5)		myInfo.setDaemon(true); //declare as daemon thread
(6)		myInfo.setPriority( (Thread.NORM_PRIORITY + 2) <= Thread.MAX_PRIORITY ? Thread.NORM_PRIORITY + 2 : Thread.MAX_PRIORITY );
(7)		myInfo.start();
(8)
(9)		ThreadGroup workerThreads	= new ThreadGroup("worker threads");
(10)
(11)		for (int i = Integer.parseInt(args[0]); i <= Integer.parseInt(args[1]); i++) {
(12)			ppt = new PrimitivePrimeTest(workerThreads, i, "calculating " +i );
(13)			ppt.start();
(14)		} //for
(15)
(16)	} //main
(17)} //class IsPrime2
(18)
(19)class PrimitivePrimeTest extends Thread {
(20)	protected int numberToTest;
(21)	boolean earlyExit=false;
(22)
(23)	public PrimitivePrimeTest(ThreadGroup tg, int numberToTest, String threadName) {
(24)		super (tg, threadName); //call to super's class constructor
(25)		this.numberToTest = numberToTest;
(26)	} //constructor
(27)
(28)	public void run() {
(29)		if (numberToTest == 1)
(30)			earlyExit = true;
(31)		for (int i=2; i < ((int) Math.sqrt(numberToTest))+1; i++) {
(32)			try {
(33)				Thread.sleep(1000);
(34)			} catch (InterruptedException ie) {
(35)				//ignore it
(36)			} //catch
(37)			if (numberToTest % i == 0) {
(38)				earlyExit=true;
(39)				break;
(40)			} //endif
(41)		} //for
(42)		if  (earlyExit != true)
(43)			System.out.println(numberToTest+" is prime");
(44)	} //run()
(45)} //class PrimitivePrimeTest
(46)
(47)class StatusInformation extends Thread {
(48)	public void run() {
(49)		while (true) {
(50)			System.out.println("no of currently running threads: "+Thread.activeCount() );
(51)			try {
(52)				Thread.sleep(500);
(53)			} catch (InterruptedException ie) {
(54)				//ignore it
(55)			} //catch
(56)		} //while
(57)	} //run()
(58)} //class StatusInformation

Beispiel 63: Ein einfacher Primzahlenprüfer   IsPrime.java

Beispielablauf:

$java IsPrime 1 25
no of currently running threads: 2
2 is prime
3 is prime
no of currently running threads: 24
no of currently running threads: 24
5 is prime
7 is prime
no of currently running threads: 11
no of currently running threads: 11
11 is prime
13 is prime
no of currently running threads: 6
no of currently running threads: 6
17 is prime
19 is prime
23 is prime
no of currently running threads: 3
no of currently running threads: 3

Das Programm prüft in jeweils einem eigenen Thread, ob die Ganzzahlen im Intervall zwischen den gegebenen Grenzen prim sind. Entdeckte Primzahlen werden mit einer entsprechenden Meldung ausgegeben.
Um die unterschiedliche Ausführungsdauer der jeweiligen Threads hervorzuheben führt jeder Programmfaden nur eine Berechnung pro Sekunde aus.
Die Berechnungsthreads sind alle in einer eigenen Threadgruppe (workerThreads) zusammengefaßt. Jeder Thread innerhalb dieser Gruppe wird durch die Zeichenkette calculating gefolgt von der zu prüfenden Zahl benannt.
Zusätzlich gibt ein Daemon-Thread alle halbe Sekunde die Anzahl der (noch) aktiven Threads aus. Dieser Thread wird automatisch durch die JVM nach dem Abarbeiten des letzten aktiven Anwenderthreads terminiert. Die Priorität des Daemon-Threads ist um zwei gegenüber dem Vorgabewert erhöhrt, sofern dadurch nicht die maximal erlaubte Priorisierung überschritten wird.



Synchronisation von Methodenzugriffen:

(1)import java.io.DataOutputStream;
(2)import java.io.FileOutputStream;
(3)import java.io.FileInputStream;
(4)import java.io.DataInputStream;
(5)import java.io.IOException;
(6)import java.io.FileNotFoundException;
(7)
(8)public class Threads2 {
(9)	public static void main(String[] args) {
(10)		for (int threadCount=0; threadCount < Integer.parseInt(args[0]); threadCount++)
(11)			(new IncrementCounter()).start();
(12)	} //main()
(13)} //class Threads2
(14)
(15)class IncrementCounter extends Thread {
(16)	public void run() {
(17)		int counterValue;
(18)		DataInputStream dis = null;
(19)		DataOutputStream dos = null;
(20)
(21)		try {
(22)			for (int i=0; i<10; i++) {
(23)				dis = new DataInputStream( new FileInputStream( "counter" ) );
(24)				counterValue = dis.readInt();
(25)				dis.close();
(26)
(27)				System.out.println(counterValue+" read by thread "+ (Thread.currentThread()).getName() );
(28)
(29)				dos = new DataOutputStream( new FileOutputStream( "counter", false ) );
(30)				dos.writeInt( ++counterValue );
(31)				dos.close();
(32)			} //for
(33)		} catch (FileNotFoundException fnfe) {
(34)			System.out.println("file counter could not be opened!\n"+fnfe.toString()+"\n"+fnfe.getMessage() );
(35)			System.exit(1);
(36)		} catch (IOException ioe) {
(37)			System.out.println("an IOException occurred in thread "+(Thread.currentThread()).getName()+"!\n"+ioe.toString()+"\n"+ioe.getMessage() );
(38)			System.exit(1);
(39)		} //catch
(40)	} //run()
(41)} //class IncrementCounter	

Beispiel 64: Gemeinsames Inkrementieren eines Zählers in einer Datei   Threads2.java

Hilfsprogramme: manuelles Rücksetzen des Zählerstandes auf 0, manuelles Auslesen des Zählerstandes und Ausgabe auf Standardausgabe

Das Programm liest den Stand einer ganzzahligen Variable aus einer Datei aus, erhöht um Eins und schreibt ihn zurück in dieselbe Datei, unter Verlust des alten Standes (Überschreiben). Die beschriebene Vorgangsfolge wird im Multithreading-Betrieb durch mehrere Ausführungsfäden nebenläufig durchgeführt.
Beim Auffangen eines Ausnahmeereignisses wird die virtuelle Maschine terminiert, da das einzelne Terminieren nur eines Threads das Ergebnis verfälschen würde.
Tritt zwischen dem Auslesevorgang aus der Datei, und dem Rückschreiben des modifizierten Wertes ein Threadwechsel ein, so kommt es zum Phänomen des lost updates.

mögliche Bildschirmausgaben:
Nach dem Einlesen der Zahl 5 durch Thread-33 tritt, vor ihrem inkrementierten Rückschreiben, der Threadwechsel ein, wodurch der nachfolgend ausgeführte Thread-34 dieselbe Zahl nochmals ausliest.

$java Threads2
...
3 read by thread Thread-28
4 read by thread Thread-30
5 read by thread Thread-33
5 read by thread Thread-34
6 read by thread Thread-42
7 read by thread Thread-45
...

Sicherlich ein Extremum dieses Verhaltens stellt das nachfolgende Beispiel dar:

mögliche Bildschirmausgaben:
Threadwechsel tritt jeweils direkt nach dem Einlesen auf. Als Konsequenz wird derselbe Zahlenwert mehrfach durch verschiedene Threads gelesen.

$java threads2
...
10 read by thread Thread-35
10 read by thread Thread-10
10 read by thread Thread-36
10 read by thread Thread-37
10 read by thread Thread-14
10 read by thread Thread-38
10 read by thread Thread-12
10 read by thread Thread-39
10 read by thread Thread-16
10 read by thread Thread-41
10 read by thread Thread-17
10 read by thread Thread-19
10 read by thread Thread-42
10 read by thread Thread-18
10 read by thread Thread-40
10 read by thread Thread-0
10 read by thread Thread-11
...

Um Daten-Inkonsistenzen zur Laufzeit zu verhindern, wie sie potentiell entstehen könnten, wenn zwei Programmfäden gleichzeitig dieselbe Methode ausführen oder zeitlich verschränkt dieselbe kritische Ressource benutzen, ist mit dem (aus Kapitel zwei bekannten) Schlüsselwort synchronized ein Hochsprachenmechanismus gegeben um mehrere Aufrufer gezielt zu serialisieren.
Technisch handelt es sich dabei um das aus den Betriebsystemen bekannte Konzept der Monitore (siehe C. A. R. Hoare: Monitors: An Operating System Structuring Concept) zur Synchronisation des Zugriffs auf kritische Abschnitte.

Jedoch besteht bei dieser Vorgehensweise die Gefahr eines Deadlocks durch wechselseitiges Blockieren!

Durch synchronisierte Blöcke können nicht nur einzelne Methoden, sondern Bündel von Zugriffen gemeinsam geschützt werden.
Hierzu wird dem Schlüsselwort synchronized ein auswertbarer Ausdruck nachgestellt, der die Resource bezüglich der zu synchronisieren ist bezeichnet.
Im Falle des betrachteten Beispiels ist daher hinsichtlich der gemeinsam beanspruchten (und daher kritischen) Ressource der Zählerstandsdatei zu synchronisieren.

(1)import java.io.DataInputStream;
(2)import java.io.DataOutputStream;
(3)import java.io.File;
(4)import java.io.FileInputStream;
(5)import java.io.FileNotFoundException;
(6)import java.io.FileOutputStream;
(7)import java.io.IOException;
(8)
(9)public class Threads21 {
(10)	public static synchronized void main(String[] args) {
(11)		File counterFile = new File ("counter");
(12)
(13)		for (int threadCount=0; threadCount < Integer.parseInt(args[0]); threadCount++)
(14)			(new IncrementCounter(counterFile)).start();
(15)	} //main()
(16)} //class Threads21
(17)
(18)class IncrementCounter extends Thread {
(19)	File	counterFile;
(20)	public IncrementCounter(File rwFile) {
(21)		counterFile = rwFile;
(22)	} //constructor
(23)
(24)	public void run() {
(25)		int counterValue;
(26)		DataInputStream dis = null;
(27)		DataOutputStream dos = null;
(28)
(29)		try {
(30)			for (int i=0; i<10; i++) {
(31)				synchronized (counterFile) {
(32)					dis = new DataInputStream( new FileInputStream( counterFile ));
(33)					counterValue = dis.readInt();
(34)					dis.close();
(35)
(36)					System.out.println(counterValue+" read by thread "+ (Thread.currentThread()).getName() );
(37)
(38)					dos = new DataOutputStream( new FileOutputStream( counterFile ));
(39)					dos.writeInt( ++counterValue );
(40)					dos.close();
(41)				}//synchronized
(42)			} //for
(43)		} catch (FileNotFoundException fnfe) {
(44)			System.out.println("file counter could not be opened!\n"+fnfe.toString()+"\n"+fnfe.getMessage() );
(45)			System.exit(1);
(46)		} catch (IOException ioe) {
(47)			System.out.println("an IOException occurred in thread "+(Thread.currentThread()).getName()+"!\n"+ioe.toString()+"\n"+ioe.getMessage() );
(48)			System.exit(1);
(49)		} //catch
(50)	} //run()
(51)} //class IncrementCounter	

Beispiel 65: Lost-Update-freie Variante des vorhergehenden Beispiels   Threads21.java

Auffallendstes Kennzeichen der Synchronisation mittels Schlüsselwort synchronized ist es, daß gemeinsam genutzte Objekte existieren müssen, um die alle Threads konkurrieren.
Auf den Einsatzfall einer eher losen Kopplung der einzelnen Ausführungseinheiten sind die Methoden wait und notify ausgelegt. Der wohl bekannteste Vertreter diese Problemklasse ist das klassische Erzeuger-Verbracher-Schema, in dem zwei grundlegend verschiedene Rollen, die des Erzeugers und die des zeitlich nachgelagerten -- und daher zu synchronisierenden -- Verbrauchers unterschieden werden.
Für diese Problemklasse bietet Java das Schlüsselwort wait an. Es erlaubt das Abwarten eines Ereignisses, welches durch notify angezeigt wird.

Das Standardschema zur Benutzung lautet:

synchronized doWhenCondition() {
   while (!condition)
      wait();
   ...Bedingung true...
} //synchronized

Als Randbedingung gilt die zwingende Einbettung in eine als synchronized deklarierte Methode, um die Modifikation der While-Bediungung durch nebenläufig ausgeführte Threads zu verhindern.
Der wait-Aufruf suspendiert die Ausführug des Prozesses, und gibt gleichzeitig (atomar) seine gesetzten Sperren frei.

Das Benutzungsschema für notify lautet:

synchronized changeCondition() {
   ...Änderung von Werten, die in der Bedingung auftreten...
   notify();
} //synchronized

Die wait-Methode steht in verschiedenen Ausgestaltungen zur Verfügung:
wait() wartet bis zur Wiedererweckung durch notify; gleiche Wirkung wie wait(0).
wait(long) wartet bis zum Ablauf des durch den Übergabeparameter fixierten Zeitraumes in Millisekunden auf den Aufruf von notify().
wait(long, int) wartet bis zum Ablauf des durch die Übergabeparameter spezifizierten Zeitraumes -- der long-Wert wird als Millisekunden interpretiert, zu dem die als int-Wert gegebenen Nannosekunden addiert werden -- auf den Aufruf von notify().

Zur Wiedererweckung wartender Threads kann die parameterlose Methode notify() benutzt werden; sie erweckt maximal einen wartenden Thread.
Den Wiederanlauf beliebig vieler wartender Threads ermöglicht notifyAll().

Anmerkungen:



Ermitteln von Threadinformation:
Die Anzahl der laufenden aktiven Threads der aktuellen Gruppe kann durch activeCount() ermittelt werden.
Informationen über jeden Thread der virtuellen Maschine liefern die Methoden getName() und setName(String). Diese beiden Methoden erlauben die Abfrage, bzw. Definition eines Namens für jeden Thread. Bei der Erzeugung eines Threads wird automatisch ein Name durch die virtuelle Maschine vergeben. (SUNs JVM setzt hier die Zeichenkette Thread-i, wobei i die laufende Nummer des Threads ist.)
enumerate(Thread[]) befüllt den als Parameter übergebenen Array mit Verweisen auf die derzeit aktiven Thread Objekte.
Der aktuelle Thread kann durch die statische Methode currentThread() ermittelt werden.



Außer durch Spezialisierung der Klasse Thread kann durch Impelementierung des Interfaces Runnable Nebenläufigkeit erzeugt werden.
Die Schnittstelle definiert ausschließlich die Operation run. Auch die Umsetzung der Klasse Thread nutzt in den vordefinerten Konstruktoren dieses Interface. Durch den Methodenaufruf start() wird im zugehörigen nebenläufigen Objekt per O.run() (O ist hierbei vom Typ Runnable) die Ausführung begonnen.
Hinweis: Dieser Anwendungsfall ist insbesondere für Applets von Bedeutung, da hier keine Spezialisierung von Thread erfolgen kann, da zwingend von Klasse Applet abgeleitet werden muß.
(Beispiel dazu)



Mehr zu Java Threads erfahren ... (eigene Vorlesung zum Thema)

back to top   3.2.3 Applets

 

Applets sind Java-Programme die innerhalb einer Browserumgebung ausgeführt werden. Daher gelten für sie einige besondere Charakteristika, die sie von den bisher betrachteten Java-Applikationen unterscheiden:

Hinweis: Zu Testzwecken kann ein Applet auch durch einen Applet-Viewer, wie der u.a. in SUNs JDK enthalten ist, ausgeführt werden. Hierfür wird neben dem Applet selbst eine minimale HTML-Seite benötigt.

Die Klassenhierarchie von Applet und ihre Klassen:

Klassenhierarchie der oberhalb der Klasse Applet

Lebenszyklus eines Applets -- die zentralen Methoden der Klasse Applet:

Zustandsübergänge im Lebenszyklus eines Java-Applets

Das Zustandsübergangsdiagramm zeigt die verschiedenen Status im Leben eines Applets.
Die Übergänge sind mit den auslösenden Ereignissen beschriftet. Ist kein Ereignis angetragen, so erfolgt der Übergang automatisch ohne weitere Bedingungen. In Klammern ist die jeweils ereignisauslösende Umgebung (AV für Appletviewer und B für Web-Browser) angegeben. Im Allgemeinen verhalten sich die beiden großen Browser gleich. Weicht das Verhalten eines Browers ab, so ist dies explizit angegeben (IE kennzeichnet hierbei den MS Internet Explorer).

Weitere wichtige Methoden, die von den Superklassen von Applet geerbt werden sind:

Der Aufruf der Methode repaint() erzeugt einen Aufruf an die update-Methode der Komponente.
repaint() stellt die asynchrone Variante des direkten Aufrufes von paint mit dem aktuellen Graphics-Objekt (paint( getGraphics() )) dar. Die Methode garantiert das Neuzeichnen nicht, im Falle hoher Auslastung der virtuellen Maschine kann es auch unterbleiben.

Die benötigten (X)HTML-Strukturen:
Leider existieren derzeit verschiedene Varianten ein Java-Applet in eine Hypertextseite einzubinden.
Zunächst die „klassische“ Applet-Referenz: <applet code="className" width="Breite in Pixel" height="Höhe in Pixel">
Zusätzlich können (gemäß HTML v4.01 Standard) noch weitere Attribute (Namen-Wert-Paare wie "code" oder "width") definiert werden:
codebase -- ermöglicht die relative Interpretation von Pfadnamen ausgehend vom in codebase definierten Katalog
archive -- ZIP komprimiertes Archiv das Applet gesucht wird.
alt -- zusätzlicher erläuternder Text.
name -- eindeutige Benennung des Applets innerhalb der beherbergenden Seite.
align -- horizontale Ausrichtung auf der Seite.
hspace -- horizontaler Abstand des Applets zur Umgebung.
vspace -- vertikaler Abstand zur Umgebung.

Ferner existieren noch einige proprietäre Attribute, die jedoch nicht durch alle Browser unterstützt werden.

Parameterübergabe an Applets:
erfolgt durch Name-Wert-Paare der Form <param name="..." value="...">, die in den Grenzen der Applet-Tags eingeschlossen sind.

<applet code="HelloWorldApplet.class" width="100" height="100">
<param name="color" value="green">
</applet>

Mit XHTML v1.0, dem Nachfolger des HTML v4.01 Standards, wurde die Applet-Syntax für veraltet (deprecated) erklärt, und nunmer ausschließlich die Object-Variante zugelassen. Über die Änderung der Tag-Namen hinausgehend dürfte die Plazierung der Referenz auf die Java-Klassendatei, und die explizite Typangabe, als Parameter die augenfälligste Änderung sein.
Diese Syntaxvariante wird jedoch, obgleich Standard, noch nicht von allen derzeit gängigen Browservarianten unterstützt.

<object>
<param name="type" value="application/x-java-applet">
<param name="JAVA_CODE" value="HelloWorldApplet.class">

Hinweis: In älteren Referenzen findet sich teilweise noch die ursprüngliche HotJava-Syntax, welche ein app-Tag definiert. Ferner enthält dieses die Referenz auf die Klasse als Attribut, jedoch ohne die Dateiextension .class explizit anzuführen.

Das Hello World-Applet:
Das absolute Minimalapplet, es schreibt lediglich den bekannten Schriftzug, kann folgendermaßen umgesetzt werden:

(1)import java.applet.Applet;
(2)import java.awt.Graphics;
(3)
(4)public class HelloWorldApplet extends Applet {
(5)
(6)	public void paint(Graphics g) {
(7)		g.drawString( 	getParameter("displayText"),
(8)							Integer.parseInt(getParameter("xPos")),
(9)							Integer.parseInt(getParameter("yPos")) );
(10)	} //paint()
(11)	public String getAppletInfo() {
(12)		return( "written by Mario Jeckle\nversion: 1.1\nCopyright (c) by Mario Jeckle");
(13)	} //getAppletInfo()
(14)	public String[][] getParameterInfo() {
(15)		String appInfo[][] = {
(16)	 		{"displayText", "anyString", "string to display"},
(17)	 		{"xPos", "numeric value" , "horizontal position"},
(18)	 		{"yPos", "numeric value", "vertical position"}
(19) 		};
(20)		return appInfo;
(21)	} //getParameterInfo()
(22)} //end class HelloWorldApplet

Beispiel 66: Hello World als Java-Applet   HelloWorldApplet.java

Applet ausführen

Interaktion mit der Ausführungsumgebung
Durch verschiedene vordefinierte Methoden und Schnittstellen kann jedes Applet mit seiner Ausführungsumgebung, dem Browser oder Applet Viewer, in Interaktion treten. Hierdurch können von Seiten des Applets auch Dienste wie Caching, Anzeigen einer Nachricht, Abspielen von Audiodateien der Ausführungsumgebung benutzt werden.
Einfachster Fall ist die Methode getAppletInfo. Sie liefert eine Zeichenkette zurück, welche üblicherweise Informationen über den Autor, die Version und die Rechte am Applet beinhaltet.
Eine ähnliche Funktion erfüllt die Methode getParameterInfo(). Sie liefert einen Array von dreielementigen String-Arrays zurück, welche die erlaubten Parameter näher beschreiben.
Mittels getParameter(String) können die Werte jedes einzelnen Parameters ausgelesen werden.

(1)import java.applet.Applet;
(2)import java.awt.Graphics;
(3)
(4)public class HelloWorldApplet extends Applet {
(5)
(6)	public void paint(Graphics g) {
(7)		g.drawString( 	getParameter("displayText"),
(8)							Integer.parseInt(getParameter("xPos")),
(9)							Integer.parseInt(getParameter("yPos")) );
(10)	} //paint()
(11)	public String getAppletInfo() {
(12)		return( "written by Mario Jeckle\nversion: 1.1\nCopyright (c) by Mario Jeckle");
(13)	} //getAppletInfo()
(14)	public String[][] getParameterInfo() {
(15)		String appInfo[][] = {
(16)	 		{"displayText", "anyString", "string to display"},
(17)	 		{"xPos", "numeric value" , "horizontal position"},
(18)	 		{"yPos", "numeric value", "vertical position"}
(19) 		};
(20)		return appInfo;
(21)	} //getParameterInfo()
(22)} //end class HelloWorldApplet

Beispiel 67: Parametrisierung des bekannten HelloWorld-Applets   HelloWorldApplet.java

Applet ausführen

Die Erweiterung des bekannten Beispielapplets zeigt den Einsatz der getAppletInfo-Methode zur Rückgabe Applet-spezifischer Inhalte.
Zusätzlich wird der anzuzeigende Text und seine Position (=Parameter der Methode drawString) über Parameter gesteuert, die durch die Methode getParameterInfo in ihrer Funktion näher beschrieben sind.
Die Parameter werden aus der (X)HTML-Quelle durch die Methode getParameter ausgelesen.

Die Schnittstelle AppletContext erlaubt die aktive Beeinflussung der Ausführungsumgebung des Applets. Die Methode getAppletContext liefert die aktuelle Context-Ausprägung zurück.
Jedes AppletContext-Objekt stellt folgende Methoden zur Verfügung:

(1)import java.applet.Applet;
(2)import java.awt.Graphics;
(3)import java.net.URL;
(4)import java.net.MalformedURLException;
(5)
(6)public class HWAURLForward extends Applet {
(7)	public void paint(Graphics g) {
(8)
(9)		g.drawString( 	getParameter("displayText"),
(10)							Integer.parseInt(getParameter("xPos")),
(11)							Integer.parseInt(getParameter("yPos")) );
(12)		try {
(13)			Thread.sleep(5000); //five seconds delay
(14)		} catch (InterruptedException iee) {
(15)			System.out.println("an InterrupedException occurred");
(16)		} //catch
(17)
(18)		try {
(19)			(this.getAppletContext()).showDocument( new URL(getParameter("URL")) );
(20)		} catch (MalformedURLException murle) {
(21)			System.out.println("illegal URL");
(22)		} //catch
(23)	} //paint()
(24)	public String getAppletInfo() {
(25)		return( "written by Mario Jeckle\nversion: 1.1\nCopyright (c) by Mario Jeckle");
(26)	} //getAppletInfo()
(27)	public String[][] getParameterInfo() {
(28)		String appInfo[][] = {
(29)	 		{"displayText", "anyString", "string to display"},
(30)	 		{"xPos", "numeric value" , "horizontal position"},
(31)	 		{"yPos", "numeric value", "vertical position"},
(32)	 		{"URL", "any valid URL according to IETF's RFC 1738", "URL to jump to"}
(33) 		};
(34)
(35)		return appInfo;
(36)	} //getParameterInfo()
(37)} //class HWAURLForward 

Beispiel 68: Hello World Applet mit URL-Forward nach fünf Sekunden   HWAURLForward.java

Das Beispiel erweitert das parametrisierte HelloWorld-Applet um eine URL-Weiterleitung nach fünf Sekunden. Die notwendige Zieladresse wird aus dem Parameter URL geladen.
zugehörige HTML-Seite



Sicherheitskonzept von Applets:
Nachfolgende Tabelle faßt die Rechte und Beschränkungen von Applikationen und Applets, sowohl für die Ausführungsumgebung Web-Browser als auch im Appletviewer, zusammen.

Aktion
Browser
Applet Viewer
Java Applikation, ohne weitere Einschränkungen
Lesen lokaler Dateien
nicht unterstützt
unterstützt
unterstützt
Schreiben lokaler Dateien
nicht unterstützt
unterstützt
unterstützt
Zugriff auf Dateinformation
nicht unterstützt
unterstützt
unterstützt
Löschen von Dateien
nicht unterstützt
nicht unterstützt
unterstützt
Ausführen anderer Programme
nicht unterstützt
unterstützt
unterstützt
Auslesen der Property user.name
nicht unterstützt
unterstützt
unterstützt
Zugriff auf Netzwerkport des Herkunfts-Servers des Applets
unterstützt
unterstützt
unterstützt
Zugriff auf Netzwerkport eines beliebigen Servers
nicht unterstützt
unterstützt
unterstützt
Dynamisches Laden von Java-Bibliotheken
nicht unterstützt
unterstützt
unterstützt
Beenden der virtuellen Maschine mit exit
nicht unterstützt
unterstützt
unterstützt
Erzeugen von Fenstern
unterstützt
mit Warnhinweis
unterstützt
unterstützt
Ausführen nativen-Codes
nicht unterstützt
nicht unterstützt
unterstützt

Das Applet openProperties zeigt die Inhalte der abfragbaren Systemeigenschaften an.

CannotDisplayApplet

Beim Versuch innerhalb eines Applets eine aus Sicherheitsgründen nicht zugleassene Funktionalität auszuführen wird ein Ausnahmeereignis-Objekt der Klasse AccessControlException erzeugt.

Hinweis: Die dargestellten Einschränkungen stellen für manche Anwendungsfälle zu deutliche Restriktionen dar. Daher existiert mit den sog. signed Applets eine Möglichkeit einzelne Einschränkungen für bestimmte Applets gezielt zu öffnen.

Drei Komponenten wirken bei der Realisierung des Java-Sicherheitsmodells zusammen:

  1. Der binäre Bytecode wird nicht direkt durch die physische Hardware ausgeführt, sondern durch die virtuelle Maschine interpretiert.
  2. Der Security Manager überwacht alle sicherheitsrelevanten Operationen.
  3. Applets mit weitergehenden Berechtigungen müssen sich explizit identifzieren.


Abspielen von Audiodateien:
War diese Funktionalität bis zum Erscheinen der Java 2 Plattform ausschließlich Applets vorbehalten steht sie nun auch in Java-Applikationen zur Verfügung.
Generelles Vorgehen:
Laden einer Audiodatei mittels der Methodenfamilie getAudioClip über eine gültige URL.
Im Anschluß kann das rückgegebene Objekt, welches die AudioClip-Schnittstelle implementiert abgespielt werden. Java ab Version 2 unterstützt MIDI-Dateien sowie Audiodateien der Formate RMF, WAVE, AIFF und AU.
Zur Soundausgabe in Applikationen wurde die Klassenmethode newAudioClip(URL) eingeführt, die eine Audiowiedergabe auch ohne Existenz eines Applet-Objekts ermöglicht.
Üblicherweise werden im Zusammenhang mit der Audiowiedergabe Threads verwendet um die Sounds mit mit anderen Ereignissen zu synchronisieren.



Anzeigen von Graphiken:

Bilder in den Formaten GIF, PNG oder JPEG können durch die Methoden getImage geladen werden.
Die Anzeige erfolgt durch drawImage(...).
Das GIF98a-Animationsformat wird ebenfalls unterstützt.

Anmerkung: Alternativ kann auch signaturgleiche Methode getImage der Klasse java.awt.Toolkit benutzt werden.

(1)import java.applet.Applet;
(2)import java.awt.Graphics;
(3)import java.awt.GridBagLayout;
(4)import java.awt.Button;
(5)import java.awt.TextField;
(6)import java.awt.Image;
(7)import java.awt.image.ImageObserver;
(8)import java.awt.event.ActionEvent;
(9)import java.awt.event.ActionListener;
(10)
(11)public class GfxViewer extends Applet implements ActionListener {
(12)	protected TextField graphicsName;
(13)	Image imgToDisplay = null;
(14)
(15)	public void init() {
(16)		graphicsName = new TextField ("enter URL of image to view", 50);
(17)		Button loadBtn = new Button ("view");
(18)		GridBagLayout gbl = new GridBagLayout();
(19)		setLayout(gbl);
(20)
(21)		loadBtn.addActionListener(this);
(22)		add(loadBtn);
(23)		add(graphicsName);
(24)	} //init
(25)
(26)	public void actionPerformed(ActionEvent event) {
(27)		imgToDisplay = getImage( getDocumentBase(), graphicsName.getText() );
(28)		repaint();
(29)	} //actionPerformed()
(30)
(31)	public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
(32)		repaint();
(33) 		if ((infoflags &  ImageObserver.SOMEBITS) != 0) {
(34)   		System.out.println("part just loaded:"+x+" "+y+" "+ width+" "+height);
(35)			return true;
(36)		} //if
(37)		if ((infoflags &  ImageObserver.ALLBITS) != 0) {
(38)   		System.out.println("image loaded completely");
(39) 			return false;
(40) 		} //if
(41)		//gets here when height and width of image is available
(42) 		return true;
(43)   } //imageUpdate()
(44)
(45)	public void paint(Graphics g) {
(46)		if (imgToDisplay != null) {
(47)			g.drawImage(imgToDisplay, 1, 1, this);
(48)		} //if
(49)	} //paint
(50)} //class GfxViewer

Beispiel 69: Ein einfacher Graphik-Viewer   GfxViewer.java

Applet ausführen

Das Beispiel implementiert einen einfachen Graphik-Viewer für die beiden standardmäßig unterstützten Formate.
Es verwendet bereits interaktive Komponenten aus dem Abstract Windows Toolkit (AWT) (siehe init- und actionPerformedMethode. Die rudimentäre Funktionalität erschöpft sich darin, eine Graphikdatei aus dem aktuellen Verzeichnis (ermittelt per getDocumentBase() zu laden, und im aktuellen Fenster anzuzeigen.
Zunächst wird das Bild per getImage-Methode referenziert (graphicsName.getText() gibt den im Textfeld eingegebenen Namen als String zurück).
Der Aufruf von repaint() wirkt auf die gesamte Komponente. Erfolgt das Neuzeichnen, so wird die paint-Methode des Applets aufgerufen. Der dort plazierte Methodenaufruf drawImage auf dem Graphics-Objekt zeichnet das Bild.

Durch Überschreiben der Methode imageUpdate (ererbt von Component, welches die Schnittstelle ImageObserver implementiert) kann der Ladevorgang des Bildes verfolgt werden. Diese Methode wird wärend des Ladevorganges immer wieder automatisch aufgerufen.

Anmerkungen:



Animationen:
...sind prinzipiell nichts anderes als die schnelle Wiedergabe verschiedener Einzelbilder. Jedoch gilt es auf einige Randbedingungen Rücksicht zu nehmen:

Das Beispiel zeigt die Realisierung einer simplen Uhr (Java-Date-Objekt) als animiertes Applet.

(1)import java.applet.Applet;
(2)import java.awt.Graphics;
(3)import java.util.Date;
(4)
(5)public class Clock extends Applet implements Runnable {
(6)	protected int counter;
(7)
(8)	public void start() {
(9)		Thread myClock = new Thread(this);
(10)		myClock.setPriority(Thread.MIN_PRIORITY);
(11)
(12)		myClock.start();
(13)	} //start()
(14)
(15)	public void run() {
(16)		while (true) {
(17)			repaint();
(18)			try {
(19)				Thread.sleep(1000);
(20)			} catch (InterruptedException ie) {
(21)				//ignore it
(22)			} //catch
(23)		} //while
(24)	} //run()
(25)
(26)  	public void paint(Graphics g) {
(27)		g.drawString( new Date().toString(),50,50);
(28)  	} //paint()
(29)} //class Clock

Beispiel 70: Eine einfache Uhr   Clock.java

Applet ausführen

Der notwendige Bildschirmneuaufbau wird in einen separaten niedrig priorisierten Thread verlagert.
Hinweis: Der neue Thread wird nicht, wie bisher per Ableitung von Thread definiert und als spezialisiertes Objekt instanziiert. Das Applet implementiert die Runnable-Schnittstelle; daher kann ein Objekt vom Typ des Applets einer Variable des Typs Thread zugewiesen werden. Erst hierdurch werden die Thread-spezifischen Methoden, wie Prioritätssänderungen, verfügbar.

Die häufigst anzutreffende Realisierung graphischer Animationen ist die Einblendung sich leicht unterscheidender Bilder, wodurch ab einer gewissen Bildfrequenz der Eindruck kontinuierlicher Bewegung entsteht (Trickfilmeffekt).
Diese Animationsvariante kann leicht mit den bisher vorgestellten Möglichkeiten realisiert werden. Hierzu werden zunächst die benötigten Bilder vollständig geladen (z.B. getImage(String) oder getImage(URL) aus der Toolkit-Klasse) und anschließend in schneller Folge am Bildschirm angezeigt (z.B. mit Methoden der drawImage(...)-Familie).
Im Kern ist das Verfahren identisch zur Anzeige statischer Graphiken.

(1)import java.awt.Graphics;
(2)import java.awt.Image;
(3)import java.applet.Applet;
(4)
(5)public class AnalogousClock extends Applet implements Runnable {
(6)	static Image images[];
(7)	static int counter=0;
(8)   Thread animator;
(9)
(10)   public void init() {
(11)		images = new Image[12];
(12)      for (int i=0;i<images.length;i++)
(13)      	images[i] = getImage( getDocumentBase(), "clock-"+i+".gif");
(14)	} //init()
(15)
(16)   public void start() {
(17)   	while ( images[0].getWidth(this) == -1 || images[0].getHeight(this) == -1)
(18)   		;
(19)   	this.resize( images[0].getWidth(this) , images[0].getHeight(this) );
(20)   	animator = new Thread (this);
(21)		animator.setPriority(Thread.MIN_PRIORITY);
(22)      animator.start ();
(23)   } //start()
(24)
(25)	public void stop() {
(26)		animator = null;
(27)	} //stop()
(28)
(29)   public void run () {
(30)		while (true) {
(31)			try {
(32)	      	Thread.sleep(200);
(33)	      } //try
(34)	      catch (InterruptedException ie) {
(35)				//ignore it
(36)	      } //catch
(37)
(38)			repaint();
(39)	      if (++counter==images.length)
(40)	         counter=0;
(41)		} //while
(42)   } //run()
(43)
(44)   public void paint (Graphics g) {
(45)   	System.out.println("drawing image: clock-"+counter+".gif");
(46)   	g.drawImage (images[counter], 0, 0, this);
(47)   } //paint()
(48)} //class AnalogousClock

Beispiel 71: Analoge Uhr als Bildfolge   AnalogousClock.java

Applet ausführen

Das Beispielprogramm zeichnet in einem eigenen Thread alle 200 Millisekunden den Bildschirm mit einem neuen Bild neu.
Zunächst werden durch die init-Methode die benötigten zwölf Graphiken in einen Image-Array geladen. Zum Ausführungsbeginn (start-Methode) wird zunächst die Fenstergröße des Applets (bei Ausführung im Applet-Viewer) auf die Dimensionen der ersten geladenen Graphik -- im Beispiel besitzen alle Graphiken dieselben Ausmaße, daher ist die Auswahl der Referenzgraphik unbedeutend -- gesetzt. Aufgrund der asynchronen Realisierung von getImage() muß auf den Abschluß des Ladevorganges gewartet werden; andenfalls liefern die Methoden getHeight und getWidth mit -1 einen ungültigen Wert zurück.
Der niederpriore Thread animator sorgt alle 200 Millisekunden für den notwendigen Bildschirmneuaufbau -- durch Aufruf der repaint-Methode --, und schaltet die Graphiken fort, wodurch der Animationseffekt entsteht.

Bei dieser Anwendung fällt ein starker Flimmereffekt ins Auge. Dieses (unschöne) Verhalten ist oftmals bei graphischen Java-Applets und -Applikationen anzutreffen. Zur Behebung existieren einige Standardlösungsvarianten:

Die einfachste Lösung liegt im Überschreiben der von Component ererbten Methode update().
Im Standardfalle löscht update() den gesamten Bildschirmbereich und ruft im Anschluß paint() zum vollständigen Neuzeichnen auf.

Im vorliegenden Beispiel ist jedoch das dem Neuzeichnen vorausgehende Löschen nicht notwendig, da die nachfolgende Graphik ohnehin die vorhergende vollständig überdeckt.
Daher kann die update-Methode auf den reinen Aufruf von paint beschränkt werden.
Das zusätzliche Codefragment ergibt sich als:

public void update(Graphics g) {
   paint(g);
} //update()

ergänzter Quellcode
ergänztes Applet ausführen

Diese einfache Maßnahme hilft oftmals bei auftretenden Problemen, jedoch ist sie nur für den eng umrissenen Problemkreis anwendbar, in dem neue graphische Elemente die vorhergehenden vollständig überdecken. Andernfalls kann es zu Darstellungsfehlern kommen.
Daher kommt bei realen Problemstellungen zumeist double Buffering zum Einsatz.

Der Mechanismus des double bufferings macht es sich zu Nutze, daß auch im nicht sichtbaren Bereich des Bildschirms graphische Operationen vorgenommen werden können. Die Anwendung verläuft üblicherweise in zwei Schritten: zunächst wird der darzustellende Bildschirminhalt verdeckt vorbereitet, und im zweiten Schritt in einer einzigen -- im Allgemeinen sehr performant ausgeführten -- Kopieroperation in den sichtbaren Bereich übertragen.

Ablaufprinzip:

  1. Erzeugung eines Image-Objekts beliebiger Größe (gesteuert durch die beiden Übergabeparameter) mittels createImage(int, int).
    createImage(int, int) ist in der Klasse Component definiert, und wird an Applet vererbt.
  2. Erzeugung des Zeichenbereichs (graphics context) durch die Methode getGraphics() auf dem Image-Objekt.
  3. Zeichenoperationen auf dem Graphikkontext.
  4. Anzeigen des Graphikkontextes inerhalb der paint-Methode mit einer Methode aus der drawImage(...)-Familie.

Codeschablone:

Image im = createImage(100,100);
Graphics graph = im.getGraphics();
//Operationen auf graph

public void paint(Graphics g) {
   g.drawImage(im, ...);
} //paint()

In paint() werden die vorgezeichneten Image-Objekte ohne weitere Veränderungen angezeigt.
Je nach Komplexität der Anwendung können so viele Image-Objekte wie benötigt parallel gehalten und bei Bedarf angezeigt werden.

back to top   3.2.4 Collection API

 

Die Collection API liefert als Bestandteil der Java Standard API Hilfsklassen zum Umgang mit generischen Objektsammlungen.
Der Begriff Collection bezeichnet Datenstrukturen wie Listen, Stacks, Schlangen, Bäume und Mengen im mathematischen Sinne.

Das Collection Framework der Java-API wird aus den vorgegebenen Schnittstellen, den Operationen und ihrer definierten Semantik, den konkreten Implementierunge der wiederverwendbaren Datenstrukturen und der zugrundeliegenden Algorithmen gebildet.
Das wohl bekannteste Beispiel eines solchen Rahmens dürfte die C++ Standard Template Libraray (Abk. STL) und die Collection-Klassen der Programmiersprache SmallTalk sein.
Neben dem Collection Framework der Java API existiert mit der Generic Collection Library for Java (Abk. JGL) auch noch eine frei verfügbare Adaption der STL an Java.

Da Java bis zur Version 1.5 keine parametrische Polymorphie unterstützte sind diese Klassen auch (noch) in ihrer generischen Fassung umgesetzt. Ihr Einsatz unter Nutzung der Java Generics wird Gegenstand des nächsten Kapitels sein.
Um ihre Einsetzbarbeit möglichst universell zu gestalten erlauben alle Collection-Klassen die Verwaltung von Objekten des Typs Object, dem gemeinsamen Basistyp aller Java-Objekte.

Durch diese Mimik geht die Typsicherheit verloren!
Das bedeutet, daß in jeder Collection ausschließlich Objekte des Typs Object verwaltet werden können, die vor dem Einstellen in die Sammlung (implizit und automatisch) in diesen Typ konvertiert werden. Diese Typumwandlung ist als up-cast typsicher möglich, da alle Java-Klassen immer direkte oder transitive Subklasse von Object sind. Bei der Entnahme von Collection-Elementen ist jedoch die Rekonstruktion des Ursprungstypen notwendig. Diese Typumwandlung ist als down-cast explizit anzugeben. Beim Konvertierungsversuch in den falschen Typ kann es hier zu einer ClassCastException kommen.
Unter praktischen Gesichtspunkten bietet sich die Vorschaltung einer Typprüfung durch instanceof an.

Andererseits schafft die Abkunft aller Java-Objekte von Object auch erst die Voraussetzungen für ein generisches Collection Framework zur Verwaltung beliebiger Java-Objekte.
Hiermit sind im Speziellen drei Methoden der Klasse Object gemeint:

Bereits seit Vesion 1.0 des JDKs stehen die Klassen Bitset, Dictionary, Hashtable, Stack und Vector zur Verfügung.
Diese wurde mit dem JDK v1.2 um einige weitere Klassen und Schnittstellen ergänzt.

Die Collection-Klassen und ihre Schnittstellen

Mit Ausnahme von Vector und Stack wurden alle dargestellten Klassen und Schnittstellen mit der Java-2-Plattform eingeführt.

Grundlegende Konzepte:
Gemeinsam sind allen Collection-Varianten die basalen Operationen

Darüberhinaus bieten die verschiedenen Collection-Klassen auch Möglichkeiten zur Suche, Sortierung usw. an.

Interessante (Realisierungs-)Details einer jeden Collection-Klasse sind weiter:

Das Standard Collection Framework umfaßt folgende ausprogrammierte Klassen:

Klasse
Charakteristika/Einsatzgebiete
Dynamischer Array, der intern durch statischen (Java nativen) Array realisiert ist.
Lesender und schreibender indizierter Zugriff auf beliebige Elementposition.
Ähnelt Vector, ist jedoch nicht threadsicher.
Bereits im JDK v1.0 enthalten, mittlerweile um Schnittstelle List angereichert. Threadsichere Variante der ArrayList.
Implementierung einer doppelt verketteten Liste. Dadurch konstanter Aufwand beim Einfügen und Löschen an jeder beliebigen Listenposition, jedoch höherer Aufwand beim Anfügen und Entnehmen von letzter Listenposition als bei ArrayList.
Stack nach dem last-in-first-out-Prinzip.
Wie Vector bereits seit dem JDK v1.0 angeboten, und daher nicht originärer Bestandteil des Collection Frameworks.
Ebenfalls wie Vector threadsicher realisiert.
Hash-Tabelle die Einstellung von Werten über einen eineindeutigen Schlüssel.
Bereits mit JDK v1.0 eingeführt; nicht Bestandteil des Collection Frameworks.
Realisierung einer Menge im mathematischen Sinne, d.h. anordnungslose (assoziative) Speicherung paarweise verschiedener (Duplikatfreiheit!) Elemente.
Gleiches Prinzip wie HashSet, jedoch interne Speicherorganisation als Baum.
Die Elemente werden, sofern nicht anders angegeben, in ihrer natürlichen Ordnung abgelegt. Hierzu muß durch die Klasse jedes abzulegenden Objektes die Comparable-Schnittstelle umgesetzt sein, d.h. die compareTo-Methode implementiert werden.
Erweiterung des HashSets um Schlüssel.
Ablage von beliebigen Werten (auch Duplikaten) gemeinsam mit einem eineindeutigen Schlüssel. (Dieser Strukturtyp wird auch als Bag bezeichnet.) Die Einträge werden intern durch eine Hash-Tabelle verwaltet.
Äquivallent zur Hashtable, abgesehen von der mangelnden Threadsicherheit und den zugelassenen Null-Elementen.
Erweiterung der HashMap auf eine Baum-basierte (rot-schwarz-Baum) Ablage eines Wertes (Object) mit einem eineindeutigen Schlüssel.
Prinzipiell identisch zur HashMap, jedoch werden Schlüssel-Wert-Paare entfernt, sobald die letzte externe Referenz auf den Schlüssel ungültig wird.

Ein einführendes Beispiel:
Die Klasse Vector ist dem „klassischen“, d.h. bereits mit JDK v1.0 eingeführten, Anteil der Collection-API zuzurechnen. Sie stellt im Grunde eine dynamische Arrayimplementierung zur Verfügung, die intern als statischer Array realisiert ist. Wie bei Arrays üblich erfolgt der Element-Zugriff durch einen ganzzahligen int-Index.
Durch den Ausbau der Collection-API in der Java-2-Plattform wurde die Vector-Implementierung nochmals überarbeitet und an die heute Struktur (namentlich die neu eingeführte Schnittstelle List) adaptiert.

(1)import java.io.FileDescriptor;
(2)import java.io.FileNotFoundException;
(3)import java.io.FileReader;
(4)import java.io.IOException;
(5)import java.io.LineNumberReader;
(6)import java.util.Vector;
(7)
(8)public class VectorEx1 {
(9)	public static void main(String[] args) {
(10)		LineNumberReader lnr;
(11)		String line;
(12)		Vector vec = new Vector();
(13)
(14)		try {
(15)			lnr = new LineNumberReader(new FileReader( FileDescriptor.in ) );
(16)
(17)			while ( (line = lnr.readLine()).compareTo("exit")!=0 ) {
(18)				vec.add(line);
(19)			} //while
(20)			lnr.close();
(21)
(22)			for (int i=0; i<vec.size(); i++)
(23)				System.out.println("element no. "+i+": "+vec.get(i) );
(24)		} catch (FileNotFoundException fnfe) {
(25)			System.out.println("cannot find stdin!");
(26)		} catch (IOException ioe) {
(27)			System.out.println("an IOException occurred\n+"+ioe.toString()+"\n"+ioe.getMessage() );
(28)		} //catch
(29)	} //main()
(30)} //class VectorEx1

Beispiel 72: Vektor-Operationen   VectorEx1.java

Das Programm liest solange von der Standardeingabe zeilenweise Zeichenketten ein, bis der String exit eingegeben wird. Jede eingelesene Zeichenkette wird -- mit der Methode add() -- als Vector-Element angelegt.
Nach Beendigung des Einlesevorganges werden die gespeicherten Vector-Elemente in der Reihenfolge der Anlage ausgegeben. Dies geschieht per indiziertem Zugriff auf jedes Element mit der Methode get(int). Der Zugriff erfolgt nicht mehr, wie bei Array üblich per direkter Indizierung in eckigen Klammern, sondern jetzt über eine eigene Methode, die jedoch den gewohnten Sicherheitsmechanismus der ArrayOutOfBoundException beim Zugriffsversuch außerhalb der Vector-Grenzen beibehält.

Beispielablauf:

$java vectorEx1
this
is
a
simple
test
containing an

empty
line
exit
element no. 0: this
element no. 1: is
element no. 2: a
element no. 3: simple
element no. 4: test
element no. 5: containing an
element no. 6:
element no. 7: empty
element no. 8: line

Die Skalierbarkeit eines Vektor-Objekts kann durch Konstruktor beeinflußt werden. Der erlaubt die Angabe zweier Freiheitsgrade, der initialen Kapazität und eines Wachstumsfaktors. Werden, wie im Beispiel, keine Parameter übergeben, so wird gegenwärtig (d.h. im JDK v1.3) ein Vector der Größe zehn mit einem Wachstumsfaktor von Null erzeugt.
Wird durch die Einfügemethode add die Aufnahmekapazität des Vectors überschritten, so wächst er um den als capacityIncrement angegebenen Faktor, oder verdoppelt seine gegenwärtige Größe falls kein Wachstumsfaktor angegeben wurde. In beiden Fällen sind jedoch alle Vector-Elemente in einen neu zu erzeugenden Array zu transferieren, wodurch es bei umfangreichen Vectoren zu Zeitverlusten kommen kann. Die in der gezeigten Implementierung benutzte Umsetzung durch System.arraycopy läuft als native Methode jedoch sehr performant ab. Vor grösseren Einfügevorgängen bekannten Umfanges bietet sich jedoch die explizite Allokation des notwendigen Speicherplatzes durch Aufruf der Methode ensureCapacity(int) an, die sicherstellt, daß mindestens die im Parameter übergebene Anzahl von Objekten ohne zusätzliche Allokations- und Kopiervorgänge in den Vector eingestellt werden kann.
Der nachfolgende Codeausschnitt aus den Java-Quellen läßt das interne Verhalten von Vector anschaulich werden.

package java.util;

public class Vector extends AbstractList implements List, Cloneable, java.io.Serializable {
   protected Object elementData[];
   protected int capacityIncrement;
   protected int elementCount;

   public Vector() {
      this(10);
   } //constructor

   public Vector(int initialCapacity) {
      this(initialCapacity, 0);
   } //constructor

   public Vector(int initialCapacity, int capacityIncrement) {
      ...
      this.elementData = new Object[initialCapacity];
      ...
   } //constructor

   public synchronized Object get(int index) {
      if (index >= elementCount)
         throw new ArrayIndexOutOfBoundsException(index);

      return elementData[index];
   } //get()

   public synchronized boolean add(Object o) {
      ...
      ensureCapacityHelper(elementCount + 1);
      elementData[elementCount++] = o;
      return true;
   } //add()

   private void ensureCapacityHelper(int minCapacity) {
      int oldCapacity = elementData.length;
      if (minCapacity > oldCapacity) {
         Object oldData[] = elementData;
         int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) : (oldCapacity * 2);
         if (newCapacity < minCapacity) {
            newCapacity = minCapacity;
         } //if
         elementData = new Object[newCapacity];
         System.arraycopy(oldData, 0, elementData, 0, elementCount);
      } //if
    } //ensureCapacityHelper()

   public Enumeration elements() {
      return new Enumeration() {
         int count = 0;

         public boolean hasMoreElements() {
            return count < elementCount;
         } //hasMoreElements()

         public Object nextElement() {
            synchronized (Vector.this) {
               if (count < elementCount) {
                  return elementData[count++];
               } //if
            } //synchronized
            throw new NoSuchElementException("Vector Enumeration");
         } //nextElement()
      }; //elements()
} //class Vector

Einen einfacheren Weg zur Traversierung eines Vectors bietet die Schnittstelle Enumeration an.
Ein solches Objekt wird durch die Methode elements() zurückgegeben. Sie abstrahiert nochmals vom indizierten Zugriff, und stellt mit den Methoden hasMoreElements() und nextElement() generische Zugriffsprimitiven bereit, die so auch in anderen Collection-Klassen Anwendung finden.
Nebenbemerkung: Die Implementierung von elements() stellt ein (schönes) Beispiel für die Verwendung anonymer innerer Klassen dar.

Bei Modifikationen durch nebenläufig ausgeführte Threads am zugrundeliegenden Vector nachdem ein Enumeration-Objekt erzeugt wurde, kann es zu Konsistenzproblemen kommen. Daher wird seit dem JDK v1.2 die Verwendung der Schnittstelle ListIterator gegenüber der weiter angebotenen Enumeration empfohlen. Ihre Implementierung ist fail-fast, d.h. sie erzeugen ein Ausnahmeereignis-Objekt der Klasse ConcurrentModificationException im Falle nebenläufiger Modifikation. Zusätzlich erlaubt der Iterator selbst die Entnahme von Elementen aus der zugrundeliegenden Sammlung.
Der zu einer Collection gehörige Iterator wird über die (von List geerbte) Methode listIterator() geliefert.
Vorwärtsreferenz: Beispiel zur Nutzung eines Iterators

Mengen im mathematischen Sinne (d.h. duplikatfrei und ungeordnet) lassen sich durch die Klasse HashSet realisieren.
Mit den (ererbten) Methoden addAll (Vereinigung), retainAll (Durchschnitt), removeAll (Differenz) können die grundlegenden Mengenoperationen realisiert werden.

Eingabedaten des Beispiels
(1)import java.util.HashSet;
(2)import java.util.Iterator;
(3)
(4)public class HashSetEx1 {
(5)	public static void main(String[] args) {
(6)		HashSet hs1 = new HashSet();
(7)		HashSet hs2 = new HashSet();
(8)		HashSet hs3;
(9)
(10)		hs1.add( new String("a") );
(11)		hs1.add( new String("b") );
(12)		hs1.add( new String("c") );
(13)
(14)		hs2.add( new String("c") );
(15)		hs2.add( new String("d") );
(16)		hs2.add( new String("e") );
(17)
(18)		System.out.println("hs1="+hs1.toString() );
(19)		System.out.println("hs2="+hs2.toString() );
(20)
(21)		//union
(22)		hs3 = new HashSet( hs1 );
(23)		hs3.addAll(hs2);
(24)		System.out.println("UNION(hs1,hs2)="+hs3.toString() );
(25)
(26)		hs3 = null;
(27)		//difference
(28)		hs3 = new HashSet( hs1 );
(29)		hs3.removeAll( hs2 );
(30)		System.out.println("hs1 WITHOUT hs2 ="+hs3.toString() );
(31)
(32)		hs3 = null;
(33)		//intersection
(34)		hs3 = new HashSet( hs1 );
(35)		hs3.retainAll( hs2 );
(36)		System.out.println("INTERSECTION(hs1,hs2)="+hs3.toString() );
(37)
(38)		hs3 = null;
(39)		//symmetric difference
(40)		hs3 = new HashSet( hs1 );
(41)		hs3.addAll( hs2 );
(42)		HashSet tmp1 = new HashSet( hs1 );
(43)		tmp1.retainAll( hs2 );
(44)		hs3.removeAll( tmp1 );
(45)		System.out.println("Symmtetric Difference(hs1,hs2)="+hs3.toString() );
(46)
(47)		hs3 = null;
(48)		//cartesian product
(49)		hs3 = new HashSet();
(50)		String c1;
(51)
(52)		for( Iterator hs1It = hs1.iterator(); hs1It.hasNext(); ) {
(53)			c1 = hs1It.next().toString();
(54)			for( Iterator hs2It = hs2.iterator(); hs2It.hasNext(); )
(55)			{
(56)				hs3.add( new String( c1 + (hs2It.next()).toString() ) );
(57)			} //for
(58)		} //while
(59)		System.out.println("Cartesian Product(hs1,hs2)="+hs3.toString() );
(60)	} //main()
(61)} //class HashSetEx1

Beispiel 73: Die grundlegenden Mengenoperationen   HashSetEx1.java

Bildschirmausgabe:

hs1=[b, a, c]
hs2=[e, d, c]
UNION(hs1,hs2)=[b, a, e, d, c]
hs1 WITHOUT hs2 =[b, a]
INTERSECTION(hs1,hs2)=[c]
Symmtetric Difference(hs1,hs2)=[b, a, e, d]
Cartesian Product(hs1,hs2)=[ce, cd, cc, be, bd, bc, ae, ad, ac]

Das Beispiel zeigt neben Realisierungen der grundlegenden Mengenoperationen auch die Nutzung eines Iterators anstelle der veralteten Enumeration.



Die ... Map-Klassen

Die von der abstrakten Basisklasse AbstractMap abgeleiteten konkreten Klassen HashMap, TreeMap und WeakHashMap stellen eine von der Collection-Hierarchie losgelöste besondere Variante allgemeiner Object-Sammlungen zur Verfügung.
Ihnen allen gemein ist die Speicherung eines eineindeutigen Zugriffsschlüssels, zusätzlich zu den Nutzinformationen der verwalteten Objekte.

Die Klasse HashMap organisiert die abgelegten Objekte intern über eine Hashfunktion, TreeMap über einen rot-schwarz-Baum.
WeakHashMap ist prinzipiell identisch zur HashMap, jedoch werden Schlüssel-Wert-Paare entfernt, sobald die letzte externe Referenz auf den Schlüssel ungültig wird.

Das Beispielprogramm ermittelt die Auftrittshäufigkeit von einzelnen Worten oder Zahlen im Eingabestrom. Hierfür wird der bekannte StreamTokenizer zur entsprechenden Eingabefilterung benutzt.
Grundgedanke des realisierten Algorithmus ist es, das Wort oder die Zahl als Schlüssel in der HashMap zu nutzen, und die bisher ermittelten Vorkommen als Nutzinformation in einem Integer-Objekt abzulegen.

(1)import java.io.StreamTokenizer;
(2)import java.io.FileReader;
(3)import java.io.FileDescriptor;
(4)import java.io.IOException;
(5)import java.util.HashMap;
(6)import java.util.Map;
(7)
(8)public class WordCount {
(9)	private static final Integer ONE = new Integer(1);
(10)
(11)	public static void main(String[] args) {
(12)		Map m = new HashMap();
(13)
(14)		try {
(15)			StreamTokenizer st = new StreamTokenizer( new FileReader( FileDescriptor.in ) );
(16)			int res;
(17)			Integer freq;
(18)
(19)			while( (res = st.nextToken()) == StreamTokenizer.TT_WORD || res == StreamTokenizer.TT_NUMBER) {
(20)				if (res == StreamTokenizer.TT_WORD)
(21)					freq = (Integer) m.get( st.sval );
(22)            else
(23)            	freq = (Integer) m.get( new String(""+st.nval) );
(24)
(25)            if (res == StreamTokenizer.TT_WORD)
(26)            	m.put( st.sval, (freq==null ? ONE : new Integer(freq.intValue() + 1)));
(27)				else
(28)					m.put( new String(""+st.nval), (freq==null ? ONE : new Integer(freq.intValue() +1)));
(29)			} //while
(30)        System.out.println(m.size()+" distinct words detected:");
(31)        System.out.println(m);
(32)		} catch (IOException ioe) {
(33)			System.out.println("an IOException occurred\n"+ioe.getMessage() );
(34)		} //catch
(35)	} //main()
(36)} //class WordCount

Beispiel 74: Ermittelt die Auftrittshäufigkeit eines Wortes   WordCount.java

Beispielablauf:
Die Datei testfile.asc:

the quick brown fox jumps over the lazy dog
1 2 3 123 1 2 X
$java wordCount < testfile
13 distinct words detected:
{X=1, lazy=1, fox=1, quick=1, 1.0=2, over=1, the=2, dog=1, 123.0=1, brown=1, 2.0=2, 3.0=1, jumps=1}


Die Positionierung eines Elements innerhalb einer der Hash-Sammlungen (HashMap, HashSet und Hashtable) wird während der Ausführung der put-Methode durch eine Hashfunktion, die auf die Methode hashCode der Klasse Object zurückgreift, bestimmt.
Beim Erzeugungsvorgang jedes Hash-...-Objekt kann der Speicherplatzbedarf und das Laufzeitverhalten über zwei Parameter beeinflußt werden. Die initiale Kapzität (engl. initial capacity) definiert die Anzahl der Einträge in der Hashtabelle, und der Lastfaktor (engl. load factor) den maximalen Füllungsgrad einer Hashtabelle bevor automatisch eine Kapazitätserweiterung durchgeführt wird.
Werden die beiden Parameter bei der Erzeugung nicht angegeben, so wird vorgabegemäß eine Datenstruktur mit Freiraum für elf Objekte, und einem Lastfaktor von 0.75 erzeugt. Mithin wird die erste Kapazitätserweiterung beim Einfügeversuch des neuten Elements durchgeführt.

Algorithmen auf Objektsammlungen vom Typ Array
... werden durch die Klasse Arrays in Form statischer Methoden angeboten.

Algorithmen auf Objektsammlungen vom Typ List
... werden durch die Klasse Collections in Form statischer Methoden angeboten.

Anmerkung zur Verwendung der beiden Sortierverfahren Mergesort und Quicksort in den Klassen Arrays und Collections:
Beim ersten Hinsehen verwundert die Implementierung zweier verschiedener Sortierverfahren ein wenig, nicht zuletzt wegen des Wechsels des Sortierverfahrens in der Implementierung der sort-Methoden der Klasse Arrays. Wird dort doch für alle Sortieroperationen auf Primitivtypen-Arrays eine modifizierte Quicksort-Variante eingesetzt, jedoch für Object-Arrays zu Mergesort übergegangen.
Dies liegt in der Natur der beiden Sortierverfahren begründet. Quicksort ist eines der bekanntesten und vermutlich auch am besten erforschtesten Sortierverfahren; mit guten Leistungsdaten im allemeinen Falle. Jedoch kann der Sortiervorgang im schlechtesten Falle bis zu n2 Schritte benötigen. Insbesondere für große n ist daher Mergesort -- mit seinen garantierten n log(n) Schritten effizienter.
Allerdings benötigt Quicksort deutlich weniger Speicherplatz (eigentlich nur den Stack durch die rekursiven Aufrufe), während Mergesort für seinen Ablauf stets den doppelten Speicherbedarf der zu sortierenden Datenstruktur für sich reklamiert.
Aus praktischen Gründen, nicht zuletzt wegen des uniformen Laufzeitverhaltens, wurde dem speicherplatzintensiveren Mergesort der Vorzug gegeben. Da in realen Anwendungen in den seltensten Fällen reine Primitivtypen-Arrays sortiert werden, nutzt der Anwender überwiegend die vorhandenen Mergesort-Implementierungen, die jedoch in (begründeten) Einzelfällen durchaus durch eigene Routinen ersetzt werden können.

back to top   3.2.5 Parametrische Polymorphie/Generics

 

Die wirkungsmächtigste Neuerung der Java Version 1.5 bildet die Einführung der sog. Generics, welche das Feld der Template-basierten Metaprogrammierung und mithin die parametrische Polymorphie eröffnen.

Das sinnvollste Einsatzfeld dieses Mechanismus bildet die Anwendung auf die Vorhandenen Klassen der Collection-API zur Realisierung typsicherer Objektsammlungen.

Die allgemeine Syntax der Generics erfordert die Nachstellung, des durch spitze Winkelklammern eingeschlossenen Typnamen, nach dem Namen der so typisierten Sammlung. So wird eine Liste, die ausschließlich Objekte des Typs String enthält als List<String> definiert.

Als Resultat einer so definierten Sammlungsklasse wird bereits zum Übersetzungszeitpunkt die Konformität abgeprüft. Daher wird bei jedem Versuch Daten in eine solch konkretisierte Sammlung einzufügen geprüft, ob diese unter Beachtung der Typrestriktion kompatibel zum in der Deklaration angegebenen Inhaltstyp sind.

Konventionsgemäß erlaubt Java ausschließlich die Festlegung von Klassen als Inhaltstypen von Objektsammlungen; Primitivtypen sind von der Verwendung ausgeschlossen.

Das nachfolgende Beispiel zeigt die Definition einer Sammlung des Typs LinkedList, deren Inhaltselemente auf Ausprägungen des Typs Integer beschränkt sind.

(1)import java.util.LinkedList;
(2)
(3)public class GenTest1 {
(4)	public static void main(String[] args) {
(5)		LinkedList<Integer>  integerList  = new LinkedList<Integer>();
(6)
(7)		integerList.add(new Integer(42));
(8)		integerList.add( 84 );
(9)
(10)		System.out.println( integerList );
(11)	} //main()
(12)} //class GenTest1

Beispiel 75: Typisierung einer generischen Liste   GenTest1.java

Das Beispiel zeigt die Definition einer typisierten LinkedList, für die bereits zum Übersetzungszeitpunkt geprüft wird, daß ausschließlich Ausprägungen der Klasse Integer der Liste hinzugefügt werden.
Diese Restriktion verhindert jedoch, vermöge der Nutzung des dynamischen Boxings nicht, daß Ausprägungen des Primitivtyps int direkt übergeben werden, da diese für den Programmierer transparent in Objekte des Type Integer umgewandelt werden.
Versuche Objekte oder Werte anderen Typs in die Liste aufzunehmen werden bereits zum Übersetzungszeitpunkt erkannt und als Fehler gemeldet.

Die Einführung typisierter Sammlungen bedingt auch die entsprechende Adaption der Hilfsklassen zur Traversierung. vor diesem Hintergrund zeigt das nachfolgende Beispiel die Typisierung eines Iterators. Als Konsequenz dieser Konkretisierung des Iterators liefert der Aufruf der Methode next statt der generischen Object-Instanz eine Ausprägung des Parametertyps String.

(1)import java.util.LinkedList;
(2)import java.util.Iterator;
(3)
(4)public class GenTest2 {
(5)	public static void main(String[] args) {
(6)		LinkedList<String>  nameList  = new LinkedList<String>();
(7)		
(8)		nameList.add( "Berta" );
(9)		nameList.add( "Anton" );
(10)		nameList.add( "Cesar" );	
(11)
(12)		Iterator<String> listIterator = nameList.iterator();
(13)		String value;
(14)		while(listIterator.hasNext()) {
(15)			value = listIterator.next();
(16)			System.out.println( value );
(17)		} //while
(18)	} //main()
(19)} //class GenTest2

Beispiel 76: Typisierung eines Iterators   GenTest2.java

Neben der Anwendung auf bestehende Klassen des Collection-Frameworks können Generics auch für eigenerstelle Klassen eingesetzt werden.
Das Beispiel zeigt die Definition der Klasse TopTen, welche eine Liste von Einträgen beliebigen Typs zusammen mit einer Plazierung gestattet. Der Inhaltstyp wird hierbei als Polymorphieparameter zum Konstruktionszeitpunkt der TopTen-Objekte festgelegt. Innerhalb der Klasse wird zur Verwaltung der Daten eine TreeMap eingesetzt, die entsprechend der Initialisierung des TopTen-Objekts erzeugt wird um auch auf dieser Ebene die Typkonformität zu gewährleisten.

(1)import java.util.TreeMap;
(2)import java.util.Collection;
(3)import java.util.Iterator;
(4)
(5)public class GenTest3 {
(6)	public static void main(String[] args) {
(7)		TopTen<String> music = new TopTen<String>();
(8)		music.addAtPosition(1, "St. Anger/Metallica");
(9)		music.addAtPosition(3, "20 Jahre - Nena feat. Nena/Nena");
(10)		music.addAtPosition(2, "9/Eros Ramazotti");
(11)		
(12)		music.print();
(13)	} //main()
(14)} //class GenTest3
(15)
(16)class TopTen<Type> {
(17)	private TreeMap<Integer,Type> content;
(18)	public TopTen() {
(19)		content = new TreeMap<Integer,Type>();
(20)	} //constructor
(21)	public boolean addAtPosition(int pos, Type value) {
(22)		if (content.size() == 10) {
(23)			return false;
(24)		} else {
(25)			content.put(new Integer(pos), value);
(26)			return true;
(27)		} //else
(28)	} //addAtPosition()
(29)	public Type getFromPosition(int pos) {
(30)		return content.get(new Integer(pos));
(31)	} //getFromPosition()
(32)	public void print() {
(33)		Iterator<Type> i = content.values().iterator();
(34)		while (i.hasNext()) {
(35)			System.out.println( i.next() );
(36)		} //while()
(37)	} //print()
(38)} //class TopTen

Beispiel 77: Anwendung von Generics für eigene Klassen   GenTest3.java

Neben der Angabe von Klassen zur Typisierung einer generischen Aufzählung können, mit derselben Mächtigkeit, auch Aufzählungstypen angegeben werden. Das nachfolgende Beispiel zeigt eine Anwendung:

(1)import java.util.LinkedList;
(2)import java.util.HashMap;
(3)
(4)public class EnumTest4 {
(5)public static void main(String argv[]) {
(6)	enum DayOfWeek {monday, tuesday, wednesday, thursday, friday,
(7)	saturday, sunday};
(8)
(9)	LinkedList<DayOfWeek> allDays = new LinkedList<DayOfWeek>();
(10)	allDays.add(DayOfWeek.monday);
(11)	allDays.add(DayOfWeek.tuesday);
(12)	allDays.add(DayOfWeek.wednesday);
(13)	allDays.add(DayOfWeek.thursday);
(14)	allDays.add(DayOfWeek.friday);
(15)	allDays.add(DayOfWeek.saturday);
(16)	allDays.add(DayOfWeek.sunday);
(17)
(18)	HashMap<DayOfWeek,Double> sales = new HashMap<DayOfWeek,Double>();
(19)
(20)	sales.put(DayOfWeek.monday, 3000.0 );
(21)	sales.put(DayOfWeek.tuesday, 5000d );
(22)	sales.put(DayOfWeek.thursday,  7500D );
(23)	sales.put(DayOfWeek.wednesday,  (double) 8000 );
(24)	sales.put(DayOfWeek.friday,  55000.0 );
(25)	sales.put(DayOfWeek.saturday, new Double("10000"));
(26)	sales.put(DayOfWeek.sunday, (new Double("0")).doubleValue() );
(27)
(28)	for (DayOfWeek w : allDays)
(29)		System.out.println(w+" "+sales.get(w));
(30)	} //main()
(31)} //class EnumTest4

Beispiel 78: Aufzählungstypen als Polymorphieparameter   EnumTest4.java

Zwar vermeidet der Einsatz der Generics die Anwendung unsicherer expliziter Typkonversionen fast völlig, jedoch ist ihre Notwendigkeit in Einzelfällen nach wie vor gegeben.
Das nachfolgende Beispiel zeigt einen solchen Fall. Es definiert eine als Vector angelegte Sammlung von Objekten des Typs Person. Gemäß der Substitutionsregeln können auch Spezialisierungen der Person, mithin Ausprägungen der davon abgeleiteten Klasse Mitarbeiter, typkonform der Sammlung hinzugefügt werden.
Da die Methode get im allgemeinen Falle Ausprägungen der Klasse Object und unter Einsatz der Generics Ausprägungen des Parametertyps (in diesem Falle: Person) liefert würde die abgespeicherte Mitarbeiter-Instanz lediglich als Ausprägung der Klasse Person aus der Sammlung entnommen werden. Um eine korrekte Weiterverarbeitung (z.B. in Form des Zugriffs auf das Attribut personalnummer) als Mitarbeiter zu gewährleisten ist daher eine explizite Typkonversion notwendig.

(1)import java.util.Vector;
(2)
(3)public class GenTest4 {
(4)	public static void main(String[] args) {
(5)		Vector<Person> persL = new Vector<Person>();
(6)
(7)		Person p = new Person();
(8)		p.name = "Max Mustermann";
(9)		persL.add(p);
(10)
(11)		Mitarbeiter m = new Mitarbeiter();
(12)		m.name = "John Doe";
(13)		m.personalnummer = "1234";
(14)		persL.add(m);
(15)
(16)		Person p2 = persL.get(0);
(17)		System.out.println( p2.name );
(18)
(19)		Mitarbeiter m2 = (Mitarbeiter) persL.get(1);
(20)		System.out.println( m2.name );
(21)		System.out.println( m2.personalnummer );
(22)	} //main()
(23)} //class GenTest4
(24)
(25)class Person {
(26)	public String name;
(27)} //class Person
(28)
(29)class Mitarbeiter extends Person {
(30)	public String personalnummer;
(31)} //class Mitarbeiter

Beispiel 79: Explizite Typwandlung trotz Generics   GenTest4.java

back to top   3.2.6 Reflection API

 

Mit der JDK-Version 1.1 wurde die Möglichkeit geschaffen zur Laufzeit Informationen über die verwendeten Informationsstrukturen zu gewinnen. Genaugenommen handelt es sich hier um Information über Information, die üblicherweise als Metainformation bezeichnet wird.
Bereits seit der Version 1.0 existierte mit der Klasse Class ein Mechanismus, der Möglichkeit zur Ermittlung des Typs im Stile der aus C++ bekannten Runtime Type Identification bietet.

Der Begriff Laufzeittyp meint den konkreten Typen eines Attributes oder einer Variable (im Allgemeinen: einer referenzierten typisierten Speicherposition). Dieser Typ muß nicht zwingend über die gesamte Ausführungszeit mit dem zum Definitionszeitpunkt festgelegten identisch sein. Er kann sich durch typkonforme Neuzuweisung ändern.

Die Klasse Class

Zur Motivation: Bisher war es uns mit dem instanceof-Operator nur möglich zu entscheiden ob ein gegebenes Objekt Ausprägung einer bestimmten Klasse ist. Hierfür mußte der Namen der Klasse zum Ausführungszeitpunkt des Operators bekannt sein.

Die Methode getClass() welche, da von Object geerbt, auf allen Java-Objekten zur Verfügung steht behebt dieses Manko.
Sie liefert ein Objekt der Klasse Class zurück welches -- neben zahlreichen weiteren Informationen -- den Namen der Klasse zur Laufzeit verfügbar werden läßt.
Class ist im Paket java.lang angesiedelt, und steht daher ohne explizite import-Anweisung in allen Java-Programmen zur Verfügung. Zu jeder Klasse im System existiert je ein solches Class-Objekt.

(1)public class RefEx1 {
(2)	public static void main(String[] args) {
(3)		Person p1 = new Person();
(4)		Mann m1 = new Mann();
(5)
(6)		System.out.println("runtime class of variable p1="+ p1.getClass().getName() );
(7)		System.out.println("runtime class of variable m1="+ m1.getClass().getName() );
(8)
(9)		p1 = m1; //up-cast is typesafe
(10)		System.out.println("runtime class of variable p1="+ p1.getClass().getName() );
(11)	} //main()
(12)} //class RefEx1
(13)
(14)class Person {
(15)} //class Person
(16)
(17)class Mann extends Person {
(18)} //class Mann
(19)
(20)class Frau extends Person {
(21)} //class Frau

Beispiel 80: Ermittlung der Laufzeitklasse   RefEx1.java

Bildschirmausgabe:

$java RefEx1
runtime class of variable p1=Person
runtime class of variable m1=Mann
runtime class of variable p1=Mann

Die Anwendung definiert neben der Hauptklasse drei weitere Klassen Person, Mann und Frau. Sie sind in der Weise organisiert, daß Mann und Frau als Subklassen von Person realisiert sind.
Zunächst werden Variablen vom Typ Person und Mann definiert und mit Ausprägungen der Definitionstypen initialisiert. Durch den Methodenaufruf getName() auf dem durch getClass() zurückgelieferten Class-Objekt kann der Name der Laufzeitklasse im Klartext ermittelt werden. So liefert die Variable p1 welche auf eine Person-Ausprägung verweist erwartungsgemäß den Namen dieser Klasse zurück; für die Variable m1 und die Klasse Mann gilt dies analog.
Durch der Zuweisung des Mann-Objekts m1 an die Variable p1 (vom Typ Person) verändert sich deren Laufzeittyp zu Mann.

Im vorangegangenen Beispiel wird ein Objekt zur Ermittlung der zugehörigen Laufzeitklasse benutzt. Ist der Klassenname bekannt, so kann durch den Methodenaufruf forName(String) das zur Klasse mit dem übergebenen Namen gehörige Class-Objekt ermittelt werden.

(1)public class RefEx2 {
(2)	public static void main(String[] args) {
(3)		try {
(4)			Class c1 = Class.forName("Person");
(5)			System.out.println("Name of class:"+c1.getName() );
(6)		} catch (ClassNotFoundException cnfe) {
(7)			System.out.println("cannot find class\n"+cnfe.toString()+"\n"+cnfe.getMessage() );
(8)		} //catch
(9)	} //main()
(10)} //class RefEx2
(11)
(12)class Person {
(13)} //class Person

Beispiel 81: Ermittlung eines Class-Objekts über den Klassennamen   RefEx2.java

Bildschirmausgabe:

$java RefEx2
Name of class:Person

Die Klasse Person wird geladen, und per getName() ihr Name -- erwartungsgemäß Person -- ausgegeben.
Kann die Klasse nicht gefunden werden, so wird ein ClassNotFoundException Ausnahmeereignis-Objekt erzeugt.

Um unnötigen Tippaufwand zu sparen exisistiert mit T.class eine abkürzende Schreibweise für alle Javatypen T aus dem Typsystem.
Beispiel:

Class c1 = Person.class;
Class c2 = int.class;
Class c3 = Double[].class;

Hinweis: Aus historischen Gründen liefert die Methode getName() für alle Array-Typen eine etwas unschöne abkürzende Syntax, die sich am im Class-File verwendeten Format orientiert:
Zunächst das Präfix [ für jede Array-Schachtelung, gefolgt von ...
B für byte
C für char
D für double
F für float
I für int
J für long
Lclassname; für eine Klasse oder Schnittstelle
S für short
Z für boolean.

so liefert (new int[3][4][5][6][7][8][9]).getClass().getName() als Namen die Zeichenkette [[[[[[[I zurück.

Die Erzeugung neuer Objekte aus einem existierenden Class-Objekt erfolgt durch die Methode newInstance(). Sie ruft intern den parameterlosen Vorgabekonstruktor der durch das Class-Objekt vertretenen Klasse auf.
Die newInstance-Methode unterstützt keine Aufrufe parametrisierter Konstruktoren. Für diesen Anwendungsfall sollte auf die Klasse Constructor der (moderneren) Reflection API zurückgegriffen werden.

(1)public class RefEx3 {
(2)	public static void main(String[] args) {
(3)		Person p1 = new Person(); //normal object creation
(4)		p1.sayHello();
(5)
(6)		try {
(7)			Person p2 = (Person) Class.forName("Person").newInstance();
(8)			p2.sayHello();
(9)		} catch (ClassNotFoundException cnfe) {
(10)			System.out.println("cannot find class\n"+cnfe.toString()+"\n"+cnfe.getMessage() );
(11)		} catch (InstantiationException ie) {
(12)			System.out.println("an InstantiationException occured\n"+ie.toString()+"\n"+ie.getMessage() );
(13)		} catch (IllegalAccessException iae) {
(14)			System.out.println("an IllegalAccessException occured\n"+iae.toString()+"\n"+iae.getMessage() );
(15)		} //catch
(16)	} //main()
(17)} //class RefEx3
(18)
(19)class Person {
(20)	public void sayHello() {
(21)		System.out.println("hello from person!");
(22)	} //sayHello()
(23)} //class Person

Beispiel 82: Erzeugung eines neuen Objekts mit der Methode newInstance   RefEx.java

Das Programm gibt zweimal die Zeichenkette hello from person! am Bildschirm aus. Die Erzeugung des zweiten Personen-Objektes (p2) erfolgt durch die Kopplung der Methoden forName und newInstance. Letztere liefert eine Ausprägung der Klasse Object zurück, weshalb -- um die Methode sayHello der davon abgeleiteten Klasse Person aufrufen zu können -- die explizite Typumwandlung erfolgen muß.

Ferner stellt die Klasse Class weitere Methoden zur Untersuchung der Charakteristika definierter Klassen bereit:

Das Reflection-Paket

Wie bereits angedeutet wurde die Klasse Class mit Einführung der Reflection API in der Java Version 1.1 stark erweitert. Sie schlägt -- über die Rückgabetypen der neu geschaffenen Methoden -- eine Brücke zu den Klassen des Reflection-Paketes java.lang.reflect.

Ausschnitt aus dem Reflection-Paket und Abhängigkeiten zu anderen Klassen

Die Reflection-API ist als Subpaket von java.lang organisiert. Ihre Klassen dienen lediglich zur Inspektion existierender Klassen einer Java-Laufzeit-Umgebung. Daher sind die Konstruktoren der von AccessibleObject abgeleiteten Klassen, sowie der der Klasse Array, als private deklariert; Ausprägungen dieser Klassen können ausschließlich durch die virtuelle Maschine erzeugt werden.

Im Folgenden werden zunächst die Methoden zur Ermittlung verschiedenster Informationen am konkreten Codebeispiel vorgestellt.
Die verschiedenen Methoden zur Einflußnahme auf bereits erzeugte Metainformationsobjekte werden im Anschluß diskutiert.

Ermittlung Klassen- und Schnittstellen-bezogener Information

Klassen und Schnittstellen werden durch die Reflection-API gleichbehandelt. Die Unterscheidung kann durch Methode isInterface auf jedem Class-Objekt getroffen werden.
Ferner stehen zur Verfügung:

Ermittlung Attribut-bezogner Informationen

Der Zugriff auf Attribute und Attribut-spezifische Informationen -- in der Java-Terminologie Felder genannt -- (wie Namen, Sichtbarkeitsbereich und Typ) wird durch die Methoden getField(String), für ein namentlich bekanntes Feld, und getFields() für alle Attribute einer Klasse, realisiert. In beiden Fällen ist wird genau ein, bzw. ein Array von Objekten der Reflection-Klasse Field zurückgeliefert.
Beide Methoden berücksichtigen dabei alle Attribute/Felder einer Klasse, einschließlich der von der Superklasse und deren Superklassen ererbten. Zur Ermittlung der direkt auf dem betrachteten Class-Objekt definierten Attribute können Methoden getDelcaredField(String) bzw. getDelcaredFields() eingesetzt werden.
Verwendung im Beispiel

Field-Objekte bieten Zugriff auf die verschiedenen deklarativen Charakteristika eines Attributs:

Ermittlung Operationen-bezogener Information

Anmerkung: Zwar spricht die Java-Reflection-API hier konsequent von Methoden, bietet jedoch keine Zugriffsmöglichkeiten auf den Implementierungskörper einer Operation. Ermittelbar sind daher nur die Signatur, die Sichtbarkeitsmodifier sowie der Rückgabetyp und, falls existent, im Ausführungsverlauf ausgelößte Ausnahmeereignisse; mithin: die Operation.

Die Reflection-API trennt Konstruktoren und sonstige Operationen voneinander. Daher existieren mit getConstructor(Class[]) und getConstructors() bzw. getMethod(String, Class[]) und getMethods() jeweils eigene Methoden zu ihrer Ermittlung.
Zurückgeliefert wird jedoch in allen Fällen genau ein, bzw. ein Array von, Method-Objekt(en).

(1)import java.lang.reflect.Constructor;
(2)import java.lang.reflect.Method;
(3)import java.lang.reflect.Modifier;
(4)import java.lang.reflect.Member;
(5)import java.lang.reflect.Field;
(6)import java.lang.ClassNotFoundException;
(7)
(8)public class ShowClass {
(9)	public static void main(String[] args) {
(10)   	try {
(11)   		printClass( Class.forName(args[0]) );
(12)   	} catch (ClassNotFoundException cnfe) {
(13)   		System.out.println("cannot find class "+args[0]);
(14)   	} //catch
(15)  	} //main()
(16)// ----------------------------------------------------------------------------
(17)	public static void printClass(Class c) {
(18)   	Package pck = c.getPackage();
(19)   	if ( pck != null )
(20)   		System.out.println("package "+pck.getName()+";" );
(21)
(22)   	if (c.isInterface()) {
(23)	   	System.out.print(Modifier.toString(c.getModifiers()) + " "+ c.getName());
(24)    	} 	else
(25)      	System.out.print(Modifier.toString(c.getModifiers()) + " class " +
(26)                       c.getName() +
(27)                       " extends " + c.getSuperclass().getName());
(28)
(29)    	Class[] interfaces = c.getInterfaces();
(30)    	if ((interfaces != null) && (interfaces.length > 0)) {
(31)      	if (c.isInterface())
(32)      		System.out.println(" extends ");
(33)      	else
(34)      		System.out.print(" implements ");
(35)      	for(int i = 0; i < interfaces.length; i++) {
(36)        		if (i > 0)
(37)        			System.out.print(", ");
(38)        		System.out.print(interfaces[i].getName());
(39)      	} //for
(40)    	} //if
(41)    	System.out.println("\n{");
(42)
(43)    	Constructor[] constructors = c.getDeclaredConstructors();
(44)		if ( constructors.length > 0 ) {
(45)    		System.out.println("   //Constructors");
(46)    		for(int i = 0; i < constructors.length; i++)
(47)      		printOperation(constructors[i]);
(48)		} //if
(49)		constructors = null;
(50)
(51)		Class[] classes = c.getDeclaredClasses();
(52)		if (classes.length > 0) {
(53)    		System.out.println("   //Inner Classes");
(54)			for (int i=0; i < classes.length; i++) {
(55)				if (classes[i].isInterface() == false ) {
(56)					printClass(classes[i] );
(57)					System.out.print("\n");
(58)				} //if
(59)			} //for
(60)    		System.out.println("   //Inner Interfaces");
(61)			for (int i=0; i < classes.length; i++) {
(62)				if (classes[i].isInterface() == true ) {
(63)					printClass(classes[i] );
(64)					System.out.print("\n");
(65)				} //if
(66)			} //for
(67)		} //if
(68)		classes = null;
(69)
(70)    	Field[] fields = c.getDeclaredFields();
(71)    	if ( fields.length > 0) {
(72)    		System.out.println("   //Attributes");
(73)    		for(int i = 0; i < fields.length; i++)
(74)      		printAttribute(fields[i]);
(75)		} //if
(76)		fields = null;
(77)
(78)
(79)    	Method[] operations = c.getDeclaredMethods();
(80)    	if ( operations.length > 0 ) {
(81)	  		System.out.println("   //Operations");
(82)	    	for(int i = 0; i < operations.length; i++)
(83)	      	printOperation(operations[i]);
(84)  		} //if
(85)  		operations = null;
(86)
(87)    	System.out.print("} //end ");
(88)    	if ( c.isInterface() )
(89)    		System.out.print("interface ");
(90)    	else
(91)    		System.out.print("class ");
(92)    	System.out.print( c.getName() );
(93)  	} //printClass(Class)
(94)// ----------------------------------------------------------------------------
(95)	public static String typename(Class t) {
(96)   	String brackets = "";
(97)    	while(t.isArray()) {
(98)      	brackets += "[]";
(99)      	t = t.getComponentType();
(100)    	} //while
(101) 	   return t.getName() + brackets;
(102)  	} //typename(Class)
(103)// ----------------------------------------------------------------------------
(104)  	public static String modifiers(int m) {
(105)		if (m == 0)
(106)	  		return "";
(107)	   else
(108)	   	return Modifier.toString(m) + " ";
(109)  	} //modifiers(int)
(110)// ----------------------------------------------------------------------------
(111)  	public static void printAttribute(Field f) {
(112)  		System.out.print("  " + modifiers(f.getModifiers()) + typename(f.getType()) + " " + f.getName() + ";     //"+f.getType().getName()+" is ");
(113)
(114)		if ( (f.getType()).isInterface() )
(115)			System.out.println("an interface");
(116)		else {
(117)			if ( f.getType().isPrimitive() )
(118)				System.out.println("a primitive type");
(119)			else {
(120)				if ( f.getType().isArray() )
(121)					System.out.println("an array");
(122)				else
(123)					System.out.println("a class");
(124)			} //else
(125)		} //else
(126)	} //printAttribute(Field)
(127)// ----------------------------------------------------------------------------
(128)	public static void printOperation(Member member) {
(129)  		Class returntype=null, parameters[], exceptions[];
(130)
(131)	   if (member instanceof Method) {
(132)	   	Method m = (Method) member;
(133)	      returntype = m.getReturnType();
(134)	      parameters = m.getParameterTypes();
(135)	      exceptions = m.getExceptionTypes();
(136)	   } else {
(137)	   	Constructor c = (Constructor) member;
(138)	      parameters = c.getParameterTypes();
(139)	      exceptions = c.getExceptionTypes();
(140)	   } //else
(141)
(142)	   System.out.print("  " + modifiers(member.getModifiers()) + ((returntype!=null)? typename(returntype)+" " : "") + member.getName() + "(");
(143)	   for(int i = 0; i < parameters.length; i++) {
(144)	   	if (i > 0)
(145)	   		System.out.print(", ");
(146)
(147)	      System.out.print(typename(parameters[i]));
(148)	   } //for
(149)
(150)	   System.out.print(")");
(151)
(152)	   if (exceptions.length > 0)
(153)	   	System.out.print(" throws ");
(154)
(155)	   for(int i = 0; i < exceptions.length; i++) {
(156)	   	if (i > 0)
(157)	   		System.out.print(", ");
(158)
(159)	      System.out.print(typename(exceptions[i]));
(160)	   } //for
(161)
(162)	   System.out.println(";");
(163)	} //printOperation(Member)
(164)} //class ShowClass
(165)
(166)// --------------------------------------------
(167)interface TestInterface {
(168)} //interface TestInterface
(169)
(170)class TestClass {
(171)	public 		TestInterface  i1;
(172)	private 		TestClass  c1;
(173)	protected  	int p1;
(174)	int[]			ta1;
(175)	int[][] 		tam1;
(176)
(177)	class Inner {
(178)		int i;
(179)		class InnerInner {
(180)			int j;
(181)		} //class InnerInner
(182)	} //class Inner
(183)	interface InnerInterface {
(184)	} //interface InnerInterface
(185)
(186)	private TestClass testc() {
(187)		return new TestClass() {
(188)			public void hello() {
(189)				System.out.println("overridden test!");
(190)			} //hello()
(191)		}; //class TestClass
(192)	} //test()
(193)} //class TestClass

Beispiel 83: Erfragen verschiedener Klassen-spezifischer Informationen durch die Reflection-API   ShowClass.java

Bildschirmausgabe:

 class TestClass extends java.lang.Object
{
   //Constructors
  TestClass();
   //Inner Classes
 class TestClass$Inner extends java.lang.Object
{
   //Constructors
  TestClass$Inner(TestClass);
   //Inner Classes
 class TestClass$Inner$InnerInner extends java.lang.Object
{
   //Constructors
  TestClass$Inner$InnerInner(TestClass$Inner);
   //Attributes
  int j;     //int is a primitive type
  final TestClass$Inner this$1;     //TestClass$Inner is a class
} //end class TestClass$Inner$InnerInner
   //Inner Interfaces
   //Attributes
  int i;     //int is a primitive type
  final TestClass this$0;     //TestClass is a class
} //end class TestClass$Inner
   //Inner Interfaces
abstract static interface TestClass$InnerInterface
{
} //end interface TestClass$InnerInterface
   //Attributes
  public TestInterface i1;     //TestInterface is an interface
  private TestClass c1;     //TestClass is a class
  protected int p1;     //int is a primitive type
  int[] ta1;     //[I is an array
  int[][] tam1;     //[[I is an array
   //Operations
  private TestClass testc();
} //end class TestClass

Das Ausgabe enthält, erwartungsgemäß, die implizite Superklasse java.lang.Object, welche durch den Übersetzer automatisch gesetzt wird, als explizite Angabe nach dem extends-Schlüsselwort.
Die inneren Klassen erscheinen in ihrer JVM-internen Namensgebung mit dem trennenden Dollarsymbol $. Auffallend ist die explizite Wiedergabe der this-Variable und des automatisch generierten Konstruktors mit dem Typ der äußeren Klasse als Übergabeparameter.
Für Array-Typen erfolgt die Benennung ebenfalls gemäß der JVM-internen Konvention.

Erzeugung neuer Objekte und Modifikationen bestehender mit der Reflection API

Neben den bisher vorgestellten Mechanismen zur Introspektion bestehender Strukturen kann die Reflection API auch zur Erzeugung neuer Objekte und Operation auf diesen eingesetzt werden. Hierfür stehen neben Methoden zum lesenden und schreibenden Zugriff auf Attribute auch Möglichkeiten zur Ausführung von Methoden auf den Objekten zur Verfügung.

Erzeugung von Objekten

Die Erzeugung neuer Objekte aus bestehenden Class-Ausprägungen war bereits mit Methoden der Class-Klasse möglich, sofern zur Objekterzeugung der parameterlose Standardkonstruktor verwendet wird.
Zur Instanziierung neuer Objekte unter Nutzung eines bestehenden parametrisierten Konstruktors ist die Verwendung der Reflection-Klasse Constructor unabdingbar.

Der Aufruf eines parametrisierten Konstruktors vollzieht sich in drei Schritten:

  1. Ermittlung des Class-Objekts für die neu zu erzeugende Instanz
  2. Erzeugung eines neuen Constructor-Objekts mit der getConstructor(Class[])-Methode der Klasse Class
  3. Objekterzeugung durch die newInstance(Object[])-Methode der Constructor-Klasse
(1)import java.lang.reflect.Constructor;
(2)import java.lang.reflect.InvocationTargetException;
(3)import java.awt.Rectangle;
(4)
(5)class SampleInstance {
(6)	public static void main(String[] args) {
(7)      Rectangle rectangle;
(8)      Class rectangleDefinition;
(9)      Class[] intArgsClass = new Class[] {int.class, int.class};
(10)      Integer height = new Integer(12);
(11)      Integer width = new Integer(34);
(12)      Object[] intArgs = new Object[] {height, width};
(13)      Constructor intArgsConstructor;
(14)
(15)      try {
(16)			rectangleDefinition = Class.forName("java.awt.Rectangle");
(17)        	intArgsConstructor = rectangleDefinition.getConstructor(intArgsClass);
(18)
(19)	      System.out.println ("Constructor: " + intArgsConstructor.toString());
(20)   	   Object object = null;
(21)
(22)			object = intArgsConstructor.newInstance(intArgs);
(23)        	System.out.println ("Object: " + object.toString());
(24)      	rectangle = (Rectangle) object;
(25)      } catch (ClassNotFoundException e) {
(26)          System.out.println("A ClassNotFoundException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(27)      } catch (NoSuchMethodException e) {
(28)          System.out.println("A NoSuchMethodException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(29)      } catch (InstantiationException e) {
(30)      	System.out.println("An InstantiationException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(31)      } catch (IllegalAccessException e) {
(32)      	System.out.println("An IllegalAccessException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(33)      } catch (IllegalArgumentException e) {
(34)      	System.out.println("An IllegalArgumentException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(35)      } catch (InvocationTargetException e) {
(36)        	System.out.println("An InvocationTargetException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(37)    	} //catch
(38)   } //main()
(39)} //class SampleInstance

Beispiel 84: Erzeugung eines AWT-Rechtecks über einen parametrisierten Konstruktor mit der Reflection-API   SampleInstance.java

Bildschirmausgabe:

Constructor: public java.awt.Rectangle(int,int)
Object: java.awt.Rectangle[x=0,y=0,width=12,height=34]

Das Beispiel spiegelt die drei Erstellungsschritte wieder:
Zunächst wird über den Namen (Aufruf: forName(...)) das zur Klasse gehörige Class-Objekt erfragt.
Die Methode getConstructor(...) liefert das Konstruktorenobjekt welches die als Parameter übergebene Signatur aufweist.
Instanziiert wird das neue Objekt durch die Methode newInstance, ausgeführt auf dem Constructor-Objekt mit den definierten Übergabeparametern.

Attributzugriff

... geschieht über Objekte der Klasse Field.
Für alle Primitivtypen existieren lesende Methoden, die einheitlich mit getT(Object) signiert sind. T ist einer der Primitivtypen ist. Der Rückgabetyp ist jeweils T.
Werte von objektartigen Attributen können mittels get(Object) erfragt werden; zurückgegeben wird hier eine Ausprägung von Object.

Symmetrisch sind die schreiben Zugriffe als setT(Object, T) realisiert. Mit set(Object, Object) steht eine Methode zur Ablage objektartiger Attribute zur Verfügung.

Das nachfolgende Beispiel zeigt die Verwendung der Lese- und Schreibmethoden.

(1)import java.lang.reflect.Field;
(2)
(3)public class AttributeAccess {
(4)	public static void main(String[] args) {
(5)		Test o1 = new Test();
(6)		o1.setI(42);
(7)
(8)		System.out.println( "i="+o1.getI() );
(9)
(10)		try {
(11)			Field iAttribute = (o1.getClass()).getField("i");
(12)			System.out.println( "i="+iAttribute.getInt(o1) );
(13)
(14)			iAttribute.setInt(o1, 50 );
(15)			System.out.println( "i="+iAttribute.getInt(o1) );
(16)		} catch (NoSuchFieldException e) {
(17)			System.out.println("A NoSuchFieldException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(18)			e.printStackTrace();
(19)		} catch (IllegalAccessException e) {
(20)			System.out.println("A IllegalAccessException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(21)			e.printStackTrace();
(22)		} //catch
(23)	} //main()
(24)} //class AttributeAccess
(25)
(26)class Test {
(27)	public int i;
(28)
(29)	public void setI(int i) {
(30)		this.i = i;
(31)	} //setI()
(32)
(33)	public int getI() {
(34)		return i;
(35)	} //getI()
(36)} //class Test

Beispiel 85: Attributzugriff mit Methoden der Reflection-Klasse Field   AttributeAccess.java

Bildschirmausgabe:

i=42
i=42
i=50

Ausführen von Methoden

Die Klasse Method weist neben den bisher vorgestellten Funktionalitäten zur Inspektion auch mit der Methode invoke(Object, Object[]) einen generischen Mechanismus zur Ausführung beliebiger Methoden auf.

Das folgende Beispiel zeigt den Aufruf der überladenen Methode hello über die invoke-Methode der Reflection-API.

(1)import java.lang.reflect.Method;
(2)import java.lang.reflect.InvocationTargetException;
(3)
(4)public class MethodExecution {
(5)	public static void main(String[] args) {
(6)		test o1 = new test();
(7)
(8)		try {
(9)			Method helloMethod = (o1.getClass()).getMethod("hello", null);
(10)			helloMethod.invoke(o1, null);
(11)
(12)			Class[] formalParameters = { String.class };
(13)			helloMethod = (o1.getClass()).getMethod("hello", formalParameters );
(14)			Object[] argumentList = { "stranger" };
(15)			helloMethod.invoke(o1, argumentList);
(16)		} catch (NoSuchMethodException e) {
(17)			System.out.println("A NoSuchMethodException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(18)			e.printStackTrace();
(19)		} catch (IllegalAccessException e) 	{
(20)			System.out.println("A NoSuchMethodException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(21)			e.printStackTrace();
(22)		} catch (InvocationTargetException e) {
(23)			System.out.println("An InvocationTargetException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(24)			e.printStackTrace();
(25)		} //catch
(26)	} //main()
(27)} //class MethodExecution
(28)
(29)class Test {
(30)	public void hello() {
(31)		System.out.println("simple hello!");
(32)	} //hello()
(33)
(34)	public void hello(String name) {
(35)		System.out.println("hello "+name);
(36)	} //hello()
(37)} //class Test

Beispiel 86: Ausführung zweiter Methoden durch die Reflection-API   MethodExecution.java

Bildschirmausgabe:

simple hello!
hello stranger


Weitere Anwendundungsbeispiele

Unter Einsatz der Reflection-API lassen sich generische Programme entwickeln, die auf beliebigen Typen operieren. Mit diesem Mechanismus läßt sich ein Verhalten vergleichbar der parametrischen Polymorphie der C++-Templates nachbilden.

(1)import java.lang.reflect.Array;
(2)
(3)public class GrowableArrayTest {
(4)	public static void main(String[] args) {
(5)		int[] ia = {1};
(6)		System.out.println("ia consists of "+ia.length+" elements");
(7)		ia = (int[]) arrayGrow(ia);
(8)		System.out.println("ia consists of "+ia.length+" elements");
(9)
(10)		int[] ib = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
(11)		System.out.println("ib consists of "+ib.length+" elements");
(12)		ib = (int[]) arrayGrow(ib);
(13)		System.out.println("ib consists of "+ib.length+" elements");
(14)
(15)		Person[] pa = new Person[3];
(16)		pa[0] = new Person("hans");
(17)		pa[1] = new Person("franz");
(18)		pa[2] = new Person("fritz");
(19)		System.out.println("pa consists of "+pa.length+" elements");
(20)		pa = (Person[]) arrayGrow(pa);
(21)		System.out.println("pa consists of "+pa.length+" elements");
(22)	} //main()
(23)
(24)	static Object arrayGrow(Object oldArray) {
(25)		Class arrayClass = oldArray.getClass();
(26)		if ( !arrayClass.isArray() )
(27)			return null;
(28)
(29)		Class componentType = arrayClass.getComponentType();
(30)
(31)		int oldLength = Array.getLength(oldArray);
(32)		int newLength = ( ((oldLength * 1.1) > oldLength + 1) ? (int) (oldLength * 1.1) : (oldLength + 1) );
(33)
(34)		Object newArray = Array.newInstance(componentType, newLength );
(35)
(36)		System.arraycopy(oldArray, 0, newArray, 0, oldLength);
(37)		return newArray;
(38)	} //arrayGrow()
(39)} //class growableArrayTest
(40)
(41)class Person {
(42)	protected String name;
(43)
(44)	public Person (String name) {
(45)		this.name = name;
(46)	} //constructor
(47)} //class Person

Beispiel 87: Ein wachsender Array   GrowableArrayTest.java

Bildschirmausgabe:

ia consists of 1 elements
ia consists of 2 elements
ib consists of 20 elements
ib consists of 22 elements
pa consists of 3 elements
pa consists of 4 elements

Das Beispiel implementiert dynamische, durch expliziten Methodenaufruf, wachsende Arrays.
Nach Ermittlung des Class-Objekts (durch getClass()) und des Komponententyps des Arrays (getComponentType()) wird ein zweiter -- der neue -- Array durch die newInstance-Methode der Klasse Array erzeugt. Als Übergabeparameter wird der Komponententyp und die Länge des neuen Arrays übergeben. Die Länge des neuen Arrays entspricht der des Alten, um zehn Prozent, jedoch mindestens ein Element, erhöht.
Abschließend wird der Inhalt des alten in den neu erzeugten Array kopiert. Hierzu wird die sehr performant ausgeführte native Methode System.arraycopy eingesetzt.



Durch die Konstruktion des Method-Objekts und der invoke-Methode läßt sich auch das aus C/C++ bekannte Verhalten der Funktionszeiger nachbilden.
Das Beispiel zeigt zweimaligen Aufruf derselben Methoden printTable. Im Körper der Methode wird jeweils die als Parameter übergebene Methode ausgeführt.
Im ersten Aufruf wird das Quadrat der Zahlen von Eins bis Zehn gebildet, im Zweiten hingegen die Wurzel gezogen.

(1)import java.lang.reflect.Method;
(2)import java.lang.reflect.InvocationTargetException;
(3)
(4)public class MethodPointerTest {
(5)	public static void main(String[] args) {
(6)		try {
(7)			printTable(1,10, MethodPointerTest.class.getMethod("square", new Class[] {double.class} ));
(8)			printTable(1,10, java.lang.Math.class.getMethod("sqrt", new Class[]{ double.class} ));
(9)		} catch (NoSuchMethodException e) {
(10)			System.out.println("A NoSuchMethodException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(11)		} //catch
(12)	} //end main()
(13)
(14)	public static double square (double x) {
(15)		return (x*x);
(16)	} //square()
(17)
(18)	public static void printTable(double from, double to, Method f) {
(19)		System.out.println(f);
(20)		for (double x=from; x <= to; x++) {
(21)			System.out.print( x );
(22)			try {
(23)				Object[] args = { new Double(x) };
(24)				Double d = (Double) f.invoke(null,args);
(25)				double y = d.doubleValue();
(26)				System.out.println("  |  "+y);
(27)			} catch (InvocationTargetException e) {
(28)				System.out.println("An InvocationTargetException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(29)			} catch (IllegalAccessException e) {
(30)				System.out.println("An IllegalAccessException occurred!\n"+e.toString()+"\n"+e.getMessage() );
(31)			} //catch
(32)		} //for
(33)	} //printTable()
(34)} //class MethodPointerTest

Beispiel 88: Verweise auf Methoden in Java   MethodPointerTest.java

Bildschirmausgabe:

public static double MethodPointerTest.square(double)
1.0  |  1.0
2.0  |  4.0
3.0  |  9.0
4.0  |  16.0
5.0  |  25.0
6.0  |  36.0
7.0  |  49.0
8.0  |  64.0
9.0  |  81.0
10.0  |  100.0
public static strictfp double java.lang.Math.sqrt(double)
1.0  |  1.0
2.0  |  1.4142135623730951
3.0  |  1.7320508075688772
4.0  |  2.0
5.0  |  2.23606797749979
6.0  |  2.449489742783178
7.0  |  2.6457513110645907
8.0  |  2.8284271247461903
9.0  |  3.0
10.0  |  3.1622776601683795

back to top   3.2.7 Abstract Windowing Toolkit (AWT)

 

Das AWT stellt den wohl interessantesten Teil eines (interaktiven) Java-Programmes dar -- die Benutzerinteraktion durch eine graphische Oberfläche.
Ziel der AWT-Entwicklung war es, die verschiedenen graphischen Primitiven wie Buttons, Menüs etc. plattformunabhängig anzubieten.
Zur Realisierung des Look and Feel bieten sich vier verschiedene Ansätze an:

Das AWT bietet neben den graphischen Primitivoperationen zum Zeichnen von einfachen geometrischen Objekten wie Linien, Rechtecken, Kreisen; Fülloperationen und Methoden zur Textausgabe einen Mechanismus zur ereignisbasierten Ablaufsteuerung an, der es erlaubt auf externe Ereignisse wie Mauseingaben zu reagieren. Ferner bietet sie die weitegehend bekannten GUI-Grundelemente wie Fenster, Dialogboxen, Menüs, ... an. Zur Entwicklung komplexerer Anwendungen steht Funktionalität zur Graphikbearbeitung und Audiowiedergabe zur Verfügung.

... und SWING?

Mit der Java Version 1.1 wurde das AWT vollständig überarbeitet, insbesondere die oftmals als Achillesferse praktisch verwendbarer Applikationen empfundene Ereignisbehandlung (engl. event handling). Seither verwirklicht auch Java das bereits von anderen Oberflächensystemen wie NextStep oder Windows NT bekannte Delegation Based Event Handling. Der Terminus beschreibt die Möglichkeit der AWT externe Ereignisse (Mausclicks, Fenster-Schließen, ...) an beliebige Objekte zur Behandlung weiterzureichen.
Ebenfalls mit der JDK-Version 1.1 wurde eine zweite Bibliothek zur Oberflächenprogrammierung vorgestellt: SWING. Sie geht deutlich über den in der AWT realisierten Funktionsumfang hinaus, und wird daher heute überwiegend zur Implementierung professioneller Anwendungen herangezogen.
Vorwärtsreferenz: Sie wird in Kapitel 3.2.8 Swing beschrieben.

Struktur des AWT

Das gesamte AWT ist im Standard-API-Paket java.awt zusammengefaßt. Es stellt die grundlegenden graphischen Primitive zur Verfügung:

Als Menü-Komponenten stehen zur Verfügung:

Die dargestellten Komponenten dienen zunächst zum Aufbau der Benutzeroberfläche (buttons, Listen, etc.) oder zur Strukturierung der Fenster (scroll bars, menues, etc.). Die dritte Kategorie bilden die Zeichenbereiche.

Ausschnitt aus der AWT-Klassenhierarchie

Ausgangspunkt jeder AWT-basierten Applikation ist die -- bereits aus der Applet-Hierarchie bekannte -- Container-Struktur.
In Containern werden die verschiedenen graphischen AWT-Primitive zusammengefaßt; Container können hierarchisch strukturiert sein, d.h. weitere Container enthalten. Der oberste Container einer Applikation bildet gleichsam die sammelnde Ordnungsstruktur. Die AWT verfügt über vier vordefinierte Containertypen:

Anmerkung: Rückblickend mag es (anfänglich) verwundern, daß die Graphikwiedergabe in Applets ohne Kenntnis und Anwendung der Containerstruktur möglich war. Dies ist jedoch nur auf den ersten Blick der Fall. Zunächst stellt der Browser oder Appletviewer das Hauptfenster nebst den Menüleisten zur Verfügung -- insofern existiert ein Container zur Aufnahme des Applets bereits durch seine Ausführungsumgebung. Zusätzlich stellt das Applet selbst, durch seine Plazierung in der Vererbungshierarchie unterhalb der Klasse java.awt.Panel, einen eigenständigen Container dar.

Layout-Manager

Ein weiterer Ausweis der Zielsetzung plattformunabhängigen Designs des AWT ist die verwirklichte Philosophie zur Plazierung der graphischen Elemente am Bildschirm. Während plattformspezifische GUI-Bibliotheken zumeist die angebotenen Primitive mit absoluten Koordinaten im Anwendungsfenster verankern verfolgt Java hier einen gegensätzlichen Weg.
Durch den Programmierer wird ausschließlich die Plazierung der Komponente im Verhältnis zu anderen Komponenten angegeben, die endgültige Ausrichtung am Schirm nimmt eine zusätzliche AWT-Einheit -- der Layout-Manager -- vor. Die Orientierung der Primitiven innerhalb einer Komponente erfolgt weiterhin durch absolute Positionierung.

Im AWT stehen folgende vordefinierte Layout-Manager zur Verfügung:

Anmerkungen:

Ein erstes Fenster

(1)import java.awt.Frame;
(2)
(3)public class AWTEx1 {
(4)	public static void main(String[] args) {
(5)		Frame wnd = new Frame("Einfaches Fenster");
(6)
(7)		wnd.setSize(400,300);
(8)		wnd.setVisible(true);
(9)	} //end main()
(10)} //class AWTEx1

Beispiel 89: Ein einfaches Fenster mit AWT   AWTEx1.java

Bildschirmausgabe unter MS-Windows

Ein einfaches Fenster mit AWT

Das Beispiel erzeugt einen Frame mit dem Titel Einfaches Fenster.
Durch die, von Component ererbte, Methode setSize(int, int) wird dem leeren Frame die Größe von 400 mal 300 Pixeln zugewiesen.
Zum Abschluß muß der Anzeigevorgang per setVisible(boolean) (ebenfalls von Component ererbt) explizit angestoßen werden.

Die Beiden Methoden setSize und setVisible greifen auf die visuellen Eigenschaften der Komponente zu, diese und weitere Methoden zu Modifikation der graphischen Erscheinung stehen auf allen Subklassen von Component zur Verfügung.

Auf Ausprägungen der Klasse Frame sind ferner verfügbar:

Bei der Ausführung fällt zunächst die Existenz der plattformüblichen Standardmenüs auf. Sie werden automatisch durch die Ausführungsumgebung zur Verfügung gestellt und mit Funktionalität versehen.
Jedoch zeigt sich, daß es nicht möglich ist das Fenster mit den üblichen Betriebssystem-spezifischen Methoden (Close-Button, Auswahl des entsprechenden Menüs, Tastenkombination, etc.) zu schließen. Erst der Abbruch des Prozesses der virtuellen Maschine terminiert die Applikation.
Denn anders als die üblichen Vorgabeaktionen ist das hierfür erforderliche Applikationsverhalten durch den Anwender selbst bereitzustellen.

Ereignisbehandlung

Im Allgemeinen erfolgt die Kommunikation zwischen einer graphischen Oberfläche und dem ausführenden Betriebsystem in Form von Nachrichten, die zwischen diesen beiden Kommunikationspartnern ausgetauscht werden.
Das Betriebssystem unterrichtet die Applikation über den Eintritt bestimmter Ereignisse (engl. event), hierzu zählen u.a. Mausbewegungen und -klicks, Tastaturanschläge sowie alle Fensteroperationen.

Am Nachrichtenverkehr sind prinzipiell drei Objekte des Systems beteiligt: Die Ereignisquelle, das Objekt bei dem das Ereignis eintrat (z.B. im Falle eines Mausklicks: der entsprechende Button); der Ereignisempfänger, ein Objekt das auf das eingetretene Ereignis reagiert; die Nachricht selbst, in objektorientierten System als eigenständiges Objekt realisiert, welches das ausgelöste Ereignis näher beschreibt.
Dieser Kommunikationsmechanismus ist als Delegation Based Event Handling bekannt, da jedes Ereignis an eine konkrete Instanz delegiert wird, die nachfolgend die Behandlung übernimmt. Es bietet zwei entscheidende Vorteile:

Hierarchie der AWT-Ereignisse

Die Abbilung zeigt eine Auswahl der verschiedenen Ereignistypen. Wie dargestellt sind alle spezifischen Ereignisse von der Klasse EventObject abgeleitet. Neben den AWT-Ereignissen existiert eine Fülle weiterer Ereignistypen, die in den verschiedenen spezifischen Paketen (wie java.beans, javax.sound, etc.) untergebracht sind.
Durch die Superklasse EventObject wird auch die an alle Unterklassen vererbte Methode getSource() definiert, welche zu jeder EventObject-Ausprägung die auslösende Instanz liefert.
Unterhalb der Klasse ComponentEvent sind die low-level-Ereignisse eingeordnet, die auf der Ebene einer visuellen Komponente aufteten können.
Die Ereignisse ActionEvent, ItemEvent und TextEvent werden als semantische Ereignisse bezeichnet, da sie nicht an ein konkretes visuelles Element gebunden sind.

Die Event-Listener-Schnittstellen und die zugehörigen Adapter-Klassen

Zur Verarbeitung der verschiedenen Nachrichtentypen muß der Empfänger eine Reihe von Methoden implementieren, die durch entsprechende Schnittstellen definiert sind. Die Abbildung stellt eine Auswahl der angebotenen Schnittstellen nebst den bereits angebotenen Implementierungen dar.

Schablone zur Reaktion auf AWT-Ereignisse

Die Reaktion auf AWT-Ereignisse wird durch sog. event handler übernommen; diese Objekte müssen die Benachrichtigung im Falle des Auftretens eines Ereignisses explizit anmelden.
Die Schritte im Einzelnen:

  1. Definition einer Event-Handler-Klasse durch Implementierung der entsprechenden Schnittstelle
  2. Registrierung des Event-Handlers

Somit kann auch das vorangegangene Beispiel entsprechend erweitert werden, um die noch ausstehende Reaktion auf das Beenden-Ereignis zu verwirklichen.

(1)import java.awt.Frame;
(2)import java.awt.Window;
(3)import java.awt.event.WindowAdapter;
(4)import java.awt.event.WindowEvent;
(5)
(6)public class AWTEx2 {
(7)	public static void main(String[] args) {
(8)		Frame wnd = new Frame("Einfaches Fenster");
(9)		wnd.addWindowListener( new WindowClosingAdapter() );
(10)		wnd.setSize(400,300);
(11)		wnd.setVisible(true);
(12)	} //main()
(13)} //class AWTEx2
(14)
(15)class WindowClosingAdapter extends WindowAdapter {
(16)	public void windowClosing( WindowEvent we) {
(17)		Window wnd = we.getWindow();
(18)
(19)		wnd.setVisible(false);
(20)		wnd.dispose();
(21)		System.exit(0);
(22)	} //windowClosing()
(23)} //class WindowClosingAdapter

Beispiel 90: Ein einfaches Fenster mit AWT   AWTEx2.java

Die Applikation erweitert das bisherige Beispiel um eine Adapter-Klasse, welche auf Fensterereignisse reagiert. Hierzu wird eine Ausprägung der Adapter-Klasse WindowClosingAdapter als Window Listener mit der Methode addWindowListener(WindowListener) registriert.
Die Adapter-Klasse erweitert die bestehende API-Klasse WindowAdapter und implementiert damit die Schnittstelle WindowListener. Sie definiert die überschriebene Methode windowClosing(WindowEvent), die bei auftreten des Ereignisses automatisch zur Verarbeitung aufgerufen wird.
Nach Ermittlung des Ereingis-sendenden Fensters per getWindow() wird dieses Fenster zunächst ausgeblendet (setVisible(false)) und im Anschluß zerstört (dispose()).
Danach terminiert die Applikation die virtuelle Maschine.

(1)import java.awt.Button;
(2)import java.awt.FlowLayout;
(3)import java.awt.Frame;
(4)import java.awt.Label;
(5)import java.awt.Window;
(6)import java.awt.event.MouseAdapter;
(7)import java.awt.event.MouseEvent;
(8)import java.awt.event.MouseEvent;
(9)import java.awt.event.MouseMotionAdapter;
(10)import java.awt.event.WindowAdapter;
(11)import java.awt.event.WindowEvent;
(12)
(13)public class AWTEx3 {
(14)	private Label mousePos,
(15)					  counterVal;
(16)
(17)	public static void main(String[] args) {
(18)		AWTEx3 app = new AWTEx3();
(19)	} //main()
(20)
(21)	public AWTEx3() {
(22)		Frame wnd = new Frame("Einfaches Fenster");
(23)		mousePos = new Label("mouse pos");
(24)		counterVal = new Label("0");
(25)		Button counter = new Button("add one!");
(26)
(27)		wnd.setSize(200,100);
(28)		wnd.setLayout(new FlowLayout() );
(29)		wnd.add (mousePos);
(30)		wnd.add(counter);
(31)		wnd.add(counterVal);
(32)
(33)		wnd.addWindowListener( new WindowClosingAdapter() );
(34)		wnd.addMouseMotionListener( new MyMouseMotionAdapter(this) );
(35)		counter.addMouseListener( new CounterClicked(this) );
(36)		wnd.addMouseListener( new WindowClicked(this) );
(37)
(38)		wnd.setVisible(true);
(39)	} //constructor
(40)
(41)	public void setMousePos(String newText) {
(42)		mousePos.setText( newText );
(43)	} //setMousePos()
(44)
(45)	public void incrementCounter() {
(46)		counterVal.setText( ""+(Integer.parseInt(counterVal.getText())+1) );
(47)	} //incrementCounter()
(48)	public void decrementCounter() {
(49)		counterVal.setText( ""+(Integer.parseInt(counterVal.getText())-1) );
(50)	} //decrementCounter()
(51)} //class AWTEx3
(52)
(53)class WindowClosingAdapter extends WindowAdapter {
(54)	public void windowClosing(WindowEvent we) {
(55)		Window wnd = we.getWindow();
(56)
(57)		wnd.setVisible(false);
(58)		wnd.dispose();
(59)		System.exit(0);
(60)	} //windowClosing()
(61)} //class WindowClosingAdapter
(62)
(63)class CounterClicked extends MouseAdapter {
(64)	private AWTEx3 app;
(65)
(66)	public CounterClicked(AWTEx3 app) {
(67)		this.app = app;
(68)	} //constructor
(69)
(70)	public void mouseClicked( MouseEvent me) {
(71)		app.incrementCounter();
(72)	} //mouseClicked()
(73)} //class CounterClicked
(74)
(75)class WindowClicked extends MouseAdapter {
(76)	private AWTEx3 app;
(77)
(78)	public WindowClicked(AWTEx3 app) {
(79)		this.app = app;
(80)	} //constructor
(81)
(82)	public void mouseClicked( MouseEvent me) {
(83)		app.decrementCounter();
(84)	} //mouseClicked()
(85)} //class WindowClicked
(86)
(87)class MyMouseMotionAdapter extends MouseMotionAdapter {
(88)	private AWTEx3 app;
(89)
(90)	public MyMouseMotionAdapter(AWTEx3 app) {
(91)		this.app = app;
(92)	} //constructor
(93)
(94)	public void mouseMoved( MouseEvent me) {
(95)		app.setMousePos( "("+me.getX()+","+me.getY()+")" );
(96)	} //mouseMoved()
(97)} //class myMouseAdapter

Beispiel 91: Event-Handling   AWTEx3.java

Bildschirmausgabe

Zwischenzustand der Applikation

Die Applikation definiert, unter Verwendung des Flow-Layouts, zwei Beschriftungsfelder (labels) mousePos und counterVal sowie eine mit add one! beschriftete Schaltfläche.
Anmerkung: Die Verwendung des Layoutmanagers ist zur Verarbeitung der Maus-Events zwingend notwendig!
Vor Anzeige des Applikationsfensters werden vier Ereignis-Handler registriert: Der bekannte WindowClosingListener zur Umsetzung des Schließen-Ereignisses, der MouseMotionListener zur Verfolgung der Mausbewegungen im Hautfenster sowie zwei MouseListener die auf Mausereignisse -- außer Bewegungen -- auf der definierten Schaltfläche und dem Haupfenster selbst reagieren.
Die Umsetzung des WindowClosingListeners ist gegenüber dem vorhergehenden Beispiel unverändert.
Innerhalb der Klassen counterClicked und windowClicked wird jeweils die Methode mouseClicked der Schnittstelle MouseListener überschrieben, sie wurde von der Superklasse MouseAdapter geerbt. Wird ein Mausclick auf den Button registriert, so wird eine Methode der Hautptklasse awtEx3 aufgerufen, welche die Beschriftung des Textfeldes entsprechend verändert. Beim Mausklick ist es in der vorliegenden Implementierung unentscheident, mit welcher Maustaste dieser erfolgte. Eine nähere Analyse, beispielsweise hinsichtlich der gedrückten Taste, des aufgetretenen Ereignisses kann über Attribute der Klasse MouseEvent erfolgen.
Die Spezialisierung der Klasse MouseMotionAdapter realisiert die Methode mouseMoved(MouseEvent). Sie wird jeweils bei Änderung der Mauskoordinate aufgerufen. Die beiden Koordinaten werden innerhalb des Methodenkörpers mittels der angebotenen API-Methoden extrahiert und an die Hauptklasse zur Anzeige übergeben.

Event Handling und anonyme innere Klassen

Bei näherer Betrachtung der verschiedenen Event-Handler des vorhergehenden Beispiels fällt deren immergleiche Grundstruktur auf: Zunächst das Erben von einer passenden Adapterklasse um eine oder mehrere Methoden zu überschreiben. Im konkreten Beispiel wird sogar zweimal dieselbe Adapterklasse überschrieben; jedoch mit unterschiedlichem Verhalten.
Die durch die Spezialisierung entstehenden Klassen werden in allen Fällen zur Erzeugung genau eines Objekts -- des jeweiligen Listeners -- herangezogen; eine sonstige Weiter- oder Wiederverwendung in der Applikation liegt nicht vor, und ist unter Berücksichtung des Klassendesigns auch für die Zukunft nicht anzunehmen.

Nimmt man die beiden Randbedingungen -- Vererbung und Überschreiben ererbter Methoden und keine Wiederverwendung -- zusammen, so offenbart sich das Sprachmittel der anonymen inneren Klassen als wesentlich adäquater zur Lösung der vorliegenden Problemstellung.

Unter Nutzung dieses Mechanismus ergibt läßt sich der Code des Beispiels wie folgt modifizieren:

(1)import java.awt.Button;
(2)import java.awt.FlowLayout;
(3)import java.awt.Frame;
(4)import java.awt.Label;
(5)import java.awt.Window;
(6)import java.awt.event.MouseAdapter;
(7)import java.awt.event.MouseEvent;
(8)import java.awt.event.MouseMotionAdapter;
(9)import java.awt.event.WindowAdapter;
(10)import java.awt.event.WindowEvent;
(11)
(12)public class AWTEx3i {
(13)	private Label mousePos,
(14)					  counterVal;
(15)
(16)	private static AWTEx3i app;
(17)
(18)	public static void main(String[] args) {
(19)		app = new AWTEx3i();
(20)	} //main()
(21)
(22)	public AWTEx3i() {
(23)		Frame wnd = new Frame("Einfaches Fenster");
(24)		mousePos = new Label("mouse pos");
(25)		counterVal = new Label("0");
(26)		Button counter = new Button("add one!");
(27)
(28)		wnd.setSize(200,100);
(29)		wnd.setLayout(new FlowLayout() );
(30)		wnd.add (mousePos);
(31)		wnd.add(counter);
(32)		wnd.add(counterVal);
(33)
(34)		wnd.addWindowListener( new WindowAdapter() {
(35)			public void windowClosing(WindowEvent we) {
(36)				Window wnd = we.getWindow();
(37)
(38)				wnd.setVisible(false);
(39)				wnd.dispose();
(40)				System.exit(0);
(41)			} //windowClosing()
(42)		}//anonymous inner class
(43)		);
(44)
(45)		wnd.addMouseMotionListener( new MouseMotionAdapter() {
(46)			public void mouseMoved( MouseEvent me) {
(47)				app.setMousePos( "("+me.getX()+","+me.getY()+")" );
(48)			} //mouseMoved()
(49)		} //anonymous inner class
(50)		);
(51)
(52)		counter.addMouseListener( new MouseAdapter() {
(53)			public void mouseClicked( MouseEvent me) 	{
(54)				app.incrementCounter();
(55)			} //mouseClicked()
(56)		} //anonymous inner class
(57)		);
(58)
(59)		wnd.addMouseListener( new MouseAdapter() {
(60)			public void mouseClicked( MouseEvent me) {
(61)				app.decrementCounter();
(62)			} //mouseClicked()
(63)		} //anonymous inner class
(64)		);
(65)
(66)		wnd.setVisible(true);
(67)	} //constructor
(68)
(69)	public void setMousePos(String newText) {
(70)		mousePos.setText( newText );
(71)	} //setMousePos()
(72)
(73)	public void incrementCounter() {
(74)		counterVal.setText( ""+(Integer.parseInt(counterVal.getText())+1) );
(75)	} //incrementCounter()
(76)
(77)	public void decrementCounter() {
(78)		counterVal.setText( ""+(Integer.parseInt(counterVal.getText())-1) );
(79)	} //incrementCounter()
(80)} //class AWTEx3i

Beispiel 92: Das Beispiel AWTEx3 unter Nutzung anonymer innerer Klassen   AWTEx3i.java

Der entstehende Code wird deutlich kompakter, und lokalisiert zusätzlich die Handlermethoden in den Registrierungsaufrufen.
Da anonyme innere Klassen (naturgemäß) keine Konstruktoren besitzen können wurden die Methoden entsprechend modifizert.

Event-Handling im Überblick

Schnittstelle
Methoden
Parameter/Zugriffsmethoden
Eventauslöser

Menüs

Das AWT bietet die auch von anderen graphischen Benutzerschnittstellen bekannten Menüprimitive und -Grundfunktionen an. Darunter verschachtelete Menüs (Submenüs), Short-Cuts, frei plazierbare Kontextmenüs, etc.
Die Abbildung zeigt die verschiedenen Primitive und ihre Beziehungen zueinander.

Struktur der Menüprimitive der AWT

Die abstrakte Klasse MenuComponent bildet die Wurzel der Menüklassenhierarchie.
Ein MenuBar stellt eine Zusammenfassung von Menüs dar. Objekte diese Klasse bilden den Ausgangspunkt einer Menüstruktur.
Innerhalb eines MenuBars können beliebig viele MenuItem-Ausprägungen organisiert werden. Ein MenuItem ist ein beliebiger Eintrag eines Menüs. Hierbei kann es sich um einen anklickbaren Menüeintrag oder ein weiteres Menü handeln, welches sich bei Mausberührung öffnet.
Üblicherweise werden konkrete Menüeinträge in Menüs zusammengefaßt. Dem Menü selbst entspricht die AWT-Klasse Menu. Jedes Menu-Objekt kann aus weiteren MenuItems bestehen. Mithin kann ein Menü Submenüs oder direkte Menüeinträge in beliebiger Reihenfolge enthalten.
Als Spezialisierung der durch die Klasse MenuItem definierten Menüeinträge stehen umschaltbare Menüpunkte durch die Klasse CheckboxMenuItem zur Verfügung. Gegenüber den herkömmlichen Menüeinträgen verfügt dieser Typ über ein zusätzliches -- in der visuellen Darstellung plattformabhängig variierendes -- Symbol welches den Eintragszustand „aktiviert“ oder „deaktiviert“ anzeigt.
Menüleisten-unabhängige Kontextmenüs werden durch die Klasse PopupMenu realisiert. Sie sind frei plazierbar, verhalten sich jedoch ansonsten wie die bekannten Menüs.

Neue (klickbare) Menüeinträge werden als Objekte der Klasse MenuItem erzeugt. Die dem Konstruktor übergebene Zeichenkette legt den späteren angezeigten Eintrag im Menü fest. (Verwendung im Beispiel).

Menüs der Beispielapplikation

Die Abbildung stellt einen Ausschnitt der Menüstruktur der Beispielapplikation dar:

Menüstruktur der Beispielapplikation

Erzeugung von Menüstrukturen

Ausgangspunkt der Menüerzeugung ist immer ein Objekt der Klasse MenuBar. Durch den parameterlosen Konstruktor wird eine leere Menüleiste erzeugt, die als Container zur Aufnahme der weiteren Menükomponenten dient. (Verwendung im Beispiel).
Die einzelnen Menüs werden durch Objekte der entsprechenden Klasse Menu repräsentiert. Der in der Menüleistete aufgeführte Menüname wird als Zeichenkettenparameter (Klasse java.lang.String) übergeben. (Verwendung im Beispiel.)
Durch die Methode add(MenuItem) werden Menüeinträge oder ganze Menüs -- allgemein: Subklassen von MenuItem -- in eine bestehende Menüleiste eingehängt. (Verwendung im Beispiel).
Zusätzlich kann dem Konstruktor eine Instanz der Klasse MenuShortcut übergeben werden. Hierdurch wird die in GUIs übliche direkte Anwahlmöglichkeit einzelner Menüeinträge über Tastenkürzel realisiert. (Verwendung im Beispiel).
Zur Erzeugung von CheckboxMenuItems, Menüeinträgen die ihren zweiwertigen Zustand automatisch darstellen, muß ein Objekt der Klasse CheckboxMenuItem erzeugt und in ein bestehendes Menü eingehängt werden. (Verwendung im Beispiel).
Die Variante der Pop-up Menüs wird durch die gleichnamige Klasse PopupMenu erzeugt. Im Unterschied zu den herkömmlichen Menüs wird dieser Menütyp nicht in der Menüleiste, sondern dem beherbergenden Fenster direkt, durch die dortige add-Methode, eingehängt. (Verwendung im Beispiel).

Die Ereignisbehandlung für Menüstrukturen folgt dem in der Übersicht Event-Handling im Überblick gegebenen Schema.
Für jeden Menüeintrag vom Typ MenuItem muß ein Objekt vom Typ ActionListener über die Methode addActionListener(ActionListener) der Klasse MenuItem registriert werden. Dieser muß die Methode actionPerformed(ActionEvent) überschreiben. Dort findet sich der Code, welcher bei Eintreten des Ereignisses (Mausclick auf Menüeintrag) abgearbeitet wird. (Verwendung im Beispiel).
Menüpunkte der Klasse CheckboxMenüItem werden durch Objekte die die Schnittstelle ItemListener implementieren überwacht. Tritt die Zustandsänderung ein, so wird die Methode itemStateChanged(ItemEvent) aufgerufen. (Verwendung im Beispiel).

(1)import java.awt.Button;
(2)import java.awt.CheckboxMenuItem;
(3)import java.awt.Color;
(4)import java.awt.Dialog;
(5)import java.awt.FlowLayout;
(6)import java.awt.Frame;
(7)import java.awt.Label;
(8)import java.awt.Menu;
(9)import java.awt.MenuBar;
(10)import java.awt.MenuItem;
(11)import java.awt.MenuShortcut;
(12)import java.awt.Point;
(13)import java.awt.PopupMenu;
(14)import java.awt.Window;
(15)import java.awt.event.ActionEvent;
(16)import java.awt.event.ActionListener;
(17)import java.awt.event.InputEvent;
(18)import java.awt.event.ItemEvent;
(19)import java.awt.event.ItemListener;
(20)import java.awt.event.KeyEvent;
(21)import java.awt.event.MouseAdapter;
(22)import java.awt.event.MouseEvent;
(23)import java.awt.event.WindowAdapter;
(24)import java.awt.event.WindowEvent;
(25)
(26)public class AWTEx4 {
(27)	private static AWTEx4 app;
(28)	private Label lbl_hello;
(29)	private Frame wnd;
(30)	private MenuItem mi_sayHello, mi_disable, mi_enable, mi_toggle, mi_up, mi_down;
(31)	private CheckboxMenuItem mi_light;
(32)	private PopupMenu myPopupMenu;
(33)
(34)	class ToggleSayHelloState implements ActionListener 	{
(35)		public void actionPerformed(ActionEvent ae) 		{
(36)			mi_sayHello.setEnabled( !mi_sayHello.isEnabled() );
(37)			mi_enable.setEnabled( !mi_sayHello.isEnabled() );
(38)			mi_disable.setEnabled( mi_sayHello.isEnabled() );
(39)		} //actionPerformed()
(40)	} //class ToggleSayHelloSate
(41)
(42)	public static void main(String[] args) {
(43)		app = new AWTEx4();
(44)	} //main()
(45)
(46)	public AWTEx4() {
(47)		wnd = new Frame("Menu Example");
(48)		lbl_hello = new Label("Hello!");
(49)		wnd.add(lbl_hello);
(50)
(51)		wnd.setSize(300,200);
(52)		wnd.setLayout(new FlowLayout() );
(53)
(54)		MenuBar myMenuBar = new MenuBar();
(55)		Menu m_hello = new Menu("Hello");
(56)		myMenuBar.add(m_hello);
(57)		mi_sayHello = new MenuItem("say hello", new MenuShortcut (KeyEvent.VK_H));
(58)		m_hello.add( mi_sayHello );
(59)		mi_disable = new MenuItem("disable say hello");
(60)		m_hello.addSeparator();
(61)		mi_enable = new MenuItem("enable say hello");
(62)		m_hello.add( mi_disable );
(63)		m_hello.add( mi_enable );
(64)
(65)		Menu m_windowAction = new Menu("Window");
(66)		Menu m_windowActionMove = new Menu("Move");
(67)		mi_light = new CheckboxMenuItem("light on/off");
(68)		mi_down = new MenuItem("down");
(69)		mi_up = new MenuItem ("up");
(70)
(71)		m_windowActionMove.add(mi_down);
(72)		m_windowActionMove.add(mi_up);
(73)		m_windowAction.add(m_windowActionMove);
(74)		m_windowAction.add(mi_light);
(75)		myMenuBar.add(m_windowAction);
(76)
(77)		//pop-up menu
(78)		myPopupMenu = new PopupMenu();
(79)		MenuItem mi_toggle = new MenuItem("toggle sayHello");
(80)		myPopupMenu.add( mi_toggle );
(81)		wnd.add( myPopupMenu );
(82)
(83)		Menu m_hlp = new Menu("Help");
(84)		m_hlp.add(new MenuItem("about"));
(85)
(86)		myMenuBar.setHelpMenu(m_hlp);
(87)
(88)		wnd.setMenuBar(myMenuBar);
(89)
(90)		mi_up.addActionListener( new ActionListener() {
(91)			public void actionPerformed(ActionEvent ae) {
(92)				Point pnt = wnd.getLocation();
(93)
(94)				for (int x=pnt.x; x >= 0 ; x--)
(95)					wnd.setLocation(x, pnt.y);
(96)				for (int y=pnt.y; y >= 0; y--)
(97)					wnd.setLocation(0, y);
(98)			} //actionPerformed()
(99)		} //anonymous inner class
(100)		);
(101)
(102)		mi_down.addActionListener( new ActionListener() {
(103)			public void actionPerformed(ActionEvent ae) {
(104)				final Dialog dlg_notImplemented = new Dialog (wnd,true);
(105)				Label lbl = new Label("function not implemented, yet!");
(106)				dlg_notImplemented.setTitle("Sorry, I'm afraid I can't to that ...");
(107)				dlg_notImplemented.setLayout( new FlowLayout() );
(108)				dlg_notImplemented.setResizable(false);
(109)				dlg_notImplemented.add(lbl);
(110)				dlg_notImplemented.setSize(250,80);
(111)
(112)				Button btn_ok = new Button("ok");
(113)				btn_ok.addActionListener( new ActionListener() {
(114)					public void actionPerformed(ActionEvent ae) {
(115)						dlg_notImplemented.setVisible(false);
(116)						dlg_notImplemented.dispose();
(117)					} //actionPerformed()
(118)				} //anonymous inner class
(119)				);
(120)
(121)				dlg_notImplemented.add(btn_ok);
(122)
(123)				dlg_notImplemented.show();
(124)			} //actionPerformed()
(125)		} //anonymous inner class
(126)		);
(127)
(128)		mi_light.addItemListener( new ItemListener() {
(129)			public void itemStateChanged(ItemEvent ie) {
(130)				if (mi_light.getState())
(131)					wnd.setBackground( new Color(33,33,33) );
(132)				else
(133)					wnd.setBackground( new Color(255,255,255) );
(134)			} //actionPerformed()
(135)		} //anonymous inner class
(136)		);
(137)
(138)		mi_sayHello.addActionListener( new ActionListener() {
(139)			public void actionPerformed(ActionEvent ae) {
(140)				lbl_hello.setVisible(true);
(141)				try {
(142)					Thread.sleep(1500);
(143)				} catch (InterruptedException e) {
(144)					//ignore it
(145)				} //catch
(146)				lbl_hello.setVisible(false);
(147)			} //actionPerformed()
(148)		} //anonymous inner class
(149)		);
(150)
(151)		ActionListener toggler = new ToggleSayHelloState();
(152)		mi_disable.addActionListener( toggler );
(153)		mi_enable.addActionListener( toggler );
(154)		mi_toggle.addActionListener( toggler );
(155)
(156)		wnd.addWindowListener( new WindowAdapter() {
(157)			public void windowClosing(WindowEvent we) {
(158)				Window wnd = we.getWindow();
(159)
(160)				wnd.setVisible(false);
(161)				wnd.dispose();
(162)				System.exit(0);
(163)			} //windowClosing()
(164)		}//anonymous inner class
(165)		);
(166)
(167)		wnd.addMouseListener( new MouseAdapter() {
(168)			public void mouseClicked( MouseEvent me) {
(169)				if ( (me.getModifiers() & InputEvent.BUTTON1_MASK) == 0 )
(170)					myPopupMenu.show( me.getComponent(), me.getX(), me.getY() );
(171)			} //mouseClicked()
(172)		} //anonymous inner class
(173)		);
(174)
(175)		wnd.setVisible(true);
(176)		lbl_hello.setVisible(false);
(177)		mi_enable.setEnabled(false);
(178)	} //main()
(179)} //class AWTEx4

Beispiel 93: Beispiel einer Menüstruktur   AWTEx4.java

Das Menü Hello

Das Menü Hello der Beispielapplikation besteht aus drei Menüeinträgen -- say hello, enable say hello und disable say hello. Für den ersten Menüpunkt ist das Tastaturkürzel CTRL+H definiert (Definition im Code). Bereits zum Startzeitpunkt der Applikation ist der Menüpunkt enable say hello deaktiviert, da der entsprechende Menüpunkt vorgabegemäß aktiviert ist. Die Instanzenmethode setEnabled der Klasse MenuItem erlaubt den Wechsel zwischen den beiden Zuständen „aktiviert“, mit normaler visueller Darstellung des Menüpunktes, und „deaktiviert“, mit entsprechender aus-gegrauter Darstellung. Standardmäßig sind alle Menüeintrage nach ihrer Ereugung aktiviert, daher muß die gewünschte Deaktivierung explizit erfolgen. (Stelle im Code).
Wird der Menüpunkt disable say hello angewählt, so zieht dies die Deaktivierung des Menüpunktes say hello und gleichzeitige (Re-)Aktivierung von enable say hello nach sich.
Die Selektierung des Menüpunktes enable say hello vollführt hingegen die duale Operation dazu, Aktivierung der Menüpunkte say hello und disable say hello. Aus diesem Grunde teilen sich beide Menüpunkte dieselbe Ereignisbehandlungs-Routine. Als Konsequenz dieser Forderung wird die Initialisierung der beiden ActionListener mit demselben Objekt notwendig (Codestelle)-- daher kann die Umsetzung an dieser Stelle nicht mehr als anonyme innere Klasse realisiert werden, sondern erfolgt als „normale“ innere Klasse (Codestelle).
Die Anwahl des Menüpunktes say hello zeigt für eineinhalb Sekunden den Schriftzug Hello! am Bildschirm an. (Ereignisbehandlungsroutine).

Das Menü Window

Das zweite Menü der Menüleiste, mit Window betitelt, enthält ein Untermenü Move und eine Ausprägung von CheckboxMenuItem.
Die Ereignisbehandlung für Submenüeinträge erfolgt analog der auf höheren Ebenen; durch Definition des entsprechenden ActionListener-Instanz. (Implementierung der Ereignisbehandlugn des Menüpunktes up). Bei Auswahl dieses Menüpunkts wird das Fenster, durch mehrmalige Änderung der Bildschirmkoordinaten in die linke obere Bildschirmecke verschoben.
Der Eintragstyp CheckboxMenuItem erfordert eine Behandlungsroutine vom Typ ItemListener. Die Anzeige des Zustands wird durch die AWT automatisch vorgenommen, und bedarf keiner Anwenderinteraktion. (Behandlungsroutine des Menüeintrages light on/off). Der Menüpunkt ändert den Bildschirmhintergrund je nach Zustand von hell nach dunkel oder umgekehrt.

Das Menü Help

Das dritte dargestellte Menü wurde als Standard-Hilfe-Menü deklariert. Existiert bereits ein Menü dieser Eigenschaft, so wird es durch das neu definierte ersetzt.
Die Aufnahme in die Menüleiste erfolgt nicht durch die bekannte add-Methode, sondern über den separaten Aufruf setHelpMenu(Menu) (Codestelle).

Das Kontextmenü

Zusätzlich definiert die Applikation ein Kontextmenü welches die Funktionalität der Menüeinträge enable say hello und disable say hello vereinigt. Auch es nutzt die Ereignisbehandlungsroutine der beiden genannten Menüeinträge mit. (Codestelle).
Die Aktivierung und Anzeige am Punkt der Aktivierung muß durch den Anwender selbst realisiert werden. Hierfür ist ein enstprechender MouseListener umzusetzen. Die Methode show(Component, int, int) erlaubt hierfür die Übergabe von Koordinaten, an denen das Kontextmenü aufgeklappt wird.

Textfelder

Editierbare Textfelder für Benutzereingaben stellt die Klasse TextField bereit.

Zur Reaktion auf Tastenanschläge kann -- wie in Übersicht dargestellt -- jedes Objekt genutzt werden, das die von Component ererbte Schnittstelle KeyListener implementiert.

(1)import java.awt.Frame;
(2)import java.awt.GridLayout;
(3)import java.awt.Label;
(4)import java.awt.TextField;
(5)import java.awt.Window;
(6)import java.awt.event.KeyAdapter;
(7)import java.awt.event.KeyEvent;
(8)import java.awt.event.WindowAdapter;
(9)import java.awt.event.WindowEvent;
(10)
(11)public class AWTEx5 {
(12)	private static AWTEx5 app;
(13)	private TextField tf_dm, tf_eur;
(14)
(15)	public static void main(String[] args) {
(16)		app = new AWTEx5();
(17)	} //main()
(18)
(19)	public AWTEx5() {
(20)		Frame wnd = new Frame("Euro-Konverter");
(21)
(22)		wnd.setSize(200,100);
(23)		wnd.setLayout(new GridLayout(2,2) );
(24)
(25)		Label lbl_dm = new Label ("DM");
(26)		Label lbl_eur = new Label ("Euro");
(27)		tf_dm = new TextField("1", 7);
(28)		tf_eur = new TextField("1.95583", 7 );
(29)
(30)		wnd.add(lbl_dm);
(31)		wnd.add(lbl_eur);
(32)		wnd.add(tf_dm);
(33)		wnd.add(tf_eur);
(34)		wnd.show();
(35)
(36)
(37)		tf_dm.addKeyListener( new KeyAdapter() {
(38)			public void keyReleased(KeyEvent e) {
(39)				tf_eur.setText( ""+Double.parseDouble(tf_dm.getText())*1.95583 );
(40)			} //keyPressed()
(41)		} //anonymous inner class
(42)		);
(43)
(44)		tf_eur.addKeyListener( new KeyAdapter() {
(45)			public void keyReleased(KeyEvent e) {
(46)				tf_dm.setText(""+Double.parseDouble(tf_eur.getText())/1.95583 );
(47)			} //keyPressed()
(48)		} //anonymous inner class
(49)		);
(50)
(51)		wnd.addWindowListener( new WindowAdapter() {
(52)			public void windowClosing(WindowEvent we) {
(53)				Window wnd = we.getWindow();
(54)
(55)				wnd.setVisible(false);
(56)				wnd.dispose();
(57)				System.exit(0);
(58)			} //windowClosing()
(59)		}//anonymous inner class
(60)		);
(61)	} //constructor
(62)} //end class AWTEx5

Beispiel 94: Ein einfacher Euro-Umrechner   AWTEx5.java

Bildschirmausgabe:

Ein Zwischenberechnungsstand

Die Applikation nutzt das GridLayout zur Ausrichtung der vier visuellen Komponenten. Hierfür wird der entsprechende Layoutmanager dahingehend parametrisiert, daß er ein quadratisches Layout mit jeweils zwei horizontalen und vertikalen Einträgen erlaubt. (Codestelle).
Die Beschriftungen der Spalten sind als Labels realisiert.
Zum Erzeugungszeitpunkt wird den Text-Feldern ein Startwert, und die horizontale Ausdehnung von sieben Zeichen zugewiesen. (Codestelle).
In den (anonymen inneren) Klassen zur Ereignisbehandlung, wird die Methode keyTyped(KeyEvent) überschrieben. Sie wird nach Abschluß des Tastendrucks aufgerufen.
In der Implementierung dieser Methode wird die tatsächliche Umrechung vorgenommen, und daß Umrechnungsergebnis im jeweils anderen Feld ausgegeben. (Codestelle).
Hinweis: Eine Behandlung des möglicherweise generierten NumberFormatException-Ausnahmeereignisses findet aus Übersichtlichkeitsgründen nicht statt.

Dialoge

Ausprägungen der Klasse Dialog stellen eine Möglichkeit zur Realisierung einfacher Ein- und Ausgabefenster dar. Diese Art Fenster eignet sich besonders für kurze Anfragen, oder Meldungen, an den Anwender.

Beispiel awtEx4 enthält eine solche Nachrichtenbox. Dialogfenster können, im Gegensatz zu den bekannten Applikationsfenstern, modal sein. Dies bedeutet, daß ein solches Fenster nicht durch den Anwender in den Hintergrund versetzt werden kann; es erzwingt eine Reaktion -- zumeist in Form der Bestätigung einer Meldung o.ä. -- bevor die Applikation fortfährt.

back to top   3.2.8 Swing

 

Mit dem JDK v1.1 wurde als zusätzliche API zur Erstellung von graphischen Oberflächen Swing vorgestellt. Bis zur aktuellen Version 1.3 ist diese API weiterhin Bestandteil des Extension Paketes, und steht daher nicht auf allen Plattformen zur Verfügung. Des weiteren können sich noch zukünftige Änderungen an den derzeit publizierten APIs ergeben, so daß bestehender Code überarbeitet werden muß.

Folgende Überlegungen führten zur Entwicklung von Swing:

Daher wurde mit den Java Foundation Classes, deren Bestandteil Swing ist, versucht eine echte Alternative zum bestehenden -- und weiter angebotenen -- AWT zu schaffen. Dabei erhöht Swing massiv die Anzahl der angebotenen visuellen Komponenten, und fügt einige sehr mächtige Primitive wie beispielsweise zur Darstellung von Bäumen hinzu.

Anders als die AWT setzt Swing nicht auf den Möglichkeiten des zugrundeliegenden GUI-Systems auf, sondern stellt selbst die gesamte Verwaltung und Verarbeitung der angebotenen Primitven zur Verfügung. Als Folge dieses Ansatzes realisiert Swing ein eigenes look-and-feel, das auf allen unterstützten Plattformen unverändert präsentiert wird. Mit diesem -- Metal genannten -- Layout entsteht erstmals ein typisches Aussehen nativer Java-Applikationen.
Technisch ist Swing mit den seit JDK v1.1 angebotenen leichtgewichtigen Komponenten, den Java Beans, realisiert. Die Realisierung graphischer Primitivoperationen erfolgt daher nicht mehr durch Operationen des zugrundeliegenden GUI-Systems, sondern durch die angebotenen Swing-Komponenten selbst.
Dies führt dazu, daß die gesamte Swing-API ohne native Methoden -- vollständig in Java codiert -- realisiert werden konnte. Hieraus ergeben sich weitere Vorteile in der Anwendung, insbesondere in der Fehlersuche.

Jedoch führt die verfolgte pure Java-Implementierung auch zu Problemen. So erhöht sich durch den de-facto emulierenden Ansatz der Speicherplatzbedarf, da nicht mehr auf die evtl. durch die Plattform angebotenen Routinen zurückgegriffen wird. Flankierend sinkt die Ausführungsgeschwindigkeit durch die gestiegene Menge an auszuführendem Java-Code.
Anmerkung:
Aus rein praktischen Erwängungen sprechen derzeit noch zwei weitere Punkte gegen den breiten Swing-Einsatz: zunächst der Reifegrad und die Entwicklungsstabilität. Als Bestandteil des Java Extension Paketes ist Swing explizit als experimenteller Bestandteil der derzeit verfügbaren Java-API gekennzeichnet. In der verbreiteten Implementierung finden sich noch kleinere und größere Fehler und Ungereimtheiten, die gegen die Verwendung in Produktivappliaktaionen sprechen. Wie bei allen anderen Bestandteilen des javax-Paketes auch, behält sich SUN explizit das Recht vor Schnittstellen und Funktionalität ohne Ankündigung zu verändern oder aus dem Angebot herauszunehmen. Daher verbietet sich, unter Berücksichtigung einer zukünftigen Pflege des entstehenden Applikationscodes, die Nutzung dieser Klassen schon fast.
Ergänzend sei noch erwähnt, daß seitens der aktuell verfügbaren Web-Browser noch keine Swing-Unterstützung umgesetzt ist. Dieses Manko läßt sich zwar durch die zusätzliche manuelle Installation des Java-Plugins beheben, verlangt dem Anwender jedoch einen zusätzlichen Installationsvorgang ab.

Swing verwirklicht durchgängig das von T. Renskaug initiierte Model-View-Controller-Konzept. Dessen hervorstechendstes Kennzeichen die Separierung des Codes in drei verschiedene Verwendungskategorien ist:

Die gesamte Verarbeitungslogik ist dabei in der Model-Klasse zentralisiert. Zu jedem Model können dabei gleichzeitig verschiedene View-Klassen existieren.

Swing reduziert diesen Mechanismus auf zwei verschiedenen Klassen-Typen. Hierbei wird die View- und die Controller-Komponente zu einer Einheit verschmolzen. Das entstehende Design wird Model-Delegate-Prinzip genannt.

(1)import java.awt.Window;
(2)import java.awt.event.ActionEvent;
(3)import java.awt.event.ActionListener;
(4)import java.awt.event.WindowAdapter;
(5)import java.awt.event.WindowEvent;
(6)import javax.swing.JButton;
(7)import javax.swing.JFrame;
(8)import javax.swing.JPanel;
(9)import javax.swing.SwingUtilities;
(10)import javax.swing.UIManager;
(11)import javax.swing.UnsupportedLookAndFeelException;
(12)
(13)public class SwingEx1 extends JFrame implements ActionListener {
(14)	public SwingEx1() {
(15)		super("first swing application");
(16)		JPanel myPanel = new JPanel();
(17)		JButton btn_win = new JButton("Windows");
(18)		JButton btn_motif = new JButton("Motif");
(19)		JButton btn_metal = new JButton("Metal");
(20)
(21)		btn_win.setToolTipText("switch to Windows look-and-feel");
(22)		btn_motif.setToolTipText("switch to Motif look-and-feel");
(23)		btn_metal.setToolTipText("switch to Metal look-and-feel");
(24)
(25)		myPanel.add( btn_win );
(26)		myPanel.add( btn_motif );
(27)		myPanel.add( btn_metal );
(28)
(29)		btn_win.addActionListener(this);
(30)		btn_motif.addActionListener(this);
(31)		btn_metal.addActionListener(this);
(32)
(33)		getContentPane().add("South", myPanel);
(34)
(35)		addWindowListener( new WindowAdapter() {
(36)			public void windowClosing(WindowEvent we) {
(37)				Window wnd = we.getWindow();
(38)
(39)				wnd.setVisible(false);
(40)				wnd.dispose();
(41)				System.exit(0);
(42)			} //windowClosing()
(43)		}//anonymous inner class
(44)		);
(45)	} //constructor
(46)
(47)	public void actionPerformed(ActionEvent ae) {
(48)		String cmd = ae.getActionCommand(),
(49)				 plaf="";
(50)
(51)		if ( cmd.equals("Metal") ) {
(52)			plaf = "javax.swing.plaf.metal.MetalLookAndFeel";
(53)		} else {
(54)			if ( cmd.equals("Motif") ) {
(55)				plaf = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
(56)			} else {
(57)				if( cmd.equals("Windows") ) {
(58)					plaf = "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
(59)				} //else
(60)			} //else
(61)		} //else
(62)
(63)		try {
(64)			UIManager.setLookAndFeel(plaf);
(65)			SwingUtilities.updateComponentTreeUI(this);
(66)		} catch (UnsupportedLookAndFeelException e) {
(67)			System.err.println(e.toString());
(68)   	} catch (ClassNotFoundException e) {
(69)			System.err.println(e.toString());
(70)		} catch (InstantiationException e) {
(71)			System.err.println(e.toString());
(72)		} catch (IllegalAccessException e) {
(73)			System.err.println(e.toString());
(74)		} //catch
(75)	} //actionPerformed()
(76)
(77)	public static void main(String[] args) {
(78)		SwingEx1 wnd = new SwingEx1();
(79)		wnd.setSize(300,200);
(80)		wnd.pack();
(81)		wnd.setVisible(true);
(82)	} //end main()
(83)} //class SwingEx1

Beispiel 95: Eine erstes Swing-Applikation   SwingEx1.java

Bildschirmausgabe:

Das Beispielprogramm im MS-Windows-Layout
... im Motif-Layout
... im Metal-Layout

Diese Swing-Applikation definiert ein einfaches Fenster mit drei Buttons.
Deutlichstes Kennzeichen der Nutzung von Swing ist das Präfix J vor den bekannten AWT-Komponentennamen. So erweitert die Applikation das Swing-Analogon der AWT-Klasse Frame, jetzt unter dem Namen JFrame.
Die Schaltflächen sind entsprechend Ausprägungen von JButton. Mit der Methode setToolTipText(String) wird die rein Swing-interne Möglichkeit zur Definition eines Hilfstexts genutzt, der bei Überstreichen einer Komponente mit der Maus automatisch angezeigt wird. Die Implementierung greift nicht auf Mechanismen des zugrundeliegenden GUI-Systems zurück, sondern ist vollständig innerhalb von Swing realisiert.
Die Ereingisbearbeitung ist identisch zur AWT v1.1 realisiert, und kann daher naherzu unverändert übernommen werden.
Als Behandlungsroutine der drei Schaltflächen ist der Wechsel des Bildschirmlayouts zur Laufzeit realisiert. Nach Aktivierung der entsprechenden Schaltfläche wird das entsprechende, innerhalb der Swing-API vordefinierte Layouts referenziert, und durch die statische Methode setLookAndFeel(String) geladen. Nach der Aktualisierung der Bildschirmdarstellung präsentiert sich die gesamte Applikation im neuen Layout.




separator line
Service provided by Mario Jeckle
Generated: 2004-06-11T07:13:10+01:00
Feedback Feedback       SiteMap SiteMap
This page's original location This page's original location: http://www.jeckle.de/vorlesung/java/script.html
RDF metadata describing this page RDF description for this page