1. 简介

在本篇文章中,我们将深入讲解 SimpleDateFormat 类的使用方式

我们会从 基本实例化方法和格式化风格 开始,逐步介绍该类提供的 处理本地化(locale)和时区(time zone) 的实用方法。

2. 基本实例化

首先来看一下如何创建一个 SimpleDateFormat 对象。

它提供了 4 个构造函数,但顾名思义,我们尽量保持简单。只需要传入一个表示日期格式的字符串(pattern)即可开始使用

举个例子,我们使用如下格式:

"dd-MM-yyyy"

这个 pattern 表示:日-月-年。我们可以通过下面的单元测试验证一下它的行为:

SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
assertEquals("24-05-1977", formatter.format(new Date(233345223232L)));

上面代码中,formatter 将毫秒时间戳转换为可读的日期格式:1977 年 5 月 24 日。

2.1 工厂方法

虽然 SimpleDateFormat 是一个非常方便的类,但 Java 官方更推荐我们使用 DateFormat 类的工厂方法:
getDateInstance()
getDateTimeInstance()
getTimeInstance()

使用这些工厂方法的代码如下:

DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT);
assertEquals("5/24/77", formatter.format(new Date(233345223232L)));

从结果可以看出,这些工厂方法的格式选项是预定义好的,限制了我们自定义格式的能力,所以本文主要还是聚焦于 SimpleDateFormat 的使用。

2.2 线程安全性

⚠️ SimpleDateFormat 不是线程安全的,这是 JavaDoc 中明确指出的:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

在多线程环境下使用时,必须格外小心。推荐的做法是配合 ThreadLocal 使用,这样每个线程都有自己独立的实例:

private final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal
  .withInitial(() -> new SimpleDateFormat("dd-MM-yyyy"));

然后通过如下方式调用:

formatter.get().format(date)

这种方式称为 线程封闭(Thread Confinement),即每个线程只使用自己的实例,避免共享带来的线程安全问题。

当然还有其他两种替代方案,但都不推荐:

❌ 使用 synchronizedReentrantLock:高并发下性能损耗大
❌ 每次都新建实例:频繁创建对象,增加 GC 压力

💡 值得一提的是,从 Java 8 开始,推荐使用新的 DateTimeFormatter 类。它是 不可变且线程安全的,更适合现代开发。

3. 解析日期字符串

SimpleDateFormatDateFormat 不仅可以格式化日期,也可以反向操作:将字符串解析为 Date 对象。

使用 parse() 方法即可实现:

SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
Date myDate = new Date(233276400000L);
Date parsedDate = formatter.parse("24-05-1977");
assertEquals(myDate.getTime(), parsedDate.getTime());

⚠️ 注意:构造 SimpleDateFormat 时使用的 pattern 必须与待解析的字符串格式一致,否则会抛出 ParseException

4. 日期时间格式模式

SimpleDateFormat 提供了丰富的格式化选项。虽然完整列表可以参考 JavaDoc,但这里我们列出一些常用的字符及其含义:

字符 含义 示例
M 月份 12; Dec
y 年份 94
d 日期 23; Mon
H 小时 03
m 分钟 57

⚠️ 输出格式还与 pattern 中字符的个数有关。例如:

  • "MM" → 输出为 06
  • "MMM" → 输出为 Jun

5. 应用本地化(Locale)

SimpleDateFormat 支持多种本地化格式,可以在构造函数中传入 Locale 参数。

举个例子,格式化为法语日期:

SimpleDateFormat franceDateFormatter = new SimpleDateFormat("EEEEE dd-MMMMMMM-yyyy", Locale.FRANCE);
Date myFriday = new Date(1539341312904L);
assertTrue(franceDateFormatter.format(myFriday).startsWith("vendredi"));

上面代码中,日期被正确格式化为法语的星期五(vendredi)。

⚠️ 注意:虽然支持多种 Locale,但 并非所有语言都有完整支持,Oracle 官方建议使用 DateFormat 的工厂方法以保证更好的兼容性。

6. 设置时区

由于 SimpleDateFormat 继承自 DateFormat,我们可以使用 setTimeZone() 方法来指定时区:

Date now = new Date();

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("EEEE dd-MMM-yy HH:mm:ssZ");

simpleDateFormat.setTimeZone(TimeZone.getTimeZone("Europe/London"));
logger.info(simpleDateFormat.format(now));

simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
logger.info(simpleDateFormat.format(now));

输出示例:

INFO: Friday 12-Oct-18 12:46:14+0100
INFO: Friday 12-Oct-18 07:46:14-0400

💡 注意:pattern 中的 Z 用于显示时区偏移量。

7. 总结

在这篇文章中,我们深入讲解了 SimpleDateFormat 的各种用法:

✅ 如何实例化 SimpleDateFormat
✅ pattern 字符串如何影响日期格式
✅ 本地化(Locale)的应用
✅ 时区的设置与切换

虽然 SimpleDateFormat 功能强大,但由于其 线程不安全 的特性,在并发场景下推荐使用 Java 8 引入的 DateTimeFormatter 替代方案

完整代码示例可参考 GitHub 项目


原始标题:A Guide to SimpleDateFormat