前提

前一篇文章已经比较详细地介绍了JSR-310中新增的常用的日期时间类,在实际应用中,我们也十分关注这些日期时间类的格式化操作,更加通俗来说就是字符串和日期时间类的相互转换问题。下面先回顾一下Java旧有的日期时间类和字符串之间的转换方案,然后重点分析JSR-310中新增的常用的日期时间类和字符串之间的转换方案。

SimpleDateFormat

Java旧有的日期时间类格式化为字符串或者字符串基于模式(Pattern)解析为日期时间类完全依赖于java.text.DateFormat的实现类java.text.SimpleDateFormatSimpleDateFormat的基本功能是完备的,但是存在两个问题:

  • 解析和格式化的效率比较低,原因是依赖了本来就效率不高的Calendar,内部有大量的字符串或者字符(char)的判断和转换代码,因此使用了大量循环、switch块等,这些因素都导致了SimpleDateFormat的效率比较低。
  • 非线程安全,这个是因为SimpleDateFormat在做转换操作的时候共享了DateFormat的一个内部Calendar的成员calendar。

效率低是可以忍受的,但是非线程安全这一点可能会导致严重的问题。对于非线程安全这个问题也有解决方案:

  • 方案一:把SimpleDateFormat实例封闭在方法中,也就是调用的时候才创建,这样虽然导致了资源浪费,但是可以避免并发问题。
  • 方案二:使用ThreadLocal装载SimpleDateFormat实例,对于同一个线程来说,共享一个SimpleDateFormat实例。

举个简单的使用例子:

public class SimpleDateFormatMain {

public static void main(String[] args) throws Exception {
java.util.Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = simpleDateFormat.format(date);
System.out.println(dateString);
date = simpleDateFormat.parse(dateString);
System.out.println(date);
simpleDateFormat.applyPattern("yyyy-MM-dd");
dateString = simpleDateFormat.format(date);
System.out.println(dateString);
}
}
// 某个时刻的输出如下
2019-01-03 23:32:05
Thu Jan 03 23:32:05 CST 2019
2019-01-03

对于Java旧有的日期时间类,SimpleDateFormat是基本能够满足的,再加上有第三方库apache-common-lang3joda-time等的补足,格式化和解析的效率也会有所提高。

JSR-310日期时间类的格式化和解析

JSR-310日期时间类的格式化依赖于日期时间格式化器java.time.format.DateTimeFormatter,它有一个建造器类java.time.format.DateTimeFormatterBuilder

DateTimeFormatterBuilder

java.time.format.DateTimeFormatterBuilder用于构建日期时间类格式化器,它在设计的时候使用了链式结构,内部持有一个DateTimeFormatterBuilder类型的parent成员指向父DateTimeFormatterBuilder实例和一个DateTimeFormatterBuilder类型的active成员指向当前的DateTimeFormatterBuilder实例。还有一点比较重要的是:DateTimeFormatterBuilder实例内部维护了一个DateTimePrinterParser列表printerParsers,真正的解析工作是委托给对应的DateTimePrinterParser实例完成的,如果没有可用或者没有添加DateTimePrinterParser,那么解析或者格式化方法相当于空跑。接着看下DateTimeFormatterBuilder提供构建DateTimeFormatter时允许添加特性的方法。

解析风格配置

// 大小写敏感 - 默认
public DateTimeFormatterBuilder parseCaseSensitive()
// 大小写不敏感
public DateTimeFormatterBuilder parseCaseInsensitive()
// 严格 - 默认
public DateTimeFormatterBuilder parseStrict()
// 宽松
public DateTimeFormatterBuilder parseLenient()

默认值配置

// 基于TemporalField实例配置解析时候写入默认值,支持的TemporalField主要在ChronoField
public DateTimeFormatterBuilder parseDefaulting(TemporalField field, long value)

追加日期时间属性格式化符号控制配置

/**
* 对于每个日期时间字段格式化的控制,实际作用是添加一个DateTimePrinterParser的实现NumberPrinterParser
* TemporalField:日期时间字段类型实例,主要实现类为在ChronoField的枚举属性
* minWidth:打印最小长度限制,范围是[1,19]
* maxWidth:打印最大长度限制,范围是[1,19]
* SignStyle:符号风格,有NORMAL、ALWAYS、NEVER、NOT_NEGATIVE、EXCEEDS_PAD五种选择
* - NORMAL:严格模式下只接收负值,宽松模式下接收所有符号
* - ALWAYS:0会替换为'+',严格模式下不接收缺失的符号,宽松模式下缺失的符号会替换为一个正数
* - NEVER:只输出绝对的固定值,严格模式下不接收任何符号,宽松模式下只接收固定长度的符号
* - NOT_NEGATIVE:以异常的方式阻止负值,严格模式下不接收任何符号,宽松模式下只接收固定长度的符号
* - EXCEEDS_PAD:只输出超出宽度限制的符号,负数替换为'-',严格模式下只输出超出宽度限制的符号,宽松模式下缺失的符号会替换为一个正数
*/
public DateTimeFormatterBuilder appendValue(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle)
// 下面2个是重载方法
// minWidth = 1,maxWidth = 19,signStyle = SignStyle.NORMAL
public DateTimeFormatterBuilder appendValue(TemporalField field)
// minWidth = maxWidth = width,signStyle = SignStyle.NOT_NEGATIVE
public DateTimeFormatterBuilder appendValue(TemporalField field, int width)

