java.util.Date 與 java.util.Calendar 是 Java 早期日期時間 API,雖設計缺陷眾多 (可變性、月份 0 基、線程不安全、時區處理混亂),但在存量代碼、舊系統維護、特定框架整合中仍廣泛存在。本文系統梳理核心用法、格式化陷阱、Calendar 操作模式,並引導遷移至 java.time (JSR-310) 現代 API。
Date now = new Date(); // 當前毫秒時間戳 long timestamp = now.getTime(); // 自 1970-01-01 00:00:00 UTC 毫秒數 System.out.println(timestamp); // 1700000000000 System.out.println(now); // toString(): "Mon Nov 14 10:30:00 CST 2023" (依賴默認 Locale/時區)
Date date = new Date();
// 用戶自定義模式
SimpleDateFormat sdf1 = new SimpleDateFormat("EEEE, MMMM dd, yyyy HH:mm:ss z", Locale.US);
System.out.println(sdf1.format(date)); // "Tuesday, November 14, 2023 10:30:00 CST"
// 標準風格 (Locale 敏感)
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.SHORT, Locale.CHINA);
System.out.println(df.format(date)); // "2023年11月14日 星期二 上午10:30"
// ⚠️ 致命陷阱:SimpleDateFormat 非線程安全!
// 錯誤:static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd"); // 多線程並發會亂碼/拋異常
// 正確:ThreadLocal 或 每次 new / 使用 DateTimeFormatter (java.time)
Calendar 為抽象類,常用 GregorianCalendar 實現。
Calendar cal = Calendar.getInstance(); // 默認時區、Locale cal.setTime(new Date()); // 從 Date 同步 // 獲取字段 int year = cal.get(Calendar.YEAR); int month = cal.get(Calendar.MONTH); // 0=1月, 11=12月 ⚠️ int day = cal.get(Calendar.DAY_OF_MONTH); int hour = cal.get(Calendar.HOUR_OF_DAY); // 24制 int dow = cal.get(Calendar.DAY_OF_WEEK); // 1=周日, 7=周六 // 設置字段 (會重新計算時間戳) cal.set(Calendar.YEAR, 2025); cal.set(Calendar.MONTH, Calendar.JANUARY); // 常量可讀 cal.set(2025, Calendar.FEBRUARY, 14, 18, 30, 0); // 批量設置 // 日期運算 cal.add(Calendar.DAY_OF_MONTH, 7); // 一週後 cal.add(Calendar.MONTH, -3); // 三個月前 cal.roll(Calendar.DAY_OF_MONTH, 10); // 滾動:不改變更大字段 (月/年)
// 本週五 cal.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY); // 本月最後一天 cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH)); // 下個月第一天 00:00:00 cal.add(Calendar.MONTH, 1); cal.set(Calendar.DAY_OF_MONTH, 1); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0);
| 常量 | 日期示例 (zh_CN) | 時間示例 |
|---|---|---|
| SHORT | 23/11/14 | 10:30 |
| MEDIUM | 2023/11/14 | 10:30:00 |
| LONG | 2023年11月14日 | 10:30:00 CST |
| FULL | 2023年11月14日 星期二 | 上午10時30分00秒 CST |
| 缺陷 | Date | Calendar | 影響 |
|---|---|---|---|
| 可變性 | ✅ (setTime) | ✅ (set/add/roll) | 共享實例導致數據竄改、難以推理 |
| 月份 0 基 | ❌ | ✅ (JANUARY=0) | 極易 Off-by-one 錯誤 |
| 年份偏移 1900 | ✅ (getYear() 返回 year-1900) | ❌ | 直接輸出誤導 |
| 線程安全 | ❌ | ❌ | 多線程環境不可共享實例 |
| 時區處理 | 模糊 (toString 用默認) | 單一時區實例 | 跨時區業務極其痛苦 |
| API 一致性 | 殘缺 (多方法棄用) | 膨脹、命名不一 | 學習成本高、易誤用 |
不可變、線程安全、領域驅動設計、ISO-8601 原生支援
| 舊 API | 新 API | 說明 |
|---|---|---|
| Date | Instant | 時間線上的瞬間 (UTC,納秒精度) |
| Calendar | ZonedDateTime / LocalDateTime | 帶時區/不帶時區的日期時間 |
| Date/Calendar (僅日期) | LocalDate | 年月日,無時間、無時區 |
| Date/Calendar (僅時間) | LocalTime | 時分秒納秒,無日期、無時區 |
| SimpleDateFormat | DateTimeFormatter | 線程安全、預定義格式、解析嚴格 |
| TimeZone | ZoneId / ZoneOffset | IANA 時區庫 (America/New_York) |
// 當前時間
Instant now = Instant.now(); // UTC
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
LocalDateTime ldt = LocalDateTime.now();
LocalDate today = LocalDate.now();
// 格式化 (線程安全,可 static final)
static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = zdt.format(FMT);
// 解析
LocalDate parsed = LocalDate.parse("2023-11-14", DateTimeFormatter.ISO_LOCAL_DATE);
// 日期運算 (不可變,返回新實例)
LocalDate nextWeek = today.plusWeeks(1);
LocalDate firstDayOfMonth = today.withDayOfMonth(1);
LocalDate lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
ZonedDateTime meeting = zdt.withHour(18).withMinute(30).withSecond(0);
// 時區轉換
ZonedDateTime nyTime = zdt.withZoneSameInstant(ZoneId.of("America/New_York"));
// 舊新互轉
Date oldDate = Date.from(instant);
Instant instant = oldDate.toInstant();
GregorianCalendar cal = GregorianCalendar.from(zdt);
ZonedDateTime zdt2 = cal.toZonedDateTime();
Instant/LocalDateTime,內部轉換getObject(rs, Instant.class)、setObject(ps, instant)JavaTimeModule、Protobuf google.protobuf.TimestampDateUtils.toInstant(date)、DateUtils.toLocalDate(cal)Date/Calendar 是 Java 早期妥協產物,可變性、月份 0 基、線程不安全、時區模糊 四大原罪導致無數 Bug。java.time (JSR-310) 以不可變、領域建模、線程安全、ISO 標準 徹底解決這些問題。維護舊代碼時理解舊 API 行為邊界,新代碼堅決使用 java.time,邊界處統一轉換,是專業 Java 開發者的必修功課。