Java | Java 標準ライブラリ:カスタム例外

Java Java
スポンサーリンク

カスタム例外を一言でいうと

「カスタム例外」は、
自分のアプリやライブラリ専用の例外クラスを新しく定義して、
エラーの意味を分かりやすく、扱いやすくするためのものです。

NullPointerExceptionIOException だけでは表現しづらい
「業務的に意味のあるエラー」を、名前のついた独自クラスとして表せるようになります。

「何が起きたか」が例外クラス名だけで伝わるようになるのが最大のメリットです。


まずは「どこにぶら下げるか」を決める(checked か unchecked か)

Exception を継承するか、RuntimeException を継承するか

カスタム例外を作るときに最初に決めるのは、
Exception を継承するか、RuntimeException を継承するかです。

Exception 継承ならチェック例外、
RuntimeException 継承なら非チェック例外(実行時例外)になります。

チェック例外にした場合、その例外を投げるメソッドは必ず throws 宣言が必要になり、
呼び出し側も try-catch か throws を強制されます。

非チェック例外にした場合、throws も try-catch も強制されません。
起きたときには普通の RuntimeException と同じように実行時に飛びます。

初心者向けの判断基準としては、次のイメージを持ってください。

「この失敗は普通に起こりうるので、呼び出し側でちゃんと対応してほしい」
という性質ならチェック例外(Exception 継承)。

「これは基本的にバグや致命的な異常で、起きたら落ちて構わない」
という性質なら非チェック例外(RuntimeException 継承)。

設計としてどちらにしたいか、という話です。


一番基本的なカスタム例外クラスの定義

チェック例外のカスタム例外(Exception 継承)

まずは、チェック例外としてのカスタム例外の例から。

例えば「業務上のルール違反」を表す BusinessException を作ってみます。

public class BusinessException extends Exception {

    public BusinessException() {
        super();
    }

    public BusinessException(String message) {
        super(message);
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
    }

    public BusinessException(Throwable cause) {
        super(cause);
    }
}
Java

ポイントは、コンストラクタで super(...) を呼んで、
メッセージや原因例外(cause)を親クラスに渡していることです。

実務では、よく使う 2 パターン(message だけ、message+cause)だけに絞っても構いません。

これを使うと、例えば次のように書けます。

public class OrderService {

    public void placeOrder(int quantity) throws BusinessException {
        if (quantity <= 0) {
            throw new BusinessException("数量は 1 以上である必要があります: " + quantity);
        }
        // 正常な処理…
    }
}
Java

呼び出し側は必ず BusinessException を意識させられます。

public class App {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        try {
            service.placeOrder(0);
        } catch (BusinessException e) {
            System.err.println("注文エラー: " + e.getMessage());
        }
    }
}
Java

「業務的に NG な状況」が BusinessException という名前で、そのままコード上に現れているのが分かると思います。

非チェック例外のカスタム例外(RuntimeException 継承)

今度は、非チェック例外としてのカスタム例外です。

public class BusinessRuntimeException extends RuntimeException {

    public BusinessRuntimeException() {
        super();
    }

    public BusinessRuntimeException(String message) {
        super(message);
    }

    public BusinessRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public BusinessRuntimeException(Throwable cause) {
        super(cause);
    }
}
Java

RuntimeException を継承しているだけで、あとはほぼ同じです。

例えば、「このアプリの設計上、本来ありえないはずの状態」を検知したときに使えます。

public class UserStatusService {

    public void processStatus(String status) {
        if (!status.equals("ACTIVE") && !status.equals("INACTIVE")) {
            throw new BusinessRuntimeException("想定外のステータス: " + status);
        }
        // 正常処理…
    }
}
Java

この場合、呼び出し側は try-catch を強制されません。
「テストで気付いて修正すべきバグ」として扱う意図が感じられます。


メッセージと cause をどう設計するか(ここは大事)

メッセージは「人間に事情を伝える文章」

例外クラス名は「何の種類のエラーか」を表し、
メッセージは「具体的に何がまずかったか」を表します。

例えば、

throw new BusinessException("数量は 1 以上である必要があります: " + quantity);
Java

というメッセージがあると、ログを見たときに

「どの条件に引っかかったか」
「実際の値がいくつだったか」

が一目で分かります。

最低限、
「どの入力値が原因か」
「どのルールに違反しているか」

が分かるようにメッセージを書くと、後からの分析が段違いに楽になります。

原因例外(cause)をラップする

他の例外を捕まえて、自分のカスタム例外に包んで投げ直したい場面はよくあります。

例えば、DAO 層での SQLException をカスタム例外にラップする例です。

public class UserDao {

    public User findById(String id) throws DataAccessException {
        try {
            // DB アクセス(ここで SQLException が出るかもしれない)
        } catch (SQLException e) {
            throw new DataAccessException("ユーザー取得に失敗しました: id=" + id, e);
        }
    }
}
Java

DataAccessException は例えば次のように定義します。

public class DataAccessException extends Exception {
    public DataAccessException(String message, Throwable cause) {
        super(message, cause);
    }
}
Java

こうしておくと、スタックトレースには

DataAccessException
の下に
SQLException

が「原因」としてぶら下がって出てきます。

原因例外を無視してしまうと、
「結局、元の何が悪かったのか」が分からなくなります。

カスタム例外を使うときは、

捕まえた例外を、そのまま cause として渡す

ことを強く意識してください。


カスタム例外を使うメリットを具体的なコードで見る

例:ユーザー登録のバリデーションエラー

ユーザー登録処理で、入力チェックに失敗したときに
「ただ IllegalArgumentException を投げる」のと
「UserValidationException を投げる」のでは、読みやすさが全然違います。

まず、カスタム例外を定義します。

public class UserValidationException extends Exception {
    public UserValidationException(String message) {
        super(message);
    }
}
Java

サービス側で使います。

public class UserService {

    public void register(String name, String email) throws UserValidationException {
        if (name == null || name.isBlank()) {
            throw new UserValidationException("名前は必須です");
        }
        if (email == null || !email.contains("@")) {
            throw new UserValidationException("メールアドレスの形式が不正です: " + email);
        }
        // 正常な登録処理…
    }
}
Java

呼び出し側は、次のように扱えます。

public class App {
    public static void main(String[] args) {
        UserService service = new UserService();
        try {
            service.register("", "invalid-email");
        } catch (UserValidationException e) {
            System.err.println("入力エラー: " + e.getMessage());
        }
    }
}
Java

ログやコードを読む人にとって、
「これはユーザー入力のバリデーションエラーなんだな」ということがひと目で分かります。

もしここで、単に IllegalArgumentException を投げていたらどうでしょうか。
例外クラス名からは「どの層の、どの種類のエラーか」が読み取りづらくなります。

カスタム例外は、
「エラーにも “ドメインの名前” を付ける」行為だと思ってください。


カスタム例外を乱造しないための考え方

何でもかんでも作ると逆効果になる

カスタム例外の利点を知ると、

アクセスエラー、保存エラー、取得エラー、更新エラー…
みたいに、細かく例外クラスを作りたくなるかもしれません。

しかし、増やしすぎると、

どれを catch すればいいのか分からない
例外クラスの一覧を見ても、違いがよく分からない

という「名前だけ増えた混乱状態」になります。

カスタム例外を作るときは、
次のような問いを自分に投げてみてください。

その例外クラス名を catch 節に書いたとき、
「どんな失敗をまとめて扱いたいか」がはっきりしているか。

例えば、

業務ルール違反全般 → BusinessException
ユーザー入力の妥当性エラー → UserValidationException
永続化層(DB やファイル)のエラー → DataAccessException

くらいの粒度にしておくと、
呼び出し側から見ても扱いやすい「まとまり」になります。

既存の標準例外で足りるなら、それを使う

例えば、引数チェックで

if (x < 0) throw new IllegalArgumentException(...);

のように、標準の IllegalArgumentException で十分な場面は多いです。

「自分のドメインにとって意味のある分類があるか?」
「それをクラスとして切り出すことで、catch する側が嬉しくなるか?」

このあたりを基準にして、
標準例外で足りるところはそれを使い、
どうしても表現しきれないところだけカスタム例外を導入する、と考えるとバランスが良くなります。


まとめ:カスタム例外を自分の中でこう位置づける

カスタム例外は、初心者向けにまとめると、

「自分のアプリの“失敗の種類”に名前を与えて、コードとログを読みやすくする仕組み」

です。

特に意識してほしいポイントは、

  • Exception を継承すればチェック例外、RuntimeException を継承すれば非チェック例外になる
  • クラス名で「どんな種類のエラーか」、メッセージで「具体的に何が悪いか」を伝える
  • 既存の例外(IOException, IllegalArgumentException など)をラップして、原因情報を失わないようにする
  • 増やしすぎると逆効果なので、「catch したいまとまり」を基準にクラス設計する

タイトルとURLをコピーしました