skip to content
BlogZzz

[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日期、時間與區域時區跨國會議、當地排程
InstantUTC 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-20
System.out.println(waitingPeriod.getMonths()); // 3

java.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 year
  • YYYY 是 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 傳入

參考資料