代码之家  ›  专栏  ›  技术社区  ›  doublep

最终瞬态字段和序列化

  •  57
  • doublep  · 技术社区  · 14 年前

    有没有可能 final transient transient . 我还有一个习惯 Map 不会更改的字段(即地图的内容已更改,但对象本身保持不变) final null 非工业化之后。

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

    • 构造函数中的初始化(我相信这在语义上与上面相同);
    • 在中指定字段 readObject() 无法执行此操作,因为该字段无效 最终的 .

    在示例中 cache public 仅用于测试。

    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
    
    5 回复  |  直到 14 年前
        1
  •  34
  •   mdma    14 年前

    简单的回答是“不”,不幸的是-我经常想要这个。但瞬变不可能是最终的。

    (严格地说,期末考试只有在第一次阅读时才是期末考试,所以有些黑客可能会在阅读前赋值,但对我来说,这一步走得太远了。)

        2
  •  17
  •   Pindatjuh    14 年前

    所以,在 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: " + 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!
        }
    
    }
    

    记得: Final is not final anymore!

        3
  •  15
  •   Boann    10 年前

    是的,这很容易实现(显然鲜为人知!) readResolve()

    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);
            }
        }
    }
    

    Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
    After: test$X@490662 'This data will be serialized' {}
    
        4
  •  5
  •   Tom Hawtin - tackline    14 年前

    解决这类问题的一般方法是使用“串行代理”(参见有效的Java第2版)。如果您需要在不破坏串行兼容性的情况下将其改装为现有的可串行化类,那么您将需要进行一些黑客攻击。

        5
  •  4
  •   seh Alexei    9 年前

    它还利用 GetField ObjectInputStream#readFields() 方法,根据序列化规范,必须在私有 readObject(...) 方法。

    该解决方案通过将检索到的字段存储在临时临时临时字段(称为 FinalExample#fields )由反序列化进程创建的临时“实例”的。然后反序列化所有对象字段并 readResolve(...) 调用:创建了一个新实例,但这次使用构造函数,使用临时字段丢弃临时实例。实例使用 GetField 实例;这是检查任何参数的地方,就像其他构造函数一样。如果构造函数引发异常,则将其转换为 InvalidObjectException

    所包含的微基准测试可确保此解决方案不会比默认的序列化/反序列化慢。事实上,它在我的电脑上:

    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;
            }
    
        }
    
    }
    

    注意:每当类引用另一个对象实例时,可能会泄漏序列化过程创建的临时“实例”:对象解析仅在读取所有子对象之后发生,因此子对象可能会保留对临时对象的引用。类可以通过检查 临时字段为空。只有当它为null时,它才是使用常规构造函数而不是通过反序列化过程创建的。

    自我提醒:也许五年后会有更好的解决方案。到时候见!

        6
  •  0
  •   Liam    4 年前

    这个问题是关于Java默认序列化程序的,但我是通过搜索Gson而来到这里的。这个答案不适用于默认的序列化程序,但它确实适用于Gson,也许还有其他的。我不喜欢(手动)使用反射或 readResolve ,所以这里还有一件事。

    反序列化时,Gson调用默认构造函数来创建对象。您可以将临时最终赋值移动到默认构造函数,它们将被正确地赋值。如果您只有一个非默认的构造函数来分配最终变量(例如,一个ID),那么将它们分配给什么并不重要,因为它们将被Gson用反射覆盖。

    这确实意味着,如果您的临时最终赋值依赖于构造函数参数,这将不起作用。

    下面是一些示例代码:

    import com.google.gson.Gson;
    import java.util.HashMap;
    
    public class Test {
        public static void main(String[] args) {
    
            BrokenTestObject broken = new BrokenTestObject("broken");
            FixedTestObject fixed = new FixedTestObject("fixed");
    
            broken = serializeAndDeserialize(broken, BrokenTestObject.class);
            fixed = serializeAndDeserialize(fixed, FixedTestObject.class);
    
            System.out.println(broken.id + ": " + broken.someCache);
            System.out.println(fixed.id + ": " + fixed.someCache);
        }
    
        public static <O> O serializeAndDeserialize(O object, Class<O> c) {
            Gson gson = new Gson();
            String json = gson.toJson(object);
            return gson.fromJson(json, c);
        }
    
        public static class BrokenTestObject {
            public final String id;
            public transient final HashMap<String, String> someCache = new HashMap<>();
    
            public BrokenTestObject(String id) {
                this.id = id;
            }
        }
    
        public static class FixedTestObject {
            public final String id;
            public transient final HashMap<String, String> someCache;
    
            public FixedTestObject(String id) {
                this.id = id;
                this.someCache = new HashMap<>();
            }
    
            //only used during deserialization
            private FixedTestObject() {
                this.id = null; //doesn't matter, will be overwritten during deserialization
                this.someCache = new HashMap<>();
            }
        }
    }
    

    broken: null
    fixed: {}