Java 的时间类(中):JDK 8 全新日期时间 API(java.time 包)详解
这是目前(2026年)Java 项目中最推荐、最主流的时间处理方式。
旧的 java.util.Date + Calendar + SimpleDateFormat 组合已被严重不推荐(线程不安全、可变、设计混乱、月份从0开始等坑)。
JDK 8 引入的 java.time 包(受 Joda-Time 启发,由 Stephen Colebourne 主导设计)彻底解决了这些问题。
核心设计理念(必须记住)
- 不可变(Immutable) → 线程安全,天生可并发
- 职责单一 → 日期、时间、带时区、时间戳、时间段各自独立类
- 人类友好 → 月份从 1 开始,星期用枚举,周一是一周第一天(ISO标准)
- 链式调用 → plusXxx / minusXxx / withXxx 方法返回新对象
- 清晰命名 → now()、of()、parse()、format() 等方法语义明确
java.time 包主要类速览(分类记忆)
| 分类 | 主要类 | 是否带时区 | 是否带时间 | 是否带日期 | 典型用途 | 是否可做键(Map/Set) |
|---|---|---|---|---|---|---|
| 本地日期 | LocalDate | × | × | ✓ | 生日、账单日、开业日期 | 是 |
| 本地时间 | LocalTime | × | ✓ | × | 上班打卡、下班时间、电影开始时间 | 是 |
| 本地日期时间 | LocalDateTime | × | ✓ | ✓ | 订单创建时间、日志时间(最常用) | 是 |
| 带时区日期时间 | ZonedDateTime / OffsetDateTime | ✓ | ✓ | ✓ | 国际会议、跨时区航班、用户所在时区 | 是 |
| 瞬时(时间戳) | Instant | UTC | ✓ | — | 机器时间戳、事件发生瞬间、缓存过期 | 是 |
| 时间段(精确) | Duration | — | — | — | 两个时间点之间精确差(秒+纳秒) | — |
| 日期段(人类) | Period | — | — | — | 年/月/日差(生日还差几个月) | — |
| 格式化 | DateTimeFormatter | — | — | — | 自定义格式化 & 解析 | — |
1. 获取当前时间(最常用几种写法)
LocalDate today = LocalDate.now(); // 2026-03-22
LocalTime nowTime = LocalTime.now(); // 02:10:45.123456789
LocalDateTime now = LocalDateTime.now(); // 2026-03-22T02:10:45.123456789
ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
Instant currentUtc = Instant.now(); // UTC 时间戳
2. 显式创建(of / parse)
// 推荐方式:of()
LocalDate birth = LocalDate.of(1995, 10, 8); // 1995-10-08
LocalDateTime meet = LocalDateTime.of(2026, 3, 22, 14, 30);
LocalDateTime meet2 = LocalDateTime.of(2026, Month.MARCH, 22, 14, 30);
// 从字符串解析(最安全方式)
LocalDate d1 = LocalDate.parse("2026-03-22");
LocalDateTime dt = LocalDateTime.parse("2026-03-22T14:30:00");
// 自定义格式解析
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");
LocalDateTime custom = LocalDateTime.parse("2026年03月22日 14:30", fmt);
3. 常用运算(加减、调整)
所有运算都返回新对象(不可变)
LocalDateTime now = LocalDateTime.now();
// 加减
LocalDateTime tomorrow = now.plusDays(1);
LocalDateTime nextWeek = now.plusWeeks(1);
LocalDateTime nextMonth = now.plusMonths(1);
LocalDateTime twoHoursAgo = now.minusHours(2);
// 更精确控制
LocalDateTime future = now.plus(3, ChronoUnit.MONTHS)
.plus(15, ChronoUnit.MINUTES);
// 调整到特定值(with 开头)
LocalDateTime startOfDay = now.withHour(0).withMinute(0).withSecond(0).withNano(0);
LocalDate firstDayOfMonth = now.toLocalDate().with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDayOfMonth = now.toLocalDate().with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextMonday = now.toLocalDate().with(TemporalAdjusters.next(DayOfWeek.MONDAY));
4. 时间间隔计算(Duration & Period)
// 精确到纳秒(机器友好)
LocalDateTime start = LocalDateTime.of(2026, 3, 22, 10, 0);
LocalDateTime end = LocalDateTime.of(2026, 3, 22, 12, 45, 30);
Duration duration = Duration.between(start, end);
System.out.println(duration.toHours()); // 2
System.out.println(duration.toMinutes()); // 165
System.out.println(duration.getSeconds()); // 9930
// 年/月/日(人类友好)
LocalDate birth = LocalDate.of(1995, 10, 8);
Period age = Period.between(birth, LocalDate.now());
System.out.println("年龄:" + age.getYears() + "年 " + age.getMonths() + "月 " + age.getDays() + "天");
5. 时区处理(最重要场景之一)
// 当前时区
ZonedDateTime beijingNow = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 转换时区
ZonedDateTime tokyo = beijingNow.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
// UTC 时间戳 ↔ 本地时间
Instant utc = Instant.now();
ZonedDateTime local = utc.atZone(ZoneId.systemDefault());
// 常见时区ID(不要硬编码字符串,推荐用 ZoneId.systemDefault() 或配置文件)
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZoneId utcZone = ZoneId.of("UTC");
6. 格式化与解析(DateTimeFormatter)
DateTimeFormatter isoDate = DateTimeFormatter.ISO_DATE; // 2026-03-22
DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME; // 2026-03-22T02:10:45.123456789
// 自定义(线程安全!可以做 static final 常量)
DateTimeFormatter custom = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String str = now.format(custom); // "2026-03-22 02:10:45"
LocalDateTime parsed = LocalDateTime.parse("2026-03-22 02:10:45", custom);
7. 常见坑 & 最佳实践(2026 年视角)
| 场景 | 错误写法 | 正确写法 / 推荐做法 |
|---|---|---|
| 格式化器复用 | new SimpleDateFormat() 多线程用 | DateTimeFormatter 做 static final 常量 |
| 生日/有效期判断 | 用 Calendar 或 Date 比较 | 用 LocalDate.isBefore() / isAfter() / isEqual() |
| 跨时区订单时间存储 | 存 LocalDateTime | 存 Instant 或 ZonedDateTime(推荐后者带时区信息) |
| 计算两个时间相差多少工作日 | 自己写循环 | 用 ChronoUnit.DAYS.between() + TemporalAdjusters |
| JSON 序列化 | 默认用 toString() | 用 Jackson 的 JavaTimeModule |
| 数据库存储 | 存 Timestamp / Date | 推荐存 Instant 或 OffsetDateTime |
8. 快速对照表(面试/日常速查)
需求 → 推荐类
纯日期(生日、假期) → LocalDate
纯时间(每天固定时间) → LocalTime
日期+时间(日志、订单) → LocalDateTime(无时区需求时最常用)
跨系统/跨时区事件 → Instant 或 ZonedDateTime
两个时间点精确间隔 → Duration
两个日期之间年月日差 → Period
需要调整到月初/月末/周一 → TemporalAdjusters
你现在是想重点练习哪一块?
- 常见面试题(生日判断、跨年计算、时区转换等)
- 实际业务代码示例(订单超时判断、排班系统、国际会议时间)
- 与旧 Date/Calendar 的转换写法
- Spring Boot + Jackson 如何优雅序列化
告诉我,我可以继续给出针对性更强的代码和说明。