FindBugs 和 PMD 的代码质量规则简介
1. 概述
在本文中,我们将重点介绍 FindBugs、PMD 和 CheckStyle 等代码分析工具中的一些重要规则。
2. 循环复杂度
2.1. 什么是循环复杂度?
代码复杂性很重要,但很难衡量。PMD 在其代码大小规则部分提供了一套可靠的规则,这些规则旨在检测有关方法大小和结构复杂性的违规行为。
CheckStyle 以其根据编码标准和格式规则分析代码的能力而闻名。但是,它也可以通过计算一些复杂度指标 来检测类/方法设计中的问题。
两种工具中最相关的复杂度测量之一是 CC(循环复杂度)。
CC值可以通过测量程序独立执行路径的数量来计算。
例如,以下方法将产生 3 的代码复杂度:
public void callInsurance(Vehicle vehicle) {
if (vehicle.isValid()) {
if (vehicle instanceof Car) {
callCarInsurance();
} else {
delegateInsurance();
}
}
}
CC 考虑了条件语句和多部分布尔表达式的嵌套。
一般来说,CC值大于11的代码被认为是非常复杂的,并且难以测试和维护。
静态分析工具使用的一些常用值如下所示:
- 1-4:低复杂度——易于测试
- 5-7:中等复杂度——可以忍受
- 8-10:高复杂度——应该考虑重构以简化测试
- 11 + 非常高的复杂性——很难测试
复杂程度也会影响代码的可测试性,CC越高,实现相关测试的难度就越高。事实上,循环复杂度值准确地显示了达到 100% 分支覆盖率分数所需的测试用例数量。
与*callInsurance()*方法关联的流程图是:
可能的执行路径是:
- 0 => 3
- 0 => 1 => 3
- 0 => 2 => 3
从数学上讲,CC 可以使用以下简单公式计算:
CC = E - N + 2P
- E:边的总数
- N:节点总数
- P:出口点总数
2.2. 如何降低循环复杂度?
为了编写更简单的代码,开发人员可能倾向于使用不同的方法,具体取决于情况:
- 通过使用设计模式避免编写冗长的switch语句,例如构建器和策略模式可能是处理代码大小和复杂性问题的良好候选者
- 通过模块化代码结构和实现单一职责原则 来编写可重用和可扩展的方法
- 遵循其他 PMD代码大小 规则可能会对 CC 产生直接影响,例如方法长度过多的规则、单个类中的字段过多、单个方法中的参数列表过多……等 您还可以考虑遵循有关代码大小和复杂性的原则和模式,例如KISS(保持简单和愚蠢)原则 和**DRY(不要重复自己) **。
3. 异常处理规则
与异常相关的缺陷可能很常见,但其中一些被大大低估了,应该予以纠正以避免生产代码中出现严重的功能障碍。 PMD 和 FindBugs 都提供了一些关于异常的规则。下面是我们挑选的在处理异常时在 Java 程序中可能被视为关键的内容。
3.1. 最后不要抛出异常
您可能已经知道,Java 中的*finally{}*块通常用于关闭文件和释放资源,将其用于其他目的可能会被视为代码异味 。
一个典型的容易出错的例程是在*finally{}*块内抛出异常:
String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
throw new IOException();
}
这个方法应该抛出一个NullPointerException,但令人惊讶的是它抛出了一个IOException,这可能会误导调用方法来处理错误的异常。
3.2. 在finally块中返回
在finally{}块中使用 return 语句可能只会令人困惑。这条规则之所以如此重要,是因为每当代码抛出异常时,它都会被return语句丢弃。
例如,以下代码运行时没有任何错误:
String content = null;
try {
String lowerCaseString = content.toLowerCase();
} finally {
return;
}
NullPointerException未被捕获,但仍被finally块中的return 语句丢弃。
3.3. 异常关闭流失败
关闭流是我们使用finally块的主要原因之一,但它看起来并不是一项微不足道的任务。
以下代码尝试在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) {
// Handling IOException
}
}
如果outStream.close()指令抛出IOException,则*outStream2.close()*将被跳过。
一个快速的解决方法是使用单独的 try/catch 块来关闭第二个流:
finally {
try {
outStream.close();
} catch (IOException e) {
// Handling IOException
}
try {
outStream2.close();
} catch (IOException e) {
// Handling IOException
}
}
如果您想要一种避免连续try/catch块的好方法,请检查 Apache commons 中的IOUtils.closeQuiety 方法,它使处理流关闭而不抛出IOException变得简单。
5. 不良做法
5.1. 类定义 compareto() 并使用 Object.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
为了消除混淆,建议确保在实现Comparable时永远不会调用Object.equals() ,相反,您应该尝试使用以下内容覆盖它:
boolean equals(Object o) {
return compareTo(o) == 0;
}
5.2. 可能的空指针取消引用
NullPointerException (NPE) 被认为是 Java 编程中遇到最多的异常,FindBugs 抱怨 Null PointeD 取消引用以避免抛出它。
这是抛出 NPE 的最基本示例:
Car car = null;
car.doSomething();
避免 NPE 的最简单方法是执行空检查:
Car car = null;
if (car != null) {
car.doSomething();
}
Null 检查可以避免 NPE,但是当广泛使用时,它们肯定会影响代码的可读性。
所以这里有一些技术可以用来避免没有空检查的 NPE: