2.4 Ausdrücke, Operanden und Operatoren
Beginnen wir mit mathematischen Ausdrücken, um dann die Schreibweise in Java zu ermitteln. Eine mathematische Formel, etwa der Ausdruck -27 * 9, besteht aus Operanden (engl. operands) und Operatoren (engl. operators). Ein Operand ist zum Beispiel eine Variable, ein Literal oder die Rückgabe eines Methodenaufrufs. Im Fall einer Variablen wird der Wert aus der Variablen ausgelesen und mit ihm die Berechnung durchgeführt.
Die Arten von Operatoren
Die Operatoren verknüpfen die Operanden. Je nach Anzahl der Operanden unterscheiden wir folgende Arten von Operatoren:
Ist ein Operator auf genau einem Operanden definiert, so nennt er sich unärer Operator (oder einstelliger Operator). Das Minus (negatives Vorzeichen) vor einem Operanden ist ein unärer Operator, da er für genau den folgenden Operanden gilt.
Die üblichen Operatoren Plus, Minus, Mal sowie Geteilt sind binäre (zweistellige) Operatoren.
Es gibt auch einen Fragezeichen-Operator für bedingte Ausdrücke, der dreistellig ist.
Operatoren erlauben die Verbindung einzelner Ausdrücke zu neuen Ausdrücken. Einige Operatoren sind aus der Schule bekannt, wie Addition, Vergleich, Zuweisung und weitere. C(++)-Programmierer werden viele Freunde wiedererkennen.
2.4.1 Zuweisungsoperator
In Java dient das Gleichheitszeichen = der Zuweisung (engl. assignment).[ 75 ](Die Zuweisungen sehen zwar so aus wie mathematische Gleichungen, doch existiert ein wichtiger Unterschied: Die Formel a = a + 1 ist – zumindest im Dezimalsystem ohne zusätzliche Algebra – mathematisch nicht zu erfüllen, da es kein a geben kann, das a = a + 1 erfüllt. Aus Programmiersicht ist es in Ordnung, da die Variable a um eins erhöht wird. ) Der Zuweisungsoperator ist ein binärer Operator, bei dem auf der linken Seite die zu belegende Variable steht und auf der rechten Seite ein Ausdruck.
[zB] Beispiel
Ein Ausdruck mit Zuweisungen:
int i = 12, j;
j = i * 2;
Die Multiplikation berechnet das Produkt von 12 und 2 und speichert das Ergebnis in j ab. Von allen primitiven Variablen, die in dem Ausdruck vorkommen, wird also der Wert ausgelesen und in den Ausdruck eingesetzt.[ 76 ](Es gibt Programmiersprachen, in denen Wertoperationen besonders gekennzeichnet werden, so etwa in LOGO. Eine Wertoperation schreibt sich dort mit einem Doppelpunkt vor der Variablen, etwa :X + :Y. ) Dies nennt sich auch Wertoperation, da der Wert der Variablen betrachtet wird und nicht ihr Speicherort oder gar ihr Variablenname.
Erst nach dem Auswerten des Ausdrucks kopiert der Zuweisungsoperator das Ergebnis in die Variable. Gibt es Laufzeitfehler, etwa durch eine Division durch null, gibt es keinen Schreibzugriff auf die Variable.
[»] Sprachvergleich
Das einfache Gleichheitszeichen = dient in Java nur der Zuweisung. Das ist in fast allen Programmiersprachen so. Selten verwenden Programmiersprachen für die Zuweisung ein anderes Symbol, etwa Pascal := oder F# und R <-. Um Zuweisungen von Vergleichen trennen zu können, definiert Java hier, der C(++)-Tradition folgend, einen binären Vergleichsoperator == mit zwei Gleichheitszeichen. Der Vergleichsoperator liefert immer den Ergebnistyp boolean:
int baba = 1;
System.out.println( baba == 1 ); // "true": Ausdruck mit Vergleich
System.out.println( baba = 2 ); // "2": Ausdruck mit Zuweisung
Zuweisungen sind auch Ausdrücke
Zwar finden sich Zuweisungen oft als Ausdrucksanweisung wieder, doch können sie an jeder Stelle stehen, an der ein Ausdruck erlaubt ist, etwa in einem Methodenaufruf wie printXXX(…):
int a = 1; // Deklaration mit Initialisierung
a = 2; // Anweisung mit Zuweisung
System.out.println( a = 3 ); // Ausdruck mit Zuweisung. Liefert 3.
Mehrere Zuweisungen in einem Schritt
Zuweisungen der Form a = b = c = 0; sind erlaubt und gleichbedeutend mit den drei Anweisungen c = 0; b = c; a = b;. Die explizite Klammerung a = (b = (c = 0)) macht noch einmal deutlich, dass sich Zuweisungen verschachteln lassen und Zuweisungen wie c = 0 Ausdrücke sind, die einen Wert liefern. Doch auch dann, wenn wir meinen, dass
eine coole Vereinfachung im Vergleich zu
b = c + d;
a = b + e;
ist, sollten wir mit einer Zuweisung pro Zeile auskommen.
Die Reihenfolge der Auswertung zeigt anschaulich folgendes Beispiel:
int b = 10;
System.out.println( (b = 20) * b ); // 400
System.out.println( b ); // 20
Im Produktivcode sollte so etwas dennoch nicht stehen.
2.4.2 Arithmetische Operatoren
Ein arithmetischer Operator verknüpft die Operanden mit den Operatoren Addition (+), Subtraktion (–), Multiplikation (*) und Division (/). Zusätzlich gibt es den Restwert-Operator (%), der den bei der Division verbleibenden Rest betrachtet. Alle Operatoren sind für ganzzahlige Werte sowie für Fließkommazahlen definiert. Die arithmetischen Operatoren sind binär, und auf der linken und rechten Seite sind die Typen numerisch. Der Ergebnistyp ist ebenfalls numerisch.
Numerische Umwandlung
Bei Ausdrücken mit unterschiedlichen numerischen Datentypen, etwa int und double, bringt der Compiler vor der Anwendung der Operation alle Operanden auf den umfassenderen Typ. Vor der Auswertung von 1 + 2.0 wird somit die Ganzzahl 1 in ein double konvertiert und dann die Addition vorgenommen – das Ergebnis ist auch vom Typ double. Das nennt sich numerische Umwandlung (engl. numeric promotion). Bei byte und short gilt die Sonderregelung, dass sie vorher in int konvertiert werden.[ 77 ](https://docs.oracle.com/javase/specs/jls/se11/html/jls-5.html#jls-5.6.1) (Auch im Java-Bytecode gibt es keine arithmetischen Operationen auf byte, short und char.) Anschließend wird die Operation ausgeführt, und der Ergebnistyp entspricht dem umfassenderen Typ.
Der Divisionsoperator
Der binäre Operator / bildet den Quotienten aus Dividend und Divisor. Auf der linken Seite steht der Dividend und auf der rechten der Divisor. Die Division ist für Ganzzahlen und für Fließkommazahlen definiert. Bei der Ganzzahldivision wird zu null hin gerundet, und das Ergebnis ist keine Fließkommazahl, sodass 1/3 das Ergebnis 0 ergibt und nicht 0,333… Den Datentyp des Ergebnisses bestimmen die Operanden und nicht der Operator. Soll das Ergebnis vom Typ double sein, muss mindestens ein Operand ebenfalls double sein.
System.out.println( 1.0 / 3 ); // 0.3333333333333333
System.out.println( 1 / 3.0 ); // 0.3333333333333333
System.out.println( 1 / 3 ); // 0
Strafe bei Division durch null
Schon die Schulmathematik lehrte uns, dass die Division durch null nicht erlaubt ist. Führen wir in Java eine Ganzzahldivision mit dem Divisor 0 durch, so bestraft uns Java mit einer ArithmeticException. Wird die ArithmeticException nicht behandelt, führt das zum Ende des ausführenden Threads, und wenn die ungeprüfte Ausnahme im main-Thread stattfindet und nicht abgefangen wird, führt dies zum Ende des Programms. Bei Fließkommazahlen liefert eine Division durch 0 keine Ausnahme, sondern +/– unendlich und bei 0.0/0.0 den Sonderwert NaN (mehr dazu folgt in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«). NaN steht für Not a Number (auch manchmal »Unzahl« genannt) und wird vom Prozessor erzeugt, falls er eine mathematische Operation wie die Division durch null nicht durchführen kann.
[»] Anekdote
Auf dem Lenkraketenkreuzer USS Yorktown gab ein Mannschaftsmitglied aus Versehen die Zahl Null ein. Das führte zu einer Division durch null, und der Fehler pflanzte sich so weit fort, dass die Software abstürzte und das Antriebssystem stoppte. Das Schiff trieb mehrere Stunden antriebslos im Wasser.
Der Restwert-Operator % *
Eine Ganzzahldivision muss nicht unbedingt glatt aufgehen, wie im Fall von 9/2. In diesem Fall gibt es den Rest 1. Diesen Rest liefert der Restwert-Operator (engl. remainder operator), oft auch Modulo genannt.[ 78 ](Mathematiker unterscheiden die beiden Begriffe Rest und Modulo, da ein Modulo nicht negativ ist, der Rest in Java aber schon. Das soll uns aber egal sein. ) Die Operanden können auch negativ sein.
[zB] Beispiel
System.out.println( 9 % 2 ); // 1
Die Division und der Restwert richten sich in Java nach einer einfachen Formel: (int)(a/b) × b + (a%b) = a.
[zB] Beispiel
Die Gleichung ist erfüllt, wenn wir etwa a = 10 und b = 3 wählen. Es gilt: (int)(10/3) = 3 und 10 % 3 ergibt 1. Dann ergeben 3 * 3 + 1 = 10.
Aus dieser Gleichung folgt, dass beim Restwert das Ergebnis nur dann negativ ist, wenn der Dividend negativ ist; das Ergebnis ist nur dann positiv, wenn der Dividend positiv ist. Es ist leicht einzusehen, dass das Ergebnis der Restwert-Operation immer echt kleiner ist als der Wert des Divisors. Wir haben den gleichen Fall wie bei der Ganzzahldivision, dass ein Divisor mit dem Wert 0 eine ArithmeticException auslöst und bei Fließkommazahlen zum Ergebnis NaN führt.
System.out.println( "+5% +3 = " + (+5% +3) ); // 2
System.out.println( "+5 / +3 = " + (+5 / +3) ); // 1
System.out.println( "+5% -3 = " + (+5% -3) ); // 2
System.out.println( "+5 / -3 = " + (+5 / -3) ); // -1
System.out.println( "-5% +3 = " + (-5% +3) ); // -2
System.out.println( "-5 / +3 = " + (-5 / +3) ); // -1
System.out.println( "-5% -3 = " + (-5% -3) ); // -2
System.out.println( "-5 / -3 = " + (-5 / -3) ); // 1
Gewöhnungsbedürftig ist die Tatsache, dass der erste Operand (Dividend) das Vorzeichen des Restes definiert und niemals der zweite (Divisor). In Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, werden wir eine Methode floorMod(…) kennenlernen, die etwas anders arbeitet.
[»] Hinweis
Um mit value % 2 == 1 zu testen, ob value eine ungerade Zahl ist, muss value positiv sein, denn -3 % 2 wertet Java zu –1 aus. Der Test auf ungerade Zahlen wird erst wieder korrekt mit value % 2 != 0.
Restwert für Fließkommazahlen
Der Restwert-Operator ist auch auf Fließkommazahlen anwendbar, und die Operanden können wiederum negativ sein.
[zB] Beispiel
Teste, ob eine double-Zahl doch eine Ganzzahl ist: (d % 1) == 0.
Wem das zu verrückt ist, der nutzt alternativ d == Math.rint(d).
Restwert für Fließkommazahlen und Math.IEEEremainder( ) *
Über die oben genannte Formel können wir auch bei Fließkommazahlen das Ergebnis einer Restwert-Operation leicht berechnen. Dabei muss beachtet werden, dass sich der Operator nicht so wie unter IEEE 754 verhält. Denn diese Norm schreibt vor, dass die Restwert-Operation den Rest von einer rundenden Division berechnet und nicht von einer abschneidenden. So wäre das Verhalten nicht analog zum Restwert bei Ganzzahlen. Java definiert den Restwert jedoch bei Fließkommazahlen genauso wie den Restwert bei Ganzzahlen. Wünschen wir ein Restwert-Verhalten, wie IEEE 754 es vorschreibt, so können wir die statische Bibliotheksmethode Math.IEEEremainder(…)[ 79 ](Es gibt auch Methoden, die nicht mit Kleinbuchstaben beginnen, wobei das sehr selten ist und nur in Sonderfällen auftritt. ieeeRemainder() sah für die Autoren nicht nett aus. ) verwenden.
Auch bei der Restwert-Operation bei Fließkommazahlen werden wir niemals eine Exception erwarten. Eventuelle Fehler werden, wie im IEEE-Standard beschrieben, mit NaN angegeben. Ein Überlauf oder Unterlauf kann zwar vorkommen, aber nicht geprüft werden.
Rundungsfehler *
Prinzipiell sollten Anweisungen wie 1.1 - 0.1 immer 1.0 ergeben, jedoch treten interne Rundungsfehler bei der Darstellung auf und lassen das Ergebnis von Berechnung zu Berechnung immer ungenauer werden. Ein besonders ungünstiger Fehler trat 1994 beim Pentium-Prozessor im Divisionsalgorithmus Radix-4 SRT auf, ohne dass der Programmierer der Schuldige war:
double x, y, z;
x = 4195835.0;
y = 3145727.0;
z = x - (x/y) * y;
System.out.println( z );
Ein fehlerhafter Prozessor liefert hier 256, obwohl laut Rechenregel das Ergebnis 0 sein muss. Laut Intel sollte für einen normalen Benutzer (Spieler, Softwareentwickler, Surfer?) der Fehler nur alle 27.000 Jahre auftauchen. Glück für die meisten. Eine Studie von IBM errechnete eine Fehlerhäufigkeit von einmal in 24 Tagen. Alles in allem nahm Intel die CPUs zurück, verlor über 400 Millionen US-Dollar und zog spät den Kopf gerade noch aus der Schlinge.
Die meisten Rundungsfehler resultieren aber daher, dass endliche Dezimalbrüche im Rechner als Näherungswerte für periodische Binärbrüche repräsentiert werden müssen. 0.1 entspricht einer periodischen Mantisse im IEEE-Format.
2.4.3 Unäres Minus und Plus
Die binären Operatoren sitzen zwischen zwei Operanden, während sich ein unärer Operator genau einen Operanden vornimmt. Das unäre Minus (Operator zur Vorzeichenumkehr) etwa dreht das Vorzeichen des Operanden um. So wird aus einem positiven Wert ein negativer und aus einem negativen Wert ein positiver.
[zB] Beispiel
Drehe das Vorzeichen einer Zahl um: a = -a;
Eine Alternative ist: a = –1 * a;
Das unäre Plus ist eigentlich unnötig; die Entwickler haben es jedoch aus Symmetriegründen mit eingeführt.
[zB] Beispiel
Minus und Plus sitzen direkt vor dem Operanden, und der Compiler weiß selbstständig, ob dies unär oder binär ist. Der Compiler erkennt auch folgende Konstruktion:
int i = - - - 2 + - + 3;
Dies ergibt den Wert –5. Es ist leichter, den Compiler zu verstehen, wenn wir die Operatorrangfolge einbeziehen und gedanklich Klammen setzen: -(-(-2)) + (-(+3)). Einen Ausdruck wie ‐--2+-+3 erkennt der Compiler dagegen nicht an, da die zusammenhängenden Minuszeichen als Dekrement interpretiert werden und nicht als unärer Operator. Das Trennzeichen, Leerzeichen in diesem Fall, ist also bedeutend.
2.4.4 Präfix- oder Postfix-Inkrement und -Dekrement
Das Herauf- und Heruntersetzen von Variablen ist eine sehr häufige Operation, wofür die Entwickler in der Vorgängersprache C auch einen Operator spendiert hatten. Die praktischen Operatoren ++ und -- kürzen die Programmzeilen zum Inkrement und Dekrement ab:
i++; // Abkürzung für i = i + 1
j--; // j = j - 1
Eine lokale Variable muss allerdings vorher initialisiert sein, da ein Lesezugriff vor einem Schreibzugriff stattfindet. Der ++/––-Operator erfüllt somit zwei Aufgaben: Neben der Wertrückgabe gibt es eine Veränderung der Variablen.
[»] Hinweis
Das Post-Inkrement finden wir auch im Namen der Programmiersprache C++. Es soll ausdrücken, dass es »C-mit-eins-drauf« ist, also ein verbessertes C. Mit dem Wissen über den Postfix-Operator ist klar, dass diese Erhöhung aber erst nach der Nutzung auftritt – also ist C++ auch nur C, und der Vorteil kommt später. Einer der Entwickler von Java, Bill Joy, beschrieb einmal Java als C++-- . Er meinte damit C++ ohne die schwer zu pflegenden Eigenschaften.
Vorher oder nachher?
Die beiden Operatoren liefern einen Ausdruck und geben daher einen Wert zurück. Es macht jedoch einen feinen Unterschied, wo dieser Operator platziert wird. Es gibt ihn nämlich in zwei Varianten: Er kann vor der Variablen stehen, wie in ++i (Präfix-Schreibweise), oder dahinter, wie bei i++ (Postfix-Schreibweise). Der Präfix-Operator verändert die Variable vor der Auswertung des Ausdrucks, und der Postfix-Operator ändert sie nach der Auswertung des Ausdrucks. Mit anderen Worten: Nutzen wir einen Präfix-Operator, so wird die Variable erst herauf- bzw. heruntergesetzt und dann der Wert geliefert.
[zB] Beispiel
Präfix/Postfix in einer Ausgabeanweisung:
Präfix-Inkrement und -Dekrement | Postfix-Inkrement und -Dekrement |
---|---|
int i = 10, j = 20; | int i = 10, j = 20; |
Mit der Möglichkeit, Variablen zu erhöhen und zu vermindern, ergeben sich vier Varianten:
Präfix | Postfix | |
---|---|---|
Inkrement | ||
Dekrement |
[»] Hinweis
In Java sind Inkrement (++) und Dekrement (--) für alle numerischen Datentypen erlaubt, also auch für Fließkommazahlen:
double d = 12;
System.out.println( --d ); // 11.0
double e = 12.456;
System.out.println( --e ); // 11.456
Einige Kuriositäten *
Wir wollen uns abschließend noch mit einer Besonderheit des Post-Inkrements und Prä-Inkrements beschäftigen, die nicht nachahmenswert ist:
int i = 1;
i = ++i;
System.out.println( i ); // 2
int j = 1;
j = j++;
System.out.println( j ); // 1
Der erste Fall überrascht nicht, denn i = ++i erhöht den Wert 1 um 1, und anschließend wird 2 der Variablen i zugewiesen. Bei j ist es raffinierter: Der Wert von j ist 1, und dieser Wert wird intern vermerkt. Anschließend erhöht j++ die Variable um 1. Doch die Zuweisung setzt j auf den gemerkten Wert, der 1 war. Also ist j = 1.
[»] Sprachvergleich: Sequenzpunkte in C(++) *
Je mehr Freiheiten ein Compiler hat, desto ungenierter kann er optimieren. Besonders Schreibzugriffe interessieren einen Compiler, denn kann er diese einsparen, läuft das Programm später ein bisschen schneller. Damit das Resultat eines Compilers jedoch beherrschbar bleibt, definiert der C(++)-Standard Sequenzpunkte (engl. sequence points), an denen alle Schreibzugriffe klar zugewiesen wurden (dass der Compiler später Optimierungen vornimmt, ist eine andere Geschichte; die Sequenzpunkte gehören zum semantischen Modell, Optimierungen verändern das nicht). Das Semikolon als Abschluss von Anweisungen bildet zum Beispiel einen Sequenzpunkt. In einem Ausdruck wie i = i + 1; j = i; muss der Schreibzugriff auf i aufgelöst sein, bevor ein Lesezugriff für die Zuweisung zu j erfolgt. Problematisch ist, dass es gar nicht so viele Sequenzpunkte gibt und es passieren kann, dass zwischen zwei Sequenzpunkten zwei mehrdeutige Schreibzugriffe auf die gleiche Variable stattfinden. Da das jedoch in C(++) undefiniert ist, kann sich der Compiler so verhalten, wie er will – er muss sich ja nur an den Sequenzpunkten so verhalten, wie gefordert. Problemfälle sind: (i=j) + i oder, weil ein Inkrement/Dekrement ein Lese-/Schreibzugriff ist, auch i = i++, was ja nichts anderes als i = (i = i + 1) ist. Bedauerlicherweise bildet die Zuweisung keinen Sequenzpunkt. Bei Zuweisungen der Art i = ++i + --i kann alles Mögliche später in i stehen, je nachdem, was der Compiler zu welchem Zeitpunkt ausführt. In Java sind diese Dinge von der Spezifikation klar geregelt; in C(++) ist nur geregelt, dass das Verhalten zwischen den Sequenzpunkten klar sein muss. Doch moderne Compiler erkennen konkurrierende Schreibzugriffe zwischen zwei Sequenzpunkten und mahnen sie (bei entsprechender Warnstufe) an.[ 80 ](Beim GCC-Compiler ist das der Schalter -Wsequence-point (der auch bei -Wall mitgenommen wird), siehe dazu http://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html. )
2.4.5 Zuweisung mit Operation (Verbundoperator)
In Java lassen sich Zuweisungen mit numerischen Operatoren kombinieren. Für einen binären Operator (symbolisch # genannt) im Ausdruck a = a # (b) kürzt der Verbundoperator (engl. compound assignment operator) den Ausdruck zu a #= b ab.[ 81 ](Die Abkürzung existiert nur im Programmcode, im Bytecode gibt es keinen Verbundoperator. )
Dazu einige Beispiele:
Ausführliche Schreibweise | Schreibweise mit Verbundoperator |
---|---|
a = a + 2 | a += 2 |
a = a - 10 | a -= 10 |
a = a * –1 | a *= –1 |
a = a / 10 | a /= 10 |
Während das Präfix-/Postfix-Inkrement/-Dekrement nur um eins vergrößert/vermindert, erlauben die Verbundoperationen in einer relativ kompakten Schreibweise auch größere Inkremente/Dekremente, wie eben a+=2 oder a-=10.
[zB] Beispiel
Eine Zuweisung ist auch immer ein Ausdruck:
int a = 0;
System.out.println( a ); // 0
System.out.println( a = 2 ); // 2
System.out.println( a += 1 ); // 3
System.out.println( a ); // 3
Besondere Obacht sollten wir auf die automatische Klammerung geben. Bei einem Ausdruck wie a *= 3 + 5 gilt a = a * (3 + 5) und nicht selbstverständlich die Punkt-vor-Strich-Regelung a = a * 3 + 5.
[»] Sprachspielerei: Ein neuer Operator?
Kaum bekannt ist der »Sleepy-Operator« -=-. Beispiel: i -=- i. Was passiert hier?
Einmalige Auswertung bei Array-Zugriffen *
Falls die linke Seite beim Verbundoperator ein Array-Zugriff ist (siehe Abschnitt 4.1, »Einfache Feldarbeit«), wird die Indexberechnung nur einmal vorgenommen. Dies ist wichtig beim Einsatz des Präfix-/Postfix-Operators oder von Methodenaufrufen, die Nebenwirkungen besitzen, also etwa Zustände wie einen Zähler verändern.[ 82 ](Details in der JLS unter https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.26.2)
[zB] Beispiel
Wir profitieren bei Array-Zugriffen vom Verbundoperator, da erstens die Schreibweise kurz ist und zweitens die Auswertung des Index nur einmal stattfindet:
int[] array1 = { 10, 90, 0 };
int i = 0;
array1[++i] = array1[++i] + 10;
System.out.println( Arrays.toString( array1 ) ); // [10, 10, 0]
int[] array2 = { 0, 90, 0 };
int j = 0;
array2[++j] += 10;
System.out.println( Arrays.toString( array2 ) ); // [0, 100, 0]
2.4.6 Die relationalen Operatoren und die Gleichheitsoperatoren
Relationale Operatoren sind Vergleichsoperatoren, die Ausdrücke miteinander vergleichen und einen Wahrheitswert vom Typ boolean ergeben. Die von Java für numerische Vergleiche zur Verfügung gestellten Operatoren sind:
größer (>)
kleiner (<)
größer/gleich (≥)
kleiner/gleich (≤)
Außerdem gibt es einen Spezialoperator instanceof zum Testen von Typbeziehungen.
Zudem kommen zwei Vergleichsoperatoren hinzu, die Java als Gleichheitsoperatoren bezeichnet:
Test auf Gleichheit (==)
Test auf Ungleichheit (!=)
Dass Java hier einen Unterschied zwischen Gleichheitsoperatoren und Vergleichsoperatoren macht, liegt an einem etwas anderen Vorrang, der uns aber nicht weiter beschäftigen soll.
Ebenso wie arithmetische Operatoren passen die relationalen Operatoren ihre Operanden an einen gemeinsamen Typ an. Handelt es sich bei den Typen um Referenztypen, so sind nur die Vergleichsoperatoren == und != erlaubt – eine Ausnahme ist der String, bei dem auch + erlaubt ist.
Kaum Verwechslungsprobleme durch == und =
Die Verwendung des relationalen Operators == und der Zuweisung = führt bei Einsteigern oft zu Problemen, da die Mathematik für Vergleiche und Zuweisungen immer nur ein Gleichheitszeichen kennt. Glücklicherweise ist das Problem in Java nicht so drastisch wie beispielsweise in C(++), da die Typen der Operatoren unterschiedlich sind. Der Vergleichsoperator ergibt immer nur den Rückgabewert boolean. Zuweisungen von numerischen Typen ergeben jedoch wieder einen numerischen Typ. Es kann also kein Problem wie das folgende geben:
int a = 10, b = 11;
boolean result1 = ( a = b ); // Compilerfehler
boolean result2 = ( a == b );
[zB] Beispiel
Die Wahrheitsvariable hasSign soll dann true sein, wenn das Zeichen sign gleich dem Minus ist:
boolean hasSign = (sign == '-');
Die Auswertungsreihenfolge ist folgende: Erst wird das Ergebnis des Vergleichs berechnet, und dieser Wahrheitswert wird anschließend in hasSign kopiert.
[»] (Anti-)Stil
Bei einem Vergleich mit == können beide Operanden vertauscht werden – wenn die beiden Seiten keine beeinflussenden Seiteneffekte produzieren, also etwa Zustände ändern. Am Ergebnis ändert sich nichts, denn der Vergleichsoperator ist kommutativ. So sind
if ( worldExpoShanghaiCostInUSD == 58000000000L )
und
if ( 58000000000L == worldExpoShanghaiCostInUSD )
semantisch gleich. Bei einem Gleichheitsvergleich zwischen Variable und Literal werden viele Entwickler mit einer Vergangenheit in der Programmiersprache C die Konstanten links und die Variable rechts setzen. Der Grund für diesen sogenannten Yoda-Stil[ 83 ](Yoda ist eine Figur aus Star Wars, die eine für uns ungewöhnliche Satzstellung nutzt. Anstatt Sätze mit Subjekt + Prädikat + Objekt (SPO) aufzubauen, nutzt Yoda die Form Objekt + Subjekt + Prädikat (OSP); Objekt und Subjekt sind umgedreht, so wie die Operanden aus dem Beispiel auch, sodass dieser Ausdruck sich so lesen würde: »Wenn 58000000000 gleich worldExpoShanghaiCostInUSD ist« statt der üblichen SPO-Lesung »wenn worldExpoShanghaiCostInUSD ist gleich 58000000000«. Im Arabischen ist diese OSP-Stellung üblich, sodass Entwickler aus dem arabischen Sprachraum diese Form eigentlich natürlich finden könnten. Wenn das mal nicht eine Studie wert ist … ) ist die Vermeidung von Fehlern. Fehlt in C ein Gleichheitszeichen, so ist if(worldExpoShanghaiCostInUSD = 58000000000L) als Zuweisung compilierbar (wenn auch mittlerweile mit einer Warnung), if(58000000000L = worldExpoShanghaiCostInUSD) aber nicht. Die erste fehlerhafte Version initialisiert eine Variable und springt immer in die if-Anweisung, da in C jeder Ausdruck (hier von der Zuweisung, die ja ein Ausdruck ist) ungleich 0 als wahr interpretiert wird. Das ist ein logischer Fehler, den die zweite Schreibweise verhindert, denn sie führt zu einem Compilerfehler. In Java ist dieser Fehlertyp nicht zu finden – es sei denn, der Variablentyp ist boolean, was sehr selten vorkommt –, und daher sollte diese Yoda-Schreibweise vermieden werden.
2.4.7 Logische Operatoren: Nicht, Und, Oder, XOR
Die Abarbeitung von Programmcode ist oft an Bedingungen geknüpft. Diese Bedingungen sind oftmals komplex zusammengesetzt, wobei drei Operatoren am häufigsten vorkommen:
Nicht (Negation): Dreht die Aussage um; aus wahr wird falsch, und aus falsch wird wahr.
Und (Konjunktion): Beide Aussagen müssen wahr sein, damit die Gesamtaussage wahr wird.
Oder (Disjunktion): Eine der beiden Aussagen muss wahr sein, damit die Gesamtaussage wahr wird.
[»] Hinweis
Mehr als diese drei logischen Operatoren sind auch nicht nötig, um alle möglichen logischen Verknüpfungen zu realisieren. In der Mathematik wird das boolesche Algebra genannt.
Mit logischen Operatoren werden Wahrheitswerte nach definierten Mustern verknüpft. Logische Operatoren operieren nur auf boolean-Typen, andere Typen führen zu Compilerfehlern. Java bietet die Operatoren Nicht (!), Und (&&), Oder (||) und XOR (^) an. XOR ist eine Operation, die nur dann wahr liefert, wenn genau einer der beiden Operanden true ist. Sind beide Operanden gleich (also entweder true oder false), so ist das Ergebnis false. XOR heißt auch exklusives oder ausschließendes Oder. Im Deutschen trifft die Formulierung »entweder … oder« diesen Sachverhalt gut: Entweder ist es das eine oder das andere, aber nicht beides zusammen. Beispiel: »Willst du entweder ins Kino oder DVD schauen?« a ^ b ist eine Abkürzung für (a && !b) || (!a && b).
boolean a | boolean b | ! a | a && b | a || b | a ^ b |
---|---|---|---|---|---|
true | true | false | true | true | false |
true | false | false | false | true | true |
true | true | false | true | true | |
false | false | true | false | false | false |
Die logischen Operatoren arbeiten immer auf dem Typ boolean. In Abschnitt 21.1.1, »Die Bit-Operatoren Komplement, Und, Oder und XOR«, werden wir sehen, dass sich die gleichen Verknüpfungen auf jedem Bit einer Ganzzahl durchführen lassen.
[»] Ausblick auf die Aussagenlogik *
Verknüpfungen dieser Art sind in der Aussagenlogik bzw. booleschen Algebra sehr wichtig. Die für uns gängigen Begriffe Und, Oder, XOR sind dort auch unter anderen Namen bekannt. Die Und-Verknüpfung nennt sich Konjunktion, die Oder-Verknüpfung Disjunktion, und das exklusive Oder heißt Kontravalenz. Die drei binären Operatoren Und, Oder, XOR decken bestimmte Verknüpfungen ab, jedoch nicht alle, die prinzipiell möglich sind. In der Aussagenlogik gibt es außerdem die Implikation (Wenn-dann-Verknüpfung) und die Äquivalenz.
2.4.8 Kurzschluss-Operatoren
Ein logischer Ausdruck muss nur dann weiter ausgewertet werden, wenn sich das Endergebnis noch ändern kann. Steht das Ergebnis schon vor der Auswertung aller Teile unumstößlich fest, kürzt der Compiler den Programmfluss ab. Die beiden Operatoren && (Und) bzw. || (Oder) bieten sich zur Optimierung der Ausdrücke an:
Und: Ist einer der beiden Ausdrücke falsch, so kann der Ausdruck schon nicht mehr wahr werden. Das Ergebnis ist falsch.
Oder: Ist mindestens einer der Ausdrücke schon wahr, so ist auch der gesamte Ausdruck wahr.
Nehmen wir zum Beispiel true || Math.random() > 0.5; hier wird es nicht zum Aufruf der Methode kommen, denn die beiden Operatoren && und || sind Kurzschluss-Operatoren (engl. short-circuit operators).[ 84 ](Den Begriff verwendet die Java-Sprachdefinition nicht! Siehe dazu auch https://docs.oracle.com/javase/specs/jls/se11/html/jls-15.html#jls-15.23. ) Das Kennzeichen der Kurzschluss-Operatoren ist also eine Abkürzung, wenn das Ergebnis des Ausdrucks feststeht; die restlichen Ausdrücke werden nicht ausgewertet.
Nicht-Kurzschluss-Operatoren *
In einigen Fällen ist es erwünscht, dass die Laufzeitumgebung alle Teilausdrücke auswertet. Das kann der Fall sein, wenn Methoden Nebenwirkungen haben sollen, etwa Zustände ändern. Daher bietet Java zusätzlich die nicht über einen Kurzschluss arbeitenden Operatoren | und & an, die eine Auswertung aller Teilausdrücke erzwingen. Das Ergebnis der Auswertung ist das Gleiche wie vorher.
Die Arbeitsweise dokumentiert das folgende Programm, bei dem gut abzulesen ist, dass eine Variable nur dann erhöht wird, wenn der Nicht-Kurzschluss-Operator auswertet:
int a = 0, b = 0, c = 0, d = 0;
System.out.println( true || a++ == 0 ); // true, a wird nicht erhöht
System.out.println( a ); // 0
System.out.println( true | b++ == 0 ); // true, b wird erhöht
System.out.println( b ); // 1
System.out.println( false && c++ == 0 ); // false, c wird nicht erhöht
System.out.println( c ); // 0
System.out.println( false & d++ == 0 ); // false, d wird erhöht
System.out.println( d ); // 1
Für XOR kann es keinen Kurzschluss-Operator geben, da immer beide Operanden ausgewertet werden müssen, bevor das Ergebnis feststeht.
[»] Hinweis
Unter gewissen Voraussetzungen kann der Verzicht auf den Kurzschlussoperator performanter sein, weil der Compiler Verzweigungen und Sprünge einsparen kann.
2.4.9 Der Rang der Operatoren in der Auswertungsreihenfolge
Aus der Schule ist der Spruch »Punktrechnung geht vor Strichrechnung« bekannt, sodass sich der Ausdruck 1 + 2 × 3 zu 7 und nicht zu 9 auswertet.[ 85 ](Dass von diesen Rechnungen eine gewisse Spannung ausgeht, zeigen diverse Fernsehkanäle, die damit ihr Abendprogramm füllen. )
[zB] Beispiel
Auch wenn bei Ausdrücken wie a() + b() * c() zuerst das Produkt gebildet wird, schreibt doch die Auswertungsreihenfolge von binären Operatoren vor, dass der linke Operand zuerst ausgewertet werden muss, was bedeutet, dass Java zuerst die Methode a() aufruft.
Neben Plus und Mal gibt es eine Vielzahl von Operatoren, die alle ihre eigenen Vorrangregeln besitzen.[ 86 ](Es gibt (sehr alte) Programmiersprachen wie APL, die keine Vorrangregeln kennen. Sie werten die Ausdrücke streng von rechts nach links oder umgekehrt aus. ) Der Multiplikationsoperator besitzt zum Beispiel eine höhere Priorität und damit eine andere Auswertungsreihenfolge als der Plus-Operator.
Die Rangordnung der Operatoren (engl. operator precedence) ist in Tabelle 2.11 aufgeführt, wobei auf den Lambda-Pfeil -> verzichtet wird. Der arithmetische Typ steht für Ganz- und Fließkommazahlen, der integrale Typ für char und Ganzzahlen und der Eintrag »primitiv« für jegliche primitiven Datentypen (also auch boolean):
Operator | Typ | Beschreibung | |
---|---|---|---|
++, -- | 1 | arithmetisch | |
+, - | 1 | arithmetisch | |
~ | 1 | integral | |
! | 1 | boolean | |
(Typ) | 1 | jeder | |
*, /, % | 2 | arithmetisch | Multiplikation, Division, Rest |
+, - | 3 | arithmetisch | binärer Operator für Addition und Subtraktion |
+ | 3 | String | |
<< | 4 | integral | |
>> | 4 | integral | |
>>> | 4 | integral | Rechtsverschiebung ohne Vorzeichenerweiterung |
<, <=, >, >= | 5 | arithmetisch | numerische Vergleiche |
instanceof | 5 | Objekt | |
==, != | 6 | primitiv | Gleich-/Ungleichheit von Werten |
==, != | 6 | Objekt | Gleich-/Ungleichheit von Referenzen |
& | 7 | integral | |
& | 7 | boolean | |
8 | integral | ||
^ | 8 | boolean | |
| | 9 | integral | |
| | 9 | boolean | |
&& | 10 | boolean | logisches konditionales Und, Kurzschluss |
|| | 11 | boolean | logisches konditionales Oder, Kurzschluss |
?: | 12 | jeder | |
= | 13 | jeder | |
*=, /=, %=, +=, | 14 | arithmetisch | |
+= | 14 | String |
Die Rechenregel für »Mal vor Plus« kann sich jeder noch leicht merken. Auch ist leicht zu merken, dass die typischen arithmetischen Operatoren wie Plus und Mal eine höhere Priorität als Vergleichsoperationen haben. Komplizierter ist die Auswertung bei den zahlreichen Operatoren, die seltener im Programm vorkommen.
[zB] Beispiel
Wie ist die Auswertung bei dem nächsten Ausdruck?
boolean A = false,
B = false,
C = true;
System.out.println( A && B || C );
Das Ergebnis könnte je nach Rangordnung true oder false sein. Doch die Tabelle lehrt uns, dass im Beispiel das Und stärker als das Oder bindet, also der Ausdruck als (A && B) || C und nicht als A && (B || C) gelesen wird und somit zu true ausgewertet wird.
Vermutlich gibt es Programmierer, die dies wissen oder eine Tabelle mit Rangordnungen am Monitor kleben haben. Aber beim Durchlesen von fremdem Code ist es nicht schön, immer wieder die Tabelle konsultieren zu müssen, die verrät, ob nun das binäre XOR oder das binäre Und stärker bindet.
[+] Tipp
Alle Ausdrücke, die über die einfache Regel »Punktrechnung geht vor Strichrechnung« hinausgehen, sollten geklammert werden. Da die unären Operatoren ebenfalls sehr stark binden, kann eine Klammerung in ihrem Fall wegfallen.
Links- und Rechtsassoziativität *
Bei den Operatoren + und * gilt die mathematische Kommutativität und Assoziativität. Das heißt, die Operanden können prinzipiell umgestellt werden, und das Ergebnis sollte davon nicht beeinträchtigt sein. Bei der Division unterscheiden wir zusätzlich Links- und Rechtsassoziativität. Deutlich wird das am Beispiel A / B / C. Den Ausdruck wertet Java von links nach rechts aus, und zwar als (A / B) / C; daher ist der Divisionsoperator linksassoziativ. Hier sind Klammern angemessen. Denn würde der Compiler den Ausdruck zu A / (B / C) auswerten, käme dies einem A * C / B gleich. In Java sind die meisten Operatoren linksassoziativ, aber es gibt Ausnahmen, wie Zuweisungen der Art A = B = C, die der Compiler zu A = (B = C) auswertet.
[»] Hinweis
Die mathematische Assoziativität ist natürlich gefährdet, wenn durch Überläufe oder Nichtdarstellbarkeit Rechenfehler mit im Spiel sind:
float a = -16777217F;
float b = 16777216F;
float c = 1F;
System.out.println( a + b + c ); // 1.0
System.out.println( a + (b + c) ); // 0.0
Mathematisch ergibt –16.777.217 + 16.777.216 den Wert –1, und –1 plus +1 ist 0. Im zweiten Fall liefert –16.777.217 + (16.777.216 + 1) = –16.777.217 + 16.777.217 = 0. Doch Java wertet a + b durch die Beschränkung von float zu 0 aus, sodass bei 0 plus c die Ausgabe 1 statt 0 erscheint.
2.4.10 Die Typumwandlung (das Casting)
Java eine statisch typisierte Sprache, aber sie ist nicht so stark typisiert, dass es hinderlich ist. So übersetzt der Compiler die folgenden Zeilen problemlos:
int anInt = 1;
long long1 = 1;
long long2 = anInt;
Streng genommen könnte ein Compiler bei einer sehr starken Typisierung die letzten beiden Zeilen ablehnen, denn das Literal 1 ist vom Typ int und kein 1L, also long, und in long2 = anInt ist die Variable anInt vom Typ int statt vom gewünschten Datentyp long.
Arten der Typumwandlung
In der Praxis kommt es also vor, dass Datentypen konvertiert werden müssen. Dies nennt sich Typumwandlung (engl. typecast, kurz cast). Java unterscheidet zwei Arten der Typumwandlung:
Automatische (implizite) Typumwandlung: Daten eines kleineren Datentyps werden automatisch (implizit) dem größeren angepasst. Der Compiler nimmt diese Anpassung selbstständig vor. Daher funktioniert unser erstes Beispiel mit etwa long2 = anInt.
Explizite Typumwandlung: Ein größerer Typ kann einem kleineren Typ mit möglichem Verlust von Informationen zugewiesen werden.
Typumwandlungen gibt es bei primitiven Datentypen und bei Referenztypen. Während die folgenden Absätze die Anpassungen bei einfachen Datentypen beschreiben, kümmert sich Kapitel 6, »Eigene Klassen schreiben«, um die Typkompatibilität bei Referenzen.
Automatische Anpassung der Größe
Werte der Datentypen byte und short werden bei Rechenoperationen automatisch in den Datentyp int umgewandelt. Ist ein Operand vom Datentyp long, dann werden alle Operanden auf long erweitert. Wird aber short oder byte als Ergebnis verlangt, dann ist dieses durch einen expliziten Typecast anzugeben, und nur die niederwertigen Bits des Ergebniswerts werden übergeben. Folgende Typumwandlungen führt Java automatisch aus:
Vom Typ | In den Typ |
---|---|
byte | short, int, long, float, double |
short | int, long, float, double |
char | int, long, float, double |
int | long, float, double |
long | float, double |
float | double |
Die Anpassung wird im Englischen widening conversion genannt, weil sie den Wertebereich automatisch erweitert. Der Typ boolean taucht nicht auf; er lässt sich in keinen anderen primitiven Typ konvertieren.
[»] Hinweis
Dass ein long auf ein double gebracht werden kann – das Gleiche gilt für int auf float –, ist wohl im Nachhinein als Fehler in der Sprache Java zu betrachten, denn es gehen Informationen verloren. Ein double kann die 64 Bit für Ganzzahlen nicht so »effizient« nutzen wie ein long. Ein Beispiel:
System.out.println( Long.MAX_VALUE ); // 9223372036854775807
double d = Long.MAX_VALUE;
System.out.printf( "%.0f%n", d ); // 9223372036854776000
System.out.println( (long) d ); // 9223372036854775807
Die implizite Typumwandlung sollte aber verlustfrei sein.
[»] Hinweis
Obwohl von der Datentypgröße her ein char (16 Bit) zwischen byte (8 Bit) und int (32 Bit) liegt, taucht der Typ nirgends in der rechten Spalte von Tabelle 2.12 auf, da char kein Vorzeichen speichern kann, während die anderen Datentypen byte, short, int, long, float, double alle ein Vorzeichen besitzen. Daher kann so etwas wie das Folgende nicht funktionieren:
byte b = 'b';
char c = b; // Type mismatch: cannot convert from byte to char
Explizite Typumwandlung
Die explizite Anpassung engt einen Typ ein, sodass diese Operation im Englischen narrowing conversion genannt wird. Der gewünschte Typ für eine Typumwandlung wird vor den umzuwandelnden Datentyp in Klammern gesetzt. Uns muss bewusst sein, dass bei jeder expliziten Typumwandlung Informationen verloren gehen können.
[zB] Beispiel
Umwandlung einer Fließkommazahl in eine Ganzzahl, der gesamte Nachkommaanteil verschwindet:
int n = (int) 3.1415; // n = 3
Eine Typumwandlung hat eine sehr hohe Priorität. Daher muss der Ausdruck gegebenenfalls geklammert werden.
[zB] Beispiel
Die Zuweisung an n verfehlt das Ziel:
int n = (int) 1.0315 + 2.1;
int m = (int)(1.0315 + 2.1); // das ist korrekt
Typumwandlung von Fließkommazahlen in Ganzzahlen
Bei der expliziten Typumwandlung von double und float in einen Ganzzahltyp kann es selbstverständlich zum Verlust von Genauigkeit kommen sowie zur Einschränkung des Wertebereichs. Bei der Konvertierung von Fließkommazahlen verwendet Java eine Rundung gegen null, schneidet also schlicht den Nachkommaanteil ab.
[zB] Beispiel
Explizite Typumwandlung einer Fließkommazahl in ein int:
System.out.println( (int) +12.34 ); // 12
System.out.println( (int) +67.89 ); // 67
System.out.println( (int) -12.34 ); // -12
System.out.println( (int) -67.89 ); // -67
int r = (int)(Math.random() * 5 ); // 0 <= r <= 4
Automatische Typumwandlung bei Berechnungen mit byte und short in int *
Eine Operation vom Typ int mit int liefert den Ergebnistyp int, und long mit long liefert ein long.
int int1 = 1, int2 = 2;
int int3 = int1 + int2;
long long1 = 1, long2 = 2;
long long3 = long1 + long2;
Diese Zeilen übersetzt der Compiler wie erwartet. Und so erscheint es intuitiv, dass das Gleiche auch für die Datentypen short und byte gilt. Während
short short1 = 1, short2 = 2;
byte byte1 = 1, byte2 = 2;
noch funktioniert, führt
short short3 = short1 + short2; // Type mismatch: cannot convert from int to short
byte byte3 = byte1 + byte2; // Type mismatch: cannot convert from int to byte
zu einem Compilerfehler.
Es ist nicht möglich, ohne explizite Typumwandlung zwei short- oder byte-Zahlen zu addieren. Richtig ist:
short short3 = (short)( short1 + short2 );
byte byte3 = (byte)( byte1 + byte2 );
Der Grund liegt beim Java-Compiler. Wenn Ganzzahl-Ausdrücke vom Typ kleiner int mit einem Operator verbunden werden, passt der Compiler eigenmächtig den Typ auf int an. Die Addition der beiden Zahlen im Beispiel arbeitet also nicht mit short- oder byte-Werten, sondern mit int-Werten; intern im Bytecode ist es ebenso realisiert. So führen also alle Ganzzahloperationen mit short und byte automatisch zum Ergebnistyp int. Und das führt bei der Zuweisung aus dem Beispiel zu einem Problem, denn steht auf der rechten Seite der Typ int und auf der linken Seite der kleinere Typ byte oder short, muss der Compiler einen Fehler melden. Mit der ausdrücklichen Typumwandlung erzwingen wir diese Konvertierung.
Dass der Compiler diese Anpassung vornimmt, müssen wir einfach akzeptieren. int und int bleibt int, long und long bleibt long. Wenn ein int mit einem long tanzt, wird der Ergebnistyp long. Arbeitet der Operator auf einem short oder byte, ist das Ergebnis automatisch int.
[+] Tipp
Kleine Typen wie short und byte führen oft zu Problemen. Wenn sie nicht absichtlich in großen Arrays verwendet werden und Speicherplatz nicht ein absolutes Kriterium ist, erweist sich int als die beste Wahl – auch weil Java nicht durch besonders intuitive Typkonvertierungen glänzt, wie das Beispiel mit dem unären Minus und Plus zeigt:
byte b = 0;
b = -b; // Cannot convert from int to byte
b = +b; // Cannot convert from int to byte
Der Compiler meldet einen Fehler, denn der Ausdruck auf der rechten Seite wird durch den unären Operator in ein int umgewandelt, was immer für die Typen byte, short und char gilt.[ 87 ](https://docs.oracle.com/javase/specs/jls/se14/html/jls-5.html#jls-5.6)
Keine Typumwandlung zwischen einfachen Typen und Referenztypen
Allgemeine Umwandlungen zwischen einfachen Typen und Referenztypen gibt es nicht. Falsch sind zum Beispiel:
String s = (String) 1; // Cannot cast from int to String
int i = (int) "1"; // Cannot cast from String to int
[»] Getrickse mit Boxing *
Einiges sieht dagegen nach Typumwandlung aus, ist aber in Wirklichkeit eine Technik, die sich Autoboxing nennt (Abschnitt 10.5, »Wrapper-Klassen und Autoboxing«, geht näher darauf ein):
Long lông = (Long) 2L; // Alternativ: Long lông = 2L;
System.out.println( (Boolean) true );
((Integer)2).toString();
Methoden zur Typumwandlung
Bei der explizierten Typumwandlung von Ganzzahlen schneidet Java die höherwertigen Bytes ab. Bei der Konvertierung von long (8 Byte) in ein int (4 Byte) fallen die oberen 4 Byte heraus. Sind diese vier oberen Byte 0x00 – wir betrachten nur die positiven Ganzzahlen –, dann gibt es keinen Verlust an Informationen. Wollen wir von long in int umwandeln, wobei aber ein Verlust von Informationen gemeldet werden soll, so kann die statische Math-Methode int toIntExact(long value) verwendet werden. Sie löst eine Ausnahme aus, wenn die Umwandlung einen Datenverlust bedeutet.
Bei der Umwandlung von Fließkommazahlen in Ganzzahlen kommt es immer zum Abschneiden der Nachkommastellen. Die Klasse Math hat Methoden, die auch runden können. Dazu zählen Math.round(float) und long round(double); Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, gibt detaillierte Auskunft über die Mathe-Klasse.
Typumwandlung beim Verbundoperator *
Beim Verbundoperator wird noch etwas mehr gemacht, als E1 #= E2 zu E1 = (E1) # (E2) aufzulösen, wobei # symbolisch für einen binären Operator steht. Interessanterweise kommt auch noch der Typ von E1 ins Spiel, denn der Ausdruck E1 # E2 wird vor der Zuweisung auf den Datentyp von E1 gebracht, sodass es genau heißen muss: E1 #= E2 wird zu E1 = (Typ von E1)((E1) # (E2)).
[zB] Beispiel
Der Verbundoperator soll eine Ganzzahl zu einer Fließkommazahl addieren:
int i = 1973;
i += 30.2;
Die Anwendung des Verbundoperators ist in Ordnung, denn der Übersetzer nimmt eine implizite Typumwandlung vor, sodass die Bedeutung bei i = (int)(i + 30.2) liegt. So viel dazu, dass Java eine intuitive und einfache Programmiersprache sein soll.
2.4.11 Überladenes Plus für Strings
Obwohl sich in Java die Operatoren fast alle auf primitive Datentypen beziehen, gibt es doch eine weitere Verwendung des Plus-Operators. Diese wurde in Java eingeführt, da ein Aneinanderhängen von Zeichenketten oft benötigt wird. Objekte vom Typ String können durch den Plus-Operator mit anderen Strings und Datentypen verbunden werden. Falls zusammenhängende Teile nicht alle den Datentyp String annehmen, werden sie automatisch in einen String umgewandelt. Der Ergebnistyp ist immer String.
[zB] Beispiel
Setze fünf Teile zu einem String zusammen:
String s = '"' + "Extrem Sandmännchen" + '"' + " frei ab " + 18;
// char String char String int
System.out.println( s ); // "Extrem Sandmännchen" frei ab 18
Die String-Konkatenation erfolgt strikt von links nach rechts und ist natürlich nicht kommutativ wie die numerische Addition. Besteht der Ausdruck aus mehreren Teilen, so muss die Auswertungsreihenfolge beachtet werden, andernfalls kommt es zu seltsamen Zusammensetzungen. So ergibt "Aufruf von " + 1 + 0 + 0 + " Ökonomen" tatsächlich »Aufruf von 100 Ökonomen« und nicht »Aufruf von 1 Ökonomen«, da der Compiler die Konvertierung in Strings dann startet, wenn er einen Ausdruck als String-Objekt erkannt hat.
Schauen wir uns die Auswertungsreihenfolge des Plus an einem Beispiel an:
System.out.println( 1 + 2 ); // 3
System.out.println( "1" + 2 + 3 ); // 123
System.out.println( 1 + 2 + "3" ); // 33
System.out.println( 1 + 2 + "3" + 4 + 5 ); // 3345
System.out.println( 1 + 2 + "3" + (4 + 5) ); // 339
System.out.println( 1 + 2 + "3" + (4 + 5) + 6 ); // 3396
[»] Hinweis
Der Plus-Operator für Zeichenketten geht streng von links nach rechts vor und bereitet mit eingebetteten arithmetischen Ausdrücken mitunter Probleme. Eine Klammerung hilft, wie im Folgenden zu sehen ist:
"Ist 1 größer als 2? " + (1 > 2 ? "ja" : "nein");
Wäre der Ausdruck um den Bedingungsoperator nicht geklammert, dann würde der Plus-Operator an die Zeichenkette die 1 anhängen, und es käme der >-Operator. Der erwartet aber kompatible Datentypen, die in unserem Fall – links stünde die Zeichenkette und rechts die Ganzzahl 2 – nicht gegeben sind.
char-Zeichen in der Konkatenation
Nur eine Zeichenkette in doppelten Anführungszeichen ist ein String, und der Plus-Operator entfaltet seine besondere Wirkung. Ein einzelnes Zeichen in einfachen Hochkommata konvertiert Java nach den Regeln der Typumwandlung bei Berechnungen in ein int, und Additionen sind Ganzzahl-Additionen.
System.out.println( '0' + 2 ); // 50, denn der ASCII-Wert von '0' ist 48
System.out.println( 'A' + 'a' ); // 162, denn 'A'=65, 'a'=97
2.4.12 Operator vermisst *
Einige Programmiersprachen haben einen Potenz-Operator (etwa **), den es in Java nicht gibt. Da es in Java keine Pointer-Operationen gibt, existieren die unter C(++) bekannten Operatorzeichen zur Referenzierung (&) und Dereferenzierung (*) nicht. Ebenso ist ein sizeof unnötig, da das Laufzeitsystem und der Compiler immer die Größe von Klassen kennen bzw. die primitiven Datentypen immer eine feste Länge haben.
Skriptsprachen wie Perl oder Python bieten nicht nur einfache Datentypen, sondern definieren zum Beispiel Listen oder Assoziativspeicher. Damit sind automatisch Operatoren assoziiert, etwa um die Datenstrukturen nach Werten zu fragen oder Elemente einzufügen. Zudem erlauben viele Skriptsprachen das Prüfen von Zeichenketten gegen reguläre Ausdrücke, etwa Perl mit den Operatoren =~ und !~.
Beim Testen von Referenzen auf Identität gibt es in Java den Operator ==. Einige Programmiersprachen bieten zusätzlich einen Operator ===, sodass mit dem einen Operator ein Test auf Gleichheit und mit dem anderen ein Test auf Identität möglich ist. Wir werden uns in Kapitel 3, »Klassen und Objekte«, mit der Identität und dem ==-Operator näher beschäftigen.