迄今为止最硬核的「Java8时间系统」设计原理与使用方法

为了使本篇文章更容易让读者读懂,我特意写了上一篇《任何人都需要知道的「世界时间系统」构成原理,尤其开发人员》的科普文章。本文才是重点,绝对要读,走起!

Java平台时间系统的设计方案

几乎任何事物都会有“起点”这样的概念,比如人生的起点就是我们出生的那一刻。

Java平台时间系统的起点就是世界时间(UTC)1970年1月1日凌晨零点零分零秒。用专业的写法是“1970-01-01T00:00:00Z”,最后的大写字母“Z”指的是0时区的意思。

在Java平台时间系统里,这个起点用单词“epoch”表示,就是“新纪元、新时代”的意思。

一般来说如果一个事物有起点,那么通常该事物也会有一个叫做“偏移量”的概念。人一出生,就有了年龄,这就是个偏移量,一旦工作,就有了工龄,这也是个偏移量。

Java平台时间系统就是用偏移量来表示时间的,表面上看起来有年月日时分秒,其实底层就是一个long类型的整数,就是自起点开始经过的毫秒数。

这一点可以很容易说明:

Date now = new Date();
System.out.println(now);
System.out.println(now.getTime());
System.out.println(System.currentTimeMillis());

输出结果如下:

Fri Mar 06 13:52:41 CST 2020
1583473961398
1583473961398

可能有的读者会问,那如何表示1970年以前的时间呢?

当然也是采用偏移量啊,只不过这个偏移量是个负的罢了,估计很多人都没见过负的毫秒数,那就来看看吧。

那就把年份设置成1969年试试吧:

Calendar before = Calendar.getInstance();
before.set(Calendar.YEAR, 1969);
System.out.println(before.getTimeInMillis());

输出结果如下:

-25985142623

看到了吧,就是一个负的整数。

偏移量和时区有关吗?

有一个更有意思的问题浮现了出来,全球有24个时区,那这个偏移量和时区有关吗?

如果无关,则所有时区的偏移量都一样,那时间也应该都一样啊,可事实是都不一样。

如果有关,则所有时区的偏移量都不一样,那就有24个偏移量,感觉似乎也不太对。

孰对孰错,试试便知,那就干起来吧。

获取上海、伦敦、芝加哥三个地方(所在时区)的时间:

Calendar cn = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
Calendar en = Calendar.getInstance(TimeZone.getTimeZone("Europe/London"));
Calendar us = Calendar.getInstance(TimeZone.getTimeZone("America/Chicago"));

打印出来看看:

System.out.println(getDate(cn));
System.out.println(getDate(en));
System.out.println(getDate(us));

输出结果如下:

2020-2-6 13:54:17
2020-2-6 05:54:17
2020-2-5 23:54:17

可以看到,时间是正确的。

再把它们的毫秒数打印出来看看:

System.out.println(cn.getTimeInMillis());
System.out.println(en.getTimeInMillis());
System.out.println(us.getTimeInMillis());

输出结果如下:

1583474057356
1583474057356
1583474057356

结论是:偏移量都一样,和时区是无关的。那日期为啥是不同的呢?这就是时区的功劳了。

再用Java8的时间API来验证一遍。

同样创建三个地方的当地时间:

LocalDateTime cnldt = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
LocalDateTime enldt = LocalDateTime.now(ZoneId.of("Europe/London"));
LocalDateTime usldt = LocalDateTime.now(ZoneId.of("America/Chicago"));

打印出来看看:

System.out.println(cnldt);
System.out.println(enldt);
System.out.println(usldt);

输出结果如下:

2020-03-06T13:54:17.370
2020-03-06T05:54:17.372
2020-03-05T23:54:17.372

同样时间是正确的。然后再打印出秒数:

System.out.println(cnldt.toEpochSecond(ZoneOffset.of("+8")));
System.out.println(enldt.toEpochSecond(ZoneOffset.of("Z")));
System.out.println(usldt.toEpochSecond(ZoneOffset.of("-6")));

输出结果如下:

1583474057
1583474057
1583474057

可以看到,它们经过的秒数是一样的。

备注:中国时间东8时区,英国时间0时区,美国时间西6时区。

这里主要想说的是,在之前的Java中是使用毫秒来衡量偏移量的,自Java8开始就使用秒和纳秒来衡量偏移量,纳秒是指最后那一个不完整的1秒。

纳秒是10的9次方分之一秒,比毫秒精确了100万倍,所有Java8的时间系统较之以前更精确了,当然是理论上的啦。

时区是颇为复杂的

大家不要小看时区,它绝对比我们认为的“不就是差几个小时嘛”要复杂些。

时区在划分时主要考虑当地的居民生活和上班情况,所以时区是和地区有密切关联的。因此时区的名字也都以地理位置来标识的。

具体格式是:大洲或大洋名称/城市或著名地点或方位名称,如Asia/Shanghai,Europe/London,America/Chicago。

当然了也有一些不规则的,如MST7MDT、US/Hawaii、SystemV/CST6、Zulu、NZ-CHAT,也许是历史遗留问题或其它原因吧,不去深究了。

在Java8中时区用ZoneId表示,意思是一个地区的ID,ID就是标识嘛,所以我觉得ZoneId更应该理解为一个地区而非一个时区。可能有人会觉得为啥不用TimeZone来表示时区呢?遗憾的是在JDK1.1的时候这个名字就被用了,而且表示的就是时区。

时区可以按如下的方式创建:

ZoneId.of("Asia/Shanghai");
ZoneId.of("Europe/London");
ZoneId.of("America/Chicago");

采用地理位置的方式来命名时区是比较生活化的,貌似一下子很难和时间计算联系在一起。

其实时区的本质不就是距离标准(0时区)时间的偏移量嘛,所以时区就是基于起点(0时区)的偏移量。这样是不是仿佛一下具有了计算性。

这个偏移量用ZoneOffset表示,0时区偏移量是0,可以表示为:

ZoneOffset.of("+0");
ZoneOffset.of("-0");

注意,虽然“+0”和“-0”在算术上是相等的,但这里是时区格式的字符串,所以“+”和“-”是不能省略的。

0时区是时区的起点,比较特殊,因此还专门有一个字母来表示,就是大写字母“Z”,因此可以这样:

ZoneOffset.of("Z");

相信大家都知道了“+”和“-”的意思了,那我就再赘述一遍吧。

加号(+)表示0时区东边的时区,如中国的东8时区,可以表示为:

ZoneOffset.of("+8");

减号(-)表示0时区西边的时区,如美国的西6时区,可以表示为:

ZoneOffset.of("-6");

上面的“+8”表示比标准时间早8个小时,“-6”表示比标准时间晚6个小时。

既然整小时都被支持了,那分钟也应该被支持的啊,没错,分钟也是支持的,像这样:

ZoneOffset.of("+01:30");
ZoneOffset.of("-02:20");

"+01:30"表示比标准时间早1小时30分,"-02:20"表示比标准时间晚2小时20分。

既然分钟都支持了,那干脆连秒也支持了吧,是的,秒也是支持的,像这样:

ZoneOffset.of("+03:40:50");
ZoneOffset.of("-04:50:30");

含义和上面一样,只是多了个秒而已。

需要说明的是,Java8支持的时间偏移量范围是从“-18:00”到“+18:00”,横跨36个小时,远超过24个时区。

理论上讲,ZoneId和ZoneOffset应该具有某种联系,因为它们的目的是一样的,只是从不同的角度来描述,都表示一个地方的当地时间距离标准时间的差值。

实际上ZoneOffset继承了ZoneId,所以“Asia/Shanghai”和“+8”其实是一样的,表示上海的当地时间比标准时间早8个小时,很简单吧,要是都这么简单那就好了。

曾经混乱的地理时区及其转换

世界时间标准是一步步建立起来的,那么在标准建立之前,一定会有相对混乱的地方。一段时间用这个时区,一段时间又改为别的时区,而且还有可能反复。

空口无凭?那就上证据,从爱国主义角度出发,先看中国的时区情况:

 1[Overlap at 1901-01-01T00:00+08:05:43 to +08:00],
2[Gap at 1940-06-01T00:00+08:00 to +09:00],
3[Overlap at 1940-10-13T00:00+09:00 to +08:00],
4[Gap at 1941-03-15T00:00+08:00 to +09:00],
5[Overlap at 1941-11-02T00:00+09:00 to +08:00],
6[Gap at 1942-01-31T00:00+08:00 to +09:00],
7[Overlap at 1945-09-02T00:00+09:00 to +08:00],
8[Gap at 1946-05-15T00:00+08:00 to +09:00],
9[Overlap at 1946-10-01T00:00+09:00 to +08:00],
10[Gap at 1947-04-15T00:00+08:00 to +09:00],
11[Overlap at 1947-11-01T00:00+09:00 to +08:00],
12[Gap at 1948-05-01T00:00+08:00 to +09:00],
13[Overlap at 1948-10-01T00:00+09:00 to +08:00],
14[Gap at 1949-05-01T00:00+08:00 to +09:00],
15[Overlap at 1949-05-28T00:00+09:00 to +08:00],
16[Gap at 1986-05-04T02:00+08:00 to +09:00],
17[Overlap at 1986-09-14T02:00+09:00 to +08:00],
18[Gap at 1987-04-12T02:00+08:00 to +09:00],
19[Overlap at 1987-09-13T02:00+09:00 to +08:00],
20[Gap at 1988-04-17T02:00+08:00 to +09:00],
21[Overlap at 1988-09-11T02:00+09:00 to +08:00],
22[Gap at 1989-04-16T02:00+08:00 to +09:00],
23[Overlap at 1989-09-17T02:00+09:00 to +08:00],
24[Gap at 1990-04-15T02:00+08:00 to +09:00],
25[Overlap at 1990-09-16T02:00+09:00 to +08:00],
26[Gap at 1991-04-14T02:00+08:00 to +09:00],
27[Overlap at 1991-09-15T02:00+09:00 to +08:00]

我们来解释下,这些都是什么意思。“Overlap”是重叠的意思,比如我把时间从9点调整到8点,那么从8点到9点这1个小时会再走一遍,这就是时间重叠。

“Gap”是裂缝的意思,比如我把时间从9点调整到10点,那么从9点到10点这1个小时就不用走了,相当于直接蹦过去了,这就是时间裂缝。

再进一步说,有重叠的说明时间是往回(后)调了,有裂缝的说明时间是往早(前)调了。

所以,“1901-01-01T00:00+08:05:43 to +08:00”表达的意思是,中国在“1901-01-01T00:00”的时刻,把我们的时间偏移量从“+08:05:43”调整到“+08:00”,就是往回调整了5分43秒。所以是“Overlap”,即重叠。

中国后续的全部都是在东8时区和东9时区之间的调整,最后一次是在“1991年09月15日凌晨02点00分”从“+09:00(东9区)”到“+08:00(东8区)”,自此直到现在,中国都是使用的东8区时间。

这些都是已经发生过的历史,Java时间系统在设计时不可能不管它的,是要支持的,所以我说时区还是有点复杂的。哈哈,历史的包袱还是有点沉重的。

美国啊,就更复杂了,中国好歹只有北京时间,美国的时间就不统一了,有东部时间、中部时间、山地时间、太平洋时间、阿拉斯加时间、夏威夷时间。

而且它的时区变换也是异常多的,大概将近200次,这里只展示一部分,这里展示的是芝加哥的当地时间,属于美国中部时间:

 1[Overlap at 1883-11-18T12:09:24-05:50:36 to -06:00],
2
3[Gap at 1918-03-31T02:00-06:00 to -05:00],
4[Overlap at 1918-10-27T02:00-05:00 to -06:00],
5[Gap at 1919-03-30T02:00-06:00 to -05:00],
6[Overlap at 1919-10-26T02:00-05:00 to -06:00],
7[Gap at 1920-06-13T02:00-06:00 to -05:00],
8[Overlap at 1920-10-31T02:00-05:00 to -06:00],
9[Gap at 1921-03-27T02:00-06:00 to -05:00],
10[Overlap at 1921-10-30T02:00-05:00 to -06:00],
11[Gap at 1922-04-30T02:00-06:00 to -05:00],
12[Overlap at 1922-09-24T02:00-05:00 to -06:00],
13
14。。。。。。。。。。
15
16[Gap at 2005-04-03T02:00-06:00 to -05:00],
17[Overlap at 2005-10-30T02:00-05:00 to -06:00],
18[Gap at 2006-04-02T02:00-06:00 to -05:00],
19[Overlap at 2006-10-29T02:00-05:00 to -06:00],
20[Gap at 2007-03-11T02:00-06:00 to -05:00],
21[Overlap at 2007-11-04T02:00-05:00 to -06:00],
22[Gap at 2008-03-09T02:00-06:00 to -05:00],
23[Overlap at 2008-11-02T02:00-05:00 to -06:00]

