Автор
Владимир Портянко
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 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();
   }
}
Результат: Exception in thread "main" java.io.NotSerializableException: DiplomacyInfo Не вышло! Собственно, вот и ответ на наш вопрос. При сериализации объекта сериализуются все объекты, на которые он ссылается в своих переменных экземпляра. И если те объекты тоже ссылаются на третьи объекты, они тоже сериализуются. И так до бесконечности. Все классы в этой цепочке должны быть Serializable, иначе их невозможно будет сериализовать и будет выброшено исключение. Это, кстати, в перспективе может создать проблемы. Что делать, например, если часть класса при сериализации нам не нужна? Или, к примеру, класс TerritoryInfo в нашей программе достался нам «по наследству» в составе какой-то библиотеки. При этом он не является Serializable, и мы, соответственно, не можем его менять. Получается, что и добавить поле TerritoryInfo в наш класс SavedGame мы не можем, ведь тогда весь класс SavedGame станет несериализуемым! Проблема :/ Сериализация и десериализация в Java - 2Проблемы такого рода решаются в 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. На досуге ты можешь прочитать вот эту отличную статью про сериализацию. В ней еще написано об интерфейсе Externalizable, о котором мы поговорим в следующей лекции. Кроме того, глава на эту тему есть в книге «Head-First Java», обрати на нее внимание :)
Комментарии (98)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Валерия
Уровень 48
Expert
22 марта, 16:23
Спасибо! Отличная лекция!)
Fakhrut Timurdinov
Уровень 32
9 февраля, 17:24
head first ужасная книга, никому не советую
Anonymous #3227998
Уровень 33
30 мая, 21:32
Книга отличная. Просто надо использовать не как справочник, а как источник ответов на "глупые вопросы новичков"... Рекомендовано к дополнительному чтению.
Павел К
Уровень 30
7 февраля, 11:08
1. Это и есть наша сохраНЁнная игра! 2. Почему расширение сохраненного файла ".ser"?
Владоs
Уровень 30
11 марта, 17:43
можно и .paprikolu
Павел К
Уровень 30
27 марта, 13:56
Тогда логичнее .patamu6ta 😁
Серега Батенин
Уровень 34
27 декабря 2022, 15:02
Недоработанная лекция! В самом конце они пометили ключевым словом только поле TerritoriesInfo , а Ресурсы и Дипломатия так и остались без маркировки. И как же у них тогда не выскочило исключение?
Gregory Parfer
Уровень 82
Expert
11 января, 17:34
Все доработано, прочитай внимательнее) Чтобы класс мог сериализоваться, он должен имплементировать маркерный интерфейс Serializable, ключевое же слово transient отвечает за то, будет ли это поле объекта пробовать сереализоваться, если поле объявлено с этим ключевым словом, то сериализоваться оно не будет (Если простыми словами, transient говорит JVM: если будешь сохранять этот объект, то вот это поле не надо). Ошибку не выбило потому что все классы имплементируют Serializable (об этом было сказано, но не показано)
Серега Батенин
Уровень 34
12 января, 13:14
Полностью с вами согласен. За исключением последнего момента. Все классы должны реализовывать маркерный интерфейс, но то что те поля его реализуют не было написано настолько явно. Даже сейчас перечитал еще раз. Написали что они должны по хорошему, но что они сделали их таковыми лично для меня не было очевидно
Арман Android Developer
11 июня 2022, 12:42
Блин ребята респект сразу видно что есть преподавательская жилка можете объяснить простыми словами сложные вещи ещё и с юмором это реально жесть я как вспомню как играл этот Червяк Джим который нельзя сохранить =)
Николай Зернов
Уровень 59
Expert
14 апреля 2022, 10:46
"... При сериализации объекта сериализуются все объекты, на которые он ссылается в своих переменных экземпляра. И если те объекты тоже ссылаются на третьи объекты, они тоже сериализуются. И так до бесконечности. Все классы в этой цепочке должны быть Serializable, иначе их невозможно будет сериализовать и будет выброшено исключение..." и далее приводится пример работы кода с использованием transient, но опускается момент того, что классы ResourcesInfo и DiplomacyInfo должны также реализовывать интерфейс Serializable. Это намеренно сделано для упрощения или ошибка?
Kurama
Уровень 50
24 октября 2022, 19:39
Да, должны. Просто говорили "что, если класс TerritoryInfo в нашей программе достался нам «по наследству» в составе какой-то библиотеки. При этом он не является Serializable, и мы, соответственно, не можем его менять", поэтому показали обновленный код мэйна для такой ситуации, то есть имелось в виду, что остальные классы нормальные (изменяемые нами с реализацией Serializable)
Anonymous #3025762
Уровень 28
4 апреля 2022, 17:53
А у меня работает!🥳
Мирослав
Уровень 28
30 марта 2022, 12:38
Классная статья! Поигрался с вариантами (Все понятно и работает)
Михаил Заика
Уровень 50
27 января 2022, 06:59
Не работает ссылка в конце лекции.
Anonymous #2372013
Уровень 26
21 января 2022, 08:47
Уважаемые админы! Обратите внимание, срок действия ссылки на статью об интерфейсе Externalizable истек. http://www.skipy.ru/technics/serialization.html