S-JIS[2020-03-21/2023-09-23] 変更履歴
Javaのswitch文・switch式について。
|
switch文・switch式は、値に応じて処理を分岐させる構文。
switch式はJava14以降(プレビュー版はJava12・Java13)で使用可能。
switch文は文(ステートメント)なので、値を返す事は出来ない。
switch式は、各caseから値を返す事により、switchの結果として値を返す。
switch文とswitch式の判別は、switchから値を受け取るようにコーディングされていたらswitch式、そうでなかったらswitch文として扱われるようだ。
switch式なのにcaseから値を返すようになっていないとコンパイルエラーになるし、
switch文なのにcaseから値を返すようになっているとコンパイルエラーになる。
C言語のswitch文とほぼ同じ構文。
switch (式) {
case null: // Java21以降[2023-09-23]
文; …
break;
case 値1:
文; …
break;
case 値2:
文; …
break;
…
case 値n:
文; …
//breakなし(fall through)
default:
文; …
break;
}
switchに使える式は、整数型(byte, char, short, intのいずれか)の値になるもの。
JDK1.5以降では列挙型も可。
JDK1.7以降では文字列も可。
Java21以降ではレコードやシールクラスも可。[2023-09-23]
「case null:
」(Java21以降)が無い場合、式がnullだとNullPointerExceptionになる。[/2023-09-23]
caseには具体的な値(定数)を指定する。
値にはstatic finalな変数(定数)も使用できるが、条件がある。→caseで使える定数
Java21より前では、
「case null:
」と書くとコンパイルエラーになる。[2023-09-23]
どのcaseにも一致しない場合の処理はdefaultに書く。
defaultは通常は一番下に書くと思うが、どこにあっても(先頭にあっても)構文上は問題ない。
Java21以降ではcase null
と一緒に「case null, default:
」とも書ける。[2023-09-23]
breakを書くとswitch文を抜ける。(「break ラベル」でそのラベルのブロックを抜ける)
breakを書かないと次のcaseへ処理が継続する。(フォールスルー(fall through)と呼ばれる)
一番最後のcase/defaultはbreakを書かなくてもswitch文を抜けるが、バグを避ける為にも常にbreakを書いておく方が良いと思う。(新しいcaseを末尾に追加した場合とか、caseの位置を移動させた場合の為)
逆に意図的にフォールスルーする場合は、breakを書き忘れているわけではないことを明示する為に、コメントで「// fall through
」と書いておくのが良いと思う。[2020-09-16]
複数の値に対して同じ処理を行いたい場合は、(fall throughを利用して)caseを並べて「case 値1: case 値2: 処理
」のように記述する。
Java14以降は値をカンマ区切りで並べて「case
値1, 値2: 処理
」のように記述することも出来る。
「switch」の前にラベルを付けて、switchブロック内のbreakでそのラベルを指定できる。(そのラベルのブロックから抜ける)
outer: switch(n) { case 1: switch(s) { case "a": System.out.println("a"); break outer; default: break; } System.out.println(1); break; default: System.out.println(n); break; }
※switch式の場合はbreakで脱出することは出来ない。
ただ、switch文における「break ラベル」は、普通はswitchの外側のループを抜けるときに使うことの方が多いだろう。
loop: for (int i = 0; i < 10; i++) { switch(i) { case 5: break loop; default: break; } System.out.println(i); }
Java14(プレビュー版ではJava12)から、switchで値を返せるようになった。
各caseの最後で(breakの代わりに)yieldを使って値を返す。
もしくはアロー構文を使って値を返す。
caseのyieldの後ろに値(式)を書くことにより、switch式の結果としてその値を返す。
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY: yield 1; case TUESDAY: yield 2; case WEDNESDAY: yield 3; case THURSDAY: yield 4; case FRIDAY: yield 5; case SATURDAY: yield 6; default: yield 100 + day.ordinal(); }; System.out.println(value);
当然、変数で値を返すことも可能。
default: int temp = 100 + day.ordinal(); yield temp;
default: int temp = day.ordinal(); yield 100 + temp;
yieldの後ろに書くのは式なので、丸括弧を付けることも出来る。
case MONDAY: yield(1);
これは一見メソッド呼び出しのように見えるが、returnの時に式を丸括弧で囲めるのと同じ。
まぎらわしいので、このような書き方はしない方が良いだろう。
yieldメソッドを定義してそれを呼び出そうとすると、「switchの外でyieldを使おうとした」というエラーになる。
構文的には、yieldメソッドの呼び出しよりも、switch式のyield構文の方が優先されるようだ。
(yieldはキーワードではないようだ。SourceVersion.isKeyword("yield")
はfalseを返す)
public class Example { public static void main(String... args) { yield(123); } private static void yield(int n) { System.out.println("yield method " + n); } }
↓
Example.java:3: エラー: switch式の外側のyield yield(123); ^ (yieldというメソッドを呼び出すには、yieldを受取り側またはタイプ名で修飾します)
yieldがstaticメソッドの場合、クラス名で修飾すれば呼び出せる。
yieldがインスタンスメソッドの場合、thisを付ければ呼び出せる。
Example.yield(123);
this.yield(123);
yieldを使ってswitch式を作る場合は「case 値: yield 式;
」と書くが、「case 値 -> 式;
」と書くことも出来る。
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY -> 1; case TUESDAY -> 2; case WEDNESDAY -> 3; case THURSDAY -> 4; case FRIDAY -> 5; case SATURDAY -> 6; default -> 100 + day.ordinal(); }; System.out.println(value);
アロー構文では、式の他にブロックか例外を書くことが出来る。
(ブロックを作らずに例外を直接書けるのは利便性の為らしい)
default -> throw new IllegalStateException(day.toString());
ブロックから値を返すには(アロー構文でも)yieldで値を書く。
case MONDAY -> { System.out.println(day); yield 1; } default -> { System.out.println(day); throw new IllegalStateException(day.toString()); }
アロー構文の場合はcaseがフォールスルーしないので、breakを書かない記法として使うことも出来るかもしれないが、紛らわしいのでこういう使い方はしない方がいいと思う。
var day = DayOfWeek.SUNDAY; switch (day) { case MONDAY -> System.out.println(1); case TUESDAY -> System.out.println(2); case WEDNESDAY -> System.out.println(3); case THURSDAY -> System.out.println(4); case FRIDAY -> System.out.println(5); case SATURDAY -> System.out.println(6); default -> System.out.println(100 + day.ordinal()); };
→複数のcaseに対して同一の処理をしたい場合は、カンマ区切りで複数の値をcaseに指定する
caseの「->」と「:
」は混在できない。
混在しているとコンパイルエラーになる。
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY -> 1; case TUESDAY: yield 2; 〜 };
↓
Example.java:9: エラー: switchでcaseの異なる種類が使用されています case TUESDAY: yield 2; ^
switch式(値を返すswitch)では、「case 値 -> 式;
」でも「case 値:
yield 式;
」でもcaseの網羅性チェックが行われる。
(switch文(値を返さないswitch)の場合は、(従来どおり)網羅性はチェックされない)
列挙型のswitch式の場合、caseに列挙子の漏れがあるとコンパイルエラーになる。
int等のswitch式の場合、defaultが無いとコンパイルエラーになる。
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; // case WEDNESDAY -> 9; }; System.out.println(value);
↓
Example.java:6: エラー: switch式がすべての可能な入力値をカバーしていません var value = switch (day) { ^
switch式(値を返すswitch)の場合、continueやbreak(ラベル指定breakも)でブロックを脱出することは出来ない。
returnも不可。
loop: for (var day : DayOfWeek.values()) { var value = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> { break; } // case WEDNESDAY -> { break loop; } // case WEDNESDAY -> { continue; } // case WEDNESDAY -> { return; } }; System.out.println(value); }
↓
Example.java:11: エラー: switch式の外側でbreakを実行しようとしています case WEDNESDAY -> { break; } ^
Java14以降では、ひとつのcaseに値をカンマ区切りで複数記述することが出来る(multiple comma-separated labels)。
(「case 値: 処理
」の形式の場合、breakが無いとフォールスルー(fall
through)することを利用し、複数の値に対して同じ処理を行いたい場合は「case 値1: case 値2: 処理
」と書いていた)
特にアロー構文「case 値 ->
」はフォールスルーしないので、複数の値に対して同じ処理を行う場合はこの記法を使う。
例 | 備考 | |
---|---|---|
従来の記法 |
var day = DayOfWeek.SUNDAY; switch (day) { case MONDAY: case FRIDAY: case SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY: case SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; }; |
|
カンマ区切りの例 |
var day = DayOfWeek.SUNDAY; switch (day) { case MONDAY, FRIDAY, SUNDAY: System.out.println(6); break; case TUESDAY: System.out.println(7); break; case THURSDAY, SATURDAY: System.out.println(8); break; case WEDNESDAY: System.out.println(9); break; }; |
|
switch式(yield)の例 |
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY, FRIDAY, SUNDAY: yield 6; case TUESDAY: yield 7; case THURSDAY, SATURDAY: yield 8; case WEDNESDAY: yield 9; }; System.out.println(value); |
|
switch式(アロー構文)の例 |
var day = DayOfWeek.SUNDAY; var value = switch (day) { case MONDAY, FRIDAY, SUNDAY -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; System.out.println(value); |
|
nullとdefaultの例 [2023-09-23] |
var day = DayOfWeek.SUNDAY; var value = switch (day) { case null, default -> 6; case TUESDAY -> 7; case THURSDAY, SATURDAY -> 8; case WEDNESDAY -> 9; }; System.out.println(value); |
case nullとdefaultラベルをカンマ区切りで一緒に書くことが出来る。(Java21以降) (「 case default, null 」という順序は不可)(defaultラベルはnull以外とは指定不可) |
switch内(case部分)でローカル変数を定義すると、その変数のスコープはswitch内全体となる。
つまり、異なるcaseで同名のローカル変数を定義しようと思っても、出来ない。(重複エラーになる)
switch(s) {
case "a":
int n = 1;
〜
break;
default:
int n = 9; ←変数nが既に定義されているというコンパイルエラー
〜
break;
}
yieldを使うときも同様。
アロー構文の場合はそもそも変数定義を書けない(ひとつの式しか書けない)ので、スコープの話は関係ない。
Java21以降(プレビュー版ではJava17以降)では、caseにデータ型を指定することで、その型にマッチしたcaseが実行される。
static void printPatternSwitch(Object o) { switch (o) { case null: System.out.println("*null*"); break; case Integer i: System.out.printf("int %d%n", i); break; case Long l: System.out.printf("long %d%n", l); break; case Double d: System.out.printf("double %f%n", d); break; case String s: System.out.printf("String %s%n", s); break; default: System.out.println(o); break; }; }
static String formatterPatternSwitch(Object o) { return switch (o) { case null -> "*null*"; case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); }; }
各caseで定義する変数名は、同一switch内で重複しても問題ない。
異なる変数を使っているcaseへのfall throughはコンパイルエラーになる。
switch (o) { case Integer i: // fall through case String s: System.out.println(s); break; default: break; }
Java21以降(プレビュー版ではJava19以降)では、レコードをswitchで指定し、レコードのフィールドを個別の変数として取り出すことが出来る。[2023-09-23]
record Record1(int n) {} record Record2(int n) {}
static void recordTest(Object o) { switch (o) { case Record1(int i): System.out.println("rec1=" + i); break; case Record2(var i): System.out.println("rec2=" + i); break; default: System.out.println("default"); break; } }
caseにレコードのクラス名を書き、その直後を丸括弧で囲んでフィールドから受け取る変数を書く。
データ型にはvarを指定可能。
Java21以降(プレビュー版ではJava17以降)では、パターンマッチした変数を使って、caseに値の条件を記述することが出来る。[2023-09-23]
caseの中に「when」を書き、whenより後に条件を記述する。
static void test(Object o) { switch (o) { case String s when s.length() == 1: System.out.println("1--" + s); break; case String s: System.out.println("n--" + s); break; default: System.out.println("default"); break; } }
なお、「case String s」を「case String s when s.length() == 1」より前に持ってくると、コンパイルエラーになる。
whenより後ろのガード部分は丸括弧で囲むことが出来る。
switch (o) { case String s when (s.length() == 1) -> 1; 〜 }
ラムダ式も「->」を使うので、コンパイルが曖昧になる場合は丸括弧で囲むということらしい。
なお、whenより前のパターンマッチング部分は丸括弧で囲むことは出来ない(コンパイルエラーになる)。
switch (o) { //× case (String s when s.length() == 2) -> 2; //× case (String s) when s.length() == 3 -> 3; 〜 }