GregorianCalendar的奇怪行为

时间:2010-05-31 15:47:03

标签: java calendar

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

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

以下是示例代码:

    // 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天)的月份相混淆。修复很容易(在设置年份和月份之前将day_of_month设置为1),但我想知道这真的是想要的行为。有什么想法吗?

8 个答案:

答案 0 :(得分:14)

获取当前日期/时间的实际最大值。 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

答案 1 :(得分:2)

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

根据getActualMaximum(..)的文件说明:

  

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

所以它应该可以工作,但你必须用一致的值来提供它。 2010年2月31日不正确,并且应用依赖于日期值的内容(如getActualMaximum)无效。它应该如何解决?通过判断月份是错误的?或者那天错了?

顺便说一下,每个人总是说使用JodaTime ..:)

答案 2 :(得分:2)

我确定这不是通缉行为。我同样确定在他们上课时没有人真正想过用例。事实上,Calendar对内部状态以及它如何管理所有设置方法中的所有潜在转换都有一个非常大的问题。

如果您不能在项目中使用JodaTime或JSR-310,请在使用Calendar类时进行大量测试。正如您在本案例中所看到的,日历代码的行为会有所不同,具体取决于您运行代码的某一天(或一天中的哪个时间)。

答案 3 :(得分:1)

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

答案 4 :(得分:1)

我想提供现代答案。

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

Sun Feb 28 23:59:59 CET 2010

如果您只需要在某个月内计算天数(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;请参阅底部的链接)。
  • On(较旧)Android使用Android版的ThreeTen Backport。它被称为ThreeTenABP。并确保使用子包从org.threeten.bp导入日期和时间类。

链接

答案 5 :(得分:0)

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

答案 6 :(得分:0)

日历从当前日期开始 - 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 :(得分:0)

问题在于DAY_OF_MONTH是从1开始的,而0日则少了一天!