Расскажите о сериализации объектов в JAVA.

Java задняя часть GitHub переводчик

Так называемая «сериализация объекта JAVA» относится к процессу записи всего содержимого, описываемого объектом JAVA, в двоичный файл в форме файлового ввода-вывода. Что касается сериализации, в основном задействованы два потока: ObjectInputStream и ObjectOutputStream.

Многие люди понимают «сериализацию» только в вызовах readObject и writeObject, но они не знают, почему JAVA может «восстановить» полный объект JAVA из двоичного файла, и они не знают, как именно объект хранится. в бинарнике.

В этой статье вы проанализируете двоичный файл и объедините правила протокола сериализации, чтобы увидеть, как выглядит объект JAVA в файле.Это может быть скучно, но определенно улучшит ваше понимание сериализации.

Древний способ сериализации

В предыдущей статье о байтовых потоках мы кратко упомянули поток декоратора DataInput/OutputStream, который позволяет нам записывать и читать из файлов с базовыми типами данных в качестве входных данных.

См. пример:

Определите тип людей:

image

Немного более сложная основная функция:

image

Видно, что этот древний метод сериализации на самом деле заключается в использовании потока DataInput/OutputStream для записи значений полей объекта в файл по одному для завершения так называемой «операции сериализации».

При восстановлении объекта он должен быть прочитан поле за полем в том порядке, в котором он был записан.Этот метод можно назвать очень античеловеческим.Если в классе сто полей, не должен ли он быть записан один сто раз вручную.

Этот метод нельзя считать реализацией "сериализации" в точном смысле, это псевдосериализация, и это всем известно.

Стандартная сериализация JAVA

Одной из причин, по которой объект необходимо сериализовать и хранить в каталоге на диске, является то, что некоторые объекты могут быть очень важными, но занимать много места и часто не используются какое-то время, поэтому размещение их в памяти, очевидно, является своего рода отходов, а выбрасывание, в свою очередь, приведет к дополнительным операциям по созданию этих объектов.

Поэтому компромиссное решение состоит в том, чтобы сначала сериализовать и сохранить эти объекты в файлы, а затем считывать их с диска, когда они используются, и это "сериализация".

Чтобы сериализовать объект, JAVA требует, чтобы класс наследовал интерфейс «java.io.Serializable», а сериализуемый интерфейс не определяет никаких методов, это «интерфейс-маркер».

Когда виртуальная машина выполняет инструкцию сериализации, она проверяет, наследует ли тип, соответствующий сериализуемому объекту, интерфейс Serializable, и если нет, то отказывает в выполнении инструкции сериализации и выдает исключение.

java.io.NotSerializableException

Общее использование сериализации выглядит следующим образом:

image

Выходной результат:

single
23

ObjectOutputStream в некотором смысле также является потоком декоратора, и все внутренние операции с потоком байтов зависят от экземпляра OutputStream, переданного при создании экземпляра.

Реализация этого класса очень сложная, он определяет множество внутренних классов, в то же время он также инкапсулирует наш DataOutputStream, поэтому в DataOutputStream также есть методы для записи базовых типов данных. Кроме того, он также предоставляет метод writeObject, который DataOutputStream не должен напрямую записывать на диск объект Java, наследующий интерфейс Serializable.

Конечно, ObjectInputStream наоборот, он используется для чтения и восстановления объекта Java с диска.

Метод writeObject принимает параметр Object и сериализует объект Java, представленный параметром, в файл на диске.Здесь будет записано много чего вместо простой записи значения поля в файл.Он имеет ссылочный формат.Так же как наш компилятор будет генерировать файлы байт-кода в определенном формате.

Следование тому же правилу облегчит восстановление, давайте взглянем на конкретное содержание этого правила.

Правила сериализованного хранения

В предыдущем разделе мы сериализовали экземпляр People в файл, теперь мы открываем двоичный файл.

image

Сериализованный объект должен храниться с таким количеством двоичных битов. Эти двоичные биты соответствуют правилам сериализации JAVA. Каждые несколько байтов используются для хранения всего. Давайте посмотрим.

1. Магический номер: находится в заголовке почти всех двоичных файлов и используется для определения типа файла текущего двоичного файла.Магический номер нашего файла сериализации объектовAC ED, занимающий два байта.

2. Номер версии протокола сериализации: указывает, какие правила сериализации использует JAVA для создания двоичных файлов, здесь00 05, могут быть и другие соглашения, обычно соглашение 5.

3. Один байт: следующий байт используется для описания текущего типа объекта,0x73Указывает, что это обычный объект Java, другие необязательные значения:

image

Обратите внимание, что строковые и массивные типы не группируются в класс обычных объектов Java, они имеют разные числовые флаги. Наши люди — это обычный объект Java, поэтому здесь он равен 0x73.

