如何定义好静态工具类


前言

静态工具类是一种特殊的类,它通常包含一系列静态方法,这些方法用于执行特定的任务或者提供一些通用功能。这类工具类不需要被实例化,可以直接通过类名调用其静态方法。静态工具类的优点包括提高了代码的复用性、简化了调用方式,并且有助于保持代码的整洁和模块化,从而增强程序的可读性和可维护性。在Java中,常见的静态工具类如Arrays、Collections、Objects等,它们提供了对数组、集合、对象等进行操作的便捷方法。


一、常规StringUtils静态工具类

下面展示的是一个相对完整,规范的静态工具类

package com.grafana.log;

public final class StringUtils {

    // 私有构造器,防止外部实例化
    private StringUtils() {
        throw new AssertionError("禁止实例化");
    }

    /**
     * 检查给定的字符串是否为空 (null 或者长度为 0)。
     *
     * @param str 要检查的字符串
     * @return 如果字符串为空返回 true,否则返回 false
     */
    public static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }

    /**
     * 检查给定的字符串是否非空。
     *
     * @param str 要检查的字符串
     * @return 如果字符串非空返回 true,否则返回 false
     */
    public static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    /**
     * 将给定的字符串转换为小写。
     *
     * @param str 要转换的字符串
     * @return 转换后的小写字符串
     */
    public static String toLowerCase(String str) {
        if (isNotEmpty(str)) {
            return str.toLowerCase();
        }
        return str;
    }

    /**
     * 将给定的字符串转换为大写。
     *
     * @param str 要转换的字符串
     * @return 转换后的大小字符串
     */
    public static String toUpperCase(String str) {
        if (isNotEmpty(str)) {
            return str.toUpperCase();
        }
        return str;
    }
}

静态工具类是对一般的通用性方法的总结归纳,禁止实列化。类上面的final关键字是为了防止类被继承,从而导致一些潜在的问题,当然final也是可以去掉的,不过去掉之前要求我们了解去掉final限制的影响和注意事项。

去掉 final 的影响:

  1. 允许继承:其他类可以继承 StringUtils 类,并重写其中的方法。
  2. 潜在问题:如果其他类继承了 StringUtils 并重写了静态方法,可能会导致意外的行为,因为静态方法与类绑定而不是与实例绑定。

是否去掉 final:

  1. 如果你确定 StringUtils 不会被继承,或者继承不会带来任何问题,那么可以去掉 final。
  2. 如果你希望确保 StringUtils 作为一个纯粹的工具类被使用,并且不希望其他类能够继承它,那么保留 final 是更好的选择。

注意事项:

  1. 如果你确实去掉了 final,建议确保类中的所有方法都是 static 的,以避免不必要的实例化和继承问题
  2. 确保类的用途符合预期,并且了解继承可能带来的影响。

二、静态工具类的线程安全问题

静态工具类一般不会有线程安全的问题,只要我们清楚线程安全问题发生的必要条件,尽量避免即可。但是笔者还是想要给出一个错误的存在线程安全的静态工具类,分析为何会有线程安全问题。示例如下:

package com.grafana.log;

public final class CounterUtils {

    // 非线程安全的静态变量
    private static int counter = 0;

    /**
     * 递增计数器
     */
    public static void incrementCounter() {
        counter++;
    }

    /**
     * 获取当前计数器值
     */
    public static int getCounter() {
        return counter;
    }
}

这是一个静态工具类CounterUtils ,里面定义了一个非线程安全的静态变量counter ,如果有多线程并发调用incrementCounter()方法,那么就可能产生并发问题(发生问题的前提是调用时没有加任何的锁控制,比如synchronized或者ReentrantLock)。

package com.grafana.log;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);

        for (int i = 0; i < 1000; i++) {
            executor.submit(() -> {
                for (int j = 0; j < 1000; j++) {
                    CounterUtils.incrementCounter();
                }
            });
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
            // 等待所有任务完成
        }

        System.out.println("最终计数器值: " + CounterUtils.getCounter());
    }
}

说明

  1. 创建线程池:使用 Executors.newFixedThreadPool(10) 创建一个包含 10 个线程的线程池。
  2. 提交任务:向线程池提交 1000 个任务,每个任务内部再循环 1000 次调用 incrementCounter 方法。
  3. 等待任务完成:使用 while (!executor.isTerminated()) 循环等待所有任务完成。为了避免忙等待,我们在循环体内让主线程短暂休眠。
  4. 输出结果:输出最终计数器值。

运行结果分析

  1. 期望结果:如果没有任何线程安全问题,最终计数器值应该是 1000 * 1000 = 1,000,000。
  2. 实际结果:由于线程安全问题,实际输出的计数器值可能会小于期望值。

实际运行结果如下:

在这里插入图片描述

很明显出现了安全问题,对共享资源地操作没有加任何限制。

解决方法有两类,一是从方法调用者入手,而是工具类本身入手。

方法调用处解决,就是加锁控制了,前面已经说过,使用synchronized或者ReentrantLock

  synchronized (Main.class){
     CounterUtils.incrementCounter();
  }
 try {
     lock.lock();
     CounterUtils.incrementCounter();
 }catch (Exception e){
     system.out.println(e.getMessage());
 }finally {
     lock.unlock();
 }

这两种方式都能解决,线程安全问题,感兴趣可以自行尝试。

工具类本身解决,这个也类似,无非就是加锁方式解决,或者使用一些原子类,这里不再展示。

但是实际的静态工具类应该避免这么些写,避免定义一些可变的全局变量。只提供全部的静态公共方法是没有问题的。就像笔者给出的 StringUtils 工具类一样。


三、使用枚举方式定义静态工具类

枚举(enum)在 Java 中是线程安全的,这是因为 Java 枚举类型的实现具有以下几个特点:

  1. 单例性:
  • 枚举中的每个元素都是一个单例,这意味着对于每一个枚举常量,只会有一个实例存在。
  • 枚举常量在编译期间会被转换成静态字段,这些字段被声明为 final 和 static,确保它们只能被初始化一次并且不会改变。
  1. 初始化过程的线程安全性:
  • 枚举类型的初始化发生在类加载的过程中,而类加载是由 JVM 管理的,它保证了类加载的线程安全性。
  • 当 JVM 加载一个枚举类时,它会确保枚举类型的初始化只发生一次,并且这个初始化过程是原子性的。
  • 因此,即使多个线程同时尝试初始化同一个枚举类,JVM 也会确保只有一个线程执行初始化逻辑。
  1. 构造器的私有性和唯一性:
  • 枚举类型的构造器是私有的,这确保了除了在枚举声明中指定的枚举常量之外,不会有其他实例被创建。
  • 枚举的构造器只能被枚举声明中的枚举常量使用,这也保证了枚举常量的唯一性和不可变性。
  1. 序列化安全性:
  • 枚举类型默认实现了 Serializable 接口,这使得枚举能够被序列化。
  • 枚举类会覆盖 readResolve 方法,确保即使在序列化之后,也只会返回枚举类型的现有实例之一,而不是创建一个新的实例。

综上所述,枚举类型的这些特性共同保证了枚举是线程安全的。因此,当你使用枚举来实现单例模式时,无需担心多线程环境下的并发问题。

下面是一个枚举实现的静态工具类

package com.grafana.log;

public enum StringUtils {

    INSTANCE;

    /**
     * 检查给定的字符串是否为空 (null 或者长度为 0)。
     *
     * @param str 要检查的字符串
     * @return 如果字符串为空返回 true,否则返回 false
     */
    public boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }

    /**
     * 检查给定的字符串是否非空。
     *
     * @param str 要检查的字符串
     * @return 如果字符串非空返回 true,否则返回 false
     */
    public boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    /**
     * 将给定的字符串转换为小写。
     *
     * @param str 要转换的字符串
     * @return 转换后的小写字符串
     */
    public String toLowerCase(String str) {
        if (isNotEmpty(str)) {
            return str.toLowerCase();
        }
        return str;
    }

    /**
     * 将给定的字符串转换为大写。
     *
     * @param str 要转换的字符串
     * @return 转换后的大小字符串
     */
    public String toUpperCase(String str) {
        if (isNotEmpty(str)) {
            return str.toUpperCase();
        }
        return str;
    }
}

测试方法调用代码

package com.grafana.log;

public class Main {
    public static void main(String[] args) {
        System.out.println(StringUtils.INSTANCE.isNotEmpty("Hello")); // 输出 true
        System.out.println(StringUtils.INSTANCE.isEmpty(null)); // 输出 true
        System.out.println(StringUtils.INSTANCE.toLowerCase("HELLO")); // 输出 hello
        System.out.println(StringUtils.INSTANCE.toUpperCase("hello")); // 输出 HELLO
    }
}


四、接口方式定义静态工具类

Java8以后,接口可以定义静态方法,以下是使用接口实现的一个静态工具类简单示例

package com.grafana.log;

public interface StringUtils {

    // 静态方法声明
    static boolean isEmpty(String str) {
        return str == null || str.isEmpty();
    }

    static boolean isNotEmpty(String str) {
        return !isEmpty(str);
    }

    static String toLowerCase(String str) {
        if (isNotEmpty(str)) {
            return str.toLowerCase();
        }
        return str;
    }

    static String toUpperCase(String str) {
        if (isNotEmpty(str)) {
            return str.toUpperCase();
        }
        return str;
    }
}

使用接口定义静态工具类也很方便,接口本身就不可实例化。

总结

在选择使用枚举、使用 final 方式还是在接口中定义静态方法作为工具类时,需要考虑几个因素,包括但不限于线程安全性、多态性、可扩展性以及设计模式的适用性。下面是对每种方式的比较:

枚举方式

  1. 优点:
  • 线程安全:枚举是线程安全的,因为枚举类型的初始化过程由 JVM 管理,并且是原子性的。
  • 单例性:每个枚举常量都是一个单例,这确保了资源的有效利用。
  • 序列化安全性:枚举类型默认实现了 Serializable 接口,并且覆盖了 readResolve 方法,以确保序列化后返回的是现有实例。
  • 易于使用:可以直接通过 INSTANCE 访问枚举中的方法。
  1. 缺点:
    不易扩展:如果需要添加更多的工具方法,需要修改枚举类本身。

使用 final 类方式

  1. 优点:
  • 易于理解和使用:静态工具类是一种常见的做法,易于理解。
  • 易于扩展:可以通过扩展类来添加更多的工具方法。
  1. 缺点:
  • 线程安全性:如果工具类中包含可变状态,则需要额外的同步机制来保证线程安全。
  • 设计模式:使用静态方法可能会导致类变得庞大,难以管理和维护。

在接口中定义静态方法

  1. 优点:
  • 易于组织:可以将相关的工具方法组织在一起,提高代码的可读性和可维护性。
  • 减少类的数量:不需要为这些静态方法创建单独的工具类。
  • 避免命名冲突:通过将静态方法放在接口中,可以避免在项目中出现多个同名的静态工具类。
  1. 缺点:
  • 不支持多态:静态方法与类本身相关联,而不是与对象关联,因此它们不支持多态行为。
  • 设计限制:如果需要定义一些非静态的行为,这种方式可能不够灵活。

总结

  • 枚举方式 适用于需要线程安全单例的情况,特别是在需要序列化支持的情况下。
  • 使用 final 类方式 更适合于简单的工具类,特别是当工具类不涉及复杂的状态管理时。
  • 在接口中定义静态方法 适用于需要将相关的工具方法组织在一起的情况,特别是在不需要多态的情况下。

根据你的具体需求和场景,可以选择最合适的方式。如果需要线程安全的单例,并且工具类的功能相对固定,那么枚举方式可能是最佳选择。如果工具类的功能需要随着项目的扩展而扩展,那么使用 final 类方式可能更合适。如果需要将相关的工具方法组织在一起,并且不需要多态性,那么在接口中定义静态方法是一个不错的选择。

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值