
[Java] 大きく変化したswitchのすべて
こんにちは!
カサレアルでJavaのコースを担当している櫻庭です。
switchは、if文と同様に、処理の分岐に使用する制御構文です。Javaの基本的な構文なので、一度は使ったことがあるはずですね。
ところが、2020年にリリースされたJava 14から五月雨式にswitchの言語仕様が変更され、元々のswitchの書き方とは大きく変わってしまいました。それにともない、switchの使い方も変化してきています。
もちろん、Javaは後方互換性を重視するプログラミング言語ですので、今までの書き方も引き続き使用できます。しかし、新しい書き方を使えば、従来のswitchの問題を解消することができます。
そこで、本エントリーでは、大きく変化したswitchの言語仕様をまとめて、紹介していきましょう。
目次
1. 従来のswitchと、その問題点
switchはJavaの基本的な制御構文なので書き方もご存じだとは思いますが、ここでおさらいしておきましょう。
従来のswitchは、式を評価した結果の値によって処理を分岐させます。どの値と一致するかはcaseラベルを用いて記述します。
switch (式) {
case 定数1:
// 式を評価した結果が定数1と一致した場合に行う処理
...
break;
case 定数2:
// 式を評価した結果が定数2と一致した場合に行う処理
...
break;
...
default:
// 式を評価した結果がいずれの定数とも一致しない場合の処理
}たとえば、int型の変数xの値が0と1、それ以外で出力を変更したいのであれば、次のように書きます。
int x = ...;
switch (x) {
case 0:
IO.println("ZERO");
break;
case 1:
IO.println("ONE");
break;
default:
IO.println("Other");
}値と定数が一致しているかどうかで処理を分岐させる場合、if文よりもswitchの方が記述量も少なく、理解しやすいコードになります。
しかし、使う上での問題や制約もありました。たとえば、以下のようなものです。
- フォールスルー
- switchの処理結果の扱い
- caseラベル
この3つについて、以下にもう少し詳しく説明しましょう。
1.1 フォールスルー
switchで想定していた動作と異なるバグが発生した場合、break文の書き忘れによることが多いのではないでしょうか。
たぶん誰もが一度や二度はbreak文を書き忘れているはず。もちろん、櫻庭も何度かやらかしています。
caseの最後にbreak文を書かなかった場合、そのまま次のcaseに記述した処理が実行されます。これをフォールスルーと呼びます。
たとえば、1, 2, 3, 4の値をとる変数に対し、1と3では”奇数”と表示し、2と4では”偶数”と表示させるには、次のように記述します。
int x = ...;
switch (x) {
case 1:
case 3:
IO.println("奇数");
break;
case 2:
case 4:
IO.println("偶数");
break;
default:
IO.println("想定外");
}このコードはあえてフォールスルーを使用した例です。
問題は、この例のようにフォールスルーをあえて使用した場合と、break文を書き忘れてしまった場合の区別がつかないことです。
コメントが書いてあれば判別がつくかもしれませんが、コードだけではなかなか判別がつきません。
つまり、フォールスルーを使用したコードが、バグなのか仕様なのか分かりにくくなってしまうのです。
1.2 switchの処理結果の扱い
switchのcaseで何らかの処理をした結果を残したい場合、結果を代入する変数が必要になります。
たとえば、三角と四角を表すenumのShapeに対して、幅と高さを使用して面積を計算してみましょう。コードは以下のようになります。
enum Shape { TRIANGLE, SQUARE }
Shape shape = ...; // 形状
double width = ...; // 幅
double height = ...; // 高さ
// 面積
double area;
switch (shape) {
case TRIANGLE:
area = width * height / 2.0;
break;
case SQUARE:
area = width * height;
break;
default:
IO.println("想定外");
}このコードはdefault節の場合、変数areaが初期化されないためコンパイルに失敗します。
しかし、変数shapeは列挙型であり、すべての定数のケースを記述しているので、defaultが実行されることはありません。とはいっても、コンパイラにはそれが分からないため、コンパイルエラーになってしまいます。
コンパイルエラーを解消するにはいくつかの方法がありますが、それぞれ一長一短です。
たとえば、変数areaの宣言時に初期値、たとえば0.0を代入しておく方法です。
この場合、switchのcaseで変数areaに処理結果を再代入します。
変数areaを再代入できるようにするということは、他の箇所でも再代入が可能ということになります。安易な変数への再代入はバグの原因になりがちで、できれば避けたいところです。
変数をfinalで定義することにより不用意な書き換えを防ぐことができますが、この方法では変数areaをfinalで宣言することもできません。
defaultで変数areaに値を代入するという方法もあります。
しかし、このコードでは意味のないdefaultで変数に値を代入するということは、やはり意味のないコードになってしまいます。
であれば、defaultを書かないという方法も可能です。
とはいうものの、安易にdefaultを記述しないのも問題です。defaultの書き忘れによるバグも発生しやすいので、できればdefaultは残しておきたいですね。
このように、switchで処理結果を残すには、いろいろと注意しなければいけない点が多くあるのです。
1.3 caseラベル
caseラベルに記述できるのは、long型以外の整数と、文字列、enumだけです。
if文の条件式の自由さは求めていませんが、もう少し多様な記述ができればと考えたことはないでしょうか。
最近、ドメイン駆動設計が一般的になってきており、値クラスのようにデータの種類を型で表すことも増えてきました。データの種類を型で表した場合、データの種類によって処理を分岐させるのであれば、caseラベルにもデータの型を使いたいところです。
もちろん、instanceof演算子とif文を組み合わせれば記述できます。しかし、if文だとどうしても記述が冗長になってしまいます。switchであれば簡潔に記述できるはずですが、従来のswitchではできませんでした。
これらの問題や制約を克服したのが、新しいswitchです。では、新しいswitchをどのように記述するのか、これから説明していきましょう。
2. 新しいswitchの書き方
新しいswitchは記法が大幅に変わりましたが、それ以外の変更もあります。また、これらの変更の中には、従来の記法と一緒に使用できるものもあります。
まとめて説明してしまうと複雑になってしまうので、変更点ごとに説明していきましょう。
- caseの記法
- 文から式へ
- 型による分類
- 網羅性のチェック
caseの記法の変更以外の変更点は従来の記法でも使用できます。
では、1つ1つ説明していきましょう。
2.1 caseの記法
switchにおけるcaseの記述が、ラムダ式のように->で記述するようになりました。
switch (式) {
case 定数1 -> {
// 式を評価した結果が定数1に一致した場合に行う処理
...
}
case 定数2 -> {
// 式を評価した結果が定数2に一致した場合に行う処理
...
}
...
default -> {
// 式を評価した結果がいずれの定数に一致しない場合に行う処理
}
}アロー(->) の後に波カッコのブロックで処理を記述します。
たとえば、はじめに示したint型の変数xの値が0と1、それ以外で出力を変更するサンプルは、次のようになります。
int x = ...;
switch (x) {
case 0 -> {
IO.println("ZERO");
}
case 1 -> {
IO.println("ONE");
}
default -> {
IO.println("Other");
}
}caseの処理をブロックで示すようになったので、break文は必要なくなりました。ブロックで記述することで、caseの処理の範囲が明確になっています。
従来の記法も引き続き使用できますが、アロー(->)での記法と従来の記法を混在させることはできません。
さらに、処理が1行の場合、波カッコを省略できます。この場合でもラムダ式とは異なり、文の最後のセミコロンは省略しないようにします。
上記の例も処理が1行なので、次のように波カッコを省略できます。
int x = ...;
switch (x) {
case 0 -> IO.println("ZERO");
case 1 -> IO.println("ONE");
default -> IO.println("Other");
}かなりスッキリして、コードの見通しがよくなりました。
caseラベルに定数をカンマ区切りで列挙することで、複数の値を使えるようになりました。
さきほどの、1, 2, 3, 4の値をとる変数に対し、1と3では”奇数”と表示し、2と4では”偶数”と表示させるサンプルは、次のようになります。
int x = ...;
switch (x) {
case 1, 3 -> IO.println("奇数");
case 2, 4 -> IO.println("偶数");
default -> IO.println("想定外");
}この記述であれば、複数の定数と一致する場合でも、フォールスルーとは異なり、意図が明確になりますね。
2.2 文から式へ
従来のswitchは値を返すことができないため、事前に変数を用意して初期化や、caseでの再代入が必要になりました。これは、問題点の節で説明したように、バグの原因にもなりかねません。
これに対し、新しいswitchでは値を返せるようになりました。
ここまで、あえてswitchとあいまいに記述していたのは、従来のswitchは値を返さない文だけであるのに対し、新しいswitchは値を返す式にもなるからです。従来はswitch文であり、現在はswitch式になったということです。
値を返すには、caseブロックでyeildを使用します。
たとえば、先ほどの面積を求めるサンプルをswitch式で記述すると、次のようになります。
// 面積
final double area = switch (polygon) {
case TRIANGLE -> {
yield width * height / 2.0;
}
case SQUARE -> {
yield height * width;
}
};switch式で値を返すことにより、変数areaをfinalで定義できるようになりました。
final変数であれば、不用意に値を書き換えられることもないため、バグの原因にもなりにくくなりますね。
defaultを記述していないことについては、後ほど説明します。
注意しなくてはいけないのが、switch文では文末にセミコロンは不要でしたが、switch式で値を返す場合はセミコロンが必要なことです。
さらに、caseの波カッコを省略できる場合は、yeildも省略できます。
上記のサンプルも処理が1行で、波カッコが省略できるので、次に示すようにyeildも省略できます。
// 面積
final double area = switch (shape) {
case TRIANGLE -> width * height / 2.0;
case SQUARE -> height * width;
};yieldは、アローを使用しない古い記述でも使用できます。
yeildは値を返すためにswitch式を抜けるので、yeildの後にbreakを記述する必要はありません。
// 面積
final double area = switch (shape) {
case TRIANGLE:
yeild width * height / 2.0;
case SQUARE:
yeild width * height;
};switchを式として使うことにより、変数の初期化忘れや、再代入による不用意な値の書き換えを防ぐことができます。分岐した処理で何らかの値を返す場合は、積極的にswitch式を使っていきましょう。
2.3 型による分類
switchの問題のところで説明したように、caseラベルに記述できるのは、long型以外の整数と、文字列、enumだけです。
これに対し、新しいswitch式ではラベルに型が使えるようになりました。つまり、caseラベルにクラスやインタフェースが使えるようになったということです。
これは、型によるパターンマッチング、もしくは単にパターンマッチングと呼ばれている機能です。
caseラベルに型を使用する場合、型名の後にローカル変数名を記述します。このローカル変数には、switchの式を評価した結果が代入され、caseのブロック内で使えます。
パターンマッチングを使わない場合、switchの評価に使用する式の値を使用するには、型ごとにキャストが必要になります。しかし、パターンマッチングを使用する場合、自動的にキャストしてくれます。
switch (式) {
case 型1 変数1 -> {
// 式を評価した結果が型1に一致した場合に行う処理
// 式を評価した結果は変数1に代入され、ブロック内で使用できる
...
}
case 型2 変数2 -> {
// 式を評価した結果が型2に一致した場合に行う処理
// 式を評価した結果は変数2に代入され、ブロック内で使用できる
...
}
...
default -> {
// 式を評価した結果がいずれの型にも一致しない場合に行う処理
}
}たとえば、会員情報を扱うMemberインタフェースがあったとしましょう。それに対し、一般会員、シルバー会員、ゴールド会員を表すStardardMemberクラス、SilverMemberクラス、GoldMemberクラスを定義したとします。
この時に、会員のランクごとに異なる処理を行うには、次のように記述します。
Member member = new ...;
switch (member) {
case StandardMember standard
-> IO.println("Standard: " + standard.getName());
case SilverMember silver
-> IO.println("Silver: " + silver.getName());
case GoldMember gold
-> IO.println("Gold: " + gold.getName());
default -> IO.println("Other: " + member.getName());
}もちろん、従来のcaseの記法でも型を使用できます。
上記のコードを従来の記法で書き直すと、次のようになります。
switch (member) {
case StandardMember standard:
IO.println("Standard: " + standard.getName());
break;
case SilverMember silver:
IO.println("Silver: " + silver.getName());
break;
case GoldMember gold:
IO.println("Gold: " + gold.getName());
break;
default: IO.println("Other: " + member.getName());
}2.3.1 nullを扱う
上記のコードではmemberがnullの場合、NullPointerException例外が発生してしまいます。
これを防ぐために、caseのラベルにnullが使えるようになりました。
switch (member) {
case StandardMember standard
-> IO.println("Standard: " + standard.getName());
case SilverMember silver
-> IO.println("Silver: " + silver.getName());
case GoldMember gold
-> IO.println("Gold: " + gold.getName());
case null -> IO.println("member is null");
default -> IO.println("Other");
}もちろん、switchの前にif文でnullチェックを行うことも可能です。しかし、switchでnullもcaseとして扱える方がコードが簡潔になりますね。
2.3.2 型に条件を追加する
型の判別に加えて、何らかの条件をcaseに記述することもできるようになりました。この条件はガード節として扱えます。
ガード節は型のラベルの後にwhenを用いて記述します。
たとえば、上記の会員によるswitchにおいて、一般会員だけ年齢で処理を分けてみましょう。年齢はMemberインタフェースのgetAge()メソッドで返すようにします。
これをガード節を使用して記述してみましょう。
switch (member) {
case StandardMember young when young.getAge() >= 20
-> IO.println("Young Standard: " + young.getName());
case StandardMember standard
-> IO.println("Adult Standard: " + standard.getName());
case SilverMember silver
-> IO.println("Silver: " + silver.getName());
case GoldMember gold
-> IO.println("Gold: " + gold.getName());
case null -> IO.println("member is null");
default -> IO.println("Other: " + member.getName());
}同じStandardMemberクラスに合致した場合でも、ガード節により処理を分けることができました。
2.3.3 レコードパターン
レコードクラスを使ってパターンマッチングする場合、レコードコンポーネントを直接ローカル変数に代入できます。これをレコードパターンと呼びます。
説明よりもコードを見ていただいた方が早いですね。
例として、立体を表すFigureインタフェースを定義してみましょう。そして、Figureインタフェースを実装したレコードクラスとして球 Sphere、直方体 Cuboid、立方体 Cubeを定義します。
interface Figure {}
record Sphere(double radius) implements Figure {}
record Cuboid(double width, double height, double depth) implements Figure {}
record Cube(double edge) implements Figure {}これらのレコードクラスに対して、体積を求めるcalcVolumeメソッドを作ってみましょう。
double calcVolume(Figure shape) {
return switch (shape) {
case Sphere(double r) -> Math.PI * r * r * r * 4.0 / 3.0;
case Cuboid(double w, double h, double d) -> w * h * d;
case Cube(double e) -> e * e * e;
default -> throw new IllegalArgumentException();
};
}レコードクラスのオブジェクトをローカル変数に代入するのではなく、Shpereレコードクラスであれば、レコードコンポーネントのradiusの値をローカル変数のrに代入しています。
レコードコンポーネントが複数ある場合、レコードコンポーネントを定義した並び順とローカル変数の並び順は同じになります。このため、Cuboidレコードクラスであれば、変数wにwidthレコードコンポーネント、変数hにheightレコードコンポーネント、変数dにdepthレコードコンポーネントの値が代入されます。
また、複数のレコードコンポーネントのうち一部だけ使用したいという場合でも、すべてのレコードコンポーネントに対応する変数を列挙する必要があります。
レコードクラスのアクセッサーメソッドを使わずにすむため、コードが簡潔に表記できますね。
2.3.4 パターンマッチングのローカル変数が不要の場合
次に示すコードのように、型の違いによって表示を変化させるだけでローカル変数を使わない場合があります。
switch (shape) {
case Sphere sphere -> IO.println("球");
case Cuboid cuboid -> IO.println("直方体");
case Cube cube -> IO.println("立方体");
default -> IO.println("その他");
};このような場合、使用しないローカル変数を_ (アンダースコア)で置き換えることができます。
switch (shape) {
case Sphere _ -> IO.println("球");
case Cuboid _ -> IO.println("直方体");
case Cube _ -> IO.println("立方体");
default -> IO.println("その他");
};上記のコードではすべてのローカル変数をアンダースコアで置き換えましたが、一部のローカル変数だけを置き換えることも可能です。
また、レコードパターンにおいても、使用していないレコードコンポーネントだけをアンダースコアで記述することができます。
2.4 網羅性のチェック
網羅性のチェックは言語仕様の変更ではありません。書き方が変わるということではなく、コンパイラーが強力になってバグが出にくくなったということです。また、不必要な記述も減らすことができます。
たとえば、「1.2 switchの処理結果の扱い」で使用した面積を求めるswitch文は本来はdefaultは必要ありません。そこで次のようにdefaultを省略して記述したとします。
double area = switch (shape) {
case TRIANGLE -> width * height / 2.0;
case SQUARE -> width * height;
};この時に、SQUAREのcaseを書き忘れてしまったらどうなるでしょう。つまり、以下のようなコードです。
double area = switch (shape) {
case TRIANGLE -> width * height / 2.0;
// case SQUARE -> width * height;
};これをコンパイルすると、コンパイルエラーが発生します。
> javac Main.java
Main.java:8: エラー: switch式がすべての可能な入力値をカバーしていません
double area = switch (shape) {
^
エラー1個つまり、switch式のcaseでとりうる値や型をチェックし、不足していればコンパイルエラーが発生するようになったのです。
これが網羅性のチェックということです。
同じように、先ほどの体積を求めるサンプルでFigureインタフェースをシールドインタフェースに変更した場合はどうなるでしょう。
sealed interface Figure
permits Shpere, Cuboid, Cube {}Figureインタフェースをシールドインタフェースに変更することで、実装クラスをSphere、Cuboid、Cubeだけに制限できます。このことにより、switch式のcaseラベルに指定する型は限定できるため、defaultが不必要になります。
ここでも、caseラベルにCubeクラスを指定するのを忘れた場合を考えてみましょう。
double calcVolume(Figure shape) {
return switch (shape) {
case Sphere(double r) -> Math.PI * r * r * r * 4.0 / 3.0;
case Cuboid(double w, double h, double d) -> w * h * d;
// case Cube(double e) -> e * e * e;
};
}これをコンパイルすると、同様にコンパイルエラーが発生します。
> javac Main.java
Main.java:11: エラー: switch式がすべての可能な入力値をカバーしていません
return switch (shape) {
^
エラー1個ここで示したように、網羅性のチェックが行われるのはenumとシールドクラス/インタフェースに限定されています。
予防的にdefaultを書いている開発者の方も多いと思いますが、網羅性がチェックできるのであればdefaultも不要です。
ただし、値を返さないswitch文では、網羅性チェックが行われないことに注意が必要です。値を返さないのであれば、網羅していなくても問題ないということなのだと思います。
switch文では予防的にdefaultを書くのを忘れないようにしましょう。
3. まとめ
Java 11などの古いJavaを使われている方からしたら、現在のswitchはまるで違うもののように見えるかもしれません。
新しいswitchを使うことでプログラムの品質を向上させることができますが、既存のコードを書き換えることはなかなか難しいですね。
ラムダ式と異なり、switchは古い記法もサポートしているため、一気に新しい形式に書き換えるのではなく、徐々にリファクタリングすることができるはずです。
そして、switchの言語仕様が変わったことにより、設計の手法も変わってきています。
たとえば、今まではデザインパターンのテンプレートパターンで記述していた処理も、switch式で記述可能です。テンプレートパターンだと処理が複数のクラスに分散してしまいますが、switch式であればswitch式内に閉じることができ、コードの見通しもよくなります。
本エントリーで示した通りswitchは大きく変わりましたが、まだその変化は終わりではありません。
現在提案されている変更点は次の2つです。
- パターンマッチングにプリミティブ型を使用できるようにする
- caseラベルに例外を記述できるようにする
後者の例外をswitchで扱う方はまだまだ先の話ですが、前者のプリミティブ型を使用できるようにする変更はJava 27でプレビューとして導入されています。
このまま進めば、次のLTSであるJava 29には導入されそうです。
このプリミティブ型を使用できるようにする変更については、また改めて紹介する予定です。
参考 Java 26におけるswitchに関連する言語仕様(JLS: Java Language Specification)
- JLS 6.3.1.6 switch Expression
- JLS 6.3.2.6 switch Statement
- JLS 6.3.3 Scope for Pattern Variables in case Labels
- JLS 14.11 The switch statement
- JLS 14.30 Patterns
- JLS 15.28 switch Expressions
- JLS 16.1.6 switch Expressions
- JLS 16.2.9 switch Statements
- JLS 18.5.5 Record Pattern Type Inference
カサレアルでは、Javaを学ぶ方に向けて「Javaプログラミング入門」や「Javaプログラミング基礎」などのコースを開催しています。
Javaに関するコースの詳細や開催日程に関しては以下のリンクをご覧ください。
バックエンド開発を学ぶ研修一覧
https://www.casareal.co.jp/ls/service/openseminar/search/backend