检查参数的有效性 《Effective Java》第八章 第49条

《Effective Java》第八章 第49条:检查参数的有效性

一、核心思想与重要性

核心思想:方法或构造器在开始执行前,应该对其参数的有效性进行检查。如果参数无效,应该立即抛出适当的异常。

重要性

  1. 防御性编程:防止错误在系统中传播,避免在后续执行中产生难以追踪的问题
  2. 快速失败(Fail-Fast):在错误发生的第一时间就暴露出来,便于调试和定位问题
  3. 契约明确:明确方法的前置条件(preconditions),形成清晰的API契约

二、基本检查方式与示例

1. 简单的参数检查

原始示例分析:

/**
 * 返回一个正整数n的模平方根
 * 
 * @param n 正整数
 * @return 模平方根
 * @throws ArithmeticException 如果n <= 0
 */
public static BigInteger modSqrt(BigInteger n) {
    if (n.signum() <= 0)
        throw new ArithmeticException("n必须是正数: " + n);
    // ... 实际计算逻辑
}

深入解析:

  • signum()方法:BigInteger的signum()返回-1(负数)、0(零)、1(正数)
  • 检查时机:在方法的最开始进行检查,确保后续计算基于有效参数
  • 异常选择:使用ArithmeticException,因为这是数学计算相关的参数错误

对比示范(错误的做法):

// 错误的做法 - 不进行检查
public static BigInteger badModSqrt(BigInteger n) {
    // 直接开始计算,如果n为负数,可能:
    // 1. 抛出令人困惑的异常
    // 2. 返回错误的结果
    // 3. 进入无限循环
    return n.sqrt();
}

2. 对象引用检查

原始示例分析:

/**
 * 根据策略和定时器执行一个动作
 * 
 * @param strategy 策略对象,不能为null
 * @param timeout 超时时间,必须为正数
 * @throws NullPointerException 如果strategy为null
 * @throws IllegalArgumentException 如果timeout <= 0
 */
public void execute(Strategy strategy, long timeout) {
    // Objects.requireNonNull是Java 7引入的工具方法
    Objects.requireNonNull(strategy, "strategy不能为null");
    
    if (timeout <= 0)
        throw new IllegalArgumentException("timeout必须是正数: " + timeout);
    
    // ... 执行逻辑
}

深入解析:

  • Objects.requireNonNull():这是Java 7引入的便捷方法,内部实现为:

    public static <T> T requireNonNull(T obj, String message) {
        if (obj == null)
            throw new NullPointerException(message);
        return obj;
    }
    
  • 返回值:该方法返回传入的对象,可以用于内联赋值

  • 异常信息:提供清晰的错误信息,帮助调用者快速定位问题

使用示范:

// 内联使用方式
public class Processor {
    private final Strategy strategy;
    
    public Processor(Strategy strategy) {
        // 在构造器中检查并赋值
        this.strategy = Objects.requireNonNull(strategy, "strategy不能为null");
    }
}

三、文档化的重要性

1. Javadoc文档化示例

/**
 * 计算二项式系数 C(n, k)
 * 
 * @param n 总元素数量,必须满足 n >= 0
 * @param k 选择元素数量,必须满足 0 <= k <= n
 * @return 二项式系数 C(n, k)
 * @throws IllegalArgumentException 如果n < 0, k < 0, 或k > n
 */
public static BigInteger binomial(int n, int k) {
    // 参数有效性检查
    if (n < 0)
        throw new IllegalArgumentException("n不能为负数: " + n);
    if (k < 0 || k > n)
        throw new IllegalArgumentException("k必须满足 0 <= k <= n: k=" + k + ", n=" + n);
    
    // 计算逻辑
    BigInteger result = BigInteger.ONE;
    for (int i = 0; i < k; i++) {
        result = result.multiply(BigInteger.valueOf(n - i))
                      .divide(BigInteger.valueOf(i + 1));
    }
    return result;
}

2. 文档化的关键要素

必须包含的内容:

  • 每个参数的限制条件
  • 参数违反限制时抛出的异常类型
  • 异常抛出的具体情况

文档示例分析:

@param n 总元素数量,必须满足 n >= 0
@throws IllegalArgumentException 如果n < 0

这种文档化方式:

  1. 指导调用者:明确告知参数要求
  2. 便于维护:后续维护者了解方法的前提条件
  3. 工具支持:IDE可以根据文档提供提示

四、特殊情况与考量

1. 非公有方法的检查

私有方法示例:

private static void sort(long a[], int offset, int length) {
    // 断言(assert)用于私有方法的参数检查
    assert a != null;
    assert offset >= 0 && offset <= a.length;
    assert length >= 0 && length <= a.length - offset;
    
    // ... 排序逻辑
}

断言(assert)的特点:[补充:1]

  • 默认禁用:需要在JVM启动时添加-ea-enableassertions参数启用
  • 适用于私有方法:因为作者可以控制所有调用
  • 性能考虑:断言被禁用时不会产生性能开销

重要区别:

