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 外の環境に依存して失敗する可能性があるので、制御構造で例外処理すべきです。一方、メソッド引数をチェックする場合、事前に引数の妥当性検証を行う場合は、正しく意図したとおりに動作していれば、不正なメソッド引数を与えるはずがないので、アサーションでチェックすることが適切です。
典型的な例では、次の三つのケースに分類できます。
例えば、メソッドが配列を受け取るとき、一つも要素が与えられない場合をチェックするために次のようなコードを書いていました。
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.Thread
の holdsLock()
と組み合わせて、ロックを保持しているかどうかをアサーションでチェックしています。なんらかのロジック不備により、ロック取得の制御をすり抜けて当該行に到達してしまうようなウィンドウが残っていれば、アサーションが失敗して AssertionError
が発生します。
private int find(Object key, Object[] arr, int start, int len) { // 現行スレッドは指定されたロックをすでに保持しているはず assert Thread.holdsLock(this); ... }