【Java】switch式とswitchパターンマッチング

# はじめに

[Java Advent Calendar 24日目](https://qiita.com/advent-calendar/2023/java)の記事です。よろしくお願いします。

「instanceofのパターンマッチング」[（この記事）](https://qiita.com/arubaito/items/0d7bbb9984ece4fa268a)について調べている時に`switch`"**式**"があることを初めて知ったので、Javaのバージョンごとに`switch`をどう使えるかざっくりと調べてみました。

switchパターンマッチングが知りたい方は[Java 17](#java-17)まで飛んでください。

# Java 12
Preview版として`switch`式が導入される。

https://openjdk.org/jeps/325

そもそもは、`switch`文のフォールスルーが厄介だったっぽい。
```java
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}
```
このようなコードって分かりにくいしバグの温床ですよねと。

そこで、`case L ->`の構文（Arrow label）が提案されてます。
```java
switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}
```
これは`case`のラベルが一致した時にラベルの右側のコード**だけ**が実行されます。

ついでに`switch`文を拡張して**式**としても拡張します。
```java
T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};
```
式なので、:arrow_down:のように書けたりもします。
```java
static void howMany(int k) {
    System.out.println(
        switch (k) {
            case  1 -> "one"
            case  2 -> "two"
            default -> "many"
        }
    );
}
```
ここで、`switch`式の`case`は網羅的でなければいけません。

`default`句があれば問題ありませんが、網羅していないとコンパイルエラーになります。
```java
public class SwitchSample {

	private enum WEEK {
 
		MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
	}

	public static void main(String[] args) {

		WEEK day = WEEK.MONDAY;

        // 網羅しているので問題なし
		int numLetters = switch (day) {
			case MONDAY, FRIDAY, SUNDAY -> 6;
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
		};

        // 網羅していないのでコンパイルエラー
		int numLetters1 = switch (day) {
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
		};

        // default句で網羅しているので問題なし
		int numLetters1 = switch (day) {
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
			default -> throw new IllegalStateException("Wat: " + day);
		};
	}

}
```

`->`と`switch`式2つを導入したおかげで、やりたいことが分かりにくかった従来のコードもスッキリです。
```従来.java
int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}
```
```今後.java
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
    default                     -> throw new IllegalStateException("Wat: " + day);
};
```

式を複数書くようなブロックが必要な場合は、`break`の後に書いた値が戻り値となります。

```java
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        break result; // breakで値を返却
    }
};
```

## まとめると
- `->`が導入されて、フォールスルーが無いように書けるようになった。
- `switch`式が導入された。
    - ブロックを使いたい場合は、`break 値`で値を返せる。

# Java 13
まだPreview版ですが、`switch`式に変更がありました。

https://openjdk.org/jeps/354

`switch`式でブロックを使う場合、Java 12のPreview版では`break 値`で値を返却していましたが、代わりに`yield`文で返すようになりました。

```java
int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result; // yieldで値を返却
    }
};
```

# Java 14
LTSであるJava 14で`switch`式が(Preview版ではなく)導入されました。

https://openjdk.org/jeps/361

Java 13までに提案されていたPreview版の`switch`式から変更はありません。
要は、、、
- Arrow label`->`が導入されました。
- `switch`式が導入されました。
    - ラベルを網羅していないとコンパイルエラーが発生します。
- `yield`文が導入されました。

です。

# Java 17
Preview版でswitchパターンマッチングが導入されました。

https://openjdk.org/jeps/406

switchパターンマッチングとは簡単に言うと、[Java 16で導入されたinstanceofパターンマッチング](https://qiita.com/arubaito/items/0d7bbb9984ece4fa268a)の`switch`版です。

Java 16で導入したinstanceofパターンマッチングを使って:arrow_down:のように書く内容を、、、
```従来.java
static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}
```
:arrow_down:のように書けてスッキリします。
```今後.java
static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}
```
この拡張に合わせて`switch`がいろいろ拡張されています。

## 使える型の拡張

そもそもは、`switch`の`case`に使える型は以下のいずれかでした。
- 整数のプリミティブ型(`char`, `byte`, `short`, `int`)
- 上のラッパ(`Character`, `Byte`, `Short`, `Integer`)
- `String`型
- `enum`型

これを拡張して、整数のプリミティブ型か任意の参照型を使えるようになります。

## nullの扱い

`null`の評価について、従来は外側で行う必要がありました。
```java
static void testFooBar(String s) {
    if (s == null) {
        System.out.println("oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}
```

が、`case null`を書けるようになり、`null`の評価ができるようになります。
```java
static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}
```
いままでだと`s`が`null`の時は、`NullPointerException`が発生してました。

### 補足
後方互換性を維持するために`switch`の動きは以下のようになります。
- `default`は今まで通り`null`に該当しません。
- セレクタ（`switch(s)`の`s`の部分）が`null`と評価された場合、`case null`か`case Object`のような総パターンの`case **`に該当するとみなして`case`を探索します。そのような`case`がない場合は、`NullPointerException`が発生します。
- セレクタが`null`でないと評価された場合、該当する`case **`を探します。

## guarded patternの追加

また、`switch`のパターンを洗練する取り組みも行われました。

先にguarded patternとやらの導入前のコードから書きます。

これは`Shape`の変数`s`が「`Triangle`型でかつ、面積`calculateArea`が`100`より大きい場合」とそれ以外で分岐する`switch`文です。

```java
class Shape {}
class Rectangle extends Shape {}
class Triangle  extends Shape { int calculateArea() { ... } }

static void testTriangle(Shape s) {
    switch (s) {
        case null:
            break;
        case Triangle t:
            if (t.calculateArea() > 100) {
                System.out.println("Large triangle");
                break;
            }
        default:
            System.out.println("A shape, possibly a small triangle");
    }
}
```
`if`ブロック内に`break`をおいてフォールスルーを使っていることに注目です。
ややこしいですね。


これに対して、「パターン`p`をboolean式`b`で絞り込む」`p && b`が導入されました（これを**guarded pattern**と言います）。

guarded patternを使うことで:arrow_down:のように書き直せます。
```java
static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (t.calculateArea() > 100) ->
            System.out.println("Large triangle");
        default ->
            System.out.println("A shape, possibly a small triangle");
    }
}
```

## パターンの優位性
パターンマッチングを導入したことにより、`case **`の`**`に複数該当する可能性が出てきます。

例えば:arrow_down:のコードを見てみましょう。
```java
static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s -> 
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}
```
`String`は`CharSequence`の実装クラスなので、`case String`に該当するならば`case CharSequence`に該当します。
`case`は上から評価されていくため、:arrow_up:のコードだと`case String`にマッチすることができません。
`case String`を先に書かないとコンパイルエラーとなります。

また、guarded patternですが、例えば`case String s && s.length() > 0`は`case String`に含まれるので、`case String s && s.length() > 0`を先にする必要があります。

要は到達不能な`case`を作っちゃダメということですかね。
`try-catch`の`Exception`をキャッチする時にサブクラスの方から`catch`しないとコンパイルエラーになるやつと考え方は一緒だと思います。

## その他
その他にも、JEPでは「パターンの網羅性」と「パターン変数のスコープ」について提言されています。
ざっくりいうと、
- 「パターンの網羅性」
    - `case`で取りうるパターンを完全に網羅するようにしなくちゃダメだよ
    - だいたい`default`句をつければいいよ
- 「パターン変数のスコープ」
    - `case`の後がパターン変数のスコープだよ
    - `switch`文を使う時にフォールスルーで変数が初期化されない可能性がある時はコンパイルエラーにするよ

ってことを言っています。
パターンの網羅性についてはsealedなクラスと相性がいいんですね。

# Java 18
さてさて、Preview版ですが引き続きswitchのパターンマッチングが導入されてます。

https://openjdk.org/jeps/420

## 変更点

Java 17からの主な変更点は以下になります。
- パターンの優位性チェックに定数のチェックが入った
    - 例えば、`case Integer i`は`case 42`を含みます
    - 例えば、`case E e`は`case A`を含みます（`A`は`enum E`の定数）
- sealedな階層構造を持つ`switch`ブロックの網羅性チェックがより正確になった

2つ目が分かりにくいですね。
例えば、以下のような`sealed`な階層構造があったとします。
```java
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
```
この時に、以下のような`switch`ブロックを考えると、
```java
static int testGenericSealedExhaustive(I<Integer> i) {
    return switch (i) {  
        case B<Integer> bi -> 42;
    }
}
```
変数`i`の型`I<T>`が`I<Integer>`となっているので、網羅性チェックでは`I<Y>`を実装している`B<Y>`のみをコンパイラはチェックします。`A<X>`は`I<String>`を実装しているので不要です。

# Java 19

引き続きPreview版です。

https://openjdk.org/jeps/427

## 変更点
Java 18からの変更点は以下になります。
- guarded pattern（`p && b`のやつ）が`when`に変更
    - `case p when b`と書くようになります 

# Java 20

引き続きPreview版です。

https://openjdk.org/jeps/433

## 変更点
Java 19からの主な変更点は以下の通りです。
- 列挙型が網羅されずに実行時に`case **`に当てはまらなかった場合にスローされる例外が、`IncompatibleClassChangeError`から`MatchException`に変更
- ジェネリクスを使ったrecord patternに対する型引数の推論が`switch`式と`switch`文もサポートするようになった

2番目のrecordに対する話ですが、:arrow_down:が関係してます。

https://openjdk.org/jeps/440

例えば、:arrow_down:のようにinstanceofパターンマッチングでRecordのパターン変数`Point p`を抽出してます。
```java
record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}
```
このコードでは、`p`はフィールド変数`x`と`y`にアクセスするために抽出してるだけです。

record patternとは、`Point(int x, int y)`のように書くことでフィールド変数を初期化して抽出してくれる仕組みのことです。
```java
static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}
```

で、本題に戻って`switch`でも:arrow_down:のように書けます。
```java
record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
            ... // Inferred record Pattern MyPair<String,Integer>(var f, var s)
        ...
    }
}
```
`case MyPair(var f, var s)`としていますが、`pair`の型が`MyPair<String, Integer>`なので推測してくれます。

# Java 21
LTSのJava 21で正式にリリースされました。

https://openjdk.org/jeps/441

## 変更点
Java 20からの主な変更点は以下になります。

- 修飾された`enum`定数を`case **`の`**`に使うことを許可する

どういうことかというと、、
:arrow_down:今までは`WEEK.MONDAY`のように修飾した`enum`定数を`case`で書くことが許されておらずコンパイルエラーが出てしまっていました。
```java
public class SwitchSample {

	private enum WEEK {
		MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
	}

	public static void main(String[] args) {

		WEEK day = WEEK.MONDAY;

		int numLetters = switch (day) {
			case WEEK.MONDAY, WEEK.FRIDAY, WEEK.SUNDAY -> 6; // コンパイルエラー
			case TUESDAY -> 7;
			case THURSDAY, SATURDAY -> 8;
			case WEDNESDAY -> 9;
			default -> throw new IllegalStateException("Wat: " + day);
		};
	}
}
```


:::note alert
`The qualified case label SwitchSample.WEEK.MONDAY must be replaced with the unqualified enum constant MONDAY`
:::
Java 21以降では修飾を可にするということです。

これは`enum`型の`switch`を使う時に冗長性の排除につながるらしいです。
:arrow_down:こう書かなければならないコードが、、
```java
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}
```
:arrow_down:こう書ける
```java
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}
```





# おわりに
平仄があってないのはすいません。
明らかな誤りや補足があれば、コメントいただけると嬉しいです。

結論だけ書いてもよかったんですが、流れがあった方が個人的に頭に入りやすいのでJavaのバージョンごとに書いてみました。