百萬年薪架構師硬核講解「Java8時間系統」設計原理與使用方法

Java平臺時間系統的設計方案


幾乎任何事物都會有“起點”這樣的概念,比如人生的起點就是我們出生的那一刻。
Java平臺時間系統的起點就是世界時間(UTC)1970年1月1日凌晨零點零分零秒。用專業的寫法是“1970-01-01T00:00:00Z”,最後的大寫字母“Z”指的是0時區的意思。
在Java平臺時間系統裡,這個起點用單詞“epoch”表示,就是“新紀元、新時代”的意思。
一般來說如果一個事物有起點,那麼通常該事物也會有一個叫做“偏移量”的概念。人一出生,就有了年齡,這就是個偏移量,一旦工作,就有了工齡,這也是個偏移量。
Java平臺時間系統就是用偏移量來表示時間的,表面上看起來有年月日時分秒,其實底層就是一個long類型的整數,就是自起點開始經過的毫秒數。
這一點可以很容易說明:

<code>Date now = new Date();
System.out.println(now);
System.out.println(now.getTime());
System.out.println(System.currentTimeMillis());/<code>


輸出結果如下:

<code>Fri Mar 06 13:52:41 CST 2020
1583473961398
1583473961398/<code>


可能有的讀者會問,那如何表示1970年以前的時間呢?
當然也是採用偏移量啊,只不過這個偏移量是個負的罷了,估計很多人都沒見過負的毫秒數,那就來看看吧。
那就把年份設置成1969年試試吧:

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


輸出結果如下:

<code>-25985142623/<code>


看到了吧,就是一個負的整數。
偏移量和時區有關嗎?
有一個更有意思的問題浮現了出來,全球有24個時區,那這個偏移量和時區有關嗎?
如果無關,則所有時區的偏移量都一樣,那時間也應該都一樣啊,可事實是都不一樣。
如果有關,則所有時區的偏移量都不一樣,那就有24個偏移量,感覺似乎也不太對。
孰對孰錯,試試便知,那就幹起來吧。
獲取上海、倫敦、芝加哥三個地方(所在時區)的時間:


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


打印出來看看:

<code>System.out.println(getDate(cn));
System.out.println(getDate(en));
System.out.println(getDate(us));/<code>


輸出結果如下:

<code>2020-2-6 13:54:17
2020-2-6 05:54:17
2020-2-5 23:54:17/<code>


可以看到,時間是正確的。
再把它們的毫秒數打印出來看看:

<code>System.out.println(cn.getTimeInMillis());
System.out.println(en.getTimeInMillis());
System.out.println(us.getTimeInMillis());/<code>


輸出結果如下:

<code>1583474057356
1583474057356
1583474057356/<code>


結論是:偏移量都一樣,和時區是無關的。那日期為啥是不同的呢?這就是時區的功勞了。
再用Java8的時間API來驗證一遍。


同樣創建三個地方的當地時間:

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


打印出來看看:

<code>System.out.println(cnldt);
System.out.println(enldt);
System.out.println(usldt);/<code>


輸出結果如下:

<code>2020-03-06T13:54:17.370
2020-03-06T05:54:17.372
2020-03-05T23:54:17.372/<code>


同樣時間是正確的。然後再打印出秒數:

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


輸出結果如下:

<code>1583474057
1583474057
1583474057/<code>


可以看到,它們經過的秒數是一樣的。
備註:中國時間東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的時候這個名字就被用了,而且表示的就是時區。
時區可以按如下的方式創建:

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


採用地理位置的方式來命名時區是比較生活化的,貌似一下子很難和時間計算聯繫在一起。
其實時區的本質不就是距離標準(0時區)時間的偏移量嘛,所以時區就是基於起點(0時區)的偏移量。這樣是不是彷彿一下具有了計算性。
這個偏移量用ZoneOffset表示,0時區偏移量是0,可以表示為:

<code>ZoneOffset.of("+0");
ZoneOffset.of("-0");/<code>


注意,雖然“+0”和“-0”在算術上是相等的,但這裡是時區格式的字符串,所以“+”和“-”是不能省略的。
0時區是時區的起點,比較特殊,因此還專門有一個字母來表示,就是大寫字母“Z”,因此可以這樣:

