Date 本质是无时区的毫秒数,toString() 因隐式使用 JVM 默认时区而显示有时区;Calendar.set() 延迟计算,需 getTime() 强制触发;SimpleDateFormat 非线程安全,应避免 static 共享。

Date 和 Calendar 是 Java 早期(JDK 1.0 / 1.1)引入的时间类,不推荐在新项目中使用——它们线程不安全、设计混乱、API 反直觉,且已被 java.time 包(Java 8+)全面取代。
但如果你必须维护旧代码、对接遗留系统,或面试被问到,下面这些实操要点能帮你避开高频翻车点。
为什么 new Date() 返回的是“当前时间”,但 toString() 却带时区?
Date 本质只是一个毫秒数(自 1970-01-01T00:00:00Z 起的 long),它**不包含时区信息**。但 Date.toString() 会隐式调用 JVM 默认时区(如 Asia/Shanghai)格式化输出,造成“Date 有时区”的错觉。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 永远别依赖
date.toString()做逻辑判断或日志解析 - 需要格式化显示时,用
SimpleDateFormat显式指定时区(注意:它也不是线程安全的) - 跨系统传时间,优先用毫秒值
date.getTime()或 ISO 8601 字符串(如"2024-05-20T14:30:00Z")
Calendar.getInstance() 返回的对象,set() 后为什么没立即生效?
Calendar 是可变对象,set() 只是标记字段已修改,真正计算时间戳要等第一次调用 get()、getTime() 或 add() —— 这叫“懒计算”。中间若再 set() 其他字段,可能触发冲突校验(比如 2 月设了 30 日)。
常见错误现象:
-
cal.set(Calendar.MONTH, 12)→ 实际变成下一年 1 月(MONTH从 0 开始,12 是非法值,自动进位) -
cal.set(2024, 1, 30)后立刻cal.get(Calendar.DATE)→ 可能返回 2 或 3(因 2 月无 30 日,自动归整) - 多线程共用同一个
Calendar实例 → 结果不可预测
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 每次使用前调用
Calendar.getInstance()新建实例,别复用 - 设完关键字段后,立刻调用
cal.getTime()强制计算,避免后续 get() 出意外 - 避免混合使用
set(int field, int value)和set(year, month, date)—— 后者会重置小时/分/秒为 0
如何安全地做“日期加减”而不踩 Calendar 的陷阱?
用 add() 比 roll() 更可靠:add() 会处理进位(如 1 月 31 日 + 1 月 → 2 月 28/29 日),roll() 只在本字段内循环(1 月 31 日 roll +1 月 → 1 月 31 日,没变)。
但 add() 仍有坑:
-
cal.add(Calendar.MONTH, 1)不等于“加 30 天”,而是按日历月逻辑(可能加 28~31 天) - 跨夏令时切换时,
add(Calendar.HOUR, 1)可能跳过或重复 1 小时(取决于时区规则) - 对
Calendar做多次add()后,再set()某个字段,可能覆盖之前计算结果
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 如果业务明确要“加 N 天”,直接操作毫秒值:
new Date(date.getTime() + N * 24L * 3600_000) - 如果必须用日历单位(如“加 1 个月”),用
add(),但之后立刻getTime()获取结果并丢弃原Calendar - 涉及跨月/跨年计算,务必用单元测试覆盖边界:1 月 31 日 +1 月、2 月 29 日 +1 年等
和 SimpleDateFormat 配合使用时,最常被忽略的线程安全问题
SimpleDateFormat 是典型的“伪线程安全”陷阱:它内部缓存了 Calendar 实例,多个线程并发调用 format() 或 parse() 会导致状态错乱,典型表现是解析出错日期(如 "2024-01-01" 解成 2023 年)、格式化丢失时区、甚至抛 NumberFormatException。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 绝对不要把
SimpleDateFormat设为 static 全局变量 - 方案一:每次用都新建(适合低频场景):
new SimpleDateFormat("yyyy-MM-dd") - 方案二:用
ThreadLocal管理(推荐):private static final ThreadLocal
SDF = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); - 方案三:升级到
DateTimeFormatter(Java 8+),它是不可变且线程安全的
旧 API 的复杂性不在语法,而在隐式状态、模糊契约和时区纠缠。哪怕只是读一段老代码,也要先确认:它用的是毫秒值比较,还是字符串比较?Calendar 实例是否被共享?SimpleDateFormat 在哪初始化的?这些细节比“怎么写”更容易决定程序是否稳定。










