Is it possible to have final transient
fields that are set to any non-default value after serialization in Java? My usecase is a cache variable — that's why it is transient
. I also have a habit of making Map
fields that won't be changed (i.e. contents of the map is changed, but object itself remains the same) final
. However, these attributes seem to be contradictory — while compiler allows such a combination, I cannot have the field set to anything but null
after unserialization.
I tried the following, without success:
- simple field initialization (shown in the example): this is what I normally do, but the initialization doesn't seem to happen after unserialization;
- initialization in constructor (I believe this is semantically the same as above though);
- assigning the field in
readObject()
— cannot be done since the field isfinal
.
In the example cache
is public
only for testing.
import java.io.*;
import java.util.*;
public class test
{
public static void main (String[] args) throws Exception
{
X x = new X ();
System.out.println (x + " " + x.cache);
ByteArrayOutputStream buffer = new ByteArrayOutputStream ();
new ObjectOutputStream (buffer).writeObject (x);
x = (X) new ObjectInputStream (new ByteArrayInputStream (buffer.toByteArray ())).readObject ();
System.out.println (x + " " + x.cache);
}
public static class X implements Serializable
{
public final transient Map <Object, Object> cache = new HashMap <Object, Object> ();
}
}
Output:
test$X@1a46e30 {}
test$X@190d11 null
You can change the contents of a field using Reflection. Works on Java 1.5+. It will work, because serialization is performed in a single thread. After another thread access the same object, it shouldn't change the final field (because of weirdness in the memory model & reflaction).
So, in
readObject()
, you can do something similar to this example:Remember: Final is not final anymore!
The short answer is "no" unfortunately - I've often wanted this. but transients cannot be final.
A final field must be initialized either by direct assignment of an initial value or in the constructor. During deserialization, neither of these are invoked, so initial values for transients must be set in the 'readObject()' private method that's invoked during deserialization. And for that to work, the transients must be non-final.
(Strictly speaking, finals are only final the first time they are read, so there are hacks that are possible that assign a value before it is read, but for me this is going one step too far.)
Yes, this is easily possible by implementing the (apparently little known!)
readResolve()
method. It lets you replace the object after it is deserialized. You can use that to invoke a constructor that will initialize a replacement object however you want. An example:Output -- the string is preserved but the transient map is reset to an empty (but non-null!) map:
The general solution to problems like this is to use a "serial proxy" (see Effective Java 2nd Ed). If you need to retrofit this to an existing serialisable class without breaking serial compatibility, then you will need to do some hacking.
Five years later, I find my original answer unsatisfactory after I stumbled across this post via Google. Another solution would be using no reflection at all, and use the technique suggested by Boann.
It also makes use of the GetField class returned by
ObjectInputStream#readFields()
method, which according to the Serialization specification must be called in the privatereadObject(...)
method.The solution makes field deserialization explicit by storing the retrieved fields in a temporary transient field (called
FinalExample#fields
) of a temporary "instance" created by the deserialization process. All object fields are then deserialized andreadResolve(...)
is called: a new instance is created but this time using a constructor, discarding the temporary instance with the temporary field. The instance explicitly restores each field using theGetField
instance; this is the place to check any parameters as would any other constructor. If an exception is thrown by the constructor it is translated to anInvalidObjectException
and deserialization of this object fails.The micro-benchmark included ensures that this solution is not slower than default serialization/deserialization. Indeed, it is on my PC:
Then here is the code:
A note of caution: whenever the class refers to another object instance, it might be possible to leak the temporary "instance" created by the serialization process: the object resolution occurs only after all sub-objects are read, hence it is possible for subobjects to keep a reference to the temporary object. Classes can check for use of such illegally constructed instances by checking that the
GetField
temporary field is null. Only when it is null, it was created using a regular constructor and not through the deserialization process.Note to self: Perhaps a better solution exists in five years. See you then!