Serialization has an undeserving bad reputation, perhaps caused by too
many developers just adding implements Serializable and accepting the
default serialized form in public API, then turning around and saying
they won't support backward compatible Serialization.
In the implementation discussed below all objects are using just one
public API class with static factory methods, to keep it simple for user
developers.
I've been adding serialization to reference collections (just a bunch of
wrapper classes that encapsulate any collection framework interface and
perform the boilerplate of retrieving referents, wrapping them in
references and removing enqueued references from those collections,
allowing the choice of Weak, Soft, Strong references with identity,
equals and comparable semantics.
All the following package private wrapper classes share a single
serialized form at present:
ReferenceCollection
ReferenceList
ReferenceSet
ReferenceSortedSet
ReferenceNavigableSet
ReferenceQueue
ReferenceDeque
ReferenceBlockingQueue
ReferenceBlockingDeque
The serial form is generated using writeReplace, and it recreates the
correct collection using ReadResolve.
Now because each wrapper class is only publicly visible to the client as
a java collection framework (JCF) interface, the serialized form (also
called a serialization proxy), rebuilds it using the standard public api
factory class during de-serialization, based on the JCF interface it
implemented. So the remote end is free to use another implementation.
Now there's a readResolve bug worth mentioning here, with regard to
circular references. writeReplace replaces all original object
instances with your serialized form object, but readResolve doesn't
replace circular referenced objects during de serialization. So if
you're utilising readResolve to replace your serialized form, you'll end
up with a mix of the serialized form object and your freshly constructed
implementation object. You'll get ClassCastExceptions etc...
Bob Lee, that's Crazy Bob from JSR330 and Google Guice, came up with the
idea of having the serialization proxy and original objects share the
same interface, then having all methods redirected to the newly built
object upon de-serialization.
So to implement that, I've got an inheritance hierarchy for the
serialization proxy, to separate each function:
SerializationOfReferenceCollection
|
ReadResolveFixCollectionCircularReferences
|
ReferenceCollectionRefreshAfterSerialization
|
ReferenceCollectionSerialData
Now right about now, you're probably saying 4 classes in an inheritance
hierarchy is a bit heavy for serialization?
Well no, not when you consider: they serialize 9 classes, and of all
those classes, only one, ReferenceCollection has to implement a final
writeReplace method, while all have to implement a readObject method
that throws an exception to prevent direct de-serialization.
So all the 9 classes are freed from the implementation of Serialization,
it's now the responsibility of the 4 classes in the serialization proxy
(Serialization builder pattern) inheritance hierarchy.
Function of each class in the inheritance hierarchy:
SerializationOfReferenceCollection is an abstract class with a static
factory method.
ReadResolveFixCollectionCircularReferences implements all the JCF
collection based interfaces and redirects their calls to the
ReferenceCollection implementation built during de-serialization.
ReferenceCollectionRefreshAfterSerialization, updates all the References
contained by the collection so they belong to the same garbage
collection ReferenceQueue and creates new References for all referents.
ReferenceCollectionSerialData, contains the fields transferred during
serialization and implements abstract methods for the super classes to
"get" these fields.
Now the interesting part is, I'm considering having three different
serialized form's, each with a different purpose, the client can choose
from:
1. A Non serialization class, that prevents serialization, where a
developer want's to prevent access to serialized state.
2. The default serial data.
3. Defensive copying of serial data, to prevent stolen references to
internal state during de-serialization.
The choice between the three serial states can be left until runtime,
the recipient of these objects when serialized doesn't have a choice
which serial form is used, only the creator of the original object does.
Items 1 and 3 would only be used in a local sense, where a client
program might try to use serialization to gain access to internal
implementation state.
Item 2 would be used in a genuine distributed environment, over a secure
connection, where there is no point using defensive copy's.
I've only implemented Item 2 of course, I decided that while it is
possible to do 1 and 3 as well to demonstrate just how flexible
serialization can be, it wasn't warranted based on that alone. It will
be possible to do this at some point in future, or to change the serial
form in a non compatible manner, by adding a new serial form class,
while retaining the original, so that both the old and new serial forms
can be de-serialized.
When you apply Object design principals of responsibility, even
serialization can be flexible.
Serialized Form lock in, is the same as inappropriate use of public
fields or other poor programming practices. Note that there are times
where standard rules don't apply like the use of public fields in
Entry's, which is totally appropriate, just as accepting the standard
serial form in package private classes is appropriate too.
Cheers,
Peter.