初识
代码的世界和现实世界一样,不会一帆风顺,起码不会像我们想象中的那么顺利。写代码的是人,人的思维存在局限性,这种局限性,体现在代码中,就是漏洞。倘若一段程序在生产环境中崩溃,势必让人十分恼火。这种让人头大的情况就是异常的一种。
Java 是这样定义异常的:
异常指的是,那些在程序运行过程中,中断正常的指令流程的事件。
实际上,异常还会在以下场景中发生:
- 用户输入错误,如:输入不符合程序预期
- 设备错误,如:网线断开连接
- 物理限制,如:内存不足
而我们并不希望程序因为上述情况挂掉,而是希望程序能够在遇到异常时,巧妙地处理之后,继续运行下去。
所幸,Java 提供了一套机制,用于在程序程序出错(出现异常)时,进行:
- 报告错误
- 保存工作结果
- 让用户以更完善的方式退出程序
亦即:异常处理机制。顾名思义,该机制用于对程序运行中不正常的情况进行处理。
简单来说,Java 中的异常处理是:当异常发生时,运行时系统逆着方法的调用栈(call stack)去寻找能够处理这种异常的东西。
以一个把字符串转换成整数的方法为例:
public Integer stringToNumber(String strToFormat){
Integer targetNum;
try{
targetNum = new Integer(strToFormat);
}catch(NumberFormatException e){
targetNum = DEFAULT_NUM;
}
return targetNum;
}
在上述代码中,当 strToFormat 不能转化成一个整形数时,就转化成一个默认的值。NumberFormatException
发生时,方法立即终止。控制权被交由 catch 代码块,也就是异常处理器(exception hanlder)。代码虽然简单,却包含了 Java 异常处理的一般步骤。
异常分类
在 Java 中,所有的异常派生于 Throwable 接口。这些异常,又被分为 Error 和 Exception。这两类异常,又衍生了一些子类。下面的图片,描述了 Java 的异常体系图:
不过,我们应当把精力放在 RuntimeException 上,因为“所有的运行时异常,都是程序员的问题”。这类异常,又被叫做未检查异常,通常交由 JVM 处理。相对应的,那些我们知道可能会出现问题、,被叫做已检查的异常。这类异常需要开发者手动向上级抛出,直到 JVM 级别,或者在调用方法时使用 try-catch
处理。
一般来说,在写一个方法时,要尽可能声明所有已检查异常,通过内部逻辑控制,避免未检查(运行时)异常。
异常捕获
在编码过程中,如果方法的检查异常被精心设计,那么我们只管抛出异常即可。但是,一旦调用方法的地方没有对这种可能出现的异常进行处理,那么程序就会终止。所以,要对异常进行捕获操作,正如之前的字符串转数字的方法一样。
根据经验,程序中应该捕获那些知道如何处理的异常,把“职责”外的异常,交给方法的调用者去处理。下面是一个异常捕获流程的示例代码:
modifier returnType someMethod(type1 param1, type2 param2) throws Exception1, Exceptio2{
try{
① // 某些操作
return returnType value;
}catch(Exceptionx | Exceptiony e){
② // 处理异常
}catch(Excpetion1 e1){
③ throw new Excption1(e1);
}catch(Exception e2){
④ throw e2;
}finally{
⑤ // 一些操作
return returnType mockValue;
}
}
在上述代码中,① 中进行操作,捕获了 Exeptionx 或者 Exceptiony 时,② 进行异常处理;捕获了 Exception1 时, ③ 把该异常包装;捕获了 Exception 2 时,④ 直接抛出异常;无论有无异常发生,⑤ 都会执行。
上面的代码还有一个问题,就是 finally 代码中包含返回值。有以下规则:
- try 代码块中包含 return 语句,finally 语句中的代码也会被执行;若 finally 中也包含 return 语句,则会替代 try 中的语句
使用异常的技巧
- 不要过分细分异常,能用一个
try
块处理的尽量用一个,然后按层次捕获异常,下面的代码是被建议的:
try{
// 某些操作
}catch(SomeException1 e1){
// 异常 1 的处理
}catch(SomeException2 e2){
// 异常 2 的处理
}
- 适当地转化异常,如:可以把一个
NumberFormatException
转化成自定义的异常 - 遇到异常情况,可以更加严格一点。如:抛出 EmptyStackException 比 NullPointerException 更加语义化
- 学会把异常传递,即:「早抛出,晚捕获」