那些 Java 中的時間用法 — Instant, DateTimeFormatter, LocalDateTime, ZonedDateTime

仁哥
13 min readJun 27, 2021

--

前言

想先說個小故事,大學三年級的我進入實驗室做專題並報名大專生計畫。截止日當天為了趕早上 8 點的截止時間,與教授寄信改稿來來回回不下20次(中間還有被教授責備的部分,也合理因為是半夜… )

結果投搞出去後才發現那是 GMT+0 的早上 8 點… 其實我根本不需要那麼趕。這也是我第一次生活中遇到跟時區直接相關的事。( 教授的部分,現在還是有聯絡,感情還是很好!不用擔心~)

疫情的影響下,在家工作進入了第七週,剛好遇到需要處理上傳客戶班表的任務,藉此整理一下 Java 中的一些時間用法。

Photo by Tomas Yates on Unsplash

時間知識

真正寫扣之前,我認為需要先去了解一些時間相關的知識。

GMT & UTC ?

GMT ( Greenwich Mean Time ) 就是我們知道的「格林威治標準時間」,而 UTC ( Coordinated Universal Time) 是「世界協調時間」。

GMT 跟 UTC 都是時間的基準。

差別在於他們的觀測 & 運算方式,GMT 是利用太陽觀測而來,而 UTC 是利用銫原子的振動頻率。

因為地球自轉的速度越來越慢,UTC 相對比較快,處於超前 GMT的狀態,因此需要每隔一段時間就要加入閨秒 ( leap second )來校正兩者之間的差距,保持在不大於 0.9 秒的差距。

所以定義上兩者 GMT & UTC 是不一樣的,但是如果不在意這不到一秒差距兩者基本上可以視為相同。

至於什麼是 GMT+8 ? 代表以格林威治表準時間為基準,往後加 8 個小時的時間。同理可證,UTC+8 代表的意思就是以世界協調時間為基準,往後加 8 個小時的時間。

Unix Time ?

Unix Time ( 也可以叫作 Unix Epoch, POSIX time 或是 Unix Timestamp )是 Unix 系統的時間表示法。

定義是從 UTC時間 1970 年 ( Unix 元年 ) 1月1日 00:00:00 開始計算的秒數。

用來表達時間軸上的一個瞬間。

時區

Photo by Vince Veras on Unsplash

地理上來說,每15個經度偏移就是相差一個小時。

同個 UTC 時間,在不同國家會有不同的當地時間。
相對地,某一瞬間每個國家的人民看到牆壁上的時鐘都有不同的當地的時間,但是他們的 UTC 都是相同的。

但是如果要考量政治、經濟等因素的話就沒有那麼單純了,像是美國只畫分成 4 個時區,中國以及印度都只有 1 個時區。

Java 時間相關類別介紹

  1. 這裡我們將利用 Instant, LocalDateTime, ZonedDateTime & DateTimeFormatter 來處理時間問題。
  2. 這些 classes 適用於 Java 8 之後。
  3. 本篇就不討論 legacy java-time classes,像是 Date, Calendar 以及 SimpleDateFormat。

Instant

Instant 是時間軸上的一個瞬間,是利用 Unix Time 的概念(秒數)來儲存,不具有時區的概念。

簡單來說,就是一個用秒數 (也可以是毫秒) 儲存的 UTC 瞬間。

  • Example I:
    建立一個當下 UTC 時間的 Instant
Instant instantNow = Instant.now();
  • Example II:
    建立一個 “ 從1970/01/01 00:00:00 算起經過 1624613234 秒的” Instant
Instant instantOfSpecificTime = Instant.ofEpochSecond(1624613234);

LocalDateTime

至於 LocalDateTime 就玄了… 不要被前面的 “Local” 給誤導了,他不是“當地”的意思,反而可以視為 “any”。

它既不存在於時間軸上,也不具有時區的概念。他就是具有時間樣貌的物件,但不代表任何一個時區或是時間軸上真正的時間,需要等到我們之後去賦予它意義。

依我的理解,LocalDateTime 就像是偵探在抓捕一個全球大盜時,找到的時間戳線索,在不知道是哪個國家時間的狀況下,先記在筆記本裡面的那段文字,等著更多線索的時候才能賦予它真正的意義。

  • Example I :
    建立一個 2021/06/26 13:00:00 的 LocalDateTime
LocalDateTime ldt = LocalDateTime.of(2021, 6, 26, 13, 0, 0);
  • Example II :
    利用 LocalDate & LocalTime 建立一個 LocalDateTime
// LocalDate & LocalTime 可以看作是分別負責日期以及時間的兩個物件。
LocalDate ld = LocalDate.of(2021, 6, 26);
LocalTime lt = LocalTime.of(13, 0, 0);
LocalDateTime ldt = LocalDateTime.of(ld, lt);

ZonedDateTime

ZonedDateTime 就是具有時區概念的時間戳記。

  • Example I :
    利用 Istant & ZoneId 建立一個 ZonedDateTime
Instant nowInstant = Instant.now();
ZoneId taipeiZoneId = ZoneId.of("Asia/Taipei");
// 儲存這個 Instant 在台北的時間
ZonedDateTime zdt = ZonedDateTime.ofInstant(nowInstant, taipeiZoneId);
  • Example II :
    利用 LocalDateTime & ZoneId 建立一個 ZonedDateTime
LocalDateTime ldt = LocalDateTime.of(2021, 6, 26, 13, 0, 0);
ZoneId taipeiZoneId = ZoneId.of("Asia/Hong_Kong");
// 儲存在香港時間的 2021/06/26 13:00:00
ZonedDateTime zdt = ZonedDateTime.of(ldt, taipeiZoneId);

DateTimeFormatter

DateTimeFormatter 是決定時間格式的物件。

  • Example I :
    建立一個格式為 “ yyyy-MM-dd HH:mm:ss “ 的 DateTimeFormatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
  • Example II :
    建立一個格式為 “ yyyy/MM HH:mm “ 且附帶時區在台北的 DateTimeFormatter
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM HH:mm").withZone(ZoneId.of("Asia/Taipei"));

處理時間問題

狀況一:將 UTC 轉換成某時區的時間

已知 UTC 為 1614472200000 ms,想要轉換成當下台北的時間 ( 要求格式:yyyy/MM/dd HH:mm)

// 已知 epochMilliSec
long
epochMilliSec = 1614472200000L;
// 利用 Epoch 轉換成 Instant,此時還沒有時區的概念
Instant instant = Instant.ofEpochMilli(epochMilliSec);
// 建立有台北時區的 ZoneId
ZoneId zoneIdOfTaipei = ZoneId.of("Asia/Taipei");
// 建立 "yyyy_MM_dd HH:mm" 時間格式的 DateTimeFormatter
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy_MM_dd HH:mm");
// 將 Instant & ZoneId 帶入並建立 ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, zoneIdOfTaipei);
// 使用指定的時間格式輸出時間
System.out.println(zdt.format(format));
// 2021_02_28 08:30

狀況二:將具有時區概念的 Timestamp 轉換成 Unix Time

已知在美國/紐約的時間是 2021/03/09 12:40:00,求當下的 UTC 時間

// 已知 timestamp & ZoneId
String timeStr = "2021/03/09 12:40:00";
String zone = "America/New_York";
// 建立有紐約時區的 ZoneId
ZoneId zoneIdOfNewYork = ZoneId.of(zone);
// 建立 "yyyy/MM/dd HH:mm:ss" 時間格式 & 附加紐約時區的 DateTimeFormatter
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(zoneIdOfNewYork);
// 利用 String & DateTimeFormatter 建立 ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.parse(timeStr, format);
// ZonedDateTime 轉換成 Instant
Instant instant = zdt.toInstant();
// Instant 轉換成 long
long
epochMilliSec = instant.toEpochMilli();
// 輸出結果
System.out.println(epochMilliSec);
// 1615311600000

寫到這是不是都沒有看到 LocalDateTime 出現?

其實在也可以利用 LocalDateTime 來轉換,但 LocalDateTime 沒有時區的概念且最終還是要處理時區問題,因此上面兩的狀況都使用 ZonedDateTime 作直接的轉換。

那什麼情況會遇到要使用 LocalDateTime 呢? 我們可以看看下面的狀況。

狀況三:指定的日期時間轉換成不同時區的時間

對於全球而言,聖誕節都是從每年的 12/25 00:00:00 開始。假設我們想知道 台北 & 紐約 當地進入聖誕節的瞬間的 UTC 分別是多少,我們就可以利用 LocalDateTime 來實作。

Photo by Annie Spratt on Unsplash
// 已知聖誕節的 Timestamp
String timeStrOfChrismas = "2018/12/25 00:00:00";
// 建立有台北時區的 ZoneId & 有紐約時區的 ZoneId
ZoneId zoneIdOfTaipei = ZoneId.of("Asia/Taipei");
ZoneId zoneIdOfNewYork = ZoneId.of("America/New_York");
// 建立 "yyyy/MM/dd HH:mm:ss" 時間格式
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
// 利用 String & ZoneId 建立 LocalDateTime --> 此時還沒有時區的概念
LocalDateTime ldt = LocalDateTime.parse(timeStrOfChrismas, format);
// 轉換成 long
long
taipeiChrismasInEpochMilli = ldt.atZone(zoneIdOfTaipei).toInstant().toEpochMilli();
long newYorkChrismasInEpochMilli = ldt.atZone(zoneIdOfTaipei).toInstant().toEpochMilli();
// 輸出結果
System.out.println("Taipei Epoch on Christmas: " + taipeiChrismasInEpochMilli + " ms");
System.out.println("New York Epoch on Christmas: " + newYorkChrismasInEpochMilli + " ms");
// Taipei Epoch on Christmas: 1545667200000 ms
// New York Epoch on Christmas:
1545714000000 ms

結語

  • 因為看到Date的很多方法都被depreciated… 所以毅然決然不研究legacy java time classes。
  • 文章中介紹的四個類別字面上都可以建立直觀。Instant 可以看成 UTC 的瞬間,ZonedDateTime 可以看成具有時區概念的時間,DateTimeFormatter 就是時間樣式,至於LocalDateTime可以看作一張寫了時間的紙條,等著被賦予意義。
  • 之前開發個人專案的時候,因為需要做即時報價的功能,所以常常碰到時間的問題。當時沒有仔細的整理好頭緒,剛好趁現在把概念弄完整,也算一了心願。

References

--

--

仁哥
仁哥

Written by 仁哥

趁年輕把想做的事盡量做|目前使用 Java 的後端工程師| 音樂欣賞者|宗教文學好像不錯

No responses yet