Effective Java 第3版を読んだので、本書で紹介されていた全90項目のプラクティスを簡潔にまとめる。
https://www.maruzen-publishing.co.jp/item/?book_no=303408

本記事は第10章「例外」の項目について記載する。

第10章 例外

項目69 例外的状態にだけ例外を使う

例外は例外的条件に対してのみ使うべきであり、通常の処理フローとして使うべきではない。 

項目70 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う

呼び出し元が適切に回復できるような状況の場合、チェックされる例外をスローすべき。

チェックされる例外は、その例外をcatch句か呼び出し元で処理させるかを強制できる。

プログラミングエラーを示す場合、実行時例外(RuntimeException)をスローすべき。

実行時例外は、キャッチされる必要は無く、一般的にはキャッチすべきではない。

そのため、回復が不可能な場合に使用すべきである。

項目71 チェックされる例外を不必要に使うのを避ける

チェックされる例外は信頼性を向上させるが、過剰に使うと、クライアント側に負荷を課すことになる。

なぜなら、メソッドを呼び出すコードは、catch句でスローされる例外に対して処理を行わなければならない。

また、その例外を外側に伝播させなければならない。

メソッドがスローする例外が複数ある場合は、catch句でそれぞれの例外に対して処理を行う必要があるが、
スローされる例外が一つだけであれば、その例外のためだけにcatch句を書くのは冗長であるといえる。

そのため、例外をスローしない代替の技法の採用を検討すべき。

  • 例外相当の場合、空のオプショナルを返却する
  • メソッドを2つに分割して、最初のメソッドで例外相当である場合、falseを返却する

項目72 標準的な例外を使う

Exception、RuntimeException、Throwable、Errorといったクラスは抽象クラスであるかのように利用すること。

標準的な例外を再利用することには、保守性と可読性が向上する利点がある。

また、利用する例外クラスが少ない場合、メモリの節約とクラスのロード時間の短縮の面でパフォーマンス的にも良い。

一般的に再利用される例外は以下のようなものがある。

例外 概要
IllegalArgumentException パラメータ値が不正である。
IllegalStateException メソッド呼び出しに対してオブジェクト状態が不正である。
NullPointerException nullを参照した。
IndexOutOfBoundException インデックス値が範囲がである。
UnsupportedOperationException オブジェクトがメソッドをサポートしていない。

独自の例外クラスを作成するのは、妥当な理由があるときに限る。

項目73 抽象概念に適した例外をスローする

対象のメソッドの処理と関係のない例外をスローした場合、クライアントはスローされた例外の取り扱いについて混乱する。

そのため、上位レイヤは下位レイヤの例外をキャッチして、上位レイヤの抽象概念に合う例外をスローすべきである。

このコードイデオムを、例外翻訳という。

try {
    // 下位レイヤの処理を呼び出し
} catch (LowLevelException exception) {
    throw new HighLevelException();
}

下位レベルの例外を上位レベルで検証する際、下位レベルの例外を上位レベルの例外に組み込むことができる。

また、下位レベルの例外を取得するためのゲッター(getCause)を上位レベルの例外は提供する。

このコードイデオムを、例外連鎖という。

try {
    // 下位レイヤの処理を呼び出し
} catch (LowLevelException exception) {
    throw new HighLevelException(exception);
}
class HighLevelException extends Exception{
    HighLevelException(Throwable cause) {
        super(cause);
    }
}

項目74 各メソッドがスローするすべての例外を文書化する

メソッドを適切に使うため、メソッドがスローする例外の説明を記載することは重要である。

検査例外の一覧とその例外がスローされる条件をJavadocの@throwsタグを使って記載する。

また、メソッドヘッダーにthrows宣言する必要がある。

非検査例外について説明の記載は強制ではないが、検査例外と同様に記載したほうが良い。

クラス内の多くのメソッドが同様の例外を同様の理由でスローする場合、クラスのドキュメンテーションコメントとして記載しても良い。

項目75 詳細メッセージにエラー記録情報を含める

スローした例外がキャッチされなかった場合、システムは自動的にその例外のスタックトレースを表示する。

スタックトレースは、その例外のtoStringメソッドが呼び出される。

エラーを解析するエンジニアにとって、例外のスタックトレースは唯一の情報といってもよい。

そのため、例外のtoStringメソッドがエラーの原因について詳細な情報を返すことは重要である。

エラー原因を特定するために、toStringメソッドは、その例外の原因となった全てのパラメータとフィールドの値を含ませる。

ただし、個人情報やパスワードや暗号鍵といった情報はセキュリティに影響する情報なので含めない。

項目76 エラーアトミック性に努める

オブジェクトを操作するようなメソッドにおいて、処理の中で例外がスローされる場合は、オブジェクトをメソッドの呼び出し前の状態にすべきである。

このような性質を持つメソッドは、エラーアトミックと呼ばれる。

エラーアトミック性を実現するための最も簡単な方法は、利用するオブジェクトを不変なオブジェクトとして設計すること。
オブジェクトが不変であれば、オブジェクトが生成された時点で整合性が取れていて、かつ変更することができないため。

可変オブジェクトを利用するようなメソッドの場合には、オブジェクトの操作を行う前にパラメータに対して検査をする。
オブジェクトの操作前に例外のスローがされるため、オブジェトの状態を変更せずに済む。

事前の検査ができない場合には、オブジェクトのコピーに対して操作を行い、操作が完了した後にオブジェクトの内容をコピーの内容で置き換える方法が良い。
オブジェクト操作がエラーとなったとしても、コピー元のオブジェクトには影響が無い。

エラーアトミック性が守れていない場合は、APIドキュメントにエラー時のオブジェクトの状態について明記すべきである。

項目77 例外を無視しない

メソッドが例外をスローすると宣言されている場合、その例外には意味があるので、利用者はその例外を握りつぶすようなことをするべきではない。

try {
  // 処理
} catch (Exception exception) {
}

例外に対してログの記録や回復処理を書くことは、殆どの場合適切である。

ただし、変更が生じないような処理であった場合は、回復処理は不要なので書く必要はない。

例外を握りつぶすことを選択した場合、その旨をコメントに記載するのとともに、変数名もignoredと命名すべき。

try {
  // 処理
} catch (Exception ignored) {
 // 本例外はオブジェクトの変更に影響が無い
}