Bună! În lecția de astăzi, vorbim despre serializare și deserializare în Java. Vom începe cu un exemplu simplu. Să presupunem că ai creat un joc pe computer. Dacă ai crescut în anii 90 și îți amintești consolele de jocuri din acea epocă, probabil știi că le lipsea ceva ce luăm de la sine înțeles astăzi — capacitatea de a salva și încărca jocuri :) Dacă nu, imaginează-ți asta! Mi-e teamă că astăzi un joc fără aceste abilități ar fi condamnat! Ce înseamnă oricum să „salvezi” și să „încarci” un joc? Ei bine, înțelegem sensul obișnuit: vrem să continuăm jocul din locul de unde am plecat. Pentru a face acest lucru, creăm un fel de „punct de verificare”, pe care apoi îl folosim pentru a încărca jocul. Dar ce înseamnă asta pentru un programator mai degrabă decât pentru un jucător ocazional? Răspunsul este simplu: noi'. Să presupunem că joci ca Spania în Strategium. Jocul tău are un stat: cine deține ce teritorii, cine are câte resurse, cine este într-o alianță cu cine, cine este în război cu cine și așa mai departe. Trebuie să salvăm cumva aceste informații, starea programului nostru, pentru a le restabili în viitor și a continua jocul. Căci tocmai pentru asta sunt serializarea și deserealizarea . Serializarea este procesul de stocare a stării unui obiect într-o secvență de octeți. Deserializareaeste procesul de restaurare a unui obiect din acești octeți. Orice obiect Java poate fi convertit într-o secvență de octeți. De ce am avea nevoie de asta? Am spus de mai multe ori că programele nu există de la sine. Cel mai adesea, ei interacționează cu alte programe, schimbă date etc. Și o secvență de octeți este un format convenabil și eficient. De exemplu, putem transforma
SavedGame
obiectul nostru într-o secvență de octeți, trimitem acești octeți prin rețea către alt computer, iar apoi pe al doilea computer transformăm acești octeți înapoi într-un obiect Java! Sună dificil, nu? Și implementarea acestui proces pare o durere :/ Din fericire, nu este așa! :) În Java,Serializable
interfața este responsabilă pentru procesul de serializare. Această interfață este extrem de simplă: nu trebuie să implementați o singură metodă pentru a o folosi! Iată cât de simplă arată clasa noastră de salvare a jocurilor:
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) +
'}';
}
}
Cele trei matrice sunt responsabile pentru informații despre teritorii, resurse și diplomație. Interfața Serializable îi spune mașinii virtuale Java: „ Totul este în regulă — dacă este necesar, obiectele din această clasă pot fi serializate ”. O interfață fără o singură interfață arată ciudat :/ De ce este necesară? Răspunsul la această întrebare poate fi văzut mai sus: servește doar la furnizarea informațiilor necesare mașinii virtuale Java. Într-una dintre lecțiile noastre anterioare, am menționat pe scurt interfețele de marcare . Acestea sunt interfețe informaționale speciale care pur și simplu marchează clasele noastre cu informații suplimentare care vor fi utile mașinii Java în viitor. Nu au nicio metodă pe care trebuie să le implementați.Serializable
este una dintre acele interfețe. Un alt punct important: De ce avem nevoie de private static final long serialVersionUID
variabila pe care am definit-o în clasă? De ce este nevoie? Acest câmp conține un identificator unic pentru versiunea clasei serializate . Orice clasă care implementează Serializable
interfața are un version
identificator. Se calculează pe baza conținutului clasei: câmpurile acesteia, ordinea în care sunt declarate, metode etc. Dacă schimbăm tipul de câmp și/sau numărul de câmpuri din clasa noastră, atunci identificatorul de versiune se schimbă imediat . serialVersionUID
este scris și atunci când clasa este serializată. Când încercăm să deserializăm, adică să restaurăm un obiect dintr-un set de octeți, valoarea asociată serialVersionUID
este comparată cu valoarea luiserialVersionUID
pentru clasa din programul nostru. Dacă valorile nu se potrivesc, atunci un java.io. InvalidClassException va fi aruncată. Vom vedea un exemplu în acest sens mai jos. Pentru a evita acest lucru, pur și simplu setăm manual identificatorul versiunii în clasa noastră. În cazul nostru, va fi pur și simplu egal cu 1 (dar puteți înlocui orice alt număr doriți). Ei bine, este timpul să încercăm să ne serializam SavedGame
obiectul și să vedem ce se întâmplă!
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();
}
}
După cum puteți vedea, am creat 2 fluxuri: FileOutputStream
și ObjectOutputStream
. Primul poate scrie date într-un fișier, iar al doilea convertește obiectele în octeți. Ați văzut deja constructe „imbricate” similare, de exemplu, , new BufferedReader(new InputStreamReader(...))
în lecțiile anterioare, așa că acestea nu ar trebui să vă sperie :) Prin crearea unui astfel de „lanț” de două fluxuri, îndeplinim ambele sarcini: transformăm obiectul SavedGame
într-un set de octeți și salvați-l într-un fișier folosind writeObject()
metoda. Și, apropo, nici nu ne-am uitat la ce am primit! E timpul să te uiți la dosar! *Notă: nu trebuie să creați fișierul în avans. Dacă un fișier cu acest nume nu există, acesta va fi creat automat* Și iată conținutul lui!
¬н 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 :( Se pare că programul nostru nu a funcționat :( De fapt, a funcționat. Vă amintiți că am trimis un set de octeți, nu doar un obiect sau text, la fișier? Ei bine, asta este ceea ce setul de octeți arată ca :) Acesta este jocul nostru salvat! Dacă dorim să restabilim obiectul original, adică să începem și să continuăm jocul de unde l-am lăsat, atunci avem nevoie de procesul invers: deserializarea . Iată cum va arăta în caz:
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);
}
}
Și iată rezultatul!
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]}
Excelent! Am reușit să salvăm mai întâi starea jocului într-un fișier, apoi să-l restaurăm din fișier. Acum să încercăm să facem același lucru, dar fără identificatorul de versiune pentru SavedGame
clasa noastră. Nu ne vom rescrie ambele clase. Codul lor va rămâne același, dar îl vom elimina private static final long serialVersionUID
din SavedGame
clasă. Iată obiectul nostru după serializare:
¬н 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 провинций
Dar uită-te la ce se întâmplă când încercăm să-l deserializăm:
InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
Aceasta este însăși excepția pe care am menționat-o mai sus. Apropo, am omis ceva important. Este logic ca șirurile de caractere și primitivele să poată fi serializate cu ușurință: Java are probabil un fel de mecanism încorporat pentru a face acest lucru. Dar ce se întâmplă dacă serializable
clasa noastră are câmpuri care nu sunt primitive, ci mai degrabă referințe la alte obiecte? De exemplu, să creăm clase separate TerritoriesInfo
și să lucreze cu clasa noastră. 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 + '\'' +
'}';
}
}
Și acum apare o întrebare: trebuie să fie toate aceste clase Serializable
dacă vrem să ne serializam SavedGame
clasa modificată?
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 +
'}';
}
}
Ei bine, hai să-l testăm! Să lăsăm totul așa cum este și să încercăm să serializam un SavedGame
obiect:
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();
}
}
Rezultat:
Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
Nu a mers! Practic, acesta este răspunsul la întrebarea noastră. Când un obiect este serializat, toate obiectele la care se face referire de variabilele sale de instanță sunt serializate. Și dacă acele obiecte fac referire și la alte obiecte, atunci ele sunt, de asemenea, serializate. Și așa mai departe la infinit. Toate clasele din acest lanț trebuie să fieSerializable
, altfel va fi imposibil să le serializeze și se va arunca o excepție. Apropo, acest lucru poate crea probleme pe viitor. Ce ar trebui să facem dacă, de exemplu, nu avem nevoie de o parte dintr-o clasă când serializăm? Sau, de exemplu, ce se întâmplă dacă TerritoryInfo
clasa ar veni la noi ca parte a unei biblioteci terță parte. Și să presupunem în continuare că nu este Serializable
și, în consecință, nu o putem schimba. Se pare că nu putem adăuga un TerritoryInfo
câmp la noiSavedGame
clasa, pentru că în acest fel ar face ca întreaga SavedGame
clasă să nu fie serializabilă! Aceasta este o problemă :/ În Java, problemele de acest fel sunt rezolvate folosind transient
cuvântul cheie. Dacă adăugați acest cuvânt cheie la un câmp al clasei dvs., atunci acel câmp nu va fi serializat. Să încercăm să facem unul dintre SavedGame
câmpurile de instanță ale clasei tranzitoriu. Apoi vom serializa și vom restaura un obiect.
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();
}
}
Și iată rezultatul:
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'}}
În plus, am primit un răspuns la întrebarea noastră despre ce valoare este atribuită unui transient
câmp. I se atribuie valoarea implicită. Pentru obiecte, aceasta este null
. Puteți citi acest articol excelent despre serializare când aveți câteva minute libere. Menționează și Externalizable
interfața, despre care vom vorbi în lecția următoare. În plus, cartea „Head-First Java” are un capitol pe acest subiect. Acordă-i puțină atenție :)
GO TO FULL VERSION