第8章:异常(Exceptions)
一、导读
1.1 本章学习目标
第8章聚焦于 Java 异常处理的最佳实践。异常是 Java 中处理错误和异常情况的核心机制,正确使用异常可以提高代码的健壮性和可维护性。本章通过7条经验法则,帮助你掌握异常处理的精髓。
本章的核心学习目标包括:
- 理解受检异常与非受检异常的区别:掌握何时使用哪种异常。
- 学会正确使用异常:避免滥用异常控制流程。
- 掌握异常链的使用:保留原始异常信息。
- 理解标准异常的使用:优先使用 Java 平台提供的标准异常。
- 学会编写自定义异常:为领域特定的错误创建有意义的异常。
1.2 为什么第八章如此重要
异常处理是程序健壮性的关键。错误的异常处理方式会导致代码难以理解、难以调试、性能下降。正确的异常处理方式则能让错误被及时发现、正确传播、清晰呈现。
1.3 核心问题引导
受检异常和非受检异常有什么区别?什么时候用哪种?
为什么不应该用异常控制流程?
什么是异常链?如何使用它?
Java 标准异常有哪些?分别适用于什么场景?
如何设计一个好的异常类?
二、核心概念详解
8.1 只针对异常的情况才使用异常(Item 69)
// 反模式:用异常控制流程
Iterator<Integer> it = list.iterator();
try {
while (true) {
int value = it.next();
process(value);
}
} catch (NoSuchElementException e) {
// 用异常来判断循环结束
}
// 正确做法:使用标准模式
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
int value = it.next();
process(value);
}异常的设计目的是处理异常情况,不应该用于正常的控制流程。原因:
性能差:异常创建时需要捕获栈的快照,开销很大。
代码不清晰:用异常控制流程的代码难以阅读。
可能掩盖 bug:如果异常被意外捕获,真正的错误可能被忽略。
8.2 对可恢复的情况使用受检异常,对编程错误使用非受检异常(Item 70)
8.2.1 三种可抛出类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 受检异常(Checked Exception) | 调用者必须处理 | IOException、SQLException |
| 非受检异常-RuntimeException | 编程错误 | NullPointerException、IllegalArgumentException |
| 错误(Error) | JVM 层面的问题 | OutOfMemoryError、StackOverflowError |
8.2.2 选择原则
// 受检异常:可恢复的情况
public String readConfig(String path) throws IOException {
// 文件可能不存在,调用者可以决定如何处理
return Files.readString(Path.of(path));
}
// 非受检异常:编程错误
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Invalid age: " + age);
}
this.age = age;
}8.3 避免不必要地使用受检异常(Item 71)
受检异常要求调用者必须处理,这会增加代码的复杂度。如果调用者无法有意义地恢复,就不应该使用受检异常。
// 反模式:不必要的受检异常
public void process(String data) throws ProcessException {
// 如果 ProcessException 是受检异常,调用者必须 try-catch
// 但调用者可能无法恢复
}
// 正确做法:使用非受检异常
public void process(String data) {
try {
// 处理逻辑
} catch (Exception e) {
throw new RuntimeException("Processing failed", e);
}
}8.4 尽量使用标准异常(Item 73)
Java 平台提供了一组标准异常,优先使用它们:
| 异常 | 使用场景 |
|---|---|
IllegalArgumentException | 参数值不合法 |
IllegalStateException | 对象状态不允许操作 |
NullPointerException | null 参数不允许 |
IndexOutOfBoundsException | 索引越界 |
ConcurrentModificationException | 并发修改 |
UnsupportedOperationException | 不支持的操作 |
// 使用标准异常
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("age out of range: " + age);
}
}
public void addElement(Object element) {
if (element == null) {
throw new NullPointerException("element must not be null");
}
}8.5 抛出与抽象相对应的异常(Item 74)
// 反模式:泄露底层异常
public class DatabaseService {
public User findUser(String id) throws SQLException {
// 直接抛出 SQLException,暴露了底层实现
return jdbcTemplate.queryForObject("SELECT ...", User.class, id);
}
}
// 正确做法:异常转译
public class DatabaseService {
public User findUser(String id) {
try {
return jdbcTemplate.queryForObject("SELECT ...", User.class, id);
} catch (SQLException e) {
throw new DataAccessException("Failed to find user: " + id, e);
}
}
}
// 更好的做法:异常链
public class DataAccessException extends RuntimeException {
public DataAccessException(String message, Throwable cause) {
super(message, cause); // 保留原始异常
}
}8.6 不要忽略异常(Item 77)
// 反模式:忽略异常
try {
// ...
} catch (IOException e) {
// 空的 catch 块!
}
// 反模式:只打印堆栈
try {
// ...
} catch (IOException e) {
e.printStackTrace(); // 没有真正处理异常
}
// 正确做法:至少记录异常信息
try {
// ...
} catch (IOException e) {
logger.error("Failed to read file", e);
throw new UncheckedIOException(e);
}
// 或者重新抛出
try {
// ...
} catch (IOException e) {
throw new RuntimeException("Failed to read file", e);
}三、实战案例
8.1 异常转译实战
// 领域异常
public class OrderNotFoundException extends RuntimeException {
private final String orderId;
public OrderNotFoundException(String orderId) {
super("Order not found: " + orderId);
this.orderId = orderId;
}
public String getOrderId() { return orderId; }
}
public class PaymentFailedException extends RuntimeException {
public PaymentFailedException(String message, Throwable cause) {
super(message, cause);
}
}
// 服务层:异常转译
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public Order findOrder(String orderId) {
try {
return orderRepository.findById(orderId);
} catch (SQLException e) {
throw new DataAccessException("Database error", e);
} catch (EntityNotFoundException e) {
throw new OrderNotFoundException(orderId);
}
}
public void processPayment(String orderId, BigDecimal amount) {
try {
paymentGateway.charge(orderId, amount);
} catch (PaymentGatewayException e) {
throw new PaymentFailedException(
"Payment failed for order: " + orderId, e);
}
}
}
// 全局异常处理(Spring)
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ErrorResponse> handleOrderNotFound(OrderNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("ORDER_NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(PaymentFailedException.class)
public ResponseEntity<ErrorResponse> handlePaymentFailed(PaymentFailedException e) {
return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
.body(new ErrorResponse("PAYMENT_FAILED", e.getMessage()));
}
}8.2 自定义异常体系
// 基础异常
public abstract class BusinessException extends RuntimeException {
private final String errorCode;
private final Map<String, Object> context;
protected BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
protected BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public String getErrorCode() { return errorCode; }
public Map<String, Object> getContext() { return Collections.unmodifiableMap(context); }
public BusinessException withContext(String key, Object value) {
context.put(key, value);
return this;
}
}
// 具体异常
public class ValidationException extends BusinessException {
private final List<FieldError> fieldErrors;
public ValidationException(List<FieldError> fieldErrors) {
super("VALIDATION_ERROR", "Validation failed");
this.fieldErrors = fieldErrors;
}
public List<FieldError> getFieldErrors() { return fieldErrors; }
}
public class AuthenticationException extends BusinessException {
public AuthenticationException(String message) {
super("AUTH_ERROR", message);
}
}
public class AuthorizationException extends BusinessException {
public AuthorizationException(String message) {
super("FORBIDDEN", message);
}
}
// 使用
throw new ValidationException(List.of(
new FieldError("email", "Invalid email format"),
new FieldError("age", "Age must be positive")
));四、本章小结
第8章系统讲解了异常处理的核心知识:
只针对异常情况使用异常:不要用异常控制正常流程。
受检异常用于可恢复情况:非受检异常用于编程错误。
避免不必要的受检异常:如果调用者无法恢复,使用非受检异常。
优先使用标准异常:IllegalArgumentException、IllegalStateException 等。
异常转译:将底层异常转译为高层异常,使用异常链保留原始信息。
不要忽略异常:至少记录异常信息。
好的异常处理让错误无处藏身,差的异常处理让 bug 潜伏更深。
五、思考与练习
思考题
受检异常的主要缺点是什么?Java 9+ 对此有什么变化?
异常链是如何工作的?为什么需要它?
如何设计一个好的异常类?需要考虑哪些因素?
全局异常处理有什么好处?
在什么场景下应该使用 Error?
编程练习
实现异常转译:将一个抛出 SQLException 的方法改为抛出领域异常。
设计异常体系:为一个电商系统设计一套完整的异常类层次结构。
实现全局异常处理:使用 Spring 的 @ControllerAdvice 实现统一的异常处理。