Java异常

异常概述

在Java中,利用面向对象的思想,将程序中的不正常情况封装成对象,以异常类的形式对问题进行描述。

将流程代码和异常代码进行分离,且不同的问题用不同的异常类来进行描述。

异常体系

Java程序中会出现的异常情况有很多,意味着描述这些问题的异常类也很多,将它们的共性进行向上抽取,就形成了Java异常体系。

Java异常体系的基本组成类型为Throwable,只有Throwable类型的实例才可以被抛出(throw)或捕获(catch)。

Java异常体系分为ExceptionError两大类,都继承了Throwable类。

这种分类体现了Java平台设计者对不同异常情况的分类:

  1. Exception是程序正常运行中可以预料的意外情况,可能并且应该被捕获,进行相应处理。

  2. Error是指在正常情况下不大可能出现的情况。绝大部分的Error都会导致程序处于非正常的、不可恢复状态。这类问题发生,一般不便于也不需要捕获。

Exception的分类

Exception又分为检查型异常(checked exception)非检查型异常(unchecked exception)

检查型异常在源代码里必须显式地进行捕获处理。它是编译器检查的一部分,因此也常被称为编译时异常

非检查型异常就是所谓的运行时异常,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译器强制要求。

Exception常见子类

RuntimeException

RuntimeException:运行时异常。

它和它的子类都是unchecked exception,如NullPointerExceptionClassCastExceptionIndexOutOfBoundsException等。

IOException

I/O异常,是由失败或已中断的I/O操作引起的。

它和它的子类都是checked exception

ClassNotFoundException

当程序在运行期间尝试通过类名去加载某个类时,若找到该类的定义就会发生这个异常。

此异常是由以下几个方法引起:

  1. java.lang.Class#forName(java.lang.String)

  2. java.lang.ClassLoader#findSystemClass(java.lang.String)

  3. java.lang.ClassLoader#loadClass(java.lang.String, boolean)

它是checked exception

Error常见子类

NoClassDefFoundError

在JVM或一个ClassLoader实例尝试加载一个类定义时,若无法找到定义就抛出这个错误。

它是在编译时能顺利地找到所需要依赖的类,但在运行时却找不到或可以找到多个所依赖的类而导致的错误。

异常抛出

通过throw将一个异常对象抛出。

throw new IllegalArgumentException("参数有误");
1

在方法定义中通过throws声明此方法可能会抛出的异常。

public boolean createNewFile() throws IOException
1

try-catch-finally

try语句块中捕获异常,并在catch语句块中处理异常,在finally语句块中做一些资源回收工作。

try {
    // 业务代码
} catch (IOException e) {
    // 异常处理
} finally {
    // 资源回收
}
1
2
3
4
5
6
7

try-with-resources

try-with-resourcesJDK7加入的特性,它可以在try()中指定一个或多个资源,而这些资源会在try() {}执行结束时自动关闭。

String readFirstLineFromFile(String path) throws IOException {  
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {  
        return br.readLine();
    }  
}
1
2
3
4
5

注意,前提是这些资源必须实现java.lang.AutoCloseable接口。另外,java.io.Closeable接口继承了这个接口,因此Closeable的实现类的实例也可使用try-with-resources

try-with-resources声明可以像普通的try声明一样同时拥有catch块与finally块,而且任意catch块或者finally块都会在资源关闭后被执行。

异常处理技巧

尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常

/* 错误示范 */
try {
    // 业务代码
    Thread.sleep(1000L)
} catch (Exception e) {
}
1
2
3
4
5
6
/* 正确示范 */
try {
    // 业务代码
    Thread.sleep(1000L);
} catch (InterruptedException e) {
}
1
2
3
4
5
6

try {}后跟了多个catch {}时,先捕获小范围的异常,后捕获大范围的异常。

不要生吞异常,比如将异常信息打印到标准出错中。应该将异常信息输出到日志中或将异常往上层抛。

/* 错误示范 */
try {
    // 业务代码
} catch (IOException e) {
    e.printStackTrace();
}
1
2
3
4
5
6

遵循Throw early, catch late原则。

如果捕获到的异常在当前阶段不便于处理,可以选择保留原有异常的case信息,直接再抛出或构建成新的异常抛出。随后,在更高的层面,因为有了更清晰的业务逻辑,往往会有更合适的处理异常的方法。

自定义异常

在实际开发中,往往需要根据业务需要自定义异常,此时除了保证提供足够的信息外,还要考虑几点:

  1. 是否需要定义为checked exception

  2. 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。比如Java标准类库中的java.net.ConnectException的出错信息大概是Connection refused,而不会包含具体的Host、IP、Port等信息。

性能问题

try-catch代码段会产生额外的性能开销,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大try-catch包住整段代码。

Java每实例化一个Exception,都会对当时的栈进行快照。