日期時間處理是業務開發的高頻剛需。Java 歷經 java.util.Date (JDK 1.0)、java.util.Calendar (JDK 1.1) 到 java.time (JDK 8+) 三代演進。本文對比三代 API 優劣,給出遷移指南、格式化解析、時區處理、線程安全、常見坑點與最佳實踐。
| 特性 | Date (JDK 1.0) | Calendar (JDK 1.1) | java.time (JDK 8+) |
|---|---|---|---|
| 核心類 | java.util.Date |
java.util.Calendar (抽象類) |
LocalDate/LocalTime/LocalDateTime/ZonedDateTime/Instant |
| 設計缺陷 | 可變、非線程安全、年份偏移 1900、月份 0-11、無時區概念 | 可變、非線程安全、API 繁瑣、月份仍 0-11、偏移量易錯 | 不可變、線程安全、領域驅動設計、清晰分離日期/時間/時區 |
| 格式化/解析 | SimpleDateFormat (非線程安全) |
同 Date | DateTimeFormatter (線程安全、不可變) |
| 時區支持 | 極弱 | TimeZone 配合 |
ZoneId/ZoneOffset 原生支持 |
| 運算/比較 | 極其困難 | add()/roll() 易混淆 |
plus/minus/until/isBefore/After 流暢 API |
| 推薦等級 | ❌ 僅兼容舊代碼 | ❌ 僅兼容舊代碼 | ✅ 新項目首選,舊項目漸進遷移 |
// 獲取當前時間
Date now = new Date(); // 精確到毫秒
System.out.println(now); // 如: Tue Oct 15 14:30:45 CST 2023
// 格式化輸出 (SimpleDateFormat 非線程安全!)
SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd"); // 日期
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 日期時間
SimpleDateFormat sdf3 = new SimpleDateFormat("HH:mm:ss"); // 僅時間
SimpleDateFormat sdf4 = new SimpleDateFormat("E, yyyy-MM-dd 'at' HH:mm:ss z"); // 完整格式
System.out.println(sdf1.format(now));
System.out.println(sdf2.format(now));
// 解析字符串
try {
Date parsed = sdf2.parse("2023-10-15 14:30:45");
} catch (ParseException e) {
e.printStackTrace();
}
Calendar cal = Calendar.getInstance(); // 默認時區、Locale
// 字段提取 (注意:月份 0-11,需 +1 顯示)
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 關鍵!
int day = cal.get(Calendar.DAY_OF_MONTH); // 或 DATE
int hour = cal.get(Calendar.HOUR_OF_DAY); // 24制
int minute = cal.get(Calendar.MINUTE);
int second = cal.get(Calendar.SECOND);
int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK); // 1=周日 ... 7=周六
// 日期運算 (可變對象,慎用於多線程)
cal.add(Calendar.MONTH, 1); // 加 1 月
cal.add(Calendar.DAY_OF_MONTH, -3); // 減 3 天
cal.set(Calendar.HOUR_OF_DAY, 0); // 設為凌晨
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
// 轉 Date
Date date = cal.getTime();
// 方案 1: ThreadLocal 緩存 Formatter (高性能)
private static final ThreadLocal TL_SDF = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static String format(Date date) {
return TL_SDF.get().format(date);
}
public static Date parse(String str) throws ParseException {
return TL_SDF.get().parse(str);
}
// 方案 2: 每次 new (低性能、簡單場景可用)
public static String formatSafe(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
LocalDate:只有日期 (年月日),如生日、截止日LocalTime:只有時間 (時分秒納秒),如鬧鐘、營業時間LocalDateTime:日期時間,無時區,如本地日程、日誌時間戳ZonedDateTime:帶時區日期時間,如國際會議、航班時間Instant:時間線上的一個瞬間 (UTC),如時間戳、事件溯源Duration:時間間隔 (秒/納秒),如耗時統計Period:日期間隔 (年月日),如年齡計算
// 當前時間 (系統默認時區)
LocalDate today = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime nowDt = LocalDateTime.now();
ZonedDateTime nowZdt = ZonedDateTime.now(); // 帶時區
Instant nowInstant = Instant.now(); // UTC 瞬間
// 指定時間
LocalDate specificDate = LocalDate.of(2023, 10, 15);
LocalTime specificTime = LocalTime.of(14, 30, 45);
LocalDateTime specificDt = LocalDateTime.of(2023, 10, 15, 14, 30, 45);
ZonedDateTime specificZdt = ZonedDateTime.of(2023, 10, 15, 14, 30, 45, 0, ZoneId.of("Asia/Shanghai"));
// 解析字符串 (ISO 8601 格式無需格式化器)
LocalDate parsedDate = LocalDate.parse("2023-10-15");
LocalDateTime parsedDt = LocalDateTime.parse("2023-10-15T14:30:45");
ZonedDateTime parsedZdt = ZonedDateTime.parse("2023-10-15T14:30:45+08:00[Asia/Shanghai]");
Instant parsedInstant = Instant.parse("2023-10-15T06:30:45Z"); // UTC
// 預定義格式化器
DateTimeFormatter ISO_DATE = DateTimeFormatter.ISO_LOCAL_DATE; // yyyy-MM-dd
DateTimeFormatter ISO_DT = DateTimeFormatter.ISO_LOCAL_DATE_TIME; // yyyy-MM-ddTHH:mm:ss
// 自定義格式化器 (線程安全,可靜態常量)
private static final DateTimeFormatter CUSTOM_FMT = DateTimeFormatter
.ofPattern("yyyy年MM月dd日 HH:mm:ss")
.withLocale(Locale.CHINA)
.withZone(ZoneId.systemDefault()); // 可選:用於 ZonedDateTime/Instant
// 格式化
String formatted = nowDt.format(CUSTOM_FMT); // 2023年10月15日 14:30:45
String formattedZdt = nowZdt.format(CUSTOM_FMT); // 自動應用時區
// 解析
LocalDateTime parsed = LocalDateTime.parse("2023年10月15日 14:30:45", CUSTOM_FMT);
LocalDateTime dt = LocalDateTime.now();
// 加減操作 (返回新對象)
LocalDateTime nextWeek = dt.plusWeeks(1);
LocalDateTime lastMonth = dt.minusMonths(1);
LocalDateTime complex = dt.plusYears(1).minusDays(3).plusHours(2);
// 精確字段修改
LocalDateTime withNoon = dt.withHour(12).withMinute(0).withSecond(0);
LocalDate firstDayOfMonth = dt.withDayOfMonth(1);
LocalDate nextFriday = dt.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
// 間隔計算
Duration duration = Duration.between(startInstant, endInstant);
long seconds = duration.getSeconds();
long millis = duration.toMillis();
Period period = Period.between(startDate, endDate);
int years = period.getYears();
int months = period.getMonths();
int days = period.getDays();
// 時區轉換
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = shanghai.withZoneSameInstant(ZoneId.of("America/New_York"));
// 同一瞬間,不同時區顯示
// 夏令時自動處理
ZoneId la = ZoneId.of("America/Los_Angeles");
ZonedDateTime dstStart = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0, la); // 夏令時開始
ZonedDateTime dstEnd = ZonedDateTime.of(2024, 11, 3, 1, 30, 0, 0, la); // 夏令時結束
// 自動處理時鐘跳轉,無需手動計算
// 固定偏移量
ZoneOffset offset8 = ZoneOffset.ofHours(8); // UTC+8
OffsetDateTime odt = OffsetDateTime.now(offset8);
// Date -> java.time
Instant instant = date.toInstant();
LocalDateTime ldt = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDate ld = ldt.toLocalDate();
ZonedDateTime zdt = instant.atZone(ZoneId.of("Asia/Shanghai"));
// java.time -> Date
Date date = Date.from(instant);
Date date2 = Date.from(zdt.toInstant());
Date date3 = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
// Calendar -> java.time (較繁瑣,建議通過 Date 中轉)
Instant instantCal = calendar.toInstant();
// 反之同理
| 場景 | 推薦類型 | 關鍵代碼片段 |
|---|---|---|
| 用戶生日存儲 | LocalDate | LocalDate birthday = LocalDate.parse("1990-05-20"); |
| 日誌時間戳 (高精度、可排序) | Instant | Instant.now() 存數據庫 TIMESTAMP / BIGINT (epochMilli) |
| 定時任務觸發時間 (本地) | LocalDateTime | LocalDateTime.of(2023, 10, 15, 2, 0) 每天凌晨 2 點 |
| 國際會議/航班時間 | ZonedDateTime | ZonedDateTime.of(..., ZoneId.of("Europe/Paris")) |
| 接口耗時統計 | Instant + Duration | Duration.between(start, end).toMillis() |
| 年齡計算 | LocalDate + Period | Period.between(birthday, LocalDate.now()).getYears() |
| 下個月第一天 00:00 | LocalDate + TemporalAdjusters | LocalDate.now().with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay() |
| 數據庫交互 (JDBC 4.2+) | 直接 set/get Object | ps.setObject(1, localDateTime); / rs.getObject(1, LocalDateTime.class) |
Calendar.MONTH 0-11,java.time 月份 1-12 (符合直覺)。SimpleDateFormat 非線程安全,DateTimeFormatter 線程安全 (不可變)。Date/Calendar 可變,java.time 所有核心類 不可變,運算返回新對象。LocalDateTime.now() 用系統默認時區,跨時區部署易出錯。關鍵業務顯式指定 ZoneId。java.time 類實現 Serializable,Jackson 需註冊 JavaTimeModule。LocalDate → DATE,LocalTime → TIME,LocalDateTime → TIMESTAMP (無時區),Instant → TIMESTAMP / BIGINT。java.time.* 禁止 java.util.Date/Calendar。LocalDateTime,Service 內部純 java.time,DAO 與 DB 交互用 Instant/LocalDateTime (JDBC 4.2+),對外輸出統一格式化。DateUtils 封裝常用格式化、解析、轉換、運算,屏蔽細節。Java 日期時間處理的進化史,正是從「以機器為中心」向「以領域為中心」設計的轉變。擁抱 java.time,不僅是 API 升級,更是代碼可讀性、正確性、可維護性的質變。新項目零猶豫用 java.time,舊項目建立邊界層漸進遷移,是當前最理性的選擇。