全網最全!徹底弄透Java處理GMT/UTC日期時間

itread01 2021-01-21 20:05:06
java UTC GMT 最全 日期


[TOC]![](https://img-blog.csdnimg.cn/20210118055156568.jpg#pic_center)# 前言你好,我是A哥(YourBatman)。本系列的目的是明明白白、徹徹底底的搞定日期/時間處理的幾乎所有case。[上篇文章](https://mp.weixin.qq.com/s/VdoQt88JfjPJTL9XgohZJQ) 鋪設所有涉及到的概念解釋,例如GMT、UTC、夏令時、時間戳等等,若你還沒看過,不僅強烈建議而是**強制建議**你前往用花5分鐘看一下,因為日期時間處理較為特殊,實戰必須基於對概念的瞭解,否則很可能依舊霧裡看花。> 說明:日期/時間的處理是日常開發非常常見的老大難,究其原因就是對日期時間的相關概念、應用場景不熟悉,所以不要忽視它上篇概念,本文落地實操,二者相輔相成,缺一不可。本文內容較多,文字較長,預計超2w字,旨在全面的徹底幫你搞定Java對日期時間的處理,**建議你可收藏**,作為參考書留以備用。## 本文提綱![](https://img-blog.csdnimg.cn/20210118055954767.png#pic_center)## 版本約定- JDK:8# 正文上文鋪了這麼多概念,作為一枚Javaer最關心當然是這些“概念”在Java裡的落地。平時工作中遇到時間如何處理?用Date還是JDK 8之後的日期時間API?如何解決跨時區轉換等等頭大問題。A哥向來管生管養,管殺管埋,因此本文就帶你領略一下,Java是如何實現GMT和UTC的?眾所周知,JDK以版本8為界,有兩套處理日期/時間的API:![](https://img-blog.csdnimg.cn/20210115104456702.png#pic_center)雖然我一直鼓勵棄用Date而支援在專案中只使用JSR 310日期時間型別,但是呢,由於Date依舊有龐大的存量使用者,所以本文也不落單,對二者的實現均進行闡述。## Date型別實現java.util.Date在JDK 1.0就已存在,用於表示日期 + 時間的型別,縱使年代已非常久遠,並且此類的具有職責不單一,使用很不方便等諸多毛病,但由於十幾二十年的歷史原因存在,它的生命力依舊頑強,使用者量巨大。先來認識下Date,看下這個例子的輸出:```java@Testpublic void test1() { Date currDate = new Date(); System.out.println(currDate.toString()); // 已經@Deprecated System.out.println(currDate.toLocaleString()); // 已經@Deprecated System.out.println(currDate.toGMTString());}```執行程式,輸出:```javaFri Jan 15 10:22:34 CST 20212021-1-15 10:22:3415 Jan 2021 02:22:34 GMT```**第一個:標準的UTC時間(CST就代表了偏移量 +0800)**第二個:本地時間,根據本地時區顯示的時間格式第三個:GTM時間,也就是格林威治這個時候的時間,可以看到它是凌晨2點(北京時間是上午10點哦)第二個、第三個其實在JDK 1.1就都標記為@Deprecated過期了,基本禁止再使用。若需要轉換為本地時間 or GTM時間輸出的話,請使用格式化器java.text.DateFormat去處理。### 時區/偏移量TimeZone在JDK8之前,Java對時區和偏移量都是使用`java.util.TimeZone`來表示的。一般情況下,使用靜態方法`TimeZone#getDefault()`即可獲得當前JVM所執行的時區,比如你在中國執行程式,這個方法返回的就是中國時區(也叫北京時區、北京時間)。有的時候你需要做**帶時區**的時間轉換,譬如:介面返回值中既要有展示北京時間,也要展示紐約時間。這個時候就要獲取到紐約的時區,以北京時間為基準在其上進行帶時區轉換一把:```java@Testpublic void test2() { String patternStr = "yyyy-MM-dd HH:mm:ss"; // 北京時間(new出來就是預設時區的時間) Date bjDate = new Date(); // 得到紐約的時區 TimeZone newYorkTimeZone = TimeZone.getTimeZone("America/New_York"); // 根據此時區 將北京時間轉換為紐約的Date DateFormat newYorkDateFormat = new SimpleDateFormat(patternStr); newYorkDateFormat.setTimeZone(newYorkTimeZone); System.out.println("這是北京時間:" + new SimpleDateFormat(patternStr).format(bjDate)); System.out.println("這是紐約時間:" + newYorkDateFormat.format(bjDate));}```執行程式,輸出:```java這是北京時間:2021-01-15 11:48:16這是紐約時間:2021-01-14 22:48:16```(11 + 24) - 22 = 13,北京比紐約快13個小時沒毛病。> 注意:兩個時間表示的應該是同一時刻,也就是常說的時間戳值是相等的那麼問題來了,你怎麼知道獲取紐約的時區用`America/New_York`這個zoneId呢?隨便寫個字串行不行?答案是當然不行,這是有章可循的。下面我介紹兩種查閱zoneId的方式,任你挑選:**方式一**:用Java程式把所有可用的zoneId打印出來,然後查閱```java@Testpublic void test3() { String[] availableIDs = TimeZone.getAvailableIDs(); System.out.println("可用zoneId總數:" + availableIDs.length); for (String zoneId : availableIDs) { System.out.println(zoneId); }}```執行程式,輸出(大部分符合規律:/前表示所屬州,/表示城市名稱):```java可用zoneId總數:628Africa/AbidjanAfrica/Accra...Asia/Chongqing // 亞洲/重慶Asia/Shanghai // 亞洲/上海Asia/Dubai // 亞洲/迪拜...America/New_York // 美洲/紐約America/Los_Angeles // 美洲/洛杉磯...Europe/London // 歐洲/倫敦...Etc/GMTEtc/GMT+0Etc/GMT+1...```值得注意的是並沒有 Asia/Beijing 哦。> 說明:此結果基於JDK 8版本,不同版本輸出的總個數可能存在差異,但主流的ZoneId一般不會有變化**方式二**:zoneId的列表是jre維護的一個文字檔案,路徑是你JDK/JRE的安裝路徑。地址在.\jre\lib目錄的為未`tzmappings`的文字檔案裡。開啟這個檔案去ctrl + f找也是可以達到查詢的目的的。這兩種房子可以幫你找到ZoneId的字典方便查閱,但是還有這麼一種情況:當前所在的城市呢,在**tzmappings**檔案里根本沒有(比如沒有收錄),那要獲取這個地方的時間去顯示怎麼破呢?雖然概率很小,但不見得沒有嘛,畢竟全球那麼多國家那麼多城市呢~Java自然也考慮到了這一點,因此也是有辦法的:指定其時區數字表示形式,其實也叫偏移量(不要告訴我這個地方的時區都不知道,那就真沒救了),如下示例```java@Testpublic void test4() { System.out.println(TimeZone.getTimeZone("GMT+08:00").getID()); System.out.println(TimeZone.getDefault().getID()); // 紐約時間 System.out.println(TimeZone.getTimeZone("GMT-05:00").getID()); System.out.println(TimeZone.getTimeZone("America/New_York").getID());}```執行程式,輸出:```javaGMT+08:00 // 效果等同於Asia/ShanghaiAsia/ShanghaiGMT-05:00 // 效果等同於America/New_YorkAmerica/New_York ```值得注意的是,這裡只能用`GMT+08:00`,而不能用`UTC+08:00`,原因下文有解釋。#### 設定預設時區一般來說,JVM在哪裡跑,預設時區就是哪。對於國內程式設計師來講,一般只會接觸到東八區,也就是北京時間(本地時間)。隨著國際合作越來越密切,很多時候需要日期時間國際化處理,舉個很實際的例子:同一份應用在阿里雲部署、在AWS(海外)上也部署一份供海外使用者使用,此時**同一份程式碼**部署在不同的時區了,怎麼破?倘若時區不同,那麼勢必影響到程式的執行結果,很容易帶來計算邏輯的錯誤,很可能就亂套了。Java讓我們有多種方式可以**手動**設定/修改預設時區:1. API方式: 強制將時區設為北京時區`TimeZone.setDefault(TimeZone.getDefault().getTimeZone("GMT+8"));`2. JVM引數方式:`-Duser.timezone=GMT+8`3. 運維設定方式:將作業系統主機時區設定為北京時區,這是推薦方式,可以完全對開發者無感,也方便了運維統一管理據我瞭解,很多公司在阿里雲、騰訊雲、國內外的雲主機上部署應用時,全部都是採用運維設定統一時區:中國時區,這種方式來管理的,這樣對程式來說就消除了預設時區不一致的問題,對開發者友好。### 讓人惱火的夏令時你知道嗎,中國曾經也使用過夏令時。> 什麼是夏令時?[戳這裡](https://mp.weixin.qq.com/s/VdoQt88JfjPJTL9XgohZJQ)離現在最近是1986年至1991年用過夏令時(每年4月中旬的第一個週日2時 - 9月中旬的第一個星期日2時止):*1986年5月4日至9月14日**1987年4月12日至9月13日**1988年4月10日至9月11日**1989年4月16日至9月17日**1990年4月15日至9月16日**1991年4月14日至9月15日*夏令時是一個“非常煩人”的東西,大大的增加了日期時間處理的複雜度。比如這個靈魂拷問:若你的出生日期是1988-09-11 00:00:00(夏令時最後一天)且存進了資料庫,想一想,對此日期的格式化有沒有可能就會出問題呢,有沒有可能被你格式化成1988-09-10 23:00:00呢?針對此拷問,我模擬瞭如下程式碼:```java@Testpublic void test5() throws ParseException { String patterStr = "yyyy-MM-dd"; DateFormat dateFormat = new SimpleDateFormat(patterStr); String birthdayStr = "1988-09-11"; // 字串 -> Date -> 字串 Date birthday = dateFormat.parse(birthdayStr); long birthdayTimestamp = birthday.getTime(); System.out.println("老王的生日是:" + birthday); System.out.println("老王的生日的時間戳是:" + birthdayTimestamp); System.out.println("==============程式經過一番週轉,我的同時 方法入參傳來了生日的時間戳============="); // 字串 -> Date -> 時間戳 -> Date -> 字串 birthday = new Date(birthdayTimestamp); System.out.println("老王的生日是:" + birthday); System.out.println("老王的生日的時間戳是:" + dateFormat.format(birthday));}```這段程式碼,在不同的JDK版本下執行,**可能**出現不同的結果,有興趣的可copy過去自行試試。關於JDK處理夏令時(特指中國的夏令時)確實出現過問題且造成過bug,當時對應的JDK版本是`1.8.0_2xx`之前版本格式化那個日期出問題了,在這之後的版本貌似就沒問題了。這裡我提供的版本資訊僅供參考,若有遇到類似case就升級JDK版本到最新吧,一般就不會有問題了。> 發生這個情況是在JDK非常小的版本號之間,不太好定位精確版本號界限,所以僅供參考總的來說,只要你使用的是較新版本的JDK,開發者是無需關心夏令時問題的,即使全球仍有很多國家在使用夏令時,咱們只需要面向**時區**做時間轉換就沒問題。### Date時區無關性類Date表示一個特定的時間**瞬間**,精度為毫秒。既然表示的是瞬間/時刻,那它必然和時區是無關的,看下面程式碼:```java@Testpublic void test6() { String patterStr = "yyyy-MM-dd HH:mm:ss"; Date currDate = new Date(System.currentTimeMillis()); // 北京時區 DateFormat bjDateFormat = new SimpleDateFormat(patterStr); bjDateFormat.setTimeZone(TimeZone.getDefault()); // 紐約時區 DateFormat newYorkDateFormat = new SimpleDateFormat(patterStr); newYorkDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York")); // 倫敦時區 DateFormat londonDateFormat = new SimpleDateFormat(patterStr); londonDateFormat.setTimeZone(TimeZone.getTimeZone("Europe/London")); System.out.println("毫秒數:" + currDate.getTime() + ", 北京本地時間:" + bjDateFormat.format(currDate)); System.out.println("毫秒數:" + currDate.getTime() + ", 紐約本地時間:" + newYorkDateFormat.format(currDate)); System.out.println("毫秒數:" + currDate.getTime() + ", 倫敦本地時間:" + londonDateFormat.format(currDate));}```執行程式,輸出:```java毫秒數:1610696040244, 北京本地時間:2021-01-15 15:34:00毫秒數:1610696040244, 紐約本地時間:2021-01-15 02:34:00毫秒數:1610696040244, 倫敦本地時間:2021-01-15 07:34:00```也就是說,同一個毫秒值,根據時區/偏移量的不同可以展示多地的時間,這就證明了Date它的時區無關性。**確切的說:Date物件裡存的是自格林威治時間( GMT)1970年1月1日0點至Date所表示時刻所經過的毫秒數**,是個數值。### 讀取字串為Date型別這是開發中極其常見的一種需求:client請求方扔給你一個字串如"2021-01-15 18:00:00",然後你需要把它轉為Date型別,怎麼破?問題來了,光禿禿的扔給我個字串說是15號晚上6點時間,我咋知道你指的是北京的晚上6點,還是東京的晚上6點呢?還是紐約的晚上6點呢?![](https://img-blog.csdnimg.cn/20210115154410245.png#pic_center)因此,對於字串形式的日期時間,只有指定了時區才有意義。也就是說**字串 + 時區** 才能精確知道它是什麼時刻,否則是存在歧義的。也許你可能會說了,自己平時開發中前端就是扔個字串給我,然後我就給格式化為一個Date型別,並沒有傳入時區引數,執行這麼久也沒見出什麼問題呀。如下所示:```java@Testpublic void test7() throws ParseException { String patterStr = "yyyy-MM-dd HH:mm:ss"; // 模擬請求引數的時間字串 String dateStrParam = "2020-01-15 18:00:00"; // 模擬服務端對此服務換轉換為Date型別 DateFormat dateFormat = new SimpleDateFormat(patterStr); System.out.println("格式化器用的時區是:" + dateFormat.getTimeZone().getID()); Date date = dateFormat.parse(dateStrParam); System.out.println(date);}```執行程式,輸出:```java格式化器用的時區是:Asia/ShanghaiWed Jan 15 18:00:00 CST 2020```看起來結果沒問題。事實上,這是因為預設情況下你們互動雙發就達成了契約:雙方均使用的是北京時間(時區),既然是相同時區,所以互通有無不會有任何問題。不信你把你介面給海外使用者除錯試?對於格式化器來講,雖然說程式設計過程中一般情況下我們並不需要給DateFormat設定時區(那就用預設時區唄)就可正常轉換。但是作為高手的你必須清清楚楚,明明白白的知道這是由於互動雙發預設**有個相同時區的契約存在**。### SimpleDateFormat格式化Java中對Date型別的輸入輸出/格式化,推薦使用DateFormat而非用其`toString()`方法。DateFormat是一個時間格式化器抽象類,SimpleDateFormat是其具體實現類,用於以**語言環境敏感**的方式格式化和解析日期。它允許格式化(日期→文字)、解析(文字→日期)和規範化。> 劃重點:對語言環境敏感,也就是說對環境Locale、時區TimeZone都是敏感的。既然敏感,那就是**可定製的**對於一個格式化器來講,**模式**(模版)是其關鍵因素,瞭解一下:**日期/時間模式**:格式化的模式由指定的字串組成,未加引號的大寫/小寫字母(A-Z a-z)代表特定模式,用來表示模式含義,若想**原樣輸出**可以用單引號''包起來,除了英文字母其它均不解釋原樣輸出/匹配。下面是它規定的模式字母(其它字母原樣輸出):字母 | 含義 | 匹配型別 | 示例-------- | ----- | ----- | -----**y** | 年 | Year | 2020,20**M** | 月 | Month | July; Jul; 07**d** | 月中的天數(俗稱日,最大值31) | Number | 10**H** | 小時(0-23) | Number| 0,23**m** | 分鐘(0-59) | Number | 30,59**s** | 秒(0-59) | Number | 30,59--- | --- | --- | yyyy-MM-dd HH:mm:ss(分隔符可以是任意字元,甚至漢字)**Y** | 當前周所在的年份 | Year | 2020(不建議使用,周若跨年有坑)**S** | 毫秒數(1-999) | Number | 999**a** | am/pm | Text | PM**z** | 時區 | 通用時區 | Pacific Standard Time; PST; GMT-08:00**Z** | 時區 | RFC 822時區 | -0800,+0800**X** | 時區 | ISO 8601時區 | -08; -0800; -08:00**G** | 年代 | Text | AD(公元)、BC(公元前)**D** | 年中的天數(1-366) | Number | 360**w** | 年中的週數(1-54) | Number | 27**W** | 月中的週數(1-5) | Number | 3**E** | 星期幾名稱 | Text | Tuesday; Tue**u** | 星期幾數字(1=Monday...) | Number | 1**k** | 小時(1-24) | Number | 不建議使用**K/h** | am/pm小時數字 | Number | 一般配合a一起使用這個表格裡出現了一些“特殊”的匹配型別,做如下解釋:- **Text**:格式化(Date -> String),如果模式字母的數目是4個或更多,則使用完整形式;否則,如果可能的話,使用簡短或縮寫形式。對於解析(String -> Date),這兩種形式都一樣,與模式字母的數量無關```java@Testpublic void test9() throws ParseException { String patternStr = "G GG GGGGG E EE EEEEE a aa aaaaa"; Date currDate = new Date(); System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); System.out.println("====================Date->String===================="); DateFormat dateFormat = new SimpleDateFormat(patternStr, Locale.CHINA); System.out.println(dateFormat.format(currDate)); System.out.println("====================String->Date===================="); String dateStrParam = "公元 公元 公元 星期六 星期六 星期六 下午 下午 下午"; System.out.println(dateFormat.parse(dateStrParam)); System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); System.out.println("====================Date->String===================="); dateFormat = new SimpleDateFormat(patternStr, Locale.US); System.out.println(dateFormat.format(currDate)); System.out.println("====================String->Date===================="); dateStrParam = "AD ad bC Sat SatUrday sunDay PM PM Am"; System.out.println(dateFormat.parse(dateStrParam));}```執行程式,輸出:```java↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓====================Date->String====================公元 公元 公元 星期六 星期六 星期六 下午 下午 下午====================String->Date====================Sat Jan 03 12:00:00 CST 1970↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓====================Date->String====================AD AD AD Sat Sat Saturday PM PM PM====================String->Date====================Sun Jan 01 00:00:00 CST 1970```觀察列印結果,除了符合模式規則外,還能在**String -> Date解析時**總結出兩點結論:1. 英文單詞,不分割槽大小寫。如SatUrday sunDay都是沒問題,但是**不能**有拼寫錯誤2. 若有多個part表示一個意思,那麼last win。如Sat SatUrday sunDay最後一個生效對於Locale地域引數,因為中文不存在格式、縮寫方面的特性,因此這些規則只對英文地域(如Locale.US生效)- **Number**:格式化(Date -> String),模式字母的數量是數字的【最小】數量,較短的數字被零填充到這個數量。對於解析(String -> Date),模式字母的數量將被忽略,除非需要分隔兩個相鄰的欄位- **Year**:對於格式化和解析,如果模式字母的數量是4個或更多,則使用特定於日曆的長格式。否則,使用日曆特定的簡短或縮寫形式- **Month**:如果模式字母的數量是3個或更多,則被解釋為文字;否則,它將被解釋為一個數字。- **通用時區**:如果該時區有名稱,如Pacific Standard Time、PST、CST等那就用名稱,否則就用GMT規則的字串,如:GMT-08:00- **RFC 822時區**:遵循RFC 822格式,向下相容通用時區(名稱部分除外)- **ISO 8601時區**:對於格式化,如果與GMT的偏移值為0(也就是格林威治時間嘍),則生成“Z”;如果模式字母的數量為1,則忽略小時的任何分數。例如,如果模式是“X”,時區是“GMT+05:30”,則生成“+05”。在進行解析時,“Z”被解析為UTC時區指示符。一般時區不被接受。如果模式字母的數量是4個或更多,在構造SimpleDateFormat或應用模式時丟擲IllegalArgumentException。 - 這個規則理解起來還是比較費勁的,在開發中一般不太建議使用此種模式。若要使用請務必本地做好測試SimpleDateFormat的使用很簡單,重點是瞭解其規則模式。最後關於SimpleDateFormat的使用再強調這兩點哈:1. SimpleDateFormat並非執行緒安全類,使用時請務必注意併發安全問題2. 若使用SimpleDateFormat去格式化成非本地區域(預設Locale)的話,那就必須在構造的時候就指定好,如Locale.US3. 對於Date型別的任何格式化、解析請統一使用SimpleDateFormat## JSR 310型別曾經有個人做了個很有意思的投票,統計對Java API的**不滿意**程度。最終Java Date/Calendar API斬獲第二爛(第一爛是Java XML/DOM),體現出它爛的點較多,這裡給你例舉幾項:1. 定義並不一致,在java.util和java.sql包中竟然都有Date類,而且呢對它進行格式化/解析類竟然又跑到java.text去了,精神分裂啊2. java.util.Date等類在建模日期的設計上行為不一致,缺陷明顯。包括易變性、糟糕的偏移值、預設值、命名等等3. java.util.Date同時包含日期和時間,而其子類java.sql.Date卻僅包含日期,這是什麼神繼承?![](https://img-blog.csdnimg.cn/20210116214805226.png#pic_center)```java@Testpublic void test10() { long currMillis = System.currentTimeMillis(); java.util.Date date = new Date(currMillis); java.sql.Date sqlDate = new java.sql.Date(currMillis); java.sql.Time time = new Time(currMillis); java.sql.Timestamp timestamp = new Timestamp(currMillis); System.out.println("java.util.Date:" + date); System.out.println("java.sql.Date:" + sqlDate); System.out.println("java.sql.Time:" + time); System.out.println("java.sql.Timestamp:" + timestamp);}```執行程式,輸出:```javajava.util.Date:Sat Jan 16 21:50:36 CST 2021java.sql.Date:2021-01-16java.sql.Time:21:50:36java.sql.Timestamp:2021-01-16 21:50:36.733```- 國際化支援得並不是好,比如跨時區操作、夏令時等等Java 自己也實在忍不了這麼難用的日期時間API了,於是在2014年隨著Java 8的釋出引入了全新的JSR 310日期時間。JSR-310源於精品時間庫joda-time打造,解決了上面提到的**所有問題**,是整個Java 8最大亮點之一。JSR 310日期/時間 **所有的** API都在java.time這個包內,沒有例外。![](https://img-blog.csdnimg.cn/20210117054303212.png#pic_center)當然嘍,本文重點並不在於討論JSR 310日期/時間體系,而是看看JSR 310日期時間型別是如何處理上面Date型別遇到的那些case的。### 時區/偏移量ZoneId在JDK 8之前,Java使用`java.util.TimeZone`來表示時區。而在JDK 8裡分別使用了ZoneId表示時區,ZoneOffset表示UTC的偏移量。值得提前強調,時區和偏移量在概念和實際作用上是有較大區別的,主要體現在:1. UTC偏移量僅僅記錄了偏移的小時分鐘而已,除此之外無任何其它資訊。舉個例子:+08:00的意思是比UTC時間早8小時,沒有地理/時區含義,相應的-03:30代表的意思僅僅是比UTC時間晚3個半小時2. 時區是特定於地區而言的,它和地理上的地區(包括規則)強繫結在一起。比如整個中國都叫東八區,紐約在西五區等等> 中國沒有夏令時,所有東八區對應的偏移量永遠是+8;紐約有夏令時,因此它的偏移量可能是-4也可能是-5哦**綜合來看,時區更好用**。令人惱火的夏令時問題,若你使用UTC偏移量去表示那麼就很麻煩,因為它可變:一年內的某些時期在原來基礎上偏移量 +1,某些時期 -1;但若你使用ZoneId時區去表示就很方便嘍,比如紐約是西五區,你在任何時候獲取其當地時間都是能得到正確答案的,因為它內建了對夏令時規則的處理,也就是說啥時候+1啥時候-1時區自己門清,不需要API呼叫者關心。UTC偏移量更像是一種寫死偏移量數值的做法,這在天朝這種沒有時區規則(沒有夏令時)的國家不會存在問題,東八區和UTC+08:00效果永遠一樣。但在一些夏令時國家(如美國、法國等等),就只能根據時區去獲取當地時間嘍。所以當你不瞭解當地規則時,最好是使用時區而非偏移量。#### ZoneId![](https://img-blog.csdnimg.cn/20210117061051448.png#pic_center)它代表一個時區的ID,如Europe/Paris。它規定了一些規則可用於將一個Instant時間戳轉換為本地日期/時間LocalDateTime。上面說了時區ZoneId是包含有規則的,實際上描述偏移量何時以及如何變化的實際規則由`java.time.zone.ZoneRules`定義。ZoneId則只是一個用於獲取底層規則的ID。之所以採用這種方法,是因為**規則是由政府定義的,並且經常變化,而ID是穩定的**。對於API呼叫者來說只需要使用這個ID(也就是ZoneId)即可,而需無關心更為底層的時區規則ZoneRules,和“政府”同步規則的事是它領域內的事就交給它嘍。如:夏令時這條規則是由各國政府制定的,而且不同國家不同年一般都不一樣,這個事就交由JDK底層的ZoneRules機制自行sync,使用者無需關心。ZoneId在系統內是唯一的,它共包含三種類型的ID:1. 最簡單的ID型別:ZoneOffset,它由'Z'和以'+'或'-'開頭的id組成。如:Z、+18:00、-18:002. 另一種型別的ID是帶有某種字首形式的偏移樣式ID,例如'GMT+2'或'UTC+01:00'。可識別的(合法的)字首是'UTC', 'GMT'和'UT'3. 第三種類型是基於區域的ID(推薦使用)。基於區域的ID必須包含兩個或多個字元,且不能以'UTC'、'GMT'、'UT' '+'或'-'開頭。基於區域的id由配置定義好的,如Europe/Paris概念說了一大推,下面給幾個程式碼示例感受下吧。1、獲取系統預設的ZoneId:```java@Testpublic void test1() { // JDK 1.8之前做法 System.out.println(TimeZone.getDefault()); // JDK 1.8之後做法 System.out.println(ZoneId.systemDefault());}輸出:Asia/Shanghaisun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null]```二者結果是一樣的,都是Asia/Shanghai。因為ZoneId方法底層就是依賴TimeZone,如圖:![](https://img-blog.csdnimg.cn/20210117064928306.png#pic_center)![](https://img-blog.csdnimg.cn/20210117065004959.png#pic_center)2、指定字串得到一個ZoneId:```java@Testpublic void test2() { System.out.println(ZoneId.of("Asia/Shanghai")); // 報錯:java.time.zone.ZoneRulesException: Unknown time-zone ID: Asia/xxx System.out.println(ZoneId.of("Asia/xxx"));}```很明顯,這個字串也是不能隨便寫的。那麼問題來了,可寫的有哪些呢?同樣的ZoneId提供了API供你獲取到所有可用的字串id,有興趣的同學建議自行嘗試:```java@Testpublic void test3() { ZoneId.getAvailableZoneIds();}```3、根據偏移量得到一個ZoneId:```java@Testpublic void test4() { ZoneId zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("+8")); System.out.println(zoneId); // 必須是大寫的Z zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("Z")); System.out.println(zoneId);}輸出:UTC+08:00UTC```這裡第一個引數傳的字首,可用值為:"GMT", "UTC", or "UT"。當然還可以傳空串,那就直接返回第二個引數ZoneOffset。若以上都不是就報錯注意:根據偏移量得到的ZoneId內部並無現成時區規則可用,因此對於有夏令營的國家轉換可能出問題,一般不建議這麼去做。4、從日期裡面獲得時區:```java@Testpublic void test5() { System.out.println(ZoneId.from(ZonedDateTime.now())); System.out.println(ZoneId.from(ZoneOffset.of("+8"))); // 報錯:java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: System.out.println(ZoneId.from(LocalDateTime.now())); System.out.println(ZoneId.from(LocalDate.now()));}```雖然方法入參是TemporalAccessor,但是隻接受帶時區的型別,LocalXXX是不行的,使用時稍加註意。#### ZoneOffset距離格林威治/UTC的時區偏移量,例如+02:00。值得注意的是它繼承自ZoneId,所以也可當作一個ZoneId來使用的,當然並不建議你這麼去做,請獨立使用。時區偏移量是時區與格林威治/UTC之間的時間差。這通常是固定的小時數和分鐘數。世界不同的地區有不同的時區偏移量。在ZoneId類中捕獲關於偏移量如何隨一年的地點和時間而變化的規則(主要是夏令時規則),所以繼承自ZoneId。1、最小/最大偏移量:因為偏移量傳入的是數字,這個是有限制的哦```java@Testpublic void test6() { System.out.println("最小偏移量:" + ZoneOffset.MIN); System.out.println("最小偏移量:" + ZoneOffset.MAX); System.out.println("中心偏移量:" + ZoneOffset.UTC); // 超出最大範圍 System.out.println(ZoneOffset.of("+20"));}輸出:最小偏移量:-18:00最小偏移量:+18:00中心偏移量:Zjava.time.DateTimeException: Zone offset hours not in valid range: value 20 is not in the range -18 to 18```2、通過時分秒構造偏移量(使用很方便,推薦):```java@Testpublic void test7() { System.out.println(ZoneOffset.ofHours(8)); System.out.println(ZoneOffset.ofHoursMinutes(8, 8)); System.out.println(ZoneOffset.ofHoursMinutesSeconds(8, 8, 8)); System.out.println(ZoneOffset.ofHours(-5)); // 指定一個精確的秒數 獲取例項(有時候也很有用處) System.out.println(ZoneOffset.ofTotalSeconds(8 * 60 * 60));}// 輸出:+08:00+08:08+08:08:08-05:00+08:00```看來,偏移量是能精確到秒的哈,只不過一般來說精確到分鐘已經到頂了。##### 設定預設時區ZoneId並沒有提供設定預設時區的方法,但是通過文章可知ZoneId獲取預設時區底層依賴的是`TimeZone.getDefault()`方法,因此設定預設時區方式完全遵照TimeZone的方式即可(共三種方式,還記得嗎?)。### 讓人惱火的夏令時因為有夏令時規則的存在,讓操作日期/時間的複雜度大大增加。但還好JDK儘量的遮蔽了這些規則對使用者的影響。因此:推薦使用時區(ZoneId)轉換日期/時間,一般情況下不建議使用偏移量ZoneOffset去搞,這樣就不會有夏令時的煩惱啦。### JSR 310時區相關性java.util.Date型別它具有時區無關性,帶來的弊端就是一旦涉及到國際化時間轉換等需求時,使用Date來處理是很不方便的。JSR 310解決了Date存在的一系列問題:對日期、時間進行了分開表示(LocalDate、LocalTime、LocalDateTime),對本地時間和帶時區的時間進行了分開管理。LocalXXX表示本地時間,也就是說是當前JVM所在時區的時間;ZonedXXX表示是一個**帶有時區**的日期時間,它們能非常方便的互相完成轉換。```java@Testpublic void test8() { // 本地日期/時間 System.out.println("================本地時間================"); System.out.println(LocalDate.now()); System.out.println(LocalTime.now()); System.out.println(LocalDateTime.now()); // 時區時間 System.out.println("================帶時區的時間ZonedDateTime================"); System.out.println(ZonedDateTime.now()); // 使用系統時區 System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York"))); // 自己指定時區 System.out.println(ZonedDateTime.now(Clock.systemUTC())); // 自己指定時區 System.out.println("================帶時區的時間OffsetDateTime================"); System.out.println(OffsetDateTime.now()); // 使用系統時區 System.out.println(OffsetDateTime.now(ZoneId.of("America/New_York"))); // 自己指定時區 System.out.println(OffsetDateTime.now(Clock.systemUTC())); // 自己指定時區}```執行程式,輸出:```java================本地時間================2021-01-1709:18:40.7032021-01-17T09:18:40.703================帶時區的時間ZonedDateTime================2021-01-17T09:18:40.704+08:00[Asia/Shanghai]2021-01-16T20:18:40.706-05:00[America/New_York]2021-01-17T01:18:40.709Z================帶時區的時間OffsetDateTime================2021-01-17T09:18:40.710+08:002021-01-16T20:18:40.710-05:002021-01-17T01:18:40.710Z```本地時間的輸出非常“乾淨”,可直接用於顯示。帶時區的時間顯示了該時間代表的是哪個時區的時間,畢竟不指定時區的時間是沒有任何意義的。LocalXXX因為它具有時區無關性,因此它不能代表一個瞬間/時刻。另外,關於LocalDateTime、OffsetDateTime、ZonedDateTime三者的跨時區轉換問題,以及它們的詳解,因為內容過多放在了下文專文闡述,保持關注。### 讀取字串為JSR 310型別一個獨立的日期時間型別字串如2021-05-05T18:00-04:00它是沒有任何意義的,因為沒有時區無法確定它代表那個瞬間,這是理論當然也適合JSR 310型別嘍。遇到一個日期時間格式字串,要解析它一般有這兩種情況:1. 不帶時區/偏移量的字串:要麼不理它說轉換不了,要麼就**約定一個時區**(一般用系統預設時區),使用LocalDateTime來解析```java@Testpublic void test11() { String dateTimeStrParam = "2021-05-05T18:00"; LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam); System.out.println("解析後:" + localDateTime);}輸出:解析後:2021-05-05T18:00```2. 帶時區字/偏移量的符串:```java@Testpublic void test12() { // 帶偏移量 使用OffsetDateTime String dateTimeStrParam = "2021-05-05T18:00-04:00"; OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStrParam); System.out.println("帶偏移量解析後:" + offsetDateTime); // 帶時區 使用ZonedDateTime dateTimeStrParam = "2021-05-05T18:00-05:00[America/New_York]"; ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStrParam); System.out.println("帶時區解析後:" + zonedDateTime);}輸出:帶偏移量解析後:2021-05-05T18:00-04:00帶時區解析後:2021-05-05T18:00-04:00[America/New_York]```請注意**帶時區解析後**這個結果:字串引數偏移量明明是-05,為毛轉換為ZonedDateTime後偏移量成為了-04呢???這裡是我故意造了這麼一個case引起你的重視,對此結果我做如下解釋:![](https://img-blog.csdnimg.cn/20210117194528171.png#pic_center)如圖,在2021.03.14 - 2021.11.07期間,紐約的偏移量是-4,其餘時候是-5。本例的日期是2021-05-05處在夏令時之中,因此偏移量是-4,這就解釋了為何你顯示的寫了-5最終還是成了-4。### JSR 310格式化針對JSR 310日期時間型別的格式化/解析,有個專門的類`java.time.format.DateTimeFormatter`用於處理。DateTimeFormatter也是一個不可變的類,所以是執行緒安全的,比SimpleDateFormat靠譜多了吧。另外它還內建了非常多的格式化模版**例項**供以使用,形如:格式化器 | 示例-------- | -----ofLocalizedDate(dateStyle) | '2021-01-03'ofLocalizedTime(timeStyle) | '10:15:30'ofLocalizedDateTime(dateTimeStyle) | '3 Jun 2021 11:05:30'**ISO_LOCAL_DATE** | '2021-12-03'**ISO_LOCAL_TIME** | '10:15:30'**ISO_LOCAL_DATE_TIME** | '2021-12-03T10:15:30'ISO_OFFSET_DATE_TIME | '2021-12-03T10:15:30+01:00'ISO_ZONED_DATE_TIME | '2021-12-03T10:15:30+01:00[Europe/Paris]'```java@Testpublic void test13() { System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now())); System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now())); System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now()));}輸出:2021-01-1722:43:21.3982021-01-17T22:43:21.4```若想自定義模式pattern,和Date一樣它也可以自己指定任意的pattern **日期/時間模式**。由於本文在Date部分詳細介紹了日期/時間模式,各個字母代表什麼意思以及如何使用,這裡就不再贅述了哈。> 雖然DateTimeFormatter支援的模式比Date略有增加,但大體還保持一致,個人覺得這塊無需再花精力。若真有需要再查官網也不遲```java@Testpublic void test14() { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("第Q季度 yyyy-MM-dd HH:mm:ss", Locale.US); // 格式化輸出 System.out.println(formatter.format(LocalDateTime.now())); // 解析 String dateTimeStrParam = "第1季度 2021-01-17 22:51:32"; LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam, formatter); System.out.println("解析後的結果:" + localDateTime);}```Q/q:季度,如3; 03; Q3; 3rd quarter。## 最佳實踐- **棄用Date,擁抱JSR 310**每每說到JSR 310日期/時間時我都會呼籲,保持慣例我這裡繼續囉嗦一句:放棄Date甚至禁用Date,使用JSR 310日期/時間吧,它才是日期時間處理的最佳實踐。另外,在使用期間關於制定時區(預設時區時)依舊有一套我心目中的最佳實踐存在,這裡分享給你:- **永遠顯式的指定你需要的時區,即使你要獲取的是預設時區**```java// 方式一:普通做法LocalDateTime.now();// 方式二:最佳實踐LocalDateTime.now(ZoneId.systemDefault());```如上程式碼二者效果一模一樣。但是方式二是最佳實踐。理由是:這樣做能讓程式碼帶有**明確的意圖**,消除模稜兩可的可能性,即使獲取的是預設時區。拿方式一來說吧,它就存在意圖不明確的地方:到底是程式碼編寫者忘記指定時區欠考慮了,還是就想用預設時區呢?這個答案如果不通讀上下文是無法確定的,從而造成了不必要的溝通維護成本。因此即使你是要獲取預設時區,也請顯示的用ZoneId.systemDefault()寫上去。- **使用JVM的預設時區需當心,建議時區和當前會話保持繫結**這個最佳實踐在特殊場景用得到。這麼做的理由是:JVM的預設時區通過靜態方法TimeZone#setDefault()可全域性設定,因此JVM的任何一個執行緒都可以隨意更改預設時區。若關於時間處理的程式碼**對時區非常敏感**的話,最佳實踐是你把時區資訊和當前會話繫結,這樣就可以不用再受到其它執行緒潛在影響了,確保了健壯性。> 說明:會話可能只是當前請求,也可能是一個Session,具體case具體分析# 總結通過[上篇文章](https://mp.weixin.qq.com/s/VdoQt88JfjPJTL9XgohZJQ) 對日期時間相關概念的鋪墊,加上本文的實操程式碼演示,達到弄透Java對日期時間的處理基本不成問題。兩篇文章的內容較多,資訊量均比較大,消化起來需要些時間。一方面我建議你先蒐藏留以當做參考書備用,另一方面建議多實踐,程式碼這東西只有多寫寫才能有更深體會。後面會**再用3 -4篇文章**對這前面這兩篇的細節、使用場景進行補充,比如如何去匹配ZoneId和Offset的對應關係,LocalDateTime、OffsetDateTime、ZonedDateTime跨時區互轉問題、在Spring MVC場景下使用的最佳實踐等等,敬請關注,一起進步。## 本文思考題看完了不一定懂,看懂了不一定會。來,文末3個思考題幫你覆盤:1. Date型別如何處理夏令時?2. ZoneId和ZoneOffset有什麼區別?3. 平時專案若遇到日期時間的處理,有哪些最佳實踐?## 推薦閱讀[**GMT UTC CST ISO 夏令時 時間戳,都是些什麼鬼?**](https://mp.weixin.qq.com/s/VdoQt88JfjPJTL9XgohZJQ)## 關注我分享、成長,拒絕淺藏輒止。關注【BAT的烏托邦】回覆關鍵字**專欄**有Spring技術棧、中介軟體等小而美的純原創專欄。本文已被 [https://www.yourbatman.cn](https://www.yourbatman.cn) 收錄。本文所屬專欄:**JDK日期時間**,公號後臺回覆專欄名即可獲取全部內容。A哥(YourBatman):Spring Framework/Boot開源貢獻者,Java架構師。非常注重**基本功修養**,相信底層基礎決定上層建築,堅實基礎才能煥發程式設計師更強生命力。文章特點為以小而美專欄形式重構知識體系,抽絲剝繭,致力於做人人能看懂的最好的專欄系列。可加我好友(**fsx1056342982**)共勉哦!![](https://img-blog.csdnimg.cn/20210121074215630.gif#pic_center)
版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1611228604.html

  1. 【计算机网络 12(1),尚学堂马士兵Java视频教程
  2. 【程序猿历程,史上最全的Java面试题集锦在这里
  3. 【程序猿历程(1),Javaweb视频教程百度云
  4. Notes on MySQL 45 lectures (1-7)
  5. [computer network 12 (1), Shang Xuetang Ma soldier java video tutorial
  6. The most complete collection of Java interview questions in history is here
  7. [process of program ape (1), JavaWeb video tutorial, baidu cloud
  8. Notes on MySQL 45 lectures (1-7)
  9. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  10. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  11. 精进 Spring Boot 03:Spring Boot 的配置文件和配置管理,以及用三种方式读取配置文件
  12. Refined spring boot 03: spring boot configuration files and configuration management, and reading configuration files in three ways
  13. 【递归,Java传智播客笔记
  14. [recursion, Java intelligence podcast notes
  15. [adhere to painting for 386 days] the beginning of spring of 24 solar terms
  16. K8S系列第八篇(Service、EndPoints以及高可用kubeadm部署)
  17. K8s Series Part 8 (service, endpoints and high availability kubeadm deployment)
  18. 【重识 HTML (3),350道Java面试真题分享
  19. 【重识 HTML (2),Java并发编程必会的多线程你竟然还不会
  20. 【重识 HTML (1),二本Java小菜鸟4面字节跳动被秒成渣渣
  21. [re recognize HTML (3) and share 350 real Java interview questions
  22. [re recognize HTML (2). Multithreading is a must for Java Concurrent Programming. How dare you not
  23. [re recognize HTML (1), two Java rookies' 4-sided bytes beat and become slag in seconds
  24. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  25. RPC 1: how to develop RPC framework from scratch
  26. 造轮子系列之RPC 1:如何从零开始开发RPC框架
  27. RPC 1: how to develop RPC framework from scratch
  28. 一次性捋清楚吧,对乱糟糟的,Spring事务扩展机制
  29. 一文彻底弄懂如何选择抽象类还是接口,连续四年百度Java岗必问面试题
  30. Redis常用命令
  31. 一双拖鞋引发的血案,狂神说Java系列笔记
  32. 一、mysql基础安装
  33. 一位程序员的独白:尽管我一生坎坷,Java框架面试基础
  34. Clear it all at once. For the messy, spring transaction extension mechanism
  35. A thorough understanding of how to choose abstract classes or interfaces, baidu Java post must ask interview questions for four consecutive years
  36. Redis common commands
  37. A pair of slippers triggered the murder, crazy God said java series notes
  38. 1、 MySQL basic installation
  39. Monologue of a programmer: despite my ups and downs in my life, Java framework is the foundation of interview
  40. 【大厂面试】三面三问Spring循环依赖,请一定要把这篇看完(建议收藏)
  41. 一线互联网企业中,springboot入门项目
  42. 一篇文带你入门SSM框架Spring开发,帮你快速拿Offer
  43. 【面试资料】Java全集、微服务、大数据、数据结构与算法、机器学习知识最全总结,283页pdf
  44. 【leetcode刷题】24.数组中重复的数字——Java版
  45. 【leetcode刷题】23.对称二叉树——Java版
  46. 【leetcode刷题】22.二叉树的中序遍历——Java版
  47. 【leetcode刷题】21.三数之和——Java版
  48. 【leetcode刷题】20.最长回文子串——Java版
  49. 【leetcode刷题】19.回文链表——Java版
  50. 【leetcode刷题】18.反转链表——Java版
  51. 【leetcode刷题】17.相交链表——Java&python版
  52. 【leetcode刷题】16.环形链表——Java版
  53. 【leetcode刷题】15.汉明距离——Java版
  54. 【leetcode刷题】14.找到所有数组中消失的数字——Java版
  55. 【leetcode刷题】13.比特位计数——Java版
  56. oracle控制用户权限命令
  57. 三年Java开发,继阿里,鲁班二期Java架构师
  58. Oracle必须要启动的服务
  59. 万字长文!深入剖析HashMap,Java基础笔试题大全带答案
  60. 一问Kafka就心慌?我却凭着这份,图灵学院vip课程百度云