Java Serialization – Compatible and Incompatible Changes

Java serialization enables writing Java objects to the file systems for permanent storage or on the network to transfer to other applications. Serialization in Java is achieved with Serializable interface. Java Serializable interface guarantees the ability to serialize the objects. This interface recommends we use serialVersionUID also.

Now, even if you use both in the application classes, do you know what can break your design even now?? Let us identify the future changes in the class which will be compatible changes and others that will prove incompatible changes.

1. Incompatible Changes

Incompatible changes to classes are those changes for which the guarantee of interoperability cannot be maintained. The incompatible changes that may occur while evolving a class are given below (considering default serialization or deserialization):

  • Deleting fields – If a field is deleted in a class, the stream written will not contain its value. When the stream is read by an earlier class, the value of the field will be set to the default value because no value is available in the stream. However, this default value may adversely impair the ability of the earlier version to fulfill its contract.
  • Moving classes up or down the hierarchy – This cannot be allowed since the data in the stream appears in the wrong sequence.
  • Changing a non-static field to static or a non-transient field to transient – When relying on default serialization, this change is equivalent to deleting a field from the class. This version of the class will not write that data to the stream, so it will not be available to be read by earlier versions of the class. As when deleting a field, the field of the earlier version will be initialized to the default value, which can cause the class to fail in unexpected ways.
  • Changing the declared type of a primitive field – Each version of the class writes the data with its declared type. Earlier versions of the class attempting to read the field will fail because the type of the data in the stream does not match the type of the field.
  • Changing the writeObject or readObject method so that it no longer writes or reads the default field data or changing it so that it attempts to write it or read it when the previous version did not. The default field data must consistently either appear or not appear in the stream.
  • Changing a class from Serializable to Externalizable or vice-versa is an incompatible change since the stream will contain data that is incompatible with the implementation of the available class.
  • Changing a class from a non-enum type to an enum type or vice versa since the stream will contain data that is incompatible with the implementation of the available class.
  • Removing either Serializable or Externalizable is an incompatible change since when written it will no longer supply the fields needed by older versions of the class.
  • Adding the writeReplace or readResolve method to a class is incompatible if the behavior would produce an object that is incompatible with any older version of the class.

2. Compatible Changes

  • Adding fields – When the class being reconstituted has a field that does not occur in the stream, that field in the object will be initialized to the default value for its type. If class-specific initialization is needed, the class may provide a readObject() method to initialize the field to non-default values.
  • Adding classes – The stream will contain the type hierarchy of each object in the stream. Comparing this hierarchy in the stream with the current class can detect additional classes. Since there is no information in the stream from which to initialize the object, the class’s fields will be initialized to the default values.
  • Removing classes – Comparing the class hierarchy in the stream with that of the current class can detect that a class has been deleted. In this case, the fields and objects corresponding to that class are read from the stream. Primitive fields are discarded, but the objects referenced by the deleted class are created, since they may be referred to later in the stream. They will be garbage-collected when the stream is garbage-collected or reset.
  • Adding writeObject/readObject methods – If the version reading the stream has these methods then readObject is expected, as usual, to read the required data written to the stream by the default serialization. It should call defaultReadObject first before reading any optional data. The writeObject method is expected as usual to call defaultWriteObject to write the required data and then may write optional data.
  • Removing writeObject/readObject methods – If the class reading the stream does not have these methods, the required data will be read by default serialization, and the optional data will be discarded.
  • Adding java.io.Serializable – This is equivalent to adding types. There will be no values in the stream for this class so its fields will be initialized to default values. The support for subclassing non-serializable classes requires that the class’s super type have a no-arg constructor and the class itself will be initialized to default values. If the no-arg constructor is not available, the InvalidClassException is thrown.
  • Changing the access to a field – The access modifiers public, package, protected, and private have no effect on the ability of serialization to assign values to the fields.
  • Changing a field from static to non-static or transient to non-transient – When relying on default serialization to compute the serializable fields, this change is equivalent to adding a field to the class. The new field will be written to the stream but earlier classes will ignore the value since serialization will not assign values to static or transient fields.

3. SerialVersionUID

The serialVersionUID is a universal version identifier for a Serializable class. Deserialization uses this number to ensure that a loaded class corresponds exactly to a serialized object. If no match is found, then an InvalidClassException is thrown.

  • Always include it as a field, for example: “private static final long serialVersionUID = 7526472295622776147L; ” include this field even in the first version of the class, as a reminder of its importance.
  • Do not change the value of this field in future versions, unless you are knowingly making changes to the class which will render it incompatible with old serialized objects. If needed, follow the above given guidelines.

4. The readObject() and writeObject() Methods

  • Deserialization must be treated as any constructor: validate the object state at the end of deserializing – this implies that readObject() should almost always be implemented in Serializable classes, such that this validation is performed.
  • If constructors make defensive copies for mutable object fields, so must readObject().

5. Best Practices

  • Use javadoc’s @serial tag to denote Serializable fields.
  • The .ser extension is conventionally used for files representing serialized objects.
  • No static or transient fields undergo default serialization.
  • Extendable classes should not be Serializable, unless necessary.
  • Inner classes should rarely if ever, implement Serializable.
  • Container classes should usually follow the style of Hashtable, which implements Serializable by storing keys and values, as opposed to a large hash table data structure.

6. Sample Implementation

import java.io.Serializable;
import java.text.StringCharacterIterator;
import java.util.*;
import java.io.*;

