Bisher bereits im Vorübergehen gestreift und einführend behandelt, sollen im folgenden einige Java-Grundlagen vertieft werden.
Anweisungen
Eine Java-Anweisung wird aus einer Zeichenmenge, die mit dem Semikolon (;) abgeschlossen wird gebildet. Bei der Eingabe des Java-Quellcodes müssen keine besonderen Formatierungsvorschriften (über die Berücksichtigung der Groß- und Kleinschreibung hinaus) eingehalten werden. Eine syntaktisch korrekte Java-Anweisung kann an beliebiger Spaltenstelle beginnen. Auch muss nicht zwingend für jede Anweisung eine neue Zeile genutzt werden; lediglich die Kennzeichnung des Anweisungsendes mit dem Semikolon ist verpflichtend. Zwischen den einzelnen Komponenten einer Anweisung können belibige Leerzeichen, Tabulatoren oder Zeilenendezeichen stehen. Mindestens ein sog. white space ist jedoch zur Abgrenzung der einzelnen Anweisungssegmente zwingend notwendig.
Aufgrund dieser Freiheitsgrade wird Java auch als format-freie Sprache bezeichnet.
In der Praxis haben sich jedoch Einrückungen als Strukturierungsmittel zu Nutzen der Übersichtlichkeit und die platzierung der Anweisungen in jeweils eigenen Zeilen bewährt.
Zusätzliche Leerzeichen steigern zwar den Platzbedarf des Quellcodes, werden jedoch vom Compiler ignoriert, und wirken sich nicht nachteilig auf das Verhalten (Ablaufgeschwindigkeit, Speicherplatzbedarf) des übersetzten Bytecodes aus.
Ausdrücke
Liefert eine Anweisung einen Wert zurück, so wird sie als Ausdruck bezeichnet.
Rückgabewerte (engl. return value) können durch Programmkonstrukte abgefragt -- und auf sie reagiert -- werden. Hierzu müssen die durch den Ausdruck retournierten Werte ausgewertet (siehe III.3.1) oder in einer Variablen abgelegt werden.
Ist der Rückgabewert nicht von Interesse so kann er ignoriert werden.
Blockstrukturen
Geschweifte Klammerung ({ }) umschliessen Blöcke. Ein solcher Block besteht aus einer Menge konkreter Anweisungen, die wie eine einzige Anweisung behandelt werden.
Variablen
Analog zur Definition der Eigenschaften einer Klasse durch Attribute (--> II.2.2.3) können auch innerhalb von Methoden benannte Speicherplätze -- sog. Variablen -- zur Aufnahme von veränderlichen Werten definiert werden. Diese Speicherplätze werden bei jedem Methodeneintritt (durch Aufruf der zugehörigen Operation) neu angelegt und beim Verlassen der Methode wieder freigegeben. Dies bedeutet: in Variablen abgelegte Werte sind nach Methodenaustritt verloren. Als synonym verwendete Bezeichnung hat sich der aus der prozeduralen Programmierung übernommene Terminus lokale Variablen eingebürgert.
Syntaktisch geschieht die Variablendefinition wie die der Attribute, durch Angabe des Datentyps, gefolgt vom eindeutigen Namen des Speicherplatzes. Hinsichtlich der Benennung gelten dieselben lexikalischen Restriktionen wie für Attribute.
Kommentare
Kommentare -- erklärender Text innerhalb des Quellcodes -- stellt primär unproduktiven Code dar. Jegliche Kommentare werden beim Compilierungsvorgang ignoriert und finden keinen Eingang in den übersetzten Bytecode.
Die Kommentierung erhöht jedoch die Lesbarkeit des entstehenden Programmtextes und vereinfacht damit den späteren Nachvollzug des zugrundeliegenden Gedankengangs für Dritte. Darüberhinaus werden in Kommentaren wichtige Details zu gewählten Algorithmen und Lösungswegen dokumentiert, um spätere Wartungsarbeiten und Weiterentwicklungen zu erleichtern.
Java unterstützt drei verschiedene Kommentierungsmöglichkeiten durch spezielle Kommentarformate:
//
). Der so eingeleitete Kommentar erstreckt sich, ohne explizites Endezeichen, bis zum Zeilenende./*
). Das Kommentarende wird durch einen Stern gefolgt von einem Querstrich (*/
) gekennzeichnet. /**
) und enden wie mehrzeilige Kommentare mit */
.Beispiel 1 (Quellcodedatei: KommentarDemo.java)
/**
Allgemeine Beschreibung der Klasse...
@see Verweis auf anderes Package oder andere Klasse
@author Mario Jeckle
@version Erster Versuch 0.1ß
*/
public class KommentarDemo //dies ist ein einzeiliger Kommentar
{
/**
Beschreibung zum Attribut Wert, das mit 42 initialisiert wird...
@see "Douglas Adams: Per Anhalter durch die Galaxis, S. 158"
*/
protected int Wert = 42;
public static void main(String[] argv)
/* Die main-Methode bildet den Startpunkt jedes Java-Programms
deshalb darf sie nicht vergessen werden */
{
//hier steht die Implementierung...
} //end main() -- Ende der main-Methode
/**
@param Zwei ganzzahlige Summanden vom Datentyp int
@return Summe der beiden Zahlen
@see "Zur Definition der Summe von ganzen Zahlen siehe Bronstein, Semendjajew"
*/
public static int berechneSumme(int summand1, int summand2)
{
return summand1+summand2;
} //end berechneSumme
}// end class KommentarDemo -- auch das ist ein Kommentar
Der JavaDoc-Generator berücksichtigt u.a. folgende Kommentare (eine vollständige Aufstellung kann der JDK-Dokumentation entnommen werden):
|
Hinweis: Die gesamte Dokumentation zum JDK von Sun wurde ausschiesslich aus dem Java-Quellcode und den darin abgelegten formalen JavaDoc-Kommentaren automatisch generiert.
Der JavaDoc-Generator erstellt nach dem Aufruf mit: javadoc -version -author -private KommentarDemo.java
verschiedene Dokumentationsdateien im HTML-Format.
Nachfolgend die erstellten Seiten für das Programm aus Beispiel 1
Operatoren dienen zur Manipulation bereitgestellter Werte. Ergebnis einer Operator-Anwendung ist ein Resultat eines feststehenden Typs. Die zu manipulierenden Eingangswerte, Operanden genannt, müssen eine Ausprägung eines zum Operator kompatiblen Typs sein. Mit anderen Worten: der Operator muss als Verknüpfung auf den Eingabedaten definiert sein.
Java bietet zunächst die -- einfachen -- bekannten binären arithmetischen Operationen:
Addition +
Subtraktion -
Multiplikation *
Division /
Modulus %
Diese Operatoren sind auf den ganzzahligen Primitivtypen byte
, short
, int
, long
ebenso wie auf den Fließkommatypen float
und double
, nicht jedoch für Wahrheitswerte, definiert. Darüberhinaus sind die arithmetischen Grundoperationen auch auf char
Datentypen anwendbar, denn diese werden hierzu als 16-Bit Zahlen (analog short
) behandelt.
Hinsichtlich des gelieferten Ergebnistyps geltelten folgende Festlegungen:
double
so wird auch das Ergebnis als 64-Bit-Zahl zurückgeliefert (der zweite Operand wird ggf. vor Ausführung der Operation (verlustlos) zu double
erweitert).float
) als Rückgabewert geliefert. Auch hierzu wird ggf. eine (verlustlose) Typerweiterung des nicht als float vorliegenden Operanden vorgenommen.Als binäre Vergleichs-Operatoren stehen zur Verfügung:
|
Zur direkten Manipulation der den Primitivtypen zugrundeliegenden Binärzahlen werden folgende bitweise agierenden Operatoren angeboten:
|
Ergänzend definiert Java, in Anlehnung an die Programmiersprache C, noch einige abkürzende Schreibweisen für gebräuchliche Operationen und Zuweisungen:
|
Operatorpräzedenz:
Analog der bekannten Regel Punkt vor Strich sind in Java ebenfalls Vorrangsregeln für die vorgestellten Operatoren definiert. Zunächst werden die Operatoren gemäß der folgenden Übersicht ausgewertet. Operationen mit Operatoren derselben Präzedenzstrufe in einer Zeile werden von links nach rechts ausgewertet.
|
Das in Methoden ausprogrammierte dynamische Verhalten (der Programmablauf) kann durch verschiedene Kontrollstrukturen beeinflusst und gesteuert werden. "Früher" war der alternative Begriff Kontrollfluss für die Abarbeitungsschritte eines Programms gebräuchlich, jedoch führt dessen Verwendung im Kontext potentiell mehrerer (unabhängiger) Kontrollflüsse eines Programms zu Verwirrungen. Wir werden deshalb den Begriff Ablaufsteuerung (engl. flow of control) bevorzugen.
Erst die Mittel zur Steuerung des Ablaufs "erwecken ein Programm zum Leben". Denn erst mit ihnen werden nicht-deterministische Elemente (d.h. Elemente deren Verhalten nicht statisch vorhersagbar ist) eingeführt. Mit ihnen ist die Prägung dynamischen Verhaltens, durch reagieren auf konkrete Situationen im Programm möglich.
if
-StatementSemantik:
Oftmals ist es erforderlich an Bedingungen geknüpfte Entscheidungen zu treffen. Konkret wird anhand einer angegebenen (für die Maschine auswertbaren) Bedingung entschieden, ob diese erfüllt ist oder nicht. Abhängig vom Ausgang dieser Bedingungsauswertung wird entweder die angegebene Alternative für den "wahr-Fall" (die Bedingung ist erfüllt) oder den "falsch-Fall" (Bedingung ist nicht erfüllt) weiter verfolgt.
Technisch gesprochen: Der Ablaufsteuerung werden, abhängig von einer im Programm auswertbaren Bedingung, zwei alternative Fortsetzungen angeboten.
Java-Syntax:
Die Bedingungsauswertung wird in Java durch das if
-Konstrukt realisiert.
if (Bedingung)
{
//true-Fall: Bedingung ist erfüllt
}
else
{
//false-Fall: Bedingung ist nicht erfüllt
}
Der mit else
eingeleitete Alternativteil ist optional, und muss nicht zwingend angegeben werden. Ist eine ohne Else-Teil spezifizierte Bedingung falsch, so wird das Programm mit der nächsten auf die if-Anweisung folgenden Anweisungszeile fortgesetzt.
Hinweis: Die Blockstruktur, welche die true- und false-Alternativen klammen müssen (strenggenommen) nur angegeben werden, wenn die beiden Alternativen jeweils aus mehr als einer Anweisung bestehen. Jedoch hat es sich in der Praxis als ratsam erwiesen auch Alternativen, aus nur einer einzigen Anweisung in Block-Klammern einzuschliessen, um eine potentielle spätere Fehlerquelle bei Erweiterung des angegebenen Codes der Alternativen von vorherein auszuschliessen.
Denn:
byte a = 1;
if (a == 1)
System.out.println("a ist gleich 1, setzte a auf 0");
a = 0;
else
System.out.println("a ist gleich 0, setzte a auf 1");
a = 1;
System.out.println("Wert von a="+a);
Diese Anweisungsfolge führt -- anders als beabsichtigt, und zu erwarten, zunächst zu einem Compilierungsfehler:
(als Programmname ist iftest.java
angenommen)
iftest.java:10: 'else' without 'if'.
else
^
Der Java-Compiler hat bei Überprüfung der Java-Syntax zum Übersetzungszeitpunkt festgestellt, dass eine else
quasi "mitten" im Programm auftritt. Dies ist das Resultat der fehlenden Blockklammern. Da die Java-Syntax zwischen if
und else
genau ein Statement erlaubt (Zur Erinnerung: Ein in geschweifte Klammern eingeschlossener Block wird wie eine einzige Anweisung behandelt) zweite auf den Bedingungsteil folgende Anweisung als Fortsetzung des Programms (und damit implizit als Ende der if
-Anweisung) behandelt. Und "mitten im Programm" (lies: außerhalb von if
-Anweisungen) dürfen keine else
-Zweige auftreten.
Beispiel 2 einer korrekten if-Anweisung:
if (a == 1)
{
System.out.println("a ist gleich Eins");
} //end if-clause
else
{
System.out.println("a ist ungleich Eins");
} //end else-clause
Zugelassende Bedingungen
sind alle Java-Ausdrücke, die als Boole'scher Ausdruck interpretiert werden können. Das sind somit:
boolean
Der von der Methode gelieferte Rückgabewert wird dann im Programmablauf an die Stelle des konkreten Aufrufs platziert. Selbstverständlich können ausschliesslich solche Methoden in Bedingungen zur Auswertung angegeben werden, die als Rückgabewert den Primitivtyp boolean
liefern. (Anmerkung: auch der Wrapper-Type java.lang.Boolean
ist nicht gestattet).
Ergänzend sei noch die (selbstverständlich existierende) Möglichkeit des Vergleichs von Konstanten-Termen sowie einfacher Literale hervorgehoben. Diese aus Sicht der Logik uninteressanteste Variante wird in der Praxis oft zur Entwicklungszeit eingesetzt, um Bedingungen schnell zu testen.
Beispiele: Die Bedingung if (1 == 1)
ist immer wahr, ebenso ist die Kurzform if (true)
gebrächlich.
In einer If-Anweisung können mehrere Bedingungen gebündelt werden:
Kann die Entscheidung für eine der beiden angebenen Alternativen nur auf Basis mehrerer auswertbarer Bedingungen getroffen werden, so können diese -- mit den bekannten logischen Operatoren verknüpft -- angegeben werden.
Beispiel 3:
int a = 11;
if (a >10 && isPrime(a) == true && 2 == 2)
{
System.out.println("a ist eine Primzahl, die grösser als 10 ist");
} //end-if
Das Beispiel 3 zeigt eine Bedingung, in der sowohl eine (im Beispiel 3 nicht dargestellte Methode (mit Rückgabewert boolean
) isPrime
aufgerufen wird, als auch eine (immer wahre) Konstantenauswertung (2==2
) vorgenommen wird.
Mit dieser Mimik läßt sich auch die oft benötigte Wertebereichsprüfung (z.b. in get- und set-Methoden) einfach realisieren:
Beispiel 4:
Ein Attribut budget
einer Klasse Projekt
darf nur zwischen 100 und 1000 liegende Werte annehmen (100<=budget>=1000). Beim Versuch einen ungültigen Wert zu setzen soll eine Fehlermeldung ausgegeben werden:
class Projekt
{
int budget;
void setBudget(int newBudget)
{
if (newBudget <= 1000 && newBudget >= 100)
{
budget = newBudget;
} //end-if
else
{
System.out.println(newBudget+" liegt außerhalb der erlaubten Grenzen");
} //end-else
} //end setBudget
} //end class Projekt
If-Anweisungen können auch geschachtelt werden
Innerhalb einer der beiden nach der Bedingung aufgeführten Alternativen gelten die selben syntaktischen Regeln wie im umgebenden Java-Programm. Das bedeutet insbesondere es dürfen weitere Alternativen, die erst nach vorhergehender Auswertung der im If-Teil formulierten Bedingung geprüft werden sollen, als vollständige If-Statements angegeben werden.
Beispiel 5:
if (schulnote == 1)
{
System.out.println("sehr gut");
} //end-if
else
{
if (schulnote == 2)
{
System.out.println("gut");
} //end-if
else
{
if (schulnote == 3)
{
System.out.println("befriedigend");
} //end-if
else
{
if (schulnote == 4)
{
System.out.println("ausreichend");
} //end-if
else
{
if (schulnote == 5)
{
System.out.println("mangelhaft");
} //end-if
else
{
if (schulnote = 6)
{
System.out.println("ungenügend");
} //endif
else
{
System.out.println("ungültiger Wert");
} //ene-else
} //end-else
} //end-else
} //end-else
}//end-else
}//end-else
Bei Analyse des Codes fällt zunächst dessen Unübersichtlichkeit ins Auge. Darüberhinaus zeigt sich der notwendige Aufwand zur Ermittlung des Fehlerfalles. Zwar hätte sich dies mit einer zusätzlichen Überprüfung vor Untersuchung auf den tatsächlichen Wert realiseren lassen, jedoch würde durch dieses Vorgehen lediglich das letzte else
eingespart.
Ergänzung: der Bedingungsoperator
Eine Alternative zur Verwendung der Schlüsselworte if
und else
bietet eine von C übernommene verkürzte Schreibweise. Sie erlaubt jedoch die Einflussnahme auf den Rückgabewert. Während dieser bei allen If
-Konstrukten immer true
oder false
ist, kann er hier beliebig gesetzt werden.
Syntaktisch handelt es sich um einen ternären Operator, der neben der Bedingung auch die beiden möglichen Rückgabewerte als Parameter erwartet. Sein Aussehen kann wie folgt beschrieben werden:Test ? Ergebnis im true-Falle : Ergebnis im false-Falle
Beispiel 6:int smaller = x /lt; y ? x : y;
Als Resultat wird der Variablen smaller
ein Wert zugewiesen. Im true-Falle (d.h. es gilt x < y
) ist dies x
, andernfalls (d.h. es gilt x > y
oder x = y
) ist dies y
.
Jede mit dem Bedingungsoperator formulierte Anweisung läßt sich ohne Informationsverlust in eine If
-Anweisung überführen.
Im Beispiel 6 wäre dies:
if (x < y)
{
smaller = x;
} //end-if
else
{
smaller = y;
} //end-if
Der Bedingungsoperator führt zwar zu kompakterem Quellcode, jedoch zu Lasten der Les- und Nachvollziehbarkeit. Im Hinblick auf spätere Wartungen und Erweiterungen des Programmtextes sollte auf dieses Konstrukt verzichtet werden.
Die Mehrfachselektion/Das switch
-Statement
Semantik:
Ein gut geeignetes Mittel zur einfachen und sicheren Realisierung von mehrfachen Fallunterscheidungen stellt die swich
-Anweisung dar.
Sie ist im wesentlichen eine Verallgemeinerung des if
-Statements.
Java-Syntax:
switch (Bedingung)
{
case Alternative1:
Anweisungen...
break;
case Alternative2:
Anweisungen...
break;
... //weitere case-Anweisungen
default:
Standardfall
} //end-switch
Die im switch
-Konstrukt mögliche Bedingung erweitert die Fähigkeiten der if
-Anweisung dahingehend, dass die Bedingung nicht mehr ausschliesslich als Boole'sche Operation interpretiert wird, sondern beliebige int
-Werte als Bedingungsergebnis zugelassen sind. Da jedoch auch einzelne Zeichen (Primitivtyp char
in Java intern wie eine Ganzzahl vom Primitivtyp short
behandelt werden, kann die Auswertung auch hinsichtlich eines einzelnen Zeichens geschehen.
Auffallend an der Syntax des switch
-Statements ist der Verzicht auf die von der if
-Anweisung her gewohnten Blockstrukturen. Stattdessen werden die einzelnen Alternativen durch ein break
abgegrenzt. Die Gründe hierfür liegen in der engen Verwandtschaft von Java zu C; bereits dort existiert das syntaxgleiche Konstrukt. Aber diese Syntax hat auch ihr gutes: Sie vermittelt uns einen guten Einblick in die Abarbeitung der Anweisung durch die Maschine. Anstatt wie beim if
vollständige Blöcke vorzugeben, von denen entweder der erste oder der zweite abgearbeitet wird haben wir es hier mit potentiell 216 Alternativen (eben dem vollständigen Werteumfang des Primitivtypen int
zu tun). Das den Aufwand, der beim Kaskadieren der else
-Blöcke gezeigt wurde, zu vermeiden, (und damit die Ausführungsgeschwindigkeit zu erhöhen) wird der benötigte Code-Teil direkt angesprungen. Die benötigte Sprungtabelle läßt sich durch Entnahme der (statischen) Fälle aus dem Code -- sie sind nach dem Schlüsselwort case
angegeben -- leicht aufbauen. Ist der zutreffende Code einmal lokalisiert, so wird er ausgeführt, bis der Interpreter auf ein weiteres Schlüsselwort trifft. Dieses mit break
bezeichnete Schlüsselwort weisst den Interpreter an, den Kontrollfluss wieder zurück auf die nächste auf die komplette switch
-Anweisung folgende Programmzeile zu setzen. Die benötigte Programmzeile ist diejenige, die auf die schliessende geschweifte Klammer folgt.
Zugegebenermassen ist es nicht zwingend notwendig, derart maschinennahe Betrachtungen soweit zu betonen, dass sie sich im Quellcode der höheren Programmiersprache niederschlagen. Im Falle von Java wurde jedoch dieses Konstrukt aus "humanen Kompatibilitätsgründen", d.h. um existierenden Programmierern (mit C-Erfahrung) den Umstieg auf Java zu erleichtern, beibehalten.
Jedoch darf der durch break
gekennzeichnete Abschluss einer Alternative nicht weggelassen werden, da sonst die Ausführung bis zum Auftreten des nächsten break
s, sogar über die anderen Alternativen hinaus, fortgesetzt würde. Hinweis: Besonders heimtückisch ist das Vergessen eines einzigen Breaks inmitten einer Menge von Alternativen, dies führt zu unschönen Ausführungsfehlern, da das Programm syntaktisch korrekt ist. Achten Sie bei der Erstellung Ihres Codes immer auf die korrekte Terminierung der einzelnen Alternativen einer switch
-Anweisung. Das gezielte durchlaufen des nachfolgenden Falles wird mit fall through bezeichnet, und mag für manche Situationen wünschenswert sein. In allen Fällen führt es jedoch zu unübersichtlicherem Code, da der beabsichtigte Kontrollfluss nicht mehr eindeutig erkennbar ist.
Durch default
wird der "Sonst-Fall" eingeleitet. Diese Alternative wird bei einem Bedingungswert für den keine explizite Alternative spezifiziert wurde angesprungen.
Beispiel 7 (Umsetzung des Beispiels 5):
switch (schulnote)
{
case 1:
System.out.println("sehr gut");
break;
case 2:
System.out.println("gut");
break;
case 3:
System.out.println("befriedigkend");
break;
case 4:
System.out.println("ausreichend");
break;
case 5:
System.out.println("mangelhaft");
break;
case 6:
System.out.println("ungenügend");
break;
default:
System.out.println("ungültiger Wert");
} //end-switch
Prinzipiell ist es möglich jedes if
-Statement in eine gleichmächtige switch
-Anweisung zu überführen. Hierbei ist lediglich auf die notwendige Abbildung des Boole'schen Ergebnistyps auf die fuer switch
benötigte int
-Zahl zu achten. Das nachfolgende Beispiel 8 illustriert ein mögliches Vorgehen in diesem Zusammenhang:
Beispiel 8:
public class switchTest
{
public static void main(String[] argv)
{
final int TRUE = 1231;
final int FALSE = 1237;
Boolean bedingung = new Boolean( 5 > 7);
switch ( bedingung.hashCode() )
{
case TRUE:
System.out.println("true");
break;
case FALSE:
System.out.println("false");
break;
} //end-switch
} //end main()
}//end switchTest
Anmerkungen:
Das Beispiel 8 nutzt die hashCode
-Methode auf Objekten der Standardklasse Boolean
(dem Wrapper-Typ für boolean
), die eine Abbildung des Wahrheitswertes auf einen int
-Wert erlaubt. Die im If-
Teil der Selektionsanweisung platzierte Bedingung wird in Beispiel 8 als Wert einem neuen Boolean
-Objekt zugewiesen. Der Wahrheitswert wird mittels der erwähnten Standardmethode in eine eindeutige int
-Repräsentation abgebildet.
Die beiden Konstanten dienen lediglich der besseren Lesbarkeit, da hashCode
true
auf 1231 bzw. false
auf 1237 abbildet.
Der default
-Fall kann hier entfallen, der gesamte mögliche Wertebereich von Boolean
durch explizite Alternativen abgedeckt wird.
Schleifen dienen der mehrfachen Ausführung identischer Codeteile. Durch entsprechende Belegung Schleifen-interner Variablen, kann so bei jedem Schleifendurchlauf verschiedenes Verhalten realisiert werden.
Semantik:
Eine For-Schleife erlaubt eine genau festgelegte Anzahl von Schleifendurchläufen.
Java-Syntax:
for (Initialisierung; Fortsetzungbedingung; Inkrement)
{
Anweisungen
}
true
, so wird die Schleife durchlaufen.Beispiel 9:
for (i = 1; i < 10; i=i+1)
{
System.out.println(i);
}//end-for
Ausgabe:
1
2
3
4
5
6
7
8
9
Hinweis:
Die Zahl 10 wird nicht ausgegeben, da nach Erhöhung des Schleifenzählers (nach dem neunten Durchlauf) auf 10 die Fortsetzungsbedingung mit 10 < 10 nicht mehr erfüllt.
Angabe mehrerer Schleifenparameter
Durch Spezifizierung mehrerer Werte für die einzelnen Schleifenparameter (Initialisierung, Fortsetzungsbedingungen, Inkremente) können auch komplexere Schleifen realisiert werden. In diesem Fall werden die einzelnen Komponenten durch Komma voneinander getrennt.
Beispiel 10:
public class fortest
{
public static void main(String[] argv)
{
int i;
int a;
int b;
for (i=1, a=1, b=1; a>=b; i++, a=i*10, b=i*i)
{
System.out.println(i+" mal 10 ist größer als "+i+" Quadrat");
} //end-for
}//end main()
}//end class fortest1
Ausgabe:
1 mal 10 ist größer oder gleich als 1 Quadrat
2 mal 10 ist größer oder gleich als 2 Quadrat
3 mal 10 ist größer oder gleich als 3 Quadrat
4 mal 10 ist größer oder gleich als 4 Quadrat
5 mal 10 ist größer oder gleich als 5 Quadrat
6 mal 10 ist größer oder gleich als 6 Quadrat
7 mal 10 ist größer oder gleich als 7 Quadrat
8 mal 10 ist größer oder gleich als 8 Quadrat
9 mal 10 ist größer oder gleich als 9 Quadrat
Die einzelnen Schleifenparameter sind optional. So erzeugt for(;;)
eine Endlosschleife ohne Schleifenzähler
Das Beispiel 10 zeigt die Mächtigkeit der For
-Schleife, hinsichtlich der Formulierung leistungsfähiger Abbruch- bzw. Fortsetzungsbedingungen. Ist ein Schleifenzähler nicht notwendig, so bieten sich als Varianten Schleifen mit expliziter Abbruchbedingung an.
Dieser Schleifentyp wird durch die while-
und do-
Schleifen zur Verfügung gestellt. Als Besonderheit bietet dieser Schleifentyp zwei unterschiedliche Syntaxvarianten an.
Java-Syntax:
do
{
//Anweisungen
} while (Fortsetzungsbedingung);
oder alternativ:
while (Fortsetzungsbedingung)
{
//Anweisungen
}
Je nach Stellung der im while
-Teil formulierten Fortsetzungsbedingung wird zwischen kopfgesteuerten-Schleifen (im Falle der Auswertung vor dem Schleifendurchlauf) bzw. fußgesteuerten-Schleifen (im Falle der Auswertung nach dem Schleifendurchlauf) unterschieden. Für die Fortsetzungsbedingungen gelten dieselben syntaktischen Festlegungen wie bei For
-Schleifen.
Als Hauptunterschied sei folgendes festgehalten:
Der bekannteste Repräsentant der Familie der (unbedingten) Sprunganweisungen ist das GOTO
-Konstrukt. Eigentlich sind diese Arten der Einflussnahme auf den Programmablauf eher verpönt (vgl. Edsger Dijkstras (mittlerweile lägendären) Leserbrief Go To Statement Considered Harmful (in: Communications of the ACM, March 1968, Vol. 11, No. 3, pp. 147-148)).
Prinzipiell lassen sich alle notwendigen Ablaufsteuerungsschritte mit den bekannten Sprachelementen Selektion (if
, auch als bequemere Mehrfachselektion (switch
)), Iteration (die verschiedenen Schleifenkonstrukte wie for
, do..while
bzw. while...do
) ausdrücken. Der bequeme unbedingte Sprung (engl. unconditional jump oder unconditional branch) stellt somit nur eine Vereinfachung eines bedingten Sprunges mit immer-wahrer Bedinung dar.
Das break
-Schlüsselwort zum Verlassen einzelner Alternativen einer Mehrfachselektion wurde bereits in Abschnitt III.1.4.1 diskutiert. Genaugenommen handelt es sich bei break
um einen unbedingten Sprung aus der aktuellen Struktur in die umgebende. Das Beispiel der switch
-Anweisung zeigt, dass es sich dabei nicht zwingend um explizite Blockstrukturen handeln muss.
Durch break
können auch beliebige Schleifen vor eintreten der Abbruchbedinung verlassen werden.
Mit Ausnahme der Anwendung als Terminierung einer Alternative innerhalb einer switch
-Struktur, die keine Alternativsyntax zulässt und vorsieht, sollte jedoch auf den Gebrauch von break
verzichtet werden, und eine Bedingung zum vorzeitigen Verlassen einer Schleife entweder in einen Fehlerfall überführt (und dann durch Exception Handling abgearbeitet werden) oder in die Abbruchbedingung der Schleife integriert werden.
Beispiel 11:
for (int i=0; i<10; i++)
{
System.out.println( i );
if ( i==7 )
{
break;
} //endif
} //end for
Die Abarbeitung der Schleife wird beim Zählerstand 7 abgebrochen.
Die Anweisung continue
veranlasst das System alle in einer Schleife folgenden Anweisungen zu ignorieren und unverzüglich die boole'sche Fortsetzungsbedingung auszuwerten.
Beispiel 12:
for (int i=0; i<10; i++)
{
if ( i==7 )
{
continue;
System.out.println("test"); // never gets here!
} //end if
else
{
System.out.println( i );
} //end else
} //end for
Die Schleife gibt alle Zahlen, mit Ausnahme der 7 am Bildschirm aus. Die Übersetzung liefert für Zeile 6 jedoch den Fehler Statement not reached.
, da alle auf continue
folgenden Anweisungen ignoriert -- und folglich niemals ausgeführt -- werden.
Zum Abschluss sei noch das bereits bekannte return
-Statement in den Zusammenhang der Sprunganweisungen gestellt. Mit ihm kann jede mit Rückgabewert deklarierte Methode an beliebiger Stelle verlassen werden. Dem Aufrufer wird der Wert des auf return
folgenden Ausdrucks übergeben. Der Compiler prüft an dieser Stelle die notwendige Typkorrektheit.
Beispiel 13
int compareInt(int a, int b)
{
if ( a<b )
{
return a;
}
else
{
if ( b<a )
{
return b;
} //endif
else
{
return 0;
} //end else
}//end else
} //end compareInt
Aufruf:
int result = compareInt(3,4);
Die Methode compareInt
liefert bei Ungleichheit der übergebenen Parameter den kleineren, andernfalls 0, an den Aufrufer zurück. Je nach Gültigkeit der geprüften Bedingung wird das if
-Konstrukt und die umgebende Methode mit den spezifizierten Rückgabewert verlassen.
Bei der Disskussion der Rückgabewerte einer Methode sind wir bereits auf mögliche Fehlersituationen und ihre Kommunikation an den Aufrufer eingegangen. Dort zeigte sich auch erstmals die Problematik im Allgemeinen zwischen erfolgreicher Ausführung -- verbunden mit dem entsprechenden zurückgelieferten Ergebnis -- und fehlerhafter Abarbeitung -- mit dem entsprechenden Fehlercode -- zu unterscheiden. Relativ leicht ist uns dies bei Methoden vom Typ boolean
gefallen, da sich dort über true
und false
eine intuitive Semantik herauslesen lässt (etwa: true
bei erfolgreicher Abarbeitung, und false
für die Gesamtheit der möglichen Fehler).
Als problematischer erweisen sich alle Methoden mit anderen Rückgabewerten (etwa die Primitivtypen, aber auch Objekte). Hierbei fällt es zunehmend schwer genau einen Rückgabewert (z.B. 0
) durchgehend als Fehlerkennzeichnung durchzuhalten. Denken Sie hierbei an diverse mathematische Formeln, die Null als legitimes Ergebnis liefern können.
Zusätzlich zeigt sich schon bei kleineren Programmierprojekten, dass die Übersichtlichkeit des entstehenden Codes durch die Vermischung von Ablauflogik und Fehlerkontrolle deutlich leidet.
Intuitiv ist auch die Möglichkeit des Auftretens identischer Fehler (d.h. Fehler identischer Ursache) an verschiedenen Stellen der Programmausführung klar. Das bedeutet, in der Umsetzungsphase wird es u.U. notwendig gleiche Fehlerabfragen und -behandlungen an verschiedenen Stellen auszucodieren. Die damit einhergehende Redundanzproblematik, von der unnötigen Aufblähung des Quellcodes einmal abgesehen, offenbart sich spätestens in späteren Wartungszyklen.
Abschliessend sei noch auf die Problematik des Erfassens und Reagierens auf alle denkbaren, d.h. praktisch möglichen, Fehlersituationen hingewiesen. Denn prinzipiell ist die Anzahl der möglichen Fehlschlagsursachen einer Methode zwar endlich, jedoch immernoch sehr viel, so dass oftmals ein einziger Rückgabewert nicht die angestrebte Aussagekraft besitzt -- und besitzen kann.
Jedoch ist eine möglichst genaue Information über die Fehlerursache für die weitere Behandlung unabdingbar.
Deshalb ist es naheliegend nach einem Mechanismus zu suchen der die genannten Kriterien erfüllt. Im einzelnen sind dies:
In den meisten objektorientierten Programmiersprachen, so auch in Java, wird aus diesem Grunde ein solcher Mechanismus eingeführt.
Hauptprimitive ist hierbei die Ausnahme (engl. Exception). Konzeptionell ist sie vergleichbar mit den Botschaften (vgl. Kapitel 3.2.5), durch die Methoden als Implementierung einer Operation aufgerufen werden.
Eine Ausnahmebehandlungsroutine wird immer dann ausgeführt, wenn eine solche Ausnahme generiert wurde. Dies kann sowohl durch das Laufzeitsystem als auch den Programmierer selbst (z.B. bei Erkennen einer Fehlersituation) geschehen.
Java-Syntax:
Strukturell ist eine Ausnahme ein Java-Objekt (java.lang.exception
). Dieses wird bei Bedarf entweder durch das Laufzeitsystem oder den Programmierer erzeugt.
Die grundsätzliche Möglichkeit einer Methode eine bestimmte Ausnahme zu erzeugen wird bereits in der Methodendeklaration angegeben:
public class TestClassWithException
{
public void aMethodWithException() throws MyException
{
//...
} //end aMethodWithException
} //end class TestClassWithException
In diesem Falle muss kein try
- und catch
-Block zum Auffangen der Exception im Programm angegeben werden.
1 Auffangen einer existierenden Exception
try
{
//beliebige Anweisungen, die Fehler generieren können
} //end try
catch (Exception e)
{
//Fehlerbehandlung
} //end catch
finally
{
//weitere Anweisungen
} //end finally
Im try
-Block wird diejenige Codesequenz eingebettet, die potentiell eine Ausnahmebedingung auslösen kann. Im zwingend darauf folgenden catch
-Block werden die evtl. im vorangegangenen try
-Block ausgelösten (auch: geworfenen -- vom englischen thrown) Exceptions aufgefangen. Jeder catch
-Block kann nur genau, durch den Typ des Übergabeparametes definierte, Ausnahme auffangen.java.lang.Exception
bildet die Elternklasse aller Java-Ausnahmen. Dies bedeutet ein so deklarierter catch
-Block fängt alle im try
-Block auftretenden Exceptions zur Behandlung auf.
Nach allen durch try
umschlossenen Anweisungen wird der finally
-Block abgearbeitet. Dies erfolgt unabhängig davon, wie die vorhergehenden Anweisungen abgeschlossen wurden, sei es regulär, durch Ausnahmen oder durch den Ablauf beeinflussende Anweisungen wie return
oder break
. An dieser Stelle sollten Anweisungen platziert werden, die unabhängig von möglicherweise eingetretenen Fehlersituationen immer ausgeführt werden müssen. Beispielsweise die Freigabe belegter Ressourcen und Speicherbereiche.
Beispiel 14:
public class ArrayExceptionTest
{
public static void main(String[] argv)
{
try
{
System.out.println("argv[1]=" +argv[1] );
} //end try
catch (Exception e)
{
System.out.println("exception caught: "+ e.getMessage() );
e.printStackTrace();
} //end catch
} //end main()
}//end class ArrayExceptionTest
Beispiel 14 illustriert den praktisch häufigsten Fall, das Auffangen einer vordefinierten Ausnahme. Das Programm nutzt die bereits bekannte Auswertung von Kommandozeilenargumenten.
Werden weniger als zwei Aufruf-Argumente angegeben, so erzeugt das Laufzeitsystem beim Versuch die Komponente argv[1]
auszugeben eine java.lang.ArrayIndexOutOfBoundsException: 1
Ausnahme.
Die beiden an jede Exception vererbten Methoden getMessage
und printStackTrace
liefern eine genauere Beschreibung des aufgetretenen Fehlers. Im Falle des Beispiels 14 liefert ein Aufruf mit
java ArrayExceptionTest
(d.h. ohne Kommandozeilenparameter):
exception caught: 1
java.lang.ArrayIndexOutOfBoundsException: 1
at ArrayExceptionTest.main(ArrayExceptionTest.java:7)
Die Meldung (engl. message) enthält als nähere Fehlerinformation dasjenige Arrayelement, welches beim Zugriff(-sversuch) den Fehler verursacht hat.
Durch printStackTrace
kann die genaue Fehlerbezeichnung, nebst Quelldatei inklusive auslösender Zeilennummer ermittelt werden.
2 Werfen von Exceptions
Dem Programmierer steht -- wie dem Laufzeitsystem -- die Möglichkeit des Werfens von existierenden oder selbstdefinierten Ausnahmen offen.
Java-Syntax:
throw new ExceptionName();
Das Schlüsselwort throw
gefolgt vom new
-Operator erzeugt ein neues Exception-Objekt. Die Ausnahme wird durch die Signatur ihres Konstruktors vertreten.
Hinweis: Prinzipiell kann ein Exception-Objekt auch explizit durch den new
-Operator erzeugt werden. Entsprechend der Exception-Definition wird auch in diesem Falle zunächst der Konstruktor ausgeführt. Jedoch erfolgt kein Ansprung des catch
-Blocks, sondern die Ausführung wird nach der Objekterzeugung fortgesetzt.
Nach einem throw
-Aufruf wird der Programmablauf innerhalb der geworfenen Ausnahme fortgesetzt. Das bedeutet alle Anweisungen nach einem solchen Aufruf sind im Ablauf niemals erreichbar, der Java-Compiler meldet deshalb immer einen Statement not reached.
Fehler.
3 Definition eigener Ausnahmen
Alle Ausnahmen werden als explizite Spezialisierungen der Klasse java.lang.Exception
definiert.
Syntaktisch ist die Deklaration einer Exception-Klasse mit der einer "normalen" Klasse identisch.
Beispiel 15 (Quelldatei: ExceptionTest.java):
public class ExceptionTest
{
public static void main(String[] args)
{
try
{
System.out.println("raising... ");
throw new myException("this is a test message...");
} //end try
catch (myException ExceptionObj)
{
System.out.println("exception catched!");
System.out.print("Stack trace:");
ExceptionObj.printStackTrace();
System.out.println("Message: "+ ExceptionObj.getMessage() );
} //end catch (myException)
finally
{
System.out.pritln("finally reached...");
} //end finally
System.out.println("...continuing");
} //end main()
} //end class Test4
//---------------------------------------------------------
class myException extends Exception
{
public myException(String exceptionMsg) //constructor
{
super(exceptionMsg);
System.out.println("myException raised");
} //end myException()
}//end class myException
Im Beispiel 15 wird eine anwenderdefinierte Exception (myException
) als Spezialisierung der Exception
-Klasse definiert. Ihr Konstruktor belegt das ererbte Meldungstext-Attribut durch expliziten Aufruf des Superklassenkonstruktors. Anschliessend wird eine Textzeile am Bildschirm ausgegeben.
In der main
-Methode des Hauptprogramms wird im try
-Block durch den Programmierer die definierte Ausnahme ausgelöst (geworfen). Im Konstruktoraufruf wird ein frei definierter Text zur näheren Fehlerbeschreibung übergeben.
Der zugehörige catch
-Block fängt die geworfene Exception auf. Er gibt die Umgebung (stack trace) sowie den zuvor bei Initialisierung des Exception-Objekts gesetzten Fehlertext aus.
Nach Abschluss der Fehlerbehandlung des catch
-Blocks wird der Programmablauf im finally
-Block fortgesetzt. Danach werden alle ausserhalb der try
-catch
-finally
-Struktur platzierten Anweisungen abgearbeitet.
Hinweis:try
-Blöcke können geschachtelt werden, um lokales Exception-Handling zu implementieren.
Definition: Stream
Gerichtete Informationsverbindung zwischen einem Informationserzeuger und einer Informationssenke, welche die bereitgestellte Information konsumiert.
Mit Stream wird eine universelle Kommunikationsprimitive bezeichnet. Abhängig weder von Art und interner Struktur der verstandten Daten, noch von der Realisierung des Erzeugers oder Konsumenten.
Stream-Implementierung in der Java-Plattform:
Die gesamte Stream-bezogene Funktionalität der Java2-Plattform findet sich im Standard-API-Package java.io
. Jegliche Stream-Verarbeitung basiert auf den beiden abstrakten Klassen InputStream
für lesende, bzw. OutputStream
für schreibende Streams. Darüberhinaus liefert die abstrakte Basisklasse auch die Standard-Ausnahme IOException
aller Streams.
Diese beiden Klassen werden gemäß der spezifischen Anforderungen an den einzusetzenden Stream spezialisiert.
Quelle eines Ein- oder Ausgabestreams kann jedes beliebige verfügbare Gerät sein (z.B. Tastatur via Kommandozeile, Datei im Dateisystem oder Netzressource). Für die häufigsten Quellen und Senken sind Standardgeräte vordefiniert. Dies sind:
System.in
System.out
System.out
auf Bildschirm umgeleitet)System.err
Generell werden Streams durch Initialisierung mit der gewünschten Eingabequelle bzw. Ausgabesenke geöffnet. Das explizite Schliessen erfolgt im Programm durch Aufruf der Methode close()
, andernfalls durch das Laufzeitsystem bei Terminierung der Applikation.
Beispiel 16 stellt eine mögliche Implementierung des UNIX-Systemkomandos cat
vor.
Zunächst wird eine Variable vom Typ InputStream
deklariert. Der so geschaffene Eingabestrom wird mit System.in
auf die Kommandozeile gesetzt. Innerhalb einer kopfgesteuerten Schleife wird zunächst in der Fortsetzungsbedingung ein einzelnes Zeichen eingelesen (durch die Methode read()
auf dem selbstdefinierten Objekt inStream
vom Typ InputStream
). Nur wenn eine von -1
verschiedene Ganzzahl zurückgegeben wurde (d.h. der Eingabestrom nicht zu Ende ist) wird das gelesene Zeichen im Schleifenkörper an der Kommandozeile ausgegeben.
Beispiel 16:(Quellcodedatei: cat.java)
import java.io.InputStream;
import java.io.IOException;
public class cat
{
public static void main(String[] args)
{
InputStream inStream;
inStream = System.in;
int readByte=0;
while ( (readByte = inStream.read()) != -1)
{
System.out.print((char) readByte);
} //end catch
inStream.close();
}//end main()
}//end cat
Der übliche Aufruf mit java cat
gibt jede durch Return bestätigte Zeichenkette am Bildschirm aus, bis die Applikation durch CTRL-C
abgebrochen wird.
Durch Umlenkung des Ausgabestroms (engl. piping) kann cat
zum Kopieren in Dateien eingesetzt werden (java cat > outfile
). Wird auch der Eingabestrom umgelenkt, so kann cat
die Funktion eines Kopierbefehls wahrnehmen (java cat < infile > outfile
).
Durch die identische Mimik, mit Ausnahme der Belegung der InputStream
-Variablen kann auch das Lesen aus einer Datei realisiert werden. Dies ist in Beispiel 17 ausschnittsweise dargestellt.
Beispiel 17:
InputStream anotherStream;
anotherStream = new FileInputStream("c:\myTestFile");
int content;
content = anotherStream.read();
Die möglichen Ausrichtungen eines InputStreams ergeben sich durch die zulässigen Typausprägungen die eine Variable der Klasse InputStream
annehmen kann. Technisch gesehen bedeutet dies: Eine InputStream
-Variable kann gemäß der Typeinschränkung nur Objekte vom Typ InputStream
oder eines Subtypen dieser Klasse beherbergen.
Durch die Klasse InputStream
wird die Möglichkeit eines Verbindungsaufbaus, mit dem Ziel Daten in eine Applikation einzulesen, aufgebaut. Die Struktur dieser Daten ist durch den Stream nicht näher definiert.
Die zugehörige Leseoperation wird durch die Subklassen der abstrakten Klasse Reader
(ebenfalls im Paket java.io
) zur Verfügung gestellt.
Analog der Eingabeverarbeitung ist die Definition und Behandlung der Ausgabestreams -- als Subklassen von OutputStream
angelegt.
Beispiel 18 zeigt die Nutzung eines In- und eines Output-Streams zur Realiserung des UNIX-Kommandos cp
Beispiel 18(Quellcodedatei: cp.java):
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.Exception;
public class cp
{
public static void main(String[] argv) throws IOException
{
if (argv.length == 2)
{
InputStream inStream=null;
OutputStream outStream=null;
int processedCharacter=0;
try
{
inStream = new FileInputStream( argv[0] );
outStream = new FileOutputStream( argv[1] ); //create new file
while ( (processedCharacter = inStream.read()) != -1)
{
//while not at end of file
outStream.write( processedCharacter );
} //end while
} //end if
catch (Exception e)
{
if(e instanceof IOException)
{
System.out.println("IOException caught");
}//endif
else
{
System.out.println("unknown exception caught");
e.printStackTrace();
} //end else
System.out.println("exception's message: "+e.getMessage() );
} //end catch
} //endif
else
{
System.out.println("wrong number of parameters");
} //end else
} //end main()
} //end class cp
Erläuterungen zu Beispiel 18:
Zunächst werden die notwendigen Variablen (ein Ein- und ein Ausgabestream sowie die int
-Variable zur Zwischenspeicherung des gelesenen Zeichens) deklariert.
Die Quelldatei und Zieldatei werden dabei als Kommandozeilenparameter übergeben.
Verläuft das Öffenen der Eingabe- und Ausgabedatei fehlerfrei wird in einer Schleife bis zum Dateiende die Quelldatei zeichenweise in die angegebene Zieldatei kopiert.
Anmerkung zur Realisierung des Exception-Handlings:
Die Applikation verwendet sowohl einen catch
-Block zum Auffangen ausgelöster Exceptions, als auch die IOException
in der Methodendeklaration der main-Methode; d.h. diese Exception wird (sofern sie durch die read
-Anweisung innerhalb der Fortsetzungsbedingung der while
-Schleife geworfen wird) nicht durch den catch
-Block aufgefangen und behandelt, da sich die while
-Anweisung nicht im zugehörigen try
-Block befindet.
Service provided by Mario Jeckle
Generated: 2004-06-07T12:31:56+01:00
Feedback SiteMap
This page's original location: http://www.jeckle.de/vorlesung/sei/kII.html
RDF description for this page