Main Page

Previous Next

Storing Objects in a File

The process of storing and retrieving objects in an external file is called serialization. Note that an array of any type is an object for the purposes of serialization, even an array of values of a primitive type, such as type int or type double. Writing an object to a file is referred to as serializing the object, and reading an object from a file is called deserializing an object. Serialization is concerned with writing objects and the fields they contain to a stream, so this excludes static members of a class. Static fields will have whatever values are assigned by default in the class definition.

I think you will be surprised at how easy this is. Perhaps the most impressive aspect of the way serialization is implemented in Java is that you can generally read and write objects of almost any class type, including objects of classes that you have defined yourself, without adding any code to the classes involved to support this mechanism. For the most part, everything is taken care of automatically:

Click To expand

Two classes from the java.io package are used for serialization. An ObjectOutputStream object manages the writing of objects to a file, and reading the objects back is handled by an object of the class ObjectInputStream. As we saw in Chapter 8, these are derived from OutputStream and InputStream, respectively.

Writing an Object to a File

The constructor for the ObjectOutputStream class requires a reference to a FileOutputStream object as an argument that defines the stream for the file where you intend to store your objects. You could create an ObjectOutputStream object with the following statements:

File theFile = new File("MyFile");
// Check out the file...

// Create the object output stream for the file
ObjectOutputStream objectOut = null;
try {
  objectOut = new ObjectOutputStream(new FileOutputStream(theFile));

} catch(IOException e) {
  e.printStackTrace(System.err);
  System.exit(1);
}

We know from our earlier investigations into file input that the FileOutputStream constructor can throw a FileNotFoundException if the File object that you pass to the constructor represents a directory rather than a file, or if the file does not exist and cannot be created for some reason. In addition the ObjectOutputStream constructor will throw an IOException if an error occurs while the stream header is written to the file. Our catch block here will handle either of these exceptions.

While the previous code fragment will work perfectly well, it does not result in a stream that is particularly efficient since each output operation will write directly to the file. In practice you will probably want to buffer write operations on the file in memory, in which case you would create the ObjectOutputStream object like this:

  objectOut = new ObjectOutputStream(
                new BufferedOutputStream(
                  new FileOutputStream(theFile)));

The BufferedOutputStream constructor creates an object that buffers the OutputStream object that is passed to it, so here we get a buffered FileOutputStream object that we pass to the ObjectOutputStream constructor. With this arranged, each write operation to the ObjectOutputStream will write the BufferedOutputStream object. The BufferedOutputStream object will write the data to an internal buffer. Data from the buffer will be written to the file whenever the buffer is full, or when the stream is closed by calling its close() method or flushed by calling its flush() method. By default the buffer has a capacity of 512 bytes. If you want to use a buffer of a different size you can use the BufferedOutputStream constructor that accepts a second argument of type int that defines the size of the buffer in bytes.

To write an object to the file MyFile, you call the writeObject()method for objectOut with a reference to the object to be written as the argument. Since this method accepts a reference of type Object as an argument, you can pass a reference of any class type to the method. There are three basic conditions that have to be met for an object to be written to a stream:

  • The class must be declared as public.

  • The class must implement the Serializable interface.

  • If the class has a direct or indirect base class that is not serializable, then that base class must have a default constructor – that is, a constructor that requires no arguments. The derived class must take care of transferring the base class data members to the stream.

Implementing the Serializable interface is a lot less difficult than it sounds, and we will see how in a moment. Later we will come back to the question of how to deal with a non-serializable base class.

If myObject is an instance of a public class that implements Serializable, then to write myObject to the stream that we defined above, you would use the statement:

try {
  objectOut.writeObject(myObject);

} catch(IOException e) {
  e.printStackTrace(System.err);
  System.exit(1);
}

The writeObject() method can throw any of the following three exceptions:

InvalidClassException

Thrown when there is something wrong with the class definition for the object being written. This might be because the class is not public, for instance.

NotSerializableException

Thrown if the object's class, or the class of a data member of the class, does not implement the Serializable interface.

IOException

Thrown when a file output error occurs.

The first two exception classes here are subclasses of ObjectStreamException, which is itself a subclass of IOException. Thus we can catch any of them with a catch block for IOException. Of course, if you want to take some specific action for any of these then you can catch them individually. Just be sure to put the catch blocks for the first two types of exception before the one for IOException.

The call to writeObject() takes care of writing everything to the stream that is necessary to reconstitute the object later in a read operation. This includes information about the class and all its superclasses, as well as the contents and types of the data members of the class. Remarkably, this works even when the data members are themselves class objects, as long as they are objects of Serializable classes. Our writeObject() call will cause the writeObject() method for each object that is a data member to be called, and this mechanism continues recursively until everything that makes up our object has been written to the stream. Each independent object that you write to the stream requires a separate call to the writeObject() method, but the objects that are members of an object are taken care of automatically. This is not completely foolproof in that the relationships between the class objects can affect this process, but for the most part this is all you need to do. We will be using serialization to write fairly complex objects to files in Chapter 20.

Writing Basic Data Types to an Object Stream

You can write data of any of the primitive types using the methods defined in the ObjectOutputStream class for this purpose. For writing individual items of data of various types, you have the following methods:

writeByte(int b)

writeByte(byte b)

writeChar(int ch)

writeShort(int n)

writeInt(int n)

writeLong(long n)

writeFloat(float x)

writeDouble(double x)

 

None of them return a value and they can all throw an IOException since they are output operations.

You can write a string object to the file as a sequence of bytes using the writeBytes() method, passing a reference to a String as the argument to the method. Each character in the string is converted to a byte using the default charset. To write a string as a sequence of Unicode characters, you use the writeChars() method, again with a reference of type String as the argument. This writes each Unicode character in the string as two bytes. Note that these methods write just a sequence of bytes or characters. No information about the original String object is written so the fact that these characters belonged to a string is lost. If you want to write a String object to the file as an object, use the writeObject() method.

You have two methods that apply to arrays of bytes. These override the methods inherited from InputStream:

Write(byte[] array)

Writes the contents of array to the file as bytes.

Write(byte[] array, int offset, int length)

Writes length elements from array to the file starting with array[offset].

In both cases just bytes are written to the stream as binary data, not the array object itself. An array of type byte[] will be written to the stream as an object by default, so you will only need to use these methods if you do not want an array of type byte[] written as an object.

You can mix writing data of the basic types and class objects to the stream. If you have a mixture of objects and data items of basic types that you want to store in a file, you can write them all to the same ObjectOutputStream. You just have to make sure that you read everything back in the sequence and form that it was written.

Implementing the Serializable Interface

A necessary condition for objects of a class to be serializable is that the class implements the Serializable interface but this may not be sufficient, as we shall see. In most instances, you need only declare that the class implements the Serializable interface to make the objects of that class type serializable. No other code is necessary. For example, the following declares a class that implements the interface.

public MyClass implements Serializable {
  // Definition of the class...
}

If your class is derived from another class that implements the Serializable interface, then your class also implements Serializable so you don't have to declare that this is the case. Let's try this out on a simple class to verify that it really works.

Try It Out – Writing Objects to a File

We will first define a serializable class that has some arbitrary fields with different data types:

import java.io.Serializable;

public class Junk implements Serializable {
  private static java.util.Random generator = new java.util.Random();
  private int answer;                            // The answer
  private double[] numbers;                      // Valuable data
  private String thought;                        // A unique thought

  public Junk(String thought) {
    this.thought = thought;
    answer = 42;                            // Answer always 42

    numbers = new double[3+generator.nextInt(4)]; // Array size 3 to 6 
    for(int i = 0 ; i<numbers.length ; i++)   // Populate with
      numbers[i] = generator.nextDouble();    // random values     
  }


  public String toString() {
    StringBuffer strBuf = new StringBuffer(thought); 
    strBuf.append('\n').append(String.valueOf(answer));
    for(int i = 0 ; i<numbers.length ; i++)
      strBuf.append("\nnumbers[")
            .append(String.valueOf(i))
            .append("] = ")
            .append(numbers[i]);      
    return strBuf.toString();
  }
}

An object of type Junk has three instance fields, a simple integer that is always 42, a String object, and an array of double values. The toString() method provides a String representation of a Junk object that we can output to the command line. The static field, generator, will not be written to the stream when an object of type Junk is serialized. The only provision we have made for serializing objects of type Junk is to declare that the class implements the Serializable interface.

We can write objects of this class type to a file with the following program:

import java.io.*;

public class SerializeObjects {
  public static void main(String[] args) {
    Junk obj1 = new Junk("A green twig is easily bent.");
    Junk obj2 = new Junk("A little knowledge is a dangerous thing.");
    Junk obj3 = new Junk("Flies light on lean horses.");
    ObjectOutputStream objectOut = null;
    try {
       // Create the object output stream
       objectOut = new ObjectOutputStream(
                    new BufferedOutputStream(
                     new FileOutputStream("C:/Beg Java Stuff/JunkObjects.bin")));

      // Write three objects to the file
      objectOut.writeObject(obj1);            // Write object
      objectOut.writeObject(obj2);            // Write object
      objectOut.writeObject(obj3);            // Write object
      System.out.println("\n\nobj1:\n" + obj1
                        +"\n\nobj2:\n" + obj2
                        +"\n\nobj3:\n" + obj3);

    } catch(IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }

    // Close the stream
    try {
       objectOut.close();                          // Close the output stream

    } catch(IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
   }
}

When I ran this, I got the output:

obj1:
A green twig is easily bent.
42
numbers[0] = 0.20157825618636616
numbers[1] = 0.7123542196242817
numbers[2] = 0.8027761971323069

obj2:
A little knowledge is a dangerous thing.
42
numbers[0] = 0.929629487353265
numbers[1] = 0.5402881072148746
numbers[2] = 0.03259660544653753
numbers[3] = 0.94945294401263
numbers[4] = 0.17383591141346522

obj3:
Flies light on lean horses.
42
numbers[0] = 0.6765377168813207
numbers[1] = 0.3933764846876555
numbers[2] = 0.7633265658906377
numbers[3] = 0.31411955819992887

You should get something vaguely similar.

How It Works

We first create three objects of type Junk in the main() method. We then define a variable that will hold the stream reference. If we were to define this variable within the first try block, then it would not exist beyond the end of the try block so we could not refer to it after that point. Within the try block we create the ObjectOutputStream object that we will use to write objects to the file C:/Beg Java Stuff/JunkObjects.bin, via a buffered output stream. Each Junk object is written to the file by passing it to the writeObject() method for the ObjectOutputStream object. Each object will be written to the file including the values of its three instance fields, answer, thought, and numbers. The String object and the array are written to the file as objects. This is taken care of automatically and requires no special provision within our code. The static field, generator, is not written to the file.

Before we exit the program we close the stream by calling its close() method. We could put the call to close() within the first try block, but if an exception was thrown due to an I/O error the method would not get called. By putting it in a separate try block we ensure that we do call the close() method. The stream would be closed automatically when the program terminates but it is good practice to close any streams as soon as you are done with them. We will read the objects back from the file a little later in this chapter.

Conditions for Serialization

In general there can be a small fly in the ointment. For implementing the Serializable interface, to be sufficient to make objects of the class serializable, all the fields in the class must be serializable (or transient – which we will come to), and all superclasses of the class must also be serializable. This implies that the fields must be either of primitive types or of class types that are themselves serializable.

If a superclass of your class is not serializable, it still may be possible to make your class serializable. The conditions that must be met for this to be feasible are:

  • Each superclass that is not serializable must have a public default constructor – a constructor with no parameters.

  • Your class must be declared as implementing the Serializable interface.

  • Your class must take responsibility for serializing and deserializing the fields for the superclasses that are not serializable.

This will usually be the case for your own classes, but there are one or two classes that come along with Java that do not implement the Serializable interface, and what's more, you can't make them serializable because they do not have a public default constructor. The Graphics class in the package java.awt is an example of such a class – we will see more of this class when we get into programming using windows. All is not lost however. There is an escape route. As long as you know how to reconstruct any fields that were not serializable when you read an object back from a stream, you can still serialize your objects by declaring the non-serializable fields as transient.

Transient Data Members of a Class

If your class has fields that are not serializable, or that you just don't want to have written to the stream, you can declare them as transient. For example:

public class MyClass implements Serializable {
  transient protected Graphics g;    // Transient class member
  
