java 的 ZoneOffset 與 ZoneId

關于時區常見的問題:如何在java8及更高版本中獲取默認的ZoneOffset?

tl;dr

OffsetDateTime.now().getOffset()

但是,建議使用時區(ZoneId) 而不是UTC的偏移量 (ZoneOffset)。

偏移量 VS 時區

一個UTC偏移量 offset-from-UTC 僅僅只記錄了時分秒而已,除此之外沒有任何其他信息。舉個例子 ,+08:00的意思時超前于UTC八個小時,而 -05:45 意思是落后于UTC五小時四十五分鐘。

而時區對于特定地區的人來說是過去,現在,未來的偏移量的歷史集合。像夏令時 這樣的異常會導致特定時間段內的偏移量會隨時間變化,無論是過去已經發生的還是 未來政客們宣布計劃的改變。

所以,當你了解時最好還是使用時區。

很多地區的偏移量都會隨時間變化。比如,美國的夏令時 會導致約半年左右的時間內都會再原來的基礎上偏移一個小時,然后再下半年再調整回這一個小時的偏移量。時區的意義就是記錄這些所有的會造成偏移的情況。

因此,沒有結合日期計算的偏移量沒有意義的。舉個例子,法國時間 (Europe/Paris),在一年的一小部分時間內偏移量為+01:00,而由于夏令時的緣故,大部分時間(三月末到十月末)的偏移量都是+02:00

// 指定區域,構造時間: 法國巴黎時間的當前時間,五月有DST,偏移量+2
LocalDateTime now = LocalDateTime.of(LocalDate.of(2020, 5, 31), LocalTime.now())
ZonedDateTime zonedDateTime = ZonedDateTime.of(now, ZoneId.of("Europe/Paris"));
zonedDateTime.getOffset(); // +02:00

// 手動調整的二月份,此時沒有DST,偏移量是+1
ZonedDateTime thirdMonth = zonedDateTime.minusMonths(3);
thirdMonth.getOffset(); // +01:00

OffsetDateTime

讓我們來定義一個OffsetDateTime,然后獲取其中的 ZoneOffset.

OffsetDateTime odt = OffsetDateTime.now();
ZoneOffset zoneOffset = odt.getOffset();

odt.toString(); // 2020-05-31T23:59:24.753+08:00
zoneOffset.toString(); // +08:00

now方法其實隱含應用了jvm當前的默認時區。我的建議是 應該永遠顯式的指定你需要的時區,即便你就是要獲取當前的默認時區。這么做的目的就是明確代碼的意圖,從而消除模棱兩可的不確定性,比如代碼到底是要獲取默認時區還是寫代碼的人忘記考慮了時區的因素,這種情況在代碼里是經常發生的。默認時區可以通過調用 ZoneId.systemDefault獲取。

OffsetDateTime odt = OffsetDateTime.now(ZoneId.systemDefault());
ZoneOffset zoneOffset = odt.getOffset();

ZoneId.systemDefault().toString(); // Asia/Shanghai
odt.toString(); // 2020-05-31T23:59:24.753+08:00
zoneOffset.toString(); // +08:00

注意,如果代碼依賴了jvm的默認時區則需要小心,因為jvm中的任何一個線程都可以隨時更改時區。如果時區對于代碼十分重要,建議保存用戶的時區到Session或者持久化存儲,以便代碼行為的統一性。

同時,你也可以通過偏移量獲取以秒為單位的偏移秒數。

int offsetSeconds = zoneOffset.getTotalSeconds();
offsetSeconds; // 28800

ZonedDateTime

另外一個例子:也許你想知道今年圣誕節的時候的Québec(魁北克)偏移量是多少。定義一個America/Montreal的時區,獲取ZonedDateTime,然后通過ZonedDateTime獲取對應的UTC偏移對象ZoneOffset.

ZoneId z = ZoneId.of( "America/Montreal" );
LocalDate ld = LocalDate.of(2020, 12, 25);
ZonedDateTime zdtXmas = ld.atStartOfDay(z);
ZoneOffset zoneOffsetXmas = zdtXmas.getOffset();

zdtXmas.toString(); // 2020-12-25T00:00-05:00[America/Montreal]
zoneOffsetXmas.toString(); // -05:00
zoneOffsetXmas.getTotalSeconds(); // -18000

ZoneId

正如yanys在評論中所提到的,你可以通過給ZoneId傳遞一個時刻Instant來獲取對應的偏移量。 Instant 類代表UTC的時間軸上的一個時刻,可以精確到納秒級別(nanoseconds,可以達到小數點后九位)。

這是獲取偏移量的另外一種方式。就像在OffsetDateTimeZonedDateTime中討論的一樣,我們可以定義一個時區,然后通過一個時刻來獲取偏移量。

Instant instant = zdtXmas.toInstant();
ZoneOffset zo = z.getRules().getOffset( instant );

For ZoneId: America/Montreal at instant: 2020-12-25T05:00:00Z the ZoneOffset is: -05:00

點擊查看完整的演示: code live at IdeOne.com.

ZoneOffset.systemDefault – Bug還是特性?

ZoneOffset 類, 是 ZoneId的一個子類, 通過繼承,擁有父類的 systemDefault 方法。然而,實際上它并不能按照預期工作.

 // 編譯失敗
ZoneOffset zoneOffset = ZoneOffset.systemDefault();

error: incompatible types: ZoneId cannot be converted to ZoneOffset

不確定這個編譯失敗是bug還是特性。如同上文所提到的,脫離日期時間獲取默認的偏移量沒有意義。所以也許 ZoneOffset.systemDefault 確實需要失敗,但是也應該在文檔中提到或者提供詳細的解釋。

我嘗試提交一個關于這個問題的bug,最后還是放棄了,因為我不知道在哪里如何提交這樣一個bug報告。

本文中的第一人稱"我"指的是原作者,也就是原貼的回答。點擊查看原帖:How to get default ZoneOffset in java8?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。