Статьи

Что такое сериализация?

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

Мы будем использовать ниже объект класса Employee в качестве примера для объяснения

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// If we use Serializable interface, static and transient variables do not get serialize
class Employee implements Serializable {
 
    // This serialVersionUID field is necessary for Serializable as well as Externalizable to provide version control,
    // Compiler will provide this field if we do not provide it which might change if we modify the class structure of our class, and we will get InvalidClassException,
    // If we provide value to this field and do not change it, serialization-deserialization will not fail if we change our class structure.
    private static final long serialVersionUID = 2L;
 
    private final String firstName; // Serialization process do not invoke the constructor but it can assign values to final fields
    private transient String middleName; // transient variables will not be serialized, serialised object holds null
    private String lastName;
    private int age;
    private static String department; // static variables will not be serialized, serialised object holds null
 
    public Employee(String firstName, String middleName, String lastName, int age, String department) {
        this.firstName = firstName;
        this.middleName = middleName;
        this.lastName = lastName;
        this.age = age;
        Employee.department = department;
 
        validateAge();
    }
 
    private void validateAge() {
        System.out.println("Validating age.");
 
        if (age < 18 || age > 70) {
            throw new IllegalArgumentException("Not a valid age to create an employee");
        }
    }
 
    @Override
    public String toString() {
        return String.format("Employee {firstName='%s', middleName='%s', lastName='%s', age='%s', department='%s'}", firstName, middleName, lastName, age, department);
    }
 
  // Custom serialization logic,
    // This will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
    private void writeObject(ObjectOutputStream oos) throws IOException {
        System.out.println("Custom serialization logic invoked.");
        oos.defaultWriteObject(); // Calling the default serialization logic
    }
 
    // Custom deserialization logic
    // This will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        System.out.println("Custom deserialization logic invoked.");
 
        ois.defaultReadObject(); // Calling the default deserialization logic
 
        // Age validation is just an example but there might some scenario where we might need to write some custom deserialization logic
        validateAge();
    }
 
}

Что такое сериализация и десериализация

В Java мы создаем несколько объектов, которые соответственно живут и умирают, и каждый объект обязательно умрет, когда JVM умрет, но иногда мы можем захотеть повторно использовать объект между несколькими JVM или нам может потребоваться перенести объект на другой компьютер по сети.

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

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

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

Чтобы сериализовать и десериализовать наш объект в файл, нам нужно вызвать ObjectOutputStream.writeObject() и ObjectInputStream.readObject() как это сделано в следующем коде:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SerializationExample {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Employee empObj = new Employee("Shanti", "Prasad", "Sharma", 25, "IT");
        System.out.println("Object before serialization  => " + empObj.toString());
 
        // Serialization
        serialize(empObj);
 
        // Deserialization
        Employee deserialisedEmpObj = deserialize();
        System.out.println("Object after deserialization => " + deserialisedEmpObj.toString());
    }
 
    // Serialization code
    static void serialize(Employee empObj) throws IOException {
        try (FileOutputStream fos = new FileOutputStream("data.obj");
             ObjectOutputStream oos = new ObjectOutputStream(fos))
        {
            oos.writeObject(empObj);
        }
    }
 
    // Deserialization code
    static Employee deserialize() throws IOException, ClassNotFoundException {
        try (FileInputStream fis = new FileInputStream("data.obj");
             ObjectInputStream ois = new ObjectInputStream(fis))
        {
            return (Employee) ois.readObject();
        }
    }
}

Только классы, которые реализуют Serializable, могут быть сериализованы

Подобно интерфейсу Cloneable для клонирования Java в сериализации, у нас есть один маркерный интерфейс Serializable, который работает как флаг для JVM. Любой класс, который реализует интерфейс Serializable напрямую или через своего родителя, может быть сериализован, а классы, которые не реализуют Serializable не могут быть сериализованы.

Процесс сериализации по умолчанию в Java является полностью рекурсивным, поэтому всякий раз, когда мы пытаемся сериализовать один объект, процесс сериализации пытается сериализовать все поля (примитивные и ссылочные) с нашим классом (кроме static и transient полей).

Когда класс реализует интерфейс Serializable , все его подклассы также сериализуемы. Но когда объект имеет ссылку на другой объект, эти объекты должны реализовывать интерфейс Serializable отдельно. Если в нашем классе есть хотя бы одна ссылка на не Serializable класс, JVM сгенерирует NotSerializableException .

Почему Serializable не реализован в Object?

Теперь возникает вопрос, если Serialization является очень базовой функциональностью и любой класс, который не реализует Serializable не может быть сериализован, то почему Serializable не реализуется самим Object ? Таким образом, все наши объекты могут быть сериализованы по умолчанию.

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

Переходные и статические поля не сериализуются

Если мы хотим сериализовать один объект, но не хотим сериализовать некоторые конкретные поля, мы можем пометить эти поля как
переходный процесс

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

  1. Сериализация не заботится о модификаторах доступа области, таких как private . Все непереходные и нестатические поля считаются частью постоянного состояния объекта и могут быть сериализованы.
  2. Мы можем присваивать значения конечным полям только в обработчиках, и процесс сериализации не вызывает никакого конструктора, но все же может присваивать значения конечным полям.

Что такое serialVersionUID и почему мы должны это объявлять?

Предположим, у нас есть класс, и мы сериализовали его объект в файл на диске, и из-за некоторых новых требований мы добавили / удалили одно поле из нашего класса. Теперь, если мы попытаемся десериализовать уже сериализованный объект, мы получим InvalidClassException , почему?

Мы получаем это, потому что по умолчанию JVM связывает номер версии с каждым сериализуемым классом для управления версиями класса. Он используется для проверки того, что сериализованные и десериализованные объекты имеют одинаковые атрибуты и, следовательно, совместимы с десериализацией. Номер версии поддерживается в поле с именем serialVersionUID . Если сериализуемый класс не объявляет
JVM serialVersionUID будет генерировать один автоматически во время выполнения.

Если мы изменяем нашу структуру класса, например, удаляем / добавляем поля, номер версии также изменяется, и в соответствии с JVM наш класс не совместим с версией класса сериализованного объекта. Вот почему мы получаем исключение, но если вы действительно думаете об этом, почему оно должно быть выброшено только потому, что я добавил поле? Не может ли поле просто установить его значение по умолчанию, а затем записать в следующий раз?

Да, это можно сделать, serialVersionUID поле serialVersionUID вручную и убедитесь, что оно всегда одинаково. Настоятельно рекомендуется, чтобы каждый сериализуемый класс объявлял свой serialVersionUID поскольку сгенерированный класс зависит от компилятора и, следовательно, может привести к неожиданным исключениям InvalidClassExceptions.

Вы можете использовать утилиту из дистрибутива JDK, которая называется
serialver чтобы увидеть, что это за код будет по умолчанию (это просто хеш-код объекта по умолчанию).

Настройка сериализации и десериализации с помощью методов writeObject и readObject

JVM имеет полный контроль над сериализацией объекта в процессе сериализации по умолчанию, но есть много недостатков в использовании процесса сериализации по умолчанию, некоторые из которых:

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

Но мы можем переопределить это поведение сериализации по умолчанию внутри нашего Java-класса и предоставить некоторую дополнительную логику для улучшения нормального процесса. Это можно сделать, предоставив два метода writeObject и readObject внутри класса, который мы хотим сериализовать:

01
02
03
04
05
06
07
08
09
10
11
12
13
// Custom serialization logic will allow us to have additional serialization logic on top of the default one e.g. encrypting object before serialization
private void writeObject(ObjectOutputStream oos) throws IOException {
  // Any Custom logic
 oos.defaultWriteObject(); // Calling the default serialization logic
  // Any Custom logic
}
 
// Custom deserialization logic will allow us to have additional deserialization logic on top of the default one e.g. decrypting object after deserialization
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
 // Any Custom logic
 ois.defaultReadObject(); // Calling the default deserialization logic
  // Any Custom logic
}

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

Хотя эти специализированные частные методы предоставляются, сериализация объектов работает аналогичным образом, вызывая ObjectOutputStream.writeObject() или ObjectInputStream.readObject() .

