背景
switch文を「なんとなく」使い続けていた時期が長かった。動くコードは書けるのに、なぜbreakが必要なのか、defaultをどこに置くべきか、enumと組み合わせるとどう変わるのか——これらを人に説明できる状態ではなかった。
改めて一度整理する機会があったので、そこで腹落ちしたことを書き残しておく。
breakを忘れると「フォールスルー」が起きる
switch文で最も見落としやすいのが、breakの存在だと思う。
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
// ...
default:
System.out.println("Invalid day");
}
各caseブロックの末尾にbreakがない場合、マッチしたcaseの処理が終わっても止まらず、次のcaseブロックへと処理が流れ続ける。これをフォールスルーと呼ぶ。
意図せずフォールスルーが起きると、想定外の複数の処理が実行されてしまう。逆に「複数のcaseに同じ処理をさせたい」というときは意図的にフォールスルーを使うこともできるが、その場合はコメントで意図を明示しないと、コードを読んだ人が「breakの書き忘れ」と誤解する。
case値に使える型と使えない型
これも曖昧に理解していた部分だった。Javaのswitch文でcase値に使えるのは、次の型に限られる。
-
char,byte,short,int -
Character,Byte,Short,Integer String-
enum型
doubleやlongは使えない。doubleの値をswitch文で扱いたい場合は、intにキャストしてから渡す必要がある。
double value = 3.14;
int intValue = (int) value; // 小数点以下は切り捨てられる
switch (intValue) {
case 3:
System.out.println("The value is 3");
break;
default:
System.out.println("The value is not 3 or 4");
break;
}
また、case値にはnullや変数は指定できない(コンパイルエラーになる)。ただしfinalで宣言した定数であれば指定可能だ。この制約を知らずにハマった経験がある。
defaultの位置は末尾が原則
defaultはswitch文のどこにでも記述できるが、通常は末尾に置く。
int x = 3;
switch (x) {
case 1:
System.out.println("x is 1");
break;
default: // 途中にdefaultを置くこともできるが…
System.out.println("x is not 1");
break;
case 2:
System.out.println("x is 2");
break;
}
この書き方は動作するが、「defaultの後ろにcaseが続く」という構造は読みにくい。特定のケースを優先したい意図があるとしても、末尾にまとめた方が可読性は高い。自分はよほどの理由がない限り末尾固定にしている。
余談だが、JavaのネストしたSwitch文では、内側のbreakだけでは外側のswitch文を抜けられないため、ラベルを使う必要がある。break outer;という書き方がそれで、知らないと「なぜbreakしても抜けないんだ」と混乱する。実際に遭遇したとき、しばらく原因がわからなかった。
enumと組み合わせると格段に読みやすくなる
switch文でリテラルを直接case値に書くと、後から読んだときに「10って何?」という状態になりやすい。enumを使うとこの問題を解消できる。
public enum Color {
RED(10), GREEN(20), BLUE(30);
int value;
Color(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
public static void printColor(Color color) {
switch (color) {
case RED:
System.out.println("The color is red and the value is " + color.getValue());
break;
case GREEN:
System.out.println("The color is green and the value is " + color.getValue());
break;
case BLUE:
System.out.println("The color is blue and the value is " + color.getValue());
break;
default:
System.out.println("Invalid color.");
break;
}
}
enumを使うことで、case値が何を表すのかが一目でわかる。また、定義された列挙子以外の値を渡せないため、型安全性も確保される。値の種類が増えたときもenumに追加するだけで済み、switch側のコードをまるごと見直す必要がない。
可読性・保守性・安全性の三つが同時に改善されるので、複数の固定値を扱う場面ではenum + switchの組み合わせを積極的に選ぶようにしている。
if文とswitch文の選びどころ
「どちらを使えばいいか」という問いに対して、自分の中の基準は以下のようになった。
1つの変数や式に対して複数の固定値を比較するならswitch文の方が見通しがよい。一方、複数の変数を組み合わせた条件や、>・<などの範囲比較が必要な場合はif文を使う。switch文は等値比較(==)しか扱えないため、範囲で分岐したい成績判定のようなケースにはそもそも使えない。
理解が曖昧だった部分と今後
今回整理して一番「ちゃんと知らなかった」と思ったのは、case値に使える型の制限だった。「数値か文字列が使える」程度の認識しかなく、longが使えないことや、変数は指定できないがfinal定数はOKという細かい仕様を意識できていなかった。
まだ深く理解しきれていないのはJava 14以降のswitch式だ。switchが値を返せるようになり、breakの代わりにyieldを使う形に変わった部分がある。従来のswitch文との違いを改めて手を動かして確認したいと思っている。
この記事を書いた人について
株式会社Flexibilityでエンジニアをしています。
DX推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。
技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。
- 会社サイト: https://www.flexi-inc.com/
- Qiita Organization: https://qiita.com/organizations/flexi-inc