導入
Javaでの例外処理において、最も避けるべきは「例外情報の欠落」です。特にリソースのクローズ処理中に別の例外が発生した場合、元の例外が上書きされて消えてしまうことがあります。これを防ぎ、複数の例外を適切に管理するために欠かせないのが、Java 7で導入されたtry-with-resourcesと、それに付随するThrowable.getSuppressed()です。本記事では、この仕組みを深く理解し、実務で堅牢なコードを書くための手法を解説します。
基礎知識
まず、Javaの例外階層(Throwable)において、すべてのエラーの頂点にはjava.lang.Throwableが存在します。その下にjava.lang.Errorとjava.lang.Exceptionがあります。
従来、finallyブロック内でリソースをcloseする際、tryブロック内の例外とclose時の例外が同時に発生すると、後の例外によって先のエラーが隠蔽(抑制)されていました。
この問題を解決するために導入されたのが「抑制された例外(Suppressed Exceptions)」という概念です。try-with-resources文を使用すると、自動クローズ時に発生した例外は、主となる例外に「抑制された例外」として付加されます。これを取得するメソッドがThrowable.getSuppressed()です。
実装/解決策
実装の基本は、AutoCloseableを実装したリソースをtry-with-resources文で宣言することです。これにより、コンパイラが自動的に例外の抑制処理を生成してくれます。もし、複雑な独自処理の中で例外をハンドリングする必要がある場合は、catchブロック内でgetSuppressed()を呼び出し、配列として取得した例外をログ出力するなどの対応を行います。
サンプルプログラム
以下のコードは、リソースのクローズ時に意図的に例外を発生させ、抑制された例外がどのように取得されるかを示した実用的な例です。
import java.io.IOException;
public class SuppressedExceptionExample {
// AutoCloseableを実装したテスト用クラス
static class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
// クローズ時に例外をスロー
throw new IOException("クローズ中に発生した例外");
}
}
public static void main(String[] args) {
try (MyResource res = new MyResource()) {
// tryブロック内で別の例外を発生させる
throw new RuntimeException("メイン処理の例外");
} catch (Exception e) {
System.err.println("キャッチした例外: " + e.getMessage());
// 抑制された例外を取得して表示
Throwable[] suppressed = e.getSuppressed();
for (Throwable t : suppressed) {
System.err.println("抑制された例外: " + t.getMessage());
}
}
}
}
応用・注意点
現場での開発において注意すべき点は以下の通りです。
1. 手動の例外追加:
getSuppressed()は読み取り専用ではありません。Throwable.addSuppressed(Throwable exception)メソッドを使うことで、独自のロジックで例外を付加することも可能です。ただし、例外の因果関係(getCause)と混同しないよう注意してください。
2. ログ出力の設計:
本番環境では、抑制された例外を見落としがちです。ログライブラリ(SLF4JやLog4j2など)を使用する場合、例外オブジェクトをそのまま渡せば、多くの場合フレームワークが自動的にgetSuppressed()の内容も展開してくれます。しかし、独自で文字列連結してログを出す場合は、明示的にループ処理で抑制された例外を追記する設計にすることをおすすめします。
3. レガシーコードとの共存:
古いJavaバージョンや、finallyでの手動クローズが残っているコードでは、この仕組みは機能しません。リファクタリングの際は、可能な限りtry-with-resourcesへの移行を優先してください。

コメント