Intro to Code Quality Rules with FindBugs and PMD – 使用FindBugs和PMD的代码质量规则介绍

最后修改: 2016年 11月 25日

1. Overview

1.概述

In this article, we will be highlighting some of the important rules featured in code analysis tools like FindBugs, PMD and CheckStyle.

在这篇文章中,我们将重点介绍FindBugs、PMD和CheckStyle等代码分析工具中的一些重要规则。

2. Cyclomatic Complexity

2.循环复杂度

2.1. What Is Cyclomatic Complexity?

2.1.什么是循环复杂度?

Code complexity is important, yet difficult metric to measure. PMD offers a solid set of rules under its Code Size Rules section, these rules are designed to detect violation regarding methods size and structure complexity.

代码的复杂性是很重要的,但也是很难衡量的指标。PMD在其代码大小规则部分提供了一套坚实的规则,这些规则旨在检测有关方法大小和结构复杂性的违规行为。

CheckStyle is known for its ability to analyse code against coding standards, and formatting rules. However, it can also detect problems in classes/methods design by calculating some complexity metrics.

CheckStyle以其对照编码标准和格式化规则分析代码的能力而闻名。然而,它也可以通过计算一些复杂性指标来检测类/方法设计中的问题。

One of the most relevant complexity measurement featured in both tools is the CC (Cyclomatic Complexity).

两个工具中最相关的复杂性测量之一是CC(Cyclomatic Complexity)。

CC value can be calculated by measuring the number of independent execution paths of a program.

CC值可以通过测量一个程序的独立执行路径的数量来计算。

For instance, the following method will yield a cyclomatic complexity of 3:

例如,以下方法将产生3的循环复杂性。

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

CC takes into account the nesting of conditional statements and multi-part boolean expressions.

CC考虑到了条件语句的嵌套和多部分布尔表达式。

Generally speaking, a code with a value higher than 11 in terms of CC, is considered very complex, and difficult de test and maintain.

一般来说,CC值高于11的代码被认为是非常复杂的,而且难以测试和维护。

Some common values used by static analysis tools are shown below:

静态分析工具使用的一些常用值如下所示。

  • 1-4: low complexity – easy to test
  • 5-7: moderate complexity – tolerable
  • 8-10: high complexity – refactoring should be considered to ease testing
  • 11 + very high complexity – very difficult to test

The complexity level also affects the testability of the code, the higher the CC, the higher the difficulty to implement pertinent tests. In fact, the cyclomatic complexity value shows exactly the number of test cases needed to achieve a 100% branches coverage score.

复杂度也会影响代码的可测试性,CC越高,实现相关测试的难度就越大。事实上,循环复杂度值正好显示了达到100%分支覆盖率所需的测试案例数量。

The flow graph associated with the callInsurance() method is:

callInsurance()方法相关的流程图是。

flowgraph_cc-1

The possible execution paths are:

可能的执行路径是。

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

Mathematically speaking, CC can be calculated using the following simple formula:

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

CC = E - N + 2P
  • E: Total number of edges
  • N: Total number of nodes
  • P: Total number of exit points

2.2. How to Reduce Cyclomatic Complexity ?

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

In order to write substantially less complex code, developers may tend to use different approaches, depending on the situation:

为了大大降低代码的复杂性,开发人员可能倾向于使用不同的方法,这取决于情况。

  • Avoid writing lengthy switch statements by using design patterns, e.g. the builder and strategy patterns may be good candidates to deal with code size and complexity issues
  • Write reusable and extensible methods by modularizing the code structure and implementing the Single Responsibility Principle
  • Following other PMD code size rules may have a direct impact on CC, e.g. excessive method length rule, too many fields in a single class, excessive parameters list in a single method…etc

You can also consider following principles and patterns regarding code size and complexity, e.g. the KISS (Keep It Simple and Stupid) principle, and DRY (Don’t Repeat Yourself).

你也可以考虑遵循关于代码大小和复杂性的原则和模式,例如KISS(保持简单和愚蠢)原则,以及DRY(不要重复自己)

3. Exception Handling Rules

3.异常处理规则

Defects related to exceptions might be usual, but some of them are hugely underestimated and should be corrected to avoid critical dysfunctioning in production code.

与异常有关的缺陷可能是常见的,但其中一些缺陷被大大低估了,应该加以纠正,以避免生产代码中出现严重的功能障碍。

PMD and FindBugs offer both a handful set of rules regarding exceptions. Here’s our pick of what may be considered critical in a Java program when handling exceptions.

PMD和FindBugs都提供了一套关于异常的少量规则。下面是我们挑选的在Java程序中处理异常时可能被认为是关键的内容。

3.1. Do Not Throw Exception in Finally

3.1.不要在Final中抛出异常

As you may already know, the finally{} block in Java is generally used for closing files and releasing resources, using it for other purposes might be considered as a code smell.

你可能已经知道,Java中的finally{}块一般用于关闭文件和释放资源,将其用于其他用途可能被视为代码气味

A typical error-prone routine is throwing an exception inside the finally{} block:

一个典型的容易出错的例程是在finally{}块内抛出一个异常。

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

This method is supposed to throw a NullPointerException, but surprisingly it throws an IOException, which may mislead the calling method to handle the wrong exception.

这个方法应该抛出一个NullPointerException,但令人惊讶的是它抛出了一个IOException,这可能会误导调用方法处理错误的异常。

3.2. Returning in the finally Block

3.2.在finally块中返回

Using the return statement inside a finally{} block may be nothing but confusing. The reason why this rule is so important, it’s because whenever a code throws an exception, it gets discarded by the return statement.

finally{}块内使用return语句可能只会让人困惑。这条规则之所以如此重要,是因为每当代码抛出一个异常,它就会被return语句抛弃。

For example, the following code runs with no errors whatsoever:

例如,以下代码的运行没有任何错误。

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

A NullPointerException was not caught, yet, still discarded by the return statement in the finally block.

一个NullPointerException没有被捕获,但仍然被finally块中的返回语句抛弃。

3.3. Failing to Close Stream on Exception

3.3.异常情况下无法关闭流

Closing streams is one of the main reasons why we use a finally block, but it’s not a trivial task as it seems to be.

关闭流是我们使用finally块的主要原因之一,但这并不是一个看似简单的任务。

The following code tries to close two streams in a finally block:

下面的代码试图在一个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
    }
}

If the outStream.close() instruction throws an IOException, the outStream2.close() will be skipped.

如果outStream.close()指令抛出一个IOExceptionoutStream2.close()将被跳过。

A quick fix would be to use a separate try/catch block to close the second stream:

一个快速的解决方法是使用一个单独的try/catch块来关闭第二个流。

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

If you want a nice way to avoid consecutive try/catch blocks, check the IOUtils.closeQuiety method from Apache commons, it makes it simple to handle streams closing without throwing an IOException.

如果你想要一个好的方法来避免连续的try/catch块,请查看来自Apache commons的IOUtils.closeQuiety方法,它使处理流的关闭变得简单而不抛出一个IOException

5. Bad Practices

5.不良做法

5.1. Class Defines compareto() and Uses Object.equals()

5.1.类定义了compareto()并使用了Object.equals()

Whenever you implement the compareTo() method, don’t forget to do the same with the equals() method, otherwise, the results returned by this code may be confusing:

每当你实现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");
}

Result:

结果。

They're not equal
They're equal

To clear confusions, it is recommended to make sure that Object.equals() is never called when implementing Comparable, instead, you should try to override it with something like this:

为了消除混淆,建议在实现Comparable时,确保永远不要调用Object.equals()相反,你应该尝试用类似这样的方法来覆盖它。

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

5.2. Possible Null Pointer Dereference

5.2.可能的空指针解读

NullPointerException (NPE) is considered the most encountered Exception in Java programming, and FindBugs complains about Null PointeD dereference to avoid throwing it.

NullPointerException(NPE)被认为是Java编程中遇到的最多的Exception,FindBugs抱怨Null PointeD的脱指,以避免抛出它。

Here’s the most basic example of throwing an NPE:

下面是抛出NPE的最基本例子。

Car car = null;
car.doSomething();

The easiest way to avoid NPEs is to perform a null check:

避免NPE的最简单方法是进行空值检查。

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

Null checks may avoid NPEs, but when used extensively, they certainly affect code readability.

空值检查可能会避免NPE,但当广泛使用时,它们肯定会影响代码的可读性。

So here’s some technics used to avoid NPEs without null checks:

因此,这里有一些用于避免NPE的技术,没有空检查。

6. Conclusion

6.结论

In this article, we have done an overall look on some of the critical defects detected by static analysis tools, with basic guidelines to address appropriately the detected issues.

在这篇文章中,我们对静态分析工具检测到的一些关键缺陷进行了全面的分析,并提出了适当解决检测到的问题的基本准则。

You can browse the full set of rules for each one of them by visiting the following links: FindBugs, PMD.

你可以通过访问以下链接来浏览每个人的全套规则。FindBugs, PMD