java.time 日期与时间
学完你要能自己回答
- Instant、LocalDateTime、ZonedDateTime 分别表示什么?什么时候该用哪一个?
- Duration 和 Period 有什么区别?跨夏令时加一天和加 24 小时为什么不同?
- 为什么推荐 uuuu 而不是 yyyy?DateTimeFormatter 是线程安全的吗?
类型地图:先选对抽象01
java.time 的设计原则是「让类型表达语义」。选错类型,后面所有转换都会别扭。先判断你要表示的是时间线上的一个点,还是日历上的一个日期。
| 类型 | 表示 | 典型用途 | 是否带时区 |
|---|---|---|---|
Instant | UTC 时间线上的瞬时点 | 日志、事件时间、数据库时间戳 | 隐含 UTC |
LocalDate | 日期(年月日) | 生日、账单日、业务日期 | 否 |
LocalTime | 一天内的时间 | 营业时间、闹钟 | 否 |
LocalDateTime | 日期 + 时间 | 本地场景、表单输入 | 否 |
ZonedDateTime | 带时区的日期时间 | 跨时区调度、DST 处理 | 是 |
OffsetDateTime | 带固定偏移的日期时间 | 协议、序列化(如 ISO 8601) | 偏移量 |
Duration | 基于时间的时间量 | 超时、耗时、秒/纳秒 | — |
Period | 基于日期的时间量 | 年龄、账单周期(年/月/日) | — |
ZoneId / ZoneOffset | 时区规则 / 固定偏移 | 时区转换 | — |
Instant instant = Instant.now(); // 机器时间(UTC)
LocalDate date = LocalDate.now(); // 本地日期
LocalDateTime local = LocalDateTime.now(); // 本地日期时间
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println(instant); // 2026-10-31T...Z
System.out.println(date); // 2026-10-31
System.out.println(zoned); // 2026-10-31T...+08:00[Asia/Shanghai]
创建、解析与格式化02
标准 ISO 格式可以直接 parse / toString;自定义格式用 DateTimeFormatter。formatter 是不可变且线程安全的,可以缓存为静态常量。
LocalDate date = LocalDate.of(2023, 9, 19);
LocalDate parsed = LocalDate.parse("2023-09-19");
LocalDateTime dateTime = LocalDateTime.parse("2023-09-19T10:30:00");
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");
LocalDateTime dt = LocalDateTime.parse("2023-09-19 10:30", fmt);
System.out.println(dt.format(fmt)); // 2023-09-19 10:30
格式化模式里 uuuu 是 proleptic year(无纪元),yyyy 是 year-of-era。对公元后的日期二者结果相同,但 yyyy 在严格解析时还需要 era 信息,容易出现「明明格式对却解析失败」。统一用 uuuu 更稳。
DateTimeFormatter safe = DateTimeFormatter.ofPattern("uuuu-MM-dd");
DateTimeFormatter risky = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println(LocalDate.parse("2024-02-29", safe)); // 2024-02-29
// LocalDate.parse("2024-02-29", risky); // 可能因缺少 era 失败
DateTimeFormatter localized = DateTimeFormatter
.ofPattern("yyyy年M月d日", Locale.CHINA);
System.out.println(LocalDate.of(2024, 1, 5).format(localized)); // 2024年1月5日
DateTimeFormatter线程安全;SimpleDateFormat不是,不要共享。- 格式化/解析时显式指定
Locale,避免服务器默认 Locale 导致月份名不同。 DateTimeFormatter.ISO_INSTANT用于 Instant;ISO_LOCAL_DATE用于 LocalDate。- 严格模式可用
DateTimeFormatterBuilder配置 ResolverStyle。
运算:Duration vs Period,DST 陷阱03
Duration 按时间量计算(秒/纳秒),Period 按日历量计算(年/月/日)。跨夏令时时,「加一天」和「加 24 小时」不是同一件事。
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime start = LocalDate.of(2024, 3, 10).atStartOfDay(ny);
// 2024-03-10T00:00-05:00[America/New_York]
System.out.println(start.plusDays(1));
// 2024-03-11T00:00-04:00[America/New_York]
System.out.println(start.plusHours(24));
// 2024-03-11T01:00-04:00[America/New_York]
System.out.println(java.time.Duration.between(
start.toInstant(), start.plusDays(1).toInstant()).toHours()); // 23
夏令时切换当天只有 23 小时(春季)或 25 小时(秋季)。用 plusDays 保持「同一本地时间」,用 plusHours(24) 保持「经过 24 小时」。业务上通常想要前者。
LocalDate birth = LocalDate.of(1995, 6, 1);
LocalDate today = LocalDate.of(2026, 10, 31);
Period age = Period.between(birth, today);
System.out.println(age); // P31Y4M30D
System.out.println(age.getYears()); // 31
Duration d = Duration.ofMinutes(90);
System.out.println(d.toMinutes()); // 90
System.out.println(d.toHoursPart() + "h " + d.toMinutesPart() + "m"); // 1h 30m
Period.between按日历字段逐项相减,不是总天数;要总天数用ChronoUnit.DAYS.between。Duration可为负;isNegative()/abs()处理方向。Instant适合计算耗时;LocalDateTime相减不考虑时区,跨 DST 会错。- JDK 21 的
Duration.toHoursPart()等拆分方法让格式化更直观。
时区转换:Instant 是桥梁04
跨时区转换的正确姿势是:先把本地时间绑定到某个时区得到 ZonedDateTime,再转成 Instant;另一端从 Instant 加目标时区。不要手工加减小时。
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZoneId newYork = ZoneId.of("America/New_York");
LocalDateTime local = LocalDateTime.of(2026, 10, 31, 9, 0);
ZonedDateTime sh = local.atZone(shanghai);
ZonedDateTime ny = sh.withZoneSameInstant(newYork);
System.out.println(sh); // 2026-10-31T09:00+08:00[Asia/Shanghai]
System.out.println(ny); // 2026-10-30T21:00-04:00[America/New_York]
withZoneSameInstant 保持时间线上的点不变,只换显示时区;withZoneSameLocal 保持本地字段不变,相当于「同一面钟的同一读数」。跨时区调度几乎总是前者。
Instant instant = Instant.parse("2026-10-31T01:00:00Z");
System.out.println(instant.atZone(shanghai)); // 2026-10-31T09:00+08:00[Asia/Shanghai]
System.out.println(instant.atZone(newYork)); // 2026-10-30T21:00-04:00[America/New_York]
DST 重叠时段(秋季回拨)同一本地时间会出现两次。atZone 默认选较早的偏移;需要较晚偏移时用 withLaterOffsetAtOverlap()。春季跳过的时间不存在,atZone 会自动前移到有效时间。
ZoneId ny = ZoneId.of("America/New_York");
LocalDateTime overlap = LocalDateTime.of(2024, 11, 3, 1, 30);
System.out.println(overlap.atZone(ny)); // -04:00(较早)
System.out.println(overlap.atZone(ny).withLaterOffsetAtOverlap()); // -05:00(较晚)
LocalDateTime gap = LocalDateTime.of(2024, 3, 10, 2, 30);
System.out.println(gap.atZone(ny)); // 2024-03-10T03:30-04:00
与旧 API 互操作05
老代码里的 java.util.Date、Calendar、java.sql.Timestamp 仍会出现在边界层。转换时优先通过 Instant 作为桥梁,并明确时区。
Date legacy = new Date();
Instant instant = legacy.toInstant();
LocalDateTime local = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
ZonedDateTime zoned = instant.atZone(ZoneId.of("Asia/Shanghai"));
Date back = Date.from(instant);
java.sql.Timestamp ts = java.sql.Timestamp.from(instant);
Instant fromTs = ts.toInstant();
java.sql.Date/Time/Timestamp分别对应日期 / 时间 / 时间戳,但都带时区陷阱,尽量在 DAO 层就转成 java.time。Calendar的月份从 0 开始,java.time从 1 开始;迁移时逐项核对。SimpleDateFormat不是线程安全的;DateTimeFormatter是。- JPA / JDBC 驱动新版本支持直接映射 java.time 类型。
可测试的时间:Clock06
直接调用 LocalDate.now() 让代码不可测试。把 Clock 作为依赖注入,生产用 Clock.systemUTC(),测试用 Clock.fixed(...)。
public class SubscriptionService {
private final Clock clock;
public SubscriptionService(Clock clock) {
this.clock = clock;
}
public boolean isExpired(LocalDate expiry) {
return LocalDate.now(clock).isAfter(expiry);
}
}
// 测试
Clock fixed = Clock.fixed(
Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC);
var service = new SubscriptionService(fixed);
System.out.println(service.isExpired(LocalDate.of(2025, 12, 31))); // true
Clock.offset(base, Duration) 模拟时间前进。不要用 Thread.sleep 等待时间流逝来测试业务逻辑。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| java.time | 8 正式 | JEP 150;取代 Date / Calendar。 |
| Instant / Duration 精度 | 8 起 | 精度依赖底层时钟,通常为微秒或纳秒。 |
| Duration.toHoursPart 等拆分方法 | 9 正式 | JDK 8 只有 toHours / toMinutes。 |
| Clock | 8 正式 | 测试时间的第一工具。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。