例外

Effective Java 第 3 版の個人的メモ

  1. 項目 69 例外的状態にだけ例外を使う
  2. 項目 70 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使う
  3. 項目 71 チェックされる例外を不必要に使うのを避ける
  4. 項目 72 標準的な例外を使う
  5. 項目 73 抽象概念に適した例外をスローする
  6. 項目 74 各メソッドがスローするすべての例外を文書化する
  7. 項目 75 詳細メッセージにエラー記録情報を含める
  8. 項目 76 エラーアトミック性に努める
  9. 項目 77 例外を無視しない

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

1.1. 結論

例外をGOTO文のように、フロー制御に使ってはいけない。

1.2. 理由

例外は基本的に、プログラムの実行状況をキャプチャするのでコストが高い。

まれに、性能上メリットがあると考える開発者がいるが、それは誤りで、コスト高になることがほとんど。

以下のようなコードはNG.

  • 可読性が悪い
  • 実行速度も遅い。
1
2
3
4
5
6
7
try {
  int i = 0;
  while (true) 
    range[i++].climb(); // 配列を全部処理する
} catch (ArrayIndexOutOfBoundsException e) {
   // 正常系
}

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

2.1. 結論

  • 呼び出し側が復帰できるとき=検査例外
  • 復帰できないとき=非検査例外

2.2. 例外の種類

  • 検査例外

    • java.lang.Exceptionとその拡張クラス
      これが発生した場合、try-catch で処理するか、throws で伝搬させることが必要になる。
      ex)
      • copyOption における IOException - 読み取りまたは書込み中に入出力エラーが発生した場合。
      • ユーザ側で、無視して続ける or プログラムを停止する or … といった選択肢がある。
      • よく 業務例外 として扱われる。
  • 非検査例外

    • java.lang.RuntimeExceptionとその拡張クラス
    • これが発生した場合、catchして処理する必要はない。処理しなかった場合処理スレッドが停止して、最終的にJVMがハンドリングし異常停止する。
      ex)
      • FileAttributeのUnsupportedOperationException - ファイルの作成時に原子的に設定できない属性が配列に含まれる場合
      • ユーザ側はこれ以上処理をすすめることが出来ない。
      • よく システム例外 として扱われる。
  • エラー

    • java.lang.Error は例外とは区別して扱われる。
    • JVMがこれ以上処理を継続できないような、異常な状態を示す。
  • Throwable

    • ExceptionやErrorのrootクラス

2.3. 検査例外不要説

  • Javaの検査例外は失敗(=非検査例外だけでよい)だと主張する人がいる。
  • 実際、OOPの他の言語 C#, Python, Ruby, …には存在しない(Goには例外すら無い)。
  • 実際、検査例外/非検査例外を区別して正しく扱うのは相当難しい。

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

3.1. 結論

タイトルは換言すると、「検査例外は必要な時のみ使いましょう。」

チェックされる例外=検査例外は、基本的に制約を生む。なので、以下の場合のみ、検査例外を定義する。

  • API を適切に利用したとしても例外の発生を防ぐことができない場合
  • 例外の補足を強制をすることで有益な回復処理が行える場合
    • ファイルをつくれなかったら(IOException)

3.2. 代替手段

  • 検査例外をスローする代わりに空のOptionalを返す
  • 状態を確かめる boolean を返すメソッドと、非検査例外を返すメソッドに分ける
    • 常に有用ではない
1
2
3
4
5
if (obj.actionPermitted(args)) {
  obj.action(args);
} else { 
  ... // Handle exceptional condition
}

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

4.1. 結論

Javaライブラリが提供している標準例外をできる限り使う。慣習に従っているので理解しやすい。

  • IllegalArgumentException
  • IllegalStateException
  • NullPointerException
  • IndexOutOfBoundsException
  • ConcurrentModificationExcetpion
  • UnsupportedOperationException

いずれの例外も活用する際はメッセージを設定すること。空だと標準ライブラリ内で発生したように錯覚する。

4.2. その他

Spring Framework を利用する際は、Springが提供する例外も同様のことが言える。

spring-core, spring-web, spring-batch などなど アプリケーションの役割やドメインごとにしっかり作られているので、新しく例外クラスを作るかどうかはその都度検討するべき。

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

5.1. 結論

以下、最善な順の対策方法です。

5.1.1. 上位レイヤで、下位レイヤからの例外を完全に防ぐ

  • (可能だったら)下位レイヤからの例外に対処する最善の方法は、上位レイヤのメソッドは、下位レイヤのメソッドが必ず成功することを保証する。
    • 使い手は、例外の発生を意識しなくて良いから。

5.1.2. 上位レイヤの呼び出し元と、下位レベルの問題を隔離して良い場合

  • 上位レイヤのメソッドで、下位レイヤの例外をログとして出力する。
    • 管理者が問題を調査でき、かつ、下位レイヤの問題と上位レイヤの呼び出し元(クライアントのコード)を隔離できる。

例:

  • DBアクセス時に2, 3回リトライして、1回目失敗時にログとして出して、2回目成功時に呼び出し元に正常として返却する場合。
  • アプリケーション終了時、コネクションのクローズミスが発生した場合。

5.1.3. 上位レイヤで下位レイヤの例外を再スローする

  • 上位レイヤは下位レベルの例外をキャッチして、上位レイヤの抽象概念の観点から説明可能な例外をスローすべき。
    • 上位レイヤのレベルで行っている処理と明らかに関係のない例外をスローすると、使い手が混乱するから。
      • もし関係のない例外をスローするAPIをリリースしてしまうと、修正するのが難しい。
        • 既存のクライアントのコードが動作しなくなる可能性があるから。

5.2. サンプル

「上位レイヤで下位レイヤの例外を再スローする」のサンプル

1
2
3
4
5
6
7
// 例外翻訳
try {
    // 処理するために下位レベルのメソッドを使用する。
    doLowerLevelMethod();
} catch (LowerLevelException e) {
    throw new HigherLevelException(...);
}

AbstractSequentialListの場合は以下のように例外翻訳をしている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/**
 * このリストの指定された位置の要素を返す。
 * @throws IndexOutOfBoundsException index が範囲外
 *         {@code index < 0 || size() <= index }.
 */
public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

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

6.1. 結論

具象メソッド/抽象メソッドのJavadoc に @throws タグを付けて、検査例外/非検査例外ともにスローされる条件を正確に書きましょう。

  • 使い手が、クラスを効果的に利用するのが困難だったり、不可能だったりするため。

6.2. コメント

例外がスローされる条件があると、以下の面で良さそう。

  • 例外で落ちてしまったときに対処しやすい。

その他

  • OSS開発の場合は除いて、全ての非検査例外についても記載するのは現実的には厳しそうなので、重要なもの/発生しそうなものについてのみ記載するで良いのでは。
  • 人によって記載レベルが変わってしまうので、JavaDoc記載ルールのようなものを作成する。
  • 以下の点でしんどいので、どこまで記載すると労力に見合った効果を発揮するのかを考える。(例外に限った話ではなく、仕様関連の話。)
    • メソッドコールの層が厚い場合に、全部Javadocを記載するのがしんどい。
    • JavaDocを記載する時、使い手の立場に立って考えないといけないのがしんどい。

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

7.1. 結論

スタックトレースの詳細メッセージに、 その例外の原因となった全てのパラメータとフィールドの値 を含めましょう。

  • エラーメッセージは、ソフトウェアエラーの解析時に、プログラマが頼らなければならない唯一の情報であることが多いため。

プログラミングによるアクセスのため、エラー情報をフィールド値に保存しましょう。

  • 例外の詳細にプログラマが、プログラミングによりアクセスしたいと思うかもしれないため。

7.2. サンプル

IndexOutOfBoundsException の場合のサンプル

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * IndexOutOfBoundsException を生成する。
 *
 * @param lowerBound もっとも小さな正当なインデックス値
 * @param upperBound もっとも大きな正当なインデックス値に1を足した値
 * @param index 実際のインデックス値
 */
 public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
     // エラーを記録する詳細メッセージを生成する
     super( "Lower bound: " + lowerBound +
          ", Upper bound: " + upperBound +
          ", Index : " + index);

     // プログラミングによるアクセスのためにエラー情報を保存する
     this.lowerBound = lowerBound;
     this.upperBound = upperBound;
     this.index = index;
 }

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

8.1. 結論

  • 一般的に、オブジェクトに対してメソッド呼び出しをして、エラーを出力した後でも、そのオブジェクトがメソッド呼び出し前と同じ状態である(エラーアトミック性)ようにするべき
    • エラーアトミック性が保たれていれば、そのオブジェクトをまた使うことができるから
  • 不変オブジェクトはエラーアトミック性を持つ
    • 不変だから
  • 可変オブジェクトがエラーアトミック性を保つためには4つの方法がある
    • 理由は後述。
  • エラーアトミック性は一般的に望ましいが、達成可能とは限らない
    • 例えば、2つのスレッドが適切な同期なしに同一オブジェクトを同時に変更しようとしら不整合な状態のままになるかもしれないから。
    • 実行の前提となる状態が壊された時。
  • エラーアトミック性が可能な場合であっても必ずしも望ましいとは限らない。
    • 複雑性、コストが増大することがあるから
  • エラーアトミック性が保たれない場合は、ドキュメント化すべき
    • API使用者が、エラー時の挙動を把握することができるから

8.2. 可変オブジェクトがエラーアトミック性を保つための4つの方法

1.2.3.は変更前にチェックして保つ方法。4.は変更後に戻して保つ方法

  • 1.バリデーションチェック
    • オブジェクトを変更する前にエラーを検出できるから
  • 2.状態変更前の事前計算
  • 3.一時的なコピーに対して操作を行い、操作が完了したらオブジェクトを置き換える
    • 例えば、Collection#sort
      • 性能のために行われているが、付加的なメリットとしてエラーアトミック性も保たれている
  • 4.回復コードを書く
    • 例えば、永続的なデータ構造に対して使用される
    • 変更前の状態に戻すための手段は2パターンある
      • 変更履歴を残しておく
      • 変更前の状態をそのまま保存しておく

8.3. 余談

分散システムだとこの話はついてまわることが多い。

  • 分散トランザクション
    • 2フェーズコミット
  • 分散システムのトランザクション
    • 例:メルペイの銀行の振替 詳細はこちら
      • A : 口座の残高を減らす
      • B : 口座の残高を増やす

9. 項目 77 例外を無視しない

9.1. 結論

  • 空のcatchブロックを使って例外を無視してはならない
    • 例外の目的「例外的状態を処理させることを強制すること」が達成されないから
    • 例外の目的
      • 検査例外はユーザに処理させるのを強制するのが目的。
      • 非検査例外はJVMに知らせることが目的。
  • 最低でもcatchブロックに、例外を無視する理由を記載すべき
    • 意図して例外を無視しているのか分からないから
    • 例:例外を無視するのが適切かもしれない状況に、FileInputStream のクローズ時例外がある。
      • すでにファイルから必要な情報を読みだしていれば、行っている処理を中断する必要がないから
      • 例外が頻繁に発生した際に、後から調査できるので、中断はせずともログは出すべき