Before you begin

This unit is part of the “Intro to Java programming” learning path. Although the concepts discussed in the individual units are standalone in nature, the hands-on component builds as you progress through the units, and I recommend that you review the prerequisites, setup, and unit details before proceeding.

Unit objectives

  • Learn what object serialization is and why you’d use it
  • Know the syntax for making objects serializable, serializing objects, and deserializing objects
  • Be able to deal with different versions of the same object in a serialization scenario

What is object serialization

Serialization is a process whereby the state of an object and its metadata (such as the object’s class name and the names of its attributes) are stored in a special binary format. Putting the object into this format —serializing it — preserves all the information necessary to reconstitute (or deserialize) the object whenever you need to do so.

The two primary use cases for object serialization are:

  • Object persistence— storing the object’s state in a permanent persistence mechanism such as a database
  • Object remoting— sending the object to another computer or system

java.io.Serializable

The first step in making serialization work is to enable your objects to use the mechanism. Every object you want to be serializable must implement an interface called java.io.Serializable:


import java.io.Serializable;
public class Person implements Serializable {
  // etc...
}

In this example, the Serializable interface marks the objects of the Person class — and every subclass of Person— to the runtime as serializable.

Any attributes of an object that are not serializable cause the Java runtime to throw a NotSerializableException if it tries to serialize your object. You can manage this behavior by using the transient keyword to tell the runtime not to try to serialize certain attributes. In that case, you are responsible for making sure that the attributes are restored (if necessary) so that your object functions correctly.

Serializing an object

Now, try an example that combines what you learned about Java I/O in Unit 21 with what you’re learning now about serialization.

Suppose you create and populate a List of Employee objects and then want to serialize that List to an OutputStream, in this case to a file. That process is shown in Listing 1.

Listing 1. Serializing an object

public class HumanResourcesApplication {
  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();
  
  public static List<Employee> createEmployees() {
    List<Employee> ret = new ArrayList<Employee>();
    Employee e = new Employee("Jon Smith", 45, 175, 75, "BLUE", Gender.MALE, 
       "123‑45‑9999", "0001", BigDecimal.valueOf(100000.0));
    ret.add(e);
    //
    e = new Employee("Jon Jones", 40, 185, 85, "BROWN", Gender.MALE, "223‑45‑9999", 
       "0002", BigDecimal.valueOf(110000.0));
    ret.add(e);
    //
    e = new Employee("Mary Smith", 35, 155, 55, "GREEN", Gender.FEMALE, "323‑45‑9999", 
       "0003", BigDecimal.valueOf(120000.0));
    ret.add(e);
    //
    e = new Employee("Chris Johnson", 38, 165, 65, "HAZEL", Gender.UNKNOWN, 
       "423‑45‑9999", "0004", BigDecimal.valueOf(90000.0));
    ret.add(e);
    // Return list of Employees
    return ret;
  }
  
  public boolean serializeToDisk(String filename, List<Employee> employees) {
    final String METHOD_NAME = "serializeToDisk(String filename, List<Employee> employees)";
    
    boolean ret = false;// default: failed
    File file = new File(filename);
    try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) {
      log.info("Writing " + employees.size() + " employees to disk (using Serializable)...");
      outputStream.writeObject(employees);
      ret = true;
      log.info("Done.");

    } catch (IOException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
       file.getName() + ", message = " + e.getLocalizedMessage(), e);
    }
    return ret;
  }

The first step is to create the objects, which is done in createEmployees() using the specialized constructor of Employee to set some attribute values. Next, you create an OutputStream— in this case, a FileOutputStream— and then call writeObject() on that stream. writeObject() is a method that uses Java serialization to serialize an object to the stream.

In this example, you are storing the List object (and its contained Employee objects) in a file, but this same technique is used for any type of serialization.

To drive the code in Listing 1, you could use a JUnit test, as shown here:


public class HumanResourcesApplicationTest {

  private HumanResourcesApplication classUnderTest;
  private List<Employee> testData;
  
  @Before
  public void setUp() {
    classUnderTest = new HumanResourcesApplication();
    testData = HumanResourcesApplication.createEmployees();
  }
  @Test
  public void testSerializeToDisk() {
    String filename = "employees‑Junit‑" + System.currentTimeMillis() + ".ser";
    boolean status = classUnderTest.serializeToDisk(filename, testData);
    assertTrue(status);
  }

}

Deserializing an object

The whole point of serializing an object is to be able to reconstitute, or deserialize, it. Listing 2 reads the file you’ve just serialized and deserializes its contents, thereby restoring the state of the List of Employee objects.

Listing 2. Deserializing objects

public class HumanResourcesApplication {

  private static final Logger log = Logger.getLogger(HumanResourcesApplication.class.getName());
  private static final String SOURCE_CLASS = HumanResourcesApplication.class.getName();
  
  @SuppressWarnings("unchecked")
  public List<Employee> deserializeFromDisk(String filename) {
    final String METHOD_NAME = "deserializeFromDisk(String filename)";
    
    List<Employee> ret = new ArrayList<>();
    File file = new File(filename);
    int numberOfEmployees = 0;
    try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
      List<Employee> employees = (List<Employee>)inputStream.readObject();
      log.info("Deserialized List says it contains " + employees.size() + 
         " objects...");
      for (Employee employee : employees) {
        log.info("Read Employee: " + employee.toString());
        numberOfEmployees++;
      }
      ret = employees;
      log.info("Read " + numberOfEmployees + " employees from disk.");
    } catch (FileNotFoundException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "Cannot find file " + 
         file.getName() + ", message = " + e.getLocalizedMessage(), e);
    } catch (IOException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "IOException occurred, 
       message = " + e.getLocalizedMessage(), e);
    } catch (ClassNotFoundException e) {
      log.logp(Level.SEVERE, SOURCE_CLASS, METHOD_NAME, "ClassNotFoundException, 
         message = " + e.getLocalizedMessage(), e);
    }
    return ret;
  }
  
}

Again, to drive the code in Listing 2, you could use a JUnit test like this one:


public class HumanResourcesApplicationTest {
  
  private HumanResourcesApplication classUnderTest;
  
  private List<Employee> testData;
  
  @Before
  public void setUp() {
    classUnderTest = new HumanResourcesApplication();
  }
  
  @Test
  public void testDeserializeFromDisk() {
    String filename = "employees‑Junit‑" + System.currentTimeMillis() + ".ser";
    int expectedNumberOfObjects = testData.size();
    classUnderTest.serializeToDisk(filename, testData);
    List<Employee> employees = classUnderTest.deserializeFromDisk(filename);
    assertEquals(expectedNumberOfObjects, employees.size());
  }

}

For most application purposes, marking your objects as serializable is all you ever need to worry about when it comes to serialization. When you do need to serialize and deserialize your objects explicitly, you can use the technique shown in Listing 1 and Listing 2. But as your application objects evolve, and you add and remove attributes to and from them, serialization takes on a new layer of complexity.

serialVersionUID

In the early days of middleware and remote object communication, developers were largely responsible for controlling the “wire format” of their objects, which caused no end of headaches as technology began to evolve.

Suppose you added an attribute to an object, recompiled it, and redistributed the code to every computer in an application cluster. The object would be stored on a computer with one version of the serialization code but accessed by other computers that might have a different version of the code. When those computers tried to deserialize the object, bad things often happened.

Java serialization metadata — the information included in the binary serialization format — is sophisticated and solves many of the problems that plagued early middleware developers. But it can’t solve every problem.

Java serialization uses a property called serialVersionUID to help you deal with different versions of objects in a serialization scenario. You don’t need to declare this property on your objects; by default, the Java platform uses an algorithm that computes a value for it based on your class’s attributes, its class name, and position in the local galactic cluster. Most of the time, that algorithm works fine. But if you add or remove an attribute, that dynamically generated value changes, and the Java runtime throws an InvalidClassException.

To avoid this outcome, get in the habit of explicitly declaring a serialVersionUID:

import java.io.Serializable;
  public class Person implements Serializable {
  private static final long serialVersionUID = 20100515;
  // etc...
  }

I recommend using a scheme of some kind for your serialVersionUID version number (I’ve used the current date in the preceding example). And you should declare serialVersionUID as private static final and of type long.

You might be wondering when to change this property. The short answer is that you should change it whenever you make an incompatible change to the class, which usually means you’ve added or removed an attribute. If you have one version of the object on one computer that has the attribute added or removed, and the object gets remoted to a computer with a version of the object where the attribute is either missing or expected, things can get weird. This is where the Java platform’s built-in serialVersionUID check comes in handy.

As a rule of thumb, any time you add or remove features (meaning attributes or any other instance-level state variables) of a class, change its serialVersionUID. Better to get a java.io.InvalidClassException on the other end of the wire than an application bug that’s caused by an incompatible class change.

Test your understanding

  1. How do you make a class “serializable”?

    1. There’s no special requirement for using Java serialization; it is built right into the Java language.
    2. Every class that needs to be serialized must, at a minimum, implement the java.io.Serializable interface.
    3. To use Java serialization, you must implement hashCode() and equals().
    4. Attribute values must be declared in alphabetical order or else they cannot be properly serialized and deserialized.
    5. None of the above
  2. What happens if you try to serialize an object for which no serialVersionUID is declared on its class?

    1. The Java serialization runtime will compute a default value for this field based on the declaration of the class, and serialization proceeds normally.
    2. You’ll get a NotSerializableException when you try to serialize the object.
    3. The JVM will throw a NoSerialVersionUIDFieldDeclared exception when the class is loaded into memory.
    4. The object will be serialized, but it cannot be deserialized — as a security measure, in case an incompatible class change was made.
    5. None of the above
  3. Choose the correct declaration of serialVersionUID:

    1. public static final String serialVersionUID = "1";
    2. private long serialVersionUID = 1L;
    3. private static final long serialVersionUID = 12345L;
    4. private static final String serialVersionUID = "Sdflkjsdfgd0980980(DF)(*)90";
    5. None of the above
  4. Under what conditions would you want to regenerate serialVersionUID for one of your classes as a matter of best practice?

    1. serialVersionUID is generated automatically, so it never requires regenerating.
    2. If your class implements more than one method and three attributes, it’s probably a good idea.
    3. If you make any change to the class that makes a previous instance of the class incompatible, you should regenerate serialVersionUID so as not to unwittingly introduce bugs into your code.
    4. If you make a change to the class, even if you are certain the change is compatible with any previous instance of the class, you should regenerate serialVersionUID just to be safe.
    5. None of the above
  5. Suppose you need to add an attribute to your class that doesn’t need to be serialized. How do you let the Java serialization runtime know?

    1. You don’t need to. The serialization runtime will automatically detect and handle the unimportant attribute.
    2. You declare the attribute using the ignoreSerial keyword.
    3. You must write custom code to handle such attributes when they are deserialized.
    4. You declare the attribute using the transient keyword.
    5. None of the above
  6. Create two classes: Container and Contained. Make both classes serializable. Each must contain a single read-only attribute: name, a String. Generate a getter for the attribute. Container has an additional read-only attribute: contained, which is a Contained instance. Generate a getter for this attribute. Initialize the name attribute in each class’s constructor. Initialize the Contained instance in Container‘s constructor. Write a JUnit test harness called testContainer_problem6() that creates an instance of Container (set the name to anything you like) and serializes it to a disk file called Container.ser. Refresh your project and make sure the file shows up.

  7. Write a separate JUnit test case called testContainer_problem7() to read in the serialized object from Question 6, and verify that the contents are as you expect.

  8. Add a new String attribute to Container and set it to anything you like. Then rerun testContainer_problem7()only (it is important that you do not regenerate the Container.ser). What do you expect will happen? Explain your answer.

