Преждевременная оптимизация является первопричиной всех бед в программировании. Дональд Кнут

Алгоритм сериализации объектов в Java

Сериализацией (Serialization) называют процесс сохранения состояния объекта в последовательность байт, в то время как десериализацией называют обратный процесс, формирующий объект из последовательности байт. Java Serialization API предоставляет разработчикам механизм, позволяющий производить сериализацию/десериализацию объектов. В этой статье вы узнаете, как сериализовать объект, и когда сериализация является необходимой. Вы так же узнаете, как работает алгоритм сериализации в Java на примере, иллюстрирующем формат сериализации объектов на низком уровне.

Итак, зачем нужна сериализация?

На сегодняшний день, традиционное приложение уровня предприятия состоит из разнообразных компонентов и распределено между различными системами и сетями. В Java всё представлено в качестве объектов (за исключением примитивных типов); для того, чтобы два объекта могли взаимодействовать между собой, должен существовать механизм, позволяющий обмениваться данными. Одним из путей достижения такого взаимодействия может служить разработка протокола обмена объектами. В этом случае получатель объекта должен работать по такому же протоколу, по которому работает отправитель объекта, что в свою очередь может привести к затруднениям при интеграции ваших компонентов со сторонними компонентами. Из этого следует, что протокол должен быть простым и обобщённым, что в свою очередь в Java достигается с помощью сериализации.

Рисунок 1 иллюстрирует взаимодействие “клиент/сервер”, в котором объект передаётся от клиента к серверу через сериализацию.

Рисунок 1. Сериализация в действии

Как сериализовать объект

Для того, чтобы объект был сериализуемым, класс этого объекта должен реализовывать интерфейс  java.io.Serializable. Листинг 1 демонстрирует пример реализации данного интерфейса.

Листинг 1. Реализация интерфейса java.io.Serializable

import java.io.Serializable;

class TestSerial implements Serializable {
    public byte version = 100;
    public byte count = 0;
}

Интерфейс java.io.Serializable не содержит методов и является маркером, который говорит механизму сериализации о том, что объект, реализующий данный интерфейс, может быть сериализован.

Теперь, когда у нас уже есть класс, реализующий интерфейс java.io.Serializable, следующим шагом станет написание алгоритма, ответственного за сериализацию экземпляра класса TestSerial, что приведено в Листинге 2.

Листинг 2. Реализация алгоритма сериализации

public static void main(String args[]) throws IOException {
    FileOutputStream fos = new FileOutputStream("temp.out");
    ObjectOutputStream oos = new ObjectOutputStream(fos);
    TestSerial ts = new TestSerial();
    oos.writeObject(ts);
    oos.flush();
    oos.close();
}

Код, приведённый в Листинге 2, сериализует состояние экземпляра класса TestSerial в файл temp.out. Для воссоздания объекта, нужно произвести десериализацию, как показано в Листинге 3.

Листинг 3. Воссоздание сериализованного объекта

public static void main(String args[]) throws IOException {
    FileInputStream fis = new FileInputStream("temp.out");
    ObjectInputStream oin = new ObjectInputStream(fis);
    TestSerial ts = (TestSerial) oin.readObject();
    System.out.println("version="+ts.version);
}

В Листинге 3, восстановление объекта осуществляется с помощью вызова oin.readObject() метода. Так как этот метод может считывать любой сериализуемый объект, приведение объекта к определённому классу является обязательным. При выполнении данного кода на экран будет выведено сообщение “version=100“.

Формат сериализации объекта

Прежде всего, давайте рассмотрим содержимое файла temp.out в шестнадцатиричном формате, приведённое в Листинге 4.

Листинг 4. Представление сериализуемого объекта в шестнадцатиричном формате

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78
70 00 64

Обратив внимание на класс TestSerial. Вы увидите, что он содержит только два байтовых поля, как показано в Листинге 5.

Листинг 5. Поля класса TestSerial

    public byte version = 100;
    public byte count = 0;

Размер каждого из полей состовляет один байт, таким образом, итоговый размер объекта (без заголовков) состовляет два байта. Но если мы посмотрим на размер сериализованного объекта, приведённого в Листинге 4, мы увидим не 2 байта, а 51 байт. Сюрприз! Откуда появились дополнительные байты и какое их предназначение? Дополнительные байты были добавлены алгоритмом сериализации и являются жизненно важными при десериализации.

Алгоритм сериализации в Java

Теперь, когда мы уже знаем, как сериализовать объект, перед нами возникает другой вопрос, каким образом сериализация работает на низком уровне ? Ответ на данный вопрос дают следующие пункты:

  • прежде всего происходит запись метаданных класса, ассоциированного с конкретным экземпляром данного класса
  • затем происходит рекурсиваня запись описания суперкласса до тех пор, пока не будет достигнут суперкласс типа java.lang.Object
  • после того, как метаданные были успешно записаны, начинается процесс записи данных экземпляра класса, причем процесс записи начинается с самого верхнего суперкласса
  • и наконец после того, как данные самого верхнего суперкласса были записаны, алгоритм сериализации рекурсивно проходит по иерархии наследования вниз к производному классу ( экземпляр которого сериализуется) и одновременно с этим проходом записывает данные, описание которых принадлежит итерируемому классу

Рассмотрим сериализацию объектов на другом примере, описывающем более широкую иерархию наследования классов. Смотрите Листинг 6.

Листинг 6. Другой пример сериализации объекта

class Parent implements Serializable {
    int parentVersion = 10;
}

public class Contain implements Serializable {
    int containVersion = 11;
}

public class TestSerial extends Parent implements Serializable {

	int version = 66;
	Contain con = new Contain();

	public int getVersion() {
		return version;
	}

	public static void main(String args[]) throws IOException {
		FileOutputStream fos = new FileOutputStream("c:\\temp.out");
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		TestSerial st = new TestSerial();
		oos.writeObject(st);
		oos.flush();
		oos.close();
	}
}

Этот пример довольно простой. Он сериализует экземпляр класса TestSerial, который расширяет Parent класс и содержит экземпляр класса Contain. Представление сериализованного объекта в шестандцатиричном формате приведено в Листинге 7.

Листинг 7. Представление сериализуемого объекта в шестнадцатиричном формате

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07
76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09
4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72
65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00
0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70
00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74
61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00
0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78
70 00 00 00 0B

На рисунке 2 изображена последовательность выполнения алгоритма сериализации.

Рисунок 2. Последовательность выполнения алгоритма сериализации

Давайте детально рассмотрим представление сериализуемого объекта, приведённое в Листинге 7. Начнем с информации, которая отвечает за идентификацию протокола сериализации:

  • AC ED: STREAM_MAGIC, определение протокола сериализации
  • 00 05: STREAM_VERSION, версия сериализации
  • 0×73: TC_OBJECT, определение нового объекта

Первым шагом алгоритма сериализации является запись метаданных класса сериализуемого экземпляра.

  • 0×72: TC_CLASSDESC, определение данного класса как нового класса
  • 00 0A: длинна имени класса.
  • 53 65 72 69 61 6c 54 65 73 74: имя класса TestSerial
  • 05 52 81 5A AC 66 02 F6: serialVersionUID класса
  • 0×02: этот флаг говорит о том, что объект поддерживает сериализацию
  • 00 02: количество полей сериализуемого класса

Далее, алгоритм записывает поле int version = 66;

  • 0×49: код типа поля. 49 представляет “I”, т.е. Int
  • 00 07: длинна имени поля
  • 76 65 72 73 69 6F 6E: имя поля version

После чего, алгоритм записывает следующее поле: Contain con = new Contain();.

  • 0×74: TC_STRING: представляет новую строку
  • 00 09: длинна строки
  • 4C 63 6F 6E 74 61 69 6E 3B: contain;, каноническая  JVM синатура
  • 0×78: TC_ENDBLOCKDATA, конец опционального блока данных для объекта

Следующим этапом алгоритма является запись определения Parent класса, который является суперклассом класса TestSerial.

  • 0×72: TC_CLASSDESC, определяет новый класс
  • 00 06: длинна имени класса
  • 70 61 72 65 6E 74: имя класса TestSerial
  • 0E DB D2 BD 85 EE 63 7A: serialVersionUID класса
  • 0×02: этот флаг говорит о том, что объект поддерживает сериализацию
  • 00 01: количество полей в классе

