A major cost of
implementing Serializable is that it
decreases the flexibility to change a class’s implementation once it has been
released. When a class implements Serializable, its
byte-stream encoding (or serialized form) becomes part
of its exported API. Once you distribute a class widely, you are generally
required to support the serialized form forever, just as you are required to
support all other parts of the exported API. If you do not make the effort to
design a custom serialized form, but merely accept the default, the
serialized form will forever be tied to the class’s original internal
representation. In other words, if you accept the default serialized form, the
class’s private and package-private instance fields become part of its exported
API, and the practice of minimizing access to fields (Item 13) loses its effectiveness
as a tool for information hiding.
A simple
example of the constraints on evolution that accompany serializability concerns
stream
unique identifiers, more commonly known as serial version UIDs. Every
serializable class has a unique identification number associated with it. If
you do not specify this number explicitly by declaring a static final long field named serialVersionUID, the system
automatically generates it at runtime by applying a complex procedure to the
class. The automatically generated value is affected by the class’s name, the
names of the interfaces it implements, and all of its public and protected
members. If you change any of these things in any way, for example, by adding a
trivial convenience method, the automatically generated serial version UID
changes. If you fail to declare an explicit serial version UID, compatibility
will be broken, resulting in an InvalidClassException
at
runtime.
A second cost of
implementing Serializable is that it
increases the likelihood of bugs and security holes. Normally,
objects are created using constructors; serialization is an extralinguistic
mechanism for creating objects. Whether you accept the default behavior or
override it, deserialization is a “hidden constructor” with all of the same issues
as other constructors. Because there is no explicit constructor associated with
deserialization, it is easy to forget that you must ensure that it guarantees
all of the invariants established by the constructors and that it does not
allow an attacker to gain access to the internals of the object under
construction. Relying on the default deserialization mechanism can easily leave
objects open to invariant corruption and illegal access (Item 76).
A third cost of
implementing Serializable is that it increases
the testing burden associated with releasing a new version of a class. When a
serializable class is revised, it is important to check that it is possible to
serialize an instance in the new release and deserialize it in old releases,
and vice versa. The amount of testing required is thus proportional to the
product of the number of serializable classes and the number of releases, which
can be large. These tests cannot be constructed automatically because, in
addition to binary compatibility, you must test for semantic
compatibility. In other words, you must ensure both that the serialization-
deserialization process succeeds and that it results in a faithful replica of
the original object.The greater the change to a serializable class, the greater
the need for testing. The need is reduced if a custom serialized form is
carefully designed when the class is first written (Items 75, 78), but it does
not vanish entirely.
Implementing the
Serializable interface is not
a decision to be undertaken lightly. It offers real benefits. It is essential if a
class is to participate in a framework that relies on serialization for object
transmission or persistence. Also, it greatly eases the use of a class as a
component in another class that must implement Serializable. There are,
however, many real costs associated with implementing Serializable. Each time you design a class, weigh the
costs against the benefits. As a rule of thumb, value classes such as Date and BigInteger
should
impement Serializable, as should most
collection classes. Classes representing active entities, such as thread pools,
should rarely implement Serializable.
Classes designed
for inheritance (Item 17) should rarely implement Serializable, and interfaces should rarely extend
it. Violating
this rule places a significant burden on anyone who extends the class or
implements the interface. There are times when it is appropriate to violate the
rule. For example, if a class or interface exists primarily to participate in a
framework that requires all participants to implement Serializable, then it makes perfect sense for the class
or interface to implement or extend Serializable.
There is one
caveat regarding the decision not to implement Serializable. If a class that is designed for inheritance
is not serializable, it may be impossible to write a serializable subclass.
Specifically, it will be impossible if the superclass does not provide an
accessible parameterless constructor. Therefore, you should
consider providing a parameterless constructor on nonserializable classes
designed for inheritance. Often this requires no effort because many
classes designed for inheritance have no state, but this is not always the
case.
Inner
classes (Item 22) should not implement Serializable. They use
compiler-generated synthetic fields to store references to enclosing
instances and to store values of local variables from enclosing scopes. How
these fields correspond to the class definition is unspecified, as are the
names of anonymous and local classes. Therefore, the default
serialized form of an inner class is illdefined. A static
member class can, however, implement Serializable.
To summarize,
the ease of implementing Serializable is specious.
Unless a class is to be thrown away after a short period of use, implementing Serializable is a serious commitment that should be made
with care. Extra caution is warranted if a class is designed for inheritance.
For such classes, an intermediate design point between implementing Serializable and prohibiting it in subclasses is to
provide an accessible parameterless constructor. This design point permits, but
does not require, subclasses to implement Serializable.
Reference: Effective Java 2nd Edition by Joshua Bloch