Десериализация — это процесс, посредством которого ранее сериализованный объект восстанавливается обратно в свою первоначальную форму, т. е. экземпляр объекта. Входными данными для процесса десериализации является поток байтов, который мы получаем с другого конца сети ИЛИ мы просто считываем его из файловой системы/базы данных. Сразу возникает вопрос: что записано внутри этого потока байтов?
Подробнее: Руководство по реализации сериализуемого интерфейса
1. Потоковое содержимое
Если быть совсем точным, этот поток байтов(или, скажем, сериализованных данных) содержит всю информацию об экземпляре, которая была сериализована процессом сериализации. Эта информация включает метаданные класса, информацию о типе полей экземпляра и значения полей экземпляра. Эта же информация необходима, когда объект реконструируется в новый экземпляр объекта.
При десериализации объекта JVM считывает метаданные его класса из потока байтов, который определяет, реализует ли класс объекта интерфейс Serializable или Externalizable.
Обратите внимание, что для того, чтобы десериализация прошла гладко, байт-код класса, объект которого десериализуется, должен присутствовать в JVM, выполняющей десериализацию. В противном случае выбрасывается 'ClassNotFoundException'. Разве это не слишком очевидно?
2. Инициализация без конструктора
Если экземпляр реализует интерфейс Serializable, то экземпляр класса создается без вызова его конструктора. Серьезно? Тогда как создается объект, если конструктор не вызывается?
Давайте посмотрим на байт-код простой программы:
открытый класс SimpleProgram{public static void main(String[] args){System.out.println("Привет, мир!");}}
Сгенерированный байт-код:
public class SimpleProgram extends java.lang.Object{public SimpleProgram();Code:0: aload_01: invokespecial #1; //Method java/lang/Object."":()V4: returnpublic static void main(java.lang.String[]);Code:0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3; //String Hello World!5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return}
В нашей первой строке мы запишем значение из «локальной таблицы переменных» в стековую память. В этом случае мы на самом деле запихиваем только неявную ссылку на «это», так что это не самая захватывающая инструкция.
Вторая инструкция — это главное. Она фактически вызывает конструктор класса super most, а в приведенном выше случае это класс Object. И как только конструктор класса super most(т.е. Object в данном случае) был вызван, остальная часть кода выполняет конкретные инструкции, написанные в коде.
В соответствии с приведенной выше концепцией, т.е. конструктором самого суперкласса, мы имеем похожую концепцию десериализации.
В процессе десериализации требуется, чтобы все родительские классы экземпляра были сериализуемыми; и если какой-либо суперкласс в иерархии не сериализуем, то он должен иметь конструктор по умолчанию. Теперь это имеет смысл. Таким образом, при десериализации сначала ищется суперкласс, пока не будет найден какой-либо несериализуемый класс.
Если все суперклассы сериализуемы, то JVM в конечном итоге достигает самого класса Object и сначала создает экземпляр класса Object. Если в промежутке между поиском суперклассов какой-либо класс будет найден несериализуемым, то его конструктор по умолчанию будет использован для выделения экземпляра в памяти.
Если какой-либо суперкласс экземпляра должен быть десериализован в несериализуемый и при этом не имеет конструктора по умолчанию, то JVM выдает исключение «NotSerializableException».
Также, прежде чем продолжить реконструкцию объекта, JVM проверяет, совпадает ли SerialVersionUID, упомянутый в потоке байтов, с serialVersionUID класса этого объекта. Если он не совпадает, то выдается исключение 'InvalidClassException'.
3. Пост-инициализация
Итак, до сих пор мы получали экземпляр, расположенный в памяти, используя один из конструкторов по умолчанию суперкласса. Обратите внимание, что после этого ни один конструктор не будет вызван ни для одного класса. После выполнения конструктора суперкласса JVM считывает поток байтов и использует метаданные экземпляра для установки информации о типе и другой метаинформации экземпляра.
После создания пустого экземпляра JVM сначала устанавливает его статические поля, а затем вызывает внутренний метод readObject() по умолчанию [если он не переопределен, в противном случае будет вызван переопределенный метод], который отвечает за установку значений из потока байтов в пустой экземпляр.
Подробнее: Пример кода для readObject() и writeObject()
После завершения метода readObject() процесс десериализации завершен, и вы готовы работать с новым десериализованным экземпляром.