Привет! В сегодняшней лекции мы поговорим о сериализации и десериализации в Java. Начнем с простого примера. Допустим, ты создатель компьютерной игры. Если ты рос в 90-е и помнишь игровые приставки тех времен, наверняка знаешь, что в них отсутствовала очевидная сегодня вещь — сохранение и загрузка игры :) Если нет… представь себе! Сериализация и десериализация в Java - 1 Боюсь, сегодня игра без такой возможности будет обречена на провал! А, собственно, что такое «сохранение» и «загрузка» игры? Ну, в обычном смысле мы понимаем, что это: мы хотим продолжить игру с того места, где закончили в прошлый раз. Для этого мы создаем некую «контрольную точку», которую потом используем для загрузки игры. Но что это значит не в житейском, а в «программистском» смысле? Ответ прост: мы сохраняем состояние нашей программы. Допустим, ты играешь в стратегию за Испанию. У твоей игры есть состояние: кто какими территориями владеет, у кого сколько ресурсов, кто с кем в союзе, а кто наоборот — в состоянии войны, и так далее. Эту информацию, состояние нашей программы, необходимо как-то сохранить, чтобы в дальнейшем восстановить данные и продолжить игру. Для этого как раз и используются механизмы сериализации и десереализации. Сериализация — это процесс сохранения состояния объекта в последовательность байт. Десериализация — это процесс восстановления объекта из этих байт. Любой 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», обрати на нее внимание :)