web-dev-qa-db-ja.com

インターフェイスでクラスをシリアル化する方法は?

私はシリアライズをあまり行ったことはありませんが、 Googleのgson を使用してJavaオブジェクトをファイルにシリアライズしようとしています。問題の例を次に示します。

public interface Animal {
    public String getName();
}


 public class Cat implements Animal {

    private String mName = "Cat";
    private String mHabbit = "Playing with yarn";

    public String getName() {
        return mName;
    }

    public void setName(String pName) {
        mName = pName;
    }

    public String getHabbit() {
        return mHabbit;
    }

    public void setHabbit(String pHabbit) {
        mHabbit = pHabbit;
    }

}

public class Exhibit {

    private String mDescription;
    private Animal mAnimal;

    public Exhibit() {
        mDescription = "This is a public exhibit.";
    }

    public String getDescription() {
        return mDescription;
    }

    public void setDescription(String pDescription) {
        mDescription = pDescription;
    }

    public Animal getAnimal() {
        return mAnimal;
    }

    public void setAnimal(Animal pAnimal) {
        mAnimal = pAnimal;
    }

}

public class GsonTest {

public static void main(String[] argv) {
    Exhibit exhibit = new Exhibit();
    exhibit.setAnimal(new Cat());
    Gson gson = new Gson();
    String jsonString = gson.toJson(exhibit);
    System.out.println(jsonString);
    Exhibit deserializedExhibit = gson.fromJson(jsonString, Exhibit.class);
    System.out.println(deserializedExhibit);
}
}

したがって、これはうまくシリアライズされますが、当然のことながら動物のタイプ情報をドロップします:

{"mDescription":"This is a public exhibit.","mAnimal":{"mName":"Cat","mHabbit":"Playing with yarn"}}

ただし、これにより、逆シリアル化で実際の問題が発生します。

Exception in thread "main" Java.lang.RuntimeException: No-args constructor for interface com.atg.lp.gson.Animal does not exist. Register an InstanceCreator with Gson for this type to fix this problem.

なぜこれが起こっているのかはわかりますが、これに対処するための適切なパターンを理解するのに苦労しています。 guide を見ましたが、直接これに対処しませんでした。

34
Ben Flynn

動物をtransientにすると、シリアル化されません。

または、defaultWriteObject(...)defaultReadObject(...)を実装することで自分でシリアル化することもできます(それらはそれが呼ばれたものだと思います...)

[〜#〜] edit [〜#〜]「インスタンス作成者の作成」に関する部分を参照してください here

Gsonは、どの実装クラスが使用されるかを知らないため、インターフェイスを逆シリアル化できません。そのため、Animalのインスタンス作成者を提供し、デフォルトまたは類似の設定を行う必要があります。

13
rapadura

これは、インターフェイスのみが静的に知られているすべての場合に機能する一般的なソリューションです。

  1. シリアライザー/デシリアライザーを作成します。

    final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> {
        public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
            final JsonObject wrapper = new JsonObject();
            wrapper.addProperty("type", object.getClass().getName());
            wrapper.add("data", context.serialize(object));
            return wrapper;
        }
    
        public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException {
            final JsonObject wrapper = (JsonObject) elem;
            final JsonElement typeName = get(wrapper, "type");
            final JsonElement data = get(wrapper, "data");
            final Type actualType = typeForName(typeName); 
            return context.deserialize(data, actualType);
        }
    
        private Type typeForName(final JsonElement typeElem) {
            try {
                return Class.forName(typeElem.getAsString());
            } catch (ClassNotFoundException e) {
                throw new JsonParseException(e);
            }
        }
    
        private JsonElement get(final JsonObject wrapper, String memberName) {
            final JsonElement elem = wrapper.get(memberName);
            if (elem == null) throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper");
            return elem;
        }
    }
    
  2. gsonが選択したインターフェイスタイプにそれを使用するようにします。

    Gson gson = new GsonBuilder().registerTypeAdapter(Animal.class, new InterfaceAdapter<Animal>())
                                 .create();
    
82
narthi

@Maciekソリューションは、メンバー変数の宣言済みタイプがインターフェース/抽象クラスである場合に最適に機能します。 registerTypeAdapter()ですべて登録しない限り、宣言された型がサブクラス/サブインターフェース/サブ抽象クラスの場合は機能しません。 registerTypeHierarchyAdapterを使用して1つずつ登録することは避けられますが、無限ループのためにStackOverflowErrorが発生することがわかります。 (以下の参照セクションをお読みください)

要するに、私の回避策は少し無意味に見えますが、StackOverflowErrorがなくても動作します。

@Override
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
    final JsonObject wrapper = new JsonObject();
    wrapper.addProperty("type", object.getClass().getName());
    wrapper.add("data", new Gson().toJsonTree(object));
    return wrapper;
}

作業の別の新しいGsonインスタンスをデフォルトのシリアライザー/デシリアライザーとして使用して、無限ループを回避しました。このソリューションの欠点は、他のTypeAdapterも失われることです。別の型のカスタムシリアル化があり、オブジェクトに表示される場合、それは単に失敗します。

それでも、私はより良い解決策を望んでいます。

参照

JsonSerializationContextおよびJsonDeserializationContextのGson 2.3.1ドキュメントによると

特定の型情報を渡す指定されたオブジェクトでデフォルトのシリアル化を呼び出します。 JsonSerializer.serialize(Object、Type、JsonSerializationContext)メソッドのパラメーターとして受信した要素で呼び出されることはありません。これを行うと、Gsonがカスタムシリアライザーを再び呼び出すため、無限ループが発生します。

そして

指定されたオブジェクトでデフォルトのデシリアライゼーションを呼び出します。 JsonDeserializer.deserialize(JsonElement、Type、JsonDeserializationContext)メソッドのパラメーターとして受信した要素で呼び出されることはありません。これを行うと、Gsonがカスタムデシリアライザーを再度呼び出すため、無限ループが発生します。

これは、以下の実装が無限ループを引き起こし、最終的にStackOverflowErrorを引き起こすと結論づけます。

@Override
public JsonElement serialize(Animal src, Type typeOfSrc,
        JsonSerializationContext context) {
    return context.serialize(src);
}
4
Victor Wong