はじめに
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のバージョンごとに書いてみました。