Effective Java: Write readObject Methods Defensively
Kyle Carter
Posted on February 1, 2022
In a previous item, a date range class was discussed. It includes Date
fields and is careful to avoid breaking its invariants of its start date needing to come before its end date. The way that it accomplishes that is via careful coding of its constructor as well as its accessors. Let's refresh our familiarity with this class:
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException("Start is after end");
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
Now let's consider if the need arose to make this class Serializable
. Thinking back to our previous item of discussion about the physical and logical models of our classes, it is reasonable to come to the conclusion that we can use this same form as the serialized form. Because of this, we may be tempted to simply throw implements Serializable
on the class and call it good. Unfortunately, this would open our class up to not keeping its invariants.
As we have discussed before, Java's default serialization system effectively creates a new hidden constructor for our class. In our existing implementation of the Period
class the constructor is very critical to facilitating the safety of our class invariants. The effectively new constructor that exists with serialization does not have these same checks that protect our invariants so we must provide additional code to keep our internal data safe.
The way that Java facilitates our taking ownership of the construction of our object during deserialization is via the readObject
method. This method takes a byte stream as its sole parameter and populates the state of an object. Usually, the byte stream that is consumed by this method will have been generated by serializing a normally constructed (and thus invariant keeping) instance of the object. However, since it is simply a stream of bytes, we can never be sure where those bytes came from and whether we can trust the source. We thus could be presented with an artificially created byte stream that does not meet our invariants and thus we can end up with an object that shouldn't be possible to create.
With this new consideration in mind we may attempt to resolve the issue by adding a method such as:
private void readObject(ObjectInputStream inputStream)
throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
While the spirit of the above is reasonable it is not enough. For the same reason that the original blog post was written (making defensive copies) this implementation opens itself up for being passed a byte stream that preserves the invariants of the class initially but then, since the reference could be modified by code outside of the class, the invariants could be broken by simply changing the value outside the class.
To solve this problem we must remember to always defensively copy any field that contains an object reference when using serialization. We thus can extend our above solution as follows to make it safe:
private void readObject(ObjectInputStream inputStream)
throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
start = new Date(start.getTime());
end = new Date(end.getTime());
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + " after " + end);
}
}
In this version, we do our defensive copy and then do our validity check. This allows us to have full control of the variables when we do the check. We, unfortunately, do need to remove the final
modifier on the member variables for this to work but that is the price we pay for safe deserialization.
The test that you can perform to determine if the default deserialization method will work is, would you be comfortable having a constructor on your class that simply took in the member variables and saved the state without any further validation? If so, then you are likely safe using the default deserialization method as far as defensive copying goes, if not, you should take steps to protect yourself from these possible issues.
The final item of consideration is that of making sure not to call overridable methods from the readObject
method. This is the same caution that applies to constructors (because readObject
is effectively a constructor) and for the same reason. If you call overridable methods from the readObject
method you are open to having those methods called before the whole state of the object is initialized which can lead to issues.
In summary, it is best to remember that when using serialization you are effectively creating a new public constructor for your class. If you wouldn't be comfortable with having such a public constructor for your class, implement the readObject
method and make sure that it takes care of the state in a defensive manner. The byte streams that your readObject
method is passed should be handled as if they didn't come from a trusted source (because it may not have). If an entire object graph must be validated after being deserialized you can use the ObjectInputValidation
interface (not discussed in this item).
Posted on February 1, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 21, 2022