SimpleDateFormat源码分析及简单运用
原创博客不易,如若转载请注明来源
本文内容分为3部分:部分源码说明
、基本使用方式
、仿写 SimpleTimeFormat
文章目录
随便说点
java.text.SimpleDateFormat
在数据采集、数据清洗的中是一个非常常用的类。
它能做什么?将不同网站、app数据中五花八门的时间格式,统一、转化、格式化为你需要的相同的格式。存入我们的库中。
它是一个线程不安全的类。为什么?那么我们应该怎么用它?下文会一一介绍。
源码分析
- 基本原理图
-
这里先对基本原理做一个介绍:
大家都知道,在使用
SimpleDateFormat
的时候需要先传入字符串形式的Pattern
。并向外提供两类方法:
-
parse
根据给定的
pattern
,从指定的字符串中解析出java.util.Date
对象,也就是时间对象。 -
format
按照给定的
pattern
,把指定的java.util.Date
对象,格式化你想要的格式。
-
-
然后会得到3个疑问:
- 代码中是怎样解析并存储传入的
pattern
字符串? parse
的时候是怎样利用解析后pattern
,解析一段字符串的?format
的时候是怎么利用pattern
,将一个Date
对象,格式化为你想要的格式的?
- 代码中是怎样解析并存储传入的
-
我们带着问题去看:
compile
这里解释问题一:怎样解析并存储 pattern?
在 SimpleDateFormat 中,会将传入的原始 pattern 和解析后的 compiledPattern,放在成员变量中。
这个 compiledPattern
在使用中 至关重要。
有两个位置可以传入原始 pattern:
- 构造方法。
- applyPattern
我们看一下这两个地方的代码:
-
构造方法:
可以看到,pattern 直接赋值。而
compiledPattern
则通过 另一个方法compile
编译得到。public SimpleDateFormat(String pattern, Locale locale) { if (pattern == null || locale == null) { throw new NullPointerException(); } initializeCalendar(locale); this.pattern = pattern; this.formatData = DateFormatSymbols.getInstanceRef(locale); this.locale = locale; initialize(locale); } private void initialize(Locale loc) { // Verify and compile the given pattern. compiledPattern = compile(pattern); /* try the cache first */ numberFormat = cachedNumberFormatData.get(loc); if (numberFormat == null) { /* cache miss */ numberFormat = NumberFormat.getIntegerInstance(loc); numberFormat.setGroupingUsed(false); /* update cache */ cachedNumberFormatData.putIfAbsent(loc, numberFormat); } numberFormat = (NumberFormat) numberFormat.clone(); initializeDefaultCentury(); }
-
applyPattern
这个方法里就更明显了,pattern直接赋值,compiledPattern 通过
compile
得到。public void applyPattern(String pattern) { applyPatternImpl(pattern); } private void applyPatternImpl(String pattern) { compiledPattern = compile(pattern); this.pattern = pattern; }
那么我们简单看一个 compile 这个方法:
代码较长,具体分析看代码中我写的注解。
private char[] compile(String pattern) {
int length = pattern.length();
boolean inQuote = false;
// 缓存buffer用来存储编译过的每一个字节
StringBuilder compiledCode = new StringBuilder(length * 2);
StringBuilder tmpBuffer = null;
int count = 0, tagcount = 0;
int lastTag = -1, prevTag = -1;
for (int i = 0; i < length; i++) {
char c = pattern.charAt(i);
// 在引号中的特殊字符处理,例如 'mm'
if (c == '\'') {
// '' is treated as a single quote regardless of being
// in a quoted section.
if ((i + 1) < length) {
c = pattern.charAt(i + 1);
if (c == '\'') {
i++;
if (count != 0) {
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
lastTag = -1;
count = 0;
}
if (inQuote) {
tmpBuffer.append(c);
} else {
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
}
continue;
}
}
if (!inQuote) {
if (count != 0) {
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
lastTag = -1;
count = 0;
}
if (tmpBuffer == null) {
tmpBuffer = new StringBuilder(length);
} else {
tmpBuffer.setLength(0);
}
inQuote = true;
} else {
int len = tmpBuffer.length();
if (len == 1) {
char ch = tmpBuffer.charAt(0);
if (ch < 128) {
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | ch));
} else {
compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | 1));
compiledCode.append(ch);
}
} else {
encode(TAG_QUOTE_CHARS, len, compiledCode);
compiledCode.append(tmpBuffer);
}
inQuote = false;
}
continue;
}
if (inQuote) {
tmpBuffer.append(c);
continue;
}
// 分隔符处理,非ymhs等字符
if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
if (count != 0) {
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
lastTag = -1;
count = 0;
}
if (c < 128) {
// ascii 字符处理
// In most cases, c would be a delimiter, such as ':'.
compiledCode.append((char)(TAG_QUOTE_ASCII_CHAR << 8 | c));
} else {
// 非 ascii 处理,存储字符长度,非ascii字符拼接在后面
// Take any contiguous non-ASCII alphabet characters and
// put them in a single TAG_QUOTE_CHARS.
int j;
for (j = i + 1; j < length; j++) {
char d = pattern.charAt(j);
if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) {
break;
}
}
compiledCode.append((char)(TAG_QUOTE_CHARS << 8 | (j - i)));
for (; i < j; i++) {
compiledCode.append(pattern.charAt(i));
}
i--;
}
continue;
}
// 日期占位符处理,例如yhms
int tag;
if ((tag = DateFormatSymbols.patternChars.indexOf(c)) == -1) {
throw new IllegalArgumentException("Illegal pattern character " +
"'" + c + "'");
}
if (lastTag == -1 || lastTag == tag) {
lastTag = tag;
count++;
continue;
}
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
lastTag = tag;
count = 1;
}
if (inQuote) {
throw new IllegalArgumentException("Unterminated quote");
}
if (count != 0) {
encode(lastTag, count, compiledCode);
tagcount++;
prevTag = lastTag;
}
forceStandaloneForm = (tagcount == 1 && prevTag == PATTERN_MONTH);
// Copy the compiled pattern to a char array
int len = compiledCode.length();
char[] r = new char[len];
compiledCode.getChars(0, len, r, 0);
return r;
}
emmmmm 注释看完了,就是这么解析的。
format
这里解释问题三:format 是怎么 format 的?
具体看代码中的注释。
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// 日期缓存在Calendar对象中,供下文使用
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
// 遍历 compiledPattern
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8; // 右移8位,得到标签数字
int count = compiledPattern[i++] & 0xff; // 得到附加内容,标签长度 or ascii字符 or 特殊字符长度
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
// ascii 字符直接追加
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
// 特殊字符,根据上文长度,截取、追加
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
// 日期占位符,取Calendar对象中的对应字段,追加
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
emmm。就是这样 format的。
parse
这里解释问题二:parse 是怎么 parse 的?
关键在于 parse 方法,代码较长,具体内容看以下代码中的注视。
// 根据 ParsePosition 记录的index,进行字符串截取。根据tag的单位,对截取的字符串进行换算
public Date parse(String text, ParsePosition pos)
{
checkNegativeNumberExpression();
int start = pos.index;
int oldStart = start;
int textLength = text.length();
boolean[] ambiguousYear = {false};
CalendarBuilder calb = new CalendarBuilder();
// 遍历compiledPattern
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
// ascii 字符,index++
if (start >= textLength || text.charAt(start) != (char)count) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
break;
case TAG_QUOTE_CHARS:
// 非ascii特殊字符,index递增,所需要增加的数值
while (count-- > 0) {
if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
pos.index = oldStart;
pos.errorIndex = start;
return null;
}
start++;
}
break;
default:
// Peek the next pattern to determine if we need to
// obey the number of pattern letters for
// parsing. It's required when parsing contiguous
// digit text (e.g., "20010704") with a pattern which
// has no delimiters between fields, like "yyyyMMdd".
boolean obeyCount = false;
// In Arabic, a minus sign for a negative number is put after
// the number. Even in another locale, a minus sign can be
// put after a number using DateFormat.setNumberFormat().
// If both the minus sign and the field-delimiter are '-',
// subParse() needs to determine whether a '-' after a number
// in the given text is a delimiter or is a minus sign for the
// preceding number. We give subParse() a clue based on the
// information in compiledPattern.
boolean useFollowingMinusSignAsDelimiter = false;
if (i < compiledPattern.length) {
int nextTag = compiledPattern[i] >>> 8;
if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
obeyCount = true;
}
if (hasFollowingMinusSign &&
(nextTag == TAG_QUOTE_ASCII_CHAR ||
nextTag == TAG_QUOTE_CHARS)) {
int c;
if (nextTag == TAG_QUOTE_ASCII_CHAR) {
c = compiledPattern[i] & 0xff;
} else {
c = compiledPattern[i+1];
}
if (c == minusSign) {
useFollowingMinusSignAsDelimiter = true;
}
}
}
// 根据position的index进行字符串截取。并根据tag单位换算时间
start = subParse(text, start, tag, count, obeyCount,
ambiguousYear, pos,
useFollowingMinusSignAsDelimiter, calb);
if (start < 0) {
pos.index = oldStart;
return null;
}
}
}
// At this point the fields of Calendar have been set. Calendar
// will fill in default values for missing fields when the time
// is computed.
pos.index = start;
Date parsedDate;
try {
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
// An IllegalArgumentException will be thrown by Calendar.getTime()
// if any fields are out of range, e.g., MONTH == 17.
catch (IllegalArgumentException e) {
pos.errorIndex = start;
pos.index = oldStart;
return null;
}
return parsedDate;
}
emmmmm. 看完了。就是这么parse的。
简单使用
本文来自hu-jinwen,请勿随意转载
时间占位符含义
字符 | 含义 |
---|---|
y | 年 |
M | 月 |
d | 日 |
h | 小时(12小时制) |
H | 小时(24小时制) |
m | 分 |
s | 秒 |
S | 毫秒 |
// 构造方法
// 无参构造方法
SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
// 构造,并且传入pattern格式
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
// 切换 pattern
simpleDateFormat.applyPattern("yyyy-MM-dd hh:mm:ss");
// 格式化时间
String timeStr = simpleDateFormat.format(new Date());
// 解析时间
Date resultDate = simpleDateFormat.parse("2018年12月21日 18时21分00秒");
多线程环境下的使用
不难发现,SimpleDateFormat 是线程不安全的。内部将 compiledPattern 保存在成员变量中,没有做任何线程同步。且用到了 Calendar。
要么每次都 new 一个对象,效率太慢了。
要么自己做线程同步,效率更慢。
这里推荐使用ThreadLocal
:
在用 ThreadLocal 的时候,需要注意 ThreadLocal 容易发生的内存泄漏的问题,这里不细讲了
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT_HOLDER = ThreadLocal.withInitial(SimpleDateFormat::new);
仿写SimpleTimeFormat
有意思的来了,
我们可以使用 SimpleDateFormat
将Date格式化想要的格式、可以从字符串中解析出Date。
但是缺少一种这样的工具,
-
将 23分12秒 转化为 1392秒。
-
将 5小时4分钟23秒 转化为 5:4:23。
-
将 5小时4分钟23秒 转化为毫秒时间戳。
这里仿照SimpleDateFormat
的模式,写了一个SimpleTimeFormat
,作为其补充。
使用方式与 SimpleDateFormat 相同,经过一段时间的使用和测试……
源代码
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Created by hu-jinwen on 2018/12/18
* <p>
* 此类语法同 SimpleDateFormat
* h = 小时
* m = 分钟
* s = 秒
* S = 毫秒
* 此类非线程安全
*/
public class SimpleTimeFormat {
/**
* 原始pattern
*/
transient private String pattern;
/**
* 编译后的pattern
*/
transient private char[] compiledPattern;
/**
* ascii字符标签
*/
private static final int TAG_ASCII_CODE = 100;
/**
* 非 ascii 字符标签
*/
private static final int TAG_QUOTE_CODE = 101;
/**
* numberFormat 对象
*/
private final DecimalFormat numberFormat = new DecimalFormat("##");
public SimpleTimeFormat() {
this("hh:mm:ss");
}
public SimpleTimeFormat(String pattern) {
if (pattern == null || pattern.length() == 0) {
throw new NullPointerException("+++++>> The pattern must not be null!");
}
applyPattern(pattern);
}
/**
* 将时长、时间戳(毫秒),转化为指定格式
*/
public String format(Long duration) {
return format(duration, new StringBuilder()).toString();
}
public String format(int duration) {
return format((long) duration);
}
/**
* 从字符串中解析时间戳
*/
public Long parse(String source) {
return parse(source, new ParsePosition(0));
}
/**
* 应用指定pattern
*/
public void applyPattern(String pattern) {
this.compiledPattern = compile(pattern);
this.pattern = pattern;
}
/**
* 时间格式编译
*/
private char[] compile(String pattern) {
int length = pattern.length();
StringBuilder compiledBuffer = new StringBuilder(length * 2);
int count = 0;
int lastTag = -1;
for (int i = 0; i < length; i++) {
char c = pattern.charAt(i);
// 特殊字符处理(特殊字符使用引号括起来)
if (c == '\'') {
if (count != 0) {
compiledBuffer.append((char) (lastTag << 8 | count));
lastTag = -1;
count = 0;
}
int j;
for (j = ++i; j < length; j++) {
char d = pattern.charAt(j);
if (d == '\'') {
break;
}
}
compiledBuffer.append((char) (TAG_QUOTE_CODE << 8 | (j - i)));
for (; i < j; i++) {
compiledBuffer.append(pattern.charAt(i));
}
continue;
}
// 分隔符处理
if (!(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')) {
if (count != 0) {
compiledBuffer.append((char) (lastTag << 8 | count));
lastTag = -1;
count = 0;
}
if (c < 128) {
compiledBuffer.append((char) (TAG_ASCII_CODE << 8 | c));
} else {
// 非 ascii 字符处理
int j;
for (j = i + 1; j < length; j++) {
char d = pattern.charAt(j);
if (d == '\'' || (d >= 'a' && d <= 'z' || d >= 'A' && d <= 'Z')) {
break;
}
}
compiledBuffer.append((char) (TAG_QUOTE_CODE << 8 | (j - i)));
for (; i < j; i++) {
compiledBuffer.append(pattern.charAt(i));
}
i--;
}
continue;
}
// 非分隔符处理
int tag;
if ((tag = TimeFormatSymBols.patternChars.indexOf(c)) == -1) {
throw new IllegalArgumentException(
MessageFormat.format("Illegal pattern character -> {0}", c));
}
if (lastTag == -1 || lastTag == tag) {
lastTag = tag;
count++;
continue;
}
compiledBuffer.append((char) (tag << 8 | count));
lastTag = tag;
count = 1;
}
if (count != 0) {
compiledBuffer.append((char) (lastTag << 8 | count));
}
int bufLen = compiledBuffer.length();
char[] r = new char[bufLen];
compiledBuffer.getChars(0, bufLen, r, 0);
return r;
}
/**
* 格式化传入的毫秒时间
*/
private StringBuilder format(Long duration, StringBuilder resultBuffer) {
Long[] timeCache = getTimeCache(duration);
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
switch (tag) {
case TAG_ASCII_CODE:
resultBuffer.append((char) count);
break;
case TAG_QUOTE_CODE:
resultBuffer.append(compiledPattern, i, count);
i += count;
break;
default:
resultBuffer.append(timeCache[tag]);
break;
}
}
return resultBuffer;
}
/**
* 解析字符串时间
*/
private Long parse(String source, ParsePosition position) {
long result = 0L;
if (this.pattern.contains(".")) {
source = source.replace(".", ",");
}
for (int i = 0; i < compiledPattern.length; i++) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i] & 0xff;
switch (tag) {
case TAG_ASCII_CODE:
position.setIndex(position.getIndex() + 1);
break;
case TAG_QUOTE_CODE:
position.setIndex(position.getIndex() + count);
i += count;
break;
default:
// 处理数值前可能出现的空格和制表符
while (true) {
char c = source.charAt(position.getIndex());
if (c != ' ' && c != '\t') {
break;
}
position.setIndex(position.getIndex() + 1);
}
// 前瞻, 处理真实数字位数与pattern中的位数不匹配
while (true) {
int nextCharIndex = position.getIndex() + count;
char c = nextCharIndex >= source.length() ? 'n' : source.charAt(nextCharIndex);
if (!Character.isDigit(c)) {
if (nextCharIndex > source.length()) {
count--;
continue;
}
break;
}
count++;
}
Number number = numberFormat.parse(source.substring(0, position.getIndex() + count), position);
result += (TimeFormatSymBols.UNITS_MILLS[tag] * number.doubleValue());
break;
}
}
return result;
}
private Long[] getTimeCache(Long duration) {
Long[] results = new Long[TimeFormatSymBols.patternChars.length()];
List<Long> units = new ArrayList<>();
// 时间格式提取
for (char c : compiledPattern) {
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
continue;
}
int tag = c >>> 8;
if (tag < TimeFormatSymBols.patternChars.length()) {
units.add((long) tag);
}
}
Collections.sort(units);
for (Long tag : units) {
long value = duration / TimeFormatSymBols.UNITS_MILLS[tag.intValue()];
duration = duration % TimeFormatSymBols.UNITS_MILLS[tag.intValue()];
results[tag.intValue()] = value;
}
return results;
}
private static class TimeFormatSymBols {
/**
* 时间格式通配符
* <p>
* h = 小时
* m = 分钟
* s = 秒
* S = 毫秒
*/
static final String patternChars = "hmsS";
/**
* 单位毫秒数
*/
static final long[] UNITS_MILLS = {60 * 60 * 1000, 60 * 1000, 1000, 1};
}
}
测试代码
@Test
void formatTest() {
// 初始化可通过构造方法传入指定 pattern
SimpleTimeFormat simpleTimeFormat = new SimpleTimeFormat("hh小时mm分钟ss");
// 根据 pattern 将指定字符串解析为时间戳
Long timestamp = simpleTimeFormat.parse("5小时4分钟23秒");
// 也可手动修改 pattern
simpleTimeFormat.applyPattern("ss:mm:hh");
// 根据 pattern 格式化时间戳
String format = simpleTimeFormat.format(timestamp);
}