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

Java 計算兩個日期相差天數完整指南:多種算法對比與避坑

     編輯:Java 相關知識

計算兩個日期相差天數是業務開發高頻需求,但陷阱重重:時區、夏令時、閏年、毫秒除法精度丟失。本文系統梳理 5 種主流算法,從舊 `Date/Calendar` 到現代 `java.time`,提供生產級工具類、單元測試用例與選型建議,助你徹底告別日期計算 Bug。

核心陷阱速覽

陷阱錯誤示例後果正確做法
直接毫秒相減除 86400000`(d2.getTime()-d1.getTime())/(1000*60*60*24)`跨夏令時/閏秒/時區導致 ±1 天置零時分秒毫秒後相減,或用 ChronoUnit
Calendar.DAY_OF_YEAR 跨年`cal2.get(DAY_OF_YEAR)-cal1.get(DAY_OF_YEAR)`跨年結果為負、閏年 366 天未處理逐年累加 getActualMaximum(DAY_OF_YEAR)
SimpleDateFormat 線程不安全static 共享 sdf高併發解析亂碼/異常局部變量/ThreadLocal/DateTimeFormatter
Date 可變性導致副作用傳入 Date 被內部修改調用方數據被竄改防御性拷貝/用不可變 LocalDate

算法一:現代標準 —— java.time ChronoUnit(Java 8+ 強烈推薦)

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;

public class DateDiffUtils {

    /**
     * 計算兩個日期相差天數(自然日,忽略時間部分)
     * @param start 開始日期(包含)
     * @param end 結束日期(不包含),若 end < start 返回負數
     * @return 相差天數
     */
    public static long daysBetween(LocalDate start, LocalDate end) {
        if (start == null || end == null) throw new IllegalArgumentException("Date cannot be null");
        return ChronoUnit.DAYS.between(start, end);
    }

    /**
     * 兼容字符串輸入(ISO-8601: yyyy-MM-dd)
     */
    public static long daysBetween(String start, String end) {
        return daysBetween(LocalDate.parse(start), LocalDate.parse(end));
    }

    /**
     * 兼容 Date/Instant 輸入(按系統默認時區截取日期部分)
     */
    public static long daysBetween(java.util.Date start, java.util.Date end) {
        if (start == null || end == null) throw new IllegalArgumentException("Date cannot be null");
        ZoneId zone = ZoneId.systemDefault();
        LocalDate sd = start.toInstant().atZone(zone).toLocalDate();
        LocalDate ed = end.toInstant().atZone(zone).toLocalDate();
        return ChronoUnit.DAYS.between(sd, ed);
    }
}

優勢

  • 不可變、線程安全、API 語義清晰(`DAYS.between` 即自然日差)。
  • 自動處理閏年、月份長短、時區轉換。
  • 支援 `LocalDate`、`LocalDateTime`、`Instant`、`ZonedDateTime` 任意組合。

算法二:LocalDate 手動置零相減(教學/無 java.time 環境)

public static long daysBetweenManual(LocalDate start, LocalDate end) {
    if (start.isAfter(end)) return -daysBetweenManual(end, start);
    long days = 0;
    LocalDate cursor = start;
    while (cursor.isBefore(end)) {
        cursor = cursor.plusDays(1);
        days++;
    }
    return days;
}

邏輯清晰但 O(n) 性能差,僅供教學或極小範圍使用。

算法三:舊 Calendar 逐年累加(JDK 7 兼容)

public static int daysBetweenCalendar(java.util.Date start, java.util.Date end) {
    if (start == null || end == null) throw new IllegalArgumentException("Date cannot be null");

    Calendar cal = Calendar.getInstance();
    cal.setTime(start);
    // 置零時分秒毫秒
    cal.set(Calendar.HOUR_OF_DAY, 0);
    cal.clear(Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND);
    long startDay = cal.getTimeInMillis();

    cal.setTime(end);
    cal.set(Calendar.HOUR_OF_DAY, 0);
    cal.clear(Calendar.MINUTE, Calendar.SECOND, Calendar.MILLISECOND);
    long endDay = cal.getTimeInMillis();

    return (int) ((endDay - startDay) / (1000 * 60 * 60 * 24));
}

注意:`clear(int... fields)` 為 Java 8+ 方法,JDK 7 需分別 `clear(MINUTE)` 等。

算法四:Joda-Time(歷史遺留項目)


// 依賴:joda-time:joda-time:2.12.7
import org.joda.time.LocalDate;
import org.joda.time.Days;

public static int daysBetweenJoda(String start, String end) {
    LocalDate sd = LocalDate.parse(start); // ISO 格式
    LocalDate ed = LocalDate.parse(end);
    return Days.daysBetween(sd, ed).getDays();
}

Joda-Time 已進入維護模式,新項目請遷移至 `java.time`。

算法五:數據庫層面計算(大數據/報表場景)

-- MySQL
SELECT DATEDIFF('2024-03-15', '2024-01-01'); -- 74

-- PostgreSQL
SELECT ('2024-03-15'::date - '2024-01-01'::date); -- 74

-- Oracle
SELECT DATE '2024-03-15' - DATE '2024-01-01' FROM dual; -- 74

-- SQL Server
SELECT DATEDIFF(DAY, '2024-01-01', '2024-03-15'); -- 74

海量數據統計時,下推到數據庫層面計算效率最高。

生產級工具類完整版

package com.example.util;

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;

public final class DateDiffUtils {

    private DateDiffUtils() {}

    private static final DateTimeFormatter[] COMMON_PATTERNS = {
        DateTimeFormatter.ISO_LOCAL_DATE,
        DateTimeFormatter.ofPattern("yyyyMMdd"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd"),
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
        DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
    };

    /**
     * 核心:自然日差值
     */
    public static long daysBetween(LocalDate start, LocalDate end) {
        return ChronoUnit.DAYS.between(start, end);
    }

    /**
     * 字符串輸入(自動嘗試常見格式)
     */
    public static long daysBetween(String startStr, String endStr) {
        LocalDate start = parseFlexible(startStr);
        LocalDate end = parseFlexible(endStr);
        return daysBetween(start, end);
    }

    /**
     * Date/Instant 輸入(指定時區,默認系統區)
     */
    public static long daysBetween(Date start, Date end) {
        return daysBetween(start, end, ZoneId.systemDefault());
    }

    public static long daysBetween(Date start, Date end, ZoneId zone) {
        if (start == null || end == null) throw new IllegalArgumentException("Date cannot be null");
        LocalDate sd = start.toInstant().atZone(zone).toLocalDate();
        LocalDate ed = end.toInstant().atZone(zone).toLocalDate();
        return daysBetween(sd, ed);
    }

    /**
     * 靈活解析字符串為 LocalDate
     */
    public static LocalDate parseFlexible(String str) {
        if (str == null || str.isEmpty()) throw new IllegalArgumentException("Empty date string");
        for (DateTimeFormatter fmt : COMMON_PATTERNS) {
            try {
                return LocalDate.parse(str, fmt);
            } catch (DateTimeParseException ignored) {}
        }
        throw new IllegalArgumentException("Unparsable date: " + str);
    }

    /**
     * 計算年齡/工齡等「滿年」數
     */
    public static int yearsBetween(LocalDate start, LocalDate end) {
        return (int) ChronoUnit.YEARS.between(start, end);
    }

    /**
     * 計算月數差
     */
    public static int monthsBetween(LocalDate start, LocalDate end) {
        return (int) ChronoUnit.MONTHS.between(start, end);
    }
}

單元測試用例(JUnit 5)

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class DateDiffUtilsTest {

    @Test void basic() {
        assertEquals(1, DateDiffUtils.daysBetween("2024-01-01", "2024-01-02"));
        assertEquals(-1, DateDiffUtils.daysBetween("2024-01-02", "2024-01-01"));
        assertEquals(0, DateDiffUtils.daysBetween("2024-01-01", "2024-01-01"));
    }

    @Test void leapYear() {
        // 2024 閏年 2 月 29 天
        assertEquals(1, DateDiffUtils.daysBetween("2024-02-28", "2024-02-29"));
        assertEquals(1, DateDiffUtils.daysBetween("2024-02-29", "2024-03-01"));
    }

    @Test void crossYear() {
        assertEquals(366, DateDiffUtils.daysBetween("2024-01-01", "2025-01-01")); // 閏年
        assertEquals(365, DateDiffUtils.daysBetween("2023-01-01", "2024-01-01")); // 平年
    }

    @Test void formats() {
        assertEquals(10, DateDiffUtils.daysBetween("20240101", "20240111"));
        assertEquals(10, DateDiffUtils.daysBetween("2024/01/01", "2024/01/11"));
        assertEquals(10, DateDiffUtils.daysBetween("2024-01-01 00:00:00", "2024-01-11 23:59:59"));
    }

    @Test void dateInput() {
        java.util.Date d1 = java.util.Date.from(LocalDate.of(2024,1,1).atStartOfDay(ZoneId.systemDefault()).toInstant());
        java.util.Date d2 = java.util.Date.from(LocalDate.of(2024,1,11).atStartOfDay(ZoneId.systemDefault()).toInstant());
        assertEquals(10, DateDiffUtils.daysBetween(d1, d2));
    }
}

選型決策表

場景推薦方案理由
Java 8+ 新項目ChronoUnit.DAYS.between(LocalDate, LocalDate)標準、不可變、線程安全、自動處理閏年/時區
JDK 7 維護項目Calendar 置零後毫秒相減無第三方依賴、邏輯可控
舊項目已用 Joda-TimeDays.daysBetween()零改動、穩定
海量統計/報表SQL DATEDIFF下推計算、避免數據傳輸
需支援多種字符串格式工具類 parseFlexible + ChronoUnit兼容性強、統一入口

最佳實踐清單

  • [ ] 業務層統一使用 `LocalDate`(無時間)或 `LocalDateTime`(含時間)。
  • [ ] 所有日期差值計算委派給 `DateDiffUtils.daysBetween()` 統一入口。
  • [ ] 持久層 MyBatis/JPA 自動映射 `java.time.*`,避免手動轉換。
  • [ ] 序列化輸出 ISO-8601 格式(`yyyy-MM-dd`/`yyyy-MM-ddTHH:mm:ss`)。
  • [ ] 單元測試覆蓋:閏年、跨年、跨月、負數、不同格式、時區邊界。
  • [ ] 代碼審查禁止出現 `getTime()/(1000*60*60*24)`、`SimpleDateFormat` 靜態共享、`Date` 可變傳遞。

日期計算看似簡單,實則暗藏時區、夏令時、閏年、不可變性等陷阱。確立 `java.time` + `ChronoUnit` 為唯一標準,封裝統一工具類,配合完整測試用例,才能從根源消除日期 Bug,讓業務邏輯專注於核心價值。

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