JavaRush /Java блог /Java Developer /Серіалізація та десеріалізація в Java
Автор
Владимир Портянко
Java-разработчик в Playtika

Серіалізація та десеріалізація в Java

Стаття з групи Java Developer
Привіт! У сьогоднішній лекції ми поговоримо про серіалізацію та десеріалізацію в Java. Почнемо з простого прикладу. Припустимо, ти творець комп'ютерної гри. Якщо ти ріс у 90-ті й пам'ятаєш ігрові приставки тих часів, напевно знаєш, що в них була відсутня очевидна сьогодні річ – збереження та завантаження гри :) Якщо ні... уяви собі! Боюся, сьогодні гра без такої можливості буде приречена на провал! А, власне, що таке "збереження" і "завантаження" гри? Ну, у звичайному сенсі ми розуміємо, що це: ми хочемо продовжити гру з того місця, де закінчили минулого разу. Для цього ми створюємо якусь "контрольну точку", яку потім використовуємо для завантаження гри. Але що це означає не в життєвому, а в "програмістському" сенсі? Відповідь проста: ми зберігаємо стан нашої програми. Припустимо, ти граєш у стратегію за Іспанію. У твоєї гри є стан: хто якими територіями володіє, у кого скільки ресурсів, хто з ким у союзі, а хто навпаки – у стані війни, і так далі. Цю інформацію, стан нашої програми, необхідно якось зберегти, щоб надалі відновити дані та продовжити гру. Для цього якраз і використовуються механізми серіалізації та десереалізації. Серіалізація – це процес збереження стану об'єкта в послідовність байт. Десеріалізація – це процес відновлення об'єкта з цих байт. Будь-який Java-об'єкт перетворюється на послідовність байт. Для чого це потрібно? Ми вже не раз казали, що програми не існують самі собою. Найчастіше вони взаємодіють одна з одною, обмінюються даними тощо. І байтовий формат для цього є зручним і ефективним. Ми можемо, наприклад, перетворити наш об'єкт класу SavedGame (збережена гра) на послідовність байт, передати ці байти мережею на інший комп'ютер, і на другому комп'ютері перетворити ці байти знову на Java-об'єкт! На слух сприймається складно, так? Судячи з усього, й організувати цей процес буде непросто :/ На щастя, ні! :) У Java за процеси серіалізації відповідає інтерфейс Serializable. Цей інтерфейс украй простий: щоб ним користуватися, не потрібно реалізовувати жодного методу! Ось так просто матиме вигляд наш клас збереження гри:

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) +
               '}';
   }
}
Три масиви даних відповідають за інформацію про території, економіку та дипломатію, а інтерфейс Serializable каже Java-машині: «все ок, якщо що, об'єкти цього класу можна серіалізувати». Інтерфейс, у якого немає жодного методу, виглядає дивно :/ Навіщо він потрібен? Відповідь на це запитання є вище: тільки щоб надати потрібну інформацію Java-машині. В одній із минулих лекцій ми мигцем згадували інтерфейси-маркери. Це спеціальні інформативні інтерфейси, які просто позначають наші класи додатковою інформацією, у майбутньому корисною для Java-машини. Жодних методів, які потрібно було б імплементувати, у них немає. Так ось, Serializable – один із таких інтерфейсів. Ще один важливий момент: змінна private static final long serialVersionUID, яку ми визначили в класі. Навіщо вона потрібна? Це поле містить унікальний ідентифікатор версії серіалізованого класу. Ідентифікатор версії є у будь-якого класу, який імплементує інтерфейс Serializable. Він обчислюється за вмістом класу – полями, порядком оголошення, методами. І якщо ми змінимо в нашому класі тип поля і/або кількість полів, ідентифікатор версії моментально зміниться. serialVersionUID теж записується під час серіалізації класу. Коли ми намагаємося здійснити десеріалізацію, тобто відновити об'єкт із набору байт, значення serialVersionUID порівнюється зі значенням serialVersionUID класу в нашій програмі. Якщо значення не збігаються, буде викинуто виняток java.io.InvalidClassException. Ми побачимо приклад цього нижче. Щоб уникнути таких ситуацій, ми просто вручну задаємо для нашого класу цей ідентифікатор версії. У нашому випадку він дорівнюватиме просто 1 (можеш підставити будь-яке інше вподобане число). Ну, саме час спробувати серіалізувати наш об'єкт SavedGame і подивитися, що вийде!

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

public class Main {

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

       //створюємо наш об'єкт
       String[] territoryInfo = {"В Іспанії 6 провінцій", "У Португалії 10 провінцій", "У Франції 8 провінцій"};
       String[] resourcesInfo = {"У Іспанії 100 золота", "У Португалії 80 золота", "У Франції 90 золота"};
       String[] diplomacyInfo = {"Франція воює з Португалією, Іспанія зайняла позицію нейтралітету"};

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

       //створюємо 2 потоки для серіалізації об'єкта і збереження його у файл
       FileOutputStream outputStream = new FileOutputStream("C:\\Users\\Username\\Desktop\\save.ser");
       ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

       // зберігаємо гру у файл
       objectOutputStream.writeObject(savedGame);

