Xで、Javaのswitchについてこんな議論を見かけました。
-
breakを書き忘れると次のcaseに処理が流れる - 意図的なフォールスルーなのか、単なる書き忘れなのか分かりにくい
これを見て気になったのが、「なぜbreakを書かないと次のcaseへ処理が流れる仕様になったのか」という点です。
従来のJavaのswitchでは、breakを書かなければ次のcaseへ処理が続きます。
switch (status) {
case ACTIVE:
start();
break;
case STOPPED:
stop();
break;
}
ACTIVE側のbreakを外せば、start()に続いてstop()まで実行されます。いわゆる フォールスルー(fall-through) です。
現在のJavaにはフォールスルーしないcase ... ->があります。それを見ると、なおさら「なぜ昔のswitchはこの仕様になったのだろう」と気になります。
調べてみると、ポイントは最初からswitchとbreakがセットで設計されたわけではないことにありました。switchの歴史をBCPL → B → C → Javaの順に追っていきます。
この記事では、従来形式のswitchとフォールスルーの成り立ちを中心に扱います。後半では、現在のJavaでcase ... ->を使う方法や、ifなど別の書き方への置き換え、静的解析についても触れます。
BCPL:switchの原型は「実行開始位置を選ぶ」多方向分岐だった
CやBの祖先にあたるBCPLには、1967年の時点ですでにswitchonという構文がありました。
BCPLのswitchonは、式の値と一致するcaseを探し、そのラベルから実行を再開する仕組みです。現在の感覚だとswitchを「条件に一致したcaseの処理だけを実行する構文」と捉えがちですが、元々の考え方はそうではありません。
switchは値に応じて「どこから実行を始めるか」を選び、そこから先は通常どおり処理を続ける多方向分岐でした。caseに制御を移したあと、そのまま後続の文を実行すれば、結果として次のcaseにも処理が流れます。現在いうフォールスルーは、後から追加された特殊な機能ではなく、この多方向分岐という設計そのものの帰結です。
BCPLにはgotoとラベルもあったので、switchonで実行開始位置を選んだあと、必要ならgotoで別の位置へ制御を移すこともできました。
1967年のBCPLにはbreak自体は存在していましたが、対象はwhileやforなどのループで、switchonから抜けるためのbreakではありません。後のBCPLに登場するendcaseも、この時点ではまだありませんでした。
つまり、最初から
case A:
...
break;
という形を前提にswitchが設計されたわけではないということです。
B言語:switchから抜ける操作をbreakとして専用化した
BCPLから、Ken Thompsonが1969〜1970年ごろにB言語を作ります。switchの基本的な考え方はBにも引き継がれました。
Bの公式チュートリアルでは、switchを multi-way branch(多方向分岐) と説明しています。
A Tutorial Introduction to the Language B
Bのマニュアルには、次のような例があります。
switch(x) {
case 'a':
y = 1;
case 'b':
z = 2;
}
この場合、x == 'a'ならy = 1とz = 2が、x == 'b'ならz = 2のみが実行され、それ以外なら何もしません。
User's Reference to B on MH-TSS
ここでも、一致したcaseから実行が始まり、その後は通常どおり後続の文へ進みます。
Bの公式チュートリアルには、switch内部の別ラベルへgotoする例も登場します。
loop:
switch (c = getchar()) {
pr:
case 'p':
case 'P':
print();
goto loop;
case 's':
case 'S':
subs();
goto pr;
/* ... */
}
sまたはSの場合はsubs()を実行したあと、同じswitch内にあるpr:へ移り、その先のprint()を実行します。
現在の感覚ではかなり自由な制御に見えますが、当時のswitchが「caseごとに独立したブロックを選ぶ」というより、複数の実行開始位置を持ち、その後は通常の制御フローとして扱う構文だったことがよく分かる例です。
ただ、こうしたgotoやラベルを多用する書き方が積極的に推奨されていたわけではありません。同じBのチュートリアルでは、良いプログラミングではラベルを少なくするべきだとも説明されています。
自由に制御できること自体は当時の言語仕様として自然だった一方、その自由さが読みづらさにつながることも、この時点ですでに意識されていたと考えられます。
一方で、あるcaseの処理を終えたあと、そのまま次へ進まずswitchの外へ出たい場面もあります。
これは、BCPLの時点から存在していたgotoとラベルでも表現できます。
switch (x) {
case 1:
foo();
goto switch_end;
case 2:
bar();
goto switch_end;
}
switch_end:
goto switch_end;に到達すると、switch_end:というラベルの位置へ制御が移ります。
Bでは、こうした「このswitchから抜ける」という定型的な制御移動に、既存のbreakを使えるようになりました。
switch (x) {
case 1:
foo();
break;
case 2:
bar();
break;
}
Bのチュートリアルでは、breakはswitchの直後へ制御を移す文として説明されており、不要なラベルを避けるのに便利だとされています。単なる文字数の短縮というより、汎用的なgoto + labelで書ける「この制御構造から抜ける」という操作を専用の構文にしたものと考えると分かりやすいです。
ここで、現在まで続く形ができました。
- 一致した
caseから実行を始める - 何もしなければ後続の文へ進む
-
switchから抜けたい場合はbreakする
現在の感覚では「breakを書き忘れるとフォールスルーする」と捉えますが、歴史的には逆です。元々は一致した位置から後続の文を実行する構文で、そこから途中で抜ける操作がbreakとして専用化されたというのが実際の順序です。
BCPLでは別の方法が追加された
一方、BCPL側では後になってswitchonから抜けるためのendcaseが追加されました。
Dennis Ritchieは『The Development of the C Language』でこの経緯を説明しています。
Dennis M. Ritchie - The Development of the C Language
Ritchieによると、彼らが1960年代に学んだ初期のBCPLにはendcaseがありませんでした。その後、BCPL側ではendcase、B/C側では既存のbreakという別々の形へ発展します。
Ritchieはこの違いを "divergent evolution"(分岐した進化) と説明しています。
「BCPLでendcaseが作られ、それをBがbreakに置き換えた」という順序ではありません。Bの元になった初期BCPLにはendcaseがなく、その後BCPL側ではendcase、B/C側ではbreakを使う形へそれぞれ発展しました。
最初から「各caseの末尾にbreakを書く」という構文を設計したわけではなく、先に「値によって実行開始位置を選ぶ」switchがあり、後から「途中でこの制御構造を抜ける」という操作が専用化された——これが現在のswitchがbreakを必要とする形になった経緯です。
C:Bのswitchとbreakを引き継ぐ
Bはその後、Dennis RitchieによってCへ発展します。
Cでも、Bのswitchとbreakの考え方がそのまま引き継がれました。
switch (x) {
case 1:
foo();
break;
case 2:
bar();
break;
}
breakを書けばswitchから抜け、書かなければ後続の文をそのまま実行する——この2通りの書き方がここで定着します。柔軟な仕組みで、複数のcaseで処理を共有したり、意図的に後続処理まで実行したりできます。
一方で、問題も昔から認識されていました。KernighanとRitchieの『The C Programming Language』では、フォールスルーには複数のcaseで処理を共有できる利点がある一方、コード変更に弱く、意図的に使う場合も慎重に扱うべきものとして説明されています。
case A:
foo();
case B:
bar();
break;
このコードを見て、AからBへ意図的に処理を続けているのか、breakを書き忘れただけなのか——という曖昧さは、後から突然生まれた問題ではありません。Cの時代から、フォールスルーは便利さと壊れやすさの両方を持つものとして認識されていました。
The C Programming Language, 2nd Edition
なぜ、現在ではより「バグりそう」に見えるのか
では、昔から危うさが認識されていたのに、なぜ現在のswitchを見ると特に不自然に感じるのでしょうか。
理由の一つは、switchの捉え方が変わってきたことにあると思います。
初期のswitchは、値によって実行開始位置を選び、その後の制御はプログラマに任せる多方向分岐でした。一致したcaseから先は通常の制御フローに任せるため、次のcaseへそのまま進む、gotoで別の位置へ移る、breakでswitchから抜ける、といった制御をプログラマが選べます。
一方、現在のJavaでswitchを見ると、次のようにcaseごとの独立した分岐として読むことが多いと思います。
switch (status) {
case ACTIVE:
start();
break;
case STOPPED:
stop();
break;
}
この読み方では、「ACTIVEならACTIVEの処理を実行して終わる」「STOPPEDならSTOPPEDの処理を実行して終わる」と捉えるのが自然です。すると、
case ACTIVE:
start();
// 意図的に次へ進めている?
// それともbreakの書き忘れ?
case STOPPED:
stop();
のようなコードは、元々のswitchの制御モデルからすれば普通に後続へ進んでいるだけでも、現在の「caseごとの分岐」という感覚から見ると不自然に映ります。
ここから先は一次資料で裏付けられる話ではなく、ここまでの経緯を踏まえた筆者の推測です。
自由な制御が読みづらさにつながることはBの時代から意識されており、フォールスルーの変更に弱い性質もCの時代には指摘されていました。昔は安全だったのに後から危険になった、というより、switchが「実行開始位置を選ぶ制御構造」よりも「値ごとに独立した処理を選ぶ分岐」として読まれるようになるにつれ、元々あった弱点がより目立つようになった、という方が実態に近いのではないかと思います。
「昔のCPUではフォールスルーする方が高速だったから、この仕様になった」という説明を見かけることがあります。
BCPLがコンパイラ記述やシステムプログラミング向けの単純な言語として設計されていたことは一次資料から確認できますが、「フォールスルーしない仕様では効率的に実装できなかった」とまでは確認できませんでした。各caseの末尾から自動的にswitchの外へ制御を移すコードを生成することもできるため、単純に「高速化のため」と断定するのは避けた方がよさそうです。
Java:C/C++に近いswitchを引き継ぐ
その後、Javaにも従来型のswitchとフォールスルーの挙動が引き継がれます。JavaはC/C++をそのままコピーした言語ではありません。
初期のJava設計資料『The Java Language Environment』では、Javaの設計目標としてSimpleとFamiliarが挙げられています。
Oracle - The Java Language Environment: Java -- Simple and Familiar
JavaはC/C++から複雑さや問題の原因になる機能を取り除く一方、C/C++を知るプログラマが馴染みやすい言語にすることも重視していました。その中でswitchについては、C/C++に近い構文と挙動が残りました。
ただし、switchの仕様を残した理由そのものを設計者が明言している資料は見つかりませんでした。JavaがC/C++に馴染みのある構文を意図的に多く採用していたことは確認できますが、switchについてそこまで断定はできません。
Java 14:case ... ->が正式化される
その後、Javaのswitchは拡張されます。JEP 361によってJava 14で正式化された構文では、case ... ->と書けます。
switch (status) {
case ACTIVE -> start();
case STOPPED -> stop();
}
この形式では、一致したcaseの右側だけが実行され、次のcaseへ処理が続きません。
JEP 361では、従来のswitchが持つデフォルトのフォールスルーについて、柔軟さよりもエラーを起こしやすい性質が問題になってきたことが、新しい構文を導入する背景の一つとして説明されています。
現在のOracleのJavaドキュメントでも、コロン形式ではbreakやyieldの書き忘れによって意図しないフォールスルーが発生する可能性があるため、arrow caseの利用が推奨されています。
Oracle Java 26 - Switch Expressions and Statements
歴史を踏まえると、この違いは分かりやすいです。従来形式は「一致した位置から後続の文を実行し、抜けたい場合はbreakする」という考え方でした。一方arrow caseでは、「一致したcaseに対応する処理だけを実行する」ことが構文そのものに組み込まれています。
単にbreakを書かなくてよくなっただけではなく、現在の「caseごとの独立した分岐」という読み方に、構文自体が近づいたとも捉えられます。
では今、従来形式のswitchを見つけたらどうするか
歴史的な理由が分かったとしても、新しいコードで従来形式を積極的に使う理由とは別の話です。
その分岐にswitchが適しているなら、基本的にはcase ... ->を使うのが分かりやすいでしょう。
switch (status) {
case ACTIVE -> start();
case STOPPED -> stop();
case PAUSED -> pause();
}
複数の値で同じ処理をしたい場合も、フォールスルーを使わずまとめて書けます。
switch (day) {
case SATURDAY, SUNDAY -> rest();
default -> work();
}
一方、条件が値の一致ではなく範囲や複数条件なら、ifの方が意図を表しやすい場合もあります。
if (score >= 80) {
rank = Rank.A;
} else if (score >= 60) {
rank = Rank.B;
} else {
rank = Rank.C;
}
逆に、同じ値について候補ごとに処理を分けているならswitchの方が読みやすいこともあります。
重要なのは、従来形式を見つけたら機械的に->へ置き換えることではなく、その分岐を何で表現するのが一番読みやすいかを考えることです。同じenumに対するswitchが何度も登場するなら、enum側に振る舞いを持たせるなど、そもそもの設計を見直せる場合もあります。
静的解析でフォールスルーを検出する
人間がレビューで毎回確認する以外にも、ツールで検出できます。javacには次のlintオプションがあります。
-Xlint:fallthrough
これを有効にすると、次のcaseへ処理が流れる箇所について警告を出せます。
Oracle Java 26 - The javac Command
ただし、これはフォールスルーを検出するもので、breakを伴う従来形式のcase ... :自体を禁止するものではありません。
CheckstyleにはUseEnhancedSwitchというチェックもあります。
<module name="Checker">
<module name="TreeWalker">
<module name="UseEnhancedSwitch"/>
</module>
</module>
Checkstyle - UseEnhancedSwitch
このチェックでは、可能な場合に従来のcase ... :ではなくcase ... ->を使うよう検査できます。ただし、意図的なフォールスルーが含まれている場合には、無理にarrow caseへ変換する対象にはなりません。
まとめ
最初に気になったのは、「なぜbreakを書かないと次のcaseへ処理が流れる仕様になったのか」という疑問でした。
歴史を遡ると、前提が現在とは違っていました。初期のswitchは、値に応じて「どこから実行を始めるか」を選ぶ多方向分岐でした。一致したcaseから先は通常どおり後続の文を実行するため、フォールスルーは特別な機能ではありません。
Bではそこから「switchを抜ける」という操作がbreakとして専用化され、その形がCを経てJavaの従来型switchにも残りました。つまり、「フォールスルーを使いたいからbreakを任意にした」のではなく、実行開始位置を選ぶswitchが先にあり、後から抜ける操作が加わった、という順序です。
意図的なフォールスルーか書き忘れかを区別しにくいという問題も、最近生まれたものではありません。Cの時代からその危うさは指摘されており、switchが「caseごとに独立した処理を選ぶ分岐」として読まれるようになるにつれ、元々あった弱点がより目立つようになった、というのが筆者の見立てです。
現在のJavaではcase ACTIVE -> start();のように、マッチしたcaseの処理だけを実行する書き方を選べます。今だけ見ると不思議な仕様でも、作られた順番と使われ方の変化を追うと、なぜこの形になったのかが分かりやすくなりました。
参考資料
- Martin Richards - BCPL Reference Manual (1967)
- A Tutorial Introduction to the Language B
- User's Reference to B on MH-TSS
- Dennis M. Ritchie - The Development of the C Language
- Brian W. Kernighan, Dennis M. Ritchie - The C Programming Language, 2nd Edition
- Oracle - The Java Language Environment: Java -- Simple and Familiar
- JEP 361: Switch Expressions
- Oracle Java 26 - Switch Expressions and Statements
- Oracle Java 26 - The javac Command
- Checkstyle - UseEnhancedSwitch