Java DateTimeFormatter 用 YYYY 格式化年末日期,年份提前一年
大写 Y 表示周所属年,年末可能与日历年不同。普通年月日改用 uuuu 或 ISO_LOCAL_DATE,并保留真正周报格式的周规则。
LocalDate 的值是 2018-12-31,用 DateTimeFormatter 格式化成 MM-d-YYYY,字符串却变成 12-31-2019。月份和日期没变,年份提前了一年,而且平时的大部分日期又看不出问题。
在 Java 8 及更新版本的 java.time 中,模式字母 Y 表示周所属年,y 表示纪元内年份,u 表示年份。普通年月日输出使用大写 YYYY,会把周所属年与日历的月、日拼在一起,年末和年初就可能出现这种错位。
把日期对象和格式化结果分开看
下面明确使用 Locale.US,避免示例依赖运行机器的默认区域设置:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
class YearFormatExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2018, 12, 31);
DateTimeFormatter wrong =
DateTimeFormatter.ofPattern("MM-d-YYYY", Locale.US);
DateTimeFormatter corrected =
DateTimeFormatter.ofPattern("MM-d-uuuu", Locale.US);
System.out.println(date); // 2018-12-31
System.out.println(date.format(wrong)); // 12-31-2019
System.out.println(date.format(corrected)); // 12-31-2018
}
}
这里没有把时间戳转换成日期,也没有切换时区。LocalDate 本身仍是原来的年月日,变化发生在字段格式化时。如果实际业务在构造 LocalDate 之前就已得到错误日期,应继续检查前面的解析或时区转换;改模式字母无法修复错误输入。
检查实际使用的格式器常量、配置文件和工具函数,尤其是复制来的 YYYY-MM-dd。同一个程序可能有多个输出路径,不能只修改页面显示却遗漏导出文件名、签名原文或接口字段。
普通日历日期使用 uuuu,或直接用 ISO 格式器
如果协议要求 2018-12-31 这种本地日期格式,可以直接使用:
String text = date.format(DateTimeFormatter.ISO_LOCAL_DATE);
这段接在前面的 date 声明之后即可。需要自己规定顺序与分隔符时,再使用 uuuu-MM-dd 等模式。
yyyy 在通常的公元日期输出中也能解决大写 Y 的问题,但它与 uuuu 不完全相同:前者是纪元内年份,后者支持延伸日历中的年份。涉及纪元、年份 0 或严格解析时,不要把两者当作无条件等价。本文的修复示例针对 ISO LocalDate 的普通年月日表示。
不要通过给日期减一年补偿。只有边界附近的一部分日期会出现周所属年不同,统一减一年会损坏其他日期,也会改变本来正确的日期对象。
周所属年本身有用途
一周不能同时属于两个周所属年。年末的几天可能已经属于下一年的第一周,年初的几天也可能仍属于上一年的最后一周。第一天星期几、第一周至少有几天等规则会受区域设置影响,不能假定所有 Locale 都在同一天跨年。
如果系统真正输出周报编号,就需要周所属年与周序号、星期等字段配套,并明确采用哪套周规则。此时不能把所有大写 Y 全局替换为 u。本次应只修改错误地用周所属年来构造普通年月日的格式器。
对于跨系统协议,显式选定区域设置或标准格式,也能避免开发机和服务器默认区域不同造成的边界差异。固定 Locale 不能替代正确选择字段:即便区域固定,YYYY 仍是周所属年。
验证跨年样本和历史输出
至少覆盖 12 月最后一周、1 月第一周以及年中日期,核对输出年份与 LocalDate.getYear() 所代表的日历年份一致。若格式兼用于解析,还要验证协议允许的日期能正确往返;不要只检查字符串长度。
保留原格式器和样例,修改后检查所有调用方。如果错误字符串已用作文件名、记录键或签名输入,应根据原始日期核对受影响记录,不能批量把文本中的年份减一。回退代码只会恢复旧格式,不会自动修复已生成的数据;涉及外部协议的字段变更,还需与消费方同步验收。


