铁文整理
11.4 断言
在一个具有自我保护能力的程序中,断言是一个常用的习语。假设确信某个属性符合要求,并且代码的执行依赖于这个属性。例如,需要计算
double y = Math.sqrt(x);
我们确信,这里的x是一个非负数值。原因是:x是另外一个计算的结果,而这个结果不可能是负值;或者x是一个方法的参数,而这个方法要求它的调用者只能提供一个正整数。然而,还是希望进行栓査,以避免让错误的数值参与计算操作。当然,也可以抛出一个异常:
if (x < 0)
throw new IllegalArgumentException("x < 0");
但是这段代码会一直保留在程序中,即使测试完毕也不会自动地删除。如果在程序中含有大量的这种检査,程序运行起来会相当慢。
断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插入的检测语句将会被自动地移走。
在Java SE 1.4中,Java语言引入了关键字assert。这个关键字有两种形式:
assert 条件;
和
assert 条件 : 表达式;
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。
注释:“表达式”部分的惟一目的是产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。正如JDK文档所描述的那样:如果使用表达式的值,就会鼓励程序员试图从断言中恢复程序的运行,这不符合断言机制的初衷。
要想断言x是一个非负数值,只需要简单地使用下面这条语句:assert x >= 0;或者将x的实际值传递给AssertionError对象,从而可以在后面显示出来:assert x >= 0 : x;
C++注释:C语言中的assert宏将断言中的条件转换成一个字符串。当断言失败时,这个字符串将会被打印出来。例如,若assert(x>=0)失败,那么将打印出失败条件x>=0。在Java中,条件并不会自动地成为错误报告中的一部分。如果希望看到这个条件,就必须将它以字符串的形式传递给入AssertionError对象:assert x >= 0 : "x >= 0";
11.4.1 启用和禁用断言
在默认情况下,断言被禁用。可以在运行程序时用-enableassertion或-ea选项启用它:
java -enableassertions MyApp
需要注意:在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。
也可以在某个类或某个包中使用断言。例如,
java -ea:MyCla5S -ea:com.mycompany.mylib... MyApp
这条命令将开启MyClass类以及在com.mycompany.mylib包和它的子包中的所有类的断言。选项-ea将开启默认包中的所有类的断言。
也可以用选项-disableassertions或-da禁用某个特定类和包的断言:
java -ea:... -da:MyClass MyApp
有些类不是由类加载器加载,而是直接由虚拟机加载。可以使用这些开关有选择地启用或禁用那些类中的断言。然而,启用和禁用所有断言的-ea和-da开关不能应用到那些没有类加载器的“系统类”上。对于这些系统类来说,需要使用-enablesystemassertions/-esa开关启用断言。
在程序中也可以控制类加载器的断言状态。有关这方面的内容请参看本节末尾的API注解。
11.4.2 使用断言的建议
在Java语言中,给出了三种处理系统错误的机制:
-
抛出一个异常
-
日志
-
使用断言
什么时候应该选择使用断言呢?请记住下面几点:
-
断言失败是致命的、不可恢复的错误。
-
断言检查只用于开发和测阶段(这种做法有时候被戏称为“在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛入水中吧”)。
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
下面看一个十分常见的例子:检査方法的参数。是否应该使用断言来检査非法的下标值或null引用呢?要想回答这个问题,首先阅读一下这个方法的文档。假设实现一个排序方法。
/**
* Sorts the specified range of the specified array into ascending numerical
* order. The range to be sorted extends from fromIndex, inclusive, to
* toIndex, exclusive.
*
* @param a
* the array to be sorted.
* @param fromIndex
* the index of the first element (inclusive) to be sorted.
* @param toIndex
* the index of the last element (exclusive) to be sorted.
* @Throws IllegalArgumentException if fromIndex > toIndex
* @Throws ArrayIndexOutOfBoundsException if fromIndex < 0 or tolndex>
* a.length
*/
static void sort(int[] a, int fromIndex, int toIndex)
文档说明,如果方法中使用了错误的下标值,那么就会抛出一个异常。这是方法与调用者之间约定的处理行为。如果实现这个方法,那就必须要遵守这个约定,并抛出表示下标值有误的异常,因此,这里使用断言不太适宜。
是否应该断言a不是null吗?这也不太适宜。当a是null时,这个方法的文档没有指出应该采取什么行动。在这种情况下,调用者可以认为这个方法将会成功地返回,而不会抛出一个断言错误。
然而,假设对这个方法的约定做一点微小的改动:
* @param a
* the array to be sorted. (Must not be null)
现在,这个方法的调用者就必须注意:不允许用null数组调用这个方法,并在这个方法的开头使用断言
assert a != null;
计算机科学家将这种约定称为前提条件。最初的方法对参数没有前提条件,即承诺在任何条件下都能够给予正确的执行。修订后的方法有一个前提条件,即a非空。如果调用者在调用这个方法时没有提供满足这个前提条件的参数,所有的断言都会失败,并且这个方法可以执行它想做的任何操作。事实上,由于可以使用断言,当方法被非法调用时,将会出现难以预料的结果。有时候会抛出一个断言错误,有时候会产生一个null指针异常,这完全取决于类加载器的配置。
11.4.3 为文档使用断言
很多程序员使用注释说明假设条件。看一下http://java.sun.com/javase/6/docs/technotes/ guides/language/assert.html上的一个示例:
if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else ... // (i % 3 == 2)
在这个示例中,使用断言会更好一些。
if (i % 3 == 0) ...
else if (i % 3 == 1) ...
else{
assert i % 3 == 2;
...
}
当然,如果再仔细地考虑一下这个问题会发现一个更有意思的内容。i%3会产生什么结果?如果i是正值,那余数肯定是0、1或2。如果i是负值,则余数可以是-1和-2。然而,实际上都认为i是非负值,因此,最好在if语句之前使用下列断言:
assert i >= 0;
无论如何,这个示例说明了程序员如何使用断言来进行自我检査。前面已经知道,断言是一种测试和调试阶段所使用的战术性工具,而日志记录是一种在程序的整个生命周期都可以使用的策略性工具。稍后将介绍日志的相关知识。
API:java.lang,ClassLoader 1.0
-
void setDefaultAssertionStatus(boolean b) 1.4:对于通过类加载器加载的所有类来说,如果没有显式地说明类或包的断言状态,就启用或禁用断言。
-
void setCIassAssertionStatus(String className, boolean b) 1.4:对于给定的类和它的内部类,启用或禁用断言。
-
void setPackageAssertionStatus(String packageName, boolean b) 1.4:对于给定包和其子包中的所有类,启用或禁用断言。
-
void clearAssertionStatus() 1.4:移去所有类和包的显式断言状态设置,并禁用所有通过这个类加载器加载的类的断言。