运行时和检查时异常
以下
检查时异常(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 类则是直接申请一个内存创建对象,异常类慢一筹也就在所难免了。
但是异常也不会经常出现的,所以相比代码可读性而言,性能考虑可以微乎其微了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于