switch式および文のパターン・マッチング

switch文は、そのセレクタ式の値に応じて、制御を複数の文または式のいずれかに移します。以前のリリースでは、セレクタ式は数値、文字列またはenum定数に評価する必要があり、caseラベルは定数である必要がありました。ただし、このリリースでは、セレクタ式には任意の参照型またはint型を指定できますが、longfloatdoubleまたはboolean型は指定できず、caseラベルにはパターンを含めることができます。その結果、switch文または式は、そのセレクタ式がパターンと一致するかどうかをテストできます。これは、セレクタ式が定数とまったく同じかどうかをテストする場合に比べて、柔軟性および表現力が向上します。

switch式および文のパターン・マッチングに関する背景情報は、JEP 441を参照してください。

「instanceof演算子のパターン・マッチング」の項の、特定の形状の周辺の長さを計算する次のコードを見ていきます:

interface Shape { }
record Rectangle(double length, double width) implements Shape { }
record Circle(double radius) implements Shape { }
...
    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        if (s instanceof Rectangle r) {
            return 2 * r.length() + 2 * r.width();
        } else if (s instanceof Circle c) {
            return 2 * c.radius() * Math.PI;
        } else {
            throw new IllegalArgumentException("Unrecognized shape");
        }
    }

次のように、パターンswitch式を使用するようにこのコードを書き直すことができます:

    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        return switch (s) {
            case Rectangle r -> 2 * r.length() + 2 * r.width();
            case Circle c    -> 2 * c.radius() * Math.PI;
            default          -> throw new IllegalArgumentException("Unrecognized shape");
        };
    }

次の例では、switch式のかわりにswitch文を使用します:

    public static double getPerimeter(Shape s) throws IllegalArgumentException {
        switch (s) {
            case Rectangle r: return 2 * r.length() + 2 * r.width();
            case Circle c:    return 2 * c.radius() * Math.PI;
            default:          throw new IllegalArgumentException("Unrecognized shape");
        }
    }

セレクタ式タイプ

セレクタ式のタイプには、整数プリミティブ型または任意の参照型(前述の例など)を指定できます。次のswitch式は、セレクタ式objを、クラス・タイプ、列挙型、レコード・タイプおよび配列タイプを含むタイプ・パターンと一致させます:
record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE; }
...
    static void typeTester(Object obj) {
        switch (obj) {
            case null     -> System.out.println("null");
            case String s -> System.out.println("String");
            case Color c  -> System.out.println("Color with " + c.values().length + " values");
            case Point p  -> System.out.println("Record class: " + p.toString());
            case int[] ia -> System.out.println("Array of int values of length" + ia.length);
            default       -> System.out.println("Something else");
        }
    }

when句

ブール式は、when句を使用してパターン・ラベルの直後に追加できます。これはガード付きパターン・ラベルと呼ばれます。when句のブール式はガードと呼ばれます。値がガード付きパターン・ラベルと一致するのは、値がパターンと一致し、したがってガードもtrueと評価される場合です。次に例を示します。

    static void test(Object obj) {
        switch (obj) {
            case String s:
                if (s.length() == 1) {
                    System.out.println("Short: " + s);
                } else {
                    System.out.println(s);
                }
                break;
            default:
                System.out.println("Not a string");
        }
    }

ブール式s.length == 1は、when句を使用してcaseラベルの直後に移行できます:

    static void test(Object obj) {
        switch (obj) {
            case String s when s.length() == 1 -> System.out.println("Short: " + s);
            case String s                      -> System.out.println(s);
            default                            -> System.out.println("Not a string");
        }
    }

最初のパターン・ラベル(ガイド付きパターン・ラベル)が一致するのは、objStringでその長さが1の場合です。2番目のパターン・ラベルが一致するのはobjが異なる長さのStringである場合です。

ガード付きパターン・ラベルの形式は、p when eで、pはパターン、eはブール式です。pで宣言されるパターン変数のスコープには、eが含まれます。

case定数としての修飾されたenum定数

修飾されたenum定数をswitch式および文のcase定数として使用できます。

セレクタ式がenum型である次のswitch式について考えてみます。

    public enum Standard { SPADE, HEART, DIAMOND, CLUB }

    static void determineSuitStandardDeck(Standard d) {
        switch (d) {
            case SPADE   -> System.out.println("Spades");
            case HEART   -> System.out.println("Hearts");
            case DIAMOND -> System.out.println("Diamonds");
            default      -> System.out.println("Clubs");   
        }
    }

次の例では、セレクタ式の型は、2つのenum型によって実装されているインタフェースです。セレクタ式の型はenum型ではないため、このswitch式ではガード付きパターンがかわりに使用されます。

    sealed interface CardClassification permits Standard, Tarot {}
    public enum Standard implements CardClassification
        { SPADE, HEART, DIAMOND, CLUB }
    public enum Tarot implements CardClassification
        { SPADE, HEART, DIAMOND, CLUB, TRUMP, EXCUSE }

    static void determineSuit(CardClassification c) {
        switch (c) {
            case Standard s when s == Standard.SPADE    -> System.out.println("Spades");
            case Standard s when s == Standard.HEART    -> System.out.println("Hearts");
            case Standard s when s == Standard.DIAMOND  -> System.out.println("Diamonds");
            case Standard s                             -> System.out.println("Clubs");   
            case Tarot t when t == Tarot.SPADE          -> System.out.println("Spades or Piques");
            case Tarot t when t == Tarot.HEART          -> System.out.println("Hearts or C\u0153ur");
            case Tarot t when t == Tarot.DIAMOND        -> System.out.println("Diamonds or Carreaux");
            case Tarot t when t == Tarot.CLUB           -> System.out.println("Clubs or Trefles");
            case Tarot t when t == Tarot.TRUMP          -> System.out.println("Trumps or Atouts");
            case Tarot t                                -> System.out.println("The Fool or L'Excuse");
        }
    }

ただし、switch式および文では修飾されたenum定数が許可されるため、この例を次のように書き換えることができます。

    static void determineSuitQualifiedNames(CardClassification c) {
        switch (c) {
            case Standard.SPADE   -> System.out.println("Spades");
            case Standard.HEART   -> System.out.println("Hearts");
            case Standard.DIAMOND -> System.out.println("Diamonds");
            case Standard.CLUB    -> System.out.println("Clubs");   
            case Tarot.SPADE      -> System.out.println("Spades or Piques");
            case Tarot.HEART      -> System.out.println("Hearts or C\u0153ur");
            case Tarot.DIAMOND    -> System.out.println("Diamonds or Carreaux");
            case Tarot.CLUB       -> System.out.println("Clubs or Trefles");
            case Tarot.TRUMP      -> System.out.println("Trumps or Atouts");
            case Tarot.EXCUSE     -> System.out.println("The Fool or L'Excuse");
        }
    }

したがって、enum定数の名前が修飾されていて、その値がセレクタ式の型への代入互換性を持っていれば、セレクタ式の型がenum型でない場合にenum定数を使用できます。

パターン・ラベルの優位性

多くのパターン・ラベルがセレクタ式の値と一致する場合があります。予測しやすくするために、ラベルはswitchブロックに記述された順序でテストされます。さらに、先行するパターン・ラベルが必ず先に一致するため、パターン・ラベルがまったく一致しない場合は、コンパイラでエラーが発生します。次の例では、コンパイル時のエラーが発生します:
    static void error(Object obj) {
        switch(obj) {
            case CharSequence cs ->
                System.out.println("A sequence of length " + cs.length());
            case String s -> // error: this case label is dominated by a preceding case label
                System.out.println("A string: " + s);
            default -> { break; }
        }
    }

最初のパターン・ラベルcase CharSequence csは、2番目のパターン・ラベルcase String sより優位となります。これは、パターンString sに一致するすべての値がパターンCharSequence csにも一致しますが、その逆の方法は一致しないためです。それは、StringCharSequenceのサブタイプであるためです。

パターン・ラベルは、定数ラベルより優先させることができます。次の例では、コンパイル時にエラーが発生します:

    static void error2(Integer value) {
        switch(value) {
            case Integer i ->
                System.out.println("Integer: " + i);
            case -1, 1 -> // Compile-time errors for both cases -1 and 1:
                          // this case label is dominated by a preceding case label       
                System.out.println("The number 42");
            default -> { break; }
        }
    }
    
    enum Color { RED, GREEN, BLUE; }
    
    static void error3(Color value) {
        switch(value) {
            case Color c ->
                System.out.println("Color: " + c);
            case RED -> // error: this case label is dominated by a preceding case label
                System.out.println("The color red");
        }        
    }

ノート:

ガード付きパターン・ラベルは定数ラベルより優位ではありません。たとえば:
    static void testInteger(Integer value) {
        switch(value) {
            case Integer i when i > 0 ->
                System.out.println("Positive integer");
            case 1 -> 
                System.out.println("Value is 1");
            case -1 -> 
                System.out.println("Value is -1");
            case Integer i ->
                System.out.println("An integer");
        }
    }

1は、ガード付きパターン・ラベルcase Integer i when i > 0と定数ラベルcase 1の両方に一致しますが、ガード付きパターン・ラベルは定数ラベルより優位ではありません。ガード付きパターンは、一般に決定不能であるため、優位性が確認されません。したがって、caseラベルを順序付けして、定数ラベルが最初に表示され、その後にガード付きパターン・ラベルが続き、その後に非ガード付きパターン・ラベルが続くようにする必要があります。

   static void testIntegerBetter(Integer value) {
        switch(value) {
            case 1 -> 
                System.out.println("Value is 1");
            case -1 -> 
                System.out.println("Value is -1");
            case Integer i when i > 0 ->
                System.out.println("Positive integer");
            case Integer i ->
                System.out.println("An integer");
        }
    }

switch式およびswitch文におけるタイプ・カバレッジ

switch式で説明したように、switch式とswitch文のswitchブロック(パターン・ラベルまたはnullラベルを使用する)は、網羅的であることが必要です。これは、可能性があるすべての値について、一致するswitchラベルが存在する必要があることを意味します。次のswitch式は、網羅的ではなく、コンパイル時エラーが発生します。タイプ・カバレッジが、StringまたはIntegerのサブタイプで構成されており、セレクタ式のタイプObjectが含まれていません:

    static int coverage(Object obj) {
        return switch (obj) {         // Error - not exhaustive
            case String s  -> s.length();
            case Integer i -> i;
        };
    }

ただし、caseラベルdefaultのタイプ・カバレッジはすべてのタイプであるため、次の例がコンパイルされます:

    static int coverage(Object obj) {
        return switch (obj) { 
            case String s  -> s.length();
            case Integer i -> i;
            default        -> 0;
        };
    }

コンパイラは、セレクタ式のタイプがシール・クラスであるかどうかを考慮します。次のswitch式がコンパイルされます。セレクタ式のタイプであるSの許可される唯一のサブクラスであるクラスABおよびCがタイプ・カバレッジであるため、defaultケース・ラベルは必要ありません:

sealed interface S permits A, B, C { }
final class A implements S { }
final class B implements S { }
record C(int i) implements S { }  // Implicitly final
...
    static int testSealedCoverage(S s) {
        return switch (s) {
            case A a -> 1;
            case B b -> 2;
            case C c -> 3;
        };
    }

コンパイラでは、そのセレクタ式のタイプが汎用シール・クラスの場合、switch式またはswitch文のタイプ・カバレッジを判断することもできます。次の例では、コンパイルが行われます。インタフェースIに使用できるサブクラスは、クラスAおよびBのみです。ただし、セレクタ式のタイプがI<Integer>であるため、網羅的にするには、switchブロックのタイプ・カバレッジはクラスBにする必要があります。

    sealed interface I<T> permits A, B {}
    final class A<X> implements I<String> {}
    final class B<Y> implements I<Y> {}

    static int testGenericSealedExhaustive(I<Integer> i) {
        return switch (i) {
        // Exhaustive as no A case possible!  
            case B<Integer> bi -> 42;
        };
    }

