Calendar 類架構速覽
public abstract class Calendar
implements Serializable, Cloneable, Comparable {
// 工廠方法獲取實例
public static Calendar getInstance()
public static Calendar getInstance(TimeZone zone)
public static Calendar getInstance(Locale locale)
public static Calendar getInstance(TimeZone zone, Locale locale)
// 核心字段 (17個)
public final static int ERA = 0; // 紀元 (BC/AD)
public final static int YEAR = 1; // 年
public final static int MONTH = 2; // 月 (0-11, JANUARY=0)
public final static int WEEK_OF_YEAR = 3; // 年內第幾周
public final static int WEEK_OF_MONTH = 4; // 月內第幾周
public final static int DATE = 5; // 日 (1-31)
public final static int DAY_OF_MONTH = 5; // 同 DATE
public final static int DAY_OF_YEAR = 6; // 年內第幾天
public final static int DAY_OF_WEEK = 7; // 週幾 (SUNDAY=1)
public final static int DAY_OF_WEEK_IN_MONTH = 8;
public final static int AM_PM = 9; // 上午/下午 (AM=0, PM=1)
public final static int HOUR = 10; // 12制時 (0-11)
public final static int HOUR_OF_DAY = 11; // 24制時 (0-23)
public final static int MINUTE = 12; // 分
public final static int SECOND = 13; // 秒
public final static int MILLISECOND = 14; // 毫秒
public final static int ZONE_OFFSET = 15; // 時區偏移 (毫秒)
public final static int DST_OFFSET = 16; // 夏令時偏移 (毫秒)
}
核心設計原理
- 單一時間源:內部維護
protected long time (毫秒時間戳,相對 1970-01-01 UTC)
- 字段延遲計算:17 個字段值存儲在
int[] fields,調用 get() 時觸發 complete() → computeFields() 從 time 反推
- 子類實現具體日曆系統:
GregorianCalendar (默認)、JapaneseImperialCalendar、BuddhistCalendar 等
- 時區與 Locale 敏感:構造時傳入決定具體行為
常用 API 分類速查
1. 字段訪問與修改
| 方法 | 說明 | 示例 |
get(field) | 獲取字段值 | cal.get(Calendar.MONTH) → 8 (9月) |
set(field, value) | 設置字段值 (延遲生效) | cal.set(Calendar.YEAR, 2024) |
add(field, amount) | 增加/減少字段值 (聯動大字段) | cal.add(Calendar.MONTH, -1) 上個月 |
roll(field, amount) | 滾動字段值 (不聯動大字段) | cal.roll(Calendar.MONTH, -1) 月份-1, 年份不變 |
clear(field) | 清空字段 (重置為最小值) | cal.clear(Calendar.HOUR_OF_DAY) |
isSet(field) | 判斷字段是否被顯式設置 | cal.isSet(Calendar.YEAR) |
2. 邊界值查詢
// 理論極值 (不依賴當前日期)
cal.getMaximum(Calendar.DATE); // 31
cal.getMinimum(Calendar.MONTH); // 0
// 實際極值 (依賴當前日期)
cal.getActualMaximum(Calendar.DATE); // 當月實際天數 (28/29/30/31)
cal.getActualMinimum(Calendar.DATE); // 總是 1
3. 日期時間物件互轉
// Calendar → Date
Date date = cal.getTime();
// Date → Calendar
cal.setTime(date);
// Calendar → 毫秒時間戳
long ms = cal.getTimeInMillis();
cal.setTimeInMillis(ms);
// 時區操作
TimeZone tz = cal.getTimeZone();
cal.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
4. 比較與判斷
cal1.equals(cal2); // 時間+時區+Locale 全等
cal1.before(cal2); // 是否早於
cal1.after(cal2); // 是否晚於
cal1.compareTo(cal2); // -1/0/1
5. 寬容模式
cal.setLenient(true); // 寬容:月份 13 自動進位到明年 1 月
cal.setLenient(false); // 嚴格:月份 13 拋 IllegalArgumentException
// 異常在 get()/add()/roll() 等觸發計算時拋出,set() 只標記
6. 週相關設置
cal.getFirstDayOfWeek(); // 週幾為每周第一天 (US=SUNDAY, EU=MONDAY)
cal.setFirstDayOfWeek(Calendar.MONDAY);
cal.getMinimalDaysInFirstWeek(); // 第一周最少天數 (1 或 7)
cal.setMinimalDaysInFirstWeek(4); // ISO 8601 標準:第一周含當年第一個周四
Java 8+ java.time 替代方案 (強烈推薦)
| Calendar | java.time (不可變、線程安全、API 清晰) |
Calendar.getInstance() | LocalDateTime.now() / ZonedDateTime.now() |
cal.get(YEAR) | ldt.getYear() |
cal.add(MONTH, 1) | ldt.plusMonths(1) |
cal.set(YEAR, 2024) | ldt.withYear(2024) |
cal.getTime() | Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant()) |
SimpleDateFormat | DateTimeFormatter.ofPattern("yyyy-MM-dd") |
實戰工具類:CalendarUtils (生產級封裝)
public final class CalendarUtils {
private CalendarUtils() {}
// 獲取 Calendar 實例 (指定時區)
public static Calendar getCalendar(TimeZone tz) {
Calendar cal = Calendar.getInstance(tz);
cal.setLenient(false); // 生產環境建議嚴格模式
return cal;
}
// 當月天數
public static int daysInMonth(int year, int month) { // month 1-12
Calendar cal = Calendar.getInstance();
cal.set(year, month - 1, 1);
return cal.getActualMaximum(Calendar.DATE);
}
// 當月第一天是周幾 (1=SUN...7=SAT)
public static int firstDayOfWeek(int year, int month) {
Calendar cal = Calendar.getInstance();
cal.set(year, month - 1, 1);
return cal.get(Calendar.DAY_OF_WEEK);
}
// 日期加減天數
public static Date addDays(Date date, int days) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.DATE, days);
return cal.getTime();
}
// 日期加減月數 (保持日數,超出則取月末)
public static Date addMonths(Date date, int months) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.MONTH, months);
return cal.getTime();
}
// 判斷是否同一天
public static boolean isSameDay(Date d1, Date d2) {
Calendar c1 = Calendar.getInstance(), c2 = Calendar.getInstance();
c1.setTime(d1); c2.setTime(d2);
return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)
&& c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
}
// 格式化輸出
public static String format(Date date, String pattern) {
return new SimpleDateFormat(pattern).format(date);
}
// 解析字串
public static Date parse(String str, String pattern) throws ParseException {
return new SimpleDateFormat(pattern).parse(str);
}
}
常見坑點避雷
- 月份 0 基:
Calendar.JANUARY = 0,設置月份時 必須 -1,獲取時 必須 +1 顯示給用戶
- 可變性:
Calendar 可變,多線程共享會導致數據競爭,每次使用 new 實例 或用 ThreadLocal
- lenient 陷阱:預設寬容模式下
set(2024, 12, 32) 不報錯,實際變為 2025-02-01,生產環境關閉寬容模式
- 時區陷阱:
getTime() 返回 Date 無時區信息,跨時區業務必須用 ZonedDateTime 或攜帶時區上下文
- 性能:
Calendar 重量級,高頻調用建議遷移 java.time 或緩存 DateTimeFormatter
遷移指南:Calendar → java.time
// 舊代碼
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.SEPTEMBER, 15);
cal.add(Calendar.DAY_OF_MONTH, 10);
Date result = cal.getTime();
// 新代碼
LocalDate date = LocalDate.of(2024, 9, 15);
LocalDate result = date.plusDays(10);
// 如需 Date
Date legacy = Date.from(result.atStartOfDay(ZoneId.systemDefault()).toInstant());
總結
Calendar 作為 Java 早期日期時間 API,核心在於「單一時間戳 + 17字段延遲計算 + 子類實現具體日曆系統」。掌握 get/set/add/roll 四大核心操作、ActualMaximum 實際邊界值、寬容模式與時區影響,即可應對 90% 維護場景。新代碼請全面遷移至 Java 8 java.time (LocalDate/LocalDateTime/ZonedDateTime),獲得不可變性、線程安全、清晰 API 與更好性能。維護舊代碼時,封裝 CalendarUtils 統一入口,關閉寬容模式,每次使用創建新實例,嚴防併發與邊界 Bug。