Java时区处理初学者指南
基本時間觀念
大多數Web應用程序必須支持不同的時區,而正確處理時區絕非易事。 更糟糕的是,您必須確保各種編程語言(例如,前端JavaScript,中間件中的Java和作為數據存儲庫的MongoDB)之間的時間戳是一致的。 這篇文章旨在解釋絕對時間和相對時間的基本概念。
時代
紀元是絕對時間基準。 大多數編程語言(例如Java,JavaScript,Python)使用Unix紀元(1970年1月1日午夜)來表示給定的時間戳,即自固定時間點引用以來經過的毫秒數。
相對數字時間戳
相對數字時間戳表示為從紀元以來經過的毫秒數。
時區
協調世界時(UTC)是最常見的時間標準。 UTC時區(相當于GMT )表示所有其他時區涉及的時間參考(通過正/負偏移量)。
UTC時區通常稱為Zulu時間(Z)或UTC + 0。 日本時區為UTC + 9,而檀香山時區為UTC-10。 在Unix時代(1970年1月1日UTC時區),東京為1970年1月1日,檀香山為1969年12月31日14:00。
ISO 8601
ISO 8601是最廣泛的日期/時間表示標準,它使用以下日期/時間格式:
| 世界標準時間 | 1970-01-01T00:00:00.000 + 00:00 |
| UTC祖魯時間 | 1970-01-01T00:00:00.000 + Z |
| 時雄 | 1970-01-01T00:00:00.000 + 09:00 |
| 火奴魯魯 | 1969-12-31T14:00:00.000-10:00 |
Java時間基礎
java.util.Date
java.util.Date絕對是最常見的時間相關類。 它表示一個固定的時間點,表示為自歷元以來經過的相對毫秒數。 java.util.Date是與時區無關的 ,除了toString方法使用本地時區生成String表示形式。
java.util.Calendar
java.util.Calendar既是日期/時間工廠,也是時區感知定時實例。 它是最不友好的Java API類之一,我們可以在以下示例中進行演示:
@Test public void testTimeZonesWithCalendar() throws ParseException {assertEquals(0L, newCalendarInstanceMillis("GMT").getTimeInMillis());assertEquals(TimeUnit.HOURS.toMillis(-9), newCalendarInstanceMillis("Japan").getTimeInMillis());assertEquals(TimeUnit.HOURS.toMillis(10), newCalendarInstanceMillis("Pacific/Honolulu").getTimeInMillis());Calendar epoch = newCalendarInstanceMillis("GMT");epoch.setTimeZone(TimeZone.getTimeZone("Japan"));assertEquals(TimeUnit.HOURS.toMillis(-9), epoch.getTimeInMillis()); }private Calendar newCalendarInstance(String timeZoneId) {Calendar calendar = new GregorianCalendar();calendar.set(Calendar.YEAR, 1970);calendar.set(Calendar.MONTH, 0);calendar.set(Calendar.DAY_OF_MONTH, 1);calendar.set(Calendar.HOUR_OF_DAY, 0);calendar.set(Calendar.MINUTE, 0);calendar.set(Calendar.SECOND, 0);calendar.set(Calendar.MILLISECOND, 0);calendar.setTimeZone(TimeZone.getTimeZone(timeZoneId));return calendar; }在Unix時代(UTC時區),東京時間提前了9個小時,而檀香山卻落后了10個小時。
更改日歷時區會在偏移時區偏移時保留實際時間。 相對時間戳隨日歷時區偏移量而變化。
Joda-Time和Java 8 Date Time API只是使java.util.Calandar過時,因此您不必再使用此古怪的API。
org.joda.time.DateTime
Joda-Time旨在通過提供以下服務來修復舊版Date / Time API:
- 不變和可變的日期結構
- 流利的API
- 更好地支持ISO 8601標準
使用Joda-Time,這就是我們之前的測試用例的樣子:
@Test public void testTimeZonesWithDateTime() throws ParseException {assertEquals(0L, newDateTimeMillis("GMT").toDate().getTime());assertEquals(TimeUnit.HOURS.toMillis(-9), newDateTimeMillis("Japan").toDate().getTime());assertEquals(TimeUnit.HOURS.toMillis(10), newDateTimeMillis("Pacific/Honolulu").toDate().getTime());DateTime epoch = newDateTimeMillis("GMT");assertEquals("1970-01-01T00:00:00.000Z", epoch.toString());epoch = epoch.toDateTime(DateTimeZone.forID("Japan"));assertEquals(0, epoch.toDate().getTime());assertEquals("1970-01-01T09:00:00.000+09:00", epoch.toString());MutableDateTime mutableDateTime = epoch.toMutableDateTime();mutableDateTime.setChronology(ISOChronology.getInstance().withZone(DateTimeZone.forID("Japan")));assertEquals("1970-01-01T09:00:00.000+09:00", epoch.toString()); }private DateTime newDateTimeMillis(String timeZoneId) {return new DateTime(DateTimeZone.forID(timeZoneId)).withYear(1970).withMonthOfYear(1).withDayOfMonth(1).withTimeAtStartOfDay(); }DateTime流利的API比java.util.Calendar#set易于使用。 DateTime是不可變的,但如果適合當前的用例,我們可以輕松地切換到MutableDateTime 。
與我們的Calendar測試用例相比,當更改時區時,相對時間戳不會改變,因此保留了相同的原始時間點。
只是人類的時間感知發生了變化( 1970-01-01T00:00:00.000Z和1970-01-01T09:00:00.000 + 09:00指向相同的絕對時間)。
相對時間與絕對時間實例
當支持時區時,基本上有兩個主要選擇:相對時間戳和絕對時間信息。
相對時間戳
時間戳的數字表示形式(自紀元以來的毫秒數)是相對信息。 該值是針對UTC時代給出的,但是您仍然需要一個時區來正確表示特定區域上的實際時間。
作為一個長值,它是最緊湊的時間表示形式,是交換大量數據時的理想選擇。
如果您不知道原始事件的時區,則可能會顯示與當前本地時區相對的時間戳,這并不總是可取的。
絕對時間戳
絕對時間戳包含相對時間以及時區信息。 在其ISO 8601字符串表示中表示時間戳是很常見的。
與數字形式(64位長)相比,字符串表示的緊湊性較低,它最多可包含25個字符(UTF-8編碼為200位)。
ISO 8601在XML文件中非常常見,因為XML模式使用的是受ISO 8601標準啟發的詞匯格式 。
當我們想針對原始時區重構時間實例時,絕對時間表示會更加方便。 電子郵件客戶端可能希望使用發件人的時區顯示電子郵件創建日期,而這只能使用絕對時間戳來實現。
謎題
以下練習旨在說明使用古老的java.text.DateFormat實用程序正確處理符合ISO 8601的日期/時間結構有多么困難。
java.text.SimpleDateFormat
首先,我們將使用以下測試邏輯來測試java.text.SimpleDateFormat解析功能:
/*** DateFormat parsing utility* @param pattern date/time pattern* @param dateTimeString date/time string value* @param expectedNumericTimestamp expected millis since epoch */ private void dateFormatParse(String pattern, String dateTimeString, long expectedNumericTimestamp) {try {Date utcDate = new SimpleDateFormat(pattern).parse(dateTimeString);if(expectedNumericTimestamp != utcDate.getTime()) {LOGGER.warn("Pattern: {}, date: {} actual epoch {} while expected epoch: {}", new Object[]{pattern, dateTimeString, utcDate.getTime(), expectedNumericTimestamp});}} catch (ParseException e) {LOGGER.warn("Pattern: {}, date: {} threw {}", new Object[]{pattern, dateTimeString, e.getClass().getSimpleName()});} }用例1
讓我們看看各種ISO 8601模式如何針對第一個解析器表現:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "1970-01-01T00:00:00.200Z", 200L);產生以下結果:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSS'Z', date: 1970-01-01T00:00:00.200Z actual epoch -7199800 while expected epoch: 200此模式不符合ISO 8601。 單引號字符是一個轉義序列,因此最后的“ Z”符號不會被視為時間指令(例如Zulu時間)。 解析后,我們將僅獲取本地時區的Date參考。
該測試是使用我當前的系統默認歐洲/雅典時區運行的,截至撰寫本文時,它比UTC提前兩個小時。
用例2
根據java.util.SimpleDateFormat文檔,以下模式: yyyy-MM-dd'T'HH:mm:ss.SSSZ應該匹配ISO 8601日期/時間字符串值:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200Z", 200L);但是相反,我們得到了以下異常:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSZ, date: 1970-01-01T00:00:00.200Z threw ParseException因此,此模式似乎無法解析Zulu時間UTC字符串值。
用例3
以下模式對于顯式偏移量非常適用:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0000", 200L);用例4
此模式還與其他時區偏移量兼容:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", "1970-01-01T00:00:00.200+0100", 200L - 1000 * 60 * 60);用例5
為了匹配祖魯語時間符號,我們需要使用以下模式:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200Z", 200L);用例6
不幸的是,最后一個模式與明確的時區偏移量不兼容:
dateFormatParse("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "1970-01-01T00:00:00.200+0000", 200L);最后出現以下異常:
Pattern: yyyy-MM-dd'T'HH:mm:ss.SSSXXX, date: 1970-01-01T00:00:00.200+0000 threw ParseExceptionorg.joda.time.DateTime
與java.text.SimpleDateFormat相反, Joda-Time與任何ISO 8601模式兼容。 以下測試用例將用于即將推出的測試用例:
/*** Joda-Time parsing utility* @param dateTimeString date/time string value* @param expectedNumericTimestamp expected millis since epoch*/ private void jodaTimeParse(String dateTimeString, long expectedNumericTimestamp) {Date utcDate = DateTime.parse(dateTimeString).toDate();if(expectedNumericTimestamp != utcDate.getTime()) {LOGGER.warn("date: {} actual epoch {} while expected epoch: {}", new Object[]{dateTimeString, utcDate.getTime(), expectedNumericTimestamp});} }Joda-Time與所有標準ISO 8601日期/時間格式兼容:
jodaTimeParse("1970-01-01T00:00:00.200Z", 200L); jodaTimeParse("1970-01-01T00:00:00.200+0000", 200L); jodaTimeParse("1970-01-01T00:00:00.200+0100", 200L - 1000 * 60 * 60);結論
如您所見,古老的Java Date / Time實用程序不容易使用。 Joda-Time是更好的選擇,提供更好的時間處理功能。
如果您碰巧使用Java 8,則值得切換到Java 8 Date / Time API ,該API是從頭開始設計的,但深受Joda-Time啟發 。
- 代碼可在GitHub上獲得 。
翻譯自: https://www.javacodegeeks.com/2014/11/a-beginners-guide-to-java-time-zone-handling.html
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Java时区处理初学者指南的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 树莓好吃吗 树莓的味道
- 下一篇: Java如何以及为什么使用Unsafe?