程序为什么需要异常
‘异常’ 听起来就像是 broken,平时工作时发现代码报异常了也是很沮丧,我们会想该怎么和异常做斗争呢?我们先需要思考下,为啥语言设计者会设计异常这个机制存在呢?我们在了解了异常为啥存在,解决了什么问题后,在工作中才能不惧怕异常,并且合理高效的利用异常机制为我们打造鲁棒性很强的系统。
有同学会想到,计算机系统本就是由中断、异常机制控制流程的,语言设计时沿袭这种思路自然而然。这也是有道理的,我们今天从代码语义的角度理解下为啥代码里需要异常。比如我这是一个账户钱包系统,你付款时需要扣款(返回你的余额)方法的伪代码:
public double pay(double amount, User user) {
double balance = queryBalance(user.getUserId());
balance = balance - amount;
updateBalance(balance);
return balance;
}
这样的定义,我们的方法和 client 的交流只能通过返回值(double类型),那么这个时候余额不够扣款怎么办呢,给 client 返回什么呢,约定 -1.0 吗,显然不可取。还有诸如其他的场景,比如数据库异常,查询余额失败时,该怎么和 client 的沟通呢,怎么通过 double类型告诉 client 发生了啥。另外,updateBalance 方法没有返回值,调用方都默认一直更新成功吗。当程序发生非正常流程时,亦或是发生错误时,我们需要返回给 client 一个安全的状态,并告诉它 cause,允许 client 保存自己的工作,以友好的方式结束程序。所以编程语言里的异常机制是帮助你更健壮构建程序,更清晰透明的传递信息,不需要惧怕他,好好利用他。
Java语言中异常的分类
这个比较基础了,Java中所有异常的父类是 Throwable,这里两个关键的属性就是 message 和 cause 了,这不管是我们自定义异常还是处理异常,都经常需要用到。Throwable 的两个子类就是 Error 和 Exception 了,Error 是不应该被程序员做 catch 处理的,如果程序发生了 Error,你基本是做不了啥;重点是 Exception,Exception 的子类分 RuntimeException 和 checked Exception,checked Exception 是需要在方法上声明的,调用方要么catch 处理掉,要么 throw 出去,RuntimeException 通常是 Java 虚拟机处理的。
Java语言中异常的使用
基本使用
我们该什么时候抛出异常,什么时候catch掉异常呢?简单来说就是:
- catch 那些我们知道怎么处理或者是就需要这层方法处理的异常;
- throws 那些我们不知道怎么处理或是应该通知调用方发生错误信息的异常。
什么时候抛出 checked exception
道理很简单,一个方法除了告诉编译器它要返回什么值以外,还要告诉编译器会发生什么错误(处理思想)。当你不清楚是否要抛出异常时,可以参考下面几点:
- 你的方法内部调用了一个会抛出checked exception 的方法,比如 - InputFileStream 的构造方法;
- 你检测出一个 error,并且用 throw 语句抛出了一个 checked exception;
RuntimeException应该在它可能发生的地方处理掉,不允许发生-比如数组下标越界,当然你自定义的逻辑异常除外。
Rethrowing and Chainning Exceptions
你可以在catch中重新throw出异常,这种情况一般在要改变异常类型时,通常是处理 checked exception,转成自定义的 RuntimeException。
try {
access database
} catch (SQLException e) {
throw new ServletException("database error: " + e.getMessage());
}
我们重新 throws 的异常展示了 error message,但是我们有更好的方式:
try {
access database
} catch (SQLException e) {
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
这种包装技术是很好的,可以抛出高质量的异常,不会丢失原始的异常信息。调用方可以用 se.getCause() 来获取原始异常信息。
final 语句的使用
final代码块保证无论是否发生异常中断,块中的代码一定会执行,确保我们代码能安全的退出;另外,finally从句中的return语句会覆盖所有的return结果。因为无论哪个地方return之前都会去执行finally从句,finally从句中有return的话就会return finally从句中的返回值提前结束方法。但是 final 会使代码很难看。
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
有没有办法解决?Java提供了 try-with-resource 语句,自动 close,可以使上面的代码变美:
static void copy(String src, String dst) throws IOException {
try(InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
对比下两个代码块,谁说的Java代码不可以写的很优美简洁了。需要注意的是,继承了AutoCloseable接口,close()方法才能自动调用。
Tips
下面这6点tips 是我工作学习中做的一下笔记,出处我也忘记了,没很好的记录下来,供大家批判讨论吧:
- Exception handling is not supposed to replace a simple test.(简单测试的效率要高于异常处理) The moral is :Use exceptions for exceptional circumstances only.
- Do not micromanage Exceptions。用尽可能少的try块,保持代码的简洁性。但是遵循:to separate normal processing from error handling.
- Marke good use of the exceptions hierarchy:
- 不要仅仅抛出一个RuntimeException。Find an appropriate subclass or create your own.
- 不要仅仅抛出一个Throwable。It makes your code hard to read and maintain.
- Respect the difference between checked and unchecked exceptions。Check Exceptions 非常繁重,不要利用Check Exception处理业务逻辑
- Do not squelch Exception. 你应该把异常处理掉,而不是仅仅catch放那。
- When you detect an error, “tough love” works better than indulgence.
- Propagating exceptions is not a sign of shame
NOTE: Rules 5 and 6 can be summarized as “throw early, catch late.”
项目中怎么优雅的处理异常
关于项目中怎么优雅的处理异常,大家可以参考这篇文章:一个成熟的Java项目如何优雅地处理异常 - 掘金 (juejin.cn),基本和我平时处理的方式一样,这里就不重复写了。不过有点要重点点出的是:项目里不同的模块或是不同的错误类型,最好是定义不同的自定义异常类型,可以统一继承自一个顶层的自定义异常,这样便于区分异常的逻辑处理,统一继承也能更好地做到异常内部处理逻辑复用。
最后
写的不全,但是是一些平时总结的笔记,以前没有分享的习惯,现在慢慢养成吧。《Effective Java》里关于异常的建议挺好的,我抽空再总结下发下一期,嘿嘿。最近找工作太难了,加油吧,通过写技术分享,保持自己的积极性!