Sérialisation en Java. Pas si simple



La sérialisation est un processus qui traduit un objet en une séquence d'octets, à partir de laquelle il peut ensuite être complètement restauré. Pourquoi est-ce nécessaire? Le fait est que, pendant l'exécution normale d'un programme, la durée de vie maximale de tout objet est connue - du lancement du programme à sa fin. La sérialisation vous permet d'étendre ce cadre et de «donner vie» à l'objet de la même manière entre les lancements de programmes.

Un bonus supplémentaire à tout est la préservation de la multiplateforme. Quel que soit le système d'exploitation dont vous disposez, la sérialisation traduit l'objet en un flux d'octets, qui peut être restauré sur n'importe quel système d'exploitation. Si vous devez transférer un objet sur le réseau, vous pouvez sérialiser l'objet, l'enregistrer dans un fichier et le transférer sur le réseau au destinataire. Il pourra récupérer l'objet reçu. La sérialisation vous permet également d'appeler à distance des méthodes (Java RMI) qui se trouvent sur différentes machines avec, éventuellement, des systèmes d'exploitation différents, et de travailler avec elles comme si elles se trouvaient sur la machine du processus Java appelant.

La mise en œuvre d'un mécanisme de sérialisation est assez simple. Votre classe doit implémenter l'interface Serializable . Cette interface est un identifiant qui n'a pas de méthodes, mais il indique à jvm que les objets de cette classe peuvent être sérialisés. Étant donné que le mécanisme de sérialisation est connecté au système d'entrée / sortie de base et traduit l'objet en un flux d'octets, pour l'exécuter, vous devez créer un flux de sortie OutputStream , le conditionner dans ObjectOutputStream et appeler la méthode writeObject (). Pour restaurer un objet, vous devez empaqueter un InputStream dans un ObjectInputStream et appeler la méthode readObject ().

Pendant le processus de sérialisation, avec l'objet sérialisable, son graphique d'objet est enregistré. C'est-à-dire tous les objets liés à cela, les objets d'autres classes seront également sérialisés avec elle.

Prenons un exemple de sérialisation d'un objet de classe Person.

import java.io.*; class Home implements Serializable { private String home; public Home(String home) { this.home = home; } public String getHome() { return home; } } public class Person implements Serializable { private String name; private int countOfNiva; private String fatherName; private Home home; public Person(String name, int countOfNiva, String fatherName, Home home) { this.name = name; this.countOfNiva = countOfNiva; this.fatherName = fatherName; this.home = home; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", countOfNiva=" + countOfNiva + ", fatherName='" + fatherName + '\'' + ", home=" + home + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Home home = new Home("Vishnevaia 1"); Person igor = new Person("Igor", 2, "Raphael", home); Person renat = new Person("Renat", 2, "Raphael", home); //      ObjectOutputStream ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("person.out")); objectOutputStream.writeObject(igor); objectOutputStream.writeObject(renat); objectOutputStream.close(); //       ObjectInputStream ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("person.out")); Person igorRestored = (Person) objectInputStream.readObject(); Person renatRestored = (Person) objectInputStream.readObject(); objectInputStream.close(); //    ByteArrayOutputStream ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream2 = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream2.writeObject(igor); objectOutputStream2.writeObject(renat); objectOutputStream2.flush(); //    ByteArrayInputStream ObjectInputStream objectInputStream2 = new ObjectInputStream( new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); Person igorRestoredFromByte = (Person) objectInputStream2.readObject(); Person renatRestoredFromByte = (Person) objectInputStream2.readObject(); objectInputStream2.close(); System.out.println("Before Serialize: " + "\n" + igor + "\n" + renat); System.out.println("After Restored From Byte: " + "\n" + igorRestoredFromByte + "\n" + renatRestoredFromByte); System.out.println("After Restored: " + "\n" + igorRestored + "\n" + renatRestored); } } 

Conclusion:

 Before Serialize: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@355da254} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@355da254} After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

Dans cet exemple, la classe Home est créée pour démontrer que lors de la sérialisation d'un objet Personne , le graphique de ses objets est sérialisé avec lui. La classe Home doit également implémenter l'interface Serializable , sinon une exception java.io.NotSerializableException se produira. L'exemple décrit également la sérialisation à l'aide de la classe ByteArrayOutputStream .

Une conclusion intéressante peut être tirée des résultats de l'exécution du programme: lors de la restauration d'objets qui avaient une référence au même objet avant la sérialisation, cet objet ne sera restauré qu'une seule fois . Cela peut être vu sur les mêmes liens dans les objets après la récupération:

 After Restored From Byte: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@27973e9b} After Restored: Person{name='Igor', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} Person{name='Renat', countOfNiva=2, fatherName='Raphael', home=Home@312b1dae} 

Cependant, on voit également que lors de l'enregistrement avec deux flux de sortie (nous avons ObjectInputStream et ByteArrayOutputStream ), l'objet home sera recréé, malgré le fait qu'il a déjà été créé auparavant dans l'un des flux. Nous le voyons à différentes adresses d'objets domestiques reçus dans deux flux. Il s'avère que si vous sérialisez avec un flux de sortie, puis restaurez l'objet, nous avons la garantie de restaurer le réseau complet d'objets sans doublons inutiles. Bien sûr, pendant l'exécution du programme, l'état des objets peut changer, mais cela est sur la conscience du programmeur.

Le problème

L'exemple montre également que lors de la restauration d'un objet, une exception ClassNotFoundException peut se produire. Quelle en est la raison? Le fait est que nous pouvons facilement sérialiser un objet de la classe Person dans un fichier, le transférer sur le réseau à notre ami, qui peut restaurer l'objet par une autre application dans laquelle la classe Person n'existe tout simplement pas.

Sa sérialisation. Comment faire?

Et si vous voulez gérer vous-même la sérialisation? Par exemple, votre objet stocke le nom d'utilisateur et le mot de passe des utilisateurs. Vous devez le sérialiser pour une transmission ultérieure sur le réseau. La transmission d'un mot de passe dans ce cas est extrêmement peu fiable. Comment résoudre ce problème? Il y a deux façons. Tout d'abord, utilisez le mot-clé transitoire . Deuxièmement, au lieu de réaliser l'intérêt sérialisable , utilisez son extension - l'interface externalisable . Considérez les exemples des première et deuxième méthodes pour les comparer.

La première façon - Sérialisation à l'aide de transitoires

 import java.io.*; public class Logon implements Serializable { private String login; private transient String password; public Logon(String login, String password) { this.login = login; this.password = password; } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Conclusion:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

La deuxième voie - Sérialisation avec l'implémentation de l'interface Externalizable

 import java.io.*; public class Logon implements Externalizable { private String login; private String password; public Logon() { } public Logon(String login, String password) { this.login = login; this.password = password; } @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(login); } @Override public String toString() { return "Logon{" + "login='" + login + '\'' + ", password='" + password + '\'' + '}'; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { login = (String) in.readObject(); } public static void main(String[] args) throws IOException, ClassNotFoundException { Logon igor = new Logon("IgorIvanovich", "Khoziain"); Logon renat = new Logon("Renat", "2500RUB"); System.out.println("Before: \n" + igor); System.out.println(renat); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Externals.out")); out.writeObject(igor); out.writeObject(renat); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Externals.out")); igor = (Logon) in.readObject(); renat = (Logon) in.readObject(); System.out.println("After: \n" + igor); System.out.println(renat); } } 

Conclusion:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} After: Logon{login='IgorIvanovich', password='null'} Logon{login='Renat', password='null'} 

La première différence entre les deux options qui attire votre attention est la taille du code. Lors de l'implémentation de l'interface Externalizable , nous devons remplacer deux méthodes: writeExternal () et readExternal () . Dans la méthode writeExternal () , nous indiquons quels champs seront sérialisés et comment, dans readExternal () comment les lire. Lorsque vous utilisez le mot transitoire, nous indiquons explicitement le ou les champs qui n'ont pas besoin d'être sérialisés. Nous notons également que dans la deuxième méthode, nous avons explicitement créé un constructeur par défaut, d'ailleurs, un constructeur public. Pourquoi est-ce fait? Essayons d'exécuter le code sans ce constructeur. Et regardez la sortie:

 Before: Logon{login='IgorIvanovich', password='Khoziain'} Logon{login='Renat', password='2500RUB'} Exception in thread "main" java.io.InvalidClassException: Logon; no valid constructor at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:169) at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:874) at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2043) at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573) at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431) at Logon.main(Logon.java:45) 

Nous avons obtenu l'exception java.io.InvalidClassException . Quelle en est la raison? Si vous suivez la trace de la pile, vous pouvez découvrir qu'il existe des lignes dans le constructeur de la classe ObjectStreamClass :

  if (externalizable) { cons = getExternalizableConstructor(cl); } else { cons = getSerializableConstructor(cl); 

Pour l'interface Externalizable , la méthode constructeur getExternalizableConstructor () sera appelée , à l' intérieur de laquelle, grâce à Reflection, nous essaierons d'obtenir le constructeur par défaut de la classe pour laquelle nous restaurons l'objet. Si nous ne pouvons pas le trouver, ou s'il n'est pas public , alors nous obtenons une exception. Vous pouvez contourner cette situation comme suit: ne créez explicitement aucun constructeur dans la classe et remplissez les champs à l'aide de setters et obtenez la valeur avec des getters. Ensuite, lors de la compilation de la classe, un constructeur par défaut sera créé, qui sera disponible pour getExternalizableConstructor () . Pour Serializable, la méthode getSerializableConstructor () obtient le constructeur de la classe Object et recherche la classe souhaitée, si elle ne la trouve pas, nous obtenons une exception ClassNotFoundException . Il s'avère que la principale différence entre Serializable et Externalizable est que le premier n'a pas besoin d'un constructeur pour créer une récupération d'objet. Il récupérera simplement complètement des octets. Pour le second, lors de la restauration, un objet sera d'abord créé à l'aide du constructeur au point de déclaration, puis les valeurs de ses champs à partir des octets reçus pendant la sérialisation y seront écrites. Personnellement, je préfère la première méthode, c'est beaucoup plus simple. De plus, même si nous devons encore définir le comportement de sérialisation, nous ne pouvons pas utiliser Externalizable , ni implémenter Serializable en y ajoutant (sans redéfinir) les méthodes writeObject () et readObject () . Mais pour qu'ils «travaillent», leur signature doit être strictement respectée.

 import java.io.*; public class Talda implements Serializable { private String name; private String description; public Talda(String name, String description) { this.name = name; this.description = description; } private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); System.out.println("Our writeObject"); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); System.out.println("Our readObject"); } @Override public String toString() { return "Talda{" + "name='" + name + '\'' + ", description='" + description + '\'' + '}'; } public static void main(String[] args) throws IOException, ClassNotFoundException { Talda partizanka = new Talda("Partizanka", "Viiiski"); System.out.println("Before: \n" + partizanka); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("Talda.out")); out.writeObject(partizanka); out.close(); ObjectInputStream in = new ObjectInputStream(new FileInputStream("Talda.out")); partizanka = (Talda) in.readObject(); System.out.println("After: \n" + partizanka); } } 

Conclusion:

 Before: Talda{name='Partizanka', description='Viiiski'} Our writeObject Our readObject After: Talda{name='Partizanka', description='Viiiski'} 

Dans nos méthodes ajoutées, defaultWriteObject () et defaultReadObject () sont appelés. Ils sont responsables de la sérialisation par défaut, comme si cela fonctionnait sans les méthodes que nous avons ajoutées.

En fait, ce n'est que la pointe de l'iceberg, si vous continuez à vous plonger dans le mécanisme de sérialisation, puis avec un degré élevé de probabilité, vous pouvez trouver plus de nuances, constatant ce que nous disons: "La sérialisation ... n'est pas si simple."

Source: https://habr.com/ru/post/fr431524/


All Articles