web-dev-qa-db-fra.com

Comment sérialiser une classe avec une interface?

Je n'ai jamais fait grand chose avec la sérialisation, mais j'essaie d'utiliser gson de Google pour sérialiser un Java objet dans un fichier. Voici un exemple de mon problème:

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);
}
}

Donc, cela sérialise bien - mais laisse tomber naturellement les informations de type sur l'animal:

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

Cela pose cependant de réels problèmes de désérialisation:

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.

Je comprends pourquoi cela se produit, mais j'ai du mal à trouver le modèle approprié pour y faire face. J'ai regardé dans le guide mais cela ne l'a pas traité directement.

35
Ben Flynn

Mettez l'animal comme transient, il ne sera alors pas sérialisé.

Ou vous pouvez le sérialiser vous-même en implémentant defaultWriteObject(...) et defaultReadObject(...) (je pense que c'est ainsi qu'on les appelait ...)

[~ # ~] modifier [~ # ~] Voir la partie sur "Écrire un créateur d'instance" ici .

Gson ne peut pas désérialiser une interface car il ne sait pas quelle classe d'implémentation sera utilisée, vous devez donc fournir un créateur d'instance pour votre animal et définir une valeur par défaut ou similaire.

13
rapadura

Voici une solution générique qui fonctionne pour tous les cas où seule l'interface est connue statiquement.

  1. Créer un sérialiseur/désérialiseur:

    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. faites en sorte que Gson l'utilise pour le type d'interface de votre choix:

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

La solution @Maciek fonctionne parfaitement si le type déclaré de la variable membre est la classe interface/abstraite. Cela ne fonctionnera pas si le type déclaré est une sous-classe/sous-interface/sous-classe abstraite, sauf si nous les enregistrons tous via registerTypeAdapter(). Nous pouvons éviter de nous inscrire un par un en utilisant registerTypeHierarchyAdapter, mais je me rends compte que cela causera StackOverflowError à cause de la boucle infinie. (Veuillez lire la section de référence ci-dessous)

En bref, ma solution de contournement semble un peu insensée mais elle fonctionne sans 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;
}

J'ai utilisé une autre nouvelle instance de travail Gson comme sérialiseur/désérialiseur par défaut pour éviter la boucle infinie. L'inconvénient de cette solution est que vous perdrez également d'autres TypeAdapter, si vous avez une sérialisation personnalisée pour un autre type et qu'elle apparaît dans l'objet, elle échouera simplement.

J'espère néanmoins une meilleure solution.

Référence

Selon la documentation Gson 2.3.1 pour JsonSerializationContext et JsonDeserializationContext

Appelle la sérialisation par défaut sur l'objet spécifié en transmettant les informations de type spécifiques. Il ne doit jamais être invoqué sur l'élément reçu en tant que paramètre de la méthode JsonSerializer.serialize (Object, Type, JsonSerializationContext). Cela entraînera une boucle infinie car Gson appellera à son tour le sérialiseur personnalisé.

et

Appelle la désérialisation par défaut sur l'objet spécifié. Il ne doit jamais être invoqué sur l'élément reçu en tant que paramètre de la méthode JsonDeserializer.deserialize (JsonElement, Type, JsonDeserializationContext). Cela entraînera une boucle infinie car Gson appellera à son tour le désérialiseur personnalisé.

Ceci conclut que l'implémentation ci-dessous provoquera une boucle infinie et provoquera éventuellement StackOverflowError.

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