介紹 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); // 25
between
方法的第一個參數是開始的日期時間,第二個參數是結束。該方法會回傳 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: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)的。