某个函数返回一个特定的代码(special code),用以表示某种错误情况。
改用异常(exception)。
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
动机(Motivation)
和生活一样,计算器偶尔也会出错。一旦事情出错,你就需要有些对策。最简单的情况下,你可以停止程序运行,返回一个错误码。这就好像因为错过一班飞机而自杀一样(如果真那么做,哪怕我是只猫,我的九条命也早赔光了)。尽管我的油腔滑调企图带来一点幽默,但这种「软件自杀」选择的确是有好处的。如果程序崩溃代价很小,用户又足够宽容,那么就放心终止程序的运行好了。但如果你的程序比较重要,就需要以比较认真的方式来处理。
问题在于:程序中发现错误的地方,并不一定知道如何处理错误。当一段副程序 (routine)发现错误时,它需要让它的调用者知道这个错误,而调用者也可能将这 个错误继续沿着调用链(call chain)传递上去。许多程序都使用特殊输出来表示错误,Unix 系统和C-based 系统的传统方式就是「以返回值表示副程序的成功或失败」。
Java 有一种更好的错误处理方式:异常(exceptions)。这种方式之所以更好,因 为它清楚地将「普通程序」和「错误处理」分开了,这使得程序更容易理解——我希望你如今已经坚信:代码的可理解性应该是我们虔诚追求的目标。
作法(Mechanics)
· | 决定待抛异常应该是checked 还是unchecked。 |
Ø | 如果调用者有责任在调用前检查必要状态,就抛出unchecked异常。 |
Ø | 如果想抛出checked 异常,你可以新建一个exception class,也可以使用现有的exception classes。 |
· | 找到该函数的所有调用者,对它们进行相应调整,让它们使用异常。 |
Ø | 如果函数抛出unchecked 异常,那么就调整调用者,使其在调用函数 前做适当检查。每次修改后,编译并测试。 |
Ø | 如果函数抛出checked 异常,那么就调整调用者,使其在try 区段中调用该函数。 |
· | 修改该函数的签名式(sigature),令它反映出新用法。 |
如果函数有许多调用者,上述修改过程可能跨度太大。你可以将它分成下列数个步骤:
· | 决定待抛异常应该是checked 还是unchecked 。 |
· | 新建一个函数,使用异常来表示错误状况,将旧函数的代码拷贝到新函数中,并做适当调整。 |
· | 修改旧函数的函数本体,让它调用上述新建函数。 |
· | 编译,测试。 |
· | 逐一修改旧函数的调用者,令其调用新函数。每次修改后,编译并测试。 |
· | 移除旧函数。 |
范例:(Example)
现实生活中你可以透支你的账户余额,计算器教科书却总是假设你不能这样做,这不是报奇怪吗?不过下而的例子仍然假设你不能这样做:
class Account...
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
private int _balance;
为了让这段代码使用异常,我首先需要决定使用checked 异常还是unchecked 异常。决策关键在于:调用者是否有责任在取款之前检查存款余额,或者是否应该由 withdraw() 函数负责检查。如果「检查余额」是调用者的责任,那么「取款金额大于存款余额」就是一个编程错误。由于这是一个编程错误(也就是一只「臭虫」〕, 所以我应该使用unchecked 异常。另一方面,如果「检查余额」是withdraw() 函数的责任,我就必须在函数接口中声明它可能抛出这个异常(译注:这是一个checked 异常),那么也就提醒了调用者注意这个异常,并采取相应措施。
范例:unchecked 异常
首先考虑unchecked 异常。使用这个东西就表示应该由调用者负责检查。首先我需要检查调用端的代码,它不应该使用withdraw() 函数的返回值,因为该返回值只用来指出程序员的错误。如果我看到下面这样的代码:
if (account.withdraw(amount) == -1)
handleOverdrawn();
else doTheUsualThing();
我应该将它替换为这样的代码:
if (!account.canWithdraw(amount))
handleOverdrawn();
else {
account.withdraw(amount);
doTheUsualThing();
}
每次修改后,编译并测试。
现在,我需要移除错误码,并在程序出错时抛出异常。由于行为(根据其文本定义 得知)是异常的、罕见的,所以我应该用一个卫语句(guard clause)检查这种情况:
void withdraw(int amount) {
if (amount > _balance)
throw new IllegalArgumentException ("Amount too large");
_balance -= amount;
}
由于这是程序员所犯的错误,所以我应该使用assertion 更清楚地指出这一点:
class Account...
void withdraw(int amount) {
Assert.isTrue ("amount too large", amount > _balance);
_balance -= amount;
}
class Assert...
static void isTrue (String comment, boolean test) {
if (! test) {
throw new RuntimeException ("Assertion failed: " + comment);
}
}
范例:checked 异常
checked 异常的处理方式略有不同。首先我要建立(或使用)一个合适的异常:
class BalanceException extends Exception {}
然后,调整调用端如下:
try {
account.withdraw(amount);
doTheUsualThing();
} catch (BalanceException e) {
handleOverdrawn();
}
接下来我要修改withdraw() 函数,让它以异常表示错误状况:
void withdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
这个过程的麻烦在于:我必须一次性修改所有调用者和被它们调用的函数,否则编译器会报错。如果调用者很多,这个步骤就实在太大了,其中没有编译和测试的保障。
这种情况下,我可以借助一个临时中间函数。我仍然从先前相同的情况出发:
if (account.withdraw(amount) == -1)
handleOverdrawn();
else doTheUsualThing();
class Account ...
int withdraw(int amount) {
if (amount > _balance)
return -1;
else {
_balance -= amount;
return 0;
}
}
首先,产生一个newWithdraw() 函数,让它抛出异常:
void newWithdraw(int amount) throws BalanceException {
if (amount > _balance) throw new BalanceException();
_balance -= amount;
}
然后,调整现有的withdraw() 函数,让它调用newWithdraw() :
int withdraw(int amount) {
try {
newWithdraw(amount);
return 0;
} catch (BalanceException e) {
return -1;
}
}
完成以后,编译并测试。现在我可以逐一将「对旧函数的调用」替换为「对新函数 的调用」:
try {
account.newWithdraw(amount);
doTheUsualThing();
} catch (BalanceException e) {
handleOverdrawn();
}
由于新旧两函数都存在,所以每次修改后我都可以编译、测试。所有调用者都被我修改完毕后,旧函数便可移除,并使用Rename Method 修改新函数名称,使它与旧函数相同。