6.6 学了「用翻译官」,这节讲「管翻译官」——模式字母的完整规矩、常用模板怎么拼、以及两代格式化器(SimpleDateFormat vs DateTimeFormatter)的关键差别。
6.7 日期时间格式化类
1. 承接:格式化到底在做什么?
6.6 已经用过 SimpleDateFormat。现在补一句本质定义:
格式化 = 在「Date / LocalDate 对象」和「字符串」之间,按模板逐位转换。
format(日期)→ 按模板拼字符串(Date → String)parse(字符串)→ 按模板读字符串(String → Date)
模板不是随便写的——它是一份「逐位咬合」的合同。
2. 模式字母:完整速查表
| 字母 | 含义 | 写 1 位 | 写 2 位 | 写 4 位 |
|---|---|---|---|---|
y |
年 | 2026 | 26 | yyyy → 2026 |
M |
月 | 9 | MM → 09 | MMM → 九月(文本) |
d |
日 | 19 | dd → 19 | — |
H |
时(24 制) | 15 | HH → 15 | — |
h |
时(12 制) | 3 | 03 | — |
m |
分 | 4 | mm → 04 | — |
s |
秒 | 31 | ss → 31 | — |
E |
星期 | E → 周六 | — | EEEE → 星期六 |
a |
上午/下午 | a → 下午 | — | — |
位数规矩:数字类字母,位数不足补零(MM 把 9 月写成 09);位数过多(MMM、EEEE)会从数字变成文本。这就是 2026-9-19 和 2026-09-19 长得不一样的根源。
3. 咬合规矩:模板和字符串必须严丝合缝
模板: yyyy - MM - dd HH : mm : ss
实际: 2026-09-19 15:04:31 ✓ 咬合成功
实际: 2026/09/19 15:04:31 ✗ parse 炸:分隔符对不上
实际: 2026-9-19 15:04:31 ✓/✗ 看 JDK 版本(宽松模式可能放行,别赌)
三条规矩:
- 非字母字符(
-:空格 汉字)在 format 时原样输出、parse 时原样匹配 - 字母位数决定期望位数和补零
- 对不上就 ParseException——受检异常(5.2 那派),编译器逼你写预案
format 的输入是 Date 对象,怎么拼都能拼出字符串;parse 的输入是人写的字符串,格式千奇百怪才需要检查。记住:危险的永远是"读入外部数据"的方向。
4. 常用格式模板:背这几个就够
| 需求 | 模板 | 输出示例 |
|---|---|---|
| 日期 | yyyy-MM-dd |
2026-09-19 |
| 时间 | HH:mm:ss |
15:04:31 |
| 日期+时间 | yyyy-MM-dd HH:mm:ss |
2026-09-19 15:04:31 |
| 中文日期 | yyyy年MM月dd日 |
2026年09月19日 |
| 带星期 | yyyy-MM-dd E |
2026-09-19 周六 |
| 精确到毫秒 | yyyy-MM-dd HH:mm:ss SSS |
2026-09-19 15:04:31 567 |
5. 线程安全:SimpleDateFormat 唯一的大坑
SimpleDateFormat 线程不安全:它内部藏着一个共享的日历字段,多个线程同时用一个 sdf 做 format/parse,字段会互相踩——日期莫名错乱,而且不报错(比炸掉更可怕,错了都不知道)。
现阶段怎么防:
// ✗ 危险:静态共享 + 将来多线程
public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
// ✓ 安全:当局部变量,用完就扔(本课所有示例都是这么写的)
public String fmt(Date d) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(d);
}
单线程阶段这么写毫无问题——但这个坑要先记在账上。
6. 新一代:DateTimeFormatter(会认 + 会用)
JDK8 的 java.time 包配套格式化器,脾气和 LocalDate 一致——不可变,所以线程安全:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class Demo07 {
public static void main(String[] args) {
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
// 格式化:格式器在前调 format
LocalDate d = LocalDate.now();
String s = f.format(d); // 2026年09月19日
// 解析:格式器当参数传入;且不抛受检异常!
LocalDate d2 = LocalDate.parse("2026-09-19", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
System.out.println(s + " / " + d2.plusDays(1)); // 2026年09月20日
}
}
注意三个和 SimpleDateFormat 的差别:
| SimpleDateFormat(老) | DateTimeFormatter(新) | |
|---|---|---|
| 所在包 | java.text |
java.time.format |
| 线程安全 | ✗ 不安全 | ✓ 不可变=安全 |
| parse 异常 | ParseException(受检,必须处理) | DateTimeParseException(运行时,不强制) |
| format 写法 | sdf.format(date) |
f.format(localDate)(格式器在前) |
| parse 写法 | sdf.parse(str) |
LocalDate.parse(str, f)(日期类在前) |
企业项目主流是新写法;教材主线用老写法——两套都要认得,面试常问"为什么 SimpleDateFormat 线程不安全"。
7. 常见误区
模板里字母是占位符,其余字符才原样输出。想输出字母本身要包单引号:"'yyyy年'yyyy" ——极少用,知道即可。
直接抛 ParseException,不返回任何东西。别写 if (d == null) 这种防御——编译就过不去(没处理受检异常)。
hh 是 12 小时制,15 点显示成 03。日志时间用 HH。再配 a 才有"下午"字样。
单线程确实没事,但代码被搬进多线程环境就炸雷(而且不报错)。养成"用完就扔"的习惯,一劳永逸。
不传格式器时默认按 yyyy-MM-dd 解析,喂别的格式直接炸 DateTimeParseException。格式自定义了,parse 时就要把同一个格式器递过去——format 和 parse 必须用同一份模板。
8. 动手试一试(30 秒实验)
新建 Demo07:
实验 1:同一个 Date,用三份模板各 format 一次(yyyy-MM-dd / yyyy年MM月dd日 E / HH:mm:ss SSS),对照输出——体会"模板决定长相"。
实验 2:把 MM 改成单 M,看 9 月输出 9 而不是 09——亲眼见补零规矩。
实验 3:parse 一个 2026/09/19 而模板是 yyyy-MM-dd,看 ParseException;换模板后再试——体会"咬合失败"。
实验 4:DateTimeFormatter 版走一遍 format + parse,对比:没写任何 throws,编译照样过(DateTimeParseException 是运行时异常)——两代待遇差一眼见分晓。
9. 自测一下
前者 2026-09-05(两位补零),后者 2026-9-5(一位不补)。位数 = 期望位数,这是补零规矩。
format 的输入是 Java 自己的日期对象,一定拼得出来;parse 的输入是外部字符串,格式不可控,对不上模板就得报错。危险的永远是"读入外部数据"。
日期莫名错乱(错值、错字段),且不抛异常。静悄悄出错最难排查——你以为是数据错了,其实是格式器被踩了。
不可变设计——对象造好就不改,多线程只读不写自然安全。同一思路:String、BigDecimal、LocalDate 全家(不可变三连)。
老:new SimpleDateFormat("yyyy年MM月dd日").parse(...) + 毫秒加 86400000L 再 format,要 throws ParseException;新:LocalDate.parse(s, f) + plusDays(1),f 用 DateTimeFormatter.ofPattern("yyyy年MM月dd日"),不抛受检异常。新写法两行干净还不用管异常。
10. 下一步学什么
- ☑ 6.8 正则表达式 ✅ —— 已完成:点此复习 —— "模板匹配字符串"思想的通用版:模式逐位咬合升级为正则四大零件,matches/split/replaceAll 三姿势上岗。
相关笔记
- 第六章 Java API/6.6 日期和时间类 ← 上一篇:翻译官的入门课,本节讲透翻译官的规矩
- 第五章 异常/5.2 运行时异常与编译时异常:ParseException vs DateTimeParseException = 受检 vs 运行时的活教材
- 第六章 Java API/6.2 字符串类:不可变设计第一次出场,本节 DateTimeFormatter 是最新成员
- 第六章 Java API/6.5 BigInteger和BigDecimal类:同款"不可变+新对象+接返回值"脾气