Check your answers

  1. How do you make a class “serializable”?

    1. There’s no special requirement for using Java serialization; it is built right into the Java language.
    2. Every class that needs to be serialized must, at a minimum, implement the java.io.Serializable interface.
    3. To use Java serialization, you must implement hashCode() and equals().
    4. Attribute values must be declared in alphabetical order or else they cannot be properly serialized and deserialized.
    5. None of the above
  2. What happens if you try to serialize an object for which no serialVersionUID is declared on its class?

    1. The Java serialization runtime will compute a default value for this field based on the declaration of the class, and serialization proceeds normally.
    2. You’ll get a NotSerializableException when you try to serialize the object.
    3. The JVM will throw a NoSerialVersionUIDFieldDeclared exception when the class is loaded into memory.
    4. The object will be serialized, but it cannot be deserialized — as a security measure, in case an incompatible class change was made.
    5. None of the above
  3. Choose the correct declaration of serialVersionUID:

    1. public static final String serialVersionUID = "1";
    2. private long serialVersionUID = 1L;
    3. private static final long serialVersionUID = 12345L;
    4. private static final String serialVersionUID = "Sdflkjsdfgd0980980(DF)(*)90";
    5. None of the above
  4. Under what conditions would you want to regenerate serialVersionUID for one of your classes as a matter of best practice?

    1. serialVersionUID is generated automatically, so it never requires regenerating.
    2. If your class implements more than one method and three attributes, it’s probably a good idea.
    3. If you make any change to the class that makes a previous instance of the class incompatible, you should regenerate serialVersionUID so as not to unwittingly introduce bugs into your code.
    4. If you make a change to the class, even if you are certain the change is compatible with any previous instance of the class, you should regenerate serialVersionUID just to be safe.
    5. None of the above
  5. Suppose you need to add an attribute to your class that doesn’t need to be serialized. How do you let the Java serialization runtime know?

    1. You don’t need to. The serialization runtime will automatically detect and handle the unimportant attribute.
    2. You declare the attribute using the ignoreSerial keyword.
    3. You must write custom code to handle such attributes when they are deserialized.
    4. You declare the attribute using the transient keyword.
    5. None of the above
  6. Create two classes: Container and Contained. Make both classes serializable. Each must contain a single read-only attribute: name, a String. Generate a getter for the attribute. Container has an additional read-only attribute: contained, which is a Contained instance. Generate a getter for this attribute. Initialize the name attribute in each class’s constructor. Initialize the Contained instance in Container‘s constructor. Write a JUnit test harness called testContainer_problem6() that creates an instance of Container (set the name to anything you like) and serializes it to a disk file called Container.ser. Refresh your project and make sure the file shows up.

    Container.java

     import java.io.Serializable;
    
     public class Container implements Serializable {
    
       private String name;
       private Contained contained;
    
     public Container(String name) {
       this.name = name;
       this.contained = new Contained("Contained:" + name);
     }
    
     public String getName() {
       return name;
     }
    
     public Contained getContained() {
       return contained;
     }
    
     }
    

    Contained.java

     import java.io.Serializable;
    
     public class Contained implements Serializable {
    
       private String name;
    
       public Contained(String name) {
         this.name = name;
       }
    
       public String getName() {
         return name;
       }
    
     }
    

    ContainerTest.java

     import java.io.FileOutputStream;
     import java.io.IOException;
     import java.io.ObjectOutputStream;
     import java.util.logging.Logger;
    
     import org.junit.Test;
    
     public class ContainerTest {
    
       private static final Logger log = Logger.getLogger(ContainerTest.class.getName());
    
       @Test
       public void testContainer_Problem6() {
         Container container = new Container("Some name");
         String file = "Container.ser";
         // Write the file out
         try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(file))) {
           outputStream.writeObject(container);
         } catch (IOException e) {
           log.severe("IOException occurred: " + e.getLocalizedMessage());
           e.printStackTrace();
         }
       }
     .
     .
     }
    
  7. Write a separate JUnit test case called testContainer_problem7() to read in the serialized object from Question 6, and verify that the contents are as you expect.

     import static org.junit.Assert.assertEquals;
    
     import java.io.FileInputStream;
     import java.io.FileOutputStream;
     import java.io.IOException;
     import java.io.ObjectInputStream;
     import java.io.ObjectOutputStream;
     import java.util.logging.Logger;
    
     import org.junit.Test;
    
     public class ContainerTest {
    
       private static final Logger log = Logger.getLogger(ContainerTest.class.getName());
    
     .
     .
     .
       @Test
       public void testContainer_Problem7() {
         Container container = new Container("Some name");
         String file = "Container.ser";
         // Now read the file back in
         try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file))) {
           container = (Container)inputStream.readObject();
           assertEquals("Some name", container.getName());
           assertEquals("Contained:Some name", container.getContained().getName());
         } catch (IOException e) {
           log.severe("IOException occurred: " + e.getLocalizedMessage());
           e.printStackTrace();
         } catch (ClassNotFoundException e) {
           log.severe("ClassNotFoundException occurred: " + e.getLocalizedMessage());
           e.printStackTrace();
         }
       }
    
     }
    
  8. Add a new String attribute to Container and set it to anything you like. Then rerun testContainer_problem7()only (it is important that you do not regenerate the Container.ser). What do you expect will happen? Explain your answer.

    Modified Container.java

     import java.io.Serializable;
    
     public class Container implements Serializable {
    
       private String name;
       private String name2;
       private Contained contained;
    
       public Container(String name) {
         this.name = name;
         this.name2 = name;
         this.contained = new Contained("Contained:" + name);
       }
    
       public String getName() {
         return name;
      }
    
      public Contained getContained() {
         return contained;
      }
     }
    

    Prediction: Because we have not declared a serialVersionUID field, the Java serialization runtime declares one for us based on the class declaration (which includes all of the attributes). Since the class declaration has changed (i.e., we added an attribute), when we attempt to read in the serialized form of the original object (the one we serialized to disk without the new attribute, name2 in my solution), we will get a java.io.InvalidClassException.

    Output:

      Jul 29, 2016 10:55:45 PM com.makotojava.intro.ContainerTest testContainer_Problem7
     SEVERE: IOException occurred: com.makotojava.intro.Container; local class incompatible: stream classdesc serialVersionUID = -758871175043299543, local class serialVersionUID = -1206311183805350848
     java.io.InvalidClassException: com.makotojava.intro.Container; local class incompatible: stream classdesc serialVersionUID = -758871175043299543, local class serialVersionUID = -1206311183805350848
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:616)
     .
     .
     (more)
    

Previous: I/ONext: Java in the cloud