最终瞬态字段和序列化

发布于 2024-09-04 03:04:27 字数 1251 浏览 5 评论 0原文

在 Java 中序列化后是否可以将 finaltransient 字段设置为任何非默认值?我的用例是一个缓存变量——这就是它是瞬态的原因。我还有一个习惯,将 Map 字段设置为不会更改(即地图内容更改,但对象本身保持不变)final。然而,这些属性似乎是矛盾的——虽然编译器允许这样的组合,但在反序列化后我不能将字段设置为除 null 之外的任何值。

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

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

在示例中,cachepublic 仅用于测试。

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

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

绿萝 2024-09-11 03:04:27

不幸的是,简短的答案是“不”——我经常想要这个。但瞬态不可能是最终的。

Final 字段必须通过直接分配初始值或在构造函数中进行初始化。在反序列化期间,这两个都不会被调用,因此瞬态的初始值必须在反序列化期间调用的“readObject()”私有方法中设置。为了使其发挥作用,瞬态必须是非最终的。

(严格来说,决赛只有在第一次被读取时才是最终的,因此可能有一些黑客可能会在读取之前分配一个值,但对我来说,这一步太过分了。)

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

最单纯的乌龟 2024-09-11 03:04:27

您可以使用反射更改字段的内容。适用于 Java 1.5+。它会起作用,因为序列化是在单个线程中执行的。在另一个线程访问同一个对象后,它不应该更改最终字段(因为内存模型和反射的奇怪性)。

因此,在 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!
    }

}

记住:决赛不再是决赛!

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!

忘你却要生生世世 2024-09-11 03:04:27

是的,通过实现(显然鲜为人知!)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' {}

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:

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

Output -- the string is preserved but the transient map is reset to an empty (but non-null!) map:

Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}
回忆凄美了谁 2024-09-11 03:04:27

此类问题的一般解决方案是使用“串行代理”(请参阅​​Effective Java 第二版)。如果您需要将其改进为现有的可序列化类而不破坏串行兼容性,那么您将需要进行一些黑客攻击。

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.

别把无礼当个性 2024-09-11 03:04:27

五年后,当我通过谷歌偶然发现这篇文章后,我发现原来的答案并不令人满意。另一种解决方案是根本不使用反射,并使用 Boann 建议的技术。

它还使用 GetFieldObjectInputStream#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;
        }

    }

}

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

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

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 private readObject(...) 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 and readResolve(...) 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 the GetField 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 an InvalidObjectException 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:

Problem: 8.598s Solution: 7.818s

Then here is the code:

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

    }

}

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!

沒落の蓅哖 2024-09-11 03:04:27

这个问题是关于Java默认序列化器的,但我是通过搜索Gson来到这里的。这个答案不适用于默认序列化器,但它确实适用于 Gson 甚至其他序列化器。我不喜欢(手动)使用 Reflection 或 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: {}

This question is about the Java default serializer, but I landed here from searching about Gson. This answer doesn't apply to the default serializer, but it does apply to Gson and maybe others. I wasn't a fan of (manually) using Reflection or readResolve, so here's something else.

When deserializing, Gson calls the default constructor to create the object. You can move your transient final assignments to the default constructor, and they will be assigned properly. If you only have a non-default constructor that assigns final variables (for example, an ID), it won't matter what you assign them to as they will be overwritten by Gson with Reflection.

This does mean that if your transient final assignments relies on constructor arguments, this won't work.

Here is some example code:

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

Prints:

broken: null
fixed: {}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文