線程安全、不可變、API 豐富,完全替代 Date/Calendar/SimpleDateFormat。
LocalDate:日期 (年月日)LocalTime:時間 (時分秒納秒)LocalDateTime:日期時間Instant:時間戳 (UTC)ZonedDateTime:帶時區DateTimeFormatter:格式化/解析 (線程安全)Duration / Period:時間間隔// 當前日期
LocalDate today = LocalDate.now();
// 加減天數
LocalDate tomorrow = today.plusDays(1);
LocalDate yesterday = today.minusDays(1);
// 格式化
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String str = today.format(fmt); // "2024-01-15"
LocalDate parsed = LocalDate.parse(str, fmt);
// 兩日期間天數
long days = ChronoUnit.DAYS.between(start, end);
// 日期區間流式生成
List<LocalDate> list = start.datesUntil(end.plusDays(1)).collect(toList());
// 舊 API 互轉
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> TL_SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 指定格式
public static SimpleDateFormat getFormatter(String pattern) {
return new SimpleDateFormat(pattern); // 每次新建,或用 Map 池化
}
// Date → String
public static String format(Date date, String pattern) {
return getFormatter(pattern).format(date);
}
// String → Date
public static Date parse(String str, String pattern) throws ParseException {
return getFormatter(pattern).parse(str);
}
// 獲取前一天 / 後一天 (Date 版)
public static Date addDays(Date date, int days) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.DAY_OF_MONTH, days);
return cal.getTime();
}
// 日期區間列表 (Date 版)
public static List<Date> range(Date start, Date end, String pattern) {
List<Date> list = new ArrayList<>();
Date cur = start;
while (!cur.after(end)) {
list.add(cur);
cur = addDays(cur, 1);
}
return list;
}
// 兩 Date 比較 (忽略時分秒)
public static boolean sameDay(Date d1, Date d2) {
return format(d1, "yyyyMMdd").equals(format(d2, "yyyyMMdd"));
}
// 判斷是否在範圍內
public static boolean between(Date target, Date start, Date end) {
return !target.before(start) && !target.after(end);
}
}
SimpleDateFormat 用 ThreadLocal 或每次新建;DateTimeFormatter 本身線程安全可共享ZoneId| 坑 | 原因 | 解決 |
|---|---|---|
| SimpleDateFormat 線程不安全 | 內部共享 Calendar | ThreadLocal / 每次新建 / DateTimeFormatter |
| yyyy vs YYYY | YYYY 是週年,跨年周會導致年份錯誤 | 嚴格用 yyyy |
| MM vs mm | MM 月份,mm 分鐘 | 仔細核對模式字符串 |
| Date 忽略時區 | Date 本質是 UTC 毫秒數,toString 用默認時區 | 顯式指定時區格式化 |
| LocalDate.parse 拋異常 | 格式不匹配 | 預校驗或捕獲 DateTimeParseException |
| 夏令時導致 23/25 小時 | Calendar.add(DAY) 可能跨越 DST 切換點 | 用 LocalDate 或 Instant 計算 |
@Test
public void testRange() {
LocalDate start = LocalDate.of(2024, 1, 1);
LocalDate end = LocalDate.of(2024, 1, 5);
List<LocalDate> list = start.datesUntil(end.plusDays(1)).collect(toList());
assertEquals(5, list.size());
assertEquals(LocalDate.of(2024, 1, 3), list.get(2));
}
@Test
public void testLegacyCompat() {
Date now = new Date();
Date tomorrow = DateUtils.addDays(now, 1);
assertTrue(tomorrow.after(now));
String str = DateUtils.format(now, "yyyy-MM-dd");
Date parsed = DateUtils.parse(str, "yyyy-MM-dd");
assertTrue(DateUtils.sameDay(now, parsed));
}
新代碼全面遷移 java.time,舊項目封裝 DateUtils 並標註 @Deprecated 引導遷移。核心口訣:
DateTimeFormatter 共享,SimpleDateFormat 隔離ZoneId.systemDefault() 或業務時區ChronoUnit.between / datesUntilyyyy-MM-dd 而非 YYYY-MM-dd掌握這套體系,日期時間處理不再是 Bug 溫床。