本文目录导读:

- 案例 1:避免“万能捕获”(最基础的精准)
- 案例 2:资源清理中的精准异常(IO/NIO 操作)
- 案例 3:数据库操作中的精准异常处理(JDBC)
- 案例 4:自定义异常与精准的业务逻辑
- 案例 5:异常处理中的“精准忽略”(anti-pattern 的反面)
- 总结:精准异常处理的 5 条黄金法则
在Java中,精准处理异常的核心原则是:捕获具体的异常类型,而不是笼统的 Exception,这能让你的代码更具可读性、更健壮,并能针对不同的错误采取不同的恢复策略。
下面通过几个案例,从简单到复杂,展示如何“精准处理”。
案例 1:避免“万能捕获”(最基础的精准)
错误做法(不精准):
try {
String input = getUserInput(); // 可能返回 null
int number = Integer.parseInt(input); // 可能抛出 NumberFormatException
System.out.println(100 / number); // 可能抛出 ArithmeticException
} catch (Exception e) {
// 不管是什么异常,都打印并吞掉(最糟糕的情况)
System.out.println("出错了:" + e.getMessage());
}
// 问题:你不知道是空指针、数字格式错误还是除以0,无法针对性处理。
精准做法:
try {
String input = getUserInput();
int number = Integer.parseInt(input);
System.out.println(100 / number);
} catch (NumberFormatException e) {
// 精准处理:用户输入的不是数字
System.out.println("输入格式错误,请输入一个整数: " + e.getMessage());
// 可以提示用户重新输入
} catch (ArithmeticException e) {
// 精准处理:算术运算异常(如除以0)
System.out.println("除数不能为0,请重新输入: " + e.getMessage());
} catch (NullPointerException e) {
// 精准处理:输入为空
System.out.println("输入不能为空");
}
每个 catch 块只处理一种类型的异常,逻辑清晰。
案例 2:资源清理中的精准异常(IO/NIO 操作)
文件操作中,try-with-resources 已经帮我们解决了关闭资源的问题,但异常处理仍然需要精准。
场景: 读取配置文件,如果文件不存在,则使用默认配置;如果格式错误,则报错。
public static Properties loadConfig(String filePath) {
Properties config = new Properties();
try (InputStream input = new FileInputStream(filePath)) {
config.load(input);
System.out.println("成功加载配置文件");
} catch (FileNotFoundException e) {
// 精准处理:文件不存在 -> 使用默认值,不崩溃
System.out.println("配置文件 " + filePath + " 未找到,使用默认配置");
// 可以在这里设置默认值:config.setProperty("key", "defaultValue");
} catch (IOException e) {
// 精准处理:读取过程中发生IO错误 -> 需要向上抛或记录严重日志
System.err.println("读取配置文件时发生IO错误: " + e.getMessage());
// 这里可以抛出一个自定义运行时异常,让上层决定如何处理
throw new RuntimeException("无法读取配置文件", e);
}
return config;
}
关键点: FileNotFoundException 是 IOException 的子类。一定要先捕获具体的子类,再捕获父类,否则子类的 catch 块永远不会被执行。
案例 3:数据库操作中的精准异常处理(JDBC)
数据库操作涉及连接、SQL语法、数据约束等多种异常。
场景: 用户注册,插入用户记录。
public void registerUser(User user) {
String sql = "INSERT INTO users (username, email) VALUES (?, ?)";
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getEmail());
pstmt.executeUpdate();
System.out.println("用户注册成功");
} catch (SQLIntegrityConstraintViolationException e) {
// 精准处理:唯一约束冲突(如用户名或邮箱已存在)
// 这是业务逻辑层面的错误,需要给用户友好的提示
System.out.println("注册失败:用户名或邮箱已被注册");
// 不要将此异常继续向上抛,因为这是预期的业务错误
} catch (SQLSyntaxErrorException e) {
// 精准处理:SQL语法错误(通常是开发Bug)
System.err.println("数据库查询错误(SQL语法问题):" + e.getMessage());
// 应该立即通知开发人员修复
} catch (SQLTransientConnectionException e) {
// 精准处理:数据库连接失败(可重试的临时问题)
System.out.println("数据库暂时不可用,请稍后再试");
// 可以增加重试逻辑
} catch (SQLException e) {
// 兜底处理:其他未预料到的数据库异常
System.err.println("未知数据库错误:" + e.getMessage());
// 记录详细日志,避免干扰用户
}
}
技巧: 利用 SQLException 的子类(在 java.sql 或 com.mysql.cj.jdbc.exceptions 等驱动包中)来区分不同类型的数据库错误。
案例 4:自定义异常与精准的业务逻辑
当系统异常无法准确描述业务意图时,自定义异常是精准处理的最佳工具。
场景: 银行转账,余额不足、账户冻结、超过日限额。
// 自定义异常类
class InsufficientBalanceException extends RuntimeException {
private double currentBalance;
private double requiredAmount;
public InsufficientBalanceException(double currentBalance, double requiredAmount) {
super("余额不足,当前余额: " + currentBalance + ", 需要: " + requiredAmount);
this.currentBalance = currentBalance;
this.requiredAmount = requiredAmount;
}
// getter...
}
class AccountFrozenException extends Exception { // 可以是检查型异常
public AccountFrozenException(String accountId) {
super("账户 " + accountId + " 已被冻结");
}
}
// 业务处理器
public void transfer(String fromAccount, String toAccount, double amount)
throws AccountFrozenException { // 声明检查型异常
// 1. 检查账户状态
if (isFrozen(fromAccount)) {
throw new AccountFrozenException(fromAccount);
}
// 2. 检查余额
double balance = getBalance(fromAccount);
if (balance < amount) {
// 精准异常:携带了具体的错误数据,方便上层展示
throw new InsufficientBalanceException(balance, amount);
}
// 3. 执行转账(省略)
System.out.println("转账成功");
}
// 调用处(精准捕获)
try {
transfer("123", "456", 5000);
} catch (InsufficientBalanceException e) {
// 精准提示:告诉用户还差多少钱
System.out.println("转账失败:" + e.getMessage() +
",您还缺 " + (e.getRequiredAmount() - e.getCurrentBalance()) + " 元");
} catch (AccountFrozenException e) {
System.out.println("账户异常:" + e.getMessage());
} catch (Exception e) {
// 其他技术异常(如网络中断)
System.err.println("系统繁忙,请稍后重试");
}
好处: 自定义异常携带了准确的业务数据(如余额、冻结原因),让 catch 块能做出最合适的响应。
案例 5:异常处理中的“精准忽略”(anti-pattern 的反面)
某些异常确实可以安全忽略,但必须显式注明原因。
// 精准忽略:明确知道该异常在此场景下无害,并记录一下
try {
connection.setAutoCommit(false);
} catch (SQLException e) {
// 此数据库驱动可能不支持自动提交设置,忽略此异常,因为后续代码会处理事务
// 但为了调试需要,记录一下日志
logger.debug("不支持设置 autoCommit false,继续执行: {}", e.getMessage());
}
// 注意:不要空 catch 块,注释必须明确说明为什么忽略。
精准异常处理的 5 条黄金法则
- 捕获最具体的类型:
catch (NumberFormatException e)而不是catch (Exception e)。 - 多 catch 块顺序:子类在前,父类在后。
- 使用
try-with-resources:自动关闭资源,并支持addSuppressed异常,不会丢失原始异常。 - 区分“可恢复”和“不可恢复”:
- 可恢复(如用户输入错误、文件不存在):处理异常,给出友好提示,让程序继续运行。
- 不可恢复(如网络断开、SQL语法错误):记录日志,向上抛(通常转为
RuntimeException),让全局错误处理器处理。
- 善用自定义异常:将技术异常(如
SQLException)转化为业务异常(如AccountFrozenException),让调用代码更清晰。
永远不要这样写:
try {
// ...
} catch (Exception e) {
e.printStackTrace(); // 或 logger.error(e.getMessage());
// 没有恢复、没有转换、甚至没有日志级别区分
}
这个代码在线上等于“我不知道发生了什么,也不打算处理”,是线上故障的常见根源。