Object serialization

Introduction

At a certain point during the development of a program of mine I decided it would be nice to save certain aspects of the program state on disk to be able to recall them the next time the program is started. For simplicity I will explore a simplified version of the stuff I serialized in real life.

Suppose we want to remember the following things about our program the moment the program is ended so that we are able to recall them later:

On this page I want to stay focussed on the topic of object serialization, so I will not go into the techniques needed to keep track of the properties mentioned above.

First I want to introduce the Options class to you. This is the class I use to hold the program state which I want to save. The Java code of this class is displayed below:

Class: Options.java

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 /**
009  * The <code>Options</code> class holds all program options which need to be
010  * saved when the user exits the program.
011  *
012  @author Patrick Holthuizen
013  */
014 public class Options {
015     // X-position of the main window
016     private Integer windowX = null;
017     // Y-position of the main window
018     private Integer windowY = null;
019     // Width of the main window
020     private Integer windowWidth = null;
021     // Height of the main window
022     private Integer windowHeight = null;
023     // Extended window state of the main window
024     private Integer windowState = null;
025     
026     /**
027      * Creates a new instance of <code>Options</code>.
028      */
029     public Options() {
030     }
031     
032     /**
033      * Gets the X-location of the main window.
034      *
035      @return the X-location of the main window
036      */
037     public Integer getWindowX() {
038         return windowX;
039     }
040     
041     /**
042      * Sets the X-location of the main window.
043      *
044      @param windowX the X-location of the main window
045      */
046     public void setWindowX(Integer windowX) {
047         this.windowX = windowX;
048     }
049     
050     /**
051      * Gets the Y-location of the main window.
052      *
053      @return the Y-location of the main window
054      */
055     public Integer getWindowY() {
056         return windowY;
057     }
058     
059     /**
060      * Sets the Y-location of the main window.
061      *
062      @param windowY the Y-location of the main window
063      */
064     public void setWindowY(Integer windowY) {
065         this.windowY = windowY;
066     }
067     
068     /**
069      * Gets the width of the main window.
070      *
071      @return the width of the main window
072      */
073     public Integer getWindowWidth() {
074         return windowWidth;
075     }
076     
077     /**
078      * Sets the width of the main window.
079      *
080      @param windowWidth the width of the main window
081      */
082     public void setWindowWidth(Integer windowWidth) {
083         this.windowWidth = windowWidth;
084     }
085     
086     /**
087      * Gets the height of the main window.
088      *
089      @return the height of the main window
090      */
091     public Integer getWindowHeight() {
092         return windowHeight;
093     }
094     
095     /**
096      * Sets the height of the main window.
097      *
098      @param windowHeight the height of the main window
099      */
100     public void setWindowHeight(Integer windowHeight) {
101         this.windowHeight = windowHeight;
102     }
103     
104     /**
105      * Gets the extended window state of the main window.
106      *
107      @return the extended window state of the main window
108      */
109     public Integer getWindowState() {
110         return windowState;
111     }
112     
113     /**
114      * Sets the extended window state of the main window.
115      *
116      @param windowState the extended window state of the main window
117      */
118     public void setWindowState(Integer windowState) {
119         this.windowState = windowState;
120     }
121 }

The simple but not so flexible implementation

So you see a lot of not so important stuff, it's just a place holder for the program state with appropriate getters and setters. Now it's time to save this data to disk. Suppose we are on the verge to exit our program, for example through the window closing event. That would be a nice place to save an object of the Options class.

To save (i.e. serialize) the object we need to do at least one thing and that is implement the interface Serializable. The interface is in the package java.io so  I also inlude the statement

import java.io.*

in the class. So now the class looks like this:

