ZonedDateTime / OffsetDateTime / DateTimeFormatter 源码精读
概述
带时区的日期时间有三层:LocalDateTime(无时区)+ ZoneOffset(固定偏移,如 +08:00)= OffsetDateTime;LocalDateTime + ZoneId(完整时区规则,含夏令时 DST)= ZonedDateTime。格式化与解析由 DateTimeFormatter(基于 DateTimeFormatterBuilder 的打印机/解析器)完成。本文基于 OpenJDK 21 源码拆解。
一、ZoneId 时区
1.1 ZoneId.systemDefault()
java
public static ZoneId systemDefault() {
// 从 JVM 默认时区转成 ZoneId
return TimeZone.getDefault().toZoneId();
}
// TimeZone.getDefault() 内部:
// System.getProperty("user.timezone") → 若设置则用之
// 否则探测系统时区(如 /etc/localtime、Windows 注册表)systemDefault 探测顺序:
① -Duser.timezone=Asia/Shanghai 显式指定
② 系统配置文件(Linux /etc/localtime / Windows 注册表)
③ 兜底 GMT(找不到时)
注意:容器内常见问题——宿主机时区未传递,导致默认 UTC1.2 ZoneId.of(String) 的解析
java
public static ZoneId of(String zoneId, Map<String, String> aliasMap) {
...
if (zoneId.startsWith("+") || zoneId.startsWith("-")) {
return ZoneOffset.of(zoneId); // ① 偏移量形式(+08:00)
} else if (zoneId.equals("Z")) {
return ZoneOffset.UTC; // ② UTC
}
...
return ZoneRegion.ofId(zoneId, normalized); // ③ 时区数据库查找
}
// ZoneRegion.ofId:
// ZoneRulesProvider.getRules(zoneId) → 从 TZDB(tzdata)加载时区规则
// 未知 ID → DateTimeException("Unknown time-zone ID")ZoneId.of 支持三种形式:
偏移量:+08:00 / -05:00 / Z
地区:Asia/Shanghai / Europe/Paris(TZDB 数据库)
别名:CST/EST/PST 等(部分支持,避免歧义)
规则来源:JVM 内置 tzdata(随 JDK 更新),可升级二、ZoneOffset 固定偏移
java
public static ZoneOffset ofHours(int hours) {
// 小时偏移:范围 -18 ~ +18
return ofHoursMinutesSeconds(hours, 0, 0);
}
public static ZoneOffset ofHoursMinutesSeconds(int hours, int minutes, int seconds) {
validate(hours, minutes, seconds);
int totalSeconds = totalSeconds(hours, minutes, seconds); // 总秒数
return ofTotalSeconds(totalSeconds);
}
public static ZoneOffset ofTotalSeconds(int totalSeconds) {
// 常用偏移缓存(-18h 到 +18h 整点秒)
// 其余创建新实例
return new ZoneOffset(totalSeconds);
}
// 内部
private final int totalSeconds; // 与 UTC 的偏移总秒数(+28800 = +08:00)
private final String id; // 如 "+08:00"ZoneOffset 特点:
固定偏移(无 DST 规则)——只是个"秒数"
totalSeconds:+28800 = +08:00
常用偏移有静态缓存(UTC 等),可 == 比较(推荐 equals)
适用于:无 DST 的固定时区表达(如数据库时间)三、ZonedDateTime 的组合与 DST 调整
java
// java.time.ZonedDateTime
public static ZonedDateTime of(LocalDate date, LocalTime time, ZoneId zone) {
return of(LocalDateTime.of(date, time), zone);
}
public static ZonedDateTime of(LocalDateTime localDateTime, ZoneId zone) {
return ofLocal(localDateTime, zone, null);
}
private static ZonedDateTime ofLocal(LocalDateTime localDateTime, ZoneId zone, ZoneOffset preferredOffset) {
// ① 查时区规则
ZoneRules rules = zone.getRules();
// ② 本地时间 → 可能的瞬时映射(DST 会造成 0/1/2 个映射)
List<ZoneOffset> validOffsets = rules.getValidOffsets(localDateTime);
...
ZoneOffset offset;
if (validOffsets.size() == 1) { // 正常:唯一偏移
offset = validOffsets.get(0);
} else if (validOffsets.size() == 0) { // DST 间隙(时钟快进,时间不存在)
// 偏移调整:向前移动处理
long epochSec = localDateTime.toEpochSecond(ZoneOffset.UTC) ...
offset = rules.getOffset(...);
} else { // DST 重叠(时钟回拨,时间存在两次)
offset = preferredOffset != null && validOffsets.contains(preferredOffset)
? preferredOffset : validOffsets.get(0);
}
return new ZonedDateTime(localDateTime, offset, zone);
}DST 三种场景:
正常(唯一偏移):直接使用
间隙 Gap(spring forward,2:30 不存在):
→ 时间向前推进到 DST 开始时刻
重叠 Overlap(fall back,1:30 出现两次):
→ 优先 preferredOffset(如 withZoneSameLocal 传入原偏移)
→ 否则取第一个偏移3.1 withZoneSameInstant 时区转换
java
public ZonedDateTime withZoneSameInstant(ZoneId zone) {
// ① 转成 UTC 时间线(epochSecond)
long epochSec = toEpochSecond();
// ② 在新时区重建(保持同一瞬时)
return ofInstant(Instant.ofEpochSecond(epochSec, nano), zone);
}
public static ZonedDateTime ofInstant(Instant instant, ZoneId zone) {
// 按新时区的偏移规则计算本地时间
ZoneRules rules = zone.getRules();
ZoneOffset offset = rules.getOffset(instant); // 该时刻的偏移
LocalDateTime ldt = LocalDateTime.ofEpochSecond(instant.getEpochSecond(),
instant.getNano(), offset);
return new ZonedDateTime(ldt, offset, zone);
}withZoneSameInstant(同一时刻换时区):
08:00 Asia/Shanghai → 20:00 America/New_York(同一瞬时)
实现:toEpochSecond → ofInstant(新时区偏移计算本地时间)
对比 withZoneSameLocal(同一本地时间换时区):
08:00 Asia/Shanghai → 08:00 America/New_York(本地时间不变,瞬时不同)3.2 toOffsetDateTime() 转换
java
public OffsetDateTime toOffsetDateTime() {
// 保留本地时间 + 当前偏移,丢弃时区规则
return OffsetDateTime.of(dateTime, offset);
}toOffsetDateTime 语义:
ZonedDateTime(含 DST 规则)→ OffsetDateTime(固定偏移快照)
丢失未来 DST 调整能力(但可再 withZoneSameLocal 恢复)
适用:序列化到数据库/传输(偏移可重算)四、OffsetDateTime 与 OffsetTime
java
// java.time.OffsetDateTime
public final class OffsetDateTime implements Temporal, ... {
private final LocalDateTime dateTime; // 本地时间
private final ZoneOffset offset; // 固定偏移
}
// java.time.OffsetTime
public final class OffsetTime implements Temporal, ... {
private final LocalTime time;
private final ZoneOffset offset;
}OffsetDateTime 组合:
LocalDateTime + ZoneOffset(固定)
= ZonedDateTime 的"快照"形式(无 DST 规则)
转换:
toZonedDateTime() → 恢复(时区需另指定或保留)
toInstant() → 本地时间减偏移得到瞬时五、DateTimeFormatter 构建
5.1 ofPattern
java
public static DateTimeFormatter ofPattern(String pattern) {
return new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter();
}
// DateTimeFormatterBuilder.appendPattern
public DateTimeFormatterBuilder appendPattern(String pattern) {
// 解析模式串:逐字符识别模式字母
parsePattern(pattern);
return this;
}
// parsePattern 内部:
// y → YEAR_OF_ERA / yyyy → YEAR(四位数)
// M → MONTH_OF_YEAR(1/12 或 01/12)
// d → DAY_OF_MONTH
// H → HOUR_OF_DAY(0-23)
// m → MINUTE_OF_HOUR
// s → SECOND_OF_MINUTE
// '文字' → 直接输出文字
// [] → 可选部分模式字母分类:
常用:y 年 / M 月 / d 日 / H 时(0-23) / h 时(1-12) / m 分 / s 秒 / S 毫秒 / n 纳秒
时区:z 时区名 / Z 偏移(±HHMM) / X/xx/xxx 偏移(ISO) / O 偏移(本地化)
数量决定输出宽度:M → 1/12,MM → 01/12,MMM → 英文缩写,MMMM → 全名5.2 Builder 的可选/样式
java
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd")
.optionalStart() // 可选段开始
.appendLiteral('T')
.appendPattern("HH:mm:ss")
.optionalEnd() // 可选段结束
.toFormatter();DateTimeFormatterBuilder 能力:
追加模式/文字/字段打印机
可选段([ ... ])→ 解析时可选
默认值(parseDefaulting)
多格式化器组合(解析"日期或日期时间")
样式(appendStyle)→ 本地化样式(SHORT/MEDIUM/LONG/FULL)六、format 与 parse 流程
6.1 format(TemporalAccessor)
java
public String format(TemporalAccessor temporal) {
// ① 检查类型(先查 createQuery 缓存实例)
StringBuilder buf = new StringBuilder(32);
// ② 打印:从第一个 printer/parser 开始逐个输出
printTo(temporal, buf);
return buf.toString();
}
// printTo → DateTimePrinterParser.format
// 各 printer 实现:
// NumberPrinterParser:数值(年/月/日...)→ appendValue 补零
// CharLiteralPrinterParser:普通字符(-/:)
// StringLiteralPrinterParser:带引号文字format 流程:
① 类型解析(TemporalAccessor 接口统一访问字段)
② 遍历 printer 链 → 每个 printer 把对应字段输出到 StringBuilder
③ 数值打印机按宽度补零(yyyy → 4 位)6.2 parse(CharSequence)
java
public TemporalAccessor parse(CharSequence text) {
// ① 未解析解析(容错,position 返回错误位置)
DateTimeParseContext context = parseUnresolved(text, position);
...
// ② 解析结果校验(必需字段)
ChronoZonedDateTime<?> zdt = context.toZonedDateTime(); // 视解析内容
...
// ③ resolver:把字段解析成完整对象(默认解析样式 SMART)
return context.resolve(); // 如 "2026-02-30" SMART → 3 月 2 日
}parse 流程:
parseUnresolved:按 printer/parser 链逐段匹配输入文本
数字 → NumberPrinterParser.parse(读数字 + 宽度)
文字 → 精确匹配
校验:解析到的字段满足必需字段集合
resolve:字段解析为对象
日期字段 → LocalDate
时间字段 → LocalTime
时区字段 → ZoneId/ZoneOffset
解析失败 → DateTimeParseException(带错误位置)6.3 解析器样式(ResolverStyle)
ResolverStyle 三种:
STRICT:严格(2026-02-30 → 异常)
SMART:智能(2026-02-30 → 修正为 3 月 2 日,默认)
LENIENT:宽松(2026-02-30 → 直接加天,也可解析 2 月 30)七、TemporalQueries 查询
java
// java.time.temporal.TemporalQueries
public static final TemporalQuery<ZoneId> zone() { return TemporalQueries.ZONE; }
public static final TemporalQuery<LocalDate> localDate() { return TemporalQueries.LOCAL_DATE; }
public static final TemporalQuery<ZoneOffset> offset() { return TemporalQueries.OFFSET; }
public static final TemporalQuery<Chronology> chronology() { return TemporalQueries.CHRONO; }
// TemporalQuery 是一个函数式接口
public interface TemporalQuery<R> {
R queryFrom(TemporalAccessor temporal);
}java
// 用法示例
ZoneId zone = dateTime.query(TemporalQueries.zone()); // 查时区
LocalDate date = dateTime.query(TemporalQueries.localDate()); // 查日期部分TemporalQueries 机制:
查询 = 向时间对象问"你是不是/包含某部分"
各类时间实现 query():
ZonedDateTime → 返回 zone、localDate、offset 等
LocalDateTime → localDate 返回,zone 返回 null
Instant → 部分返回 null
用途:通用代码处理多种时间类型(写库/传输前的类型判断)八、实现要点
ZonedDateTime/Formatter 核心:
ZoneId.systemDefault:user.timezone 属性 → 系统探测
ZoneId.of:偏移形式 / UTC / TZDB 地区(ZoneRegion.ofId)
ZoneOffset:totalSeconds 固定偏移(+28800 = +08:00)
ZonedDateTime.of:rules.getValidOffsets 处理 DST 三场景
(唯一 / 间隙前推 / 重叠取 preferred)
withZoneSameInstant:toEpochSecond → ofInstant(保持瞬时)
toOffsetDateTime:保留本地时间 + 偏移快照
ofPattern:Builder.parsePattern 逐字母构建打印机链
format:printer 链输出到 StringBuilder
parse:parseUnresolved → resolver.resolve(STRICT/SMART/LENIENT)
TemporalQueries:TemporalQuery 函数式接口统一查询
常见陷阱:
容器时区未设置 → systemDefault 返回 UTC
withZoneSameLocal vs withZoneSameInstant 混淆 → 时刻错误
ZonedDateTime 序列化(JPA)需时区类型适配
SMART 模式 "2026-02-30" 不报错 → 数据验证需 STRICT
OffsetDateTime 无 DST → 转换瞬间结果与 ZonedDateTime 可能不同