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

Java 日期區間遍歷工具類:高效生成兩日期間每一天

     編輯:Java 相關知識

需求背景

報表統計、排程任務、日曆渲染等場景常需「給定起訖日期,列出中間每一天」。JDK 8 前需手寫曆法計算,JDK 8+ 引入 java.time 套件後可用流式 API 優雅解決。本文提供兩種實作:傳統 Calendar 版(相容 JDK 7)與現代 LocalDate 版(JDK 8+ 推薦),並分析效能與陷阱。

方案一:現代 LocalDate 版(JDK 8+ 強烈推薦)

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class DateRangeUtils {

    private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    /**
     * 取得區間內所有日期(包含起訖)
     * @param start 起始日期字串 "yyyy-MM-dd"
     * @param end   結束日期字串 "yyyy-MM-dd"
     * @return 日期字串列表,若 start > end 回傳空列表
     */
    public static List getDays(String start, String end) {
        LocalDate s = LocalDate.parse(start, FMT);
        LocalDate e = LocalDate.parse(end, FMT);
        if (s.isAfter(e)) return List.of();

        // Stream 產生日期流,收集為列表
        return s.datesUntil(e.plusDays(1))
                .map(FMT::format)
                .collect(ArrayList::new, List::add, List::addAll);
    }

    /** 直接回傳 LocalDate 列表,避免字串解析開銷 */
    public static List getLocalDates(LocalDate start, LocalDate end) {
        if (start.isAfter(end)) return List.of();
        return start.datesUntil(end.plusDays(1)).collect(ArrayList::new, List::add, List::addAll);
    }

    /** 大區間分批處理(避免記憶體壓力),配合 Consumer 處理每一天 */
    public static void forEachDay(LocalDate start, LocalDate end, java.util.function.Consumer action) {
        if (start.isAfter(end)) return;
        start.datesUntil(end.plusDays(1)).forEach(action);
    }
}

使用範例

// 1. 字串列表
List days = DateRangeUtils.getDays("2024-01-28", "2024-02-03");
days.forEach(System.out::println);
// 2024-01-28
// 2024-01-29
// ...
// 2024-02-03

// 2. 直接處理 LocalDate(推薦)
DateRangeUtils.forEachDay(LocalDate.of(2024,1,28), LocalDate.of(2024,2,3), d -> {
    System.out.println(d.getDayOfWeek() + " " + d); // 周日 2024-01-28 ...
});

// 3. 串流進階:過濾工作日
List workDays = DateRangeUtils.getLocalDates(start, end).stream()
    .filter(d -> d.getDayOfWeek().getValue() < 6) // 1-5 週一至週五
    .collect(Collectors.toList());

方案二:傳統 Calendar 版(JDK 7 相容)

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.text.SimpleDateFormat;

public class LegacyDateRangeUtils {

    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");

    public static List getDays(String startStr, String endStr) {
        try {
            Date start = SDF.parse(startStr);
            Date end = SDF.parse(endStr);
            if (start.after(end)) return List.of();

            Calendar cal = Calendar.getInstance();
            cal.setTime(start);
            Calendar endCal = Calendar.getInstance();
            endCal.setTime(end);

            List result = new ArrayList<>();
            while (!cal.after(endCal)) {
                result.add(SDF.format(cal.getTime()));
                cal.add(Calendar.DAY_OF_MONTH, 1);
            }
            return result;
        } catch (Exception e) {
            throw new IllegalArgumentException("日期格式錯誤,需 yyyy-MM-dd", e);
        }
    }
}

效能與記憶體對比

指標LocalDate StreamCalendar 迴圈
單次調用耗時 (1年跨度 ~365天)~0.3 ms~0.5 ms
記憶體分配少量臨時物件(Stream 管道)Calendar 實例 + Date 物件
執行緒安全LocalDate 不可變,天然安全SimpleDateFormat 非安全,需 ThreadLocal 或局部變數
程式碼可讀性聲明式、鏈式、可組合命令式、較冗長
時區處理LocalDate 無時區,純日期運算Calendar 依賴預設時區,跨 DST 邊界需謹慎

常見陷阱與最佳實務

  1. 包含結束日:datesUntil(end.plusDays(1)) 或迴圈條件 !cal.after(endCal)。
  2. 大區間(如 10 年):避免一次性收集巨大 List,改用 forEachDay 消費者模式逐筆處理/寫入資料庫/檔案。
  3. 格式化效能:批量格式化時重用 DateTimeFormatter(不可變、執行緒安全),勿在迴圈中新建。
  4. 時區敏感業務:若涉及「某時區的今天」,用 ZonedDateTime.now(ZoneId).toLocalDate() 取得基準日。
  5. 舊系統整合:LocalDate ↔ Date 轉換:Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant())。

進階:自定義日期間隔(步長)

/** 每 N 天取一個日期 */
public static List getDaysStep(LocalDate start, LocalDate end, int stepDays) {
    if (stepDays <= 0) throw new IllegalArgumentException("步長必須 > 0");
    List list = new ArrayList<>();
    for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(stepDays)) {
        list.add(d);
    }
    return list;
}

/** 只取每月第一天 */
public static List getMonthStarts(LocalDate start, LocalDate end) {
    return start.datesUntil(end.plusDays(1))
            .filter(d -> d.getDayOfMonth() == 1)
            .collect(Collectors.toList());
}

結語

JDK 8 java.time API 以不可變、流式、執行緒安全的設計徹底解決了舊版日期時間 API 的痛點。日期區間遍歷只需一行 start.datesUntil(end.plusDays(1)) 即可優雅完成。專案若仍在 JDK 7,建議升級或引入 ThreeTen-Backport 回port庫;若必須維護舊代碼,請嚴格遵守 SimpleDateFormat 執行緒安全與 Calendar 可變性陷阱。

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