介紹 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,可提供 LocalDateTimeZoneId

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); // 25

between 方法的第一個參數是開始的日期時間,第二個參數是結束。該方法會回傳 Duration 物件,我們可呼叫 toDaystoHours 等方法,換算成不同的單位(無條件捨去小數點)。

(三)進行比較

以下方法可比較兩個日期時間物件的早晚。

  • 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:59

DateTimeFormatter 類別也提供了一些內建的常數物件,可以直接使用。以下挑選其中幾個,並整理了輸出字串的結果:

物件 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)的。


張貼留言