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

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

ノート:

これは、設計、仕様および実装は完了しているが、永続的でないプレビュー機能であり、将来のJava SEリリースでは別の形式で存在するか、完全になくなる可能性があることを意味します。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。

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

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

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

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

    public static double getPerimeter(Shape shape) throws IllegalArgumentException {
        return switch (shape) {
            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 shape) throws IllegalArgumentException {
        switch (shape) {
            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");
        }
    }

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

多くのパターン・ラベルがセレクタ式の値と一致する場合があります。予測しやすくするために、ラベルは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 ->
                throw new IllegalStateException("Invalid argument");  
        }
    }

最初のパターン・ラベル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 ->
                throw new IllegalStateException("Invalid argument");
        }
    }
    
    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 error4(Integer value) {
        switch(value) {
            case Integer i && i > 0 ->
                System.out.println("Positive integer");
            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("Value is 1 or -1");
            default ->
                throw new IllegalStateException("Invalid argument");
        }
    }    

優位性に関連するこれらのコンパイラ・エラーを解決するには、ガード付きパターン・ラベルの前に定数ラベルを記述し、ガードのないタイプのパターン・ラベルの前にガード付きパターン・ラベルを記述するようにしてください:

    static void checkIntegers(Integer value) {
        switch(value) {
            case -1, 1 -> // Constant labels
                System.out.println("Value is 1 or -1");
            case Integer i && i > 0 -> // Guarded pattern label
                System.out.println("Positive integer");
            case Integer i -> // Non-guarded type pattern label
                System.out.println("Neither positive, 1, nor -1");
        }
    }

すべての値に一致するラベルが2つあります: defaultラベルと合計タイプ・パターン(「Null一致caseラベル」を参照)。switchブロックにこれら2つのラベルを複数持つことはできません。

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

「Switch式」で説明しているように、switch式およびswitch文(パターンまたはnullラベルが使用されている)のswitchブロックは網羅的にして、考えられるすべての値に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;
        };
    }

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

「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式では、矢印の右側に表示される式、ブロックまたは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 ->
                throw new IllegalStateException("Invalid argument"); 
        }
    }

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

switch文では、caseラベルのパターン変数を、switchラベル付き文グループで使用できます。ただし、プログラム・フローがdefault文グループを経由する場合でも、他のswitchラベル付き文グループでは使用できません。次に例を示します:

    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:
                throw new IllegalStateException("Invalid argument");
        }
    }

パターン変数cのスコープは、case Character c文グループ(2つのif文とそれに続くprintln文)で構成されます。switch文でcase Character c文グループを実行し、defaultのcaseラベルを経由してから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のcaseラベルを経由します。ここで、パターン変数iが初期化されていないことになります。

同様に、caseラベルに複数のパターン変数を宣言することはできません。cまたはiのいずれかが(objの値に応じて)初期化されることは、許可されません。
    case Character c, Integer i: ...
    case Character c, Integer i -> ...

Null一致caseラベル

このプレビュー機能の前に、セレクタ式の値がNULLの場合、switch式およびswitch文はNullPointerExceptionをスローします。ただし、パターン・ラベルにより柔軟性が向上しました。新しいnull一致caseラベルは2つになりました。まず、null caseラベルを使用できます:

    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をスローするのではなく、objがnullの場合にnull!を出力します。

次に、パターンが合計タイプ・パターンのパターン・ラベルは、セレクタ式の値がnullの場合にnullに一致します。Sの型消去がTの型消去のサブタイプである場合、タイプ・パターンT tは、タイプS合計です。たとえば、タイプ・パターンObject objは、タイプStringの合計です。次のswitch文を考えてみます:

        String s = ...
        switch (s) {
            case Object obj -> ... // total type pattern, so it matches null!
        }

パターン・ラベルcase Object objは、sがnullと評価された場合に適用されます。

セレクタ式がnullと評価され、switchブロックにnull一致のパターン・ラベルがない場合、NullPointerExceptionは通常どおりにスローされます。