Item 39
contains an immutable date-range class containing mutable private Date fields. The class goes to great lengths to
preserve its invariants and its immutability by defensively copying Date objects in its constructor and accessors.
Here is the class:
// Immutable class that uses defensive copying
public final class Period {
private final Date start;
private final Date end;
/**
* @param start the beginning of the period
* @param end the end of the period; must not precede
start
* @throws IllegalArgumentException if start is after end
* @throws NullPointerException if start or end is null
*/
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(
start + " after " + end);
}
public Date start () { return new Date(start.getTime());
}
public Date end () { return new Date(end.getTime()); }
public String toString() { return start + " -
" + end; }
... // Remainder omitted
}
Suppose you
decide that you want this class to be serializable. Because the physical
representation of a Period object exactly
mirrors its logical data content, it is not unreasonable to use the default
serialized form (Item 75). Therefore, it might seem that all you have to do to
make the class serializable is to add the words “implements
Serializable” to the class declaration. If you did so, however, the class
would no longer guarantee its critical invariants.
The problem is
that the readObject method is
effectively another public constructor, and it demands all of the same care as
any other constructor. Just as a constructor must check its arguments for
validity (Item 38) and make defensive copies of parameters where appropriate (Item
39), so must a readObject method. If a readObject method fails to do either of these things, it
is a relatively simple matter for an attacker to violate the class’s
invariants.
Loosely
speaking, readObject is a
constructor that takes a byte stream as its sole parameter. In normal use, the
byte stream is generated by serializing a normally constructed instance. The
problem arises when readObject is presented
with a byte stream that is artificially constructed to generate an object that
violates the invariants of its class. To fix this problem, provide a readObject method for Period
that
calls defaultReadObject and then checks
the validity of the deserialized object. If the validity check fails, the readObject method throws an InvalidObjectException, preventing the deserialization
from completing:
// readObject method with validity checking
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after
"+ end);
}
While this fix
prevents an attacker from creating an invalid Period
instance,
there is a more subtle problem still lurking. It is possible to create a
mutable Period instance by
fabricating a byte stream that begins with a valid Period instance and then appends extra references to
the private Date fields internal to the Period instance.
The source of
the problem is that Period’s readObject method is not doing enough defensive copying.
When
an object is deserialized, it is critical to defensively copy any field
containing an object reference that a client must not possess. Therefore,
every serializable immutable class containing private mutable components must
defensively copy these components in its readObject
method.
The following readObject method
suffices to ensure Period’s invariants
and to maintain its immutability:
// readObject method with defensive copying and
validity checking
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
// Defensively copy our mutable components
start = new Date(start.getTime());
end = new Date(end.getTime());
// Check that our invariants are satisfied
if (start.compareTo(end) > 0)
throw new InvalidObjectException(start +" after
"+ end);
}
Note that the
defensive copy is performed prior to the validity check and that we did not use
Date’s clone
method
to perform the defensive copy. Both of these details are required to protect Period against attack (Item 39). Note also that
defensive copying is not possible for final fields. To use the readObject method, we must make the start and end
fields
nonfinal.
Here is a
simple litmus test for deciding whether the default readObject method is acceptable for a class: would you
feel comfortable adding a public constructor that took as parameters the values
for each nontransient field in the object and stored the values in the fields
with no validation whatsoever? If not, you must provide a readObject method, and it must perform all the validity
checking and defensive copying that would be required of a constructor. Alternatively,
you can use the serialization proxy pattern (Item 78).
There is one
other similarity between readObject methods and
constructors, concerning nonfinal serializable classes. A readObject method must not invoke an overridable method,
directly or indirectly (Item 17). If this rule is violated and the method is
overridden, the overriding method will run before the subclass’s state has been
deserialized. A program failure is likely to result.
To summarize,
anytime you write a readObject method, adopt
the mind-set that you are writing a public constructor that must produce a
valid instance regardless of what byte stream it is given. Do not assume that
the byte stream represents an actual serialized instance. While the examples in
this item concern a class that uses the default serialized form, all of the
issues that were raised apply equally to classes with custom serialized forms.
Here, in summary form, are the guidelines for writing a bulletproof readObject method:
• For classes
with object reference fields that must remain private, defensively copy each
object in such a field. Mutable components of immutable classes fall into this
category.
• Check any
invariants and throw an InvalidObjectException if a check
fails. The checks should follow any defensive copying.
• If an entire
object graph must be validated after it is deserialized, use the ObjectInputValidation interface [JavaSE6,
Serialization].
• Do not
invoke any overridable methods in the class, directly or indirectly.
Reference: Effective Java 2nd Edition by Joshua Bloch