首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 計算兩個日期差值的現代最佳實踐:從 Date 到 java.time

     編輯:Java 相關知識

計算日期差值是業務開發高頻需求。早期 `java.util.Date`/`Calendar` 設計缺陷多(可變、非線程安全、月份 0 基、時區混亂),Java 8 引入 `java.time` (JSR-310) 徹底重塑日期時間 API。本文對比新舊方案,給出生產級推薦代碼。

❌ 避坑:舊 API 典型問題代碼


// 原始碼風格:手動計算毫秒差,易溢出、忽略夏令時、不精確
public void dateDiff(String start, String end, String format) {
    SimpleDateFormat sdf = new SimpleDateFormat(format); // 非線程安全!
    long nd = 1000 * 24 * 60 * 60;
    long diff = sdf.parse(end).getTime() - sdf.parse(start).getTime();
    long day = diff / nd;
    long hour = diff % nd / (1000 * 60 * 60);
    // ... 輸出
}

痛點:`SimpleDateFormat` 非線程安全併發報錯、手動毫秒換算忽略閏秒/夏令時/時區變更、`Date` 可變導致防禦性拷貝、代碼冗長易錯。

✅ 推薦:java.time 現代 API 方案 (Java 8+)

核心類速查

類用途關鍵方法
LocalDate無時區日期 (年月日)`between`, `plusDays`, `until`, `ChronoUnit.DAYS.between`
LocalDateTime無時區日期時間`toLocalDate`, `truncatedTo`, `until`
ZonedDateTime帶時區日期時間`withZoneSameInstant`, `toOffsetDateTime`
Instant時間戳 (納秒精度)`Duration.between`, `atZone`
Period日期差值 (年月日)`getYears`, `getMonths`, `getDays`
Duration時間差值 (時分秒納秒)`toDays`, `toHours`, `toMinutes`, `getSeconds`
ChronoUnit單位化差值計算`DAYS.between`, `HOURS.between`, `MILLIS.between`

場景化最佳實踐代碼

1. 純日期差天數 (最常見)


public long daysBetween(LocalDate start, LocalDate end) {
    return ChronoUnit.DAYS.between(start, end); // 負值表示 start 在 end 之後
}

// 解析字符串
public long daysBetween(String startStr, String endStr, String pattern) {
    DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
    LocalDate start = LocalDate.parse(startStr, fmt);
    LocalDate end = LocalDate.parse(endStr, fmt);
    return ChronoUnit.DAYS.between(start, end);
}

// 使用示例
long days = daysBetween("2024-01-01", "2024-12-31", "yyyy-MM-dd"); // 365

2. 日期時間差值 (精確到秒/毫秒)


public Duration durationBetween(LocalDateTime start, LocalDateTime end) {
    return Duration.between(start, end);
}

// 輸出人類可讀格式
public String formatDuration(Duration d) {
    long days = d.toDays();
    long hours = d.toHoursPart();      // Java 9+ 
    long minutes = d.toMinutesPart();
    long seconds = d.toSecondsPart();
    long millis = d.toMillisPart();
    return String.format("%d天 %d時 %d分 %d秒 %d毫秒", days, hours, minutes, seconds, millis);
}

// Java 8 兼容寫法
long hours = d.minusDays(days).toHours();
long minutes = d.minusHours(hours).toMinutes();
// ...

3. 帶時區的精確差值 (跨時區/夏令時安全)


public Duration zonedDurationBetween(ZonedDateTime start, ZonedDateTime end) {
    // 自動處理時區偏移、夏令時轉換
    return Duration.between(start, end);
}

// 實例:北京時間 2024-03-10 02:30 到紐約時間 2024-03-10 03:30
ZoneId beijing = ZoneId.of("Asia/Shanghai");
ZoneId newYork = ZoneId.of("America/New_York");
ZonedDateTime start = ZonedDateTime.of(2024, 3, 10, 2, 30, 0, 0, beijing);
ZonedDateTime end = ZonedDateTime.of(2024, 3, 10, 3, 30, 0, 0, newYork);
Duration d = zonedDurationBetween(start, end); // 正確計算物理時間差,非壁鐘時間差

4. 業務友好的年月日差值 (Period)


public Period periodBetween(LocalDate start, LocalDate end) {
    return Period.between(start, end); // 年月日分離,適合「相差 1 年 2 個月 3 天」顯示
}

Period p = periodBetween(LocalDate.of(2020, 1, 15), LocalDate.of(2024, 3, 20));
System.out.printf("%d年 %d月 %d天%n", p.getYears(), p.getMonths(), p.getDays()); // 4年 2月 5天

5. 線程安全的格式化解析


// DateTimeFormatter 線程安全,可作為靜態常量復用
private static final DateTimeFormatter FMT_YYYY_MM_DD = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter FMT_ISO = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

public LocalDate parseDate(String str) {
    return LocalDate.parse(str, FMT_YYYY_MM_DD); // 無需同步,高併發安全
}

🔄 遷移清單:舊代碼現代化改造

舊 API新 API 替代遷移要點
`new Date()``Instant.now()` / `LocalDateTime.now()`時區感知:`Instant` 為 UTC,`LocalDateTime` 為本地
`System.currentTimeMillis()``Instant.now().toEpochMilli()`納秒精度:`Instant.now().getNano()`
`Calendar.getInstance()``LocalDateTime.now()` / `ZonedDateTime.now()`不可變,鏈式調用 `plusDays(1).withHour(12)`
`SimpleDateFormat``DateTimeFormatter`線程安全、預定義常量 `ISO_LOCAL_DATE` 等
`Date.before/after``LocalDate.isBefore/isAfter`語義更清晰
`TimeZone``ZoneId` / `ZoneOffset`支援 IANA 時區數據庫自動更新

⚠️ 常見陷阱避坑指南

  • Duration vs Period:`Duration` 基於時間物理長度 (適合時分秒);`Period` 基於日曆日期 (適合年月日) —— **不可混用**。
  • toDays() vs toDaysPart():`toDays()` 返回總天數 (含時分秒換算);`toDaysPart()` 僅返回天數部分 (Java 9+)。
  • 時區 vs 偏移量:`ZoneId` 含夏令時規則 (如 `Europe/Paris`);`ZoneOffset` 固定偏移 (如 `+08:00`)。跨時區計算必須用 `ZonedDateTime`。
  • 序列化:`java.time` 類實現 `Serializable`,但建議用 ISO-8601 字符串 (`toString()`/`parse()`) 跨系統傳輸。
  • 數據庫交互:JDBC 4.2+ 直接支援 `LocalDate`/`LocalDateTime`/`Instant` (via `setObject`/`getObject`),無需轉 `java.sql.Date`。

總結

拋棄 `Date`/`Calendar`/`SimpleDateFormat`,全面擁抱 `java.time`。核心口訣:日期用 LocalDate,時間用 LocalDateTime,跨時區用 ZonedDateTime,差值用 ChronoUnit/Duration/Period,格式化用 DateTimeFormatter 靜態常量。代碼將更簡潔、線程安全、語義精確、免疫時區陷阱。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved