Appearance
异常处理
一、为什么需要异常处理?
C 语言用返回值表示错误(如返回 -1 或 NULL),但返回值可以被忽略,且错误码与正常返回值混在一起,不够清晰。
Java 的异常机制将"正常流程"和"错误处理"分离,强制调用方关注异常(checked exception),或者让异常沿调用栈自动向上传播(unchecked exception),避免错误被无声忽略。
二、异常体系
2.1 继承结构
Throwable
├── Error(不应该被捕获)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── NoClassDefFoundError
└── Exception
├── RuntimeException(非受检异常)
│ ├── NullPointerException
│ ├── IndexOutOfBoundsException
│ └── IllegalArgumentException
└── 其他 Exception(受检异常)
├── IOException
├── SQLException
└── ClassNotFoundException2.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() 附加到原始异常上,避免异常丢失。
