异常处理
try-catch / checked vs unchecked
这节我们啃 Java 异常处理:异常的继承体系、try-catch-finally 三件套、面试常问的 checked vs unchecked 区别、还有 Java 7+ 的 try-with-resources 优雅写法。
学完这节:你能画出异常继承树、说清
RuntimeException 和其他 Exception 的本质区别、能用 try-with-resources 替代繁琐的 finally 关闭代码。
1
异常体系:Throwable 家族树
💡 动机:先把家族关系理清,再学单个异常就不乱
Java 所有异常都继承自 Throwable,分两族:Error(系统级)和 Exception(程序级)。
Throwable // 所有异常的祖宗
├── Error // 系统级错误,程序通常无法处理
│ ├── OutOfMemoryError // 内存溢出
│ ├── StackOverflowError // 栈溢出(递归太深)
│ └── ...
└── Exception // 程序级异常,可以/应该处理
├── IOException // checked:必须 try-catch 或 throws
├── SQLException // checked
├── ParseException // checked
└── RuntimeException // ⭐ unchecked:可以不处理
├── NullPointerException
├── ArrayIndexOutOfBoundsException
├── ArithmeticException
├── ClassCastException
├── IllegalArgumentException
├── NumberFormatException
└── ...
记忆口诀:
- Error = "天灾"(OOM、栈溢出)→ 别想着 catch,没救
- Exception = "人祸"(你代码写错了)→ 应该处理
- RuntimeException = "逻辑 bug" → 应该改代码,不是 catch
2
try-catch-finally:异常处理三件套
💡 动机:怎么"抓住"异常不让程序崩?
try {
// 可能抛异常的代码
int result = 10 / 0;
} catch (ArithmeticException e) {
// 抓住特定异常,处理
System.out.println("除数不能为 0");
e.printStackTrace(); // 打印堆栈(调试用)
} catch (Exception e) { // 可以有多个 catch,从具体到宽泛
System.out.println("其他异常:" + e.getMessage());
} finally {
// 无论是否异常,都会执行(常用于释放资源)
System.out.println("这行总会跑");
}
三件套要点:
try块:可能抛异常的代码catch块:从具体到宽泛,不然编译警告(子类必须在父类前)finally块:无论是否异常都跑(除了System.exit())- Java 7+ 可以
catch (IOException | SQLException e)多异常一起抓
⭐ return 和 finally 的优先级
// 面试题:下面的方法返回什么?
public static int test() {
try {
return 1;
} finally {
return 2; // ⚠️ finally 里的 return 会覆盖 try 里的
}
}
// 答:返回 2(finally 优先级更高)
// 实际开发:不要在 finally 里 return!会吞掉异常
3
checked vs unchecked ⭐ 面试常问
💡 动机:面试必问——这两类异常到底怎么区分?
| 对比 | Checked(受检) | Unchecked(非受检) |
|---|---|---|
| 父类 | Exception(不含 RuntimeException) |
RuntimeException 及其子类 |
| 编译器 | ⭐ 强制处理(try-catch 或 throws) | 不强制,可以不管 |
| 常见例子 | IOException, SQLException, ParseException | NPE, 数组越界, 除零, 类型转换 |
| 本质 | 外部环境问题(文件不存在、网络断了) | 代码 bug(逻辑错误) |
| 处理方式 | 必须处理或往上抛 | 应该改代码,不靠 catch |
// checked:必须处理
public void readFile() throws IOException { // 显式声明抛出
Files.readAllBytes(Paths.get("a.txt"));
}
// unchecked:编译不报错,但运行会崩
public void npe() {
String s = null;
s.length(); // 编译过,运行时 NPE
}
⭐ 面试题:Error 和 Exception 区别?
- Error = 系统级,JVM 自己处理不了(如 OOM),程序无法恢复
- Exception = 程序级,可以/应该处理(catch 或 throws)
- 两者都继承自 Throwable,但 Error 不是 Exception 的子类
4
5 个最常见的 RuntimeException
💡 动机:认识常见异常,看到报错就知道改哪里
点下面 5 个按钮,看每种异常会怎么触发、长啥样:
// 选上面一个场景看代码和异常
5 个常见异常的"翻译":
NullPointerException= 空指针:对象是null却调用了方法/属性ArrayIndexOutOfBoundsException= 数组越界:下标 < 0 或 ≥ lengthArithmeticException= 除零:整数除以 0(浮点除 0 得 Infinity)ClassCastException= 类型转换失败:(String) someObject但它不是 StringNumberFormatException= 数字格式错:Integer.parseInt("abc")
5
try-with-resources:优雅的资源关闭
💡 动机:原来关资源要写一堆 finally,Java 7 给了个简化写法
读写文件、网络连接、数据库连接……这些用完必须关闭,否则资源泄漏。
老写法(finally 关闭)
// ❌ 啰嗦版:每个资源都要 finally 关闭
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("a.txt"));
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try { br.close(); } catch (IOException e) { /* 忽略 */ }
}
}
新写法(try-with-resources)
// ✅ 优雅版:Java 7+,资源自动关闭
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// 出 try 块时,br 自动调用 close(),连 catch 都省了
使用条件:资源类必须实现
AutoCloseable 接口(Java 7+ 标准库里几乎所有资源类都已实现):
InputStream / OutputStream、Reader / WriterConnection / Statement / ResultSet(JDBC)Socket等
多资源用分号隔开:
try (InputStream in = new FileInputStream("a.txt");
OutputStream out = new FileOutputStream("b.txt")) {
// 两者都会自动关闭
}
6
自定义异常 + 最佳实践 ⭐ 面试常问
💡 动机:业务里"用户余额不足"、"订单不存在"这种业务错误怎么抛?
自定义异常 3 步
// 1. 继承 RuntimeException(业务异常一般是 unchecked)
public class InsufficientBalanceException extends RuntimeException {
// 2. 提供构造器
public InsufficientBalanceException(String message) {
super(message);
}
}
// 3. 在业务代码里抛
public void withdraw(double amount) {
if (balance < amount) {
throw new InsufficientBalanceException(
"余额不足:需要 " + amount + ",余额 " + balance
);
}
balance -= amount;
}
⭐ 异常处理 5 大最佳实践
- 能处理就处理,处理不了就抛出(不要 catch 了什么都不做)
- 异常信息要具体:
throw new IllegalArgumentException("age 不能为负: " + age),不是throw new IllegalArgumentException("error") - 不要用异常控制流程(try 里 for 循环,break 跳出来——这是反模式)
- 不要吞异常:catch 里至少
log.error(...)一下 - 资源关闭用 try-with-resources,别写 finally 关闭
⭐ 经典面试题:try-catch-finally 执行顺序
// 问:try 里 return 了,finally 还跑吗?
public static int test() {
try {
return 1;
} finally {
System.out.println("finally 跑了");
// return 2; // ⚠️ 千万别这么写
}
}
// 输出:先打印 "finally 跑了",然后返回 1
// 答:finally 在 return 之前 跑(修改 return 值除外)
你的作业(分步走)
- 触发 5 个常见异常:写代码故意触发
NullPointerException、ArrayIndexOutOfBoundsException、ArithmeticException、ClassCastException、NumberFormatException这 5 个,每个 catch 住并打印异常名 + 消息 - try-with-resources:写代码读
a.txt第一行,用 try-with-resources(不是 finally),验证文件关闭正常 - 自定义异常:写个
IllegalAgeException extends RuntimeException,然后写个register(int age)方法,age < 0 或 > 150 抛异常 - catch 顺序:写代码故意先 catch
Exception再 catchNullPointerException,看编译报什么错——体会"先具体后宽泛"的规则 - 面试题答题:凭记忆写下"checked vs unchecked 的区别"3 条 + "finally 和 return 哪个先执行"答案,贴给我看