CodeGym /Blog Java /Random-PL /Serializacja i deserializacja w Javie
Autor
Volodymyr Portianko
Java Engineer at Playtika

Serializacja i deserializacja w Javie

Opublikowano w grupie Random-PL
Cześć! W dzisiejszej lekcji porozmawiamy o serializacji i deserializacji w Javie. Zaczniemy od prostego przykładu. Wyobraź sobie, że jesteś twórcą gier komputerowych. 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ź sobie to!Serializacja i deserializacja w Javie - 1Obawiam się, że dzisiejsza gra bez tych umiejętności byłaby skazana na porażkę! W każdym razie, co to znaczy „zapisać” i „wczytać” grę? Cóż, rozumiemy codzienne znaczenie: chcemy kontynuować grę od miejsca, w którym ją przerwaliśmy. Aby to zrobić, tworzymy pewien „punkt kontrolny”, którego używamy później do załadowania gry. Ale co to oznacza dla programisty, a nie dla zwykłego gracza? Odpowiedź jest prosta: zapisujemy stan naszego programu. Załóżmy, że grasz w Hiszpanię w grze strategicznej. Twoja gra ma stan: jakie terytoria każdy ma, ile zasobów każdy ma, jakie istnieją sojusze iz kim, kto jest w stanie wojny i tak dalej. Ta informacja, stan naszego programu, musi zostać jakoś zachowana, aby przywrócić dane i kontynuować grę. Zdarza się, Serializacja w Javie to proces zapisywania stanu obiektu jako sekwencji bajtów. Deserializacja w Javie to proces przywracania obiektu z tych bajtów. Każdy obiekt Java można przekonwertować na sekwencję bajtów. Dlaczego tego potrzebujemy? Wielokrotnie powtarzaliśmy, że programy nie istnieją same z siebie. Najczęściej wchodzą ze sobą w interakcje, wymieniają dane itp. Format bajtowy jest do tego wygodny i wydajny. Na przykład możemy przekonwertować obiekt naszej SavedGameclass na sekwencję bajtów, prześlij te bajty przez sieć do innego komputera, a następnie na innym komputerze przekonwertuj te bajty z powrotem na obiekt Java! Brzmi trudno, co? Wygląda na to, że trudno byłoby to wszystko urzeczywistnić: / Na szczęście tak nie jest! :) W Javie za proces serializacji odpowiada interfejs Serializable . Ten interfejs jest niezwykle prosty: nie musisz implementować ani jednej metody, aby z niego korzystać! Zobacz, jak prosta jest nasza klasa do zapisywania gier:

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) +
               '}';
   }
}
Trzy tablice są odpowiedzialne za informacje o terytoriach, zasobach i dyplomacji, a interfejs Serializable mówi maszynie Java: „ wszystko jest w porządku, jeśli obiekty tej klasy mogą być serializowane ”. Interfejs bez jednego interfejsu wygląda dziwnie :/ Dlaczego jest potrzebny? Odpowiedź na to pytanie znajduje się powyżej: jest ona potrzebna tylko do dostarczenia niezbędnych informacji do maszyny Java. W poprzedniej lekcji krótko 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ć. Oto Serializable — jeden taki interfejs. Oto kolejny ważny punkt: Dlaczego potrzebujemyprywatna statyczna końcowa długa zmienna serialVersionUID, którą zdefiniowaliśmy w klasie? To pole zawiera unikalny identyfikator wersji serializowanej klasy. Każda klasa, która implementuje interfejs Serializable , ma identyfikator wersji. Jest ona ustalana na podstawie zawartości klasy — pól i kolejności ich deklaracji oraz metod i kolejności ich deklaracji. A jeśli zmienimy typ pola i/lub liczbę pól w naszej klasie, identyfikator wersji zmieni się natychmiast. SerialVersionUID jest również zapisywany , gdy klasa jest serializowana. Kiedy próbujemy deserializować, czyli odtworzyć obiekt z sekwencji bajtów, wartość serialVersionUID jest porównywana z wartością serialVersionUIDklasy w naszym programie. Jeśli wartości się nie zgadzają, zostanie zgłoszony wyjątek java.io.InvalidClassException. Zobaczymy przykład tego poniżej. Aby uniknąć takich sytuacji, po prostu ręcznie ustawiamy identyfikator wersji dla naszej klasy. W naszym przypadku będzie to po prostu równe 1 (możesz użyć dowolnej innej liczby). Cóż, nadszedł czas, aby spróbować serializować nasz obiekt SavedGame 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[] 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();
   }
}
Jak widać, stworzyliśmy 2 strumienie: FileOutputStream i ObjectOutputStream . Pierwszy wie, jak zapisać dane do pliku, a drugi konwertuje obiekty na bajty. Widzieliście już podobne zagnieżdżone konstrukcje, na przykład new BufferedReader(new InputStreamReader(...)) , w poprzednich lekcjach, więc nie powinny cię one przestraszyć :) Tworząc ten łańcuch dwóch strumieni, wykonujemy oba zadania: konwertujemy obiekt SavedGame na ciąg bajtów i zapisujemy go do pliku za pomocą metody writeObject() . A tak przy okazji, nawet nie spojrzeliśmy na to, co mamy! Czas spojrzeć na plik! *Uwaga: nie jest konieczne wcześniejsze tworzenie pliku. Jeśli plik o podanej nazwie nie istnieje, zostanie utworzony automatycznie* A oto jego zawartość: ¬н sr SavedGame [dyplomatyczneInfot [Ljava/lang/String;[ resourceInfoq ~ [ terytoriumInfoq ~ xpur [Ljava.lang. String;¬ТVзй{G xp t pФранция РІРѕСЋРμС‚ СЃ Россией, Р˜СЃРїР°РЅРё СЏ заняла позицию РЅРμйтралит етаuq ~ t "РЈ Р˜СЃРїР°РЅРёРё 100 золотаt РЈ Р РѕСЃСЃРёРё 80 золо таt !РЈ Франции 90 Р·РѕР »РѕС‚Р°uq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 провинцийt %РЈ Р РѕСЃСЃРёРё 1 0 провинцийt &РЈ Франции 8 провинций Oh, oh :( Wygląda na to, że nasz program nie zadziałał : ( Właściwie zadziałał. Pamiętasz, że wysłaliśmy do pliku sekwencję bajtów, a nie tylko obiekt lub tekst? Cóż, o to właśnie chodzi z tą sekwencją bajtów wygląda :) 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.U nas wygląda to tak:

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{territoryInfo=[Hiszpania ma 6 prowincji, Rosja ma 10 prowincji, Francja ma 8 prowincji], resourceInfo=[Hiszpania ma 100 sztuk złota, Rosja ma 80 sztuk złota, Francja ma 90 sztuk złota],dyplomatycznyInfo=[Francja jest w stanie wojny z Rosją, Hiszpania zajęła neutralne stanowisko]} Znakomicie! 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 usuniemy identyfikator wersji z naszej klasy SavedGame . Nie będziemy przepisywać obu naszych zajęć. Ich kod będzie taki sam. Po prostu usuniemy private static final long serialVersionUID z klasy SavedGame . Oto nasz obiekt po serializacji: ¬н sr SavedGameі€MіuОm‰ [ dyplomacyInfot [Ljava/lang/String;[ resourceInfoq ~ [ terytoriumInfoq ~ 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: niezgodna klasa lokalna: strumień classdesc serialVersionUID = -196410440475012755, klasa lokalna serialVersionUID = -6675950253085108747 Swoją drogą przegapiliśmy coś ważnego. Oczywiście łańcuchy i prymitywy są łatwo serializowane: Java z pewnością ma do tego wbudowany mechanizm. Ale co, jeśli nasza klasa możliwa do serializacji ma pola, które nie są prymitywami, ale raczej odniesieniami do innych obiektów? Na przykład utwórzmy oddzielne klasy TerritoryInfo , ResourceInfo i DiplomacyInfo do pracy z naszą klasą 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 + '\'' +
               '}';
   }
}
A teraz stajemy przed pytaniem: czy wszystkie te klasy muszą być serializowalne , jeśli chcemy serializować naszą klasę 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 +
               '}';
   }
}
W porządku! Przetestujmy to! Na razie zostawimy wszystko bez zmian i spróbujemy serializować obiekt 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();
   }
}
Wynik: wyjątek w wątku „main” java.io.NotSerializableException: DiplomacyInfo Nie zadziałało! Oto 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. Na przykład, co powinniśmy zrobić, jeśli nie potrzebujemy części klasy podczas serializacji? A co by było, gdybyśmy otrzymali naszą klasę TerritoryInfo „poprzez dziedziczenie” jako część biblioteki? I załóżmy dalej, że niei w związku z tym nie możemy tego zmienić. Oznaczałoby to, że nie możemy dodać pola TerritoryInfo do naszej klasy SavedGame , ponieważ wtedy cała klasa SavedGame stałaby się niemożliwa do serializacji! To jest problem: / Serializacja i deserializacja w Javie - 2W Javie tego rodzaju problem rozwiązuje słowo kluczowe transient . Jeśli dodasz to słowo kluczowe do pola swojej klasy, pole to nie zostanie serializowane. Spróbujmy sprawić, by jedno z pól naszej klasy SavedGame stało się przejściowe , a następnie dokonamy serializacji i przywrócimy pojedynczy obiekt.

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


   }
}
Oto wynik: SavedGame{territoryInfo=null, resourceInfo=ResourceInfo{info='Hiszpania ma 100 sztuk złota, Rosja ma 80 sztuk złota, Francja ma 90 sztuk złota'},dyplomatyczneInfo=DiplomacyInfo{info='Francja jest w stanie wojny z Rosją, Hiszpania zajął stanowisko neutralne'}} To powiedziawszy, otrzymaliśmy odpowiedź na pytanie, jaka wartość zostanie przypisana do pola przejściowego . Jest przypisana wartość domyślna. Dla obiektów jest to null . Możesz przeczytać doskonały rozdział na ten temat w książce „Java Head-First”, zwróć na to uwagę :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION