Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 3. Auflage
 <<    <     >    >>   API  Kapitel 5 - Ausdrücke

5.1 Eigenschaften von Ausdrücken



Wie in den meisten anderen Programmiersprachen gehören auch in Java Ausdrücke zu den kleinsten ausführbaren Einheiten eines Programms. Sie dienen dazu, Variablen einen Wert zuzuweisen, numerische Berechnungen durchzuführen oder logische Bedingungen zu formulieren.

Ein Ausdruck besteht immer aus mindestens einem Operator und einem oder mehreren Operanden, auf die der Operator angewendet wird. Nach den Typen der Operanden unterscheidet man numerische, relationale, logische, bitweise, Zuweisungs- und sonstige Operatoren. Jeder Ausdruck hat einen Rückgabewert, der durch die Anwendung des Operators auf die Operanden entsteht. Der Typ des Rückgabewerts bestimmt sich aus den Typen der Operanden und der Art des verwendeten Operators.

Neben der Typisierung ist die Stelligkeit eines Operators von Bedeutung. Operatoren, die lediglich ein Argument erwarten, nennt man einstellig, solche mit zwei Argumenten zweistellig. Beispiele für einstellige Operatoren sind das unäre Minus (also das negative Vorzeichen) oder der logische Nicht-Operator. Arithmetische Operatoren wie Addition oder Subtraktion sind zweistellig. Darüber hinaus gibt es in Java - wie in C - auch den dreistelligen Fragezeichenoperator.

Für die richtige Interpretation von Ausdrücken muß man die Bindungs- und Assoziativitätsregeln der Operatoren kennen. Bindungsregeln beschreiben die Reihenfolge, in der verschiedene Operatoren innerhalb eines Ausdrucks ausgewertet werden. So besagt beispielsweise die bekannte Regel »Punktrechnung vor Strichrechnung«, daß der Multiplikationsoperator eine höhere Bindungskraft hat als der Additionsoperator und demnach in Ausdrücken zuerst ausgewertet wird. Assoziativität beschreibt die Auswertungsreihenfolge von Operatoren derselben Bindungskraft, also beispielsweise die Auswertungsreihenfolge einer Kette von Additionen und Subtraktionen. Da Summationsoperatoren linksassoziativ sind, wird beispielsweise der Ausdruck a-b+c wie (a-b)+c ausgewertet und nicht wie a-(b+c).

Neben ihrer eigentlichen Funktion, einen Rückgabewert zu produzieren, haben einige Operatoren auch Nebeneffekte. Als Nebeneffekt bezeichnet man das Verhalten eines Ausdrucks, auch ohne explizite Zuweisung die Inhalte von Variablen zu verändern. Meist sind diese Nebeneffekte erwünscht, wie etwa bei der Verwendung der Inkrement- und Dekrementoperatoren. Komplexere Ausdrücke können wegen der oftmals verdeckten Auswertungsreihenfolge der Teilausdrücke jedoch unter Umständen schwer verständliche Nebeneffekte enthalten und sollten deshalb mit Vorsicht angewendet werden.

Im Gegensatz zu vielen anderen Programmiersprachen ist in Java die Reihenfolge der Auswertung der Operanden innerhalb eines Teilausdrucks wohldefiniert. Die Sprachdefinition schreibt explizit vor, den linken Operanden eines Ausdrucks vollständig vor dem rechten Operanden auszuwerten. Falls also beispielsweise i den Wert 2 hat, dann ergibt der Ausdruck (i=3) * i in jedem Fall den Wert 9 und nicht 6.

Neben der natürlichen Auswertungsreihenfolge, die innerhalb eines Ausdrucks durch die Bindungs- und Assoziativitätsregeln vorgegeben wird, läßt sich durch eine explizite Klammerung jederzeit eine andere Reihenfolge erzwingen. Während das Ergebnis des Ausdrucks 4+2*5 wegen der Bindungsregeln 14 ist, liefert (4+2)*5 das Resultat 30. Die Regeln für die Klammerung von Teilausdrücken in Java gleichen denen aller anderen Programmiersprachen und brauchen daher nicht weiter erläutert zu werden.

In Java gibt es ein Konzept, das sich Definite Assignment nennt. Gemeint ist damit die Tatsache, daß jede lokale Variable vor ihrer ersten Verwendung definitiv initialisiert sein muß. Das wünscht sich eigentlich zwar auch jeder Programmierer, aber in Java wird dies durch den Compiler sichergestellt! Dazu muß im Quelltext eine Datenflußanalyse durchgeführt werden, die jeden möglichen Ausführungspfad von der Deklaration einer Variablen bis zu ihrer Verwendung ermittelt und sicherstellt, daß kein Weg existiert, der eine Initialisierung auslassen würde.

Die folgende Methode Test läßt sich beispielsweise deshalb nicht fehlerfrei kompilieren, weil k vor der Ausgabeanweisung nicht initialisiert wird, wenn i kleiner 2 ist:

001 public static void Test(int i)
002 {
003   int k;
004   if (i >= 2) {
005     k = 5;
006   }
007   System.out.println(k);
008 }
Listing 5.1: Fehler beim Kompilieren durch unvollständige Initialisierung

Ein solches Verhalten des Compilers ist natürlich höchst wünschenswert, denn es zeigt einen tatsächlichen Fehler an, der sonst unbemerkt geblieben wäre. Daß die Datenflußanalyse allerdings auch Grenzen hat, zeigt das folgende Beispiel, bei dem ebenfalls ein Compiler-Fehler auftritt:

001 public static void Test(int i)
002 {
003   int k;
004   if (i < 2) {
005     k = 5;
006   }
007   if (i >= 2) {
008     k = 6;
009   }
010   System.out.println(k);
011 }
Listing 5.2: Fehler beim Kompilieren durch unvollständige Datenflußanalyse

Natürlich kann hier k nicht uninitialisiert bleiben, denn eine der beiden Bedingungen ist immer wahr. Leider gehen die Fähigkeiten der Compiler noch nicht so weit, dies zu erkennen.

Es wäre in diesem Fall natürlich vernünftiger gewesen, den else-Zweig an die erste Verzweigung anzuhängen und damit die zweite ganz einzusparen.

In Wirklichkeit funktioniert die Datenflußanalyse bei vernünftigem Programmierstil recht gut. Die wenigen Fälle, in denen der Compiler sich irrt, können in der Regel durch explizite Initialisierungen aufgelöst werden.


 Titel   Inhalt   Suchen   Index   DOC  Handbuch der Java-Programmierung, 3. Auflage, Addison Wesley, Version 3.0.1
 <<    <     >    >>   API  © 1998-2003 Guido Krüger, http://www.javabuch.de