异常知识点总结

本贴最后更新于 2160 天前,其中的信息可能已经时过境迁

运行时和检查时异常

以下
检查时异常(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 类则是直接申请一个内存创建对象,异常类慢一筹也就在所难免了。

但是异常也不会经常出现的,所以相比代码可读性而言,性能考虑可以微乎其微了。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 203 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...