Javaのチェック例外を、境界と回復可能性で設計する
結論
チェック例外を使うかどうかは、「呼び出し側へ回復処理を要求する契約か」で決めます。公開メソッドで一律に禁止する、あるいは下位層の例外をそのまま公開する、という決め方はしません。
public Report loadReport(Path path) throws ReportFormatException {
// 呼び出し側が別ファイルの選択や入力修正を行える
}
層をまたぐときは、その層の言葉へ例外を翻訳し、元の原因を保持します。
困る場面
Java では RuntimeException と Error の派生クラスが非チェック例外で、それ以外の Throwable 派生クラスがチェック例外です。これは言語仕様上の分類です。
API 設計で迷うのは、失敗を型として強制的に処理させる価値があるかどうかです。たとえば、ファイル未検出は利用者に別ファイルを選ばせられる一方、プログラム内部の不変条件違反は通常その場で回復できません。
仕組み
チェック例外は、throws 宣言と catch-or-declare 規則によって呼び出し側へ伝播します。
catch する場所は、例外の種類ではなく、具体的な対応を取れる場所です。
選択の理由
| 条件 | 選択の候補 |
|---|---|
| 呼び出し側が通常の分岐として回復できる | チェック例外、結果型 |
| プログラムの契約違反 | 非チェック例外 |
| 値がないことが通常の結果 | Optional |
| 失敗理由を列挙し、合成したい | 専用の結果型 |
チェック例外にすると呼び出し側の対応は可視化されますが、回復方法がないのに形式的な catch を増やすこともあります。効果と負担を API ごとに見ます。
たとえば、利用者が別のファイルを選び直せる ReportFormatException や、入力を修正して再実行できる ImportException には、チェック例外として対応を要求する余地があります。一方、内部状態が設計上あり得ない値になった場合や、呼び出し側に回復手段がない場合は、IllegalStateException などの非チェック例外で失敗を伝える方が自然です。どちらも名前だけで決めず、呼び出し側が実行可能な対応を持つかで判断します。
実装例
インフラ層の例外をアプリケーション層の意味へ変換する例です。
public Contract loadContract(ContractId id) {
try {
return repository.findById(id)
.orElseThrow(() -> new ContractNotFoundException(id));
} catch (DataAccessException e) {
throw new ContractLoadException(id, e);
}
}
複数件を処理し、失敗を最後にまとめる必要があるなら、原因を捨てません。
var failures = new ArrayList<Exception>();
for (Path path : paths) {
try {
importFile(path);
} catch (ImportException e) {
failures.add(e);
}
}
if (!failures.isEmpty()) {
var summary = new BatchImportException(failures.size());
failures.forEach(summary::addSuppressed);
throw summary;
}
運用とレビュー
- catch した場所で、再試行、代替、通知、変換のどれを行うか明示する
-
throw new RuntimeException(e)だけを機械的に増やさない - ログ出力と再送出を両方行い、同じ失敗を重複記録しない
- Spring MVC などの境界では、例外を HTTP 応答へ変換する責任を集約する
判断を変える条件
公開ライブラリへチェック例外を追加すると、利用者のソース互換性へ影響します。内部アプリケーションより慎重に設計します。逆に閉じたバッチ処理なら、結果を集約する専用型で運用しやすくなる場合があります。
確認方法
- 呼び出し側に実行可能な回復手段があるか
- 例外名が下位ライブラリではなく業務上の失敗を表すか
- 原因例外が
causeまたはsuppressedとして残るか - 正常系、失敗系、再試行時をテストできるか
チェック例外と非チェック例外の分類は、参照する Java SE 版の Java 言語仕様(JLS)11 章で確認できます。Java SE仕様一覧
まとめ
チェック例外は善悪ではなく、呼び出し側へ回復を要求する契約です。回復できる境界で捕捉し、層をまたぐときは意味を翻訳し、原因を失わないようにします。
Discussion