【问题标题】:Strange behaviour with GregorianCalendarGregorianCalendar 的奇怪行为
【发布时间】:2011-02-26 00:56:31
【问题描述】:

我刚刚在 GregorianCalendar 类中遇到了一个奇怪的行为,我想知道我是否真的在做坏事。

仅当初始化日期的月份的实际最大值大于我要将日历设置为的月份时才会附加。

这里是示例代码:

    // today is 2010/05/31  
    GregorianCalendar cal = new GregorianCalendar();

    cal.set(Calendar.YEAR, 2010);
    cal.set(Calendar.MONTH, 1); // FEBRUARY

    cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
    cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
    cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
    cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
    cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));

    return cal.getTime(); // => 2010/03/03, wtf

我知道问题是由于日历初始化日期是一个 31 天的月份(可能),这与设置为 2 月(28 天)的月份混淆了。修复很简单(只需在设置 year 和 month 之前将 day_of_month 设置为 1 ),但我想知道这真的是想要的行为。有什么想法吗?

【问题讨论】:

    标签: java calendar


    【解决方案1】:

    是的,这就是它的预期工作方式。如果您从具有精确日期的GregorianCalendar 开始,然后通过使其不一致来修改它,那么您不应该相信您获得的结果。

    根据有关getActualMaximum(..) 的文档,它指出:

    例如,如果此实例的日期为 2004 年 2 月 1 日,则 DAY_OF_MONTH 字段的实际最大值为 29,因为 2004 年是闰年,如果此实例的日期为 2005 年 2 月 1 日,则为 28 .

    所以它应该可以工作,但你必须为它提供一致的值。 2010 年 2 月 31 日 不正确,应用依赖于日期值的内容(如 getActualMaximum)无法正常工作。它应该如何自行修复?通过决定那个月是错误的?还是那一天是错误的?

    顺便说一句,每个人都说使用JodaTime.. :)

    【讨论】:

    • 如果您将月份设置为 2 月,正常的 API 会意识到这会使日期无效,因此需要对其进行调整。但是java.util.Calendar 有很多东西,但不是普通的API。请参阅此处的第一条评论,了解 JodaTime 是如何做到的:blog.smart-java.nl/blog/index.php/2010/01/20/…
    【解决方案2】:

    也许setLenient(boolean lenient) 会帮你解决。运行以下代码时出现异常。

    如果不是,Joda 是一个更好的答案。

    import java.util.Calendar;
    
    public class CalTest
    {
        public static void main(String[] args)
        {
            // today is 2010/05/31
            Calendar cal = Calendar.getInstance();
            cal.setLenient(false);
    
            cal.set(Calendar.YEAR, 2010);
            cal.set(Calendar.MONTH, 1); // FEBRUARY
    
            cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
            cal.set(Calendar.HOUR_OF_DAY, cal.getActualMaximum(Calendar.HOUR_OF_DAY));
            cal.set(Calendar.MINUTE, cal.getActualMaximum(Calendar.MINUTE));
            cal.set(Calendar.SECOND, cal.getActualMaximum(Calendar.SECOND));
            cal.set(Calendar.MILLISECOND, cal.getActualMaximum(Calendar.MILLISECOND));
    
            System.out.println(cal.getTime());
        }
    }
    

    【讨论】:

    【解决方案3】:

    我确定这不是想要的行为。我同样确定没有人在上课时真正考虑过这个用例。事实上,Calendar 在内部状态以及如何管理所有 set 方法中的所有潜在转换方面存在很大问题。

    如果您不能在项目中使用 JodaTime 或 JSR-310,请在使用 Calendar 类时进行大量单元测试。正如您在这种情况下所看到的,日历代码的行为会根据您运行代码的月份的哪一天(或一天中的什么时间)而有所不同。

    【讨论】:

      【解决方案4】:

      它正在获取当前日期/时间的实际最大值。 5 月有 31 天,比 2 月 28 日多 3 天,因此将移至 3 月 3 日。

      获取/创建后需要调用Calendar#clear()

      GregorianCalendar cal = new GregorianCalendar();
      cal.clear();
      // ...
      

      这会导致:

      Sun Feb 28 23:59:59 GMT-04:00 2010
      

      (根据我的时区,这是正确的)

      正如其中一个答案所说,java.util.CalendarDate 是史诗般的失败。在进行密集的日期/时间操作时考虑JodaTime

      【讨论】:

        【解决方案5】:

        原因应该是,MONTH 具有类似枚举的逻辑结构。您可以轻松填充和读取数组/集合/列表。由于国际化,它必须是可枚举的(间接访问)。 DAY 只是一个可直接访问的整数。这就是区别。

        【讨论】:

          【解决方案6】:

          日历从当天开始 - 在您的示例中为 2010 年 5 月 31 日。当您将月份设置为 2 月时,日期更改为 2010 年 2 月 31 日,标准化为 2010 年 3 月 3 日,因此 cal.getActualMaximum(Calendar.DAY_OF_MONTH) 为 3 月返回 31。

          Calendar c = Calendar.getInstance();
          c.set(Calendar.YEAR, 2010);
          c.set(Calendar.MONTH, Calendar.MAY);
          c.set(Calendar.DAY_OF_MONTH, 31);
          System.out.println(c.getTime());
          c.set(Calendar.MONTH, Calendar.FEBRUARY);
          System.out.println(c.getTime());
          

          输出:

          Mon May 31 20:20:25 GMT+03:00 2010
          Wed Mar 03 20:20:25 GMT+03:00 2010
          

          要修复你的代码,你可以添加 cal.clear();或在设置月份之前设置第 1..28 天

          【讨论】:

            【解决方案7】:

            问题是DAY_OF_MONTH是从1开始的,0的天数少了一天!

            【讨论】:

              【解决方案8】:

              我想提供现代答案。

                  ZonedDateTime endOfFebruary2010 = LocalDate.of(2010, Month.MARCH, 1)
                          .atStartOfDay(ZoneId.systemDefault())
                          .minusNanos(1);
                  System.out.println(endOfFebruary2010);
              

              在我的时区运行,这会打印:

              2010-02-28T23:59:59.999999999+01:00[欧洲/哥本哈根]

              无论您在年月的哪个时间运行,打印输出都是一样的。对时区的依赖可能是不幸的,但可以通过指定您想要的时区来修正,例如ZoneId.of("Asia/Oral")。我正在使用并推荐 java.time,这是现代 Java 日期和时间 API。

              如果您不可避免地需要一个老式的java.util.Date 对象(并且仅在这种情况下),请转换:

                  Date oldFashionedDate = Date.from(endOfFebruary2010.toInstant());
                  System.out.println(oldFashionedDate);
              

              2010 年 2 月 28 日星期日 23:59:59 CET

              如果您只需要某个月份的天数(这是在a duplicate question 中提出的):

                  YearMonth ym = YearMonth.of(2011, Month.FEBRUARY);
                  int numDays = ym.lengthOfMonth();
                  System.out.println(numDays);
              

              28

              据我了解,您真正的问题是:

              …我想知道这真的是想要的行为。有什么想法吗 ?

              我坚信,无参数 GregorianCalendar 构造函数返回当前日期和当前时间是想要的行为。而Calender.set() 仅设置您明确设置的字段并尝试保持其他字段不变。 2010 年 2 月 31 日溢出到 3 月,没有任何错误迹象,因为该月只有 28 天。这些设计决策的组合使我得出一个不可避免的结论:您观察到的行为是设计使然。

              如果您认为这是一个糟糕的设计,我们很多人都同意您的看法。这也是为什么 CalendarGregorianCalendar 的替代品在四年前出现了 java.time。你再也不需要使用Calendar了。

              保留:我们的工具仍然依赖于 Java 1.7

              java.time 在 Java 7 上运行良好。它至少需要 Java 6

              • 在 Java 8 及更高版本以及更新的 Android 设备上(据我所知,从 API 级别 26 开始)现代 API 是内置的。
              • 在 Java 6 和 7 中,获取 ThreeTen Backport,即新类的后向端口(对于 JSR 310,ThreeTen;请参阅底部的链接)。
              • 在(较旧的)Android 上使用 ThreeTen Backport 的 Android 版本。它被称为 ThreeTenABP。并确保从 org.threeten.bp 导入日期和时间类以及子包。

              链接

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-06-08
                • 2015-07-20
                • 2010-10-03
                • 2021-07-12
                • 2013-10-04
                相关资源
                最近更新 更多