從頭到尾說一說Java時間日期體系的前世今生

阿里妹導讀
在計算機領域作者重新梳理了計算機世界裡日期時間體系的前世今生。
突擊檢查

如下程式碼輸出什麼,機器當下所設定的時區為美國時區,在北京時間 2024-12-07 11:20:51 時,傳入字串“2024-12-07 11:46:36”。最終輸出應該是true,還是false呢?

前言
約38億年前地球出現生命體,約46億年前太陽系形成,大約138億年前宇宙大爆炸,那再往前呢?想起呂秀才對姬無命發出靈魂之問『時間是否有開端,宇宙是否有盡頭』。施一公曾經在一次演講中說,宇宙中從來不存在時間,只存在運動。地球公轉太陽一圈是一年,這是運動,地球自轉一圈是一天,這也是運動。從來就沒有時間,或者說時間就是空間。
『三十年春,秦晉圍鄭。鄭伯使燭之武如秦』兩千多年前我們就以時間記事,在造物主已經締造的這一片井然有序的世界裡,我們憑空創建出一個新的概念,並不斷嘗試融入這個世界體系–沙漏、水鍾、日晷等等。今天站在計算機這個領域,也讓我們重新梳理一遍,計算機世界裡日期時間體系的前世今生。
日期從1970 年1月1日說起
任何一個軟體開發人員對這個時間應該都不陌生,有時我們忘記初始化或者忘記賦值時,日期就會顯示為1970-01-01,我們也叫日期初始值。那為什麼日期的初始值是從1970-01-01開始呢?有一個說法是說遵循了Unix的時間計數,Unix認為 1970年1月1日0點 [1]是時間紀元,那為什麼Unix要以這個時間為準呢?
有一處說法是說,當時操作性系統都是32位,如果每一個數值代表一秒,那麼最多可以表示2^32-1,也就是2147483647秒,換算成年大概是68年。而Unix系統就是由Ken Thompson、Dennis Ritchie和Douglas McIlroy等人在貝爾實驗室開發於1969年開發的,他們為了讓時間儘可能的多利用起來,便用了下一年,即 1970年1月1日作為開始,然後這個約定也逐步延伸到其他各個計算機領域。
時間從GMT與UTC說起
聊完日期我們再來看時間,愛好體育的應該都知道,看歐冠得半夜起來看,看NBA得早上起來看,現在是北京時間的14點,同時也是紐約時間的凌晨1點半。那是因為我們各地處不同時區,那時區以什麼為初始劃分的呢?
GMT 格林威治時間

GMT的全稱是 Greenwich Mean Time [2]即格林威治標準時間,是一種與地球自轉相關、以太陽日為單位的時間標準。在十七世紀,格林威治皇家天文臺為了海上霸權的擴張計劃,選擇了穿過英國倫敦格林威治天文臺子午儀中心的一條經線作為零度參考線,也就是我們教科書上記載的本初子午線。

並約定從本初子午線起,經度每向東或者向西間隔15°,就劃分一個新的時區[3],每個時區間隔1小時,在這個區域內,大家使用同樣的標準時間。但各個國家也會基於各個國家的情況拆分或合併時區,比如中國橫跨5個時區,但我們統一使用東八區;而美國則有東部時間、西部時間、夏威夷時間等等。
從 1924 年開始,格林威治天文臺每小時就會向全世界播報時間,最終截止到 1979 年。至於為什麼會終止,自然有它的缺點和侷限性,那我們就得聊聊UTC時間了。
UTC 世界協調時間

UTC的全稱是 Coordinated Universal Time [4]協調世界時間,也稱世界標準時間。據說按英語的簡稱是CUT,按法語的簡稱是TUC,然後大家相互拉扯一波後,統一叫了UTC。

