10

我遇到了一种奇怪的行为,这让我很好奇,但还没有令人满意的解释。

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

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]

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

原谅可怕的非环绕线,但这是比较两者的最简单方法。如果滚动到大约 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,我假设它在所有平台上的行为都相同。对这里正在发生的事情有任何见解吗?

(注意:这个 SO 问题有些相关,但不一样。)


编辑:

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

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

因此,他们并没有对解析日期的年份感到好奇,而是默认将内部日历设置回 80 年。该部分本身没有记录,但是当您了解它时,所有部分都可以组合在一起。

4

5 回答 5

6

我不确定为什么 Tom 说“这与序列化有关”,但他说得对:

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 的总空间。)

于 2009-08-11T06:09:06.367 回答
2

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

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

于 2009-08-11T05:58:04.393 回答
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());
    }
}
于 2009-08-11T06:05:01.383 回答
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());
}
于 2009-08-11T04:49:12.020 回答
0
System.out.println(new SimpleDateFormat().getCalendar());
System.out.println(new GregorianCalendar());

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

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

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

将其与以下内容进行比较

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

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

于 2009-08-11T05:27:50.647 回答