2.9 Weitere Operatoren
 
2.9.1 Bitoperationen
 
Zahlen und Werte sind im Computer als Sammlung von Bits gegeben. Ein Bit ist ein Informationsträger, für den Wert wahr oder falsch. Bits werden zusammengesetzt zu Folgen wie einem Byte, das aus 8 Bits besteht. Die Belegung der Bits ergibt einen Wert. Wenn also jedes der 8 Bits unterschiedliche Belegungen annehmen kann, so ergeben sich 256 unterschiedliche Zahlen. Jede Stelle im Bit bekommt dabei eine Wertigkeit zugeordnet.
Beispiel Die Wertebelegung für die Zahl 19. Sie ist zusammengesetzt aus einer Anzahl von Summanden der Form 2n
. Die Zahl 19 berechnet sich aus 16+2+1. Genau diese Bits sind gesetzt.
|
Tabelle 2.9
Bitbelegung für die Zahl 19
Bit
|
7
|
6
|
5
|
4
|
3
|
2
|
1
|
0
|
Wertigkeit
|
27
=128
|
26
=64
|
25
=32
|
24
=16
|
23
=8
|
22
=4
|
21
=2
|
20
=0
|
Belegung für 19
|
0
|
0
|
0
|
1
|
0
|
0
|
1
|
1
|
Mit Bitoperatoren lassen sich Binäroperationen auf Operanden durchführen, um beispielsweise Bits eines Worts zu setzen. Zu den Bitoperationen zählen Verknüpfungen, Schiebeoperationen und das Komplement. Durch die bitweisen Operatoren können die einzelnen Bits abgefragt und manipuliert werden. Als Verknüpfungen bietet Java die folgenden Bitoperatoren an:
Tabelle 2.10
Bitoperatoren in Java
Operator
|
Bezeichnung
|
Funktion
|
~
|
Komplement
|
alle Bits werden invertiert
|
|
|
bitweises Oder
|
bei a|b wird jedes Bit von a und b einzeln Oder-verknüpft
|
&
|
bitweises Und
|
bei a&b wird jedes Bit von a und b einzeln Und-verknüpft
|
^
|
bitweises exklusives Oder (Xor)
|
bei a^b wird jedes Bit von a und b einzeln Xor-verknüpft
|
Betrachten wir allgemein die binäre Verknüpfung a # b. Bei der binären bitweisen Und-Verknüpfung mit & gilt für jedes Bit: Nur wenn beide Operanden a und b 1 sind, dann ist auch das Ergebnis 1. Bei der Oder-Verknüpfung mit | muss nur einer der Operanden 1 sein, damit das Ergebnis 1 ist. Bei einem exklusiven Oder (Xor) ist das Ergebnis 1, wenn nur genau einer der Operanden 1 ist. Sind beide gemeinsam 0 oder 1, ist das Ergebnis 0. Dies entspricht einer binären Addition oder Subtraktion. Fassen wir das Ergebnis noch einmal in einer Tabelle zusammen:
Tabelle 2.11
Bitoperatoren in einer Wahrheitstafel
Bit 1
|
Bit 2
|
~Bit 1
|
Bit 1 & Bit 2
|
Bit 1 | Bit 2
|
Bit 1 ^ Bit 2
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
1
|
1
|
0
|
1
|
1
|
1
|
0
|
0
|
0
|
1
|
1
|
1
|
1
|
0
|
1
|
1
|
0
|
2.9.2 Vorzeichenlose Bytes in ein Integer und Char konvertieren
 
