演進脈絡:從 Date 到 Calendar 再到 java.time
- JDK 1.0:
java.util.Date 包含年/月/日/時/分/秒完整功能,但設計缺陷多(不可變、非執行緒安全、國際化弱)。
- JDK 1.1:引入
Calendar、DateFormat 等,Date 降級為「毫秒時間戳包裝器」(自 1970-01-01 00:00:00 UTC 起算的毫秒數)。
- JDK 8:全新
java.time 套件(JSR-310),不可變、執行緒安全、豐富 API,**新專案首選**。
本文聚焦 Date/Calendar/SimpleDateFormat 經典體系(維護舊專案必備),文末附遷移建議。
核心類別速覽
| 類別 | 套件 | 核心職責 | 執行緒安全 |
| Date | java.util | 時間戳容器(long milliseconds) | ✅ 不可變 |
| Calendar | java.util | 日期運算、欄位存取、時區、國際化 | ❌ 可變、非安全 |
| DateFormat | java.text | 抽象基類,格式化/解析 | ❌ 非安全 |
| SimpleDateFormat | java.text | 自訂模式格式化/解析 | ❌ 非安全 |
實戰範例集錦
1. 取得當前時間戳與 Date 物件
// 方式一:Date 無參建構子(內部呼叫 System.currentTimeMillis())
Date now = new Date();
long timestamp = now.getTime(); // 1001803809710 (例)
// 方式二:直接取得時間戳
long ts = System.currentTimeMillis();
Date now2 = new Date(ts);
2. 自訂格式輸出:SimpleDateFormat
// 模式字元對照:E=星期, M=月, d=日, y=年, H=時(24), m=分, s=秒, S=毫秒
SimpleDateFormat sdf = new SimpleDateFormat("EEEE-MMMM-dd-yyyy HH:mm:ss.SSS");
String formatted = sdf.format(new Date());
// 輸出範例:Saturday-September-29-2001 18:50:09.710
// 常用預設模式
SimpleDateFormat shortFmt = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat fullFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat iso8601Fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); // ISO 8601 帶時區
3. 字串解析為 Date
SimpleDateFormat parser = new SimpleDateFormat("MM-dd-yyyy");
String input = "9-29-2001";
try {
Date date = parser.parse(input); // 嚴格依模式解析
System.out.println(date.getTime()); // 1001736000000
} catch (ParseException e) {
// 輸入不符模式拋出 ParseException
e.printStackTrace();
}
4. 內建風格格式化:DateFormat.getXxxInstance()
Date now = new Date();
// 參數:日期風格, 時間風格 (SHORT/MEDIUM/LONG/FULL)
DateFormat shortDt = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
DateFormat medDt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
DateFormat longDt = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
DateFormat fullDt = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL);
System.out.println(shortDt.format(now)); // 9/29/01 6:44 PM
System.out.println(medDt.format(now)); // Sep 29, 2001 6:44:45 PM
System.out.println(longDt.format(now)); // September 29, 2001 6:44:45 PM EDT
System.out.println(fullDt.format(now)); // Saturday, September 29, 2001 6:44:45 PM EDT
5. Calendar 日期運算與欄位存取
Calendar cal = Calendar.getInstance(); // 預設時區、Locale
cal.setTime(new Date()); // 從 Date 初始化
// 取得欄位
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH); // 0-based! 0=一月
int day = cal.get(Calendar.DAY_OF_MONTH);
int dow = cal.get(Calendar.DAY_OF_WEEK); // 1=Sunday ... 7=Saturday
// 日期運算(可正負)
cal.add(Calendar.DAY_OF_MONTH, 10); // 加 10 天
cal.add(Calendar.MONTH, -1); // 減 1 月
cal.roll(Calendar.DAY_OF_MONTH, 5); // 滾動:不改變較大欄位(月/年)
// 設定特定時間
cal.set(2024, Calendar.DECEMBER, 25, 12, 0, 0); // 2024-12-25 12:00:00
cal.set(Calendar.HOUR_OF_DAY, 0); // 清零時分秒
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
⚠️ 必須避開的經典坑
| 坑點 | 錯誤代碼 | 正確做法 | 原因 |
| SimpleDateFormat 非執行緒安全 | static final SimpleDateFormat SF = new SimpleDateFormat("..."); 多執行緒共用 | 每次 new、ThreadLocal、或改用 DateTimeFormatter (JDK 8+) | 內部共享 Calendar 導致競態 |
| 月份 0-based | cal.set(2024, 12, 25) → 2025 年 1 月 | 使用 Calendar.JANUARY...DECEMBER 常數 | Calendar.MONTH 0=一月 |
| DAY_OF_WEEK 1=Sunday | if (dow == 7) 認為週六 | 使用 Calendar.SATURDAY 常數 | 1=Sunday, 7=Saturday |
| Date 建構子棄用 | new Date(2024, 11, 25) | Calendar.set + getTime() 或 LocalDate.of | JDK 1.1 起棄用,行為易誤解 |
| 時區預設為 JVM 環境 | 跨時區部署時間不一致 | 明確設定 cal.setTimeZone(TimeZone.getTimeZone("UTC")) | 生產環境必須統一時區 |
| 格式化/解析模式不嚴謹 | "yyyy-MM-dd" 解析 "2024/12/25" 拋異常 | 輸入預處理或多模式嘗試 | SimpleDateFormat 預設嚴格模式 |
JDK 8+ java.time 遷移速查表
新專案請直接使用 java.time,以下為對應關係:
| 舊 API | 新 API (java.time) | 說明 |
| Date | Instant | 時間戳(納秒精度) |
| Calendar | ZonedDateTime / LocalDateTime | 帶時區/無時區日期時間 |
| DateFormat | DateTimeFormatter | 不可變、執行緒安全、支援 ISO 標準 |
| TimeZone | ZoneId / ZoneOffset | 時區 ID / 固定偏移 |
| Date.toInstant() | Instant.ofEpochMilli(ts) | 互轉橋樑 |
| cal.toInstant() | ZonedDateTime.ofInstant(instant, zone) | Calendar → 新類型 |
新 API 範例
// 當前時間(系統預設時區)
LocalDateTime now = LocalDateTime.now();
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Taipei"));
// 格式化(執行緒安全,可 static final)
static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = now.format(FMT);
// 解析
LocalDate date = LocalDate.parse("2024-12-25");
LocalDateTime dt = LocalDateTime.parse("2024-12-25T12:00:00");
// 日期運算(不可變,鏈式呼叫)
LocalDate nextWeek = date.plusWeeks(1).minusDays(2);
// 期間計算
Period p = Period.between(LocalDate.now(), LocalDate.of(2025, 1, 1));
System.out.println(p.getYears() + "年" + p.getMonths() + "月" + p.getDays() + "日");
總結建議
- 維護舊代碼:熟練掌握
Calendar + SimpleDateFormat(ThreadLocal) 組合,嚴守時區與執行緒安全。
- 新專案/重構:全面拥抱
java.time,享受不可變、流暢 API、標準化格式帶來的生產力與穩定性。
- 資料庫交互:JDBC 4.2+ 直接支援
LocalDate/LocalDateTime/Instant(PreparedStatement.setObject())。
- 序列化/JSON:Jackson/Gson 內建支援 java.time,配置
SerializationFeature.WRITE_DATES_AS_TIMESTAMPS = false 輸出 ISO 字串。