上述所說GMT時間是以地球自轉與圍太陽公轉來計時的,GMT時間認為地球自轉一圈是24*3600秒,而地球的運動軌跡受很多方面影響,比如潮汐摩擦、氣象變化、地震及地質活動等等,運動的時間週期並不是完全規律和相同的。這樣會導致其實一天並不完全是24*3600秒,這樣平均算下來GMT的一秒就不是完全意義上最精確的一秒。但偏差通常也不會很大,基本為毫秒級偏差,但日積月累如果不加以扶正,就會越差越遠。
而UTC的計數是基於 原子鐘(Atomic Clock) [5]的計數,比如銫原子鐘採用銫-133原子的特性,在特定能級躍遷時會產生一個非常確定的頻率9,192,631,770赫茲。然後基於銫-133原子的運動經過換算確定出我們需要的時間週期,據說這種誤差可達每百萬年內不到一秒。
UTC 最終由兩部分構成:原子時間與世界時間。原子時間基於原子鐘,來標準化我們鐘錶中每一秒時間前進的資料;世界時間是結合GMT時間,我們用多少個原子時來決定一個地球日的時間長度。從1972年開始,UTC被正式採用為國際標準時間。這年實施了一種新的時間調整機制,包括使用閏秒[6]以便對齊地球自轉與原子時間。
JDK 時間日期的發展史
糟糕的java.util.Date
說起Date那可是JDK的正牌嫡系,從1.0開始就一直存在並延續至今。但只要大家用過一些程式碼掃描工具,基本都是在提示你儘量不要使用Date。在oracle的官方JDK文件中,有超過一半的函式都是deprecated,要細說Date的問題,那可真是一言難盡。

不能單獨表示日期或時間

Sat Dec 07 17:36:58 CST 2024 這是我們輸出new Date()之後的資料,因為Date本質是某一個時刻的時間戳,導致它不能單獨表示日期,更不能表示不帶日期的時間。

令人捉摸不透的API

單就Date的方法名來看,應該是非常友好的。它提供了getYear(), getDay()等等,你但凡用過一次,一定讓你抓狂。
publicstaticvoidmain(String[] args){    Date date = new Date();// 輸出 6    System.out.println(date.getDay());// 輸出 11    System.out.println(date.getMonth());// 輸出 124    System.out.println(date.getYear());}
day和month是從0開始計數的,所以月最大是11,日最大是30,年輸出124是因為2024年距離1900年有124年。至於為什麼是減1900,有知道的小夥伴評論區打出來😂。

不支援時區設定

Date now = Calendar.getInstance(Locale.CHINA).getTime();
曾經寫過一段這樣的程式碼,取當前的中國時間,被老闆臭罵一頓。。。Date的本質是一個時間戳。當前此時此刻,全球任何一個地方的時間戳都是同一個,Date本身不支援時區。PS.本質上這行程式碼也指定不了時區哦~

Date是可變的

Date是一個非常基礎底層的類,但它卻設計為可變。當我們計算這個data3天后是不是週末,如果程式計算中把這個date加了3天,那麼你手上拿著得date也變成了3天后的日期。相比同為底層基礎類的String,做得就優秀多了。
難當大任的Calendar
JDK剛推出就發現了問題,於是趕緊在1.1版本推出了Calendar,嘗試用來解決令人詬病的Date,並將Date一眾函式都標記為了deprecated。但Calendar依然是可變物件、最多也只能精確到毫秒、執行緒不安全、API的使用複雜且笨重等等,Calendar整體而言並沒有挽回頹勢。
曙光來臨之JSR310
在聊JSR310之前,不得不先提一提  Joda-Time [7]這個開源Java庫。Joda-Time以清晰的API、良好的時區支援、不可變性、強型別化等特性,得到了開發者社群的廣泛好評,並在很多專案中被採用,被視為改善Java日期和時間處理的標杆庫。Joda-Time如此優秀,Oracle也開啟了收編之旅。2013年Java8釋出,其中針對日期時間帶來一套全新的標準規約 JSR310 [8],而JSR310的核心製作者就是Joda-Time的作者Stephen Colebourne。
Instant
/** * The number of seconds from the epoch of 1970-01-01T00:00:00Z. */private final long seconds;/** * The number of nanoseconds, later along the time-line, from the seconds field. * This is always positive, and never exceeds 999,999,999. */private final int nanos;
Instant這個單詞的中文含義是『瞬間』,嚴格來說Java8之前的Date就應該是現在的Instant。Instant類有維護2個核心欄位,當前距離時間紀元的秒數以及秒中的納秒部分。它指代當前這個時刻,全球任一位置這一時刻都是同一時刻。這一時刻川建國同學在高床軟枕打著呼,這一時刻我泡著龍井寫著文稿。

LocalDateTime

/******************** LocalDate ********************//**     * The year.     */private final int year;/**     * The month-of-year.     */private final short month;/**     * The day-of-month.     */private final short day;/******************** LocalTime ********************//**     * The hour.     */private final byte hour;/**     * The minute.     */private final byte minute;/**     * The second.     */private final byte second;/**     * The nanosecond.     */private final int nano;
LocalDateTime由LocalDate和LocalTime組成,分別日期和時間,以此來解決Date中不能單獨表示日期和時間的問題。它們都與時區無關,只客觀代表一個無時區的時間,比如2024-12-08 13:46:21,LocalDateTime記錄著它的年、月、日、時、分、秒、納秒。但具體是北京時間的13點還是倫敦時間的13點,由上下文語境自行處理。

Duration

Duration中文含義譯為『期間』,通常用來計算2個時間之前相差的週期,不得不說這一套時間JDK確實定義得語義非常清晰。
InstantstartInstant = xxx;InstantendInstant = xxx;Duration.between(startInstant,endInstant).toMinutes();
這個很好理解,比較2個時間戳時間的相差分鐘數。但如果換成LocalDateTime,會是怎樣呢?
LocalDateTimestartTime = xxx;LocalDateTimeendTime = xxx;Duration.between(startTime,endTime).toMinutes();
因為LocalDateTime是不帶時區的,所以LocalDateTime是不能直接換成成Instant的。而Duration的比較也是不帶時區的,或者你可以理解它是把時間放在同一個時區進行比較,來抹去時區的影響。
/********************* JDK Duration.between 部分原始碼 *******************************/@Overridepubliclonguntil(Temporal endExclusive, TemporalUnit unit){    LocalDateTime end = LocalDateTime.from(endExclusive);if (unit instanceof ChronoUnit) {if (unit.isTimeBased()) {long amount = date.daysUntil(end.date);if (amount == 0) {return time.until(end.time, unit);            }long timePart = end.time.toNanoOfDay() - time.toNanoOfDay();if (amount > 0) {                amount--;  // safe                timePart += NANOS_PER_DAY;  // safe            } else {                amount++;  // safe                timePart -= NANOS_PER_DAY;  // safe            }// 餘下省略}
上述是Duration部分原始碼,它首先計算出2個時間相差多少天,再比較當天的時間裡相差多少納秒,再進行累加。所以你傳過來2024-12-08 和 2024-12-04,那就是相差4天,至於是北京時間的12-08還是倫敦時間的12-04,在Duration裡都被抹去了時區的概念。看到這裡,上面的程式設計題裡做對了嗎?

ZonedDateTime

真正需要使用時區,我們就需要用到ZonedDateTime。「zoned」這個單詞在英漢詞典中是zone的過去分時,譯為『劃為區域的』。
// 輸出:2024-12-08T14:18:32.554144+08:00[Asia/Shanghai]ZonedDateTime defaultZoneTime = ZonedDateTime.now(); // 預設時區// 輸出:2024-12-08T01:18:32.560931-05:00[America/New_York]ZonedDateTime usZoneTime = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定時區獲取當前時間
因為LocalDateTime是沒有時區的,如果我們需要將LocalDateTime轉成ZonedDateTime,就需要帶上時區資訊。
LocalDateTime localDateTime = LocalDateTime.of(2024, 12, 8, 14, 21, 17);ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault());ZonedDateTime usZonedDateTime = localDateTime.atZone(ZoneId.of("America/New_York"));
隨著JDK不斷地釋出演進,Time模組確實得到了質的提升,這裡不一一細說Java日期時間相關API。如果你還在苦於對Date做各種Utils的花式包裝,請擁抱java.time吧。
時間日期引起的慘案
夏令時與冬令時
曾經小A做了一個鑑權系統,用於對請求做加密解密,保證每一次都是真實合法有效的介面請求。其中做了一個判定,如果請求的時間距現在已經超過10分鐘,就會拒絕該次請求。從邏輯上來說,這很合理,但問題的雪崩卻出現在3月的那個晚上。。。

什麼是夏令時

夏令時[9]又稱夏時制,英文原文為Daylight Saving Time,從名字上可以看出,夏令時誕生的背景是為了更好的利用白天的時間。夏令時概念的提出最早可以追溯到1895年,紐西蘭昆蟲學家喬治·哈德遜向惠靈頓哲學學會提出,提前2小時的日光節約提案,以此在工作結束後,可以獲得多出一段的白晝時間。
具體夏令時的實施,以美國為例,美國會在每年3月的第二個星期日的凌晨2:00,時鐘會往前調1個小時變為3:00。再在每年11月的第一個星期日的凌晨2:00,將時鐘在往後調1個小時變成1:00,此時的回撥也被稱為“冬令時”。

夏令時實施的國家與地區

藍色為正在實施夏令時的國家和地區

灰色為曾經實施但現在已經取消夏令時的國家和地區

黑色為從未實施夏令時的過去和地區

1916年4月30日,德國與奧匈帝國成為世界上第一組實施夏時制的國家,目的是為了能在戰爭期間節約煤炭消耗。在1970年代,由於美洲與歐洲地區也受到能源危機影響,至此夏令時開始廣泛被實施。當下全球有共約70多個國家和時區在使用夏令時,我國也曾短暫使用過夏令時,但因節約能源效果不顯著,以及對日常生活工作等帶來的一些影響,到1992年全國宣佈取消夏令時。
閏年與閏秒
2008年是閏年存在2月29日,但微軟一些軟體在處理部分任務的時候會因為閏年導致處理錯誤。微軟甚至在SQL Server 2008 CTP釋出後曾經宣讀了一份證明,建議使用者不要在2月29日安裝和執行軟體,以減少影響。並且在Windows Small Business Server上還會出現更嚴重的錯誤:因為在微軟的日曆里根本沒那麼一天,因此就無法頒發證書。

為什麼要閏年

閏年大家比較熟悉,閏年的設定是為了使日曆年與太陽年(即地球繞太陽公轉一週的時間)更精準地一致。嚴格來說地球繞太陽一圈的時間,大約是365.2422天。經過大約四年,累計誤差將接近一天(0.2422 * 4 ≈ 0.9688天),但如果每4年就加1天,這樣每128年又會多算出1天。所以基於此定義出了普通閏年與世紀閏年。
  • 普通閏年:公曆年份是4的倍數,且不是100的倍數的,為閏年(如2004年、2020年等就是閏年)。
  • 世紀閏年:公曆年份是整百數的,必須是400的倍數才是閏年(如1900年不是閏年,2000年是閏年)。

為什麼要閏秒

閏秒[10]本質上和閏年的作用是一樣的,也是解決時間解釋運動中所存在的偏差。閏秒的調整是為了確保協調世界時(UTC)與地球自轉時間(UT1)[11]保持一致。由於地球自轉速度的不均勻性和減慢,UTC需要定期新增或刪除一秒鐘來進行調整,這一秒鐘稱為“閏秒”。
國際地球自轉與參考系統服務(IERS)是負責監測和釋出閏秒調整的機構。ERS會根據地球自轉的實際變化和測量資料,決定是否需要調整閏秒。閏秒通常在6月30日或12月31日的最後一秒新增或刪除。這意味著在某些年份,時間序列可能會變為:23:59:59 → 23:59:60 → 00:00:00。
寫在最後
『存在不一定合理,但一定有原因』這是曾經我的主管跟我說的,至今我也受益其中。對所有事情懷有一絲懷疑心態,搞懂它的前世今生,或許它不那麼合理,但至少當時這樣做解決了一定的問題,我們在做新設計的時候可以提前考慮與規避。水多了加面,面多了加水,如果我們只是看到當下的混亂就指著“前人”沒有設計思想沒有技術匠心,卻不瞭解最初“前人”這樣做的意圖與背景,罵著“前人”的我們終有一天也會成為後人眼中的“前人”。
參考連結:
[1]https://en.wikipedia.org/wiki/Unix_time
[2]https://baike.baidu.com/item/世界時/692237
[3]https://www.timeanddate.com/time/zones/
[4]https://www.utctime.net/
[5]https://baike.baidu.com/item/原子鐘/765460
[6]https://baike.baidu.com/item/閏秒
[7]https://www.joda.org/joda-time/
[8]https://jcp.org/en/jsr/detail
[9]https://baike.baidu.com/item/夏令時/1809579
[10]https://baike.baidu.com/item/閏秒/696742
[11]https://zh.wikipedia.org/wiki/世界時
高效構建安全合規的企業新賬號
透過此方案可以統一企業內不同賬號內的基線,靈活適配不同企業對賬號初始化的個性需求。   
點選閱讀原文檢視詳情。

相關文章