1. 简介
大家好,我是老谭说架构,今天要跟大家探讨下和深入了解 SimpleDateFormat 类。
我们将了解简单的实例 和格式化样式 以及该类为处理语言环境和时区而公开的有用方法。
Java 的 SimpleDateFormat
类不是线程安全的,这是因为它的内部状态(例如,日期格式的模式字符串)是可变的。在一个多线程环境中,多个线程同时访问同一个 SimpleDateFormat
实例可能会导致数据竞争,从而产生不可预期的结果。
2. 简单实例
首先,让我们看看如何实例化一个新的 SimpleDateFormat 对象。
有4 个可能的构造函数- 但为了与名称保持一致,让我们保持简单。我们开始所需的只是一个 表示我们想要的 日期模式 的 字符串 。
让我们从破折号分隔的日期模式开始,如下所示:
js
"dd-MM-yyyy"
这将正确格式化日期,从当前日期开始,再从当前月份开始,最后从当前年份开始。我们可以使用简单的单元测试来测试我们的新格式化程序。我们将实例化一个新的 SimpleDateFormat 对象,并传入一个已知日期:
java
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
assertEquals("24-05-1977", formatter.format(new Date(233345223232L)));
在上面的代码中,格式化程序将 毫秒 长转换 为人类可读的日期 - 1977 年 5 月 24 日。
2.1. 工厂方法
尽管SimpleDateFormat 是一个可以快速构建日期格式化程序的便捷类,但我们还是鼓励使用DateFormat类上的工厂方法 getDateFormat() 、getDateTimeFormat() 、getTimeFormat() 。
使用这些工厂方法时,上面的例子看起来有点不同:
java
DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT);
assertEquals("5/24/77", formatter.format(new Date(233345223232L)));
从上面我们可以看出,格式化选项的数量由DateFormat类上的字段**预先确定。这在很大程度上 限制了我们可用的格式化选项, 这就是为什么我们在本文中坚持使用 SimpleDateFormat 。
2.2. 线程安全
SimpleDateFormat 的 JavaDoc明确指出 :
日期格式不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问某个格式,则必须进行外部同步。
问题原因:
SimpleDateFormat
对象包含一个Calendar
实例,用于存储日期和时间信息。- 当
SimpleDateFormat
对象被多个线程同时使用时,它们可能会修改Calendar
实例的内部状态,从而导致日期和时间格式不一致。
所以 SimpleDateFormat 实例不是线程安全的,我们应该在并发环境中谨慎使用它们。
解决这个问题的最佳方法是将它们与ThreadLocal结合使用。这样,每个线程最终都有自己的 SimpleDateFormat 实例,并且缺乏共享使得程序线程安全:
java
private final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal
.withInitial(() -> new SimpleDateFormat("dd-MM-yyyy"));
使用 ThreadLocal
: 将 SimpleDateFormat
对象存储在 ThreadLocal
中,这样每个线程都会拥有自己的 SimpleDateFormat
实例:
java
private static ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
withInitial 方法的参数 是SimpleDateFormat 实例的供应商 。每次 ThreadLocal 需要创建实例时,它都会使用这个供应商。
然后我们可以通过ThreadLocal实例使用格式化程序:
java
formatter.get().format(date)
ThreadLocal.get() 方法 首先为当前线程初始化SimpleDateFormat,然后重用该实例。
我们将这种技术称为线程限制,因为我们将每个实例的使用限制在一个特定的线程中。
还有另外两种方法可以解决同一问题:
- 使用 同步 块或 ReentrantLock
- 按需创建
SimpleDateFormat
的废弃实例
这两种方法都不推荐:前者在争用较高时会对性能造成很大影响,而后者会创建大量对象,给垃圾收集带来压力。
值得一提的是,从 Java 8 开始,引入了一个新的DateTimeFormatter类。新的DateTimeFormatter类是不可变的和线程安全的。如果我们使用 Java 8 或更高版本,建议使用新的*DateTimeFormatter类。
3. 解析日期
SimpleDateFormat 和 DateFormat 不仅允许我们格式化日期 - 而且我们还可以反转操作。使用 parse 方法,我们可以输入 日期的 字符串 表示形式并返回等效的 Date 对象:
java
SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy");
Date myDate = new Date(233276400000L);
Date parsedDate = formatter.parse("24-05-1977");
assertEquals(myDate.getTime(), parsedDate.getTime());复制
这里需要注意的是,构造函数中提供的模式应该与使用 解析方法 解析的日期格式相同。
4.日期时间模式
SimpleDateFormat 在格式化日期时提供了大量不同的选项。虽然完整列表可在JavaDocs中找到,但让我们探索一些更常用的选项:
日期组件返回的输出也在很大程度上取决于字符串 中 使用的字符数。例如,我们以六月为例。如果我们将日期字符串定义为:**
arduino
"MM"复制
那么我们的结果将显示为数字代码 - 06。但是,如果我们在日期字符串中添加另一个 M:
arduino
"MMM"复制
然后我们得到的格式化日期显示为单词Jun。
5. 应用区域设置
SimpleDateFormat 类还支持多种语言环境, 这些 语言环境是在调用构造函数时设置的。****
让我们通过用法语格式化日期来实践一下。我们将实例化一个 SimpleDateFormat 对象,同时将 Locale.FRANCE 提供给构造函数。
java
SimpleDateFormat franceDateFormatter = new SimpleDateFormat("EEEEE dd-MMMMMMM-yyyy", Locale.FRANCE);
Date myFriday = new Date(1539341312904L);
assertTrue(franceDateFormatter.format(myFriday).startsWith("vendredi"));复制
通过提供给定日期(星期三下午),我们可以断言我们的 franceDateFormatter 已正确格式化日期。新日期正确地以 Vendredi 开头- 法语中的星期五!
值得注意的是构造函数的 Locale 版本中的一个小问题 -虽然支持许多语言环境,但不能保证完全覆盖。Oracle 建议在 DateFormat 类上使用工厂方法 来确保语言环境覆盖。
6. 改变时区
由于 SimpleDateFormat 扩展了 DateFormat 类,我们还可以使用setTimeZone 方法 来操作时区。让我们来看看实际操作:
java
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));复制
在上面的例子中,我们 在同一个SimpleDateFormat 对象上为两个不同的时区提供了相同的日期 。我们还在模式 字符串 的末尾 添加了"Z"字符以指示时区差异 。然后为用户记录格式 方法的输出 。
点击运行,我们可以看到相对于两个时区的当前时间:
java
INFO: Friday 12-Oct-18 12:46:14+0100
INFO: Friday 12-Oct-18 07:46:14-0400
7. 总结
我们深入探讨了 SimpleDateFormat 的复杂性和它的线程非安全性问题。研究了如何实例化 SimpleDateFormat 以及模式 字符串 如何影响日期格式 。在最终尝试使用时区 之前,我们尝试过改变输出字符串的语言环境 。在多线程环境中,最好避免使用 SimpleDateFormat
对象,而是选择其他线程安全的替代方案,例如 DateTimeFormatter
或使用 ThreadLocal
来确保线程安全性。