switch式および文のパターン・マッチング
switch
文は、そのセレクタ式の値に応じて、制御を複数の文または式のいずれかに移します。以前のリリースでは、セレクタ式は数値、文字列またはenum
定数に評価する必要があり、caseラベルは定数である必要がありました。ただし、このリリースでは、セレクタ式には任意の参照型またはint
型を指定できますが、long
、float
、double
またはboolean
型は指定できず、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");
}
}
セレクタ式タイプ
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
句のブール式はガードと呼ばれます。値がガード付きパターン・ラベルと一致するのは、値がパターンと一致し、したがってガードも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");
}
}
最初のパターン・ラベル(ガイド付きパターン・ラベル)が一致するのは、obj
がString
でその長さが1の場合です。2番目のパターン・ラベルが一致するのはobj
が異なる長さのString
である場合です。
ガード付きパターン・ラベルの形式は、p when e
で、pはパターン、e
はブール式です。pで宣言されるパターン変数のスコープには、eが含まれます。
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
ブロックに記述された順序でテストされます。さらに、先行するパターン・ラベルが必ず先に一致するため、パターン・ラベルがまったく一致しない場合は、コンパイラでエラーが発生します。次の例では、コンパイル時のエラーが発生します:
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 -> { break; }
}
}
最初のパターン・ラベル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 -> { break; }
}
}
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 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文におけるタイプ・カバレッジ
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
の許可される唯一のサブクラスであるクラス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;
};
}
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はシールされています。型CとDが、可能性のあるすべてのインスタンスに対応します:
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 {}
クラスME
のswitch
式は網羅的であり、この例はコンパイルされます。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.
}
}
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"); } }
このコードが許可され、セレクタ式の値
obj
がCharacter
であった場合、switch
文はcase Character c
文グループを実行してから、case Integer i
ラベルを経由します。ここで、パターン変数i
が初期化されていないことになります。
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
をスローするのではなく、obj
がnull
の場合に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 obj
はString型のオブジェクトと一致しますが、この例ではNullPointerException
がスローされます。セレクタ式はnull
と評価され、switch
式にnull
ケース・ラベルが含まれていません。