       //закриваємо потік і звільняємо ресурси
       objectOutputStream.close();
   }
}
Як бачиш, ми створили 2 потоки – FileOutputStream і ObjectOutputStream. Перший із них вміє записувати дані у файл, а другий - перетворює об'єкти в байти. Ти вже бачив подібні «вкладені» конструкції, наприклад, new BufferedReader(new InputStreamReader(...)), у минулих лекціях, тож вони не повинні тебе лякати :) Створивши такий «ланцюжок» із двох потоків, ми виконуємо обидва завдання: перетворюємо об'єкт SavedGame на набір байт і зберігаємо його у файл за допомогою методу writeObject(). А, до речі, ми ж навіть не перевірили, що в нас вийшло! Саме час зазирнути у файл! *Примітка: файл необов'язково створювати заздалегідь. Якщо файлу з такою назвою не існує, його буде створено автоматично* А ось і його вміст!
¬н 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 провинций
Ой-ой :( Здається, не спрацювала наша програма :( Насправді, спрацювала. Ти ж пам'ятаєш, що ми передавали у файл саме набір байт, а не просто об'єкт або текст? Ну, ось так цей набір і виглядає :) Це і є наша збережена гра! Якщо ж ми хочемо відновити наш вихідний об'єкт, тобто, завантажитися і продовжити гру з того місця, де зупинилися, нам потрібен зворотний процес, десеріалізація. Ось який вигляд вона матиме в нашому випадку:

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);
   }
}
А ось і результат!
SavedGame{territoriesInfo=[В Іспанії 6 провінцій, У Португалії 10 провінцій, У Франції 8 провінцій], resourcesInfo=[В Іспанії 100 золота, У Португалії 80 золота, У Франції 90 золота], diplomacyInfo=[Франція воює з Португалією, Іспанія зайняла позицію нейтралітету]}
Чудово! У нас вийшло спочатку зберегти стан нашої гри у файл, а потім відновити її з файлу. А тепер давай спробуємо зробити те саме, але приберемо з нашого класу SavedGame ідентифікатор версії. Не будемо переписувати обидва наші класи, код у них буде тим самим, просто з класу SavedGame приберемо private static final long serialVersionUID. Ось наш об'єкт після серіалізації:
¬н 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 провинций
А при спробі його десеріалізувати сталося ось що:
InvalidClassException: local class incompatible: stream classdesc classdesc serialVersionUID = -196410440475012755, local class serialVersionUID = -6675950253085108747
Це той самий виняток, про який ішлося вище. До речі, ми упустили один важливий момент. Зрозуміло, що рядки і примітиви серіалізуються легко: напевно в Java є якісь вбудовані механізми для цього. Але що, якщо в нашому serializable-класі є поля, виражені не примітивами, а посиланнями на інші об'єкти? Давай, наприклад, створимо окремі класи TerritoriesInfo, 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 + '\'' +
               '}';
   }
}
А ось тепер перед нами постало питання: а чи повинні всі ці класи бути Serializable, якщо ми хочемо серіалізувати клас 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 +
               '}';
   }
}
Що ж, давай перевіримо це на практиці! Залишимо поки що все як є і спробуємо серіалізувати об'єкт SavedGame:

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

public class Main {

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

       //створюємо наш об'єкт
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("У Іспанії 6 провінцій, у Португалії 10 провінцій, у Франції 8 провінцій");
       ResourcesInfo resourcesInfo = new ResourcesInfo("У Іспанії 100 золота, у Португалії 80 золота, у Франції 90 золота");
       DiplomacyInfo diplomacyInfo = new DiplomacyInfo("Франція воює з Португалією, Іспанія зайняла позицію нейтралітету");


       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();
   }
}
Результат:
Виключення в потоці "main" java.io.NotSerializableException: DiplomacyInfo
Не вийшло! Власне, ось і відповідь на наше запитання. Під час серіалізації об'єкта серіалізуються всі об'єкти, на які він посилається у своїх змінних екземпляра. І якщо ті об'єкти теж посилаються на треті об'єкти, вони теж серіалізуються. І так до нескінченності. Усі класи в цьому ланцюжку мають бути Serializable, інакше їх неможливо буде серіалізувати і буде викинуто виняток. Це, до речі, у перспективі може створити проблеми. Що робити, наприклад, якщо частина класу під час серіалізації нам не потрібна? Або, наприклад, клас TerritoryInfo в нашій програмі дістався нам «у спадок» у складі якоїсь бібліотеки. Водночас він не є Serializable, і ми, відповідно, не можемо його змінювати. Виходить, що і додати поле TerritoryInfo в наш клас SavedGame ми не можемо, адже тоді весь клас SavedGame стане несеріалізованим! Проблема :/ Проблеми такого роду розв'язуються в Java за допомогою ключового слова transient. Якщо додати до поля класу це ключове слово – значення цього поля не буде серіалізовано. Давай спробуємо зробити одне з полів нашого класу SavedGame 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;
   }

   //...геттери, сеттери, toString()...
}



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

public class Main {

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

       //створюємо наш об'єкт
       TerritoriesInfo territoriesInfo = new TerritoriesInfo("У Іспанії 6 провінцій, у Португалії 10 провінцій, у Франції 8 провінцій");
       ResourcesInfo resourcesInfo = new ResourcesInfo("У Іспанії 100 золота, у Португалії 80 золота, у Франції 90 золота");
       DiplomacyInfo diplomacyInfo = new DiplomacyInfo("Франція воює з Португалією, Іспанія зайняла позицію нейтралітету");


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


   }
}
А ось і результат:
SavedGame{territoriesInfo=null, resourcesInfo=ResourcesInfo{info='В Іспанії 100 золота, у Португалії 80 золота, у Франції 90 золота'}, diplomacyInfo=DiplomacyInfo{info='Франція воює з Португалією, Іспанія зайняла позицію нейтралітету'}}
Заразом ми отримали відповідь на питання, яке ж значення буде присвоєно transient-полю. Йому присвоюється значення за замовчуванням. У випадку з об'єктами це null.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