追加基于基础值进行减少配置

// 例如field=YEAR,width=2,baseValue=2018,那么当前格式化的实例的有效值为[2018,2117],2019->1,2117->99
// width范围是[1,10],maxWidth范围是[1,10]
public DateTimeFormatterBuilder appendValueReduced(TemporalField field, int width, int maxWidth, int baseValue)
public DateTimeFormatterBuilder appendValueReduced(TemporalField field, int width, int maxWidth, ChronoLocalDate baseDate)

追加小数(点)配置

// decimalPoint = true则输出小数,minWidth范围是[0,9],maxWidth范围是[1,9]
public DateTimeFormatterBuilder appendFraction(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint)

追加文本格式配置

public DateTimeFormatterBuilder appendText(TemporalField field)
public DateTimeFormatterBuilder appendText(TemporalField field, TextStyle textStyle)
public DateTimeFormatterBuilder appendText(TemporalField field, Map<Long, String> textLookup)

追加瞬时时间配置

public DateTimeFormatterBuilder appendInstant()
public DateTimeFormatterBuilder appendInstant(int fractionalDigits)

追加时区相关的配置

// 时间偏移量如+01:00
public DateTimeFormatterBuilder appendOffsetId()
// 指定格式的时间偏移量
public DateTimeFormatterBuilder appendOffset(String pattern, String noOffsetText)
// 指定文本风格的本地时间偏移量
public DateTimeFormatterBuilder appendLocalizedOffset(TextStyle style)
public DateTimeFormatterBuilder appendZoneId()
public DateTimeFormatterBuilder appendZoneRegionId()
public DateTimeFormatterBuilder appendZoneOrOffsetId()
public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle)
public DateTimeFormatterBuilder appendZoneText(TextStyle textStyle, Set<ZoneId> preferredZones)
// 太平洋时间时区偏移量
public DateTimeFormatterBuilder appendGenericZoneText(TextStyle textStyle)
public DateTimeFormatterBuilder appendGenericZoneText(TextStyle textStyle, Set<ZoneId> preferredZones)
// 日历配置
public DateTimeFormatterBuilder appendChronologyId()
public DateTimeFormatterBuilder appendChronologyText(TextStyle textStyle)

追加本地日期时间配置

public DateTimeFormatterBuilder appendLocalized(FormatStyle dateStyle, FormatStyle timeStyle)

追加常量文字(字符串)配置

public DateTimeFormatterBuilder appendLiteral(char literal)
public DateTimeFormatterBuilder appendLiteral(String literal)

追加其他格式化器的属性到当期建造器

public DateTimeFormatterBuilder append(DateTimeFormatter formatter)
// 配置候选格式化器
public DateTimeFormatterBuilder appendOptional(DateTimeFormatter formatter)

追加通用格式配置

// pattern的解析基本包含了上面提到的其他种类的配置
public DateTimeFormatterBuilder appendPattern(String pattern)

上面只是分析完毕,实际上理解这些配置方法的成本还是挺高的,可以参考DateTimeFormatter中已经存在的一些静态变量ISO_LOCAL_TIMEISO_OFFSET_TIMEISO_LOCAL_DATE_TIME等学习怎么使用DateTimeFormatterBuilder

public static final DateTimeFormatter ISO_LOCAL_TIME;
static {
ISO_LOCAL_TIME = new DateTimeFormatterBuilder()
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.optionalStart()
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
.optionalStart()
.appendFraction(NANO_OF_SECOND, 0, 9, true)
.toFormatter(ResolverStyle.STRICT, null);
}

模仿上面的代码,我们做一个简单的例子:格式化用LocalDateTime存储的日期时间2018-1-5 15:30:30为”当前时间是:2018年1月5日 15时30分30秒,祝你生活愉快!”。

public class DateTimeFormatterBuilderMain {

public static void main(String[] args) throws Exception {
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
builder.appendLiteral("当前时间是:");
builder.appendValue(ChronoField.YEAR, 4);
builder.appendLiteral("年");
builder.appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NORMAL);
builder.appendLiteral("月");
builder.appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NORMAL);
builder.appendLiteral("日");
builder.appendLiteral(" ");
builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
builder.appendLiteral("时");
builder.appendLiteral(":");
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
builder.appendLiteral("分");
builder.appendLiteral(":");
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
builder.appendLiteral("秒");
builder.appendLiteral(",祝你生活愉快!");
DateTimeFormatter formatter = builder.toFormatter();
System.out.println(formatter.format(LocalDateTime.now()));

}
}
// 某个时刻执行后输出结果
当前时间是:20191515时:50分:50秒,祝你生活愉快!

从理论上来看,如果能够熟练使用上面分析过的规则,那么可以格式化或者反向解析任意格式的日期时间或者字符串。

DateTimeFormatter

java.time.format.DateTimeFormatter在设计上是一个不可变类,也就是它是线程安全的,DateTimeFormatter的静态方法和实例方法只要返回DateTimeFormatter类型,那么必定是一个新的实例。它主要职责是格式化日期时间。一般情况下,构造DateTimeFormatter实例可以使用它提供的静态工厂方法,这些静态方法如果不能满足需求,可以考虑使用DateTimeFormatterBuilder定制化建造DateTimeFormatter实例。常用的2个静态工厂方法是:

public static DateTimeFormatter ofPattern(String pattern)
public static DateTimeFormatter ofPattern(String pattern, Locale locale)

字符串pattern基本可以填写任意合法的日期时间格式,因为底层使用DateTimeFormatterBuilder#appendPattern()进行解析,例如:

DateTimeFormatter.ofPattern("yyyy-MM-dd")
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒")

至于日期时间实例的格式化,主要通过下面的两个方法:

public String format(TemporalAccessor temporal)
public void formatTo(TemporalAccessor temporal, Appendable appendable)

举个简单的例子:

public class DateTimeFormatterMain {

public static void main(String[] args) throws Exception {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
LocalDateTime localDateTime = LocalDateTime.now();
String value = formatter.format(localDateTime);
StringBuilder builder = new StringBuilder();
formatter.formatTo(localDateTime, builder);
System.out.println(value);
System.out.println(builder.toString());
}
}
// 某个时刻的输出
20190105162801
20190105162801

字符串反解析为日期时间类型的(parse)方法并不存在于DateTimeFormatter类中,parse方法存在于日期时间类自身之中,这样的设计才是合理的,思想和领域驱动的方向是一致的,这里用LocalDateTime为例:

// 使用DateTimeFormatter.ISO_LOCAL_DATE_TIME进行解析
public static LocalDateTime parse(CharSequence text)
// 使用传入的自定义DateTimeFormatter进行解析
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter)

举个简单的例子:

public class DateTimeFormatterMain {

public static void main(String[] args) throws Exception {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒");
String dateTime = "2019年01月05日 16时28分01秒";
LocalDateTime parseResult = LocalDateTime.parse(dateTime, formatter);
System.out.println(parseResult);
}
}
// 某个时刻的输出
2019-01-05T16:28:01

由于DateTimeFormatter实例创建的时候相对耗时,因此需要考虑避免多次创建DateTimeFormatter实例,可以考虑编写一个工具类,用哈希表缓存pattern -> DateTimeFormatter

// 这里只列举LocalDateTime和LocalDate的例子,其他的日期时间类可以以此类推
public enum DateTimeFormatUtils {

// 单例
SINGLETON;

private static final ConcurrentMap<String, DateTimeFormatter> FORMATTERS = new ConcurrentHashMap<>();

public String formatLocalDateTime(LocalDateTime value, String pattern) {
return getOrCreateDateTimeFormatter(pattern).format(value);
}

public LocalDateTime parseLocalDateTime(String value, String pattern) {
return LocalDateTime.parse(value, getOrCreateDateTimeFormatter(pattern));
}

public String formatLocalDate(LocalDate value, String pattern) {
return getOrCreateDateTimeFormatter(pattern).format(value);
}

public LocalDate parseLocalDate(String value, String pattern) {
return LocalDate.parse(value, getOrCreateDateTimeFormatter(pattern));
}

private DateTimeFormatter getOrCreateDateTimeFormatter(String pattern) {
return FORMATTERS.computeIfAbsent(pattern, DateTimeFormatter::ofPattern);
}
}

最后还要注意一点:格式化或者解析的时候使用的模式pattern必须是合法日期时间表示格式(例如年份用yyyy表示),并且严格区分日期时间、只有日期属性和只有时间属性三种不同的情况,如果使用yyyy-MM-dd HH:mm:ss模式创建的DateTimeFormatter去格式化LocalTime或者LocalDate,会抛出异常,异常的类型是DateTimeException或者其子类,属于运行时异常。

小结

在JavaEE开发中,特别在系统交互中,日期时间字段的转换是比较重要的。其实JSR-310中的日期时间API的格式化和解析和旧有的日期时间API的格式化和解析从本质上是没有区别的,都是字符串解析和转换的游戏,但是个人是推荐使用JSR-310中的日期时间API的格式化和解析,原因是:

  • 性能上有很大提升(直观上推测,没有做严格测试)。
  • 类库设计上更加合理。
  • 线程安全。

(本文完 e-a-2019-1-5 c-2-d)