Javaschubla.de - Java als erste Programmiersprache
In dieser Lektion geht es um die switch-case-Kontrollstruktur, um den ternären Operator "... ? ... : ..." und um die Operatoren &, |, ^, ~, <<, >> und >>>. Dass sie so spät kommen, liegt daran, dass ich der Meinung bin, dass sie nicht besonders wichtig sind. Aber man muss sie natürlich verstehen, wenn sie in einem gegebenen Programm vorkommen. Außerdem werden die wichtigen Schlüsselwörter continue und break im Zusammenhang mit Schleifen behandelt.
Manchmal hat man eine lange geschachtelte if-else-Anweisung, die dann unübersichtlich ist, wie in diesem Beispiel, wo den Zahlen 1 bis 7 die Wochentage Montag bis Sonntag zugeordnet werden.
class Wochentag { public static void main(String[] args) { System.out.println(weekday(4)); } static String weekday(int tag) { String antwort = ""; if (tag == 1) { antwort = "Montag"; } else if (tag == 2) { antwort = "Dienstag"; } else if (tag == 3) { antwort = "Mittwoch"; } else if (tag == 4) { antwort = "Donnerstag"; } else if (tag == 5) { antwort = "Freitag"; } else if (tag == 6) { antwort = "Samstag"; } else if (tag == 7) { antwort = "Sonntag"; } else { antwort = "ungültiger Wochentag"; } return antwort; } }
Zur Erinnerung:
Das kann man auch mit switch-case schreiben.
class Wochentag { public static void main(String[] args) { System.out.println(weekday(4)); } static String weekday(int tag) { String antwort = ""; switch (tag) { case 1: antwort = "Montag"; break; case 2: antwort = "Dienstag"; break; case 3: antwort = "Mittwoch"; break; case 4: antwort = "Donnerstag"; break; case 5: antwort = "Freitag"; break; case 6: antwort = "Samstag"; break; case 7: antwort = "Sonntag"; break; default : antwort = "ungültiger Wochentag"; } return antwort; } }
Mit switch(variablenName)
gibt man an, von welcher Variable verschiedene Fälle betrachtet werden sollen. Mit case
gibt man die verschiedenen Fälle an, mit default
denjenigen, dass keiner der anderen Fälle eingetreten ist. Wozu wird nun das break benötigt? Wenn man kein break einfügt, werden alle weiteren Anweisungen bis zum ersten break ausgeführt - wenn man also das break bei case 5 vergessen würde, würde erst antwort = "Freitag" ausgeführt werden und dann antwort = "Samstag". Das ist eine typische Fehlerquelle, man kann es sich aber auch zunutze machen, wie man im nächsten Beispiel sieht:
class VokaleUndKonsonanten { public static void main(String[] args) { System.out.println(vocKons('i')); } static String vocKons(char c) { String antwort = ""; switch (c) { case 'a': case 'e': case 'i': case 'o': case 'u': case 'ä': case 'ö': case 'ü': case 'A': case 'E': case 'I': case 'O': case 'U': case 'Ä': case 'Ö': case 'Ü': antwort = "Vokal"; break; default: antwort = "Konsonant"; } return antwort; } }
(Das Programm berücksichtigt nur Vokale und Konsonanten, auch bei Zahlen, Sonderzeichen und nicht-deutschen Vokalen wie é würde es "Konsonant" ausgeben. Wenn du Lust hast, erweitere es. Statt diese switch-case-Anweisung hätte man auch "aeiouäöüAEIOUÄÖÜ".indexOf(c) != -1 verwenden können, um festzustellen, dass c einer dieser Buchstaben ist.)
Man kann nur auf Variablen von den Typen byte, short, int und char switchen, nicht auf long, float, double, boolean oder String.
Ein switch-case ist schneller als eine lange if-else-Schachtelung. Die "Sprungadresse", wo der Code für einen bestimmten Fall steht, wird quasi sofort ausgerechnet, statt dass alle ifs durchgetestet werden.
Ternär bedeutet, dass der Operator drei Operanden erwartet. Viele Operatoren sind binär, sie erwarten zwei Operanden, z.B. + - * / % && ||. Beispiel: bei 2+3 ist + der Operator und 2 und 3 sind die Operanden, bei b1&&b2 sind b1 und b2 die beiden Operanden. Einige Operatoren sind unär, sie erwarten nur einen Operanden, z.B. + und - als Vorzeichen, ! sowie ++ und --.
Nun zum ternären Operator ... ? ... : ...
(die ... bedeuten die Stellen, wo die drei Operanden hinkommen). Nehmen wir als Beispiel die Bestimmung des Minimums zweier Zahlen. Mit if würde man das so machen:
class Minimum { public static void main(String[] args) { int zahl1 = 5, zahl2 = 3; int min = 0; if (zahl1<zahl2) { min = zahl1; } else { min = zahl2; } System.out.println("Minimum: " + min); } }
Mit dem ternären Operator kann man das so schreiben:
class Minimum { public static void main(String[] args) { int zahl1 = 5, zahl2 = 3; int min = 0; min = zahl1<zahl2 ? zahl1 : zahl2; System.out.println("Minimum: " + min); } }
Das bewirkt genau das gleiche wie der obige Quellcode mit if. Wenn zahl1<zahl2 erfüllt ist, dann wird min der erste Wert direkt nach dem ? zugewiesen, also zahl1, wenn der Ausdruck zahl1<zahl2 hingegen false ist, dann wird min der zweite Wert, der hinter dem :, zugewiesen, also zahl2.
Wir wissen schon: AND (und) ist &&, OR (oder) ist ||. Aber vielleicht ist euch aufgefallen, dass es auch mit & und | funktioniert und dasselbe herauskommt, egal ob man etwa a>3 && a<15 oder a>3 & a<15 benutzt. Warum sollte man dann dennoch immer && und || benutzen?
Es gibt einen nicht sofort erkennbaren Unterschied zwischen && und &. Bei && wird die Überprüfung sofort abgebrochen, sobald das Ergebnis des Ausdrucks klar ist, bei & wird immer bis zum Ende durchgeprüft. Wenn also schon a>3 false ist, etwa wenn a 2 ist, dann ist der Ausdruck a>3 && a<15 auf jeden Fall false, man braucht a<15 gar nicht mehr zu überprüfen. a>3 && a<15 bricht für ein a, das kleiner oder gleich 3 ist, schon nach a>3 ab und überprüft nicht mehr a<15 - bei & würde auch das noch überprüft werden.
Genau so ist es mit || und |: a<10 || a>20 würde mit der Überprüfung sofort abbrechen, wenn a tatsächlich kleiner als 10 ist, also a<10 true, denn der Gesamtausdruck ist ja wahr, wenn eine von beiden Möglichkeiten wahr ist. Bei | würde nichtsdestotrotz noch a>20 überprüft werden.
Aber es geht nicht nur um gesparte Laufzeit. Manchmal ist es sehr wichtig, dass die Überprüfung baldmöglichst abbricht, etwa wenn sonst durch 0 geteilt werden würde.
Im folgenden Beispiel wird überprüft, ob sich die Produktionsmenge zum Vorjahr um mindestens 50% erhöht hat, also aufs Doppelte gestiegen ist. Wenn im Vorjahr 0 produziert wurde, soll auch true zurück gegeben werden, egal wie der neue Wert lautet.
class ProduktionsSteigerung { public static void main(String[] args) { System.out.println( raisedEnough(250, 100) ); // true System.out.println( raisedEnough(400, 200) ); // true System.out.println( raisedEnough(199, 100) ); // false System.out.println( raisedEnough(80, 60) ); // false System.out.println( raisedEnough(60, 80) ); // false System.out.println( raisedEnough(10, 0) ); // true } static boolean raisedEnough(int neu, int alt) { if (alt == 0 || neu/alt >= 2) { return true; } else { return false; } } }
(Integerdivision reicht aus, man braucht nicht mit (double) zu casten.)
Wenn man | statt || verwendet hätte, dann wäre, auch wenn alt 0 ist, neu/alt berechnet werden und bei dieser Division durch 0 eine Exception geworfen worden: "ArithmeticException: / by zero" .
Nebenbemerkung: Bei Fließkommadivision gibt es keinen Fehler (Exception) beim Teilen durch 0. Z.B.
Dass die Auswertung bei && und || frühzeitig abbricht, sobald das Ergebnis feststeht, nennt man "short circuit evaluation", ungefähr "Kurzschlussauswertung".
Bitoperatoren benutzt man selten in Java. So etwas lohnt sich in maschinennahen Sprachen wie C und Assembler, in Java bringt es kaum einen Nutzen. Wen es also nicht interessiert, der kann diesen Abschnitt getrost überspringen.
Beispiel: 6 & 5: 6 binär ist 110, 5 binär ist 101. Zur besseren Übersicht hilft es, die Zahlen untereinander zu schreiben:
110 101
1&1 ergibt 1, alles andere (1&0, 0&1, 0&0) ergibt 0, also ist das Ergebnis 100 also 4. Probiere es aus: System.out.println(6&5);
gibt 4 aus.
Anderes Beispiel: 7&8:
111 1000
ergibt 0000 also 0.
Beispiel: 6 | 5: Bitweise untereinander schreiben:
110 101
0|0 ergibt 0, alles andere (1|0, 0|1, 1|1) ergibt 1, also ist das Ergebnis 111 also 7. System.out.println(6|5);
gibt wie erwartet 7 aus.
Anderes Beispiel: 7|8:
111 1000
ergibt 1111 also 15.
Beispiel 6 ^ 5 bitweise untereinander schreiben:
110 101
Sowohl 1^0 als auch 0^1 ergibt 1, während 0^0 und 1^1 0 ergeben, also ist das Ergebnis 011 also 3.
Anderes Beispiel: 7^8:
111 1000
ergibt 1111 also 15.
Beispiel ~6
0000 0000 0000 0000 0000 0000 0000 0110 = 6 1111 1111 1111 1111 1111 1111 1111 1001 = -7
<< ist ein Linksshift, z.B. 8 << 2 shiftet die 8 um zwei Stellen nach links und füllt rechts Nullen nach.
8 << 2 binär: 1000 << 2 ergibt 100000 also 32.
So wie eine 0 anhängen im Zehnersystem einer Multiplikation mit 10 entspricht, entspricht es im Binärsystem (Dualsystem) der Multiplikation mit 2, also ist eine Bitverschiebung um 2 Stellen nach links eine Multiplikation mit 4. Allerdings nur, wenn es dabei nicht zu einem Overflow kommt. 8 << 29 ergibt 0, weil das einzige Bit links runter gefallen ist, nachdem es 29-mal nach links verschoben wurde.
Der Linksshift funktioniert genau so für negative Zahlen, -8 << 2 ergibt wie erwartet -32. -8 << 2 in Binärdarstellung (Zweierkomplement, ints sind 32 Bit groß.)
1111 1111 1111 1111 1111 1111 1111 1000 << 2 ergibt 1111 1111 1111 1111 1111 1111 1110 0000 also -32.
>> ist ein Rechtsshift. Links werden Nullen nachgefüllt, wenn die Zahl positiv ist, Einsen, wenn sie negativ ist. So ist ein Shift um 1 nach rechts eine Division durch 2, sowohl für positive als auch für negative Zahlen.
8 >> 2 1000 >> 2 ergibt 10 also 2.
(Genauer gesagt: 0000 0000 0000 0000 0000 0000 0000 1000 >> 2 ergibt 0000 0000 0000 0000 0000 0000 0000 0010.)
-8 >> 2 1111 1111 1111 1111 1111 1111 1111 1000 >> 2 ergibt 1111 1111 1111 1111 1111 1111 1111 1110 also -2.
>>> ist ein Rechtsshift, bei dem immer Nullen nachgezogen werden. Für positive Zahlen kommt also dasselbe heraus wie bei >>, bei negativen etwas anderes, nämlich immer eine positive Zahl. Beispiel:
-8 >>> 2 1111 1111 1111 1111 1111 1111 1111 1000 >>> 2 ergibt 0011 1111 1111 1111 1111 1111 1111 1110 also 1073741822.
(Solche Einzeiler kann man übrigens mit Eclipse schnell testen, indem man eine "Scrapbook Page" anlegt, die Zeile schreibt, markiert, darauf rechtsklickt und ausführen wählt.)
Das einzige, was man sich meiner Meinung nach über Bitoperatoren unbedingt merken muss, ist, dass x^y nicht für "x hoch y" steht - dafür gibt es Math.pow(x, y).
continue bewirkt, dass der aktuelle Schleifendurchlauf abgebrochen und der nächste begonnen wird. (Bei einer for-Schleife wird trotzdem das Inkrement oder wasauchimmer als drittes im Schleifenkopf angegeben wurde dennoch durchgeführt.)
Ein nicht sehr sinnvolles, aber leicht verständliches Beispiel:
for (int i = 1; i <= 10; i++) { if (i == 3) { continue; } System.out.println(i*i); }
gibt die Quadratzahlen von 1*1 bis 10*10 aus, jedoch ohne 9. Denn bei i gleich 3 wird continue aufgerufen, der Rest des Schleifenkörpers wird nicht ausgeführt, es kommt also nicht zum System.out.println(i*i). Jedoch wird i++ ausgeführt. Mit i gleich 4 geht es weiter.
break kennen wir schon von switch-case, aber es hat noch einen anderen Nutzen: eine Schleife wird damit komplett verlassen. Ein noch unsinnigeres, aber leicht zu verstehendes Beispiel:
int i = 1; while (i <= 10) { System.out.println(i*i); if (i == 3) { break; } i++; }
gibt 1, 4 und 9 aus und bricht dann ab. Natürlich wäre es in diesem Fall viel sinnvoller gewesen, einfach als Schleifenbedingung i <= 3 zu nehmen.
In echten Programmen wäre es eher so, dass man auf dem i oder abhängig von Benutzereingaben Berechnungen durchführt und je nach Ergebnis diesen Schleifendurchgang mit continue oder die gesamte Schleife mit break abbricht.
Denk dir ein Beispiel für continue oder break aus.
Denk dir ein Beispiel für den ternären Operator aus.
Wenn du Lust hast, spiel mit den Bitoperatoren herum.
Die nächste Lektion behandelt die Rangordnung der Operatoren.