1. 概述

工具类(Utility classes)只包含静态成员,这些成员围绕特定主题组织在一起。因此,工具类本身是无状态的,而其成员包含可在多个层中重用的代码。

本文将解释为什么静态代码分析工具会报告工具类不应有公共构造函数。我们将通过实现私有构造函数来解决这个问题。此外,我们还将探讨哪些Lombok注解可以帮助我们生成私有构造函数,以及如何禁用这些警告。

最后,我们将评估在Java中实现工具类的一些替代方法。

2. 工具类

与定义对象的类不同,工具类不保存任何数据或状态,它们只包含行为。工具类只包含静态成员。它们的所有方法都是静态的,而数据仅作为方法参数传递。

2.1. 为什么需要工具类?

在面向对象编程中,我们旨在对问题域建模,并将相似功能族组合在一起。

我们也可以选择编写纯函数来建模代码库中的通用行为,尤其是在使用函数式编程时。与对象方法不同,这些纯函数与任何对象的实例无关。然而,它们需要一个"家"。Java没有专门用于容纳一组函数的类型,因此我们常常创建一个工具类。

Java中流行的工具类示例有来自java.utilArraysCollections,以及来自org.apache.commons.lang3StringUtils

2.2. Java中的实现

Java没有提供创建工具类的特殊关键字或方式。因此,我们通常将工具类创建为一个普通的Java类,但只包含静态成员

public final class StringUtils {

    public static boolean isEmpty(String source) {
        return source == null || source.length() == 0;
    }

    public static String wrap(String source, String wrapWith) {
        return isEmpty(source) ? source : wrapWith + source + wrapWith;
    }
}

在这个例子中,我们将工具类标记为publicfinal。工具类通常设为公共的,因为它们旨在跨多个层重用。

final关键字防止子类化。由于工具类不是为继承而设计的,我们不应该对它们进行子类化。

2.3. 公共构造函数警告

让我们尝试使用流行的静态代码分析工具SonarQube来分析我们的示例工具类。我们可以使用构建工具插件(这里用Maven)在Java项目上运行SonarQube分析:

mvn clean verify sonar:sonar -Dsonar.host.url=http://localhost:9000 -Dsonar.login=admin123

静态代码分析结果是一个主要的代码异味。SonarQube警告我们隐藏工具类中的隐式公共构造函数

sonar public constructor 3-1

尽管我们没有在工具类中添加构造函数,但Java隐式添加了一个默认的公共构造函数。因此,允许API用户创建它的实例:

StringUtils utils = new StringUtils();

这是对我们工具类的误用,因为它不是为实例化而设计的。因此,SonarQube规则建议我们添加一个私有构造函数来隐藏默认的公共构造函数。

3. 添加私有构造函数

现在让我们通过在工具类中添加一个私有构造函数来解决报告的代码异味问题。

3.1. 默认私有构造函数

让我们在工具类中添加一个无参数的私有构造函数。我们永远不会真正使用这个私有构造函数。因此,如果它被调用,抛出异常是一个好习惯:

public final class StringUtils {

    private StringUtils() {
        throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");
    }
  
    // public static methods
}

我们应该注意,私有构造函数也无法被测试。因此,这种方法将在我们的代码覆盖率测量中导致一行未覆盖的代码。

3.2. 使用Lombok的NoArgsConstructor

我们可以利用NoArgsConstructor Lombok注解来自动生成私有构造函数**:

@NoArgsConstructor(access= AccessLevel.PRIVATE)
public final class StringUtils {

    // public static methods
}

这样,我们可以避免手动添加一行未覆盖的代码。

3.3. 使用Lombok的UtilityClass

我们也可以使用UtilityClass Lombok注解,它将整个类标记为工具类

@UtilityClass
public class StringUtils {

    // public static methods
}

在这种情况下,Lombok将自动:

  • 生成一个抛出异常的私有构造函数
  • 将我们添加的任何显式构造函数标记为错误
  • 将类标记为final

我们应该注意,目前UtilityClass注解仍是一个实验性功能。

4. 禁用警告

如果我们决定不遵循推荐的解决方案,我们也可以选择禁用公共构造函数警告。

4.1. 抑制警告

让我们利用Java的SuppressWarnings注解来在单个类级别禁用警告

@SuppressWarnings("java:S1118")
public final class StringUtils {

    // public static methods
}

我们应该传递正确的SonarQube规则ID作为值参数。我们可以在SonarQube服务器UI中找到它:

sonar rule id 2

4.2. 停用规则

在SonarQube开箱即用的质量配置文件中,我们无法停用任何预定义的规则。因此,为了在整个项目级别禁用警告,我们首先需要创建一个自定义质量配置文件:

sonar deactivate rule 2

在我们的自定义质量配置文件中,我们可以搜索并停用任何预定义的Java规则。

5. 替代实现

让我们看看除了使用类之外,创建工具类的一些可能的替代方法。

5.1. 静态接口方法

自Java 8以来,我们可以在接口中**定义和实现静态方法**:

public interface StringUtils {

    static boolean isEmpty(String source) {
        return source == null || source.length() == 0;
    }

    static String wrap(String source, String wrapWith) {
        return isEmpty(source) ? source : wrapWith + source + wrapWith;
    }
}

由于我们不能实例化接口,我们消除了工具类实例化的问题。然而,我们正在制造另一个问题。由于接口旨在由其他类实现,API用户可能会错误地实现这个接口。

此外,接口不能包含私有常量和静态初始化块。

5.2. 静态枚举方法

枚举是托管实例的容器。然而,我们可以创建一个只包含静态方法的零实例枚举作为工具类:

public enum StringUtils {;

    public static boolean isEmpty(String source) {
        return source == null || source.length() == 0;
    }

    public static String wrap(String source, String wrapWith) {
        return isEmpty(source) ? source : wrapWith + source + wrapWith;
    }
}

由于我们不能实例化枚举类型,我们消除了工具类实例化的问题。另一方面,顾名思义,枚举类型是为创建实际枚举而设计的,而不是工具类。

6. 结论

在本文中,我们探讨了工具类并解释了为什么它们不应该有公共构造函数

在示例中,我们涵盖了手动实现私有构造函数以及使用Lombok注解。接下来,我们看到了如何抑制和禁用相关的SonarQube警告。最后,我们研究了使用接口和枚举创建工具类的两种替代方法。

与往常一样,源代码可在GitHub上获取。


原始标题:Solving the Hide Utility Class Public Constructor Sonar Warning