8Effective Java(第3版)

异常

异常处理策略

受检异常非受检异常异常链标准异常

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第8章:异常(Exceptions)

一、导读

1.1 本章学习目标

第8章聚焦于 Java 异常处理的最佳实践。异常是 Java 中处理错误和异常情况的核心机制,正确使用异常可以提高代码的健壮性和可维护性。本章通过7条经验法则,帮助你掌握异常处理的精髓。

本章的核心学习目标包括:

  • 理解受检异常与非受检异常的区别:掌握何时使用哪种异常。
  • 学会正确使用异常:避免滥用异常控制流程。
  • 掌握异常链的使用:保留原始异常信息。
  • 理解标准异常的使用:优先使用 Java 平台提供的标准异常。
  • 学会编写自定义异常:为领域特定的错误创建有意义的异常。

1.2 为什么第八章如此重要

异常处理是程序健壮性的关键。错误的异常处理方式会导致代码难以理解、难以调试、性能下降。正确的异常处理方式则能让错误被及时发现、正确传播、清晰呈现。

1.3 核心问题引导

受检异常和非受检异常有什么区别?什么时候用哪种?

为什么不应该用异常控制流程?

什么是异常链?如何使用它?

Java 标准异常有哪些?分别适用于什么场景?

如何设计一个好的异常类?


二、核心概念详解

8.1 只针对异常的情况才使用异常(Item 69)

java
// 反模式:用异常控制流程
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 选择原则

java
// 受检异常:可恢复的情况
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)

受检异常要求调用者必须处理,这会增加代码的复杂度。如果调用者无法有意义地恢复,就不应该使用受检异常。

java
// 反模式:不必要的受检异常
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对象状态不允许操作
NullPointerExceptionnull 参数不允许
IndexOutOfBoundsException索引越界
ConcurrentModificationException并发修改
UnsupportedOperationException不支持的操作
java
// 使用标准异常
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)

java
// 反模式:泄露底层异常
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)

java
// 反模式:忽略异常
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 异常转译实战

java
// 领域异常
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 自定义异常体系

java
// 基础异常
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 实现统一的异常处理。


8/10异常
0%