第7章 错误处理
一、导读
1.1 本章学习目标
第7章讲解了错误处理的最佳实践。Uncle Bob 强调,错误处理很重要,但如果它掩盖了逻辑,那就错了。本章的核心学习目标包括:
- 掌握使用异常替代错误码:异常比错误码更清晰。
- 学会先写 try-catch-finally:从异常开始设计。
- 理解提供异常上下文:包含足够的调试信息。
- 掌握使用 unchecked 异常:减少代码噪音。
- 学会定义异常类:提供调用者需要的上下文。
- 掌握不返回 null,不传递 null:避免 NullPointerException。
1.2 为什么本章如此重要
错误处理是代码质量的关键指标。Uncle Bob 指出:错误处理很重要,但如果它掩盖了逻辑,那就错了。好的错误处理应该清晰、简洁,不影响主逻辑的可读性。
1.3 核心问题引导
你的代码用错误码还是异常处理错误?
异常信息是否包含足够的上下文?
你的方法返回 null 吗?这导致了什么问题?
如何设计异常类?
unchecked 异常和 checked 异常如何选择?
二、核心概念详解
2.1 使用异常替代错误码
2.1.1 错误码的问题
java
// 差:返回错误码
public class DeviceController {
public int sendShutdown(int deviceId) {
if (deviceId < 0) {
return -1; // 错误码:无效设备
}
if (!deviceExists(deviceId)) {
return -2; // 错误码:设备不存在
}
// 发送关闭命令
return 0; // 成功
}
}
// 调用者必须检查返回值
int result = controller.sendShutdown(1);
if (result == -1) {
// 处理无效设备
} else if (result == -2) {
// 处理设备不存在
}
// 容易忘记检查!2.1.2 异常的优势
java
// 好:使用异常
public class DeviceController {
public void sendShutdown(int deviceId) {
if (deviceId < 0) {
throw new IllegalArgumentException("Device ID must be positive");
}
if (!deviceExists(deviceId)) {
throw new DeviceNotFoundException("Device not found: " + deviceId);
}
// 发送关闭命令
}
}
// 调用者无法忽略异常
try {
controller.sendShutdown(1);
} catch (DeviceNotFoundException e) {
// 处理设备不存在
}2.2 先写 try-catch-finally
2.2.1 从异常开始设计
当你写可能抛出异常的代码时,先写 try-catch-finally,再写业务逻辑。
java
// 从异常开始设计
public class StorageService {
public void saveFile(String content, String fileName) {
try {
// 先写 try 块
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(content.getBytes());
fos.close();
} catch (FileNotFoundException e) {
// 再写 catch 块
logger.error("File not found: {}", fileName, e);
throw new StorageException("Cannot save file: " + fileName, e);
} catch (IOException e) {
logger.error("IO error saving file: {}", fileName, e);
throw new StorageException("IO error: " + fileName, e);
} finally {
// 最后写 finally 块
// 确保资源释放
}
}
}2.3 提供异常上下文
2.3.1 包含足够的信息
java
// 差:异常信息不足
try {
// ...
} catch (Exception e) {
throw new RuntimeException("Error"); // 什么错误?在哪里?
}
// 好:包含上下文
try {
processOrder(order);
} catch (PaymentException e) {
throw new OrderProcessingException(
"Failed to process order " + order.getId() +
" for customer " + order.getCustomerId() +
": " + e.getMessage(), e);
}2.4 使用 unchecked 异常
2.4.1 checked vs unchecked
java
// checked 异常:强制处理,但增加代码噪音
public void readFile(String path) throws IOException {
// 每个调用者都必须处理 IOException
}
// unchecked 异常:不强制处理,减少噪音
public void readFile(String path) {
try {
// 读取文件
} catch (IOException e) {
throw new UncheckedIOException("Cannot read file: " + path, e);
}
}2.5 不返回 null
2.5.1 null 的问题
java
// 差:返回 null
public List<Order> getOrders(CustomerId customerId) {
List<Order> orders = orderRepository.findByCustomerId(customerId);
if (orders == null) {
return null; // 调用者必须检查 null
}
return orders;
}
// 调用者
List<Order> orders = service.getOrders(customerId);
if (orders != null) { // 容易忘记检查!
for (Order order : orders) {
// ...
}
}
// 好:不返回 null
public List<Order> getOrders(CustomerId customerId) {
List<Order> orders = orderRepository.findByCustomerId(customerId);
return orders != null ? orders : Collections.emptyList();
}
// 调用者不需要检查 null
List<Order> orders = service.getOrders(customerId);
for (Order order : orders) { // 安全,空列表不会进入循环
// ...
}2.5.2 使用 Optional
java
// 使用 Optional 替代 null
public Optional<User> findUser(UserId userId) {
User user = userRepository.findById(userId);
return Optional.ofNullable(user);
}
// 调用者
Optional<User> user = service.findUser(userId);
user.ifPresent(u -> {
// 处理用户
});
// 或者
User user = service.findUser(userId)
.orElseThrow(() -> new UserNotFoundException(userId));三、实战案例
3.1 案例一:完整的错误处理
java
// 改善前:混乱的错误处理
public String processOrder(Order order) {
if (order == null) return "error: null order";
if (order.getItems().isEmpty()) return "error: no items";
try {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
// 保存
database.save(order);
return "success";
} catch (Exception e) {
return "error: " + e.getMessage();
}
}
// 改善后:清晰的错误处理
public class OrderService {
public OrderResult processOrder(Order order) {
validateOrder(order);
try {
Money total = calculateTotal(order);
order.setTotal(total);
orderRepository.save(order);
return OrderResult.success(order.getId());
} catch (DatabaseException e) {
logger.error("Failed to save order {}: {}",
order.getId(), e.getMessage(), e);
return OrderResult.systemError("Failed to save order");
}
}
private void validateOrder(Order order) {
if (order == null) {
throw new IllegalArgumentException("Order must not be null");
}
if (order.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have items");
}
}
}
// 结果对象
public class OrderResult {
private final boolean success;
private final OrderId orderId;
private final String errorMessage;
public static OrderResult success(OrderId orderId) {
return new OrderResult(true, orderId, null);
}
public static OrderResult systemError(String message) {
return new OrderResult(false, null, message);
}
}四、本章小结
第7章讲解了错误处理的最佳实践,核心要点包括:
使用异常替代错误码:异常更清晰,无法被忽略。
先写 try-catch-finally:从异常开始设计。
提供异常上下文:包含足够的调试信息。
使用 unchecked 异常:减少代码噪音。
不返回 null:使用空集合或 Optional。
不传递 null:在方法入口验证参数。
Uncle Bob 的核心观点是:错误处理应该清晰、简洁,不影响主逻辑的可读性。
五、思考与练习
思考题
你的代码用错误码还是异常?
异常信息是否包含足够的上下文?
你的方法返回 null 吗?如何改善?
编程练习
将一段使用错误码的代码改造为使用异常。
为一个方法添加完整的异常处理。
找到所有返回 null 的方法,使用 Optional 或空集合替代。