JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · TIME · JAVA.TIME

java.time 日期与时间

JDK 21 · 速查主题 06 / 12 · 预计 25 分钟
核心25 分钟
一句话java.time 用「机器时间 Instant + 人类时间 Local/Zoned + 时间量 Duration/Period」三组类型取代了 Date 与 Calendar。它不可变、线程安全,但 DST、格式化和时区转换仍然是最容易写错的地方。

学完你要能自己回答

  1. Instant、LocalDateTime、ZonedDateTime 分别表示什么?什么时候该用哪一个?
  2. Duration 和 Period 有什么区别?跨夏令时加一天和加 24 小时为什么不同?
  3. 为什么推荐 uuuu 而不是 yyyy?DateTimeFormatter 是线程安全的吗?

类型地图:先选对抽象01

java.time 的设计原则是「让类型表达语义」。选错类型,后面所有转换都会别扭。先判断你要表示的是时间线上的一个点,还是日历上的一个日期。

类型表示典型用途是否带时区
InstantUTC 时间线上的瞬时点日志、事件时间、数据库时间戳隐含 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]
业务日期不要用 Instant。 「用户在 2026-10-31 下单」是日历概念,不是 UTC 瞬时;用 LocalDate 才能避免时区把日期挪到前一天。

创建、解析与格式化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.time8 正式JEP 150;取代 Date / Calendar。
Instant / Duration 精度8 起精度依赖底层时钟,通常为微秒或纳秒。
Duration.toHoursPart 等拆分方法9 正式JDK 8 只有 toHours / toMinutes。
Clock8 正式测试时间的第一工具。

费曼自检SELF CHECK

先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。

01. Instant、LocalDateTime、ZonedDateTime 的选择标准是什么?
需要时间线上的绝对瞬时(日志、事件、跨时区比较)用 Instant;只需要日历上的日期或本地时间用 LocalDate / LocalDateTime;需要处理时区规则、DST、跨时区调度时用 ZonedDateTime。业务日期通常不应使用 Instant。
02. 跨夏令时「加一天」和「加 24 小时」为什么可能不同?
plusDays 保持本地时间字段不变,跨越 DST 切换日时实际经过的时间可能是 23 或 25 小时;plusHours(24) 固定经过 24 小时,本地读数会偏移一小时。前者符合日历语义,后者符合时间量语义。
03. withZoneSameInstant 和 withZoneSameLocal 有什么区别?
withZoneSameInstant 保持同一瞬时,只改变显示时区,适合跨时区转换;withZoneSameLocal 保持本地日期时间字段不变,只换时区规则,可能导致瞬时改变。跨时区调度几乎总是用前者。
04. 为什么 DateTimeFormatter 可以做成静态常量,而 SimpleDateFormat 不行?
DateTimeFormatter 是不可变且线程安全的;SimpleDateFormat 内部有可变状态(如 Calendar),多线程共享会出错。JDK 8 之后应优先使用 DateTimeFormatter。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。