計算日期差值是業務開發高頻需求。早期 `java.util.Date`/`Calendar` 設計缺陷多(可變、非線程安全、月份 0 基、時區混亂),Java 8 引入 `java.time` (JSR-310) 徹底重塑日期時間 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` 可變導致防禦性拷貝、代碼冗長易錯。
| 類 | 用途 | 關鍵方法 |
|---|---|---|
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` |
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
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();
// ...
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); // 正確計算物理時間差,非壁鐘時間差
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天
// 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 時區數據庫自動更新 |
拋棄 `Date`/`Calendar`/`SimpleDateFormat`,全面擁抱 `java.time`。核心口訣:日期用 LocalDate,時間用 LocalDateTime,跨時區用 ZonedDateTime,差值用 ChronoUnit/Duration/Period,格式化用 DateTimeFormatter 靜態常量。代碼將更簡潔、線程安全、語義精確、免疫時區陷阱。