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:<class> mypackage.myprogram
java -da:<package>… mypackage.myprogram
在一个java命令中使用多项 -ea -da 参数时,后面的参数设定会覆盖前面参数的设定,比如我们可以默认启用所有的assertion功能,但针对特定的包却关闭此功能:
java -ea -da:<package>… 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:<classname> java -ea:assertionclass
启用assertionclass类的assertion
-da:<classname> java -da:assertionclass
关闭assertionclass类的assertion
-ea:<packagename>
java -ea:pkg0…
启用pkg0包的assertion
-da:<packagename>
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: <progname> 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语句是,考虑的关键是是否有助于提高代码的健壮性,是否有助于代码出错后为调试过程提供有用的信息。
