主题
Chapter 13 · 异常处理
程序写得再好,也躲不过"网络断了、文件不见了、用户乱输入"这些事 — 异常机制就是 Java 的"应急预案"。
🎯 本章目标
- 理解 异常的本质:错误传递机制
- 区分 检查异常 vs 非检查异常
- 掌握
try-catch-finally与try-with-resources - 会用
throw和throws - 学会自定义异常
1. 没有异常会怎样?
c
// C 语言风格:用返回值判断错误
int result = divide(10, 0);
if (result == ERROR_DIV_ZERO) {
// 处理
}问题:
- 错误信息少(只有错误码,没有上下文)
- 容易忽略(忘了检查返回值就当没事发生)
- 代码满是 if 判断
Java 用 异常机制 解决:错误自动"抛出来",不处理就崩,强制你面对。
2. 异常体系
Throwable
├── Error → 严重错误,程序无法处理(OutOfMemoryError、StackOverflowError)
└── Exception → 一般错误
├── RuntimeException → 非检查异常(运行时异常)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ ├── IllegalArgumentException
│ └── ArithmeticException
└── 其他 Exception 子类 → 检查异常(编译期强制处理)
├── IOException
├── SQLException
└── ClassNotFoundException检查异常 vs 非检查异常
| 类型 | 必须处理? | 例子 | 设计意图 |
|---|---|---|---|
| Checked Exception | 是 | IOException、SQLException | 可恢复的、外部环境问题 |
| Unchecked Exception(RuntimeException) | 否 | NPE、IndexOutOfBoundsException | 程序员的 bug,应该修复 |
| Error | 否 | OOM、StackOverflowError | JVM 的事,应用搞不定 |
3. try-catch-finally
java
try {
// 可能出错的代码
int x = 10 / 0;
} catch (ArithmeticException e) {
// 处理这个异常
System.out.println("除零了:" + e.getMessage());
} catch (Exception e) {
// 兜底
System.out.println("其他异常");
} finally {
// 不管有没有异常,都会执行
System.out.println("收尾工作");
}多 catch(注意顺序)
子类异常必须写在父类前面!否则永远进不去子类的 catch。
java
try { ... }
catch (FileNotFoundException e) { } // 子类(先)
catch (IOException e) { } // 父类(后)JDK 7+ 支持 多重 catch(|):
java
try { ... }
catch (IOException | SQLException e) {
// 两种异常一起处理
}finally 一定会执行吗?
几乎一定。除非:
- JVM 崩了(
System.exit(0)) - 线程被 kill -9
- 死循环 / 无限阻塞
面试坑:
java
int test() {
try { return 1; }
finally { return 2; } // 返回 2!finally 覆盖 try 的返回值
}4. try-with-resources(推荐)
JDK 7+ 引入,自动关闭资源(替代笨重的 finally):
java
// ❌ 老写法
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("a.txt"));
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try { br.close(); } catch (IOException e) { ... }
}
}
// ✅ 新写法(自动关闭)
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
return br.readLine();
} catch (IOException e) {
e.printStackTrace();
}要求:资源必须实现 AutoCloseable 接口(绝大多数 IO 类都实现了)。
5. throw vs throws
| 关键字 | 位置 | 作用 |
|---|---|---|
throw | 方法体内 | 抛出 一个异常对象 |
throws | 方法签名上 | 声明 这个方法可能抛出的异常 |
java
public void readFile(String path) throws IOException { // 声明
if (path == null) {
throw new IllegalArgumentException("path 不能为 null"); // 抛出
}
// ... 可能抛 IOException
}调用方必须处理
java
void useIt() {
try {
readFile("a.txt"); // 必须 try-catch
} catch (IOException e) { ... }
}
// 或者继续往上抛
void useIt() throws IOException {
readFile("a.txt");
}6. 自定义异常
业务异常通常自定义:
java
public class InsufficientBalanceException extends RuntimeException {
private double current;
private double required;
public InsufficientBalanceException(double current, double required) {
super(String.format("余额不足,当前 %.2f, 需要 %.2f", current, required));
this.current = current;
this.required = required;
}
public double getCurrent() { return current; }
public double getRequired() { return required; }
}💡 怎么选父类?
- 业务可恢复 → 继承
RuntimeException(推荐,调用方自愿处理)- 必须强制处理 → 继承
Exception
7. 异常处理最佳实践
java
// ❌ 吞掉异常(最严重的反模式)
try { ... } catch (Exception e) {}
// ❌ 用 e.printStackTrace() 当日志(生产环境别这样)
catch (Exception e) { e.printStackTrace(); }
// ❌ 抓到异常啥也不做
catch (Exception e) { /* TODO */ }
// ❌ 抓 Throwable / Error
catch (Throwable t) { ... }
// ✅ 1. 用日志框架
catch (IOException e) {
log.error("读取配置失败", e);
throw new ConfigLoadException("无法启动", e);
}
// ✅ 2. 抛具体异常,不要包装太宽
throw new UserNotFoundException("用户 " + id + " 不存在");
// ✅ 3. 异常链:保留原始原因
throw new ServiceException("失败", originalException);8 大军规
- 不要吞异常
- catch 写得越小越好(别一上来就 catch Exception)
- finally 不要 return
- 优先用 try-with-resources
- 业务异常用 RuntimeException 子类
- 保留异常链(
new BusinessException("msg", originalException)) - 日志记录 + 抛新异常,不要又记又抛重复
- 不要用异常控制业务流程(性能差,可读性差)
8. 实战练习
| 文件 | 内容 |
|---|---|
BasicExceptionDemo.java | try-catch-finally |
TryWithResourcesDemo.java | 自动关闭资源 |
CustomException.java | 自定义业务异常 |
BankExceptionDemo.java | 银行业务异常实战 |
9. 浏览器演示
打开 demo.html:
- 异常类层级图
- 触发各种异常体验
- finally 执行顺序
10. 面试可能会问什么?
Q1: 检查异常和非检查异常的区别?
- 检查异常:编译期强制处理(catch 或 throws),通常表示外部环境问题
- 非检查异常(RuntimeException 及其子类):编译期不强制,通常表示程序 bug
Q2: try-catch-finally 中 finally 一定会执行吗?
几乎一定。除非:
- JVM 退出(System.exit)
- finally 块前有死循环或无限阻塞
- 当前线程被 kill
Q3: throw 和 throws 区别?
throw在 方法体内 抛出异常对象throws在 方法签名 上声明异常
Q4: NullPointerException 怎么避免?
- 提前判空
- 用
Objects.requireNonNull(obj, "msg") - 用
Optional - JDK 14+:开启
-XX:+ShowCodeDetailsInExceptionMessages,NPE 直接告诉你哪个变量是 null - 谨慎用基本类型包装类
Q5: try 块里有 return,finally 会执行吗?
会。finally 在 return 之前执行:
java
int test() {
try { return 1; } // ① 计算 1,存到返回寄存器
finally { System.out.println("finally"); } // ② 执行
// ③ 真正 return 1
}Q6: try-with-resources 的关闭顺序?
逆序:后声明的资源先关闭。
java
try (Resource a = ...; Resource b = ...) {
// 先关 b 再关 a
}Q7: 自定义异常应该继承谁?
- 业务异常 / 调用方有时不想处理 →
RuntimeException - 想强制调用方处理(如 IO) →
Exception
现代 Java 倾向 RuntimeException(Spring/Hibernate 都这么干)。
🎁 本章小结
✅ 异常 = 程序的"应急预案"
✅ Checked 编译期强制处理;Unchecked 不强制
✅ try-catch-finally:finally 几乎一定执行
✅ try-with-resources 自动关闭,优先用
✅ throw 抛出,throws 声明
✅ 自定义异常优先继承 RuntimeException🔗 导航
- ⬅️ 上一章:Chapter 12 · 常用类
- ➡️ 下一章:Chapter 14 · 集合框架