Liegt ein Byte als Datentyp vor, so kann es zwar zu einem int automatisch angepasst werden, aber die automatische Typkonvertierung erfüllt nicht immer den gewünschten Zweck:
byte b1 = 100;
byte b2 = (byte)200;
System.out.println( b1 ); // 100
System.out.println( b2 ); // -56
Beim zweiten Byte ist eine Typanpassung nötig, da 0x90 größer als 0x7f ist und daher den Zahlenbereich eines Byte –128 bis 127 überschreitet. Dennoch kann ein Byte das gegebene Bitmuster annehmen und repräsentiert dann die negative Zahl -56. Wird diese ausgegeben, findet bei println(int) eine automatische Typanpassung auf ein int statt und die negative Zahl als byte wird zur negativen int-Zahl. Das bedeutet, das Vorzeichen wird übernommen. In einigen Fällen ist es jedoch wünschenswert, ein byte vorzeichenlos zu behandeln. Bei der Ausgabe soll dann ein Datenwert zwischen 0 und 255 herauskommen. Um das zu erreichen, schneiden wir mit der Und-Verknüpfung alle anderen Bits ausgenommen die unteren 8 heraus. Das reicht schon. Die Lösung zeigt die Funktion byteToInt():
static int byteToInt( byte b )
{
return b & 0xff;
}
Die Schreibweise (int)b ist nicht nötig, da die automatische Typanpassung das Byte b in einer arithmetischen Operation automatisch auf ein int konvertiert.
Mit einer ähnlichen Arbeitsweise können wir auch die Frage lösen, wie sich ein Byte, dessen Integerwert im Minusbereich liegt, in ein char konvertieren lässt. Der erste Ansatz über eine Typumwandlung (char)byte ist falsch und auf der Ausgabe dürfte nur ein rechteckiges Kästchen oder ein Fragezeichen erscheinen:
byte b = (byte) 'ß';
System.out.println( (char) b ); // Ausgabe ist ?
Das Dilemma ist wieder die fehlerhafte Vorzeichenanpassung. Bei der Benutzung des Bytes wird es zuerst in ein int konvertiert. Das »ß« wird dann zu –33. Im nächsten Schritt wird diese –33 dann zu einem char umgesetzt. Das ergibt 65503, was einen Unicode-Bereich trifft, der zurzeit kein Zeichen definiert. Das wird wohl auch noch etwas dauern, bis die ersten Außerirdischen uns neue Zeichensätze schenken. Gelöst wird der Fall wie oben, in dem von b nur die unteren 8 Bits betrachtet werden. Das geschieht wieder durch ein Ausblenden über den Und-Operator. Damit ergibt sich korrekt:
char c = (char) (b & 0x00ff);
2.9.3 Variablen mit Xor vertauschen
 
Eine besonders trickreiche Idee für das Vertauschen von Variableninhalten arbeitet mit der Xor-Funktion und benötigt keine temporäre Zwischenvariable. Die Zeilen zum Vertauschen von x und y lauten wie folgt:
int x = 12,
y = 49;
x ^= y; // x = x ^ y
y ^= x; // y = y ^ x
x ^= y; // x = x ^ y
System.out.println( x + " " + y); // Ausgabe ist: 49 12
Der Trick funktioniert, da wir mit Xor etwas »rein- und rausrechnen« können. Zuerst rechnen wir in der ersten Zeile das y in das x. Wenn wir anschließend die Zuweisung an das y machen, dann ist das der letzte schreibende Zugriff auf y, also muss hier schon das vertauschte Ergebnis stehen. Das stimmt auch, denn expandieren wir die zweite Zeile, steht dort »y wird zugewiesen an y ^ x« und dies ist y ^ (x ^ y ). Der letzte Ausdruck verkürzt sich zu y = x, da aus der Definition der Xor-Funktion für einen Wert a hervorgeht a ^ a = 0. Die Zuweisung hätten wir zwar gleich so schreiben können, aber dann wäre der Wert von y verloren gegangen. Der steckt aber noch in x aus der ersten Zuweisung. Betrachten wir daher die letzte Zeile x ^ y. y hat den Startwert von x, doch in x steckt ein Xor-y drin. Daher ergibt x ^ y den Wert x ^ x ^ y und der verkürzt sich zu y. Demnach haben wir den Inhalt der Variablen vertauscht. Im Übrigen können wir für die drei Xor-Zeilen alternativ schreiben:
y ^= x ^= y; // Auswertung automatisch y ^= (x ^= y)
x ^= y;
Da liegt es doch nahe, die Ausdrücke weiter abzukürzen zu x ^= y ^= x ^= y. Doch leider ist das falsch (es kommt für x immer Null heraus). Den motivierten Lesern bleibt dies als Denksportaufgabe.
2.9.4 Die Verschiebeoperatoren
 
Unter Java gibt es drei Verschiebeoperatoren (engl. Shift-Operator). Sie bewegen die Bits eines Datenworts in eine Richtung. Obwohl es nur zwei Richtungen rechts und links gibt, muss noch der Fall betrachtet werden, ob das Vorzeichen beim Links-Shift beachtet wird oder nicht.
i << n
Der Operand i wird unter Berücksichtigung des Vorzeichens n-mal nach links geshiftet (mit 2 multipliziert). Der rechts frei werdende Bitplatz wird mit 0 aufgefüllt. Das Vorzeichen ändert sich jedoch, sobald eine 1 von (Position MSB1
– 1) nach (MSB) geshiftet wird. Das ist jedoch akzeptabel, da es ja ohnehin zu einem Bereichsüberlauf gekommen wäre.
i >> n
Da es in Java nur vorzeichenbehaftete Datentypen gibt, kommt es unter Berücksichtigung des Vorzeichens beim normalen Rechts-Shift zu einer vorzeichenrichtigen Ganzzahldivision durch 2. Dabei bleibt das linke Vorzeichen-Bit unberührt. Je nachdem, ob das Vorzeichen-Bit gesetzt ist oder nicht, wird eine 1 oder eine 0 von links eingeschoben.
i >>> n
Der Operator >>> verschiebt eine Variable (Bitmuster) bitweise um n Schritte nach rechts ohne das Vorzeichen der Variablen zu berücksichtigen (vorzeichenloser Rechts-Shift). So werden auf der linken Seite (MSB) nur Nullen eingeschoben; das Vorzeichen wird mitgeschoben. Bei einer positiven Zahl hat dies keinerlei Auswirkungen und das Verhalten ist wie beim >>-Operator.
Beispiel Verschiebung einer positiven und negativen Zahl:
Listing 2.10
ShiftRightDemo.java
class ShiftRightDemo
{
public static void main( String args[] )
{
int i = 64;
int j = i >>> 1;
System.out.println( " 64 >>> 1 = " + j );
i = -64;
j = i >>> 1;
System.out.println( "-64 >>> 1 = " + j );
}
}
|
Die Ausgabe ist für den negativen Operanden besonders spannend:
64 >>> 1 = 32
-64 >>> 1 = 2147483616
Ein <<<-Operator macht allerdings keinen Sinn, da beim Links-Shiften sowieso nur Nullen eingefügt werden.
Division und Multiplikation mit Verschiebeoperatoren
Wird ein Wert um eine Stelle nach links geschoben, so kommt in das niedrigste Bit (das Bit ganz rechts) eine Null hinein und alle Bits schieben sich eine Stelle weiter. Das Resultat ist, dass die Zahl dadurch mit 2 multipliziert wird. Natürlich müssen wir mit einem Verlust von Informationen rechnen, wenn das höchste Bit gesetzt ist, denn dann wird es herausgeschoben, aber das Problem haben wir auch schon bei der normalen Multiplikation. Es gibt in Java keinen Operator, der die Bits rollt, der also die an einer Stelle herausfallenden wieder an der anderen Seite einfügt.
Wenn ein einmaliger Shift nach links mit 2 multipliziert, so würde eine Verschiebung um zwei Stellen nach links eine Multiplikation mit 4 bewirken. Allgemein gilt: Bei einem Shift von i nach links ergibt sich eine Multiplikation mit 2i
. Wir können dies dazu nutzen, beliebige Multiplikationen durch Verschiebung nachzubilden.
Beispiel Multiplikation mit 10 durch Verschiebung der Bits:
10*n == n<<3 + n<<1
Das funktioniert, da 10*n = (8+2)*n = 8*n + 2*n gilt.
|
Die Bitoperatoren in Assembler verglichen mit <<<, << und >>
Auch in Assembler gibt es zwei Gruppen von Schiebeoperatoren: die arithmetischen Schiebebefehle (SAL und SAR), die das Vorzeichen des Operanden beachten, und die logischen Schiebebefehle (SHL und SHR), die den Operanden ohne Beachtung eines etwaigen Vorzeichens schieben. Die Befehle SAL und SHL haben die gleiche Wirkung. So ist >>> der Bitoperator, in dem das Vorzeichen nicht beachtet wird, wie SHR in Assembler. Es gibt in Java auch keinen Bitoperator <<<, da – wie in Assembler – SAL = SHL gilt (<<< würde die gleiche Wirkung haben wie <<).
2.9.5 Setzen, Löschen, Umdrehen und Testen von Bits
 