// 公有方法 - 使用异常
public void publicMethod(Object param) {
    if (param == null)
        throw new NullPointerException("param不能为null");
    // ...
}

// 私有方法 - 可以使用断言
private void privateMethod(Object param) {
    assert param != null : "param不能为null";
    // ...
}

2. 构造器参数检查的特殊性

构造器检查示例:

public class Period {
    private final Date start;
    private final Date end;
    
    /**
     * @param start 开始时间
     * @param end 结束时间,必须晚于或等于开始时间
     * @throws IllegalArgumentException 如果start在end之后
     * @throws NullPointerException 如果start或end为null
     */
    public Period(Date start, Date end) {
        // 防御性拷贝之前的检查
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        
        // 有效性检查(在防御性拷贝之后!)
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(start + "晚于" + end);
    }
}

关键要点:

  1. 检查时机:在防御性拷贝之后进行检查,确保检查的是实际存储的数据
  2. 不可变对象(final修饰):对于不可变对象,构造器是唯一设置状态的机会,必须严格检查

3. 检查的例外情况

不进行检查的情况:

// 情况1:检查开销极大,且检查在计算过程中自然进行
public void processLargeArray(int[] array) {
    // 不预先检查数组长度,在遍历过程中遇到问题再处理
    for (int i = 0; i < array.length; i++) {
        // 处理逻辑
    }
}

// 情况2:检查会破坏互操作性
// 例如:实现某个接口,该接口没有定义参数无效时的行为

有效性检查的成本考量:

// 需要权衡的例子:集合排序
public void sortList(List<Integer> list) {
    // 是否要检查list是否为null?
    // 是否要检查list中的元素是否为null?
    // 这取决于方法契约和性能要求
    
    // Collections.sort()的做法:让NPE自然发生
    Collections.sort(list);
}

五、最佳实践总结

1. 检查原则

必须检查的情况:

  • 公有方法、受保护方法、包级私有方法的参数
  • 构造器参数
  • 会存储起来供后续使用的参数

检查方式选择:

// 方式1:手动检查(最直接)
if (param == null) {
    throw new NullPointerException("参数不能为null");
}

// 方式2:使用工具类(更简洁)
Objects.requireNonNull(param, "参数不能为null");

// 方式3:使用断言(仅私有方法)
assert param != null;

// 方式4:第三方库(如Guava的Preconditions)
Preconditions.checkNotNull(param, "参数不能为null");
Preconditions.checkArgument(param > 0, "参数必须为正数: %s", param);

2. 异常选择指南

异常类型使用场景示例
NullPointerException参数为null,且null不是有效值Objects.requireNonNull(param)
IllegalArgumentException参数值不合法(非null)if (value < 0) throw new IllegalArgumentException(...)
IndexOutOfBoundsException索引参数越界`if (index < 0
ArithmeticException数学计算相关的参数错误if (divisor == 0)

3. 现代Java的增强

Java 8+ 的改进:

// 使用Objects新增的方法
public void process(String text, int count) {
    // 检查多个条件
    Objects.checkFromIndexSize(offset, length, array.length);
    Objects.checkIndex(index, size);
}

// 结合Optional(用于返回类型,而非参数)
public Optional<String> findNameById(long id) {
    Objects.requireNonNull(id);
    // ...
}

4. 综合示例

/**
 * 用户注册服务
 * 
 * @param username 用户名,不能为null或空,长度3-20字符
 * @param password 密码,不能为null,长度8-50字符
 * @param age 年龄,必须18-120岁
 * @throws NullPointerException 如果username或password为null
 * @throws IllegalArgumentException 如果参数值不合法
 */
public void registerUser(String username, String password, int age) {
    // 层次化检查
    Objects.requireNonNull(username, "用户名不能为null");
    Objects.requireNonNull(password, "密码不能为null");
    
    if (username.trim().isEmpty())
        throw new IllegalArgumentException("用户名不能为空");
    if (username.length() < 3 || username.length() > 20)
        throw new IllegalArgumentException("用户名长度必须为3-20字符");
    if (password.length() < 8 || password.length() > 50)
        throw new IllegalArgumentException("密码长度必须为8-50字符");
    if (age < 18 || age > 120)
        throw new IllegalArgumentException("年龄必须为18-120岁");
    
    // 业务逻辑
    validatePasswordComplexity(password);
    checkUsernameAvailability(username);
    
    // 创建用户
    User user = new User(username, password, age);
    userRepository.save(user);
}

六、作者思想要点总结

  1. 防御性编程是责任:作为API设计者,有责任防止无效参数破坏系统状态
  2. 快速失败原则:尽早发现问题,避免错误传播
  3. 文档与实现一致:文档中声明的限制必须在代码中强制执行
  4. 区分公有与非公有:公有API必须严格检查,私有方法可以适当放宽
  5. 性能权衡:在99%的情况下,参数检查的开销是值得的
  6. 异常也是API的一部分:抛出的异常类型和消息是API契约的重要组成部分

通过遵循这些原则,可以创建更健壮、更易于使用和维护的Java代码。参数有效性检查是高质量API设计的基石之一。

补充:

1、深入理解Java断言(assert)的优势与使用场景

一、断言的核心哲学:开发阶段的调试工具

1. 断言的本质定位

断言不是参数验证机制,而是内部不变性(internal invariants)的检查工具

// 错误的用法:将断言用于参数验证
public void processOrder(Order order) {
    assert order != null : "订单不能为null";  // ❌ 错误用法!
    // ...
}

// 正确的用法:检查内部不变性
private void calculateDiscount(double price) {
    // 经过一系列复杂计算后...
    double finalPrice = applyDiscount(price);
    
    // 断言检查:确保我们的算法没有产生不可能的结果
    assert finalPrice >= 0 : "最终价格不能为负数: " + finalPrice;
    assert finalPrice <= price : "折扣后价格不能超过原价: " + finalPrice;
    
    return finalPrice;
}

二、断言的实际优势

1. 性能优势:零开销发布

生产环境代码 vs 开发环境代码:

// 场景:图像处理库中的私有方法
private void transformImage(int[] pixels, int width, int height) {
    // 方式1:使用if检查(始终执行)
    if (pixels == null) throw new NullPointerException();
    if (width <= 0 || height <= 0) throw new IllegalArgumentException();
    if (pixels.length != width * height) throw new IllegalArgumentException();
    // 这些检查在生产环境中每次都会执行,可能影响性能
    
    // 方式2:使用断言(仅开发环境执行)
    assert pixels != null;
    assert width > 0 && height > 0 : "尺寸必须为正数";
    assert pixels.length == width * height : 
        "像素数组长度必须等于宽度×高度: " + pixels.length + " != " + (width * height);
    
    // 核心算法(在紧密循环中执行)
    for (int i = 0; i < pixels.length; i++) {
        // 复杂的图像变换算法
        pixels[i] = transformPixel(pixels[i]);
    }
}

性能对比:

  • 生产环境(断言禁用):断言代码被完全移除,零性能开销
  • 开发环境(断言启用):可以捕获算法实现的bug
2. 代码清晰性:分离业务逻辑与调试代码
// 不使用断言的情况
private void optimizeRoute(List<Point> waypoints) {
    // 参数检查(业务逻辑的一部分)
    Objects.requireNonNull(waypoints, "路径点列表不能为null");
    if (waypoints.size() < 2) {
        throw new IllegalArgumentException("至少需要两个路径点");
    }
    
    // 调试检查(与业务逻辑无关,只是验证实现正确性)
    if (!isSortedByDistance(waypoints)) {
        log.error("算法错误:路径点未按距离排序");
        // 但这里不能抛出异常,因为不是业务错误
    }
    
    // 核心算法...
}

// 使用断言的情况
private void optimizeRoute(List<Point> waypoints) {
    // 业务逻辑的参数检查
    Objects.requireNonNull(waypoints, "路径点列表不能为null");
    if (waypoints.size() < 2) {
        throw new IllegalArgumentException("至少需要两个路径点");
    }
    
    // 内部不变性检查(使用断言)
    assert isSortedByDistance(waypoints) : "路径点应该已按距离排序";
    assert isValidCoordinateRange(waypoints) : "坐标值超出有效范围";
    
    // 核心算法...
    List<Point> optimized = doOptimization(waypoints);
    
    // 后置条件检查
    assert optimized.size() <= waypoints.size() : 
        "优化后路径点不应增加: " + optimized.size() + " > " + waypoints.size();
}

三、断言与异常的关键区别

1. 目的不同
特性if检查 + 异常断言
目的验证外部输入的有效性验证内部逻辑的正确性
目标用户API调用者代码开发者
生产环境必须启用应该禁用
错误性质可预期的错误条件理论上不可能发生的情况

五、为什么断言默认禁用?

1. 设计哲学
// 假设这个断言被误用于参数验证
public void processData(String data) {
    assert data != null : "数据不能为null";  // ❌ 错误用法
    
    // 生产环境(断言禁用):
    // 如果调用者传入null,会直接导致NullPointerException
    // 但可能在很深的调用栈中,难以调试
    
    int length = data.length();  // 这里可能抛出NPE
    // ...
}

正确的做法:

public void processData(String data) {
    // 业务逻辑验证:使用异常
    Objects.requireNonNull(data, "数据不能为null");
    
    // 内部逻辑验证:可以使用断言
    assert data.length() > 0 : "数据不应该为空字符串";
    assert isValidFormat(data) : "数据格式不正确";
    
    // ...
}

六、 黄金法则

  1. 对外(public/protected方法):总是使用异常进行参数验证
  2. 对内(private方法)
    • 如果调用者是你控制的代码,可以使用断言
    • 但更安全的做法是:重要的验证仍用异常,内部不变性检查用断言
  3. 不变性检查(不变性:在程序执行过程中始终保持为真的条件或属性。):总是使用断言
    • 前置条件(假设调用前已满足的条件)
    • 后置条件(方法执行后必须成立的条件)
    • 类的不变条件(对象状态必须始终满足的条件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值