  // Rest of the class definition
}

Declaring a data member as transient will prevent the writeObject() method from attempting to write the data member to the stream. When the class object is read back, it will be created properly, including any members that you declared as transient. They just won't have their values set, because they were not written to the stream. Unless you arrange for something to be done about it, the transient fields will be null.

You may well want to declare some data members of a class as transient. You would do this when they have a value that is not meaningful long term or out of context – objects that represent the current time, or today's date, for instance. You must either provide code to explicitly reconstruct the members that you declare as transient when the object that contains them is read from the stream or accept the construction time defaults.

Reading an Object from a File

Reading objects back from a file is just as easy as writing them. First, you need to create an ObjectInputStream object for the file. To do this you just pass a reference to a FileInputStream object that encapsulates the file to the ObjectInputStream class constructor:

File theFile = new File("MyFile");
// Perhaps check out the file...

// Create the object output stream for the file
ObjectInputStream objectIn = null;
try {
  objectIn = new ObjectInputStream(new FileInputStream(theFile));

} catch(IOException e) {
  e.printStackTrace(System.err);
  System.exit(1);
}

The ObjectInputStream constructor will throw an exception of type StreamCorruptedException – a subclass of IOException – if the stream header is not correct, or of type IOException if an error occurs while reading the stream header. Of course, as we saw in the last chapter, the FileInputStream constructor can throw an exception of type FileNotFoundException.

Once you have created the ObjectInputStream object you call its readObject() method to read an object from the file:

Object myObject = null;
try {
  myObject = objectIn.readObject();

} catch(ClassNotFoundException e){
  e.printStackTrace(System.err);
  System.exit(1);

} catch(IOException e){
  e.printStackTrace(System.err);
  System.exit(1);
}

The readObject() method can throw the following exceptions.

ClassNotFoundException

Thrown if the class for an object read from the stream cannot be found.

InvalidClassException

Thrown if there is something wrong with the class for an object. This is commonly caused by changing the definition of a class for an object between writing and reading the file.

StreamCorruptedException

When objects are written to the stream, additional control data is written so that the object data can be validated when it is read back. This exception is thrown when the control information in the stream is inconsistent.

OptionalDataException

Thrown when basic types of data are read rather than an object. For instance, if you wrote a String object using the writeChars() method and then attempted to read it back using the readObject() method, this exception would be thrown.

IOException

Thrown if an error occurred reading the stream.

Clearly, if you do not have a full and accurate class definition for each type of object that you want to read from the stream, the stream object will not know how to create the object and the read will fail. The last four of the five possible exceptions are flavors of IOException, so you can use that as a catchall as we have in the code fragment above. However, ClassNotFoundException is derived from Exception, so you must put a separate catch block for this exception in your program. Otherwise it will not compile.

As the code fragment implies, the readObject() method will return a reference to the object as type Object, so you need to cast it to the appropriate class type in order to use it. Note that arrays are considered to be objects and are treated as such during serialization, so if you explicitly read an array from a file, you will have to cast it to the appropriate array type.

For example, if the object in the previous code fragment was of type MyClass, you could read it back from the file with the statements:

MyClass theObject = null;    // Store the object here

try {
  theObject = (MyClass)(objectIn.readObject());

} catch(ClassNotFoundException e) {
  e.printStackTrace(System.err);
  System.exit(1);

} catch(IOException e) {
  e.printStackTrace(System.err);
  System.exit(1);
}

To deserialize the object, we call the method readObject() and cast the reference returned to the type MyClass.

Armed with the knowledge of how the readObject() method works, we can now read the file that we wrote in the previous example.

Try It Out – Deserializing Objects

We can read the file containing Junk objects with the following code:

import java.io.*;

class DeserializeObjects {    
  public static void main(String args[]) {
    ObjectInputStream objectIn = null;  // Stores the stream reference
    int objectCount = 0;                // Number of objects read
    Junk object = null;                 // Stores an object reference
    try {
      objectIn = new ObjectInputStream(
                   new BufferedInputStream(
                     new FileInputStream("C:/Beg Java Stuff/JunkObjects.bin")));

       // Read from the stream until we hit the end
       while(true) {
        object = (Junk)objectIn.readObject();  // Read an object
        objectCount++;                         // Increment the count
        System.out.println(object);            // Output the object
      }

    } catch(ClassNotFoundException e) {
      e.printStackTrace(System.err);
      System.exit(1);

    } catch(EOFException e)          // This will execute when we reach EOF {
      System.out.println("EOF reached. "+ objectCount + " objects read.");

    } catch(IOException e)                // This is for other I/O errors   {
      e.printStackTrace(System.err);
      System.exit(1);
    }

    // Close the stream
    try {
      objectIn.close();                          // Close the input stream

    } catch(IOException e) {
      e.printStackTrace(System.err);
      System.exit(1);
    }
  }
}

I got the output:

A green twig is easily bent.
42
numbers[0] = 0.20157825618636616
numbers[1] = 0.7123542196242817
numbers[2] = 0.8027761971323069
A little knowledge is a dangerous thing.
42
numbers[0] = 0.929629487353265
numbers[1] = 0.5402881072148746
numbers[2] = 0.03259660544653753
numbers[3] = 0.94945294401263
numbers[4] = 0.17383591141346522
Flies light on lean horses.
42
numbers[0] = 0.6765377168813207
numbers[1] = 0.3933764846876555
numbers[2] = 0.7633265658906377
numbers[3] = 0.31411955819992887
EOF reached. 3 objects read.

You should get output corresponding to the objects that were written to your file.

How It Works

We first define the objectIn variable that will store the reference to the stream. We will use the objectCount variable to accumulate a count of the total number of objects read from the stream. The object variable will store the reference to each object that we read. To make the program a little more general, we have implemented the read operation in a loop to show how you might read the file when you don't know how many objects there are in it. To read each object, we just call the readObject() method for the input stream and cast the reference returned to type Junk before storing it in object.

So we can see what we have read, the string representation of each object is displayed on the command line. The while loop will continue indefinitely reading objects from the stream. When the end of the file is reached, an exception of type EOFExcepion will be thrown so the loop will be terminated and the code in the catch block for this exception will execute. This outputs a message to the command line showing the number of objects that were read. As you can see, we get back all the objects that we wrote to the file originally.

Determining the Class of a Deserialized Object

Clearly, since the readObject() method returns the object that it reads from the stream as type Object, you need to know what the original type of the object was in order to be able to cast it to its actual type. For the most part, you will know what the class of the object is when you read it back. It is possible that in some circumstances you won't know exactly, but you have a rough idea, in which case you can test it. To bring the problem into sharper focus let's consider a hypothetical situation.

Suppose you have a file containing objects that represent employees. The basic characteristics of all employees are defined in a base class, Person, but various different types of employee are represented by subclasses of Person. You might have subclasses Manager, Secretary, Admin, and ShopFloor for instance. The file can contain any of the subclass types in any sequence. Of course, you can cast any object read from the file to type Person because that is the base class, but you want to know precisely what each object is so you can call some type specific methods. Since you know what the possible types are you can check the type of the object against each of these types and cast accordingly.

Person person = null;
try {
  person = (Person)objectIn.readObject();
  if(person instanceof Manager)
    processManager((Manager)person);
  else if(person instanceof Secretary)
    processSecretary((Secretary)person);
    // and so on…

} catch (IOException e){
}

Here we determine the specific class type of the object read from the file before calling a method that deals with that particular type of object. Don't forget though that the instanceof operator does not guarantee that the object being tested is actually of the type – Manager say. The object could also be of any type that is a subclass of Manager. In any event, the cast to type Manager will be perfectly legal.

Where you need to be absolutely certain of the type, you can use a different approach:

 if(person.getClass().getName().equals(Manager))
   processManager((Manager)person);
 else if(person.getClass().getName().equals(Secretary))
    processSecretary((Secretary)person);
 // and so on…

This calls the getClass() method (inherited from Object) for the object read from the file and that returns a reference to the Class object representing the class of the object. Calling the getName() method for the Class object returns the fully qualified name of the class. This approach guarantees that the object is of the type for which we are testing, and is not a subclass of that type.

Another approach would be to just execute a cast to a particular type, and catch the ClassCastException that is thrown when the cast is invalid. This is fine if you do not expect the exception to be thrown under normal conditions, but if on a regular basis the object read from the stream might be other than the type to which you are casting, you will be better off with code that avoids the need to throw and catch the exception as this adds quite a lot of overhead.

Reading Basic Data from an Object Stream

The ObjectInputStream class defines methods for reading basic types of data back from an object stream. They are:

readBoolean()

readByte()

readChar()

readShort()

readInt()

readLong()

readFloat()

readDouble()

They each return a value of the corresponding type and they can all throw an IOException if an error occurs, or an EOFException if the end-of-file is reached.

Just to make sure that the process of serializing and deserializing objects is clear, we will use it in a simple example.

Using Object Serialization

Back in Chapter 6, we produced an example that created PolyLine objects containing Point objects in a generalized linked list. This is a good basis for demonstrating how effectively serialization takes care of handling objects that are members of objects. We can just modify the class TryPolyLine to use serialization.

Try It Out – Serializing a Linked List

The classes PolyLine, Point, and LinkedList and the inner class ListItem are exactly the same as in Chapter 6 except that we need to implement the Serializable interface in each of them.

The PolyLine definition needs to be amended to:

import java.io.Serializable;

public final class PolyLine implements Serializable  {
  // Class definition as before...
}

The Point definition needs a similar change:

import java.io.Serializable;

public class Point implements Serializable {
  // Class definition as before...
}

The LinkedList class and its inner class likewise:

import java.io.Serializable;

public class LinkedList implements Serializable {
  // Class definition as before...
  private class ListItem implements Serializable {
    // Inner class definition as before...
  }
}

Of course, each file must also have an import statement for the java.io.Serializable class as in the code above.

The modified version of the TryPolyLine class to write the PolyLine objects to a stream looks like this:

import java.io.*;

public class TryPolyLine {
  public static void main(String[] args) {
    // Create an array of coordinate pairs
    double[][] coords = { {1., 1.}, {1., 2.}, { 2., 3.},
JavaScript Editor
 Java Tutorials Free JavaScript Editor