JDK-6358365 : (cal) REGRESSION: NPE in GregCal getActualMaximum() in JDK1.5.0_06 (serialization)
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util:i18n
  • Affected Version: 5.0
  • Priority: P3
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux
  • CPU: x86
  • Submitted: 2005-12-02
  • Updated: 2010-07-29
  • Resolved: 2005-12-05
Related Reports
Duplicate :  
Description
FULL PRODUCT VERSION :
java version "1.5.0_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_06-b05)
Java HotSpot(TM) Server VM (build 1.5.0_06-b05, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux [host] 2.4.21-37.ELsmp #1 SMP Wed Sep 7 13:28:55 EDT 2005 i686 i686 i386 GNU/Linux


A DESCRIPTION OF THE PROBLEM :
This was working in 1.5.0_05, and is broken in 1.5.0_06: If you serialize and then de-serialize a GregorianCalendar object, calling getActualMaximum() or getActualMinimum() will cause a NullPointerException to be thrown. As an example we have line 1608 of GregorianCalendar.getActualMaximum():

1605: GregorianCalendar gc = getNormalizedCalendar();
1606: BaseCalendar.Date date = gc.cdate;
1607: BaseCalendar cal = gc.calsys;
1608: int normalizedYear = date.getNormalizedYear();

The "date" variable is null on line 1608, as the call to getNormalizedCalendar() did not set the "cdate" field of "gc": In getNormalizedCalendar() we have:

2604: gc = (GregorianCalendar) this.clone();
2605: gc.setLenient(true);
2606: gc.complete();

Line 2604 will clone "this", where "this" has a null "cdate": it is a transient field that was lost when we serialized the object. In the "cdate" declaration in GregorianCalendar, there is a comment stating that it is *guaranteed* to be set after complete() is called (as we do on line 2606, above). The complete() method belongs to Calendar, and calls back into GregorianCalendar.computeFields(). The diff for computeFields() from 1.5.0_05 to 1.5.0_06 is:

1963,1967c1963,1967
<           mask |= computeFields(fieldMask,
<                                 (mask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK))
<                                 == (ZONE_OFFSET_MASK|DST_OFFSET_MASK) ? fields : null,
<                                 ZONE_OFFSET);
<           assert mask == ALL_FIELDS;
---
>           if (fieldMask != 0) {
>               mask |= computeFields(fieldMask,
>                                     mask & (ZONE_OFFSET_MASK|DST_OFFSET_MASK));
>               assert mask == ALL_FIELDS;
>           }

The lower section of code is 1.5.0_06, and when called "fieldMask" is 0. On the surface it makes sense to not call GregorianDate.computeFields() when "fieldMask" is 0, since there are no fields to compute. However, computeFields() has a side effect:

2065: cdate = gdate;
[...or...]
2070: cdate = (BaseCalendar.Date) jcal.newCalendarDate(getZone());

That is, it sets our "cdate" field, preventing the NullPointerException we originally received. In the code from 1.5.0_05 in the above diff, we see that computeFields() is always called, even when not needed, as it sets inner state of the class.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1) Create a GregorianCalendar object and set its time;
2) Serialize the object;
3) De-serialize the object; and
4) Call getActualMaximum on the new object.

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
% javac Test.java; java Test
Before serialization: 31
Serialized OK
After serialization: 31
ACTUAL -
% javac Test.java; java Test
Before serialization: 31
Serialized OK
After serialization: Exception in thread "main" java.lang.NullPointerException
        at java.util.GregorianCalendar.getActualMaximum(GregorianCalendar.java:1608)
        at Test.main(Test.java:27)

ERROR MESSAGES/STACK TRACES THAT OCCUR :
Exception in thread "main" java.lang.NullPointerException
        at java.util.GregorianCalendar.getActualMaximum(GregorianCalendar.java:1608)
        at Test.main(Test.java:27)

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

import java.io.*;
import java.util.*;


public class Test
{
    public static void main(String [] argv)
            throws Exception
    {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(new Date());
        System.err.println("Before serialization: " +
                calendar.getActualMaximum(Calendar.DATE));

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        new ObjectOutputStream(outputStream).writeObject(calendar);

        byte[] serialized = outputStream.toByteArray();
        System.err.println("Serialized OK");

        ObjectInputStream inputStream = new ObjectInputStream(
                 new ByteArrayInputStream(serialized));
        calendar = (GregorianCalendar)inputStream.readObject();

        System.err.print("After serialization: ");
        System.err.println(calendar.getActualMaximum(Calendar.DATE));
    }
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
After de-serializing a GregorianCalendar, call:

        calendar.setTime(calendar.getTime());

Although a simple workaround exists, it is not practical in our case. We are seeing the error after the unmarshalling of arguments when using remote J2EE session beans. As such we don't have much control over implementing the above workaround. A more complicated workaround could be created for our specific case, but we have decided to stay with 1.5.0_05 until the problem is fixed.

Release Regression From : 5.0u5
The above release value was the last known release where this 
bug was known to work. Since then there has been a regression.

Comments
EVALUATION 5.0u6 needs the same 6263644 fix. Closing this CR as a duplicate.
05-12-2005