Статьи

Все, что вам нужно знать о Java-сериализации

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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. Я покрыл клонирование в деталях в 3 статье длиной Java Клонирование серии , которая включает в себя статью , такой как Java Клонирование и типы Cloning (поверхностная и глубокая) подробно на примере , Java Клонирование — Конструктор копирования Versus Cloning , 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исключение из writeObjectи readObject, если мы не хотим, чтобы наш класс был сериализован или десериализован.

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

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