精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

建議收藏:徹底弄透Java處理GMT/UTC日期時間

開發 后端
本文內容較多,文字較長,預計超2w字,旨在全面的徹底幫你搞定Java對日期時間的處理,建議你可收藏,作為參考書留以備用。

 你好,我是A哥(YourBatman)。

本系列的目的是明明白白、徹徹底底的搞定日期/時間處理的幾乎所有case。上篇文章 鋪設所有涉及到的概念解釋,例如GMT、UTC、夏令時、時間戳等等,若你還沒看過,不僅強烈建議而是強制建議你前往用花5分鐘看一下,因為日期時間處理較為特殊,實戰必須基于對概念的了解,否則很可能依舊霧里看花。

  • ❝說明:日期/時間的處理是日常開發非常常見的老大難,究其原因就是對日期時間的相關概念、應用場景不熟悉,所以不要忽視它❞上篇概念,本文落地實操,二者相輔相成,缺一不可。本文內容較多,文字較長,預計超2w字,旨在全面的徹底幫你搞定Java對日期時間的處理,建議你可收藏,作為參考書留以備用。

本文提綱


版本約定

JDK:8

✍正文

上文鋪了這么多概念,作為一枚Javaer最關心當然是這些“概念”在Java里的落地。平時工作中遇到時間如何處理?用Date還是JDK 8之后的日期時間API?如何解決跨時區轉換等等頭大問題。A哥向來管生管養,管殺管埋,因此本文就帶你領略一下,Java是如何實現GMT和UTC的?

眾所周知,JDK以版本8為界,有兩套處理日期/時間的API:


雖然我一直鼓勵棄用Date而支持在項目中只使用JSR 310日期時間類型,但是呢,由于Date依舊有龐大的存量用戶,所以本文也不落單,對二者的實現均進行闡述。

Date類型實現

java.util.Date在JDK 1.0就已存在,用于表示日期 + 時間的類型,縱使年代已非常久遠,并且此類的具有職責不單一,使用很不方便等諸多毛病,但由于十幾二十年的歷史原因存在,它的生命力依舊頑強,用戶量巨大。

先來認識下Date,看下這個例子的輸出:

  1. @Test 
  2. public void test1() { 
  3.     Date currDate = new Date(); 
  4.     System.out.println(currDate.toString()); 
  5.     // 已經@Deprecated 
  6.     System.out.println(currDate.toLocaleString()); 
  7.     // 已經@Deprecated 
  8.     System.out.println(currDate.toGMTString()); 

運行程序,輸出:

  1. Fri Jan 15 10:22:34 CST 2021 
  2. 2021-1-15 10:22:34 
  3. 15 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所運行的時區,比如你在中國運行程序,這個方法返回的就是中國時區(也叫北京時區、北京時間)。

有的時候你需要做帶時區的時間轉換,譬如:接口返回值中既要有展示北京時間,也要展示紐約時間。這個時候就要獲取到紐約的時區,以北京時間為基準在其上進行帶時區轉換一把:

  1. @Test 
  2. public void test2() { 
  3.     String patternStr = "yyyy-MM-dd HH:mm:ss"
  4.     // 北京時間(new出來就是默認時區的時間) 
  5.     Date bjDate = new Date(); 
  6.  
  7.     // 得到紐約的時區 
  8.     TimeZone newYorkTimeZone = TimeZone.getTimeZone("America/New_York"); 
  9.     // 根據此時區 將北京時間轉換為紐約的Date 
  10.     DateFormat newYorkDateFormat = new SimpleDateFormat(patternStr); 
  11.     newYorkDateFormat.setTimeZone(newYorkTimeZone); 
  12.     System.out.println("這是北京時間:" + new SimpleDateFormat(patternStr).format(bjDate)); 
  13.     System.out.println("這是紐約時間:" + newYorkDateFormat.format(bjDate)); 

運行程序,輸出:

  1. 這是北京時間:2021-01-15 11:48:16 
  2. 這是紐約時間:2021-01-14 22:48:16 

(11 + 24) - 22 = 13,北京比紐約快13個小時沒毛病。

  • ❝注意:兩個時間表示的應該是同一時刻,也就是常說的時間戳值是相等的❞那么問題來了,你怎么知道獲取紐約的時區用America/New_York這個zoneId呢?隨便寫個字符串行不行?

答案是當然不行,這是有章可循的。下面我介紹兩種查閱zoneId的方式,任你挑選:

方式一:用Java程序把所有可用的zoneId打印出來,然后查閱

  1. @Test 
  2. public void test3() { 
  3.     String[] availableIDs = TimeZone.getAvailableIDs(); 
  4.     System.out.println("可用zoneId總數:" + availableIDs.length); 
  5.     for (String zoneId : availableIDs) { 
  6.         System.out.println(zoneId); 
  7.     } 

運行程序,輸出(大部分符合規律:/前表示所屬州,/表示城市名稱):

  1. 可用zoneId總數:628 
  2. Africa/Abidjan 
  3. Africa/Accra 
  4. ... 
  5. Asia/Chongqing // 亞洲/重慶 
  6. Asia/Shanghai // 亞洲/上海 
  7. Asia/Dubai // 亞洲/迪拜 
  8. ... 
  9. America/New_York // 美洲/紐約 
  10. America/Los_Angeles // 美洲/洛杉磯 
  11. ... 
  12. Europe/London // 歐洲/倫敦 
  13. ... 
  14. Etc/GMT 
  15. Etc/GMT+0 
  16. Etc/GMT+1 
  17. ... 

值得注意的是并沒有 Asia/Beijing 哦。

  • ❝說明:此結果基于JDK 8版本,不同版本輸出的總個數可能存在差異,但主流的ZoneId一般不會有變化❞

方式二:zoneId的列表是jre維護的一個文本文件,路徑是你JDK/JRE的安裝路徑。地址在.\jre\lib目錄的名為tzmappings的文本文件里。打開這個文件去ctrl + f找也是可以達到查找的目的的。

這兩種房子可以幫你找到ZoneId的字典方便查閱,但是還有這么一種情況:當前所在的城市呢,在tzmappings文件里根本沒有(比如沒有收錄),那要獲取這個地方的時間去顯示怎么破呢?雖然概率很小,但不見得沒有嘛,畢竟全球那么多國家那么多城市呢~

Java自然也考慮到了這一點,因此也是有辦法的:指定其時區數字表示形式,其實也叫偏移量(不要告訴我這個地方的時區都不知道,那就真沒救了),如下示例

  1. @Test 
  2. public void test4() { 
  3.     System.out.println(TimeZone.getTimeZone("GMT+08:00").getID()); 
  4.     System.out.println(TimeZone.getDefault().getID()); 
  5.  
  6.     // 紐約時間 
  7.     System.out.println(TimeZone.getTimeZone("GMT-05:00").getID()); 
  8.     System.out.println(TimeZone.getTimeZone("America/New_York").getID()); 

運行程序,輸出:

  1. GMT+08:00 // 效果等同于Asia/Shanghai 
  2. Asia/Shanghai 
  3. GMT-05:00 // 效果等同于America/New_York 
  4. America/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. 運維設置方式:將操作系統主機時區設置為北京時區,這是推薦方式,可以完全對開發者無感,也方便了運維統一管理

據我了解,很多公司在阿里云、騰訊云、國內外的云主機上部署應用時,全部都是采用運維設置統一時區:中國時區,這種方式來管理的,這樣對程序來說就消除了默認時區不一致的問題,對開發者友好。

讓人惱火的夏令時

你知道嗎,中國曾經也使用過夏令時。

  • ❝什么是夏令時?戳這里❞

離現在最近是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呢?

針對此拷問,我模擬了如下代碼:

  1. @Test 
  2. public void test5() throws ParseException { 
  3.     String patterStr = "yyyy-MM-dd"
  4.     DateFormat dateFormat = new SimpleDateFormat(patterStr); 
  5.  
  6.     String birthdayStr = "1988-09-11"
  7.     // 字符串 -> Date -> 字符串 
  8.     Date birthday = dateFormat.parse(birthdayStr); 
  9.     long birthdayTimestamp = birthday.getTime(); 
  10.     System.out.println("老王的生日是:" + birthday); 
  11.     System.out.println("老王的生日的時間戳是:" + birthdayTimestamp); 
  12.  
  13.     System.out.println("==============程序經過一番周轉,我的同時 方法入參傳來了生日的時間戳============="); 
  14.     // 字符串 -> Date -> 時間戳 -> Date -> 字符串 
  15.     birthday = new Date(birthdayTimestamp); 
  16.     System.out.println("老王的生日是:" + birthday); 
  17.     System.out.println("老王的生日的時間戳是:" + dateFormat.format(birthday)); 

這段代碼,在不同的JDK版本下運行,可能出現不同的結果,有興趣的可copy過去自行試試。

關于JDK處理夏令時(特指中國的夏令時)確實出現過問題且造成過bug,當時對應的JDK版本是1.8.0_2xx之前版本格式化那個日期出問題了,在這之后的版本貌似就沒問題了。這里我提供的版本信息僅供參考,若有遇到類似case就升級JDK版本到最新吧,一般就不會有問題了。

  • ❝發生這個情況是在JDK非常小的版本號之間,不太好定位精確版本號界限,所以僅供參考❞

總的來說,只要你使用的是較新版本的JDK,開發者是無需關心夏令時問題的,即使全球仍有很多國家在使用夏令時,咱們只需要面向時區做時間轉換就沒問題。

Date時區無關性

類Date表示一個特定的時間瞬間,精度為毫秒。既然表示的是瞬間/時刻,那它必然和時區是無關的,看下面代碼:

  1. @Test 
  2. public void test6() { 
  3.     String patterStr = "yyyy-MM-dd HH:mm:ss"
  4.     Date currDate = new Date(System.currentTimeMillis()); 
  5.  
  6.     // 北京時區 
  7.     DateFormat bjDateFormat = new SimpleDateFormat(patterStr); 
  8.     bjDateFormat.setTimeZone(TimeZone.getDefault()); 
  9.     // 紐約時區 
  10.     DateFormat newYorkDateFormat = new SimpleDateFormat(patterStr); 
  11.     newYorkDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York")); 
  12.     // 倫敦時區 
  13.     DateFormat londonDateFormat = new SimpleDateFormat(patterStr); 
  14.     londonDateFormat.setTimeZone(TimeZone.getTimeZone("Europe/London")); 
  15.  
  16.     System.out.println("毫秒數:" + currDate.getTime() + ", 北京本地時間:" + bjDateFormat.format(currDate)); 
  17.     System.out.println("毫秒數:" + currDate.getTime() + ", 紐約本地時間:" + newYorkDateFormat.format(currDate)); 
  18.     System.out.println("毫秒數:" + currDate.getTime() + ", 倫敦本地時間:" + londonDateFormat.format(currDate)); 

運行程序,輸出:

  1. 毫秒數:1610696040244, 北京本地時間:2021-01-15 15:34:00 
  2. 毫秒數:1610696040244, 紐約本地時間:2021-01-15 02:34:00 
  3. 毫秒數: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點呢?

因此,對于字符串形式的日期時間,只有指定了時區才有意義。也就是說字符串 + 時區 才能精確知道它是什么時刻,否則是存在歧義的。

也許你可能會說了,自己平時開發中前端就是扔個字符串給我,然后我就給格式化為一個Date類型,并沒有傳入時區參數,運行這么久也沒見出什么問題呀。如下所示:

  1. @Test 
  2. public void test7() throws ParseException { 
  3.     String patterStr = "yyyy-MM-dd HH:mm:ss"
  4.  
  5.     // 模擬請求參數的時間字符串 
  6.     String dateStrParam = "2020-01-15 18:00:00"
  7.  
  8.     // 模擬服務端對此服務換轉換為Date類型 
  9.     DateFormat dateFormat = new SimpleDateFormat(patterStr); 
  10.     System.out.println("格式化器用的時區是:" + dateFormat.getTimeZone().getID()); 
  11.     Date date = dateFormat.parse(dateStrParam); 
  12.     System.out.println(date); 

運行程序,輸出:

  1. 格式化器用的時區是:Asia/Shanghai 
  2. Wed Jan 15 18:00:00 CST 2020 

看起來結果沒問題。事實上,這是因為默認情況下你們交互雙發就達成了契約:雙方均使用的是北京時間(時區),既然是相同時區,所以互通有無不會有任何問題。不信你把你接口給海外用戶調試試?

對于格式化器來講,雖然說編程過程中一般情況下我們并不需要給DateFormat設置時區(那就用默認時區唄)就可正常轉換。但是作為高手的你必須清清楚楚,明明白白的知道這是由于交互雙發默認有個相同時區的契約存在。

SimpleDateFormat格式化

Java中對Date類型的輸入輸出/格式化,推薦使用DateFormat而非用其toString()方法。

DateFormat是一個時間格式化器抽象類,SimpleDateFormat是其具體實現類,用于以語言環境敏感的方式格式化和解析日期。它允許格式化(日期→文本)、解析(文本→日期)和規范化。

  • ❝劃重點:對語言環境敏感,也就是說對環境Locale、時區TimeZone都是敏感的。既然敏感,那就是可定制的❞

對于一個格式化器來講,模式(模版)是其關鍵因素,了解一下:

日期/時間模式:格式化的模式由指定的字符串組成,未加引號的大寫/小寫字母(A-Z a-z)代表特定模式,用來表示模式含義,若想原樣輸出可以用單引號''包起來,除了英文字母其它均不解釋原樣輸出/匹配。下面是它規定的模式字母(其它字母原樣輸出):

這個表格里出現了一些“特殊”的匹配類型,做如下解釋:

Text:格式化(Date -> String),如果模式字母的數目是4個或更多,則使用完整形式;否則,如果可能的話,使用簡短或縮寫形式。對于解析(String -> Date),這兩種形式都一樣,與模式字母的數量無關

  1. @Test 
  2. public void test9() throws ParseException { 
  3.     String patternStr = "G GG GGGGG E EE EEEEE a aa aaaaa"
  4.     Date currDate = new Date(); 
  5.  
  6.     System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); 
  7.     System.out.println("====================Date->String===================="); 
  8.     DateFormat dateFormat = new SimpleDateFormat(patternStr, Locale.CHINA); 
  9.     System.out.println(dateFormat.format(currDate)); 
  10.  
  11.     System.out.println("====================String->Date===================="); 
  12.     String dateStrParam = "公元 公元 公元 星期六 星期六 星期六 下午 下午 下午"
  13.     System.out.println(dateFormat.parse(dateStrParam)); 
  14.  
  15.     System.out.println("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓"); 
  16.     System.out.println("====================Date->String===================="); 
  17.     dateFormat = new SimpleDateFormat(patternStr, Locale.US); 
  18.     System.out.println(dateFormat.format(currDate)); 
  19.  
  20.     System.out.println("====================String->Date===================="); 
  21.     dateStrParam = "AD ad bC Sat SatUrday sunDay PM PM Am"
  22.     System.out.println(dateFormat.parse(dateStrParam)); 

運行程序,輸出:

  1. ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓中文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 
  2. ====================Date->String==================== 
  3. 公元 公元 公元 星期六 星期六 星期六 下午 下午 下午 
  4. ====================String->Date==================== 
  5. Sat Jan 03 12:00:00 CST 1970 
  6. ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓英文地區模式↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 
  7. ====================Date->String==================== 
  8. AD AD AD Sat Sat Saturday PM PM PM 
  9. ====================String->Date==================== 
  10. 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.US
  3. 對于Date類型的任何格式化、解析請統一使用SimpleDateFormat

JSR 310類型

曾經有個人做了個很有意思的投票,統計對Java API的不滿意程度。最終Java Date/Calendar API斬獲第二爛(第一爛是Java XML/DOM),體現出它爛的點較多,這里給你例舉幾項:

定義并不一致,在java.util和java.sql包中竟然都有Date類,而且呢對它進行格式化/解析類竟然又跑到java.text去了,精神分裂啊

java.util.Date等類在建模日期的設計上行為不一致,缺陷明顯。包括易變性、糟糕的偏移值、默認值、命名等等

java.util.Date同時包含日期和時間,而其子類java.sql.Date卻僅包含日期,這是什么神繼承?


  1. @Test 
  2. public void test10() { 
  3.     long currMillis = System.currentTimeMillis(); 
  4.  
  5.     java.util.Date date = new Date(currMillis); 
  6.     java.sql.Date sqlDate = new java.sql.Date(currMillis); 
  7.     java.sql.Time time = new Time(currMillis); 
  8.     java.sql.Timestamp timestamp = new Timestamp(currMillis); 
  9.  
  10.     System.out.println("java.util.Date:" + date); 
  11.     System.out.println("java.sql.Date:" + sqlDate); 
  12.     System.out.println("java.sql.Time:" + time); 
  13.     System.out.println("java.sql.Timestamp:" + timestamp); 

運行程序,輸出:

  1. java.util.Date:Sat Jan 16 21:50:36 CST 2021 
  2. java.sql.Date:2021-01-16 
  3. java.sql.Time:21:50:36 
  4. java.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這個包內,沒有例外。


當然嘍,本文重點并不在于討論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

 

它代表一個時區的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:00
  2. 另一種類型的ID是帶有某種前綴形式的偏移樣式ID,例如'GMT+2'或'UTC+01:00'。可識別的(合法的)前綴是'UTC', 'GMT'和'UT'
  3. 第三種類型是基于區域的ID(推薦使用)。基于區域的ID必須包含兩個或多個字符,且不能以'UTC'、'GMT'、'UT' '+'或'-'開頭。基于區域的id由配置定義好的,如Europe/Paris

概念說了一大推,下面給幾個代碼示例感受下吧。

1、獲取系統默認的ZoneId:

  1. @Test 
  2. public void test1() { 
  3.     // JDK 1.8之前做法 
  4.     System.out.println(TimeZone.getDefault()); 
  5.     // JDK 1.8之后做法 
  6.     System.out.println(ZoneId.systemDefault()); 
  7.  
  8. 輸出: 
  9. Asia/Shanghai 
  10. sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=29,lastRule=null

二者結果是一樣的,都是Asia/Shanghai。因為ZoneId方法底層就是依賴TimeZone,如圖:


2、指定字符串得到一個ZoneId:

  1. @Test 
  2. public void test2() { 
  3.     System.out.println(ZoneId.of("Asia/Shanghai")); 
  4.     // 報錯:java.time.zone.ZoneRulesException: Unknown time-zone ID: Asia/xxx 
  5.     System.out.println(ZoneId.of("Asia/xxx")); 

很明顯,這個字符串也是不能隨便寫的。那么問題來了,可寫的有哪些呢?同樣的ZoneId提供了API供你獲取到所有可用的字符串id,有興趣的同學建議自行嘗試:

  1. @Test 
  2. public void test3() { 
  3.     ZoneId.getAvailableZoneIds(); 

3、根據偏移量得到一個ZoneId:

  1. @Test 
  2. public void test4() { 
  3.     ZoneId zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("+8")); 
  4.     System.out.println(zoneId); 
  5.     // 必須是大寫的Z 
  6.     zoneId = ZoneId.ofOffset("UTC", ZoneOffset.of("Z")); 
  7.     System.out.println(zoneId); 
  8.  
  9. 輸出: 
  10. UTC+08:00 
  11. UTC 

這里第一個參數傳的前綴,可用值為:"GMT", "UTC", or "UT"。當然還可以傳空串,那就直接返回第二個參數ZoneOffset。若以上都不是就報錯

注意:根據偏移量得到的ZoneId內部并無現成時區規則可用,因此對于有夏令營的國家轉換可能出問題,一般不建議這么去做。

4、從日期里面獲得時區:

  1. @Test 
  2. public void test5() { 
  3.     System.out.println(ZoneId.from(ZonedDateTime.now())); 
  4.     System.out.println(ZoneId.from(ZoneOffset.of("+8"))); 
  5.  
  6.     // 報錯:java.time.DateTimeException: Unable to obtain ZoneId from TemporalAccessor: 
  7.     System.out.println(ZoneId.from(LocalDateTime.now())); 
  8.     System.out.println(ZoneId.from(LocalDate.now())); 

雖然方法入參是TemporalAccessor,但是只接受帶時區的類型,LocalXXX是不行的,使用時稍加注意。

ZoneOffset

距離格林威治/UTC的時區偏移量,例如+02:00。值得注意的是它繼承自ZoneId,所以也可當作一個ZoneId來使用的,當然并不建議你這么去做,請獨立使用。

時區偏移量是時區與格林威治/UTC之間的時間差。這通常是固定的小時數和分鐘數。世界不同的地區有不同的時區偏移量。在ZoneId類中捕獲關于偏移量如何隨一年的地點和時間而變化的規則(主要是夏令時規則),所以繼承自ZoneId。

1、最小/最大偏移量:因為偏移量傳入的是數字,這個是有限制的哦

  1. @Test 
  2. public void test6() { 
  3.     System.out.println("最小偏移量:" + ZoneOffset.MIN); 
  4.     System.out.println("最小偏移量:" + ZoneOffset.MAX); 
  5.     System.out.println("中心偏移量:" + ZoneOffset.UTC); 
  6.     // 超出最大范圍 
  7.     System.out.println(ZoneOffset.of("+20")); 
  8.  
  9. 輸出: 
  10. 最小偏移量:-18:00 
  11. 最小偏移量:+18:00 
  12. 中心偏移量:Z 
  13.  
  14. java.time.DateTimeException: Zone offset hours not in valid range: value 20 is not in the range -18 to 18 

2、通過時分秒構造偏移量(使用很方便,推薦):

  1. @Test 
  2. public void test7() { 
  3.     System.out.println(ZoneOffset.ofHours(8)); 
  4.     System.out.println(ZoneOffset.ofHoursMinutes(8, 8)); 
  5.     System.out.println(ZoneOffset.ofHoursMinutesSeconds(8, 8, 8)); 
  6.  
  7.     System.out.println(ZoneOffset.ofHours(-5)); 
  8.  
  9.     // 指定一個精確的秒數  獲取實例(有時候也很有用處) 
  10.     System.out.println(ZoneOffset.ofTotalSeconds(8 * 60 * 60)); 
  11.  
  12. // 輸出: 
  13. +08:00 
  14. +08:08 
  15. +08:08:08 
  16. -05:00 
  17. +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表示是一個帶有時區的日期時間,它們能非常方便的互相完成轉換。

  1. @Test 
  2. public void test8() { 
  3.     // 本地日期/時間 
  4.     System.out.println("================本地時間================"); 
  5.     System.out.println(LocalDate.now()); 
  6.     System.out.println(LocalTime.now()); 
  7.     System.out.println(LocalDateTime.now()); 
  8.  
  9.     // 時區時間 
  10.     System.out.println("================帶時區的時間ZonedDateTime================"); 
  11.     System.out.println(ZonedDateTime.now()); // 使用系統時區 
  12.     System.out.println(ZonedDateTime.now(ZoneId.of("America/New_York"))); // 自己指定時區 
  13.     System.out.println(ZonedDateTime.now(Clock.systemUTC())); // 自己指定時區 
  14.     System.out.println("================帶時區的時間OffsetDateTime================"); 
  15.     System.out.println(OffsetDateTime.now()); // 使用系統時區 
  16.     System.out.println(OffsetDateTime.now(ZoneId.of("America/New_York"))); // 自己指定時區 
  17.     System.out.println(OffsetDateTime.now(Clock.systemUTC())); // 自己指定時區 

運行程序,輸出:

  1. ================本地時間================ 
  2. 2021-01-17 
  3. 09:18:40.703 
  4. 2021-01-17T09:18:40.703 
  5. ================帶時區的時間ZonedDateTime================ 
  6. 2021-01-17T09:18:40.704+08:00[Asia/Shanghai] 
  7. 2021-01-16T20:18:40.706-05:00[America/New_York] 
  8. 2021-01-17T01:18:40.709Z 
  9. ================帶時區的時間OffsetDateTime================ 
  10. 2021-01-17T09:18:40.710+08:00 
  11. 2021-01-16T20:18:40.710-05:00 
  12. 2021-01-17T01:18:40.710Z 

本地時間的輸出非常“干凈”,可直接用于顯示。帶時區的時間顯示了該時間代表的是哪個時區的時間,畢竟不指定時區的時間是沒有任何意義的。LocalXXX因為它具有時區無關性,因此它不能代表一個瞬間/時刻。

另外,關于LocalDateTime、OffsetDateTime、ZonedDateTime三者的跨時區轉換問題,以及它們的詳解,因為內容過多放在了下文專文闡述,保持關注。

讀取字符串為JSR 310類型

一個獨立的日期時間類型字符串如2021-05-05T18:00-04:00它是沒有任何意義的,因為沒有時區無法確定它代表那個瞬間,這是理論當然也適合JSR 310類型嘍。

遇到一個日期時間格式字符串,要解析它一般有這兩種情況:

1.不帶時區/偏移量的字符串:要么不理它說轉換不了,要么就約定一個時區(一般用系統默認時區),使用LocalDateTime來解析

  1. @Test 
  2. public void test11() { 
  3.     String dateTimeStrParam = "2021-05-05T18:00"
  4.     LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam); 
  5.     System.out.println("解析后:" + localDateTime); 
  6.  
  7. 輸出: 
  8. 解析后:2021-05-05T18:00 

2.帶時區字/偏移量的符串:

  1. @Test 
  2. public void test12() { 
  3.     // 帶偏移量 使用OffsetDateTime  
  4.     String dateTimeStrParam = "2021-05-05T18:00-04:00"
  5.     OffsetDateTime offsetDateTime = OffsetDateTime.parse(dateTimeStrParam); 
  6.     System.out.println("帶偏移量解析后:" + offsetDateTime); 
  7.  
  8.  // 帶時區 使用ZonedDateTime  
  9.     dateTimeStrParam = "2021-05-05T18:00-05:00[America/New_York]"
  10.     ZonedDateTime zonedDateTime = ZonedDateTime.parse(dateTimeStrParam); 
  11.     System.out.println("帶時區解析后:" + zonedDateTime); 
  12.  
  13. 輸出: 
  14. 帶偏移量解析后:2021-05-05T18:00-04:00 
  15. 帶時區解析后:2021-05-05T18:00-04:00[America/New_York] 

請注意帶時區解析后這個結果:字符串參數偏移量明明是-05,為毛轉換為ZonedDateTime后偏移量成為了-04呢???

這里是我故意造了這么一個case引起你的重視,對此結果我做如下解釋:


如圖,在2021.03.14 - 2021.11.07期間,紐約的偏移量是-4,其余時候是-5。本例的日期是2021-05-05處在夏令時之中,因此偏移量是-4,這就解釋了為何你顯示的寫了-5最終還是成了-4。

JSR 310格式化

針對JSR 310日期時間類型的格式化/解析,有個專門的類java.time.format.DateTimeFormatter用于處理。

DateTimeFormatter也是一個不可變的類,所以是線程安全的,比SimpleDateFormat靠譜多了吧。另外它還內置了非常多的格式化模版實例供以使用,形如:

  1. @Test 
  2. public void test13() { 
  3.     System.out.println(DateTimeFormatter.ISO_LOCAL_DATE.format(LocalDate.now())); 
  4.     System.out.println(DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.now())); 
  5.     System.out.println(DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(LocalDateTime.now())); 
  6.  
  7. 輸出: 
  8. 2021-01-17 
  9. 22:43:21.398 
  10. 2021-01-17T22:43:21.4 

若想自定義模式pattern,和Date一樣它也可以自己指定任意的pattern 日期/時間模式。由于本文在Date部分詳細介紹了日期/時間模式,各個字母代表什么意思以及如何使用,這里就不再贅述了哈。

  • ❝雖然DateTimeFormatter支持的模式比Date略有增加,但大體還保持一致,個人覺得這塊無需再花精力。若真有需要再查官網也不遲❞
  1. @Test 
  2. public void test14() { 
  3.     DateTimeFormatter formatter = DateTimeFormatter.ofPattern("第Q季度 yyyy-MM-dd HH:mm:ss", Locale.US); 
  4.  
  5.     // 格式化輸出 
  6.     System.out.println(formatter.format(LocalDateTime.now())); 
  7.  
  8.     // 解析 
  9.     String dateTimeStrParam = "第1季度 2021-01-17 22:51:32"
  10.     LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStrParam, formatter); 
  11.     System.out.println("解析后的結果:" + localDateTime); 

Q/q:季度,如3; 03; Q3; 3rd quarter。

最佳實踐

  • 棄用Date,擁抱JSR 310

每每說到JSR 310日期/時間時我都會呼吁,保持慣例我這里繼續啰嗦一句:放棄Date甚至禁用Date,使用JSR 310日期/時間吧,它才是日期時間處理的最佳實踐。

另外,在使用期間關于制定時區(默認時區時)依舊有一套我心目中的最佳實踐存在,這里分享給你:

  • 永遠顯式的指定你需要的時區,即使你要獲取的是默認時區
  1. // 方式一:普通做法 
  2. LocalDateTime.now(); 
  3.  
  4. // 方式二:最佳實踐 
  5. LocalDateTime.now(ZoneId.systemDefault()); 

如上代碼二者效果一模一樣。但是方式二是最佳實踐。

理由是:這樣做能讓代碼帶有明確的意圖,消除模棱兩可的可能性,即使獲取的是默認時區。拿方式一來說吧,它就存在意圖不明確的地方:到底是代碼編寫者忘記指定時區欠考慮了,還是就想用默認時區呢?這個答案如果不通讀上下文是無法確定的,從而造成了不必要的溝通維護成本。因此即使你是要獲取默認時區,也請顯示的用ZoneId.systemDefault()寫上去。

  • 使用JVM的默認時區需當心,建議時區和當前會話保持綁定

這個最佳實踐在特殊場景用得到。這么做的理由是:JVM的默認時區通過靜態方法TimeZone#setDefault()可全局設置,因此JVM的任何一個線程都可以隨意更改默認時區。若關于時間處理的代碼對時區非常敏感的話,最佳實踐是你把時區信息和當前會話綁定,這樣就可以不用再受到其它線程潛在影響了,確保了健壯性。

❝說明:會話可能只是當前請求,也可能是一個Session,具體case具體分析❞

✍總結

通過上篇文章 對日期時間相關概念的鋪墊,加上本文的實操代碼演示,達到弄透Java對日期時間的處理基本不成問題。

兩篇文章的內容較多,信息量均比較大,消化起來需要些時間。一方面我建議你先搜藏留以當做參考書備用,另一方面建議多實踐,代碼這東西只有多寫寫才能有更深體會。

后面會再用3 -4篇文章對這前面這兩篇的細節、使用場景進行補充,比如如何去匹配ZoneId和Offset的對應關系,LocalDateTime、OffsetDateTime、ZonedDateTime跨時區互轉問題、在Spring MVC場景下使用的最佳實踐等等,敬請關注,一起進步。

本文是 A哥(YourBatman)原創文章,未經允許/開白不得轉載,謝謝合作。

 

責任編輯:姜華 來源: BAT的烏托邦
相關推薦

2017-07-27 15:50:19

Java時間日期

2024-06-17 09:40:45

UTCDay.js庫時間轉換

2023-12-27 08:16:54

Sessiontoken安全性

2025-08-08 00:00:00

2022-08-24 11:54:10

Pandas可視化

2021-05-27 05:34:22

Git開源控制系統

2022-03-24 07:38:07

注解SpringBoot項目

2021-01-26 09:25:02

Nginx開源軟件服務器

2020-12-18 08:03:00

插件MyBatis Executor

2025-05-22 08:21:28

2024-02-22 14:51:38

Java字符串

2022-05-18 11:35:17

Python字符串

2021-10-12 13:35:30

C++Set紅黑樹

2010-07-19 15:37:48

Perl日期時間

2021-03-02 07:13:54

Java8版本升級

2020-07-06 11:53:08

TCP三次握手協議

2025-04-02 09:10:00

LinuxShell腳本

2022-07-20 09:05:06

Python編程語言

2019-09-03 10:55:20

Python函數lambad

2022-07-20 00:15:48

SQL數據庫編程語言
點贊
收藏

51CTO技術棧公眾號

亚洲精品一区在线观看香蕉| 中文字幕 久热精品 视频在线 | 91xxx视频| 一本久道中文无码字幕av| 国产suv精品一区二区33| 自拍偷自拍亚洲精品被多人伦好爽| 性欧美精品高清| 欧美亚洲国产一区在线观看网站| 成人免费看片视频| 国产一级片黄色| 国产黄在线播放| 香蕉久久夜色精品| 正在播放亚洲1区| 第九区2中文字幕| 一级片一级片一级片| 国产免费av国片精品草莓男男| 成人免费视频视频在线观看免费| 午夜精品视频网站| 国产精品亚洲二区在线观看| 国产在线高清| 欧美日韩国产在线一区| 精品福利视频一区二区三区| 欧美日韩国产综合视频在线| 老熟妻内射精品一区| 日韩三级av高清片| 亚洲一区二区视频| 人体精品一二三区| 亚洲制服中文字幕| 国产精品186在线观看在线播放| 久久国产日韩| 日韩精品在线观看网站| 免费看污黄网站| 污视频在线免费| 久久久久久久久久久9不雅视频 | 日一区二区三区| 精品久久久久一区二区国产| 日本一区二区三区www| 欧美日韩三级在线观看| 欧美一级二级视频| 26uuu色噜噜精品一区| 亚洲丝袜av一区| 免费成人午夜视频| 国产高潮流白浆喷水视频| 日韩高清电影免费| 欧美日韩免费视频| 日本不卡一区| 后入内射欧美99二区视频| 可以看av的网站久久看| 亚洲精品99久久久久| 色噜噜狠狠一区二区| 成年人在线免费观看| 成人亚洲一区二区一| 国产人妖伪娘一区91| 亚洲一卡二卡| 国产对白videos麻豆高潮| 中文字幕日韩在线| 亚洲一区二区影院| 亚洲欧美日韩国产yyy| 日夜干在线视频| 成人av资源站| 97av在线影院| 爱爱爱爱免费视频| 欧美白人做受xxxx视频| 亚洲精品孕妇| 欧美老少配视频| 日本黄色一级网站| 成人看片网页| 日韩欧美国产高清91| 欧美日韩在线观看一区| 国 产 黄 色 大 片| 岛国一区二区在线观看| 91文字幕巨乱亚洲香蕉| 99视频在线观看免费| 精品一区二区免费视频| 国产精品久久久久久五月尺| 福利网址在线观看| 久久久久久婷| 国产成人精品视| 特级西西www444人体聚色| 日韩精品免费一区二区夜夜嗨| 欧美成人精品二区三区99精品| 亚洲一二区在线观看| 成人国产激情在线| 欧美日韩一区二区在线观看| 9久久婷婷国产综合精品性色 | 丁香婷婷成人| 91精选在线观看| 不卡的在线视频| 欧美黄色a视频| 5566中文字幕一区二区电影 | 免费在线黄色网| 日韩高清一区| 欧美va亚洲va香蕉在线| 亚洲AV成人精品| 性欧美xxx69hd高清| 色综合色综合色综合| 国产三级日本三级在线播放| 成人国产精品入口免费视频| 4438x成人网最大色成网站| 国产欧美精品一二三| 国产一区二区高清在线| 亚洲成**性毛茸茸| 毛茸茸多毛bbb毛多视频| 97人人做人人爽香蕉精品| 欧美婷婷六月丁香综合色| 欧美成年人视频在线观看| 欧美日韩黄色| 亚洲男人天堂久| 麻豆av免费看| 中文字幕一区二区三区四区久久 | www国产精品av| 日韩国产欧美一区| www黄色在线观看| 成年人网站91| 亚洲国产日韩综合一区| 污视频在线看网站| 91国在线观看| 国产精品一级无码| 久久av综合| 欧美精品免费在线观看| 成人免费毛片日本片视频| 精品freesex老太交| 精品国产3级a| 日本黄色小视频在线观看| 欧美二区视频| 美女视频久久黄| 99久在线精品99re8热| 可以看av的网站久久看| 成人免费看片视频| 欧美18xxxxx| 亚洲另类在线制服丝袜| 日韩欧美xxxx| 国产一区二区高清在线| 亚洲新声在线观看| 在线中文字日产幕| 清纯唯美激情亚洲| 亚洲午夜色婷婷在线| 久久国产免费观看| 久久国产精品无码网站| 欧美久久在线| 色帝国亚洲欧美在线| 91成人在线观看喷潮| 三级在线免费观看| 免费福利视频一区二区三区| 日韩视频123| 中国1级黄色片| 久久国产直播| 国产精品 日韩| 亚洲AV无码一区二区三区少妇| 国产一区视频导航| 99国精产品一二二线| 天堂а在线中文在线无限看推荐| 成人午夜免费av| 国产av不卡一区二区| 成人在线网站| 日韩精品中文字幕有码专区| 精品少妇爆乳无码av无码专区| 美国十次了思思久久精品导航| 欧美日韩综合网| 性xxxxfjsxxxxx欧美| 欧美精品电影在线播放| 成年人视频软件| 免费人成网站在线观看欧美高清| 久久久人人爽| 国产中文在线播放| 欧美午夜宅男影院在线观看| 国产chinesehd精品露脸| 成人综合久久| 欧美另类老女人| 国产精品无码AV| 亚洲欧美偷拍三级| 日韩成人av免费| 91精品天堂福利在线观看| 91精品久久久久| av每日在线更新| 狠狠色狠色综合曰曰| 色婷婷精品久久二区二区密 | 女人床在线观看| 日本久久伊人| 欧美精品第一页在线播放| 久久婷婷综合国产| 日韩av一二三| 视频在线一区二区三区| 草莓视频成人appios| 中文字幕在线观看日韩| 国产精品无码久久av| 亚洲精品成a人| 轻点好疼好大好爽视频| 第四色在线一区二区| 欧美一二三视频| 国产高清在线| 7777女厕盗摄久久久| 免费麻豆国产一区二区三区四区| 成人av免费网站| 免费高清一区二区三区| 在线看片福利| 国产亚洲视频在线| 92久久精品一区二区| 一区二区三区在线观看动漫| 捆绑凌虐一区二区三区| 久久精品人人做人人爽电影蜜月| 特级西西444www大精品视频| 香蕉久久久久久| 欧美激情视频一区二区三区不卡| 视频一区二区三区在线看免费看| 五月综合激情网| 日日碰狠狠丁香久燥| 98精品久久久久久久| 国产精品手机在线| 2024最新电影在线免费观看| 在线区一区二视频| 日韩高清dvd碟片| aaa亚洲精品一二三区| 污版视频在线观看| 精品一区二区三区在线| 日韩美女主播视频| caopen在线视频| 欧美精品一卡二卡| 日韩成人在线免费视频| 国产精品萝li| 五月天六月丁香| 国产精品视频久久一区| 免费国产成人看片在线| 久久av免费看| 成人蜜桃视频| 欧美a视频在线| 亚洲2020天天堂在线观看| 日本不卡在线| 在线视频综合导航| 久久久久久久九九九九| 国产日韩亚洲欧美综合| 日韩女优在线视频| 久久成人精品无人区| 日本不卡在线观看视频| 亚洲综合色网| 先锋影音日韩| 网红女主播少妇精品视频| 91高跟黑色丝袜呻吟在线观看| 香蕉视频亚洲一级| www.亚洲免费视频| www.av在线| 国产婷婷97碰碰久久人人蜜臀| 亚洲精品国产精| 欧美一区二区三区系列电影| 三级网站在线播放| 精品国产乱码久久久久久虫虫漫画| 亚洲精品乱码久久久久久自慰| 欧美日韩a区| 中文字幕一区二区中文字幕| 精品国产乱码久久久久久果冻传媒| 97久久天天综合色天天综合色hd| 久久精品国产精品亚洲毛片| 国产精品黄页免费高清在线观看| 日本蜜桃在线观看视频| 亚洲国产精品推荐| 精品国产va久久久久久久| 欧美人伦禁忌dvd放荡欲情| 黄色片中文字幕| 精品久久久一区二区| 国产一卡二卡在线播放| 亚洲综合视频网| 久久高清无码视频| 一级中文字幕一区二区| 欧美成人三级视频| 一区二区三区欧美日韩| 国产suv一区二区三区| 亚洲欧美另类小说视频| 中文字幕在线2021| 亚洲综合一区二区三区| 日本五十路女优| 亚洲成av人影院| 中国一级免费毛片| 久久久影视传媒| 99久久久久久久久久| 久久人人爽爽爽人久久久| 日本高清www| 国产日韩精品一区二区三区| 欧美丰满老妇熟乱xxxxyyy| 91视视频在线观看入口直接观看www| av在线播放网址| 91视频观看免费| 亚洲第一香蕉网| 96av麻豆蜜桃一区二区| 欧洲一级黄色片| 久久久久久久久久电影| 女人十八毛片嫩草av| 亚洲欧美在线视频观看| 午夜69成人做爰视频| 亚洲电影一级黄| 六月丁香婷婷综合| 欧美日韩国产首页| 精品国产无码AV| 亚洲风情亚aⅴ在线发布| 日韩一级免费毛片| 亚洲三级 欧美三级| 91成人高清| 欧美巨大黑人极品精男| a级片在线免费| 欧美在线视频播放| 性欧美freehd18| 成人国产1314www色视频| 婷婷成人影院| 亚洲第一精品区| 99精品视频免费观看视频| 日本一区二区在线视频观看| 日韩理论电影大全| 欧美中日韩在线| 久久中文字幕人妻| 国产亚洲欧美激情| 国产性一乱一性一伧一色| 在线看不卡av| 天天操天天干天天| 日韩欧美一区二区三区在线| 视频一区二区三区在线看免费看| 久久的精品视频| 美女色狠狠久久| 国偷自产av一区二区三区小尤奈| 香蕉视频国产精品| 污污视频网站免费观看| www.亚洲精品| 欧美久久久久久久久久久久| 欧美日韩国产一级二级| 九色在线视频| 91a在线视频| 国产精品黄网站| 2022中文字幕| 国产在线日韩欧美| 国产又色又爽又高潮免费| 欧美国产精品一区| 麻豆久久久久久久久久| 精品99999| wwwwxxxx在线观看| yy111111少妇影院日韩夜片| 亚洲免费二区| 永久免费黄色片| 亚洲人精品一区| 国产精品久久久久久免费 | 午夜伦理一区二区| 超碰免费在线97| 欧美成人性生活| 精品国产乱码久久久久久樱花| 黄频视频在线观看| 国产一区二区伦理片| 久久国产精品国语对白| 91精品国产综合久久小美女| 色的视频在线免费看| 成人国产精品av| 五月婷婷亚洲| 中文字幕第66页| 又紧又大又爽精品一区二区| 国产高清免费观看| 久久久噜噜噜久久| 日韩大尺度在线观看| 黄色网页免费在线观看| 2024国产精品| 最新国产中文字幕| 久久韩剧网电视剧| 日韩精品亚洲专区在线观看| 欧美日韩午夜爽爽| 不卡一区二区在线| 天堂中文在线网| 一区二区三区久久精品| а√资源新版在线天堂| 97se亚洲综合| 亚洲经典三级| 扒开jk护士狂揉免费| 欧美性视频一区二区三区| 免费大片在线观看www| 亚洲在线视频观看| 一本一本久久| 美国美女黄色片| 日韩一区二区视频在线观看| sm在线观看| 日本婷婷久久久久久久久一区二区 | 亚洲国产高清国产精品| 激情成人综合网| 国产真人真事毛片| 亚洲欧洲美洲在线综合| 日韩成人综合网| 国产一区二区视频播放| 国产三级欧美三级日产三级99| 国产精品嫩草影院精东| 性色av一区二区三区免费| 成人在线国产| 亚洲国产精品第一页| 一本一道久久a久久精品| 欧美成人三区| 国产欧美日韩一区| 久久精品国产亚洲aⅴ | 欧美视频日韩| 亚洲av片不卡无码久久| 欧美丰满一区二区免费视频| 99色在线观看| 亚洲看片网站| 92国产精品观看| 国产精品久久欧美久久一区| 97av在线视频| 欧美日韩国产色综合一二三四| wwwwxxxx国产| 欧美成人精品3d动漫h|