1. 時區的概念
世界上存在許多時區:當在明斯克是中午時,紐約才剛早上,而東京已經是晚上。若你儲存的日期與時間不包含時區資訊,很容易產生混淆:例如伺服器在德國,而使用者在符拉迪沃斯托克,畫面上的時間 "2025-06-01 12:00" 對兩者的意義完全不同。
時區(timezone)——是一組規則,定義要在格林威治時間(UTC)上加減多少,才能得到特定地區的「當地」時間。
在 Java 中,用於處理時區的是類別 ZoneId。以下是幾個時區識別碼的例子:
- "Europe/Minsk"
- "UTC"
- "America/New_York"
- "Asia/Tokyo"
為什麼這很重要?
- 讓來自不同國家的使用者看到正確的時間顯示。
- 正確記錄事件時間(例如記錄檔、訂票、截止期限)。
- 考量夏令/冬令時間的切換(謝啦,歐洲!)。
2. ZonedDateTime——含時區的日期與時間
ZonedDateTime 是一個類別,會同時保存日期、時間以及時區資訊。它就像 LocalDateTime,只是還「知道」自己位於哪個地區。
建立 ZonedDateTime
系統時區中的目前日期與時間
import java.time.ZonedDateTime;
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 例如: 2025-06-01T15:30:00+03:00[Europe/Minsk]
特定時區的時間
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("明斯克: " + MinskTime);
System.out.println("紐約: " + newYorkTime);
從 LocalDateTime 建立
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]
取得與設定時區
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("東京: " + tokyoTime);
時區之間的轉換:withZoneSameInstant()
有時你需要知道同一個事件在另一個時區會是什麼時間。可使用 withZoneSameInstant():
ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("明斯克的會議時間: " + MinskMeeting);
System.out.println("同一事件在紐約: " + newYorkMeeting);
注意: withZoneSameInstant() 會將時間換算為在另一個時區的同一瞬間。如果使用 withZoneSameLocal(),則日期與時間會保持不變、只改變時區——這幾乎總是錯誤!
3. Instant——絕對時間點
Instant 是一個代表絕對時間點的類別,與時區無關。技術上,它是自 1970 年 1 月 1 日起經過的秒與奈秒數(UTC)。如果時間有一本護照——Instant 就是它的號碼。
建立 Instant
import java.time.Instant;
Instant now = Instant.now();
System.out.println(now); // 例如: 2025-06-01T12:30:00.123Z
請注意字母 Z——這表示「Zulu time」,也就是 UTC。
從 Unix 紀元的秒數建立
Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z
轉換 Instant ↔ ZonedDateTime/LocalDateTime
從 ZonedDateTime 轉為 Instant
ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);
從 Instant 轉為 ZonedDateTime
ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);
從 Instant 轉為 LocalDateTime
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);
4. 實作:不同時區的現在時間與時區轉換
取得不同時區的目前時間
我們來做個小程式,顯示明斯克、紐約與東京的現在時間:
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class TimeZonesDemo {
public static void main(String[] args) {
ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("明斯克: " + Minsk);
System.out.println("紐約: " + newYork);
System.out.println("東京: " + tokyo);
}
}
跨時區換算時間
假設有個事件安排在明斯克的 18:00。要怎麼知道在紐約與東京會是幾點?
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MeetingTime {
public static void main(String[] args) {
LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("明斯克的會議: " + minskEvent);
System.out.println("在紐約: " + newYorkEvent);
System.out.println("在東京: " + tokyoEvent);
}
}
將 LocalDateTime 轉為 ZonedDateTime,反之亦然
Local → Zoned:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(zonedTime);
Zoned → Local:
LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);
5. 重要說明與細節
為什麼不能只存 LocalDateTime
LocalDateTime 只是日期與時間,不含時區。對多數商務邏輯而言,這還不夠!例如,如果你把 "2025-06-01 12:00" 儲存為 LocalDateTime,對明斯克與紐約的使用者來說,這代表的是完全不同的真實時間點。
盡量保存絕對時間(例如,Instant),或保存帶時區的時間(ZonedDateTime),如果事件確實與特定時區綁定。LocalDateTime 只適合處理「浮動」日期(例如生日,不考慮一天中的時間與時區)。
夏令/冬令時間的問題
時區不僅是相對 UTC 的偏移,還包含夏令/冬令時間的切換規則。例如,有些國家在特定日子會把時間撥快或撥慢一小時——如果你只保存 LocalDateTime,你無法知道這個時間點是否真的存在。
「時間缺口」範例:
- 在美國,每年三月的凌晨 2:00 會把時鐘撥到 3:00。
- 在紐約,時間 "2025-03-10 02:30" 從未存在過!
使用 ZonedDateTime,你能避免這類驚喜:程式庫會自動檢查時間是否合法。
示意圖:LocalDateTime、ZonedDateTime、Instant 之間的關係
graph TD
A["LocalDateTime
(日期 + 時間,無時區)"] -->|+ ZoneId| B["ZonedDateTime
(日期 + 時間 + 時區)"]
B -->|"toInstant()"| C["Instant
(絕對時間,UTC)"]
C -->|"atZone(ZoneId)"| B
B -->|"toLocalDateTime()"| A
6. 使用 ZonedDateTime 與 Instant 的常見錯誤
錯誤 №1:用 LocalDateTime 來表示全球性的事件。
如果你把來自不同國家的使用者會議時間儲存為 LocalDateTime,每個人都會看到自己的「12:00」,但其實這些是不同的時間點。針對全球性事件請使用 ZonedDateTime 或 Instant。
錯誤 №2:在解析字串時忽略時區。
如果你解析字串 "2025-06-01T12:00:00" 而未指定時區,得到的是 LocalDateTime,而不是 ZonedDateTime。若要取得 ZonedDateTime,請使用帶時區的字串,或顯式加上它。
錯誤 №3:錯誤的時區轉換。
使用 withZoneSameLocal() 取代 withZoneSameInstant() 可能導致錯誤的時間。若要在另一個時區取得相同的時間點,務必使用 withZoneSameInstant()。
錯誤 №4:沒有考慮夏令/冬令時間的切換。
如果你在切換邊界附近排程事件,務必使用 ZonedDateTime,並相信程式庫——它知道所有的切換規則與「時間缺口」。
錯誤 №5:忽略時區就比較 ZonedDateTime。
兩個 ZonedDateTime 的時區不同,但本地時間相同,可能代表不同的時間點。比較時請使用 toInstant()。
GO TO FULL VERSION