1. 引言
在日常 Java 开发中,我们经常需要处理一些重复性的基础工作,比如日期格式化、字符串校验、集合操作、文件读写等。虽然 JDK 提供了丰富的 API,但直接使用往往代码冗长、容易出错。本文将分享一套我自己封装并持续维护的 Java 工具类合集,涵盖日期、字符串、集合、文件、加密、并发等高频场景,帮助大家减少重复代码、提升开发效率。
所有工具类均基于 JDK 8+ 编写,无任何第三方依赖,可直接复制到项目中使用。
工具类总览
下表汇总了本文将要介绍的 6 个工具类,方便你快速了解每个工具类的核心能力与适用场景:
| 工具类名称 | 核心功能 | 主要方法 | 适用场景 |
|---|---|---|---|
| DateUtils | 日期时间格式化、解析、计算与区间判断 | now()、format()、parse()、daysBetween()、isBetween() |
日志时间戳、业务时间计算、时间区间校验 |
| StringUtils | 字符串判空、拼接、脱敏与格式校验 | isEmpty()、isBlank()、join()、maskMobile()、isNumeric() |
参数校验、手机号脱敏、文本拼接 |
| CollectionUtils | 集合判空、去重、分批与转换 | isEmpty()、isNotEmpty()、distinctByKey()、partition()、toMap() |
批量数据处理、列表去重、分批入库 |
| FileUtils | 文件读写、复制与递归删除 | readFile()、writeFile()、copyFile()、deleteRecursively() |
配置文件读取、临时文件清理、文件备份 |
| EncryptUtils | MD5、SHA-256 哈希加密 | md5()、sha256() |
密码存储、数据完整性校验 |
| ThreadUtils | 线程休眠、线程池创建与超时执行 | sleep()、newFixedPool()、executeWithTimeout() |
异步任务、定时休眠、带超时的远程调用 |
2. 日期时间工具类 DateUtils
日期处理是开发中最常见的需求之一。这里封装了格式化、解析、计算、区间判断等常用方法。
java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
/**
* 日期时间工具类(基于 java.time,线程安全)
*/
public final class DateUtils {
private static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
private DateUtils() {
}
/** 格式化当前时间 */
public static String now() {
return format(LocalDateTime.now(), DEFAULT_PATTERN);
}
/** 按指定格式格式化时间 */
public static String format(LocalDateTime dateTime, String pattern) {
return dateTime.format(DateTimeFormatter.ofPattern(pattern));
}
/** 解析字符串为 LocalDateTime */
public static LocalDateTime parse(String dateTimeStr, String pattern) {
return LocalDateTime.parse(dateTimeStr, DateTimeFormatter.ofPattern(pattern));
}
/** 计算两个日期相差的天数 */
public static long daysBetween(LocalDate start, LocalDate end) {
return ChronoUnit.DAYS.between(start, end);
}
/** 判断某个时间是否在区间内(含边界) */
public static boolean isBetween(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
return !target.isBefore(start) && !target.isAfter(end);
}
}
3. 字符串工具类 StringUtils
字符串判空、去空格、拼接、脱敏是业务代码中的高频操作,封装后可以让代码更简洁。
java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字符串工具类
*/
public final class StringUtils {
private StringUtils() {
}
/** 判断字符串是否为空(null 或空串) */
public static boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
/** 判断字符串是否为空(null、空串或全空白) */
public static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
/** 使用分隔符拼接字符串列表 */
public static String join(List<String> list, String delimiter) {
return list.stream().collect(Collectors.joining(delimiter));
}
/** 手机号脱敏:保留前 3 后 4 位 */
public static String maskMobile(String mobile) {
if (isBlank(mobile) || mobile.length() != 11) {
return mobile;
}
return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
}
/** 判断字符串是否为纯数字 */
public static boolean isNumeric(String str) {
if (isBlank(str)) {
return false;
}
return str.chars().allMatch(Character::isDigit);
}
}
4. 集合工具类 CollectionUtils
集合判空、分组、去重、转换是日常开发中使用频率极高的操作。
java
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 集合工具类
*/
public final class CollectionUtils {
private CollectionUtils() {
}
/** 判断集合是否为空 */
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}
/** 判断集合是否非空 */
public static boolean isNotEmpty(Collection<?> collection) {
return !isEmpty(collection);
}
/** 按指定字段去重 */
public static <T> List<T> distinctByKey(List<T> list, Function<? super T, ?> keyExtractor) {
Set<Object> seen = new HashSet<>();
return list.stream()
.filter(e -> seen.add(keyExtractor.apply(e)))
.collect(Collectors.toList());
}
/** 将列表按指定大小分批 */
public static <T> List<List<T>> partition(List<T> list, int size) {
if (isEmpty(list) || size <= 0) {
return Collections.emptyList();
}
List<List<T>> result = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
result.add(new ArrayList<>(list.subList(i, Math.min(i + size, list.size()))));
}
return result;
}
/** 将列表转换为 Map(key 冲突时保留第一个) */
public static <T, K> Map<K, T> toMap(List<T> list, Function<? super T, ? extends K> keyExtractor) {
return list.stream().collect(Collectors.toMap(keyExtractor, Function.identity(), (a, b) -> a));
}
}
5. 文件工具类 FileUtils
文件读写、复制、删除等操作封装后,可以避免大量样板代码。
java
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
/**
* 文件工具类
*/
public final class FileUtils {
private FileUtils() {
}
/** 读取文件全部内容为字符串 */
public static String readFile(String path) throws IOException {
return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
}
/** 写入字符串到文件(自动创建父目录) */
public static void writeFile(String path, String content) throws IOException {
Path filePath = Paths.get(path);
if (filePath.getParent() != null) {
Files.createDirectories(filePath.getParent());
}
Files.write(filePath, content.getBytes(StandardCharsets.UTF_8));
}
/** 复制文件 */
public static void copyFile(String source, String target) throws IOException {
Files.copy(Paths.get(source), Paths.get(target), StandardCopyOption.REPLACE_EXISTING);
}
/** 递归删除目录 */
public static void deleteRecursively(File file) throws IOException {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
Files.deleteIfExists(file.toPath());
}
}
6. 加密工具类 EncryptUtils
MD5、SHA-256 等哈希算法常用于密码存储和完整性校验。
java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* 加密工具类
*/
public final class EncryptUtils {
private EncryptUtils() {
}
/** 计算 MD5 哈希(转十六进制字符串) */
public static String md5(String input) {
return hash(input, "MD5");
}
/** 计算 SHA-256 哈希(转十六进制字符串) */
public static String sha256(String input) {
return hash(input, "SHA-256");
}
private static String hash(String input, String algorithm) {
try {
MessageDigest digest = MessageDigest.getInstance(algorithm);
byte[] bytes = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("不支持的算法: " + algorithm, e);
}
}
}
7. 并发工具类 ThreadUtils
线程休眠、线程池创建等操作封装后,可以让并发代码更简洁、更安全。
java
import java.util.concurrent.*;
/**
* 并发工具类
*/
public final class ThreadUtils {
private ThreadUtils() {
}
/** 线程休眠(不抛出受检异常) */
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/** 创建固定大小线程池(带命名工厂) */
public static ExecutorService newFixedPool(int size, String poolName) {
ThreadFactory factory = new ThreadFactory() {
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, poolName + "-" + (++count));
}
};
return Executors.newFixedThreadPool(size, factory);
}
/** 带超时地执行任务,返回结果或默认值 */
public static <T> T executeWithTimeout(Callable<T> task, long timeout, TimeUnit unit, T defaultValue) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<T> future = executor.submit(task);
return future.get(timeout, unit);
} catch (Exception e) {
return defaultValue;
} finally {
executor.shutdownNow();
}
}
}
性能与边界条件
使用上述工具类时,有几个性能与边界条件需要特别留意:
- executeWithTimeout 的线程池开销 :该方法每次调用都会通过
Executors.newSingleThreadExecutor()新建一个线程池,并在finally中调用shutdownNow()销毁。在高频调用场景下,频繁创建和销毁线程池会带来不小的资源开销,甚至可能成为性能瓶颈。建议在频繁调用时复用同一个线程池,例如将线程池提升为类的静态成员,或由调用方统一创建并传入。 - sleep 的中断处理 :
sleep方法捕获InterruptedException后调用Thread.currentThread().interrupt()恢复中断标志,而不是吞掉异常。这样调用方仍可通过Thread.interrupted()感知中断状态,避免中断信号被静默丢失,是推荐的中断处理策略。 - newFixedPool 的线程数设置 :线程池大小应根据任务类型合理设置。CPU 密集型任务建议设置为
CPU 核数 + 1,I/O 密集型任务可适当调大(如CPU 核数 * 2),避免线程数过少导致吞吐不足,或过多导致上下文切换开销增大。
JMH 基准测试:新建线程池 vs 复用静态线程池
为了量化「每次新建线程池」与「复用静态线程池」两种实现方式的性能差异,下面使用 JMH(Java Microbenchmark Harness)编写基准测试,分别在 1000、10000、100000 次调用下对比两者的吞吐量和平均耗时。
java
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* JMH 基准测试:executeWithTimeout 两种实现方式对比
* 运行方式:mvn clean package && java -jar target/benchmarks.jar
*/
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
@Threads(1)
public class ThreadUtilsBenchmark {
/** 方式一:每次调用新建线程池(对应原实现) */
public static <T> T executeWithTimeoutNew(Callable<T> task, long timeout, TimeUnit unit, T defaultValue) {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
Future<T> future = executor.submit(task);
return future.get(timeout, unit);
} catch (Exception e) {
return defaultValue;
} finally {
executor.shutdownNow();
}
}
/** 方式二:复用静态线程池 */
private static final ExecutorService SHARED_EXECUTOR = Executors.newSingleThreadExecutor();
public static <T> T executeWithTimeoutShared(Callable<T> task, long timeout, TimeUnit unit, T defaultValue) {
try {
Future<T> future = SHARED_EXECUTOR.submit(task);
return future.get(timeout, unit);
} catch (Exception e) {
return defaultValue;
}
}
private static Callable<Integer> task() {
return () -> 42;
}
@Benchmark
public void newPool_1000(Blackhole bh) {
for (int i = 0; i < 1000; i++) {
bh.consume(executeWithTimeoutNew(task(), 1, TimeUnit.SECONDS, -1));
}
}
@Benchmark
public void sharedPool_1000(Blackhole bh) {
for (int i = 0; i < 1000; i++) {
bh.consume(executeWithTimeoutShared(task(), 1, TimeUnit.SECONDS, -1));
}
}
@Benchmark
public void newPool_10000(Blackhole bh) {
for (int i = 0; i < 10000; i++) {
bh.consume(executeWithTimeoutNew(task(), 1, TimeUnit.SECONDS, -1));
}
}
@Benchmark
public void sharedPool_10000(Blackhole bh) {
for (int i = 0; i < 10000; i++) {
bh.consume(executeWithTimeoutShared(task(), 1, TimeUnit.SECONDS, -1));
}
}
@Benchmark
public void newPool_100000(Blackhole bh) {
for (int i = 0; i < 100000; i++) {
bh.consume(executeWithTimeoutNew(task(), 1, TimeUnit.SECONDS, -1));
}
}
@Benchmark
public void sharedPool_100000(Blackhole bh) {
for (int i = 0; i < 100000; i++) {
bh.consume(executeWithTimeoutShared(task(), 1, TimeUnit.SECONDS, -1));
}
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(ThreadUtilsBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
测试结果(示例数据,实际以本机环境为准):
| 调用次数 | 实现方式 | 吞吐量(ops/ms) | 平均耗时(ms/op) |
|---|---|---|---|
| 1000 | 每次新建线程池 | 约 0.8 | 约 1.25 |
| 1000 | 复用静态线程池 | 约 12.5 | 约 0.08 |
| 10000 | 每次新建线程池 | 约 0.7 | 约 1.43 |
| 10000 | 复用静态线程池 | 约 13.0 | 约 0.077 |
| 100000 | 每次新建线程池 | 约 0.6 | 约 1.67 |
| 100000 | 复用静态线程池 | 约 12.8 | 约 0.078 |
测试结论与推荐方案:
- 复用静态线程池的吞吐量约为每次新建线程池的 15~20 倍,平均耗时从毫秒级降到亚毫秒级。调用次数越多,差距越明显,100000 次调用时新建线程池的耗时已接近复用方案的 20 倍。
- 推荐方案 :在频繁调用
executeWithTimeout的场景下,务必复用线程池。可将线程池提升为ThreadUtils的静态成员,或由调用方统一创建并传入。同时注意,复用线程池后需在应用关闭时显式调用shutdown()释放资源,避免线程泄漏。 - 补充说明:若任务本身耗时较长(如远程调用),线程池创建开销占比会相对下降,但复用线程池仍能显著降低调度成本,且能避免频繁创建线程带来的 GC 压力。
异常处理与注意事项
工具类封装了常用能力,但异常处理策略各不相同。使用前先了解哪些方法会抛出受检异常、哪些异常被包装或吞掉,能避免在业务代码里踩坑。
1. 受检异常:谁抛出、如何应对
- FileUtils :
readFile()、writeFile()、copyFile()、deleteRecursively()四个方法都声明抛出IOException。调用方必须显式处理,要么用try-catch捕获并记录日志,要么在方法签名上throws IOException向上抛出,交由上层统一处理。例如:
java
try {
String content = FileUtils.readFile("config.json");
// 处理文件内容
} catch (IOException e) {
log.error("读取配置文件失败", e);
// 返回默认配置或抛出业务异常
}
- EncryptUtils :
md5()、sha256()不抛出受检异常,内部已将NoSuchAlgorithmException包装为RuntimeException,调用方无需强制捕获。 - ThreadUtils :
sleep()、newFixedPool()、executeWithTimeout()均不抛出受检异常,InterruptedException在内部被恢复中断标志后吞掉,TimeoutException等被捕获后返回默认值。
2. EncryptUtils 中 RuntimeException 包装的合理性
hash() 方法捕获 NoSuchAlgorithmException 后抛出 RuntimeException("不支持的算法: " + algorithm, e),这种设计是合理的:
- 算法名是编译期常量 :
MD5、SHA-256都是 JDK 内置算法,运行时几乎不可能缺失。若声明为受检异常,会让每个调用方都写无意义的try-catch,徒增样板代码。 - 失败即快速失败:一旦算法确实不存在(如 JDK 版本裁剪),立即抛出运行时异常让程序尽早暴露问题,而不是静默返回错误结果。
- 保留原始异常链 :包装时传入原始异常
e作为 cause,排查问题时仍能定位到根因。
3. executeWithTimeout 超时后默认值的适用场景与潜在风险
executeWithTimeout 在任务超时、被中断或执行异常时都会返回 defaultValue,这带来便利的同时也隐藏着风险:
- 适用场景:适合「拿不到结果也能继续」的降级场景,例如缓存查询失败时返回空列表、远程调用超时返回预设的兜底配置,保证主流程不被中断。
- 潜在风险 :默认值会掩盖真实失败原因 。超时、异常、正常返回三种情况在调用方看来结果相同,无法区分。若默认值被当作真实业务数据继续参与计算,可能产生错误结果且难以排查。例如超时返回
0作为订单金额,会导致后续统计失真。 - 建议 :对关键业务,不要盲目依赖默认值。可改为抛出异常或返回
Optional,让调用方显式感知失败;若必须用默认值,至少记录一条 warn 日志,便于事后追踪。
8. 使用示例与总结
下面是一个综合使用示例,演示如何将这些工具类组合起来完成一个简单的业务场景。
java
public class Demo {
public static void main(String[] args) throws Exception {
// 1. 日期处理
System.out.println("当前时间: " + DateUtils.now());
// 2. 字符串脱敏
String mobile = "13812345678";
System.out.println("脱敏手机号: " + StringUtils.maskMobile(mobile));
// 3. 集合分批处理
List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<List<Integer>> batches = CollectionUtils.partition(ids, 3);
System.out.println("分批结果: " + batches);
// 4. 文件写入与读取
FileUtils.writeFile("test.txt", "Hello Java Tools!");
System.out.println("文件内容: " + FileUtils.readFile("test.txt"));
// 5. 加密
System.out.println("MD5: " + EncryptUtils.md5("hello"));
System.out.println("SHA-256: " + EncryptUtils.sha256("hello"));
// 6. 并发
ThreadUtils.sleep(100);
System.out.println("休眠完成");
}
}
下面是一个更贴近真实业务的综合案例:模拟用户注册流程,综合运用字符串校验、加密、日期记录与集合去重等能力。
java
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class RegisterDemo {
/** 用户实体 */
static class User {
String mobile;
String password;
LocalDate registerDate;
User(String mobile, String password, LocalDate registerDate) {
this.mobile = mobile;
this.password = password;
this.registerDate = registerDate;
}
@Override
public String toString() {
return "User{mobile='" + mobile + "', password='" + password + "', registerDate=" + registerDate + "}";
}
}
public static void main(String[] args) {
// 1. 模拟注册请求参数
String mobile = "13812345678";
String password = "abc123456";
// 2. 使用 StringUtils 校验手机号和密码非空
if (StringUtils.isBlank(mobile) || StringUtils.isBlank(password)) {
System.out.println("注册失败:手机号和密码不能为空");
return;
}
System.out.println("校验通过:手机号与密码均非空");
// 3. 使用 EncryptUtils 对密码进行 SHA-256 加密
String encryptedPassword = EncryptUtils.sha256(password);
System.out.println("密码加密结果: " + encryptedPassword);
// 4. 使用 DateUtils 记录注册时间
String registerTime = DateUtils.now();
System.out.println("注册时间: " + registerTime);
// 5. 模拟一批用户(含同一天重复注册的账号),使用 CollectionUtils 按注册日期去重
List<User> userList = new ArrayList<>();
userList.add(new User("13812345678", encryptedPassword, LocalDate.now()));
userList.add(new User("13912345678", encryptedPassword, LocalDate.now()));
userList.add(new User("13712345678", encryptedPassword, LocalDate.now().minusDays(1)));
List<User> distinctUsers = CollectionUtils.distinctByKey(userList, u -> u.registerDate);
System.out.println("按注册日期去重后的用户数: " + distinctUsers.size());
distinctUsers.forEach(System.out::println);
}
}
以上工具类覆盖了日常开发中最常见的基础场景。你可以根据项目实际需要继续扩展,比如增加 JSON 解析、Excel 导出、HTTP 请求封装等。希望这套工具类能帮你减少重复劳动,把更多精力放在核心业务逻辑上。
9. 扩展方向
以上工具类只是起点,实际项目中还可以继续封装更多高频能力,下面列出 5 个值得扩展的方向:
- JSON 解析工具类(基于 Jackson):封装对象与 JSON 字符串之间的序列化、反序列化及格式化输出,适用于接口对接、配置解析等场景。
- HTTP 请求工具类(基于 HttpClient):封装 GET、POST、文件上传等常用请求,统一处理超时、重试与异常,适用于调用第三方 REST 接口。
- Excel 导入导出工具类(基于 EasyExcel):提供注解驱动的 Excel 读写能力,支持大数据量流式处理,适用于报表导出与批量数据导入。
- 正则校验工具类:封装邮箱、手机号、身份证、URL 等常见格式的正则校验,统一校验逻辑,适用于表单参数合法性检查。
- 日志打印工具类:封装统一的日志格式与级别控制,支持参数化输出与异常堆栈打印,适用于规范项目日志、便于排查问题。