アサーション

Revised: Apr./25th/2004; Since: Apr./24th/2004

アサーション(表明)はオブジェクト指向の概念の一つです。 Bertrand Meyer という人が、Eiffel という言語で実装した契約に基づく設計 (DbC: Design by Contract) で採用したアサーション機能がよく知られています。Java では J2SE 1.4 で言語仕様に取り込まれました。

アサーションとは何か

アサーションは、プログラムの仕様をソースコードに明記するものです。具体的には、プログラムの実行時には発生し得ない場合を false とする boolean 式を含み、仮に false になるようなら java.lang.AssertionError を発生させます。AssertionError が発生する場合は、プログラムが予期しない動作を実行していることを示しています。当該行に制御が渡るまでに、なんらかのバグが作り込まれているということです。

アサーションは行として記述して、チェックポイントの役割を果たします。極論すれば、チェックポイント間を結ぶのが制御です。何らかの処理の前に記述すれば事前条件をチェックし、後ならば事後条件をチェックできます。処理に依存しないクラス固有の不変条件も、ありえない変数値、到達するはずのない制御としてチェック可能です。

アサーションのシンタックス

Java の言語仕様で採用されたアサーション機能は極めてシンプルです。新しく追加されたキーワード "assert" を使い、シンタックスは次の二通りが使えます。

assert Expression1 ;
assert Expression1 : Expression2 ;

いずれの場合も、Expression1 は boolean 値 (true/false) を返す式です。Expression2 は値を返す任意の式で、値は AssertionError のコンストラクタ引数になりますす。この値の文字列表現が詳細メッセージに使われ、通常は、変数の値などの、デバッグに役立つトレース情報を記述します。

// x が null だと AssertionError
assert x != null;

// y が 負だと AssertionError
// 詳細メッセージは "y = " + y
assert y >= 0: "y = " + y;

アサーションを適切に記述する場合、アサーションが通れば、その点までの制御は意図どおりに通っていることを確認できたことになります。アサーションは、チェックポイントの役割を果たします。アサーション条件は、適切に動作していれば成功する条件を記述するので、デバッグ済みの完成したコードは、有効/無効に関わらず動作に変化がないようにします。

アサーションをコンパイル時に有効にするには、コマンド "javac" にフラグ "-source 1.4" を指定します。実行時に有効にするには、コマンド "java" にフラグ "-ea" などを指定します。

アサーションのコンパイル

アサーションは、デフォルトでは無効になっています。コンパイル時に、"assert" をキーワードにするかどうか指定するフラグがあります。

javac コマンドのアサーションフラグ:

>javac -source 1.4 MyClass  // assert をキーワードとして認識する
>javac -source 1.3 MyClass  // assert をキーワードとして認識しない

SDK 1.4 でのデフォルトは "-source 1.3" です。デフォルトの場合は、そのソースコードの中でアサーション機能が無効と考えられ、"assert" が識別子/ラベルとして使われていても、警告はされますがコンパイルされます。一方、"-source 1.4" を指定すると、アサーション機能が有効と考えられ、エラーとなってクラスファイルも生成されません。

識別子として使った場合

オプションをつけずに、"assert" を識別子やラベルに使ったプログラムをコンパイルすると、「警告」はされますが、エラーではなく、コンパイルは成功します。"-source 1.4" を指定した場合は、「エラー」となりコンパイルできません。

オプションなし (-source 1.3):コンパイル成功(警告)
C:\java>javac AssertionKeyword.java
AssertionKeyword.java:3: 警告: リリース 1.4 では assert はキーワードなので識別子
として使うことはできません。
                int assert = 10;
                    ^
警告 1 個

C:\java>dir /B AssertionKeyword.*
AssertionKeyword.class        <- コンパイル成功
AssertionKeyword.java
-source 1.4:コンパイル失敗(エラー)
C:\java>javac -source 1.4 AssertionKeyword.java
AssertionKeyword.java:3: リリース 1.4 では assert はキーワードなので識別子として
使うことはできません。
                int assert = 10;
                    ^
エラー 1 個

アサーション機能を使った場合

逆に、アサーション機能を記述した場合は、デフォルトの場合は無効な構文を記述したことになり、エラーとなります。"-source 1.4" を使うとコンパイルに成功し、警告が出されません。

オプションなし (-source 1.3):コンパイル失敗
C:\java>javac AssertionDemo.java
AssertionDemo.java:3: 警告: リリース 1.4 では assert はキーワードなので識別子と
して使うことはできません。
                assert x >= 0;
                ^
