計算兩個日期相差天數是極高頻需求,但「是否跨年、是否含時間、夏令時、閏年」等細節極易導致 Bug。本文對比三種主流實現,深度分析差異並給出生產級推薦方案。
| 方法 | 核心 API | 語義 | 適用場景 | 陷阱 |
|---|---|---|---|---|
| Calendar 日期字段差 | Calendar.DAY_OF_YEAR + 年份循環 | 日曆日差(忽略具體時間) | 業務按「自然日」結算 | 跨年閏年邏輯易寫錯 |
| 毫秒數相減 | (d2.getTime()-d1.getTime())/(1000*3600*24) | 24小時倍數差(精確到毫秒) | 科學計算、精確間隔 | 夏令時、閏秒導致非整數天 |
| Java 8 LocalDate | ChronoUnit.DAYS.between(d1, d2) | 日曆日差(無時間分量,不可變線程安全) | 現代項目首選 | 需 Java 8+,舊 Date 需轉換 |
public static int daysDiffCalendar(Date date1, Date date2) {
Calendar cal1 = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal1.setTime(date1);
cal2.setTime(date2);
// 統一歸零時分秒毫秒,純比較「日期」
cal1.set(Calendar.HOUR_OF_DAY, 0);
cal1.set(Calendar.MINUTE, 0);
cal1.set(Calendar.SECOND, 0);
cal1.set(Calendar.MILLISECOND, 0);
cal2.set(Calendar.HOUR_OF_DAY, 0);
cal2.set(Calendar.MINUTE, 0);
cal2.set(Calendar.SECOND, 0);
cal2.set(Calendar.MILLISECOND, 0);
long diffMillis = cal2.getTimeInMillis() - cal1.getTimeInMillis();
return (int) (diffMillis / (1000 * 60 * 60 * 24));
}
關鍵點:必須歸零時間分量,否則 23:59 和 00:01 相差 2 分鐘卻算 1 天。
public static int daysDiffMillis(Date date1, Date date2) {
long diff = date2.getTime() - date1.getTime();
return (int) (diff / (1000L * 60 * 60 * 24));
}
Date d1 = parse("2015-01-01 21:00:00");
Date d2 = parse("2015-01-02 01:00:00"); // 相差 4 小時
daysDiffMillis(d1, d2); // 返回 0(不足 24h)
daysDiffCalendar(d1, d2); // 返回 1(跨自然日)
結論:業務上「跨自然日算 1 天」時禁用此法。
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
public static long daysDiffModern(Date date1, Date date2) {
LocalDate ld1 = date1.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate ld2 = date2.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
return ChronoUnit.DAYS.between(ld1, ld2); // 可為負
}
// 純字符串解析
public static long daysDiffString(String d1, String d2, String pattern) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern);
LocalDate ld1 = LocalDate.parse(d1, fmt);
LocalDate ld2 = LocalDate.parse(d2, fmt);
return ChronoUnit.DAYS.between(ld1, ld2);
}
DAYS.between 即「日曆日差」Period 獲取年/月/日組合差值long stayDays = ChronoUnit.DAYS.between(checkIn, checkOut) + 1;
public static long workDaysBetween(LocalDate start, LocalDate end) {
long days = 0;
LocalDate curr = start;
while (!curr.isAfter(end)) {
if (curr.getDayOfWeek().getValue() < 6) days++; // 1-5 週一至週五
curr = curr.plusDays(1);
}
return days;
}
// 生產環境建議引入 ThreeTen-Extra 的 WorkingDays 或自定義節假日日曆
ZoneId 明確指定業務時區(如 ZoneId.of("Asia/Shanghai"))ZoneId.systemDefault()(容器部署可能為 UTC)between 結果可為負,業務層取絕對值或拋異常Calendar/DateTimeFormatter,改用緩存或工具類public final class DateUtils {
private static final DateTimeFormatter DEFAULT_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd").withResolverStyle(ResolverStyle.STRICT);
public static long daysBetween(Date d1, Date d2) {
if (d1 == null || d2 == null) throw new IllegalArgumentException();
LocalDate ld1 = d1.toInstant().atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
LocalDate ld2 = d2.toInstant().atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
return ChronoUnit.DAYS.between(ld1, ld2);
}
public static long daysBetween(String d1, String d2) {
return daysBetween(LocalDate.parse(d1, DEFAULT_FMT), LocalDate.parse(d2, DEFAULT_FMT));
}
public static long daysBetween(LocalDate d1, LocalDate d2) {
return ChronoUnit.DAYS.between(d1, d2);
}
}
計算天數差語義優於實現:業務按自然日 → LocalDate + ChronoUnit.DAYS;科學計算精確間隔 → 毫秒數 / 86400000。Java 8+ 專案強制遷移至 java.time,舊項目封裝工具類統一歸零時間分量。牢記「歸零時分秒、鎖定時區、不可變 API」三板斧,杜絕日期計算 Bug。