介紹 Java 8 的日期時間 API 與實用操作範例
本文最後更新於:2025-09-24
最近工作遇到一個環節,是需要透過專屬的 SDK 的某個 method,從外部系統取得日期時間的資料,當時發現回傳的是 Date 物件。
前公司對於日期時間一律使用 Date 類別來處理。而現在公司的程式專案大多使用 Java 8 的 LocalDateTime 等類別,所以需要進行轉換。這是我第一次在工作中接觸,因此在本文做個整理。
一、Instant
Java 以前的 Date,是位於 java.util 套件。Date 中儲存的是「1970-01-01 00:00:00」(格林威治時間)開始的毫秒數。它代表 1970 年起的時間偏移量,同時也是世界上的某一個瞬間。
而新的日期時間類別,是位於 java.time 套件。該套件有許多類別,其中 Instant 正好也是代表某個瞬間,不存在時區概念。
Date 提供了 toInstant 方法進行轉換。
Date date = new Date(1757901600000L);
Instant instant = date.toInstant();
System.out.println(instant); // 2025-09-15T02:00:00Z在本文前言提到的工作任務中,我是將 Date 轉換成基礎的 Instant,儲存到資料庫時(如透過 Spring Data JPA)也是選擇此型態。不直接使用後面介紹的 LocalDateTime,是為了避免日後閱讀程式的人去思考「這是哪個時區」。
若要直接自行建立 Instant 物件,可參考以下方法。
now:現在時間ofEpochSecond:1970 年起的秒數偏移量ofEpochMilli:1970 年起的毫秒數偏移量
Instant 物件可以再進一步轉換成其他日期時間物件,以下陸續介紹。
二、LocalDateTime
LocalDateTime 顧名思義就是當地時間。它只是儲存「年月日」與「時分秒」,同樣也不存在時區的概念。
(一)直接建立
以下是用指定的日期時間(24 小時制),來建立出 LocalDateTime 物件。
LocalDateTime dateTime = LocalDateTime.of(2025, 9, 15, 2, 0, 0);
System.out.println(dateTime); // 2025-09-15T02:00而使用 now 方法,可基於現在的系統時間與時區,建立出 LocalDateTime 物件。值得注意的是,遠端伺服器上的系統時區可能是 UTC+0,若讀者在臺灣本地開發,系統時區則是 UTC+8,所以結果會不一樣。
(二)轉換成指定時區
透過 LocalDateTime.ofInstant 方法,可將 Instant 轉換成 LocalDateTime。然而,在將世界上某個瞬間轉換成當地時間時,Java 需要知道這個「當地」是什麼時區,它不會幫我們預設是格林威治時間。
時區的資料可透過 ZoneId 物件來提供。以下的例子,是轉換成 UTC+8 的時區。
// 原始時間
Instant instant = Instant.ofEpochSecond(1757901600);
System.out.println(instant); // 2025-09-15T02:00:00Z
// 轉換後的時間
ZoneId zoneId = ZoneId.of("UTC+8");
LocalDateTime dateTime = LocalDateTime.ofInstant(instant, zoneId);
System.out.println(dateTime); // 2025-09-15T10:00在 ZoneId.of 方法中傳入「UTC」加上正負時差,即可表示時區。若時差是負的,則將「+」改成「-」即可,如「UTC-10」。
除了 UTC,該方法也支援「hh」、「hhmm」、「hh:mm」等多種格式,如「+08」、「-1000」、「+09:30」,讀者可根據需要來使用。
ZoneId 還有一個子類別叫做 ZoneOffset,它能讓我們以直接傳入參數的方式,來建立時區資料。
// 原始時間
Instant instant = Instant.ofEpochSecond(1757901600);
System.out.println(instant); // 2025-09-15T02:00:00Z
// 轉換成 UTC+8 時區
ZoneId zoneId = ZoneOffset.ofHours(8);
LocalDateTime dateTime = LocalDateTime.ofInstant(instant, zoneId);
System.out.println(dateTime); // 2025-09-15T10:00
// 轉換成 9 小時 30 分時區
zoneId = ZoneOffset.ofHoursMinutes(9, 30);
dateTime = LocalDateTime.ofInstant(instant, zoneId);
System.out.println(dateTime); // 2025-09-15T11:30
// 轉換成 UTC+0 時區
zoneId = ZoneOffset.UTC;
dateTime = LocalDateTime.ofInstant(instant, zoneId);
System.out.println(dateTime); // 2025-09-15T02:00從上面的例子可看出,在程式碼中能直接在參數傳入小時和分鐘數,來代表時差。而針對 UTC+0,也有 ZoneOffset.UTC 這個常數可用。
在本文前言提到的工作任務中,我可根據不同的應用情境,將基礎的 Instant 轉換成不同時區。如此就能保留轉換成 UTC+8 或 UTC+0 的彈性。
三、ZonedDateTime
ZonedDateTime 是一個本身同時攜帶日期時間與時區的物件。我自身尚未在工作中使用 ZonedDateTime,因此本文僅簡單帶過。但如果在程式專案中,經常處理很多種時區,也許可以嘗試使用它。
要建立 ZonedDateTime,可提供 LocalDateTime 與 ZoneId。
LocalDateTime localDateTime = LocalDateTime.of(2025, 9, 15, 2, 0, 0);
ZoneId zoneId = ZoneId.of("UTC+8");
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, zoneId);
System.out.println(zonedDateTime); // 2025-09-15T02:00+08:00[UTC+08:00]以下的例子,是將 ZonedDateTime 轉換成另一個時區。
LocalDateTime localDateTime = LocalDateTime.of(2025, 9, 15, 8, 0, 0);
ZoneId utc8ZoneId = ZoneId.of("UTC+8");
ZoneId utc9ZoneId = ZoneId.of("UTC+9");
ZonedDateTime taiwanDateTime = ZonedDateTime.of(localDateTime, utc8ZoneId);
ZonedDateTime japanDateTime = taiwanDateTime.withZoneSameInstant(utc9ZoneId);
System.out.println(japanDateTime); // 2025-09-15T09:00+09:00[UTC+09:00]只要呼叫 withZoneSameInstant 方法,並傳入目標時區,即可在不同時區之間轉換。
若讀者想將 ZonedDateTime 儲存到資料庫,資料庫可能不會記錄時區的資訊。或是後續在程式中讀取出來,會發現時區跑掉了。實作上可以將日期時間與時區,在資料庫中儲存成不同的欄位。程式查詢出來後再自行組裝成 ZonedDateTime。
四、日期時間的操作
(一)取得資訊
我們可以從 LocalDateTime 中取出「年月日」與「時分秒」,以及其他資訊。
以「2025-02-15 16:30:00 」為例,將各方法的輸出整理如下:
| 方法 | 取得資料 | 範例輸出 |
|---|---|---|
getYear() |
年 | 2025 |
getMonthValue() |
月 | 2 |
getDayOfMonth() |
日 | 15 |
getDayOfYear() |
該年的第幾天 | 46 |
getDayOfWeek().getValue() |
星期幾(值從 1 開始) | 6 |
getHour() |
小時 | 16 |
getMinute() |
分鐘 | 30 |
getSecond() |
秒 | 0 |
(二)進行運算
以下也整理出部份對日期時間進行運算的方法。
| 方法 | 用途 |
|---|---|
plusYears |
增加年 |
plusMonths |
增加月 |
plusDays |
增加天 |
plusWeeks |
增加週 |
minusHours |
減小時 |
minusMinutes |
減分鐘 |
要注意的是,這些方法會回傳新的日期時間物件,要記得接收起來,而原本的物件本身是不可變的(immutable)。
另外,如果想比較兩個日期之間的差距,可透過 Duration 類別的 between 方法來達成。
LocalDateTime dateTime1 = LocalDateTime.of(2025, 9, 14, 20, 0, 0);
LocalDateTime dateTime2 = LocalDateTime.of(2025, 9, 15, 3, 0, 0);
LocalDateTime dateTime3 = LocalDateTime.of(2025, 9, 15, 21, 0, 0);
long duration = Duration.between(dateTime1, dateTime2).toDays();
System.out.println(duration); // 0
duration = Duration.between(dateTime1, dateTime3).toDays();
System.out.println(duration); // 1
duration = Duration.between(dateTime1, dateTime3).toHours();
System.out.println(duration); // 25between 方法的第一個參數是開始的日期時間,第二個參數是結束。該方法會回傳 Duration 物件,我們可呼叫 toDays、toHours 等方法,換算成不同的單位(無條件捨去小數點)。
(三)進行比較
以下方法可比較兩個日期時間物件的早晚。
isBefore:呼叫的物件是否比較早。isAfter:呼叫的物件是否比較晚。isEqual:兩個物件所代表的日期時間是否相同。
五、與字串之間的轉換
使用 DateTimeFormatter,可以幫助我們將上述的日期時間物件,與字串之間互相轉換。
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
LocalDateTime dateTime = LocalDateTime.of(2025, 1, 15, 2, 45, 0);
String dateTimeStr = dateTime.format(formatter);
System.out.println(dateTimeStr); // 20250115_024500
dateTimeStr = "20251231_235959";
dateTime = LocalDateTime.parse(dateTimeStr, formatter);
System.out.println(dateTime); // 2025-12-31T23:59:59DateTimeFormatter 類別也提供了一些內建的常數物件,可以直接使用。以下挑選其中幾個,並整理了輸出字串的結果:
| 物件 | LocalDateTime | ZonedDateTime |
|---|---|---|
ISO_LOCAL_DATE |
2025-01-15 | 2025-01-15 |
ISO_LOCAL_TIME |
10:00:00 | 10:00:00 |
ISO_LOCAL_DATE_TIME |
2025-01-15T10:00:00 | 2025-01-15T10:00:00 |
ISO_DATE_TIME |
2025-01-15T10:00:00 | 2025-01-15T10:00:00+09:00[UTC+09:00] |
BASIC_ISO_DATE |
20250115 | 20250115+0900 |
附帶一提,舊版的 SimpleDateFormat 存在執行緒不安全的問題。新版的 DateTimeFormatter 已有進行改良,是執行緒安全(thread-safe)的。