Javaのswitchとは?条件分岐のための使い方やif文との使い分け方
Javaのswitch文は、条件分岐の構文の一つです。この記事では、まずはswitch文の構文と使い方の基本をお伝えします。
なお、実際にswitchを使う際は、同じ条件分岐の構文であるif文との使い分けに迷うかもしれません。それに、長らく仕様が変わらなかったswitch文に、最近新しい仕様が追加される気配があります。
ですので、この記事ではswitch/if文の使い分けの考え方と、新しいswitch文の姿もお伝えします!!
目次
1.switch文の基本
1-1.switch文の構文
switch文の基本的な構文は以下のものです。“switch” の後にある () に判定したい値・変数を指定し、“case” の後ろにマッチさせたい値・条件を指定し、必要なだけ羅列します。caseの後ろには“:”が必要ですので、忘れないようにしましょう。
switch (式) { case 値1: 式の評価結果が値1と等しい場合に実行する処理; break; case 値2: ~値2と等しい場合に実行する処理; //breakはなくても良い; case 値n: ~値nと等しい場合に実行する処理; break; default: 式がcaseのどの値とも等しくない場合に実行する処理; break; }
caseは分岐に必要な分だけ記述できます。breakは必須ではありませんが、caseごとに記述することが普通でしょう(breakの使い方は後述します)。
実際に良く見られるswitch文は以下のような感じです。式の評価結果はその都度変化し、値は固定となっていて、別の場所で宣言されている変数を参照するのが普通です。
// このような処理結果の定数が、同一クラス内あるいは別クラスで定義されていて… public static final int SUCCESS = 1; public static final int FAIL = 2; public static final int EXCEPTION = 3; // switch文で判断したい値に対して、定数をマッチングさせる int result = someProcess(); switch (result) { case SUCCESS: // 正常時の処理 break; case FAIL: // 失敗時の処理 break; case EXCEPTION: // エラー時の処理 break; }
// または、このようなenumがどこかにあり… enum ProcessResult { SUCCESS, FAIL, EXCEPTION; } // 変数が指すenumの実体に対してswitchする ProcessResult result = someProcess(); // enumのフィールドのどれかを指す switch (result) { case SUCCESS: // 正常時の処理 break; case FAIL: // 失敗時の処理 break; case EXCEPTION: // エラー時の処理 break; }
if文で表現するなら、if~else if~elseと似た意味になります。switch文は、長いelse-ifの条件を“==”いらずでシンプルに書けるのが良いところですね。また、switch文全体で式の評価が1回で済むのが、if文と比べてパフォーマンス面でも優れるところです。
1-2.switch文で使える型
1つのswitch文で扱える型は、以下のいずれか1つだけです(Java 10時点)。複数の型を混ぜては使えません。longや浮動小数点数(float、double)が使えないことにも注意しましょう。実務上ではint/String/enumが多いと思います。
- char、byte、short、int、enum(J2SE 5以降)、String(Java 7以降)
- およびswitch文に対応するプリミティブ型のラッパークラス(Char、Byte、Short、Integer)
例として、式と値にintとStringが混在している下記switch文は、コンパイルエラーとなってしまいます。
int value = 1; switch (value) { // 式はintである case 1: case "String": // intを式にしているswitchで、値をStringとしているのでコンパイルエラー }
さらに、式・値の両方でnullは扱えません。値がnullだとコンパイルエラーになります。式がnullの場合は、コンパイルエラーでなく実行時エラー(NullPointerException)となりますので、実行するまでエラーとなることが分からず発見しづらいです。
String s = "ABC"; switch (s) { case null: // コンパイルエラー!! nullは指定できない break; }
String s = null; switch (s) { // コンパイルエラーとはならないが、実行時にNullPointerExceptionが発生する case "A": break; }
switch文の値で使えるのは、それぞれの型のリテラル値・変数・演算結果です。しかも値はコンパイル時に確定していなければなりませんので、普通は静的に宣言した定数、あるいは定数に対する静的な演算結果を用います。enumのフィールドは定数扱いなので、そのまま指定できます。
例えば以下のような書き方ができます。プログラマーが読んだ時にわかりやすくするのにも使えたりしますね。
String s = "ABC"; switch (s) { case "A" + "B" + "C": // このswitch文を実行するとここに来る。コンパイル時に"A" + "B" + "C"→"ABC"となるため break; }
final x = 100; int i = 1000; switch (i) { x * 10: // このswitch文を実行するとここに来る。 // xは100だとfinalで確定しており、さらに10を掛けると1000になることがコンパイル時点で確定するため break; }
式と値が等しいかの判定は、Stringならequals()の結果で、それら以外(プリミティブ型のラッパークラス含む)なら==の結果です。
1-3.caseを中断するbreakとフォールスルー
breakはfor文やwhile文での意味と近く、実行しているcase句の処理をそこで打ち切り、switch文を抜けます。
switch (式) { case 値1: 文1; // 値1に該当する場合はここから処理開始 break; // ここでswitchを抜ける case 値2: 文2; // 値2に該当する場合はここから処理開始 break; // ここでswitchを抜ける case 値3: 文3; // 値3に該当する場合はここから処理開始 break; // ここでswitchを抜ける }
case句の中にbreakがないと、その次にあるcaseに処理の流れがそのまま移ります。これをフォールスルー(fall through)と言います。日本語に直訳すれば「通り抜けて落ちる」で、そのまま下へすとーんと落ちていくイメージです。これがswitch文の大きな特徴の一つです。
switch (式) { case 値1: 文1; //breakを記述していない!! case 値2: 文2; break; case 値3: 文3; break; }
この例だと、式が値1に該当する場合は文1が実行された後、値2の文2がそのまま実行されます。値2でbreakしていますので、そこでswitch文全体が終了します。これは、特定の値の時だけ先に追加で実行したい処理がある場合に使えます。
なお、case内には処理を書かなくてもいいので、複数の値で同じ処理を実行するだけなら、以下の書き方もできます。これはif文での「または(||)」と似ていますね。
switch (式) { case 値1: case 値2: 値1と値2の共通処理; break; case 値3: 文3; break; }
1-4.defaultはその他の場合に使う
defaultは、全てのcaseに該当しない場合の処理を書く時に使用します。if~else if~elseで言う、最後のelseに相当するものです。
switch (式) { case 値1: 文1; break; case 値2: 文2; break; default: 文3; break; }
なお、defaultはswitch文のどこに出て来ても問題ありません。そのため、必ず最後に来るif文のelseとは扱いが違います。ですが、defaultはelseと同様に、caseの一番最後に置くことが普通でしょう。
例えば、以下のswitch文も構文上は正しいものです。ここでdefaultでのbreakがないと後ろのcaseがフォールスルーで実行されてしまいます。特別な理由がなければ最後にしておくのが無難でしょう。
switch (式) { default: 文3; break; // このbreakがないと、後ろのcaseへフォールスルーしてしまう case 値1: 文1; break; case 値2: 文2; break; }
2.switch文のはまりポイントと対応方法
ここではswitch文でよくはまるポイントと対応方法をお伝えします。もしswitch文でトラブルが起きたら、以下のことに該当しないか調べてみてください。
2-1.式や値にnullは使えない
前述のとおり、switch文の式や値にnullは使えません。実行時にswitch文に関係する箇所でNullPointerExceptionが発生したなら、式に関係する変数が指しているものがnullでないかを確認しましょう。
2-2.桁の大きな整数や浮動小数点数は使えない
これも前述のとおり、式や値にlongやfloat、double(およびそれらのラッパークラスであるLong、Float、Double)は使えません。数値を表現するクラスであるNumberやBigIntegerやBigDecimalも同様に使えません。
2-3.通常のクラスやインターフェイスは使えない
switchで使える以外の型、つまり自分で作ったクラスやインターフェイスでもswitchはできません。
2-4.caseには同じ値を指定できない
caseには同じ値を指定できません。コンパイルエラーのメッセージもまだわかりやすいのですぐ対応できると思いますが、定数を羅列した場合に同じ値があるかどうかは分かりづらいので、定数の定義元をしっかり確認するようにしましょう。
// コンパイルエラー(エラーメッセージは"重複 case") int x = 0; switch (x) { 0: break; 0: break; }
2-5.switch文内の変数のスコープはブロック全体
これは結構ややこしいです。例えば、以下のswitch文はコンパイルエラーになります。ですので、なるべく複数のcaseにまたがるような変数は使わず、なるべく一つのcaseの中に収まるように使うのが良いでしょう。
int x = 1; switch (x) { case 0: // コンパイルエラー!! この時点では変数yが宣言されていない System.out.println(y); case 10: int y = 0; case 100: // コンパイルエラー!! case 10の後なので変数yは使えるが、直接このcaseに来た時は変数が未初期化な可能性がある System.out.println(y); }
2-6.switch内で宣言した変数はブロックの外では使えない
for/while/if文などと同じで、switch文全体が大きな一つのブロックになるので、{}の中で宣言した変数はswitch文の外部では使えません。見落としがちなので注意しましょう。
int x = 1; switch (x) { case 0: int x2 = 0; } System.out.println(x2); // コンパイルエラー!! x2はswitch文のブロックの中でのみ有効
2-7.ネストしたswitchからbreakするにはラベル付きbreakを使う
あまりないと思いますが、switch文をネストさせたいことがあるかもしれません。その場合、ネストが深い方のswitch文をbreakしただけでは、そのswitch文だけがbreakされるので、大元のswitch文の後続処理はそのまま実行されます。
int x = 1; int y = 100; switch (x) { // 大元のswitch case 1: switch (y) { // ネストしたswitch case 100: break; // ここでbreakしても、 } System.out.println("after switch(y)"); // この文は実行される break; }
もし、ネストが深いswitch文から直接switch文全体をbreakしたいなら、ラベル付きbreakが使えます。例は以下のとおりです。
int x = 1; int y = 100; SWITCH1: switch (x) { case 1: switch (y) { case 100: break SWITCH1; // ラベル付きbreakして大元のswitch文全体から抜ければ、 } System.out.println("after switch(y)"); // この文は実行されない break; }
2-8.switchで処理したい値以外が来た時の対処方法
switchで処理したい値以外が来た時の対処方法にはいくつかあります。
1つ目はそもそも必要な値以外をcaseに書かないか、既にお伝えしたdefaultを使えば良いでしょう。defaultを使う場合も指定はするけれども、中では何もしないのです。何事もなかったかのごとく後続の処理が動けばいいならこれが最も簡単です。
int x = 100; swtich(x) { 10: 10の時の処理; break; 20: 20の時の処理; break; }
int x = 100; swtich(x) { 10: 10の時の処理; break; 20: 20の時の処理; break; default: // 何もしない }
2つ目は、defaultは使うけれども例外をthrowするものです。イディオムの一つでもあります。良く見かけるのはIllegalArgumentExceptionやIllegalStateExceptionをthrowするものですが、他の例外や自作の例外をthrowしてもOKです。if-elseのelseで例外をthrowするのと同じですね。
int x = 100; int result; swtich(x) { 10: result = x * 10; break; 20: result = x * 100; break; default: throw new IllegalStateException(); // 10, 20以外の値なら例外をthrowする } System.out.println(result);
これの良いところは、想定した値以外では絶対に後続処理が動かないことです。こういう書き方だと、例でのresultには必ず値が設定されることが保証されますので、変数の初期化漏れやデフォルト値のままだった…というようなうっかりミスを防げます。
それに、例外をthrowすることで「想定している値以外は絶対に許可しない」というプログラマーの意思を表現できます。1つ目のやり方だと、考慮漏れなのかあるいは分かっていてそうしているのか、設計書やコメントでも残っていなければわからないですよね。
3.switch文とif文との使い分けの判断基準
switch文とif文を使い分ける時の判断基準の一つは、判定処理にかかるコストと速さです。
if文は全ての条件判定を上から順番に行うので、柔軟な条件判定ができる代わりに、判定にかかるコスト(処理時間含む)が大きくなりがちです。判定条件が複雑になればなるほど、if文全体の実行コストが上がっていきます。良く発生する条件ほどif文の前の方に持ってくるべきなのは、これが理由です。
if (判定条件1) { // 何かの処理 } else if (判定条件2) { // 何かの処理 } else if (判定条件3) { // 何かの処理 } else if (判定条件4) { // 何かの処理 } else { // ここに来るためには、ここまでの全てのif文の中の判定条件を検証しなければならない }
switch文は、該当する値の箇所へ処理が直接ジャンプする、いわゆる「GOTO文」のイメージです。GOTO文のような処理は選択肢がいくら多くてもジャンプに必要な時間はほぼ一定で、それがswitch文の特徴でもあります(ちなみに、GOTO文は最近のプログラミング言語には仕様上で存在しないことが多いです)。
ですので、上のif文で例えるなら、switch文は途中の判定処理1~3を実行すらせず、いきなり判定条件4のブロック内に処理が移る、ということです。これがswitch文とGOTO文が同じと言えるところです。ただし、その代償としてswitch文では複雑な条件判定は行えません。
従って、switch文とif文の使い分けの方針の一つとしては、以下を考えて、当てはまるものが多ければswitch文…としても良いのではないのでしょうか。もちろん、見た目が好きというのもありなのですが、選んだ理由をきちんと言えると格好いいですよね。
- 判定条件で使う変数は1つだけか
- 判定条件で使う変数の型は1つだけ、かつswitch文で使えるものか
- 判定条件は == あるいは equals だけか
- 判定条件の値、総数がプログラミング時に静的に決定できるか
- 判定処理の速度がプログラムの要件・仕様上で重要か
4.switchの未来、JEP 325:Switch Expressions
switchの構文そのものはJavaの初期から変わっていません。これまでのJava仕様の改定では、扱える値の型が増えただけでした。もちろん、switch文でenumやStringが扱えるようになっただけでも大きな進歩です。
ですが、「JEP 325 Switch Expressions」によりswitch文の構文に大きな変化が訪れる予定なので、先取りして知識を得ておいても良いでしょう。JEP 325はJava 12で取り込まれる可能性がある仕様です(2018/10時点)。
JEP 325では、switch文自体が戻り値を返せるようになったり、ラムダ式のように使えるようになります。例えば以下のものはJEP 325から抜粋したものですが、もっとswitch文を活用できるようになりそうですね。
// caseで実行する文をラムダ式のように書け、caseごとにブロックが独立する!! switch (day) { case MONDAY, FRIDAY, SUNDAY -> System.out.println(6); case TUESDAY -> System.out.println(7); case THURSDAY, SATURDAY -> System.out.println(8); case WEDNESDAY -> System.out.println(9); }
// swtich文自体が戻り値を持つ!! int numLetters = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; };
// switchの式と戻り値の型が違っていてもいい!! int result = switch (s) { case "Foo": break 1; case "Bar": break 2; default: System.out.println("Neither Foo nor Bar, hmmm..."); break 0; };
5.まとめ
この記事ではswitchの基本とはまりポイント、なぜswitchが早いのかをお伝えしました。switch文にはif文にはない特徴がありますので、ぜひ活用していきましょう。
また、現在時点で開発中のJEP 325では、switch文の構文が拡張されます。このような情報を先取りすることで、周囲の一歩先を行けるかもしれませんね。
コメント