Рубрики
Без рубрики

Что такое сериализация? Все, Что Вам Нужно Знать О Сериализации Java, Объясняется На Примере

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

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

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

// 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() как это сделано в следующем коде:

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();
        }
    }
}

Сериализуемыми могут быть только классы, реализующие сериализуемость

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

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

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

Почему Сериализуемый не реализуется объектом?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 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() запускает протокол сериализации. Сначала объект проверяется, чтобы убедиться, что он реализует Сериализуемый а затем проверяется, предоставлен ли какой-либо из этих частных методов. Если они предоставлены, класс stream передается в качестве параметра этим методам, предоставляя коду контроль над его использованием.

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

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

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

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

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

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

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

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 могут быть использованы для достижения одноэлементного поведения.   Эти методы используются для того, чтобы объект мог предоставить альтернативное представление для самого себя в потоке объектов. Проще говоря, readResolve можно использовать для изменения данных, которые десериализуются с помощью метода readObject, а writeReplace можно использовать для изменения данных, которые сериализуются с помощью writeObject.

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

Вывод

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

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

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

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

Оригинал: “https://dev.to/njnareshjoshi/what-is-serialization-everything-you-need-to-know-about-java-serialization-explained-with-example-9mj”