マルチ catch を一言でいうと
マルチ catch は、
「複数の例外型を、同じ 1 つの catch ブロックでまとめて処理できる構文」です。
catch (IOException | SQLException e) のように、| で例外型を並べて書きます。
「この 2 つ(あるいはそれ以上)の例外は、同じ扱いでいいや」というときに、
重複した catch ブロックを書く必要がなくなります。
まずは「マルチ catch がない世界」を見てみる
従来の書き方:例外ごとに同じような catch が並ぶ
マルチ catch が導入される前(Java 7 より前)は、
複数の例外を同じように処理したくても、こう書くしかありませんでした。
try {
doSomething(); // IOException も SQLException も投げるかもしれない
} catch (IOException e) {
log.error("失敗しました", e);
throw new RuntimeException(e);
} catch (SQLException e) {
log.error("失敗しました", e);
throw new RuntimeException(e);
}
Javacatch の中身が ほぼコピペ ですよね。
例外型だけ違って、やっていることは同じ。
こういう「中身が同じ catch が並ぶ」コードは、
- 読みにくい
- 修正漏れが起きやすい(片方だけ直してもう片方を忘れる)
という問題を抱えています。
マルチ catch の基本構文
| で例外型を並べる
同じ処理でよい例外をまとめて扱いたいとき、
マルチ catch を使うとこう書けます。
try {
doSomething();
} catch (IOException | SQLException e) {
log.error("失敗しました", e);
throw new RuntimeException(e);
}
Javacatch (IOException | SQLException e) を日本語にすると、
「IOException か SQLException のどちらかが飛んできたら、
それを e という変数で受けて、このブロックで処理する」
という意味です。
ポイントは、
|で例外型を並べる- 変数名(ここでは
e)は 1 つだけ - ブロックの中では、その変数を「共通の親型」として扱う
ということです。
マルチ catch の「型」のイメージ
変数 e の型はどう見なされるか
catch (IOException | SQLException e) {
...
}
Javaこのとき、e の静的型は「2 つの例外型の共通の親クラス」です。IOException と SQLException の共通の親は Exception なので、
コンパイラ的には「e は Exception として扱える」と考えられます。
そのため、ブロックの中では、IOException や SQLException に特有のメソッドは呼べませんが、Exception として共通の操作(getMessage() など)は問題なく使えます。
catch (IOException | SQLException e) {
System.out.println(e.getMessage()); // OK(Exception のメソッド)
// e.getSQLState(); // コンパイルエラー(SQLException にしかないメソッド)
}
Java「マルチ catch の変数は、“列挙した例外型の共通の親”として扱われる」
という感覚を持っておくと、挙動が理解しやすくなります。
マルチ catch を使うときの制約と注意点
共通の親子関係にある型は一緒に書けない
例えば、次のようなマルチ catch はコンパイルエラーになります。
catch (IOException | Exception e) {
...
}
JavaIOException は Exception のサブクラスなので、
「Exception を捕まえる」と書いた時点で IOException も含まれてしまいます。
つまり、
- 親クラスとそのサブクラスを同じマルチ catch に並べるのは NG
- 「どうせ全部
Exceptionで受けるなら、Exceptionだけ書けばいいよね?」
という話になります。
マルチ catch に並べるのは、
「兄弟関係にある例外型(共通の親を持つが、互いに親子ではない)」
にするのが基本です。
変数を再代入できない(実質 final)
マルチ catch の変数 e は、暗黙に「実質 final」です。
catch (IOException | SQLException e) {
// e = new IOException(); // コンパイルエラー
}
Javaこれは、マルチ catch に限らず、
「複数の例外型をまとめて扱うときに、変数を別の例外に差し替えるのは危険」
という考え方に基づいています。
マルチ catch が「効く」典型的な場面
例1:ログを出してラップして投げ直すだけのとき
さきほどの例のように、
「ログを出して、共通のラップ例外に包んで投げ直す」
というパターンは、マルチ catch のド定番です。
try {
doSomething();
} catch (IOException | SQLException e) {
log.error("処理に失敗しました", e);
throw new RuntimeException("処理失敗", e);
}
Javaもしこれを個別の catch で書くと、
ほぼ同じコードが 2 回出てきてしまいます。
マルチ catch によって、
- 「この 2 つの例外は、アプリケーションとしては同じ扱いでよい」
- 「違いを意識する必要はない」
という設計意図を、コードで表現できます。
例2:入力チェック系の例外をまとめて扱う
例えば、ある処理の中で
NumberFormatException(文字列を数値に変換できない)DateTimeParseException(文字列を日付に変換できない)
のどちらかが飛ぶ可能性があり、
どちらも「入力フォーマットエラー」として扱いたい場合。
try {
parseInput(input);
} catch (NumberFormatException | DateTimeParseException e) {
throw new IllegalArgumentException("入力フォーマットが不正です: " + input, e);
}
Java「どの例外が飛んだかの細かい違いは気にせず、
ユーザーには“入力が不正”とだけ伝えたい」
というときに、マルチ catch はとても自然です。
マルチ catch を使うかどうかの判断基準
自分にこう問いかけてみる
マルチ catch を使うか迷ったら、
その catch に対して自分にこう聞いてみてください。
「この例外たちを、“アプリケーションとしては同じ扱い”にしてよいか?」
もし答えが「はい」なら、マルチ catch の候補です。
逆に、
- 例外ごとにログメッセージを変えたい
- 片方はリトライしたいが、もう片方は即座に失敗にしたい
- 片方はユーザーに見せるメッセージを変えたい
といった「意味の違い」があるなら、
無理にマルチ catch にまとめず、
個別の catch に分けた方が読みやすくなります。
「とりあえず全部まとめる」は危険
マルチ catch が便利だからといって、
catch (IOException | SQLException | RuntimeException | Exception e) {
...
}
Javaのように、何でもかんでも突っ込むのは危険です。
それをやると、
- どの例外がどこから来たのか分かりにくくなる
- 本来分けて扱うべき例外まで、雑に一緒くたにされてしまう
という状態になります。
マルチ catch はあくまで、
「意味的に同じ扱いでよい例外だけをまとめる」
ための道具だと意識しておくと、設計が崩れにくくなります。
まとめ:マルチ catch を自分の言葉で説明するなら
あなたの言葉でマルチ catch を説明すると、こうなります。
「マルチ catch は、catch (A | B e) のように書いて、
複数の例外型を 1 つの catch ブロックでまとめて処理できる構文。
型チェックと変数は 1 回だけ書けばよく、
中身が同じ catch をコピペで並べる必要がなくなる。
変数 e は“列挙した例外型の共通の親型”として扱われ、
親子関係にある型同士を同じマルチ catch に並べることはできない。
『アプリケーションとして同じ扱いでよい例外だけをまとめる』
という意識で使うと、コードが短くなるだけでなく、
例外設計の意図もはっきり伝わる。」

