把异常当回事,代码才靠谱
《代码整洁之道》第七章专门讲错误处理,核心观点很明确:错误处理不是边角料,而是代码健壮性的核心,必须认真设计。糟糕的错误处理会让代码混乱不堪,而优雅的处理方式能让主逻辑清晰可读。
用异常代替返回码
以前写代码总爱用返回码表示错误,比如:
java
// 反例:返回码让调用者被迫处理错误
public int deletePage(String pageId) {
if (pageId == null) return -1;
if (!pageExists(pageId)) return -2;
// 执行删除逻辑
return 0;
}
// 调用者必须嵌套判断
int code = deletePage("123");
if (code == 0) {
// 处理成功
} else if (code == -1) {
log.error("pageId为空");
} else if (code == -2) {
log.error("页面不存在");
}
这种方式会导致调用者代码充满if-else
,主逻辑被错误处理淹没。改用异常后清爽很多:
java
// 正例:异常分离错误处理和主逻辑
public void deletePage(String pageId) throws InvalidPageIdException, PageNotFoundException {
if (pageId == null) throw new InvalidPageIdException("pageId不能为空");
if (!pageExists(pageId)) throw new PageNotFoundException("页面不存在: " + pageId);
// 执行删除逻辑
}
// 调用者集中处理异常
try {
deletePage("123");
// 处理成功逻辑
} catch (InvalidPageIdException e) {
log.error(e.getMessage());
} catch (PageNotFoundException e) {
log.error(e.getMessage());
}
异常的优势:错误处理代码从主逻辑中抽离,调用者可以选择立即处理或向上传递,灵活性更高。
先写Try-Catch-Finally,再填逻辑
作者建议"先写try-catch-finally
",这是个反直觉但有效的技巧。比如要实现一个读取文件并解析的功能:
java
// 先搭好异常处理框架
public String parseFile(String path) {
FileReader reader = null;
try {
// 后续填读取逻辑
} catch (FileNotFoundException e) {
log.error("文件不存在: " + path, e);
throw new ParsingException("解析失败", e);
} catch (IOException e) {
log.error("读取失败: " + path, e);
throw new ParsingException("解析失败", e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.warn("关闭文件失败", e);
}
}
}
}
先确定异常边界和资源释放方式,再填充核心逻辑,能避免遗漏错误处理。这本质是把"异常安全"作为前提,而不是事后补救。
别返回null,别传递null
返回null
是很多bug的根源,比如:
java
// 反例:返回null让调用者防不胜防
public List<User> getUsers() {
if (dbConnection == null) return null; // 潜在NPE风险
// 查询数据库
}
// 调用者忘记判空就会炸
List<User> users = getUsers();
users.forEach(u -> log.info(u.getName())); // NPE!
更安全的做法是返回空集合或抛出异常:
java
// 正例:返回空集合或抛异常
public List<User> getUsers() {
if (dbConnection == null) return Collections.emptyList(); // 空集合更安全
// 或者抛异常:throw new DbConnectionException("数据库连接未初始化");
// 查询数据库
}
同理,也别在参数中传递null
,可以用Objects.requireNonNull
提前校验:
java
public void createUser(User user) {
Objects.requireNonNull(user, "user不能为null");
Objects.requireNonNull(user.getName(), "用户名不能为null");
// 执行创建逻辑
}
null
的问题:它不携带任何信息,出了问题很难定位根源。用空集合、特殊值或异常,能传递更丰富的上下文。
异常处理的其他实践
-
异常要包含上下文 :别只抛
new Exception("错误")
,要说明"什么操作失败,原因是什么",比如new PaymentFailedException("订单123支付失败,余额不足")
。 -
使用 unchecked 异常:可控异常(checked exception)会导致接口僵化,一旦新增异常,所有调用者都得改。优先用非可控异常,让调用者自主决定是否处理。
-
错误处理也是"一件事" :
try
块里只放可能出错的核心逻辑,catch
块专注处理一种错误,别在里面塞额外逻辑。
代码对比:混乱 vs 整洁
混乱的错误处理:
java
public void processOrder(String orderId) {
if (orderId == null) {
System.out.println("订单ID为空");
return;
}
Order order = orderDao.getById(orderId);
if (order == null) {
System.out.println("订单不存在");
return;
}
if (!order.isPaid()) {
System.out.println("订单未支付");
return;
}
// 处理订单逻辑
}
整洁的错误处理:
java
public void processOrder(String orderId) {
try {
validateOrderId(orderId);
Order order = getValidatedOrder(orderId);
// 处理订单逻辑
} catch (InvalidOrderIdException | OrderNotFoundException | UnpaidOrderException e) {
log.error("处理订单失败: " + orderId, e);
}
}
private void validateOrderId(String orderId) {
if (orderId == null || orderId.isEmpty()) {
throw new InvalidOrderIdException("订单ID不能为空");
}
}
private Order getValidatedOrder(String orderId) {
Order order = orderDao.getById(orderId);
if (order == null) throw new OrderNotFoundException("订单不存在: " + orderId);
if (!order.isPaid()) throw new UnpaidOrderException("订单未支付: " + orderId);
return order;
}
核心差异:整洁的代码把错误判断和主逻辑分离,通过异常传递错误信息,调用者能快速定位问题,主流程一目了然。
错误处理的终极目标是:让正常流程的代码像"没有错误可能"一样简洁,而错误情况的处理又足够清晰,能快速排查问题 。这需要把异常当"一等公民"来设计,而不是随便加个try-catch
应付了事。