<code>ZoneOffset.of("Z");/<code>

相信大家都知道了“+”和“-”的意思了,那我就再贅述一遍吧。
加號(+)表示0時區東邊的時區,如中國的東8時區,可以表示為:

<code>ZoneOffset.of("+8");/<code>


減號(-)表示0時區西邊的時區,如美國的西6時區,可以表示為:

<code>ZoneOffset.of("-6");/<code>


上面的“+8”表示比標準時間早8個小時,“-6”表示比標準時間晚6個小時。
既然整小時都被支持了,那分鐘也應該被支持的啊,沒錯,分鐘也是支持的,像這樣:

<code>ZoneOffset.of("+01:30");
ZoneOffset.of("-02:20");/<code>


"+01:30"表示比標準時間早1小時30分,"-02:20"表示比標準時間晚2小時20分。
既然分鐘都支持了,那乾脆連秒也支持了吧,是的,秒也是支持的,像這樣:

<code>ZoneOffset.of("+03:40:50");
ZoneOffset.of("-04:50:30");/<code>


含義和上面一樣,只是多了個秒而已。
需要說明的是,Java8支持的時間偏移量範圍是從“-18:00”到“+18:00”,橫跨36個小時,遠超過24個時區。
理論上講,ZoneId和ZoneOffset應該具有某種聯繫,因為它們的目的是一樣的,只是從不同的角度來描述,都表示一個地方的當地時間距離標準時間的差值。


實際上ZoneOffset繼承了ZoneId,所以“Asia/Shanghai”和“+8”其實是一樣的,表示上海的當地時間比標準時間早8個小時,很簡單吧,要是都這麼簡單那就好了。
曾經混亂的地理時區及其轉換
世界時間標準是一步步建立起來的,那麼在標準建立之前,一定會有相對混亂的地方。一段時間用這個時區,一段時間又改為別的時區,而且還有可能反覆。
空口無憑?那就上證據,從愛國主義角度出發,先看中國的時區情況:

<code> 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]/<code>


我們來解釋下,這些都是什麼意思。“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次,這裡只展示一部分,這裡展示的是芝加哥的當地時間,屬於美國中部時間:

<code> 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]/<code>


可以看到首次調整是在“1883-11-18T12:09:24”的時候把時間偏移量從“-05:50:36”調整到了“-06:00”,等於回調了9分24秒,所以是“Overlap”,即重疊。
仔細看的話會發現後續的調整都集中到每年的3/4/6月份和9/10/11月份,而且都是在西5區和西6區之間的變換。
相信大家都已經猜出來了,美國是分“冬令時(正常時間)”和“夏令時”的國家。所以每年都會調整2次,那為什麼上面的最後一次調整是2008年呢?後續的調整呢?


上面那些都是歷史了,所以需要都記錄下來,其實這個調整是有規律的,因此只需要記錄下規律,而不需要記錄每次變更的日誌了。
美國芝加哥(中部時間)當地的冬令時和夏令時的變換規律是:

<code>[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]/<code>


冬令時到夏令時的轉換是在,每年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時間系統」設計原理與使用方法


時間的獲取與跨時區轉換
獲取自己所在地區的當前時間,是這樣子的:

<code>LocalDateTime.now();/<code>


Java會利用操作系統設置的地區信息。
如果要獲取指定地區的當前時間,需要自己指定一個時區(地區),是這樣子的:

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


如果知道了一個地區的時間偏移量,那就指定一個時區偏(地區)移量,也可以這樣子:

<code>LocalDateTime.now(ZoneOffset.of("-6"));/<code>


如果要獲取UTC(標準)時間,可以這樣子:

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


因為倫敦時間就是標準時間,也是0時區時間,也是沒有時區偏移量的時間,“Z”的意思就是偏移量為0。


如果在一個非常確定的情況下進行跨時區轉換時間的話,是這樣子的:

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


