Vollständige Anleitung zum Wechseln von Ausdrücken in Java 12


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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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; // don't forget to `break` or you're screwed! break; case FALSE: result = false; break; case FILE_NOT_FOUND: // intermediate variable for demo purposes; // wait for it... var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; default: // ... here we go: // can't declare another variable with the same name var ex2 = new IllegalArgumentException("Seriously?!"); throw ex2; } 

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()); // without default branch, the method wouldn't compile default: throw new IllegalArgumentException("Seriously?!"); } } 

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()); // as we'll see in "Exhaustiveness", `default` is not necessary default -> throw new IllegalArgumentException("Seriously?!"); }; 

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"; // `default, case FILE_NOT_FOUND -> ...` does not work // (neither does other way around), but that makes // sense because using only `default` suffices default -> "insane"; }; 

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"); // in colon-form, if `ternaryBool` is `TRUE` or `FALSE`, // we would see both messages; in arrow-form, only one // branch is executed default -> System.out.println("Bool was insane"); } 

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"); // return with `break`, not `return` break true; } case FALSE -> { System.out.println("Bool false"); break false; } case FILE_NOT_FOUND -> { var ex = new UncheckedIOException("This is ridiculous!", new FileNotFoundException()); throw ex; } default -> { var ex = new IllegalArgumentException("Seriously?!"); throw ex; } }; 

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"; // note that we don't throw the exception! // but it's `Serializable`, so it matches the target type default -> new IllegalArgumentException("insane"); }; 

Was wird jetzt passieren?


 // compiler infers super type of `String` and // `IllegalArgumentException` ~> `Serializable` var serializableMessage = switch (bool) { case TRUE, FALSE -> "sane"; // note that we don't throw the exception! default -> new IllegalArgumentException("insane"); }; 

(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) { // `return` is only possible from block case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

... Sie können return in einem Ausdruck verwenden ...


 public String sanity(Bool ternaryBool) { String result = switch (ternaryBool) { // this does not compile - error: // "return outside of enclosing switch expression" case TRUE, FALSE -> { return "sane"; } default -> { return "This is ridiculous!"; } }; } 

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:


 // compile error: // "the switch expression does not cover all possible input values" boolean result = switch (ternaryBool) { case TRUE -> true; // no case for `FALSE` case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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.


 // compiles without `default` branch because // all cases for `ternaryBool` are covered boolean result = switch (ternaryBool) { case TRUE -> true; case FALSE -> false; case FILE_NOT_FOUND -> throw new UncheckedIOException( "This is ridiculous!", new FileNotFoundException()); }; 

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?

Source: https://habr.com/ru/post/de443464/


All Articles