報表統計、排程任務、日曆渲染等場景常需「給定起訖日期,列出中間每一天」。JDK 8 前需手寫曆法計算,JDK 8+ 引入 java.time 套件後可用流式 API 優雅解決。本文提供兩種實作:傳統 Calendar 版(相容 JDK 7)與現代 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());
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 Stream | Calendar 迴圈 |
|---|---|---|
| 單次調用耗時 (1年跨度 ~365天) | ~0.3 ms | ~0.5 ms |
| 記憶體分配 | 少量臨時物件(Stream 管道) | Calendar 實例 + Date 物件 |
| 執行緒安全 | LocalDate 不可變,天然安全 | SimpleDateFormat 非安全,需 ThreadLocal 或局部變數 |
| 程式碼可讀性 | 聲明式、鏈式、可組合 | 命令式、較冗長 |
| 時區處理 | LocalDate 無時區,純日期運算 | Calendar 依賴預設時區,跨 DST 邊界需謹慎 |
datesUntil(end.plusDays(1)) 或迴圈條件 !cal.after(endCal)。forEachDay 消費者模式逐筆處理/寫入資料庫/檔案。DateTimeFormatter(不可變、執行緒安全),勿在迴圈中新建。ZonedDateTime.now(ZoneId).toLocalDate() 取得基準日。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 可變性陷阱。