Michal Krasny
Michal Krasny

Reputation: 5916

Why GregorianCalendar.getTimeInMillis() changes the value of the instance?

I found a very strange behavior of GregorianCalendar.getTimeInMillis(), it seems that it changes the value of the instance content. In the following code you can see that two blocks of code differ in only one commented line, where getTimeInMillis() is called. Why is the result different when I uncomment the line?

With commented call the output is

2014-10-25T22:00:00Z -> 2014-10-26T22:00:00.000+01:00
2014-10-25T22:00:00Z -> 2014-10-27T00:00:00.000+01:00

but when I uncomment the getTimeInMillis() line, both results are the same:

2014-10-25T22:00:00Z -> 2014-10-27T00:00:00.000+01:00
2014-10-25T22:00:00Z -> 2014-10-27T00:00:00.000+01:00

Code:

package com.test;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

public class Main {

public static void main(String[] args) {
    try {
        XMLGregorianCalendar date1 = DatatypeFactory.newInstance()
                .newXMLGregorianCalendar("2014-10-25T22:00:00Z");
        XMLGregorianCalendar date2 = DatatypeFactory.newInstance()
                .newXMLGregorianCalendar("2014-10-25T22:00:00Z");
        int days = 1;

        GregorianCalendar gregorianCalendar1 = date1.toGregorianCalendar();
        // gregorianCalendar1.getTimeInMillis();  //UNCOMMENT THIS LINE TO GET A DIFFERENT RESULT
        gregorianCalendar1.setTimeZone(TimeZone.getDefault());
        gregorianCalendar1.add(Calendar.DAY_OF_MONTH, days);
        XMLGregorianCalendar newXMLGregorianCalendar1 = DatatypeFactory
                .newInstance().newXMLGregorianCalendar(gregorianCalendar1);
        System.out.printf("%s -> %s\n", date1, newXMLGregorianCalendar1);

        GregorianCalendar gregorianCalendar2 = date2.toGregorianCalendar();
        gregorianCalendar2.getTimeInMillis();
        gregorianCalendar2.setTimeZone(TimeZone.getDefault());
        gregorianCalendar2.add(Calendar.DAY_OF_MONTH, days);
        XMLGregorianCalendar newXMLGregorianCalendar2 = DatatypeFactory
                .newInstance().newXMLGregorianCalendar(gregorianCalendar2);
        System.out.printf("%s -> %s\n", date2, newXMLGregorianCalendar2);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Upvotes: 3

Views: 2298

Answers (2)

Marco13
Marco13

Reputation: 54649

It's a time zone change. Not on December 31st in Shanghai, but manually, in your code.

Particularly, you are changing the time zone after having forced the calendar to compute its fields (based on the "old" time zone). This messes up the internal state of the calendar. Of course, this should not be the case, but is only one of the many strange behaviors exposed by the Calendar classes - and, most likely, mainly caused by their mutability.

Some of the potential difficulties are also stated in a comment in the implementation of Calendar#setTimeZone:

* Consider the sequence of calls: 
* cal.setTimeZone(EST); cal.set(HOUR, 1); cal.setTimeZone(PST).
* Is cal set to 1 o'clock EST or 1 o'clock PST?  Answer: PST. 

You could possibly work around this by studying the source code of GregorianCalendar and trying to avoid the critical sequences of calls. But as others already have pointed out: The whole old Date/Time API is horribly broken. If you have the chance, you should consider using the new Date/Time API of Java 8 (or the Joda Time API, which is similar enough to Java 8 to make it easy to later change existing Joda-based code to Java 8 code).


Here is an example that demonstrates the difference between setting the time zone before the call to getTimeMillis and after the call to getTimeMillis:

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

public class GregorianCalendarTest {

    public static void main(String[] args) {
        String fromSettingTimeZoneBeforeCall = createString(true);
        String fromSettingTimeZoneAfterCall = createString(false);

        System.out.println("Before: "+fromSettingTimeZoneBeforeCall);
        System.out.println("After : "+fromSettingTimeZoneAfterCall);
    }

    private static String createString(boolean setTimeZoneBeforeCall)
    {
        try {
            XMLGregorianCalendar date = DatatypeFactory.newInstance()
                .newXMLGregorianCalendar("2014-10-25T22:00:00Z");
            int days = 1;

            GregorianCalendar gregorianCalendar = date.toGregorianCalendar();
            System.out.println("After creating: "+gregorianCalendar);

            if (!setTimeZoneBeforeCall)
            {
                gregorianCalendar.getTimeInMillis(); 
                System.out.println("After millis  : "+gregorianCalendar);
            }

            gregorianCalendar.setTimeZone(TimeZone.getDefault());
            System.out.println("After timezone: "+gregorianCalendar);

            if (setTimeZoneBeforeCall)
            {
                gregorianCalendar.getTimeInMillis(); 
                System.out.println("After millis  : "+gregorianCalendar);
            }

            gregorianCalendar.add(Calendar.DAY_OF_MONTH, days);
            System.out.println("After adding  : "+gregorianCalendar);

            XMLGregorianCalendar newXMLGregorianCalendar = DatatypeFactory
                .newInstance().newXMLGregorianCalendar(gregorianCalendar);
            System.out.println("After all     : "+gregorianCalendar);

            return newXMLGregorianCalendar.toString();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

EDIT: This behavior is also described in this bug report: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=5026826

Upvotes: 2

Ioannis Deligiannis
Ioannis Deligiannis

Reputation: 2719

Pre-Java 8 Calendar implementations have been under a lot of criticism for "weird" behavior. I think that this is due to the following documentation:

Getting and Setting Calendar Field Values The calendar field values can be set by calling the set methods. Any field values set in a Calendar will not be interpreted until it needs to calculate its time value (milliseconds from the Epoch) or values of the calendar fields. Calling the get, getTimeInMillis, getTime, add and roll involves such calculation.

Note that the toString() method is marked as debug-only:

Return a string representation of this calendar. This method is intended to be used only for debugging purposes, and the format of the returned string may vary between implementations. The returned string may be empty but may not be null.

Though this will not probably end-up in a bug (as long as you don't use toString() in actual logic), it is better to use Joda-Time or new Java-8 Date and Time

Upvotes: 2

Related Questions