switch式および文のパターン・マッチング
switch
文は、そのセレクタ式の値に応じて、制御を複数の文または式のいずれかに移します。以前のリリースでは、セレクタ式は数値、文字列またはenum
定数に評価する必要があり、caseラベルは定数である必要がありました。ただし、このリリースでは、セレクタ式には任意のタイプを指定でき、case
ラベルにはパターンを含めることができます。その結果、switch
文または式は、そのセレクタ式がパターンと一致するかどうかをテストできます。これは、セレクタ式が定数とまったく同じかどうかをテストする場合に比べて、柔軟性および表現力が向上します。
ノート:
これはプレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。
switch
式および文のパターン・マッチングに関する背景情報は、JEP 433を参照してください。
「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
句を含むパターン・ラベルはガード付きパターン・ラベルと呼ばれ、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が含まれます。
パターン・ラベルの優位性
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");
}
}
ガード付きパターン・ラベル(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
の許可される唯一のサブクラスであるクラス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.
}
}
具体的には、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
の場合、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
をスローするのではなく、obj
がnull
の場合に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 obj
はString型のオブジェクトと一致しますが、この例ではNullPointerException
がスローされます。セレクタ式はnull
と評価され、switch
式にnull
ケース・ラベルが含まれていません。
カッコ付きパターン
カッコ付きパターンは、カッコで囲まれたパターンです。ガード付きパターンはパターンと式を組み合せるため、解析のあいまいさが生じる可能性があります。パターンをカッコで囲むことで、これらのあいまいさを回避したり、パターンを含む式を異なる方法で解析するようにコンパイラに強制したり、コードの可読性を高めることができます。次に例を示します。
static Function<Integer, String> testParen(Object obj) {
boolean b = true;
return switch (obj) {
case String s && b -> t -> s;
default -> t -> "Default string";
};
}
この例はコンパイルされます。ただし、最初の矢印トークン(->
)がcase
ラベルの一部であり、ラムダ式の一部ではないことを明確にしたい場合は、ガード付きパターンをカッコで囲むことができます:
static Function<Integer, String> testParen(Object obj) {
boolean b = true;
return switch (obj) {
case (String s && b) -> t -> s;
default -> t -> "Default string";
};
}