首页 > 应用开发 > Java > 都什么年代了你还在用 Date
上篇文章搞清楚了时区,这篇文章就主要来谈一谈 Java 中处理日期时间用什么 API 比较好。我本来不准备写这篇文章的,因为我觉得 Java17 都特么出来了,大家对 Java8 提供的时间日期 API 都很熟悉了。但是经过我调研,很多中小公司还在用老版本的
Date 来处理时间日期,视 Java8 提供的时间日期 API 于无物,所以还是想来推荐一下新一代的时间日期 API,希望对大家有帮助。
老版本的
Date 相信大家都很熟悉了,这里就简单介绍几个点
对于老版本的
Date、
SimpleDateFormat 相信大家都很熟悉,值得注意的是,你是无法直接设置
Date 的时区信息的,但与之矛盾的是我们在代码中从数据库读取一个带时区的时间,例如:
2021-11-01 13:50:47.138494+00 ,它却能够自动解析成当前服务器所在时区的时间封装在
Date 对象中。其实这是它的一个成员变量
private transient BaseCalendar.Date cdate 去做的事情,由于 Unix 时间戳是和时区无关的,所以在从数据库读取时它会将数据库带有时区时间转换为 Unix 时间戳,然后在用这个时间戳转换为当前服务器所在时区的时间,并且携带着时区信息。
其实我们可以看一下
Date 构造方法源码,他就是获取的当前 Unix 时间戳。
public
Date
(
)
{
this
(System
.
currentTimeMillis
(
)
)
;
}
复杂的时区转换计算
如果我们需要显示一个
Date 对象在不同时区的时间,那么我们需要通过
SimpleDateFormat 来实现
SimpleDateFormat sdf
=
new
SimpleDateFormat
(
"yyyy-MM-dd HH:mm:ss"
)
;
Date date
=
new
Date
(
)
;
//默认北京时区的时间
System
.out
.
println
(
"北京时区:"
+ sdf
.
getTimeZone
(
)
)
;
System
.out
.
println
(
"北京时区时间:"
+ sdf
.
format
(date
)
)
;
sdf
.
setTimeZone
(TimeZone
.
getTimeZone
(ZoneId
.
of
(
"Asia/Jakarta"
)
)
)
;
System
.out
.
println
(
"雅加达时区:"
+ sdf
.
getTimeZone
(
)
)
;
System
.out
.
println
(
"雅加达时区时间:"
+ sdf
.
format
(date
)
)
;
如果你想得到的不是字符串而是转换为另一个时区的
Date 对象,那么你还需要再从字符串反转到
Date ......这无疑是非常麻烦的
要说
Date 最难受的地方之一就是两个日期的时间差计算,之前 JDK 并没有提供直接的 API 来计算两个
Date 的间隔。通常我们是把
Date 转成时间戳之后进行操作
Date date1
=
new
Date
(
)
;
TimeUnit
.
SECONDS
.
sleep
(
10
)
;
Date date2
=
new
Date
(
)
;
long time1
= date1
.
getTime
(
)
;
long time2
= date2
.
getTime
(
)
;
System
.out
.
println
(
"间隔秒数:"
+
(time2
- time1
)
/
1000
)
;
System
.out
.
println
(
"间隔小时数:"
+
(time2
- time1
)
/
(
1000
*
3600
)
)
;
//...
是不是很麻烦?如果你并不觉得麻烦,那么你只是没有见过更好的方式。我们来看看新版 API 怎么来做(后面会详细介绍)
Duration duration
= Duration
.
between
(time2
, time1
)
;
long days
= duration
.
toDays
(
)
;
//获取天数间隔
long hours
= duration
.
toHours
(
)
;
//获取小时间隔
//...
这样是不是很简单!
在谈新版的时间 API 之前,我们先要搞清楚一个问题,那就是你真的有必要把时区信息存储到数据库吗?其实我觉得对于绝大多数的公司应该都是不需要的,下我以我们公司印度尼西亚业务为例来分析这个问题。
我们目前是先在代码中获取当前服务器(我们服务器在亚马逊
UTC-3 )所在时区的
Date 对象,在存入数据库时,在数据库层面将其转换为
UTC+8 时区的时间存储到数据库中,例如
2021-10-24 15:47:47.138494-03 存到数据库中是
2021-10-25 02:47:47.138494+08,然后在前端页面可以直接调用 API 根据带时区的时间计算出前端设备当地时区的时间。
xxx//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494
不过不是很明白,我们既然存时区,那么应该也要存印度尼西亚的时区啊......毕竟我们的业务在印尼,也不在北京......
正常来说服务器所在地一般是唯一的,即使有
100 个服务实例来做负载均衡,总不可能出现一半服务器在美国,一半在中国吧?那么代码中的时间日期操作都是默认使用服务器所在地时区(
UTC-3),既然如此那么我们数据库就可以不存储时区信息,只存储一个时间描述例如
2021-10-24 13:50:47.138494,它本身没有时区,但是我们都知道它的默认时区就是
UTC-3 。
这样在前端代码中只需要调用 API 的时候带上带上该时间的所属时区
UTC-3 即可算出前端设备当地时区的时间。
//java学习交流:737251827 进入可领取学习资源及对十年开发经验大佬提问,免费解答!
xxx
//假如设备在印度尼西亚,那么前端 API 获得的时间就是 2021-10-25 01:47:47.138494
而且这个不带时区的时间不会受数据库时区的影响,无论你把数据库设置成哪个时区,它都不会变化。这种方式就是整个业务里我们把所有时区都干掉,只留一个服务器时区,当有任何涉及时区的业务功能时,只需要把
源时间换算成服务器所在时区的时间即可。
不存储时区还有一种情况就是有些程序员喜欢存时间戳,不得不吐槽一下我觉得这是最 low 的方式之一(也许你能说出它仅有的个别优点,但我不接受反驳)。虽然时间戳是和时区无关的,但是它的可读性真的太差了......而且代码中也不好进行操作
其实对比一下就能发现数据库存不存储时区信息,对于前后端的操作区别不大的。而且我觉得绝大多数业务场景,不存储时区相对更简单!
看源码是一个好习惯,看源码注释更是一个好习惯。这三个类的注释说的很清楚,首先这些类是线程安全的,对于它的任何操作都会产生一个新的实例,这和
String 类是一样的。其次它不存储或表示时区。 相反,它是对日期时间的描述。
值得注意的是,它不存储时区,但不代表它没有时区,细品这句话!通过一张图来理解三者的关系

下面以
LocalDateTime 为例简单介绍下用法
LocalDateTime time1
= LocalDateTime
.
now
(
)
;
LocalDateTime time2
= LocalDateTime
.
now
(
)
;
time1
.
isAfter
(time2
)
; time1
.
isBefore
(time2
)
;
//比较时间
time1
.
plusDays
(
1L
)
; time1
.
minusHours
(
1L
)
;
//加减时间日期
LocalDateTime
.
parse
(
"2021-11-19T15:16:17"
)
;
//解析时间
LocalDateTime
.
of
(
2019
,
11
,
30
,
15
,
16
,
17
)
;
//指定日期时间
LocalDateTime
.
now
(ZoneId
.
of
(
"Asia/Jakarta"
)
)
;
//其他时区相对此服务器时区的时间
如果你可以不在数据库中存储时区信息的话,那么请使用这个类。如果你一定要存储,那么也请使用下面的
OffsetDateTime 而不要使用
Date 。
带有时区偏移量的日期时间类,相当于
OffsetDateTime = LocalDateTime + ZoneOffset

大多数情况下我们使用带有偏移量的日期时间已经能够满足需求。
真正带有完整时区信息的日期时间类

Duration、Period
这两个类是代表一段时间或者说是两个时间的间隔,以
Duration 为例,试想在 Java8 之前你有一个业务要表示一个令牌的有效期为
7 天,那么通常的做法可能是存储令牌的创建时间,然后在代码中用系统当前时间减去令牌创建时间和
7 天做比较,例如
long duration
= System
.
currentTimeMillis
(
)
- token
.
getCreateTime
(
)
.
getTime
(
)
;
if
(duration
<
7
*
24
*
60
*
60
*
1000
)
{
//令牌合法}
但是在使用 Duration 之后,
Duration duration
= Duration
.
between
(token
.
getCreateTime
(
)
, LocalDateTime
.
now
(
)
)
;
boolean negative
= duration
.
minus
(effective
)
.
isNegative
(
)
;
//是否过期
其次
Duration 在 SpringBoot 项目中,配置也很方便
token
:
effective
-time
:
7d # d
:天
, h
:小时
, m
:分钟
, s
:秒
在实体类中,可以使用
@ConfigurationProperties 或者
@Value 将它直接映射成
Duration 对象,当然这依赖于
SpringBoot 中提供的丰富的
类型转换器。下一篇文章会介绍
第一眼看到这两个类是不是想起了熟悉的
Collection 和
Collections ,与之类似这两个类是时间矫正器接口和时间矫正器的工具类。新版日期时间类几乎都实现了
TemporalAdjuster ,以便于针对所有日期时间都可以对其进行计算得到另外一个时间,例如
LocalDateTime
.
now
(
)
.
with
(TemporalAdjusters
.
firstDayOfMonth
(
)
)
;
//当月第一天
LocalDateTime
.
now
(
)
.
with
(TemporalAdjusters
.
firstDayOfNextMonth
(
)
)
;
//下个月第一天
LocalDateTime
.
now
(
)
.
with
(TemporalAdjusters
.
dayOfWeekInMonth
(
2
,DayOfWeek
.
MONDAY
)
)
;
//第N个星期几
LocalDateTime
.
now
(
)
.
with
(TemporalAdjusters
.
next
(DayOfWeek
.
MONDAY
)
)
;
//下个星期几
//...
有这么丰富的 API ,你还需要写一堆日期时间的工具类吗?
不可否认存在一种现象就是你的项目一直用的都是
Date,而
leader 又不愿意花费时间精力去升级,或者老的业务限制的情况,那么某些场景下你可以使用 Java8 提供的
Instant 将两者互转来简化一些业务代码操作
LocalDateTime
.
ofInstant
(
new
Date
(
)
.
toInstant
(
)
, ZoneId
.
systemDefault
(
)
)
;
//Date 转 LocalDateTime
//java学习交流:737251827 进入可领取学习资源及对十年开发经验大佬提问,免费解答!
Date
.
from
(LocalDateTime
.
now
(
)
.
atZone
(ZoneId
.
systemDefault
(
)
)
.
toInstant
(
)
)
;
// LocalDateTime 转 Date
Java8 的时间日期工具还有很多用法,这里就不一一介绍了,总之
Date 有的功能
LocalDateTime 都有,
Date 没有的功能,
LocalDateTime 还有很多。使用新版的日期时间,几乎是不存在原来的
DateUtil 的。所以还需要我告诉你选谁吗~~
人们总是对于自己熟悉的东西持有倾向,对于不熟悉的新事物往往会抵触,曾经我也不止一次的抵触我亲爱的架构师让我们更换新的技术组件,但后来我都爱上了这些新的技术。
抵触新技术不是一个优秀程序员该有,这会阻碍你的成长。新技术的出现往往是弥补老技术的缺陷,没有哪个组织会花费人力物力出一个废物组件......所以每当有新技术组件出现时,请尝试它,也许会有意想不到的收获!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70010294/viewspace-2848336/ ,如需转载,请注明出处,否则将追究法律责任。