AssertionDemo.java:3: ';' がありません。
                assert x >= 0;
                         ^
エラー 1 個
警告 1 個

C:\java>dir /b AssertionDemo.*
AssertionDemo.java        <- コンパイルに失敗し、クラスが生成できていない
-source 1.4:コンパイル成功
C:\java>javac -source 1.4 AssertionDemo.java

C:\java>dir /B AssertionDemo.*
AssertionDemo.class        <- コンパイル成功
AssertionDemo.java

アサーションの実行

java コマンドによる実行時にも、フラグで有効にしないと、アサーション行は空行と等価の扱いで無視されます。開発時にはデバッグのために有効にして、本番稼動時には無効にするのが基本的な使い方です。

java コマンドのアサーションフラグ:

-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
              指定したクラス/パッケージのアサーションを有効にする
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
              指定したクラス/パッケージのアサーションを無効にする
-esa | -enablesystemassertions
              システムクラス/ライブラリのアサーションを有効にする
-dsa | -disablesystemassertions
              システムクラス/ライブラリのアサーションを無効にする

ピリオド三つ(...)がクラス名に対するワイルドカードとして機能します。

"-ea/-da" の場合、クラス/パッケージ名を指定しないと、全てのクラスローダーが全てのパッケージに対してアサーションを有効にしてクラスをロードします。このとき、システムライブラリ(コアパッケージ"rt.jar")は、明示的なクラスローダを持たないので除外されます。コアパッケージのアサーションも有効にしたい場合は、"-esa/-dsa" を追加して指定します。

アサーションの例

次のソースコードの実行を考えて見ます。

class AssertionDemo {
	private static void verify(int x) {
		// x が負だとアサーションエラー
		assert x >= 0;
		System.out.println("x = " + x);
	}
	public static void main(String[] args) {
		int x = 0;
		verify(x);
		x = -1;
		verify(x);
	}
}

デフォルトオプション

デフォルトは "-da (disable assertions)" で、アサーション行は空行と等価で、単に無視されます。

C:\java>javac -source 1.4 AssertionDemo.java

C:\java>java AssertionDemo
x = 0
x = -1

いろいろなオプションの指定

"-ea" は、システムクラスを除いた、全てのクラスで有効にします。"-ea:..." とすると、デフォルトパッケージ(カレントディレクトリ内の名前のないパッケージ)配下の全てのクラスのアサーションが有効になります。

C:\java>java -ea AssertionDemo
x = 0
Exception in thread "main" java.lang.AssertionError
        at AssertionDemo.verify(AssertionDemo.java:3)
        at AssertionDemo.main(AssertionDemo.java:10)

C:\java>java -ea:... AssertionDemo
x = 0
Exception in thread "main" java.lang.AssertionError
        at AssertionDemo.verify(AssertionDemo.java:3)
        at AssertionDemo.main(AssertionDemo.java:10)

クラス com.msugai.MyClass だけで有効にして、クラス MyClassDemo を実行するには、次のように指定します。

>java -ea:com.msugai.MyClass MyClassDemo

尚、このとき、"MyClassDemo" から、"com.msugai.MyClass" が使われるかどうかはチェックされません。実際に存在するかもチェックされません。アサーションが有効か無効かを意味するアサーション状態は、クラスローダーが保持するテーブル上のフラグで管理され、ロードしたクラスのフラグが立っている場合は、クラス初期化時にアサーションが有効になります。存在しないクラスの場合はロードされないし初期化もされないだけです。

パッケージ com.msugai とそのサブパッケージに属する全てのクラスでだけ有効にする:

>java -ea:com.msugai... MyClassDemo

パッケージ com.msugai とそのサブパッケージに属する全てのクラスだけで有効にするが、クラス MyAltClass では無効にする:

>java -ea:com.msugai... -da:com.msugai.MyAltClass MyClassDemo

assert 式1: 式2;

上に挙げた例を次のように書き換えて実行してみましょう。

class AssertionDemo2 {
	private static void verify(int x) {
		// x が負だとアサーションエラー
		assert x >= 0: "x は正でなければならない! x = " + x;
	}
	public static void main(String[] args) {
		int x = 0;
		verify(x);
		x = -1;
		verify(x);
	}
}

"-source 1.4" をつけてコンパイルし、"-ea" でアサーションを有効にして実行してみます。

C:\java>javac -source 1.4 AssertionDemo2.java

C:\java>java -ea AssertionDemo2
Exception in thread "main" java.lang.AssertionError: x は正でなければ
ならない! x = -1
        at AssertionDemo2.verify(AssertionDemo2.java:4)
        at AssertionDemo2.main(AssertionDemo2.java:10)

