Java 日期時間 API 經歷了三代演進:java.util.Date(1.0,缺陷多)、java.util.Calendar(1.1,設計混亂)、java.time.*(Java 8,不可變、線程安全、領域驅動設計)。雖然新項目應優先使用 Java 8+ API,但維護舊系統時仍需面對 Date 與 Calendar。本文匯總三代 API 核心用法,並提供一個生產級的 DateUtils 工具類,涵蓋格式化、解析、計算、比較、時區、閏年等高頻場景。
| 特性 | Date (1.0) | Calendar (1.1) | java.time (8+) |
|---|---|---|---|
| 線程安全 | ❌ 可變 | ❌ 可變 | ✅ 不可變 |
| 時區處理 | ❌ 弱 | ⚠️ 複雜 | ✅ 強 |
| API 直觀度 | ⚠️ 方法名誤導 | ❌ get/set 混亂 | ✅ 領域建模 |
| 格式化 | SimpleDateFormat (非線程安全) | 同左 | DateTimeFormatter (線程安全) |
| 互操作 | 基礎 | 基礎 | toInstant()、Date.from() |
// Date/Calendar 時代
long ts = System.currentTimeMillis(); // 推薦
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(date);
// Java 8+ 推薦
Instant now = Instant.now(); // UTC 時間戳
long epochMilli = now.toEpochMilli();
LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted8 = ldt.format(dtf);
// 舊 API(注意 SimpleDateFormat 非線程安全,建議 ThreadLocal 或每次 new)
String str = "2024-06-15 14:30:00";
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str);
// Java 8+(DateTimeFormatter 線程安全,可靜態常量)
DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(str, FMT);
ZonedDateTime zdt = ZonedDateTime.parse("2024-06-15T14:30:00+08:00", DateTimeFormatter.ISO_ZONED_DATE_TIME);
// Calendar
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 7); // 7 天后
cal.add(Calendar.MONTH, -1); // 1 月前
// Java 8+ 鏈式調用,語義清晰
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
long daysBetween = ChronoUnit.DAYS.between(today, nextWeek); // 7
Period period = Period.between(today, nextWeek); // P7D
// Java 8+
Year year = Year.of(2024);
boolean leap = year.isLeap(); // true
YearMonth ym = YearMonth.of(2024, 2);
int lastDay = ym.lengthOfMonth(); // 29
// 判斷兩日期是否同一周(以 ISO 周為準)
LocalDate d1 = LocalDate.of(2024, 6, 10); // 週一
LocalDate d2 = LocalDate.of(2024, 6, 16); // 週日
boolean sameWeek = d1.get(WeekFields.ISO.weekOfYear()) == d2.get(WeekFields.ISO.weekOfYear())
&& d1.getYear() == d2.getYear();
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = shanghai.withZoneSameInstant(ZoneId.of("America/New_York"));
// 或指定時區解析
ZonedDateTime.parse("2024-06-15 14:30:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.of("UTC")));
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.Locale;
public final class DateUtils {
private DateUtils() {}
// 線程安全格式化器常量
public static final DateTimeFormatter FMT_SECOND = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter FMT_DAY = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final DateTimeFormatter FMT_MILLIS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public static final DateTimeFormatter FMT_COMPACT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
/** 當前系統時間 (LocalDateTime) */
public static LocalDateTime now() { return LocalDateTime.now(); }
/** 當前日期 (LocalDate) */
public static LocalDate today() { return LocalDate.now(); }
/** 格式化 LocalDateTime -> String */
public static String format(LocalDateTime dt, DateTimeFormatter fmt) {
return dt == null ? "" : dt.format(fmt);
}
/** 解析 String -> LocalDateTime */
public static LocalDateTime parse(String str, DateTimeFormatter fmt) {
return str == null || str.isEmpty() ? null : LocalDateTime.parse(str, fmt);
}
/** 日期加減天數 */
public static LocalDate plusDays(LocalDate date, long days) {
return date == null ? null : date.plusDays(days);
}
/** 兩日期相差天數 */
public static long daysBetween(LocalDate start, LocalDate end) {
if (start == null || end == null) return 0;
return ChronoUnit.DAYS.between(start, end);
}
/** 判斷閏年 */
public static boolean isLeapYear(int year) {
return Year.of(year).isLeap();
}
/** 獲取某年某月最後一天 */
public static int lastDayOfMonth(int year, int month) {
return YearMonth.of(year, month).lengthOfMonth();
}
/** 判斷兩日期是否同一周 (ISO-8601 週一為周首) */
public static boolean isSameWeek(LocalDate d1, LocalDate d2) {
if (d1 == null || d2 == null) return false;
WeekFields wf = WeekFields.ISO;
return d1.getYear() == d2.getYear()
&& d1.get(wf.weekOfYear()) == d2.get(wf.weekOfYear());
}
/** 獲取某日期所在周的周一日期 */
public static LocalDate mondayOfWeek(LocalDate date) {
return date.with(WeekFields.ISO.dayOfWeek(), 1); // 1=週一
}
/** 時間戳 <-> LocalDateTime (系統默認時區) */
public static LocalDateTime toLocalDateTime(long epochMilli) {
return Instant.ofEpochMilli(epochMilli).atZone(ZoneId.systemDefault()).toLocalDateTime();
}
public static long toEpochMilli(LocalDateTime ldt) {
return ldt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
/** 生成唯一流水號:yyyyMMddHHmmss + 隨機數 */
public static String genSerial(int randomDigits) {
String base = LocalDateTime.now().format(FMT_COMPACT);
if (randomDigits <= 0) return base;
StringBuilder sb = new StringBuilder(base);
for (int i = 0; i < randomDigits; i++) sb.append((int)(Math.random() * 10));
return sb.toString();
}
}
DateUtils.toLocalDateTime(date.getTime()) 轉為 LocalDateTime,內部業務邏輯全用新 API,出口再 Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()) 轉回 Date。LocalDateTime、LocalDate、Instant,直接映射 DATETIME/DATE/TIMESTAMP,無需手動轉換。LocalDateTime 序列化為 ISO-8601 字符串(2024-06-15T14:30:00),前端解析友好。DateTimeFormatter,消除線程安全隱患。Java 8 java.time 包以不可變、線程安全、領域驅動設計徹底解決了舊 API 的痛點。新項目無理由不使用;舊項目通過邊界轉換漸進式遷移。掌握 LocalDate、LocalDateTime、ZonedDateTime、Instant、Duration/Period、DateTimeFormatter 核心類,配合 ChronoUnit、TemporalAdjusters 完成 90% 以上的日期時間業務場景。