关于Java:为什么新的SimpleDateFormat对象包含错误年份的日历?

Why does a new SimpleDateFormat object contain calendar with the wrong year?

我突然发现了一种奇怪的行为,这让我很好奇,至今还没有一个令人满意的解释。

为了简单起见,我将我注意到的症状简化为以下代码:

1
2
3
4
5
6
7
8
9
import java.text.SimpleDateFormat;
import java.util.GregorianCalendar;

public class CalendarTest {
    public static void main(String[] args) {
        System.out.println(new SimpleDateFormat().getCalendar());
        System.out.println(new GregorianCalendar());
    }
}

当我运行此代码时,我得到与以下输出非常相似的结果:

1
2
java.util.GregorianCalendar[time=-1274641455755,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=1929,MONTH=7,WEEK_OF_YEAR=32,WEEK_OF_MONTH=2,DAY_OF_MONTH=10,DAY_OF_YEAR=222,DAY_OF_WEEK=7,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=8,HOUR_OF_DAY=20,MINUTE=55,SECOND=44,MILLISECOND=245,ZONE_OFFSET=-28800000,DST_OFFSET=0]
java.util.GregorianCalendar[time=1249962944248,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="America/Los_Angeles",offset=-28800000,dstSavings=3600000,useDaylight=true,transitions=185,lastRule=java.util.SimpleTimeZone[id=America/Los_Angeles,offset=-28800000,dstSavings=3600000,useDaylight=true,startYear=0,startMode=3,startMonth=2,startDay=8,startDayOfWeek=1,startTime=7200000,startTimeMode=0,endMode=3,endMonth=10,endDay=1,endDayOfWeek=1,endTime=7200000,endTimeMode=0]],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2009,MONTH=7,WEEK_OF_YEAR=33,WEEK_OF_MONTH=3,DAY_OF_MONTH=10,DAY_OF_YEAR=222,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=8,HOUR_OF_DAY=20,MINUTE=55,SECOND=44,MILLISECOND=248,ZONE_OFFSET=-28800000,DST_OFFSET=3600000]

(如果我提供有效的格式字符串(如"yyyy-MM-dd"to simpledateformat),也会发生同样的事情。)

原谅那些可怕的非包装线,但这是比较两者的最简单方法。如果滚动到2/3左右,您将看到日历的年份值分别为1929年和2009年。(还有一些其他差异,如一年中的某周、某周中的某一天和DST偏移量。)这两个都是公历日历的明显例子,但它们差异的原因令人费解。

据我所知,格式化程序在格式化传递给它的日期对象时会产生准确的结果。显然,正确的功能比正确的参考年份更重要,但这种差异仍然令人不安。我不认为我需要在一个全新的日期格式设置工具上设置日历来获取当前年份…

我已经用Java 5(OS X 10.4,PowerPC)和Java 6(OS X 10.6,英特尔)在MACS上测试了这一点,结果也一样。由于这是一个Java库API,所以我假设它在所有平台上都是相同的。有什么见解吗?

(注:这个问题有些关联,但不尽相同。)

编辑:

下面的答案有助于解释这种行为。事实证明,simpledateformat的javadocs实际上在某种程度上记录了这一点:

"For parsing with the abbreviated year pattern ("y" or"yy"), SimpleDateFormat must interpret the abbreviated year relative to some century. It does this by adjusting dates to be within 80 years before and 20 years after the time the SimpleDateFormat instance is created."

所以,他们不喜欢解析日期的年份,而是默认将内部日历设置为80年。这部分本身并没有记录,但是当你知道它的时候,所有的部分都结合在一起。


我不知道汤姆为什么说"这与序列化有关",但他说得对:

1
2
3
4
5
private void initializeDefaultCentury() {
    calendar.setTime( new Date() );
    calendar.add( Calendar.YEAR, -80 );
    parseAmbiguousDatesAsAfter(calendar.getTime());
}

它是simpledateformat.java中的813行,这一过程非常晚。到那时为止,年份是正确的(和日期部分的其余部分一样),然后它会减少80。

啊哈!

parseAmbiguousDatesAsAfter()的调用与set2DigitYearStart()调用的私有函数相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Define one-century window into which to disambiguate dates using
 * two-digit years.
 */

private void parseAmbiguousDatesAsAfter(Date startDate) {
    defaultCenturyStart = startDate;
    calendar.setTime(startDate);
    defaultCenturyStartYear = calendar.get(Calendar.YEAR);
}

/**
 * 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
 * <wyn>startDate</wyn> to <wyn>startDate + 100 years</wyn>.
 * @see #get2DigitYearStart
 * @since 1.2
 */

public void set2DigitYearStart(Date startDate) {
    parseAmbiguousDatesAsAfter(startDate);
}

现在我明白了。彼得对"苹果和桔子"的评论是对的!SimpleDateFormat中的年份是"默认世纪"的第一年,即两位数年份字符串(例如,"1/12/14")被解释为的范围。参见http://java.xun.com /J2SE/1.4.2/DOCS/API/Java/TeX/SimuleDeaFr.html GET2DigialStime% 28% 29:

因此,在"效率"超过"清晰"的胜利中,simpledateFormat中的年份用于存储"分析两位数年份的100年期间的开始",而不是当前年份!

谢谢,这很有趣——最后让我安装了JDK源代码(我的/分区上只有4GB的总空间)。


simpledateformat具有可变的内部状态。这就是为什么我像避免瘟疫一样避免它(我推荐乔达时间)。这个内部日历可能是在解析日期的过程中使用的,但是没有理由在解析日期之前将其初始化为任何特定的内容。

下面是一些需要说明的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;

public class DateTest {
    public static void main(String[] args) {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat();
        System.out.println("sdf cal:" + simpleDateFormat.getCalendar());
        System.out.println("new cal:" + new GregorianCalendar());
        System.out.println("new date:" + simpleDateFormat.format(new Date()));
        System.out.println("sdf cal:" + simpleDateFormat.getCalendar());
    }
}


你在调查内部行为。如果这超出了已发布的API的范围,那么您将看到未定义的内容,您不应该关心它。

除此之外,我相信1929年是用来考虑何时将两位数的年份解释为19xx而不是20xx。


通过simpledateformat,似乎它与序列化有关:

1
2
3
4
5
6
7
8
/* Initialize the fields we use to disambiguate ambiguous years. Separate
 * so we can call it from readObject().
 */

private void initializeDefaultCentury() {
    calendar.setTime( new Date() );
    calendar.add( Calendar.YEAR, -80 );
    parseAmbiguousDatesAsAfter(calendar.getTime());
}


1
2
System.out.println(new SimpleDateFormat().getCalendar());
System.out.println(new GregorianCalendar());

比较上面的代码就是比较苹果和梨

第一个提供了将字符串解析为日期的工具,反之亦然第二个是一个日期实用程序,它允许您操作日期

没有真正的原因可以解释为什么应该提供类似的输出。

将其与以下内容进行比较

1
2
System.out.println(new String() );
System.out.println(new Date().toString() );

两行都将输出一个字符串,但逻辑上您不会期望相同的结果