switch式または文のセレクタ式の型を、汎用レコードにすることもできます。常に、switch式または文は網羅的であることが必要です。次の例ではコンパイルが行われません2つの値(両方が型A)を含むPairとの一致はありません:

record Pair<T>(T x, T y) {}
class A {}
class B extends A {}

    static void notExhaustive(Pair<A> p) {
        switch (p) {                 
            // error: the switch statement does not cover all possible input values
            case Pair<A>(A a, B b) -> System.out.println("Pair<A>(A a, B b)");
            case Pair<A>(B b, A a) -> System.out.println("Pair<A>(B b, A a)");
        }        
    }

次の例では、コンパイルが行われます。インタフェースIはシールされています。型CDが、可能性のあるすべてのインスタンスに対応します:

record Pair<T>(T x, T y) {}
sealed interface I permits C, D {}
record C(String s) implements I {}
record D(String s) implements I {}

    static void exhaustiveSwitch(Pair<I> p) {
        switch (p) {
            case Pair<I>(I i, C c) -> System.out.println("C = " + c.s());
            case Pair<I>(I i, D d) -> System.out.println("D = " + d.s());
        }
    }

switch式または文が完了時に網羅的だが、実行時にはそうでない場合、MatchExceptionがスローされます。これが発生する可能性があるのは、網羅的なswitch式または文を含むクラスがコンパイルされるが、switch式または文の分析に使用されたシール階層がその後で変更され再コンパイルされている場合です。そのような変更には移行互換性がないため、switch文または式の実行時にMatchExceptionがスローされることにつながります。したがって、switch式または文を含むクラスを再コンパイルする必要があります。

次の2つのクラスMEおよびSealを考えてみます:

class ME {
    public static void main(String[] args) {
        System.out.println(switch (Seal.getAValue()) {
            case A a -> 1;
            case B b -> 2;
        });
    }
}
sealed interface Seal permits A, B {
    static Seal getAValue() {
        return new A();
    }
}
final class A implements Seal {}
final class B implements Seal {}

クラスMEswitch式は網羅的であり、この例はコンパイルされます。MEを実行すると、値1が出力されます。ただし、Sealを次のように編集し、MEではなく、このクラスをコンパイルするとします:

sealed interface Seal permits A, B, C {
    static Seal getAValue() {
        return new A();
    }
}
final class A implements Seal {}
final class B implements Seal {}
final class C implements Seal {}

MEを実行すると、MatchExceptionがスローされます:

Exception in thread "main" java.lang.MatchException
            at ME.main(ME.java:3)

レコード・パターンの型引数の推論

コンパイラは、パターンを受け入れるすべての構文(switch文、instanceof式、拡張for文)で、汎用レコード・パターンの型引数を推論できます。

次の例で、コンパイラはMyPair(var s, var i)MyPair<String, Integer>(String s, Integer i)と推論します:

record MyPair<T, U>(T x, U y) { }

   static void recordInference(MyPair<String, Integer> p){
        switch (p) {
            case MyPair(var s, var i) -> 
                System.out.println(s + ", #" + i);
        }
    }   

レコード・パターンの型引数の推論の詳細は、「レコード・パターン」を参照してください。

パターン変数宣言のスコープ

「instanceof演算子のパターン・マッチング」の項で説明したように、パターン変数のスコープは、instanceof演算子がtrueの場合にのみプログラムが到達できる箇所です:

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        if (shape instanceof Rectangle s) {
            // You can use the pattern variable s of type Rectangle here.
        } else if (shape instanceof Circle s) {
            // You can use the pattern variable s of type Circle here
            // but not the pattern variable s of type Rectangle.
        } else {
            // You cannot use either pattern variable here.
        }
    }

