解説 · セクション 2

例外処理 (応用版)

JDBC で『SQLException を捕まえて投げ直す』『try-with-resources で確実に閉じる』を未経験者向けに整理します。

読み終わったら問題に挑戦できます → 演習に進む

身近に置き換えると

あなたが「配達員」として荷物を届ける場面を想像してください。配達には会社のトラック(リソース)を使います。もし途中で「通行止め」というトラブル(例外)が起きたら、自分では解決できないので営業所長に「通行止めで進めません」と報告(throws リレー)します。営業所長は、お客様に「配達できませんでした」と謝りますが、その際「原因は通行止めです」と元の理由も添えて伝えます(例外チェーン)。そして、配達が成功しても失敗しても、最後に必ずトラックの鍵は会社に返却しなければなりません(finally / try-with-resources)。

3 行でまとめると

  • データベース接続などの「借りた道具」は、try-with-resources を使って確実に返却(クローズ)する。
  • 自分では対処できない例外は throws で呼び出し元(上司)に報告し、リレーのように受け渡す。
  • 別の例外に変換して投げ直すときは、元の例外を「原因」として含める(例外チェーン)。

図で見ると

flowchart TD
    A[メソッド実行] --> B{例外発生?}
    B -- No --> C[正常終了]
    B -- Yes: SQLException --> D[catchで捕まえる]
    D --> E["新しい例外に原因を含めて投げる
例外チェーン"] C --> F["自動クローズ
try-with-resources"] E --> F F --> G[呼び出し元へ]

正常に終わっても、途中でトラブル(例外)が起きて中断しても、最後に必ず「自動クローズ」が行われるのがポイントです。

順を追って理解する

1. 報告必須のトラブルと、そうでないトラブル (checked vs unchecked)

Javaの例外には、コンパイラが「必ず対処または報告しなさい」と厳しくチェックする checked 例外SQLException など)と、チェックされない unchecked 例外RuntimeException の仲間)があります。RuntimeException 系は、ゼロ割りや「箱が空(Null)」といったプログラムのミスが主なので、わざわざメソッドに「この例外が起きるかも」と宣言(throws)する必要はありません。

2. 自分では対処せずに上司に報告する (throws リレー)

データベースのトラブル(SQLException)が起きたとき、そのメソッド内ではどうしようもないことがあります。その場合は、メソッド名の後ろに throws SQLException と書き、「このメソッドは SQLException を投げる可能性があります」と宣言します。これで、エラーの対処を呼び出し元(上司)に任せる「リレー」ができます。

3. 借りたものは必ず返す (finally と try-with-resources)

データベースとの接続などは、使い終わったら必ず切断(close)しなければなりません。昔は finally ブロックの中に close() を書いて、成功しても失敗しても必ず実行させるのが定石でした。しかし今は try-with-resources という構文を使います。try (借りる処理) { ... } と書くだけで、ブロックを抜けるときに自動で close() してくれます。ただし、これが使えるのは AutoCloseable というインターフェースを持っているクラスだけです。

4. 複数のトラブルを待ち構える (catch の順序)

複数の例外を catch するときは、具体的なクラスを先、共通の親クラスを後 に書くという絶対のルールがあります。たとえば、具体的な SQLException を先に書き、より広い範囲をカバーする Exception を後に書きます。網の目が細かいアミを先に置かないと、すべて粗いアミ(親クラス)に引っかかってしまうからです。

5. 原因を包んで投げ直す (例外チェーン)

SQLException を捕まえた後、「データベースエラー」という事実を、アプリケーション固有の RuntimeException などに変換して投げ直すことがよくあります。このとき、throw new RuntimeException("DBエラー", e); のように、捕まえた元の例外 e を一緒に渡します。これを 例外チェーン と呼び、後でエラーの根本原因(通行止めだったという事実)をたどれるようにする重要なテクニックです。

応用ではこう書く

public class UserRepository {
    // RuntimeException 系は unchecked なので throws 宣言は不要
    public User findById(int id) {
        String sql = "SELECT * FROM users WHERE id = ?";
        
        // try-with-resources 構文 (AutoCloseable なので自動で close される)
        try (Connection conn = Database.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {
            
            stmt.setInt(1, id);
            ResultSet rs = stmt.executeQuery();
            
            if (rs.next()) {
                return new User(rs.getString("name"));
            }
            return null;
            
        } catch (SQLException e) {
            // 具体的な例外 (SQLException) を先に catch する
            // 元の例外 e を渡して例外チェーンを作る (unchecked 例外に変換して投げる)
            throw new RuntimeException("ユーザーの取得に失敗しました", e);
            
        } catch (Exception e) {
            // 共通親 (Exception) は後に書く
            throw new RuntimeException("予期せぬエラーが発生しました", e);
        }
        // finally を書かなくても、Connection や PreparedStatement は自動でクローズされる
    }
    
    // もし自分で処理せずそのまま投げるなら、throws リレーを使う
    public void update() throws SQLException {
        // ...
    }
}

JDBCのコードでは、ConnectionPreparedStatementtry (...) の中で宣言します。これらは AutoCloseable を実装しているため、処理が終わるか例外が発生した時点で自動的に close() が呼ばれ、リソースの閉じ忘れ(close漏れ)を完全に防ぐことができます。

よくある誤解

誤解: catch ブロックの順番は適当でよい

正しくは: catch は上から順に判定されます。もし親クラスである Exception を先に書いてしまうと、すべてそこで捕まってしまい、後に書いた SQLException のブロックには絶対に到達しません。そのため、Javaではコンパイルエラーになります。必ず「具体的な子クラス」から「抽象的な親クラス」の順に書きましょう。

誤解: try-with-resources を使えば、どんなクラスでも自動でクローズされる

正しくは: 自動でクローズされるのは、AutoCloseable インターフェースを実装しているクラス(JDBCの Connection など)だけです。自分で作ったクラスを自動クローズさせたい場合は、implements AutoCloseable と記述し、close() メソッドを実装する必要があります。

つまずいたら

このトピックで詰まったら基礎復習へ: 章 6 例外処理

準備できたら問題に挑戦 → 演習に進む (7 問)