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

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

ノート:

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

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

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");
        }        
    }

ガード付きパターン・ラベル(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式または文が完了時に網羅的だが、実行時にはそうでない場合、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)

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

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ケース・ラベル

このプレビュー機能以前は、セレクタ式の値が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ケース・ラベルが含まれていません。