JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · CORE · DATE TIME

java.time:不可变的时间模型

JDK 8 · 速查主题 04 / 06 · 预计 28 分钟
核心28 分钟JEP 150
一句话java.time 用不可变对象和清晰的类型分工取代 Date/Calendar:Instant 表示时间线上的点,Local* 表示无时区的人类时间,ZonedDateTime 负责时区与夏令时规则。

学完你要能自己回答

  1. java.time 为什么能取代 Date/Calendar?它修正了哪些设计问题?
  2. LocalDateTime、Instant、ZonedDateTime 分别表示什么?跨时区存储该用哪个?
  3. DateTimeFormatter 为什么可以共享,而 SimpleDateFormat 不行?

类型地图:先分清「点」与「读数」01

java.time 的核心区分是:时间线上的瞬时点(Instant)与人类可读的日期时间(Local*)。前者适合存储和比较,后者适合展示和业务规则。时区是把两者连接起来的规则。

类型表示典型用途
Instant时间线上的瞬时点(UTC 纪元)日志、存储、超时、比较
LocalDate日期,无时区生日、账单日
LocalTime时间,无日期营业时间、每日时刻
LocalDateTime日期 + 时间,无时区本地日程、表单
ZonedDateTime日期 + 时间 + ZoneId跨时区展示、夏令时
OffsetDateTime日期 + 时间 + 偏移量协议、数据库时间戳
Duration精确时长(秒/纳秒)超时、性能计时
Period日期跨度(年/月/日)年龄、账期

LocalDateTime 不是「UTC 时间」,它根本没有时区;同一个 LocalDateTime 在上海和纽约代表两个不同的瞬时点。需要跨时区比较或存储时,使用 Instant 或 ZonedDateTime。

Instant 没有时区,LocalDate 没有时间,ZonedDateTime 才有完整的时区规则。选类型时先问:这是「时间线上的点」还是「墙上的读数」?

不可变:plusDays 返回新对象02

java.time 的核心类都是不可变的:plusDays、withYear、minusHours 都返回新对象,原对象不变。因此它们天然线程安全,可以自由共享,也不会出现「谁把日期改了」的问题。

LocalDate release = LocalDate.of(2014, 3, 18);
System.out.println(release.plusDays(30));
System.out.println(release.getDayOfWeek());
System.out.println(ChronoUnit.DAYS.between(release, LocalDate.of(2014, 4, 17)));

Period p = Period.between(release, LocalDate.of(2014, 4, 17));
System.out.println(p.getDays());

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd", Locale.US);
System.out.println(release.format(fmt));

ZonedDateTime shanghai = release.atStartOfDay(ZoneId.of("Asia/Shanghai"));
System.out.println(shanghai.toInstant());
System.out.println(shanghai.plusHours(10).toLocalTime());
2014-04-17
TUESDAY
30
30
2014/03/18
2014-03-17T16:00:00Z
10:00

注意 Period.getDays() 只返回「天数部分」,不是总天数。3 月 18 日到 4 月 17 日是 30 天,所以这里刚好等于 30;跨月跨年时要用 ChronoUnit.DAYS.between 拿总天数。

DateTimeFormatter 也是不可变且线程安全的,可以定义为 static final 常量;SimpleDateFormat 不是,绝不能多线程共享。

格式化与解析:pattern 的大小写陷阱03

解析和格式化都通过 DateTimeFormatter。默认使用 ISO-8601;自定义 pattern 时注意大小写:yyyy 年、MM 月、dd 日、HH 24 小时、mm 分、ss 秒。

LocalDate d = LocalDate.parse("2014-03-18");
System.out.println(d.format(DateTimeFormatter.BASIC_ISO_DATE));

try {
    LocalDate.parse("2014-02-30");
} catch (DateTimeParseException e) {
    System.out.println(e.getClass().getSimpleName());
}
20140318
DateTimeParseException

LocalDate.parse 默认严格校验,2 月 30 日不会自动纠正为 2 月 28 日,而是抛 DateTimeParseException。需要宽松解析时显式使用 ResolverStyle,但业务日期通常不该宽松。

DateTimeFormatter.ofPattern("yyyy-MM-dd") 与 DateTimeFormatter.ISO_LOCAL_DATE 在大多数场景等价;但 YYYY 是 week-based year,跨年周会得到意外结果,不要写错。

时区与夏令时:LocalDateTime 不够用04

ZonedDateTime 带 ZoneId,按该地区的规则处理偏移量和夏令时。夏令时跳过的本地时间会被推到合法时间;重叠的本地时间需要显式选择 earlier/later offset。

ZoneId ny = ZoneId.of("America/New_York");
LocalDateTime gap = LocalDateTime.of(2014, 3, 9, 2, 30);
System.out.println(ZonedDateTime.of(gap, ny));
System.out.println(ZonedDateTime.of(gap, ny).plusHours(1));
2014-03-09T03:30-04:00[America/New_York]
2014-03-09T04:30-04:00[America/New_York]

2014-03-09 凌晨 2:30 在纽约不存在(夏令时从 2 点跳到 3 点),ZonedDateTime.of 把它推到了 3:30。plusHours(1) 是时间线上的加一小时,不是墙上时间加一小时。

存储时间优先用 Instant(或 epoch millis),展示时再套 ZoneId;不要把 LocalDateTime 当 UTC 存库,否则跨时区会出现固定偏移。

与 Date / Calendar 互操作05

旧 API 通过 toInstant()、Date.from()、GregorianCalendar.toZonedDateTime() 互转。迁移时不要逐行翻译,直接把业务类型换成 java.time,再在边界处转换。

Date legacy = new Date(0L);
System.out.println(legacy.toInstant());
1970-01-01T00:00:00Z

java.sql.Date 的 toInstant() 在 JDK 8 会直接抛 UnsupportedOperationException,因为它只表示日期;应改用 toLocalDate()。java.sql.Timestamp 则可以转 toInstant()。

Date.getYear() 返回「年份减 1900」,Calendar.MONTH 从 0 开始,Calendar 还默认宽松解析。这些历史包袱正是 java.time 要解决的问题。

版本边界BOUNDARY

这张表比 API 本身更值得记。写代码时先确认你的运行时版本。

特性 / API状态说明
java.time8 正式JEP 150;不可变、线程安全,默认 ISO-8601。
`LocalDate.datesUntil`9 起JDK 8 用循环 + plusDays 生成日期序列。
`Duration.toSecondsPart`9 起JDK 8 用 getSeconds() % 60。
`DateTimeFormatter` 线程安全8 起可复用;SimpleDateFormat 仍不是线程安全的。
`java.sql.Date.toInstant`8 抛 UOE改用 toLocalDate();Timestamp 可转 Instant。
ThreeTen-BackportJava 7 可用无法升级到 8 时的过渡方案,API 接近但包名不同。

费曼自检SELF CHECK

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

01. LocalDateTime 和 Instant 的本质区别是什么?
LocalDateTime 是无时区的墙上读数,不对应唯一瞬时点;Instant 是时间线上的点,以 UTC 纪元为基准。跨时区比较、存储和计算时长应使用 Instant。
02. 为什么 java.time 的类是线程安全的?
因为它们是 immutable 的:所有修改方法都返回新对象,内部字段不会被改变。因此可以安全共享,不需要同步。
03. Period 和 Duration 分别表示什么?
Period 表示日期跨度(年/月/日),用于日历意义上的差;Duration 表示精确时长(秒/纳秒),用于时间线上的差。前者受月份长度影响,后者不受。
04. java.sql.Date.toInstant() 在 JDK 8 为什么不能用?
它只表示日期,没有时间部分,JDK 8 的实现直接抛 UnsupportedOperationException;应使用 toLocalDate()。需要瞬时点时用 java.sql.Timestamp 或 java.util.Date。
学习进度 记录保存在本机浏览器 · 不上传

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

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