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()
方法的控制流图如下:
可能的执行路径:
- 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;
}
NullPointerException
被 finally
中的 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();
}
但过度使用空检查会降低代码可读性。推荐替代方案:
6. 总结
本文介绍了静态分析工具检测的关键代码缺陷及基本修复方案。这些工具能帮助开发者提前发现潜在问题,显著提升代码质量。
完整规则参考: