この節では、アサーションの仕様ではなく、アサーションを使用するときの注意事項について説明します。標準化コミュニティから見れば、この節で説明することは標準ではありません。
この節では、アサーション要素について適切な使用法と不適切な使用法の例を示します。これらの例はすべてを網羅しているわけではなく、アサーションの本来の使用法を理解してもらうことを目的としています。
一般的には、プログラムの動作に関する重要な前提を示す短いアサーションを頻繁に使用することが適切です。
アサーション機能が存在しなければ、多くのプログラマは次のようなコメントを使用します。
if (i%3 == 0) { ... } else if (i%3 == 1) { ... } else { // (i%3 == 2) ... } |
このように不変条件を表明している部分がコードに存在する場合、assert に変更するべきです。上記例の場合、assert が if-else 文の else 節を保護するには次のように変更します。
if (i % 3 == 0) { ... } else if (i%3 == 1) { ... } else { assert i%3 == 2; ... } |
i が負の場合、% 演算子は真の mod 演算子ではないため上記例のアサーションは失敗しますが、残りの計算は続けられ、その結果は負になります。
アサーションを効果的に使用できるもう一つの例は、デフォルトの case を持たない switch 文です。
たとえば :
switch(suit) { case Suit.CLUBS: ... break; case Suit.DIAMONDS: ... break; case Suit.HEARTS: ... break; case Suit.SPADES: ... } |
プログラマはおそらく、「上記 switch 文の 4 つの case のうちの 1 つが常に実行される」と仮定しています。この仮定をテストするには、次のようなデフォルトの case を追加します。
default: assert false; |
通常は、プログラマが「本来到達すべき場所ではない」と仮定している場所ならば、次の文はどこにでも置けるはずです。
assert false; |
たとえば、次のようなメソッドがあると仮定します。
void foo() { for (...) { if (...) return; } // Execution should never reach this point!!! } |
最後のコメントを次のように変更します。
assert false; |
この技法を使用するときには十分に注意してください。ある文が到達不能である場合 (JLS 14.19 を参照)、その文が到達不能であることを表明しようとすると、コンパイル時にエラーが発生します。
assert 構文は、「規約による設計」を完全に適用していない機能ですが、プログラミングにおける非公式の「規約による設計」スタイルをサポートするのに役立ちます。
規約上、公開メソッドの事前条件はメソッド内の明示的なチェックによって実施され、結果として、指定された特別な例外が発生します。次に例を示します。
/** * Sets the refresh rate. * * @param rate refresh rate, in frames per second. * @throws IllegalArgumentException if rate <= 0 or * rate> MAX_REFRESH_RATE. */ public void setRefreshRate(int rate) { // Enforce specified precondition in public method if (rate <= 0 || rate> MAX_REFRESH_RATE) throw new IllegalArgumentException("Illegal rate: " + rate); setRefreshInterval(1000/rate); } |
assert 構文を追加しても、この規約には影響ありません。アサーションが有効であるかどうかにかかわらず、アサーションが入っているメソッドが必ず引数チェックを実施するような事前条件に対しては、アサーションは不適切です。さらに、assert 構文は指定されたタイプの例外をスローしません。
しかし、private メソッドには事前条件があり、かつ、クラスの作成者が「クライアントがそのクラスで何をしてもその事前条件は保たれる」と考えている場合には、アサーションは適切です。次に例を示します。
/** * Sets the refresh interval (must correspond to a legal frame rate). * * @param interval refresh interval in milliseconds. */ private void setRefreshInterval(int interval) { // Confirm adherence to precondition in nonpublic method assert interval> 0 && interval <= 1000/MAX_REFRESH_RATE; ... // Set the refresh interval } |
MAX_REFRESH_RATE が 1000 よりも大きく、ユーザが選択したリフレッシュレートが 1000 よりも大きい場合、上記アサーションは失敗するので注意してください。 つまり、これはライブラリのバグを示しています。
public メソッドに指定されているかどうかにかかわらず、事後条件チェックはアサーションで実装するのが最適です。次に例を示します。
/** * Returns a BigInteger whose value is (this-1 mod m). * * @param m the modulus. * @return this-1 mod m. * @throws ArithmeticException m <= 0, or this BigInteger * has no multiplicative inverse mod m (that is, this BigInteger * is not relatively prime to m). */ public BigInteger modInverse(BigInteger m) { if (m.signum <= 0) throw new ArithmeticException("Modulus not positive: " + m); if (!this.gcd(m).equals(ONE)) throw new ArithmeticException(this + " not invertible mod " + m); ... // Do the computation assert this.multiply(result).mod(m).equals(ONE); return result; } |
実際には、2 番目の事前条件(this.gcd(m).equals(ONE)) は冗長であるので、計算を実行する前にチェックしません。この事前条件のチェックは、標準アルゴリズムによるモジュラ乗法逆数計算の副作用として行われます。
場合によっては、計算を実行する前にいくつかのデータを保存しておき、計算が完了した後に事後条件をチェックする必要があります。このような事後条件のチェックを行うには、2 つの assert 文と、計算の後にチェック (または再チェック) できるように 1 つまたは複数の変数の状態を保存するように設計された単純な内部クラスを使用します。たとえば、次のようなコードがあると仮定します。
void foo(int[] array) { // Manipulate array ... // At this point, array will contain exactly the ints that it did // prior to manipulation, in the same order. } |
次に、上記メソッドを変更して、形だけのアサーションから機能するアサーションに変更する方法を示します。
void foo(final int[] array) { class DataCopy { private int[] arrayCopy; DataCopy() { arrayCopy = (int[])(array.clone()); } boolean isConsistent() { return Arrays.equals(array, arrayCopy); } } DataCopy copy = null; // Always succeeds; has side effect of saving a copy of array assert (copy = new DataCopy()) != null; ... // Manipulate array assert copy.isConsistent(); } |
このメソッドを簡単に説明すると、複数のデータフィールドを保存して、計算前後の値に関連する複雑なアサーションを任意にテストします。
最初の assert 文 (副作用として単独で実行される) をよりわかりやすくすると、次のようになります。
copy = new DataCopy(); |
しかし、アサーションが有効であるかどうかにかかわらず、この文は配列をコピーするので、「無効である場合、アサーションは何にも影響してはならない」という規則に違反します。
すでに述べたとおり、アサーションは内部不変条件をチェックするのに適切です。アサーション機構自体は表明を行うために特別なスタイルを要求しません。ときには、必要な制約をチェックする数多くの式を単一の内部メソッドに結合して、そのメソッドをアサーションで呼び出すようにする方が便利な場合もあります。たとえば、何かのバランスツリーのデータ構造を実装しようとしていると仮定します。この場合はおそらく、ツリーが実際に (データ構造が示すとおりに) 効率的に構築されているかどうかをチェックする private メソッドを実装する方が適切です。
// Returns true if this tree is properly balanced private boolean balanced() { ... } |
このメソッドはクラス不変条件です。どのメソッドにおいても、クラス不変条件は常に (メソッドが完了する前でも後でも) 真である必要があります。これをチェックするには、次のようにアサーションでチェックします。
assert balanced(); |
各 public メソッドとコンストラクタの直前に、assert 行を置きます。データ構造がネイティブメソッドによって実装されている場合を除いて、一般的に、各 public メソッドの先頭に同様なチェックを置く必要はありません。この場合、メソッドの呼び出し間に、メモリー破壊のバグが「ネイティブピア」のデータ構造を破壊する可能性があります。このようなメソッドの先頭にあるアサーションが失敗した場合、このようなメモリー破壊が発生したことを意味します。同様に、ほかのクラスによって状態が変更される可能性があるクラスにおいては、クラス不変条件のチェックをメソッドの先頭に置くことが望まれます。しかし、クラスの状態はほかのクラスから直接見ることができないように設計することが推奨されています。
リソースが制限されているデバイスを開発しているプログラマは、クラスファイルからアサーションを完全に取り去ってしまいたいと思うかもしれません。こうすることによって、フィールドではアサーションを有効にできなくなりますが、クラスファイルのサイズを減らすことができるので、おそらく、クラスをロードする速度を上げることができます。高品質の JIT が存在しない場合、アサーションのトレースを減らし、ランタイム性能を上げることができます。
アサーション機能は、クラスファイルからアサーションのトレースを削除する機能を直接的にはサポートしていません。しかし、次のように「条件付きコンパイル」(JLS 14.19 を参照) で assert 文を使用することができます。
static final boolean asserts = ... ; // false to eliminate asserts if (asserts) assert <expr> ; |
この方法でアサーションを使用した場合、コンパイラは自由に、自分が生成するクラスファイルからアサーションのトレースをすべて削除できます。リソースが制限されているデバイスのコードを生成するときには、この方法をできるだけ使用することが推奨されます。
重要なシステムを扱うプログラマにとっては、フィールドでアサーションが無効にならないほうが望ましいかもしれません。次に、アサーションが無効になっている場合、そのクラスをロードしないようにする例を示します。
static { boolean assertsEnabled = false; assert assertsEnabled = true; // Intentional side effect!!! if (!assertsEnabled) throw new RuntimeException("Asserts must be enabled!!!"); } |