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

Java 日期時間處理完全指南:Date、Calendar 與新時代 java.time 實戰

     編輯:Java 相關知識

日期時間處理是業務開發的高頻剛需。Java 歷經 java.util.Date (JDK 1.0)、java.util.Calendar (JDK 1.1) 到 java.time (JDK 8+) 三代演進。本文對比三代 API 優劣,給出遷移指南、格式化解析、時區處理、線程安全、常見坑點與最佳實踐。

一、三代 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
推薦等級 ❌ 僅兼容舊代碼 ❌ 僅兼容舊代碼 ✅ 新項目首選,舊項目漸進遷移

二、舊 API 實戰 (維護舊代碼必看)

1. Date 基礎用法


// 獲取當前時間
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();
}

2. Calendar 運算與字段提取


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();

3. 線程安全包裝器 (舊代碼必加)


// 方案 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);
}

三、新時代 java.time 核心實戰 (JDK 8+)

1. 核心類選型指南

  • LocalDate:只有日期 (年月日),如生日、截止日
  • LocalTime:只有時間 (時分秒納秒),如鬧鐘、營業時間
  • LocalDateTime:日期時間,無時區,如本地日程、日誌時間戳
  • ZonedDateTime:帶時區日期時間,如國際會議、航班時間
  • Instant:時間線上的一個瞬間 (UTC),如時間戳、事件溯源
  • Duration:時間間隔 (秒/納秒),如耗時統計
  • Period:日期間隔 (年月日),如年齡計算

2. 創建與獲取


// 當前時間 (系統默認時區)
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

3. 格式化與解析 (DateTimeFormatter 線程安全、不可變)


// 預定義格式化器
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);

4. 日期時間運算 (不可變,鏈式調用)


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();

5. 時區處理 (核心優勢)


// 時區轉換
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);

6. 舊新 API 互轉 (遷移關鍵)


// 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)

五、避坑指南

  1. 月份陷阱:Calendar.MONTH 0-11,java.time 月份 1-12 (符合直覺)。
  2. 線程安全:SimpleDateFormat 非線程安全,DateTimeFormatter 線程安全 (不可變)。
  3. 可變 vs 不可變:Date/Calendar 可變,java.time 所有核心類 不可變,運算返回新對象。
  4. 時區預設陷阱:LocalDateTime.now() 用系統默認時區,跨時區部署易出錯。關鍵業務顯式指定 ZoneId。
  5. 序列化:java.time 類實現 Serializable,Jackson 需註冊 JavaTimeModule。
  6. 數據庫類型映射:LocalDate → DATE,LocalTime → TIME,LocalDateTime → TIMESTAMP (無時區),Instant → TIMESTAMP / BIGINT。

六、遷移策略建議

  1. 新代碼全面使用 java.time,引入 java.time.* 禁止 java.util.Date/Calendar。
  2. 邊界轉換層統一處理:Controller 接收字符串 → LocalDateTime,Service 內部純 java.time,DAO 與 DB 交互用 Instant/LocalDateTime (JDBC 4.2+),對外輸出統一格式化。
  3. 工具類封裝:提供 DateUtils 封裝常用格式化、解析、轉換、運算,屏蔽細節。
  4. 漸進式替換舊代碼:優先替換高頻、易錯、併發敏感模塊。

Java 日期時間處理的進化史,正是從「以機器為中心」向「以領域為中心」設計的轉變。擁抱 java.time,不僅是 API 升級,更是代碼可讀性、正確性、可維護性的質變。新項目零猶豫用 java.time,舊項目建立邊界層漸進遷移,是當前最理性的選擇。

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