CodeGym /Blog Java /Random-FR /Sérialisation et désérialisation en Java
Auteur
Volodymyr Portianko
Java Engineer at Playtika

Sérialisation et désérialisation en Java

Publié dans le groupe Random-FR
Salut! Dans la leçon d'aujourd'hui, nous parlerons de la sérialisation et de la désérialisation en Java. Nous allons commencer par un exemple simple. Imaginez que vous êtes un développeur de jeux informatiques. Si vous avez grandi dans les années 90 et que vous vous souvenez des consoles de jeux de cette époque, vous savez probablement qu'il leur manquait quelque chose que nous tenons pour acquis aujourd'hui : la possibilité de sauvegarder et de charger des jeux :) Sinon, imaginez ça !Sérialisation et désérialisation en Java - 1J'ai bien peur qu'un jeu sans ces capacités aujourd'hui soit voué à l'échec ! Quoi qu'il en soit, qu'est-ce que cela signifie de "sauvegarder" et de "charger" un jeu ? Eh bien, nous comprenons le sens quotidien : nous voulons continuer le jeu là où nous l'avons laissé. Pour ce faire, nous créons un certain "point de contrôle" que nous utiliserons plus tard pour charger le jeu. Mais qu'est-ce que cela signifie pour un programmeur plutôt que pour un joueur occasionnel ? La réponse est simple : nous sauvegardons l'état de notre programme. Disons que vous jouez contre l'Espagne dans un jeu de stratégie. Votre jeu a un état : quels territoires chacun possède, combien de ressources chacun possède-t-il, quelles alliances existent et avec qui, qui est en guerre, etc. Cette information, l'état de notre programme, doit en quelque sorte être sauvegardée afin de restaurer les données et de continuer le jeu. Comme ça arrive, La sérialisation en Java est le processus de sauvegarde de l'état d'un objet sous la forme d'une séquence d'octets. La désérialisation en Java est le processus de restauration d'un objet à partir de ces octets. Tout objet Java peut être converti en une séquence d'octets. Pourquoi avons nous besoin de ça? Nous avons répété à maintes reprises que les programmes n'existent pas en eux-mêmes. Le plus souvent, ils interagissent entre eux, échangent des données, etc. Un format d'octet est pratique et efficace pour cela. Par exemple, nous pouvons convertir un objet de notre SavedGameclasse en une séquence d'octets, transférez ces octets sur le réseau vers un autre ordinateur, puis sur l'autre ordinateur, reconvertissez ces octets en un objet Java ! Cela semble difficile, hein ? Il semble que ce serait difficile de faire en sorte que tout cela se produise : / Heureusement, ce n'est pas le cas ! :) En Java, l' interface Serializable est responsable du processus de sérialisation. Cette interface est extrêmement simple : vous n'avez pas besoin d'implémenter une seule méthode pour l'utiliser ! Regardez à quel point notre classe pour sauvegarder des jeux est simple :

import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoryInfo;
   private String[] resourceInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoryInfo, String[] resourceInfo, String[] diplomacyInfo){
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(String[] territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public String[] getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(String[] resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

   public String[] getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(String[] diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + Arrays.toString(territoryInfo) +
               ", resourceInfo=" + Arrays.toString(resourceInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
Trois tableaux sont responsables des informations sur les territoires, les ressources et la diplomatie, et l'interface sérialisable indique à la machine Java : " tout va bien si les objets de cette classe peuvent être sérialisés ". Une interface sans une seule interface a l'air bizarre :/ Pourquoi est-ce nécessaire ? La réponse à cette question est donnée ci-dessus : il suffit de fournir les informations nécessaires à la machine Java. Dans une leçon précédente, nous avons brièvement mentionné les interfaces de marqueur. Ce sont des interfaces d'information spéciales qui marquent simplement nos classes avec des informations supplémentaires qui seront utiles à la machine Java à l'avenir. Ils n'ont pas de méthodes que vous devez mettre en œuvre. Voici Serializable - une de ces interfaces. Voici un autre point important : pourquoi avons-nous besoin devariable privée statique finale longue serialVersionUID que nous avons définie dans la classe ? Ce champ contient l'identifiant de version unique de la classe sérialisée. Chaque classe qui implémente l' interface Serializable a un identifiant de version. Il est déterminé en fonction du contenu de la classe — les champs et leur ordre de déclaration, et les méthodes et leur ordre de déclaration. Et si nous changeons un type de champ et/ou le nombre de champs dans notre classe, l'identifiant de version change instantanément. Le serialVersionUID est également écrit lorsque la classe est sérialisée. Lorsque nous essayons de désérialiser, c'est-à-dire de restaurer un objet à partir d'une séquence d'octets, la valeur de serialVersionUID est comparée à la valeur de serialVersionUIDde la classe dans notre programme. Si les valeurs ne correspondent pas, une java.io.InvalidClassException sera levée. Nous en verrons un exemple ci-dessous. Pour éviter de telles situations, nous définissons simplement l'identifiant de version de notre classe manuellement. Dans notre cas, il sera simplement égal à 1 (vous pouvez utiliser n'importe quel autre nombre que vous aimez). Eh bien, il est temps d'essayer de sérialiser notre objet SavedGame et de voir ce qui se passe !

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       String[] territoryInfo = {"Spain has 6 provinces", "Russia has 10 provinces", "France has 8 provinces"};
       String[] resourceInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a neutral position"};

       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       // Create 2 streams to serialize the object and save it to a file
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // Save the game to a file
       objectOutputStream.writeObject(savedGame);

       // Close the stream and release resources
       objectOutputStream.close();
   }
}
Comme vous pouvez le voir, nous avons créé 2 flux : FileOutputStream et ObjectOutputStream . Le premier sait comment écrire des données dans le fichier et le second convertit les objets en octets. Vous avez déjà vu des constructions imbriquées similaires, par exemple, new BufferedReader(new InputStreamReader(...)) , dans les leçons précédentes, elles ne devraient donc pas vous effrayer :) En créant cette chaîne de deux flux, nous effectuons les deux tâches : nous convertissons l' objet SavedGame en une séquence d'octets et l'enregistrons dans un fichier à l'aide de la méthode writeObject() . Et, soit dit en passant, nous n'avons même pas regardé ce que nous avons eu ! Il est temps de regarder le dossier ! *Remarque : il n'est pas nécessaire de créer le fichier à l'avance. Si un fichier avec le nom spécifié n'existe pas, alors il sera créé automatiquement* Et voici son contenu : ¬í sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ territorialInfoq ~ xpur [Ljava.lang. Chaîne;¬ТVзй{G xp t pФранция воюет СЃ Россией, Р˜СЃРїР°РЅР ёСЏ заняла позицию нейтралит етаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотаt РЈ Р РѕСЃСЃРёРё 80 Р·РѕР» отаt !РЈ Франции 90 Р·РѕР »РѕС‚Р°uq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10                  Oh, oh :( Il semble que notre programme n'ait pas fonctionné : ( En fait, cela a fonctionné. Vous vous souvenez que nous avons envoyé une séquence d'octets, et pas simplement un objet ou un texte, au fichier ? Eh bien, c'est ce que cette séquence d'octets ressemble :) C'est notre partie sauvegardée ! Si nous voulons restaurer notre objet d'origine, c'est-à-dire démarrer et continuer le jeu là où nous l'avons laissé, nous avons besoin du processus inverse : la désérialisation. Voici à quoi cela ressemble pour nous :

import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);
   }
}
Et voici le résultat ! SavedGame{territoryInfo=[L'Espagne a 6 provinces, la Russie a 10 provinces, la France a 8 provinces], resourceInfo=[L'Espagne a 100 or, la Russie a 80 or, la France a 90 or], diplomacyInfo=[La France est en guerre avec la Russie, L'Espagne a adopté une position neutre]} Excellent ! Nous avons réussi à enregistrer d'abord l'état de notre jeu dans un fichier, puis à le restaurer à partir du fichier. Essayons maintenant de faire la même chose, mais nous allons supprimer l'identifiant de version de notre classe SavedGame . Nous ne réécrirons pas nos deux classes. Leur code sera le même. Nous allons simplement supprimer le private static final long serialVersionUID de la classe SavedGame . Voici notre objet après sérialisation : ¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ territorialInfoq ~ xpur [Ljava.lang.String;¬ТVзй{G xp t pФранция воюет СЃ Россией, Р˜СЃРїР°РЅРёСЏ заняла позицию РЅРµР№С‚СЂР°Р»РёС‚РµС ‚Р°uq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотР°t РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 РїСЂРѕРІР ёРЅС†РёР№ Mais regardez ce qui se passe lorsque nous essayons de le désérialiser : InvalidClassException : classe locale incompatible : flux classdesc serialVersionUID = -196410440475012755, classe locale serialVersionUID = -6675950253085108747 Soit dit en passant, nous avons raté quelque chose d'important. De toute évidence, les chaînes et les primitives sont facilement sérialisées : Java a certainement un mécanisme intégré pour cela. Mais que se passe-t-il si notre classe sérialisable a des champs qui ne sont pas des primitifs, mais plutôt des références à d'autres objets ? Par exemple, créons des classes séparées TerritoryInfo , ResourceInfo et DiplomacyInfo pour travailler avec notre classe SavedGame .

public class TerritoryInfo {

   private String info;

   public TerritoryInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "TerritoryInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class ResourceInfo {

   private String info;

   public ResourceInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "ResourceInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}

public class DiplomacyInfo {

   private String info;

   public DiplomacyInfo(String info) {
       this.info = info;
   }

   public String getInfo() {
       return info;
   }

   public void setInfo(String info) {
       this.info = info;
   }

   @Override
   public String toString() {
       return "DiplomacyInfo{" +
               "info='" + info + '\'' +
               '}';
   }
}
Et maintenant, nous sommes confrontés à une question : toutes ces classes doivent-elles être sérialisables si nous voulons sérialiser notre classe SavedGame ?

import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoryInfo getTerritoryInfo() {
       return territoryInfo;
   }

   public void setTerritoryInfo(TerritoryInfo territoryInfo) {
       this.territoryInfo = territoryInfo;
   }

   public ResourceInfo getResourceInfo() {
       return resourceInfo;
   }

   public void setResourceInfo(ResourceInfo resourceInfo) {
       this.resourceInfo = resourceInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

   public void setDiplomacyInfo(DiplomacyInfo diplomacyInfo) {
       this.diplomacyInfo = diplomacyInfo;
   }

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoryInfo=" + territoryInfo +
               ", resourceInfo=" + resourceInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
Très bien alors! Testons-le ! Pour l'instant, nous allons tout laisser tel quel et essayer de sérialiser un objet SavedGame :

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}
Résultat : Exception dans le thread "main" java.io.NotSerializableException : DiplomacyInfo Cela n'a pas fonctionné ! Alors, voici la réponse à notre question. Lorsqu'un objet est sérialisé, tous les objets référencés par ses variables d'instance sont sérialisés. Et si ces objets font également référence à d'autres objets, ils sont également sérialisés. Et ainsi de suite pour toujours. Toutes les classes de cette chaîne doivent être Serializable , sinon il sera impossible de les sérialiser et une exception sera levée. Soit dit en passant, cela peut créer des problèmes sur la route. Par exemple, que devons-nous faire si nous n'avons pas besoin d'une partie d'une classe lors de la sérialisation ? Ou si nous obtenions notre classe TerritoryInfo "par héritage" dans le cadre d'une bibliothèque ? Et supposez en outre que ce n'est paset, par conséquent, nous ne pouvons pas le changer. Cela signifierait que nous ne pouvons pas ajouter un champ TerritoryInfo à notre classe SavedGame , car alors toute la classe SavedGame deviendrait non sérialisable ! C'est un problème : / Sérialisation et désérialisation en Java - 2En Java, ce type de problème est résolu par le mot-clé transient . Si vous ajoutez ce mot-clé à un champ de votre classe, ce champ ne sera pas sérialisé. Essayons de rendre l'un des champs de notre classe SavedGame transient , puis nous sérialiserons et restaurerons un seul objet.

import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoryInfo territoryInfo;
   private ResourceInfo resourceInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoryInfo territoryInfo, ResourceInfo resourceInfo, DiplomacyInfo diplomacyInfo) {
       this.territoryInfo = territoryInfo;
       this.resourceInfo = resourceInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   // ...getters, setters, toString()...
}



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // Create our object
       TerritoryInfo territoryInfo = new TerritoryInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourceInfo resourceInfo = new ResourceInfo("Spain has 100 gold, Russia has 80 gold, France has 90 gold");
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo("France is at war with Russia, Spain has taken a neutral position");


       SavedGame savedGame = new SavedGame(territoryInfo, resourceInfo, diplomacyInfo);

       FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);

       objectOutputStream.writeObject(savedGame);

       objectOutputStream.close();
   }
}


import java.io.*;

public class Main {

   public static void main(String[] args) throws IOException, ClassNotFoundException {

       FileInputStream fileInputStream = new FileInputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);

       SavedGame savedGame = (SavedGame) objectInputStream.readObject();

       System.out.println(savedGame);

       objectInputStream.close();


   }
}
Et voici le résultat : SavedGame{territoryInfo=null, resourceInfo=ResourceInfo{info='L'Espagne a 100 or, la Russie a 80 or, la France a 90 or'}, diplomacyInfo=DiplomacyInfo{info='La France est en guerre avec la Russie, l'Espagne a pris une position neutre'}} Cela dit, nous avons obtenu une réponse à la question de savoir quelle valeur sera attribuée à un champ transitoire . La valeur par défaut lui est attribuée. Pour les objets, c'est null . Vous pouvez lire un excellent chapitre sur ce sujet dans le livre 'Head-First Java', faites-y attention :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION