Salut! Dans la leçon d'aujourd'hui, nous parlons de sérialisation et de désérialisation en Java. Nous allons commencer par un exemple simple. Disons que vous avez créé un jeu vidéo. 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 !
J'ai bien peur qu'aujourd'hui un jeu sans ces capacités soit voué à l'échec ! Qu'est-ce que cela signifie de "sauvegarder" et de "charger" un jeu de toute façon ? Eh bien, nous comprenons le sens ordinaire : nous voulons continuer le jeu là où nous l'avons laissé. 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 pour un programmeur plutôt que pour un joueur occasionnel ? La réponse est simple : nous. Disons que vous jouez avec l'Espagne dans Strategium. Votre jeu a un état : qui possède quels territoires, qui a combien de ressources, qui est dans une alliance avec qui, qui est en guerre avec qui, etc. Nous devons en quelque sorte sauvegarder cette information, l'état de notre programme, afin de la restaurer à l'avenir et continuer le jeu. Car c'est précisément à cela que servent la sérialisation et la déséréalisation . La sérialisation est le processus de stockage de l'état d'un objet dans une séquence d'octets. Désérialisationest le processus de restauration d'un objet à partir de ces octets. Tout objet Java peut être converti en une séquence d'octets. Pourquoi aurions-nous besoin de cela ? Nous avons dit plus d'une fois que les programmes n'existent pas seuls. Le plus souvent, ils interagissent avec d'autres programmes, échangent des données, etc. Et une séquence d'octets est un format pratique et efficace. Par exemple, nous pouvons transformer notre
En Java, les problèmes de ce genre sont résolus à l'aide du

