SimpleDateFormat转换两位年'yy'少了100年?和defaultCenturyStartYear的值有关

最近遇到了一个哭笑不得的事情,生产上面一个日期207x年变成了197x年,少了100年,排查下来原因也是让人大跌眼镜,某位同学使用了SimpleDateForma类将一个两位数年的日期格式’yy/MM/dd’转换为Date类型,然后再转成’yyyy-MM-dd’字符串,而就是这个转换过程中丢掉了100年。

问题排查:

首先感觉比较奇怪的是这个场景肯定测试过,这么显眼的问题不可能没有发现,那么是否与年份有关系,于是试了一些两位数的日期年份,发现超过当前年20年之后就会变成19xx年,少了100年,而20年之内就是正常的20xx年,那么肯定是 SimpleDateFormat 类parse ‘yy’的过程中有相关设置,于是就去翻相关代码,发现了这样的逻辑:

SimpleDateFormatinitialize的时候会执行initializeDefaultCentury()方法,方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* Initialize the fields we use to disambiguate ambiguous years. Separate
* so we can call it from readObject().
*/
private void initializeDefaultCentury() {
calendar.setTimeInMillis(System.currentTimeMillis());
//注意此处将当前年年份减去80年
calendar.add( Calendar.YEAR, -80 );
parseAmbiguousDatesAsAfter(calendar.getTime());
}

/* Define one-century window into which to disambiguate dates using
* two-digit years.
*/
private void parseAmbiguousDatesAsAfter(Date startDate) {
defaultCenturyStart = startDate;
calendar.setTime(startDate);
//此处年份已经比当前少了80年
defaultCenturyStartYear = calendar.get(Calendar.YEAR);
}

可以看到,为了消除两位数的年的时间模糊,会去定义一个默认的世纪开始年份,默认值为当前年份向前80年,然后当执行parse方法时,会调用subParse方法,源码大致如下,只保留了一下相关逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* Private member function that converts the parsed date strings into
* timeFields. Returns -start (for ParsePosition) if failed.
* @param text the time text to be parsed.
* @param start where to start parsing.
* @param patternCharIndex the index of the pattern character.
* @param count the count of a pattern character.
* @param obeyCount if true, then the next field directly abuts this one,
* and we should use the count to know when to stop parsing.
* @param ambiguousYear return parameter; upon return, if ambiguousYear[0]
* is true, then a two-digit year was parsed and may need to be readjusted.
* @param origPos origPos.errorIndex is used to return an error index
* at which a parse error occurred, if matching failure occurs.
* @return the new start position if matching succeeded; -1 indicating
* matching failure, otherwise. In case matching failure occurred,
* an error index is set to origPos.errorIndex.
*/
private int subParse(String text, int start, int patternCharIndex, int count,
boolean obeyCount, boolean[] ambiguousYear,
ParsePosition origPos,
boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) {
Number number;
int value = 0;
ParsePosition pos = new ParsePosition(0);
pos.index = start;
......

parsing:
{
......
case PATTERN_YEAR: // 'y'
......

// If there are 3 or more YEAR pattern characters, this indicates
// that the year value is to be treated literally, without any
// two-digit year adjustments (e.g., from "01" to 2001). Otherwise
// we made adjustments to place the 2-digit year in the proper
// century, for parsed strings from "00" to "99". Any other string
// is treated literally: "2250", "-1", "1", "002".
if (count <= 2 && (pos.index - actualStart) == 2
&& Character.isDigit(text.charAt(actualStart))
&& Character.isDigit(text.charAt(actualStart + 1))) {
// Assume for example that the defaultCenturyStart is 6/18/1903.
// This means that two-digit years will be forced into the range
// 6/18/1903 to 6/17/2003. As a result, years 00, 01, and 02
// correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
// to 1904, 1905, etc. If the year is 03, then it is 2003 if the
// other fields specify a date before 6/18, or 1903 if they specify a
// date afterwards. As a result, 03 is an ambiguous year. All other
// two-digit years are unambiguous.
int ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
ambiguousYear[0] = value == ambiguousTwoDigitYear;
value += (defaultCenturyStartYear/100)*100 +
(value < ambiguousTwoDigitYear ? 100 : 0);
}
calb.set(field, value);
return pos.index;

......
// Parsing failed.
origPos.errorIndex = pos.index;
return -1;
}

如果’yy’的值比当前年份减去80年的defaultCenturyStartYear后两位小,那么取defaultCenturyStartYear两位补齐’yy’,并且再加上100年,否则直接补齐不加100,那么对于’71’,当前defaultCenturyStartYear为1941,71大于41,所以最终就变成了1971,所以问题原因就在于defaultCenturyStartYear这个值默认是当前年份减去80的年,当然这个值也能修改

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Sets the 100-year period 2-digit years will be interpreted as being in
* to begin on the date the user specifies.
*
* @param startDate During parsing, two digit years will be placed in the range
* <code>startDate</code> to <code>startDate + 100 years</code>.
* @see #get2DigitYearStart
* @since 1.2
*/
public void set2DigitYearStart(Date startDate) {
parseAmbiguousDatesAsAfter(new Date(startDate.getTime()));
}

结论:

所以,当解析两位年份的时候,SimpleDateFormatparse方法会自动补齐前两位,补齐的规则是先初始化一个世纪开始年份,默认是当前日期减去80年的年份,然后补齐的年份会处于这个世纪开始年份的100年内,不能超过,因此就出现了超过当前20年的两位年份被补齐成过去的日期,少了100年,当然这个世纪开始年份也可以进行修改,设置成当前世纪的开始年份,这样日期都会补齐为当前世纪。