1. 为什么需要java.time?
如果你还在用java.util.Date和Calendar处理时间,那就像用算盘计算航天轨道一样过时了。2014年随Java 8发布的java.time包彻底改变了游戏规则——它解决了传统日期API的三大致命伤:
-
线程安全陷阱:老式API中SimpleDateFormat等类不是线程安全的,需要额外同步处理。我在2016年就遇到过生产环境因日期格式化导致的并发问题,当时用ThreadLocal才勉强解决。
-
设计混乱:Date的月份从0开始但日期从1开始,这种反人类设计让无数开发者栽跟头。有次我调试到凌晨3点才发现是因为getMonth()返回11实际是12月。
-
扩展性差:处理时区、闰秒等场景需要自己造轮子。曾有个海外项目需要支持萨摩亚时区(UTC+13→UTC+14),用Calendar实现差点让我崩溃。
2. java.time核心类解析
2.1 不可变对象设计
所有java.time中的类都是不可变的(immutable),这意味着:
- 线程安全无需同步
- 可以放心作为方法参数传递
- 修改操作会返回新对象而非改变原对象
java复制LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusDays(7); // 返回新对象
2.2 关键类分工
| 类名 | 用途 | 示例 |
|---|---|---|
| Instant | 时间戳(UTC时区) | 记录日志时间 |
| LocalDate | 不含时间的日期 | 生日、纪念日 |
| LocalTime | 不含日期的时间 | 营业时间 |
| LocalDateTime | 日期+时间(无时区) | 本地活动时间 |
| ZonedDateTime | 带时区的完整时间 | 跨国会议时间 |
| Period | 日期区间(年/月/日) | 计算年龄 |
| Duration | 时间间隔(纳秒级) | 测量代码执行时间 |
2.3 时区处理实战
处理跨时区业务时,一定要明确:
- 存储用Instant(UTC时间戳)
- 显示用ZonedDateTime
- 转换用ZoneId
java复制// 上海时间转纽约时间
ZonedDateTime shanghaiTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYorkTime = shanghaiTime.withZoneSameInstant(ZoneId.of("America/New_York"));
3. 高频使用场景详解
3.1 日期计算
计算信用卡还款日(账单日后20天):
java复制LocalDate billDate = LocalDate.of(2023, Month.JUNE, 15);
LocalDate dueDate = billDate.plusDays(20);
3.2 时间格式化
注意:DateTimeFormatter也是线程安全的!
java复制DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault());
String formatted = formatter.format(Instant.now());
3.3 时间段检查
判断当前是否在促销时段内:
java复制LocalTime now = LocalTime.now();
LocalTime start = LocalTime.of(9, 30);
LocalTime end = LocalTime.of(21, 0);
if (now.isAfter(start) && now.isBefore(end)) {
System.out.println("促销进行中");
}
4. 性能优化与坑点
4.1 对象复用
频繁创建DateTimeFormatter会影响性能:
java复制// 错误示范:每次调用都新建
public String formatDate(LocalDate date) {
return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
// 正确做法:静态常量
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String formatDate(LocalDate date) {
return date.format(FORMATTER);
}
4.2 夏令时陷阱
处理夏令时切换日期的业务逻辑要特别小心:
java复制// 假设2023年3月12日2:00时钟拨快1小时(美国夏令时开始)
LocalDateTime beforeDst = LocalDateTime.of(2023, 3, 12, 1, 59);
ZonedDateTime zoned = beforeDst.atZone(ZoneId.of("America/New_York"));
ZonedDateTime oneMinLater = zoned.plusMinutes(1);
// 输出:2023-03-12T03:00-04:00[America/New_York]
System.out.println(oneMinLater);
4.3 数据库交互
JPA/Hibernate中推荐这样映射:
java复制@Entity
public class Order {
@Column
private LocalDateTime createTime;
@Column
@Convert(converter = ZoneIdConverter.class)
private ZoneId timeZone;
}
5. 版本兼容方案
5.1 兼容旧系统
与java.util.Date互转:
java复制// Date → Instant
Instant instant = oldDate.toInstant();
// Instant → Date
Date newDate = Date.from(instant);
5.2 Android支持
Android API 26+才原生支持java.time,低版本需要添加ThreeTenABP库:
gradle复制implementation 'com.jakewharton.threetenabp:threetenabp:1.4.0'
使用前需要初始化:
java复制AndroidThreeTen.init(context);
6. 最佳实践建议
- 始终明确时区:在系统入口处统一转换时区,内部统一使用UTC时间
- 避免使用LocalDateTime.now():除非明确需要当前默认时区的时间
- 日志记录用Instant:保证时间戳的全局一致性
- 前端展示格式化:后端返回时间戳,由前端根据用户时区格式化显示
- 测试覆盖特殊日期:闰秒、闰年、月末、夏令时切换等边界情况
我在金融项目中的经验是:所有涉及利息计算的业务必须使用UTC时间,显示时才按用户时区转换。曾经因为时区处理不当导致多计算了一天的利息,损失了数万元。
