日期格式化与解析
学习目标
- 能用
DateTimeFormatter在日期对象和文本之间稳定转换。 - 能为
LocalDate、LocalTime和LocalDateTime选择合适的格式。 - 能处理 Locale、严格校验、可选字段和外部输入错误。
核心知识点
专业术语
- 解析(parse):把文本按格式转换成
LocalDate、LocalTime或LocalDateTime。 - 格式化(format):把日期时间对象按格式输出为文本。
DateTimeFormatter:不可变且线程安全的格式化器,可以预定义、按模式或用构建器创建。uuuu与yyyy:uuuu表示带正负号的年,更适合严格的日期时间 API;yyyy是年-of-era 字段。
白话解释与边界
日期对象是结构化数据,字符串只是外部表示。解析时必须知道输入格式和地区规则,不能依赖机器默认格式;格式化时要区分“给用户看的本地化文本”和“给接口传输的稳定协议”。DateTimeFormatter 本身线程安全,可以缓存为常量。
模式字母有严格含义:MM 是月份、mm 是分钟,HH 是 24 小时制、hh 是 12 小时制。解析失败应在边界层捕获并返回清晰的输入错误,不要把格式异常吞掉后生成错误日期。
常用用法
LocalDate.parse:将文本解析为日期
日期没有时分秒,适合生日、账期和截止日;输入不符合模式时会抛 DateTimeParseException。
// 语义:日期没有时分秒,适合生日、账期和截止日。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");
// 关键变化:DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd");;按 "uuuu-MM-dd" 调用 ofPattern 创建值,结果写入 formatter。
LocalDate date = LocalDate.parse("2026-09-27", formatter);
// 关键变化:LocalDate date = LocalDate.parse("2026-09-27", formatter);;按 "2026-09-27", formatter 调用 parse,解析结果写入 date。
System.out.println(date.format(formatter));
// 输出:2026-09-27LocalDate.format:将日期格式化为文本
date.format(formatter) 使用同一个 "uuuu-MM-dd" 格式器,把日期恢复为 "2026-09-27"。
String text = LocalDate.of(2026, 9, 27).format(DateTimeFormatter.ofPattern("uuuu-MM-dd"));
// 关键变化:String text = LocalDate.of(2026, 9, 27).format(DateTimeFormatter.ofPattern("uuuu-MM-dd"));;使用 DateTimeFormatter.ofPattern("uuuu-MM-dd") 调用 format,格式化文本写入 text。
// 输出:2026-09-27LocalTime.parse:将文本解析为本地时间
HH 是 00–23 的 24 小时制;若业务使用上午/下午,要配合 a 和 hh,不要混写成 HH a。
// 语义:HH 是 00–23 的 24 小时制。
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
// 关键变化:DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");;按 "HH:mm:ss" 调用 ofPattern 创建值,结果写入 formatter。
LocalTime time = LocalTime.parse("09:30:15", formatter);
// 关键变化:LocalTime time = LocalTime.parse("09:30:15", formatter);;按 "09:30:15", formatter 调用 parse,解析结果写入 time。
System.out.println(time.format(formatter));
// 输出:09:30:15LocalTime.format:将本地时间格式化为文本
需要输出固定的 24 小时制文本时,time.format(formatter) 按 "HH:mm:ss" 得到 "09:30:15"。
String text = LocalTime.of(9, 30, 15).format(DateTimeFormatter.ofPattern("HH:mm:ss"));
// 关键变化:String text = LocalTime.of(9, 30, 15).format(DateTimeFormatter.ofPattern("HH:mm:ss"));;使用 DateTimeFormatter.ofPattern("HH:mm:ss") 调用 format,格式化文本写入 text。
// 输出:09:30:15LocalDateTime.parse:将文本解析为本地日期时间
它仍然没有时区,不能独立表示跨地区的唯一时刻;会议预约等业务还需要补充 ZoneId。
// 语义:它仍然没有时区,不能独立表示跨地区的唯一时刻。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");
// 关键变化:DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm");;按 "uuuu-MM-dd HH:mm" 调用 ofPattern 创建值,结果写入 formatter。
LocalDateTime value = LocalDateTime.parse("2026-09-27 09:30", formatter);
// 关键变化:LocalDateTime value = LocalDateTime.parse("2026-09-27 09:30", formatter);;按 "2026-09-27 09:30", formatter 调用 parse,解析结果写入 value。
System.out.println(value.format(formatter));
// 输出:2026-09-27 09:30LocalDateTime.format:将本地日期时间格式化为文本
value.format(formatter) 保留 value 的日期和钟表读数,但输出仍不包含时区或偏移。
String text = LocalDateTime.of(2026, 9, 27, 9, 30)
.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm"));
// 关键变化:String text = LocalDateTime.of(2026, 9, 27, 9, 30) .format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm"));;使用 DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm") 调用 format,格式化文本写入 text。
// 关键变化:.format(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm"));;使用 DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm") 格式化 上一个链式结果,返回文本结果。
// 输出:2026-09-27 09:30DateTimeFormatter.ISO_*:使用稳定 ISO 格式
接口和日志优先使用预定义 ISO 格式,避免团队各自拼接模式;ISO_OFFSET_DATE_TIME 和 ISO_ZONED_DATE_TIME 分别适合带偏移和区域的值。
// 语义:接口和日志优先使用预定义 ISO 格式,避免团队各自拼接模式。
import java.time.Instant;
import java.time.format.DateTimeFormatter;
Instant event = Instant.parse("2026-09-27T01:30:00Z");
// 关键变化:Instant event = Instant.parse("2026-09-27T01:30:00Z");;按 "2026-09-27T01:30:00Z" 调用 parse,解析结果写入 event。
System.out.println(DateTimeFormatter.ISO_INSTANT.format(event));
// 输出:2026-09-27T01:30:00ZofPattern:按模式构造格式化器
常用字母要区分大小写:MM 月、mm 分钟,dd 日,HH 小时;模式应集中定义而不是散落在业务代码里。
// 语义:常用字母要区分大小写:MM 月、mm 分钟,dd 日,HH 小时。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
var formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");
// 关键变化:var formatter = DateTimeFormatter.ofPattern("uuuu/MM/dd");;按 "uuuu/MM/dd" 调用 ofPattern 创建值,结果写入 formatter。
System.out.println(LocalDate.of(2026, 9, 27).format(formatter));
// 输出:2026/09/27withLocale:按地区格式化文本
月份和星期名称会受 Locale 影响;协议文本应使用固定 Locale,用户界面再根据用户地区选择本地化格式。
// 语义:月份和星期名称会受 Locale 影响。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
var formatter = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH);
// 关键变化:var formatter = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH);;按 "dd MMM uuuu", Locale.ENGLISH 调用 ofPattern 创建值,结果写入 formatter。
System.out.println(LocalDate.of(2026, 9, 27).format(formatter));
// 输出:27 Sep 2026parse 的异常边界:捕获格式错误
只在输入边界捕获并转换异常;不要用 try/catch 掩盖内部业务逻辑的日期错误。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
try {
var formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd")
.withResolverStyle(ResolverStyle.STRICT);
// 关键变化:var formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd") .withResolverStyle(ResolverStyle.STRICT);;按 "uuuu-MM-dd" 调用 ofPattern 创建值,结果写入 formatter。
// 关键变化:.withResolverStyle(ResolverStyle.STRICT);;调用 withResolverStyle,实参为 ResolverStyle.STRICT;把解析器严格模式设置为 ResolverStyle.STRICT;返回配置后的 formatter。
LocalDate.parse("2026-02-30", formatter);
// 关键变化:LocalDate.parse("2026-02-30", formatter);;按 "2026-02-30", formatter 解析值,返回对应类型实例。
} catch (DateTimeParseException ex) {
System.out.println("invalid date");
// 输出:invalid date
}不常用但需要知道
SimpleDateFormat:兼容旧日期格式化器
旧接口需要它时应局部创建,不能在多线程之间共享同一实例。
import java.text.SimpleDateFormat;
import java.util.Date;
var formatter = new SimpleDateFormat("yyyy-MM-dd");
String text = formatter.format(new Date(0));
System.out.println(text.length());
// 输出:10LocalDateTime.of:由字段构造本地日期时间
字段已经拆分且不含时区时,用它构造并立即校验日期时间。
import java.time.LocalDateTime;
var value = LocalDateTime.of(2026, 10, 3, 9, 30);
System.out.println(value);
// 输出:2026-10-03T09:30
System.out.println(value.getHour());
// 输出:9LocalTime.of:由字段构造本地时间
它只表达一天内的时间,不携带日期、时区或偏移量。
import java.time.LocalTime;
var value = LocalTime.of(9, 30, 5);
System.out.println(value);
// 输出:09:30:05
System.out.println(value.getSecond());
// 输出:5Duration.of:按单位构造时间段
它把数值与 ChronoUnit 组合成基于秒和纳秒的时间量。
import java.time.Duration;
import java.time.temporal.ChronoUnit;
Duration timeout = Duration.of(3, ChronoUnit.MINUTES);
System.out.println(timeout.toSeconds());
// 输出:180
System.out.println(timeout);
// 输出:PT3MDateTimeFormatterBuilder:构造多个可选部分
构建器适合兼容多种输入格式;可选分支过多会降低协议清晰度,优先统一上游格式。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendPattern("uuuu-MM-dd")
.optionalStart().appendPattern(" HH:mm").optionalEnd()
.toFormatter();
// 关键变化:appendPattern("uuuu-MM-dd") 向 formatter builder 追加日期模式并返回同一 builder;后续可选时间段追加 " HH:mm"。
// 关键变化:optionalStart() 不接收显式参数,标记可选日期时间格式段开始并返回同一个 formatter builder。
// 关键变化:.toFormatter();;调用 toFormatter,实参为 无显式参数;完成 formatter builder 配置并返回 DateTimeFormatter。
System.out.println(LocalDateTime.parse("2026-09-27 09:30", formatter));
// 输出:2026-09-27T09:30ResolverStyle.STRICT:严格解析日期
严格模式拒绝不可能的日期;使用 uuuu 比 yyyy 更适合配合严格解析,避免 era 信息缺失。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
var formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd")
.withResolverStyle(ResolverStyle.STRICT);
// 关键变化:var formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd") .withResolverStyle(ResolverStyle.STRICT);;按 "uuuu-MM-dd" 调用 ofPattern 创建值,结果写入 formatter。
// 关键变化:.withResolverStyle(ResolverStyle.STRICT);;调用 withResolverStyle,实参为 ResolverStyle.STRICT;把解析器严格模式设置为 ResolverStyle.STRICT;返回配置后的 formatter。
System.out.println(LocalDate.parse("2026-09-27", formatter));
// 输出:2026-09-27parseBest:按输入字段选择结果类型
它适合兼容带不带时间的输入;返回类型不固定时,调用方要把类型分支写清楚。
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd['T'HH:mm]");
// 关键变化:DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd['T'HH:mm]");;按 "uuuu-MM-dd['T'HH:mm]" 调用 ofPattern 创建值,结果写入 formatter。
TemporalAccessor parsed = formatter.parseBest(
"2026-09-27T09:30", LocalDateTime::from, LocalDate::from);
System.out.println(parsed);
// 输出:2026-09-27T09:30DateTimeFormatter 常量:重复使用线程安全格式器
格式化器不可变且线程安全,可以作为 static final 共享;不要为每次请求重复拼接相同模式。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;
System.out.println(formatter.format(LocalDate.of(2026, 9, 27)));
// 输出:2026-09-27TemporalAccessor:读取通用时间字段
通用字段适合格式化器扩展和框架适配;普通业务代码优先转换为具体的 LocalDate 等类型。
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
var parsed = DateTimeFormatter.ISO_LOCAL_DATE.parse("2026-09-27");
// 关键变化:var parsed = DateTimeFormatter.ISO_LOCAL_DATE.parse("2026-09-27");;按 "2026-09-27" 调用 parse,解析结果写入 parsed。
System.out.println(parsed.get(ChronoField.DAY_OF_MONTH));
// 输出:27简单案例
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class DateFormatDemo {
public static void main(String[] args) {
DateTimeFormatter input = DateTimeFormatter.ofPattern("uuuu/MM/dd HH:mm");
// 关键变化:DateTimeFormatter input = DateTimeFormatter.ofPattern("uuuu/MM/dd HH:mm");;按 "uuuu/MM/dd HH:mm" 调用 ofPattern 创建值,结果写入 input。
DateTimeFormatter output = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");
// 关键变化:DateTimeFormatter output = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss");;按 "uuuu-MM-dd HH:mm:ss" 调用 ofPattern 创建值,结果写入 output。
LocalDateTime value = LocalDateTime.parse("2026/09/27 09:30", input);
// 关键变化:LocalDateTime value = LocalDateTime.parse("2026/09/27 09:30", input);;按 "2026/09/27 09:30", input 调用 parse,解析结果写入 value。
System.out.println(output.format(value));
// 输出:2026-09-27 09:30:00
}
}易混点
MM是月份、mm是分钟;HH是 24 小时、hh是 12 小时,格式模式大小写不能混淆。LocalDateTime的格式化结果没有时区,不能据此推断全球唯一时刻。uuuu适合java.time的年份和严格解析,yyyy表示 era 里的年份,边界语义不同。DateTimeFormatter是线程安全的,旧式SimpleDateFormat不是;新代码应优先使用java.time。
课后小问
为什么接口日期建议集中使用 ISO 格式?
- 答案:ISO 格式明确、跨语言一致,且可以直接由
java.time的预定义格式化器解析。 - 解析:团队自定义格式如果没有统一文档,很容易出现月份/分钟或时区字段误读;协议应稳定优先。
- 答案:ISO 格式明确、跨语言一致,且可以直接由
为什么严格解析常用
uuuu而不是yyyy?- 答案:
uuuu直接表示 proleptic year,和java.time的严格解析语义更匹配。 - 解析:
yyyy是 year-of-era,严格模式下可能需要 era 信息;使用uuuu能避免常见的解析冲突。
- 答案:
本节小结
- 解析把文本变成结构化日期,格式化把日期变成外部文本,二者都需要明确格式。
LocalDate、LocalTime和LocalDateTime分别表示日期、时间和无时区的日期时间。DateTimeFormatter可按模式、ISO 常量或构建器创建,并且可以安全共享。- 严格校验、Locale 和异常边界决定了外部时间输入是否可靠。
快速回顾
- 能正确区分
MM/mm与HH/hh。 - 能为协议选择 ISO 格式,为界面选择 Locale 格式。
- 能使用
uuuu和ResolverStyle.STRICT校验日期。 - 能在解析失败的输入边界处理
DateTimeParseException。
阿源的知识库