Java 异常体系:Checked 与 Unchecked 的真相(个人学习用)
目录
二、Checked Exception vs Unchecked Exception
2.2 Unchecked Exception(运行时异常)
前言
异常处理看起来简单,但面试中经常出现"诡异"的执行顺序题。本文从异常继承体系出发,把 Checked/Unchecked 的区别、finally 的坑、以及 try-with-resources 的语法糖讲清楚。
一、异常继承体系
Throwable
├── Error(程序无法处理,JVM 层面的问题)
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ └── ...
└── Exception
├── RuntimeException(Unchecked,运行时异常)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ClassCastException
│ ├── IllegalArgumentException
│ └── ...
└── 非 RuntimeException(Checked,编译时异常)
├── IOException
├── SQLException
├── FileNotFoundException
└── ...
关键分界线:是否继承 RuntimeException
二、Checked Exception vs Unchecked Exception
2.1 Checked Exception(编译时异常)
编译器强制要求你处理,不处理就编译不过。
// 必须 try-catch 或 throws,否则编译报错
public void readFile() {
FileInputStream fis = new FileInputStream("test.txt"); // IOException 是 Checked Exception
}
常见的 Checked Exception:
IOException/FileNotFoundExceptionSQLExceptionClassNotFoundExceptionInterruptedException
2.2 Unchecked Exception(运行时异常)
编译器不强制处理,运行时才暴露。
// 编译能过,运行时才崩
public void demo() {
String s = null;
s.length(); // NullPointerException,运行时才报
}
常见的 Unchecked Exception:
NullPointerException⭐ 最常见ArrayIndexOutOfBoundsExceptionClassCastExceptionArithmeticException(除以零)IllegalArgumentExceptionConcurrentModificationException
2.3 对比总结
| 对比项 | Checked Exception | Unchecked Exception |
|---|---|---|
| 继承关系 | Exception 的子类(非 RuntimeException) | RuntimeException 的子类 |
| 编译器检查 | 强制处理(try-catch 或 throws) | 不强制 |
| 典型场景 | 外部资源异常(IO、网络、数据库) | 代码逻辑错误(空指针、越界) |
| 是否可恢复 | 通常可恢复 | 通常不可恢复(代码 bug) |
| 是否应该捕获 | 应该 | 应该预防而非捕获 |
⭐ 面试题:
Error和Exception的区别?
答:Error是程序无法处理的严重错误(如 OOM),发生后程序应该直接退出。Exception是程序可以处理的异常,应该被捕获和恢复。
三、try-catch-finally 执行顺序
3.1 正常流程
try {
System.out.println("1. try");
int result = 10 / 2;
} catch (ArithmeticException e) {
System.out.println("2. catch");
} finally {
System.out.println("3. finally");
}
// 输出:1. try → 3. finally
有异常时:
try {
System.out.println("1. try");
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("2. catch");
} finally {
System.out.println("3. finally");
}
// 输出:1. try → 2. catch → 3. finally
结论:finally 永远会执行(除了 System.exit() 或 JVM 崩溃的情况)。
3.2 经典坑:finally 中的 return ⭐⭐⭐
public static int test() {
int x = 1;
try {
x++;
return x; // ① 准备返回 x=2
} finally {
x++; // ② x 变成 3
return x; // ③ finally 的 return 覆盖了 try 的 return
}
}
// 返回值:3(不是 2!)
为什么?finally 的 return 会覆盖 try 或 catch 中的 return。
更坑的版本:
public static int test() {
int x = 1;
try {
x++;
return x; // ① 准备返回 x=2,先把 2 存起来
} finally {
x++; // ② x 变成 3,但存起来的返回值还是 2
}
}
// 返回值:2(不是 3!)
这个更难理解:try 中 return x 时,JVM 会先把返回值暂存,然后执行 finally,最后返回暂存的值。finally 中修改 x 不影响已暂存的返回值。
⭐ 面试题:try 中 return 后,finally 还会执行吗?
答:会。finally 在 return 之前执行。但如果 finally 也有 return,会覆盖 try 的 return。
3.3 多 catch 的顺序
try {
// ...
} catch (Exception e) {
// ...
} catch (IOException e) { // ❌ 编译错误!IOException 是 Exception 的子类,永远不会走到
// ...
}
规则:子类异常必须写在父类异常前面,否则编译报错。
四、throw 与 throws
// throws:声明方法可能抛出的异常(方法签名上)
public void readFile() throws IOException {
// ...
}
// throw:手动抛出一个异常(方法体内)
public void setAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
}
| 对比项 | throw | throws |
|---|---|---|
| 位置 | 方法体内 | 方法签名 |
| 作用 | 抛出一个异常实例 | 声明可能抛出的异常类型 |
| 数量 | 一次只能抛一个 | 可以声明多个 |
五、try-with-resources
5.1 传统写法的冗余
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 使用 fis
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
finally 里又套了一层 try-catch,非常丑陋。
5.2 try-with-resources 语法
try (FileInputStream fis = new FileInputStream("test.txt")) {
// 使用 fis
} catch (IOException e) {
e.printStackTrace();
}
// fis 会自动关闭,不需要 finally
5.3 原理
try-with-resources 是语法糖,编译器会自动帮你生成 finally 中的 close() 调用。
前提是资源类必须实现 AutoCloseable 接口(Closeable 继承了 AutoCloseable)。
// 自定义可关闭资源
public class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("工作");
}
@Override
public void close() {
System.out.println("资源关闭");
}
}
// 使用
try (MyResource resource = new MyResource()) {
resource.doSomething();
}
// 输出:工作 → 资源关闭
5.4 多资源关闭顺序
try (FileInputStream fis = new FileInputStream("in.txt");
FileOutputStream fos = new FileOutputStream("out.txt")) {
// ...
}
// 关闭顺序:先 fos,后 fis(与声明顺序相反,LIFO)
⭐ 面试题:try-with-resources 中如果 try 和 close 都抛异常,哪个会被捕获?
答:try 中的异常会被捕获,close 中的异常会被抑制(suppressed exception),可以通过getSuppressed()获取。
六、自定义异常
// 自定义 Checked Exception
public class BusinessException extends Exception {
private int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
// 自定义 Unchecked Exception
public class ValidationException extends RuntimeException {
public ValidationException(String message) {
super(message);
}
}
实际开发中通常继承 RuntimeException,因为 Checked Exception 会让代码充满 try-catch,影响可读性。
七、异常处理最佳实践
- 不要用异常做流程控制——异常应该处理真正的异常情况
- 不要捕获
Exception或Throwable——应该精确捕获具体异常 - 不要忽略异常——空的
catch块是定时炸弹 - 尽早抛出,尽晚捕获——在能处理的地方才捕获
- 使用 try-with-resources——自动关闭资源,避免资源泄漏
// ❌ 反面教材
try {
// 一大堆代码
} catch (Exception e) {
// 什么都不做
}
// ✅ 正确做法
try {
// 尽量小的代码块
} catch (FileNotFoundException e) {
log.warn("文件不存在: {}", filename);
throw new BusinessException(404, "文件未找到");
}
八、面试高频题总结
| 问题 | 核心答案 |
|---|---|
| Error vs Exception | Error 不可恢复(OOM),Exception 可处理 |
| Checked vs Unchecked | Checked 编译时强制处理,Unchecked 运行时才暴露 |
| finally 一定会执行吗 | 会,除非 System.exit() 或 JVM 崩溃 |
| finally 中 return 的坑 | 会覆盖 try/catch 的 return,慎用 |
| try-with-resources 原理 | 语法糖,自动调用 close(),资源需实现 AutoCloseable |
| throw vs throws | throw 抛异常实例,throws 声明异常类型 |
更多推荐
所有评论(0)