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),即每个线程只使用自己的实例,避免共享带来的线程安全问题。
当然还有其他两种替代方案,但都不推荐:
❌ 使用 synchronized
或 ReentrantLock
:高并发下性能损耗大
❌ 每次都新建实例:频繁创建对象,增加 GC 压力
💡 值得一提的是,从 Java 8 开始,推荐使用新的 DateTimeFormatter
类。它是 不可变且线程安全的,更适合现代开发。
3. 解析日期字符串
SimpleDateFormat
和 DateFormat
不仅可以格式化日期,也可以反向操作:将字符串解析为 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 项目。