6 Java - die Hauptbestandteile der Sprache

Wir haben nun viele Vorbereitungen getroffen und wollen zu den wichtigsten Bestandteilen von Java selbst kommen. Dabei sollen in diesem umfangreichen Kapitel - es wird unser dickster Brocken in dem Buch - die folgenden Themenschwerpunkte behandelt werden:

Dieses Kapitel wird nicht nur umfangreich, sondern leider auch recht theoretisch. Dies lässt sich nicht ganz vermeiden. Da müssen Sie durch. Dies ist aber sicher nicht Java-spezifisch, sondern trifft auf jede Programmiersprache zu. Damit das Kapitel aber nicht ausschließlich Theorie beinhaltet, werden wir einige Beispiele angehen. Diese Beispiele verwenden am Anfang gelegentlich Java-Techniken, die Ihnen unter Umständen noch nicht vertraut sind oder bis zu der jeweiligen Stelle noch nicht explizit eingeführt wurden. Dies betrifft beispielsweise Schleifen, die eine Wiederholung von Programmschritten erlauben. Wir werden etwa die for-Schleife an mehreren Stellen benutzen. Es ist bei diesen Beispielen jedoch nicht unbedingt erforderlich, dass Sie die Syntax zu diesem Zeitpunkt schon vollständig verstehen. Die Verwendung der etwas weitergehenden Java-Techniken dient nur dazu, die Beispiele interessanter zu gestalten und das eigentliche Ziel des Beispiels in einen passenden Kontext zu setzen.

6.1 Token

Token bedeutet übersetzt Zeichen oder Merkmal. Wenn ein Compiler eine lesbare Datei in Maschinenanweisungen übersetzt, muss er zunächst herausfinden, welche Token oder Symbole im Code dargestellt sind. Ein Token kann man als Sinnzusammenhang verstehen. So ist etwa in der menschlichen Sprache ein Wort nicht nur die Summe seiner Buchstaben oder Zeichen, sondern besitzt einen konkreten Sinnzusammenhang, den das interpretierende System (der menschliche Geist) mit einer bestimmten Bedeutung assoziiert. Allerdings muss das interpretierende System auch die Sprache verstehen, sonst bleibt ein Token einfach nur die Summe seiner Buchstaben oder Zeichen.

Beispiel:

Jeder gute Hesse wird aus der Summe der Zeichen B,e,m,b,e,l sofort den Sinn erfassen und mit dem Token Bembel einen Krug für Apfelwein (und vielleicht laue Sommernächte in einer Äpplerkneipe und den nachfolgenden Kater am nächsten Morgen) assoziieren, während die Summe der Zeichen B,e,m,b,e,l für die meisten Norddeutschen wahrscheinlich einfach nur eine bedeutungslose Aneinanderreihung von Zeichen bleibt.

Wenn von dem Java-Compiler der Quellcode kompiliert werden soll, muss er ihn dabei in einzelne kleine Bestandteile (Token) zerlegen. Quelltext muss sich dabei in logisch sinnvolle Einheiten zerlegen und in gültige Arten von Token einordnen lassen. Die Sprachelemente werden auf ihre Richtigkeit geprüft. Außerdem werden Leerzeichen und Kommentare aus dem Text entfernt. Dieser Teil der Übersetzung eines Quelltextes zum Bytecode wird von dem so genannten Parser übernommen.

An der Stelle des Parsers greift auch die erste Kontrolleinheit des Sicherheits- und Stabilitätskonzepts von Java.

Wenn in einem Quelltext einem Token Ganzzahl (ein Variablenbezeichner) der Datentyp short zugeordnet ist (eine 16-Bit-Zahl), dann erkennt der Compiler den Token jedes Mal, wenn es in diesem Zusammenhang benutzt wird, und benutzt die spezifisch zugeordneten 16 Bits des Speichers. Alle Operationen mit Ganzzahl werden mit dem spezifischen Wert, der in dieser 16-Bit-Adresse enthalten ist, durchgeführt. Also nicht mit den Buchstaben des Tokens selbst, sondern mit dem, was der Compiler mit dem Token »assoziiert«.

Es gibt in Java fünf Arten von Token:

1. Bezeichner oder Identifier
2. Schlüsselworte
3. Literale
4. Operatoren
5. Trennzeichen

Diese Einteilung in fünf Tokenarten gibt es übrigens für die meisten anderen Computersprachen analog.

Kommentare oder Leerraum (Leerzeichen, Tabulatoren und Zeilenvorschübe) sind in der Aufzählung nicht enthalten, aber sie existieren natürlich ebenfalls, werden sozusagen als selbstverständlich vorausgesetzt. Technisch gesehen sind sie sogar eigentlich keine Token (wie man beispielsweise daran erkennen kann, dass sie vom Compiler entfernt werden).

6.1.1 Der Unicode-Zeichensatz

Java benutzt auf Quelltextebene keinen 8-Bit-ASCII-Code, sondern den 16-Bit-Unicode-Zeichensatz. Damit können zusätzlichen Zeichen kodiert werden, die nicht im lateinischen/englischen Alphabet enthalten sind. Zeichenketten nehmen zwar doppelt so viel Platz ein, jedoch wird die Internationalisierung leichter. Es gibt dort sogar ein Zeichen für den Euro1.

Die Unicode-Spezifikation ist ein zweibändiger Listensatz mit zig Tausenden von Zeichen. Sie werden sicher einsehen, dass ein Auflisten der Zeichen zwar das Buch füllen, Ihnen jedoch nicht viel nützen würde. Da jedoch die ersten 256 Zeichen dem normalen ASCII-Zeichensatz entsprechen (Byte 1 ist auf 0 gesetzt), brauchen Sie sowieso normalerweise darauf kaum Rücksicht zu nehmen. Sie können einfach wie bisher die Zahlen und Buchstaben für Variablen-, Methoden- oder Klassennamen verwenden. Eine ASCII-Codierung wird einfach durch eine kanonische Übersetzung (d.h. die Reihenfolge und Anordnung der ASCII-Codierung ist auch in der neuen Codierung als Block wiederzufinden) mit Voranstellen der Zeichenfolge \u00 und folgender Hexadezimalzahl in das dazu passende Unicode-Zeichen übersetzt. Dies definiert in Java eine Escape-Sequenz, mit der alle Unicode-Zeichen verschlüsselt werden können. Sobald die Unicode-Sequenz beendet ist, werden die folgenden Zeichen nahtlos angefügt.

Durch die mögliche Unicode-Escape-Darstellung von Zeichen im Quelltext ist es in Java im Gegensatz zu anderen Programmiersprachen erlaubt, Umlaute und andere Sonderzeichen in Bezeichnern zu verwenden. Dabei kann die Angabe dieser Sonderzeichen (und natürlich auch gewöhnlicher Zeichen) sowohl direkt als auch über Angabe der Escape-Sequenz erfolgen.

Alle Literale (d.h. alle vorkommenden Zeichen wie Buchstaben, Zahlen, Sonderzeichen usw.) in Java bestehen aus Unicode. Das JDK-Tool native2unicode übersetzt bei Bedarf 8-Bit-Native-Code in Unicode. Der Java-Compiler erwartet auf jeden Fall Unicode, jedoch müssen Sie sich bei der Arbeit mit einem gewöhnlichen ASCII-Editor darum meistens nicht kümmern. Vor der Kompilierung wird der Quelltext vom Compiler als Erstes automatisch in eine Folge von Unicode-Zeichen transformiert. Erst nach der Transformation erfolgt dann die eigentliche Kompilierung.

Zwei Bezeichner gelten in Java dann und nur dann als identisch, wenn ihre Unicode- Darstellung übereinstimmend ist. Dies ist auch der Grund, warum in Java unbedingt zwischen Groß- und Kleinschreibung zu unterscheiden ist. Die beiden folgenden Bezeichner sind also identisch:
float Übung;
float \u00DCbung;

Wenn Sie nun der vollständige Unicode-Zeichensatz doch interessiert, so nutzen wir Java und schreiben ein kleines Programm, das diesen - zumindest teilweise - ausgibt. Auch hier werden wir noch nicht zu detailliert auf die Syntax eingehen, aber doch schon ein wenig erläutern, was in dem Programm passiert. Wir verwenden hier zum ersten Mal eine Schleife. Für Sie sollte an der Stelle nur wichtig sein, dass sie die Anweisungen in dem zugeordneten Block so oft wiederholt, wie es in der Schleife angegeben ist.

Geben Sie den nachfolgenden Quelltext ein.

/* Beispielprogramm zur Bildschirmausgabe vom ASCII- bzw. Unicode-Zeichensatz */
class Unicode {
public static void main (String args[]) { 
 int i;
 for(i=0; i < 1500;i++) 
  System.out.print((char)i);
 }
}

Speichern und kompilieren Sie das Programm.

Starten Sie das Programm.

Wenn Sie das Programm eingeben, speichern (unter dem Namen Unicode.java), kompilieren und dann mittels des java-Interpreters ausführen, bekommen Sie auf dem Standardausgabegerät (meist der Bildschirm) die bekannten Zeichen des ASCII-Codes und dann einen Teilbereich des Unicode-Zeichensatzes ausgegeben (viele, viele Fragezeichen, d.h. noch nicht definiert).

Abbildung 6.1:  Zeichen des Unicode-Zeichensatzes

Das Programm hat eine ähnliche Struktur wie unser HelloJava-Programm. Es besteht nur aus einer Methode, der main()-Methode, die jedes eigenständige Java-Programm benötigt. Sie erinnern sich vielleicht, dass jede Klasse, die in dem Aufruf durch den Klassennamen spezifiziert wird, eine Methode namens main() (public static void main(String args[])) benötigt. Der Interpreter java endet nach der vollständigen Abarbeitung von main().

Innerhalb der main()-Methode finden Sie wie bei unserem HelloJava-Programm eine Methode zur Ausgabe auf dem Standardausgabegerät - System.out.print(). Der Unterschied zu System.out.println() ist der, dass in hier kein Zeilenvorschub nach der Ausgabe jedes Zeichens erfolgt.

Eine weitere Änderung innerhalb der Methode System.out.print() ist offensichtlich. Es wird kein Text in Hochkommata eingeschlossen ausgegeben, sondern mit (char)i eine Variable. Dabei ist i die vorher mit int i; eingeführte Zählvariable einer Schleife (der for-Schleife), die von 0 bis kleiner 1500 zählt. Würden wir nun nur diese Zählvariable innerhalb der Methode System.out.print() notieren, würde auf dem Standardausgabegerät eine Zahlenkolonne von 0 bis 1499 ausgegeben. Da wir das nicht wollten, muss der numerische Wert in das zugehörige Zeichen des Unicodes überführt werden. Man nennt diesen Vorgang Casting. Das in Klammern vorangestellte (char) verwandelt die Zählvariable für die Ausgabe entsprechend. Sie können so etwas für alle primitiven Datentypen in Java machen (Achtung - boolean Typen sind eine Besonderheit). Dazu folgt gleich mehr.

6.1.2 Die UTF-8-Codierung

Mit UTF-8-Codierung wird ein Verfahren bezeichnet, das eine Folge von den 16 Bit langen Unicode-Zeichen zur effizienteren Speicherung in eine Datei codiert, indem die Unicode-Zeichen in Abhängigkeit von ihrem Wert in einem, zwei oder drei Byte verschlüsselt werden.

Das Verfahren ist dem Huffman-Algorithmus (siehe Anhang) bei allgemeinen Komprimierungsverfahren nicht ganz unähnlich. Jedoch besteht hier nicht das Problem, zuerst die Häufigkeit einzelner Zeichen bestimmen zu müssen. Hier setzt man einfach voraus, dass die Zeichen des ASCII-Zeichensatzes (\u0001 - \u007F) besonders häufig vorkommen (und natürlich das erste Byte sowieso auf 0 gesetzt ist) und deshalb eine besonders kurze Darstellung in einem Byte benötigen. Die Zeichen von \u0080 bis \u07FF werden in zwei Byte und die Zeichen von \u0800 bis \uFFFF werden in drei Byte verschlüsselt.

In Java stellen unter anderem die Klassen DataOutputStream und DataInputStream Methoden zum Speichern und Lesen von Zeichen im UTF-8-Code zur Verfügung. Wir werden diese im Kapitel über Ein- und Ausgabe in Java näher behandeln.

6.1.3 Kleiner Vorgriff auf die Datentypen von Java

Wir werden etwas weiter hinten die Datentypen von Java im Detail beleuchten, aber da wir bereits bei den Token mehrfach auf die verschiedenen Datentypen zu sprechen kommen, soll ein kleiner Vorgriff für Klarheit sorgen. Java besitzt acht primitive Datentypen:

1. Vier Ganzzahltypen mit unterschiedlichen Wertebereichen (byte, int, short, long)
2. Einen logischen Datentyp (boolean)
3. Zwei Gleitzahltypen mit unterschiedlichen Wertebereichen (float, double)
4. Einen Zeichentyp (char)

6.1.4 Die Java-Token-Details >

Java unterscheidet seine Token nach bestimmten Kriterien:

Token Beschreibung Beispiele
Schlüsselworte Alle Worte, die ein essenzieller Teil der Java-Sprachdefinition sind. public, class, static, void, String, else,
if, synchronized, this, while
Bezeichner oder Identifier Namen für Klassen, Objekte, Variablen, Konstanten, Bezeichnungsfelder, Methoden usw., zusammengesetzt aus alphanumerischen Unicode-Zeichen. An der ersten Stelle eines Bezeichners darf keine Zahl stehen. HelloWorld, main, args, System, out, println, j, n32e20
Literale Mit einem Literal können Variablen und Konstanten bestimmte Werte zugewiesen werden. Dies können sämtliche in der Java-Sprachdefinition erlaubten Arten von Werten sein (numerische Werte, boolesche Werte, Buchstaben, Zeichenketten). "HelloJava", 42, false, true
Trennzeichen Unter Trennzeichen versteht man alle Symbole, die dazu benutzt werden, Trennungen und Zusammenfassungen von Code anzuzeigen. ( ) } [ ] ; , .
Operatoren Operatoren sind Zeichen oder Zeichenkombinationen, die eine auszuführende Operation mit einer oder mehreren Variablen oder Konstanten angibt. Es
gibt diverse Typen von Operatoren.
+, -, *, /, , >>>, <<<
Leerräume Zeichen, die in beliebiger Anzahl und an jedem Ort zwischen allen Token mit Funktion platziert werden können und keinerlei andere Bedeutung haben, als den Quellcode übersichtlich zu gestalten. [Space], [Tab], [Zeilenende], [Formularvorschub]
Kommentare Ein Kommentar dient zur übersichtlichen Gestaltung des Quellcodes und dazu, dass man bei späterer Kontrolle überhaupt noch etwas versteht. Der Compiler ignoriert Kommentare. Eine Besonderheit ist der javadoc-Kommentar, der vom Java-Dokumentations-Tool ausgewertet wird. // Kommentar bis zum nächsten Zeilenende /*Eingebetteter Kommentar*/ /** javadoc-Kommentar */

Tabelle 6.1:   Die Token-Typen

Wir werden nun die Token im Detail beleuchten.

Schlüsselworte

Unter Schlüsselworten versteht man in Java alle Buchstabenfolgen, die ein wesentlicher Teil der Java-Sprachdefinition sind und die in Java eine besondere Bedeutung haben. Es gibt zusätzlich einige Token, die vorsorglich für spätere Versionen von Java reserviert wurden (byvalue, cast, const, future, generic, goto, inner, operator, outer, rest und var). Dass dies Sinn machen kann, zeigt das Schlüsselwort transient, das in der Version 1.0 noch ohne Bedeutung war. Mittlerweile hat es in Java einen wohldefinierten Wert. Die meisten dieser derzeit nicht verwendeten, jedoch vorsorglich reservierten Schlüsselworte werden Programmierern, die von C/C++ oder PASCAL kommen, bekannt vorkommen.

Es folgt eine alphabetische Tabelle mit den Java-Schlüsselworten (ohne nähere Erläuterung, die folgt später).

abstract boolean break byte
case cast catch char
class const continue default
do double else extends
false final finally float
for future generic goto
if implements import inner
instanceof int interface long
native new null operator
outer package private protected
public rest return short
static super switch synchronized
this throw throws transient
true try var void
volatile while    

Tabelle 6.2:   Die Java-Schlüsselworte

Die Bedeutungen der einzelnen Schlüsselworte sollen wie bereits angedeutet an anderen Stellen erläutert werden, aber einige Anmerkungen sind jetzt bereits notwendig:

Es wird ein Schlüsselwort auffallen, das in grauer Basic-Urzeit für viel Freude sorgte: goto. Es ist in Java zwar reserviert, indes ohne Bedeutung. Es besteht sogar berechtigte Hoffnung, dass es für immer ohne Bedeutung bleibt. Denkbar ist sogar, dass es nur deshalb als Schlüsselwort ohne Bedeutung reserviert wurde, damit es niemals mehr in einem Java- Quelltext auftaucht.
  • Schlüsselworte haben in Java eine spezifische Bedeutung, können also nicht als Bezeichner für irgendetwas anderes wie beispielsweise Variablen, Konstanten, Klassennamen usw. benutzt werden. Beachten Sie jedoch, dass für Schlüsselworte (wie auch sonst) Groß- und Kleinschreibung zu unterscheiden ist. Ein Schlüsselwort Var gibt es beispielsweise nicht, und Sie könnten es z.B. im Extremfall als Name für eine Variablen nehmen. Davon ist allerdings dringend abzuraten. Java wird damit keine Probleme haben, aber Personen, die den Quelltext lesen und warten sollen. Sie können jedoch Schlüsselworte als Teil eines längeren Tokens verwenden, um damit die Bedeutung deutlich zu machen.

Die Token true und false sind rein technisch betrachtet nur Werte für boolesche Variablen und Konstanten. Daher sollte man davon Abstand nehmen, sie als Identifier zu verwenden (benutzerdefinierte Namen und Beschriftungen).
  • Java besitzt diverse Standardpakete. Dort sind viele Elemente enthalten, deren Namen zwar keine Schlüsselworte sind, die aber bei anderweitiger Verwendung die Lesbarkeit des Quelltextes gewaltig reduzieren (was in der Tat denkbar ist, wenn man die jeweiligen Pakete nicht importiert).

Zu Paketen siehe Seite 303.

Bezeichner und Namenskonventionen

Unter einem Bezeichner oder Identifier versteht man in Java ein Wort, das durch Zuordnung zu einer Variablen, Konstanten, Klasse, Objekt, Beschriftung oder Methode für Java zu einem Token wird. Es gibt in Java - wie in allen Programmiersprachen - einige Regeln zur Namenvergabe bei Bezeichnern, die Sie zwingend einhalten müssen:

1. Wie schon vorher erwähnt, dürfen Bezeichner nicht mit Java-Schlüsselworten und sollten nicht mit Namen von Java-Paketen identisch sein.
2. Bezeichner sind in Java Zeichenketten, bestehend aus Unicode-Buchstaben und Zahlen, die im Prinzip eine unbeschränkte Länge haben dürfen (bis auf technische Einschränkungen durch das Computersystem).
3. Das erste Zeichen eines Bezeichners muss ein Buchstabe, der Unterstrich (_) oder das Dollarzeichen ($) sein. Alle folgenden Zeichen müssen entweder Buchstaben oder Zahlen sein. Es müssen jedoch nicht unbedingt lateinische Buchstaben oder Zahlen sein. Sie können jedes Alphabet, das von Unicode unterstützt wird, benutzen.

Zwei Token gelten nur dann als derselbe Bezeichner, wenn sie dieselbe Länge haben und jedes Zeichen im ersten Token genau mit dem korrespondierenden Zeichen des zweiten Tokens identisch ist, d.h. den vollkommen identischen Unicode-Wert hat. Java unterscheidet deshalb Groß- und Kleinschreibung. Die beiden Token Ganzzahl und ganzzahl sind nicht identisch.

Bezeichner gültig ungültig Begründung, falls ungültig
HelloJava x    
42HelloJava   x Der Bezeichner beginnt mit einer Zahl.
safetyfirst x    
Rock&Roll   x & ist kein Zeichen eines Alphabets.
Rock und Roll   x Leerzeichen sind verboten.
Rock_Roll x    
42   x Der Bezeichner beginnt mit einer Zahl.
_42 x    
a-b   x Das Minuszeichen ist kein Zeichen eines Alphabets. Java würde zwei verschiedene Bezeichner interpretieren, mit denen eine Operation durchgeführt werden soll.

Tabelle 6.3:   Beispiele für gültige und ungültige Bezeichner

Neben den zwingenden Namenskonventionen gibt es einige Regeln, die man bei der Vergabe von Namen einhalten sollte. Es hat sich eingebürgert, diese Namenskonventionen in Java einzuhalten, was zwar nicht zwingend, aber für eine Lesbarkeit des Quelltexts auf Grund allgemeiner Bekanntheit sinnvoll ist:

1. Man sollte möglichst sprechende Bezeichner zu verwenden. Ausnahmen sind Schleifen, wo meistens nur ein Buchstabe als Zählvariable (normalerweise i oder j) verwendet wird.
2. Konstanten (Elemente mit dem Modifier final) sollten vollständig groß geschrieben werden.
3. Die Identifier von Klassen sollten mit einem Großbuchstaben beginnen und anschließend klein geschrieben werden. Wenn sich ein Bezeichner aus mehreren Worten zusammensetzt, dürfen diese nicht getrennt werden. Die jeweiligen Anfangsbuchstaben werden jedoch innerhalb des Gesamtbezeichners jeweils groß geschrieben.
4. Die Identifier von Variablen, Methoden und Elementen beginnen mit Kleinbuchstaben und werden auch anschließend klein geschrieben. Wenn sich ein Bezeichner aus mehreren Worten zusammensetzt, dürfen diese nicht getrennt werden. Die jeweiligen Anfangsbuchstaben von den folgenden Worten werden jedoch innerhalb des Gesamtbezeichners jeweils groß geschrieben.

Es folgen einige unübliche (keine falschen) Bezeichner. Da es bei Bezeichnern von der Art des Elements abhängt, ob der gewählte Bezeichner den gängigen Namenskonventionen entspricht, ist in der Tabelle die Art des Elements aufgeführt.

Art unüblicher Bezeichner Begründung
Klasse Hellojava Das zweite Wort des Bezeichners sollte groß geschrieben werden.
Klasse hellojava Der Bezeichner enthält gleich zwei Verstöße gegen die guten Sitten. Der erste Buchstabe des Bezeichners und der des zweiten Worts innerhalb des Bezeichners sollten groß geschrieben werden.
Klasse HELloJava Zu viele Buchstaben sind am Anfang groß geschrieben. Ein solcher Bezeichner würde auch bei Methoden, Variablen oder anderen Elementen aufstoßen.
Klasse i Kein sprechender Name, außerdem klein geschrieben.
Konstante Ende Nicht vollständig groß geschrieben.
Variable Wert Bei Variablen sollte der erste Buchstabe klein geschrieben werden.
Variable werterfassung Auch bei Variablen sollte der Beginn eines zweiten Wortes in dem Bezeichner groß geschrieben werden.

Tabelle 6.4:   Beispiele für einige unübliche Bezeichner

Namensräume

Java stellt eine Technik zur Verfügung, mit der Namenskonflikte aufgelöst werden können, wenn es zwei identische Bezeichner im Quelltext gibt. Java arbeitet mit so genannten Namensräumen. Man versteht unter einem Namensraum einen Bereich, in dem ein bestimmter Bezeichner benutzt werden kann.

Namensräume sind in Java einer Hierarchie zugeordnet. Es gilt dabei die Regel, dass ein Bezeichner einen identischen Bezeichner in einem übergeordneten Namensraum überdeckt. Außerdem trennt Java die Namensräume von lokalem und nicht-lokalem Code. Die Hierarchie der Namensräume gliedert sich wie folgt:

1. Außen (aus Sicht der Mengenlehre zu sehen) steht der Namensraum des Pakets, zu dem die Klasse gehört.
2. Danach folgt der Namensraum der Klasse.
3. Es folgen die Namensräume der einzelnen Methoden. Dabei überdecken die Bezeichner von Methodenparametern die Bezeichner von Elementen der Klasse. Sollten Elemente der Klasse überdeckt werden, können sie immer noch mit dem Verweisoperator this qualifiziert werden.
4. Innerhalb von Methoden gibt es unter Umständen noch weitere Namensräume in Form von geschachtelten Blöcken (etwa try/catch-Blöcke). Variablen, die innerhalb eines solchen geschachtelten Blocks deklariert werden, sind außerhalb des Blocks unsichtbar.

Um einen Bezeichner zuzuordnen, werden die Namensräume immer von innen nach außen aufgelöst. Wenn der Compiler einen Bezeichner vorfindet, wird er zuerst im lokalen Namensraum suchen. Sofern er dort nicht fündig wird, sucht er im übergeordneten Namensraum. Das Verfahren setzt sich analog bis zum ersten Treffer (ggf. bis zur obersten Ebene) fort. Es gelten immer nur die Vereinbarungen des Namensraums, wo der Treffer erfolgt ist.

6.1.5 Literale

Literale sind spezielle Token, die zu speichernde Werte als Datentypen byte, short, int, long, float, double, boolean und char darstellen. Darüber hinaus werden Literale dazu benutzt, Werte darzustellen, die in Zeichenketten gespeichert werden. Um es etwas verständlicher auszudrücken - Literale sind das, was bei einer Zuweisung einer Variablen zugewiesen bzw. das, was als Wert direkt bei einer Operation verwendet wird.

Ganzzahl-Literale

Die Datentypen int und long können dezimal, aber auch hexadezimal sowie oktal beschrieben werden. Man nennt diese beiden Datentypen Integer-Literale oder auch Ganzzahl-Literal. Die Voreinstellung ist dezimal und gilt immer dann, wenn die Werte ohne weitere Angaben dargestellt werden. Hexadezimale Darstellung beginnt immer mit der Sequenz 0x oder 0X. Oktale Darstellungen beginnen mit einer führenden Null. Negativen Ganzzahlen wird (nicht gerade überraschend) ein Minuszeichen vorangestellt. Integer-Literale haben per Voreinstellung den Typ int. Durch Anhängen von l oder L kann man jedoch explizit den Typ long wählen. Sofern man für einen int-Datentyp einen Wert wählt, der den zulässigen Wertebereich überschreitet, muss dies sogar erfolgen. Die nachfolgende Tabelle zeigt einige Beispiele mit Zahlen in verschiedenen Darstellungen.

Integer-Literal Dezimalwert Hexadezimalwert Oktalwert Typ
123 123 7B 173 int
0123 83 53 123 int
0x123 291 123 443 int
0x123L 291 123 443 long

Tabelle 6.5:   Ganzzahl-Literale

Gleitpunkt-Literale

Die beiden Gleitzahltypen vom Datentyp float und double werden Gleitzahl-Literale oder Gleitpunkt-Literale genannt. Der Dezimalpunkt trennt Vor- und Nachkommateil der Gleitzahl. Standardeinstellung ist double. Wenn ein Gleitzahl-Literal als float interpretiert werden soll, muss ein f oder ein F angehängt werden. Daher wird die Zuweisung

float gleitzahl = 0.123;

einen Compilerfehler erzeugen, während die drei folgenden Zuweisungen korrekt sind:

float gleitzahl = 0.123F;
float gleitzahl = 0.123f;
double gleitzahl = 0.123;

Negativen Gleitzahlen wird wieder ein Minuszeichen vorangestellt. Mit dem nachgestellten e oder E, gefolgt von einem Exponenten (ein negativer Exponent ist ebenso erlaubt), können für Gleitzahl-Literale Exponenten verwendet werden. Allerdings nur dann, wenn kein Dezimalpunkt vorhanden ist.

Zeichenliterale

Zeichenliterale werden durch ein einzelnes, zwischen hochgestellten und einfachen Anführungszeichen stehendes Zeichen ausgedrückt. Das gilt für alle Zeichenwerte, egal ob es sich dabei um ein lateinisches Zeichen oder ein anderes Unicode-Zeichen handelt. Als einzelne Zeichen gelten alle druckbaren Zeichen mit Ausnahme des Bindestrichs (-) und des Backslash (\). Die Zeichen werden in Unicode-Format gespeichert. Zeichenliterale lassen sich aber auch in Escape-Format darstellen. Die Escape-Zeichenliterale beginnen immer mit dem Backslash-Zeichen. Diesem folgt eines der Zeichen (b, t, n, f, r, ", ` oder \) oder eine Serie von Oktalziffern (3-stellig) oder ein u gefolgt von einer 4-stelligen Serie Hexadezimalziffern, die für ein nicht zeilenbeendendes Unicode-Zeichen stehen. Die vier Stellen der hexadezimalen Unicode-Darstellung (/u0000 bis /uFFFF) stehen damit für 65.535 mögliche Kodierungen. Damit ist es insbesondere möglich solche Zeichen innerhalb von Zeichenketten darzustellen, die ohne diese Maskierung eine besondere Funktion haben. Dies ist z.B. der Fall, wenn Sie das doppelte Hochkommata innerhalb einer Zeichenkette ausgeben wollen (es dient normalerweise als Begrenzung von Zeichenketten). In der nachfolgende Tabelle sehen Sie einige Beispiele:

Escape-Literal Unicode--Steuersequenz Oktal-Sequenz Bedeutung
\b \u0008 \010 Rückschritt (Backspace)
\t \u0009 \011 Tab
\n \u000a \012 Neue Zeile
\f \u000c \014 Formularvorschub (Formfeed)
\r \u000d \015 Wagenrücklauf (Return)
\" \u0022 \042 Doppeltes Anführungszeichen
\' \u0027 \047 Einfaches Anführungszeichen
\\ \u005c \134 Backslash

Tabelle 6.6:   Zeichenliterale in verschiedenen Darstellungen

Das nachfolgende kleine Beispiel zeigt die Verwendung von so maskierten Sonderzeichen, die innerhalb von Zeichenketten verwendet werden. Das Beispiel bewirkt nicht mehr, als Text auf dem Bildschirm auszugeben, der von Sonderzeichen durchsetzt ist.

Geben Sie den nachfolgenden Quelltext ein.

Abbildung 6.2:  Ausgabe von Sonderzeichen über Zeichenliterale


class Literale {
public static void main(String argv[]) {
System.out.println("Tab\tTab\tTab");
System.out.println("Einfache Hochkommata \'");
System.out.println("Eine neue Zeile\n\042, die mit einem doppelten Hochkomma beginnt und 
endet.\"");
System.out.println(
 "Jetzt kommt noch ein Backslash\\.");
 }
}

Speichern und kompilieren Sie die Datei.

Lassen Sie das Programm laufen.

Die als oktale Escape-Literale bezeichneten Zeichenliterale können zur Darstellung aller Unicode-Werte von \u0000 bis \u00ff (alte ASCII-Begrenzung) benutzt werden. Bei oktaler Darstellung mit Basis 8 ist diese Darstellung auf \000 bis \377 begrenzt. Beachten Sie, dass Oktalzahlen nur von 0 bis einschließlich 7 gehen.

Die Unicode-Zeichenliterale werden schon zu einem sehr frühen Zeitpunkt vom Java- Compiler javac interpretiert. Wenn man daher die Escape-Unicode-Literale dazu verwendet, ein zeilenbeendendes Zeichen, wie zum Beispiel Wagenrücklauf oder neue Zeile, darzustellen, wird das Zeilenende-Zeichen vor dem schließenden einfachen Anführungszeichen erscheinen. Das Resultat ist dann ein Kompilierfehler. Benutzen Sie also besser nicht das \u-Format, um ein Zeilenende-Zeichen darzustellen. Verwenden Sie statt dessen die Zeichen \n oder \r.

Zeichenketten-Literale

Zeichenketten-Literale sind aus mehreren Zeichenliteralen zusammengesetzte Ketten (Strings)2. Bei Zeichenketten-Literalen werden null oder mehr Zeichen in (doppelte) Anführungszeichen dargestellt. Java erzeugt Zeichenketten als Instanz der Klasse String. Damit stehen alle Methoden der Klasse String zur Manipulation einer Zeichenkette zur Verfügung. Obwohl viele Merkmale von Zeichenketten identisch zu den Merkmalen von Zeichenarrays sind, sind sie im Unterschied zu C/C++ keine einfachen Zeichenarrays.

Zeichenketten-Literale stehen zwischen zwei doppelten Anführungszeichen und können Steuerzeichen wie Tabulatoren, Zeilenvorschübe, nichtdruckbare Unicode-Zeichen oder druckbare Unicode-Spezialzeichen enthalten. Bei den verwendeten Zeichen kann es sich wie bei Zeichenliterale gleichfalls um Escape-Sequenzen handeln.

Beispiele:

"Safety First"
"2 CV"
"DS 21"
""  // leere Zeichenkette

Beide Anführungszeichen müssen in derselben Zeile des Quellcodes stehen, damit die Zeichenkette nicht automatisch ein Zeichen für eine neue Zeile enthält.

Wenn Sie den Effekt einer neuen Zeile innerhalb einer Zeichenkette erreichen wollen, müssen Sie eine Escape-Sequenz wie zum Beispiel \n oder \r benutzen. Die Zeichen für doppelte Anführungszeichen (") und Backslash (\) müssen ebenfalls durch die Verwendung von Escape-Sequenzen (\" und \\) dargestellt werden (wir haben es in dem letzten Programmbeispiel verwendet).

Beispiele:

"Tic \t Tac \t Toe "
"2 \n CV"

Einige Anmerkungen zu Zeichenketten:

  • Da Zeichenketten in Java native Datentypen sind, lässt sich eine Addition von Zeichenketten mit dem Verknüpfungsoperator (+) realisieren. Wenn Sie eine Zeichenkette aus mehreren kleinen Zeichenketten zusammengesetzten wollen, können Sie diese einfach mit dem Zeichenkettenverknüpfungs-Operator (+) verbinden.
  • Wenn ein Wert, der keine Zeichenkette ist, mit einer Zeichenkette verbunden wird, so wird er vor dem Verbinden automatisch zu einer Zeichenkette konvertiert. Das bedeutet, dass beispielsweise auch ein numerischer Wert einer Zeichenkette hinzugefügt werden kann. Der numerische Wert wird zu einer entsprechenden Ziffernzeichenfolge konvertiert, die der ursprünglichen Zeichenkette dann hinzugefügt wird.
  • Zeichenketten sind in Java auf der anderen Seite Objekte, oder genauer: Referenztypen. Als Instanzen der Klasse String lassen sich auf Zeichenketten viele nützlich Methoden dieser Klasse anwenden. Diese Methoden können zum Vergleichen oder Durchsuchen von Zeichenketten verwendet werden, oder dienen zum Extrahieren einzelner Zeichen. Wichtige Methoden sind equals(), die Zeichenketten auf Gleichheit überprüft, oder length(), die die Länge eines Strings zurückgibt.

Boolesche Literale

Es gibt, wie schon mehrfach erwähnt, nur zwei boolesche Literale: true und false. Es gibt keinen Nullwert und kein numerisches Äquivalent.

Trennzeichen

Trennzeichen sind in Java speziellen Token, die nur aus einem einzigen Zeichen bestehen, und andere Token trennen. Java kennt neun Trennzeichen:

Token Beschreibung
( Der Token »KlammerAuf« wird sowohl zum Öffnen einer Parameterliste für eine Methode als auch zur Festlegung eines Vorrangs für Operationen in einem Ausdruck benutzt.
) Der Token »KlammerZu« wird sowohl zum Schließen einer mit dem Token »KlammerAuf« geöffneten Parameterliste für eine Methode als auch zur Beendigung eines mit dem Token »KlammerAuf« festgelegten Vorrangs für Operationen in einem Ausdruck benutzt.
{ Der Token »GeschweifteKlammerAuf« wird zu Beginn eines Blocks mit Anweisungen oder einer Initialisierungsliste gesetzt.
} Der Token »GeschweifteKlammerZu« wird an das Ende eines mit dem Token »GeschweifteKlammerAuf« geöffneten Blockes mit Anweisungen oder einer Initialisierungsliste gesetzt und schließt den Block wieder.
[ Der Token »EckigeKlammerAuf« steht vor einem Ausdruck, der als Index für ein Datenfeld dient.
] Der Token »EckigeKlammerZu« folgt einem Ausdruck, der als Index für ein Datenfeld dient und beschließt den Index.
; Das Semikolon dient sowohl zum Beenden einer Ausdrucksanweisung, als auch zum Trennen der Teile bei einer for-Anweisung.
, Der Token »Komma« ist multifunktional und wird in vielen Zusammenhängen als Begrenzer verwendet.
. Der Token »Punkt« wird zum einen als Dezimalpunkt, zum anderen als Trennzeichen von Paketnamen, Klassennamen oder Methoden- und Variablennamen benutzt.

Tabelle 6.7:   Java-Trennzeichen

6.1.6 Operatoren

Operatoren sind dazu da, eine Aktion zu spezifizieren, die mit einem oder mehreren gegebenen Operanden durchgeführt werden soll. In Java werden die Operatoren in mehreren Kategorien eingeteilt. Es gibt fünf arithmetische Operatoren (+, -, *, /, %), sechs Zuweisungsoperatoren (=, +=, *=, -=, /=, %=), einen Dekrementoperator (--), einen Inkrementoperator (++), vier bitweise arithmetische Operatoren (&, |, ^, ~), drei bitweise Verschiebungsoperatoren (<<, >>, >>>), sechs bitweise Zuweisungsoperatoren (&=, |=, ^=, <<=, >>=, >>>=), fünf Vergleichsoperatoren (==, !=, <>, <=, >=), drei logische Vergleichsoperatoren (&&, ||, !) und zwei Operatoren, die innerhalb if-then-else-Konstrukten als Ersatz für eine ausführliche Schreibweise fungieren, wenn sie zusammen benutzt werden (?, :). Dazu kommen noch der Typcast-Operator, der instanceof-Operator und der new-Operator, die aber an dieser Stelle nicht besprochen werden sollen. Die anderen Java-Operatoren sollen nun nach Kategorien getrennt beschrieben werden.

Arithmetische Operatoren

Arithmetische Operatoren benutzen ein oder zwei Operanden. In Fall von zwei Operanden sind diese entweder ganzzahlige Werte oder Fließkommazahlen. Als Rückgabe einer arithmetischen Operation erhalten Sie einen neuen Wert, dessen Datentyp sich auf Grund der Datentypen der Operanden wie folgt ergibt:

  • Zwei ganzzahlige Datentypen (byte, short, int oder long) als Operanden ergeben immer einen ganzzahligen Datentyp als Ergebnis. Dabei kann als Datentyp des Ergebnisses immer nur ein Datentyp int oder long entstehen. byte und short sind nicht möglich, und der Datentyp long entsteht dann und nur dann, wenn einer der beiden Operanden bereits vom Datentyp long war oder das Ergebnis von der Größe her nur als long dargestellt werden kann.
  • Zwei Fließkommatypen als Operanden ergeben immer einen Fließkommatypen als Ergebnis. Die Anzahl der Stellen des Ergebnisses ist immer das Maximum der Stellenanzahl der beiden Operanden.
  • Wenn die Operanden ein ganzzahliger Typ und ein Fließkommatyp sind, ist das Ergebnis immer ein Fließkommatyp.

Java kennt die folgenden arithmetischen Operatoren mit zwei Operanden:

Operator Bedeutung Beispiel
+ Additionsoperator 3 + 4
- Subtraktionsoperator 4 - 3
* Multiplikationsoperator 2 * 3
/ Divisionsoperator 2 / 3
% Modulo-Operator (gibt den Rest einer Division zurück) 15 % 9

Tabelle 6.8:   Die arithmetischen Java-Operatoren mit zwei Operanden

Die meisten Operatoren sind sicher klar. Eine Ausnahme ist möglicherweise der Modulo-Operator. Dieser ist oft nur Profis bekannt, was aber seine Bedeutung auf keinen Fall mindert. Viele Verschlüsselungs- und Komprimierungsverfahren kommen ohne ihn nicht aus und auch sonst ist er - gerade bei etwas komplexeren Anwendungen - unabdingbar. Der arithmetische Modulo- oder Rest-Operator gibt den Rest einer Division zurück. Wenn der Ausdruck x%y ausgewertet wird, ist der Rückgabewert der Operation der Rest der Division. Der Begriff Modulo stammt aus der Mathematik und wird hauptsächlich in Algebra und Zahlentheorie verwendet. Zwei ganze Zahlen werden als »kongruent modulo einem Faktor n« bezeichnet, wenn ihre Differenz ein Vielfaches von dem Faktor n ist. Sofern sie nicht »kongruent modulo einem Faktor n« sind, bleibt ein Rest. Alles klar? Mathematik ist doch einfach schön, oder ;-)?! Ein paar Beispiele zur Verdeutlichung:

Operation Rest Erklärung
1%7 1 Die Zahl 7 geht 0-mal in die Zahl 1 und als Rest bleibt 1. 1%7 = (0*7) - (0*7) + 1 = 1
4%7 4 Die Zahl 7 geht 0-mal in die Zahl 4 und als Rest bleibt 4. 4%7 = (0*7) - (0*7) + 4 = 4
8%7 1 Die Zahl 7 geht 1-mal in die Zahl 8 und als Rest bleibt 1. 8%7 = (1*7) - (1*7) + 1 = 1
9%7 2 Die Zahl 7 geht 1-mal in die Zahl 9 und als Rest bleibt 2. 9%7 = (1*7) - (1*7) + 2 = 2
13%7 6 Die Zahl 7 geht 1-mal in die Zahl 13 und als Rest bleibt 6. 13%7 = (1*7) - (1*7) + 6 = 6
14%7 0 Die Zahl 7 geht genau 2-mal in die Zahl 14. Es bleibt kein Rest übrig. 14%7 = (2*7) - (2*7) + 0 = 0
7000000%7 0 Auch hier gilt trotz der großen Zahl vor dem Operator: die Division geht ohne Rest auf. 7000000/7 = (1000000*7) - (1000000*7) + 0 = 0
13%11 2 Noch ein Beispiel mit Rest und einem anderen Modulo-Faktor. Die Zahl 11 geht 1-mal in die Zahl 13 und als Rest bleibt 2. 13/11 = (1*11) - (1*11) + 2 = 2

Tabelle 6.9:   Modulo-Beispiele mit Ganzzahlen

C-Programmierer kennen den Modulo-Operator von Ganzzahlen und Java erweitert den Rest-Operator sogar, weil auch Operationen mit Fließkommazahlen definiert sind! Es ist einfach die natürliche Fortsetzung der Operation auf die Menge der Fließkommazahlen. Der ausgegebene Wert ist immer noch der »Rest nach der Division«. Auch hier ein paar Beispiele zur Verdeutlichung:

Operation Rest Erklärung
12%2.5 2.0 Die Zahl 2.5 geht 4-mal in die Zahl 12 und als Rest bleibt 2. Allerdings als Fließkommazahl. 12/2.5 = 2.0
4%3.3 0.7 Die Zahl 3.3 geht 1-mal in die Zahl 4 und als Rest bleibt 0.7. Natürlich als Fließkommazahl. 4/3.3 = 0.7

Tabelle 6.10:   Modulo-Beispiele mit Fließkommazahlen

Die technische Formel, um den Wert von x%y zu bestimmen, wobei wenigstens x oder y ein Fließkommatyp ist, sieht folgendermaßen aus:

x-((int)(x/y)*y)

Der (int)-Operator ist ein Beispiel für den Casting-Operator, zu dem wir später noch kommen werden.

Java kennt auch einstellige arithmetische Operatoren. So gibt es zwei einstellige arithmetische Operatoren (d.h. mit nur einem Operanden) in Java, die eine recht offensichtliche Bedeutung haben und dem Operanden einfach vorangestellt werden:

  • Die einstellige arithmetische Negierung: -
  • Das Gegenteil der arithmetischen Negierung: +

Die einstellige Negierung ergibt die arithmetische Vorzeichenumdrehung ihres numerischen Operanden. Daher ergibt ein Ausdruck wie beispielsweise -x das arithmetisch Negative von jedem beliebigen Wert x. Der einstellige Operator + tauchte das erste Mal in ANSI C auf und wurde hauptsächlich aus Symmetriegründen eingeführt. Er gibt ganz einfach den Wert seines Operanden aus; mit anderen Worten: Er tut gar nichts!

Zu den einstelligen arithmetischen Operatoren werden auch die Inkrement-/Dekrement-Operatoren gezählt. Sie werden nur in Verbindung mit einem ganzzahligen oder einem Fließkommaoperanden benutzt. Inkrement- und Dekrement-Operatoren sind C-Programmierern altvertraut. Sie werden zum Auf- und Abwerten eines einzelnen Wertes verwendet und sind zwei der Bestandteile einer Programmiersprache wie C, die bei häufiger Verwendung den Begriff »Lesbarkeit« bei einem Quelltext ad absurdum führen. Viele C-Profis werden mir da widersprechen, aber Abkürzungen helfen meist nur demjenigen, der einen Quelltext erstellt und nicht demjenigen, der das Programm warten soll (dabei schließe ich mit ein, dass es sich um dieselbe Person handelt). Die Verwendung von rein aus Abkürzungsgründen eingeführten Konstrukten muss bei einer professionellen Programmierung mit einer solch umfangreichen Dokumentation innerhalb des Quelltexten per Kommentare einhergehen, sodass die Ersparnis gegenüber normaler Zuweisung aufgehoben wird. Bezüglich der Performance wird kein Vorteil erzielt, denn der Compiler optimiert sowieso.

Dennoch haben die Operatoren natürlich ihre Berechtigung und die Warnung beinhaltet ausdrücklich die häufige und vor allem verschachtelte Verwendung.

Der Inkrement-Operator (++) erhöht den Wert des Operanden um 1. Die Reihenfolge von Operand und Operator ist wichtig. Wenn der Operator vor dem Operanden steht, erfolgt die Erhöhung des Wertes, bevor der Wert dem Operanden zugewiesen wird. Wenn er hinter dem Operanden steht, erfolgt die Erhöhung, nachdem der Wert bereits zugewiesen wurde.

Der Dekrementoperator (--) arbeitet analog. Er erniedrigt den Wert des Operanden um 1. Die Reihenfolge von Operand und Operator ist auch hier von Bedeutung. Wenn der Operator vor dem Operanden steht, erfolgt die Erniedrigung des Wertes, bevor der Wert dem Operanden zugewiesen wird. Wenn er hinter dem Operanden steht, erfolgt die Erniedrigung, nachdem der Wert bereits zugewiesen wurde.

Testen wir insbesondere die Inkrement-/Dekrement-Operatoren in einem kleinen Beispiel.

Geben Sie den nachfolgenden Quelltext ein.


class OpTest {
  public static void main(String args[])  {
  int i=1;
  int j=3;
  System.out.println("Startwert von i: " + i);
  System.out.println("i++:" + i++);
  System.out.println("++i:" + ++i);
  System.out.println("i*j:" + i*j);
  System.out.println("i%j:" + i%j);
  System.out.println("Startwert von j: " + j);
  System.out.println("j--:" + j--);
  System.out.println("--j:" + --j);
  }
}

Abbildung 6.3:  Die Wirkung von Operatoren

Speichern und kompilieren Sie die Datei.

Lassen Sie das Programm laufen.

Beachten Sie die Stellen, wo mit ++ der Wert erhöht, aber noch der nicht erhöhte Wert ausgegeben wird.

Arithmetische Zuweisungsoperatoren

In Java gibt es neben dem direkten Zuweisungsoperators (=) noch die arithmetischen Zuweisungsoperatoren. Diese sind eigentlich nur als Abkürzung für arithmetisch Operationen zu verstehen. Wie auch die arithmetischen Operatoren, können sie sowohl mit ganzen Zahlen als auch mit Fließkommazahlen verwendet werden. Das Ergebnis einer Zuweisung über einen arithmetischen Zuweisungsoperatoren steht immer auf der linken Seite.

Operator Bedeutung Beispiel Entspricht
+= Additions- und Zuweisungsoperator x += 5 x = x + 5
-= Subtraktions- und Zuweisungs-operator x -= 3 x = x - 3
*= Multiplikations- und Zuweisungs-operator x *= 10 x = x * 10
/= Divisions- und Zuweisungsoperator x /= 3 x = x / 3
%= Modulo- und Zuweisungsoperator x %= 7 x = x % 7
= direkter Zuweisungsoperator x = 3  

Tabelle 6.11:   Die arithmetischen Zuweisungsoperatoren

Bitweise arithmetische Operatoren

Bitweise Arithmetik wird im Wesentlichen zum Setzen und Testen einzelner Bits und Kombinationen einzelner Bits innerhalb einer Variablen benutzt. Es gibt eigentlich wenig triftige Gründe, unter Java solch eine bitweise Arithmetik anzuwenden. Die wichtigsten Gründe zur Benutzung bitweiser Operatoren betreffen die direkte Kommunikation mit Hardwarekomponenten oder die Arbeit mit Komprimierungsprozessen (dazu etwas mehr im Anhang unter dem Abschnitt zur Arbeitsweise von Komprimierungsprogrammen) bzw. Verschlüsselungsoperationen. Das wahrscheinlich wichtigste Argument ist, dass bestimmte Vorgänge bei der Arbeit auf Bitebene bezüglich der Performance verbessert werden können.

Binäre Arbeit auf Datentypen ist in Java im Vergleich zu anderen Programmiertechniken ziemlich ungefährlich. Da die Datentypen auf allen Plattformen gleich implementiert sind und auch sonst extrem stabil gehandhabt werden, kann die binäre Vorgehensweise unter Java nicht zu den Problemen führen, die bei anderen Sprachen wie C/C++ die Geschichte recht heikel machen können (Zugriff auf falsche Speicherbereiche, Zerstören von Nachbarinformationen usw.).

Bitweise Arithmetik ist nur für die vier Integer-Typen und für Zeichentypen definiert, nicht aber für boolesche Typen und Fließkommatypen.

Operator Beschreibung Bedeutung
& Bitweiser AND-Operator Die Operation x & y verknüpft alle korrespondierenden Bits von x und y per UND.
| Bitweiser OR-Operator Die Operation x | y verknüpft alle korrespondierenden Bits von x und y per ODER.
^ Bitweiser XOR-Operator Die Operation x ^ y verknüpft alle korrespondierenden Bits von x und y per EXKLUSIV-ODER.
~ Bitweiser Komplement-Operator Einstelliger, dem Operanden vorangestellter Operator, der alle Bits des Operanden invertiert.

Tabelle 6.12:   Die Bit-Operatoren von Java

Wir werden in den folgenden Beispielen Binär- und Hexadezimalrechnung verwenden. Zu beiden Themen finden Sie einen kleinen Exkurs im Anhang.

Anhand von Variablen des Typs byte lassen sich Binäroperationen am leichtesten beschreiben, aber der gewählte Beispieldatentyp ist ansonsten willkürlich.

Ohne Vorzeichen besteht ein Byte aus 8 Bits, die wiederum nur die Werte 1 oder 0 annehmen können. Jede binäre Darstellung von 0 und 1 steht für eine dezimale Zahl. Die 8-Bit-Binärkkombination 00101010 steht für die Zahl 42. In Hexadezimalzahlen wird dies durch 0x2A ausgedrückt.

Für die bitweise Arithmetik gelten nun die folgenden einfachen Regeln:

Bitweiser AND-Operator (&)

Wenn Sie den AND-Operator (&) mit zwei Bytes benutzen und das Ergebnis in einem dritten Byte ablegen, dann hat das resultierende Byte nur für die Bits den Wert 1, wenn alle beide Operanden an der gleichen Stelle Bits mit dem Wert 1 hatten.

Beispiele:

00101010 & 00101010 = 00101010
00101010 & 00001000 = 00001000
00101010 & 10000000 = 00000000
11110000 & 00001111 = 00000000
10101010 & 01010101 = 00000000
11100011 & 11101100 = 11100000

Tabelle 6.13:   Beispiele für &

Die Benutzung von AND hat immer das Resultat, dass maximal die gleichen oder weniger Bits auf 1 gesetzt werden. Das Resultat ist also immer die gleiche oder eine kleinere Dezimal- oder Hexadezimalzahl als das Maximum der beiden Zahlen vorher. Wir machen uns einmal den Spaß und stellen obige bitweise Arithmetik im Dezimal- und im Hexadezimalsystem dar (um deutlich zu machen, dass & nichts mit dem normalen arithmetischen + zu tun hat).

2A & 2A = 2A
2A & 8 = 8
2A & 80 = 0
2A & F = 0
AA & 155 = 0
E3 & EC = E0

Tabelle 6.14:   Hexadezimale Beispiele für &

42 & 42 = 42
42 & 8 = 8
42 & 128 = 0
42 & 15 = 0
170 & 341 = 0
227 & 236 = 224

Tabelle 6.15:   Dezimale Beispiele für &

Bitweiser OR-Operator (|)

Wenn Sie den OR-Operatoren (|) mit zwei Bytes benutzen und das Ergebnis in einem dritten Byte ablegen, dann hat das resultierende Byte nur für die Bits den Wert 1, wenn mindestens einer der Operanden ein Bit an dieser Position mit dem Wert 1 hatte.

Nehmen wir die oben angeführten Beispiele zur Hand:

00101010 | 00101010 = 00101010
00101010 | 00001000 = 00101010
00101010 | 10000000 = 10101010
11110000 | 00001111 = 11111111
10101010 | 01010101 = 11111111
11100011 | 11101100 = 11101111

Tabelle 6.16:   Beispiele für |

Die Benutzung von OR-Operator (|) resultiert immer darin, dass die gleichen oder mehr Bits auf 1 gesetzt werden.

Bitweiser XOR -Operator (^)

Wenn Sie den XOR-Operator (^) (EXKLUSIV-ODER) mit zwei Bytes benutzen und das Ergebnis in einem dritten Byte ablegen, dann hat das resultierende Byte für ein Bit nur dann den Wert 1, wenn das dazugehörige Bit in genau einem der beiden Operanden-Bytes gesetzt wird. Es muss und darf nur einem der Operanden ein Bit an dieser Position auf 1 gesetzt sein.

Ein Hinweis zur Eingabe von ^: Vielen Anwendern ist sicher schon aufgefallen, dass die Eingabe von ^ nicht so ganz logisch erfolgt (etwa, wenn sie in Excel potenzieren wollen). Sie müssen nach der Betätigung der ^-Taste eine weitere Taste (etwa die Leertaste) drücken - erst dann wird das ^-Zeichen auf dem Bildschirm erscheinen.

Nehmen wir wieder die oben angeführten Beispiele:

00101010 ^ 00101010 = 00000000
00101010 ^ 00001000 = 00100010
00101010 ^ 10000000 = 10101010
11110000 ^ 00001111 = 11111111
10101010 ^ 01010101 = 11111111
11100011 ^ 11101100 = 00001111

Tabelle 6.17:   Beispiele für ^

Der Bitweiser Komplement-Operator (~)

Der bitweise Komplement-Operator (~) unterscheidet sich massiv von den anderen drei bitweisen Operatoren, denn Komplementieren ist eine einstellige bitweise Operation (d.h. nur ein Operand). Wenn ein Byte komplementiert wird, werden alle seine Bits invertiert. Auch hier wollen wir zur Verdeutlichung ein paar Beispiele heranziehen:

~ 00101010 = 11010101
~ 00001000 = 11110111
~ 10000000 = 01111111
~ 00001111 = 11110000
~ 01010101 = 10101010
~ 11101100 = 00010011

Tabelle 6.18:   Beispiele für ~

Wir wollen die bitweise Arithmetik in einem Beispiel testen und dabei die verschiedenen Operatoren verwenden.

class BitOp {
public static void main(String argv[]) {
char meinChar1 = 'a';
char meinChar2 = 'b';
System.out.println((int)meinChar1 + " " + (int)meinChar2);
System.out.println((meinChar1 + meinChar2) + " " + (char)(meinChar1 + meinChar2));
System.out.println((meinChar1 & meinChar2) + " " + (char)(meinChar1 & meinChar2));
System.out.println((meinChar1 | meinChar2) + " " + (char)(meinChar1 | meinChar2));
System.out.println((meinChar1 ^ meinChar2) + " " + (char)(meinChar1 ^ meinChar2));
System.out.println(~meinChar1 + " " + (char)(~meinChar1));
}  }

Das Beispiel arbeitet mit zwei char-Variablen, die die Werte a und b zugewiesen bekommen. Das Zeichen a hat den Wert 97 in der Unicode-Darstellung, das Zeichen b den Wert 98 (das wird mit der ersten Ausgabe im Beispiel demonstriert). Das entspricht binär 0110 0001 und 0110 0010. Beachten Sie, dass das Beispiel Casting anwendet.

Wenn nun die beiden char-Variablen einfach addiert werden, werden deren Werte dezimal in der Unicode-Kodierung addiert. Das ergibt 195, was binär 1100 0011 entspricht (die zweite Ausgabe).

Bitweise Addition führt dazu, dass binär 0110 0000 entsteht (dann und dann entsteht der Wert 1, wenn alle beide Operanden an der gleichen Stelle Bits mit dem Wert 1 hatten). Das entspricht dem Dezimalwert 96 (die dritte Ausgabe).

Analog verhält es sich mit binärem Oder (dezimal entsteht 99 und binär 0110 0011) und den beiden anderen binären Operationen, was die nachfolgenden Ausgaben demonstrieren.

Abbildung 6.4:  Die Ergebnisse binärer Operationen

Bitweise Verschiebungsoperatoren

Bitweise Verschiebungsoperatoren verschieben die Bits in der Darstellung einer ganzen Zahl. Die Bits des ersten Operanden werden um die Anzahl an Positionen verschoben, die im zweiten Operanden angegeben wird. Im Fall der Verschiebung nach links ist es immer eine Null, mit der die rechte Seite aufgefüllt wird. Dieser Vorgang entspricht dem Multiplizieren mit 2 hoch der Zahl, die durch den zweiten Operanden definiert wird. Der normale Verschiebungsoperator nach rechts vervielfacht das Vorzeichenbit. Dieser Vorgang entspricht der Division durch 2 hoch der Zahl, die durch den zweiten Operanden definiert wird. Die Verschiebung nach rechts mit Füllnullen vervielfacht eine Null von der linken Seite.

Operator Beschreibung Bedeutung
<< Operator für bitweise Verschiebung nach links Die Operation x << y bedeutet, dass alle Bits von x um y Positionen nach links verschoben werden. Die rechte Seite der Darstellung von x wird mit Nullen aufgefüllt.
>> Operator für bitweise Verschiebung nach rechts Die Operation x >> y bedeutet, dass alle Bits von x um y Positionen nach rechts verschoben werden. Dabei wird das Vorzeichenbit vervielfacht.
>>> Operator für bitweise Verschiebung nach rechts mit Füllnullen Die Operation x >>> y bedeutet, dass alle Bits von x um y Positionen nach rechts verschoben werden. Die linke Seite der Darstellung von x wird mit Nullen aufgefüllt.

Tabelle 6.19:   Die bitweisen Verschiebungsoperatoren

01001111 << 1 = 10011110
00111100 << 2 = 11110000
01001111 >> 1 = 00100111
11110000 >> 2 = 11111100
01001111 >>> 1 = 00100111
11110000 >>> 2 = 00111100

Tabelle 6.20:   Beispiele für bitweise Verschiebung

class BitVerschieb {
 public static void main(String argv[]) {
 int a = 42;
 int b;
 b = a >> 1;
 System.out.println(b);
 System.out.println(b << 2);
 System.out.println(b >>> 1);
 }  }

Das Beispiel arbeitet mit zwei int-Variablen. Die Variable a bekommt den Startwert 42 zugewiesen und b den Wert, der aus der Operation a >> 1 entsteht. Da dies der Division durch 2 entspricht, muss das Ergebnis 21 sein (was die erste Ausgabe demonstriert). Binär wird aus der Darstellung 0010 1010 für 42 jedes Bit um den Faktor 1 nach rechts verschoben (0001 0101), was dem Wert 21 in der Unicode-Darstellung entspricht.

Die zweite Operation ist das Verschieben der Bits um zwei Stellen nach links (0101 0100), was dezimal der Multiplikation mit dem Faktor 4 entspricht.

Die dritte Operation entspricht verschiebt die Binärdarstellung 0001 0101 (den Wert von b) mit Füllnullen um eine Stelle nach links. Das ergibt 0000 1010, was dezimal dem Wert 10 entspricht.

Abbildung 6.5:  Die Ergebnisse binärer Verschiebe-Operationen

Bitweise Zuweisungsoperatoren

Bitweise Zuweisungsoperatoren verwenden einen Wert, führen eine entsprechende bitweise Operation mit dem zweiten Operanden durch und legen das Ergebnis als Inhalt des ersten Operanden ab.

Operator Beschreibung
&= Bitweiser AND-Zuweisungsoperator.
|= Bitweiser OR-Zuweisungsoperator.
^= Bitweiser XOR-Zuweisungsoperator.
<<= Zuweisungsoperator für die bitweise Verschiebung nach links.
>>= Zuweisungsoperator für die bitweise Verschiebung nach rechts.
>>>= Zuweisungsoperator für die bitweise Verschiebung nach rechts mit Füllnullen.

Tabelle 6.21:   Die bitweisen Zuweisungsoperatoren

Vergleichsoperatoren

Vergleichsoperatoren haben zwei Operanden gleichen Typs und vergleichen diese. Als Rückgabewert der Operation entsteht immer ein boolescher Wert (true oder false).

Der Rückgabewert eines Java-Vergleichs ist auf jeden Fall ein Wahrheitswert. Es ist in Java nicht möglich, einen numerischen Rückgabewert zu erhalten (wie etwa in C/C++, wo man auf Gleichheit mit 0 oder Ungleichheit testen kann).

Die Vergleichoperatoren lassen sich in Bezug auf Vergleichslogik wie folgt einteilen:

1. Relationale Operatoren
2. Logische Gleichheitsoperatoren

Relationale Operatoren sind zum Ordnen von Größen bestimmt. Die Ordnung erfolgt beispielsweise danach, ob ein Wert größer oder kleiner als ein anderer Wert ist.

Die Werte zweier Variablen vom Typ char können ebenfalls verglichen werden und sind in der allgemeinen Beschreibung bereits enthalten. Es werden die Variablen vom Typ char bei der Verwendung eines Vergleichsoperators wie ganze 16-Bit-Zahlen (Werte 0 bis 65535) entsprechend ihrer Unicode-Kodierung behandelt.

Vergleichsoperatoren werden oft in Schleifen verwendet, die auf einen booleschen Wert abprüfen.

Operator Beschreibung
== Gleichheitsoperator
!= Ungleichheitsoperator
< Kleiner-als-Operator
> Größer-als-Operator
<= Kleiner-als-oder-gleich-Operator
>= Größer-als-oder-gleich-Operator

Tabelle 6.22:   Die logischen Java-Vergleichsoperatoren

Und wenn es für Profis jetzt auch als lächerlicher Hinweis erscheint: Verwechseln Sie den Gleiheitsoperator (zwei hintereinander folgenden Gleichheitszeichen) nicht mit dem Zuweisungsoperator (ein einzelnes Gleichheitszeichen). Einige andere Programmiersprachen unterscheiden dahingehend übrigens nicht und verwenden das einfache Gleichzeichen für beide Fälle (Vergleich und Zuweisung).

Wir wollen an dieser Stelle wieder ein weiteres kleines Java-Programm schreiben, um sowohl die Schleife in Verbindung mit boolesche Werten zu erläutern als auch die Unicode-Kodierung von einigen Zeichen ansehen. Damit wir schon so langsam einen etwas anspruchsvolleren Java-Quelltext erstellen, setzen wir dabei ein paar Programmiertechniken voraus, die zwar bisher noch nicht angesprochen wurden (etwa Variablen, Casting und Schleifen), vielen Lesern aber sicher aus anderen Programmiersprachen bekannt sein dürften. Wir werden zudem das Programm Schritt für Schritt durchsprechen. Wenn Ihnen dennoch das Programm zu kompliziert ist - keine Angst, wir werden die einzelnen Techniken im Laufe des Kapitels noch sehr ausführlich beschreiben.

class Zeichenspiele {
 public static void main (String args[]) { 
  int i;
  char zeichen;
  for (i=10; i < 20 ;i++) {  
    System.out.print(i);
    zeichen = (char)i;
    System.out.print(" " + (char)i + " ");
    zeichen = (char)(zeichen - 10);
    System.out.print(zeichen);
    System.out.print(" ");
    System.out.println((int)zeichen);
    System.out.println("------");
}   }  }

Was tut dieses Programm? Wenn Sie das Programm wieder eingegeben, gespeichert und kompiliert haben, können Sie es mittels des Interpreters ausführen. Auf dem Standardausgabegerät (meist dem Bildschirm) bekommen Sie eine Blockstruktur angezeigt. Insgesamt 10 Blöcke und eine Trennlinie werden ausgegeben.

Abbildung 6.6:  Die Ausgabe der Schleife

Das Programm hat eine ähnliche Struktur wie unser HelloJava - Programm und auch das Unicode-Programm. Es besteht im Wesentlichen wieder nur aus der main()-Methode, die jedes eigenständige Java-Programm benötigt. Am Anfang des Programms werden zwei Variablen vereinbart:

int i;
char zeichen;

Die erste Variable dient später als eine Zählvariable, die zweite später zur Ausgabe.

Innerhalb der main()-Methode finden Sie weiterhin eine for-Schleife. Eine solche Schleife dient dazu, die in ihrem Inneren angegebenen Anweisungen so lange zu wiederholen, wie eine bestimmte Bedingung erfüllt ist. Diese Bedingung wird von der for-Schleife über (i=10; i < 20 ;i++) abgeprüft. Solange der Vergleich in dieser Überprüfung den Wert true liefert, wird die Schleife wiederholt. Innerhalb der for-Schleife finden Sie wie bei unseren bisherigen Beispielprogrammen die Methode System.out.println() sowie die verwandte Methode System.out.print(), die ohne Zeilenvorschub arbeitet.

Die for-Schleife durchläuft 10 Durchgänge (von 10 bis inklusive 19), was die 10 Blöcke bewirkt. System.out.println("------"); erzeugt den jeweiligen Trennstrich zur optischen Unterscheidung der einzelnen Blocks.

In unserem Beispiel geben wir als Erstes die Zählvariable innerhalb der Methode System.out.print() auf dem Standardausgabegerät aus (System.out.print(i);). Damit kontrollieren wir den Schleifenverlauf und den Wert der Zählvariable.

Hinter der Zeile

zeichen = (char)i;

versteckt sich eine Zuweisung in Verbindung mit einer Technik, die sich Casting nennt. Vorerst reicht es aus, dass Sie sich darunter eine Umwandlung einer Zahl in ein char-Zeichen vorstellen. Per Casting wird der numerische Wert in das zugehörige Zeichen des Unicodes überführt. Zudem werden einige Leerstrings als optische Trennung hinzugefügt. Als Abänderung wird das Ergebnis der Aktion in der am Anfang definierten Zeichen-Variable zeichen gespeichert.

Die nachfolgende Ausgabe kennen wir. Es ist die Zeichenvariable. Wir hätten auch hier die Zeichenvariable zeichen direkt nehmen können, was wir im übernächsten Schritt tun werden.

Im Folgeschritt wird es jetzt spannend. Wir machen uns zunutze, dass die Variablen vom Typ char bei der Verwendung eines Vergleichsoperators oder Arithmetik mit Zahlen (wie hier) wie ganze 16-Bit-Zahlen (Werte 0 bis 65535) entsprechend ihrer Unicode-Kodierung behandelt werden. Die Arithmetik zeichen - 10 reduziert den Wert der Unicode-Kodierung von zeichen um den Faktor 10. Um allerdings dieses Ergebnis in einer Zeichenvariable speichern zu können, müssen wir wieder explizit Casting anwenden.

System.out.print(zeichen); ist die Kontrollausgabe. Sie sehen, dass das ausgegebene Zeichen sich von der vorherigen Ausgabe unterscheidet (um die Codeverschiebung 10 nach unten).

Der letzte Schritt vor der Trennlinie gibt den jetzigen Unicode von zeichen aus. Auch dazu verwenden wir wieder die Typumwandlung mittels Casting, nur in die Richtung »Zeichen in Ganzzahl vom Typ int«.

Die Priorität von relationalen Operatoren liegt unter der arithmetischer Operatoren, jedoch über der von Zuweisungsoperatoren.

Interessant ist die Anwendung von Vergleichsoperatoren, wenn die Operanden Objekte sind. In diesem Fall muss man massiv zwischen den Objekten und den darin gespeicherten Werten unterscheiden. Zwar bedeutet eine Objektgleichheit immer die Gleichheit der Werte (in dem Fall liegt die Referenz auf den gleichen Speicherplatz vor), jedoch gilt die Umkehrung nicht. Zwei verschiedene Objekte können selbstverständlich gleiche Werte beinhalten. Der Vergleich mittels der Vergleichsoperatoren liefert dann Ungleichheit, obwohl der Inhalt unter Umständen gleich ist. Um die Gleichheit des Inhalts zu überprüfen, gibt es die in allen Objekten verfügbare Methode equals() (vererbt von Object). Ziehen wir zur Verdeutlichung das folgende Beispiel heran, wo Vergleiche zwischen Strings und einem Random-Objekt (ein Zufallsobjekt) durchgeführt wird.

import java.util.*;
public class ObjektVergleich {
public static void main(String args[]) {
String a = new String("hallo");
String b = new String("hallo");
Random c = new Random();
Random d = new Random();  
String e = a;
System.out.println(
  "Sind a und b das gleiche Objekt? " +  (a==b));
System.out.println(
  "Haben a und b den gleichen Inhalt? " + a.equals(b));
  System.out.println(
  "Sind a und e das gleiche Objekt? " +  (a==e));
  System.out.println(
  "Haben a und e den gleichen Inhalt? " +
a.equals(e));
  System.out.println(
  "Sind c und d das gleiche Objekt? " + (c==d));
  System.out.println(
  "Haben c und d den gleichen Inhalt? " +
 c.equals(d));
  }
}

Das Programm arbeitet mit fünf Variablen: drei Strings und zwei Random-Objekte. Zwei Strings werden explizit mit dem new-Operator erzeugt, sind also explizit verschiedene Objekte. Die dritte String-Variable bekommt jedoch das erste String-Objekt zugewiesen. Es handelt sich bei a und c um eine Referenz auf den gleichen Speicherbereich, d.h., die Objekte sind identisch. Die beiden Random-Objekte sind auch im Inhalt verschieden. Schauen Sie sich die Ausgabe des Programms zur Verdeutlichung an.

Abbildung 6.7:  Unterschied von Objektgleichheit und Gleichheit  des Inhalts

Zeile 1 und 2 bestätigen, dass die beiden Variablen a und b verschiedene Objekte sind, jedoch den gleichen Wert haben, also die gleichen Zeichen in der identischen Reihenfolge enthalten sind.

Zeile 3 bestätigt, dass die beiden Variablen a und e auch im Sinn der Objektgleichheit identisch sind. Daraus folgt trivialerweise die Gleichheit des Inhalts (Zeile 4).

Zeile 5 und 6 bestätigen, dass c und d nicht identisch sind; weder bezüglich des Inhalts, noch im Sinne von Objektgleichheit.

Die logischen Vergleichsoperatoren

Die logischen Gleichheitsoperatoren sind nicht zum Ordnen gedacht, sondern sie sagen nur aus, ob zwei Werte gleich sind oder nicht.

Die Gleichheitsoperatoren haben eine niedrigere Priorität als relationalen Operatoren.

Die logischen Vergleichsoperatoren können mit Operanden jeden Typs verwendet werden. Im Fall primitiver Datentypen werden die Werte der Operanden einfach verglichen. Die Vergleiche erzeugen auf jeden Fall nur boolesche Ergebnisse (Letzteres ist jedoch identisch mit gewöhnlichen Vergleichsoperatoren). Dabei stellt Java - im Gegensatz zu vielen anderen Programmiersprachen - die UND- und ODER-Verknüpfung in zwei verschiedenen Varianten zur Verfügung. Es gibt einmal die so genannte Short-Circuit-Evaluation, zum anderen die Bewertung ohne diese Technik.

Bei der Short-Circuit-Evaluation eines logischen Ausdrucks wird von links nach rechts ausgewertet und eine Bewertung abgebrochen, wenn bereits ein ausgewerteter Teilausdruck die Erfüllung des gesamten Ausdrucks unmöglich macht. Mit anderen Worten: Eine Bewertung wird abgebrochen, wenn die weitere Auswertung eines Ausdrucks keine Rolle mehr spielt. Damit kann beispielsweise bei umfangreicheren Konstrukten eine Steigerung der Performance erreicht werden.

Operator Beschreibung Bedeutung
&& Logischer AND-Operator mit Short-Circuit-Evaluation Die Operation x && y liefert true, wenn sowohl x als auch y true sind. Ist bereits x false, wird y nicht mehr bewertet.
|| Logischer OR-Operator mit Short-Circuit-Evaluation Die Operation x || y liefert true, wenn mindestens einer der beiden Operanden true ist. Ist bereits x true, wird y nicht mehr bewertet.
! Logischer NOT-Operator Vorangestellter Operator mit einem Operanden. Umkehrung des Wahrheitswerts.

Tabelle 6.23:   Logische Operatoren   Teil 1

Analog zu den drei genannten logischen Vergleichsoperatoren gibt es auch hier die folgenden drei Operatoren, die schon bei den bitweisen Operatoren aufgetaucht sind.

Im Prinzip beinhaltet die bitweise Betrachtungsweise bereits die logische, wenn man beachtet, dass der boolesche Datentyp nur ein Bit groß ist.

Operator Beschreibung Bedeutung
& Logischer AND-Operator ohne Short-Circuit-Evaluation Die Operation x & y liefert true, wenn sowohl x als auch y true sind. Beide Operanden werden bewertet.
| Logischer OR-Operator ohne Short-Circuit-Evaluation Die Operation x | y liefert true, wenn mindestens einer der beiden Operanden true ist. Beide Operanden werden bewertet.
^ EXKLUSIV-ODER Die Operation x ^ y liefert true, wenn beide Operanden verschiedene Wahrheitswerte haben.

Tabelle 6.24:   Logische Operatoren   Teil 2

Das nachfolgende Beispiel demonstriert die Verwendung von den logischen Operatoren (auch im Hinblick auf die binäre Anwendung bei der bitweisen Betrachtungsweise).

Abbildung 6.8:  Die Anwendung  von logischen Operatoren

class ShortCircuit {
 public static void main (String args[]) 
 { 
  boolean a=true;
  boolean b=false;
  int i=4;
  int j=5;
  System.out.println(!a);
  System.out.println(a&&b);
  System.out.println(a||b);
  System.out.println(a&b);
  System.out.println(a|b);
  System.out.println(a^b);
  System.out.println(i&j);
  System.out.println(i|j);
  System.out.println(i^j);
 }
}

Der Komma-Operator wird nur zur Trennung verwendet - etwa im Rahmen von for- Schleifen. Viele Quellen zu Java geben das Komma gar nicht als eigenen Operator an. Wir kommen darauf zurück, wenn er verwendet wird. Weitere Operatoren und einige Zusatzinformationen werden wir im Zusammenhang mit Ausdrücken behandeln, weil diese Operatoren doch einige weitere Grundlagen zu Datentypen voraussetzen und diese nach den Kommentaren abgehandelt werden.

6.1.7 Operatoren-Priorität

Wie jede Programmiersprache muss auch Java Operatoren nach Prioritäten gewichten. In der folgenden Tabelle sollen sämtliche Operatoren von Java aufgelistet werden, wobei der Operator mit höchstem Vorrang ganz oben steht. Operatoren in der gleichen Zeile haben gleiche Priorität. Sämtliche Java-Operatoren bewerten mit Ausnahme der einstelligen Operatoren von links nach rechts.

Beschreibung Operatoren
Hochvorrangig . [] ()
Einstellig + - ~ ! ++ -- instanceof
Multiplikativ * / %
Additiv + -
Relational < <= >= > >
Gleichheit == !=
Bitweises AND &
Bitweises XOR ^
Bitweises OR |
Short-turn AND &&
Short-turn OR ||
Bedingung ?:
Zuweisung = und alle Zuweisungoperatoren mit verbundener Operation

Tabelle 6.25:   Die Java-Operatoren nach Priorität geordnet

6.1.8 Kommentare

Java unterstützt drei verschiedene Kommentararten. Weder Kommentare noch deren Inhalte sind als echte Token zu verstehen. Java besitzt neben den zwei in C/C++ vorhandenen Kommentartypen - den so genannten traditionellen Kommentaren aus der Tradition der C-Sprache - noch einen weiteren Kommentartyp, der vom Dokumentationstool javadoc verwendet wird.

Traditionelle Kommentare

Einer der traditionellen C-artigen Kommentare beginnt mit einem Slash, gefolgt von einem Stern (/*), schließt beliebigen Text ein und endet mit den gleichen Zeichen in umgekehrter Reihenfolge - einem Stern gefolgt von einem Slash (*/). Diese Form von Kommentar kann überall beginnen und aufhören, mit Ausnahme innerhalb eines Zeichenketten-Literals, eines Zeichenliterals und eines anderen Kommentars. Letzteres bedeutet vor allem, dass Kommentare gleichen Typs nicht verschachtelt werden können. Bei einem verschachteln Konstrukt kann es zu einem Fehler des Compilers kommen. Diese Form eines Kommentars kann sich über mehrere Zeilen erstrecken oder nur in einer einzigen Zeile (außerhalb eines Token) enthalten sein. Man nutzt sie gerne, um ganze Teile eines Programms auszukommentieren und sie bei Bedarf wieder zur Verfügung zu haben, indem man einfach Anfang- und Endzeichen des Kommentars löscht.

Der zweite der traditionellen Kommentare beginnt mit einem Doppel-Slash (//) und endet mit dem Zeilenende. Das bedeutet, alle Zeichen in einer Zeile hinter dem Doppel-Slash werden vom Compiler ignoriert. Man nutzt diese Kommentarform gerne, wenn nur eine Zeile eines Programms stört und man sie bei Bedarf wieder zur Verfügung haben möchte, indem man einfach die Kommentarzeichen löscht.

javadoc-Kommentare

Diese nicht in C/C++ vorhandene Kommentarart in Java ist ein Spezialfall der ersten Form des traditionellen Kommentars. Sie hat die gleichen Eigenschaften, allerdings kann der Inhalt dieses Kommentars noch in einer vom javadoc-Werkzeug automatisch generierten Dokumentation verwendet werden. Er beginnt mit /** und endet mit */. Innerhalb des Containers befindet sich der Kommentar. Das nachfolgende Beispiel zeigt die Anwendung.

/**
* Um was es geht.
* @see         Querverweis.
* @version        3.42
* @author        Ralph Steyer
*/
class HelloWorld {
/* traditionelle Kommentarform  */
  public static void main (String args[]) {
   System.out.println("Hello Java!"); // Ausgabe
 }
}

Beachten Sie, dass der javadoc-Kommentar außerhalb der Klassendefinition steht. Dies ist deshalb wichtig, weil der Generator bei einer Verschachtelung mit einer Klassendefinition mit Ignoranz des Kommentars reagiert. Mehr zu den Parametern innerhalb des javadoc-Kommentars finden Sie bei der Beschreibung von javadoc in dem Kapitel zu den JDK-Tools.

6.2 Typen

Ein Typ bzw. Datentyp gibt in einer Computersprache an, wie ein einfaches Objekt (wie zum Beispiel eine Variable) im Speicher des Computers dargestellt wird. Er enthält normalerweise ebenfalls Hinweise darüber, die Operationen mit und an ihm ausgeführt werden können. Viele Computersprachen lassen es beispielsweise nicht zu, dass mit einer alphanumerischen Zeichenfolge direkte arithmetische Operationen durchgeführt werden, außer es besteht eine explizite Anweisung, diese Zeichenfolge zuerst in eine Zahl umzuwandeln.

Java besitzt, wie schon mehrfach erwähnt, acht primitive Datentypen:

  • Vier Ganzzahltypen mit unterschiedlichen Wertebereichen
  • Einen logischen Datentyp
  • Zwei Gleitzahltypen mit unterschiedlichen Wertebereichen nach der internationalen Norm IEEE Standard for Binary Floating-Point Arithmetic, ANSI/IEEE Std. 754-1985 (IEEE, New York) zur Definition von Gleitpunktzahlen und Arithmetik
  • Einen Zeichentyp

Der Begriff »primitiv« ist übrigens nicht abwertend zu verstehen und erklärt sich dadurch, dass diese Datentypen im System integriert und nicht als Objekten zu verstehen sind.

An dieser Stelle soll noch einmal explizit darauf hingewiesen werden, dass in Java die primitiven Datentypen plattformunabhängig sind. Eine weitere wichtige Eigenschaft von primitiven Datentypen in Java ist, dass sie immer einen wohldefinierten Default- Anfangswert haben. Im Gegensatz zu einigen anderen Programmiersprachen muss deshalb ein primitiver Datentyp nicht unbedingt mit einem Anfangswert manuell initialisiert werden, sondern ist sozusagen von Natur aus einsatzbereit. Eine Ausnahme bilden mit primitiven Datentypen definierte lokale Variablen, welchen auf jeden Fall vor einer Verwendung ein Anfangswert zugewiesen werden muss.

Bezeichnung Länge Defaultwert Kurzbeschreibung
byte 8 Bit 0 Kleinster Wertebereich mit Vorzeichen. Wird zum Darstellen von Ganzzahlwerten (ganzzahliges Zweierkomplement) von (- 2 hoch 7 = -128) bis (+ 2 hoch 7 - 1 = 127) verwendet.
short 16 Bit 0 Kurze Darstellung von Ganzzahlwerten mit Vorzeichen als ganzzahliges Zweierkomplement von (- 2 hoch 15 = -32.768) bis (+ 2 hoch 15 - 1 = 32.767).
int 32 Bit 0 Standardwertebereich mit Vorzeichen zur Darstellung von Ganzzahlwerten (ganzzahliges Zweierkomplement). Bereich von (- 2 hoch 31 = -2.147.483.648) bis (+ 2 hoch 31 - 1 = 2.147.483.647).
long 64 Bit 0 Größter Wertebereich mit Vorzeichen zur Darstellung von Ganzzahlwerten (ganzzahliges Zweierkom-plement). Wertebereich von -9.223.372.036.854.775.808
(- 2 hoch 63) bis 9.223.372.036.854.775.807
(+ 2 hoch 63 - 1).
float 32 Bit 0.0 Kürzester Wertebereich mit Vorzeichen zur Darstellung von Gleitkommazahlwerten. Dies entspricht Fließkommazahlen mit einfacher Genauigkeit, die den IEEE 754-1985-Standard benutzen.
      Der Wertebereich liegt ungefähr zwischen +/- 3,4E+38. Es existiert ein Literal zur Darstellung von plus/minus unendlich, sowie der Wert NaN (Not a Number) zur Darstellung von nicht definierten Ergebnissen.
double 64 Bit 0.0 Größter Wertebereich mit Vorzeichen zur Darstellung von Gleitkommazahlwerten. Der Wertebereich liegt ungefähr zwischen +/- 1,8E+308. Auch diese Fließkommazahlen benutzen den IEEE-754-1985-Standard. Es existiert ein Literal zur Darstellung von plus/minus Unendlich, sowie der Wert NaN (Not a Number) zur Darstellung von nicht definierten Ergebnissen.
char 16 Bit \u0000 Darstellung eines Zeichens des Unicode-Zeichensatzes. Zur Darstellung von alphanumerischen Zeichen wird dieselbe Kodierung wie beim ASCII-Zeichensatz verwendet, aber das höchste Byte ist auf 0 gesetzt. Der Datentyp ist als einziger primitiver Java-Datentyp vorzeichenlos! Der Maximalwert, den char annehmen kann, ist \uFFFF (siehe dazu den Abschnitt zum -Unicode).
boolean 1 Bit false Diese können die Werte true (wahr) oder false (falsch) annehmen. Alle logischen Vergleiche in Java liefern den Typ boolean.

Tabelle 6.26:   Primitive Datentypen der Java-Sprache

Einige Bemerkungen zu Datentypen

  • Boolesche Variablen sind dem Java-Typ boolean zugeordnet und können nur die Werte true oder false annehmen. Die Zuordnung eines booleschen Datentyps mit einer Zahl (egal ob Null oder ungleich Null) erzeugt einen Fehler durch den Compiler. Zahlenoperationen können mit diesem Typ explizit nicht durchgeführt werden. Werte vom Typ boolean sind zu allen anderen primitiven Datentypen inkompatibel und lassen sich nicht durch Casting in andere Typen überführen.
  • Einer char-Variable kann, da sie 2 Byte lang ist, eine Ganzzahl zwischen 0 und 65535 zugewiesen werden. Ohne Konvertierung! Es wird bei einer evtl. Ausgabe dann das zugeordnete Unicode-Zeichen dargestellt. Die Umkehrung - also die Zuweisung von char-Zeichen an andere Datentypen - funktioniert nur für den Typ int ohne Probleme. Für die anderen Datentypen ist keine direkte Zuweisung möglich. Hier müssen Cast-Konstrukte helfen (Ausnahme Datentyp boolean - hier ist keinerlei Konvertierung möglich).
  • Boolesche Literale sind nur die Schlüsselwörter true und false. Diese Schlüsselwörter können in einem Java-Quelltext überall da verwendet werden, wo es Sinn macht.

Es gibt einige andere virtuelle Maschinen, die noch weitere Datentypen besitzen.

6.2.1 Operationen mit den Datentypen

Die meisten Operationen, die sich mit den unterschiedlichen Datentypen ausführen lassen, sind für alle Datentypen recht ähnlich. Es gibt jedoch einige wichtige Abweichungen. Lassen Sie uns die Operationen mit den Datentypen nach den Datentypen aufschlüsseln.

Operationen mit booleschen Variablen

Operationen mit booleschen Variablen sind in Java etwas Besonderes, denn der Datentyp ist nicht numerisch. Dennoch - viele der gleichen Operatorsymbole werden genauso wie bei anderen Ausdrücken verwendet. In den meisten Fällen handelt es sich bei diesen Bedeutungen um eine natürliche Erweiterung der Operationen, die mit ganzzahligen Typen durchgeführt werden.

Operation Name Bedeutung
= Zuweisung Einer booleschen Variable wird der Wert true oder false zugewiesen.
== Gleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann true zurückgegeben, wenn beide boolesche Operanden denselben Wert (true oder false) haben. Ansonsten wird false zurückgegeben (entspricht dem nicht-exklusiven Oder NXOR).
!= Ungleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann true zurückgegeben, wenn beide boolesche Operanden unterschiedliche Werte (true oder false) haben. Ansonsten wird false zurückgegeben (entspricht dem exklusiven Oder XOR).
! Logisches NOT Wenn der Operand false ist, wird true zurückgegeben und umgekehrt.
& AND Es wird dann und nur dann true zurückgegeben, wenn beide Operanden true sind.
¦ OR Es wird dann und nur dann false zurückgegeben, wenn beide Operanden false sind.
^ XOR Es wird true zurückgegeben, wenn genau ein Operand true ist. Man nennt dies ein exklusives Oder, weil der Wert eines Operanden für das Ergebnis true ausschließt, dass der Wert des anderen Operanden identisch ist.
&& Logisches AND Für boolesche Variablen ist das logische AND das Gleiche wie AND (&): Es wird dann und nur dann true zurückgegeben, wenn beide Operanden true sind.
¦¦ Logisches OR Für boolesche Variablen ist das logische OR das Gleiche wie OR (¦): Es wird dann und nur dann false zurückgegeben, wenn beide Operanden false sind.
?: if-else Diese Operation benötigt einen booleschen Ausdruck vor dem Fragezeichen. Wenn er true ist, wird der Wert vor dem Doppelpunkt zurückgegeben, ansonsten der Wert hinter dem Doppelpunkt.

Tabelle 6.27:   Operationen mit booleschen Variablen

Operationen mit Zeichenvariablen

Der in Java verwendete Unicode-Standard ermöglicht den Gebrauch von Alphabeten vieler verschiedener Sprachen. Das lateinische Alphabet, die Zahlen und Satzzeichen haben in der Kodierung die gleichen Werte wie der ASCII-Zeichensatz.

Zeichenvariablen können Operanden in jeder ganzzahligen Operation sein und werden dabei wie ganze 16-Bit-Zahlen ohne Vorzeichen behandelt. Das Resultat einer solchen Operation ist entweder vom Datentyp int oder long. Wenn beide Operanden char-Zeichen sind, ist auch der resultierende Datentyp immer ein Zeichen. Falls man ein numerisches Ergebnis als Zeichen ausdrücken will, muss eine explizite Umwandlung in den Datentyp char erfolgen. Wenn Sie Zeichen in einen kleineren Typ umwandeln, werden Sie unter Umständen Informationen verlieren. Dies betrifft beispielsweise den Han-Zeichensatz (Chinesisch, Japanisch oder Koreanisch), wenn Sie eine Variable vom Datentyp char in eine Variable vom Datentyp short umwandeln.

Operationen mit nur einer Zeichenvariable als Operanden (beispielsweise Zuweisungsoperationen, logische Verneinungen oder Inkrement- und Dekrementoperationen) bedürfen keiner besonderen Konvertierung. Die einstelligen Vorzeichenoperatoren (+ und -) haben keinerlei Bedeutung für Operanden vom Typ char, da diese keine Vorzeichen haben.

Operation Name Bedeutung
= Zuweisung Einer Zeichenvariable wird ein Wert zugewiesen.
== Gleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden denselben Wert (im Sinne von gleichem Unicode-Wert) haben. Ansonsten wird false zurückgegeben.
!= Ungleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden unterschiedliche Werte (im Sinne von gleichem Unicode-Wert) haben. Ansonsten wird false zurückgegeben.
<, <=, >, >= Relational Weitere Operatoren zum Vergleich innerhalb einer Kontrollstruktur. Mit diesen Operatoren wird auf Ungleichheit im Sinn »kleiner«, »kleinergleich«, »größer«, »größergleich« der Werte (im Sinne von gleichem Unicode-Wert) abgeprüft.
+,- Vorzeichen Vorzeichenoperatoren bei einem Operanden.
+, -, *, / binäre Arithmetik Additions-, Subtraktions-, Multiplikations-, und Divisionsoperatoren zur Arithmetik mit Zeichenvariablen. Die Zeichenvariablen gehen in die Berechnung mit ihrem Unicode-Wert ein.
+=, -=, *=, /= Zuweisung Additions-, Subtraktions-, Multiplikations-, und Divisionszuweisungen.
++, -- binäre Arithmetik Inkrement- und Dekrementoperatoren für den Unicode-Wert der Zeichenvariablen.
<<, >>, >>> Verschiebung Bitweise Verschiebungsoperatoren. Operatoren für bitweise Verschiebung nach links, für bitweise Verschiebung nach rechts und für bitweise Verschiebung nach rechts mit Füllnullen.
<<=, >>=, >>>= Verschiebung und Zuweisung Bitweise Verschiebungs- und Zuweisungsoperatoren (nach links, nach rechts und nach rechts mit Füllnullen).
~ Bitweises NOT Bitweiser logischer Verneinungsoperator (Komplementieren). Eine einstellige bitweise Operation. Wenn ein Zeichen komplementiert wird, werden alle seine Bits invertiert.
& Bitweises AND Wenn Sie den AND-Operator (&) mit zwei Zeichen benutzen und das Ergebnis in einem dritten Zeichen ablegen, dann hat nur das resultierende Zeichen nur für die Bits den Eintrag 1, wenn alle beide Operanden an der gleichen Stelle Bits mit dem Wert 1 hatten.
| Bitweises OR Wenn Sie den OR-Operator(|) mit zwei Zeichenvariablen benutzen und das Ergebnis in einem dritten Zeichen ablegen, dann hat nur das resultierende Zeichen nur für die Bits den Eintrag 1, wenn einer der Operanden ein Bit an dieser Position = 1 hatte.
^ Bitweises exklusives OR Wenn Sie den XOR-Operator (^) (exklusives OR) mit zwei Zeichenvariablen benutzen und das Ergebnis in einem dritten Zeichen ablegen, dann hat nur das resultierende Zeichen nur für die Bits den Eintrag 1, wenn das dazugehörige Bit in genau einem der beiden Operanden-Bytes gesetzt wird. Es muss und darf genau einer der Operanden ein Bit an dieser Position auf 1 gesetzt haben.
&=, |=, ^= Bitweise Zuweisung Bitweise AND-, OR-, exklusive OR (XOR)- und Zuweisungsoperatoren.

Tabelle 6.28:   Operationen mit Zeichen-Variablen (mindestens einer der Operanden ist vom Typ char)

class CharOperationen {
 public static void main (String args[]) { 
  char meinChar1='a';
  char meinChar2='b';
  int b=5;
  System.out.println(meinChar1==meinChar2);
  System.out.println(meinChar1==b);
  System.out.println(meinChar1<meinChar2);
 }  }

Abbildung 6.9:  Die Anwendung von char-Operationen

Diejenigen, welchen die verkürzte Schreibweise von Operatoren nicht ganz so vertraut ist, können im Anhang eine kleine Erläuterung finden.

Operationen mit Gleitkommazahlen

Gleitkommazahlen oder Fließkommazahlen bestehen in Java immer (d.h. auf jeder Plattform) aus einer so genannten Zweier-Komplement-Repräsentation der ganzen Zahlen. Der eine Teil der Darstellung wird für den Nachkomma-Anteil und das Vorzeichen verwendet, der zweite Teil ist die Darstellung eines Exponenten zur Basis 2 mit einer Ausgleichszahl.

Im Anhang finden Sie einige Hintergrundinformationen zu der Theorie des Zweier- Komplements.

Daneben existieren einige spezielle Bitkonfigurationen mit besonderer Bedeutung:

  • negative Unendlichkeit
  • Null
  • positive Unendlichkeit
  • keine Zahl

Die Fließkommaoperationen selbst sind in Java für alle Plattformen gültig, was in vielen anderen Programmiersprachen nicht zutrifft. Die meisten Fließkommaoperationen und Ganzzahloperationen sind identisch. Wichtigste Ausnahmen sind die Fließkommaoperationen, die den Nachkommateil betreffen und bei Ganzzahloperationen natürlich keinen Sinn machen. Damit sind binäre Verschiebungen gemeint, die insofern unsinnig sind, dass damit eine kaum kontrollierbare Verschiebung von Nachkommateil in den Exponten-Anteil und umgekehrt erfolgen könnte.

Operation Name Bedeutung
=,+=, -=, *=, /= Zuweisung Einer Gleitzahlvariablen wird ein Wert zugewiesen, wobei bei einer arithmetischen Zuweisung die entsprechende arithmetische Operation vorher durch-geführt wird.
== Gleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden denselben Wert haben. Ansonsten wird false zurückgegeben.
!= Ungleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden unterschiedliche Werte haben. Ansonsten wird false zurückgegeben.
<, <=, >, >= Relational Weitere Operatoren zum Vergleich innerhalb einer Kontrollstruktur. Mit diesen Operatoren wird auf Ungleichheit im Sinn »kleiner«, »kleinergleich«, »größer«, »größergleich« der Werte geprüft. Auch hier wird, unabhängig vom Typ der Operanden, immer ein boolesches Ergebnis zurückgegeben.
+,- Vorzeichen Vorzeichenoperatoren bei einem Operanden.
+, -, *, / binäre Arithmetik Additions-, Subtraktions-, Multiplika-tions-, und Divisionsoperatoren.
+=, -=, *=, /= Zuweisung Additions-, Subtraktions-, Multiplika-tions-, und Divisionszuweisungen.
++, -- binäre Arithmetik Inkrement- und Dekrementoperatoren für den Wert der Variablen.

Tabelle 6.29:   Operationen mit den Typen float   und double

Wenn eine Operation mit zwei Operanden des Typs float durchgeführt wird, ist das Ergebnis auch immer eine Variable vom Datentyp float. Analog verhält es sich mit dem Datentyp double. Wenn eine binäre Operation mindestens einen Operanden des Typs double enthält, ist das Ergebnis auch vom Datentyp double. Wenn eine Operation zwischen einem Ganzzahl-Datentyp und einem Gleitzahl-Datentyp durchgeführt wird, bestimmt der Gleitzahl-Datentyp den Datentyp des Ergebnisses.

Gleitzahl-Datentypen können im Prinzip in jeden anderen Datentyp außer boolean konvertiert werden. Die Festlegung auf einen kleineren Typen kann wie immer zu Informationsverlust führen. Fließkommazahlen werden in Java nach dem IEEE-754-Rundungsmodus »Runden auf den nächsten Wert« gerundet. Wenn ein Gleitzahl-Datentyp in eine ganze Zahl konvertiert wird, werden Nachkommastellen abgeschnitten.

Java erzeugt keinerlei Ausnahmen bei Benutzung der Fließkomma-Arithmetik. Ein Overflow oder Überlauf (d.h. ein größeres Ergebnis durch eine Operation, als durch den Wertebereich des jeweiligen Typen ausgedrückt werden kann), oder das Teilen aller möglichen Zahlen außer Null durch Null und ähnliche Operationen, resultieren in der Ausgabe von positiven bzw. negativen unendlichen Werten. Dies ist durchaus ein sinnvoller Wert, denn man kann ihn mit Vergleichsoperatoren auswerten. Ein Unterlauf (d.h. ein kleineres Ergebnis - außer Null - durch eine Operation, als durch den Wertebereich des jeweiligen Typen ausgedrückt werden kann) gibt einen speziellen Wert aus, der positiv oder negativ Null genannt wird. Das Teilen von Null durch Null ergibt den Wert NaN (keine Zahl). Auch dies ist durchaus ein sinnvoller Wert, denn man kann ihn mit Vergleichsoperatoren auswerten. Er wird den Wert false bewirken.

Operationen mit ganzzahligen Variablen

Ganzzahlige Variablen werden in Java allesamt als Zweierkomplement-Zahlen mit Vorzeichen verwendet.

Operation Name Bedeutung
=,+=, -=, *=, /= Zuweisung Einer Ganzzahl-Variablen wird ein Wert zugewiesen.
== Gleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden denselben Wert haben. Ansonsten wird false zurückgegeben.
!= Ungleichheit Innerhalb einer Kontrollstruktur wird ein Vergleich durchgeführt. Es wird nur dann der boolesche Wert true zurückgegeben, wenn beide Operanden unterschiedliche Werte haben. Ansonsten wird false zurückgegeben.
<, <=, >, >= Relational Weitere Operatoren zum Vergleich innerhalb einer Kontrollstruktur. Mit diesen Operatoren wird auf Ungleichheit im Sinn »kleiner«, »kleinergleich«, »größer«, »größergleich« der Werte geprüft. Auch hier wird, unabhängig vom Typ der Operanden, immer ein boolesches Ergebnis zurückgegeben.
+,- Vorzeichen Vorzeichenoperatoren bei einem Operanden.
+, -, *, / binäre Arithmetik Additions-, Subtraktions-, Multiplikations- und Divisionsoperatoren.
+=, -=, *=, /= Zuweisung Additions-, Subtraktions-, Multiplikations- und Divisionszuweisungen.
++, -- binäre Arithmetik Inkrement- und Dekrementoperatoren für den Wert der Variablen.
<<, >>, >>> Verschiebung Bitweise Verschiebungsoperatoren. Operatoren für bitweise Verschiebung nach links, für bitweise Verschiebung nach rechts und für bitweise Verschiebung nach rechts mit Füllnullen.
<<=, >>=, >>>= Verschiebung und Zuweisung Bitweise Verschiebungs- und Zuweisungs-operatoren (nach links, nach rechts und nach rechts mit Füllnullen).
~ Bitweises NOT Bitweiser logischer Verneinungsoperator (Komplementieren). Eine einstellige bitweise Operation. Wenn ein Zeichen komplementiert wird, werden alle seine Bits invertiert.
& Bitweises AND Wenn Sie den AND-Operator (&) mit zwei Ganzzahlen benutzen und das Ergebnis in einer dritten Ganzzahlen ablegen, dann hat nur die resultierende Ganzzahl nur für die Bits den Eintrag 1, wenn alle beide Operanden an der gleichen Stelle Bits mit dem Wert 1 hatten.
| Bitweises OR Wenn Sie den OR-Operator (|) mit zwei Ganzzahlen benutzen und das Ergebnis in einer dritten Ganzzahlen ablegen, dann hat nur die resultierende Ganzzahlen nur für die Bits den Eintrag 1, wenn einer der Operanden ein Bit an dieser Position mit dem Wert 1 hatte.
^ Bitweises exklusives OR Wenn Sie den XOR-Operator (^) (exklusives OR) mit zwei Ganzzahlen benutzen und das Ergebnis in einer dritten Ganzzahl ablegen, dann hat nur die resultierende Ganzzahl nur für die Bits den Eintrag 1, wenn das dazugehörige Bit in genau einem der beiden Operanden-Bytes gesetzt wird. Es muss und darf genau einer der Operanden ein Bit an dieser Position auf 1 gesetzt haben.
&=, |=, ^= Bitweise Zuweisung Bitweise AND-, OR-, ausschließliche OR (XOR)- und Zuweisungsoperatoren.

Tabelle 6.30:   Operationen mit ganzzahligen Operanden

Wenn eine Operation mit zwei Ganzzahl-Operanden durchgeführt wird, ist das Ergebnis immer vom Datentyp int oder long. Das Ergebnis wird nur dann als long dargestellt, wenn einer der Operanden vom Datentyp long war oder das Ergebnis nicht ohne Überlauf in einer Variable vom Datentyp int darzustellen ist. Die Datentypen byte oder short können als Ergebnis nur vorkommen, wenn es explizit festgelegt wird. Wenn eine Operation zwischen einem Ganzzahl-Datentyp und einem Gleitzahl-Datentyp durchgeführt wird, bestimmt der Gleitzahl-Datentyp den Datentyp des Ergebnisses.

Die vier ganzzahligen Typen können im Prinzip in jeden anderen Datentyp außer boolean konvertiert werden. Die Festlegung auf einen kleineren Typen kann wie immer zu Informationsverlusten führen. Die Konvertierung in eine Fließkommazahl (float oder double) führt normalerweise zu Ungenauigkeiten, außer die ganze Zahl ist eine Zweierpotenz.

Wenn irgendeine Operation eine Zahl erzeugt, die den erlaubten Wertebereich eines Datentyps überschreitet, wird die Ausnahme oder der Überlauf nicht angezeigt. Als Ergebnis werden die unteren Bits, die noch hineinpassen, zurückgegeben. Nur wenn der rechte Operand beim Teilen einer ganzen Zahl oder bei einer Modulus-Operation Null ist, wird eine arithmetische Ausnahme erzeugt.

6.3 Datenfelder (Arrays)

Datenfelder (Arrays) gehören zwar nicht zu den primitiven Datentypen, sondern neben Klassen und Schnittstellen zu den Referenzvariablen. Da sie jedoch von der Logik gut zu den primitiven Datentypen passen, aus ihnen oft zusammengesetzt werden und vergleichsweise einfach sind, werden wie sie hier im Zusammenhang behandeln.

Arrays sind in Java gegenüber anderen Programmiersprachen wie C/C++ oder PASCAL anders konzipiert. Ein Datenfeld ist eine Ansammlung von Objekten eines bestimmten Typs (es sind keine verschiedenen Typen innerhalb eines Arrays erlaubt, allerdings kann ein Array selbst wieder Arrays enthalten und damit bewirkt das keinerlei Einschränkung), die über einen laufenden Index adressierbar sind. Arrays sind also auch selbst nichts anderes als (besondere) Objekte. Sie werden wie normale Objekte dynamisch angelegt und am Ende ihrer Verwendung vom Garbage Collector beseitigt. Weiterhin stehen in Arrays als Ableitung von Object alle Methoden dieser obersten Klasse zur Verfügung. Array-Bezeichner haben wie normale Objektvariablen einen Verweistyp. Es kann sich bei Arrays um Datenfelder bestehend aus sämtlichen primitiven Variablentypen (byte, char, short, int, long, float, double, boolean), aber auch anderen Datenfeldern oder Objekten handeln. Letzteres ist besonders deshalb wichtig, da Java keine multidimensionalen Arrays im herkömmlichen Sinn unterstützt, sondern für einen solchen Fall ein Array mit Arrays erwartet (und die können wiederum weitere Datenfelder enthalten - im Prinzip beliebig viele Ebenen). Verschachtelte Arrays wäre also eine korrektere Bezeichnung.

Gegenüber normalen Objekten haben Arrays zwei wesentliche Einschränkungen:

  • Arrays haben keine Konstruktoren. Statt dessen wird der new-Operator mit spezieller Syntax aufgerufen.
  • Es können keine Subklassen eines Arrays definiert werden.

Um ein Datenfeld in Java zu erstellen, muss man drei Schritte durchführen:

1. Deklarieren des Datenfelds
2. Zuweisen von Speicherplatz
3. Füllen des Datenfelds

Einige Anmerkungen zu Datenfeldern:

  • Es ist möglich, mehrere Schritte zur Erstellung eines Datenfeldes mit einer Anweisung zu erledigen.
  • Die Indizierung von Datenfeldern beginnt mit 0 (wie bei C und C++).
  • Sie können ein Datenfeld nur teilweise bei der Initialisierung füllen.
  • Datenfeldindizes müssen entweder vom Typ int (ganze 32-Bit-Zahl) sein, oder als int festgesetzt werden. Daher ist die größtmögliche Datenfeldgröße 2.147.483.647.

Datenfelder können in Java durch die Verwendung des new-Operators dynamisch erstellt werden. Ein Datenfeld ist in Java - wie schon angedeutet - eine Variable, kein Zeiger. Oder genauer, ein Array ist ein so genannter Referenztyp, ein besonderer Typ von Objekt. Die Besonderheit beruht darauf, dass Arrays »klassenlose« Objekte sind. Sie werden vom Compiler erzeugt, besitzen aber keine explizite Klassendefinition. Vom Java-Laufzeitsystem werden Arrays wie gewöhnliche Objekte behandelt. Das hat weitreichende (positive) Konsequenzen. So kann auf kein Datenfeldelement in Java zugegriffen werden, das noch nicht erstellt worden ist; dadurch wird das Programm vor dem Abstürzen und nicht initialisierten Zeigern (Pointer) bewahrt. Des Weiteren besitzen Arrays Methoden und Instanzvariablen.

6.3.1 Deklarieren von Datenfeldern

Im ersten Schritt beim Anlegen eines Datenfeldes muss immer eine Variable erstellt werden, in welcher das Datenfeld gespeichert werden soll.

Die Technik ist identisch mit anderen Variablen. Es müssen der Datentyp der Variable (byte, char, short, int, long, float, double, boolean oder ein Objekttyp) und der Name der Variablen festgelegt werden. Im Unterschied zu normalen Variablen muss jedoch mit eckigen Klammern die Variable als Datenfeld gekennzeichnet werden.

Es sind im Prinzip zwei Positionen denkbar, wo diese eckigen Klammern die Variable als Datenfeld kennzeichnen können - nach der Variablen oder nach dem Datentyp der Variable.

Beispiele:

int antwort[];
int[] antwort;

Frage: Welche Variante ist richtig? Antwort: Beide! Java unterstützt beide Syntaxtechniken und Sie können sich die Syntax auswählen, die Ihnen am besten zusagt.

Arrays mit anderen Arrays als Inhalt müssen natürlich auch deklariert werden können. Dazu wird pro Dimension einfach ein weiteres Paar an eckigen Klammern angefügt. Ein Datenfeld mit einem Datenfeld als Inhalt der Elemente wird z.B. wie folgt deklariert:

int antwort [] [];
int[] [] []  antwort;

6.3.2 Erstellen von Datenfeldobjekten

Wir haben die Erstellung von Objekten im Allgemeinen noch nicht durchgenommen, jedoch bereits in einigen Abhandlungen (Objektorientierung von Java usw.) dazu einige Bemerkungen gemacht. Deshalb wird es Sie wahrscheinlich nicht überraschen, wenn Sie die Erzeugung von Datenfeldobjekten mittels new-Operator sehen. Dies ist die direkte Erzeugung eines Datenfeldobjekts (und eines anderen Objekts). Daneben gibt es die Möglichkeit, durch direktes Initialisieren des Array-Inhalts ein Datenfeldobjekt zu erzeugen.

Die Erzeugung eines Datenfeldes mit dem new-Operator

Bei der direkten Erzeugung eines Datenfeldobjekts wird eine neue Instanz eines Arrays erstellt. Das erfolgt schematisch so:

new <Datentyp>[<Größe und Dimension>]

Beispiele:

new int[5]
new double[5] [7]

In der Regel wird das Array direkt einer Variablen zugewiesen. Das sieht dann schematisch so aus:

<Variable> = new <Datentyp>[<Größe und Dimension>]

Beispiele:

a = new int[5]
b = new double[5] [7]

Diese Variablen, welchen das Array zugewiesen werden soll, müssen vorher entsprechend (also mit der passenden Dimensionsangabe) deklariert worden sein. In der Regel fasst man diese Deklaration und die Zuweisung des Arrays in einem Schritt zusammen. Etwa wie im folgenden Beispiel:

int[] antwort = new int[5];

Es entsteht ein neues Array vom Datentyp int mit fünf Elementen. Die Anzahl der Elemente muss bei der direkten Erzeugung eines Datenfeldobjekts angegeben werden.

Beim Erzeugen eines Datenfeldobjekts mit new werden alle Elemente des Arrays automatisch initialisiert. Dabei gelten die Defaultwerte des jeweiligen Datentyps (false für boolesche, \0 für Zeichen-Arrays und 0 bzw. 0.0 für alle numerischen Datenfelder).

Erzeugung eines Datenfeldes mit direktem Initialisieren des Array-Inhalts

Für diese Technik der Erzeugung eines Datenfeldobjekts müssen Sie nach dem Gleichzeichen die Elemente des Arrays in geschweifte Klammern und mit Komma getrennt angegeben. Etwa wie in dem nachfolgenden Beispiel:

int[] antwort = {41,42,43,44};

Ein Array mit der Anzahl der angegebenen Elemente wird automatisch erzeugt.

Ein Datenfeld mit einem Datenfeld als Inhalt der Elemente wird z.B. wie folgt erzeugt (2x2-Array):

int[][] antwort = {{41,42},{43,44}};

Diese Technik der Erzeugung und Initialisierung von Arrays nennt man auch »Literale Initialisierung«.

6.3.3 Dynamische Arrays

Es ist in Java extrem leicht, dynamische Arrays zu erstellen. Genau genommen ist es quasi »natürlich«, denn Arrays werden unter Java grundsätzlich erst zur Laufzeit erzeugt. Arrays unter Java werden als semidynamisch bezeichnet. Das bedeutet, die Größe eines Arrays kann bei Bedarf erst zur Laufzeit festgelegt werden. Nach der Festlegung kann die Größe des Arrays jedoch nicht mehr geändert werden.

Dieses dynamische Verhalten der Java-Arrays beruht darauf, dass zum Zeitpunkt der Deklaration noch nicht festgelegt wird, wie viele Elemente das Array hat. Dies geschieht erst bei der Initialisierung. Nur wenn man Deklaration und Initialisierung in einem Schritt erledigt, hat man das Array quasi statisch erzeugt.

Ein wichtiger Fall von der dynamischen Festlegung der Array-Größe kennen Sie bereits. Die Übergabewerte an ein Java-Programm, die in der main()-Methode angegeben werden, sind ein Array (String[] args bzw. String args[]), dessen Größe erst zur Laufzeit dynamisch bestimmt wird (einfach auf Grund der Tatsache, wie viele Übergabewerte vom Aufrufer mitgegeben werden). Der Programmcode legt die Größe nicht fest.

6.3.4 Speichern und Zugreifen auf Datenfeldelemente

Einen Weg, um Elemente von Arrays mit Inhalt zu versehen, haben wir gerade gesehen: die literale Initialisierung. Dies ist aber nur für die anfängliche Bestückung zu verwenden und hilft im Fall der direkten Erzeugung eines Datenfeldobjekts nicht weiter.

Wir benötigen eine Technik, um auf die Werte von sämtliche Elementen eines Arrays zugreifen, sie zu testen und manipulieren zu können. Also Zugreifen auf Datenfeldelemente im allgemeinen Sinn.

Zugreifen auf Elemente von Datenfeldern mit primitiven Datentypen

Wenn Sie schon mit Arrays gearbeitet haben, wird Ihnen bei normalen Arrays mit primitiven Datentypen keine Überraschung bevorstehen - Sie geben einfach den Namen der Datenfeldvariable und in eckigen Klammern den Index an. Um auf das zweite Element des Datenfelds

int[] antwort = {41,42,43,44};

zugreifen zu können, werden wir einfach

antwort [1];

angeben. Der Index 1 für das zweite (!) Element unseres Datenfeldes ist kein Fehler. Der Index eines Arrays beginnt mit 0 (wie bei C/C++).

Wenn Sie bei einem Array versuchen, auf Elemente zuzugreifen, die sich außerhalb der Array-Grenzen befinden, wird entweder ein Kompilierungsfehler oder ein Laufzeitfehler (genauer - eine Ausnahme) erzeugt. Der erste Fehler tritt ein, wenn innerhalb des Quelltexts eine falsche Zuweisung vom Compiler bereits erkennbar ist, der zweite Fall, wenn das Array-Element erst zur Laufzeit berechnet wird. Eine solche Ausnahme ist dann von der Form ArrayIndexOutOfBoundsException. Auf jeden Fall wird der Fehler abgefangen und kann nicht wie bei C einen Pointer ins Nirwana schicken und damit übelste Systemabstürze verursachen.

Ändern von Elemente in Datenfeldern mit primitiven Datentypen

Die Änderung von Werten in Datenfeldern mit primitiven Datentypen ist identisch mit der Änderung von Werten normaler Variablen. Geben Sie einfach den Namen an und weisen Sie einen Wert zu.

Beispiel:

antwort [2] = 42;

Wir wollen Arrays in einem Beispiel testen:

class Array1 {
 public static void main (String args[]) {
  int i;
// Datenfeld mit 10 int-Werten 
// - nicht gefüllt
  int[] datenfelder = new int[10]; 
// fülle Datenfelder
  for (i=0;i < 10; i++) datenfelder[i] = i;
// Ausgabe
  for (i=9;i > -1; i--)
   System.out.print(datenfelder[i] + " ");
   System.out.println("\n____________________");
// andere Variante für ein Datenfeld mit 
// 10 char-Werten - nicht gefüllt
  char datenfelder2[] = new char[10]; 
// fülle Datenfelder
  for (i=0;i < 10; i++)
   datenfelder2[i] = (char)(i*5);
// Ausgabe
  for (i=9;i > -1; i--)
   System.out.print(datenfelder2[i] + " ");
   System.out.println("\n____________________");
// Datenfeld mit 10 long-Werten und gleich 
// bei der Deklaration Speicher zugewiesen 
// und gefüllt 
  long[] fi = {1,2,3,5,7,11,13,17,23,29};
  for (i=9;i > -1; i--)
   System.out.print(fi[i] + " ");
   System.out.println("\n____________________");
// andere Variante für ein Datenfeld mit 
// int-Werten und gleich bei der Deklaration 
// Speicher zugewiesen und gefüllt 
  int NochEinFeld[] = {1,2,3};
  for (i=0;i < 3; i++)
   System.out.print(NochEinFeld[i] + " ");
 }
}

Dieses Programmbeispiel enthält neben Datenfeldern einige interessante Java-Techniken, die wir an dieser Stelle diskutieren wollen. Die Kommentare innerhalb des Source erklären bereits einige Schritte, aber es gibt noch einige Dinge, auf die gesondert eingegangen werden sollte.

Die Zuweisung von Inhalt für die Elemente der Arrays erfolgt in den oben beschriebenen Techniken jeweils über for-Schleifen oder bereits durch direktes Initialisieren des Array-Inhalts. Dabei werden Arrays mit verschiedenen Datentypen verwendet. Zum Teil wird Casting verwendet. Beachten Sie die Trennlinie, die mit einem einleitenden \n beginnt. Damit wird ein Zeilenvorschub innerhalb eines Strings ausgelöst.

Mittels der for-Schleife for (i=9;i > -1; i--) erfolgt eine Ausgabe, indem der Index der Datenfelder von hinten nach vorne durchgezählt wird. Dies ist genauso zulässig. Dabei wird der Dekrementoperator (--) verwendet, um innerhalb der for-Schleife in den angegebenen Grenzen herabzuzählen.

Abbildung 6.10:  Eindimensionale Arrays

Multidimensionale Datenfelder

Wir hatten bereits festgehalten, dass Java keine multidimensionalen Arrays im herkömmlichen Sinn unterstützt, sondern für diesen Fall ein Array mit Arrays erwartet (und die können wiederum weitere Datenfelder enthalten - im Prinzip beliebig viele Ebenen). Wir wollen der Einfachheit halber dennoch von multidimensionalen Arrays reden (obwohl verschachtelte Arrays eigentlich zutreffender wäre).

Die Deklaration und Erzeugung haben wir schon gesehen, aber wie erfolgt der Zugriff? Hier ist tatsächlich eine Umstellung gegenüber Arrays in anderen Programmiersprachen notwendig. Ein Zugriff auf das zweite Element in der zweiten Spalte erfolgt beispielsweise über

antwort [2][2];

Eine Zuweisung erfolgt analog über

antwort [2][2] = 42;.

Der Unterschied gegenüber Arrays in anderen Programmiersprachen ist, dass die eckigen Klammern in der beschriebenen Weise angegeben werden müssen. Ein Zugriff in der Form antwort[2, 2] oder ähnlich wird einen Fehler erzeugen.

Das nachfolgende kleine Beispiel arbeitet mit multidimensionalen Arrays.

public class Array2 {
  public static void main(String args[]) {
  int a[][] = new int[2][3];
  a[0][0] = 1;
  a[0][1] = 2;
  a[0][2] = 3;
  a[1][0] = 4;
  a[1][1] = 5;
  a[1][2] = 6;
  System.out.println("" + a[0][0] + a[0][1] + a[0][2]);
  System.out.println("" + a[1][0] + a[1][1] + a[1][2]);
  System.out.println(a[0][0] + a[0][1] + a[0][2]);
  System.out.println(a[1][0] + a[1][1] + a[1][2]);
  }
}

Der den ersten beiden Ausgaben vorangestellte Leerstring verhindert, dass die Array-Elemente als numerische Werte zusammengezählt werden. Die Ausgaben drei und vier hingegen lassen es zu.

Abbildung 6.11:  Multidimensionales Array

Arrays mit allgemeinen Objekten als Inhalt der Elemente

Multidimensionale Arrays sind ein spezieller Fall von Arrays mit Objekten als Inhalt der Elemente. Darin kann sich allerdings ebenso jegliche andere Form von Objekten befinden.

Ein Array mit Objekten als Inhalt der Elemente enthält Referenzen auf diese Objekte. Wenn einem Datenfeldelement ein Wert zugewiesen wird, erstellen Sie eine Referenz auf das betreffende Objekt. Verschieben Sie die Werte in den Arrays, weisen Sie die Referenz neu zu. Es wird also nicht der Wert von einem Element in ein anderes Element kopiert, wie es bei einem Array mit primitiven Datentypen der Fall wäre.

Arrays mit Objekten als Inhalt sind eine einfache Möglichkeit, innerhalb eines Arrays verschiedene Arten von Informationen unterzubringen. Es gibt zwar in Java keine direkte Möglichkeit, ein Array mit verschiedenen Datentypen zu deklarieren, aber wenn man einfach Objekte mit unterschiedlichen Datentypen als Inhalt verwendet, ist das kein Problem (obgleich etwas Hintergrundwissen zu Java notwendig ist). Das nachfolgende kleine Beispiel arbeitet mit zwei so genannten Wrapperklassen, um dort Informationen eines bestimmten Datentyps (im Beispiel int und float) unterzubringen. Die damit erzeugten Objekte werden einem Array zugewiesen, das vom Typ eine Instanz der Klasse Number ist (der abstrakten Superklasse der beiden Wrapperklassen). Damit befinden sich in dem Array verschiedenen primitive Datentypen. Zwar in Objekte »eingepackt«, aber wie das Beispiel weiter zeigt, stehen Methoden bereit, die primitiven Werte wieder zu extrahieren.

public class Array3 {
  public static void main(String args[]) {
  Integer b = new Integer(5);
  Float c = new Float("4.4f");
  Number a[] = {b,c};
  System.out.println(a[0].intValue());
  System.out.println(a[1].floatValue());
  }
}

Die Ausgabe wird zuerst der Ganzzahlwert 5 und dann der Gleitzahlwert 4.4 sein.

Es ist sogar in Java mit dieser Technik möglich, nicht-rechteckige Arrays zu erzeugen. Damit werden Verfahren wie Records oder Typedef-Konstrukte, wie sie in anderen Programmiersprachen vorkommen, überflüssig. Das nachfolgende Beispiel demonstriert eine solche Technik.

public class Array4 {
  public static void main(String args[]) {
  int a[][] = {   {8},
      {7, 7},
      {6, 6, 6},
      {5, 5, 5, 5},
      {4, 4, 4, 4, 4},
      {1, 2, 3, 4, 5, 6}  };
  for (int i=0; i < a.length; i++) {
  for (int j=0;j<a[i].length;++j) {
     System.out.print(a[i][j]);
      }
     System.out.println("");
}  }  }

Beachten Sie in dem Beispiel, dass wir dort viele interessante Details ausnutzen:

  • Zwei Zählvariablen, die beide erst innerhalb der for-Schleife deklariert werden (das nennt man schleifenlokal).
  • Der Inkrementoperator sowohl voran- als auch nachgestellt.
  • Zwei ineinander verschachtelte for-Schleifen.
  • Das Charakteristikum von Arrays, Objekte zu sein, lässt uns Methoden und Eigenschaften verwenden, die auf jedem Objekt bereitstehen. Etwa die Eigenschaft length, mit der die Größe jeder Dimension bestimmt werden kann. Das macht das Beispiel unempfindlich gegenüber Änderungen der Dimensionen und vor allem kann man für alle Größen die gleiche for-Schleife verwenden (das Abbruchkriterium wird anhand des Arrays dynamisch bestimmt).

Abbildung 6.12:  Ein nicht-rechteckiges Array

6.3.5 Collections

Ein Spezialfall von Arrays sind so genannte Collections. Auch diese dienen dazu, Mengen von Daten aufzunehmen. Die Daten werden aber gekapselt abgelegt und es ist nur mithilfe vorgegebener Methoden möglich, darauf zuzugreifen. Ein wichtiger Spezialfall einer Collection ist eine Hashtabelle, die die Klasse Hashtable repräsentiert. Dies ist eine Collection, in der die Daten paarweise mit einer Referenzbeziehung gespeichert sind. Collections sind im Wesentlichen im Paket java.util untergebracht. Dort gibt es etwa die Klasse Vector als Java-Repräsentation einer linearen Liste oder das Interface Enumeration, was den sequenziellen Zugriff auf die Elemente eines Vektors mittels eines Iterators erlaubt.

6.4 Ausdrücke, Operatoren und Casting

In diesem Abschnitt sollen die Details zu Operatoren und Ausdrücken ergänzt und besprochen werden, die bisher entweder noch nicht behandelt wurden oder scheinbar »vom Himmel gefallen« sind. Dies betrifft einige besondere Operatoren und auch den Vorgang des Castings.

6.4.1 Ausdrücke

Die Ausdrücke in Java und Ausdrücke in C/C++ sind sehr ähnlich. Sie drücken einen Wert entweder direkt oder durch Berechnung aus. Es kann sich gleichfalls um Kontrollfluss-Ausdrücke handeln, die den Ablauf der Programmausführungen festlegen. Diese Ausdrücke können Konstanten, Variablen, Schlüsselworte, Operatoren und andere Ausdrücke beinhalten. Wir haben in unseren bisherigen Beispielen natürlich schon diverse Java-Ausdrücke verwendet.

Ausdruck Beschreibung
42 arithmetische Konstante
3.141592654 arithmetische Konstante
3*4 multiplikativer Ausdruck
1 + 3 / 4 additiver Ausdruck mit Division
2^32 Exponential-Ausdruck
x=42 Zuweisungs-Ausdruck
ausgabe[42] Datenfeldindizierung

Tabelle 6.31:   Beispiele für gültige Java-Ausdrücke

Java unterstützt, außer in Initialisierungs- und Weiterführungsklauseln für Schleifen- Anweisungen - z.B. for (k=1, j=1; j+k < 65; j++, k++) - keine mit Kommata zusammengesetzten Anweisungen.

Man kann Ausdrücke am einfachsten folgendermaßen definieren:

Ausdrücke sind das Ergebnis der Verbindung von Operanden und Operatoren über die syntaktischen Regeln der Sprache.

Ausdrücke werden also für die Durchführung von Operationen (Manipulationen) an Variablen oder Werten verwendet. Dabei sind Spezialfälle wie arithmetische Konstanten kein Widerspruch, sondern nur die leere Operation.

Bewertung von Ausdrücken

Ausdrücke kommen selbstverständlich auch bei komplizierten Kombinationen von Operatoren und Operanden vor. Deshalb muss Java diese Kombinationen bewerten, also eine Reihenfolge festlegen, wie diese komplexeren Ausdrücke auszuwerten sind. Das ist in der menschlichen Logik nicht anders. Sie kennen sicher die Punkt-vor-Strichrechnung in der Mathematik. Überhaupt ist die Bewertung von Ausdrücken in Java meistens durch die Bewertung von Ausdrücken in der Mathematik intuitiv herleitbar.

Wir werden uns nun mit drei Begriffen auseinander setzen müssen:

1. Operatorassoziativität
2. Operatorvorrang
3. Bewertungsreihenfolge

Operatorassoziativität klingt zwar am kompliziertesten, der Vorgang ist aber die einfachste der Bewertungsregeln. Vielleicht kennen Sie die Assoziativitätsregel noch aus der Schulmathematik. Alle arithmetischen Operatoren bewerten (assoziieren) Ausdrücke defaultmäßig von links nach rechts. Das heißt, wenn derselbe Operator in einem Ausdruck mehr als einmal auftaucht - wie beispielsweise der +-Operator bei dem Ausdruck 1 + 2 + 3 - dann wird der am weitesten links erscheinende zuerst bewertet, gefolgt von dem rechts daneben usw. Unterziehen wir folgende arithmetische Zuweisung einer näheren Betrachtung:

x=1 + 2 + 3;

In diesem Beispiel wird der Wert des Ausdrucks auf der rechten Seite von dem Gleichheitszeichen zusammengerechnet und der Variablen x auf der linken Seite zugeordnet. Für das Zusammenrechnen des Werts auf der rechten Seite bedeutet die Tatsache, dass der Operator + von links nach rechts assoziiert, dass der Wert von 1 + 2 zuerst berechnet wird. Erst im nächsten Schritt wird zu diesem Ergebnis dann der Wert 3 addiert. Anschließend wird das Resultat dann der Variablen x zugewiesen. Immer, wenn derselbe Operator mehrfach benutzt wird, können Sie die Assoziativitätsregel anwenden.

Diese Regel ist nicht ganz so trivial, wie sie im ersten Moment erscheint. Darüber lässt sich bewusst das Prinzip des Castings beinflussen. Beachten Sie das nachfolgende kleine Beispiel.

class OperatorAsso {
 public static void main (String args[])  {
    System.out.println("" + 1 + 2 + 3);
    System.out.println(1 + 2 + 3 + "");
 }  }

Die erste Ausgabe wird 123 sein, die zweite jedoch 6. Warum?

  • In der ersten Ausgabe wird zuerst "" + 1 bewertet. Das erzwingt ein Resultat vom Typ String. Danach wird "1" + 2 bewertet ("1" ist ein String!) und es ergibt den String "12". Dann folgt das Spiel erneut.
  • In der zweiten Version wird zuerst 1 + 2 bewertet und es entsteht die Zahl 3. Erst im letzten Schritt wird mit 6 + "" eine Konvertierung in einen String erzwungen.

Operatorvorrang bedeutet die Beachtung der Priorität von Java-Operatoren. Wenn Sie einen Ausdruck mit unterschiedlichen Operatoren haben, muss Java entscheiden, wie Ausdrücke bewertet werden. Hier ist das Beispiel der Punkt-vor-Strich-Rechnung aus der Mathematik wieder sinnvoll. Java hält sich strikt an Regeln der Operatorvorrangigkeit. Je höher ein Operator priorisiert ist, desto eher wird er bewertet. Die multiplikativen Operatoren (*, / und %) haben Vorrang vor den additiven Operatoren (+ und -). In anderen Worten: In einem zusammengesetzten Ausdruck, der sowohl multiplikative als auch additive Operatoren enthält, werden die multiplikativen Operatoren zuerst bewertet. Dies ist übrigens einfach die Punkt-vor-Strich-Rechnung.

Beispiel:

a = b-c*d/e;

Weil die Operatoren * und / Vorrang haben, wird der Unterausdruck zuerst berechnet. Erst danach wird die Subtraktion ausgeführt. Immer wenn Sie die Bewertungsreihenfolge von Operatoren in einem Ausdruck ändern wollen, müssen Sie Klammern benutzen. Klammern haben eine höhere Priorität als arithmetische Operatoren. Jeder Ausdruck in Klammern wird zuerst bewertet.

Beispiel:

a = (b-c)*d/e;

Hier würde zuerst die Subtraktion von b und c durchgeführt.

Auch einstellige Operatoren haben eine sehr hohe Priorität.

Beispiel:

a=-b*c;

Der einstellige arithmetische Minusoperator bewirkt, dass -b mit c multipliziert wird.

Mehr zu der Operatoren-Priorität finden Sie auf Seite 200

Das letzte zu beachtende Detail bei der Bewertung ist die Bewertungsreihenfolge. Der Unterschied zwischen Bewertungsreihenfolge und Operatorvorrang ist der, dass bei der Bewertungsreihenfolge die Operanden bewertet werden.

Die Bewertungsreihenfolge legt fest, welche Operatoren in einem Ausdruck zuerst benutzt werden und welche Operanden zu welchen Operatoren gehören. Außerdem dienen die Regeln für die Bewertungsreihenfolge dazu festzulegen, wann Operanden bewertet werden.

Es gibt drei plattformunabhängige Grundregeln in Java, wie ein Ausdruck bewertet wird:

1. Bei allen binären Operatoren wird der linke Operand vor dem rechten bewertet.
2. Zuerst werden immer die Operanden, danach erst die Operatoren bewertet.
3. Wenn mehrere Parameter, die durch Kommata voneinander getrennt sind, durch einen Methodenaufruf zur Verfügung gestellt werden, werden diese Parameter immer von links nach rechts bewertet.

6.4.2 Operatoren

Wir werden uns nun der Operatoren noch einmal unter einem anderen Gesichtspunkt annehmen. Operatoren sind in diesem Zusammenhang spezielle Symbole (eine der Schlüsselkategorien von Token in Java), die verwendet werden, um die Durchführung von Operationen (Manipulationen) an Variablen oder Werten auszuführen. Wir haben Operatoren ja schon behandelt und ein Teil der hier diskutierten Ergebnisse wird Ihnen mittlerweile bekannt sein. Allerdings ist es dennoch sogar bei Überschneidungen keine reine Wiederholung, denn wir werden für die Operatoren aus der veränderten Betrachtungsweise neue Erkenntnisse herausziehen und die noch offenen Operatoren behandeln.

Die Operandenanzahl

Java-Operatoren können einen, zwei oder drei Operanden haben. Operatoren, die einen Operanden haben, werden als einstellige oder monadische Operatoren bezeichnet. Einige einstellige Operatoren stehen vor dem Operanden (so genannte Präfixoperatoren), andere stehen hinter dem Operanden (sie werden Postfixoperatoren genannt). Der Dekrementoperator (--) und der Inkrementoperator (++) sind Beispiele für einstellige Operatoren. Operatoren mit zwei Operanden werden als binäre oder dyadische Operatoren bezeichnet. Die arithmetischen Operatoren sind Beispiele dafür. Es gibt in Java ebenfalls einen Operator mit drei Operanden - den Bedingungsoperator zur Abkürzung der if-Struktur. Er wird als tenärer oder triadischer Operator bezeichnet.

Der triadische Operator

Dieser Operator ist ein unverändert gebliebenes Überbleibsel der C-Sprache. Es handelt sich dabei um eine Abkürzung für die gewöhnlichen if-else-Ausdrücke. Er besteht aus folgendem Konstrukt:

[ergebnis] = [bed1][Vergloperator][bed2] ? [erg1] : [erg2]

Zur Erläuterung:

Der Wert [ergebnis] bekommt den Wert [erg1] zugeordnet, wenn der Vergleich (der boolesche Ausdruck vor dem Fragezeichen) zwischen [bed1] und [bed2] das Ergebnis true gebracht hat, ansonsten bekommt [ergebnis] den Wert [erg2] zugeordnet. Ziehen wir ein praktisches Beispiel zur Verdeutlichung her.

class If_oder_Nicht {
 public static void main (String args[])  {
  String ergebnis;
  int a = 42;
  int b = 21;
  ergebnis = a > b? "a ist groesser" : 
  "a ist kleiner";
  System.out.println(ergebnis);
 }  }

Das Programmierbeispiel wird den Text »a ist groesser« (der Wert von a) auf dem Bildschirm ausgeben, denn der boolesche Wert vor dem Fragezeichen hat den Wert true.

Man nennt diesen Vorgang bedingte Bewertung. Trotz der weiten Verbreitung in C-Welten, sollte dieses Konstrukt um der Lesbarkeit des Quelltextes willen gar nicht oder möglichst nur sehr sparsam verwendet werden. Eine saubere if-else-Struktur ist fast genauso schnell getippt und bedeutend besser zu lesen.

6.4.3 Typkonvertierungen und der Casting-Operator

Unter Casting versteht man die Umwandlung von einem Datentypen in einen anderen Datentypen. Java ist eine streng typisierte Sprache, weil sehr intensive Typüberprüfungen stattfinden. Außerdem gelten strikte Beschränkungen für die Umwandlung (Konvertierung) von Werten eines Datentyps zu einem anderen.

Java unterstützt zwei unterschiedliche Arten von Konvertierungen:

  • Explizite Konvertierungen, um absichtlich den Datentyp eines Wertes verändern.
  • Ad hoc-Konvertierungen ohne Zutun des Programmierers.

Ad-hoc-Typkonvertierungen

Java führt bei der Bewertung von Ausdrücken einige Typkonvertierungen ad hoc durch, ja sogar ohne dass Sie es unter Umständen überhaupt wissen. Dies erfolgt dann, wenn die Situation es erfordert. Einen solche Situation tritt ein, wenn

  • bei einer Zuweisung der Typ der Variablen und der Typ des zugewiesenen Ausdruck nicht identisch sind,
  • der Wertebereich der Zuweisung eines Ausdrucks nicht ausreicht,
  • verschiedene Datentypen in einem Ausdruck verknüpft werden oder
  • die an einem Methodenaufruf übergebenen Parameter vom Datentyp nicht mit den geforderten Datentypen übereinstimmen.

Die Regeln für eine dann durchgeführte automatische Konvertierung sind für numerische Datentypen untereinander allerdings sehr einfach.

  • Wenn nur Ganzzahltypen miteinander kombiniert werden, legt der größte Datentyp den Ergebnisdatentyp fest. Wenn also einer der beiden Operanden den Datentyp long hat, wird der andere gleichfalls zu long konvertiert und das Ergebnis vom Typ long sein. Aus der Kombination short und int wird int, byte und short wird zu short und byte und int wird zu int. Wenn das Ergebnis einer Verknüpfung vom Wert so groß ist, dass es im so vorgesehenen Wertebereich nicht mehr dargestellt werden kann, wird der nächstgrößere Datentyp genommen. So kann es vorkommen, dass aus der Verknüpfung von zwei int -Werten der Typ long entsteht.
  • Bei Operationen mit Fließkommazahlen gelten weitgehend die analogen Regeln. Wenn wenigstens einer der Operanden den Datentyp double hat, wird der andere ebenso zu double konvertiert, und das Ergebnis ist dann ebenfalls vom Typ double. Aus zwei float-Datentypen wird allerdings nicht (!) der Typ double, wenn der Wertebereich nicht ausreicht (also keine ad-hoc-Konvertierung). Statt dessen wird der wohldefinierte Wert Infinity zurückgegeben.

Es ist also offensichtlich so, dass, wenn der entstehende Datentyp größer als der zu konvertierende Datentyp ist, die Konvertierung ohne Probleme ad hoc funktioniert, da keine Informationen verloren gehen können.

class Casting1 {
 public static void main (String args[])  {
   byte a = 125;
   short b = 32000;
   float c = 45.9f;
   float d = 4E37f;
   double e = 4E37;
   System.out.println(a);
   System.out.println(a + 3);
   System.out.println((byte)(a + 3));
   System.out.println(b);
   System.out.println(a * b);
   System.out.println((short)(a * b));
   System.out.println(c);
   System.out.println(d);
   System.out.println(c * d);
   System.out.println(c * e);
   System.out.println((float)(c * d));
   System.out.println((double)c * d);
 }
}

Das Beispiel verknüpft Variablen verschiedenen Datentyps, und zwar so, dass dabei das Ergebnis jeweils den Wertebereich des ursprünglichen Datentyps sprengt. Es wird automatisch gecastet, wie die jeweils nachfolgende explizite Rückkonvertierung auf den ursprünglichen Datentyp zeigt (dazu gleich mehr).

Abbildung 6.13:  Beispiele für ad-hoc-Konvertierungen

Wenn in Ausdrücken Verbindungen zwischen verschiedenen Familien von Datentypen (etwa char mit int oder byte mit double) durchgeführt werden, gilt Folgendes:

  • Der Datentyp char wird auf int gecastet.
  • Ganzzahlen werden bei Verbindung mit Fließkommazahlen auf float oder double konvertiert (je nach Größe des beteiligten Fließkomma-Operanden).
class Casting2 {
 public static void main (String args[])  {
   byte a = 125;
   float b = 45.9f;
   float c = 4E37f;
   double d = 4E37;
   char e = 'c';
   System.out.println(a + b);
   System.out.println(a * c);
   System.out.println(a * d);
   System.out.println(e);
   System.out.println(e+1);
 }
}

Die zweite und die dritte Ausgabe zeigen, dass es einen Unterschied ausmacht, ob ein Ganzzahlwert mit einem float- oder einem double-Datentyp verknüpft wird. Ausgabe fünf zeigt die ad-hoc-Konvertierung von einem char-Datentyp in einen int-Datentyp.

Abbildung 6.14:  Beispiele für ad-hoc-Konvertierungen zwischen verschiedenen Familien von Datentypen

Grundsätzlich kann man mit dem +-Operator primitive Werte mit einem String verbinden. In diesem Fall wird immer ein String erzeugt.

Bei booleschen Werten ist die Situation etwas komplizierter. Diese lassen sich in Java grundsätzlich nicht in andere primitive Datentypen konvertieren (weder ad hoc, noch explizit). Man muss bei Bedarf eine Hilfskonstruktion (etwa mit einer if-Struktur) verwenden.

Explizite Konvertierungen

Eine explizite Konvertierung ist immer dann notwendig, wenn Sie eine Umwandlung in einen anderen Datentyp wünschen und diese nicht ad hoc auf Grund der oben beschriebenen Situationen eintritt.

Um beispielsweise einen in einem großen Datentyp gespeicherten Wert in eine kleineren Datentyp umzuwandeln, müssen Sie explizites Casting anwenden. Dazu müssen Sie in der Regel den Casting-Operator (manchmal Festlegungsoperator genannt) verwenden.

Es gibt neben der Verwendung des Casting-Operators noch die Fälle, dass Methoden Typkonvertierungen durchführen.

Der Casting-Operator besteht bei Casting auf einen primitven Datentyp nur aus einem Datentypnamen in runden Klammern. Er ist ein einstelliger Operator mit hoher Priorität und steht vor seinem Operanden. Er ist also immer von der folgenden Form:

(<Datentyp>) <Wert>

Der Festlegungsoperator ergibt den Datentyp, wie er in den Klammern bezeichneten Typ festgelegt wird.

Es gibt die folgenden Casting-Operatoren:

Operator Beispiel Erläuterung
(byte) (byte) (x/y) Wandelt das Ergebnis der Division x geteilt durch y in einen Wert vom Datentyp byte um.
(short) (short) x Wandelt den Datentyp x in einen Wert vom Datentyp short um.
(int) (int) (x/y) Wandelt das Ergebnis der Division x geteilt durch y in einen Wert vom Datentyp int um.
(long) (long) x Wandelt den Datentyp x in einen Wert vom Datentyp long um.
(float) (float) x Wandelt den Datentyp x in einen Wert vom Datentyp float um.
(double) (double) x Wandelt den Datentyp x in einen Wert vom Datentyp double um.
(char) (char) x Wandelt den Datentyp x in einen Wert vom Datentyp char um.

Tabelle 6.32:   Casting-Operatoren von primitiven Datentypen

Casting hat eine höhere Priorität als Arithmetik. Deshalb müssen arithmetische Operationen in Verbindung mit Casting in Klammern gesetzt werden.

Nicht alle Konvertierungen sind möglich. Variablen eines arithmetischen Typs können auf jeden anderen arithmetischen Typen festgelegt werden. Boolesche Werte können nicht auf irgendeinen anderen Wert festgelegt werden.

Konvertieren von Objekten

Mit Einschränkungen lassen sich sogar Klasseninstanzen in Instanzen anderer Klassen konvertieren (sowohl ad hoc als auch explizit). Die wesentliche Einschränkung ist, dass die Klassen durch Vererbung miteinander verbunden sein müssen. Allgemein gilt, dass ein Objekt einer Klasse auf seine Superklasse festgelegt werden kann. So kann beispielsweise ein Graphics2D-Objekt auf ein Graphics-Objekt konvertiert werden (Graphics ist die Superklasse von Graphics2D). Beispiel:

Graphics a = (Graphics) g2d;

Bei der Subklasse (die ja in der Regel mehr Informationen enthält) gibt es im Allgemeinen Probleme. Weil eine Festlegung immer eine unbedingte Typenkonvertierung beinhaltet (sofern überhaupt eine möglich ist), ist sie als Typenzwang bekannt. Dennoch gibt es diverse Situationen, wo eine Konvertierung eines Objekts auf die Subklasse sinnvoll ist. Etwa, wenn man im Rahmen der paint()-Standardmethode Java 2D verwenden will. In diesem Fall castet man das Graphics-Objekt auf ein Graphics2D-Objekt, dessen fehlende Informationen dann default belegt werden. Etwa so:

public void paint(Graphics g)  {
Graphics2D g2d = (Graphics2D) g;
...// tue etwas sinnvolles
}

Beim Konvertieren von einer Instanz auf eine Instanz seiner Superklasse gehen die spezifischen Informationen, welchen nur in der zu konvertierenden Subklasse vorhanden sind, natürlich verloren.

Die Technik zum expliziten Konvertieren mit dem Casting-Operator ist analog dem Fall des Castings bei primitiven Datentypen. Er ist also immer von der folgenden Form:

(<Klassenname>) <Objekt>

<Klassenname> ist der Name der Klasse, in die das Objekt konvertiert werden soll. <Objekt> ist eine Referenz auf das konvertierte Objekt. Casting erstellt eine neue Instanz der neuen Klasse. Das alte Objekt existiert unverändert weiter.

Konvertieren von Objekten in Schnittstellen

Obwohl Schnittstellen bisher noch nicht behandelt wurden, gehört eine Diskussion der Konvertierung von Objekten in Schnittstellen der Vollständigkeit halber dazu. Mit Einschränkungen lassen sich Klasseninstanzen in Schnittstellen konvertieren. Dabei ist allerdings zwingend, dass die Klasse selbst oder eine Superklasse des Objekts die Schnittstelle implementiert. Durch Casting eines Objekts in eine Schnittstelle kann dann eine Methode dieser Schnittstelle verwendet werden, obwohl die Klasse des Objekts diese Schnittstelle unter Umständen nicht direkt implementiert hat.

Konvertierung von primitiven Datentypen in Objekte und umgekehrt

Wenn Sie jetzt nach der Technik fragen, wie primitive Datentypen in Objekte und umgekehrt konvertiert werden können, ist die Antwort ganz einfach: überhaupt nicht! Weder ad hoc noch durch explizites Casting. Das hat sehr weitreichende Konsequenzen, denn ein String ist ja beispielsweise in Java kein primitiver Datentyp. Soll das etwa bedeuten, dass aus einem String, der nur eine Zahl enthält, diese nicht extrahiert werden kann? Nein, natürlich nicht. Nur halt nicht per Casting.

Im Java-Packet java.lang gibt es als Ersatz dafür Sonderklassen, die primitiven Datentypen entsprechen. Man nennt sie Wrapper-Klassen oder kurz Wrapper. Mit den in den Klassen definierten Klassenmethoden können Sie mithilfe des new-Operators jeweils ein Objekt-Gegenstück zu jedem primitiven Datentypen erstellen.

Beispiel:

Integer Objekt_vomTyp_int = new Integer(42);

Das Objekt Objekt_vomTyp_int ist eine Instanz der Klasse Integer und bekommt direkt den Wert 42 übergeben. Aber auch die Klasse String, auf der sämtliche Stringrepräsentationen von Java basieren, ist hier zu erwähnen. Die Konstruktoren der Wrapper-Klassen erlauben es auch, einen String als Übergabewert anzugeben. Über diesen Weg führt dann am sinnvollsten die Extrahierung einer Zahl aus einem String.

Neben der Klasse Integer gibt es für jeden primitiven Datentyp ein Wrapper-Äquivalent.

Wrapper Beschreibung
Boolean Wrappt einen primitiven boolean-Wert in ein Objekt.
Byte Wrappt einen primitiven byte-Wert in ein Objekt.
Character Wrappt einen primitiven char-Wert in ein Objekt.
Double Wrappt einen primitiven double-Wert in ein Objekt.
Float Wrappt einen primitiven float-Wert in ein Objekt.
Integer Wrappt einen primitiven int-Wert in ein Objekt.
Long Wrappt einen primitiven long-Wert in ein Objekt.
Short Wrappt einen primitiven short-Wert in ein Objekt.

Tabelle 6.33:   Wrapper für primitive Datentypen

In den gleichen Zusammenhang sind die Klassen Number (die abstrakte Superklasse von Byte, Double, Float, Integer, Long und Short) und Void (eine nicht-instanzierbare Platzhalterklasse zum Bereitstellen einer Referenz auf das Klassenobjekt, das den primitive Java-Typ void repräsentiert) zu setzten.

Die Extrahierung aus einem per Wrapper erzeugten Objekt funktioniert mit den über die Klasse bereitgestellten Methoden. So stellt die Klasse Integer beispielsweise folgende Methoden bereit (Auswahl):

Methode Beschreibung
byte byteValue() Rückgabe des Werts von einem -Integer als byte
int compareTo(Integer anotherInteger) Numerischer Vergleich zweier Integer
int compareTo(Object o) Vergleich von Integer mit einem anderen Objekt
static Integer decode(String nm) Dekodierung eines Strings in ein -Integer
double doubleValue() Rückgabe des Werts von einem -Integer als double
boolean equals(Object obj) Objektvergleich
float floatValue() Rückgabe des Wert von einem Integer als float
static Integer getInteger(String nm) static Integer getInteger(String nm, int val) static Integer getInteger(String nm, Integer val) Rückgabe eines Integer-Objekts auf Grund verschiedener Übergabewerte.
int hashCode() Rückgabe eines Hashcodes für diesen Integer
int intValue() Rückgabe des Werts von einem -Integer als int
long longValue() Rückgabe des Werts von einem -Integer als long
static int parseInt(String s) Parsed das Stringargument und gibt den gefundenen Integerwert dezimal zurück.
short shortValue() Rückgabe des Werts von einem -Integer als short

Tabelle 6.34:   Auswahl von Methoden der Interger-Klasse

Die anderen Wrapper-Klassen stellen die gleichen oder verwandte Methoden bereit.

Das nachfolgende Beispiel extrahiert den Wert des Objekts als primitiven Datentyp int aus dem Integer-Objekt. Die verwendete Methode ist intValue().

class Multi {
  public static void main(String args[])  {
    Integer a = new Integer(args[0]);
    Integer b = new Integer(args[1]);
    System.out.println(a.intValue()*b.intValue());
 }  }

Wenn Sie das Programm mit zwei Integerwerten als Übergabewerte aufrufen (also z.B. java Multi 3 4), werden diese miteinander multipliziert und ausgegeben. Da in Java Übergabewerte an ein Programm immer als Strings übergeben werden (ein dynamisches String-Array), muss man eventuell dort zu übergebende primitive Werte mit geeigneten Wrappern behandeln und dann mit passenden Methoden die primitiven Werte extrahieren.

Beachten Sie, dass das Programm nicht gegen Falscheingaben (falsches Format der Übergabewerte oder gar fehlende Parameter) gesichert ist.

6.4.4 Der Instanceof-Operator

Der Instanceof-Operator ist ein binärer Operator mit zwei Operanden: einem Objekt auf der linken und einem Klassennamen auf der rechten Seite. Wenn das Objekt auf der linken Seite des Operators tatsächlich eine Instanz der Klasse auf der rechten Seite des Operators ist (ohne einer ihrer Subklassen), dann gibt dieser Operator den booleschen Wert true zurück. Ansonsten wird false zurückgegeben.

Beispiel:

neuesObjekt instanceof BestehendeKlasse

Dieser Operator wurde in früheren Java-Versionen oft genutzt, um im Ereignisbehandlungsmodell (Version 1.0) zu überprüfen, von welcher Komponente ein Ereignis ausging, etwa von einem Button oder einem Menü-Eintrag. Wir werden bei der Diskussion der Ereignisbehandlungsmodelle darauf zurückkommen.

6.5 Anweisungen

Wie viele Elemente sind auch Anweisungen in Java denen in C/C++ sehr ähnlich. Anweisungen werden einfach der Reihe nach ausgeführt. Ausnahmen sind Kontrollfluss-Anweisungen oder Ausnahmeanweisungen. Sie werden aufgrund ihres Effektes ausgeführt und haben selbst keine Werte. Java hat viele verschiedene Arten von Anweisungen.

Anweisungart Beschreibung
Leere Anweisung Sie tut nichts und dient als Platzhalter.
Blockanweisungen Ein Zusammenfassen von größeren Sourceteilen zu Blockstrukturen.
Bezeichnete Anweisung Jede Anweisung kann mit einer Bezeichnung beginnen. Diese Bezeichnungen dürfen keine Schlüsselworte, bereits festgelegte lokale Variablen oder schon in diesem Modul verwendeten Bezeichnungen sein.
Deklarationen Einführung eines primitiven Datentyps, eines Datenfelds, einer Klasse, einer Schnittstelle oder eines Objekts.
Ausdrucksanweisungen Ausdrucksanweisungen werden am häufigsten verwendet. Java verwendet sieben verschiedene Ausdrucksanweisungsarten: Zuordnung Pre-Inkrement Pre-Dekrement Post-Inkrement Post-Dekrement Methodenaufruf Zuweisungsausdruck
Auswahlanweisung Sie sucht einen von mehreren möglichen Kontrollflüssen aus. Es gibt in Java drei verschiedene Arten von Auswahlanweisungen: if, if-else switch-case-default. Dies ist identisch mit C und C++.
Iterationsanweisung Sie gibt an, wann und wie Schleifen gestartet werden. Es gibt in Java drei Arten von Iterationsanweisungen: while do for Bis auf Sprünge und Bezeichnungen sind sie identisch mit den gleichnamigen Iterationsanweisungen in C und C++.
Sprunganweisungen Sprunganweisungen geben die Steuerung entweder an den Anfang oder das Ende des derzeitigen Blocks oder aber an bezeichnete Anweisungen weiter. Beachten Sie, dass es in Java keine goto-Anweisung gibt. Das optionale Bezeichnungsargument für Unterbrechungs- und Fortsetzungsanweisungen ist in C oder C++ nicht vorhanden. Diese Bezeichnungen müssen im selben Block stehen, und continue-Bezeichnungen müssen bei den Iterationsanweisungen stehen. Diese Anweisungen beinhalten auch Implikationen für Synchronisierungsanweisungen im selben Block. Bei den vier Arten von Sprunganweisungen handelt es sich um: break continue return throw throw-Anweisungen sind ein wichtiger Bestandteil des Ausnahmemechanismus.
Synchronisationsanweisung Sie wird für den Umgang mit Multithreading benutzt. Die Schlüsselworte synchronized und threadsafe werden zum Markieren von Methoden und Blöcken benutzt, die eventuell vor gleichzeitiger Verwendung geschützt werden sollen.
Schutzanweisung Schutzanweisungen werden zur sicheren Handhabung von Code, der Ausnahmen auslösen könnte (beispielsweise das Teilen durch Null), gebraucht. Diese Anweisungen benutzen die Schlüsselworte try, catch und finally.
Unerreichbare Anweisung Sie erzeugt einen Fehler zur Kompilierzeit.

Tabelle 6.35:   Anweisungsarten   in Java

Schauen wir uns die Anweisungen im Detail an.

6.5.1 Blöcke und Anweisungen

Methoden und statische Konstruktoren (spezielle Konstruktoren, um Variablen mit einem Startwert zu belegen) werden in Java durch Anweisungsblöcke definiert. Bei einem Anweisungsblock handelt es sich um eine Reihe von Anweisungen, die in geschweiften Klammern ({}) stehen. Diese Struktur ist ohne Veränderung aus C/C++ übernommen. Wenn die Form einer Anweisung eine Anweisung oder eine Unteranweisung verlangt, kann jeder sinnvolle Block anstelle der Unteranweisung eingefügt werden.

Ein Anweisungsblock hat seinen eigenen Geltungsbereich für die in ihm enthaltenen Anweisungen. Das bedeutet, dass lokale Variablen in diesem Block deklariert werden können, die außerhalb dieses Blocks nicht verfügbar sind, und deren Existenz erlischt, wenn der Block ausgeführt wurde.

Blöcke können beliebig ineinander geschachtelt werden. Sie müssen allerdings die Schachtelung wieder sauber schließen. Viele Fehler beruhen auf geöffneten und nicht korrekt geschlossenen Blöcken. Der Java-Compiler überprüft natürlich solche Blockstrukturen. Die im Fehlerfall zurückgegebene Meldung kann jedoch oft in die Irre führen, da der endgültige Fehler sich erst an einer anderen Stelle auswirkt.

6.5.2 Leere Anweisungen

In Java ist es erlaubt, leere Anweisungen zu erstellen. Leere Anweisungen erscheinen als eine eigene Anweisung erst einmal unsinnig, denn es handelt sich dabei um eine Anweisung, die zwar Platz im Code belegt, aber keine Funktion besitzt. Es gibt jedoch ein paar sinnvolle Anwendungen:

1. Man kann Kennzeichen setzen.
2. Man kann eine leere Anweisung bei Bedarf mit Debugging-Befehlen füllen. Wenn die Debugging-Befehle nicht gebraucht werden, kommentiert man sie aus und lässt die leere Anweisung stehen.
3. Man kann eine leere Anweisungen schon einmal prophylaktisch in einen Source einfügen und erst mit Befehlen füllen, wenn man sich richtig an die Programmstelle begibt. Das hilft erheblich gegen Vergesslichkeit.

Für den Compiler sieht eine leere Anweisung nur wie ein zusätzliches Semikolon aus.

6.5.3 Bezeichnete Anweisung

Jede Anweisung kann in Java eine Bezeichnung bekommen. Die eigentliche Benennung hat die gleichen Eigenschaften wie jeder andere Bezeichner; sie darf nicht den gleichen Namen wie ein Schlüsselwort oder ein anderer, bereits deklarierter lokaler Bezeichner haben. Wenn sie aber den gleichen Namen wie eine Variable, eine Methode oder ein Typ hat, die für diesen Block verfügbar sind, dann erhält die neue Benennung innerhalb dieses Blocks Vorrang, und die außenstehende Variable, die Methode oder der Typ wird versteckt. Die Reichweite der Benennung erstreckt sich über den ganzen Block. Der Benennung folgt immer ein Doppelpunkt. Diese Technik ist uralt. Man kennt sie sogar schon von der Batch-Programmierung unter DOS.

Die Benennungen werden nur von den Sprunganweisungen break und continue benutzt. Wir kommen bei der Behandlung der beiden Anweisungen darauf zurück.

Wer sich noch an goto erinnert und diese Sprunganweisung lieben gelernt hat, muss enttäuscht werden. Obwohl das (Un-)Wort in Java reserviert ist, hat es keine Funktion.

6.5.4 Deklarationen

Deklarationen definieren eine Variable. Dabei ist es egal, ob diese Variable eine Klasse, eine Schnittstelle, ein Datenfeld, ein Objekt oder ein primitiver Typ ist. Das konkrete Format einer solchen Anweisung jedoch hängt davon ab, welcher der fünf verschiedenen Typen deklariert wird.

Klassendeklaration

Eine Klassendeklaration besteht immer aus sechs Teilen, die in der nachfolgend in der Tabelle angegebenen Reihenfolge zusammengesetzt werden.

Bestandteil Status Beschreibung
Klassenmodifier Optional Dies sind die Schlüsselworte abstract, final oder public. Eine Klasse kann nicht sowohl final als auch abstract als Modifier haben.
Klasse Zwingend Das Schlüsselwort class.
Bezeichner Zwingend Ein normaler Bezeichner.
Super Optional Das Schlüsselwort extends gefolgt von einem Typnamen. Der Typ muss eine verfügbare Klasse sein, die nicht final ist.
Schnittstellen Optional Das Schlüsselwort implements, gefolgt von einer durch Kommata getrennten Liste mit Schnittstellen.
Klassenkörper Zwingend  

Tabelle 6.36:   Klassendeklaration

Schnittstellendeklaration

Eine Schnittstellendeklaration besteht immer aus fünf Teilen, die in der nachfolgend in der Tabelle angegebenen Reihenfolge zusammengesetzt werden.

Bestandteil Status Beschreibung
Schnittstellenmodifier Optional Dies sind die Schlüsselworte -abstract oder public. Alle Schnittstellen sind abstrakt, der Modifier kann jedoch trotzdem aufgrund
der Eindeutigkeit gesetzt werden.
Interface Zwingend Das Schlüsselwort interface.
Bezeichner Zwingend Ein normaler Bezeichner.
Schnittstellenerweiterung Optional Das Schlüsselwort extends, gefolgt von einer durch Kommata getrennten Liste von Schnittstellenbezeichnern.
Schnittstellenkörper Zwingend  

Tabelle 6.37:   Schnittstellendeklaration

Datenfelddeklaration

Eine Datenfelddeklaration besteht immer aus fünf Teilen, die in der nachfolgend in der Tabelle angegebenen Reihenfolge zusammengesetzt werden.

Bestandteil Status Beschreibung
Datenfeldmodifier Optional Dies sind die Schlüsselworte public, -protected, private oder synchronized.
Typenname Zwingend Der Name des Typen oder der Klasse, die aufgestellt werden.
Klammern Zwingend [ ].
Initialisierung Optional  
Semikolon Zwingend ;

Tabelle 6.38:   Datenfelddeklaration

Objektdeklaration

Ein Objekt zu deklarieren bedeutet, eine Referenz zu erstellen. Diese Referenz verweist nur dann auf ein Objekt, wenn dieses Objekt initialisiert oder dieser Referenz zugeordnet wurde. Eine Objektdeklaration besteht aus den folgenden Teilen:

Bestandteil Status Beschreibung
Objektmodifier Optional Dies sind die Schlüsselworte public, -protected, private oder synchronized.
Typenname Zwingend Der Name der Klasse, deren Instanz dieses Objekt ist.
Initialisierung Optional  
Semikolon Zwingend
;

Tabelle 6.39:   Objektdeklaration

Deklaration primitiver Typen

Eine Variable eines primitiven Typen zu deklarieren bedeutet, dass Speicherplatz für diese Variable eingerichtet wird. Wenn ihr kein Wert bei einer expliziten Initialisierung zugewiesen worden ist, nimmt sie ihren spezifischen Grundwert an. Die Deklaration von primitiver Datentypen setzt sich aus den folgenden Teilen zusammen:

Bestandteil Status Beschreibung
Datenfeldmodifier Optional Dies sind die Schlüsselworte public, -protected, private oder synchronized.
Typenname Zwingend Die Bezeichnung des Datentyps: boolean, char, byte, short, int, long, float, double.
Initialisierung Optional  
Semikolon Zwingend ;

Tabelle 6.40:   Deklaration primitiver Typen

6.5.5 Ausdrucksanweisungen

In Java gibt es wie bereits beschrieben sieben verschiedene Arten von Ausdrucksanweisungen.

Ausdrucksanweisungen Beispiel
Zuordnung a = 42
Pre-Inkrement ++wert
Pre-Dekrement --wert
Post-Inkrement wert++
Post-Dekrement wert--
Methodenaufruf System.out.println("Hello World!")
Zuweisungsausdruck Byte wert = new Byte();

Tabelle 6.41:   Ausdrucksanweisungen

1. Alle Ausdrucksanweisungen müssen mit einem Semikolon beendet werden.
2. Eine Ausdrucksanweisung wird immer vollständig durchgeführt, bevor die nächste Anweisung ausgeführt wird.
3. Eine Zuweisungsanweisung kann einen Ausdruck rechts von dem Gleichheitszeichen (dem Zuweisungsoperator =) stehen haben. Dieser Ausdruck kann jede der sieben Ausdrucksanweisungen sein. Es darf in Java immer nur die rechte Seite einer solchen Zuweisung festgelegt werden.
4. Unter Umständen kann der Rückgabewert einer Methode, die nicht leer ist, ohne Zuweisung aufgerufen werden. Dazu muss der Rückgabewert explizit als void festgelegt werden.
5. Eine Ausdrucksanweisung kann natürlich aus komplexen Verschachtelungen bestehen. Klammern können zur Festlegung der Reihenfolge dienen, in der die einzelnen Unteranweisungen bewertet werden.

6.5.6 Auswahlanweisungen

Bei Auswahlanweisungen bleibt es gleichermaßen spannend, denn auch diese gehören zu den wichtigsten Anweisungen in einer Programmiersprache. Java unterstützt drei verschiedene Arten von Auswahlanweisungen:

if 
if-else
switch

Die if- und die if-else-Anweisung

Eine if-Anweisung testet eine boolesche Variable oder einen Ausdruck. Wenn die boolesche Variable oder der Ausdruck den Wert true hat, wird die nachstehende Anweisung oder der nachstehende Anweisungsblock ausgeführt. Wenn die boolesche Variable oder der Ausdruck den Wert false hat, wird die nachstehende Anweisung oder der nachstehende Anweisungsblock ignoriert und mit dem folgenden Block bzw. der folgenden Anweisung fortgefahren.

Eng verwandt ist die if-else-Anweisung, die genau genommen nur eine Erweiterung der if-Anweisung ist. Sie hat zusätzlich nur noch einen zusätzlichen else-Teil. Dieser else-Teil - eine Anweisung oder ein Block - wird dann ausgeführt, wenn der boolesche Test im if-Teil der Anweisung den Wert false ergibt. Ein kleines Beispiel zeigt die Verwendung.

import java.util.*;
public class IfTest {
public static void main(String args[]) {
Random a = new Random();
if(a.nextFloat()<0.5) System.out.println("Klein");
else System.out.println("Gross"); 
}  }

In dem Beispiel wird mit einem Zufallsobjekt gearbeitet. Die Methode nextFloat() extrahiert daraus den Zufallswert, der zwischen 0.0 und 1.0 liegt. Die nachfolgende if-else-Anweisung wird je nach Wert den ersten oder den zweiten Zweig auswählen.

Wenn Sie innerhalb des else-Zweigs eine neue if-Anweisung notieren, haben Sie mit der resultierenden if-else-if-Anweisung einen Spezialfall der if-else-Anweisung. Schreiben wir unser Beispiel entsprechend um.

import java.util.*;
public class IfTest2 {
  public static void main(String args[]) {
  Random a = new Random();
  if(a.nextFloat()<0.2)
  System.out.println("Ganz klein");
  else if(a.nextFloat()<0.4)
  System.out.println("Klein"); 
  else if(a.nextFloat()<0.6)
  System.out.println("Mittel"); 
  else if(a.nextFloat()<0.8)
  System.out.println("Gross"); 
  else System.out.println("Ganz Gross"); 
}  }

Ein solches Konstrukt kann für eine etwas größere Auswahl von Möglichkeiten durchaus Sinn machen. Es gibt jedoch eine dann meist noch etwas besser geeignete Auswahlanweisung - die switch-Anweisung.

Die switch-Anweisung

Eine switch-Anweisung ermöglicht das Weitergeben des Kontrollflusses an eine von vielen Anweisungen in ihrem Block mit Unteranweisungen. An welche Anweisung innerhalb der switch-Anweisung der Kontrollfluss weitergereicht wird, hängt vom Wert des Ausdrucks in der Anweisung ab. Es wird die erste Anweisung nach einer case-Bezeichnung ausgeführt, die denselben Wert wie der Ausdruck hat. Wenn es keine entsprechenden Werte gibt, wird die erste Anweisung hinter der default-Bezeichnung ausgeführt. Wenn auch die nicht vorhanden ist, wird die erste Anweisung nach dem switch-Block ausgeführt. Die zu testenden switch-Ausdrücke und case-Bezeichnungskonstanten müssen alle vom Typ byte, short, char oder int sein. Mit dieser Auswahlanweisung haben Sie eine handlichere Auswahlanweisung, die oft die gleichen Möglichkeiten wie die Erweiterung der if-else-Anweisung bietet. Einschränkung gegenüber dieser ist jedoch, dass nur diskrete Werte getestet werden können (also Gleichheit) und keine Vergleiche auf »kleiner« oder »größer« möglich sind. Auch dürfen keine zwei case-Bezeichnungen im gleichen Block denselben Wert haben. So etwas ist in der if-else-if-Anweisung im Prinzip denkbar (mehrfacher Vergleich in verschiedenen Zweigen auf den gleichen Wert), wenn auch nicht sonderlich sinnvoll, denn nach dem ersten Treffer werden folgende Treffer nicht mehr entdeckt, weil der Kontrollfluss aus der if-else-if-Anweisung herausspringt. Bezeichnungen beeinflussen den Kontrollfluß nicht. Die Kontrolle behandelt diese Bezeichnungen so, als seien sie nicht vorhanden. Daher können beliebig viele Bezeichnungen vor derselben Codezeile stehen.

Schreiben wir unser Beispiel für die if-else-if-Anweisung um (beachten Sie, dass wir wegen der Datentypen mit dem Faktor 5 multiplizieren und casten).

import java.util.*;
public class SwitchTest {
  public static void main(String args[]) {
  Random a = new Random();
  switch((byte)(a.nextFloat()*5)) {
    case 1: System.out.println("Ganz klein");
    case 2: System.out.println("Klein"); 
    case 3: System.out.println("Mittel"); 
    case 4: System.out.println("Gross"); 
    default: System.out.println("Ganz Gross"); 
   }
  }
}

Was wird die Ausgabe sein? Vielleicht werden Sie überrascht. Im Gegensatz zu der if-else-if-Anweisung springt der Kontrollfluss nach einem Treffer nicht automatisch aus der Struktur, sondern es wird von dem ersten Treffer an die Struktur bis zum Ende ausgeführt. Das heißt, alle nachfolgend notierten Anweisungen werden explizit ausgeführt.

Dies ist in diversen anderen Sprachen anders und damit lauert hier eine gefährliche Fehlerquelle. Wenn dieses Verhalten unterbunden werden soll, setzt man in jedem Block hinter einem Label eine break-Anweisung. Damit vermeiden Sie die Ausführung von mehr als einem Block. Die gesamte switch-case-Struktur wird nach einem Treffer beendet. Unser Beispiel sieht dann so aus:

import java.util.*; 
public class SwitchTest2 {
  public static void main(String args[]) {
  Random a = new Random(); 
  switch((byte)(a.nextFloat()*5)) {
    case 1: { 
  System.out.println("Ganz klein");
   break;
  }
    case 2: { 
  System.out.println("Klein");
   break;
  } 
    case 3: { 
  System.out.println("Mittel");
   break;
  } 
    case 4: { 
  System.out.println("Gross");
   break;
  } 
    default: { 
  System.out.println("Ganz Gross");
   break;
  } 
   }
  }
}

Natürlich können Sie Breaks auch gezielt einsetzen, um durch Auslassen von break- Anweisungen den Durchlauf von mehreren Blöcken zu erreichen. Etwa bei einer angeordneten Auswahl, wo ab einer gewissen Größe alle Folgeanweisungen Sinn machen.

6.5.7 Iterationsanweisungen

Iterationsanweisungen werden auch Wiederholungsanweisungen genannt und dieser Name macht deutlich, um was es geht: die kontrollierte Wiederholung von Anweisungsfolgen zur Laufzeit.

Es gibt in Java drei Arten von Iterationsanweisungen:

while 
do 
for

Diese sind fast identisch zu den analogen Anweisungen in C und C++, mit der Ausnahme, dass in Java die Anweisungen die optionalen Parameter continue und break haben.

Die while-Anweisungen

Die while-Anweisung testet eine boolesche Variable oder einen Ausdruck. Solange der Test den Wert true hat, wird die Unteranweisung oder der Block ausgeführt. Erst wenn die boolesche Variable oder der Ausdruck den Wert false ausweist, wird die Wiederholung eingestellt und die Kontrolle an die nächste Anweisung nach dem while-Konstrukt weitergegeben.

Die Syntax sieht so aus:

while(<Bedingung>) <Unteranweisung oder Block>

Erstellen wir wieder ein kleines Testprogramm.

class WhileTest {
 public static void main (String args[])  {
  int testvariable=0; // Eine numerische Variable
  while (testvariable < 5) {
   System.out.println(testvariable); 
   testvariable = testvariable + 1;
  }
 System.out.println("Und Schluss"); 
 }  }

Die Ausgabe wird die Zahlenkolonne von 0 bis 4 und danach Und Schluss sein. Um die while-Schleife jemals verlassen zu können, wird der Wert der Testvariablen in jedem Schleifendurchgang um den Wert 1 erhöht.

Wenn der Ausdruck nicht von Anfang an true ist, wird der Block in der Unteranweisung niemals ausgeführt. Wenn er dahingegen true ist, dann wird dieser Codeblock so lange wiederholt, bis er nicht mehr true ist, oder eine Sprung-Anweisung ausgeführt wird, und er die Kontrolle an eine Anweisung außerhalb der Schleife weitergibt.

Eine while-Schleife ist ein guter Kandidat für so genannte Endlosschleifen. Lassen Sie in unserem Beispiel einfach mal die Zeile testvariable = testvariable + 1; weg. Die Bedingung zum Durchlaufen der Schleife wird immer erfüllt sein und Sie hängen in einer Endlosschleife. Mit ein bisschen Glück lässt sich das Programm noch vom Betriebssystem beenden, mit Pech müssen Sie den Rechner ausschalten.

Die do-Anweisung

Auch die do-Anweisung (oder auch do-while-Anweisung genannt) testet eine boolesche Variable oder einen Ausdruck. Solange dieser Test den Wert true hat, wird die Unteranweisung oder der Block ausgeführt. Erst wenn die boolesche Variable oder der Ausdruck den Wert false ausweist, wird die Wiederholung eingestellt und die Schleife verlassen. Es gibt aber einen ganz wichtigen Unterschied zur while-Schleife: der Codeblock innerhalb der do-Anweisung wird auf jeden Fall mindestens einmal ausgeführt. Dies geschieht immer, ob die Bedingung erfüllt ist oder nicht. Rein von der Syntax her kann man es sich dadurch verdeutlichen, dass die Überprüfung am Ende der Struktur steht. Sie sieht immer so aus:

do <Unteranweisung oder Block> while(<Bedingung>)

Programmbeispiel:

import java.util.*;
class DoTest {
 public static void main (String args[]) {
  byte testvariable=0; // Eine numerische Variable
  Random a = new Random();
  do {
   System.out.println(testvariable); 
   testvariable = (byte)(a.nextFloat()*5);
  }
 while (testvariable < 3);
 System.out.println("Und Schluss"); 
 }  }

Das Beispiel wiederholt so lange die Schleife, bis die zufällig generierte Testvariable größer als 3 ist. Die do-Schleife wird aber auf jeden Fall einmal durchlaufen.

Die for-Anweisung

Die for-Anweisungen sind uns ja schon ein paar Mal begegnet. Es sind die komplexesten der drei Iterationsanweisungen. Eine for-Anweisung sieht wie folgt aus:

for (<Initialisierung>; <Test>;<In- oder Dekrement>) 
<Unteranweisung oder Block>

Einige Erklärungen zu der Syntax:

1. Hinter for kann optional ein Leerzeichen folgen.
2. Der Initialisierungsteil kann eine durch Kommata getrennte Reihe von Deklarations- und Zuweisungsanweisungen enthalten. Erst durch ein Semikolon wird der Initialisierungsteil beendet. Diese Deklarationen haben nur Gültigkeit für den Bereich der for-Anweisung und ihrer Unteranweisungen.
3. Der Testteil enthält eine boolesche Variable oder einen Ausdruck, der einmal pro Schleifendurchlauf neu bewertet wird. Wenn der Vergleich den Wert false ergibt, wird die for-Schleife verlassen. Auch dieser Teil wird wieder durch ein Semikolon beendet.
4. Auch der In- oder Dekrementteil kann eine durch Kommata getrennte Reihe von Ausdrücken sein, die einmal pro Durchlauf der Schleife bewertet werden. Diese mehrfachen Ausdrücke in diesem Teil der for-Schleife machen nur dann Sinn, wenn sie bereits im Initialisierungsteil deklariert wurden. Dieser Teil wird gewöhnlich dazu verwendet, einen Index, der im Testteil überprüft wird, zu inkrementieren (hochzuzählen) oder zu dekrementieren (herabzuzählen).

Nutzen wir zur praktischen Veranschaulichung der for-Schleife ein Beispiel, das die Summe einer Reihe von 1 bis zu einem vorgegebenen Endwert berechnet.

/* Berechnet die Summe einer Reihe mit vorgegebener Zahl über eine for-Schleife - eine 
Alternative zu der echten Gauss-Formel */
class Gauss {
 public static void main(String argv[]) {
 long ergebnis=0;
 int ende=70;
 for (int i=1;i<=ende;i++) {
  ergebnis += i;
 }
 System.out.println("Ergebnis Summe 1 bis " + ende 
   + ": "+ergebnis);
 }  }

Das Beispiel arbeitet in der Schleife mit so genannten schleifenlokalen Variablen. Das bedeutet in unserem Fall, dass die Zählvariablen in for-Schleifen nicht unbedingt außerhalb der Schleife vereinbart werden müssen, sondern ebenso direkt im Initialisierungsteil der for-Schleife direkt vereinbart werden können. Sie sind dann auch nur dort bekannt. Diese im Initialisierungsteil der for-Schleife direkt vereinbarten Zählvariablen sind ein Sonderfall von normalen lokalen Variablen und überdecken ggf. gleichnamige Variablen im übergeordneten Programmblock. Die Variablen im übergeordneten Programmblock bleiben hierdurch unverändert.

Die Berechnung der Summe einer Reihe geht natürlich viel eleganter, wenn man die echte Gauß-Formel verwendet. Das könnte dann so aussehen:

/* Berechnet die Summe einer Reihe mit vorgegebener Zahl nach der Gauss-Formel */
class Gauss2 {
 public static void main(String argv[]) {
 int n;
 long x;
 n = 70;
 x = (long)((n+1) / 2.0 * n );
 System.out.println("Ergebnis Summe 1 bis "+n+": "+x);
 }  }

Die Schleife for(;;), d.h. ohne explizite Parameter, wird keinen Compilerfehler erzeugen, denn syntaktisch ist sie völlig korrekt. Analog der Syntax while(true) wird sie eine Endlosschleife erzeugen.

6.5.8 Java-Sprunganweisungen

Ähnlich dem alten goto-Befehl gibt es auch in Java Sprunganweisungen. Diese unterbrechen einen jeweiligen Programmablauf und übergeben den Programmfluss an eine andere Stelle. Java kennt vier Arten von Sprunganweisungen:

break 
continue
return 
throw

Die break-Anweisung

Die Unteranweisungsblöcke von Schleifen und switch-Anweisungen können durch die Verwendung der break-Anweisung verlassen werden (wir haben es im Beispiel der switch-Anweisungen ja schon gesehen). Eine unbezeichnete break-Anweisung springt zu der nächsten Zeile nach der derzeitigen (innersten) Wiederholungs- oder switch-Anweisung. Als Beispiel soll hier auf das Beispiel bei der Behandlung von switch-case verwiesen werden (siehe Seite 244).

Mit einer bezeichneten break-Anweisung am Anfang einer Schleife kann an eine Anweisung mit dieser Bezeichnung in der derzeitigen Methode gesprungen werden. Dazu müssen Sie vor dem Anfangsteil der Schleife ein Label (eine Beschriftung) mit einem Doppelpunkt eingeben. Sofern Sie ein falsch gesetztes oder nicht vorhandenes Label in einer Sprunganweisung angeben, wird der Compiler eine Fehlermeldung ausgeben.

Wenn es beim Auslösen der break-Anweisung eine umgebende Ausnahmebehandlungsroutine mit finally-Teil gibt, wird immer dieser Teil zuerst ausgeführt, bevor die Kontrolle weitergegeben wird.

Richtig interessant werden die benannten Schleifen aber eigentlich erst in Verbindung mit verschachtelten Schleifen. Es wird Ihnen vielleicht auffallen, dass diese bezeichneten Anweisungen ziemlich an das goto-return-Konstrukt aus alten Programmiererzeiten erinnern. Diese Assoziation ist leider nicht ganz von der Hand zu weisen, denn für ein solches Verfahren lässt sie sich missbrauchen. Glücklicherweise sind mit diesen Konstrukten keine beliebigen Sprünge innerhalb des Programms erlaubt, sondern es lassen sich in Java immer nur Sprünge über mehrere Blöcke innerhalb einer Schleife durchführen.

Die continue-Anweisung

Durch die Anweisung continue wird im Gegensatz zu break nicht die gesamte Schleife abgebrochen, sondern der aktuelle Schleifendurchlauf wird unterbrochen. Es wird zum Anfang der Schleife zurückgekehrt, falls hinter continue kein Bezeichner steht. Ansonsten wird zu einer äußeren Schleife zurückgekehrt, die eine Markierung gleichen Namens enthält.

Eine continue-Anweisung darf nur in einem Unteranweisungsblock einer Iterationsanweisung stehen (while, do oder for). Bei einer unbezeichneten continue-Anweisung werden die restlichen Anweisungen im innersten Block der Wiederholungsanweisung übersprungen und die Schleife wieder von vorne durchlaufen, beispielsweise um Fehler abzufangen. Ein Beispiel ist die Verhinderung einer Division durch Null (obwohl das besser mit einer Ausnahmebehandlung abgefangen werden kann). Der nachfolgende Quellcode skizziert die Technik:

float zuteilender, ergebnis;
int teiler;
...
for (teiler = -42; teiler < 42; teiler++) {
  if (teiler ==0) continue;
  ergebnis= zuteilender / teiler;
  ...
}

Auch bei der continue-Anweisung gibt es die Möglichkeit, die Schleife mit einem Bezeichnungsparameter zu versehen. Dies ermöglicht eine Kontrolle darüber, mit welcher Ebene der verschachtelten Iterationsanweisungen fortgefahren werden soll. Auch für die continue-Anweisung gilt, dass ein evtl. vorhandener finally-Teil einer derzeit aktiven try- Anweisung in der angezeigten verschachtelten Ebene immer zuerst ausgeführt wird.

Ein vollständiges Beispiel sieht so aus:

class ContiTest {
 public static void main (String args[]) {
  int x=0; 
  while (x<10) { 
    if (++x <10) continue; 
    System.out.println("x ist 10"); 
   }  
 }  }

Im obigen Beispiel wird nur ein einziges Mal der String x ist 10 ausgegeben. Nämlich genau dann, wenn x identisch mit 10 ist, da ansonsten vorher immer wieder zum Schleifenursprung zurückgekehrt wird. Beachten Sie das Konstrukt (++x <10). Wir verwenden dabei den Inkrement-Operator, um mit der if-Anweisung keine Endlosschleife zu erzeugen (die Zählvariable muss ja irgendwie weitergezählt werden und der Bereich nach continue wird nicht erreicht).

Die return-Anweisung

Eine return-Anweisung gibt die Kontrolle an den Aufrufer einer Methode zurück. Wenn sich die return-Anweisung in einer Methode befindet, die nicht als void deklariert wurde und kein Konstruktor ist, kann und muss sie einen Parameter (den so genannten Rückgabewert) des Typs zurückgeben, wie in der Deklaration der Methode angegeben. Diese Rückgabewerte können dann von dem aufrufenden Programm weiter verarbeitet werden.

Wenn es einen finally-Teil einer umgebenden Ausnahmebehandlung gibt, wird dieser Teil zuerst ausgeführt, bevor die Kontrolle weitergegeben wird.

Für das nachfolgende Beispiel müssen wir selbst geschriebene Methoden einsetzen. Diese werden vor einem Aufruf in der main()-Methode innerhalb der Klasse, aber außerhalb der main()-Methode deklariert.

Die Anordnung der Methoden vor der main()-Methode ist nicht zwingend erforderlich. Sie kann auch danach erfolgen.
class ReturnTest {
  static String methode1()  {
    return "ABC";
  }
  static int methode2()  {
    return 42;
  }
  static void methode3()  {
    System.out.println("Methode 3");
    return;
  }
  public static void main(String args[])  {
    System.out.println(methode1());
    System.out.println(methode2());
    methode3();
  }
}

Beachten Sie, dass die Aufrufe der Methoden. methode1() und methode2()direkt als Parameter in der println()-Methode stehen. Ausgegeben wird der Rückgabewert der Methoden:

ABC
42
Methode 3

Die throw-Anweisung

In Java ist es möglich, durch die throw-Anweisung eine Laufzeitausnahme des Programms zu erzeugen. Dies bedeutet, dass der normale Programmablauf durch eine Ausnahme unterbrochen wird, die zuerst von dem Programm behandelt werden muss, bevor der normale Programmablauf weitergeht. Die Laufzeitausnahme verwendet ein Objekt als Argument. Dieses Objekt, das hinter der Anweisung throw steht, bezeichnet einen Referenzausdruck, der gewöhnlich von der Klasse Exception abgeleitet sein muss.

Wenn ein Programminterpreter auf eine throw-Anweisung stößt, wird die Ausführung des Programms so lange unterbrochen, bis eine catch-Anweisung mit einem Formalparameter gefunden wird, der dem Klassentyp oder dem Typ einer Superklasse des Argument-Objekts der throw-Anweisung entspricht .

Ein gutes Beispiel für eine solche Ausnahme ist der Versuch, eine nicht vorhandene Datei zu öffnen. Eine Klasse, die die Dateibehandlung implementiert, kann prüfen, ob die Berechtigungen für einen Dateizugriff auf eine spezifizierte Datei ausreichend sind und ob die Datei vorhanden ist. Ist dies nicht der Fall, so wird eine Ausnahme erzeugt (eine »Exception geworfen«).

Der finally-Teil einer try-Anweisung wird ausgeführt, sobald auf ihn gestoßen wird.

Mehr zur throw-Anweisung folgt bei der Behandlung von Ausnahmen.

Synchronisationsanweisung

Die Synchronisationsanweisung wird für den Umgang mit Multithreading benutzt. Die Schlüsselworte synchronized und threadsafe werden zum Markieren von Methoden und Blöcken benutzt, die eventuell vor gleichzeitiger Verwendung geschützt werden sollen.

Der umfangreiche Themenkomplex »Multithreading« soll an anderer Stelle ausführlich behandelt werden.

6.5.9 Schutzanweisung

Java verfolgt zum Abfangen von Laufzeitfehlern ein Konzept, das mit so genannten Ausnahmen arbeitet. Wir werden diesem Ausnahmekonzept ein eigenes Kapitel (zusammen mit dem Debugging von Sourcecode) widmen. Nur so viel schon vorab:

Ausnahmen sind nicht planbare Situationen, die eine unmittelbare Reaktion durch das Programm bzw. den Anwender notwendig machen. Dies ist beispielsweise der Fall, wenn ein Zugriff auf ein Diskettenlaufwerk erfolgt, wo keine Diskette eingelegt ist. Dieser Vorgang wird in Java eine Ausnahme erzeugen, die dann unmittelbar vom Programm bearbeitet werden muss.

Schutzanweisungen werden zur sicheren Handhabung von Code, der Ausnahmen auslösen könnte (beispielsweise das Teilen durch Null), gebraucht. Diese Anweisungen benutzen die drei folgenden Schlüsselworte:

try 
catch
finally

Diese Anweisungen werden zur Handhabung von Ausnahmen in einer Methode benutzt. Diese Ausnahmenbehandlung ist der switch-case-default-Anweisung recht ähnlich. Nach dieser Analogie verhält sich die try-Anweisung wie die switch-Anweisung. Allerdings hat die try-Anweisung anstelle eines ganzzahligen Parameters einen Codeblock. Wenn innerhalb eines try-Blockes eine Ausnahme auftaucht, wird die Ausführung dieses Blocks unterbrochen und die Kontrolle mit dem richtigen Objekttyp als Parameter an die catch-Anweisung weitergegeben. Die catch-Anweisungen haben Ausnahmeobjekte als Parameter. Die finally-Anweisung erlaubt die Abwicklung wichtiger Abläufe (wie zum Beispiel das Schließen von Dateien) bevor die Ausführung unterbrochen wird.

6.5.10 Unerreichbare Anweisung

Es ist leider leicht möglich, eine Methode zu schreiben, die Codezeilen enthält, die nie erreicht werden können - eine so genannte unerreichbare Anweisung. Sie ist zwar eigentlich nicht schädlich in dem Sinn, dass sie etwas Falsches tut, aber wenn man sich darauf verlässt, dass bestimmte Codezeilen ausgeführt werden und sie werden einfach nicht erreicht, kann der Schaden mindestens genauso groß sein. Der Java-Compiler bemerkt dies glücklicherweise rechtzeitig und erzeugt einen Fehler zur Kompilierzeit.

6.6 Klassen und Objekte

Kommen wir nur zu einem der absolut zentralen Begriffe in Java: Klassen. Wir haben uns mit den allgemeinen Hintergründen von Klassen schon in dem Abschnitt über die Theorie der Objektorientierung beschäftigt (siehe dazu Seite 138) und wollen nun diesen theoretischen Background in Java-Praxis überführen. Ihnen werden sicher viele Dinge sofort einleuchten, falls Sie sich durch diesen Buchabschnitt gearbeitet haben oder Sie sowieso mit der OOP vertraut sind.

Da Java absolut objektorientiert ist, können Sie keine prozeduralen Programme schreiben. Man verwendet statt dessen Klassen als Teile der Objektmuster. Klassen definieren den Zustand und das Verhalten von Objekten.

In der objektorientierten Sichtweise steht immer das Objekt im Mittelpunkt. Jedwede Operation ist in der Klasse implementiert, zu der ein Objekt gehört. Bei den Elementen einer Klasse, die als Felder bezeichnet werden, handelt es sich im Wesentlichen um Variablen, auf die die ganze Klasse und bei Bedarf auch andere Klassen zugreifen können. Die Ausführung einer Operation übernehmen die Objektmethoden. Sie verhalten sich ähnlich wie Funktionen in anderen Sprachen. Soll nun eine bestehende Operation um eine neue Funktionalität erweitert werden, so werden die Veränderungen in der Klasse vorgenommen und die zugehörigen Methoden dort geschrieben. Die neue Form der Operation wird einfach als Erweiterung innerhalb der Klasse hinzugefügt. Nach außen erscheint das Objekt unverändert (bzgl. der bisherigen Funktionalität) und lässt sich wie gehabt unter einem Namen ansprechen. Weitergehende Änderungen im Programm sind nicht notwendig. Daten und Methoden werden in Klassen gekapselt.

Ein großer Teil der Attraktivität von Klassen und OOP basiert auf der Fähigkeit der Vererbung. Dies gibt einem die Möglichkeit, auf Basis alter Klassen (mit geringerem Aufwand als bei vollständiger Neuentwicklung) neue Klassen zu erstellen. Weil diese neuen Klassen die Eigenschaften einer anderen Klasse erben können, werden sie Subklassen, und die Klasse, aus der sie abgeleitet werden, Superklasse genannt. Gemeinsame Erscheinungsbilder werden in der objektorientierten Philosophie soweit wie irgend möglich in einer Klasse zusammengefaßt werden. Erst wo Unterscheidungen möglich bzw. notwendig sind, die nicht für alle Mitglieder einer Klasse gelten, werden Untergruppierungen - Subklassen - gebildet.

6.6.1 Allgemeines zu Klassen in Java

Jedes Java-Programm besteht aus einer Sammlung von Klassen, aus denen zur Laufzeit die notwendigen Objekte erzeugt werden. Der gesamte Code, der bei Java verwendet wird, wird in Klassen eingeteilt. Jede Klasse, abstrakt oder nicht, definiert das Verhalten eines Objekts durch verschiedene Methoden. Verhalten und Eigenschaft kann von der einen Klasse zur nächsten weitervererbt werden. Alle Klassen in Java haben eine gemeinsame Oberklasse, die Klasse Object. Diese wiederum verfügt nur über eine Metaklasse. Auch Java selbst (als Entwicklungsplattform) ist aus Klassen aufgebaut, die mit dem JDK frei verfügbar sind. Das eigentliche RUNTIME-Modul bestand bis zur Version 1.2 aus der Datei »Java Core Classes« (classes.zip), mittlerweile besteht dieses im Wesentlichen aus der Datei rt.jar.

Kommen wir nun zu der allgemeinen Struktur einer Java-Klasse. Jede Klasse in Java besteht, was die Syntax angeht, aus zwei Teilen: der Deklaration und dem Body (Körper). Wir kennen ja schon diverse Klassen aus unseren Beispielprogrammen. Nehmen wir nun einmal eines unserer einfachsten Beispiele und sehen es unter dem Gesichtspunkt der Klassenstruktur an.

/* Beispielprogramm zur Bildschirmausgabe vom ASCII bzw. Unicode-Zeichensatz */
class Unicode2 {
public static void main (String args[]) { 
 int i;
 for (i=0; i < 256;i++) System.out.print((char)i);
 }  }

Die Deklaration ist class Unicode2, der Rest innerhalb der geschweiften Klammern ist der Klassenkörper. Dies ist jedoch wirklich nur die einfachste Form einer Klasse.

6.6.2 Allgemeine Klassendeklaration

Generell haben Klassendeklarationen in Java folgendes Format:

[<modifiers>] class <NeueKlasse> 
  [extends <NamederSuperKlasse>] 
  [implements <NamederSchnittstelle(n)>]

Alle Angaben in eckigen Klammern sind optional.

Es kann nur eine Superklasse, aber mehr als eine Schnittstelle implementiert werden. Dann müssen mehrere Schnittstellen, per Komma getrennt, hintereinander geschrieben werden.

Es gibt also vier Eigenschaften einer Klasse, die in einer Deklaration definiert werden können:

1. Modifier
2. Klassenname
3. Superklasse
4. Schnittstellen

Manche Quellen zerlegen den Modifier in zwei Bestandteile (die Sichtbarkeit und den Benutzungsgrad), was in sofern sinnvoll ist, da sich der Modifier in der Tat aus diesen zwei logischen Bestandteilen zusammensetzen kann, die nach gewissen Regeln kombiniert werden können (siehe nächsten Abschnitt).

Sie haben ja bisher schon gesehen, dass die Klassendeklaration oft nur das Schlüsselwort class und den Namen der Klasse enthält. Selbst ein Modifier muss nicht immer angegeben werden. Superklassen oder Schnittstellen erst recht nicht. Modifier sind jedoch meist üblich.

6.6.3 Klassen-Modifier

Modifier beginnen eine Klassendeklaration und legen fest, wie die Klasse gehandhabt werden kann, wenn sie fertig ist. Dass wir bisher weitgehend auf Modifier verzichtet haben, liegt daran, dass Klassen einen voreingestellten Defaultstatus haben. Wenn Sie davon abweichen wollen, müssen Sie explizit einen erlaubten Modifier verwenden. Diese stehen Ihnen zur Verfügung:

public 
final
abstract

Das Schlüsselwort public regelt die Sichtbarkeit der Klasse (andere Sichtbarkeitsmodifier, wie sie etwa bei Methoden Verwendung finden, sind bei Klassen nicht erlaubt), während die anderen beiden Schlüsselworte den Benutzungsgrad festlegen. Es darf zwar die Sichtbarkeit mit jedem der beiden Schlüsselworte für den Benutzungsgrad kombiniert, die beiden Schlüsselworte für den Benutzungsgrad dürfen jedoch nicht zusammen verwendet werden.

Freundliche Klassen - der voreingestellte Defaultstatus

Der voreingestellte Defaultstatus einer Klasse ist immer »freundlich« und wird immer dann verwendet, wenn Sie auf public am Anfang einer Klassendeklaration verzichten. Unsere bisherigen Beispiele waren fast immer freundlich.

Die freundliche Grundeinstellung aller Klassen bedeutet, dass diese Klassen zwar erweitert und von anderen Klassen benutzt werden können, aber nur von Objekten innerhalb desselben Pakets.

Wir haben das Paketkonzept bisher noch nicht weiter erläutert und werden im Anschluss an die Klassen intensiver darauf eingehen (Seite 303). Jedoch eine kleine Erklärung vorab. Das Paket-Konzept von Java dient dazu, mehrere Klassen zu einem Paket über die Anweisung package zusammenzufassen.

In C/C++ gibt es eine Notation zum Verbergen eines Namens, sodass nur die Funktionen innerhalb einer bestimmten Quelldatei darauf zugreifen können. Diese Schutzebene ist die besagte »freundliche« Ebene von Java. Für die Bezeichnung friendly wurde früher alternativ package verwendet.

Öffentliche Klassen - der Modifier public

Eine Klasse wird als öffentlich deklariert, wenn man den Modifier public vor die Klassendeklaration setzt. Dies bedeutet, dass alle Objekte auf public-Klassen zugreifen können. Die Erweiterung gegenüber freundlichen Klassen ist die, dass sie ebenfalls von allen Objekten benutzt und erweitert werden können, die nicht zum eigenen Paket gehören.

Die Deklaration des Klassennamens einer öffentlichen Klasse muss immer identisch sein mit dem Namen, unter dem der Source dieser Datei gespeichert wird (natürlich ohne die Erweiterung .java). Aber auch für nicht-öffentliche Klassen macht es Sinn, eine Datei, in der ausschließlich diese Klasse definiert ist, mit dem Klassennamen und der Erweiterung .java zu bezeichnen.

Erstellen wir ein kleines Beispiel, das unter dem Namen PublicTest.java (!) gespeichert werden soll.

public class FalscherName {
}

Wenn Sie das Beispiel kompilieren wollen, meldet der Compiler folgenden Fehler:

PublicTest.java:1: class FalscherName is public, should be declared in a file named 
FalscherName.java
public class FalscherName

^

1 error

Eine zwingende Folge der Tatsache, dass der Name der Quelltextdatei mit dem Namen der öffentlichen Klasse übereinstimmen muss, ist, dass es nur eine öffentliche Klasse in einer Java-Datei geben darf. Dabei sollte man auch Schnittstellen beachten. Auch diese können als öffentlich deklariert werden und müssen dann in einer Java-Datei des Schnittstellennamens gespeichert werden. Also kann auch keine öffentliche Klasse gemeinsam mit einer öffentlichen Schnittstelle in einer Java-Datei gespeichert (oder genauer - kompiliert) werden.

Finale Klassen - der Modifier final

Finale Klassen können nicht weiter abgeleitet werden. Mit anderen Worten: Sie dürfen keine Subklassen haben. Der Modifier final muss zu diesem Zweck am Beginn der Klassendeklaration gesetzt werden.

Beispiel:

final class Unicode

Finale Klassen dienen beispielsweise zum Erstellen eines Standards, der nicht mehr verändert werden soll. Testen wir in einem kleinen Beispiel, was passiert, wenn Sie eine Subklasse von einer finalen Klasse bilden wollen. Dabei können Sie beide Klassen in eine Java-Quelltextdatei ModifierTest.java notieren. Der Compiler macht daraus zwei .class-Dateien.

final class MeineFinaleKlasse {
 int a = 42;
}
class ModifierTest extends MeineFinaleKlasse {
}

Wenn Sie das Beispiel kompilieren wollen, meldet der Compiler folgenden Fehler:

ModifierTest.java:5: cannot inherit from final MeineFinaleKlasse
class ModifierTest extends MeineFinaleKlasse
                           ^
1 error

Abstrakte Klassen - der Modifier abstract

Unter einer abstrakten Klasse versteht man eine Klasse, von der nie eine direkte Instanz benötigt wird und von der auch keine Instanz gebildet werden kann (siehe dazu Seite 140). Prinzipiell dienen abstrakte Klassen dazu, unvollständigen Code zu deklarieren. Es handelt sich also in der Regel um eine Klasse, in der mindestens eine Methode nicht vollständig ist. Wenn Sie dies ohne die Kennzeichnung als abstrakt durchführen, wird durch den javac-Compiler ein Fehler erzeugt.

Die Deklaration einer Klasse als abstrakt bedeutet jedoch nicht, dass vollständiger Code einen Fehler erzeugt. Auch muss eine abstrakte Klasse keinen unvollständigen Code enthalten. Es ist nur wenig sinnvoll, solchen Code als abstrakt zu kennzeichnen.

Java charakterisiert abstrakte Klassen mit dem Schlüsselwort abstract.

Abstrakte Klassen werden im Allgemeinen zur Strukturierung eines Klassenbaums verwendet. Eine weitere Aufgabe von ihnen ist die Vereinbarung von Methodenschnittstellen, auch wenn die Methoden selbst erst zu einem späteren Zeitpunkt konkret implementiert werden. Die Namen der Methoden und deren Parameter sind dann trotzdem in den Unterklassen schon bekannt. Noch eine Aufgabe für abstrakte Klassen besteht darin, zwei oder mehrere Klassen zusammenzufassen, die zwar einige Gemeinsamkeiten besitzen, jedoch nicht auseinander herzuleiten sind und wo keine gemeinsame »normale« Superklasse vorhanden ist. Das soll bedeuten, es ist nicht offensichtlich, welche die Superklasse der anderen ist. In einer abstrakten Oberklasse können die Gemeinsamkeiten zusammengefaßt werden. Die Bezeichnung »Unvollständig« legt noch eine Verwendung nahe: es können dort alle Eigenschaften untergebracht werden, die für sich alleine noch keine vollständige Funktionalität gewährleisten, jedoch bereits ein Grundgerüst festlegen, das dann relativ leicht in einer Subklasse vervollständigt werden kann.

Testen wir ein Beispiel für eine abstrakte Klasse und deren Vervollständigung in einer Subklasse (AbstractTest.java).

abstract class MeineAbstrakteKlasse {
  abstract void test();
  int test2()  {
    System.out.println("abc");
    return(0);
  }
}
class AbstractTest extends MeineAbstrakteKlasse {
 void test() {
    System.out.println("Das war mal abstrakt");
    test2();
 }
  public static void main(String args[]) {
   AbstractTest x = new AbstractTest();
   x.test();
   x.test2();
  }
}

Bei dem Beispiel greifen wir einigen Techniken vor, die erst in der Folge konkret besprochen werden. Sie sind aber für eine Demonstration notwendig.

1. Die Methode test() in der abstrakten Klasse besteht nur aus der Methodendeklaration, hat also keinen Methoden-Körper. Eine solche Methode muss - wie auch die Klasse - den Modifier abstract vorangestellt bekommen.
2. Die Methode test2() in der abstrakten Klasse ist vollständig. Das ist wie gesagt kein Widerspruch zu Abstraktheit der Klasse.
3. In der Subklasse wird die Methode test() überschrieben, d.h. mit einem Methodenkörper versehen.
4. In der main()-Methode erfolgt der Zugriff auf die Methoden über das aus der Klasse erzeugte Objekt.

Wenn Sie das Programm ausführen, erhalten Sie folgende Ausgabe:

Das war mal abstrakt
abc
abc

Testen Sie die Wirkung von einer abstrakten Klasse als Superklasse, indem Sie die Methode test() in der Subklasse nicht redefinieren und auch nicht anwenden. Dazu können Sie einfach die Zeilen

 void test() {
    System.out.println("Das war mal abstrakt");
    test2();
 }
und 
   x.test();

auskommentieren. Benennen Sie außerdem die Klassen am besten um in AbstractTest2 und MeineAbstrakteKlasse2. Speichern Sie das Beispiel unter AbstractTest2.java:

abstract class MeineAbstrakteKlasse2 {
  abstract void test();
  int test2()  {
    System.out.println("abc");
    return(0);
  }
}
class AbstractTest2 extends MeineAbstrakteKlasse2 {
/*
 void test() {
    System.out.println("Das war mal abstrakt");
    test2();
 }
*/
  public static void main(String args[])  {
   AbstractTest2 x = new AbstractTest2();
//   x.test();
   x.test2();
  }
}

Wenn Sie dann versuchen, das Beispiel zu kompilieren, erhalten Sie folgende Fehlermeldung:

AbstractTest2.java:10: AbstractTest2 should be declared abstract; it does
not define test() in MeineAbstrakteKlasse2
class AbstractTest2 extends MeineAbstrakteKlasse2
^
1 error

Die Interpretation dieser Meldung ist folgende: Die Subklasse einer abstrakten Klasse muss sämtliche (!) dort abstract definierten Methoden vervollständigen. Andernfalls muss die Subklasse selbst wieder als abstrakt deklariert werden. In der abstrakten Superklasse bereits vollständig definierte Methoden können dagegen in der Subklasse unmittelbar verwendet werden.

An dieser Stelle nochmals der Hinweis, dass die Schlüsselwörter final und abstract nicht zusammen in einer Klasse verwendet werden können. Das ist aber offensichtlich, denn eine finale Klasse kann nicht vererbt werden, eine abstrakte hingegen muss, um davon einen Nutzen zu haben.

6.6.4 Der Klassenname

Jede Klasse benötigt einen Namen. Damit ist fast schon das Wichtigste gesagt, was für den Klassennamen von Bedeutung ist. Es gibt jedoch noch einige wenige Regeln, die die Vergabe von Klassennamen beschränken. Wir kennen die Regeln schon von den Token im Allgemeinen.

Zwingende Namenskonventionen für Klassen

1. Klassennamen dürfen im Prinzip eine unbeschränkte Länge haben (bis auf technische Einschränkungen durch das Computersystem).
2. Genau wie bei allen anderen Bezeichnern in Java sind die einzigen Bedingungen für Klassennamen, dass diese mit einem Buchstaben oder einem der beiden Zeichen _ oder $ beginnen müssen. Es dürfen weiter nur Unicode-Zeichen oberhalb von dem Hexadezimalwert 00C0 (Grundbuchstaben und Zahlen sowie einige andere Sonderzeichen) verwendet werden.
3. Zwei Token gelten nur dann als derselbe Bezeichner, wenn sie dieselbe Länge haben und jedes Zeichen im ersten Token genau mit dem korrespondierenden Zeichen des zweiten Tokens identisch ist, d.h. den vollkommen identischen Unicode-Wert hat. Java unterscheidet deshalb Groß- und Kleinschreibung. Die beiden Klassennamen HelloJava und helloJava sind nicht identisch.
4. Klassennamen dürfen keinem Java-Schlüsselwort gleichen und sie sollten nicht mit Namen von Java-Paketen identisch sein.

Es gibt neben den zwingenden Regeln ein paar unverbindliche, aber gängige Konventionen für Klassennamen.

Übliche Konventionen für Klassennamen

1. Man sollte möglichst sprechende Klassennamen verwenden.
2. Die Identifier von Klassen sollten mit einem Großbuchstaben beginnen und anschließend klein geschrieben werden. Wenn sich ein Bezeichner aus mehreren Worten zusammensetzt, dürfen diese nicht getrennt werden. Die jeweiligen Anfangsbuchstaben werden jedoch innerhalb des Gesamtbezeichners jeweils groß geschrieben.

6.6.5 Superklassen und das Schlüsselwort extends

Wir wollen in Java nicht jedes Mal das Rad neu erfinden, sondern bereits bestehende Funktionalitäten immer wieder nutzen und nur für unsere Zwecke anpassen. Vererbung und Erweiterung einer Superklasse sind die OO-Zauberwörter.

Und wie geht das? Ganz einfach. Mit dem Schlüsselwort extends. Sie spezifizieren damit die Klasse, auf die Ihre neue Klasse aufbaut (die Superklasse). Durch die Erweiterung einer Superklasse machen Sie aus Ihrer Klasse eine neue Kopie dieser Klasse und ermöglichen gleichzeitig Veränderungen an dieser neuen Kopie. Wenn Sie (was jedoch absolut sinnlos ist) die neue Klasse überhaupt nicht verändern würden und sowohl den Körper leer, als auch den Modifier der Klasse unverändert ließen, dann würden sich die Klassen identisch verhalten.

Ziehen wir wieder einmal ein Beispiel heran, wo wir eine Klasse als Ableitung einer Superklasse schon verwendet haben .

import java.awt.Graphics; 
public class HelloJavaApplet extends java.applet.Applet {
    public void paint(Graphics g) {
  g.drawString("Hello Java!", 5, 25);
 }  }

Das war unser erstes Applet. Es ist als Subklasse der Klasse java.applet.Applet definiert. Wir haben sie einfach ein bißchen erweitert.

Einige Anmerkungen zur Vererbung in Java

Wenn Sie sich erinnern, hatten wir schon angeführt, dass in Java jede Klasse letztendlich eine Ableitung einer einzigen obersten Klasse - der Klasse java.lang.Object - ist. Um das System konsitent zu halten, werden so genannte Metaklassen definiert (siehe dazu Seite 140). Wenn Sie also Ihre Klasse nicht als Erweiterung irgendeiner anderen Klasse deklarieren, so wird grundsätzlich angenommen, dass sie eine Erweiterung der Klasse java.lang.Object darstellt.

Die zweite Anmerkung soll noch einmal betonen, dass es in Java keine Mehrfachvererbung gibt. Deshalb können Java-Klassen, anders als in C++, immer nur eine Superklasse haben.

6.6.6 Designregeln für Klassen

Wenn Sie Klassen erstellen, gibt es einige kleine Regeln für das Design. Diese sind zwar als unverbindlich anzusehen, jedoch als Richtschnur ganz hilfreich. Insbesondere dann, wenn die Projekte umfangreicher werden3.

1. Daten sollten soweit wie möglich privat deklariert werden (dazu kommen wir gleich). Es entspricht dem objektorientierten Konzept, nur die Informationen nach außen zu geben, die für eine konkrete Funktionalität notwendig und sinnvoll sind. Sie unterbinden durch die Geheimhaltung von Daten einer Klasse zugleich, dass diese in Anwendungen dieser Klasse verändert werden. Damit lassen sich Veränderungen innerhalb der Klasse leichter realisieren, weil die Effekte auf Anwendungen der Klasse minimiert sind. Ergänzend lassen sich Bugs leichter entdecken.
2. Es gibt in Java Variablentypen, die nicht zwingend initialisiert werden müssen. Tun Sie es dennoch.
3. Teilen Sie eine Klasse auf, wenn Sie viele Variablen deklarieren wollen. Erstellen Sie eine Klasse mit den Variablen und binden Sie diese dann in die eigentliche Klasse ein.
4. Vergeben Sie möglichst sprechende Namen für Ihre Klassen. Dies gilt auch übertragen für Methoden und Felder, aber gerade da hat sich ein ziemlicher Abkürzungswahn durchgesetzt. Für Klassen sollten Sie aber auf jeden Fall bei sprechenden Namen bleiben.

6.6.7 Innere und anonyme Klassen bzw. Schnittstellen

Eine innere Klasse ist innerhalb einer anderen Klasse definiert. Sonst gibt es eigentlich keinen Unterschied zu »normalen« Klassen. Klassen und Schnittstellen können damit innerhalb anderer Klassen eingebettet werden. Solche inneren Klassen können ausschließlich die Klassen unterstützen, in die sie integriert sind. Die Verwendung von inneren Klassen ist seit Java 1.1 möglich. Es gibt dafür im Wesentlichen die folgenden Gründe:

1. Ein Objekt einer inneren Klasse kann auf die Implementation des Objekts zugreifen, das es kreiert hat. Auch auf Daten, die dort als privat deklariert wurden!
2. Innere Klassen können vor den anderen Klassen des gleichen Pakets versteckt werden.
3. Die Verwendung von inneren Klassen ist bei der Erstellung von ereignisbezogenen Anwendungen sehr bequem.

Das Gerüst einer solchen Struktur sieht ungefähr so aus:

class SchreibeDatei {
  public class UeberpruefeLaufwerk {
      // Rest von UeberpruefeLaufwerk
  }
  // Rest von SchreibeDatei
}

Der Name einer inneren Klasse (oder genauer - der Zugriff darauf) wird durch die umschließende Klasse bestimmt. Es gilt die Punktnotation. In unserem skizzierten Beispiel erfolgt der Zugriff auf die innere Klasse über

SchreibeDatei.UeberpruefeLaufwerk .

»Anonymous Classes« steht für eine Abart der inneren Klassen. Es handelt sich um eine Kurzform von inneren Klassen. Sie bekommen keinen Namen, sondern nur eine Implementation mit new.

Der Compiler generiert bei anonymen Klassen eine namenlose (anonymous) Klasse, die wie spezifiziert eine bestehende Klasse dann überschreibt. Das Ende einer anonymen Klassen wird durch das Ende des mit new eingeleiteten Ausdrucks festgelegt. Skizziert sieht das so aus:

private Vector history = new Vector();
 public void watch(Ueberwache o) {
  o.addObserver(new Observer() {
  public void update(Ueberwache o, Object arg) {
  history.addElement(arg);
  }
};
}

Wie Anmerkung drei bei inneren Klassen schon andeutet, werden innere bzw. anonyme Klassen gerne im Zusammenhang mit der Behandlung von Ereignissen verwendet. Wir wollen deshalb auf die Erstellung von interaktiven grafischen Oberflächen vorgreifen und eine solche innere (oder genau genommen anonyme) Klasse in Aktion sehen.

import java.awt.*;
import java.awt.event.*;
class Anonym extends Frame {
  public Anonym() {
   addWindowListener(new WindowAdapter() {
   public void windowClosing(WindowEvent e)  {
        dispose();
        System.exit(0);
      }
   });  }
  public static void main(String args[]) {
    Anonym mainFrame = new Anonym();
    mainFrame.setSize(400, 400);
    mainFrame.setTitle("Test");
    mainFrame.setVisible(true);
  }
}

Das Beispiel erzeugt ein Fenster. Die Anwendung der anonymen Klasse besteht darin, dass in der Methode addWindowListener() ein Adapter erzeugt wird, worin die Reaktion auf den Klick auf das Schließsymbol des Fensters realisiert wird.

Abbildung 6.15:  Die Anwendung einer anonymen Klasse - hier wird der Schließbutton mit Funktionalität versehen

Innere Klassen und Interfaces können dieselben Zugriffsmodifizierer verwenden wie die anderen Mitglieder einer Klasse.

6.6.8 Adapterklassen

Eine Adapterklasse ist eine Klasse, die ein Interface mit leeren Methodenrümpfen implementiert, die bei Bedarf (also immer, wenn eine Methode nicht als void deklariert ist) nur einen Standardwert zurückgeben. Adapterklassen werden deshalb verwendet, damit eine Klasse, die eine Schnittstelle nur teilweise nutzen möchte (also etwa nur eine Methode aus einer ganzen Liste von dort deklarierten Methoden), nicht alle Methoden der Schnittstelle überschreiben muss. Das leistet ja wie gesagt die Adapterklasse, die dann als Superklasse der eigentlichen Klasse fungiert. Nachteil: Damit ist der Weg der Vererbung aufgebraucht.

Adapterklassen findet man hauptsächlich bei der Eventprogrammierung.

6.6.9 Konstruktoren und der new-Operator

Der Begriff Konstruktoren (Constructors) ist schon einige Male gefallen. Es handelt sich um sehr spezielle Methoden (deshalb wird auch oft von Konstruktormethoden gesprochen), die bei der Erstellung einer Instanz genutzt werden, um ein Objekt zu initialisieren und bestimmte Eigenschaften der Instanz festzulegen, Aufgaben auszuführen und Speicher für die Instanz zu allokieren.

Methoden im Allgemeinen werden wir im Detail gleich im Anschluss behandeln.

Konstruktoren müssen immer denselben Namen wie die Klasse selbst haben! Weiterhin dürfen Konstruktoren (obwohl sie Methoden sind) keine Rückgabeparameter haben (nicht einmal die Deklaration als void ist erlaubt), weil sie ausschließlich dazu benutzt werden, die Instanz der Klasse zurückzugeben.

Meist sind Konstruktoren public. Sie dürfen nicht als native, abstract, static, synchronized oder final deklariert werden.

Aber wie werden nun Konstruktoren konkret eingesetzt? Wenn eine Instanz einer Klasse erstellt wird, ist es nötig, dass ein Speicherbereich für verschiedene Informationen reserviert wird. Wenn Sie nun eine Variable für eine Instanz am Anfang einer Klasse deklarieren, dann sagen Sie dem Compiler damit lediglich, dass eine Variable eines bestimmten Namens in dieser Klasse verwendet wird. Daher ist es notwendig, dass Sie der Variable zusätzlich unter Verwendung des Operators new eine konkretes Objekt zuweisen.

Die Deklaration der Variable ist eine gewöhnliche Variablendeklaration, nur ist der Typ der Variablen vom Typ des Objekts, das darin gespeichert werden soll. Die Syntax sieht folgendermaßen aus:

<KlassenName> <instanzderKlasse>;

Die Zuweisung der konkreten Klasseninstanz mit dem Konstruktor erfolgt dann so:

<instanzderKlasse> = 
  new <KlassenName>(<optionale_parameter>);

Die beiden Schritte werden oft zusammen erledigt. Das sieht dann folgendermaßen aus:

<KlassenName> <instanzderKlasse> = 
  new <KlassenName>(<optionale_parameter>);

instanzderKlasse ist die Variable, welche die Instanz der Klasse KlassenName aufnimmt. Der Name der Klasse taucht zweimal auf. Auf der linken Seite ist es in der Tat die Klasse selbst, aber rechts der Name der Konstruktormethode. Achten Sie unbedingt auf die Klammern. Diese müssen auf jeden Fall vorhanden sein (ein Konstruktor ist wie gesagt eine Methode!), während die Parameter darin optional sind.

Allgemein wird mit einem Konstruktor der Compiler angewiesen, Speicherplatz für eine Instanz dieser Klasse zu reservieren und den Namen dieser Instanz der entsprechenden Speicheradresse zuzuordnen.

In jeder Klasse gibt es eine Konstruktormethode ohne Parameter, auch wenn sie nicht explizit deklariert wird. Dann wird sie von einer Superklasse vererbt. D.h., wenn in einer Klasse keine Konstruktormethode definiert wurde, dann ruft Java eine Default-Konstruktormethode ohne Parameter auf.

Es kann in einer Klasse mehrere Konstruktormethoden geben. Konstruktoren verhalten sich wie andere Methoden und können überladen werden. Damit ist es möglich, ein Objekt auf vielfältige Art zu erstellen, indem mehrere Konstruktoren mit unterschiedlichen Parametern erstellt werden. Die Konstruktoren müssen sich wie andere Methoden innerhalb einer Klasse irgendwo in der Methodenunterschrift unterscheiden, damit sie überladen werden können.

Die Methodenunterschrift bei Konstruktoren lässt dazu nicht viel Freiheiten, denn der Name des Konstruktors (ein Teil der Methodenunterschrift) ist zwingend mit dem Namen der Klasse identisch. Es kann also nur Unterschiede in der Parameterliste geben.

Die Parameterliste dient zum Unterscheiden von verschiedenen Konstruktoren einer Klasse. Mehr dazu finden Sie bei den Methoden, Seite 285.

Das nachfolgende Programmbeispiel soll die Verwendung des new-Operators mit drei verschiedenen Konstruktoren demonstrieren. Dabei greifen wir auf eine Standardklasse von Java zurück (Date), die wir in der ersten Zeile importieren. Diese beinhaltet mehrere Konstruktormethoden.

import java.util.Date;
class Konstrukt1 {
 public static void main (String args[]) {
  Date datum1, datum2, datum3;
  datum1 = new Date();
  System.out.println("Datum 1 ist " + datum1);
  datum2 = new Date(97, 8, 8);
  System.out.println("Datum 2 ist: " + datum2);
  datum3 = new Date("April, 17, 1963, 3:24, PM");
  System.out.println("Datum 3 ist " + datum3);
 }  }

Den drei Variablen datum1, datum2 und datum3 werden jeweils andere Konstruktoren der Klasse Date() mit dem new-Operator zugewiesen (über die unterschiedlichen Parameter). Sie enthalten also drei verschiedene Instanzen von der Klasse Date, die sich dann auch in unterschiedlichen Formaten in der Ausgabe äußern.

Beim Kompilieren des Beispiels werden Sie wahrscheinlich eine Warnung erhalten, dass Elemente als deprecated gelten. Das ist für uns hier kein Problem. Die Warnung bedeutet nur, dass es für diese Elemente neuere Varianten gibt, die statt der veralteten Elemente verwendet werden sollten. Die alten »Knaben« funktionieren aber immer noch.

Wir haben jetzt gesehen, wie die Konstruktoren einer Klasse zum Erstellen eines Objekts aufgerufen werden, aber noch nicht, wie sie innerhalb der Klasse erstellt werden. Erstellen wir dazu am besten eine eigene Klasse mit mehreren Konstruktoren zum Überladen und erzeugen dann davon mehrere Objekte. (Hinweis: Speichern Sie das Beispiel unter Konstrukt2.java ab.)

class MeineKonst {
  int a = 1;
  int b;
  static int c;
  MeineKonst()  {
  }
  public MeineKonst(int b)  {
    this.b = b;
  }
  public MeineKonst(int c, int d)  {
    b = c * d;
  }
}
public class Konstrukt2 {
 int aussen=5;
 public static void main(String args[]) {
  Konstrukt2 u = new Konstrukt2();
  System.out.println(
  "Die Variable aussen in der Klasse Konstrukt2: " 
  + u.aussen);
  MeineKonst x = new MeineKonst();
  System.out.println(
  "Variable a in Klasse MeineKonst mit Objekt x " 
  + "(leerer Konstruktor): " + x.a);
  x.b = 4;
  System.out.println(
  "Variable b in Klasse MeineKonst mit Objekt x " 
  + "(leerer Konstruktor): " + x.b);
  MeineKonst y = new MeineKonst(42);
  System.out.println(
  "Variable b in Klasse MeineKonst mit Objekt y " 
  + "(Konstruktor mit einem int): " + y.b);
  y.c = 4;
  System.out.println(
  "Klassenvariable c in Klasse MeineKonst " 
  + "(Konstruktor mit einem int): " + y.c);
  MeineKonst z = new MeineKonst(5,6);
  System.out.println(
  "Variable a in Klasse MeineKonst mit Objekt z " 
  + "(Konstruktor mit zwei int): " + z.a);
  System.out.println(
  "Variable b in Klasse MeineKonst mit Objekt z " 
  + "(Konstruktor mit zwei int): " + z.b);
  }
}

Die erste Klasse beinhaltet drei Konstruktoren, wo jeweils Variablen deklariert werden. In der Klasse Konstrukt2 werden damit drei Instanzen erzeugt und die verschiedenen Variablen über die erzeugten Objekte ausgegeben. Die Ausgabe sieht folgendermaßen aus:

Die Variable aussen in der Klasse Konstrukt2: 5
Variable a in Klasse MeineKonst mit Objekt x (leerer Konstruktor): 1
Variable b in Klasse MeineKonst mit Objekt x (leerer Konstruktor): 4
Variable b in Klasse MeineKonst mit Objekt y (Konstruktor mit einem int): 42
Klassenvariable c in Klasse MeineKonst (Konstruktor mit einem int): 4
Variable a in Klasse MeineKonst mit Objekt z (Konstruktor mit zwei int): 1
Variable b in Klasse MeineKonst mit Objekt z (Konstruktor mit zwei int): 30

Abbildung 6.16:  Verschiedene Konstruktoren im Einsatz

In diesem Beispiel sind einige interessante Details zu beachten.

Sie finden hier den Einsatz des Schlüsselworts this. Dieses erlaubt es, aus einer Methode auf das Objekt zuzugreifen, in dem die Methode enthalten ist.

Auf this gehen wir auf Seite 274 näher ein.

Die verschiedenen Konstruktoren in der Klasse MeineKonst werden überladen. Die Identifikation des richtigen Konstruktors erfolgt über den Typ und die Anzahl der Parameter, die beim Aufruf verwendet werden. Wenn ein Konstruktor nicht in der aktuellen Klasse gefunden wird, wird die Suche in der Superklasse fortgesetzt.

Wenn Sie selbst einen (beliebigen) Konstruktor in einer Klasse erzeugen, haben Sie keinen Zugriff mehr auf den Default-Konstruktor (der sonst aus einer Superklasse vererbt wird). Diesen müssen Sie entweder reimplementieren (ein leerer Methodenkörper genügt bereits) oder Sie verwenden den Zugriff über das Schlüsselwort super, um den Konstruktor der Superklasse anzusprechen. Das wollen wir anhand von Beispielen demonstrieren.

public class Konstrukt3 {
  int a = 5;
  public static void main(String args[]) {
    Konstrukt3 u = new Konstrukt3();
    System.out.println(
    "Die Instanzvariable a ueber das Objekt u: " 
  + u.a);
  }
}

Das Beispiel Konstrukt3 verwendet den - in der Klasse nicht explizit implementierten - Default-Konstruktor. Er wird über die Vererbung aus der Superklasse (in diesem Beispiel die Superklasse aller Java-Objekte - Object) bereitgestellt.

Wenn wir das Beispiel geringfügig verändern, hat man keinen Zugriff mehr.

public class Konstrukt4 {
  Konstrukt4(int a)  {
  }
  int a = 5;
  public static void main(String args[]) {
    Konstrukt4 u = new Konstrukt4();
    System.out.println(
    "Die Instanzvariable a ueber das Objekt u: " 
  + u.a);
 }  }

Das Beispiel Konstrukt4 führt einen neuen Konstruktor ein, der einen Parameter verwendet. Er wird aber in dem Beispiel nicht verwendet, sondern es wird versucht, in der main()-Methode den parameterlosen Default-Konstruktor zu verwenden. Wenn Sie das Beispiel aber kompilieren, erhalten Sie folgende Fehlermeldung:

Konstrukt4.java:9: cannot resolve symbol
symbol  : constructor Konstrukt4  ()
location: class Konstrukt4
    Konstrukt4 u = new Konstrukt4();
                   ^
1 error

Obwohl der Default-Konstruktor nicht explizit überschrieben wurde, steht er nicht mehr zur Verfügung.

Zum Stichwort »Überschreiben« siehe Seite 285

Das Problem bleibt auch bestehen, wenn in einer Superklasse ein expliziter Konstruktor definiert wurde. Auch dann kann man in der Subklasse nicht mehr auf den Default-Konstruktor zugreifen.

/* Nicht lauffähig - zur Demonstration
   eines Fehlers */
class SKlasse {
 SKlasse(int a, int b) {
 }  }
public class Konstrukt5 extends SKlasse {
  int a = 5;
  public static void main(String args[]) {
    Konstrukt5 u = new Konstrukt5();
    System.out.println(
   "Die Instanzvariable a ueber das Objekt u: " 
  + u.a);
  }  }

Konstruktoren kann man zwar - wie wir gesehen haben - überladen, aber Überschreiben geht rein technisch nicht so, wie es bei gewöhnlichen Methoden in Java möglich ist. Das liegt daran, dass Konstruktoren immer denselben Namen wie die Klasse haben müssen und sich die Namen von Superklasse und Subklasse unterscheiden müssen. Aber es gilt, dass auch ein Konstruktor mit veränderter Methodenunterschrift den Konstruktor der Superklasse verdeckt. Den verdeckten Konstruktor müssen Sie entweder redefinieren oder darauf über super zugreifen, wie im nachfolgenden Beispiel:

public class Konstrukt6 { 
  int a = 5;
  float b;
  Konstrukt6()  {
  }
  Konstrukt6(int b)  {
   this.a=b;
  }
  Konstrukt6(float c)  {
   super();
   this.b=c;
  }
  public static void main(String args[])  {
    Konstrukt6 u = new Konstrukt6();
    System.out.println(
  "Die Instanzvariable a ueber das Objekt u: " 
  + u.a);
    Konstrukt6 v = new Konstrukt6(6);
    System.out.println(
  "Die Instanzvariable a ueber das Objekt v: " 
  + v.a);
    Konstrukt6 w = new Konstrukt6(6);
    System.out.println(
  "Die Instanzvariable a ueber das Objekt w: " 
  + w.a);
    Konstrukt6 x = new Konstrukt6(1.2f);
    System.out.println(
  "Die Instanzvariable b ueber das Objekt x: " 
  + x.b);
  }
}

Die Ausgabe sieht dann wie folgt aus:

Die Instanzvariable a ueber das Objekt u: 5

Die Instanzvariable a ueber das Objekt v: 6

Die Instanzvariable a ueber das Objekt w: 6

Die Instanzvariable b ueber das Objekt x: 1.2

6.6.10 Das Speichermanagement

Java verfügt über ein ausgefeiltes Speichermanagement. Immer wenn mit dem new-Operator und einem Konstruktor eine neue Instanz einer Klasse erstellt wird, belegt das Laufzeitsystem von Java einen Teil des Speichers, in dem die zu der Klasse bzw. ihrer Instanz gehörenden Informationen abgelegt werden. Sie müssen zwar damit für jedes neue Objekt eine Zuweisung von Speicherplatz vornehmen, jedoch nicht mehr die benötigte Größe des Speicherplatzes spezifizieren, sondern nur noch den Namen des benötigten Objekts. Das bedeutet ebenfalls, dass der new-Anweisung bereits der richtige Datentyp zugewiesen wurde, wenn sie eine Referenz auf einen zugewiesenen Speicherbereich zurückgibt, weshalb sie keine Datentypkonvertierung wie in C benötigt.

Wie soll aber dieser Speicher wieder freigegeben werden, wenn das Objekt nicht länger benötigt wird? Bei Speichermanagement fällt vielen C-Programmierern garantiert der Begriff Destruktor als potenzielles Gegenstück zum Konstruktor ein. Wir haben ja gesehen, dass Java beim Erzeugen einer Instanz einen Konstruktor aufruft und da ist es naheliegend, dass zum Beseitigen des Objekts aus dem Speicher ein Destruktor aufgerufen werden muss. Wir haben es uns jedoch an verschiedenen Stellen schon klar gemacht, dass dies nicht der Fall ist. Java gibt Speicherplatz immer automatisch mit einem Papierkorb frei. Es gibt in diesem Sinn keinen Destruktor in Java.

Mit der Benutzung des Operators new wird von Ihnen zum einen die explizite Zuweisung von Speicherplatz für jedes neue Objekt verlangt, zum anderen wird aber auch sichergestellt, dass es automatisch zerstört werden kann und der Speicher dann wieder vollkommen freigegeben wird. Dafür ist die Garbage Collection zuständig, mit der Java automatisch Objekte freigibt, wenn es keine Referenzen mehr auf diese Objekte gibt. Die explizite Speicherfreigabe (wie sie etwa in C/C++ mit der Methode free() durchgeführt wird) ist schlicht und einfach überflüssig.

Der Speicher wird also immer automatisch freigegeben, der Papierkorb kann aber auch mit der Methode System.gc() direkt manuell aufgerufen werden. Dies ist jedoch in der Regel weder notwendig, noch zu empfehlen. Falls Sie sich um die Speicherbereinigung und vor allem den ordnungsgemäßen Abschluss von Prozessen selbst kümmern wollen, steht die Methode finalize() zur Verfügung.

Der Finalisierer - die Methode finalize()

Es gibt Situationen, in denen es erforderlich ist, dass ein Objekt vor seiner Beseitigung noch gezielte Abschlussaktionen ausführen sollte, beispielsweise das Beenden von Leseoperationen aus Dateien oder das Schließen von offenen Netzwerkverbindungen. Man nennt diesen Beendigungsvorgang Finalisierung. Wenn man unter Java überhaupt von einem Gegenstück zu den Konstruktoren reden kann, dann ist dies die finalize()>Methode:

protected void finalize() throws Throwable

Die finalize()-Methode gehört zur Klasse java.lang.Object und ist somit in allen Klassen vorhanden. Sie ist der Grundeinstellung nach leer und wird normalerweise während der automatischen Speicherbereinigung vom Runtime-System in Java aufgerufen. Sie hat die Aufgabe, laufende Prozesse ordnungsgemäß zu beenden, bevor ein Objekt zerstört wird.

Eine recht ähnliche Struktur (die sogar ähnlich geschrieben wird), werden wir noch bei den Ausnahmen sehen - den finally-Block.

Wenn man sicherstellen möchte, dass laufende Prozesse ordnungsgemäß beendet werden, sollte die finalize()-Methode zusätzlich direkt im Code untergebracht werden. Dies sieht dann ungefähr so aus:

void finalize() {
 ... irgendwelche Abschlussarbeiten ...
}

Wenn dann die Instanz dieser Klasse nicht mehr benutzt und zerstört werden soll, wird die Methode automatisch aufgerufen, um alle Prozesse wie benötigt zu schließen. Man kann diese Methode gleichfalls mit finalize() direkt aufrufen. Beachten Sie, dass die Methode finalize() in java. lang.Object als protected deklariert ist und daher auch geschützt bleiben muss und maximal weiter eingeschränkt werden kann. Überdies ist der genaue Zeitpunkt, wann die Methode finalize() aufgerufen wird, nicht genau zu bestimmen, weil der Prozess der Garbage Collection nicht genau vorhersagbar ist. Der Garbage Collector läuft zwar als permanenter Hintergrundthread, jedoch nur mit niedriger Priorität. Daher ist die finalize()-Methode eigentlich nur zum Abfangen von potenziellen Fehlersituationen zu gebrauchen. Standardabschlussarbeiten sollten besser an anderen Stellen im Code durchgeführt werden.

Bei der Zerstörung von Applets sollten Sie nicht die finalize()-Methode, sondern die destroy()-Methode verwenden. Die allgemeinere finalize()-Methode ist hier nicht zu verwenden, da diese im Gegensatz zur destroy()-Methode nicht immer beim Beenden von einem Browser oder dem Neuladen eines Applets automatisch ausgeführt wird.

Ein chronologischer Ablauf einer Speicherbereinigung in Java sieht etwa so aus:

1. Zunächst prüft der Garbage Collector, ob es noch Verweise auf ein zu löschendes Objekt gibt. Wenn es keine Verweise mehr gibt, hängt das weitere Vorgehen davon ab, ob die zugehörige Klasse einen Finalisierer besitzt. Wenn kein Finalisierer vorhanden ist, wird das Objekt direkt entfernt und der Speicher sofort freigegeben.
2. Falls das Objekt dagegen über einen Finalisierer verfügt, wird er von dem Garbage Collector aufgerufen.
3. Nachdem die Methode finalize() aufgerufen wurde, wird erneut geprüft, ob es keine Verweise auf die Instanz mehr gibt. Diese erneute Überprüfung ist notwendig, da prinzipiell im Finalisierer wieder ein Verweis auf die Instanz erzeugt werden kann. Es wird der Speicher erst dann freigegeben, wenn die Prüfung keinen neuen Verweis gefunden hat.

Ein paar Anmerkungen zur finalize()-Methode.

1. Es besteht keine Garantie dafür, dass finalize() ausgeführt wird. Es kann nämlich passieren, dass der Garbage Collector während der Laufzeit des Interpreters gar nicht aufgerufen wird.
2. Der Finalisierer einer Instanz wird höchstens einmal aufgerufen.
3. Falls bei der Ausführung von finalize() eine Exception erzeugt wird, die nicht in der Methode selbst abgefangen wird, dann wird die Ausführung wie sonst auch abgebrochen. Im Gegensatz zu anderen Methoden werden Exceptions im Finalisierer ignoriert und nicht an das Programm weitergeleitet.
4. Der Aufrufzeitpunkt von finalize() ist nicht vorhersehbar.
5. Es können keine Aussagen darüber gemacht werden, in welcher Reihenfolge Objekte entfernt werden.

6.6.11 Das Schlüsselwort this

Bei der Arbeit mit Konstruktoren hatten wir gerade das Schlüsselwort this verwendet. Um was geht es dabei? Über die Punktnotation kann man auf andere Klassen bzw. Objekte zugreifen. Wie aber kann eine Klasse bzw. ein Objekt auf sich selbst zugreifen? Dafür stellt Java dieses Schlüsselwort this zur Verfügung, das es ja ebenso schon in C/C++ gab. Es gibt im Allgemeinen zwei Situationen, die den Gebrauch dieser Variablen rechtfertigen:

1. Es gibt in einer Klasse zwei Variablen mit gleichem Namen - eine gehört zu der Klasse, die andere zu einer spezifischen Methode in der Klasse. Die Benutzung der Syntax this.<VariablenName> ermöglicht es, auf diejenige Variable zuzugreifen, die zu der Klasse gehört. Das genau haben wir in den Konstruktor-Beispielen gemacht.
2. Eine Klasse muss sich selbst als Parameter für eine Methode weiterreichen bzw. der Rückgabetyp einer Methode ist eine Objektinstanz der Klasse. Ein Beispiel sind Applets, die auf Methoden wie beispielsweise showStatus() zugreifen können.

Das Schlüsselwort this kann aber allgemeiner verstanden werden. Es kann an jeder Stelle verwendet werden, an der das Objekt erscheinen kann, dessen Methode gerade aktiv ist. Etwa als Argument einer Methode, als Ausgabewert oder in Form der Punktnotation zum Zugriff auf eine Instanzvariable. In vielen Anweisungen steckt das Schlüsselwort implizit drin, denn man kann oft darauf verzichten, wenn die Situation eindeutig ist.

Die nachfolgenden Beispiele zeigen die Verwendung von this in verschiedenen Varianten. Das Beispiel This1 zeigt, wie Sie innerhalb einer Methode auf eine Variable der Klasse zugreifen können. Beachten Sie, dass die Instanzvariable in der Klasse denselben Namen hat wie die als Parameter übergebene Variable. Ohne das vorangestellte this wäre die Zuweisung in der Methode meinemethode(int x) eine Selbstzuweisung. Die Zeile

this.x = x;

bedeutet, dass der Instanzvariable x der Wert zugewiesen wird, der als Parameter der Methode übergeben wird. Dass der Name des Parameters identisch ist, stört nicht. Java stellt dafür die Namensräume bereit, die eine Eindeutigkeit gewährleisten.

class This1 {
int x = 0;
void meinemethode(int x) {
// Zuweisung der Variable in der Klasse mit dem 
// Wert des Parameters
 this.x = x; 
}
void printwas() {
 System.out.println(
  "Wert der Variable in der Klasse: " + x );
 }
public static void main (String args[]) {
// Erzeugen einer Instanz
 This1 a = new This1();
 a.meinemethode(42);
// Ausgabe der Variable der Klasse
 a.printwas();
 }  }

In der Methode meinemethode(int x) setzen Sie den Wert der Variable in der Klasse auf 42. Diese geben Sie dann in der Methode printwas() aus. Die Ausgabe wird

Wert der Variable in der Klasse: 42

sein.

Erstellen wir noch ein (relativ) einfaches Beispiel, das this verwendet. Allerdings setzen wir dort nicht den Wert der Variablen in der Klasse - wir fragen ihn nur ab. Dabei werden wir zwei Variablen gleichen Namens benutzen. Eine ist in der Klasse als Instanzvariable definiert, die andere als lokale Variable in der Methode, wo wir beide Werte ausgeben wollen.

Das Schlüsselwort this kann wie das Schlüsselwort super nur im Körper einer nicht- statischen Methode verwendet werden.
class This2 {
int x = 42;
void meinemethode() {
 int x = 24;
 System.out.println(
  "Wert der Variable in der Klasse: " + this.x );
 System.out.println(
  "Wert der Variable in der Methode: " + x );
}
public static void main (String args[])  {
// Erzeugen einer Instanz
 This2 a = new This2();
 a.meinemethode();
 }  }

Die Ausgabe wird

Wert der Variable in der Klasse: 42
Wert der Variable in der Methode: 24

sein.

Da das Schlüsselwort this so wichtig ist und gerade Einsteigern oft Schwierigkeiten bereitet, behandeln wir noch ein weiteres Beispiel. Hier nutzen wir this wieder, um aus einem Konstruktor auf das erst zur Laufzeit erzeugte Objekt (ein Frame) zugreifen zu können. Auf »diesem« Frame-Objekt fügen wir Schaltflächen hinzu, legen von »diesem« Frame-Objekt das Layout und die Größe fest und zeigen »dieses« Frame-Objekt dann an4.

import java.awt.*;
import javax.swing.*;
public class This3 extends JFrame {
  JButton jButton1 = new JButton();
  FlowLayout flowLayout1 = new FlowLayout();
  JButton jButton2 = new JButton();
  public This3() {
    this.getContentPane().setLayout(flowLayout1);
    this.jButton2.setText("jButton2");
    this.jButton1.setText("jButton1");
    this.getContentPane().add(jButton1, null);
    this.getContentPane().add(jButton2, null);
    this.setSize(300,200);
    this.show();
  }
  public static void main(String[] args) {
    This3 meinFrame1 = new This3();
 }  }

Beachten Sie, dass Sie das Programm nicht mit dem Schließbutton beenden können (es ist kein Eventhandling integriert). In der DOS-Box können Sie es mit STRG+C beenden.

Abbildung 6.17:  Ein Java-Programm mit grafischer Oberfläche, die mit this anzusprechen ist

Da der Frame im Konstruktor eindeutig identifizierbar ist, könnte auch durchgängig auf this verzichtet werden. So funktioniert das Beispiel auch:

import java.awt.*;
import javax.swing.*;
public class This4 extends JFrame {
  JButton jButton1 = new JButton();
  FlowLayout flowLayout1 = new FlowLayout();
  JButton jButton2 = new JButton();
  public This4() {
    getContentPane().setLayout(flowLayout1);
    jButton2.setText("jButton2");
    jButton1.setText("jButton1");
    getContentPane().add(jButton1, null);
    getContentPane().add(jButton2, null);
    setSize(300,200);
    show();
  }
  public static void main(String[] args) {
    This4 meinFrame1 = new This4();
 }  }

6.7 Methoden

Methoden sind das Java-Gegenstück zu Funktionen in Programmiersprachen wie C/C++ oder PASCAL. Sie bilden das Kernstück jeder Klasse und sind für die Handhabung aller Aufgaben zuständig, die von dieser Klasse durchgeführt werden sollen. Zwar ist die eigentliche Implementierung der Methode im Body (Körper) der Methode enthalten. Zusätzlich gibt es wie bei den Klassen die Deklaration, die einen großen Teil von Informationen über die Methode enthält.

Java ist in der Lage, rekursive Aufrufe von Methoden zu verwalten.

6.7.1 Deklaration einer Methode

Die Deklarationen von Methoden sehen generell folgendermaßen aus:

[<modifiers>] <Returnwert> <namedermethode> ([<Parameter>]) 
  [throws] [<ExceptionListe>]

Dabei ist alles in eckigen Klammern optional.

Die konkret verwendete Kombination aus diesen Teilen der Deklaration nennt man die Methodenunterschrift. Diese besteht aus mindestens dem Namen der Methode, dem Rückgabetyp und den Klammern sowie dort eventuell enthaltenen Parametern. Oft wird für denselben Zusammenhang der Begriff Methodensignatur verwendet.

6.7.2 Die Zugriffsspezifizierung

Man kann bei Methoden wie bei Klassen den Zugriff beschränken. Es können zwar immer die Methoden innerhalb einer gegebenen Klasse auf alle anderen Methoden dieser Klasse zugreifen, jedoch für Methoden anderer Objekte können Beschränkungen aufgebaut werden. Beachten Sie dabei bitte, dass jede Klasse nur einen Zugriffsmodifier haben kann. Es macht wenig Sinn, wenn eine Methode als weniger streng deklariert ist als die Klasse, zu der sie gehört. Eine Klassendeklaration sollte also nie mehr einschränken, als die darin enthaltenen Methoden an maximalen Rechten haben sollen.

Wir wollen bei Methoden bzgl. des Zugriffs von Zugriffsspezifier reden und den Begriff Modifier erst bei der Sichtbarkeit von Methoden verwenden. Damit sind wir nicht ganz konsistent zu der Terminologie bei den Klassen, aber es wurde ja bereits dort auf die mögliche Zerlegung des Modifiers in zwei Bestandteile (die Sichtbarkeit und den Benutzungsgrad) hingewiesen. Bei Klassen ist jedoch nicht die Vielfalt wie bei Methoden gegeben und deshalb ist eine solche Zerlegung der Übersichtlichkeit halber nur bei Methoden sinnvoll.

Freundliche Methoden

Der voreingestellte Defaultstatus einer Methode ist immer freundlich und wird immer dann verwendet, wenn Sie keine explizite Zugriffsspezifizierung am Anfang einer Deklaration vornehmen. Dies ist identisch mit Klassen. Die freundliche Grundeinstellung aller Methoden bedeutet, dass diese Methoden sowohl innerhalb der Klasse, als auch innerhalb des zugehörigen Paketes benutzt werden können.

Beispiel:

void speichereNichtgeheimeZahl()

Öffentliche Methoden

Öffentliche Methoden werden mit dem Zugriffsspezifier public deklariert. Dies ist analog dem Verfahren bei Klassen. Dies bedeutet, dass alle Methoden auf public-Methoden zugreifen können, egal in welchen Klassen oder Paketen sie definiert sind. Die Erweiterung gegenüber freundlichen Methoden ist wieder die, dass sie auch von allen Methoden benutzt werden können, die nicht zum eigenen Paket gehören.

Beispiel:

public void speichereNichtgeheimeZahl()

Geschützte Methoden

So genannte geschützte Methoden werden mit dem Zugriffsspezifier protected realisiert. Geschützte Methoden sind im Wesentlichen identisch mit öffentlichen Methoden, mit Ausnahme der Tatsache, dass sie nicht für Objekte außerhalb der Pakets der Klasse verfügbar sind. Allerdings haben Subklassen der Klasse immer noch den vollen Zugriff auf diese Methoden der Superklasse.

Beispiel:

protected void speichereGeheimzahl()

Ein Kompilierfehler kann unter Umständen darauf zurückzuführen sein, dass Sie versuchen, auf eine Methode außerhalb des sichtbaren Anwendungsbereichs einer geschützten Methode zuzugreifen. Die dann entstehende Fehlermeldung ist leider missverständlich, denn Sie weist nicht darauf hin, dass Sie versuchen, auf eine geschützte Methode zuzugreifen. Statt dessen sieht Sie etwa so aus:
No method matching speichereGeheimzahl() found in class java.xyz.jhg. 

Der Grund ist der, dass die geschützten Methoden vor nicht-privilegierten Klassen versteckt werden. Wenn deshalb eine Klasse kompiliert wird, die die Sicherheitsbestimmungen nicht erfüllt, werden Methoden dieser Art vor dem Compiler versteckt. Ähnliche Fehlermeldungen begegnen Ihnen, wenn Sie versuchen, außerhalb Ihrer Zugriffsrechte auf eine private oder freundliche Methode zuzugreifen, oder wenn Sie versuchen, von einer nicht-priviligierten Klasse auf ein Feld zuzugreifen.

Private Methoden

Private Methoden stellen die höchste auf eine Methode anwendbare Sicherheitsstufe dar. Sie werden durch die Benutzung von dem Zugriffsspezifier private eingerichtet. Eine private Methode ist nur für die anderen Methoden in derselben Klasse verfügbar. Sogar eine Subklasse dieser Klasse kann auf eine private Methode nicht zugreifen.

Beispiel:

private void speichereGeheimzahl()

In früheren Java-Versionen war es erlaubt, so genannte privat geschützte Methoden zu deklarieren, indem die Zugriffs-Spezifier private und protected in Kombination verwendet wurden. Methoden mit einer solchen Kennzeichnung sollten sowohl für die Klasse als auch für deren Subklassen verfügbar sein, aber nicht für den Rest des Pakets oder für Klassen außerhalb des Pakets. Die Kombination ist seit dem JDK 1.2 verboten.

6.7.3 Die Methodenmodifier

Methodenmodifier ermöglichen es Ihnen, ähnlich wie Klassenmodifier, bestimmte Eigenschaften einer Methode festzulegen. So lässt sich eine Methode etwa in Instanz- und Klassenmethoden unterscheiden. Aber es gibt noch weitere Modifier.

Klassenmethoden versus Instanzmethoden

Klassenmethoden werden mit dem Modifier static realisiert.

Beispiel:

static void toggleStatus()

Diese Klassenmethoden oder statischen Methoden sind solche Methoden, die in der Klasse agieren und auf die verwandten Klassenvariablen (auf die wir noch bei der Besprechung der Variablen intensiver eingehen) direkt zugreifen können. Ein wichtiger Vertreter von Klassenmethoden ist die main()-Methode, auf der jedes eigenständige Java-Programm aufbaut.

Für die Erklärung muss ein wenig ausgeholt werden. Es muss in Java zwischen einer spezifischen Instanz einer Klasse und der Klasse selbst unbedingt unterschieden werden. Klassenmethoden liegen außerhalb der konkreten Objekte, aber innerhalb der Klassen. Sie werden über das Metaklassenkonzept in die streng objektorientierte Theorie von Java eingebunden. Methoden einer Klasse sind global, betreffen also immer die gesamte Klasse samt sämtlicher Instanzen der Klasse. Klassenmethoden können deshalb an beliebigen Stellen genutzt werden, unabhängig davon, ob eine konkrete Instanz der Klasse existiert oder nicht.

Eine Instanzmethode ist dagegen nur über ein Objekt verwendbar. Zwar können andere Instanzmethoden einer Klasse eine Instanzmethode aufrufen, aber das verlagert das Problem nur weiter. Die konkrete Anwendung erfolgt über ein Objekt.

Die Deklaration von Instanz- und Klassenmethoden unterscheidet sich nicht sonderlich. Es gibt nur einen Faktor, der eine Instanzmethode und eine Klassenmethode unterscheidet. Durch das Platzieren des Modifiers static vor eine Methodendeklaration machen Sie diese Methode zu einer statischen Methode.

Einige Aussagen zu Instanz- und Klassenmethoden sollen die Unterschiede verdeutlichen:

Nicht-statische Methoden können zwar mit statischen Variablen (auf die wir noch zu sprechen kommen) arbeiten, statische Methoden demgegenüber nur mit statischen Variablen und anderen statischen Methoden. Es ist ein klassischer Fehler, wenn Sie versuchen, aus einer statischen Methode eine nicht-statische Variable oder eine nicht-statische Methode zu referenzieren. Sie erhalten dann eine Fehlermeldung der folgenden Art durch den Compiler:

"Can't make a static reference to nonstatic..."

bzw.

...non-static variable x cannot be referenced from a static context

Versuchen Sie einmal, das nachfolgende Beispiel zu kompilieren - Sie werden die Fehlermeldung erhalten.

/* Dieses Beispiel soll einen Fehler produzieren - nicht lauffähig */
class StaticTest {
 int x=42; // falsch
// So ist es richtig
// static int x=42;
 public static void main (String args[]) {
    System.out.println("x ist " + x); 
 }  }

Im auskommentierten Teil sehen Sie, wie die Variable richtig deklariert ist.

Wenn Sie also eine Methode erstellen, die ausschließlich mit statischen Variablen operiert, dann sollten Sie diese Methode am besten auch als statisch deklarieren. Auch wenn sie eine Methode erstellen wollen, die während der »Lebenszeit« der zugehörigen Klasse in einer Schleife läuft - wie zum Beispiel ein Thread, der aus einem Socket liest - und Sie wollen diese Informationen mit allen Instanzen dieser Klasse teilen, dann können Sie das erreichen, indem Sie diese Methode statisch machen. Auch wenn Sie mehrere Instanzen der Klasse erstellen müssen, ist das Resultat, dass Sie nur eine Kopie der Methode zu erstellen brauchen, was eine Menge Speicherplatz spart.

Wir kommen bei der Behandlung von statischen Variablen nochmals auf Klassen- und Instanzmethoden zurück (Seite 298).

Abstrakte Methoden

Eine weitere Kennzeichnung von Methoden ist, dass sie als abstrakte Methoden festgelegt werden. Dies erledigt der Modifier abstract.

Beispiel: abstract void toggleStatus();

Wir kennen den Modifier abstract ja schon von den Klassen. Im Falle von abstrakten Methoden handelt es sich um Methoden, die zwar deklariert sind, aber nicht in die aktuelle Klasse implementiert wurden. Das bedeutet, abstrakte Methoden besitzen keinen Körper. Daher muss eine abstrakte Methode in der Subklasse der aktuellen Klasse überschrieben und implementiert werden, bevor mit der Methode etwas anzufangen ist.

Sie deklarieren eine abstrakte Methode, indem Sie das Schlüsselwort abstract vor den Methodennamen setzen, und den Body (Körper) der Methode durch ein einfaches Semikolon (;) ersetzen. Eine komplette abstrakte Methode könnte demnach folgendermaßen aussehen:

abstract boolean sonstnix();

Ein paar Bemerkungen zu abstrakten Methoden:

1. Weder statische Methoden noch Klassen-Konstruktoren dürfen als abstrakt deklariert werden.
2. Abstrakte Methoden dürfen nie als final deklariert werden, weil dadurch verhindert wird, dass diese Methoden überschrieben werden können. Sie wären nutzlos.
3. Eine Klasse, die eine abstrakte Methode enthält, muss selbst als abstrakt deklariert werden. Sofern eine Subklasse dieser Klasse nicht alle abstrakten Methoden überschreibt, muss auch sie als abstrakt deklariert werden.

Finale Methoden

Eine weitere Spezifizierung von Methoden erfolgt über den Modifier final. Dieser spezifiziert finale Methoden.

Beispiel: final void toggleStatus();

Das Schlüsselwort final vor eine Methodendeklaration verhindert, dass irgendwelche Subklassen der derzeitigen Klasse diese Methode überschreiben. Methoden, die auf keinen Fall geändert werden sollen, sollten deshalb immer als final deklariert werden.

Native Methoden

Native Methoden werden mit dem Modifier native gekennzeichnet.

Beispiel: native void toggleStatus();

Native Methoden sind Methoden, die nicht in Java geschrieben sind, aber dennoch innerhalb von Java verwendet werden sollen. Meist handelt es sich um Methoden in C/C++. Die Syntax zum Verwenden von nativen Methoden ist ähnlich der abstrakter Methoden. Der Modifier native wird vor der Methode deklariert, und der Body (Körper) der Methode wird durch ein Semikolon ersetzt.

Es darf nicht vergessen werden, dass die Deklaration den Compiler über die Eigenschaften der Methode informiert. Deshalb ist es von höchster Wichtigkeit, dass Sie denselben Rückgabewert und dieselbe Parameterliste verwenden, wie sie auch im ursprünglichen Code vorhanden waren.

Synchronisierte Methoden

Im Rahmen von Multithreading ist es möglich, Methoden zu synchronisieren. Dies erfolgt mit dem vorangestellten Modifier synchronized.

Beispiel: synchronized void toggleStatus();

Mehr dazu finden Sie Seite 392.

6.7.4 Die Rückgabewerte von Methoden

Die Rückgabewerte von Methoden sind wichtige Informationen einer Methode, ob sie korrekt abgearbeitet wurde oder was sie genau ausgeführt hat. Rückgabewerte von Java-Methoden können von jedem erlaubten Datentyp sein, nicht nur primitive Datentypen, sondern auch komplexe Objekte (beispielsweise Zeichenketten oder Arrays).

Eine Methode muss immer einen Wert zurückgeben (und zwar genau den Datentyp, der in der Deklaration angegeben wurde), es sei denn, sie ist mit dem Schlüsselwort void deklariert worden. Das Schlüsselwort void bedeutet gerade, dass sie explizit keinen Rückgabewert hat.

Rückgabewerte werden mit der Anweisung return() zurückgegeben. Innerhalb der Klammer steht der gewünschte Rückgabewert. Bei als void deklarierten Methoden kann die return-Anweisung auch ohne Klammern notiert werden. Das ist etwa dann sinnvoll, wenn ein Rücksprung aus einer solchen Methode mit bestimmten Bedingungen gekoppelt wird.

/* Beispielprogramm zur Demonstration der Rückgabewerte mit return*/
import java.util.*;
class Rueckgabe {
 public static int zurueck1()  {
  return(42); 
 }
 public static void zurueck2() {
  do {
  Random a = new Random();
  System.out.println(a.nextFloat()); 
  if(a.nextFloat()>0.5) return; 
  }
  while(true);
 }
 public static String zurueck3() {
  return("Und tschuess"); 
 }
 public static void main (String args[]) {
  int i=zurueck1();
  System.out.println(i);
  zurueck2();
  System.out.println(zurueck3());
 }
}

Die Ausgabe ist der Wert 42, der Rückgabewert der Methode zurueck1(). Der Rückgabewert (der Datentyp int) wird einer lokalen Variablen zugewiesen und diese dann ausgegeben. Dann wird so lange die Schleife in der Methode zurueck2() wiederholt, bis die Bedingung für die Auslösung von return (ohne Parameter) erfüllt ist. Abschließend wird der von der Methode zurueck3() zurückgegebene Wert (ein String) direkt in der Ausgabemethode verwendet.

6.7.5 Der Methodenname

Bezüglich der Methodennamen gelten die gleichen Regeln wie bei allen Token.

Zwingende Namenskonventionen für Methoden

1. Methodennamen dürfen im Prinzip eine unbeschränkte Länge haben (bis auf technische Einschränkungen durch das Computersystem).
2. Genau wie bei allen anderen Bezeichnern in Java sind die einzigen Bedingungen für Methodennamen, dass diese mit einem Buchstaben oder einem der beiden Zeichen _ oder $ beginnen müssen. Es dürfen weiter nur Unicode-Zeichen über dem Hexadezimalwert 00C0 (Grundbuchstaben und Zahlen sowie einige andere Sonderzeichen) verwendet werden.
3. Zwei Token gelten nur dann als derselbe Bezeichner, wenn sie dieselbe Länge haben und jedes Zeichen im ersten Token genau mit dem korrespondierenden Zeichen des zweiten Tokens identisch ist, d.h. den vollkommen identischen Unicode-Wert hat. Java unterscheidet deshalb Groß- und Kleinschreibung. Die beiden Methodennamen main() und Main() sind nicht identisch.
4. Methodennamen dürfen keinem Java-Schlüsselwort gleichen und sie sollten nicht mit Namen von Java-Paketen identisch sein.

Es gibt neben den zwingenden Regeln für Methodennamen ein paar unverbindliche, allerdings gängige Konventionen.

Übliche Konventionen für Methodennamen

1. Man sollte möglichst sprechende Methodennamen verwenden.
2. Die Methodennamen sollten mit einem Kleinbuchstaben beginnen und anschließend klein geschrieben werden. Wenn sich ein Bezeichner aus mehreren Worten zusammensetzt, dürfen diese nicht getrennt werden. Die jeweiligen Anfangsbuchstaben werden jedoch innerhalb des Gesamtbezeichners jeweils groß geschrieben.

6.7.6 Die Parameterliste

Eine Parameterliste ist eine Reihe von Informationen, die als Parameter in Klammern eingeschlossen an die Methode weitergegeben werden. Jeder Parameter besteht aus der Angabe des Datentyps und des Variablennamens, wie er innerhalb der Methode verwendet werden soll. Die Anzahl der Parameter ist beliebig und kann Null sein. Im letzteren Fall lassen Sie die Klammern leer. Wenn mehr als ein Parameter angegeben wird, werden die Parameter durch Kommata getrennt.

Beispiele:

public static int zurueck()
public static void vierParameter(
  int para1, int para2, float para3, boolean para4)

6.7.7 Der Methodenkörper

Im Methodenkörper kann man lokale Variablen deklarieren, andere Methoden aufrufen oder jegliche Form von Anweisungen aufrufen, solange sie keinen syntaktischen Regeln widersprechen. Wenn ein Rückgabewert gefordert wird, muss die return-Anweisung die letzte Anweisung im Quelltext sein. Andernfalls meldet der Compiler eine unerreichbare Anweisung und wird die Methode nicht übersetzen.

6.7.8 Methoden überladen und überschreiben

Hier kommen wir nun zu zwei grundlegenden Java-Techniken - dem Überladen und dem Überschreiben von Methoden.

Überladen (Overloading)

Java erlaubt, wie wir wissen, ein Überladen von Methoden, jedoch kein Überladen von Operatoren. Das so genannte Methoden-Overloading ist ein Teil der Realisierung des polymorphen Verhaltens von Java. Wir hatten bei den allgemeinen Betrachtungen zu OOP die Technik des Überladens von Methoden schon besprochen, doch noch einmal zur Erinnerung: Überladen kann bedeuten, dass bei Namensgleichheit von Methoden in Superklasse und abgeleiteter Klasse die Methoden der abgeleiteten Klasse die der Superklasse überdecken. Dabei wird aber zwingend vorausgesetzt, dass sich die Parameter signifikant unterscheiden, sonst handelt es sich bei der Konstellation Superklasse-Subklasse um den Vorgang des Überschreibens. Signifikant für die Unterscheidung kann die Anzahl der Parameter sein, aber auch bei gleicher Anzahl von Parametern alleine der Parametertyp.

In der gleichen Klasse kann man sowieso nur Methoden mit gleichem Bezeichner deklarieren, wenn diese sich signifikant irgendwo sonst unterscheiden. Das Überladen einer Methode ist hauptsächlich ein allgemeiner Prozess zum Erstellen mehrerer Methoden (selbst in der gleichen Klasse) mit demselben Methodennamen, jedoch mit unterschiedlichen Parametersignaturen und wahrscheinlich auch unterschiedlichen Funktionalitäten im jeweiligen Methodenkörper. Zur Laufzeit wird das Programm dann automatisch bestimmen, welche Version der Methode aufzurufen ist. Die unterschiedlichen Parametersignaturen sind das Merkmal, nach dem die Auswahl erfolgt. Gerade bei den Konstruktoren (auf die wir gleich noch zu sprechen kommen) als speziellen Methoden lässt sich die Technik des Überladens natürlich einsetzen.

class SuperKlasse {
 public static void gebeaus(int i) {
  System.out.println("Das ist die Superklasse: " 
  + i);
 }  }
class Ueberlade extends SuperKlasse {
public static void gebeaus(int i, int j) {
System.out.println(
"Das ist die Subklasse mit zwei Parametern: " 
  + (i + j));
}
public static void gebeaus(float i) {
System.out.println(
"Die Subklasse mit einem float-Parameter: " + i);
}
public static void main (String args[]) {
/* Aufruf der Methode mit zwei Parametern in der Subklasse */
  gebeaus(2,2); 
/* Aufruf der Methode in der Superklasse auf Grund des int-Parameters */
  gebeaus(1); 
/* Aufruf der Methode mit einem float-Parameter in der Subklasse */
  gebeaus(1.2f); 
 }  }

Die Kommentare im Source dokumentieren, wann und warum welche Methode aufgerufen wird. Zuerst wird auf Grund der zwei Parameter die passende Methode der Subklasse aufgerufen, anschließend auf Grund des einen int-Parameters die Methode in der Superklasse. Der dritte Aufruf nimmt die Methode mit einem float-Parameter in der Subklasse.

Abbildung 6.18:  Die verschiedenen Methodenaufrufe

Die Parametersignatur und die Parameterliste sind zu unterscheiden. Die Parametersignatur einer Methode ist das Merkmal, nach dem die Auswahl beim Überladen erfolgt. Sie gibt nur an, welche Art Datentyp von Parameter an eine Methode weitergereicht wird, sowie die Anzahl der Parameter, jedoch nicht den Namen des Parameters. Die Parameterliste einer Methode dagegen enthält zusätzlich den Namen des Parameters.

Der Unterschied zwischen Parameterliste und Parametersignatur ist entscheidend, denn das Deklarieren einer Methode desselben Namens (in der gleichen Klasse oder in Super- und Subklasse), aber mit einer unterschiedlichen Parametersignatur, führt wie gesagt zum Überladen einer Methode, das Deklarieren einer neuen Methode mit der gleichen Parametersignatur, und nur maximal einer unterschiedlichen Parameterliste (das bedeutet nur maximal unterschiedliche Namen der Parameter bei sonst unveränderten Angaben) jedoch nicht. Im Gegenteil, es wird sogar ein Compilerfehler erzeugt, wenn zwei oder mehr Methoden desselben Namens und derselben Parametersignatur in der gleichen Klasse deklariert werden. In unterschiedlichen Klassen können jedoch Methoden mit gleichem Namen und identischer Parameterliste deklariert werden. Auch wenn diese über Vererbung in Verbindung stehen. Dann heißt der Vorgang Überschreiben (Overriding).

Overriding

Auch die Technik des Überschreibens hatten wir bei den allgemeinen Betrachtungen zu OOP schon besprochen. Der Mechanismus des Überschreibens von Methoden ist dem Überladen ähnlich, sollte jedoch nicht mit ihm verwechselt werden. Überschreiben ermöglicht ebenfalls eine Spezialisierung von Objekten. Wir hatten schon angesprochen, dass das Erstellen zweier Methoden des gleichen Namens und der identischen Parametersignatur innerhalb derselben Klasse nicht gestattet ist, aber um eine Klasse zu erweitern, kann man sie in einer Subklasse überschreiben. Es bedeutet, dass Methoden einer Subklasse die Methoden der Elternklasse verdecken.

Der Unterschied zwischen Überschreiben und Überladen einer Methode in einer Superklasse ist mehr formeller als inhaltlicher Natur. Eine Methode zu überschreiben bedeutet, dass die Methode in der Subklasse den identischen Deklarationskopf (also neben der Namensgleichheit auch vollkommen identische Argumente) wie die Methoden der Superklasse erhält, während bei dem Überladen der Methoden wie gesagt zwar die Namen der Methoden, aber nicht die Argumente identisch sind.

class SuperKlasse2 {
//Methode der Superklasse
 public static void gebeaus(int i) {
  System.out.println(i + 10);
 }  }
class Ueberschreibe extends SuperKlasse2 {
/* gleicher Methodenname in der Subklasse */  
//Identische Methodendeklaration
 public static void gebeaus(int i) {
  System.out.println(i - 10);
 }
 public static void main (String args[])  {
// Methode der Superklasse wird überschrieben
  gebeaus(42); 
 }
}

Die Ausgabe wird 32 sein, d.h., die Methode der Superklasse wird überschrieben.

Das Schlüsselwort super

Es gibt diverse Gründe, warum man eine Methode, die bereits in einer Superklasse implementiert war, sowohl in der überschriebenen Variante, als auch gleichzeitig in der Originalversion nutzen möchte. Dazu dient das Schlüsselwort super5. Ein Methodenaufruf wird bei deren Verwendung in der Hierarchie nach oben zur Superklasse weitergereicht. Schauen wir ein Beispiel mit dem Schlüsselwort super an.

class MeineSuperklasse {
//Methode der Superklasse
 void gebeaus(int i)  {
  System.out.println(i + 10);
 }  }
class Ueberschreibe2 extends MeineSuperklasse {
/* gleicher Methodenname, aber anderer Parameter in der Subklasse */  
//Identische Methodendeklaration
 void gebeaus(int i) {
   System.out.println(i - 10); //Ausgabe 
//Zugriff auf die Methode der Superklasse
   super.gebeaus(42); 
  }
  public static void main (String args[]) {
    Ueberschreibe2 u = new Ueberschreibe2();
// Die Methode der Subklasse wird aufgerufen
    u.gebeaus(42); 
   }  
}

Die Ausgabe wird 32 und in der nächsten Zeile 52 (das Ergebnis der Superklassen-Methode) sein. Um eine Methode in einer Superklasse aufzurufen, wird also die folgende Syntax verwendet:

super.<methodenname>(<Parameter>);

Beachten Sie in dem Beispiel, dass wir erst ein Objekt der Klasse Ueberschreibe2 erzeugen, um auf die Methode gebeaus() in der Klasse Ueberschreibe2 zugreifen zu können. In dieser Methode rufen wir erst die Methode der Superklasse auf. Wenn Sie direkt in der main()-Methode auf die Superklasse referenzieren wollen, erhalten Sie die Meldung "Undefined variable ...". Wie wir beim this-Schlüsselwort noch sehen werden haben, können Sie nur in dem Körper einer nicht-statischen Methode das Schlüsselwort super verwenden.

Schauen wir uns super noch in einem anderen Zusammenhang an. Besondere Methoden sind die Konstruktoren, die man gleichfalls in der Superklasse aufrufen kann. Jedoch ist bei Konstruktoren der Methodenname überflüssig, denn der muss sowieso identisch zum Klassenname sein. Deshalb wird bei Konstruktoren die folgende Syntax verwendet:

super(<Parameter>);

Wenn der Konstruktor keine Parameter hat, kann man auch einfach

super();

notieren.

Der Einsatz von super in Konstruktor-Methoden bewirkt den Aufruf der unmittelbaren Superklasse.

// die Superklasse
class ErsteEbene { 
// Konstruktor 1 der Superklasse ohne Parameter
 ErsteEbene() {
   System.out.println(
    "Der erste Konstruktor der Superklasse wird 
     aufgerufen.");
  }
/* Konstruktor 2 der Superklasse mit einem int-
   Parameter. Zusätzlich erledigt dieser 
   Konstruktor noch einige Aufgaben. In unserem 
   Fall 2 Bildschirmausgaben */
 ErsteEbene(int i)  {
   System.out.println(
    "Der zweite Konstruktor der Superklasse wird 
     aufgerufen.");
   System.out.println(
   "Der Wert der Variablen ist "+ i);
  }
}
// Die Subklasse der Klasse ErsteEbene
class ZweiteEbene extends ErsteEbene { 
 ZweiteEbene() // Der Konstruktor 1 der Subklasse
 {
// Aufruf des Konstruktors der Superklasse ohne 
// Parameter
  super(); 
  System.out.println("Der Konstruktor 1 der 
Subklasse wird aufgerufen.");
 }
// Der Konstruktor 2 der Subklasse
 ZweiteEbene(String a)  {
// Aufruf des Konstruktors der Superklasse mit 
// Parameter
  super(5); 
  System.out.println(
   "Konstruktor 2 der Subklasse wird aufgerufen. 
    Uebergabewert war: " + a);
 }
}
public class SuperTest {
 public static void main (String args[]) {
/* Die Anwendung des new-Operators erstellt eine Instanz der Klasse zweiteEbene. Es wird 
Speicher zugewiesen und - für uns hier wichtig - der Konstruktor der Klasse zweiteEbene 
aufgerufen. */
  new ZweiteEbene(); 
  new ZweiteEbene("Ueber und ueber"); 
 }
}

Die Ausgabe sieht wie folgt aus:

Der erste Konstruktor der Superklasse wird aufgerufen.

Der Konstruktor 1 der Subklasse wird aufgerufen.

Der zweite Konstruktor der Superklasse wird aufgerufen.

Der Wert der Variablen ist 5.

Konstruktor 2 der Subklasse wird aufgerufen. Uebergabewert war: Ueber und ueber

Abbildung 6.19:  Die Ausgabe beweist den Zugriff auf die Konstruktoren der Superklasse.

Der Aufruf des Konstruktors der Superklasse muss die erste Anweisung in Konstruktor der Subklasse sein.

6.8 Schnittstellen und das Schlüsselwort implements

Der Verzicht von Java auf Mehrfachvererbung wird über ein so genanntes Schnittstellenkonzept kompensiert (siehe dazu auch Seite 148). Objekte können eine beliebige Anzahl an Schnittstellen (Interfaces) implementieren. Schnittstellen sind im Prinzip Klassen sehr ähnlich6, jedoch im Gegensatz dazu unterstützen sie Mehrfachvererbung und sind auch sonst flexibler in Bezug auf Vererbung, als dies bei Klassen der Fall ist.

Zumindest eine Schnittstelle wird in Ihrer Java-Karriere eine große Rolle spielen: die Schnittstelle Runnable, die für die Multithreading-Technik zentrale Bedeutung hat.

Eine Schnittstelle ist in der Java-Sprache eine Sammlung von Methoden-Namen ohne konkrete Definition sowie einer Reihe von Konstanten. Obwohl eine einzelne Java-Klasse nur genau eine Superklasse haben kann (und auch genau eine haben muss!), können in einer Klasse mehrere Schnittstellen implementiert werden.

Schnittstellen ermöglichen, ähnlich wie abstrakte Klassen, das Erstellen von Pseudoklassen, die ganz aus abstrakten Methoden zusammengesetzt sind. Neben der Eigenschaft als Alternative zu der Mehrfachvererbung werden Schnittstellen (gerade durch die Eigenschaft, keine konkrete Definition zu besitzen) dazu benutzt, eine bestimmte Funktionalität zu definieren, die in mehreren Klassen benutzt werden kann oder wenn die genaue Umsetzung einer Funktionalität noch nicht sicher ist (sie wird erst später in der auf die Schnittstelle aufbauenden Klasse realisiert). Sofern Sie derartige Methoden in einer Schnittstelle unterbringen, können Sie gemeinsame Verhaltensweisen definieren und die spezifische Implementierung dann den Klassen selbst überlassen. Daher liegt die Verantwortung für die Spezifizierung von Methoden dieser Implementierungen, ähnlich wie bei abstrakten Klassen, immer bei den Klassen, die eine Schnittstelle implementieren. Durch die Implementierung von Schnittstellen muss jede nicht-abstrakte Klasse alle in der Schnittstelle deklarierten Methoden überschreiben! Das ist analog der Verwendung von abstrakten Klassen. Auch sonst sind sich abstrakte Klassen und Schnittstellen sehr ähnlich. Wesentliche Unterschiede sind wie gesagt die Implementation in Klassen und die Tatsache, dass eine Schnittstelle keine Methoden mit Körper enthalten kann (was ja in einer abstrakten Klasse nicht verboten ist).

6.8.1 Erstellung einer Schnittstelle

Die Syntax zur Erstellung einer Schnittstelle und der konkrete Erstellungsprozess ist der/dem von Klassen sehr ähnlich. Es gibt vor allem den wichtigen Unterschied, dass keine Methode in der Schnittstelle einen Körper haben darf und keine Variablen deklariert werden dürfen, die nicht als Konstanten dienen. Die Deklaration einer Schnittstelle erfolgt mit folgender Syntax:

[public] interface <NamederSchnittstelle> 
  [extends <SchnittstellenListe>]

Alles in eckigen Klammern Geschriebene ist optional.

Öffentliche Schnittstellen

Schnittstellen können als Voreinstellung von allen Klassen im selben Paket implementiert werden (freundliche Einstellung). Damit verhalten sie sich wie freundliche Klassen und Methoden. Indem Sie Ihre Schnittstelle explizit als public deklarieren, ermöglichen Sie es - wie auch bei Klassen und Methoden - den Klassen und Objekten außerhalb eines gegebenen Pakets, diese Schnittstelle zu implementieren.

Analog public-Klassen müssen öffentlich deklarierte Schnittstellen zwingend in einer Datei namens < NamederSchnittstelle >.java definiert werden.

Andere Zugriffsmodifier als public sind bei Schnittstellen nicht erlaubt!

6.8.2 Namensregeln für Schnittstellen

Die Regeln für die Benennung von Schnittstellen sind dieselben wie für Klassen. Es gibt wenige Regeln, die die Vergabe von Namen beschränken.

Zwingende Namenskonventionen für Schnittstellen

1. Schnittstellennamen dürfen im Prinzip eine unbeschränkte Länge haben (bis auf technische Einschränkungen durch das Computersystem).
2. Genau wie bei allen anderen Bezeichnern in Java sind die einzigen Bedingungen für Schnittstellennamen, dass diese mit einem Buchstaben oder einem der beiden Zeichen _ oder $ beginnen müssen. Es dürfen weiter nur Unicode-Zeichen über dem Hexadezimalwert 00C0 (Grundbuchstaben und Zahlen sowie einige andere Sonderzeichen) verwendet werden.
3. Zwei Token gelten nur dann als derselbe Bezeichner, wenn sie dieselbe Länge haben und jedes Zeichen im ersten Token genau mit dem korrespondierenden Zeichen des zweiten Tokens identisch ist, d.h., den vollkommen identischen Unicode-Wert hat.
4. Schnittstellennamen dürfen keinem Java-Schlüsselwort gleichen und sie sollten nicht mit Namen von Java-Paketen identisch sein.

Wie auch bei Klassen ist es üblich, den ersten Buchstaben jedes Schnittstellennamens groß zu schreiben.

6.8.3 Die Erweiterung anderer Schnittstellen

Java-Schnittstellen können gemäß dem OOP-Konzept der Vererbung auch andere Schnittstellen erweitern, um somit zuvor geschriebene Schnittstellen weiterentwickeln zu können. Die neue Subschnittstelle erbt dabei auf die gleiche Art und Weise wie bei Klassen die Eigenschaften von der Superschnittstelle. Vererbt werden alle Methoden und statischen Konstanten der Superschnittstelle.

Zwar können Schnittstellen andere Schnittstellen erweitern, aber weil diese keine konkreten Methoden definieren dürfen, dürfen auch die Subschnittstellen keine Methoden der Superschnittstellen definieren. Statt dessen ist dies die Aufgabe jeder Klasse, die die abgeleitete Schnittstelle verwendet. Die Klasse muss sowohl die in der Subschnittstelle deklarierten Methoden als auch alle Methoden der Superschnittstellen definieren! Wenn Sie also eine erweiterte Schnittstelle in einer Klasse implementieren, müssen Sie sowohl die Methoden der neuen als auch die der alten Schnittstelle überschreiben.

Schnittstellen können Klassen nicht erweitern.

Wenn Sie mehr als eine Schnittstelle erweitern wollen, müssen Sie die einzelnen Schnittstellennamen in der Schnittstellenliste der Schnittstellendeklaration durch ein Komma von dem anderen trennen.

6.8.4 Der Körper einer Schnittstelle

Da eine Schnittstelle nur abstrakte Methoden und Konstanten (als final deklarierte Variablen) enthalten darf, können im Körper einer Schnittstelle zwar keine bestimmten Implementierungen spezifiziert werden, aber ihre Eigenschaften können dennoch festgelegt werden. Ein großer Teil der Vorzüge von Schnittstellen resultiert aus der Fähigkeit, Methoden deklarieren zu können.

Methoden in Schnittstellen

Die Hauptaufgabe von Schnittstellen ist das Deklarieren von abstrakten Methoden, die in anderen Klassen definiert werden. Bei diesem Prozess müssen ein paar wichtige Dinge beachtet werden.

Der einzig signifikante Unterschied in der Syntax der Deklaration einer Methode in einer Schnittstelle und der Syntax für die Deklaration einer Methode in einer Klasse ist der, dass im Gegensatz zu Methoden, die in Klassen deklariert werden, die Methoden in Schnittstellen keinen Körper haben können. Eine Schnittstellen-Methode besteht nur aus einer Deklaration. Eine Deklaration einer Methode legt zwar nicht fest, wie diese sich verhalten wird. Nichtsdestotrotz wird definiert, welche Informationen sie als Übergabeparameter benötigt und welche (falls überhaupt) Informationen als Rückgabewert zurückgegeben werden.

Weil die folgenden Implementierungen der Methoden in den Klassen von den Deklarationen der Methoden in den Schnittstellen abhängig sind, ist eine genaue Planung der nötigen Eigenschaften der Methoden - ihrer Rückgabewerte, Parameter und Ausnahmelisten - extrem wichtig.

Die konkrete Syntax von Methodendeklarationen in Schnittstellen hat folgende Syntax:

[public] <rückgabeWert>   <nameMethode>([<Parameter>]) 
  [throws <ExceptionListe>];

Alles in eckigen Klammern Geschriebene ist optional.

Die Deklaration einer Methode in Schnittstellen endet direkt mit einem Semikolon (anders als bei normalen Methodendeklarationen in Klassen).

Die Verwendung des Schlüsselworts public bei Methodendeklarationen in Schnittstellen ist zwar bei der Deklaration einer Methode möglich, aber da alle Methoden in Schnittstellen defaultmäßig public sind, kann man diesen Modifier weglassen. Die anderen potenziellen Methodenmodifier (native, static, synchronized, final, private oder protected) dürfen bei der Deklaration einer Methode in einer Schnittstelle nicht verwendet werden.

Variablen in Schnittstellen

Auch wenn Schnittstellen im Allgemeinen zur abstrakten Implementierung von Methoden verwendet werden, so können sie dennoch zusätzlich einen bestimmten Typ von Variablen enthalten. Da Schnittstellenmethoden keinen Code im Körper enthalten können, müssen alle Variablen, die in einer Schnittstelle deklariert werden, außerhalb der Methodenkörper stehen. Es werden also globale Felder für die Klasse sein. Weiterhin sind alle Felder, die in einer Schnittstelle deklariert werden, unabhängig vom Modifier, der bei der Deklaration des Feldes benutzt wurde, immer public, final und static. Sie müssen das nicht explizit in der Felddeklaration angeben, obwohl es der besseren Lesbarkeit halber sinnvoll ist.

Felder in Schnittstellen werden wie finale statische Felder in Klassen dazu verwendet, Konstanten zu definieren, die allen Klassen zur Verfügung stehen, die diese Schnittstellen implementieren.

Weil alle Felder final sind, müssen sie in der Schnittstelle unbedingt initialisiert werden, wenn sie von der Schnittstelle selbst deklariert werden. Wenn Sie das unterlassen, meldet der Compiler einen Fehler.

Ausnahmen in Schnittstellen

Auch bei Schnittstellenmethoden gibt es die Möglichkeit, Ausnahmen anzugeben. Im Allgemeinen ist eine Ausnahme eine unerwünschte Erscheinung wie ein Fehler. Deshalb ist es sinnvoll, in jedes Programm Code zur Fehlerbehandlung einzubauen, um solche Fehler abzufangen. Solche unerwünschten und undefinierten Vorkommnisse müssen behandelt werden. Java besitzt besondere Konstrukte, um mit solchen Problemen umgehen zu können. Ausnahmen in Java sind Objekte, die erzeugt werden, wenn eine Ausnahmebedingung vorliegt. Diese Objekte werden dann von der Methode an das aufrufende Objekt zurückgegeben und müssen von diesem geeignet behandelt werden. Diese Erzeugung von Ausnahmen wird mit der Anweisung throw ausgeführt. Das Auffangen von Ausnahmen (catch) wird im Allgemeinen dadurch erreicht, dass man Anweisungen, die Ausnahmen erzeugen könnten, im Inneren eines try-catch-Block schreibt.

Um nun eine Ausnahme erzeugen zu können, muss der Ausnahmetyp (oder einer seiner Superklassen) in der Ausnahmenliste für die Methode enthalten sein. Bei Schnittstellenmethoden sind Ausnahmen allerdings von geringer Bedeutung, denn es handelt sich nur um abstrakte Methoden. Alleine für das Überschreiben von Schnittstellenmethoden sind die Ausnahmen von Bedeutung. Dies sind die Regeln für das Überschreiben von Schnittstellenmethoden, die Ausnahmen erzeugen:

  • Die neue Ausnahmenliste in der Implementierung darf nur Ausnahmen enthalten, die auch in der ursprünglichen Ausnahmenliste oder in deren Subklassen vorhanden sind.
  • Die neue Ausnahmenliste muss nicht unbedingt Ausnahmen enthalten, egal wie viele in der ursprünglichen Liste vorhanden sind. Der Grund ist, dass die alte Liste der neuen Methode durch Vererbung zugewiesen wird.
  • Die überschriebene Methode kann jede in der ursprünglichen Ausnahmenliste enthaltenen oder aus dieser Liste abgeleiteten Ausnahmen, unabhängig von der eigenen Ausnahmenliste auswerfen.
  • Im Allgemeinen bestimmt die Ausnahmenliste der in der Schnittstelle deklarierten Methode, und nicht die der redeklarierten Methode, welche Ausnahmen ausgeworfen werden können und welche nicht.

6.8.5 Überschreiben und Verwenden von Schnittstellenmethoden

Eine Schnittstellenmethoden kann nicht verwendet werden, bis sie nicht in der Klasse überschrieben worden ist, die diese Methode implementiert. Das ist zwangsläufig, da eine Schnittstellenmethode ja keinerlei Körper haben darf. Eine Funktion gibt es aber selbstverständlich dennoch. Eine Methodendeklaration in einer Schnittstelle legt das Verhalten einer Methode insoweit fest, als dass der Methodenname, der Rückgabetyp und die Parametersignatur definiert werden.

Die Implementierung von Schnittstellen in Klassen erfolgt über die implements-Anweisung, wie wir bei der Behandlung der Klassendeklaration gesehen haben.

Wenn eine solche Schnittstellenmethode in einer Klasse überschrieben wird, dann gibt es mehrere Aspekte, die geändert werden können. Genau genommen ist das Einzige, was sich an der Methode nie verändern darf, der Methodenname. Aber auch andere Faktoren unterliegen bezüglich einer Veränderung strengen Regeln:

1. Wie wir schon gesehen haben, sind alle in einer Schnittstelle deklarierten Methoden als Grundeinstellung mit dem Zugriffslevel public ausgestattet. Eine solche Methode kann nicht so überschrieben werden, dass der Zugriff auf sie noch weiter beschränkt wird. Deshalb müssen alle in einer Schnittstelle deklarierten und in einer Klasse überschriebenen Methoden mit dem Zugriffsmodifier public versehen werden. Von den übrigen Modifiern, die auf Methoden angewendet werden können, dürfen nur native und abstract auf solche Methoden angewendet werden, die ursprünglich in einer Schnittstelle deklariert wurden.
2. Schnittstellenmethoden können eine Parameterliste definieren, die an die Methode weitergegeben werden müssen. Wenn in der Klasse eine neue Methode mit dem gleichen Namen, jedoch einer anderen Parameterliste deklarieren, wird wie allgemein üblich die in der Schnittstelle deklarierte Methode überladen und nicht überschrieben. Dies ist zwar nicht falsch, aber dann muss noch zusätzlich die Methode überschrieben werden, denn wir hatten ja schon festgehalten, dass jede Methode in einer Schnittstelle überschrieben werden muss, außer Sie erklären Sie für abstrakt, indem Sie dieselbe Parametersignatur wie in Ihrer Schnittstelle verwenden. Das bedeutet, dass sich für eine Redefinition zwar die Namen von Variablen ändern dürfen, nicht aber deren Anordnung und Typ.

Da Körper von Schnittstellenmethoden leer sind, muss als wesentliche Aufgabe bei der Implementierung einer solchen Methode in eine Klasse ein Körper für die ursprünglich in der Schnittstelle deklarierten Methoden erstellt werden. Und zwar wie bereits mehrfach erwähnt für jede ursprünglich in Ihrer Schnittstelle deklarierte Methode, außer die Methode soll native oder die neue Klasse abstrakt sein.

Wie Sie konkret eine Schnittstelle nur überschreiben, hängt von der Aufgabe ab, die die jeweilige Methode erfüllen soll. Eine Schnittstelle stellt zwar sicher, dass Methoden in einer nicht-abstrakten Klasse definiert und entsprechende Datentypen zurückgegeben werden, ansonsten werden von ihr aber keine weiteren Restriktionen oder Begrenzungen für die Körper der Methoden in den Klassen festgelegt. Allerdings gibt es dennoch einige potenzielle Fallen. Zwar ist immer sichergestellt, dass eine nicht-abstrakte Klasse, die eine Schnittstelle implementiert, jede in dieser Schnittstelle deklarierte Methode enthält. Allerdings ist damit noch lange nicht gewährleistet, dass diese Methoden von ihrer Funktionalität her auch richtig implementiert werden. Die Erstellung eines Methodenkörpers, der nur aus geöffneten und geschlossenen geschweiften Klammern besteht, reicht beispielsweise aus, um die Bedingung einer Methode zu erfüllen, deren Rückgabetyp void ist. Ansonsten langt eine return-Anweisung, die eine geforderten Datentyp zurückliefert. Damit tricksen Sie den Compiler insofern aus, als dass Sie die Bedingungen für die Implementierung einer Schnittstelle erfüllt haben. Das kann indes zu vielerlei Problemen führen. Mit solchen Methoden ohne sinnvolle Funktion lassen sich Operationen mit Klassen durchführen, die dann jedoch unbefriedigende Resultate erzielen.

Es gibt mehrere Verfahren, mit denen Klassen, die Schnittstellen implementieren, von anderen Klassen benutzt werden können. Alle Anwendungen hängen von einer ganz bestimmten Bedingung ab: Es muss sichergestellt werden, dass die in der Schnittstelle deklarierten Felder und Methoden in der gegebenen Klasse definiert werden.

6.8.6 Verwenden von Feldern einer Schnittstelle

Die Felder einer Schnittstelle müssen sowohl statisch als auch final sein. Der Zugriff auf ein Feld einer Schnittstelle erfolgt entweder direkt oder über die Benutzung der standardisierten Punktschreibweise:

<InterfaceName>.<feld>

6.8.7 Beispiele mit Schnittstellen

Schnittstellen sind zwar nicht schwer zu verstehen, aber das Thema war bisher ziemlich abstrakt. Wir werden also ein paar Beispiele erstellen, die Schnittstellen erzeugen und diese dann in einer Klasse verwenden. Gehen wir die Geschichte erst einmal ganz einfach an - mit einer Schnittstelle, wo nur zwei Konstanten deklariert werden, die dann in einer Klasse ausgegeben werden.

interface MeineSchnittstelle {
 int zaehler = 42;
 String str = "Test in der Schnittstelle.";
}
public class InterfaceTest 
  implements MeineSchnittstelle {
public static void main (String args[]) {
  System.out.println(zaehler); //Ausgabe 
  System.out.println(str); //Ausgabe 
 }  }

Die Ausgabe wird der Wert der beiden in der Schnittstelle definierten Konstanten sein. Beachten Sie, dass wir die optionalen Angaben bei der Schnittstellendeklaration weggelassen haben. Die Eigenschaften der Schnittstellen legen automatisch (wie oben beschrieben) zahlreiche Randbedingungen fest.

Testen Sie das Beispiel einmal, indem Sie die Wertzuweisung der Variablen in der Schnittstelle weglassen. Sie werden folgende Fehlermeldung erhalten:

InterfaceTest.java:3: = expected

int zaehler;

^

InterfaceTest.java:4: = expected

String str;

^

Jedes Feld einer Schnittstelle muss zwingend eine Wertzuweisung dort bekommen. Es ist ja als Konstante zu verstehen.

Erweitern wir nun das Beispiel. Dazu erzeugen wir eine weitere Schnittstelle und implementieren auch diese in einer Klasse. Dieses Mal jedoch nicht in der public-Klasse, sondern in einer neuen Klasse. Aus dieser erzeugen wir dann in der main()-Methode der öffentlichen Klasse ein Objekt und greifen über dieses auf die Methode zu, die die zweite Schnittstelle als Methodendeklaration enthält und zwingend in der implementierenden Klasse überschrieben werden muss.

interface MeineSchnittstelle {
 int zaehler = 42;
 String str = "Test in der Schnittstelle.";
}
interface MeineZweiteSchnittstelle {
 public void meineMethode(); 
}
class MethodenAufruf implements 
  MeineSchnittstelle, MeineZweiteSchnittstelle {
 public void meineMethode()  {
  System.out.println(zaehler); //Ausgabe 
  System.out.println(str); //Ausgabe 
 }  }
public class InterfaceTest2 {
public static void main (String args[])  {
  MethodenAufruf u = new MethodenAufruf();
  u.meineMethode();
 }  }

Die Ausgabe wird identisch mit dem ersten Beispiel sein.

Testen Sie den Fall, dass Sie die Methode einer Schnittstelle in der implementierenden Klasse nicht überschreiben. Sie erhalten eine Fehlermeldung der folgenden Art:

InterfaceTest2.java:11: class InterfaceTest2 must be declared abstract. It does not define void meineMethode() from interface MeineZweiteSchnittstelle. public class InterfaceTest2 implements MeineSchnittstelle, MeineZweiteSchnittstelle

6.9 Variablen

Wir haben den Begriff Variable bisher schon sehr intensiv benutzt. Jeder, der mit irgendwelchen Programmiersprachen Erfahrung hat, kann sich darunter etwas vorstellen. Variablen sind Stellen im Hauptspeicher, in denen irgendwelche Werte gespeichert werden können. Dazu haben sie einen Namen, einen Datentyp und eben einen Wert. Wir kennen schon alle Java-Datentypen, die Namensregeln für Variablen und wissen bereits, dass vor einer Verwendung einer Variablen sie deklariert werden muss. Außerdem ist uns bekannt, dass Java keine Konstanten im üblichen Sinn kennt und diese über das Schlüsselwort final bei Variablen realisiert.

Soweit wahrscheinlich nichts Neues. Die nachfolgende Aussage ist jedoch schon etwas interessanter.

Java kennt nur drei unterschiedliche Arten von Variablen:

1. Instanzvariablen
2. Klassenvariablen
3. Lokale Variablen

Doch wie unterscheiden sich diese drei Arten von Variablen im Detail?

6.9.1 Instanzvariablen

Instanzvariablen werden zum Definieren von Attributen oder eines Zustandes eines bestimmten Objekts benutzt. Dazu werden Instanzvariablen innerhalb der Klasse definiert. Man findet auch gelegentlich den Begriff Objektvariable statt Instanzvariable, der dasselbe bezeichnet.

Grundsätzlich stehen Instanzvariablen nur über ein Objekt zur Verfügung (analog der Situation von Instanzmethoden). Um auf eine Instanzvariable im allgemeinen Fall zuzugreifen, verwenden Sie die Punktnotation. Dabei steht auf der linken Seite des Punktes das Objekt (die Instanz), zu der die Variable gehört, und auf der rechten Seite der Name der Variablen.

Beispiele:

MeinObjekt.meineVariable;
DeinObjekt.dortigeInstanzVariable;

Auf beiden Seiten des Punktes steht ein Ausdruck (im Java-Sinn). Damit können Instanzvariablen auch verschachtelt werden. Wenn die Instanzvariable selbst ein Objekt beinhaltet und dieses Objekt wiederum eine eigene Instanzvariable, so kann auf darauf über die Punktnotation Bezug genommen werden. Die Punktnotation wird dabei von links nach rechts bewertet.

Beispiel: MeinObj.meineVar.enthalteneInstanzVariable;

Zuerst wird im dem Beispiel die Variable meineVar von dem Objekt MeinObj bewertet. Diese zeigt auf ein anderes Objekt mit der Variable enthalteneInstanzVariable. Deren Wert wird dann zurückgegeben.

6.9.2 Klassenvariablen

Klassenvariablen sind mit Instanzvariablen vergleichbar, außer der extrem wichtigen Tatsache, dass ihre Werte auf sämtliche Instanzen der jeweiligen Klasse und die Klasse selbst zutreffen. Sie stehen also in der Klasse selbst zur Verfügung, aber auch jede Instanz einer Klasse bekommt eine Referenz auf die Klassenvariable (ein Zeiger auf den gleichen Speicherbereich) vererbt. Egal wie die Variable angesprochen wird (in der Klasse, über die Klasse oder über eine beliebige Instanz) - es ist immer der gleiche Speicherbereich. Klassenvariablen fungieren als eine Art globale Variablen, die der Klasse selbst und all ihren Instanzen zur Verfügung stehen.

Klassenvariablen werden wie eine Instanzvariable innerhalb der Klasse definiert. Zusätzlich muss nur das Schlüsselwort static vorangestellt werden. Dies ist identisch zu dem Vorgang, mit dem Instanzmethoden zu Klassenmethoden geadelt werden.

Der Zugriff auf Klassenvariablen erfolgt wieder mit der Punktnotation. Dabei steht bei einem Aufruf von außen vor dem Punkt der Name der Klasse und nicht der einer Instanz. Bei einem Zugriff aus der Klasse heraus kann man die Klassenvariable direkt oder eine beliebige Instanz der Klasse ansprechen.

6.9.3 Klassenvariablen versus Instanzvariablen

Versuchen wir noch einmal, die Unterschiede zwischen Instanz- und Klassenvariablen exakt herauszukristallisieren. Wie bei Klassenmethoden gilt, dass Klassenvariablen außerhalb der konkreten Objekte liegen, aber innerhalb der Klassen. Sie werden über das Metaklassenkonzept in die streng objektorientierte Theorie von Java eingebunden. Klassenvariablen können deshalb an beliebigen Stellen genutzt werden, unabhängig davon, ob eine konkrete Instanz der Klasse existiert oder nicht.

Eine Instanzvariable ist dagegen nur über ein Objekt verwendbar. Zwar können Instanzmethoden einer Klasse eine Instanzvariable aufrufen, aber das verlagert das Problem wie bei der Situation mit den Instanzmethoden nur weiter. Die konkrete Anwendung erfolgt über ein Objekt. Das nachfolgenden Beispiel demonstriert die Unterschiede nochmals detailiert.

class Variable1 {
int a = 5;
static int b = 6;
public static void main(String args[]) {
  // Erzeugung von zwei Instanzen von Variable1
Variable1 u = new Variable1();
Variable1 v = new Variable1();
System.out.println(
   "Die Instanzvariable a ueber das Objekt u: " 
  + u.a);
System.out.println(
   "Die Instanzvariable a ueber das Objekt v: " 
  + v.a);
  // Über u wird dortige Instanzvariable geändert
u.a = 111;
System.out.println(
   "Instanzvariable a ueber Objekt u nach Aenderung von u.a: " + u.a);
System.out.println(
   "Instanzvariable a ueber Objekt v nach Aenderung von u.a: " + v.a);
  // Zugriff auf Klassenvariable
System.out.println(
 "Direkter Zugriff auf Klassenvariable b: " + b);
System.out.println(
   "Zugriff auf Klassenvariable b ueber Klassenname: " + Variable1.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt u: "
   + u.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt v: " 
  + v.b);
  // Direkte Aenderung der Klassenvariable
b = 765;
System.out.println(
  "Direkter Zugriff auf Klassenvariable b: " + b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Klassenname: " + Variable1.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt u: " 
  + u.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt v: " 
  + v.b);
  // Aenderung der Klassenvariable ueber Objekt v
b = 1;
System.out.println(
  "Direkter Zugriff auf Klassenvariable b: " + b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Klassenname: " + Variable1.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt u: " 
  + u.b);
System.out.println(
  "Zugriff auf Klassenvariable b ueber Objekt v: " 
  + v.b);
  }
}

Wie schon bei den Methoden betont - Klassenmethoden können nicht direkt auf Instanzvariablen zugreifen. Wie auch? Diese sind ja an die Existenz eines zugeordneten Objekts gebunden.

Die Ausgabe wird folgende sein:

Die Instanzvariable a ueber das Objekt u: 5
Die Instanzvariable a ueber das Objekt v: 5
Instanzvariable a ueber Objekt u nach Aenderung von u.a: 111
Instanzvariable a ueber Objekt v nach Aenderung von u.a: 5
Direkter Zugriff auf Klassenvariable b: 6
Zugriff auf Klassenvariable b ueber Klassenname: 6
Zugriff auf Klassenvariable b ueber Objekt u: 6
Zugriff auf Klassenvariable b ueber Objekt v: 6
Direkter Zugriff auf Klassenvariable b: 765
Zugriff auf Klassenvariable b ueber Klassenname: 765
Zugriff auf Klassenvariable b ueber Objekt u: 765
Zugriff auf Klassenvariable b ueber Objekt v: 765
Direkter Zugriff auf Klassenvariable b: 1
Zugriff auf Klassenvariable b ueber Klassenname: 1
Zugriff auf Klassenvariable b ueber Objekt u: 1
Zugriff auf Klassenvariable b ueber Objekt v: 1

Das Beispiel demonstriert (und dokumentiert im Source die entscheidenden Stellen), dass eine Änderung einer Klassenvariablen (egal, wie und wo sie erfolgt), sich in der Klasse und allen Instanzen auswirkt, während die Änderung einer Instanzvariablen für die Schwesterinstanzen der gleichen Klasse verborgen bleibt.

6.9.4 Lokale Variablen

Java kennt selbstverständlich lokale Variablen (wir haben sie schon vielfach verwendet). Lokale Variablen werden innerhalb von Methodendefinitionen deklariert und können auch nur dort benutzt werden. Dies können Indexvariablen von Schleifen (so genannte schleifenlokale Variablen) sein oder temporäre Variablen zur Aufnahme von Werten, die nur innerhalb der Methodendefinitionen Sinn machen. Es gibt gleichfalls auf Blöcke beschränkte lokale Variablen.

Lokale Variablen existieren nur solange im Speicher, wie die Methode, die Schleife oder der Block existiert.

Wesentlicher Unterschied von lokalen Variablen zu Instanz- und Klassenvariablen ist, dass sie unbedingt einen Wert zugewiesen bekommen müssen, bevor sie benutzt werden können. Instanz- und Klassenvariablen haben einen typspezifischen Defaultwert. Es gibt einen Compilerfehler, wenn lokale Variablen nicht vor der ersten Verwendung initialisiert wurden.

Ein weiterer Unterschied von lokalen Variablen zu Instanz- und Klassenvariablen ist, dass man auf sie immer direkt und nicht über die Punktnotation zugreifen kann. Der Bereich Konstanten ist in Java sowieso eine Besonderheit. Im Fall von lokalen Variablen beinhaltet dies, dass sie nicht als Konstanten gesetzt werden können. Das bedeutet, das Schlüsselwort final ist bei lokalen Variablen nicht erlaubt.

6.9.5 Die Java-Konstanten

Konstanten sind Speicherbereiche mit Werten, die sich nie ändern. Wie gerade angedeutet, können in Java solche Konstanten ausschließlich für Instanz- und Klassenvariablen, aber nie für lokale Variablen erstellt werden.

Um eine Konstante in Java zu deklarieren, benutzen Sie nur das Schlüsselwort final und stellen es einer Variablendeklaration voran. Zusätzlich müssen Sie dieser Variablen einen Anfangswert zuweisen (der sich dann auch nie mehr ändert - sonst hätten wir ja keine Konstante).

In der Java-API wird ein große Anzahl von Konstanten zur Verfügung gestellt, mit deren Hilfe Sie einfacher und übersichtlicher programmieren können.

Variablendeklarationen in Schnittstellen bedeuten immer die Erstellung von Konstanten, unabhängig davon, ob final vorangestellt wird oder nicht.

6.10 Der Zugriff auf Java-Elemente

Wir kommen nun zu einem Abschnitt, in dem geklärt werden soll, in welchen Situationen wie auf Java-Elemente (Variablen, Methoden usw.) zugegriffen werden kann. Sie also irgendwie anzusprechen. Dabei muss man verschiedene Situationen unterscheiden.

Klassenelemente innerhalb der jeweiligen eigenen Klasse kann man direkt ansprechen, indem man einfach den Bezeichner dafür angibt. Der Aufruf einer Methode oder einer Variablen wird immer soweit unten in einer Hierarchie wie möglich erfolgen. Wird ein Aufruf in einer tiefen Ebene nicht gefunden, wird die nächsthöhere Ebene durchsucht und so fort.

Noch nicht in Detail geklärt (obwohl schon oft verwendet) ist die Frage, wie Klassenelemente aus anderen Klassen benutzt werden können. Das erfolgt einfach, indem per Punktnotation der fremde Klassenname vorangestellt wird (gegebenenfalls über ihren vollqualifizierten Namen in einer Paketstruktur -dazu gleich mehr) und dann das darin enthaltene Klassenelement, das man ansprechen möchte, etwa das Element out in der Klasse System. Kommt Ihnen das bekannt vor? Wir nutzen das Verfahren bei System.out.println();.

Bezüglich der Zugänglichkeit der Klassen muss allerdings noch beachtet werden, dass das Paket, in dem sie enthalten sind, verfügbar ist. Was das genau bedeutet, besprechen wir im nächsten Abschnitt.

Auch bei fremden Klassen müssen Sie die Regel für den Zugriff auf Objektelemente natürlich einhalten. Sie erzeugen aus der anderen Klasse ein Objekt und sprechen gewünschte Instanzvariablen und Instanzmethoden über die Punktnotation an. Dabei steht auf der linken Seite des Punktes das Objekt (die Instanz), zu dem die Felder oder Methoden gehören, und auf der rechten Seite der Name der Felder oder Methoden.

Beispiele:

MeinObjekt.meineVariable;
DeinObjekt.methode(arg1,arg2);

Auf beiden Seiten des Punktes steht ein Ausdruck (im Java-Sinn). Damit kann die Punktnotation ebenfalls verschachtelt werden. Wenn etwa die aufgerufene Methode selbst ein Objekt beinhaltet und diese Objekt wiederum eine eigene Instanzvariable, so kann auf darauf über die Punktnotation Bezug genommen werden. Die Punktnotation wird dabei von links nach rechts bewertet.

6.11 Pakete und die import-Anweisung

Pakete (Packages) sind in Java Gruppierungen von Klassen und Schnittstellen, eine spezielle Art des Designs und der Organisation von Java im Großen. Sie sind das Java-Analogon zu Bibliotheken vieler anderer Computersprachen. Die Java-Laufzeitbibliothek, das Java-API, wird in Form von Paketen zur Verfügung stellt. Ein Paket von Java-Klassen enthält normalerweise logisch zusammenhängende Klassen und Schnittstellen.

6.11.1 Die Verwendung von Paketen

Jede Klasse in Java ist Bestandteil eines Pakets. Der vollqualifizierte Name einer Klasse besteht immer aus dem Namen des Pakets, gefolgt von einem Punkt, eventuellen Unterpaketen, die wieder durch Punkte getrennt werden, bis hin zum eigentlichen Klassennamen. Um nun eine Klasse verwenden zu können, muss dem Compiler gesagt werden, in welchem Paket er sie suchen soll. Das kann einmal mit dem vollqualifiziertn Namen erfolgen, zum Beispiel: java.awt.Frame;

Das bedeutet, dass der Compiler in dem Paket java nach dem Unterpaket awt und dort die Klasse Frame sucht.

Ein Paket stellt eine Abbildung eines Unterverzeichnisses dar, wo nach der gewünschten Klasse vom Compiler gesucht werden soll. Wird eine Klasse also in der genannten Paketstruktur gesucht, sucht der Compiler in der Verzeichnisstruktur java/awt/Frame.

Dabei stellt sich natürlich die Frage, in welchem Verzeichnis der Compiler die Suche beginnt.

Zuerst einmal wird das aktuelle Verzeichnis nach solchen Unterverzeichnissen durchsucht. Findet der Compiler dort nichts, sucht er entsprechend dem voreingestellten Suchpfad des Systems. Früher war dafür die Umgebungsvariable CLASSPATH verantwortlich, aber seit dem JDK 1.2 wird diese nicht mehr herangezogen7. Nach der Installation des JDK »wissen« alle Tools implizit, von welchem Verzeichnis aus sie nach Klassen suchen sollen. Das ergibt sich aus dem JDK-Unterverzeichnis bin. Von da aus werden die Systemklassen von Java (also Packages des Java-API) gesucht. Wenn Sie eigene Packages erstellen und in einem anderen Programm verwenden wollen, müssen die dabei verwendeten Verzeichnisse dem Suchpfad hinzugefügt werden (etwa mit der Option -classpath beim Aufruf des Compilers).

Mehr dazu finden Sie bei der Behandlung des Compilers im Kapitel über das JDK.

Es gibt nun einige Namenskonventionen für Pakete. Die Standardklassen von Java sind in eine Paketstruktur eingeteilt, die an das Domain-System von Internet-Namen angelehnt ist. Dabei sollte jeder Anbieter von eigenen Java-Paketen diese nach seinem DNS-Namen strukturieren, nur in umgekehrter Reihenfolge. Wenn etwa eine Firma RJS mit der Internetadresse www.rjs.de Pakete bereitstellt, sollte die Paketstruktur de.rjs lauten. Eventuelle Unterpakete sollten darunter angeordnet werden. Einzige (offizielle) Ausnahmen sind die Klassen, die im Standardumfang einer Java-Installation enthalten sind. Diese beginnen mit java oder javax. Das System ist aber nicht zwingend. Nur kann es bei Nichtbeachtung dazu führen, dass es in größeren Projekten zu Namenskonflikten kommt. Es gelten ansonsten nur wieder die üblichen Regeln für Token.

Sie werden auf Ihrem Rechner keine explizite Verzeichnisstruktur im JDK-Verzeichnis finden, die mit java oder javax beginnt. Das liegt daran, dass die Java-Systemklassen in gepackter Form ausgeliefert werden. Die jar-Datei rt.jar beinhaltet aber die gesamte Verzeichnisstruktur.

Wenn Sie nun aus der angegebenen Klasse eine Methode oder Variable benötigen, müssen Sie diese Pfadangabe um den Methoden- bzw. Variablennamen erweitern. Und zwar für jedes Element, das Sie aus der Klasse benötigen.

Damit wird die Lesbarkeit des Quelltexts beeinträchtigt und vor allem der Tippaufwand sehr hoch. Dies macht eine einfachere und schnellere Technik notwendig.

Um Elemente einer fremden Klasse innerhalb von Klassen wie eine Bibliothek nutzen zu können, kann man sie vorher importieren. Das geschieht durch eine import-Zeile, die vor der Definition irgendeiner Klasse in der Java-Datei stehen muss. Wenn Sie in einem Paket selbst eine andere Klasse importieren wollen, muss die import-Anweisung hinter der package-Anweisung stehen (darauf kommen wir gleich zurück).

Wir können also die obige Klasse importieren, indem wir die gesamte Punktnotation (das Paket, zu dem sie gehört, und die Klasse selbst) am Anfang einer Datei mit dem Schlüsselwort import angeben. Danach können wir Komponenten aus der Klasse direkt ansprechen. In unserem Beispiel würde es wie folgt aussehen: import java.awt.Frame;

In diesen Fall können Sie später im Quelltext auf die Klasse Frame einfach über Ihren Namen zugreifen. Etwa so: Frame a = new Frame();

Es gibt ein Java-Paket, das Sie nie explizit importieren müssen. Das ist java.lang. Dieses Paket wird immer automatisch importiert. Darin finden Sie so wichtige Klassen wie Object oder String.

Die import-Anweisung dient nur dazu, Java-Klassen über einen verkürzten Namen innerhalb der aktuellen Bezugsklasse zugänglich zu machen und damit den Code zu vereinfachen. Sie hat nicht den Sinn (wie die include-Anweisung in C), die Klassen zugänglich zu machen oder Sie einzulesen.

Es kann durchaus mehrfache import-Anweisung innerhalb einer Klassendefinition geben (wenn Sie mehrere Klassen importieren wollen). Sie müssen hinter der optionalen Anweisung package (im Fall von Paketen selbst, sonst fehlt die package-Anweisung) stehen. Wenn Sie nun mehrere Klassen aus dem gleichen Paket verwenden wollen, können Sie diese nacheinander importieren.

Wenn Sie allerdings mehrere Klassen aus einem Paket benötigen, arbeitet man sinnvollerweise mit Platzhaltern. Mittels einer solchen Wildcard - dem Stern * - kann das ganze Paket auf einmal importiert werden. Das geschieht wieder durch die import-Zeile, wobei nur der Klassenname durch den Stern ersetzt wird. Danach können Sie alle Klassen aus dem Paket direkt über ihren Namen ansprechen.

Das Sternchen importiert keine (!) untergeordneten Pakete. Um also alle Klassen einer komplexen Pakethierarchie zu importieren, müssen Sie explizit auf jeder Hierarchieebene eine import-Anweisung erstellen.

Der import-Befehl kennt im Prinzip drei Varianten:

import <package>.<klasse>;
import <package>.*;
import <package>;

Wir kennen die beiden ersten Formen ja schon. Die erste Form wird immer dann eingesetzt, wenn man gezielt auf eine Klasse zugreifen möchte und nur diese Klasse aus einem Paket benötigt. Der Vorteil ist, dass eine Klasse innerhalb des Source-Codes direkt über ihren Namen angesprochen werden kann.

Bei Fall 2 kann eine Klasse innerhalb des Source-Codes ebenfalls direkt über ihren Namen angesprochen werden. Der Vorteil ist das Einbinden aller Klassen aus einem Paket mit einer Anweisung.

Die dritte Form dient dazu, mit möglichst wenigen Anweisungen ein Paket oder sogar mehrere Pakete zu importieren. Allerdings kommt jetzt wieder zum tragen, dass keine untergeordneten Pakete importiert werden. Deshalb kann in dem Fall eine Klasse innerhalb des Source-Codes nicht direkt über ihren Namen angesprochen werden. Statt dessen muss vor dem Klassennamen das letzte Element aus dem Package-Namen per Punktnotation gesetzt werden. Dies ist sukzessive fortzusetzen, wenn es mehrere Paketebenen gibt, die nicht im import-Befehl angegeben wurden. In diesem Fall muss unter Umständen eine längere Pfadangabe die Klasse referenzieren.

Zwar darf ein Paket viele unterschiedliche Klassen enthalten (egal, ob logisch zusammenhängend oder nicht), jedoch darf jede Datei, die etwas mit dem Aufbau von Paketen zu tun hat, maximal eine öffentliche Klassendefinition enthalten.

Da das Importieren von Elementen in Java kein echter Import in dem Sinne ist, dass das resultierende Programm alle angegebenen Klassen irgendwie verwalten muss, sondern nur eine Pfadangabe, kann man beliebig viele Pakete und Klassen importieren, ohne dass das resultierende Programm größer oder sonst ineffektiver wird. Nicht explizit benötigte Klassen werden vom Compiler wegoptimiert. Das nennt man »type import on demand.«

6.11.2 Erstellung eines Paketes

Jedem Entwickler bleibt es selbst überlassen, wie er seine Klassen und Schnittstellen zu Paketen zusammenfasst und gruppiert. Eine Java-Datei wird ganz einfach zu einem Paket bzw. einem bestehenden Paket zugeordnet. Sie müssen nur ganz am Anfang der Datei als erste gültige Anweisung (auch vor der ersten Klassendefinition, aber abgesehen von Kommentaren) das Schlüsselwort package und den Namen des Pakets, gefolgt von einem Semikolon, setzen. Anschließend können Sie wie gewohnt Ihre Klassen definieren.

Beispiel:

package meinErstesPaket;
public class meineErsteKlasse {
...
}

Wenn sich innerhalb einer Quelldatei mehrere Klassendefinitionen befinden, dann werden alle Klassen dem durch das Schlüsselwort package angegebenen Paket zugeordnet.

Grundsätzlich spiegelt die Paketstruktur eine Verzeichnisstruktur wider. Wenn Sie also ein Paket mit dem Namen de schaffen wollen, benötigen Sie ein Unterverzeichnis dieses Namens, worin die zu dem Paket gehörenden Dateien gespeichert werden. Beim Kompilieren werden dann auch die .class-Dateien dort erstellt. Das Verfahren setzt sich mit eventuellen Unterverzeichnissen fort, wenn entsprechende Unterpakete erstellt werden sollen. Spielen wir das Verfahren in einem effektiven Beispiel durch.

Erstellen Sie in ihrem Java-Stammverzeichnis ein Verzeichnis de.

Erstellen Sie dort die nachfolgenden Dateien A.java und B.java.

package de;
public class A {
public int a=42;
}
Erste Datei in Paket de
package de;
public class B {
public String b="Die Antwort ist ";
}

Erstellen Sie im Verzeichnis de das Unterverzeichnis rjs.

Erstellen Sie dort die nachfolgende Datei C.java.
package de.rjs;
public class C {
public void ausgabe(String a, int b) {
  System.out.println(a + b);
 }  }

Gehen Sie zurück ins Stammverzeichnis.

Erstellen Sie dort die Datei PaketTest.java.

import de.*;
import de.rjs.*;
public class PaketTest {
public static void main(String args[]) {
  A k1 = new A();
  B k2 = new B();
  C k3 = new C();
  k3.ausgabe(k2.b, k1.a);
 }  }

Wenn die Datei PaketTest.java kompiliert wird, wird der Compiler die importierten Dateien automatisch mit übersetzen, wenn diese noch nicht kompiliert sind. Unter Umständen ist dabei notwendig, die individuelle Aufrufoption -classpath mit den Pfadangaben beim Compiler zu verwenden. Die früher notwendige Angabe CLASSPATH ist im JDK 1.2 bzw. 1.3 nicht mehr notwendig und kann höchstens den Compiler abschießen.

In den Kapiteln über das JDK und die Java-Neuerungen finden Sie mehr dazu.

6.11.3 Das anonyme Default-Paket

Wenn die package-Anweisung in einer Java-Datei fehlt, wird die Klasse einem voreingestellten Paket ohne Namen (einem so genannten anonymen Paket) zugeordnet. Die Klassen dieses Pakets können dann direkt von allen Klassen importiert werden, die im gleichen Verzeichnis stehen (und nur von diesen). Hierbei wird dann die import-Anweisung ohne weitere Qualifizierung angegeben oder ganz darauf verzichtet.

6.11.4 Zugriffslevel

Eine Klasse, die von anderen Klassen verwendet werden soll, muss eine der beiden nachfolgenden Bedingungen erfüllen:

1. Beide Klassen gehören zum selben Paket. Das ist beim anonymen Paket trivialerweise der Fall.
2. Eine Klasse aus einem fremden Paket, auf die zugegriffen werden soll, muss als public deklariert sein.

6.12 Zusammenfassung

Die Hauptbestandteile der Sprache Java lassen sich in folgende logische Struktur unterteilen:

1. Token
2. Typen
3. Ausdrücke
4. Anweisungen
5. Klassen
6. Schnittstellen
7. Pakete

Token heißt übersetzt Zeichen oder Merkmal und kann als Sinnzusammenhang verstanden werden. Es gib in Java fünf Arten von Token:

1. Bezeichner oder Identifier
2. Schlüsselworte
3. Literale
4. Operatoren
5. Trennzeichen

Daneben gibt es noch Kommentare und Leerräume. Java stellt Token im Unicode-Zeichensatz da.

Ein Datentyp gibt in der Computersprache an, wie ein einfaches Objekt (wie zum Beispiel eine Variable) im Speicher des Computer dargestellt wird. Er enthält normalerweise ebenfalls Hinweise darüber, die Operationen mit und an ihm ausgeführt werden können. Java besitzt acht primitive Datentypen.

Ausdrücke drücken einen Wert entweder direkt oder durch Berechnung aus. Es kann sich auch um Kontrollfluss-Ausdrücke handeln, die den Ablauf der Programmausführungen festlegen. Diese Ausdrücke können Konstanten, Variablen, Schlüsselworte, Operatoren und andere Ausdrücke beinhalten.

Operatoren sind in diesem Zusammenhang spezielle Symbole (eine der Schlüsselkategorien von Token in Java), die verwendet werden, um die Durchführung von Operationen (Manipulationen) an Variablen oder Werten auszuführen.

Anweisungen in Java werden der Reihe nach ausgeführt. Java hat viele verschiedene Arten von Anweisungen.

Klassen sind einer der zentralen Begriffe in Java. Da Java absolut objektorientiert ist, können Sie keine prozeduralen Programme schreiben. Man verwendet statt dessen Klassen. Klassen definieren den Zustand und das Verhalten von Objekten.

In der objektorientierten Sichtweise steht immer das Objekt im Mittelpunkt. Jedwede Operation ist in der Klasse implementiert, zu der ein Objekt gehört. Bei den Elementen einer Klasse, die als Felder bezeichnet werden, handelt es sich im Wesentlichen um Variablen, auf die die ganze Klasse und bei Bedarf auch andere Klassen zugreifen können. Die Ausführung einer Operation übernehmen die Objektmethoden. Soll eine bestehende Operation um eine neue Funktionalität erweitert werden, so werden die Veränderungen in der Klasse vorgenommen und die zugehörigen Methoden dort geschrieben. Die neue Form der Operation wird einfach als Erweiterung innerhalb der Klasse hinzugefügt. Nach außen erscheint das Objekt unverändert und lässt sich wie gehabt unter einem Namen ansprechen. Weitergehende Änderungen im Programm sind nicht notwendig. Daten und Methoden werden in Klassen gekapselt.

Klassen sind eine Art Beschreibung oder Bauplan für konkrete Objekte, die Objekte selbst sind im OO-Sprachgebrauch Instanzen dieser Klassen. Ein großer Teil der Attraktivität von Klassen und OOP basiert auf der Fähigkeit der Vererbung. Dies gibt einem die Möglichkeit, auf Basis alter Klassen (mit geringerem Aufwand als bei vollständiger Neuentwicklung) neue Klassen zu erstellen. Weil diese neuen Klassen die Eigenschaften einer anderen Klasse erben können, werden sie Subklassen, und die Klasse, aus der sie abgeleitet werden, Superklasse genannt.

Jedes Java-Programm besteht aus einer Sammlung von Klassen. Der gesamte Code, der bei Java verwendet wird, wird in Klassen eingeteilt. Jede Klasse definiert das Verhalten eines Objekts durch verschiedene Methoden. Verhalten und Eigenschaften können von der einen Klasse zur nächsten weitervererbt werden. Alle Klassen in Java haben eine gemeinsame Oberklasse, die Klasse Object. Diese wiederum verfügt nur über eine Metaklasse.

Methoden sind das Java-Gegenstück zu Funktionen in anderen Programmiersprachen. Sie bilden das Kernstück jeder Klasse und sind für die Handhabung aller Aufgaben zuständig, die von dieser Klasse durchgeführt werden sollen.

Schnittstellen sind eine Sammlung von Methodennamen ohne konkrete Definition. Während Klassen Objekte richtig definieren, helfen Schnittstellen bei der Definition von Klassen. Schnittstellen können lediglich ein paar Methoden und Konstanten definieren, die dann von einem Objekt implementiert werden. Schnittstellen können nur abstrakte Methoden und finale Felder definieren, eine Angabe der Implementierung dieser Methoden ist allerdings nicht möglich.

Objekte können eine beliebige Anzahl an Schnittstellen oder abstrakten Klassen implementieren.

Schnittstellen bilden in Java den Ersatz für die Mehrfachvererbung, die Java nicht unterstützt. Eine Klasse kann nur eine Superklasse, jedoch dafür mehrere Schnittstellen haben.

Schnittstellen werden auch dazu benutzt, eine bestimmte Funktionalität zu definieren, die in mehreren Klassen benutzt werden kann oder wenn die genaue Umsetzung einer Funktionalität noch nicht sicher ist.

Pakete bilden in Java Gruppierungen von Klassen und Schnittstellen. Sie sind das Java-Analogon zu Bibliotheken vieler anderer Computersprachen. Die Java-Laufzeitbibliothek, das Java-API, wird in Form von Paketen zur Verfügung stellt. Ein Paket von Java-Klassen enthält normalerweise logisch zusammenhängende Klassen und Schnittstellen.

Variablen sind Stellen im Hauptspeicher, in denen irgendwelche Werte gespeichert werden können. Dazu haben sie einen Namen, einen Datentyp und eben einen Wert. Java kennt nur drei unterschiedliche Arten von Variablen:

1. Instanzvariablen
2. Klassenvariablen
3. Lokale Variablen

Eine besondere Form von Variable ist in Java diejenige, die das Schlüsselwort final voranstellt. Es ist die Java-Form von einer Konstanten.

1

Bemerkenswert, weil ihn doch niemand außer Politikern will ;-).

2

So wie in unserem letzten Beispiel, wo wir darauf vorgegriffen haben.

3

Für kleinere Projekte - wie auch unsere Beispiele - kann man meist darauf verzichten.

4

Die Markierung des Wortes »diesem« soll deutlich machen, was this eigentlich bedeutet.

5

Wir haben den Einsatz schon bei Konstruktoren gesehen.

6

Sie werden manchmal sogar als - besondere - Klassen gesehen.

7

Falls sie gesetzt ist, kann das höchstens zu Fehlern führen. Statt dessen gibt es aber die Compiler-Option -classpath, die bei Bedarf gesetzt werden kann.


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 6 Java - die Hauptbestandteile der Sprache