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

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

ノート:

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

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

「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句を含むパターン・ラベルはガード付きパターン・ラベルと呼ばれ、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が含まれます。

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

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

ガード付きパターン・ラベル(when句を参照)も、定数ラベルより優先させることができます:

    static void error4(Integer value) {
        switch(value) {
            case Integer i when 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");
        }
    }    

ガード付きパターン・ラベルcase Integer i when i > 0は値-1と一致しませんが、それでもコンパイラによってエラーが生成されます。

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

    static void checkIntegers(Integer value) {
        switch(value) {
            case -1, 1 -> // Constant labels
                System.out.println("Value is 1 or -1");
            case Integer i when 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");
        }
    }

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.
        }
    }

具体的には、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の場合、switch式およびswitch文はNullPointerExceptionをスローしていました。しかしながら、柔軟性を高めるために、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ケース・ラベルを別のパターン・ラベルと組み合せることができます:

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

セレクタ式が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ケース・ラベルが含まれていません。

カッコ付きパターン

カッコ付きパターンは、カッコで囲まれたパターンです。ガード付きパターンはパターンと式を組み合せるため、解析のあいまいさが生じる可能性があります。パターンをカッコで囲むことで、これらのあいまいさを回避したり、パターンを含む式を異なる方法で解析するようにコンパイラに強制したり、コードの可読性を高めることができます。次に例を示します。

    static Function<Integer, String> testParen(Object obj) {
        boolean b = true;
        return switch (obj) {
            case String s && b -> t -> s;
            default            -> t -> "Default string";
        };
    }    

この例はコンパイルされます。ただし、最初の矢印トークン(->)がcaseラベルの一部であり、ラムダ式の一部ではないことを明確にしたい場合は、ガード付きパターンをカッコで囲むことができます:

    static Function<Integer, String> testParen(Object obj) {
        boolean b = true;
        return switch (obj) {
            case (String s && b) -> t -> s;
            default              -> t -> "Default string";
        };
    }