简体   繁体   English

GregorianCalendar的奇怪行为

[英]Strange behaviour with GregorianCalendar

I just encountered a strange behaviour with the GregorianCalendar class, and I was wondering if I really was doing something bad. 我刚刚遇到GregorianCalendar类的一个奇怪的行为,我想知道我是不是真的做了坏事。

This only appends when the initialization date's month has an actualMaximum bigger than the month I'm going to set the calendar to. 这仅在初始化日期的月份的actualMaximum大于我要将日历设置为的月份时附加。

Here is the example code : 这是示例代码:

    // 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

I know the problem is caused by the fact that the calendar initialization date is a 31 day month ( may ), which mess with the month set to february (28 days). 我知道问题是由于日历初始化日期是31天月(可能),这与设置为2月(28天)的月份相混淆。 The fix is easy ( just set day_of_month to 1 before setting year and month ), but I was wondering is this really was the wanted behaviour. 修复很容易(在设置年份和月份之前将day_of_month设置为1),但我想知道这真的是想要的行为。 Any thoughts ? 有什么想法吗 ?

It is getting the actual maximums of the current date/time. 它获得当前日期/时间的实际最大值。 May has 31 days which is 3 more than 28 February and it will thus shift to 3 March. 5月有31天,比2月28日多3天,因此将转移到3月3日。

You need to call Calendar#clear() after obtaining/creating it: 获取/创建后,您需要调用Calendar#clear()

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

This results in: 这导致:

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

(which is correct as per my timezone) (根据我的时区,这是正确的)

As said in one of the answers, the java.util.Calendar and Date are epic failures. 如其中一个答案所述, java.util.CalendarDate是史诗般的失败。 Consider JodaTime when doing intensive date/time operations. 在进行密集的日期/时间操作时考虑JodaTime

Yes, this is how it is intended to work. 是的,这就是它的工作方式。 If you start from a GregorianCalendar that has a precise date and you modify it by making it inconsistent then you shouldn't trust the results you obtain. 如果您从具有精确日期的GregorianCalendar开始,并通过使其不一致来修改它,那么您不应该信任您获得的结果。

According to the documentation about getActualMaximum(..) it states: 根据有关getActualMaximum(..)的文档,它指出:

For example, if the date of this instance is February 1, 2004, the actual maximum value of the DAY_OF_MONTH field is 29 because 2004 is a leap year, and if the date of this instance is February 1, 2005, it's 28. 例如,如果此实例的日期是2004年2月1日,则DAY_OF_MONTH字段的实际最大值为29,因为2004年是闰年,如果此实例的日期是2005年2月1日,则为28。

So it is supposed to work but you have to feed it with consistent values. 所以它应该可以工作,但你必须用一致的值来提供它。 31 February 2010 is not correct and applying things that relies on the date value (like getActualMaximum ) can't work. 2010年2月31日不正确,并且应用依赖于日期值的内容(如getActualMaximum )无效。 How should it fix it by itself? 它应该如何解决? By deciding that month is wrong? 通过判断月份是错误的? or that the day is wrong? 或者那天错了?

By the way, as everyone always states use JodaTime .. :) 顺便说一句,因为每个人总是使用JodaTime .. :)

I'm sure it is not wanted behavior. 我敢肯定这不是通缉行为。 I'm just equally sure no one really thought that use case through when they made the class. 我同样确定在他们上课时没有人真正想过用例。 The fact of the matter is that Calendar has a very big problem with internal state and how it manages all of the potential transitions in all the set methods. 事实上,Calendar对内部状态以及它如何管理所有set方法中的所有潜在转换都有一个非常大的问题。

If you can't use JodaTime or JSR-310 in your project, unit test heavily when using the Calendar class. 如果您不能在项目中使用JodaTime或JSR-310,那么在使用Calendar类时要进行大量的单元测试。 As you can see in this case Calendar code behaves differently depending on what day of the month (or what time of the day) you run the code. 正如您在本案例中所看到的,日历代码的行为会有所不同,具体取决于您运行代码的某一天(或一天的哪个时间)。

Maybe setLenient(boolean lenient) will sort it out for you. 也许setLenient(boolean lenient)会为你排序。 I get an exception when I run the code below. 当我运行下面的代码时,我得到一个例外。

If not, Joda is a better answer. 如果没有,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());
    }
}

I should like to contribute the modern answer. 我想提出现代的答案。

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

Running in my time zone this prints: 在我的时区运行打印:

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

The printout is the same no matter the time of year and month you run it. 无论您运行它的年份和月份,打印输出都是相同的。 The dependency on time zone may be unfortunate, but can be mended by specifying which time zone you want, for example ZoneId.of("Asia/Oral") . 对时区的依赖可能是不幸的,但可以通过指定您想要的时区来修补,例如ZoneId.of("Asia/Oral") I am using and recommending java.time , the modern Java date and time API. 我正在使用并推荐java.time ,即现代Java日期和时间API。

If you indispensably need an old-fashioned java.util.Date object (and only in this case), convert: 如果你不可缺少地需要一个老式的java.util.Date对象(仅在这种情况下),请转换:

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

Sun Feb 28 23:59:59 CET 2010 Sun Feb 28 23:59:59 CET 2010

If you only needed a count of days in some month (this was asked in a duplicate question ): 如果您只需要在某个月内计算天数(这是在一个重复的问题中提出的 ):

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

28 28

As I understand, your real question was: 据我了解,你真正的问题是:

…I was wondering is this really was the wanted behaviour. ......我想知道这真的是想要的行为。 Any thoughts ? 有什么想法吗 ?

I strongly believe that it is wanted behaviour that the no-arg GregorianCalendar constructor returns the current day and the current time of day. 我强烈认为,无需参与的GregorianCalendar构造函数返回当前时间和当前时间。 And that Calender.set() only sets the fields that you explicitly set and tries to keep other fields unchanged. 并且Calender.set()仅设置您明确设置的字段并尝试保持其他字段不变。 And that February 31, 2010 overflows into March without any sign of error because there were only 28 days in the month. 2010年2月31日溢出到3月,没有任何错误迹象,因为本月只有28天。 The combination of these design decisions leads me to the inevitable conclusion: the behaviour you observed is by design. 这些设计决策的结合使我得出了不可避免的结论:您观察到的行为是设计的。

If you think this is a poor design, we are many that agree with you. 如果你认为这是一个糟糕的设计,我们很多人都同意你的意见。 This was also why the replacement for Calendar and GregorianCalendar came out with java.time four years ago now. 这也是为什么更换CalendarGregorianCalendar用出来java.time四年前了。 You will never need to use Calendar again. 您永远不需要再次使用Calendar

Reservation: Our tool is still dependent on Java 1.7 预订:我们的工具仍然依赖于Java 1.7

java.time works nicely on Java 7. It just requires at least Java 6 . java.time在Java 7上运行良好。它至少需要Java 6

  • In Java 8 and later and on newer Android devices (from API level 26, I'm told) the modern API comes built-in. 在Java 8及更高版本和更新的Android设备上(从API级别26,我被告知)现代API内置。
  • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom). 在Java 6和7中获取ThreeTen Backport,新类的后端端口(适用于JSR 310的ThreeTen;请参见底部的链接)。
  • On (older) Android use the Android edition of ThreeTen Backport. 在(较旧的)Android上使用Android版的ThreeTen Backport。 It's called ThreeTenABP. 它被称为ThreeTenABP。 And make sure you import the date and time classes from org.threeten.bp with subpackages. 并确保从子包中导入org.threeten.bp的日期和时间类。

Links 链接

The reason should be, that MONTH has an enumeration-like logical structure. 原因应该是,MONTH具有类似枚举的逻辑结构。 You can easyly fill and read Arrays/Collections/Lists. 您可以轻松填写​​和阅读数组/集合/列表。 Due to internationalization it has to be enumeratable (indirect access). 由于国际化,它必须是可枚举的(间接访问)。 DAY is just an direct accessible Integer. DAY只是一个可直接访问的Integer。 That's the difference. 这就是区别。

Calendar starts with current day - 31 may 2010 in your example. 日历从当前日期开始 - 2010年5月31日在您的示例中。 When you set month to February, date changes to 31 February 2010 which normalized to 3 March 2010, so cal.getActualMaximum(Calendar.DAY_OF_MONTH) returns 31 for March. 当您将月份设置为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());

output: 输出:

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

To fix you code, you can add cal.clear(); 要修复代码,可以添加cal.clear(); or set day 1..28 before setting month 或在设定月份前设定第1..28天

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

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

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