迄今為止最硬核的「Java8時間系統」設計原理與使用方法

  • 2020 年 3 月 12 日
  • 筆記

為了使本篇文章更容易讓讀者讀懂,我特意寫了上一篇《任何人都需要知道的「世界時間系統」構成原理,尤其開發人員》的科普文章。本文才是重點,絕對要讀,走起!

 

 

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技術棧,電腦基礎,用心寫文章,喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。


      

>>> 熱門文章集錦 <<<

 

畢業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定時任務的思考與探索,結果尷尬了