Skip to content

Calendar

Calendar 是什么?

Calendar 是 JDK 1.1 引入的抽象类,用来替代 Date 的日期计算能力。它比 Date 稍微好一点,但也好不到哪去——月份依然从 0 开始、非线程安全、设计混乱。

为什么先讲它?因为老项目里可能还在用,了解它才能理解新 API 有多好。

基本使用

创建实例

java
// 使用静态工厂方法
Calendar cal = Calendar.getInstance();
// 返回的是 GregorianCalendar(阳历)

// 手动指定
Calendar cal2 = Calendar.getInstance(TimeZone.getTimeZone("America/New_York"));

常用字段

Calendar 用常量表示各个字段:

java
Calendar cal = Calendar.getInstance();

// 年
cal.get(Calendar.YEAR);        // 2026

// 月(坑:从 0 开始)
cal.get(Calendar.MONTH);       // 2(三月),不是二月
cal.get(Calendar.MARCH);       // 用常量更清晰,但值一样是 2

// 日
cal.get(Calendar.DAY_OF_MONTH); // 22
cal.get(Calendar.DAY_OF_WEEK); // 1(周日)
cal.get(Calendar.DAY_OF_YEAR); // 今天是第几天

// 时分秒
cal.get(Calendar.HOUR_OF_DAY); // 24小时制
cal.get(Calendar.HOUR);        // 12小时制
cal.get(Calendar.MINUTE);
cal.get(Calendar.SECOND);

设置值

java
Calendar cal = Calendar.getInstance();

// 单独设置某个字段
cal.set(Calendar.YEAR, 2026);
cal.set(Calendar.MONTH, Calendar.MARCH); // 用常量,不容易搞错
cal.set(Calendar.DAY_OF_MONTH, 22);

// 一次性设置
cal.set(2026, Calendar.MARCH, 22);

// 带时分秒
cal.set(2026, Calendar.MARCH, 22, 14, 30, 0);

计算日期

java
Calendar cal = Calendar.getInstance();

// 加减日期
cal.add(Calendar.DAY_OF_MONTH, 7);  // 7 天后
cal.add(Calendar.MONTH, -1);        // 1 个月前

// 获取月末
Calendar endOfMonth = Calendar.getInstance();
endOfMonth.set(Calendar.DAY_OF_MONTH,
    endOfMonth.getActualMaximum(Calendar.DAY_OF_MONTH));

// 获取月初
Calendar startOfMonth = Calendar.getInstance();
startOfMonth.set(Calendar.DAY_OF_MONTH, 1);

常用场景

月初和月末

java
// 月初
Calendar start = Calendar.getInstance();
start.set(Calendar.DAY_OF_MONTH, 1);
start.set(Calendar.HOUR_OF_DAY, 0);
start.set(Calendar.MINUTE, 0);
start.set(Calendar.SECOND, 0);
start.set(Calendar.MILLISECOND, 0);

// 月末
Calendar end = Calendar.getInstance();
end.set(Calendar.DAY_OF_MONTH,
    end.getActualMaximum(Calendar.DAY_OF_MONTH));
end.set(Calendar.HOUR_OF_DAY, 23);
end.set(Calendar.MINUTE, 59);
end.set(Calendar.SECOND, 59);
end.set(Calendar.MILLISECOND, 999);

// 转 Date
Date startDate = start.getTime();
Date endDate = end.getTime();

日期间隔

java
public static long daysBetween(Date start, Date end) {
    Calendar c1 = Calendar.getInstance();
    c1.setTime(start);
    c1.set(Calendar.HOUR_OF_DAY, 0);
    c1.set(Calendar.MINUTE, 0);
    c1.set(Calendar.SECOND, 0);
    c1.set(Calendar.MILLISECOND, 0);

    Calendar c2 = Calendar.getInstance();
    c2.setTime(end);
    c2.set(Calendar.HOUR_OF_DAY, 0);
    c2.set(Calendar.MINUTE, 0);
    c2.set(Calendar.SECOND, 0);
    c2.set(Calendar.MILLISECOND, 0);

    return (c2.getTimeInMillis() - c1.getTimeInMillis()) / (1000 * 60 * 60 * 24);
}

判断闰年

java
public static boolean isLeapYear(int year) {
    Calendar cal = Calendar.getInstance();
    cal.set(year, Calendar.DECEMBER, 31);
    return cal.getActualMaximum(Calendar.DAY_OF_YEAR) == 366;
}

线程安全问题

Calendar 是非线程安全的:

java
public class UnsafeCalendar implements Runnable {
    private static Calendar shared = Calendar.getInstance(); // 坑!

    @Override
    public void run() {
        shared.set(Calendar.DAY_OF_MONTH, 22);
        // 并发时会被其他线程覆盖
        System.out.println(shared.getTime());
    }
}

解决方案一:每次创建新实例

java
public Date getNextWeek() {
    Calendar cal = Calendar.getInstance(); // 每个线程用独立的实例
    cal.add(Calendar.DAY_OF_MONTH, 7);
    return cal.getTime();
}

解决方案二:ThreadLocal

java
private static final ThreadLocal<Calendar> calendarThreadLocal =
    ThreadLocal.withInitial(() -> Calendar.getInstance());

public static Calendar getThreadLocalCalendar() {
    return calendarThreadLocal.get();
}

与新 API 的对比

操作CalendarJDK 8+ LocalDate
创建Calendar.getInstance()LocalDate.now()
设置月set(Calendar.MONTH, 2)withMonth(3)
加减add(Calendar.DAY, 7)plusDays(7)
月末getActualMaximum()lengthOfMonth()
线程安全
月份起点0(一月)3(三月)

Calendar 月份从 0 开始是最大的坑:

java
Calendar cal = Calendar.getInstance();

// 这是三月,不是二月!
cal.set(Calendar.MONTH, 2); // 3 月

// 要设置二月应该这样:
cal.set(Calendar.MONTH, Calendar.FEBRUARY); // 用常量
// 或者
cal.set(Calendar.MONTH, 1); // 2 月,但谁记得住?

总结

Calendar 的问题:

  1. 月份从 0 开始:最大的坑,容易出错
  2. 非线程安全:共享实例会出问题
  3. API 混乱:Date 和 Calendar 职责不清
  4. 使用繁琐:计算月末都要绕一圈

建议:新代码直接用 JDK 8+ 的 LocalDate / LocalDateTime,Calendar 只用于维护旧系统。

基于 VitePress 构建