Java 14で正式導入されたアロー構文(->)を伴う switch は、従来の switch 文に比べて記述がシンプルになりました。しかし、そのシンプルさゆえに「今書いているのは switch 文なのか、それとも switch 式なのか」という区別で迷うケースがあります。
先日、Qiitaの質問機能で以下のような興味深いコードと疑問が投稿されていました。
URL: https://qiita.com/hiroki-shosinsher/questions/ddfb8eeeaf9e38c70a4a
この記事では、Java言語仕様(JLS: Java Language Specification、Javaの文法を定義する公式仕様書)の原文と、実際のコンパイル結果に基づいて、この switch の正体を解き明かします。
対象読者
- アロー構文の
switchを使ったことはあるが、「文」と「式」の区別を意識したことがない方 - 「式(Expression)」と「文(Statement)」というJavaの基本概念をなんとなく知っている方(記事中でも都度補足します)
検証環境
- OpenJDK 21.0.11(eclipse-temurin:21)
議論の対象となったコード
public class Sample{
public static void main(String[] args){
int a = 2;
switch(a){
case 1 -> System.out.println("A");
case 2 -> System.out.println("B");
case 3 -> System.out.println("C");
default -> System.out.println("D");
}; // ← この閉じカッコ直後のセミコロンにも注目
}
}
このコードについて、以下のような疑問が生じていました。
- 変数の代入などに使っておらず、値を返していない(画面表示のみ)から
switch文ではないか? - 閉じカッコの後ろのセミコロン(
;)はswitch式の証拠か、それともswitch文に余計なセミコロンがついているだけか? - 全体をカッコで囲むとコンパイルエラーになるのは、これが「文」だからではないか?
結論:これは「アロー構文を使ったswitch文」+「空文」である
言語仕様に照らすと、このコードの正体は次のとおりです。
-
switch(a){ ... }の部分は、アロー構文を使った switch文(SwitchStatement) - 閉じカッコ直後の
;は、switch式の証拠ではなく、単なる 空文(Empty Statement、JLS §14.6)。何もしない無害な文です
つまり冒頭の疑問1の「これは switch 文ではないか?」は結論として正しいです。ただしその理由は「値を代入していないから」ではなく、「値が要求されない文脈に置かれているから」です。この違いが本記事のポイントになります。
なぜそう言えるのか、仕様書の定義とコンパイル実験で1つずつ確認していきます。
根拠1:switch式は単体で「文」になれない
本題に入る前に、2つの用語を整理しておきます。
-
式(Expression):評価すると値を生み出す構文。
1 + 2や"A".length()など -
文(Statement):処理の実行単位となる構文。
if文やreturn文など
Javaでは、式を単体で文として置けるのは、ごく限られた種類だけです。これを 式文(ExpressionStatement) と呼びます。JLS §14.8 で式文にできる式は以下に限定されています。
StatementExpression:
- Assignment(代入式)
- PreIncrementExpression / PreDecrementExpression(前置インクリメント・デクリメント)
- PostIncrementExpression / PostDecrementExpression(後置インクリメント・デクリメント)
- MethodInvocation(メソッド呼び出し式)
- ClassInstanceCreationExpression(クラスインスタンス生成式)
このリストに SwitchExpression(switch式)は含まれていません。
したがって「void的なswitch式を文の位置に単体で置いている」という解釈は文法上成立しません。文が要求される位置に単体で現れた switch は、必ず switch文 として解釈されます。
根拠2:そもそも「値を返さないswitch式」は存在しない
「値を代入していないだけで、実はvoidなswitch式なのでは?」という解釈も考えられますが、これも仕様が明確に否定しています。
JLS §15.28.1 では、switch式の各アームが生み出す値を「結果式(Result Expression)」と呼び、次のように規定しています。
It is a compile-time error if a switch expression has no result expressions.
(switch式に結果式が1つもない場合、コンパイルエラーとなる)
そして System.out.println(...) のようなvoidメソッドの呼び出し式は「値を生み出さない」ため、結果式になれません。実際に代入してswitch式として解釈させようとすると、コンパイルエラーになります。
// 実験:代入してswitch式として解釈させてみる
Object x = switch(a){
case 1 -> System.out.println("A");
default -> System.out.println("D");
};
error: incompatible types: bad type in switch expression
case 1 -> System.out.println("A");
^
void cannot be converted to Object
「voidをObjectに変換できない」、つまりswitch式の結果としてvoidは認められないことがエラーメッセージからも確認できます。値を返さないswitchが書ける時点で、それはswitch式ではなくswitch文なのです。
根拠3:アローの右側の書き方では「文」と「式」を区別できない
「-> の右側の書き方を見れば区別できるのでは?」と思うかもしれませんが、実はできません。アロー構文の文法(SwitchRule)は、switch文(JLS §14.11.1)とswitch式(JLS §15.28)で共通だからです。
SwitchRule:
- SwitchLabel
->Expression ;- SwitchLabel
->Block- SwitchLabel
->ThrowStatement
注目すべきは1行目です。アローの右側に式を書く場合、switch文でもswitch式でも末尾のセミコロンは文法上必須です。試しに省略すると、switch文であってもエラーになります。
error: ';' expected
case 1 -> System.out.println("A")
^
つまり、各 case の行末にセミコロンがあるかどうかは、文と式を見分ける材料にはなりません。
なお、switch文の場合は追加の制約として「アローの右側の式は式文にできる式(StatementExpression)に限る」という規定があります(JLS §14.11.2)。今回の System.out.println(...) はメソッド呼び出し式なので、この制約も満たしています。
では、閉じカッコの後ろのセミコロンの正体は?
ここまでで switch(a){ ... } がswitch文だと確定しました。switch文の文法は switch ( Expression ) SwitchBlock であり、末尾にセミコロンを含みません。
では }; のセミコロンは何かというと、空文(Empty Statement、JLS §14.6) です。セミコロン単体は「何もしない文」として合法なので、コンパイルエラーにはなりません。if (...) { ... }; の末尾に付いたセミコロンと同じ、消しても挙動が変わらない余計な1文字です。
カッコ ( ) で囲むとエラーになる理由
最後に、疑問3の「カッコで囲むとエラーになるのは文だから?」を検証します。
(switch(a){
case 1 -> System.out.println("A");
default -> System.out.println("D");
});
error: not a statement
(switch(a){
^
カッコ ( ) の中に書けるのは「式」だけです。そのためカッコで囲むと、switchは式として解釈することを強制されます。しかし、
- 根拠2のとおり、結果式がすべてvoidのswitch式はそもそも不正
- 仮に式として成立しても、「カッコで囲まれた式(Parenthesized Expression)」は根拠1の式文リストに含まれないため、文の位置には置けない
という二重の理由でエラーになります。「文だからカッコで囲めない」という直感は結果的に近いところを突いていますが、正確には「カッコによって式としての解釈を強制した結果、式として成立できなかった」というのが実態です。
まとめ:見分け方はただ1つ「値が要求される文脈かどうか」
3つの疑問への回答をまとめます。
| 疑問 | 回答 |
|---|---|
| 1. 値を返していないからswitch文では? | 結論は正しい。ただし正確な理由は「値が要求されない文脈(文の位置)にあるから」 |
| 2. 末尾のセミコロンはswitch式の証拠? | いいえ。単なる空文。消しても挙動は変わらない |
| 3. カッコで囲むとエラーになるのは文だから? | 半分正解。カッコは式としての解釈を強制し、その解釈が成立しないためエラーになる |
アロー構文・各caseのセミコロン・末尾のセミコロンは、いずれも文と式の判別材料になりません。見分け方はただ1つ。
そのswitchが「値を要求される文脈」(代入の右辺、メソッドの引数、return文など)に置かれていれば式、そうでなければ文。
これだけです。コンパイラもこの文脈によって解釈を切り替えています。
【おまけ】実務上の最大の違いは「網羅性」
「文か式かなんて、結局ラベルの違いでは?」と思うかもしれませんが、実務に直結する大きな違いが1つあります。網羅性(exhaustiveness)チェック、つまり default を省略できるかどうかです。
switch式は「必ず値を生み出す」必要があるため、すべての入力をカバーしなければならず、default(またはenum全定数などの網羅)が必須です。
// switch式:defaultを省略するとコンパイルエラー
String s = switch(a){
case 1 -> "A";
};
// error: the switch expression does not cover all possible input values
一方、今回のような int をセレクタとするswitch文は、アロー構文であっても網羅性を要求されません。
// switch文:defaultなしでもコンパイル成功
switch(a){
case 1 -> System.out.println("A");
case 2 -> System.out.println("B");
}
※ ただしJava 21以降、case に型パターン・レコードパターン・null を使う場合、またはセレクタが従来のswitchで扱えなかった型(Object などの型)の場合は、switch文でも網羅性が必須になります(enhanced switch statement、JLS §14.11.2)。String やラッパー型・enumをセレクタに定数の case だけを書く従来型のswitch文は、引き続き網羅性を要求されません。
つまり「文か式か」は単なるラベルの違いではなく、「default を書き忘れたときにコンパイラが守ってくれるかどうか」という実用上の違いとして現れます。アロー構文を使うときは、自分が書いているのが文なのか式なのかを意識しておくと、コンパイルエラーの意味がすっと理解できるようになります。
おわりに
実はこの記事を書き始めた段階では、私は「これはvoid型のswitch式を式文として書いたもの」という推測を立てていました。しかしJLSの原文を確認すると、根拠にしようとした規定(式文リストへのSwitch式の包含や、void型switch式の定義)はどこにも存在せず、コンパイル実験でも否定されました。
仕様の原文と実際のコンパイラで裏を取ることが大切でした。本記事の引用元は以下のとおりですので、ぜひみなさんも確認してみてください。
参考(JLS SE 21)