Die Bitoperatoren lassen sich zusammen mit den Shift-Operatoren gut dazu verwenden, ein Bit zu setzen respektive herauszufinden, ob ein Bit gesetzt ist. Betrachten wir folgende Funktionen, die ein bestimmtes Bit setzen, abfragen, invertieren und löschen.
Beispiel Anwendung aller Bitoperatoren:
int setBit( int n, int pos )
{
return n | (1 << pos);
}
int clearBit( int n, int pos )
{
return n & ~ (1 << pos);
}
int flipBit( int n, int pos )
{
return n ^ (1 << pos);
}
|
boolean testBit( int n, int pos )
{
int mask = 1 << pos;
return (n & mask) == mask;
// alternativ: return (n & 1<<pos)!=0;
}
|
2.9.6 Der Bedingungsoperator
 
In Java gibt es ebenso wie in C(++) einen Operator, der drei Operanden benutzt. Dies ist der Bedingungsoperator, der auch Konditionaloperator, ternärer Operator beziehungsweise trinärer Operator genannt wird. Er erlaubt es, den Wert eines Ausdrucks von einer Bedingung abhängig zu machen, ohne dass dazu eine if-Anweisung verwendet werden muss. Die Operanden sind durch ? beziehungsweise : voneinander getrennt:
ConditionalOrExpression ? Expression : ConditionalExpression
Der erste Ausdruck muss vom Typ boolean sein und bestimmt, ob das Ergebnis Expression oder ConditionalExpression ist. Der Bedingungsoperator kann eingesetzt werden, wenn der zweite und dritte Operand ein numerischer Typ, boolescher Typ oder Referenztyp ist. Der Aufruf von Methoden, die demnach void zurückgeben, ist nicht gestattet.
Eine Anwendung für den trinären Operator ist oft eine Zuweisung an eine Variable:
Variable = Bedingung ? Ausdruck1 : Ausdruck2;
Der Wert der Variablen wird jetzt in Abhängigkeit von der Bedingung gesetzt. Ist sie erfüllt, dann erhält die Variable den Wert des ersten Ausdrucks, andernfalls wird der Wert des zweiten Ausdrucks zugewiesen.
Beispiel So etwa für ein Maximum:
max = ( a > b ) ? a : b;
Dies entspricht beim herkömmlichen Einsatz für if/else:
if ( a > b )
max = a;
else
max = b;
|
Mit dem Rückgabewert können wir alles Mögliche machen, etwa ihn direkt ausgeben. Das wäre mit if/else nur mit temporären Variablen möglich.
System.out.println( ( a > b ) ? a : b );
Beispiele
Der Bedingungsoperator findet sich häufig in kleinen Funktionen. Dazu einige Beispiele.
Beispiel Um das Maximum oder Minimum zweier Ganzzahlen zurückzugeben, definieren wir die kompakte Funktion:
static int min( int a, int b ) { return a < b ? a : b; }
static int max( int a, int b ) { return a > b ? a : b; }
|
Beispiel Der Absolutwert einer Zahl wird zurückgegeben durch
x >= 0 ? x : -x
|
Beispiel Der Groß-/Kleinbuchstabe im ASCII-Alphabet soll zurückgegeben werden. Dabei ist c vom Typ char.
// Konvertierung in Großbuchstaben
c = (char)(Character.isLowerCase(c) ? (c-'a'+'A' ) : c);
// Konvertierung in Kleinbuchstaben
c = (char)(Character.isUpperCase(c) ? (c-'A'+'a' ) : c);
Da diese Umwandlung nur für die 26 ASCII-Buchstaben funktioniert, ist es besser, Bibliotheksmethoden für alle Unicode-Zeichen zu verwenden. Dazu dienen die Funktionen toUpperCase() und toLowerCase().
|
Beispiel Es soll eine Zahl n, die zwischen 0 und 15 liegt, zur Hexadezimalzahl konvertiert werden.
(char)( (n < 10) ? ('0' + n) : ('a' – 10 + n ) )
|
Einige Fallen
Die Anwendung des trinären Operators führt schnell zu schlecht lesbaren Programmen und sollte daher vorsichtig eingesetzt werden. In C(++) führt die unbeabsichtigte Mehrfachauswertung in Makros zu schwer auffindbaren Fehlern. Gut, dass uns das in Java nicht passieren kann. Durch ausreichende Klammerung muss sichergestellt werden, dass die Ausdrücke auch in der beabsichtigten Reihenfolge ausgewertet werden. Im Gegensatz zu den meisten Operatoren ist der Bedingungsoperator rechtsassoziativ. (Die Zuweisung ist ebenfalls rechtsassoziativ.) Die Anweisung
b1 ? a1 : b2 ? a2 : a3
ist demnach gleichbedeutend mit
b1 ? a1 : ( b2 ? a2 : a3 )
Beispiel Wollen wir eine Funktion schreiben, die für eine Zahl n abhängig vom Vorzeichen -1, 0 oder 1 liefert, lösen wir das Problem mit geschachteltem trinären Operator.
int sign( int n )
{
return (n < 0) ? -1 : (n > 0) ? 1 : 0;
}
|
Der Bedingungsoperator ist kein lvalue
Der trinäre Operator liefert als Ergebnis einen Ausdruck zurück, der auf der rechten Seite einer Zuweisung verwendet werden kann. Da er rechts vorkommt, nennt er sich auch rvalue. Er lässt sich nicht so auf der linken Seite einer Zuweisung einsetzen, dass er eine Variable auswählt, der ein Wert zugewiesen wird.2
Beispiel Folgende Anwendung des trinären Operators ist in Java nicht möglich.33
((richtung>=0) ? hoch : runter) = true;
|
2.9.7 Überladenes Plus für Strings
 
Obwohl sich in Java die Operatoren fast alle auf primitive Datentypen beziehen, gibt es doch eine bemerkenswerte Verwendung des Plus-Operators. Objekte vom Typ String können durch den Plus-Operator mit anderen Strings verbunden werden. Dies wurde in Java eingeführt, da ein Aneinanderhängen von Zeichenketten oft benötigt wird. Im Kapitel über die verschiedenen Klassen wird String noch etwas präziser dargestellt. Insbesondere werden die Gründe dargelegt, die zur Einführung eines String-Objekts auf der einen Seite, aber auch zur Sonderbehandlung in der Sprachdefinition auf der anderen Seite führten.
Listing 2.11
HelloName.java
// Ein Kleines ,Trallala'-Programm in Java
class HelloName
{
public static void main( String args[] )
{
// Zwei Strings deklarieren
String name,
intro;
// Ersetze "Ulli" durch deinen Namen
name = "Ulli";
// Nun die Ausgabe
intro = "Tri Tra Trallala \"Trullala\", sage " + name;
System.out.println( intro );
}
}
Nachdem ein String intro aus verschiedenen Objekten zusammengesetzt wurde, läuft die Ausgabe auf dem Bildschirm über die Funktion println() ab.
Tri Tra Trallala "Trullala", sage Ulli
Die Funktion schreibt den String auf die Konsolenausgabe und setzt hinter die Zeile noch einen Zeilenvorschub. (Obwohl das \n nicht unbedingt plattformunabhängig ist, wollen wir es trotzdem nutzen.3
) Das Objekt System.out definiert den Ausgabekanal.
Beispiel Da der Plus-Operator für Zeichenketten streng von links nach rechts geht, bringt das mit eingebetteten arithmetischen Ausdrücken mitunter Probleme. Diese müssen dann geklammert werden, wie im Folgenden zu sehen:
"Ist 1 größer als 2? " + (1>2 ? "nein" : "ja");
|
Wäre der Ausdruck um den Bedingungsoperator nicht geklammert, dann würde der Plus-Operator den Ausdruck 1>2 auswerten, in einen String umwandeln und diesen an die erste Zeichenkette anhängen. Jetzt käme das Fragezeichen. Dies ist aber nur für boolesche Werte erlaubt, aber links stände die Zeichenkette. Das wäre ein Fehler.
1
Most Significant Bit. Das Bit mit der höchsten Wertigkeit in der binären Darstellung.
2
In C(++) kann dies durch *((Bedingung) ? &a : &b) = Ausdruck; über Pointer gelöst werden.
3
Besser ist es, das Absatzendezeichen aus der Systemeigenschaft zu nehmen. Dazu dient die Anweisung System.getProperty("line.separator"), die einen String liefert.
|