66

final transientJavaでのシリアル化後に、デフォルト以外の値に設定されたフィールドを持つことは可能ですか?私のユースケースはキャッシュ変数です—それが理由ですtransient。また、変更されないフィールドを作成する習慣がありMapます(つまり、マップの内容は変更されますが、オブジェクト自体は同じままです)finalnullただし、これらの属性は矛盾しているようです。コンパイラはそのような組み合わせを許可しますが、シリアル化を解除した後以外にフィールドを設定することはできません。

私は次のことを試みましたが、成功しませんでした。

  • 単純なフィールドの初期化(例に示されています):これは私が通常行うことですが、初期化はシリアル化解除後に行われないようです。
  • コンストラクターでの初期化(これは意味的には上記と同じだと思います)。
  • でフィールドを割り当てる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
4

6 に答える 6

34

残念ながら、簡単な答えは「いいえ」です。私はこれが欲しかったことがよくあります。しかし、過渡現象は最終的なものにはなり得ません。

最終フィールドは、初期値を直接割り当てるか、コンストラクターで初期化する必要があります。デシリアライズ中は、どちらも呼び出されないため、トランジェントの初期値は、デシリアライズ中に呼び出される「readObject()」プライベートメソッドで設定する必要があります。そしてそれが機能するためには、過渡現象は非最終的でなければなりません。

(厳密に言えば、ファイナルは最初に読み取られたときにのみファイナルであるため、読み取られる前に値を割り当てる可能性のあるハックがありますが、私にとってはこれは一歩進んでいます。)

于 2010-06-03T19:00:00.223 に答える
17

リフレクションを使用して、フィールドの内容を変更できます。Java1.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!
    }

}

覚えておいてください:ファイナルはもうファイナルではありません!

于 2010-06-03T19:00:32.593 に答える
14

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' {}
于 2014-11-06T17:44:15.720 に答える
5

このような問題の一般的な解決策は、「シリアルプロキシ」を使用することです(Effective Java 2nd Edを参照)。シリアルの互換性を損なうことなく、これを既存のシリアル化可能なクラスに後付けする必要がある場合は、ハッキングを行う必要があります。

于 2010-06-04T10:42:52.413 に答える
4

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年以内に存在します。またね!

于 2015-07-12T16:34:20.283 に答える
0

この質問はJavaのデフォルトのシリアライザーに関するものですが、Gsonについて検索してここにたどり着きました。この回答はデフォルトのシリアライザーには適用されませんが、Gsonやその他の人には適用されます。私は(手動で)Reflectionまたはを使用するのが好きではなかったreadResolveので、ここに別の何かがあります。

デシリアライズするとき、Gsonはデフォルトのコンストラクターを呼び出してオブジェクトを作成します。一時的な最終割り当てをデフォルトのコンストラクターに移動すると、適切に割り当てられます。最終変数(たとえば、ID)を割り当てるデフォルト以外のコンストラクターしかない場合、それらはGson with Reflectionによって上書きされるため、何に割り当てるかは重要ではありません。

これは、一時的な最終割り当てがコンストラクター引数に依存している場合、これが機能しないことを意味します。

次にいくつかのサンプルコードを示します。

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: {}
于 2021-01-16T07:08:38.023 に答える