可以看到首次调整是在“1883-11-18T12:09:24”的时候把时间偏移量从“-05:50:36”调整到了“-06:00”,等于回调了9分24秒,所以是“Overlap”,即重叠。

仔细看的话会发现后续的调整都集中到每年的3/4/6月份和9/10/11月份,而且都是在西5区和西6区之间的变换。

相信大家都已经猜出来了,美国是分“冬令时(正常时间)”和“夏令时”的国家。所以每年都会调整2次,那为什么上面的最后一次调整是2008年呢?后续的调整呢?

上面那些都是历史了,所以需要都记录下来,其实这个调整是有规律的,因此只需要记录下规律,而不需要记录每次变更的日志了。

美国芝加哥(中部时间)当地的冬令时和夏令时的变换规律是:

[Gap -06:00 to -05:00, SUNDAY on or after MARCH 8 at 02:00 WALL, standard offset -06:00],
[Overlap -05:00 to -06:00, SUNDAY on or after NOVEMBER 1 at 02:00 WALL, standard offset -06:00]

冬令时到夏令时的转换是在,每年3月8日及其之后最近的一个周日凌晨2点,把时区从“-6”变到“-5”,即提前1小时,所以是“Gap”裂缝。

夏令时到冬令时的转换是在,每年11月1日及其之后最近的一个周日凌晨2点,把时区从“-5”变到“-6”,即延后1小时,所以是“Overlap”重叠。

“standard offset -06:00”的意思是,这里(当地)的标准时间偏移量是比UTC晚6个小时,为了照顾当地人们的生活和上班习惯,在夏天到来时,把时间提前1个小时。

“WALL”这个单词是墙的意思,所以“at 02:00 WALL”的意思就是在你看到墙上挂的钟表是凌晨2点的时候。是对当前正在使用(还未调整)的时间的一种指代吧。

上面那些已经记录下来的转换历史日志,是为了对过去时间的计算用的,而这个转换规则,是为了对未来的时间计算用的。

还好中国没有冬令时和夏令时的概念,中国只是改变了上下班的时间,冬天下班早些,因此中国没有转换规则,一年四季都是比UTC早8小时。

“当地时间”的计算方法

在Java时间系统里,时间就是自“时间起点”开始经过的毫秒数,这对全球24个时区都是一样的。

如果把这个毫秒数直接转化为时间,它对应的就是UTC时间,即0时区的时间,也是英国伦敦的时间。

如果某地不是位于0时区的话,那就再加上或减去当地时区对应的时间偏移量,得到的就是当地时间。

比如中国就是“毫秒数”再加上8个小时对应的毫秒数,美国中部就是”毫秒数“再减去6个小时对应的毫秒数。

不要以为这样就完事了,历史上同一个地方的时区都是比较混乱的,可能反复变换过几十次甚至上百次,那么这个地方对应的时区到底该怎么取呢?

还好,上面说了,Java时间系统已经记录下了每个地方时区变更历史日志了,这些反复的变更其实构成了一个个连续的区间。

每个区间的两端都是一个日期(时间),其实也是一个“毫秒数”。这样当我们拿到一个时间“毫秒数”后,就去和这个地方的所有变更区间两端的“毫秒数”进行比对。

确认出我们拿到的这个“毫秒数”落到了哪个区间,然后就使用这个区间对应的时区时间偏移量即可。这样所有的历史(过去的)时间就都算出来了。

那对于未来的时间呢?像美国那样的有冬令时和夏令时变换规则的,就按规则去计算。像中国这种没有变换规则的,就按历史上最后一次变换后对应的时区时间偏移量去计算。

