异常,天使还是魔鬼?
我们写代码分两种:
- 自信,愉快地写的,业务逻辑正常
- 忐忑,苦恼地写的,小心处理各种错误和意外逻辑
为了缓解负面的情绪,OOP的异常机制横空出世!
try {
//主体逻辑
}
catch(Exception e) {
//异常逻辑
}
finally {
//公用的清理工作,不要在这return
}
这个机制就像是伏妖袋一样的,捕到了,贼开心;没捕到呢???
三个江湖派别
将对异常的态度,对所有编程语言分一下类:
-
嗤之以鼻派:C【用个毛】
-
拥趸派:Java、C#【用的一层层地嵌套,不用不爽】
-
吃瓜派:C++、Objective-C、Swift【支持,但很少用】
所以我们能看到,C一般不出错,要不就是重大错误;而Java虽然经常报错,至少能磕磕碰碰往前跑(但有些异常提醒画面,还不如崩溃了呢…)
异常的种类
大类上分:
- 业务异常
- 系统异常
业务异常
-
定义:根据业务需要而定义的异常(比如你想请客,但钱不够)
-
特点:
- 业务特殊性
- 一定有地方抛出,否则也没有存在的必要
- 一定会在系统抛出,但不一定在本系统捕获
系统异常
-
定义:程序运行中,依赖的底层模块或更底层的runtime(通用模块)抛出的异常
-
特点:
- 抽象程度高
- 意料之外(靠经验发现)
我们千万别把此类系统异常,当作一个正常业务的逻辑分支!比如:
void OperationInQueue {
while(status == Status.NORMAL) {
try {
String data = mg.pop(); // 从中间队列获取数据
processData(data);
}
catch(NoDataException e) {
DoSomething();
}
catch(Exception e) {
status = Status.ERROR;
}
}
}
我们千万别写成:
void OperationInQueue {
while(status == Status.NORMAL) {
try {
if(mg.isEmpty()){
DoSomething();
} else {
String data = mg.pop(); // 从中间队列获取数据
processData(data);
}
}
catch(Exception e) {
status = Status.ERROR;
}
}
}
从某种角度来说,这段代码没有错,但是它的逻辑像是我们在“期待这个异常”,它不是业务逻辑啊!一定要分开处理!而且耦合度高的代码,不是我们要的吧…
此外,系统异常有个重要的分支——runtime异常,我们大部分人也是和这个东西打交道。(比如,数组越界,空指针等等常见异常)
throw——手榴弹什么时候扔
throw
就像手榴弹,一旦被抛出,(往上抛)在它没被catch之前,整个程序进入紧张状态,直到捕获了才松懈
- 本质:向所有模块宣布:“各单位注意!!我抛出了一个异常手榴弹!!!”
- 目的:在开发和测试阶段确保它零概率被抛到最上层,也就是在被最终用户感知前,被中间层的代码处理掉
其实就是给你一个“亡羊补牢”的机会啊!我们来看一个例子:
public class TestException {
public static void main(String[] args) {
method1();
}
private static void method1() {
try {
method2();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static void method2() throws FileNotFoundException {
File f = new File("D:/LOL.exe");
System.out.println("试图打开 D:/LOL.exe");
new FileInputStream(f);
System.out.println("成功打开");
}
}
method2中需要进行异常处理,但是method2不打算处理,而是把这个异常通过throws抛出去,那么method1就会接到该异常。 处理办法也是两种,要么是try-catch处理掉,要么也是抛出去,这里的选择是处理掉
问题来了:我们什么时候该把自定义异常往外抛呢?
记住,不要畏手畏脚,怕别人淦你,根据业务需求,该抛抛(比如你是负责饭馆的模块,只知道怎么收钱,但你不负责处理别人钱不够的情况,这就需要别人来处理顾客没钱的事实),做好沟通就行了(特别你是做底层的话)
catch——收炸弹的垃圾筐
catch
的本质,其实就是带导航的goto
,自动定位异常,而不是发散的
优势:
-
try-catch的机制使得主体逻辑和错误逻辑互相隔离,可读性强,且易于维护(这就是C没有的)
-
借于OOP的体系,catch可以有大大小小不同的垃圾筐叠成,每个层级过滤不同种的垃圾,throw不差别抛出,但我catch有差别的接,从而让不同异常体现出不同的等级
-
Exception可以捕到所有它的子类异常,使得开发者可以提高编程效率
void personEat(Person p,Food f) {
try {
person.eat(f);
}
catch(NotEnoughMoneyException e){
// 处理钱不够的异常
}
catch(Exception e){
Log(e.Message); // 边边角角先放一边
}
}
这个特性极大简化了逻辑,提高编程效率,避免程序员开发时因为边角分心
那我们catch到的异常,接着抛还是捕获了?——还是沟通,还是看业务
千万不要好心一人全揽了,导致异常消息队列数据大量流失嗷!
异常使用技巧
看病要趁早
关系到后面逻辑的要早抛,别等到方法深处才抛,看个付钱的例子:
public void pay(BigDecimal money) {
if(money < needToPay){
throw new NotEnoughMoney("Money is not enough!");
}
// 之后的逻辑是在钱够的情况下进行的,要提前检查钱够不够
}
不要加大catch负担
能在主体逻辑里避坑的,先处理掉,别太依赖异常啊!
try {
// 并非多此一举,这是减少依赖,方便移植
if(a != null)
a.call(b);
// 其他逻辑
}
catch (Exception e){
Log(e.Message);
}
避免try花了眼
尽量不要在方法中写try-catch,不然到时候各种嵌套,眼花缭乱,不方便维护,实在要用,写个static工具类封装起来吧,求你了!
克制!不滥用!
给我记住几条铁则就好:
- 最底下的model层不用考虑异常处理,抛就完了
- 业务层尽量精确地按异常种类处理异常
- 最上面的展示层捕获最终的所有漏网之鱼(比如用Exception爸爸)
总结
-
异常不是给用户看的,是给我们看到的!
-
异常机制的最大作用就是空间的隔离:主题体逻辑与错误逻辑分开
-
啥时候抛,啥时候捕,全靠沟通(如果是自己个人的话,看上面的铁则做就行了)