はじめに
Java Advent Calendar 24日目の記事です。よろしくお願いします。
「instanceofのパターンマッチング」(この記事)について調べている時にswitch
"式"があることを初めて知ったので、Javaのバージョンごとにswitch
をどう使えるかざっくりと調べてみました。
switchパターンマッチングが知りたい方はJava 17まで飛んでください。
Java 12
Preview版としてswitch
式が導入される。
そもそもは、switch
文のフォールスルーが厄介だったっぽい。
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)が提案されてます。
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
文を拡張して式としても拡張します。
T result = switch (arg) {
case L1 -> e1;
case L2 -> e2;
default -> e3;
};
式なので、のように書けたりもします。
static void howMany(int k) {
System.out.println(
switch (k) {
case 1 -> "one"
case 2 -> "two"
default -> "many"
}
);
}
ここで、switch
式のcase
は網羅的でなければいけません。
default
句があれば問題ありませんが、網羅していないとコンパイルエラーになります。
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つを導入したおかげで、やりたいことが分かりにくかった従来のコードもスッキリです。
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);
}
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
の後に書いた値が戻り値となります。
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
式に変更がありました。
switch
式でブロックを使う場合、Java 12のPreview版ではbreak 値
で値を返却していましたが、代わりにyield
文で返すようになりました。
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版ではなく)導入されました。
Java 13までに提案されていたPreview版のswitch
式から変更はありません。
要は、、、
- Arrow label
->
が導入されました。 -
switch
式が導入されました。- ラベルを網羅していないとコンパイルエラーが発生します。
-
yield
文が導入されました。
です。
Java 17
Preview版でswitchパターンマッチングが導入されました。
switchパターンマッチングとは簡単に言うと、Java 16で導入されたinstanceofパターンマッチングのswitch
版です。
Java 16で導入したinstanceofパターンマッチングを使ってのように書く内容を、、、
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;
}
のように書けてスッキリします。
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
の評価について、従来は外側で行う必要がありました。
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
の評価ができるようになります。
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
文です。
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を使うことでのように書き直せます。
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 **
の**
に複数該当する可能性が出てきます。
例えばのコードを見てみましょう。
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
は上から評価されていくため、のコードだと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のパターンマッチングが導入されてます。
変更点
Java 17からの主な変更点は以下になります。
- パターンの優位性チェックに定数のチェックが入った
- 例えば、
case Integer i
はcase 42
を含みます - 例えば、
case E e
はcase A
を含みます(A
はenum E
の定数)
- 例えば、
- sealedな階層構造を持つ
switch
ブロックの網羅性チェックがより正確になった
2つ目が分かりにくいですね。
例えば、以下のようなsealed
な階層構造があったとします。
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
この時に、以下のようなswitch
ブロックを考えると、
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版です。
変更点
Java 18からの変更点は以下になります。
- guarded pattern(
p && b
のやつ)がwhen
に変更-
case p when b
と書くようになります
-
Java 20
引き続きPreview版です。
変更点
Java 19からの主な変更点は以下の通りです。
- 列挙型が網羅されずに実行時に
case **
に当てはまらなかった場合にスローされる例外が、IncompatibleClassChangeError
からMatchException
に変更 - ジェネリクスを使ったrecord patternに対する型引数の推論が
switch
式とswitch
文もサポートするようになった
2番目のrecordに対する話ですが、が関係してます。
例えば、のようにinstanceofパターンマッチングでRecordのパターン変数Point p
を抽出してます。
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)
のように書くことでフィールド変数を初期化して抽出してくれる仕組みのことです。
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
で、本題に戻ってswitch
でものように書けます。
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で正式にリリースされました。
変更点
Java 20からの主な変更点は以下になります。
- 修飾された
enum
定数をcase **
の**
に使うことを許可する
どういうことかというと、、
今まではWEEK.MONDAY
のように修飾したenum
定数をcase
で書くことが許されておらずコンパイルエラーが出てしまっていました。
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);
};
}
}
The qualified case label SwitchSample.WEEK.MONDAY must be replaced with the unqualified enum constant MONDAY
Java 21以降では修飾を可にするということです。
これはenum
型のswitch
を使う時に冗長性の排除につながるらしいです。
こう書かなければならないコードが、、
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");
}
}
}
こう書ける
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のバージョンごとに書いてみました。