views:

160

answers:

3

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 is final.

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
+1  A: 

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.)

mdma
Thanks. I suspected it was that way too, but wasn't sure I didn't miss something.
doublep
A: 

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:

import java.lang.reflect.Field;

public class FinalTransient {

    private final transient Object a = null;

    public static void main(String... args) throws Exception {
        FinalTransient b = new FinalTransient();

        System.out.println("First: " + b.a); // e.g. after serialization

        Field f = b.getClass().getDeclaredField("a");
        f.setAccessible(true);
        f.set(b, 6); // e.g. putting back your cache

        System.out.println("Second: " + b.a); // wow: it has a value!
    }

}

Remember: Final is not final anymore!

Pindatjuh
Well, it looks too messy, I guess it's easier to give up on `final` here ;)
doublep
A: 

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.

Tom Hawtin - tackline