1. はじめに
Java の標準ライブラリにはすでに多くの例外があります。たとえば NullPointerException、IllegalArgumentException、IOException などです。しかし、標準の例外だけでは、プログラム内で発生したエラーを明確かつ分かりやすく表現できないことがあります。
実例:
あなたは銀行アプリを作っています。ユーザーが口座残高を超える金額を引き出そうとしました。IllegalArgumentException を投げることもできますが、InsufficientFundsException のような独自の例外を用意した方がはるかに分かりやすくなります。コードを見ただけで何が起きたかがすぐに分かるからです。
独自例外は、アプリケーションを正しくカスタマイズするための手段です。問題への対応をきめ細かく調整でき、名前(論理的な名前にした場合)から何が起きたのかがすぐに分かります。さらに、自己文書化性があるため、シグネチャに throws MyException を含むメソッドは、どのようなエラーが起こり得るかを明確に示します。加えて、必要であれば(たとえば残高や操作金額などの)追加フィールドを持たせることもいつでも可能です。
2. 独自の例外を作成するには?
とても簡単です。標準の例外クラスのいずれかを継承する新しいクラスを作成します。
- 検査例外(checked)の場合は、Exception を継承します。
- 非検査例外(unchecked)の場合は、RuntimeException を継承します。
例: 検査例外
public class InvalidCredentialsException extends Exception {
public InvalidCredentialsException(String message) {
super(message); // メッセージを親クラスに渡す
}
}
これで、コード内でこの例外を投げられます:
if (!login.equals("admin") || !password.equals("1234")) {
throw new InvalidCredentialsException("ユーザー名またはパスワードが正しくありません");
}
例: 非検査例外
public class NegativeBalanceException extends RuntimeException {
public NegativeBalanceException(String message) {
super(message);
}
}
checked はいつ、unchecked はいつ使うべきか?
- Checked — エラーが予期でき、かつ処理可能な場合(例: バリデーションエラー、ファイルの不在、ユーザーデータの不正など)。
- Unchecked — プログラムのロジック上のバグに起因する場合(例: 0 除算、不変条件の破りなど)。
3. コンストラクタ: 例外を情報豊かにするには
通常、独自の例外クラスには少なくとも String message を受け取るコンストラクタを実装します。さらに次のようなものを追加することもよくあります。
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException() {
super();
}
public ScoreLimitExceededException(String message) {
super(message);
}
public ScoreLimitExceededException(String message, Throwable cause) {
super(message, cause);
}
public ScoreLimitExceededException(Throwable cause) {
super(cause);
}
}
補足:
- message — エラーのテキストによる説明。
- cause — 原因(別の例外)。あるエラーを別の例外で「ラップ」したい場合に使います。
ヒント: どのコンストラクタが必要か分からない場合は、少なくとも文字列を受け取るものを用意しておきましょう。
4. 独自例外をコードで使う
例を見てみましょう。ユーザーがいて、そのユーザーにはスコアがあり、100 を超えて追加することはできません。
public class User {
private String name;
private int score;
public User(String name) {
this.name = name;
this.score = 0;
}
public void addScore(int points) throws ScoreLimitExceededException {
if (score + points > 100) {
throw new ScoreLimitExceededException("スコア上限を超えました! 追加しようとした値: " + points);
}
this.score += points;
}
}
例外クラス:
public class ScoreLimitExceededException extends Exception {
public ScoreLimitExceededException(String message) {
super(message);
}
}
ハンドリング:
try {
user.addScore(60);
user.addScore(50); // ここで例外が投げられる!
} catch (ScoreLimitExceededException e) {
System.out.println("エラー: " + e.getMessage());
}
結果:
エラー: スコア上限を超えました! 追加しようとした値: 50
ここで疑問が浮かぶかもしれません。例外の代わりに if で条件分岐し、たとえば false や特別な値を返して「失敗した」ことを示すだけではダメなのか? 例えば次のように:
public boolean addScore(int points) {
if (score + points > 100) {
return false; // 処理したくない場合は、何らかの RuntimeException を投げてもよい
}
this.score += points;
return true;
}
この方法は一見シンプルに見えますが、深刻なエラーやアプリケーションロジックの破壊が関わる場合には欠点があります。
第一に、エラーを示すために false や別の値を返す設計では、呼び出し側が戻り値を常に確認することが義務になります。もしプログラマがそれを忘れると、エラーは見逃され、プログラムの予測不能な挙動につながります。これに対し例外は(検査例外であれば)処理を強制し、非検査例外であっても、捕捉されなければ問題を明確に知らせます。
第二に、例外はエラーの意味をより明確に伝えます。false の返却は「失敗」「適用不可」「利用不可」など何を意味するのか曖昧です。ScoreLimitExceededException であれば「スコア上限を超えた」ということが明確かつ一意に伝わります。これはコードの可読性と保守性を向上させます。
第三に、例外はエラー処理を集約できます。addScore を呼び出す場所のあちこちに if チェックを散らすのではなく、例外を 1 か所で捕捉して、ユーザーへのメッセージ表示、ログ記録、トランザクションのロールバックなど適切な対応を行えます。
最後に、上限超過のような問題はまさに例外的状況です(だから「例外」と呼ばれます)。通常の実行フローはスコアが正常に加算されることを前提としています。そうでないなら、それはビジネスロジックやオブジェクトの不変条件の破りであり、例外の利用に最適なシナリオです。
5. 例外に独自フィールドを追加する
エラー処理に役立つ追加データを、独自の例外に持たせると便利なことがあります。
例:
public class ScoreLimitExceededException extends Exception {
private int currentScore;
private int attemptedAdd;
public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
super(message);
this.currentScore = currentScore;
this.attemptedAdd = attemptedAdd;
}
public int getCurrentScore() {
return currentScore;
}
public int getAttemptedAdd() {
return attemptedAdd;
}
}
使用例:
if (score + points > 100) {
throw new ScoreLimitExceededException(
"スコア上限を超えました!",
this.score,
points
);
}
6. 役立つ細かなポイント
例外の命名規則は?
Java では、ユーザー定義の例外名に Exception サフィックスを付けるのが一般的です。 InvalidUserInputException、InsufficientFundsException、ScoreLimitExceededException など。
例外名を単に Error や Warning とするのは避けましょう。他の開発者(数週間後の自分自身も含む)を混乱させます。
どこで、いつ例外を投げるべきか?
- ユーザーデータのバリデーション時(例: 空の名前、負の年齢)。
- ビジネスルールの違反時(例: 上限超過、残高を超える引き出し)。
- 外部サービスとの連携でのエラー時(例: サービスが利用不可、タイムアウト)。
7. 独自例外を作成する際のよくある誤り
エラー № 1: 間違ったクラスから継承している。
Exception(または RuntimeException)から継承し、Throwable や Error からは継承しないでください。
エラー № 2: メッセージ付きコンストラクタがない。
文字列(String message)を受け取るコンストラクタがないと、例外は無言になり、デバッグが難しくなります。
エラー № 3: ビジネスロジックに標準例外を使ってしまう。
独自の「」ではなくNullPointerException や IllegalArgumentException を投げるべきではない状況があります。意味が伝わる「説明的な」独自例外を使いましょう。
エラー № 4: 独自例外の乱立。
些細な事柄ごとに個別の例外クラスを作らないでください。あなたのドメインに固有でないエラーについては、標準の例外を使いましょう。
エラー № 5: シリアライズ非対応(まれだが起こり得る)。
例外をネットワーク経由で渡したり保存したりする可能性があるなら、implements Serializable を実装することを検討してください。ただし、シンプルなアプリケーションでは必須ではありません。
GO TO FULL VERSION