开发者

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.

This only appends when the initialization date's month has an actualMaximum bigger than the month I'm going to set the calendar to.

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 t开发者_开发知识库he 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). 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. 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.

You need to call Calendar#clear() after obtaining/creating it:

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. Consider JodaTime when doing intensive date/time operations.


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.

According to the documentation about getActualMaximum(..) it states:

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.

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. 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.. :)


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.

If you can't use JodaTime or JSR-310 in your project, unit test heavily when using the Calendar class. 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. I get an exception when I run the code below.

If not, Joda is a better answer.

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]

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"). I am using and recommending java.time, the modern Java date and time API.

If you indispensably need an old-fashioned java.util.Date object (and only in this case), convert:

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

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

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. And that Calender.set() only sets the fields that you explicitly set and tries to keep other fields unchanged. And that February 31, 2010 overflows into March without any sign of error because there were only 28 days in the month. 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. You will never need to use Calendar again.

Reservation: Our tool is still dependent on Java 1.7

java.time works nicely on Java 7. It just requires at least 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.
  • 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).
  • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

Links

  • Oracle tutorial: Date Time explaining how to use java.time.
  • Java Specification Request (JSR) 310, where java.time was first described.
  • ThreeTen Backport project, the backport of java.timeto Java 6 and 7 (ThreeTen for JSR-310).
  • ThreeTenABP, Android edition of ThreeTen Backport
  • Question: How to use ThreeTenABP in Android Project, with a very thorough explanation.


The reason should be, that MONTH has an enumeration-like logical structure. 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. That's the difference.


Calendar starts with current day - 31 may 2010 in your example. 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.

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(); or set day 1..28 before setting month


The problem is that DAY_OF_MONTH is 1-based, day 0 is one day less!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