<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
摘要
J2SE 1.4 在JAVA中新增添了assertion(暫譯作斷定)功能。 最簡單的情形下,在JAVA代碼中任一行可以寫入一條布爾表達式, 在這一行代碼的最前面加上assert要害字,就可以實現這樣的功能:假如表達式為真,則代碼繼續執行;否則,拋出異常。為了實現這項功能, 在JAVA中新添加了assert要害字,AssertionError類, java.lang.ClassLoader中增加了幾個新的方法。本文章具體介紹了assert要害字的使用, 從命令行控制assertion功能,從代碼內部控制assertion功能,以及何時使用assertion功能等內容。下文中提到assert時特指assert要害字,而提到assertion則表示斷定語句或斷定功能。
assertion功能提供了一種在代碼中進行正確性檢查的機制,這種檢查通常用於開發和調試階段,到了軟件完成部署後就可以關閉。這使得程序員可以在代碼中加入調試檢查語句,同時又可以在軟件部署後關閉該功能而避免對軟件速度和內存消耗的影響。基本上,assertion功能就是JAVA中的一種新的錯誤檢查機制,只不過這項功能可以根據需要關閉。
通常在C和C++中,斷定功能語句是可以通過預處理過程而不編譯進最終的執行代碼,由於JAVA中沒有宏功能,所以在以前的java版本中斷定功能沒有被廣泛的使用,在JDK1.4中通過增加assert要害字改變了這種狀況。
這項新功能最重要的特點是斷定語句可以在運行時任意的開啟或關閉,這意味著這些起錯誤檢查功能的語句不必在開發過程結束後從源代碼中刪除。
assertion語法非常簡單,但正確的使用能幫助我們編寫出健壯(ROBAST)可靠的代碼。這篇文章中,我們不僅學習如何編寫assertion語句,更要討論應該在什麼情況下使用assertion語句。
一、assertion語法基本知識
我們可以用新的JAVA要害字assert來書寫斷定語句。一條斷定語句有以下兩種合法的形式:
assert expression1;
assert expression1 : expression2;
expression1是一條被判定的布爾表達式,必須保證在程序執行過程中它的值一定是真;expression2是可選的,用於在expression1為假時,傳遞給拋出的異常AssertionError的構造器,因此expression2的類型必須是合法的AssertionError構造器的參數類型。以下是幾條斷定語句的例子:
assert 0 < value;
assert ref != null;
assert count == (oldCount + 1);
assert ref.m1(parm);
assert要害字後面的表達式一定要是boolean類型,否則編譯時就會出錯。
以下是使用斷定語句的一個完整例子(見粗體語句行):
public class aClass {
public void aMethod( int value ) {
assert value >= 0;
System.out.println( "OK" );
}
public static void main( String[] args ){
aClass foo = new aClass();
System.out.print( "aClass.aMethod( 1 ): " );
foo.aMethod( 1 );
System.out.print( "aClass.aMethod( -1 ): " );
foo.aMethod( -1 );
}
}
這段程序通過語句 assert value >= 0; 來判定傳入aMethod方法中的參數是否不小於0,假如傳入一個負數,則會觸發AssertionError的異常。
為了和J2SE 1.4 以前的程序兼容,在JDK1.4 中的javac 和 java 命令在默認情況下都是關閉assertion功能的,即不答應使用assert作為要害字,這就保證了假如你以前編寫的程序中假如使用了assert作為變量名或是方法名,程序不必修改仍然可以運行。但需要注重的是,這些程序是無法使用JDK1.4 的javac進行重新編譯的,只能使用JDK1.3或之前的版本編譯。為了編譯我們前面寫的小程序,首先要使用符合J2SE 1.4 的編譯器,同時還要使用幾個命令行參數來使編譯器啟用assertion功能。
使用以下的命令來編譯aClass.java:
javac -source 1.4 aClass.java
假如我們使用 java aClass 來運行這段程序,就會發現assertion語句實際上並未得到執行,和javac一樣,java命令在默認情況下,關閉了assertion功能,因而會忽略assertion語句。如何啟用assertion語句將在下一節討論。
二、通過命令行控制assertion功能
assertion語句的一項最重要的特點是它可以被關閉,關閉的作用是這條代碼雖然仍存在於程序當中,但程序運行時,JVM會忽略它的存在,不予執行,這樣代碼的運行速度不會由於assertion語句的存在而受到影響,假如代碼執行過程中出現了問題,我們又可以啟用assertion語句,幫助我們進行分析判定。默認情況下,這項功能是關閉的。(提示:本小節介紹的命令行參數都是針對SUN提供的JDK1.4而言,假如使用其他公司的JDK則未必會完全一樣。)
JDK1.4 中,通過java命令的命令行選項 -ea (-enableassertions 的縮寫)來啟用。以下兩個命令是等效的:
java -ea myPackage.myProgram
java -enableassertions myPackage.myProgram
同樣,我們通過 -da (-disableassertions 的縮寫)來關閉assertion功能:
java -da myPackage.myProgram
java -disableassertions myPackage.myProgram
assertion功能還可以針對特定的包(package)或類(class)分別啟用和關閉。針對類時,使用完整的類名;針對包時,包名後面緊跟“...”:
java -ea: myPackage.myProgram
java -da:... myPackage.myProgram
在一個java命令中使用多項 -ea -da 參數時,後面的參數設定會覆蓋前面參數的設定,比如我們可以默認啟用所有的assertion功能,但針對特定的包卻關閉此功能:
java -ea -da:... myPackage.myProgram
對於未命名的包(位於當前目錄中)都屬於默認包,可以使用以下的命令控制:
java -ea:... myPackage.myProgram
java -da:... myPackage.myProgram
對於隨JVM安裝時自己附帶的所有系統類,可以分別使用 -esa(-enablesystemassertions)和-dsa(-disablesystemassertions)來控制assertion功能的啟用和關閉。在表1.1中列出了控制assertion功能參數的所有用法。
表1 JDK1.4 中java命令和assertion功能有關的命令行參數
命令行參數
實例
含義
-ea
Java -ea
啟用除系統類外所有類的assertion
-da Java -da
關閉除系統類外所有類的assertion
-ea: Java -ea:AssertionClass
啟用AssertionClass類的assertion
-da: Java -da:AssertionClass
關閉AssertionClass類的assertion
-ea:
Java -ea:pkg0...
啟用pkg0包的assertion
-da:
Java -da:pkg0...
關閉pkg0包的assertion
-esa Java -esa
啟用系統類中的assertion
-dsa Java -dsa
關閉系統類中的assertion
至此,我們前面編寫的小程序aClass可以用以下的任意命令運行:
java -ea aClass
java -ea:aClass aClass
java -ea:... aClass
運行結果如下:
aClass.aMethod( 1 ): OK
aClass.aMethod( -1 ): java.lang.AssertionError
at aClass.aMethod(aClass.java:3)
at aClass.main(aClass.java:12)
Exception in thread "main"
三、assertion命令行參數之間的繼續關系
assertion功能的啟用和關閉可以一直控制到每一個類,一個命令行可以容納任意多個-ea -da 參數,這些參數之間是如何相互起作用的,基本上遵循兩個原則:特定具體的設定優先於一般的設定,後面的設定優先於前面的設定。我們看下面的例子:
// Base.java
package tmp;
public class Base{
public void m1( boolean test ){
assert test : "Assertion failed: test is " + test;
System.out.println( "OK" );
}
}
// Derived.java
//
package tmp.sub;
import tmp.Base;
public class Derived extends Base{
public void m2( boolean test ){
assert test : "Assertion failed: test is " + test;
System.out.println( "OK" );
}
public static void printAssertionError( AssertionError ae ){
StackTraceElement[] stackTraceElements = ae.getStackTrace();
StackTraceElement stackTraceElement = stackTraceElements[ 0 ];
System.err.println( "AssertionError" );
System.err.println( " class= " + stackTraceElement.getClassName() );
System.err.println( " method= " + stackTraceElement.getMethodName() );
System.err.println( " message= " + ae.getMessage() );
}
public static void main( String[] args ){
try{
Derived derived = new Derived();
System.out.print( "derived.m1( false ): " );
derived.m1( false );
System.out.print( "derived.m2( false ): " );
derived.m2( false );
}catch( AssertionError ae ){
printAssertionError( ae );
}
}
}
Base類和Derived類個有一個方法m1和m2,因為Derived是Base的子類,所以它同時繼續了方法m1。
首先在啟用所有類的assertion功能後,運行程序:
java -ea tmp.sub.Derived
derived.m1( false ): AssertionError
class= tmp.Base
method= m1
message= Assertion failed: test is false
然後,我們單獨關閉Base類的assertion功能的情況下,運行程序:
java -ea -da:tmp.Base tmp.sub.Derived
derived.m1( false ): OK
derived.m2( false ): AssertionError
class= tmp.sub.Derived
method= m2
message= Assertion failed: test is false
可以看到,derived.m1(false)語句沒有觸發異常,顯然這條語句是受到Base類的assertion功能狀態控制的。假如繼續研究,會發現以下兩條語句的作用是一樣的:
java -da:tmp.Base -ea:tmp... tmp.sub.Derived
java -ea:tmp... -da:tmp.Base tmp.sub.Derived
這說明前面提到的兩條原則是在起作用。
四、在程序代碼中控制assertion功能
assertion功能的啟用和關閉也可以通過代碼內部進行控制,一般情況下,是不需要這樣做的,除非我們是在編寫java程序的調試器,或是某個控制java程序運行的程序。
每一個java類都有一個代表其assertion功能啟用與否的標識符。當程序運行到assertion語句行時,JVM就會檢查這行assertion語句所在類的assertion標識符,假如是true,那就會執行這條語句,否則就忽略這條語句。
這個assertion標識符可以ClassLoader的以下方法設定:
public void setClassAssertionStatus(String className, boolean enabled);
className--需要設定assertion標識符的類
enabled--assertion功能啟用或是關閉
這個assertion標識符也可以針對整個包一起控制,用ClassLoader的另一個方法設定:
public void setPackageAssertionStatus(String packageName, boolean enabled);
className--需要設定assertion標識符的包
enabled--assertion功能啟用或是關閉
注重這個方法對於包packageName 的所有子包也起作用。
ClassLoader還有一個方法可以設定所有通過此ClassLoader裝載的類的默認assertion狀態:
public void setDefaultAssertionStatus(boolean enabled);
最後,ClassLoader有一個方法可以清除所有以前進行的設定:
public void clearAssertionStatus();
Class類也新增加了一個與assertion功能有關的方法,利用這個方法可以知道某個類的assertion功能是啟用的還是關閉的:
public boolean desiredAssertionStatus();
注重:通過ClassLoader來設定assertion標識符只會影響此後通過該ClassLoader裝載的類,而不會改變此前已經裝載的類的assertion標識符狀態。
五、AssertionError介紹
java.lang包增加了AssertinError類,它是Error的直接子類,因此代表程序出現了嚴重的錯誤,這種異常通常是不需要程序員使用catch語句捕捉的。AssertionError除了一個不帶參數的缺省構造器外,還有7個帶單個參數的構造器,分別為:
object
boolean
char
int
long
float
double
我們前面提到的assertion語句的兩種語法形式如下:
assert expression1;
assert expression1 : expression2;
第一種形式假如拋出異常,則調用AssertionError的缺省構造器,對於第二種形式,則根據expression2值的類型,分別調用7種單參數構造器中的一種。
下面我們對例一稍做修改,看看第二種assertion表達式的用法:
public class aClass2{
public void m1( int value ){
assert 0 <= value : "Value must be non-negative: value= " + value;
System.out.println( "OK" );
}
public static void printAssertionError( AssertionError ae ){
StackTraceElement[] stackTraceElements = ae.getStackTrace();
StackTraceElement stackTraceElement = stackTraceElements[ 0 ];
System.err.println( "AssertionError" );
System.err.println( " class= " + stackTraceElement.getClassName() );
System.err.println( " method= " + stackTraceElement.getMethodName() );
System.err.println( " message= " + ae.getMessage() );
}
public static void main( String[] args ){
try{
aClass2 fooBar = new aClass2 ();
System.out.print( " aClass2.m1( 1 ): " );
fooBar.m1( 1 );
System.out.print( " aClass2.m1( -1 ): " );
fooBar.m1( -1 );
}
catch( AssertionError ae ){
printAssertionError( ae );
}
}
}
運行結果如下:
aClass2.m1( 1 ): OK
aClass2.m1( -1 ): AssertionError
class= aClass2
method= m1
message= Value must be non-negative: value= -1
從以上的結果可以可以發現,assertion語句 : 之後的參數被傳遞給了AssertionError的構造器,成為StackTrace的一部分。
因為AssertionError代表正常時不應該出現的錯誤,所以一旦出現,應盡快拋出,中止程序的執行,以引起程序維護人員的注重。但有時我們也需要捕捉AssertionError,執行一些任務,然後,重新拋出AssertionError。比如,我們的程序在網絡中的某處有控制台監控整個系統的運行,我們就需要首先獲得關於AssertionError的異常信息,通過網絡傳送給控制台,然後再拋出AssertionError,中止程序,就象例3做的那樣:
public void method() {
AssertionError ae = null;
try {
int a = anotherMethod();
// ...
assert i==10;
// ...
}catch( AssertionError ae2 ){
ae = ae2;
StackTraceElement stes[] = ae.getStackTrace();
if (stes.length>0) {
StackTraceElement first = stes[0];
System.out.println( "NOTE: Assertion failure in "+
first.getFileName()+" at line "+first.getLineNumber() );
} else {
System.out.println( "NOTE: No info available." );
}
throw ae;
}
}
六、是否使用assertion的幾條准則
對assertion而言,重要的不是如何使用,而是何時何地使用。這一節將介紹幾條准則,歸納在表2當中,可以幫助我們在決定是否應該使用assertion語句這樣的問題時,做出正確的判定。
表2:是否使用assertion語句的判定原則
應該使用的情形
不應該使用的情形
用於保證內部數據結構的正確
不用於保證命令行參數的正確
用於保證私有(private)方法參數的正確
不用於保證公共(public)方法參數的正確
用於檢查任何方法結束時狀態的正確
不用於檢查外界對公共方法的使用是否正確
用於檢查決不應該出現的狀態
不用於保證應該由用戶提供的信息的正確性
用於檢查決不應該出現的狀態,即使你肯定它不會發生
不要用於代替if語句
用於在任何方法的開始檢查相關的初始狀態
不要用做外部控制條件
用於檢查一個長循環執行過程中的的某些狀態
不要用於檢查編譯器、操作系統、硬件的正確性,除非在調試過程中你有充分的理由相信它們有錯誤
assertion語句並不是if (expression) then 語句的簡寫,相反,它是保證代碼健壯的重要手段。重要的是正確的區分何時使用assertion,何時使用一般的條件表達式。以下幾條是使用assertion語句時需注重的情形。
不要使用assertion來保證實命令行參數的正確
使用命令行參數的程序都要檢查這些參數的正確性,但這應該通過正常的條件檢查來實現。以下就是一個錯誤使用assertion的例子。
public class Application{
static public void main( String args[] ) {
// BAD!!
assert args.length == 3;
int a = Integer.parseInt( args[0] );
int b = Integer.parseInt( args[1] );
int c = Integer.parseInt( args[2] );
}
}
假如你的程序必須有三個參數,否則不能運行的話,那更好的方法是拋出適當的RuntimeException:
public class App{
static public void main( String args[] ) {
if (args.length != 3)
throw new RuntimeException( "Usage: a b c" );
int a = Integer.parseInt( args[0] );
int b = Integer.parseInt( args[1] );
int c = Integer.parseInt( args[2] );
}
}
assertion語句的作用是保證程序內部的一致性,而不是用戶與程序之間的一致性。
使用assertion來保證傳遞給私有方法參數的正確性
以下的私有方法有兩個參數,一個是必須的,一個是可選的。
private void method( Object required, Object optional ) {
assert( required != null ) : "method(): required=null";
}
通常,私有方法只是在類的內部被調用,因而是程序員可以控制的,我們可以預期它的狀態是正確和一致的。我們也就可以假設對它的調用是正確的,這自然包括調用參數的正確,因此可以使用assertion語句來保證這種准確性。
這一原則同樣適用protected和package-protected方法。
不要使用assertion來保證傳遞給公共方法參數的正確性
下面這個公共方法有兩個參數,source和sink分別代表頭和尾,它們之間是互連的。在斷開它們之間的連接之前,必須保證它們之間已經是互連的:
public void disconnect( Source source, sink sink ) {
// BAD!!
assert source.isConnected( sink ) :
"disconnect(): not connected "+source+","+sink;
}
由於這個方法是public,因此source和sink之間的關系是我們不能控制的。這種我們不能保證正確的場合是不適合使用assertion語句的。
更重要的是,public方法可能被許多不同的程序調用,它必須保證在不同的調用情形下,它的接口特性是完全相同的。由於assertion語句是不能保證會被運行的,這取決於運行環境中的assertion功能是否被啟用,假如assertion功能未被啟用,就無法保證這個public 方法參數的正確。
這種情況下,你應該假設調用代碼是有可能出錯的,拋出適當的異常:
public void disconnect( Source source, sink sink ) throws IOException{
if (!source.isConnected( sink )) {
throw new IOException(
"disconnect(): not connected "+source+","+sink );
}
}
不要使用assertion來保證外部對公共方法的用法模式是否正確
下面這個public類可能處於兩種狀態,open或是closed。打開一個已經打開的Connection,關閉一個已經關閉的Connection都是錯誤的。但我們不應該使用assertion功能來保證這種錯誤不會發生:
public class Connection{
private boolean isOpen = false;
public void open() {
// ...
isOpen = true;
}
public void close() {
// BAD!!
assert isOpen : "Cannot close a connection that is not open!";
// ...
}
}
我們只有在Connection類是private類時,或者保證這個類對外界是不可見的,並且願意相信所有使用這個類的的代碼都是正確的情況下,才可以使用這種用法。
然而,Connection類是被公共使用的,完全有可能某個使用Connection類的程序存在漏洞,而試圖關閉一個未打開的連接。由於存在這種可能,使用拋出異常是更適合的:
public class Connection{
private boolean isOpen = false;
public void open() {
// ...
isOpen = true;
}
public void close() throws ConnectionException {
if (!isOpen) {
throw new ConnectionException(
"Cannot close a connection that is not open!" );
}
// ...
}
}
不要使用assertion來保證對用戶提供的某項信息的要求
在下面這段代碼裡,程序員使用assertion來確保郵政編碼有5或9位數字:
public void processZipCode( String zipCode ) {
if (zipCode.length() == 5) {
// ...
} else if (zipCode.length() == 9) {
// ...
} else {
// BAD!!
assert false : "Only 5- and 9-digit zip codes supported";
}
}
assertion應該用來保證內部的一致性,而不是保證正確的輸入。上面的代碼應該在錯誤是直接拋出異常:
public void processZipCode( String zipCode )
throws ZipCodeException {
if (zipCode.length() == 5) {
// ...
} else if (zipCode.length() == 9) {
// ...
} else {
throw new ZipCodeException(
"Only 5- and 9-digit zip codes supported" );
}
}
使用assert來保證對內部數據結構方面假設的正確
下面的私有方法的參數是3個整數構成的數組。我們可以用assertion來確認這個數組有正確的長度:
private void showDate( int array[] ) {
assert( array.length==3 );
}
我們預期對這個方法的調用都是正確的,即只提供長度為3的數組,assertion語句在此正是起到這個作用。
Java語言對數組已經有邊界檢查的功能,這保證程序不會讀取數組邊界之外的值,這段代碼中assertion語句的作用就不如在C或C++中的作用那麼重要,但也不意味著這是多余的。
使用assertion來檢查任何方法將結束時狀態
我們看下面的例子是如何在方法返回之前檢查最後的狀態:
public class Connection{
private boolean isOpen = false;
public void open() {
// ...
isOpen = true;
// ...
assert isOpen;
}
public void close() throws ConnectionException {
if (!isOpen) {
throw new ConnectionException(
"Cannot close a connection that is not open!" );
}
// ...
isOpen = false;
// ...
assert !isOpen;
}
}
這樣做的好處是這些方法內部的代碼不管如何復雜,或是經過多少修改變動,利用最後的一條assertion語句我們都可以保證方法返回時某個狀態的正確。
使用assertion檢查不應該發生的狀態
下面的代碼正是起到這種作用:
private int getValue() {
if (/* something */) {
return 0;
} else if (/* something else */) {
return 1;
} else {
return 2;
}
}
public void method() {
int a = getValue(); // returns 0, 1, or 2
if (a==0) {
// deal with 0 ...
} else if (a==1) {
// deal with 1 ...
} else if (a==2) {
// deal with 2 ...
} else {
assert false : "Impossible: a is out of range";
}
}
這個例子中,getValue的返回值只能是0,1,2,正常時出現其他值的情形是不應該的,使用assertion來確保這一點是最合適的。
提示:一個很好的編程習慣是對每組if else 語句總是寫一條最後的else語句來包括所有的其他情況,假如你能保證程序一定不會進入這條語句,加進一條assert false;。
使用assertion檢查任何方法開始時的初始狀態
在這個例子裡,方法processZipCode()必須保證zipCode的格式是有效的,才進行進一步的處理:
public void processZipCode( String zipCode ) {
assert zipCodeMapIsValid();
// ...
}
這樣做可以使程序中的漏洞及早被發現。
最後的原則:有勝於無
加入assertion語句是非常簡單的,可以在代碼編寫的任何階段加入,而且對程序運行速度性能等方面帶來的影響也是稍微的,因此假如你對程序的某些環節有懷疑,不確定的時候,盡管加入assertion語句。一條永遠不會觸發的assertion語句並沒有什麼壞處,但假如應該觸發卻沒有assertion語句存在,那時給我們帶來的麻煩卻是巨大的。
七、結束語:
我們可以發現,幾乎所有的assertion代碼看起來都是多余的,而這正是assertion語句的特點,假如我們發現某條assertion代碼在程序中的作用是必不可少的,那我們肯定是錯誤的使用了assertion語句。assertion語句並不構成程序正常運行邏輯的一部分,時刻記住在運行時它們可能不會被執行。我們在判定是否應該使用assertion語句是,考慮的要害是是否有助於提高代碼的健壯性,是否有助於代碼出錯後為調試過程提供有用的信息。