複数の catch ブロック

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • 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 は算術演算 (主に整数のゼロ除算) の失敗を、NullPointerExceptionnull 参照の使用を表します。これらをまとめて 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 を見ればゼロ割りだと判別できます。

戻り値で例外種類を区別するのは、本格的な業務コードでは OptionalResult 型、独自例外を使う方が綺麗ですが、まずは「区別する」感覚を int の符号で掴むのが入門としては手堅いやり方です。

catch の順序は「具体 → 抽象」

複数の catch を並べるときに 絶対に守らないといけないルール が、上から順に「具体的 (サブクラス) → 抽象的 (スーパークラス)」の順で書く、という原則です。NumberFormatExceptionIllegalArgumentException を継承しており、さらにそれは RuntimeExceptionExceptionThrowable という階層になっています。

diagram (will load when visible)

もし Exception を上に書いてしまうと、続く NumberFormatExceptioncatch には永遠に到達できず、コンパイラが 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-1ArithmeticException-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 がなぜ便利なのかも腹落ちします。

diagram (will load when visible)

上の図は、今回の safeDivide の制御フローです。どのルートを通っても finally には必ず立ち寄り、最後に return で抜ける、という構造を視覚的に押さえておきましょう。

safeDivide を書いてみる

課題の Solution.safeDivide(String a, String b) は次の仕様です。

  • abInteger.parseIntint にパースして、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 / yArithmeticException が投げられ、下の catch で受けて -2 を返します。例外が起きなければ通常通り x / y の結果を返します。

ここで「int 同士の割り算ではゼロ除算で例外が出る」ことを覚えておいてください。double 同士なら 1.0 / 0.0Infinity という特殊な値になり例外は出ません。整数と浮動小数で挙動が違うのは 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 を書くと、trycatch 内で書いた return上書きされて消える という非直感的な挙動になります。finally には後始末の処理だけを書き、returntry/catch 側にまとめておくのが安全です

もう 1 つ、catch (NumberFormatException | ArithmeticException e) のように multi-catch で書くときに、e = new RuntimeException(); のように 代入 しようとするとコンパイルエラーになることも覚えておきましょう。multi-catch の例外変数は暗黙的に final 扱いです。普通は中身を書き換えないので問題になりませんが、たまにレガシーコードで見かけて驚くことがあります。

どうしても再 throw したいときは、throw e; (元の例外をそのまま投げる) か、throw new MyException("わかりやすいメッセージ", e); のように 原因例外を包んだ 別の例外を投げるのが定石です。e を直接書き換える機会はほぼありません。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • ファイル + ネットワークの両方を catchcatch (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 を使う。

やってみよう

それでは課題に取りかかりましょう。やることは次のとおりです。

  1. Solution.safeDivide(String a, String b)try-catch で書く
  2. Integer.parseInt でパースしてから割り算し、結果を return する
  3. NumberFormatExceptioncatch-1 を返す
  4. ArithmeticExceptioncatch-2 を返す
  5. 順序は 具体 → 抽象 で並べる (今回はどちらも RuntimeException の直接の子クラスなので、どちらが先でも到達可能ですが、習慣としてエラーの発生順=パース→割り算の順に書くのがおすすめ)

テストは safeDivide("10", "2")5safeDivide("10", "0")-2safeDivide("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 つができることを確認しましょう。

  1. 複数 catch の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. 複数 catch とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. クラス名は Solution、メソッド名は safeDivide、引数は String a, String b、戻り値の型は int
  2. try の中で Integer.parseInt を 2 回呼んでから割り算を行い、NumberFormatException を捕まえる catch-1 を返すこと
  3. もう 1 つの catchArithmeticException を捕まえて -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

ヒント

main.java
main.java
学習モード

メモ

複数の catch ブロック

⌘S で保存