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

时间:2009-08-11 04:14:15

标签: java date calendar simpledateformat

我发现了一种奇怪的行为,这让我感到很好奇,而且还没有令人满意的解释。

为简单起见,我已将我注意到的症状减少到以下代码:

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());
    }
}

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

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]

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

原谅可怕的非缠绕线,但这是比较两者的最简单方法。如果您滚动到大约2/3的路径,您将看到日历的YEAR值分别为1929和2009。 (还有一些其他差异,例如一年中的星期,星期几和DST偏移。)两者都显然是GregorianCalendar的实例,但它们之所以不同是令人费解的。

据我所知,格式化器在格式化传递给它的Date对象时生成准确。显然,正确的功能比正确的参考年份更重要,但差异仍然令人不安。我不认为我必须在一个全新的日期格式化程序上设置日历才能获得当前年份......

我在使用Java 5(OS X 10.4,PowerPC)和Java 6(OS X 10.6,Intel)的Mac上进行了测试,结果相同。由于这是一个Java库API,我认为它在所有平台上的行为都相同。对这里正在发生的事情的任何见解?

(注意:This SO question有些相关,但不一样。)


修改

以下答案都有助于解释这种行为。事实证明,SimpleDateFormat的Javadoc实际上在某种程度上证明了这一点:

  

“对于使用缩写年份模式(”y“或”yy“)进行解析,SimpleDateFormat必须解释相对于某个世纪的缩写年份。它通过将日期调整为在80年前和20年后的时间来实现SimpleDateFormat实例已创建。“

因此,他们不会对解析日期的年份感到满意,而是默认将内部日历设置为80年。这部分本身没有记录,但是当你知道它时,这些部分都是合适的。

5 个答案:

答案 0 :(得分:5)

我不确定为什么汤姆说“这与序列化有关”,但他有正确的路线:

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

这是SimpleDateFormat.java中的第813行,这个过程非常晚。到目前为止,年份是正确的(正如日期部分的其余部分一样),然后它减少了80.

啊哈!

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

/* 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
 * <code>startDate</code> to <code>startDate + 100 years</code>.
 * @see #get2DigitYearStart
 * @since 1.2
 */
public void set2DigitYearStart(Date startDate) {
    parseAmbiguousDatesAsAfter(startDate);
}

现在我看到发生了什么。彼得在评论“苹果和橘子”时说得对! SimpleDateFormat中的年份是“默认世纪”的第一年,即两位数年份字符串(例如“1/12/14”)被解释为的范围。见http://java.sun.com/j2se/1.4.2/docs/api/java/text/SimpleDateFormat.html#get2DigitYearStart%28%29

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

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

答案 1 :(得分:2)

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

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

答案 2 :(得分:2)

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

以下是一些代码来说明:

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());
    }
}

答案 3 :(得分:1)

通过SimpleDateFormat来看,它似乎与序列化有关:

/* 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());
}

答案 4 :(得分:0)

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

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

第一个为您提供了一个将String解析为Dates的工具,反之亦然 第二个是DateUtility,允许您操作日期

没有理由说应该提供类似的输出。

将其与以下内容进行比较

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

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