前言
Calendar 是 JDK 1.1 引入的日期時間操作主力類,雖然 JDK 8+ 推出 java.time,但大量存量代碼仍在使用。本文系統整理 Calendar 常用 API,重點揭示 set() 與 add() 的關鍵差異,這是無數開發者踩過的大坑。
一、Calendar 基礎認知
- 抽象類,用
Calendar.getInstance() 獲取具體實現 (預設 GregorianCalendar)
- 欄位常數:
YEAR, MONTH, DATE/DAY_OF_MONTH, HOUR_OF_DAY, MINUTE, SECOND, MILLISECOND
- 月份 0-based:JANUARY=0 ... DECEMBER=11 (極易寫錯)
- 非執行緒安全,多執行緒環境用 ThreadLocal 或 java.time
二、核心 API 速查表
| 操作 | 方法 | 範例 |
| 獲取實例 | getInstance() | Calendar cal = Calendar.getInstance(); |
| 獲取欄位值 | get(field) | int year = cal.get(Calendar.YEAR); |
| 設定欄位值 | set(field, value) | cal.set(Calendar.YEAR, 2024); |
| 批次設定 | set(y, M, d, h, m, s) | cal.set(2024, 5, 20, 14, 30, 0); |
| 增加/減少 | add(field, amount) | cal.add(Calendar.DATE, -30); |
| 滾動 (不進位) | roll(field, amount) | cal.roll(Calendar.MONTH, 1); |
| 轉 Date | getTime() | Date date = cal.getTime(); |
| 從 Date 設定 | setTime(date) | cal.setTime(new Date()); |
| 毫秒時間戳 | getTimeInMillis() | long ms = cal.getTimeInMillis(); |
| 設定毫秒時間戳 | setTimeInMillis(ms) | cal.setTimeInMillis(0); |
| 清除/重設 | clear() / clear(field) | cal.clear(); |
| 比較 | before/after/equals/compareTo | cal1.after(cal2) |
| 最大/最小值 | getActualMaximum(field) | int days = cal.getActualMaximum(Calendar.DATE); |
| 週相關 | getFirstDayOfWeek() | cal.getFirstDayOfWeek(); // 預設 SUNDAY=1 |
三、核心陷阱:set() vs add()
錯誤代碼:想取「兩個月前」卻用了 set
// 需求:獲取兩個月前的日期 (假設今天 2024-05-20)
Calendar cal = Calendar.getInstance();
cal.set(Calendar.DATE, -60); // ❌ 錯誤!將「日」欄位直接設為 -60
// 結果:月份不變,日期變成負數 → 內部正規化後跑到極早的日期
正確代碼:使用 add() 進行日期運算
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MONTH, -2); // ✅ 正確:月份欄位減 2,自動處理進位/閏年
// 結果:2024-03-20
根本原因對比
| 特性 | set(field, value) | add(field, amount) |
| 語意 | 直接覆寫欄位值 | 在欄位值基礎上增減 |
| 進位處理 | 不自動進位 (需手動正規化) | 自動處理進位、借位、閏年 |
| 適用場景 | 設定具體年月日時分秒 | 日期運算:N 天前、M 月後 |
| 範例 | cal.set(MONTH, 11) → 12月 | cal.add(MONTH, 1) → 下個月 |
補救寫法:若必須用 set 也可先 get 再運算
// 笨但可行
int today = cal.get(Calendar.DATE);
cal.set(Calendar.DATE, today - 60); // 先獲取當前值再運算
// 但強烈建議直接用 add()
四、實戰避坑案例集
案例 1:定時任務「每天 00:00 執行」計算下次執行時間
// ❌ 錯誤:set 會重置時分秒,但若當前已過 00:00 會停在今天
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
// ✅ 正確:先歸零,若已過今天 00:00 再加一天
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
if (cal.getTimeInMillis() <= System.currentTimeMillis()) {
cal.add(Calendar.DATE, 1);
}
案例 2:獲取「本月第一天 00:00」與「本月最後一天 23:59:59」
Calendar cal = Calendar.getInstance();
// 本月第一天
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);
Date monthStart = cal.getTime();
// 本月最後一天
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(Calendar.MILLISECOND, 999);
Date monthEnd = cal.getTime();
案例 3:計算兩日期相差月數
public static int monthDiff(Date start, Date end) {
Calendar s = Calendar.getInstance(); s.setTime(start);
Calendar e = Calendar.getInstance(); e.setTime(end);
return (e.get(Calendar.YEAR) - s.get(Calendar.YEAR)) * 12
+ (e.get(Calendar.MONTH) - s.get(Calendar.MONTH));
}
案例 4:判斷閏年與月份天數
// 方法 1:Calendar 內建
cal.set(Calendar.YEAR, 2024);
cal.set(Calendar.MONTH, Calendar.FEBRUARY);
int days = cal.getActualMaximum(Calendar.DAY_OF_MONTH); // 29
// 方法 2:手動判斷 (不依賴 Calendar 實例)
public static boolean isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
public static int daysInMonth(int year, int month) { // month 1-12
if (month == 2) return isLeap(year) ? 29 : 28;
if (month == 4 || month == 6 || month == 9 || month == 11) return 30;
return 31;
}
五、Date ↔ Calendar ↔ String 互轉最佳實踐
// 1. Date → Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(date);
// 2. Calendar → Date
Date date = cal.getTime();
// 3. Date → String (執行緒安全寫法)
String str = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
// 4. String → Date
Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(str);
// 5. Calendar → String (直接用 getTime() 轉 Date 再格式化)
String str = new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime());
// 6. String → Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(str));
六、從 Calendar 遷移到 java.time (JDK 8+)
| Calendar 代碼 | java.time 等效 | 優勢 |
| cal.add(MONTH, -2) | localDate.minusMonths(2) | 不可變、鏈式、無 0-based 月份坑 |
| cal.set(YEAR, 2024) | localDate.withYear(2024) | 語意清晰、回傳新物件 |
| cal.getActualMaximum(DATE) | yearMonth.lengthOfMonth() | YearMonth 物件語意精確 |
| cal.getTimeInMillis() | instant.toEpochMilli() | Instant 專為時間戳設計 |
| new SimpleDateFormat... | DateTimeFormatter.ofPattern... | 執行緒安全、可快取、支援解析嚴格模式 |
七、總結:Calendar 使用鐵律
- 日期運算永遠用 add(),絕不使用 set() 做加減
- 月份記住 0-based:用 Calendar.JANUARY...DECEMBER 常數
- 獲取月份天數用 getActualMaximum(Calendar.DATE)
- SimpleDateFormat 區域變數或 ThreadLocal,別 static 共用
- 新代碼優先用 java.time (LocalDate/LocalDateTime/ZonedDateTime)
- 多執行緒環境不要共享 Calendar 實例