Javaでのシリアル化後に_final transient
_フィールドをデフォルト以外の値に設定することは可能ですか?私のユースケースはキャッシュ変数です—それがtransient
である理由です。また、変更されない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
_
短い答えは残念ながら「いいえ」です-私はしばしばこれを望んでいました。しかし、トランジェントは最終的なものにはなりません。
最終フィールドは、初期値を直接割り当てるか、コンストラクターで初期化する必要があります。逆シリアル化中にはこれらのどちらも呼び出されないため、トランジェントの初期値は、逆シリアル化中に呼び出される「readObject()」プライベートメソッドに設定する必要があります。そして、それが機能するためには、トランジェントが非ファイナルである必要があります。
(厳密に言えば、決勝戦は最初に読み取られるときだけ決勝戦なので、値が読み取られる前に値を割り当てるハックが存在する可能性がありますが、私にとってこれは一歩行き過ぎです。)
Reflectionを使用してフィールドの内容を変更できます。 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!
}
}
覚えておいてください FinalはもうFinalではありません!
はい、これは(明らかにほとんど知られていない!)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);
}
}
}
出力-文字列は保持されますが、一時的なマップは空の(しかしnullではない!)マップにリセットされます。
Before: test$X@172e0cc 'This data will be serialized' {This data=is transient}
After: test$X@490662 'This data will be serialized' {}
このような問題の一般的な解決策は、「シリアルプロキシ」を使用することです(効果的なJava 2nd Edを参照)。シリアル互換性を壊すことなく、これを既存のシリアル化可能クラスに組み込む必要がある場合は、いくつかのハッキングを行う必要があります。
5年後、Google経由でこの投稿を偶然見つけたので、元の回答に満足できません。別の解決策は、まったく反射を使用せず、Boannによって提案された手法を使用することです。
また、ObjectInputStream#readFields()
メソッドによって返される GetField クラスを利用します。このクラスは、シリアライゼーション仕様に従って、プライベートreadObject(...)
メソッドで呼び出す必要があります。
このソリューションは、取得したフィールドを、逆シリアル化プロセスによって作成された一時的な "インスタンス"の一時的な一時フィールド(_FinalExample#fields
_と呼ばれる)に格納することで、フィールドの逆シリアル化を明示的にします。次に、すべてのオブジェクトフィールドが逆シリアル化され、readResolve(...)
が呼び出されます。新しいインスタンスが作成されますが、今回はコンストラクターを使用して、一時フィールドを持つ一時インスタンスを破棄します。インスタンスは、GetField
インスタンスを使用して各フィールドを明示的に復元します。これは、他のコンストラクターと同様にパラメーターをチェックする場所です。コンストラクターによって例外がスローされた場合、例外はInvalidObjectException
に変換され、このオブジェクトの逆シリアル化は失敗します。
含まれているマイクロベンチマークは、このソリューションがデフォルトのシリアライゼーション/デシリアライゼーションより遅くないことを保証します。確かに、それは私のPCにあります。
_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であることを確認することにより、このような不正に構築されたインスタンスの使用を確認できます。 nullの場合のみ、逆シリアル化プロセスではなく、通常のコンストラクタを使用して作成されました。
自己紹介:おそらく5年以内にもっと良い解決策が存在するでしょう。ではまたね!