4. Байт: Этот байт указывает тип данных, к которому принадлежит текущий объект, будь то класс или ссылка Ссылка здесь отличается от указателя ссылки в Java. Если вы дважды сериализуете один и тот же объект, Java не будет перезаписывать файл, последний сохранится как ссылочный тип, об этом позже. Люди — это класс, поэтому значение здесь равно 0x72.

5. Длина полного имени класса:0x0017Эти два байта описывают длину полного имени текущего объекта, поэтому следующие 23 байта представляют собой полное имя текущего объекта.После преобразования значение, выраженное этими 23 байтами, будет следующим: TestSerializable.People.

Затем см.:

image

6. Версия серийного номера: следующие восемь байтов,3A -> B5Описывает сериализованный номер версии текущего объекта класса. Поскольку это значение явно не указано в определенном нами классе People, компилятор сгенерирует serialVersionUID с определенным алгоритмом на основе соответствующей информации класса People, который занимает восемь байтов.

7. Тип сериализации: байт, используемый для указания типа сериализации текущего объекта,0x02Это означает, что текущий объект сериализуем.

8. Количество полей: два байта, обозначающие количество полей, которые необходимо сериализовать в текущем объекте, вот мы,0x0002, соответствующие нашим полям имени и возраста.

Далее идет описание полей:

image

9. Тип поля: однобайтовый,0x4CСоответствующее значение ASCII — L, что означает, что тип текущего поля является общим типом класса.

10. Длина имени поля: два байта,0x0003Указывает, что следующие три байта представляют собой полное имя текущего поля, а 0x616765 соответствует возрасту персонажа.

11. Имя типа поля: три байта,0x740013, где 0x74 — флаг начала типа поля, то есть в каждом из трех байтов, описывающих имя типа поля, первый байт равен 0x74, а последние два байта описывают длину имени типа поля, 0x0013 соответствует 19. Таким образом, следующие 19 байтов представляют собой полное имя типа текущего поля. Вычислено здесь, это точно, Ljava/lang/Integer;.

Далее нужно описать имя нашего второго поля.Конкретный процесс аналогичен, поэтому я не буду повторять его здесь.Мы продолжим вводить его после имени поля.

image

12. Терминатор описания поля: один байт, фиксированное значение0x78Отмечает конец всех описаний информации о типах полей.

13. Описание родительского типа: один байт,0x70Представляет собой null, то есть нет родительского класса, а не класса Object.

Следующий абзац на самом деле представляет собой процесс сериализации в Java объекта Integer, а затем в 0x7872, то есть класс Integer имеет родительский класс, поэтому он сериализует экземпляр родительского класса Number. Зачем это делать, я думаю, вам должно быть понятно, создание каждого объекта подкласса будет соответствовать созданию объекта родительского класса.

Так что пока

image

Последний — 0x7870, указывающий на то, что вся информация об объекте была сериализована, а следующий — часть данных каждого поля.

Первые четыре байта, 0x00000017, — это значение возраста нашего первого поля, равное 23. 0x74 указывает, что тип второго поля — String, длина значения — 0x0006, а последние шесть байт — это именно строка single.

До сих пор был введен формат всего файла сериализации.Подведем итог:

Весь файл сериализации разделен на две части: описание типа поля и часть данных поля. Среди них, если тип поля является распространенным типом JAVA, он будет продолжать сериализовать объект своего родительского класса.Это очень важно понимать.В нашем примере сериализуются всего три объекта, а именно Люди, Целое число, Количество.Три объекта, если их поля были назначены извне, эти значения также сохраняются в этом порядке.

Некоторое углубленное понимание сериализации

Сериализация циклических ссылок

Рассмотрим эти два класса:

image

image

Определения этих двух классов почти одинаковы, с внутренним определением поля People.

image

Пусть два объекта ClassA и ClassB совместно используют один и тот же экземпляр People, тогда возникает вопрос, если я сериализую эти два объекта, будет ли этот общий объект People сериализован дважды?

Открываем бинарный файл, на этот раз бинарный файл немного сложнее:

image

Я обвел несколько 0x7870, что означает конец сериализации информации о типе объекта. Давайте кратко проанализируем это, и мы не будем вдаваться в подробности. Пожалуйста, обратитесь к вышеприведенному содержанию для деталей.

Первая часть фактически сериализует тип ClassA, что указывает на то, что тип ClassA имеет только одно поле, и это поле является типом объекта, записывает имя типа поля и другую информацию.

Вторая часть сериализует тип People, в том числе сериализует в нем поле name, и хранит назначенное извне значение поля name, string: single.

Третья часть, сериализация типа ClassB, сериализация типа ClassB немного меньше, чем у ClassA, хотя внутри они имеют одно и то же определение.

image

