5年後、Google経由でこの投稿に出くわした後、元の回答が不十分であることがわかりました。別の解決策は、反射をまったく使用せず、Boannによって提案された手法を使用することです。
また、メソッドによって返されるGetFieldObjectInputStream#readFields()
クラスを利用します。これは、Serialization仕様に従って、プライベート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年以内に存在します。またね!