Вызов ObjectOutputStream.writeObject() или ObjectInputStream.readObject() запускает протокол сериализации. Сначала проверяется объект, чтобы убедиться, что он реализует Serializable а затем проверяется, предоставляется ли какой-либо из этих закрытых методов. Если они предоставляются, класс потока передается в качестве параметра этим методам, предоставляя коду контроль над его использованием.

Мы можем вызвать ObjectOutputStream.defaultWriteObject() и
ObjectInputStream.defaultReadObject() из этих методов для получения логики сериализации по умолчанию. Эти вызовы делают то, на что они похожи – они выполняют запись и чтение сериализованного объекта по умолчанию, что важно, потому что мы не заменяем обычный процесс, мы только добавляем к нему.

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

Остановка сериализации и десериализации

Предположим, у нас есть класс, который получил возможность сериализации от своего родителя, что означает, что наш класс расширяется от другого класса, который реализует Serializable .

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

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

1
2
3
4
5
6
7
private void writeObject(ObjectOutputStream oos) throws IOException {
  throw new NotSerializableException("Serialization is not supported on this object!");
}
 
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
  throw new NotSerializableException("Serialization is not supported on this object!");
}

Однако это является нарушением принципа подстановки Лискова. И
Методы writeReplace и readResolve могут использоваться для достижения одноэлементного поведения. Эти методы используются, чтобы позволить объекту предоставить альтернативное представление для себя в ObjectStream. Проще говоря, readResolve можно использовать для изменения данных, десериализованных с помощью метода readObject, а writeReplace можно использовать для изменения данных, сериализованных с помощью writeObject.

Сериализация Java также может использоваться для глубокого клонирования объекта . Клонирование Java – самая дискуссионная тема в сообществе Java, и оно, безусловно, имеет свои недостатки, но это все еще самый популярный и простой способ создания копии объекта до тех пор, пока этот объект не заполнит обязательные условия клонирования Java. Я подробно рассмотрел клонирование в серии статей о клонировании Java, состоящей из 3 статей, в которую входят такие статьи, как клонирование Java и типы клонирования (мелкое и глубокое). Достаточно , прочитайте их, если хотите узнать больше о клонировании.

Вывод

  1. Сериализация – это процесс сохранения состояния объекта в последовательности байтов, которые затем могут быть сохранены в файле или отправлены по сети, а десериализация – это процесс восстановления объекта из этих байтов.
  2. Только подклассы интерфейса Serializable могут быть сериализованы.
  3. Если наш класс не реализует интерфейс Serializable или имеет ссылку на не Serializable класс, то JVM сгенерирует NotSerializableException .
  4. Все transient и static поля не сериализуются.
  5. serialVersionUID используется для проверки того, что сериализованные и десериализованные объекты имеют одинаковые атрибуты и, следовательно, совместимы с десериализацией.
  6. Мы должны создать поле serialVersionUID в нашем классе, поэтому, если мы изменим нашу структуру класса (добавление / удаление полей), JVM не будет через InvalidClassException . Если мы не предоставляем его, JVM предоставляет тот, который может измениться при изменении структуры нашего класса.
  7. Мы можем переопределить поведение сериализации по умолчанию внутри нашего Java-класса, предоставив реализацию методов writeObject и readObject .
  8. И мы можем вызвать ObjectOutputStream.defaultWriteObject() и ObjectInputStream.defaultReadObject из writeObject и readObject чтобы получить логику сериализации и десериализации по умолчанию.
  9. Мы можем NotSerializableException исключение NotSerializableException из writeObject и readObject , если мы не хотим, чтобы наш класс был сериализован или десериализован.

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

Я также написал серию статей, в которых объясняются номера пунктов с 74 по 78 «Эффективной Java», в которых дополнительно обсуждается, как можно усовершенствовать процесс сериализации Java. Пожалуйста, прочитайте их, если хотите.

Вы можете найти полный исходный код этой статьи в этом Github-репозитории, и, пожалуйста, не стесняйтесь оставить свой ценный отзыв.

Опубликовано на Java Code Geeks с разрешения Нареша Джоши, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Что такое сериализация? Все, что вам нужно знать о сериализации Java, объясненной на примере

Мнения, высказанные участниками Java Code Geeks, являются их собственными.