スイッチのパターンマッチング
1. 概要
Java SE 17リリースでは、プレビュー機能として switch 式およびステートメント( JEP 406 )のパターンマッチングが導入されています。 パターンマッチングは、スイッチケースの条件を定義する際の柔軟性を高めます。
パターンを含めることができるケースラベルに加えて、セレクター式は少数のタイプに限定されなくなりました。 パターンマッチングの前は、 switch のケースは、定数値と正確に一致する必要があるセレクター式の単純なテストのみをサポートしていました。
このチュートリアルでは、switchステートメントに適用できる3つの異なるパターンタイプについて説明します。 また、すべての値のカバー、サブクラスの順序付け、null値の処理など、switchの詳細についても説明します。
2. Switchステートメント
Javaでswitchを使用して、いくつかの事前定義されたcaseステートメントの1つに制御を転送します。 どのステートメントが選択されるかは、switchセレクター式の値によって異なります。
以前のバージョンのJavaでは、セレクター式は、数値、文字列、または定数である必要がありました。 また、ケースラベルには定数のみを含めることができます。
final String b = "B";
switch (args[0]) {
case "A" -> System.out.println("Parameter is A");
case b -> System.out.println("Parameter is b");
default -> System.out.println("Parameter is unknown");
};
この例では、変数bが final でない場合、コンパイラーは定数式に必要なエラーをスローします。
3. パターンマッチング
一般に、パターンマッチングは、JavaSE14のプレビュー機能として最初に導入されました。
これは、パターンの1つの形式(タイプパターン)のみに制限されていました。 典型的なパターンは、タイプ名と結果をバインドする変数で構成されます。
instanceof演算子に型パターンを適用すると、型のチェックとキャストが簡単になります。 さらに、両方を1つの式に組み合わせることができます。
if (o instanceof String s) {
System.out.printf("Object is a string %s", s);
} else if (o instanceof Number n) {
System.out.printf("Object is a number %n", n);
}
この組み込みの言語拡張機能により、コードの記述が少なくなり、読みやすさが向上します。
4. スイッチのパターン
instanceof のパターンマッチングは、JavaSE16の永続的な機能になりました。
Java 17では、パターンマッチングのアプリケーションもスイッチ式に拡張されています。
ただし、それでもプレビュー機能であるため、プレビューを有効にして使用する必要があります。
java --enable-preview --source 17 PatternMatching.java
4.1. タイプパターン
タイプパターンと演算子のインスタンスをswitchステートメントに適用する方法を見てみましょう。
例として、 if-else ステートメントを使用して、さまざまなタイプをdoubleに変換するメソッドを作成します。 タイプがサポートされていない場合、このメソッドは単にゼロを返します。
static double getDoubleUsingIf(Object o) {
double result;
if (o instanceof Integer) {
result = ((Integer) o).doubleValue();
} else if (o instanceof Float) {
result = ((Float) o).doubleValue();
} else if (o instanceof String) {
result = Double.parseDouble(((String) o));
} else {
result = 0d;
}
return result;
}
switch の型パターンを使用すると、少ないコードで同じ問題を解決できます。
static double getDoubleUsingSwitch(Object o) {
return switch (o) {
case Integer i -> i.doubleValue();
case Float f -> f.doubleValue();
case String s -> Double.parseDouble(s);
default -> 0d;
};
}
以前のバージョンのJavaでは、セレクター式は少数のタイプに制限されていました。 ただし、タイプパターンでは、スイッチセレクター式は任意のタイプにすることができます。
4.2. ガードパターン
タイプパターンは、特定のタイプに基づいて制御を転送するのに役立ちます。 ただし、場合によっては、渡された値に対して追加のチェックを実行する必要もあります。
たとえば、 if ステートメントを使用して、Stringの長さを確認できます。
static double getDoubleValueUsingIf(Object o) {
return switch (o) {
case String s -> {
if (s.length() > 0) {
yield Double.parseDouble(s);
} else {
yield 0d;
}
}
default -> 0d;
};
}
ガードされたパターンを使用して同じ問題を解決できます。 パターンとブール式の組み合わせを使用します。
static double getDoubleValueUsingGuardedPatterns(Object o) {
return switch (o) {
case String s && s.length() > 0 -> Double.parseDouble(s);
default -> 0d;
};
}
4.3. 括弧で囲まれたパターン
ケースラベルに条件付きロジックがあることに加えて、括弧で囲まれたパターンにより、それらをグループ化することができます。
追加のチェックを実行するときは、ブール式で括弧を使用するだけです。
static double getDoubleValueUsingParenthesizedPatterns(Object o) {
return switch (o) {
case String s && s.length() > 0 && !(s.contains("#") || s.contains("@")) -> Double.parseDouble(s);
default -> 0d;
};
}
括弧を使用することで、追加のif-elseステートメントを回避できます。
5. スイッチの詳細
次に、switchでパターンマッチングを使用する際に考慮すべきいくつかの特定のケースを見てみましょう。
5.1. すべての値をカバー
switch でパターンマッチングを使用する場合、Javaコンパイラはタイプカバレッジをチェックします。
任意のオブジェクトを受け入れるが、Stringの場合のみをカバーするswitch条件の例を考えてみましょう。
static double getDoubleUsingSwitch(Object o) {
return switch (o) {
case String s -> Double.parseDouble(s);
};
}
この例では、次のコンパイルエラーが発生します。
[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[10,16] the switch expression does not cover all possible input values
これは、スイッチ ケースラベルには、セレクター式のタイプを含める必要があるためです。
特定のセレクタータイプの代わりに、defaultケースラベルを適用することもできます。
5.2. サブクラスの注文
スイッチでパターンマッチングのあるサブクラスを使用する場合、ケースの順序が重要になります。
Stringの場合がCharSequenceの場合の後に来る例を考えてみましょう。
static double getDoubleUsingSwitch(Object o) {
return switch (o) {
case CharSequence c -> Double.parseDouble(c.toString());
case String s -> Double.parseDouble(s);
default -> 0d;
};
}
StringはCharSequenceのサブクラスであるため、の例では次のコンパイルエラーが発生します。
[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[12,18] this case label is dominated by a preceding case label
このエラーの背後にある理由は、メソッドに渡された文字列オブジェクトは最初のケース自体で処理されるため、実行が2番目のケースに進む可能性がないためです。
5.3. ヌル値の処理
以前のバージョンのJavaでは、null値をswitchステートメントに渡すたびに、NullPointerExceptionが発生していました。
ただし、タイプパターンを使用すると、nullチェックを個別のケースラベルとして適用できるようになりました。
static double getDoubleUsingSwitch(Object o) {
return switch (o) {
case String s -> Double.parseDouble(s);
case null -> 0d;
default -> 0d;
};
}
null固有のケースラベルがない場合、合計タイプのパターンラベルはnull値と一致します。
static double getDoubleUsingSwitchTotalType(Object o) {
return switch (o) {
case String s -> Double.parseDouble(s);
case Object ob -> 0d;
};
}
switch 式は、nullケースとtotalタイプのケースの両方を持つことができないことに注意してください。
このようなswitchステートメントは、次のコンパイルエラーを引き起こします。
[ERROR] Failed to execute goal ... on project core-java-17: Compilation failure
[ERROR] /D:/Projects/.../HandlingNullValuesUnitTest.java:[14,13] switch has both a total pattern and a default label
最後に、パターンマッチングを使用する switch ステートメントは、NullPointerExceptionをスローする可能性があります。
ただし、これができるのは、switchブロックにnullが一致する大文字と小文字のラベルがない場合のみです。
6. 結論
この記事では、 Java SE 17 のプレビュー機能であるスイッチ式とステートメントのパターンマッチングについて説明しました。ケースラベルでパターンを使用すると、その選択はパターンによって決定されることがわかりました。単純な同等性チェックではなく、マッチング。
例では、switchステートメントに適用できる3つの異なるパターンタイプについて説明しました。 最後に、すべての値のカバー、サブクラスの順序付け、null値の処理など、いくつかの特定のケースを調査しました。
いつものように、完全なソースコードはGitHubでから入手できます。