calendar.set 是直接赋值并触发自动归整,calendar.add 是智能边界处理的相对位移;推荐 java 8+ 使用不可变的 localdatetime 替代。

Calendar.set 是直接赋值,不考虑当前日期上下文
用 set 就像拧螺丝——你指定哪一位(年/月/日),它就硬塞进去,不管其他字段是否合理。比如当前是 2023-01-31,调用 calendar.set(Calendar.MONTH, 1)(即二月),结果不是 2023-02-31(不存在),而是自动滚到 2023-03-03。这不是 bug,是它的设计逻辑:先改字段,再重算整个日期。
- 适合场景:初始化一个已知确定的日期,比如设成“2025-12-25”,且你确定所有字段合法
- 常见错误:在已有日期基础上只改月份或日,却没意识到会触发自动归整(roll-over)
- 注意
Calendar.MONTH是从 0 开始的,set(Calendar.MONTH, 1)是二月,不是一月 - 如果只想改某一位而不影响其他(比如只把日改成 15,不管当月有没有这一天),得先
clear()再set(),否则残留的时分秒等字段可能干扰结果
Calendar.add 是相对位移,会智能处理边界
add 更像拨动日历翻页:加一个月,就真往未来跳一个自然月;减一天,就回到昨天。它会主动处理大小月、闰年、时区偏移,结果总是有效日期。
- 适合场景:计算“三天后”“下个月第一天”“一年前同一天”这类相对时间
- 性能上比
set+getTime()+setTime()更轻量,因为不涉及中间 Date 对象转换 - 注意:对
Calendar.DAY_OF_MONTH加负数时,不会回退到上月的“第 X 天”,而是按真实天数倒推,比如 3 月 1 日减 1 天 = 2 月 28 日(非闰年) - 和
roll()不同,add()会进位/借位(比如 1 月 31 日加一个月 → 3 月 3 日),而roll()只在本字段内循环(1 月 31 日 roll 月 → 2 月 31 日 → 无效,但实际行为是 2 月 28 日)
为什么 set 后 getTime() 有时不准?
因为 Calendar 是懒计算的:你调 set() 并不立刻更新内部毫秒值,直到第一次调 getTime() 或 get() 才真正归整并计算。这中间如果多次 set,只有最后一次生效,前面的会被覆盖。
- 典型现象:连续调
set(Calendar.YEAR, 2020)和set(Calendar.MONTH, 0),你以为设了 2020-01,但如果中间穿插了get(Calendar.DAY_OF_MONTH),可能拿到的是旧值归整后的日(比如原先是 2023-01-31,设年为 2020 后还没计算,get 日仍返回 31,再设月为 0,最终才统一归整成 2020-01-31) - 解决办法:要么一次性
set所有字段,要么在关键点手动调一次calendar.getTimeInMillis()强制刷新 - 更稳妥的做法是:用完
set立即getTime()拿结果,别依赖后续多次set的叠加效果
替代方案:LocalDateTime 更可靠
如果你用的是 Java 8+,Calendar 其实早该被替换了。它可变、线程不安全、字段语义混乱(DAY_OF_YEAR 和 DATE 容易混),而 LocalDateTime 是不可变、语义清晰、API 直观。
立即学习“Java免费学习笔记(深入)”;
-
localDateTime.withYear(2025)是纯赋值,不会滚到下月 -
localDateTime.plusMonths(1)是位移,自动处理月末逻辑(2023-01-31.plusMonths(1) → 2023-02-28) - 没有
set和add的语义混淆,也没有懒计算陷阱 - 唯一要注意的是:它不带时区,如果业务涉及跨时区计算,得用
ZonedDateTime
老项目里还绕不开 Calendar,但新代码里,能不用就别碰——尤其当你要做“今天加 7 天”这种简单事,一行 LocalDateTime.now().plusDays(7) 比三行 Calendar 安全得多。










