异常处理
异常处理是 Java 程序健壮性的重要保障。理解异常体系、掌握异常处理最佳实践,是 Java 开发者的必备技能。
一、异常体系
1.1 异常继承结构
Java 异常体系的核心是 Throwable 类,所有异常都继承自它。
┌─────────────┐
│ Throwable │
└──────┬──────┘
│
┌──────────────┴──────────────┐
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Error │ │ Exception │
└──────┬──────┘ └──────┬──────┘
│ │
┌───────┼───────┐ ┌───────────┼───────────┐
│ │ │ │ │ │
┌───┴───┐ ┌─┴───┐ ┌─┴───┐ ┌───┴───┐ ┌─────┴─────┐ ┌───┴───┐
│Virtual │ │OutOf│ │Stack │ │Runtime│ │ IOException│ │ SQL │
│Machine │ │Memory│ │Over- │ │Exception│ │ │ │Exception│
│Error │ │Error │ │flow │ │ │ │ │ │ │
└────────┘ └─────┘ └──────┘ └────┬───┘ └────────────┘ └───────┘
│
┌───────┼───────┐
│ │ │
┌───┴───┐ ┌─┴───┐ ┌─┴───────┐
│NullP- │ │Array│ │ClassCast│
│ointer │ │Index│ │Exception│
│Except-│ │Out- │ │ │
│ion │ │Bound│ │ │
└───────┘ └─────┘ └─────────┘1.2 三大类别的区别
| 类别 | 说明 | 特点 | 处理方式 |
|---|---|---|---|
| Error | 系统级错误 | JVM 无法恢复的严重问题 | 无法处理,程序终止 |
| Checked Exception | 受检异常 | 编译器强制处理 | 必须捕获或声明抛出 |
| Unchecked Exception | 非受检异常 | 编译器不强制处理 | 可选捕获或抛出 |
// Error 示例:通常不需要捕获
OutOfMemoryError // 内存溢出
StackOverflowError // 栈溢出(递归过深)
VirtualMachineError // 虚拟机错误
// Checked Exception 示例:必须处理
IOException // IO 异常
SQLException // 数据库异常
ClassNotFoundException // 类找不到
FileNotFoundException // 文件不存在
// Unchecked Exception(RuntimeException 子类):可选处理
NullPointerException // 空指针
ArrayIndexOutOfBoundsException // 数组越界
ClassCastException // 类型转换异常
ArithmeticException // 算术异常(如除零)
IllegalArgumentException // 非法参数
NumberFormatException // 数字格式异常1.3 Error vs Exception
| 对比项 | Error | Exception |
|---|---|---|
| 含义 | 系统级错误 | 程序级错误 |
| 来源 | JVM | 应用程序 |
| 可恢复性 | 不可恢复 | 可恢复 |
| 处理方式 | 不建议捕获 | 必须处理或声明 |
| 典型例子 | OutOfMemoryError | NullPointerException |
1.4 Checked vs Unchecked Exception
| 对比项 | Checked Exception | Unchecked Exception |
|---|---|---|
| 父类 | Exception(非 RuntimeException) | RuntimeException |
| 编译检查 | 强制处理 | 不强制 |
| 设计理念 | 可预期的异常 | 编程错误 |
| 例子 | IOException, SQLException | NPE, ArrayIndexOutOfBounds |
// Checked Exception:编译器强制处理
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path); // 必须处理或声明抛出
}
// Unchecked Exception:编译器不强制
public void process(String s) {
System.out.println(s.length()); // 可能 NPE,但编译器不强制处理
}二、try-catch-finally
2.1 基本语法
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e) {
// 处理 ExceptionType1 类型的异常
} catch (ExceptionType2 e) {
// 处理 ExceptionType2 类型的异常
} finally {
// 无论是否异常都会执行(可选)
}2.2 执行流程
┌─────────────────────────────────────────────┐
│ try 块 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ 正常执行完毕 │ 或 │ 抛出异常 │ │
│ └──────┬──────┘ └──────┬──────┘ │
└──────────┼────────────────────┼────────────┘
│ │
│ 匹配 catch
│ │
│ ┌─────┴─────┐
│ │ catch 块 │
│ └─────┬─────┘
│ │
└────────┬───────────┘
│
┌─────┴─────┐
│ finally 块│ ← 无论是否异常都执行
└───────────┘2.3 多重捕获
// 多个 catch 块(从具体到宽泛)
try {
// ...
} catch (FileNotFoundException e) {
System.out.println("文件不存在");
} catch (IOException e) {
System.out.println("IO 异常");
} catch (Exception e) {
System.out.println("其他异常");
}
// JDK 7+ 多异常捕获
try {
// ...
} catch (FileNotFoundException | SQLException e) {
System.out.println("文件或数据库异常");
}2.4 finally 块
finally 的执行特点:
- 无论是否发生异常,finally 都会执行
- 即使 try 或 catch 中有 return,finally 也会执行
- finally 中的 return 会覆盖 try/catch 中的 return
// 示例 1:finally 总是执行
public void test() {
try {
System.out.println("try");
return;
} finally {
System.out.println("finally"); // 仍然执行
}
}
// 输出:try → finally
// 示例 2:finally 覆盖返回值
public int test() {
try {
return 1;
} finally {
return 2; // 覆盖 try 中的 return
}
}
// 返回 2
// 示例 3:finally 不执行的情况
try {
System.exit(0); // JVM 退出,finally 不执行
} finally {
System.out.println("不会执行");
}2.5 try-with-resources(JDK 7+)
实现了 AutoCloseable 接口的资源可以自动关闭。
// 传统方式
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// 使用资源
} finally {
if (fis != null) {
fis.close(); // 手动关闭
}
}
// try-with-resources(推荐)
try (FileInputStream fis = new FileInputStream("test.txt")) {
// 使用资源
} // 自动关闭
// 多个资源
try (
FileInputStream fis = new FileInputStream("in.txt");
FileOutputStream fos = new FileOutputStream("out.txt")
) {
// 使用资源
} // 自动关闭(按声明逆序关闭)2.6 常见陷阱
陷阱 1:finally 覆盖异常
public void test() {
try {
throw new RuntimeException("try 异常");
} finally {
throw new RuntimeException("finally 异常"); // 覆盖 try 中的异常
}
}
// 抛出 "finally 异常","try 异常" 被吞掉陷阱 2:finally 覆盖返回值
public int test() {
int x = 1;
try {
return x;
} finally {
x = 2; // 修改 x 不影响返回值(返回值已缓存)
}
}
// 返回 1
public int test() {
try {
return 1;
} finally {
return 2; // 直接 return 会覆盖
}
}
// 返回 2三、throw 与 throws
3.1 throw 关键字
throw 用于主动抛出异常。
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0-150 之间");
}
this.age = age;
}
// 抛出自定义异常
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException("余额不足");
}
balance -= amount;
}3.2 throws 关键字
throws 用于方法签名中声明可能抛出的异常。
// 声明抛出 Checked Exception
public void readFile(String path) throws IOException, FileNotFoundException {
FileReader reader = new FileReader(path);
}
// 声明抛出多个异常
public void connect() throws IOException, SQLException {
// ...
}
// 子类重写方法时,抛出异常的限制
class Parent {
public void method() throws IOException { }
}
class Child extends Parent {
// 重写方法:可以抛出相同异常或子类异常,或不抛出
@Override
public void method() throws FileNotFoundException { } // ✅ IOException 子类
// @Override
// public void method() throws Exception { } // ❌ 编译错误:不能抛出更宽泛的异常
}3.3 throw vs throws
| 对比项 | throw | throws |
|---|---|---|
| 位置 | 方法体内 | 方法签名后 |
| 作用 | 抛出异常对象 | 声明可能抛出的异常类型 |
| 后面跟什么 | 异常对象(一个) | 异常类型(可多个) |
| 使用场景 | 主动抛出异常 | 声明受检异常 |
// throw:主动抛出
public void validate(String name) {
if (name == null) {
throw new NullPointerException("名称不能为空"); // 抛出对象
}
}
// throws:声明异常
public void process() throws IOException, SQLException { // 声明类型
// 可能抛出 IOException 或 SQLException
}四、自定义异常
4.1 创建自定义异常
// 自定义受检异常
public class InsufficientBalanceException extends Exception {
public InsufficientBalanceException() {
super();
}
public InsufficientBalanceException(String message) {
super(message);
}
public InsufficientBalanceException(String message, Throwable cause) {
super(message, cause);
}
}
// 自定义非受检异常
public class InvalidParameterException extends RuntimeException {
public InvalidParameterException(String message) {
super(message);
}
}4.2 使用自定义异常
public class BankAccount {
private double balance;
public void withdraw(double amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException(
"余额不足:当前余额 " + balance + ",取款金额 " + amount
);
}
balance -= amount;
}
}
// 使用
BankAccount account = new BankAccount();
try {
account.withdraw(1000);
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage());
}4.3 设计原则
| 原则 | 说明 |
|---|---|
| 选择正确的父类 | 可恢复 → Checked Exception;编程错误 → Unchecked Exception |
| 提供有意义的构造器 | 无参、带消息、带原因 |
| 保留异常链 | 使用 cause 参数保留原始异常 |
| 命名规范 | 以 Exception 结尾 |
// 保留异常链
public class DataAccessException extends Exception {
public DataAccessException(String message, Throwable cause) {
super(message, cause); // 保留原始异常
}
}
// 使用
try {
// 数据库操作
} catch (SQLException e) {
throw new DataAccessException("数据访问失败", e); // 保留原始异常
}五、异常处理最佳实践
5.1 应该做的
| 实践 | 说明 |
|---|---|
| 只捕获能处理的异常 | 不要为了通过编译而捕获 |
| 保留原始异常信息 | 使用异常链 |
| 尽早抛出异常 | fail-fast 原则 |
| 使用具体异常类型 | 避免直接 catch Exception |
| 记录异常日志 | 便于问题排查 |
| 合理使用自定义异常 | 提供业务语义 |
// ✅ 正确做法
// 1. 保留原始异常
try {
// ...
} catch (SQLException e) {
throw new BusinessException("操作失败", e); // 保留原因
}
// 2. 尽早抛出
public void setName(String name) {
if (name == null) {
throw new IllegalArgumentException("名称不能为空"); // 尽早失败
}
this.name = name;
}
// 3. 使用具体异常
try {
// ...
} catch (FileNotFoundException e) {
// 处理文件不存在
} catch (IOException e) {
// 处理其他 IO 异常
}
// 4. 记录日志
try {
// ...
} catch (Exception e) {
log.error("操作失败", e);
throw e;
}5.2 不应该做的
| 反模式 | 说明 |
|---|---|
| 捕获后不处理 | 吞掉异常,隐藏问题 |
| 捕获 Exception | 太宽泛,可能捕获不应捕获的异常 |
| finally 中 return | 覆盖返回值和异常 |
| 异常代替流程控制 | 性能差,代码可读性低 |
| 忽略 Checked Exception | 空 catch 块 |
// ❌ 错误做法
// 1. 吞掉异常
try {
// ...
} catch (Exception e) {
// 空块,异常被忽略
}
// 2. 捕获太宽泛
try {
// ...
} catch (Exception e) { // 捕获了所有异常,包括 Error
e.printStackTrace();
}
// 3. finally 中 return
public int test() {
try {
return 1;
} finally {
return 2; // 覆盖返回值
}
}
// 4. 异常代替流程控制
public boolean isNumber(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false; // 不推荐
}
}
// 5. 空 catch
try {
// ...
} catch (IOException e) {
// 什么都不做
}5.3 异常处理原则
┌────────────────────────────────────────────┐
│ 异常处理决策树 │
├────────────────────────────────────────────┤
│ │
│ 能处理这个异常吗? │
│ │ │
│ ┌────┴────┐ │
│ 能 不能 │
│ │ │ │
│ ↓ ↓ │
│ 处理它 声明抛出(throws) │
│ │ │ │
│ ↓ 需要转换异常吗? │
│ 继续执行 │ │
│ ┌──┴──┐ │
│ 是 否 │
│ │ │ │
│ ↓ ↓ │
│ 包装抛出 直接抛出 │
│ │
└────────────────────────────────────────────┘六、异常链
6.1 什么是异常链
异常链(Exception Chaining)是指在捕获一个异常后,抛出另一个异常,同时保留原始异常信息。
public class BusinessException extends Exception {
public BusinessException(String message, Throwable cause) {
super(message, cause); // 保留原始异常
}
}
// 使用
try {
// 数据库操作
} catch (SQLException e) {
throw new BusinessException("业务操作失败", e); // 保留 SQLException 作为原因
}
// 获取原始异常
catch (BusinessException e) {
System.out.println(e.getMessage()); // "业务操作失败"
System.out.println(e.getCause()); // SQLException
e.printStackTrace(); // 打印完整异常链
}6.2 异常链的作用
| 作用 | 说明 |
|---|---|
| 保留根因 | 不丢失原始异常信息 |
| 语义转换 | 将底层异常转换为业务异常 |
| 便于排查 | 完整的异常调用链 |
七、常见面试题
Q1: Error 和 Exception 有什么区别?
A:
- Error:系统级错误,JVM 无法恢复,程序通常终止(如 OutOfMemoryError)
- Exception:程序级错误,可以捕获处理,程序可以继续运行
Q2: Checked Exception 和 Unchecked Exception 的区别?
A:
| 对比项 | Checked | Unchecked |
|---|---|---|
| 父类 | Exception(非 RuntimeException) | RuntimeException |
| 编译检查 | 强制处理 | 不强制 |
| 代表 | IOException, SQLException | NPE, ArrayIndexOutOfBounds |
Q3: finally 块一定会执行吗?
A: 不一定。以下情况 finally 不会执行:
System.exit()调用- JVM 崩溃
- 线程被杀死(如
Thread.stop())
Q4: try 块中有 return,finally 还会执行吗?
A: 会。finally 在 return 之前执行,但如果 finally 中也有 return,会覆盖 try 中的返回值。
public int test() {
try {
return 1;
} finally {
return 2; // 最终返回 2
}
}Q5: 以下代码输出什么?
public static int test() {
int x = 1;
try {
return x++;
} finally {
x++;
}
}
System.out.println(test());A: 输出 1。执行流程:
x++返回 1,x 变为 2- return 值(1)已缓存
- finally 中
x++,x 变为 3(但不影响返回值) - 返回缓存的值 1
Q6: 如何设计一个良好的异常处理体系?
A:
- 分层设计:DAO 层抛出 SQLException,Service 层转换为业务异常
- 异常分类:系统异常、业务异常、第三方异常
- 统一处理:使用框架(如 Spring 的
@ExceptionHandler)统一处理 - 日志记录:异常发生时记录完整日志
- 用户友好:向前端返回友好的错误信息
// 分层异常处理示例
// DAO 层
public User findById(Long id) throws DataAccessException {
try {
// 数据库操作
} catch (SQLException e) {
throw new DataAccessException("查询失败", e);
}
}
// Service 层
public User getUser(Long id) throws BusinessException {
try {
return userDao.findById(id);
} catch (DataAccessException e) {
throw new BusinessException("获取用户信息失败", e);
}
}
// Controller 层(统一处理)
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e) {
return Result.fail(e.getMessage());
}Q7: 为什么不推荐用异常做流程控制?
A:
- 性能差:异常创建开销大,需要填充堆栈信息
- 可读性低:代码难以理解
- 难以维护:正常流程和异常处理混在一起
// ❌ 不推荐
public boolean isValid(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}
// ✅ 推荐
public boolean isValid(String s) {
if (s == null) return false;
return s.matches("\\d+");
}八、总结
| 概念 | 核心要点 | 面试关键词 |
|---|---|---|
| 异常体系 | Throwable → Error/Exception → Checked/Unchecked | 继承关系、分类 |
| try-catch-finally | 捕获异常、finally 必执行 | 执行顺序、return 覆盖 |
| throw vs throws | 抛出对象 vs 声明类型 | 位置、作用不同 |
| 自定义异常 | 继承 Exception 或 RuntimeException | 命名、构造器、异常链 |
| 最佳实践 | 早抛出、晚捕获、保留异常链 | 不要吞掉异常 |
最后更新:2026年3月2日