即如果不出意外的话,中国永远是采用东8区,时间永远比UTC早8小时。

从“毫秒数”计算出具体时间

首先需要说明的是,Java8获取的还是毫秒级别的偏移量,而且和之前的方法是一样,并不是直接获取的纳秒。

证明如下图01:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

后来又将毫秒转换为秒和纳秒,证明如下图02:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

所以说Java8时间系统的精度并没有提升,至少在某些方面没有提升。

当毫秒被转化为秒和纳秒后,首先要加上或减去时区的时间偏移量,这个偏移量是精确到秒级的。所以不影响纳秒的数值。

然后开始计算日期和时间,日期和时间肯定要分开计算的,用秒数除以86400(每天的秒数)并取整得到的就是自1970-01-01经过的天数,这个天数可能是负的。

由于大月为31天/月,小月为30天/月,2月份为平年28天/闰年29天,所以从天数转化为年/月/日的时候也是比较繁琐的,而且正的天数是往后算,负的天数是往前算,也是不一样的。

日期这就算出来了,然后再算时间。用计算天数时剩下(不足1天)的秒数,再加上纳秒那部分,去计算出时/分/秒/纳秒,这部分的计算要相对容易些了。

这样时间(LocalTime)也计算出来了,在加上前面算出来的日期(LocalDate),就是现在的日期时间(LocalDateTime)了。

这就是JDK8里面的计算方法,如下图03:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

时间的获取与跨时区转换

获取自己所在地区的当前时间,是这样子的:

LocalDateTime.now();

Java会利用操作系统设置的地区信息。

如果要获取指定地区的当前时间,需要自己指定一个时区(地区),是这样子的:

LocalDateTime.now(ZoneId.of("America/Chicago"));

如果知道了一个地区的时间偏移量,那就指定一个时区偏(地区)移量,也可以这样子:

LocalDateTime.now(ZoneOffset.of("-6"));

如果要获取UTC(标准)时间,可以这样子:

LocalDateTime.now(ZoneId.of("Europe/London"));
LocalDateTime.now(ZoneOffset.of("Z"));

因为伦敦时间就是标准时间,也是0时区时间,也是没有时区偏移量的时间,“Z”的意思就是偏移量为0。

如果在一个非常确定的情况下进行跨时区转换时间的话,是这样子的:

ZoneOffsetTransition zot = ZoneOffsetTransition.of(LocalDateTime.now().withNano(0), ZoneOffset.of("+8"), ZoneOffset.of("-6"));
zot.getDateTimeBefore();
zot.getDateTimeAfter();

of方法的第一个参数是待转换的时间,第二个参数是该时间对应的偏移量,第三个参数是转换后的偏移量。

其实内部原理很简单,就是加上或减去这两个偏移量之间的差值。

由于过去很多地方都进行过时区的多次反复变更,如果想知道某个地方过去的某个时间当时所采用的时区,可以这样子:

ZoneRules rules = ZoneId.of("Asia/Shanghai").getRules();
LocalDateTime someTime = //过去的某个时间;
ZoneOffset offset = rules.getOffset(someTime);

就是根据地区获取到该地区的变换规则,根据规则获取过去某个时间当时的偏移量,当然这个时间也可以是未来的时间。

这在一般情况下都会得到唯一的准确的结果,但发生在日期调整的特殊时刻时就不是这样的了。

比如美国在夏天到来时会在某个周日的凌晨2点把时间往前调一个小时,就是从2点直接蹦到3点,时间偏移量就是从-6变为-5。

如果我们要找2点半对应的时间偏移量,其实是没有的。因为这个时间根本就没有出现过,是被蹦过去了。这是时间裂缝,我们等于掉到裂缝里了。

同样美国在冬天到来时会在某个周日的凌晨2点把时间往回调一个小时,就是从2点直接退到1点,时间偏移量就是从-5变为-6。

如果我们要找1点半对应的时间偏移量,其实是有2个。因为这个时间实际上出现过两次,因为1点到2点又重复走了一遍。这就是时间重复,我们等于掉到重复里了。

对于这两种情况,系统给的是调整前的时间偏移量,而且明确说明这只是个“最佳”结果而非“正确”结果,应用程序应该自己认真对待这种情况。

系统给出的这个“最佳”结果,对于过去的时间和未来的时间都是一样的,即在“临界区”的时间段内选的都是调整前的时间偏移量。

这个是使用当地的时间获取当地的时间变换规则,其实还有更麻烦的场景。像下面这个。

就是我们想知道在中国过去(或未来)的某个时间的时候,美国的芝加哥对应时间是几点?

这时候其实需要知道在中国的这个时间的时候,美国芝加哥的时间的偏移量是多少?

因为芝加哥的时间偏移量也是反复变化的,所以还需像上面那样去获取,就是这样子:

ZoneRules usaRules = ZoneId.of("America/Chicago").getRules();
LocalDateTime chinaTime = //中国过去的某个时间;

可是遗憾的是,我们不能用中国的当地时间去获取芝加哥对应时候的时间偏移量。因为中国的时间是按中国的偏移量算出来的哦。

那怎么办呢?方法还是有的。有一点一定要记清楚,就是在某一瞬间,虽然全球时间各不一样,但是经过的“毫秒数”却都是一样的。

所以先把中国过去的这个时间转化为“毫秒数”,或者说转化为那一瞬间,然后再用这一瞬间去获取芝加哥在这一瞬间的时间偏移量。

因为这一瞬间是全球都一样的。首先用中国的变换规则获取中国过去那个时间的偏移量,因为从时间到瞬间的变换需要知道时间偏移量。

因为不知道时间偏移量的话,我们无法确定这个时间是哪里的时间,可能是现在东8区的时间,也可能是1个小时前东9区的时间,还可能是1个小时后东7区的时间。

我去,好麻烦啊,先用中国变换规则和中国时间计算出那一瞬间吧,像这样子:

ZoneRules chinaRules = ZoneId.of("Asia/Shanghai").getRules();
ZoneOffset chinaOffset = chinaRules.getOffset(chinaTime);
Instant instant = chinaTime.toInstant(chinaOffset);

算出的这个瞬间instant是世界通用的,然后用它去计算芝加哥在这一瞬间的时间偏移量,像这样子:

ZoneRules usaRules = ZoneId.of("America/Chicago").getRules();
ZoneOffset usaOffset = usaRules.getOffset(instant);

现在事情已经明朗了,待转换的时间,转换前时间偏移量,转换后时间偏移量这三者都有了,就变成一个确定的情况了。

方法和一开始用的是一样的,像这样子:

ZoneOffsetTransition china2usa = ZoneOffsetTransition.of(chinaTime, chinaOffset, usaOffset);
china2usa.getDateTimeBefore();
china2usa.getDateTimeAfter();

现在终于可以说一句,时区不是颇为复杂,而是相当复杂啊。

时间系统的常用类揭秘

对系统默认时区的获取依然是依赖TimeZone这个很早期的类,如下图04:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

使用这个默认的时区获取系统默认时钟,如下图05:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

在默认时钟里其实就是获取了当前经过的毫秒数,还是用的老方法,如下图06:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

至此,毫秒数和时区都已经具备,一个具体的时间就此产生了。这不就是Java时间系统的原理嘛!

LocalDate类揭秘,先看它的存储字段,如下图07:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

只存储年/月/日三个字段。

系统当前日期的获取方法,就是用系统当前默认时钟,算出来的,如下图08:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

算法也简单,从时钟里取出经过的秒数和时区偏移量对应的秒数,加起来,然后再转换为天数。

这就是自1970年1月1日起经过的天数,然后再计算出具体日期即可,如下图09:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

LocalTime类揭秘,先看它的存储字段,如下图10:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

只存储时/分/秒/纳秒四个字段。

系统当前时间的获取方法,就是用系统当前默认时钟,算出来的,如下图11:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

算法也简单,从时钟里取出经过的秒数和时区偏移量对应的秒数,加起来,然后再算出最后那部分不能构成整天的剩余秒数。

将这部分秒数转换为纳秒,再加上时钟里原本的那部分纳秒,这就是不能构成整天的总纳秒,然后算出时间,如下图12:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

LocalDateTime类揭秘,先看它的存储字段,如下图13:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

只存储了日期和时间两个字段。

