解説 · セクション 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のコードでは、Connection や PreparedStatement を try (...) の中で宣言します。これらは AutoCloseable を実装しているため、処理が終わるか例外が発生した時点で自動的に close() が呼ばれ、リソースの閉じ忘れ(close漏れ)を完全に防ぐことができます。
よくある誤解
誤解: catch ブロックの順番は適当でよい
正しくは: catch は上から順に判定されます。もし親クラスである Exception を先に書いてしまうと、すべてそこで捕まってしまい、後に書いた SQLException のブロックには絶対に到達しません。そのため、Javaではコンパイルエラーになります。必ず「具体的な子クラス」から「抽象的な親クラス」の順に書きましょう。
誤解: try-with-resources を使えば、どんなクラスでも自動でクローズされる
正しくは: 自動でクローズされるのは、AutoCloseable インターフェースを実装しているクラス(JDBCの Connection など)だけです。自分で作ったクラスを自動クローズさせたい場合は、implements AutoCloseable と記述し、close() メソッドを実装する必要があります。
つまずいたら
このトピックで詰まったら基礎復習へ: 章 6 例外処理