本文目录
1 Instant
2 ZoneId
3 LocalDateTime
4 ZonedDateTime
5 关系图解
6 时间格式化
7 相关文章
Instant 表示基于世界标准时间(UTC)的时间线上的某一个时间点,通常用作事件的时间戳。
Instant 取值为 基于世界标准时间 从 1970/01/01 00:00:00.000 到 某一个时间点 经历的时长(秒 + 纳秒),例如:
instantValue = 0s:表示 1970/01/01 00:00:00.000 (UTC) 或 1970/01/01 08:00:00.000 (UTC+8)
instantValue = 3600s:表示 1970/01/01 01:00:00.000 (UTC) 或 1970/01/01 09:00:00.000 (UTC+8)
// 获取当前时间点
Instant instant = Instant.now();
// 获取指定时间点(毫秒)
Instant instant = Instant.ofEpochMilli(long epochMilli);
// 获取指定时间点(秒)
Instant instant = Instant.ofEpochSecond(long epochSecond);
// 获取时间点值(毫秒)
long instantValue = instant.toEpochMilli();
注意:当通过指定时间点的方式创建Instant对象时,参数值应该是基于世界标准时间的值。
ZoneId 表示时区,例如:北京时区为东八区,即 UTC+8 。
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取指定时区:东八区
ZoneId zoneId = ZoneId.of("+08:00");
LocalDateTime 表示一个不含时区信息的时间,其组成包含两个部分:LocalDate 和 LocalTime
LocalDate 仅包含日期部分(即 年 月 日)
LocalTime 仅包含时间部分(即 时 分 秒)
通过1、2两节内容可知,要表示一个时间,需要 Instant 和 ZoneId 两个信息,即 基于世界标准时间的时间点 和 要表示的时间的时区 信息,这样,才可以计算得到一个相应的时间信息,例如:
instantValue = 3600s:表示 1970/01/01 01:00:00.000 (UTC)
zoneId = UTC+8:表示 东八区
localDateTime = 1970/01/01 01:00:00.000(UTC) + 时区偏移量(+8小时) = 1970/01/01 09:00:00.000 (UTC+8)
// 获取当前时间点
Instant instant = Instant.now();
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取当前系统默认时区的本地时间
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zoneId);
// 快捷方式:获取当前系统默认时区的本地时间
LocalDateTime nowLocalDateTime = LocalDateTime.now();
通过上述说明可知,LocalDateTime仅保存一个时间信息,在构造其对象时,提供的 Instant 和 ZoneId 信息,只是为了计算对应时区的一个时间信息,LocalDateTime 对象构造完成后,即表示一个时间,但并不保存时区信息。因此,也可以通过直接指定时间的方式构造 LocalDateTime 对象。
// 直接指定时间
LocalDateTime localDateTime = LocalDateTime.of(year, month, dayOfMonth, hour, minute, second, nanoOfSecond);
由于 LocalDateTime 对象不保存时区信息,因此,就无法从其中获取 Instant 信息。因为没有时区信息,无法确定 LocalDateTime 表示的是哪里的时间,因此,无法确定对应的世界标准时间的时间点。例如:
1970年01月01日 16:00:00
只有一个时间信息,没有时区信息,无法知道这个时间是北京时间,还是纽约时间。
在实际应用过程中,LocalDateTime 对象通常用于时间格式化(显示时间),而不用于不同时区的时间转换。
LocalDateTime 表示一个无时区信息的时间,而 ZonedDateTime 表示一个包含时区信息的时间。
当为 LocalDateTime 附上时区信息时,即可转化为 ZonedDateTime 对象。
// 表示 localDateTime 为 UTC+8 时区的时间
ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.of("+08:00"));
Instant 也是一个仅包含时间信息的对象,因此,为 Instant 附上时区信息时,同样,可转化为 ZonedDateTime 对象。
// 基于 UTC+8 时区,将 instant 转化为 ZonedDateTime 对象。
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("+08:00"));
// 等同于如下代码
ZonedDateTime zonedDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("+08:00")).atZone(ZoneId.of("+08:00"));
由于 ZonedDateTime 包含时区信息,因此,可以从其中获取 Instant 对象信息,例如:
1970年01月01日 16:00:00 UTC+8
明确是东八区的时间,则对应的世界标准时间为 1970年01月01日 08:00:00 ,其 instantValue = 28800秒。
// 时区
ZoneId zoneId = zonedDateTime.getZone();
// 时间点
Instant instant = zonedDateTime.toInstant();
// 世界标准时间的时间点值
long timestamp = zonedDateTime.toInstant().toEpochMilli();
上图描述了 Instant、ZoneId、LocalDateTime 和 ZonedDateTime 之间的关系。
需要说明的是,Instant 和 ZoneId 结合得到 LocalDateTime 步骤中,ZoneId 仅用于确定时间偏移量,从而计算得到该时区对应的时间,并由 LocalDateTime 对象保存时间信息,而时区信息并没有在 LocalDateTime 对象保存。
常见的时间异常情况,大部分是由于时区问题没有处理正确导致。这种情况,在网络传递时间数据的时候,时常发生,其根本原因是两端基于不同的时区对时间进行处理,例如:
服务端:基于UTC,返回时间字符串信息,如 timestamp = 1970/01/01 08:00:00,对应北京时间为 1970/01/01 16:00:00。
客户端:基于本地时区,处理服务端返回的时间字符串信息,即 将 1970/01/01 08:00:00 当成北京时间,导致时间差8小时。
解决方案:以Instant值作为时间值进行存储和网络传输,各端以Instant值为基础,各自构造其所需时区的时间对象。
在 Java 8 之前,通常使用 SimpleDateFormat 对时间进行格式化,但 SimpleDateFormat 不是线程安全的类,即 在多线程环境下使用共享的 SimpleDateFormat 对象(例如 static 对象),则可能发生异常情况。
因此,为了保证线程安全,在每个线程中,需要创建一个 SimpleDateFormat 对象,保证不同线程之间不会共享一个 SimpleDateFormat 对象。或者,通过 ThreadLocal 方式,达到线程隔离效果。
在 Java 8 之后,JDK 提供了 DateTimeFormatter 类,用于提供时间格式化方法。
// 初始化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 获取 当前系统默认时区 的 本地时间
LocalDateTime nowDateTime = LocalDateTime.now();
// 格式化时间
String dateTimeString = formatter.format(nowDateTime);
// 解析 时间字符串 得到 LocalDateTime 对象
LocalDateTime dateTimeObj = LocalDateTime.from(formatter.parse(dateTimeString));
《字符串格式化:Formatter类》