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

Java Calendar API 完全指南:核心原理、17 個欄位詳解、日期運算與線程安全替代方案

     編輯:Java 相關知識

java.util.Calendar 是 Java 1.1 時代引入的日期時間抽象,雖然 Java 8+ 推出 java.time (JSR-310) 作為現代替代,但大量存量系統、Android 平台、第三方庫仍廣泛使用 Calendar。本文系統梳理其核心架構、17 個欄位語義、關鍵 API、常見陷阱與遷移建議。

一、核心架構設計

工廠模式獲取實例

// 1. 默認 Locale/TimeZone → GregorianCalendar (公曆)
Calendar cal = Calendar.getInstance();

// 2. 指定時區/地區 → 可能返回 JapaneseImperialCalendar, BuddhistCalendar 等
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"), Locale.JAPAN);

內部雙重狀態機制

  • 時間戳:protected long time — 毫秒級絕對時間 (自 1970-01-01 UTC)。
  • 欄位快取:protected int[] fields — 17 個日曆欄位 (YEAR, MONTH...)。
  • 狀態標記:isTimeSet, areFieldsSet, areAllFieldsSet、stamp[] (修改時間戳)。

核心不變量:time 與 fields[] 雙向同步。get() 觸發 complete() → computeFields() (time → fields);set() 修改 fields[] 並標記 isTimeSet=false,下次 getTime() 觸發 updateTime() → computeTime() (fields → time)。

二、17 個欄位完整語義表

常量索引含義取值範圍/說明
ERA0紀元0=BC (公元前), 1=AD (公元)
YEAR1年如 2024
MONTH2月 (0 基)0=JANUARY ... 11=DECEMBER, 12=UNDECIMBER (希伯來曆)
WEEK_OF_YEAR3年中第幾週1~53,依 setFirstDayOfWeek/setMinimalDaysInFirstWeek
WEEK_OF_MONTH4月內第幾週1~6
DATE / DAY_OF_MONTH5月內第幾天1~31
DAY_OF_YEAR6年中第幾天1~366
DAY_OF_WEEK7星期幾1=SUNDAY ... 7=SATURDAY
DAY_OF_WEEK_IN_MONTH8月中第幾個該星期幾1~5 (如「第 2 個週三」)
AM_PM9上午/下午0=AM, 1=PM
HOUR1012 小時制時0~11 (0 代表 12 點)
HOUR_OF_DAY1124 小時制時0~23
MINUTE12分0~59
SECOND13秒0~59
MILLISECOND14毫秒0~999
ZONE_OFFSET15時區偏移 (毫秒)不含夏令時,如 GMT+8 = 28800000
DST_OFFSET16夏令時偏移 (毫秒)通常 0 或 3600000

三、關鍵 API 分類詳解

1. 獲取/設置欄位

int year = cal.get(Calendar.YEAR);           // 獲取
cal.set(Calendar.YEAR, 2024);                // 設置單欄位
cal.set(2024, Calendar.JUNE, 15);            // 設置 年月日
cal.set(2024, Calendar.JUNE, 15, 14, 30, 0); // 設置 年月日時分秒

2. 日期運算:add vs roll 核心差異

方法行為影響大欄位典型場景
add(field, amount)增加/減少指定欄位,**自動進位/借位**是「3 個月後」、「10 天前」、「明年這天」
roll(field, amount)在欄位最小/最大值間**循環滾動**,**不影響大欄位**否「下一個月」(年份不變)、「下一小時」(日期不變)
Calendar cal = Calendar.getInstance();
cal.set(2024, Calendar.JUNE, 15); // 2024-06-15

cal.add(Calendar.MONTH, 3);   // 2024-09-15 (年份可能變)
cal.roll(Calendar.MONTH, 3);  // 2024-09-15 (年份不變,若 12 月 roll +1 → 1 月同年)

cal.add(Calendar.MONTH, -10); // 2023-08-15 (年份變)
cal.roll(Calendar.MONTH, -10); // 2024-08-15 (年份不變!)

3. 邊界查詢

int maxDay = cal.getActualMaximum(Calendar.DATE);      // 當月實際天數 (28/29/30/31)
int minDay = cal.getActualMinimum(Calendar.DATE);      // 總是 1
int maxMonth = cal.getMaximum(Calendar.MONTH);         // 11 (DECEMBER) - 全年最大
int actualMaxMonth = cal.getActualMaximum(Calendar.MONTH); // 視當年曆法,通常也是 11

4. 寬容/非寬容模式

cal.setLenient(true);   // 默認:允許月份 13 → 自動進位到明年 1 月
cal.setLenient(false);  // 嚴格:月份 13 → get() 時拋 IllegalArgumentException
// 非寬容模式下,set() 不檢查,get()/getTime()/add()/roll() 才檢查拋異常

5. 日期比較

Calendar c1 = ..., c2 = ...;
boolean before = c1.before(c2);
boolean after = c1.after(c2);
boolean equals = c1.equals(c2); // 比較 time + 時區 + 首周設置等所有狀態
int cmp = c1.compareTo(c2);     // -1/0/1

6. 時區與毫秒互轉

cal.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = cal.getTime();              // Calendar → Date
cal.setTime(new Date());                // Date → Calendar
long ms = cal.getTimeInMillis();        // Calendar → 毫秒時間戳
cal.setTimeInMillis(System.currentTimeMillis()); // 毫秒 → Calendar

四、常見陷阱與避坑指南

陷阱現象根因修正
MONTH 0 基設 6 月傳 6 → 實際 7 月JANUARY=0用常量 Calendar.JUNE 或 month-1
WEEK_OF_YEAR 邊界1 月 1 日屬於上一年第 52/53 週首周定義配置 setFirstDayOfWeek/setMinimalDaysInFirstWeek
可變性共享多線程共享 Calendar 實例數據錯亂非線程安全**每線程獨立實例** 或用 ThreadLocal,或遷移 java.time
set() 不即時生效set 後立即 get 其他欄位未更新惰性計算調用 getTime() 或 getTimeInMillis() 強制同步
ZONE_OFFSET/DST_OFFSET 不可 rollroll 這兩欄位拋異常設計限制用 setTimeZone() 管理時區

五、自定義工具類封裝 (生產就緒)

public final class Calendars {
    private Calendars() {}

    public static int year(Calendar c) { return c.get(Calendar.YEAR); }
    public static int month(Calendar c) { return c.get(Calendar.MONTH); } // 0-based
    public static int month1(Calendar c) { return month(c) + 1; } // 1-based
    public static int day(Calendar c) { return c.get(Calendar.DATE); }
    public static int dayOfWeek(Calendar c) { return c.get(Calendar.DAY_OF_WEEK); } // 1=Sun

    public static int daysInMonth(Calendar c) { return c.getActualMaximum(Calendar.DATE); }
    public static int daysInPrevMonth(Calendar c) {
        Calendar tmp = (Calendar) c.clone();
        tmp.add(Calendar.MONTH, -1);
        return tmp.getActualMaximum(Calendar.DATE);
    }
    public static int firstDayOfWeek(Calendar c) {
        Calendar tmp = (Calendar) c.clone();
        tmp.set(Calendar.DATE, 1);
        return tmp.get(Calendar.DAY_OF_WEEK);
    }
    public static int lastDayOfWeek(Calendar c) {
        Calendar tmp = (Calendar) c.clone();
        tmp.set(Calendar.DATE, 1);
        tmp.roll(Calendar.DATE, -1);
        return tmp.get(Calendar.DAY_OF_WEEK);
    }
    // ... 更多常用封裝
}

六、遷移到 java.time (JSR-310) 的對照表

Calendarjava.time (Java 8+)優勢
Calendar.getInstance()LocalDate.now() / ZonedDateTime.now()不可變、線程安全、領域模型清晰
cal.set(y, m, d)LocalDate.of(y, m+1, d)月份 1-based,符合直覺
cal.add(MONTH, 3)date.plusMonths(3)流式 API、不可變返回新對象
cal.getTimeInMillis()zonedDateTime.toInstant().toEpochMilli()明確時區語義
cal.setTimeZone(tz)zonedDateTime.withZoneSameInstant(ZoneId)不可變轉換
SimpleDateFormatDateTimeFormatter線程安全、預定義格式豐富

七、總結

  1. Calendar 核心是 time (long) ↔ fields[17] (int[]) 雙向同步,理解這點可解決 90% 困惑。
  2. MONTH 0-based、WEEK_OF_YEAR 依賴首周配置、add/roll 進位差異 是三大核心認知點。
  3. 非線程安全、可變性 是最大架構缺陷,新專案**強制使用 java.time**。
  4. 存量系統維護時,封裝工具類、嚴格單線程使用、遷移邊界隔離。

Calendar 是 Java 日期時間處理的「上一代標準」,理解其設計權衡與陷阱,既能維護舊代碼,也能更深刻體會 java.time 不可變、領域驅動設計的優雅。

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