旧版jdk中calendar类存在时区缓存、字段计算不一致、add/roll语义混淆、跨时区转换易出错等陷阱;应显式指定时区、禁用宽松模式、标准化周字段、慎用add与roll、避免date互转时区丢失。

旧版 JDK(如 JDK 7 及更早)没有 java.time 包,Calendar 是当时处理日期偏移、时区、字段计算等逻辑的主力类——但它不是“即用即对”的工具,很多行为必须手动校正。
为什么 new GregorianCalendar() 不等于“当前系统时间”的安全快照
GregorianCalendar 构造时若不显式传入 TimeZone 和 Locale,会使用 JVM 启动时缓存的默认值。而这个默认值可能被其他代码通过 TimeZone.setDefault() 修改过,导致后续创建的实例时区错乱。
- 始终显式传入
TimeZone.getTimeZone("GMT")或TimeZone.getDefault(),避免隐式依赖 - 如果业务要求严格按系统本地时区,用
TimeZone.getDefault().clone()防止被外部修改影响 - 构造后立即调用
calendar.setLenient(false),否则像 “2023-02-30” 这种非法日期会被静默纠正为 2023-03-02,掩盖输入错误
get() 和 set() 的字段陷阱:WEEK_OF_YEAR 和 DAY_OF_WEEK_IN_MONTH
Calendar 的某些字段(如 WEEK_OF_YEAR)依赖 firstDayOfWeek 和 minimalDaysInFirstWeek 设置,不同 Locale 下结果可能完全不同。例如美国地区周从周日开始,而德国从周一,同一日期的 WEEK_OF_YEAR 值可能差 1。
- 不要直接用
calendar.get(Calendar.WEEK_OF_YEAR)做跨区域比较 - 需要标准化时,先调用
calendar.setFirstDayOfWeek(Calendar.MONDAY)和calendar.setMinimalDaysInFirstWeek(4) -
DAY_OF_WEEK_IN_MONTH表示“本月第几个星期几”,但 2 月可能只有 4 个周四,而 3 月有 5 个——用它做循环或判断前,务必先getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH)
add() 和 roll() 的语义差异常被误用
add() 会触发进位/借位(比如在 1 月 31 日加 1 个月 → 2 月 28 日),而 roll() 只在当前单位内滚动(1 月 31 日 roll 月份 → 1 月 31 日不变;roll 天数 → 1 月 30 日)。很多人想“只改月份不改日”,却用了 add(Calendar.MONTH, 1),结果因天数越界被自动调整。
- 要保持日不变地切换月份,优先用
roll(Calendar.MONTH, 1),再检查是否需要手动修正日(如从 1 月 31 日 roll 到 2 月后,get(Calendar.DAY_OF_MONTH)仍是 31,但 2 月没这一天) -
add()适合自然周期推进(如“30 天后”),roll()适合界面微调(如点击“下个月”按钮) - 所有
add()和roll()调用后,建议立刻calendar.getTimeInMillis()检查毫秒值是否符合预期,避免字段状态未刷新
与 Date 互转时的时区丢失风险
Calendar.getTime() 返回的 Date 对象本身不带时区信息,它只是毫秒值;而 Calendar.setTime(Date) 会把该毫秒值按当前 Calendar 的时区重新解释。这在跨时区服务中极易引发“时间漂移”。
- 永远不要用
new Date().getTime()直接塞给另一个时区的Calendar实例 - 跨时区转换,应统一走
calendar.setTimeZone(targetZone); calendar.setTimeInMillis(sourceDate.getTime()) - 调试时打印
calendar.getTimeZone().getID()和calendar.getTimeInMillis(),比只看calendar.getTime()的 toString() 更可靠
最麻烦的不是 API 写法,而是 Calendar 把“时区”“历法规则”“字段计算策略”全耦合在一个实例里,稍不注意,一次 set() 就让后续所有 get() 结果不可信。老项目维护时,宁可多写几行 clone + setTimeZone + setLenient,也不要依赖默认行为。