01 /*
02  * Options.java
03  *
04  * Created on 24 juli 2005, 22:10
05  */
06 package main;
07 
08 import java.io.*;
09 
10 /**
11  * The <code>Options</code> class holds all program options which need to be
12  * saved when the user exits the program.
13  *
14  @author Patrick Holthuizen
15  */
16 public class Options implements Serializable {
17     // X-position of the main window
18     private Integer windowX = null;
19     // Y-position of the main window
20     private Integer windowY = null;
21     
22     /*** the rest of the class ***/

Note that the interface Serializable is a special interface as we don't need to implement any methods for it. The class Options as it is now may be serialized through the default Java serialization mechanism, for example like this:

Options options = new Options();
try {
    FileOutputStream os = new FileOutputStream("options.ext");
    ObjectOutputStream oos = new ObjectOutputStream(os);
    oos.writeObject(options);
    oos.close();
    os.close();
catch(Exception e) {
 e.printStackTrace();
}

Keep your class compatible with future versions

The example in the previous section works pretty cool. Just add a few keywords and the objects of your class can be persisted. The method used in the previous section has one huge drawback: serialized classes are not compatible when anything in the source of the class Options changes. An important advice: do not stop when you have implemented the example above, just keep on reading and you will be rewarded with a full backward compatible serialization mechanism!

The class variable: serialVersionUID

By implementing the interface Serializable you may implement a special class variable which is called serialVersionUID. This special variable may best be declared like this:

private static final long serialVersionUID = 1L;

The value of the variable (1L) may be chosen randomly, it just denotes the version of the current class structure. The Java serialization mechanism uses this variable to determine the version of the class that is or will be serialized.

If you do not define the serialVersionUID Java will calculate one for itself based the definition of the class variables, methods and Java version you are running. This method is very fragile and produces different versions on a lot of occasions you do not want the version to change. In fact you actually never want the version to change because it is impossible to deserialize an object with a different version as the one you are using. To control the version of the class you use you must implement the serialVersionUID.

Sometimes, in examples, you see complicated serialVersionUID definitions like:

private static final long serialVersionUID = -1814239825517340645L;

The use of the long numbers is not necessary. In such examples it is most likely that an object was serialized with the simple, not backward compatible method and the author realized that he needed backward compatibility after all. You may still deserialize the object if the serialVersionUID of the new class is the same as the serialVersionUID of the old class. In this case the new class is defined with that serialVersionUID. If you are smart, and you are, you do not have to use this method. In case you have to use it you may use the program serialver to determine the serialVersionUID of an existing class file.

Just implementing the serialVersionUID gives you a lot of backward compatibility already, because the default serialization mechanism does not protest as it does not encounter different serialVersionUID's as an added bonus the default serialization mechanism deserializes objects by mapping the field names of the serialized object to the field names of the runtime version.

Defining the methods readObject and writeObject

After implementing the serialVersionUID you get the field mapping for free across class versions, but this is often not good enough for providing real backwards compatibility.

There are two special methods you may implement to further perfect your class serialization: readObject and writeObject.

If you implement any of these methods you must define them with the following signature:

private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException

private void writeObject(ObjectOutputStream outthrows IOException

Attention
You will notice that the special methods readObject and writeObject do not override anything. This is normal behavior, the serialization mechanism uses the Java reflection mechanism to determine whether any of these methods exist and execute them if they do.

The first statements in both methods are also fixed. The readObject method should always start with the statement:

in.defaultReadObject();

The writeObject method should always start with the statement:

out.defaultWriteObject();

To explain the use of these calls it is easier to start with the call of defaultWriteObject(). The method defaultWriteObject() performs the default serialization mechanism by writing an object descriptor followed by the serialization of all fields of an object. We are not really interested in the field serialization because we are going to write these ourselves, but we definitely are interested in the object descriptor. The use of in.defaultReadObject() is there for the same reasons.

So, with the following implementations of the methods we have exactly the same functionality as the default serialization mechanism:

1 private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
2     in.defaultReadObject();
3     
4     // Enter you user code here    
5 }

1 private void writeObject(ObjectOutputStream outthrows IOException {
2     out.defaultWriteObject();
3     
4     // Enter you user code here
5 }

Really implementing the methods readObject and writeObject

Now that we have the definitions of readObject and writeObject in place we may start to think about implementing them with our own user code. First we'll have to identify the class variables we want to serialize, in our case we want to serialize them all. The easiest way is to serialize them in the default way, by using the readObject and writeObject methods of each stream. This will give us the following implementations of our methods:

01 private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
02     in.defaultReadObject();
03 
04     // Enter you user code here
05     setWindowX((Integer)in.readObject());
06     setWindowY((Integer)in.readObject());
07     setWindowWidth((Integer)in.readObject());
08     setWindowHeight((Integer)in.readObject());
09     setWindowState((Integer)in.readObject());
10 }

01 private void writeObject(ObjectOutputStream outthrows IOException {
02     out.defaultWriteObject();
03 
04     // Enter your user code here
05     out.writeObject(getWindowX());
06     out.writeObject(getWindowY());
07     out.writeObject(getWindowWidth());
08     out.writeObject(getWindowHeight());
09     out.writeObject(getWindowState());
10 }

You might already guessed it, but this implementation of the object serialization does exactly the same thing as the default serialization mechanism. So we still have gained nothing and have done a lot of work. But the pay-off comes in the next section.

Before we move on to the next section the class will be enriched with a sub-version number of the class. It's use is not explained here but will become clear in the next section too.

The sub-version will be implemented by a method. The serialization methods will use this sub-version number to determine the control flow withing each method.

Here you will find the Options class completely again with all the functionality in it. The last thing we do before moving on to the next section is to imagine that this version of the class is the one which is distributed with the first version of our application.

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 import java.io.*;
009 
010 /**
011  * The <code>Options</code> class holds all program options which need to be
012  * saved when the user exits the program.
013  *
014  @author Patrick Holthuizen
015  */
016 public class Options implements Serializable {
017     // Serial version UID
018     private static final long serialVersionUID = 1L;
019     // X-position of the main window
020     private Integer windowX = null;
021     // Y-position of the main window
022     private Integer windowY = null;
023     // Width of the main window
024     private Integer windowWidth = null;
025     // Height of the main window
026     private Integer windowHeight = null;
027     // Extended window state of the main window
028     private Integer windowState = null;
029     
030     /**
031      * Creates a new instance of <code>Options</code>.
032      */
033     public Options() {
034     }
035     
036     /**
037      * Gets the serial sub-version UID.
038      *
039      @return the serial sub-version UID
040      */
041     private Long getSerialSubVersionUID() {
042         return 1L;
043     }
044     
045     /**
046      * Gets the X-location of the main window.
047      *
048      @return the X-location of the main window
049      */
050     public Integer getWindowX() {
051         return windowX;
052     }
053     
054     /**
055      * Sets the X-location of the main window.
056      *
057      @param windowX the X-location of the main window
058      */
059     public void setWindowX(Integer windowX) {
060         this.windowX = windowX;
061     }
062     
063     /**
064      * Gets the Y-location of the main window.
065      *
066      @return the Y-location of the main window
067      */
068     public Integer getWindowY() {
069         return windowY;
070     }
071     
072     /**
073      * Sets the Y-location of the main window.
074      *
075      @param windowY the Y-location of the main window
076      */
077     public void setWindowY(Integer windowY) {
078         this.windowY = windowY;
079     }
080     
081     /**
082      * Gets the width of the main window.
083      *
084      @return the width of the main window
085      */
086     public Integer getWindowWidth() {
087         return windowWidth;
088     }
089     
090     /**
091      * Sets the width of the main window.
092      *
093      @param windowWidth the width of the main window
094      */
095     public void setWindowWidth(Integer windowWidth) {
096         this.windowWidth = windowWidth;
097     }
098     
099     /**
100      * Gets the height of the main window.
101      *
102      @return the height of the main window
103      */
104     public Integer getWindowHeight() {
105         return windowHeight;
106     }
107     
108     /**
109      * Sets the height of the main window.
110      *
111      @param windowHeight the height of the main window
112      */
113     public void setWindowHeight(Integer windowHeight) {
114         this.windowHeight = windowHeight;
115     }
116     
117     /**
118      * Gets the extended window state of the main window.
119      *
120      @return the extended window state of the main window
121      */
122     public Integer getWindowState() {
123         return windowState;
124     }
125     
126     /**
127      * Sets the extended window state of the main window.
128      *
129      @param windowState the extended window state of the main window
130      */
131     public void setWindowState(Integer windowState) {
132         this.windowState = windowState;
133     }
134     
135     /**
136      * Reads an instance of <code>Options</code> from the specified input stream.
137      *
138      @param in the input stream to read the object from
139      @throws java.lang.ClassNotFoundException 
140      @throws java.io.IOException 
141      */
142     private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
143         in.defaultReadObject();
144 
145         // Enter you user code here
146         Long serialSubVersionUID = (Long)in.readObject();
147         if(serialSubVersionUID == 1L) {
148             // Do not set the serialSubVersionUID
149             setWindowX((Integer)in.readObject());
150             setWindowY((Integer)in.readObject());
151             setWindowWidth((Integer)in.readObject());
152             setWindowHeight((Integer)in.readObject());
153             setWindowState((Integer)in.readObject());
154         else {
155             throw new ClassNotFoundException("Unsupported object version");
156         }
157     }
158 
159     /**
160      * Writes an instance of <code>Options</code> to the specified output stream.
161      
162      @param out the output stream to write the object to
163      @throws java.io.IOException 
164      */
165     private void writeObject(ObjectOutputStream outthrows IOException {
166         out.defaultWriteObject();
167 
168         // Enter your user code here
169         out.writeObject(getSerialSubVersionUID());
170         out.writeObject(getWindowX());
171         out.writeObject(getWindowY());
172         out.writeObject(getWindowWidth());
173         out.writeObject(getWindowHeight());
174         out.writeObject(getWindowState());
175     }
176 }

Code comments (you may skip these if you want)

Creating a new version of the Options class

In this section we will see the benefits of all our hard work fall into place. So our current class is in the field and as time flies by we decided to create a new version of our Options class. We decided to change the definition of the class by extending it with an additional property and to change the definition of the window location and window size. The latter two are going to be implemented by the Dimension class.

Here comes the complete listing of the Options class. I'll be discussing the changes later.

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 import java.awt.*;
009 import java.io.*;
010 
011 /**
012  * The <code>Options</code> class holds all program options which need to be
013  * saved when the user exits the program.
014  *
015  @author Patrick Holthuizen
016  */
017 public class Options implements Serializable {
018     // Serial version UID
019     private static final long serialVersionUID = 1L;
020     // Location of the main window
021     private Dimension windowLocation = null;
022     // Size of the main window
023     private Dimension windowSize = null;
024     // Extended window state of the main window
025     private Integer windowState = null;
026     // The title of the main window
027     private String windowTitle = null;
028     
029     /**
030      * Creates a new instance of <code>Options</code>.
031      */
032     public Options() {
033     }
034     
035     /**
036      * Gets the serial sub-version UID.
037      *
038      @return the serial sub-version UID
039      */
040     private Long getSerialSubVersionUID() {
041         return 2L;
042     }
043     
044     /**
045      * Gets the location of the main window.
046      *
047      @return the location of the main window
048      */
049     public Dimension getWindowLocation() {
050         return windowLocation;
051     }
052     
053     /**
054      * Sets the location of the main window.
055      *
056      @param windowLocation the location of the main window
057      */
058     public void setWindowLocation(Dimension windowLocation) {
059         this.windowLocation = windowLocation;
060     }
061     
062     /**
063      * Gets the size of the main window.
064      *
065      @return the size of the main window
066      */
067     public Dimension getWindowSize() {
068         return windowSize;
069     }
070     
071     /**
072      * Sets the size of the main window.
073      *
074      @param windowSize the size of the main window
075      */
076     public void setWindowSize(Dimension windowSize) {
077         this.windowSize = windowSize;
078     }
079     
080     /**
081      * Gets the extended window state of the main window.
082      *
083      @return the extended window state of the main window
084      */
085     public Integer getWindowState() {
086         return windowState;
087     }
088     
089     /**
090      * Sets the extended window state of the main window.
091      *
092      @param windowState the extended window state of the main window
093      */
094     public void setWindowState(Integer windowState) {
095         this.windowState = windowState;
096     }
097     
098     /**
099      * Gets the window title of the main window.
100      *
101      @return the window title of the main window
102      */
103     public String getWindowTitle() {
104         return windowTitle;
105     }
106     
107     /**
108      * Sets the window title of the main window.
109      *
110      @param windowTitle the window title of the main window
111      */
112     public void setWindowTitle(String windowTitle) {
113         this.windowTitle = windowTitle;
114     }
115     
116     /**
117      * Reads an instance of <code>Options</code> from the specified input stream.
118      *
119      @param in the input stream to read the object from
120      @throws java.lang.ClassNotFoundException if the class has an unrecognizable
121      * format
122      @throws java.io.IOException if some I/O exception occurs
123      */
124     private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
125         in.defaultReadObject();
126 
127         // Enter you user code here
128         Long serialSubVersionUID = (Long)in.readObject();
129         if(serialSubVersionUID == 1L) {
130             Integer windowX = (Integer)in.readObject();
131             Integer windowY = (Integer)in.readObject();
132             setWindowLocation(new Dimension(windowX, windowY));
133             Integer windowWidth = (Integer)in.readObject();
134             Integer windowHeight = (Integer)in.readObject();
135             setWindowSize(new Dimension(windowWidth, windowHeight));
136             setWindowState((Integer)in.readObject());
137             setWindowTitle(null);
138         else if(serialSubVersionUID == 2L) {
139             setWindowLocation((Dimension)in.readObject());
140             setWindowSize((Dimension)in.readObject());
141             setWindowState((Integer)in.readObject());
142             setWindowTitle((String)in.readObject());
143         else {
144             throw new ClassNotFoundException("Unsupported object version");
145         }
146     }
147 
148     /**
149      * Writes an instance of <code>Options</code> to the specified output stream.
150      
151      @param out the output stream to write the object to
152      @throws java.io.IOException if some I/O exception occurs
153      */
154     private void writeObject(ObjectOutputStream outthrows IOException {
155         out.defaultWriteObject();
156 
157         // Enter your user code here
158         out.writeObject(getSerialSubVersionUID());
159         out.writeObject(getWindowLocation());
160         out.writeObject(getWindowSize());
161         out.writeObject(getWindowState());
162         out.writeObject(getWindowTitle());
163     }
164 }

Code comments

The sub-version number

As you can see a method has been introduced to get a sub-version from an object instance. In the first version of our class this method always returned the number 1 and in the second version of our class this method returns 2.

This method is used in the writeObject method to serialize this number with the other data of an object. The sub-version number is serialized first so we can retrieve it first in the readObject method to determine the control flow.

Remark:
Notice that the special variable serialVersionUID is never touched and that we use an alternative way to handle class versions. We can not use the serialVersionUID because if it changes Java refuses to read the class with a different version number and we are not able to execute our readObject method successfully.

The method writeObject

Look at the differences between the writeObject method of the two versions of the classes.
Version 1 of the class writes the following objects to the stream: Long followed by five Integers.
Version 2 writes: Long, Dimension, Dimension, Integer and String.
It's also worth noticing that the writeObject method always writes the object in the latest version format to a stream, all version differences are handled by the readObject method. Using this technique our class is always able to read object of older versions and upgrades them automatically to the latest version when rewriting them.

The method readObject

In the readObject method all version handling is done, which means that the method readObject of the latest version of the Options class is able to read every version of a serialized Options class and upgrade it to the latest version.

If you take a look at the code you will notice a some tests on the serialSubVersionUID of the class. For each serialSubVersionUID you want to support (most preferably all) you must implement code to deserialize the object of that specific version and upgrade it to the latest version of the class.

serialSubVersionUID is equal to 1

All fields are being read in the version 1 format of the Options class. The fields windowX and windowY are converted to a Dimension. The same is done for the fields windowWidth and windowHeight.
The window title is not available in the first version of our class. Therefore the window title is initialized to null.

serialSubVersionUID is equal to 2

Because this is the current version of the class no conversion needs to be done and the code is very straightforward.

Handling null values

Until now everything works properly unless one of the objects is null. If one of the objects to serialize is null a NullPointerException is thrown.

Sometimes you want to serialize an object which might be null and serialize and deserialize it that way. Suppose in the example above that the window title might be null when an object of the Options class is serialized.

The most elegant way to handle null values is to put all fields which may contain null values in a Map which can be serialized as a whole. Of course you may put fields which are not null in a Map object.

In our case we are going to use the map implementation HashMap and we will use it serialize all fields with it (both null capable or not).

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011 
012 /**
013  * The <code>Options</code> class holds all program options which need to be
014  * saved when the user exits the program.
015  *
016  @author Patrick Holthuizen
017  */
018 public class Options implements Serializable {
019     // Serial version UID
020     private static final long serialVersionUID = 1L;
021     // Location of the main window
022     private Dimension windowLocation = null;
023     // Size of the main window
024     private Dimension windowSize = null;
025     // Extended window state of the main window
026     private Integer windowState = null;
027     // The title of the main window
028     private String windowTitle = null;
029     
030     /**
031      * Creates a new instance of <code>Options</code>.
032      */
033     public Options() {
034     }
035     
036     /**
037      * Gets the serial sub-version UID.
038      *
039      @return the serial sub-version UID
040      */
041     private Long getSerialSubVersionUID() {
042         return 2L;
043     }
044     
045     /**
046      * Gets the location of the main window.
047      *
048      @return the location of the main window
049      */
050     public Dimension getWindowLocation() {
051         return windowLocation;
052     }
053     
054     /**
055      * Sets the location of the main window.
056      *
057      @param windowLocation the location of the main window
058      */
059     public void setWindowLocation(Dimension windowLocation) {
060         this.windowLocation = windowLocation;
061     }
062     
063     /**
064      * Gets the size of the main window.
065      *
066      @return the size of the main window
067      */
068     public Dimension getWindowSize() {
069         return windowSize;
070     }
071     
072     /**
073      * Sets the size of the main window.
074      *
075      @param windowSize the size of the main window
076      */
077     public void setWindowSize(Dimension windowSize) {
078         this.windowSize = windowSize;
079     }
080     
081     /**
082      * Gets the extended window state of the main window.
083      *
084      @return the extended window state of the main window
085      */
086     public Integer getWindowState() {
087         return windowState;
088     }
089     
090     /**
091      * Sets the extended window state of the main window.
092      *
093      @param windowState the extended window state of the main window
094      */
095     public void setWindowState(Integer windowState) {
096         this.windowState = windowState;
097     }
098     
099     /**
100      * Gets the window title of the main window.
101      *
102      @return the window title of the main window
103      */
104     public String getWindowTitle() {
105         return windowTitle;
106     }
107     
108     /**
109      * Sets the window title of the main window.
110      *
111      @param windowTitle the window title of the main window
112      */
113     public void setWindowTitle(String windowTitle) {
114         this.windowTitle = windowTitle;
115     }
116     
117     /**
118      * Reads an instance of <code>Options</code> from the specified input stream.
119      *
120      @param in the input stream to read the object from
121      @throws java.lang.ClassNotFoundException if the class has an unrecognizable
122      * format
123      @throws java.io.IOException if some I/O exception occurs
124      */
125     private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
126         in.defaultReadObject();
127 
128         // Enter you user code here
129         Long serialSubVersionUID = (Long)in.readObject();
130         if(serialSubVersionUID == 1L) {
131             Integer windowX = (Integer)in.readObject();
132             Integer windowY = (Integer)in.readObject();
133             setWindowLocation(new Dimension(windowX, windowY));
134             Integer windowWidth = (Integer)in.readObject();
135             Integer windowHeight = (Integer)in.readObject();
136             setWindowSize(new Dimension(windowWidth, windowHeight));
137             setWindowState((Integer)in.readObject());
138             setWindowTitle(null);
139         else if(serialSubVersionUID == 2L) {
140             Map<String, Object> map = (Map<String, Object>)in.readObject();
141             setWindowLocation((Dimension)map.get("windowLocation"));
142             setWindowSize((Dimension)map.get("windowSize"));
143             setWindowState((Integer)map.get("windowSize"));
144             setWindowTitle((String)map.get("windowTitle"));
145         else {
146             throw new ClassNotFoundException("Unsupported object version");
147         }
148     }
149 
150     /**
151      * Writes an instance of <code>Options</code> to the specified output stream.
152      
153      @param out the output stream to write the object to
154      @throws java.io.IOException if some I/O exception occurs
155      */
156     private void writeObject(ObjectOutputStream outthrows IOException {
157         out.defaultWriteObject();
158 
159         // Enter your user code here
160         out.writeObject(getSerialSubVersionUID());
161         Map<String, Object> map = new HashMap<String, Object>();
162         map.put("windowLocation", getWindowLocation());
163         map.put("windowSize", getWindowSize());
164         map.put("windowState", getWindowState());
165         map.put("windowTitle", getWindowTitle());
166         out.writeObject(map);
167     }
168 }

Note: I didn't know which map class was best to use, finally wanted to choose between the EnumMap and the HashMap. Although the EnumMap provides more type safety it introduces a lot of compatibility problems opposed to the HashMap.

Serializing in a secure way

Suppose version three of the Options class contains a password field we wish to serialize. First we expand the Options class with the field and extend the methods readObject and writeObject. The result is shown below:

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011 
012 /**
013  * The <code>Options</code> class holds all program options which need to be
014  * saved when the user exits the program.
015  *
016  @author Patrick Holthuizen
017  */
018 public class Options implements Serializable {
019     // Serial version UID
020     private static final long serialVersionUID = 1L;
021     // Location of the main window
022     private Dimension windowLocation = null;
023     // Size of the main window
024     private Dimension windowSize = null;
025     // Extended window state of the main window
026     private Integer windowState = null;
027     // The title of the main window
028     private String windowTitle = null;
029     // The user-id to be used to connect to the database
030     private String userId = null;
031     // The password related to the user-id
032     private String password = null;
033     
034     /**
035      * Creates a new instance of <code>Options</code>.
036      */
037     public Options() {
038     }
039     
040     /**
041      * Gets the serial sub-version UID.
042      *
043      @return the serial sub-version UID
044      */
045     private Long getSerialSubVersionUID() {
046         return 3L;
047     }
048     
049     /**
050      * Gets the location of the main window.
051      *
052      @return the location of the main window
053      */
054     public Dimension getWindowLocation() {
055         return windowLocation;
056     }
057     
058     /**
059      * Sets the location of the main window.
060      *
061      @param windowLocation the location of the main window
062      */
063     public void setWindowLocation(Dimension windowLocation) {
064         this.windowLocation = windowLocation;
065     }
066     
067     /**
068      * Gets the size of the main window.
069      *
070      @return the size of the main window
071      */
072     public Dimension getWindowSize() {
073         return windowSize;
074     }
075     
076     /**
077      * Sets the size of the main window.
078      *
079      @param windowSize the size of the main window
080      */
081     public void setWindowSize(Dimension windowSize) {
082         this.windowSize = windowSize;
083     }
084     
085     /**
086      * Gets the extended window state of the main window.
087      *
088      @return the extended window state of the main window
089      */
090     public Integer getWindowState() {
091         return windowState;
092     }
093     
094     /**
095      * Sets the extended window state of the main window.
096      *
097      @param windowState the extended window state of the main window
098      */
099     public void setWindowState(Integer windowState) {
100         this.windowState = windowState;
101     }
102     
103     /**
104      * Gets the window title of the main window.
105      *
106      @return the window title of the main window
107      */
108     public String getWindowTitle() {
109         return windowTitle;
110     }
111     
112     /**
113      * Sets the window title of the main window.
114      *
115      @param windowTitle the window title of the main window
116      */
117     public void setWindowTitle(String windowTitle) {
118         this.windowTitle = windowTitle;
119     }
120     
121     /**
122      * Gets the user-id which can be used to connect to the dabase.
123      *
124      @return the user-id
125      */
126     public String getUserId() {
127         return userId;
128     }
129     
130     /**
131      * Sets the user-id which can be used to connect to the dabase.
132      *
133      @param userId the user-id
134      */
135     public void setUserId(String userId) {
136         this.userId = userId;
137     }
138     
139     /**
140      * Gets the password related to the user-id.
141      *
142      @return the password related to the user-id
143      */
144     public String getPassword() {
145         return password;
146     }
147     
148     /**
149      * Gets the password related to the user-id.
150      *
151      @param password the password related to the user-id
152      */
153     public void setPassword(String password) {
154         this.password = password;
155     }
156     
157     /**
158      * Reads an instance of <code>Options</code> from the specified input stream.
159      *
160      @param in the input stream to read the object from
161      @throws java.lang.ClassNotFoundException if the class has an unrecognizable
162      * format
163      @throws java.io.IOException if some I/O exception occurs
164      */
165     private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
166         in.defaultReadObject();
167 
168         // Enter you user code here
169         Long serialSubVersionUID = (Long)in.readObject();
170         if(serialSubVersionUID == 1L) {
171             Integer windowX = (Integer)in.readObject();
172             Integer windowY = (Integer)in.readObject();
173             setWindowLocation(new Dimension(windowX, windowY));
174             Integer windowWidth = (Integer)in.readObject();
175             Integer windowHeight = (Integer)in.readObject();
176             setWindowSize(new Dimension(windowWidth, windowHeight));
177             setWindowState((Integer)in.readObject());
178             setWindowTitle(null);
179             setUserId(null);
180             setPassword(null);
181         else if(serialSubVersionUID == 2L) {
182             Map<String, Object> map = (Map<String, Object>)in.readObject();
183             setWindowLocation((Dimension)map.get("windowLocation"));
184             setWindowSize((Dimension)map.get("windowSize"));
185             setWindowState((Integer)map.get("windowSize"));
186             setWindowTitle((String)map.get("windowTitle"));
187             setUserId(null);
188             setPassword(null);
189         else if(serialSubVersionUID == 3L) {
190             Map<String, Object> map = (Map<String, Object>)in.readObject();
191             setWindowLocation((Dimension)map.get("windowLocation"));
192             setWindowSize((Dimension)map.get("windowSize"));
193             setWindowState((Integer)map.get("windowSize"));
194             setWindowTitle((String)map.get("windowTitle"));
195             setUserId((String)map.get("userId"));
196             setPassword((String)map.get("password"));
197         else {
198             throw new ClassNotFoundException("Unsupported object version");
199         }
200     }
201 
202     /**
203      * Writes an instance of <code>Options</code> to the specified output stream.
204      
205      @param out the output stream to write the object to
206      @throws java.io.IOException if some I/O exception occurs
207      */
208     private void writeObject(ObjectOutputStream outthrows IOException {
209         out.defaultWriteObject();
210 
211         // Enter your user code here
212         out.writeObject(getSerialSubVersionUID());
213         Map<String, Object> map = new HashMap<String, Object>();
214         map.put("windowLocation", getWindowLocation());
215         map.put("windowSize", getWindowSize());
216         map.put("windowState", getWindowState());
217         map.put("windowTitle", getWindowTitle());
218         map.put("userId", getUserId());
219         map.put("password", getPassword());
220         out.writeObject(map);
221     }
222 }

I think the extension is pretty straightforward and doesn't need additional explanation. Technically the code above works properly the only problem we want to address is the storage of the new fields (user-id and password). These fields are written to the output stream in plain text format and therefore readable.
For example the following code:

Options options = new Options();
options.setUserId("patrick");
options.setPassword("verysecretpassword");

try {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(".options"));
    oos.writeObject(options);
    oos.close();
catch(Exception e) {
    e.printStackTrace();
}

After executing the code above a file with the name .options is created and written on the file system. If you take a look at the contents of the file it isn't really hard to locate the strings "patrick" and "verysecretpassword" in it. This is really a security threat which we going to solve now. Although the method we use is not really waterproof it is much more secure in a way that it will be much harder for regular end users to obtain account sensitive information.

Instead of having the readable password in the serialized file we are going to move the sensitive information to our program code. As said before this will not be much safer when you need security against hackers, but it will probably be good enough to hold of regular end users from obtaining the information.

Providing security we are going to serialize the Options class in an encrypted way. To encrypt it we are going to use the the SealedObject class to encrypt the class and write that class to the object output stream. The last version of the will be extended by the encryption handling and the code is shown below:

001 /*
002  * Options.java
003  *
004  * Created on 24 juli 2005, 22:10
005  */
006 package main;
007 
008 import java.awt.*;
009 import java.io.*;
010 import java.util.*;
011 import javax.crypto.*;
012 import javax.crypto.spec.*;
013 
014 /**
015  * The <code>Options</code> class holds all program options which need to be
016  * saved when the user exits the program.
017  *
018  @author Patrick Holthuizen
019  */
020 public class Options implements Serializable {
021     // Serial version UID
022     private static final long serialVersionUID = 1L;
023     // Location of the main window
024     private transient Dimension windowLocation = null;
025     // Size of the main window
026     private transient Dimension windowSize = null;
027     // Extended window state of the main window
028     private transient Integer windowState = null;
029     // The title of the main window
030     private transient String windowTitle = null;
031     // The user-id to be used to connect to the database
032     private transient String userId = null;
033     // The password related to the user-id
034     private transient String password = null;
035     
036     /**
037      * Creates a new instance of <code>Options</code>.
038      */
039     public Options() {
040     }
041     
042     /**
043      * Gets the serial sub-version UID.
044      *
045      @return the serial sub-version UID
046      */
047     private Long getSerialSubVersionUID() {
048         return 3L;
049     }
050     
051     /**
052      * Gets the location of the main window.
053      *
054      @return the location of the main window
055      */
056     public Dimension getWindowLocation() {
057         return windowLocation;
058     }
059     
060     /**
061      * Sets the location of the main window.
062      *
063      @param windowLocation the location of the main window
064      */
065     public void setWindowLocation(Dimension windowLocation) {
066         this.windowLocation = windowLocation;
067     }
068     
069     /**
070      * Gets the size of the main window.
071      *
072      @return the size of the main window
073      */
074     public Dimension getWindowSize() {
075         return windowSize;
076     }
077     
078     /**
079      * Sets the size of the main window.
080      *
081      @param windowSize the size of the main window
082      */
083     public void setWindowSize(Dimension windowSize) {
084         this.windowSize = windowSize;
085     }
086     
087     /**
088      * Gets the extended window state of the main window.
089      *
090      @return the extended window state of the main window
091      */
092     public Integer getWindowState() {
093         return windowState;
094     }
095     
096     /**
097      * Sets the extended window state of the main window.
098      *
099      @param windowState the extended window state of the main window
100      */
101     public void setWindowState(Integer windowState) {
102         this.windowState = windowState;
103     }
104     
105     /**
106      * Gets the window title of the main window.
107      *
108      @return the window title of the main window
109      */
110     public String getWindowTitle() {
111         return windowTitle;
112     }
113     
114     /**
115      * Sets the window title of the main window.
116      *
117      @param windowTitle the window title of the main window
118      */
119     public void setWindowTitle(String windowTitle) {
120         this.windowTitle = windowTitle;
121     }
122     
123     /**
124      * Gets the user-id which can be used to connect to the dabase.
125      *
126      @return the user-id
127      */
128     public String getUserId() {
129         return userId;
130     }
131     
132     /**
133      * Sets the user-id which can be used to connect to the dabase.
134      *
135      @param userId the user-id
136      */
137     public void setUserId(String userId) {
138         this.userId = userId;
139     }
140     
141     /**
142      * Gets the password related to the user-id.
143      *
144      @return the password related to the user-id
145      */
146     public String getPassword() {
147         return password;
148     }
149     
150     /**
151      * Gets the password related to the user-id.
152      *
153      @param password the password related to the user-id
154      */
155     public void setPassword(String password) {
156         this.password = password;
157     }
158     
159     /**
160      * Gets a secret DES key used to encrypt the object contents.
161      *
162      @return the secret DES key
163      */
164     private SecretKey getSecretKey() {
165         SecretKey result = null;
166         
167         // Now our security exposure is "reduced" to this method.
168         String keyString = "A4SFR55DSH3J4H3JJJ3H34FGFG34";
169         byte key[] = keyString.getBytes();
170         try {
171             DESKeySpec desKeySpec = new DESKeySpec(key);
172             SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
173             result = keyFactory.generateSecret(desKeySpec);        
174         catch(Exception e) {
175             e.printStackTrace();
176         }
177         
178         return result;
179     }
180     
181     /**
182      * Reads an instance of <code>Options</code> from the specified input stream.
183      *
184      @param in the input stream to read the object from
185      @throws java.lang.ClassNotFoundException if the class has an unrecognizable
186      * format
187      @throws java.io.IOException if some I/O exception occurs
188      */
189     private void readObject(ObjectInputStream inthrows ClassNotFoundException, IOException {
190         in.defaultReadObject();
191 
192         // Enter you user code here
193         Long serialSubVersionUID = (Long)in.readObject();
194         if(serialSubVersionUID == 1L) {
195             Integer windowX = (Integer)in.readObject();
196             Integer windowY = (Integer)in.readObject();
197             setWindowLocation(new Dimension(windowX, windowY));
198             Integer windowWidth = (Integer)in.readObject();
199             Integer windowHeight = (Integer)in.readObject();
200             setWindowSize(new Dimension(windowWidth, windowHeight));
201             setWindowState((Integer)in.readObject());
202             setWindowTitle(null);
203             setUserId(null);
204             setPassword(null);
205         else if(serialSubVersionUID == 2L) {
206             Map<String, Object> map = (Map<String, Object>)in.readObject();
207             setWindowLocation((Dimension)map.get("windowLocation"));
208             setWindowSize((Dimension)map.get("windowSize"));
209             setWindowState((Integer)map.get("windowSize"));
210             setWindowTitle((String)map.get("windowTitle"));
211             setUserId(null);
212             setPassword(null);
213         else if(serialSubVersionUID == 3L) {
214             SealedObject sealedObject = (SealedObject)in.readObject();
215             try {
216                 Map<String, Object> map = (Map<String, Object>)sealedObject.getObject(getSecretKey());
217                 setWindowLocation((Dimension)map.get("windowLocation"));
218                 setWindowSize((Dimension)map.get("windowSize"));
219                 setWindowState((Integer)map.get("windowSize"));
220                 setWindowTitle((String)map.get("windowTitle"));
221                 setUserId((String)map.get("userId"));
222                 setPassword((String)map.get("password"));
223             catch(Exception e) {
224                 throw new IOException("Unable to decrypt the object: " + e.getMessage());
225             }
226         else {
227             throw new ClassNotFoundException("Unsupported object version");
228         }
229     }
230 
231     /**
232      * Writes an instance of <code>Options</code> to the specified output stream.
233      
234      @param out the output stream to write the object to
235      @throws java.io.IOException if some I/O exception occurs
236      */
237     private void writeObject(ObjectOutputStream outthrows IOException {
238         HashMap<String, Object> map = new HashMap<String, Object>();
239         map.put("windowLocation", getWindowLocation());
240         map.put("windowSize", getWindowSize());
241         map.put("windowState", getWindowState());
242         map.put("windowTitle", getWindowTitle());
243         map.put("userId", getUserId());
244         map.put("password", getPassword());
245         // Seal object
246         SealedObject sealedObject = null;
247         try {
248             Cipher desCipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
249             desCipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
250             sealedObject = new SealedObject(map, desCipher);
251         catch(Exception e) {
252             throw new IOException("Unable to encrypt the object: " + e.getMessage());
253         }
254         
255         out.defaultWriteObject();
256         out.writeObject(getSerialSubVersionUID());
257         out.writeObject(sealedObject);
258     }
259 }

Code comments

Remarks

The solution provided on this page is a lightweight solution. This solution will probably be insufficient for secure and large scale environments. Consider using a more centralized mechanism for these situations.

The sources on this page are all implemented using JDK 6.0.

Comments

If you have any comments please send an e-mail to Patrick Holthuizen.

My status