![]() |
|
|||||
Werden diese Ausdrücke als Anweisungen verwendet, nennt man sie Expression-Statements. Beispielea+= b; i++; --j; textFile.close(); // Aufruf der Methode close() new Test(); // Erschaffen einer Instanz der Klasse Test 3.3 Kontrollanweisungen
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Ist eine der Bedingungen true, wird die zugehörige Anweisung ausgeführt und alle weiteren der Leiter übersprungen (siehe Abb. 3.1).
Im nachfolgenden Code-Fragment wird geprüft, ob der Wert der Zeichen-Variable c ein Umlaut oder ß ist, um dann den entsprechenden Zähler zu inkrementieren (daneben zeigt es auch die Faszination deutscher Variablen):
if (c=='ä' || c=='Ä') ä++; // verstösst gegen die else if (c=='ö' || c=='Ö') ö++; // Namenskonvention für else if (c=='ü' || c=='Ü') ü++; // Variable! else if (c=='ß') ß++; // siehe Abschnitt 1.7
Die switch-Anweisung hat die folgende Form:
switch (intExpression) { case constExpression1: statementSequenz [break;] case constExpression2: statementSequenz [break;] ... case constExpressionM: statementSequenz [break;] [ default: statementSequenz ] }
|
Mit statementSequenz ist kein Block, sondern sind null bis beliebig viele Anweisungen gemeint.
Unter constExpression versteht man konstante Ausdrücke2 mit unterschiedlichen Werten, die sich implizit in den Typ int umwandeln lassen. Die break-Anweisung bzw. der default-Teil sind optional.
Die Ausführung der Anweisung läuft wie folgt ab:
Die Ausführung der switch-
Anweisung
| 1. | Der Wert der intExpression wird berechnet. |
| 2. | Sollte er mit einem der Werte constExpression1,...,constExpressionM übereinstimmen, werden alle im switch folgenden Anweisungen (statementSequenz) ausgeführt, bis eine break-Anweisung auftritt. Diese beendet dann die switch-Anweisung. |
| 3. | Stimmt keiner der Werte überein, wird – sofern vorhanden – die statementSequenz hinter default ausgeführt, ansonsten nichts. |
Die folgende switch-Anweisung variiert das if-Beispiel. Um die Wirkung von switch zu demonstrieren, wird c auf 'ä' gesetzt:
int ä=0,ü=0,ö=0,ß=0,z=0; char c= 'ä';
switch (c) { case 'ä': case 'Ä': ä++; // break; case 'ö': case 'Ö': ö++; // break; case 'ü': case 'Ü': ü++; // break; case 'ß': ß++; // break; default: if ('0'<=c && c<='9' ) z++; }
System.out.print(ä+","+ö+","+ü+","+ß+","+z); // :: 1,1,1,1,0
Erklärung: Das Zeichen c sowie die Zeichen-Literale werden in den Typ int umgewandelt (Punkt 1). Ohne break werden alle Zähler bis auf z hochgezählt (Punkt 2).
Entfernt man die Kommentare, wird nur ein Zähler hochgezählt, wobei das letzte break nur »gute Sitte« ist, da es in diesem Fall nicht notwendig ist.
Primitive Typen wie boolean, long, float oder double werden als Auswahlwerte hinter case nicht unterstützt.
Es gibt drei Iterations-Anweisungen: while, do und for. Die for-Schleife wird aufgrund ihrer Flexibilität sehr häufig verwendet und wurde ja auch bereits in mehreren Beispielen genutzt.
Die while-Anweisung hat die folgende Form:
while (condition) statement

Die Ausführung der while- Anweisung
Die in Klammern stehende Bedingung ist ein logischer Ausdruck. Die Ausführung der Anweisung läuft wie folgt ab:
Ist der Wert der Bedingung true, wird statement so lange ausgeführt, bis der Wert false ist. Dazu wird beim Eintritt in die Schleife und nach jeder Ausführung von statement der Bedingungsausdruck erneut berechnet und getestet.
Im folgenden Code-Fragment wird eine while-Schleife variiert, wobei der Variablen k eine zufällige Zahl zwischen 0..9 zugewiesen wird, die in den while-Bedingungen getestet wird:
int k; while ((k=(int)(Math.random()*10))<9); // leere Anweisung System.out.println(k); // :: 9
while ((k=(int)(Math.random()*10))<8) System.out.println(k); // :: Zufallszahlen zwischen 0..7 k=(int)(Math.random()*10);
while (k!=8) { System.out.println(k); // :: Zufallszahlen ungleich 8 k=(int)(Math.random()*10); }
Erklärung: Die Methode random() der Klasse Math liefert Zufallszahlen vom Typ double im Intervall [0,1).
Der Ausdruck (int)(Math.random()*10) liefert Zahlen vom Typ int zwischen 0 und 9.
Die do-Anweisung ist eine Variante von while und hat folgende Form:
do
statement
while (condition);
Der Unterschied zur while-Schleife liegt darin, dass statement zumindest einmal ausgeführt wird, bevor die Bedingung false werden kann.
Die nachfolgende Schleife wird nur einmal durchlaufen, d.h., sie ist äquivalent zu einem einfachen println().
do System.out.println(Math.random()); while (false);
Im folgenden Code-Fragment wird zur Ziffer k das zugehörige Zeichen c erzeugt, und Ziffer sowie Zeichen werden an jeweils einen String angehängt (wobei die implizite Konvertierung ausgenutzt wird):
String s1="", s2="";
int k;
char c;
do { k=(int)(Math.random()*10); // Zahl 0..9 c=(char)(k+'0'); // Ziffer '0'.. '9' System.out.println(c==k); // :: false
s1+=k; s2+=c; // anhängen an s1, s2 } while (s1.length()<5);
System.out.println(s1+"="+s2); // :: 87039 = 87039
Erklärung: Beim Vergleich c==k werden zwei Werte vom Typ int verglichen, was natürlich false ergibt.
Bevor k und c an s1 bzw. s2 angehängt werden, erfolgt eine Umwandlung in zwei Strings, die dann wieder gleich sind.
Die for-Anweisung hat gegenüber den beiden anderen Schleifen den Vorteil, alle relevanten Informationen zur Iteration an einer Stelle, dem Kopfteil, zusammenzufassen.
Die for-Schleife hat folgende Form:
for (initialisations; condition; iterations)
statement
Dabei sind initialisations und iterations kommaseparierte Listen von Expression-Statements (siehe 3.2.3):
initialisations: initialisation [ ,initialisation,... ]
iterations: iteration [ ,iteration,... ]
| Die Initialisierungsanweisungen können mit genau einer Deklaration beginnen, d.h. nachfolgend sind keine mehr möglich (siehe nächstes Beispiel). Die deklarierten Variablen sind lokal und existieren nur innerhalb der for-Schleife. |
| Jede der drei Anweisungen Initialisierung, Bedingung oder Iteration kann leer sein. |

Die Ausführung der for-Anweisung
Die Ausführung der Anweisung läuft wie folgt ab:
| 1. | Sofern nicht leer, wird/werden die Initialisierungs-Anweisung(en) ausgeführt. |
| 2. | Sofern nicht leer, wird die Bedingung ausgewertet. Ist sie leer oder true, wird die Anweisung statement ausgeführt, ansonsten ist die for-Schleife beendet. |
| 3. | Sofern nicht leer, wird/werden die Iteration(en) ausgeführt. Dann wird erneut der zweite Punkt ausgeführt. |
Die for-Schleife kommt aufgrund ihrer Flexibilität in unzähligen Variationen vor, von denen einige nachfolgend vorgestellt werden sollen.
for (;;) { ... } // endlos aufgrund des 2./3. Punktes
Initialisierung mit Deklaration:
double[] darr= {1.,0.,-.1,-4e-2,5d};
double sum=0.0; for (int i=0; i<darr.length;i++) // Deklaration von i sum+=darr[i];
System.out.println(sum); // :: 5.86
System.out.println(i); // C-Fehler
Nach der ersten der genannten Regeln existiert die lokale Variable i nur innerhalb der for-Schleife, ist also danach nicht mehr existent.
Zwei mögliche Alternativen sind:
for (int i=0; i<darr.length;) // Iteration ist zu suchen sum+=darr[i++];
for (int i=0; i<darr.length; sum+=darr[i++]);
System.out.println(i); // wieder C-Fehler (s. Regel)
Der Iterationsteil erledigt mit einem Seiteneffekt auch gleichzeitig die Summenoperation.
Siehe dazu das Beispiel im folgenden continue-Abschnitt.
Deklaration, Iteration mit mehreren Variablen:
int[] iarr= {1,2,3,4,5};
for (int i=0, j=iarr.length-1, t; i<j; i++, j--) { t= iarr[j]; iarr[j]=iarr[i]; iarr[i]=t; }
Aufruf einer Methode in einem for-Teil:
for (int i=0; i<iarr.length; System.out.print(iarr[i++]+" "));
Der Typ darf bei der Deklaration nur einmal aufgeführt werden. Die folgenden zwei Variationen sind also falsch (Fehler sind hervorgehoben!):
for (int i=0, int j=iarr.length-1, t; i<j; i++,j--) {...}
for (int i=0, j=iarr.length-1, int t; i<j; i++,j--) {...}
Iteration mit Objekten:3
Eine for-Schleife kann nicht nur durch eine Menge primitiver Typen wie int iterieren, sondern auch durch eine Menge von Objekten.
Nachfolgend wird eine String-Kollektion4 stringSet angelegt. Anschließend wird mittels der Methoden iterator() ein Iterator-Objekt5 für diese Kollektion angelegt.
Mit den Methoden hasNext() und next() des Iterators und einer for-Schleife können alle Elemente von stringSet durchlaufen werden.
Collection stringSet= new HashSet(); stringSet.add("Hallo"); stringSet.add("Welt");
for (Iterator i= stringSet.iterator(); i.hasNext();) System.out.print(i.next()+" "); // :: Hallo Welt
Erklärung: In Analogie zum int-Iterator wurde hier bewusst ein i gewählt. Wie zu sehen, ist der Iterationsteil leer, da i.next() einerseits das Objekt – in diesem Fall den String – und andererseits zum nächsten Element in der Kollektion wechselt.
Java kennt keine allgemeine Sprunganweisung in Form von goto wie C/C++. In speziellen Fällen erlaubt Java jedoch durchaus kontrollierte Sprünge. Zu den Sprunganweisungen zählen:
Sprunganweisungen:
break & Co.
| break: gültig innerhalb von switch, Schleifen und mit Marken versehenen Blöcken |
| continue: gültig innerhalb von Schleifen |
| return: gültig außerhalb eines Initialisierers |
| throw: muss eine im Programmkontext gültige Ausnahme werfen6 |
Bei Sprunganweisungen prüft der Compiler, ob der nachfolgende Code noch erreicht werden kann:
| Code, der aufgrund von Sprunganweisungen nicht mehr ausgeführt werden kann, erzeugt einen Compilerfehler. |
Label: Markieren von Anweisungen
Mit Ausnahme von Deklarationen können Anweisungen (inklusive Blöcke) mit einer Marke versehen werden.
Marken müssen gültige Identifier sein und – separiert durch einen Doppelpunkt – vor die zu markierende Anweisung gesetzt werden. Anweisungen mit derselben Marke dürfen nicht ineinander geschachtelt werden. Die Namen für Marken und Variablen können durchaus gleich sein.
Marken sind nur vor Anweisungen bzw. Blöcken sinnvoll, in denen ein break oder continue vorkommt.
Eine continue-Anweisung kann nur in einer Iteration auftreten. Die Form ist:
continue [label];

Die Ausführung der continue-Anweisung
Die Ausführung läuft wie folgt ab:
| 1. | Ohne Marke wird die innerste Iterationsanweisung an dieser Stelle abgebrochen, mit Marke die mit dieser Marke versehene Iteration. |
| 2. | Ist die Iteration eine for-Schleife, wird/werden als Nächstes die Iterationsanweisung(en) iterations (siehe for-Definition) ausgeführt. |
| 3. | Es wird die Bedingung (condition) der Iterationsanweisung ausgewertet. Ist sie true, wird der nächste Schleifendurchlauf durchgeführt, ansonsten wird die Schleife beendet. |
Im folgenden Code-Fragment werden fünf Summen von zufälligen Zahlen aus dem Intervall [0.5;1) berechnet, bis die Summe größer als 10.0 ist. Die Anzahl der zufälligen Werte und die Summe werden ausgegeben:
double sum, k; m1: for (int i=0; i<5; i++) { sum= 0.0; for (int j=0;;j++) { // leere Bedingung if ((k=Math.random())<0.5) continue; sum+=k; if (sum>10.0) { System.out.println(j+": "+sum); continue m1; } } }
Die innere continue-Anweisung gehört zur inneren for-Schleife. Um zur äußeren for-Schleife zu springen, muss continue mit einer Marke, hier m1, versehen werden.
Eine break-Anweisung hat die folgende Form:
break [label];
Ohne Marke kann break nur in switch-Anweisungen oder Iterationen verwendet werden, mit Marke innerhalb jeder Anweisung, die mit dieser Marke versehen ist.

Die Ausführung der break- Anweisung
Die Ausführung der break-Anweisung läuft wie folgt ab:
| Ohne Marke wird die innerste switch-Anweisung bzw. Iteration beendet, in der break enthalten ist. |
| Mit Marke wird die switch-Anweisung bzw. Iteration beendet, die mit dieser Marke versehen ist (und break enthält). |
Im folgenden Code-Fragment werden so lange zufällige Zahlen zwischen 0..9 erzeugt, bis alle Zahlen im Array iarr getroffen wurden. Treffer werden mit true in einem logischen Array barr markiert.
int[] iarr= {3,0,7}; boolean[] barr= new boolean[iarr.length]; int n=0, k; // n: Anzahl Treffer
m1: while (true) { // label m1 k=(int)(Math.random()*10); for (int i=0; i<iarr.length; i++) if (!barr[i] && k==iarr[i]) { // Treffer barr[i]= true; // markieren if (++n<3) break; // springt aus for else break m1; // springt aus while } }
Im nächsten Beispiel springt break aus dem zugehörigen Block:
m: { if (true) break m; // ohne if gibt es einen C-Fehler System.out.println("unerreichbar"); }
Ohne die if-Anweisung ist der Code in println() unerreichbar, d.h., es gibt einen Compilerfehler. Mit if ist der Compiler guten Glaubens, der Code hinter break könne vielleicht doch noch erreicht werden.
Die try-Anweisung hat folgende Form:
try { tryStatementSequenz } [catchClauses] [finallyClause]
Mindestens einer der optionalen Anweisungsteile catchClauses bzw. finallyClause muss vorhanden sein.
Die try-Anweisung wird immer in Verbindung mit der Behandlung von Ausnahmen (exception-handling mechanism) vorgestellt. Dies ist insofern richtig, da der try-Block mit der catch-Option eine (von zwei) Möglichkeiten ist, Ausnahmen abzufangen.
Die Behandlung von Ausnahmen mittels catchClauses wird später zusammen mit den Ausnahmen behandelt. Im Folgenden wird die weniger bekannte Variante
try { tryStatementSequenz }
finally { finallyStatementSequenz }

Die Ausführung von try-finally
Die Ausführung läuft wie folgt:
| Wird (irgend)eine Anweisung des try-Blocks tryStatementSequenz ausgeführt, werden abschließend (nach try und catch) die Anweisungen der finallyStatementSequenz ausgeführt, sofern nicht |
| im try-Block eine Anweisung System.exit() erfolgt oder |
| der ausführende Thread vorher »stirbt«.7 |
| Eine return-, break- oder continue-Sprunganweisung im try-Block wird erst nach der Ausführung des finally-Blocks durchgeführt. |
| Wird ein Ergebnis mittels return zurückgeliefert, kann durch den finally-Block der Wert des Ergebnisses allerdings nicht mehr verändert werden. |
|
Cleanup-Idiom: Die try-finally-Anweisung stellt sicher, dass Aktionen, die abschließend ausgeführt werden müssen, auch unbedingt zur Ausführung kommen. |
Das Einsatzfeld des Cleanup-Idioms ist größer als erwartet:
| Code, der Ressourcen freigeben muss, z.B. Dateien schließen oder Netzwerkverbindungen abbauen, und zwar unabhängig davon, wie die vorherigen Aktionen beendet wurden, gehört besser in eine try-finally-Anweisung. |
| Da Java im Gegensatz zu C++ keine expliziten Destruktoren besitzt, gehören dazu auch alle Cleanup-Anweisungen, die (irrtümlich) in der finalize-Methode für Objekte untergebracht werden (siehe hierzu auch 5.9.1).8 |
Das Idiom wird erst später benutzt.
Die statische Methode trySum() demonstriert die oben angegebene Ausführungsregel:
static double trySum(double[] darr) { double sum=0.0; for (int i=0; i<darr.length;sum+=darr[i++]); try { return sum; } finally { sum=0.0; Arrays.fill(darr,0.0); // setzt alle Elemente auf 0.0 } }
Der Aufruf mit einem Array darr liefert dann:
double[] darr= {0,1,2}; System.out.println(trySum(darr)+" "+darr[2]); // :: 3.0 0.0
Eine zu einer for-Schleife äquivalente while-Schleife kann z.B. wegen einer enthaltenen continue-Anweisung nur mittels try-finally realisiert werden.
Der äqivalente Code ist nebeneinander angeordnet:
for (int i= 0; i<10; i++) { if (i%2== 0) continue; System.out.print(i+" "); } |
int i=0; while (i<10) { try { if (i%2== 0) continue; System.out.print(i+" "); } finally { i++; } } |
Ohne Deklaration ist es unmöglich, ein Java-Programm zu schreiben. Deklariert werden
| Klassen und Interfaces, |
| Variablen (lokale und Member- bzw. Feld-Variablen) sowie |
| Methoden und Konstruktoren von Klassen. |
Modifier: Modifikationen von Klassen bzw.
Klassenmitgliedern
Alle Deklarationen erlauben optionale Modifikationen mittels eines oder mehrerer Modifikatoren (Modifiers).
Die erlaubten Modifikatoren sind abhängig von der Art der Deklaration und werden zur Unterscheidung mit einem Präfix versehen, z.B. bezeichnet cModifiers die erlaubten Modifikatoren für Klassen.
Da einige Modifikatoren erst im Zusammenhang mit Vererbung bzw. Threads verstanden werden können, werden nur die zu diesem Kapitel passenden besprochen.
Klassen sowie Interfaces sind Kompilations-Einheiten (siehe erstes Kapitel) und werden wie folgt deklariert:
[cModifiers] class ClassName [SuperClassInterfaces] {...}
[public] interface InterfaceName [SuperInterfaces] {...}
Erlaubte cModifier für Klassen sind public, abstract und final.
public: Erlaubt den Zugriff auf Klassen bzw. Interfaces auch außerhalb des Packages (siehe 1.2.3).
abstract: Erlaubt keine Anlage von Instanzen der Klasse. Dies ermöglicht es, die Klasse nicht vollständig zu implementieren.
final: »Darf nicht mehr geändert werden«
final: Bedeutet so viel wie »darf nicht mehr geändert werden« und verhindert bei Klassen die Anlage von Sub- bzw. Unterklassen.
Default-Zugriff: nur im Package im Zugriff
Die Modifikatoren abstract und final werden noch einmal in Kapitel 5, Vererbung, behandelt.
default (friendly): Wird vor class bzw. interface kein Modifikator verwendet, ist dies die Default-Zugriffsart, die häufig auch friendly oder Package-Access genannt wird.
Klassen bzw. Interfaces stehen dann nur innerhalb des Packages im Zugriff, außerhalb des Packages können sie nicht benutzt werden.
Ein einfaches Beispiel einer Klassen-Deklaration, die nur im Package im Zugriff steht:
class Color {
int rgb; // 8 Bit pro: alpha, red, green, blue
}
Bei einem Interface kann zwar ohne Fehler auch der Modifikator abstract verwendet werden, sinnvoll ist aber nur der Modifikator public, denn:
Interfaces sind immer abstrakt
| Da Interfaces implizit abstrakt sind (und dies logisch auch nur sein können), ist der Modifikator abstract vor einem Interface überflüssig und zu vermeiden.9 |

Streng typisiert: Keine Variable ohne Typ
Java ist eine streng typisierte Sprache. Dies bedeutet:
| Jede Variable muss vor ihrer Benutzung deklariert und einen Wert zugewiesen bekommen. |
| Jeder Variablen wird bei der Deklaration ein primitiver Typ oder Objekttyp zugewiesen. |
Variablen können bei der Deklaration explizit oder implizit mit Hilfe von Literalen, Variablen oder Methoden initialisiert werden (siehe 1.6).
Die Deklaration von Variablen hat die Form:
[vModifiers] Type varName [= initialValue] [, ...] ;
Die Option [,...] bedeutet, dass mehrere Variablen – durch Komma getrennt – vom gleichen Typ deklariert und initialisiert werden können.
Erlaubte vModifiers für Instanz- und Klassen-Variablen sind:
public, protected, private, static, final, transient, volatile
Lokale Variablen erlauben nur den Modifikator final.10
Die Bedeutung »darf nicht mehr geändert werden« verhindert bei Variablen, dass ein einmal gesetzter Wert nicht mehr geändert werden kann. Die Variable ist eine Konstante. Für die Wirkung von final gilt folgende Regel:
| Bei Variablen vom primitiven Typ ist der Wert der Variablen konstant. |
| Bei Referenz-Variablen ist (nur) die Referenz konstant, der Wert des Objekts, das referenziert wird, kann jedoch geändert werden. |
Gegenüber C/C++ bietet Java die Möglichkeit, den Wert nicht direkt bei der Deklaration der final-Variable zu setzen, sondern erst später. Diese Art nennt man blank final.
Bei Klassen- und Instanz-Variablen muss die Initialisierung der blank finals allerdings sofort beim Laden der Klasse bzw. der Objektanlage erfolgen.11
Die folgenden Anweisungen demonstrieren die Auswirkung von final auf Referenzen:
final float E=2.7182818f; // der Wert von E ist konstant
final int[] iarr= {-1,2,3}; // Referenz konstant int[] jarr= {1,2,3};
iarr[0]= 1; // ok, da Änderung am Wert des Objekts jarr= iarr; // auch ok, jarr ist nicht konstant iarr= jarr; // C-Fehler: Referenz soll geändert werden
Die Methode finalTest() initialisiert eine lokale blank final Variable E erst am Ende:
void finalTest () { final float E; // hier noch kein Wert! //... hier Code ohne Zugriff auf E E= 2.7182818f; // nun Wert nicht mehr änderbar }
Java-Programme sind hinsichtlich Variablen recht einfach aufgebaut:
Packages enthalten Klassen, Klassen enthalten Felder und Methoden, Methoden und Blöcke enthalten lokale Variablen.
Lokale Variable: Lebensdauer und Gültigkeit
Lokale Variablen haben nur eine beschränkte Lebenszeit. Sie werden bei Eintritt in die Methode oder den Block, in dem sie deklariert sind, erschaffen und bei Austritt zerstört bzw. entsorgt.
Damit stehen lokale Variablen nur innerhalb ihres Gültigkeitsbereichs im Zugriff.
Lokale Variablen müssen immer explizit initialisiert werden (siehe 1.6), wobei dies der Compiler »rigoros« prüft, wie anhand der nachfolgenden Methode oops() zu erkennen ist.
float oops(float p) { float res;
if (p!=0) res=1/p; return res; // C-Fehler }
Die bereits ungültige lokale Variable j erzeugt auch in der nächsten Methode uups() einen Compiler-Fehler:
void uups () { { int j=2; } // j nur im Block gültig System.out.println(j); // C-Fehler }
Alle Felder von Klassen werden grundsätzlich initialisiert (siehe 1.6). Man unterscheidet bei Feldern
| Klassen-Variablen, die nur eimal pro Klasse vorkommen und |
| Instanz-Variablen, die in jeder Instanz der Klasse existieren. |
Klassen-Variablen existieren nach Laden der Klasse
Klassen-Variablen werden durch den Modifikator static gekennzeichnet, weshalb sie auch statische Variablen genannt werden. Sie werden – bevor irgendwelche Instanzen existieren – bereits beim Laden der Klasse angelegt und initialisiert.
Instanz-Variablen existieren nach Erschaffen des Objekts
Instanz-Variablen werden erst mit der Erzeugung der Instanz einer Klasse (z.B. mittels new) angelegt und initialisiert.
Die Klasse Color (aus 3.4.1) wird um eine konstante Klassen-Variable erweitert, die zum Ausmaskieren der Farbe Rot benutzt werden kann:
class Color {
final static int REDMASK= 0xff0000; // Klassen-Variable int rgb; // Instanz-Variable
}
In anderen Klassen kann man direkt auf die Klassen-Variable REDMASK von Color zugreifen, ohne ein Color-Objekt erschaffen zu müssen:
class TestColor { static void test() { System.out.println(Color.REDMASK); // über Klasse Color c= new Color(); System.out.println(c.rgb); // nur über Objekt System.out.println(new Color().rgb); // auch ok! } }
In der letzten Anweisung wird über den Referenzausdruck (siehe 3.5.3) new Color() und nicht über eine Referenz wie c direkt auf die Instanz-Variable rgb zugegriffen.
Array-Initializer:
Anlage kleiner Arrays ohne new
Bei der Array-Deklaration kann auch ohne new mit Hilfe eines Array-Initializers ein Array-Objekt erschaffen werden, auf das die Referenz dann zeigt. Die Initialisierung von Arrays bei der Anlage ist besonders für kleine Arrays sehr praktisch.
Array-Initializer ist kein Array-Literal
int[][] imatrix= {{1,0},{0,1}}; // 2x2 Einheitsmatrix
Diese spezielle Form kann aber nur bei der Deklaration direkt verwendet werden, anschließend nicht mehr.
int[][] imatrix; imatrix= {{1,0,0},{0,1,0},{0,0,1}}; // C-Fehler imatrix= new int[][] {{1,0,0},{0,1,0},{0,0,1}}; // ok!
Methoden werden mit folgender Syntax deklariert:
[mModifiers] ResultType methodName (parameterList) [ThrowsList]
{ /* Block */ }
public, protected, private, static, final, abstract, native, synchronized
| den Typ des Werts, den die Methode bei Aufruf mittels des Schlüsselworts return zurückliefert. Der Wert ist dabei das Ergebnis eines typkompatiblen Ausdrucks, der hinter return steht. |
| void, d.h., die Methode liefert keinen Wert als Ergebnis zurück und return darf nur ohne einen Ausdruck verwendet werden. |
In Java ist void kein Typ, d.h., eine Typumwandlung mittels eines Casts nach void wie in C/C++ ist nicht möglich:
(void) anyExpression; // C-Fehler
Das Resultat einer Methode hat einen beliebigen Typ
Für das Resultat einer Methode gibt es keine Einschränkung wie bei C/ C++. Alle Typen – auch mehrdimensionale Arrays – sind erlaubt.12
Die parameterList ist eine kommaseparierte Liste von Deklarationen einzelner Parameter-Variablen. Hat die Methode keine Parameter, so muss trotzdem ein leeres Klammernpaar stehen.
final-Parameter: nicht veränderbar in der Methode
Der einzige erlaubte Modifikator bei der Deklaration der Parameter ist final. Einem final-Parameter kann in der Methode kein Wert zugewiesen werden.
Eine Initialisierung der Parameter mit einem Default-Wert wie bei C/C++ ist nicht erlaubt.
throws:
Auflistung aller nicht abgefangenen, zu prüfenden Ausnahmen
Die ThrowsList gibt alle zu prüfenden Ausnahmen (checked Exceptions) an, die von der Methode während der Laufzeit ausgelöst, aber nicht behandelt werden.
Ausnahmen sind Klassen, die von der Klasse java.lang.Throwable abgeleitet sind. Sie werden hinter dem Schlüsselwort throws durch Komma getrennt aufgeführt (siehe 7.4.2).
Die Klasse Color wird um eine Methode getRed() mit Rückgabewert und setRed() ohne Rückgabewert erweitert, d.h., return muss entweder mit oder ohne Wert aufgeführt werden:
class Color { final static int REDMASK= 0xff0000; int rgb; // 8 Bit pro: alpha, red, green, blue
int getRed() { return (rgb & REDMASK) >>> 16; }
void setRed(int red) { if (red<0 || red>0xff) return; // nur 0..255 erlaubt rgb=(rgb&= 0xff00ffff)|(red<<16); } }
Die statisch erklärte Methode createIntMatrix() legt eine Integer-Matrix mit rows-Zeilen und cols-Spalten an und liefert sie als Ergebnis zurück:
public static int[][] createIntMatrix(int rows, int cols) {
return new int[rows][cols];
}
Unter der Signatur einer Methode versteht man den Methodennamen und die Liste der Typen ihrer Parameter, wobei die Anordnung wichtig ist.
Die Signatur der Methode createIntMatrix() ist
createIntMatrix, int , int
Zur Signatur zählt nach Java-Konvention nicht der Rückgabetyp.13 Es gilt folgende Regel:
Signaturen: Unterscheidungsmerkmal für Methoden
| In einer Klasse können keine zwei Methoden mit derselben Signatur vorkommen.14 |
Die Klasse SignaturFehler führt folglich zu einem Compiler-Fehler:
class SignaturFehler { int foo(int i) {}; // Signatur: foo, int void foo(int k) {}; // Signatur: foo, int }
Der Aufruf bzw. die Ausführung einer Methode ist davon abhängig, ob sie innerhalb oder außerhalb der Klasse aufgerufen wird, in der sie deklariert ist, und ob sie eine Instanz-Methode oder Klassen-Methode ist.
Für alle Methoden ist allerdings der folgende Mechanismus der Werteübergabe der Argumente und des Aufrufs gleich.
Beim Aufruf einer Methode werden die formalen Parameter durch aktuelle Argumente ersetzt, mit denen die Methode dann ausgeführt wird. Ist die Methode parameterlos, muss – wie bei der Deklaration – hinter dem Methodennamen eine leere Klammer gesetzt werden.
Es sind folgende allgemeinen Regeln zu beachten:
| 1. | Für jeden formalen Parameter muss beim Aufruf ein Ausdruck übergeben werden, dessen Typ implizit in den des formalen Parameters umgewandelt werden kann. |
| 2. | Bevor die Methode ausgeführt wird, werden erst alle Werte der Argumente von rechts nach links berechnet. |
| 3. | Die Werte der Argumentausdrücke sind dann die Werte, die für die formalen Parameter verwendet werden. |
Pass by Value: Wertübergabe
der Argumente per Kopie
Der letzte Punkt wird auch Pass by Value genannt und bedeutet, dass nicht mit dem Argument selbst, sondern mit einer Wertekopie des Arguments gearbeitet wird.15 5
| Beim Methoden-Aufruf kann statt einer Variablen immer ein Ausdruck als Argument übergeben werden. |
Der Aufruf der Methode createIntMatrix() verdeutlicht die Regeln:
char c= '2'; int[][] imatrix= createIntMatrix(c-='0',++c); System.out.print(imatrix.length+","); System.out.print(imatrix[0].length); // :: 2,3
Erklärung: Beide Argumentausdrücke sind vom Typ char. Sie werden nach der zweiten Regel zuerst berechnet. Beide Ergebnisse werden nach der ersten Regel von char nach int umgewandelt und als Wert übergeben.
In den folgenden beiden Abschnitten wird die unterschiedliche Wirkung des Pass by Value auf primitive Typen und Referenz-Typen behandelt.
Anhand der Klasse MethodTest1 und ihrer statischen Methode primitiveArgs() soll die Wirkung des Pass by Value anhand von Parametern des primitiven Typs demonstriert werden:
class MethodTest1 {
static long primitiveArgs (double d, int i) { if (d!=d) // wenn NaN Û Double.isNaN(d) return i*i; // numeric promotion else { d*=d; // ändert den Parameterwert return (long)d; // Cast notwendig } } // ... }
Beim letzten return muss der Wert von d explizit nach long konvertiert werden.
Diese Methode wird nun in einem Code-Fragment (innerhalb derselben Klasse) aufgerufen:
Primitive Argumente ändern ihre Werte nie durch Methoden-Aufruf
System.out.println(primitiveArgs(Double.NaN,1)); // :: 1 double x=2.; System.out.println(primitiveArgs(x,(int)x)); // :: 4 System.out.println(x); // :: 2.0
Aufgrund Pass by Value hat der Wert von x sich nicht geändert!
Da Referenzen nur den Zeiger auf ein Objekt halten, wird bei der Wertübergabe nicht das Objekt kopiert, sondern nur der Zeiger.
Innerhalb der Methode wird also mit einer zweiten Referenz, einer Kopie des übergebenen Referenz-Arguments, gearbeitet, das auf dasselbe Original-Objekt zeigt.
|
Referenzierte Objekte können verändert werden!
Auch in diesem Fall soll anhand einer Klasse MethodTest2 und ihrer statischen Methode refArgs() die Wirkung des Pass by Value anhand von Referenz-Parametern demonstriert werden:
class MethodTest2 {
static int[] refArgs (int[] v1, int[] v2) {
int[] res= v2; // Anlage eines Resultat-Vektors?
for (int i=0; i<res.length;i++)
res[i]= v1[i]+v2[i]; // Summe der Vektoren
v1= v2;
return res;
}
}
Die Methode wird in einer anderen Klasse wie folgt benutzt:
int[] iv1= {1,0,0}, iv2= {0,1,-1};
int[] iv3= MethodTest2.refArgs(iv1,iv2);
System.out.println(iv1[0]+","+iv1[1]+","+iv1[2]); // :: 1,0,0
System.out.println(iv2[0]+","+iv2[1]+","+iv2[2]); // :: 1,1,-1
System.out.println(iv3[0]+","+iv3[1]+","+iv3[2]); // :: 1,1,-1
Erklärung: In der Methode refArgs() wird eine lokale Referenz res angelegt, die auf dasselbe Array wie die Referenz v2 zeigt. Damit werden die Ergebnisse der Addition der Vektor-Elemente in v2 gespeichert.
Anschließend wird die Referenz v1 auf den Wert von v2 gesetzt, d.h., innerhalb der Methode zeigt nun auch v1 auf den Ergebnisvektor v2.
Nach der o.a. Regel hat dies dann folgende Auswirkungen nach außen:
| Die Referenzen iv2 und iv3 zeigen auf dasselbe Array, d.h., die int-Elemente wurden durch den Aufruf geändert (zweite Regel). |
| Die Referenz iv1 zeigt weiterhin auf {1,0,0}, da v1 nur eine Referenzkopie von iv1 ist (erste Regel). |
Die Anlage einer weiteren Referenz in refArgs() war ziemlich überflüssig, da:
res[i]= v1[i]+v2[i]; Û v2[i]= v1[i]+v2[i];
Die Anlage eines neuen Arrays wäre wohl besser gewesen:
int[] res= new int[v2.length]; // anstatt: int[] res= v2;
Wird eine Methode über ihren einfachen Namen aufgerufen
methodName(actualArguments)
so muss die Methode zur Deklaration der Klasse gehören, in der sie aufgerufen wird. Die Methode kann dabei durchaus zu einer Superklasse oder einem Interface gehören, das die Klasse implementiert (siehe Abschnitt zur Vererbung in Kapitel 5.
Um auch auf Methoden anderer Klassen zugreifen zu können, wird vor den Methodennamen ein qualifizierender Ausdruck gestellt. Dabei unterscheidet man zwischen Instanz- und Klassen-Methoden.
Methoden-Aufruf über Referenz-Expression auf Instanzen
Instanz-Methoden operieren auf Instanzen und werden in der Form
referenzExpression.methodName(actualArguments)
aufgerufen. Dabei steht referenzExpression für alles, was eine Referenz auf eine Instanz der entsprechenden Klasse oder des Interfaces liefert, von der oder dem man die Methode aufrufen möchte:
| 1. | eine Variable, Array-Element bzw. Literal der Klasse |
| 2. | ein Methoden-Aufruf/Operation (mit Referenz als Rückgabewert) |
| 3. | einen new-Ausdruck der Klasse |
| 4. | das Schlüsselwort this oder super |
Die letzten beiden Schlüsselwörter sind dazu da, Zweideutigkeiten aufzulösen, d.h., this ist eine Referenz auf das Objekt, das die Methode ausführt, und super ist eine Anweisung, die nachstehende Methode aus einer der Superklassen zu nehmen.16
Nachfolgend werden Methoden über Referenz-Ausdrücke aufgerufen.
i= ("abc"+"de").length()); // siehe 1./2. Punkt oben
Erklärung: Die Operation + liefert eine Referenz auf ein String-Objekt "abcde", d.h., sie ist gleichbedeutend mit "abcde".length(). Zu Klassen-Literalen können nach dem ersten Punkt Instanz-Methoden aufgerufen werden.
j= "abc".substring(1).length(); // siehe 2. Punkt oben
Erklärung: Die Methode "abc".substring(1) liefert einen (Teil-) String zurück, von dem wieder eine Methode aufgerufen werden kann.
new für Array-Anlage
new für Objekt-Elemente eines Arrays
Color[] c= new Color[1]; // legt ein Array an (c[0]= new Color()).setRed(0xab); // siehe 1. Punkt oben // c[0]= new Color(); c[0].setRed(0xab);
Erklärung: Bei der Anlage eines Arrays von Referenzen auf Objekte werden mit dem new des Arrays nicht die Objekte erschaffen, sondern nur die Referenzen.
Sollen zu den Referenzen auch Objekte erschaffen werden, so sind diese wieder mittels new zu bilden.
Deshalb wird zuerst mittels new eine neue Color-Instanz geschaffen und zugewiesen. Von c[0] wird dann die Methode setRed() aufgerufen (die äquivalente Anweisung befindet sich oben in der letzten Kommentarzeile).
k= new Color().getRed(); // siehe o.a. 3. Punkt
Erklärung: Mit new wird eine anonyme Instanz von Color erschaffen, d.h., ihr ist explizit keine Referenz zugewiesen. Über die anonyme Color-Instanz wird dann getRed() aufgerufen. Diese Class Instance Creation Expression ist (laut Tabelle 2.2) im Rang höher als der .Dot-Operator, braucht also nicht geklammert zu werden.
Auf diese Color-Instanz kann anschließend nicht mehr zugegriffen werden.
Der Aufruf von Klassen-Methoden erfolgt über den Klassennamen:
Einschränkungen für Klassen-Methoden
className.methodName(actualArguments)
Statische bzw. Klassen-Methoden können nur auf statische bzw. Klassen-Variablen (siehe 1.6) zugreifen oder auf Objekte, die ihnen als Argumente übergeben werden. Es gilt folgende Regel:
| können nicht auf die Instanz-Variablen der Klasse zugreifen, da diese nur in den Objekten der Klassen existieren. |
| können keine Instanz-Methoden der Klasse aufrufen. |
| können aufgerufen werden, ohne dass irgendwelche Instanzen der Klasse erschaffen werden müssen. |
Klassen-Methoden:
Eine (neue) Art der prozeduralen Programmierung
Somit können statische Methoden auf Instanzen und ihren Feldern nur operieren, wenn ihnen diese als Argumente übergeben werden.
Sofern statische Methoden nur auf den Parametern/Argumenten operieren, sind sie äquivalent zu Funktionen in prozeduralen Sprachen wie C.
Als Beispiel soll eine swapObject()-Methode dienen, die beliebige Objekt-Referenzen tauscht:
class SwapDemo { static void swapObject (Object[] o,int first,int second) { Object t= o[first]; o[first]=o[second]; o[second]= t; } }
Hier wird ausgenutzt, dass Änderungen an Array-Elementen durchaus den Methoden-Aufruf überleben. Der Aufruf in einer anderen Klasse erfolgt über den Klassennamen:
Integer iarr[]= {new Integer(1), new Integer(-1)};
Swap.swapObject(iarr,0,1); System.out.println(iarr[0]+","+iarr[1]); // :: -1,1
|
Statische Methoden können zwar auch über Instanzen aufgerufen werden. Dies ist aber irreführend und sollte unbedingt vermieden werden. |
Dieses Kapitel behandelte Anweisungen, wobei nur die Anweisungen bzw. Optionen besprochen wurden, die nicht den Rahmen eines Kapitels gesprengt hätten.
Neben den üblichen Kontrollanweisungen wurde die try-Anweisung in der try-finally-Variante vorgestellt, die zum ersten Cleanup-Idiom geführt hat.
Einen weiteren Schwerpunkt bildeten die Deklarationen von Klassen, ihrer Modifikatoren, der lokalen Variablen und Felder, der Methoden und der Bedeutung von static und final.
Die Signatur einer Methode wurde angesprochen und ihr Aufruf mit Argumenten. Besonders der Übergabemechanismus Pass by Value für Argumente wurde in Hinblick auf die Auswirkungen für primitive Parameter und Referenz-Parameter anhand von Beispielen vorgestellt.
Zu jeder Frage können jeweils ein oder mehrere Antworten bzw. Aussagen richtig sein.
for (int i=1, j=10; i<j; i++, --j) { if (j%i==0) { System.out.println("continue"); continue; } if (i==j-1) { System.out.println("\nbreak"); break; } System.out.print(i+":"+j+" "); }
int i=0; double d=10.;
i: for (int i= 0; i<3; i++) for (int j=3; j>0; j--) { if (i==j) { System.out.print("b "); break i; } else if (j>i) { System.out.print("c "); continue; } System.out.print(i+":"+j+" "); }
E: Der Code führt aufgrund der Marke zu einem Compiler-Fehler.
m: for (int i= 4; i<7; i++) switch (i%4) { case 0: System.out.println("1.case"); break; case 3-1: System.out.println("2.case"); break m; case 3: System.out.println("3.case"); default: System.out.println("default"); }
for (int i=0; i<5; i++) { m: try { if (i==3) { System.out.println("break"); break; } try { if (i%2==0) i++; } finally { i++; break m; } } finally { System.out.println("finally"); i++; } System.out.println(i+" "); }
static void f6(int[] iarr, int i, boolean b) { /*...*/ }
Die nachfolgenden Argumente iarr und barr sind wie folgt deklariert:
int[] iarr= {1,2,3}; byte[] barr= {1,2,3};
C: f6(new int [] {1,2,3},2<<1,0./0.==0./0.);
class F7 { static int i; public static void main (String a[]) { final int j; System.out.println(i); System.out.println(j=10); } }
C: F7.main() kann nicht außerhalb des Packages aufgerufen werden.
D: Innerhalb des Packages ist der Aufruf F7.main("Test"); korrekt.
E: final int j; führt zu einem Compiler-Fehler, da die Initialisierung fehlt.
class F8 { // 1 final int a=1; // 2 static final int b=2; // 3 public static void f (double a, final int c) { // 4 int b=3; // 5 System.out.println(a); // 6 System.out.println(b); // 7 System.out.println(c=7); // 8 System.out.println(1/0); // 9 } }
A: Fehler in // 4: Der Name a ist bereits vergeben.
B: Fehler in // 5: Der Name b ist bereits vergeben.
C: Fehler in // 6: Doppeldeutig, da unklar, welches a gemeint ist.
D: Fehler in // 7: Doppeldeutig, da unklar, welches b gemeint ist.
E: Fehler in // 8: c kann kein Wert zugewiesen werden.
F: Fehler in // 9: Integer-Division durch Null.
class F9 {
void f9() { return; }
}
B: Der Aufruf von F9.f9(); macht überhaupt nichts, d.h. ändert nichts.
A: Der einzige erlaubte Modifikator für Parameter ist final.
B: Eine Methode kann beliebig viele return-Anweisungen haben.
D: Alle Methoden einer Klasse können sich gegenseitig aufrufen.
E: Eine Klasse, die final erklärt wird, kann nur final erklärte Methoden enthalten.
F: Wird eine Methode final erklärt, muss die zugehörige Klasse auch final erklärt werden.
G: Eine Klasse (die nicht in einer anderen enthalten ist) kann nicht private erklärt werden.
H: Eine (nicht final erklärte) Klassen-Variable kann nur von static erklärten Methoden geändert werden.
I: Die Methode private static void main(String[] args) erzeugt einen Compiler-Fehler, da sie private erklärt wurde.
J: Eine public erklärte Methode kann auch außerhalb des Packages aufgerufen werden.
K: Wird einer Methode mit einem Array-Parameter beim Aufruf ein Array übergeben, müssen die Elemente den gleichen Typ haben oder implizit in den Typ der Elemente des Array-Parameters konvertiert werden können.
L: Wird einer Methode mit einem Referenz-Parameter beim Aufruf eine Referenz mit einem anderen Typ übergeben, muss diese implizit in den Typ des Parameters konvertiert werden können.
1 Detailänderungen mit dem Ziel der Verbesserung, wie z.B. Gültigkeitsbereich von Iterationsvariablen bei for-Schleifen oder um C/C++ Tricks zu unterbinden (wie z.B. Duff´s Device).
2 Konstante Ausdrücke kann bereits der Compiler errechnen.
Siehe hierzu den final-Modifier.
3 Zum Verständnis des Kapitels ist dieses Beispiel nicht notwendig. Es setzt Kenntnisse zum Package java.util voraus, welches erst im zweiten Teil des Buchs besprochen wird.
4 Kollektionen sind »Behälter« für beliebige Objekte, in diesem Fall eine (mathematische) Menge stringSet von Strings (siehe 14. Kapitel).
5 Mit Hilfe von Iteratoren können die Elemente einer Kollektion »besucht« werden.
6 Wird in Kapitel 7, Ausnahmen, behandelt.
7 Siehe hierzu Kapitel 9, Threads. Es reicht allerdings auch,
den Computer auszuschalten.
8 Siehe hierzu auch den Abschnitt 5.9.1
9 Dies steht leider im Gegensatz zu vielen Interface-Deklarationen in der Plattform.
10 Interfaces und die vModifiers (bis auf static und final) werden erst im Zusammenhang mit Vererbung (siehe Kapitel 5) näher besprochen.
11 Siehe zu Konstruktoren 5.5.2 und statischen Initialisierern 5.7.2
12 Damit dies syntaktisch möglich ist, werden die Array-Klammern direkt hinter den Typ geschrieben und nicht wie bei C/C++ hinter die Variable.
13 Vorsicht: Die Definition ist nicht einheitlich. Beim Design anderer Sprachen zählt in der Regel auch der Rückgabetyp zur Signatur.
14 Vorsicht: Konstruktoren zählen nicht zu den Methoden (siehe auch 5.6.2).
15 Ist das Argument ein Ausdruck, ist evident, dass mit dem Ergebnis
gearbeitet werden muss.
16 super wird erst im Zusammenhang mit Vererbung (siehe Kapitel 5) behandelt.
| << zurück |
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
| |||||
Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de