public final class UserDetails implements Serializable {

/**
* This constructor requires all fields
*
* @param aFirstName
* contains only letters, spaces, and apostrophes.
* @param aLastName
* contains only letters, spaces, and apostrophes.
* @param aAccountNumber
* is non-negative.
* @param aDateOpened
* has a non-negative number of milliseconds.
*/
public UserDetails(String aFirstName, String aLastName, int aAccountNumber,
						Date aDateOpened)
{
  super();
  setFirstName(aFirstName);
  setLastName(aLastName);
  setAccountNumber(aAccountNumber);
  setDateOpened(aDateOpened);
  // there is no need here to call verifyUserDetails.
}

// The default constructor
public UserDetails() {
  this("FirstName", "LastName", 0, new Date(System.currentTimeMillis()));
}

public final String getFirstName() {
  return fFirstName;
}

public final String getLastName() {
  return fLastName;
}

public final int getAccountNumber() {
  return fAccountNumber;
}

/**
* Returns a defensive copy of the field so that no one can change this
* field.
*/
public final Date getDateOpened() {
  return new Date(fDateOpened.getTime());
}

/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setFirstName(String aNewFirstName) {
  verifyNameProperty(aNewFirstName);
  fFirstName = aNewFirstName;
}

/**
* Names must contain only letters, spaces, and apostrophes. Validate before
* setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setLastName(String aNewLastName) {
  verifyNameProperty(aNewLastName);
  fLastName = aNewLastName;
}

/**
* Validate before setting field to new value.
*
* @throws IllegalArgumentException
* if the new value is not acceptable.
*/
public final void setAccountNumber(int aNewAccountNumber) {
  validateAccountNumber(aNewAccountNumber);
  fAccountNumber = aNewAccountNumber;
}

public final void setDateOpened(Date aNewDate) {
  // make a defensive copy of the mutable date object
  Date newDate = new Date(aNewDate.getTime());
  validateAccountOpenDate(newDate);
  fDateOpened = newDate;
}

/**
* The client's first name.
*
* @serial
*/
private String fFirstName;

/**
* The client's last name.
*
* @serial
*/
private String fLastName;

/**
* The client's account number.
*
* @serial
*/
private int fAccountNumber;

/**
* The date the account was opened.
*
* @serial
*/
private Date fDateOpened;

/**
* Determines if a de-serialized file is compatible with this class.
* Included here as a reminder of its importance.
*/
private static final long serialVersionUID = 7526471155622776147L;

/**
* Verify that all fields of this object take permissible values
*
* @throws IllegalArgumentException
* if any field takes an unpermitted value.
*/
private void verifyUserDetails() {
  validateAccountNumber(fAccountNumber);
  verifyNameProperty(fFirstName);
  verifyNameProperty(fLastName);
  validateAccountOpenDate(fDateOpened);
}

/**
* Ensure names contain only letters, spaces, and apostrophes.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void verifyNameProperty(String aName) {
boolean nameHasContent = (aName != null) && (!aName.equals(""));
  if (!nameHasContent) {
    throw new IllegalArgumentException(
    "Names must be non-null and non-empty.");
  }

StringCharacterIterator iterator = new StringCharacterIterator(aName);
char character = iterator.current();
  while (character != StringCharacterIterator.DONE) {
    boolean isValidChar = (Character.isLetter(character)
    || Character.isSpaceChar(character) || character == ''');
    if (isValidChar) {
      // do nothing
    } else {
      String message = "Names can contain only letters, spaces, and apostrophes.";
      throw new IllegalArgumentException(message);
    }
    character = iterator.next();
  }
}

/**
* AccountNumber must be non-negative.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountNumber(int aAccountNumber) {
  if (aAccountNumber < 0) {
    String message = "Account Number must be greater than or equal to 0.";
    throw new IllegalArgumentException(message);
  }
}

/**
* DateOpened must be after 1970.
*
* @throws IllegalArgumentException
* if field takes an unpermitted value.
*/
private void validateAccountOpenDate(Date aDateOpened) {
  if (aDateOpened.getTime() < 0) {
    throw new IllegalArgumentException(
      "Date Opened must be after 1970.");
  }
}

/**
* Always treat deserialization as a full-blown constructor, by validating
* the final state of the de-serialized object.
*/
private void readObject(ObjectInputStream aInputStream)
throws ClassNotFoundException, IOException {
  // always perform the default deserialization first
  aInputStream.defaultReadObject();

  // make defensive copy of the mutable Date field
  fDateOpened = new Date(fDateOpened.getTime());

  // ensure that object state has not been corrupted or tampered with
  // malicious code
  verifyUserDetails();
}

/**
* This is the default implementation of writeObject. Customise if
* necessary.
*/
private void writeObject(ObjectOutputStream aOutputStream)
throws IOException {
  // perform the default serialization for all non-transient, non-static
  // fields
  aOutputStream.defaultWriteObject();
}
}

Let us see now how to do serialization and deserialization in Java.

7. Demo

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Calendar;
import java.util.Date;

public class TestUserDetails {

  public static void main(String[] args) {

    // Create new UserDetails object
    UserDetails myDetails = new UserDetails("Lokesh", "Gupta", 102825,
    new Date(Calendar.getInstance().getTimeInMillis()));

    // Serialization code
    try {
      FileOutputStream fileOut = new FileOutputStream("userDetails.ser");
      ObjectOutputStream out = new ObjectOutputStream(fileOut);
      out.writeObject(myDetails);
      out.close();
      fileOut.close();
    } catch (IOException i) {
      i.printStackTrace();
    }

    // deserialization code
    @SuppressWarnings("unused")
    UserDetails deserializedUserDetails = null;
    try {
      FileInputStream fileIn = new FileInputStream("userDetails.ser");
      ObjectInputStream in = new ObjectInputStream(fileIn);
      deserializedUserDetails = (UserDetails) in.readObject();
      in.close();
      fileIn.close();

      // verify the object state
      System.out.println(deserializedUserDetails.getFirstName());
      System.out.println(deserializedUserDetails.getLastName());
      System.out.println(deserializedUserDetails.getAccountNumber());
      System.out.println(deserializedUserDetails.getDateOpened());
    } catch (IOException ioe) {
      ioe.printStackTrace();
    } catch (ClassNotFoundException cnfe) {
      cnfe.printStackTrace();
    }
  }
}

