Der gute alte switch
seit dem ersten Tag in Java. Wir alle benutzen es und sind daran gewöhnt - besonders an seine Macken. (Ärgert sich sonst noch jemand über die break
?) Aber jetzt beginnt sich alles zu ändern: In Java 12 ist der Schalter anstelle eines Operators zu einem Ausdruck geworden:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Switch kann jetzt das Ergebnis seiner Arbeit zurückgeben, das einer Variablen zugewiesen werden kann. Sie können auch die Syntax im Lambda-Stil verwenden, mit der Sie den Durchgang für alle case
entfernen case
in denen keine break
Anweisung vorhanden ist.
In diesem Handbuch werde ich Ihnen alles erklären, was Sie über Switch-Ausdrücke in Java 12 wissen müssen.
Vorschau
Gemäß der vorläufigen Spezifikation der Sprache werden Switch-Ausdrücke gerade erst in Java 12 implementiert.
Dies bedeutet, dass dieses Steuerelementkonstrukt in zukünftigen Versionen der Sprachspezifikation geändert werden kann.
Um die neue Version von switch
Sie die --enable-preview
sowohl während des Kompilierens als auch während des Programmstarts verwenden (Sie müssen beim Kompilieren auch --release 12
- note by the translator).
Beachten Sie also, dass switch als Ausdruck derzeit nicht die endgültige Syntax in Java 12 hat.
Wenn Sie selbst damit spielen möchten, können Sie mein Java X-Demo-Projekt auf einem Github besuchen .
Problem mit Anweisungen im Schalter
Bevor wir zu einem Überblick über die Innovationen bei Switch übergehen , wollen wir kurz eine Situation bewerten. Nehmen wir an, wir stehen vor einem "schrecklichen" ternären Boulean und wollen ihn in einen normalen Boulean umwandeln. Hier ist eine Möglichkeit, dies zu tun:
boolean result; switch(ternaryBool) { case TRUE: result = true;
Stimmen Sie zu, dass dies sehr unpraktisch ist. Wie viele andere Schalteroptionen in "nature" berechnet das obige Beispiel einfach den Wert einer Variablen und weist ihn zu, aber die Implementierung wird umgangen (das Bezeichnerergebnis deklarieren und später verwenden), wiederholt (my break
'und immer das Ergebnis von Copy-Pasta) und fehleranfällig (einen anderen Zweig vergessen? Oh!). Es gibt eindeutig etwas zu verbessern.
Versuchen wir, diese Probleme zu lösen, indem wir den Schalter in einer separaten Methode platzieren:
private static boolean toBoolean(Bool ternaryBool) { switch(ternaryBool) { case TRUE: return true; case FALSE: return false; case FILE_NOT_FOUND: throw new UncheckedIOException("This is ridiculous!", new FileNotFoundException());
Dies ist viel besser: Es gibt keine Dummy-Variable, es gibt keine break
die den Code und die Compiler-Meldungen über das Fehlen von default
(auch wenn dies nicht erforderlich ist, wie in diesem Fall).
Wenn Sie jedoch darüber nachdenken, müssen wir keine Methoden erstellen, um die umständliche Sprachfunktion zu umgehen. Und dies auch ohne Berücksichtigung, dass ein solches Refactoring nicht immer möglich ist. Nein, wir brauchen eine bessere Lösung!
Switch-Ausdrücke einführen!
Wie ich am Anfang des Artikels gezeigt habe, können Sie das obige Problem ab Java 12 wie folgt lösen:
boolean result = switch(ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException());
Ich denke, das ist ziemlich offensichtlich: Wenn ternartBool
TRUE
, wird result
'auf true
(mit anderen Worten, TRUE
wird zu true
). FALSE
wird false
.
Es entstehen sofort zwei Gedanken:
switch
kann ein Ergebnis haben;- Was ist mit den Pfeilen?
Bevor ich mich mit den Details der neuen Switch- Funktionen befasse, werde ich zunächst auf diese beiden Hauptaspekte eingehen.
Ausdruck oder Aussage
Sie werden überrascht sein, dass der Schalter jetzt ein Ausdruck ist. Aber was war er vorher?
Vor Java 12 war ein Switch ein Operator - ein zwingendes Konstrukt, das den Kontrollfluss reguliert.
Stellen Sie sich die Unterschiede zwischen der alten und der neuen Version von switch als den Unterschied zwischen if
und dem ternären Operator vor. Beide überprüfen den logischen Zustand und führen je nach Ergebnis eine Verzweigung durch.
Der Unterschied besteht darin, dass der ternäre Operator ein Ergebnis zurückgibt, if
nur der entsprechende Block ausgeführt wird:
if(condition) { result = doThis(); } else { result = doThat(); } result = condition ? doThis() : doThat();
Das Gleiche gilt für switch : Wenn Sie vor Java 12 den Wert berechnen und das Ergebnis speichern möchten, weisen Sie ihn entweder einer Variablen zu (und break
dann ab) oder geben ihn von einer speziell für die switch
erstellten Methode zurück.
Nun wird der gesamte Ausdruck der switch-Anweisung ausgewertet (der entsprechende Zweig wird zur Ausführung ausgewählt) und das Ergebnis der Berechnungen kann einer Variablen zugeordnet werden.
Ein weiterer Unterschied zwischen dem Ausdruck und der Anweisung besteht darin, dass die switch-Anweisung , da sie Teil der Anweisung ist, im Gegensatz zur klassischen switch-Anweisung mit einem Semikolon enden muss.
Pfeil oder Doppelpunkt
Im Einführungsbeispiel wurde die neue Syntax im Lambda-Stil mit einem Pfeil zwischen dem Etikett und dem laufenden Teil verwendet. Es ist wichtig zu verstehen, dass es dafür nicht erforderlich ist, switch
als Ausdruck zu verwenden. Das folgende Beispiel entspricht dem am Anfang des Artikels angegebenen Code:
boolean result = switch(ternaryBool) { case TRUE: break true; case FALSE: break false; case FILE_NOT_FOUND: throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); default: throw new IllegalArgumentException("Seriously?!!?"); };
Beachten Sie, dass Sie jetzt break
mit einem Wert verwenden können! Dies passt perfekt zu switch
alten Stil, die break
ohne Bedeutung verwenden. In welchem Fall bedeutet ein Pfeil einen Ausdruck anstelle eines Operators. Warum ist er hier? Nur Hipster-Syntax?
In der Vergangenheit markieren Doppelpunktmarkierungen einfach den Einstiegspunkt in den Anweisungsblock. Ab diesem Punkt beginnt die Ausführung des gesamten folgenden Codes, auch wenn ein anderes Label angetroffen wird. Beim switch
wissen wir, dass dies zum nächsten case
(Durchfall): Das Falletikett bestimmt, wohin der Kontrollfluss springt. Um es abzuschließen, müssen Sie break
oder return
.
Die Verwendung des Pfeils bedeutet wiederum, dass nur der Block rechts davon ausgeführt wird. Und kein "scheitern".
Mehr zur Entwicklung des Schalters
Mehrere Tags auf Fall
Bisher hat jeder case
nur ein Etikett. Aber jetzt hat sich alles geändert - ein case
kann mehreren Labels entsprechen:
String result = switch(ternaryBool) { case TRUE, FALSE -> "sane";
Das Verhalten sollte offensichtlich sein: TRUE
und FALSE
führen zum gleichen Ergebnis - der Ausdruck "sane" wird ausgewertet.
Dies ist eine ziemlich nette Neuerung, die die mehrfache Verwendung von case
ersetzte, als ein Pass-Through-Übergang zum nächsten case
implementiert case
.
Typen außerhalb von Enum
Alle switch
Beispiele in diesem Artikel verwenden enum
. Was ist mit anderen Typen? Ausdrücke und switch
können auch mit String
, int
(siehe Dokumentation ) short
, byte
, char
und ihren Wrappern funktionieren. Bisher hat sich hier nichts geändert, obwohl die Idee, Datentypen wie float
und long
immer noch gültig ist (vom zweiten bis zum letzten Absatz).
Mehr zum Pfeil
Schauen wir uns zwei Eigenschaften an, die für die Pfeilform eines Trenndatensatzes spezifisch sind:
- Fehlen eines End-to-End-Übergangs zum nächsten
case
; - Blöcke von Operatoren.
Kein Durchgang zum nächsten Fall
Folgendes sagt JEP 325 dazu:
Das aktuelle Design der switch
in Java ist eng mit Sprachen wie C und C ++ verwandt und unterstützt standardmäßig die End-to-End-Semantik. Obwohl diese traditionelle Steuermethode häufig zum Schreiben von Code auf niedriger Ebene nützlich ist (z. B. Parser für die Binärcodierung), überwiegen die Fehler dieses Ansatzes, da switch
in Code höherer Ebene verwendet wird, seine Flexibilität.
Ich stimme voll und ganz zu und begrüße die Möglichkeit, switch ohne Standardverhalten zu verwenden:
switch(ternaryBool) { case TRUE, FALSE -> System.out.println("Bool was sane");
Es ist wichtig zu lernen, dass dies nichts damit zu tun hat, ob Sie switch als Ausdruck oder Anweisung verwenden. Ausschlaggebend ist hier der Pfeil gegen den Doppelpunkt.
Bedienerblöcke
Wie bei Lambdas kann der Pfeil entweder auf einen Operator (wie oben) oder auf einen mit geschweiften Klammern hervorgehobenen Block zeigen:
boolean result = switch(Bool.random()) { case TRUE -> { System.out.println("Bool true");
Blöcke, die für mehrzeilige Operatoren erstellt werden müssen, haben einen zusätzlichen Vorteil (der bei Verwendung eines Doppelpunkts nicht erforderlich ist). Um dieselben Variablennamen in verschiedenen Zweigen zu verwenden, erfordert der switch
keine spezielle Verarbeitung.
Wenn es Ihnen ungewöhnlich erschien, Blöcke mit break
anstatt return
, dann machen Sie sich keine Sorgen - das verwirrte mich auch und schien seltsam. Aber dann habe ich darüber nachgedacht und bin zu dem Schluss gekommen, dass es sinnvoll ist, da es den alten Stil des switch
Konstrukts switch
, das break
ohne Werte verwendet.
Erfahren Sie mehr über switch-Anweisungen
Und zu guter Letzt die Besonderheiten der Verwendung von switch
als Ausdruck:
- mehrere Ausdrücke;
- vorzeitige Rückkehr (vorzeitige
return
); - Abdeckung aller Werte.
Bitte beachten Sie, dass es keine Rolle spielt, welches Formular verwendet wird!
Mehrere Ausdrücke
Schalterausdrücke sind mehrere Ausdrücke. Dies bedeutet, dass sie keinen eigenen Typ haben, sondern einer von mehreren Typen sein können. Am häufigsten werden Lambda-Ausdrücke als solche Ausdrücke verwendet: s -> s + " "
, kann Function<String, String>
, kann aber auch Function<Serializable, Object>
oder UnaryOperator<String>
.
Mithilfe von Schalterausdrücken wird ein Typ durch die Interaktion zwischen dem Verwendungsort des Schalters und den Typen seiner Zweige bestimmt. Wenn ein Schalterausdruck einer typisierten Variablen zugewiesen, als Argument übergeben oder anderweitig in einem Kontext verwendet wird, in dem der genaue Typ bekannt ist (dies wird als Zieltyp bezeichnet), müssen alle seine Zweige mit diesem Typ übereinstimmen. Folgendes haben wir bisher getan:
String result = switch (ternaryBool) { case TRUE, FALSE -> "sane"; default -> "insane"; };
Infolgedessen wird switch
der result
vom Typ String
zugewiesen. Daher ist String
der Zieltyp, und alle Zweige müssen ein Ergebnis vom Typ String
.
Das gleiche passiert hier:
Serializable serializableMessage = switch (bool) { case TRUE, FALSE -> "sane";
Was wird jetzt passieren?
(Zur Verwendung des var-Typs lesen Sie in unserem letzten Artikel 26 Empfehlungen zur Verwendung des var-Typs in Java - Anmerkung des Übersetzers)
Wenn der Zieltyp aufgrund der Tatsache, dass wir var verwenden, unbekannt ist, wird der Typ berechnet, indem der spezifischste Supertyp der von den Zweigen erstellten Typen ermittelt wird.
Vorzeitige Rückkehr
Der Unterschied zwischen dem Ausdruck und der switch
hat zur Folge, dass Sie mit return
die switch
:
public String sanity(Bool ternaryBool) { switch (ternaryBool) {
... Sie können return
in einem Ausdruck verwenden ...
public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) {
Dies ist sinnvoll, unabhängig davon, ob Sie einen Pfeil oder einen Doppelpunkt verwenden.
Alle Optionen abdecken
Wenn Sie switch
als Operator verwenden, spielt es keine Rolle, ob alle Optionen abgedeckt sind oder nicht. Natürlich können Sie den case
versehentlich überspringen und der Code funktioniert nicht richtig, aber der Compiler kümmert sich nicht darum - Sie, Ihre IDE und Ihre Code-Analyse-Tools bleiben damit allein.
Schalterausdrücke verschärfen dieses Problem. Wohin soll der Schalter gehen, wenn das gewünschte Etikett fehlt? Die einzige Antwort, die Java geben kann, ist die Rückgabe von null
für Referenztypen und eines Standardwerts für Grundelemente. Dies würde viele Fehler im Hauptcode verursachen.
Um ein solches Ergebnis zu verhindern, kann Ihnen der Compiler helfen. Bei switch-Anweisungen besteht der Compiler darauf, dass alle möglichen Optionen abgedeckt sind. Schauen wir uns ein Beispiel an, das zu einem Kompilierungsfehler führen kann:
Die folgende Lösung ist interessant: Durch Hinzufügen des default
wird der Fehler zwar behoben, dies ist jedoch nicht die einzige Lösung. Sie können dennoch case
für FALSE
hinzufügen.
Ja, der Compiler kann endlich feststellen, ob alle Aufzählungswerte abgedeckt sind (ob alle Optionen ausgeschöpft sind) und keine nutzlosen Standardwerte festlegen! Lassen Sie uns einen Moment in stiller Dankbarkeit sitzen.
Dies wirft jedoch immer noch eine Frage auf. Was ist, wenn jemand einen verrückten Bool nimmt und in einen Quaternion Boolean verwandelt, indem er einen vierten Wert hinzufügt? Wenn Sie den switch-Ausdruck für den erweiterten Bool neu kompilieren, wird ein Kompilierungsfehler angezeigt (der Ausdruck ist nicht mehr vollständig). Ohne Neukompilierung wird dies zu einem Laufzeitproblem. Um dieses Problem zu beheben, wechselt der Compiler zum default
, der sich wie der bisher verwendete verhält und eine Ausnahme auslöst.
In Java 12 funktioniert das Überspannen aller Werte ohne den default
nur für enum
. Wenn der switch
in zukünftigen Versionen von Java jedoch leistungsfähiger wird, kann er auch mit beliebigen Typen arbeiten. Wenn Fallbezeichnungen nicht nur die Gleichheit überprüfen, sondern auch Vergleiche durchführen können (z. B. _ <5 -> ...), werden alle Optionen für numerische Typen abgedeckt.
Denken
Wir haben aus dem Artikel gelernt, dass Java 12 einen switch
in einen Ausdruck verwandelt und ihm neue Funktionen verleiht:
- jetzt kann ein
case
mehreren Etiketten entsprechen; - Die neue Pfeilform
case … -> …
folgt der Syntax von Lambda-Ausdrücken:
- Einzeilige Operatoren oder Blöcke sind zulässig.
- der Übergang zum nächsten
case
verhindert;
- Jetzt wird der gesamte Ausdruck als Wert ausgewertet, der dann einer Variablen zugewiesen oder als Teil einer größeren Anweisung übergeben werden kann.
- Mehrfachausdruck: Wenn der Zieltyp bekannt ist, müssen alle Zweige diesem entsprechen. Andernfalls wird ein bestimmter Typ definiert, der allen Zweigen entspricht.
break
kann einen Wert aus einem Block zurückgeben;- Für einen
switch
mit enum
überprüft der Compiler den Umfang aller seiner Werte. Wenn die default
fehlt, wird ein Zweig hinzugefügt, der eine Ausnahme auslöst.
Wohin wird es uns führen? Erstens, da dies nicht die endgültige Version von switch
, haben Sie immer noch Zeit, Feedback zur Amber- Mailingliste zu hinterlassen, wenn Sie mit etwas nicht einverstanden sind.
Unter der Annahme, dass der Schalter unverändert bleibt, wird die Pfeilform meiner Meinung nach zur neuen Standardoption. Ohne eine durchgehende Passage zum nächsten case
und mit präzisen Lambda-Ausdrücken (es ist sehr natürlich, einen Fall und eine Anweisung in einer Zeile zu haben) sieht der switch
viel kompakter aus und beeinträchtigt die Lesbarkeit des Codes nicht. Ich bin mir sicher, dass ich nur dann einen Doppelpunkt verwenden werde, wenn ich durch den Durchgang gehen muss.
Was denken Sie? Zufrieden mit dem Ergebnis?