前一篇文章已经比较详细地介绍了JSR-310中新增的常用的日期时间类,在实际应用中,我们也十分关注这些日期时间类的格式化操作,更加通俗来说就是字符串和日期时间类的相互转换问题。下面先回顾一下Java旧有的日期时间类和字符串之间的转换方案,然后重点分析JSR-310中新增的常用的日期时间类和字符串之间的转换方案。
Java旧有的日期时间类格式化为字符串或者字符串基于模式(Pattern
)解析为日期时间类完全依赖于java.text.DateFormat
的实现类java.text.SimpleDateFormat
。SimpleDateFormat
的基本功能是完备的,但是存在两个问题:
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-lang3
、joda-time
等的补足,格式化和解析的效率也会有所提高。
JSR-310
日期时间类的格式化依赖于日期时间格式化器java.time.format.DateTimeFormatter
,它有一个建造器类java.time.format.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_TIME
、ISO_OFFSET_TIME
、ISO_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()));
}
}
// 某个时刻执行后输出结果
当前时间是:2019年1月5日 15时:50分:50秒,祝你生活愉快!
从理论上来看,如果能够熟练使用上面分析过的规则,那么可以格式化或者反向解析任意格式的日期时间或者字符串。
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());
}
}
// 某个时刻的输出
2019年01月05日 16时28分01秒
2019年01月05日 16时28分01秒
字符串反解析为日期时间类型的(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)
本文是Throwable的原创文章,转载请提前告知作者并且标明出处。 博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议 本文永久链接是:https://cloud.tencent.com/developer/article/1649971