JavaRush /Blog Java /Random-PL /Serializacja i deserializacja w Javie

Serializacja i deserializacja w Javie

Opublikowano w grupie Random-PL
Cześć! Na dzisiejszym wykładzie porozmawiamy o serializacji i deserializacji w Javie. Zacznijmy od prostego przykładu. Załóżmy, że jesteś twórcą gry komputerowej. Jeśli dorastałeś w latach 90-tych i pamiętasz konsole do gier z tamtych czasów, prawdopodobnie wiesz, że dzisiaj brakowało im czegoś oczywistego - zapisywania i ładowania gier :) Jeśli nie... wyobraź sobie! Obawiam się, że dzisiaj gra bez takiej możliwości będzie skazana na porażkę! A właściwie, czym jest „zapisywanie” i „ładowanie” gry? Cóż, w typowym sensie rozumiemy, o co chodzi: chcemy kontynuować grę od miejsca, w którym zakończyliśmy ostatnim razem. W tym celu tworzymy swego rodzaju „punkt kontrolny”, za pomocą którego następnie ładujemy grę. Ale co to oznacza, nie w sensie codziennym, ale w sensie „programisty”? Odpowiedź jest prosta: zapisujemy stan naszego programu. Załóżmy, że grasz w grę strategiczną dotyczącą Hiszpanii. Twoja gra ma stan: kto jest właścicielem jakich terytoriów, kto ile zasobów, kto z kim jest w sojuszu i kto, przeciwnie, jest w stanie wojny i tak dalej. Ta informacja, stan naszego programu, musi zostać w jakiś sposób zapisana, aby później móc przywrócić dane i kontynuować grę. Właśnie do tego służą mechanizmy serializacji i deserializacji . Serializacja to proces przechowywania stanu obiektu w sekwencji bajtów. Deserializacja to proces rekonstrukcji obiektu z tych bajtów. Dowolny obiekt Java jest konwertowany na sekwencję bajtów. Po co to jest? Niejednokrotnie mówiliśmy, że programy nie istnieją same w sobie. Najczęściej wchodzą ze sobą w interakcję, wymieniają dane itp. A format bajtu jest do tego wygodny i wydajny. Możemy na przykład zamienić nasz obiekt klasy SavedGame(zapisaną grę) w ciąg bajtów, przesłać te bajty przez sieć do innego komputera, a na drugim komputerze zamienić te bajty z powrotem w obiekt Java! Trudno to usłyszeć, prawda? Najwyraźniej zorganizowanie tego procesu nie będzie łatwe :/ Na szczęście nie! :) W Javie za procesy serializacji odpowiedzialny jest interfejs Serializable . Ten interfejs jest niezwykle prosty: nie musisz wdrażać żadnej metody, aby z niego korzystać! Tak będzie wyglądać nasza klasa zapisu stanu 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 zbiory danych odpowiadają za informacje o terytoriach, ekonomii i dyplomacji, a interfejs Serializable informuje maszynę Java: „ wszystko jest w porządku, jeśli w ogóle, obiekty tej klasy można serializować ”. Interfejs, który nie ma żadnych metod, wygląda dziwnie :/ Po co jest potrzebny? Odpowiedź na to pytanie znajduje się powyżej: tylko w celu dostarczenia niezbędnych informacji maszynie Java. W jednym z poprzednich wykładów krótko wspominaliśmy o interfejsach znaczników. Są to specjalne interfejsy informacyjne, które po prostu oznaczają nasze zajęcia dodatkowymi informacjami, które przydadzą się maszynie Java w przyszłości. Nie mają żadnych metod, które należy wdrożyć. Zatem Serializable jest jednym z takich interfejsów. Kolejna ważna kwestia: zmienna, którą private static final long serialVersionUIDzdefiniowaliśmy w klasie. Dlaczego jest to potrzebne? To pole zawiera unikalny identyfikator wersji serializowanej klasy . Każda klasa implementująca interfejs Serializable ma identyfikator wersji. Oblicza się go na podstawie zawartości klasy - pól, kolejności deklaracji, metod. A jeśli zmienimy typ pola i/lub liczbę pól w naszej klasie, identyfikator wersji natychmiast się zmieni. serialVersionUID jest również zapisywany podczas serializacji klasy. Kiedy próbujemy deserializować, czyli przywrócić obiekt ze zbioru bajtów, wartość serialVersionUIDjest porównywana z wartością serialVersionUIDklasy w naszym programie. Jeśli wartości nie będą zgodne, zostanie zgłoszony wyjątek java.io.InvalidClassException. Poniżej zobaczymy przykład tego. Aby uniknąć takich sytuacji, po prostu ręcznie ustawiamy ten identyfikator wersji dla naszej klasy. W naszym przypadku będzie to po prostu 1 (możesz zastąpić dowolną inną liczbę). Cóż, czas spróbować serializować nasz obiekt SavedGamei 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 {

       // tworzymy nasz obiekt
       String[] territoryInfo = {„Hiszpania ma 6 prowincji”, „Rosja ma 10 prowincji”, „Francja ma 8 prowincji”};
       String[] resourcesInfo = {„Hiszpania ma 100 sztuk złota”, „Rosja ma 80 sztuk złota”, „Francja ma 90 sztuk złota”};
       String[] diplomacyInfo = {„Francja jest w stanie wojny z Rosją, Hiszpania zajęła stanowisko neutralne”};

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

       //utwórz 2 wątki, aby serializować obiekt i zapisać go w pliku
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // zapisz grę do pliku
       objectOutputStream.writeObject(savedGame);

       // zamknij strumień i zwolnij zasoby
       objectOutputStream.close();
   }
}
Jak widać utworzyliśmy 2 wątki - FileOutputStreami ObjectOutputStream. Pierwszy z nich może zapisywać dane do pliku, a drugi konwertować obiekty na bajty. Podobne „zagnieżdżone” konstrukcje widziałeś już np. new BufferedReader(new InputStreamReader(...))na poprzednich wykładach, więc nie powinny Cię przestraszyć :) Tworząc taki „łańcuch” dwóch wątków, wykonujemy obydwa zadania: zamieniamy obiekt SavedGamena zbiór bajtów i zapisz go w pliku za pomocą metody writeObject(). A swoją drogą, nawet nie sprawdziliśmy, co mamy! Czas zajrzeć do pliku! *Uwaga: plik nie musi być tworzony wcześniej. Jeśli plik o tej nazwie nie istnieje, zostanie utworzony automatycznie* A oto jego zawartość! ¬н sr SavedGame [ dyplomacjaInfot [Ljava/lang/String;[ zasobyInfoq ~ [ terytoriaInfoq ~ 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 проввинций Ups :( Wygląda na to, że nasz program nie zadziałał :( Właściwie zadziałało. Pamiętacie, że przenieśliśmy do pliku dokładnie zestaw bajtów , i nie tylko obiekt lub tekst? Cóż, tak wygląda ten zestaw :) To jest nasza zapisana gra! Jeśli chcemy przywrócić nasz oryginalny obiekt, czyli załadować i kontynuować grę od miejsca, w którym ją przerwaliśmy, potrzebujemy proces odwrotny, deserializacja ... Tak to będzie wyglądać w naszym przypadku:
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=[Hiszpania ma 6 prowincji, Rosja ma 10 prowincji, Francja ma 8 prowincji], ResourcesInfo=[Hiszpania ma 100 sztuk złota, Rosja ma 80 sztuk złota, Francja ma 90 sztuk złota], dyplomacjaInfo=[Francja jest w stanie wojny z Rosją, Hiszpania zajęła stanowisko neutralności]} Świetnie! Udało nam się najpierw zapisać stan naszej gry do pliku, a następnie przywrócić go z pliku. Teraz spróbujmy zrobić to samo, ale usuńmy SavedGameidentyfikator wersji z naszej klasy. Nie będziemy przepisywać obu naszych klas, kod w nich będzie taki sam, po prostu SavedGameusuniemy private static final long serialVersionUID. Oto nasz obiekt po serializacji: ¬н sr SavedGameі€MіuОm‰ [ dyplomacjaInfot [Ljava/lang/String;[ ResourcesInfoq ~ [ terytoriaInfoq ~ xpur [Ljava.lang.String;ТВзй{G xp t pФранция РІРѕС ЋРµС ‚ СЃ Россией, Р˜СЃРїР°РЅРЏР·Р°РЅСЏР»Р° RїРѕР·РёС†РёСЋ нейт ралитетаuq ~ t "РЈ Р˜СЃРї ании 100 Р · олотаt РЈ Р РѕСЃСЃРёРё 80 золотаt !РЈ Франции 90 золотаuq ~ t &РЈ Р˜СЃРїР°РЅРёРё 6 R їСЂРѕРІРёРЅС† РёР№t %РЈ Р РѕСЃСЃРёРё 10 провнинцийt &РЈ Р ¤СЂР°РЅС†РёРё 8 проввинцинА A podczas próby deserializacji stało się coś takiego: InvalidClassException: klasa lokalna niekompatybilna: strumień classdesc serialVersionUID = - 196410440475012755, klasa lokalna serialVersionUID = -6675950253085108747 To ten sam wyjątek, o którym wspomniano powyżej. Więcej na ten temat możesz przeczytać w artykule jednego z naszych uczniów. Swoją drogą przeoczyliśmy jeden ważny punkt. Jasne jest, że stringi i prymitywy można łatwo serializować: Java z pewnością je ma, w takim razie są do tego wbudowane mechanizmy. Ale co, jeśli nasza serializableklasa - ma pola, które są wyrażane nie jako elementy pierwotne, ale jako odniesienia do innych obiektów? Stwórzmy na przykład osobne klasy TerritoriesInfodo 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 + '\'' +
               '}';
   }
}
Ale teraz mamy pytanie: czy wszystkie te klasy powinny być serializowane, jeśli chcemy serializować zmienioną klasę 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 +
               '}';
   }
}
Cóż, sprawdźmy to w praktyce! Zostawmy wszystko tak jak jest i spróbujmy 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 {

       // tworzymy nasz obiekt
       TerritoriesInfo territoriesInfo = new TerritoriesInfo(„Hiszpania ma 6 prowincji, Rosja ma 10 prowincji, Francja ma 8 prowincji”);
       ResourcesInfo resourcesInfo = new ResourcesInfo(„Hiszpania ma 100 sztuk złota, Rosja ma 80 sztuk złota, Francja ma 90 sztuk złota”);
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo(„Francja jest w stanie wojny z Rosją, Hiszpania zajęła stanowisko neutralne”);


       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: wyjątek w wątku „main” java.io.NotSerializableException: DiplomacyInfo nie powiodło się! Właściwie, oto odpowiedź na nasze pytanie. Kiedy serializujesz obiekt, wszystkie obiekty, do których odwołuje się on w swoich zmiennych instancji, są serializowane. A jeśli te obiekty odwołują się również do obiektów trzecich, są również serializowane. I tak w nieskończoność. Wszystkie klasy w tym łańcuchu muszą umożliwiać serializację, w przeciwnym razie nie będzie można ich serializować i zostanie zgłoszony wyjątek. Nawiasem mówiąc, może to powodować problemy w przyszłości. Co powinniśmy zrobić np. jeśli nie potrzebujemy części klasy podczas serializacji? Lub na przykład TerritoryInfoodziedziczyliśmy klasę w naszym programie jako część jakiejś biblioteki. Nie można go jednak serializować i dlatego nie możemy go zmienić. Okazuje się, że nie możemy dodać pola TerritoryInfodo naszej klasy , bo wtedy całej klasy nie będzie można serializować! Problem:/ Problemy tego rodzaju rozwiązuje się w Javie za pomocą słowa kluczowego . Jeśli dodasz to słowo kluczowe do pola klasy, wartość tego pola nie zostanie serializowana. Spróbujmy utworzyć jedno z pól naszej klasy , po czym dokonamy serializacji i przywrócimy jeden obiekt. SavedGameSavedGameSerializacja i deserializacja w Javie - 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;
   }

   //... pobierające, ustawiające, toString()...
}



import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Main {

   public static void main(String[] args) throws IOException {

       // tworzymy nasz obiekt
       TerritoriesInfo territoriesInfo = new TerritoriesInfo(„Hiszpania ma 6 prowincji, Rosja ma 10 prowincji, Francja ma 8 prowincji”);
       ResourcesInfo resourcesInfo = new ResourcesInfo(„Hiszpania ma 100 sztuk złota, Rosja ma 80 sztuk złota, Francja ma 90 sztuk złota”);
       DiplomacyInfo diplomacyInfo =  new DiplomacyInfo(„Francja jest w stanie wojny z Rosją, Hiszpania zajęła stanowisko neutralne”);


       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='Hiszpania ma 100 sztuk złota, Rosja 80 sztuk złota, Francja ma 90 sztuk złota'}, dyplomacjaInfo=DiplomacyInfo{info='Francja jest w stanie wojny z Rosją, Hiszpania zajęła stanowisko neutralne'}} Jednocześnie otrzymaliśmy odpowiedź na pytanie, jaka wartość zostanie przypisana transientpolu -. Przypisuje się mu wartość domyślną. W przypadku obiektów tak jest null. W wolnej chwili możesz przeczytać ten znakomity artykuł na temat serializacji . Mówi także o interfejsie Externalizable, o którym porozmawiamy w następnym wykładzie. Dodatkowo w książce „Head-First Java” jest rozdział na ten temat, zwróćcie na to uwagę :)
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION