最后的瞬态场和序列化

在Java中进行序列化后,是否有可能将final transient字段设置为任何非默认值? 我的用例是一个cachingvariables – 这就是为什么它是transient 。 我也有一个习惯,就是不要改变Map字段(即地图内容改变,但是对象本身保持不变)。 然而,这些属性似乎是矛盾的 – 虽然编译器允许这样的组合,我不能有字段设置为任何东西,但在反序列化之后为null

我尝试了以下,没有成功:

  • 简单的字段初始化(在这个例子中显示):这是我通常做的,但是在反序列化之后似乎没有发生初始化;
  • 在构造函数中初始化(我相信这在语义上与上面相同);
  • readObject()分配字段 – 由于该字段是final因此无法完成。

在这个例子中, cachepublic仅用于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> (); } } 

输出:

 test$X@1a46e30 {} test$X@190d11 null 

简而言之,不幸的是 – 我经常想要这个。 但瞬态不能是最终的。

最后一个字段必须通过直接赋值初始值或在构造函数中初始化。 在反序列化过程中,这两者都不会被调用,因此必须在反序列化过程中调用的'readObject()'私有方法中设置初始值。 而为了工作,瞬变必须是非最终的。

(严格地说,决赛只是在第一次阅读时才是最终决赛,所以在阅读之前有一些可能的分配值,但是对于我来说这太过分了。)

您可以使用“reflection”更改字段的内容。 适用于Java 1.5+。 这将工作,因为序列化是在一个线程中执行的。 在另一个线程访问同一个对象之后,它不应该改变最后的字段(因为内存模型和reflection的怪异)。

所以,在readObject() ,你可以做一些类似于这个例子:

 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: " + ba); // eg after serialization Field f = b.getClass().getDeclaredField("a"); f.setAccessible(true); f.set(b, 6); // eg putting back your cache System.out.println("Second: " + ba); // wow: it has a value! } } 

记住: 决赛不再是决赛!

是的,通过实现readResolve()方法,这很容易实现。 它允许您在反序列化之后replace对象。 你可以使用它来调用一个构造函数,它将初始化一个replace对象。 一个例子:

 import java.io.*; import java.util.*; public class test { public static void main(String[] args) throws Exception { X x = new X(); x.name = "This data will be serialized"; x.cache.put("This data", "is transient"); System.out.println("Before: " + x + " '" + x.name + "' " + x.cache); ByteArrayOutputStream buffer = new ByteArrayOutputStream(); new ObjectOutputStream(buffer).writeObject(x); x = (X)new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject(); System.out.println("After: " + x + " '" + x.name + "' " + x.cache); } public static class X implements Serializable { public final transient Map<Object,Object> cache = new HashMap<>(); public String name; public X() {} // normal constructor private X(X x) { // constructor for deserialization // copy the non-transient fields this.name = x.name; } private Object readResolve() { // create a new object from the deserialized one return new X(this); } } } 

输出 – string被保留,但瞬态映射被重置为空(但非空!)映射:

 Before: test$X@172e0cc 'This data will be serialized' {This data=is transient} After: test$X@490662 'This data will be serialized' {} 

这样的问题的一般解决scheme是使用“串行代理”(参见Effective Java 2nd Ed)。 如果你需要在不破坏串行兼容性的情况下对现有的serialisable类进行翻新,那么你将需要做一些黑客工作。

五年后,我发现我原来的答案不满意,我偶然发现通过谷歌这个职位。 另一种解决scheme是根本不使用reflection,并使用Boann提出的技术。

它还使用ObjectInputStream#readFields()方法返回的GetField类,该方法根据序列化规范必须在专用的readObject(...)方法中调用。

该解决scheme通过将检索到的字段存储在由反序列化过程创build的临时“实例”的临时瞬态字段(称为FinalExample#fields )中,使字段反序列化变得明确。 然后所有对象字段被反序列化,并调用readResolve(...) :创build一个新的实例,但是这次使用一个构造函数,放弃临时实例的临时实例。 该实例使用GetField实例显式地恢复每个字段; 这是任何其他构造函数检查任何参数的地方。 如果构造函数抛出exception,则会将其转换为InvalidObjectException并且此对象的反序列化将失败。

包含的微基准testing确保此解决scheme不会比默认的序列化/反序列化慢。 事实上,这是在我的电脑上:

 Problem: 8.598s Solution: 7.818s 

那么这里是代码:

 import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectInputStream.GetField; import java.io.ObjectOutputStream; import java.io.ObjectStreamException; import java.io.Serializable; import org.junit.Test; import static org.junit.Assert.*; public class FinalSerialization { /** * Using default serialization, there are problems with transient final * fields. This is because internally, ObjectInputStream uses the Unsafe * class to create an "instance", without calling a constructor. */ @Test public void problem() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); WrongExample x = new WrongExample(1234); oos.writeObject(x); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); WrongExample y = (WrongExample) ois.readObject(); assertTrue(y.value == 1234); // Problem: assertFalse(y.ref != null); ois.close(); baos.close(); bais.close(); } /** * Use the readResolve method to construct a new object with the correct * finals initialized. Because we now call the constructor explicitly, all * finals are properly set up. */ @Test public void solution() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); FinalExample x = new FinalExample(1234); oos.writeObject(x); oos.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); FinalExample y = (FinalExample) ois.readObject(); assertTrue(y.ref != null); assertTrue(y.value == 1234); ois.close(); baos.close(); bais.close(); } /** * The solution <em>should not</em> have worse execution time than built-in * deserialization. */ @Test public void benchmark() throws Exception { int TRIALS = 500_000; long a = System.currentTimeMillis(); for (int i = 0; i < TRIALS; i++) { problem(); } a = System.currentTimeMillis() - a; long b = System.currentTimeMillis(); for (int i = 0; i < TRIALS; i++) { solution(); } b = System.currentTimeMillis() - b; System.out.println("Problem: " + a / 1000f + "s Solution: " + b / 1000f + "s"); assertTrue(b <= a); } public static class FinalExample implements Serializable { private static final long serialVersionUID = 4772085863429354018L; public final transient Object ref = new Object(); public final int value; private transient GetField fields; public FinalExample(int value) { this.value = value; } private FinalExample(GetField fields) throws IOException { // assign fields value = fields.get("value", 0); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { fields = stream.readFields(); } private Object readResolve() throws ObjectStreamException { try { return new FinalExample(fields); } catch (IOException ex) { throw new InvalidObjectException(ex.getMessage()); } } } public static class WrongExample implements Serializable { private static final long serialVersionUID = 4772085863429354018L; public final transient Object ref = new Object(); public final int value; public WrongExample(int value) { this.value = value; } } } 

需要注意的是:每当类引用另一个对象实例时,可能会泄漏由序列化过程创build的临时“实例”:只有在读取所有子对象之后才会发生对象parsing,因此子对象保持对临时对象的引用。 类可以通过检查GetField临时字段为空来检查这种非法构造的实例的使用。 只有当它是空的,它是使用常规的构造函数创build的,而不是通过反序列化过程。

自我提醒:五年内可能会有更好的解决scheme。 回头见!