日期时间API
Java 8 引入 java.time 包,用于替代老的 Date、Calendar 和 SimpleDateFormat。新时间 API 不可变、线程安全、语义清晰,是现代 Java 项目处理日期时间的首选。
# 1. 老 API 的问题
| 老 API | 问题 |
|---|---|
Date | 名称像日期,实际表示时间点;很多方法已废弃 |
Calendar | 可变、API 繁琐 |
SimpleDateFormat | 非线程安全 |
错误示例:
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");
多线程共享 SimpleDateFormat 可能解析错乱。
# 2. 核心类型
| 类型 | 含义 |
|---|---|
LocalDate | 日期,不含时间和时区 |
LocalTime | 时间,不含日期和时区 |
LocalDateTime | 日期时间,不含时区 |
Instant | 时间线上的瞬时点,适合机器时间 |
ZonedDateTime | 带时区日期时间 |
Duration | 时间量,偏秒和纳秒 |
Period | 日期量,偏年月日 |
DateTimeFormatter | 格式化和解析 |
选择:
只要日期 -> LocalDate
业务日期时间 -> LocalDateTime
跨时区/绝对时间点 -> Instant / ZonedDateTime
计算耗时 -> Duration
计算年月日差 -> Period
# 3. LocalDate
LocalDate today = LocalDate.now();
LocalDate date = LocalDate.of(2026, 6, 25);
常用方法:
date.plusDays(1);
date.minusMonths(1);
date.getDayOfWeek();
date.isAfter(LocalDate.now());
LocalDate 不可变,调用 plusDays 会返回新对象。
# 4. LocalDateTime
LocalDateTime now = LocalDateTime.now();
LocalDateTime time = LocalDateTime.of(2026, 6, 25, 10, 30);
适合表示业务上的本地日期时间,例如订单创建时间。但如果涉及跨时区传输,必须明确时区。
# 5. Instant
Instant 表示 UTC 时间线上的瞬时点。
Instant now = Instant.now();
适合:
- 存储事件发生时间。
- 日志时间。
- 跨系统传输。
- 与时间戳互转。
long millis = Instant.now().toEpochMilli();
Instant instant = Instant.ofEpochMilli(millis);
# 6. 时区
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZonedDateTime zoned = ZonedDateTime.now(shanghai);
LocalDateTime 不包含时区,不能代表全球唯一时间点。
2026-06-25 10:00
├─ Asia/Shanghai
└─ America/New_York
代表不同瞬时点
跨系统接口建议传递 ISO-8601 字符串或时间戳,并明确时区语义。
# 7. 格式化与解析
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String text = LocalDateTime.now().format(formatter);
LocalDateTime parsed = LocalDateTime.parse(text, formatter);
DateTimeFormatter 是线程安全的,可以定义为常量。
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
# 8. Duration 与 Period
Duration 表示时间长度:
Duration duration = Duration.between(startInstant, endInstant);
long seconds = duration.toSeconds();
Period 表示日期间隔:
Period period = Period.between(startDate, endDate);
int months = period.getMonths();
区别:
| 类型 | 适合 |
|---|---|
Duration | 秒、毫秒、纳秒级耗时 |
Period | 年、月、日日期差 |
# 9. 与 Date 转换
Date date = Date.from(instant);
Instant instant = date.toInstant();
与 LocalDateTime 转换需要时区:
LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
# 10. 常见坑
- 把
LocalDateTime当成绝对时间点跨时区传输。 - 日期格式使用
YYYY而不是yyyy。 - 多线程共享
SimpleDateFormat。 - 数据库存储时区语义不明确。
- 计算自然月差时误用固定天数。
YYYY 是 week-based-year,跨年周可能出错。普通年份使用 yyyy。
# 11. 时间建模与存储建议
时间问题的难点不在 API,而在语义。建模前先问清楚:这个值表示“日历日期”“本地业务时间”,还是“全球唯一瞬时点”。
| 业务语义 | 推荐类型 | 示例 |
|---|---|---|
| 生日、账单日 | LocalDate | 1990-01-01 |
| 门店营业时间 | LocalTime | 09:00 |
| 用户填写的预约时间 | LocalDateTime + 业务时区 | 2026-06-25 10:00 |
| 事件发生时间 | Instant | 统一时间线 |
| 跨地区会议 | ZonedDateTime | 带时区规则 |
| 接口传输 | ISO-8601 或 epoch millis | 明确时区语义 |
用户输入本地时间
│ 携带业务时区
▼
ZonedDateTime
│ 转换为统一时间线
▼
Instant
│ 存储/传输
▼
按展示时区格式化
数据库字段也要有约定:如果存时间戳,要明确是 UTC 还是数据库会话时区;如果存字符串,要明确格式和时区;如果存日期,不要额外拼接午夜时间点来冒充日期。
时间 API 使用原则:
- 机器记录和跨系统传输优先
Instant。 - 人类日历概念优先
LocalDate、LocalTime。 - 涉及时区规则用
ZonedDateTime,不要只保存偏移量。 - 格式化器可复用,
SimpleDateFormat不要多线程共享。 - 测试时间逻辑时使用
Clock注入当前时间,避免测试依赖真实系统时间。
# Tips 快问快答
Q:现代 Java 日期时间首选哪个包?
A:java.time。
Q:LocalDateTime 有时区吗? A:没有。
Q:跨系统传输时间推荐什么?
A:使用 Instant、时间戳或明确带时区的 ISO-8601 字符串。
Q:DateTimeFormatter 线程安全吗? A:是,可以作为静态常量。
Q:SimpleDateFormat 线程安全吗? A:不安全。
Q:Duration 和 Period 区别是什么? A:Duration 表示时间量,Period 表示年月日日期量。
Q:为什么 YYYY 有坑?
A:它是基于周的年份,普通日期格式应使用 yyyy。
Q:LocalDate 适合表示什么? A:只包含日期的业务值,例如生日、账单日。