Contents

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()*方法关联的流程图是:

/uploads/code_quality_metrics/1.png

可能的执行路径是:

  • 0 => 3
  • 0 => 1 => 3
  • 0 => 2 => 3

从数学上讲,CC 可以使用以下简单公式计算:

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

2.2. 如何降低循环复杂度?

为了编写更简单的代码,开发人员可能倾向于使用不同的方法,具体取决于情况:

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:

  • **编码时避免使用关键字null *:*这条规则很简单,在初始化变量或返回值时避免使用关键字null
  • 使用@NotNull@Nullable 注解
  • 使用java.util.Optional
  • 实现空对象模式