JDK8新时间API的用法

JDK8新时间API的用法

马草原 710 2022-06-04

JDK8新时间API的用法

对于时间API,一般业务用到的比较少,因此平时不怎么太关注。

最近在做国际化业务,经常涉及不同时区、时间格式等问题;团队要求使用
LocalDateTime等新实践API替代Date类。

为什么用LocalDateTime替代Date类?

  1. 可变性Date类是可变的,而LocalDateTime是不可变的。这意味着一旦创建了LocalDateTime对象,就不会发生任何更改。这有助于避免在多线程环境下的并发问题。
  2. 线程安全性:由于LocalDateTime是不可变的,它可以在多线程环境下安全地使用,而不需要额外的同步操作。
  3. 可读性LocalDateTime具有更好的可读性和可理解性。它提供了明确的方法来获取和设置日期时间的各个部分,例如年、月、日、小时、分钟和秒等。
  4. 更灵活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();

注意:

  1. Instant提供了多种版本的plus重载方法,上面版本的plus方法接收两个入参:参数1为long类型变量,指代时间点平移的大小,参数2为TemporalUnit类型变量,指代某种时间单位。
  2. 上面方法调用的第二个参数DAYSChronoUnit枚举类的成员,ChronoUnit枚举实现了TemporalUnit接口,并定义了多种时间单位,小到诺秒NANOS,大到世纪CENTURIES,都被定义在其中。
  3. 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-02023-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:00GMT+1:00Etc/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结果。

lazada