Effective Javaを勉強します【第9章】

項目57. 例外的状態にだけ例外を使用する

例外は、例外的条件に対してのみ使用すべきであり、通常の制御フローに対しては、 決して使用すべきではない。

例えば、パフォーマンス改善を目的として以下のような例外を利用したループを書く人がいる。しかしパフォーマンスが改善されることはない上、可読性が低下し、本来キャッチしなければならない悪い例外を見逃す原因となるため、この書き方は決してしてはならない。

try {
    int i = 0;
    while(true) {
        range[i++].climb();
    }
} catch(ArrayIndexOutOfBoundsException e) {
}

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

Java には、次の通り3種類の例外がある。

  • チェックされる例外
  • 実行時例外
  • エラー

それぞれの使い方について、一般的なルールを紹介する。

チェックされる例外

呼び出し側が適切に回復できるような状況に対しては、チェックされる例外を使用する。 逆に言えば、チェックされる例外がAPIからスローされるということは、APIの設計者はその状況から回復することを要求している。 したがって、チェックされる例外を無視する(catch 節で何も処理しない)ことは、基本的には良いことではない。

実行時例外とエラー

どちらも一般に回復が不可能で、実行を継続すべきでないような場合に用いる。 このような時は、適切なエラーメッセージを表示し、スレッドを停止させる。

実行時例外

実行時例外は、ArrayIndexOutOfException など、事前条件違反などによるプログラミングエラーに用いる。

エラー

エラーはJVMが実行を継続できない資源不足、不変式エラーなど、JVMが使用するのに予約されているという慣例がある。 逆に言えば、実装するすべてのチェックされない例外は RuntimeException をサブクラス化すべき。(エラーは用いない)

項目59. チェックされる例外を不必要に使用するのを避ける

チェックされる例外は、プログラマに例外状態の処理を強制し信頼性を大きく向上させるが、その分プログラマの負荷にもなる。

例外処理の負荷が正当化されるのは、APIの適切な使用で例外状態を防ぐことができず、かつ、 そのAPIを使用しているプログラマが例外に対して有用な処理をすることができる場合である。

上記以外の場合では、チェックされない例外を用いる方が適切であると言える。

以下は、チェックされる例外の不適切な使用例である。(プログラマはこれ以上の有用な処理ができない)

try {
    obj.action(args);
} catch (TheCheckedException e) {
    e.printStackTrace();
    System.exit(1);
}

チェックされる例外をチェックされない例外に変更するための方法として、 例外をスローするメソッドを、例外がスローされるかを判断する boolean を返すメソッドと 正常系の処理をするメソッドの2つに分割する方法がある。

上記のチェックされる例外の不適切な使用例は、以下のように修正することになる。

if(obj.actionPermitted(args)) {
    obj.action(args);
} else {
    // 例外状態を処理する
    ...
}

ただし上記は、外部要因により actionPermittedaction の間にオブジェクトの状態が 変化しない場合に限られるため注意すること。

項目60. 標準例外を使用する

Java は汎用的ないくつかのチェックされない例外(標準例外)を提供している。

標準例外を利用することで、APIの学習コストが下がる、 見慣れない例外を利用する場合と比較してコードの可読性が向上する、 例外クラスが少なくなることでメモリの削減とクラスのロード時間の短縮が期待できる、 などのメリットがある。

次に示すのは、利用頻度の高い標準例外である。適切な場合に利用すると良い。

例外 使用する機会
IllegalArgumentException パラメータの値が不適切
IllegalStateException メソッドの呼び出しに対してオブジェクトの状態が不正
NullPointerException パラメータの値が禁止されている null
IndexOutOfBoundsException インデックスパラメータの値が範囲外
ConcurrentModificationException 禁止されているオブジェクトの並行した変更を検出
UnsupportedOperationException オブジェクトがメソッドをサポートしていない

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

下位レイヤからスローされた例外をそのまま上位レイヤに伝播すると、上位レイヤの概念と合わないことがあり、利用者を混乱させる可能性がある。

そのような場合、上位レイヤは下位レベルの例外をキャッチして、上位レイヤの中で上位レベルの概念から説明可能な例外をスローすべきである。これを例外翻訳と呼ぶ。

try{
    // 下位レイヤの処理呼び出し
    ...
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}

また、上位レベルの例外のデバッグに、下位レベルの例外の情報が役立つ場合には、例外連鎖と呼ばれる例外翻訳の特別な形式を採用し、下位レベルの例外を上位レベルの例外に渡してやるとよい。受け取った下位レベルの例外は Throwable.getCause メソッドで取り出すことができる。

try{
    // 下位レイヤの処理呼び出し
    ...
} catch (LowerLevelException cause) {
    throw new HigherLevelException(cause);
}

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

項目名の通り、各メソッドがスローするすべての例外を文書化すべきである。

  • チェックされる例外をここに宣言し、各例外がスローされる条件を Javadoc@throws タグを用いて文書化すること。
  • チェックされない例外も同様に @throws タグを用いて文書化する。チェックされない例外はメソッド実行に関する事前条件を示しているため、非常に重要である。
  • チェックされない例外には throws 予約語を用いない方がよい。チェックされる例外と区別したいため。
  • クラス内の多くのメソッドで、ある特定の例外がスローされる場合は、クラスの Javadoc にその例外について記述しても良い。

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

プログラムがキャッチされなかった例外により失敗すると、システムは自動的にその例外のスタックトレースを表示する。スタックトレースには、その例外の toString メソッドの結果である例外の詳細メッセージが含まれる。

スタックトレースは例外発生時の調査に必須であり、唯一の情報源となることも多い。したがって、例外の toString メソッドで必要な情報を出力するようにすることが大切である。

  • 例外の toString メソッドでは、その例外の原因となったすべてのパラメータとフィールドの値を出力すべき
  • 一方でスタックトレースには一般的にファイル名や行番号が含まれ、ソースコードを参照することで多くの情報を得ることができるため、冗長になりうる情報を含める必要はない
  • 例外の文字列表現に、適切なエラー記録情報を含めることを保証する方法の1つは、例外のコンストラクタでそれらを要求すること。以下は IndexOutOfBoundsException で実装した場合の例。
/**
 * IndexOutOfBoundsException を生成する
 *
 * @param lowerBound 最も小さな正当インデックス値
 * @param upperBound 最も大きな正当インデックス値に 1 を足した値
 * @param index 実際のインデックス値
 */
public IndexOutOfBoundsExcetion(int lowerBound, int upperBound, int index) {
    super("Lower bound: " + lowerBound + 
        ", Upper Bound: " + upperBound + 
        ", Index: " + index);

    this.lowerBound = lowerBound;
    this.upperBound = upperBound;
    this.index = index;
}

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

一般に、メソッド仕様の一部である例外は、オブジェクトをメソッド呼び出しの前と同じ状態にすべき。 呼び出し元が例外から回復することを期待されているチェックされる例外に関しては、特に重要。 (呼び出し元が回復した後、再度呼び出すことができるようにするため)

エラーアトミック性は一般に望ましいが、達成するためのコストや複雑性を考慮して検討する必要がある。 エラーアトミック性を達成するには次のような方法がある。

  • 不変オブジェクトを設計する

    もっとも簡単にエラーアトミック性を達成する方法は、オブジェクトを不変にすることである。 生成された時に整合性が取れていなければならず、その後変更されることはないため。

  • パラメータの検査をする

    メソッドの最初にパラメータの正当性を検査し、オブジェクトの変更が行われる前に例外をスローする。 例えば、次のコードでは最初の if 文で、パラメータの正当性を検査している。

    java public Object pop() { if (size == 0) { throw new EmptyStackException(); } Object result == elements[--size]; elements[size] = null; return result; }

  • 計算の順番を工夫する

    オブジェクトの変更が必要な計算より前に、失敗するかもしれない計算を行うよう、メソッド内の処理順を設計する。

  • 回復コードを書く

    操作の途中で発生するエラーを捉えて、操作が始まる前の時点までオブジェクトの状態を戻す回復コードを書く。

  • 一時的コピーを利用する

    オブジェクトの一時的コピーに対して操作を行い、操作完了時にオブジェクトの内容を一時的コピーの内容で置き換える。

項目65. 例外を無視しない

例外を無視せず、なぜその例外がスローされるのか設計者の意図についてよく考えること。どうしても空の catch ブロックを使うのであれば、なぜそうして例外を無視するのが適切なのかコメントを含めるべき。