После того, как алгоритм записал информацию о Parent классе, выполнение переходит к записи информации, описывающей поля класса и их содержимое. В нашем случае класс содержит одно предварительно проинициализированное поле int parentVersion = 100;.

  • 0×49: код типа поля, 49 представляет “I”, т.е. Int
  • 00 0D: длинна имени поля
  • 70 61 72 65 6E 74 56 65 72 73 69 6F 6E: parentVersion, имя поля
  • 0×78: TC_ENDBLOCKDATA, конец опционального блока данных для объекта
  • 0×70: TC_NULL, этот байт говорит нам о том, что даный класс не имеет суперклассов и является корневым в иерархии наследования

Итак, на данном этапе алгоритм сериализации записал метаданные класса, ассоциируемого с экземпляром, а так же метаданные суперкласса класса сериализуемого экземпляра. Далее алгоритм записывает данные, ассоциируемые с экземпляром. Прежде всего это значиния суперклассов (в нашем случае значение поля parentVersion класса Parent), после чего данные класса сериализуемого экземпляра (в нашем случае данные полей класса TestSerial):

  • 00 00 00 0A: 10, значение поля parentVersion
  • 00 00 00 42: 66, значение поля version

Следующие байты вызывают особый интерес. Алгоритму необходимо записать информацию об экземпляре класса Contain, приведённом в Листинге 8.

Листинг 8. Экземпляр класса Contain

Contain con = new Contain();

Далее, алгоритм сериализации записывает метаданные класса Contain:

  • 0×73: TC_OBJECT, назначение нового объекта
  • 0×72: TC_CLASSDESC, определяет новый класс
  • 00 07: длинна имени класса
  • 63 6F 6E 74 61 69 6E: contain, имя поля класса
  • FC BB E6 0E FB CB 60 C7: serialVersionUID класса
  • 0×02: этот флаг говорит о том, что объект поддерживает сериализацию
  • 00 01: количество полей в классе

После чего очередь переходит к записи данных о поле containVersion:

  • 0×49: код типа поля, 49 представляет “I”, т.е. Int
  • 00 0E: длинна имени поля
  • 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: имя поля containVersion
  • 0×78: TC_ENDBLOCKDATA, конец опционального блока данных для объекта

После записи вышеперечисленных байт, алгоритм проверят класс Contain на наличие суперкласса. Если таков существует в иерархии наследования, алгоритм переходит к записи метаданных суперкласса. В нашем случае у класса Contain нет родителя, следовательно, алгоритм записывает TC_NULL:

  • 0×70: TC_NULL

В конечном итоге, алгоритм записывает данные полей класса Contain:

  • 00 00 00 0B: 11, значение поля containVersion

Настал момент вздохнуть с облегчением – все байты рассмотрены.

Заключение

В данной статье мы познакомились с механизмом сериализации объектов в Java, а так же рассмотрели работу алгоритма сериализации в деталях. Надеюсь, полученная информация в достаточной мере помогла Вам развеять все страхи при произнесении такого магического слова, как сериализация :-) .

Примечание автора перевода

За возможные ошибки в переводе прошу сильно не пинать. Спасибо за внимание :) .

Источник: http://www.javaworld.com/community/node/2915

Тэги: ,

4 Responses to “Алгоритм сериализации объектов в Java”

  1. Vald |

    Сенкс, полезная статья

  2. Эдик |

    Да, безусловно это переводной материал и т.п. Но запись претендует на обучающий и рассказывающий характер. И в этом свете было бы неплохо внести коррекции переводчика и написать, например, правильный вариант работы с потоками. Академический, скажем так. Иначе создается прецендент для обучающихся программистов. А они потом еще и тыкать будут, мол, смотрите тут же написано, что так можно :)

  3. Evgenij Nerush |

    2Эдик
    >>было бы неплохо внести коррекции переводчика и написать, например, правильный вариант работы с потоками.
    Было бы на много лучше описать правильный пример работы с потоками с следующей статье, рассматривающей тонкости использования сериализации. Автор возьмет это на заметку ;)

  4. artemv12.blogspot.com |

    yo, хорошая статья, но не рассматривает некот. интересные case-ы, например, когда Parent не impl. Serializable. И что делать с final полями.
    В реальной жизни не всё можно Serializ-овать используя ObjectOutputStream.defaultWriteObject().

Оставить сообщение