The program output:

Lokesh
Gupta
102825
Wed Nov 21 15:06:34 GMT+05:30 2012

Happy Learning !!

References: http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html

Leave a Comment

  1. It’s important to understand that when an instance is serialized, the byte code for its class is not serialized. Serialization applies only to the data carried by the class. When serialized, the fully-qualified class name is included in the instance; the instance created upon deserialization is of the class with the same fully-qualified class name in the program that is reading the serialization stream.

    What does this mean?

    It means you have to be careful when refactoring! If you serialize data from one version of an application, then refactor the application to move one of the serialized classes from one package to another, deserialization will fail!

    It means you should avoid serialization of anonymous classes! Anonymous classes “have no name”, but when serializing an instance of one, Java will create a name using the file and the order of the class declaration in the file. This breaks deserialization if you remove an anonymous class declaration, or if you rearrange the code in a way that changes the lexical order of anonymous class declarations!

    Reply
    • Static variables are not serialized when an instance of a class is serialized, because they belong to the class, not to the instance.

      The Class class itself is Serializable, and my guess is that if its static members are Serializable, they will be serialized along with the Class — so you might be able to use that to serialize static variables — but the deserialized Class won’t replace the local instance of the Class of the same name, so you’d have to manually replace the static variables of the local Class after deserialization, and at that point you might as well just write a Class to hold non-static copies of the static variables of interest and serialize that instead.

      Reply
  2. Hi Lokesh,

    Nice work :) Every software developers dream to summarize all FAQ of JAVA at one place.

    I have been asked in an interview how to achieve Serialization if the object is Singleton.
    Any thought on this.

    Thanks a bunch.

    Reply
    • Give the object a method “protected Object readResolve() throws ObjectStreamException”.

      This method is called after deserialization of an instance from the ObjectInputStream is complete, but before the result is assigned to any variable. The default implementation returns the deserialized instance, but you can override it to return any Object (including ones that will break your code, so don’t do those); in your case, to return the Singleton.

      For instance, if you have a large number of instances of a Serializable class that delegates some computation “SomeFunction.doSomeFunction(int x, int y)” to a Singleton that implements Serializable and SomeFunction, as shown below. The readResolve implementation ensures that all class instances that referred to _singleton when serialized end up referring to (the active instance of) _singleton after deserialization, rather than each carrying its own anonymous copy.

      Reply
  3. Hi Lokesh,

    Can you please tell us how can we control the fields to be serialized with the help of readObject.
    I dont want to use transient .

    Reply
  4. Hi lokes, I have a query regarding serialization. As we know that at the time of deserialization the class constructor does not run. As far as i know if we deserialize our object, its constructors doesn’t get called, but default constructor of its parent will be called. My question is that at the time of the deserializtion, how the jvm creates the object without calling the constructor ??
    I serialized a class’s object and then deserialized it and compared the references. Both references were not same. Please can you help :)

    Reply
    • While serializing an object, the object’s type (class metadata) , field’s data types and field’s data values are converted into a series of bytes and written in form of bytes. This same information is used in reconstructing the object back during deserialization process.

      In fact, this is very interesting topic and I just thought to write a separate post on it. Wait till tomorrow. I will explain in detail.

      Reply
      • Good article. I have come across a framework where the bean objects which get stored in session are to be serialized (a kind of mandate). Few modern day frameworks involve concepts of passivation of data.

        Reply
      • in case the DTO object is changed as incompatible level later , what will happen to the old serialized session data (DTO object) in the server? is it get cleaned automatically? assume serialVersionUID is incremented to avoid mixing different versions as you mentioned in the related article.
        Congrats for your great texts!!!

        Reply
  5. I am understanding what text you have provided in this guide….It helping me a lot. but i could not able to understand the code thoroughly. Can you give me a suggestion for that and how to be a good programmer?

    Reply

Leave a Comment

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode