Java 异常使用小结

1,307 阅读6分钟

程序为什么需要异常

‘异常’ 听起来就像是 broken,平时工作时发现代码报异常了也是很沮丧,我们会想该怎么和异常做斗争呢?我们先需要思考下,为啥语言设计者会设计异常这个机制存在呢?我们在了解了异常为啥存在,解决了什么问题后,在工作中才能不惧怕异常,并且合理高效的利用异常机制为我们打造鲁棒性很强的系统。

有同学会想到,计算机系统本就是由中断、异常机制控制流程的,语言设计时沿袭这种思路自然而然。这也是有道理的,我们今天从代码语义的角度理解下为啥代码里需要异常。比如我这是一个账户钱包系统,你付款时需要扣款(返回你的余额)方法的伪代码:

public double pay(double amount, User user) {
    double balance = queryBalance(user.getUserId());
    balance = balance - amount;
    updateBalance(balance);
    return balance;
}

这样的定义,我们的方法和 client 的交流只能通过返回值(double类型),那么这个时候余额不够扣款怎么办呢,给 client 返回什么呢,约定 -1.0 吗,显然不可取。还有诸如其他的场景,比如数据库异常,查询余额失败时,该怎么和 client 的沟通呢,怎么通过 double类型告诉 client 发生了啥。另外,updateBalance 方法没有返回值,调用方都默认一直更新成功吗。当程序发生非正常流程时,亦或是发生错误时,我们需要返回给 client 一个安全的状态,并告诉它 cause,允许 client 保存自己的工作,以友好的方式结束程序。所以编程语言里的异常机制是帮助你更健壮构建程序,更清晰透明的传递信息,不需要惧怕他,好好利用他。

Java语言中异常的分类

这个比较基础了,Java中所有异常的父类是 Throwable,这里两个关键的属性就是 message 和 cause 了,这不管是我们自定义异常还是处理异常,都经常需要用到。Throwable 的两个子类就是 Error 和 Exception 了,Error 是不应该被程序员做 catch 处理的,如果程序发生了 Error,你基本是做不了啥;重点是 Exception,Exception 的子类分 RuntimeException 和 checked Exception,checked Exception 是需要在方法上声明的,调用方要么catch 处理掉,要么 throw 出去,RuntimeException 通常是 Java 虚拟机处理的。

Java语言中异常的使用

基本使用

我们该什么时候抛出异常,什么时候catch掉异常呢?简单来说就是:

  • catch 那些我们知道怎么处理或者是就需要这层方法处理的异常;
  • throws 那些我们不知道怎么处理或是应该通知调用方发生错误信息的异常。

什么时候抛出 checked exception

道理很简单,一个方法除了告诉编译器它要返回什么值以外,还要告诉编译器会发生什么错误(处理思想)。当你不清楚是否要抛出异常时,可以参考下面几点:

  1. 你的方法内部调用了一个会抛出checked exception 的方法,比如 - InputFileStream 的构造方法;
  2. 你检测出一个 error,并且用 throw 语句抛出了一个 checked exception;

RuntimeException应该在它可能发生的地方处理掉,不允许发生-比如数组下标越界,当然你自定义的逻辑异常除外。

Rethrowing and Chainning Exceptions

你可以在catch中重新throw出异常,这种情况一般在要改变异常类型时,通常是处理 checked exception,转成自定义的 RuntimeException。

try {
    access database
} catch (SQLException e) {
    throw new ServletException("database error: " + e.getMessage());
}

我们重新 throws 的异常展示了 error message,但是我们有更好的方式:

try {
    access database
} catch (SQLException e) {
    Throwable se = new ServletException("database error");
    se.initCause(e);
    throw se;
}

这种包装技术是很好的,可以抛出高质量的异常,不会丢失原始的异常信息。调用方可以用 se.getCause() 来获取原始异常信息。

final 语句的使用

final代码块保证无论是否发生异常中断,块中的代码一定会执行,确保我们代码能安全的退出;另外,finally从句中的return语句会覆盖所有的return结果。因为无论哪个地方return之前都会去执行finally从句,finally从句中有return的话就会return finally从句中的返回值提前结束方法。但是 final 会使代码很难看。

static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
        } finally {
        out.close();
        }
    } finally {
        in.close();
    }
}

有没有办法解决?Java提供了 try-with-resource 语句,自动 close,可以使上面的代码变美:

static void copy(String src, String dst) throws IOException {
    try(InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst)) {
        
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
        out.write(buf, 0, n);
    }
}

对比下两个代码块,谁说的Java代码不可以写的很优美简洁了。需要注意的是,继承了AutoCloseable接口,close()方法才能自动调用。

Tips

下面这6点tips 是我工作学习中做的一下笔记,出处我也忘记了,没很好的记录下来,供大家批判讨论吧:

  1. Exception handling is not supposed to replace a simple test.(简单测试的效率要高于异常处理) The moral is :Use exceptions for exceptional circumstances only.
  2. Do not micromanage Exceptions。用尽可能少的try块,保持代码的简洁性。但是遵循:to separate normal processing from error handling.
  3. Marke good use of the exceptions hierarchy
    • 不要仅仅抛出一个RuntimeException。Find an appropriate subclass or create your own.
    • 不要仅仅抛出一个Throwable。It makes your code hard to read and maintain.
    • Respect the difference between checked and unchecked exceptions。Check Exceptions 非常繁重,不要利用Check Exception处理业务逻辑
  4. Do not squelch Exception. 你应该把异常处理掉,而不是仅仅catch放那。
  5. When you detect an error, “tough love” works better than indulgence.
  6. Propagating exceptions is not a sign of shame

NOTE: Rules 5 and 6 can be summarized as “throw early, catch late.”

项目中怎么优雅的处理异常

关于项目中怎么优雅的处理异常,大家可以参考这篇文章:一个成熟的Java项目如何优雅地处理异常 - 掘金 (juejin.cn),基本和我平时处理的方式一样,这里就不重复写了。不过有点要重点点出的是:项目里不同的模块或是不同的错误类型,最好是定义不同的自定义异常类型,可以统一继承自一个顶层的自定义异常,这样便于区分异常的逻辑处理,统一继承也能更好地做到异常内部处理逻辑复用。

最后

写的不全,但是是一些平时总结的笔记,以前没有分享的习惯,现在慢慢养成吧。《Effective Java》里关于异常的建议挺好的,我抽空再总结下发下一期,嘿嘿。最近找工作太难了,加油吧,通过写技术分享,保持自己的积极性!