JDK8新时间API的用法
对于时间API,一般业务用到的比较少,因此平时不怎么太关注。
最近在做国际化业务,经常涉及不同时区、时间格式等问题;团队要求使用
LocalDateTime
等新实践API替代Date
类。
为什么用LocalDateTime
替代Date
类?
- 可变性:
Date
类是可变的,而LocalDateTime
是不可变的。这意味着一旦创建了LocalDateTime
对象,就不会发生任何更改。这有助于避免在多线程环境下的并发问题。 - 线程安全性:由于
LocalDateTime
是不可变的,它可以在多线程环境下安全地使用,而不需要额外的同步操作。 - 可读性:
LocalDateTime
具有更好的可读性和可理解性。它提供了明确的方法来获取和设置日期时间的各个部分,例如年、月、日、小时、分钟和秒等。 - 更灵活:
LocalDateTime
提供了更多的功能和灵活性。它可以轻松地进行日期时间的运算和比较,以及格式化为字符串和解析字符串为日期时间。
新API的使用
时间戳相关
Instant
类是JDK8
提供的一个用于记录时刻/时间点
的类。通俗来说,这个类的作用等同于一个时间戳。
根据JDK8官方文档的注释,Instant是指时间线上的一个瞬时点(An instantaneous point on the time-line.)。
构造Instant实例:
// 1 获取当前时刻的Instant实例
Instant currentInstant = Instant.now();
// 2 根据毫秒级时间戳,获取对应时刻的Instant实例
Instant expiredInstant = Instant.ofEpochMilli(taskDO.getExpireTimeStamp());
// 3 根据秒级时间戳,获取对应时刻的Instant实例
Instant expiredInstant = Instant.ofEpochSecond(taskDO.getExpireTimeStamp()/1000L);
通过用Instant
来代替旧的System
方法,可以更高性能的获取当前时刻的时间戳:
// 使用静态方法获取当前时刻的instant实例
Instant currentInstant = Instant.now();
// 获取毫秒级时间戳
currentInstant.toEpochMilli();
// 获取秒级时间戳
currentInstant.getEpochSecond();
// 旧的获取当前时间戳的方法。存在性能问题
System.currentTimeMillis();
关于System方法有并发性能问题:System.currentTimeMillis()性能问题
计算当前时刻的相对时刻
场景:计算相对当前时刻而言,7天后的时刻,用于给出数据的过期时间
通过使用Instant.plus()
方法,在当前时刻的基础上增加7天时间,获得对应的时间点,实现时间点在时间轴上的平移。举例如下:
// 获取当前时刻15天后的时间戳
Long expireTimeStamp = Instant.now().plus(15L, DAYS).toEpochMilli();
注意:
Instant
提供了多种版本的plus
重载方法,上面版本的plus
方法接收两个入参:参数1为long
类型变量,指代时间点平移的大小,参数2为TemporalUnit
类型变量,指代某种时间单位。- 上面方法调用的第二个参数
DAYS
是ChronoUnit
枚举类的成员,ChronoUnit
枚举实现了TemporalUnit
接口,并定义了多种时间单位,小到诺秒NANOS
,大到世纪CENTURIES
,都被定义在其中。 Instant
提供了minus
方法,表示plus
方法的反操作,内部逻辑也是反向调用plus
方法,这里不再赘述。
比较时间先后
场景: 比较两个时间点的先后,判断是否过期/超期
通过isAfter()
、isBefore()
方法,对两个Instant实例进行比较,判断时间线上的先后顺序。举例如下:
// 通过Instant.ofEpochMilli()方法从毫秒级时间戳构造Instant实例
Instant expireInstant = Instant.ofEpochMilli(taskDO.getExpireTimeStamp());
// 判断当前时间是否处于过期时间前,即:判断是否未过期
Boolean notExpired = Instant.now().isBefore(expireInstant);
// isAfter方法的调用方式相同,内部实现逻辑也相同,这里不再赘述。
日期时间相关
-
LocalDate用于表示一个日期,这个日期是指以年、月、日为单位表示的一个时间概念,不包括时分秒信息。
-
LocalTime用于表示一个时间,这个时间是指以时、分、秒为单位表示的一个时间概念,不包括年月日信息。
-
LocalDateTime是上述两个类的合成,内部仅有两个实例变量,分别是一个LocalDate实例和一个LocalTime实例。
注意:这3个类不包括任何时区信息,在表示效果上,这3个类内部存储的年月日时分秒信息与使用字符串存储基本没有区别。如果想要考虑时区相关因素,请参考下面的【时区相关】章节。
构造实例
// 传入年、月、日的int型变量,构造对应日期的localDate实例
LocalDate localDate = LocalDate.of(1996, 11, 7);
// 获取当前时刻的localDate实例
LocalDate currentDate = LocalDate.now();
// 传入时、分、秒的int型变量,构造对应时间的localTime实例
LocalTime localTime = LocalTime.of(12, 23, 5);
// 传入时、分、秒、诺秒的int型变量,构造对应时间的localTime实例
LocalTime localTime = LocalTime.of(12, 23, 5, 999001);
// 获取当前时刻的localTime实例
LocalTime currentTime = LocalTime.now();
// 通过传入localDate、localTime实例,构造对应的localDateTime实例
LocalDateTime localDateTime = LocalDateTime.of(LocalDate.now(), LocalTime.now());
// 通过传入年、月、日、时、分、秒的int型变量,构造对应的localDateTime实例
LocalDateTime localDateTime = LocalDateTime.of(1996, 11, 7, 12, 23, 5);
// 通过传入年、月、日、时、分、秒、诺秒的int型变量,构造对应的localDateTime实例
LocalDateTime localDateTime = LocalDateTime.of(1996, 11, 7, 12, 23, 5, 999001);
// 获取当前时刻的localDateTime实例
LocalDateTime currentDateTime = LocalDateTime.now();
根据时、分、秒、毫秒构造LocalTime或LocalDateTime
场景:需要用时、分、秒、毫秒来构造LocalTime、LocalDateTime,但不想把毫秒转为诺秒
首先构造LocalTime
实例,然后通过plus()
方法,在对应实例上增加毫秒级时间差值。举例如下:
LocalTime localTime = LocalTime.of(23, 15, 20)
.plus(199, ChronoUnit.MILLIS);
注意:这里的plus
方法与Instant
类的plus
方法都是对他们的接口Temporal
的方法实现,但两者实现方式不同。
注意:
如果使用jackson
序列化LocalDateTime
,会出现序列化错误。这时需要引入相关的jackson-datatype
依赖解决问题:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.14.0</version>
</dependency>
时间间隔相关
Duration类是一个表示时间间隔的类,主要以较小的时间单位(日、时、分、秒等单位)来表示时间间隔。如果想要以较大的时间单位表示时间间隔,建议用Period类。
用处:计算今天早上9点到明天下午2点之间的时间间隔,并计算这段时间间隔有多少【天】,多少【小时】,多少【分钟】,多少【秒】
注意:Duration类可以用来表示超过24小时的时间间隔,但最大只支持以【日】为单位表示间隔,不支持以更大的单位,如【月】来表示间隔。
举例如下:Duration可以表示2022-0-0
到2023-0-0
之间的时间间隔,但通过API只能知道,这段时间间隔有365天(因为有toDays()
方法),不能通过API知道,这段时间间隔有1年(因为没有相关方法)。
构造实例
// 使用同样类型的两个时间变量构造Duration
Duration duration = Duration.between(LocalTime startLocalTime, LocalTime endLocalTime);
Duration duration = Duration.between(LocalDate startLocalDate, LocalDate endLocalDate);
Duration duration = Duration.between(LocalDateTime startLocalDateTime, LocalDateTime endLocalDateTime);
Duration duration = Duration.between(Instant startInstant, Instant endInstant);
以指定时间单位表示对应时间间隔
场景:判断两个时间点之间的间隔是否超过1小时。举例如下:
// 计算两个时间之间的间隔
Duration duration = Duration.between(Instant.ofEpochMilli(task.getGmtCreate()),Instant.now());
// 计算这段时间间隔有多少天
long durationInDays = duration.toDays();
// 计算这段时间间隔有多少小时
long durationInHours = duration.toHours();
// 计算这段时间间隔有多少分钟
long durationInMinutes = duration.toMinutes();
// 计算这段时间间隔有多少秒
long durationInSecond = duration.get(ChronoUnit.SECONDS);
// 计算这段时间间隔有多少毫秒
long durationInMillis = duration.toMillis();
// 计算这段时间间隔有多少诺秒
long durationInNanos = duration.toNanos();
注意:Duration的get
方法只支持秒和诺秒两个入参,输入其他时间单位会报错。
注意:Duration.get(ChronoUnit.NANOS)
和Duration.toNanos()
都是合法的方法调用,但两者含义不同:前者表示获取当前duration
中不足1秒的部分,并以诺秒表示;后者表示获取当前duration
的全部时间间隔,并以诺秒表示。
举例:对于一个表示1秒99诺秒的Duration,调用第一个方法会返回99,调用第二个方法会返回1000000099
(1 秒 = 1000000000 诺秒)。
Period类是一个表示时间间隔的类,主要以较大的时间单位(日、月、年)来表示时间间隔。如果想要以较小的时间单位表示时间间隔,建议用Duration类。
注意:Period类可以计算24小时内的时间间隔,但无法展示。这是因为Period最小只支持getDays()
方法,也就是说,只能知道这段时间间隔有几天,更小的时间单位,如小时,Period不支持。
构造实例
// 获取两个时间点之间的时间间隔,与Duration相同
Period period = Period.between(LocalDate startLocalDate, LocalDate endLocalDate);
// 通过具体的年月日差值指定时间间隔,如下的方法调用会得到一个时间间隔为2年3个月1天的Period实例。
Period period = Period.of(2, 3, 1);
以指定时间单位表示对应时间间隔
场景:以天为单位获取Period实例对应时间间隔
Period period = Period.between(currentLocalDate2, requestLocalDate2);
// 计算这段时间间隔有几年
int periodInYears = period.getYears();
// 计算这段时间间隔有几个月
long periodInMonths = period.toTotalMonths();
// 没有计算天数的方法
注意:与Duration类似,Period的getDays()
方法并不是返回一段时间间隔里的总天数,而是返回这段时间里不足1月的时间的总天数。
时区相关
ZoneId是一个用于描述时区的类。
确定好时区后,时间戳信息才能正确的转化为时间信息,反之亦然。
构造实例
// 根据UTC前缀创建ZoneId实例
ZoneId zoneId1 = ZoneId.of("UTC+1");
// 根据GMT前缀创建ZoneId实例
ZoneId zoneId2 = ZoneId.of("GMT+1");
// 根据Etc/GMT前缀创建ZoneId实例
ZoneId zoneId3 = ZoneId.of("Etc/GMT-1");
注意:ZoneId不支持接受如UTC+1:00
、GMT+1:00
、Etc/GMT-1:00
等参数进行实例构造。
注意:GMT和Etc/GMT是不同的时区表示方式。
根据Instant+ZoneId获取LocalDateTime
场景:需要根据时间戳和时区信息获取本地化时间
Instant instant = Instant.now();
ZoneId zoneId = ZondId.of("UTC+8");
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
ZoneOffset是ZoneId的子类,用于表示不同时区间的时间差异。
构造实例
// 构造相差+8小时的时差
ZoneOffset zoneOffset1 = ZoneOffset.ofHours(8);
// 构造相差-2小时30分钟的时差。
ZoneOffset zoneOffset2 = ZoneOffset.ofHoursMinutes(-2, -30);
// 构造相差+1小时30分50秒的时差
ZoneOffset zoneOffset3 = ZoneOffset.ofHoursMinutesSeconds(1, 30, 50);
// 构造相差-3600秒的时差
ZoneOffset zoneOffset4 = ZoneOffset.ofTotalSeconds(-3600);
注意:当构造负时差时,相关的时分秒都要是负数
无时区时间转时间戳
对无时区时间传入时区信息,使其转换为时间戳
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 3, 11, 22, 30);
ZoneOffset zoneOffset1 = ZoneOffset.ofHours(8);
Instant instant = localDateTime.toInstant(zoneOffset1);
ZonedDateTime也是用于表示日期时间的类,与LocalDateTime类似,区别在于他带有时区信息,也就是说可以用于国际化时间转换。
构造实例
ZoneId zoneId = ZoneId.of("UTC+8");
// 根据无时区时间获取
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 3, 11, 22, 30);
ZonedDateTime zonedDateTime1 = localDateTime.atZone(zoneId);
ZonedDateTime zonedDateTime2 = ZonedDateTime.of(localDateTime, zonedId);
// 根据时间戳获取
Instant instant = Instant.now();
ZonedDateTime zonedDateTime3 = instant.atZone(zoneId);
ZonedDateTime zonedDateTime4 = ZonedDateTime.ofInstant(insant, zoneId);
在本地时间不变的情况下获取不同时区的国际化时间
场景:需要获取A地5:30
的时间戳和B地5:30
的时间戳,重点在于无时区时间相同,但不在乎是否是同一时刻。
ZoneId zoneId = ZoneId.of("UTC+8");
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 3, 11, 22, 30);
ZonedDateTime zonedDateTime1 = localDateTime.atZone(zoneId);
ZoneId zoneId2 = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime2 = zonedDateTime1.withZoneSameLocal(zoneId2);
注意:上面的例子用LocalDateTime.atZone()
也可以实现,使用withZoneSameLocal()
方法看似是多余的。但有时并不能获取到ZonedDateTime对应的LocalDateTime,这是就需要withZoneSameLocal()
方法。
在时间戳不变的情况下获取其他地区的国际化时间
场景:需要获取A地5:30
时B地的无时区时间,重点在于两个时间要在同一时刻。
ZoneId zoneId = ZoneId.of("UTC+8");
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 3, 11, 22, 30);
ZonedDateTime zonedDateTime1 = localDateTime.atZone(zoneId);
ZoneId zoneId2 = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime2 = zonedDateTime1.withZoneSameInstant(zoneId2);
获取有时区时间在当前时区的无时区时间
ZoneId zoneId = ZoneId.of("UTC+8");
LocalDateTime localDateTime = LocalDateTime.of(2022, 3, 3, 11, 22, 30);
ZonedDateTime zonedDateTime1 = localDateTime.atZone(zoneId);
ZoneId zoneId2 = ZoneId.of("UTC+1");
ZonedDateTime zonedDateTime2 = zonedDateTime2.withZoneSameLocal(zoneId2);
LocalDateTime localDateTime2 = zonedDateTime2.toLocalDateTime();
注意:上面的代码并非无意义。ZonedDateTime本质上是用LocalDateTime+时区信息来表示国际化时间。当withZoneSameLocal()
方法或其他实例构造方法被调用时,ZonedDateTime类已经完成了对LocalDateTime和时区信息的正确赋值。所以对于一个已经构造完成的ZonedDateTime实例,直接调用toLocalDateTime()
方法即可获取正确的LocalDateTime结果。