- はじめに
- プレビュー版からの変更点
- Pattern Matching for switch による機能拡張
- switch ラベルによるパターンマッチ
- case ラベルの guard 条件指定
- null と switch
- enum 定数 ラベルの改善
- case ラベルの Dominance(優位性)
- 型の網羅性(Exhaustiveness)
- パターン変数宣言のスコープ
- switch で発生するエラー
はじめに
Java17 でファースト・プレビューとして公開された、 switch でのパターンマッチが、Java21 で正式リリースにになります。 JEP 440 Record Patterns と合わせて、レコードのパターンマッチができるようになりました。
ここでは JEP 441 Pattern Matching for switch の内容について見ていきます。
なお、レコードパターン(JEP 440 Record Patterns)については以下を参考にしてください。
プレビュー版からの変更点
JEP 441 Pattern Matching for switch は以下のプレビューを経てリリースとなりました。
Java17 : JEP 406 としてファースト・プレビュー
Java18 : JEP 420 としてセカンド・プレビュー
- ファースト・プレビューからの主な変更点は以下
- ガードされたパターンより前に、同じ型の定数ケースラベルを配置するよう優先度チェックを変更
- sealed クラスの網羅性チェック強化
Java19 : JEP 427 としてサード・プレビュー
- セカンド・プレビューからの主な変更点は以下
- ガードされたパターンが、スイッチブロックの
when
節に置き換えられた - セレクタ式の値が
null
の場合の実行時セマンティクスをレガシースイッチセマンティクスと整合するよう変更
Java20 : JEP 433 としてフォース・プレビュー
- サード・プレビューからの主な変更点は以下
- 実行時に、 enum に対するマッチが失敗した場合、
IncompatibleClassChangeError
ではなくMatchException
をスローするよう変更 - スイッチラベルの文法がより簡素化された
- ジェネリック レコード パターンの型引数の推論が、switch式とステートメントでサポートされるようになった
Java21 : JEP 441 として正式リリース
- フォース・プレビューからの主な変更点は以下
- 括弧付きパターンを削除
- 修飾されたenum定数を
case
定数として使用できるようになった
Pattern Matching for switch による機能拡張
Pattern Matching for switch により大きく以下の機能拡張が行われます。
switch
文とswitch
式のセレクタ式で使用できる型の範囲を広げる(スイッチブロックの網羅性の分析を含む)- 定数に加え、パターンと
null
を含むようにcase
ラベルを拡張する when
節をcase
ラベルの後に付けることで、ガード条件を指定できるようにする- enum 定数の
case
ラベルを改善する
従来の switch
のセレクタ式には、整数プリミティブ型(long
を除く。対応するボックス型含む)、String
、または enum
型のいずれかでなければなりませんでした。
Pattern Matching for switch ではこの制限が緩和され、任意の参照型を指定することができるようになります。
スイッチブロック内のスイッチラベルの文法は以下のように変更となり、ラベルに null
と パターンを指定できるようになります。
SwitchLabel: case CaseConstant { , CaseConstant } case null [, default] ← ★追加 case Pattern [ Guard ] ← ★追加 default
詳細を以下に順に見ていきましょう。
switch ラベルによるパターンマッチ
case
ラベルには、定数だけでなくパターンを使えるようになりました。
スイッチラベルがパターンを持つ case
ラベルの場合、選択されるラベルは等号テストではなく、パターンマッチングの結果によって決定されます。
型へのマッチは以下のように書くことができます。
static String formatterPatternSwitch(Object obj) { return switch (obj) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); case int[] ia -> String.format("Array of ints); default -> obj.toString(); }; }
レコードパターンと合わせて以下のような操作が可能となります。
sealed interface Shape permits Circle, Rectangle, Square {} record Circle(double radius) implements Shape {} record Rectangle(double width, double height) implements Shape {} record Square(double side) implements Shape {}
public double getArea(Shape shape) { return switch (shape) { case Circle(var radius) -> Math.PI * radius * radius; case Rectangle(var width, var height) -> width * height; case Square(var side) -> side * side; }; }
case ラベルの guard 条件指定
switch ブロックの when
節に、パターンの case
ラベルに対するガードを指定できるようになりました。
guarded pattern case
labels が導入され、オプションの guard (ブール値表現) をパターンラベルの後に置くことができます。(Java18 までは case Triangle t && (t.calculateArea() > 100) -> xx
のように &&
でガード条件を記載したが、 Java19 のプレビューから when
節が導入)
static void testStringNew(String response) { switch (response) { case null -> { } case "y", "Y" -> { System.out.println("You got it"); } case "n", "N" -> { System.out.println("Shame"); } case String s when s.equalsIgnoreCase("YES") -> { System.out.println("You got it");} case String s when s.equalsIgnoreCase("NO") -> { System.out.println("Shame"); } case String s -> { System.out.println("Sorry?"); } } }
これにより、すべての条件ロジックをswitchラベルの中に取り込むことができます。なお、ガードを持つことができるのは、パターンラベルだけです。
null と switch
従来、switch
文や式は、セレクタ式がnull
と評価されるとNullPointerException
をスローしましたが、Pattern Matching for switch によりラベルに null
を指定できるようになります。
static void testFooBarNew(String s) { switch (s) { case null -> System.out.println("Oops"); case "Foo", "Bar" -> System.out.println("Great"); default -> System.out.println("Ok"); } }
これは、case null
を使わない以下の(従来の)コードと同等です。
static void testFooBarOld(String s) { if (s == null) { System.out.println("Oops!"); return; } switch (s) { case "Foo", "Bar" -> System.out.println("Great"); default -> System.out.println("Ok"); } }
case null
が無い場合は、従来と同様に NullPointerException
をスローします。
switch
のセマンティクスとの後方互換性を保つために、default
ラベルは null
セレクタにマッチしないことに注意してください。
つまり、以下のコードは、
switch (s) { case "Foo", "Bar" -> System.out.println("Great"); default -> System.out.println("Ok"); }
以下と同等になります。
switch (s) { case null -> throw new NullPointerException(); case "Foo", "Bar" -> System.out.println("Great"); default -> System.out.println("Ok"); }
null
を含めて default
の挙動を行うには、以下のように書くことができます。
switch (s) { case "Foo", "Bar" -> System.out.println("Great"); case null, default -> System.out.println("Ok"); }
enum 定数 ラベルの改善
switch で enum を利用する場合、swiche
のセレクタ式はenum型でなければならず、ラベルはenumの定数の単純な名前である必要がありました。
例えば以下のように、case
ラベルには HEARTS
のような enum 定数名以外は指定できませんでした。
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES } static void testforHearts(Suit s) { switch (s) { case HEARTS -> System.out.println("It's a heart!"); default -> System.out.println("Some other suit"); } }
新しいコードでは、セレクタ式がenum型であるという条件が緩和され、case
定数にはenum定数の修飾名を使用することができるようになりました。
例えば以下のようなシールされた enum があった場合を考えます。
sealed interface CardClassification permits Suit, Tarot {} public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES } final class Tarot implements CardClassification {}
enum に対する制限のあるコードでは以下のような冗長な条件指定が必要でした。
static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) { switch (c) { case Suit s when s == Suit.CLUBS -> { System.out.println("It's clubs"); } case Suit s when s == Suit.DIAMONDS -> { System.out.println("It's diamonds"); } case Suit s when s == Suit.HEARTS -> { System.out.println("It's hearts"); } case Suit s -> { System.out.println("It's spades"); } case Tarot t -> { System.out.println("It's a tarot"); } } }
上記は以下のように書くことができるようになります。
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) { switch (c) { case Suit.CLUBS -> { System.out.println("It's clubs"); } case Suit.DIAMONDS -> { System.out.println("It's diamonds"); } case Suit.HEARTS -> { System.out.println("It's hearts"); } case Suit.SPADES -> { System.out.println("It's spades"); } case Tarot t -> { System.out.println("It's a tarot"); } } }
case ラベルの Dominance(優位性)
case ラベルには、 try - catch
節で複数の例外をキャッチする際の、例外の指定順序と同じような、条件の優位性が存在します。
例えば以下の例は、コンパイルエラーとなります。
switch (obj) { case CharSequence cs -> // ... case String s -> // Error - pattern is dominated by previous pattern default -> // ... }
パターン String s
にマッチするすべての値はパターン CharSequence cs
にもマッチするが、その逆はないので、最初の case
ラベル case CharSequence cs
は、2番目の case
ラベル case String s
を dominate していると言います(2番目のパターンの型である String
が、1番目のパターン型である CharSequence
のサブタイプであるため)。
以下の(match-all スイッチラベルを持つ)例も同様にコンパイルエラーとなります。
switch(s) { case Object o: // ... default: // ... }
guard 条件指定がある場合は、guard の中身についてのチェックは決定不可能なので行われませんが、guard 無しのパターンの前に記載する必要があります。
case
ラベルの順序は、定数 case
ラベルがガードされたパターン case
ラベルの前に書き、それらがガードされていないパターン case
ラベルの前に書きます。
Integer i = ... switch (i) { case -1, 1 -> ... // Special cases case Integer j when j > 0 -> ... // Positive integer cases case Integer j -> ... // All the remaining integers }
型の網羅性(Exhaustiveness)
スイッチ式は、セレクタ式のすべての可能な値をスイッチブロック内で処理することを要求します。
これは、switch
式の評価が成功すると必ず値が得られるという特性を維持するためです。
以下は型のカバレッジが網羅的でないため、コンパイルエラーとなります。
static int coverage(Object obj) { return switch (obj) { // Error - still not exhaustive case String s -> s.length(); case Integer i -> i; }; }
default
ラベルの型網羅性はすべての型なので、以下のようにすれば、網羅性を満たすため、合法になります。
static int coverage(Object obj) { return switch (obj) { case String s -> s.length(); case Integer i -> i; default -> 0; }; }
パターン以外の switch
式には、型のカバレッジの概念がすでに存在します。
例えば以下は網羅的と判断されます。
enum Color { RED, YELLOW, GREEN } int numLetters = switch (color) { case RED -> 3; case GREEN -> 5; case YELLOW -> 6; }
例えば、case GREEN
の条件を削除した場合は、網羅的ではないため、コンパイルエラーとなります。
match-all 節 を付けた場合は以下のようになります。
int numLetters = switch (color) { case RED -> 3; case GREEN -> 5; case YELLOW -> 6; default -> throw new ArghThisIsIrritatingException(color.toString()); }
これは、将来 Color.BLUE
などが追加された場合にコンパイルエラーとして検出できないため、可能であれば、match-all 節のない網羅的な switch
書くべきです。
網羅性の要件は、パターン switch
式とパターン switch
文の両方に適用されます。
後方互換性を確保するために、既存のすべての switch
ステートメントは変更せずにコンパイルされます。
セレクタ式の型がシールクラスの場合には、シールクラスの permits
句が考慮されるため、型の網羅性が保証されます。
switch
ステートメントが switch
拡張機能のいずれかを使用している場合、コンパイラーはそれが網羅的であるかどうかをチェックします(将来のJava言語のコンパイラーは、網羅的でないレガシーな switch
ステートメントに対して警告を発するかもしれません)。
パターン変数宣言のスコープ
パターン変数宣言のスコープは以下のように拡張されます。
- ガードされた
case
ラベルのパターン内で発生するパターン変数宣言のスコープは、ガード、すなわちwhen
式を含む switch
ルールのcase
ラベルに現れるパターン変数宣言のスコープには、矢印の右側に現れる式、ブロック、またはthrow
ステートメントが含まれるswitch
ラベルの付いたステートメントグループのcase
ラベルに現れるパターン変数宣言のスコープは、ステートメントグループのブロックステートメントを含む(パターン変数を宣言したcase
ラベルを通過することは禁止)
以下の例は、ケースブロックで c
と i
の両方がスコープに入るが、どちらかしか初期化されていない状況となるため許可されません。
switch (obj) { case Character c, Integer i: // error default: break; }
以下も同様に許可されていません。
switch (obj) { case Character c, Integer i -> ... // error default -> ...; }
以下のように break
が無く、フォールスルーする場合もコンパイルエラーとなります。
switch (obj) { case Character c: System.out.println("Character: " + c); case Integer i: System.out.println("Integer: " + i); default: break; }
一方、以下のようにパターン変数を宣言していないラベルをパススルーするようなケースは合法です。
switch (obj) { case String s: System.out.println("A string: " + s); default: System.out.println("Done"); }
switch で発生するエラー
パターン switch
の中にセレクタ式の値と一致するラベルがない場合も(パターンスイッチは網羅的でなければならないので) MatchException
がスローされます。
パターンマッチの過程でエラーが発生する場合、パターンマッチは MatchException
をスローするように定義されています(アクセサーメソッド内の処理で例外など)。
このようなパターンが switch
のラベルとして現れると、 switch
も MatchException
をスローします。
例えば以下のケースが該当します。
record R(int i) { public int i() { return i / 0; } // bad (but legal) accessor method for i } static void exampleAnR(R r) { switch(r) { case R(var i): System.out.println(i); } }
以下のケースは、 example(new R(42))
を実行すると、ArithmeticException
がスローされます。
static void example(Object obj) { switch (obj) { case R r when (r.i / 0 == 1): System.out.println("It's an R!"); default: break; } }
enum
に対する switch
は、switch
がコンパイルされた後に enum
クラスが変更された場合に、従来は IncompatibleClassChangeError
をスローしましたが、今後は MatchException
をスローするよう変更されています。