运行时和检查时异常
以下
检查时异常(Checked Exception)
简称为 CE
运行时异常
(Runtime Exception)` 简称为 RE
关系
CE 与 RE 并不是矛盾关系,而是交叉关系,因为他们有共同的父类 Exception
,所以按照矛盾关系去思考对比这两者间的关系是错误的想法。
语法
从语法上说,编译器会在编译时检查 CE
,如果没有 try ...catch
或者 throws
编译就不会通过。编译器不会检查 RE
,所以没有 try ...catch
或者 throws
也没问题。
语义
从语义上说,CE
是在本层无法处理,这时需要通知给上层方法或者客户端。比如 IOException
,在本层发现 IO 失败,是源错误,自己 try ...catch
并通知上层或者直接 throws
。
而 RE
,可能是在本层无法处理,比如传入参数为 Null 但不应该为 Null,此时应该通知上层。这是和 CE
相同的地方。
大部分情况是你的程序逻辑本身有问题,比如数组越界,此时做一下边界限制,在本层处理即可。
设计
从设计角度来讲,C++ 所有的异常都是 CE
,Java 如果也是那样就烦死了,几乎每个方法都要 throws。
最佳实践
系统提示友好
对于可能会给用户看的异常信息,封装一层,写明错误,自定义错误码。
异常分类
不建议的写法
try {
//xxx
}catch (Exception e){
log.error(e);
}
建议的写法
try {
//xxx
}catch (FileNotFoundException e){
log.error("file not found");
}catch (Exception e){
log.error(e);
}
这就比较直观,节省时间,不需要一层层看代码在分析了。
一次抛出多个异常
在注册页面,可能会出现一次抛出多个异常的情况,比如邮箱存在,密码少于 6 位等。
这种情况需要将可能出现的异常放到集合中,最后统一抛出。
public static void doStuff() throws MyException {
List list = new ArrayList();
// 第一个逻辑片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
// 第二个逻辑片段
try {
// Do Something
} catch (Exception e) {
list.add(e);
}
if (list.size() > 0) {
throw new MyException(list);
}
}
}
class MyException extends Exception {
// 容纳所有的异常
private List causes = new ArrayList();
// 构造函数,传递一个异常列表
public MyException(Listextends Throwable> _causes) {
causes.addAll(_causes);
}
// 读取所有的异常
public List getExceptions() {
return causes;
}
}
异常链传递
由 service 包装后上抛,最外层的 Contrller 使用 @ControllerAdvice
根据异常的类型来决定返回自定义错误信息还是 sever error
检查时异常尽量转换为运行时异常
我们实现接口方法,接口方法并没有用 throws 修饰,此时如果实现类抛出了 CE,此时要不修改接口,要不 try..catch 转换为 RE,修改接口肯定是最差的方案,因为所有继承的都需要修改,同时也破坏了迪米特法则,因为接口的其他实现并不认识这个异常类。如果我们每个 CE 都 try,catch,又得加好多代码,可读性变差。最好的方案是包装 CE 为 RE。
那什么情况下需要包装 CE 为 RE 呢?
受检异常转换为非受检异常是需要根据项目的场景来决定
的,例如同样是刷卡, 员工拿 着自己的工卡到考勤机上打考勤,此时如果附近有磁性物质干
扰 ,则考勤机可以把这种受 检异常转化为非受检异常,黄灯闪烁后不做任何记录登记,因为
考勤 失败 这种 情景 不是 “致命"的 业务逻辑,出错了,重新刷一下即可。但是到银行网点取
钱就不一样了,拿着银行卡到银行取钱,同样有磁性物质干扰,刷不出来,那这种异常就必
须登记处理,否则会成为威胁银行卡安全的事件。汇总成一句话 :当受检异常威胁到了系统
的安全性、稳定性、可靠性、正确性时,则必须处理,不能转化为非受检异常,其他情况则
可以转换为非受检异常。
不要在 finally 中处理返回值
public static void main(String[] args) {
try {
doStuff(-1);
doStuff(100);
} catch (Exception e) {
System.out.println("这里是永远都不会到达的");
}
}
// 该方法抛出受检异常
public static int doStuff(int _p) throws Exception {
try {
if (_p < 0) {
throw new DataFormatException(" 数提格式错误");
} else {
return _p;
}
} catch (Exception e) {
//异常处理
throw e;
} finally {
return -1;
}
}
doStuff(-l)的值是-1,doStuff(100)的值也是-1,调用 doStuff 方法永远都不
会抛出异常。
为什么明明把异常 throw 出去了,但 main 方法却捕捉不到呢?这是因为异常线程在监
视到有异常发生时,就会登记当前的异常类型为 DataFormatException,但是当执行器执行
finally 代码块时,则会重新为 doStuff 方法賦值,也就是吿诉调用者“该方法执行正确,没
有产生异常,返回值是 1”,于是乎,异常神奇的消失了。
会覆盖 try 代码块中的 return
public static int doStuff() {
int a = 1;
try {
return a;
} catch (Exception e) {
} finally {
//重新修改一下返回值
a = 1;
}
return 0;
}
我们知道方法是在栈内存中运行的,并且会按照“先进后出”的原则执行,main 方法调
用了 doStuff 方法,则 main 方法在下层,doStuff 在上层,当 doStuff 方法执行完“returna”
时,此方法的返回值已经确定是 in 类型 1(a 变量的值,注意基本类型都是值拷贝,而不是
引用),此后 finally 代码块再修改 a 的值已经与 doStuff 返回者没有任何关系了,因此该方法
永远都会返回 1。
public static Person doStuff() {
Person person = new Person();
person.setName("张 三 ");
try {
return person;
} catch (Exception e) {
} finally {
//重新修改一下返回值
person.setName("李 四 ");
}
person.setName(" 王 五 ");
return person;
}
@Getter
@Setter
static class Person {
private String name;
}
此方法的返回值永远都是 name 为李四的 Person 对象,原因是 Person 是一个引用对象,
在 try 代码块中的返回值是 Person 对象的地址
会屏蔽异常
与 return 语句相似,System.exit(O)或 Runtime.getRuntime().exit(O)出现在异常代码块中
也会产生非常多的错误假象,增加代码的复杂性
多使用异常,把性能问题放一边
Java 的异常处理机制确实比较慢,这个“比较慢”是相对于诸如 String、Integer 等对
象来说的,单单从对象的创建上来说,new—个 IOException 会比 String 慢 5 倍,这从异常
的处理机制上也可以解释:因为它要执行 filllnStackTrace 方法,要记录当前栈的快照,而
String 类则是直接申请一个内存创建对象,异常类慢一筹也就在所难免了。
但是异常也不会经常出现的,所以相比代码可读性而言,性能考虑可以微乎其微了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于