第 29 章:异常处理基础
学习目标
- 理解异常体系(Error / Exception / RuntimeException)
- 掌握 try-catch-finally 执行顺序
- 学会 try-with-resources
- 学会自定义异常
一、为什么需要异常?
java
// ❌ 用返回值表示错误:调用方容易忽略
public int divide(int a, int b) {
if (b == 0) return -1; // -1 是错误码还是正常结果?
return a / b;
}
// ✅ 用异常:调用方必须处理,且错误信息完整
public int divide(int a, int b) {
if (b == 0) throw new ArithmeticException("除数不能为 0");
return a / b;
}核心价值:把「正常逻辑」和「错误处理」分离,让主流程保持清晰。
二、异常体系
受检 vs 非受检
| 维度 | 受检异常(Checked) | 非受检异常(Unchecked) |
|---|---|---|
| 代表 | IOException、SQLException | RuntimeException 及其子类 |
| 编译器 | 强制处理(try 或 throws) | 不强制 |
| 语义 | 外部环境问题(文件不存在、网络断开) | 程序 bug(空指针、越界) |
| 建议 | 能恢复就捕获,不能就包装上抛 | 修 bug,别用 catch 掩盖 |
java
// 受检异常:必须处理,否则编译不过
public void readFile() throws IOException { // ① 声明抛出
Files.readString(Path.of("a.txt"));
}
// 非受检异常:不用声明
public void parse(String s) {
Integer.parseInt(s); // 可能抛 NumberFormatException(RuntimeException 子类)
}三、try-catch-finally
java
public static int read(String path) {
try { // ① 可能出错的代码
return Integer.parseInt(path);
} catch (NumberFormatException e) { // ② 捕获具体异常
System.out.println("格式错误:" + e.getMessage());
return -1;
} catch (Exception e) { // ③ 兜底(必须放最后)
return -2;
} finally { // ④ 无论如何都执行
System.out.println("清理资源");
}
}执行顺序
关键:
finally一定执行(除非System.exit()或 JVM 崩溃)。
坑:finally 里 return 会吞掉异常
java
// ❌ 永远返回 1,异常被吞掉
public int bad() {
try {
throw new RuntimeException("出错了");
} finally {
return 1; // ❌ 覆盖了异常
}
}规约:
finally里不要写 return / break / continue。
多异常合并捕获(JDK 7+)
java
try {
doSomething();
} catch (IOException | SQLException e) { // ① 用 | 分隔
log.error("IO 或 SQL 出错", e);
}四、try-with-resources(JDK 7+)
java
// ❌ 老写法:嵌套 finally,啰嗦且容易漏关
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("a.txt"));
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try { br.close(); } catch (IOException ignored) { }
}
}
// ✅ 新写法:自动关闭
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) { // ①
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
// ② 离开 try 块自动调用 br.close()原理:资源类实现了 AutoCloseable 接口,编译器自动插入 close() 调用。
java
public class MyResource implements AutoCloseable {
public void use() { System.out.println("使用资源"); }
@Override
public void close() { // ① 实现 close
System.out.println("资源已关闭");
}
}
try (MyResource r = new MyResource()) {
r.use();
}
// 输出:使用资源 → 资源已关闭规约:所有 IO 流、数据库连接、网络连接,一律用 try-with-resources。
五、抛出异常
java
// throw:抛出一个异常对象
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负:" + age);
}
this.age = age;
}
// throws:声明方法可能抛出的异常
public void readConfig() throws IOException {
Files.readString(Path.of("config.yml"));
}| 关键字 | 位置 | 作用 |
|---|---|---|
throw | 方法体内 | 实际抛出一个异常对象 |
throws | 方法签名上 | 声明「我可能抛这些异常,调用方注意」 |
六、自定义异常
java
// ① 继承 RuntimeException(业务异常推荐,不强制调用方 try)
public class BusinessException extends RuntimeException {
private final int code; // ② 携带业务错误码
public BusinessException(int code, String message) {
super(message); // ③ 交给父类保存 message
this.code = code;
}
public BusinessException(int code, String message, Throwable cause) {
super(message, cause); // ④ 保留原始异常链,排查必备
this.code = code;
}
public int getCode() { return code; }
}使用:
java
public User getUser(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(40401, "用户不存在: " + id);
}
return user;
}为什么继承 RuntimeException 而不是 Exception? 业务异常通常由全局异常处理器统一兜底(见第 51 章),不需要每一层都 try-catch,否则代码会被 try 淹没。
异常链:不要丢掉 cause
java
// ❌ 丢失原始堆栈,线上排查抓瞎
try {
jdbc.query(sql);
} catch (SQLException e) {
throw new BusinessException(50000, "查询失败"); // ❌ e 被丢弃
}
// ✅ 保留原始异常
try {
jdbc.query(sql);
} catch (SQLException e) {
throw new BusinessException(50000, "查询失败", e); // ✅
}七、异常处理规约
| 规约 | 说明 |
|---|---|
不要捕获 Throwable / Error | 虚拟机级错误,捕获也没法恢复 |
| 不要空 catch | catch (Exception e) {} 等于把 bug 藏起来 |
| 不要用异常控制流程 | 异常创建要抓堆栈,比 if 慢几十倍 |
| catch 要具体 | 先具体后宽泛,Exception 放最后 |
打日志用 log.error("msg", e) | 第二参数传异常对象才有堆栈 |
不要 e.printStackTrace() | 生产环境输出到 stderr,不进日志文件 |
java
// ❌ 用异常做流程控制(慢!)
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
// ✅ 用判断
return s != null && s.matches("-?\\d+");八、本章小结
| 要点 | 关键 |
|---|---|
| Throwable | Error(别管) + Exception(要管) |
| 受检异常 | 编译器强制处理 |
| 非受检异常 | RuntimeException,通常是 bug |
| finally | 一定执行,别在里面 return |
| try-with-resources | 自动关闭,IO 首选 |
| 自定义异常 | 业务异常继承 RuntimeException,带错误码 |
| 异常链 | 一定要传 cause |
动手练习
练习 1:基础题
预测输出顺序:
java
public static int test() {
try {
System.out.println("A");
return 1;
} finally {
System.out.println("B");
}
}
// 调用 test(),输出什么?返回什么?练习 2:进阶题
实现一个 BusinessException,包含错误码、错误消息、时间戳,并写一个静态工厂方法 BusinessException.of(ErrorCode code, Object... args) 支持消息模板填充。
下一章:第 30 章:异常最佳实践 →