CodeGym /Blog Java /Random-PL /Jaka jest różnica między serializacją a deserializacją w ...
Autor
Aditi Nawghare
Software Engineer at Siemens

Jaka jest różnica między serializacją a deserializacją w Javie?

Opublikowano w grupie Random-PL
Cześć! W dzisiejszej lekcji porozmawiamy o serializacji i deserializacji w Javie. Zaczniemy od prostego przykładu. Załóżmy, że stworzyłeś grę komputerową. Jeśli dorastałeś w latach 90. i pamiętasz konsole do gier z tamtej epoki, prawdopodobnie wiesz, że brakowało im czegoś, co dziś uważamy za oczywiste — możliwości zapisywania i wczytywania gier :) Jeśli nie, wyobraź to sobie! Jaka jest różnica między serializacją a deserializacją w Javie?  - 1 Obawiam się, że dzisiaj gra bez tych umiejętności byłaby skazana na porażkę! Co to w ogóle znaczy „zapisać” i „wczytać” grę? Cóż, rozumiemy zwykłe znaczenie: chcemy kontynuować grę od miejsca, w którym ją przerwaliśmy. W tym celu tworzymy swego rodzaju „punkt kontrolny”, którego następnie używamy do wczytywania gry. Ale co to oznacza dla programisty, a nie dla zwykłego gracza? Odpowiedź jest prosta: my'. Załóżmy, że grasz Hiszpanią w Strategium. Twoja gra ma stan: kto jest właścicielem terytoriów, kto ma ile zasobów, kto jest w sojuszu z kim, kto jest w stanie wojny z kim i tak dalej. Musimy w jakiś sposób zapisać te informacje, stan naszego programu, aby w przyszłości odtworzyć go i kontynuować grę. Do tego właśnie służą serializacja i deserealizacja . Serializacja to proces przechowywania stanu obiektu w sekwencji bajtów. Deserializacjato proces przywracania obiektu z tych bajtów. Każdy obiekt Java można przekonwertować na sekwencję bajtów. Dlaczego mielibyśmy tego potrzebować? Wielokrotnie mówiliśmy, że programy nie istnieją same z siebie. Najczęściej wchodzą w interakcje z innymi programami, wymieniają dane itp. Sekwencja bajtów to wygodny i wydajny format. Na przykład możemy przekształcić nasz SavedGameobiekt w ciąg bajtów, wysłać te bajty przez sieć do innego komputera, a następnie na drugim komputerze zamienić te bajty z powrotem w obiekt Java! Brzmi trudno, prawda? A wdrożenie tego procesu wydaje się uciążliwe :/ Na szczęście tak nie jest! :) W Javie,Serializableinterfejs odpowiada za proces serializacji. Ten interfejs jest niezwykle prosty: nie musisz implementować ani jednej metody, aby z niego korzystać! Oto jak prosta jest nasza klasa oszczędzania gry:

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) +
               '}';
   }
}
Trzy tablice są odpowiedzialne za informacje o terytoriach, zasobach i dyplomacji. Interfejs Serializable mówi wirtualnej maszynie Java: „ Wszystko jest OK — w razie potrzeby obiekty tej klasy można serializować ”. Interfejs bez jednego interfejsu wygląda dziwnie :/ Dlaczego jest potrzebny? Odpowiedź na to pytanie można zobaczyć powyżej: służy jedynie do dostarczenia niezbędnych informacji wirtualnej maszynie Java. W jednej z naszych poprzednich lekcji pokrótce wspomnieliśmy o interfejsach znaczników . Są to specjalne interfejsy informacyjne, które po prostu oznaczają nasze klasy dodatkowymi informacjami, które będą przydatne dla maszyny Java w przyszłości. Nie mają żadnych metod, które trzeba wdrożyć.Serializablejest jednym z tych interfejsów. Kolejna ważna kwestia: po co nam zmienna private static final long serialVersionUID, którą zdefiniowaliśmy w klasie? Dlaczego jest potrzebny? To pole zawiera unikalny identyfikator wersji serializowanej klasy . Każda klasa, która implementuje Serializableinterfejs, ma versionidentyfikator. Obliczana jest na podstawie zawartości klasy: jej pól, kolejności ich deklarowania, metod itp. Jeśli zmienimy typ pola i/lub liczbę pól w naszej klasie, to od razu zmienia się identyfikator wersji . serialVersionUIDjest również zapisywany, gdy klasa jest serializowana. Kiedy próbujemy deserializować, czyli odtworzyć obiekt ze zbioru bajtów, skojarzony serialVersionUIDjest porównywany z wartościąserialVersionUIDdla klasy w naszym programie. Jeśli wartości się nie zgadzają, to java.io. Zostanie zgłoszony wyjątek InvalidClassException . Przykład tego zobaczymy poniżej. Aby tego uniknąć, po prostu ręcznie ustawiamy identyfikator wersji w naszej klasie. W naszym przypadku będzie to po prostu równe 1 (ale możesz zastąpić dowolną inną liczbę). Cóż, nadszedł czas, aby spróbować serializować nasz SavedGameobiekt i zobaczyć, co się stanie!

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();
   }
}
Jak widać stworzyliśmy 2 strumienie: FileOutputStreami ObjectOutputStream. Pierwszy może zapisywać dane do pliku, a drugi konwertuje obiekty na bajty. Podobne „zagnieżdżone” konstrukcje, np. , widziałeś już new BufferedReader(new InputStreamReader(...))na poprzednich lekcjach, więc nie powinny Cię one przerażać :) Tworząc taki „łańcuch” dwóch strumieni, wykonujemy oba zadania: przekształcamy obiekt SavedGamew zbiór bajtów i zapisz go do pliku przy użyciu writeObject()metody. A tak przy okazji, nawet nie spojrzeliśmy na to, co mamy! Czas spojrzeć na plik! *Uwaga: nie musisz wcześniej tworzyć pliku. Jeśli plik o tej nazwie nie istnieje, zostanie utworzony automatycznie* A oto jego zawartość!

