Sérialisation Java: readObject () vs readResolve ()


127

Le livre Effective Java et d'autres sources fournissent une assez bonne explication sur comment et quand utiliser la méthode readObject () lorsque vous travaillez avec des classes Java sérialisables. La méthode readResolve (), en revanche, reste un peu mystérieuse. En gros, tous les documents que j'ai trouvés ne mentionnent qu'un seul des deux ou ne mentionnent les deux qu'individuellement.

Les questions qui restent sans réponse sont:

  • Quelle est la différence entre les deux méthodes?
  • Quand quelle méthode doit-elle être mise en œuvre?
  • Comment utiliser readResolve (), en particulier pour renvoyer quoi?

J'espère que vous pourrez faire la lumière à ce sujet.


Exemple du JDK d'Oracle:String.CaseInsensitiveComparator.readResolve()
kevinarpe

Réponses:


138

readResolveest utilisé pour remplacer l'objet lu dans le flux. La seule utilisation que j'ai jamais vue pour cela est l'application de singletons; quand un objet est lu, remplacez-le par l'instance singleton. Cela garantit que personne ne peut créer une autre instance en sérialisant et en désérialisant le singleton.


3
Le code malveillant (ou même les données) peut contourner ce problème de plusieurs manières.
Tom Hawtin - tackline

6
Josh Bloch parle des conditions dans lesquelles cela se rompt dans Java 2 éd. Point 77. Il en parle dans cette conférence qu'il a donnée dans Google IO il y a quelques années (parfois vers la fin de la conférence): youtube.com/watch?v=pi_I7oD_uGI
calvinkrishy

17
Je trouve cette réponse légèrement insuffisante, car elle ne mentionne pas de transientchamps. readResolveest utilisé pour résoudre l'objet après sa lecture. Un exemple d'utilisation est peut-être qu'un objet contient un cache qui peut être recréé à partir de données existantes et qui n'a pas besoin d'être sérialisé; les données mises en cache peuvent être déclarées transientet readResolve()peuvent les reconstruire après la désérialisation. Ce sont des choses comme ça à quoi sert cette méthode.
Jason C

2
@JasonC votre commentaire selon lequel "Des choses comme ça [la gestion transitoire] sont à quoi sert cette méthode " est trompeur. Voir le document Java pour Serializable: il dit "Les classes qui doivent désigner un remplaçant lorsqu'une instance de celui-ci est lue à partir du flux doivent implémenter cette [ readResolve] méthode spéciale ...".
Opher

2
La méthode readResolve peut également être utilisée dans un cas d'angle dans lequel supposons que vous ayez sérialisé un grand nombre d'objets et les avez stockés dans la base de données. Si, ultérieurement, vous souhaitez migrer ces données vers un nouveau format, vous pouvez facilement y parvenir avec la méthode readResolve.
Nilesh Rajani

29

L'article 90, Effective Java, 3e édition couvre readResolveet writeReplacepour les proxys série - leur utilisation principale. Les exemples n'écrivent pas readObjectet les writeObjectméthodes car ils utilisent la sérialisation par défaut pour lire et écrire des champs.

readResolveest appelé après readObjecta retourné (à l'inverse writeReplaceest appelé avant writeObjectet probablement sur un objet différent). L'objet renvoyé par la méthode remplace l' thisobjet renvoyé à l'utilisateur ObjectInputStream.readObjectet toute autre référence en arrière à l'objet dans le flux. Les deux readResolveet writeReplacepeuvent renvoyer des objets de types identiques ou différents. Le renvoi du même type est utile dans certains cas où les champs doivent l'être finalet où la compatibilité descendante est requise ou les valeurs doivent être copiées et / ou validées.

L'utilisation de readResolven'applique pas la propriété singleton.


9

readResolve peut être utilisé pour modifier les données sérialisées via la méthode readObject. Par exemple, l'API xstream utilise cette fonctionnalité pour initialiser certains attributs qui n'étaient pas dans le XML à désérialiser.

http://x-stream.github.io/faq.html#Serialization


1
XML et Xstream ne sont pas pertinents pour une question sur la sérialisation Java, et la question a reçu une réponse correcte il y a des années. -1
Marquis de Lorne

5
La réponse acceptée indique que readResolve est utilisé pour remplacer un objet. Cette réponse fournit les informations supplémentaires utiles qui peuvent être utilisées pour modifier un objet lors de la désérialisation. XStream a été donné à titre d'exemple, pas comme la seule bibliothèque possible dans laquelle cela se produit.
Enwired le

5

readResolve est destiné au moment où vous devrez peut-être renvoyer un objet existant, par exemple parce que vous recherchez des entrées en double qui devraient être fusionnées, ou (par exemple dans des systèmes distribués éventuellement cohérents) parce que c'est une mise à jour qui peut arriver avant que vous en soyez conscient toutes les anciennes versions.


readResolve () était clair pour moi mais j'ai toujours des questions inexplicables en tête mais votre réponse vient de lire dans mes pensées, merci
Rajni Gangwar

5

readObject () est une méthode existante dans la classe ObjectInputStream.Lors de la lecture de l'objet au moment de la désérialisation, la méthode readObject vérifie en interne si l'objet de classe qui est désérialisé a la méthode readResolve ou non si la méthode readResolve existe, il invoquera la méthode readResolve et retournera la même chose exemple.

Donc, l'intention d'écrire la méthode readResolve est une bonne pratique pour obtenir un modèle de conception de singleton pur où personne ne peut obtenir une autre instance en sérialisant / désérialisant.



2

Lorsque la sérialisation est utilisée pour convertir un objet afin qu'il puisse être enregistré dans un fichier, nous pouvons déclencher une méthode, readResolve (). La méthode est privée et est conservée dans la même classe dont l'objet est récupéré lors de la désérialisation. Il garantit qu'après la désérialisation, l'objet retourné est le même que celui qui a été sérialisé. C'est,instanceSer.hashCode() == instanceDeSer.hashCode()

La méthode readResolve () n'est pas une méthode statique. After in.readObject()est appelé pendant la désérialisation, il s'assure simplement que l'objet retourné est le même que celui qui a été sérialisé comme ci-dessous tandis queout.writeObject(instanceSer)

..
    ObjectOutput out = new ObjectOutputStream(new FileOutputStream("file1.ser"));
    out.writeObject(instanceSer);
    out.close();

De cette manière, cela facilite également l' implémentation du modèle de conception singleton , car chaque fois que la même instance est renvoyée.

public static ABCSingleton getInstance(){
    return ABCSingleton.instance; //instance is static 
}

1

Je sais que cette question est vraiment ancienne et a une réponse acceptée, mais comme elle apparaît très haut dans la recherche Google, j'ai pensé que je pèserais car aucune réponse fournie ne couvre les trois cas que je considère importants - dans mon esprit, l'utilisation principale de ceux-ci méthodes. Bien sûr, tous supposent qu'il existe un besoin de format de sérialisation personnalisé.

Prenez, par exemple, les classes de collection. La sérialisation par défaut d'une liste chaînée ou d'un BST entraînerait une énorme perte d'espace avec très peu de gain de performances par rapport à la simple sérialisation des éléments dans l'ordre. Cela est encore plus vrai si une collection est une projection ou une vue - conserve une référence à une structure plus grande que celle qu'elle expose par son API publique.

  1. Si l'objet sérialisé a des champs immuables qui nécessitent une sérialisation personnalisée, la solution d'origine de writeObject/readObjectest insuffisante, car l'objet désérialisé est créé avant de lire la partie du flux écrite dans writeObject. Prenez cette implémentation minimale d'une liste chaînée:

    public class List<E> extends Serializable {
        public final E head;
        public final List<E> tail;
    
        public List(E head, List<E> tail) {
            if (head==null)
                throw new IllegalArgumentException("null as a list element");
            this.head = head;
            this.tail = tail;
        }
    
        //methods follow...
    }

Cette structure peut être sérialisée en écrivant récursivement le headchamp de chaque lien, suivi d'une nullvaleur. La désérialisation d'un tel format devient cependant impossible: impossible de readObjectmodifier les valeurs des champs membres (désormais fixées à null). Voici la paire writeReplace/ readResolve:

private Object writeReplace() {
    return new Serializable() {
        private transient List<E> contents = List.this;

        private void writeObject(ObjectOutputStream oos) {
            List<E> list = contents;
            while (list!=null) {
                oos.writeObject(list.head);
                list = list.tail;
            }
            oos.writeObject(null);
        }

        private void readObject(ObjectInputStream ois) {
            List<E> tail = null;
            E head = ois.readObject();
            if (head!=null) {
                readObject(ois); //read the tail and assign it to this.contents
                this.contents = new List<>(head, this.contents)
            }                     
        }


        private Object readResolve() {
            return this.contents;
        }
    }
}

Je suis désolé si l'exemple ci-dessus ne compile pas (ou ne fonctionne pas), mais j'espère qu'il sera suffisant pour illustrer mon propos. Si vous pensez qu'il s'agit d'un exemple très tiré par les cheveux, rappelez-vous que de nombreux langages fonctionnels fonctionnent sur la JVM et que cette approche devient essentielle dans leur cas.

  1. Nous pouvons souhaiter désérialiser en fait un objet d'une classe différente de celle que nous avons écrite dans le ObjectOutputStream. Ce serait le cas avec des vues telles qu'une java.util.Listimplémentation de liste qui expose une tranche d'un plus long ArrayList. De toute évidence, sérialiser toute la liste de sauvegarde est une mauvaise idée et nous ne devrions écrire que les éléments de la tranche visualisée. Pourquoi s'arrêter là cependant et avoir un niveau d'indirection inutile après la désérialisation? Nous pourrions simplement lire les éléments du flux dans un ArrayListet le renvoyer directement au lieu de l'encapsuler dans notre classe de vue.

  2. Alternativement, avoir une classe de délégué similaire dédiée à la sérialisation peut être un choix de conception. Un bon exemple serait de réutiliser notre code de sérialisation. Par exemple, si nous avons une classe de générateur (similaire à StringBuilder pour String), nous pouvons écrire un délégué de sérialisation qui sérialise toute collection en écrivant un générateur vide dans le flux, suivi de la taille de la collection et des éléments renvoyés par l'itérateur de la collection. La désérialisation impliquerait la lecture du générateur, l'ajout de tous les éléments lus ultérieurement et le renvoi du résultat final build()des délégués readResolve. Dans ce cas, nous aurions besoin d'implémenter la sérialisation uniquement dans la classe racine de la hiérarchie de collection, et aucun code supplémentaire ne serait nécessaire à partir des implémentations actuelles ou futures, à condition qu'elles implémentent abstrait iterator()etbuilder()méthode (cette dernière pour recréer la collection du même type - ce qui serait une fonctionnalité très utile en soi). Un autre exemple serait d'avoir une hiérarchie de classes dont nous ne contrôlons pas totalement le code - notre (nos) classe (s) de base d'une bibliothèque tierce pourrait avoir n'importe quel nombre de champs privés dont nous ne savons rien et qui peuvent changer d'une version à l'autre, cassant nos objets sérialisés. Dans ce cas, il serait plus sûr d'écrire les données et de reconstruire l'objet manuellement lors de la désérialisation.


0

La méthode readResolve

Pour les classes Serializable et Externalizable, la méthode readResolve permet à une classe de remplacer / résoudre l'objet lu dans le flux avant qu'il ne soit renvoyé à l'appelant. En implémentant la méthode readResolve, une classe peut contrôler directement les types et les instances de ses propres instances en cours de désérialisation. La méthode est définie comme suit:

ANY-ACCESS-MODIFIER Objet readResolve () lance ObjectStreamException;

La méthode readResolve est appelée lorsque ObjectInputStream a lu un objet dans le flux et se prépare à le renvoyer à l'appelant. ObjectInputStream vérifie si la classe de l'objet définit la méthode readResolve. Si la méthode est définie, la méthode readResolve est appelée pour permettre à l'objet du flux de désigner l'objet à renvoyer. L'objet renvoyé doit être d'un type compatible avec toutes les utilisations. S'il n'est pas compatible, une exception ClassCastException sera levée lorsque l'incompatibilité de type est découverte.

Par exemple, une classe Symbol pourrait être créée pour laquelle une seule instance de chaque liaison de symbole existait dans une machine virtuelle. La méthode readResolve serait implémentée pour déterminer si ce symbole était déjà défini et remplacer l'objet Symbol équivalent préexistant pour maintenir la contrainte d'identité. De cette manière, l'unicité des objets Symbol peut être maintenue à travers la sérialisation.


0

Comme déjà répondu, readResolveest une méthode privée utilisée dans ObjectInputStream lors de la désérialisation d'un objet. Ceci est appelé juste avant que l'instance réelle ne soit renvoyée. Dans le cas de Singleton, nous pouvons ici forcer le retour d'une référence d'instance de singleton déjà existante au lieu d'une référence d'instance désérialisée. De même, nous avons writeReplacepour ObjectOutputStream.

Exemple pour readResolve:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class SingletonWithSerializable implements Serializable {
private static final long serialVersionUID = 1L;

public static final SingletonWithSerializable INSTANCE = new SingletonWithSerializable();

private SingletonWithSerializable() {
    if (INSTANCE != null)
        throw new RuntimeException("Singleton instance already exists!");
}

private Object readResolve() {
    return INSTANCE;
}

public void leaveTheBuilding() {
    System.out.println("SingletonWithPublicFinalField.leaveTheBuilding() called...");
}

public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
    SingletonWithSerializable instance = SingletonWithSerializable.INSTANCE;

    System.out.println("Before serialization: " + instance);

    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("file1.ser"))) {
        out.writeObject(instance);
    }

    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("file1.ser"))) {
        SingletonWithSerializable readObject = (SingletonWithSerializable) in.readObject();
        System.out.println("After deserialization: " + readObject);
    }

}

}

Production:

Before serialization: com.ej.item3.SingletonWithSerializable@7852e922
After deserialization: com.ej.item3.SingletonWithSerializable@7852e922
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.