![]() | ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
![]() |
![]() |
![]() |
![]() |
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.
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.
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 JDK stellt eine Referenzimplementierung der kompletten Java-Plattform zur Verfügung.
Seine Hauptbestandteile sind:
javac
javac example.java
javac HelloWorld.java
erzeugt die Dateien HelloWorld.class und SayHello.classjava
java example
.class
darf nicht angegeben werden!java HelloWorld
führt die Applikation aus.appletviewer
appletviewer example.html
appletviewer HelloWorldApplet1.html
javadoc
javadoc example.java
javadoc -private -version -author -windowtitle "Beispiel einer JavaDoc generierten Dokumentation" Hello World.java
jdb
jdb example
javap
javap example
Die Rückübersetzung des Compilates HelloWorld.class mit javap -c HelloWorld
liefert:
|
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.
![]() |
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.
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
.
![]() |
![]() |
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.
![]() |
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)
![]() |
|
(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.
![]() |
![]() |
Bereits am HelloWorld-Beispiel wird der wesentliche Aufbau einer Javaquellcodedatei sichtbar.
|
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.
![]() |
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.
Im Einzelnen werden angeboten:
![]() |
|
Siehe Java Language Specification
Anmerkungen:
inconvertible types
).bool
als expliziter Datentyp (üblicherweise der Länge 1 Byte) definiert, der jedoch die Wahrheitswerte als Integer-Zahlen ablegt. So gilt: true
== 1 bzw. false
== 0 (siehe Beispiel)NaN
(Not a Number) ein ausgezeichneter Wert für nicht interpretierbare Belegungen.char
ist zwar als Zeichensymbol ausgelegt, kann aber auch in mathematischen Termen anstelle von short
verwendet werden; hier wird char
jedoch vorzeichenlos(!) interpretiert.char
zu int
erfolgt implizit, die Umgekehrung bedarf jedoch einer ausdrücklichen Typumwandlung.object
(32-bittige Referenz auf ein Java-Objekt) und returnAddress
(32-Bit) zwei zusätzliche Datentypen an.\uxxxx
für Unicodezeichen (xxxx
ist die hexadezimale Zeichennummer)...l
oder ...L
für Longwerte0...
für Oktalwerte0x...
für Hexadezimalwerte...f
oder ...F
für Floatwerte...d
oder ...D
für Doublewerte
|
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.
|
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
|
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
![]() |
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.
![]() |
boolean
liefern:
<
, <=
, >
und >=
(siehe Java Language Specification)==
und !=
(siehe Java Language Specification)int
oder long
) zurückliefern:
*
, /
und %
(siehe Java Language Specification).+
und -
(siehe Java Language Specification).++
(siehe Java Language Specification bzw. siehe Java Language Specification).--
(siehe Java Language Specification bzw. siehe Java Language Specification).<<
, >>
bzw. >>>
(Einführung von Füllnullen) (siehe Java Language Specification).-2 >>> 2 = -1
aber: -2 >> = 1073741823
~
(siehe Java Language Specification).&
, |
und ^
(siehe Java Language Specification).if
) ? :
(siehe Java Language 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).
![]() |
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.
boolean
liefern:
<
, <=
, >
und >=
(siehe Java Language Specification)==
und !=
(siehe Java Language Specification)float
oder double
) zurückliefern:
*
, /
und %
(siehe Java Language Specification).+
und -
(siehe Java Language Specification).++
(siehe Java Language Specification bzw. siehe Java Language Specification).--
(siehe Java Language Specification bzw. siehe Java Language Specification).if
) ? :
(siehe Java Language Specification).Anmerkung: Identisch zu ANSI C/C++ wurde der unäre Operator +
lediglich aus Symmetriegründen zum unären eingeführt.
![]() |
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
.
==
und !=
(siehe Java Language Specification).!
(siehe Java Language Specification).&
, ^
und |
(siehe Java Language Specification).&&
bzw. ||
(siehe Java Language Specification bzw. siehe Java Language Specification).short circuit
-Verfahren. Hierbei wird nicht zwingend der gesamte Ausdruck ausgewertet. Bei der Ver-und-ung wird abgebrochen, sobald ein falsches Ergebnis vorliegt; bzw. bei der Ver-oder-ung sobald ein wahres Ergebnis vorliegt.? :
(siehe Java Language Specification)![]() |
+
zur Stringkonkatenation. Ist einer der beiden angegebenen Operaden nicht vom Typ String, so wird zur Laufzeit eine Stringkonversion durchgeführt (siehe Java Language Specification). Diese Stringkonversion ist immer möglich, da alle Objekte von java.lang.Object
die Methode toString
erben.instanceof
-Operator erledigt werden. Er liefert einen Wahrheitswert zurück.object instanceof class
.![]() |
+=, -=, *=, /=, &=, |=, ^=, %=, <<=, >>=, >>>=
.*
und &
werden wegen des Fehlens expliziter Speicherreferenzen nicht unterstützt.sizeof
-Operator. Für ihn existiert kein Anwendungsfall, da einerseits die Größer der built-in Typen festgelegt ist, andererseits keine Speicherblöcke wahlfrei allokiert werden können.&&, ||
und ? :
, werden vor der Ausführung des Operators ausgewertet.![]() |
Es gelten folgende Operatorpräzedenzen (geordnet von oben (entspricht höchster Präzedenz) nach unten (niedrigster Präzedenz)):
![]() |
|
![]() |
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.
![]() |
|
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.
![]() |
![]() |
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
|
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:
|
Beispiel 7: Switch-Statement SwitchTest.java
Anmerkungen:
break
-Anweisung kann eine Marke nachgestellt werden, die angibt auf welcher Schachtelungsebene fortgesetzt werden soll (siehe Java Language Specification). Hierbei sind jedoch nur Rückwärtsreferenzen zugelassen.case
-Körper für mehrere Alternativen gelten, so müssen diese explizit mit einleitendem case
aufgeführt werden. Pro case
ist jeweils nur genau eine konstante Bedingung zugelassen.switch
-Statement syntaktisch unverändert von C/C++ übernommen wurde, existieren in Java zustäzliche Restriktionen, welche die Kompatibilität einschränken.switch
-Anweisung die zugehörigen case
-Alternativen direkt enthalten; Konstruktionen wie duff's devicesiehe Java Language Specification sind daher nicht compilierbar.switch
-Bedingung muß vom Typ byte
, short
, char
oder int
sein.![]() |
Java bietet die bereits in C/C++ eingeführten Schleifenkonstrukte
for
(siehe Java Language Specification)while
(siehe Java Language Specification)do
(siehe Java Language Specification)break
und continue
(siehe Java Language Specification)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.
|
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)
|
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.
|
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
|
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.
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.
|
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.
![]() |
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
|
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(...)
.
|
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.
|
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:
|
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.
|
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ß.
|
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.
|
Beispiel 19: Behandlung und Weiterreichung einer Ausnahme OwnException5.java
![]() |
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:
|
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.
|
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.
|
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.
![]() |
![]() |
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:
new
erzeugt werden.ArrayIndexOutOfBoundsException
abgefangen.a[i]
greift auf das i+1-te Element des Arrays a
zu.new
initialisiert.0
für alle numerischen Datentypen, false
für boolean
und Unicode \u0000
für char
gesetzt. Objektwertige Typen werden durch ihren Konstruktor initialisiert.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 [ ]
|
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.
|
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:
|
Beispiel 25: verschachtelte mehrdimensionale Arraydefinition Array3.java
Die Definitionint[][] 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.
|
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.
|
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).
![]() |
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.
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:
![]() |
|
Ein umfangreicheres Beispiel:
|
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
Seit Java v1.1 besteht auch die Möglichkeit Klassen innerhalb von Klassen zu definieren.
Merkmale:
UmgebendeKlasse$innereKlasse
erzeugt.(Naive) Erweiterung des vorhergehenden Beispiels: Die Matrikelnummer ist als eigenständige Klasse innerhalb von Student2
realisiert:
|
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!
|
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:
|
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.
![]() |
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:
![]() |
|
Die Syntaxkomponenten im Einzelnen:
![]() |
|
|
Beispiel 32: Einige Attributdefinitionen Attributes.java
Abschlußbemerkungen:
pi
im Beispiel) als static final
.![]() |
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:
![]() |
|
Die Syntaxkomponenten im Einzelnen:
![]() |
|
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.
|
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 static
Klassenmethoden 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.
|
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 this
gefolgt 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.
|
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.
|
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.
|
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.
|
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
)
|
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:
![]() |
|
Anmerkung: Der Zugriff auf als protected
deklarierte Attribute und Methoden ist auch über Objektreferenzen möglich, die denselben Typ haben wie die definierende Klasse.
![]() |
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:
![]() |
|
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.
|
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:
|
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:
|
Beispiel 42: Definition eines klassenartigen Aufzählungstyps EnumTest3.java
![]() |
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.
![]() |
|
Die Abbildung 9 zeigt die Organisation der Wrapperklassen innerhalb der Standard-API im Überblick:
|
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:
final
deklariert, und können daher nicht weiter spezialisiert werden....Value
booleanValue
, byteValue
, shortValue
, intValue
, longValue
, floatValue
, doubleValue
public T TValue()
, wobei T
der jeweilige Primitivtyp ist (entsprechend für den Wrappertypen um int: public int intValue()
.Boolean (boolean value)
, Boolean (String s)
, Integer (int value)
, Integer (String s)
)MIN_VALUE
oder oberhalb MAX_VALUE
) überschreitet. Ebenso wenn die Zeichenkette Symbole enthält, die nich in eine numerische Darstellung überführt werden können.compareTo
equals
die Objekte direkt.
|
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.
parseT
-Methoden angeboten, wobei T den konkreten Primitivtypen bezeichne. Diese liefern, im Falle der Gültigkeit, aus einer Zeichenkette den jeweiligen Primitvtypen zurück.parseByte (String s)
, parseByte(String s, int radix)
, parseInt (String s)
, parseInt (String s, int radix)
, etc.)valueOf
-Methode eine Factorymethode zur Erzeugung eines neuen Wrapperobjektes aus einer Zeichenkette definiert.BooleanHolder
, byteHolder
, ShortHolder
, IntHolder
, LongHolder
, FloatHolder
, DoubleHolder
).![]() |
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:
|
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:
|
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.
![]() |
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:
extends
ausgedrückt.ClassCastException
ausgelöst (C++ retourniert hier ein null
Objekt).
|
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.
final
unterbunden werden.
|
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.
super
dient als Platzhalter zur dynamischen Referenzierung des Superklassenobjekts.sHello
, die zunächst auf dem Superklassenobjekt die Methode hello
ausführt, und im Anschluß die gleichnamige auf dem aktuellen Objekt.super
zu super.super.foo()
--, ist nicht möglich.super(...)
zur Verfügung, durch welche der entsprechende Superklassenkonstruktor aufgerufen werden kann. Wird der Konstruktor der Superklasse nicht explizit aufgerufen, so erfolgt autmatisch ein Aufruf des parameterlosen Standardkonstruktors der Superklasse.
|
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.
protected
, Subklasse: private
ist möglich.abstract
als nicht instanziierbar gekennzeichnet.Object
selbst, die API-Klasse Object
an diese Stelle.T[]
in Object[]
umgewandelt werden (up cast).![]() |
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:
final static
, und damit als Konstanten, deklariert.abstract
, und damit -- innerhalb der Schnittstelle als -- nicht implementierbar, deklariert.
|
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:
![]() |
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:
JAR
-Datei (intern ZIP-Komprimierung) zusammengefaßt werden.*
terminiert -- importiert werden alle Unterpakete einschließlich der dort definierten Klassen.public
deklarierter Attribute oder Methoden innerhalb eines Paketes. Hier werden alle Elemente desselben Paketes als vertrauenswürdig (engl. trusted) angesehen, und erhalten daher auch Zugriff auf als protected
deklarierte Information.Hinweise:
import
-Anweisung zur Verfügung.javax
sind nicht Bestandteil des offiziellen JDKs und stehen daher oftmals nicht auf allen unterstützten Plattformen zur Verfügung.CLASSPATH
muß zwingend zumindest Dateisystemkataloge der benutzerdefinierten Pakte umfassen. In älteren JDK-Versionen (< v1.3) zusätzlich noch die Systempakete, bzw. die Datei classes.zip
.Beispiele aus der Java-API:
Beispiel:
Quellcodedateien:
|
Beispiel 51: Klasse TestClass innerhalb des Paketes testPackage TestClass.java
|
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.
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
:
|
Beispiel 53: Statischer Import SITest.java
![]() |
![]() |
![]() |
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.
|
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.
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.
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.
m3
(Element des root set) und dem als n2
benannten Speicherobjekt.n2
wird markiert.n2
zum Speicherobjekt n21
.n21
.n21
zurück zu n2
.n2
) ist bereits markiert. (Implizit ist ein Zyklus erkannt worden!)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:
verbose:gc
Xnoclassgc
Xincgc
|
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:
|
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:
java.lang.ref
.![]() |
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.
Die wesentlichen Bestandteile der JVM sind:
undefined
gesetzt. Konkrete Größe des virtuellen pc-Registers hängt von der Adresslänge der realen Plattform ab.constant_pool
Tabelle der class-Datei abgelegt sind. Die Funktion dieses Bereichs ähnelt dem einer konventionellen Symboltabelle. (vgl. JVM-Spezifikation)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:
pc
-- program counteroptop
frame
vars
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.
Datentypen der JVM:
byte
short
int
long
char
float
double
boolean
boolean
-Typen enthalten als Operationen auf int
-Typen um.returnAddress
null
sein, wobei die JVM keine konkrete Darstellung dieses Wertes unterstellt.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:
![]() |
|
Instruktionen zur expliziten Modifikation des Operanden-Stacks:
![]() |
|
Instruktion zur Steuerung des Kontrollflußes:
![]() |
|
Instruktionen zur Operation auf Klassen und Objekten:
![]() |
|
Instruktionen zur Methodenausführung:
![]() |
|
Instruktionen zum Zugriff auf Attribute:
![]() |
|
Instruktionen zur Operation auf Arrays:
![]() |
|
Instruktionen zur Typkonversion:
![]() |
|
Instruktionen zur Durchführung arithmetischer Operationen:
Eingangsperanden werden vom Stack entnommen und das Berechnungsergebnis ebenda abgelegt.
![]() |
|
Sonstige Instruktionen:
![]() |
|
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.
![]() |
|
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 |
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 |
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 |
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.
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:
java.io.DataInput
, java.io.DataOutput
, java.io.DataInputStream
und java.io.DataOutputStream
unterstützen dieses Format. (Beispiel)class
-Datei werden nicht zusätzlich optimiert abgelegt, daher erfolgt weder ein Auffüllen auf spezifische Wortgrenzen, noch ein Alignment an solchen.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 |
Download des Beispiels |
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
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.
![]() |
|
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
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:
![]() |
|
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.
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.
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.
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
.
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.
Konstante vom Typ CONSTANT_Utf8
. Sie leitet den in der Klasse Act
spezifizierten Methodennamen doMathForever
ein.
Konstante vom Typ CONSTANT_Utf8
, die den fixen String Exceptions einleitet.
Konstante vom Typ CONSTANT_Utf8
, die den fixen String LineNumberTable einleitet.
Konstante vom Typ CONSTANT_Utf8
, die den fixen String SourceFile einleitet.
Konstante vom Typ CONSTANT_Utf8
, die den fixen String LocalVariables einleitet.
Konstante vom Typ CONSTANT_Utf8
, die den fixen String Code einleitet.
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.
Konstante vom Typ CONSTANT_Utf8
, die den String Act einleitet. Vom zweiten Element des Konstantenpools referenziertes Element. Name der Klasse.
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.
Konstante vom Typ CONSTANT_Utf8
, die den String snipet.java -- den Namen der Quellcodedatei in der sich die Definition der Klasse Act
befindet -- einleitet.
Konstante vom Typ CONSTANT_Utf8
, die den String ()V einleitet. Methodendeskriptor, der weder Übergabeargumente noch Rückgabetyp besitzt.
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.
![]() |
|
Referenz in den Konstantenpool, auf die Klasse selbst (this
) und die Superklasse (super
).
interface_count
: Anzahl der durch die Klasse implementierten Schnittstellen.fields_count
: Anzahl der Klassen- oder Instanzvariablen der Klasse.
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.
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.
![]() |
|
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
: 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
)
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.
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;
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
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.
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.
Analog der Definition für Methoden, die maximale Höhe des Operandenstacks und die Anzahl der lokalen Variablen.
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
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 der Quellcodezeilennummern zu den resultierenden Opcodes.
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:
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.
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)
InputStream
FileInputStream
PipedInputStream
FilterInputStream
LineNumberInputStream
deprecatedDataInputStream
BufferedInputStream
InputStream
s die Möglichkeit des gepufferten Lesens aus dem Eingabestrom. Zusätzlich wird das Setzen von, und die Rücksetzung des Strompositionszeigers zu, Markierungspunkten unterstützt.PushbackInputStream
InputStream
s die Möglichkeit der Wiedereinstellung von bereits gelesenen Byte-Inhalten in den Eingabestrom.ByteArrayInputStream
StringBufferInputStream
deprecated
Lesen aus String.Achtung: Diese Klasse geht von der (i. A. falschen) Entsprechung zwischen Bytes und Charactern aus. StringReader liefert für denselben Anwendungsfalle eine korrekte Implementierung.SequenceInputStream
ObjectInputStream
java.io.Serializable
oder java.io.Externalizable
unterstützten.OutputStream
FileOutputStream
PipedOutputStream
PipedInputStream
s.FilterOutputStream
DataOutputStream
BufferedOutputStream
OutputStream
s die Möglichkeit des gepufferten (d.h. nicht mehr Byte-weisen) Schreibens.PrintStream
OutputStream
s diverse Möglichkeiten zur Ausgabe der verschiedenen Java Datentypen. Anders als die übrigen Stromtypen erzeugt PrintStream
keine IOException
.PrintWriter
existiert eine Standardimplementierung, die auf die Besonderheiten im Zusammenhang mit Unicode-Ausgaben Rücksicht nimmt.ByteArrayOutputStream
ObjectOutputStream
java.io.Serializable
-Schnittstelle implementieren können über diesen Mechanismus persistent geschrieben werden.Als Character Streams stehen zur Verfügung:
(Einrückungen kennzeichnen Subklassenbeziehungen, Kursivsetzungen abstrakte Klassen)
Reader
BufferedReader
LineNumberReader
CharArrayReader
InputStreamReader
FileReader
FilterReader
PushbackReader
PipedReader
StringReader
String
liest.Writer
BufferedWriter
system properties
).CharArrayWriter
OutputStreamWriter
FileWriter
FilterWriter
PipedWriter
StringWriter
String
schreibt.PrintWriter
Ü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.
java.io.InputStream
:
int available()
Liefert die Anzahl Bytes die an der Eingabeschnittstelle zur Verfügung stehen. Diese Anzahl kann ohne Blockierung des Aufrufers gelesen werden.void close()
Schließt Eingabestrom unter Freigabe der belegten Systemressourcenvoid mark(int)
Markiert die gegenwärtige Position des Eingabezeigers im Eingabestrom; ein folgender Aufruf von reset
setzt den Eingabezeiger wieder an die markierte Position zurück.boolean markSupported()
Gibt Auskunft darüber, ob der Eingabestrom die Positonsmarkierung, und das Rücksetzen darauf, unterstützt.int read()
Ließt den nächsten Bytewert (>0 und <256) aus dem Eingabestrom. Ist das Ende des Eingabestromes erreicht, wird -1
retourniert (kein Ausnahmeereignis! Die Verwendung des StreamTokenizers ermöglicht hier ein handlicheres Vorgehen).InputStream
sind gezwungen diese Methode zu überschreiben.int read (byte[])
Ließt eine Bytesequenz aus dem Eingabestrom, und legt sie im übergebenen Array ab. Die Anzahl der gelesenen Zeichen, bzw. -1
beim Erreichen den Eingabeendes, wird zurückgegeben.int read(byte[] b, int off, int len
Ließt Bytesequenz der Länge len
und speichert sie ab Position off
im übergebenen Array.void reset()
Setzt den Stromzeiger auf die Position der letzten vorhergehenden Markierung zurück, falls eine solche existiert.long skip(long n)
Versucht den Stromzeiger um n
Bytepositionen vorzurücken. Die Anzahl der tatsächlich übersprungenen Bytes wird zurückgegeben.java.io.Reader
:
void close()
Schließt den Ausgabestrom.void mark(int readAheadLimit)
Markiert gegenwärtige Position des Eingabezeigers. Ein nachfolgender Aufruf von reset
versucht den Positionszeiger auf die Markierte Stelle zurückzusetzen, falls die aktuelle Position nicht weiter als readAheadLimit
Zeichen entfernt liegt.void markSupported()
Gibt Auskunft darüber, ob der Eingabestrom die Positonsmarkierung, und das Rücksetzen darauf, unterstützt.int read()
zur Extraktion genau eines Zeichens (16 Bit Character).int read(char[] cbuf)
zur Extraktion einer Sequenz, beginnend ab der aktuellen Stromzeigerpositon.int read(char[] cbuf, int off, int len)
zur Extraktion einer Sequenz der Länge len
oder weniger von Zeichen, und Ablage in Array beginnend ab der Position off
.boolean ready()
Liefert dieser Aufruf true
zurück, so liegen weitere Eingaben vor, die durch ein folgendes read
gelesen werden können.reset()
Versucht den Positionszeiger auf die durch die letzte gesetzte Markierung bezeichnete Stelle zurückzurücken.long skip(long n)
Versucht n
Zeichenpositionen zu überspringen. Die Anzahl der tatsächlich übersprungenen Positionen wird zurückgeliefert.java.io.OutputStream
:
void close()
Schließt Ausgabestrom und gibt durch ihn belegte Systemressourcen frei.void flush()
Leert Ausgabestrom und schreibt alle Pufferbereiche.void write(byte[])
Schreibt Byte-Array in Ausgabestrom.void write(byte[] b, int off, int len)
Schreibt len
Bytes eines Byte-Arrays ab Position off
.void write(int)
Schreibt Bytewert in Ausgabestrom.OutputStream
müssen diese dieser Methode überschreiben.java.io.Writer
:
void close()
Schließt Ausgabestrom nach erfolgter Pufferleerung (flushing).Writer
müssen diese Methode überschreiben.void flush()
Leert Pufferbereiche.write(int)
Schreibt ein Unicode-Zeichen.write(char[])
Schreibt Character-Array.write(char[] cbuf, int off, int len)
Schreibt Teil der Länge len
eines Character-Arrays beginnend ab Position off
.write(String)
Schreibt String
.write(String str, int off, int len)
Schreibt Teil der Länge len
eines String
s beginnend ab Position off
.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.
|
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:
![]() |
|
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).
|
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
|
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.
|
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.
|
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.
![]() |
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:
Thread
.Thread
die Schnittstelle Runnable
. Sie definiert die Operation run()
.run()
.start()
aufgerufen. start()
ist asynchron und kehrt sofort nach Erzeugung des Threads zum Aufrufer zurück.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.
|
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:
start()
sleep(long)
und
sleep(long, int)
join()
,
join(long)
und
join(long, int)
run()
). Optional kann eine maximale abzuwartende Zeitspanne definiert werden.yield
destroy()
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:
wait()
wait(long)
wait(long, int)
notify()
oder notifyAll()
ausführt.notify()
notifyAll()
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:
int getPriority()
setPriority(int)
SecurityException
, bzw. -- bei ungültigen Übergabeparametern -- IllegalArgumentException
erzeugt.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.
|
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:
|
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.
|
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:
synchronized
-Angaben können in erbenden Klassen überschrieben werden. Das Verhalten der Superklasse bleibt hiervon jedoch unberührt.Ermitteln von Threadinformation:
Die Anzahl der laufenden aktiven Threads der aktuellen Gruppe kann durch activeCount()
ermittelt werden.
Informationen über jeden Thread der virtuellen Maschine liefern die Methoden getName()
und setName(String)
. Diese beiden Methoden erlauben die Abfrage, bzw. Definition eines Namens für jeden Thread. Bei der Erzeugung eines Threads wird automatisch ein Name durch die virtuelle Maschine vergeben. (SUNs JVM setzt hier die Zeichenkette Thread-i
, wobei i die laufende Nummer des Threads ist.)enumerate(Thread[])
befüllt den als Parameter übergebenen Array mit Verweisen auf die derzeit aktiven Thread Objekte.
Der aktuelle Thread kann durch die statische Methode currentThread()
ermittelt werden.
Außer durch Spezialisierung der Klasse Thread
kann durch Impelementierung des Interfaces Runnable
Nebenläufigkeit erzeugt werden.
Die Schnittstelle definiert ausschließlich die Operation run
. Auch die Umsetzung der Klasse Thread
nutzt in den vordefinerten Konstruktoren dieses Interface. Durch den Methodenaufruf start()
wird im zugehörigen nebenläufigen Objekt per O.run()
(O
ist hierbei vom Typ Runnable
) die Ausführung begonnen.
Hinweis: Dieser Anwendungsfall ist insbesondere für Applets von Bedeutung, da hier keine Spezialisierung von Thread
erfolgen kann, da zwingend von Klasse Applet
abgeleitet werden muß.
(Beispiel dazu)
Mehr zu Java Threads erfahren ... (eigene Vorlesung zum Thema)
![]() |
Applets sind Java-Programme die innerhalb einer Browserumgebung ausgeführt werden. Daher gelten für sie einige besondere Charakteristika, die sie von den bisher betrachteten Java-Applikationen unterscheiden:
Applet
abgeleitet sein.main
-Methode.init
und start
automatisch aus.Hinweis: Zu Testzwecken kann ein Applet auch durch einen Applet-Viewer, wie der u.a. in SUNs JDK enthalten ist, ausgeführt werden. Hierfür wird neben dem Applet selbst eine minimale HTML-Seite benötigt.
Die Klassenhierarchie von Applet
und ihre Klassen:
Panel
: einfachste Container-Klasse. Sie stellt Platz zum Einhängen beliebiger weiterer Komponenten zur Verfügung.Container
: Generischer Container des Abstract Window Toolkit (AWT), der andere AWT-Komponenten enthalten kann.Component
: Objekt das über eine graphische Repräsentation verfügt, die am Bildschirm angezeigt werden kann, und die die Möglichkeit zur Benutzerinteraktion bietet.Lebenszyklus eines Applets -- die zentralen Methoden der Klasse Applet
:
init()
start()
init()
-Methode durch den Browser aktiviert. Darüberhinaus wird diese Methode immer dann aufgerufen, wenn die beherbergende Web-Seite neu geladen wird.stop()
destroy()
stop()
durch den Browser aufgerufen, um die durch das Applet belegten Systemressourcen zurückzufordern.flush
-Methode für Images).Das Zustandsübergangsdiagramm zeigt die verschiedenen Status im Leben eines Applets.
Die Übergänge sind mit den auslösenden Ereignissen beschriftet. Ist kein Ereignis angetragen, so erfolgt der Übergang automatisch ohne weitere Bedingungen. In Klammern ist die jeweils ereignisauslösende Umgebung (AV
für Appletviewer und B
für Web-Browser) angegeben. Im Allgemeinen verhalten sich die beiden großen Browser gleich. Weicht das Verhalten eines Browers ab, so ist dies explizit angegeben (IE
kennzeichnet hierbei den MS Internet Explorer).
Weitere wichtige Methoden, die von den Superklassen von Applet
geerbt werden sind:
paint(Graphics)
update(Graphics)
Der Aufruf der Methode repaint()
erzeugt einen Aufruf an die update
-Methode der Komponente.repaint()
stellt die asynchrone Variante des direkten Aufrufes von paint
mit dem aktuellen Graphics-Objekt (paint( getGraphics() )
) dar. Die Methode garantiert das Neuzeichnen nicht, im Falle hoher Auslastung der virtuellen Maschine kann es auch unterbleiben.
Die benötigten (X)HTML-Strukturen:
Leider existieren derzeit verschiedene Varianten ein Java-Applet in eine Hypertextseite einzubinden.
Zunächst die „klassische“ Applet-Referenz:
<applet code="className" width="Breite in Pixel" height="Höhe in Pixel">
Zusätzlich können (gemäß HTML v4.01 Standard) noch weitere Attribute (Namen-Wert-Paare wie "code" oder "width") definiert werden:codebase
-- ermöglicht die relative Interpretation von Pfadnamen ausgehend vom in codebase definierten Katalogarchive
-- ZIP komprimiertes Archiv das Applet gesucht wird.alt
-- zusätzlicher erläuternder Text.name
-- eindeutige Benennung des Applets innerhalb der beherbergenden Seite.align
-- horizontale Ausrichtung auf der Seite.hspace
-- horizontaler Abstand des Applets zur Umgebung.vspace
-- vertikaler Abstand zur Umgebung.
Ferner existieren noch einige proprietäre Attribute, die jedoch nicht durch alle Browser unterstützt werden.
Parameterübergabe an Applets:
erfolgt durch Name-Wert-Paare der Form <param name="..." value="...">
, die in den Grenzen der Applet-Tags eingeschlossen sind.
<applet code="HelloWorldApplet.class" width="100" height="100">
<param name="color" value="green">
</applet>
Mit XHTML v1.0, dem Nachfolger des HTML v4.01 Standards, wurde die Applet-Syntax für veraltet (deprecated) erklärt, und nunmer ausschließlich die Object
-Variante zugelassen. Über die Änderung der Tag-Namen hinausgehend dürfte die Plazierung der Referenz auf die Java-Klassendatei, und die explizite Typangabe, als Parameter die augenfälligste Änderung sein.
Diese Syntaxvariante wird jedoch, obgleich Standard, noch nicht von allen derzeit gängigen Browservarianten unterstützt.
<object>
<param name="type" value="application/x-java-applet">
<param name="JAVA_CODE" value="HelloWorldApplet.class">
Hinweis: In älteren Referenzen findet sich teilweise noch die ursprüngliche HotJava-Syntax, welche ein app
-Tag definiert. Ferner enthält dieses die Referenz auf die Klasse als Attribut, jedoch ohne die Dateiextension .class
explizit anzuführen.
Das Hello World-Applet:
Das absolute Minimalapplet, es schreibt lediglich den bekannten Schriftzug, kann folgendermaßen umgesetzt werden:
|
Beispiel 66: Hello World als Java-Applet HelloWorldApplet.java
Interaktion mit der Ausführungsumgebung
Durch verschiedene vordefinierte Methoden und Schnittstellen kann jedes Applet mit seiner Ausführungsumgebung, dem Browser oder Applet Viewer, in Interaktion treten. Hierdurch können von Seiten des Applets auch Dienste wie Caching, Anzeigen einer Nachricht, Abspielen von Audiodateien der Ausführungsumgebung benutzt werden.
Einfachster Fall ist die Methode getAppletInfo
. Sie liefert eine Zeichenkette zurück, welche üblicherweise Informationen über den Autor, die Version und die Rechte am Applet beinhaltet.
Eine ähnliche Funktion erfüllt die Methode getParameterInfo()
. Sie liefert einen Array von dreielementigen String-Arrays zurück, welche die erlaubten Parameter näher beschreiben.
Mittels getParameter(String)
können die Werte jedes einzelnen Parameters ausgelesen werden.
|
Beispiel 67: Parametrisierung des bekannten HelloWorld-Applets HelloWorldApplet.java
Die Erweiterung des bekannten Beispielapplets zeigt den Einsatz der getAppletInfo
-Methode zur Rückgabe Applet-spezifischer Inhalte.
Zusätzlich wird der anzuzeigende Text und seine Position (=Parameter der Methode drawString
) über Parameter gesteuert, die durch die Methode getParameterInfo
in ihrer Funktion näher beschrieben sind.
Die Parameter werden aus der (X)HTML-Quelle durch die Methode getParameter
ausgelesen.
Die Schnittstelle AppletContext
erlaubt die aktive Beeinflussung der Ausführungsumgebung des Applets. Die Methode getAppletContext
liefert die aktuelle Context-Ausprägung zurück.
Jedes AppletContext
-Objekt stellt folgende Methoden zur Verfügung:
getApplet(String)
Applet
-Objekt zurück, das entsprechend der übergebenen Zeichenkette benannt ist.getApplets()
getAudioClip(URL)
AudioClip
-Objekt zurück.getImage(URL)
Image
-Objekt zurück.showDocument(URL)
und showDocument(URL, String)
showStatus(String)
|
Beispiel 68: Hello World Applet mit URL-Forward nach fünf Sekunden HWAURLForward.java
Das Beispiel erweitert das parametrisierte HelloWorld-Applet um eine URL-Weiterleitung nach fünf Sekunden. Die notwendige Zieladresse wird aus dem Parameter URL
geladen.
zugehörige HTML-Seite
Sicherheitskonzept von Applets:
Nachfolgende Tabelle faßt die Rechte und Beschränkungen von Applikationen und Applets, sowohl für die Ausführungsumgebung Web-Browser als auch im Appletviewer, zusammen.
![]() |
|
Das Applet openProperties
zeigt die Inhalte der abfragbaren Systemeigenschaften an.
Beim Versuch innerhalb eines Applets eine aus Sicherheitsgründen nicht zugleassene Funktionalität auszuführen wird ein Ausnahmeereignis-Objekt der Klasse AccessControlException
erzeugt.
Hinweis: Die dargestellten Einschränkungen stellen für manche Anwendungsfälle zu deutliche Restriktionen dar. Daher existiert mit den sog. signed Applets eine Möglichkeit einzelne Einschränkungen für bestimmte Applets gezielt zu öffnen.
Drei Komponenten wirken bei der Realisierung des Java-Sicherheitsmodells zusammen:
Abspielen von Audiodateien:
War diese Funktionalität bis zum Erscheinen der Java 2 Plattform ausschließlich Applets vorbehalten steht sie nun auch in Java-Applikationen zur Verfügung.
Generelles Vorgehen:
Laden einer Audiodatei mittels der Methodenfamilie getAudioClip
über eine gültige URL.
Im Anschluß kann das rückgegebene Objekt, welches die AudioClip
-Schnittstelle implementiert abgespielt werden. Java ab Version 2 unterstützt MIDI-Dateien sowie Audiodateien der Formate RMF
, WAVE
, AIFF
und AU
.
Zur Soundausgabe in Applikationen wurde die Klassenmethode newAudioClip(URL)
eingeführt, die eine Audiowiedergabe auch ohne Existenz eines Applet-Objekts ermöglicht.
Üblicherweise werden im Zusammenhang mit der Audiowiedergabe Threads verwendet um die Sounds mit mit anderen Ereignissen zu synchronisieren.
Anzeigen von Graphiken:
Bilder in den Formaten GIF
, PNG
oder JPEG
können durch die Methoden getImage
geladen werden.
Die Anzeige erfolgt durch drawImage(...)
.
Das GIF98a-Animationsformat wird ebenfalls unterstützt.
Anmerkung: Alternativ kann auch signaturgleiche Methode getImage
der Klasse java.awt.Toolkit
benutzt werden.
|
Beispiel 69: Ein einfacher Graphik-Viewer GfxViewer.java
Das Beispiel implementiert einen einfachen Graphik-Viewer für die beiden standardmäßig unterstützten Formate.
Es verwendet bereits interaktive Komponenten aus dem Abstract Windows Toolkit (AWT) (siehe init
- und actionPerformed
Methode. Die rudimentäre Funktionalität erschöpft sich darin, eine Graphikdatei aus dem aktuellen Verzeichnis (ermittelt per getDocumentBase()
zu laden, und im aktuellen Fenster anzuzeigen.
Zunächst wird das Bild per getImage
-Methode referenziert (graphicsName.getText()
gibt den im Textfeld eingegebenen Namen als String zurück).
Der Aufruf von repaint()
wirkt auf die gesamte Komponente. Erfolgt das Neuzeichnen, so wird die paint
-Methode des Applets aufgerufen. Der dort plazierte Methodenaufruf drawImage
auf dem Graphics
-Objekt zeichnet das Bild.
Durch Überschreiben der Methode imageUpdate
(ererbt von Component
, welches die Schnittstelle ImageObserver
implementiert) kann der Ladevorgang des Bildes verfolgt werden. Diese Methode wird wärend des Ladevorganges immer wieder automatisch aufgerufen.
Anmerkungen:
getImage
-Methoden sind asynchron ausgelegt, und kehren daher sofort nach ihrem Aufruf zurück, unabhängig davon ob das referenzierte Bild existiert.drawImage
-Methoden sind (ebenfalls) asynchron ausgelegt, und kehren sofort nach ihrem Aufruf zurück, das Laden des Bildes erfolgt ggf. in einem eigenen Thread.MediaTracker
stellt eine Implementierung zur Verfügung, welche es ermöglicht das Laden eines Bildes oder einer Audiodatei zu verfolgen.Animationen:
...sind prinzipiell nichts anderes als die schnelle Wiedergabe verschiedener Einzelbilder. Jedoch gilt es auf einige Randbedingungen Rücksicht zu nehmen:
repaint()
-Methode kann es zu Flimmer- oder Flackereffekten kommen.Das Beispiel zeigt die Realisierung einer simplen Uhr (Java-Date
-Objekt) als animiertes Applet.
|
Beispiel 70: Eine einfache Uhr Clock.java
Der notwendige Bildschirmneuaufbau wird in einen separaten niedrig priorisierten Thread verlagert.
Hinweis: Der neue Thread wird nicht, wie bisher per Ableitung von Thread
definiert und als spezialisiertes Objekt instanziiert. Das Applet implementiert die Runnable
-Schnittstelle; daher kann ein Objekt vom Typ des Applets einer Variable des Typs Thread
zugewiesen werden. Erst hierdurch werden die Thread-spezifischen Methoden, wie Prioritätssänderungen, verfügbar.
Die häufigst anzutreffende Realisierung graphischer Animationen ist die Einblendung sich leicht unterscheidender Bilder, wodurch ab einer gewissen Bildfrequenz der Eindruck kontinuierlicher Bewegung entsteht (Trickfilmeffekt).
Diese Animationsvariante kann leicht mit den bisher vorgestellten Möglichkeiten realisiert werden. Hierzu werden zunächst die benötigten Bilder vollständig geladen (z.B. getImage(String)
oder getImage(URL)
aus der Toolkit
-Klasse) und anschließend in schneller Folge am Bildschirm angezeigt (z.B. mit Methoden der drawImage(...)
-Familie).
Im Kern ist das Verfahren identisch zur Anzeige statischer Graphiken.
|
Beispiel 71: Analoge Uhr als Bildfolge AnalogousClock.java
Das Beispielprogramm zeichnet in einem eigenen Thread alle 200 Millisekunden den Bildschirm mit einem neuen Bild neu.
Zunächst werden durch die init
-Methode die benötigten zwölf Graphiken in einen Image-Array geladen. Zum Ausführungsbeginn (start
-Methode) wird zunächst die Fenstergröße des Applets (bei Ausführung im Applet-Viewer) auf die Dimensionen der ersten geladenen Graphik -- im Beispiel besitzen alle Graphiken dieselben Ausmaße, daher ist die Auswahl der Referenzgraphik unbedeutend -- gesetzt. Aufgrund der asynchronen Realisierung von getImage()
muß auf den Abschluß des Ladevorganges gewartet werden; andenfalls liefern die Methoden getHeight
und getWidth
mit -1
einen ungültigen Wert zurück.
Der niederpriore Thread animator
sorgt alle 200 Millisekunden für den notwendigen Bildschirmneuaufbau -- durch Aufruf der repaint
-Methode --, und schaltet die Graphiken fort, wodurch der Animationseffekt entsteht.
Bei dieser Anwendung fällt ein starker Flimmereffekt ins Auge. Dieses (unschöne) Verhalten ist oftmals bei graphischen Java-Applets und -Applikationen anzutreffen. Zur Behebung existieren einige Standardlösungsvarianten:
Die einfachste Lösung liegt im Überschreiben der von Component
ererbten Methode update()
.
Im Standardfalle löscht update()
den gesamten Bildschirmbereich und ruft im Anschluß paint()
zum vollständigen Neuzeichnen auf.
Im vorliegenden Beispiel ist jedoch das dem Neuzeichnen vorausgehende Löschen nicht notwendig, da die nachfolgende Graphik ohnehin die vorhergende vollständig überdeckt.
Daher kann die update
-Methode auf den reinen Aufruf von paint
beschränkt werden.
Das zusätzliche Codefragment ergibt sich als:
public void update(Graphics g) {
paint(g);
} //update()
ergänzter Quellcode
ergänztes Applet ausführen
Diese einfache Maßnahme hilft oftmals bei auftretenden Problemen, jedoch ist sie nur für den eng umrissenen Problemkreis anwendbar, in dem neue graphische Elemente die vorhergehenden vollständig überdecken. Andernfalls kann es zu Darstellungsfehlern kommen.
Daher kommt bei realen Problemstellungen zumeist double Buffering zum Einsatz.
Der Mechanismus des double bufferings macht es sich zu Nutze, daß auch im nicht sichtbaren Bereich des Bildschirms graphische Operationen vorgenommen werden können. Die Anwendung verläuft üblicherweise in zwei Schritten: zunächst wird der darzustellende Bildschirminhalt verdeckt vorbereitet, und im zweiten Schritt in einer einzigen -- im Allgemeinen sehr performant ausgeführten -- Kopieroperation in den sichtbaren Bereich übertragen.
Ablaufprinzip:
Image
-Objekts beliebiger Größe (gesteuert durch die beiden Übergabeparameter) mittels createImage(int, int)
.createImage(int, int)
ist in der Klasse Component
definiert, und wird an Applet
vererbt.getGraphics()
auf dem Image
-Objekt.paint
-Methode mit einer Methode aus der drawImage(...)
-Familie.Codeschablone:
Image im = createImage(100,100);
Graphics graph = im.getGraphics();
//Operationen auf graph
public void paint(Graphics g) {
g.drawImage(im, ...);
} //paint()
In paint()
werden die vorgezeichneten Image
-Objekte ohne weitere Veränderungen angezeigt.
Je nach Komplexität der Anwendung können so viele Image
-Objekte wie benötigt parallel gehalten und bei Bedarf angezeigt werden.
![]() |
Die Collection API liefert als Bestandteil der Java Standard API Hilfsklassen zum Umgang mit generischen Objektsammlungen.
Der Begriff Collection bezeichnet Datenstrukturen wie Listen, Stacks, Schlangen, Bäume und Mengen im mathematischen Sinne.
Das Collection Framework der Java-API wird aus den vorgegebenen Schnittstellen, den Operationen und ihrer definierten Semantik, den konkreten Implementierunge der wiederverwendbaren Datenstrukturen und der zugrundeliegenden Algorithmen gebildet.
Das wohl bekannteste Beispiel eines solchen Rahmens dürfte die C++ Standard Template Libraray (Abk. STL) und die Collection-Klassen der Programmiersprache SmallTalk sein.
Neben dem Collection Framework der Java API existiert mit der Generic Collection Library for Java (Abk. JGL) auch noch eine frei verfügbare Adaption der STL an Java.
Da Java bis zur Version 1.5 keine parametrische Polymorphie unterstützte sind diese Klassen auch (noch) in ihrer generischen Fassung umgesetzt. Ihr Einsatz unter Nutzung der Java Generics wird Gegenstand des nächsten Kapitels sein.
Um ihre Einsetzbarbeit möglichst universell zu gestalten erlauben alle Collection-Klassen die Verwaltung von Objekten des Typs Object
, dem gemeinsamen Basistyp aller Java-Objekte.
Durch diese Mimik geht die Typsicherheit verloren!
Das bedeutet, daß in jeder Collection ausschließlich Objekte des Typs Object
verwaltet werden können, die vor dem Einstellen in die Sammlung (implizit und automatisch) in diesen Typ konvertiert werden. Diese Typumwandlung ist als up-cast typsicher möglich, da alle Java-Klassen immer direkte oder transitive Subklasse von Object
sind. Bei der Entnahme von Collection-Elementen ist jedoch die Rekonstruktion des Ursprungstypen notwendig. Diese Typumwandlung ist als down-cast explizit anzugeben. Beim Konvertierungsversuch in den falschen Typ kann es hier zu einer ClassCastException
kommen.
Unter praktischen Gesichtspunkten bietet sich die Vorschaltung einer Typprüfung durch instanceof
an.
Andererseits schafft die Abkunft aller Java-Objekte von Object
auch erst die Voraussetzungen für ein generisches Collection Framework zur Verwaltung beliebiger Java-Objekte.
Hiermit sind im Speziellen drei Methoden der Klasse Object
gemeint:
boolean equals(Object)
false
zurück, da sich die Objekte in ihrer (für den Benutzer nicht sicht- und zugreifbare) Objektidentität unterscheiden.equals()
bildet eine Äquivalenzrelation auf Object
und ist daher reflexiv, symmetrisch und transitiv.hashCode()
int
-Wert als eineindeutige Kennung jedes Objekts. Identische Objekte (d.h. Objekte für die equals
den Wert true
liefert) haben identische Kennungen.int
-Repräsentation kann als Hash-Code verwendet werden.Bereits seit Vesion 1.0 des JDKs stehen die Klassen Bitset
, Dictionary
, Hashtable
, Stack
und Vector zur Verfügung.
Diese wurde mit dem JDK v1.2 um einige weitere Klassen und Schnittstellen ergänzt.
Mit Ausnahme von Vector
und Stack
wurden alle dargestellten Klassen und Schnittstellen mit der Java-2-Plattform eingeführt.
Grundlegende Konzepte:
Gemeinsam sind allen Collection-Varianten die basalen Operationen
Darüberhinaus bieten die verschiedenen Collection-Klassen auch Möglichkeiten zur Suche, Sortierung usw. an.
Interessante (Realisierungs-)Details einer jeden Collection-Klasse sind weiter:
Das Standard Collection Framework umfaßt folgende ausprogrammierte Klassen:
![]() |
|
Ein einführendes Beispiel:
Die Klasse Vector
ist dem „klassischen“, d.h. bereits mit JDK v1.0 eingeführten, Anteil der Collection-API zuzurechnen. Sie stellt im Grunde eine dynamische Arrayimplementierung zur Verfügung, die intern als statischer Array realisiert ist. Wie bei Arrays üblich erfolgt der Element-Zugriff durch einen ganzzahligen int
-Index.
Durch den Ausbau der Collection-API in der Java-2-Plattform wurde die Vector
-Implementierung nochmals überarbeitet und an die heute Struktur (namentlich die neu eingeführte Schnittstelle List
) adaptiert.
|
Beispiel 72: Vektor-Operationen VectorEx1.java
Das Programm liest solange von der Standardeingabe zeilenweise Zeichenketten ein, bis der String exit eingegeben wird. Jede eingelesene Zeichenkette wird -- mit der Methode add()
-- als Vector
-Element angelegt.
Nach Beendigung des Einlesevorganges werden die gespeicherten Vector
-Elemente in der Reihenfolge der Anlage ausgegeben. Dies geschieht per indiziertem Zugriff auf jedes Element mit der Methode get(int)
. Der Zugriff erfolgt nicht mehr, wie bei Array üblich per direkter Indizierung in eckigen Klammern, sondern jetzt über eine eigene Methode, die jedoch den gewohnten Sicherheitsmechanismus der ArrayOutOfBoundException
beim Zugriffsversuch außerhalb der Vector
-Grenzen beibehält.
Beispielablauf:
$java vectorEx1
this
is
a
simple
test
containing an
empty
line
exit
element no. 0: this
element no. 1: is
element no. 2: a
element no. 3: simple
element no. 4: test
element no. 5: containing an
element no. 6:
element no. 7: empty
element no. 8: line
Die Skalierbarkeit eines Vektor
-Objekts kann durch Konstruktor beeinflußt werden. Der erlaubt die Angabe zweier Freiheitsgrade, der initialen Kapazität und eines Wachstumsfaktors. Werden, wie im Beispiel, keine Parameter übergeben, so wird gegenwärtig (d.h. im JDK v1.3) ein Vector
der Größe zehn mit einem Wachstumsfaktor von Null erzeugt.
Wird durch die Einfügemethode add
die Aufnahmekapazität des Vector
s überschritten, so wächst er um den als capacityIncrement angegebenen Faktor, oder verdoppelt seine gegenwärtige Größe falls kein Wachstumsfaktor angegeben wurde. In beiden Fällen sind jedoch alle Vector
-Elemente in einen neu zu erzeugenden Array zu transferieren, wodurch es bei umfangreichen Vector
en zu Zeitverlusten kommen kann. Die in der gezeigten Implementierung benutzte Umsetzung durch System.arraycopy
läuft als native Methode jedoch sehr performant ab. Vor grösseren Einfügevorgängen bekannten Umfanges bietet sich jedoch die explizite Allokation des notwendigen Speicherplatzes durch Aufruf der Methode ensureCapacity(int)
an, die sicherstellt, daß mindestens die im Parameter übergebene Anzahl von Objekten ohne zusätzliche Allokations- und Kopiervorgänge in den Vector
eingestellt werden kann.
Der nachfolgende Codeausschnitt aus den Java-Quellen läßt das interne Verhalten von Vector
anschaulich werden.
package java.util;
public class Vector extends AbstractList implements List, Cloneable, java.io.Serializable {
protected Object elementData[];
protected int capacityIncrement;
protected int elementCount;
public Vector() {
this(10);
} //constructor
public Vector(int initialCapacity) {
this(initialCapacity, 0);
} //constructor
public Vector(int initialCapacity, int capacityIncrement) {
...
this.elementData = new Object[initialCapacity];
...
} //constructor
public synchronized Object get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData[index];
} //get()
public synchronized boolean add(Object o) {
...
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = o;
return true;
} //add()
private void ensureCapacityHelper(int minCapacity) {
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement) : (oldCapacity * 2);
if (newCapacity < minCapacity) {
newCapacity = minCapacity;
} //if
elementData = new Object[newCapacity];
System.arraycopy(oldData, 0, elementData, 0, elementCount);
} //if
} //ensureCapacityHelper()
public Enumeration elements() {
return new Enumeration() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
} //hasMoreElements()
public Object nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData[count++];
} //if
} //synchronized
throw new NoSuchElementException("Vector Enumeration");
} //nextElement()
}; //elements()
} //class Vector
Einen einfacheren Weg zur Traversierung eines Vector
s bietet die Schnittstelle Enumeration
an.
Ein solches Objekt wird durch die Methode elements()
zurückgegeben. Sie abstrahiert nochmals vom indizierten Zugriff, und stellt mit den Methoden hasMoreElements()
und nextElement()
generische Zugriffsprimitiven bereit, die so auch in anderen Collection-Klassen Anwendung finden.
Nebenbemerkung: Die Implementierung von elements()
stellt ein (schönes) Beispiel für die Verwendung anonymer innerer Klassen dar.
Bei Modifikationen durch nebenläufig ausgeführte Threads am zugrundeliegenden Vector
nachdem ein Enumeration
-Objekt erzeugt wurde, kann es zu Konsistenzproblemen kommen. Daher wird seit dem JDK v1.2 die Verwendung der Schnittstelle ListIterator
gegenüber der weiter angebotenen Enumeration
empfohlen. Ihre Implementierung ist fail-fast, d.h. sie erzeugen ein Ausnahmeereignis-Objekt der Klasse ConcurrentModificationException
im Falle nebenläufiger Modifikation. Zusätzlich erlaubt der Iterator
selbst die Entnahme von Elementen aus der zugrundeliegenden Sammlung.
Der zu einer Collection gehörige Iterator wird über die (von List
geerbte) Methode listIterator()
geliefert.
Vorwärtsreferenz: Beispiel zur Nutzung eines Iterator
s
Mengen im mathematischen Sinne (d.h. duplikatfrei und ungeordnet) lassen sich durch die Klasse HashSet
realisieren.
Mit den (ererbten) Methoden addAll
(Vereinigung), retainAll
(Durchschnitt), removeAll
(Differenz) können die grundlegenden Mengenoperationen realisiert werden.
|
Beispiel 73: Die grundlegenden Mengenoperationen HashSetEx1.java
Bildschirmausgabe:
hs1=[b, a, c]
hs2=[e, d, c]
UNION(hs1,hs2)=[b, a, e, d, c]
hs1 WITHOUT hs2 =[b, a]
INTERSECTION(hs1,hs2)=[c]
Symmtetric Difference(hs1,hs2)=[b, a, e, d]
Cartesian Product(hs1,hs2)=[ce, cd, cc, be, bd, bc, ae, ad, ac]
Das Beispiel zeigt neben Realisierungen der grundlegenden Mengenoperationen auch die Nutzung eines Iterator
s anstelle der veralteten Enumeration
.
Die von der abstrakten Basisklasse AbstractMap
abgeleiteten konkreten Klassen HashMap
, TreeMap
und WeakHashMap
stellen eine von der Collection
-Hierarchie losgelöste besondere Variante allgemeiner Object
-Sammlungen zur Verfügung.
Ihnen allen gemein ist die Speicherung eines eineindeutigen Zugriffsschlüssels, zusätzlich zu den Nutzinformationen der verwalteten Objekte.
Die Klasse HashMap
organisiert die abgelegten Objekte intern über eine Hashfunktion, TreeMap
über einen rot-schwarz-Baum.WeakHashMap
ist prinzipiell identisch zur HashMap
, jedoch werden Schlüssel-Wert-Paare entfernt, sobald die letzte externe Referenz auf den Schlüssel ungültig wird.
Das Beispielprogramm ermittelt die Auftrittshäufigkeit von einzelnen Worten oder Zahlen im Eingabestrom. Hierfür wird der bekannte StreamTokenizer
zur entsprechenden Eingabefilterung benutzt.
Grundgedanke des realisierten Algorithmus ist es, das Wort oder die Zahl als Schlüssel in der HashMap
zu nutzen, und die bisher ermittelten Vorkommen als Nutzinformation in einem Integer
-Objekt abzulegen.
|
Beispiel 74: Ermittelt die Auftrittshäufigkeit eines Wortes WordCount.java
Beispielablauf:
Die Datei testfile.asc:
the quick brown fox jumps over the lazy dog
1 2 3 123 1 2 X
$java wordCount < testfile
13 distinct words detected:
{X=1, lazy=1, fox=1, quick=1, 1.0=2, over=1, the=2, dog=1, 123.0=1, brown=1, 2.0=2, 3.0=1, jumps=1}
Die Positionierung eines Elements innerhalb einer der Hash-Sammlungen (HashMap
, HashSet
und Hashtable
) wird während der Ausführung der put
-Methode durch eine Hashfunktion, die auf die Methode hashCode
der Klasse Object
zurückgreift, bestimmt.
Beim Erzeugungsvorgang jedes Hash-...-Objekt kann der Speicherplatzbedarf und das Laufzeitverhalten über zwei Parameter beeinflußt werden. Die initiale Kapzität (engl. initial capacity) definiert die Anzahl der Einträge in der Hashtabelle, und der Lastfaktor (engl. load factor) den maximalen Füllungsgrad einer Hashtabelle bevor automatisch eine Kapazitätserweiterung durchgeführt wird.
Werden die beiden Parameter bei der Erzeugung nicht angegeben, so wird vorgabegemäß eine Datenstruktur mit Freiraum für elf Objekte, und einem Lastfaktor von 0.75 erzeugt. Mithin wird die erste Kapazitätserweiterung beim Einfügeversuch des neuten Elements durchgeführt.
Algorithmen auf Objektsammlungen vom Typ Array
... werden durch die Klasse Arrays
in Form statischer Methoden angeboten.
asList(Object[])
List
e fester Größe aus einem Array beliebiger Objekte.binarySearch(...)
Object
-Arrays eine Suche nach beliebigen Inhaltswerten zur Verfügung.equals(...)
fill(...)
sort(...)
Algorithmen auf Objektsammlungen vom Typ List
... werden durch die Klasse Collections
in Form statischer Methoden angeboten.
binarySearch(...)
List
implementiert) durch.AbstractSequentialLists
und deren Subklassen (in der Standard-API realisiert: LinkedList
) explizit von dieser Mimik aus, und realisiert für diese Sammlungstypen eine sequentielle Suche.copy(List, List)
Enumeration(Collection)
Enumeration
zur Traversierung zurück.Iterator
zurückliefern würde.fill(List, Object)
List
e durch das gegebene Object
.min(...)
und max(...)
List
e, ggf. unter Anwendung einer speziellen Vergleichsoperation.
Die Suche wird nach der Methode des sequentiellen Schwellenwertes durchgeführt.nCopies(Object)
List
e mit n Kopien eines bestimmten Objekts.reverse(List)
List
e um.reverseOrder()
Comparator
-Objekt zurück welches die Anordnung der List
e umkehrt.shuffle(List)
List
e.sort(...)
List
e nach einem modifizierten Mergesort-Verfahren.List
e in ein Object
-Array erzeugt, auf dem dann die sort
-Methode der Klasse Arrays
aufgerufen wird, die den Mergesort dann für Object
s implementiert.synchronizedCollection(Collection)
,
synchronizedList(List)
,
synchronizedMap(Map)
,
synchronizedSet(Set)
,
synchronizedSortedMap(SortedMap)
und
synchronizedSortedSet(SortedSet)
unmodifiableCollection(Collection)
,
unmodifiableList(List)
,
unmodifiableMap(Map)
,
unmodifiableSet(Set)
,
unmodifiableSortedMap(SortedMap)
und
unmodifiableSortedSet(SortedSet)
Anmerkung zur Verwendung der beiden Sortierverfahren Mergesort und Quicksort in den Klassen Arrays
und Collections
:
Beim ersten Hinsehen verwundert die Implementierung zweier verschiedener Sortierverfahren ein wenig, nicht zuletzt wegen des Wechsels des Sortierverfahrens in der Implementierung der sort
-Methoden der Klasse Arrays
. Wird dort doch für alle Sortieroperationen auf Primitivtypen-Arrays eine modifizierte Quicksort-Variante eingesetzt, jedoch für Object
-Arrays zu Mergesort übergegangen.
Dies liegt in der Natur der beiden Sortierverfahren begründet. Quicksort ist eines der bekanntesten und vermutlich auch am besten erforschtesten Sortierverfahren; mit guten Leistungsdaten im allemeinen Falle. Jedoch kann der Sortiervorgang im schlechtesten Falle bis zu n2 Schritte benötigen. Insbesondere für große n ist daher Mergesort -- mit seinen garantierten n log(n) Schritten effizienter.
Allerdings benötigt Quicksort deutlich weniger Speicherplatz (eigentlich nur den Stack durch die rekursiven Aufrufe), während Mergesort für seinen Ablauf stets den doppelten Speicherbedarf der zu sortierenden Datenstruktur für sich reklamiert.
Aus praktischen Gründen, nicht zuletzt wegen des uniformen Laufzeitverhaltens, wurde dem speicherplatzintensiveren Mergesort der Vorzug gegeben. Da in realen Anwendungen in den seltensten Fällen reine Primitivtypen-Arrays sortiert werden, nutzt der Anwender überwiegend die vorhandenen Mergesort-Implementierungen, die jedoch in (begründeten) Einzelfällen durchaus durch eigene Routinen ersetzt werden können.
![]() |
Die wirkungsmächtigste Neuerung der Java Version 1.5 bildet die Einführung der sog. Generics, welche das Feld der Template-basierten Metaprogrammierung und mithin die parametrische Polymorphie eröffnen.
Das sinnvollste Einsatzfeld dieses Mechanismus bildet die Anwendung auf die Vorhandenen Klassen der Collection-API zur Realisierung typsicherer Objektsammlungen.
Die allgemeine Syntax der Generics erfordert die Nachstellung, des durch spitze Winkelklammern eingeschlossenen Typnamen, nach dem Namen der so typisierten Sammlung. So wird eine Liste, die ausschließlich Objekte des Typs String
enthält als List<String>
definiert.
Als Resultat einer so definierten Sammlungsklasse wird bereits zum Übersetzungszeitpunkt die Konformität abgeprüft. Daher wird bei jedem Versuch Daten in eine solch konkretisierte Sammlung einzufügen geprüft, ob diese unter Beachtung der Typrestriktion kompatibel zum in der Deklaration angegebenen Inhaltstyp sind.
Konventionsgemäß erlaubt Java ausschließlich die Festlegung von Klassen als Inhaltstypen von Objektsammlungen; Primitivtypen sind von der Verwendung ausgeschlossen.
Das nachfolgende Beispiel zeigt die Definition einer Sammlung des Typs LinkedList
, deren Inhaltselemente auf Ausprägungen des Typs Integer
beschränkt sind.
|
Beispiel 75: Typisierung einer generischen Liste GenTest1.java
Das Beispiel zeigt die Definition einer typisierten LinkedList
, für die bereits zum Übersetzungszeitpunkt geprüft wird, daß ausschließlich Ausprägungen der Klasse Integer
der Liste hinzugefügt werden.
Diese Restriktion verhindert jedoch, vermöge der Nutzung des dynamischen Boxings nicht, daß Ausprägungen des Primitivtyps int
direkt übergeben werden, da diese für den Programmierer transparent in Objekte des Type Integer
umgewandelt werden.
Versuche Objekte oder Werte anderen Typs in die Liste aufzunehmen werden bereits zum Übersetzungszeitpunkt erkannt und als Fehler gemeldet.
Die Einführung typisierter Sammlungen bedingt auch die entsprechende Adaption der Hilfsklassen zur Traversierung. vor diesem Hintergrund zeigt das nachfolgende Beispiel die Typisierung eines Iterators. Als Konsequenz dieser Konkretisierung des Iterators liefert der Aufruf der Methode next
statt der generischen Object
-Instanz eine Ausprägung des Parametertyps String
.
|
Beispiel 76: Typisierung eines Iterators GenTest2.java
Neben der Anwendung auf bestehende Klassen des Collection-Frameworks können Generics auch für eigenerstelle Klassen eingesetzt werden.
Das Beispiel zeigt die Definition der Klasse TopTen
, welche eine Liste von Einträgen beliebigen Typs zusammen mit einer Plazierung gestattet. Der Inhaltstyp wird hierbei als Polymorphieparameter zum Konstruktionszeitpunkt der TopTen
-Objekte festgelegt. Innerhalb der Klasse wird zur Verwaltung der Daten eine TreeMap
eingesetzt, die entsprechend der Initialisierung des TopTen
-Objekts erzeugt wird um auch auf dieser Ebene die Typkonformität zu gewährleisten.
|
Beispiel 77: Anwendung von Generics für eigene Klassen GenTest3.java
Neben der Angabe von Klassen zur Typisierung einer generischen Aufzählung können, mit derselben Mächtigkeit, auch Aufzählungstypen angegeben werden. Das nachfolgende Beispiel zeigt eine Anwendung:
|
Beispiel 78: Aufzählungstypen als Polymorphieparameter EnumTest4.java
Zwar vermeidet der Einsatz der Generics die Anwendung unsicherer expliziter Typkonversionen fast völlig, jedoch ist ihre Notwendigkeit in Einzelfällen nach wie vor gegeben.
Das nachfolgende Beispiel zeigt einen solchen Fall. Es definiert eine als Vector
angelegte Sammlung von Objekten des Typs Person
. Gemäß der Substitutionsregeln können auch Spezialisierungen der Person
, mithin Ausprägungen der davon abgeleiteten Klasse Mitarbeiter
, typkonform der Sammlung hinzugefügt werden.
Da die Methode get
im allgemeinen Falle Ausprägungen der Klasse Object
und unter Einsatz der Generics Ausprägungen des Parametertyps (in diesem Falle: Person
) liefert würde die abgespeicherte Mitarbeiter
-Instanz lediglich als Ausprägung der Klasse Person
aus der Sammlung entnommen werden. Um eine korrekte Weiterverarbeitung (z.B. in Form des Zugriffs auf das Attribut personalnummer
) als Mitarbeiter
zu gewährleisten ist daher eine explizite Typkonversion notwendig.
|
Beispiel 79: Explizite Typwandlung trotz Generics GenTest4.java
![]() |
Mit der JDK-Version 1.1 wurde die Möglichkeit geschaffen zur Laufzeit Informationen über die verwendeten Informationsstrukturen zu gewinnen. Genaugenommen handelt es sich hier um Information über Information, die üblicherweise als Metainformation bezeichnet wird.
Bereits seit der Version 1.0 existierte mit der Klasse Class
ein Mechanismus, der Möglichkeit zur Ermittlung des Typs im Stile der aus C++ bekannten Runtime Type Identification bietet.
Der Begriff Laufzeittyp meint den konkreten Typen eines Attributes oder einer Variable (im Allgemeinen: einer referenzierten typisierten Speicherposition). Dieser Typ muß nicht zwingend über die gesamte Ausführungszeit mit dem zum Definitionszeitpunkt festgelegten identisch sein. Er kann sich durch typkonforme Neuzuweisung ändern.
Class
Zur Motivation: Bisher war es uns mit dem instanceof
-Operator nur möglich zu entscheiden ob ein gegebenes Objekt Ausprägung einer bestimmten Klasse ist. Hierfür mußte der Namen der Klasse zum Ausführungszeitpunkt des Operators bekannt sein.
Die Methode getClass()
welche, da von Object
geerbt, auf allen Java-Objekten zur Verfügung steht behebt dieses Manko.
Sie liefert ein Objekt der Klasse Class
zurück welches -- neben zahlreichen weiteren Informationen -- den Namen der Klasse zur Laufzeit verfügbar werden läßt.Class
ist im Paket java.lang
angesiedelt, und steht daher ohne explizite import
-Anweisung in allen Java-Programmen zur Verfügung. Zu jeder Klasse im System existiert je ein solches Class
-Objekt.
|
Beispiel 80: Ermittlung der Laufzeitklasse RefEx1.java
Bildschirmausgabe:
$java RefEx1
runtime class of variable p1=Person
runtime class of variable m1=Mann
runtime class of variable p1=Mann
Die Anwendung definiert neben der Hauptklasse drei weitere Klassen Person
, Mann
und Frau
. Sie sind in der Weise organisiert, daß Mann
und Frau
als Subklassen von Person
realisiert sind.
Zunächst werden Variablen vom Typ Person
und Mann
definiert und mit Ausprägungen der Definitionstypen initialisiert. Durch den Methodenaufruf getName()
auf dem durch getClass()
zurückgelieferten Class
-Objekt kann der Name der Laufzeitklasse im Klartext ermittelt werden. So liefert die Variable p1
welche auf eine Person
-Ausprägung verweist erwartungsgemäß den Namen dieser Klasse zurück; für die Variable m1
und die Klasse Mann
gilt dies analog.
Durch der Zuweisung des Mann
-Objekts m1
an die Variable p1
(vom Typ Person
) verändert sich deren Laufzeittyp zu Mann
.
Im vorangegangenen Beispiel wird ein Objekt zur Ermittlung der zugehörigen Laufzeitklasse benutzt. Ist der Klassenname bekannt, so kann durch den Methodenaufruf forName(String)
das zur Klasse mit dem übergebenen Namen gehörige Class
-Objekt ermittelt werden.
|
Beispiel 81: Ermittlung eines Class-Objekts über den Klassennamen RefEx2.java
Bildschirmausgabe:
$java RefEx2
Name of class:Person
Die Klasse Person
wird geladen, und per getName()
ihr Name -- erwartungsgemäß Person
-- ausgegeben.
Kann die Klasse nicht gefunden werden, so wird ein ClassNotFoundException
Ausnahmeereignis-Objekt erzeugt.
Um unnötigen Tippaufwand zu sparen exisistiert mit T.class
eine abkürzende Schreibweise für alle Javatypen T aus dem Typsystem.
Beispiel:
Class c1 = Person.class;
Class c2 = int.class;
Class c3 = Double[].class;
Hinweis: Aus historischen Gründen liefert die Methode getName()
für alle Array-Typen eine etwas unschöne abkürzende Syntax, die sich am im Class-File verwendeten Format orientiert:
Zunächst das Präfix [
für jede Array-Schachtelung, gefolgt von ...B
für byte
C
für char
D
für double
F
für float
I
für int
J
für long
Lclassname;
für eine Klasse oder SchnittstelleS
für short
Z
für boolean
.
so liefert (new int[3][4][5][6][7][8][9]).getClass().getName()
als Namen die Zeichenkette [[[[[[[I
zurück.
Die Erzeugung neuer Objekte aus einem existierenden Class
-Objekt erfolgt durch die Methode newInstance()
. Sie ruft intern den parameterlosen Vorgabekonstruktor der durch das Class
-Objekt vertretenen Klasse auf.
Die newInstance
-Methode unterstützt keine Aufrufe parametrisierter Konstruktoren. Für diesen Anwendungsfall sollte auf die Klasse Constructor
der (moderneren) Reflection API zurückgegriffen werden.
|
Beispiel 82: Erzeugung eines neuen Objekts mit der Methode newInstance RefEx.java
Das Programm gibt zweimal die Zeichenkette hello from person! am Bildschirm aus. Die Erzeugung des zweiten Person
en-Objektes (p2
) erfolgt durch die Kopplung der Methoden forName
und newInstance
. Letztere liefert eine Ausprägung der Klasse Object
zurück, weshalb -- um die Methode sayHello
der davon abgeleiteten Klasse Person
aufrufen zu können -- die explizite Typumwandlung erfolgen muß.
Ferner stellt die Klasse Class
weitere Methoden zur Untersuchung der Charakteristika definierter Klassen bereit:
getSuperclass()
java.lang.Object
, Schnittstellen, alle Primitivtypen und void
wird null
zurückgeliefert.getInterfaces()
isInterface()
true
falls das Class
-Objekt eine Schnittstelle ist, andernfalls false
.isInstance(Object)
instanceof
-Operator.isAssignableFrom(Class)
checkcast
; ohne jedoch bei Unmöglichkeit der Zuweisung ein Ausnahmeereignis-Objekt zu generieren.Wie bereits angedeutet wurde die Klasse Class
mit Einführung der Reflection API in der Java Version 1.1 stark erweitert. Sie schlägt -- über die Rückgabetypen der neu geschaffenen Methoden -- eine Brücke zu den Klassen des Reflection-Paketes java.lang.reflect
.
Die Reflection
-API ist als Subpaket von java.lang
organisiert. Ihre Klassen dienen lediglich zur Inspektion existierender Klassen einer Java-Laufzeit-Umgebung. Daher sind die Konstruktoren der von AccessibleObject
abgeleiteten Klassen, sowie der der Klasse Array
, als private
deklariert; Ausprägungen dieser Klassen können ausschließlich durch die virtuelle Maschine erzeugt werden.
Im Folgenden werden zunächst die Methoden zur Ermittlung verschiedenster Informationen am konkreten Codebeispiel vorgestellt.
Die verschiedenen Methoden zur Einflußnahme auf bereits erzeugte Metainformationsobjekte werden im Anschluß diskutiert.
Klassen und Schnittstellen werden durch die Reflection-API gleichbehandelt. Die Unterscheidung kann durch Methode isInterface
auf jedem Class
-Objekt getroffen werden.
Ferner stehen zur Verfügung:
getModifiers
int
-Wert, der die gesetzten Modifier Bit-codiert enthält. Zur Entschlüsselung der einzelnen Modifier sollten die Methoden der Reflection-Klasse Modifier
eingesetzt werden, um auch hinsichtlich zukünftiger Java-Spracherweiterungen stabilen Code zu produzieren.toString()
-Methode der Klasse Modifier
, welche die in der Java Language Specification definierte kanonische Repräsentation zurückliefert.getComponentType()
getDeclaredClasses()
Class
-Array zurück.getName()
getPackage
Package
. Bei Klassen im Vorgabepaket, d.h. ohne explizite package
-Definition, wird null
zurückgegeben.Der Zugriff auf Attribute und Attribut-spezifische Informationen -- in der Java-Terminologie Felder
genannt -- (wie Namen, Sichtbarkeitsbereich und Typ) wird durch die Methoden getField(String)
, für ein namentlich bekanntes Feld, und getFields()
für alle Attribute einer Klasse, realisiert. In beiden Fällen ist wird genau ein, bzw. ein Array von Objekten der Reflection-Klasse Field
zurückgeliefert.
Beide Methoden berücksichtigen dabei alle Attribute/Felder einer Klasse, einschließlich der von der Superklasse und deren Superklassen ererbten. Zur Ermittlung der direkt auf dem betrachteten Class
-Objekt definierten Attribute können Methoden getDelcaredField(String)
bzw. getDelcaredFields()
eingesetzt werden.
Verwendung im Beispiel
Field
-Objekte bieten Zugriff auf die verschiedenen deklarativen Charakteristika eines Attributs:
getModifiers()
.int
-Wert, der die gesetzten Modifier Bit-codiert enthält. Zur Entschlüsselung der einzelnen Modifier sollten die Methoden der Reflection-Klasse Modifier
eingesetzt werden, um auch hinsichtlich zukünftiger Java-Spracherweiterungen stabilen Code zu produzieren.toString()
-Methode der Klasse Modifier
, welche die in der Java Language Specification definierte kanonische Repräsentation zurückliefert.getType()
.Class
zurück, auch wenn die Primitivtypen nicht als (first class) Objekte realisiert sind.Class
-Objekt notwendig. isArray()
klärt ob es sich bei dem Attributtypen um einen Array-Typ handelt. Da Java mehrdimensionale Arrays als geschachtelte eindimensionale Pendants realisiert muß in einem solchen Falle per getComponentType()
weiternavigiert werden. Diese Methode liefert wiederum ein Class
-Objekt zurück.getName()
zurückgeliefert.getName()
.String
.Anmerkung: Zwar spricht die Java-Reflection-API hier konsequent von Methoden, bietet jedoch keine Zugriffsmöglichkeiten auf den Implementierungskörper einer Operation. Ermittelbar sind daher nur die Signatur, die Sichtbarkeitsmodifier sowie der Rückgabetyp und, falls existent, im Ausführungsverlauf ausgelößte Ausnahmeereignisse; mithin: die Operation.
Die Reflection-API trennt Konstruktoren und sonstige Operationen voneinander. Daher existieren mit getConstructor(Class[])
und getConstructors()
bzw. getMethod(String, Class[])
und getMethods()
jeweils eigene Methoden zu ihrer Ermittlung.
Zurückgeliefert wird jedoch in allen Fällen genau ein, bzw. ein Array von, Method
-Objekt(en).
getModifiers()
int
-Wert, der die gesetzten Modifier Bit-codiert enthält. Zur Entschlüsselung der einzelnen Modifier sollten die Methoden der Reflection-Klasse Modifier
eingesetzt werden, um auch hinsichtlich zukünftiger Java-Spracherweiterungen stabilen Code zu produzieren.toString()
-Methode der Klasse Modifier
, welche die in der Java Language Specification definierte kanonische Repräsentation zurückliefert.getParameterTypes()
Class
-Objekten, die die formalen Parameter der Operation repräsentieren.getReturnType()
.getExceptionTypes()
Class
-Objekten der deklarierten Exceptions der Operation.
|
Beispiel 83: Erfragen verschiedener Klassen-spezifischer Informationen durch die Reflection-API ShowClass.java
Bildschirmausgabe:
class TestClass extends java.lang.Object
{
//Constructors
TestClass();
//Inner Classes
class TestClass$Inner extends java.lang.Object
{
//Constructors
TestClass$Inner(TestClass);
//Inner Classes
class TestClass$Inner$InnerInner extends java.lang.Object
{
//Constructors
TestClass$Inner$InnerInner(TestClass$Inner);
//Attributes
int j; //int is a primitive type
final TestClass$Inner this$1; //TestClass$Inner is a class
} //end class TestClass$Inner$InnerInner
//Inner Interfaces
//Attributes
int i; //int is a primitive type
final TestClass this$0; //TestClass is a class
} //end class TestClass$Inner
//Inner Interfaces
abstract static interface TestClass$InnerInterface
{
} //end interface TestClass$InnerInterface
//Attributes
public TestInterface i1; //TestInterface is an interface
private TestClass c1; //TestClass is a class
protected int p1; //int is a primitive type
int[] ta1; //[I is an array
int[][] tam1; //[[I is an array
//Operations
private TestClass testc();
} //end class TestClass
Das Ausgabe enthält, erwartungsgemäß, die implizite Superklasse java.lang.Object
, welche durch den Übersetzer automatisch gesetzt wird, als explizite Angabe nach dem extends
-Schlüsselwort.
Die inneren Klassen erscheinen in ihrer JVM-internen Namensgebung mit dem trennenden Dollarsymbol $
. Auffallend ist die explizite Wiedergabe der this
-Variable und des automatisch generierten Konstruktors mit dem Typ der äußeren Klasse als Übergabeparameter.
Für Array-Typen erfolgt die Benennung ebenfalls gemäß der JVM-internen Konvention.
Neben den bisher vorgestellten Mechanismen zur Introspektion bestehender Strukturen kann die Reflection API auch zur Erzeugung neuer Objekte und Operation auf diesen eingesetzt werden. Hierfür stehen neben Methoden zum lesenden und schreibenden Zugriff auf Attribute auch Möglichkeiten zur Ausführung von Methoden auf den Objekten zur Verfügung.
Die Erzeugung neuer Objekte aus bestehenden Class
-Ausprägungen war bereits mit Methoden der Class
-Klasse möglich, sofern zur Objekterzeugung der parameterlose Standardkonstruktor verwendet wird.
Zur Instanziierung neuer Objekte unter Nutzung eines bestehenden parametrisierten Konstruktors ist die Verwendung der Reflection-Klasse Constructor
unabdingbar.
Der Aufruf eines parametrisierten Konstruktors vollzieht sich in drei Schritten:
Class
-Objekts für die neu zu erzeugende InstanzConstructor
-Objekts mit der getConstructor(Class[])
-Methode der Klasse Class
newInstance(Object[])
-Methode der Constructor
-Klasse
|
Beispiel 84: Erzeugung eines AWT-Rechtecks über einen parametrisierten Konstruktor mit der Reflection-API SampleInstance.java
Bildschirmausgabe:
Constructor: public java.awt.Rectangle(int,int)
Object: java.awt.Rectangle[x=0,y=0,width=12,height=34]
Das Beispiel spiegelt die drei Erstellungsschritte wieder:
Zunächst wird über den Namen (Aufruf: forName(...)
) das zur Klasse gehörige Class
-Objekt erfragt.
Die Methode getConstructor(...)
liefert das Konstruktorenobjekt welches die als Parameter übergebene Signatur aufweist.
Instanziiert wird das neue Objekt durch die Methode newInstance
, ausgeführt auf dem Constructor
-Objekt mit den definierten Übergabeparametern.
... geschieht über Objekte der Klasse Field
.
Für alle Primitivtypen existieren lesende Methoden, die einheitlich mit getT(Object)
signiert sind. T ist einer der Primitivtypen ist. Der Rückgabetyp ist jeweils T.
Werte von objektartigen Attributen können mittels get(Object)
erfragt werden; zurückgegeben wird hier eine Ausprägung von Object
.
Symmetrisch sind die schreiben Zugriffe als setT(Object, T)
realisiert. Mit set(Object, Object)
steht eine Methode zur Ablage objektartiger Attribute zur Verfügung.
Das nachfolgende Beispiel zeigt die Verwendung der Lese- und Schreibmethoden.
|
Beispiel 85: Attributzugriff mit Methoden der Reflection-Klasse Field AttributeAccess.java
Bildschirmausgabe:
i=42
i=42
i=50
Die Klasse Method
weist neben den bisher vorgestellten Funktionalitäten zur Inspektion auch mit der Methode invoke(Object, Object[])
einen generischen Mechanismus zur Ausführung beliebiger Methoden auf.
Das folgende Beispiel zeigt den Aufruf der überladenen Methode hello
über die invoke
-Methode der Reflection-API.
|
Beispiel 86: Ausführung zweiter Methoden durch die Reflection-API MethodExecution.java
Bildschirmausgabe:
simple hello!
hello stranger
Unter Einsatz der Reflection-API lassen sich generische Programme entwickeln, die auf beliebigen Typen operieren. Mit diesem Mechanismus läßt sich ein Verhalten vergleichbar der parametrischen Polymorphie der C++-Templates nachbilden.
|
Beispiel 87: Ein wachsender Array GrowableArrayTest.java
Bildschirmausgabe:
ia consists of 1 elements
ia consists of 2 elements
ib consists of 20 elements
ib consists of 22 elements
pa consists of 3 elements
pa consists of 4 elements
Das Beispiel implementiert dynamische, durch expliziten Methodenaufruf, wachsende Arrays.
Nach Ermittlung des Class
-Objekts (durch getClass()
) und des Komponententyps des Arrays (getComponentType()
) wird ein zweiter -- der neue -- Array durch die newInstance
-Methode der Klasse Array
erzeugt. Als Übergabeparameter wird der Komponententyp und die Länge des neuen Arrays übergeben. Die Länge des neuen Arrays entspricht der des Alten, um zehn Prozent, jedoch mindestens ein Element, erhöht.
Abschließend wird der Inhalt des alten in den neu erzeugten Array kopiert. Hierzu wird die sehr performant ausgeführte native Methode System.arraycopy
eingesetzt.
Durch die Konstruktion des Method
-Objekts und der invoke
-Methode läßt sich auch das aus C/C++ bekannte Verhalten der Funktionszeiger nachbilden.
Das Beispiel zeigt zweimaligen Aufruf derselben Methoden printTable
. Im Körper der Methode wird jeweils die als Parameter übergebene Methode ausgeführt.
Im ersten Aufruf wird das Quadrat der Zahlen von Eins bis Zehn gebildet, im Zweiten hingegen die Wurzel gezogen.
|
Beispiel 88: Verweise auf Methoden in Java MethodPointerTest.java
Bildschirmausgabe:
public static double MethodPointerTest.square(double)
1.0 | 1.0
2.0 | 4.0
3.0 | 9.0
4.0 | 16.0
5.0 | 25.0
6.0 | 36.0
7.0 | 49.0
8.0 | 64.0
9.0 | 81.0
10.0 | 100.0
public static strictfp double java.lang.Math.sqrt(double)
1.0 | 1.0
2.0 | 1.4142135623730951
3.0 | 1.7320508075688772
4.0 | 2.0
5.0 | 2.23606797749979
6.0 | 2.449489742783178
7.0 | 2.6457513110645907
8.0 | 2.8284271247461903
9.0 | 3.0
10.0 | 3.1622776601683795
![]() |
Das AWT stellt den wohl interessantesten Teil eines (interaktiven) Java-Programmes dar -- die Benutzerinteraktion durch eine graphische Oberfläche.
Ziel der AWT-Entwicklung war es, die verschiedenen graphischen Primitiven wie Buttons, Menüs etc. plattformunabhängig anzubieten.
Zur Realisierung des Look and Feel bieten sich vier verschiedene Ansätze an:
Das AWT bietet neben den graphischen Primitivoperationen zum Zeichnen von einfachen geometrischen Objekten wie Linien, Rechtecken, Kreisen; Fülloperationen und Methoden zur Textausgabe einen Mechanismus zur ereignisbasierten Ablaufsteuerung an, der es erlaubt auf externe Ereignisse wie Mauseingaben zu reagieren. Ferner bietet sie die weitegehend bekannten GUI-Grundelemente wie Fenster, Dialogboxen, Menüs, ... an. Zur Entwicklung komplexerer Anwendungen steht Funktionalität zur Graphikbearbeitung und Audiowiedergabe zur Verfügung.
Mit der Java Version 1.1 wurde das AWT vollständig überarbeitet, insbesondere die oftmals als Achillesferse praktisch verwendbarer Applikationen empfundene Ereignisbehandlung (engl. event handling). Seither verwirklicht auch Java das bereits von anderen Oberflächensystemen wie NextStep oder Windows NT bekannte Delegation Based Event Handling. Der Terminus beschreibt die Möglichkeit der AWT externe Ereignisse (Mausclicks, Fenster-Schließen, ...) an beliebige Objekte zur Behandlung weiterzureichen.
Ebenfalls mit der JDK-Version 1.1 wurde eine zweite Bibliothek zur Oberflächenprogrammierung vorgestellt: SWING. Sie geht deutlich über den in der AWT realisierten Funktionsumfang hinaus, und wird daher heute überwiegend zur Implementierung professioneller Anwendungen herangezogen.
Vorwärtsreferenz: Sie wird in Kapitel 3.2.8 Swing beschrieben.
Das gesamte AWT ist im Standard-API-Paket java.awt
zusammengefaßt. Es stellt die grundlegenden graphischen Primitive zur Verfügung:
List
Label
Choice
FileDialog
Canvas
Button
Checkbox
Als Menü-Komponenten stehen zur Verfügung:
Menu
CheckboxMenuItem
MenuBar
MenuItem
PopupMenu
Die dargestellten Komponenten dienen zunächst zum Aufbau der Benutzeroberfläche (buttons, Listen, etc.) oder zur Strukturierung der Fenster (scroll bars, menues, etc.). Die dritte Kategorie bilden die Zeichenbereiche.
Ausgangspunkt jeder AWT-basierten Applikation ist die -- bereits aus der Applet
-Hierarchie bekannte -- Container
-Struktur.
In Containern werden die verschiedenen graphischen AWT-Primitive zusammengefaßt; Container können hierarchisch strukturiert sein, d.h. weitere Container enthalten. Der oberste Container einer Applikation bildet gleichsam die sammelnde Ordnungsstruktur. Die AWT verfügt über vier vordefinierte Containertypen:
Dialog
FileDialog
ist ein Standard-Dialog zur Erfragung eines Dateinamens realisiert.Frame
Panel
Window
Anmerkung: Rückblickend mag es (anfänglich) verwundern, daß die Graphikwiedergabe in Applets ohne Kenntnis und Anwendung der Containerstruktur möglich war. Dies ist jedoch nur auf den ersten Blick der Fall. Zunächst stellt der Browser oder Appletviewer das Hauptfenster nebst den Menüleisten zur Verfügung -- insofern existiert ein Container zur Aufnahme des Applets bereits durch seine Ausführungsumgebung. Zusätzlich stellt das Applet selbst, durch seine Plazierung in der Vererbungshierarchie unterhalb der Klasse java.awt.Panel
, einen eigenständigen Container dar.
Ein weiterer Ausweis der Zielsetzung plattformunabhängigen Designs des AWT ist die verwirklichte Philosophie zur Plazierung der graphischen Elemente am Bildschirm. Während plattformspezifische GUI-Bibliotheken zumeist die angebotenen Primitive mit absoluten Koordinaten im Anwendungsfenster verankern verfolgt Java hier einen gegensätzlichen Weg.
Durch den Programmierer wird ausschließlich die Plazierung der Komponente im Verhältnis zu anderen Komponenten angegeben, die endgültige Ausrichtung am Schirm nimmt eine zusätzliche AWT-Einheit -- der Layout-Manager -- vor. Die Orientierung der Primitiven innerhalb einer Komponente erfolgt weiterhin durch absolute Positionierung.
Im AWT stehen folgende vordefinierte Layout-Manager zur Verfügung:
FlowLayout
BorderLayout
GridLayout
CardLayout
GridBag
Anmerkungen:
setLayout(LayoutManager)
mit dem Übergabeparameter null
aufgerufen, so wird kein Layoutmanager verwendet, sondern die Positionierung der graphischen Elemente vollständig dem Anwender überlassen.Frame
s durch mehrere Panel
s definiert, die unterschiedliche Layoutmanager nutzen.LayoutManager
lassen sich eigene Layout-Manager erzeugen.Ein erstes Fenster
|
Beispiel 89: Ein einfaches Fenster mit AWT AWTEx1.java
Bildschirmausgabe unter MS-Windows
Das Beispiel erzeugt einen Frame
mit dem Titel Einfaches Fenster.
Durch die, von Component
ererbte, Methode setSize(int, int)
wird dem leeren Frame
die Größe von 400 mal 300 Pixeln zugewiesen.
Zum Abschluß muß der Anzeigevorgang per setVisible(boolean)
(ebenfalls von Component
ererbt) explizit angestoßen werden.
Die Beiden Methoden setSize
und setVisible
greifen auf die visuellen Eigenschaften der Komponente zu, diese und weitere Methoden zu Modifikation der graphischen Erscheinung stehen auf allen Subklassen von Component
zur Verfügung.
setSize(Dimension)
und setSize(int, int)
setVisible(boolean)
setLocation(int, int)
setBounds(int, int, int, int)
setSize
und setLocation
, ändert Lage und Größe der Komponente.requestFocus()
transferFocus()
setEnabled(boolean)
setFont(Font)
setComponentOrientation(ComponentOrientation)
LEFT_TO_RIGHT
und RIGHT_TO_LEFT
setCursor(Cursor)
setBackground(Color)
setForeground(Color)
Auf Ausprägungen der Klasse Frame
sind ferner verfügbar:
setState(int)
Frame.NORMAL
(Vorgabe) und Frame.ICONIFIED
(verkleinert) um.setIconImage(Image)
setMenuBar(MenuBar)
setResizeable(boolean)
setTitle(String)
Bei der Ausführung fällt zunächst die Existenz der plattformüblichen Standardmenüs auf. Sie werden automatisch durch die Ausführungsumgebung zur Verfügung gestellt und mit Funktionalität versehen.
Jedoch zeigt sich, daß es nicht möglich ist das Fenster mit den üblichen Betriebssystem-spezifischen Methoden (Close-Button, Auswahl des entsprechenden Menüs, Tastenkombination, etc.) zu schließen. Erst der Abbruch des Prozesses der virtuellen Maschine terminiert die Applikation.
Denn anders als die üblichen Vorgabeaktionen ist das hierfür erforderliche Applikationsverhalten durch den Anwender selbst bereitzustellen.
Im Allgemeinen erfolgt die Kommunikation zwischen einer graphischen Oberfläche und dem ausführenden Betriebsystem in Form von Nachrichten, die zwischen diesen beiden Kommunikationspartnern ausgetauscht werden.
Das Betriebssystem unterrichtet die Applikation über den Eintritt bestimmter Ereignisse (engl. event), hierzu zählen u.a. Mausbewegungen und -klicks, Tastaturanschläge sowie alle Fensteroperationen.
Am Nachrichtenverkehr sind prinzipiell drei Objekte des Systems beteiligt: Die Ereignisquelle, das Objekt bei dem das Ereignis eintrat (z.B. im Falle eines Mausklicks: der entsprechende Button); der Ereignisempfänger, ein Objekt das auf das eingetretene Ereignis reagiert; die Nachricht selbst, in objektorientierten System als eigenständiges Objekt realisiert, welches das ausgelöste Ereignis näher beschreibt.
Dieser Kommunikationsmechanismus ist als Delegation Based Event Handling bekannt, da jedes Ereignis an eine konkrete Instanz delegiert wird, die nachfolgend die Behandlung übernimmt. Es bietet zwei entscheidende Vorteile:
Die Abbilung zeigt eine Auswahl der verschiedenen Ereignistypen. Wie dargestellt sind alle spezifischen Ereignisse von der Klasse EventObject
abgeleitet. Neben den AWT-Ereignissen existiert eine Fülle weiterer Ereignistypen, die in den verschiedenen spezifischen Paketen (wie java.beans
, javax.sound
, etc.) untergebracht sind.
Durch die Superklasse EventObject
wird auch die an alle Unterklassen vererbte Methode getSource()
definiert, welche zu jeder EventObject
-Ausprägung die auslösende Instanz liefert.
Unterhalb der Klasse ComponentEvent
sind die low-level-Ereignisse eingeordnet, die auf der Ebene einer visuellen Komponente aufteten können.
Die Ereignisse ActionEvent
, ItemEvent
und TextEvent
werden als semantische Ereignisse bezeichnet, da sie nicht an ein konkretes visuelles Element gebunden sind.
Zur Verarbeitung der verschiedenen Nachrichtentypen muß der Empfänger eine Reihe von Methoden implementieren, die durch entsprechende Schnittstellen definiert sind. Die Abbildung stellt eine Auswahl der angebotenen Schnittstellen nebst den bereits angebotenen Implementierungen dar.
Die Reaktion auf AWT-Ereignisse wird durch sog. event handler übernommen; diese Objekte müssen die Benachrichtigung im Falle des Auftretens eines Ereignisses explizit anmelden.
Die Schritte im Einzelnen:
Somit kann auch das vorangegangene Beispiel entsprechend erweitert werden, um die noch ausstehende Reaktion auf das Beenden-Ereignis zu verwirklichen.
|
Beispiel 90: Ein einfaches Fenster mit AWT AWTEx2.java
Die Applikation erweitert das bisherige Beispiel um eine Adapter-Klasse, welche auf Fensterereignisse reagiert. Hierzu wird eine Ausprägung der Adapter-Klasse WindowClosingAdapter
als Window Listener mit der Methode addWindowListener(WindowListener)
registriert.
Die Adapter-Klasse erweitert die bestehende API-Klasse WindowAdapter
und implementiert damit die Schnittstelle WindowListener
. Sie definiert die überschriebene Methode windowClosing(WindowEvent)
, die bei auftreten des Ereignisses automatisch zur Verarbeitung aufgerufen wird.
Nach Ermittlung des Ereingis-sendenden Fensters per getWindow()
wird dieses Fenster zunächst ausgeblendet (setVisible(false)
) und im Anschluß zerstört (dispose()
).
Danach terminiert die Applikation die virtuelle Maschine.
|
Beispiel 91: Event-Handling AWTEx3.java
Bildschirmausgabe
Die Applikation definiert, unter Verwendung des Flow
-Layouts, zwei Beschriftungsfelder (labels) mousePos
und counterVal
sowie eine mit add one!
beschriftete Schaltfläche.
Anmerkung: Die Verwendung des Layoutmanagers ist zur Verarbeitung der Maus-Events zwingend notwendig!
Vor Anzeige des Applikationsfensters werden vier Ereignis-Handler registriert: Der bekannte WindowClosingListener
zur Umsetzung des Schließen-Ereignisses, der MouseMotionListener
zur Verfolgung der Mausbewegungen im Hautfenster sowie zwei MouseListener
die auf Mausereignisse -- außer Bewegungen -- auf der definierten Schaltfläche und dem Haupfenster selbst reagieren.
Die Umsetzung des WindowClosingListeners
ist gegenüber dem vorhergehenden Beispiel unverändert.
Innerhalb der Klassen counterClicked
und windowClicked
wird jeweils die Methode mouseClicked
der Schnittstelle MouseListener
überschrieben, sie wurde von der Superklasse MouseAdapter
geerbt. Wird ein Mausclick auf den Button registriert, so wird eine Methode der Hautptklasse awtEx3
aufgerufen, welche die Beschriftung des Textfeldes entsprechend verändert. Beim Mausklick ist es in der vorliegenden Implementierung unentscheident, mit welcher Maustaste dieser erfolgte. Eine nähere Analyse, beispielsweise hinsichtlich der gedrückten Taste, des aufgetretenen Ereignisses kann über Attribute der Klasse MouseEvent
erfolgen.
Die Spezialisierung der Klasse MouseMotionAdapter
realisiert die Methode mouseMoved(MouseEvent)
. Sie wird jeweils bei Änderung der Mauskoordinate aufgerufen. Die beiden Koordinaten werden innerhalb des Methodenkörpers mittels der angebotenen API-Methoden extrahiert und an die Hauptklasse zur Anzeige übergeben.
Bei näherer Betrachtung der verschiedenen Event-Handler des vorhergehenden Beispiels fällt deren immergleiche Grundstruktur auf: Zunächst das Erben von einer passenden Adapterklasse um eine oder mehrere Methoden zu überschreiben. Im konkreten Beispiel wird sogar zweimal dieselbe Adapterklasse überschrieben; jedoch mit unterschiedlichem Verhalten.
Die durch die Spezialisierung entstehenden Klassen werden in allen Fällen zur Erzeugung genau eines Objekts -- des jeweiligen Listeners -- herangezogen; eine sonstige Weiter- oder Wiederverwendung in der Applikation liegt nicht vor, und ist unter Berücksichtung des Klassendesigns auch für die Zukunft nicht anzunehmen.
Nimmt man die beiden Randbedingungen -- Vererbung und Überschreiben ererbter Methoden und keine Wiederverwendung -- zusammen, so offenbart sich das Sprachmittel der anonymen inneren Klassen als wesentlich adäquater zur Lösung der vorliegenden Problemstellung.
Unter Nutzung dieses Mechanismus ergibt läßt sich der Code des Beispiels wie folgt modifizieren:
|
Beispiel 92: Das Beispiel AWTEx3 unter Nutzung anonymer innerer Klassen AWTEx3i.java
Der entstehende Code wird deutlich kompakter, und lokalisiert zusätzlich die Handlermethoden in den Registrierungsaufrufen.
Da anonyme innere Klassen (naturgemäß) keine Konstruktoren besitzen können wurden die Methoden entsprechend modifizert.
![]() |
|
Das AWT bietet die auch von anderen graphischen Benutzerschnittstellen bekannten Menüprimitive und -Grundfunktionen an. Darunter verschachtelete Menüs (Submenüs), Short-Cuts, frei plazierbare Kontextmenüs, etc.
Die Abbildung zeigt die verschiedenen Primitive und ihre Beziehungen zueinander.
Die abstrakte Klasse MenuComponent
bildet die Wurzel der Menüklassenhierarchie.
Ein MenuBar
stellt eine Zusammenfassung von Menüs dar. Objekte diese Klasse bilden den Ausgangspunkt einer Menüstruktur.
Innerhalb eines MenuBar
s können beliebig viele MenuItem
-Ausprägungen organisiert werden. Ein MenuItem
ist ein beliebiger Eintrag eines Menüs. Hierbei kann es sich um einen anklickbaren Menüeintrag oder ein weiteres Menü
handeln, welches sich bei Mausberührung öffnet.
Üblicherweise werden konkrete Menüeinträge in Menüs zusammengefaßt. Dem Menü selbst entspricht die AWT-Klasse Menu
. Jedes Menu
-Objekt kann aus weiteren MenuItem
s bestehen. Mithin kann ein Menü Submenüs oder direkte Menüeinträge in beliebiger Reihenfolge enthalten.
Als Spezialisierung der durch die Klasse MenuItem
definierten Menüeinträge stehen umschaltbare Menüpunkte durch die Klasse CheckboxMenuItem
zur Verfügung. Gegenüber den herkömmlichen Menüeinträgen verfügt dieser Typ über ein zusätzliches -- in der visuellen Darstellung plattformabhängig variierendes -- Symbol welches den Eintragszustand „aktiviert“ oder „deaktiviert“ anzeigt.
Menüleisten-unabhängige Kontextmenüs werden durch die Klasse PopupMenu
realisiert. Sie sind frei plazierbar, verhalten sich jedoch ansonsten wie die bekannten Menüs.
Neue (klickbare) Menüeinträge werden als Objekte der Klasse MenuItem
erzeugt. Die dem Konstruktor übergebene Zeichenkette legt den späteren angezeigten Eintrag im Menü fest. (Verwendung im Beispiel).
Die Abbildung stellt einen Ausschnitt der Menüstruktur der Beispielapplikation dar:
Ausgangspunkt der Menüerzeugung ist immer ein Objekt der Klasse MenuBar
. Durch den parameterlosen Konstruktor wird eine leere Menüleiste erzeugt, die als Container zur Aufnahme der weiteren Menükomponenten dient. (Verwendung im Beispiel).
Die einzelnen Menüs werden durch Objekte der entsprechenden Klasse Menu
repräsentiert. Der in der Menüleistete aufgeführte Menüname wird als Zeichenkettenparameter (Klasse java.lang.String
) übergeben. (Verwendung im Beispiel.)
Durch die Methode add(MenuItem)
werden Menüeinträge oder ganze Menüs -- allgemein: Subklassen von MenuItem
-- in eine bestehende Menüleiste eingehängt. (Verwendung im Beispiel).
Zusätzlich kann dem Konstruktor eine Instanz der Klasse MenuShortcut
übergeben werden. Hierdurch wird die in GUIs übliche direkte Anwahlmöglichkeit einzelner Menüeinträge über Tastenkürzel realisiert. (Verwendung im Beispiel).
Zur Erzeugung von CheckboxMenuItems
, Menüeinträgen die ihren zweiwertigen Zustand automatisch darstellen, muß ein Objekt der Klasse CheckboxMenuItem
erzeugt und in ein bestehendes Menü eingehängt werden. (Verwendung im Beispiel).
Die Variante der Pop-up Menüs wird durch die gleichnamige Klasse PopupMenu
erzeugt. Im Unterschied zu den herkömmlichen Menüs wird dieser Menütyp nicht in der Menüleiste, sondern dem beherbergenden Fenster direkt, durch die dortige add
-Methode, eingehängt. (Verwendung im Beispiel).
Die Ereignisbehandlung für Menüstrukturen folgt dem in der Übersicht Event-Handling im Überblick gegebenen Schema.
Für jeden Menüeintrag vom Typ MenuItem
muß ein Objekt vom Typ ActionListener
über die Methode addActionListener(ActionListener)
der Klasse MenuItem
registriert werden. Dieser muß die Methode actionPerformed(ActionEvent)
überschreiben. Dort findet sich der Code, welcher bei Eintreten des Ereignisses (Mausclick auf Menüeintrag) abgearbeitet wird. (Verwendung im Beispiel).
Menüpunkte der Klasse CheckboxMenüItem
werden durch Objekte die die Schnittstelle ItemListener
implementieren überwacht. Tritt die Zustandsänderung ein, so wird die Methode itemStateChanged(ItemEvent)
aufgerufen. (Verwendung im Beispiel).
Beispiel 93: Beispiel einer Menüstruktur AWTEx4.java
Das Menü Hello der Beispielapplikation besteht aus drei Menüeinträgen -- say hello, enable say hello und disable say hello. Für den ersten Menüpunkt ist das Tastaturkürzel CTRL+H
definiert (Definition im Code). Bereits zum Startzeitpunkt der Applikation ist der Menüpunkt enable say hello deaktiviert, da der entsprechende Menüpunkt vorgabegemäß aktiviert ist. Die Instanzenmethode setEnabled
der Klasse MenuItem
erlaubt den Wechsel zwischen den beiden Zuständen „aktiviert“, mit normaler visueller Darstellung des Menüpunktes, und „deaktiviert“, mit entsprechender aus-gegrauter Darstellung. Standardmäßig sind alle Menüeintrage nach ihrer Ereugung aktiviert, daher muß die gewünschte Deaktivierung explizit erfolgen. (Stelle im Code).
Wird der Menüpunkt disable say hello angewählt, so zieht dies die Deaktivierung des Menüpunktes say hello und gleichzeitige (Re-)Aktivierung von enable say hello nach sich.
Die Selektierung des Menüpunktes enable say hello vollführt hingegen die duale Operation dazu, Aktivierung der Menüpunkte say hello und disable say hello. Aus diesem Grunde teilen sich beide Menüpunkte dieselbe Ereignisbehandlungs-Routine. Als Konsequenz dieser Forderung wird die Initialisierung der beiden ActionListener
mit demselben Objekt notwendig (Codestelle)-- daher kann die Umsetzung an dieser Stelle nicht mehr als anonyme innere Klasse realisiert werden, sondern erfolgt als „normale“ innere Klasse (Codestelle).
Die Anwahl des Menüpunktes say hello zeigt für eineinhalb Sekunden den Schriftzug Hello! am Bildschirm an. (Ereignisbehandlungsroutine).
Das zweite Menü der Menüleiste, mit Window betitelt, enthält ein Untermenü Move und eine Ausprägung von CheckboxMenuItem
.
Die Ereignisbehandlung für Submenüeinträge erfolgt analog der auf höheren Ebenen; durch Definition des entsprechenden ActionListener
-Instanz. (Implementierung der Ereignisbehandlugn des Menüpunktes up). Bei Auswahl dieses Menüpunkts wird das Fenster, durch mehrmalige Änderung der Bildschirmkoordinaten in die linke obere Bildschirmecke verschoben.
Der Eintragstyp CheckboxMenuItem
erfordert eine Behandlungsroutine vom Typ ItemListener
. Die Anzeige des Zustands wird durch die AWT automatisch vorgenommen, und bedarf keiner Anwenderinteraktion. (Behandlungsroutine des Menüeintrages light on/off). Der Menüpunkt ändert den Bildschirmhintergrund je nach Zustand von hell nach dunkel oder umgekehrt.
Das dritte dargestellte Menü wurde als Standard-Hilfe-Menü deklariert. Existiert bereits ein Menü dieser Eigenschaft, so wird es durch das neu definierte ersetzt.
Die Aufnahme in die Menüleiste erfolgt nicht durch die bekannte add
-Methode, sondern über den separaten Aufruf setHelpMenu(Menu)
(Codestelle).
Zusätzlich definiert die Applikation ein Kontextmenü welches die Funktionalität der Menüeinträge enable say hello und disable say hello vereinigt. Auch es nutzt die Ereignisbehandlungsroutine der beiden genannten Menüeinträge mit. (Codestelle).
Die Aktivierung und Anzeige am Punkt der Aktivierung muß durch den Anwender selbst realisiert werden. Hierfür ist ein enstprechender MouseListener
umzusetzen. Die Methode show(Component, int, int)
erlaubt hierfür die Übergabe von Koordinaten, an denen das Kontextmenü aufgeklappt wird.
Editierbare Textfelder für Benutzereingaben stellt die Klasse TextField
bereit.
Zur Reaktion auf Tastenanschläge kann -- wie in Übersicht dargestellt -- jedes Objekt genutzt werden, das die von Component
ererbte Schnittstelle KeyListener
implementiert.
Beispiel 94: Ein einfacher Euro-Umrechner AWTEx5.java
Bildschirmausgabe:
Die Applikation nutzt das GridLayout
zur Ausrichtung der vier visuellen Komponenten. Hierfür wird der entsprechende Layoutmanager dahingehend parametrisiert, daß er ein quadratisches Layout mit jeweils zwei horizontalen und vertikalen Einträgen erlaubt. (Codestelle).
Die Beschriftungen der Spalten sind als Label
s realisiert.
Zum Erzeugungszeitpunkt wird den Text-Feldern ein Startwert, und die horizontale Ausdehnung von sieben Zeichen zugewiesen. (Codestelle).
In den (anonymen inneren) Klassen zur Ereignisbehandlung, wird die Methode keyTyped(KeyEvent)
überschrieben. Sie wird nach Abschluß des Tastendrucks aufgerufen.
In der Implementierung dieser Methode wird die tatsächliche Umrechung vorgenommen, und daß Umrechnungsergebnis im jeweils anderen Feld ausgegeben. (Codestelle).
Hinweis: Eine Behandlung des möglicherweise generierten NumberFormatException
-Ausnahmeereignisses findet aus Übersichtlichkeitsgründen nicht statt.
Ausprägungen der Klasse Dialog
stellen eine Möglichkeit zur Realisierung
einfacher Ein- und Ausgabefenster dar. Diese Art Fenster eignet sich besonders für kurze Anfragen, oder Meldungen, an den Anwender.
Beispiel awtEx4 enthält eine solche Nachrichtenbox. Dialogfenster können, im Gegensatz zu den bekannten Applikationsfenstern, modal sein. Dies bedeutet, daß ein solches Fenster nicht durch den Anwender in den Hintergrund versetzt werden kann; es erzwingt eine Reaktion -- zumeist in Form der Bestätigung einer Meldung o.ä. -- bevor die Applikation fortfährt.
![]() |
Mit dem JDK v1.1 wurde als zusätzliche API zur Erstellung von graphischen Oberflächen Swing
vorgestellt. Bis zur aktuellen Version 1.3 ist diese API weiterhin Bestandteil des Extension Paketes, und steht daher nicht auf allen Plattformen zur Verfügung. Des weiteren können sich noch zukünftige Änderungen an den derzeit publizierten APIs ergeben, so daß bestehender Code überarbeitet werden muß.
Folgende Überlegungen führten zur Entwicklung von Swing:
Daher wurde mit den Java Foundation Classes, deren Bestandteil Swing ist, versucht eine echte Alternative zum bestehenden -- und weiter angebotenen -- AWT zu schaffen. Dabei erhöht Swing massiv die Anzahl der angebotenen visuellen Komponenten, und fügt einige sehr mächtige Primitive wie beispielsweise zur Darstellung von Bäumen hinzu.
Anders als die AWT setzt Swing nicht auf den Möglichkeiten des zugrundeliegenden GUI-Systems auf, sondern stellt selbst die gesamte Verwaltung und Verarbeitung der angebotenen Primitven zur Verfügung. Als Folge dieses Ansatzes realisiert Swing ein eigenes look-and-feel, das auf allen unterstützten Plattformen unverändert präsentiert wird. Mit diesem -- Metal genannten -- Layout entsteht erstmals ein typisches Aussehen nativer Java-Applikationen.
Technisch ist Swing mit den seit JDK v1.1 angebotenen leichtgewichtigen Komponenten, den Java Beans, realisiert. Die Realisierung graphischer Primitivoperationen erfolgt daher nicht mehr durch Operationen des zugrundeliegenden GUI-Systems, sondern durch die angebotenen Swing-Komponenten selbst.
Dies führt dazu, daß die gesamte Swing-API ohne native Methoden -- vollständig in Java codiert -- realisiert werden konnte. Hieraus ergeben sich weitere Vorteile in der Anwendung, insbesondere in der Fehlersuche.
Jedoch führt die verfolgte pure Java-Implementierung auch zu Problemen. So erhöht sich durch den de-facto emulierenden Ansatz der Speicherplatzbedarf, da nicht mehr auf die evtl. durch die Plattform angebotenen Routinen zurückgegriffen wird. Flankierend sinkt die Ausführungsgeschwindigkeit durch die gestiegene Menge an auszuführendem Java-Code.
Anmerkung:
Aus rein praktischen Erwängungen sprechen derzeit noch zwei weitere Punkte gegen den breiten Swing-Einsatz: zunächst der Reifegrad und die Entwicklungsstabilität. Als Bestandteil des Java Extension Paketes ist Swing explizit als experimenteller Bestandteil der derzeit verfügbaren Java-API gekennzeichnet. In der verbreiteten Implementierung finden sich noch kleinere und größere Fehler und Ungereimtheiten, die gegen die Verwendung in Produktivappliaktaionen sprechen. Wie bei allen anderen Bestandteilen des javax-Paketes auch, behält sich SUN explizit das Recht vor Schnittstellen und Funktionalität ohne Ankündigung zu verändern oder aus dem Angebot herauszunehmen. Daher verbietet sich, unter Berücksichtigung einer zukünftigen Pflege des entstehenden Applikationscodes, die Nutzung dieser Klassen schon fast.
Ergänzend sei noch erwähnt, daß seitens der aktuell verfügbaren Web-Browser noch keine Swing-Unterstützung umgesetzt ist. Dieses Manko läßt sich zwar durch die zusätzliche manuelle Installation des Java-Plugins beheben, verlangt dem Anwender jedoch einen zusätzlichen Installationsvorgang ab.
Swing verwirklicht durchgängig das von T. Renskaug initiierte Model-View-Controller-Konzept. Dessen hervorstechendstes Kennzeichen die Separierung des Codes in drei verschiedene Verwendungskategorien ist:
Die gesamte Verarbeitungslogik ist dabei in der Model-Klasse zentralisiert. Zu jedem Model können dabei gleichzeitig verschiedene View-Klassen existieren.
Swing reduziert diesen Mechanismus auf zwei verschiedenen Klassen-Typen. Hierbei wird die View- und die Controller-Komponente zu einer Einheit verschmolzen. Das entstehende Design wird Model-Delegate-Prinzip genannt.
|
Beispiel 95: Eine erstes Swing-Applikation SwingEx1.java
Bildschirmausgabe:
Diese Swing-Applikation definiert ein einfaches Fenster mit drei Buttons.
Deutlichstes Kennzeichen der Nutzung von Swing ist das Präfix J
vor den bekannten AWT-Komponentennamen. So erweitert die Applikation das Swing-Analogon der AWT-Klasse Frame
, jetzt unter dem Namen JFrame
.
Die Schaltflächen sind entsprechend Ausprägungen von JButton
. Mit der Methode setToolTipText(String)
wird die rein Swing-interne Möglichkeit zur Definition eines Hilfstexts genutzt, der bei Überstreichen einer Komponente mit der Maus automatisch angezeigt wird. Die Implementierung greift nicht auf Mechanismen des zugrundeliegenden GUI-Systems zurück, sondern ist vollständig innerhalb von Swing realisiert.
Die Ereingisbearbeitung ist identisch zur AWT v1.1 realisiert, und kann daher naherzu unverändert übernommen werden.
Als Behandlungsroutine der drei Schaltflächen ist der Wechsel des Bildschirmlayouts zur Laufzeit realisiert. Nach Aktivierung der entsprechenden Schaltfläche wird das entsprechende, innerhalb der Swing-API vordefinierte Layouts referenziert, und durch die statische Methode setLookAndFeel(String)
geladen. Nach der Aktualisierung der Bildschirmdarstellung präsentiert sich die gesamte Applikation im neuen Layout.
Service provided by Mario Jeckle
Generated: 2004-06-11T07:13:10+01:00 Feedback
SiteMap
This page's original location: http://www.jeckle.de/vorlesung/java/script.html
RDF description for this page