第3章 セキュアJava プログラミング
[3-5.]
Javaのアサーション
J2SE 1.4 からJava の新しい言語要素としてassert ステートメントが導入された。これはソフトウェアの正しさを確実にするための手法の一つ「アサーション」を記述するためのものである。



 PDF
アサーション
アサーション(assertion,「表明」と訳される)は,ソフトウェアが正しく動作することをより確実にするために使われる手法の一つである。アサーションは,データ処理のロジックの各地点でどのような条件が成立しているはずであるか,という検証用のコードをそれぞれの箇所に配置し違反を自動で検出するという形をとる。アサーションへの違反が検出されたら何らかの誤りがあることになり,プログラムには修正の必要があることが分かる。したがって,多くのアサーションを入れ十分な種類のテストデータを使ってソフトウェアの開発とテストを進めれば,その過程でプログラムの誤りの多くが訂正されると期待できるのである。
 
Java でも,J2SE 1.4(注1)から,新たな言語要素としてアサーションの機能が導入された。
(注1・Java 2 Standard Edition, version 1.4。しばらく前までJDK 1.4 とよばれていたものである。)
assert ステートメント
アサーション機能を実現するものとしてJava の言語仕様に加えられたものはassert ステートメントである。assert ステートメントは次のいずれかの形式で記述する。
    assert exp1;
    assert exp1 : exp2;
exp1が検証されるべき条件を記述するboolean 型の式で,これがfalse(偽)の場合アサーション違反となり,エラーAssertionError がスローされる。exp2はexp1の値がfalse となってAssertionError オブジェクトが作られるときのコンストラクタへのパラメタとなる。
 
リスト1を見ていただきたい。このプログラムの断片は,13行目のコメントにある通り,「変数swは0 または正の値をとり決して負の値にはならない」という仮定の下で書かれたコードである。もしその仮定が誤っていたらプログラムは誤ったまま処理を続行し,しかもその誤りには誰も気付かないかも知れない。
リスト1
    ...
 11 if( sw == 0 ){
 12   hogehoge();
 13 } else {     // ここではsw > 0 であるはず
 14   foobar();
 15 }
    ...
もし,プログラムを書くときに何らかの仮定を置いているのなら,コメントに「〜であるはずだ」と書くのではなく,そのことが検証できるアサーションを書くべきだ。
 
リスト2は,リスト1のコメントをassertステートメントに書き換えたものである。14 行目でもし変数sw の値が正の数でなければ,このassert ステートメントからAssertionError が発生する(画面1)
リスト1
    ...
 11 if( sw == 0 ){
 12   hogehoge();
 13 } else {
 14   assert sw > 0 : "sw = " + sw;  ← 追加
 15   foobar();
 16 }
    ...
画面1 仮定していた条件が成立しないとAssertionError が発生する
 画面1 仮定していた条件が成立しないとAssertionError が発生する
コンパイル
今後は状況が変わるかもしれないが,いまのところJavaではassertステートメントを含むコードはまだあまり一般的でないという扱いのようだ。アサーションを含むコードに対応したJ2SE 1.4 SDKのjavacでコンパイルする際,コマンドラインオプションに「-source 1.4」を指定する必要がある。
    例  javac -source 1.4 SomeClass.java
-source オプションのデフォルト値はいまのところ「1.3」であり,指定を省略するとassert ステートメントのところでコンパイルエラーとなるので注意が必要だ。
 
assertステートメントがコンパイルされると,それに対応するアサーションのコードが生成されてクラスのバイトコードの中に組み込まれる。アサーションのコードは常にクラスの中に存在し,原則として取り除くことはできない。
アサーションのオン/オフ
各クラスに含まれたアサーションのコードは,プログラム実行時の明示的な指示により機能させる。アサーションを有効にするにはjava コマンドに「-ea」オプションを指定してクラスを起動する。
    例  java -ea SomeClass
このようにして実行すると,この実行にかかわる全てのクラスのアサーションがはたらくようになる。「-ea」オプションを付けなければどのアサーションも機能しない。
 
実行時のアサーションのオン/オフを指定するオプションにはいくつかバリエーションがあり,それらを使うと「プログラム全体」「単一のクラス」「パッケージ」「システムクラス」といった単位に対する細かな指定が可能である。オプションの詳細はJava のアサーションのドキュメントをご覧いただきたい。
体系だてたアサーションの配置
上の例のように,プログラマが頭の中だけで設定している各種の仮定があればそれをassertステートメントに記述するといったアプローチも十分に役立つものであるが,より体系だててアサーションを使用すると効果が大きい。
 
バートランド・メイヤー(Bertrand Meyer)は,呼び出す側のモジュールと呼ばれる側のモジュールの間の役割分担がより厳密に守られるようにするということの積み重ねでソフトウェア全体の正しさを確保しようとする手法「契約によるプログラミング」(のちに「契約による設計」に発展させた)を導入する際,効果的なアサーションの書き方についても手本を示している。
 
メイヤーは次の3種類の条件に対してアサーションを記述するように言っている(図1)。
図1 3種類の条件を検証する4箇所のアサーション
 図1 3種類の条件を検証する4箇所のアサーション
事前条件
事前条件は,メソッド呼び出しで呼び出し元がメソッドに渡すパラメタが満たすべき条件である。
 
事前条件のアサーションは,メソッド呼び出しの最初の段階で,呼び出し元が渡したパラメタがすべて呼ばれる側のメソッドにとって受け入れることのできるものであることを検証するものである。事前条件への違反があれば呼び出し元のメソッドに問題があることが分かる。
事後条件
事後条件は,メソッドの処理結果が満たすべき条件である。
 