系统当前日期时间的获取方法,也是用系统当前默认时钟,算出来的,如下图14:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

具体算法和上面算日期、算时间的一模一样。

OffsetDateTime类揭秘,先看它的存储字段,如下图15:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个本地日期时间和一个时区偏移量两个字段。

说明一下,只要是算时间的,都会用的时区偏移量,只不过是前面算LocalDateTime时没有存而已,这里存了。

系统当前带时区偏移量的日期时间获取方法,和之前的也完全一样,如下图16:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

OffsetTime类揭秘,先看它的存储字段,如下图17:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个本地时间和一个时区偏移量两个字段。

系统当前带时区偏移量的时间获取方法,和之前的也完全一样,如下图18:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

ZonedDateTime类揭秘,先看它的存储字段,如下图19:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个本地日期时间、一个时区偏移量和一个地区三个字段。

这里的ZoneId和ZoneOffset同时出现并不意味着重复的意思,因为一个ZoneId在不同的历史时期或一年中不同的时候可能对应的ZoneOffset是不同的。

系统当前带地区偏移量的日期时间获取方法,和之前的也完全一样,如下图20:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

ZoneOffset类揭秘,先看它的存储字段,如下图21:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个总秒数和一个偏移量Id。

其本质就是偏移的秒数,但是直接用秒数在有些时候不够人性化,所以还给了个字符串类型的Id,它的格式如下图22:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

这种格式比较友好、比较直观,但最后还是要给算成一个总秒数。算是换了一种好的表达方式吧。

Instant类揭秘,先看它的存储字段,如下图23:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个秒数和一个纳秒数两个字段。

这两个字段的值就是从系统当前经过的“毫秒数”里算出来的。所以它是一个时刻,就是一瞬间的意思。

系统当前默认时刻的获取方法,如下图24:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

可以看到是UTC的时刻,即0时区的时刻。再次说明全世界任何地方的时刻都是一样的,而时间的不同就是因为时区的不同造成的时间偏移量不同。

Duration类揭秘,先看它的存储字段,如下图25:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个秒数和一个纳秒数两个字段。

这两字段存储的是一段时间(也称时长),所有这个类表示一段时间,这段时间可以是正的,也可以是负的。

Period类揭秘,先看它的存储字段,如下图26:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

一个年数、一个月数和一个日数三个字段。

这个类也表示一段时间(也称时长),只不过它是以对人类有意义的方式来存储,比如截止到今天,我已经工作了10年9个月6天啦。

Duration类和Period类都表示一段时间,除了表达方式上的不同之外,还有一个重要的点,Duration类在进行加减的时候,都是加减的精确时间,比如1天就是24小时。

Period类在进行加减的时候,加减的都是概念上的时间,特别是在时区调整的时候,它会维持当地时间的合理性,而Duration类则不会。

比如夏令时到来,在时区即将提前1一个的时候,在18:00的时候加上1天,如果是Period类,则加完后是第二天的18:00,他会自动处理时区提前产生的裂缝。

如果是Duration类,则加完后是第二天的19:00,它是精确的加上了24小时,又由于时区提前产生了1小时的裂缝,因此等于加上了25小时。

Period类的年数/月数/日数三个字段之间,互相不影响,每个都可以随意的为正数或负数。

Year类只存了一个年份、YearMonth类只存了年月、MonthDay类只存了月日,这些都是在特定情况下会用到的类,它们的情况和大多数人理解的一样。

常用的时间操作

如果要获取当前时间的话,用的都是now()方法,默认是本地时区,也可以指定别的时区,如下图27:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要从指定的数据构建的话,用的都是of()方法,如下图28:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要从字符串解析的话,用的都是parse()方法,如下图29:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要格式化的话,用的都是format()方法,如下图30:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要获取指定字段的值的话,用的都是get()方法,如下图31:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要比较时间的早晚或相等的话,用的都是is()方法,如下图32:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要加上一段时间的话,用的都是plus()方法,如下图33:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要减去一段时间的话,用的都是minus()方法,如下图34:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要设置字段为特定值的话,用的都是with()方法,如下图35:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

如果要附加上一些本来不含有的额外信息的话,用的都是at()方法,如下图36:

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

