36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
对于运行时异常,我们在编写代码的时候,可以不用主动去 try-catch,编译器在编译代码的时候,并不会检查代码是否有对运行时异常做了处理。
相反,对于编译时异常,我们在编写代码的时候,需要主动去 try-catch 或者在函数定义中声明,否则编译就会报错。
所以,运行时异常也叫作非受检异常(Unchecked Exception),编译时异常也叫作受检异常(Checked Exception)。
如何选择
如果你熟悉的编程语言中(比如 Java),定义了两种异常类型,那在异常出现的时候,我们应该选择抛出哪种异常类型呢?是受检异常还是非受检异常?
对于代码 bug(比如数组越界)以及不可恢复异常(比如数据库连接失败),即便我们捕获了,也做不了太多事情,所以,我们倾向于使用非受检异常。
对于可恢复异常、业务异常,比如提现金额大于余额的异常,我们更倾向于使用受检异常,明确告知调用者需要捕获处理。
实际上,Java 支持的受检异常一直被人诟病,很多人主张所有的异常情况都应该使用非受检异常。
支持这种观点的理由主要有以下三个。
- 受检异常需要显式地在函数定义中声明。如果函数会抛出很多受检异常,那函数的定义就会非常冗长,这就会影响代码的可读性,使用起来也不方便。
- 编译器强制我们必须显示地捕获所有的受检异常,代码实现会比较繁琐。而非受检异常正好相反,我们不需要在定义中显示声明,并且是否需要捕获处理,也可以自由决定。
- 受检异常的使用违反开闭原则。如果我们给某个函数新增一个受检异常,这个函数所在的函数调用链上的所有位于其之上的函数都需要做相应的代码修改,直到调用链中的某个函数将这个新增的异常 try-catch 处理掉为止。而新增非受检异常可以不改动调用链上的代码。我们可以灵活地选择在某个函数中集中处理,比如在 Spring 中的 AOP 切面中集中处理异常。
不过,非受检异常也有弊端,它的优点其实也正是它的缺点。
从刚刚的表述中,我们可以看出,非受检异常使用起来更加灵活,怎么处理的主动权这里就交给了程序员。我们前面也讲到,过于灵活会带来不可控,非受检异常不需要显式地在函数定义中声明,那我们在使用函数的时候,就需要查看代码才能知道具体会抛出哪些异常。非受检异常不需要强制捕获处理,那程序员就有可能漏掉一些本应该捕获处理的异常。
对于应该用受检异常还是非受检异常,网上的争论有很多,但并没有一个非常强有力的理由能够说明一个就一定比另一个更好。所以,我们只需要根据团队的开发习惯,在同一个项目中,制定统一的异常处理规范即可。
如何处理函数抛出的异常
刚刚我们讲了两种异常类型,现在我们再来讲下,如何处理函数抛出的异常?
总结一下,一般有下面三种处理方法。
- 直接吞掉
- 原封不动地 re-throw
- 包装成新的异常 re-throw
直接吞掉。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
public void func2() {
//...
try {
func1();
} catch (Exception1 e) {
//吐掉:try-catch打印日志
log.warn("...", e);
}
//...
}
原封不动地 re-throw。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
//原封不动的re-throw Exception1
public void func2() throws Exception1 {
//...
func1();
//...
}
包装成新的异常 re-throw。具体的代码示例如下所示:
public void func1() throws Exception1 {
// ...
}
public void func2() throws Exception2 {
//...
try {
func1();
} catch (Exception1 e) {
// wrap成新的Exception2然后re-throw
throw new Exception2("...", e);
}
//...
}
当我们面对函数抛出异常的时候,应该选择上面的哪种处理方式呢?我总结了下面三个参考原则:
- 如果 func1() 抛出的异常是可以恢复,且 func2() 的调用方并不关心此异常,我们完全可以在 func2() 内将 func1() 抛出的异常吞掉;
- 如果 func1() 抛出的异常对 func2() 的调用方来说,也是可以理解的、关心的 ,并且在业务概念上有一定的相关性,我们可以选择直接将 func1 抛出的异常 re-throw;
- 如果 func1() 抛出的异常太底层,对 func2() 的调用方来说,缺乏背景去理解、且业务概念上无关,我们可以将它重新包装成调用方可以理解的新异常,然后 re-throw。
总之,是否往上继续抛出,要看上层代码是否关心这个异常。关心就将它抛出,否则就直接吞掉。
是否需要包装成新的异常抛出,看上层代码是否能理解这个异常、是否业务相关。如果能理解、业务相关就可以直接抛出,否则就封装成新的异常抛出。