switchによるパターン・マッチング

switch式または文は、そのセレクタ式の値に応じて、制御を複数の文または式のいずれかに移します。セレクタ式は、任意の参照型またはプリミティブ型にできます。

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

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が含まれます。

次の例もwhen句を示しています。switch式のセレクタ式として、プリミティブ型doubleを使用します:

String doubleToRating(double rating) {
    return switch(rating) {
        case 0d -> "0 stars";
        case double d when d > 0d && d < 2.5d
            -> d + " is not good";
        case double d when d >= 2.5f && d < 5d
            -> d + " is better";
        case 5d -> "5 stars";
        default -> "Invalid rating";
    };
}

ノート:

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

詳細は、JEP 455: パターン、instanceofおよびswitchでのプリミティブ型(プレビュー)を参照してください。

次の例では、セレクタ式としてlongを使用します:

void bigNumbers(long v) {
    switch (v) {
        case long x when x < 1_000_000L ->
            System.out.println("Less than a million");
        case long x when x < 1_000_000_000L ->
            System.out.println("Less than a billion");
        case long x when x < 1_000_000_000_000L ->
            System.out.println("Less than a trillion");
        case long x when x < 1_000_000_000_000_000L ->
            System.out.println("Less than a quadrillion");
        default -> System.out.println("At least a quadrillion");    
    }
}

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

多くのパターン・ラベルがセレクタ式の値と一致する場合があります。予測しやすくするために、ラベルはswitchブロックに記述された順序でテストされます。さらに、先行するパターン・ラベルが必ず先に一致するため、パターン・ラベルがまったく一致しない場合は、コンパイラでエラーが発生します。次の例では、コンパイル時のエラーが発生します:

static void error(Object obj) {
    switch(obj) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        // error: this case label is dominated by a preceding case label
        case String s -> 
            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);
        // Compile-time errors for both cases -1 and 1:
        // this case label is dominated by a preceding case label
        case -1, 1 ->        
            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);
        // error: this case label is dominated by a preceding case label   
        case RED -> 
            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式または文では、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が初期化されていないことになります。

    パターン変数を使用できるその他の例は、「パターン変数のスコープとinstanceof」を参照してください。

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