开始前,会这些就够了
- 知道
LocalDate表示日历日期,本身没有时区。 - 会捕获
DateTimeParseException,并把输入错误与成功结果分开。 - 先读 日期与时间:分清日历时间、时间点和时区:先知道
LocalDate是日历日期,本例日期变化与时区转换无关。 - 先读 异常与资源释放:失败也要有出口:把日期解析失败作为明确分支处理,避免捕获之后返回成功。
导入生日字段时,你用 DateTimeFormatter.ofPattern("uuuu-MM-dd") 解析 2023-02-29,程序没有报错。把结果打印出来,得到的却是 2023-02-28。原始文字中的日期不存在,为什么会成功?
因为“按这个格式读取”还包含“把读出的年、月、日组合成日期”的规则。ofPattern 默认使用 SMART。它遇到某些无效组合时会作调整,而不是一律拒绝。
把例子带到自己的电脑上
下载完整练习包
需要 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- Main.java:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 Main.java
java -cp . Main解压后,在含 Main.java 的目录打开终端。命令分别回车执行。固定输入的 Java 17 单文件实验;两套真实 JDK 输出可复核,互动页只回放这些记录。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
同一段日期文字,解析成功还可能改变年月日
正在准备实验控件…
十条记录均为本页 Java 程序在 JDK 17/21 的真实输出。页面只回放固定 ISO LocalDate 输入,Locale.ROOT;没有执行 JVM,也没有模拟时区、其他历法或生日业务规则。
同一个字符串,先对照三种结果
对 2023-02-29 使用 uuuu-MM-dd:STRICT 抛 DateTimeParseException;SMART 得到 2023-02-28;LENIENT 得到 2023-03-01。2023 年的二月没有第 29 天,这三个结果体现了三种不同处理规则。
再看 2024-02-30。2024 年是闰年,SMART 调整到 2024-02-29,LENIENT 得到 2024-03-01,STRICT 仍拒绝。最后放入本来就有效的 2024-02-29,三种风格在本例中都保留该日期。
这不是程序读取了机器当前日期,也不是时区把日期推前一天。本例只用 LocalDate、固定输入与 Locale.ROOT,没有时区转换。互动区可切换输入和风格,直接比较这些实测记录。
先读出字段,再决定字段能否组成日期
可以把解析理解成两步:先从文字中得到年、月、日等字段,再把字段组合成某个日期。第二步就是这里讨论的 resolve。文字长得像“年-月-日”,并不意味着这个年、月、日的组合真的存在。
在本例的 ISO 日历中,STRICT 要求组合有效;SMART 对处于日字段允许范围、却超过当月末日的数值调整到月末;LENIENT 则把多出的天数按日期运算继续向后推。三种风格各有用途,导入人工填写的生日通常希望明确拒绝错误,而不能悄悄改成另一天。
注意 SMART 不等于“任意错误都能修好”。例如字段本身超出其允许范围,与一个正常日数落在较短月份,是不同情况。本文完整运行覆盖的是列出的三组输入,不把它们推广为所有非法文本的处理表。
为什么改成 STRICT 后,合法日期也可能失败
再做一组对照:同为 2024-02-29、同为 STRICT,uuuu-MM-dd 可以得到日期,yyyy-MM-dd 在本例却抛异常。问题已经不是闰年,而是模式提取了什么字段。
u 表示 year,y 表示 year-of-era,也就是某个纪元中的年份。对于本文没有纪元字段的 yyyy-MM-dd 模式,严格解析不能像宽松方式那样替你默认补齐纪元信息,因此不足以组成需要的 LocalDate。
如果接口约定就是这种 ISO 数字日期,可以明确使用 uuuu-MM-dd 配合 STRICT。若业务真的需要纪元年,则要把纪元表达与日历系统一起设计;不要只机械地替换字母。本文没有覆盖其他历法、纪元名称或负年份输入。
日历合法之后,业务校验仍然要继续
严格解析可以回答“这个输入是否能构成约定格式下的有效日期”。它不知道生日是否在未来、预约是否落在营业日、有效期是否超过业务上限。解析成功后仍要用业务规则检查这些条件,并保留明确的失败信息。
导入时也应保留原始输入,便于定位是哪一行、哪个字段需要修改。如果你选择允许调整的规则,应在接口约定和展示中说清楚调整结果;不能只因为 parse 没抛异常,就声称原始文字完整保留了它表达的日期。
保存完整程序,核对真实输出
程序打印 3 个输入 × 3 种风格,再打印一次 yyyy 与 STRICT 的对照,共 10 行。输出只保留异常类型,避免把随版本变化的英文错误消息当成测试合同;内部还核对 ofPattern 的默认风格确实为 SMART。
2026-10-26 实际使用 Microsoft OpenJDK 17.0.20.1 和 21.0.12,两个版本分别以 --release 17 编译并运行三次,输出一致。页面互动区回放这些固定记录,浏览器没有启动 JVM。固定源码阅读使用 OpenJDK jdk17u 的 jdk-17.0.17+10 标签,它是解释机制的参考版本,不冒充本机发行版的精确源码。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Locale;
public class Main {
static String parse(String input, String pattern, ResolverStyle style) {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern(pattern, Locale.ROOT).withResolverStyle(style);
try {
return LocalDate.parse(input, formatter).toString();
} catch (DateTimeParseException e) {
return e.getClass().getSimpleName();
}
}
static void show(String input, String pattern, ResolverStyle style) {
String result = parse(input, pattern, style);
System.out.printf(
"{\"input\":\"%s\",\"pattern\":\"%s\",\"style\":\"%s\",\"result\":\"%s\"}%n",
input, pattern, style, result);
}
public static void main(String[] args) {
if (DateTimeFormatter.ofPattern("uuuu-MM-dd").getResolverStyle()
!= ResolverStyle.SMART) throw new AssertionError("unexpected default");
String[] inputs = {"2023-02-29", "2024-02-30", "2024-02-29"};
for (String input : inputs) {
for (ResolverStyle style : ResolverStyle.values()) {
show(input, "uuuu-MM-dd", style);
}
}
show("2024-02-29", "yyyy-MM-dd", ResolverStyle.STRICT);
if (!parse("2023-02-29", "uuuu-MM-dd", ResolverStyle.SMART).equals("2023-02-28")) {
throw new AssertionError("wrong SMART resolution");
}
if (!parse("2023-02-29", "uuuu-MM-dd", ResolverStyle.STRICT)
.equals("DateTimeParseException")) throw new AssertionError("invalid date accepted");
}
}
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-26 使用 完整 JDK 17 或更新版本;实测 Windows / JDK 17.0.20.1 / 21.0.12 复跑这份源码并核对完整输出。固定输入的 Java 17 单文件实验;两套真实 JDK 输出可复核,互动页只回放这些记录。
运行后对照结果
{"input":"2023-02-29","pattern":"uuuu-MM-dd","style":"STRICT","result":"DateTimeParseException"}
{"input":"2023-02-29","pattern":"uuuu-MM-dd","style":"SMART","result":"2023-02-28"}
{"input":"2023-02-29","pattern":"uuuu-MM-dd","style":"LENIENT","result":"2023-03-01"}
{"input":"2024-02-30","pattern":"uuuu-MM-dd","style":"STRICT","result":"DateTimeParseException"}
{"input":"2024-02-30","pattern":"uuuu-MM-dd","style":"SMART","result":"2024-02-29"}
{"input":"2024-02-30","pattern":"uuuu-MM-dd","style":"LENIENT","result":"2024-03-01"}
{"input":"2024-02-29","pattern":"uuuu-MM-dd","style":"STRICT","result":"2024-02-29"}
{"input":"2024-02-29","pattern":"uuuu-MM-dd","style":"SMART","result":"2024-02-29"}
{"input":"2024-02-29","pattern":"uuuu-MM-dd","style":"LENIENT","result":"2024-02-29"}
{"input":"2024-02-29","pattern":"yyyy-MM-dd","style":"STRICT","result":"DateTimeParseException"}结论要落在实际使用的格式与日历上
这份实验覆盖固定四位正年份、两位月日、ISO LocalDate、Locale.ROOT 和三个解析风格。它没有验证含时区时间戳、用户地区格式、前后空格、非 ISO 历法或业务期限。
阅读固定版 IsoChronology 源码,可以看到 LENIENT 从年初加月份和天数、SMART 调整月末,以及 STRICT 下纪元年字段处理的路径。源码用来解释本例结果,真实接入仍要为你接受的输入集合设计测试。
记住这些要点
ofPattern默认使用 SMART,解析成功不保证日期没有被调整。- 用 STRICT 检查日历组合,并为接口选择准确的年份模式。
- uuuu 的年与 yyyy 的纪元年不是同一个字段。
- 格式解析、日历合法性与业务限制,分别需要明确规则。
别把例子用过头
- 把“不抛异常”当成“原始年月日一定存在”。
- 只添加 STRICT,却沿用没有纪元字段的 yyyy 模式并忽略含义。
- 把 SMART 理解为可修复所有非法日期。
- 认为严格日期解析已经验证了生日、预约或有效期的业务条件。
自己试一小步
为一个只接受 ISO 数字生日的接口,怎样写出可解释的拒绝规则?
- 增加 2024-13-01、2024-01-00 和 2024-02-31,先预测三种风格的差异再运行。
- 把“日历日期有效”和“生日不能晚于固定的今天”拆成两个判断。
- 选择一个固定今天,让失败样例不随机器时间变化;分别给格式错误与业务错误取清楚的名称。
做到这些,就算完成
- 记录每个输入的原始文字、模式、风格和输出,不能只记是否抛异常。
- 验证正常日期没有被无意修改,同时验证应拒绝的日期确实被拒绝。
- 不把本页未执行的新输入写成已验证结果;自己运行后再补实验记录。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Java 17 ResolverStyle:STRICT、SMART、LENIENT (新标签页)↗
三种解析风格对字段组合的处理;SMART 在 ISO 日期中对超过月末的日数进行调整。
- Java 17 DateTimeFormatter:模式与解析阶段 (新标签页)↗
ofPattern默认 SMART,模式中的 u 与 y 不同,以及解析分为读取字段与 resolve 两阶段。 - OpenJDK 17u 固定源码:IsoChronology (新标签页)↗
resolveYearOfEra与resolveYMD的严格纪元处理、月末调整和宽松日期运算路径。
找一份资料继续练
先选一项,按下面的范围继续学;不必从头读完整门课程。
Java 日期:二月三十日为什么没有报错 (新标签页)↗
回到既有 Java 21 日期资料,把 SMART/STRICT 的两组练习扩充为本文的三种解析风格,再单独记录 uuuu 与 yyyy;保留所用 JDK 与原始输入。
查看学习准备与访问说明 →