事後条件のアサーションは,呼び出されたメソッドの実行の最後の段階で,そのメソッドが呼び出し元に返そうとしている結果が正しいことを検証するものである。事後条件への違反があれば,結果を返そうとしているメソッドに問題があることが分かる。
クラス不変条件
クラス不変条件は,オブジェクトの内部状態が適切に保たれていることを示す条件である。
 
クラス不変条件のアサーションは,メソッド呼び出しの最初の段階でそのオブジェクトの内部状態が壊れずに正しいものになっていること,およびメソッド実行の最後の段階でもオブジェクトの内部状態が壊れずに正しく保たれていることの両方を検証するものである。メソッド呼び出しの最初の段階でクラス不変条件への違反があれば,すでにそれ以前の処理でオブジェクトの内容が損なわれていることが分かる。メソッド実行の最後の段階でクラス不変条件への違反があれば,いま実行したメソッドに問題があることが分かる。
事前条件のアサーション
リスト3は,与えられたlongの値に対する平方根の 近似値をふたたびlongの値で返すメソッド squarerootの始まりの部分である。平方根の計算には 負の値を与えてはならないから,
    assert x >= 0;
のようなアサーションが書かれている。
リスト3 事前条件のアサーション
  1 public long squareroot( long x ) {
  2     assert x >= 0;
  3
    ...
 29
 30 }
事後条件のアサーション
リスト4は,上と同じlongによる平方根計算,squareroot メソッドの終わりの部分である。23 行目で得られた結果resultが,与えられた入力xの平方根の近似値(端数切り捨て)になっているかどうかを検証している。
リスト4 事後条件のアサーション
  1 public long squareroot( long x ) {
  2
    ...
 22
 23 result = 結果;
 24
 25 assert
 26 result * result <= x &&
 27 (result + 1) * (result + 1) > x;
 28
 29 return result;
 30 }
クラス不変条件のアサーション
リスト5は,簡単な預金口座のクラスの一部である。このクラスは最初の2つのフィールドdepositとwithdrawalにそれぞれ預入額と引出額の合計を別々に保持した上で3番目のフィールドbalanceに最終的な残高も持っている,という想定である。balanceの値は次の式で計算される値に維持されることになっている。
    deposit - withdrawal
そうした条件のもと,ある金額だけ預金を引き出すdoWithdrawalメソッドが15行目以降に定義されている。このメソッドでは,データ処理に入る前の21行目とデータ処理を行ったあとの28行目にクラス不変条件のアサーションが書かれている。ここでのアサーションの条件式はselftestというメソッドにくくりだしてある。
リスト5 クラス不変条件のアサーション
  1 public class MyAccount {
  2   private long deposit;      // 預入額の合計
  3   private long withdrawal;   // 引出額の合計
  4   private long balance;      // 残高
  5
  6   // クラス不変条件
  7   protected boolean selftest() {
  8     // 残高は負であってはならず,
  9     // 預入額合計と引出額合計の差に等しいこと。
 10     return
 11       balance >= 0 &&
 12       balance == (deposit - withdrawal);
 13   }
 14
 15   // 預金の引き出し処理
 16   public void doWithdrawal( long delta ) {
 17     // 事前条件のアサーション
 18     assert delta > 0;
 19         // 引出額は0やマイナスではいけない
 20     // クラス不変条件のアサーション
 21     assert selftest();
 22
 23     // データ処理
 24     withdrawal += delta;
 25     balance -= delta
 26
 27     // クラス不変条件のアサーション
 28     assert selftest();
 29   }
 30
 31     ...
 32
 33 }
これらのアサーションを配置しておくと,誤って25行目のbalance の更新処理を記述し忘れたような場合,オブジェクトの内部状態が正しく維持されていないことをこれらのアサーションが教えてくれる。
処理ロジックとアサーションの棲み分け
プログラム本来のデータ処理ロジックとそれを検証するためのアサーションを混同してはならない。
 
メソッドの入力パラメタに妥当でないデータが入り込むことが明らかな文脈では,事前条件のアサーションではなく,入力データチェックのロジックが必要である。入力データが十分に吟味され,無害化されたのちに正当なデータしか渡ってこないはずのメソッドの呼び出しでは,事前条件のアサーションの出番だ。
 
事後条件についても同様である。プログラム内部のロジックは一応正しいのに,外部からの干渉があり正しい処理結果が出せないことがときどき起こるといった場合がある。たとえば確保したはずのファイルがたまたま同時に動いている別のプログラムにより別のものに入れ替えられてしまうなどである。その場合,処理が成功したかどうかの検証は事後条件のアサーションではなく,正規の処理ロジックとして記述すべきだ。
 
上記に挙げた検証は,最初はアサーションとして書かれていてもよい。もし,ロジックにすることをプログラマが思いついていないのなら,むしろ積極的にアサーションを書いておくべきだ。そして,ソフトウェアのテストの過程でこれらの問題が外部に由来すると明らかになった時点で,裏方の存在であるアサーションから正規の処理ロジックへ「昇格」させるのである。
まとめ
Javaにもアサーション機能が備わった。アサーションはプログラムの正しさを確実にするための手段である。プログラマが頭の中だけで仮定している条件はアサーションに明示して検証すべきである。組織だったアサーションの設定の仕方には,事前条件,事後条件,クラス不変条件を検証するというやり方がある。アサーションはあくまで検証のためのものであるから,本来のデータ処理ロジックと混同しないことが大切だ。
参考文献
『オブジェクト指向入門』,Bertrand Meyer著,酒匂寛,酒匂順子訳,二木厚吉監修,1990年,アスキー出版局
『Assertion Facility』(英文)