知识导航
异常会伴随着我们开发程序的整个周期,也只有正确处理好这些异常,才能保证程序的可靠性和稳定性。
各个主流编程语言都提供了很完善的异常处理机制,Java 也不例外,今天就梳理一下 Java 的异常
Java 类库的异常设计
Exception 和 Error 都是继承了 Throwable 类。
throw 和 catch 只能作用在 Throwable 类的实例上。比如:
throw new RuntimeException("Error !");
Exception 和 Error 提现了 Java 设计者对于异常的不同分类。
Exception 是在程序运行过程中,可以预料到的正常情况,应该捕获这些异常,并作出相应的处理。
Exception 又可以分为可检查(Checked)和不可检查(Unchecked)异常。可检查异常是在源代码里必须进行显示的处理,这是编译器检查的一部分;不可检查异常就是运行时异常,如 NullPointerException,ArrayIndexOutOfBoundsException,通常是正确编码就可以解决的异常,可以按需求进行补货。
Error 表示在正常情况下,不大可能出现的情况,会导致程序处于非正常、不可恢复的状态。不便于也不需要捕获,如 OutOfMemoryError,StackOverflowError
尽量不要捕获 Exception 异常
不要去捕获 Exception 异常,应该去捕获特定异常。应为在日常的开发和团队合作中,读代码的机会会很多,软件工程是门代码合作的艺术,我们有义务让自己的代码能体现出更多的信息,而捕获 Exception 异常恰恰隐藏了我们的意图。
另外也要保证不要捕获到不应该捕获的异常,比如 RuntimeException,你可能更希望这个异常能抛出去
不要生吞异常
这是非常需要注意的事,比如下面的代码:
try {
// 业务代码
// …
} catch (IOException e) {
e.printStackTrace();
}
printStackTrace 在 JDK 文档中第一句话就是:
Prints this throwable and its backtrace to the standard error stream。
在稍微复杂一点的生产系统中,标准输出不是个合适的选项,一般我们自己也不知道到底输出到哪去了,导致无法找到堆栈轨迹。没有人能够轻易判断到底是哪里出现了异常,以及是什么原因出现了异常。
生吞异常,本质上是不知道该如何处理,掩盖了问题。
最好是使用 第三方日志系统,输出到日志文件中
Throw early, catch late
来体会下面的一段代码:
public void readFile(String filename) {
Objects. requireNonNull(filename);
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}
如果文件名是 null 的话,会抛出空指针异常。所以这里尽早的发现了这个问题,并且抛出了合理的异常出去。实际情况中,可能是配置文件没找到,配置文件格式不对,需要尽早的发现此类问题,尽早的抛出去。
抛出去之后,到底应该怎么捕获呢?可以直接打印日志出去,或者保留原有的 cause,构造新的异常抛出去,在上层逻辑(业务逻辑)中就知道应该做何种处理了
合理的捕获和抛出异常,也是一门很大的学问。
当我们的服务性能下降、吞吐量下降的时候,去日志找发生最频繁的异常也是很好的思路。