¬н 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 :( Wygląda na to, że nasz program nie zadziałał :( W rzeczywistości zadziałał. Pamiętasz, że wysłaliśmy zestaw bajtów, a nie tylko obiekt lub tekst, do pliku? Cóż, o to chodzi zestaw bajtów wygląda tak :) To jest nasz zapisany stan gry!Jeśli chcemy przywrócić nasz pierwotny obiekt, tj. rozpocząć i kontynuować grę w miejscu, w którym ją przerwaliśmy, potrzebujemy procesu odwrotnego: deserializacji.Oto jak będzie wyglądać w naszym sprawa:

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);
   }
}
A oto wynik!

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]}
Doskonały! Udało nam się najpierw zapisać stan naszej gry do pliku, a następnie odtworzyć go z pliku. Teraz spróbujmy zrobić to samo, ale bez identyfikatora wersji dla naszej SavedGameklasy. Nie będziemy przepisywać obu naszych zajęć. Ich kod pozostanie taki sam, ale usuniemy go private static final long serialVersionUIDz SavedGameklasy. Oto nasz obiekt po serializacji:

¬н 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 провинций
Ale spójrz, co się stanie, gdy spróbujemy go deserializować:

InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
To właśnie ten wyjątek, o którym wspomnieliśmy powyżej. Swoją drogą przegapiliśmy coś ważnego. Ma sens, że łańcuchy i prymitywy można łatwo serializować: Java prawdopodobnie ma jakiś wbudowany mechanizm, aby to zrobić. Ale co, jeśli nasza serializableklasa ma pola, które nie są prymitywami, ale raczej odniesieniami do innych obiektów? Na przykład utwórzmy osobne klasy TerritoriesInfoi do pracy z naszą klasą. 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 + '\'' +
               '}';
   }
}
I teraz pojawia się pytanie: czy wszystkie te klasy muszą być, Serializablejeśli chcemy serializować naszą zmienioną SavedGameklasę?

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 +
               '}';
   }
}
Cóż, przetestujmy to! Zostawmy wszystko tak, jak jest i spróbujmy serializować SavedGameobiekt:

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();
   }
}
Wynik:

Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo
To nie zadziałało! W zasadzie to jest odpowiedź na nasze pytanie. Gdy obiekt jest serializowany, serializowane są wszystkie obiekty, do których odwołują się jego zmienne instancji. A jeśli te obiekty odwołują się również do innych obiektów, to są również serializowane. I tak w nieskończoność. Wszystkie klasy w tym łańcuchu muszą byćSerializable , w przeciwnym razie ich serializacja będzie niemożliwa i zostanie zgłoszony wyjątek. Nawiasem mówiąc, może to powodować problemy na drodze. Co powinniśmy zrobić, jeśli na przykład nie potrzebujemy części klasy podczas serializacji? Lub na przykład, co by było, gdyby TerritoryInfoklasa trafiła do nas w ramach jakiejś biblioteki innej firmy. I załóżmy dalej, że tak nie jest Serializable, a zatem nie możemy tego zmienić. Okazuje się, że nie możemy dodać TerritoryInfopola do naszegoSavedGameclass, ponieważ uczynienie tego uniemożliwiłoby serializację całej SavedGameklasy! To jest problem :/ Jaka jest różnica między serializacją a deserializacją w Javie?  - 2W Javie tego rodzaju problemy rozwiązuje się za pomocą transientsłowa kluczowego. Jeśli dodasz to słowo kluczowe do pola swojej klasy, pole to nie zostanie serializowane. Spróbujmy uczynić jedno z SavedGamepól instancji klasy przejściowym. Następnie dokonamy serializacji i przywrócimy jeden obiekt.

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();


   }
}
A oto wynik:

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'}}
Dodatkowo otrzymaliśmy odpowiedź na nasze pytanie, jaka wartość jest przypisywana do transientpola. Zostaje mu przypisana wartość domyślna. Dla obiektów jest to null. Możesz przeczytać ten doskonały artykuł na temat serializacji, gdy masz kilka wolnych minut. Wspomina również o Externalizableinterfejsie, o którym porozmawiamy w następnej lekcji. Dodatkowo książka „Head-First Java” zawiera rozdział poświęcony temu zagadnieniu. Poświęć mu trochę uwagi :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION