1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Javaのswitch文、breakとフォールスルーをちゃんと理解するのに時間がかかった

1
Posted at

背景

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

doublelongは使えない。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推進・システム開発を軸に、エンジニアが自律的に動ける環境を大事にしている会社です。

技術的に面白いことをやっていきたい方や、働き方に柔軟さを求めている方は、
よかったら一度のぞいてみてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?