Java 的 Exception 机制是程序健壮性的基石,提供了结构化、可恢复的错误处理能力。理解异常的分类、传播和处理是 Java 开发的必备技能
一、Java 异常体系总览
java
// Java 异常继承结构
Throwable (可抛出)
├── Error (错误,不可恢复)
│ ├── OutOfMemoryError // 内存溢出
│ ├── StackOverflowError // 栈溢出
│ └── VirtualMachineError // JVM 内部错误
│
└── Exception (异常,可恢复)
├── RuntimeException (运行时异常,非检查型)
│ ├── NullPointerException // 空指针
│ ├── ArrayIndexOutOfBoundsException // 数组越界
│ ├── ClassCastException // 类型转换失败
│ ├── IllegalArgumentException // 非法参数
│ ├── ArithmeticException // 算术异常(如除零)
│ ├── NumberFormatException // 数字格式错误
│ └── ConcurrentModificationException // 并发修改
│
└── 受检异常 (Checked Exception,必须处理)
├── IOException // IO 操作失败
├── SQLException // 数据库访问错误
├── ClassNotFoundException // 类未找到
├── InterruptedException // 线程中断
├── FileNotFoundException // 文件不存在
└── ParseException // 解析错误
核心区别:
①Error :JVM 层面错误,程序不应捕获(如 OOM)
②RuntimeException :编码错误,可预防(如 NPE)
③Checked Exception:外部异常,必须处理(如 IOException)
二、受检异常 vs 非受检异常
| 对比维度 | Checked Exception | Unchecked Exception (RuntimeException) |
|---|---|---|
| 继承父类 | Exception(不包括 RuntimeException) |
RuntimeException |
| 编译时检查 | 必须处理(try-catch 或 throws) | 无需强制处理 |
| 设计理念 | 可恢复的外部错误(如文件不存在) | 可预防的编程错误(如空指针) |
| 典型例子 | IOException, SQLException |
NullPointerException, IllegalArgumentException |
| 处理策略 | 捕获并恢复,或继续抛出 | 修复代码 bug,通常不应捕获 |
| 官方推荐 | 尽量少用(Java 8+ Stream 不支持) | 优先使用 |
Java 8 Stream 中的限制
java
// ❌ 编译错误:Stream 的 lambda 不支持受检异常
list.stream().map(s -> {
return new SimpleDateFormat("yyyy").parse(s); // ParseException 是受检异常
});
// ✅ 解决方案:包装为运行时异常
list.stream().map(s -> {
try {
return new SimpleDateFormat("yyyy").parse(s);
} catch (ParseException e) {
throw new RuntimeException(e); // 包装
}
});
// 或自定义函数式接口
@FunctionalInterface
public interface FunctionWithException<T, R> {
R apply(T t) throws Exception;
}
三、异常处理机制:try-catch-finally-throw-throws
1. try-catch 基础语法
java
public void readFile(String path) {
try {
// 可能抛出异常的代码块
FileInputStream fis = new FileInputStream(path);
// ... 读取文件
} catch (FileNotFoundException e) {
// 捕获特定异常
System.err.println("文件不存在: " + path);
log.error("读取失败", e);
} catch (IOException e) {
// 捕获更广泛的异常
System.err.println("IO 错误: " + e.getMessage());
} catch (Exception e) {
// 捕获所有 Exception(不推荐)
System.err.println("未知错误");
}
}
2. finally 块:资源释放保证
java
public void processFile(String path) {
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// ... 处理文件
return "success"; // 即使有 return,finally 也会执行
} catch (IOException e) {
throw new RuntimeException("处理失败", e);
} finally {
// 必须执行的代码(资源释放)
if (fis != null) {
try {
fis.close(); // 关闭文件流
} catch (IOException e) {
log.warn("关闭流失败", e);
}
}
System.out.println("无论如何都会执行"); // 即使 try 中有 return
}
}
注意: finally 在 try 或 catch 执行后必然执行,除非:
System.exit()
JVM 崩溃
线程被杀死
3. try-with-resources(Java 7+):自动关闭资源
java
// Java 7 前:必须手动关闭
public void readFileOld(String path) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
return br.readLine();
} finally {
if (br != null) br.close(); // 繁琐且易遗漏
}
}
// Java 7+: 自动关闭
public void readFileNew(String path) throws IOException {
// 实现 AutoCloseable 接口的资源自动关闭
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine(); // 无需 finally,br 自动关闭
}
}
// 多个资源
try (FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")) {
// 同时管理多个流
byte[] buffer = new byte[1024];
fis.read(buffer);
fos.write(buffer);
} // fis 和 fos 自动关闭
实现原理:
java
// 编译器生成的等效代码
BufferedReader br = new BufferedReader(...);
Throwable primaryExc = null;
try {
return br.readLine();
} catch (Throwable t) {
primaryExc = t;
throw t;
} finally {
if (br != null) {
if (primaryExc != null) {
try {
br.close();
} catch (Throwable suppressedExc) {
primaryExc.addSuppressed(suppressedExc); // Java 7+ 抑制异常
}
} else {
br.close();
}
}
}
4. throw:主动抛出异常
java
public void setAge(int age) {
if (age < 0 || age > 150) {
// 抛出运行时异常(无需声明)
throw new IllegalArgumentException("年龄必须在 0-150 之间: " + age);
}
this.age = age;
}
public void process() throws IOException { // 声明抛出受检异常
if (fileNotExist) {
throw new FileNotFoundException("文件不存在");
}
}
5. throws:声明可能抛出的异常
java
// 方法签名中声明受检异常
public void readConfig() throws IOException, ParseException {
// 可能抛出 IO 或解析异常
FileInputStream fis = new FileInputStream("config.properties");
parseConfig(fis);
}
// 调用者必须处理
public void init() {
try {
readConfig();
} catch (IOException e) {
log.error("读取配置失败", e);
} catch (ParseException e) {
log.error("解析配置失败", e);
}
}
规则:
①受检异常 必须在方法签名中声明(throws)
②非受检异常 (RuntimeException)无需声明
③调用者必须捕获或继续声明受检异常
四、异常处理最佳实践
1. 优先使用非受检异常
java
// ✅ 推荐:运行时异常
public void process(String input) {
if (input == null) {
throw new IllegalArgumentException("输入不能为空");
}
}
// ❌ 不推荐:受检异常
public void process(String input) throws InvalidInputException {
if (input == null) {
throw new InvalidInputException("输入不能为空");
}
}
2. 精准捕获,不要吞掉异常
java
// ❌ 反模式:吞掉异常
try {
// 业务代码
} catch (Exception e) {
// 什么都没有做!异常信息丢失
}
// ❌ 反模式:只打印日志
try {
// 业务代码
} catch (SQLException e) {
log.error("数据库错误", e); // 记录后未抛出,上层无法感知
}
// ✅ 正确:捕获后要么恢复,要么抛出
try {
// 业务代码
} catch (SQLException e) {
log.error("数据库错误", e);
throw new DataAccessException("操作失败", e); // 包装后抛出
}
// ✅ 正确:特定异常恢复
try {
return jdbcTemplate.queryForObject(...);
} catch (EmptyResultDataAccessException e) {
return null; // 查询不到返回 null,是合法恢复
}
3. finally 块中不要抛出异常
java
// ❌ 危险:finally 中的异常会覆盖 try 中的异常
try {
return riskyOperation(); // 抛出 IOException
} finally {
cleanUp(); // 抛出 RuntimeException,会覆盖 IOException!
}
// ✅ 正确:finally 中捕获异常
try {
return riskyOperation();
} finally {
try {
cleanUp();
} catch (Exception e) {
log.warn("清理失败", e); // 记录但不抛出
}
}
4. 自定义异常层次结构
java
// 业务异常基类(非受检)
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() { return errorCode; }
}
// 具体业务异常
public class UserNotFoundException extends BusinessException {
public UserNotFoundException(Long userId) {
super("USER_NOT_FOUND", "用户不存在: " + userId);
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(Long accountId) {
super("INSUFFICIENT_BALANCE", "账户余额不足: " + accountId);
}
}
// 使用
@Service
public class AccountService {
public void deduct(Long accountId, BigDecimal amount) {
Account account = accountDao.findById(accountId);
if (account == null) {
throw new UserNotFoundException(accountId); // 抛出业务异常
}
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException(accountId);
}
// ... 扣减逻辑
}
}
5. 异常链(Exception Chaining)
java
// 保留原始异常信息
try {
// 底层操作
} catch (SQLException e) {
// ✅ 正确:将原始异常作为 cause 传递
throw new DataAccessException("数据库操作失败", e);
// ❌ 错误:丢失原始堆栈
throw new DataAccessException("数据库操作失败: " + e.getMessage());
}
6. Java 7+ 多异常捕获
java
// Java 7 前:重复代码
try {
// ...
} catch (IOException e) {
handleException(e);
} catch (SQLException e) {
handleException(e);
}
// Java 7+:多异常统一捕获
try {
// ...
} catch (IOException | SQLException e) {
handleException(e);
}
// 注意:只有 final 的变量可用
try {
// ...
} catch (final Exception e) { // e 必须是 final 或 effectively final
logger.log(e);
throw e; // 重新抛出
}
7. try-with-resources 的最佳实践
java
// Java 9+: 可以使用 final 或 effectively final 资源
BufferedReader br1 = new BufferedReader(...);
BufferedReader br2 = new BufferedReader(...);
try (br1; br2) { // 直接使用已声明的变量
// ...
}
// 资源关闭顺序:与声明顺序相反
// 声明:A → B → C
// 关闭:C → B → A
8. 异常日志记录规范
java
// ✅ 记录有意义的上下文
try {
processUser(user);
} catch (Exception e) {
log.error("处理用户失败, userId={}, userName={}, reason={}",
user.getId(), user.getName(), e.getMessage(), e); // 最后一个参数是异常
}
// ❌ 避免无意义日志
log.error("出错啦"); // 没有上下文
log.error("error: " + e); // 未打印堆栈
五、Java 7+ 异常新特性
1. Suppressed Exceptions(抑制异常)
java
try (Resource resource = new Resource()) {
resource.doWork(); // 抛出 WorkException
} catch (WorkException e) {
// Java 7+: 获取被抑制的异常(close 方法抛出的异常)
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("被抑制异常: " + suppressed);
}
}
2. 更精准的重新抛出
java
// Java 7 前:只能声明 throws Exception
public void process() throws Exception {
try {
// ...
} catch (Exception e) {
throw e; // 编译器要求 throws Exception(太宽泛)
}
}
// Java 7+: 编译器推断实际抛出的异常类型
public void process() throws IOException, SQLException {
try {
// ...
} catch (Exception e) {
throw e; // 编译器知道可能是 IOException 或 SQLException
}
}
六、终极最佳实践清单
✅ Do(应该做的)
①优先抛出非受检异常(RuntimeException)
②使用 try-with-resources 管理资源
③保持异常链(throw new MyException("msg", cause))
④自定义业务异常层次
⑤finally 中只做资源清理,不抛异常
⑥只捕获能处理的异常,其他继续抛出
⑦异常日志包含完整上下文
❌ Don't(不应该做的)
①不要吞掉异常(空 catch 块)
②不要用异常控制流程(性能差)
③不要捕获 Throwable/Error(可能捕获 OOM)
④不要在 finally 中使用 return(会覆盖 try 的 return)
⑤不要抛出 Exception 基类(信息不足)
⑥不要在循环中频繁抛出异常(性能杀手)
⑦不要在构造函数中抛出受检异常(工厂模式替代)
七、性能考虑
1.异常处理有性能开销,不应用于正常的控制流
2.创建异常对象成本较高(需要收集堆栈信息)
3.在性能关键路径上,优先使用条件检查而非异常
java
// 性能较差:使用异常
try {
return array[index];
} catch (ArrayIndexOutOfBoundsException e) {
return defaultValue;
}
// 性能较好:使用条件检查
if (index >= 0 && index < array.length) {
return array[index];
} else {
return defaultValue;
}
总结
Java 异常机制的核心要点:
1.分类清晰 :Error(不可恢复)、受检异常(必须处理)、非受检异常(可不处理)
2.处理灵活 :try-catch-finally、throws、throw
3.资源安全 :try-with-resources 自动管理资源
4.扩展性强 :支持自定义异常
5.信息丰富:异常链、堆栈跟踪、suppressed异常
正确使用异常机制可以:
1.提高代码的健壮性 和可维护性
2.提供清晰的错误信息和调试线索
3.实现优雅的错误处理和资源清理
4.分离正常逻辑 和错误处理逻辑