詳解 Java 日期與時間修改

力扣leetcode 發佈 2020-03-05T10:33:23+00:00

最近在家辦工接到的一項工作是和時區有關的,據用戶反饋,由於美國的 Puerto Rico 州不使用夏令時,在其他州施行夏令時時,這個州的用戶選不到適合自己的時區,導致時間無法正確顯示。最終筆者為軟體添加了太平洋標準時區解決了這個問題。時區、夏令時、標準時間...


最近在家辦工接到的一項工作是和時區有關的,據用戶反饋,由於美國的 Puerto Rico 州不使用夏令時,在其他州施行夏令時時,這個州的用戶選不到適合自己的時區,導致時間無法正確顯示。最終筆者為軟體添加了太平洋標準時區解決了這個問題。

時區、夏令時、標準時間...日期和時間是計算機處理的重要數據,在絕大多數軟體程序中,我們都要和日期和時間打交道。本篇文章我們將系統地學習 Java 對日期和時間的處理。(本文參考了 「連結」廖雪峰 Java 教程-日期和時間(點擊連結查看) 文章中的資料,事實上,筆者並不認為本文比廖大佬的文章更好,有時間的讀者可以直接閱讀原教程。)


一、時區

​地球人都知道,我們地球是自西向東自轉的,所以東邊會比西邊早看到太陽,東邊的時間也總比西邊的快。如果全球採用統一的時間,比如都用北京時間,會產生什麼問題呢?

當正午十二點的太陽照射到北京時,身處地球另一面的紐約還是漆黑一片。對於紐約來說,日常作息時間就成了晚上九點開始上班,因為那時太陽剛剛升起;所有紐約人都上班到第二天早上六點下班,因為那時太陽剛剛落下。

雖然對於長期居住在一個地方的人來說,他可以適應自己本地的作息時間,但當他去其他地方旅遊或是與其他地方的人交流時,就必須查詢當地的作息時間,這會帶來很大的不便。

於是,在 1879 年,加拿大鐵路工程師弗萊明首次提出全世界按統一標準劃分「時區」。1884 年華盛頓子午線國際會議正式通過採納這種時區劃分,稱為世界標準時制度。

時區劃分的初衷是 儘量使中午貼近太陽上中天的時間,從此以後,各地的時間經過換算,都能統一地早上六點起床,中午十二點午餐,晚上六點下班。

全球共分為 24 個時區,所以每個時區占 15˚ 經度。理論時區 以能被 15 整除的經線為中心,向東西兩側延伸 7.5˚。國際規定經過英國格林威治天文台的那一條經線為 0˚ 經線,這條經線也被稱作 本初子午線。選擇格林威治既是因為當初「日不落帝國」的強大,也是由於格林威治常年提供準確的航海觀測數據,19 世紀晚期,72% 的世界貿易都依靠以格林威治作為本初子午線的航海圖表。

為了避開國界線,有的時區的形狀並不規則,而是比較大的國家以國家內部行政分界線為時區界線,這是 實際時區,也稱為 法定時區

身處地球的不同地區,時間可能是不同的,所以光靠時間我們無法確定一個時刻,要確定一個時刻必須要帶上時區。

表示時區有兩種常見的寫法,最常見的是 GMT,它的全稱是 Greenwich Mean Time,意思是格林威治標準時間,世界各地根據東西偏移量計算時區。比如,北京位於東八區,記做 GMT+8,紐約位於西五區,記做 GMT-5。

還有一種寫法是 UTC,它的全稱是 Coordinated Universal Time,意思是協調世界時,如果時間以 UTC 表示,則在時間後面直接加上一個「Z」(不加空格),「Z」是協調世界時中 0 時區的標誌。比如,「09:30 UTC」 寫作 「09:30Z」 或是 「0930Z」。「14:45:15 UTC」 寫作 「14:45:15Z」 或 「144515Z」。因為在北約音標字母中用 「Zulu」 表示 「Z」,所以 UTC 時間也被稱做祖魯時間。

GMT 和 UTC 基本一樣,只不過 UTC 使用更加精確的原子鐘計時,每隔幾年會有一個閏秒。但我們無需關注兩者的差別,計算機在聯網時會自動與時間伺服器同步時間。

計算不同時區的時間差很簡單,我們平時常用的北京時間位於東八區,即:GMT+8,它的值是在 GMT 的基礎上增加了 8 小時,紐約位於西五區,即:GMT-5,它的值是在 GMT 的基礎上減少了 5 小時。所以北京時間通常比紐約時間快 13 個小時。

我們現在知道,每往西越過一個時區,時間便提前一小時。據此我們來思考一個有趣的問題:如果我們一直往西,以每小時一個時區的速度前進,時間是否會靜止呢?

1.比如我們從北京出發,此時時間是 2020-2-11 8:00 GMT+8

2.當我們花費一個小時,走到東七區時,時間是 2020-2-11 8:00 GMT+7

3.當我們走到本初子午線時,時間是 2020-2-11 8:00 GMT

4.當我們走到西五區時,時間是 2020-2-11 8:00 GMT-5

......

我們都知道地球是個球體,當我們繞地球一圈回到北京時,如果時間還是 2020-2-11 8:00 GMT+8,豈不是時間真的靜止了?進一步思考,如果我們以半小時一個時區的速度向西前進,豈不是時間還會倒流?

常識告訴我們,時間是不可能靜止也不可能倒流的。那麼這裡的問題出在哪裡呢?問題就出在東西時區的交界處。上文說到,地球分為 24 個時區,包括標準時區、東一區~東十二區、西一區~西十二區。實際上,東十二區和西十二區是同一時區。

從 0˚ 經線開始,每往西跨一個時區時間便減少 1 小時,每往東跨一個時區便增加 1 小時。如此一來,到了另一端 180˚ 經線時,就會有 24 小時的落差,為了平衡這一落差,人們規定由西向東越過此線日期需減少一天,由東向西越過此線時日期需增加一天。故而這一條線被稱之為 國際日期變更線,也叫 換日線,它位於本初子午線的另一面。和時區界限類似,為了避開國界線,換日線並不與 180˚ 經線重合,換日線實際上是不規則的。


如果我們接著走下去:

5.當我們走到東 / 西十二區時,時間是 2020-2-11 8:00 GMT±12

6.我們越過國際換日線,日期增加一天,時間是 2020-2-12 8:00 GMT±12

7.當我們走到東十一區時,時間是 2020-2-12 8:00 GMT+11

8.當我們回到北京時,時間是 2020-2-12 8:00 GMT+8

此時,我們的環球之旅剛好用了 24 小時。

再來看一下如果我們以每半小時一個時區的速度向西行走,時間為什麼不會逆流:

1.我們還是從北京出發,此時時間是 2020-2-11 8:00 GMT+8

2.當我們花費半小時,走到東七區時,時間是 2020-2-11 7:30 GMT+7

3.當我們走到本初子午線時,時間是 2020-2-11 4:00 GMT

4.當我們走到西五區時,時間是 2020-2-11 1:30 GMT-5

5.當我們走到東 / 西十二區時,時間是 2020-2-10 22:00 GMT±12

6.我們越過國際換日線,日期增加一天,時間是 2020-2-11 22:00 GMT±12

7.當我們走到東十一區時,時間是 2020-2-11 21:30 GMT+11

8.當我們回到北京時,時間是 2020-2-11 20:00 GMT+8

此時,我們的環球之旅剛好用了 12 小時。


二、夏令時

​由於夏季和冬季白晝時間不一致,部分國家施行了夏令時制度,目的是讓人們根據白晝時間來調整作息。

夏令時:在夏天開始的時候,把時間往後撥 1 小時,夏天結束的時候,再把時間往前撥 1 小時。

施行夏令時使得人們可以儘量在白天工作,從而減少照明,節省電能。但夏令時也帶來了很多的不便,如夏令時開始和結束時,人們不得不調整睡眠時間;夏令時也使得時間計算變得複雜,在夏令時結束的當天,某些時間會出現兩次,容易造成交通、生產、會議安排等時間的混亂。中國曾經施行過一段時間夏令時,在 1992 年就被廢除了,而美國大部分地區現在還在使用夏令時。

美國使用夏令時時,紐約時間按照西四區計算,即:GMT-4。這段時間北京時間比紐約時間快 12 個小時,夏令時結束後,紐約時間又恢復到西五區 GMT-5。

由於各國規定有所差異,所以夏令時計算非常複雜。當我們需要計算夏令時時,應儘量使用 Java 庫提供的類,避免自己計算夏令時。


三、舊 API

Java 標準庫提供了兩套關於時間和日期的 API:

  • 舊 API:位於 java.util 包中,裡面主要有 Date、Calendar、TimeZone 類
  • 新 API:位於 java.time 包中,裡面主要有 LocalDateTime、ZonedDateTime、ZoneId 類

有兩套 API 的原因是舊 API 在設計時沒有考慮好時區問題,常量設計也有些不合理,導致使用起來不夠方便。新 API 很好地解決了這些問題。我們在開發時,除非維護老代碼,其他時候都應該儘量使用新 API。


3.1. Date

Date 類用於存儲日期和時間,查看其源碼可以發現,它保存了一個 long 類型的時間戳。時間戳是指格林威治時間從 1970 年 1 月 1 日零點到此刻經歷的秒數或毫秒數。

Date 的基本用法如下:

Date 在使用時有幾個缺點:

  • 每次獲取年份、月份都需要轉換
  • 只能獲取當前時區的時間,無法設置時區
  • 無法加減日期和時間
  • 無法計算某個月的第幾個星期幾


3.2. SimpleDateFormat

默認輸出的時間字符串的格式通常不能滿足我們的要求,所以我們需要用 SimpleDateFormat 來格式化輸出,它使用一些預定義的字符串表示格式化,較常用的字符串有:

  • y:年
  • M:月
  • d:日
  • H:小時
  • m:分鐘
  • s:秒
  • S:毫秒
  • a:上午 / 下午
  • E:星期
  • z:時區

附:Java 官網文檔中給出的預定義字符串表格

SimpleDateFormat 的使用:

這裡的時區信息輸出為 CST,表示 China Standard Time,也就是中國標準時間。

SimpleDateFormat 會根據預定義字符的長度列印不同長度的信息。以 M 為例:

  • M:輸出 2
  • MM:輸出 02
  • MMM:輸出 2月
  • MMMM:輸出 二月

如果預定義字符串的長度短於需要輸出的信息,這時 Java 會輸出 能包含全部信息的最短字符串,也就是說 Java 不會丟棄任何信息,如上例中只用了一個 y,仍然輸出了 2020,並不會只輸出一個 2。

我們來發揮一下極客精神,探索一下預定義字符串過長 Java 會怎麼處理:

本例中,每個預定義字符的長度都為 10,可以看到,系統對年、日、時、分、秒、毫秒的處理是用前置 0 補足位數,對月份、上午 / 下午、星期、時區的處理是輸出全中文。

SimpleDateFormat 可以設置時區,我們可以用 SimpleDateFormat 把 Date 獲取的時間轉換為其他時區顯示出來:

3.3. Calendar

舊 API 中,為了加減日期和時間,Java 提供了 Calendar 類。

Calendar 的基本使用:

Calendar 修復了 Date 獲取年份時必須 + 1900 的問題,但月份仍然使用 0~11 表示 1~12 月,星期採用 1~7 表示周日~周六。雖然咱們程式設計師都從 0 開始計數,但日期和時間一般都是要展示給用戶看的,每次顯示時都要轉換實在是太不方便了,這也是需要新 API 的原因之一。

Calendar 提供的日期和時間的加減功能使用如下:

使用日期加減時有一點需要特別注意,我們來看一個例子:

我們將 12 月 31 日減去 1 個月,再加上 1 個月,日期變成了 12 月 30 日!這是因為 11 月 沒有 31 日,所以 12 月 31 日減去 1 個月時, Calendar 會自動將日期調整到 11 月 30 日,再加 1 個月,就變成了 12 月 30 日。也就是說 Calendar 加減時,會根據月份自動調整日期。

上文介紹 Date 時我們說到,單靠 Date 和 SimpleDateFormat 只能把本地時區的時間用其他時區顯示出來,無法自由的實現時區的轉換,比如我們身在中國,無法把紐約時間 GMT-5 轉換為東京時間 GMT+9。但 Calendar 是可以設置時區的,所以我們現在有了一種間接轉換任意時區的方法:

實際轉換過程為:Calendar 保存的 紐約時間先轉換成 Date 保存的 北京時間,再用 SimpleDateFormat 將 Date 轉換成 東京時間展示出來。

上例中還可以看到,Calendar 使用 set 方法設置指定時間,除了此例中的一次性全部指定的方式外,也可以單個指定:

四、新 API

由於舊 API 存在的諸多不便,從 Java 8 開始,jaca.time 包提供了一套新的日期和時間的 API。主要有 LocalDateTime、ZonedDateTime、Instant、ZoneId、Duration、DateTimeFormatter。

新 API 不僅使用更方便,而且修正了 舊 API 中不合理的常量設計:

  • 新 API 中,Month 取值範圍變成:1~12,表示 1~12月
  • 新 API 中,Week 取值範圍變成:1~7,表示周一~周日

4.1. LocalDateTime

LocalDateTime 用來代替 Date 和 Calendar,LocalDateTime 的基本用法如下:

LocalDateTime 使用 now() 函數獲取當前日期和時間,輸出時嚴格按照 ISO 8601 格式列印。ISO 8601 是國際標準化組織的日期和時間的表示方法,全稱為《數據存儲和交換形式·信息交換·日期和時間的表示方法》,ISO 8601 規定使用 T 分隔日期和時間。標準格式如下:

  • 日期:yyyy-MM-dd
  • 時間:HH:mm:ss
  • 帶毫秒的時間:HH:mm:ss.SSS
  • 日期和時間:yyyy-MM-dd'T'HH:mm:ss
  • 帶毫秒的日期和時間:yyyy-MM-dd'T'HH:mm:ss.SSS

我們可以通過 parse() 函數解析一個符合 ISO 8601 格式的字符串,創建出 LocalDateTime:

除此之外,我們還可以通過 of() 函數指定日期和時間創建 LocalDateTime:

LocalDateTime 存儲了當前的日期信息和時間信息,如果我們只需要當前日期或當前時間,可以使用 LocalDate 和 LocalTime:

同 LocalDateTime 類一樣,LocalDate 和 LocalTime 類也可以通過 now()、parse()、of() 方法創建。

LocalDateTime 在加減日期時,可以採用簡潔的鏈式調用:

和 Calendar 一樣,LocalDateTime 在加減時,仍然會自動調整日期:

與 Calendar 不同的是,LocalDateTime 是不可變類,如此例中調用 minusMonths(1) 和 plusMonths(1) 方法後,dt 的值並沒有改變,這個函數返回的是一個調整後的新值,我們將這個新值賦值給了 dt2。

對應 Calendar 的 set() 方法,LocalDateTime 調整時間使用 withXxx() 方法:

  • 調整年:withYear()
  • 調整月:withMonth()
  • 調整日:withDayOfMonth()
  • 調整時:withHour()
  • 調整分:withMinute()
  • 調整秒:withSecond()

LocalDateTime 還有一個 with() 方法允許我們做更複雜的運算:

要比較兩個日期的先後,可以使用 LocalDateTime 的 isBefore()、isAfter() 方法:

4.2. ZonedDateTime

LocalDateTime 和 Date 類一樣,總是表示本地時區的時間,如果要表示帶時區的時間,需要使用 ZonedDateTime,它相當於 LocalDateTime + ZoneId。LocalDateTime 提供的方法,如 now()、of()、plusDays() 等,ZonedDateTime 也都提供。

ZonedDateTime 的使用:

ZonedDateTime 通過 now() 函數獲取當前時區的時間,通過 now(ZoneId zone) 函數獲取指定時區的時間。這樣獲取到的兩個時間雖然時區不同,但表示的都是同一時刻(毫秒數不同是由於執行代碼會花費一點時間)。

通過給 LocalDateTime 設置 ZoneId,也可以創建出 ZonedDateTime:

通過這種方式創建的 ZonedDateTime 日期和時間一樣,但時區不同,所以表示的是兩個不同時刻。

ZonedDateTime 可以通過 toLocalDateTime() 函數轉換成 LocalDateTime:

我們看到,ZonedDateTime 轉換成 LocalDateTime 時,不會自動切換成本地時區的時間,而是直接丟棄時區信息。

由於 ZonedDateTime 自帶時區信息,所以在涉及時區轉換時使用 ZonedDateTime 非常方便。如上文中提到的將紐約時間轉換成的東京時間,使用 ZonedDateTime 實現如下:

4.3. DateTimeFormatter

上文已經說到,DateTimeFormatter 是用來代替 SimpleDateFormat 的。與 SimpleDateFormat 相比,DateTimeFormatter 的一個明顯優勢在於它是 線程安全 的。SimpleDateFormat 由於不是線程安全的,使用時只能在方法內部創建新的局部變量,而 DateTimeFormatter 可以只創建一個實例。

DataTimeFormat 預定義的字符串和 SimpleDateFormat 一模一樣,來看下 DateTimeFormatter 的基本使用:

還記得 LocalDateTime 的 parse() 方法嗎?我們查看一下它的源碼:

從源碼中我們看到,parse() 方法可以傳入兩個參數,第二個參數就是一個 DateTimeFormatter,也就是說不僅 ISO 8601 標準格式的字符串可以被解析,我們完全可以自定義被解析的字符串格式。

DataTimeFormatter 的 ofPattern() 方法還可以傳入一個 Locale 參數,這個參數的作用是使用當地的習慣來格式化時間:


4.4. Instant

在新 API 中,使用 Instant 表示時間戳,它類似於 System.currentTimeMillis()。Instant 使用如下:

給 Instant 加上一個時區,就可以創建出 ZonedDateTime:

五、新舊 API 的轉換

舊 API 轉新 API 可以通過 toInstant() 方法轉換為 Instant,再由 Instant 轉換成 ZonedDateTime:

新 API 轉舊 API 時,需要藉助 long 類型時間戳實現:

以上,就是 Java 日期和時間的全部內容了,有任何收穫或疑問歡迎在評論區與大家一起討論交流。


本文作者:Alpinist Wang

聲明:本文歸 「力扣」 版權所有,如需轉載請聯繫。文章封面圖和文中部分圖片來源於網絡,如有侵權聯繫刪除。

關鍵字: