目录
- 前言
- 一、时区与时间
- 1. 世界标准时:UTC、GMT、UT
- 2. 地区时:Asia/Shanghai、UTC+8
- 3. 时区:ZoneId、TimeZone
- 4. 时间偏移量:ZoneOffset
- 5. 时区简称:CTT、PRC
- 二、主要时间类
- 1. 重要时间接口:Temporal
- 2. 时间单位、时间字段、时长:ChronoUnit、ChronoField、Duration
- A .ChronoUnit
- B .ChronoField
- C .Duration
- 3. 时间点:Instant
- 4. 本地时间:LocalTime、LocalDate、LocalDateTime
- 5. 偏移量时间:OffsetTime、OffsetDateTime
- 6. 时区时间:ZonedDateTime
- 7. 动态时间(时钟):Clock——SystemClock、FixedClock、OffsetClock、TickClock
- 8. 时间属性查询:TemporalQuery
- 9. 时间属性调整:TemporalAdjuster
- 10. 时间转换(含Date)
- 总结
- 参考
前言
鉴于每次用到时间的相关方法时,都是通过谷歌搜索如何实现,自己对这些实现背后隐藏的内容知之甚少。所以,这次打算开一篇文章,学习JDK中主要的时间类,了解之前用到时间类、参数和方法代表的含义。
Java8 掌握Date与Java.time转换的核心思路,轻松解决各种时间转换问题
一、时区与时间
1. 世界标准时:UTC、GMT、UT
UTC(Coordinated Universal Time,世界标准时,世界协调时)是最主要的世界时间标准。UTC基于国际原子时,并通过不规则的加入闰秒来抵消地球自转变慢的影响。鉴于UTC已经是Java中主要的世界时表示方式,除非特殊提示否则下文中都UTC来表示世界标准时时间。
GMT(Greenwich Mean Time,格林尼治时间),由于地球每天的自转是有些不规则的,而且正在缓慢减速,因此格林尼治平时基于天文观测本身的缺陷,目前已经被原子钟报时的协调世界时(UTC)所取代。
UT(Universal Time,世界时),是一种以格林威治子夜起算的平太阳时。世界时是以地球自转为基准得到的时间尺度,其精度受到地球自转不均匀变化和极移的影响。
Instant(1.8)是jdk8时间框架的基础类,它以世界标准时UTC作为基础时区构建,基本上jdk8中的时间类都会于它打交道,支持以GMT、UT作为基础时区。
TimeZone(1.7前)它以世界标准时GMT作为基础时区构建,不支持UTC、UT时区。
用Instant来直接输出UTC当前的时间:
// Instant.now()代表UTC当前时间的秒 + 纳秒(从1970-1-1 00:00:00 开始所经过秒-纳秒数)
Instant instant = Instant.now(); // UTC(Z) ~ GMT ~ UT
System.out.println(instant);
输出如下时间:
2023-03-02T07:53:02.377Z
2. 地区时:Asia/Shanghai、UTC+8
地区时:是适用相同时区规则的地理区域,是UTC偏移量位置处的时间。在同一时刻,地区时根据UTC距当地的偏移量来计算其本地时间。
中国占据东五区、东六区、东七区、东八区、东九区5个时区,为了统一时间,1949年中华人民共和国成立后,中国大陆全境统一划为东八区,同时以北京时间作为全国唯一的标准时间。北京时间,又名中国标准时间(China Standard Time,CST,UTC+8),是我国的标准时间,比世界协调时快八小时(UTC+8)。注意CST并不能作为北京时间的时区ID来生成ZoneId,原因见时区简称。
对比一下世界标准时、其它地区时和北京时间的时间差异:
Instant instant = Instant.now();
System.out.println("UTC:" + instant);
System.out.println("纽约 UTC-5:" + instant.atZone(ZoneId.of("America/New_York")));
System.out.println("东京 UTC+9:" + instant.atZone(ZoneId.of("Asia/Tokyo")));
System.out.println("上海 UTC+8:" + instant.atZone(ZoneId.of("Asia/Shanghai")));
输出内容如下:
UTC:2023-03-02T08:37:10.736Z
纽约 UTC-5:2023-03-02T03:37:10.736-05:00[America/New_York]
东京 UTC+9:2023-03-02T17:37:10.736+09:00[Asia/Tokyo]
上海 UTC+8:2023-03-02T16:37:10.736+08:00[Asia/Shanghai]
可以通过这些网站查询各个时区以及主要城市的时区:timeanddate,时区列表
3. 时区:ZoneId、TimeZone
时区:是地球上使用的同一时间定义的国家或区域,它描述了这个国家或区域内的偏移量范围。
- ZoneId代表时区。它的内部描述了这个国家或区域的整体时间规则。 即
ZoneId
=ZoneOffset
+ZoneRules
。- ZoneRules代表这个时区的时区规则细则,包含这个国家或区域的时区偏移量的变动和这个变动的时间,不同的时间这个时区可能会有不同的时间偏移量(ZoneOffset)情况。比如
Asia/Shanghai
,Asia/Singapore
,虽然当前它们都使用的UTC+8
这个时区的区时,但是不代表这些国家或地区的曾经或者未来还会使用这个区时,我国就在1986年至1991年使用过六年夏令时,即每年在4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时整(北京夏令时),夏令时内时间将会向后调快一小时,使用+9:00时区的区时。ZoneRules中就完整的记录了这些变动。
时区和区时:时区是一个国家或区域,区时是这个国家或区域的本地时间
在Java中,以ZoneId
(1.8)和TimeZone
(1.1)来表示时区概念。
ZoneId:
当前默认时区:
ZoneId zoneId = ZoneId.systemDefault(); // 本质是调用TimeZone.getDefault().toZoneId()
TimeZone timeZone = TimeZone.getDefault();
// 可以设置程序全局的默认时区
TimeZone.setDefault(timeZone);
ZoneId(1.8):
如下程序,展示了通过时区ID获取该时区的本地时间:
System.out.println(ZonedDateTime.now(ZoneId.of("UTC"))); // 2023-03-22T07:22:03.133Z[UTC]
System.out.println(ZonedDateTime.now(ZoneId.of("GMT"))); // 2023-03-22T07:22:03.133Z[GMT]
System.out.println(ZonedDateTime.now(ZoneId.of("Asia/Shanghai"))); // 2023-03-22T15:22:03.133+08:00[Asia/Shanghai]
TimeZone(1.1):
Date类中没有时区概念 可以借助SimpleDateFormat实现:
TimeZone utc = TimeZone.getTimeZone("UTC+8");
TimeZone gmt = TimeZone.getTimeZone("GMT+8");
TimeZone ut = TimeZone.getTimeZone("UT+8");
Date date = new Date(); // 2023-04-14 11:04:17
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(utc);
System.out.println(sdf.format(date)); // 2023-04-14 03:04:17
sdf.setTimeZone(gmt);
System.out.println(sdf.format(date)); // 2023-04-14 11:04:17
sdf.setTimeZone(ut);
System.out.println(sdf.format(date)); // 2023-04-14 03:04:17
上面的程序中,UTC+8、GMT+8、UT+8三个时区并没有输出相同的时间,只有GMT+8正确的输出了当前时间,原因是TimeZone是基于GMT构建的,它并不支持UTC+8、UT+8这两种时间标准时的偏移设置。此外以UTC
作为时区ID时可以被识别成功,UT
则不行。
刚开始用的时候,由于使用UTC与UT没报错,我还以为TimeZone支持这两种世界标准时,但是通过分析源码才发现TimeZone会将所有没有解析成功的时区ID转为GMT。
private static TimeZone getTimeZone(String ID, boolean fallback) {
TimeZone tz = ZoneInfo.getTimeZone(ID); // 时区ID中仅存在UTC,不存在其偏移设置即UTC+1这种格式;不存在UT。
if (tz == null) {
tz = parseCustomTimeZone(ID); // 解析GMT的自定义偏移时间设置,如GMT+14:17
if (tz == null && fallback) {
tz = new ZoneInfo(GMT_ID, 0); // 所有未被识别的时区都会被设置为“GMT+0”
.....
}
TimeZone中可以正确使用的ID
ZoneId与TimeZone兼容性
为了兼容ZoneId带来的改变,1.8版本中为TimeZone类添加了ZoneId与TimeZone互转的方法:
ZoneId zone = ZoneId.of("Asia/Shanghai");
TimeZone timeZone = TimeZone.getTimeZone(zone);
ZoneId toZoneId = timeZone.toZoneId();
查看可用时区
如上所示,通过ZoneId和TimeZone都是通过时区ID来创建对应时区,那么你要怎么知道有哪些时区ID可用呢?
可以通过如下方式查看可用ID:
// ZoneId可用的时区ID
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
// TimeZone可用的时区ID
String[] availableIds = TimeZone.getAvailableIDs();
// 或通过ZoneInfoFile查看时区ID
String[] zoneIds = ZoneInfoFile.getZoneIds();
查看指定时区
如果要查找指定偏移量位置的时区可以通过如下方式实现(单位毫秒):
// 查找位于+8时区的所有时区ID
String[] availableIDs = ZoneInfo.getAvailableIDs(8 * 60 * 60 * 1000);
String[] availableIDs1= TimeZone.getAvailableIDs(8 * 60 * 60 * 1000);
查看时区的历史区时
我国就在1986年至1991年使用过六年夏令时(每年在4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时整(北京夏令时),时间调快一个小时)。ZoneId由于记录了这些时区的历史变动,所以它可以很轻易的得到我国实行夏令时当时的本地时间:
// 输出曾经的夏令时时间
ZoneId ctt = ZoneId.of("Asia/Shanghai");
Instant parse = Instant.parse("1986-06-01T00:00:00.000Z");
LocalDateTime localDateTime = LocalDateTime.ofInstant(parse, ctt); // 1986-06-01T09:00
ZoneOffset offset = ctt.getRules().getOffset(parse); // +09:00
4. 时间偏移量:ZoneOffset
时间偏移量,是时区和特定地点的时间差异,通常以小时和分钟为单位。由于世界各国位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差,或者时间偏移量、时区偏移量。
ZoneOffset支持的偏移量范围(-18:00 to +18:00),它表示的是从UTC开始的偏移量范围,相对于世界标准时所在的0°(经度)来说,每相隔一个时区(15°)即相差一小时,往东经一个时区,时间快1h,偏移量+1,反之相反。
ZoneOffset与ZoneId:
- ZoneId内部包含了一系列的偏移量,它表示的是这个国家和区域的历史偏移量变动情况,而且这个时区的偏移量存在持续变动的可能。
- ZoneOffset表示的是固定偏移量,这些偏移量永远是固定的。(ZoneId和ZoneOffset的区别即是ZonedDateTime和OffsetDateTime的区别)
时间偏移量通常以如下的格式显示:
- ±[hh]:[mm]
- ±[hh][mm]
- ±[hh]
- ±[h]
在Java中,表示时间偏移量的类为:ZoneOffset(1.8)、TimeZone(1.1)
- ZoneId可以通过当前时间获取这个时区的当前偏移量ZoneOffset。
- ZoneOffset表明有指定数量的时差,类中的时间偏移量以秒或毫秒数存储,如偏移量为:+02:30,那么其存储的值为(2 * 60 + 30) * 60 /s。
ZoneId、ZoneOffset:
Instant instant = Instant.now();
// 由于时区的偏移量存在变动可能,因此需要根据当前时间来获取这个时区当前的时间偏移量
ZoneOffset offset = ZoneId.of("Asia/Shanghai").getRules().getOffset(instant);
ZoneOffset zoneOffset = ZoneOffset.of("+02:30"); // ZoneOffset.ofHoursMinutes(2, 30);
TimeZone:
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+12:24"));
5. 时区简称:CTT、PRC
时区ID是代表一系列偏移量的特定区域。时区简称则是时区ID的缩写或简称。
时区简称对照表:
// ZoneId时区ID简称对照表
Map<String, String> shortIds = ZoneId.SHORT_IDS;
// ZoneInfoFile时区ID简称对照表,这也是TimeZone内置的简称表
Map<String, String> aliasMap = ZoneInfoFile.getAliasMap();
ZoneId(1.8):
上海时间(“CTT”, “Asia/Shanghai”)。
// 使用时区简称
ZoneId ctt = ZoneId.of("CTT", ZoneId.SHORT_IDS);
// 或者使用TimeZone版本的简称来获取时区ID,该表中也有("PRC", "Asia/Shanghai")
ZoneId zoneCst = ZoneId.of("CTT", ZoneInfoFile.getAliasMap());
// 或者自定义简称ID也行……
TimeZone(1.1):
// 使用时区简称
// TimeZone做了一个兼容处理,如果找不到对应的时区ID,则尝试寻找简称对照表
TimeZone timeZoneCtt = TimeZone.getTimeZone("CTT");
注意:
- ZoneId要使用时区简称,必须搭配时区简称表使用,而TimeZone则不用。
- ZoneId的简称更新后,与TimeZone中的简称存在不匹配的情况。因此在TimeZone使用一些简称时,转ZoneId可能会出现时区错乱,反之同理。
- CST(中国标准时间),在ZoneId和TimeZone中都无法正确识别,因为CST已经被美国的地方时区占用了:CST -> Central Standard Time (North America) America/Chicago。
- 一般我们要表达中国所在时区时,
Asia/Shanghai、Asia/Chongqing、UTC+8、GMT+8、UT+8
等时区ID、PRC、CTT
等时区简称、+8、+08:00
等偏移量设置都可以。
二、主要时间类
1. 重要时间接口:Temporal
Temporal:框架级接口,定义对时间对象的读写访问,比如对日期、时间、偏移量或这些对象的一些组合的读写访问。这个接口对实现的可变性没有限制,但是强烈建议将该类的实现类设为不可变(final)。
Temporal实例支持读写操作,它同样存在线程安全问题。同String
,Temporal
的所有实现类的写操作都会生产新的对象而非修改原对象,所以它们都是线程安全的。
主要实现类:
主要方法:
TemporalAccessor:
- 判断当前时间类是否支持这个时间字段
TemporalAccessor.isSupported(TemporalField field)
,如果不支持的话,那么执行参数中带TemporalField的方法都会报错。- 获取当前时间类指定时间字段的值范围
TemporalAccessor.range(TemporalField field)
- 获取当前时间类指定时间字段的值,精度为int
TemporalAccessor.get(TemporalField field)
- 获取当前时间类指定时间字段的值,精度为long
TemporalAccessor.getLong(TemporalField field)
- 获取当前时间类的指定属性值
TemporalAccessor.query(TemporalQuery<R> query)
Temporal:
- 判断当前时间类是否支持这个时间单位
Temporal.isSupported(TemporalUnit unit)
,如果不支持的话,那么执行参数中带TemporalUnit的方法都会报错。- 调整当前时间类的局部时间
Temporal.with(TemporalField field, long newValue)
- 调整时间类
Temporal.with(TemporalAdjuster adjuster)
- 时间类增加N个单位的时间
Temporal.plus(long amountToAdd, TemporalUnit unit)
- 时间类减少N个单位的时间
Temporal.minus(long amountToSubtract, TemporalUnit unit)
- 比较两个时间相差多少个时间单位
Temporal.until(Temporal endExclusive, TemporalUnit unit)
- 时间类增加N个时间长度
Temporal.plus(TemporalAmount amount)
- 时间类减少N个时间长度
Temporal.minus(TemporalAmount amount)
2. 时间单位、时间字段、时长:ChronoUnit、ChronoField、Duration
由于这三个类中有很多相同的方法,也有一些方法是基于TemporalAccessor和Temporal类的,所以可能会省略这一部分内容。
A .ChronoUnit
ChronoUnit(1.8)|TimeUnit(1.5):枚举类,代表一段日期周期的时间单位。
ChronoUnit枚举的时间单位:
NANOS("Nanos", Duration.ofNanos(1)),
MICROS("Micros", Duration.ofNanos(1000)),
MILLIS("Millis", Duration.ofNanos(1000_000)),
SECONDS("Seconds", Duration.ofSeconds(1)),
MINUTES("Minutes", Duration.ofSeconds(60)),
HOURS("Hours", Duration.ofSeconds(3600)),
HALF_DAYS("HalfDays", Duration.ofSeconds(43200)),
DAYS("Days", Duration.ofSeconds(86400)),
// ……
ChronoUnit方法使用:
ChronoUnit的使用比较简单:
- 比较两个时间类相差多少个单位
TemporalUnit.between(Temporal temporal1Inclusive, Temporal temporal2Exclusive)
,要求temporal2Exclusive参数能够转为temporal1Inclusive这个时间类,否则将会抛java.time.DateTimeException
,它实际上调用的是Temporal.until方法。
Instant instant = Instant.now();
ChronoUnit hours = ChronoUnit.HOURS;
// 时间差值计算
System.out.println("不同时间类的相差时间:" + hours.between(instant.atOffset(ZoneOffset.ofHours(-5)), instant.atOffset(ZoneOffset.ofHours(8))));
System.out.println("不同时间类的相差时间:" + hours.between(instant.atZone(ZoneId.of("-5")), instant.atZone(ZoneId.of("+8"))));
System.out.println("不同时间类的相差时间:" + hours.between(LocalDateTime.ofInstant(instant, ZoneId.of("-5")), LocalDateTime.ofInstant(instant, ZoneId.of("+8"))));
计算结果如下:
不同时间类的相差时间:0
不同时间类的相差时间:0
不同时间类的相差时间:13
ChronoUnit.between计算结果差异原因:
OffsetDateTime、ZonedDateTime在计算差值的时候,会将两个类的偏移量和时区通过OffsetDateTime.withOffsetSameInstant(ZoneOffset offset)
和ZonedDateTime.withZoneSameInstant(ZoneId zone)
方法转为相同的偏移量和时区,而后计算它们在相同时间线上的时间差值。而LocalDateTime则不然,由于它不具备任何时区和偏移量的性质,导致它就是一个固定的模糊时间(无法用于形容任何时间点上的时间),它的计算也只是比较两个时间的差值。
不同时间类的between计算:
long d1 = ChronoUnit.DAYS.between(LocalDateTime.now(), ZonedDateTime.now());
long d2 = ChronoUnit.DAYS.between(OffsetDateTime.now(), ZonedDateTime.now());
long d3 = ChronoUnit.DAYS.between(ZonedDateTime.now(), OffsetDateTime.now());
long d4 = ChronoUnit.DAYS.between(ZonedDateTime.now(), LocalDateTime.now()); // 抛出异常
在Temporal实现类中的运用:
ChronoUnit在源码里的其它时间类中用处不小,一般用在如下方法中:
- 当前时间类是否支持这个时间单位
Temporal.isSupported(TemporalUnit unit)
,如果不支持的话,那么执行下面几个方法都会报错。- 时间类增加N个单位的时间
Temporal.plus(long amountToAdd, TemporalUnit unit)
- 时间类减少N个单位的时间
Temporal.minus(long amountToSubtract, TemporalUnit unit)
- 比较两个时间相差多少个时间单位
Temporal.until(Temporal endExclusive, TemporalUnit unit)
LocalDate localDate = LocalDate.parse("2023-03-21");
System.out.println(localDate.isSupported(ChronoUnit.DAYS) && localDate.isSupported(ChronoUnit.MONTHS)
&& localDate.isSupported(ChronoUnit.YEARS)); // true
System.out.println(localDate.plus(10, ChronoUnit.DAYS)); // 2023-03-31
System.out.println(localDate.minus(1, ChronoUnit.MONTHS)); // 2023-02-21
System.out.println(localDate.until(localDate.minus(10, ChronoUnit.YEARS), ChronoUnit.YEARS)); // -10
B .ChronoField
ChronoUnit表示的是时间单位,那么ChronoField表示什么呢?
你可以把它理解为更易于人类查看和使用的友好时间字段。
我们可以通过日历知道当前是今年的几月几号星期几,通过时钟知道当前是多少时分秒,通过太阳来判断一天中的上午和下午,那么Java时间类要怎么知道并将其转换表达给我们呢?在Java8中可以通过ChronoField时间字段类实现。
通过这个枚举,我们可以知道当前时钟的秒
SECOND_OF_MINUTE
、分MINUTE_OF_HOUR
、小时HOUR_OF_DAY
,一天什么时间处于上午或下午AMPM_OF_DAY
,一年中当前时间是几月MONTH_OF_YEAR
几号DAY_OF_MONTH
星期几DAY_OF_WEEK
。java通过取模求余等操作计算时间戳处于某个ChronoField枚举的哪一个时间范围内,从而计算出更直观的时间表现形式。
ChronoField部分枚举:
//……
SECOND_OF_MINUTE("SecondOfMinute", SECONDS, MINUTES, ValueRange.of(0, 59), "second"),
MINUTE_OF_HOUR("MinuteOfHour", MINUTES, HOURS, ValueRange.of(0, 59), "minute"),
HOUR_OF_DAY("HourOfDay", HOURS, DAYS, ValueRange.of(0, 23), "hour"),
DAY_OF_WEEK("DayOfWeek", DAYS, WEEKS, ValueRange.of(1, 7), "weekday"),
DAY_OF_MONTH("DayOfMonth", DAYS, MONTHS, ValueRange.of(1, 28, 31), "day"),
MONTH_OF_YEAR("MonthOfYear", MONTHS, YEARS, ValueRange.of(1, 12), "month"),
// ……
简单提一下ValueRange.of(long minSmallest, long minLargest, long maxSmallest, long maxLargest)
的四个参数:
- minSmallest minLargest:这两个值是限定最小值范围的,表示的是最小值处于这个区间内[minSmallest, minLargest] 。
- maxSmallest maxLargest:这两个值是限定最大值范围,表示的是最大值处于这个区间内[maxSmallest, maxLargest]。如上面的月份ValueRange.of(1, 28, 31),月份的最大值就位于[28, 31]这个区间内。
获取时间字段值:
通过getFrom
方法可以获取指定时间的某个时间字段,它实际调用的是TemporalAccessor.get或TemporalAccessor.getLong方法。
LocalDateTime now = LocalDateTime.now();
ChronoField.DAY_OF_YEAR.getFrom(now);
ChronoField.DAY_OF_MONTH.getFrom(now);
ChronoField.DAY_OF_WEEK.getFrom(now);
在TemporalAccessor实现类中的运用:
ChronoField是TemporalField的实现类,广泛用于各个时间类中。一般ChronoField有如下用途:
- 判断当前时间类是否支持这个时间字段
TemporalAccessor.isSupported(TemporalField field)
,如果不支持的话,那么执行参数中带TemporalField的方法都会报错。- 获取当前时间类指定时间字段的值范围
TemporalAccessor.range(TemporalField field)
- 获取当前时间类指定时间字段的值,精度为int
TemporalAccessor.get(TemporalField field)
- 获取当前时间类指定时间字段的值,精度为long
TemporalAccessor.getLong(TemporalField field)
使用起来都比较简单,需要注意:
- get方法是获取now这个时间点的某个时间字段的,譬如是几点钟
- range则是获取处于now这个时间点下的某个时间字段的范围,譬如这个时间点所在的月有多少天。
LocalDateTime now = LocalDateTime.now(); // 2023-04-21T18:34:57.537
System.out.println(now.isSupported(ChronoField.AMPM_OF_DAY) && now.isSupported(ChronoField.DAY_OF_MONTH)); // true
// 0代表am 1代表pm
System.out.println(now.get(ChronoField.AMPM_OF_DAY)); // 1
System.out.println(now.getLong(ChronoField.MILLI_OF_SECOND)); // 537
System.out.println(now.range(ChronoField.DAY_OF_MONTH)); // 1-30 月份范围是在1——28~31,4月只有30天,所以这里是1-30
C .Duration
Duration:代表时间的持续的长度,也就是时长,存储时间字段为秒(
seconds
)和毫秒(nanoseconds
),在Duration中,1秒、1分30秒、2天10小时表示的都是时长。
Duration实例支持修改其单位和具体长度,它的所有修改操作都同String一样,会生产新的对象而非修改原对象,所以它是线程安全的。
时长计算:
Instant now = Instant.now();
Duration days = Duration.ofDays(1);
System.out.printf("时间长度:%s,增加后:%s,倍数计算:%s \n", days,
days.plus(Duration.ofHours(6)), days.multipliedBy(10));
// 时间长度:PT24H,增加后:PT30H,倍数计算:PT240H
时间计算:
Duration通过addTo
和subtractFrom
来增加或减少时长,它实际调用的是Temporal.plus和Temporal.minus方法。
System.out.printf("时间加减:%s,增加后:%s,减少后:%s \n", now, days.addTo(now), days.subtractFrom(now));
// 时间加减:2023-03-10T09:44:20.420Z,增加后:2023-03-11T09:44:20.420Z,减少后:2023-03-09T09:44:20.420Z
在TemporalAccessor实现类中的运用:
Duration是TemporalAmount的实现类。一般Duration有如下用途:
- 时间类增加N个时间长度
Temporal.plus(TemporalAmount amount)
- 时间类减少N个时间长度
Temporal.minus(TemporalAmount amount)
Duration的使用与ChronoUnit有所不同,Duration是支持计算的时长,所以它不拘泥于某个具体的时间单位,你可以在一次时间加减中,添加任意的年月日时分秒日期
Instant now = Instant.now(); // 2023-04-23T03:53:40.525Z
Duration plus = Duration.ofDays(10).plus(Duration.ofHours(10)).plus(Duration.ofMinutes(10));
System.out.println(plus); // PT250H10M
System.out.println(now.plus(plus)); // 2023-05-03T14:03:40.525Z
3. 时间点:Instant
Instant:该类表示的是UTC时间线上的某个瞬时时间点的纳秒值,也因此它能很容易的转为各种时间类。
- 同除了时钟类外的其它Temporal实现类一样,表示的是一个静态时间实例。
- Instant实现了
Temporal
和TemporalAccessor
接口的所有方法,这些方法描述和使用见ChronoUnit、ChronoField、Duration。
主要属性:
private final long seconds; // 秒
private final int nanos; // 纳秒
创建实例:
Instant无法通过构造函数初始化,可以通过如下方式创建实例(后续的时间类创建方式大同小异,后面的类省略)
- now:生成当前时间线上的时间实例
- of:从一个时间实例或时间戳转换为当前类的时间实例
- parse:根据时间格式从字符串解析时间
// Instant EPOCH = new Instant(0, 0); // EPOCH为Instant内部属性,表示时间1970-01-01 00:00:00
Instant instant = Instant.now();
Instant now = Instant.now(Clock.systemUTC());
Instant ofEpochMilli = Instant.ofEpochMilli(System.currentTimeMillis());
Instant parse = Instant.parse("2023-04-14T08:22:28.987Z");
时间对比:
时间是用秒seconds
和毫秒nanos
存储,谁的值小谁就越早,反之越晚。其它时间类的比较也类似。
boolean after = instant.isAfter(minus); // true
boolean before = instant.isBefore(plus); // true
4. 本地时间:LocalTime、LocalDate、LocalDateTime
基于ISO-8601日历系统,它不属于UTC时间线上的任意一个时间点,仅仅表示一个含义较为模糊的本地时间。
前面多次提到它表述的是模糊时间,是因为它由Instant.getEpochSecond()
+ZoneOffset.getTotalSeconds()
转化而来,且转化后并没有保存额外的偏移量时间,导致它无法直接还原为UTC时间点。通俗来讲,给你一个LocalDateTime且不告诉你这个地区的时区或偏移量,你就不可能知道这个时间描述的是哪个时区或地点的时间。在国际化项目中一般不推荐使用本地时间,如果确实需要使用本地时间的话,需要确保你知道转换成这个本地时间的时差或时区值。
- LocalDate:存储
年月日
数据,如2023-04-19。- LocalTime:存储
时分秒纳秒
数据,如13:45:30.123456789。- LocalDateTime:存储
LocalTime、LocalDate
数据,它集成了前两个类,后文方法都是以LocalDateTime为示例。- 这三个类都实现了
Temporal
和TemporalAccessor
接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。
主要属性
// LocalTime
private final byte hour;
private final byte minute;
private final byte second;
private final int nano;
// LocalDate
private final int year;
private final short month;
private final short day;
// LocalDateTime
private final LocalDate date;
private final LocalTime time;
三者转换:
// 转LocalDateTime
LocalDateTime of = LocalDateTime.of(localDate, localTime);
localDate.atTime(localTime);
localTime.atDate(localDate);
// 转LocalDate、LocalTime
of.toLocalDate();
of.toLocalTime();
获取时间字段:
由于LocalDate、LocalTime中就保存时间的年 月 日
、 时 分 秒 纳秒
属性,所以可以轻易拿到它们。根据这七个基础属性,还可以延伸出其它内容:获取当前是星期几、几月几号等。
int year = now.getYear();
int monthValue = now.getMonthValue();
Month month = now.getMonth();
DayOfWeek dayOfWeek = now.getDayOfWeek();
int dayOfMonth = now.getDayOfMonth();
时间加减:
LocalDateTime对时间的加减方法进行了扩展,它支持通过方法来直接加减年 月 日
、 时 分 秒 纳秒
,这些方法所产生的日期也同样是一个新的实例(日期不变时不会生成新实例)。
- 这些加减方法会自行处理加减溢出的情况,比如2023-04-30 + 1天,那么结果为2023-05-01。
- OffsetDateTime和ZonedDateTime 同名方法直接调用LocalDateTime方法。
// 不推荐使用这种方式来多次操作改动一个LocalDateTime对象,这种方式会频繁创建实例。
LocalDateTime plusAndMinus = now.plusYears(1).plusMonths(12).plusWeeks(1).minusDays(40)
.minusHours(1).minusMinutes(1).plusSeconds(120).plusNanos(10);
局部时间调整:
与上面的时间加减不同,with方法会直接修改LocalDate、LocalTime的属性值,并且根据变动结果生成一个新的实例对象
- 日期时间不变不会产生新对象
- OffsetDateTime和ZonedDateTime 同名方法是基于LocalDateTime的。
LocalDateTime withLocal = now.withYear(2022).withMonth(12).withDayOfMonth(23)
.withHour(10).withMinute(55).withSecond(12).withNano(123456);
5. 偏移量时间:OffsetTime、OffsetDateTime
基于ISO-8601日历系统,表示某个UTC瞬时时间点偏移后的偏移时间。
OffsetDateTime用法与LocalDateTime大致相当,可以理解为如下内容:
- OffsetDateTime = ZoneOffset + LocalDateTime
- OffsetTime= ZoneOffset + LocalTime
- 这两者都实现了
Temporal
和TemporalAccessor
接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。
主要属性:
// OffsetTime
private final LocalTime time;
private final ZoneOffset offset;
// OffsetDateTime
private final LocalDateTime dateTime;
private final ZoneOffset offset;
调整偏移量:
withOffsetSameLocal
:仅调整偏移量。
OffsetDateTime of = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(8));
System.out.println(of); // 2023-04-23T17:32:09.640+08:00
System.out.println(of.withOffsetSameLocal(ZoneOffset.ofHours(-5))); // 2023-04-23T17:32:09.640-05:00
withOffsetSameInstant
:调整偏移量时同时调整本地时间至该偏移量处。
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(8))); // 2023-04-23T17:33:31.002+08:00
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(-5))); // 2023-04-23T04:33:31.002-05:00
6. 时区时间:ZonedDateTime
基于ISO-8601日历系统,表示的是某个时区的瞬时时间点。
这个类涵盖了所有日期和时间的属性:存储精度为毫秒、有本地日期时间、时区和偏移量信息。所以它是java8time包中功能最丰富的类,上面提到的方法ZonedDateTime基本上都可以调用。
ZonedDateTime用法与LocalDateTime也差不多:
- ZonedDateTime= ZoneId + ZoneOffset + LocalDateTime
- ZonedDateTime实现了
Temporal
和TemporalAccessor
接口的所有方法,方法描述和使用见ChronoUnit、ChronoField、Duration。
主要属性:
private final LocalDateTime dateTime;
private final ZoneOffset offset;
private final ZoneId zone;
调整时区:
withZoneSameLocal
:仅调整时区。
OffsetDateTime of = OffsetDateTime.of(LocalDateTime.now(), ZoneOffset.ofHours(8));
System.out.println(of); // 2023-04-23T17:32:09.640+08:00
System.out.println(of.withOffsetSameLocal(ZoneOffset.ofHours(-5))); // 2023-04-23T17:32:09.640-05:00
withZoneSameInstant
:调整时区时同时调整本地时间为该时区的区时。
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(8))); // 2023-04-23T17:33:31.002+08:00
System.out.println(of.withOffsetSameInstant(ZoneOffset.ofHours(-5))); // 2023-04-23T04:33:31.002-05:00
7. 动态时间(时钟):Clock——SystemClock、FixedClock、OffsetClock、TickClock
Clock时钟:与前面的几种静态时间不同,它能实时获取当前时间线的时间点,包括日期和时间。时钟类虽然有时区概念,但是它的内部不会用到时区,所有输出的时间瞬时点或时间戳都是UTC+0的时间,这个时区一般会用在实例化其它时间类。
Clock是一个抽象类,它有如下4种实现类(静态内部类,只能通过Clock的静态方法创建实例):
- SystemClock:系统时钟,调用该时钟获取时间,将会实时返回**System.currentTimeMillis()**的值。
- FixedClock:静态时钟,调用该时钟的时间,将会一直返回初始化该时钟的时间。这个类是静态时间可以用来测试其它时钟类。
- OffsetClock:偏移时钟,该时钟存在一个基础时钟
Clock
和偏移时长Duration
,每次调用该时钟的时间时,将会返回基础时钟时间±偏移时间。该时钟一般用来模拟过去或未来的时间,以便触发某种操作。- TickClock:滴答时钟,该时钟存在一个基础时钟
Clock
和间隔时长Duration
,调用该时钟获取时间,将会返回已经过时间线上距离该时长的最近的时间。
实例化Clock:
Clock systemClock = Clock.systemDefaultZone();
Clock.fixed(Instant.now(), ZoneId.systemDefault());
Clock.offset(systemClock, Duration.ofDays(1));
Clock.tick(systemClock, Duration.ofMinutes(10));
以Clock实例化其它时间类:
你可能会注意到,前面提到的几种类的now(Clock clock)
方法就是以Clock为参数的构造方法,并且无参方法now()
都是通过Clock.systemUTC()
或now(Clock.systemDefaultZone())
来获取当前时间戳和本地默认时区的:
Instant instant = Instant.now(Clock.systemUTC());
LocalDateTime localDateTime = LocalDateTime.now(Clock.systemUTC());
OffsetDateTime offsetDateTime = OffsetDateTime.now(Clock.systemUTC());
ZonedDateTime zonedDateTime = ZonedDateTime.now(Clock.systemDefaultZone());
使用时钟类:
Clock类有两个常用方法,Instant instant()
获取当前的瞬时时间点、long millis()
获取当前的时间戳,方法的使用还是比较简单的,这里就主要说一下TickClock。
Clock clock = Clock.fixed(Instant.parse("2023-05-04T01:20:20Z"), ZoneId.systemDefault());
Clock tick = Clock.tick(clock, Duration.ofMinutes(3));
Clock.tickMinutes(ZoneId.systemDefault());
Clock.tickSeconds(ZoneId.systemDefault());
System.out.println(clock.instant()); // 2023-05-04T01:20:20Z
System.out.println(tick.instant()); // 2023-05-04T01:18:00Z
这里间隔时长设置为3分钟,从0分钟开始,每3分钟会滴答一次,已过时间中离20分钟最近的一次滴答是18分钟。
这里将间隔时长设置为0-59分钟内的任意时间,这个滴答时钟运行都是正常的,奇怪的是设置为61时,这里的值就变为了
2023-05-04T00:40:00Z
。源码中文档的表述是:“Obtains a clock that returns instants from the specified clock truncated to the nearest occurrence of the specified duration”,翻译过来就是:获得一个时钟,该时钟返回从指定时钟截断到最近出现的指定时长的实例。这个时钟在源码中的计算方式为:millis - Math.floorMod(millis, tickNanos / 1000_000L)
。不是很清楚这里官网的表述到底是什么意思,可以看看这篇:Why TickClock out of control when the Duration value more than 60mins
8. 时间属性查询:TemporalQuery
函数式接口:
@FunctionalInterface
public interface TemporalQuery<R> {
R queryFrom(TemporalAccessor temporal);
}
作用和功能:
TemporalQuery的作用是查询时间实例(Instant、LocaDateTime、Year等)拥有的某个属性对象,比如获取这个时间下某个时间单位的长度、最小时间精度、时区、偏移量、本地时间
等。
- 能查询所有实现Temporal、TemporalAccessor接口的类
- 查询某个类属性的前提是这个时间类支持这个属性,否则会抛出异常:java.time.DateTimeException
TemporalAccessor.query(TemporalQuery<R> query)
方法就是基于TemporalQuery。
Integer nano = Instant.now().query(temporal -> temporal.get(ChronoField.NANO_OF_SECOND)); // 获取时间的纳秒数:nanos
Integer milli = Instant.now().query(temporal -> temporal.get(ChronoField.MILLI_OF_SECOND)); // 获取时间的毫秒数:nanos / 1000_000
ZoneId query = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).query(TemporalQueries.zoneId()); // 获取时间的时区
LocalDate localDate = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")).query(TemporalQueries.localDate()); //获取本地日期
9. 时间属性调整:TemporalAdjuster
函数式接口:
@FunctionalInterface
public interface TemporalAdjuster {
Temporal adjustInto(Temporal temporal);
}
作用和功能:
TemporalAdjuster主要作用是根据当前的时间类调整来目标时间类,调整结果的类型为目标时间类。
- 适用于所有Temporal、TemporalAdjuster接口的实现类
- 为没有某个属性的时间类赋予这个属性就会抛出异常:java.time.DateTimeException
Temporal.with(TemporalAdjuster adjuster)
方法就是基于TemporalAdjuster,很多时间类的源码都用到了with这个功能。
比如我们要将LocalDateTime的时间调整为当天的最后一刻或者变更年份:
LocalDateTime now = LocalDateTime.now();
LocalDateTime adjustInto = (LocalDateTime) LocalTime.MAX.adjustInto(now); // 2023-04-19T23:59:59.999999999
LocalDateTime adjustInto1 = (LocalDateTime) Year.of(2025).adjustInto(now); // 2025-04-19T13:46:45.908
10. 时间转换(含Date)
时间转换可以参考这里:Java8 掌握Date与Java.time转换的核心思路,轻松解决各种时间转换问题
总结
最近加班严重,花了近两个月时间才把这篇文章完善得差不多了。本来打算只对简单了解一下LocalDateTime、LocalDate等常用类,没想到看源码越看越懵,没法只有多花了很多时间把整个体系了解了一下。总的来说,java.time还有很多类都没深入过,一些内容了解得还不够深,期待后续补全吧,不过像Year,YearMonth这种简单的类就不会再写文章出来了。可能后续会单独写写TemporalQuery和TemporalAdjuster的使用方法,以及各个时间类的转换、常用时间方法等等。
如果看到结尾觉得这篇文章还有用处的话,🙏请大家多支持支持,多多点赞收藏关注,球球了(_)。
参考
java.time包源码
Clock
Time zone
Class ZoneId
How to set time zone of a java.util.Date?
What’s the difference between Instant and LocalDateTime?