SavedGame
objet en une séquence d'octets, envoyer ces octets sur le réseau à un autre ordinateur, puis, sur le deuxième ordinateur, transformer ces octets en un objet Java ! Cela semble difficile, non ? Et la mise en œuvre de ce processus semble pénible :/ Heureusement, ce n'est pas le cas ! :) En Java, leSerializable
interface 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 ! Voici à quel point notre classe de sauvegarde de jeu est simple :
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) +
'}';
}
}
Les trois tableaux sont responsables des informations sur les territoires, les ressources et la diplomatie. L'interface Serializable indique à la machine virtuelle Java : " Tout va bien — si nécessaire, 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 se trouve ci-dessus : elle ne sert qu'à fournir les informations nécessaires à la machine virtuelle Java. Dans l'une de nos leçons précédentes, 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.Serializable
est l'une de ces interfaces. Autre point important : Pourquoi avons-nous besoin de la private static final long serialVersionUID
variable que nous avons définie dans la classe ? Pourquoi est-ce nécessaire ? Ce champ contient un identifiant unique pour la version de la classe sérialisée . Toute classe qui implémente l' Serializable
interface a un version
identifiant. Il est calculé en fonction du contenu de la classe : ses champs, l'ordre dans lequel ils sont déclarés, les méthodes, etc. Si on change le type de champ et/ou le nombre de champs dans notre classe, alors l'identifiant de version change immédiatement . 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, l'associé serialVersionUID
est comparé à la valeur deserialVersionUID
pour la classe de notre programme. Si les valeurs ne correspondent pas, alors un fichier java.io. InvalidClassException sera levée. Nous en verrons un exemple ci-dessous. Pour éviter cela, nous définissons simplement l'identifiant de version manuellement dans notre classe. Dans notre cas, il sera simplement égal à 1 (mais vous pouvez remplacer n'importe quel autre nombre que vous aimez). Eh bien, il est temps d'essayer de sérialiser notre SavedGame
objet 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[] 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 neutral position"};
SavedGame savedGame = new SavedGame(territoryInfo, resourcesInfo, 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 free resources
objectOutputStream.close();
}
}
Comme vous pouvez le voir, nous avons créé 2 flux : FileOutputStream
et ObjectOutputStream
. Le premier peut écrire des données dans un 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, donc celles-ci ne devraient pas vous effrayer :) En créant une telle "chaîne" de deux flux, nous effectuons les deux tâches : nous convertissons l' SavedGame
objet en un ensemble d'octets et enregistrez-le dans un fichier à l'aide de la writeObject()
méthode. 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 : vous n'avez pas besoin de créer le fichier à 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 ~ [ territoriesInfoq ~ xpur [Ljava.lang.String;ТVзй{G xp t pФранция воюет СЃ Россией, Рспания заняла позицию нейтралитетаuq ~ t "РЈ Рспании 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Рспании 6 провинцийt %РЈ Р РѕСЃСЃРёРё 10 провинцийt &РЈ Франции 8 провинций
Uh-oh :( Il semble que notre programme n'a pas fonctionné :( En fait, il a fonctionné. Vous souvenez-vous que nous avons envoyé un ensemble d'octets, pas simplement un objet ou un texte, au fichier ? jeu d'octets ressemble à :) Ceci 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 : 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=["Spain has 6 provinces, Russia has 10 provinces, France has 8 provinces], resourcesInfo=[Spain has 100 gold, Russia has 80 gold, France has 90 gold], diplomacyInfo=[France is at war with Russia, Spain has taken a neutral position]}
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 sans l'identifiant de version de notre SavedGame
classe. Nous ne réécrirons pas nos deux classes. Leur code restera le même, mais nous le supprimerons private static final long serialVersionUID
de la SavedGame
classe. Voici notre objet après sérialisation :
¬н sr SavedGameі€MіuОm‰ [ diplomacyInfot [Ljava/lang/String;[ resourcesInfoq ~ [ territoriesInfoq ~ 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: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
C'est l'exception que nous avons mentionnée ci-dessus. Soit dit en passant, nous avons raté quelque chose d'important. Il est logique que les chaînes et les primitives puissent être facilement sérialisées : Java a probablement une sorte de mécanisme intégré pour le faire. Mais que se passe-t-il si notre serializable
classe 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 et TerritoriesInfo
séparées pour travailler avec notre classe. ResourcesInfo
DiplomacyInfo
SavedGame
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 + '\'' +
'}';
}
}
Et maintenant, une question se pose : toutes ces classes doivent-elles l'être Serializable
si nous voulons sérialiser notre SavedGame
classe modifiée ?
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, testons-le! Laissons tout tel quel et essayons de sérialiser un SavedGame
objet :
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(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 in thread "main" java.io.NotSerializableException: DiplomacyInfo
Ça n'a pas marché ! En gros, c'est 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 à l'infini. Toutes les classes de cette chaîne doivent êtreSerializable
, 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. Que devons-nous faire si, par exemple, nous n'avons pas besoin d'une partie d'une classe lorsque nous sérialisons ? Ou, par exemple, que se passe-t-il si la TerritoryInfo
classe nous est parvenue dans le cadre d'une bibliothèque tierce. Et supposons en outre que ce n'est pas le cas Serializable
et, par conséquent, nous ne pouvons pas le changer. Il s'avère que nous ne pouvons pas ajouter de TerritoryInfo
champ à notreSavedGame
class, car cela rendrait toute la SavedGame
classe non sérialisable ! C'est un problème :/ 
transient
mot-clé. Si vous ajoutez ce mot-clé à un champ de votre classe, ce champ ne sera pas sérialisé. Essayons de rendre l'un des SavedGame
champs d'instance de la classe transitoire. Ensuite, nous sérialiserons et restaurerons un objet.
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
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(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='Spain has 100 gold, Russia has 80 gold, France has 90 gold'}, diplomacyInfo=DiplomacyInfo{info='France is at war with Russia, Spain has taken a neutral position'}}
De plus, nous avons obtenu une réponse à notre question sur la valeur attribuée à un transient
champ. Il obtient la valeur par défaut. Pour les objets, c'est null
. Vous pouvez lire cet excellent article sur la sérialisation lorsque vous avez quelques minutes à perdre. Il mentionne également l' Externalizable
interface, dont nous parlerons dans la prochaine leçon. De plus, le livre "Head-First Java" contient un chapitre sur ce sujet. Accordez-lui un peu d'attention :)
GO TO FULL VERSION