☕ 第六章 Java API
📝 一句话概括

6.6 学了「用翻译官」,这节讲「管翻译官」——模式字母的完整规矩、常用模板怎么拼、以及两代格式化器(SimpleDateFormat vs DateTimeFormatter)的关键差别。

6.7 日期时间格式化类

1. 承接:格式化到底在做什么?

6.6 已经用过 SimpleDateFormat。现在补一句本质定义:

格式化 = 在「Date / LocalDate 对象」和「字符串」之间,按模板逐位转换。

模板不是随便写的——它是一份「逐位咬合」的合同。

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 版本(宽松模式可能放行,别赌)

三条规矩:

  1. 非字母字符(- : 空格 汉字)在 format 时原样输出、parse 时原样匹配
  2. 字母位数决定期望位数和补零
  3. 对不上就 ParseException——受检异常(5.2 那派),编译器逼你写预案
💡 format 永远不炸,parse 才会炸

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. 常见误区

⚠️ 误区 1:把模式里的字母当成普通字符输出

模板里字母是占位符,其余字符才原样输出。想输出字母本身要包单引号:"'yyyy年'yyyy" ——极少用,知道即可。

⚠️ 误区 2:以为 parse 失败返回 null 或返回默认值

直接抛 ParseException,不返回任何东西。别写 if (d == null) 这种防御——编译就过不去(没处理受检异常)。

⚠️ 误区 3:用 `hh` 显示下午时间

hh 是 12 小时制,15 点显示成 03。日志时间用 HH。再配 a 才有"下午"字样。

⚠️ 误区 4:一个 SimpleDateFormat 静态共享以为没事

单线程确实没事,但代码被搬进多线程环境就炸雷(而且不报错)。养成"用完就扔"的习惯,一劳永逸。

⚠️ 误区 5:LocalDate.parse 不传格式器直接喂 "2026年09月19日"

不传格式器时默认按 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. 自测一下

❓ Q1:`yyyy-MM-dd` 模板下,9 月 5 日 format 出来是什么?模板改成 `yyyy-M-d` 呢?

前者 2026-09-05(两位补零),后者 2026-9-5(一位不补)。位数 = 期望位数,这是补零规矩。

❓ Q2:为什么 format 永远不抛异常,parse 却要处理受检异常?

format 的输入是 Java 自己的日期对象,一定拼得出来;parse 的输入是外部字符串,格式不可控,对不上模板就得报错。危险的永远是"读入外部数据"。

❓ Q3:SimpleDateFormat 线程不安全,出问题时的表现是什么?为什么比直接报错更可怕?

日期莫名错乱(错值、错字段),且不抛异常。静悄悄出错最难排查——你以为是数据错了,其实是格式器被踩了。

❓ Q4:DateTimeFormatter 靠什么做到线程安全?这个设计思路在哪些类里见过?

不可变设计——对象造好就不改,多线程只读不写自然安全。同一思路:String、BigDecimal、LocalDate 全家(不可变三连)。

❓ Q5(毕业题):把字符串 "2026年09月19日" 解析成日期再加一天输出,两种写法各怎么写?哪种不用处理异常?

老:new SimpleDateFormat("yyyy年MM月dd日").parse(...) + 毫秒加 86400000L 再 format,要 throws ParseException;新:LocalDate.parse(s, f) + plusDays(1),f 用 DateTimeFormatter.ofPattern("yyyy年MM月dd日"),不抛受检异常。新写法两行干净还不用管异常。

10. 下一步学什么


相关笔记