Calendar.add()会自动进位/借位并传播溢出,而set()和roll()不会;必须调用getTime()获取结果;现代Java应优先使用java.time API。

Calendar.add() 是最常用也最容易出错的日期计算方式
直接修改 Calendar 实例的字段(如 set())不会触发自动进位或借位,而 add() 会——比如给 1 月 31 日加 1 个月,结果是 2 月 28/29 日(取决于闰年),不是 2 月 31 日报错。但很多人误以为它和 roll() 一样只影响当前字段。
-
add()会“溢出传播”:加天数可能影响月、年;加月可能影响年;加小时可能影响日 -
roll()不传播:对 1 月 31 日roll(Calendar.MONTH, 1),结果仍是 1 月 28/29/30/31 日(只在 1 月内滚动) - 必须调用
getTime()获取计算后的Date,否则字段变更未生效(Calendar是可变对象,但内部状态需显式提取)
Calendar cal = Calendar.getInstance(); cal.set(2023, Calendar.JANUARY, 31); // 注意:月份从 0 开始 cal.add(Calendar.MONTH, 1); // 得到 2023-02-28 Date result = cal.getTime(); // 必须调用!
为什么 new GregorianCalendar().add() 后 getTime() 还是旧时间?
常见错误是重复使用同一个 Calendar 实例却忘记重置或未正确初始化。更隐蔽的问题是时区和默认 Locale 导致的隐式行为差异——比如在某些 JVM 环境下,getInstance() 返回的 GregorianCalendar 可能启用 lenient 模式,允许非法日期临时存在,直到你调用 getTime() 才真正校正。
- 默认
lenient = true:设 2023-02-30 会被静默转为 2023-03-02,不报错但结果意外 - 设
cal.setLenient(false)后,非法日期设置会立即抛IllegalArgumentException - 构造后立刻
clear()再set(),避免残留字段干扰(尤其复用实例时)
Calendar 计算跨月/跨年时的时区陷阱
Calendar 的所有计算都基于其内部 TimeZone,但 add() 对小时、分钟的操作受 DST(夏令时)影响。例如在 CEST 时区(UTC+2),3 月最后一个周日凌晨 2:00 会跳到 3:00,此时对那个时刻加 1 小时,结果不是 3:00 而是 4:00(跳过了不存在的 3:00–4:00 区间)。
- 用
cal.getTimeZone().getOffset(cal.getTimeInMillis())查当前毫秒值对应的时区偏移 - 跨 DST 边界计算建议先转成 UTC 时间(用
SimpleDateFormat配合TimeZone.getTimeZone("UTC"))再操作 - 避免用
add(Calendar.HOUR, 24)代替add(Calendar.DAY_OF_MONTH, 1):前者受 DST 影响,后者按日历日推进
替代方案:Java 8+ 应该优先用 LocalDate / LocalDateTime
Calendar 是遗留 API,线程不安全、设计反直觉、时区处理隐晦。现代代码中,除非维护老系统或对接旧接口,否则应直接用 java.time 类型。
立即学习“Java免费学习笔记(深入)”;
-
LocalDate.plusMonths(1)行为明确:1 月 31 日 → 2 月 28 日(非异常) -
ZonedDateTime.withEarlierOffsetAtOverlap()显式控制 DST 重叠行为 -
Duration和Period分离了“时间量”与“日历量”,避免混淆
LocalDate date = LocalDate.of(2023, 1, 31);
LocalDate nextMonth = date.plusMonths(1); // 2023-02-28
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Europe/Berlin"));
ZonedDateTime tomorrow = zdt.plusDays(1); // 自动处理 DST
用 Calendar 做跨月加减时,务必确认 lenient 模式和时区设置;如果逻辑涉及 DST、长期调度或需要不可变语义,java.time 不是“升级选项”,而是唯一合理选择。










