11 switch式および文

switchキーワードは、文または式として使用できます。あらゆる式と同様、switch式は1つの値に評価され、文での使用が可能です。switch式では、フォール・スルーを防ぐためのbreak文が不要になるcase L ->ラベルを使用できます。switch式の値の指定には、yield文を使用します。

switch式の設計に関する背景情報は、JEP 361を参照してください。

矢印case

曜日の文字数を出力する次のswitch文について検討してみます。

public enum Day { SUNDAY, MONDAY, TUESDAY,
    WEDNESDAY, THURSDAY, FRIDAY, SATURDAY; }
// ...

int numLetters = 0;
Day day = Day.WEDNESDAY;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
       numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
}
System.out.println(numLetters);

曜日名の長さは、変数numLettersに格納するかわりに値を戻せた方が便利で、これはswitch式を使用して行うことができます。また、フォール・スルーを防ぐのに、記述が面倒で付け忘れがちなbreak文が必要がなくなれば、さらに便利です。これは、矢印caseを使用して実行できます。次のswitch式では、矢印caseを使用して曜日の文字数を出力します:

Day day = Day.WEDNESDAY;    
System.out.println(
    switch (day) {
        case MONDAY, FRIDAY, SUNDAY -> 6;
        case TUESDAY                -> 7;
        case THURSDAY, SATURDAY     -> 8;
        case WEDNESDAY              -> 9;
    }
);    

矢印caseの形式は次のとおりです:

case label_1, label_2, ..., label_n -> expression;|throw-statement;|block 

Javaランタイムが矢印の左側にある任意のラベルに一致すると、矢印の右側にあるコードが実行されて、フォール・スルーしません。つまり、switch式(または文)内の他のコードは実行されません。矢印の右側のコードが式である場合、その式の値はswitch式の値になります。

矢印caseをswitch文で使用できます。次の例は最初の例に似ていますが、コロンcaseではなく矢印caseを使用します:

int numLetters = 0;
Day day = Day.WEDNESDAY;
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> numLetters = 6;
    case TUESDAY                -> numLetters = 7;
    case THURSDAY, SATURDAY     -> numLetters = 8;
    case WEDNESDAY              -> numLetters = 9;
};
System.out.println(numLetters);

矢印caseとその右側のコードを合せて、switchラベル付きルールと呼びます。

コロンcaseとyield文

コロンcaseとは、case L:の形式のcaseラベルです。コロンcaseをswitch式で使用できます。コロンcaseと右側のコードを合せて、switchラベル付き文グループと呼びます:

Day day = Day.WEDNESDAY;
int numLetters = switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        yield 6;
    case TUESDAY:
        System.out.println(7);
        yield 7;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        yield 8;
    case WEDNESDAY:
        System.out.println(9);
        yield 9;
};
System.out.println(numLetters);

前の例ではyield文が使用されています。引数を1つ取りますが、それはコロンcaseがswitch式で生成する値です。

yield文により、switch文とswitch式を容易に区別できるようになります。switch文(switch式ではない)は、break文のターゲットとすることができます。反対に、switch式(switch文ではない)は、yield文のターゲットとすることができます。

ノート:

矢印caseを使用することをお薦めします。コロンcaseの使用時は、break文またはyield文を挿入するのを忘れがちです。これを忘れると、コード内で思いがけないフォール・スルーが発生する場合があります。

矢印caseで、複数の文または式でないコード、あるいはthrow文を指定するには、それらを1つのブロックに入れます。矢印caseが生成する値をyield文で指定します:

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> {
        System.out.println(6);
        yield 6;
    }
    case TUESDAY -> {
        System.out.println(7);
        yield 7;
    }
    case THURSDAY, SATURDAY -> {
        System.out.println(8);
        yield 8;
    }
    case WEDNESDAY -> {
        System.out.println(9);
        yield 9;
    }
};  

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式および文でのプリミティブ値

プリミティブ型のcharbyteshortおよびintに加えて、switch式または文のセレクタ式は、long型、float型、double型およびboolean型と、対応するboxed型にすることができます。

ノート:

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

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

セレクタ式の型がlongfloatdoubleおよびbooleanの場合、caseラベルはセレクタ式または対応するboxed型と同じ型である必要があります。たとえば:

void whichFloat(float v) {
    switch (v) {
        case 0f ->
            System.out.println("Zero");
        case float x when x > 0f && x <= 10f ->
            System.out.println(x + " is between 1 and 10");
        case float x ->
            System.out.println(x + " is larger than 10");
    }            
}

caseラベル0f0に変更すると、次のようなコンパイル時のエラーが発生します:

error: constant label of type int is not compatible with switch selector type float

2つの浮動小数点リテラルを表現的に等価caseラベルとして使用することはできません。(表現の等価性の詳細は、DoubleクラスのJavaDoc APIドキュメントを参照してください。)次の例では、コンパイラ・エラーが生成されます。値0.999999999fは、1.0fと表現的に等価です:

void duplicateLabels(float v) {
    switch (v) {
        case 1.0f -> System.out.println("One");
        // error: duplicate case label
        case 0.999999999f -> System.out.println("Almost one");
        default -> System.out.println("Another float value");
    }            
}

boolean switch式または文には文や式を含めることができるため、boolean値の切替えは、3項条件演算子(?:)の代替として便利です。たとえば、次のコードでは、boolean switch式を使用して、falseの場合にロギングを実行します:

long startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
    case true  -> user.id();
    case false -> { log("Unrecognized user"); yield -1L; }
});

switchの網羅性

switch式または文のcaseは網羅的であることが求められるため、考えられるすべての値に一致するcaseラベルが存在する必要があります。そのため、switch式または文には通常default句が必要です。ただし、既知の定数をすべて網羅するenum switch式では、この項の冒頭にある曜日の名前の文字数を出力する例のように、コンパイラによって暗黙的なdefault句が挿入されます。

パターン・ラベルまたはnullラベルを使用する場合、switch文のcaseは網羅的である必要があります。詳細は、「switchによるパターン・マッチング」および「nullケース・ラベル」を参照してください。

次の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式または文が網羅的であるためには、そのラベルのタイプ・カバレッジにセレクタ式のタイプが含まれている必要があります。

コンパイラは、セレクタ式のタイプがシール・クラスであるかどうかを考慮します。次の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式または文を含むクラスを再コンパイルする必要があります。

次のクラスPrintAおよびシールされたインタフェースOnlyABについて考えてみます:

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

クラスPrintAswitch式は網羅的であり、この例はコンパイルされます。PrintAを実行すると、値1が出力されます。しかし、OnlyABを次のように編集して、PrintAではなく、このインタフェースをコンパイルするとします:

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

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

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

完了とswitch式

switch式は、値で正常に完了するか、例外をスローして突然完了するかのいずれかである必要があります

A.たとえば、次のコードは、switchラベルの付いたルールにyield文がないため、コンパイルされません:

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // error: block doesn't contain a yield statement
    }
    default -> 1;
};

次の例は、switchラベルの付いた文グループにyield文がないため、コンパイルされません:

i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // error: group doesn't contain a yield statement
};

switch式は1つの値に評価される(または例外をスローする)必要があるため、次の例のように、breakyieldreturnまたはcontinue文を使用してswitch式をジャンプすることはできません:

z: 
    for (int i = 0; i < MAX_VALUE; ++i) {
        int k = switch (e) { 
            case 0:  
                yield 1;
            case 1:
                yield 2;
            default: 
                continue z; 
                // error: illegal jump through a switch expression 
        };
    // ...
    }