[Learn] Java Date/Time:日期、時間、時區與 Clock
/ 6 min read
Updated:Table of Contents
日期時間程式最常見的問題,不是少呼叫一個 method,而是選錯資料型別
生日只需要日期,事件紀錄需要全球一致的時間點,跨國會議則需要保留地區時區規則
這篇會分開整理以下概念
- LocalDate、LocalTime 與 LocalDateTime 的差異
- Instant 與 ZonedDateTime 如何表示真正的時間點
- Period 與 Duration 如何選擇
- DateTimeFormatter 如何解析與格式化
- 如何透過 Clock 讓時間相關邏輯可以測試
java.time 先分清楚時間代表什麼
舊的 Date 與 Calendar API 容易混合日期、時間與時區概念
java.time 使用不同 immutable type 表達不同語意
| Type | 表達內容 | 常見用途 |
|---|---|---|
LocalDate | 日期,不含時間與時區 | 生日、結帳日 |
LocalTime | 時間,不含日期與時區 | 每日營業時間 |
LocalDateTime | 日期與時間,不含時區 | 尚未指定地區的排程 |
ZonedDateTime | 日期、時間與區域時區 | 跨國會議、當地排程 |
Instant | UTC timeline 上的單一時間點 | 建立時間、到期時間 |
Period | 以年、月、日表示的日期差距 | 年齡、訂閱週期 |
Duration | 以秒與 nanosecond 表示的長度 | timeout、執行時間 |
先問 domain 真正需要的是日期、當地時間,還是全世界一致的時間點,再選型別
建立、計算與格式化日期
LocalDate 適合不需要時區的日期
import java.time.LocalDate;import java.time.Period;
LocalDate enrolledOn = LocalDate.of(2026, 8, 20);LocalDate reviewOn = enrolledOn.plusMonths(3);Period waitingPeriod = Period.between(enrolledOn, reviewOn);
System.out.println(reviewOn); // 2026-11-20System.out.println(waitingPeriod.getMonths()); // 3java.time type 是 immutable,plusMonths 不會修改原本物件,而是回傳新值
字串與日期之間使用 DateTimeFormatter
import java.time.format.DateTimeFormatter;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");
LocalDate date = LocalDate.parse("2026/09/02", formatter);String text = date.format(formatter);
System.out.println(text); // 2026/09/02格式固定為 ISO 8601 時,可以直接使用預先定義的 formatter
LocalDate date = LocalDate.parse("2026-09-02", DateTimeFormatter.ISO_LOCAL_DATE);pattern 大小寫代表不同欄位
MM是月份mm是分鐘uuuu是 proleptic yearYYYY是 week-based-year,不適合代替一般日期中的年份DateTimeFormatter是 immutable 且 thread-safe,可以安全重複使用
在簡化一點說明
uuuu 是這一天是幾年?
YYYY 是這一天所在的「整週」算哪一年的?
星期一 2019/12/30星期二 2019/12/31星期三 2020/01/01星期四 2020/01/02星期五 2020/01/03星期六 2020/01/04星期日 2020/01/05按照 ISO 週規則,這整週算 2020 年的第 1 週
所以對 2019/12/30 這一天:
用哪個格式 結果 原因
uuuu 2019 這一天還在 2019 年
YYYY 2020 這一週算在 2020 年
只是要顯示年月日,就用 uuuu-MM-dd 只有在處理「某年第幾週」時,才需要考慮 YYYY
Instant 與 ZonedDateTime 處理真正的時間點
LocalDateTime 沒有 offset 或 time zone,單獨一個值無法判斷它在 UTC timeline 上是哪一刻
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2026, 9, 2, 20, 0);如果這是台北時間,需要搭配 ZoneId
import java.time.Instant;import java.time.ZoneId;import java.time.ZonedDateTime;
ZoneId taipei = ZoneId.of("Asia/Taipei");ZonedDateTime taipeiMeeting = meeting.atZone(taipei);Instant meetingInstant = taipeiMeeting.toInstant();
ZonedDateTime newYorkMeeting = meetingInstant.atZone( ZoneId.of("America/New_York")); // 2026-09-02T12:00:00Z
System.out.println(taipeiMeeting); // 2026-09-02T20:00+08:00[Asia/Taipei]System.out.println(newYorkMeeting); // 2026-09-02T08:00-04:00[America/New_York]兩個 ZonedDateTime 顯示不同當地時間,但指向同一個 Instant
保存事件發生時間時通常使用 Instant,顯示給使用者時再依使用者的 ZoneId 轉換
不要用固定 offset 取代區域時區規則
Asia/Taipei、America/New_York 這類 region-based ZoneId 能套用該地區的時區規則與 daylight saving time 變化
使用 Clock 讓現在時間可以測試
直接在 domain logic 內呼叫 Instant.now(),測試結果會依執行當下而改變
可以把 Clock 當成 dependency 傳入
import java.time.Clock;import java.time.Instant;
public static boolean isExpired(Instant expiresAt, Clock clock) { return !expiresAt.isAfter(clock.instant());}正式環境使用 system clock
boolean expired = isExpired( Instant.parse("2026-09-03T00:00:00Z"), Clock.systemUTC());測試則固定現在時間
import java.time.ZoneOffset;
Clock fixedClock = Clock.fixed( Instant.parse("2026-09-02T12:00:00Z"), ZoneOffset.UTC);
boolean expired = isExpired( Instant.parse("2026-09-02T11:59:59Z"), fixedClock);
System.out.println(expired); // true這樣測試不需要等待,也不會因執行速度或時區不同而偶發失敗
常見錯誤
用 LocalDateTime 保存絕對時間
LocalDateTime 不包含時區,無法單獨代表全球一致的時間點
事件時間適合保存為 Instant,需要當地時區語意時使用 ZonedDateTime
混用 Period 與 Duration
Period.ofDays(1) 表示日曆上的一天,Duration.ofHours(24) 表示固定 24 小時
遇到 daylight saving time 時,兩者不一定會得到相同當地時間
結論
- LocalDate、LocalTime 與 LocalDateTime 都不包含時區
- Instant 表示 UTC timeline 上的單一時間點
- ZonedDateTime 同時保存當地日期時間與 region-based ZoneId
- 保存事件時間時優先使用 Instant,顯示時再依使用者的 ZoneId 轉換
- DateTimeFormatter 是 immutable 且 thread-safe,pattern 大小寫不可混用
- Period 表示日曆日期差距,Duration 表示固定時間長度
- 需要測試現在時間時,把 Clock 當成 dependency 傳入
參考資料
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/package-summary.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/LocalDate.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/LocalDateTime.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Instant.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/ZonedDateTime.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Period.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Duration.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/Clock.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/time/format/DateTimeFormatter.html