Skip to content

日期格式化与解析 ​

学习目标 ​

  • 能用 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。

java
// 语义:日期没有时分秒,适合生日、账期和截止日。
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-27

LocalDate.format:将日期格式化为文本 ​

date.format(formatter) 使用同一个 "uuuu-MM-dd" 格式器,把日期恢复为 "2026-09-27"。

java
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-27

LocalTime.parse:将文本解析为本地时间 ​

HH 是 00–23 的 24 小时制;若业务使用上午/下午,要配合 a 和 hh,不要混写成 HH a。

java
// 语义: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:15

LocalTime.format:将本地时间格式化为文本 ​

需要输出固定的 24 小时制文本时,time.format(formatter) 按 "HH:mm:ss" 得到 "09:30:15"。

java
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:15

LocalDateTime.parse:将文本解析为本地日期时间 ​

它仍然没有时区,不能独立表示跨地区的唯一时刻;会议预约等业务还需要补充 ZoneId。

java
// 语义:它仍然没有时区,不能独立表示跨地区的唯一时刻。
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:30

LocalDateTime.format:将本地日期时间格式化为文本 ​

value.format(formatter) 保留 value 的日期和钟表读数,但输出仍不包含时区或偏移。

java
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:30

DateTimeFormatter.ISO_*:使用稳定 ISO 格式 ​

接口和日志优先使用预定义 ISO 格式,避免团队各自拼接模式;ISO_OFFSET_DATE_TIME 和 ISO_ZONED_DATE_TIME 分别适合带偏移和区域的值。

java
// 语义:接口和日志优先使用预定义 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:00Z

ofPattern:按模式构造格式化器 ​

常用字母要区分大小写:MM 月、mm 分钟,dd 日,HH 小时;模式应集中定义而不是散落在业务代码里。

java
// 语义:常用字母要区分大小写: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/27

withLocale:按地区格式化文本 ​

月份和星期名称会受 Locale 影响;协议文本应使用固定 Locale,用户界面再根据用户地区选择本地化格式。

java
// 语义:月份和星期名称会受 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 2026

parse 的异常边界:捕获格式错误 ​

只在输入边界捕获并转换异常;不要用 try/catch 掩盖内部业务逻辑的日期错误。

java
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:兼容旧日期格式化器 ​

旧接口需要它时应局部创建,不能在多线程之间共享同一实例。

java
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());
// 输出:10

LocalDateTime.of:由字段构造本地日期时间 ​

字段已经拆分且不含时区时,用它构造并立即校验日期时间。

java
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());
// 输出:9

LocalTime.of:由字段构造本地时间 ​

它只表达一天内的时间,不携带日期、时区或偏移量。

java
import java.time.LocalTime;
var value = LocalTime.of(9, 30, 5);
System.out.println(value);
// 输出:09:30:05
System.out.println(value.getSecond());
// 输出:5

Duration.of:按单位构造时间段 ​

它把数值与 ChronoUnit 组合成基于秒和纳秒的时间量。

java
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);
// 输出:PT3M

DateTimeFormatterBuilder:构造多个可选部分 ​

构建器适合兼容多种输入格式;可选分支过多会降低协议清晰度,优先统一上游格式。

java
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:30

ResolverStyle.STRICT:严格解析日期 ​

严格模式拒绝不可能的日期;使用 uuuu 比 yyyy 更适合配合严格解析,避免 era 信息缺失。

java
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-27

parseBest:按输入字段选择结果类型 ​

它适合兼容带不带时间的输入;返回类型不固定时,调用方要把类型分支写清楚。

java
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:30

DateTimeFormatter 常量:重复使用线程安全格式器 ​

格式化器不可变且线程安全,可以作为 static final 共享;不要为每次请求重复拼接相同模式。

java
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-27

TemporalAccessor:读取通用时间字段 ​

通用字段适合格式化器扩展和框架适配;普通业务代码优先转换为具体的 LocalDate 等类型。

java
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

简单案例 ​

java
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。

课后小问 ​

  1. 为什么接口日期建议集中使用 ISO 格式?

    • 答案:ISO 格式明确、跨语言一致,且可以直接由 java.time 的预定义格式化器解析。
    • 解析:团队自定义格式如果没有统一文档,很容易出现月份/分钟或时区字段误读;协议应稳定优先。
  2. 为什么严格解析常用 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。