switch式または文では、caseラベルで宣言されたパターン変数のスコープに次のものが含まれます。

  • caseラベルのwhen句:
    static void test(Object obj) {
        switch (obj) {
            case Character c when c.charValue() == 7:
                System.out.println("Ding!");
                break;
            default:
                break;
            }
        }
    }

    パターン変数cのスコープには、宣言cを含むcaseラベルのwhen句が含まれます。

  • caseラベルの矢印の右側にある、式、ブロック、またはthrow文:

        static void test(Object obj) {
            switch (obj) {
                case Character c -> {
                    if (c.charValue() == 7) {
                        System.out.println("Ding!");
                    }
                    System.out.println("Character, value " + c.charValue());
                }
                case Integer i ->
                    System.out.println("Integer: " + i);  
                default -> {
                    break; 
                }
            }
        }

    パターン変数cのスコープには、case Character c ->の右側のブロックが含まれます。パターン変数iのスコープには、case Integer i ->の右側にあるprintln文が含まれます。

  • switchラベルが付いた文のグループ(caseラベル):

        static void test(Object obj) {
            switch (obj) {
                case Character c:
                    if (c.charValue() == 7) {
                        System.out.print("Ding ");
                    }
                    if (c.charValue() == 9) {
                        System.out.print("Tab ");
                    }
                    System.out.println("character, value " + c.charValue());
                default:
                    // You cannot use the pattern variable c here:
                    break;
            }
        }

    パターン変数cのスコープには、case Character c文グループ(2つのif文とそれに続くprintln文)が含まれます。switch文でcase Character c文グループを実行し、defaultラベルを経由してからdefault文グループを実行できる場合でも、スコープにはdefault文グループは含まれません。

    ノート:

    パターン変数を宣言するcaseラベルを経由する可能性がある場合は、コンパイル時エラーが発生します。次の例ではコンパイルが行われません:
        static void test(Object obj) {
            switch (obj) {
                case Character c:
                    if (c.charValue() == 7) {
                        System.out.print("Ding ");
                    }
                    if (c.charValue() == 9) {
                        System.out.print("Tab ");
                    }
                    System.out.println("character");
                case Integer i:                 // Compile-time error
                    System.out.println("An integer " + i);
                default:
                    System.out.println("Neither character nor integer");
            }
        }

    このコードが許可され、セレクタ式の値objCharacterであった場合、switch文はcase Character c文グループを実行してから、case Integer iラベルを経由します。ここで、パターン変数iが初期化されていないことになります。

nullケース・ラベル

セレクタ式の値がnullの場合、NullPointerExceptionをスローするために使用されるswitch式およびswitch文。現在は、柔軟性を高めるために、nullケース・ラベルを使用できます。
    static void test(Object obj) {
        switch (obj) {
            case null     -> System.out.println("null!");
            case String s -> System.out.println("String");
            default       -> System.out.println("Something else");
        }
    }

この例では、NullPointerExceptionをスローするのではなく、objnullの場合にnull!を出力します。

nullケース・ラベルをdefaultケース・ラベル以外と組み合せることはできません。次のコードでは、コンパイラ・エラーが生成されます。

    static void testStringOrNull(Object obj) {
        switch (obj) {
            // error: invalid case label combination
            case null, String s -> System.out.println("String: " + s);
            default             -> System.out.println("Something else");
        }
    }

一方、次のコードはコンパイルされます。

    static void testStringOrNull(Object obj) {
        switch (obj) {
            case String s       -> System.out.println("String: " + s);
            case null, default  -> System.out.println("null or not a string");
        }
    }

セレクタ式がnullと評価されたときに、switchブロックにnullケース・ラベルが含まれない場合、NullPointerExceptionが通常どおりにスローされます。次のswitch文を考えてみます:

        String s = null;
        switch (s) {
            case Object obj -> System.out.println("This doesn't match null");
            // No null label; NullPointerException is thrown
            // if s is null
        }

パターン・ラベルcase Object objString型のオブジェクトと一致しますが、この例ではNullPointerExceptionがスローされます。セレクタ式はnullと評価され、switch式にnullケース・ラベルが含まれていません。