繁体   English   中英

GregorianCalendar的奇怪行为

[英]Strange behaviour with GregorianCalendar

我刚刚遇到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),但我想知道这真的是想要的行为。 有什么想法吗 ?

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

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

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

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

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

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

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

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

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

我想提出现代的答案。

    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

如果您只需要在某个月内计算天数(这是在一个重复的问题中提出的 ):

    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上使用Android版的ThreeTen Backport。 它被称为ThreeTenABP。 并确保从子包中导入org.threeten.bp的日期和时间类。

链接

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

日历从当前日期开始 - 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天

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM