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: