Java ZonedDateTime

ZonedDateTime 是 Java 8 引入的日期時間類別,包含完整的日期、時間和時區資訊,適合處理跨時區的應用場景。

引入套件

import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;

建立 ZonedDateTime

取得當前日期時間(含時區)

// 使用系統預設時區
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);  // 2024-12-10T14:30:45.123+08:00[Asia/Taipei]

// 指定時區
ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(nowInTokyo);  // 2024-12-10T15:30:45.123+09:00[Asia/Tokyo]

指定日期時間和時區

// of(年, 月, 日, 時, 分, 秒, 奈秒, 時區)
ZonedDateTime zdt = ZonedDateTime.of(
    2024, 12, 25, 14, 30, 0, 0,
    ZoneId.of("Asia/Taipei")
);
System.out.println(zdt);  // 2024-12-25T14:30+08:00[Asia/Taipei]

// 使用 LocalDateTime + 時區
LocalDateTime ldt = LocalDateTime.of(2024, 12, 25, 14, 30);
ZonedDateTime zdt2 = ZonedDateTime.of(ldt, ZoneId.of("Asia/Taipei"));
ZonedDateTime zdt3 = ldt.atZone(ZoneId.of("Asia/Taipei"));

從字串解析

ZonedDateTime zdt = ZonedDateTime.parse("2024-12-25T14:30:00+08:00[Asia/Taipei]");

// 使用自訂格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
ZonedDateTime zdt2 = ZonedDateTime.parse("2024-12-25 14:30:00 CST", formatter);

時區 ZoneId

常用時區

ZoneId taipei = ZoneId.of("Asia/Taipei");      // 台北 UTC+8
ZoneId tokyo = ZoneId.of("Asia/Tokyo");        // 東京 UTC+9
ZoneId shanghai = ZoneId.of("Asia/Shanghai");  // 上海 UTC+8
ZoneId newYork = ZoneId.of("America/New_York"); // 紐約 UTC-5/-4
ZoneId london = ZoneId.of("Europe/London");    // 倫敦 UTC+0/+1
ZoneId utc = ZoneId.of("UTC");                 // UTC 時區

// 系統預設時區
ZoneId defaultZone = ZoneId.systemDefault();

列出所有可用時區

Set<String> zoneIds = ZoneId.getAvailableZoneIds();
zoneIds.stream()
    .filter(z -> z.startsWith("Asia"))
    .sorted()
    .forEach(System.out::println);

使用 ZoneOffset

// 直接指定時區偏移
ZoneOffset offset = ZoneOffset.ofHours(8);      // +08:00
ZoneOffset offset2 = ZoneOffset.of("+08:00");   // +08:00
ZoneOffset offset3 = ZoneOffset.ofHoursMinutes(5, 30);  // +05:30

ZonedDateTime zdt = ZonedDateTime.of(
    LocalDateTime.now(),
    offset
);

取得日期時間資訊

ZonedDateTime zdt = ZonedDateTime.of(2024, 12, 25, 14, 30, 45, 0, ZoneId.of("Asia/Taipei"));

// 日期部分
int year = zdt.getYear();            // 2024
int month = zdt.getMonthValue();     // 12
int day = zdt.getDayOfMonth();       // 25
DayOfWeek dayOfWeek = zdt.getDayOfWeek();  // WEDNESDAY

// 時間部分
int hour = zdt.getHour();     // 14
int minute = zdt.getMinute(); // 30
int second = zdt.getSecond(); // 45

// 時區資訊
ZoneId zone = zdt.getZone();         // Asia/Taipei
ZoneOffset offset = zdt.getOffset(); // +08:00

// 轉換為其他類型
LocalDateTime ldt = zdt.toLocalDateTime();
LocalDate ld = zdt.toLocalDate();
LocalTime lt = zdt.toLocalTime();
Instant instant = zdt.toInstant();

時區轉換

withZoneSameInstant(保持同一時刻)

ZonedDateTime taipeiTime = ZonedDateTime.of(2024, 12, 25, 14, 0, 0, 0, ZoneId.of("Asia/Taipei"));
System.out.println("台北時間: " + taipeiTime);  // 2024-12-25T14:00+08:00[Asia/Taipei]

// 轉換到東京(同一時刻,不同時區)
ZonedDateTime tokyoTime = taipeiTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("東京時間: " + tokyoTime);  // 2024-12-25T15:00+09:00[Asia/Tokyo]

// 轉換到紐約
ZonedDateTime newYorkTime = taipeiTime.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("紐約時間: " + newYorkTime);  // 2024-12-25T01:00-05:00[America/New_York]

withZoneSameLocal(保持相同本地時間)

ZonedDateTime taipeiTime = ZonedDateTime.of(2024, 12, 25, 14, 0, 0, 0, ZoneId.of("Asia/Taipei"));

// 保持本地時間 14:00,但改變時區
ZonedDateTime tokyoSameLocal = taipeiTime.withZoneSameLocal(ZoneId.of("Asia/Tokyo"));
System.out.println(tokyoSameLocal);  // 2024-12-25T14:00+09:00[Asia/Tokyo]

日期時間運算

ZonedDateTime zdt = ZonedDateTime.of(2024, 12, 25, 14, 0, 0, 0, ZoneId.of("Asia/Taipei"));

// 加減運算
ZonedDateTime plus1Day = zdt.plusDays(1);
ZonedDateTime plus2Hours = zdt.plusHours(2);
ZonedDateTime minus1Month = zdt.minusMonths(1);

// 修改特定欄位
ZonedDateTime newHour = zdt.withHour(10);
ZonedDateTime newMonth = zdt.withMonth(6);

日光節約時間處理

// 洛杉磯有日光節約時間
ZoneId la = ZoneId.of("America/Los_Angeles");

// 2024年3月10日 02:00 跳到 03:00(日光節約開始)
LocalDateTime beforeDST = LocalDateTime.of(2024, 3, 10, 1, 30);
ZonedDateTime zdtBefore = ZonedDateTime.of(beforeDST, la);
System.out.println(zdtBefore);  // 2024-03-10T01:30-08:00[America/Los_Angeles]

ZonedDateTime zdtAfter = zdtBefore.plusHours(1);
System.out.println(zdtAfter);  // 2024-03-10T03:30-07:00[America/Los_Angeles]
// 注意:02:30 被跳過了!

// 2024年11月3日 02:00 回到 01:00(日光節約結束)
// 這時 01:30 會出現兩次

比較 ZonedDateTime

ZonedDateTime taipei = ZonedDateTime.of(2024, 12, 25, 14, 0, 0, 0, ZoneId.of("Asia/Taipei"));
ZonedDateTime tokyo = ZonedDateTime.of(2024, 12, 25, 15, 0, 0, 0, ZoneId.of("Asia/Tokyo"));

// 比較(會考慮時區,這兩個其實是同一時刻)
boolean isBefore = taipei.isBefore(tokyo);  // false
boolean isAfter = taipei.isAfter(tokyo);    // false
boolean isEqual = taipei.isEqual(tokyo);    // true(同一時刻)

// compareTo 比較
int result = taipei.compareTo(tokyo);  // 0(相等)

格式化輸出

ZonedDateTime zdt = ZonedDateTime.of(2024, 12, 25, 14, 30, 45, 0, ZoneId.of("Asia/Taipei"));

// 預設格式
System.out.println(zdt.toString());
// 2024-12-25T14:30:45+08:00[Asia/Taipei]

// 自訂格式
DateTimeFormatter f1 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println(zdt.format(f1));  // 2024-12-25 14:30:45 CST

DateTimeFormatter f2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
System.out.println(zdt.format(f2));  // 2024-12-25 14:30:45 +0800

DateTimeFormatter f3 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss VV");
System.out.println(zdt.format(f3));  // 2024-12-25 14:30:45 Asia/Taipei

實用範例

世界時鐘

public static void showWorldClock() {
    ZonedDateTime now = ZonedDateTime.now();
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
    
    String[] zones = {
        "Asia/Taipei", "Asia/Tokyo", "Asia/Shanghai",
        "Europe/London", "America/New_York", "America/Los_Angeles"
    };
    
    String[] names = {
        "台北", "東京", "上海", "倫敦", "紐約", "洛杉磯"
    };
    
    for (int i = 0; i < zones.length; i++) {
        ZonedDateTime zdt = now.withZoneSameInstant(ZoneId.of(zones[i]));
        System.out.printf("%s: %s%n", names[i], zdt.format(formatter));
    }
}

安排跨時區會議

public static void scheduleMeeting(ZonedDateTime meetingTime, String... zones) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm (z)");
    
    System.out.println("會議時間:");
    for (String zone : zones) {
        ZonedDateTime localTime = meetingTime.withZoneSameInstant(ZoneId.of(zone));
        System.out.println("  " + zone + ": " + localTime.format(formatter));
    }
}

// 台北時間下午3點的會議
ZonedDateTime meeting = ZonedDateTime.of(2024, 12, 25, 15, 0, 0, 0, ZoneId.of("Asia/Taipei"));
scheduleMeeting(meeting, "Asia/Taipei", "Asia/Tokyo", "America/New_York");
// 會議時間:
//   Asia/Taipei: 2024-12-25 15:00 (CST)
//   Asia/Tokyo: 2024-12-25 16:00 (JST)
//   America/New_York: 2024-12-25 02:00 (EST)

計算兩地時差

public static String getTimeDifference(String zone1, String zone2) {
    ZonedDateTime now = ZonedDateTime.now();
    ZonedDateTime time1 = now.withZoneSameInstant(ZoneId.of(zone1));
    ZonedDateTime time2 = now.withZoneSameInstant(ZoneId.of(zone2));
    
    int diff = time2.getOffset().getTotalSeconds() - time1.getOffset().getTotalSeconds();
    int hours = diff / 3600;
    
    return zone2 + " 比 " + zone1 + (hours >= 0 ? " 快 " : " 慢 ") + Math.abs(hours) + " 小時";
}

System.out.println(getTimeDifference("Asia/Taipei", "America/New_York"));
// America/New_York 比 Asia/Taipei 慢 13 小時

轉換為 UTC 時間戳

public static long toUtcTimestamp(ZonedDateTime zdt) {
    return zdt.toInstant().toEpochMilli();
}

public static ZonedDateTime fromUtcTimestamp(long timestamp, String zone) {
    return ZonedDateTime.ofInstant(
        Instant.ofEpochMilli(timestamp),
        ZoneId.of(zone)
    );
}

ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Taipei"));
long timestamp = toUtcTimestamp(zdt);
System.out.println("UTC 時間戳: " + timestamp);

ZonedDateTime restored = fromUtcTimestamp(timestamp, "Asia/Taipei");
System.out.println("還原時間: " + restored);

重點整理

  • ZonedDateTime 包含完整的日期 + 時間 + 時區資訊
  • 使用 ZoneId 表示時區(如 Asia/Taipei
  • withZoneSameInstant() 轉換時區但保持同一時刻
  • withZoneSameLocal() 保持本地時間但改變時區
  • 自動處理日光節約時間(DST)
  • 跨時區應用建議使用 ZonedDateTimeInstant
  • 儲存到資料庫時建議轉為 UTC