Skip to content

异常处理

一、为什么需要异常处理?

C 语言用返回值表示错误(如返回 -1 或 NULL),但返回值可以被忽略,且错误码与正常返回值混在一起,不够清晰。

Java 的异常机制将"正常流程"和"错误处理"分离,强制调用方关注异常(checked exception),或者让异常沿调用栈自动向上传播(unchecked exception),避免错误被无声忽略。

二、异常体系

2.1 继承结构

Throwable
├── Error(不应该被捕获)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── NoClassDefFoundError
└── Exception
    ├── RuntimeException(非受检异常)
    │   ├── NullPointerException
    │   ├── IndexOutOfBoundsException
    │   └── IllegalArgumentException
    └── 其他 Exception(受检异常)
        ├── IOException
        ├── SQLException
        └── ClassNotFoundException

2.2 Error vs Exception

Error: JVM 级别的严重问题,应用程序不应该尝试捕获。如 OutOfMemoryError(内存耗尽)、StackOverflowError(递归过深)。发生了也无力回天,让它崩溃。

Exception: 应用程序可以处理的异常,分为两类:

  • 受检异常(Checked):编译期强制检查,必须 try-catch 或 throws。用于可预见的可恢复场景(如文件不存在)
  • 非受检异常(Unchecked):RuntimeException 及其子类,编译期不检查。通常是编程 Bug(如空指针),应该修复代码而非捕获

三、异常处理机制

3.1 try-catch-finally 的执行顺序

java
public static int test() {
    try {
        return 1;
    } catch (Exception e) {
        return 2;
    } finally {
        return 3;  // finally 中的 return 会覆盖 try/catch 的 return!
    }
}
// 结果:返回 3

原理: finally 块在方法返回前执行(即使 try 中有 return)。如果 finally 中也有 return,会覆盖之前的返回值。不要在 finally 中写 return。

3.2 try-with-resources(Java 7+)

为什么需要: 传统的 try-finally 关闭资源时,如果 close() 也抛异常,会覆盖 try 块中的原始异常,导致排查困难。try-with-resources 自动关闭资源,且保留原始异常,close() 的异常作为受抑制异常(suppressed)附加。

java
// 传统写法:繁琐且异常被覆盖
BufferedReader br = null;
try {
    br = new BufferedReader(new FileReader("file.txt"));
    return br.readLine();
} finally {
    if (br != null) br.close();  // close() 抛异常会覆盖 try 中的异常
}

// try-with-resources:简洁且保留原始异常
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
    return br.readLine();
}

原理: 实现了 AutoCloseable 接口的类都可以在 try-with-resources 中使用。编译器在字节码中自动生成 finally 块调用 close(),并用 Throwable.addSuppressed() 附加关闭异常。

四、异常设计最佳实践

4.1 什么时候用受检异常,什么时候用非受检?

场景用哪种示例
调用方可以恢复受检异常文件不存在 → 创建新文件
调用方无法恢复非受检异常空指针 → 代码 Bug,应修复
外部系统故障受检异常数据库连接失败
参数校验失败非受检异常参数为 null

4.2 异常链

为什么需要: 底层异常(如 IOException)对上层调用者没有意义。需要包装成业务异常,同时保留原始异常以便排查。

java
try {
    // 底层操作
} catch (IOException e) {
    throw new BusinessException("文件处理失败", e);  // 传入 cause,保留异常链
}

4.3 异常处理的反模式

java
// 反例1:吞掉异常
try { ... } catch (Exception e) { }  // 错误被无声忽略

// 反例2:只打印不处理
try { ... } catch (Exception e) { e.printStackTrace(); }  // 程序继续执行,状态可能不对

// 反例3:捕获过于宽泛
try { ... } catch (Exception e) { ... }  // 把 Error 也捕获了

// 反例4:在循环中使用 try-catch
for (...) {
    try { ... } catch (Exception e) { ... }  // JIT 可能不优化,性能差
}

五、性能影响

为什么异常影响性能? 创建异常时,Throwable.fillInStackTrace() 会遍历当前调用栈并记录,这是昂贵的操作。正常流程不应使用异常控制。

java
// 反例:用异常控制流程
try {
    Integer.parseInt(str);
} catch (NumberFormatException e) {
    // 不是数字
}

// 正例:用正则或字符判断
if (str != null && str.matches("\\d+")) {
    int num = Integer.parseInt(str);
}

六、面试要点

Q:Error 和 Exception 的区别?

Error 是 JVM 级别严重问题(OOM、StackOverflow),不应捕获;Exception 是程序可处理的异常,分为受检(编译期强制处理)和非受检(RuntimeException)。

Q:受检异常和非受检异常的区别?

受检异常编译期强制 try-catch 或 throws(如 IOException),用于可恢复场景;非受检异常(RuntimeException)编译期不检查,通常是编程 Bug,应修复代码而非捕获。

Q:finally 一定会执行吗?

不一定。System.exit(0) 强制终止 JVM 时 finally 不执行;守护线程中 JVM 退出时也不执行;System.exit() 前 finally 一定会执行(除非 kill -9)。

Q:try-with-resources 的原理?

编译器在字节码中生成 finally 块,调用 AutoCloseable.close()。如果 close() 也抛异常,会通过 addSuppressed() 附加到原始异常上,避免异常丢失。