迄今为止最硬核的「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:
后来又将毫秒转换为秒和纳秒,证明如下图02:
所以说java8时间系统的精度并没有提升,至少在某些方面没有提升。
当毫秒被转化为秒和纳秒后,首先要加上或减去时区的时间偏移量,这个偏移量是精确到秒级的。所以不影响纳秒的数值。
然后开始计算日期和时间,日期和时间肯定要分开计算的,用秒数除以86400(每天的秒数)并取整得到的就是自1970-01-01经过的天数,这个天数可能是负的。
由于大月为31天/月,小月为30天/月,2月份为平年28天/闰年29天,所以从天数转化为年/月/日的时候也是比较繁琐的,而且正的天数是往后算,负的天数是往前算,也是不一样的。
日期这就算出来了,然后再算时间。用计算天数时剩下(不足1天)的秒数,再加上纳秒那部分,去计算出时/分/秒/纳秒,这部分的计算要相对容易些了。
这样时间(localtime)也计算出来了,在加上前面算出来的日期(localdate),就是现在的日期时间(localdatetime)了。
这就是jdk8里面的计算方法,如下图03:
时间的获取与跨时区转换
获取自己所在地区的当前时间,是这样子的:
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:
使用这个默认的时区获取系统默认时钟,如下图05:
在默认时钟里其实就是获取了当前经过的毫秒数,还是用的老方法,如下图06:
至此,毫秒数和时区都已经具备,一个具体的时间就此产生了。这不就是java时间系统的原理嘛!
localdate类揭秘,先看它的存储字段,如下图07:
只存储年/月/日三个字段。
系统当前日期的获取方法,就是用系统当前默认时钟,算出来的,如下图08:
算法也简单,从时钟里取出经过的秒数和时区偏移量对应的秒数,加起来,然后再转换为天数。
这就是自1970年1月1日起经过的天数,然后再计算出具体日期即可,如下图09:
localtime类揭秘,先看它的存储字段,如下图10:
只存储时/分/秒/纳秒四个字段。
系统当前时间的获取方法,就是用系统当前默认时钟,算出来的,如下图11:
算法也简单,从时钟里取出经过的秒数和时区偏移量对应的秒数,加起来,然后再算出最后那部分不能构成整天的剩余秒数。
将这部分秒数转换为纳秒,再加上时钟里原本的那部分纳秒,这就是不能构成整天的总纳秒,然后算出时间,如下图12:
localdatetime类揭秘,先看它的存储字段,如下图13:
只存储了日期和时间两个字段。
系统当前日期时间的获取方法,也是用系统当前默认时钟,算出来的,如下图14:
具体算法和上面算日期、算时间的一模一样。
offsetdatetime类揭秘,先看它的存储字段,如下图15:
一个本地日期时间和一个时区偏移量两个字段。
说明一下,只要是算时间的,都会用的时区偏移量,只不过是前面算localdatetime时没有存而已,这里存了。
系统当前带时区偏移量的日期时间获取方法,和之前的也完全一样,如下图16:
offsettime类揭秘,先看它的存储字段,如下图17:
一个本地时间和一个时区偏移量两个字段。
系统当前带时区偏移量的时间获取方法,和之前的也完全一样,如下图18:
zoneddatetime类揭秘,先看它的存储字段,如下图19:
一个本地日期时间、一个时区偏移量和一个地区三个字段。
这里的zoneid和zoneoffset同时出现并不意味着重复的意思,因为一个zoneid在不同的历史时期或一年中不同的时候可能对应的zoneoffset是不同的。
系统当前带地区偏移量的日期时间获取方法,和之前的也完全一样,如下图20:
zoneoffset类揭秘,先看它的存储字段,如下图21:
一个总秒数和一个偏移量id。
其本质就是偏移的秒数,但是直接用秒数在有些时候不够人性化,所以还给了个字符串类型的id,它的格式如下图22:
这种格式比较友好、比较直观,但最后还是要给算成一个总秒数。算是换了一种好的表达方式吧。
instant类揭秘,先看它的存储字段,如下图23:
一个秒数和一个纳秒数两个字段。
这两个字段的值就是从系统当前经过的“毫秒数”里算出来的。所以它是一个时刻,就是一瞬间的意思。
系统当前默认时刻的获取方法,如下图24:
可以看到是utc的时刻,即0时区的时刻。再次说明全世界任何地方的时刻都是一样的,而时间的不同就是因为时区的不同造成的时间偏移量不同。
duration类揭秘,先看它的存储字段,如下图25:
一个秒数和一个纳秒数两个字段。
这两字段存储的是一段时间(也称时长),所有这个类表示一段时间,这段时间可以是正的,也可以是负的。
period类揭秘,先看它的存储字段,如下图26:
一个年数、一个月数和一个日数三个字段。
这个类也表示一段时间(也称时长),只不过它是以对人类有意义的方式来存储,比如截止到今天,我已经工作了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:
如果要从指定的数据构建的话,用的都是of()方法,如下图28:
如果要从字符串解析的话,用的都是parse()方法,如下图29:
如果要格式化的话,用的都是format()方法,如下图30:
如果要获取指定字段的值的话,用的都是get()方法,如下图31:
如果要比较时间的早晚或相等的话,用的都是is()方法,如下图32:
如果要加上一段时间的话,用的都是plus()方法,如下图33:
如果要减去一段时间的话,用的都是minus()方法,如下图34:
如果要设置字段为特定值的话,用的都是with()方法,如下图35:
如果要附加上一些本来不含有的额外信息的话,用的都是at()方法,如下图36:
以上这些方法的含义对于不同的类是一样的,而且常用的操作基本都包括了。真是比之前的date好用太多了。
java时间系统的设计者们建议我们如果可能的话尽量使用本地时间,即localdatetime/localdate/localtime,不要使用带有时区或时间偏移量的时间,那样会增加许多复杂性。
如果确实需要处理时区的话,把时区加到用户界面(ui)层来处理。
时间系统的很多类都被设计为值类型,就是在加、减一段时间和设置指定字段的值之后,并不是修改现有实例对象,而是产生了新的实例对象,所以都是线程安全的。
作者个人见解
java8时间系统,从设计层面来看,很简单,其实越简单越好。从实现层面来看,实现原理也很简单,实现代码也不太复杂。
从api层面来看,常用操作都被支持,方法名称设计非常统一,比较人性化,不会出现每个类各自为政。
最后一点建议:
如果是自己单独使用的话,尽量使用java8的日期时间,确实好用太多了。
如果是和orm框架一起使用的话,提前测试一下,因为不一定支持,可能还要使用date。
(end)
作者现任架构师,工作11年,java技术栈,计算机基础,用心写文章,喜欢研究技术,崇尚简单快乐。追求以通俗易懂的语言解说技术,希望所有的读者都能看懂并记住。
>>> 热门文章集锦 <<<
爸爸又给spring mvc生了个弟弟叫spring webflux
【面试】吃透了这些redis知识点,面试官一定觉得你很nb(干货 | 建议珍藏)
【面试】如果你这样回答“什么是线程安全”,面试官都会对你刮目相看(建议珍藏)
【面试】迄今为止把同步/异步/阻塞/非阻塞/bio/nio/aio讲的这么清楚的好文章(快快珍藏)
【面试】一篇文章帮你彻底搞清楚“i/o多路复用”和“异步i/o”的前世今生(深度好文,建议珍藏)
>>> 玩转springboot系列文章 <<<
【玩转springboot】用好条件相关注解,开启自动配置之门
【玩转springboot】看似复杂的environment其实很简单
【玩转springboot】让错误处理重新由web服务器接管
【玩转springboot】springboot应用的启动过程一览表
【玩转springboot】通过事件机制参与springboot应用的启动过程
>>> 品spring系列文章 <<<
品spring:springboot和spring到底有没有本质的不同?
品spring:springboot轻松取胜bean定义注册的“第一阶段”
品spring:springboot发起bean定义注册的“二次攻坚战”
品spring:注解之王@configuration和它的一众“小弟们”
品spring:对@postconstruct和@predestroy注解的处理方法
品spring:对@autowired和@value注解的处理方法