複数の catch ブロック
このレッスンで分かること
catch (A | B e)で複数の例外を 1 つの catch でまとめて受ける (multi-catch)|で並べた例外型はサブクラス関係にあってはいけない- ブロック内の
eは共通の親型として扱われる- 個別に処理を分けたいなら従来通り
catchを縦に並べる
複数の catch ブロック とは
Java の
try-catchで複数の例外を別々に処理する書き方と、multi-catch 構文・finallyの役割を、文字列割り算メソッドを通して学ぼう。
1 つの try で複数の例外を捌く
ここまでに学んできた try-catch は、Exception e のようにすべての例外をひとつのブロックで受け止める書き方が中心でした。実務に踏み込むと、ひとつの処理の中で性質の異なる例外が複数発生することはむしろ普通です。たとえばユーザーから受け取った文字列を int にパースしてから割り算する処理では、パース失敗 (NumberFormatException) とゼロ除算 (ArithmeticException) という、まったく別の原因のエラーが起こり得ます。これらを「ひと括りに Exception e で受け止めて全部 -1 を返す」では、呼び出し側が「パース失敗だったのかゼロ割りだったのか」を判別できません。
例外処理は「落とさないこと」だけが目的ではありません。何が起きたかを正しく区別して、上位に伝えることこそが本質です。1 つの
tryの中で複数のcatchを書き分けるのは、その第一歩です。
本レッスンでは、複数の catch を並べる基本形、Java 7 以降の multi-catch 構文、そして finally の役割までを、Solution.safeDivide(String a, String b) を題材に丁寧に追っていきます。
なぜ複数の catch が必要なのか
例外には「ジャンル」があります。NumberFormatException は数値変換の失敗を、ArithmeticException は算術演算 (主に整数のゼロ除算) の失敗を、NullPointerException は null 参照の使用を表します。これらをまとめて Exception で受けてしまうと、コードからは「何が起きてもとりあえずカバー」しているように見えても、原因に応じた違う処理 (たとえばパース失敗ならユーザーに 入力が数値ではありません と伝え、ゼロ割りなら 0 では割れません と伝える) が書けません。
Java
try {
int x = Integer.parseInt(a);
int y = Integer.parseInt(b);
return x / y;
} catch (NumberFormatException e) {
return -1; // パース失敗
} catch (ArithmeticException e) {
return -2; // ゼロ除算
}上のコードでは、パース時に投げられる NumberFormatException と割り算で投げられる ArithmeticException を、別の catch で別の戻り値にマッピングしています。呼び出し側は -1 を見ればパース失敗、-2 を見ればゼロ割りだと判別できます。
戻り値で例外種類を区別するのは、本格的な業務コードでは
OptionalやResult型、独自例外を使う方が綺麗ですが、まずは「区別する」感覚をintの符号で掴むのが入門としては手堅いやり方です。
catch の順序は「具体 → 抽象」
複数の catch を並べるときに 絶対に守らないといけないルール が、上から順に「具体的 (サブクラス) → 抽象的 (スーパークラス)」の順で書く、という原則です。NumberFormatException は IllegalArgumentException を継承しており、さらにそれは RuntimeException → Exception → Throwable という階層になっています。
もし Exception を上に書いてしまうと、続く NumberFormatException の catch には永遠に到達できず、コンパイラが exception has already been caught というエラーで止めてくれます。Java は親切なのでビルド時点で気付けますが、初学者は「とにかく上に大きい網を張りたい」気持ちでよく逆順に書いてしまうので注意しましょう。
multi-catch 構文で重複を減らす
複数の例外に対して 同じ処理をしたい 場合、Java 7 以降では | (パイプ) を使ってまとめられます。これを multi-catch 構文と呼びます。
Java
try {
riskyOperation();
} catch (NumberFormatException | ArithmeticException e) {
System.err.println("想定済みの失敗: " + e.getMessage());
return -1;
}multi-catch の中の変数 e は、列挙した例外型の共通の親 (この場合は RuntimeException) として扱われます。e.getMessage() のような共通メソッドは呼べますが、NumberFormatException 固有のメソッドは直接は呼べません。戻り値や処理内容が同じなら multi-catch、違うなら別々の catch という使い分けを覚えてください。
今回の課題は NumberFormatException で -1、ArithmeticException で -2 と戻り値が違うので、multi-catch ではなく従来の「catch を 2 つ並べる」書き方を使います。multi-catch は知識として押さえつつ、課題では適切な形を選ぶ、という感覚を養いましょう。
Java 8 以前のレガシーコードでは
catchブロックの中身をほぼコピペで並べることがよくありました。コードレビューでmulti-catch にできますという指摘が入ったら、迷わずまとめる方向に直すのが今風の書き方です。
finally で「必ず通る道」を作る
try-catch には finally という第三のブロックがあります。finally は 例外が起きても起きなくても必ず実行されるブロック で、ファイルや DB 接続の close() など、後始末の処理を書く場所として使われます。
Java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileDemo {
public static String readFirstLine(String path) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
return br.readLine();
} catch (IOException e) {
return null;
} finally {
if (br != null) {
try { br.close(); } catch (IOException ignore) { /* もみ消し */ }
}
}
}
}このように、finally 内で close() を呼ぶことで、例外が起きてもファイルハンドルがリークしません。Java 7 以降は try-with-resources 構文 (try (BufferedReader br = ...)) でもっと綺麗に書けますが、finally の存在理由を理解しておくと、try-with-resources がなぜ便利なのかも腹落ちします。
上の図は、今回の safeDivide の制御フローです。どのルートを通っても finally には必ず立ち寄り、最後に return で抜ける、という構造を視覚的に押さえておきましょう。
safeDivide を書いてみる
課題の Solution.safeDivide(String a, String b) は次の仕様です。
aとbをInteger.parseIntでintにパースして、a / bを返す- パースに失敗したら (
NumberFormatException)-1を返す - ゼロで割ろうとしたら (
ArithmeticException)-2を返す
素直に書くとこうなります。
Java
public class Solution {
public static int safeDivide(String a, String b) {
try {
int x = Integer.parseInt(a);
int y = Integer.parseInt(b);
return x / y;
} catch (NumberFormatException e) {
return -1;
} catch (ArithmeticException e) {
return -2;
}
}
}Integer.parseInt("abc") の時点で NumberFormatException が投げられるので、上の catch で受け止めて -1 を返します。y = 0 のときは、続く x / y で ArithmeticException が投げられ、下の catch で受けて -2 を返します。例外が起きなければ通常通り x / y の結果を返します。
ここで「
int同士の割り算ではゼロ除算で例外が出る」ことを覚えておいてください。double同士なら1.0 / 0.0はInfinityという特殊な値になり例外は出ません。整数と浮動小数で挙動が違うのは Java の特徴です。
よくある間違い
try-catch 周りでよく踏むワナを 3 つ紹介します。
Exceptionを一番上に書いて到達不可コードを作る —catch (Exception e)を最初に置き、その下にcatch (NumberFormatException e)を書くと、後者には永遠に到達しません。Java はビルドエラー (exception has already been caught) で止めてくれますが、原因がピンと来ないと数十分溶かしがちです。順序は 具体 → 抽象 が鉄則ですcatchでもみ消して原因が分からなくなる —catch (Exception e) { /* 何もしない */ }のようにブロックを空にすると、例外が握り潰されて呼び出し側が異常に気付けません。最低でもSystem.err.println(e)でログを出すか、別の例外で再throwするのが鉄則ですfinallyの中でreturnしてしまう —finallyの中でreturnを書くと、tryやcatch内で書いたreturnが 上書きされて消える という非直感的な挙動になります。finallyには後始末の処理だけを書き、returnはtry/catch側にまとめておくのが安全です
もう 1 つ、catch (NumberFormatException | ArithmeticException e) のように multi-catch で書くときに、e = new RuntimeException(); のように 代入 しようとするとコンパイルエラーになることも覚えておきましょう。multi-catch の例外変数は暗黙的に final 扱いです。普通は中身を書き換えないので問題になりませんが、たまにレガシーコードで見かけて驚くことがあります。
どうしても再
throwしたいときは、throw e;(元の例外をそのまま投げる) か、throw new MyException("わかりやすいメッセージ", e);のように 原因例外を包んだ 別の例外を投げるのが定石です。eを直接書き換える機会はほぼありません。
実務で遭遇するパターン
中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。
- ファイル + ネットワークの両方を catch ─
catch (IOException | TimeoutException e)で 1 つのリトライ処理に集約 - パース系のまとめ受け ─
catch (NumberFormatException | DateTimeParseException e)でバリデーションエラーに変換 - 外部 API のエラー ─ HTTP のリトライ対象例外を 1 ヶ所でまとめる
- 例外を再 throw ─ 複数例外を 1 つのアプリ例外に集約してから throw
コードレビューで指摘されがちなポイント
PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。
catch (Exception e)の代わりに multi-catch ─ 広く受けるのを避けるための適切な使い方ならOK- サブクラス関係を並べている ─
catch (IOException | FileNotFoundException e)はコンパイルエラー - 順序を間違える ─ 個別 catch では狭い型を先、
Exceptionを後ろにする eの型が暗黙に親型 ─ 中でe.specificMethod()を呼ぶとコンパイルエラー、設計を見直す
パフォーマンス考慮事項
- コンパイラが個別 catch と同等のバイトコード生成 ─ パフォーマンス差はゼロ
- コード行数が減る ─ 同じ処理を 3 回書かなくて済む
stacktraceの生成コストは変わらない ─ catch 側ではなく throw 側で発生- ログ集計しやすい ─ 同じハンドラに集まるので、共通ログフォーマットに集約できる
ここまでの要点
catch (A | B e) で複数例外をまとめて受ける。サブクラス関係は不可、e は共通親型。処理を分けたいなら別 catch を使う。
やってみよう
それでは課題に取りかかりましょう。やることは次のとおりです。
Solution.safeDivide(String a, String b)をtry-catchで書くInteger.parseIntでパースしてから割り算し、結果をreturnするNumberFormatExceptionのcatchで-1を返すArithmeticExceptionのcatchで-2を返す- 順序は 具体 → 抽象 で並べる (今回はどちらも
RuntimeExceptionの直接の子クラスなので、どちらが先でも到達可能ですが、習慣としてエラーの発生順=パース→割り算の順に書くのがおすすめ)
テストは safeDivide("10", "2") で 5、safeDivide("10", "0") で -2、safeDivide("abc", "2") で -1 のような呼び出しを 4 件以上行います。finally ブロックを書く必要はありませんが、書いてみてどんな順序で実行されるかを System.out.println で確認するのも理解を深めるのに役立ちます。
慣れてきたら、第二引数が "xyz" のときの戻り値はどうなるか (パースが先に失敗するので -1)、"10" と "-2" ならどうか (普通に -5 が返る)、などを手元で予想してから実行してみましょう。例外の発生する順序とそれを受ける catch の対応が見えてくると、try-catch 全体の挙動がぐっと身近になります。
よくある質問
Q. 中級の内容は実務でどれくらい使いますか?
A. Collection(List/Map/Set)、Stream、例外処理、ジェネリクスは毎日のように登場します。Date/Time API、Files、try-with-resources も実務で頻出するため、本コースの内容は実プロジェクトでそのまま役立ちます。
Q. Stream と for ループはどっちで書くべき?
A. 可読性で選んでください。filter + map + collect が綺麗にハマるなら Stream、副作用や複雑な分岐が多いなら for ループの方が読みやすいです。一律にどちらを使うべきという正解はなく、チームのコーディング規約に合わせるのが現実的です。
Q. 次のステップでは何を学ぶべきですか?
A. 中級の基礎が固まったら、Spring Boot で Web API を作る、JUnit でテストを書く、Maven/Gradle でビルドを管理する、といった実プロジェクトのスキルに進むと効果的です。OSS のコードを読む経験も大きく成長を促します。
次のレッスン
次は throws と例外の伝播 で、throws と例外の伝播 を学びます。
事前確認 — 進む前に次の 3 つができることを確認しましょう。
- 複数 catch の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. 複数 catch とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- クラス名は
Solution、メソッド名はsafeDivide、引数はString a, String b、戻り値の型はint tryの中でInteger.parseIntを 2 回呼んでから割り算を行い、NumberFormatExceptionを捕まえるcatchで-1を返すこと- もう 1 つの
catchでArithmeticExceptionを捕まえて-2を返すこと。Exceptionでひと括りにせず、種類ごとに別々のcatchで書き分けること
入出力例
test-cases.txt
safeDivide("10", "2") → 5
safeDivide("10", "0") → -2
safeDivide("abc", "2") → -1
safeDivide("10", "xyz") → -1
safeDivide("-10", "2") → -5
safeDivide("7", "2") → 3