of方法的第一個參數是待轉換的時間,第二個參數是該時間對應的偏移量,第三個參數是轉換後的偏移量。
其實內部原理很簡單,就是加上或減去這兩個偏移量之間的差值。
由於過去很多地方都進行過時區的多次反覆變更,如果想知道某個地方過去的某個時間當時所採用的時區,可以這樣子:

<code>ZoneRules rules = ZoneId.of("Asia/Shanghai").getRules();
LocalDateTime someTime = //過去的某個時間;
ZoneOffset offset = rules.getOffset(someTime);/<code>


就是根據地區獲取到該地區的變換規則,根據規則獲取過去某個時間當時的偏移量,當然這個時間也可以是未來的時間。
這在一般情況下都會得到唯一的準確的結果,但發生在日期調整的特殊時刻時就不是這樣的了。
比如美國在夏天到來時會在某個週日的凌晨2點把時間往前調一個小時,就是從2點直接蹦到3點,時間偏移量就是從-6變為-5。


如果我們要找2點半對應的時間偏移量,其實是沒有的。因為這個時間根本就沒有出現過,是被蹦過去了。這是時間裂縫,我們等於掉到裂縫裡了。
同樣美國在冬天到來時會在某個週日的凌晨2點把時間往回調一個小時,就是從2點直接退到1點,時間偏移量就是從-5變為-6。
如果我們要找1點半對應的時間偏移量,其實是有2個。因為這個時間實際上出現過兩次,因為1點到2點又重複走了一遍。這就是時間重複,我們等於掉到重複裡了。
對於這兩種情況,系統給的是調整前的時間偏移量,而且明確說明這只是個“最佳”結果而非“正確”結果,應用程序應該自己認真對待這種情況。
系統給出的這個“最佳”結果,對於過去的時間和未來的時間都是一樣的,即在“臨界區”的時間段內選的都是調整前的時間偏移量。
這個是使用當地的時間獲取當地的時間變換規則,其實還有更麻煩的場景。像下面這個。
就是我們想知道在中國過去(或未來)的某個時間的時候,美國的芝加哥對應時間是幾點?
這時候其實需要知道在中國的這個時間的時候,美國芝加哥的時間的偏移量是多少?

因為芝加哥的時間偏移量也是反覆變化的,所以還需像上面那樣去獲取,就是這樣子:

<code>ZoneRules usaRules = ZoneId.of("America/Chicago").getRules();
LocalDateTime chinaTime = //中國過去的某個時間;/<code>


可是遺憾的是,我們不能用中國的當地時間去獲取芝加哥對應時候的時間偏移量。因為中國的時間是按中國的偏移量算出來的哦。
那怎麼辦呢?方法還是有的。有一點一定要記清楚,就是在某一瞬間,雖然全球時間各不一樣,但是經過的“毫秒數”卻都是一樣的。
所以先把中國過去的這個時間轉化為“毫秒數”,或者說轉化為那一瞬間,然後再用這一瞬間去獲取芝加哥在這一瞬間的時間偏移量。
因為這一瞬間是全球都一樣的。首先用中國的變換規則獲取中國過去那個時間的偏移量,因為從時間到瞬間的變換需要知道時間偏移量。
因為不知道時間偏移量的話,我們無法確定這個時間是哪裡的時間,可能是現在東8區的時間,也可能是1個小時前東9區的時間,還可能是1個小時後東7區的時間。
我去,好麻煩啊,先用中國變換規則和中國時間計算出那一瞬間吧,像這樣子:


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


算出的這個瞬間instant是世界通用的,然後用它去計算芝加哥在這一瞬間的時間偏移量,像這樣子:

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


現在事情已經明朗了,待轉換的時間,轉換前時間偏移量,轉換後時間偏移量這三者都有了,就變成一個確定的情況了。
方法和一開始用的是一樣的,像這樣子:

<code>ZoneOffsetTransition china2usa = ZoneOffsetTransition.of(chinaTime, chinaOffset, usaOffset);
china2usa.getDateTimeBefore();
china2usa.getDateTimeAfter();/<code>


現在終於可以說一句,時區不是頗為複雜,而是相當複雜啊。
時間系統的常用類揭秘
對系統默認時區的獲取依然是依賴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)


分享到:


相關文章: