Java随谈(九)日期的实战运用
日期的必要性
在实战开发中,日期处理是离不开的。
- 数据库中存的
create_time、update_time,审批流里存start_time、end_time,订单里有pay_time等。 - 代码中的日期业务逻辑(比如需要校验起止日期小于一个月)。
- 计时
- 分布式ID生成。
既然如此,我们可以站在设计者的角度试想关于日期的操作我们需要设计哪些方法。
- 获取当前时间
- 时间的转化(例子:转化为其他格式的时间,获取对应的年,月,日)
- 时间的对比,计算(例子:两个时间的大小对比,如何获取下一个月)
- 时区问题(例子:如何从北京时间转化为伦敦时间)
日期工具类
幸运的是,Java自身封装了简单的日期工作类可以直接使用,不需要引入其他依赖。
传统Java获取日期工具类
// 获取当前时间
Date now = new Date();
// 创建格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 格式化为字符串
String formattedDate = sdf.format(now);
System.out.println(formattedDate);
在Java的设计初期,传统日期工具类调用SimpleDateFormat后线程不安全,且对象可变。
同时SimpleDateFormat比较耗费性能。
开发人员通过直接编写代码来实现线程安全。但Date的可变性问题还是不能解决。
// 线程安全版本
private static ThreadLocal<DateFormat> threadlocalDateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateStr) throws ParseException {
return threadlocalDateFormat.get().parse(dateStr);
}
public static String format(Date date) {
return threadlocalDateFormat.get().format(date);
}
Apache Commons Lang3
后面有很多第三方工具包开始支持日期操作,减少重复开发。其中Apache Commons Lang3是设计的典范。
在DateFormatUtils结合ThreadLocal解决了线程安全问题。
在DateUtils中封装了很多日期的常用操作,减少开发人员重复编写样板代码。
引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
简单的例子
// 输出最新的时间,举例 2026-08-27 17:01:00
Date now = new Date();
String formattedDate = DateFormatUtils.format(now, "yyyy-MM-dd HH:mm:ss");
System.out.println(formattedDate);
// 加1天
Date tomorrow = DateUtils.addDays(now, 1);
// 减8小时
Date eightHoursAgo = DateUtils.addHours(now, -8);
// 加1个月
Date oneMonthsLater = DateUtils.addMonths(now, 1);
Joda-Time
这个第三方工具包
设计考虑了不变性,天然支持线程安全
同时提供了多种多样的日期计算与操作
支持链式操作
在Java 8之前,当时成为了事实的标准。
引入依赖
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
简单的例子
// 输出最新的时间,举例 2026-08-27 17:01:00
String formattedDate = DateTime.now().toString("yyyy-MM-dd HH:mm:ss");
System.out.println(formattedDate);
JDK8之后的日期工具类
由 Joda-Time 的作者 Stephen Colebourne 设计了 java.time (JSR-310) 包,继承了Joda-Time的所有优势,而且无需引入新的包依赖。
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 格式化为字符串用于展示
String formatted = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 获取明天
LocalDateTime tomorrow = now.plusDays(1);
// 获取下个月
LocalDate nextMonth = now.plusMonths(1);
时间戳
时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数 。
在Java中常用13位时间戳,例如 1718764800000(毫秒)。
使用时间戳的好处:整型存储、高效比较、没有时区歧义。
所以在数据库里存时间,很多团队都倾向于直接存 BIGINT 类型的毫秒时间戳,而不是 DATETIME。
// 传统方式
long timestamp = System.currentTimeMillis();
// java 8 之后
long timestamp = Instant.now().toEpochMilli();
时间戳经常用于存储数据库的新增,更新时间,也用于方法的调用时间等逻辑。
日期工具类的实战
性能监控
在实战中常用于切面,用于监控方法级别的耗时。
System.currentTimeMillis() 是 native 方法,调用开销极低,直接从系统时钟获取。
// 获取开始时间戳 (long 整型)
long startTime = System.currentTimeMillis();
// 模拟业务逻辑
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取结束时间戳
long endTime = System.currentTimeMillis();
// 计算耗时 (整型运算)
long costTime = endTime - startTime;
System.out.println("接口耗时: " + costTime + " 毫秒");
数据库到前端页面的总体链路
数据库存时间 → 后端读取转化为时间类做业务处理 → 从时间类格式化返回给前端。
有些业务只关心天维度,不关心具体几点。比如签到、按天统计的报表。
这种情况下,数据库里甚至可以直接存整型,比如 20260827。优点是查询方便、可以作为分表分库的路由键。
// 从数据库获取日期时间戳,单位毫秒 (例如: 1724743664000)
long dbTimestamp = 1724743664000L;
// 第一步:时间戳转化为 Instant
Instant instant = Instant.ofEpochMilli(dbTimestamp);
// 指定时区 Instant 转化为 LocalDateTime
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
// 省略业务处理
// 第二步:LocalDateTime -> 格式化字符串 (用于前端展示)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String displayDate = localDateTime.format(formatter);
System.out.println("数据库时间戳: " + dbTimestamp);
System.out.println("转换后日期对象: " + localDateTime);
System.out.println("前端展示格式: " + displayDate);
// 若产品只需要支持到天维度,且不关心时区,可以将时间简化为yyyyMMdd格式的天维度数据
// 从数据库获取天维度的日期
int date = 20260827;
// 将年月日的整数转化为日期类LocalDate
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd")
LocalDate localDate = LocalDate.parse(String.valueOf(date), formatter2);
// 省略业务处理
// 若是简单的大小对比,可以直接通过 int 来判断,无需转化为日期类。
// 将年月日转化为字符串传递给前端
DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy/MM/dd")
String displayLocalDate = localDate.format(formatter3);
System.out.println("数据库日期: " + date);
System.out.println("转换后日期对象: " + localDate);
System.out.println("前端展示格式: " + displayLocalDate);
参考文章
- 【Joda-Time 與 JSR310 】(1)Date 與 Calendar 怎麼了?
- 【Joda-Time 與 JSR310 】(2)時間的 ABC
- 【Joda-Time 與 JSR310 】(3)使用 Joda-Time
- 【Joda-Time 與 JSR310 】(4)使用 JDK8 日期時間 API
- Why JSR-310 isn't Joda-Time
- 《Effective Java》
拓展
你能设计一个基于java time的日期工具类吗?
设计目标:支持下列的操作。
- 支持常见的时间戳、天维度日期(
yyyyMMdd)的与日期之间的转化。 - 获取某一天的开始时间,0点0分0秒,以及结束时间23点59分59秒。
- 校验当天是否为周末。
- 起止时间内一共有几个工作日。
- 起止时间是否在一个月内, 是否超过了三个月。

浙公网安备 33010602011771号