很多人第一次接触 Java 操作时间时,都是从Date开始的。那会儿还在学校写作业,能把new Date()打印出来看到一串数字就挺开心的。但等到真进了公司,遇到一点时间计算、时区、格式化的需求……基本上十个里有九个被坑得体无完肤。
1. Date 的设计真的很“古早”
Date类最早是 1995 年跟着 Java 1.0 一起诞生的。那时候没有考虑多线程、国际化、时区差异这些复杂的东西,它的职责又多又杂,设计极其反人类。
比如:
Date date = new Date(2025, 10, 3); // 输出啥你猜?
System.out.println(date);
结果你会发现打印出来的年份比你输入的多了1900年。是的,Date的年份是从 1900 开始算的。传 2025 其实是 3925 年。
还有月份,Date的月份是从 0 开始的。你写 10,实际是 11 月。
这些历史包袱让Date的 API 既不直观,又极容易出错。要不是兼容老项目,现在没人会用它。
2. SimpleDateFormat 线程不安全的“神坑”
最常见的翻车点之一,就是多线程格式化时间。
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
System.out.println(sdf.format(new Date()));
});
}
这段代码在单线程下没问题,但多线程时就会出现各种奇怪的格式,比如日期错乱、时间乱码甚至抛异常。 原因是SimpleDateFormat内部维护了可变状态,多线程共享同一个实例时会互相干扰。
解决办法? 要么每次都新建一个SimpleDateFormat,要么加锁(性能差),要么用ThreadLocal包一层(麻烦)。 所以后来才有了更安全的替代品:DateTimeFormatter。
3. Calendar:从坑里走出来,又掉进了另一个坑
Java 1.1 为了解决Date不够用的问题,引入了Calendar。结果越补越乱。 看下面这段代码:
Calendar calendar = Calendar.getInstance();
calendar.set(2025, 10, 3, 14, 30, 0);
Date date = calendar.getTime();
System.out.println(date);
打印结果依旧是 11 月(月份依旧从 0 开始),而且Calendar的 API 极其臃肿,字段多得离谱:YEAR、MONTH、WEEK_OF_YEAR、DAY_OF_MONTH、HOUR_OF_DAY……每个方法都像雷区,稍不注意就会算错。
再比如,你想让时间加一天:
calendar.add(Calendar.DAY_OF_MONTH, 1);
表面上没问题,但如果这一天跨了月或者跨了年,那行为可能完全不是你想的那样。 更别提它和Date之间频繁转换的麻烦程度,真是一言难尽。
4. Java 8 的救世主:LocalDateTime 系列
直到 Java 8 的java.time包出现,这些问题才算真正解决。LocalDate、LocalTime、LocalDateTime、Instant、ZoneId、ZonedDateTime等类不仅线程安全,而且设计直观,API 语义清晰。
举个例子,同样是获取当前时间并加一天:
LocalDateTime now = LocalDateTime.now();
LocalDateTime tomorrow = now.plusDays(1);
System.out.println("今天:" + now);
System.out.println("明天:" + tomorrow);
就这么简单,而且不会有时区错乱、月份偏移的坑。
如果你要转成时间戳,也很方便:
long timestamp = now.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
System.out.println("当前时间戳:" + timestamp);
格式化和解析也有安全又优雅的写法:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
LocalDateTime parsed = LocalDateTime.parse(formatted, formatter);
System.out.println(formatted);
System.out.println(parsed);
再也不用担心线程安全问题,也不用在脑子里算“月份减一”这种反人类的逻辑。
5. 兼容老项目怎么办?
如果项目里到处都是Date,也不是非得一夜之间改完。 你可以逐步过渡,比如利用Instant进行转换:
// Date -> LocalDateTime
Date date = new Date();
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// LocalDateTime -> Date
Date newDate = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
这样你就可以慢慢替换旧逻辑,而不用一下推翻全部代码。
6. 总结
Date和Calendar是“能跑但不该用”的遗产。 在现代 Java 开发中,时间操作首选java.time,这是线程安全、语义清晰、国际化友好的标准库。
如果你还在用Date来做时间计算、格式化,那真该停下来想一想了。 毕竟,时间问题一旦出错,影响的往往不只是日志,而是系统逻辑、财务计算,甚至跨时区的业务结算。
别等到线上多加一天工资的时候,才意识到这锅原来是Date背的。