第 16 章:异常处理基础
学习目标
- 理解异常体系(Throwable / 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 IllegalArgumentException("除数不能为 0");
return a / b;
}二、异常体系(够用版)
Throwable
├─ Error:系统级错误(OOM / StackOverflow),不要捕获
└─ Exception
├─ RuntimeException:非受检异常(空指针、越界、除零)
└─ 其他 Exception:受检异常(IOException、SQLException)| 类型 | 代表 | 编译器强制? | 建议 |
|---|---|---|---|
| 受检异常(Checked) | IOException | ✅ 强制 try 或 throws | 能恢复就 catch |
| 非受检异常(Unchecked) | NullPointerException | ❌ 不强制 | 别 catch 掩盖,改代码 |
java
// 受检:必须处理
public void readFile() throws IOException {
String content = Files.readString(Path.of("a.txt"));
}
// 非受检:不强制
public void parse(String s) {
Integer.parseInt(s); // 可能抛 NumberFormatException
}⚠️ 坑 1:不要 catch RuntimeException(空指针、越界等)——这些是 bug,应该改代码,而不是 catch 后吞掉。
三、try-catch-finally
java
try {
// ① 可能出错的代码
int result = Integer.parseInt("abc");
} catch (NumberFormatException e) {
// ② 捕获具体异常
System.out.println("格式错误:" + e.getMessage());
} catch (Exception e) {
// ③ 兜底(必须放最后,匹配范围从小到大)
e.printStackTrace();
} finally {
// ④ 无论如何都执行(清理资源)
System.out.println("清理");
}⚠️ 坑 2:catch 顺序必须从小到大,先子类后父类,否则编译报错。
javacatch (Exception e) { } // ❌ 写前面会编译报错 catch (NumberFormatException e) { } // 子类必须放前面
⚠️ 坑 3:
finally里写return会吞掉异常,业务代码绝对禁止:javatry { throw new RuntimeException("业务错误"); } finally { return; } // ❌ 异常被吞,调用方根本不知道出错
四、try-with-resources(JDK 7+,推荐)
自动关闭实现了 AutoCloseable 接口的资源(如文件流、数据库连接):
java
// ❌ 传统写法:容易忘记关闭
BufferedReader reader = null;
try {
reader = Files.newBufferedReader(Path.of("a.txt"));
return reader.readLine();
} finally {
if (reader != null) reader.close(); // 啰嗦
}
// ✅ try-with-resources:自动关闭
try (BufferedReader reader = Files.newBufferedReader(Path.of("a.txt"))) {
return reader.readLine();
} // 自动调用 reader.close()多个资源:
java
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()
) {
// 使用 conn / ps / rs
} // 自动按相反顺序关闭五、抛异常的规则
什么时候 throw?
- 参数校验失败 →
throw new IllegalArgumentException(...) - 不支持的操作 →
throw new UnsupportedOperationException(...) - 自定义业务异常(下章讲) →
throw new BusinessException(...)
什么时候 throws?
- 自己是"受害者",不能处理 → 往上抛
- 例如:DAO 层读数据库抛
SQLException,Service 层可以包装后继续抛
java
public void readFile() throws IOException { // 声明抛出
Files.readString(Path.of("a.txt"));
}
public void processFile() { // 不 try,继续上抛
readFile(); // 调用方也得处理或继续抛
}六、本章小结
| 要点 | 关键 |
|---|---|
| Throwable | Error(系统级,不捕获)+ Exception |
| 受检异常 | 编译期强制处理,代表外部环境问题 |
| 非受检异常 | RuntimeException,代表程序 bug,不要 catch |
| try-catch-finally | catch 顺序:子类→父类,finally 不写 return |
| try-with-resources | 自动关闭资源,JDK 7+ 强烈推荐 |
| 何时抛/接 | 不能处理就抛,能处理就接 |
动手练习
- 除零异常:写
divide(int a, int b),除零时抛 IllegalArgumentException - 读取文件:用 try-with-resources 读文件,捕获 IOException
- 异常链:catch 一个异常后包装成业务异常再抛出,带上 cause
下一章:第 17 章:自定义异常与最佳实践 →