以上这些方法的含义对于不同的类是一样的,而且常用的操作基本都包括了。真是比之前的Date好用太多了。

Java时间系统的设计者们建议我们如果可能的话尽量使用本地时间,即LocalDateTime/LocalDate/LocalTime,不要使用带有时区或时间偏移量的时间,那样会增加许多复杂性。

如果确实需要处理时区的话,把时区加到用户界面(UI)层来处理。

时间系统的很多类都被设计为值类型,就是在加、减一段时间和设置指定字段的值之后,并不是修改现有实例对象,而是产生了新的实例对象,所以都是线程安全的。

作者个人见解

Java8时间系统,从设计层面来看,很简单,其实越简单越好。从实现层面来看,实现原理也很简单,实现代码也不太复杂。

从API层面来看,常用操作都被支持,方法名称设计非常统一,比较人性化,不会出现每个类各自为政。

最后一点建议:

如果是自己单独使用的话,尽量使用Java8的日期时间,确实好用太多了。

如果是和ORM框架一起使用的话,提前测试一下,因为不一定支持,可能还要使用Date。

(END)

作者现任架构师,工作11年,Java技术栈,计算机基础,用心写文章,喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。

迄今为止最硬核的「Java8时间系统」设计原理与使用方法

>>> 热门文章集锦 <<<

毕业10年,我有话说

我是一个协程

我是一个跳表

线程池开门营业招聘开发人员的一天

递归 —— 你值得拥有

迄今为止最好理解的ZooKeeper入门文章

基于角色的访问控制(RBAC)

彻彻底底给你讲明白啥是SpringMvc异步处理

【面试】我是如何面试别人List相关知识的,深度有点长文

我是如何在毕业不久只用1年就升为开发组长的

爸爸又给Spring MVC生了个弟弟叫Spring WebFlux

【面试】我是如何在面试别人Spring事务时“套路”对方的

【面试】Spring事务面试考点吐血整理(建议珍藏)

【面试】吃透了这些Redis知识点,面试官一定觉得你很NB(干货 | 建议珍藏)

【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)

【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)

【面试】一篇文章帮你彻底搞清楚“I/O多路复用”和“异步I/O”的前世今生(深度好文,建议珍藏)

【面试】如果把线程当作一个人来对待,所有问题都瞬间明白了

Java多线程通关———基础知识挑战

品Spring:帝国的基石

>>> 玩转SpringBoot系列文章 <<<

【玩转SpringBoot】配置文件yml的正确打开姿势

【玩转SpringBoot】用好条件相关注解,开启自动配置之门

【玩转SpringBoot】给自动配置来个整体大揭秘

【玩转SpringBoot】看似复杂的Environment其实很简单

【玩转SpringBoot】翻身做主人,一统web服务器

【玩转SpringBoot】让错误处理重新由web服务器接管

【玩转SpringBoot】SpringBoot应用的启动过程一览表

【玩转SpringBoot】通过事件机制参与SpringBoot应用的启动过程

【玩转SpringBoot】异步任务执行与其线程池配置

 

>>> 品Spring系列文章 <<<

品Spring:帝国的基石

品Spring:bean定义上梁山

品Spring:实现bean定义时采用的“先进生产力”

品Spring:注解终于“成功上位”

品Spring:能工巧匠们对注解的“加持”

品Spring:SpringBoot和Spring到底有没有本质的不同?

品Spring:负责bean定义注册的两个“排头兵”

品Spring:SpringBoot轻松取胜bean定义注册的“第一阶段”

品Spring:SpringBoot发起bean定义注册的“二次攻坚战”

品Spring:注解之王@Configuration和它的一众“小弟们”

品Spring:bean工厂后处理器的调用规则

品Spring:详细解说bean后处理器

品Spring:对@PostConstruct和@PreDestroy注解的处理方法

品Spring:对@Resource注解的处理方法

品Spring:对@Autowired和@Value注解的处理方法

品Spring:真没想到,三十步才能完成一个bean实例的创建

品Spring:关于@Scheduled定时任务的思考与探索,结果尴尬了

上一篇:ovirt系统磁盘删除后清理功能验证步骤


下一篇:【腾讯Bugly干货分享】iOS黑客技术大揭秘