【问题标题】:Why does a new SimpleDateFormat object contain calendar with the wrong year?为什么新的 SimpleDateFormat 对象包含错误年份的日历?
【发布时间】:2010-11-18 12:14:17
【问题描述】:

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

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

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 的 Javadocs 实际上在某种程度上记录了这一点:

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

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

【问题讨论】:

    标签: java date calendar simpledateformat


    【解决方案1】:

    我不确定 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 的总空间。)

    【讨论】:

    • Tom 被告知要进行序列化,因为评论中提到了 readObject(),这是一种用于反序列化对象的方法。但是,注释意味着 initializeDefaultCentury() 是一个单独的方法,因此它可以从 readObject() 调用——该行为并不严格与序列化相关。
    • 感谢您的深入回答。不过,@Peter 的“苹果和橙子”声称完全不同。实际发生的是 SimpleDateFormat 有意修改了它在内部存储的日历的年份——否则,它的日历将与刚刚创建的 GregorianCalendar 相同。 (此外,源代码中方法的行位置不是“早”或“晚”——重要的是它们被调用的顺序。)我发现 SimpleDateFormat 文档实际上解释了这一点,所以我不同意“效率高于清晰度”,但我接受这是最完整的答案。
    • 迟到是指呼叫序列中的“迟到”,而不是行号;或更清楚地说:“SimpleDateFormat 使用 Locale 对象来查找要构造的日历,构造(对于大多数 Locale)一个 GregorianCalendar,然后对其进行修改。彼得所说的是 SimpleDateFormat 有一个 GregorianCalendar,它用于存储其内部状态,而 GregorianCalendar 虽然可以获取,但不一定会与我们独立创建的日历具有相同的状态。
    • 是的,我同意你关于序列化的观点——代码被重构为序列化,但序列化不是/为什么/年份设置为(现在 - 80 年)。跨度>
    【解决方案2】:

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

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

    【讨论】:

    • 实际上,现在我知道我在寻找什么,我发现它在 API 中发布的。在格式模式字母表下,Year 的项目符号记录了“创建 SimpleDateFormat 实例之前 80 年和之后 20 年”的默认倾斜世纪。这与任何默认值一样合理,并且很有用。 (现在我不必担心它是否是错误!)如果get2DigitYearStart() 至少在其自己的文档中提到这一点会很好...... :-)
    【解决方案3】:

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

    【讨论】:

    • 良好的反馈。就我而言,可变的内部状态无关紧要,因为我将它用作私有变量并且从不允许其任何状态转义(它纯粹用于格式化 Unix 时间戳)。我也期待将 Joda Time 集成到 Java 7 中,但是对于手头的代码,尽可能简单(更少的外部 JAR)是可取的。谢谢!
    【解决方案4】:

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

    【讨论】:

    • 注释暗示 readObject() 也调用了这个方法,但是没有解释为什么...
    【解决方案5】:
    System.out.println(new SimpleDateFormat().getCalendar());
    System.out.println(new GregorianCalendar());
    

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

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

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

    与下面的比较

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

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

    【讨论】:

    • 实际上,您设计的代码示例毫无意义——我实际上是在比较 GregorianCalendar 的两个实例(注意第一行对 getCalendar() 的调用),而不是像 Date 和 String 这样完全不同的东西。我知道 SimpleDateFormat 和 Calendar 的作用。
    • Peter 的观点是,他的代码 exa,ple 也在比较同一事物的两个实例——java.lang.String——它们有两种不同的方式:一个是“顶级”字符串,另一个由 Date 对象生成(并表示该 Date 的内部结构)。他的示例和您的示例之间的唯一区别是 SimpleDateFormat 的日历 /is/ 是其内部状态的一部分,并且可能刚刚返回 String ——也就是说,在 toString() 之后类可能不保留对它的引用叫做。但它可能,如果不往里看,我们不知道它没有。
    • 感谢您的澄清,但我不同意 /only/ 的区别在于打印值是否是另一个对象内部状态的一部分。根据定义,他提供的两个字符串是不同的——一个是当前日期。我知道这两个日历可能并不相同,但我只关注一些小的差异,其他一切(大部分)相同。
    猜你喜欢
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    • 2019-04-10
    • 1970-01-01
    • 2018-09-20
    • 1970-01-01
    • 2018-04-20
    相关资源
    最近更新 更多