例外情報に、指定したメッセージが含まれていることが分かります。

利用方法

アサーションは、コメントレベルで記載していたプログラムの仕様を、言語仕様レベルで明記したものです。あるいは、今までは if 文や例外の throw で明記していたものを制御フローから追い出すものです。

概論

アサーションの基本的な使い方はチェックポイントです。アサーション条件は、それまでの処理にバグが作り込まれている場合だけ失敗するようにすべきです。

例えば、ファイルのオープンを含む場合は、他のスレッドや JVM 外の環境に依存して失敗する可能性があるので、制御構造で例外処理すべきです。一方、メソッド引数をチェックする場合、事前に引数の妥当性検証を行う場合は、正しく意図したとおりに動作していれば、不正なメソッド引数を与えるはずがないので、アサーションでチェックすることが適切です。

典型的な例では、次の三つのケースに分類できます。

precondition
メソッドの起動時の初期条件のチェック。メソッドの引数の種類や範囲など、処理の実行前に初期化されているべき前提条件のチェック。
postcondition
メソッドのリターン前の終了条件のチェック。リソースの解放やフラグのオフ、セットしたデータの範囲など、処理の実行後に満たされていると期待する処理結果のチェック。
invariant
ありえない条件のチェック。完全に排他的であるべき行や値など、正常に動作していればありえない条件を確認するためのチェック。

メソッド引数のチェック

例えば、メソッドが配列を受け取るとき、一つも要素が与えられない場合をチェックするために次のようなコードを書いていました。

void getVlas(String[] args) throws IllegalArgumentException {
	if (args.length == 0) {
		throw new IllegalArgumentException();
	}
	...
}

throw する例外の種類は何でも構いません。ここでは例外処理が必須でない非検査例外(unchecked exceptions classes)である IllegalArgumentException をスローしましたが、検査例外(checked exceptions classes)をスローして、呼び出し元での例外処理を必須として要求することもできます。

メソッド呼び出し前に引数の妥当性検証を行うなどして、不正な引数を与えないような設計を意図している場合は、制御構造でチェックするのは冗長です。本番稼動後もこのコードは残り、コンピュータ制御は必ずこの無意味な条件分岐ロジックを通ることになります。アサーションを使うと、本番稼動時にはアサーションを無効にする "-da" で実行すれば、空行と等価なので制御に影響を及ぼしません。

また、云うまでもなく、例外をスローするのは当該メソッド内部からなので、呼び出し元の当該メソッドのクライアント・コードにとっては後の祭りです。アサーションを使えば、メソッド呼び出し前に、引数の妥当性のチェックが行えます。妥当でない値を与えようとするならば、それまでの処理にバグがあるとみなすことができます。妥当性検証を、呼び出し元から一階層上に委譲することが可能になるのです。

上記のコードを単純に置き換えるだけならば、次のコードが使えます。

void getVlas(String[] args) {
	// 引数が空であることはありえないはず
	assert args.length != 0;
	...
}

当該メソッドを提供する側にしてみれば、例外をスローしてもアサーションを使っても、呼び出し元(クライアント・コード)に対する意思表示としては変わりないかもしれません。一方、当該メソッドを利用する側(クライアント・コード)からすると、当該メソッド呼び出し前に、アサーションを記述することで、メソッド引数代入する値の妥当性検証が行えるのがメリットになります。当該メソッドの開発者が手を抜いて、例外をスローしていなくとも、与える値の妥当性検証が行えるので、それまでの制御の妥当性をチェックするのに役立てられるわけです。

上のコードの例の場合、当該メソッドの呼び出し元では、次のようにして、自分がセットする値の妥当性をチェックすることができるでしょう。

// 配列 args が空であるはずがない
assert args.length !=0;
obj.getVals(args);

スレッドのロック状態のチェック

Sun のドキュメントには幾つかの例が載っているますが、スレッドがロックを保持していることのチェックは最も有意義なものの一つでしょう。

java.lang.ThreadholdsLock() と組み合わせて、ロックを保持しているかどうかをアサーションでチェックしています。なんらかのロジック不備により、ロック取得の制御をすり抜けて当該行に到達してしまうようなウィンドウが残っていれば、アサーションが失敗して AssertionError が発生します。

private int find(Object key, Object[] arr, int start, int len) {
	// 現行スレッドは指定されたロックをすでに保持しているはず
	assert Thread.holdsLock(this);
	...
}


Copyright © 2004 SUGAI, Manabu. All Rights Reserved.