最终瞬态字段和序列化
在 Java 中序列化后是否可以将 finaltransient
字段设置为任何非默认值?我的用例是一个缓存变量——这就是它是瞬态的原因。我还有一个习惯,将 Map
字段设置为不会更改(即地图内容更改,但对象本身保持不变)final
。然而,这些属性似乎是矛盾的——虽然编译器允许这样的组合,但在反序列化后我不能将字段设置为除 null
之外的任何值。
我尝试了以下操作,但没有成功:
- 简单的字段初始化(如示例所示):这是我通常所做的,但反序列化后似乎没有发生初始化;
- 构造函数中的初始化(我相信这在语义上与上面相同);
- 在
readObject()
中分配字段 - 无法完成,因为该字段是final
。
在示例中,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
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
不幸的是,简短的答案是“不”——我经常想要这个。但瞬态不可能是最终的。
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.)
您可以使用反射更改字段的内容。适用于 Java 1.5+。它会起作用,因为序列化是在单个线程中执行的。在另一个线程访问同一个对象后,它不应该更改最终字段(因为内存模型和反射的奇怪性)。
因此,在
readObject()
中,您可以执行与此示例类似的操作:记住:决赛不再是决赛!
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!
是的,通过实现(显然鲜为人知!)
readResolve()
方法,这很容易实现。它允许您在反序列化后替换对象。您可以使用它来调用构造函数,该构造函数将根据需要初始化替换对象。示例:输出 - 字符串被保留,但瞬态映射被重置为空(但非空!)映射:
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:
此类问题的一般解决方案是使用“串行代理”(请参阅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.
五年后,当我通过谷歌偶然发现这篇文章后,我发现原来的答案并不令人满意。另一种解决方案是根本不使用反射,并使用 Boann 建议的技术。
它还使用 GetField由
ObjectInputStream#readFields()
方法返回的类,根据序列化规范,必须在私有readObject(...)
方法中调用该类。该解决方案通过将检索到的字段存储在反序列化过程创建的临时“实例”的临时瞬态字段(称为 FinalExample#fields)中,使字段反序列化变得显式。然后反序列化所有对象字段并调用 readResolve(...) :创建一个新实例,但这次使用构造函数,丢弃带有临时字段的临时实例。该实例使用
GetField
实例显式恢复每个字段;与任何其他构造函数一样,这是检查任何参数的地方。如果构造函数抛出异常,则会将其转换为InvalidObjectException
并且该对象的反序列化失败。包含的微基准确保该解决方案不慢于默认的序列化/反序列化。事实上,它在我的电脑上:
然后这是代码:
注意事项:每当类引用另一个对象实例时,就有可能泄漏序列化过程创建的临时“实例”:仅发生对象解析读取所有子对象后,因此子对象可以保留对临时对象的引用。类可以通过检查
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 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!
这个问题是关于Java默认序列化器的,但我是通过搜索Gson来到这里的。这个答案不适用于默认序列化器,但它确实适用于 Gson 甚至其他序列化器。我不喜欢(手动)使用 Reflection 或 readResolve,所以这里有其他内容。
反序列化时,Gson 调用默认构造函数来创建对象。您可以将临时最终分配移至默认构造函数,它们将被正确分配。如果您只有一个分配最终变量(例如 ID)的非默认构造函数,那么您将它们分配给什么并不重要,因为它们将被 Gson 使用反射覆盖。
这确实意味着,如果您的瞬态最终赋值依赖于构造函数参数,则这将不起作用。
这是一些示例代码:
打印:
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:
Prints: