异常
约 1186 字大约 4 分钟
布欧-Lewyon
2026-05-15
异常是 Java 报告与处理运行时错误的主要手段。理解异常的类型体系与正确使用姿势,能帮你写出更健壮的代码。
异常层次
Throwable
├── Error (不可恢复,如 OutOfMemoryError、StackOverflowError)
└── Exception
├── RuntimeException(非受检异常)
└── 其他 Exception (受检异常,如 IOException、SQLException)Error:JVM 层面的严重问题,应用不应捕获,捕获也无法恢复。RuntimeException及其子类:非受检异常(unchecked),编译器不强制处理。常见:NullPointerException、IndexOutOfBoundsException、IllegalArgumentException、ArithmeticException。- 其他
Exception子类:受检异常(checked),编译器强制要么try-catch要么在方法签名上声明throws。常见:IOException、SQLException、FileNotFoundException。
捕获与处理
try-catch-finally
try {
// 可能抛出异常的代码
FileReader reader = new FileReader("test.txt");
} catch (FileNotFoundException e) {
System.err.println("文件未找到: " + e.getMessage());
} finally {
// 不论是否抛异常都会执行,通常用于释放资源
System.out.println("清理工作");
}catch可写多个,范围小的异常放前面。finally可选;若try/catch中有return,finally仍会执行,避免在finally中写return,它会覆盖前面的返回值。
try-with-resources(Java 7+)
实现了 AutoCloseable 的资源可自动关闭,无需显式写 finally:
try (FileReader reader = new FileReader("test.txt");
BufferedReader br = new BufferedReader(reader)) {
String line = br.readLine();
} catch (IOException e) {
System.err.println("IO 异常: " + e.getMessage());
}- 支持在
try括号中声明多个资源,按声明逆序关闭。 catch和finally在try-with-resources中仍然可选。
方法签名声明:throws
若当前方法不处理某个受检异常,可在方法签名上声明 throws,让调用者处理:
public void readConfig() throws IOException {
// 可能抛出 IOException,不在此方法内捕获
}不要在 main 方法一路 throws Exception 到顶层就算了——用户应该至少看到有意义的错误提示。
抛出异常:throw
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法: " + age);
}
this.age = age;
}- 抛
RuntimeException或其子类(如IllegalArgumentException、NullPointerException)时无需在方法签名上加throws。 - 抛受检异常时需要方法声明
throws或方法内try-catch。
自定义异常
当业务错误需要被明确识别和单独处理时,定义专用的异常类比抛 RuntimeException("出错了") 更可维护。自定义异常的本质是继承 Exception(受检)或 RuntimeException(非受检),并可在构造器中传入额外的业务上下文。
以下定义一个账户余额不足的异常。它继承了 RuntimeException,因此调用者无须强制 try-catch;构造器中传入了余额和所需金额,方便上层捕获后展示具体信息:
public class InsufficientBalanceException extends RuntimeException {
private final double currentBalance;
private final double requiredAmount;
public InsufficientBalanceException(double currentBalance, double requiredAmount) {
super(String.format("余额不足: 当前 %.2f,需要 %.2f", currentBalance, requiredAmount));
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
public double getCurrentBalance() {
return currentBalance;
}
}- 继承
RuntimeException则非受检(建议多数业务异常用非受检);继承Exception则受检。 - 构造器中传入足够上下文,方便上层捕获后区分处理或打印日志。
异常中记录日志
try {
// 操作
} catch (BusinessException e) {
logger.error("业务处理失败: userId={}, orderId={}", userId, orderId, e);
throw e; // 或转抛另一种异常
}- 记录日志时始终传入异常对象作为最后一个参数,以保留堆栈轨迹(stack trace)。
- 不要只记录
e.getMessage()而丢掉了堆栈。
常见的坑
| 反例 | 为什么不好 | 改进 |
|---|---|---|
catch (Exception e) { e.printStackTrace(); } | 打印到 stderr 而非日志系统,且不做处理 | 记录日志或转抛 |
catch (NullPointerException e) { ... } | NPE 通常是代码缺陷,不应靠捕获处理 | 在源头防御(判空) |
try { ... } catch (Exception e) {} 空吞异常 | 错误被静默忽略,调试极其困难 | 至少记录日志 |
在 finally 中 return | 覆盖了 try / catch 中的返回值或异常 | 不要在 finally 写 return |
小结
- 异常三层:
Error(不处理)、受检异常(编译器强制处理)、非受检RuntimeException(可选处理)。 - 受检异常需要
try-catch或throws声明;非受检异常(如 NPE)通常来自代码缺陷,应修复而非捕获。 try-with-resources自动关闭AutoCloseable资源,优先于传统try-finally。- 自定义异常继承
RuntimeException(非受检)或Exception(受检),构造器中传入足够业务上下文。 - 日志务必传入异常对象保留堆栈,不得只记
e.getMessage()或空吞异常。 - 易错:
finally中写return会覆盖try/catch中的返回值或异常——经典的隐蔽 bug,永远不要这样做。 - 思考任务:写一个方法
parseIntSafely(String s),内部用Integer.parseInt并捕获NumberFormatException,返回OptionalInteger或默认值;再用try-with-resources读取一个不存在的文件,观察异常信息。
上一节:大数计算与精度
下一节:泛型基础
