S-JIS[2012-11-24/2014-12-27] 変更履歴

Java 例外の投げ方メモ

Javaでプログラマーはどのような例外を投げればいいかについて。


NullPointerExceptionを自分でスローしてはならない

NullPointerExceptionは、nullオブジェクトに対してメソッドを呼ぼうとしたりフィールドアクセスしたりしたとき等にJavaランタイムシステムが発生させるもの。
したがって、Java自体の開発者でない限り、NullPointerExceptionをスローするようなコーディングをすべきではない。

駄目な例:

public String append(String s, int n) {
	if (s == null) {
		throw new NullPointerException(MessageFormat.format("null不可。s={0}", s));
	}
	if (n < 0) {
		throw new IllegalArgumentException(MessageFormat.format("0以上でないと駄目。n={0}", n));
	}

	return s + n;
}

この場合はIllegalArgumentExceptionをスローすべきだろう。
「値がnullだからNullPointerException」という考え方ならば、この例のnの方のチェックは、「値がマイナスだからMinusException」とかでないと不整合だ。
しかし普通は、値に応じた例外なんか用意しないと思う。「0,1,2のみOK」の場合なんて、どういう例外名になるんだ(苦笑)

無駄な例:

public boolean equals(String s, int n) {
	if (s == null) {
		throw new NullPointerException(MessageFormat.format("s={0}", s));
	}

	String s2 = String.valueOf(n);
	return s.equals(s2);
}

sがnullのときにNullPointerExceptionが返ればいいなら、この例の場合、nullチェックなどする必要は無い。
自分でif文でチェックしなくても、sがnullなら「s.equals()」でNullPointerExceptionが発生してくれる。
まぁどの変数がnullだったかについてのメッセージは出てくれないけど、スタックトレースからソースを追えば簡単に分かるし。
(スタックトレースやソースを追えない状況なら、変数名をメッセージに出していても無駄だし)


IllegalArgumentException

メソッドの引数の値が仕様の範囲外であるときは、IllegalArgumentExceptionをスローする。

public String append(String s, int n) {
	if (s == null) {
		throw new IllegalArgumentException("sはnull不可。");
	}
	if (n < 0) {
		throw new IllegalArgumentException(MessageFormat.format("nは0以上。n={0}", n));
	}

	return s + n;
}

IllegalArgumentExceptionは、呼び出し元に問題がある場合にスローするべきだと考える。[2014-12-27]
つまり、「自分のメソッドの想定外の値が渡されるのは 呼び出し元に問題があるので、自分としては処理を続行できない」というのがIllegalArgumentExceptionの意図だと思う。
(つまり、IllegalArgumentExceptionが発生する状況というのは、何らかのバグがある、ということになる)

画面やコンソール等からユーザーによって入力された値やファイルから読み込んだ値が正しいかどうかをチェック(精査・バリデーション)するようなメソッドの場合、判定結果として正しくない値だったことを返すのにIllegalArgumentExceptionを使うのは相応しくない。
それは、(チェックメソッドにはどのような値が渡されても構わないのであり、)チェックメソッド自体にとっては不正な引数ではないからだ。


IllegalStateException

内部状態が仕様の範囲外であるときは、IllegalStateExceptionをスローする。
ここで言っている内部状態とは、フィールドの値とか、呼び出したメソッドの戻り値とかのこと。

private Map<MyEnum, String> map;

public String getValue(MyEnum key) {
	if (map == null) {
		// 仕様:先にmapが初期化されている必要がある
		throw new IllegalStateException(MessageFormat.format("map={0}", map));
	}

	String value = map.get(key);
	if (value == null) {
		// 仕様:全てのキーに対して値が取れる(なので、無かったらエラー)
		throw new IllegalStateException(MessageFormat.format("key={0}, value={1}", key, value));
	}

	return value;
}

assert

値が正しい(仕様の範囲内である)ことをチェックする構文としてassertがある。
使い所はIllegalArgumentExceptionIllegalStateExceptionと被るが。

private Map<MyEnum, String> map;

private String getValue(MyEnum key) {
	assert key != null : key; //引数のチェック
	assert map != null : map; //フィールドのチェック

	String value = map.get(key);
	assert value != null : value; //戻り値のチェック

	return value;
}

assertは、値が不正(条件判定が偽)だとAssertionErrorを発生させる。
また、実行時オプション(javaコマンドのVM引数)で「-ea」を指定しないとチェックを行わない。
したがって、assertの実行はデバッグ時のみ行われる想定だと思われる。

個人的には、
publicメソッドの引数は(自分以外のプログラマーが呼ぶ可能性があるので)IllegalArgumentExceptionを返す方式で常にチェックし、
privateメソッドの引数は(呼び出し元(つまり自分のコーディング)がちゃんと値チェックをしていればいいので)assertで念の為チェックする程度。
外部のクラス・ライブラリーの戻り値のチェックはIllegalStateExceptionを返す方式とし、
内部の(自分で作った部分だけに依存する)状態はassertでチェックする。
という使い分けになるのかなぁと思う。

むしろassertはコメント代わりかな。「この場所ではこの変数はこういう値のはず」という意図を示す。
(実際にチェックを実行できる分、単なるコメントより強力)


UnsupportedOperationException

その操作をサポート(実装)していないというときは、UnsupportedOperationExceptionをスローする。

private String 種族;

private int manaPoint;

public void addManaPoint(int n) {
	//グラスランナーはMPを持っていないので、増減させることは出来ない

	if (種族.equals("グラスランナー")) {
		throw new UnsupportedOperationException(MessageFormat.format("種族={0}", 種族));
	}

	manaPoint += n;
}

よくあるのは、インターフェースで複数のメソッドが定義されていて、それを具象クラスで実装した際に、一部のメソッドだけUnsupportedOperationExceptionを返すようにすること。
例えばListにはadd()とかremove()があるが、不変リストではadd()やremove()を実行するとUnsupportedOperationExceptionがスローされる。

class UnmodifiableList<E> implements List<E> {
〜
	@Override
	public void add(E element) {
		throw new UnsupportedOperationException();
	}
〜
}

オブジェクト指向プログラミングの考え方からすると、インターフェースを実装しているからには、具象クラスの各メソッドは“インターフェースが要求している仕様”を満たして実行できる必要がある。
しかし現実にはそういう訳にもいかない場面があるので、UnsupportedOperationExceptionをスローするのは妥協の産物として仕方ない気がする。
(例外をスローする方式だと、実行するまでそのメソッドが使えないことが分からないので、良くないのだが)


InternalError

この場所は絶対に実行されない(実行されたとしたらJava内部のバグとしか思えない)という場所ではInternalErrorをスローする。
この「絶対」は、プログラムを変更してコンパイルし直さない限り実行されない(通常の方法で実行時の値を変える程度では実行されない)というレベル。

今のところ、clone()くらいしか使い道は思い付かない。

class ExampleClone implements Cloneable {

	@Override
	public ExampleClone clone() {
		try {
			return (ExampleClone) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new InternalError();
		}
	}
}

super.clone()は、対象クラスがCloneableインターフェースを実装していないとCloneNotSupportedExceptionをスローする。
Cloneableを実装していないと正しい実行が出来ないし、例外を発生させようと思ったらCloneableを外すしかない。
正常系と例外発生系を同時にテストするのは不可能。
なので、カバレッジを100%にしろとか言われると困るんだよね〜(苦笑)


独自例外

前述のIllegalArgumentExceptionIllegalStateException等が実際にスローされる場面は、基本的にプログラムのバグが顕在化する事であり、プログラムを異常終了させるしかない。(プログラムを実行しているユーザーがどうにか出来るものではない)

異常終了させたくない場合(対話型ツールとかでエラーメッセージを出力して処理を続行するような場合など)は、アプリケーション独自の例外を用意し、各メソッドではその独自例外をスローするのが良い。
そして上位層のプログラムで独自例外をキャッチし、例外を処理した後アプリケーション全体を続行させる。


例外の基礎へ戻る / Java目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま