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
にも一致しますが、その逆の方法は一致しないためです。それは、String
がCharSequence
のサブタイプであるためです。
パターン・ラベルは、定数ラベルより優先させることができます。次の例では、コンパイル時にエラーが発生します:
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
の許可される唯一のサブクラスであるクラスA
、B
および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");
}
}
このコードが許可され、セレクタ式の値obj
がCharacter
であった場合、switch
文はcase Character c
文グループを実行してから、case Integer i
のcaseラベルを経由します。ここで、パターン変数i
が初期化されていないことになります。
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
は通常どおりにスローされます。