Среди них заштрихованная часть — это полное имя класса ClassB, а красная линия — это порядковый номер версии класса, который автоматически генерируется компилятором, поскольку мы не указали его явно. Затем укажите, что есть поле, тип поля — это тип объекта, а имя имеет длину шесть байтов.

0x71Указывает, что это поле является ссылкой.По соглашению, эта часть должна описывать имя типа поля, но поскольку этот тип был сериализован, ссылка используется для прямого указания на ранее сериализованный тип People.

Последняя часть по соглашению должна описывать данные поля, описывая тип данных, длину значения и само значение. Но поскольку значение поля people нашего типа ClassB совпадает со значением поля people класса ClassA, виртуальная машина не будет настолько глупой, чтобы повторно сериализовать объект people, а даст ссылочный номер объекта people выше.

Сказав так много, каков вывод?Если вы хотите сериализовать несколько объектов с одним и тем же типом класса, Java будет описывать тип только один раз, и если для одного и того же объекта существует файл сериализации Для нескольких сериализаций Java сохранит только один копия данных объекта, и все нижеперечисленное указано здесь по ссылке.

пользовательская сериализация

Для всех классов, наследующих интерфейс Serializable, при сериализации виртуальная машина сериализует все поля в этих классах, игнорируя модификаторы доступа, но иногда нам не нужно сериализовать все поля, а только опционально сериализовать некоторые из этих полей.

Нам просто нужно использовать ключевое слово transient перед полями, которые мы не хотим сериализовать.

private transient String name;

Даже если вы присвоите значение полю имени вашего объекта, оно не будет сохранено в файле в конце.При десериализации поле имени этого объекта по-прежнему имеет системное значение по умолчанию, равное нулю.

Кроме того, JAVA также позволяет нам переопределять writeObject или readObject для реализации нашей собственной логики сериализации.

Но объявления этих двух методов должны быть исправлены.

private void writeObject(java.io.ObjectOutputStream s) 

private void readObject(java.io.ObjectInputStream s) 

Правильно, он изменен приватно. Когда вы сериализуете объект через метод writeObject ObjectOutputStream, виртуальная машина автоматически определит, имеет ли класс, соответствующий объекту, реализацию двух вышеуказанных методов. Если да, она обратится к Call метод, который мы определяем в классе, и отказываемся от соответствующего метода, реализованного JDK.

Давайте посмотрим на пример:

image

Имя модифицируется ключевым словом transient, то есть механизм сериализации по умолчанию не сериализует это поле, и мы перезаписываем writeObject и readObject, после вызова метода сериализации по умолчанию мы записываем и читаем поле имени соответственно.

image

Выходной результат:

single
20

Заинтересованные студенты могут сами взглянуть на сериализованный двоичный файл.В поле имени нет информации описания, но после описания всего объекта «люди» следует наш символ «single».

Процесс десериализации аналогичен: сначала объект «люди» создается путем отражения в соответствии с механизмом десериализации JDK по умолчанию, а затем строка в конце файла считывается и присваивается текущему объекту «люди».

Проблема с сериализованной версией

Идентификатор сериализованной версии, мы всегда упоминали об этом, но мы никогда не объясняли, для чего используется этот идентификатор версии. Если вы используете его хорошо, вы можете использовать его для реализации механизма управления разрешениями.Если вы используете его неправильно, это может привести к сбою десериализации.

JAVA рекомендует, чтобы каждый класс, наследующий интерфейс Serializable, определял поле сериализованной версии.

private static final long serialVersionUID = xxxxL;

Это значение можно понимать как уникальный идентификатор текущего типа.Каждый объект будет записывать номер версии внешнего типа при сериализации.При десериализации он сначала проверит номер версии в бинарном файле и версию в целевом типе. Совпадут ли числа, если нет, то он откажется от десериализации.

Это значение не является обязательным. Если вы его не укажете, компилятор сгенерирует уникальный серийный номер по некоторому алгоритму на основе базовой информации о текущем классе. Однако, если ваш класс немного изменится, это значение изменится. файл не сможет быть десериализован, потому что вы не знаете, каким становится значение.

Поэтому JAVA предлагает всем нам самим определить такой номер версии, чтобы вы могли контролировать, можно ли успешно десериализовать сериализованный объект.

До сих пор мы кратко представили связанное с сериализацией содержание, многие из которых описаны в сочетании с двоичными файлами, что может показаться скучным, но после прочтения это должно улучшить ваше первоначальное понимание сериализации объектов JAVA. Если у вас есть какие-либо вопросы, вы можете оставить сообщение для обсуждения и обмена!


Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:

(https://github.com/SingleYam/overview_java)

Добро пожаловать в официальную учетную запись WeChat: OneJavaCoder, все статьи будут синхронизированы в официальной учетной записи.

image