はじめに
JavaでJDBCを扱う際、古いサンプルコードでは必ずと言っていいほど if (ps != null) ps.close(); という記述が登場します。
「Connectionが確立していれば、prepareStatementがnullを返すことはないのでは?」
「単体テストでこのnullパスを通すのは不可能に近いのでは?」
こうした疑問を、Javaの言語仕様(JLS)と公式APIドキュメントをベースに解消します。
1. なぜ「nullチェック」という慣習が生まれたのか
結論から言うと、Java 6以前の try-catch-finally 構造における「二次災害(NPE)」を防ぐための防御策でした。
例外発生時の挙動(言語仕様)
PreparedStatement ps = null;
try {
// ここで例外(SQLException等)が発生すると...
ps = conn.prepareStatement(sql);
// ↑ この代入(ps = ...)自体が行われない!
} finally {
// psは初期値の null のままここに到達する
ps.close(); // ここで NullPointerException が発生し、元の例外が書き換わってしまう
}
2. 単体テストにおける「カバレッジ地獄」
このガード句を律儀に書くと、単体テスト(JUnit等)で以下の問題に直面します。
-
再現の困難さ: JDBCドライバが正常なら、
connが有効な限りprepareStatementはインスタンスを返します。 -
不自然なモック: 「例外を投げずに
nullを返す」という、実際のドライバではあり得ない挙動を Mockito 等で無理やり作る必要があります。
「起こり得ないパターンのために、テストコードを複雑にする」 のは保守性の観点からアンチパターンと言えます。
3. 現代の正解:try-with-resources
Java 7で導入された try-with-resources 構文を使えば、これらの問題は根本から解消されます。
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 実行処理
} catch (SQLException e) {
// 例外処理
}
仕様上のメリット
・暗黙のnullチェック: コンパイル時に生成されるコードには、自動的に if (ps != null) 相当のチェックが含まれます。
・カバレッジの適正化: 開発者が if 文を書かないため、テスト不能な分岐に悩まされることがなくなります。
4. トランザクション制御(ロールバック)との共存
「try-with-resources は自動クローズされるから、明示的なロールバックができないのでは?」という懸念がありますが、リソースの宣言順序を理解すれば安全に記述可能です。
try (Connection conn = getConnection()) {
conn.setAutoCommit(false);
// Connectionを活かしたまま、Statementをtry-with-resourcesで管理
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.executeUpdate();
conn.commit();
} catch (SQLException e) {
conn.rollback(); // psは閉じられるが、connはまだ生きているのでロールバック可能!
throw e;
}
}
クローズの順序(言語仕様)
JLS(Java Language Specification)により、リソースは 「宣言された順序の逆」 でクローズされます。
上記コードでは ps → conn の順で閉じられるため、catch ブロック内では conn はまだ有効です。
5. 結論
・PreparedStatement の null チェックは、古い finally 構文特有の課題に対する回答だった。
・現代の Java では try-with-resources を使うことで、この不毛なチェックとテスト地獄から解放される。
・トランザクション制御も、リソースの入れ子構造を理解すれば安全に実装できる。
参考文献
・Oracle Java SE Documentation : try-with-resources
・Connection (Java Platform SE 8)