JavaのSwitchについて

Java 17と21(LTE)で switch が大きく変わったが、意外と罠がある & うまく使うと便利なので情報をまとめてみた

## Java 17での変更

Java 17 から switch の新しい記法と switch式が導入された
従来の Java の switch文は書く分岐で処理を完了する場合、break の指定が必要だった
あえて break を使わずに条件によって複数の処理を行わせる効率的な書き方もできたが、あまり可読性は高くなく、break の指定漏れによるミスの方が多かった
そのためか、Java 以降に登場したメジャーな言語の複数分岐では基本的にそれぞれの条件が独立しているのが多かった
Java 17 から追加された switch はその流れを汲んでおり、break なしで記載することができる

```
jshell> switch (i) {
   ...>     case 1 -> System.out.println("one");
   ...>     case 2 -> System.out.println("two");
   ...>     default -> System.out.println("else");
   ...> }
else

jshell> switch (day) {
   ...>     case "MONDAY", "TUESDAY", "WEDNESDAY" -> System.out.println("Weekday");
   ...>     case "SATURDAY", "SUNDAY" -> System.out.println("Weekend");
   ...>     default -> System.out.println("Invalid day");
   ...> }

jshell> 
```

また、Java 17 から switchを式として利用可能になった

```
jshell> var str = switch (i) {
   ...>     case 1 -> "one";
   ...>     case 2 -> "two";
   ...>     default -> "else";
   ...> }
str ==> "else"

jshell> var str = switch (i) {
   ...>     case 1: yield "one";
   ...>     case 2: yield "two";
   ...>     default: yield "else";
   ...> }
str ==> "else"

jshell> 
```


### enum

switch式は可読性も優れているが、私が何より気に入っているのは enum を利用する際の静的チェックである
以下のように switch式では網羅性が担保できない場合はビルドエラーになる

```
jshell> enum Fruits {APPLE, BANANA}

jshell> var f = Fruits.APPLE;
f ==> APPLE

jshell> var str = switch (f) {
   ...>     case APPLE -> "apple";
   ...> }
|  エラー:
|  switch式がすべての可能な入力値をカバーしていません
|  var str = switch (f) {
|            ^-----------...

jshell> 
```

この挙動は従来の記法でも同じである

```
jshell> 

jshell> var str = switch (a) {
   ...>     case APPLE: yield "apple";
   ...> }
|  エラー:
|  switch式がすべての可能な入力値をカバーしていません
|  var str = switch (a) {
|            ^----------...

jshell> 
```

ちなみに default を書くと必ず網羅性が担保されてしまうので、switch式では default は書かない方が無難である

尚、以下のように記法の種類に関係なく、switch文ではビルドエラーにならない

```
jshell> switch (a) {
   ...>     case APPLE -> System.out.println("apple");
   ...> }
apple

jshell> switch (a) {
   ...>     case APPLE: System.out.println("apple"); break;
   ...> }
apple

jshell> 
```

…と思っていたのだが、Java 21で少し事情が変わったようだ

## Java 21での変更

### null 指定

Java 21で null の条件を指定できるようになったのだが、この機能を指定すると switch文でも switch式でも網羅性が静的チェックできるようになったようだ
(ちなみに17ではプレビュー扱いだった)

```
jshell> switch (a) {
   ...>     case APPLE: System.out.println("apple");
   ...>     case null: System.out.println("null");
   ...> }
|  エラー:
|  switch文がすべての可能な入力値をカバーしていません
|  switch (a) {
|  ^-----------...

jshell> 
```

これなら基本的にnull指定しておいた方が無難かもしれない

### パターンマッチング

Java 21 で switch のパターンマッチングが利用可能になった

```
jshell> Object o = "str"
o ==> "str"

jshell> switch (o) {
   ...>     case String s: System.out.println("str"); break;
   ...>     case Integer i: System.out.println("int"); break;
   ...>     default: System.out.println("default");
   ...> }
str

jshell> switch (o) {
   ...>     case String s -> System.out.println("str");
   ...>     case Integer i -> System.out.println("int");
   ...>     default -> System.out.println("default");
   ...> }
str

jshell> 
```

Java 17 で instanceof のパターンマッチングが利用可能になって便利だが、この記法を使うと複数の子クラスへの対応がシンプルになる

### sealed class

sealed class(シールクラス)の詳細は割愛するが、それを switch の条件で指定することで enum のように網羅されていない場合はビルドエラーできる

```
jshell> sealed interface Fruits permits Apple, Banana {}
|  次を変更しました: インタフェース Fruits。しかし、 class Apple, and class Bananaが宣言されるまで、参照できません

jshell> final class Apple implements Fruits { }
|  次を作成しました: クラス Apple。しかし、 class Fruitsが宣言されるまで、参照できません

jshell> final class Banana implements Fruits { }
|  次を作成しました: クラス Banana

jshell> final Fruits f = new Apple()
f ==> Apple@1b2c6ec2

jshell> switch (f) {
   ...>     case Apple a: System.out.println(a); break;
   ...>     case Banana b: System.out.println(b); break;
   ...> }
REPL.$JShell$12$Apple@1b2c6ec2

jshell> 
```

こちらは enum と違い、null と同様、switch式だけでなく switch文でもビルドエラーになる

```
jshell> var str = switch (f) {
   ...>     case Apple a -> "apple";
   ...> }
|  エラー:
|  switch式がすべての可能な入力値をカバーしていません
|  var str = switch (f) {
|            ^-----------...

jshell> switch (f) {
   ...>     case Apple a: System.out.println(a); break;
   ...> }
|  エラー:
|  switch文がすべての可能な入力値をカバーしていません
|  switch (f) {
|  ^-----------...

jshell>
```

また、switchのパターンマッチングの場合、when節を追加して更に条件を絞り込める

```
jshell> var str = switch (f) {
   ...>     case Apple a when a != null -> "apple";
   ...>     default -> "default";
   ...> }
str ==> "apple"

jshell> var str = switch (f) {
   ...>     case Apple a when a == null -> "apple";
   ...>     default -> "default";
   ...> }
str ==> "default"

jshell> 
```

## まとめ

- Java 17
  - switch の新構文と switch式が追加された
  - switch式では default を指定しない方が enum の変更時にビルドエラーで気づける
- Java 21
  - null を条件指定できるようになった
  - パターンマッチングを指定できるようになった
  - null指定やsealed classのパターンマッチを使うことで、switch式や switch文で網羅性をチェックできる
  - switchのパターンマッチでwhen節を指定することで条件を更に絞り込める

※ バージョンはプレビューのものを除く