JavaRush /Blog Java /Random-FR /La sérialisation telle qu'elle est. Partie 1
articles
Niveau 15

La sérialisation telle qu'elle est. Partie 1

Publié dans le groupe Random-FR
À première vue, la sérialisation semble être un processus trivial. Franchement, quoi de plus simple ? Déclaré la classe pour implémenter l'interface java.io.Serializable- et c'est tout. Vous pouvez sérialiser la classe sans problème. La sérialisation telle qu'elle est.  Partie 1 - 1Théoriquement, c'est vrai. En pratique, il y a beaucoup de subtilités. Ils sont liés aux performances, à la désérialisation, à la sécurité des classes. Et avec bien d’autres aspects. De telles subtilités seront discutées. Cet article peut être divisé en les parties suivantes :
  • Subtilités des mécanismes
  • Pourquoi est-ce nécessaire ?Externalizable
  • Performance
  • mais d'autre part
  • Sécurité des données
  • Sérialisation d'objetsSingleton
Passons à la première partie -

Subtilités des mécanismes

Tout d’abord, une petite question. De combien de façons existe-t-il de rendre un objet sérialisable ? La pratique montre que plus de 90 % des développeurs répondent à cette question à peu près de la même manière (jusqu'au libellé) - il n'y a qu'une seule façon. En attendant, ils sont deux. Tout le monde ne se souvient pas du deuxième, et encore moins ne dit rien d’intelligible sur ses caractéristiques. Alors quelles sont ces méthodes ? Tout le monde se souvient du premier. Il s’agit de la mise en œuvre déjà mentionnée java.io.Serializableet ne nécessite aucun effort. La deuxième méthode est également la mise en place d'une interface, mais différente : java.io.Externalizable. Contrairement à java.io.Serializable, il contient deux méthodes qui doivent être implémentées : writeExternal(ObjectOutput)et readExternal(ObjectInput). Ces méthodes contiennent la logique de sérialisation/désérialisation. Commentaire.SerializableDans ce qui suit , je ferai parfois référence à la sérialisation avec une implémentation standard et une implémentation Externalizableétendue. Un autrecommentaire. Je n'aborde délibérément pas maintenant les options de contrôle de sérialisation standard telles que la définition de readObjectet writeObject, car Je pense que ces méthodes sont quelque peu incorrectes. Ces méthodes ne sont pas définies dans l'interface Serializableet sont, en fait, des accessoires permettant de contourner les limitations et de rendre flexible la sérialisation standard. ExternalizableDes méthodes offrant de la flexibilité y sont intégrées dès le début . Posons encore une question. Comment fonctionne réellement la sérialisation standard, en utilisant java.io.Serializable? Et cela fonctionne via l'API Reflection. Ceux. la classe est analysée comme un ensemble de champs, dont chacun est écrit dans le flux de sortie. Je pense qu'il est clair que cette opération n'est pas optimale en termes de performances. Nous découvrirons combien exactement plus tard. Il existe une autre différence majeure entre les deux méthodes de sérialisation mentionnées. À savoir, dans le mécanisme de désérialisation. Lorsqu'elle est utilisée, Serializablela désérialisation se produit comme ceci : la mémoire est allouée à un objet, après quoi ses champs sont remplis avec les valeurs du flux. Le constructeur de l'objet n'est pas appelé. Ici, nous devons considérer cette situation séparément. D'accord, notre classe est sérialisable. Et son parent ? Complètement facultatif ! De plus, si vous héritez d'une classe Object, le parent n'est certainement PAS sérialisable. Et même si Objectnous ne savons rien des champs, ils peuvent très bien exister dans nos propres classes parentes. Qu'est-ce qui va leur arriver? Ils n'entreront pas dans le flux de sérialisation. Quelles valeurs prendront-ils lors de la désérialisation ? Regardons cet exemple :

package ru.skipy.tests.io;

import java.io.*;

/**
 * ParentDeserializationTest
 *
 * @author Eugene Matyushkin aka Skipy
 * @since 05.08.2010
 */
public class ParentDeserializationTest {

    public static void main(String[] args){
        try {
            System.out.println("Creating...");
            Child c = new Child(1);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            c.field = 10;
            System.out.println("Serializing...");
            oos.writeObject(c);
            oos.flush();
            baos.flush();
            oos.close();
            baos.close();
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            System.out.println("Deserializing...");
            Child c1 = (Child)ois.readObject();
            System.out.println("c1.i="+c1.getI());
            System.out.println("c1.field="+c1.getField());
        } catch (IOException ex){
            ex.printStackTrace();
        } catch (ClassNotFoundException ex){
            ex.printStackTrace();
        }
    }

    public static class Parent {
        protected int field;
        protected Parent(){
            field = 5;
            System.out.println("Parent::Constructor");
        }
        public int getField() {
            return field;
        }
    }

    public static class Child extends Parent implements Serializable{
        protected int i;
        public Child(int i){
            this.i = i;
            System.out.println("Child::Constructor");
        }
        public int getI() {
            return i;
        }
    }
}
C'est transparent - nous avons une classe parent non sérialisable et une classe enfant sérialisable. Et c'est ce qui arrive:

Creating...
Parent::Constructor
Child::Constructor
Serializing...
Deserializing...
Parent::Constructor
c1.i=1
c1.field=5
Autrement dit, lors de la désérialisation, le constructeur sans paramètres de la classe parent NON-sérialisable est appelé . Et s'il n'existe pas de constructeur de ce type, une erreur se produira lors de la désérialisation. Le constructeur de l'objet enfant, celui que l'on désérialise, n'est pas appelé, comme cela a été dit plus haut. C'est ainsi que se comportent les mécanismes standards lorsqu'ils sont utilisés Serializable. Lors de son utilisation, Externalizablela situation est différente. Tout d'abord, le constructeur sans paramètres est appelé, puis la méthode readExternal est appelée sur l'objet créé, qui lit réellement toutes ses données. Par conséquent, toute classe implémentant l’interface Externalisable doit avoir un constructeur public sans paramètres ! De plus, puisque tous les descendants d'une telle classe seront également considérés comme implémentant l'interface Externalizable, ils doivent également avoir un constructeur sans paramètre ! Allons plus loin. Il existe un modificateur de champ tel que transient. Cela signifie que ce champ ne doit pas être sérialisé. Cependant, comme vous le comprenez vous-même, cette instruction n'affecte que le mécanisme de sérialisation standard. Lorsqu'il est utilisé, Externalizablepersonne ne prend la peine de sérialiser ce champ, ni de le soustraire. Si un champ est déclaré transitoire, alors lorsque l'objet est désérialisé, il prend la valeur par défaut. Autre point assez subtil. Avec la sérialisation standard, les champs comportant le modificateur staticne sont pas sérialisés. Par conséquent, après la désérialisation, ce champ ne change pas de valeur. Bien sûr, lors de la mise en œuvre, Externalizablepersonne ne prend la peine de sérialiser et de désérialiser ce champ, mais je vous recommande fortement de ne pas le faire, car cela peut conduire à des erreurs subtiles. Les champs avec un modificateur finalsont sérialisés comme les champs normaux. À une exception près : ils ne peuvent pas être désérialisés lors de l’utilisation d’Externalisable. Car final-поляils doivent être initialisés dans le constructeur, et après cela il sera impossible de changer la valeur de ce champ dans readExternal. Par conséquent, si vous devez sérialiser un objet comportant finalun champ -, vous n'aurez qu'à utiliser la sérialisation standard. Encore un point que beaucoup de gens ne connaissent pas. La sérialisation standard prend en compte l'ordre dans lequel les champs sont déclarés dans une classe. Dans tous les cas, c'était le cas dans les versions précédentes : dans la version JVM 1.6 de l'implémentation Oracle, l'ordre n'a plus d'importance, le type et le nom du champ sont importants. La composition des méthodes est très susceptible d’affecter le mécanisme standard, même si les champs peuvent généralement rester les mêmes. Pour éviter cela, il existe le mécanisme suivant. À chaque classe qui implémente l'interface Serializable, un champ supplémentaire est ajouté au stade de la compilation -private static final long serialVersionUID. Ce champ contient l'identifiant de version unique de la classe sérialisée. Il est calculé en fonction du contenu de la classe - champs, leur ordre de déclaration, méthodes, leur ordre de déclaration. Par conséquent, avec tout changement dans la classe, ce champ changera de valeur. Ce champ est écrit dans le flux lorsque la classe est sérialisée. À propos, c'est peut-être le seul cas que je connaisse où staticun champ est sérialisé. Lors de la désérialisation, la valeur de ce champ est comparée à celle de la classe dans la machine virtuelle. Si les valeurs ne correspondent pas, une exception comme celle-ci est levée :

java.io.InvalidClassException: test.ser2.ChildExt;
    local class incompatible: stream classdesc serialVersionUID = 8218484765288926197,
                                   local class serialVersionUID = 1465687698753363969
Il existe cependant un moyen, sinon de contourner, du moins de tromper ce contrôle. Cela peut être utile si l’ensemble des champs de classe et leur ordre sont déjà définis, mais que les méthodes de classe peuvent changer. Dans ce cas, la sérialisation ne présente aucun risque, mais le mécanisme standard ne permettra pas de désérialiser les données à l'aide du bytecode de la classe modifiée. Mais comme je l'ai dit, il peut être trompé. À savoir, définissez manuellement le champ dans la classe private static final long serialVersionUID. En principe, la valeur de ce champ peut être absolument quelconque. Certaines personnes préfèrent le définir égal à la date à laquelle le code a été modifié. Certains utilisent même 1L. Pour obtenir la valeur standard (celle qui est calculée en interne), vous pouvez utiliser l'utilitaire Serialver inclus dans le SDK. Une fois définie de cette manière, la valeur du champ sera fixe, donc la désérialisation sera toujours autorisée. De plus, dans la version 5.0, ce qui suit apparaissait approximativement dans la documentation : il est fortement recommandé que toutes les classes sérialisables déclarent explicitement ce champ, car le calcul par défaut est très sensible aux détails de la structure de la classe, qui peut varier en fonction de l'implémentation du compilateur, et ainsi provoquer InvalidClassExceptiondes conséquences inattendues… la désérialisation. Il est préférable de déclarer ce champ comme private, car il fait uniquement référence à la classe dans laquelle il est déclaré. Bien que le modificateur ne soit pas spécifié dans la spécification. Considérons maintenant cet aspect. Disons que nous avons cette structure de classe :

public class A{
    public int iPublic;
    protected int iProtected;
    int iPackage;
    private int iPrivate;
}

public class B extends A implements Serializable{}
En d’autres termes, nous avons une classe héritée d’un parent non sérialisable. Est-il possible de sérialiser cette classe, et que faut-il pour cela ? Qu’arrivera-t-il aux variables de la classe parent ? La réponse est la suivante. Oui, Bvous pouvez sérialiser une instance d'une classe. Que faut-il pour cela ? Mais la classe doit Aavoir un constructeur sans paramètres, publicou protected. Ensuite, lors de la désérialisation, toutes les variables de classe Aseront initialisées à l'aide de ce constructeur. Les variables de classe Bseront initialisées avec les valeurs du flux de données sérialisé. Théoriquement, il est possible de définir dans une classe Bles méthodes dont j'ai parlé au début - readObjectet writeObject, - au début desquelles effectuer la (dé-)sérialisation des variables de classe Bvia in.defaultReadObject/out.defaultWriteObject, puis la (dé-)sérialisation des variables disponibles de la classe A(dans notre cas, ce sont iPublic, iProtectedet iPackage, s'il Bse trouve dans le même package que A). Cependant, à mon avis, il est préférable d'utiliser pour cela la sérialisation étendue. Le prochain point que je voudrais aborder est la sérialisation de plusieurs objets. Disons que nous avons la structure de classe suivante :

public class A implements Serializable{
    private C c;
    private B b;
    public void setC(C c) {this.c = c;}
    public void setB(B b) {this.b = b;}
    public C getC() {return c;}
    public B getB() {return b;}
}
public class B implements Serializable{
    private C c;
    public void setC(C c) {this.c = c;}
    public C getC() {return c;}
}
public class C implements Serializable{
    private A a;
    private B b;
    public void setA(A a) {this.a = a;}
    public void setB(B b) {this.b = b;}
    public B getB() {return b;}
    public A getA() {return a;}
}
La sérialisation telle qu'elle est.  Partie 1 - 2Que se passe-t-il si vous sérialisez une instance de la classe A? Il fera glisser une instance de la classe B, qui, à son tour, fera glisser une instance Cqui a une référence à l' instance A, la même avec laquelle tout a commencé. Cercle vicieux et récursivité infinie ? Heureusement, non. Regardons le code de test suivant :

// initiaizing
A a = new A();
B b = new B();
C c = new C();
// setting references
a.setB(b);
a.setC(c);
b.setC(c);
c.setA(a);
c.setB(b);
// serializing
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(a);
oos.writeObject(b);
oos.writeObject(c);
oos.flush();
oos.close();
// deserializing
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
A a1 = (A)ois.readObject();
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
// testing
System.out.println("a==a1: "+(a==a1));
System.out.println("b==b1: "+(b==b1));
System.out.println("c==c1: "+(c==c1));
System.out.println("a1.getB()==b1: "+(a1.getB()==b1));
System.out.println("a1.getC()==c1: "+(a1.getC()==c1));
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1.getA()==a1: "+(c1.getA()==a1));
System.out.println("c1.getB()==b1: "+(c1.getB()==b1));
Qu'est-ce que nous faisons? Nous créons une instance des classes A, Bet C, leur donnons des liens les unes vers les autres, puis sérialisons chacune d'elles. Ensuite, nous les désérialisons et effectuons une série de vérifications. Que se passera-t-il en conséquence :

a==a1: false
b==b1: false
c==c1: false
a1.getB()==b1: true
a1.getC()==c1: true
b1.getC()==c1: true
c1.getA()==a1: true
c1.getB()==b1: true
Alors, que pouvez-vous apprendre de ce test ? D'abord. Les références d'objet après la désérialisation sont différentes des références qui la précèdent. En d'autres termes, lors de la sérialisation/désérialisation, l'objet a été copié. Cette méthode est parfois utilisée pour cloner des objets. La deuxième conclusion est plus significative. Lors de la sérialisation/désérialisation de plusieurs objets comportant des références croisées, ces références restent valides après la désérialisation. En d'autres termes, si avant la sérialisation, ils pointaient vers un objet, alors après la désérialisation, ils pointeraient également vers un objet. Encore un petit test pour le confirmer :

B b = new B();
C c = new C();
b.setC(c);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(b);
oos.writeObject(c);
oos.writeObject(c);
oos.writeObject(c);
oos.flush();
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
B b1 = (B)ois.readObject();
C c1 = (C)ois.readObject();
C c2 = (C)ois.readObject();
C c3 = (C)ois.readObject();
System.out.println("b1.getC()==c1: "+(b1.getC()==c1));
System.out.println("c1==c2: "+(c1==c2));
System.out.println("c1==c3: "+(c1==c3));
Un objet de classe Ba une référence à un objet de classe C. Une fois sérialisé, bil est sérialisé avec une instance de la classe С, après quoi la même instance de c est sérialisée trois fois. Que se passe-t-il après la désérialisation ?

b1.getC()==c1: true
c1==c2: true
c1==c3: true
Comme vous pouvez le constater, les quatre objets désérialisés représentent en réalité un seul objet : les références à celui-ci sont égales. Exactement comme avant la sérialisation. Autre point intéressant : que se passera-t-il si nous implémentons simultanément Externalizableet Serializable? Comme dans cette question – éléphant contre baleine – qui vaincra qui ? Va vaincre Externalizable. Le mécanisme de sérialisation vérifie d'abord sa présence, et ensuite seulement sa présence. SerializableAinsi, si la classe B, qui implémente Serializable, hérite de la classe A, qui implémente Externalizable, les champs de la classe B ne seront pas sérialisés. Le dernier point est l'héritage. Lors de l'héritage d'une classe qui implémente Serializable, aucune action supplémentaire n'est nécessaire. La sérialisation s'étendra également à la classe enfant. Lorsque vous héritez d'une classe qui implémente Externalizable, vous devez remplacer les méthodes readExternal et writeExternal de la classe parent. Sinon, les champs de la classe enfant ne seront pas sérialisés. Dans ce cas, il faudra penser à appeler les méthodes parents, sinon les champs parents ne seront pas sérialisés. * * * Nous en avons probablement fini avec les détails. Il existe cependant une question que nous n’avons pas abordée et qui est de nature mondiale. A savoir -

Pourquoi avez-vous besoin d'Externalisable ?

Pourquoi avons-nous besoin d’une sérialisation avancée ? La réponse est simple. Premièrement, cela donne beaucoup plus de flexibilité. Deuxièmement, cela peut souvent apporter des gains significatifs en termes de volume de données sérialisées. Troisièmement, il existe un aspect tel que la performance, dont nous parlerons ci-dessous . Tout semble clair avec flexibilité. En effet, nous pouvons contrôler les processus de sérialisation et de désérialisation comme nous le souhaitons, ce qui nous rend indépendants de tout changement dans la classe (comme je le disais juste au-dessus, les changements dans la classe peuvent fortement affecter la désérialisation). Par conséquent, je voudrais dire quelques mots sur le gain de volume. Disons que nous avons la classe suivante :

public class DateAndTime{

  private short year;
  private byte month;
  private byte day;
  private byte hours;
  private byte minutes;
  private byte seconds;

}
Le reste n'a pas d'importance. Les champs pourraient être de type int, mais cela ne ferait qu'améliorer l'effet de l'exemple. Bien qu'en réalité, les champs peuvent être saisis intpour des raisons de performances. En tout cas, le point est clair. La classe représente une date et une heure. Cela nous intéresse principalement du point de vue de la sérialisation. La chose la plus simple à faire serait peut-être de stocker un simple horodatage. Il est de type long, c'est-à-dire une fois sérialisé, cela prendrait 8 octets. De plus, cette approche nécessite des méthodes pour convertir les composants en une seule valeur et inversement, c'est-à-dire – perte de productivité. L'avantage de cette approche est une date complètement folle pouvant tenir en 64 bits. Il s’agit d’une énorme marge de sécurité, qui n’est le plus souvent pas nécessaire dans la réalité. La classe donnée ci-dessus prendra 2 + 5*1 = 7 octets. Plus les frais généraux pour la classe et 6 champs. Existe-t-il un moyen de compresser ces données ? À coup sûr. Les secondes et les minutes sont comprises entre 0 et 59, c'est-à-dire pour les représenter, 6 bits suffisent au lieu de 8. Heures – 0-23 (5 bits), jours – 0-30 (5 bits), mois – 0-11 (4 bits). Au total, tout sans tenir compte de l'année - 26 bits. Il reste encore 6 bits pour la taille de int. Théoriquement, dans certains cas, cela peut suffire pour un an. Sinon, l'ajout d'un autre octet augmente la taille du champ de données à 14 bits, ce qui donne une plage de 0 à 16 383. C'est plus que suffisant dans les applications réelles. Au total, nous avons réduit la taille des données nécessaires au stockage des informations nécessaires à 5 octets. Sinon jusqu'à 4. L'inconvénient est le même que dans le cas précédent : si vous stockez la date emballée, des méthodes de conversion sont nécessaires. Mais je veux procéder de cette façon : stockez-le dans des champs séparés et sérialisez-le sous forme packagée. C'est là qu'il est judicieux d'utiliser Externalizable:

// data is packed into 5 bytes:
//  3         2         1
// 10987654321098765432109876543210
// hhhhhmmmmmmssssssdddddMMMMyyyyyy yyyyyyyy
public void writeExternal(ObjectOutput out){
    int packed = 0;
    packed += ((int)hours) << 27;
    packed += ((int)minutes) << 21;
    packed += ((int)seconds) << 15;
    packed += ((int)day) << 10;
    packed += ((int)month) << 6;
    packed += (((int)year) >> 8) & 0x3F;
    out.writeInt(packed);
    out.writeByte((byte)year);
}

public void readExternal(ObjectInput in){
    int packed = in.readInt();
    year = in.readByte() & 0xFF;
    year += (packed & 0x3F) << 8;
    month = (packed >> 6) & 0x0F;
    day = (packed >> 10) & 0x1F;
    seconds = (packed >> 15) & 0x3F;
    minutes = (packed >> 21) & 0x3F;
    hours = (packed >> 27);
}
En fait, c'est tout. Après sérialisation, nous obtenons une surcharge par classe, deux champs (au lieu de 6) et 5 octets de données. Ce qui est déjà nettement mieux. Les conditionnements ultérieurs peuvent être confiés à des bibliothèques spécialisées. L'exemple donné est très simple. Son objectif principal est de montrer comment la sérialisation avancée peut être utilisée. Même si le gain possible en volume de données sérialisées est loin d'être le principal avantage, à mon avis. Le principal avantage, outre la flexibilité... (passez en douceur à la section suivante...) Lien vers la source : La sérialisation telle qu'elle est
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION