1. 概述

本文将重点介绍静态代码分析工具 FindBugs、PMD 和 CheckStyle 中检测的关键规则。这些工具能帮助开发者识别代码中的潜在问题,提升代码质量和可维护性。

2. 圈复杂度(Cyclomatic Complexity)

2.1 什么是圈复杂度?

代码复杂度是衡量代码质量的重要指标,但难以量化。PMD 在其代码大小规则中提供了完善的检测规则,专门识别方法大小和结构复杂度问题。

CheckStyle 虽以编码规范检查著称,但也能通过计算复杂度指标发现类/方法设计问题。

两个工具都支持的核心指标是圈复杂度(CC),它通过计算程序中独立执行路径的数量来评估复杂度。例如以下方法的圈复杂度为 3:

public void callInsurance(Vehicle vehicle) {
    if (vehicle.isValid()) {
        if (vehicle instanceof Car) {
            callCarInsurance();
        } else {
            delegateInsurance();
        }
    }
}

圈复杂度会考虑条件语句嵌套和布尔表达式组合。通常认为:

  • CC > 11 的代码属于高复杂度,难以测试和维护
  • CC 值直接影响测试难度:复杂度越高,实现有效测试越困难
  • CC 值等于实现 100% 分支覆盖所需的最少测试用例数

常见复杂度分级标准:

  • 1-4:低复杂度 – 易于测试
  • 5-7:中等复杂度 – 尚可接受
  • 8-10:高复杂度 – 建议重构以降低测试难度
  • 11+:极高复杂度 – 测试极其困难

callInsurance() 方法的控制流图如下:

flowgraph_cc-1

可能的执行路径:

  • 0 → 3
  • 0 → 1 → 3
  • 0 → 2 → 3

数学计算公式:

CC = E - N + 2P
  • E:边总数
  • N:节点总数
  • P:退出点总数

2.2 如何降低圈复杂度?

开发者可通过以下策略降低代码复杂度:

  • 用设计模式替代冗长的 switch 语句(如建造者模式、策略模式)
  • 遵循单一职责原则编写可复用方法
  • 遵守 PMD 的代码大小规则可间接降低 CC,例如:
    • 避免方法过长
    • 控制类字段数量
    • 限制方法参数数量

推荐实践原则:

3. 异常处理规则

异常处理缺陷很常见,但某些问题容易被低估,可能导致生产环境严重故障。PMD 和 FindBugs 提供了丰富的异常检测规则,以下是 Java 开发中需重点关注的规则。

3.1 禁止在 finally 块中抛出异常

finally{} 块通常用于资源释放,其他用途可能属于代码异味。典型错误模式:

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    throw new IOException();
}

此方法本应抛出 NullPointerException,但实际抛出 IOException,导致调用方处理错误的异常类型。

3.2 避免在 finally 块中使用 return

finally{} 中使用 return 会丢弃已抛出的异常:

String content = null;
try {
    String lowerCaseString = content.toLowerCase();
} finally {
    return;
}

NullPointerExceptionfinally 中的 return 语句静默丢弃,程序继续执行而不报错。

3.3 异常发生时确保流关闭

关闭流是 finally 块的典型用途,但实现不当可能导致资源泄漏:

OutputStream outStream = null;
OutputStream outStream2 = null;
try {
    outStream = new FileOutputStream("test1.txt");
    outStream2  = new FileOutputStream("test2.txt");
    outStream.write(bytes);
    outStream2.write(bytes);
} catch (IOException e) {
    e.printStackTrace();
} finally {
    try {
        outStream.close();
        outStream2.close(); // 若前一行抛异常,此行被跳过
    } catch (IOException e) {
        // 处理 IOException
    }
}

outStream.close() 抛异常,outStream2.close() 不会执行。改进方案:

finally {
    try {
        outStream.close();
    } catch (IOException e) {
        // 处理 IOException
    }
    try {
        outStream2.close();
    } catch (IOException e) {
        // 处理 IOException
    }
}

更优雅的解决方案:使用 Apache Commons 的 IOUtils.closeQuietly 方法,它能在不抛异常的情况下关闭流。

5. 不良实践

5.1 实现 compareTo() 但未重写 equals()

当实现 compareTo() 时,必须同步实现 equals(),否则会导致逻辑矛盾:

Car car = new Car();
Car car2 = new Car();
if(car.equals(car2)) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}
if(car.compareTo(car2) == 0) {
    logger.info("They're equal");
} else {
    logger.info("They're not equal");
}

输出结果:

They're not equal
They're equal

✅ 正确做法:重写 equals() 使其与 compareTo() 保持一致:

boolean equals(Object o) { 
    return compareTo(o) == 0; 
}

5.2 潜在的空指针解引用

NullPointerException(NPE)是 Java 最常见的异常。FindBugs 通过检测空指针解引用风险来预防 NPE。

基础错误示例:

Car car = null;
car.doSomething(); // 直接抛 NPE

⚠️ 常见防御方案:

Car car = null;
if (car != null) {
    car.doSomething();
}

但过度使用空检查会降低代码可读性。推荐替代方案:

  • **杜绝显式使用 null**:避免用 null 初始化变量或作为返回值
  • 使用 @NotNull@Nullable 注解
  • 采用 java.util.Optional
  • 实现空对象模式

6. 总结

本文介绍了静态分析工具检测的关键代码缺陷及基本修复方案。这些工具能帮助开发者提前发现潜在问题,显著提升代码质量。

完整规则参考:


« 上一篇: Java SHA-256哈希