Using a DataOutputStream
, you
could write an application that saves the data content of your objects one
at a time as simple types. However, Java provides an even more powerful
mechanism called object serialization that does almost all the work for
you. In its simplest form, object serialization is an
automatic way to save and load the state of an object. However, object
serialization has greater depths that we cannot plumb within the scope of
this book, including complete control over the serialization process and
interesting twists such as class versioning.
Basically, an instance of any class that implements the Serializable
interface
can be saved to and restored from a stream. The stream subclasses,
ObjectInputStream
and
ObjectOutputStream
, are
used to serialize primitive types and objects. Subclasses of Serializable
classes are also serializable. The
default serialization mechanism saves the value of all of the object’s
fields (public and private), except those that are static and those marked
transient.
One of the most important (and tricky) things about serialization is
that when an object is serialized, any object references it contains are
also serialized. Serialization can capture entire “graphs” of
interconnected objects and put them back together on the receiving end
(we’ll demonstrate this in an upcoming example). The implication is that
any object we serialize must contain only references to other Serializable
objects. We can prune the tree and
limit the extent of what is serialized by marking nonserializable
variables as transient
or overriding
the default serialization mechanisms. The transient
modifier can be applied to any
instance variable to indicate that its contents are not useful outside of
the current context and should not be saved.
In the following example, we create a Hashtable
and write it to a disk file called
hash.ser. The Hashtable
object is already serializable because
it implements the Serializable
interface.
import
java.io.*
;
import
java.util.*
;
public
class
Save
{
public
static
void
main
(
String
[]
args
)
{
Hashtable
hash
=
new
Hashtable
();
hash
.
put
(
"string"
,
"Gabriel Garcia Marquez"
);
hash
.
put
(
"int"
,
new
Integer
(
26
));
hash
.
put
(
"double"
,
new
Double
(
Math
.
PI
));
try
{
FileOutputStream
fileOut
=
new
FileOutputStream
(
"hash.ser"
);
ObjectOutputStream
out
=
new
ObjectOutputStream
(
fileOut
);
out
.
writeObject
(
hash
);
out
.
close
();
}
catch
(
Exception
e
)
{
System
.
out
.
println
(
e
);
}
}
}
First, we construct a Hashtable
with a few elements in it. Then, in the lines of code inside the try
block, we write the Hashtable
to a file called
hash.ser, using the writeObject()
method of
ObjectOutputStream
. The ObjectOutputStream
class is a lot like the DataOutputStream
class, except that it includes
the powerful writeObject()
method.
The Hashtable
that we created has
internal references to the items it contains. Thus, these components are
automatically serialized along with the Hashtable
. We’ll see this in the next example
when we deserialize the Hashtable
.
import
java.io.*
;
import
java.util.*
;
public
class
Load
{
public
static
void
main
(
String
[]
args
)
{
try
{
FileInputStream
fileIn
=
new
FileInputStream
(
"hash.ser"
);
ObjectInputStream
in
=
new
ObjectInputStream
(
fileIn
);
Hashtable
hash
=
(
Hashtable
)
in
.
readObject
();
System
.
out
.
println
(
hash
.
toString
()
);
}
catch
(
Exception
e
)
{
System
.
out
.
println
(
e
);
}
}
}
In this example, we read the Hashtable
from the hash.ser
file, using the readObject()
method of
ObjectInputStream
. The ObjectInputStream
class is a lot like DataInputStream
, except that it includes the
readObject()
method. The return type of
readObject()
is Object
, so we need to cast it to a Hashtable
. Finally, we print the contents of the
Hashtable
using its toString()
method.
Often, simple deserialization alone is not enough to
reconstruct the full state of an object. For example, the object may
have had transient fields representing state that could not be
serialized, such as network connections, event registration, or decoded
image data. Objects have an opportunity to do their own setup after
deserialization by implementing a special method named readObject()
.
Not to be confused with the readObject()
method of the ObjectInputStream
, this method is implemented
by the serializable object itself. To be recognized and used, the
readObject()
method must have a
specific signature, and it must be private. The following snippet is
taken from an animated JavaBean that we’ll talk about in Chapter 22:
private
void
readObject
(
ObjectInputStream
s
)
throws
IOException
,
ClassNotFoundException
{
s
.
defaultReadObject
();
initialize
();
if
(
isRunning
)
start
();
}
When the readObject()
method
with this signature exists in an object, it is called during the
deserialization process. The argument to the method is the ObjectInputStream
doing the object
construction. We delegate to its defaultReadObject()
method to do the normal
deserialization from the stream and then do our custom setup. In this
case, we call one of our methods named initialize()
and, depending on our state, a
method called start()
.
Using a custom implementation of readObject()
and a corresponding writeObject()
method, we could take complete
control of the serialized form of the object by reading and writing to
the stream using lower-level write operations (bytes, strings, etc.)
instead of delegating to the default implementation as we did
before.
We’ll talk a little more about serialization in Chapter 22 when we discuss JavaBeans.
Java object serialization was designed to accommodate
certain kinds of compatible class changes or
evolution in the structure of classes. For example, changing the methods
of a class does not necessarily mean that its serialized representation
must change because only the data of variables is stored. Nor would
simply adding a new field to a class necessarily prohibit us from
loading an old serialized version of the class. We could simply allow
the new variable to take its default value. By default, however, Java is
very picky and errs on the side of caution. If you make any kind of
change to the structure of your class, by default you’ll get an InvalidClassException
when trying to read
previously serialized forms of the class.
Java detects these versions by performing a hash function on the structure of the class and storing a 64-bit value called the Serial Version UID (SUID), along with the serialized data. It can then compare the hash to the class when it is loaded.
Java allows us to take control of this process by looking for a special, magic field in our classes that looks like the following:
static
final
long
serialVersionUID
=
-
6849794470754667710L
;
(The value is, of course, different for every class.) If it finds
this static serialVersionUID long
field in the class, it uses its value instead of performing the hash on
the class. This value will be written out with serialized versions of
the class and used for comparison when they are deserialized. This means
that we are now in control of which versions of the class are compatible
with which serialized representations. For example, we can create our
serializable class from the beginning with our own SUID and then only
increment it if we make a truly incompatible change and want to prevent
older forms of the class from being loaded:
class
MyDataObject
implements
Serializable
{
static
final
long
serialVersionUID
=
1
;
// Version 1
...
}
A utility called serialver that comes with the JDK allows you to calculate the hash that Java would otherwise use for the class. This is necessary if you did not plan ahead and already have serialized objects stored and need to modify the class afterward. Running the serialver command on the class displays the SUID that is necessary to match the value already stored:
%
serialver
SomeObject
static
final
long
serialVersionUID
=
-
6849794470754667710L
;
By placing this value into your class, you can “freeze” the SUID at the specified value, allowing the class to change without affecting versioning.
Get Learning Java, 4th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.