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


Effective Javaの独自解釈です。
第3版の項目70について、自分なりにコード書いたりして解釈してみました。

ざっくり

「回復可能」とは、例外が発生してもカレントスレッドを停止させず、別の制御に状態を戻せること。例外が発生する可能性をメソッド実装者が認識しているので、チェック例外を出力し、メソッドの呼び出し元にハンドリングを強制させる。
「プログラミングエラー」とは、メソッドが実装者の禁止している使われ方で使われ、カレントスレッドを停止させる必要があるもの。(一般にキャッチすべきではない)実行時例外を出力させる。

チェック例外が必要な例

以下のようなサービス層のメソッドを考える。

  • 指定したIDの本を購入
  • 参照テーブルは「本」「銀行口座」「購入済み本」テーブル
  • 「銀行口座」に本を購入できる金額があれば、「購入済み本」に本を追加し、「銀行口座」の残高を減らす
  • 「銀行口座」に本を購入できる金額がなければ、残高不足例外を出力

※ テーブル定義などは省略しています。

本購入メソッド


     /**
     * 指定したIDの本を購入
     * 購入した本は購入済み本テーブルに追加し、残高を減らす
     * 残高不足の場合は例外を出力
     *
     * @param bookId 購入対象の本ID
     * @param userId ユーザーID
     * @throws InsufficientFundsException 残高不足例外
     */
    public void buyBook(String bookId, int userId) throws InsufficientFundsException {

        // 購入する本の値段を取得
        Book selectedBook = bookDao.selectByBookId(bookId);
        int bookPrice = selectedBook.getPrice();

        // 残高を取得
        BankAccount myBankAccount = bankAccountDao.selectByUserId(userId);
        int myBalance = myBankAccount.getBalance();

        // 残高不足の場合に例外を出力
        if (bookPrice > myBalance) {
            int shortage = bookPrice - myBalance;
            throw new InsufficientFundsException("残高不足です。", shortage);
        }

        // 購入した本を購入済み本テーブルに追加
        BoughtBook myBoughtBook = new BoughtBook();
        myBoughtBook.setUserId(userId);
        myBoughtBook.setBookId(bookId);
        boughtBookDao.insert(myBoughtBook);

        // 残高を減らし更新
        int afterMyBalance = myBalance - bookPrice;
        myBankAccount.setBalance(afterMyBalance);
        bankAccountDao.update(myBankAccount);
    }

残高不足例外クラス


/**
 * 残高不足例外
 */
public class InsufficientFundsException extends Exception {

    private int shortage;

    /**
     * 例外メッセージと不足金額
     *
     * @param message  例外メッセージ
     * @param shortage 不足金額
     */
    public InsufficientFundsException(String message, int shortage) {
        super(message);
        this.shortage = shortage;
    }

    public int getShortage() {
        return this.shortage;
    }
}

チェック例外を出力する理由

本を購入できるかできないかは現在の銀行口座の残高に依存する。要するに、本が購入できないという例外的状況が発生する可能性があるので、チェック例外を出力して、呼び出し元に他の処理へ変更させるなり、エラーメッセージを出させるなりしてハンドリングを強制させる。
もし非チェック例外を出力した場合、呼び出し側がハンドリングの実装が必要なことに気づかず、回復処理に入らないままカレントスレッドが停止してしまう恐れがある。

ハンドリングに追加情報を含める

出力する例外には追加情報を含めると、ハンドリング側で役立つことがある。
例えば上記コードでは、不足残高情報を例外に含めることで、ハンドリング側でユーザーにエラーメッセージを出したい時に、残高がいくら不足しているのかを明示することができる。

実行時例外が必要な例

以下のようなサービス層のメソッドを考える。

  • 指定した額を指定した相手に送金
  • 参照するのは送金相手の「銀行口座」テーブル
  • 「銀行口座」の残高を、指定した送金額だけ追加
  • 指定した送金額が正数でなければ引数不正例外を出力

※自分の残高を減らす処理は省略しています。

送金メソッド

    /**
     * 指定した相手に、指定した額を送金する
     * 送金額は正数を指定すること
     *
     * @param transferPrice 送金額
     * @param targetUserId  送金相手のID
     */
    public void transferMoney(int transferPrice, int targetUserId) {

        if (transferPrice <= 0) {
            throw new IllegalArgumentException("送金額は正数で指定する必要があります。");
        }

        BankAccount targetBankAccount = bankAccountDao.selectByUserId(targetUserId);
        int nowBalance = targetBankAccount.getBalance();
        int afterBalance = nowBalance + transferPrice;
        targetBankAccount.setBalance(afterBalance);
        bankAccountDao.update(targetBankAccount);
    }

実行時例外を出力する理由

このメソッドは正数以外の送金額指定を禁止している。負数が指定された場合は相手の残高を減らしてしまうからだ。
メソッド呼び出し側の実装で正数以外の指定を100%防げるので、もし正数以外が指定された場合はプログラムのバグと判断し、実行時例外を出力し、カレントスレッドを停止させる。処理を続けた場合、意図せぬ処理が走ることがあり危険だからである。

(補足) 実行時例外全てがキャッチすべきでないという訳ではない

実行時例外は全てキャッチしてはいけないという訳ではなく、Springのデータアクセス例外は実行時例外として実装されており、キャッチして別の検査例外を再スローすることがある。