Java 最终瞬态字段和序列化
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2968876/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
final transient fields and serialization
提问by doublep
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.
final transient
在 Java 中序列化后是否可以将字段设置为任何非默认值?我的用例是一个缓存变量——这就是为什么它是transient
. 我也有制作Map
不会改变的字段的习惯(即地图的内容改变了,但对象本身保持不变)final
。然而,这些属性似乎是矛盾的——虽然编译器允许这样的组合,但null
在反序列化之后,我不能将该字段设置为任何内容。
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
readObject()
— 无法完成,因为该字段是final
。
In the example cache
is public
only for testing.
示例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> ();
}
}
Output:
输出:
test$X@1a46e30 {}
test$X@190d11 null
采纳答案by mdma
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.
final 字段必须通过直接分配初始值或在构造函数中进行初始化。在反序列化期间,这些都不会被调用,因此必须在反序列化期间调用的“readObject()”私有方法中设置瞬态的初始值。为了让它起作用,瞬态必须是非最终的。
(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.)
(严格来说,finals 只是在第一次被读取时才是 final 的,所以有可能在读取之前分配一个值,但对我来说这太过分了。)
回答by Pindatjuh
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).
您可以使用反射更改字段的内容。适用于 Java 1.5+。它会起作用,因为序列化是在单个线程中执行的。在另一个线程访问同一个对象后,它不应该更改最终字段(因为内存模型和反射中的怪异)。
So, in readObject()
, you can do something similar to this example:
因此,在 中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!
}
}
Remember: Final is not final anymore!
请记住:决赛不再是决赛!
回答by Tom Hawtin - tackline
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.
此类问题的一般解决方案是使用“串行代理”(参见 Effective Java 2nd Ed)。如果您需要在不破坏串行兼容性的情况下将其改装为现有的可序列化类,那么您将需要进行一些黑客攻击。
回答by Boann
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:
是的,这很容易通过实现(显然鲜为人知!)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);
}
}
}
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' {}
回答by Pindatjuh
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.
五年后,当我通过谷歌偶然发现这篇文章后,我发现我原来的答案并不令人满意。另一种解决方案是完全不使用反射,并使用 Boann 建议的技术。
It also makes use of the GetFieldclass returned by ObjectInputStream#readFields()
method, which according to the Serialization specification must be called in the private readObject(...)
method.
它还利用方法返回的GetField类ObjectInputStream#readFields()
,根据序列化规范必须在私有readObject(...)
方法中调用该类。
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.
该解决方案通过将检索到的字段存储FinalExample#fields
在由反序列化过程创建的临时“实例”的临时临时字段(称为)中,使字段反序列化显式化。然后所有对象字段都被反序列化并被readResolve(...)
调用:创建一个新实例,但这次使用构造函数,丢弃带有临时字段的临时实例。实例使用实例显式恢复每个字段GetField
;这是检查任何参数的地方,就像任何其他构造函数一样。如果构造函数抛出异常,则将其转换为 anInvalidObjectException
并且此对象的反序列化失败。
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.
注意事项:每当类引用另一个对象实例时,可能会泄漏序列化过程创建的临时“实例”:对象解析仅在读取所有子对象后发生,因此子对象是可能的保持对临时对象的引用。类可以通过检查GetField
临时字段是否为空来检查此类非法构造实例的使用。只有当它为空时,才使用常规构造函数创建,而不是通过反序列化过程创建的。
Note to self: Perhaps a better solution exists in five years. See you then!
自我注意:也许五年后会有更好的解决方案。回头见!