JavaRush /Blog Java /Random-FR /Sérialisation et désérialisation en Java

Sérialisation et désérialisation en Java

Publié dans le groupe Random-FR
Bonjour! Dans la conférence d'aujourd'hui, nous parlerons de la sérialisation et de la désérialisation en Java. Commençons par un exemple simple. Disons que vous êtes le créateur d'un jeu informatique. 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 d'évident aujourd'hui : sauvegarder et charger des jeux :) Sinon... imaginez ! J'ai peur qu'aujourd'hui un jeu sans une telle option soit voué à l'échec ! Et au fait, qu’est-ce que « sauvegarder » et « charger » un jeu ? Eh bien, dans le sens habituel, nous comprenons ce que c'est : nous voulons continuer le jeu là où nous l'avons laissé la dernière fois. Pour ce faire, nous créons une sorte de « point de contrôle », que nous utilisons ensuite pour charger le jeu. Mais qu’est-ce que cela signifie, non pas au sens courant, mais au sens de « programmeur » ? La réponse est simple : nous sauvegardons l’état de notre programme. Disons que vous jouez à un jeu de stratégie pour l'Espagne. Votre jeu a un État : qui possède quels territoires, qui a combien de ressources, qui est en alliance avec qui, et qui au contraire est en guerre, etc. Ces informations, l'état de notre programme, doivent être sauvegardées d'une manière ou d'une autre afin de restaurer ultérieurement les données et de continuer le jeu. C'est précisément à cela que servent les mécanismes de sérialisation et de désérialisation . La sérialisation est le processus de stockage de l'état d'un objet dans une séquence d'octets. La désérialisation est le processus de reconstruction d'un objet à partir de ces octets. Tout objet Java est converti en une séquence d'octets. Pourquoi est-ce? Nous avons dit à plusieurs reprises que les programmes n'existent pas en eux-mêmes. Le plus souvent, ils interagissent entre eux, échangent des données, etc. Et le format d'octet est pratique et efficace pour cela. Nous pouvons, par exemple, transformer notre objet de classe SavedGame(une partie sauvegardée) en une séquence d'octets, transférer ces octets via le réseau vers un autre ordinateur, et sur le deuxième ordinateur, transformer à nouveau ces octets en un objet Java ! C'est difficile à entendre, non ? Apparemment, organiser ce processus ne sera pas facile :/ Heureusement, non ! :) En Java, l'interface Serialisable est responsable des 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 ! Voici à quoi ressemblera notre classe de sauvegarde :
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private static final long serialVersionUID = 1L;

   private String[] territoriesInfo;
   private String[] resourcesInfo;
   private String[] diplomacyInfo;

   public SavedGame(String[] territoriesInfo, String[] resourcesInfo, String[] diplomacyInfo){
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public String[] getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(String[] territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public String[] getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(String[] resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

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

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

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + Arrays.toString(territoriesInfo) +
               ", resourcesInfo=" + Arrays.toString(resourcesInfo) +
               ", diplomacyInfo=" + Arrays.toString(diplomacyInfo) +
               '}';
   }
}
Trois ensembles de données sont responsables des informations sur les territoires, l'économie et la diplomatie, et l'interface sérialisable indique à la machine Java : « tout va bien, le cas échéant, les objets de cette classe peuvent être sérialisés ». Une interface qui n'a aucune méthode semble bizarre :/ Pourquoi est-elle nécessaire ? La réponse à cette question est ci-dessus : uniquement pour fournir les informations nécessaires à la machine Java. Dans l'une des conférences précédentes, nous avons brièvement évoqué les interfaces de marqueurs. Ce sont des interfaces informatives spéciales qui marquent simplement nos classes avec des informations supplémentaires qui seront utiles à la machine Java à l'avenir. Ils n’ont aucune méthode à mettre en œuvre. Ainsi, Serialisable est l’une de ces interfaces. Autre point important : la variable private static final long serialVersionUIDque nous avons définie dans la classe. Pourquoi est-ce nécessaire ? Ce champ contient l'identifiant de version unique de la classe sérialisée . Toute classe qui implémente l'interface Serialisable possède un identifiant de version. Il est calculé en fonction du contenu de la classe - champs, ordre de déclaration, méthodes. Et si nous modifions le type de champ et/ou le nombre de champs dans notre classe, l'identifiant de version changera instantanément. 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'un ensemble d'octets, la valeur serialVersionUIDest comparée à la valeur 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 manuellement cet ID de version pour notre classe. Dans notre cas, il sera simplement égal à 1 (vous pouvez le remplacer par n'importe quel autre nombre). Eh bien, il est temps d'essayer de sérialiser notre objet SavedGameet 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[] resourcesInfo = {"Spain has 100 gold", "Russia has 80 gold", "France has 90 gold"};
       String[] diplomacyInfo = {"France is at war with Russia, Spain has taken a position of neutrality"};

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

       //create 2 threads 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 game to file
       objectOutputStream.writeObject(savedGame);

       // close the stream and release resources
       objectOutputStream.close();
   }
}
Comme vous pouvez le voir, nous avons créé 2 fils de discussion - FileOutputStreamet ObjectOutputStream. Le premier d'entre eux peut écrire des données dans un fichier et le second peut convertir des objets en octets. Vous avez déjà vu des constructions « imbriquées » similaires, par exemple, new BufferedReader(new InputStreamReader(...))dans des cours précédents, elles ne devraient donc pas vous effrayer :) En créant une telle « chaîne » de deux threads, nous effectuons les deux tâches : nous transformons l'objet SavedGameen un ensemble d'octets et enregistrez-le dans un fichier en utilisant la méthode writeObject(). Et d’ailleurs, nous n’avons même pas vérifié ce que nous avions ! Il est temps de regarder le dossier ! *Remarque : le fichier n'a pas besoin d'être créé à l'avance. Si un fichier portant ce nom n'existe pas, il sera créé automatiquement* Et voici son contenu ! ¬н sr SavedGame [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoiresInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранцвоюет SЃ R оссиеР№, Р˜С ЃРїР° РЅРёСЏ Р·Р°Ряла позицию нейтралитетаuq ~ t "РЈ Р˜ спании 100 золотаt Р J R РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜С ЃРїР°РЅРёРё 6 РїСЂРѕРІРёРЅС †РёР№t %РЈ Р РѕСЃСЃРёРё 10 РїСЂРѕРІРёРЅС †РёР№t &РЈ ФранцРеРё 8 проввинций Oups :( Il semble que notre programme n'a pas fonctionné :( En fait, il a fonctionné. Vous vous souvenez que nous avons transféré exactement un ensemble d'octets vers le dossier, et pas seulement un objet ou un texte ? Eh bien, voici à quoi ressemble cet ensemble :) Ceci est notre partie sauvegardée ! Si nous voulons restaurer notre objet d'origine, c'est-à-dire charger et continuer le jeu là où nous l'avons laissé, nous avons besoin du processus inverse, désérialisation ... Voici à quoi cela ressemblera dans notre cas :
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{territoriesInfo=[L'Espagne a 6 provinces, la Russie a 10 provinces, la France a 8 provinces], resourcesInfo=[L'Espagne a 100 pièces d'or, la Russie a 80 pièces d'or, la France a 90 pièces d'or], diplomacyInfo=[La France est en guerre contre la Russie, L'Espagne a occupé une position de neutralité]} Génial ! Nous avons réussi à d'abord sauvegarder l'état de notre jeu dans un fichier, puis à le restaurer à partir du fichier. Essayons maintenant de faire de même, mais supprimons SavedGamel'identifiant de version de notre classe. Nous ne réécrirons pas nos deux classes, le code qu'elles contiennent sera le même, nous supprimerons SavedGamesimplement private static final long serialVersionUID. Voici notre objet après la sérialisation: ¬н sr Savedgameі € MIUоM ‰ [DiplomacyInfot [Ljava / Lang / String; [ResourcesInfoq ~ [TerritriesInfoq ~ xpur [ljava.lang.string; твзй {g xp t p¤р ° ћѕ ‚ СЃ Россией, Р˜СЃРїР°РЅРЏР·Р°РЅСЏР»Р° RїРѕР·РёС†РёСЋ РЅРµР№С ‚ралитетаuq ~ t "РЈ Р˜СЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 Р·Рѕ лотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 Р їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt &РЈ Р ¤СЂР°РЅС†РёРё 8 проввинцинА Et en essayant de le désérialiser, voici ce qui s'est passé : InvalidClassException : classe locale incompatible : stream classdesc serialVersionUID = - 196410440475012755, classe locale serialVersionUID = -6675950253085108747 C'est la même exception qui a été mentionnée ci-dessus. Vous pouvez en savoir plus à ce sujet dans l' article d'un de nos étudiants. Au fait, nous avons manqué un point important. Il est clair que les chaînes et les primitives sont facilement sérialisés : Java en a probablement, alors il existe des mécanismes intégrés pour cela. Mais que se passe-t-il si notre serializable-class possède des champs qui ne sont pas exprimés comme des primitives, mais comme des références à d'autres objets ? Créons, par exemple, des classes distinctes TerritoriesInfopour travailler avec notre classe . ResourcesInfoDiplomacyInfoSavedGame
public class TerritoriesInfo {

   private String info;

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

   public String getInfo() {
       return info;
   }

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

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

public class ResourcesInfo {

   private String info;

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

   public String getInfo() {
       return info;
   }

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

   @Override
   public String toString() {
       return "ResourcesInfo{" +
               "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 + '\'' +
               '}';
   }
}
Mais maintenant nous avons une question : toutes ces classes doivent-elles être sérialisables si nous voulons sérialiser la classe modifiée SavedGame?
import java.io.Serializable;
import java.util.Arrays;

public class SavedGame implements Serializable {

   private TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       this.diplomacyInfo = diplomacyInfo;
   }

   public TerritoriesInfo getTerritoriesInfo() {
       return territoriesInfo;
   }

   public void setTerritoriesInfo(TerritoriesInfo territoriesInfo) {
       this.territoriesInfo = territoriesInfo;
   }

   public ResourcesInfo getResourcesInfo() {
       return resourcesInfo;
   }

   public void setResourcesInfo(ResourcesInfo resourcesInfo) {
       this.resourcesInfo = resourcesInfo;
   }

   public DiplomacyInfo getDiplomacyInfo() {
       return diplomacyInfo;
   }

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

   @Override
   public String toString() {
       return "SavedGame{" +
               "territoriesInfo=" + territoriesInfo +
               ", resourcesInfo=" + resourcesInfo +
               ", diplomacyInfo=" + diplomacyInfo +
               '}';
   }
}
Eh bien, vérifions cela en pratique ! Laissons tout tel quel pour l'instant et essayons de sérialiser l'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
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, 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.NotSerializingException : échec de DiplomacyInfo ! En fait, voici la réponse à notre question. Lorsque vous sérialisez un objet, tous les objets auxquels il fait référence dans ses variables d'instance sont sérialisés. Et si ces objets font également référence à des objets tiers, ils sont également sérialisés. Et ainsi de suite à l’infini. Toutes les classes de cette chaîne doivent être sérialisables, sinon elles ne seront pas sérialisables et une exception sera levée. Cela pourrait d’ailleurs créer des problèmes à l’avenir. Que devons-nous faire, par exemple, si nous n’avons pas besoin d’une partie de la classe lors de la sérialisation ? Ou, par exemple, TerritoryInfonous avons hérité d'une classe de notre programme dans le cadre d'une bibliothèque. Cependant, il n’est pas sérialisable et, par conséquent, nous ne pouvons pas le modifier. Il s'avère que nous ne pouvons pas ajouter de champ TerritoryInfoà notre classe , car alors la classe entière deviendra non sérialisable ! Problème :/ Les problèmes de ce type sont résolus en Java à l'aide du mot-clé . Si vous ajoutez ce mot-clé à un champ de classe, la valeur de ce champ ne sera pas sérialisée. Essayons de créer l'un des champs de notre classe , après quoi nous sérialiserons et restaurerons un objet. SavedGameSavedGameSérialisation et désérialisation en Java - 2transientSavedGame transient
import java.io.Serializable;

public class SavedGame implements Serializable {

   private transient TerritoriesInfo territoriesInfo;
   private ResourcesInfo resourcesInfo;
   private DiplomacyInfo diplomacyInfo;

   public SavedGame(TerritoriesInfo territoriesInfo, ResourcesInfo resourcesInfo, DiplomacyInfo diplomacyInfo) {
       this.territoriesInfo = territoriesInfo;
       this.resourcesInfo = resourcesInfo;
       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
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces");
       ResourcesInfo resourcesInfo = new ResourcesInfo("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 position of neutrality");


       SavedGame savedGame = new SavedGame(territoriesInfo, resourcesInfo, 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{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='L'Espagne a 100 pièces d'or, la Russie a 80 pièces d'or, la France a 90 pièces d'or'}, diplomacyInfo=DiplomacyInfo{info='La France est en guerre contre la Russie, L'Espagne a adopté une position de neutralité'}} Dans le même temps, nous avons reçu une réponse à la question de savoir quelle valeur sera attribuée transientau champ -. Une valeur par défaut lui est attribuée. Dans le cas des objets, c'est null. À votre guise, vous pouvez lire cet excellent article sur la sérialisation . Il parle également de l'interface Externalizable, dont nous parlerons dans la prochaine conférence. De plus, il y a un chapitre sur ce sujet dans le livre « Head-First Java », faites attention :)
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION