1 Einführung in Java
1.1 Was ist Java?
1.2 Entstehungsgeschichte
1.3 Die Java-Plattform
Prinzipiell kann, wie erwähnt, eine Java-Applikation auf jeder beliebigen Plattform zur Ausführung gebracht werden --- unabhängig davon welches Gerät die notwendige virtuelle Maschine implementiert.
So existieren Visionen und erste Umsetzungen von JVMs für Elektrogeräte wie Kaffeemaschinen, Kühlschränke, Radios etc.
Zur Erinnerung: die Intention der ursprünglichen Java-Enwicklung zielte nicht auf Desktop Rechner oder gar das Internet.
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:
javacjavac example.javajavac HelloWorld.java erzeugt die Dateien HelloWorld.class und SayHello.classjavajava example.class darf nicht angegeben werden!java HelloWorld führt die Applikation aus.appletviewerappletviewer example.htmlappletviewer HelloWorldApplet1.htmljavadocjavadoc example.javajavadoc -private -version -author -windowtitle "Beispiel einer JavaDoc generierten Dokumentation" Hello World.javajdbjdb examplejavapjavap exampleDie 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.
1.4 Vom Quellcode zum lauffähigen ProgrammDie 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.
2. Syntax und Semantik der Programmiersprache Java
2.1 C, C++, C# und JavaWie 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.
Microsofts C#Die durch Microsoft entwickelte Sprache C# (sprich: C sharp) weißt einige interessante Parallelen zu Java auf, führt jedoch auch neue Konzepte ein.
Jenseits der Einordnung in die Komponentenarchitektur der .NET-Plattform, welche nur schwer mit der der Java-Plattform (insbesondere der J2EE-Plattform) verglichen werden kann, stellt C# jedoch eine vollwertige -- sehr stark an Java orientierte -- Programmiersprache dar. Bereits das, zu erwartende, Standardbeispiel der HelloWorld-Applikation läßt die enge Verwandschaft, abzulesen an der Syntax deutlich werden (Vergleiche HelloWorld in Java, in in C#).
Im Gegensatz zu C# ist Java auf der Programmierebene nicht rein objektorientiert wie beispielsweise SmallTalk. So treten auch hier die primitiven Datentypen als nicht-objektartige Werte auf.
Ebenso wie in Java ist ausschließlich Einfachvererung zugelassen, ergänzt wird diese um Schnittstellen. Auch C# erlaubt es einer Klasse mehrere Schnittstellen zu implementieren. Die Syntax unterscheidet jedoch nicht mehr explizit zwischen Schnittstellenimplementierung und Erben von einer Klasse (siehe Beispiel). Unverändert zu Java wird auch jede Klasse, die über keine Elternklasse verfügt automatisch als Subklasse von object eingeordnet.
Blattknoten einer Vererbungshierarchie (d.h. Klassen für die durch den Programmierer vorgegeben wird, sie sollen nicht weiter durch Ableitung spezialisiert werden) können mit dem Schlüsselwort sealed -- in Java: final -- gekennzeichnet werden.
Zusätzlich zu Klassen beinhaltet C#, wieder, den Sprachmechanismus der Struktur (struct). Hierüber wird die Differenzierung zwischen Wert- und Referenztypen realisiert. Während Klassen, Java-konform Referenztypen bezeichnen, übernehmen Strukturen die der Werttypen. Desweiteren werden Structs durch den Compiler als Bestandteile umgebender Objekte übersetzt. Durch diese Charakteristika unterscheiden sich C#-Structs stark von den dortigen Klassenstrukturen, weshalb sie auch nicht analog zu deren Schema verwirklicht sind. Strukturen können weder erben noch vererbt werden, lediglich die Möglichkeit Schnittstellen zu implementieren bleibt erhalten.
Deutlich komfortabler als in Java fällt jedoch der Umgang mit Referenz- und Werttypen aus. Das in C# angebotene boxing und unboxing übertrifft deutlich die Benutzungsfreundlichkeit der Java-Wrapperklassen (Beispiel). Dieser Mißstand wurde jedoch in der Javasprachversion 1.5 behoben, die dieses Konzept ebenfalls einführt.
Erstmals reichert C# auch den Sprachumfang C/C++-basierter Sprachen um neue syntaktische Konstrukte an. Hierunter fallen beispielsweise Schlüsselworte zur Definition von Delegationsobjekten, die als solche explizit im Programmcode deklariert werden können.
Wie Java compiliert auch C# in eine Zwischenrepräsentation. Allerdings mit dem Unterschied, daß diese nicht interpretativ abgearbeitet wird, sondern erneut in maschinenspezifisches natives Format übersetzt wird.
Die Tabelle stellt einige Sprachmerkmale von C++, Java und C# gegenüber
(Tabelle in Anlehnung an: Eisenecker, U.: Dissonanz oder Wohlklang, in: iX 9/2000, Hannover, 2000, p. 48-51)
| |||||||||||||||||||||||||||||||||||||||||||||||||
(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.
2.2 Grundstrukturen
2.2.1 ProgrammaufbauBereits 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.
2.2.2 Einfache DatentypenWie 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
2.2.3 OperatorenJava 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.
Operatoren auf integralen Typen (siehe Java API Specification)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).
Operatoren auf Fließkommatypen (siehe Java Language Specification)Auf diesen Typen sind die auch auf integralen Typen zugelassenen arithmetischen-, numerischen Vergleichs-, Inkrement- und Dekrement-Operatoren verfügbar. Ferner existiert der numerische Cast (siehe Java API Specification).
Auch die Fließkommaoperatoren lösen keine Exceptions aus. überlauf wird als positiv Unendlich, Unterlauf entsprechend als negativ Unendlich dargestellt. Liefert die Operation kein mathematisch interpretierbares Resultat, wird der Wert auf NaN gesetzt. In der Konsequenz resultiert auch NaN falls ein so gesetzter Operand erneut verknüpft wird. Dies gilt nicht für die Gleichheitsoperation.
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.
Operationen auf Boole'schen TypenAuf 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)
Operatoren auf objektwertigen Typen+ 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.
Schlussbemerkungen+=, -=, *=, /=, &=, |=, ^=, %=, <<=, >>=, >>>=.* 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.
OperatorpräzedenzEs gelten folgende Operatorpräzedenzen (geordnet von oben (entspricht höchster Präzedenz) nach unten (niedrigster Präzedenz)):
| |||||||||||||||||||||||||||||||||||
Automatische TypkonversionDie 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.
2.3 Kontrollstrukturen
2.3.1 Selektion und Mehrfachselektion -- das if und case-StatementDas 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.
2.3.2 Iteration -- for-, do-while-SchleifenJava 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.
2.3.3 Ausnahmen und ihre Behandlung -- Exception HandlingDie 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
ZwangsbedingungenZur 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)optGenerell 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.
2.4 Von komplexen zu objektorientierten Datenstrukturen
2.4.1 ArraysWie 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).
2.4.2 KlassenKlassen 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.
2.4.3 AttributeAttribute 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.
2.4.4 Operationen und MethodenOperationen 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 staticKlassenmethoden definiert.
Auf sie kann unabhängig von der Existenz konkreter Objekte der Klasse zugegriffen werden.
Aufgrund ihrer instanzenunabhängigen Natur besitzen statische Methoden keine this Referenz, da ein so zu referenzierendes Objekt nicht exisitiert.
|
Beispiel 34: Statische Methoden StaticMethodTest.java
Besondere Methoden:
Konstruktoren
Identisch benannt zur beherbergenden Klasse
Syntaktisch ähneln die Konstruktoren den „normalen“ Operationsdeklarationen. Jedoch mit dem Unterschied, daß kein Rückgabetyp spezifiert werden kann.
Der Konstruktorenaufruf wird automatisch durch den übersetzer plaziert.
Existiert kein expliziter Konstruktor, so wird während des übersetzungsvorganges ein unparametrisierter Vorgabekonstruktor erzeugt. Dieser enthält keinerlei eigene Funktionalität, sondern ruft nur den entsprechenden parameterlosen Konstruktor der Superklasse auf. Verfügt eine Klasse hingegen ausschließlich über parametrisierte Konstruktoren, so wird kein unparametrisierter Defaultkonstruktor angelegt.
Aufrufe unter den verschiedenen Konstruktoren können durch das bekannte Schlüsselwort this vorgenommen werden. Hierbei werden die einzelnen Konstruktoren wie normale Methoden behandelt. Der Aufruf erfolgt durch thisgefolgt von den Konstruktorenparametern in runden Klammern. Der kaskadierende Konstruktorenaufruf muß zwingend als erstes Statement des Anweisungsblockes plaziert werden.
Zwar verfügt die Konstruktormethode über keinen expliziten Rückgabetyp, wird aber intern als void-Typisiert behandelt.
|
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.
2.4.5 AufzählungstypenAb 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
2.4.6 Wrapper-TypenKorrespondierend 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....ValuebooleanValue, byteValue, shortValue, intValue, longValue, floatValue, doubleValuepublic 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.compareToequals 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).
2.4.7 Boxing/UnboxingZwar 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.
2.4.8 VererbungWie 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).
2.4.10 SchnittstellenIn 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 InterfaceBodyAuch 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:
2.4.10 PaketePakete 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
3 Die Java-Plattform
3.1 Die Laufzeitumgebung
3.1.1 Garbage CollectionWie 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:gcXnoclassgcXincgc |
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.
3.1.2 Virtuelle MaschineKern 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 counteroptopframevarsDie 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:
byteshortintlongcharfloatdoublebooleanboolean-Typen enthalten als Operationen auf int-Typen um.returnAddressnull 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)
InputStreamFileInputStreamPipedInputStreamFilterInputStreamLineNumberInputStreamdeprecatedDataInputStreamBufferedInputStreamInputStreams 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.PushbackInputStreamInputStreams die Möglichkeit der Wiedereinstellung von bereits gelesenen Byte-Inhalten in den Eingabestrom.ByteArrayInputStreamStringBufferInputStreamdeprecated
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.SequenceInputStreamObjectInputStreamjava.io.Serializable oder java.io.Externalizable unterstützten.OutputStreamFileOutputStreamPipedOutputStreamPipedInputStreams.FilterOutputStreamDataOutputStreamBufferedOutputStreamOutputStreams die Möglichkeit des gepufferten (d.h. nicht mehr Byte-weisen) Schreibens.PrintStreamOutputStreams 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.ByteArrayOutputStreamObjectOutputStreamjava.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)
ReaderBufferedReaderLineNumberReaderCharArrayReaderInputStreamReaderFileReaderFilterReaderPushbackReaderPipedReaderStringReaderString liest.WriterBufferedWritersystem properties).CharArrayWriterOutputStreamWriterFileWriterFilterWriterPipedWriterStringWriterString 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 Strings 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.
3.2.2 Threads und NebenläufigkeitJava 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.yielddestroy()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...
} //synchronizedAls 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();
} //synchronizedDie 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.